-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy path__init__.py
427 lines (344 loc) · 11.3 KB
/
__init__.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
"""HTMLephant - A small and lazy HTML generator.
"""
###############################################################################
# Constants
###############################################################################
DOCTYPE = '<!DOCTYPE html>\n'
class Entities:
DOUBLE_QUOTE = '"'
GREATER_THAN = '>'
LESS_THAN = '<'
###############################################################################
# HTMLElement Base Class
###############################################################################
class HTMLElement:
# Specify whether this is a void-type element which is self-closing and
# prohibited from text content or children.
IS_VOID = False
# Specify the names of any of attributes that must be specified when
# instantiating this element.
REQUIRED_ATTRS = ()
# Specify whether the text content of this element should be indented
# along with its tags.
INDENT_TEXT = True
# Specify whether to escape the text.
ESCAPE_TEXT = True
# Specify the number of spaces to indent this element's children.
CHILD_INDENT = 2
def __init__(self, text=None, children=None, **attrs):
# Assert that TAG_NAME is defined.
if not hasattr(self, 'TAG_NAME'):
raise AssertionError('Please subclass HTMLElement and define a '
'TAG_NAME attribute')
# Assert that text and children are None for a void-type element.
if self.IS_VOID and (text or children is not None):
raise AssertionError(
'text and children are prohibited for void tag "{}"'
.format(self.TAG_NAME)
)
# Assert that all required attributes were specified.
if any(k not in attrs for k in self.REQUIRED_ATTRS):
raise AssertionError(
'missing required attrs {} for tag "{}"'.format(
[k for k in self.REQUIRED_ATTRS if k not in attrs],
self.TAG_NAME
)
)
# Save the text content value.
self.text = text
# Save the attributes dict.
self.attrs = attrs
# Save the child list.
self.children = None if self.IS_VOID else (children or [])
@staticmethod
def encode_attr_key(k):
"""
Yield the attribute key with any leading underscore stripped (to
provide for the specification of attribute name keyword arguments that
would otherwise collide with a Python keyword, e.g. class) and, if a
leading underscore is present, replace all following underscores with
a hyphens (to provide for the specification of attribute names
containing a hyphen which is standard HTML stuff). Note that both of
these cases could be handled with an unpacked dict as the kwargs,
i.e. func(**{'class': '...', 'data-count': 1}), but that's a lot of
extra characters.
"""
escaped = k[0] == '_'
i = 1 if escaped else 0
k_len = len(k)
while i < k_len:
c = k[i]
if c == '_' and escaped:
yield '-'
else:
yield c
i += 1
@staticmethod
def encode_attr_value(v):
"""
Yield the attribute value with any double-quotes replaced with the
corresponding HTML entity.
"""
for c in str(v):
if c == '"':
yield from Entities.DOUBLE_QUOTE
else:
yield c
@staticmethod
def escape_text(text):
"""
Yield text with any offending character replaced with its corresponding
HTML entity.
"""
for c in text:
if c == '<':
yield from Entities.LESS_THAN
elif c == '>':
yield from Entities.GREATER_THAN
else:
yield c
@staticmethod
def _pad_gen(num):
"""Yield the specified number of SPACE characters.
"""
while num > 0:
yield ' '
num -= 1
def html(self, indent=0):
"""
Yield the HTML characters that comprise this element and all of its
children.
"""
# Yield the start tag.
yield from self._pad_gen(indent)
yield '<'
yield from self.TAG_NAME
if self.attrs:
for k, v in self.attrs.items():
if v is None:
continue
yield ' '
yield from self.encode_attr_key(k)
yield '='
yield '"'
yield from self.encode_attr_value(v)
yield '"'
yield '>'
yield '\n'
# Yield the text.
if self.text:
text = (self.escape_text(self.text) if self.ESCAPE_TEXT
else self.text)
if not self.INDENT_TEXT:
yield from text
else:
yield from self._pad_gen(indent + self.CHILD_INDENT)
for c in text:
yield c
if c == '\n':
yield from self._pad_gen(indent + self.CHILD_INDENT)
yield '\n'
# Yield the children.
if self.children:
for child in self.children:
yield from child.html(indent + self.CHILD_INDENT)
# Maybe yield closing tag.
if not self.IS_VOID:
yield from self._pad_gen(indent)
yield '<'
yield '/'
yield from self.TAG_NAME
yield '>'
yield '\n'
# Define an HTMLElemnt subclass from which to derive void elements.
class VoidHTMLElement(HTMLElement):
IS_VOID = True
# Define a NOEL (No-Element) class that implements a static html() method that
# allows it to be used wherever a normal element can appear, but which yields
# only a single empty string. This is useful for inline conditionals, e.g.
# x = Element(<variable>) if <variable> else NOEL
class NOEL():
@staticmethod
def html(indent=None):
yield ''
###############################################################################
# Common Element Subclasses
###############################################################################
class Address(HTMLElement):
TAG_NAME = 'address'
class Anchor(HTMLElement):
TAG_NAME = 'a'
REQUIRED_ATTRS = ('href',)
class Article(HTMLElement):
TAG_NAME = 'article'
class BlockQuote(HTMLElement):
TAG_NAME = 'blockquote'
REQUIRED_ATTRS = ('cite',)
class Body(HTMLElement):
TAG_NAME = 'body'
class Br(VoidHTMLElement):
TAG_NAME = 'br'
class Button(HTMLElement):
TAG_NAME = 'button'
class Div(HTMLElement):
TAG_NAME = 'div'
class Em(HTMLElement):
TAG_NAME = 'em'
class Form(HTMLElement):
TAG_NAME = 'form'
class H1(HTMLElement):
TAG_NAME = 'h1'
class H2(HTMLElement):
TAG_NAME = 'h2'
class H3(HTMLElement):
TAG_NAME = 'h3'
class H4(HTMLElement):
TAG_NAME = 'h4'
class H5(HTMLElement):
TAG_NAME = 'h5'
class H6(HTMLElement):
TAG_NAME = 'h6'
class Html(HTMLElement):
TAG_NAME = 'html'
REQUIRED_ATTRS = ('lang',)
class Hr(VoidHTMLElement):
TAG_NAME = 'hr'
class Head(HTMLElement):
TAG_NAME = 'head'
class Header(HTMLElement):
TAG_NAME = 'header'
class Img(VoidHTMLElement):
TAG_NAME = 'img'
REQUIRED_ATTRS = ('src', 'alt')
class Input(VoidHTMLElement):
TAG_NAME = 'input'
REQUIRED_ATTRS = ('type',)
class Label(HTMLElement):
TAG_NAME = 'label'
REQUIRED_ATTRS = ('_for',)
class Li(HTMLElement):
TAG_NAME = 'li'
class Link(VoidHTMLElement):
TAG_NAME = 'link'
REQUIRED_ATTRS = ('href', 'rel')
class Main(HTMLElement):
TAG_NAME = 'main'
class Meta(VoidHTMLElement):
TAG_NAME = 'meta'
class Nav(HTMLElement):
TAG_NAME = 'nav'
class Ol(HTMLElement):
TAG_NAME = 'ol'
class Option(HTMLElement):
TAG_NAME = 'option'
REQUIRED_ATTRS = ('value',)
class Paragraph(HTMLElement):
TAG_NAME = 'p'
class Picture(HTMLElement):
TAG_NAME = 'picture'
class PictureSource(VoidHTMLElement):
TAG_NAME = 'source'
REQUIRED_ATTRS = ('srcset',)
class Script(HTMLElement):
TAG_NAME = 'script'
ESCAPE_TEXT = False
class Section(HTMLElement):
TAG_NAME = 'section'
class Select(HTMLElement):
TAG_NAME = 'select'
class Span(HTMLElement):
TAG_NAME = 'span'
class Strong(HTMLElement):
TAG_NAME = 'strong'
class Style(HTMLElement):
TAG_NAME = 'style'
ESCAPE_TEXT = False
class Table(HTMLElement):
TAG_NAME = 'table'
class Tbody(HTMLElement):
TAG_NAME = 'tbody'
class Td(HTMLElement):
TAG_NAME = 'td'
class Template(HTMLElement):
TAG_NAME = 'template'
class Textarea(HTMLElement):
TAG_NAME = 'textarea'
INDENT_TEXT = False
class Th(HTMLElement):
TAG_NAME = 'th'
class Thead(HTMLElement):
TAG_NAME = 'thead'
class Title(HTMLElement):
TAG_NAME = 'title'
class Time(HTMLElement):
TAG_NAME = 'time'
REQUIRED_ATTRS = ('datetime',)
class Tr(HTMLElement):
TAG_NAME = 'tr'
class Ul(HTMLElement):
TAG_NAME = 'ul'
class Video(HTMLElement):
TAG_NAME = 'video'
class VideoSource(VoidHTMLElement):
TAG_NAME = 'source'
REQUIRED_ATTRS = ('src',)
###############################################################################
# <meta> helpers
###############################################################################
# Standard: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta/name
StdMeta = lambda k, v: Meta(name=k, content=v)
# OpenGraph: https://ogp.me/
OGMeta = lambda k, v: Meta(property=f'og:{k}', content=v)
# Microdata meta helper.
MDMeta = lambda k, v: Meta(itemprop=k, content=v)
###############################################################################
# GenReader Class
###############################################################################
class GenReader:
"""
A file-like character generator wrapper/encoder that implements readinto().
"""
def __init__(self, gen, encoding='utf-8'):
self.gen = gen
self.encoding = encoding
def readinto(self, buf):
# Write up to len(buf) bytes into buf from the generator and return the
# number of bytes written.
i = 0
buf_size = len(buf)
while i < buf_size:
try:
char = next(self.gen)
except StopIteration:
break
for byte in char.encode(self.encoding):
buf[i] = byte
i += 1
return i
###############################################################################
# Document Function
###############################################################################
def Document(body_els, head_els=()):
"""
Yield an HTML document that comprises the specified body and head child
elements.
"""
yield from DOCTYPE
yield from Html(
lang='en',
children=(
Head(
children=(
Meta(charset='utf-8'),
Meta(
name='viewport',
content='width=device-width, initial-scale=1'
),
*head_els
)
),
Body(children=body_els)
)
).html()
# Define a GenReader-wrapped version of Document.
DocumentStream = lambda *args, **kwargs: GenReader(Document(*args, **kwargs))