degal/html.py
changeset 53 14d73f544764
parent 52 3071d0709c4a
child 61 fad360dd01da
--- 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="&lt;a&quot;b&gt;"'
+            >>> 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="&lt;"'
+        """
+
+        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()
+