@barefootjs/rust 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 +194 -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 +85 -0
- package/dist/adapter/boolean-result.d.ts.map +1 -0
- package/dist/adapter/emit-context.d.ts +107 -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/index.d.ts +6 -0
- package/dist/adapter/index.d.ts.map +1 -0
- package/dist/adapter/index.js +189091 -0
- package/dist/adapter/lib/constants.d.ts +25 -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/minijinja-naming.d.ts +64 -0
- package/dist/adapter/lib/minijinja-naming.d.ts.map +1 -0
- package/dist/adapter/lib/types.d.ts +32 -0
- package/dist/adapter/lib/types.d.ts.map +1 -0
- package/dist/adapter/memo/seed.d.ts +84 -0
- package/dist/adapter/memo/seed.d.ts.map +1 -0
- package/dist/adapter/minijinja-adapter.d.ts +421 -0
- package/dist/adapter/minijinja-adapter.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 +63 -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 +29 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +189111 -0
- package/dist/conformance-pins.d.ts +13 -0
- package/dist/conformance-pins.d.ts.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +189112 -0
- package/package.json +67 -0
- package/runtime/Cargo.lock +124 -0
- package/runtime/Cargo.toml +21 -0
- package/runtime/src/backend_minijinja.rs +176 -0
- package/runtime/src/bin/bf-render.rs +147 -0
- package/runtime/src/evaluator.rs +770 -0
- package/runtime/src/lib.rs +19 -0
- package/runtime/src/manifest.rs +258 -0
- package/runtime/src/num.rs +558 -0
- package/runtime/src/runtime.rs +1548 -0
- package/runtime/src/search_params.rs +173 -0
- package/runtime/tests/eval_vectors.rs +94 -0
- package/runtime/tests/evaluator.rs +407 -0
- package/runtime/tests/helper_vectors.rs +348 -0
- package/runtime/tests/manifest.rs +169 -0
- package/runtime/tests/omit.rs +79 -0
- package/runtime/tests/props_attr.rs +75 -0
- package/runtime/tests/query.rs +50 -0
- package/runtime/tests/render_child.rs +210 -0
- package/runtime/tests/search_params.rs +68 -0
- package/runtime/tests/spread_attrs.rs +94 -0
- package/runtime/tests/template_primitives.rs +376 -0
- package/runtime/tests/vector-divergences.json +33 -0
- package/src/__tests__/minijinja-adapter-unit.test.ts +392 -0
- package/src/__tests__/minijinja-adapter.test.ts +58 -0
- package/src/__tests__/minijinja-counter.test.ts +61 -0
- package/src/__tests__/minijinja-query-href.test.ts +101 -0
- package/src/__tests__/minijinja-spread-attrs.test.ts +227 -0
- package/src/adapter/analysis/component-tree.ts +119 -0
- package/src/adapter/boolean-result.ts +177 -0
- package/src/adapter/emit-context.ts +119 -0
- package/src/adapter/expr/array-method.ts +346 -0
- package/src/adapter/expr/emitters.ts +608 -0
- package/src/adapter/index.ts +6 -0
- package/src/adapter/lib/constants.ts +37 -0
- package/src/adapter/lib/ir-scope.ts +95 -0
- package/src/adapter/lib/minijinja-naming.ts +85 -0
- package/src/adapter/lib/types.ts +35 -0
- package/src/adapter/memo/seed.ts +135 -0
- package/src/adapter/minijinja-adapter.ts +1796 -0
- package/src/adapter/props/prop-classes.ts +65 -0
- package/src/adapter/spread/spread-codegen.ts +168 -0
- package/src/adapter/value/parsed-literal.ts +76 -0
- package/src/build.ts +38 -0
- package/src/conformance-pins.ts +101 -0
- package/src/index.ts +12 -0
- package/src/test-render.ts +680 -0
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
//! Golden helper-vector conformance, ported from
|
|
2
|
+
//! `packages/adapter-jinja/python/tests/test_helper_vectors.py` (itself a
|
|
3
|
+
//! port of `packages/adapter-perl/t/helper_vectors.t`).
|
|
4
|
+
//!
|
|
5
|
+
//! Runs `packages/adapter-tests/vectors/vectors.json` -- generated from the
|
|
6
|
+
//! JS reference implementations (spec/template-helpers.md) -- against this
|
|
7
|
+
//! crate's `runtime` free functions, bound to the exact shape a compiled
|
|
8
|
+
//! minijinja template would execute (a `bf.<method>(...)` call, or the
|
|
9
|
+
//! native operator the adapter emits for `add`/`sub`/`mul`/`div`/`neg`).
|
|
10
|
+
//!
|
|
11
|
+
//! Per PR #2084 ("Promote golden vectors to a first-class JSON corpus with
|
|
12
|
+
//! JSON-declared per-backend divergences"), this backend's divergences and
|
|
13
|
+
//! unsupported helpers are NOT hard-coded in this file -- they are declared
|
|
14
|
+
//! in `tests/vector-divergences.json` (schema: `packages/adapter-tests/
|
|
15
|
+
//! vectors/README.md`, "Divergence declarations"), the same convention the
|
|
16
|
+
//! Go/Perl/Python/Ruby runners use. Only the language-specific bindings
|
|
17
|
+
//! table (`call_binding` below) stays code, mirroring
|
|
18
|
+
//! `packages/adapter-go-template/runtime/vectors_test.go`.
|
|
19
|
+
//!
|
|
20
|
+
//! Two of the Python port's divergences VANISH for this backend because
|
|
21
|
+
//! `f64` arithmetic is IEEE-754-native (see `num.rs`'s module docstring):
|
|
22
|
+
//! `add`'s safe-integer-edge rounding and `div`'s zero-divisor case both now
|
|
23
|
+
//! match the JS-normative expect exactly, so they carry NO entry in
|
|
24
|
+
//! `tests/vector-divergences.json`.
|
|
25
|
+
|
|
26
|
+
use barefootjs::backend_minijinja;
|
|
27
|
+
use barefootjs::num::{self, JsValue};
|
|
28
|
+
use barefootjs::runtime;
|
|
29
|
+
use barefootjs::SearchParams;
|
|
30
|
+
use serde::Deserialize;
|
|
31
|
+
use serde_json::Value as JsonValue;
|
|
32
|
+
use std::collections::{BTreeMap, HashMap, HashSet};
|
|
33
|
+
use std::path::PathBuf;
|
|
34
|
+
|
|
35
|
+
fn vectors_path() -> PathBuf {
|
|
36
|
+
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../adapter-tests/vectors/vectors.json")
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
fn divergences_path() -> PathBuf {
|
|
40
|
+
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/vector-divergences.json")
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
fn obj(v: &JsValue, key: &str) -> JsValue {
|
|
44
|
+
v.as_object().and_then(|m| m.get(key)).cloned().unwrap_or(JsValue::Null)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/// Mirrors `test_helper_vectors.py`'s `_truthy_pred`.
|
|
48
|
+
fn truthy_pred(item: &JsValue, field: &str) -> bool {
|
|
49
|
+
runtime::js_truthy(&obj(item, field))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/// Mirrors `_field_eq_pred`, routed through `num::strict_eq` (kind-aware
|
|
53
|
+
/// JS `===`) rather than a bespoke comparison -- the SAME choice
|
|
54
|
+
/// `runtime::index_of`/`last_index_of` make (see `runtime.rs`'s module
|
|
55
|
+
/// docstring), for the same reason.
|
|
56
|
+
fn field_eq_pred(item: &JsValue, field: &str, value: &JsValue) -> bool {
|
|
57
|
+
item.as_object().map(|m| m.get(field).map(|v| num::strict_eq(v, value)).unwrap_or(false)).unwrap_or(false)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
fn bind_sort(recv: &JsValue, spec_args: &[JsValue]) -> JsValue {
|
|
61
|
+
let mut keys = Vec::new();
|
|
62
|
+
for chunk in spec_args.chunks(4) {
|
|
63
|
+
if chunk.len() < 4 {
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
let mut m = BTreeMap::new();
|
|
67
|
+
m.insert("key_kind".to_string(), chunk[0].clone());
|
|
68
|
+
m.insert("key".to_string(), chunk[1].clone());
|
|
69
|
+
m.insert("compare_type".to_string(), chunk[2].clone());
|
|
70
|
+
m.insert("direction".to_string(), chunk[3].clone());
|
|
71
|
+
keys.push(JsValue::Object(m));
|
|
72
|
+
}
|
|
73
|
+
let mut opts = BTreeMap::new();
|
|
74
|
+
opts.insert("keys".to_string(), JsValue::Array(keys));
|
|
75
|
+
runtime::sort(recv, &JsValue::Object(opts))
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
fn bind_reduce(args: &[JsValue]) -> JsValue {
|
|
79
|
+
// [recv, op, key_kind, key, type, init, direction]
|
|
80
|
+
let (recv, op, key_kind, key, rtype, init, direction) = (&args[0], &args[1], &args[2], &args[3], &args[4], &args[5], &args[6]);
|
|
81
|
+
let rtype_s = rtype.as_str().unwrap_or("numeric");
|
|
82
|
+
let seed = if rtype_s == "numeric" { JsValue::Number(runtime::js_number(init)) } else { init.clone() };
|
|
83
|
+
let mut opts = BTreeMap::new();
|
|
84
|
+
opts.insert("op".to_string(), op.clone());
|
|
85
|
+
opts.insert("key_kind".to_string(), key_kind.clone());
|
|
86
|
+
opts.insert("key".to_string(), key.clone());
|
|
87
|
+
opts.insert("type".to_string(), rtype.clone());
|
|
88
|
+
opts.insert("init".to_string(), seed);
|
|
89
|
+
opts.insert("direction".to_string(), direction.clone());
|
|
90
|
+
runtime::reduce(recv, &JsValue::Object(opts))
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
fn bind_flat_map_tuple(recv: &JsValue, flat_args: &[JsValue]) -> JsValue {
|
|
94
|
+
let specs: Vec<(String, String)> = flat_args.chunks(2).filter(|c| c.len() == 2).map(|c| (runtime::js_string(&c[0]), runtime::js_string(&c[1]))).collect();
|
|
95
|
+
runtime::flat_map_tuple(recv, &specs)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/// One binding per canonical helper id. `add`/`sub`/`mul`/`div`/`neg` are
|
|
99
|
+
/// not `runtime` functions (the adapter lowers JS `+`/`-`/`*`/`/`/unary
|
|
100
|
+
/// `-` to the native minijinja operator, not a `bf.` call, mirroring the
|
|
101
|
+
/// Perl/Python ports); `mod` DOES route through `runtime::js_mod`-backed
|
|
102
|
+
/// dispatch since the plan requires the minijinja emitter to use `bf.mod`
|
|
103
|
+
/// for JS `%`.
|
|
104
|
+
fn call_binding(fn_name: &str, args: &[JsValue]) -> Option<JsValue> {
|
|
105
|
+
let a = |i: usize| args.get(i).cloned().unwrap_or(JsValue::Null);
|
|
106
|
+
Some(match fn_name {
|
|
107
|
+
"add" => JsValue::Number(num::to_f64(&a(0)) + num::to_f64(&a(1))),
|
|
108
|
+
"sub" => JsValue::Number(num::to_f64(&a(0)) - num::to_f64(&a(1))),
|
|
109
|
+
"mul" => JsValue::Number(num::to_f64(&a(0)) * num::to_f64(&a(1))),
|
|
110
|
+
"div" => JsValue::Number(num::to_f64(&a(0)) / num::to_f64(&a(1))),
|
|
111
|
+
"mod" => JsValue::Number(num::js_mod(runtime::js_number(&a(0)), runtime::js_number(&a(1)))),
|
|
112
|
+
"neg" => JsValue::Number(-num::to_f64(&a(0))),
|
|
113
|
+
"string" => JsValue::String(runtime::js_string(&a(0))),
|
|
114
|
+
"json" => JsValue::String(backend_minijinja::encode_json(&a(0))),
|
|
115
|
+
"number" => JsValue::Number(runtime::js_number(&a(0))),
|
|
116
|
+
"floor" => JsValue::Number(num::js_floor(runtime::js_number(&a(0)))),
|
|
117
|
+
"ceil" => JsValue::Number(num::js_ceil(runtime::js_number(&a(0)))),
|
|
118
|
+
"round" => JsValue::Number(num::js_round(runtime::js_number(&a(0)))),
|
|
119
|
+
"to_fixed" => {
|
|
120
|
+
let digits = if args.len() > 1 { num::to_f64(&a(1)) as i32 } else { 0 };
|
|
121
|
+
JsValue::String(num::to_fixed(runtime::js_number(&a(0)), digits))
|
|
122
|
+
}
|
|
123
|
+
"lower" => JsValue::String(runtime::js_string(&a(0)).to_lowercase()),
|
|
124
|
+
"upper" => JsValue::String(runtime::js_string(&a(0)).to_uppercase()),
|
|
125
|
+
"trim" => JsValue::String(runtime::trim(&a(0))),
|
|
126
|
+
"starts_with" => JsValue::Bool(runtime::starts_with(&a(0), &a(1), &a(2))),
|
|
127
|
+
"ends_with" => JsValue::Bool(runtime::ends_with(&a(0), &a(1), &a(2))),
|
|
128
|
+
"replace" => JsValue::String(runtime::replace(&a(0), &a(1), &a(2))),
|
|
129
|
+
"repeat" => JsValue::String(runtime::repeat(&a(0), &a(1))),
|
|
130
|
+
"pad_start" => JsValue::String(runtime::pad(&runtime::js_string(&a(0)), &a(1), &a(2), true)),
|
|
131
|
+
"pad_end" => JsValue::String(runtime::pad(&runtime::js_string(&a(0)), &a(1), &a(2), false)),
|
|
132
|
+
"split" => {
|
|
133
|
+
let sep = if args.len() > 1 { Some(a(1)) } else { None };
|
|
134
|
+
let limit = if args.len() > 2 && !matches!(a(2), JsValue::Null) { Some(num::to_f64(&a(2)) as i64) } else { None };
|
|
135
|
+
runtime::split(&a(0), sep.as_ref(), limit)
|
|
136
|
+
}
|
|
137
|
+
"len" => JsValue::Number(runtime::length(&a(0))),
|
|
138
|
+
"at" => runtime::at(&a(0), &a(1)),
|
|
139
|
+
"includes" => JsValue::Bool(runtime::includes(&a(0), &a(1))),
|
|
140
|
+
"index_of" => JsValue::Number(runtime::array_index_of(&a(0), &a(1), false) as f64),
|
|
141
|
+
"last_index_of" => JsValue::Number(runtime::array_index_of(&a(0), &a(1), true) as f64),
|
|
142
|
+
"concat" => runtime::concat(&a(0), &a(1)),
|
|
143
|
+
"slice" => runtime::slice(&a(0), &a(1), &a(2)),
|
|
144
|
+
"reverse" => runtime::reverse(&a(0)),
|
|
145
|
+
"flat" => {
|
|
146
|
+
let depth = if args.len() > 1 { num::to_f64(&a(1)) as i64 } else { 1 };
|
|
147
|
+
JsValue::Array(runtime::flat(a(0).as_array().unwrap_or(&[]), depth))
|
|
148
|
+
}
|
|
149
|
+
"flat_dynamic" => JsValue::Array(runtime::flat_dynamic(a(0).as_array().unwrap_or(&[]), &a(1))),
|
|
150
|
+
"join" => JsValue::String(runtime::join(&a(0), &a(1))),
|
|
151
|
+
"arr" => JsValue::Array(args.to_vec()),
|
|
152
|
+
"filter_truthy" => JsValue::Array(a(0).as_array().unwrap_or(&[]).iter().filter(|x| runtime::js_truthy(x)).cloned().collect()),
|
|
153
|
+
"search_params_get" => match SearchParams::new(&runtime::js_string(&a(0))).get(&runtime::js_string(&a(1))) {
|
|
154
|
+
Some(v) => JsValue::String(v.to_string()),
|
|
155
|
+
None => JsValue::Null,
|
|
156
|
+
},
|
|
157
|
+
"query" => JsValue::String(runtime::query(&a(0), &args[1..])),
|
|
158
|
+
"every" => JsValue::Bool(a(0).as_array().unwrap_or(&[]).iter().all(|item| truthy_pred(item, a(1).as_str().unwrap_or("")))),
|
|
159
|
+
"some" => JsValue::Bool(a(0).as_array().unwrap_or(&[]).iter().any(|item| truthy_pred(item, a(1).as_str().unwrap_or("")))),
|
|
160
|
+
"filter" => JsValue::Array(a(0).as_array().unwrap_or(&[]).iter().filter(|item| field_eq_pred(item, a(1).as_str().unwrap_or(""), &a(2))).cloned().collect()),
|
|
161
|
+
"find" => a(0).as_array().unwrap_or(&[]).iter().find(|item| field_eq_pred(item, a(1).as_str().unwrap_or(""), &a(2))).cloned().unwrap_or(JsValue::Null),
|
|
162
|
+
"find_index" => JsValue::Number(a(0).as_array().unwrap_or(&[]).iter().position(|item| field_eq_pred(item, a(1).as_str().unwrap_or(""), &a(2))).map(|i| i as f64).unwrap_or(-1.0)),
|
|
163
|
+
"find_last" => a(0).as_array().unwrap_or(&[]).iter().rev().find(|item| field_eq_pred(item, a(1).as_str().unwrap_or(""), &a(2))).cloned().unwrap_or(JsValue::Null),
|
|
164
|
+
"find_last_index" => {
|
|
165
|
+
let items = a(0).as_array().unwrap_or(&[]).to_vec();
|
|
166
|
+
let idx = (0..items.len()).rev().find(|&i| field_eq_pred(&items[i], a(1).as_str().unwrap_or(""), &a(2)));
|
|
167
|
+
JsValue::Number(idx.map(|i| i as f64).unwrap_or(-1.0))
|
|
168
|
+
}
|
|
169
|
+
"sort" => bind_sort(&a(0), &args[1..]),
|
|
170
|
+
"reduce" => bind_reduce(args),
|
|
171
|
+
"flat_map" => runtime::flat_map(&a(0), a(1).as_str().unwrap_or(""), a(2).as_str().unwrap_or("")),
|
|
172
|
+
"flat_map_tuple" => bind_flat_map_tuple(&a(0), &args[1..]),
|
|
173
|
+
_ => return None,
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/// Spec value-compat comparison against a JSON-decoded expect -- sentinel
|
|
178
|
+
/// hashes, booleans by truthiness, numbers numerically, arrays/objects
|
|
179
|
+
/// recursively.
|
|
180
|
+
fn matches(got: &JsValue, expect: &JsonValue) -> bool {
|
|
181
|
+
if expect.is_null() {
|
|
182
|
+
return matches!(got, JsValue::Null);
|
|
183
|
+
}
|
|
184
|
+
if let Some(kind) = expect.get("$num").and_then(|v| v.as_str()) {
|
|
185
|
+
return match got {
|
|
186
|
+
JsValue::Number(g) => match kind {
|
|
187
|
+
"NaN" => g.is_nan(),
|
|
188
|
+
"Infinity" => *g == f64::INFINITY,
|
|
189
|
+
"-Infinity" => *g == f64::NEG_INFINITY,
|
|
190
|
+
_ => false,
|
|
191
|
+
},
|
|
192
|
+
_ => false,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
if let Some(b) = expect.as_bool() {
|
|
196
|
+
return py_truthy(got) == b;
|
|
197
|
+
}
|
|
198
|
+
if let Some(arr) = expect.as_array() {
|
|
199
|
+
return match got {
|
|
200
|
+
JsValue::Array(g) => g.len() == arr.len() && g.iter().zip(arr).all(|(g, e)| matches(g, e)),
|
|
201
|
+
_ => false,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
if let Some(o) = expect.as_object() {
|
|
205
|
+
return match got {
|
|
206
|
+
JsValue::Object(g) => g.len() == o.len() && o.iter().all(|(k, v)| g.get(k).map(|gv| matches(gv, v)).unwrap_or(false)),
|
|
207
|
+
_ => false,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
if matches!(got, JsValue::Null | JsValue::Array(_) | JsValue::Object(_)) {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
if let Some(n) = expect.as_f64() {
|
|
214
|
+
return matches!(got, JsValue::Number(g) if *g == n);
|
|
215
|
+
}
|
|
216
|
+
if let Some(s) = expect.as_str() {
|
|
217
|
+
return matches!(got, JsValue::String(g) if g == s);
|
|
218
|
+
}
|
|
219
|
+
false
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/// Python truthiness of a decoded value (empty array/object IS falsy,
|
|
223
|
+
/// unlike JS) -- only used for the boolean-expect comparison branch above,
|
|
224
|
+
/// mirroring `test_helper_vectors.py`'s `bool(got) == expect`, which relies
|
|
225
|
+
/// on Python's own (non-JS) `bool()` conversion.
|
|
226
|
+
fn py_truthy(v: &JsValue) -> bool {
|
|
227
|
+
match v {
|
|
228
|
+
JsValue::Null => false,
|
|
229
|
+
JsValue::Bool(b) => *b,
|
|
230
|
+
JsValue::Number(n) => !n.is_nan() && *n != 0.0,
|
|
231
|
+
JsValue::String(s) => !s.is_empty(),
|
|
232
|
+
JsValue::Array(a) => !a.is_empty(),
|
|
233
|
+
JsValue::Object(o) => !o.is_empty(),
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/// Shape of `tests/vector-divergences.json`
|
|
238
|
+
/// (`packages/adapter-tests/vectors/README.md`, "Divergence declarations").
|
|
239
|
+
/// Keyed by the case key `fn + "/" + note`, it is the single source of
|
|
240
|
+
/// truth for this backend's divergences and unsupported helpers -- the spec
|
|
241
|
+
/// stays backend-neutral, and this file is also validated centrally by
|
|
242
|
+
/// `packages/adapter-tests/src/__tests__/divergences.test.ts`.
|
|
243
|
+
#[derive(Deserialize)]
|
|
244
|
+
struct DivergenceEntry {
|
|
245
|
+
#[serde(default)]
|
|
246
|
+
expect: Option<JsonValue>,
|
|
247
|
+
#[serde(default)]
|
|
248
|
+
throws: bool,
|
|
249
|
+
#[allow(dead_code)]
|
|
250
|
+
#[serde(default)]
|
|
251
|
+
exception: Option<String>,
|
|
252
|
+
reason: String,
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
#[derive(Deserialize)]
|
|
256
|
+
struct DivergenceFile {
|
|
257
|
+
#[allow(dead_code)]
|
|
258
|
+
version: i64,
|
|
259
|
+
#[allow(dead_code)]
|
|
260
|
+
backend: String,
|
|
261
|
+
divergences: HashMap<String, DivergenceEntry>,
|
|
262
|
+
unsupported: HashMap<String, String>,
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
#[test]
|
|
266
|
+
fn helper_vectors_match_js_reference_or_declared_divergence() {
|
|
267
|
+
let path = vectors_path();
|
|
268
|
+
if !path.exists() {
|
|
269
|
+
eprintln!("skipping: golden vectors not available outside the monorepo checkout ({path:?})");
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
let doc: JsonValue = serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
|
|
273
|
+
let cases = doc["cases"].as_array().expect("vectors.json has no cases");
|
|
274
|
+
assert!(!cases.is_empty(), "vectors.json contains no cases");
|
|
275
|
+
|
|
276
|
+
// Declarations live in tests/vector-divergences.json, not inline, so
|
|
277
|
+
// this harness and the other backends' harnesses share one JSON
|
|
278
|
+
// schema (packages/adapter-tests/vectors/README.md, "Divergence
|
|
279
|
+
// declarations"). This harness still enforces the machinery itself:
|
|
280
|
+
// stale declarations (the backend now matches JS) and dead
|
|
281
|
+
// declarations (the case they reference no longer exists) both fail
|
|
282
|
+
// the suite.
|
|
283
|
+
let div_path = divergences_path();
|
|
284
|
+
let div_raw = std::fs::read_to_string(&div_path).unwrap_or_else(|e| panic!("read {div_path:?}: {e}"));
|
|
285
|
+
let div_file: DivergenceFile = serde_json::from_str(&div_raw).unwrap_or_else(|e| panic!("parse {div_path:?}: {e}"));
|
|
286
|
+
|
|
287
|
+
let mut declared = HashSet::new();
|
|
288
|
+
let mut failures = Vec::new();
|
|
289
|
+
|
|
290
|
+
for case in cases {
|
|
291
|
+
let fn_name = case["fn"].as_str().unwrap();
|
|
292
|
+
let note = case["note"].as_str().unwrap();
|
|
293
|
+
let key = format!("{fn_name}/{note}");
|
|
294
|
+
let raw_args = case["args"].as_array().cloned().unwrap_or_default();
|
|
295
|
+
let args: Vec<JsValue> = raw_args.iter().map(JsValue::from_json).collect();
|
|
296
|
+
let expect = &case["expect"];
|
|
297
|
+
|
|
298
|
+
if let Some(reason) = div_file.unsupported.get(fn_name) {
|
|
299
|
+
eprintln!("SKIP {key}: unsupported on this backend: {reason}");
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
let got = match call_binding(fn_name, &args) {
|
|
304
|
+
Some(v) => v,
|
|
305
|
+
None => {
|
|
306
|
+
failures.push(format!("{key}: no Rust binding for helper '{fn_name}' -- add it to call_binding"));
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
if let Some(entry) = div_file.divergences.get(&key) {
|
|
312
|
+
declared.insert(key.clone());
|
|
313
|
+
if matches(&got, expect) {
|
|
314
|
+
failures.push(format!("stale divergence declaration for '{key}' -- the backend now matches JS ({got:?}); remove it"));
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
if entry.throws {
|
|
318
|
+
failures.push(format!("{key}: throws divergences are not supported by the Rust harness -- bindings return values, not Results"));
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
let pinned = match &entry.expect {
|
|
322
|
+
Some(v) => v,
|
|
323
|
+
None => {
|
|
324
|
+
failures.push(format!("{key}: malformed divergence declaration -- missing expect"));
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
if !matches(&got, pinned) {
|
|
329
|
+
failures.push(format!("{key} (declared divergence: {}): got {got:?}, pinned {pinned:?}", entry.reason));
|
|
330
|
+
}
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if !matches(&got, expect) {
|
|
335
|
+
failures.push(format!("{key}: got {got:?}, want {expect:?}"));
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// A declaration referencing a case that no longer exists is dead --
|
|
340
|
+
// likely a renamed note. Fail so the key gets re-pointed.
|
|
341
|
+
for key in div_file.divergences.keys() {
|
|
342
|
+
if !declared.contains(key) {
|
|
343
|
+
failures.push(format!("divergence declaration matches no vector case -- renamed note? {key}"));
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
assert!(failures.is_empty(), "\n{}", failures.join("\n"));
|
|
348
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
//! Tests for `manifest::register_components_from_manifest` /
|
|
2
|
+
//! `derive_stash_from_defaults` -- the production manifest-registration
|
|
3
|
+
//! path `integrations/axum` uses (port of `packages/adapter-jinja/python/
|
|
4
|
+
//! tests`' coverage for `runtime.py`'s `register_components_from_manifest`
|
|
5
|
+
//! / `_derive_stash_from_defaults`, adjusted for this crate's
|
|
6
|
+
//! registration-only shape -- see `src/manifest.rs`'s module docstring).
|
|
7
|
+
|
|
8
|
+
use barefootjs::manifest::{derive_stash_from_defaults, register_components_from_manifest, to_template_name};
|
|
9
|
+
use barefootjs::num::JsValue;
|
|
10
|
+
use barefootjs::{backend_minijinja, BfInstance, RenderSession};
|
|
11
|
+
use std::collections::{BTreeMap, HashMap};
|
|
12
|
+
use std::path::PathBuf;
|
|
13
|
+
use std::sync::Arc;
|
|
14
|
+
|
|
15
|
+
struct TempDir(PathBuf);
|
|
16
|
+
|
|
17
|
+
impl TempDir {
|
|
18
|
+
fn new(tag: &str) -> TempDir {
|
|
19
|
+
let dir = std::env::temp_dir().join(format!(
|
|
20
|
+
"bf-manifest-test-{tag}-{}-{}",
|
|
21
|
+
std::process::id(),
|
|
22
|
+
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos()
|
|
23
|
+
));
|
|
24
|
+
std::fs::create_dir_all(&dir).unwrap();
|
|
25
|
+
TempDir(dir)
|
|
26
|
+
}
|
|
27
|
+
fn write(&self, name: &str, content: &str) {
|
|
28
|
+
std::fs::write(self.0.join(name), content).unwrap();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
impl Drop for TempDir {
|
|
33
|
+
fn drop(&mut self) {
|
|
34
|
+
std::fs::remove_dir_all(&self.0).ok();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
fn obj(pairs: Vec<(&str, JsValue)>) -> JsValue {
|
|
39
|
+
JsValue::Object(pairs.into_iter().map(|(k, v)| (k.to_string(), v)).collect())
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
#[test]
|
|
43
|
+
fn to_template_name_matches_ts_to_snake_case() {
|
|
44
|
+
assert_eq!(to_template_name("ToggleItem"), "toggle_item");
|
|
45
|
+
assert_eq!(to_template_name("PostListItem"), "post_list_item");
|
|
46
|
+
assert_eq!(to_template_name("AIChatInteractive"), "a_i_chat_interactive");
|
|
47
|
+
assert_eq!(to_template_name("Counter"), "counter");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
#[test]
|
|
51
|
+
fn derive_stash_prefers_prop_over_static_value() {
|
|
52
|
+
let defaults = obj(vec![
|
|
53
|
+
("count", obj(vec![("value", JsValue::Number(0.0))])),
|
|
54
|
+
("initial", obj(vec![("propName", JsValue::String("initial".into())), ("value", JsValue::Null)])),
|
|
55
|
+
]);
|
|
56
|
+
// No matching prop: static fallback for both.
|
|
57
|
+
let empty = JsValue::Object(BTreeMap::new());
|
|
58
|
+
let out = derive_stash_from_defaults(&defaults, &empty);
|
|
59
|
+
assert_eq!(out.as_object().unwrap().get("count"), Some(&JsValue::Number(0.0)));
|
|
60
|
+
assert_eq!(out.as_object().unwrap().get("initial"), Some(&JsValue::Null));
|
|
61
|
+
|
|
62
|
+
// Caller passed `initial`: propName-linked entry picks it up; unrelated
|
|
63
|
+
// `count` entry (no propName) stays on its static default.
|
|
64
|
+
let props = obj(vec![("initial", JsValue::Number(5.0))]);
|
|
65
|
+
let out = derive_stash_from_defaults(&defaults, &props);
|
|
66
|
+
assert_eq!(out.as_object().unwrap().get("initial"), Some(&JsValue::Number(5.0)));
|
|
67
|
+
assert_eq!(out.as_object().unwrap().get("count"), Some(&JsValue::Number(0.0)));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
#[test]
|
|
71
|
+
fn derive_stash_rest_props_prefers_props_own_key() {
|
|
72
|
+
let defaults = obj(vec![("rest", obj(vec![("isRestProps", JsValue::Bool(true)), ("value", obj(vec![]))]))]);
|
|
73
|
+
let props = obj(vec![("rest", obj(vec![("class", JsValue::String("x".into()))]))]);
|
|
74
|
+
let out = derive_stash_from_defaults(&defaults, &props);
|
|
75
|
+
let rest = out.as_object().unwrap().get("rest").unwrap().as_object().unwrap();
|
|
76
|
+
assert_eq!(rest.get("class"), Some(&JsValue::String("x".into())));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
#[test]
|
|
80
|
+
fn derive_stash_bare_value_entry_passes_through() {
|
|
81
|
+
// A non-object entry (bare JSON value) is used as-is -- mirrors
|
|
82
|
+
// Python's `if not isinstance(d, dict): extra[name] = d`.
|
|
83
|
+
let defaults = obj(vec![("flag", JsValue::Bool(true))]);
|
|
84
|
+
let out = derive_stash_from_defaults(&defaults, &JsValue::Object(BTreeMap::new()));
|
|
85
|
+
assert_eq!(out.as_object().unwrap().get("flag"), Some(&JsValue::Bool(true)));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/// End-to-end: register a flat manifest (the shape every `integrations/*`
|
|
89
|
+
/// example actually produces) and confirm a real template's
|
|
90
|
+
/// `bf.render_child('post_list_item', ...)` resolves through it, with the
|
|
91
|
+
/// child's own ssrDefaults seeding an unset field.
|
|
92
|
+
#[test]
|
|
93
|
+
fn register_from_flat_manifest_wires_render_child() {
|
|
94
|
+
let dir = TempDir::new("flat");
|
|
95
|
+
dir.write(
|
|
96
|
+
"post_list.j2",
|
|
97
|
+
"{{ bf.render_child('post_list_item', {'title': 'Hello'}) | safe }}",
|
|
98
|
+
);
|
|
99
|
+
dir.write("PostListItem.j2", "{{ title }}/{{ bf.string(pinned) }}");
|
|
100
|
+
|
|
101
|
+
let manifest = obj(vec![
|
|
102
|
+
("__barefoot__", obj(vec![("markedTemplate", JsValue::String(String::new()))])),
|
|
103
|
+
(
|
|
104
|
+
"PostListItem",
|
|
105
|
+
obj(vec![
|
|
106
|
+
("markedTemplate", JsValue::String("templates/PostListItem.j2".into())),
|
|
107
|
+
("ssrDefaults", obj(vec![("pinned", obj(vec![("value", JsValue::Bool(false))]))])),
|
|
108
|
+
]),
|
|
109
|
+
),
|
|
110
|
+
]);
|
|
111
|
+
|
|
112
|
+
let session = RenderSession::new();
|
|
113
|
+
register_components_from_manifest(&session, &manifest, &HashMap::new());
|
|
114
|
+
|
|
115
|
+
let env = backend_minijinja::build_environment(&dir.0);
|
|
116
|
+
let root = BfInstance::root(Arc::clone(&session), "test".to_string());
|
|
117
|
+
let html = backend_minijinja::render_named(&env, "post_list", root.as_mj_value(), &JsValue::Object(BTreeMap::new())).unwrap();
|
|
118
|
+
assert_eq!(html.trim(), "Hello/false");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/// A `signal_init` override for a registry key wins over the manifest's
|
|
122
|
+
/// own static ssrDefaults value.
|
|
123
|
+
#[test]
|
|
124
|
+
fn register_from_flat_manifest_applies_signal_init_override() {
|
|
125
|
+
let dir = TempDir::new("override");
|
|
126
|
+
dir.write("root.j2", "{{ bf.render_child('widget', {}) | safe }}");
|
|
127
|
+
dir.write("Widget.j2", "{{ bf.string(level) }}");
|
|
128
|
+
|
|
129
|
+
let manifest = obj(vec![(
|
|
130
|
+
"Widget",
|
|
131
|
+
obj(vec![
|
|
132
|
+
("markedTemplate", JsValue::String("templates/Widget.j2".into())),
|
|
133
|
+
("ssrDefaults", obj(vec![("level", obj(vec![("value", JsValue::Number(1.0))]))])),
|
|
134
|
+
]),
|
|
135
|
+
)]);
|
|
136
|
+
|
|
137
|
+
let mut signal_init = HashMap::new();
|
|
138
|
+
signal_init.insert("widget".to_string(), obj(vec![("level", JsValue::Number(9.0))]));
|
|
139
|
+
|
|
140
|
+
let session = RenderSession::new();
|
|
141
|
+
register_components_from_manifest(&session, &manifest, &signal_init);
|
|
142
|
+
|
|
143
|
+
let env = backend_minijinja::build_environment(&dir.0);
|
|
144
|
+
let root = BfInstance::root(Arc::clone(&session), "test".to_string());
|
|
145
|
+
let html = backend_minijinja::render_named(&env, "root", root.as_mj_value(), &JsValue::Object(BTreeMap::new())).unwrap();
|
|
146
|
+
assert_eq!(html.trim(), "9");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/// `ui/<name>/index`-shaped entries (the component-library registry
|
|
150
|
+
/// convention Python's port targets) still register correctly.
|
|
151
|
+
#[test]
|
|
152
|
+
fn register_from_ui_prefixed_manifest_entry() {
|
|
153
|
+
let dir = TempDir::new("ui");
|
|
154
|
+
dir.write("root.j2", "{{ bf.render_child('button', {}) | safe }}");
|
|
155
|
+
dir.write("Button.j2", "btn");
|
|
156
|
+
|
|
157
|
+
let manifest = obj(vec![(
|
|
158
|
+
"ui/button/index",
|
|
159
|
+
obj(vec![("markedTemplate", JsValue::String("templates/Button.j2".into()))]),
|
|
160
|
+
)]);
|
|
161
|
+
|
|
162
|
+
let session = RenderSession::new();
|
|
163
|
+
register_components_from_manifest(&session, &manifest, &HashMap::new());
|
|
164
|
+
|
|
165
|
+
let env = backend_minijinja::build_environment(&dir.0);
|
|
166
|
+
let root = BfInstance::root(Arc::clone(&session), "test".to_string());
|
|
167
|
+
let html = backend_minijinja::render_named(&env, "root", root.as_mj_value(), &JsValue::Object(BTreeMap::new())).unwrap();
|
|
168
|
+
assert_eq!(html.trim(), "btn");
|
|
169
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
//! `runtime::omit` -- the residual-object helper backing `bf.omit`, which
|
|
2
|
+
//! the minijinja adapter emits for an object-rest `.map()` destructure
|
|
3
|
+
//! binding whose rest is used (#2087 Phase B):
|
|
4
|
+
//!
|
|
5
|
+
//! `{% set rest = bf.omit(item, ["id", "title"]) %}`
|
|
6
|
+
//!
|
|
7
|
+
//! Builds a TRUE residual object (every key of `item` NOT in the exclude
|
|
8
|
+
//! list) so a member read (`rest.flag`, native Jinja attribute access) and
|
|
9
|
+
//! the existing `{...rest}` spread emit (`bf.spread_attrs`) both see only
|
|
10
|
+
//! the non-destructured keys, matching the Hono/CSR IIFE
|
|
11
|
+
//! (`(({ id: __bfR0, title: __bfR1, ...__bfRest }) => __bfRest)(__bfItem())`).
|
|
12
|
+
//! No shared cross-adapter golden-vector entry exists for this helper (it is
|
|
13
|
+
//! new with #2087 and specific to the structural-destructure lowering, not
|
|
14
|
+
//! the `spec/template-helpers.md` catalogue `helper_vectors.rs` runs) -- this
|
|
15
|
+
//! file is this crate's own conformance pin, alongside `spread_attrs.rs`.
|
|
16
|
+
|
|
17
|
+
use barefootjs::num::JsValue;
|
|
18
|
+
use barefootjs::runtime;
|
|
19
|
+
use std::collections::BTreeMap;
|
|
20
|
+
|
|
21
|
+
fn obj(pairs: &[(&str, JsValue)]) -> JsValue {
|
|
22
|
+
JsValue::Object(pairs.iter().map(|(k, v)| (k.to_string(), v.clone())).collect::<BTreeMap<_, _>>())
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
fn arr(items: &[&str]) -> JsValue {
|
|
26
|
+
JsValue::Array(items.iter().map(|s| JsValue::from(*s)).collect())
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
fn s(v: &str) -> JsValue {
|
|
30
|
+
JsValue::from(v)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
fn run(recv: &JsValue, exclude: &JsValue) -> JsValue {
|
|
34
|
+
runtime::omit(recv, exclude)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
#[test]
|
|
38
|
+
fn excludes_listed_keys() {
|
|
39
|
+
let item = obj(&[("id", s("t1")), ("title", s("one")), ("flag", s("a"))]);
|
|
40
|
+
let out = run(&item, &arr(&["id", "title"]));
|
|
41
|
+
assert_eq!(out, obj(&[("flag", s("a"))]));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
#[test]
|
|
45
|
+
fn keeps_non_excluded_keys_including_non_identifier_names() {
|
|
46
|
+
let item = obj(&[
|
|
47
|
+
("id", s("t1")),
|
|
48
|
+
("title", s("one")),
|
|
49
|
+
("data-priority", s("high")),
|
|
50
|
+
("tag", s("urgent")),
|
|
51
|
+
]);
|
|
52
|
+
let out = run(&item, &arr(&["id", "title"]));
|
|
53
|
+
assert_eq!(out, obj(&[("data-priority", s("high")), ("tag", s("urgent"))]));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
#[test]
|
|
57
|
+
fn empty_exclude_returns_full_object() {
|
|
58
|
+
let item = obj(&[("a", s("1")), ("b", s("2"))]);
|
|
59
|
+
assert_eq!(run(&item, &arr(&[])), item);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
#[test]
|
|
63
|
+
fn excluding_every_key_returns_empty_object() {
|
|
64
|
+
let item = obj(&[("a", s("1")), ("b", s("2"))]);
|
|
65
|
+
assert_eq!(run(&item, &arr(&["a", "b"])), obj(&[]));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
#[test]
|
|
69
|
+
fn non_object_receiver_degrades_to_empty_object() {
|
|
70
|
+
assert_eq!(run(&JsValue::Null, &arr(&["id"])), obj(&[]));
|
|
71
|
+
assert_eq!(run(&s("not a hash"), &arr(&["id"])), obj(&[]));
|
|
72
|
+
assert_eq!(run(&JsValue::Array(vec![s("a")]), &arr(&["id"])), obj(&[]));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
#[test]
|
|
76
|
+
fn non_array_exclude_degrades_to_no_op_filter() {
|
|
77
|
+
let item = obj(&[("a", s("1"))]);
|
|
78
|
+
assert_eq!(run(&item, &JsValue::Null), item);
|
|
79
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
//! `props_attr` -- the `bf-p` hydration-payload attribute. The encoded JSON
|
|
2
|
+
//! is embedded in a SINGLE-quoted attribute, so it must be
|
|
3
|
+
//! attribute-escaped: a raw `'` inside a string value (e.g. a blog
|
|
4
|
+
//! paragraph) terminates the attribute early and the client hydrates from
|
|
5
|
+
//! truncated JSON (empty island text; found via the shared blog-ssr e2e).
|
|
6
|
+
//! Same fix across the Perl, Python, Ruby, and Rust runtimes -- keep the
|
|
7
|
+
//! four tests in sync. Like `render_child.rs`, the method is driven through
|
|
8
|
+
//! a real template render (the `bf` object needs a live `&State`).
|
|
9
|
+
|
|
10
|
+
use barefootjs::num::JsValue;
|
|
11
|
+
use barefootjs::{backend_minijinja, BfInstance, RenderSession};
|
|
12
|
+
use std::collections::BTreeMap;
|
|
13
|
+
use std::path::PathBuf;
|
|
14
|
+
use std::sync::Arc;
|
|
15
|
+
|
|
16
|
+
struct TempDir(PathBuf);
|
|
17
|
+
|
|
18
|
+
impl TempDir {
|
|
19
|
+
fn new() -> TempDir {
|
|
20
|
+
let dir = std::env::temp_dir().join(format!(
|
|
21
|
+
"bf-props-attr-test-{}-{}",
|
|
22
|
+
std::process::id(),
|
|
23
|
+
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos()
|
|
24
|
+
));
|
|
25
|
+
std::fs::create_dir_all(&dir).unwrap();
|
|
26
|
+
TempDir(dir)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
impl Drop for TempDir {
|
|
31
|
+
fn drop(&mut self) {
|
|
32
|
+
std::fs::remove_dir_all(&self.0).ok();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
fn render_props_attr(props: Option<JsValue>) -> String {
|
|
37
|
+
let dir = TempDir::new();
|
|
38
|
+
std::fs::write(dir.0.join("host.j2"), "<div{{ bf.props_attr() | safe }}></div>").unwrap();
|
|
39
|
+
let env = backend_minijinja::build_environment(&dir.0);
|
|
40
|
+
let session = RenderSession::new();
|
|
41
|
+
let mut root = BfInstance::root(Arc::clone(&session), "test");
|
|
42
|
+
root.props = props;
|
|
43
|
+
backend_minijinja::render_named(&env, "host", root.as_mj_value(), &JsValue::Object(BTreeMap::new())).unwrap()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
fn obj(entries: &[(&str, &str)]) -> JsValue {
|
|
47
|
+
JsValue::Object(entries.iter().map(|(k, v)| (k.to_string(), JsValue::String(v.to_string()))).collect())
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
#[test]
|
|
51
|
+
fn empty_props_emit_nothing() {
|
|
52
|
+
assert_eq!(render_props_attr(None), "<div></div>");
|
|
53
|
+
assert_eq!(render_props_attr(Some(JsValue::Object(BTreeMap::new()))), "<div></div>");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
#[test]
|
|
57
|
+
fn json_is_attribute_escaped() {
|
|
58
|
+
let html = render_props_attr(Some(obj(&[("note", "it's <b> & co")])));
|
|
59
|
+
assert_eq!(html, "<div bf-p='{"note":"it's <b> & co"}'></div>");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
#[test]
|
|
63
|
+
fn attribute_round_trips_through_entity_decoding() {
|
|
64
|
+
let html = render_props_attr(Some(obj(&[("note", "it's <b> & co")])));
|
|
65
|
+
let start = html.find("bf-p='").unwrap() + 6;
|
|
66
|
+
let end = html[start..].find('\'').unwrap() + start;
|
|
67
|
+
let decoded = html[start..end]
|
|
68
|
+
.replace(""", "\"")
|
|
69
|
+
.replace("'", "'")
|
|
70
|
+
.replace("<", "<")
|
|
71
|
+
.replace(">", ">")
|
|
72
|
+
.replace("&", "&");
|
|
73
|
+
let parsed: serde_json::Value = serde_json::from_str(&decoded).unwrap();
|
|
74
|
+
assert_eq!(parsed, serde_json::json!({"note": "it's <b> & co"}));
|
|
75
|
+
}
|