@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.
Files changed (87) hide show
  1. package/README.md +194 -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 +85 -0
  5. package/dist/adapter/boolean-result.d.ts.map +1 -0
  6. package/dist/adapter/emit-context.d.ts +107 -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/index.d.ts +6 -0
  13. package/dist/adapter/index.d.ts.map +1 -0
  14. package/dist/adapter/index.js +189091 -0
  15. package/dist/adapter/lib/constants.d.ts +25 -0
  16. package/dist/adapter/lib/constants.d.ts.map +1 -0
  17. package/dist/adapter/lib/ir-scope.d.ts +50 -0
  18. package/dist/adapter/lib/ir-scope.d.ts.map +1 -0
  19. package/dist/adapter/lib/minijinja-naming.d.ts +64 -0
  20. package/dist/adapter/lib/minijinja-naming.d.ts.map +1 -0
  21. package/dist/adapter/lib/types.d.ts +32 -0
  22. package/dist/adapter/lib/types.d.ts.map +1 -0
  23. package/dist/adapter/memo/seed.d.ts +84 -0
  24. package/dist/adapter/memo/seed.d.ts.map +1 -0
  25. package/dist/adapter/minijinja-adapter.d.ts +421 -0
  26. package/dist/adapter/minijinja-adapter.d.ts.map +1 -0
  27. package/dist/adapter/props/prop-classes.d.ts +33 -0
  28. package/dist/adapter/props/prop-classes.d.ts.map +1 -0
  29. package/dist/adapter/spread/spread-codegen.d.ts +63 -0
  30. package/dist/adapter/spread/spread-codegen.d.ts.map +1 -0
  31. package/dist/adapter/value/parsed-literal.d.ts +28 -0
  32. package/dist/adapter/value/parsed-literal.d.ts.map +1 -0
  33. package/dist/build.d.ts +29 -0
  34. package/dist/build.d.ts.map +1 -0
  35. package/dist/build.js +189111 -0
  36. package/dist/conformance-pins.d.ts +13 -0
  37. package/dist/conformance-pins.d.ts.map +1 -0
  38. package/dist/index.d.ts +12 -0
  39. package/dist/index.d.ts.map +1 -0
  40. package/dist/index.js +189112 -0
  41. package/package.json +67 -0
  42. package/runtime/Cargo.lock +124 -0
  43. package/runtime/Cargo.toml +21 -0
  44. package/runtime/src/backend_minijinja.rs +176 -0
  45. package/runtime/src/bin/bf-render.rs +147 -0
  46. package/runtime/src/evaluator.rs +770 -0
  47. package/runtime/src/lib.rs +19 -0
  48. package/runtime/src/manifest.rs +258 -0
  49. package/runtime/src/num.rs +558 -0
  50. package/runtime/src/runtime.rs +1548 -0
  51. package/runtime/src/search_params.rs +173 -0
  52. package/runtime/tests/eval_vectors.rs +94 -0
  53. package/runtime/tests/evaluator.rs +407 -0
  54. package/runtime/tests/helper_vectors.rs +348 -0
  55. package/runtime/tests/manifest.rs +169 -0
  56. package/runtime/tests/omit.rs +79 -0
  57. package/runtime/tests/props_attr.rs +75 -0
  58. package/runtime/tests/query.rs +50 -0
  59. package/runtime/tests/render_child.rs +210 -0
  60. package/runtime/tests/search_params.rs +68 -0
  61. package/runtime/tests/spread_attrs.rs +94 -0
  62. package/runtime/tests/template_primitives.rs +376 -0
  63. package/runtime/tests/vector-divergences.json +33 -0
  64. package/src/__tests__/minijinja-adapter-unit.test.ts +392 -0
  65. package/src/__tests__/minijinja-adapter.test.ts +58 -0
  66. package/src/__tests__/minijinja-counter.test.ts +61 -0
  67. package/src/__tests__/minijinja-query-href.test.ts +101 -0
  68. package/src/__tests__/minijinja-spread-attrs.test.ts +227 -0
  69. package/src/adapter/analysis/component-tree.ts +119 -0
  70. package/src/adapter/boolean-result.ts +177 -0
  71. package/src/adapter/emit-context.ts +119 -0
  72. package/src/adapter/expr/array-method.ts +346 -0
  73. package/src/adapter/expr/emitters.ts +608 -0
  74. package/src/adapter/index.ts +6 -0
  75. package/src/adapter/lib/constants.ts +37 -0
  76. package/src/adapter/lib/ir-scope.ts +95 -0
  77. package/src/adapter/lib/minijinja-naming.ts +85 -0
  78. package/src/adapter/lib/types.ts +35 -0
  79. package/src/adapter/memo/seed.ts +135 -0
  80. package/src/adapter/minijinja-adapter.ts +1796 -0
  81. package/src/adapter/props/prop-classes.ts +65 -0
  82. package/src/adapter/spread/spread-codegen.ts +168 -0
  83. package/src/adapter/value/parsed-literal.ts +76 -0
  84. package/src/build.ts +38 -0
  85. package/src/conformance-pins.ts +101 -0
  86. package/src/index.ts +12 -0
  87. package/src/test-render.ts +680 -0
