Skip to content

Instantly share code, notes, and snippets.

@kstep
Created August 15, 2012 13:12
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save kstep/3360002 to your computer and use it in GitHub Desktop.
Save kstep/3360002 to your computer and use it in GitHub Desktop.
Simple Python HTML template DSL
__all__ = []
def export(value):
__all__.append(value.__name__)
return value
import sys
module = sys.modules[__name__]
class Context(object):
level = 0
indent = ' '
def __enter__(self):
self.level += 1
return self.__lshift__
def __exit__(self, type_, value, btrace):
self.level -= 1
def __lshift__(self, data):
print(self.indent * self.level + str(data))
return self
class StrContext(Context):
data = ''
def __lshift__(self, data):
self.data += self.indent * self.level + str(data) + "\n"
return self
def __str__(self):
return self.data
context = StrContext()
__all__.append('context')
def build_attrs(attrs):
return ' ' + ' '.join(
'%s="%s"' % (
name.replace("_", "-").strip('-'),
(lambda v: ' '.join(v) if hasattr(v, '__iter__') else v)(
(value() if hasattr(value, '__call__') else value)
))
for (name, value) in attrs.iteritems() if value is not None
) if attrs else ''
@export
def css(**attrs):
return ' '.join("%s: %s;" % (name.replace('_', '-'), value)
for (name, value) in attrs.iteritems())
class TAG(object):
name = ''
attrs = {}
class __metaclass__(type):
def __enter__(cls):
return cls().__enter__()
def __exit__(cls, type_, value, btrace):
context.level -= 1
context << '</%s>' % cls.name
def __getattr__(cls, name):
return cls[name.replace('_', '-')]
def __getitem__(cls, name):
self = cls()
self.attrs['class'] = [name]
return self
def __enter__(self):
context << '<%s%s>' % (self.name, build_attrs(self.attrs))
context.level += 1
return context
def __exit__(self, type_, value, btrace):
context.level -= 1
context << '</%s>' % self.name
def __call__(self, _content=None, **attrs):
if attrs:
self.attrs.update(attrs)
if _content is not None:
context << '<%s%s>%s</%s>' % (self.name, build_attrs(self.attrs), _content, self.name)
return self
def __init__(self, _content=None, **attrs):
self.attrs = self.attrs.copy()
self(_content, **attrs)
def __getattr__(self, name):
return self[name.replace('_', '-')]
def __getitem__(self, name):
self.attrs.setdefault('class', []).append(name)
return self
class EMPTYTAG(object):
name = ''
attrs = {}
def __new__(cls, **attrs):
if attrs:
_attrs = cls.attrs.copy()
_attrs.update(attrs)
else:
_attrs = cls.attrs
context << '<%s%s />' % (cls.name, build_attrs(_attrs))
@export
class COMMENT(TAG):
def __enter__(self):
context << '<!-- '
return context
def __exit__(self, type_, value, traceback):
context << ' -->'
@export
class HTML(TAG):
name = 'html'
doctype = ''
def __enter__(self):
context << '''<!DOCTYPE %s>''' % (self.doctype)
return super(HTML, self).__enter__()
@export
class HTML5(HTML):
doctype = 'HTML'
simple_tags = ('head body title ' + # Main elements
'div p blockquote ' + # Blocks
'h1 h2 h3 h4 h5 h6 ' + # Headers
'u b i s a em strong span font ' + # Inline markup
'del ins ' + # Annotation
'ul ol li dd dt dl ' + # Lists
'article section nav aside ' + # HTML5
'audio video object embed param ' + # Media
'fieldset legend button textarea label select option ' + # Forms
'table thead tbody tr th td caption ' + # Tables
''
)
empty_tags = 'meta link br hr input'
for tag in simple_tags.split():
name = tag.upper()
__all__.append(name)
setattr(module, name, type(name, (TAG,), {'name': tag}))
for tag in empty_tags.split():
name = tag.upper()
__all__.append(name)
setattr(module, name, type(name, (EMPTYTAG,), {'name': tag}))
@export
class SCRIPT(TAG):
name = 'script'
attrs = {'type': 'text/javascript'}
@export
class CSS(LINK):
attrs = {'type': 'text/css', 'rel': 'stylesheet'}
def __new__(cls, href):
super(CSS, cls).__new__(cls, href=href)
@export
class JS(SCRIPT):
def __init__(self, src):
super(JS, self).__init__('', src=src)
@export
class FORM(TAG):
name = 'form'
attrs = {'method': 'POST'}
def __init__(self, action='', **attrs):
super(FORM, self).__init__(action=action, **attrs)
@kstep
Copy link
Author

