Richard Jones' Log: Simple, elegant HTML generation

Wed, 16 Sep 2009

OK, I looked. I searched. I didn't find. So here you go...

from cgi import escape
class HTML(object):
    '''Easily generate HTML.

        >>> h = HTML()
        >>> p = h.p('hello, world!')
        >>> p.text('more text')
        >>> with h.table(border='1', newlines=True):
        ...     for i in range(2):
        ...         with h.tr:
        ...             h.td('he<l>lo', a='"foo"')
        ...             h.td('there')
        ... 
        >>> print h
        <p>hello, world!more text</p>
        <table border="1">
        <tr><td a="&quot;foo&quot;">he&lt;l&gt;lo</td><td>there</td></tr>
        <tr><td a="&quot;foo&quot;">he&lt;l&gt;lo</td><td>there</td></tr>
        </table>

    '''
    def __init__(self, name=None, stack=None):
        self.name = name
        self.content = []
        self.attrs = {}
        # insert newlines between content?
        self.newlines = False
        if stack is None:
            stack = [self]
        self.stack = stack
    def __getattr__(self, name):
        # adding a new tag or newline
        if name == 'newline':
            e = '\n'
        else:
            e = HTML(name, self.stack)
        self.stack[-1].content.append(e)
        return e
    def text(self, text):
        # adding text
        self.content.append(escape(text))
    def __call__(self, *content, **kw):
        # customising a tag with content or attributes
        if content:
            self.content = map(escape, content)
        if 'newlines' in kw:
            # special-case to allow control over newlines
            self.newlines = kw.pop('newlines')
        for k in kw:
            self.attrs[k] = escape(kw[k]).replace('"', '"')
        return self
    def __enter__(self):
        # we're now adding tags to me!
        self.stack.append(self)
        return self
    def __exit__(self, exc_type, exc_value, exc_tb):
        # we're done adding tags to me!
        self.stack.pop()
    def __str__(self):
        # turn me and my content into text
        join = '\n' if self.newlines else ''
        if self.name is None:
            return join.join(map(str, self.content))
        a = ['%s="%s"'%i for i in self.attrs.items()]
        l = [self.name] + a
        s = '<%s>%s'%(' '.join(l), join)
        if self.content:
            s += join.join(map(str, self.content))
            s += join + '</%s>'%self.name
        return s

There was an escaping error in this blog post (the < and > around the "l" first in "hello") which I didn't catch before Planet Python grabbed my feed, so it's wrong on there but correct here. Hrm.

Also, look, ma! A ternary expression! My first in Python!

Did you like "join.join"? Heh. I know...