-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathaquarium.py
executable file
·455 lines (364 loc) · 13.2 KB
/
aquarium.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
#!/usr/bin/env python3
""" aquarium is an html generator
It uses python's special methods (__getattr__, __call__, __enter__ and __exit___) to implement
a framework for easily composing html inside python itself.
Instead of using a string template that embeds python, this embeds the string template into python.
This code
import aquarium
doc = aquarium.Doc()
with doc.html(lang = "en"):
with doc.header():
doc.meta(charset = "utf-8")
with doc.body():
doc.p("Hello world!")
print(doc)
will output a simple valid HTML5 webpage:
<!DOCTYPE html>
<html lang='en'>
<header>
<meta charset='utf-8' />
</header>
<body>
<p>Hello world!</p>
</body>
</html>
Using the with-clause, we can make enclosing tags. Using chaining, we can embed tags.
Inner text is added by simply calling an enclosing tag with an unnamed text argument.
Keyword arguments are converted into tag attributes.
So for example the following code snippet
with doc.section(klass = "navbar").ul():
for i, (name, url) in enumerate(navitems):
doc.li(klass = "active") if i == activenavitem else "").a(name, href = url)
might procuce the following:
<section class='navbar'>
<ul>
<li class=''>
<a href='/'>Home</a>
</li>
<li class='active'>
<a href='/about.html'>About</a>
</li>
<li class=''>
<a href='/contact.html'>Contact</a>
</li>
</ul>
</section>
depending on the contents of navitems and activenavitem.
Aquarium is heavily inspired by airium, a python to HTML and HTML to python generator.
While Aquarium cannot convert HTML back into a valid aquarium script, it does generate HTML
about twice as fast as airium does.
"""
class Text:
""" A simple piece of text, embedded inside an enclosing tag
It is created by either calling a Doc or Tag instance with an unnamed argument.
For example,
doc("hello world!")
creates and adds a "hello world!" Text to the document.
Similarly, doc.p("hello world!") adds a "hello world!" text embedded in a <p>aragraph tag.
An initial Text tag is (mis)used by a Doc as a doctype element.
Text elements are terminal: they cannot have children. This class therefore does not
override the special __call__, __getattr__ or __enter__ / __exit__ methods.
"""
def __init__(self, doc, content: str):
self.doc = doc
self.content = content
# Generate
def text(self, lb, level:int = 0) -> str:
return str(self.content)
# Debug
def struct(self, level:int = 0) -> str:
print(self.doc.indent(level) + f"Text<{self.content}>")
class Tag:
""" A tag element. Tags can be nested. Typically, a document has one root tag.
Tags are created by using the Doc's __getattr__ special method.
A Tag can have tag attributes added by adding keyword arguments to its __call__.
Chaining tags will create a hierarchy, since the Tag class also overrides its __getattr__
special method to create enclosed child tags.
The Tag class also overrides __enter__ and __exit__, so that tags can be used in a with-clause
to create an enclosed section.
"""
def __init__(self, doc, parent, tagname: str):
#print(f"new Tag({doc}, {parent}, {tagname})")
self.doc = doc
self.parent = parent
self._tagname = tagname
self.children = []
self._params = {}
self.deferred = False
self.chain = None
# Compose
def __call__(self, _t = None, *args, **kwargs):
if _t is not None:
self.children.append(Text(self.doc, _t))
else:
for arg in args:
self.children.append(Text(self.doc, arg))
self._params.update(kwargs)
return self
def __getattr__(self, tagname):
tag = Tag(self.doc, self, tagname = tagname)
self.doc.current.children.append(tag)
self.doc.current = tag
# continue the current chain
tag.chain = self
return tag
def __enter__(self):
# start a deferred chain
self.deferred = True
return self
def __exit__(self, *args):
# first unchain any existing chain(s)
while self.doc.current != self:
self.doc.unchain()
assert(self.doc.current == self)
# now unchain this chain
self.doc.unchain()
# Generate
def param(self, key, value):
""" Generates a single tag attribute and its value
"""
return f"{self.doc.attrname(key)}='{value}'"
def params(self):
""" Generates all of the tag attribute/value pairs for this Tag.
"""
params = " ".join([self.param(key, value) for (key, value) in self._params.items()])
return "" if params == "" else " " + params
def opentag(self) -> str:
""" Generates the opening tag (plus attributes) for this Tag
"""
return f"<{self.doc.tagname(self._tagname)}{self.params()}>"
def closetag(self) -> str:
""" Generates the closing tag for this Tag
"""
return f"</{self.doc.tagname(self._tagname)}>"
def openclosetag(self) -> str:
""" Generates an open and closed tag for this Tag
"""
if self.doc.single_tag(self._tagname):
# since the tag can be a single tag, we create a <tag key='value'... /> string
return f"<{self.doc.tagname(self._tagname)}{self.params()} />"
else:
# the tag is not a singletag, so create the typical <tag key='value'...></tag> pair
return self.opentag() + self.closetag()
def text(self, lb, level:int = 0) -> str:
""" Generates the tag, along with its attributes and enclosed inner HTML, at the appropriate indentation level, using the specified linebreak character
"""
indent = self.doc.indent(level)
if len(self.children) == 0:
return indent + self.openclosetag()
elif len(self.children) == 1 and type(self.children[0]) == Text:
# since the tag has only one Text element, we do not use the linebreak character but instead generate a single line,
# eg. <tag key='value'...>text</tag>
return indent + self.opentag() + self.children[0].text(lb, 0) + self.closetag()
else:
return indent + self.opentag() + lb + lb.join([child.text(lb, level + 1) for child in self.children]) + lb + indent + self.closetag()
# Debug
def struct(self, level:int = 0):
print(self.doc.indent(level) + f"<{self.doc.tagname(self._tagname)}{self.params()}>: {'chainstart' if self.chain is None else ''} {'deferred' if self.deferred else ''}")
for child in self.children:
child.struct(level + 1)
class Doc:
""" The Doc class represents a document
It has a number of children, these can be Tags or Texts.
Typically, an html document will have a single Text child (representing the doctype string), followed by a single Tag html child.
The html Tag child will have a head Tag child and a body Tag child.
The Doc maintains a current Tag element, to which additional Tags / Texts are appended.
The Doc stores chained state by initialising each individual chain of tags, and properly escapes and collapses these at the end
of chains and with-clauses.
"""
def __init__(self,
doctype:str = None,
base_indent:str = '\t',
current_level:int = 0,
source_minify:bool = False,
source_line_break_character:str = '\n'
):
""" Creates a new document
The default doctype string is for html documents. If not None, a Text instance with that string is added as a first child.
The document can have an initial indentation level (default is 0), and is by default indented with multiples of the base_indent string.
The generated code uses the source_line_break_character to break lines in the output.
If source_minify is True, no indentation and linebreaking is done.
"""
#print(f"new Doc({doctype})")
# for example, source_minify = True?
self.children = []
self.current = None
if doctype is not None:
self.children.append(Text(self, doctype))
self.base_indent = base_indent
self.current_level = current_level
self.source_minify = source_minify
self.source_line_break_character = source_line_break_character
# Compose
def __call__(self, content:str) -> Text:
""" Creates a new Text instance, as in
doc("hello world!")
"""
text = Text(self, content)
if self.current is None:
self.children.append(text)
else:
self.current.children.append(text)
return text
def __getattr__(self, tagname:str) -> Tag:
""" Creates a new Tag instance, as in
doc.div()
"""
# check, is this attribute in our
#print()
#print(f"__getattr__({tagname})")
# eg doc.html(...)...
# we are creating a new chain. First collapse the old chain
if self.current is not None:
if not self.current.deferred:
# the current chain is not deferred, so unchain it and start a new one
self.unchain()
tag = Tag(self, self.current, tagname = tagname)
if self.current is None:
self.children.append(tag)
else:
self.current.children.append(tag)
self.current = tag
#print(f"new chain at {tag} started")
return tag
# Generate
def text(self) -> str:
""" Outputs the document
This determines the linebreak character to use, and outputs all children starting at the current identation level
"""
lb = "" if self.source_minify else self.source_line_break_character
return lb.join([child.text(lb, self.current_level) for child in self.children])
def __repr__(self) -> str:
return self.text()
# Implementation
def unchain(self):
# we need unchain this chain
while self.current.chain is not None:
self.current = self.current.parent
self.current = self.current.parent
def indent(self, level):
if self.source_minify:
return ""
return self.base_indent * level
tag_substitutes = {}
def tagname(self, name:str) -> str:
return self.tag_substitutes.get(name, name)
attr_substitutes = {}
def attrname(self, name:str) -> str:
return self.attr_substitutes.get(name, name)
single_tags = set([])
def single_tag(self, name:str) -> bool:
return name in self.single_tags
# Debug
def struct(self):
print(f"Doc<>")
for child in self.children:
child.struct()
class Html(Doc):
def __init__(self,
doctype:str = "<!DOCTYPE html>",
base_indent:str = ' ',
current_level:int = 0,
source_minify:bool = False,
source_line_break_character:str = "\n"
):
super(Html, self).__init__(doctype, base_indent, current_level, source_minify, source_line_break_character)
self.tag_substitutes = {
'del_': 'del',
}
self.attr_substitutes = {
'klass': 'class',
'async_': 'async',
'for_': 'for',
'in_': 'in',
# from XML
'xmlns_xlink': 'xmlns:xlink',
# from SVG ns
'fill_opacity': 'fill-opacity',
'stroke_width': 'stroke-width',
'stroke_dasharray': ' stroke-dasharray',
'stroke_opacity': 'stroke-opacity',
'stroke_dashoffset': 'stroke-dashoffset',
'stroke_linejoin': 'stroke-linejoin',
'stroke_linecap': 'stroke-linecap',
'stroke_miterlimit': 'stroke-miterlimit',
# you may add translations to this dict after creating the Airium instance:
# a = Airium()
# a.ATTRIBUTE_NAME_SUBSTITUTES.update({
# # e.g.
# 'clas': 'class',
# 'data_img_url_small': 'data-img_url_small',
# })
}
self.single_tags = set([
# You may change this list after creating the Airium instance by overriding it, like this:
# a = Airium()
# a.SINGLE_TAGS = ['hr', 'br', 'foo', 'ect']
# or by extend or append:
# a.SINGLE_TAGS.extend(['foo', 'ect'])
# You can also change the class itself, ie.
# Airium.SINGLE_TAGS.extend(...)
# This change will persist across all files that use airium in a single project
'input', 'hr', 'br', 'img', 'area', 'link',
'col', 'meta', 'base', 'param', 'wbr',
'keygen', 'source', 'track', 'embed',
])
class Rss(Doc):
def __init__(self,
doctype:str = "<?xml version='1.0' encoding='UTF-8'?>",
base_indent:str = ' ',
current_level:int = 0,
source_minify:bool = False,
source_line_break_character:str = "\n"
):
super(Rss, self).__init__(doctype, base_indent, current_level, source_minify, source_line_break_character)
self.tag_substitutes.update({
'_tags__taglist': 'tags:taglist',
'_tags__tag': 'tags:tag',
})
self.attr_substitutes.update({
'_xmlns__tags': 'xmlns:tags',
'_xmlns__conversation': 'xmlns:conversation',
})
if __name__ == "__main__":
doc = Html()
with doc.body():
with doc.main():
with doc.nav():
with doc.section(klass = "titlesection"):
with doc.a(href = "/"):
doc.div(klass = "titleimage").img(id = "titleimage", src = "/images/title.png")
with doc.div(klass = "sitenavigation"):
#with doc.span("home").a(os.path.join("/", self.config.tgtsubdir)):
# doc("Home")
with doc.ul(klass = "links"):
doc.li().a(href = "", _t = "Home")
doc.li().a(href = "blog", _t = "Blog")
doc.li().a(href = "projects", _t = "Projects")
doc.li().a(href = "sketches", _t = "Sketches")
doc.li().a(href = "articles", _t = "Articles")
doc.li().a(href = "contact.html", _t = "Contact")
doc.li().a(href = "about.html", _t = "About")
with doc.article():
doc.p("The article...")
# with doc.html(lang="en"):
# with doc.head():
# pass
# with doc.body():
# doc.p("head")
# with doc.a(href="url"):
# doc.p("hello world")
# doc.p("foot")
# with doc.body():
# doc.p(_t = "hello world")
# doc.a(href="http://blah").h1(_t = "blah")
# doc.p(_t = "footer")
print(doc.struct())
print(doc.text())
doc = Doc()
navitems = [("Home", "/"), ("About", "/about.html"), ("Contact", "/contact.html")]
activenavitem = 1
with doc.section(klass = "navbar").ul():
for i, (name, url) in enumerate(navitems):
doc.li(klass = "active" if i == activenavitem else "").a(name, href = url)
print(doc)