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