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.
- Pyjo/App/ASGI.py +735 -0
- Pyjo/App/CGI.py +773 -0
- Pyjo/App/FastCGI.py +631 -0
- Pyjo/App/Lambda.py +987 -0
- Pyjo/App/WSGI.py +427 -0
- Pyjo/App/__init__.py +409 -0
- Pyjo/Asset/File.py +377 -0
- Pyjo/Asset/Memory.py +235 -0
- Pyjo/Asset/__init__.py +200 -0
- Pyjo/Base.py +215 -0
- Pyjo/Cache.py +119 -0
- Pyjo/Collection.py +570 -0
- Pyjo/Content/MultiPart.py +325 -0
- Pyjo/Content/Single.py +193 -0
- Pyjo/Content/__init__.py +806 -0
- Pyjo/Cookie/Request.py +90 -0
- Pyjo/Cookie/Response.py +294 -0
- Pyjo/Cookie/__init__.py +143 -0
- Pyjo/DOM/CSS.py +1019 -0
- Pyjo/DOM/HTML.py +550 -0
- Pyjo/DOM/__init__.py +1329 -0
- Pyjo/Date.py +294 -0
- Pyjo/EventEmitter.py +522 -0
- Pyjo/Exception.py +494 -0
- Pyjo/File.py +959 -0
- Pyjo/GetOpt.py +605 -0
- Pyjo/Headers.py +760 -0
- Pyjo/HelloWorld.py +77 -0
- Pyjo/Home.py +109 -0
- Pyjo/IOLoop/ChildProcess.py +591 -0
- Pyjo/IOLoop/Client.py +736 -0
- Pyjo/IOLoop/Datagram.py +982 -0
- Pyjo/IOLoop/Messages.py +664 -0
- Pyjo/IOLoop/Server.py +458 -0
- Pyjo/IOLoop/Stdio.py +446 -0
- Pyjo/IOLoop/Stream.py +644 -0
- Pyjo/IOLoop/Subprocess.py +419 -0
- Pyjo/IOLoop/TLS.py +360 -0
- Pyjo/IOLoop/__init__.py +1036 -0
- Pyjo/IOLoop/certs/README.md +41 -0
- Pyjo/IOLoop/certs/ca.crt +20 -0
- Pyjo/IOLoop/certs/server.crt +21 -0
- Pyjo/IOLoop/certs/server.key +28 -0
- Pyjo/JSON/Pointer.py +135 -0
- Pyjo/JSON/RPC.py +772 -0
- Pyjo/JSON/Schema.py +2506 -0
- Pyjo/JSON/__init__.py +573 -0
- Pyjo/Loader.py +342 -0
- Pyjo/Log.py +723 -0
- Pyjo/Message/Request.py +725 -0
- Pyjo/Message/Response.py +476 -0
- Pyjo/Message/__init__.py +830 -0
- Pyjo/Parameters.py +472 -0
- Pyjo/Path.py +419 -0
- Pyjo/Promise.py +894 -0
- Pyjo/Prove.py +1305 -0
- Pyjo/Reactor/Asyncio.py +854 -0
- Pyjo/Reactor/Poll.py +439 -0
- Pyjo/Reactor/__init__.py +525 -0
- Pyjo/SSE.py +119 -0
- Pyjo/Server/ASGI.py +486 -0
- Pyjo/Server/CGI.py +259 -0
- Pyjo/Server/Daemon.py +1007 -0
- Pyjo/Server/FastCGI.py +517 -0
- Pyjo/Server/Hot/__init__.py +687 -0
- Pyjo/Server/Hot/__main__.py +5 -0
- Pyjo/Server/Lambda.py +641 -0
- Pyjo/Server/Prefork.py +917 -0
- Pyjo/Server/Prethread.py +394 -0
- Pyjo/Server/Reload/Backend/Poll.py +114 -0
- Pyjo/Server/Reload/Backend/__init__.py +126 -0
- Pyjo/Server/Reload/__init__.py +482 -0
- Pyjo/Server/Reload/__main__.py +5 -0
- Pyjo/Server/Stdio.py +228 -0
- Pyjo/Server/WSGI.py +349 -0
- Pyjo/Server/__init__.py +396 -0
- Pyjo/String/Bytes.py +532 -0
- Pyjo/String/HTML.py +210 -0
- Pyjo/String/Mixin.py +333 -0
- Pyjo/String/Template.py +470 -0
- Pyjo/String/Unicode.py +553 -0
- Pyjo/String/__init__.py +0 -0
- Pyjo/TOML.py +232 -0
- Pyjo/Template.py +1290 -0
- Pyjo/Test/Pyjo.py +1454 -0
- Pyjo/Test/__init__.py +1233 -0
- Pyjo/Test/pytest_plugin.py +305 -0
- Pyjo/Transaction/HTTP.py +374 -0
- Pyjo/Transaction/WebSocket.py +709 -0
- Pyjo/Transaction/__init__.py +429 -0
- Pyjo/URL/Template.py +459 -0
- Pyjo/URL/__init__.py +651 -0
- Pyjo/Upload.py +139 -0
- Pyjo/UserAgent/CookieJar.py +391 -0
- Pyjo/UserAgent/Proxy.py +138 -0
- Pyjo/UserAgent/Server.py +239 -0
- Pyjo/UserAgent/Transactor.py +766 -0
- Pyjo/UserAgent/__init__.py +1243 -0
- Pyjo/Util.py +3567 -0
- Pyjo/WebSocket.py +332 -0
- Pyjo/YAML.py +1167 -0
- Pyjo/__init__.py +441 -0
- Pyjo/__main__.py +35 -0
- Pyjo/py.typed +0 -0
- Pyjoyment/Command/Author/__init__.py +20 -0
- Pyjoyment/Command/Author/generate/__init__.py +106 -0
- Pyjoyment/Command/Author/generate/app.py +299 -0
- Pyjoyment/Command/Author/generate/dockerfile.py +281 -0
- Pyjoyment/Command/Author/generate/kubernetes.py +514 -0
- Pyjoyment/Command/Author/generate/lambda_zip.py +372 -0
- Pyjoyment/Command/Author/generate/lite_app.py +113 -0
- Pyjoyment/Command/Author/generate/plugin.py +267 -0
- Pyjoyment/Command/Author/generate/pyproject.py +123 -0
- Pyjoyment/Command/Author/inflate.py +101 -0
- Pyjoyment/Command/__init__.py +332 -0
- Pyjoyment/Command/asgi.py +92 -0
- Pyjoyment/Command/cgi.py +90 -0
- Pyjoyment/Command/daemon.py +160 -0
- Pyjoyment/Command/eval.py +128 -0
- Pyjoyment/Command/fastcgi.py +99 -0
- Pyjoyment/Command/get.py +305 -0
- Pyjoyment/Command/lambda.py +117 -0
- Pyjoyment/Command/prefork.py +181 -0
- Pyjoyment/Command/prethread.py +126 -0
- Pyjoyment/Command/routes.py +133 -0
- Pyjoyment/Command/stdio.py +92 -0
- Pyjoyment/Command/version.py +141 -0
- Pyjoyment/Command/wsgi.py +93 -0
- Pyjoyment/Commands.py +546 -0
- Pyjoyment/Controller.py +1307 -0
- Pyjoyment/HelloWorld.py +56 -0
- Pyjoyment/Lite.py +494 -0
- Pyjoyment/Plugin/AllowedHosts.py +205 -0
- Pyjoyment/Plugin/Config.py +248 -0
- Pyjoyment/Plugin/DefaultHelpers.py +1202 -0
- Pyjoyment/Plugin/EPLRenderer.py +134 -0
- Pyjoyment/Plugin/EPRenderer.py +212 -0
- Pyjoyment/Plugin/HeaderCondition.py +98 -0
- Pyjoyment/Plugin/Health.py +325 -0
- Pyjoyment/Plugin/JSONConfig.py +172 -0
- Pyjoyment/Plugin/Mount.py +223 -0
- Pyjoyment/Plugin/PyRenderer.py +209 -0
- Pyjoyment/Plugin/TOMLConfig.py +141 -0
- Pyjoyment/Plugin/TagHelpers.py +959 -0
- Pyjoyment/Plugin/YAMLConfig.py +178 -0
- Pyjoyment/Plugin/__init__.py +80 -0
- Pyjoyment/Plugins.py +215 -0
- Pyjoyment/Renderer.py +673 -0
- Pyjoyment/Routes/Match.py +251 -0
- Pyjoyment/Routes/Pattern.py +443 -0
- Pyjoyment/Routes/Route.py +827 -0
- Pyjoyment/Routes/__init__.py +419 -0
- Pyjoyment/Sessions.py +255 -0
- Pyjoyment/Static.py +421 -0
- Pyjoyment/Types.py +238 -0
- Pyjoyment/Validator/Validation.py +395 -0
- Pyjoyment/Validator/__init__.py +304 -0
- Pyjoyment/__init__.py +1083 -0
- Pyjoyment/__main__.py +5 -0
- Pyjoyment/py.typed +0 -0
- Pyjoyment/resources/public/favicon.ico +0 -0
- Pyjoyment/resources/public/favicon.svg +345 -0
- Pyjoyment/resources/public/pyjo/bootstrap/LICENSE +21 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.bundle.min.js.map +1 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.css +6 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.js +7 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.min.css.map +1 -0
- Pyjoyment/resources/public/pyjo/failunicat.png +0 -0
- Pyjoyment/resources/public/pyjo/failunicat.svg +50 -0
- Pyjoyment/resources/public/pyjo/highlight.js/LICENSE +29 -0
- Pyjoyment/resources/public/pyjo/highlight.js/highlight-mojo-dark.css +56 -0
- Pyjoyment/resources/public/pyjo/highlight.js/highlight.min.js +1262 -0
- Pyjoyment/resources/public/pyjo/highlight.js/pyjoyment.min.js +1 -0
- Pyjoyment/resources/public/pyjo/logo-black-4x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-black-4x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white-2x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white-2x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white-4x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white-4x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo.png +0 -0
- Pyjoyment/resources/public/pyjo/logo.svg +346 -0
- Pyjoyment/resources/public/pyjo/notfound.png +0 -0
- Pyjoyment/resources/public/pyjo/notfound.svg +3 -0
- Pyjoyment/resources/public/pyjo/nounicat.png +0 -0
- Pyjoyment/resources/public/pyjo/nounicat.svg +5 -0
- Pyjoyment/resources/public/pyjo/pinstripe-dark.png +0 -0
- Pyjoyment/resources/public/pyjo/pinstripe-dark.svg +4 -0
- Pyjoyment/resources/public/pyjo/pinstripe-light.png +0 -0
- Pyjoyment/resources/public/pyjo/pinstripe-light.svg +4 -0
- Pyjoyment/resources/public/pyjo/pyjo.css +204 -0
- Pyjoyment/resources/templates/pyjo/debug.html.ep +346 -0
- Pyjoyment/resources/templates/pyjo/exception.html.ep +32 -0
- Pyjoyment/resources/templates/pyjo/not_found.html.ep +45 -0
- pyjoyment-0.1.0.dist-info/METADATA +376 -0
- pyjoyment-0.1.0.dist-info/RECORD +200 -0
- pyjoyment-0.1.0.dist-info/WHEEL +4 -0
- pyjoyment-0.1.0.dist-info/entry_points.txt +8 -0
- 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 & 123</div>"
|
|
661
|
+
dom.new_tag('div', 'test & 123')
|
|
662
|
+
|
|
663
|
+
# "<div id="foo">test & 123</div>"
|
|
664
|
+
dom.new_tag('div', 'test & 123', id='foo')
|
|
665
|
+
|
|
666
|
+
# "<div data-bar="test" data-foo="1">test & 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
|