qmsk/web/html.py
changeset 92 e5799432071c
child 102 611787305686
equal deleted inserted replaced
91:292b26405ee7 92:e5799432071c
       
     1 from qmsk.utils import flatten, merge
       
     2 from html import escape
       
     3 
       
     4 import qmsk.web
       
     5 
       
     6 class Tag :
       
     7     def __init__ (self, _name=None, *_contents,
       
     8             _selfclosing=True,
       
     9             _whitespace_sensitive=False,
       
    10             **_attrs) :
       
    11         self.name = _name
       
    12         self.contents = _contents or [ ]
       
    13         self.attrs = _attrs or { }
       
    14 
       
    15         # options
       
    16         self.selfclosing = _selfclosing
       
    17         self.whitespace_sensitive = _whitespace_sensitive
       
    18 
       
    19     def __call__ (self, *_contents, 
       
    20             _selfclosing=None,
       
    21             _whitespace_sensitive=None,
       
    22             **_attrs):
       
    23         return type(self)(self.name, *tuple(flatten(self.contents, _contents)),
       
    24                 _selfclosing            = self.selfclosing if _selfclosing is None else _selfclosing,
       
    25                 _whitespace_sensitive   = self.whitespace_sensitive if _whitespace_sensitive is None else _whitespace_sensitive,
       
    26                 **merge(self.attrs, _attrs)
       
    27         )
       
    28 
       
    29     def render_attrs (self):
       
    30         for name, value in self.attrs.items():
       
    31             name = name.strip('_').replace('_', '-')
       
    32 
       
    33             if value is True:
       
    34                 value = name
       
    35             elif value is None:
       
    36                 continue
       
    37             else:
       
    38                 value = str(value)
       
    39 
       
    40             yield '{name}="{value}"'.format(name=name, value=escape(value, quote=True))
       
    41 
       
    42     def render (self, indent):
       
    43         """
       
    44             Iterate over lines of output HTML.
       
    45         """
       
    46 
       
    47         open = (self.contents or not self.selfclosing)
       
    48         
       
    49         if self.name and self.attrs and open:
       
    50             yield indent, '<{name} {attrs}>'.format(name=self.name, attrs=' '.join(self.render_attrs()))
       
    51         elif self.name and self.attrs:
       
    52             yield indent, '<{name} {attrs} />'.format(name=self.name, attrs=' '.join(self.render_attrs()))
       
    53         elif self.name and open:
       
    54             yield indent, '<{name}>'.format(name=self.name)
       
    55         elif self.name:
       
    56             yield indent, '<{name} />'.format(name=self.name)
       
    57 
       
    58         for item in self.contents:
       
    59             if isinstance(item, Tag):
       
    60                 yield from item.render(indent=indent+1)
       
    61             elif self.whitespace_sensitive:
       
    62                 yield 0, str(item)
       
    63             else:
       
    64                 yield indent + 1, str(item)
       
    65         
       
    66         if self.name and open:
       
    67             yield indent, '</{name}>'.format(name=self.name)
       
    68 
       
    69     def __str__ (self):
       
    70         """
       
    71             Render as HTML.
       
    72         """
       
    73 
       
    74         return '\n'.join('\t'*indent+line for indent, line in self.render(indent=0))
       
    75 
       
    76     def __repr__ (self):
       
    77         return '{name}({args})'.format(
       
    78                 name    = self.__class__.__name__,
       
    79                 args    = ', '.join(
       
    80                     [repr(self.name)]
       
    81                     + [repr(item) for item in self.contents]
       
    82                     + ['{name}={value!r}'.format(name=name, value=value) for name, value in self.attrs.items()]
       
    83                 ),
       
    84         )
       
    85 
       
    86 class HTML:
       
    87     pre     = Tag('pre', _whitespace_sensitive=True)
       
    88 
       
    89     def __getattr__ (self, name):
       
    90         """
       
    91             Get an empty Tag object.
       
    92         """
       
    93 
       
    94         return Tag(name)
       
    95 
       
    96     def __call__ (self, *values):
       
    97         """
       
    98             Raw HTML.
       
    99         """
       
   100         return Tag(None, *values)
       
   101 
       
   102 class HTML5 (HTML):
       
   103     span    = Tag('span', _selfclosing=False)
       
   104     script  = Tag('script', _selfclosing=False)
       
   105 
       
   106 html5   = HTML5()
       
   107 
       
   108 class HTMLHandler (qmsk.web.Handler):
       
   109     """
       
   110         A handler that renders a full HTML page.
       
   111     """
       
   112 
       
   113     # HTML5
       
   114     html = html5
       
   115 
       
   116     DOCTYPE = 'html'
       
   117     HTML_XMLNS = None
       
   118     HTML_LANG = 'en'
       
   119 
       
   120     # <head>
       
   121     TITLE = None
       
   122     STYLE = None
       
   123     SCRIPT = None
       
   124     CSS = (
       
   125 
       
   126     )
       
   127     JS = (
       
   128 
       
   129     )
       
   130     HEAD = None
       
   131 
       
   132     def title (self):
       
   133         return self.TITLE
       
   134 
       
   135     def render (self):
       
   136         raise NotImplementedError()
       
   137 
       
   138     def render_html (self) :
       
   139         """
       
   140             Render HTML <html> tag.
       
   141         """
       
   142 
       
   143         html = self.html
       
   144 
       
   145         return html.html(
       
   146             html.head(
       
   147                 html.title(self.title()),
       
   148                 (
       
   149                     html.link(rel='Stylesheet', type="text/css", href=src) for src in self.CSS
       
   150                 ), 
       
   151                 (
       
   152                     html.script(src=src, type='text/javascript', _selfclosing=False) for src in self.JS
       
   153                 ),
       
   154                 html.style(type='text/css')(self.STYLE) if self.STYLE else None,
       
   155                 html.script(type='text/javascript')(self.SCRIPT) if self.SCRIPT else None,
       
   156                 self.HEAD,
       
   157             ),
       
   158             html.body(
       
   159                 self.render(),
       
   160             ),
       
   161         xmlns=self.HTML_XMLNS, lang=self.HTML_LANG)
       
   162 
       
   163     def render_response (self):
       
   164         """
       
   165             Render entire HTML response.
       
   166         """
       
   167 
       
   168         return """\
       
   169 <!DOCTYPE {doctype}>
       
   170 {html}\
       
   171 """.format(doctype=self.DOCTYPE, html=self.render_html())
       
   172 
       
   173 if __name__ == '__main__':
       
   174     html = HTML5()
       
   175 
       
   176     print(html.html(
       
   177         html.head(
       
   178             html.title("Testing")
       
   179         ),
       
   180         html.body(
       
   181             html.h1("Testing"),
       
   182             html.p("Just testing this..."),
       
   183             html("Raw HTML <tags>"),
       
   184             html.pre(repr(
       
   185                 html.a(href="/foo")("Foo!")
       
   186             ))
       
   187         ),
       
   188     ))