python-liquid 2.0.2__tar.gz → 2.2.0__tar.gz
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.
- {python_liquid-2.0.2 → python_liquid-2.2.0}/PKG-INFO +4 -2
- {python_liquid-2.0.2 → python_liquid-2.2.0}/README.md +1 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/__init__.py +1 -1
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/ast.py +17 -4
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/builtin/__init__.py +4 -0
- python_liquid-2.2.0/liquid/builtin/filters/extra.py +83 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/builtin/filters/string.py +45 -4
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/builtin/loaders/caching_file_system_loader.py +4 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/builtin/loaders/file_system_loader.py +21 -4
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/builtin/tags/case_tag.py +3 -3
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/builtin/tags/comment_tag.py +2 -2
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/builtin/tags/doc_tag.py +1 -1
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/builtin/tags/if_tag.py +1 -1
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/builtin/tags/render_tag.py +85 -28
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/exceptions.py +3 -2
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/extra/__init__.py +2 -1
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/extra/filters/babel.py +1 -1
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/extra/tags/__init__.py +2 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/extra/tags/extends_tag.py +1 -2
- python_liquid-2.2.0/liquid/extra/tags/snippet_tag.py +107 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/filter.py +4 -1
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/static_analysis.py +105 -32
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/template.py +61 -6
- {python_liquid-2.0.2 → python_liquid-2.2.0}/pyproject.toml +5 -4
- python_liquid-2.0.2/liquid/builtin/filters/extra.py +0 -21
- {python_liquid-2.0.2 → python_liquid-2.2.0}/.gitignore +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/LICENSE +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/analyze_tags.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/builtin/content.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/builtin/drops/__init__.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/builtin/drops/drops.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/builtin/expressions/__init__.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/builtin/expressions/_tokenize.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/builtin/expressions/arguments.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/builtin/expressions/filtered.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/builtin/expressions/logical.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/builtin/expressions/loop.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/builtin/expressions/path.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/builtin/expressions/primitive.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/builtin/filters/__init__.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/builtin/filters/array.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/builtin/filters/math.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/builtin/filters/misc.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/builtin/illegal.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/builtin/loaders/__init__.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/builtin/loaders/choice_loader.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/builtin/loaders/dict_loader.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/builtin/loaders/mixins.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/builtin/loaders/package_loader.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/builtin/output.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/builtin/tags/__init__.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/builtin/tags/assign_tag.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/builtin/tags/capture_tag.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/builtin/tags/cycle_tag.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/builtin/tags/decrement_tag.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/builtin/tags/echo_tag.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/builtin/tags/for_tag.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/builtin/tags/ifchanged_tag.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/builtin/tags/include_tag.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/builtin/tags/increment_tag.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/builtin/tags/inline_comment_tag.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/builtin/tags/liquid_tag.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/builtin/tags/tablerow_tag.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/builtin/tags/unless_tag.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/context.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/environment.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/expression.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/extra/filters/__init__.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/extra/filters/_json.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/extra/filters/array.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/extra/filters/html.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/extra/filters/translate.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/extra/tags/_with.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/extra/tags/macro_tag.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/extra/tags/translate_tag.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/future/__init__.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/future/environment.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/future/filters/__init__.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/future/tags/__init__.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/lex.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/limits.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/loader.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/messages.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/mode.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/output.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/parser.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/py.typed +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/span.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/stream.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/stringify.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/tag.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/token.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/undefined.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/utils/__init__.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/utils/chain_map.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/utils/html.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/utils/lru_cache.py +0 -0
- {python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/utils/text.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-liquid
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: A Python engine for the Liquid template language.
|
|
5
5
|
Project-URL: Change Log, https://github.com/jg-rp/liquid/blob/main/CHANGES.md
|
|
6
6
|
Project-URL: Documentation, https://jg-rp.github.io/liquid/
|
|
@@ -19,9 +19,10 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.11
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.12
|
|
21
21
|
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
23
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
23
24
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
24
|
-
Requires-Python: >=3.
|
|
25
|
+
Requires-Python: >=3.9
|
|
25
26
|
Requires-Dist: babel>=2
|
|
26
27
|
Requires-Dist: markupsafe>=3
|
|
27
28
|
Requires-Dist: python-dateutil>=2.8.1
|
|
@@ -105,6 +106,7 @@ $ conda install -c conda-forge python-liquid
|
|
|
105
106
|
|
|
106
107
|
- [Python Liquid2](https://github.com/jg-rp/python-liquid2): A new Python engine for Liquid with [extra features](https://jg-rp.github.io/python-liquid2/migration/#new-features).
|
|
107
108
|
- [Ruby Liquid2](https://github.com/jg-rp/ruby-liquid2): Liquid2 templates for Ruby.
|
|
109
|
+
- [Micro Liquid](https://github.com/jg-rp/micro-liquid): A stripped-down Liquid-like template engine for Python. You can think of it as a non-evaluating alternative to Python's [f-strings](https://docs.python.org/3/tutorial/inputoutput.html#formatted-string-literals) or [t-strings](https://docs.python.org/3.14/whatsnew/3.14.html#whatsnew314-pep750).
|
|
108
110
|
- [LiquidScript](https://github.com/jg-rp/liquidscript): A JavaScript engine for Liquid with a similar high-level API to Python Liquid.
|
|
109
111
|
|
|
110
112
|
## Quick Start
|
|
@@ -75,6 +75,7 @@ $ conda install -c conda-forge python-liquid
|
|
|
75
75
|
|
|
76
76
|
- [Python Liquid2](https://github.com/jg-rp/python-liquid2): A new Python engine for Liquid with [extra features](https://jg-rp.github.io/python-liquid2/migration/#new-features).
|
|
77
77
|
- [Ruby Liquid2](https://github.com/jg-rp/ruby-liquid2): Liquid2 templates for Ruby.
|
|
78
|
+
- [Micro Liquid](https://github.com/jg-rp/micro-liquid): A stripped-down Liquid-like template engine for Python. You can think of it as a non-evaluating alternative to Python's [f-strings](https://docs.python.org/3/tutorial/inputoutput.html#formatted-string-literals) or [t-strings](https://docs.python.org/3.14/whatsnew/3.14.html#whatsnew314-pep750).
|
|
78
79
|
- [LiquidScript](https://github.com/jg-rp/liquidscript): A JavaScript engine for Liquid with a similar high-level API to Python Liquid.
|
|
79
80
|
|
|
80
81
|
## Quick Start
|
|
@@ -9,7 +9,9 @@ from enum import auto
|
|
|
9
9
|
from typing import TYPE_CHECKING
|
|
10
10
|
from typing import Collection
|
|
11
11
|
from typing import Iterable
|
|
12
|
+
from typing import Optional
|
|
12
13
|
from typing import TextIO
|
|
14
|
+
from typing import Union
|
|
13
15
|
|
|
14
16
|
from .exceptions import DisabledTagError
|
|
15
17
|
from .output import NullIO
|
|
@@ -104,7 +106,7 @@ class Node(ABC):
|
|
|
104
106
|
"""Return variables this node adds to the node's block scope."""
|
|
105
107
|
return []
|
|
106
108
|
|
|
107
|
-
def partial_scope(self) -> Partial
|
|
109
|
+
def partial_scope(self) -> Optional[Partial]:
|
|
108
110
|
"""Return information about a partial template loaded by this node."""
|
|
109
111
|
return None
|
|
110
112
|
|
|
@@ -121,19 +123,30 @@ class Partial:
|
|
|
121
123
|
"""Partial template meta data.
|
|
122
124
|
|
|
123
125
|
Args:
|
|
124
|
-
name:
|
|
126
|
+
name: The name of the partial or an expression resolving to the name
|
|
127
|
+
associated with the partial template.
|
|
125
128
|
scope: The kind of scope the partial template should have when loaded.
|
|
126
129
|
in_scope: Names that will be added to the partial template scope.
|
|
130
|
+
key: A hash of the partial template name and any arguments the partial
|
|
131
|
+
template will be rendered with that might affect its scope. If a
|
|
132
|
+
key is provided, static analysis helpers will visit a partial
|
|
133
|
+
template once for each distinct key.
|
|
127
134
|
"""
|
|
128
135
|
|
|
129
|
-
__slots__ = ("name", "scope", "in_scope")
|
|
136
|
+
__slots__ = ("name", "scope", "in_scope", "key")
|
|
130
137
|
|
|
131
138
|
def __init__(
|
|
132
|
-
self,
|
|
139
|
+
self,
|
|
140
|
+
name: Union[Expression, str],
|
|
141
|
+
scope: PartialScope,
|
|
142
|
+
in_scope: Iterable[Identifier],
|
|
143
|
+
*,
|
|
144
|
+
key: Optional[int] = None,
|
|
133
145
|
) -> None:
|
|
134
146
|
self.name = name
|
|
135
147
|
self.scope = scope
|
|
136
148
|
self.in_scope = in_scope
|
|
149
|
+
self.key = key
|
|
137
150
|
|
|
138
151
|
|
|
139
152
|
class IllegalNode(Node):
|
|
@@ -21,6 +21,7 @@ from .filters.array import sort_natural
|
|
|
21
21
|
from .filters.array import sum_
|
|
22
22
|
from .filters.array import uniq
|
|
23
23
|
from .filters.array import where
|
|
24
|
+
from .filters.extra import escapejs
|
|
24
25
|
from .filters.extra import safe
|
|
25
26
|
from .filters.math import abs_
|
|
26
27
|
from .filters.math import at_least
|
|
@@ -57,6 +58,7 @@ from .filters.string import replace_last
|
|
|
57
58
|
from .filters.string import rstrip
|
|
58
59
|
from .filters.string import slice_
|
|
59
60
|
from .filters.string import split
|
|
61
|
+
from .filters.string import squish
|
|
60
62
|
from .filters.string import strip
|
|
61
63
|
from .filters.string import strip_html
|
|
62
64
|
from .filters.string import strip_newlines
|
|
@@ -174,6 +176,7 @@ def register(env: Environment) -> None: # noqa: PLR0915
|
|
|
174
176
|
env.add_filter("base64_decode", base64_decode)
|
|
175
177
|
env.add_filter("base64_url_safe_encode", base64_url_safe_encode)
|
|
176
178
|
env.add_filter("base64_url_safe_decode", base64_url_safe_decode)
|
|
179
|
+
env.add_filter("squish", squish)
|
|
177
180
|
|
|
178
181
|
env.add_filter("find", find)
|
|
179
182
|
env.add_filter("find_index", find_index)
|
|
@@ -197,3 +200,4 @@ def register(env: Environment) -> None: # noqa: PLR0915
|
|
|
197
200
|
env.add_filter("date", date)
|
|
198
201
|
|
|
199
202
|
env.add_filter("safe", safe)
|
|
203
|
+
env.add_filter("escapejs", escapejs)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Filters that don't exist in the reference implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from liquid import Markup
|
|
9
|
+
from liquid.filter import string_filter
|
|
10
|
+
from liquid.filter import with_environment
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from liquid import Environment
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@with_environment
|
|
17
|
+
@string_filter
|
|
18
|
+
def safe(val: str, *, environment: Environment) -> str:
|
|
19
|
+
"""Return a copy of _val_ that will not be automatically HTML escaped on output."""
|
|
20
|
+
if environment.autoescape:
|
|
21
|
+
return Markup(val)
|
|
22
|
+
return val
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# `escapejs` is inspired by https://github.com/salesforce/secure-filters and Django's
|
|
26
|
+
# escapejs filter, https://github.com/django/django/blob/485f483d49144a2ea5401442bc3b937a370b3ca6/django/utils/html.py#L63
|
|
27
|
+
|
|
28
|
+
_ESCAPE_MAP = {
|
|
29
|
+
"\\": "\\u005C",
|
|
30
|
+
"'": "\\u0027",
|
|
31
|
+
'"': "\\u0022",
|
|
32
|
+
">": "\\u003E",
|
|
33
|
+
"<": "\\u003C",
|
|
34
|
+
"&": "\\u0026",
|
|
35
|
+
"=": "\\u003D",
|
|
36
|
+
"-": "\\u002D",
|
|
37
|
+
";": "\\u003B",
|
|
38
|
+
"`": "\\u0060",
|
|
39
|
+
"\u2028": "\\u2028",
|
|
40
|
+
"\u2029": "\\u2029",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
_ESCAPE_MAP.update({chr(c): f"\\u{c:04X}" for c in range(32)})
|
|
44
|
+
_ESCAPE_RE = re.compile("[" + re.escape("".join(_ESCAPE_MAP.keys())) + "]")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@with_environment
|
|
48
|
+
@string_filter
|
|
49
|
+
def escapejs(val: str, *, environment: Environment) -> str:
|
|
50
|
+
"""Escape characters for safe use in JavaScript string literals.
|
|
51
|
+
|
|
52
|
+
This filter escapes a string for embedding inside **JavaScript string
|
|
53
|
+
literals**, using either single or double quotes (e.g. `'...'` or `"..."`).
|
|
54
|
+
It replaces control characters and potentially dangerous symbols with
|
|
55
|
+
their corresponding Unicode escape sequences.
|
|
56
|
+
|
|
57
|
+
**Important:** This filter does **not** make strings safe for use in
|
|
58
|
+
JavaScript template literals (backtick strings), or in raw JavaScript
|
|
59
|
+
expressions. Use it only when placing data inside quoted JS strings
|
|
60
|
+
within inline `<script>` blocks or event handlers.
|
|
61
|
+
|
|
62
|
+
**Recommended alternatives:**
|
|
63
|
+
- Pass data using HTML `data-*` attributes and read them in JS via
|
|
64
|
+
`element.dataset`.
|
|
65
|
+
- For structured data, prefer a JSON-serialization approach using the
|
|
66
|
+
JSON filter.
|
|
67
|
+
|
|
68
|
+
Escaped characters include:
|
|
69
|
+
- ASCII control characters (U+0000 to U+001F)
|
|
70
|
+
- Characters like quotes, angle brackets, ampersands, equals signs
|
|
71
|
+
- Line/paragraph separators (U+2028, U+2029)
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
val: The input string to escape.
|
|
75
|
+
environment: The active Liquid environment
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
A JavaScript-safe string, with problematic characters escaped as Unicode.
|
|
79
|
+
"""
|
|
80
|
+
escaped = _ESCAPE_RE.sub(lambda m: _ESCAPE_MAP[m.group()], val)
|
|
81
|
+
if environment.autoescape:
|
|
82
|
+
return Markup(escaped)
|
|
83
|
+
return escaped
|
|
@@ -56,7 +56,35 @@ def downcase(val: str) -> str:
|
|
|
56
56
|
@with_environment
|
|
57
57
|
@string_filter
|
|
58
58
|
def escape(val: str, *, environment: Environment) -> str:
|
|
59
|
-
"""
|
|
59
|
+
"""Escape special characters in a string for safe use in HTML.
|
|
60
|
+
|
|
61
|
+
This filter replaces the characters `&`, `<`, `>`, `'`, and `"` with their
|
|
62
|
+
corresponding HTML-safe sequences:
|
|
63
|
+
|
|
64
|
+
- `&` -> `&`
|
|
65
|
+
- `<` -> `<`
|
|
66
|
+
- `>` -> `>`
|
|
67
|
+
- `'` -> `'`
|
|
68
|
+
- `"` -> `"`
|
|
69
|
+
|
|
70
|
+
This helps prevent HTML injection (XSS) when rendering untrusted content in
|
|
71
|
+
HTML element bodies or attributes.
|
|
72
|
+
|
|
73
|
+
Important: This filter does **not** make strings safe for use in JavaScript,
|
|
74
|
+
including in `<script>` blocks, inline event handler attributes (e.g. `onerror`),
|
|
75
|
+
or other JavaScript contexts. For those cases, use the `escapejs` filter instead.
|
|
76
|
+
|
|
77
|
+
When `autoescape` is enabled in the environment, this filter uses the same
|
|
78
|
+
escaping logic as the environment (via `markupsafe.escape()`). Otherwise, it
|
|
79
|
+
falls back to Python's standard `html.escape()`.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
val: The input string to escape.
|
|
83
|
+
environment: The current rendering environment.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
A string with HTML-special characters replaced by safe escape sequences.
|
|
87
|
+
"""
|
|
60
88
|
if environment.autoescape:
|
|
61
89
|
return markupsafe_escape(str(val))
|
|
62
90
|
return html.escape(val)
|
|
@@ -65,10 +93,14 @@ def escape(val: str, *, environment: Environment) -> str:
|
|
|
65
93
|
@with_environment
|
|
66
94
|
@string_filter
|
|
67
95
|
def escape_once(val: str, *, environment: Environment) -> str:
|
|
68
|
-
"""
|
|
96
|
+
"""Escape a string for HTML, but avoid double-escaping existing entities.
|
|
69
97
|
|
|
70
|
-
|
|
71
|
-
|
|
98
|
+
Converts characters like `&`, `<`, and `>` to their HTML-safe sequences,
|
|
99
|
+
but leaves existing HTML entities untouched (e.g., `&` stays `&`).
|
|
100
|
+
|
|
101
|
+
This is useful when escaping content that may already be partially escaped.
|
|
102
|
+
|
|
103
|
+
See the `escape` filter for details and limitations.
|
|
72
104
|
"""
|
|
73
105
|
if environment.autoescape:
|
|
74
106
|
return Markup(val).unescape()
|
|
@@ -373,3 +405,12 @@ def base64_url_safe_decode(val: str) -> str:
|
|
|
373
405
|
return base64.urlsafe_b64decode(val).decode()
|
|
374
406
|
except binascii.Error as err:
|
|
375
407
|
raise FilterError("invalid base64-encoded string", token=None) from err
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
RE_WHITESPACE = re.compile(r"\s+")
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
@string_filter
|
|
414
|
+
def squish(val: str) -> str:
|
|
415
|
+
"""Strip whitespace and replace whitespace with a single space."""
|
|
416
|
+
return RE_WHITESPACE.sub(" ", val.strip())
|
{python_liquid-2.0.2 → python_liquid-2.2.0}/liquid/builtin/loaders/caching_file_system_loader.py
RENAMED
|
@@ -21,6 +21,8 @@ class CachingFileSystemLoader(CachingLoaderMixin, FileSystemLoader):
|
|
|
21
21
|
search_path: One or more paths to search.
|
|
22
22
|
encoding: Open template files with the given encoding.
|
|
23
23
|
ext: A default file extension. Should include a leading period.
|
|
24
|
+
reject_symlinks: When `True`, reject paths to symlinks that resolve to files
|
|
25
|
+
outside the search path. Defaults to `False`.
|
|
24
26
|
auto_reload: If `True`, automatically reload a cached template if it has been
|
|
25
27
|
updated.
|
|
26
28
|
namespace_key: The name of a global render context variable or loader keyword
|
|
@@ -39,6 +41,7 @@ class CachingFileSystemLoader(CachingLoaderMixin, FileSystemLoader):
|
|
|
39
41
|
encoding: str = "utf-8",
|
|
40
42
|
ext: Optional[str] = None,
|
|
41
43
|
*,
|
|
44
|
+
reject_symlinks: bool = False,
|
|
42
45
|
auto_reload: bool = True,
|
|
43
46
|
namespace_key: str = "",
|
|
44
47
|
capacity: int = 300,
|
|
@@ -54,4 +57,5 @@ class CachingFileSystemLoader(CachingLoaderMixin, FileSystemLoader):
|
|
|
54
57
|
search_path=search_path,
|
|
55
58
|
encoding=encoding,
|
|
56
59
|
ext=ext,
|
|
60
|
+
reject_symlinks=reject_symlinks,
|
|
57
61
|
)
|
|
@@ -27,6 +27,8 @@ class FileSystemLoader(BaseLoader):
|
|
|
27
27
|
search_path: One or more paths to search.
|
|
28
28
|
encoding: Encoding to use when opening files.
|
|
29
29
|
ext: A default file extension. Should include a leading period.
|
|
30
|
+
reject_symlinks: When `True`, reject paths to symlinks that resolve to files
|
|
31
|
+
outside the search path. Defaults to `False`.
|
|
30
32
|
"""
|
|
31
33
|
|
|
32
34
|
def __init__(
|
|
@@ -35,6 +37,7 @@ class FileSystemLoader(BaseLoader):
|
|
|
35
37
|
*,
|
|
36
38
|
encoding: str = "utf-8",
|
|
37
39
|
ext: Optional[str] = None,
|
|
40
|
+
reject_symlinks: bool = False,
|
|
38
41
|
):
|
|
39
42
|
super().__init__()
|
|
40
43
|
if not isinstance(search_path, Iterable) or isinstance(search_path, str):
|
|
@@ -43,6 +46,7 @@ class FileSystemLoader(BaseLoader):
|
|
|
43
46
|
self.search_path = [Path(path) for path in search_path]
|
|
44
47
|
self.encoding = encoding
|
|
45
48
|
self.ext = ext
|
|
49
|
+
self.reject_symlinks = reject_symlinks
|
|
46
50
|
|
|
47
51
|
def resolve_path(self, template_name: str) -> Path:
|
|
48
52
|
"""Return a path to the template identified by _template_name_.
|
|
@@ -56,14 +60,27 @@ class FileSystemLoader(BaseLoader):
|
|
|
56
60
|
if self.ext and not template_path.suffix:
|
|
57
61
|
template_path = template_path.with_suffix(self.ext)
|
|
58
62
|
|
|
59
|
-
if os.path.pardir in template_path.parts:
|
|
63
|
+
if os.path.pardir in template_path.parts or template_path.is_absolute():
|
|
60
64
|
raise TemplateNotFoundError(template_name)
|
|
61
65
|
|
|
62
|
-
for
|
|
63
|
-
source_path =
|
|
64
|
-
|
|
66
|
+
for base in self.search_path:
|
|
67
|
+
source_path = base.joinpath(template_path)
|
|
68
|
+
|
|
69
|
+
if not source_path.exists() or not source_path.is_file():
|
|
65
70
|
continue
|
|
71
|
+
|
|
72
|
+
if self.reject_symlinks:
|
|
73
|
+
try:
|
|
74
|
+
resolved = source_path.resolve(strict=False)
|
|
75
|
+
base_resolved = base.resolve(strict=False)
|
|
76
|
+
except OSError:
|
|
77
|
+
continue
|
|
78
|
+
|
|
79
|
+
if not resolved.is_relative_to(base_resolved):
|
|
80
|
+
continue
|
|
81
|
+
|
|
66
82
|
return source_path
|
|
83
|
+
|
|
67
84
|
raise TemplateNotFoundError(template_name)
|
|
68
85
|
|
|
69
86
|
def _read(self, source_path: Path) -> tuple[str, float]:
|
|
@@ -11,7 +11,7 @@ from typing import Union
|
|
|
11
11
|
from liquid.ast import BlockNode
|
|
12
12
|
from liquid.ast import Node
|
|
13
13
|
from liquid.builtin.expressions import parse_primitive
|
|
14
|
-
from liquid.builtin.expressions.logical import _eq
|
|
14
|
+
from liquid.builtin.expressions.logical import _eq # type: ignore
|
|
15
15
|
from liquid.exceptions import LiquidSyntaxError
|
|
16
16
|
from liquid.expression import Expression
|
|
17
17
|
from liquid.parser import get_parser
|
|
@@ -85,7 +85,7 @@ class CaseNode(Node):
|
|
|
85
85
|
count += count_
|
|
86
86
|
# Only render `else` blocks if all preceding `when` blocks are falsy.
|
|
87
87
|
# Multiple `else` blocks are OK.
|
|
88
|
-
elif
|
|
88
|
+
elif default:
|
|
89
89
|
count += block.render(context, buffer)
|
|
90
90
|
|
|
91
91
|
return count
|
|
@@ -106,7 +106,7 @@ class CaseNode(Node):
|
|
|
106
106
|
count += count_
|
|
107
107
|
# Only render `else` blocks if all preceding `when` blocks are falsy.
|
|
108
108
|
# Multiple `else` blocks are OK.
|
|
109
|
-
elif
|
|
109
|
+
elif default:
|
|
110
110
|
count += await block.render_async(context, buffer)
|
|
111
111
|
|
|
112
112
|
return count
|
|
@@ -37,7 +37,7 @@ class CommentNode(ast.Node):
|
|
|
37
37
|
def __str__(self) -> str:
|
|
38
38
|
return f"{{% comment %}}{self.text}{{% endcomment %}}"
|
|
39
39
|
|
|
40
|
-
def render_to_output(self,
|
|
40
|
+
def render_to_output(self, context: RenderContext, buffer: TextIO) -> int: # noqa: ARG002
|
|
41
41
|
"""Render the node to the output buffer."""
|
|
42
42
|
return 0
|
|
43
43
|
|
|
@@ -61,7 +61,7 @@ class CommentTag(Tag):
|
|
|
61
61
|
|
|
62
62
|
# A block `{% comment %}`
|
|
63
63
|
token = stream.eat(TOKEN_TAG)
|
|
64
|
-
text = []
|
|
64
|
+
text: list[str] = []
|
|
65
65
|
|
|
66
66
|
while True:
|
|
67
67
|
if stream.current.kind == TOKEN_EOF:
|
|
@@ -50,7 +50,7 @@ class DocTag(Tag):
|
|
|
50
50
|
|
|
51
51
|
# This only happens if the doc tag is malformed
|
|
52
52
|
token = stream.eat(TOKEN_TAG)
|
|
53
|
-
text = []
|
|
53
|
+
text: list[str] = []
|
|
54
54
|
|
|
55
55
|
if stream.current.kind == TOKEN_EXPRESSION:
|
|
56
56
|
raise LiquidSyntaxError("unexpected expression", token=stream.current)
|
|
@@ -152,7 +152,7 @@ class IfTag(Tag):
|
|
|
152
152
|
condition = BooleanExpression.parse(self.env, tokens)
|
|
153
153
|
parse_block = get_parser(self.env).parse_block
|
|
154
154
|
consequence = parse_block(stream, ENDIFBLOCK)
|
|
155
|
-
alternatives = []
|
|
155
|
+
alternatives: list[ConditionalBlockNode] = []
|
|
156
156
|
|
|
157
157
|
while stream.current.is_tag(TAG_ELSIF):
|
|
158
158
|
# If the expression can't be parsed, eat the "elsif" block and
|
|
@@ -7,6 +7,7 @@ from typing import TYPE_CHECKING
|
|
|
7
7
|
from typing import Iterable
|
|
8
8
|
from typing import Optional
|
|
9
9
|
from typing import TextIO
|
|
10
|
+
from typing import Union
|
|
10
11
|
|
|
11
12
|
from liquid.ast import Node
|
|
12
13
|
from liquid.ast import Partial
|
|
@@ -21,11 +22,12 @@ from liquid.builtin.expressions import parse_identifier
|
|
|
21
22
|
from liquid.builtin.expressions import parse_primitive
|
|
22
23
|
from liquid.builtin.tags.for_tag import ForLoop
|
|
23
24
|
from liquid.builtin.tags.include_tag import TAG_INCLUDE
|
|
25
|
+
from liquid.exceptions import LiquidSyntaxError
|
|
24
26
|
from liquid.exceptions import TemplateNotFoundError
|
|
25
27
|
from liquid.tag import Tag
|
|
28
|
+
from liquid.template import BoundTemplate
|
|
26
29
|
from liquid.token import TOKEN_AS
|
|
27
30
|
from liquid.token import TOKEN_FOR
|
|
28
|
-
from liquid.token import TOKEN_STRING
|
|
29
31
|
from liquid.token import TOKEN_TAG
|
|
30
32
|
from liquid.token import TOKEN_WITH
|
|
31
33
|
from liquid.token import TOKEN_WORD
|
|
@@ -50,7 +52,8 @@ class RenderNode(Node):
|
|
|
50
52
|
def __init__(
|
|
51
53
|
self,
|
|
52
54
|
token: Token,
|
|
53
|
-
name: StringLiteral,
|
|
55
|
+
name: Union[StringLiteral, Identifier],
|
|
56
|
+
*,
|
|
54
57
|
var: Optional[Expression] = None,
|
|
55
58
|
loop: bool = False,
|
|
56
59
|
alias: Optional[Identifier] = None,
|
|
@@ -77,14 +80,26 @@ class RenderNode(Node):
|
|
|
77
80
|
|
|
78
81
|
def render_to_output(self, context: RenderContext, buffer: TextIO) -> int:
|
|
79
82
|
"""Render the node to the output buffer."""
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
+
if isinstance(self.name, Identifier):
|
|
84
|
+
# We're expecting an inline snippet.
|
|
85
|
+
template: Optional[BoundTemplate] = context.resolve(
|
|
86
|
+
self.name, token=self.token, default=None
|
|
83
87
|
)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
+
if not isinstance(template, BoundTemplate):
|
|
89
|
+
raise TemplateNotFoundError(
|
|
90
|
+
self.name,
|
|
91
|
+
filename=context.template.full_name(),
|
|
92
|
+
token=self.name.token,
|
|
93
|
+
)
|
|
94
|
+
else:
|
|
95
|
+
try:
|
|
96
|
+
template = context.env.get_template(
|
|
97
|
+
self.name.value, context=context, tag=self.tag
|
|
98
|
+
)
|
|
99
|
+
except TemplateNotFoundError as err:
|
|
100
|
+
err.token = self.name.token
|
|
101
|
+
err.template_name = context.template.full_name()
|
|
102
|
+
raise
|
|
88
103
|
|
|
89
104
|
# Evaluate keyword arguments once. Unlike 'include', 'render' can not
|
|
90
105
|
# mutate variables in the outer scope, so there's no need to re-evaluate
|
|
@@ -145,14 +160,26 @@ class RenderNode(Node):
|
|
|
145
160
|
self, context: RenderContext, buffer: TextIO
|
|
146
161
|
) -> int:
|
|
147
162
|
"""Render the node to the output buffer."""
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
163
|
+
if isinstance(self.name, Identifier):
|
|
164
|
+
# We're expecting an inline snippet.
|
|
165
|
+
template: Optional[BoundTemplate] = context.resolve(
|
|
166
|
+
self.name, token=self.token, default=None
|
|
151
167
|
)
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
168
|
+
if not isinstance(template, BoundTemplate):
|
|
169
|
+
raise TemplateNotFoundError(
|
|
170
|
+
self.name,
|
|
171
|
+
filename=context.template.full_name(),
|
|
172
|
+
token=self.name.token,
|
|
173
|
+
)
|
|
174
|
+
else:
|
|
175
|
+
try:
|
|
176
|
+
template = await context.env.get_template_async(
|
|
177
|
+
self.name.value, context=context, tag=self.tag
|
|
178
|
+
)
|
|
179
|
+
except TemplateNotFoundError as err:
|
|
180
|
+
err.token = self.name.token
|
|
181
|
+
err.template_name = context.template.full_name()
|
|
182
|
+
raise
|
|
156
183
|
|
|
157
184
|
# Evaluate keyword arguments once. Unlike 'include', 'render' can not
|
|
158
185
|
# mutate variables in the outer scope, so there's no need to re-evaluate
|
|
@@ -215,7 +242,15 @@ class RenderNode(Node):
|
|
|
215
242
|
self, static_context: RenderContext, *, include_partials: bool = True
|
|
216
243
|
) -> Iterable[Node]:
|
|
217
244
|
"""Return this node's children."""
|
|
218
|
-
if
|
|
245
|
+
if isinstance(self.name, Identifier):
|
|
246
|
+
# We're expecting an inline snippet.
|
|
247
|
+
# Always visit inline snippets, even if include_partials is False.
|
|
248
|
+
template: Optional[BoundTemplate] = static_context.resolve(
|
|
249
|
+
self.name, token=self.token, default=None
|
|
250
|
+
)
|
|
251
|
+
if template:
|
|
252
|
+
yield from template.nodes
|
|
253
|
+
elif include_partials:
|
|
219
254
|
name = self.name.evaluate(static_context)
|
|
220
255
|
try:
|
|
221
256
|
template = static_context.env.get_template(
|
|
@@ -231,7 +266,14 @@ class RenderNode(Node):
|
|
|
231
266
|
self, static_context: RenderContext, *, include_partials: bool = True
|
|
232
267
|
) -> Iterable[Node]:
|
|
233
268
|
"""Return this node's children."""
|
|
234
|
-
if
|
|
269
|
+
if isinstance(self.name, Identifier):
|
|
270
|
+
# We're expecting an inline snippet.
|
|
271
|
+
template: Optional[BoundTemplate] = static_context.resolve(
|
|
272
|
+
self.name, token=self.token, default=None
|
|
273
|
+
)
|
|
274
|
+
if template:
|
|
275
|
+
return template.nodes
|
|
276
|
+
elif include_partials:
|
|
235
277
|
name = await self.name.evaluate_async(static_context)
|
|
236
278
|
try:
|
|
237
279
|
template = await static_context.env.get_template_async(
|
|
@@ -246,12 +288,11 @@ class RenderNode(Node):
|
|
|
246
288
|
|
|
247
289
|
def expressions(self) -> Iterable[Expression]:
|
|
248
290
|
"""Return this node's expressions."""
|
|
249
|
-
yield self.name
|
|
250
291
|
if self.var:
|
|
251
292
|
yield self.var
|
|
252
293
|
yield from (arg.value for arg in self.args)
|
|
253
294
|
|
|
254
|
-
def partial_scope(self) -> Partial
|
|
295
|
+
def partial_scope(self) -> Optional[Partial]:
|
|
255
296
|
"""Return information about a partial template loaded by this node."""
|
|
256
297
|
scope: list[Identifier] = [
|
|
257
298
|
Identifier(arg.name, token=arg.token) for arg in self.args
|
|
@@ -267,7 +308,17 @@ class RenderNode(Node):
|
|
|
267
308
|
)
|
|
268
309
|
)
|
|
269
310
|
|
|
270
|
-
|
|
311
|
+
partial_name = self.name.value if isinstance(self.name, StringLiteral) else ""
|
|
312
|
+
partial_key = hash((partial_name, *[arg.name for arg in self.args]))
|
|
313
|
+
|
|
314
|
+
# Static analysis will use the parent template name if Partial.name is
|
|
315
|
+
# empty. Which is what we want for inline snippets.
|
|
316
|
+
return Partial(
|
|
317
|
+
name=partial_name,
|
|
318
|
+
scope=PartialScope.ISOLATED,
|
|
319
|
+
in_scope=scope,
|
|
320
|
+
key=partial_key,
|
|
321
|
+
)
|
|
271
322
|
|
|
272
323
|
|
|
273
324
|
BIND_TAGS = frozenset((TOKEN_WITH, TOKEN_FOR))
|
|
@@ -284,12 +335,18 @@ class RenderTag(Tag):
|
|
|
284
335
|
"""Parse tokens from _stream_ into an AST node."""
|
|
285
336
|
token = stream.eat(TOKEN_TAG)
|
|
286
337
|
tokens = stream.into_inner(tag=token, eat=False)
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
338
|
+
name: Union[Expression, Identifier] = parse_primitive(self.env, tokens)
|
|
339
|
+
|
|
340
|
+
if isinstance(name, Path):
|
|
341
|
+
head = name.head()
|
|
342
|
+
if len(name.path) != 1 or not isinstance(head, str):
|
|
343
|
+
raise LiquidSyntaxError(
|
|
344
|
+
"expected an identifier, found a path",
|
|
345
|
+
token=name.token,
|
|
346
|
+
)
|
|
347
|
+
name = Identifier(head, token=name.token)
|
|
348
|
+
elif not isinstance(name, StringLiteral):
|
|
349
|
+
raise LiquidSyntaxError("expected a string or identifier", token=name.token)
|
|
293
350
|
|
|
294
351
|
alias: Optional[Identifier] = None
|
|
295
352
|
var: Optional[Path] = None
|
|
@@ -311,4 +368,4 @@ class RenderTag(Tag):
|
|
|
311
368
|
|
|
312
369
|
# Zero or more keyword arguments
|
|
313
370
|
args = KeywordArgument.parse(self.env, tokens)
|
|
314
|
-
return self.node_class(token, name, var, loop, alias, args)
|
|
371
|
+
return self.node_class(token, name, var=var, loop=loop, alias=alias, args=args)
|
|
@@ -169,9 +169,10 @@ class TemplateNotFoundError(LiquidError):
|
|
|
169
169
|
def __init__(
|
|
170
170
|
self,
|
|
171
171
|
*args: object,
|
|
172
|
-
filename:
|
|
172
|
+
filename: Optional[str] = None,
|
|
173
|
+
token: Optional[Token] = None,
|
|
173
174
|
):
|
|
174
|
-
super().__init__(*args, token=
|
|
175
|
+
super().__init__(*args, token=token, template_name=filename)
|
|
175
176
|
|
|
176
177
|
def __str__(self) -> str:
|
|
177
178
|
if not self.token:
|
|
@@ -21,6 +21,7 @@ from .tags import BlockTag
|
|
|
21
21
|
from .tags import CallTag
|
|
22
22
|
from .tags import ExtendsTag
|
|
23
23
|
from .tags import MacroTag
|
|
24
|
+
from .tags import SnippetTag
|
|
24
25
|
from .tags import TranslateTag
|
|
25
26
|
from .tags import WithTag
|
|
26
27
|
|
|
@@ -38,7 +39,6 @@ __all__ = (
|
|
|
38
39
|
"DateTime",
|
|
39
40
|
"ExtendsTag",
|
|
40
41
|
"GetText",
|
|
41
|
-
"IfNotTag",
|
|
42
42
|
"index",
|
|
43
43
|
"JSON",
|
|
44
44
|
"MacroTag",
|
|
@@ -49,6 +49,7 @@ __all__ = (
|
|
|
49
49
|
"script_tag",
|
|
50
50
|
"sort_numeric",
|
|
51
51
|
"stylesheet_tag",
|
|
52
|
+
"SnippetTag",
|
|
52
53
|
"Translate",
|
|
53
54
|
"Unit",
|
|
54
55
|
"WithTag",
|