@barefootjs/jinja 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +66 -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 +84 -0
  5. package/dist/adapter/boolean-result.d.ts.map +1 -0
  6. package/dist/adapter/emit-context.d.ts +106 -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/expr/operand.d.ts +25 -0
  13. package/dist/adapter/expr/operand.d.ts.map +1 -0
  14. package/dist/adapter/index.d.ts +6 -0
  15. package/dist/adapter/index.d.ts.map +1 -0
  16. package/dist/adapter/index.js +189097 -0
  17. package/dist/adapter/jinja-adapter.d.ts +394 -0
  18. package/dist/adapter/jinja-adapter.d.ts.map +1 -0
  19. package/dist/adapter/lib/constants.d.ts +21 -0
  20. package/dist/adapter/lib/constants.d.ts.map +1 -0
  21. package/dist/adapter/lib/ir-scope.d.ts +50 -0
  22. package/dist/adapter/lib/ir-scope.d.ts.map +1 -0
  23. package/dist/adapter/lib/jinja-naming.d.ts +77 -0
  24. package/dist/adapter/lib/jinja-naming.d.ts.map +1 -0
  25. package/dist/adapter/lib/types.d.ts +27 -0
  26. package/dist/adapter/lib/types.d.ts.map +1 -0
  27. package/dist/adapter/memo/seed.d.ts +81 -0
  28. package/dist/adapter/memo/seed.d.ts.map +1 -0
  29. package/dist/adapter/props/prop-classes.d.ts +33 -0
  30. package/dist/adapter/props/prop-classes.d.ts.map +1 -0
  31. package/dist/adapter/spread/spread-codegen.d.ts +61 -0
  32. package/dist/adapter/spread/spread-codegen.d.ts.map +1 -0
  33. package/dist/adapter/value/parsed-literal.d.ts +28 -0
  34. package/dist/adapter/value/parsed-literal.d.ts.map +1 -0
  35. package/dist/build.d.ts +28 -0
  36. package/dist/build.d.ts.map +1 -0
  37. package/dist/build.js +189117 -0
  38. package/dist/conformance-pins.d.ts +12 -0
  39. package/dist/conformance-pins.d.ts.map +1 -0
  40. package/dist/index.d.ts +9 -0
  41. package/dist/index.d.ts.map +1 -0
  42. package/dist/index.js +189118 -0
  43. package/package.json +66 -0
  44. package/python/VERSION +1 -0
  45. package/python/barefootjs/__init__.py +57 -0
  46. package/python/barefootjs/backend_jinja.py +125 -0
  47. package/python/barefootjs/evaluator.py +787 -0
  48. package/python/barefootjs/runtime.py +1334 -0
  49. package/python/barefootjs/search_params.py +89 -0
  50. package/python/pyproject.toml +32 -0
  51. package/python/tests/__init__.py +0 -0
  52. package/python/tests/test_eval_vectors.py +88 -0
  53. package/python/tests/test_evaluator.py +406 -0
  54. package/python/tests/test_helper_vectors.py +288 -0
  55. package/python/tests/test_omit.py +62 -0
  56. package/python/tests/test_props_attr.py +54 -0
  57. package/python/tests/test_query.py +41 -0
  58. package/python/tests/test_render.py +102 -0
  59. package/python/tests/test_render_child.py +96 -0
  60. package/python/tests/test_search_params.py +50 -0
  61. package/python/tests/test_spread_attrs.py +86 -0
  62. package/python/tests/test_template_primitives.py +347 -0
  63. package/python/tests/vector-divergences.json +42 -0
  64. package/src/__tests__/jinja-adapter-unit.test.ts +390 -0
  65. package/src/__tests__/jinja-adapter.test.ts +53 -0
  66. package/src/__tests__/jinja-counter.test.ts +62 -0
  67. package/src/__tests__/jinja-query-href.test.ts +99 -0
  68. package/src/__tests__/jinja-spread-attrs.test.ts +225 -0
  69. package/src/adapter/analysis/component-tree.ts +119 -0
  70. package/src/adapter/boolean-result.ts +176 -0
  71. package/src/adapter/emit-context.ts +118 -0
  72. package/src/adapter/expr/array-method.ts +346 -0
  73. package/src/adapter/expr/emitters.ts +608 -0
  74. package/src/adapter/expr/operand.ts +35 -0
  75. package/src/adapter/index.ts +6 -0
  76. package/src/adapter/jinja-adapter.ts +1747 -0
  77. package/src/adapter/lib/constants.ts +33 -0
  78. package/src/adapter/lib/ir-scope.ts +95 -0
  79. package/src/adapter/lib/jinja-naming.ts +114 -0
  80. package/src/adapter/lib/types.ts +30 -0
  81. package/src/adapter/memo/seed.ts +132 -0
  82. package/src/adapter/props/prop-classes.ts +65 -0
  83. package/src/adapter/spread/spread-codegen.ts +166 -0
  84. package/src/adapter/value/parsed-literal.ts +76 -0
  85. package/src/build.ts +37 -0
  86. package/src/conformance-pins.ts +101 -0
  87. package/src/index.ts +9 -0
  88. package/src/test-render.ts +714 -0
