@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.
- package/README.md +66 -0
- package/dist/adapter/analysis/component-tree.d.ts +26 -0
- package/dist/adapter/analysis/component-tree.d.ts.map +1 -0
- package/dist/adapter/boolean-result.d.ts +84 -0
- package/dist/adapter/boolean-result.d.ts.map +1 -0
- package/dist/adapter/emit-context.d.ts +106 -0
- package/dist/adapter/emit-context.d.ts.map +1 -0
- package/dist/adapter/expr/array-method.d.ts +75 -0
- package/dist/adapter/expr/array-method.d.ts.map +1 -0
- package/dist/adapter/expr/emitters.d.ts +143 -0
- package/dist/adapter/expr/emitters.d.ts.map +1 -0
- package/dist/adapter/expr/operand.d.ts +25 -0
- package/dist/adapter/expr/operand.d.ts.map +1 -0
- package/dist/adapter/index.d.ts +6 -0
- package/dist/adapter/index.d.ts.map +1 -0
- package/dist/adapter/index.js +189097 -0
- package/dist/adapter/jinja-adapter.d.ts +394 -0
- package/dist/adapter/jinja-adapter.d.ts.map +1 -0
- package/dist/adapter/lib/constants.d.ts +21 -0
- package/dist/adapter/lib/constants.d.ts.map +1 -0
- package/dist/adapter/lib/ir-scope.d.ts +50 -0
- package/dist/adapter/lib/ir-scope.d.ts.map +1 -0
- package/dist/adapter/lib/jinja-naming.d.ts +77 -0
- package/dist/adapter/lib/jinja-naming.d.ts.map +1 -0
- package/dist/adapter/lib/types.d.ts +27 -0
- package/dist/adapter/lib/types.d.ts.map +1 -0
- package/dist/adapter/memo/seed.d.ts +81 -0
- package/dist/adapter/memo/seed.d.ts.map +1 -0
- package/dist/adapter/props/prop-classes.d.ts +33 -0
- package/dist/adapter/props/prop-classes.d.ts.map +1 -0
- package/dist/adapter/spread/spread-codegen.d.ts +61 -0
- package/dist/adapter/spread/spread-codegen.d.ts.map +1 -0
- package/dist/adapter/value/parsed-literal.d.ts +28 -0
- package/dist/adapter/value/parsed-literal.d.ts.map +1 -0
- package/dist/build.d.ts +28 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +189117 -0
- package/dist/conformance-pins.d.ts +12 -0
- package/dist/conformance-pins.d.ts.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +189118 -0
- package/package.json +66 -0
- package/python/VERSION +1 -0
- package/python/barefootjs/__init__.py +57 -0
- package/python/barefootjs/backend_jinja.py +125 -0
- package/python/barefootjs/evaluator.py +787 -0
- package/python/barefootjs/runtime.py +1334 -0
- package/python/barefootjs/search_params.py +89 -0
- package/python/pyproject.toml +32 -0
- package/python/tests/__init__.py +0 -0
- package/python/tests/test_eval_vectors.py +88 -0
- package/python/tests/test_evaluator.py +406 -0
- package/python/tests/test_helper_vectors.py +288 -0
- package/python/tests/test_omit.py +62 -0
- package/python/tests/test_props_attr.py +54 -0
- package/python/tests/test_query.py +41 -0
- package/python/tests/test_render.py +102 -0
- package/python/tests/test_render_child.py +96 -0
- package/python/tests/test_search_params.py +50 -0
- package/python/tests/test_spread_attrs.py +86 -0
- package/python/tests/test_template_primitives.py +347 -0
- package/python/tests/vector-divergences.json +42 -0
- package/src/__tests__/jinja-adapter-unit.test.ts +390 -0
- package/src/__tests__/jinja-adapter.test.ts +53 -0
- package/src/__tests__/jinja-counter.test.ts +62 -0
- package/src/__tests__/jinja-query-href.test.ts +99 -0
- package/src/__tests__/jinja-spread-attrs.test.ts +225 -0
- package/src/adapter/analysis/component-tree.ts +119 -0
- package/src/adapter/boolean-result.ts +176 -0
- package/src/adapter/emit-context.ts +118 -0
- package/src/adapter/expr/array-method.ts +346 -0
- package/src/adapter/expr/emitters.ts +608 -0
- package/src/adapter/expr/operand.ts +35 -0
- package/src/adapter/index.ts +6 -0
- package/src/adapter/jinja-adapter.ts +1747 -0
- package/src/adapter/lib/constants.ts +33 -0
- package/src/adapter/lib/ir-scope.ts +95 -0
- package/src/adapter/lib/jinja-naming.ts +114 -0
- package/src/adapter/lib/types.ts +30 -0
- package/src/adapter/memo/seed.ts +132 -0
- package/src/adapter/props/prop-classes.ts +65 -0
- package/src/adapter/spread/spread-codegen.ts +166 -0
- package/src/adapter/value/parsed-literal.ts +76 -0
- package/src/build.ts +37 -0
- package/src/conformance-pins.ts +101 -0
- package/src/index.ts +9 -0
- package/src/test-render.ts +714 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"""Golden helper-vector conformance, ported from
|
|
2
|
+
packages/adapter-perl/t/helper_vectors.t.
|
|
3
|
+
|
|
4
|
+
Runs `packages/adapter-tests/vectors/vectors.json` -- generated from
|
|
5
|
+
the JS reference implementations (spec/template-helpers.md) -- against this
|
|
6
|
+
package's `BarefootJS` runtime. One binding per canonical helper id in the
|
|
7
|
+
spec catalogue, bound to the exact code shape a compiled Jinja template
|
|
8
|
+
would execute (a `bf.<method>(...)` call, or the native Python operator the
|
|
9
|
+
adapter emits for `add`/`sub`/`mul`/`div`/`neg`, mirroring how
|
|
10
|
+
`helper_vectors.t` binds those to native Perl operators).
|
|
11
|
+
|
|
12
|
+
Per spec/template-helpers.md's "Adapter status model", this backend's
|
|
13
|
+
divergences from the JS-normative expect live in
|
|
14
|
+
`tests/vector-divergences.json` (package-local, next to this file) -- keyed
|
|
15
|
+
by `fn/note`, mirroring the Perl harness's `%DIVERGENCES` table exactly in
|
|
16
|
+
spirit (values differ where Python's actual behaviour differs from Perl's).
|
|
17
|
+
This harness still fails on stale or dead declarations in that file.
|
|
18
|
+
|
|
19
|
+
Skipped everywhere the golden vectors file isn't available (i.e. outside a
|
|
20
|
+
monorepo checkout), matching the Perl/Go harnesses' `skip_all` policy.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import builtins
|
|
26
|
+
import json
|
|
27
|
+
import math
|
|
28
|
+
import os
|
|
29
|
+
import unittest
|
|
30
|
+
|
|
31
|
+
from barefootjs import BarefootJS
|
|
32
|
+
from barefootjs.backend_jinja import default_json_encoder
|
|
33
|
+
|
|
34
|
+
VECTORS_PATH = os.path.join(
|
|
35
|
+
os.path.dirname(__file__), "..", "..", "..", "adapter-tests", "vectors", "vectors.json"
|
|
36
|
+
)
|
|
37
|
+
DIVERGENCES_PATH = os.path.join(os.path.dirname(__file__), "vector-divergences.json")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class _PureBackend:
|
|
41
|
+
"""Pure-Python backend (no Jinja Environment needed) -- `json` is the
|
|
42
|
+
only helper that reaches the backend."""
|
|
43
|
+
|
|
44
|
+
def encode_json(self, data):
|
|
45
|
+
return default_json_encoder(data)
|
|
46
|
+
|
|
47
|
+
def mark_raw(self, s):
|
|
48
|
+
return s
|
|
49
|
+
|
|
50
|
+
def materialize(self, value):
|
|
51
|
+
return value() if callable(value) else value
|
|
52
|
+
|
|
53
|
+
def render_named(self, *args, **kwargs):
|
|
54
|
+
return ""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
bf = BarefootJS(None, {"backend": _PureBackend()})
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _truthy_pred(field):
|
|
61
|
+
return lambda item: item.get(field) if isinstance(item, dict) else None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _field_eq_pred(field, value):
|
|
65
|
+
# Python's `==` already distinguishes JS-typed operands correctly
|
|
66
|
+
# (int 2 != str "2"), unlike Perl's `eq`-based approach which needed a
|
|
67
|
+
# looks_like_number dispatch to fake the distinction -- see the
|
|
68
|
+
# runtime.py module docstring's `includes` divergence note.
|
|
69
|
+
return lambda item: isinstance(item, dict) and item.get(field) == value
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _bind_sort(recv, *spec):
|
|
73
|
+
keys = []
|
|
74
|
+
spec = list(spec)
|
|
75
|
+
while len(spec) >= 4:
|
|
76
|
+
kind, name, compare_type, direction = spec[:4]
|
|
77
|
+
spec = spec[4:]
|
|
78
|
+
keys.append(
|
|
79
|
+
{"key_kind": kind, "key": name, "compare_type": compare_type, "direction": direction}
|
|
80
|
+
)
|
|
81
|
+
return bf.sort(recv, {"keys": keys})
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _bind_reduce(recv, op, key_kind, key, rtype, init, direction):
|
|
85
|
+
seed = float(init) if rtype == "numeric" else init
|
|
86
|
+
return bf.reduce(
|
|
87
|
+
recv,
|
|
88
|
+
{"op": op, "key_kind": key_kind, "key": key, "type": rtype, "init": seed, "direction": direction},
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _bind_flat_map_tuple(recv, *flat):
|
|
93
|
+
flat = list(flat)
|
|
94
|
+
specs = []
|
|
95
|
+
while len(flat) >= 2:
|
|
96
|
+
specs.append((flat[0], flat[1]))
|
|
97
|
+
flat = flat[2:]
|
|
98
|
+
return bf.flat_map_tuple(recv, *specs)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# One binding per canonical helper id. `add`/`sub`/`mul`/`div`/`neg` are not
|
|
102
|
+
# BarefootJS methods (matching the Perl port -- the adapter lowers JS
|
|
103
|
+
# `+`/`-`/`*`/`/`/unary `-` to the native template-language operator, not a
|
|
104
|
+
# `bf.` call); `mod` DOES route through `bf.mod` since the plan requires the
|
|
105
|
+
# Jinja emitter to use `bf.mod` for JS `%` (Python `%` differs from JS `%` on
|
|
106
|
+
# negative operands, unlike native `+`/`-`/`*`/`/`).
|
|
107
|
+
BINDINGS = {
|
|
108
|
+
"add": lambda a, b: a + b,
|
|
109
|
+
"sub": lambda a, b: a - b,
|
|
110
|
+
"mul": lambda a, b: a * b,
|
|
111
|
+
"div": lambda a, b: a / b,
|
|
112
|
+
"mod": lambda a, b: bf.mod(a, b),
|
|
113
|
+
"neg": lambda a: -a,
|
|
114
|
+
"string": bf.string,
|
|
115
|
+
"json": bf.json,
|
|
116
|
+
"number": bf.number,
|
|
117
|
+
"floor": bf.floor,
|
|
118
|
+
"ceil": bf.ceil,
|
|
119
|
+
"round": bf.round,
|
|
120
|
+
"to_fixed": lambda *a: bf.to_fixed(*a),
|
|
121
|
+
"lower": bf.lc,
|
|
122
|
+
"upper": bf.uc,
|
|
123
|
+
"trim": bf.trim,
|
|
124
|
+
"starts_with": lambda *a: bf.starts_with(*a),
|
|
125
|
+
"ends_with": lambda *a: bf.ends_with(*a),
|
|
126
|
+
"replace": lambda *a: bf.replace(*a),
|
|
127
|
+
"repeat": lambda *a: bf.repeat(*a),
|
|
128
|
+
"pad_start": lambda *a: bf.pad_start(*a),
|
|
129
|
+
"pad_end": lambda *a: bf.pad_end(*a),
|
|
130
|
+
"split": lambda *a: bf.split(*a),
|
|
131
|
+
"len": bf.length,
|
|
132
|
+
"at": lambda *a: bf.at(*a),
|
|
133
|
+
"includes": lambda *a: bf.includes(*a),
|
|
134
|
+
"index_of": lambda *a: bf.index_of(*a),
|
|
135
|
+
"last_index_of": lambda *a: bf.last_index_of(*a),
|
|
136
|
+
"concat": lambda a, b: bf.concat(a, b),
|
|
137
|
+
# The adapters always pass three value args (None for an absent end) --
|
|
138
|
+
# mirror that exact shape, like helper_vectors.t does.
|
|
139
|
+
"slice": lambda recv, start, end=None: bf.slice(recv, start, end),
|
|
140
|
+
"reverse": bf.reverse,
|
|
141
|
+
"flat": lambda *a: bf.flat(*a),
|
|
142
|
+
"flat_dynamic": lambda *a: bf.flat_dynamic(*a),
|
|
143
|
+
"join": lambda *a: bf.join(*a),
|
|
144
|
+
"arr": lambda *a: list(a),
|
|
145
|
+
# Mirrors the JS `arr.filter(Boolean)` lowering, using JS truthiness.
|
|
146
|
+
"filter_truthy": lambda arr: [x for x in arr if bf.truthy(x)],
|
|
147
|
+
"search_params_get": lambda q, k: BarefootJS.search_params(q).get(k),
|
|
148
|
+
"query": lambda *a: bf.query(*a),
|
|
149
|
+
# Higher-order entries arrive in the canonical projection form (spec:
|
|
150
|
+
# items + field [+ value]); rebuild the predicate the adapter compiles.
|
|
151
|
+
"every": lambda items, field: bf.every(items, _truthy_pred(field)),
|
|
152
|
+
"some": lambda items, field: bf.some(items, _truthy_pred(field)),
|
|
153
|
+
"filter": lambda items, field, value: bf.filter(items, _field_eq_pred(field, value)),
|
|
154
|
+
"find": lambda items, field, value: bf.find(items, _field_eq_pred(field, value)),
|
|
155
|
+
"find_index": lambda items, field, value: bf.find_index(items, _field_eq_pred(field, value)),
|
|
156
|
+
"find_last": lambda items, field, value: bf.find_last(items, _field_eq_pred(field, value)),
|
|
157
|
+
"find_last_index": lambda items, field, value: bf.find_last_index(
|
|
158
|
+
items, _field_eq_pred(field, value)
|
|
159
|
+
),
|
|
160
|
+
"sort": _bind_sort,
|
|
161
|
+
"reduce": _bind_reduce,
|
|
162
|
+
"flat_map": lambda *a: bf.flat_map(*a),
|
|
163
|
+
"flat_map_tuple": _bind_flat_map_tuple,
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
# Per-backend status declarations (spec/template-helpers.md "Adapter status
|
|
167
|
+
# model"), loaded from tests/vector-divergences.json (package-local, always
|
|
168
|
+
# present regardless of whether the golden vectors themselves are available
|
|
169
|
+
# outside the monorepo checkout). Forms:
|
|
170
|
+
# {"expect": <value>} assert the pinned value (exact, no
|
|
171
|
+
# numeric coercion -- deliberately
|
|
172
|
+
# stricter than the spec's
|
|
173
|
+
# value-compat `_match` so an
|
|
174
|
+
# int-vs-float rounding accident
|
|
175
|
+
# can't hide behind the comparison)
|
|
176
|
+
# {"expect": {"$num": "NaN"}} assert a real NaN result
|
|
177
|
+
# {"throws": true, "exception": <n>} assert the call raises the named
|
|
178
|
+
# builtin exception (default
|
|
179
|
+
# `Exception` if `exception` absent)
|
|
180
|
+
with open(DIVERGENCES_PATH, encoding="utf-8") as _fh:
|
|
181
|
+
_divergences_doc = json.load(_fh)
|
|
182
|
+
DIVERGENCES = _divergences_doc["divergences"]
|
|
183
|
+
UNSUPPORTED = _divergences_doc["unsupported"]
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _match(got, expect):
|
|
187
|
+
"""Spec value-compat comparison against a JSON-decoded expect --
|
|
188
|
+
sentinel hashes, booleans by truthiness, numbers numerically,
|
|
189
|
+
arrays/hashes recursively."""
|
|
190
|
+
if expect is None:
|
|
191
|
+
return got is None
|
|
192
|
+
if isinstance(expect, dict) and "$num" in expect:
|
|
193
|
+
kind = expect["$num"]
|
|
194
|
+
if isinstance(got, bool) or not isinstance(got, (int, float)):
|
|
195
|
+
return False
|
|
196
|
+
g = float(got)
|
|
197
|
+
if kind == "NaN":
|
|
198
|
+
return g != g
|
|
199
|
+
inf = float("inf")
|
|
200
|
+
return g == (inf if kind == "Infinity" else -inf)
|
|
201
|
+
if isinstance(expect, bool):
|
|
202
|
+
return bool(got) == expect
|
|
203
|
+
if isinstance(expect, list):
|
|
204
|
+
if not isinstance(got, list) or len(got) != len(expect):
|
|
205
|
+
return False
|
|
206
|
+
return all(_match(g, e) for g, e in zip(got, expect))
|
|
207
|
+
if isinstance(expect, dict):
|
|
208
|
+
if not isinstance(got, dict) or len(got) != len(expect):
|
|
209
|
+
return False
|
|
210
|
+
return all(k in got and _match(got[k], v) for k, v in expect.items())
|
|
211
|
+
if got is None or isinstance(got, (list, dict)):
|
|
212
|
+
return False
|
|
213
|
+
if isinstance(expect, (int, float)) and not isinstance(expect, bool):
|
|
214
|
+
if isinstance(got, bool) or not isinstance(got, (int, float)):
|
|
215
|
+
return False
|
|
216
|
+
# Exact comparison when both are Python `int` (JSON-integral values
|
|
217
|
+
# decode to `int`): a float() cast would silently round a huge
|
|
218
|
+
# integer to its nearest float64 and could mask a genuine mismatch
|
|
219
|
+
# (e.g. the safe-integer-edge divergence, where the wrong exact int
|
|
220
|
+
# happens to float-round to the right answer).
|
|
221
|
+
if isinstance(got, int) and isinstance(expect, int):
|
|
222
|
+
return got == expect
|
|
223
|
+
return float(got) == float(expect)
|
|
224
|
+
return got == expect
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@unittest.skipUnless(os.path.exists(VECTORS_PATH), "golden vectors not available outside the monorepo checkout")
|
|
228
|
+
class HelperVectorsTest(unittest.TestCase):
|
|
229
|
+
def test_vectors(self):
|
|
230
|
+
with open(VECTORS_PATH, encoding="utf-8") as fh:
|
|
231
|
+
doc = json.load(fh)
|
|
232
|
+
|
|
233
|
+
self.assertTrue(doc["cases"], "vectors.json contains no cases")
|
|
234
|
+
seen_declarations = set()
|
|
235
|
+
|
|
236
|
+
for case in doc["cases"]:
|
|
237
|
+
fn, note, args, expect = case["fn"], case["note"], case["args"], case["expect"]
|
|
238
|
+
key = f"{fn}/{note}"
|
|
239
|
+
with self.subTest(key=key):
|
|
240
|
+
if fn in UNSUPPORTED:
|
|
241
|
+
self.skipTest(f"unsupported on this backend: {UNSUPPORTED[fn]}")
|
|
242
|
+
continue
|
|
243
|
+
bind = BINDINGS.get(fn)
|
|
244
|
+
self.assertIsNotNone(bind, f"no Python binding for helper '{fn}' -- add it to BINDINGS")
|
|
245
|
+
|
|
246
|
+
divergence = DIVERGENCES.get(key)
|
|
247
|
+
if divergence and divergence.get("throws"):
|
|
248
|
+
seen_declarations.add(key)
|
|
249
|
+
exception_name = divergence.get("exception")
|
|
250
|
+
exception_cls = getattr(builtins, exception_name) if exception_name else Exception
|
|
251
|
+
with self.assertRaises(
|
|
252
|
+
exception_cls,
|
|
253
|
+
msg=f"{key} (declared divergence: {divergence['reason']})",
|
|
254
|
+
):
|
|
255
|
+
bind(*args)
|
|
256
|
+
continue
|
|
257
|
+
|
|
258
|
+
try:
|
|
259
|
+
got = bind(*args)
|
|
260
|
+
except Exception as exc: # noqa: BLE001
|
|
261
|
+
self.fail(f"{key} raised unexpectedly: {exc!r}")
|
|
262
|
+
continue
|
|
263
|
+
|
|
264
|
+
if divergence:
|
|
265
|
+
seen_declarations.add(key)
|
|
266
|
+
label = f"{key} (declared divergence: {divergence['reason']})"
|
|
267
|
+
self.assertFalse(
|
|
268
|
+
_match(got, expect),
|
|
269
|
+
f"stale divergence declaration for '{key}' -- the backend now matches JS; remove it",
|
|
270
|
+
)
|
|
271
|
+
want = divergence["expect"]
|
|
272
|
+
if isinstance(want, dict) and want.get("$num") == "NaN":
|
|
273
|
+
self.assertTrue(
|
|
274
|
+
isinstance(got, float) and got != got,
|
|
275
|
+
f"{label}: got {got!r}, wanted real NaN",
|
|
276
|
+
)
|
|
277
|
+
else:
|
|
278
|
+
self.assertEqual(got, want, label)
|
|
279
|
+
continue
|
|
280
|
+
|
|
281
|
+
self.assertTrue(_match(got, expect), f"{key}: got {got!r}, want {expect!r}")
|
|
282
|
+
|
|
283
|
+
stale = [k for k in DIVERGENCES if k not in seen_declarations]
|
|
284
|
+
self.assertEqual(stale, [], f"divergence declarations match no vector case -- renamed note? {stale}")
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
if __name__ == "__main__":
|
|
288
|
+
unittest.main()
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""`BarefootJS.omit` -- the object-rest `.map()` destructure residual-object
|
|
2
|
+
helper (#2087 Phase B). Companion to `test_spread_attrs.py`: `omit` builds
|
|
3
|
+
the TRUE residual dict (`{ id, title, ...rest }` -> `bf.omit(item, ['id',
|
|
4
|
+
'title'])`) that `rest.flag` member reads and `bf.spread_attrs(rest)`
|
|
5
|
+
forwarding both read from.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import unittest
|
|
11
|
+
|
|
12
|
+
from barefootjs import BarefootJS
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class _PureBackend:
|
|
16
|
+
def mark_raw(self, s):
|
|
17
|
+
return s
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def run(recv, keys):
|
|
21
|
+
bf = BarefootJS(None, {"backend": _PureBackend()})
|
|
22
|
+
return bf.omit(recv, keys)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class OmitTest(unittest.TestCase):
|
|
26
|
+
def test_basic_shape(self):
|
|
27
|
+
self.assertEqual(
|
|
28
|
+
run({"id": "t1", "title": "one", "flag": "a"}, ["id", "title"]),
|
|
29
|
+
{"flag": "a"},
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
def test_no_keys_excluded(self):
|
|
33
|
+
self.assertEqual(run({"a": 1, "b": 2}, []), {"a": 1, "b": 2})
|
|
34
|
+
|
|
35
|
+
def test_all_keys_excluded(self):
|
|
36
|
+
self.assertEqual(run({"a": 1, "b": 2}, ["a", "b"]), {})
|
|
37
|
+
|
|
38
|
+
def test_non_identifier_key(self):
|
|
39
|
+
# The rest-spread fixture's residual carries a non-identifier
|
|
40
|
+
# sibling key (`data-priority`) through untouched.
|
|
41
|
+
self.assertEqual(
|
|
42
|
+
run(
|
|
43
|
+
{"id": "t1", "title": "one", "data-priority": "high", "tag": "urgent"},
|
|
44
|
+
["id", "title"],
|
|
45
|
+
),
|
|
46
|
+
{"data-priority": "high", "tag": "urgent"},
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def test_non_dict_input(self):
|
|
50
|
+
self.assertEqual(run(None, ["id"]), {})
|
|
51
|
+
self.assertEqual(run("not a dict", ["id"]), {})
|
|
52
|
+
self.assertEqual(run([1, 2, 3], ["id"]), {})
|
|
53
|
+
|
|
54
|
+
def test_key_not_present_is_noop(self):
|
|
55
|
+
self.assertEqual(run({"a": 1}, ["missing"]), {"a": 1})
|
|
56
|
+
|
|
57
|
+
def test_empty_keys_arg(self):
|
|
58
|
+
self.assertEqual(run({"a": 1}, None), {"a": 1})
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
if __name__ == "__main__":
|
|
62
|
+
unittest.main()
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""`BarefootJS.props_attr` -- the `bf-p` hydration-payload attribute.
|
|
2
|
+
|
|
3
|
+
The encoded JSON is embedded in a SINGLE-quoted attribute, so it must be
|
|
4
|
+
attribute-escaped: a raw `'` inside a string value (e.g. a blog paragraph)
|
|
5
|
+
terminates the attribute early and the client hydrates from truncated JSON
|
|
6
|
+
(empty island text; found via the shared blog-ssr e2e). Same fix across the
|
|
7
|
+
Perl, Python, Ruby, and Rust runtimes -- keep the four tests in sync.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import re
|
|
14
|
+
import unittest
|
|
15
|
+
|
|
16
|
+
from barefootjs import BarefootJS
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class _PureBackend:
|
|
20
|
+
def encode_json(self, value):
|
|
21
|
+
return json.dumps(value, sort_keys=True, separators=(",", ":"))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def bf_with(props):
|
|
25
|
+
bf = BarefootJS(None, {"backend": _PureBackend()})
|
|
26
|
+
if props is not None:
|
|
27
|
+
bf._props(props)
|
|
28
|
+
return bf
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class PropsAttrTest(unittest.TestCase):
|
|
32
|
+
def test_empty_props_emit_nothing(self):
|
|
33
|
+
self.assertEqual(bf_with(None).props_attr(), "")
|
|
34
|
+
self.assertEqual(bf_with({}).props_attr(), "")
|
|
35
|
+
|
|
36
|
+
def test_json_is_attribute_escaped(self):
|
|
37
|
+
attr = bf_with({"note": "it's <b> & co"}).props_attr()
|
|
38
|
+
self.assertEqual(attr, " bf-p='{"note":"it's <b> & co"}'")
|
|
39
|
+
|
|
40
|
+
def test_attribute_round_trips_through_entity_decoding(self):
|
|
41
|
+
attr = bf_with({"note": "it's <b> & co"}).props_attr()
|
|
42
|
+
value = re.search(r"bf-p='([^']*)'", attr).group(1)
|
|
43
|
+
decoded = (
|
|
44
|
+
value.replace(""", '"')
|
|
45
|
+
.replace("'", "'")
|
|
46
|
+
.replace("<", "<")
|
|
47
|
+
.replace(">", ">")
|
|
48
|
+
.replace("&", "&")
|
|
49
|
+
)
|
|
50
|
+
self.assertEqual(json.loads(decoded), {"note": "it's <b> & co"})
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
if __name__ == "__main__":
|
|
54
|
+
unittest.main()
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""`BarefootJS.query` -- ported from packages/adapter-perl/t/query.t.
|
|
2
|
+
|
|
3
|
+
The full CROSS-BACKEND behaviour (control flow + form-encoding parity with
|
|
4
|
+
the browser's URLSearchParams) is defined ONCE in the shared golden helper
|
|
5
|
+
vectors and run by `test_helper_vectors.py`. This file keeps a few
|
|
6
|
+
representative cases for always-on coverage plus the Python-runtime-SPECIFIC
|
|
7
|
+
defensive behaviour the golden vectors can't express: a `None` value (JSON
|
|
8
|
+
has no `undefined`; a JSON `null` stringifies to "null" under JS `String()`,
|
|
9
|
+
so it can't be a shared vector -- this runtime coerces `None` to '' and omits
|
|
10
|
+
the empty pair, mirroring the Perl port's documented `undef` handling)."""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import unittest
|
|
15
|
+
|
|
16
|
+
from barefootjs import BarefootJS
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class QueryTest(unittest.TestCase):
|
|
20
|
+
def setUp(self):
|
|
21
|
+
self.bf = BarefootJS(None, {"backend": None})
|
|
22
|
+
|
|
23
|
+
def test_order_preserved_repeated_key_overwrites_at_first_position(self):
|
|
24
|
+
self.assertEqual(
|
|
25
|
+
self.bf.query("/blog", True, "sort", "title", True, "tag", "go", True, "sort", "date"),
|
|
26
|
+
"/blog?sort=date&tag=go",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
def test_form_encoding_tilde_star_space(self):
|
|
30
|
+
self.assertEqual(self.bf.query("/s", True, "t", "a~b *c"), "/s?t=a%7Eb+*c")
|
|
31
|
+
|
|
32
|
+
def test_array_value_appends_pair_per_nonempty_member(self):
|
|
33
|
+
self.assertEqual(self.bf.query("/list", True, "tag", ["a", "", "b"]), "/list?tag=a&tag=b")
|
|
34
|
+
|
|
35
|
+
def test_none_value_coerced_to_empty_and_omitted(self):
|
|
36
|
+
self.assertEqual(self.bf.query("/list", True, "tag", None), "/list")
|
|
37
|
+
self.assertEqual(self.bf.query("/list", True, "tag", None, True, "keep", "me"), "/list?keep=me")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
if __name__ == "__main__":
|
|
41
|
+
unittest.main()
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Jinja2 end-to-end render test, modeled on
|
|
2
|
+
packages/adapter-xslate/t/render.t.
|
|
3
|
+
|
|
4
|
+
Writes a `.jinja` template equivalent to what the `@barefootjs/jinja`
|
|
5
|
+
compile-time adapter emits, then renders it through the runtime + the Jinja
|
|
6
|
+
backend. Exercises scope markers, hydration attrs, text slots via HTML
|
|
7
|
+
comment markers, autoescaping, and `spread_attrs`.
|
|
8
|
+
|
|
9
|
+
Jinja idiom note: helpers that return already-safe HTML syntax but are NOT
|
|
10
|
+
wrapped by the backend's `mark_raw` (`scope_attr`, `hydration_attrs`,
|
|
11
|
+
`text_start`, `text_end`, `comment`, `scope_comment`) need an explicit
|
|
12
|
+
`| safe` filter at the call site -- the Jinja-syntax equivalent of Kolon's
|
|
13
|
+
`| mark_raw` pipe seen in the Perl port's `render.t`. `spread_attrs` already
|
|
14
|
+
returns a `markupsafe.Markup` value (via `backend.mark_raw`), so it needs no
|
|
15
|
+
filter, matching the Perl template's call site exactly.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import tempfile
|
|
22
|
+
import unittest
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
from barefootjs import BarefootJS
|
|
26
|
+
from barefootjs.backend_jinja import JinjaBackend
|
|
27
|
+
|
|
28
|
+
WIDGET_TEMPLATE = (
|
|
29
|
+
'<div bf-s="{{ bf.scope_attr() }}" {{ bf.hydration_attrs() | safe }}>'
|
|
30
|
+
"count: {{ bf.text_start('s0') | safe }}{{ count }}{{ bf.text_end() | safe }} "
|
|
31
|
+
"<span {{ bf.spread_attrs(attrs) }}>{{ label }}</span>"
|
|
32
|
+
"</div>"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class RenderTest(unittest.TestCase):
|
|
37
|
+
def setUp(self):
|
|
38
|
+
self._tmpdir = tempfile.TemporaryDirectory()
|
|
39
|
+
self.addCleanup(self._tmpdir.cleanup)
|
|
40
|
+
template_dir = Path(self._tmpdir.name)
|
|
41
|
+
(template_dir / "widget.jinja").write_text(WIDGET_TEMPLATE, encoding="utf-8")
|
|
42
|
+
self.backend = JinjaBackend(paths=[str(template_dir)])
|
|
43
|
+
self.bf = BarefootJS(None, {"backend": self.backend})
|
|
44
|
+
self.bf._scope_id("Widget_test")
|
|
45
|
+
|
|
46
|
+
def test_scope_and_hydration_markers(self):
|
|
47
|
+
out = self.backend.render_named(
|
|
48
|
+
"widget", self.bf, {"count": 7, "label": "<x>", "attrs": {"id": "n", "class": "c"}}
|
|
49
|
+
)
|
|
50
|
+
self.assertIn('bf-s="Widget_test" bf-r=""', out)
|
|
51
|
+
|
|
52
|
+
def test_reactive_text_slot_with_comment_markers(self):
|
|
53
|
+
out = self.backend.render_named(
|
|
54
|
+
"widget", self.bf, {"count": 7, "label": "<x>", "attrs": {"id": "n", "class": "c"}}
|
|
55
|
+
)
|
|
56
|
+
self.assertIn("count: <!--bf:s0-->7<!--/-->", out)
|
|
57
|
+
|
|
58
|
+
def test_plain_interpolation_is_autoescaped(self):
|
|
59
|
+
out = self.backend.render_named(
|
|
60
|
+
"widget", self.bf, {"count": 7, "label": "<x>", "attrs": {"id": "n", "class": "c"}}
|
|
61
|
+
)
|
|
62
|
+
self.assertIn("<x>", out)
|
|
63
|
+
|
|
64
|
+
def test_spread_attrs_renders_raw_with_sorted_keys(self):
|
|
65
|
+
out = self.backend.render_named(
|
|
66
|
+
"widget", self.bf, {"count": 7, "label": "<x>", "attrs": {"id": "n", "class": "c"}}
|
|
67
|
+
)
|
|
68
|
+
self.assertIn('<span class="c" id="n">', out)
|
|
69
|
+
|
|
70
|
+
def test_backend_unit_operations(self):
|
|
71
|
+
self.assertEqual(self.backend.materialize("plain"), "plain")
|
|
72
|
+
self.assertEqual(self.backend.materialize(lambda: "lazy"), "lazy")
|
|
73
|
+
encoded = self.backend.encode_json({"b": 2, "a": 1})
|
|
74
|
+
self.assertEqual(encoded, '{"a":1,"b":2}') # canonical (sorted) key order
|
|
75
|
+
|
|
76
|
+
def test_render_named_mangles_reserved_word_props(self):
|
|
77
|
+
template_dir = Path(self._tmpdir.name)
|
|
78
|
+
(template_dir / "kw.jinja").write_text("{{ class_ }}-{{ for_ }}", encoding="utf-8")
|
|
79
|
+
out = self.backend.render_named("kw", self.bf, {"class": "X", "for": "Y"})
|
|
80
|
+
self.assertEqual(out, "X-Y")
|
|
81
|
+
|
|
82
|
+
def test_render_child_end_to_end(self):
|
|
83
|
+
"""A parent template renders a registered child via `bf.render_child`,
|
|
84
|
+
exercising `render_child` -> renderer -> `render_named` end-to-end,
|
|
85
|
+
including keyword mangling flowing through both hops."""
|
|
86
|
+
template_dir = Path(self._tmpdir.name)
|
|
87
|
+
(template_dir / "parent.jinja").write_text(
|
|
88
|
+
"parent:{{ bf.render_child('child', class='c1', label='hi') }}", encoding="utf-8"
|
|
89
|
+
)
|
|
90
|
+
(template_dir / "child.jinja").write_text("[{{ class_ }}:{{ label }}]", encoding="utf-8")
|
|
91
|
+
|
|
92
|
+
def child_renderer(props, caller):
|
|
93
|
+
child_bf = BarefootJS(None, {"backend": self.backend})
|
|
94
|
+
return self.backend.render_named("child", child_bf, props)
|
|
95
|
+
|
|
96
|
+
self.bf.register_child_renderer("child", child_renderer)
|
|
97
|
+
out = self.backend.render_named("parent", self.bf, {})
|
|
98
|
+
self.assertEqual(out, "parent:[c1:hi]")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
if __name__ == "__main__":
|
|
102
|
+
unittest.main()
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""`render_child` renderer-invocation contract, ported from
|
|
2
|
+
packages/adapter-perl/t/render_child.t.
|
|
3
|
+
|
|
4
|
+
Renderer contract (#1897): the renderer is invoked with `(props, invoking_bf)`
|
|
5
|
+
so nested renders can chain scope/slot identity off the caller, not the
|
|
6
|
+
registrant.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import unittest
|
|
12
|
+
|
|
13
|
+
from barefootjs import BarefootJS
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class _StubBackend:
|
|
17
|
+
def materialize(self, value):
|
|
18
|
+
return value() if callable(value) else value
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def new_bf():
|
|
22
|
+
return BarefootJS(None, {"backend": _StubBackend()})
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class RenderChildTest(unittest.TestCase):
|
|
26
|
+
def test_renderer_receives_invoking_instance(self):
|
|
27
|
+
bf = new_bf()
|
|
28
|
+
bf._scope_id("Root_test")
|
|
29
|
+
|
|
30
|
+
seen = {}
|
|
31
|
+
|
|
32
|
+
def probe(props, caller):
|
|
33
|
+
seen["props"] = props
|
|
34
|
+
seen["caller"] = caller
|
|
35
|
+
return "ok"
|
|
36
|
+
|
|
37
|
+
bf.register_child_renderer("probe", probe)
|
|
38
|
+
|
|
39
|
+
self.assertEqual(bf.render_child("probe", value=1), "ok")
|
|
40
|
+
self.assertEqual(seen["props"]["value"], 1)
|
|
41
|
+
self.assertIs(seen["caller"], bf)
|
|
42
|
+
|
|
43
|
+
# A nested invocation from a different instance passes THAT instance.
|
|
44
|
+
child = new_bf()
|
|
45
|
+
child._scope_id("Root_test_s0")
|
|
46
|
+
child._child_renderers(bf._child_renderers())
|
|
47
|
+
child.render_child("probe")
|
|
48
|
+
self.assertIs(seen["caller"], child)
|
|
49
|
+
|
|
50
|
+
def test_renderer_exceptions_propagate(self):
|
|
51
|
+
bf = new_bf()
|
|
52
|
+
|
|
53
|
+
def boom(props, caller):
|
|
54
|
+
raise RuntimeError("renderer exploded")
|
|
55
|
+
|
|
56
|
+
bf.register_child_renderer("boom", boom)
|
|
57
|
+
|
|
58
|
+
with self.assertRaisesRegex(RuntimeError, "renderer exploded"):
|
|
59
|
+
bf.render_child("boom")
|
|
60
|
+
|
|
61
|
+
def test_single_dict_form(self):
|
|
62
|
+
# Mirrors the Perl port's hashref form for callers that can't splat
|
|
63
|
+
# a hash into positional/keyword args.
|
|
64
|
+
bf = new_bf()
|
|
65
|
+
seen = {}
|
|
66
|
+
|
|
67
|
+
def probe(props, caller):
|
|
68
|
+
seen["props"] = props
|
|
69
|
+
return "ok"
|
|
70
|
+
|
|
71
|
+
bf.register_child_renderer("probe", probe)
|
|
72
|
+
bf.render_child("probe", {"value": 42})
|
|
73
|
+
self.assertEqual(seen["props"]["value"], 42)
|
|
74
|
+
|
|
75
|
+
def test_missing_renderer_raises(self):
|
|
76
|
+
bf = new_bf()
|
|
77
|
+
with self.assertRaises(RuntimeError):
|
|
78
|
+
bf.render_child("missing")
|
|
79
|
+
|
|
80
|
+
def test_reserved_word_prop_is_mangled(self):
|
|
81
|
+
bf = new_bf()
|
|
82
|
+
seen = {}
|
|
83
|
+
|
|
84
|
+
def probe(props, caller):
|
|
85
|
+
seen["props"] = props
|
|
86
|
+
return "ok"
|
|
87
|
+
|
|
88
|
+
bf.register_child_renderer("probe", probe)
|
|
89
|
+
bf.render_child("probe", {"class": "x", "id": "y"})
|
|
90
|
+
self.assertEqual(seen["props"]["class_"], "x")
|
|
91
|
+
self.assertEqual(seen["props"]["id"], "y")
|
|
92
|
+
self.assertNotIn("class", seen["props"])
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
if __name__ == "__main__":
|
|
96
|
+
unittest.main()
|