Home Blog CV Projects Patterns Notes Book Colophon Search

Python HTML Tags

31 Oct, 2024

Here's some code for rendering HTML in Python fast, and correctly. See also Lua HTML Templates and Start an HTML Template System.

from html import escape

class SafelyEscapedHTML(str):
    pass

def _dynamic_escape(s):
    if type(s) is SafelyEscapedHTML:
        return str(s)
    else:
        return escape(s)
 
class Tag:
    def __init__(self, name, needs_closing=True):
        self.name = name
        self.needs_closing = needs_closing
        self.attrs = {}
        self.children = []

    def __call__(self, *class_, **attrs):
        assert not self.children
        new_tag = Tag(self.name, self.needs_closing)
        if class_:
            assert len(class_) == 1
            assert 'class' not in 'attrs'
            new_tag.attrs['class'] = class_[0]
        new_tag.attrs.update(attrs)
        return new_tag

    def __getitem__(self, children):
        new_tag = Tag(self.name, self.needs_closing)
        new_tag.attrs = self.attrs
        if type(children) is tuple:
            for child in children:
                new_tag.children.append(child)
        else:
            new_tag.children = [children]
        return new_tag

    def __repr__(self):
        return f'<{self.name} {repr(self.attrs)} children={len(self.children)} needs_closing={self.needs_closing} id={id(self)}>'

    def __str__(self):
        _dynamic_escaped_name = _dynamic_escape(self.name)
        result = f'<{_dynamic_escaped_name}'
        for name, value in self.attrs.items():
            result += f' {_dynamic_escape(name)}="{_dynamic_escape(value)}"'
        result += '>'
        for child in self.children:
            if isinstance(child, Tag):
                result += str(child)
            else:
                result += _dynamic_escape(child)
        if self.needs_closing:
            result += f'</{_dynamic_escaped_name}>'
        return result

    def template(self):
        strings = []
        placeholders = []

        _dynamic_escaped_name = _dynamic_escape(self.name)
        strings.append(f'<{_dynamic_escaped_name}')
        for name, value in self.attrs.items():
            if isinstance(value, Placeholder):
                strings[-1] += f' {_dynamic_escape(name)}="'
                placeholders.append(value.name)
                strings.append('"')
            else:
                strings[-1] += f' {_dynamic_escape(name)}="{_dynamic_escape(value)}"'
        strings[-1] += '>'
        for child in self.children:
            if isinstance(child, Tag):
                child_strings, child_placeholders = child.template()
                strings[-1] += child_strings[0]
                strings += child_strings[1:]
                placeholders += child_placeholders 
            elif isinstance(child, Placeholder):
                strings.append('')
                placeholders.append(child.name)
            else:
                strings[-1] += _dynamic_escape(child)
        if self.needs_closing:
            strings[-1] += f'</{_dynamic_escaped_name}>'
        return (strings, placeholders)

class Template:
    def __init__(self, page):
        strings, placeholders = page.template()
        self.strings = strings
        self.placeholders = placeholders

    def render(self, **values):
        return ''.join(_render((self.strings, self.placeholders), **values))

class Placeholder:
    def __init__(self, name):
        self.name = name


def _render(template, **values):
    strings, placeholders = template
    replaced = []
    for placeholder_name in template[1]:
        value = ''
        if placeholder_name in values:
            value = values[placeholder_name]
            if type(value) is not SafelyEscapedHTML:
                value = escape(value)
            replaced.append(value)
        else:
            raise Exception(f'Placeholder {placeholder_name} not found.')
    yield strings[0]
    for i in range(len(placeholders)):
        yield replaced[i]
        yield strings[i+1]


def example():
    html = Tag('html')
    head = Tag('head')
    title = Tag('title')
    body = Tag('body')
    p = Tag('p')
    a = Tag('a')
    link = Tag('link', needs_closing=False)
    br = Tag('br', needs_closing=False)
    
    def main(link_, href='here'):
        return html[
            head[
                title['Hello, world!'],
                link(**{'type':"stylesheet"})
            ],
            body[
                p('main')[
                    link_,
                    a(href=href)['This &<> is a link'],
                    '.'
                ],
                p['Hello', br, br],
            ]
        ]
    
    import time

    m = main('th"ere')
    print('Page direct:', str(m))
    num = 100000
    start = time.time()
    for x in range(num):
        str(main('th"ere'))
    duration = time.time() - start
    rps = int(num/duration)
    print('Page direct renders per second:', rps)
    
    template = Template(main(Placeholder('one'), Placeholder('two')))
    print('Via template with placeholders:', template.render(one='o&ne', two=SafelyEscapedHTML('t<b>w</b>o')))
    num = 1000000
    start = time.time()
    for x in range(num):
        template.render(one='o&ne', two=SafelyEscapedHTML('t<b>w</b>o'))
    duration = time.time() - start
    rps = int(num/duration)
    print('Template renders per second:', rps)

if __name__ == '__main__':
    example()

Comments

Be the first to comment.

Add Comment





Copyright James Gardner 1996-2020 All Rights Reserved. Admin.