Pyjoyment 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (200) hide show
  1. Pyjo/App/ASGI.py +735 -0
  2. Pyjo/App/CGI.py +773 -0
  3. Pyjo/App/FastCGI.py +631 -0
  4. Pyjo/App/Lambda.py +987 -0
  5. Pyjo/App/WSGI.py +427 -0
  6. Pyjo/App/__init__.py +409 -0
  7. Pyjo/Asset/File.py +377 -0
  8. Pyjo/Asset/Memory.py +235 -0
  9. Pyjo/Asset/__init__.py +200 -0
  10. Pyjo/Base.py +215 -0
  11. Pyjo/Cache.py +119 -0
  12. Pyjo/Collection.py +570 -0
  13. Pyjo/Content/MultiPart.py +325 -0
  14. Pyjo/Content/Single.py +193 -0
  15. Pyjo/Content/__init__.py +806 -0
  16. Pyjo/Cookie/Request.py +90 -0
  17. Pyjo/Cookie/Response.py +294 -0
  18. Pyjo/Cookie/__init__.py +143 -0
  19. Pyjo/DOM/CSS.py +1019 -0
  20. Pyjo/DOM/HTML.py +550 -0
  21. Pyjo/DOM/__init__.py +1329 -0
  22. Pyjo/Date.py +294 -0
  23. Pyjo/EventEmitter.py +522 -0
  24. Pyjo/Exception.py +494 -0
  25. Pyjo/File.py +959 -0
  26. Pyjo/GetOpt.py +605 -0
  27. Pyjo/Headers.py +760 -0
  28. Pyjo/HelloWorld.py +77 -0
  29. Pyjo/Home.py +109 -0
  30. Pyjo/IOLoop/ChildProcess.py +591 -0
  31. Pyjo/IOLoop/Client.py +736 -0
  32. Pyjo/IOLoop/Datagram.py +982 -0
  33. Pyjo/IOLoop/Messages.py +664 -0
  34. Pyjo/IOLoop/Server.py +458 -0
  35. Pyjo/IOLoop/Stdio.py +446 -0
  36. Pyjo/IOLoop/Stream.py +644 -0
  37. Pyjo/IOLoop/Subprocess.py +419 -0
  38. Pyjo/IOLoop/TLS.py +360 -0
  39. Pyjo/IOLoop/__init__.py +1036 -0
  40. Pyjo/IOLoop/certs/README.md +41 -0
  41. Pyjo/IOLoop/certs/ca.crt +20 -0
  42. Pyjo/IOLoop/certs/server.crt +21 -0
  43. Pyjo/IOLoop/certs/server.key +28 -0
  44. Pyjo/JSON/Pointer.py +135 -0
  45. Pyjo/JSON/RPC.py +772 -0
  46. Pyjo/JSON/Schema.py +2506 -0
  47. Pyjo/JSON/__init__.py +573 -0
  48. Pyjo/Loader.py +342 -0
  49. Pyjo/Log.py +723 -0
  50. Pyjo/Message/Request.py +725 -0
  51. Pyjo/Message/Response.py +476 -0
  52. Pyjo/Message/__init__.py +830 -0
  53. Pyjo/Parameters.py +472 -0
  54. Pyjo/Path.py +419 -0
  55. Pyjo/Promise.py +894 -0
  56. Pyjo/Prove.py +1305 -0
  57. Pyjo/Reactor/Asyncio.py +854 -0
  58. Pyjo/Reactor/Poll.py +439 -0
  59. Pyjo/Reactor/__init__.py +525 -0
  60. Pyjo/SSE.py +119 -0
  61. Pyjo/Server/ASGI.py +486 -0
  62. Pyjo/Server/CGI.py +259 -0
  63. Pyjo/Server/Daemon.py +1007 -0
  64. Pyjo/Server/FastCGI.py +517 -0
  65. Pyjo/Server/Hot/__init__.py +687 -0
  66. Pyjo/Server/Hot/__main__.py +5 -0
  67. Pyjo/Server/Lambda.py +641 -0
  68. Pyjo/Server/Prefork.py +917 -0
  69. Pyjo/Server/Prethread.py +394 -0
  70. Pyjo/Server/Reload/Backend/Poll.py +114 -0
  71. Pyjo/Server/Reload/Backend/__init__.py +126 -0
  72. Pyjo/Server/Reload/__init__.py +482 -0
  73. Pyjo/Server/Reload/__main__.py +5 -0
  74. Pyjo/Server/Stdio.py +228 -0
  75. Pyjo/Server/WSGI.py +349 -0
  76. Pyjo/Server/__init__.py +396 -0
  77. Pyjo/String/Bytes.py +532 -0
  78. Pyjo/String/HTML.py +210 -0
  79. Pyjo/String/Mixin.py +333 -0
  80. Pyjo/String/Template.py +470 -0
  81. Pyjo/String/Unicode.py +553 -0
  82. Pyjo/String/__init__.py +0 -0
  83. Pyjo/TOML.py +232 -0
  84. Pyjo/Template.py +1290 -0
  85. Pyjo/Test/Pyjo.py +1454 -0
  86. Pyjo/Test/__init__.py +1233 -0
  87. Pyjo/Test/pytest_plugin.py +305 -0
  88. Pyjo/Transaction/HTTP.py +374 -0
  89. Pyjo/Transaction/WebSocket.py +709 -0
  90. Pyjo/Transaction/__init__.py +429 -0
  91. Pyjo/URL/Template.py +459 -0
  92. Pyjo/URL/__init__.py +651 -0
  93. Pyjo/Upload.py +139 -0
  94. Pyjo/UserAgent/CookieJar.py +391 -0
  95. Pyjo/UserAgent/Proxy.py +138 -0
  96. Pyjo/UserAgent/Server.py +239 -0
  97. Pyjo/UserAgent/Transactor.py +766 -0
  98. Pyjo/UserAgent/__init__.py +1243 -0
  99. Pyjo/Util.py +3567 -0
  100. Pyjo/WebSocket.py +332 -0
  101. Pyjo/YAML.py +1167 -0
  102. Pyjo/__init__.py +441 -0
  103. Pyjo/__main__.py +35 -0
  104. Pyjo/py.typed +0 -0
  105. Pyjoyment/Command/Author/__init__.py +20 -0
  106. Pyjoyment/Command/Author/generate/__init__.py +106 -0
  107. Pyjoyment/Command/Author/generate/app.py +299 -0
  108. Pyjoyment/Command/Author/generate/dockerfile.py +281 -0
  109. Pyjoyment/Command/Author/generate/kubernetes.py +514 -0
  110. Pyjoyment/Command/Author/generate/lambda_zip.py +372 -0
  111. Pyjoyment/Command/Author/generate/lite_app.py +113 -0
  112. Pyjoyment/Command/Author/generate/plugin.py +267 -0
  113. Pyjoyment/Command/Author/generate/pyproject.py +123 -0
  114. Pyjoyment/Command/Author/inflate.py +101 -0
  115. Pyjoyment/Command/__init__.py +332 -0
  116. Pyjoyment/Command/asgi.py +92 -0
  117. Pyjoyment/Command/cgi.py +90 -0
  118. Pyjoyment/Command/daemon.py +160 -0
  119. Pyjoyment/Command/eval.py +128 -0
  120. Pyjoyment/Command/fastcgi.py +99 -0
  121. Pyjoyment/Command/get.py +305 -0
  122. Pyjoyment/Command/lambda.py +117 -0
  123. Pyjoyment/Command/prefork.py +181 -0
  124. Pyjoyment/Command/prethread.py +126 -0
  125. Pyjoyment/Command/routes.py +133 -0
  126. Pyjoyment/Command/stdio.py +92 -0
  127. Pyjoyment/Command/version.py +141 -0
  128. Pyjoyment/Command/wsgi.py +93 -0
  129. Pyjoyment/Commands.py +546 -0
  130. Pyjoyment/Controller.py +1307 -0
  131. Pyjoyment/HelloWorld.py +56 -0
  132. Pyjoyment/Lite.py +494 -0
  133. Pyjoyment/Plugin/AllowedHosts.py +205 -0
  134. Pyjoyment/Plugin/Config.py +248 -0
  135. Pyjoyment/Plugin/DefaultHelpers.py +1202 -0
  136. Pyjoyment/Plugin/EPLRenderer.py +134 -0
  137. Pyjoyment/Plugin/EPRenderer.py +212 -0
  138. Pyjoyment/Plugin/HeaderCondition.py +98 -0
  139. Pyjoyment/Plugin/Health.py +325 -0
  140. Pyjoyment/Plugin/JSONConfig.py +172 -0
  141. Pyjoyment/Plugin/Mount.py +223 -0
  142. Pyjoyment/Plugin/PyRenderer.py +209 -0
  143. Pyjoyment/Plugin/TOMLConfig.py +141 -0
  144. Pyjoyment/Plugin/TagHelpers.py +959 -0
  145. Pyjoyment/Plugin/YAMLConfig.py +178 -0
  146. Pyjoyment/Plugin/__init__.py +80 -0
  147. Pyjoyment/Plugins.py +215 -0
  148. Pyjoyment/Renderer.py +673 -0
  149. Pyjoyment/Routes/Match.py +251 -0
  150. Pyjoyment/Routes/Pattern.py +443 -0
  151. Pyjoyment/Routes/Route.py +827 -0
  152. Pyjoyment/Routes/__init__.py +419 -0
  153. Pyjoyment/Sessions.py +255 -0
  154. Pyjoyment/Static.py +421 -0
  155. Pyjoyment/Types.py +238 -0
  156. Pyjoyment/Validator/Validation.py +395 -0
  157. Pyjoyment/Validator/__init__.py +304 -0
  158. Pyjoyment/__init__.py +1083 -0
  159. Pyjoyment/__main__.py +5 -0
  160. Pyjoyment/py.typed +0 -0
  161. Pyjoyment/resources/public/favicon.ico +0 -0
  162. Pyjoyment/resources/public/favicon.svg +345 -0
  163. Pyjoyment/resources/public/pyjo/bootstrap/LICENSE +21 -0
  164. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.bundle.min.js.map +1 -0
  165. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.css +6 -0
  166. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.js +7 -0
  167. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.min.css.map +1 -0
  168. Pyjoyment/resources/public/pyjo/failunicat.png +0 -0
  169. Pyjoyment/resources/public/pyjo/failunicat.svg +50 -0
  170. Pyjoyment/resources/public/pyjo/highlight.js/LICENSE +29 -0
  171. Pyjoyment/resources/public/pyjo/highlight.js/highlight-mojo-dark.css +56 -0
  172. Pyjoyment/resources/public/pyjo/highlight.js/highlight.min.js +1262 -0
  173. Pyjoyment/resources/public/pyjo/highlight.js/pyjoyment.min.js +1 -0
  174. Pyjoyment/resources/public/pyjo/logo-black-4x.png +0 -0
  175. Pyjoyment/resources/public/pyjo/logo-black-4x.svg +346 -0
  176. Pyjoyment/resources/public/pyjo/logo-white-2x.png +0 -0
  177. Pyjoyment/resources/public/pyjo/logo-white-2x.svg +346 -0
  178. Pyjoyment/resources/public/pyjo/logo-white-4x.png +0 -0
  179. Pyjoyment/resources/public/pyjo/logo-white-4x.svg +346 -0
  180. Pyjoyment/resources/public/pyjo/logo-white.png +0 -0
  181. Pyjoyment/resources/public/pyjo/logo-white.svg +346 -0
  182. Pyjoyment/resources/public/pyjo/logo.png +0 -0
  183. Pyjoyment/resources/public/pyjo/logo.svg +346 -0
  184. Pyjoyment/resources/public/pyjo/notfound.png +0 -0
  185. Pyjoyment/resources/public/pyjo/notfound.svg +3 -0
  186. Pyjoyment/resources/public/pyjo/nounicat.png +0 -0
  187. Pyjoyment/resources/public/pyjo/nounicat.svg +5 -0
  188. Pyjoyment/resources/public/pyjo/pinstripe-dark.png +0 -0
  189. Pyjoyment/resources/public/pyjo/pinstripe-dark.svg +4 -0
  190. Pyjoyment/resources/public/pyjo/pinstripe-light.png +0 -0
  191. Pyjoyment/resources/public/pyjo/pinstripe-light.svg +4 -0
  192. Pyjoyment/resources/public/pyjo/pyjo.css +204 -0
  193. Pyjoyment/resources/templates/pyjo/debug.html.ep +346 -0
  194. Pyjoyment/resources/templates/pyjo/exception.html.ep +32 -0
  195. Pyjoyment/resources/templates/pyjo/not_found.html.ep +45 -0
  196. pyjoyment-0.1.0.dist-info/METADATA +376 -0
  197. pyjoyment-0.1.0.dist-info/RECORD +200 -0
  198. pyjoyment-0.1.0.dist-info/WHEEL +4 -0
  199. pyjoyment-0.1.0.dist-info/entry_points.txt +8 -0
  200. pyjoyment-0.1.0.dist-info/licenses/LICENSE +201 -0
