Coverage for control/html.py : 93%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""HTML generation made easy.
3* for each HTML element there is a function to wrap attributes and content in it.
4* additional support for more involved patches of HTML (`details`, `input`, icons)
5* escaping of HTML elements.
7"""
9from config import Config as C, Names as N
10from control.utils import (
11 pick as G,
12 cap1,
13 asString,
14 E,
15 AMP,
16 LT,
17 APOS,
18 QUOT,
19 DOLLAR,
20 ONE,
21 MINONE,
22)
24CW = C.web
26EMPTY_ELEMENTS = set(CW.emptyElements)
27ICONS = CW.icons
29CLASS = "class"
32class HtmlElement:
33 """Wrapping of attributes and content into an HTML element."""
35 def __init__(self, name):
36 """## Initialization
38 An HtmlElement object.
40 Parameters
41 ----------
42 name: string
43 See below.
44 """
46 self.name = name
47 """*string* The element name.
48 """
50 @staticmethod
51 def atNormal(k):
52 """Substitute the `cls` attribute name with `class`. """
54 return CLASS if k == N.cls else k
56 @staticmethod
57 def attStr(atts, addClass=None):
58 """Stringify attributes.
60 !!! hint
61 Attributes with value `True` are represented as bare attributes, without
62 value. For example: `{open=True}` translates into `open`.
64 !!! caution
65 Use the name `cls` to get a `class` attribute inside an HTML element.
66 The name `class` interferes too much with Python syntax to be usable
67 as a keyowrd argument.
69 Parameters
70 ----------
71 atts: dict
72 A dictionary of attributes.
73 addClass: string
74 An extra `class` attribute. If there is already a class attribute
75 `addClass` will be appended to it.
76 Otherwise a fresh class attribute will be created.
78 Returns
79 -------
80 string
81 The serialzed attributes.
82 """
84 if addClass:
85 if atts and N.cls in atts:
86 atts[N.cls] += f" {addClass}"
87 elif atts:
88 atts[N.cls] = addClass
89 else:
90 atts = dict(cls=addClass)
91 return E.join(
92 f""" {HtmlElement.atNormal(k)}""" + (E if v is True else f"""='{v}'""")
93 for (k, v) in atts.items()
94 if v is not None
95 )
97 def wrap(self, material, addClass=None, **atts):
98 """Wraps attributes and content into an element.
100 !!! caution
101 No HTML escaping of special characters will take place.
102 You have to use `control.html.HtmlElements.he` yourself.
104 Parameters
105 ----------
106 material: string | iterable
107 The element content. If the material is not a string but another
108 iterable, the items will be joined by the empty string.
110 addClass: string
111 An extra `class` attribute. If there is already a class attribute
112 `addClass` will be appended to it.
113 Otherwise a fresh class attribute will be created.
115 Returns
116 -------
117 string
118 The serialzed element.
120 """
122 name = self.name
123 content = asString(material)
124 attributes = HtmlElement.attStr(atts, addClass=addClass)
125 return (
126 f"""<{name}{attributes}>"""
127 if name in EMPTY_ELEMENTS
128 else f"""<{name}{attributes}>{content}</{name}>"""
129 )
132class HtmlElements:
133 """Wrap specific HTML elements and patterns.
135 !!! note
136 Nearly all elements accept an arbitrary supply of attributes
137 in the `**atts` parameter, which will not further be documented.
138 """
140 @staticmethod
141 def he(val):
142 """Escape HTML characters.
144 The following characters will be replaced by entities:
145 ```
146 & < ' "
147 ```
149 The dollar sign will be wrapped into a `<span>`.
150 """
152 return (
153 E
154 if val is None
155 else (
156 str(val)
157 .replace(AMP, f"""&{N.amp};""")
158 .replace(LT, f"""&{N.lt};""")
159 .replace(APOS, f"""&{N.apos};""")
160 .replace(QUOT, f"""&{N.quot};""")
161 .replace(DOLLAR, f"""<{N.span}>{DOLLAR}</{N.span}>""")
162 )
163 )
165 @staticmethod
166 def a(material, href, **atts):
167 """A.
169 Hyperlink.
171 Parameters
172 ----------
173 material: string | iterable
174 Text of the link.
175 href: url
176 Destination of the link.
178 Returns
179 -------
180 string(html)
181 """
183 return HtmlElement(N.a).wrap(material, href=href, **atts)
185 @staticmethod
186 def br():
187 """BR.
189 Line break.
191 Returns
192 -------
193 string(html)
194 """
196 return HtmlElement(N.br).wrap(E)
198 @staticmethod
199 def dd(material, **atts):
200 """DD.
202 The definition part of a term.
204 Parameters
205 ----------
206 material: string | iterable
208 Returns
209 -------
210 string(html)
211 """
213 return HtmlElement(N.dd).wrap(material, **atts)
215 @staticmethod
216 def details(summary, material, itemkey, **atts):
217 """DETAILS.
219 Collapsible details element.
221 Parameters
222 ----------
223 summary: string | iterable
224 The summary.
225 material: string | iterable
226 The expansion.
227 itemkey: string
228 Identifier for reference from Javascript.
230 Returns
231 -------
232 string(html)
233 """
235 content = asString(material)
236 return HtmlElement(N.details).wrap(
237 HtmlElement(N.summary).wrap(summary) + content, itemkey=itemkey, **atts
238 )
240 @staticmethod
241 def detailx(
242 icons, material, itemkey, openAtts={}, closeAtts={}, **atts,
243 ):
244 """detailx.
246 Collapsible details pseudo element.
248 Unlike the HTML `details` element, this one allows separate open and close
249 controls. There is no summary.
251 !!! warning
252 The `icon` names must be listed in the web.yaml config file
253 under the key `icons`. The icon itself is a Unicode character.
255 !!! hint
256 The `atts` go to the outermost `div` of the result.
258 Parameters
259 ----------
260 icons: string | (string, string)
261 Names of the icons that open and close the element.
262 itemkey: string
263 Identifier for reference from Javascript.
264 openAtts: dict, optinal, `{}`
265 Attributes for the open icon.
266 closeAtts: dict, optinal, `{}`
267 Attributes for the close icon.
269 Returns
270 -------
271 string(html)
272 """
274 content = asString(material)
275 (iconOpen, iconClose) = (icons, icons) if type(icons) is str else icons
276 triggerElements = [
277 (HtmlElements.iconx if icon in ICONS else HtmlElements.span)(
278 icon, itemkey=itemkey, trigger=value, **triggerAtts,
279 )
280 for (icon, value, triggerAtts) in (
281 (iconOpen, ONE, openAtts),
282 (iconClose, MINONE, closeAtts),
283 )
284 ]
285 return (
286 *triggerElements,
287 HtmlElement(N.div).wrap(content, itemkey=itemkey, body=ONE, **atts),
288 )
290 @staticmethod
291 def div(material, **atts):
292 """DIV.
294 Parameters
295 ----------
296 material: string | iterable
298 Returns
299 -------
300 string(html)
301 """
303 return HtmlElement(N.div).wrap(material, **atts)
305 @staticmethod
306 def dl(items, **atts):
307 """DL.
309 Definition list.
311 Parameters
312 ----------
313 items: iterable of (string, string)
314 These are the list items, which are term-definition pairs.
316 Returns
317 -------
318 string(html)
319 """
321 return HtmlElement(N.dl).wrap(
322 [
323 HtmlElement(N.dt).wrap(item[0]) + HtmlElement(N.dd).wrap(item[1])
324 for item in items
325 ],
326 **atts,
327 )
329 @staticmethod
330 def dt(material, **atts):
331 """DT.
333 Term of a definition.
335 Parameters
336 ----------
337 material: string | iterable
339 Returns
340 -------
341 string(html)
342 """
344 return HtmlElement(N.dt).wrap(material, **atts)
346 @staticmethod
347 def h(level, material, **atts):
348 """H1, H2, H3, H4, H5, H6.
350 Parameters
351 ----------
352 level: int
353 The heading level.
354 material: string | iterable
355 The heading content.
357 Returns
358 -------
359 string(html)
360 """
362 return HtmlElement(f"{N.h}{level}").wrap(material, **atts)
364 @staticmethod
365 def icon(icon, asChar=False, **atts):
366 """icon.
368 Pseudo element for an icon.
370 !!! warning
371 The `icon` names must be listed in the web.yaml config file
372 under the key `icons`. The icon itself is a Unicode character.
374 Parameters
375 ----------
376 icon: string
377 Name of the icon.
378 asChar: boolean, optional, `False`
379 If `True`, just output the icon character.
380 Otherwise, wrap it in a `<span>` with all
381 attributes that might have been passed.
383 Returns
384 -------
385 string(html)
386 """
388 iconChar = G(ICONS, icon, default=ICONS[N.noicon])
389 if asChar:
390 return G(ICONS, icon, default=ICONS[N.noicon])
391 addClass = f"{N.symbol} i-{icon} "
392 return HtmlElement(N.span).wrap(iconChar, addClass=addClass, **atts)
394 @staticmethod
395 def iconx(icon, href=None, **atts):
396 """iconx.
398 Pseudo element for a clickable icon.
399 It will be wrapped in an `<a href="...">...</a>` element or a <span...>
400 if `href` is `None`.
402 If `href` is the empty string, the element will still be wrapped in
403 an `<a ...>` element, but without a `href` attribute.
405 !!! warning
406 The `icon` names must be listed in the web.yaml config file
407 under the key `icons`. The icon itself is a Unicode character.
409 Parameters
410 ----------
411 icon: string
412 Name of the icon.
413 href: url, optional, `None`
414 Destination of the icon when clicked.
415 Will be left out when equal to the empty string.
417 Returns
418 -------
419 string(html)
420 """
422 iconChar = G(ICONS, icon, default=ICONS[N.noicon])
423 addClass = f"{N.icon} i-{icon} "
424 if href: 424 ↛ 425line 424 didn't jump to line 425, because the condition on line 424 was never true
425 atts["href"] = href
427 return HtmlElement(N.span if href is None else N.a).wrap(
428 iconChar, addClass=addClass, **atts
429 )
431 @staticmethod
432 def iconr(itemKey, tag, msg=None):
433 """iconr.
435 Refresh icon.
436 Special case of `iconx`, but used for refreshing an element.
438 !!! warning
439 The `icon` names must be listed in the web.yaml config file
440 under the key `icons`. The icon itself is a Unicode character.
442 Parameters
443 ----------
444 itemkey: string
445 Identifier for reference from Javascript.
446 tag: string
447 Attribute `tag`, used by Javascript for scrolling to this
448 element after the refresh. It is meant to distinhuish it from
449 other refresh icons for the same element.
450 msg: string, optional, `None`
451 Message for in the tooltip.
453 Returns
454 -------
455 string(html)
456 """
458 if msg is None: 458 ↛ 459line 458 didn't jump to line 459, because the condition on line 458 was never true
459 msg = E
460 return HtmlElements.iconx(
461 N.refresh,
462 cls="small",
463 action=N.refresh,
464 title=f"""{cap1(N.refresh)} {msg}""",
465 targetkey=itemKey,
466 tag=tag,
467 )
469 @staticmethod
470 def img(src, href=None, title=None, imgAtts={}, **atts):
471 """IMG.
473 Image element.
475 !!! note
476 The `atts` go to the outer element, which is either `<img>` if it is
477 not further wrapped, or `<a>`.
478 The `imgAtts` only go to the `<img>` element.
480 Parameters
481 ----------
482 src: url
483 The url of the image.
484 href: url, optional, `None`
485 The destination to navigate to if the image is clicked.
486 The images is then wrapped in an `<a>` element.
487 If missing, the image is not wrapped further.
488 title: string, optional, `None`
489 Tooltip.
490 imgAtts: dict, optional `{}`
491 Attributes that go to the `<img>` element.
493 Returns
494 -------
495 string(html)
496 """
498 return (
499 HtmlElements.a(
500 HtmlElement(N.img).wrap(E, src=src, **imgAtts),
501 href,
502 title=title,
503 **atts,
504 )
505 if href
506 else HtmlElement(N.img).wrap(E, src=src, title=title, **imgAtts, **atts)
507 )
509 @staticmethod
510 def input(material, **atts):
511 """INPUT.
513 The element to receive types user input.
515 !!! caution
516 Do not use this for checkboxes. Use
517 `control.html.HtmlElements.checkbox` instead.
519 Parameters
520 ----------
521 material: string | iterable
522 This goes into the `value` attribute of the element, after HTML escaping.
524 Returns
525 -------
526 string(html)
527 """
529 content = asString(material)
530 return HtmlElement(N.input).wrap(E, value=HtmlElements.he(content), **atts)
532 @staticmethod
533 def join(material):
534 """fragment.
536 This is a pseudo element.
537 The material will be joined together, without wrapping it in an element.
538 There are no attributes.
540 Parameters
541 ----------
542 material: string | iterable
544 Returns
545 -------
546 string(html)
547 """
549 return asString(material)
551 @staticmethod
552 def checkbox(var, **atts):
553 """INPUT type=checkbox.
555 The element to receive user clicks.
557 Parameters
558 ----------
559 var: string
560 The name of an identifier for the element.
562 Returns
563 -------
564 string(html)
565 """
567 return HtmlElement(N.input).wrap(
568 E, type=N.checkbox, id=var, addClass=N.option, **atts,
569 )
571 @staticmethod
572 def p(material, **atts):
573 """P.
575 Paragraph.
577 Parameters
578 ----------
579 material: string | iterable
581 Returns
582 -------
583 string(html)
584 """
586 return HtmlElement(N.p).wrap(material, **atts)
588 @staticmethod
589 def script(material, **atts):
590 """SCRIPT.
592 Parameters
593 ----------
594 material: string | iterable
595 The Javascript.
597 Returns
598 -------
599 string(html)
600 """
602 return HtmlElement(N.script).wrap(material, **atts)
604 @staticmethod
605 def span(material, **atts):
606 """SPAN.
608 Inline element.
610 Parameters
611 ----------
612 material: string | iterable
614 Returns
615 -------
616 string(html)
617 """
619 return HtmlElement(N.span).wrap(material, **atts)
621 @staticmethod
622 def table(headers, rows, **atts):
623 """TABLE.
625 The table element.
627 Parameters
628 ----------
629 headers, rows: iterables of iterables
630 An iterable of rows.
631 Each row is a tuple: an iterable of cells, and a CSS class for the row.
632 Each cell is a tuple: material for the cell, and a CSS class for the cell.
634 !!! note
635 Cells in normal rows are wrapped in `<td>`, cells in header rows go
636 into `<th>`.
638 Returns
639 -------
640 string(html)
641 """
643 th = HtmlElement(N.th).wrap
644 td = HtmlElement(N.td).wrap
645 headerMaterial = HtmlElements.wrapTable(headers, th)
646 rowMaterial = HtmlElements.wrapTable(rows, td)
647 material = HtmlElement(N.body).wrap(headerMaterial + rowMaterial)
648 return HtmlElement(N.table).wrap(material, **atts)
650 @staticmethod
651 def textarea(material, **atts):
652 """TEXTAREA.
654 Input element for larger text, typically Markdown.
656 Parameters
657 ----------
658 material: string | iterable
660 Returns
661 -------
662 string(html)
663 """
665 content = asString(material)
666 return HtmlElement(N.textarea).wrap(content, **atts)
668 @staticmethod
669 def wrapTable(data, td):
670 """Rows and cells.
672 Parameters
673 ----------
674 data: iterable of iterables.
675 Rows and cells within them, both with CSS classes.
676 td: function
677 Funnction for wrapping the cells, typically boiling down
678 to wrapping them in either `<th>` or `<td>` elements.
680 Returns
681 -------
682 string(html)
683 """
685 tr = HtmlElement(N.tr).wrap
686 material = []
687 for (rowData, rowAtts) in data:
688 rowMaterial = []
689 for (cellData, cellAtts) in rowData:
690 rowMaterial.append(td(cellData, **cellAtts))
691 material.append(tr(rowMaterial, **rowAtts))
692 return material