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