trmnl-liquid-py 0.1.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.
@@ -0,0 +1,34 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ ## 0.1.0 - unreleased
6
+
7
+ Initial public release target for the Python port of `usetrmnl/trmnl-liquid` 0.8.2.
8
+
9
+ ### Added
10
+
11
+ - TRMNL-compatible `Environment` and one-shot `render()` API.
12
+ - Inline `{% template %}` definitions backed by an in-memory template system.
13
+ - TRMNL filter surface for the supported non-I18n 0.8.2 scope.
14
+ - Ruby Liquid lax filter argument syntax used by TRMNL templates.
15
+ - Redcarpet-compatible Markdown rendering implemented on Mistune public APIs.
16
+ - RQRCode-compatible SVG output and QR mask selection using public `qrcode` APIs.
17
+ - Ruby-compatible scalar coercion for fallback filters.
18
+ - Ruby-compatible runtime error rendering and nested block continuation semantics.
19
+ - Typed package support through PEP 561 `py.typed` metadata.
20
+
21
+ ### Compatibility
22
+
23
+ - Ruby/Python exact differential corpus: 574 cases with 0 known supported-scope mismatches.
24
+ - All 73 official TRMNL Liquid 0.8.2 RSpec examples classified and tracked:
25
+ - 46 exact differential examples,
26
+ - 16 property/API contract examples,
27
+ - 11 Rails/I18n examples explicitly deferred.
28
+ - CI coverage for Python 3.11, 3.12, 3.13, and 3.14.
29
+
30
+ ### Deferred
31
+
32
+ - Rails/ActionView integration.
33
+ - Full I18n/localized `l_word` and `l_date` behavior.
34
+ - Features introduced after upstream `trmnl-liquid` 0.8.2.
@@ -0,0 +1,25 @@
1
+ MIT License
2
+
3
+ Portions derived from usetrmnl/trmnl-liquid:
4
+ Copyright (c) 2025 Rockwell Schrock
5
+
6
+ Python port:
7
+ Copyright (c) 2026 Tomasz Jagusz
8
+
9
+ Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ of this software and associated documentation files (the "Software"), to deal
11
+ in the Software without restriction, including without limitation the rights
12
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ copies of the Software, and to permit persons to whom the Software is
14
+ furnished to do so, subject to the following conditions:
15
+
16
+ The above copyright notice and this permission notice shall be included in all
17
+ copies or substantial portions of the Software.
18
+
19
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
+ SOFTWARE.
@@ -0,0 +1,190 @@
1
+ Metadata-Version: 2.5
2
+ Name: trmnl-liquid-py
3
+ Version: 0.1.0
4
+ Summary: TRMNL Liquid 0.8.2 compatibility layer for Python
5
+ Project-URL: Homepage, https://github.com/Misiu/trmnl-liquid-py
6
+ Project-URL: Repository, https://github.com/Misiu/trmnl-liquid-py
7
+ Project-URL: Issues, https://github.com/Misiu/trmnl-liquid-py/issues
8
+ Project-URL: Changelog, https://github.com/Misiu/trmnl-liquid-py/blob/main/CHANGELOG.md
9
+ Author: Misiu
10
+ License: MIT
11
+ License-File: LICENSE
12
+ Keywords: home-assistant,liquid,templates,trmnl
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.11
22
+ Requires-Dist: mistune==3.3.4
23
+ Requires-Dist: python-dateutil==2.9.0.post0
24
+ Requires-Dist: python-liquid==2.3.1
25
+ Requires-Dist: qrcode==8.2
26
+ Description-Content-Type: text/markdown
27
+
28
+ # trmnl-liquid-py
29
+
30
+ A Python compatibility layer for [`usetrmnl/trmnl-liquid`](https://github.com/usetrmnl/trmnl-liquid), targeting byte-for-byte compatible rendering for the supported TRMNL Liquid 0.8.2 surface.
31
+
32
+ > [!NOTE]
33
+ > The first release target is `trmnl-liquid-py` **0.1.0**, compatible with `trmnl-liquid` **0.8.2** for the supported non-I18n surface. Rails/ActionView and full I18n behavior are intentionally deferred.
34
+
35
+ ## Installation
36
+
37
+ Once 0.1.0 is published to PyPI:
38
+
39
+ ```bash
40
+ python -m pip install trmnl-liquid-py
41
+ ```
42
+
43
+ Python **3.11–3.14** is supported.
44
+
45
+ ## Quick start
46
+
47
+ ```python
48
+ from trmnl_liquid import Environment
49
+
50
+ env = Environment()
51
+ template = env.from_string("Hello {{ name }}!")
52
+ print(template.render(name="TRMNL"))
53
+ ```
54
+
55
+ For one-shot rendering:
56
+
57
+ ```python
58
+ from trmnl_liquid import render
59
+
60
+ html = render("{{ value | number_with_delimiter }}", value=1234567)
61
+ ```
62
+
63
+ ## Inline templates
64
+
65
+ TRMNL adds an inline `{% template %}` definition that can later be rendered with Liquid's `{% render %}` tag:
66
+
67
+ ```liquid
68
+ {% template card %}
69
+ <div>{{ title }}</div>
70
+ {% endtemplate %}
71
+
72
+ {% render 'card', title: 'Status' %}
73
+ ```
74
+
75
+ `trmnl-liquid-py` reproduces TRMNL 0.8.2's raw-body and template-storage semantics, including its exact `{% endtemplate %}` terminator behavior.
76
+
77
+ ## TRMNL filters
78
+
79
+ The 0.1.0 compatibility surface includes:
80
+
81
+ - `append_random`
82
+ - `days_ago`
83
+ - `group_by`
84
+ - `find_by`
85
+ - `markdown_to_html`
86
+ - `number_with_delimiter`
87
+ - `number_to_currency`
88
+ - `l_word` fallback behavior
89
+ - `l_date` fallback behavior
90
+ - `map_to_i`
91
+ - `pluralize`
92
+ - `json`
93
+ - `parse_json`
94
+ - `sample`
95
+ - `where_exp`
96
+ - `ordinalize`
97
+ - `qr_code`
98
+
99
+ Important TRMNL/Ruby syntax differences are preserved where required. For example, Ruby Liquid's lax filter syntax accepts a leading comma before the first filter argument:
100
+
101
+ ```liquid
102
+ {{ value | qr_code, 11 }}
103
+ ```
104
+
105
+ ### Markdown
106
+
107
+ `markdown_to_html` reproduces the tested output of TRMNL 0.8.2's default Redcarpet 3.6.1 configuration using Mistune on Python.
108
+
109
+ ```liquid
110
+ {{ markdown | markdown_to_html }}
111
+ ```
112
+
113
+ ### QR codes
114
+
115
+ `qr_code` renders the SVG shape expected from TRMNL's RQRCode-based implementation, including compatible QR mask selection/scoring behavior.
116
+
117
+ ```liquid
118
+ {{ 'https://example.com' | qr_code: 11 }}
119
+ ```
120
+
121
+ ## Compatibility target
122
+
123
+ The baseline is exactly:
124
+
125
+ - `trmnl-liquid` **0.8.2**
126
+ - Python **3.11, 3.12, 3.13, 3.14**
127
+ - non-I18n behavior
128
+ - Rails/ActionView and full localized `l_word` / `l_date` behavior deferred
129
+ - features added after upstream 0.8.2 excluded until a later compatibility target is adopted
130
+
131
+ Ruby `trmnl-liquid` 0.8.2 is the reference implementation. Differences in parsing, coercion, formatting, Markdown rendering, QR generation, template behavior, error handling, or rendered output are treated as compatibility issues when they affect the supported scope.
132
+
133
+ <!-- compatibility-report:start -->
134
+ ### Compatibility evidence
135
+
136
+ | Gate | Result |
137
+ | --- | ---: |
138
+ | Ruby 0.8.2 vs Python exact differential corpus | **574/574** |
139
+ | Known mismatches in the supported scope | **0** |
140
+ | Official TRMNL 0.8.2 RSpec examples mapped | **73/73** |
141
+ | Upstream examples covered by exact differential tests | **46** |
142
+ | Upstream examples covered by property/unit tests | **16** |
143
+ | Upstream examples covered in the initial non-I18n scope | **62/73** |
144
+ | Deferred Rails/I18n upstream examples | **11** |
145
+ | Python CI matrix | **3.11, 3.12, 3.13, 3.14** |
146
+ | Comparison | **exact rendered output** |
147
+
148
+ The `Compatibility` workflow runs the Ruby oracle first and fails on any mismatch. It then checks that this generated report still matches the checked-in corpus and upstream-spec manifest.
149
+ <!-- compatibility-report:end -->
150
+
151
+ See [`COMPATIBILITY.md`](COMPATIBILITY.md) for the compatibility contract, upstream spec mapping, and Ruby oracle details.
152
+
153
+ ## Development
154
+
155
+ Install the project with development dependencies, then run the normal quality gates:
156
+
157
+ ```bash
158
+ python -m pip install -e . pytest ruff mypy build twine \
159
+ types-python-dateutil==2.9.0.20260807 \
160
+ types-qrcode==8.2.0.20260518
161
+ ruff check .
162
+ mypy src/trmnl_liquid
163
+ pytest
164
+ python -m build
165
+ twine check dist/*
166
+ ```
167
+
168
+ The Ruby/Python differential suite additionally requires Ruby and the bundle in `compatibility/ruby`:
169
+
170
+ ```bash
171
+ bundle install --gemfile compatibility/ruby/Gemfile
172
+ python compatibility/compare.py
173
+ python -m compatibility.report --check
174
+ ```
175
+
176
+ ## Release process
177
+
178
+ Releases are tag-driven. There is no manual publish workflow and no PyPI API token stored in GitHub.
179
+
180
+ 1. Update `src/trmnl_liquid/__about__.py` and `CHANGELOG.md` in a normal PR.
181
+ 2. Merge the PR to `main` and wait for both `CI` and `Compatibility` to pass on `main`.
182
+ 3. Create and push a tag whose name is exactly the package version, for example `0.1.0`.
183
+ 4. The `Release` workflow verifies that the tag equals `__version__` and points to a commit on `main`.
184
+ 5. The workflow builds and validates the wheel/sdist, publishes them to PyPI through Trusted Publishing, and only after a successful PyPI publish creates the matching GitHub Release with the distributions attached.
185
+
186
+ A tag such as `v0.1.0` is intentionally invalid when `__version__ == "0.1.0"`.
187
+
188
+ ## License
189
+
190
+ MIT. The repository license includes attribution for portions derived from `usetrmnl/trmnl-liquid`.
@@ -0,0 +1,163 @@
1
+ # trmnl-liquid-py
2
+
3
+ A Python compatibility layer for [`usetrmnl/trmnl-liquid`](https://github.com/usetrmnl/trmnl-liquid), targeting byte-for-byte compatible rendering for the supported TRMNL Liquid 0.8.2 surface.
4
+
5
+ > [!NOTE]
6
+ > The first release target is `trmnl-liquid-py` **0.1.0**, compatible with `trmnl-liquid` **0.8.2** for the supported non-I18n surface. Rails/ActionView and full I18n behavior are intentionally deferred.
7
+
8
+ ## Installation
9
+
10
+ Once 0.1.0 is published to PyPI:
11
+
12
+ ```bash
13
+ python -m pip install trmnl-liquid-py
14
+ ```
15
+
16
+ Python **3.11–3.14** is supported.
17
+
18
+ ## Quick start
19
+
20
+ ```python
21
+ from trmnl_liquid import Environment
22
+
23
+ env = Environment()
24
+ template = env.from_string("Hello {{ name }}!")
25
+ print(template.render(name="TRMNL"))
26
+ ```
27
+
28
+ For one-shot rendering:
29
+
30
+ ```python
31
+ from trmnl_liquid import render
32
+
33
+ html = render("{{ value | number_with_delimiter }}", value=1234567)
34
+ ```
35
+
36
+ ## Inline templates
37
+
38
+ TRMNL adds an inline `{% template %}` definition that can later be rendered with Liquid's `{% render %}` tag:
39
+
40
+ ```liquid
41
+ {% template card %}
42
+ <div>{{ title }}</div>
43
+ {% endtemplate %}
44
+
45
+ {% render 'card', title: 'Status' %}
46
+ ```
47
+
48
+ `trmnl-liquid-py` reproduces TRMNL 0.8.2's raw-body and template-storage semantics, including its exact `{% endtemplate %}` terminator behavior.
49
+
50
+ ## TRMNL filters
51
+
52
+ The 0.1.0 compatibility surface includes:
53
+
54
+ - `append_random`
55
+ - `days_ago`
56
+ - `group_by`
57
+ - `find_by`
58
+ - `markdown_to_html`
59
+ - `number_with_delimiter`
60
+ - `number_to_currency`
61
+ - `l_word` fallback behavior
62
+ - `l_date` fallback behavior
63
+ - `map_to_i`
64
+ - `pluralize`
65
+ - `json`
66
+ - `parse_json`
67
+ - `sample`
68
+ - `where_exp`
69
+ - `ordinalize`
70
+ - `qr_code`
71
+
72
+ Important TRMNL/Ruby syntax differences are preserved where required. For example, Ruby Liquid's lax filter syntax accepts a leading comma before the first filter argument:
73
+
74
+ ```liquid
75
+ {{ value | qr_code, 11 }}
76
+ ```
77
+
78
+ ### Markdown
79
+
80
+ `markdown_to_html` reproduces the tested output of TRMNL 0.8.2's default Redcarpet 3.6.1 configuration using Mistune on Python.
81
+
82
+ ```liquid
83
+ {{ markdown | markdown_to_html }}
84
+ ```
85
+
86
+ ### QR codes
87
+
88
+ `qr_code` renders the SVG shape expected from TRMNL's RQRCode-based implementation, including compatible QR mask selection/scoring behavior.
89
+
90
+ ```liquid
91
+ {{ 'https://example.com' | qr_code: 11 }}
92
+ ```
93
+
94
+ ## Compatibility target
95
+
96
+ The baseline is exactly:
97
+
98
+ - `trmnl-liquid` **0.8.2**
99
+ - Python **3.11, 3.12, 3.13, 3.14**
100
+ - non-I18n behavior
101
+ - Rails/ActionView and full localized `l_word` / `l_date` behavior deferred
102
+ - features added after upstream 0.8.2 excluded until a later compatibility target is adopted
103
+
104
+ Ruby `trmnl-liquid` 0.8.2 is the reference implementation. Differences in parsing, coercion, formatting, Markdown rendering, QR generation, template behavior, error handling, or rendered output are treated as compatibility issues when they affect the supported scope.
105
+
106
+ <!-- compatibility-report:start -->
107
+ ### Compatibility evidence
108
+
109
+ | Gate | Result |
110
+ | --- | ---: |
111
+ | Ruby 0.8.2 vs Python exact differential corpus | **574/574** |
112
+ | Known mismatches in the supported scope | **0** |
113
+ | Official TRMNL 0.8.2 RSpec examples mapped | **73/73** |
114
+ | Upstream examples covered by exact differential tests | **46** |
115
+ | Upstream examples covered by property/unit tests | **16** |
116
+ | Upstream examples covered in the initial non-I18n scope | **62/73** |
117
+ | Deferred Rails/I18n upstream examples | **11** |
118
+ | Python CI matrix | **3.11, 3.12, 3.13, 3.14** |
119
+ | Comparison | **exact rendered output** |
120
+
121
+ The `Compatibility` workflow runs the Ruby oracle first and fails on any mismatch. It then checks that this generated report still matches the checked-in corpus and upstream-spec manifest.
122
+ <!-- compatibility-report:end -->
123
+
124
+ See [`COMPATIBILITY.md`](COMPATIBILITY.md) for the compatibility contract, upstream spec mapping, and Ruby oracle details.
125
+
126
+ ## Development
127
+
128
+ Install the project with development dependencies, then run the normal quality gates:
129
+
130
+ ```bash
131
+ python -m pip install -e . pytest ruff mypy build twine \
132
+ types-python-dateutil==2.9.0.20260807 \
133
+ types-qrcode==8.2.0.20260518
134
+ ruff check .
135
+ mypy src/trmnl_liquid
136
+ pytest
137
+ python -m build
138
+ twine check dist/*
139
+ ```
140
+
141
+ The Ruby/Python differential suite additionally requires Ruby and the bundle in `compatibility/ruby`:
142
+
143
+ ```bash
144
+ bundle install --gemfile compatibility/ruby/Gemfile
145
+ python compatibility/compare.py
146
+ python -m compatibility.report --check
147
+ ```
148
+
149
+ ## Release process
150
+
151
+ Releases are tag-driven. There is no manual publish workflow and no PyPI API token stored in GitHub.
152
+
153
+ 1. Update `src/trmnl_liquid/__about__.py` and `CHANGELOG.md` in a normal PR.
154
+ 2. Merge the PR to `main` and wait for both `CI` and `Compatibility` to pass on `main`.
155
+ 3. Create and push a tag whose name is exactly the package version, for example `0.1.0`.
156
+ 4. The `Release` workflow verifies that the tag equals `__version__` and points to a commit on `main`.
157
+ 5. The workflow builds and validates the wheel/sdist, publishes them to PyPI through Trusted Publishing, and only after a successful PyPI publish creates the matching GitHub Release with the distributions attached.
158
+
159
+ A tag such as `v0.1.0` is intentionally invalid when `__version__ == "0.1.0"`.
160
+
161
+ ## License
162
+
163
+ MIT. The repository license includes attribution for portions derived from `usetrmnl/trmnl-liquid`.
@@ -0,0 +1,78 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "trmnl-liquid-py"
7
+ dynamic = ["version"]
8
+ description = "TRMNL Liquid 0.8.2 compatibility layer for Python"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Misiu" }]
13
+ keywords = ["trmnl", "liquid", "templates", "home-assistant"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.11",
19
+ "Programming Language :: Python :: 3.12",
20
+ "Programming Language :: Python :: 3.13",
21
+ "Programming Language :: Python :: 3.14",
22
+ "Typing :: Typed",
23
+ ]
24
+ dependencies = [
25
+ "mistune==3.3.4",
26
+ "python-dateutil==2.9.0.post0",
27
+ "python-liquid==2.3.1",
28
+ "qrcode==8.2",
29
+ ]
30
+
31
+ [project.urls]
32
+ Homepage = "https://github.com/Misiu/trmnl-liquid-py"
33
+ Repository = "https://github.com/Misiu/trmnl-liquid-py"
34
+ Issues = "https://github.com/Misiu/trmnl-liquid-py/issues"
35
+ Changelog = "https://github.com/Misiu/trmnl-liquid-py/blob/main/CHANGELOG.md"
36
+
37
+ [tool.hatch.version]
38
+ path = "src/trmnl_liquid/__about__.py"
39
+
40
+ [tool.hatch.build.targets.wheel]
41
+ packages = ["src/trmnl_liquid"]
42
+
43
+ [tool.hatch.build.targets.sdist]
44
+ include = [
45
+ "src/trmnl_liquid",
46
+ "README.md",
47
+ "CHANGELOG.md",
48
+ "LICENSE",
49
+ "pyproject.toml",
50
+ ]
51
+
52
+ [tool.pytest.ini_options]
53
+ testpaths = ["tests"]
54
+ pythonpath = ["."]
55
+ addopts = "-ra --strict-config --strict-markers"
56
+
57
+ [tool.ruff]
58
+ target-version = "py311"
59
+ line-length = 100
60
+
61
+ [tool.ruff.lint]
62
+ select = ["E", "F", "I", "UP", "B", "SIM", "RUF"]
63
+
64
+ [tool.mypy]
65
+ python_version = "3.11"
66
+ strict = true
67
+ packages = ["trmnl_liquid"]
68
+
69
+ [dependency-groups]
70
+ dev = [
71
+ "build>=1.3",
72
+ "mypy>=1.17",
73
+ "pytest>=8.4",
74
+ "ruff>=0.12",
75
+ "twine>=6.1",
76
+ "types-python-dateutil==2.9.0.20260807",
77
+ "types-qrcode==8.2.0.20260518",
78
+ ]
@@ -0,0 +1,3 @@
1
+ """Package metadata kept separate from runtime imports."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,6 @@
1
+ """TRMNL Liquid compatibility layer for Python."""
2
+
3
+ from .__about__ import __version__
4
+ from .environment import Environment, render
5
+
6
+ __all__ = ["Environment", "__version__", "render"]
@@ -0,0 +1,125 @@
1
+ """Ruby-compatible nested control-flow nodes for python-liquid.
2
+
3
+ Ruby Liquid rescues runtime errors per child node in every ``BlockBody``. We keep
4
+ python-liquid's public ``IfTag`` and ``ForTag`` parsers and adapt only their parsed
5
+ child blocks to :class:`TRMNLBlockNode`.
6
+
7
+ References:
8
+ - Ruby Liquid 5.13 BlockBody rendering:
9
+ https://github.com/Shopify/liquid/blob/v5.13.0/lib/liquid/block_body.rb
10
+ - python-liquid 2.3.1 IfTag/IfNode:
11
+ https://github.com/jg-rp/liquid/blob/v2.3.1/liquid/builtin/tags/if_tag.py
12
+ - python-liquid 2.3.1 ForTag/ForNode:
13
+ https://github.com/jg-rp/liquid/blob/v2.3.1/liquid/builtin/tags/for_tag.py
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from typing import TYPE_CHECKING
19
+
20
+ from liquid.ast import BlockNode, ConditionalBlockNode, Node
21
+ from liquid.builtin.expressions import LoopExpression
22
+ from liquid.builtin.tags.for_tag import ForNode, ForTag
23
+ from liquid.builtin.tags.if_tag import IfNode, IfTag
24
+ from liquid.expression import Expression
25
+ from liquid.token import Token
26
+
27
+ from .template import TRMNLBlockNode
28
+
29
+ if TYPE_CHECKING:
30
+ from liquid.stream import TokenStream
31
+
32
+
33
+ def _block(block: BlockNode) -> TRMNLBlockNode:
34
+ return TRMNLBlockNode(block.token, block.nodes)
35
+
36
+
37
+ def _optional_block(block: BlockNode | None) -> TRMNLBlockNode | None:
38
+ return _block(block) if block is not None else None
39
+
40
+
41
+ class TRMNLIfNode(IfNode):
42
+ """IfNode whose branch bodies continue after individual runtime errors."""
43
+
44
+ def __init__(
45
+ self,
46
+ token: Token,
47
+ condition: Expression,
48
+ consequence: BlockNode,
49
+ alternatives: list[ConditionalBlockNode],
50
+ default: BlockNode | None,
51
+ ) -> None:
52
+ wrapped_alternatives = [
53
+ ConditionalBlockNode(
54
+ alternative.token,
55
+ expression=alternative.expression,
56
+ block=_block(alternative.block),
57
+ )
58
+ for alternative in alternatives
59
+ ]
60
+ super().__init__(
61
+ token=token,
62
+ condition=condition,
63
+ consequence=_block(consequence),
64
+ alternatives=wrapped_alternatives,
65
+ default=_optional_block(default),
66
+ )
67
+
68
+ @classmethod
69
+ def from_upstream(cls, node: IfNode) -> TRMNLIfNode:
70
+ """Wrap an upstream IfNode without changing its parsed expressions."""
71
+ return cls(
72
+ token=node.token,
73
+ condition=node.condition,
74
+ consequence=node.consequence,
75
+ alternatives=node.alternatives,
76
+ default=node.default,
77
+ )
78
+
79
+
80
+ class TRMNLIfTag(IfTag):
81
+ """Built-in ``if`` parser with TRMNL's nested block runtime semantics."""
82
+
83
+ def parse(self, stream: TokenStream) -> Node:
84
+ node = super().parse(stream)
85
+ if not isinstance(node, IfNode):
86
+ return node
87
+ return TRMNLIfNode.from_upstream(node)
88
+
89
+
90
+ class TRMNLForNode(ForNode):
91
+ """ForNode whose iteration body continues after individual runtime errors."""
92
+
93
+ def __init__(
94
+ self,
95
+ token: Token,
96
+ expression: LoopExpression,
97
+ block: BlockNode,
98
+ default: BlockNode | None = None,
99
+ ) -> None:
100
+ super().__init__(
101
+ token=token,
102
+ expression=expression,
103
+ block=_block(block),
104
+ default=_optional_block(default),
105
+ )
106
+
107
+ @classmethod
108
+ def from_upstream(cls, node: ForNode) -> TRMNLForNode:
109
+ """Wrap an upstream ForNode without changing its parsed expression."""
110
+ return cls(
111
+ token=node.token,
112
+ expression=node.expression,
113
+ block=node.block,
114
+ default=node.default,
115
+ )
116
+
117
+
118
+ class TRMNLForTag(ForTag):
119
+ """Built-in ``for`` parser with TRMNL's nested block runtime semantics."""
120
+
121
+ def parse(self, stream: TokenStream) -> Node:
122
+ node = super().parse(stream)
123
+ if not isinstance(node, ForNode):
124
+ return node
125
+ return TRMNLForNode.from_upstream(node)