@barefootjs/jinja 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +66 -0
  2. package/dist/adapter/analysis/component-tree.d.ts +26 -0
  3. package/dist/adapter/analysis/component-tree.d.ts.map +1 -0
  4. package/dist/adapter/boolean-result.d.ts +84 -0
  5. package/dist/adapter/boolean-result.d.ts.map +1 -0
  6. package/dist/adapter/emit-context.d.ts +106 -0
  7. package/dist/adapter/emit-context.d.ts.map +1 -0
  8. package/dist/adapter/expr/array-method.d.ts +75 -0
  9. package/dist/adapter/expr/array-method.d.ts.map +1 -0
  10. package/dist/adapter/expr/emitters.d.ts +143 -0
  11. package/dist/adapter/expr/emitters.d.ts.map +1 -0
  12. package/dist/adapter/expr/operand.d.ts +25 -0
  13. package/dist/adapter/expr/operand.d.ts.map +1 -0
  14. package/dist/adapter/index.d.ts +6 -0
  15. package/dist/adapter/index.d.ts.map +1 -0
  16. package/dist/adapter/index.js +189097 -0
  17. package/dist/adapter/jinja-adapter.d.ts +394 -0
  18. package/dist/adapter/jinja-adapter.d.ts.map +1 -0
  19. package/dist/adapter/lib/constants.d.ts +21 -0
  20. package/dist/adapter/lib/constants.d.ts.map +1 -0
  21. package/dist/adapter/lib/ir-scope.d.ts +50 -0
  22. package/dist/adapter/lib/ir-scope.d.ts.map +1 -0
  23. package/dist/adapter/lib/jinja-naming.d.ts +77 -0
  24. package/dist/adapter/lib/jinja-naming.d.ts.map +1 -0
  25. package/dist/adapter/lib/types.d.ts +27 -0
  26. package/dist/adapter/lib/types.d.ts.map +1 -0
  27. package/dist/adapter/memo/seed.d.ts +81 -0
  28. package/dist/adapter/memo/seed.d.ts.map +1 -0
  29. package/dist/adapter/props/prop-classes.d.ts +33 -0
  30. package/dist/adapter/props/prop-classes.d.ts.map +1 -0
  31. package/dist/adapter/spread/spread-codegen.d.ts +61 -0
  32. package/dist/adapter/spread/spread-codegen.d.ts.map +1 -0
  33. package/dist/adapter/value/parsed-literal.d.ts +28 -0
  34. package/dist/adapter/value/parsed-literal.d.ts.map +1 -0
  35. package/dist/build.d.ts +28 -0
  36. package/dist/build.d.ts.map +1 -0
  37. package/dist/build.js +189117 -0
  38. package/dist/conformance-pins.d.ts +12 -0
  39. package/dist/conformance-pins.d.ts.map +1 -0
  40. package/dist/index.d.ts +9 -0
  41. package/dist/index.d.ts.map +1 -0
  42. package/dist/index.js +189118 -0
  43. package/package.json +66 -0
  44. package/python/VERSION +1 -0
  45. package/python/barefootjs/__init__.py +57 -0
  46. package/python/barefootjs/backend_jinja.py +125 -0
  47. package/python/barefootjs/evaluator.py +787 -0
  48. package/python/barefootjs/runtime.py +1334 -0
  49. package/python/barefootjs/search_params.py +89 -0
  50. package/python/pyproject.toml +32 -0
  51. package/python/tests/__init__.py +0 -0
  52. package/python/tests/test_eval_vectors.py +88 -0
  53. package/python/tests/test_evaluator.py +406 -0
  54. package/python/tests/test_helper_vectors.py +288 -0
  55. package/python/tests/test_omit.py +62 -0
  56. package/python/tests/test_props_attr.py +54 -0
  57. package/python/tests/test_query.py +41 -0
  58. package/python/tests/test_render.py +102 -0
  59. package/python/tests/test_render_child.py +96 -0
  60. package/python/tests/test_search_params.py +50 -0
  61. package/python/tests/test_spread_attrs.py +86 -0
  62. package/python/tests/test_template_primitives.py +347 -0
  63. package/python/tests/vector-divergences.json +42 -0
  64. package/src/__tests__/jinja-adapter-unit.test.ts +390 -0
  65. package/src/__tests__/jinja-adapter.test.ts +53 -0
  66. package/src/__tests__/jinja-counter.test.ts +62 -0
  67. package/src/__tests__/jinja-query-href.test.ts +99 -0
  68. package/src/__tests__/jinja-spread-attrs.test.ts +225 -0
  69. package/src/adapter/analysis/component-tree.ts +119 -0
  70. package/src/adapter/boolean-result.ts +176 -0
  71. package/src/adapter/emit-context.ts +118 -0
  72. package/src/adapter/expr/array-method.ts +346 -0
  73. package/src/adapter/expr/emitters.ts +608 -0
  74. package/src/adapter/expr/operand.ts +35 -0
  75. package/src/adapter/index.ts +6 -0
  76. package/src/adapter/jinja-adapter.ts +1747 -0
  77. package/src/adapter/lib/constants.ts +33 -0
  78. package/src/adapter/lib/ir-scope.ts +95 -0
  79. package/src/adapter/lib/jinja-naming.ts +114 -0
  80. package/src/adapter/lib/types.ts +30 -0
  81. package/src/adapter/memo/seed.ts +132 -0
  82. package/src/adapter/props/prop-classes.ts +65 -0
  83. package/src/adapter/spread/spread-codegen.ts +166 -0
  84. package/src/adapter/value/parsed-literal.ts +76 -0
  85. package/src/build.ts +37 -0
  86. package/src/conformance-pins.ts +101 -0
  87. package/src/index.ts +9 -0
  88. package/src/test-render.ts +714 -0
