@barefootjs/jsx 0.26.1 → 0.26.3
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/adapters/interface.d.ts +29 -0
- package/dist/adapters/interface.d.ts.map +1 -1
- package/dist/adapters/jsx-adapter.d.ts +9 -0
- package/dist/adapters/jsx-adapter.d.ts.map +1 -1
- package/dist/adapters/loop-bound-names.d.ts.map +1 -1
- package/dist/adapters/parsed-expr-emitter.d.ts +0 -10
- package/dist/adapters/parsed-expr-emitter.d.ts.map +1 -1
- package/dist/adapters/test-adapter.d.ts.map +1 -1
- package/dist/analyzer-context.d.ts +11 -1
- package/dist/analyzer-context.d.ts.map +1 -1
- package/dist/analyzer.d.ts +40 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/expression-parser.d.ts.map +1 -1
- package/dist/index.js +662 -223
- package/dist/ir-to-client-js/build-references.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/branch-loop.d.ts +2 -0
- package/dist/ir-to-client-js/control-flow/plan/branch-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-branch-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-component-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-composite-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-event-delegation.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-inner-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/loop.d.ts +12 -0
- package/dist/ir-to-client-js/control-flow/plan/loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/shared.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/event-delegation.d.ts +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/event-delegation.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow.d.ts.map +1 -1
- package/dist/ir-to-client-js/html-template.d.ts +19 -1
- package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
- package/dist/ir-to-client-js/plan/build-static-array-child-init.d.ts.map +1 -1
- package/dist/ir-to-client-js/types.d.ts +4 -4
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/loop-destructure.d.ts.map +1 -1
- package/dist/strip-types.d.ts +18 -0
- package/dist/strip-types.d.ts.map +1 -1
- package/dist/types.d.ts +96 -32
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +94 -0
- package/src/__tests__/client-js-generation.test.ts +10 -3
- package/src/__tests__/compiler-stress-1244.test.ts +12 -1
- package/src/__tests__/event-delegation-scope-2367.test.ts +108 -0
- package/src/__tests__/flatmap-segments.test.ts +80 -0
- package/src/__tests__/map-arbitrary-body.test.ts +226 -0
- package/src/__tests__/map-body-no-silent-divergence.test.ts +360 -0
- package/src/__tests__/map-multi-return-body.test.ts +172 -0
- package/src/__tests__/unsupported-expression.test.ts +20 -2
- package/src/adapters/interface.ts +40 -0
- package/src/adapters/jsx-adapter.ts +10 -0
- package/src/adapters/loop-bound-names.ts +6 -2
- package/src/adapters/parsed-expr-emitter.ts +5 -10
- package/src/adapters/test-adapter.ts +10 -0
- package/src/analyzer-context.ts +35 -1
- package/src/analyzer.ts +162 -24
- package/src/compiler.ts +2 -2
- package/src/expression-parser.ts +27 -19
- package/src/ir-to-client-js/build-references.ts +3 -2
- package/src/ir-to-client-js/collect-elements.ts +3 -3
- package/src/ir-to-client-js/control-flow/plan/branch-loop.ts +2 -0
- package/src/ir-to-client-js/control-flow/plan/build-branch-loop.ts +7 -2
- package/src/ir-to-client-js/control-flow/plan/build-component-loop.ts +4 -0
- package/src/ir-to-client-js/control-flow/plan/build-composite-loop.ts +15 -2
- package/src/ir-to-client-js/control-flow/plan/build-event-delegation.ts +16 -3
- package/src/ir-to-client-js/control-flow/plan/build-inner-loop.ts +26 -3
- package/src/ir-to-client-js/control-flow/plan/build-loop.ts +17 -2
- package/src/ir-to-client-js/control-flow/plan/loop.ts +12 -0
- package/src/ir-to-client-js/control-flow/shared.ts +2 -1
- package/src/ir-to-client-js/control-flow/stringify/event-delegation.ts +9 -2
- package/src/ir-to-client-js/control-flow.ts +11 -0
- package/src/ir-to-client-js/html-template.ts +133 -37
- package/src/ir-to-client-js/plan/build-static-array-child-init.ts +22 -10
- package/src/ir-to-client-js/types.ts +4 -3
- package/src/jsx-to-ir.ts +690 -164
- package/src/loop-destructure.ts +9 -5
- package/src/rich-type-refusal.ts +6 -2
- package/src/strip-types.ts +47 -0
- package/src/types.ts +110 -35
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
attrValueToString,
|
|
30
30
|
} from '../../utils.ts'
|
|
31
31
|
import { buildChildRefBindings, buildStaticChildRefBindings } from '../shared.ts'
|
|
32
|
+
import { renderPreamble, irToHtmlTemplate } from '../../html-template.ts'
|
|
32
33
|
|
|
33
34
|
/**
|
|
34
35
|
* Mirror of the helper in `build-loop-child-arm.ts` — kept local to avoid
|
|
@@ -115,7 +116,7 @@ export function buildInnerLoopsPlan(args: BuildInnerLoopsArgs): InnerLoopsPlan {
|
|
|
115
116
|
const useReactive = refsParent && !!inner.template
|
|
116
117
|
|
|
117
118
|
const emit: InnerLoopReactiveEmit | InnerLoopStaticEmit = useReactive
|
|
118
|
-
? buildReactiveEmit(inner, level, wrapOuter, uidSuffix)
|
|
119
|
+
? buildReactiveEmit(inner, level, wrapOuter, uidSuffix, outerLoopParam, outerLoopParamBindings)
|
|
119
120
|
: buildStaticEmit(inner, level, uidSuffix)
|
|
120
121
|
|
|
121
122
|
const arrayExpr = useReactive ? wrapOuter(inner.array) : inner.array
|
|
@@ -155,6 +156,8 @@ function buildReactiveEmit(
|
|
|
155
156
|
level: DepthLevel,
|
|
156
157
|
wrapOuter: (expr: string) => string,
|
|
157
158
|
uidSuffix: string,
|
|
159
|
+
outerLoopParam?: string,
|
|
160
|
+
outerLoopParamBindings?: readonly LoopParamBinding[],
|
|
158
161
|
): InnerLoopReactiveEmit {
|
|
159
162
|
const wrapInner = (expr: string) => wrapLoopParamAsAccessor(expr, inner.param, inner.paramBindings)
|
|
160
163
|
const { head: paramHead, unwrap: paramUnwrap } = destructureLoopParam(inner.param, inner.paramBindings)
|
|
@@ -229,7 +232,20 @@ function buildReactiveEmit(
|
|
|
229
232
|
const indexAlias = nestedLoopIndexAlias(inner, `__innerIdx${uidSuffix}`, paramHead, level.comps, level.events)
|
|
230
233
|
if (indexAlias) preludeStatements.push(indexAlias)
|
|
231
234
|
if (paramUnwrap) preludeStatements.push(paramUnwrap)
|
|
232
|
-
if (inner.
|
|
235
|
+
if (inner.preamble) {
|
|
236
|
+
// Leaf JSX renders under both param contexts, mirroring the
|
|
237
|
+
// wrapInner(wrapOuter(...)) applied to the js text.
|
|
238
|
+
const leafLoopParams = outerLoopParam
|
|
239
|
+
? [
|
|
240
|
+
{ param: outerLoopParam, bindings: outerLoopParamBindings },
|
|
241
|
+
{ param: inner.param, bindings: inner.paramBindings },
|
|
242
|
+
]
|
|
243
|
+
: [{ param: inner.param, bindings: inner.paramBindings }]
|
|
244
|
+
preludeStatements.push(renderPreamble(inner.preamble, {
|
|
245
|
+
transformJs: (t) => wrapInner(wrapOuter(t)),
|
|
246
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, leafLoopParams, undefined, true),
|
|
247
|
+
}))
|
|
248
|
+
}
|
|
233
249
|
|
|
234
250
|
const childRefs = buildChildRefBindings(inner.bindings.refs, inner.param, inner.paramBindings)
|
|
235
251
|
|
|
@@ -265,7 +281,14 @@ function buildStaticEmit(inner: NestedLoop, level: DepthLevel, uidSuffix: string
|
|
|
265
281
|
const preludeStatements: string[] = []
|
|
266
282
|
const indexAlias = nestedLoopIndexAlias(inner, `__innerIdx${uidSuffix}`, inner.param, level.comps, level.events)
|
|
267
283
|
if (indexAlias) preludeStatements.push(indexAlias)
|
|
268
|
-
|
|
284
|
+
// Static `forEach` receives the literal item, so neither the js text nor a
|
|
285
|
+
// leaf's param reads get accessor-wrapped (mirrors the verbatim emission
|
|
286
|
+
// of the js segments above).
|
|
287
|
+
if (inner.preamble) {
|
|
288
|
+
preludeStatements.push(renderPreamble(inner.preamble, {
|
|
289
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, undefined, undefined, true),
|
|
290
|
+
}))
|
|
291
|
+
}
|
|
269
292
|
return {
|
|
270
293
|
mode: 'static',
|
|
271
294
|
rawKey: inner.key ?? null,
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
import { buildLoopReactiveEffectsPlan } from './build-reactive-effects.ts'
|
|
40
40
|
import { buildComponentLoopPlan } from './build-component-loop.ts'
|
|
41
41
|
import { buildTopLevelCompositePlan } from './build-composite-loop.ts'
|
|
42
|
+
import { renderPreamble, irToHtmlTemplate } from '../../html-template.ts'
|
|
42
43
|
import type {
|
|
43
44
|
LoopPlan,
|
|
44
45
|
PlainLoopPlan,
|
|
@@ -92,6 +93,7 @@ export function buildPlainLoopPlan(elem: TopLevelLoop, profileComponentName?: st
|
|
|
92
93
|
|
|
93
94
|
return {
|
|
94
95
|
kind: 'plain',
|
|
96
|
+
rowConstruction: 'string-template',
|
|
95
97
|
containerVar: `_${varSlotId(elem.slotId)}`,
|
|
96
98
|
markerId: elem.markerId,
|
|
97
99
|
profileLoopId: profileComponentName ? `${profileComponentName}#binding:${elem.slotId}` : undefined,
|
|
@@ -100,7 +102,15 @@ export function buildPlainLoopPlan(elem: TopLevelLoop, profileComponentName?: st
|
|
|
100
102
|
paramHead,
|
|
101
103
|
paramUnwrap,
|
|
102
104
|
indexParam: elem.index || '__idx',
|
|
103
|
-
|
|
105
|
+
// Stage 3 / D4 — js segments get the loop-param accessor wrap; jsx leaves
|
|
106
|
+
// render as HTML-string templates under this loop's param context so a
|
|
107
|
+
// leaf that reads the item (`r`) becomes `r()`.
|
|
108
|
+
mapPreambleWrapped: elem.preamble
|
|
109
|
+
? renderPreamble(elem.preamble, {
|
|
110
|
+
transformJs: wrap,
|
|
111
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: elem.param, bindings: elem.paramBindings }], undefined, true),
|
|
112
|
+
})
|
|
113
|
+
: '',
|
|
104
114
|
template: elem.template,
|
|
105
115
|
skeletonTemplate: elem.skeletonTemplate,
|
|
106
116
|
skeletonPaths: elem.skeletonPaths,
|
|
@@ -139,6 +149,7 @@ export function buildStaticLoopPlan(elem: TopLevelLoop, unsafeLocalNames: Set<st
|
|
|
139
149
|
|
|
140
150
|
return {
|
|
141
151
|
kind: 'static',
|
|
152
|
+
rowConstruction: 'string-template',
|
|
142
153
|
containerVar: `_${varSlotId(elem.slotId)}`,
|
|
143
154
|
arrayExpr: elem.array,
|
|
144
155
|
param: elem.param,
|
|
@@ -188,7 +199,11 @@ function buildStaticLoopMaterialize(
|
|
|
188
199
|
if (!setIntersects(elem.arrayFreeIdentifiers, unsafeLocalNames)) return null
|
|
189
200
|
return {
|
|
190
201
|
itemTemplate: elem.staticItemTemplate,
|
|
191
|
-
mapPreamble: elem.
|
|
202
|
+
mapPreamble: elem.preamble
|
|
203
|
+
? renderPreamble(elem.preamble, {
|
|
204
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: elem.param, bindings: elem.paramBindings }], undefined, true),
|
|
205
|
+
})
|
|
206
|
+
: '',
|
|
192
207
|
bodyIsMultiRoot: elem.bodyIsMultiRoot ?? false,
|
|
193
208
|
}
|
|
194
209
|
}
|
|
@@ -45,6 +45,18 @@ interface LoopPlanCommon {
|
|
|
45
45
|
* — initial mount, SSR hydration, and same-key remount after unmount.
|
|
46
46
|
*/
|
|
47
47
|
childRefs: readonly LoopChildRefBinding[]
|
|
48
|
+
/**
|
|
49
|
+
* How this variant constructs each row's DOM (Stage 3 root cure,
|
|
50
|
+
* spec/callback-fidelity.md). `'string-template'` = rows are built from
|
|
51
|
+
* interpolated innerHTML template literals, so a `.map()` preamble carrying
|
|
52
|
+
* lowered JSX leaves can be spliced via `renderPreamble()`. `'dom-ops'` =
|
|
53
|
+
* rows are built imperatively (createElement/reconcile paths) and CANNOT
|
|
54
|
+
* host a string-lowered preamble — the plan dispatcher refuses a JSX-bearing
|
|
55
|
+
* preamble on such a variant instead of letting it drop silently. Required
|
|
56
|
+
* on every variant (like `childRefs`) so a new loop shape must declare its
|
|
57
|
+
* capability — the compile error is the point.
|
|
58
|
+
*/
|
|
59
|
+
rowConstruction: 'string-template' | 'dom-ops'
|
|
48
60
|
}
|
|
49
61
|
|
|
50
62
|
/** Fields shared by every dynamic (`mapArray`-driven) loop variant. */
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import type { LoopChildEvent, LoopChildRef, TopLevelLoop, NestedLoop, CollectedLoop } from '../types.ts'
|
|
16
16
|
import type { IRLoopChildComponent, LoopParamBinding } from '../../types.ts'
|
|
17
|
+
import { preambleAnalysisText } from '../../types.ts'
|
|
17
18
|
import { quotePropName, wrapLoopParamAsAccessor, irChildrenFreeIds, attrValueToString } from '../utils.ts'
|
|
18
19
|
import { irChildrenToJsExpr } from '../html-template.ts'
|
|
19
20
|
import { emitListenerBlock } from './stringify/event-listener.ts'
|
|
@@ -83,7 +84,7 @@ export function nestedLoopReferencesIndex(
|
|
|
83
84
|
for (const a of inner.bindings.reactiveAttrs) {
|
|
84
85
|
if (exprRefs(a.expression, a.freeIdentifiers)) return true
|
|
85
86
|
}
|
|
86
|
-
if (inner.
|
|
87
|
+
if (inner.preamble && extractFreeIdentifiersFromStatementText(preambleAnalysisText(inner.preamble)).has(index)) return true
|
|
87
88
|
if (inner.template && extractFreeIdentifiersFromTemplateText(inner.template).has(index)) return true
|
|
88
89
|
for (const ev of events) {
|
|
89
90
|
if (exprRefs(ev.handler)) return true
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* if (<container>) <container>.addEventListener('<eventName>', (__bfEvt) => {
|
|
7
7
|
* const target = __bfEvt.target
|
|
8
8
|
* const <slot>El = target.closest('[bf="<slotId>"]')
|
|
9
|
-
* if (<slot>El) {
|
|
9
|
+
* if (<slot>El && <container>.contains(<slot>El)) {
|
|
10
10
|
* <item-lookup specific>
|
|
11
11
|
* <handlerCall>
|
|
12
12
|
* return
|
|
@@ -94,8 +94,15 @@ export function stringifyEventDelegation(lines: string[], plan: EventDelegationP
|
|
|
94
94
|
lines.push(` const target = __bfEvt.target`)
|
|
95
95
|
for (const ev of evs) {
|
|
96
96
|
const childVar = varSlotId(ev.childSlotId)
|
|
97
|
+
// Bound the slot match to the delegating container (#2367). `bf` ids are
|
|
98
|
+
// unique only *within* a component, so an unscoped
|
|
99
|
+
// `target.closest('[bf="sN"]')` can match a same-id element in an ancestor
|
|
100
|
+
// component and silently take the wrong branch. The container holds every
|
|
101
|
+
// slot it delegates on, so a same-id ancestor is never a descendant of it —
|
|
102
|
+
// the `container.contains(...)` guard rejects the foreign match and the
|
|
103
|
+
// handler falls through to the correct branch.
|
|
97
104
|
lines.push(` const ${childVar}El = target.closest('[bf="${ev.childSlotId}"]')`)
|
|
98
|
-
lines.push(` if (${childVar}El) {`)
|
|
105
|
+
lines.push(` if (${childVar}El && ${containerVar}.contains(${childVar}El)) {`)
|
|
99
106
|
const handlerCall = withTurn(`(${ev.handler.trim()})(__bfEvt)`, profileComponentName, ev.childSlotId, ev.eventName)
|
|
100
107
|
switch (itemLookup.kind) {
|
|
101
108
|
case 'keyed':
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import type { ClientJsContext, TopLevelLoop } from './types.ts'
|
|
24
|
+
import { internalInvariant } from '../errors.ts'
|
|
24
25
|
import { buildInsertPlan } from './control-flow/plan/build-insert.ts'
|
|
25
26
|
import { stringifyInsert } from './control-flow/stringify/insert.ts'
|
|
26
27
|
import { buildLoopPlan } from './control-flow/plan/build-loop.ts'
|
|
@@ -70,6 +71,16 @@ export function emitLoopUpdates(lines: string[], ctx: ClientJsContext, unsafeLoc
|
|
|
70
71
|
unsafeLocalNames,
|
|
71
72
|
profileComponentName: ctx.profile ? ctx.componentName : undefined,
|
|
72
73
|
})
|
|
74
|
+
// Stage 3 root cure — a JSX-bearing preamble can only be spliced into a
|
|
75
|
+
// string-templated row (renderPreamble). Every shape that reaches a
|
|
76
|
+
// 'dom-ops' variant today is already refused in Phase 1 with a proper
|
|
77
|
+
// source location; this backstop exists for FUTURE variants, so a new
|
|
78
|
+
// plan kind that declares 'dom-ops' cannot silently drop the preamble —
|
|
79
|
+
// it fails the build here instead.
|
|
80
|
+
internalInvariant(
|
|
81
|
+
!(elem.preamble && elem.preamble.builderNames.length > 0 && plan.rowConstruction === 'dom-ops'),
|
|
82
|
+
`loop variant '${plan.kind}' declares dom-ops row construction but received a JSX-bearing preamble — add a Phase-1 refusal (or wire renderPreamble support) for this shape`,
|
|
83
|
+
)
|
|
73
84
|
stringifyLoop(lines, plan)
|
|
74
85
|
emitLoopEventDelegation(lines, elem, plan.kind, ctx.profile ? ctx.componentName : undefined)
|
|
75
86
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* IR → HTML template string generation and validation.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import type { AttrValue, IRAttribute, IRNode, IRProp } from '../types.ts'
|
|
5
|
+
import type { AttrValue, IRAttribute, IRNode, IRProp, MapCallbackPreamble } from '../types.ts'
|
|
6
6
|
import { isBooleanAttr } from '../html-constants.ts'
|
|
7
7
|
import { toHtmlAttrName, attrValueToString, quotePropName, PROPS_PARAM, DATA_BF_PH, keyAttrName, loopStartMarker, loopEndMarker, loopItemMarker, freeIdsFromRefs, setIntersects, wrapExprWithLoopParams } from './utils.ts'
|
|
8
8
|
import type { LoopParamSpec } from './utils.ts'
|
|
@@ -556,6 +556,71 @@ function itemAnchorTemplate(keyExpr: string): string {
|
|
|
556
556
|
return `<!--${loopItemMarker('${' + keyExpr + '}')}-->`
|
|
557
557
|
}
|
|
558
558
|
|
|
559
|
+
/**
|
|
560
|
+
* The ONLY door from a structured `.map()` preamble to emitted text (Stage 3
|
|
561
|
+
* root cure, spec/callback-fidelity.md). `js` segments contribute their JS
|
|
562
|
+
* text (template variant when requested, then the caller's context transform —
|
|
563
|
+
* e.g. the loop-param accessor wrap); `jsx` segments render their compiled IR
|
|
564
|
+
* as a template-literal HTML string. There is no sentinel substitution because
|
|
565
|
+
* there is no sentinel: mixed content stays structured until this function.
|
|
566
|
+
* Emitters that cannot host a string-templated preamble must not call this —
|
|
567
|
+
* they refuse at plan dispatch instead of splicing.
|
|
568
|
+
*/
|
|
569
|
+
export function renderPreamble(
|
|
570
|
+
preamble: Pick<MapCallbackPreamble, 'segments'>,
|
|
571
|
+
opts: {
|
|
572
|
+
/** Pick `templateText` on js segments (hydrate/CSR template contexts). */
|
|
573
|
+
textVariant?: 'client' | 'template'
|
|
574
|
+
/** Context transform for js text (e.g. wrapLoopParamAsAccessor). */
|
|
575
|
+
transformJs?: (text: string) => string
|
|
576
|
+
/** Context-appropriate leaf renderer (an irToHtmlTemplate variant). */
|
|
577
|
+
renderLeaf: (ir: IRNode) => string
|
|
578
|
+
},
|
|
579
|
+
): string {
|
|
580
|
+
let out = ''
|
|
581
|
+
for (const seg of preamble.segments) {
|
|
582
|
+
if (seg.kind === 'js') {
|
|
583
|
+
const text = opts.textVariant === 'template' ? (seg.templateText ?? seg.text) : seg.text
|
|
584
|
+
out += opts.transformJs ? opts.transformJs(text) : text
|
|
585
|
+
} else {
|
|
586
|
+
out += '`' + opts.renderLeaf(escapeLeafTextExpressions(seg.ir)) + '`'
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return out
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* SSR/CSR escaping parity for preamble leaves, decided once at the door: a
|
|
594
|
+
* JSX-runtime SSR adapter renders the leaf's raw JSX and auto-escapes text
|
|
595
|
+
* interpolations (`{c}`), so the client's HTML-string lowering must escape the
|
|
596
|
+
* same positions — wrap every text-position expression in `escapeText(...)`.
|
|
597
|
+
* Pure (returns a transformed copy); the neutral IR is never mutated with a
|
|
598
|
+
* client-only concern. Attribute values already flow through the template
|
|
599
|
+
* emitters' own attr escaping.
|
|
600
|
+
*/
|
|
601
|
+
function escapeLeafTextExpressions(ir: IRNode): IRNode {
|
|
602
|
+
switch (ir.type) {
|
|
603
|
+
case 'element':
|
|
604
|
+
return { ...ir, children: ir.children.map(escapeLeafTextExpressions) }
|
|
605
|
+
case 'fragment':
|
|
606
|
+
return { ...ir, children: ir.children.map(escapeLeafTextExpressions) }
|
|
607
|
+
case 'expression': {
|
|
608
|
+
if (ir.expr === 'null' || ir.expr === 'undefined') return ir
|
|
609
|
+
// Already-wrapped or slotted expressions keep their existing handling.
|
|
610
|
+
if (ir.slotId || ir.expr.trimStart().startsWith('escapeText(')) return ir
|
|
611
|
+
return { ...ir, expr: `escapeText((${ir.expr}))`, templateExpr: ir.templateExpr ? `escapeText((${ir.templateExpr}))` : ir.templateExpr }
|
|
612
|
+
}
|
|
613
|
+
case 'conditional':
|
|
614
|
+
return {
|
|
615
|
+
...ir,
|
|
616
|
+
whenTrue: escapeLeafTextExpressions(ir.whenTrue),
|
|
617
|
+
whenFalse: ir.whenFalse ? escapeLeafTextExpressions(ir.whenFalse) : ir.whenFalse,
|
|
618
|
+
}
|
|
619
|
+
default:
|
|
620
|
+
return ir
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
559
624
|
export function irToHtmlTemplate(node: IRNode, restSpreadNames?: Set<string>, loopDepth = 0, loopParams?: ReadonlyArray<string | LoopParamSpec>, branchSlotsVar?: string, insideLoop = false, inHoistedChildren = false): string {
|
|
560
625
|
const recurse = (n: IRNode): string => irToHtmlTemplate(n, restSpreadNames, loopDepth, loopParams, branchSlotsVar, insideLoop, inHoistedChildren)
|
|
561
626
|
const wrapExpr = (expr: string) => wrapExprWithLoopParams(expr, loopParams)
|
|
@@ -624,20 +689,29 @@ export function irToHtmlTemplate(node: IRNode, restSpreadNames?: Set<string>, lo
|
|
|
624
689
|
// as HTML (template.innerHTML), so re-escape for the HTML parser.
|
|
625
690
|
return escapeHtml(node.value)
|
|
626
691
|
|
|
627
|
-
case 'expression':
|
|
692
|
+
case 'expression': {
|
|
628
693
|
if (node.expr === 'null' || node.expr === 'undefined') return ''
|
|
694
|
+
const inner = wrapInterpolation(wrapExpr(node.expr))
|
|
695
|
+
// Stage 3 / D4 — an element-array child ({out}) built by an arbitrary
|
|
696
|
+
// .map() preamble is an array of HTML strings; join it rather than let
|
|
697
|
+
// `${[...]}` `String`-comma-collapse it. Only reached on a JS-runtime
|
|
698
|
+
// adapter (the flag is set in Phase 1 only there); a plain local can be
|
|
699
|
+
// read twice safely.
|
|
700
|
+
const valueExpr = node.joinArrayChild
|
|
701
|
+
? `Array.isArray(${inner}) ? ${inner}.join('') : (${inner} ?? '')`
|
|
702
|
+
: inner
|
|
629
703
|
if (node.slotId) {
|
|
630
|
-
const inner = wrapInterpolation(wrapExpr(node.expr))
|
|
631
704
|
// In branch-slot context `wrapInterpolation` routes the value
|
|
632
705
|
// through `__bfSlot`, which returns raw `<!--bf-slot:N-->` markers
|
|
633
706
|
// for live `Node` values (spliced back by `insert()`). Escaping
|
|
634
707
|
// would corrupt those markers and drop slotted content (#1694
|
|
635
708
|
// regression). `__bfSlot` owns coercion of its own value, so the
|
|
636
709
|
// text-escape applies only to the non-slot (plain text) form.
|
|
637
|
-
const slotted = branchSlotsVar ?
|
|
710
|
+
const slotted = branchSlotsVar || node.joinArrayChild ? valueExpr : escapeTextSlotExpr(valueExpr)
|
|
638
711
|
return `<!--bf:${node.slotId}-->\${${slotted}}<!--/-->`
|
|
639
712
|
}
|
|
640
|
-
return `\${${
|
|
713
|
+
return `\${${valueExpr}}`
|
|
714
|
+
}
|
|
641
715
|
|
|
642
716
|
case 'conditional': {
|
|
643
717
|
const trueBranch = recurse(node.whenTrue)
|
|
@@ -740,15 +814,17 @@ export function irToHtmlTemplate(node: IRNode, restSpreadNames?: Set<string>, lo
|
|
|
740
814
|
let mapExpr: string
|
|
741
815
|
|
|
742
816
|
if (node.flatMapCallback) {
|
|
743
|
-
// Complex flatMap:
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
}
|
|
817
|
+
// Complex flatMap: the body is structured segments, rendered through
|
|
818
|
+
// the same single door as map preambles.
|
|
819
|
+
const body = renderPreamble(node.flatMapCallback, {
|
|
820
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, restSpreadNames, loopDepth + 1, loopParams, branchSlotsVar, insideLoop),
|
|
821
|
+
})
|
|
749
822
|
mapExpr = `\${${wrappedArray}.flatMap(${node.flatMapCallback.params} => ${body}).join('')}`
|
|
750
|
-
} else if (node.
|
|
751
|
-
|
|
823
|
+
} else if (node.preamble) {
|
|
824
|
+
// Stage 3 / D4 — render JSX leaves in an arbitrary array-builder
|
|
825
|
+
// preamble (the hydrate-template context uses the bare loop param).
|
|
826
|
+
const preamble = renderPreamble(node.preamble, { textVariant: 'client', renderLeaf: (ir) => irToHtmlTemplate(ir, restSpreadNames, loopDepth + 1, loopParams, branchSlotsVar, insideLoop) })
|
|
827
|
+
mapExpr = `\${${wrappedArray}.${iterMethod}(${callbackParam} => { ${preamble} return \`${childTemplate}\` }).join('')}`
|
|
752
828
|
} else {
|
|
753
829
|
mapExpr = `\${${wrappedArray}.${iterMethod}(${callbackParam} => \`${childTemplate}\`).join('')}`
|
|
754
830
|
}
|
|
@@ -1138,12 +1214,18 @@ export function irToPlaceholderTemplate(node: IRNode, restSpreadNames?: Set<stri
|
|
|
1138
1214
|
// as HTML (template.innerHTML), so re-escape for the HTML parser.
|
|
1139
1215
|
return escapeHtml(node.value)
|
|
1140
1216
|
|
|
1141
|
-
case 'expression':
|
|
1217
|
+
case 'expression': {
|
|
1142
1218
|
if (node.expr === 'null' || node.expr === 'undefined') return ''
|
|
1219
|
+
const wrapped = wrapExpr(node.expr)
|
|
1220
|
+
// Stage 3 / D4 — join an element-array child ({out}) built by the preamble.
|
|
1221
|
+
const value = node.joinArrayChild
|
|
1222
|
+
? `Array.isArray(${wrapped}) ? ${wrapped}.join('') : (${wrapped} ?? '')`
|
|
1223
|
+
: wrapped
|
|
1143
1224
|
if (node.slotId) {
|
|
1144
|
-
return `<!--bf:${node.slotId}-->\${${escapeTextSlotExpr(
|
|
1225
|
+
return `<!--bf:${node.slotId}-->\${${node.joinArrayChild ? value : escapeTextSlotExpr(wrapped)}}<!--/-->`
|
|
1145
1226
|
}
|
|
1146
|
-
return `\${${
|
|
1227
|
+
return `\${${value}}`
|
|
1228
|
+
}
|
|
1147
1229
|
|
|
1148
1230
|
case 'conditional': {
|
|
1149
1231
|
const trueBranch = recurse(node.whenTrue)
|
|
@@ -1180,14 +1262,14 @@ export function irToPlaceholderTemplate(node: IRNode, restSpreadNames?: Set<stri
|
|
|
1180
1262
|
const iterMethod = node.method ?? 'map'
|
|
1181
1263
|
let mapExpr: string
|
|
1182
1264
|
if (node.flatMapCallback) {
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
body = body.replace(frag.placeholder, `\`${renderedIr}\``)
|
|
1187
|
-
}
|
|
1265
|
+
const body = renderPreamble(node.flatMapCallback, {
|
|
1266
|
+
renderLeaf: (ir) => irToPlaceholderTemplate(ir, restSpreadNames, loopDepth + 1, loopParams),
|
|
1267
|
+
})
|
|
1188
1268
|
mapExpr = `\${${wrappedArray}.flatMap(${node.flatMapCallback.params} => ${body}).join('')}`
|
|
1189
|
-
} else if (node.
|
|
1190
|
-
|
|
1269
|
+
} else if (node.preamble) {
|
|
1270
|
+
// Stage 3 / D4 — render JSX leaves in an arbitrary array-builder preamble.
|
|
1271
|
+
const preamble = renderPreamble(node.preamble, { textVariant: 'client', renderLeaf: (ir) => irToPlaceholderTemplate(ir, restSpreadNames, loopDepth + 1, loopParams) })
|
|
1272
|
+
mapExpr = `\${${wrappedArray}.${iterMethod}(${callbackParam} => { ${preamble} return \`${childTemplate}\` }).join('')}`
|
|
1191
1273
|
} else {
|
|
1192
1274
|
mapExpr = `\${${wrappedArray}.${iterMethod}(${callbackParam} => \`${childTemplate}\`).join('')}`
|
|
1193
1275
|
}
|
|
@@ -1553,12 +1635,18 @@ function irToComponentTemplateWithOpts(node: IRNode, opts: TemplateOptions): str
|
|
|
1553
1635
|
// as HTML (template.innerHTML), so re-escape for the HTML parser.
|
|
1554
1636
|
return escapeHtml(node.value)
|
|
1555
1637
|
|
|
1556
|
-
case 'expression':
|
|
1638
|
+
case 'expression': {
|
|
1557
1639
|
if (node.expr === 'null' || node.expr === 'undefined') return ''
|
|
1640
|
+
const wrapped = transformExpr(node.expr, node.templateExpr)
|
|
1641
|
+
// Stage 3 / D4 — join an element-array child ({out}) built by the preamble.
|
|
1642
|
+
const value = node.joinArrayChild
|
|
1643
|
+
? `Array.isArray(${wrapped}) ? ${wrapped}.join('') : (${wrapped} ?? '')`
|
|
1644
|
+
: wrapped
|
|
1558
1645
|
if (node.slotId) {
|
|
1559
|
-
return `<!--bf:${node.slotId}-->\${${
|
|
1646
|
+
return `<!--bf:${node.slotId}-->\${${node.joinArrayChild ? value : escapeTextSlotExpr(wrapped)}}<!--/-->`
|
|
1560
1647
|
}
|
|
1561
|
-
return `\${${
|
|
1648
|
+
return `\${${value}}`
|
|
1649
|
+
}
|
|
1562
1650
|
|
|
1563
1651
|
case 'conditional': {
|
|
1564
1652
|
// A client-only conditional (auto-deferred brand read or manual
|
|
@@ -2134,10 +2222,14 @@ function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): strin
|
|
|
2134
2222
|
// before init's createEffect overwrites the slot (#1128). Emit
|
|
2135
2223
|
// an empty placeholder instead.
|
|
2136
2224
|
const expr = transformed === UNSAFE_TEMPLATE_EXPR ? "''" : transformed
|
|
2225
|
+
// Stage 3 / D4 — join an element-array child ({out}) built by the preamble.
|
|
2226
|
+
const value = node.joinArrayChild
|
|
2227
|
+
? `Array.isArray(${expr}) ? ${expr}.join('') : (${expr} ?? '')`
|
|
2228
|
+
: expr
|
|
2137
2229
|
if (node.slotId) {
|
|
2138
|
-
return `<!--bf:${node.slotId}-->\${${escapeTextSlotExpr(expr)}}<!--/-->`
|
|
2230
|
+
return `<!--bf:${node.slotId}-->\${${node.joinArrayChild ? value : escapeTextSlotExpr(expr)}}<!--/-->`
|
|
2139
2231
|
}
|
|
2140
|
-
return `\${${
|
|
2232
|
+
return `\${${value}}`
|
|
2141
2233
|
}
|
|
2142
2234
|
|
|
2143
2235
|
case 'conditional': {
|
|
@@ -2287,16 +2379,20 @@ function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): strin
|
|
|
2287
2379
|
const iterMethod = node.method ?? 'map'
|
|
2288
2380
|
let mapExpr: string
|
|
2289
2381
|
if (node.flatMapCallback) {
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2382
|
+
// Module-scope template context: pick the templateText variant
|
|
2383
|
+
// (destructured-prop refs rewritten to _p.xxx) and apply the
|
|
2384
|
+
// props-object rewrite to js segments only — a rendered leaf handles
|
|
2385
|
+
// its own props context via recurseInLoopBody.
|
|
2386
|
+
const body = renderPreamble(node.flatMapCallback, {
|
|
2387
|
+
textVariant: 'template',
|
|
2388
|
+
transformJs: (t) => applyPropsRewrite(t, propsObjectName ?? null),
|
|
2389
|
+
renderLeaf: (ir) => recurseInLoopBody(ir),
|
|
2390
|
+
})
|
|
2296
2391
|
mapExpr = `\${${iterArrayExpr}.flatMap(${node.flatMapCallback.params} => ${body}).join('')}`
|
|
2297
|
-
} else if (node.
|
|
2298
|
-
|
|
2299
|
-
|
|
2392
|
+
} else if (node.preamble) {
|
|
2393
|
+
// Stage 3 / D4 — template-variant js text with the props rewrite
|
|
2394
|
+
// applied per segment; JSX leaves render via the loop-body recursion.
|
|
2395
|
+
const preamble = renderPreamble(node.preamble, { textVariant: 'template', transformJs: (t) => applyPropsRewrite(t, propsObjectName ?? null), renderLeaf: (ir) => recurseInLoopBody(ir) })
|
|
2300
2396
|
mapExpr = `\${${iterArrayExpr}.${iterMethod}(${callbackParam} => { ${preamble} return \`${childTemplate}\` }).join('')}`
|
|
2301
2397
|
} else {
|
|
2302
2398
|
mapExpr = `\${${iterArrayExpr}.${iterMethod}(${callbackParam} => \`${childTemplate}\`).join('')}`
|
|
@@ -17,11 +17,23 @@
|
|
|
17
17
|
* stringifier never inspects raw IR.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import type { IRLoopChildComponent } from '../../types.ts'
|
|
21
|
-
import type { NestedLoop, TopLevelLoop } from '../types.ts'
|
|
22
|
-
import type { ClientJsContext } from '../types.ts'
|
|
20
|
+
import type { IRLoopChildComponent, MapCallbackPreamble } from '../../types.ts'
|
|
21
|
+
import type { ClientJsContext, NestedLoop, TopLevelLoop } from '../types.ts'
|
|
23
22
|
import { quotePropName, varSlotId, attrValueToString, buildLoopChildIndexExpr } from '../utils.ts'
|
|
24
|
-
import { irChildrenToJsExpr } from '../html-template.ts'
|
|
23
|
+
import { irChildrenToJsExpr, renderPreamble, irToHtmlTemplate } from '../html-template.ts'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Render a loop preamble for the static-array init context. Static `forEach`
|
|
27
|
+
* binds params as plain values (see `buildStaticPropsExpr` note), so neither
|
|
28
|
+
* js text nor leaf-JSX param reads get accessor-wrapped.
|
|
29
|
+
*/
|
|
30
|
+
function staticPreludeStatements(preamble: MapCallbackPreamble | undefined): string[] {
|
|
31
|
+
return preamble
|
|
32
|
+
? [renderPreamble(preamble, {
|
|
33
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, undefined, undefined, true),
|
|
34
|
+
})]
|
|
35
|
+
: []
|
|
36
|
+
}
|
|
25
37
|
import { buildCompSelector } from '../control-flow/shared.ts'
|
|
26
38
|
|
|
27
39
|
/** The inline prop shape carried on `IRLoopChildComponent.props`. */
|
|
@@ -94,7 +106,7 @@ function buildSingleCompPlan(
|
|
|
94
106
|
arrayExpr: elem.array,
|
|
95
107
|
param: elem.param,
|
|
96
108
|
indexParam: elem.index || '__idx',
|
|
97
|
-
outerPreludeStatements: elem.
|
|
109
|
+
outerPreludeStatements: staticPreludeStatements(elem.preamble),
|
|
98
110
|
propsExpr: buildStaticPropsExpr(props),
|
|
99
111
|
}
|
|
100
112
|
}
|
|
@@ -113,7 +125,7 @@ function buildOuterNestedPlan(
|
|
|
113
125
|
param: elem.param,
|
|
114
126
|
indexParam,
|
|
115
127
|
offsetExpr: buildLoopChildIndexExpr(indexParam, elem.offset),
|
|
116
|
-
outerPreludeStatements: elem.
|
|
128
|
+
outerPreludeStatements: staticPreludeStatements(elem.preamble),
|
|
117
129
|
propsExpr: buildStaticPropsExpr(comp.props),
|
|
118
130
|
}
|
|
119
131
|
}
|
|
@@ -142,13 +154,13 @@ function buildInnerLoopNestedPlan(
|
|
|
142
154
|
outerParam: elem.param,
|
|
143
155
|
outerIndexParam,
|
|
144
156
|
outerOffsetExpr: buildLoopChildIndexExpr(outerIndexParam, elem.offset),
|
|
145
|
-
outerPreludeStatements: elem.
|
|
157
|
+
outerPreludeStatements: staticPreludeStatements(elem.preamble),
|
|
146
158
|
innerContainerSlotId: innerLoop.containerSlotId ?? null,
|
|
147
159
|
innerArrayExpr: innerLoop.array,
|
|
148
160
|
innerParam: innerLoop.param,
|
|
149
161
|
innerIndexParam,
|
|
150
162
|
innerOffsetExpr: buildLoopChildIndexExpr(innerIndexParam, innerLoop.offset),
|
|
151
|
-
innerPreludeStatements: innerLoop.
|
|
163
|
+
innerPreludeStatements: staticPreludeStatements(innerLoop.preamble),
|
|
152
164
|
depth: innerLoop.depth,
|
|
153
165
|
comps,
|
|
154
166
|
}
|
|
@@ -189,11 +201,11 @@ function buildComponentRootedInnerLoopPlan(
|
|
|
189
201
|
// position, so there's no synthetic fallback and index-less loops keep
|
|
190
202
|
// byte-identical output.
|
|
191
203
|
outerIndexParam: elem.index,
|
|
192
|
-
outerPreludeStatements: elem.
|
|
204
|
+
outerPreludeStatements: staticPreludeStatements(elem.preamble),
|
|
193
205
|
innerArrayExpr: innerLoop.array,
|
|
194
206
|
innerParam: innerLoop.param,
|
|
195
207
|
innerIndexParam: innerLoop.index,
|
|
196
|
-
innerPreludeStatements: innerLoop.
|
|
208
|
+
innerPreludeStatements: staticPreludeStatements(innerLoop.preamble),
|
|
197
209
|
depth: innerLoop.depth,
|
|
198
210
|
comps,
|
|
199
211
|
}
|
|
@@ -22,6 +22,7 @@ import type {
|
|
|
22
22
|
ImportInfo,
|
|
23
23
|
TypeInfo,
|
|
24
24
|
TypeDefinition,
|
|
25
|
+
MapCallbackPreamble,
|
|
25
26
|
} from '../types.ts'
|
|
26
27
|
import type { CsrInlinabilityMap } from './csr-substitute.ts'
|
|
27
28
|
import type { SkeletonSlotPaths } from './html-template.ts'
|
|
@@ -356,7 +357,7 @@ export interface BranchLoop extends LoopCore {
|
|
|
356
357
|
index: string | null // Index parameter (e.g., 'i')
|
|
357
358
|
template: string // HTML template for each item
|
|
358
359
|
containerSlotId: string // bf slot ID of the container element (e.g., 's1' for <ul bf="s1">)
|
|
359
|
-
|
|
360
|
+
preamble?: MapCallbackPreamble
|
|
360
361
|
// Composite loop fields (loops whose body contains child components)
|
|
361
362
|
nestedComponents?: IRLoopChildComponent[]
|
|
362
363
|
innerLoops?: NestedLoop[]
|
|
@@ -442,7 +443,7 @@ export interface NestedLoop extends LoopCore {
|
|
|
442
443
|
* `renderItem` callback so locals referenced by the cloned-template IIFE
|
|
443
444
|
* (and any subsequent reads) are in scope (#1052).
|
|
444
445
|
*/
|
|
445
|
-
|
|
446
|
+
preamble?: MapCallbackPreamble
|
|
446
447
|
/** Whether the inner array references the outer loop param (needs reactive mapArray) */
|
|
447
448
|
refsOuterParam?: boolean
|
|
448
449
|
/** Child components inside inner loop items (for initChild/createComponent) */
|
|
@@ -599,7 +600,7 @@ export interface TopLevelLoop extends LoopCore {
|
|
|
599
600
|
raw: string // Full comparator body for client JS
|
|
600
601
|
}
|
|
601
602
|
chainOrder?: 'filter-sort' | 'sort-filter'
|
|
602
|
-
|
|
603
|
+
preamble?: MapCallbackPreamble
|
|
603
604
|
}
|
|
604
605
|
|
|
605
606
|
/**
|