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/Template.py
ADDED
|
@@ -0,0 +1,1290 @@
|
|
|
1
|
+
r"""
|
|
2
|
+
Pyjo.Template - Python-ish templates
|
|
3
|
+
====================================
|
|
4
|
+
::
|
|
5
|
+
|
|
6
|
+
from Pyjo.Template import Template
|
|
7
|
+
|
|
8
|
+
# Use Python modules
|
|
9
|
+
mt = Template()
|
|
10
|
+
print(mt.render('''
|
|
11
|
+
% import time
|
|
12
|
+
<div>
|
|
13
|
+
% now = time.gmtime(0)
|
|
14
|
+
Time: <%= time.strftime('%H:%M:%S', now) %>
|
|
15
|
+
</div>
|
|
16
|
+
'''))
|
|
17
|
+
|
|
18
|
+
# Or import the module, as in Google Python Style Guide
|
|
19
|
+
|
|
20
|
+
import Pyjo.Template
|
|
21
|
+
|
|
22
|
+
# Use Python modules
|
|
23
|
+
mt = Pyjo.Template.new()
|
|
24
|
+
print(mt.render('''
|
|
25
|
+
% import time
|
|
26
|
+
<div>
|
|
27
|
+
% now = time.gmtime(0)
|
|
28
|
+
Time: <%= time.strftime('%H:%M:%S', now) %>
|
|
29
|
+
</div>
|
|
30
|
+
'''))
|
|
31
|
+
|
|
32
|
+
# Render with arguments
|
|
33
|
+
print(mt.render('''
|
|
34
|
+
% numbers, title = args
|
|
35
|
+
<div>
|
|
36
|
+
<h1><%= title %></h1>
|
|
37
|
+
% for i in numbers {
|
|
38
|
+
Test <%= i %>
|
|
39
|
+
% }
|
|
40
|
+
</div>
|
|
41
|
+
''', range(1, 14), 'Hello World!'))
|
|
42
|
+
|
|
43
|
+
# Render with named variables
|
|
44
|
+
print(Pyjo.Template.new(vars=True).render('''
|
|
45
|
+
<div>
|
|
46
|
+
<h1><%= title %></h1>
|
|
47
|
+
%= 5 + 5
|
|
48
|
+
</div>
|
|
49
|
+
''', {'title': 'Hello World!'}))
|
|
50
|
+
|
|
51
|
+
:mod:`Pyjo.Template` is a minimalistic, fast, and very Python-ish template
|
|
52
|
+
engine, designed specifically for all those small tasks that come up during
|
|
53
|
+
big projects. Like preprocessing a configuration file, generating text from
|
|
54
|
+
heredocs and stuff like that.
|
|
55
|
+
|
|
56
|
+
Syntax
|
|
57
|
+
------
|
|
58
|
+
|
|
59
|
+
::
|
|
60
|
+
|
|
61
|
+
<% Python code %>
|
|
62
|
+
<%= Python expression, replaced with result %>
|
|
63
|
+
<%== Python expression, replaced with XML escaped result %>
|
|
64
|
+
<%# Comment, useful for debugging %>
|
|
65
|
+
<%% Replaced with "<%", useful for generating templates %>
|
|
66
|
+
% Python code line, treated as "<% line =%>" (explained later)
|
|
67
|
+
%= Python expression line, treated as "<%= line %>"
|
|
68
|
+
%== Python expression line, treated as "<%== line %>"
|
|
69
|
+
%# Comment line, useful for debugging
|
|
70
|
+
%% Replaced with "%", useful for generating templates
|
|
71
|
+
|
|
72
|
+
Escaping behavior can be reversed with the :attr:`~Pyjo_Template.auto_escape`
|
|
73
|
+
attribute. ::
|
|
74
|
+
|
|
75
|
+
<%= Python expression, replaced with XML escaped result %>
|
|
76
|
+
<%== Python expression, replaced with result %>
|
|
77
|
+
|
|
78
|
+
Markup, an object with the method ``__html__``, as :mod:`Pyjo.String.HTML`,
|
|
79
|
+
is always excluded from automatic escaping, and a value is rendered as an
|
|
80
|
+
interpolation of :mod:`Pyjo.String.Template`: ``None`` is nothing, a template
|
|
81
|
+
is markup, and a list is rendered item by item. ::
|
|
82
|
+
|
|
83
|
+
% from Pyjo.String.HTML import html
|
|
84
|
+
<%= html('<div>excluded!</div>') %>
|
|
85
|
+
|
|
86
|
+
The indentation of the template does not matter: a code line that ends with
|
|
87
|
+
``:`` opens a block, which ``end`` closes, and a line that starts with
|
|
88
|
+
``elif``, ``else``, ``except``, ``finally`` or ``case`` closes the block
|
|
89
|
+
before it and opens the next one. ``end`` closes the whole ``match`` after its
|
|
90
|
+
last ``case``. Braces can open and close a block too, which makes
|
|
91
|
+
one-liners easy: ``{`` at the end of a line that starts with a keyword of a
|
|
92
|
+
block opens it, and ``}`` at the start of a line closes it, as in ``} else
|
|
93
|
+
{``. Any other brace is the one of Python, so a ``dict`` or a ``set`` that
|
|
94
|
+
starts at the end of such a line is put in parentheses. ::
|
|
95
|
+
|
|
96
|
+
% for name in ['Baerbel', 'Wolfgang']:
|
|
97
|
+
% if name.startswith('B'):
|
|
98
|
+
<b><%= name %></b>
|
|
99
|
+
% else:
|
|
100
|
+
<i><%= name %></i>
|
|
101
|
+
% end
|
|
102
|
+
% end
|
|
103
|
+
<% for i in range(3) { %><%= i %><% } %>
|
|
104
|
+
|
|
105
|
+
Whitespace characters around tags can be trimmed by adding an additional equal
|
|
106
|
+
sign to the end of a tag. ::
|
|
107
|
+
|
|
108
|
+
<% for i in range(1, 4) { %>
|
|
109
|
+
<%= 'Trim all whitespace characters around this expression' =%>
|
|
110
|
+
<% } %>
|
|
111
|
+
|
|
112
|
+
Newline characters can be escaped with a backslash. ::
|
|
113
|
+
|
|
114
|
+
This is <%= 1 + 1 %> a\
|
|
115
|
+
single line
|
|
116
|
+
|
|
117
|
+
And a backslash in front of a newline character can be escaped with another
|
|
118
|
+
backslash. ::
|
|
119
|
+
|
|
120
|
+
This will <%= 1 + 1 %> result\\
|
|
121
|
+
in multiple\\
|
|
122
|
+
lines
|
|
123
|
+
|
|
124
|
+
A newline character gets appended automatically to every template, unless the
|
|
125
|
+
last character is a backslash. And empty lines at the end of a template are
|
|
126
|
+
ignored. ::
|
|
127
|
+
|
|
128
|
+
There is <%= 1 + 1 %> no newline at the end here\
|
|
129
|
+
|
|
130
|
+
You can capture whole template blocks for reuse later with the ``begin`` and
|
|
131
|
+
``end`` keywords. Just be aware that both keywords are part of the surrounding
|
|
132
|
+
tag and not actual Python code, so there can only be whitespace after
|
|
133
|
+
``begin`` and before ``end``. ::
|
|
134
|
+
|
|
135
|
+
<% block = begin %>
|
|
136
|
+
<% name = args[0] =%>
|
|
137
|
+
Hello <%= name %>.
|
|
138
|
+
<% end %>
|
|
139
|
+
<%= block('Baerbel') %>
|
|
140
|
+
<%= block('Wolfgang') %>
|
|
141
|
+
|
|
142
|
+
Python lines can also be indented freely. ::
|
|
143
|
+
|
|
144
|
+
% block = begin
|
|
145
|
+
% name = args[0]
|
|
146
|
+
Hello <%= name %>.
|
|
147
|
+
% end
|
|
148
|
+
%= block('Baerbel')
|
|
149
|
+
%= block('Wolfgang')
|
|
150
|
+
|
|
151
|
+
:mod:`Pyjo.Template` templates get compiled to a Python function, that means
|
|
152
|
+
you can access arguments simply via ``args``. ::
|
|
153
|
+
|
|
154
|
+
% foo, bar = args
|
|
155
|
+
% x = args[0]
|
|
156
|
+
test 123 <%= foo %>
|
|
157
|
+
|
|
158
|
+
The compilation of templates to Python code can make debugging a bit tricky,
|
|
159
|
+
but :mod:`Pyjo.Template` will return :mod:`Pyjo.Exception` objects that
|
|
160
|
+
stringify to error messages with context. ::
|
|
161
|
+
|
|
162
|
+
name 'xx' is not defined at template line 4.
|
|
163
|
+
Context:
|
|
164
|
+
1: <html>
|
|
165
|
+
2: </head>
|
|
166
|
+
3: <body>
|
|
167
|
+
4: % i = 2; xx
|
|
168
|
+
5: %= i * 2
|
|
169
|
+
6: </body>
|
|
170
|
+
Traceback (most recent call first):
|
|
171
|
+
File "template", line 4, in "_template"
|
|
172
|
+
File "path/to/Pyjo/Template.py", line 123, in "process"
|
|
173
|
+
File "path/to/myapp.py", line 123, in "<module>"
|
|
174
|
+
|
|
175
|
+
Debugging
|
|
176
|
+
---------
|
|
177
|
+
|
|
178
|
+
You can set the ``PYJO_TEMPLATE_DEBUG`` environment variable to get some
|
|
179
|
+
advanced diagnostics information printed to ``STDERR``. ::
|
|
180
|
+
|
|
181
|
+
PYJO_TEMPLATE_DEBUG=1
|
|
182
|
+
|
|
183
|
+
See also
|
|
184
|
+
--------
|
|
185
|
+
|
|
186
|
+
:mod:`Pyjoyment`, https://mojolicious.org.
|
|
187
|
+
|
|
188
|
+
Ported from `Mojo::Template <https://github.com/mojolicious/mojo/blob/v9.49/lib/Mojo/Template.pm>`_
|
|
189
|
+
of Mojolicious 9.49.
|
|
190
|
+
|
|
191
|
+
Classes
|
|
192
|
+
-------
|
|
193
|
+
"""
|
|
194
|
+
|
|
195
|
+
import Pyjo.Base
|
|
196
|
+
import Pyjo.Exception
|
|
197
|
+
import Pyjo.File
|
|
198
|
+
import Pyjo.String.HTML
|
|
199
|
+
import Pyjo.String.Template
|
|
200
|
+
|
|
201
|
+
from Pyjo.String.Unicode import split
|
|
202
|
+
from Pyjo.Util import decode
|
|
203
|
+
|
|
204
|
+
import builtins
|
|
205
|
+
import inspect
|
|
206
|
+
import keyword
|
|
207
|
+
import os
|
|
208
|
+
import re
|
|
209
|
+
import sys
|
|
210
|
+
import traceback
|
|
211
|
+
import types
|
|
212
|
+
|
|
213
|
+
from collections.abc import Callable, Iterator
|
|
214
|
+
from dataclasses import dataclass
|
|
215
|
+
from typing import Any, Self, cast
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
type Anything = builtins.object
|
|
219
|
+
|
|
220
|
+
DEBUG = bool(os.environ.get('PYJO_TEMPLATE_DEBUG'))
|
|
221
|
+
|
|
222
|
+
# The keywords that open a block with a brace, and those that continue the block before
|
|
223
|
+
BLOCK_KEYWORDS = {
|
|
224
|
+
'async',
|
|
225
|
+
'class',
|
|
226
|
+
'case',
|
|
227
|
+
'def',
|
|
228
|
+
'elif',
|
|
229
|
+
'else',
|
|
230
|
+
'except',
|
|
231
|
+
'finally',
|
|
232
|
+
'for',
|
|
233
|
+
'if',
|
|
234
|
+
'match',
|
|
235
|
+
'try',
|
|
236
|
+
'while',
|
|
237
|
+
'with',
|
|
238
|
+
}
|
|
239
|
+
CONTINUE_KEYWORDS = {'case', 'elif', 'else', 'except', 'finally'}
|
|
240
|
+
|
|
241
|
+
RE_KEYWORD = re.compile(r'\s*(?:\}\s*)?([A-Za-z_]\w*)')
|
|
242
|
+
RE_STRING_PREFIX = re.compile(r'(?<![\w])([rRbBfFtTuU]{1,2})\Z')
|
|
243
|
+
|
|
244
|
+
_SANDBOXES: dict[str, types.ModuleType] = {}
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class Pyjo_Template(Pyjo.Base.object):
|
|
248
|
+
"""
|
|
249
|
+
:mod:`Pyjo.Template` inherits all methods from :mod:`Pyjo.Base` and
|
|
250
|
+
implements the following new ones.
|
|
251
|
+
"""
|
|
252
|
+
|
|
253
|
+
def __init__(
|
|
254
|
+
self,
|
|
255
|
+
*,
|
|
256
|
+
append: str = '',
|
|
257
|
+
auto_escape: bool | None = None,
|
|
258
|
+
capture_end: str = 'end',
|
|
259
|
+
capture_start: str = 'begin',
|
|
260
|
+
code: str = '',
|
|
261
|
+
comment_mark: str = '#',
|
|
262
|
+
compiled: Callable[..., Any] | None = None,
|
|
263
|
+
encoding: str | None = 'UTF-8',
|
|
264
|
+
escape: Callable[[Any], str] | None = None,
|
|
265
|
+
escape_mark: str = '=',
|
|
266
|
+
expression_mark: str = '=',
|
|
267
|
+
line_start: str = '%',
|
|
268
|
+
name: str | None = None,
|
|
269
|
+
namespace: str = 'Pyjo.Template.Sandbox',
|
|
270
|
+
prepend: str = '',
|
|
271
|
+
replace_mark: str = '%',
|
|
272
|
+
tag_end: str = '%>',
|
|
273
|
+
tag_start: str = '<%',
|
|
274
|
+
tree: list[list[str]] | None = None,
|
|
275
|
+
trim_mark: str = '=',
|
|
276
|
+
unparsed: str = '',
|
|
277
|
+
vars: bool | None = None,
|
|
278
|
+
) -> None:
|
|
279
|
+
"""::
|
|
280
|
+
|
|
281
|
+
mt = Pyjo.Template.new()
|
|
282
|
+
mt = Pyjo.Template.new(auto_escape=True)
|
|
283
|
+
|
|
284
|
+
Construct a new :mod:`Pyjo.Template` object.
|
|
285
|
+
"""
|
|
286
|
+
self.auto_escape = auto_escape
|
|
287
|
+
"""::
|
|
288
|
+
|
|
289
|
+
boolean = mt.auto_escape
|
|
290
|
+
mt.auto_escape = boolean
|
|
291
|
+
|
|
292
|
+
Activate automatic escaping. ::
|
|
293
|
+
|
|
294
|
+
# "<html>"
|
|
295
|
+
Pyjo.Template.new(auto_escape=True).render("<%= '<html>' %>")
|
|
296
|
+
"""
|
|
297
|
+
|
|
298
|
+
self.append = append
|
|
299
|
+
"""::
|
|
300
|
+
|
|
301
|
+
code = mt.append
|
|
302
|
+
mt.append = 'print("Processed template")'
|
|
303
|
+
|
|
304
|
+
Append Python code to compiled template, as lines of their own.
|
|
305
|
+
"""
|
|
306
|
+
|
|
307
|
+
self.capture_end = capture_end
|
|
308
|
+
"""::
|
|
309
|
+
|
|
310
|
+
end = mt.capture_end
|
|
311
|
+
mt.capture_end = 'end'
|
|
312
|
+
|
|
313
|
+
Keyword indicating the end of a capture block, defaults to ``end``. ::
|
|
314
|
+
|
|
315
|
+
<% block = begin %>
|
|
316
|
+
Some data!
|
|
317
|
+
<% end %>
|
|
318
|
+
"""
|
|
319
|
+
|
|
320
|
+
self.capture_start = capture_start
|
|
321
|
+
"""::
|
|
322
|
+
|
|
323
|
+
start = mt.capture_start
|
|
324
|
+
mt.capture_start = 'begin'
|
|
325
|
+
|
|
326
|
+
Keyword indicating the start of a capture block, defaults to
|
|
327
|
+
``begin``. ::
|
|
328
|
+
|
|
329
|
+
<% block = begin %>
|
|
330
|
+
Some data!
|
|
331
|
+
<% end %>
|
|
332
|
+
"""
|
|
333
|
+
|
|
334
|
+
self.code = code
|
|
335
|
+
"""::
|
|
336
|
+
|
|
337
|
+
code = mt.code
|
|
338
|
+
mt.code = code
|
|
339
|
+
|
|
340
|
+
Python code for template if available.
|
|
341
|
+
"""
|
|
342
|
+
|
|
343
|
+
self.comment_mark = comment_mark
|
|
344
|
+
"""::
|
|
345
|
+
|
|
346
|
+
mark = mt.comment_mark
|
|
347
|
+
mt.comment_mark = '#'
|
|
348
|
+
|
|
349
|
+
Character indicating the start of a comment, defaults to ``#``. ::
|
|
350
|
+
|
|
351
|
+
<%# This is a comment %>
|
|
352
|
+
"""
|
|
353
|
+
|
|
354
|
+
self.compiled = compiled
|
|
355
|
+
"""::
|
|
356
|
+
|
|
357
|
+
compiled = mt.compiled
|
|
358
|
+
mt.compiled = compiled
|
|
359
|
+
|
|
360
|
+
Compiled template code if available.
|
|
361
|
+
"""
|
|
362
|
+
|
|
363
|
+
self.encoding = encoding
|
|
364
|
+
"""::
|
|
365
|
+
|
|
366
|
+
encoding = mt.encoding
|
|
367
|
+
mt.encoding = 'UTF-8'
|
|
368
|
+
|
|
369
|
+
Encoding used for template files, defaults to ``UTF-8``.
|
|
370
|
+
"""
|
|
371
|
+
|
|
372
|
+
# Unlike Mojolicious: the values are escaped as the interpolations of a template, as markup is what has
|
|
373
|
+
# __html__, not every Mojo::ByteStream
|
|
374
|
+
self.escape: Callable[[Any], str] = escape or Pyjo.String.Template.escape
|
|
375
|
+
"""::
|
|
376
|
+
|
|
377
|
+
cb = mt.escape
|
|
378
|
+
mt.escape = lambda value: ...
|
|
379
|
+
|
|
380
|
+
A callback used to escape the results of escaped expressions, defaults
|
|
381
|
+
to :func:`Pyjo.String.Template.escape`. ::
|
|
382
|
+
|
|
383
|
+
mt.escape = lambda value: str(value)[::-1]
|
|
384
|
+
"""
|
|
385
|
+
|
|
386
|
+
self.escape_mark = escape_mark
|
|
387
|
+
"""::
|
|
388
|
+
|
|
389
|
+
mark = mt.escape_mark
|
|
390
|
+
mt.escape_mark = '='
|
|
391
|
+
|
|
392
|
+
Character indicating the start of an escaped expression, defaults to
|
|
393
|
+
``=``. ::
|
|
394
|
+
|
|
395
|
+
<%== foo %>
|
|
396
|
+
"""
|
|
397
|
+
|
|
398
|
+
self.expression_mark = expression_mark
|
|
399
|
+
"""::
|
|
400
|
+
|
|
401
|
+
mark = mt.expression_mark
|
|
402
|
+
mt.expression_mark = '='
|
|
403
|
+
|
|
404
|
+
Character indicating the start of an expression, defaults to ``=``. ::
|
|
405
|
+
|
|
406
|
+
<%= foo %>
|
|
407
|
+
"""
|
|
408
|
+
|
|
409
|
+
self.line_start = line_start
|
|
410
|
+
"""::
|
|
411
|
+
|
|
412
|
+
start = mt.line_start
|
|
413
|
+
mt.line_start = '%'
|
|
414
|
+
|
|
415
|
+
Character indicating the start of a code line, defaults to ``%``. ::
|
|
416
|
+
|
|
417
|
+
% foo = 23
|
|
418
|
+
"""
|
|
419
|
+
|
|
420
|
+
self._name = name
|
|
421
|
+
|
|
422
|
+
# Unlike Mojolicious: a namespace is a module, which Python has as Perl has a package
|
|
423
|
+
self.namespace = namespace
|
|
424
|
+
"""::
|
|
425
|
+
|
|
426
|
+
namespace = mt.namespace
|
|
427
|
+
mt.namespace = 'main'
|
|
428
|
+
|
|
429
|
+
Namespace used to compile templates, defaults to
|
|
430
|
+
``Pyjo.Template.Sandbox``: the module of the name, if it is loaded, or
|
|
431
|
+
a module of its own. Note that namespaces should only be shared very
|
|
432
|
+
carefully between templates, since the globals of the module will not
|
|
433
|
+
be cleared automatically.
|
|
434
|
+
"""
|
|
435
|
+
|
|
436
|
+
self.prepend = prepend
|
|
437
|
+
"""::
|
|
438
|
+
|
|
439
|
+
code = mt.prepend
|
|
440
|
+
mt.prepend = 'this = args[0]'
|
|
441
|
+
|
|
442
|
+
Prepend Python code to compiled template, as lines of their own.
|
|
443
|
+
"""
|
|
444
|
+
|
|
445
|
+
self.replace_mark = replace_mark
|
|
446
|
+
"""::
|
|
447
|
+
|
|
448
|
+
mark = mt.replace_mark
|
|
449
|
+
mt.replace_mark = '%'
|
|
450
|
+
|
|
451
|
+
Character used for escaping the start of a tag or line, defaults to
|
|
452
|
+
``%``. ::
|
|
453
|
+
|
|
454
|
+
<%% foo = 23 %>
|
|
455
|
+
"""
|
|
456
|
+
|
|
457
|
+
self.tag_start = tag_start
|
|
458
|
+
"""::
|
|
459
|
+
|
|
460
|
+
start = mt.tag_start
|
|
461
|
+
mt.tag_start = '<%'
|
|
462
|
+
|
|
463
|
+
Characters indicating the start of a tag, defaults to ``<%``. ::
|
|
464
|
+
|
|
465
|
+
<% foo = 23 %>
|
|
466
|
+
"""
|
|
467
|
+
|
|
468
|
+
self.tag_end = tag_end
|
|
469
|
+
"""::
|
|
470
|
+
|
|
471
|
+
end = mt.tag_end
|
|
472
|
+
mt.tag_end = '%>'
|
|
473
|
+
|
|
474
|
+
Characters indicating the end of a tag, defaults to ``%>``. ::
|
|
475
|
+
|
|
476
|
+
<%= foo %>
|
|
477
|
+
"""
|
|
478
|
+
|
|
479
|
+
self.tree: list[list[str]] = tree if tree is not None else []
|
|
480
|
+
"""::
|
|
481
|
+
|
|
482
|
+
tree = mt.tree
|
|
483
|
+
mt.tree = [['text', 'foo'], ['line']]
|
|
484
|
+
|
|
485
|
+
Template in parsed form if available. Note that this structure should
|
|
486
|
+
only be used very carefully since it is very dynamic.
|
|
487
|
+
"""
|
|
488
|
+
|
|
489
|
+
self.trim_mark = trim_mark
|
|
490
|
+
"""::
|
|
491
|
+
|
|
492
|
+
mark = mt.trim_mark
|
|
493
|
+
mt.trim_mark = '-'
|
|
494
|
+
|
|
495
|
+
Character activating automatic whitespace trimming, defaults to
|
|
496
|
+
``=``. ::
|
|
497
|
+
|
|
498
|
+
<%= foo =%>
|
|
499
|
+
"""
|
|
500
|
+
|
|
501
|
+
self.unparsed = unparsed
|
|
502
|
+
"""::
|
|
503
|
+
|
|
504
|
+
unparsed = mt.unparsed
|
|
505
|
+
mt.unparsed = '<%= 1 + 1 %>'
|
|
506
|
+
|
|
507
|
+
Raw unparsed template if available.
|
|
508
|
+
"""
|
|
509
|
+
|
|
510
|
+
self.vars = vars
|
|
511
|
+
"""::
|
|
512
|
+
|
|
513
|
+
boolean = mt.vars
|
|
514
|
+
mt.vars = boolean
|
|
515
|
+
|
|
516
|
+
Instead of a list of values, use a dict with named variables to pass
|
|
517
|
+
data to templates. ::
|
|
518
|
+
|
|
519
|
+
# "works!"
|
|
520
|
+
Pyjo.Template.new(vars=True).render('<%= test %>!', {'test': 'works'})
|
|
521
|
+
"""
|
|
522
|
+
|
|
523
|
+
# Not in Mojolicious: the template lines of the lines of the compiled code, and its code objects, which find
|
|
524
|
+
# the frames of the template in a traceback
|
|
525
|
+
self._body: list[int] = []
|
|
526
|
+
self._lines: list[int] = []
|
|
527
|
+
self._codes: set[types.CodeType] = set()
|
|
528
|
+
self._projection: list[str] = []
|
|
529
|
+
|
|
530
|
+
@property
|
|
531
|
+
def name(self) -> str:
|
|
532
|
+
"""::
|
|
533
|
+
|
|
534
|
+
name = mt.name
|
|
535
|
+
mt.name = 'foo.ep'
|
|
536
|
+
|
|
537
|
+
Name of template currently being processed, defaults to ``template``.
|
|
538
|
+
"""
|
|
539
|
+
if self._name is None:
|
|
540
|
+
self._name = 'template'
|
|
541
|
+
return self._name
|
|
542
|
+
|
|
543
|
+
@name.setter
|
|
544
|
+
def name(self, value: str) -> None:
|
|
545
|
+
self._name = value
|
|
546
|
+
|
|
547
|
+
def parse(self, template: str) -> Self:
|
|
548
|
+
"""::
|
|
549
|
+
|
|
550
|
+
mt = mt.parse('<%= 1 + 1 %>')
|
|
551
|
+
|
|
552
|
+
Parse template into :attr:`tree`.
|
|
553
|
+
"""
|
|
554
|
+
|
|
555
|
+
# Clean start
|
|
556
|
+
tree: list[list[str]] = []
|
|
557
|
+
self.unparsed = template
|
|
558
|
+
self.tree = tree
|
|
559
|
+
self.compiled = None
|
|
560
|
+
|
|
561
|
+
tag = self.tag_start
|
|
562
|
+
replace = self.replace_mark
|
|
563
|
+
expr = self.expression_mark
|
|
564
|
+
escp = self.escape_mark
|
|
565
|
+
cpen = self.capture_end
|
|
566
|
+
cmnt = self.comment_mark
|
|
567
|
+
cpst = self.capture_start
|
|
568
|
+
trim = self.trim_mark
|
|
569
|
+
end = self.tag_end
|
|
570
|
+
start = self.line_start
|
|
571
|
+
|
|
572
|
+
q = re.escape
|
|
573
|
+
line_re = re.compile(rf'^(\s*){q(start)}(?:({q(replace)})|({q(cmnt)})|({q(expr)}))?(.*)$')
|
|
574
|
+
token_re = re.compile(
|
|
575
|
+
'('
|
|
576
|
+
# Replace
|
|
577
|
+
rf'{q(tag)}(?:{q(replace)}|{q(cmnt)})'
|
|
578
|
+
# Expression
|
|
579
|
+
rf'|{q(tag + expr)}(?:{q(escp)})?(?:\s*{q(cpen)}(?!\w))?'
|
|
580
|
+
# Code
|
|
581
|
+
rf'|{q(tag)}(?:\s*{q(cpen)}(?!\w))?'
|
|
582
|
+
# End
|
|
583
|
+
rf'|(?:(?<!\w){q(cpst)}\s*)?(?:{q(trim)})?{q(end)}'
|
|
584
|
+
')'
|
|
585
|
+
)
|
|
586
|
+
cpen_re = re.compile(rf'^{q(tag)}(?:{q(expr)})?(?:{q(escp)})?\s*{q(cpen)}(.*)$')
|
|
587
|
+
end_re = re.compile(rf'^(?:({q(cpst)})\s*)?({q(trim)})?{q(end)}$')
|
|
588
|
+
|
|
589
|
+
# Split lines
|
|
590
|
+
op = 'text'
|
|
591
|
+
trimming: str | None = None
|
|
592
|
+
capture = False
|
|
593
|
+
for line in split('\n', template):
|
|
594
|
+
line = line or ''
|
|
595
|
+
|
|
596
|
+
# Turn Python line into mixed line
|
|
597
|
+
if op == 'text' and (m := line_re.match(line)):
|
|
598
|
+
# Escaped start
|
|
599
|
+
if m[2]:
|
|
600
|
+
line = f'{m[1]}{start}{m[5]}'
|
|
601
|
+
|
|
602
|
+
# Comment
|
|
603
|
+
elif m[3]:
|
|
604
|
+
line = f'{tag}{m[3]} {trim}{end}'
|
|
605
|
+
|
|
606
|
+
# Expression or code
|
|
607
|
+
else:
|
|
608
|
+
line = f'{m[1]}{tag}{m[4]}{m[5]} {end}' if m[4] else f'{tag}{m[5]} {trim}{end}'
|
|
609
|
+
|
|
610
|
+
# Escaped line ending
|
|
611
|
+
if line.endswith('\\\\'):
|
|
612
|
+
line = line[:-2] + '\\\n'
|
|
613
|
+
elif line.endswith('\\'):
|
|
614
|
+
line = line[:-1]
|
|
615
|
+
else:
|
|
616
|
+
line += '\n'
|
|
617
|
+
|
|
618
|
+
# Mixed line
|
|
619
|
+
for token in split(token_re, line):
|
|
620
|
+
token = token or ''
|
|
621
|
+
|
|
622
|
+
# Capture end
|
|
623
|
+
if m := cpen_re.match(token):
|
|
624
|
+
token, capture = f'{tag}{m[1]}', True
|
|
625
|
+
|
|
626
|
+
# End
|
|
627
|
+
if op != 'text' and (m := end_re.match(token)):
|
|
628
|
+
# Capture start
|
|
629
|
+
if m[1]:
|
|
630
|
+
tree.insert(len(tree) - 1, ['cpst'])
|
|
631
|
+
|
|
632
|
+
# Trim left side
|
|
633
|
+
if (trimming := m[2]) and len(tree) > 1:
|
|
634
|
+
_trim(tree)
|
|
635
|
+
|
|
636
|
+
# Hint at end
|
|
637
|
+
op = 'text'
|
|
638
|
+
tree.append([op, ''])
|
|
639
|
+
|
|
640
|
+
# Code
|
|
641
|
+
elif token == tag:
|
|
642
|
+
op = 'code'
|
|
643
|
+
|
|
644
|
+
# Expression
|
|
645
|
+
elif token == f'{tag}{expr}':
|
|
646
|
+
op = 'expr'
|
|
647
|
+
|
|
648
|
+
# Expression that needs to be escaped
|
|
649
|
+
elif token == f'{tag}{expr}{escp}':
|
|
650
|
+
op = 'escp'
|
|
651
|
+
|
|
652
|
+
# Comment
|
|
653
|
+
elif token == f'{tag}{cmnt}':
|
|
654
|
+
op = 'cmnt'
|
|
655
|
+
|
|
656
|
+
# Text (comments are just ignored)
|
|
657
|
+
elif op != 'cmnt':
|
|
658
|
+
# Replace
|
|
659
|
+
if token == f'{tag}{replace}':
|
|
660
|
+
token = tag
|
|
661
|
+
|
|
662
|
+
# Trim right side (convert whitespace to line noise)
|
|
663
|
+
if trimming and (m := re.match(r'\s+', token)):
|
|
664
|
+
tree.append(['code', m[0]])
|
|
665
|
+
token = token[m.end() :]
|
|
666
|
+
trimming = None
|
|
667
|
+
|
|
668
|
+
# Token (with optional capture end)
|
|
669
|
+
if capture:
|
|
670
|
+
tree.append(['cpen'])
|
|
671
|
+
tree.append([op, token])
|
|
672
|
+
capture = False
|
|
673
|
+
|
|
674
|
+
# Optimize successive text lines separated by a newline
|
|
675
|
+
if (
|
|
676
|
+
(_at(tree, -4) and tree[-4][0] != 'line')
|
|
677
|
+
or (not _at(tree, -3) or tree[-3][0] != 'text' or not tree[-3][1].endswith('\n'))
|
|
678
|
+
or ((_at(tree, -2) or [''])[0] != 'line' or tree[-1][0] != 'text')
|
|
679
|
+
):
|
|
680
|
+
tree.append(['line'])
|
|
681
|
+
continue
|
|
682
|
+
tree[-3][1] += tree.pop()[1]
|
|
683
|
+
|
|
684
|
+
return self
|
|
685
|
+
|
|
686
|
+
def process(self, *args: Anything) -> Any:
|
|
687
|
+
"""::
|
|
688
|
+
|
|
689
|
+
output = mt.process()
|
|
690
|
+
output = mt.process(*args)
|
|
691
|
+
output = mt.process({'foo': 'bar'})
|
|
692
|
+
|
|
693
|
+
Process previously parsed template and return the result, or a
|
|
694
|
+
:mod:`Pyjo.Exception` object if rendering failed. ::
|
|
695
|
+
|
|
696
|
+
# Parse and process
|
|
697
|
+
print(Pyjo.Template.new().parse('Hello <%= args[0] %>').process('Bender'))
|
|
698
|
+
|
|
699
|
+
# Reuse template (for much better performance)
|
|
700
|
+
mt = Pyjo.Template.new()
|
|
701
|
+
print(mt.render('Hello <%= args[0] %>!', 'Bender'))
|
|
702
|
+
print(mt.process('Fry'))
|
|
703
|
+
print(mt.process('Leela'))
|
|
704
|
+
"""
|
|
705
|
+
|
|
706
|
+
# Use a local stack trace for compile exceptions
|
|
707
|
+
compiled = self.compiled
|
|
708
|
+
if compiled is None:
|
|
709
|
+
try:
|
|
710
|
+
compiled = self._compile()._eval(args)
|
|
711
|
+
except _CompileError as e:
|
|
712
|
+
exception = Pyjo.Exception.new(e.message).inspect(self.unparsed, '\n'.join(self._projection))
|
|
713
|
+
exception.trace().verbose = True
|
|
714
|
+
return exception
|
|
715
|
+
self.compiled = compiled
|
|
716
|
+
|
|
717
|
+
# Unlike Mojolicious: the result is markup, as a template renders its own markup
|
|
718
|
+
try:
|
|
719
|
+
return Pyjo.String.HTML.new(compiled(*args))
|
|
720
|
+
|
|
721
|
+
# An exception of Pyjoyment passes through, as Mojo passes on an object
|
|
722
|
+
except Pyjo.Exception.object as e:
|
|
723
|
+
return e
|
|
724
|
+
|
|
725
|
+
# Use a real stack trace for normal exceptions
|
|
726
|
+
except Exception as e:
|
|
727
|
+
exception = Pyjo.Exception.new(e)
|
|
728
|
+
caller = inspect.currentframe()
|
|
729
|
+
exception.frames = self._frames(e) + traceback.extract_stack(caller and caller.f_back)[::-1]
|
|
730
|
+
del caller
|
|
731
|
+
exception.inspect(self.unparsed, '\n'.join(self._projection)).verbose = True
|
|
732
|
+
return exception
|
|
733
|
+
|
|
734
|
+
def render(self, template: str, *args: Anything) -> Any:
|
|
735
|
+
"""::
|
|
736
|
+
|
|
737
|
+
output = mt.render('<%= 1 + 1 %>')
|
|
738
|
+
output = mt.render('<%= args[0] + args[1] %>', *args)
|
|
739
|
+
output = mt.render('<%= foo %>', {'foo': 'bar'})
|
|
740
|
+
|
|
741
|
+
Render template and return the result, or a :mod:`Pyjo.Exception`
|
|
742
|
+
object if rendering failed. ::
|
|
743
|
+
|
|
744
|
+
# Longer version
|
|
745
|
+
output = mt.parse('<%= 1 + 1 %>').process()
|
|
746
|
+
|
|
747
|
+
# Render with arguments
|
|
748
|
+
print(Pyjo.Template.new().render('<%= args[0] %>', 'bar'))
|
|
749
|
+
|
|
750
|
+
# Render with named variables
|
|
751
|
+
print(Pyjo.Template.new(vars=True).render('<%= foo %>', {'foo': 'bar'}))
|
|
752
|
+
"""
|
|
753
|
+
return self.parse(template).process(*args)
|
|
754
|
+
|
|
755
|
+
def render_file(self, path: Anything, *args: Anything) -> Any:
|
|
756
|
+
"""::
|
|
757
|
+
|
|
758
|
+
output = mt.render_file('/tmp/foo.ep')
|
|
759
|
+
output = mt.render_file('/tmp/foo.ep', *args)
|
|
760
|
+
output = mt.render_file('/tmp/bar.ep', {'foo': 'bar'})
|
|
761
|
+
|
|
762
|
+
Same as :meth:`render`, but renders a template file.
|
|
763
|
+
"""
|
|
764
|
+
if self._name is None:
|
|
765
|
+
self.name = str(path)
|
|
766
|
+
data = Pyjo.File.path(path).slurp()
|
|
767
|
+
assert isinstance(data, bytes)
|
|
768
|
+
if (encoding := self.encoding) is None:
|
|
769
|
+
template = data.decode('latin-1')
|
|
770
|
+
elif (template := decode(encoding, data)) is None:
|
|
771
|
+
raise Pyjo.Exception.new(f'Template "{path}" has invalid encoding')
|
|
772
|
+
|
|
773
|
+
return self.render(template, *args)
|
|
774
|
+
|
|
775
|
+
# Unlike Mojolicious: Python needs the indentation of its blocks, so the code is not line for line the template,
|
|
776
|
+
# and the template lines of the code are kept for the tracebacks
|
|
777
|
+
def _compile(self) -> Self:
|
|
778
|
+
compiler = _Compiler(self.auto_escape)
|
|
779
|
+
try:
|
|
780
|
+
compiler.compile(self.tree)
|
|
781
|
+
finally:
|
|
782
|
+
self._projection = compiler.projection
|
|
783
|
+
self._body = [line for _, line in compiler.out]
|
|
784
|
+
self.code = '\n'.join(text for text, _ in compiler.out)
|
|
785
|
+
self.tree = []
|
|
786
|
+
return self
|
|
787
|
+
|
|
788
|
+
def _eval(self, args: tuple[Anything, ...]) -> Callable[..., Any]:
|
|
789
|
+
name = self._filename()
|
|
790
|
+
|
|
791
|
+
# Variables
|
|
792
|
+
variables: list[str] = []
|
|
793
|
+
if self.vars and len(args) and isinstance(args[0], dict):
|
|
794
|
+
keys = cast(dict[Anything, Anything], args[0]).keys()
|
|
795
|
+
names = [k for k in keys if isinstance(k, str) and k.isidentifier() and not keyword.iskeyword(k)]
|
|
796
|
+
if len(names):
|
|
797
|
+
variables = [f'{k} = args[0].get({k!r})' for k in names] + ['args = args[1:]']
|
|
798
|
+
|
|
799
|
+
# Wrap lines, the code of prepend and append as lines of their own, and the output as a list, which the markup
|
|
800
|
+
# of Pyjo.String.HTML does not turn into markup as a string would
|
|
801
|
+
head = [
|
|
802
|
+
'def _wrap(_escape, _str, _html):',
|
|
803
|
+
' def _template(*args):',
|
|
804
|
+
' _O = []',
|
|
805
|
+
*(f' {line}' for line in [*self.prepend.split('\n'), *variables] if line.strip()),
|
|
806
|
+
]
|
|
807
|
+
tail = [
|
|
808
|
+
*(f' {line}' for line in self.append.split('\n') if line.strip()),
|
|
809
|
+
" return ''.join(_O)",
|
|
810
|
+
' return _template',
|
|
811
|
+
]
|
|
812
|
+
body = self.code.split('\n') if len(self._body) else []
|
|
813
|
+
self._lines = [1] * len(head) + self._body + [max(self._body, default=1) + 1] * len(tail)
|
|
814
|
+
code = '\n'.join([*head, *body, *tail])
|
|
815
|
+
|
|
816
|
+
if DEBUG:
|
|
817
|
+
sys.stderr.write(f'-- Code for {name}\n{code}\n\n')
|
|
818
|
+
|
|
819
|
+
try:
|
|
820
|
+
compiled = compile(code, name, 'exec')
|
|
821
|
+
except SyntaxError as e:
|
|
822
|
+
raise _CompileError(f'{e.msg} at {name} line {self._line(e.lineno)}.\n') from None
|
|
823
|
+
namespace: dict[str, Any] = {}
|
|
824
|
+
exec(compiled, _sandbox(self.namespace), namespace)
|
|
825
|
+
self._codes = set(_code_objects(compiled))
|
|
826
|
+
return namespace['_wrap'](self.escape, Pyjo.String.Template.raw, Pyjo.String.HTML.new)
|
|
827
|
+
|
|
828
|
+
def _filename(self) -> str:
|
|
829
|
+
return self.name.replace('"', '')
|
|
830
|
+
|
|
831
|
+
def _frames(self, e: BaseException) -> list[traceback.FrameSummary]:
|
|
832
|
+
frames: list[traceback.FrameSummary] = []
|
|
833
|
+
lines = _split(self.unparsed)
|
|
834
|
+
for frame, lineno in traceback.walk_tb(e.__traceback__):
|
|
835
|
+
code = frame.f_code
|
|
836
|
+
if code in self._codes:
|
|
837
|
+
num = self._line(lineno)
|
|
838
|
+
line = lines[num - 1] if 0 < num <= len(lines) else ''
|
|
839
|
+
frames.append(traceback.FrameSummary(self._filename(), num, code.co_name, lookup_line=False, line=line))
|
|
840
|
+
else:
|
|
841
|
+
frames.append(traceback.FrameSummary(code.co_filename, lineno, code.co_name))
|
|
842
|
+
return frames[::-1]
|
|
843
|
+
|
|
844
|
+
def _line(self, lineno: int | None) -> int:
|
|
845
|
+
if lineno is None or not len(self._lines):
|
|
846
|
+
return 1
|
|
847
|
+
return self._lines[min(max(lineno, 1), len(self._lines)) - 1]
|
|
848
|
+
|
|
849
|
+
|
|
850
|
+
# A block of the compiled code: a capture of begin and end, or a block of Python
|
|
851
|
+
@dataclass
|
|
852
|
+
class _Block:
|
|
853
|
+
keyword: str
|
|
854
|
+
line: int
|
|
855
|
+
head: tuple[str, str] | None = None
|
|
856
|
+
count: int = 0
|
|
857
|
+
|
|
858
|
+
|
|
859
|
+
class _CompileError(Exception):
|
|
860
|
+
def __init__(self, message: str) -> None:
|
|
861
|
+
super().__init__(message)
|
|
862
|
+
self.message = message
|
|
863
|
+
|
|
864
|
+
|
|
865
|
+
# A logical line of Python code, as the template has it
|
|
866
|
+
@dataclass
|
|
867
|
+
class _Logical:
|
|
868
|
+
text: str
|
|
869
|
+
start: int
|
|
870
|
+
brace_open: int = -1
|
|
871
|
+
brace_close: int = -1
|
|
872
|
+
colon: bool = False
|
|
873
|
+
|
|
874
|
+
|
|
875
|
+
class _Compiler:
|
|
876
|
+
def __init__(self, escape: bool | None) -> None:
|
|
877
|
+
self.escape = escape
|
|
878
|
+
self.out: list[tuple[str, int]] = []
|
|
879
|
+
self.projection: list[str] = []
|
|
880
|
+
self.blocks: list[_Block] = []
|
|
881
|
+
self.depth = 2
|
|
882
|
+
self.line = 1
|
|
883
|
+
self.closed = False
|
|
884
|
+
|
|
885
|
+
# Code lines of the template that are not compiled yet, as lines with their numbers
|
|
886
|
+
self.buffer: list[tuple[int, str]] = []
|
|
887
|
+
|
|
888
|
+
# An expression of several lines
|
|
889
|
+
self.expr: list[tuple[int, str]] | None = None
|
|
890
|
+
self.expr_op = ''
|
|
891
|
+
|
|
892
|
+
# The statement that continues after the end of a capture
|
|
893
|
+
self.continuation: tuple[str, str] | None = None
|
|
894
|
+
self.captures = 0
|
|
895
|
+
|
|
896
|
+
def compile(self, tree: list[list[str]]) -> None:
|
|
897
|
+
capture = multi = False
|
|
898
|
+
i = 0
|
|
899
|
+
while (i := i + 1) <= len(tree) and (following := _at(tree, i)) is not None:
|
|
900
|
+
op, value = (tree[i - 1] + [''])[:2]
|
|
901
|
+
if op == 'line':
|
|
902
|
+
self.line += 1
|
|
903
|
+
if len(self.buffer):
|
|
904
|
+
self.buffer.append((self.line, ''))
|
|
905
|
+
if self.expr is not None:
|
|
906
|
+
self.expr.append((self.line, ''))
|
|
907
|
+
continue
|
|
908
|
+
newline = value.endswith('\n')
|
|
909
|
+
if newline:
|
|
910
|
+
value = value[:-1]
|
|
911
|
+
|
|
912
|
+
# Capture start before a token that is not code or an expression
|
|
913
|
+
if capture and op not in ('code', 'expr', 'escp', 'cpst'):
|
|
914
|
+
self.begin('code', '')
|
|
915
|
+
capture = False
|
|
916
|
+
|
|
917
|
+
# Text (quote and fix line ending), and the code before it unless it is the hint at the end of a tag
|
|
918
|
+
if op == 'text':
|
|
919
|
+
if value or newline:
|
|
920
|
+
self.flush()
|
|
921
|
+
self.text(value, newline)
|
|
922
|
+
|
|
923
|
+
# Multi-line expression
|
|
924
|
+
elif multi and op in ('code', 'expr', 'escp'):
|
|
925
|
+
assert self.expr is not None
|
|
926
|
+
self._project(value)
|
|
927
|
+
self.expr[-1] = (self.expr[-1][0], self.expr[-1][1] + value)
|
|
928
|
+
|
|
929
|
+
# Code
|
|
930
|
+
elif op == 'code':
|
|
931
|
+
self._project(value)
|
|
932
|
+
if capture:
|
|
933
|
+
self.begin('code', value)
|
|
934
|
+
capture = False
|
|
935
|
+
elif self.continuation is not None:
|
|
936
|
+
kind, head = self.continuation
|
|
937
|
+
self.continuation = None
|
|
938
|
+
if following[0] == 'cpst':
|
|
939
|
+
self.continuation = (kind, head + value)
|
|
940
|
+
elif kind == 'code':
|
|
941
|
+
self.code(head + value)
|
|
942
|
+
else:
|
|
943
|
+
self.expression(kind, [(self.line, head + value)])
|
|
944
|
+
else:
|
|
945
|
+
self.code(value)
|
|
946
|
+
|
|
947
|
+
# Capture end
|
|
948
|
+
elif op == 'cpen':
|
|
949
|
+
self.flush()
|
|
950
|
+
self.end()
|
|
951
|
+
|
|
952
|
+
# Expression
|
|
953
|
+
if op in ('expr', 'escp') and not multi:
|
|
954
|
+
self.flush()
|
|
955
|
+
self._project(value)
|
|
956
|
+
escaped = op == 'escp' and not self.escape or op == 'expr' and self.escape
|
|
957
|
+
kind = 'escp' if escaped else 'expr'
|
|
958
|
+
if capture:
|
|
959
|
+
self.begin(kind, value)
|
|
960
|
+
capture = False
|
|
961
|
+
else:
|
|
962
|
+
self.expr, self.expr_op = [(self.line, value)], kind
|
|
963
|
+
if op in ('expr', 'escp', 'code') and self.expr is not None:
|
|
964
|
+
# Multi-line
|
|
965
|
+
multi = following[0] != 'text'
|
|
966
|
+
if not multi:
|
|
967
|
+
self.expression(self.expr_op, self.expr)
|
|
968
|
+
self.expr = None
|
|
969
|
+
|
|
970
|
+
# Capture start
|
|
971
|
+
if op == 'cpst':
|
|
972
|
+
capture = True
|
|
973
|
+
|
|
974
|
+
self.flush()
|
|
975
|
+
if len(self.blocks):
|
|
976
|
+
block = self.blocks[-1]
|
|
977
|
+
what = 'begin' if block.keyword == 'begin' else f'block "{block.keyword}"'
|
|
978
|
+
raise _CompileError(f'Missing end of {what} from line {block.line} at template line {self.line}.\n')
|
|
979
|
+
|
|
980
|
+
def begin(self, kind: str, value: str) -> None:
|
|
981
|
+
if self.continuation is not None:
|
|
982
|
+
kind, head = self.continuation
|
|
983
|
+
self.continuation = None
|
|
984
|
+
else:
|
|
985
|
+
head = self.flush(final=False)
|
|
986
|
+
self.captures += 1
|
|
987
|
+
name = f'_begin{self.captures}'
|
|
988
|
+
self.emit(f'def {name}(*args):')
|
|
989
|
+
self.open(_Block('begin', self.line, head=(kind, head + value + name)))
|
|
990
|
+
self.emit('_O = []')
|
|
991
|
+
|
|
992
|
+
def close(self) -> _Block:
|
|
993
|
+
block = self.blocks.pop()
|
|
994
|
+
if not block.count:
|
|
995
|
+
self.emit('pass', self.depth)
|
|
996
|
+
self.depth -= 1
|
|
997
|
+
return block
|
|
998
|
+
|
|
999
|
+
def code(self, value: str) -> None:
|
|
1000
|
+
if not value.strip():
|
|
1001
|
+
return
|
|
1002
|
+
if not len(self.buffer):
|
|
1003
|
+
self.buffer.append((self.line, ''))
|
|
1004
|
+
first, *rest = value.split('\n')
|
|
1005
|
+
num, text = self.buffer[-1]
|
|
1006
|
+
self.buffer[-1] = (num, text + first)
|
|
1007
|
+
self.buffer.extend((self.line, line) for line in rest)
|
|
1008
|
+
|
|
1009
|
+
def emit(self, text: str, depth: int | None = None, line: int | None = None) -> None:
|
|
1010
|
+
self.out.append(
|
|
1011
|
+
(' ' * 4 * (self.depth if depth is None else depth) + text, self.line if line is None else line)
|
|
1012
|
+
)
|
|
1013
|
+
if len(self.blocks):
|
|
1014
|
+
self.blocks[-1].count += 1
|
|
1015
|
+
self.closed = False
|
|
1016
|
+
|
|
1017
|
+
def end(self) -> None:
|
|
1018
|
+
if not len(self.blocks):
|
|
1019
|
+
raise _CompileError(f'Unexpected end at template line {self.line}.\n')
|
|
1020
|
+
if self.blocks[-1].keyword == 'begin':
|
|
1021
|
+
self.emit("return _html(''.join(_O))")
|
|
1022
|
+
block = self.close()
|
|
1023
|
+
if block.keyword == 'begin':
|
|
1024
|
+
self.continuation = block.head
|
|
1025
|
+
elif block.keyword == 'case' and len(self.blocks) and self.blocks[-1].keyword == 'match':
|
|
1026
|
+
self.close()
|
|
1027
|
+
self.closed = True
|
|
1028
|
+
|
|
1029
|
+
def expression(self, kind: str, parts: list[tuple[int, str]]) -> None:
|
|
1030
|
+
call = '_escape' if kind == 'escp' else '_str'
|
|
1031
|
+
(first_line, first), *rest = parts
|
|
1032
|
+
if not len(rest):
|
|
1033
|
+
self.emit(f'_O.append({call}({first.strip()}))', line=first_line)
|
|
1034
|
+
return
|
|
1035
|
+
self.emit(f'_O.append({call}(({first}', line=first_line)
|
|
1036
|
+
for num, part in rest:
|
|
1037
|
+
self.out.append((part, num))
|
|
1038
|
+
self.out.append((')))', rest[-1][0]))
|
|
1039
|
+
|
|
1040
|
+
def flush(self, final: bool = True) -> str:
|
|
1041
|
+
if not len(self.buffer):
|
|
1042
|
+
return ''
|
|
1043
|
+
logicals, tail = _scan('\n'.join(text for _, text in self.buffer), final)
|
|
1044
|
+
starts = [num for num, _ in self.buffer]
|
|
1045
|
+
self.buffer = []
|
|
1046
|
+
for logical in logicals:
|
|
1047
|
+
self.statement(logical, starts)
|
|
1048
|
+
return tail
|
|
1049
|
+
|
|
1050
|
+
def open(self, block: _Block) -> None:
|
|
1051
|
+
self.blocks.append(block)
|
|
1052
|
+
self.depth += 1
|
|
1053
|
+
|
|
1054
|
+
def statement(self, logical: _Logical, starts: list[int]) -> None:
|
|
1055
|
+
text, line = logical.text, starts[logical.start]
|
|
1056
|
+
|
|
1057
|
+
# Brace that opens a block
|
|
1058
|
+
brace = logical.brace_open >= 0
|
|
1059
|
+
if brace:
|
|
1060
|
+
text = text[: logical.brace_open].rstrip() + ':' + text[logical.brace_open + 1 :]
|
|
1061
|
+
|
|
1062
|
+
# Brace that closes a block
|
|
1063
|
+
if logical.brace_close >= 0:
|
|
1064
|
+
if not len(self.blocks) or self.blocks[-1].keyword == 'begin':
|
|
1065
|
+
raise _CompileError(f'Unexpected "}}" at template line {line}.\n')
|
|
1066
|
+
self.close()
|
|
1067
|
+
self.closed = True
|
|
1068
|
+
text = text[logical.brace_close + 1 :]
|
|
1069
|
+
if not text.strip() or text.lstrip().startswith('#'):
|
|
1070
|
+
return
|
|
1071
|
+
|
|
1072
|
+
# Keyword that continues the block before
|
|
1073
|
+
word = m[1] if (m := RE_KEYWORD.match(text)) else ''
|
|
1074
|
+
if word in CONTINUE_KEYWORDS and not self.closed and len(self.blocks) and self.blocks[-1].keyword != 'begin':
|
|
1075
|
+
if not (word == 'case' and self.blocks[-1].keyword == 'match'):
|
|
1076
|
+
self.close()
|
|
1077
|
+
|
|
1078
|
+
first, *rest = text.lstrip().split('\n')
|
|
1079
|
+
self.emit(first if len(rest) else first.rstrip(), line=line)
|
|
1080
|
+
for i, continued in enumerate(rest, 1):
|
|
1081
|
+
self.out.append((continued, starts[logical.start + i]))
|
|
1082
|
+
if brace or logical.colon:
|
|
1083
|
+
self.open(_Block(word, line))
|
|
1084
|
+
|
|
1085
|
+
def text(self, value: str, newline: bool) -> None:
|
|
1086
|
+
pieces = value.split('\n')
|
|
1087
|
+
if newline:
|
|
1088
|
+
pieces[-1] += '\n'
|
|
1089
|
+
pieces = [p + '\n' for p in pieces[:-1]] + [pieces[-1]]
|
|
1090
|
+
if len(pieces) == 1:
|
|
1091
|
+
if pieces[0]:
|
|
1092
|
+
self.emit(f'_O.append({pieces[0]!r})')
|
|
1093
|
+
self._project(f'_O.append({pieces[0]!r})')
|
|
1094
|
+
return
|
|
1095
|
+
self.emit(f'_O.append({pieces[0]!r}')
|
|
1096
|
+
self._project(f'_O.append({pieces[0]!r}')
|
|
1097
|
+
for piece in pieces[1:-1]:
|
|
1098
|
+
self.line += 1
|
|
1099
|
+
self.out.append((repr(piece), self.line))
|
|
1100
|
+
self._project(repr(piece))
|
|
1101
|
+
self.line += 1
|
|
1102
|
+
self.out.append((f'{pieces[-1]!r})', self.line))
|
|
1103
|
+
self._project(f'{pieces[-1]!r})')
|
|
1104
|
+
|
|
1105
|
+
def _project(self, fragment: str) -> None:
|
|
1106
|
+
while len(self.projection) < self.line:
|
|
1107
|
+
self.projection.append('')
|
|
1108
|
+
self.projection[self.line - 1] += fragment
|
|
1109
|
+
|
|
1110
|
+
|
|
1111
|
+
def _at(tree: list[list[str]], i: int) -> list[str] | None:
|
|
1112
|
+
return tree[i] if -len(tree) <= i < len(tree) else None
|
|
1113
|
+
|
|
1114
|
+
|
|
1115
|
+
def _code_objects(code: types.CodeType) -> Iterator[types.CodeType]:
|
|
1116
|
+
yield code
|
|
1117
|
+
for const in code.co_consts:
|
|
1118
|
+
if isinstance(const, types.CodeType):
|
|
1119
|
+
yield from _code_objects(const)
|
|
1120
|
+
|
|
1121
|
+
|
|
1122
|
+
def _sandbox(name: str) -> dict[str, Any]:
|
|
1123
|
+
if (module := sys.modules.get(name)) is None and (module := _SANDBOXES.get(name)) is None:
|
|
1124
|
+
module = _SANDBOXES[name] = types.ModuleType(name)
|
|
1125
|
+
|
|
1126
|
+
# No spec, as the module has no source that linecache could read, which warns of a spec without a loader
|
|
1127
|
+
del module.__spec__
|
|
1128
|
+
return module.__dict__
|
|
1129
|
+
|
|
1130
|
+
|
|
1131
|
+
# Logical lines of Python code, the brace and the colon that open a block, the brace that closes one, and the end of
|
|
1132
|
+
# the code that is not a complete logical line yet
|
|
1133
|
+
def _scan(code: str, final: bool) -> tuple[list[_Logical], str]:
|
|
1134
|
+
logicals: list[_Logical] = []
|
|
1135
|
+
i = start = line = first_line = 0
|
|
1136
|
+
n = len(code)
|
|
1137
|
+
depth = 0
|
|
1138
|
+
brace = close = last = -1
|
|
1139
|
+
first = -1
|
|
1140
|
+
|
|
1141
|
+
def finish(end: int) -> None:
|
|
1142
|
+
nonlocal start, depth, brace, close, last, first, first_line
|
|
1143
|
+
text = code[start:end]
|
|
1144
|
+
if text.strip():
|
|
1145
|
+
lead = len(text) - len(text.lstrip())
|
|
1146
|
+
begin = start + lead
|
|
1147
|
+
logicals.append(
|
|
1148
|
+
_Logical(
|
|
1149
|
+
text[lead:],
|
|
1150
|
+
first_line + text.count('\n', 0, lead),
|
|
1151
|
+
brace - begin if brace >= 0 and depth == 1 else -1,
|
|
1152
|
+
close - begin if close >= 0 else -1,
|
|
1153
|
+
depth == 0 and last >= 0 and code[last] == ':',
|
|
1154
|
+
)
|
|
1155
|
+
)
|
|
1156
|
+
start = end + 1
|
|
1157
|
+
depth = 0
|
|
1158
|
+
brace = close = last = first = -1
|
|
1159
|
+
first_line = line + 1
|
|
1160
|
+
|
|
1161
|
+
while i < n:
|
|
1162
|
+
c = code[i]
|
|
1163
|
+
if c == '#':
|
|
1164
|
+
while i < n and code[i] != '\n':
|
|
1165
|
+
i += 1
|
|
1166
|
+
continue
|
|
1167
|
+
if c == '\\' and i + 1 < n and code[i + 1] == '\n':
|
|
1168
|
+
i += 2
|
|
1169
|
+
line += 1
|
|
1170
|
+
continue
|
|
1171
|
+
if c in '"\'':
|
|
1172
|
+
m = RE_STRING_PREFIX.search(code, start, i)
|
|
1173
|
+
end = _skip_string(code, i, m[1] if m else '')
|
|
1174
|
+
if end < 0:
|
|
1175
|
+
break
|
|
1176
|
+
line += code.count('\n', i, end)
|
|
1177
|
+
i = end
|
|
1178
|
+
last = i - 1
|
|
1179
|
+
if first < 0:
|
|
1180
|
+
first = i
|
|
1181
|
+
continue
|
|
1182
|
+
if c == '\n':
|
|
1183
|
+
if depth == 0 or (depth == 1 and brace >= 0 and last == brace and _block_keyword(code[start:i])):
|
|
1184
|
+
finish(i)
|
|
1185
|
+
line += 1
|
|
1186
|
+
i += 1
|
|
1187
|
+
continue
|
|
1188
|
+
if c in ' \t\r\f':
|
|
1189
|
+
i += 1
|
|
1190
|
+
continue
|
|
1191
|
+
if c in '([{':
|
|
1192
|
+
if c == '{' and depth == 0:
|
|
1193
|
+
brace = i
|
|
1194
|
+
depth += 1
|
|
1195
|
+
elif c in ')]}':
|
|
1196
|
+
if depth == 0 and c == '}' and first < 0 and close < 0:
|
|
1197
|
+
close = i
|
|
1198
|
+
i += 1
|
|
1199
|
+
continue
|
|
1200
|
+
depth = max(depth - 1, 0)
|
|
1201
|
+
if depth == 0:
|
|
1202
|
+
brace = -1
|
|
1203
|
+
last = i
|
|
1204
|
+
if first < 0:
|
|
1205
|
+
first = i
|
|
1206
|
+
i += 1
|
|
1207
|
+
|
|
1208
|
+
if final and start < n:
|
|
1209
|
+
if not (depth == 1 and brace >= 0 and last == brace and _block_keyword(code[start:n])):
|
|
1210
|
+
brace = -1
|
|
1211
|
+
finish(n)
|
|
1212
|
+
return logicals, code[start:] if start < n else ''
|
|
1213
|
+
|
|
1214
|
+
|
|
1215
|
+
def _block_keyword(text: str) -> bool:
|
|
1216
|
+
return bool(m := RE_KEYWORD.match(text)) and m[1] in BLOCK_KEYWORDS
|
|
1217
|
+
|
|
1218
|
+
|
|
1219
|
+
# The end of a string literal, or -1 for a string that is not terminated
|
|
1220
|
+
def _skip_string(code: str, i: int, prefix: str) -> int:
|
|
1221
|
+
quote = code[i : i + 3] if code[i : i + 3] in ('"""', "'''") else code[i]
|
|
1222
|
+
formatted = any(p in prefix.lower() for p in 'ft')
|
|
1223
|
+
i += len(quote)
|
|
1224
|
+
n = len(code)
|
|
1225
|
+
while i < n:
|
|
1226
|
+
if code.startswith(quote, i):
|
|
1227
|
+
return i + len(quote)
|
|
1228
|
+
c = code[i]
|
|
1229
|
+
if c == '\\':
|
|
1230
|
+
i += 2
|
|
1231
|
+
continue
|
|
1232
|
+
if c == '\n' and len(quote) == 1:
|
|
1233
|
+
return -1
|
|
1234
|
+
if formatted and c == '{':
|
|
1235
|
+
if code.startswith('{{', i):
|
|
1236
|
+
i += 2
|
|
1237
|
+
continue
|
|
1238
|
+
if (i := _skip_field(code, i + 1)) < 0:
|
|
1239
|
+
return -1
|
|
1240
|
+
continue
|
|
1241
|
+
i += 1
|
|
1242
|
+
return -1
|
|
1243
|
+
|
|
1244
|
+
|
|
1245
|
+
# The end of a replacement field of an f-string or a t-string
|
|
1246
|
+
def _skip_field(code: str, i: int) -> int:
|
|
1247
|
+
depth = 0
|
|
1248
|
+
n = len(code)
|
|
1249
|
+
while i < n:
|
|
1250
|
+
c = code[i]
|
|
1251
|
+
if c in '"\'':
|
|
1252
|
+
m = RE_STRING_PREFIX.search(code, 0, i)
|
|
1253
|
+
if (i := _skip_string(code, i, m[1] if m else '')) < 0:
|
|
1254
|
+
return -1
|
|
1255
|
+
continue
|
|
1256
|
+
if c in '([{':
|
|
1257
|
+
depth += 1
|
|
1258
|
+
elif c in ')]}':
|
|
1259
|
+
if depth == 0 and c == '}':
|
|
1260
|
+
return i + 1
|
|
1261
|
+
depth -= 1
|
|
1262
|
+
i += 1
|
|
1263
|
+
return -1
|
|
1264
|
+
|
|
1265
|
+
|
|
1266
|
+
def _split(source: str) -> list[str]:
|
|
1267
|
+
lines = source.split('\n')
|
|
1268
|
+
while len(lines) and lines[-1] == '':
|
|
1269
|
+
lines.pop()
|
|
1270
|
+
return lines
|
|
1271
|
+
|
|
1272
|
+
|
|
1273
|
+
def _trim(tree: list[list[str]]) -> None:
|
|
1274
|
+
|
|
1275
|
+
# Skip captures
|
|
1276
|
+
i = -3 if tree[-2][0] in ('cpst', 'cpen') else -2
|
|
1277
|
+
|
|
1278
|
+
# Only trim text
|
|
1279
|
+
if (node := _at(tree, i)) is None or node[0] != 'text':
|
|
1280
|
+
return
|
|
1281
|
+
|
|
1282
|
+
# Convert whitespace text to line noise
|
|
1283
|
+
if m := re.search(r'\s+\Z', node[1]):
|
|
1284
|
+
node[1] = node[1][: m.start()]
|
|
1285
|
+
tree.insert(len(tree) + i, ['code', m[0]])
|
|
1286
|
+
|
|
1287
|
+
|
|
1288
|
+
new = Pyjo_Template.new
|
|
1289
|
+
object = Pyjo_Template
|
|
1290
|
+
Template = Pyjo_Template
|