Simple DSL for creating html in Python


In Clojure world we have hiccup for creating html:

[:div.top
  [:h1 "Hello world]
  [:p hello-text]]

In JS world we have JSX (it’s not internal DSL, but it’s relevant):

var html = (
    <div className="top">
        <h1>Hello world</h1>
        <p>{helloText}</p>
    </div>
);

But in Python we don’t have similar DSL (upd: actually we have: lxml.E, pyxl, Dominate and The DOM), and isn’t it be cool (actually it isn’t, I don’t recommend to do something like this, it’s just an experiment) to write something like this:

h.div(klass='top')[
    h.h1["Hello word"],
    h.p[hello_text]]

Let’s start with simplest part, implement ability to call h.p and h.div, for this I’ll use magic of metaclasses and __getattr__:

class hBase(type):
    def __getattr__(cls, name):
        return cls(name)
        
        
class h(metaclass=hBase):
    def __init__(self, name):
        self._name = name
        
    def __str__(self):
        return '<{name}></{name}>'.format(name=self._name)
        
    def __repr__(self):
        return str(self)
        
        
In [3]: h.div
Out [3]: <div></div>

It’s very simple, now is the time to add ability to define childs for html element with h.div[h.h2, h.p], magic of __getitem__ will help me:

class hBase(type):
    def __getattr__(cls, name):
        return cls(name)


class h(metaclass=hBase):
    def __init__(self, name, childs=None):
        self._name = name
        self._childs = childs
        
    def __getitem__(self, childs):
        if not hasattr(childs, '__iter__'):
            childs = [childs]
        return type(self)(self._name, childs)
        
    def _format_childs(self):
        if self._childs is None:
            return ''
        if isinstance(self._childs, str):
            return self._childs
        else:
            return '\n'.join(map(str, self._childs))
        
    def __str__(self):
        return '<{name}>{childs}</{name}>'.format(
            name=self._name,
            childs=self._format_childs())
            
    def __repr__(self):
        return str(self)


In [7]: h.div[h.h2['Hello world'], h.p['Just text.']]
Out [7]:
<div><h2>Hello world</h2>
<p>Just text.</p></div>

Cool, it works! So now let’s add ability to define attributes with h.div(id="my-id"), but before I need to notice that in python we not allowed to use class as a name of argument, so I’ll use klass instead. So here I’ll use magic of __call__:

class hBase(type):
    def __getattr__(cls, name):
        return cls(name)


class h(metaclass=hBase):
    def __init__(self, name, childs=None, attrs=None):
        self._name = name
        self._childs = childs
        self._attrs = attrs
        
    def __getitem__(self, childs):
        if not hasattr(childs, '__iter__'):
            childs = [childs]
        return type(self)(self._name, childs, self._attrs)
        
    def __call__(self, **attrs):
        return type(self)(self._name, self._childs, attrs)
        
    def _format_attr(self, name, val):
         if name == 'klass':
             name = 'class'
         return '{}="{}"'.format(name, str(val).replace('"', '\"'))
        
    def _format_attrs(self):
        if self._attrs:
            return ' ' + ' '.join([self._format_attr(name, val)
                                   for name, val in self._attrs.items()])
        else:
            return ''
        
    def _format_childs(self):
        if self._childs is None:
            return ''
        if isinstance(self._childs, str):
            return self._childs
        else:
            return '\n'.join(map(str, self._childs))
        
    def __str__(self):
        return '<{name}{attrs}>{childs}</{name}>'.format(
            name=self._name,
            attrs=self._format_attrs(),
            childs=self._format_childs())
            
    def __repr__(self):
        return str(self)
            
            
In [19]: hello_text = 'Hi!'
In [20]: h.div(klass='top')[
          h.h1["Hello word"],
          h.p[hello_text]]
Out [20]:
<div class="top"><h1>Hello word</h1>
<p>Hi!</p></div>

Yep, it’s working, and it’s a simple DSL/template language just in 44 lines of code, thanks to Python magic methods. It can be used in more complex situations, for example – blog page:

from collections import namedtuple


BlogPost = namedtuple('BlogPost', ('title', 'text'))
posts = [BlogPost('Title {}'.format(n),
                  'Text {}'.format(n))
         for n in range(5)]

In [30]: h.body[
    h.div(klass='header')[
        h.h1['Web page'],
        h.img(klass='logo', src='logo.png')],
    h.div(klass='posts')[(
        h.article[
            h.h2(klass='title')[post.title],
            post.text]
        for post in posts)]]
Out [30]:
<body><div class="header"><h1>Web page</h1>
<img class="logo" src="logo.png"></img></div>
<div class="posts"><article><h2 class="title">Title 0</h2>
Text 0</article>
<article><h2 class="title">Title 1</h2>
Text 1</article>
<article><h2 class="title">Title 2</h2>
Text 2</article>
<article><h2 class="title">Title 3</h2>
Text 3</article>
<article><h2 class="title">Title 4</h2>
Text 4</article></div></body>

And after that little experiment I have to say that everything is a LISP if you’re brave enough =)



comments powered by Disqus