@@ -0,0 +1,225 @@
1
+ /**
2
+ * JinjaAdapter — conditional-spread / nullish-omission / hyphenated-key
3
+ * regression tests (#textarea / #checkbox Phase 2b parity).
4
+ *
5
+ * Ported from `packages/adapter-xslate/src/__tests__/xslate-spread-attrs.test.ts`,
6
+ * translating each expected template string to Jinja syntax. The shared
7
+ * adapter-conformance fixtures (`textarea`, `checkbox`) only exercise the
8
+ * falsy branch of the conditional spread and the unset branch of the
9
+ * optional attr, so these unit tests pin the truthy / present branches the
10
+ * fixtures can't reach.
11
+ *
12
+ * One test (`hyphenated child attr hash key`) is NOT a byte-for-byte
13
+ * translation: Jinja dict-literal keys are ALWAYS quoted (`jinjaHashKey`
14
+ * unconditionally single-quotes, unlike Kolon's bareword-key sugar —
15
+ * `{key: value}` in Jinja/Python means "look up variable `key`", not the
16
+ * string `"key"` — see `lib/jinja-naming.ts`'s docstring), so there is no
17
+ * "stays unquoted" case to assert for a bare-identifier-safe prop name like
18
+ * `size`; only that BOTH keys are correctly quoted.
19
+ */
20
+
21
+ import { test, expect, describe } from 'bun:test'
22
+ import { compileJSX } from '@barefootjs/jsx'
23
+ import type { ComponentIR } from '@barefootjs/jsx'
24
+ import { JinjaAdapter } from '../adapter'
25
+
26
+ function compileToIR(source: string, adapter?: JinjaAdapter): ComponentIR {
27
+ const result = compileJSX(source.trimStart(), 'test.tsx', {
28
+ adapter: adapter ?? new JinjaAdapter(),
29
+ outputIR: true,
30
+ })
31
+ const irFile = result.files.find(f => f.type === 'ir')
32
+ if (!irFile) throw new Error('No IR output')
33
+ return JSON.parse(irFile.content) as ComponentIR
34
+ }
35
+
36
+ function compileAndGenerate(source: string, adapter?: JinjaAdapter) {
37
+ const a = adapter ?? new JinjaAdapter()
38
+ const ir = compileToIR(source, a)
39
+ return a.generate(ir)
40
+ }
41
+
42
+ describe('JinjaAdapter - conditional inline-object spread (textarea aria-describedby)', () => {
43
+ // `{...(cond ? { 'aria-describedby': cond } : {})}` lowers to a Jinja inline
44
+ // ternary of dicts so the falsy `{}` branch OMITS the key
45
+ // (bf.spread_attrs does not emit empty entries). The shared fixture only
46
+ // exercises the falsy branch; this pins the truthy one.
47
+ test('emits a Jinja inline ternary of dicts through bf.spread_attrs', () => {
48
+ const { template } = compileAndGenerate(`
49
+ function Box({ describedBy }: { describedBy?: string }) {
50
+ return <div {...(describedBy ? { 'aria-describedby': describedBy } : {})} />
51
+ }
52
+ `)
53
+ expect(template).toContain(
54
+ "bf.spread_attrs(({'aria-describedby': describedBy} if bf.truthy(describedBy) else {}))",
55
+ )
56
+ })
57
+
58
+ test('resolves the value reference and preserves the static key for a second prop', () => {
59
+ const { template } = compileAndGenerate(`
60
+ function Box({ label }: { label: string }) {
61
+ return <div {...(label ? { 'data-label': label } : {})} />
62
+ }
63
+ `)
64
+ expect(template).toContain(
65
+ "bf.spread_attrs(({'data-label': label} if bf.truthy(label) else {}))",
66
+ )
67
+ })
68
+
69
+ test('falls back to BF101 for a computed (non-static) object key', () => {
70
+ const adapter = new JinjaAdapter()
71
+ const ir = compileToIR(`
72
+ function Box({ k, v }: { k?: string; v?: string }) {
73
+ return <div {...(v ? { [k]: v } : {})} />
74
+ }
75
+ `, adapter)
76
+ adapter.generate(ir)
77
+ const errs = (adapter as unknown as { errors: { code: string }[] }).errors
78
+ expect(errs.some(e => e.code === 'BF101')).toBe(true)
79
+ })
80
+ })
81
+
82
+ describe('JinjaAdapter - local-const conditional-spread resolution (#checkbox icon)', () => {
83
+ // A FUNCTION-scope const holding a `cond ? {…} : {}` ternary, spread as a bare
84
+ // identifier (`{...attrs}`), resolves through the same Jinja
85
+ // ternary-of-dicts lowering as the inline form. CheckIcon's
86
+ // `const sizeAttrs = size ? {…} : {}` is exactly this shape.
87
+ test('resolves a bare-identifier spread of a function-scope conditional const', () => {
88
+ const { template } = compileAndGenerate(`
89
+ function Box({ flag }: { flag?: boolean }) {
90
+ const attrs = flag ? { 'data-on': 'yes' } : {}
91
+ return <div {...attrs} />
92
+ }
93
+ `)
94
+ expect(template).toContain(
95
+ "bf.spread_attrs(({'data-on': 'yes'} if bf.truthy(flag) else {}))",
96
+ )
97
+ })
98
+
99
+ // A const that aliases another bare identifier must NOT be forwarded (loop
100
+ // guard): the resolver bails, so the spread falls through to the standard
101
+ // lowering emitting the bare `attrs` variable.
102
+ test('does not forward a const that aliases another identifier (loop guard)', () => {
103
+ const { template } = compileAndGenerate(`
104
+ function Box({ other }: { other?: object }) {
105
+ const attrs = other
106
+ return <div {...attrs} />
107
+ }
108
+ `)
109
+ expect(template).toContain('bf.spread_attrs(attrs)')
110
+ })
111
+ })
112
+
113
+ describe('JinjaAdapter - Record<staticKeys,scalar>[propKey] spread value (#checkbox icon)', () => {
114
+ // `const sizeMap: Record<IconSize, number> = { sm: 16, ... }` indexed by a
115
+ // prop inside a conditional-spread object value lowers to an inline
116
+ // bracket-indexed Jinja dict `{...}[key]` — the SAME bracket-index syntax
117
+ // JS itself uses (unlike Kolon, which had to steer around Perl's
118
+ // arrow-deref `->{$key}` to the bracket form). This is CheckIcon's
119
+ // `{ width: sizeMap[size], height: sizeMap[size] }` shape.
120
+ test('lowers an indexed module-const map to an inline bracket-indexed dict', () => {
121
+ const { template } = compileAndGenerate(`
122
+ const sizeMap: Record<string, number> = { sm: 16, md: 20, lg: 24, xl: 32 }
123
+ function Box({ size }: { size?: string }) {
124
+ const attrs = size ? { width: sizeMap[size] } : {}
125
+ return <div {...attrs} />
126
+ }
127
+ `)
128
+ expect(template).toContain(
129
+ "{'sm': 16, 'md': 20, 'lg': 24, 'xl': 32}[size]",
130
+ )
131
+ })
132
+
133
+ test('lowers string-valued record maps too', () => {
134
+ const { template } = compileAndGenerate(`
135
+ const labelMap: Record<string, string> = { a: 'Alpha', b: 'Beta' }
136
+ function Box({ k }: { k?: string }) {
137
+ const attrs = k ? { 'data-label': labelMap[k] } : {}
138
+ return <div {...attrs} />
139
+ }
140
+ `)
141
+ expect(template).toContain("{'a': 'Alpha', 'b': 'Beta'}[k]")
142
+ })
143
+
144
+ // A non-scalar record value (object) is out of shape: the spread object value
145
+ // can't lower, so the whole spread falls back to BF101.
146
+ test('refuses a non-scalar record value with BF101 (out-of-shape fallback)', () => {
147
+ const adapter = new JinjaAdapter()
148
+ const ir = compileToIR(`
149
+ const sizeMap: Record<string, object> = { sm: { w: 1 } }
150
+ function Box({ size }: { size?: string }) {
151
+ const attrs = size ? { width: sizeMap[size] } : {}
152
+ return <div {...attrs} />
153
+ }
154
+ `, adapter)
155
+ adapter.generate(ir)
156
+ const errs = (adapter as unknown as { errors: { code: string }[] }).errors
157
+ expect(errs.some(e => e.code === 'BF101')).toBe(true)
158
+ })
159
+ })
160
+
161
+ describe('JinjaAdapter - props-object inherited-attribute enumeration (#checkbox)', () => {
162
+ // A SolidJS props-object component reads inherited attributes (`props.id`)
163
+ // not enumerated in `propsParams`. The bare optional attribute must be
164
+ // guarded so it's omitted when unset (Hono parity), even though `id`
165
+ // isn't a declared param.
166
+ test('guards a props-object bare optional attr (props.id) with is defined and is not none', () => {
167
+ const { template } = compileAndGenerate(`
168
+ "use client"
169
+ interface P { tone?: string }
170
+ export function Widget(props: P) {
171
+ return <button id={props.id}>x</button>
172
+ }
173
+ `)
174
+ expect(template).toContain('{% if id is defined and id is not none %}')
175
+ expect(template).toContain('id="{{ bf.string(id) }}"')
176
+ })
177
+ })
178
+
179
+ describe('JinjaAdapter - hyphenated child attr dict key (#checkbox)', () => {
180
+ // A child component prop whose JSX name isn't a bare identifier
181
+ // (`<CheckIcon data-slot="..."/>`) must be quoted in the `render_child`
182
+ // dict — same as EVERY other key, since Jinja dict-literal keys are
183
+ // ALWAYS quoted (see the file header for why this diverges from the
184
+ // Kolon port's "only quote when non-bareword-safe" assertion).
185
+ test('quotes every child attribute name in render_child, hyphenated or not', () => {
186
+ const { template } = compileAndGenerate(`
187
+ "use client"
188
+ import { Leaf } from './leaf'
189
+ export function Host() {
190
+ return <div><Leaf data-slot="indicator" size="sm" /></div>
191
+ }
192
+ `)
193
+ expect(template).toContain("'data-slot': 'indicator'")
194
+ expect(template).toContain("'size': 'sm'")
195
+ })
196
+ })
197
+
198
+ describe('JinjaAdapter - nullish optional-attribute omission (textarea rows)', () => {
199
+ // A no-destructure-default, nillable-typed prop is `None` when the caller
200
+ // omits it; guard its bare-reference attribute with a Jinja
201
+ // "is defined and is not none" test so it DROPS instead of rendering
202
+ // `attr=""` — matching Hono's nullish-attribute omission. Concrete/defaulted
203
+ // props are never `None` and stay unconditional.
204
+ test('guards a no-default nillable attr with a Jinja defined+not-none check', () => {
205
+ const { template } = compileAndGenerate(`
206
+ function C({ rows }: { rows?: number }) {
207
+ return <textarea rows={rows} />
208
+ }
209
+ `)
210
+ expect(template).toContain('{% if rows is defined and rows is not none %}')
211
+ expect(template).toContain('rows="{{ bf.string(rows) }}"')
212
+ })
213
+
214
+ test('leaves a defaulted attr unconditional (scope did not widen)', () => {
215
+ const { template } = compileAndGenerate(`
216
+ function C({ value = '' }: { value?: string }) {
217
+ return <textarea value={value} />
218
+ }
219
+ `)
220
+ // `value` has a destructure default → never None → unconditional, exactly
221
+ // like Hono's value="".
222
+ expect(template).toContain('value="{{ bf.string(value) }}"')
223
+ expect(template).not.toContain('is not none')
224
+ })
225
+ })
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Component-tree analysis for the Jinja2 template adapter.
3
+ *
4
+ * Ported from `packages/adapter-xslate/src/adapter/analysis/component-tree.ts`.
5
+ * Pure functions over the IR — they read no adapter instance state.
6
+ * `collectImportedLoopChildComponentErrors` returns its diagnostics instead
7
+ * of pushing onto the adapter's error list, so the adapter stays the sole
8
+ * owner of `errors`.
9
+ */
10
+
11
+ import type {
12
+ ComponentIR,
13
+ IRNode,
14
+ IRComponent,
15
+ IRElement,
16
+ IRFragment,
17
+ IRConditional,
18
+ IRLoop,
19
+ IRIfStatement,
20
+ IRProvider,
21
+ IRAsync,
22
+ CompilerError,
23
+ } from '@barefootjs/jsx'
24
+
25
+ /**
26
+ * Whether the component needs the client runtime — it owns reactive state
27
+ * (signals / effects / onMount) or the analyzer flagged it as needing init.
28
+ */
29
+ export function hasClientInteractivity(ir: ComponentIR): boolean {
30
+ return (
31
+ ir.metadata.signals.length > 0 ||
32
+ ir.metadata.effects.length > 0 ||
33
+ ir.metadata.onMounts.length > 0 ||
34
+ (ir.metadata.clientAnalysis?.needsInit ?? false)
35
+ )
36
+ }
37
+
38
+ /**
39
+ * Build a `BF103` diagnostic for every component reference inside a loop body
40
+ * whose name is imported from a relative-path module. Mirror of the Go /
41
+ * Xslate adapter's check — the Jinja adapter has the same
42
+ * cross-template-registration constraint at request time (each `.jinja`
43
+ * component file must be registered with the shared `jinja2.Environment`
44
+ * loader alongside the parent). Returns the diagnostics so the caller pushes
45
+ * them onto its own error list.
46
+ */
47
+ export function collectImportedLoopChildComponentErrors(
48
+ ir: ComponentIR,
49
+ componentName: string,
50
+ ): CompilerError[] {
51
+ const errors: CompilerError[] = []
52
+ const relativeImports = new Set<string>()
53
+ for (const imp of ir.metadata.templateImports ?? ir.metadata.imports ?? []) {
54
+ if (!imp.source.startsWith('./') && !imp.source.startsWith('../')) continue
55
+ if (imp.isTypeOnly) continue
56
+ for (const spec of imp.specifiers) {
57
+ relativeImports.add(spec.alias ?? spec.name)
58
+ }
59
+ }
60
+ if (relativeImports.size === 0) return errors
61
+
62
+ const loc = { file: componentName + '.tsx', start: { line: 1, column: 0 }, end: { line: 1, column: 0 } }
63
+ const visit = (node: IRNode, inLoop: boolean): void => {
64
+ switch (node.type) {
65
+ case 'component': {
66
+ const comp = node as IRComponent
67
+ if (inLoop && relativeImports.has(comp.name)) {
68
+ errors.push({
69
+ code: 'BF103',
70
+ severity: 'error',
71
+ message: `Component <${comp.name}> is imported from a sibling module and used inside a loop. The Jinja adapter emits a cross-template call; the child template must be registered alongside the parent at render time.`,
72
+ loc: comp.loc ?? loc,
73
+ suggestion: {
74
+ message:
75
+ `Options:\n` +
76
+ ` 1. Compile '${comp.name}' (its source file) with the same adapter and register the resulting Jinja template alongside the parent at render time.\n` +
77
+ ` 2. Inline <${comp.name}> directly inside the loop body so no cross-file template lookup is needed.\n` +
78
+ ` 3. Mark the loop position as @client-only so the template is materialised on the client instead of at SSR time.`,
79
+ },
80
+ })
81
+ }
82
+ for (const child of comp.children) visit(child, inLoop)
83
+ break
84
+ }
85
+ case 'element':
86
+ for (const child of (node as IRElement).children) visit(child, inLoop)
87
+ break
88
+ case 'fragment':
89
+ for (const child of (node as IRFragment).children) visit(child, inLoop)
90
+ break
91
+ case 'conditional': {
92
+ const cond = node as IRConditional
93
+ visit(cond.whenTrue, inLoop)
94
+ if (cond.whenFalse) visit(cond.whenFalse, inLoop)
95
+ break
96
+ }
97
+ case 'loop':
98
+ for (const child of (node as IRLoop).children) visit(child, true)
99
+ break
100
+ case 'if-statement': {
101
+ const stmt = node as IRIfStatement
102
+ visit(stmt.consequent, inLoop)
103
+ if (stmt.alternate) visit(stmt.alternate, inLoop)
104
+ break
105
+ }
106
+ case 'provider':
107
+ for (const child of (node as IRProvider).children) visit(child, inLoop)
108
+ break
109
+ case 'async': {
110
+ const a = node as IRAsync
111
+ visit(a.fallback, inLoop)
112
+ for (const child of a.children) visit(child, inLoop)
113
+ break
114
+ }
115
+ }
116
+ }
117
+ visit(ir.root, false)
118
+ return errors
119
+ }
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Structural classifier for JS expressions whose result is a boolean value
3
+ * (or unambiguously stringifies to "true"/"false" in JS).
4
+ *
5
+ * Ported from `packages/adapter-xslate/src/adapter/boolean-result.ts`
6
+ * (itself ported from the Mojo adapter's `bf->bool_str` classifier). Used by
7
+ * the Jinja adapter for TWO purposes — one inherited from Xslate, one new:
8
+ *
9
+ * 1. **Attribute/text stringification** (inherited): route a boolean-shaped
10
+ * reactive binding through the runtime `bf.bool_str` helper so the
11
+ * serialised value matches JS `String(boolean)` ("true"/"false"). Python
12
+ * has a real `bool` type (unlike Perl's `1`/`''`), but Python's own
13
+ * `str(True)` == `"True"` (capitalised) — still wrong for HTML output —
14
+ * so the same explicit routing is required.
15
+ * 2. **Condition-position truthy wrapping** (new — `isBooleanResultParsed` is
16
+ * exported, not just the string-based `isBooleanResultExpr`): Python
17
+ * truthiness diverges from JS specifically on empty containers (`[]` /
18
+ * `{}` are JS-truthy, Python-falsy). Perl doesn't have this problem — a
19
+ * Perl array/hash REFERENCE is always true, matching JS objects/arrays
20
+ * being unconditionally truthy — which is why Xslate never needed a
21
+ * truthy-routing layer for `if`/ternary/`&&`/`||` conditions. The Jinja
22
+ * adapter's condition-emission call sites (see `jinja-adapter.ts`'s
23
+ * `convertConditionToJinja`) reuse this SAME structural classifier: a
24
+ * condition that is already unambiguously boolean-shaped emits directly;
25
+ * everything else is wrapped in `bf.truthy(...)` (a JS-faithful
26
+ * `ToBoolean`) before being used as an `{% if %}` / ternary test.
27
+ *
28
+ * The classifier walks a `ParsedExpr` produced by
29
+ * `@barefootjs/jsx::parseExpression` — same AST the filter / loop lowerings
30
+ * already use — so detection is structural rather than regex-text-matching.
31
+ * Wrapped expression text is left to the caller's existing
32
+ * `convertExpressionToJinja` pipeline; this module only decides whether to
33
+ * wrap.
34
+ *
35
+ * Detected shapes:
36
+ * - `binary` with a comparison operator (`<`, `>`, `<=`, `>=`, `==`, `===`,
37
+ * `!=`, `!==`)
38
+ * - `unary` with logical `!`
39
+ * - `literal` with `literalType: 'boolean'`
40
+ * - `logical` (`&&` / `||` / `??`) when both sides are themselves
41
+ * boolean-result (catches `x > 0 && y < 10`; intentionally does NOT
42
+ * catch `x() || 'fallback'` whose right side stringifies as a regular
43
+ * value)
44
+ * - `conditional` (`?:`) when both branches are themselves boolean-result
45
+ *
46
+ * Anything else returns `false` — including bare identifiers (`accepted`)
47
+ * and call expressions (`accepted()`) whose return type the adapter has no
48
+ * way to infer from source text alone.
49
+ */
50
+
51
+ import { parseExpression, type ParsedExpr } from '@barefootjs/jsx'
52
+
53
+ const COMPARISON_OPS = new Set([
54
+ '<',
55
+ '>',
56
+ '<=',
57
+ '>=',
58
+ '==',
59
+ '===',
60
+ '!=',
61
+ '!==',
62
+ ])
63
+
64
+ /**
65
+ * Structural boolean-result check over an already-parsed `ParsedExpr` tree.
66
+ * Exported (unlike Xslate's private equivalent) so the condition-position
67
+ * truthy-wrapping call sites can reuse it without a stringify → re-parse
68
+ * round-trip.
69
+ */
70
+ export function isBooleanResultParsed(node: ParsedExpr): boolean {
71
+ switch (node.kind) {
72
+ case 'literal':
73
+ return node.literalType === 'boolean'
74
+ case 'binary':
75
+ return COMPARISON_OPS.has(node.op)
76
+ case 'unary':
77
+ return node.op === '!'
78
+ case 'logical':
79
+ // `x > 0 && y < 10` is boolean; `x() || 'fallback'` is not.
80
+ // Only both-sides-boolean qualifies.
81
+ return (
82
+ isBooleanResultParsed(node.left) && isBooleanResultParsed(node.right)
83
+ )
84
+ case 'conditional':
85
+ // `cond ? bool : bool` is boolean; `cond ? 'a' : 'b'` is not.
86
+ return (
87
+ isBooleanResultParsed(node.consequent) &&
88
+ isBooleanResultParsed(node.alternate)
89
+ )
90
+ default:
91
+ return false
92
+ }
93
+ }
94
+
95
+ export function isBooleanResultExpr(expr: string): boolean {
96
+ const parsed = parseExpression(expr.trim())
97
+ if (!parsed) return false
98
+ return isBooleanResultParsed(parsed)
99
+ }
100
+
101
+ /**
102
+ * True when `expr`'s top-level shape is an explicit JS `String(x)` call
103
+ * (the `EVAL_BUILTIN_IDENTS` builtin the compiler recognizes structurally —
104
+ * `packages/jsx/src/expression-parser.ts`'s `EVAL_BUILTIN_IDENTS`; lowered
105
+ * by this adapter's `String` template primitive to `bf.string(x)`, see
106
+ * `lib/constants.ts`).
107
+ *
108
+ * Guards the `isAriaBooleanAttr`-driven `bf.bool_str(...)` override in
109
+ * `jinja-adapter.ts`'s `elementAttrEmitter`: `bf.string` and `bf.bool_str`
110
+ * produce IDENTICAL text for a real Python `bool` (both are `"true"` /
111
+ * `"false"`), so applying `bf.bool_str` to `String(x)`'s ALREADY-STRINGIFIED
112
+ * result is not a no-op — it is a Python-truthiness test over that STRING
113
+ * ("false" is a non-empty Python string, hence truthy, so
114
+ * `bf.bool_str(bf.string(false))` would wrongly render `"true"`). The Kolon
115
+ * port has the identical double-wrap shape and "works" only by an
116
+ * unrelated accident (`JSON::PP::Boolean` stringifies to `"0"`/`"1"`, and
117
+ * Perl specifically treats the STRING `"0"` as falsy) that doesn't hold in
118
+ * Python. An author who explicitly writes `String(...)` has already opted
119
+ * into JS `String()` semantics — `bf.string(x)` alone (which DOES special-
120
+ * case booleans, see `runtime.js_string`) is the complete, correct
121
+ * lowering; no attribute-name-driven override should run again on top of
122
+ * it.
123
+ */
124
+ export function isExplicitStringCall(expr: string): boolean {
125
+ const parsed = parseExpression(expr.trim())
126
+ return (
127
+ !!parsed &&
128
+ parsed.kind === 'call' &&
129
+ parsed.callee.kind === 'identifier' &&
130
+ parsed.callee.name === 'String' &&
131
+ parsed.args.length === 1
132
+ )
133
+ }
134
+
135
+ /**
136
+ * ARIA attributes whose spec values are `"true"`, `"false"`, and (for
137
+ * tri-state members) `"mixed"`. When a fixture binds one of these to an
138
+ * arbitrary JS expression (`aria-checked={accepted()}`), the expression's
139
+ * actual type isn't recoverable from source text — but the attribute name
140
+ * itself witnesses that the binding is boolean-shaped. Routing these through
141
+ * `bf.bool_str` produces the spec-canonical `"true"` / `"false"` even when
142
+ * the expression is opaque.
143
+ *
144
+ * Deliberately conservative — only includes ARIA attributes whose spec value
145
+ * set is exactly `true | false` or `true | false | mixed`. Tokenised ARIA
146
+ * attributes (`aria-current` is `page | step | …`, `aria-sort` is
147
+ * `ascending | descending | …`) are intentionally excluded so a
148
+ * string-valued binding doesn't get coerced to `"true"` / `"false"`.
149
+ */
150
+ const ARIA_BOOLEAN_ATTRS = new Set([
151
+ // Strict boolean state (true | false; some allow `undefined` = attribute
152
+ // absent, which the runtime emits as no-attr regardless).
153
+ 'aria-atomic',
154
+ 'aria-busy',
155
+ 'aria-disabled',
156
+ 'aria-hidden',
157
+ 'aria-modal',
158
+ 'aria-multiline',
159
+ 'aria-multiselectable',
160
+ 'aria-readonly',
161
+ 'aria-required',
162
+ // true | false | undefined (absent) — selection / disclosure state.
163
+ 'aria-selected',
164
+ 'aria-expanded',
165
+ // Tri-state (true | false | mixed). The `bool_str` helper only maps
166
+ // truthy / falsy to true / false — a fixture that wants the literal
167
+ // "mixed" would bind a string-valued JSX attr (`aria-checked="mixed"`),
168
+ // which lowers through the `literal` emit path and never touches this
169
+ // code.
170
+ 'aria-checked',
171
+ 'aria-pressed',
172
+ ])
173
+
174
+ export function isAriaBooleanAttr(name: string): boolean {
175
+ return ARIA_BOOLEAN_ATTRS.has(name)
176
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * The contract the extracted expression-emitter modules depend on instead of
3
+ * the concrete `JinjaAdapter`.
4
+ *
5
+ * Ported from `packages/adapter-xslate/src/adapter/emit-context.ts`. The
6
+ * Jinja adapter's top-level expression lowering is mutually recursive with
7
+ * the adapter's own const/record resolution and its filter-predicate
8
+ * emitter, so the extracted `JinjaTopLevelEmitter` still needs to call back
9
+ * into shared per-compile state and recursive entry points.
10
+ * `JinjaEmitContext` is that seam: the emitter takes a `JinjaEmitContext`
11
+ * built by the adapter's private `emitCtx` getter (the adapter does NOT
12
+ * `implements` this interface, so the wrapped members stay private and off
13
+ * its exported public type). The emitter depends on this narrow interface
14
+ * rather than the full class, so the coupling is explicit and it's
15
+ * unit-testable against a stub.
16
+ *
17
+ * Keep this surface minimal: add a member only when an extracted module
18
+ * genuinely needs it, so the seam documents the real cross-module coupling
19
+ * rather than re-exposing the whole adapter.
20
+ */
21
+
22
+ import type { ParsedExpr, CompilerError, IRMetadata } from '@barefootjs/jsx'
23
+
24
+ export interface JinjaEmitContext {
25
+ /**
26
+ * (#1922) Local binding names the request-scoped `searchParams()` env signal
27
+ * is imported under. Non-empty enables the env-signal method-call lowering.
28
+ */
29
+ readonly _searchParamsLocals: Set<string>
30
+
31
+ /**
32
+ * Inline a module-scope pure string-literal const by name as the resolved
33
+ * literal value, or null when the name is not such a const.
34
+ */
35
+ _resolveModuleStringConst(name: string): string | null
36
+
37
+ /** Resolve a literal const (`const totalPages = 5`) to its Jinja value, or null. */
38
+ _resolveLiteralConst(name: string): string | null
39
+
40
+ /**
41
+ * Resolve a static property access on a module object-literal const
42
+ * (`variantClasses.ghost`) to its Jinja value at compile time, or null.
43
+ */
44
+ _resolveStaticRecordLiteral(objectName: string, key: string): string | null
45
+
46
+ /** Record a BF101 unsupported-expression diagnostic. */
47
+ _recordExprBF101(message: string, reason?: string): void
48
+
49
+ /** Lower a filter/predicate body to its Jinja form, bound to `param`. */
50
+ _renderJinjaFilterExprPublic(expr: ParsedExpr, param: string): string
51
+ }
52
+
53
+ /**
54
+ * The contract the extracted object-literal / conditional-spread lowering
55
+ * (`spread/spread-codegen.ts`) depends on. Declared separately from
56
+ * `JinjaEmitContext` so each extracted module's real coupling is documented
57
+ * precisely. Mirror of the Xslate adapter's `XslateSpreadContext`.
58
+ */
59
+ export interface JinjaSpreadContext {
60
+ /** Component name, for diagnostic source locations. */
61
+ readonly componentName: string
62
+
63
+ /** Per-compile diagnostic list the spread lowering appends to. */
64
+ readonly errors: CompilerError[]
65
+
66
+ /** Local-constant metadata, for resolving `Record[key]` spread values. */
67
+ readonly localConstants: IRMetadata['localConstants']
68
+
69
+ /** Prop params, for classifying a bare-identifier index as a prop. */
70
+ readonly propsParams: { name: string }[]
71
+
72
+ /**
73
+ * Lower a JS expression to its Jinja form (the core recursive entry).
74
+ *
75
+ * When the IR already carries a structured `ParsedExpr` tree, pass it as
76
+ * `preParsed` so the converter threads it straight through instead of
77
+ * re-parsing `expr`. With `preParsed` set, `expr` is unused for parsing
78
+ * (the converter derives any diagnostic text from the tree), so callers
79
+ * may pass `''`.
80
+ */
81
+ convertExpressionToJinja(expr: string, preParsed?: ParsedExpr): string
82
+
83
+ /**
84
+ * Lower a JS expression to a Jinja CONDITION (routes through `bf.truthy`
85
+ * unless the expression is structurally already boolean-shaped — see
86
+ * `boolean-result.ts`). Used for the conditional-spread ternary's test,
87
+ * which is a condition position, not a value position. Same `preParsed`
88
+ * contract as `convertExpressionToJinja`.
89
+ */
90
+ convertConditionToJinja(expr: string, preParsed?: ParsedExpr): string
91
+ }
92
+
93
+ /**
94
+ * The contract the extracted in-template memo / context seeding
95
+ * (`memo/seed.ts`) depends on. The seed lowering recurses into the core
96
+ * expression lowering to compute a derived signal/memo value or a context
97
+ * default; that recursive entry is its only adapter coupling.
98
+ */
99
+ export interface JinjaMemoContext {
100
+ /**
101
+ * Lower a JS expression to its Jinja form (the core recursive entry). See
102
+ * `JinjaSpreadContext.convertExpressionToJinja` for the `preParsed` contract.
103
+ */
104
+ convertExpressionToJinja(expr: string, preParsed?: ParsedExpr): string
105
+
106
+ /**
107
+ * Per-compile diagnostic list `convertExpressionToJinja` appends to on an
108
+ * unsupported shape (`_recordExprBF101`). `memo/seed.ts`'s
109
+ * `generateDerivedMemoSeed` is a SPECULATIVE "try this in-template
110
+ * recomputation, else fall back to the static ssrDefault seed" attempt per
111
+ * plan step — unlike every other `convertExpressionToJinja` call site, a
112
+ * failure here must NOT become a hard compile error, so it snapshots this
113
+ * array's length before calling in and truncates back to it on failure
114
+ * (discarding whatever `_recordExprBF101` appended) rather than letting
115
+ * the error escape.
116
+ */
117
+ readonly errors: CompilerError[]
118
+ }