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/CSS.py
ADDED
|
@@ -0,0 +1,1019 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pyjo.DOM.CSS - CSS selector engine
|
|
3
|
+
==================================
|
|
4
|
+
::
|
|
5
|
+
|
|
6
|
+
from Pyjo.DOM.CSS import CSS
|
|
7
|
+
|
|
8
|
+
# Select elements from DOM tree
|
|
9
|
+
css = CSS(tree=tree)
|
|
10
|
+
elements = css.select('h1, h2, h3')
|
|
11
|
+
|
|
12
|
+
# Or import the module, as in Google Python Style Guide
|
|
13
|
+
|
|
14
|
+
import Pyjo.DOM.CSS
|
|
15
|
+
|
|
16
|
+
# Select elements from DOM tree
|
|
17
|
+
css = Pyjo.DOM.CSS.new(tree=tree)
|
|
18
|
+
elements = css.select('h1, h2, h3')
|
|
19
|
+
|
|
20
|
+
:mod:`Pyjo.DOM.CSS` is the CSS selector engine used by :mod:`Pyjo.DOM`, based
|
|
21
|
+
on the `HTML Living Standard <https://html.spec.whatwg.org>`_ and
|
|
22
|
+
`Selectors Level 3 <https://www.w3.org/TR/css3-selectors/>`_.
|
|
23
|
+
|
|
24
|
+
Selectors
|
|
25
|
+
---------
|
|
26
|
+
|
|
27
|
+
All CSS selectors that make sense for a standalone parser are supported.
|
|
28
|
+
|
|
29
|
+
``*``
|
|
30
|
+
~~~~~
|
|
31
|
+
|
|
32
|
+
Any element. ::
|
|
33
|
+
|
|
34
|
+
all = css.select('*')
|
|
35
|
+
|
|
36
|
+
``E``
|
|
37
|
+
~~~~~
|
|
38
|
+
|
|
39
|
+
An element of type ``E``. ::
|
|
40
|
+
|
|
41
|
+
title = css.select('title')
|
|
42
|
+
|
|
43
|
+
``E[foo]``
|
|
44
|
+
~~~~~~~~~~
|
|
45
|
+
|
|
46
|
+
An ``E`` element with a ``foo`` attribute. ::
|
|
47
|
+
|
|
48
|
+
links = css.select('a[href]')
|
|
49
|
+
|
|
50
|
+
``E[foo="bar"]``
|
|
51
|
+
~~~~~~~~~~~~~~~~
|
|
52
|
+
|
|
53
|
+
An ``E`` element whose ``foo`` attribute value is exactly equal to ``bar``. ::
|
|
54
|
+
|
|
55
|
+
case_sensitive = css.select('input[type="hidden"]')
|
|
56
|
+
case_sensitive = css.select('input[type=hidden]')
|
|
57
|
+
|
|
58
|
+
``E[foo="bar" i]``
|
|
59
|
+
~~~~~~~~~~~~~~~~~~
|
|
60
|
+
|
|
61
|
+
An ``E`` element whose ``foo`` attribute value is exactly equal to any
|
|
62
|
+
(ASCII-range) case-permutation of ``bar``. Note that this selector is
|
|
63
|
+
**EXPERIMENTAL** and might change without warning! ::
|
|
64
|
+
|
|
65
|
+
case_insensitive = css.select('input[type="hidden" i]')
|
|
66
|
+
case_insensitive = css.select('input[type=hidden i]')
|
|
67
|
+
case_insensitive = css.select('input[class~="foo" i]')
|
|
68
|
+
|
|
69
|
+
This selector is part of `Selectors Level 4
|
|
70
|
+
<https://dev.w3.org/csswg/selectors-4>`_, which is still a work in progress.
|
|
71
|
+
|
|
72
|
+
``E[foo="bar" s]``
|
|
73
|
+
~~~~~~~~~~~~~~~~~~
|
|
74
|
+
|
|
75
|
+
An ``E`` element whose ``foo`` attribute value is exactly and case-sensitively
|
|
76
|
+
equal to ``bar``. Note that this selector is **EXPERIMENTAL** and might change
|
|
77
|
+
without warning! ::
|
|
78
|
+
|
|
79
|
+
case_sensitive = css.select('input[type="hidden" s]')
|
|
80
|
+
|
|
81
|
+
This selector is part of `Selectors Level 4
|
|
82
|
+
<https://dev.w3.org/csswg/selectors-4>`_, which is still a work in progress.
|
|
83
|
+
|
|
84
|
+
``E[foo~="bar"]``
|
|
85
|
+
~~~~~~~~~~~~~~~~~
|
|
86
|
+
|
|
87
|
+
An ``E`` element whose ``foo`` attribute value is a list of
|
|
88
|
+
whitespace-separated values, one of which is exactly equal to ``bar``. ::
|
|
89
|
+
|
|
90
|
+
foo = css.select('input[class~="foo"]')
|
|
91
|
+
foo = css.select('input[class~=foo]')
|
|
92
|
+
|
|
93
|
+
``E[foo^="bar"]``
|
|
94
|
+
~~~~~~~~~~~~~~~~~
|
|
95
|
+
|
|
96
|
+
An ``E`` element whose ``foo`` attribute value begins exactly with the string
|
|
97
|
+
``bar``. ::
|
|
98
|
+
|
|
99
|
+
begins_with = css.select('input[name^="f"]')
|
|
100
|
+
begins_with = css.select('input[name^=f]')
|
|
101
|
+
|
|
102
|
+
``E[foo$="bar"]``
|
|
103
|
+
~~~~~~~~~~~~~~~~~
|
|
104
|
+
|
|
105
|
+
An ``E`` element whose ``foo`` attribute value ends exactly with the string
|
|
106
|
+
``bar``. ::
|
|
107
|
+
|
|
108
|
+
ends_with = css.select('input[name$="o"]')
|
|
109
|
+
ends_with = css.select('input[name$=o]')
|
|
110
|
+
|
|
111
|
+
``E[foo*="bar"]``
|
|
112
|
+
~~~~~~~~~~~~~~~~~
|
|
113
|
+
|
|
114
|
+
An ``E`` element whose ``foo`` attribute value contains the substring ``bar``. ::
|
|
115
|
+
|
|
116
|
+
contains = css.select('input[name*="fo"]')
|
|
117
|
+
contains = css.select('input[name*=fo]')
|
|
118
|
+
|
|
119
|
+
``E[foo|="en"]``
|
|
120
|
+
~~~~~~~~~~~~~~~~
|
|
121
|
+
|
|
122
|
+
An ``E`` element whose ``foo`` attribute has a hyphen-separated list of values
|
|
123
|
+
beginning (from the left) with ``en``. ::
|
|
124
|
+
|
|
125
|
+
english = css.select('link[hreflang|=en]')
|
|
126
|
+
|
|
127
|
+
``E:root``
|
|
128
|
+
~~~~~~~~~~
|
|
129
|
+
|
|
130
|
+
An ``E`` element, root of the document. ::
|
|
131
|
+
|
|
132
|
+
root = css.select(':root')
|
|
133
|
+
|
|
134
|
+
``E:nth-child(n)``
|
|
135
|
+
~~~~~~~~~~~~~~~~~~
|
|
136
|
+
|
|
137
|
+
An ``E`` element, the ``n-th`` child of its parent. ::
|
|
138
|
+
|
|
139
|
+
third = css.select('div:nth-child(3)')
|
|
140
|
+
odd = css.select('div:nth-child(odd)')
|
|
141
|
+
even = css.select('div:nth-child(even)')
|
|
142
|
+
top3 = css.select('div:nth-child(-n+3)')
|
|
143
|
+
|
|
144
|
+
``E:nth-last-child(n)``
|
|
145
|
+
~~~~~~~~~~~~~~~~~~~~~~~
|
|
146
|
+
|
|
147
|
+
An ``E`` element, the ``n-th`` child of its parent, counting from the last one. ::
|
|
148
|
+
|
|
149
|
+
third = css.select('div:nth-last-child(3)')
|
|
150
|
+
odd = css.select('div:nth-last-child(odd)')
|
|
151
|
+
even = css.select('div:nth-last-child(even)')
|
|
152
|
+
bottom3 = css.select('div:nth-last-child(-n+3)')
|
|
153
|
+
|
|
154
|
+
``E:nth-of-type(n)``
|
|
155
|
+
~~~~~~~~~~~~~~~~~~~~
|
|
156
|
+
|
|
157
|
+
An ``E`` element, the ``n-th`` sibling of its type. ::
|
|
158
|
+
|
|
159
|
+
third = css.select('div:nth-of-type(3)')
|
|
160
|
+
odd = css.select('div:nth-of-type(odd)')
|
|
161
|
+
even = css.select('div:nth-of-type(even)')
|
|
162
|
+
top3 = css.select('div:nth-of-type(-n+3)')
|
|
163
|
+
|
|
164
|
+
``E:nth-last-of-type(n)``
|
|
165
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
166
|
+
|
|
167
|
+
An ``E`` element, the ``n-th`` sibling of its type, counting from the last one. ::
|
|
168
|
+
|
|
169
|
+
third = css.select('div:nth-last-of-type(3)')
|
|
170
|
+
odd = css.select('div:nth-last-of-type(odd)')
|
|
171
|
+
even = css.select('div:nth-last-of-type(even)')
|
|
172
|
+
bottom3 = css.select('div:nth-last-of-type(-n+3)')
|
|
173
|
+
|
|
174
|
+
``E:first-child``
|
|
175
|
+
~~~~~~~~~~~~~~~~~
|
|
176
|
+
|
|
177
|
+
An ``E`` element, first child of its parent. ::
|
|
178
|
+
|
|
179
|
+
first = css.select('div p:first-child')
|
|
180
|
+
|
|
181
|
+
``E:last-child``
|
|
182
|
+
~~~~~~~~~~~~~~~~
|
|
183
|
+
|
|
184
|
+
An ``E`` element, last child of its parent. ::
|
|
185
|
+
|
|
186
|
+
last = css.select('div p:last-child')
|
|
187
|
+
|
|
188
|
+
``E:first-of-type``
|
|
189
|
+
~~~~~~~~~~~~~~~~~~~
|
|
190
|
+
|
|
191
|
+
An ``E`` element, first sibling of its type. ::
|
|
192
|
+
|
|
193
|
+
first = css.select('div p:first-of-type')
|
|
194
|
+
|
|
195
|
+
``E:last-of-type``
|
|
196
|
+
~~~~~~~~~~~~~~~~~~
|
|
197
|
+
|
|
198
|
+
An ``E`` element, last sibling of its type. ::
|
|
199
|
+
|
|
200
|
+
last = css.select('div p:last-of-type')
|
|
201
|
+
|
|
202
|
+
``E:only-child``
|
|
203
|
+
~~~~~~~~~~~~~~~~
|
|
204
|
+
|
|
205
|
+
An ``E`` element, only child of its parent. ::
|
|
206
|
+
|
|
207
|
+
lonely = css.select('div p:only-child')
|
|
208
|
+
|
|
209
|
+
``E:only-of-type``
|
|
210
|
+
~~~~~~~~~~~~~~~~~~
|
|
211
|
+
|
|
212
|
+
An ``E`` element, only sibling of its type. ::
|
|
213
|
+
|
|
214
|
+
lonely = css.select('div p:only-of-type')
|
|
215
|
+
|
|
216
|
+
``E:empty``
|
|
217
|
+
~~~~~~~~~~~
|
|
218
|
+
|
|
219
|
+
An ``E`` element that has no children (including text nodes). ::
|
|
220
|
+
|
|
221
|
+
empty = css.select(':empty')
|
|
222
|
+
|
|
223
|
+
``E:any-link``
|
|
224
|
+
~~~~~~~~~~~~~~
|
|
225
|
+
|
|
226
|
+
Alias for ``E:link``. Note that this selector is **EXPERIMENTAL** and might
|
|
227
|
+
change without warning! This selector is part of `Selectors Level 4
|
|
228
|
+
<https://dev.w3.org/csswg/selectors-4>`_, which is still a work in progress.
|
|
229
|
+
|
|
230
|
+
``E:link``
|
|
231
|
+
~~~~~~~~~~
|
|
232
|
+
|
|
233
|
+
An ``E`` element being the source anchor of a hyperlink of which the target is
|
|
234
|
+
not yet visited (``:link``) or already visited (``:visited``). Note that
|
|
235
|
+
:mod:`Pyjo.DOM.CSS` is not stateful, therefore ``:any-link``, ``:link`` and
|
|
236
|
+
``:visited`` yield exactly the same results. ::
|
|
237
|
+
|
|
238
|
+
links = css.select(':any-link')
|
|
239
|
+
links = css.select(':link')
|
|
240
|
+
links = css.select(':visited')
|
|
241
|
+
|
|
242
|
+
``E:visited``
|
|
243
|
+
~~~~~~~~~~~~~
|
|
244
|
+
|
|
245
|
+
Alias for ``E:link``.
|
|
246
|
+
|
|
247
|
+
``E:scope``
|
|
248
|
+
~~~~~~~~~~~
|
|
249
|
+
|
|
250
|
+
An ``E`` element being a designated reference element. Note that this selector
|
|
251
|
+
is **EXPERIMENTAL** and might change without warning! ::
|
|
252
|
+
|
|
253
|
+
scoped = css.select('a:not(:scope > a)')
|
|
254
|
+
scoped = css.select('div :scope p')
|
|
255
|
+
scoped = css.select('~ p')
|
|
256
|
+
|
|
257
|
+
This selector is part of `Selectors Level 4
|
|
258
|
+
<https://dev.w3.org/csswg/selectors-4>`_, which is still a work in progress.
|
|
259
|
+
|
|
260
|
+
``E:checked``
|
|
261
|
+
~~~~~~~~~~~~~
|
|
262
|
+
|
|
263
|
+
A user interface element ``E`` which is checked (for instance a radio-button or
|
|
264
|
+
checkbox). ::
|
|
265
|
+
|
|
266
|
+
input = css.select(':checked')
|
|
267
|
+
|
|
268
|
+
``E.warning``
|
|
269
|
+
~~~~~~~~~~~~~
|
|
270
|
+
|
|
271
|
+
An ``E`` element whose class is "warning". ::
|
|
272
|
+
|
|
273
|
+
warning = css.select('div.warning')
|
|
274
|
+
|
|
275
|
+
``E#myid``
|
|
276
|
+
~~~~~~~~~~
|
|
277
|
+
|
|
278
|
+
An ``E`` element with ``ID`` equal to "myid". ::
|
|
279
|
+
|
|
280
|
+
foo = css.select('div#foo')
|
|
281
|
+
|
|
282
|
+
``E:not(s1, s2)``
|
|
283
|
+
~~~~~~~~~~~~~~~~~
|
|
284
|
+
|
|
285
|
+
An ``E`` element that does not match either compound selector ``s1`` or
|
|
286
|
+
compound selector ``s2``. Note that support for compound selectors is
|
|
287
|
+
**EXPERIMENTAL** and might change without warning! ::
|
|
288
|
+
|
|
289
|
+
others = css.select('div p:not(:first-child, :last-child)')
|
|
290
|
+
|
|
291
|
+
Support for compound selectors was added as part of `Selectors Level 4
|
|
292
|
+
<https://dev.w3.org/csswg/selectors-4>`_, which is still a work in progress.
|
|
293
|
+
|
|
294
|
+
``E:is(s1, s2)``
|
|
295
|
+
~~~~~~~~~~~~~~~~
|
|
296
|
+
|
|
297
|
+
An ``E`` element that matches compound selector ``s1`` and/or compound selector
|
|
298
|
+
``s2``. Note that this selector is **EXPERIMENTAL** and might change without
|
|
299
|
+
warning! ::
|
|
300
|
+
|
|
301
|
+
headers = css.select(':is(section, article, aside, nav) h1')
|
|
302
|
+
|
|
303
|
+
This selector is part of `Selectors Level 4
|
|
304
|
+
<https://dev.w3.org/csswg/selectors-4>`_, which is still a work in progress.
|
|
305
|
+
|
|
306
|
+
``E:has(rs1, rs2)``
|
|
307
|
+
~~~~~~~~~~~~~~~~~~~
|
|
308
|
+
|
|
309
|
+
An ``E`` element, if either of the relative selectors ``rs1`` or ``rs2``, when
|
|
310
|
+
evaluated with ``E`` as the :scope elements, match an element. Note that this
|
|
311
|
+
selector is **EXPERIMENTAL** and might change without warning! ::
|
|
312
|
+
|
|
313
|
+
link = css.select('a:has(> img)')
|
|
314
|
+
|
|
315
|
+
This selector is part of `Selectors Level 4
|
|
316
|
+
<https://dev.w3.org/csswg/selectors-4>`_, which is still a work in progress.
|
|
317
|
+
Also be aware that this feature is currently marked ``at-risk``, so there is a
|
|
318
|
+
high chance that it will get removed completely.
|
|
319
|
+
|
|
320
|
+
``E:text(string_or_regex)``
|
|
321
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
322
|
+
|
|
323
|
+
An ``E`` element containing text content that substring matches
|
|
324
|
+
``string_or_regex`` case-insensitively or that regex matches
|
|
325
|
+
``string_or_regex``. For regular expressions use the format ``:text(/.../)``.
|
|
326
|
+
Note that this selector is **EXPERIMENTAL** and might change without warning! ::
|
|
327
|
+
|
|
328
|
+
# Substring match
|
|
329
|
+
login = css.select(':text(Log in)')
|
|
330
|
+
|
|
331
|
+
# Regex match
|
|
332
|
+
login = css.select(':text(/Log ?in/)')
|
|
333
|
+
|
|
334
|
+
# Regex match (case-insensitive)
|
|
335
|
+
login = css.select(':text(/(?i:Log ?in)/)')
|
|
336
|
+
|
|
337
|
+
This is a custom selector for :mod:`Pyjo.DOM` and not part of any spec.
|
|
338
|
+
|
|
339
|
+
``A|E``
|
|
340
|
+
~~~~~~~
|
|
341
|
+
|
|
342
|
+
An ``E`` element that belongs to the namespace alias ``A`` from `CSS Namespaces
|
|
343
|
+
Module Level 3 <https://www.w3.org/TR/css-namespaces-3/>`_. Keyword arguments
|
|
344
|
+
passed to selector methods are used to declare namespace aliases. ::
|
|
345
|
+
|
|
346
|
+
elem = css.select('lq|elem', lq='http://example.com/q-markup')
|
|
347
|
+
|
|
348
|
+
Using an empty alias searches for an element that belongs to no namespace. ::
|
|
349
|
+
|
|
350
|
+
div = css.select('|div')
|
|
351
|
+
|
|
352
|
+
``E F``
|
|
353
|
+
~~~~~~~
|
|
354
|
+
|
|
355
|
+
An ``F`` element descendant of an ``E`` element. ::
|
|
356
|
+
|
|
357
|
+
headlines = css.select('div h1')
|
|
358
|
+
|
|
359
|
+
``E > F``
|
|
360
|
+
~~~~~~~~~
|
|
361
|
+
|
|
362
|
+
An ``F`` element child of an ``E`` element. ::
|
|
363
|
+
|
|
364
|
+
headlines = css.select('html > body > div > h1')
|
|
365
|
+
|
|
366
|
+
``E + F``
|
|
367
|
+
~~~~~~~~~
|
|
368
|
+
|
|
369
|
+
An ``F`` element immediately preceded by an ``E`` element. ::
|
|
370
|
+
|
|
371
|
+
second = css.select('h1 + h2')
|
|
372
|
+
|
|
373
|
+
``E ~ F``
|
|
374
|
+
~~~~~~~~~
|
|
375
|
+
|
|
376
|
+
An ``F`` element preceded by an ``E`` element. ::
|
|
377
|
+
|
|
378
|
+
second = css.select('h1 ~ h2')
|
|
379
|
+
|
|
380
|
+
``E, F, G``
|
|
381
|
+
~~~~~~~~~~~
|
|
382
|
+
|
|
383
|
+
Elements of type ``E``, ``F`` and ``G``. ::
|
|
384
|
+
|
|
385
|
+
headlines = css.select('h1, h2, h3')
|
|
386
|
+
|
|
387
|
+
``E[foo=bar][bar=baz]``
|
|
388
|
+
~~~~~~~~~~~~~~~~~~~~~~~
|
|
389
|
+
|
|
390
|
+
An ``E`` element whose attributes match all following attribute selectors. ::
|
|
391
|
+
|
|
392
|
+
links = css.select('a[foo^=b][foo$=ar]')
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
Debugging
|
|
396
|
+
---------
|
|
397
|
+
|
|
398
|
+
You can set the ``PYJO_DOM_CSS_DEBUG`` environment variable to get some
|
|
399
|
+
advanced diagnostics information printed to :data:`sys.stderr`. ::
|
|
400
|
+
|
|
401
|
+
PYJO_DOM_CSS_DEBUG=1
|
|
402
|
+
|
|
403
|
+
See also
|
|
404
|
+
--------
|
|
405
|
+
|
|
406
|
+
:mod:`Pyjoyment`, https://mojolicious.org.
|
|
407
|
+
|
|
408
|
+
Ported from `Mojo::DOM::CSS <https://github.com/mojolicious/mojo/blob/v9.53/lib/Mojo/DOM/CSS.pm>`_
|
|
409
|
+
of Mojolicious 9.53.
|
|
410
|
+
|
|
411
|
+
Classes
|
|
412
|
+
-------
|
|
413
|
+
"""
|
|
414
|
+
|
|
415
|
+
import Pyjo.Base
|
|
416
|
+
|
|
417
|
+
from Pyjo.Util import getenv, trim, warn
|
|
418
|
+
|
|
419
|
+
import collections
|
|
420
|
+
import pprint
|
|
421
|
+
import re
|
|
422
|
+
|
|
423
|
+
from typing import Any, cast
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
# Any: a node of the tree of Pyjo.DOM.HTML
|
|
427
|
+
type Tree = list[Any]
|
|
428
|
+
|
|
429
|
+
# Any: a selector is a list of compounds, lists of the parts of the selector,
|
|
430
|
+
# with the combinators between them, as in Mojo
|
|
431
|
+
type Selector = list[Any]
|
|
432
|
+
|
|
433
|
+
DEBUG = bool(getenv('PYJO_DOM_CSS_DEBUG'))
|
|
434
|
+
|
|
435
|
+
_ESCAPE = r'\\[^0-9a-fA-F]|\\[0-9a-fA-F]{1,6}'
|
|
436
|
+
_ATTR_RE = re.compile(
|
|
437
|
+
rf"""
|
|
438
|
+
\[
|
|
439
|
+
((?:{_ESCAPE}|[\w\-])+) # Key
|
|
440
|
+
(?:
|
|
441
|
+
(\W)?= # Operator
|
|
442
|
+
(?:"((?:\\"|[^"])*)"|'((?:\\'|[^'])*)'|([^\]]+?)) # Value
|
|
443
|
+
(?:\s+(?:(i|I)|s|S))? # Case-sensitivity
|
|
444
|
+
)?
|
|
445
|
+
\]
|
|
446
|
+
""",
|
|
447
|
+
re.X,
|
|
448
|
+
)
|
|
449
|
+
_SEPARATOR_RE = re.compile(r'\s*,\s*')
|
|
450
|
+
_COMBINATOR_RE = re.compile(r'\s*([>+~])\s*')
|
|
451
|
+
_DESCENDANT_RE = re.compile(r'\s+')
|
|
452
|
+
_CLASS_ID_RE = re.compile(rf'([.#])((?:{_ESCAPE}\s?|[^,.#:\[\s>~+])+)')
|
|
453
|
+
_PSEUDO_CLASS_RE = re.compile(r':([\w\-]+)(?:\(((?:\([^)]+\)|[^)])+)\))?', re.S)
|
|
454
|
+
_TAG_RE = re.compile(rf'((?:{_ESCAPE}\s|\\[\s\S]|[^,.#:\[\s>~+])+)')
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
class Pyjo_DOM_CSS(Pyjo.Base.object):
|
|
458
|
+
"""
|
|
459
|
+
:mod:`Pyjo.DOM.CSS` inherits all methods from :mod:`Pyjo.Base` and
|
|
460
|
+
implements the following new ones.
|
|
461
|
+
"""
|
|
462
|
+
|
|
463
|
+
def __init__(self, tree: Tree | None = None) -> None:
|
|
464
|
+
self.tree: Tree | None = tree
|
|
465
|
+
"""::
|
|
466
|
+
|
|
467
|
+
tree = css.tree
|
|
468
|
+
css.tree = ['root']
|
|
469
|
+
|
|
470
|
+
Document Object Model. Note that this structure should only be used very
|
|
471
|
+
carefully since it is very dynamic.
|
|
472
|
+
"""
|
|
473
|
+
|
|
474
|
+
def matches(self, css: object, **ns: str) -> bool | None:
|
|
475
|
+
"""::
|
|
476
|
+
|
|
477
|
+
boolean = css.matches('head > title')
|
|
478
|
+
boolean = css.matches('svg|line', svg='http://www.w3.org/2000/svg')
|
|
479
|
+
|
|
480
|
+
Check if first node in :attr:`tree` matches the CSS selector. Trailing
|
|
481
|
+
keyword arguments can be used to declare xml namespace aliases.
|
|
482
|
+
"""
|
|
483
|
+
tree = self._tree()
|
|
484
|
+
if tree[0] != 'tag':
|
|
485
|
+
return None
|
|
486
|
+
return _match(_compile(css, **ns), tree, tree, _root(tree))
|
|
487
|
+
|
|
488
|
+
def select(self, css: object, **ns: str) -> list[Tree]:
|
|
489
|
+
"""::
|
|
490
|
+
|
|
491
|
+
results = css.select('head > title')
|
|
492
|
+
results = css.select('svg|line', svg='http://www.w3.org/2000/svg')
|
|
493
|
+
|
|
494
|
+
Run CSS selector against :attr:`tree`. Trailing keyword arguments can be
|
|
495
|
+
used to declare xml namespace aliases.
|
|
496
|
+
"""
|
|
497
|
+
return _select_all(self._tree(), _compile(css, **ns))
|
|
498
|
+
|
|
499
|
+
def select_one(self, css: object, **ns: str) -> Tree | None:
|
|
500
|
+
"""::
|
|
501
|
+
|
|
502
|
+
result = css.select_one('head > title')
|
|
503
|
+
result = css.select_one('svg|line', svg='http://www.w3.org/2000/svg')
|
|
504
|
+
|
|
505
|
+
Run CSS selector against :attr:`tree` and stop as soon as the first node
|
|
506
|
+
matched. Trailing keyword arguments can be used to declare xml namespace
|
|
507
|
+
aliases.
|
|
508
|
+
"""
|
|
509
|
+
return _select_one(self._tree(), _compile(css, **ns))
|
|
510
|
+
|
|
511
|
+
def _tree(self) -> Tree:
|
|
512
|
+
assert self.tree is not None
|
|
513
|
+
return self.tree
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def _absolutize(group: list[Selector]) -> list[Selector]:
|
|
517
|
+
return [s if _is_scoped(s) else [[['pc', 'scope', None]], ' ', *s] for s in group]
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def _all_tags(tree: Tree) -> list[Tree]:
|
|
521
|
+
tags: list[Tree] = []
|
|
522
|
+
queue = tree[1 if tree[0] == 'root' else 4 :]
|
|
523
|
+
while len(queue):
|
|
524
|
+
current = queue.pop(0)
|
|
525
|
+
if current[0] != 'tag':
|
|
526
|
+
continue
|
|
527
|
+
tags.append(current)
|
|
528
|
+
queue[0:0] = current[4:]
|
|
529
|
+
|
|
530
|
+
return tags
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def _attr(name: str, value_re: re.Pattern[str] | None, current: Tree) -> bool:
|
|
534
|
+
attrs = current[2]
|
|
535
|
+
if name not in attrs:
|
|
536
|
+
return False
|
|
537
|
+
|
|
538
|
+
value = attrs[name]
|
|
539
|
+
if value is None and value_re is not None:
|
|
540
|
+
return False
|
|
541
|
+
return value_re is None or bool(value_re.search(cast(str, value)))
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def _compile(css: object, **ns: str) -> list[Selector]:
|
|
545
|
+
string = trim(str(css))
|
|
546
|
+
|
|
547
|
+
group: list[Selector] = [[]]
|
|
548
|
+
pos = 0
|
|
549
|
+
while True:
|
|
550
|
+
selectors = group[-1]
|
|
551
|
+
if not len(selectors) or not isinstance(selectors[-1], list):
|
|
552
|
+
selectors.append([])
|
|
553
|
+
last = cast(list[Any], selectors[-1])
|
|
554
|
+
|
|
555
|
+
# Separator
|
|
556
|
+
if m := _SEPARATOR_RE.match(string, pos):
|
|
557
|
+
group.append([])
|
|
558
|
+
|
|
559
|
+
# Combinator
|
|
560
|
+
elif m := _COMBINATOR_RE.match(string, pos):
|
|
561
|
+
if not len(last):
|
|
562
|
+
last.append(['pc', 'scope', None])
|
|
563
|
+
selectors.append(m[1])
|
|
564
|
+
|
|
565
|
+
# Descendant combinator
|
|
566
|
+
elif m := _DESCENDANT_RE.match(string, pos):
|
|
567
|
+
if not len(last):
|
|
568
|
+
last.append(['pc', 'scope', None])
|
|
569
|
+
selectors.append(' ')
|
|
570
|
+
|
|
571
|
+
# Class or ID
|
|
572
|
+
elif m := _CLASS_ID_RE.match(string, pos):
|
|
573
|
+
name, op = ('class', '~') if m[1] == '.' else ('id', '')
|
|
574
|
+
last.append(['attr', name, _value(op, m[2])])
|
|
575
|
+
|
|
576
|
+
# Attributes
|
|
577
|
+
elif m := _ATTR_RE.match(string, pos):
|
|
578
|
+
value = m[3] if m[3] is not None else m[4] if m[4] is not None else m[5]
|
|
579
|
+
last.append(['attr', _unescape(m[1]), _value(m[2] or '', value, m[6])])
|
|
580
|
+
|
|
581
|
+
# Pseudo-class
|
|
582
|
+
elif m := _PSEUDO_CLASS_RE.match(string, pos):
|
|
583
|
+
name, args = m[1].lower(), m[2]
|
|
584
|
+
value: Any = args
|
|
585
|
+
|
|
586
|
+
# ":text" (raw text)
|
|
587
|
+
if name == 'text':
|
|
588
|
+
value = [re.compile(r[1]) if (r := re.match(r'^/(.+)/$', args)) else re.compile(re.escape(args), re.I)]
|
|
589
|
+
|
|
590
|
+
# ":is" and ":not" (contains more selectors)
|
|
591
|
+
if name in ('has', 'is', 'not'):
|
|
592
|
+
value = _compile(args, **ns)
|
|
593
|
+
|
|
594
|
+
# ":nth-*" (with An+B notation)
|
|
595
|
+
if name.startswith('nth-'):
|
|
596
|
+
value = _equation(args)
|
|
597
|
+
|
|
598
|
+
# ":first-*", ":last-*" (rewrite to ":nth-(last-)*")
|
|
599
|
+
if r := re.match(r'^(?:first-(.+)|(last-.+))$', name):
|
|
600
|
+
name, value = 'nth-' + (r[2] or r[1]), [0, 1]
|
|
601
|
+
|
|
602
|
+
last.append(['pc', name, value])
|
|
603
|
+
|
|
604
|
+
# Tag
|
|
605
|
+
elif m := _TAG_RE.match(string, pos):
|
|
606
|
+
name = m[1]
|
|
607
|
+
alias = None
|
|
608
|
+
if r := re.match(r'^([^|]*)\|', name):
|
|
609
|
+
name = name[r.end() :]
|
|
610
|
+
alias = r[1] if r[1] != '*' else None
|
|
611
|
+
if alias is not None and len(alias):
|
|
612
|
+
if alias not in ns:
|
|
613
|
+
return [[['invalid']]]
|
|
614
|
+
namespace = ns[alias]
|
|
615
|
+
else:
|
|
616
|
+
namespace = alias
|
|
617
|
+
last.append(['tag', None if name == '*' else _name(name), _unescape(namespace)])
|
|
618
|
+
|
|
619
|
+
else:
|
|
620
|
+
if pos < len(string):
|
|
621
|
+
raise ValueError(f'Unknown CSS selector: {string}')
|
|
622
|
+
break
|
|
623
|
+
|
|
624
|
+
pos = m.end()
|
|
625
|
+
|
|
626
|
+
if DEBUG:
|
|
627
|
+
warn(f'-- CSS Selector ({string})\n{pprint.pformat(group)}')
|
|
628
|
+
return group
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
def _equation(equation: str | None) -> list[int]:
|
|
632
|
+
if not equation:
|
|
633
|
+
return [0, 0]
|
|
634
|
+
|
|
635
|
+
# "even"
|
|
636
|
+
if re.match(r'^\s*even\s*$', equation, re.I):
|
|
637
|
+
return [2, 0]
|
|
638
|
+
|
|
639
|
+
# "odd"
|
|
640
|
+
if re.match(r'^\s*odd\s*$', equation, re.I):
|
|
641
|
+
return [2, 1]
|
|
642
|
+
|
|
643
|
+
# "4", "+4" or "-4"
|
|
644
|
+
if m := re.match(r'^\s*((?:\+|-)?\d+)\s*$', equation):
|
|
645
|
+
return [0, int(m[1])]
|
|
646
|
+
|
|
647
|
+
# "n", "4n", "+4n", "-4n", "n+1", "4n-1", "+4n-1" (and other variations)
|
|
648
|
+
if not (m := re.match(r'^\s*((?:\+|-)?(?:\d+)?)?n\s*((?:\+|-)\s*\d+)?\s*$', equation, re.I)):
|
|
649
|
+
return [0, 0]
|
|
650
|
+
a = m[1] or ''
|
|
651
|
+
# Unlike Mojolicious: "+" is 1, as "" is, where Perl takes it for the number 0
|
|
652
|
+
return [-1 if a == '-' else 1 if a in ('', '+') else int(a), int(''.join((m[2] or '0').split()))]
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
def _evaluate(group: list[Selector], tree: Tree, scope: Tree, pool: list[Tree]) -> list[Tree]:
|
|
656
|
+
results: list[Tree] = []
|
|
657
|
+
seen: set[int] = set()
|
|
658
|
+
for selector in group:
|
|
659
|
+
for node in _evaluate_one(selector, tree, scope, pool):
|
|
660
|
+
if id(node) not in seen:
|
|
661
|
+
seen.add(id(node))
|
|
662
|
+
results.append(node)
|
|
663
|
+
|
|
664
|
+
return results
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
def _evaluate_one(selector: Selector, tree: Tree, scope: Tree, pool: list[Tree]) -> list[Tree]:
|
|
668
|
+
|
|
669
|
+
# Match the leftmost compound, then propagate forward through combinators
|
|
670
|
+
parts = list(selector)
|
|
671
|
+
compound = parts.pop(0)
|
|
672
|
+
if not isinstance(compound, list):
|
|
673
|
+
return []
|
|
674
|
+
candidates = [n for n in pool if _selector(cast(list[Any], compound), n, tree, scope)]
|
|
675
|
+
|
|
676
|
+
while len(parts):
|
|
677
|
+
combinator = parts.pop(0)
|
|
678
|
+
next = parts.pop(0) if len(parts) else None
|
|
679
|
+
if not isinstance(next, list):
|
|
680
|
+
return []
|
|
681
|
+
|
|
682
|
+
seen: set[int] = set()
|
|
683
|
+
new: list[Tree] = []
|
|
684
|
+
for node in candidates:
|
|
685
|
+
for cand in _step_forward(combinator, node):
|
|
686
|
+
if id(cand) in seen:
|
|
687
|
+
continue
|
|
688
|
+
seen.add(id(cand))
|
|
689
|
+
if _selector(cast(list[Any], next), cand, tree, scope):
|
|
690
|
+
new.append(cand)
|
|
691
|
+
candidates = new
|
|
692
|
+
|
|
693
|
+
return candidates
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
def _is_scoped(selector: Selector) -> bool:
|
|
697
|
+
compounds = [cast(list[list[Any]], c) for c in selector if isinstance(c, list)]
|
|
698
|
+
for pc in (p for c in compounds for p in c if p[0] == 'pc'):
|
|
699
|
+
# Selector with ":scope"
|
|
700
|
+
if pc[1] == 'scope':
|
|
701
|
+
return True
|
|
702
|
+
|
|
703
|
+
# Argument of functional pseudo-class with ":scope"
|
|
704
|
+
if pc[1] in ('has', 'is', 'not') and any(_is_scoped(s) for s in pc[2]):
|
|
705
|
+
return True
|
|
706
|
+
|
|
707
|
+
return False
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
def _match(group: list[Selector], current: Tree, tree: Tree, scope: Tree) -> bool | None:
|
|
711
|
+
for selector in group:
|
|
712
|
+
if _match_one(selector, current, tree, scope):
|
|
713
|
+
return True
|
|
714
|
+
return None
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
def _match_one(selector: Selector, current: Tree, tree: Tree, scope: Tree) -> bool:
|
|
718
|
+
|
|
719
|
+
# Match the rightmost compound, then propagate backward through combinators
|
|
720
|
+
parts = list(reversed(selector))
|
|
721
|
+
compound = parts.pop(0)
|
|
722
|
+
if not isinstance(compound, list) or not _selector(cast(list[Any], compound), current, tree, scope):
|
|
723
|
+
return False
|
|
724
|
+
|
|
725
|
+
candidates = [current]
|
|
726
|
+
while len(parts):
|
|
727
|
+
combinator = parts.pop(0)
|
|
728
|
+
next = parts.pop(0) if len(parts) else None
|
|
729
|
+
if not isinstance(next, list):
|
|
730
|
+
return False
|
|
731
|
+
|
|
732
|
+
seen: set[int] = set()
|
|
733
|
+
new: list[Tree] = []
|
|
734
|
+
for node in candidates:
|
|
735
|
+
for cand in _step_back(combinator, node, scope):
|
|
736
|
+
if id(cand) in seen:
|
|
737
|
+
continue
|
|
738
|
+
seen.add(id(cand))
|
|
739
|
+
if _selector(cast(list[Any], next), cand, tree, scope):
|
|
740
|
+
new.append(cand)
|
|
741
|
+
if not len(new):
|
|
742
|
+
return False
|
|
743
|
+
candidates = new
|
|
744
|
+
|
|
745
|
+
return True
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
def _name(name: str) -> re.Pattern[str]:
|
|
749
|
+
return re.compile(rf'(?:^|:){re.escape(_unescape(name) or "")}$')
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
def _namespace(ns: str, current: Tree) -> bool:
|
|
753
|
+
attr = f'xmlns:{m[1]}' if (m := re.match(r'^([^:]+):', current[1])) else 'xmlns'
|
|
754
|
+
node: Tree | None = current
|
|
755
|
+
while node is not None:
|
|
756
|
+
if node[0] == 'root':
|
|
757
|
+
break
|
|
758
|
+
if attr in node[2]:
|
|
759
|
+
return node[2][attr] == ns
|
|
760
|
+
|
|
761
|
+
node = node[3]
|
|
762
|
+
|
|
763
|
+
# Failing to match yields true if searching for no namespace, false otherwise
|
|
764
|
+
return not len(ns)
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
def _pc(name: str, args: Any, current: Tree, tree: Tree, scope: Tree) -> bool:
|
|
768
|
+
|
|
769
|
+
# ":scope" (root can only be a :scope)
|
|
770
|
+
if name == 'scope':
|
|
771
|
+
return current is scope
|
|
772
|
+
if current[0] == 'root':
|
|
773
|
+
return False
|
|
774
|
+
|
|
775
|
+
# ":checked"
|
|
776
|
+
if name == 'checked':
|
|
777
|
+
return 'checked' in current[2] or 'selected' in current[2]
|
|
778
|
+
|
|
779
|
+
# ":not"
|
|
780
|
+
if name == 'not':
|
|
781
|
+
return not _match(args, current, current, scope)
|
|
782
|
+
|
|
783
|
+
# ":is"
|
|
784
|
+
if name == 'is':
|
|
785
|
+
return bool(_match(args, current, current, scope))
|
|
786
|
+
|
|
787
|
+
# ":has"
|
|
788
|
+
if name == 'has':
|
|
789
|
+
return _select_one(current, args) is not None
|
|
790
|
+
|
|
791
|
+
# ":empty"
|
|
792
|
+
if name == 'empty':
|
|
793
|
+
return not any(n[0] not in ('comment', 'pi') for n in current[4:])
|
|
794
|
+
|
|
795
|
+
# ":root"
|
|
796
|
+
if name == 'root':
|
|
797
|
+
return current[3] is not None and current[3][0] == 'root'
|
|
798
|
+
|
|
799
|
+
# ":text"
|
|
800
|
+
if name == 'text':
|
|
801
|
+
return any(n[0] in ('text', 'raw') and args[0].search(n[1]) for n in current[4:])
|
|
802
|
+
|
|
803
|
+
# ":any-link", ":link" and ":visited"
|
|
804
|
+
if name in ('any-link', 'link', 'visited'):
|
|
805
|
+
if current[0] != 'tag' or 'href' not in current[2]:
|
|
806
|
+
return False
|
|
807
|
+
return current[1] in ('a', 'area', 'link')
|
|
808
|
+
|
|
809
|
+
# ":only-child" or ":only-of-type"
|
|
810
|
+
if name in ('only-child', 'only-of-type'):
|
|
811
|
+
type = current[1] if name == 'only-of-type' else None
|
|
812
|
+
return all(s is current for s in _siblings(current, type))
|
|
813
|
+
|
|
814
|
+
# ":nth-child", ":nth-last-child", ":nth-of-type" or ":nth-last-of-type"
|
|
815
|
+
if isinstance(args, list):
|
|
816
|
+
type = current[1] if name in ('nth-of-type', 'nth-last-of-type') else None
|
|
817
|
+
siblings = _siblings(current, type)
|
|
818
|
+
index = next(i for i, s in enumerate(siblings) if s is current)
|
|
819
|
+
if name in ('nth-last-child', 'nth-last-of-type'):
|
|
820
|
+
index = len(siblings) - 1 - index
|
|
821
|
+
index += 1
|
|
822
|
+
|
|
823
|
+
a, b = cast(list[int], args)
|
|
824
|
+
delta = index - b
|
|
825
|
+
if delta == 0:
|
|
826
|
+
return True
|
|
827
|
+
# Perl's % takes the sign of the right operand, as Python's does
|
|
828
|
+
return a != 0 and (delta < 0) == (a < 0) and delta % a == 0
|
|
829
|
+
|
|
830
|
+
# Everything else
|
|
831
|
+
return False
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
def _root(tree: Tree) -> Tree:
|
|
835
|
+
while tree[0] != 'root':
|
|
836
|
+
tree = tree[3]
|
|
837
|
+
return tree
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
def _select(scope: Tree, group: list[Selector]) -> list[Tree]:
|
|
841
|
+
|
|
842
|
+
# Scoped selectors require the whole tree to be searched
|
|
843
|
+
tree = scope
|
|
844
|
+
if any(_is_scoped(s) for s in group):
|
|
845
|
+
group, tree = _absolutize(group), _root(scope)
|
|
846
|
+
|
|
847
|
+
# Pool includes $tree so ":scope" can match it, but results exclude it
|
|
848
|
+
tags = _all_tags(tree)
|
|
849
|
+
match = {id(n) for n in _evaluate(group, tree, scope, [tree, *tags])}
|
|
850
|
+
|
|
851
|
+
return [n for n in tags if id(n) in match]
|
|
852
|
+
|
|
853
|
+
|
|
854
|
+
# Mojo's _select with $one false
|
|
855
|
+
def _select_all(scope: Tree, group: list[Selector]) -> list[Tree]:
|
|
856
|
+
return _select(scope, group)
|
|
857
|
+
|
|
858
|
+
|
|
859
|
+
# Mojo's _select with $one true
|
|
860
|
+
def _select_one(scope: Tree, group: list[Selector]) -> Tree | None:
|
|
861
|
+
|
|
862
|
+
# Scoped selectors require the whole tree to be searched
|
|
863
|
+
selectors, tree = group, scope
|
|
864
|
+
if any(_is_scoped(s) for s in selectors):
|
|
865
|
+
selectors, tree = _absolutize(selectors), _root(scope)
|
|
866
|
+
|
|
867
|
+
# Without combinators the first match in document order can be returned right away
|
|
868
|
+
if all(len(s) == 1 and isinstance(s[0], list) for s in selectors):
|
|
869
|
+
queue: collections.deque[Tree] = collections.deque(tree[1 if tree[0] == 'root' else 4 :])
|
|
870
|
+
while queue:
|
|
871
|
+
current = queue.popleft()
|
|
872
|
+
if current[0] != 'tag':
|
|
873
|
+
continue
|
|
874
|
+
if any(_selector(s[0], current, tree, scope) for s in selectors):
|
|
875
|
+
return current
|
|
876
|
+
queue.extendleft(reversed(current[4:]))
|
|
877
|
+
return None
|
|
878
|
+
|
|
879
|
+
results = _select(scope, group)
|
|
880
|
+
return results[0] if len(results) else None
|
|
881
|
+
|
|
882
|
+
|
|
883
|
+
def _selector(selector: list[Any], current: Tree, tree: Tree, scope: Tree) -> bool:
|
|
884
|
+
|
|
885
|
+
# The root might be the scope
|
|
886
|
+
is_tag = current[0] == 'tag'
|
|
887
|
+
for s in selector:
|
|
888
|
+
type = s[0]
|
|
889
|
+
|
|
890
|
+
# Tag
|
|
891
|
+
if is_tag and type == 'tag':
|
|
892
|
+
if s[1] is not None and not s[1].search(current[1]):
|
|
893
|
+
return False
|
|
894
|
+
if s[2] is not None and not _namespace(s[2], current):
|
|
895
|
+
return False
|
|
896
|
+
|
|
897
|
+
# Attribute
|
|
898
|
+
elif is_tag and type == 'attr':
|
|
899
|
+
if not _attr(s[1], s[2], current):
|
|
900
|
+
return False
|
|
901
|
+
|
|
902
|
+
# Pseudo-class
|
|
903
|
+
elif type == 'pc':
|
|
904
|
+
if not _pc(s[1], s[2], current, tree, scope):
|
|
905
|
+
return False
|
|
906
|
+
|
|
907
|
+
# No match
|
|
908
|
+
else:
|
|
909
|
+
return False
|
|
910
|
+
|
|
911
|
+
return True
|
|
912
|
+
|
|
913
|
+
|
|
914
|
+
def _siblings(current: Tree, type: str | None = None) -> list[Tree]:
|
|
915
|
+
parent = current[3]
|
|
916
|
+
siblings = [n for n in parent[1 if parent[0] == 'root' else 4 :] if n[0] == 'tag']
|
|
917
|
+
if type is not None:
|
|
918
|
+
siblings = [n for n in siblings if n[1] == type]
|
|
919
|
+
return siblings
|
|
920
|
+
|
|
921
|
+
|
|
922
|
+
def _step_back(combinator: str, node: Tree, scope: Tree) -> list[Tree]:
|
|
923
|
+
|
|
924
|
+
# " " (ancestors) and ">" (parent only)
|
|
925
|
+
if combinator in (' ', '>'):
|
|
926
|
+
ancestors: list[Tree] = []
|
|
927
|
+
while node is not scope and node[0] != 'root' and (node := node[3]) is not None:
|
|
928
|
+
ancestors.append(node)
|
|
929
|
+
if combinator == '>' or node is scope:
|
|
930
|
+
break
|
|
931
|
+
return ancestors
|
|
932
|
+
|
|
933
|
+
# "~" (preceding siblings) and "+" (immediately preceding)
|
|
934
|
+
if node[0] == 'root':
|
|
935
|
+
return []
|
|
936
|
+
prev: list[Tree] = []
|
|
937
|
+
for sib in _siblings(node):
|
|
938
|
+
if sib is node:
|
|
939
|
+
break
|
|
940
|
+
prev.append(sib)
|
|
941
|
+
return prev[-1:] if combinator == '+' else prev
|
|
942
|
+
|
|
943
|
+
|
|
944
|
+
def _step_forward(combinator: str, node: Tree) -> list[Tree]:
|
|
945
|
+
|
|
946
|
+
# " " (descendants)
|
|
947
|
+
if combinator == ' ':
|
|
948
|
+
return _all_tags(node)
|
|
949
|
+
|
|
950
|
+
# ">" (children only)
|
|
951
|
+
if combinator == '>':
|
|
952
|
+
return [n for n in node[1 if node[0] == 'root' else 4 :] if n[0] == 'tag']
|
|
953
|
+
|
|
954
|
+
# "~" (following siblings) and "+" (immediately following)
|
|
955
|
+
if node[0] == 'root':
|
|
956
|
+
return []
|
|
957
|
+
following: list[Tree] = []
|
|
958
|
+
found = False
|
|
959
|
+
for sib in _siblings(node):
|
|
960
|
+
if found:
|
|
961
|
+
following.append(sib)
|
|
962
|
+
if sib is node:
|
|
963
|
+
found = True
|
|
964
|
+
return following[:1] if combinator == '+' else following
|
|
965
|
+
|
|
966
|
+
|
|
967
|
+
def _unescape(value: str | None) -> str | None:
|
|
968
|
+
if value is None:
|
|
969
|
+
return None
|
|
970
|
+
|
|
971
|
+
# Remove escaped newlines
|
|
972
|
+
value = value.replace('\\\n', '')
|
|
973
|
+
|
|
974
|
+
# Unescape Unicode characters
|
|
975
|
+
value = re.sub(r'\\([0-9a-fA-F]{1,6})\s?', lambda m: _chr(int(m[1], 16)), value)
|
|
976
|
+
|
|
977
|
+
# Remove backslash
|
|
978
|
+
return value.replace('\\', '')
|
|
979
|
+
|
|
980
|
+
|
|
981
|
+
def _chr(code: int) -> str:
|
|
982
|
+
# Unlike Mojolicious: Perl makes a character of any number, Python only up
|
|
983
|
+
# to 0x10FFFF, so a larger one is the replacement character
|
|
984
|
+
return chr(code) if code <= 0x10FFFF else '\ufffd'
|
|
985
|
+
|
|
986
|
+
|
|
987
|
+
def _value(op: str, value: str | None, insensitive: str | None = None) -> re.Pattern[str] | None:
|
|
988
|
+
if value is None:
|
|
989
|
+
return None
|
|
990
|
+
escaped = re.escape(_unescape(value) or '')
|
|
991
|
+
flags = re.I if insensitive else 0
|
|
992
|
+
|
|
993
|
+
# "~=" (word)
|
|
994
|
+
if op == '~':
|
|
995
|
+
return re.compile(rf'(?:^|\s+){escaped}(?:\s+|$)', flags)
|
|
996
|
+
|
|
997
|
+
# "|=" (hyphen-separated)
|
|
998
|
+
if op == '|':
|
|
999
|
+
return re.compile(rf'^{escaped}(?:-|$)', flags)
|
|
1000
|
+
|
|
1001
|
+
# "*=" (contains)
|
|
1002
|
+
if op == '*':
|
|
1003
|
+
return re.compile(escaped, flags)
|
|
1004
|
+
|
|
1005
|
+
# "^=" (begins with)
|
|
1006
|
+
if op == '^':
|
|
1007
|
+
return re.compile(rf'^{escaped}', flags)
|
|
1008
|
+
|
|
1009
|
+
# "$=" (ends with)
|
|
1010
|
+
if op == '$':
|
|
1011
|
+
return re.compile(rf'{escaped}$', flags)
|
|
1012
|
+
|
|
1013
|
+
# Everything else
|
|
1014
|
+
return re.compile(rf'^{escaped}$', flags)
|
|
1015
|
+
|
|
1016
|
+
|
|
1017
|
+
new = Pyjo_DOM_CSS.new
|
|
1018
|
+
object = Pyjo_DOM_CSS
|
|
1019
|
+
CSS = Pyjo_DOM_CSS
|