Pyjoyment 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (200) hide show
  1. Pyjo/App/ASGI.py +735 -0
  2. Pyjo/App/CGI.py +773 -0
  3. Pyjo/App/FastCGI.py +631 -0
  4. Pyjo/App/Lambda.py +987 -0
  5. Pyjo/App/WSGI.py +427 -0
  6. Pyjo/App/__init__.py +409 -0
  7. Pyjo/Asset/File.py +377 -0
  8. Pyjo/Asset/Memory.py +235 -0
  9. Pyjo/Asset/__init__.py +200 -0
  10. Pyjo/Base.py +215 -0
  11. Pyjo/Cache.py +119 -0
  12. Pyjo/Collection.py +570 -0
  13. Pyjo/Content/MultiPart.py +325 -0
  14. Pyjo/Content/Single.py +193 -0
  15. Pyjo/Content/__init__.py +806 -0
  16. Pyjo/Cookie/Request.py +90 -0
  17. Pyjo/Cookie/Response.py +294 -0
  18. Pyjo/Cookie/__init__.py +143 -0
  19. Pyjo/DOM/CSS.py +1019 -0
  20. Pyjo/DOM/HTML.py +550 -0
  21. Pyjo/DOM/__init__.py +1329 -0
  22. Pyjo/Date.py +294 -0
  23. Pyjo/EventEmitter.py +522 -0
  24. Pyjo/Exception.py +494 -0
  25. Pyjo/File.py +959 -0
  26. Pyjo/GetOpt.py +605 -0
  27. Pyjo/Headers.py +760 -0
  28. Pyjo/HelloWorld.py +77 -0
  29. Pyjo/Home.py +109 -0
  30. Pyjo/IOLoop/ChildProcess.py +591 -0
  31. Pyjo/IOLoop/Client.py +736 -0
  32. Pyjo/IOLoop/Datagram.py +982 -0
  33. Pyjo/IOLoop/Messages.py +664 -0
  34. Pyjo/IOLoop/Server.py +458 -0
  35. Pyjo/IOLoop/Stdio.py +446 -0
  36. Pyjo/IOLoop/Stream.py +644 -0
  37. Pyjo/IOLoop/Subprocess.py +419 -0
  38. Pyjo/IOLoop/TLS.py +360 -0
  39. Pyjo/IOLoop/__init__.py +1036 -0
  40. Pyjo/IOLoop/certs/README.md +41 -0
  41. Pyjo/IOLoop/certs/ca.crt +20 -0
  42. Pyjo/IOLoop/certs/server.crt +21 -0
  43. Pyjo/IOLoop/certs/server.key +28 -0
  44. Pyjo/JSON/Pointer.py +135 -0
  45. Pyjo/JSON/RPC.py +772 -0
  46. Pyjo/JSON/Schema.py +2506 -0
  47. Pyjo/JSON/__init__.py +573 -0
  48. Pyjo/Loader.py +342 -0
  49. Pyjo/Log.py +723 -0
  50. Pyjo/Message/Request.py +725 -0
  51. Pyjo/Message/Response.py +476 -0
  52. Pyjo/Message/__init__.py +830 -0
  53. Pyjo/Parameters.py +472 -0
  54. Pyjo/Path.py +419 -0
  55. Pyjo/Promise.py +894 -0
  56. Pyjo/Prove.py +1305 -0
  57. Pyjo/Reactor/Asyncio.py +854 -0
  58. Pyjo/Reactor/Poll.py +439 -0
  59. Pyjo/Reactor/__init__.py +525 -0
  60. Pyjo/SSE.py +119 -0
  61. Pyjo/Server/ASGI.py +486 -0
  62. Pyjo/Server/CGI.py +259 -0
  63. Pyjo/Server/Daemon.py +1007 -0
  64. Pyjo/Server/FastCGI.py +517 -0
  65. Pyjo/Server/Hot/__init__.py +687 -0
  66. Pyjo/Server/Hot/__main__.py +5 -0
  67. Pyjo/Server/Lambda.py +641 -0
  68. Pyjo/Server/Prefork.py +917 -0
  69. Pyjo/Server/Prethread.py +394 -0
  70. Pyjo/Server/Reload/Backend/Poll.py +114 -0
  71. Pyjo/Server/Reload/Backend/__init__.py +126 -0
  72. Pyjo/Server/Reload/__init__.py +482 -0
  73. Pyjo/Server/Reload/__main__.py +5 -0
  74. Pyjo/Server/Stdio.py +228 -0
  75. Pyjo/Server/WSGI.py +349 -0
  76. Pyjo/Server/__init__.py +396 -0
  77. Pyjo/String/Bytes.py +532 -0
  78. Pyjo/String/HTML.py +210 -0
  79. Pyjo/String/Mixin.py +333 -0
  80. Pyjo/String/Template.py +470 -0
  81. Pyjo/String/Unicode.py +553 -0
  82. Pyjo/String/__init__.py +0 -0
  83. Pyjo/TOML.py +232 -0
  84. Pyjo/Template.py +1290 -0
  85. Pyjo/Test/Pyjo.py +1454 -0
  86. Pyjo/Test/__init__.py +1233 -0
  87. Pyjo/Test/pytest_plugin.py +305 -0
  88. Pyjo/Transaction/HTTP.py +374 -0
  89. Pyjo/Transaction/WebSocket.py +709 -0
  90. Pyjo/Transaction/__init__.py +429 -0
  91. Pyjo/URL/Template.py +459 -0
  92. Pyjo/URL/__init__.py +651 -0
  93. Pyjo/Upload.py +139 -0
  94. Pyjo/UserAgent/CookieJar.py +391 -0
  95. Pyjo/UserAgent/Proxy.py +138 -0
  96. Pyjo/UserAgent/Server.py +239 -0
  97. Pyjo/UserAgent/Transactor.py +766 -0
  98. Pyjo/UserAgent/__init__.py +1243 -0
  99. Pyjo/Util.py +3567 -0
  100. Pyjo/WebSocket.py +332 -0
  101. Pyjo/YAML.py +1167 -0
  102. Pyjo/__init__.py +441 -0
  103. Pyjo/__main__.py +35 -0
  104. Pyjo/py.typed +0 -0
  105. Pyjoyment/Command/Author/__init__.py +20 -0
  106. Pyjoyment/Command/Author/generate/__init__.py +106 -0
  107. Pyjoyment/Command/Author/generate/app.py +299 -0
  108. Pyjoyment/Command/Author/generate/dockerfile.py +281 -0
  109. Pyjoyment/Command/Author/generate/kubernetes.py +514 -0
  110. Pyjoyment/Command/Author/generate/lambda_zip.py +372 -0
  111. Pyjoyment/Command/Author/generate/lite_app.py +113 -0
  112. Pyjoyment/Command/Author/generate/plugin.py +267 -0
  113. Pyjoyment/Command/Author/generate/pyproject.py +123 -0
  114. Pyjoyment/Command/Author/inflate.py +101 -0
  115. Pyjoyment/Command/__init__.py +332 -0
  116. Pyjoyment/Command/asgi.py +92 -0
  117. Pyjoyment/Command/cgi.py +90 -0
  118. Pyjoyment/Command/daemon.py +160 -0
  119. Pyjoyment/Command/eval.py +128 -0
  120. Pyjoyment/Command/fastcgi.py +99 -0
  121. Pyjoyment/Command/get.py +305 -0
  122. Pyjoyment/Command/lambda.py +117 -0
  123. Pyjoyment/Command/prefork.py +181 -0
  124. Pyjoyment/Command/prethread.py +126 -0
  125. Pyjoyment/Command/routes.py +133 -0
  126. Pyjoyment/Command/stdio.py +92 -0
  127. Pyjoyment/Command/version.py +141 -0
  128. Pyjoyment/Command/wsgi.py +93 -0
  129. Pyjoyment/Commands.py +546 -0
  130. Pyjoyment/Controller.py +1307 -0
  131. Pyjoyment/HelloWorld.py +56 -0
  132. Pyjoyment/Lite.py +494 -0
  133. Pyjoyment/Plugin/AllowedHosts.py +205 -0
  134. Pyjoyment/Plugin/Config.py +248 -0
  135. Pyjoyment/Plugin/DefaultHelpers.py +1202 -0
  136. Pyjoyment/Plugin/EPLRenderer.py +134 -0
  137. Pyjoyment/Plugin/EPRenderer.py +212 -0
  138. Pyjoyment/Plugin/HeaderCondition.py +98 -0
  139. Pyjoyment/Plugin/Health.py +325 -0
  140. Pyjoyment/Plugin/JSONConfig.py +172 -0
  141. Pyjoyment/Plugin/Mount.py +223 -0
  142. Pyjoyment/Plugin/PyRenderer.py +209 -0
  143. Pyjoyment/Plugin/TOMLConfig.py +141 -0
  144. Pyjoyment/Plugin/TagHelpers.py +959 -0
  145. Pyjoyment/Plugin/YAMLConfig.py +178 -0
  146. Pyjoyment/Plugin/__init__.py +80 -0
  147. Pyjoyment/Plugins.py +215 -0
  148. Pyjoyment/Renderer.py +673 -0
  149. Pyjoyment/Routes/Match.py +251 -0
  150. Pyjoyment/Routes/Pattern.py +443 -0
  151. Pyjoyment/Routes/Route.py +827 -0
  152. Pyjoyment/Routes/__init__.py +419 -0
  153. Pyjoyment/Sessions.py +255 -0
  154. Pyjoyment/Static.py +421 -0
  155. Pyjoyment/Types.py +238 -0
  156. Pyjoyment/Validator/Validation.py +395 -0
  157. Pyjoyment/Validator/__init__.py +304 -0
  158. Pyjoyment/__init__.py +1083 -0
  159. Pyjoyment/__main__.py +5 -0
  160. Pyjoyment/py.typed +0 -0
  161. Pyjoyment/resources/public/favicon.ico +0 -0
  162. Pyjoyment/resources/public/favicon.svg +345 -0
  163. Pyjoyment/resources/public/pyjo/bootstrap/LICENSE +21 -0
  164. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.bundle.min.js.map +1 -0
  165. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.css +6 -0
  166. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.js +7 -0
  167. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.min.css.map +1 -0
  168. Pyjoyment/resources/public/pyjo/failunicat.png +0 -0
  169. Pyjoyment/resources/public/pyjo/failunicat.svg +50 -0
  170. Pyjoyment/resources/public/pyjo/highlight.js/LICENSE +29 -0
  171. Pyjoyment/resources/public/pyjo/highlight.js/highlight-mojo-dark.css +56 -0
  172. Pyjoyment/resources/public/pyjo/highlight.js/highlight.min.js +1262 -0
  173. Pyjoyment/resources/public/pyjo/highlight.js/pyjoyment.min.js +1 -0
  174. Pyjoyment/resources/public/pyjo/logo-black-4x.png +0 -0
  175. Pyjoyment/resources/public/pyjo/logo-black-4x.svg +346 -0
  176. Pyjoyment/resources/public/pyjo/logo-white-2x.png +0 -0
  177. Pyjoyment/resources/public/pyjo/logo-white-2x.svg +346 -0
  178. Pyjoyment/resources/public/pyjo/logo-white-4x.png +0 -0
  179. Pyjoyment/resources/public/pyjo/logo-white-4x.svg +346 -0
  180. Pyjoyment/resources/public/pyjo/logo-white.png +0 -0
  181. Pyjoyment/resources/public/pyjo/logo-white.svg +346 -0
  182. Pyjoyment/resources/public/pyjo/logo.png +0 -0
  183. Pyjoyment/resources/public/pyjo/logo.svg +346 -0
  184. Pyjoyment/resources/public/pyjo/notfound.png +0 -0
  185. Pyjoyment/resources/public/pyjo/notfound.svg +3 -0
  186. Pyjoyment/resources/public/pyjo/nounicat.png +0 -0
  187. Pyjoyment/resources/public/pyjo/nounicat.svg +5 -0
  188. Pyjoyment/resources/public/pyjo/pinstripe-dark.png +0 -0
  189. Pyjoyment/resources/public/pyjo/pinstripe-dark.svg +4 -0
  190. Pyjoyment/resources/public/pyjo/pinstripe-light.png +0 -0
  191. Pyjoyment/resources/public/pyjo/pinstripe-light.svg +4 -0
  192. Pyjoyment/resources/public/pyjo/pyjo.css +204 -0
  193. Pyjoyment/resources/templates/pyjo/debug.html.ep +346 -0
  194. Pyjoyment/resources/templates/pyjo/exception.html.ep +32 -0
  195. Pyjoyment/resources/templates/pyjo/not_found.html.ep +45 -0
  196. pyjoyment-0.1.0.dist-info/METADATA +376 -0
  197. pyjoyment-0.1.0.dist-info/RECORD +200 -0
  198. pyjoyment-0.1.0.dist-info/WHEEL +4 -0
  199. pyjoyment-0.1.0.dist-info/entry_points.txt +8 -0
  200. pyjoyment-0.1.0.dist-info/licenses/LICENSE +201 -0
Pyjo/DOM/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