@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
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@barefootjs/rust",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "minijinja (Rust) adapter for BarefootJS — compiles IR to .j2 templates and ships a Rust rendering runtime (packages/adapter-rust/runtime/); runs under any Rust web framework (axum, etc.)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./adapter": {
|
|
14
|
+
"types": "./dist/adapter/index.d.ts",
|
|
15
|
+
"import": "./dist/adapter/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./test-render": {
|
|
18
|
+
"bun": "./src/test-render.ts"
|
|
19
|
+
},
|
|
20
|
+
"./build": {
|
|
21
|
+
"types": "./dist/build.d.ts",
|
|
22
|
+
"import": "./dist/build.js"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"src",
|
|
28
|
+
"runtime",
|
|
29
|
+
"README.md"
|
|
30
|
+
],
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "bun run build:js && bun run build:types",
|
|
33
|
+
"build:js": "bun build ./src/index.ts ./src/adapter/index.ts ./src/build.ts --root ./src --outdir ./dist --format esm --external @barefootjs/jsx --external @barefootjs/shared",
|
|
34
|
+
"build:types": "tsgo --emitDeclarationOnly --outDir ./dist",
|
|
35
|
+
"test": "bun test",
|
|
36
|
+
"clean": "rm -rf dist",
|
|
37
|
+
"prepack": "node ../../scripts/swap-publish-config.mjs pack",
|
|
38
|
+
"postpack": "node ../../scripts/swap-publish-config.mjs unpack"
|
|
39
|
+
},
|
|
40
|
+
"keywords": [
|
|
41
|
+
"jinja",
|
|
42
|
+
"jinja2",
|
|
43
|
+
"minijinja",
|
|
44
|
+
"rust",
|
|
45
|
+
"axum",
|
|
46
|
+
"barefoot",
|
|
47
|
+
"ssr"
|
|
48
|
+
],
|
|
49
|
+
"author": "kobaken <kentafly88@gmail.com>",
|
|
50
|
+
"license": "MIT",
|
|
51
|
+
"repository": {
|
|
52
|
+
"type": "git",
|
|
53
|
+
"url": "https://github.com/piconic-ai/barefootjs",
|
|
54
|
+
"directory": "packages/adapter-rust"
|
|
55
|
+
},
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"@barefootjs/shared": "0.17.1"
|
|
58
|
+
},
|
|
59
|
+
"peerDependencies": {
|
|
60
|
+
"@barefootjs/jsx": ">=0.2.0"
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"@barefootjs/adapter-tests": "0.1.0",
|
|
64
|
+
"@barefootjs/jsx": "0.17.1",
|
|
65
|
+
"typescript": "^5.0.0"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# This file is automatically @generated by Cargo.
|
|
2
|
+
# It is not intended for manual editing.
|
|
3
|
+
version = 4
|
|
4
|
+
|
|
5
|
+
[[package]]
|
|
6
|
+
name = "barefootjs"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
dependencies = [
|
|
9
|
+
"minijinja",
|
|
10
|
+
"serde",
|
|
11
|
+
"serde_json",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[[package]]
|
|
15
|
+
name = "itoa"
|
|
16
|
+
version = "1.0.18"
|
|
17
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
18
|
+
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
|
19
|
+
|
|
20
|
+
[[package]]
|
|
21
|
+
name = "memchr"
|
|
22
|
+
version = "2.8.2"
|
|
23
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
24
|
+
checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
|
|
25
|
+
|
|
26
|
+
[[package]]
|
|
27
|
+
name = "memo-map"
|
|
28
|
+
version = "0.3.3"
|
|
29
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
30
|
+
checksum = "38d1115007560874e373613744c6fba374c17688327a71c1476d1a5954cc857b"
|
|
31
|
+
|
|
32
|
+
[[package]]
|
|
33
|
+
name = "minijinja"
|
|
34
|
+
version = "2.21.0"
|
|
35
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
36
|
+
checksum = "cb3d648e68cea56d9858d535ee28f9538404e2dd8cb08ed0bd05dca379477f39"
|
|
37
|
+
dependencies = [
|
|
38
|
+
"memo-map",
|
|
39
|
+
"serde",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[[package]]
|
|
43
|
+
name = "proc-macro2"
|
|
44
|
+
version = "1.0.106"
|
|
45
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
46
|
+
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
|
47
|
+
dependencies = [
|
|
48
|
+
"unicode-ident",
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
[[package]]
|
|
52
|
+
name = "quote"
|
|
53
|
+
version = "1.0.46"
|
|
54
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
55
|
+
checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
|
|
56
|
+
dependencies = [
|
|
57
|
+
"proc-macro2",
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
[[package]]
|
|
61
|
+
name = "serde"
|
|
62
|
+
version = "1.0.228"
|
|
63
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
64
|
+
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
|
65
|
+
dependencies = [
|
|
66
|
+
"serde_core",
|
|
67
|
+
"serde_derive",
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
[[package]]
|
|
71
|
+
name = "serde_core"
|
|
72
|
+
version = "1.0.228"
|
|
73
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
74
|
+
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
|
75
|
+
dependencies = [
|
|
76
|
+
"serde_derive",
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
[[package]]
|
|
80
|
+
name = "serde_derive"
|
|
81
|
+
version = "1.0.228"
|
|
82
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
83
|
+
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
|
84
|
+
dependencies = [
|
|
85
|
+
"proc-macro2",
|
|
86
|
+
"quote",
|
|
87
|
+
"syn",
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
[[package]]
|
|
91
|
+
name = "serde_json"
|
|
92
|
+
version = "1.0.150"
|
|
93
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
94
|
+
checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
|
|
95
|
+
dependencies = [
|
|
96
|
+
"itoa",
|
|
97
|
+
"memchr",
|
|
98
|
+
"serde",
|
|
99
|
+
"serde_core",
|
|
100
|
+
"zmij",
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
[[package]]
|
|
104
|
+
name = "syn"
|
|
105
|
+
version = "2.0.118"
|
|
106
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
107
|
+
checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
|
|
108
|
+
dependencies = [
|
|
109
|
+
"proc-macro2",
|
|
110
|
+
"quote",
|
|
111
|
+
"unicode-ident",
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
[[package]]
|
|
115
|
+
name = "unicode-ident"
|
|
116
|
+
version = "1.0.24"
|
|
117
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
118
|
+
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
|
119
|
+
|
|
120
|
+
[[package]]
|
|
121
|
+
name = "zmij"
|
|
122
|
+
version = "1.0.21"
|
|
123
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
124
|
+
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "barefootjs"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
description = "Rust runtime for BarefootJS marked templates (minijinja adapter)."
|
|
6
|
+
license = "MIT"
|
|
7
|
+
|
|
8
|
+
[lib]
|
|
9
|
+
name = "barefootjs"
|
|
10
|
+
path = "src/lib.rs"
|
|
11
|
+
|
|
12
|
+
[[bin]]
|
|
13
|
+
name = "bf-render"
|
|
14
|
+
path = "src/bin/bf-render.rs"
|
|
15
|
+
|
|
16
|
+
[dependencies]
|
|
17
|
+
minijinja = { version = "2", features = ["loader"] }
|
|
18
|
+
serde = { version = "1", features = ["derive"] }
|
|
19
|
+
serde_json = { version = "1", features = ["float_roundtrip"] }
|
|
20
|
+
|
|
21
|
+
[dev-dependencies]
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
//! minijinja `Environment` construction, the custom formatter, canonical
|
|
2
|
+
//! JSON encoding, and named-template rendering.
|
|
3
|
+
//!
|
|
4
|
+
//! Port of `packages/adapter-jinja/python/barefootjs/backend_jinja.py`'s
|
|
5
|
+
//! four engine-specific operations the runtime delegates to
|
|
6
|
+
//! (`encode_json`, `mark_raw`, `materialize`, `render_named`) -- adjusted
|
|
7
|
+
//! for the design contract's minijinja Environment shape (see the design
|
|
8
|
+
//! doc's "minijinja Environment contract" section, verified against
|
|
9
|
+
//! minijinja 2.21.0):
|
|
10
|
+
//!
|
|
11
|
+
//! * `ChainableUndefined` equivalent: [`UndefinedBehavior::Chainable`].
|
|
12
|
+
//! * `trim_blocks` / `lstrip_blocks`: both `true` (Jinja2 parity).
|
|
13
|
+
//! * `.j2` is NOT auto-escaped by minijinja's own extension-sniffing
|
|
14
|
+
//! default, so [`Environment::set_auto_escape_callback`] forces
|
|
15
|
+
//! `AutoEscape::Html` unconditionally.
|
|
16
|
+
//! * A custom [`Environment::set_formatter`] absorbs JS/minijinja
|
|
17
|
+
//! semantic differences UNIFORMLY (no per-fixture hacks): `undefined`/
|
|
18
|
+
//! `none` print nothing, safe values pass through raw, strings escape
|
|
19
|
+
//! with MarkupSafe-compatible entities (`'` for `'`, NOT
|
|
20
|
+
//! minijinja's default `'` -- fixtures pin `'`), numbers
|
|
21
|
+
//! format via [`crate::num::format_js_number`] (a fallback: templates
|
|
22
|
+
//! normally route through `bf.string()` first), bools print
|
|
23
|
+
//! `true`/`false`.
|
|
24
|
+
//!
|
|
25
|
+
//! `materialize` has no Rust analogue: JSX children are captured via
|
|
26
|
+
//! minijinja `{% set %}...{% endset %}` blocks, which always yield a
|
|
27
|
+
//! concrete rendered `Value::from_safe_string` (never a lazy callable, no
|
|
28
|
+
//! `bf-render` payload path constructs one either), so there is nothing to
|
|
29
|
+
//! resolve.
|
|
30
|
+
|
|
31
|
+
use crate::num::{self, JsValue};
|
|
32
|
+
use crate::runtime;
|
|
33
|
+
use minijinja::value::Value as MjValue;
|
|
34
|
+
use minijinja::{escape_formatter, path_loader, AutoEscape, Environment, Error, Output, State, UndefinedBehavior};
|
|
35
|
+
use std::collections::BTreeMap;
|
|
36
|
+
use std::path::Path;
|
|
37
|
+
|
|
38
|
+
/// Build the minijinja `Environment` per the design contract. `templates_dir`
|
|
39
|
+
/// contains `<snake_case>.j2` files loaded by base name (no `templates/`
|
|
40
|
+
/// prefix, no extension) via [`Environment::get_template`].
|
|
41
|
+
pub fn build_environment(templates_dir: &Path) -> Environment<'static> {
|
|
42
|
+
let mut env = Environment::new();
|
|
43
|
+
env.set_loader(path_loader(templates_dir));
|
|
44
|
+
env.set_undefined_behavior(UndefinedBehavior::Chainable);
|
|
45
|
+
env.set_trim_blocks(true);
|
|
46
|
+
env.set_lstrip_blocks(true);
|
|
47
|
+
env.set_auto_escape_callback(|_name| AutoEscape::Html);
|
|
48
|
+
env.set_formatter(bf_formatter);
|
|
49
|
+
env
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/// MarkupSafe-compatible escape for plain (non-safe) string interpolation:
|
|
53
|
+
/// `&` `&`, `<` `<`, `>` `>`, `"` `"`, `'` `'`. The ONLY
|
|
54
|
+
/// divergence from minijinja's own default HTML escaper
|
|
55
|
+
/// (`crate::utils::write_escaped` in the minijinja source) is the
|
|
56
|
+
/// apostrophe entity (minijinja default: `'`) -- everything else
|
|
57
|
+
/// matches minijinja's own table already, kept explicit here so the whole
|
|
58
|
+
/// policy lives in one place next to the formatter that uses it.
|
|
59
|
+
///
|
|
60
|
+
/// NOT the same escaper as `runtime::html_escape` (used by `spread_attrs` /
|
|
61
|
+
/// `hydration_attrs` / `data_key_attr`'s raw attribute-syntax construction),
|
|
62
|
+
/// which uses `"` for `"` (matching Go's `template.HTMLEscapeString`
|
|
63
|
+
/// byte-for-byte so SSR output stays identical across adapters). Two
|
|
64
|
+
/// call sites, two historically-pinned entity choices; unifying them is
|
|
65
|
+
/// out of scope for this port.
|
|
66
|
+
fn markupsafe_escape(s: &str) -> String {
|
|
67
|
+
let mut out = String::with_capacity(s.len());
|
|
68
|
+
for c in s.chars() {
|
|
69
|
+
match c {
|
|
70
|
+
'&' => out.push_str("&"),
|
|
71
|
+
'<' => out.push_str("<"),
|
|
72
|
+
'>' => out.push_str(">"),
|
|
73
|
+
'"' => out.push_str("""),
|
|
74
|
+
'\'' => out.push_str("'"),
|
|
75
|
+
_ => out.push(c),
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
out
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
fn bf_formatter(out: &mut Output, state: &State, value: &MjValue) -> Result<(), Error> {
|
|
82
|
+
if value.is_undefined() || value.is_none() {
|
|
83
|
+
return Ok(());
|
|
84
|
+
}
|
|
85
|
+
if value.is_safe() {
|
|
86
|
+
out.write_str(value.as_str().unwrap_or_default()).map_err(Error::from)?;
|
|
87
|
+
return Ok(());
|
|
88
|
+
}
|
|
89
|
+
match value.kind() {
|
|
90
|
+
minijinja::value::ValueKind::Bool => {
|
|
91
|
+
out.write_str(if value.is_true() { "true" } else { "false" }).map_err(Error::from)?;
|
|
92
|
+
}
|
|
93
|
+
minijinja::value::ValueKind::Number => {
|
|
94
|
+
let n = f64::try_from(value.clone()).unwrap_or(f64::NAN);
|
|
95
|
+
out.write_str(&num::format_js_number(n)).map_err(Error::from)?;
|
|
96
|
+
}
|
|
97
|
+
minijinja::value::ValueKind::String => {
|
|
98
|
+
out.write_str(&markupsafe_escape(value.as_str().unwrap_or(""))).map_err(Error::from)?;
|
|
99
|
+
}
|
|
100
|
+
// Arrays/maps/objects are never interpolated directly by generated
|
|
101
|
+
// templates (they always go through `bf.json` / `bf.string` /
|
|
102
|
+
// `bf.spread_attrs` first) -- fall back to minijinja's own default
|
|
103
|
+
// rendering for defensive completeness.
|
|
104
|
+
_ => escape_formatter(out, state, value)?,
|
|
105
|
+
}
|
|
106
|
+
Ok(())
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/// Canonical JSON encoding for `bf.json` / `bf-p` / `bf-scope`: sorted keys
|
|
110
|
+
/// (free, since [`JsValue::Object`] is a `BTreeMap`), compact separators
|
|
111
|
+
/// (`serde_json::to_string`'s default), non-finite floats -> JSON `null`
|
|
112
|
+
/// recursively (`JsValue::to_json`'s documented contract -- see `num.rs`).
|
|
113
|
+
pub fn encode_json(v: &JsValue) -> String {
|
|
114
|
+
serde_json::to_string(&v.to_json()).expect("JsValue::to_json always produces a JSON-encodable document")
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
fn build_context(bf_value: MjValue, vars: &JsValue) -> BTreeMap<String, MjValue> {
|
|
118
|
+
let mut ctx = BTreeMap::new();
|
|
119
|
+
if let Some(map) = vars.as_object() {
|
|
120
|
+
for (k, v) in map {
|
|
121
|
+
ctx.insert(runtime::mangle_ident(k), runtime::js_to_mj(v));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
ctx.insert("bf".to_string(), bf_value);
|
|
125
|
+
ctx
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/// Render `<name>.j2` with `bf_value` bound as the `bf` variable, plus the
|
|
129
|
+
/// supplied template vars (keyword-mangled -- the one point every props
|
|
130
|
+
/// dict is turned into template variables). Used for the top-level entry
|
|
131
|
+
/// render (`bf-render`'s own `env`, no template is executing yet).
|
|
132
|
+
pub fn render_named(env: &Environment<'static>, template_name: &str, bf_value: MjValue, vars: &JsValue) -> Result<String, Error> {
|
|
133
|
+
let template = env.get_template(&format!("{template_name}.j2"))?;
|
|
134
|
+
template.render(build_context(bf_value, vars))
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/// Same as [`render_named`] but sourced from a `State` -- the re-entrant
|
|
138
|
+
/// path `bf.render_child` uses to render a child template from WITHIN an
|
|
139
|
+
/// already-executing template, via `state.env()`.
|
|
140
|
+
pub fn render_named_from_state(state: &State, template_name: &str, bf_value: MjValue, vars: &JsValue) -> Result<String, Error> {
|
|
141
|
+
let template = state.env().get_template(&format!("{template_name}.j2"))?;
|
|
142
|
+
template.render(build_context(bf_value, vars))
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/// Like [`render_named_from_state`], but `vars` are ALREADY
|
|
146
|
+
/// `minijinja::Value`s with ALREADY keyword-mangled keys (built by
|
|
147
|
+
/// `render_child`), rather than a `JsValue` document to mangle-and-convert
|
|
148
|
+
/// here. This is the child-render path: `render_child` must keep props as
|
|
149
|
+
/// `Value` end-to-end so a safe (JSX children) value survives -- routing
|
|
150
|
+
/// through `&JsValue`/[`build_context`] would strip the safe flag off any
|
|
151
|
+
/// safe-string entry (see `runtime::BfInstance::render_child`'s docstring).
|
|
152
|
+
pub fn render_named_from_state_values(
|
|
153
|
+
state: &State,
|
|
154
|
+
template_name: &str,
|
|
155
|
+
bf_value: MjValue,
|
|
156
|
+
mut vars: BTreeMap<String, MjValue>,
|
|
157
|
+
) -> Result<String, Error> {
|
|
158
|
+
let template = state.env().get_template(&format!("{template_name}.j2"))?;
|
|
159
|
+
vars.insert("bf".to_string(), bf_value);
|
|
160
|
+
template.render(vars)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/// Like [`render_named`], plus extra raw `minijinja::Value` context entries
|
|
164
|
+
/// (keyword-mangled like every other var) inserted after the `vars`-derived
|
|
165
|
+
/// ones. Used by `bf-render`'s entry point to bind `searchParams` -- a
|
|
166
|
+
/// [`crate::search_params::SearchParams`] `Object`, which has no
|
|
167
|
+
/// [`JsValue`] representation (it isn't a JSON-shaped value), so it can't
|
|
168
|
+
/// flow through the normal `vars: &JsValue` path.
|
|
169
|
+
pub fn render_entry(env: &Environment<'static>, template_name: &str, bf_value: MjValue, vars: &JsValue, extra_vars: &[(String, MjValue)]) -> Result<String, Error> {
|
|
170
|
+
let template = env.get_template(&format!("{template_name}.j2"))?;
|
|
171
|
+
let mut ctx = build_context(bf_value, vars);
|
|
172
|
+
for (k, v) in extra_vars {
|
|
173
|
+
ctx.insert(runtime::mangle_ident(k), v.clone());
|
|
174
|
+
}
|
|
175
|
+
template.render(ctx)
|
|
176
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
//! Conformance renderer binary: `bf-render <path-to-payload.json>`.
|
|
2
|
+
//!
|
|
3
|
+
//! Implements the bf-render payload protocol (design doc, "bf-render
|
|
4
|
+
//! payload protocol" section) verbatim. HTML -> stdout on success; on
|
|
5
|
+
//! error, a message -> stderr and exit 1.
|
|
6
|
+
//!
|
|
7
|
+
//! Non-finite numbers (JSON cannot spell NaN/Infinity) travel as
|
|
8
|
+
//! `{"__bf_special": "nan" | "inf" | "-inf"}` sentinel objects, decoded
|
|
9
|
+
//! recursively AFTER `serde_json` parses the document (the TS side
|
|
10
|
+
//! transforms values recursively BEFORE `JSON.stringify`; this binary is
|
|
11
|
+
//! the mirror-image decode step).
|
|
12
|
+
|
|
13
|
+
use barefootjs::backend_minijinja;
|
|
14
|
+
use barefootjs::num::JsValue;
|
|
15
|
+
use barefootjs::runtime::{js_to_mj, BfInstance, ChildRendererSpec, RenderSession};
|
|
16
|
+
use barefootjs::search_params::SearchParams;
|
|
17
|
+
use minijinja::value::Value as MjValue;
|
|
18
|
+
use serde::Deserialize;
|
|
19
|
+
use serde_json::Value as JsonValue;
|
|
20
|
+
use std::path::PathBuf;
|
|
21
|
+
use std::process::ExitCode;
|
|
22
|
+
use std::sync::Arc;
|
|
23
|
+
|
|
24
|
+
#[derive(Deserialize)]
|
|
25
|
+
struct ChildPayload {
|
|
26
|
+
name: String,
|
|
27
|
+
template: String,
|
|
28
|
+
#[serde(default)]
|
|
29
|
+
ssr_defaults: JsonValue,
|
|
30
|
+
#[serde(default)]
|
|
31
|
+
rest_props_name: Option<String>,
|
|
32
|
+
#[serde(default)]
|
|
33
|
+
param_names: Vec<String>,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
#[derive(Deserialize)]
|
|
37
|
+
struct Payload {
|
|
38
|
+
templates_dir: String,
|
|
39
|
+
entry: String,
|
|
40
|
+
scope_id: String,
|
|
41
|
+
#[serde(default)]
|
|
42
|
+
vars: JsonValue,
|
|
43
|
+
#[serde(default)]
|
|
44
|
+
search_params: Option<String>,
|
|
45
|
+
#[serde(default)]
|
|
46
|
+
children: Vec<ChildPayload>,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/// Recursively decode a parsed JSON document into the working [`JsValue`]
|
|
50
|
+
/// domain, resolving `{"__bf_special": "nan" | "inf" | "-inf"}` sentinel
|
|
51
|
+
/// objects into real non-finite `f64`s along the way -- these are the ONE
|
|
52
|
+
/// shape `serde_json::Value` cannot carry natively (see `num.rs`'s module
|
|
53
|
+
/// docstring), so decoding happens here rather than via
|
|
54
|
+
/// `JsValue::from_json` (which has no sentinel to look for -- ordinary
|
|
55
|
+
/// JSON documents never need one).
|
|
56
|
+
fn decode_value(v: &JsonValue) -> JsValue {
|
|
57
|
+
if let JsonValue::Object(map) = v {
|
|
58
|
+
if map.len() == 1 {
|
|
59
|
+
if let Some(JsonValue::String(kind)) = map.get("__bf_special") {
|
|
60
|
+
let n = match kind.as_str() {
|
|
61
|
+
"nan" => f64::NAN,
|
|
62
|
+
"inf" => f64::INFINITY,
|
|
63
|
+
"-inf" => f64::NEG_INFINITY,
|
|
64
|
+
_ => f64::NAN,
|
|
65
|
+
};
|
|
66
|
+
return JsValue::Number(n);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return JsValue::Object(map.iter().map(|(k, v)| (k.clone(), decode_value(v))).collect());
|
|
70
|
+
}
|
|
71
|
+
match v {
|
|
72
|
+
JsonValue::Null => JsValue::Null,
|
|
73
|
+
JsonValue::Bool(b) => JsValue::Bool(*b),
|
|
74
|
+
JsonValue::Number(n) => JsValue::Number(n.as_f64().unwrap_or(f64::NAN)),
|
|
75
|
+
JsonValue::String(s) => JsValue::String(s.clone()),
|
|
76
|
+
JsonValue::Array(a) => JsValue::Array(a.iter().map(decode_value).collect()),
|
|
77
|
+
JsonValue::Object(_) => unreachable!("handled above"),
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
fn run(payload_path: &str) -> Result<String, String> {
|
|
82
|
+
let text = std::fs::read_to_string(payload_path).map_err(|e| format!("failed to read {payload_path}: {e}"))?;
|
|
83
|
+
let payload: Payload = serde_json::from_str(&text).map_err(|e| format!("invalid payload JSON: {e}"))?;
|
|
84
|
+
|
|
85
|
+
let templates_dir = PathBuf::from(&payload.templates_dir);
|
|
86
|
+
let env = backend_minijinja::build_environment(&templates_dir);
|
|
87
|
+
|
|
88
|
+
let session = RenderSession::new();
|
|
89
|
+
for child in &payload.children {
|
|
90
|
+
session.register_child_renderer(
|
|
91
|
+
// Keyed by the snake_case template base name -- matches the
|
|
92
|
+
// literal `bf.render_child('<snake_name>', ...)` call sites the
|
|
93
|
+
// TS emitter generates (`toTemplateName(comp.name)`), mirroring
|
|
94
|
+
// `buildChildRenderers`'s `bf.register_child_renderer(snakeName, ...)`.
|
|
95
|
+
child.template.clone(),
|
|
96
|
+
ChildRendererSpec {
|
|
97
|
+
component_name: child.name.clone(),
|
|
98
|
+
template: child.template.clone(),
|
|
99
|
+
// Converted from JSON to `Value` HERE, at registration time
|
|
100
|
+
// -- `render_child` keeps props as `Value` end-to-end from
|
|
101
|
+
// this point on (see `ChildRendererSpec::ssr_defaults`'s
|
|
102
|
+
// docstring).
|
|
103
|
+
ssr_defaults: js_to_mj(&decode_value(&child.ssr_defaults)),
|
|
104
|
+
rest_props_name: child.rest_props_name.clone(),
|
|
105
|
+
param_names: child.param_names.clone(),
|
|
106
|
+
},
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let root = BfInstance::root(Arc::clone(&session), payload.scope_id.clone());
|
|
111
|
+
let vars = decode_value(&payload.vars);
|
|
112
|
+
|
|
113
|
+
let mut extra: Vec<(String, MjValue)> = Vec::new();
|
|
114
|
+
if let Some(query) = &payload.search_params {
|
|
115
|
+
extra.push(("searchParams".to_string(), SearchParams::new(query).to_value()));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
backend_minijinja::render_entry(&env, &payload.entry, root.as_mj_value(), &vars, &extra).map_err(|e| {
|
|
119
|
+
let mut msg = format!("{e:#}");
|
|
120
|
+
let mut source = std::error::Error::source(&e);
|
|
121
|
+
while let Some(s) = source {
|
|
122
|
+
msg.push_str(&format!("\ncaused by: {s}"));
|
|
123
|
+
source = s.source();
|
|
124
|
+
}
|
|
125
|
+
msg
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
fn main() -> ExitCode {
|
|
130
|
+
let payload_path = match std::env::args().nth(1) {
|
|
131
|
+
Some(p) => p,
|
|
132
|
+
None => {
|
|
133
|
+
eprintln!("usage: bf-render <path-to-payload.json>");
|
|
134
|
+
return ExitCode::FAILURE;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
match run(&payload_path) {
|
|
138
|
+
Ok(html) => {
|
|
139
|
+
print!("{html}");
|
|
140
|
+
ExitCode::SUCCESS
|
|
141
|
+
}
|
|
142
|
+
Err(msg) => {
|
|
143
|
+
eprintln!("{msg}");
|
|
144
|
+
ExitCode::FAILURE
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|