Last active
May 25, 2018 03:13
-
-
Save carlsmith/996534c507c1278087f7e19efcc87465 to your computer and use it in GitHub Desktop.
[ALPHA] Hypertext Markup Engine | Taking another pop at generating HTML5 documents in Python 2
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from htme import * | |
subpages = { | |
"unknown": { | |
"type": 'Unknown User', | |
"body": 'Your <a href="{URL}">Google Account</a> is unknown.' | |
}, | |
"denial": { | |
"type": 'Unauthorized User', | |
"body": 'Your rank is too low to access this part of the app.' | |
}, | |
"login": { | |
"type": 'Anonymous User', | |
"body": 'You must <a href="{URL}">login</a> to your Google Account.' | |
}, | |
"deadend": { | |
"type": 'File Not Found', | |
"body": 'This is a deadend. Go to the <a href=/>homepage</a>?' | |
} | |
} | |
description = "A subpage for `{[type]}` errors." | |
docs = Engine(favicon="/static/favicon.png") | |
docs.install(Style("/static/subpage.css")) | |
error_span, message_paragraph = SPAN(), P() | |
docs *= HEADER(Anchor("/", "Write Dot Run")) | |
docs *= MAIN(H1("ERROR: ", error_span), message_paragraph) | |
docs *= FOOTER( | |
DIV(Anchor("/terms", "Terms of Use")), SPAN("|"), | |
DIV(Anchor("/privacy", "Privacy Policy")) | |
) | |
for key, subpage in subpages.items(): | |
docs.title = subpage["type"] | |
docs.description = description.format(subpage) | |
error_span /= subpage["type"] | |
message_paragraph /= subpage["body"] | |
docs.freeze(key) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
HTME | The Hypertext Markup Engine | |
================================== | |
HTME is a simple and Pythonic domain specific language that makes it easy to | |
express DOM fragments and full HTML5 documents (with full support for inline | |
SVG and MathML) compatible with any modern version of Python 2 or 3. | |
## The Inheritance Hierarchy | |
The library defines an abstract base class named `Element` that the standard | |
element classes (`VoidElement`, `RawTextElement`, `EscapableRawTextElement` | |
and `NormalElement`) inherit from (they are also ABCs). | |
### Standard Element Classes | |
All of the standard HTML5, SVG and MathML elements are implemented as a set | |
of concrete classes, all spelt in uppercase (`DIV`, `P`, `SPAN` etc). | |
### Magic Element Classes | |
Most elements are instances of the standard element classes, but there are | |
also magic elements (like `Logic` and `Style`) that inherit from the standard | |
element classes. | |
## DOM Serialisation | |
> There should be one-- and preferably only one --obvious way to do it. | |
> The Zen of Python --- Tim Peters | |
While they probably could (it's Python), HTME does not aim to allow users to | |
create totally arbitrary HTML. The idea is to allow users to express the DOM | |
of any valid HTML5 document. Users build something closer to a HTML5 AST. | |
### Determinism | |
HTME elements and engines are always serialised into HTML5 according to a set | |
of simple, deterministic rules that make it easy to always know exactly what | |
the output will be. | |
### One Syntax to Rule Them All | |
As well as being deterministic, the subset of HTML5 syntax we use also allows | |
HTME to support inline SVG and MathML without having to differentiate between | |
HTML and XML elements, at all. Users and library authors use exactly the same | |
base classes (along with their signatures, operators and methods), and all of | |
the output uses the exact same syntax and style, whether it is HTML, SVG or | |
MathML. | |
HTML and XML are different, but the HTML5 spec requires vendors to parse any | |
inline XML a little differently, so they can be written the same way. | |
### Tag Names | |
The W3C HTML5 Syntax Specification is clear that tag names, *even those for | |
foreign elements*, may be written with any mix of lowercase and uppercase | |
letters. | |
While XML is case-sensitive, in HTML5, *inline* SVG and MathML are not. They | |
are not strictly speaking *XML*. | |
HTME outputs all HTML5, SVG and MathML tag names in lowercase. | |
### Attributes | |
Attribute names are always spelled in lowercase. | |
Each attribute is delimited from the tag name and any other attributes by a | |
single space. | |
HTME wraps all attribute values in doublequotes, and automatically escapes | |
any doublequotes, open angle brackets and ampersands in the value. Nothing | |
else is ever escaped by the library. | |
### Closing Slashes | |
All void elements use a closing slash. The HTML5 spec allows closing slashes | |
on void elements (though it is redundant). Inline XML requires them. | |
https://dev.w3.org/html5/html-author/#self-closing-tag | |
The output does not use a space before the closing slash at the end of void | |
elements. See: | |
https://stackoverflow.com/questions/462741/space-before-closing-slash | |
### Whitespace | |
HTME never introduces insignificant whitespace. Unless the user includes a | |
newline in a text node, the output is a single line of HTML. | |
## Merging Namespaces | |
This approach also solves the namespace problem. HTML and SVG both define A, | |
SCRIPT and STYLE elements, but they are indistinct in HTME. There is no need | |
to treat them differently. | |
### Limitations | |
HTME is not able to generate legit, standalone SVG files. That is beyond the | |
scope of the library. The focus is only HTML5 with inline SVG and MathML. | |
### Summary of DOM Serialisation | |
HTME provides all of its HTML5, SVG and MathML elements as a single collection | |
of elements that have one API and generate output using the same, predictable | |
syntax. | |
We hope devs will be encouraged to create Python routines that generate cool | |
vector graphics (icons, borders, text effects etc) for pages and apps. These | |
routines can be wrapped in magic elements, so it is easy to generate floral | |
dividers, space invader background patterns and pacman text animations.""" | |
from __future__ import print_function, unicode_literals | |
from types import GeneratorType | |
# The ASCII Global Constants... | |
empty, tab, newline, space, underscore, quote = "", "\t", "\n", " ", "_", "'" | |
bar, colon, bang, ask, pound, dollar = "|", ":", "!", "?", "#", "$" | |
dot, comma, plus, minus, ampersand, at = ".", ",", "+", "-", "&", "@" | |
modulo, caret, asterisk, equals, semicolon = "%", "^", "*", "=", ";" | |
slash, backslash, backtick, tilde, doublequote = "/", "\\", "`", "~", '"' | |
openparen, closedparen, openbracket, closedbracket = "(", ")", "[", "]" | |
openbrace, closedbrace, openangle, closedangle = "{", "}", "<", ">" | |
# The Generic Helper Functions... | |
def read(path): # TODO: look into `io.open` for unicode (bipy) support | |
"""This simple helper function wraps `file.read` with an idiomatic with | |
statement, so the body of a file can be read easily.""" | |
with open(path, "r") as file: return file.read() | |
def readlines(path): | |
"""This simple helper function wraps `file.readlines` with an idiomatic | |
with statement, so the lines in the body of a file can be read easily.""" | |
with open(path, "r") as file: return file.readlines() | |
def write(content, path): | |
"""This simple helper function wraps `file.write` with an idiomatic with | |
statement, so files can be written to (and created if they do not exist) | |
easily. It takes a path or filename (`path`) followed by the object that | |
is being written to the file (`content`).""" | |
with open(path, "w+") as file: file.write(str(content)) | |
def flatten(structure, terminal=None): | |
"""This function flattens a nested sequence into an instance of the | |
`Children` class, which is derived from (and a pure superset of) the | |
`list` class. | |
This function is important to the internal API, where it is used to | |
flatten every child array passed to a constructor or operator. Users | |
do not normally need this function, but they are free to. | |
The first arg (`structure`) is required, and is the object that needs | |
flattening. The second arg (`terminal`) is optional. If provided, it | |
is used instead of the default function that determines if an item is | |
terminal or not. It defaults to a function that treats lists, tuples | |
and any of their derived classes (including `Children`) as recursive, | |
and all other types of object (except `generator`) as terminal: | |
>>> flatten([0, [], 1, 2, (3), (4, 5, [6]), 7, ((())), [[], 8], [[9]]]) | |
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] | |
Generator arguments are exhausted automatically, essentially treating | |
them as tuples: | |
>>> flatten(n * 2 for n in (1, 2, 3)) | |
[2, 4, 6] | |
Aside: Other Python Hypertext DSLs use flattening as a core concept. In | |
HTME, flattening is a non-concept; it just happens automatically. HTME | |
allows users to pass nested families to the element constructors and | |
operators, but this is not a central feature or concept.""" | |
results = Children() # the list of results that will be returned | |
# Set the function that determines if an item is terminal or non-terminal: | |
terminal = terminal or (lambda item: not isinstance(item, (list, tuple))) | |
for item in structure: | |
# Prevent dicts from being accidently provided as children: | |
if isinstance(item, dict): raise ValueError("dicts cannot be children") | |
# Invoke element classes to convert them to element instances: | |
if type(item) is type and issubclass(item, Element): item = item() | |
# Convert generators to tuples (so generator expressions work as args): | |
elif isinstance(item, GeneratorType): item = tuple(item) | |
# Append a terminal or recursively flattened non-terminal to `results`: | |
results += [item] if terminal(item) else flatten(item) | |
return results | |
def cat(sequence, seperator=""): | |
"""This function takes a sequence as a required arg (`sequence`). Each | |
item in the sequence is converted to a string and concatenated together | |
using the optional second arg (`seperator=""`). The result is returned: | |
>>> print(cat(["<", "spam", ">"])) | |
<spam> | |
>>> print(cat(("spam", "eggs", "spam and eggs"), space)) | |
spam eggs spam and eggs | |
This function is used extensively throughout the library, just because | |
it is often a bit prettier than using `join` directly, especially with | |
the default seperator.""" | |
return seperator.join(str(item) for item in sequence) | |
def ext(path, length=1): | |
"""This helper takes a required arg (`path`), which should be a path | |
or filename string. By default, this function returns everything after | |
the last dot in the path. It is useful for getting an extension from | |
a filename: | |
>>> print(ext("/static/jquery.min.js")) | |
js | |
The second arg (`length`) defaults to `1`. If provided, it must be a | |
positive integer that defines the number of subextensions to include | |
(which will include the dots between the subextensions): | |
>>> print(ext("/static/jquery.min.js", 2)) | |
min.js | |
""" | |
return cat(path.split(dot)[-length:], dot) | |
# The Core Library Classes... | |
class Children(list): | |
"""This class is derived from `list`. Slice operations on an element's | |
children evaluate to an instance of this class. Using a standard `list` | |
would not work with mutation operations on slices, for example: | |
element[1:3] **= {"class": "selected"} | |
That code would not work if `element[1:3]` returned a list or tuple | |
as those types do not understand the `**=` operator.""" | |
@property | |
def elements(self): | |
"""This property returns an iterable of the children in the list | |
that are instances of `Element`: | |
>>> children = DIV(P("ElementA"), "ElementB", P("ElementC"))[:] | |
>>> for element in children.elements: print(element) | |
<p>ElementA</p> | |
<p>ElementC</p> | |
""" | |
return ( child for child in self if isinstance(child, Element) ) | |
@property | |
def normal_elements(self): | |
"""This property returns an iterable of the children in the list | |
that are instances of `NormalElement`. | |
>>> children = DIV(IMG({"src": "img.png"}), "Text", P("Open"))[:] | |
>>> for element in children.normal_elements: print(element) | |
<p>Open</p> | |
""" | |
return ( child for child in self if isinstance(child, NormalElement) ) | |
def __ipow__(self, attributes): | |
"""This method implements the `**=` operator for `Children` that | |
only operates on children that are instance of `Element`. | |
>>> div = DIV(P("ALI"), P("BOB"), P("CAZ")) | |
>>> div[1:] **= {"class": "foo"} | |
>>> div | |
<div><p>ALI</p><p class="foo">BOB</p><p class="foo">CAZ</p></div> | |
""" | |
for child in self.elements: child **= attributes | |
def __ifloordiv__(self, attributes): | |
"""This method implements the `//=` operator for `Children` that | |
only operates on children that are instance of `Element`. | |
>>> div = DIV(P("ALI"), P({"class": "foo"}, "BOB"), P("CAZ")) | |
>>> div[1:] //= {"class": "bar"} | |
>>> div | |
<div><p>ALI</p><p class="bar">BOB</p><p class="bar">CAZ</p></div> | |
""" | |
for child in self.elements: child //= attributes | |
def __imul__(self, families): | |
"""This method implements the `*=` operator for `Children` that | |
only operates on children that are instance of `NormalElement`. | |
>>> div = DIV(P("ALI"), P("BOB")) | |
>>> div[:] *= SPAN("SUB") | |
>>> div | |
<div><p>ALI<span>SUB</span></p><p>BOB<span>SUB</span></p></div> | |
""" | |
for child in self.normal_elements: child *= families | |
def __idiv__(self, families): | |
"""This method implements the `/=` operator for `Children` that | |
only operates on children that are instance of `NormalElement`. | |
>>> div = DIV(P("ALI"), P("BOB")) | |
>>> div[:] /= SPAN("SUB") | |
>>> div | |
<div><p><span>SUB</span></p><p><span>SUB</span></p></div> | |
""" | |
for child in self.normal_elements: child /= families | |
def __getitem__(self, arg): | |
"""This method overrides `list.__getitem__` to ensure slices of | |
slices recursively evaluate to `Children` (not `list`), which | |
depends on this method in Python 3. | |
>>> DIV(P("ALI"), P("BOB"), P("CAZ"))[:][1] | |
<p>BOB</p> | |
Note: Python invokes different methods to handle slice operations, | |
depending on whether it is Python 2 or 3.""" | |
result = super(Children, self).__getitem__(arg) | |
return Children(result) if type(result) is list else result | |
def __setitem__(self, *args): | |
"""This method ensures slices of slices recursively evaluate to | |
`Children` (not `list`) in Python 3. | |
>>> children = DIV(P("ALI"), P("BOB"), P("CAZ"))[:-1] | |
>>> children[1] = ASIDE("REPLACEMENT") | |
>>> children | |
[<p>ALI</p>, <aside>REPLACEMENT</aside>] | |
>>> SECTION(children) | |
<section><p>ALI</p><aside>REPLACEMENT</aside></section> | |
Note: Python invokes different methods to handle slice operations, | |
depending on whether it is Python 2 or 3.""" | |
if type(args[0]) is slice: return self[args[0]] | |
else: super(Children, self).__setitem__(*args) | |
def __getslice__(self, start, end=None): | |
"""This method ensures slices of slices recursively evaluate to | |
`Children` (not `list`) in Python 2. | |
>>> div = DIV(P("ALI"), P("BOB")) | |
>>> type(div[:]).__name__ | |
'Children' | |
>>> type(div[:][:]).__name__ | |
'Children' | |
Note: Python invokes different methods to handle slice operations, | |
depending on whether it is Python 2 or 3.""" | |
return Children(self[slice(start, end)]) | |
def __setslice__(self, start, end, other): | |
"""This method ensures slices of slices recursively evaluate to | |
`Children` (not `list`) in Python 2: | |
>>> div = DIV(P("ALI"), P({"class": "foo"}, "BOB"), P("CAZ")) | |
>>> div[1:][:] **= {"class": "bar"} | |
>>> div | |
<div><p>ALI</p><p class="bar">BOB</p><p class="bar">CAZ</p></div> | |
>>> div = DIV(P("ALI"), P("BOB"), P("CAZ")) | |
>>> div[1:][:] /= "REPLACEMENT" | |
>>> div | |
<div><p>ALI</p><p>REPLACEMENT</p><p>REPLACEMENT</p></div> | |
Note: Python invokes different methods to handle slice operations, | |
depending on whether it is Python 2 or 3.""" | |
return self[start:end] | |
__itruediv__ = __idiv__ | |
def blit(self, index, other): | |
"""This method takes an index and a child or a tuple of them (taken | |
from an overloaded operator). It swaps the item that is currently at | |
the index with the child elements. | |
>>> div = DIV(P("A"), P("B"), P("C")) | |
>>> div.children.blit(0, SPAN("X")) | |
>>> div | |
<div><span>X</span><p>B</p><p>C</p></div> | |
>>> div.children.blit(1, [SPAN("Y"), SPAN("Z")]) | |
>>> div | |
<div><span>X</span><span>Y</span><span>Z</span><p>C</p></div> | |
""" | |
# Flatten the values, then reverse them, so they can be iteratively | |
# blitted into the array and end up back in the same order that they | |
# were expressed in (`other` is wrapped in a list because it is often | |
# a single node (`flatten` expects a sequence): | |
values = flatten([other]) | |
values.reverse() | |
# Remove the single item being blitted over, then insert each of the | |
# new items, so they rack up in the right order: | |
del self[index] | |
for value in values: self.insert(index, value) | |
class Element(object): | |
"""This is an abstract base class for all elements. Also see the derived | |
classes: `VoidElement`, `RawTextElement`, `EscapableRawTextElement` and | |
`NormalElement`.""" | |
def __init__(self, attributes=None): | |
"""This method computes the name of the element, based on the name of | |
the (derived) class, by converting it to lowercase and replacing any | |
underscores with hyphens. The only arg (`attributes`) is optional, | |
and should be a dict (the default is empty). It sets the initial | |
HTML attributes for the element: | |
>>> IMG({"src": "img.png"}) | |
<img src="img.png"/> | |
""" | |
self.attributes = {} if attributes is None else attributes | |
self.freezer = {} | |
def __repr__(self): | |
"""This method makes the representation of elements use their HTML | |
representation.""" | |
attributes = self.render_attributes() | |
return "<{0}{1}/>".format(self.name, attributes) | |
def __call__(self, key, *args, **kargs): | |
"""This method makes it possible to access and format frozen elements | |
by invoking the instance, passing in the key for the frozen element. | |
Users can also pass args and keyword args, which just get passed on | |
to the `str.format` method, which is called on the frozen string | |
before it is returned: | |
>>> img = IMG({"src": "{IMG}.png"}) | |
>>> img.freeze("foo") | |
>>> print(img("foo", IMG="mugshot")) | |
<img src="mugshot.png"/> | |
""" | |
return self.freezer[key].format(*args, **kargs) | |
def __ipow__(self, other): | |
"""This method allows the `**=` operator to be used to merge a dict | |
of attributes with the element attributes: | |
>>> img = IMG() | |
>>> img **= {"src": "img.png"} | |
>>> img | |
<img src="img.png"/> | |
Note: This method complements `NormalElement.__imul__`, which provides | |
similar functionality for appending children.""" | |
self.attributes.update(other) | |
return self | |
def __ifloordiv__(self, other): | |
"""This method allows the `//=` operator to be used to assign a new | |
dict of attributes to `self.attributes`: | |
>>> img = IMG({"src": "img.png"}) | |
>>> img //= {"src": "mugshot.png", "class": "selected"} | |
>>> img | |
<img class="selected" src="mugshot.png"/> | |
Note: This method mutates the attributes dict in place. | |
Note: This method complements `NormalElement.__idiv__`, which provides | |
similar functionality for replacing children.""" | |
self.attributes.clear() | |
self.attributes.update(other) | |
return self | |
def __getitem__(self, name): | |
"""This method allows the square brackets suffix operator to be used | |
to access element attributes by name: | |
>>> img = IMG({"src": "img.png"}) | |
>>> print(img["src"]) | |
img.png | |
Note: This method complements `NormalElement.__getitem__`, which also | |
supports accessing children.""" | |
return self.attributes[name] | |
def __setitem__(self, name, other): | |
"""This method complements the `Element.__getitem__` method, allowing | |
single attributes to be updated: | |
>>> img = IMG({"src": "img.png"}) | |
>>> img["src"] = "mugshot.png" | |
>>> print(img["src"]) | |
mugshot.png | |
Note: The `**=` and `//=` operators can update multiple attributes in | |
a single invocation.""" | |
self.attributes[name] = other | |
return self | |
def __eq__(self, other): | |
"""This method checks whether two elements are equal by checking that | |
they both evaluate to the same string: | |
>>> x = IMG({"src": "img.png", "class": "selected"}) | |
>>> y = IMG({"class": "selected", "src": "img.png"}) | |
>>> x == y | |
True | |
""" | |
return repr(self) == repr(other) | |
def __ne__(self, other): | |
"""This method checks that two elements are not equal by checking that | |
they both evaluate to different strings: | |
>>> x = IMG({"src": "img.png", "class": "selected"}) | |
>>> y = IMG({"class": "selected", "src": "img.png"}) | |
>>> x != y | |
False | |
""" | |
return repr(self) != repr(other) | |
def __len__(self): | |
"""This method complements `NormalElement.__len__`, but it only ever | |
returns zero as `VoidElement` instances have no children. | |
>>> len(IMG({"src": "img.png"})) == 0 | |
True | |
""" | |
return 0 | |
@property | |
def name(self): | |
"""This computed property returns the tag name, which is computed | |
from the class name by converting it to lowercase, then replacing | |
each underscore with a hyphen. It is computed on demand, so that | |
an element instance can have its class reassigned: | |
>>> element = LINK() | |
>>> element **= {"src": "img.png"} | |
>>> element.__class__ = IMG | |
>>> element | |
<img src="img.png"/> | |
The method iterates over the method resolution order, looking for | |
the first class name that does not change when it is converted to | |
uppercase. This allows element subclasses to inherit their names | |
from a standard element base class (subclasses always include at | |
least one lowercase character in their names): | |
>>> class Foo(DIV): pass | |
>>> Foo() | |
<div></div> | |
Note: If a name starts with an underscore, that character is treated | |
as though it is not there (this feature is used internally): | |
>>> class _FAKE(VoidElement): pass | |
>>> _FAKE() | |
<fake/> | |
>>> class _FAKE(NormalElement): pass | |
>>> _FAKE() | |
<fake></fake> | |
""" | |
for Class in self.__class__.__mro__: | |
name = Class.__name__ | |
if name.upper() != name: continue | |
if name[0] == underscore: name = name[1:] | |
return name.lower().replace(underscore, minus) | |
def render_attributes(self): | |
"""This helper method returns the HTML representation of the | |
element's attributes dict, as the attributes would be appear | |
in an opening tag. | |
This method is generally only used internally, though users have | |
access to it if they want to use it. | |
Attribute values are wrapped in double quotes, and ampersands, open | |
angle brackets and doublequotes are automatically escaped. | |
>>> div = DIV({"foo": "spam & eggs", "bar": '<>"'}) | |
>>> print(div.render_attributes().strip()) | |
bar="<>"" foo="spam & eggs" | |
If an attribute has an empty string as its value, the method | |
outputs it as a boolean attribute. | |
>>> DIV({"contenteditable": ""}) | |
<div contenteditable></div> | |
If an attribute's value is `None`, the attribute is not rendered | |
at all: | |
>>> DIV({"foo": None, "bar": "include"}) | |
<div bar="include"></div> | |
Note: Attribute names are rendered in alphabetical order: | |
>>> DIV({"aa": "0", "bb": "3", "ab": "1", "ba": "2"}) | |
<div aa="0" ab="1" ba="2" bb="3"></div> | |
Rendering attributes that way makes the entire rendering process | |
deterministic. This allows the equality operators to compare the | |
HTML two elements render to (or compare an element to a string). | |
Note: Attribute values are converted to strings (with `str`) in | |
the output: | |
>>> div = DIV({"aa": 0, "bb": 3, "ab": 1, "ba": 2}) | |
>>> div["aa"], div | |
(0, <div aa="0" ab="1" ba="2" bb="3"></div>) | |
If an attribute's value is a list or tuple (or any subclass), then | |
each of the values in the sequence are joined with spaces to form | |
the output (subvalues are converted to strings in the process): | |
>>> attributes = { | |
... "viewbox": (1, 1, 100, 100), | |
... "xmlns": "http://www.w3.org/2000/svg" | |
... } | |
>>> SVG(attributes) | |
<svg viewbox="1 1 100 100" xmlns="http://www.w3.org/2000/svg"></svg> | |
Note: Attribute names that start with a dollar (`$`) are using HTME | |
shorthand for *data attributes*. The dollar will be automatically | |
replaced in the rendered output with `data-`: | |
>>> DIV({"$count": 10}) | |
<div data-count="10"></div> | |
""" | |
if not self.attributes: return "" | |
results = [] | |
escapees = {'<': '<', '&': '&', '"': '"'} | |
# Sort the keys so attributes are rendered in alphabetical order: | |
keys = list(self.attributes.keys()) | |
keys.sort() | |
for key in keys: | |
value = self.attributes[key] | |
# grab the value, and expand the key if it is a data attribute: | |
if key.startswith(dollar): key = "data-{}".format(key[1:]) | |
# join the items in lists and tuples with space seperators | |
if isinstance(value, (list, tuple)): value = cat(value, space) | |
# Push the attribute to `results` as a string of html syntax: | |
if value is None: continue # ignore null attributes | |
elif value == "": results.append(key) # fix boolean attributes | |
else: | |
value = cat(escapees.get(char, char) for char in str(value)) | |
results.append('{}="{}"'.format(key, value)) | |
# Create a string that can be directly concatenated to the tag name: | |
return space + cat(results, space) if results else "" | |
def freeze(self, key): | |
"""This method freezes the current state of the element, turning it | |
into a string of HTML. The one required arg is the key used to store | |
the frozen element in `self.freezer`. | |
>>> key = "image element" | |
>>> img = IMG({"src": "{IMG}"}) | |
>>> img.freeze(key) | |
>>> print(img(key, IMG="img.png")) | |
<img src="img.png"/> | |
""" | |
self.freezer[key] = str(self) | |
def write(self, path): | |
"""This method renders the element, writes it to the given path, | |
then returns `self`, so it can be printed or method chained.""" | |
write(str(self), path) | |
return self | |
class VoidElement(Element): | |
"""This is an abstract base class for Void Elements (those that cannot | |
have children). It just renames `Element` which already implements all | |
of an element's core functionality.""" | |
class RawTextElement(Element): | |
"""This class implements Raw Text Elements (TITLE, STYLE, SCRIPT and | |
TEXTAREA). Instances of those subclasses do not have children, though | |
they do have a single string (the `content` attribute) that is like a | |
single child. This class does not implement the child operators. The | |
constructor implements a variant of the standard signature that can | |
only takes one optional child, which must be a text node: | |
>>> SCRIPT({"id": "foo"}, "let square = x => x * x") | |
<script id="foo">let square = x => x * x</script> | |
>>> SCRIPT(u"let square = x => x * x") | |
<script>let square = x => x * x</script> | |
>>> SCRIPT({"src": "logic.js"}) | |
<script src="logic.js"></script> | |
>>> STYLE() | |
<style></style> | |
>>> STYLE("body { margin: 0 }", "p { color: red }") | |
Traceback (most recent call last): | |
... | |
ValueError: raw text elements only contain 1 child, not 2 | |
>>> STYLE(P("")) | |
Traceback (most recent call last): | |
... | |
TypeError: the content of raw text elements must be a text node, not P | |
""" | |
def __init__(self, *args): | |
"""This method takes an attributes dict or a content string or an | |
attributes dict, followed by a content string. Both values default | |
to empty.""" | |
if not args: self.attributes, self.content = {}, "" | |
else: | |
args = list(args) # tuples do not support `pop` (see below) | |
# pop any attributes dict so `args` is falsey if we are done | |
self.attributes = args.pop(0) if isinstance(args[0], dict) else {} | |
# pop any content argument, so `args` is falsey if we are done | |
self.content = args.pop(0) if args else "" | |
if args: # if more than one arg follows the optional first arg... | |
message = "raw text elements only contain 1 child, not {}" | |
raise ValueError(message.format(len(args) + 1)) | |
def __repr__(self): | |
"""This method makes raw text elements render as HTML. It checks that | |
the `content` is not an element, before passing it to `str.format`, | |
and raises a type error if it is.""" | |
if isinstance(self.content, Element): | |
message = "the content of raw text elements must be a text node" | |
message += ", not " + self.content.__class__.__name__ | |
raise TypeError(message) | |
attributes = self.render_attributes() | |
return "<{0}{1}>{2}</{0}>".format(self.name, attributes, self.content) | |
class EscapableRawTextElement(RawTextElement): | |
"""This class implements Escapable Raw Text Elements, but as HTME does | |
not escape anything itself, the implementation is the same as for Raw | |
Text Elements. It only exists so that TITLE and TEXTAREA elements | |
have the correct type.""" | |
class NormalElement(Element): | |
"""This is an abstract base class that extends `Element` with support | |
for elements that have children.""" | |
def __init__(self, *args): | |
"""This method adds support for children, which `Element`s do not | |
support. See the `argparse` method for notes on the signature this | |
method uses: | |
>>> print(UL({"class": "nav"}, LI("Coffee"), LI("Tea"), LI("Milk"))) | |
<ul class="nav"><li>Coffee</li><li>Tea</li><li>Milk</li></ul> | |
""" | |
attributes, self.children = self.signature(args) | |
super(NormalElement, self).__init__(attributes) | |
def __repr__(self): | |
"""This method makes the representation of elements use their HTML | |
representation.""" | |
children = self.render_children() | |
attributes = self.render_attributes() | |
return "<{0}{1}>{2}</{0}>".format(self.name, attributes, children) | |
def __imul__(self, *families): | |
"""This method allows families to be appended to `self.children`, | |
using the `*=` operator: | |
>>> ul = UL({"class": "nav"}) | |
>>> ul *= LI("Coffee"), LI("Tea"), LI("Milk") | |
>>> print(ul) | |
<ul class="nav"><li>Coffee</li><li>Tea</li><li>Milk</li></ul> | |
Note: `Document.__imul__` operates the same way, operating on the | |
`Document.tree` element.""" | |
self.children += flatten(families) | |
return self | |
def __idiv__(self, *families): | |
"""This method allows families to be assigned to `self.children`, | |
replacing any existing children, using the `/=` operator: | |
>>> ul = UL(LI("Coffee"), LI("Tea")) | |
>>> ul /= LI("Milk") | |
>>> print(ul) | |
<ul><li>Milk</li></ul> | |
Note: This method mutates the list of children in place. | |
Note: The `NormalElement.__itruediv__` method is aliased to this one, | |
as Python 3 renamed it and changed its semantics (the differences | |
do not affect the operator's HTME semantics). | |
Note: `Document.__idiv__` maps this method to the tree. """ | |
# This is slightly convoluted, but slices of `Children` are instances | |
# of `Children` too, recursively, so we have to juggle things a bit | |
# to replace all the children (mutating `self.children` in place): | |
del self.children[:] | |
self.children += flatten(families) | |
return self | |
def __getitem__(self, arg): | |
"""This method allows the square brackets suffix operator to be used | |
to access element attributes by name and children by index: | |
>>> ul = UL({"class": "nav"}, LI("Coffee"), LI("Tea"), LI("Milk")) | |
>>> print(ul["class"]) | |
nav | |
>>> print(ul[1]) | |
<li>Tea</li> | |
>>> print(ul[1][0]) | |
Tea | |
The method also supports slicing an element into an instance of | |
`Children` containing the children expressed by the slice. | |
>>> print(UL(LI("Coffee"), LI("Tea"), LI("Milk"))[1:]) | |
[<li>Tea</li>, <li>Milk</li>] | |
Note: More doctests for slices are implemented within `Children`.""" | |
# If the arg is an int, return the child at that index. If the arg is | |
# a slice, return an instance of `Children` containing the contents | |
# of the slice. Otherwise, treat the arg as an attributes dict key, | |
# and return its value: | |
if isinstance(arg, int): return self.children[arg] | |
elif isinstance(arg, slice): return Children(self.children[arg]) | |
else: return self.attributes[arg] | |
def __setitem__(self, arg, other): | |
"""This method complements the `NormalElement.__getitem__` method, | |
handling assignments to keys, indexes and slices. | |
>>> ul = UL({"class": "nav"}, LI("Coffee"), LI("Tea"), LI("Milk")) | |
>>> ul["class"] = "buttons" | |
>>> ul[1] = P("replacement") | |
>>> print(ul) | |
<ul class="buttons"><li>Coffee</li><p>replacement</p><li>Milk</li></ul> | |
>>> ul = UL(DIV("content")) | |
>>> ul[0] = LI(), LI(), LI() | |
>>> print(ul) | |
<ul><li></li><li></li><li></li></ul> | |
Note: Most doctests for slices are implemented within `Children`.""" | |
# This makes the same distinction as `__getitem__`, but assigns to | |
# the index, attribute or list of `Children` expressed by a slice: | |
if isinstance(arg, int): self.children.blit(arg, other) | |
elif isinstance(arg, slice): return Children(self.children[arg]) | |
else: self.attributes[arg] = other | |
def __len__(self): | |
"""This method implements `len(element)`, returning the total number | |
of children the element has: | |
>>> ul = UL({"class": "nav"}, LI("Coffee"), LI("Tea"), LI("Milk")) | |
>>> len(ul) == 3 | |
True | |
""" | |
return len(self.children) | |
def __iter__(self): | |
"""This method allows iteration over an open element's children, | |
doing something like `child for child in element`: | |
>>> ul = UL(LI("Coffee"), LI("Tea"), LI("Milk")) | |
>>> for li in ul: print(li) | |
<li>Coffee</li> | |
<li>Tea</li> | |
<li>Milk</li> | |
""" | |
for child in self.children: yield child | |
__itruediv__ = __idiv__ | |
@staticmethod | |
def signature(args): | |
"""This static method implements the standard element signature, so | |
other classes can use it in their own way. It returns a two-tuple: | |
(attributes, children). | |
The Standard Signature takes an optional attributes dict, followed by | |
zero or more families (see `flatten`).""" | |
args = list(args) | |
attributes = args.pop(0) if args and isinstance(args[0], dict) else {} | |
return attributes, flatten(args) | |
def render_children(self): | |
"""This method turns the list of children into HTML: | |
>>> ul = UL(LI("Coffee"), LI("Tea"), LI("Milk")) | |
>>> print(ul.render_children()) | |
<li>Coffee</li><li>Tea</li><li>Milk</li> | |
This method is generally used internally, though it is exposed to | |
users, so they can use it if they wish. This method mainly exists | |
to complement the `Element.render_attributes` method.""" | |
return cat(self.children) | |
# The standard HTML5 Void Element classes... | |
class AREA(VoidElement): pass | |
class BASE(VoidElement): pass | |
class BR(VoidElement): pass | |
class CIRCLE(VoidElement): pass | |
class COL(VoidElement): pass | |
class EMBED(VoidElement): pass | |
class HR(VoidElement): pass | |
class IMG(VoidElement): pass | |
class INPUT(VoidElement): pass | |
class LINK(VoidElement): pass | |
class META(VoidElement): pass | |
class PARAM(VoidElement): pass | |
class SOURCE(VoidElement): pass | |
class TRACK(VoidElement): pass | |
class WBR(VoidElement): pass | |
# The standard HTML5 Raw Text Element classes... | |
class SCRIPT(RawTextElement): pass | |
class STYLE(RawTextElement): pass | |
# The standard HTML5 Escapable Raw Text Element classes... | |
class TEXTAREA(EscapableRawTextElement): pass | |
class TITLE(EscapableRawTextElement): pass | |
# The standard HTML5 Template Element class... | |
class TEMPLATE(NormalElement): pass | |
# The standard HTML5 Normal Element classes... | |
class A(NormalElement): pass | |
class ABBR(NormalElement): pass | |
class ADDRESS(NormalElement): pass | |
class APPLET(NormalElement): pass | |
class ARTICLE(NormalElement): pass | |
class ASIDE(NormalElement): pass | |
class AUDIO(NormalElement): pass | |
class B(NormalElement): pass | |
class BDI(NormalElement): pass | |
class BDO(NormalElement): pass | |
class BLOCKQUOTE(NormalElement): pass | |
class BODY(NormalElement): pass | |
class BUTTON(NormalElement): pass | |
class CANVAS(NormalElement): pass | |
class CAPTION(NormalElement): pass | |
class CITE(NormalElement): pass | |
class CODE(NormalElement): pass | |
class COLGROUP(NormalElement): pass | |
class CONTENT(NormalElement): pass | |
class DATA(NormalElement): pass | |
class DATALIST(NormalElement): pass | |
class DD(NormalElement): pass | |
class DEL(NormalElement): pass | |
class DETAILS(NormalElement): pass | |
class DFN(NormalElement): pass | |
class DIALOG(NormalElement): pass | |
class DIR(NormalElement): pass | |
class DIV(NormalElement): pass | |
class DL(NormalElement): pass | |
class DT(NormalElement): pass | |
class ELEMENT(NormalElement): pass | |
class EM(NormalElement): pass | |
class FIELDSET(NormalElement): pass | |
class FIGCAPTION(NormalElement): pass | |
class FIGURE(NormalElement): pass | |
class FOOTER(NormalElement): pass | |
class FORM(NormalElement): pass | |
class H1(NormalElement): pass | |
class H2(NormalElement): pass | |
class H3(NormalElement): pass | |
class H4(NormalElement): pass | |
class H5(NormalElement): pass | |
class H6(NormalElement): pass | |
class HEAD(NormalElement): pass | |
class HEADER(NormalElement): pass | |
class HGROUP(NormalElement): pass | |
class HTML(NormalElement): pass | |
class I(NormalElement): pass | |
class INS(NormalElement): pass | |
class KBD(NormalElement): pass | |
class LABEL(NormalElement): pass | |
class LEGEND(NormalElement): pass | |
class LI(NormalElement): pass | |
class MAIN(NormalElement): pass | |
class MAP(NormalElement): pass | |
class MARK(NormalElement): pass | |
class MENU(NormalElement): pass | |
class MENUITEM(NormalElement): pass | |
class METER(NormalElement): pass | |
class NAV(NormalElement): pass | |
class NOBR(NormalElement): pass | |
class NOEMBED(NormalElement): pass | |
class NOSCRIPT(NormalElement): pass | |
class OBJECT(NormalElement): pass | |
class OL(NormalElement): pass | |
class OPTGROUP(NormalElement): pass | |
class OPTION(NormalElement): pass | |
class OUTPUT(NormalElement): pass | |
class P(NormalElement): pass | |
class PICTURE(NormalElement): pass | |
class PRE(NormalElement): pass | |
class PROGRESS(NormalElement): pass | |
class Q(NormalElement): pass | |
class RP(NormalElement): pass | |
class RT(NormalElement): pass | |
class RTC(NormalElement): pass | |
class RUBY(NormalElement): pass | |
class S(NormalElement): pass | |
class SAMP(NormalElement): pass | |
class SECTION(NormalElement): pass | |
class SELECT(NormalElement): pass | |
class SHADOW(NormalElement): pass | |
class SLOT(NormalElement): pass | |
class SMALL(NormalElement): pass | |
class SPAN(NormalElement): pass | |
class STRONG(NormalElement): pass | |
class SUB(NormalElement): pass | |
class SUMMARY(NormalElement): pass | |
class SUP(NormalElement): pass | |
class SVG(NormalElement): pass | |
class TABLE(NormalElement): pass | |
class TBODY(NormalElement): pass | |
class TD(NormalElement): pass | |
class TFOOT(NormalElement): pass | |
class TH(NormalElement): pass | |
class THEAD(NormalElement): pass | |
class TIME(NormalElement): pass | |
class TR(NormalElement): pass | |
class TT(NormalElement): pass | |
class U(NormalElement): pass | |
class UL(NormalElement): pass | |
class VAR(NormalElement): pass | |
class VIDEO(NormalElement): pass | |
# The standard SVG Void Element classes (that are not also HTML5 elements)... | |
# The magic element classes that are only used by the engine... | |
class _HTML(NormalElement): | |
"""This class implements the main `html` element, which has a different | |
signature to other elements, and works differently. Instances of this | |
element are used to represent the document as a whole (the doctype is | |
prepended to this element automatically when it is represented). This | |
class is only used internally. It is not part of the API at all.""" | |
def __init__(self, lang, *signature): | |
"""These elements are created to hold the `head` and `body` elements | |
of a document. The constructor takes a `lang` argument, which sets | |
the `lang` attribute of the `html` element. It also takes a `head` | |
and `body` element, which become its only children. All three | |
arguments are required.""" | |
self.attributes, self.children = self.signature(signature) | |
self.attributes.update({"lang": lang}) | |
def __repr__(self): | |
"""This overrides `NormalElement.__repr__` so the representation can be | |
concatenated to the HTML5 doctype to create a complete document.""" | |
return "<!doctype html>" + super(_HTML, self).__repr__() | |
class _BODY(NormalElement): | |
"""This class implements the `body` element, which is only directly used | |
internally (but exposed to library users as `Document.body`). | |
The constructor takes a reference to the instance of `Document` that the | |
body element is bound to. The reference is used by the `__repr__` method | |
to append the browser upgrade stuff and to prepend any resource elements | |
to the rendered body. The remaining arguments follow the same pattern as | |
standard elements use.""" | |
def __init__(self, document, *signature): | |
super(_BODY, self).__init__(*signature) | |
self.document = document | |
def __repr__(self): | |
"""This overrides `NormalElement.__repr__` so the representation can be | |
trimmed to remove the closing tag (`</body>`). This makes it easier | |
to concatenate script tags to the body (before replacing the tag).""" | |
children = self.render_children() | |
attributes = self.render_attributes() | |
augmentation = self.document.render_augmentation() | |
args = self.name, attributes, children, augmentation | |
return "<{0}{1}>{2}{3}</{0}>".format(*args) | |
# The Magic Element Helper Classes... | |
class Tree(NormalElement): | |
"""This abstract element implements just the body of a proper HTML element. | |
It has children, and all the methods that open elements have for working | |
with children, but it does not have a name or attributes (nor the | |
associated methods). | |
When an instance of `Tree` is rendered, it just concatenates each of its | |
children together and returns the HTML. | |
Note: An instance of this class is exposed to users as `Engine.tree`.""" | |
def __init__(self): self.children = Children() | |
def __repr__(self): return self.render_children() | |
class Comment(Element): | |
"""This class implements HTML comments, which are standard elements, | |
but have non-standard functionality. | |
>>> Comment("hello world") | |
<!-- hello world --> | |
""" | |
def __init__(self, content): | |
"""This method overrides the inhrited one, as comments have no | |
attributes (or children), and just have a string of arbitrary | |
content instead.""" | |
self.content = content | |
def __repr__(self): | |
"""This method overrides the inherited one, instead wrapping the | |
`content` attribute in opening and closing HTML comment tags.""" | |
return "<!-- {0} -->".format(self.content) | |
class Legacy(NormalElement): | |
"""This class implements the Internet Explorer less-than-or-equal tags | |
that render their content if the browser is an older version of IE. | |
>>> Legacy(7, P("upgrade"), P("now")) | |
<!--[if lte IE 7]><p>upgrade</p><p>now</p><![endif]--> | |
""" | |
def __init__(self, version, *children): | |
"""This method takes a required version number (an integer) and zero | |
or more children. The version number is used to create the tags, and | |
the children are (potentially) rendered inside it.""" | |
self.version = version | |
self.children = Children(children) | |
def __repr__(self): | |
args = self.version, self.render_children() | |
return "<!--[if lte IE {0}]>{1}<![endif]-->".format(*args) | |
class Favicon(META): | |
"""This class extends `META` with functionality for creating link tags | |
for favicons. The constructor takes a path, followed by zero or more | |
ints, which (if provided) set the `sizes` attribute, with each size | |
converted into the correct syntax. This class assumes all favicons | |
are square (providing `16` as a size sets it to `16x16`). When no | |
sizes are provided, the filetype should be SVG: | |
>>> Favicon("icon.svg") | |
<meta href="icon.svg" rel="icon" sizes="any" type="image/svg+xml"/> | |
>>> Favicon("icon.png", 16) | |
<meta href="icon.png" rel="icon" sizes="16x16" type="image/png"/> | |
>>> Favicon("icon.png", 64, 128) | |
<meta href="icon.png" rel="icon" sizes="64x64 128x128" type="image/png"/> | |
""" | |
def __init__(self, href, *sizes): | |
if not sizes: size, type = "any", "image/svg+xml" | |
else: | |
size = space.join( "{0}x{0}".format(size) for size in sizes ) | |
type = "image/" + ext(href) | |
self.attributes = { | |
"rel": "icon", "href": href, "sizes": size, "type": type | |
} | |
class Mobicon(Favicon): | |
"""This class extends `Favicon` by removing the `type` attribute and | |
updating a `rel` attribute to `apple-touch-icon`. Note that these tags | |
are also parsed by Android. | |
>>> Mobicon("icon.svg") | |
<meta href="icon.svg" rel="apple-touch-icon" sizes="any"/> | |
>>> Mobicon("icon.png", 16) | |
<meta href="icon.png" rel="apple-touch-icon" sizes="16x16"/> | |
""" | |
def __init__(self, *args): | |
super(Mobicon, self).__init__(*args) | |
self["rel"] = "apple-touch-icon" | |
self.attributes.pop("type") | |
class Anchor(A): | |
"""This class implements a magic element that generates anchor tags. The | |
constructor requires a path (the `href` value), followed by the Standard | |
Signature: | |
>>> Anchor("/about", "The about us page.") | |
<a href="/about">The about us page.</a> | |
>>> Anchor("/about", {"class": "button"}, "The about us page.") | |
<a class="button" href="/about">The about us page.</a> | |
""" | |
def __init__(self, path, *signature): | |
super(Anchor, self).__init__(*signature) | |
self **= {"href": path} | |
class Style(LINK): | |
"""This class implements a magic element that generates elements for | |
loading CSS stylesheets. The constructor requires a url (the `href` | |
value) to be passed before an optional attributes dict. | |
>>> Style("/static/magic.css") | |
<link href="/static/magic.css" rel="stylesheet"/> | |
""" | |
def __init__(self, path, *signature): | |
super(Style, self).__init__(*signature) | |
self **= {"rel": "stylesheet", "href": path} | |
class Logic(SCRIPT): | |
"""This class implements a magic element that generates elements for | |
loading JavaScript scripts. The constructor requires a link (the `href` | |
value) to be passed before the regular arguments that all elements take. | |
>>> Logic("/static/wizardry.js") | |
<script src="/static/wizardry.js"></script> | |
""" | |
def __init__(self, path, *args): | |
super(Logic, self).__init__(*args) | |
self **= {"src": path} | |
# The Hypertext Markup Engine... | |
class Engine(object): # TODO: improve doctest | |
"""This class models HTML5 documents. The API is covered in the intro and | |
API docs. | |
Note: The methods have doctests, but the engine needs in an external file | |
full of example documents to test against, which is not ready, so this is | |
it for now: | |
>>> docs = Engine(favicon="favicon.png") | |
>>> docs.install(Style("magic.css"), Logic("wizardry.js")) | |
>>> docs.uninstall(Style("magic.css")) | |
>>> docs.title = "404 Error" | |
>>> docs.description = "A subpage for file-not-found errors." | |
>>> docs *= HEADER(Anchor("/", "Write Dot Run")) | |
>>> str(docs) == read("test.htme") | |
True | |
>>> str(docs) == readlines("test.htme")[0] | |
True | |
""" | |
def __init__( | |
self, | |
lang="en", # required ISO 639-1 language code | |
charset="utf-8", # required character encoding | |
ie_version="edge", # optional X-UA-Compatible metatag version | |
base=None, # setting for the optional base element | |
title="", # required body of the title element | |
author=None, # optional content of the author metatag | |
description=None, # optional content of the description metatag | |
viewport=True, # how to handle rendering the viewport metatag | |
scale=1, # sets the `initial-scale` viewport attribute | |
scalable=None, # sets the `user-scalable` viewport attribute | |
minimum_scale=None, # sets the `minimum-scale` viewport attribute | |
maximum_scale=None, # sets the `maximum-scale` viewport attribute | |
width="device-width", # sets the `width` viewport attribute | |
height=None, # sets the `height` viewport attribute | |
manifest=None, # optional url for the web manifest file | |
favicon=None, # optional url for the favicon image file | |
icons=None, # list of optional favicon and mobicon elements | |
installation=None, # list of resource elements appended to the head | |
augmentation=None, # list of resource elements appended to the body | |
freezer=None, # optional alternative initial frozen docs | |
html_attributes=None, # the hash for the html element attributes | |
head_attributes=None, # the hash for the head element attributes | |
body_attributes=None, # the hash for the body element attributes | |
tree=None # optional alternative initial tree element | |
): | |
"""This method just copies its keyword args to `self`, handling any | |
defaults. | |
Note that if `freezer` is provided *as a keyword argument*, it can be | |
an instance of `Element` or `Engine`, and its `freezer` attribute is | |
shallow copied to this engine. Of course, a dict can also be passed, | |
and it will be used directly (without copying).""" | |
# This function returns its first arg if its first arg is expressly | |
# not `None`; otherwise it returns its second arg: | |
extant = lambda arg, default: default if arg is None else arg | |
# Directly copy arguments with simple defaults over to `self`: | |
self.lang = lang | |
self.charset = charset | |
self.ie_version = ie_version | |
self.base = base | |
self.title = title | |
self.author = author | |
self.description = description | |
self.viewport = viewport | |
self.scale = scale | |
self.scalable = scalable | |
self.minimum_scale = minimum_scale | |
self.maximum_scale = maximum_scale | |
self.width = width | |
self.height = height | |
self.manifest = manifest | |
self.favicon = favicon | |
# Use the `extant` function to handle the mutable defaults: | |
self.tree = extant(tree, Tree()) | |
self.icons = extant(icons, []) | |
self.installation = extant(installation, []) | |
self.augmentation = extant(augmentation, []) | |
self.html_attributes = extant(html_attributes, {}) | |
self.head_attributes = extant(head_attributes, {}) | |
self.body_attributes = extant(body_attributes, {}) | |
# Copy the freezer from an element or engine if one is passed in, | |
# else use the arg directly (still defaulting to an empty dict): | |
if isinstance(freezer, (Element, Engine)): | |
self.freezer = freezer.freezer.copy() | |
else: self.freezer = {} if freezer is None else freezer | |
def __repr__(self): | |
"""This method makes the representation of the document render and | |
return its HTML representation. | |
Note: This method always returns a valid HTML5 document. | |
Note: The design of the library fundamentally depends on this method | |
never mutating `self`.""" | |
head = HEAD( | |
self.head_attributes, self.render_charset(), | |
self.render_ie_version(), self.render_title(), | |
self.render_author(), self.render_description(), | |
self.render_viewport(), self.render_favicon(), | |
self.render_manifest(), self.render_icons(), | |
self.render_installation() | |
) | |
body = _BODY(self, self.body_attributes, str(self.tree)) | |
return str(_HTML(self.lang, self.html_attributes, head, body)) | |
def __call__(self, key, *args, **kargs): | |
"""This method makes it possible to access and format frozen documents | |
by invoking the instance, passing in the key for the frozen document. | |
Users can also pass args and keyword args, which just get passed on | |
to the `str.format` method, which is called on the frozen string | |
before it is returned.""" | |
return self.freezer[key].format(*args, **kargs) | |
def __imul__(self, other): | |
"""This method implements the `*=` operator for documents, so they work | |
like elements, passing everything on to `self.tree`.""" | |
self.tree *= other | |
return self | |
def __idiv__(self, other): | |
"""This method implements the `/=` operator for documents, so they work | |
like elements, passing everything on to `self.tree`.""" | |
self.tree /= other | |
return self | |
def __getitem__(self, index): | |
"""This method implements the square bracket suffix operator for | |
documents, so they work like elements, passing the arguments | |
through to `self.tree`. | |
Note: The tree only has children.""" | |
return self.tree[index] | |
def __setitem__(self, index, other): | |
"""This method implements the square bracket suffix operator for | |
documents, so they work like elements, passing the arguments | |
through to `self.tree`. | |
Note: The tree only has children.""" | |
self.tree[index] = other | |
return self | |
def __eq__(self, other): | |
"""This method checks whether two documents are equal by checking | |
that they both evaluate to the same string. | |
>>> a, b = Engine(), Engine() | |
>>> a *= P(), P(), P() | |
>>> b *= [P, P, P] | |
>>> a == b | |
True | |
""" | |
return repr(self) == repr(other) | |
def __ne__(self, other): | |
"""This method checks that two documents are not equal by checking | |
that they both evaluate to different strings. | |
>>> a, b = Engine(), Engine() | |
>>> a *= P(), P(), P() | |
>>> b *= [P, P, ASIDE] | |
>>> a != b | |
True | |
""" | |
return repr(self) != repr(other) | |
def __len__(self): | |
"""This method implements `len(docs)`, where `docs` is an instance | |
of `Engine`. It returns the total number of elements in the tree. | |
>>> doc = Engine() | |
>>> len(doc) == 0 | |
True | |
>>> doc *= P, P, P | |
>>> len(doc) == 3 | |
True | |
""" | |
return len(self.tree) | |
def __iter__(self): | |
"""This method allows iteration over the current children of the tree, | |
with `child for child in docs`. | |
>>> doc = Engine() | |
>>> doc *= P(), P(), P() | |
>>> for child in doc: print(child) | |
<p></p> | |
<p></p> | |
<p></p> | |
""" | |
for child in self.tree: yield child | |
def install(self, *tags): | |
"""This method takes any number of tags, and just appends them to | |
`self.installation`. | |
>>> doc = Engine() | |
>>> doc.install(Logic("logic.js")) | |
>>> print(doc.render_installation()) | |
<script src="logic.js"></script> | |
""" | |
self.installation += list(tags) | |
def augment(self, *tags): | |
"""This method takes any number of tags, and just appends them to | |
`self.augmentation`. | |
>>> doc = Engine() | |
>>> doc.augment(Logic("logic.js")) | |
>>> print(doc.render_augmentation()) | |
<script src="logic.js"></script> | |
""" | |
self.augmentation += list(tags) | |
def iconify(self, *elements): | |
"""This method takes any number of elements, and just appends them | |
to `self.icons`. | |
>>> doc = Engine() | |
>>> doc.iconify(Favicon("logo.png", 16)) | |
>>> print(doc.render_icons()) | |
<meta href="logo.png" rel="icon" sizes="16x16" type="image/png"/> | |
""" | |
self.icons += list(elements) | |
@staticmethod | |
def un(array, args): | |
"""This method iterates over a sequence of args. If an arg is some | |
instance of `int`, the tag with that index is deleted from `array` | |
(possibly raising an `IndexError`). If the arg is not an integer, | |
the `remove` method of the array is invoked to remove the first | |
tag that is equal to the arg (possibly raising a `ValueError`). | |
If `args` is empty, every tag is removed from the array. | |
Note: Instances derived from `Element` are equal to another object | |
if the string representation of the instance is equal to the string | |
representation of the other object, and not equal otherwise.""" | |
if not args: del array[:] | |
else: | |
args = list(args) | |
args.sort() | |
for offset, arg in enumerate(args): | |
if isinstance(arg, int): del array[arg - offset] | |
else: array.remove(arg) | |
def uninstall(self, *args): | |
"""This applies the `un` method to the installation list. | |
>>> doc = Engine(installation=[Style("style.css")]) | |
>>> doc.uninstall(Style("style.css")) | |
>>> print(doc.render_installation()) | |
<BLANKLINE> | |
""" | |
self.un(self.installation, args) | |
def unaugment(self, *args): | |
"""This applies the `un` method to the augmentation list. | |
>>> doc = Engine(augmentation=[Logic("logic.js")]) | |
>>> doc.unaugment(Logic("logic.js")) | |
>>> print(doc.render_augmentation()) | |
<BLANKLINE> | |
""" | |
self.un(self.augmentation, args) | |
def uniconify(self, *args): | |
"""This applies the `un` method to the icons list. | |
>>> doc = Engine() | |
>>> doc.iconify(Favicon("logo.png", 16, 32)) | |
>>> doc.uniconify(Favicon("logo.png", 16, 32)) | |
>>> print(doc.render_icons()) | |
<BLANKLINE> | |
""" | |
self.un(self.icons, args) | |
def render_installation(self): | |
"""This method renders the installation elements (the elements that | |
load JavaScript and CSS files at the end of the head element) by | |
concatenating the elements in the installation list. | |
>>> Engine().render_installation() == empty | |
True | |
>>> doc = Engine(installation=[Style("style.css")]) | |
>>> print(doc.render_installation()) | |
<link href="style.css" rel="stylesheet"/> | |
""" | |
return cat(self.installation) | |
def render_augmentation(self): | |
"""This method renders the augmentation elements (the elements that | |
load JavaScript and CSS files at the end of the body element) by | |
concatenating the elements in the augmentation list. | |
>>> Engine().render_augmentation() == empty | |
True | |
>>> doc = Engine(augmentation=[Logic("logic.js")]) | |
>>> print(doc.render_augmentation()) | |
<script src="logic.js"></script> | |
""" | |
return cat(self.augmentation) | |
@staticmethod | |
def expand(candidate, fallback): | |
"""This static method takes the value of an engine attribute and an | |
element (`candidate` and `fallback`), both required. It implements | |
the standard attribute expansion logic. | |
If the candidate is `None`, an empty string is returned. If it is an | |
element, then the candidate itself is returned, otherwise the fallback | |
is returned. | |
This is used internally by many of the internal `Engine.render_*` | |
methods (and is effectively doctested by them).""" | |
if candidate is None: return "" | |
if isinstance(candidate, Element): return candidate | |
return fallback | |
def render_charset(self): | |
"""This method renders the meta charset element. The associated | |
attribute is required, and defaults to `utf-8`. | |
>>> Engine().render_charset() | |
<meta charset="utf-8"/> | |
>>> Engine(charset="ANSI").render_charset() | |
<meta charset="ANSI"/> | |
""" | |
return self.expand(self.charset, META({"charset": self.charset})) | |
def render_ie_version(self): | |
"""This method expands the `X-UA-Compatible` engine attribute: | |
>>> Engine().render_ie_version() | |
<meta content="ie=edge" http-equiv="x-ua-compatible"/> | |
>>> Engine(ie_version=7).render_ie_version() | |
<meta content="ie=7" http-equiv="x-ua-compatible"/> | |
>>> Engine(ie_version=None).render_ie_version() == empty | |
True | |
""" | |
return self.expand(self.ie_version, META({ | |
"http-equiv": "x-ua-compatible", | |
"content": "ie={}".format(self.ie_version) | |
})) | |
def render_base(self): | |
"""This method expands the `base` engine attribute: | |
>>> Engine().render_base() == empty | |
True | |
>>> Engine(base="http://www.example.com/page.html").render_base() | |
<base href="http://www.example.com/page.html"/> | |
Users wanting to set the `target` attribute must provide an element: | |
>>> Engine(base=BASE({"target": "_blank"})).render_base() | |
<base target="_blank"/> | |
""" | |
return self.expand(self.base, BASE({"href": self.base})) | |
def render_title(self): | |
"""This method renders the `title` element, which is required, and | |
defaults to an empty string: | |
>>> Engine().render_title() | |
<title></title> | |
>>> Engine(title="spam and eggs").render_title() | |
<title>spam and eggs</title> | |
""" | |
return TITLE(self.title) | |
def render_author(self): | |
"""This method expands the `author` engine attribute: | |
>>> Engine().render_author() == empty | |
True | |
>>> Engine(author="batman").render_author() | |
<meta content="batman" name="author"/> | |
""" | |
return self.expand(self.author, META({ | |
"name": "author", "content": self.author | |
})) | |
def render_description(self): | |
"""This method expands the `description` engine attribute: | |
>>> Engine().render_description() == empty | |
True | |
>>> Engine(description="spam and eggs").render_description() | |
<meta content="spam and eggs" name="description"/> | |
""" | |
return self.expand(self.description, META({ | |
"name": "description", "content": self.description | |
})) | |
def render_viewport(self): | |
"""This method renders the viewport META tag that sets the initial | |
scale, maximum and minimum scale, scalability, height and width of | |
the viewport. This helps mobile devices to render pages correctly. | |
If `mobile` is `None` or `False`, no element is rendered. Otherwise, | |
the element is constructed from the `scale`, `scalable`, `width`, | |
`height`, `maximum_scale` and `minimum_scale` attributes: | |
>>> doc = Engine() | |
>>> doc.render_viewport() | |
<meta content="width=device-width, initial-scale=1" name="viewport"/> | |
>>> doc.scale = 1.5 | |
>>> doc.width = 1280 | |
>>> doc.render_viewport() | |
<meta content="width=1280, initial-scale=1.5" name="viewport"/> | |
>>> doc.scale = None | |
>>> doc.height = "device-height" | |
>>> doc.render_viewport() | |
<meta content="width=1280, height=device-height" name="viewport"/> | |
>>> doc.viewport = META({"name": "viewport", "content": "width=800"}) | |
>>> doc.render_viewport() | |
<meta content="width=800" name="viewport"/> | |
>>> doc.viewport = None | |
>>> doc.render_viewport() == empty | |
True | |
""" | |
if self.viewport is None: return "" | |
if isinstance(self.viewport, Element): return self.viewport | |
values = [] | |
if self.width is not None: | |
values.append("width={0}".format(self.width)) | |
if self.height is not None: | |
values.append("height={0}".format(self.height)) | |
if self.scale is not None: | |
values.append("initial-scale={0}".format(self.scale)) | |
if self.scalable is not None: | |
values.append("user-scalable={0}".format(self.scalable)) | |
if self.minimum_scale is not None: | |
values.append("minimum-scale={0}".format(self.minimum_scale)) | |
if self.maximum_scale is not None: | |
values.append("maximum-scale={0}".format(self.maximum_scale)) | |
return META({"name": "viewport", "content": cat(values, ", ")}) | |
def render_favicon(self): | |
"""This method expands the `favicon` engine attribute: | |
>>> Engine().render_favicon() == empty | |
True | |
>>> Engine(favicon="logo.png").render_favicon() | |
<link href="logo.png" rel="icon"/> | |
""" | |
return self.expand(self.favicon, LINK({ | |
"rel": "icon", "href": self.favicon | |
})) | |
def render_icons(self): | |
"""This method renders the elements that link to favicon and mobicon | |
image files, based on the items in `icons`. | |
>>> doc = Engine() | |
>>> doc.iconify(Favicon("logo.png", 16, 32)) | |
>>> print(doc.render_icons()) | |
<meta href="logo.png" rel="icon" sizes="16x16 32x32" type="image/png"/> | |
>>> doc = Engine() | |
>>> doc.iconify(Mobicon("logo.svg")) | |
>>> print(doc.render_icons()) | |
<meta href="logo.svg" rel="apple-touch-icon" sizes="any"/> | |
""" | |
return cat(self.icons) | |
def render_manifest(self): | |
"""This method expands the `manifest` engine attribute: | |
>>> Engine().render_manifest() == empty | |
True | |
>>> Engine(manifest="site.webmanifest").render_manifest() | |
<link href="site.webmanifest" rel="manifest"/> | |
""" | |
return self.expand(self.manifest, LINK({ | |
"rel": "manifest", "href": self.manifest | |
})) | |
def freeze(self, key): | |
"""This method freezes the current state of the instance, turning it | |
into a string. The one required arg is the key used to store the | |
frozen doc in `self.freezer`.""" | |
self.freezer[key] = str(self) | |
def write(self, path): | |
"""This method renders the entire doc, writes it to the given path, | |
then returns `self`, so it can be printed or method chained.""" | |
write(str(self), path) | |
return self | |
# Run the doctests if this file is executed as a script... | |
if __name__ == "__main__": | |
from doctest import testmod | |
print(testmod()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment