Skip to content

Instantly share code, notes, and snippets.

@carlsmith
Last active May 25, 2018 03:13
Show Gist options
  • Save carlsmith/996534c507c1278087f7e19efcc87465 to your computer and use it in GitHub Desktop.
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
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)
"""
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="&lt;>&quot;" foo="spam &amp; 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 = {'<': '&lt;', '&': '&amp;', '"': '&quot;'}
# 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