kstep commented Aug 15, 2012

Example markup:

from html import *

def layout():
    def head(title, styles=[], scripts=[], meta={}, http={}, prop={}):
        with HEAD:
            TITLE(title)

            for style in styles:
                CSS(style)

            for script in scripts:
                JS(script)

            for name, value in meta:
                META(name=name, content=value)

            for name, value in http:
                META(http_equiv=name, content=value)

            for name, value in prop:
                META(property=name, content=value)

    def field(id, name=None, value=None, label=None, type='text', help=None):
        with DIV.control_group:
            if label is not None:
                LABEL.control_label(label, for_=id)

            with DIV.controls:
                INPUT(id=id, type=type, name=name, value=value)
                if help is not None:
                    P.help_block(help)

    with HTML5:
        head('Page title',
                styles=['http://netdna.bootstrapcdn.com/twitter-bootstrap/2.0.4/css/bootstrap-combined.min.css'],
                scripts=['http://code.jquery.com/jquery.min.js',
                    'http://netdna.bootstrapcdn.com/twitter-bootstrap/2.0.4/js/bootstrap.min.js'])

        with BODY.main:
            with DIV.content(id='content'):
                H1('Hello, world!')

                with FORM('/my-page').well.form_horizontal, FIELDSET:
                    field(id='input01', label='Text input', help='This is a help text.')
                    field(id='input02', label='Checkbox', type='checkbox')

                    with DIV.control_group:
                        LABEL.control_label('Select list', for_='input03')

                        with DIV.controls, SELECT(id='input03'):
                            for option in xrange(1, 9):
                                OPTION(option)

                    with DIV.form_actions:
                        BUTTON(type='submit').btn.btn_primary('Save changes')
                        BUTTON.btn('Cancel')

layout()
print(context)

Output:

<!DOCTYPE HTML>
<html>
  <head>
    <title>Page title</title>
    <link href="http://netdna.bootstrapcdn.com/twitter-bootstrap/2.0.4/css/bootstrap-combined.min.css" type="text/css" rel="stylesheet" />
    <script src="http://code.jquery.com/jquery.min.js" type="text/javascript"></script>
    <script src="http://netdna.bootstrapcdn.com/twitter-bootstrap/2.0.4/js/bootstrap.min.js" type="text/javascript"></script>
  </head>
  <body class="main">
    <div class="content" id="content">
      <h1>Hello, world!</h1>
      <form action="/my-page" method="POST" class="well form-horizontal">
        <fieldset>
          <div class="control-group">
            <label for="input01" class="control-label">Text input</label>
            <div class="controls">
              <input type="text" id="input01" />
              <p class="help-block">This is a help text.</p>
            </div>
          </div>
          <div class="control-group">
            <label for="input02" class="control-label">Checkbox</label>
            <div class="controls">
              <input type="checkbox" id="input02" />
            </div>
          </div>
          <div class="control-group">
            <label for="input03" class="control-label">Select list</label>
            <div class="controls">
              <select id="input03">
                <option>1</option>
                <option>2</option>
                <option>3</option>
                <option>4</option>
                <option>5</option>
                <option>6</option>
                <option>7</option>
                <option>8</option>
              </select>
            </div>
          </div>
          <div class="form-actions">
            <button type="submit" class="btn btn-primary">Save changes</button>
            <button class="btn">Cancel</button>
          </div>
        </fieldset>
      </form>
    </div>
  </body>
</html>

@kostyll
Copy link

kostyll commented Aug 28, 2014

Hi! I've mode some corrections to improve thread-safe support.
Here is the link - https://gist.github.com/kostyll/4e76a79f93e4c807fdd1
If you have a bit of free time, I would be glad you to look through my code :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment