Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/turbohtml/build.py: 96%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

28 statements  

1""" 

2A terse builder for constructing HTML trees, the way ``lxml.builder.E`` did. 

3 

4This is a thin ergonomic layer over the existing element API: every ``E.<tag>(...)`` call builds a real 

5:class:`turbohtml.Element` and appends its children through :meth:`turbohtml.Element.append`, so the result is an 

6ordinary turbohtml tree that serializes with :meth:`turbohtml.Element.serialize`. Sorting a call's arguments into the 

7attribute mapping and the children runs in C too; this module only names the tags. 

8 

9A call takes its arguments in order: a leading mapping is the element's attributes, a string becomes a 

10:class:`turbohtml.Text` node, and any other node is appended as-is:: 

11 

12 from turbohtml.build import E 

13 

14 doc = E.div({"class": "card"}, E.h1("Title"), E.p("body")) 

15 print(doc.serialize()) # <div class="card"><h1>Title</h1><p>body</p></div> 

16 

17Text serializes escaped, except inside ``script``, ``style``, ``xmp``, ``iframe``, ``noembed``, ``noframes`` and 

18``plaintext``, whose content the HTML serialization writes as is. Keep untrusted text out of those elements: a 

19``</script`` in it ends the element and the rest reads as markup. 

20""" 

21 

22from __future__ import annotations 

23 

24from collections.abc import Iterable, Mapping 

25from typing import TYPE_CHECKING, Final, TypeAlias 

26 

27from ._html import _build_document, _build_element 

28 

29if TYPE_CHECKING: 

30 from ._html import Document, Element, Node 

31 

32#: Attributes accepted by :class:`turbohtml.Element`: a name maps to a string, a token list, or ``None`` for a bare 

33#: boolean attribute such as ``disabled``. 

34Attributes: TypeAlias = "Mapping[str, str | list[str] | None]" 

35 

36#: A single builder argument: a leading attribute mapping, an existing node, or a string that becomes a 

37#: :class:`turbohtml.Text` node. 

38Content: TypeAlias = "Attributes | Node | str" 

39 

40 

41def _build(tag: str, args: tuple[Content, ...]) -> Element: 

42 """ 

43 Build one element: a leading mapping sets its attributes, and the rest become children in order. 

44 

45 :raises ValueError: if the tag or an attribute name carries a character HTML forbids there, or if a 

46 void element such as ``br`` or ``img`` is given children. 

47 :raises TypeError: if an attribute mapping is not the first argument. 

48 """ 

49 return _build_element(tag, args, Mapping) 

50 

51 

52class _TagFactory: 

53 """The callable returned by ``E.<tag>``; it remembers the tag name and builds that element when called.""" 

54 

55 __slots__ = ("_tag",) 

56 

57 def __init__(self, tag: str) -> None: 

58 """Remember the tag name this factory builds.""" 

59 self._tag = tag 

60 

61 def __call__(self, *args: Content) -> Element: 

62 """ 

63 Build the element from a leading attribute mapping and the child nodes and strings that follow. 

64 

65 :raises ValueError: if the tag or an attribute name carries a character HTML forbids there, or if a 

66 void element such as ``br`` or ``img`` is given children. 

67 :raises TypeError: if an attribute mapping is not the first argument. 

68 """ 

69 return _build(self._tag, args) 

70 

71 

72class ElementMaker: 

73 """ 

74 A factory whose attribute access names the tag to build: ``E.div`` returns a callable that builds a ``<div>``. 

75 

76 Use the singleton ``E``, or instantiate a private maker. ``E.section(...)`` and ``E("section", ...)`` are 

77 equivalent; the call form takes a tag that is not a Python identifier. 

78 """ 

79 

80 def __getattr__(self, tag: str) -> _TagFactory: 

81 """Return the factory for ``tag``; dunder lookups fall through so copy and pickle protocols are not hijacked.""" 

82 if tag.startswith("__") and tag.endswith("__"): 

83 raise AttributeError(tag) 

84 return _TagFactory(tag) 

85 

86 def __call__(self, tag: str, /, *args: Content) -> Element: 

87 """ 

88 Build ``tag`` from a leading attribute mapping and the child nodes and strings that follow. 

89 

90 :raises ValueError: if the tag or an attribute name carries a character HTML forbids there, or if a 

91 void element such as ``br`` or ``img`` is given children. 

92 :raises TypeError: if an attribute mapping is not the first argument. 

93 """ 

94 return _build(tag, args) 

95 

96 

97#: The shared builder: ``E.div(...)`` or ``E("div", ...)`` builds a ``<div>`` element. 

98E: Final = ElementMaker() 

99 

100 

101def document( 

102 *, 

103 title: str | None = None, 

104 lang: str | None = None, 

105 charset: str | None = "utf-8", 

106 head: Iterable[Node | str] = (), 

107 body: Iterable[Node | str] = (), 

108) -> Document: 

109 """ 

110 Build a complete HTML5 document around the given head and body content. 

111 

112 Where ``E`` builds a bare fragment, this scaffolds the whole page: a ``<!DOCTYPE html>`` followed by ``<html>`` 

113 holding a ``<head>`` and a ``<body>``. The head leads with a ``<meta charset>`` and a ``<title>`` (when supplied), 

114 then the ``head`` content; the body holds the ``body`` content. The result is a :class:`turbohtml.Document`, so 

115 ``document(...).serialize()`` emits the doctype and the full shell:: 

116 

117 from turbohtml.build import E, document 

118 

119 page = document(title="Report", lang="en", body=[E.h1("Sales"), E.p("Up 4%")]) 

120 page.serialize() 

121 # <!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><title>Report</title></head> 

122 # <body><h1>Sales</h1><p>Up 4%</p></body></html> 

123 

124 :param title: the ``<title>`` text; ``None`` omits the element. 

125 :param lang: the ``lang`` attribute for ``<html>``; ``None`` omits it. 

126 :param charset: the ``<meta charset>`` value; ``None`` omits the meta element (e.g. when the charset is set by an 

127 HTTP header instead). 

128 :param head: extra ``<head>`` content, appended after the meta and title; a string becomes a 

129 :class:`turbohtml.Text` node. 

130 :param body: the ``<body>`` content; a string becomes a :class:`turbohtml.Text` node. 

131 :returns: the assembled :class:`turbohtml.Document`. 

132 :raises TypeError: if ``title``, ``lang``, or ``charset`` is neither a str nor ``None``, or a head or body item is 

133 not a node or string. 

134 """ 

135 return _build_document(head, body, title, lang, charset) 

136 

137 

138__all__ = [ 

139 "Attributes", 

140 "Content", 

141 "E", 

142 "ElementMaker", 

143 "document", 

144]