qmsk/web/html.py
author Tero Marttila <terom@paivola.fi>
Thu, 29 Jan 2015 22:14:59 +0200
changeset 102 611787305686
parent 92 e5799432071c
permissions -rw-r--r--
qmsk.web.html: HTMLMixin
from qmsk.utils import flatten, merge
from html import escape

import qmsk.web.application

class Tag :
    def __init__ (self, _name=None, *_contents,
            _selfclosing=True,
            _whitespace_sensitive=False,
            **_attrs) :
        self.name = _name
        self.contents = _contents or [ ]
        self.attrs = _attrs or { }

        # options
        self.selfclosing = _selfclosing
        self.whitespace_sensitive = _whitespace_sensitive

    def __call__ (self, *_contents, 
            _selfclosing=None,
            _whitespace_sensitive=None,
            **_attrs):
        return type(self)(self.name, *tuple(flatten(self.contents, _contents)),
                _selfclosing            = self.selfclosing if _selfclosing is None else _selfclosing,
                _whitespace_sensitive   = self.whitespace_sensitive if _whitespace_sensitive is None else _whitespace_sensitive,
                **merge(self.attrs, _attrs)
        )

    def render_attrs (self):
        for name, value in self.attrs.items():
            name = name.strip('_').replace('_', '-')

            if value is True:
                value = name
            elif value is None:
                continue
            else:
                value = str(value)

            yield '{name}="{value}"'.format(name=name, value=escape(value, quote=True))

    def render (self, indent):
        """
            Iterate over lines of output HTML.
        """

        open = (self.contents or not self.selfclosing)
        
        if self.name and self.attrs and open:
            yield indent, '<{name} {attrs}>'.format(name=self.name, attrs=' '.join(self.render_attrs()))
        elif self.name and self.attrs:
            yield indent, '<{name} {attrs} />'.format(name=self.name, attrs=' '.join(self.render_attrs()))
        elif self.name and open:
            yield indent, '<{name}>'.format(name=self.name)
        elif self.name:
            yield indent, '<{name} />'.format(name=self.name)

        for item in self.contents:
            if isinstance(item, Tag):
                yield from item.render(indent=indent+1)
            elif self.whitespace_sensitive:
                yield 0, str(item)
            else:
                yield indent + 1, str(item)
        
        if self.name and open:
            yield indent, '</{name}>'.format(name=self.name)

    def __str__ (self):
        """
            Render as HTML.
        """

        return '\n'.join('\t'*indent+line for indent, line in self.render(indent=0))

    def __repr__ (self):
        return '{name}({args})'.format(
                name    = self.__class__.__name__,
                args    = ', '.join(
                    [repr(self.name)]
                    + [repr(item) for item in self.contents]
                    + ['{name}={value!r}'.format(name=name, value=value) for name, value in self.attrs.items()]
                ),
        )

class HTML:
    pre     = Tag('pre', _whitespace_sensitive=True)

    def __getattr__ (self, name):
        """
            Get an empty Tag object.
        """

        return Tag(name)

    def __call__ (self, *values):
        """
            Raw HTML.
        """
        return Tag(None, *values)

class HTML5 (HTML):
    span    = Tag('span', _selfclosing=False)
    script  = Tag('script', _selfclosing=False)

html5   = HTML5()

class HTMLMixin:
    """
        A handler that renders a full HTML page.
    """

    # HTML5
    html = html5

    DOCTYPE = 'html'
    HTML_XMLNS = None
    HTML_LANG = 'en'

    # <head>
    TITLE = None
    STYLE = None
    SCRIPT = None
    CSS = (

    )
    JS = (

    )
    HEAD = None

    def title (self):
        return self.TITLE

    def render (self):
        raise NotImplementedError()

    def render_html (self) :
        """
            Render HTML <html> tag.
        """

        html = self.html

        title = self.title()

        assert title

        return html.html(
            html.head(
                html.title(title),
                (
                    html.link(rel='Stylesheet', type="text/css", href=src) for src in self.CSS
                ), 
                (
                    html.script(src=src, type='text/javascript', _selfclosing=False) for src in self.JS
                ),
                html.style(type='text/css')(self.STYLE) if self.STYLE else None,
                html.script(type='text/javascript')(self.SCRIPT) if self.SCRIPT else None,
                self.HEAD,
            ),
            html.body(
                self.render(),
            ),
        xmlns=self.HTML_XMLNS, lang=self.HTML_LANG)

    def render_response (self):
        """
            Render entire HTML response.
        """

        return """\
<!DOCTYPE {doctype}>
{html}\
""".format(doctype=self.DOCTYPE, html=self.render_html())

if __name__ == '__main__':
    html = HTML5()

    print(html.html(
        html.head(
            html.title("Testing")
        ),
        html.body(
            html.h1("Testing"),
            html.p("Just testing this..."),
            html("Raw HTML <tags>"),
            html.pre(repr(
                html.a(href="/foo")("Foo!")
            ))
        ),
    ))

class HTMLHandler (HTMLMixin, qmsk.web.application.Handler):
    pass