@@ -0,0 +1,50 @@
1
+ """BarefootJS.SearchParams -- Python-specific concerns, ported from
2
+ packages/adapter-perl/t/search_params.t.
3
+
4
+ The cross-language VALUE semantics of `get` are owned by the
5
+ language-independent golden vectors (`search_params_get` in
6
+ `test_helper_vectors.py`), so Go/Perl/Python parity there is mechanical.
7
+ This file covers only what those value vectors can't: the lazy factory
8
+ seam, lenient parsing (never raises), and UTF-8 decoding.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import unittest
14
+
15
+ from barefootjs import BarefootJS, SearchParams
16
+
17
+
18
+ class SearchParamsTest(unittest.TestCase):
19
+ def test_lazy_factory(self):
20
+ sp = BarefootJS.search_params("sort=price")
21
+ self.assertIsInstance(sp, SearchParams)
22
+ self.assertEqual(sp.get("sort"), "price")
23
+ self.assertIsInstance(BarefootJS.search_params(), SearchParams)
24
+
25
+ def test_none_composition_coalesces_only_none(self):
26
+ # The adapters lower `searchParams().get(k) ?? d` to a Python
27
+ # expression that coalesces only `None` (not a bare `or`, which
28
+ # would also default a present-but-empty value) -- so an absent key
29
+ # falls back to the author's default while a present-but-empty
30
+ # value keeps ''.
31
+ absent = BarefootJS.search_params("other=x")
32
+ got = absent.get("sort")
33
+ self.assertEqual(got if got is not None else "none", "none")
34
+
35
+ empty = BarefootJS.search_params("sort=")
36
+ got = empty.get("sort")
37
+ self.assertEqual(got if got is not None else "none", "")
38
+
39
+ def test_utf8_percent_decoding(self):
40
+ sp = BarefootJS.search_params("q=%E2%9C%93")
41
+ self.assertEqual(sp.get("q"), "✓")
42
+
43
+ def test_lenient_parsing_never_raises(self):
44
+ BarefootJS.search_params(None) # should not raise
45
+ self.assertIsNone(BarefootJS.search_params("&&&").get("x"))
46
+ self.assertIsNone(BarefootJS.search_params("=novalue").get("x"))
47
+
48
+
49
+ if __name__ == "__main__":
50
+ unittest.main()
@@ -0,0 +1,86 @@
1
+ """`BarefootJS.spread_attrs` -- ported from packages/adapter-perl/t/spread_attrs.t.
2
+
3
+ JSX intrinsic-element spread runtime helper (#1407 follow-up). Mirrors the
4
+ JS `spreadAttrs` runtime and the Go/Perl adapters' equivalents so SSR output
5
+ stays byte-equal across every adapter -- cross-adapter parity regressions
6
+ surface here first.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import unittest
12
+
13
+ from barefootjs import BarefootJS
14
+
15
+
16
+ class _PureBackend:
17
+ """`spread_attrs` only reaches the backend for `mark_raw`, which is the
18
+ identity here."""
19
+
20
+ def mark_raw(self, s):
21
+ return s
22
+
23
+
24
+ def run(bag):
25
+ bf = BarefootJS(None, {"backend": _PureBackend()})
26
+ return str(bf.spread_attrs(bag))
27
+
28
+
29
+ class SpreadAttrsTest(unittest.TestCase):
30
+ def test_basic_shapes(self):
31
+ self.assertEqual(run(None), "")
32
+ self.assertEqual(run({}), "")
33
+ self.assertEqual(run("not a hash"), "")
34
+ self.assertEqual(run({"id": "a"}), 'id="a"')
35
+
36
+ def test_alphabetic_key_order(self):
37
+ self.assertEqual(run({"id": "a", "class": "on"}), 'class="on" id="a"')
38
+
39
+ def test_key_remapping(self):
40
+ self.assertEqual(run({"className": "foo"}), 'class="foo"')
41
+ self.assertEqual(run({"htmlFor": "x"}), 'for="x"')
42
+ self.assertEqual(run({"dataPriority": "high"}), 'data-priority="high"')
43
+ # SVG XML attrs are case-sensitive -- preserve verbatim.
44
+ self.assertEqual(run({"viewBox": "0 0 10 10"}), 'viewBox="0 0 10 10"')
45
+ self.assertEqual(run({"clipPathUnits": "userSpaceOnUse"}), 'clipPathUnits="userSpaceOnUse"')
46
+ # JS-reference parity (#1411): a leading uppercase letter emits a
47
+ # leading dash.
48
+ self.assertEqual(run({"XData": "x"}), '-x-data="x"')
49
+
50
+ def test_event_handlers_js_predicate_parity(self):
51
+ self.assertEqual(run({"onClick": "fn", "id": "a"}), 'id="a"')
52
+ self.assertEqual(run({"on_custom": "fn", "id": "a"}), 'id="a"')
53
+ self.assertEqual(run({"on0": "fn", "id": "a"}), 'id="a"')
54
+ self.assertEqual(run({"oncology": "x"}), 'oncology="x"')
55
+
56
+ def test_children_skipped_ref_passed_through(self):
57
+ self.assertEqual(run({"children": "x", "id": "a"}), 'id="a"')
58
+ # JS `spreadAttrs` does NOT filter `ref` (`applyRestAttrs` does --
59
+ # that's a separate divergence).
60
+ self.assertEqual(run({"ref": "x", "id": "a"}), 'id="a" ref="x"')
61
+
62
+ def test_boolean_values(self):
63
+ # Contract: callers MUST use a real Python bool for boolean
64
+ # attributes -- unlike Perl, no sentinel object is needed.
65
+ self.assertEqual(run({"hidden": True, "id": "a"}), 'hidden id="a"')
66
+ self.assertEqual(run({"hidden": False, "id": "a"}), 'id="a"')
67
+ # Plain numeric 0 renders as a value (matches `tabindex="0"`).
68
+ self.assertEqual(run({"tabindex": 0}), 'tabindex="0"')
69
+
70
+ def test_nullish_skip(self):
71
+ self.assertEqual(run({"a": None, "b": "x"}), 'b="x"')
72
+
73
+ def test_html_escape(self):
74
+ self.assertEqual(run({"title": '<b>"x"</b>'}), 'title="&lt;b&gt;&#34;x&#34;&lt;/b&gt;"')
75
+ self.assertEqual(run({"alt": "tom & jerry"}), 'alt="tom &amp; jerry"')
76
+
77
+ def test_style_object_lowering(self):
78
+ self.assertEqual(
79
+ run({"style": {"backgroundColor": "red", "color": "white"}}),
80
+ 'style="background-color:red;color:white"',
81
+ )
82
+ self.assertEqual(run({"style": "color:red"}), 'style="color:red"')
83
+
84
+
85
+ if __name__ == "__main__":
86
+ unittest.main()
@@ -0,0 +1,347 @@
1
+ """JS-compat helper coverage (#1189), ported from
2
+ packages/adapter-perl/t/template_primitives.t.
3
+
4
+ Covers the array/string method surface NOT already exercised byte-for-byte
5
+ by the shared golden vectors (`test_helper_vectors.py`) -- receiver-type
6
+ dispatch edge cases, mutation isolation (a helper must return a NEW list,
7
+ never alias the caller's), and the structured `sort` comparator dispatch.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import unittest
14
+
15
+ from barefootjs import BarefootJS
16
+
17
+
18
+ class _PureBackend:
19
+ def encode_json(self, data):
20
+ return json.dumps(data, separators=(",", ":"), sort_keys=True)
21
+
22
+ def mark_raw(self, s):
23
+ return s
24
+
25
+ def materialize(self, value):
26
+ return value() if callable(value) else value
27
+
28
+ def render_named(self, *args, **kwargs):
29
+ return ""
30
+
31
+
32
+ def _is_nan(n):
33
+ return isinstance(n, float) and n != n
34
+
35
+
36
+ class TemplatePrimitivesTest(unittest.TestCase):
37
+ def setUp(self):
38
+ self.bf = BarefootJS(None, {"backend": _PureBackend()})
39
+
40
+ def test_json(self):
41
+ self.assertEqual(self.bf.json({"a": 1}), '{"a":1}')
42
+ self.assertEqual(self.bf.json([1, 2, 3]), "[1,2,3]")
43
+ self.assertEqual(self.bf.json("hi"), '"hi"')
44
+ self.assertEqual(self.bf.json(None), "null")
45
+
46
+ def test_string(self):
47
+ self.assertEqual(self.bf.string(42), "42")
48
+ self.assertEqual(self.bf.string("hi"), "hi")
49
+ self.assertEqual(self.bf.string(None), "")
50
+ # Python has a real boolean type -- no Perl-style divergence.
51
+ self.assertEqual(self.bf.string(True), "true")
52
+ self.assertEqual(self.bf.string(False), "false")
53
+ self.assertEqual(self.bf.string(1.0), "1")
54
+
55
+ def test_number(self):
56
+ self.assertEqual(self.bf.number("3.14"), 3.14)
57
+ self.assertEqual(self.bf.number(42), 42)
58
+ self.assertTrue(_is_nan(self.bf.number("not a num")))
59
+ self.assertTrue(_is_nan(self.bf.number(None)))
60
+
61
+ def test_floor_ceil_round(self):
62
+ self.assertEqual(self.bf.floor(3.7), 3)
63
+ self.assertEqual(self.bf.floor(-3.2), -4)
64
+ self.assertTrue(_is_nan(self.bf.floor("not")))
65
+
66
+ self.assertEqual(self.bf.ceil(3.1), 4)
67
+ self.assertEqual(self.bf.ceil(-3.7), -3)
68
+ self.assertTrue(_is_nan(self.bf.ceil("not")))
69
+
70
+ self.assertEqual(self.bf.round(3.5), 4)
71
+ self.assertEqual(self.bf.round(3.4), 3)
72
+ # JS Math.round ties go toward +Infinity, not away from zero.
73
+ self.assertEqual(self.bf.round(-1.5), -1)
74
+ self.assertEqual(self.bf.round(-1.6), -2)
75
+ self.assertTrue(_is_nan(self.bf.round("not")))
76
+
77
+ def test_includes_dispatch(self):
78
+ # `Array.prototype.includes(x)` + `String.prototype.includes(sub)`
79
+ # lower to the same `bf.includes(recv, elem)` shape -- see #1448
80
+ # Tier A. `BarefootJS.includes` dispatches on `recv`'s Python type:
81
+ # a `list` scans elements via `evaluator._same_value_zero` (#2075 --
82
+ # SameValueZero, handling None/undefined parity, and no
83
+ # numeric/string coercion -- see the cross-type cases below);
84
+ # anything else (a `dict`, or a scalar) falls back to a substring
85
+ # search via `js_string` coercion.
86
+ self.assertTrue(self.bf.includes(["a", "b", "c"], "b"))
87
+ self.assertFalse(self.bf.includes(["a", "b", "c"], "z"))
88
+ self.assertTrue(self.bf.includes([1, 2, 3], 2))
89
+ self.assertFalse(self.bf.includes([], "a"))
90
+ self.assertTrue(self.bf.includes([None, "a"], None))
91
+ self.assertFalse(self.bf.includes(["a", "b"], None))
92
+
93
+ # SameValueZero never coerces across types.
94
+ self.assertFalse(self.bf.includes([2], "2"))
95
+ self.assertTrue(self.bf.includes([2], 2))
96
+ self.assertTrue(self.bf.includes(["2"], "2"))
97
+
98
+ self.assertTrue(self.bf.includes("hello world", "world"))
99
+ self.assertFalse(self.bf.includes("hello world", "earth"))
100
+ self.assertTrue(self.bf.includes("hello", ""))
101
+ self.assertFalse(self.bf.includes("", "x"))
102
+ self.assertFalse(self.bf.includes(None, "x"))
103
+
104
+ self.assertFalse(self.bf.includes({"a": 1}, "a"))
105
+
106
+ def test_index_of_last_index_of(self):
107
+ arr = ["a", "b", "c", "b", "d"]
108
+ self.assertEqual(self.bf.index_of(arr, "a"), 0)
109
+ self.assertEqual(self.bf.index_of(arr, "b"), 1)
110
+ self.assertEqual(self.bf.index_of(arr, "d"), 4)
111
+ self.assertEqual(self.bf.index_of(arr, "z"), -1)
112
+ self.assertEqual(self.bf.index_of([], "a"), -1)
113
+ self.assertEqual(self.bf.index_of("not an array", "a"), -1)
114
+
115
+ self.assertEqual(self.bf.last_index_of(arr, "b"), 3)
116
+ self.assertEqual(self.bf.last_index_of(arr, "a"), 0)
117
+ self.assertEqual(self.bf.last_index_of(arr, "z"), -1)
118
+
119
+ self.assertEqual(self.bf.index_of([None, "x", None], None), 0)
120
+ self.assertEqual(self.bf.last_index_of([None, "x", None], None), 2)
121
+
122
+ def test_at(self):
123
+ arr = ["a", "b", "c"]
124
+ self.assertEqual(self.bf.at(arr, 0), "a")
125
+ self.assertEqual(self.bf.at(arr, 2), "c")
126
+ self.assertEqual(self.bf.at(arr, -1), "c")
127
+ self.assertEqual(self.bf.at(arr, -3), "a")
128
+ self.assertIsNone(self.bf.at(arr, 3))
129
+ self.assertIsNone(self.bf.at(arr, -4))
130
+ self.assertIsNone(self.bf.at([], 0))
131
+ self.assertIsNone(self.bf.at(None, 0))
132
+ self.assertIsNone(self.bf.at({"a": 1}, 0))
133
+
134
+ def test_concat_mutation_isolation(self):
135
+ self.assertEqual(self.bf.concat(["a", "b"], ["c", "d"]), ["a", "b", "c", "d"])
136
+ self.assertEqual(self.bf.concat(None, ["a"]), ["a"])
137
+ self.assertEqual(self.bf.concat(["a"], None), ["a"])
138
+
139
+ left = ["a", "b"]
140
+ right = ["c", "d"]
141
+ out = self.bf.concat(left, right)
142
+ out.append("mutated")
143
+ self.assertEqual(left, ["a", "b"])
144
+ self.assertEqual(right, ["c", "d"])
145
+
146
+ def test_slice_mutation_isolation_and_clamping(self):
147
+ arr = ["a", "b", "c", "d", "e"]
148
+ self.assertEqual(self.bf.slice(arr, 1, 3), ["b", "c"])
149
+ self.assertEqual(self.bf.slice(arr, 2, None), ["c", "d", "e"])
150
+ self.assertEqual(self.bf.slice(arr, -2, None), ["d", "e"])
151
+ self.assertEqual(self.bf.slice(arr, 0, -1), ["a", "b", "c", "d"])
152
+ self.assertEqual(self.bf.slice(arr, 100, None), [])
153
+ self.assertEqual(self.bf.slice(arr, 3, 1), [])
154
+ self.assertEqual(self.bf.slice(None, 0, None), [])
155
+
156
+ src = ["a", "b", "c"]
157
+ out = self.bf.slice(src, 0, 2)
158
+ out.append("mutated")
159
+ self.assertEqual(src, ["a", "b", "c"])
160
+
161
+ def test_reverse_mutation_isolation(self):
162
+ self.assertEqual(self.bf.reverse(["a", "b", "c"]), ["c", "b", "a"])
163
+ self.assertEqual(self.bf.reverse([]), [])
164
+
165
+ src = ["a", "b", "c"]
166
+ out = self.bf.reverse(src)
167
+ out.append("mutated")
168
+ self.assertEqual(src, ["a", "b", "c"])
169
+ self.assertEqual(self.bf.reverse(None), [])
170
+
171
+ def test_trim(self):
172
+ self.assertEqual(self.bf.trim(" padded "), "padded")
173
+ self.assertEqual(self.bf.trim(""), "")
174
+ self.assertEqual(self.bf.trim(None), "")
175
+ self.assertEqual(self.bf.trim({"a": 1}), "")
176
+ self.assertEqual(self.bf.trim(42), "42")
177
+
178
+ def test_split(self):
179
+ self.assertEqual(self.bf.split("a,b,c", ","), ["a", "b", "c"])
180
+ self.assertEqual(self.bf.split("a.b.c", "."), ["a", "b", "c"])
181
+ self.assertEqual(self.bf.split("a,", ","), ["a", ""])
182
+ self.assertEqual(self.bf.split(",a", ","), ["", "a"])
183
+ self.assertEqual(self.bf.split("abc", ""), ["a", "b", "c"])
184
+ self.assertEqual(self.bf.split("", ""), [])
185
+ self.assertEqual(self.bf.split("abc", ","), ["abc"])
186
+ self.assertEqual(self.bf.split("a,b,c"), ["a,b,c"])
187
+ self.assertEqual(self.bf.split("a,b,c,d", ",", 2), ["a", "b"])
188
+ self.assertEqual(self.bf.split("a,b", ",", 0), [])
189
+ self.assertEqual(self.bf.split(None, ","), [""])
190
+ self.assertEqual(self.bf.split(42, ","), ["42"])
191
+
192
+ def test_starts_with_ends_with_positions(self):
193
+ self.assertTrue(self.bf.starts_with("hello world", "hello"))
194
+ self.assertTrue(self.bf.starts_with("anything", ""))
195
+ self.assertTrue(self.bf.ends_with("hello world", "world"))
196
+ self.assertTrue(self.bf.starts_with("abc", "b", 1))
197
+ self.assertFalse(self.bf.starts_with("abc", "a", 99))
198
+ self.assertTrue(self.bf.starts_with("abc", "a", -5))
199
+ self.assertTrue(self.bf.ends_with("abc", "b", 2))
200
+ self.assertTrue(self.bf.ends_with("abc", "c", 99))
201
+ self.assertFalse(self.bf.ends_with("abc", "a", -1))
202
+
203
+ def test_replace(self):
204
+ self.assertEqual(self.bf.replace("hello world", "o", "0"), "hell0 world")
205
+ self.assertEqual(self.bf.replace("abc", "", "X"), "Xabc")
206
+ self.assertEqual(self.bf.replace("ab", "a", "$&"), "$&b")
207
+
208
+ def test_repeat(self):
209
+ self.assertEqual(self.bf.repeat("ab", 3), "ababab")
210
+ self.assertEqual(self.bf.repeat("ab", 0), "")
211
+ self.assertEqual(self.bf.repeat("ab", -2), "")
212
+ self.assertEqual(self.bf.repeat("ab", 2.9), "abab")
213
+
214
+ def test_pad_start_pad_end(self):
215
+ self.assertEqual(self.bf.pad_start("42", 5, "0"), "00042")
216
+ self.assertEqual(self.bf.pad_end("42", 5, "."), "42...")
217
+ self.assertEqual(self.bf.pad_start("42", 5), " 42")
218
+ self.assertEqual(self.bf.pad_start("x", 5, "ab"), "ababx")
219
+ self.assertEqual(self.bf.pad_start("hello", 3, "0"), "hello")
220
+ self.assertEqual(self.bf.pad_start("42", 5, ""), "42")
221
+ self.assertEqual(self.bf.pad_start("7", 4.9, "0"), "0007")
222
+
223
+ def test_sort_structured_comparator_dispatch(self):
224
+ items = [
225
+ {"name": "c", "price": 30},
226
+ {"name": "a", "price": 10},
227
+ {"name": "b", "price": 20},
228
+ ]
229
+ self.assertEqual(
230
+ self.bf.sort(
231
+ items,
232
+ {"keys": [{"key_kind": "field", "key": "price", "compare_type": "numeric", "direction": "asc"}]},
233
+ ),
234
+ [{"name": "a", "price": 10}, {"name": "b", "price": 20}, {"name": "c", "price": 30}],
235
+ )
236
+ self.assertEqual(
237
+ self.bf.sort(
238
+ items,
239
+ {"keys": [{"key_kind": "field", "key": "price", "compare_type": "numeric", "direction": "desc"}]},
240
+ ),
241
+ [{"name": "c", "price": 30}, {"name": "b", "price": 20}, {"name": "a", "price": 10}],
242
+ )
243
+ self.assertEqual(
244
+ self.bf.sort([3, 1, 2], {"keys": [{"key_kind": "self", "compare_type": "numeric", "direction": "asc"}]}),
245
+ [1, 2, 3],
246
+ )
247
+
248
+ # Mutation isolation.
249
+ src = [{"price": 3}, {"price": 1}, {"price": 2}]
250
+ out = self.bf.sort(
251
+ src, {"keys": [{"key_kind": "field", "key": "price", "compare_type": "numeric", "direction": "asc"}]}
252
+ )
253
+ out.append({"price": 99})
254
+ self.assertEqual(src, [{"price": 3}, {"price": 1}, {"price": 2}])
255
+
256
+ self.assertEqual(
257
+ self.bf.sort(None, {"keys": [{"key_kind": "self", "compare_type": "numeric", "direction": "asc"}]}), []
258
+ )
259
+ self.assertEqual(self.bf.sort([], {"keys": [{"key_kind": "field", "key": "price"}]}), [])
260
+
261
+ def test_sort_multi_key_tie_break(self):
262
+ items = [{"p": 1, "name": "b"}, {"p": 1, "name": "a"}, {"p": 0, "name": "c"}]
263
+ self.assertEqual(
264
+ self.bf.sort(
265
+ items,
266
+ {
267
+ "keys": [
268
+ {"key_kind": "field", "key": "p", "compare_type": "numeric", "direction": "asc"},
269
+ {"key_kind": "field", "key": "name", "compare_type": "string", "direction": "asc"},
270
+ ]
271
+ },
272
+ ),
273
+ [{"p": 0, "name": "c"}, {"p": 1, "name": "a"}, {"p": 1, "name": "b"}],
274
+ )
275
+
276
+ def test_sort_auto_compare(self):
277
+ self.assertEqual(
278
+ self.bf.sort([3, 1, 2], {"keys": [{"key_kind": "self", "compare_type": "auto", "direction": "asc"}]}),
279
+ [1, 2, 3],
280
+ )
281
+ self.assertEqual(
282
+ self.bf.sort(
283
+ ["charlie", "alice", "bob"],
284
+ {"keys": [{"key_kind": "self", "compare_type": "auto", "direction": "asc"}]},
285
+ ),
286
+ ["alice", "bob", "charlie"],
287
+ )
288
+
289
+
290
+ class EvalDelegationTest(unittest.TestCase):
291
+ """Verifies `bf.*_eval` methods correctly delegate to the `evaluator`
292
+ module (JSON-string seam wiring), not just the underlying evaluator
293
+ functions tested directly in test_evaluator.py / test_eval_vectors.py."""
294
+
295
+ def setUp(self):
296
+ self.bf = BarefootJS(None, {"backend": _PureBackend()})
297
+
298
+ def _node(self, **kwargs):
299
+ return kwargs
300
+
301
+ def test_sort_eval(self):
302
+ cmp = self._node(
303
+ kind="binary",
304
+ op="-",
305
+ left={"kind": "member", "object": {"kind": "identifier", "name": "a"}, "property": "v"},
306
+ right={"kind": "member", "object": {"kind": "identifier", "name": "b"}, "property": "v"},
307
+ )
308
+ out = self.bf.sort_eval([{"v": 3}, {"v": 1}, {"v": 2}], json.dumps(cmp), "a", "b")
309
+ self.assertEqual([x["v"] for x in out], [1, 2, 3])
310
+
311
+ def test_reduce_eval(self):
312
+ body = self._node(
313
+ kind="binary",
314
+ op="+",
315
+ left={"kind": "identifier", "name": "acc"},
316
+ right={"kind": "identifier", "name": "item"},
317
+ )
318
+ out = self.bf.reduce_eval([1, 2, 3], json.dumps(body), "acc", "item", 0)
319
+ self.assertEqual(out, 6)
320
+
321
+ def test_filter_every_some_find_eval(self):
322
+ pred = self._node(
323
+ kind="binary",
324
+ op=">=",
325
+ left={"kind": "member", "object": {"kind": "identifier", "name": "u"}, "property": "age"},
326
+ right={"kind": "literal", "value": 18},
327
+ )
328
+ pred_json = json.dumps(pred)
329
+ rows = [{"age": 15}, {"age": 30}, {"age": 18}]
330
+ self.assertEqual([r["age"] for r in self.bf.filter_eval(rows, pred_json, "u")], [30, 18])
331
+ self.assertFalse(self.bf.every_eval(rows, pred_json, "u"))
332
+ self.assertTrue(self.bf.some_eval(rows, pred_json, "u"))
333
+ self.assertEqual(self.bf.find_eval(rows, pred_json, "u")["age"], 30)
334
+ self.assertEqual(self.bf.find_index_eval(rows, pred_json, "u", forward=False), 2)
335
+
336
+ def test_flat_map_eval_and_map_eval(self):
337
+ field = self._node(kind="member", object={"kind": "identifier", "name": "i"}, property="tags")
338
+ rows = [{"tags": ["a", "b"]}, {"tags": ["c"]}]
339
+ self.assertEqual(self.bf.flat_map_eval(rows, json.dumps(field), "i"), ["a", "b", "c"])
340
+
341
+ name_field = self._node(kind="member", object={"kind": "identifier", "name": "u"}, property="name")
342
+ users = [{"name": "Ada"}, {"name": "Grace"}]
343
+ self.assertEqual(self.bf.map_eval(users, json.dumps(name_field), "u"), ["Ada", "Grace"])
344
+
345
+
346
+ if __name__ == "__main__":
347
+ unittest.main()
@@ -0,0 +1,42 @@
1
+ {
2
+ "version": 1,
3
+ "backend": "python",
4
+ "runner": "packages/adapter-jinja/python/tests/test_helper_vectors.py",
5
+ "spec": "spec/template-helpers.md",
6
+ "divergences": {
7
+ "add/beyond the safe-integer edge rounds as a double": {
8
+ "expect": 9007199254740993,
9
+ "reason": "Python int arithmetic is exact (arbitrary precision), not double-rounded"
10
+ },
11
+ "div/zero divisor yields Infinity": {
12
+ "throws": true,
13
+ "exception": "ZeroDivisionError",
14
+ "reason": "Python native / raises ZeroDivisionError on a zero divisor"
15
+ },
16
+ "number/empty string coerces to 0": {
17
+ "expect": { "$num": "NaN" },
18
+ "reason": "deliberate: empty input must not silently zero downstream arithmetic (matches the Perl port)"
19
+ },
20
+ "number/null coerces to 0": {
21
+ "expect": { "$num": "NaN" },
22
+ "reason": "deliberate: unset props must not silently zero downstream arithmetic (matches the Perl port)"
23
+ },
24
+ "string/null renders as the string \"null\"": {
25
+ "expect": "",
26
+ "reason": "deliberate: an unset prop must not surface a literal \"null\" in HTML (matches the Perl port)"
27
+ },
28
+ "sort/localeCompare orders case-insensitively (ICU collation)": {
29
+ "expect": ["B", "a"],
30
+ "reason": "Python str comparison is codepoint order, not ICU collation"
31
+ },
32
+ "sort/relational compare on numeric strings is lexical": {
33
+ "expect": ["9", "10"],
34
+ "reason": "the \"auto\" compare goes numeric when both keys look_like_number (Perl/Go parity)"
35
+ },
36
+ "reduce/numeric-string items concatenate under JS +": {
37
+ "expect": 11.0,
38
+ "reason": "numeric folds parse numeric strings instead of concatenating (Perl/Go parity)"
39
+ }
40
+ },
41
+ "unsupported": {}
42
+ }