Skip to content

Instantly share code, notes, and snippets.

@huyx
Created January 7, 2014 08:35
Show Gist options
  • Save huyx/8296337 to your computer and use it in GitHub Desktop.
Save huyx/8296337 to your computer and use it in GitHub Desktop.
从 flaskext.htmlbuilder 精简过来
# -*- 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('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
if quote:
string = string.replace('"', "&quot;")
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