@barefootjs/jinja 0.18.3 → 0.18.5
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/dist/adapter/expr/array-method.d.ts.map +1 -1
- package/dist/adapter/expr/emitters.d.ts +2 -2
- package/dist/adapter/expr/emitters.d.ts.map +1 -1
- package/dist/adapter/index.js +52 -19
- package/dist/adapter/jinja-adapter.d.ts +8 -0
- package/dist/adapter/jinja-adapter.d.ts.map +1 -1
- package/dist/adapter/lib/constants.d.ts.map +1 -1
- package/dist/build.js +52 -19
- package/dist/conformance-pins.d.ts.map +1 -1
- package/dist/index.js +54 -35
- package/dist/render-divergences.d.ts.map +1 -1
- package/package.json +3 -3
- package/python/barefootjs/runtime.py +64 -6
- package/python/tests/test_helper_vectors.py +6 -0
- package/python/tests/test_template_primitives.py +45 -0
- package/src/__tests__/jinja-adapter-unit.test.ts +21 -0
- package/src/adapter/expr/array-method.ts +19 -0
- package/src/adapter/expr/emitters.ts +30 -11
- package/src/adapter/jinja-adapter.ts +62 -8
- package/src/adapter/lib/constants.ts +3 -0
- package/src/conformance-pins.ts +0 -5
- package/src/render-divergences.ts +1 -26
|
@@ -143,7 +143,7 @@ import {
|
|
|
143
143
|
} from '@barefootjs/jsx'
|
|
144
144
|
import { isAriaBooleanAttr, isBooleanResultExpr, isExplicitStringCall } from './boolean-result.ts'
|
|
145
145
|
import type { ParsedExpr, LoweringMatcher } from '@barefootjs/jsx'
|
|
146
|
-
import { BF_SLOT, BF_COND, BF_REGION } from '@barefootjs/shared'
|
|
146
|
+
import { BF_SLOT, BF_COND, BF_REGION, escapeHtml } from '@barefootjs/shared'
|
|
147
147
|
|
|
148
148
|
import type { JinjaRenderCtx } from './lib/types.ts'
|
|
149
149
|
import { JINJA_PRIMITIVE_EMIT_MAP } from './lib/constants.ts'
|
|
@@ -206,6 +206,14 @@ export class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<JinjaRend
|
|
|
206
206
|
private options: Required<JinjaAdapterOptions>
|
|
207
207
|
private errors: CompilerError[] = []
|
|
208
208
|
private inLoop: boolean = false
|
|
209
|
+
/**
|
|
210
|
+
* `IRLoop.depth` of the loop currently being rendered (save/restore
|
|
211
|
+
* around `renderChildren(loop.children)`, mirroring `inLoop` above).
|
|
212
|
+
* `renderAttributes` reads this to derive the `key` → `data-key`/
|
|
213
|
+
* `data-key-N` suffix — the depth is IR-computed (jsx-to-ir.ts), not
|
|
214
|
+
* re-derived here (#2168 nested-loop-outer-binding).
|
|
215
|
+
*/
|
|
216
|
+
private currentLoopKeyDepth = 0
|
|
209
217
|
/**
|
|
210
218
|
* SolidJS-style props identifier (`function(props: P)`) and the
|
|
211
219
|
* analyzer-extracted prop names. Stashed at `generate()` entry so the
|
|
@@ -401,7 +409,9 @@ export class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<JinjaRend
|
|
|
401
409
|
}
|
|
402
410
|
|
|
403
411
|
emitText(node: IRText): string {
|
|
404
|
-
|
|
412
|
+
// IRText carries the entity-DECODED value (Phase 1 decodes JSX
|
|
413
|
+
// character references); re-escape for direct HTML emission.
|
|
414
|
+
return escapeHtml(node.value)
|
|
405
415
|
}
|
|
406
416
|
|
|
407
417
|
emitExpression(node: IRExpression): string {
|
|
@@ -777,7 +787,11 @@ export class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<JinjaRend
|
|
|
777
787
|
// `{...rest}` → `bf.spread_attrs(rest)` emit path both see only the
|
|
778
788
|
// keys NOT already destructured.
|
|
779
789
|
const indexLocalLines: string[] = []
|
|
780
|
-
if (loop.
|
|
790
|
+
if (loop.objectIteration) {
|
|
791
|
+
// `key`/`value` bind directly in the for-header (see `array` below)
|
|
792
|
+
// via Jinja's own dict iteration — no derived `loop.index0` local
|
|
793
|
+
// needed, unlike the array `iterationShape` cases.
|
|
794
|
+
} else if (loop.iterationShape === 'keys') {
|
|
781
795
|
indexLocalLines.push(`{% set ${jinjaIdent(param)} = loop.index0 %}`)
|
|
782
796
|
} else if (loop.index) {
|
|
783
797
|
indexLocalLines.push(`{% set ${jinjaIdent(loop.index)} = loop.index0 %}`)
|
|
@@ -802,10 +816,13 @@ export class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<JinjaRend
|
|
|
802
816
|
|
|
803
817
|
const prevInLoop = this.inLoop
|
|
804
818
|
this.inLoop = true
|
|
819
|
+
const prevLoopKeyDepth = this.currentLoopKeyDepth
|
|
820
|
+
this.currentLoopKeyDepth = loop.depth
|
|
805
821
|
// Re-render children now that inLoop is set (so nested components use the
|
|
806
822
|
// loop-child naming convention). renderedChildren above was computed with
|
|
807
823
|
// the previous flag; recompute under the loop flag.
|
|
808
824
|
const childrenUnderLoop = this.renderChildren(loop.children)
|
|
825
|
+
this.currentLoopKeyDepth = prevLoopKeyDepth
|
|
809
826
|
this.inLoop = prevInLoop
|
|
810
827
|
void renderedChildren
|
|
811
828
|
|
|
@@ -822,7 +839,19 @@ export class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<JinjaRend
|
|
|
822
839
|
// Scoped per-call-site marker so sibling `.map()`s under the same parent
|
|
823
840
|
// each get their own reconciliation range.
|
|
824
841
|
lines.push(`{{ bf.comment("loop:${loop.markerId}") | safe }}`)
|
|
825
|
-
|
|
842
|
+
// `objectIteration` (#2168 object-entries-map): Python `dict` preserves
|
|
843
|
+
// JS `Object.entries()`'s insertion-order semantics natively, so this
|
|
844
|
+
// lowers straight to Jinja's own dict-iteration forms — no runtime
|
|
845
|
+
// helper needed. `.items()` binds `index` (the KEY) alongside `param`;
|
|
846
|
+
// `.keys()`/`.values()` bind `param` alone.
|
|
847
|
+
const forHeader = loop.objectIteration === 'entries'
|
|
848
|
+
? `{% for ${jinjaIdent(loop.index ?? param)}, ${jinjaIdent(param)} in ${array}.items() %}`
|
|
849
|
+
: loop.objectIteration === 'keys'
|
|
850
|
+
? `{% for ${jinjaIdent(param)} in ${array}.keys() %}`
|
|
851
|
+
: loop.objectIteration === 'values'
|
|
852
|
+
? `{% for ${jinjaIdent(param)} in ${array}.values() %}`
|
|
853
|
+
: `{% for ${jinjaIdent(loopVar)} in ${array} %}`
|
|
854
|
+
lines.push(forHeader)
|
|
826
855
|
for (const il of indexLocalLines) lines.push(il)
|
|
827
856
|
|
|
828
857
|
// Handle filter().map() pattern by wrapping children in if-condition
|
|
@@ -974,11 +1003,33 @@ export class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<JinjaRend
|
|
|
974
1003
|
type Segment = { kind: 'entries'; parts: string[] } | { kind: 'spread'; expr: string }
|
|
975
1004
|
const segments: Segment[] = [{ kind: 'entries', parts: [] }]
|
|
976
1005
|
const currentEntries = () => this.componentPropSegmentEntries(segments)
|
|
1006
|
+
// Named JSX-valued props OTHER than the reserved `children`
|
|
1007
|
+
// (`header={<strong>Title</strong>}`, #2168 jsx-element-prop) each get
|
|
1008
|
+
// their own `{% set %}` capture, prepended to the final returned
|
|
1009
|
+
// string below — same mechanism as the reserved children capture,
|
|
1010
|
+
// just keyed by the prop's own name instead of `children`.
|
|
1011
|
+
const namedSlotSetBlocks: string[] = []
|
|
977
1012
|
|
|
978
1013
|
for (const p of comp.props) {
|
|
979
1014
|
// Skip callback props (onXxx) and `ref` — both are client-only for
|
|
980
1015
|
// SSR (Hono renders neither; the client JS wires them at hydration).
|
|
981
1016
|
if ((p.name.match(/^on[A-Z]/) || p.name === 'ref') && p.value.kind === 'expression') continue
|
|
1017
|
+
if (p.value.kind === 'jsx-children' && p.name !== 'children') {
|
|
1018
|
+
const prevInLoop = this.inLoop
|
|
1019
|
+
this.inLoop = false
|
|
1020
|
+
const slotBody = this.renderChildren(p.value.children)
|
|
1021
|
+
this.inLoop = prevInLoop
|
|
1022
|
+
// Purely counter-based — NOT derived from `p.name` or `comp.slotId`.
|
|
1023
|
+
// A JSX prop name can contain characters (`data-slot`) that aren't a
|
|
1024
|
+
// valid Jinja `{% set %}` target, and `comp.slotId` alone would
|
|
1025
|
+
// collide across two named-slot props on the same component
|
|
1026
|
+
// invocation (unlike the reserved children slot, there's only ever
|
|
1027
|
+
// one of those per invocation).
|
|
1028
|
+
const captureName = `bf_prop_${this.childrenCaptureCounter++}`
|
|
1029
|
+
namedSlotSetBlocks.push(`{% set ${captureName} %}${slotBody}{% endset %}`)
|
|
1030
|
+
currentEntries().push(`${jinjaHashKey(p.name)}: ${captureName}`)
|
|
1031
|
+
continue
|
|
1032
|
+
}
|
|
982
1033
|
if (p.value.kind === 'spread') {
|
|
983
1034
|
const trimmed = p.value.expr.trim()
|
|
984
1035
|
// SolidJS-style props identifier (`function(props: P)`) has no
|
|
@@ -1042,12 +1093,12 @@ export class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<JinjaRend
|
|
|
1042
1093
|
const captureName = `bf_children_${comp.slotId ?? 'c' + this.childrenCaptureCounter++}`
|
|
1043
1094
|
currentEntries().push(`${jinjaHashKey('children')}: ${captureName}`)
|
|
1044
1095
|
const dict = this.combineComponentPropSegments(segments)
|
|
1045
|
-
return
|
|
1096
|
+
return `${namedSlotSetBlocks.join('')}{% set ${captureName} %}${childrenBody}{% endset %}{{ bf.render_child('${tplName}', ${dict}) | safe }}`
|
|
1046
1097
|
}
|
|
1047
1098
|
|
|
1048
1099
|
const isEmpty = segments.every(s => s.kind === 'entries' && s.parts.length === 0)
|
|
1049
1100
|
const dictEntries = isEmpty ? '' : `, ${this.combineComponentPropSegments(segments)}`
|
|
1050
|
-
return
|
|
1101
|
+
return `${namedSlotSetBlocks.join('')}{{ bf.render_child('${tplName}'${dictEntries}) | safe }}`
|
|
1051
1102
|
}
|
|
1052
1103
|
|
|
1053
1104
|
private childrenCaptureCounter = 0
|
|
@@ -1132,7 +1183,7 @@ export class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<JinjaRend
|
|
|
1132
1183
|
* AttrValue lowering for intrinsic-element attributes (Jinja).
|
|
1133
1184
|
*/
|
|
1134
1185
|
private readonly elementAttrEmitter: AttrValueEmitter = {
|
|
1135
|
-
emitLiteral: (value, name) => `${name}="${value.value}"`,
|
|
1186
|
+
emitLiteral: (value, name) => `${name}="${escapeHtml(value.value)}"`,
|
|
1136
1187
|
emitExpression: (value, name) => {
|
|
1137
1188
|
// `style={{ … }}` object literal → a CSS string with dynamic values
|
|
1138
1189
|
// interpolated, instead of refusing the bare object with BF101 (#1322).
|
|
@@ -1337,7 +1388,10 @@ export class JinjaAdapter extends BaseAdapter implements IRNodeEmitter<JinjaRend
|
|
|
1337
1388
|
// Rewrite JSX special-prop names to their HTML-attribute counterparts.
|
|
1338
1389
|
let attrName: string
|
|
1339
1390
|
if (attr.name === 'className') attrName = 'class'
|
|
1340
|
-
else if (attr.name === 'key')
|
|
1391
|
+
else if (attr.name === 'key') {
|
|
1392
|
+
const depth = this.currentLoopKeyDepth
|
|
1393
|
+
attrName = depth > 0 ? `data-key-${depth}` : 'data-key'
|
|
1394
|
+
}
|
|
1341
1395
|
else attrName = attr.name
|
|
1342
1396
|
const lowered = emitAttrValue(attr.value, this.elementAttrEmitter, attrName)
|
|
1343
1397
|
if (lowered) parts.push(lowered)
|
|
@@ -22,6 +22,9 @@ export const JINJA_TEMPLATE_PRIMITIVES: Record<string, PrimitiveSpec> = {
|
|
|
22
22
|
'Math.floor': { arity: 1, emit: (args) => `bf.floor(${args[0]})` },
|
|
23
23
|
'Math.ceil': { arity: 1, emit: (args) => `bf.ceil(${args[0]})` },
|
|
24
24
|
'Math.round': { arity: 1, emit: (args) => `bf.round(${args[0]})` },
|
|
25
|
+
'Math.min': { arity: 2, emit: (args) => `bf.min(${args[0]}, ${args[1]})` },
|
|
26
|
+
'Math.max': { arity: 2, emit: (args) => `bf.max(${args[0]}, ${args[1]})` },
|
|
27
|
+
'Math.abs': { arity: 1, emit: (args) => `bf.abs(${args[0]})` },
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
/**
|
package/src/conformance-pins.ts
CHANGED
|
@@ -104,9 +104,4 @@ export const conformancePins: ConformancePins = {
|
|
|
104
104
|
// the shape loudly instead of emitting entity-escaped markup that
|
|
105
105
|
// silently renders tags as text.
|
|
106
106
|
'dangerous-inner-html': [{ code: 'BF101', severity: 'error' }],
|
|
107
|
-
// Edge-case sweep (Priority 12): `.replaceAll` has no lowering yet —
|
|
108
|
-
// only first-occurrence `.replace` is wired to the runtime helpers.
|
|
109
|
-
// Refused with BF101 rather than reusing the first-only lowering,
|
|
110
|
-
// which would silently change semantics.
|
|
111
|
-
'string-replaceall': [{ code: 'BF101', severity: 'error' }],
|
|
112
107
|
}
|
|
@@ -14,29 +14,4 @@
|
|
|
14
14
|
|
|
15
15
|
import type { RenderDivergences } from '@barefootjs/jsx'
|
|
16
16
|
|
|
17
|
-
export const renderDivergences: RenderDivergences = {
|
|
18
|
-
'arithmetic-text':
|
|
19
|
-
'`(count() + 2) * 3` renders 10 instead of 18 — the parenthesised sub-expression loses its grouping (silent wrong arithmetic)',
|
|
20
|
-
'html-entity-text':
|
|
21
|
-
'`©` in JSX literal text: Hono decodes to `©`, this adapter re-emits the raw entity — same DOM, different bytes',
|
|
22
|
-
'math-methods':
|
|
23
|
-
'Math.min/max/abs over a signal render empty (only Math.floor is in the template-primitive registry)',
|
|
24
|
-
'boolean-attr-literals':
|
|
25
|
-
'camelCase boolean alias `readOnly`: Hono SSRs `readOnly="true"`, this adapter emits bare presence',
|
|
26
|
-
'camelcase-attributes':
|
|
27
|
-
'`htmlFor` is not lowered to `for` (Hono maps it)',
|
|
28
|
-
'static-attr-escape':
|
|
29
|
-
'static attribute values are not HTML-escaped (`title="Fish & Chips"` emitted raw; Hono escapes)',
|
|
30
|
-
'svg-icon':
|
|
31
|
-
'SVG camelCase presentation attrs (`strokeWidth`, `strokeLinecap`) pass through unmapped; Hono lowers to kebab-case',
|
|
32
|
-
'object-entries-map':
|
|
33
|
-
'`Object.entries(prop).map(([k, v]) => …)` renders an EMPTY list — the object-shaped prop silently produces zero iterations',
|
|
34
|
-
'nested-loop-outer-binding':
|
|
35
|
-
'nested-loop inner items carry `data-key` where the reference emits the depth-suffixed `data-key-1`',
|
|
36
|
-
'jsx-element-prop':
|
|
37
|
-
'a JSX element passed as a NON-children prop renders an empty slot — the element value is silently dropped',
|
|
38
|
-
'string-slice':
|
|
39
|
-
'`.slice()` on a STRING renders empty (array-slice helper misfires on strings)',
|
|
40
|
-
'string-trim-sided':
|
|
41
|
-
'`.trimStart()` / `.trimEnd()` render empty (no lowering)',
|
|
42
|
-
}
|
|
17
|
+
export const renderDivergences: RenderDivergences = {}
|