degal/html.py
changeset 53 14d73f544764
parent 52 3071d0709c4a
child 61 fad360dd01da
equal deleted inserted replaced
52:3071d0709c4a 53:14d73f544764
     1 """
     1 """
     2     Generating HTML tags
     2     Generating HTML tags
     3 """
     3 """
     4 
     4 
     5 from cgi import escape
     5 from cgi import escape
     6 
     6 import itertools as _itertools
     7 def tag_attr (name, value) :
     7 
     8     return u'%s="%s"' % (name.rstrip('_'), escape(value, True))
     8 class IRenderable (object) :
     9 
     9     """
    10 def tag_subcontent (subcontent) :
    10         Something that's renderable as the contents of a HTML tag
    11     if not subcontent :
    11     """
    12         # skip
    12 
    13         return
    13     def render_raw_lines (self, indent=u'\t') :
    14 
    14         """
    15     elif not isinstance(subcontent, basestring) and hasattr(subcontent, '__iter__') :
    15             Render the indented lines for tag and contents, without newlines
    16         # render each sub-item recursively
    16         """
    17         for sc in subcontent :
    17 
    18             for line in tag_subcontent(sc) :
    18         abstract
    19                 yield line
    19 
       
    20 class Tag (IRenderable) :
       
    21     """
       
    22         A HTML tag, with attributes and contents, which can a mixture of data and other renderables(tags).
       
    23 
       
    24         Provides various kinds of rendering output
       
    25     """
       
    26 
       
    27     @staticmethod
       
    28     def process_contents (contents) :
       
    29         """
       
    30             Postprocess contents iterable to return new list.
       
    31 
       
    32             Items that are None will be omitted from the return value.
       
    33 
       
    34             >>> Tag.process_contents([])
       
    35             []
       
    36             >>> Tag.process_contents([None])
       
    37             []
       
    38             >>> Tag.process_contents([u'foo'])
       
    39             [u'foo']
       
    40         """
       
    41 
       
    42         return [c for c in contents if c is not None]
       
    43 
       
    44     @staticmethod
       
    45     def process_attrs (attrs) :
       
    46         """
       
    47             Postprocess attributes.
       
    48 
       
    49             Key-value pairs where the value is None will be ommitted, and any trailing underscores in the key omitted.
       
    50 
       
    51             TODO: only remove one underscore
       
    52 
       
    53             >>> Tag.process_attrs(dict())
       
    54             {}
       
    55             >>> Tag.process_attrs(dict(foo='bar'))
       
    56             {'foo': 'bar'}
       
    57             >>> Tag.process_attrs(dict(class_='bar', frob=None))
       
    58             {'class': 'bar'}
       
    59         """
       
    60 
       
    61         return dict((k.rstrip('_'), v) for k, v in attrs.iteritems() if v is not None)
       
    62 
       
    63     def __init__ (self, name, *contents, **attrs) :
       
    64         """
       
    65             Construct tag with given name/attributes or contents.
       
    66 
       
    67             The filtering rules desribed in process_contents/process_attrs apply.
       
    68 
       
    69             >>> Tag('foo')
       
    70             Tag('foo')
       
    71             >>> Tag('foo', 'quux')
       
    72             Tag('foo', 'quux')
       
    73             >>> Tag('foo', 'quux', bar=5)
       
    74             Tag('foo', 'quux', bar=5)
       
    75             >>> Tag('foo', class_='ten')
       
    76             Tag('foo', class='ten')
       
    77         """
       
    78 
       
    79         self.name = name
       
    80 
       
    81         # store filtered/processed versions
       
    82         self.contents = self.process_contents(contents)
       
    83         self.attrs = self.process_attrs(attrs)
       
    84 
       
    85     def __call__ (self, *contents, **attrs) :
       
    86         """
       
    87             Return a new Tag with this tag's attributes and contents, as well as the given attributes/contents.
       
    88 
       
    89             The filtering rules desribed in process_contents/process_attrs apply.
       
    90 
       
    91             >>> Tag('foo')('bar')
       
    92             Tag('foo', 'bar')
       
    93             >>> Tag('a', href='index.html')("Home")
       
    94             Tag('a', 'Home', href='index.html')
       
    95             >>> Tag('bar', None)(5, foo=None, class_='bar')
       
    96             Tag('bar', 5, class='bar')
       
    97             >>> Tag('a')('b')('c')(asdf=5)
       
    98             Tag('a', 'b', 'c', asdf=5)
       
    99             >>> t1 = Tag('a'); t2 = t1('b'); t1
       
   100             Tag('a')
       
   101         """
       
   102 
       
   103         # merge attrs/contents
       
   104         # XXX: new_attrs is not an iterator...
       
   105         new_attrs = dict(_itertools.chain(self.attrs.iteritems(), attrs.iteritems()))
       
   106         new_contents = _itertools.chain(self.contents, contents)
       
   107         
       
   108         # build new tag
       
   109         return Tag(self.name, *new_contents, **new_attrs)
       
   110 
       
   111     @staticmethod
       
   112     def format_attr (name, value) :
       
   113         """
       
   114             Format a single HTML tag attribute
       
   115 
       
   116             >>> Tag.format_attr('name', 'value')
       
   117             u'name="value"'
       
   118             >>> Tag.format_attr('this', '<a"b>')
       
   119             u'this="&lt;a&quot;b&gt;"'
       
   120             >>> Tag.format_attr('xx', 1337)
       
   121             u'xx="1337"'
       
   122         """
       
   123 
       
   124         return u'%s="%s"' % (name, escape(unicode(value), True))
       
   125  
       
   126     def render_attrs (self) :
       
   127         """
       
   128             Return the HTML attributes string
       
   129 
       
   130             >>> Tag('x', foo=5, bar='<').render_attrs()
       
   131             u'foo="5" bar="&lt;"'
       
   132         """
       
   133 
       
   134         return " ".join(self.format_attr(n, v) for n, v in self.attrs.iteritems())
       
   135 
       
   136     def render_contents (self, **render_opts) :
       
   137         """
       
   138             Render the contents of the tag as a series of indented lines, with given render_lines options for subtags
       
   139 
       
   140             >>> list(Tag('x', 5).render_contents())
       
   141             [u'5']
       
   142             >>> list(Tag('x', 'line1', 'line2').render_contents())
       
   143             [u'line1', u'line2']
       
   144             >>> list(Tag('x', 'a', Tag('b', 'bb'), 'c').render_contents())
       
   145             [u'a', u'<b>', u'\\tbb', u'</b>', u'c']
       
   146         """
       
   147 
       
   148         for content in self.contents :
       
   149             if isinstance(content, IRenderable) :
       
   150                 # sub-tags
       
   151                 for line in content.render_raw_lines(**render_opts) :
       
   152                     yield line
       
   153             
       
   154             else :
       
   155                 # escape raw values
       
   156                 yield escape(unicode(content))
       
   157 
       
   158     def render_raw_lines (self, indent=u'\t') :
       
   159         """
       
   160             Render the tag and indented content
       
   161 
       
   162             >>> list(Tag('xx', 'yy', zz='foo').render_raw_lines(indent=' '))
       
   163             [u'<xx zz="foo">', u' yy', u'</xx>']
       
   164         """
       
   165 
       
   166         # render attr string, including preceding space
       
   167         attrs_stuff = (" " + self.render_attrs()) if self.attrs else ""
       
   168 
       
   169         if self.contents :
       
   170             # tag with content
       
   171             yield u"<%s%s>" % (self.name, attrs_stuff)
       
   172 
       
   173             for line in self.render_contents(indent=indent) :
       
   174                 yield indent + line
       
   175 
       
   176             yield u"</%s>" % (self.name, )
       
   177 
       
   178         else :
       
   179             # singleton tag
       
   180             yield u"<%s%s />" % (self.name, attrs_stuff)
    20     
   181     
    21     else :
   182     def render_lines (self, indent=u'\t', newline=u'\n') :
    22         yield subcontent
   183         """
    23 
   184             Render full output lines with given newlines
    24 def tag_content (content) :
   185 
    25     if not content:
   186             >>> list(Tag('xx', 'yy').render_lines())
    26         # no output
   187             [u'<xx>\\n', u'\\tyy\\n', u'</xx>\\n']
    27         return
   188         """
       
   189 
       
   190         for line in self.render_raw_lines(indent) :
       
   191             yield line + newline
       
   192 
       
   193     def render_unicode (self, **render_opts) :
       
   194         """
       
   195             Render full tag as a single unicode string
       
   196 
       
   197             >>> Tag('xx', 'yy').render_unicode()
       
   198             u'<xx>\\n\\tyy\\n</xx>\\n'
       
   199         """
       
   200 
       
   201         return "".join(self.render_lines(**render_opts))
       
   202 
       
   203     def render_str (self, charset='ascii', **render_opts) :
       
   204         """
       
   205             Render full tag as an encoded string
       
   206 
       
   207             >>> Tag('xx', 'yy').render_str()
       
   208             '<xx>\\n\\tyy\\n</xx>\\n'
       
   209         """
       
   210 
       
   211         return self.render_unicode(**render_opts).encode(charset)
       
   212 
       
   213     def render_out (self, stream, charset, **render_opts) :
       
   214         """
       
   215             Render output into the given stream, encoding using the given charset
       
   216 
       
   217             >>> from StringIO import StringIO; buf = StringIO(); Tag('xx', 'yy').render_out(buf, 'ascii'); buf.getvalue()
       
   218             '<xx>\\n\\tyy\\n</xx>\\n'
       
   219         """
       
   220 
       
   221         for line in self.render_lines(**render_opts) :
       
   222             stream.write(line.encode(charset))
    28     
   223     
    29     elif isinstance(content, basestring) :
   224     # default output
    30         # escape raw vlues
   225     __str__ = render_str
    31         yield escape(unicode(content))
   226     __unicode__ = render_unicode
    32 
   227 
    33     else :
   228     # default .render method
    34         # treat it as subcontent
   229     render = render_unicode
    35         for line in tag_subcontent(content) :
   230 
    36             yield line
   231     def __repr__ (self) :
    37 
   232         return 'Tag(%s)' % ', '.join(
    38 def tag (name, *content, **attrs) :
   233             [
    39     attr_stuff = " " + " ".join(tag_attr(n, v) for n, v in attrs.iteritems()) if attrs else ""
   234                 repr(self.name)
       
   235             ] + [
       
   236                 repr(c) for c in self.contents
       
   237             ] + [
       
   238                 '%s=%r' % (name, value) for name, value in self.attrs.iteritems()
       
   239             ]
       
   240         )
       
   241 
       
   242 class Text (IRenderable) :
       
   243     """
       
   244         Raw HTML text
       
   245     """
       
   246 
       
   247     def __init__ (self, line) :
       
   248         """
       
   249             Initialize to render as the given lines
       
   250         """
       
   251 
       
   252         self.lines = [line]
    40     
   253     
    41     if content and all(content) :
   254     def render_raw_lines (self, indent=u'\t') :
    42         # tag with content
   255         return self.lines
    43         yield u"<%s%s>" % (name, attr_stuff)
       
    44         
       
    45         for c in content :
       
    46             for line in tag_content(c) :
       
    47                 yield u"\t" + line
       
    48 
       
    49         yield u"</%s>" % (name, )
       
    50 
       
    51     else :
       
    52         # singleton tag
       
    53         yield u"<%s%s />" % (name, attr_stuff)
       
    54 
       
    55 def raw (data) :
       
    56     yield data
       
    57 
   256 
    58 class TagFactory (object) :
   257 class TagFactory (object) :
       
   258     """
       
   259         Build Tags with names give as attribute names
       
   260     """
       
   261 
    59     def __getattr__ (self, name) :
   262     def __getattr__ (self, name) :
    60         def build_tag (*content, **attrs) :
   263         """
    61             return tag(name, *content, **attrs)
   264             Get a Tag object with the given name, but no contents
    62 
   265 
    63         return build_tag
   266             >>> TagFactory().a(href='bar')('quux')
    64 
   267             Tag('a', 'quux', href='bar')
       
   268         """
       
   269 
       
   270         return Tag(name)
       
   271 
       
   272 # pretty names
       
   273 tag = Tag
    65 tags = TagFactory()
   274 tags = TagFactory()
    66 
   275 raw = Text
    67 def render_lines (tags, charset=None) :
   276 
    68     for line in tag_content(tags) :
   277 # testing
    69         if charset :
   278 if __name__ == '__main__' :
    70             yield line.encode(charset)
   279     import doctest
    71 
   280 
    72         else :
   281     doctest.testmod()
    73             yield line
   282 
    74 
       
    75 def render (tags, charset=None) :
       
    76     data = u'\n'.join(render_lines(tags))
       
    77 
       
    78     if charset :
       
    79         return data.encode(charset)
       
    80 
       
    81     else :
       
    82         return data
       
    83 
       
    84 def render_out (tags, out, charset='utf-8') :
       
    85     """
       
    86         Write the rendered tags into the given output stream using the given encoding
       
    87     """
       
    88 
       
    89     for line in render_lines(tags, charset) :
       
    90         out.write(line + '\n')
       
    91