Pyjo/DOM/HTML.py ADDED
@@ -0,0 +1,550 @@
1
+ """
2
+ Pyjo.DOM.HTML - HTML/XML engine
3
+ ===============================
4
+ ::
5
+
6
+ from Pyjo.DOM.HTML import HTML
7
+
8
+ # Turn HTML into DOM tree
9
+ html = HTML()
10
+ html.parse('<div><p id="a">Test</p><p id="b">123</p></div>')
11
+ tree = html.tree
12
+
13
+ # Or import the module, as in Google Python Style Guide
14
+
15
+ import Pyjo.DOM.HTML
16
+
17
+ # Turn HTML into DOM tree
18
+ html = Pyjo.DOM.HTML.new()
19
+ html.parse('<div><p id="a">Test</p><p id="b">123</p></div>')
20
+ tree = html.tree
21
+
22
+ :mod:`Pyjo.DOM.HTML` is the HTML/XML engine used by :mod:`Pyjo.DOM`, based on
23
+ the `HTML Living Standard <https://html.spec.whatwg.org>`_ and the
24
+ `Extensible Markup Language (XML) 1.0 <https://www.w3.org/TR/xml/>`_.
25
+
26
+ See also
27
+ --------
28
+
29
+ :mod:`Pyjoyment`, https://mojolicious.org.
30
+
31
+ Ported from `Mojo::DOM::HTML <https://github.com/mojolicious/mojo/blob/v9.53/lib/Mojo/DOM/HTML.pm>`_
32
+ of Mojolicious 9.53.
33
+
34
+ Functions
35
+ ---------
36
+ """
37
+
38
+ import Pyjo.Base
39
+
40
+ from Pyjo.Util import html_attr_unescape, html_unescape, xml_escape
41
+
42
+ import re
43
+
44
+ from collections.abc import Callable
45
+ from typing import Any, Self, cast
46
+
47
+
48
+ # Any: a node is a list of its type, its content and its parent, and a tag also
49
+ # of its name, its attributes and its child nodes, as in Mojo
50
+ type Tree = list[Any]
51
+
52
+ _ATTR = r"""
53
+ ([^<>=\s/0-9.\-][^<>=\s/]*|/) # Key
54
+ (?:
55
+ \s*=\s*
56
+ (?s:(?P<quote>["'])(.*?)(?P=quote)|([^>\s]*)) # Value
57
+ )?
58
+ \s*
59
+ """
60
+ _ATTR_RE = re.compile(_ATTR, re.X)
61
+ _MARKUP = {
62
+ 'doctype': re.compile(
63
+ r"""
64
+ <!DOCTYPE(
65
+ \s+\w+ # Doctype
66
+ (?:(?:\s+\w+)?(?:\s+(?:"[^"]*"|'[^']*'))+)? # External ID
67
+ (?:\s+\[.+?\])? # Int Subset
68
+ \s*)>
69
+ """,
70
+ re.X | re.I | re.S,
71
+ ),
72
+ 'comment': re.compile(r'<!--(.*?)(?:--!?|(?<=<!--)-?(?=>))>', re.S),
73
+ 'cdata': re.compile(r'<!\[CDATA\[(.*?)\]\]>', re.I | re.S),
74
+ 'pi': re.compile(r'<\?(.*?)\?>', re.S),
75
+ }
76
+ _TAG_RE = re.compile(r'<(/\s*)?([^<>\s/0-9.\-][^<>\s/]*)\s*')
77
+ _TEXT_RE = re.compile(r'[^<]+')
78
+ _STRAY_RE = re.compile(r'=\s*')
79
+
80
+ # HTML elements that only contain raw text
81
+ RAW = {'script', 'style'}
82
+
83
+ # HTML elements that only contain raw text and entities
84
+ RCDATA = {'title', 'textarea'}
85
+
86
+ # HTML elements with optional end tags
87
+ END = {'body': 'head', 'optgroup': 'optgroup', 'option': 'option'}
88
+
89
+ # HTML elements that break paragraphs
90
+ for _name in (
91
+ *'address article aside blockquote details dialog div dl fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6'.split(),
92
+ *'header hgroup hr main menu nav ol p pre section table ul'.split(),
93
+ ):
94
+ END[_name] = 'p'
95
+
96
+ # Container HTML elements that create their own scope
97
+ SCOPE = {'math', 'svg'}
98
+
99
+ # SVG and MathML elements that are HTML integration points
100
+ INTEGRATION = {'desc', 'foreignobject', 'mi', 'mn', 'mo', 'ms', 'mtext', 'title'}
101
+
102
+ # HTML table elements with optional end tags
103
+ TABLE = {'colgroup', 'tbody', 'td', 'tfoot', 'th', 'thead', 'tr'}
104
+
105
+ # HTML elements with optional end tags and scoping rules
106
+ CLOSE: dict[str, tuple[set[str], set[str]]] = {'li': ({'li'}, {'ul', 'ol'}), 'tr': ({'tr'}, {'table'})}
107
+ for _name in ('colgroup', 'tbody', 'tfoot', 'thead'):
108
+ CLOSE[_name] = (TABLE, {'table'})
109
+ for _name in ('dd', 'dt'):
110
+ CLOSE[_name] = ({'dd', 'dt'}, {'dl'})
111
+ for _name in ('rp', 'rt'):
112
+ CLOSE[_name] = ({'rp', 'rt'}, {'ruby'})
113
+ for _name in ('td', 'th'):
114
+ CLOSE[_name] = ({'th', 'td'}, {'table'})
115
+
116
+ # HTML parent elements that signal no more content when closed, but that are also phrasing content
117
+ NO_MORE_CONTENT = {'ruby': ['rt', 'rp'], 'select': ['option', 'optgroup']}
118
+
119
+ # HTML elements without end tags
120
+ EMPTY = set('area base br col embed hr img input keygen link menuitem meta param source track wbr'.split())
121
+
122
+ # HTML elements categorized as phrasing content (and obsolete inline elements)
123
+ PHRASING = set(
124
+ (
125
+ 'a abbr area audio b bdi bdo br button canvas cite code data datalist del dfn em embed i iframe img input ins kbd '
126
+ 'keygen label link map mark math meta meter noscript object output picture progress q ruby s samp script select '
127
+ 'slot small span strong sub sup svg template textarea time u var video wbr '
128
+ 'acronym applet basefont big font strike tt'
129
+ ).split()
130
+ )
131
+
132
+ # HTML elements that don't get their self-closing flag acknowledged
133
+ BLOCK = set(
134
+ (
135
+ 'a address applet article aside b big blockquote body button caption center code col colgroup dd details dialog '
136
+ 'dir div dl dt em fieldset figcaption figure font footer form frameset h1 h2 h3 h4 h5 h6 head header hgroup html '
137
+ 'i iframe li listing main marquee menu nav nobr noembed noframes noscript object ol optgroup option p plaintext '
138
+ 'pre rp rt s script section select small strike strong style summary table tbody td template textarea tfoot th '
139
+ 'thead title tr tt u ul xmp'
140
+ ).split()
141
+ )
142
+
143
+
144
+ class Pyjo_DOM_HTML(Pyjo.Base.object):
145
+ """
146
+ :mod:`Pyjo.DOM.HTML` inherits all methods from :mod:`Pyjo.Base` and
147
+ implements the following new ones.
148
+ """
149
+
150
+ def __init__(self, tree: Tree | None = None, **kwargs: bool | None) -> None:
151
+ self.tree: Tree = tree if tree is not None else ['root']
152
+ """::
153
+
154
+ tree = html.tree
155
+ html.tree = ['root']
156
+
157
+ Document Object Model. Note that this structure should only be used very
158
+ carefully since it is very dynamic.
159
+ """
160
+ self._xml: bool | None = None
161
+ self._xml_set = False
162
+ if 'xml' in kwargs:
163
+ self.xml = kwargs['xml']
164
+
165
+ @property
166
+ def xml(self) -> bool | None:
167
+ """::
168
+
169
+ boolean = html.xml
170
+ html.xml = boolean
171
+
172
+ Disable HTML semantics in parser and activate case-sensitivity, defaults
173
+ to auto-detection based on a leading XML declaration.
174
+ """
175
+ return self._xml
176
+
177
+ @xml.setter
178
+ def xml(self, value: bool | None) -> None:
179
+ self._xml = value
180
+ self._xml_set = True
181
+
182
+ def parse(self, html: object) -> Self:
183
+ """::
184
+
185
+ html = html.parse('<foo bar="baz">I ♥ Pyjoyment!</foo>')
186
+
187
+ Parse HTML/XML fragment.
188
+ """
189
+ string = str(html)
190
+
191
+ # Detect XML from a leading declaration
192
+ if not self._xml_set and re.match(r'?\s*<\?xml[\s?]', string, re.I):
193
+ self.xml = True
194
+
195
+ xml = self.xml
196
+ tree: Tree = ['root']
197
+ current = [tree]
198
+ unclosed: set[str] = set()
199
+ pos = 0
200
+ while (offset := pos) < len(string):
201
+ # Text
202
+ if m := _TEXT_RE.match(string, offset):
203
+ pos = m.end()
204
+ _node(current[0], 'text', html_unescape(m[0]))
205
+ continue
206
+
207
+ # DOCTYPE, comment, CDATA and processing instruction (only trying the regex for the opener keeps failures fast)
208
+ next = string[offset + 1 : offset + 2]
209
+ if next in ('!', '?'):
210
+ opener = string[offset : offset + 9].upper()
211
+ type = (
212
+ 'comment'
213
+ if opener[:4] == '<!--'
214
+ else 'doctype'
215
+ if opener == '<!DOCTYPE'
216
+ else 'cdata'
217
+ if opener == '<![CDATA['
218
+ else 'pi'
219
+ if next == '?'
220
+ else None
221
+ )
222
+
223
+ if type and type not in unclosed:
224
+ if m := _MARKUP[type].match(string, offset):
225
+ pos, value = m.end(), m[1]
226
+
227
+ # DOCTYPE, CDATA (outside of foreign content) and processing instructions end at the first ">"
228
+ # in HTML
229
+ i = -1 if xml or type == 'comment' else value.find('>')
230
+ if i >= 0 and not (type == 'cdata' and _foreign(current[0])):
231
+ pos = m.start(1) + i + 1
232
+ value = value[:i]
233
+
234
+ _node(current[0], type, value)
235
+ continue
236
+
237
+ # Without an end for this one, there is none for later ones either (DOCTYPE can fail for other
238
+ # reasons)
239
+ if type != 'doctype':
240
+ unclosed.add(type)
241
+
242
+ # Tag
243
+ if m := _TAG_RE.match(string, offset):
244
+ pos = m.end()
245
+ is_end, tag = m[1] is not None, m[2] if xml else m[2].lower()
246
+
247
+ # Stray "=" right after the tag name (like an empty attribute name)
248
+ if stray := _STRAY_RE.match(string, pos):
249
+ pos = stray.end()
250
+
251
+ # Attributes
252
+ attrs: dict[str, str | None] = {}
253
+ closing = False
254
+ while a := _ATTR_RE.match(string, pos):
255
+ pos = a.end()
256
+ key = a[1] if xml else a[1].lower()
257
+ value = a[3] if a[3] is not None else a[4]
258
+
259
+ # Empty tag
260
+ if key == '/':
261
+ closing = True
262
+ continue
263
+
264
+ attrs[key] = html_attr_unescape(value) if value is not None else value
265
+
266
+ # Incomplete tag
267
+ if not string.startswith('>', pos):
268
+ pos = offset
269
+
270
+ # End
271
+ elif is_end:
272
+ pos += 1
273
+
274
+ # No more content
275
+ if not xml and (tags := NO_MORE_CONTENT.get(tag)):
276
+ for t in tags:
277
+ _end(t, xml, current)
278
+
279
+ _end(tag, xml, current)
280
+ continue
281
+
282
+ # Start
283
+ else:
284
+ pos += 1
285
+
286
+ # "image" is an alias for "img"
287
+ if not xml and tag == 'image':
288
+ tag = 'img'
289
+ _start(tag, attrs, xml, current)
290
+
291
+ # Element without end tag (self-closing)
292
+ if not xml and tag in EMPTY or (xml or tag not in BLOCK) and closing:
293
+ _end(tag, xml, current)
294
+
295
+ # Raw text elements (not in foreign content)
296
+ if xml or tag not in RAW and tag not in RCDATA or _foreign(current[0][3]):
297
+ continue
298
+
299
+ if tag == 'script':
300
+ script, found_end, pos = _script_content(string, pos)
301
+ if len(script):
302
+ _node(current[0], 'raw', script)
303
+ if found_end:
304
+ _end(tag, False, current)
305
+ continue
306
+
307
+ # Without an end tag for this element, there is none for later elements of the same type either
308
+ if tag in unclosed:
309
+ continue
310
+ if not (
311
+ raw := re.compile(rf'(.*?)</{re.escape(tag)}(?=[\s/>])[^>]*>?', re.S | re.I).match(string, pos)
312
+ ):
313
+ unclosed.add(tag)
314
+ continue
315
+ pos = raw.end()
316
+ if tag in RCDATA:
317
+ _node(current[0], 'text', html_unescape(raw[1]))
318
+ else:
319
+ _node(current[0], 'raw', raw[1])
320
+ _end(tag, False, current)
321
+ continue
322
+
323
+ # Runaway "<"
324
+ _node(current[0], 'text', '<')
325
+ pos = offset + 1
326
+
327
+ self.tree = tree
328
+ return self
329
+
330
+ def render(self) -> str:
331
+ """::
332
+
333
+ string = html.render()
334
+
335
+ Render DOM to HTML/XML.
336
+ """
337
+ return _render(self.tree, self.xml)
338
+
339
+ def tag(self, name: str, content: str | Callable[[], str] | None = None, /, **attrs: Any) -> Self:
340
+ """::
341
+
342
+ html = html.tag('div', 'safe content', id='foo')
343
+
344
+ Generate HTML/XML tag.
345
+ """
346
+ self.tree = ['root', _tag(name, content, attrs)]
347
+ return self
348
+
349
+
350
+ def tag_to_html(name: str, content: str | Callable[[], str] | None = None, /, **attrs: Any) -> str:
351
+ """::
352
+
353
+ string = tag_to_html('div', 'safe content', id='foo')
354
+
355
+ Generate HTML/XML tag and render it right away. This is a significantly
356
+ faster alternative to :meth:`~Pyjo_DOM_HTML.tag` for template systems that
357
+ have to generate a lot of tags.
358
+ """
359
+ return _render(_tag(name, content, attrs), None)
360
+
361
+
362
+ def _end(end: str, xml: bool | None, current: list[Tree]) -> None:
363
+
364
+ # Search stack for start tag
365
+ next: Tree | None = current[0]
366
+ while next is not None:
367
+ # Ignore useless end tag
368
+ if next[0] == 'root':
369
+ return
370
+
371
+ # Don’t traverse a container tag
372
+ if next[1] in SCOPE and next[1] != end:
373
+ return
374
+
375
+ # Right tag
376
+ if next[1] == end:
377
+ current[0] = next[3]
378
+ return
379
+
380
+ # Phrasing content can only cross phrasing content
381
+ if not xml and end in PHRASING and next[1] not in PHRASING:
382
+ return
383
+
384
+ next = next[3]
385
+
386
+
387
+ def _foreign(next: Tree) -> bool:
388
+
389
+ # Search stack for SVG and MathML elements, but stop at HTML integration points
390
+ while next[0] != 'root':
391
+ if next[1] in SCOPE:
392
+ return True
393
+ if next[1] in INTEGRATION:
394
+ return False
395
+ if next[1] == 'annotation-xml' and re.match(
396
+ r'(?:text/html|application/xhtml\+xml)\Z', next[2].get('encoding') or '', re.I
397
+ ):
398
+ return False
399
+ next = next[3]
400
+
401
+ return False
402
+
403
+
404
+ def _node(current: Tree, type: str, content: str) -> None:
405
+ current.append([type, content, current])
406
+
407
+
408
+ def _render(tree: Tree, xml: bool | None) -> str:
409
+
410
+ # Tag
411
+ type = tree[0]
412
+ if type == 'tag':
413
+ # Start tag
414
+ tag = tree[1]
415
+ result = f'<{tag}'
416
+
417
+ # Attributes
418
+ for key in sorted(tree[2]):
419
+ value = tree[2][key]
420
+ if value is None:
421
+ result += f' {key}="{key}"' if xml else f' {key}'
422
+ continue
423
+ result += f' {key}="' + xml_escape(value if isinstance(value, str) else str(value)) + '"'
424
+
425
+ # No children
426
+ if len(tree) <= 4:
427
+ return f'{result} />' if xml else f'{result}>' if tag in EMPTY else f'{result}></{tag}>'
428
+
429
+ # Children
430
+ result += '>' + ''.join(_render(n, xml) for n in tree[4:])
431
+
432
+ # End tag
433
+ return f'{result}</{tag}>'
434
+
435
+ # Text (escaped)
436
+ if type == 'text':
437
+ return xml_escape(tree[1])
438
+
439
+ # Raw text
440
+ if type == 'raw':
441
+ return tree[1]
442
+
443
+ # Root
444
+ if type == 'root':
445
+ return ''.join(_render(n, xml) for n in tree[1:])
446
+
447
+ # DOCTYPE
448
+ if type == 'doctype':
449
+ return '<!DOCTYPE' + tree[1] + '>'
450
+
451
+ # Comment
452
+ if type == 'comment':
453
+ return '<!--' + tree[1] + '-->'
454
+
455
+ # CDATA
456
+ if type == 'cdata':
457
+ return '<![CDATA[' + tree[1] + ']]>'
458
+
459
+ # Processing instruction
460
+ if type == 'pi':
461
+ return '<?' + tree[1] + '?>'
462
+
463
+ # Everything else
464
+ return ''
465
+
466
+
467
+ _SCRIPT_END_RE = re.compile(r'</script(?=[\s/>])[^>]*>?', re.S | re.I)
468
+ _SCRIPT_END_TAG_RE = re.compile(r'</script(?=[\s/>])', re.S | re.I)
469
+
470
+
471
+ def _script_content(html: str, pos: int) -> tuple[str, bool, int]:
472
+ start = pos
473
+
474
+ state = 0
475
+
476
+ while True:
477
+ pos = re.compile(r'[^<]*' if state == 0 else r'[^<\-]*', re.S).match(html, pos).end() # pyright: ignore[reportOptionalMemberAccess]
478
+
479
+ p = pos
480
+ if p >= len(html):
481
+ return html[start:], False, pos
482
+
483
+ if state == 0:
484
+ if m := _SCRIPT_END_RE.match(html, pos):
485
+ return html[start:p], True, m.end()
486
+ elif html.startswith('<!--', pos):
487
+ pos, state = pos + 4, 1
488
+ else:
489
+ pos = p + 1
490
+ elif state == 1:
491
+ if m := _SCRIPT_END_RE.match(html, pos):
492
+ return html[start:p], True, m.end()
493
+ elif html.startswith('-->', pos):
494
+ pos, state = pos + 3, 0
495
+ elif m := re.compile(r'<script(?=[\s/>])', re.S | re.I).match(html, pos):
496
+ pos, state = m.end(), 2
497
+ else:
498
+ pos = p + 1
499
+ else:
500
+ if m := _SCRIPT_END_TAG_RE.match(html, pos):
501
+ pos, state = m.end(), 1
502
+ elif html.startswith('-->', pos):
503
+ pos, state = pos + 3, 0
504
+ else:
505
+ pos = p + 1
506
+
507
+
508
+ def _start(start: str, attrs: dict[str, str | None], xml: bool | None, current: list[Tree]) -> None:
509
+
510
+ # Autoclose optional HTML elements
511
+ if not xml and current[0][0] != 'root':
512
+ if end := END.get(start):
513
+ _end(end, False, current)
514
+
515
+ elif close := CLOSE.get(start):
516
+ allowed, scope = close
517
+
518
+ # Close allowed parent elements in scope
519
+ parent = current[0]
520
+ while parent[0] != 'root' and parent[1] not in scope:
521
+ if parent[1] in allowed:
522
+ _end(parent[1], False, current)
523
+ parent = parent[3]
524
+
525
+ # New tag
526
+ new: Tree = ['tag', start, attrs, current[0]]
527
+ current[0].append(new)
528
+ current[0] = new
529
+
530
+
531
+ def _tag(name: str, content: str | Callable[[], str] | None, attrs: dict[str, Any]) -> Tree:
532
+ tree: Tree = ['tag', name, None, None]
533
+
534
+ # Content
535
+ if content is not None:
536
+ tree.append(['raw', content()] if callable(content) else ['text', content])
537
+
538
+ # Attributes
539
+ tree[2] = attrs = dict(attrs)
540
+ if not isinstance(data := attrs.get('data'), dict):
541
+ return tree
542
+ del attrs['data']
543
+ for key, value in cast(dict[Any, Any], data).items():
544
+ attrs['data-' + str(key).replace('_', '-').lower()] = value
545
+ return tree
546
+
547
+
548
+ new = Pyjo_DOM_HTML.new
549
+ object = Pyjo_DOM_HTML
550
+ HTML = Pyjo_DOM_HTML