terom@0: """
terom@0: Generating XHTML output from nested python objects
terom@0:
terom@0: XXX: use a 'real' XML builder?
terom@0:
terom@0: To use:
terom@0:
terom@0: >>> from html import tags
terom@0: >>> str(tags.a(href="http://www.google.com")("Google !"))
terom@0: '\\n\\tGoogle <this>!\\n\\n'
terom@0: """
terom@0:
terom@0: from cgi import escape
terom@0: import itertools as _itertools, types as _types
terom@0:
terom@0: class IRenderable (object) :
terom@0: """
terom@0: Something that's renderable as the contents of a HTML tag.
terom@0:
terom@0: This is just used by Container for rendering Tags as actual HTML, vs just plain strs as escaped data.
terom@0:
terom@0: Additionally, some str vs unicode vs file stuff..
terom@0: """
terom@0:
terom@0: def render_raw_lines (self, indent=u'\t') :
terom@0: """
terom@0: Render the indented lines for tag and contents, without newlines
terom@0: """
terom@0:
terom@0: abstract
terom@0:
terom@0: def render_lines (self, indent=u'\t', newline=u'\n') :
terom@0: """
terom@0: Render full output lines with given newlines
terom@0:
terom@0: >>> list(Tag('xx', 'yy').render_lines())
terom@0: [u'\\n', u'\\tyy\\n', u'\\n']
terom@0: """
terom@0:
terom@0: for line in self.render_raw_lines(indent=indent) :
terom@0: yield line + newline
terom@0:
terom@0: def render_unicode (self, **render_opts) :
terom@0: """
terom@0: Render full tag as a single unicode string
terom@0:
terom@0: >>> Tag('xx', 'yy').render_unicode()
terom@0: u'\\n\\tyy\\n\\n'
terom@0: """
terom@0:
terom@0: return "".join(self.render_lines(**render_opts))
terom@0:
terom@0: def render_str (self, encoding='ascii', **render_opts) :
terom@0: """
terom@0: Render full tag as an encoded string
terom@0:
terom@0: >>> Tag('xx', 'yy').render_str()
terom@0: '\\n\\tyy\\n\\n'
terom@0: """
terom@0:
terom@0: return self.render_unicode(**render_opts).encode(encoding)
terom@0:
terom@0: def render_out (self, stream, encoding=None, **render_opts) :
terom@0: """
terom@0: Render output into the given stream, encoding using the given encoding if given.
terom@0:
terom@0: >>> from StringIO import StringIO; buf = StringIO(); Tag('xx', 'yy').render_out(buf, 'ascii'); buf.getvalue()
terom@0: '\\n\\tyy\\n\\n'
terom@0: """
terom@0:
terom@0: for line in self.render_lines(**render_opts) :
terom@0: if encoding :
terom@0: line = line.encode(encoding)
terom@0:
terom@0: stream.write(line)
terom@0:
terom@0: def render_file (self, file, encoding=None, **render_opts) :
terom@0: """
terom@0: Render output to given file, overwriteing anything already there
terom@0: """
terom@0:
terom@0: self.render_out(file.open_write(encoding), **render_opts)
terom@0:
terom@0: # default output
terom@0: __str__ = render_str
terom@0: __unicode__ = render_unicode
terom@0:
terom@0: # default .render method
terom@0: render = render_unicode
terom@0:
terom@0: class Container (IRenderable) :
terom@0: """
terom@0: A container holds a sequence of other renderable items.
terom@0:
terom@0: This is just used as the superclass for Tag, and just serves to gives us useful handling for e.g. generators as
terom@0: tag contents (iterate through them instead of repr'ing them).
terom@0: """
terom@0:
terom@0: @classmethod
terom@0: def process_contents (cls, contents) :
terom@0: """
terom@0: Postprocess contents iterable to return new list.
terom@0:
terom@0: Items that are None will be omitted from the return value.
terom@0:
terom@0: Certain core sequence types will be recognized and flattened for output: tuples, lists, and generators.
terom@0:
terom@0: >>> list(Container.process_contents([]))
terom@0: []
terom@0: >>> list(Container.process_contents([None]))
terom@0: []
terom@0: >>> list(Container.process_contents([u'foo']))
terom@0: [u'foo']
terom@0: """
terom@0:
terom@0: for content in contents :
terom@0: if content is None :
terom@0: continue
terom@0:
terom@0: # Hardcoded list of special-case nested contents
terom@0: elif isinstance(content, (_types.TupleType, _types.ListType, _types.GeneratorType)) :
terom@0: for subcontent in cls.process_contents(content) :
terom@0: yield subcontent
terom@0:
terom@0: else :
terom@0: # normal, handle as IRenderable/unicode data
terom@0: yield content
terom@0:
terom@0: def __init__ (self, *contents) :
terom@0: """
terom@0: Construct this container with the given sub-items
terom@0: """
terom@0:
terom@0: # store postprocessed
terom@0: self.contents = list(self.process_contents(contents))
terom@0:
terom@0: def render_raw_lines (self, **render_opts) :
terom@0: """
terom@0: Render our contents as a series of non-indented lines, with the contents handling indentation themselves.
terom@0:
terom@0: >>> list(Container(5).render_raw_lines())
terom@0: [u'5']
terom@0: >>> list(Container('line1', 'line2').render_raw_lines())
terom@0: [u'line1', u'line2']
terom@0: >>> list(Container('a', Tag('b', 'bb'), 'c').render_raw_lines())
terom@0: [u'a', u'', u'\\tbb', u'', u'c']
terom@0: >>> list(Container(Tag('hr'), Tag('foo')('bar')).render_raw_lines())
terom@0: [u'
', u'', u'\\tbar', u'']
terom@0: """
terom@0:
terom@0: for content in self.contents :
terom@0: if isinstance(content, IRenderable) :
terom@0: # sub-items
terom@0: for line in content.render_raw_lines(**render_opts) :
terom@0: yield line
terom@0:
terom@0: else :
terom@0: # escape raw values
terom@0: yield escape(unicode(content))
terom@0:
terom@0: def __repr__ (self) :
terom@0: return 'Container(%s)' % ', '.join(repr(c) for c in self.contents)
terom@0:
terom@0: class Tag (Container) :
terom@0: """
terom@0: A HTML tag, with attributes and contents, which can a mixture of data and other renderables(tags).
terom@0:
terom@0: This is the core object, and the ~only one you really need to pay attention to.
terom@0:
terom@0: Provides various kinds of rendering output via IRenderable.
terom@0: """
terom@0:
terom@0: @staticmethod
terom@0: def process_attrs (attrs) :
terom@0: """
terom@0: Postprocess attributes.
terom@0:
terom@0: Key-value pairs where the value is None will be ommitted, and any trailing underscores in the key omitted.
terom@0:
terom@0: TODO: only remove one underscore
terom@0:
terom@0: >>> dict(Tag.process_attrs(dict()))
terom@0: {}
terom@0: >>> dict(Tag.process_attrs(dict(foo='bar')))
terom@0: {'foo': 'bar'}
terom@0: >>> dict(Tag.process_attrs(dict(class_='bar', frob=None)))
terom@0: {'class': 'bar'}
terom@0: """
terom@0:
terom@0: return ((k.rstrip('_'), v) for k, v in attrs.iteritems() if v is not None)
terom@0:
terom@5: def __init__ (self, _name, *contents, **attrs) :
terom@0: """
terom@0: Construct tag with given name/attributes or contents.
terom@0:
terom@0: The filtering rules desribed in process_contents/process_attrs apply.
terom@0:
terom@0: >>> Tag('foo')
terom@0: Tag('foo')
terom@0: >>> Tag('foo', 'quux')
terom@0: Tag('foo', 'quux')
terom@0: >>> Tag('foo', 'quux', bar=5)
terom@0: Tag('foo', 'quux', bar=5)
terom@0: >>> Tag('foo', class_='ten')
terom@0: Tag('foo', class='ten')
terom@5: >>> Tag('bar', name='foo')
terom@5: Tag('bar', name='foo')
terom@0: """
terom@0:
terom@0: # store contents as container
terom@0: super(Tag, self).__init__(*contents)
terom@0:
terom@0: # store postprocessed stuff
terom@5: self.name = _name
terom@0: self.attrs = dict(self.process_attrs(attrs))
terom@0:
terom@0: def __call__ (self, *contents, **attrs) :
terom@0: """
terom@0: Return a new Tag with this tag's attributes and contents, as well as the given attributes/contents.
terom@0:
terom@0: The filtering rules desribed in process_contents/process_attrs apply.
terom@0:
terom@0: >>> Tag('foo')('bar')
terom@0: Tag('foo', 'bar')
terom@0: >>> Tag('a', href='index.html')("Home")
terom@0: Tag('a', 'Home', href='index.html')
terom@0: >>> Tag('bar', None)(5, foo=None, class_='bar')
terom@0: Tag('bar', 5, class='bar')
terom@0: >>> Tag('a')('b')('c')(asdf=5)
terom@0: Tag('a', 'b', 'c', asdf=5)
terom@0: >>> t1 = Tag('a'); t2 = t1('b'); t1
terom@0: Tag('a')
terom@0: """
terom@0:
terom@0: # merge attrs/contents
terom@0: # XXX: new_attrs is not an iterator...
terom@0: new_attrs = dict(_itertools.chain(self.attrs.iteritems(), attrs.iteritems()))
terom@0: new_contents = _itertools.chain(self.contents, contents)
terom@0:
terom@0: # build new tag
terom@0: return Tag(self.name, *new_contents, **new_attrs)
terom@0:
terom@0: @staticmethod
terom@0: def format_attr (name, value) :
terom@0: """
terom@0: Format a single HTML tag attribute
terom@0:
terom@0: >>> Tag.format_attr('name', 'value')
terom@0: u'name="value"'
terom@0: >>> Tag.format_attr('this', '')
terom@0: u'this="<a"b>"'
terom@0: >>> Tag.format_attr('xx', 1337)
terom@0: u'xx="1337"'
terom@0: """
terom@0:
terom@0: return u'%s="%s"' % (name, escape(unicode(value), True))
terom@0:
terom@0: def render_attrs (self) :
terom@0: """
terom@0: Return the HTML attributes string
terom@0:
terom@0: >>> Tag('x', foo=5, bar='<').render_attrs()
terom@0: u'foo="5" bar="<"'
terom@0: """
terom@0:
terom@8: return " ".join(self.format_attr(n, v) for n, v in self.attrs.iteritems() if not n.startswith('_'))
terom@0:
terom@0: def render_raw_lines (self, indent=u'\t') :
terom@0: """
terom@0: Render the tag and indented content
terom@0:
terom@0: >>> list(Tag('xx', 'yy', zz='foo').render_raw_lines(indent=' '))
terom@0: [u'', u' yy', u'']
terom@0: """
terom@8:
terom@8: # opts
terom@8: selfclosing = self.attrs.get('_selfclosing')
terom@8: whitespace_sensitive = self.attrs.get('_whitespace_sensitive')
terom@0:
terom@0: # render attr string, including preceding space
terom@0: attrs_stuff = (" " + self.render_attrs()) if self.attrs else ""
terom@0:
terom@8: if self.contents or selfclosing is False:
terom@8:
terom@8: if not whitespace_sensitive :
terom@8: # wrapping tags
terom@8: yield u"<%s%s>" % (self.name, attrs_stuff)
terom@8:
terom@8: # subcontents
terom@8: for line in super(Tag, self).render_raw_lines(indent=indent) :
terom@8: yield indent + line
terom@8:
terom@8: yield u"%s>" % (self.name, )
terom@0:
terom@8: else :
terom@8: # whole tag
terom@8: yield u"<%s%s>%s%s>" % (self.name, attrs_stuff, ''.join(super(Tag, self).render_raw_lines(indent=indent)), self.name)
terom@0: else :
terom@0: # singleton tag
terom@0: yield u"<%s%s />" % (self.name, attrs_stuff)
terom@0:
terom@0:
terom@0: def __repr__ (self) :
terom@0: return 'Tag(%s)' % ', '.join(
terom@0: [
terom@0: repr(self.name)
terom@0: ] + [
terom@0: repr(c) for c in self.contents
terom@0: ] + [
terom@0: '%s=%r' % (name, value) for name, value in self.attrs.iteritems()
terom@0: ]
terom@0: )
terom@0:
terom@0: class Text (IRenderable) :
terom@0: """
terom@0: Raw HTML text
terom@0: """
terom@0:
terom@0: def __init__ (self, line) :
terom@0: """
terom@0: Initialize to render as the given lines
terom@0: """
terom@0:
terom@0: self.lines = [line]
terom@0:
terom@0: def render_raw_lines (self, indent=u'\t') :
terom@0: return self.lines
terom@0:
terom@0: class Document (IRenderable) :
terom@0: """
terom@0: A full XHTML document with XML header, doctype, head and body.
terom@0:
terom@0: XXX: current rendering is a bit of a kludge
terom@0:
terom@0:
terom@0:
terom@0:
terom@0:
terom@0:
terom@0: ...
terom@0:
terom@0:
terom@0: ...
terom@0:
terom@0:
terom@0: """
terom@0:
terom@0: def __init__ (self,
terom@0: head, body,
terom@0: xml_version='1.0', xml_encoding='utf-8',
terom@0: doctype='html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"',
terom@0: html_xmlns='http://www.w3.org/1999/xhtml', html_lang='en'
terom@0: ) :
terom@0: # store
terom@0: self.xml_version = xml_version
terom@0: self.xml_encoding = xml_encoding
terom@0: self.doctype = doctype
terom@0:
terom@0: # build the document
terom@0: self.document = Tag('html', **{'xmlns': html_xmlns, 'xml:lang': html_lang})(
terom@0: Tag('head', head),
terom@0: Tag('body', body),
terom@0: )
terom@0:
terom@0: def render_raw_lines (self, **render_opts) :
terom@0: """
terom@0: Render the two header lines, and then the document
terom@0: """
terom@0:
terom@0: yield '' % (self.xml_version, self.xml_encoding)
terom@0: yield '' % (self.doctype)
terom@0:
terom@0: for line in self.document.render_raw_lines(**render_opts) :
terom@0: yield line
terom@0:
terom@0: def _check_encoding (self, encoding) :
terom@0: if encoding and encoding != self.xml_encoding :
terom@0: raise ValueError("encoding mismatch: %r should be %r" % (encoding, self.xml_encoding))
terom@0:
terom@0: def render_str (self, encoding=None, **render_opts) :
terom@0: """
terom@0: Wrap render_str to verify that the right encoding is used
terom@0: """
terom@0:
terom@0: self._check_encoding(encoding)
terom@0:
terom@0: return super(XHTMLDocument, self).render_str(self.xml_encoding, **render_opts)
terom@0:
terom@0: def render_out (self, stream, encoding=None, **render_opts) :
terom@0: """
terom@0: Wrap render_out to verify that the right encoding is used
terom@0: """
terom@0:
terom@0: self._check_encoding(encoding)
terom@0:
terom@0: return super(XHTMLDocument, self).render_out(stream, self.xml_encoding, **render_opts)
terom@0:
terom@0: class TagFactory (object) :
terom@0: """
terom@0: Build Tags with names give as attribute names
terom@50:
terom@50: >>> str(TagFactory().raw("><")
terom@50: '><'
terom@0: """
terom@58:
terom@58: # full XHTML document
terom@58: document = Document
terom@50:
terom@50: # raw HTML
terom@50: raw = Text
terom@0:
terom@0: def __getattr__ (self, name) :
terom@0: """
terom@0: Get a Tag object with the given name, but no contents
terom@0:
terom@0: >>> TagFactory().a(href='bar')('quux')
terom@0: Tag('a', 'quux', href='bar')
terom@0: """
terom@0:
terom@0: return Tag(name)
terom@0:
terom@0: # pretty names
terom@0: container = Container
terom@0: tag = Tag
terom@0: tags = TagFactory()
terom@0: raw = Text
terom@0: document = Document
terom@0:
terom@0: # testing
terom@0: if __name__ == '__main__' :
terom@0: import doctest
terom@0:
terom@0: doctest.testmod()
terom@0: