Created
January 7, 2014 08:35
-
-
Save huyx/8296337 to your computer and use it in GitHub Desktop.
从 flaskext.htmlbuilder 精简过来
This file contains hidden or 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
# -*- coding: utf-8 -*- | |
u"""html builder | |
从 flaskext.htmlbuilder 精简过来 | |
对比了很多 html 生成工具,包括看起开很不错的: | |
- [PyH]() | |
- [Dominate](https://github.com/Knio/dominate) -- 从 pyy 进化而来 | |
但还是觉得这个更好: | |
- [Flask-HTMLBuilder](http://majorz.github.io/flask-htmlbuilder/) | |
只可惜是和 Flask 集成在一起的,这里只是把 Flask 相关的部分功能和代码去掉。 | |
""" | |
from __future__ import absolute_import | |
from keyword import kwlist | |
__all__ = ['html', 'render'] | |
class HTMLDispatcher(object): | |
def __getattr__(self, attr): | |
if attr in special_elements: | |
return special_elements[attr]() | |
return Element(attr) | |
html = HTMLDispatcher() | |
"""The :data:`html` instance is used as a factory for building HTML tree | |
structure. Example:: | |
headline = html.div(id='headline')( | |
html.h1(class_='left')('Headline text') | |
) | |
The tree structure can be directly rendered to HTML using `str` or `unicode`. | |
Example:: | |
>>> unicode(headline) | |
u'<div id="headline"><h1 class="left">Headline text</h1></div>' | |
This is useful when combined with a template engine like Jinja 2. An | |
alternative approach is to use the :func:`render` function when indentation is | |
needed. Or another approach is to use the :func:`render_template` function in | |
this module that is a powerful standalone solution for full HTML document | |
rendering. | |
This extension provides a special HTML builder syntax accessed through | |
the :data:`html` instance. | |
Void HTML element:: | |
>>> str(html.br) | |
'<br />' | |
Void element with attributes:: | |
>>> str(html.img(src='/img/logo.png', class_='centered')) | |
'<img class="centered" src="/img/logo.png" />' | |
.. note:: | |
Since attribute names like `class` are reserved Python keywords those need | |
to be escaped with an underscore "_" symbol at the end of the name. The | |
same holds true for HTML elements like `del`, which needs to be declared as | |
`html.del_`. | |
Non-void element:: | |
>>> str(html.div()) | |
'<div></div>' | |
Element with children:: | |
>>> str(html.div(html.p('First'), html.p('Second'))) | |
'<div><p>First</p><p>Second</p></div>' | |
Element with attributes and children:: | |
>>> str(html.div(class_='centered')(html.p('First'))) | |
'<div class="centered"><p>First</p></div>' | |
.. note:: | |
Attribute definition is done in a different call from child elements | |
definition as you can see in the above example. This approach is taken | |
because Python does not allow keyword arguments (the attributes in this | |
case) to be placed before the list arguments (child elements). `__call__` | |
chaining allows the definition syntax to be closer to HTML. | |
""" | |
def render(element, level=0): | |
"""Renders the HTML builder `element` and it's children to a string. | |
Example:: | |
>>> print( | |
... render( | |
... html.form(action='/search', name='f')( | |
... html.input(name='q'), | |
... html.input(name='search_button', type='submit') | |
... ) | |
... ) | |
... ) | |
<form action="/search" name="f"> | |
<input name="q" /> | |
<input type="submit" name="search_button" /> | |
</form> | |
The :func:`render` function accepts the following arguments: | |
:param element: element created through the `html` builder instance, a list | |
of such elements, or just a string. | |
:param level: indentation level of the rendered element with a step of two | |
spaces. If `level` is `None`, the `element` will be rendered | |
without indentation and new line transferring, passing the | |
same rule to the child elements. | |
""" | |
if hasattr(element, 'render'): | |
return element.render(level) | |
elif hasattr(element, '__iter__'): | |
return _render_iteratable(element, level) | |
elif isinstance(element, basestring): | |
return _render_string(element, level) | |
elif element is None: | |
return '' | |
raise TypeError('Cannot render %r' % element) | |
class BaseElement(object): | |
__slots__ = [] | |
def __str__(self): | |
return str(self.render(None)) | |
def __unicode__(self): | |
return unicode(self.render(None)) | |
def __html__(self): | |
return self.__unicode__() | |
def render(self, level): | |
raise NotImplementedError('render() method has not been implemented') | |
class Element(BaseElement): | |
__slots__ = ['_name', '_children', '_attributes'] | |
def __init__(self, name): | |
self._name = _unmangle_element_name(name) | |
# `None` indicates a void element or a list content for non-void | |
# elements. | |
self._children = None | |
# `None` indicates no attributes, or it is a list if there are any. | |
self._attributes = None | |
def __call__(self, *children, **attributes): | |
# Consequent calling the instances of that class with keyword | |
# or list arguments or without arguments populates the HTML element | |
# with attribute and children data. | |
if attributes: | |
# Keyword arguments are used to indicate attribute definition. | |
self._attributes = attributes | |
elif children: | |
# Child nodes are passed through the list arguments. | |
self._children = children | |
else: | |
# Create an empty non-void HTML element. | |
self._children = [] | |
return self | |
def __repr__(self): | |
result = '<' + type(self).__name__ + ' ' + self._name | |
if self._attributes is not None: | |
result += _serialize_attributes(self._attributes) | |
if self._children: | |
result += ' ...' | |
result += '>' | |
return result | |
def render(self, level): | |
# Keeping this method intentionally long for execution speed gain. | |
result = _indent(level) + '<' + self._name | |
if self._attributes is not None: | |
result += _serialize_attributes(self._attributes) | |
if self._children is None: | |
result += ' />' | |
else: | |
result += '>' | |
if self._children: | |
if len(self._children) == 1 and isinstance(self._children[0], basestring) or self._children[0] is None: | |
result += escape(self._children[0]) | |
else: | |
result += _new_line(level) | |
if level is not None: | |
level += 1 | |
result += _render_iteratable(self._children, level) | |
if level is not None: | |
level -= 1 | |
result += _indent(level) | |
result += '</' + self._name + '>' | |
result += _new_line(level) | |
return result | |
class Comment(BaseElement): | |
"""`html.comment` is used for rendering HTML comments. | |
Example:: | |
>>> print(render([ | |
... html.comment('Target less enabled mobile browsers'), | |
... html.link(rel='stylesheet', media='handheld', | |
... href='css/handheld.css') | |
... ])) | |
<!--Target less enabled mobile browsers--> | |
<link media="handheld" href="css/handheld.css" rel="stylesheet" /> | |
""" | |
__slots__ = ['_comment'] | |
def __init__(self): | |
self._comment = None | |
def __call__(self, comment): | |
self._comment = comment | |
return self | |
def render(self, level): | |
result = _indent(level) + '<!--' | |
if self._comment is not None: | |
result += self._comment | |
result += '-->' + _new_line(level) | |
return result | |
class Doctype(BaseElement): | |
"""`html.doctype` is used for rendering HTML doctype definition at the | |
beginning of the HTML document. Example:: | |
>>> print(render([ | |
... html.doctype('html'), | |
... html.html( | |
... html.head('...'), | |
... html.body('...') | |
... ) | |
... ])) | |
<!doctype html> | |
<html> | |
<head>...</head> | |
<body>...</body> | |
</html> | |
""" | |
__slots__ = ['_doctype'] | |
def __init__(self): | |
self._doctype = None | |
def __call__(self, doctype): | |
self._doctype = doctype | |
return self | |
def render(self, level): | |
return _indent(level) + '<!doctype ' + self._doctype + '>' + \ | |
_new_line(level) | |
class Safe(BaseElement): | |
"""`html.safe` renders HTML text content without escaping it. This is | |
useful for insertion of prerendered HTML content. Example:: | |
>>> print(render([ | |
... html.div( | |
... html.safe('<strong>Hello, World!</strong>') | |
... ) | |
... ])) | |
<div> | |
<strong>Hello, World!</strong> | |
</div> | |
""" | |
__slots__ = ['_content'] | |
def __init__(self): | |
self._content = None | |
def __call__(self, content): | |
self._content = content | |
return self | |
def render(self, level): | |
return _indent(level) + self._content + _new_line(level) | |
class Join(BaseElement): | |
"""`html.join` is used for rendering a list of HTML builder elements | |
without indenting them and transferring each of them to a new line. This | |
is necessary when rendering a paragraph content for example and all text | |
and other elements need to stick together. Example:: | |
>>> print(render([ | |
... html.p( | |
... html.join( | |
... 'Read the ', html.a(href='/docs')('documentation'), '.' | |
... ) | |
... ) | |
... ])) | |
<p> | |
Read the <a href="/docs">documentation</a>. | |
</p> | |
""" | |
__slots__ = ['_children'] | |
def __init__(self): | |
self._children = None | |
def __call__(self, *children): | |
self._children = children | |
return self | |
def render(self, level): | |
return _indent(level) + _render_iteratable(self._children, None) + \ | |
_new_line(level) | |
class NewLine(BaseElement): | |
"""`html.newline` adds an empty new line in the content. This is only | |
needed for better readibility of the HTML source code. Example:: | |
>>> print(render([ | |
... html.p('First'), | |
... html.newline(), | |
... html.p('Second') | |
... ])) | |
<p>First</p> | |
<p>Second</p> | |
""" | |
__slots__ = [] | |
def __call__(self): | |
return self | |
def render(self, level): | |
return _indent(level) + _new_line(level) | |
class BaseHasElement(BaseElement): | |
__slots__ = ['_children', '_name'] | |
def __init__(self): | |
self._children = None | |
self._name = None | |
def __call__(self, *arguments): | |
if self._name is None: | |
self._name = arguments[0] | |
else: | |
self._children = arguments | |
return self | |
special_elements = { | |
'comment': Comment, | |
'doctype': Doctype, | |
'safe': Safe, | |
'join': Join, | |
'newline': NewLine, | |
} | |
def _indent(level): | |
"""Indent a line that will contain HTML data.""" | |
if level is None: | |
return '' | |
return ' ' * level * 2 | |
def _new_line(level): | |
if level is None: | |
return '' | |
else: | |
return '\n' | |
def _render_string(string, level): | |
"""Renders HTML escaped text.""" | |
return _indent(level) + escape(string) + _new_line(level) | |
def _render_iteratable(iteratable, level): | |
"""Renders iteratable sequence of HTML elements.""" | |
return ''.join([render(element, level) for element in iteratable]) | |
def _serialize_attributes(attributes): | |
"""Serializes HTML element attributes in a name="value" pair form.""" | |
result = '' | |
for name, value in attributes.iteritems(): | |
if value is None or (hasattr(value, 'is_none') and value.is_none()): | |
continue | |
result += ' ' + _unmangle_attribute_name(name) + '="' \ | |
+ escape(value, True) + '"' | |
return result | |
_PYTHON_KEYWORD_MAP = dict((reserved + '_', reserved) for reserved in kwlist) | |
def _unmangle_element_name(name): | |
"""Unmangles element names so that correct Python method names are | |
used for mapping element names.""" | |
# Python keywords cannot be used as method names, an underscore should | |
# be appended at the end of each of them when defining attribute names. | |
return _PYTHON_KEYWORD_MAP.get(name, name) | |
def _unmangle_attribute_name(name): | |
"""Unmangles attribute names so that correct Python variable names are | |
used for mapping attribute names.""" | |
# Python keywords cannot be used as variable names, an underscore should | |
# be appended at the end of each of them when defining attribute names. | |
name = _PYTHON_KEYWORD_MAP.get(name, name) | |
# Attribute names are mangled with double underscore, as colon cannot | |
# be used as a variable character symbol in Python. Single underscore is | |
# used for substituting dash. | |
name = name.replace('__', ':').replace('_', '-') | |
return name | |
def escape(string, quote=False): | |
"""Standard HTML text escaping, but protecting against the agressive | |
behavior of Jinja 2 `Markup` and the like. | |
""" | |
if string is None: | |
return '' | |
elif hasattr(string, '__html__'): | |
return unicode(string) | |
string = string.replace('&', '&').replace('<', '<').replace('>', '>') | |
if quote: | |
string = string.replace('"', """) | |
return string | |
if __name__ == '__main__': | |
form = html.form(method='POST')( | |
html.input(name='name') | |
) | |
print render(form) | |
print render([html.doctype('html'), | |
html.head(html.title(u'你好')), | |
html.body( | |
form, | |
), | |
]) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment