terom@52: """ terom@61: Generating XHTML output terom@61: terom@61: XXX: use a 'real' XML builder? terom@52: """ terom@52: terom@52: from cgi import escape terom@80: import itertools as _itertools, types as _types 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@61: def render_lines (self, indent=u'\t', newline=u'\n') : terom@61: """ terom@61: Render full output lines with given newlines terom@61: terom@61: >>> list(Tag('xx', 'yy').render_lines()) terom@61: [u'\\n', u'\\tyy\\n', u'\\n'] terom@61: """ terom@61: terom@77: for line in self.render_raw_lines(indent=indent) : terom@61: yield line + newline terom@61: terom@61: def render_unicode (self, **render_opts) : terom@61: """ terom@61: Render full tag as a single unicode string terom@61: terom@61: >>> Tag('xx', 'yy').render_unicode() terom@61: u'\\n\\tyy\\n\\n' terom@61: """ terom@61: terom@61: return "".join(self.render_lines(**render_opts)) terom@61: terom@61: def render_str (self, encoding='ascii', **render_opts) : terom@61: """ terom@61: Render full tag as an encoded string terom@61: terom@61: >>> Tag('xx', 'yy').render_str() terom@61: '\\n\\tyy\\n\\n' terom@61: """ terom@61: terom@61: return self.render_unicode(**render_opts).encode(encoding) terom@61: terom@69: def render_out (self, stream, encoding=None, **render_opts) : terom@61: """ terom@69: Render output into the given stream, encoding using the given encoding if given. terom@61: terom@61: >>> from StringIO import StringIO; buf = StringIO(); Tag('xx', 'yy').render_out(buf, 'ascii'); buf.getvalue() terom@61: '\\n\\tyy\\n\\n' terom@61: """ terom@61: terom@61: for line in self.render_lines(**render_opts) : terom@69: if encoding : terom@69: line = line.encode(encoding) terom@69: terom@69: stream.write(line) terom@61: terom@69: def render_file (self, file, encoding=None, **render_opts) : terom@69: """ terom@144: Render output to given File, overwriteing anything already there terom@69: """ terom@69: terom@77: self.render_out(file.open_write(encoding), **render_opts) terom@69: terom@61: # default output terom@61: __str__ = render_str terom@61: __unicode__ = render_unicode terom@61: terom@61: # default .render method terom@61: render = render_unicode terom@61: terom@78: class Container (IRenderable) : terom@78: """ terom@78: A container holds a sequence of other renderable items. terom@78: """ terom@78: terom@78: @classmethod terom@78: def process_contents (cls, contents) : terom@78: """ terom@78: Postprocess contents iterable to return new list. terom@78: terom@78: Items that are None will be omitted from the return value. terom@78: terom@80: Certain core sequence types will be recognized and flattened for output: tuples, lists, and generators. terom@80: terom@78: >>> list(Container.process_contents([])) terom@78: [] terom@78: >>> list(Container.process_contents([None])) terom@78: [] terom@78: >>> list(Container.process_contents([u'foo'])) terom@78: [u'foo'] terom@78: """ terom@78: terom@78: for content in contents : terom@78: if content is None : terom@78: continue terom@80: terom@80: # Hardcoded list of special-case nested contents terom@80: elif isinstance(content, (_types.TupleType, _types.ListType, _types.GeneratorType)) : terom@80: for subcontent in cls.process_contents(content) : terom@80: yield subcontent terom@80: terom@78: else : terom@80: # normal, handle as IRenderable/unicode data terom@78: yield content terom@78: terom@78: def __init__ (self, *contents) : terom@78: """ terom@78: Construct this container with the given sub-items terom@78: """ terom@78: terom@78: # store postprocessed terom@78: self.contents = list(self.process_contents(contents)) terom@78: terom@78: def render_raw_lines (self, **render_opts) : terom@78: """ terom@78: Render our contents as a series of non-indented lines, with the contents handling indentation themselves. terom@78: terom@78: >>> list(Container(5).render_raw_lines()) terom@78: [u'5'] terom@78: >>> list(Container('line1', 'line2').render_raw_lines()) terom@78: [u'line1', u'line2'] terom@78: >>> list(Container('a', Tag('b', 'bb'), 'c').render_raw_lines()) terom@78: [u'a', u'', u'\\tbb', u'', u'c'] terom@78: >>> list(Container(Tag('hr'), Tag('foo')('bar')).render_raw_lines()) terom@78: [u'
', u'', u'\\tbar', u''] terom@78: """ terom@78: terom@78: for content in self.contents : terom@78: if isinstance(content, IRenderable) : terom@78: # sub-items terom@78: for line in content.render_raw_lines(**render_opts) : terom@78: yield line terom@78: terom@78: else : terom@78: # escape raw values terom@78: yield escape(unicode(content)) terom@78: terom@78: def __repr__ (self) : terom@78: return 'Container(%s)' % ', '.join(repr(c) for c in self.contents) terom@78: terom@78: class Tag (Container) : 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_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@78: >>> dict(Tag.process_attrs(dict())) terom@53: {} terom@78: >>> dict(Tag.process_attrs(dict(foo='bar'))) terom@53: {'foo': 'bar'} terom@78: >>> dict(Tag.process_attrs(dict(class_='bar', frob=None))) terom@53: {'class': 'bar'} terom@53: """ terom@53: terom@78: return ((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@78: terom@78: # store contents as container terom@78: super(Tag, self).__init__(*contents) terom@78: terom@78: # store postprocessed stuff terom@53: self.name = name terom@78: self.attrs = dict(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_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@78: # wrapping tags terom@53: yield u"<%s%s>" % (self.name, attrs_stuff) terom@78: terom@78: # subcontents terom@78: for line in super(Tag, self).render_raw_lines(indent=indent) : terom@53: yield indent + line terom@53: terom@53: yield u"" % (self.name, ) terom@53: terom@53: else : terom@53: # singleton tag terom@53: yield u"<%s%s />" % (self.name, attrs_stuff) terom@52: 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@61: class XHTMLDocument (IRenderable) : terom@61: """ terom@78: A full XHTML document with XML header, doctype, head and body. terom@78: terom@78: XXX: current rendering is a bit of a kludge terom@78: terom@61: terom@61: terom@61: terom@61: terom@61: terom@61: ... terom@61: terom@61: terom@61: ... terom@61: terom@61: terom@61: """ terom@61: terom@61: def __init__ (self, terom@61: head, body, terom@61: xml_version='1.0', xml_encoding='utf-8', terom@61: doctype='html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"', terom@61: html_xmlns='http://www.w3.org/1999/xhtml', html_lang='en' terom@61: ) : terom@61: # store terom@61: self.xml_version = xml_version terom@61: self.xml_encoding = xml_encoding terom@61: self.doctype = doctype terom@61: terom@61: # build the document terom@61: self.document = Tag('html', **{'xmlns': html_xmlns, 'xml:lang': html_lang})( terom@61: Tag('head', head), terom@61: Tag('body', body), terom@61: ) terom@61: terom@61: def render_raw_lines (self, **render_opts) : terom@61: """ terom@61: Render the two header lines, and then the document terom@61: """ terom@61: terom@61: yield '' % (self.xml_version, self.xml_encoding) terom@61: yield '' % (self.doctype) terom@61: terom@61: for line in self.document.render_raw_lines(**render_opts) : terom@61: yield line terom@61: terom@61: def _check_encoding (self, encoding) : terom@61: if encoding and encoding != self.xml_encoding : terom@76: raise ValueError("encoding mismatch: %r should be %r" % (encoding, self.xml_encoding)) terom@61: terom@61: def render_str (self, encoding=None, **render_opts) : terom@61: """ terom@61: Wrap render_str to verify that the right encoding is used terom@61: """ terom@61: terom@61: self._check_encoding(encoding) terom@61: terom@61: return super(XHTMLDocument, self).render_str(self.xml_encoding, **render_opts) terom@61: terom@61: def render_out (self, stream, encoding=None, **render_opts) : terom@61: """ terom@61: Wrap render_out to verify that the right encoding is used terom@61: """ terom@61: terom@61: self._check_encoding(encoding) terom@61: terom@61: return super(XHTMLDocument, self).render_out(stream, self.xml_encoding, **render_opts) terom@61: 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@78: container = Container 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: