@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,173 @@
1
+ //! Port of `packages/adapter-jinja/python/barefootjs/search_params.py`
2
+ //! (itself a port of `packages/adapter-perl/lib/BarefootJS/SearchParams.pm`).
3
+ //!
4
+ //! Request-scoped SSR view of the query string behind the reactive
5
+ //! `searchParams()` environment signal. The framework integration builds
6
+ //! one per request from the request URL and threads it into the template
7
+ //! scope as `searchParams` (the camelCase JS name the adapters keep, like
8
+ //! every other signal/prop var); the compiled template reads it via
9
+ //! `{{ searchParams.get('key') }}` -- so this is exposed to minijinja
10
+ //! templates as a [`minijinja::value::Object`] (implemented below) whose
11
+ //! `.get(key)` method call dispatches to [`SearchParams::get`].
12
+ //!
13
+ //! Semantics mirror the browser's `URLSearchParams.get` exactly under the
14
+ //! adapters' `?? -> or` lowering: `get()` returns the first value for a
15
+ //! key, or `None`/absent when the key is absent. The minijinja lowering of
16
+ //! `??` should coalesce only an absent/none result (a bare `or` would ALSO
17
+ //! coalesce a present-but-empty string, which is wrong), preserving the
18
+ //! distinction JS `??` draws between `null` and `''` -- the `Object` impl
19
+ //! below returns `Value::UNDEFINED` (not `Value::from(())`) for an absent
20
+ //! key so the emitted `(x if (x is defined and x is not none) else d)`
21
+ //! lowering falls through to the default exactly once.
22
+
23
+ use minijinja::value::{Enumerator, Object, Value as MjValue};
24
+ use minijinja::{Error, ErrorKind, State};
25
+ use std::collections::HashMap;
26
+ use std::sync::Arc;
27
+
28
+ /// Percent/`+`-decode a query-string component, mirroring
29
+ /// `URLSearchParams`'s `application/x-www-form-urlencoded` parsing. Never
30
+ /// panics on malformed input (lenient parsing, matching the browser).
31
+ ///
32
+ /// Operates byte-wise (not char-wise): percent-escapes are always ASCII, so
33
+ /// copying every non-escape byte verbatim keeps multi-byte UTF-8 sequences
34
+ /// intact while still recognizing `%XX` triples.
35
+ fn decode(s: &str) -> String {
36
+ let bytes = s.replace('+', " ").into_bytes();
37
+ let mut raw: Vec<u8> = Vec::with_capacity(bytes.len());
38
+ let mut i = 0usize;
39
+ while i < bytes.len() {
40
+ if bytes[i] == b'%' && i + 2 < bytes.len() {
41
+ let (a, b) = (bytes[i + 1], bytes[i + 2]);
42
+ if (a as char).is_ascii_hexdigit() && (b as char).is_ascii_hexdigit() {
43
+ let hex = std::str::from_utf8(&bytes[i + 1..i + 3]).unwrap_or("00");
44
+ if let Ok(byte) = u8::from_str_radix(hex, 16) {
45
+ raw.push(byte);
46
+ i += 3;
47
+ continue;
48
+ }
49
+ }
50
+ }
51
+ raw.push(bytes[i]);
52
+ i += 1;
53
+ }
54
+ // Lenient: a byte run that isn't valid UTF-8 is kept (with replacement
55
+ // characters for the invalid bytes) rather than raising -- mirrors
56
+ // Perl's `utf8::decode`, which never dies.
57
+ String::from_utf8(raw).unwrap_or_else(|e| String::from_utf8_lossy(e.as_bytes()).into_owned())
58
+ }
59
+
60
+ #[derive(Debug)]
61
+ pub struct SearchParams {
62
+ values: HashMap<String, Vec<String>>,
63
+ }
64
+
65
+ impl SearchParams {
66
+ /// Parse a raw query string into the reader. A leading '?' is
67
+ /// tolerated, '+' decodes to a space, and %XX escapes are decoded.
68
+ pub fn new(query: &str) -> SearchParams {
69
+ let query = query.strip_prefix('?').unwrap_or(query);
70
+ let mut values: HashMap<String, Vec<String>> = HashMap::new();
71
+ for pair in query.split(['&', ';']) {
72
+ if pair.is_empty() {
73
+ continue;
74
+ }
75
+ let (key, val) = match pair.split_once('=') {
76
+ Some((k, v)) => (k, Some(v)),
77
+ None => (pair, None),
78
+ };
79
+ let key = decode(key);
80
+ let val_decoded = val.map(decode).unwrap_or_default();
81
+ values.entry(key).or_default().push(val_decoded);
82
+ }
83
+ SearchParams { values }
84
+ }
85
+
86
+ /// First value for `key`, or `None` when the key is absent. A
87
+ /// present-but-empty value returns `Some("")`.
88
+ pub fn get(&self, key: &str) -> Option<&str> {
89
+ self.values.get(key).and_then(|v| v.first()).map(|s| s.as_str())
90
+ }
91
+
92
+ /// Wrap as a template-facing `minijinja::value::Value` (`from_object`
93
+ /// wraps in its own internal `Arc`, matching the `self: &Arc<Self>`
94
+ /// convention the `Object` trait's methods below use).
95
+ pub fn to_value(self) -> MjValue {
96
+ MjValue::from_object(self)
97
+ }
98
+ }
99
+
100
+ impl Object for SearchParams {
101
+ fn call_method(
102
+ self: &Arc<Self>,
103
+ _state: &State<'_, '_>,
104
+ method: &str,
105
+ args: &[MjValue],
106
+ ) -> Result<MjValue, Error> {
107
+ if method != "get" {
108
+ return Err(Error::from(ErrorKind::UnknownMethod));
109
+ }
110
+ let key = args
111
+ .first()
112
+ .and_then(|v| v.as_str())
113
+ .ok_or_else(|| Error::new(ErrorKind::MissingArgument, "get() requires a key"))?;
114
+ match self.get(key) {
115
+ // Present-but-empty stays a real (empty) string so `?? d` does
116
+ // NOT fall back to the default -- only an absent key does.
117
+ Some(v) => Ok(MjValue::from(v)),
118
+ None => Ok(MjValue::UNDEFINED),
119
+ }
120
+ }
121
+
122
+ fn enumerate(self: &Arc<Self>) -> Enumerator {
123
+ Enumerator::NonEnumerable
124
+ }
125
+ }
126
+
127
+ #[cfg(test)]
128
+ mod tests {
129
+ use super::*;
130
+
131
+ #[test]
132
+ fn lazy_factory_and_get() {
133
+ let sp = SearchParams::new("sort=price");
134
+ assert_eq!(sp.get("sort"), Some("price"));
135
+ let empty = SearchParams::new("");
136
+ assert_eq!(empty.get("anything"), None);
137
+ }
138
+
139
+ #[test]
140
+ fn none_composition_coalesces_only_none() {
141
+ let absent = SearchParams::new("other=x");
142
+ assert_eq!(absent.get("sort"), None);
143
+
144
+ let empty = SearchParams::new("sort=");
145
+ assert_eq!(empty.get("sort"), Some(""));
146
+ }
147
+
148
+ #[test]
149
+ fn utf8_percent_decoding() {
150
+ let sp = SearchParams::new("q=%E2%9C%93");
151
+ assert_eq!(sp.get("q"), Some("\u{2713}"));
152
+ }
153
+
154
+ #[test]
155
+ fn lenient_parsing_never_panics() {
156
+ let _ = SearchParams::new("");
157
+ assert_eq!(SearchParams::new("&&&").get("x"), None);
158
+ assert_eq!(SearchParams::new("=novalue").get("x"), None);
159
+ }
160
+
161
+ #[test]
162
+ fn plus_decodes_to_space() {
163
+ let sp = SearchParams::new("q=a+b");
164
+ assert_eq!(sp.get("q"), Some("a b"));
165
+ }
166
+
167
+ #[test]
168
+ fn leading_question_mark_tolerated() {
169
+ let sp = SearchParams::new("?a=1&b=2");
170
+ assert_eq!(sp.get("a"), Some("1"));
171
+ assert_eq!(sp.get("b"), Some("2"));
172
+ }
173
+ }
@@ -0,0 +1,94 @@
1
+ //! Golden ParsedExpr-evaluator vectors, ported from
2
+ //! `packages/adapter-jinja/python/tests/test_eval_vectors.py` (itself a
3
+ //! port of `packages/adapter-perl/t/eval_vectors.t`).
4
+ //!
5
+ //! Runs `packages/adapter-tests/vectors/eval-vectors.json` --
6
+ //! generated from the JS reference evaluator, shared with the Go and Perl
7
+ //! evaluators -- against `barefootjs::evaluator::evaluate`. The evaluator
8
+ //! is JS-faithful by contract, so unlike the helper vectors there are NO
9
+ //! Rust-side divergences here: each case's real ParsedExpr tree, evaluated
10
+ //! against its environment, must reproduce the JS-computed expect exactly.
11
+
12
+ use barefootjs::evaluator::{self, Env};
13
+ use barefootjs::num::JsValue;
14
+ use serde_json::Value as JsonValue;
15
+ use std::path::PathBuf;
16
+
17
+ fn vectors_path() -> PathBuf {
18
+ PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../adapter-tests/vectors/eval-vectors.json")
19
+ }
20
+
21
+ /// Spec value-compat comparison -- non-finite sentinel hashes, booleans by
22
+ /// TYPE (the evaluator result must ITSELF be a real bool, not a truthy
23
+ /// number), numbers numerically, arrays/objects recursively, strings by
24
+ /// equality.
25
+ fn matches(got: &JsValue, expect: &JsonValue) -> bool {
26
+ if expect.is_null() {
27
+ return matches!(got, JsValue::Null);
28
+ }
29
+ if let Some(kind) = expect.get("$num").and_then(|v| v.as_str()) {
30
+ return match got {
31
+ JsValue::Number(g) => match kind {
32
+ "NaN" => g.is_nan(),
33
+ "Infinity" => *g == f64::INFINITY,
34
+ "-Infinity" => *g == f64::NEG_INFINITY,
35
+ _ => false,
36
+ },
37
+ _ => false,
38
+ };
39
+ }
40
+ if let Some(b) = expect.as_bool() {
41
+ return matches!(got, JsValue::Bool(g) if *g == b);
42
+ }
43
+ if let Some(arr) = expect.as_array() {
44
+ return match got {
45
+ JsValue::Array(g) => g.len() == arr.len() && g.iter().zip(arr).all(|(g, e)| matches(g, e)),
46
+ _ => false,
47
+ };
48
+ }
49
+ if let Some(obj) = expect.as_object() {
50
+ return match got {
51
+ JsValue::Object(g) => g.len() == obj.len() && obj.iter().all(|(k, v)| g.get(k).map(|gv| matches(gv, v)).unwrap_or(false)),
52
+ _ => false,
53
+ };
54
+ }
55
+ // Numeric comparison only when BOTH are real numbers (not
56
+ // numeric-looking strings) -- e.g. String(42) must return the string
57
+ // "42", and evaluating it as the number 42 must NOT pass.
58
+ if let Some(n) = expect.as_f64() {
59
+ return matches!(got, JsValue::Number(g) if *g == n);
60
+ }
61
+ if let Some(s) = expect.as_str() {
62
+ return matches!(got, JsValue::String(g) if g == s);
63
+ }
64
+ false
65
+ }
66
+
67
+ #[test]
68
+ fn eval_vectors_match_js_reference() {
69
+ let path = vectors_path();
70
+ if !path.exists() {
71
+ eprintln!("skipping: eval vectors not available outside the monorepo checkout ({path:?})");
72
+ return;
73
+ }
74
+ let doc: JsonValue = serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
75
+ let cases = doc["cases"].as_array().expect("eval-vectors.json has no cases");
76
+ assert!(!cases.is_empty(), "eval-vectors.json contains no cases");
77
+
78
+ let mut failures = Vec::new();
79
+ for case in cases {
80
+ let note = case["note"].as_str().unwrap_or("<unnamed>");
81
+ let expr = &case["expr"];
82
+ let expect = &case["expect"];
83
+ let env: Env = case["env"]
84
+ .as_object()
85
+ .map(|m| m.iter().map(|(k, v)| (k.clone(), JsValue::from_json(v))).collect())
86
+ .unwrap_or_default();
87
+
88
+ let got = evaluator::evaluate(expr, &env);
89
+ if !matches(&got, expect) {
90
+ failures.push(format!("{note}: got {got:?}, want {expect:?}"));
91
+ }
92
+ }
93
+ assert!(failures.is_empty(), "\n{}", failures.join("\n"));
94
+ }