@barefootjs/rust 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 +50 -17
- package/dist/adapter/lib/constants.d.ts.map +1 -1
- package/dist/adapter/minijinja-adapter.d.ts +8 -0
- package/dist/adapter/minijinja-adapter.d.ts.map +1 -1
- package/dist/build.js +50 -17
- package/dist/conformance-pins.d.ts.map +1 -1
- package/dist/index.js +52 -33
- package/dist/render-divergences.d.ts.map +1 -1
- package/package.json +3 -3
- package/runtime/src/num.rs +30 -0
- package/runtime/src/runtime.rs +78 -10
- package/runtime/tests/helper_vectors.rs +6 -0
- package/runtime/tests/template_primitives.rs +42 -0
- package/src/__tests__/minijinja-adapter-unit.test.ts +21 -0
- package/src/adapter/expr/array-method.ts +19 -0
- package/src/adapter/expr/emitters.ts +13 -7
- package/src/adapter/lib/constants.ts +3 -0
- package/src/adapter/minijinja-adapter.ts +68 -8
- package/src/conformance-pins.ts +0 -5
- package/src/render-divergences.ts +1 -26
|
@@ -168,7 +168,7 @@ import {
|
|
|
168
168
|
} from '@barefootjs/jsx'
|
|
169
169
|
import { isAriaBooleanAttr, isBooleanResultExpr, isExplicitStringCall } from './boolean-result.ts'
|
|
170
170
|
import type { ParsedExpr, LoweringMatcher, LoopBindingPathSegment } from '@barefootjs/jsx'
|
|
171
|
-
import { BF_SLOT, BF_COND, BF_REGION } from '@barefootjs/shared'
|
|
171
|
+
import { BF_SLOT, BF_COND, BF_REGION, escapeHtml } from '@barefootjs/shared'
|
|
172
172
|
|
|
173
173
|
import type { JinjaRenderCtx } from './lib/types.ts'
|
|
174
174
|
import { JINJA_PRIMITIVE_EMIT_MAP } from './lib/constants.ts'
|
|
@@ -254,6 +254,14 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
|
|
|
254
254
|
private options: Required<MinijinjaAdapterOptions>
|
|
255
255
|
private errors: CompilerError[] = []
|
|
256
256
|
private inLoop: boolean = false
|
|
257
|
+
/**
|
|
258
|
+
* `IRLoop.depth` of the loop currently being rendered (save/restore
|
|
259
|
+
* around `renderChildren(loop.children)`, mirroring `inLoop` above).
|
|
260
|
+
* `renderAttributes` reads this to derive the `key` → `data-key`/
|
|
261
|
+
* `data-key-N` suffix — the depth is IR-computed (jsx-to-ir.ts), not
|
|
262
|
+
* re-derived here (#2168 nested-loop-outer-binding).
|
|
263
|
+
*/
|
|
264
|
+
private currentLoopKeyDepth = 0
|
|
257
265
|
/**
|
|
258
266
|
* SolidJS-style props identifier (`function(props: P)`) and the
|
|
259
267
|
* analyzer-extracted prop names. Stashed at `generate()` entry so the
|
|
@@ -449,7 +457,9 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
|
|
|
449
457
|
}
|
|
450
458
|
|
|
451
459
|
emitText(node: IRText): string {
|
|
452
|
-
|
|
460
|
+
// IRText carries the entity-DECODED value (Phase 1 decodes JSX
|
|
461
|
+
// character references); re-escape for direct HTML emission.
|
|
462
|
+
return escapeHtml(node.value)
|
|
453
463
|
}
|
|
454
464
|
|
|
455
465
|
emitExpression(node: IRExpression): string {
|
|
@@ -810,7 +820,11 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
|
|
|
810
820
|
// adds one `{% set %}` local per binding (`rest` aliases the item so
|
|
811
821
|
// `rest.flag` resolves).
|
|
812
822
|
const indexLocalLines: string[] = []
|
|
813
|
-
if (loop.
|
|
823
|
+
if (loop.objectIteration) {
|
|
824
|
+
// `key`/`value` bind directly in the for-header (see below) via the
|
|
825
|
+
// `|items` filter — no derived `loop.index0` local needed, unlike
|
|
826
|
+
// the array `iterationShape` cases.
|
|
827
|
+
} else if (loop.iterationShape === 'keys') {
|
|
814
828
|
indexLocalLines.push(`{% set ${minijinjaIdent(param)} = loop.index0 %}`)
|
|
815
829
|
} else if (loop.index) {
|
|
816
830
|
indexLocalLines.push(`{% set ${minijinjaIdent(loop.index)} = loop.index0 %}`)
|
|
@@ -843,10 +857,13 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
|
|
|
843
857
|
|
|
844
858
|
const prevInLoop = this.inLoop
|
|
845
859
|
this.inLoop = true
|
|
860
|
+
const prevLoopKeyDepth = this.currentLoopKeyDepth
|
|
861
|
+
this.currentLoopKeyDepth = loop.depth
|
|
846
862
|
// Re-render children now that inLoop is set (so nested components use the
|
|
847
863
|
// loop-child naming convention). renderedChildren above was computed with
|
|
848
864
|
// the previous flag; recompute under the loop flag.
|
|
849
865
|
const childrenUnderLoop = this.renderChildren(loop.children)
|
|
866
|
+
this.currentLoopKeyDepth = prevLoopKeyDepth
|
|
850
867
|
this.inLoop = prevInLoop
|
|
851
868
|
void renderedChildren
|
|
852
869
|
|
|
@@ -863,7 +880,25 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
|
|
|
863
880
|
// Scoped per-call-site marker so sibling `.map()`s under the same parent
|
|
864
881
|
// each get their own reconciliation range.
|
|
865
882
|
lines.push(`{{ bf.comment("loop:${loop.markerId}") | safe }}`)
|
|
866
|
-
|
|
883
|
+
// `objectIteration` (#2168 object-entries-map): minijinja has no
|
|
884
|
+
// built-in `.items()` OBJECT METHOD (unlike Python's dict) — `|items`
|
|
885
|
+
// is a FILTER, yielding `[key, value]` pairs, which the `for` tag's
|
|
886
|
+
// own tuple-unpack target (`for a, b in ...`, mirroring Jinja2) binds
|
|
887
|
+
// directly. There's no `|keys`/`|values` filter, so `'keys'`/`'values'`
|
|
888
|
+
// reuse the SAME `|items` pairs and bind the unused half to a
|
|
889
|
+
// throwaway name. Order is whatever the underlying `BTreeMap` gives —
|
|
890
|
+
// sorted-by-key, not JS insertion order (a deliberate design choice
|
|
891
|
+
// for a DIFFERENT feature, canonical JSON encoding — see `num.rs`);
|
|
892
|
+
// this happens to satisfy the current fixture, but is a documented
|
|
893
|
+
// known limitation for out-of-alphabetical-order data, same as Go.
|
|
894
|
+
const forHeader = loop.objectIteration === 'entries'
|
|
895
|
+
? `{% for ${minijinjaIdent(loop.index ?? param)}, ${minijinjaIdent(param)} in ${array}|items %}`
|
|
896
|
+
: loop.objectIteration === 'keys'
|
|
897
|
+
? `{% for ${minijinjaIdent(param)}, __bf_v in ${array}|items %}`
|
|
898
|
+
: loop.objectIteration === 'values'
|
|
899
|
+
? `{% for __bf_k, ${minijinjaIdent(param)} in ${array}|items %}`
|
|
900
|
+
: `{% for ${minijinjaIdent(loopVar)} in ${array} %}`
|
|
901
|
+
lines.push(forHeader)
|
|
867
902
|
for (const il of indexLocalLines) lines.push(il)
|
|
868
903
|
|
|
869
904
|
// Handle filter().map() pattern by wrapping children in if-condition
|
|
@@ -1017,11 +1052,33 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
|
|
|
1017
1052
|
type Segment = { kind: 'entries'; parts: string[] } | { kind: 'spread'; expr: string }
|
|
1018
1053
|
const segments: Segment[] = [{ kind: 'entries', parts: [] }]
|
|
1019
1054
|
const currentEntries = () => this.componentPropSegmentEntries(segments)
|
|
1055
|
+
// Named JSX-valued props OTHER than the reserved `children`
|
|
1056
|
+
// (`header={<strong>Title</strong>}`, #2168 jsx-element-prop) each get
|
|
1057
|
+
// their own `{% set %}` capture, prepended to the final returned
|
|
1058
|
+
// string below — same mechanism as the reserved children capture,
|
|
1059
|
+
// just keyed by the prop's own name instead of `children`.
|
|
1060
|
+
const namedSlotSetBlocks: string[] = []
|
|
1020
1061
|
|
|
1021
1062
|
for (const p of comp.props) {
|
|
1022
1063
|
// Skip callback props (onXxx) and `ref` — both are client-only for
|
|
1023
1064
|
// SSR (Hono renders neither; the client JS wires them at hydration).
|
|
1024
1065
|
if ((p.name.match(/^on[A-Z]/) || p.name === 'ref') && p.value.kind === 'expression') continue
|
|
1066
|
+
if (p.value.kind === 'jsx-children' && p.name !== 'children') {
|
|
1067
|
+
const prevInLoop = this.inLoop
|
|
1068
|
+
this.inLoop = false
|
|
1069
|
+
const slotBody = this.renderChildren(p.value.children)
|
|
1070
|
+
this.inLoop = prevInLoop
|
|
1071
|
+
// Purely counter-based — NOT derived from `p.name` or `comp.slotId`.
|
|
1072
|
+
// A JSX prop name can contain characters (`data-slot`) that aren't a
|
|
1073
|
+
// valid minijinja `{% set %}` target, and `comp.slotId` alone would
|
|
1074
|
+
// collide across two named-slot props on the same component
|
|
1075
|
+
// invocation (unlike the reserved children slot, there's only ever
|
|
1076
|
+
// one of those per invocation).
|
|
1077
|
+
const captureName = `bf_prop_${this.childrenCaptureCounter++}`
|
|
1078
|
+
namedSlotSetBlocks.push(`{% set ${captureName} %}${slotBody}{% endset %}`)
|
|
1079
|
+
currentEntries().push(`${minijinjaHashKey(p.name)}: ${captureName}`)
|
|
1080
|
+
continue
|
|
1081
|
+
}
|
|
1025
1082
|
if (p.value.kind === 'spread') {
|
|
1026
1083
|
const trimmed = p.value.expr.trim()
|
|
1027
1084
|
// SolidJS-style props identifier (`function(props: P)`) has no
|
|
@@ -1085,12 +1142,12 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
|
|
|
1085
1142
|
const captureName = `bf_children_${comp.slotId ?? 'c' + this.childrenCaptureCounter++}`
|
|
1086
1143
|
currentEntries().push(`${minijinjaHashKey('children')}: ${captureName}`)
|
|
1087
1144
|
const dict = this.combineComponentPropSegments(segments)
|
|
1088
|
-
return
|
|
1145
|
+
return `${namedSlotSetBlocks.join('')}{% set ${captureName} %}${childrenBody}{% endset %}{{ bf.render_child('${tplName}', ${dict}) | safe }}`
|
|
1089
1146
|
}
|
|
1090
1147
|
|
|
1091
1148
|
const isEmpty = segments.every(s => s.kind === 'entries' && s.parts.length === 0)
|
|
1092
1149
|
const dictEntries = isEmpty ? '' : `, ${this.combineComponentPropSegments(segments)}`
|
|
1093
|
-
return
|
|
1150
|
+
return `${namedSlotSetBlocks.join('')}{{ bf.render_child('${tplName}'${dictEntries}) | safe }}`
|
|
1094
1151
|
}
|
|
1095
1152
|
|
|
1096
1153
|
private childrenCaptureCounter = 0
|
|
@@ -1175,7 +1232,7 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
|
|
|
1175
1232
|
* AttrValue lowering for intrinsic-element attributes (Jinja).
|
|
1176
1233
|
*/
|
|
1177
1234
|
private readonly elementAttrEmitter: AttrValueEmitter = {
|
|
1178
|
-
emitLiteral: (value, name) => `${name}="${value.value}"`,
|
|
1235
|
+
emitLiteral: (value, name) => `${name}="${escapeHtml(value.value)}"`,
|
|
1179
1236
|
emitExpression: (value, name) => {
|
|
1180
1237
|
// `style={{ … }}` object literal → a CSS string with dynamic values
|
|
1181
1238
|
// interpolated, instead of refusing the bare object with BF101 (#1322).
|
|
@@ -1380,7 +1437,10 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
|
|
|
1380
1437
|
// Rewrite JSX special-prop names to their HTML-attribute counterparts.
|
|
1381
1438
|
let attrName: string
|
|
1382
1439
|
if (attr.name === 'className') attrName = 'class'
|
|
1383
|
-
else if (attr.name === 'key')
|
|
1440
|
+
else if (attr.name === 'key') {
|
|
1441
|
+
const depth = this.currentLoopKeyDepth
|
|
1442
|
+
attrName = depth > 0 ? `data-key-${depth}` : 'data-key'
|
|
1443
|
+
}
|
|
1384
1444
|
else attrName = attr.name
|
|
1385
1445
|
const lowered = emitAttrValue(attr.value, this.elementAttrEmitter, attrName)
|
|
1386
1446
|
if (lowered) parts.push(lowered)
|
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
|
}
|
|
@@ -16,29 +16,4 @@
|
|
|
16
16
|
|
|
17
17
|
import type { RenderDivergences } from '@barefootjs/jsx'
|
|
18
18
|
|
|
19
|
-
export const renderDivergences: RenderDivergences = {
|
|
20
|
-
'arithmetic-text':
|
|
21
|
-
'`(count() + 2) * 3` renders 10 instead of 18 — the parenthesised sub-expression loses its grouping (silent wrong arithmetic)',
|
|
22
|
-
'html-entity-text':
|
|
23
|
-
'`©` in JSX literal text: Hono decodes to `©`, this adapter re-emits the raw entity — same DOM, different bytes',
|
|
24
|
-
'math-methods':
|
|
25
|
-
'Math.min/max/abs over a signal render empty (only Math.floor is in the template-primitive registry)',
|
|
26
|
-
'boolean-attr-literals':
|
|
27
|
-
'camelCase boolean alias `readOnly`: Hono SSRs `readOnly="true"`, this adapter emits bare presence',
|
|
28
|
-
'camelcase-attributes':
|
|
29
|
-
'`htmlFor` is not lowered to `for` (Hono maps it)',
|
|
30
|
-
'static-attr-escape':
|
|
31
|
-
'static attribute values are not HTML-escaped (`title="Fish & Chips"` emitted raw; Hono escapes)',
|
|
32
|
-
'svg-icon':
|
|
33
|
-
'SVG camelCase presentation attrs (`strokeWidth`, `strokeLinecap`) pass through unmapped; Hono lowers to kebab-case',
|
|
34
|
-
'object-entries-map':
|
|
35
|
-
'`Object.entries(prop).map(([k, v]) => …)` renders an EMPTY list — the object-shaped prop silently produces zero iterations',
|
|
36
|
-
'nested-loop-outer-binding':
|
|
37
|
-
'nested-loop inner items carry `data-key` where the reference emits the depth-suffixed `data-key-1`',
|
|
38
|
-
'jsx-element-prop':
|
|
39
|
-
'a JSX element passed as a NON-children prop renders an empty slot — the element value is silently dropped',
|
|
40
|
-
'string-slice':
|
|
41
|
-
'`.slice()` on a STRING renders empty (array-slice helper misfires on strings)',
|
|
42
|
-
'string-trim-sided':
|
|
43
|
-
'`.trimStart()` / `.trimEnd()` render empty (no lowering)',
|
|
44
|
-
}
|
|
19
|
+
export const renderDivergences: RenderDivergences = {}
|