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/__init__.py ADDED
@@ -0,0 +1,1329 @@
1
+ r"""
2
+ Pyjo.DOM - Minimalistic HTML/XML DOM parser with CSS selectors
3
+ ==============================================================
4
+ ::
5
+
6
+ from Pyjo.DOM import DOM
7
+
8
+ # Parse
9
+ dom = DOM('<div><p id="a">Test</p><p id="b">123</p></div>')
10
+
11
+ # Find
12
+ print(dom.at('#b').text)
13
+ print(dom.find('p').map('text').join("\n"))
14
+ print(dom.find('[id]').map('attr', 'id').join("\n"))
15
+
16
+ # Or import the module, as in Google Python Style Guide
17
+
18
+ import Pyjo.DOM
19
+
20
+ # Parse
21
+ dom = Pyjo.DOM.new('<div><p id="a">Test</p><p id="b">123</p></div>')
22
+
23
+ # Find
24
+ print(dom.at('#b').text)
25
+ print(dom.find('p').map('text').join("\n"))
26
+ print(dom.find('[id]').map('attr', 'id').join("\n"))
27
+
28
+ # Iterate
29
+ dom.find('p[id]').reverse().each(lambda e, num: print(e['id']))
30
+
31
+ # Loop
32
+ for e in dom.find('p[id]').each():
33
+ print(e['id'], ':', e.text)
34
+
35
+ # Modify
36
+ dom.find('div p').last().append('<p id="c">456</p>')
37
+ dom.at('#c').prepend(dom.new_tag('p', '789', id='d'))
38
+ dom.find(':not(p)').map('strip')
39
+
40
+ # Render
41
+ print(dom)
42
+
43
+ :mod:`Pyjo.DOM` is a minimalistic and relaxed HTML/XML DOM parser with CSS
44
+ selector support. It will even try to interpret broken HTML and XML, so you
45
+ should not use it for validation or sanitization. Its interpretation of broken
46
+ markup can differ from that of a web browser.
47
+
48
+ Nodes and elements
49
+ ------------------
50
+
51
+ When we parse an HTML/XML fragment, it gets turned into a tree of nodes. ::
52
+
53
+ <!DOCTYPE html>
54
+ <html>
55
+ <head><title>Hello</title></head>
56
+ <body>World!</body>
57
+ </html>
58
+
59
+ There are currently eight different kinds of nodes, ``cdata``, ``comment``,
60
+ ``doctype``, ``pi``, ``raw``, ``root``, ``tag`` and ``text``. Elements are
61
+ nodes of the type ``tag``. ::
62
+
63
+ root
64
+ |- doctype (html)
65
+ +- tag (html)
66
+ |- tag (head)
67
+ | +- tag (title)
68
+ | +- text (Hello)
69
+ +- tag (body)
70
+ +- text (World!)
71
+
72
+ While all node types are represented as :mod:`Pyjo.DOM` objects, some methods
73
+ like :meth:`~Pyjo_DOM.attr` and :attr:`~Pyjo_DOM.namespace` only apply to
74
+ elements.
75
+
76
+ HTML and XML
77
+ ------------
78
+
79
+ :mod:`Pyjo.DOM` defaults to HTML semantics, that means all tags and attribute
80
+ names are lowercased and selectors need to be lowercase as well. ::
81
+
82
+ # HTML semantics
83
+ dom = Pyjo.DOM.new('<P ID="greeting">Hi!</P>')
84
+ print(dom.at('p[id]').text)
85
+
86
+ If the document starts with an XML declaration, the parser will automatically
87
+ switch into XML mode and everything becomes case-sensitive. ::
88
+
89
+ # XML semantics
90
+ dom = Pyjo.DOM.new('<?xml version="1.0"?><P ID="greeting">Hi!</P>')
91
+ print(dom.at('P[ID]').text)
92
+
93
+ HTML or XML semantics can also be forced with the :attr:`~Pyjo_DOM.xml`
94
+ attribute. ::
95
+
96
+ # Force HTML semantics
97
+ dom = Pyjo.DOM.new().set(xml=False).parse('<P ID="greeting">Hi!</P>')
98
+ print(dom.at('p[id]').text)
99
+
100
+ # Force XML semantics
101
+ dom = Pyjo.DOM.new().set(xml=True).parse('<P ID="greeting">Hi!</P>')
102
+ print(dom.at('P[ID]').text)
103
+
104
+ See also
105
+ --------
106
+
107
+ :mod:`Pyjoyment`, https://mojolicious.org.
108
+
109
+ Ported from `Mojo::DOM <https://github.com/mojolicious/mojo/blob/v9.51/lib/Mojo/DOM.pm>`_
110
+ of Mojolicious 9.51.
111
+
112
+ Classes
113
+ -------
114
+ """
115
+
116
+ import Pyjo.Base
117
+ import Pyjo.Collection
118
+ import Pyjo.DOM.CSS
119
+ import Pyjo.DOM.HTML
120
+ import Pyjo.String.Mixin
121
+
122
+ import builtins
123
+ import re
124
+
125
+ from collections.abc import Callable, Iterator
126
+ from typing import Any, Self, cast, overload
127
+
128
+
129
+ type Anything = builtins.object
130
+
131
+ # Any: a node of the tree of Pyjo.DOM.HTML
132
+ type Tree = list[Any]
133
+
134
+
135
+ class _hybridmethod[C, **P, R]:
136
+ """A method that is called on the class or on an object, as Perl's can be."""
137
+
138
+ def __init__(self, func: Callable[..., R]) -> None:
139
+ self.func = func
140
+ self.__doc__ = func.__doc__
141
+
142
+ def __get__(self, obj: Anything, objtype: type | None = None) -> Callable[..., R]:
143
+ def bound(*args: Any, **kwargs: Any) -> R:
144
+ return self.func(obj if obj is not None else objtype, *args, **kwargs)
145
+
146
+ bound.__doc__ = self.__doc__
147
+ return bound
148
+
149
+
150
+ class Pyjo_DOM(Pyjo.Base.object, Pyjo.String.Mixin.object):
151
+ """
152
+ :mod:`Pyjo.DOM` inherits all methods from :mod:`Pyjo.Base` and
153
+ :mod:`Pyjo.String.Mixin` and implements the following new ones.
154
+ """
155
+
156
+ def __init__(self, html: object = None) -> None:
157
+ """::
158
+
159
+ dom = Pyjo.DOM.new()
160
+ dom = Pyjo.DOM.new('<foo bar="baz">I ♥ Pyjoyment!</foo>')
161
+
162
+ Construct a new :mod:`Pyjo.DOM` object and :meth:`parse` HTML/XML
163
+ fragment if necessary.
164
+ """
165
+ self._html = Pyjo.DOM.HTML.new()
166
+ if html is not None:
167
+ self.parse(html)
168
+
169
+ def __bool__(self) -> bool:
170
+ """::
171
+
172
+ boolean = bool(dom)
173
+
174
+ Always true.
175
+ """
176
+ return True
177
+
178
+ def __contains__(self, name: object) -> bool:
179
+ """::
180
+
181
+ boolean = 'id' in dom
182
+
183
+ Check if this element has the attribute.
184
+ """
185
+ return name in self.attr()
186
+
187
+ def __delitem__(self, name: str) -> None:
188
+ """::
189
+
190
+ del dom['id']
191
+
192
+ Remove an attribute of this element.
193
+ """
194
+ self.attr().pop(name, None)
195
+
196
+ @overload
197
+ def __getitem__(self, key: str) -> str | None: ...
198
+
199
+ @overload
200
+ def __getitem__(self, key: int) -> 'Pyjo_DOM': ...
201
+
202
+ @overload
203
+ def __getitem__(self, key: slice) -> 'Pyjo.Collection.object[Pyjo_DOM]': ...
204
+
205
+ def __getitem__(self, key: str | int | slice) -> 'str | None | Pyjo_DOM | Pyjo.Collection.object[Pyjo_DOM]':
206
+ """::
207
+
208
+ value = dom['id']
209
+ node = dom[0]
210
+
211
+ An attribute of this element, as :meth:`attr` gives it, or a child node,
212
+ as :attr:`child_nodes` has it. ::
213
+
214
+ # "test"
215
+ dom.parse('<div id="test">Test</div>').at('div')['id']
216
+
217
+ # "<!-- Test -->"
218
+ dom.parse('<!-- Test --><b>123</b>')[0]
219
+ """
220
+ if isinstance(key, str):
221
+ return self.attr(key)
222
+ if isinstance(key, slice):
223
+ return Pyjo.Collection.object(self.child_nodes[key])
224
+ return self.child_nodes[key]
225
+
226
+ def __iter__(self) -> Iterator['Pyjo_DOM']:
227
+ """::
228
+
229
+ for node in dom:
230
+ ...
231
+
232
+ Iterate the child nodes, as :attr:`child_nodes` has them.
233
+ """
234
+ return iter(self.child_nodes)
235
+
236
+ def __len__(self) -> int:
237
+ """::
238
+
239
+ size = len(dom)
240
+
241
+ Number of child nodes.
242
+ """
243
+ return len(_nodes(self.tree))
244
+
245
+ def __setitem__(self, name: str, value: str | None) -> None:
246
+ """::
247
+
248
+ dom['id'] = 'test'
249
+
250
+ Set an attribute of this element, as :meth:`attr` does.
251
+ """
252
+ self.attr({name: value})
253
+
254
+ @property
255
+ def all_text(self) -> str:
256
+ r"""::
257
+
258
+ text = dom.all_text
259
+
260
+ Extract text content from all descendant nodes of this element. For
261
+ HTML documents ``script`` and ``style`` elements are excluded. ::
262
+
263
+ # "foo\nbarbaz\n"
264
+ dom.parse("<div>foo\n<p>bar</p>baz\n</div>").at('div').all_text
265
+ """
266
+ return _text(_nodes(self.tree), self.xml, True)
267
+
268
+ def ancestors(self, css: object = None) -> 'Pyjo.Collection.object[Pyjo_DOM]':
269
+ """::
270
+
271
+ collection = dom.ancestors()
272
+ collection = dom.ancestors('div ~ p')
273
+
274
+ Find all ancestor elements of this node matching the CSS selector and
275
+ return a :mod:`Pyjo.Collection` object containing these elements as
276
+ :mod:`Pyjo.DOM` objects. All selectors from :mod:`Pyjo.DOM.CSS` are
277
+ supported. ::
278
+
279
+ # List tag names of ancestor elements
280
+ print(dom.ancestors().map('tag').join("\\n"))
281
+ """
282
+ return _select(self._collect(_ancestors(self.tree)), css)
283
+
284
+ def append(self, new: object) -> Self:
285
+ """::
286
+
287
+ dom = dom.append('<p>I ♥ Pyjoyment!</p>')
288
+ dom = dom.append(Pyjo.DOM.new())
289
+
290
+ Append HTML/XML fragment to this node (for all node types other than
291
+ ``root``). ::
292
+
293
+ # "<div><h1>Test</h1><h2>123</h2></div>"
294
+ dom.parse('<div><h1>Test</h1></div>').at('h1').append('<h2>123</h2>').root
295
+
296
+ # "<p>Test 123</p>"
297
+ dom.parse('<p>Test</p>').at('p').child_nodes.first().append(' 123').root
298
+ """
299
+ return self._add(1, new)
300
+
301
+ def append_content(self, new: object) -> Self:
302
+ """::
303
+
304
+ dom = dom.append_content('<p>I ♥ Pyjoyment!</p>')
305
+ dom = dom.append_content(Pyjo.DOM.new())
306
+
307
+ Append HTML/XML fragment (for ``root`` and ``tag`` nodes) or raw
308
+ content to this node's content. ::
309
+
310
+ # "<div><h1>Test123</h1></div>"
311
+ dom.parse('<div><h1>Test</h1></div>').at('h1').append_content('123').root
312
+
313
+ # "<!-- Test 123 --><br>"
314
+ dom.parse('<!-- Test --><br>').child_nodes.first().append_content('123 ').root
315
+
316
+ # "<p>Test<i>123</i></p>"
317
+ dom.parse('<p>Test</p>').at('p').append_content('<i>123</i>').root
318
+ """
319
+ return self._content(True, False, new)
320
+
321
+ def at(self, css: object, **ns: str) -> 'Pyjo_DOM | None':
322
+ r"""::
323
+
324
+ result = dom.at('div ~ p')
325
+ result = dom.at('svg|line', svg='http://www.w3.org/2000/svg')
326
+
327
+ Find first descendant element of this element matching the CSS
328
+ selector and return it as a :mod:`Pyjo.DOM` object, or ``None`` if none
329
+ could be found. All selectors from :mod:`Pyjo.DOM.CSS` are supported. ::
330
+
331
+ # Find first element with "svg" namespace definition
332
+ namespace = dom.at('[xmlns\:svg]')['xmlns:svg']
333
+
334
+ Trailing keyword arguments can be used to declare xml namespace
335
+ aliases. ::
336
+
337
+ # "<rect></rect>"
338
+ dom.parse('<svg xmlns="http://www.w3.org/2000/svg"><rect /></svg>').at(
339
+ 'svg|rect', svg='http://www.w3.org/2000/svg'
340
+ )
341
+ """
342
+ if (result := self._css().select_one(css, **ns)) is None:
343
+ return None
344
+ return self._build(result, self.xml)
345
+
346
+ # attr(**values) with no values is attr(), which the first overload types
347
+ @overload
348
+ def attr(self) -> dict[str, str | None]: ... # pyright: ignore[reportOverlappingOverload]
349
+
350
+ @overload
351
+ def attr(self, name: str, /) -> str | None: ...
352
+
353
+ @overload
354
+ def attr(self, name: str, value: str | None, /) -> Self: ...
355
+
356
+ @overload
357
+ def attr(self, values: dict[str, Any], /) -> Self: ...
358
+
359
+ @overload
360
+ def attr(self, **values: Any) -> Self: ...
361
+
362
+ def attr(self, *args: Any, **kwargs: Any) -> Any:
363
+ """::
364
+
365
+ attrs = dom.attr()
366
+ foo = dom.attr('foo')
367
+ dom = dom.attr({'foo': 'bar'})
368
+ dom = dom.attr('foo', 'bar')
369
+ dom = dom.attr(foo='bar')
370
+
371
+ This element's attributes. ::
372
+
373
+ # Remove an attribute
374
+ del dom.attr()['id']
375
+
376
+ # Attribute without value
377
+ dom.attr(selected=None)
378
+
379
+ # List id attributes
380
+ print(dom.find('*').map('attr', 'id').compact().join("\\n"))
381
+ """
382
+
383
+ # Hash
384
+ tree = self.tree
385
+ attrs: dict[str, Any] = tree[2] if tree[0] == 'tag' else {}
386
+ if not len(args) and not len(kwargs):
387
+ return attrs
388
+
389
+ # Get
390
+ if len(args) == 1 and not len(kwargs) and not isinstance(args[0], dict):
391
+ return attrs.get(args[0])
392
+
393
+ # Set
394
+ values = cast(dict[str, Any], args[0] if len(args) == 1 else dict(zip(args[::2], args[1::2], strict=True)))
395
+ attrs.update(values, **kwargs)
396
+
397
+ return self
398
+
399
+ @property
400
+ def child_nodes(self) -> 'Pyjo.Collection.object[Pyjo_DOM]':
401
+ """::
402
+
403
+ collection = dom.child_nodes
404
+
405
+ Return a :mod:`Pyjo.Collection` object containing all child nodes of
406
+ this element as :mod:`Pyjo.DOM` objects. ::
407
+
408
+ # "<p><b>123</b></p>"
409
+ dom.parse('<p>Test<b>123</b></p>').at('p').child_nodes.first().remove()
410
+
411
+ # "<!DOCTYPE html>"
412
+ dom.parse('<!DOCTYPE html><b>123</b>').child_nodes.first()
413
+
414
+ # " Test "
415
+ dom.parse('<b>123</b><!-- Test -->').child_nodes.last().content
416
+ """
417
+ return self._collect(_nodes(self.tree))
418
+
419
+ def children(self, css: object = None) -> 'Pyjo.Collection.object[Pyjo_DOM]':
420
+ """::
421
+
422
+ collection = dom.children()
423
+ collection = dom.children('div ~ p')
424
+
425
+ Find all child elements of this element matching the CSS selector and
426
+ return a :mod:`Pyjo.Collection` object containing these elements as
427
+ :mod:`Pyjo.DOM` objects. All selectors from :mod:`Pyjo.DOM.CSS` are
428
+ supported. ::
429
+
430
+ # Show tag name of random child element
431
+ print(dom.children().shuffle().first().tag)
432
+ """
433
+ return _select(self._collect(_nodes(self.tree, True)), css)
434
+
435
+ @property
436
+ def content(self) -> str:
437
+ """::
438
+
439
+ string = dom.content
440
+ dom.content = '<p>I ♥ Pyjoyment!</p>'
441
+ dom.content = Pyjo.DOM.new()
442
+
443
+ Return this node's content or replace it with HTML/XML fragment (for
444
+ ``root`` and ``tag`` nodes) or raw content. ::
445
+
446
+ # "<b>Test</b>"
447
+ dom.parse('<div><b>Test</b></div>').at('div').content
448
+
449
+ # "<div><h1>123</h1></div>"
450
+ dom.parse('<div><h1>Test</h1></div>').at('h1').set(content='123').root
451
+
452
+ # "<p><i>123</i></p>"
453
+ dom.parse('<p>Test</p>').at('p').set(content='<i>123</i>').root
454
+
455
+ # "<div><h1></h1></div>"
456
+ dom.parse('<div><h1>Test</h1></div>').at('h1').set(content='').root
457
+
458
+ # " Test "
459
+ dom.parse('<!-- Test --><br>').child_nodes.first().content
460
+
461
+ # "<div><!-- 123 -->456</div>"
462
+ dom.parse('<div><!-- Test -->456</div>').at('div').child_nodes.first().set(content=' 123 ').root
463
+ """
464
+ type = self.type
465
+ if type in ('root', 'tag'):
466
+ html = Pyjo.DOM.HTML.new(xml=self.xml)
467
+ return ''.join(html.set(tree=n).render() for n in _nodes(self.tree))
468
+
469
+ return self.tree[1]
470
+
471
+ @content.setter
472
+ def content(self, value: object) -> None:
473
+ type = self.type
474
+ if type in ('root', 'tag'):
475
+ self._content(False, True, value)
476
+ return
477
+
478
+ self.tree[1] = value if isinstance(value, str) else str(value)
479
+
480
+ @property
481
+ def descendant_nodes(self) -> 'Pyjo.Collection.object[Pyjo_DOM]':
482
+ """::
483
+
484
+ collection = dom.descendant_nodes
485
+
486
+ Return a :mod:`Pyjo.Collection` object containing all descendant nodes
487
+ of this element as :mod:`Pyjo.DOM` objects. ::
488
+
489
+ # "<p><b>123</b></p>"
490
+ dom.parse('<p><!-- Test --><b>123<!-- 456 --></b></p>').descendant_nodes.grep(
491
+ lambda e: e.type == 'comment'
492
+ ).map('remove').first()
493
+
494
+ # "<p><b>test</b>test</p>"
495
+ dom.parse('<p><b>123</b>456</p>').at('p').descendant_nodes.grep(
496
+ lambda e: e.type == 'text'
497
+ ).map('content', 'test').first().root
498
+ """
499
+ return self._collect(_all(_nodes(self.tree)))
500
+
501
+ def find(self, css: object, **ns: str) -> 'Pyjo.Collection.object[Pyjo_DOM]':
502
+ r"""::
503
+
504
+ collection = dom.find('div ~ p')
505
+ collection = dom.find('svg|line', svg='http://www.w3.org/2000/svg')
506
+
507
+ Find all descendant elements of this element matching the CSS selector
508
+ and return a :mod:`Pyjo.Collection` object containing these elements as
509
+ :mod:`Pyjo.DOM` objects. All selectors from :mod:`Pyjo.DOM.CSS` are
510
+ supported. ::
511
+
512
+ # Find a specific element and extract information
513
+ id = dom.find('div')[23]['id']
514
+
515
+ # Extract information from multiple elements
516
+ headers = dom.find('h1, h2, h3').map('text').each()
517
+
518
+ # Count all the different tags
519
+ counts = dom.find('*').reduce(lambda a, b: a | {b.tag: a.get(b.tag, 0) + 1}, {})
520
+
521
+ # Find elements with a class that contains dots
522
+ divs = dom.find('div.foo\.bar').each()
523
+
524
+ Trailing keyword arguments can be used to declare xml namespace
525
+ aliases. ::
526
+
527
+ # "<rect></rect>"
528
+ dom.parse('<svg xmlns="http://www.w3.org/2000/svg"><rect /></svg>').find(
529
+ 'svg|rect', svg='http://www.w3.org/2000/svg'
530
+ ).first()
531
+ """
532
+ return self._collect(self._css().select(css, **ns))
533
+
534
+ def following(self, css: object = None) -> 'Pyjo.Collection.object[Pyjo_DOM]':
535
+ """::
536
+
537
+ collection = dom.following()
538
+ collection = dom.following('div ~ p')
539
+
540
+ Find all sibling elements after this node matching the CSS selector and
541
+ return a :mod:`Pyjo.Collection` object containing these elements as
542
+ :mod:`Pyjo.DOM` objects. All selectors from :mod:`Pyjo.DOM.CSS` are
543
+ supported. ::
544
+
545
+ # List tags of sibling elements after this node
546
+ print(dom.following().map('tag').join("\\n"))
547
+ """
548
+ return _select(self._collect(_siblings(self.tree, True, True)), css)
549
+
550
+ @property
551
+ def following_nodes(self) -> 'Pyjo.Collection.object[Pyjo_DOM]':
552
+ """::
553
+
554
+ collection = dom.following_nodes
555
+
556
+ Return a :mod:`Pyjo.Collection` object containing all sibling nodes
557
+ after this node as :mod:`Pyjo.DOM` objects. ::
558
+
559
+ # "C"
560
+ dom.parse('<p>A</p><!-- B -->C').at('p').following_nodes.last().content
561
+ """
562
+ return self._collect(_siblings(self.tree, False, True))
563
+
564
+ def matches(self, css: object, **ns: str) -> bool | None:
565
+ """::
566
+
567
+ boolean = dom.matches('div ~ p')
568
+ boolean = dom.matches('svg|line', svg='http://www.w3.org/2000/svg')
569
+
570
+ Check if this element matches the CSS selector. All selectors from
571
+ :mod:`Pyjo.DOM.CSS` are supported. ::
572
+
573
+ # True
574
+ dom.parse('<p class="a">A</p>').at('p').matches('.a')
575
+ dom.parse('<p class="a">A</p>').at('p').matches('p[class]')
576
+
577
+ # False
578
+ dom.parse('<p class="a">A</p>').at('p').matches('.b')
579
+ dom.parse('<p class="a">A</p>').at('p').matches('p[id]')
580
+
581
+ Trailing keyword arguments can be used to declare xml namespace
582
+ aliases. ::
583
+
584
+ # True
585
+ dom.parse('<svg xmlns="http://www.w3.org/2000/svg"><rect /></svg>').matches(
586
+ 'svg|rect', svg='http://www.w3.org/2000/svg'
587
+ )
588
+ """
589
+ return self._css().matches(css, **ns)
590
+
591
+ @property
592
+ def namespace(self) -> str | None:
593
+ r"""::
594
+
595
+ namespace = dom.namespace
596
+
597
+ Find this element's namespace, or return ``None`` if none could be
598
+ found. ::
599
+
600
+ # "http://www.w3.org/2000/svg"
601
+ Pyjo.DOM.new('<svg xmlns:svg="http://www.w3.org/2000/svg"><svg:circle>3.14</svg:circle></svg>').at(
602
+ 'svg\:circle'
603
+ ).namespace
604
+
605
+ # Find namespace for an element with namespace prefix
606
+ namespace = dom.at('svg > svg\:circle').namespace
607
+
608
+ # Find namespace for an element that may or may not have a namespace prefix
609
+ namespace = dom.at('svg > circle').namespace
610
+ """
611
+ if (tree := self.tree)[0] != 'tag':
612
+ return None
613
+
614
+ # Extract namespace prefix and search parents
615
+ ns = f'xmlns:{m[1]}' if (m := re.match(r'^(.*?):', tree[1])) else None
616
+ for node in (tree, *_ancestors(tree)):
617
+ # Namespace for prefix
618
+ attrs = node[2]
619
+ if ns:
620
+ if ns in attrs:
621
+ return attrs[ns]
622
+
623
+ # Namespace attribute
624
+ elif attrs.get('xmlns') is not None:
625
+ return attrs['xmlns']
626
+
627
+ return None
628
+
629
+ @_hybridmethod
630
+ def new_tag(
631
+ self_or_cls: 'Pyjo_DOM | type[Pyjo_DOM]',
632
+ name: str,
633
+ content: str | Callable[[], str] | None = None,
634
+ /,
635
+ **attrs: Any,
636
+ ) -> 'Pyjo_DOM': # noqa: N805
637
+ """::
638
+
639
+ tag = Pyjo.DOM.object.new_tag('div')
640
+ tag = dom.new_tag('div')
641
+ tag = dom.new_tag('div', id='foo', hidden=None)
642
+ tag = dom.new_tag('div', 'safe content')
643
+ tag = dom.new_tag('div', 'safe content', id='foo')
644
+ tag = dom.new_tag('div', 'safe content', data={'pyjo': 'rocks'})
645
+ tag = dom.new_tag('div', lambda: 'unsafe content', id='foo')
646
+
647
+ Construct a new :mod:`Pyjo.DOM` object for an HTML/XML tag with or
648
+ without attributes and content. The ``data`` attribute may contain a
649
+ dict with key/value pairs to generate attributes from. ::
650
+
651
+ # "<br>"
652
+ dom.new_tag('br')
653
+
654
+ # "<div></div>"
655
+ dom.new_tag('div')
656
+
657
+ # "<div hidden id="foo"></div>"
658
+ dom.new_tag('div', id='foo', hidden=None)
659
+
660
+ # "<div>test &amp; 123</div>"
661
+ dom.new_tag('div', 'test & 123')
662
+
663
+ # "<div id="foo">test &amp; 123</div>"
664
+ dom.new_tag('div', 'test & 123', id='foo')
665
+
666
+ # "<div data-bar="test" data-foo="1">test &amp; 123</div>"
667
+ dom.new_tag('div', 'test & 123', data={'foo': 1, 'Bar': 'test'})
668
+
669
+ # "<div id="foo">test & 123</div>"
670
+ dom.new_tag('div', lambda: 'test & 123', id='foo')
671
+
672
+ # "<div>Hello<b>Pyjo!</b></div>"
673
+ dom.parse('<div>Hello</div>').at('div').append_content(dom.new_tag('b', 'Pyjo!')).root
674
+ """
675
+ new = self_or_cls.new()
676
+ new._html.tag(name, content, **attrs)
677
+ if isinstance(self_or_cls, Pyjo_DOM):
678
+ new._html.xml = self_or_cls._html.xml
679
+ return new
680
+
681
+ @property
682
+ def next(self) -> 'Pyjo_DOM | None':
683
+ """::
684
+
685
+ sibling = dom.next
686
+
687
+ Return :mod:`Pyjo.DOM` object for next sibling element, or ``None`` if
688
+ there are no more siblings. ::
689
+
690
+ # "<h2>123</h2>"
691
+ dom.parse('<div><h1>Test</h1><h2>123</h2></div>').at('h1').next
692
+ """
693
+ return self._maybe(_sibling(self.tree, True, True, 0))
694
+
695
+ @property
696
+ def next_node(self) -> 'Pyjo_DOM | None':
697
+ """::
698
+
699
+ sibling = dom.next_node
700
+
701
+ Return :mod:`Pyjo.DOM` object for next sibling node, or ``None`` if
702
+ there are no more siblings. ::
703
+
704
+ # "456"
705
+ dom.parse('<p><b>123</b><!-- Test -->456</p>').at('b').next_node.next_node
706
+
707
+ # " Test "
708
+ dom.parse('<p><b>123</b><!-- Test -->456</p>').at('b').next_node.content
709
+ """
710
+ return self._maybe(_sibling(self.tree, False, True, 0))
711
+
712
+ @property
713
+ def parent(self) -> 'Pyjo_DOM | None':
714
+ """::
715
+
716
+ parent = dom.parent
717
+
718
+ Return :mod:`Pyjo.DOM` object for parent of this node, or ``None`` if
719
+ this node has no parent. ::
720
+
721
+ # "<b><i>Test</i></b>"
722
+ dom.parse('<p><b><i>Test</i></b></p>').at('i').parent
723
+ """
724
+ if (tree := self.tree)[0] == 'root':
725
+ return None
726
+ return self._maybe(_parent(tree))
727
+
728
+ def parse(self, html: object) -> Self:
729
+ """::
730
+
731
+ dom = dom.parse('<foo bar="baz">I ♥ Pyjoyment!</foo>')
732
+
733
+ Parse HTML/XML fragment with :mod:`Pyjo.DOM.HTML`. ::
734
+
735
+ # Parse XML
736
+ dom = Pyjo.DOM.new().set(xml=True).parse('<foo>I ♥ Pyjoyment!</foo>')
737
+ """
738
+ self._html.parse(html)
739
+ return self
740
+
741
+ def preceding(self, css: object = None) -> 'Pyjo.Collection.object[Pyjo_DOM]':
742
+ """::
743
+
744
+ collection = dom.preceding()
745
+ collection = dom.preceding('div ~ p')
746
+
747
+ Find all sibling elements before this node matching the CSS selector and
748
+ return a :mod:`Pyjo.Collection` object containing these elements as
749
+ :mod:`Pyjo.DOM` objects. All selectors from :mod:`Pyjo.DOM.CSS` are
750
+ supported. ::
751
+
752
+ # List tags of sibling elements before this node
753
+ print(dom.preceding().map('tag').join("\\n"))
754
+ """
755
+ return _select(self._collect(_siblings(self.tree, True, False)), css)
756
+
757
+ @property
758
+ def preceding_nodes(self) -> 'Pyjo.Collection.object[Pyjo_DOM]':
759
+ """::
760
+
761
+ collection = dom.preceding_nodes
762
+
763
+ Return a :mod:`Pyjo.Collection` object containing all sibling nodes
764
+ before this node as :mod:`Pyjo.DOM` objects. ::
765
+
766
+ # "A"
767
+ dom.parse('A<!-- B --><p>C</p>').at('p').preceding_nodes.first().content
768
+ """
769
+ return self._collect(_siblings(self.tree, False, False))
770
+
771
+ def prepend(self, new: object) -> Self:
772
+ """::
773
+
774
+ dom = dom.prepend('<p>I ♥ Pyjoyment!</p>')
775
+ dom = dom.prepend(Pyjo.DOM.new())
776
+
777
+ Prepend HTML/XML fragment to this node (for all node types other than
778
+ ``root``). ::
779
+
780
+ # "<div><h1>Test</h1><h2>123</h2></div>"
781
+ dom.parse('<div><h2>123</h2></div>').at('h2').prepend('<h1>Test</h1>').root
782
+
783
+ # "<p>Test 123</p>"
784
+ dom.parse('<p>123</p>').at('p').child_nodes.first().prepend('Test ').root
785
+ """
786
+ return self._add(0, new)
787
+
788
+ def prepend_content(self, new: object) -> Self:
789
+ """::
790
+
791
+ dom = dom.prepend_content('<p>I ♥ Pyjoyment!</p>')
792
+ dom = dom.prepend_content(Pyjo.DOM.new())
793
+
794
+ Prepend HTML/XML fragment (for ``root`` and ``tag`` nodes) or raw
795
+ content to this node's content. ::
796
+
797
+ # "<div><h2>Test123</h2></div>"
798
+ dom.parse('<div><h2>123</h2></div>').at('h2').prepend_content('Test').root
799
+
800
+ # "<!-- Test 123 --><br>"
801
+ dom.parse('<!-- 123 --><br>').child_nodes.first().prepend_content(' Test').root
802
+
803
+ # "<p><i>123</i>Test</p>"
804
+ dom.parse('<p>Test</p>').at('p').prepend_content('<i>123</i>').root
805
+ """
806
+ return self._content(False, False, new)
807
+
808
+ @property
809
+ def previous(self) -> 'Pyjo_DOM | None':
810
+ """::
811
+
812
+ sibling = dom.previous
813
+
814
+ Return :mod:`Pyjo.DOM` object for previous sibling element, or ``None``
815
+ if there are no more siblings. ::
816
+
817
+ # "<h1>Test</h1>"
818
+ dom.parse('<div><h1>Test</h1><h2>123</h2></div>').at('h2').previous
819
+ """
820
+ return self._maybe(_sibling(self.tree, True, False, -1))
821
+
822
+ @property
823
+ def previous_node(self) -> 'Pyjo_DOM | None':
824
+ """::
825
+
826
+ sibling = dom.previous_node
827
+
828
+ Return :mod:`Pyjo.DOM` object for previous sibling node, or ``None`` if
829
+ there are no more siblings. ::
830
+
831
+ # "123"
832
+ dom.parse('<p>123<!-- Test --><b>456</b></p>').at('b').previous_node.previous_node
833
+
834
+ # " Test "
835
+ dom.parse('<p>123<!-- Test --><b>456</b></p>').at('b').previous_node.content
836
+ """
837
+ return self._maybe(_sibling(self.tree, False, False, -1))
838
+
839
+ def remove(self) -> 'Pyjo_DOM | None':
840
+ """::
841
+
842
+ parent = dom.remove()
843
+
844
+ Remove this node and return :attr:`root` (for ``root`` nodes) or
845
+ :attr:`parent`. ::
846
+
847
+ # "<div></div>"
848
+ dom.parse('<div><h1>Test</h1></div>').at('h1').remove()
849
+
850
+ # "<p><b>456</b></p>"
851
+ dom.parse('<p>123<b>456</b></p>').at('p').child_nodes.first().remove().root
852
+ """
853
+ return self.replace('')
854
+
855
+ def replace(self, new: object) -> 'Pyjo_DOM | None':
856
+ """::
857
+
858
+ parent = dom.replace('<div>I ♥ Pyjoyment!</div>')
859
+ parent = dom.replace(Pyjo.DOM.new())
860
+
861
+ Replace this node with HTML/XML fragment and return :attr:`root` (for
862
+ ``root`` nodes) or :attr:`parent`. ::
863
+
864
+ # "<div><h2>123</h2></div>"
865
+ dom.parse('<div><h1>Test</h1></div>').at('h1').replace('<h2>123</h2>')
866
+
867
+ # "<p><b>123</b></p>"
868
+ dom.parse('<p>Test</p>').at('p').child_nodes[0].replace('<b>123</b>').root
869
+ """
870
+ if (tree := self.tree)[0] == 'root':
871
+ return self.parse(new)
872
+ return self._replace(_parent(tree), tree, _nodes(self._parse(new)))
873
+
874
+ @property
875
+ def root(self) -> 'Pyjo_DOM':
876
+ """::
877
+
878
+ root = dom.root
879
+
880
+ Return :mod:`Pyjo.DOM` object for ``root`` node.
881
+ """
882
+ if not (tree := _ancestors(self.tree, True)):
883
+ return self
884
+ return self._build(tree[0], self.xml)
885
+
886
+ @property
887
+ def selector(self) -> str | None:
888
+ """::
889
+
890
+ selector = dom.selector
891
+
892
+ Get a unique CSS selector for this element. ::
893
+
894
+ # "ul:nth-child(1) > li:nth-child(2)"
895
+ dom.parse('<ul><li>Test</li><li>123</li></ul>').find('li').last().selector
896
+
897
+ # "p:nth-child(1) > b:nth-child(1) > i:nth-child(1)"
898
+ dom.parse('<p><b><i>Test</i></b></p>').at('i').selector
899
+ """
900
+ if (tree := self.tree)[0] != 'tag':
901
+ return None
902
+ return ' > '.join(
903
+ reversed([f'{n[1]}:nth-child({len(_siblings(n, True, False)) + 1})' for n in (tree, *_ancestors(tree))])
904
+ )
905
+
906
+ def strip(self) -> 'Pyjo_DOM | None':
907
+ """::
908
+
909
+ parent = dom.strip()
910
+
911
+ Remove this element while preserving its content and return
912
+ :attr:`parent`. ::
913
+
914
+ # "<div>Test</div>"
915
+ dom.parse('<div><h1>Test</h1></div>').at('h1').strip()
916
+ """
917
+ if (tree := self.tree)[0] != 'tag':
918
+ return self
919
+ return self._replace(tree[3], tree, _nodes(tree))
920
+
921
+ @property
922
+ def tag(self) -> str | None:
923
+ """::
924
+
925
+ tag = dom.tag
926
+ dom.tag = 'div'
927
+
928
+ This element's tag name. ::
929
+
930
+ # List tag names of child elements
931
+ print(dom.children().map('tag').join("\\n"))
932
+ """
933
+ if (tree := self.tree)[0] != 'tag':
934
+ return None
935
+ return tree[1]
936
+
937
+ @tag.setter
938
+ def tag(self, tag: str) -> None:
939
+ if (tree := self.tree)[0] != 'tag' or not tag:
940
+ return
941
+ tree[1] = tag
942
+
943
+ @property
944
+ def text(self) -> str:
945
+ r"""::
946
+
947
+ text = dom.text
948
+
949
+ Extract text content from this element only (not including child
950
+ elements). ::
951
+
952
+ # "bar"
953
+ dom.parse("<div>foo<p>bar</p>baz</div>").at('p').text
954
+
955
+ # "foo\nbaz\n"
956
+ dom.parse("<div>foo\n<p>bar</p>baz\n</div>").at('div').text
957
+
958
+ To extract text content from all descendant nodes see :attr:`all_text`.
959
+ """
960
+ return _text(_nodes(self.tree), False, False)
961
+
962
+ # Renamed from Mojolicious: to_string
963
+ def to_str(self) -> str:
964
+ """::
965
+
966
+ string = dom.to_str()
967
+
968
+ Render this node and its content to HTML/XML. ::
969
+
970
+ # "<b>Test</b>"
971
+ dom.parse('<div><b>Test</b></div>').at('div b').to_str()
972
+ """
973
+ return self._html.render()
974
+
975
+ @property
976
+ def tree(self) -> Tree:
977
+ """::
978
+
979
+ tree = dom.tree
980
+ dom.tree = ['root']
981
+
982
+ Document Object Model. Note that this structure should only be used very
983
+ carefully since it is very dynamic.
984
+ """
985
+ return self._html.tree
986
+
987
+ @tree.setter
988
+ def tree(self, tree: Tree) -> None:
989
+ self._html.tree = tree
990
+
991
+ @property
992
+ def type(self) -> str:
993
+ """::
994
+
995
+ type = dom.type
996
+
997
+ This node's type, usually ``cdata``, ``comment``, ``doctype``, ``pi``,
998
+ ``raw``, ``root``, ``tag`` or ``text``. ::
999
+
1000
+ # "cdata"
1001
+ dom.parse('<![CDATA[Test]]>').child_nodes.first().type
1002
+
1003
+ # "comment"
1004
+ dom.parse('<!-- Test -->').child_nodes.first().type
1005
+
1006
+ # "doctype"
1007
+ dom.parse('<!DOCTYPE html>').child_nodes.first().type
1008
+
1009
+ # "pi"
1010
+ dom.parse('<?xml version="1.0"?>').child_nodes.first().type
1011
+
1012
+ # "raw"
1013
+ dom.parse('<script>Test</script>').at('script').child_nodes.first().type
1014
+
1015
+ # "root"
1016
+ dom.parse('<p>Test</p>').type
1017
+
1018
+ # "tag"
1019
+ dom.parse('<p>Test</p>').at('p').type
1020
+
1021
+ # "text"
1022
+ dom.parse('<p>Test</p>').at('p').child_nodes.first().type
1023
+ """
1024
+ return self.tree[0]
1025
+
1026
+ @property
1027
+ def val(self) -> str | list[str] | None:
1028
+ """::
1029
+
1030
+ value = dom.val
1031
+
1032
+ Extract value from form element (such as ``button``, ``input``,
1033
+ ``option``, ``select`` and ``textarea``), or return ``None`` if this
1034
+ element has no value. In the case of ``select`` with ``multiple``
1035
+ attribute, find ``option`` elements with ``selected`` attribute and
1036
+ return a list with all values, or ``None`` if none could be found. ::
1037
+
1038
+ # "a"
1039
+ dom.parse('<input name=test value=a>').at('input').val
1040
+
1041
+ # "b"
1042
+ dom.parse('<textarea>b</textarea>').at('textarea').val
1043
+
1044
+ # "c"
1045
+ dom.parse('<option value="c">Test</option>').at('option').val
1046
+
1047
+ # "d"
1048
+ dom.parse('<select><option selected>d</option></select>').at('select').val
1049
+
1050
+ # "e"
1051
+ dom.parse('<select multiple><option selected>e</option></select>').at('select').val[0]
1052
+
1053
+ # "on"
1054
+ dom.parse('<input name=test type=checkbox>').at('input').val
1055
+ """
1056
+
1057
+ # "option"
1058
+ if (tag := self.tag) == 'option':
1059
+ return value if (value := self['value']) is not None else self.text
1060
+
1061
+ # "input" ("type=checkbox" and "type=radio")
1062
+ type = self['type'] or ''
1063
+ if tag == 'input' and type in ('radio', 'checkbox'):
1064
+ return value if (value := self['value']) is not None else 'on'
1065
+
1066
+ # "textarea", "input" or "button"
1067
+ if tag != 'select':
1068
+ return self.text if tag == 'textarea' else self['value']
1069
+
1070
+ # "select"
1071
+ v = (
1072
+ self.find('option:checked:not([disabled])')
1073
+ .grep(lambda e: not e.ancestors('optgroup[disabled]').size)
1074
+ .map('val')
1075
+ )
1076
+ if 'multiple' in self:
1077
+ return v.to_list() if v.size else None
1078
+ return v.last()
1079
+
1080
+ # Left out from Mojolicious: with_roles, as Pyjoyment has no roles
1081
+
1082
+ def wrap(self, new: object) -> Self:
1083
+ """::
1084
+
1085
+ dom = dom.wrap('<div></div>')
1086
+ dom = dom.wrap(Pyjo.DOM.new())
1087
+
1088
+ Wrap HTML/XML fragment around this node (for all node types other than
1089
+ ``root``), placing it as the last child of the first innermost element. ::
1090
+
1091
+ # "<p>123<b>Test</b></p>"
1092
+ dom.parse('<b>Test</b>').at('b').wrap('<p>123</p>').root
1093
+
1094
+ # "<div><p><b>Test</b></p>123</div>"
1095
+ dom.parse('<b>Test</b>').at('b').wrap('<div><p></p>123</div>').root
1096
+
1097
+ # "<p><b>Test</b></p><p>123</p>"
1098
+ dom.parse('<b>Test</b>').at('b').wrap('<p></p><p>123</p>').root
1099
+
1100
+ # "<p><b>Test</b></p>"
1101
+ dom.parse('<p>Test</p>').at('p').child_nodes.first().wrap('<b>').root
1102
+ """
1103
+ return self._wrap(False, new)
1104
+
1105
+ def wrap_content(self, new: object) -> Self:
1106
+ """::
1107
+
1108
+ dom = dom.wrap_content('<div></div>')
1109
+ dom = dom.wrap_content(Pyjo.DOM.new())
1110
+
1111
+ Wrap HTML/XML fragment around this node's content (for ``root`` and
1112
+ ``tag`` nodes), placing it as the last children of the first innermost
1113
+ element. ::
1114
+
1115
+ # "<p><b>123Test</b></p><p></p>"
1116
+ dom.parse('<p>Test<p>').at('p').wrap_content('<b>123</b>').root
1117
+
1118
+ # "<p><b>Test</b></p><p>123</p>"
1119
+ dom.parse('<b>Test</b>').wrap_content('<p></p><p>123</p>')
1120
+ """
1121
+ return self._wrap(True, new)
1122
+
1123
+ @property
1124
+ def xml(self) -> bool | None:
1125
+ """::
1126
+
1127
+ boolean = dom.xml
1128
+ dom.xml = boolean
1129
+
1130
+ Disable HTML semantics in parser and activate case-sensitivity, defaults
1131
+ to auto-detection based on XML declarations.
1132
+ """
1133
+ return self._html.xml
1134
+
1135
+ @xml.setter
1136
+ def xml(self, xml: bool | None) -> None:
1137
+ self._html.xml = xml
1138
+
1139
+ def _add(self, offset: int, new: object) -> Self:
1140
+ if (tree := self.tree)[0] == 'root':
1141
+ return self
1142
+
1143
+ if (parent := _parent(tree)) is None:
1144
+ return self
1145
+ i = _offset(parent, tree) + offset
1146
+ parent[i:i] = _link(parent, _nodes(self._parse(new)))
1147
+
1148
+ return self
1149
+
1150
+ def _build(self, tree: Tree, xml: bool | None) -> 'Pyjo_DOM':
1151
+ return self.new().set(tree=tree, xml=xml)
1152
+
1153
+ def _collect(self, nodes: list[Tree]) -> 'Pyjo.Collection.object[Pyjo_DOM]':
1154
+ xml = self.xml
1155
+ return Pyjo.Collection.object(self._build(n, xml) for n in nodes)
1156
+
1157
+ def _content(self, start: bool, offset: bool, new: object) -> Self:
1158
+ tree = self.tree
1159
+ if tree[0] not in ('root', 'tag'):
1160
+ old = self.content
1161
+ self.content = old + str(new) if start else str(new) + old
1162
+ return self
1163
+
1164
+ i = len(tree) if start else _start(tree)
1165
+ tree[i : i + (len(tree) - 1 if offset else 0)] = _link(tree, _nodes(self._parse(new)))
1166
+
1167
+ return self
1168
+
1169
+ def _css(self) -> 'Pyjo.DOM.CSS.object':
1170
+ return Pyjo.DOM.CSS.new(tree=self.tree)
1171
+
1172
+ def _maybe(self, tree: Tree | None) -> 'Pyjo_DOM | None':
1173
+ return self._build(tree, self.xml) if tree is not None else None
1174
+
1175
+ def _parse(self, new: object) -> Tree:
1176
+ if not isinstance(new, Pyjo_DOM):
1177
+ return Pyjo.DOM.HTML.new(xml=self.xml).parse(new).tree
1178
+ tree = _clone(new.tree)
1179
+ return tree if tree[0] == 'root' else _fragment(tree)
1180
+
1181
+ def _replace(self, parent: Tree | None, child: Tree, nodes: list[Tree]) -> 'Pyjo_DOM | None':
1182
+ if parent is not None:
1183
+ i = _offset(parent, child)
1184
+ parent[i : i + 1] = _link(parent, nodes)
1185
+ return self.parent
1186
+
1187
+ def _wrap(self, content: bool, new: object) -> Self:
1188
+ tree = self.tree
1189
+ if tree[0] == 'root' and not content:
1190
+ return self
1191
+ if tree[0] not in ('root', 'tag') and content:
1192
+ return self
1193
+
1194
+ # Find innermost tag
1195
+ current = None
1196
+ first = _nodes(parsed := self._parse(new), True)
1197
+ while len(first):
1198
+ current = first[0]
1199
+ first = _nodes(current, True)
1200
+ if current is None:
1201
+ return self
1202
+
1203
+ # Wrap content
1204
+ if content:
1205
+ current.extend(_link(current, _nodes(tree)))
1206
+ start = _start(tree)
1207
+ tree[start:] = _link(tree, _nodes(parsed))
1208
+ return self
1209
+
1210
+ # Wrap element
1211
+ self._replace(_parent(tree), tree, _nodes(parsed))
1212
+ current.extend(_link(current, [tree]))
1213
+ return self
1214
+
1215
+
1216
+ def _all(nodes: list[Tree]) -> list[Tree]:
1217
+ return [m for n in nodes for m in ([n, *_all(_nodes(n))] if n[0] == 'tag' else [n])]
1218
+
1219
+
1220
+ def _ancestors(tree: Tree, root: bool = False) -> list[Tree]:
1221
+ # Unlike Mojolicious: Mojo takes the second child node of a root for its
1222
+ # parent, so the root of a root with a text node after the first one is
1223
+ # that text node
1224
+ if tree[0] == 'root' or (parent := _parent(tree)) is None:
1225
+ return []
1226
+ ancestors: list[Tree] = [parent]
1227
+ while parent[0] == 'tag' and (parent := parent[3]) is not None:
1228
+ ancestors.append(parent)
1229
+ return ancestors[-1:] if root else ancestors[:-1]
1230
+
1231
+
1232
+ def _clone(tree: Tree, parent: Tree | None = None) -> Tree:
1233
+ if tree[0] == 'tag':
1234
+ new: Tree = ['tag', tree[1], dict(tree[2]), parent]
1235
+ new.extend(_clone(n, new) for n in tree[4:])
1236
+ elif tree[0] == 'root':
1237
+ new = ['root']
1238
+ new.extend(_clone(n, new) for n in tree[1:])
1239
+ else:
1240
+ new = [tree[0], tree[1], parent]
1241
+ return new
1242
+
1243
+
1244
+ def _fragment(*nodes: Tree) -> Tree:
1245
+ root: Tree = ['root', *nodes]
1246
+ _link(root, list(nodes), True)
1247
+ return root
1248
+
1249
+
1250
+ def _link(parent: Tree, children: list[Tree], fragment: bool = False) -> list[Tree]:
1251
+
1252
+ # Link parent to children (raw text is only allowed in script and style elements)
1253
+ raw = fragment or parent[0] == 'tag' and parent[1] in ('script', 'style')
1254
+ for node in children:
1255
+ if node[0] == 'raw' and not raw:
1256
+ node[0] = 'text'
1257
+ node[3 if node[0] == 'tag' else 2] = parent
1258
+
1259
+ return children
1260
+
1261
+
1262
+ def _nodes(tree: Tree | None, tags: bool = False) -> list[Tree]:
1263
+ if tree is None:
1264
+ return []
1265
+ nodes = tree[_start(tree) :]
1266
+ return [n for n in nodes if n[0] == 'tag'] if tags else nodes
1267
+
1268
+
1269
+ def _offset(parent: Tree, child: Tree) -> int:
1270
+ i = _start(parent)
1271
+ while i < len(parent) and parent[i] is not child:
1272
+ i += 1
1273
+ return i
1274
+
1275
+
1276
+ def _parent(tree: Tree) -> Tree | None:
1277
+ return tree[3 if tree[0] == 'tag' else 2]
1278
+
1279
+
1280
+ def _select(collection: 'Pyjo.Collection.object[Pyjo_DOM]', css: object) -> 'Pyjo.Collection.object[Pyjo_DOM]':
1281
+ return collection.grep('matches', css) if css else collection
1282
+
1283
+
1284
+ # Mojo's _siblings with $i defined
1285
+ def _sibling(tree: Tree, tags: bool, tail: bool, i: int) -> Tree | None:
1286
+ if tree[0] == 'root':
1287
+ return None
1288
+ nodes = _siblings(tree, tags, tail)
1289
+ return nodes[i] if -len(nodes) <= i < len(nodes) else None
1290
+
1291
+
1292
+ def _siblings(tree: Tree, tags: bool, tail: bool) -> list[Tree]:
1293
+ if tree[0] == 'root':
1294
+ return []
1295
+
1296
+ nodes = _nodes(_parent(tree))
1297
+ if (match := next((i for i, n in enumerate(nodes) if n is tree), None)) is None:
1298
+ return []
1299
+ nodes = nodes[match + 1 :] if tail else nodes[:match]
1300
+
1301
+ return [n for n in nodes if n[0] == 'tag'] if tags else nodes
1302
+
1303
+
1304
+ def _start(tree: Tree) -> int:
1305
+ return 1 if tree[0] == 'root' else 4
1306
+
1307
+
1308
+ def _text(nodes: list[Tree], xml: bool | None, all: bool) -> str:
1309
+ text = ''
1310
+ queue = list(nodes)
1311
+ while len(queue):
1312
+ node = queue.pop(0)
1313
+ type = node[0]
1314
+
1315
+ # Text
1316
+ if type in ('text', 'cdata', 'raw'):
1317
+ text += node[1]
1318
+
1319
+ # Nested tag
1320
+ elif type == 'tag' and all:
1321
+ if xml or node[1] not in ('script', 'style'):
1322
+ queue[0:0] = _nodes(node)
1323
+
1324
+ return text
1325
+
1326
+
1327
+ new = Pyjo_DOM.new
1328
+ object = Pyjo_DOM
1329
+ DOM = Pyjo_DOM