@@ -0,0 +1,50 @@
1
+ //! `runtime::query` -- ported from
2
+ //! `packages/adapter-jinja/python/tests/test_query.py` (itself a port of
3
+ //! `packages/adapter-perl/t/query.t`).
4
+ //!
5
+ //! The full CROSS-BACKEND behaviour (control flow + form-encoding parity
6
+ //! with the browser's `URLSearchParams`) is defined ONCE in the shared
7
+ //! golden helper vectors and run by `tests/helper_vectors.rs`. This file
8
+ //! keeps a few representative cases for always-on coverage plus a
9
+ //! Rust-runtime-specific defensive case the golden vectors can't express: a
10
+ //! `null` value (JSON has no `undefined`; a JSON `null` stringifies to
11
+ //! `"null"` under JS `String()`, so it can't be a shared vector -- this
12
+ //! runtime coerces `null` to `''` and omits the empty pair, mirroring the
13
+ //! Perl/Python ports' documented `undef`/`None` handling).
14
+
15
+ use barefootjs::num::JsValue;
16
+ use barefootjs::runtime;
17
+
18
+ fn s(v: &str) -> JsValue {
19
+ JsValue::from(v)
20
+ }
21
+ fn b(v: bool) -> JsValue {
22
+ JsValue::Bool(v)
23
+ }
24
+
25
+ #[test]
26
+ fn order_preserved_repeated_key_overwrites_at_first_position() {
27
+ let triples = [b(true), s("sort"), s("title"), b(true), s("tag"), s("go"), b(true), s("sort"), s("date")];
28
+ assert_eq!(runtime::query(&s("/blog"), &triples), "/blog?sort=date&tag=go");
29
+ }
30
+
31
+ #[test]
32
+ fn form_encoding_tilde_star_space() {
33
+ let triples = [b(true), s("t"), s("a~b *c")];
34
+ assert_eq!(runtime::query(&s("/s"), &triples), "/s?t=a%7Eb+*c");
35
+ }
36
+
37
+ #[test]
38
+ fn array_value_appends_pair_per_nonempty_member() {
39
+ let triples = [b(true), s("tag"), JsValue::Array(vec![s("a"), s(""), s("b")])];
40
+ assert_eq!(runtime::query(&s("/list"), &triples), "/list?tag=a&tag=b");
41
+ }
42
+
43
+ #[test]
44
+ fn none_value_coerced_to_empty_and_omitted() {
45
+ let triples = [b(true), s("tag"), JsValue::Null];
46
+ assert_eq!(runtime::query(&s("/list"), &triples), "/list");
47
+
48
+ let triples2 = [b(true), s("tag"), JsValue::Null, b(true), s("keep"), s("me")];
49
+ assert_eq!(runtime::query(&s("/list"), &triples2), "/list?keep=me");
50
+ }
@@ -0,0 +1,210 @@
1
+ //! `render_child` renderer-invocation contract, ported from
2
+ //! `packages/adapter-jinja/python/tests/test_render_child.py` (itself a
3
+ //! port of `packages/adapter-perl/t/render_child.t`).
4
+ //!
5
+ //! Renderer contract (#1897): a child's scope/slot identity chains off the
6
+ //! CALLER's `bf` instance (`self` inside `Object::call_method`), not off
7
+ //! whichever instance originally registered the renderer. Unlike the
8
+ //! Python port (which can stub a bare `BarefootJS` instance and call
9
+ //! `render_child` directly with a hand-built backend), this crate's
10
+ //! `render_child` takes a live `&minijinja::State` -- there is no public
11
+ //! way to construct one outside an active render -- so these tests drive
12
+ //! the SAME contract through real `.j2` template renders (mirroring what
13
+ //! `bf-render` does), which is the natural Rust-shaped equivalent and
14
+ //! additionally exercises the actual `Object::call_method` dispatch path.
15
+
16
+ use barefootjs::num::JsValue;
17
+ use barefootjs::runtime::{js_to_mj, ChildRendererSpec};
18
+ use barefootjs::{backend_minijinja, BfInstance, RenderSession};
19
+ use minijinja::value::Value as MjValue;
20
+ use std::collections::BTreeMap;
21
+ use std::path::PathBuf;
22
+ use std::sync::Arc;
23
+
24
+ struct TempDir(PathBuf);
25
+
26
+ impl TempDir {
27
+ fn new(tag: &str) -> TempDir {
28
+ let dir = std::env::temp_dir().join(format!("bf-render-child-test-{tag}-{}-{}", std::process::id(), std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos()));
29
+ std::fs::create_dir_all(&dir).unwrap();
30
+ TempDir(dir)
31
+ }
32
+ fn write(&self, name: &str, content: &str) {
33
+ std::fs::write(self.0.join(name), content).unwrap();
34
+ }
35
+ }
36
+
37
+ impl Drop for TempDir {
38
+ fn drop(&mut self) {
39
+ std::fs::remove_dir_all(&self.0).ok();
40
+ }
41
+ }
42
+
43
+ /// `vars` for [`backend_minijinja::render_entry`] (top-level render), which
44
+ /// still takes JSON-shaped `&JsValue`.
45
+ fn no_defaults() -> JsValue {
46
+ JsValue::Object(BTreeMap::new())
47
+ }
48
+
49
+ /// `ChildRendererSpec::ssr_defaults`, which is `minijinja::Value` (props
50
+ /// stay `Value` end-to-end through `render_child` -- see its docstring).
51
+ fn no_ssr_defaults() -> MjValue {
52
+ MjValue::from(BTreeMap::<String, MjValue>::new())
53
+ }
54
+
55
+ #[test]
56
+ fn nested_render_child_chains_scope_off_the_caller_not_the_registrant() {
57
+ let dir = TempDir::new("nested");
58
+ dir.write("root.j2", "{{ bf.render_child('mid', {'_bf_slot': 's0'}) | safe }}");
59
+ dir.write("mid.j2", "[{{ bf.scope_attr() }}:{{ bf.render_child('leaf', {'_bf_slot': 'inner'}) | safe }}]");
60
+ dir.write("leaf.j2", "leaf@{{ bf.scope_attr() }}");
61
+
62
+ let env = backend_minijinja::build_environment(&dir.0);
63
+ let session = RenderSession::new();
64
+ session.register_child_renderer(
65
+ "mid".to_string(),
66
+ ChildRendererSpec { component_name: "Mid".to_string(), template: "mid".to_string(), ssr_defaults: no_ssr_defaults(), rest_props_name: None, param_names: vec![] },
67
+ );
68
+ session.register_child_renderer(
69
+ "leaf".to_string(),
70
+ ChildRendererSpec { component_name: "Leaf".to_string(), template: "leaf".to_string(), ssr_defaults: no_ssr_defaults(), rest_props_name: None, param_names: vec![] },
71
+ );
72
+
73
+ let root = BfInstance::root(Arc::clone(&session), "Root_test");
74
+ let out = backend_minijinja::render_entry(&env, "root", root.as_mj_value(), &no_defaults(), &[]).unwrap();
75
+
76
+ // `mid`'s scope is `<root scope>_s0`; `leaf` is rendered from INSIDE
77
+ // `mid`'s template (where `bf` is the `mid` instance), so its scope
78
+ // chains off `mid`'s scope (`Root_test_s0_inner`), NOT off the root's
79
+ // scope directly (`Root_test_inner`) -- proving the caller-chaining
80
+ // contract.
81
+ assert_eq!(out, "[Root_test_s0:leaf@Root_test_s0_inner]");
82
+ }
83
+
84
+ #[test]
85
+ fn missing_renderer_propagates_as_render_error() {
86
+ let dir = TempDir::new("missing");
87
+ dir.write("root.j2", "{{ bf.render_child('missing') }}");
88
+
89
+ let env = backend_minijinja::build_environment(&dir.0);
90
+ let session = RenderSession::new();
91
+ let root = BfInstance::root(session, "Root_test");
92
+ let err = backend_minijinja::render_entry(&env, "root", root.as_mj_value(), &no_defaults(), &[]).unwrap_err();
93
+ assert!(err.to_string().contains("No renderer registered for child component 'missing'"), "unexpected error: {err}");
94
+ }
95
+
96
+ #[test]
97
+ fn reserved_word_prop_is_mangled() {
98
+ let dir = TempDir::new("reserved");
99
+ dir.write("root.j2", "{{ bf.render_child('kw', {'class': 'x', 'id': 'y'}) }}");
100
+ dir.write("kw.j2", "{{ class_ }}-{{ id }}");
101
+
102
+ let env = backend_minijinja::build_environment(&dir.0);
103
+ let session = RenderSession::new();
104
+ session.register_child_renderer(
105
+ "kw".to_string(),
106
+ ChildRendererSpec { component_name: "Kw".to_string(), template: "kw".to_string(), ssr_defaults: no_ssr_defaults(), rest_props_name: None, param_names: vec![] },
107
+ );
108
+ let root = BfInstance::root(session, "Root_test");
109
+ let out = backend_minijinja::render_entry(&env, "root", root.as_mj_value(), &no_defaults(), &[]).unwrap();
110
+ assert_eq!(out, "x-y");
111
+ }
112
+
113
+ #[test]
114
+ fn rest_bag_routing_and_ssr_defaults_merge() {
115
+ let dir = TempDir::new("restbag");
116
+ // `Card` declares `title` as a named param and destructures the rest
117
+ // into `rest`; `subtitle` isn't a declared param, so it must land in
118
+ // `rest`, not as a top-level stash var.
119
+ dir.write("root.j2", "{{ bf.render_child('card', {'title': 'Hi', 'subtitle': 'Sub', 'extra': 'E'}) }}");
120
+ dir.write("card.j2", "{{ title }}|{{ rest.subtitle }}|{{ rest.extra }}|{{ theme }}");
121
+
122
+ let env = backend_minijinja::build_environment(&dir.0);
123
+ let session = RenderSession::new();
124
+ let mut defaults = BTreeMap::new();
125
+ defaults.insert("theme".to_string(), JsValue::from("dark"));
126
+ session.register_child_renderer(
127
+ "card".to_string(),
128
+ ChildRendererSpec {
129
+ component_name: "Card".to_string(),
130
+ template: "card".to_string(),
131
+ ssr_defaults: js_to_mj(&JsValue::Object(defaults)),
132
+ rest_props_name: Some("rest".to_string()),
133
+ param_names: vec!["title".to_string()],
134
+ },
135
+ );
136
+ let root = BfInstance::root(session, "Root_test");
137
+ let out = backend_minijinja::render_entry(&env, "root", root.as_mj_value(), &no_defaults(), &[]).unwrap();
138
+ assert_eq!(out, "Hi|Sub|E|dark");
139
+ }
140
+
141
+ #[test]
142
+ fn loop_child_without_slot_gets_a_fresh_component_prefixed_scope() {
143
+ let dir = TempDir::new("loopchild");
144
+ dir.write("root.j2", "{{ bf.render_child('item', {}) }}|{{ bf.render_child('item', {}) }}");
145
+ dir.write("item.j2", "{{ bf.scope_attr() }}");
146
+
147
+ let env = backend_minijinja::build_environment(&dir.0);
148
+ let session = RenderSession::new();
149
+ session.register_child_renderer(
150
+ "item".to_string(),
151
+ ChildRendererSpec { component_name: "Item".to_string(), template: "item".to_string(), ssr_defaults: no_ssr_defaults(), rest_props_name: None, param_names: vec![] },
152
+ );
153
+ let root = BfInstance::root(session, "Root_test");
154
+ let out = backend_minijinja::render_entry(&env, "root", root.as_mj_value(), &no_defaults(), &[]).unwrap();
155
+ let parts: Vec<&str> = out.split('|').collect();
156
+ assert_eq!(parts.len(), 2);
157
+ for p in &parts {
158
+ assert!(p.starts_with("Item_"), "expected an Item_<rand> scope id, got {p:?}");
159
+ }
160
+ assert_ne!(parts[0], parts[1], "two loop-child renders must get distinct scope ids");
161
+ }
162
+
163
+ #[test]
164
+ fn childless_invocation_renders_with_no_props() {
165
+ let dir = TempDir::new("childless");
166
+ dir.write("root.j2", "{{ bf.render_child('leaf') }}");
167
+ dir.write("leaf.j2", "leaf-ok");
168
+
169
+ let env = backend_minijinja::build_environment(&dir.0);
170
+ let session = RenderSession::new();
171
+ session.register_child_renderer(
172
+ "leaf".to_string(),
173
+ ChildRendererSpec { component_name: "Leaf".to_string(), template: "leaf".to_string(), ssr_defaults: no_ssr_defaults(), rest_props_name: None, param_names: vec![] },
174
+ );
175
+ let root = BfInstance::root(session, "Root_test");
176
+ let out = backend_minijinja::render_entry(&env, "root", root.as_mj_value(), &no_defaults(), &[]).unwrap();
177
+ assert_eq!(out, "leaf-ok");
178
+ }
179
+
180
+ #[test]
181
+ fn children_safe_value_survives_render_child_and_bf_string_unescaped() {
182
+ // Regression for the "children lost their safe flag" bug: a JSX
183
+ // children capture (`{% set cap %}...{% endset %}`, mirroring what the
184
+ // TS emitter generates for children forwarding) is a SAFE `Value`.
185
+ // Passed as a child prop through `bf.string(...)` (as the emitter does
186
+ // at every text/attribute-position value) and through
187
+ // `bf.render_child`'s prop plumbing, it must reach the child template
188
+ // STILL safe -- i.e. NOT HTML-escaped -- proving both fixes: (1)
189
+ // `render_child` keeps props as `Value` end-to-end instead of
190
+ // round-tripping through `JsValue` (which has no safe/unsafe
191
+ // distinction), and (2) `bf.string` passes an already-safe string
192
+ // input through unchanged (mirrors the Python runtime's `js_string`,
193
+ // where a `str` input -- including a `Markup` -- is returned as-is).
194
+ let dir = TempDir::new("safechildren");
195
+ dir.write(
196
+ "root.j2",
197
+ "{% set cap %}<span>hi</span>{% endset %}{{ bf.render_child('wrap', {'inner': bf.string(cap)}) | safe }}",
198
+ );
199
+ dir.write("wrap.j2", "[{{ inner }}]");
200
+
201
+ let env = backend_minijinja::build_environment(&dir.0);
202
+ let session = RenderSession::new();
203
+ session.register_child_renderer(
204
+ "wrap".to_string(),
205
+ ChildRendererSpec { component_name: "Wrap".to_string(), template: "wrap".to_string(), ssr_defaults: no_ssr_defaults(), rest_props_name: None, param_names: vec![] },
206
+ );
207
+ let root = BfInstance::root(session, "Root_test");
208
+ let out = backend_minijinja::render_entry(&env, "root", root.as_mj_value(), &no_defaults(), &[]).unwrap();
209
+ assert_eq!(out, "[<span>hi</span>]");
210
+ }
@@ -0,0 +1,68 @@
1
+ //! `barefootjs::SearchParams` -- Rust-specific concerns, ported from
2
+ //! `packages/adapter-jinja/python/tests/test_search_params.py` (itself a
3
+ //! port of `packages/adapter-perl/t/search_params.t`).
4
+ //!
5
+ //! The cross-language VALUE semantics of `get` are owned by the
6
+ //! language-independent golden vectors (`search_params_get` in
7
+ //! `tests/helper_vectors.rs`), so Go/Perl/Python/Rust parity there is
8
+ //! mechanical. This file covers only what those value vectors can't: the
9
+ //! constructor seam, lenient parsing (never panics), and UTF-8 decoding.
10
+ //! (Mirrors `src/search_params.rs`'s inline `#[cfg(test)]` module -- kept
11
+ //! as its own top-level file too, per the design doc's file layout, since
12
+ //! it is the direct structural port of the Python test module.)
13
+
14
+ use barefootjs::SearchParams;
15
+
16
+ #[test]
17
+ fn constructor_and_get() {
18
+ let sp = SearchParams::new("sort=price");
19
+ assert_eq!(sp.get("sort"), Some("price"));
20
+ assert_eq!(SearchParams::new("").get("sort"), None);
21
+ }
22
+
23
+ #[test]
24
+ fn none_composition_coalesces_only_none() {
25
+ // The adapters lower `searchParams().get(k) ?? d` to a minijinja
26
+ // expression that coalesces only an absent key (not a bare `or`,
27
+ // which would also default a present-but-empty value) -- so an
28
+ // absent key falls back to the author's default while a
29
+ // present-but-empty value keeps ''.
30
+ let absent = SearchParams::new("other=x");
31
+ assert_eq!(absent.get("sort").unwrap_or("none"), "none");
32
+
33
+ let empty = SearchParams::new("sort=");
34
+ assert_eq!(empty.get("sort").unwrap_or("none"), "");
35
+ }
36
+
37
+ #[test]
38
+ fn utf8_percent_decoding() {
39
+ let sp = SearchParams::new("q=%E2%9C%93");
40
+ assert_eq!(sp.get("q"), Some("\u{2713}"));
41
+ }
42
+
43
+ #[test]
44
+ fn lenient_parsing_never_panics() {
45
+ let _ = SearchParams::new("");
46
+ assert_eq!(SearchParams::new("&&&").get("x"), None);
47
+ assert_eq!(SearchParams::new("=novalue").get("x"), None);
48
+ }
49
+
50
+ #[test]
51
+ fn to_value_exposes_get_method_to_templates() {
52
+ // Smoke-test the `minijinja::value::Object` wiring end-to-end via a
53
+ // real template render (`bf-render`'s own path), rather than only unit
54
+ // testing `SearchParams::get` directly.
55
+ let dir = std::env::temp_dir().join(format!("bf-search-params-test-{}", std::process::id()));
56
+ std::fs::create_dir_all(&dir).unwrap();
57
+ std::fs::write(dir.join("t.j2"), "{{ searchParams.get('sort') }}|{{ (searchParams.get('missing') if (searchParams.get('missing') is defined and searchParams.get('missing') is not none) else 'default') }}").unwrap();
58
+
59
+ let env = barefootjs::backend_minijinja::build_environment(&dir);
60
+ let session = barefootjs::RenderSession::new();
61
+ let root = barefootjs::BfInstance::root(session, "test");
62
+ let vars = barefootjs::num::JsValue::Object(Default::default());
63
+ let extra = vec![("searchParams".to_string(), SearchParams::new("sort=price").to_value())];
64
+ let out = barefootjs::backend_minijinja::render_entry(&env, "t", root.as_mj_value(), &vars, &extra).unwrap();
65
+ assert_eq!(out, "price|default");
66
+
67
+ std::fs::remove_dir_all(&dir).ok();
68
+ }
@@ -0,0 +1,94 @@
1
+ //! `runtime::spread_attrs` -- ported from
2
+ //! `packages/adapter-jinja/python/tests/test_spread_attrs.py` (itself a
3
+ //! port of `packages/adapter-perl/t/spread_attrs.t`).
4
+ //!
5
+ //! JSX intrinsic-element spread runtime helper (#1407 follow-up). Mirrors
6
+ //! the JS `spreadAttrs` runtime and the Go/Perl/Python adapters'
7
+ //! equivalents so SSR output stays byte-equal across every adapter --
8
+ //! cross-adapter parity regressions surface here first.
9
+
10
+ use barefootjs::num::JsValue;
11
+ use barefootjs::runtime;
12
+ use std::collections::BTreeMap;
13
+
14
+ fn obj(pairs: &[(&str, JsValue)]) -> JsValue {
15
+ JsValue::Object(pairs.iter().map(|(k, v)| (k.to_string(), v.clone())).collect::<BTreeMap<_, _>>())
16
+ }
17
+
18
+ fn s(v: &str) -> JsValue {
19
+ JsValue::from(v)
20
+ }
21
+
22
+ fn run(bag: &JsValue) -> String {
23
+ runtime::spread_attrs(bag)
24
+ }
25
+
26
+ #[test]
27
+ fn basic_shapes() {
28
+ assert_eq!(run(&JsValue::Null), "");
29
+ assert_eq!(run(&obj(&[])), "");
30
+ assert_eq!(run(&s("not a hash")), "");
31
+ assert_eq!(run(&obj(&[("id", s("a"))])), "id=\"a\"");
32
+ }
33
+
34
+ #[test]
35
+ fn alphabetic_key_order() {
36
+ assert_eq!(run(&obj(&[("id", s("a")), ("class", s("on"))])), "class=\"on\" id=\"a\"");
37
+ }
38
+
39
+ #[test]
40
+ fn key_remapping() {
41
+ assert_eq!(run(&obj(&[("className", s("foo"))])), "class=\"foo\"");
42
+ assert_eq!(run(&obj(&[("htmlFor", s("x"))])), "for=\"x\"");
43
+ assert_eq!(run(&obj(&[("dataPriority", s("high"))])), "data-priority=\"high\"");
44
+ // SVG XML attrs are case-sensitive -- preserve verbatim.
45
+ assert_eq!(run(&obj(&[("viewBox", s("0 0 10 10"))])), "viewBox=\"0 0 10 10\"");
46
+ assert_eq!(run(&obj(&[("clipPathUnits", s("userSpaceOnUse"))])), "clipPathUnits=\"userSpaceOnUse\"");
47
+ // JS-reference parity (#1411): a leading uppercase letter emits a
48
+ // leading dash.
49
+ assert_eq!(run(&obj(&[("XData", s("x"))])), "-x-data=\"x\"");
50
+ }
51
+
52
+ #[test]
53
+ fn event_handlers_js_predicate_parity() {
54
+ assert_eq!(run(&obj(&[("onClick", s("fn")), ("id", s("a"))])), "id=\"a\"");
55
+ assert_eq!(run(&obj(&[("on_custom", s("fn")), ("id", s("a"))])), "id=\"a\"");
56
+ assert_eq!(run(&obj(&[("on0", s("fn")), ("id", s("a"))])), "id=\"a\"");
57
+ assert_eq!(run(&obj(&[("oncology", s("x"))])), "oncology=\"x\"");
58
+ }
59
+
60
+ #[test]
61
+ fn children_skipped_ref_passed_through() {
62
+ assert_eq!(run(&obj(&[("children", s("x")), ("id", s("a"))])), "id=\"a\"");
63
+ // JS `spreadAttrs` does NOT filter `ref` (`applyRestAttrs` does --
64
+ // that's a separate divergence).
65
+ assert_eq!(run(&obj(&[("ref", s("x")), ("id", s("a"))])), "id=\"a\" ref=\"x\"");
66
+ }
67
+
68
+ #[test]
69
+ fn boolean_values() {
70
+ assert_eq!(run(&obj(&[("hidden", JsValue::Bool(true)), ("id", s("a"))])), "hidden id=\"a\"");
71
+ assert_eq!(run(&obj(&[("hidden", JsValue::Bool(false)), ("id", s("a"))])), "id=\"a\"");
72
+ // Plain numeric 0 renders as a value (matches `tabindex="0"`).
73
+ assert_eq!(run(&obj(&[("tabindex", JsValue::Number(0.0))])), "tabindex=\"0\"");
74
+ }
75
+
76
+ #[test]
77
+ fn nullish_skip() {
78
+ assert_eq!(run(&obj(&[("a", JsValue::Null), ("b", s("x"))])), "b=\"x\"");
79
+ }
80
+
81
+ #[test]
82
+ fn html_escape() {
83
+ assert_eq!(run(&obj(&[("title", s("<b>\"x\"</b>"))])), "title=\"&lt;b&gt;&#34;x&#34;&lt;/b&gt;\"");
84
+ assert_eq!(run(&obj(&[("alt", s("tom & jerry"))])), "alt=\"tom &amp; jerry\"");
85
+ }
86
+
87
+ #[test]
88
+ fn style_object_lowering() {
89
+ assert_eq!(
90
+ run(&obj(&[("style", obj(&[("backgroundColor", s("red")), ("color", s("white"))]))])),
91
+ "style=\"background-color:red;color:white\""
92
+ );
93
+ assert_eq!(run(&obj(&[("style", s("color:red"))])), "style=\"color:red\"");
94
+ }