|
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 )) |