--- a/degal/html.py Thu Jun 04 11:23:28 2009 +0300
+++ b/degal/html.py Fri Jun 05 17:25:58 2009 +0300
@@ -3,89 +3,280 @@
"""
from cgi import escape
-
-def tag_attr (name, value) :
- return u'%s="%s"' % (name.rstrip('_'), escape(value, True))
-
-def tag_subcontent (subcontent) :
- if not subcontent :
- # skip
- return
+import itertools as _itertools
- elif not isinstance(subcontent, basestring) and hasattr(subcontent, '__iter__') :
- # render each sub-item recursively
- for sc in subcontent :
- for line in tag_subcontent(sc) :
- yield line
-
- else :
- yield subcontent
+class IRenderable (object) :
+ """
+ Something that's renderable as the contents of a HTML tag
+ """
-def tag_content (content) :
- if not content:
- # no output
- return
+ def render_raw_lines (self, indent=u'\t') :
+ """
+ Render the indented lines for tag and contents, without newlines
+ """
+
+ abstract
+
+class Tag (IRenderable) :
+ """
+ A HTML tag, with attributes and contents, which can a mixture of data and other renderables(tags).
+
+ Provides various kinds of rendering output
+ """
+
+ @staticmethod
+ def process_contents (contents) :
+ """
+ Postprocess contents iterable to return new list.
+
+ Items that are None will be omitted from the return value.
+
+ >>> Tag.process_contents([])
+ []
+ >>> Tag.process_contents([None])
+ []
+ >>> Tag.process_contents([u'foo'])
+ [u'foo']
+ """
+
+ return [c for c in contents if c is not None]
+
+ @staticmethod
+ def process_attrs (attrs) :
+ """
+ Postprocess attributes.
+
+ Key-value pairs where the value is None will be ommitted, and any trailing underscores in the key omitted.
+
+ TODO: only remove one underscore
+
+ >>> Tag.process_attrs(dict())
+ {}
+ >>> Tag.process_attrs(dict(foo='bar'))
+ {'foo': 'bar'}
+ >>> Tag.process_attrs(dict(class_='bar', frob=None))
+ {'class': 'bar'}
+ """
+
+ return dict((k.rstrip('_'), v) for k, v in attrs.iteritems() if v is not None)
+
+ def __init__ (self, name, *contents, **attrs) :
+ """
+ Construct tag with given name/attributes or contents.
+
+ The filtering rules desribed in process_contents/process_attrs apply.
+
+ >>> Tag('foo')
+ Tag('foo')
+ >>> Tag('foo', 'quux')
+ Tag('foo', 'quux')
+ >>> Tag('foo', 'quux', bar=5)
+ Tag('foo', 'quux', bar=5)
+ >>> Tag('foo', class_='ten')
+ Tag('foo', class='ten')
+ """
+
+ self.name = name
+
+ # store filtered/processed versions
+ self.contents = self.process_contents(contents)
+ self.attrs = self.process_attrs(attrs)
+
+ def __call__ (self, *contents, **attrs) :
+ """
+ Return a new Tag with this tag's attributes and contents, as well as the given attributes/contents.
+
+ The filtering rules desribed in process_contents/process_attrs apply.
+
+ >>> Tag('foo')('bar')
+ Tag('foo', 'bar')
+ >>> Tag('a', href='index.html')("Home")
+ Tag('a', 'Home', href='index.html')
+ >>> Tag('bar', None)(5, foo=None, class_='bar')
+ Tag('bar', 5, class='bar')
+ >>> Tag('a')('b')('c')(asdf=5)
+ Tag('a', 'b', 'c', asdf=5)
+ >>> t1 = Tag('a'); t2 = t1('b'); t1
+ Tag('a')
+ """
+
+ # merge attrs/contents
+ # XXX: new_attrs is not an iterator...
+ new_attrs = dict(_itertools.chain(self.attrs.iteritems(), attrs.iteritems()))
+ new_contents = _itertools.chain(self.contents, contents)
+
+ # build new tag
+ return Tag(self.name, *new_contents, **new_attrs)
+
+ @staticmethod
+ def format_attr (name, value) :
+ """
+ Format a single HTML tag attribute
+
+ >>> Tag.format_attr('name', 'value')
+ u'name="value"'
+ >>> Tag.format_attr('this', '<a"b>')
+ u'this="<a"b>"'
+ >>> Tag.format_attr('xx', 1337)
+ u'xx="1337"'
+ """
+
+ return u'%s="%s"' % (name, escape(unicode(value), True))
+
+ def render_attrs (self) :
+ """
+ Return the HTML attributes string
+
+ >>> Tag('x', foo=5, bar='<').render_attrs()
+ u'foo="5" bar="<"'
+ """
+
+ return " ".join(self.format_attr(n, v) for n, v in self.attrs.iteritems())
+
+ def render_contents (self, **render_opts) :
+ """
+ Render the contents of the tag as a series of indented lines, with given render_lines options for subtags
+
+ >>> list(Tag('x', 5).render_contents())
+ [u'5']
+ >>> list(Tag('x', 'line1', 'line2').render_contents())
+ [u'line1', u'line2']
+ >>> list(Tag('x', 'a', Tag('b', 'bb'), 'c').render_contents())
+ [u'a', u'<b>', u'\\tbb', u'</b>', u'c']
+ """
+
+ for content in self.contents :
+ if isinstance(content, IRenderable) :
+ # sub-tags
+ for line in content.render_raw_lines(**render_opts) :
+ yield line
+
+ else :
+ # escape raw values
+ yield escape(unicode(content))
+
+ def render_raw_lines (self, indent=u'\t') :
+ """
+ Render the tag and indented content
+
+ >>> list(Tag('xx', 'yy', zz='foo').render_raw_lines(indent=' '))
+ [u'<xx zz="foo">', u' yy', u'</xx>']
+ """
+
+ # render attr string, including preceding space
+ attrs_stuff = (" " + self.render_attrs()) if self.attrs else ""
+
+ if self.contents :
+ # tag with content
+ yield u"<%s%s>" % (self.name, attrs_stuff)
+
+ for line in self.render_contents(indent=indent) :
+ yield indent + line
+
+ yield u"</%s>" % (self.name, )
+
+ else :
+ # singleton tag
+ yield u"<%s%s />" % (self.name, attrs_stuff)
- elif isinstance(content, basestring) :
- # escape raw vlues
- yield escape(unicode(content))
+ def render_lines (self, indent=u'\t', newline=u'\n') :
+ """
+ Render full output lines with given newlines
- else :
- # treat it as subcontent
- for line in tag_subcontent(content) :
- yield line
+ >>> list(Tag('xx', 'yy').render_lines())
+ [u'<xx>\\n', u'\\tyy\\n', u'</xx>\\n']
+ """
-def tag (name, *content, **attrs) :
- attr_stuff = " " + " ".join(tag_attr(n, v) for n, v in attrs.iteritems()) if attrs else ""
+ for line in self.render_raw_lines(indent) :
+ yield line + newline
+
+ def render_unicode (self, **render_opts) :
+ """
+ Render full tag as a single unicode string
+
+ >>> Tag('xx', 'yy').render_unicode()
+ u'<xx>\\n\\tyy\\n</xx>\\n'
+ """
+
+ return "".join(self.render_lines(**render_opts))
+
+ def render_str (self, charset='ascii', **render_opts) :
+ """
+ Render full tag as an encoded string
+
+ >>> Tag('xx', 'yy').render_str()
+ '<xx>\\n\\tyy\\n</xx>\\n'
+ """
+
+ return self.render_unicode(**render_opts).encode(charset)
+
+ def render_out (self, stream, charset, **render_opts) :
+ """
+ Render output into the given stream, encoding using the given charset
+
+ >>> from StringIO import StringIO; buf = StringIO(); Tag('xx', 'yy').render_out(buf, 'ascii'); buf.getvalue()
+ '<xx>\\n\\tyy\\n</xx>\\n'
+ """
+
+ for line in self.render_lines(**render_opts) :
+ stream.write(line.encode(charset))
- if content and all(content) :
- # tag with content
- yield u"<%s%s>" % (name, attr_stuff)
-
- for c in content :
- for line in tag_content(c) :
- yield u"\t" + line
+ # default output
+ __str__ = render_str
+ __unicode__ = render_unicode
- yield u"</%s>" % (name, )
+ # default .render method
+ render = render_unicode
- else :
- # singleton tag
- yield u"<%s%s />" % (name, attr_stuff)
+ def __repr__ (self) :
+ return 'Tag(%s)' % ', '.join(
+ [
+ repr(self.name)
+ ] + [
+ repr(c) for c in self.contents
+ ] + [
+ '%s=%r' % (name, value) for name, value in self.attrs.iteritems()
+ ]
+ )
-def raw (data) :
- yield data
+class Text (IRenderable) :
+ """
+ Raw HTML text
+ """
+
+ def __init__ (self, line) :
+ """
+ Initialize to render as the given lines
+ """
+
+ self.lines = [line]
+
+ def render_raw_lines (self, indent=u'\t') :
+ return self.lines
class TagFactory (object) :
- def __getattr__ (self, name) :
- def build_tag (*content, **attrs) :
- return tag(name, *content, **attrs)
-
- return build_tag
-
-tags = TagFactory()
-
-def render_lines (tags, charset=None) :
- for line in tag_content(tags) :
- if charset :
- yield line.encode(charset)
-
- else :
- yield line
-
-def render (tags, charset=None) :
- data = u'\n'.join(render_lines(tags))
-
- if charset :
- return data.encode(charset)
-
- else :
- return data
-
-def render_out (tags, out, charset='utf-8') :
"""
- Write the rendered tags into the given output stream using the given encoding
+ Build Tags with names give as attribute names
"""
- for line in render_lines(tags, charset) :
- out.write(line + '\n')
+ def __getattr__ (self, name) :
+ """
+ Get a Tag object with the given name, but no contents
+ >>> TagFactory().a(href='bar')('quux')
+ Tag('a', 'quux', href='bar')
+ """
+
+ return Tag(name)
+
+# pretty names
+tag = Tag
+tags = TagFactory()
+raw = Text
+
+# testing
+if __name__ == '__main__' :
+ import doctest
+
+ doctest.testmod()
+