@barefootjs/jsx 0.31.8 → 0.31.10
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/analyzer.d.ts.map +1 -1
- package/dist/expression-parser.d.ts +20 -0
- package/dist/expression-parser.d.ts.map +1 -1
- package/dist/index.js +226 -66
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/template-parse.d.ts +66 -38
- package/dist/ir-to-client-js/control-flow/stringify/template-parse.d.ts.map +1 -1
- package/dist/ir-to-client-js/emit-registration.d.ts.map +1 -1
- package/dist/ir-to-client-js/html-template.d.ts +15 -1
- package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
- package/dist/ir-to-client-js/imports.d.ts +2 -2
- package/dist/ir-to-client-js/imports.d.ts.map +1 -1
- package/dist/ssr-defaults.d.ts +10 -0
- package/dist/ssr-defaults.d.ts.map +1 -1
- package/dist/ssr-seed-plan.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +210 -210
- package/src/__tests__/aliased-destructured-prop-csr.test.ts +8 -2
- package/src/__tests__/csr-template-loop-shadowing.test.ts +5 -2
- package/src/__tests__/date-lowering.test.ts +1 -1
- package/src/__tests__/env-signal-template-prelude.test.ts +4 -2
- package/src/__tests__/inner-loop-mathml-namespace.test.ts +135 -0
- package/src/__tests__/markup-prop-brand.test.ts +161 -0
- package/src/__tests__/mathml-mapArray-namespace.test.ts +230 -0
- package/src/__tests__/props-destructuring.test.ts +40 -3
- package/src/__tests__/ssr-defaults.test.ts +230 -0
- package/src/__tests__/ssr-seed-plan.test.ts +72 -0
- package/src/__tests__/text-slot-escaping.test.ts +21 -12
- package/src/analyzer.ts +83 -26
- package/src/expression-parser.ts +10 -1
- package/src/ir-to-client-js/collect-elements.ts +38 -5
- package/src/ir-to-client-js/control-flow/stringify/inner-loop.ts +10 -10
- package/src/ir-to-client-js/control-flow/stringify/loop-child-arm.ts +3 -4
- package/src/ir-to-client-js/control-flow/stringify/loop.ts +18 -14
- package/src/ir-to-client-js/control-flow/stringify/template-parse.ts +142 -78
- package/src/ir-to-client-js/emit-registration.ts +5 -1
- package/src/ir-to-client-js/html-template.ts +103 -12
- package/src/ir-to-client-js/imports.ts +4 -0
- package/src/ir-to-client-js/index.ts +6 -1
- package/src/ssr-defaults.ts +171 -6
- package/src/ssr-seed-plan.ts +82 -2
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
import { varSlotId, DATA_BF_PH, keyAttrName, profileBindingId } from '../../utils.ts'
|
|
15
15
|
import { emitComponentAndEventSetup } from '../shared.ts'
|
|
16
16
|
import { emitAttrUpdate } from '../../emit-reactive.ts'
|
|
17
|
-
import {
|
|
17
|
+
import { namespaceWrapForTemplate } from './template-parse.ts'
|
|
18
18
|
import { emitListenerLine } from './event-listener.ts'
|
|
19
19
|
import { nameForRegistryRef } from '../../component-scope.ts'
|
|
20
20
|
import { claimPlanLiteral, claimWriterVarName, type ClaimSlotSpec } from './claim-plan.ts'
|
|
@@ -139,9 +139,8 @@ export function stringifyBranchInnerLoops(
|
|
|
139
139
|
lines.push(`${indent} ${inner.paramUnwrap}`)
|
|
140
140
|
}
|
|
141
141
|
{
|
|
142
|
-
const
|
|
143
|
-
const innerHtml =
|
|
144
|
-
const childPath = isSvg ? '.firstElementChild.firstElementChild' : '.firstElementChild'
|
|
142
|
+
const { wrapTag, childPath } = namespaceWrapForTemplate(inner.wrappedTemplate)
|
|
143
|
+
const innerHtml = wrapTag ? `<${wrapTag}>${inner.wrappedTemplate}</${wrapTag}>` : inner.wrappedTemplate
|
|
145
144
|
lines.push(`${indent} let __bel${uid} = __existing ?? (() => { const __t = document.createElement('template'); __t.innerHTML = \`${innerHtml}\`; return __t.content${childPath}.cloneNode(true) })()`)
|
|
146
145
|
}
|
|
147
146
|
if (inner.wrappedKey) {
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
import { emitRefCall, varSlotId, profileBindingId } from '../../utils.ts'
|
|
33
33
|
import { emitAttrUpdate } from '../../emit-reactive.ts'
|
|
34
34
|
import { stringifyReactiveEffects } from './reactive-effects.ts'
|
|
35
|
-
import { emitTemplateCloneInline, emitLoopItemElementSetup, emitHoistedTemplateDecl, hoistedCloneExpr,
|
|
35
|
+
import { emitTemplateCloneInline, emitLoopItemElementSetup, emitHoistedTemplateDecl, hoistedCloneExpr, namespaceWrapForTemplate, multiRootNamespaceWrapForTemplate, wrapHtmlForNamespace } from './template-parse.ts'
|
|
36
36
|
import { buildSkeletonPathPlan, type SkeletonPathPlan } from './skeleton-paths.ts'
|
|
37
37
|
import { stringifyComponentLoop } from './component-loop.ts'
|
|
38
38
|
import { stringifyCompositeLoop } from './composite-loop.ts'
|
|
@@ -401,17 +401,21 @@ export function stringifyStaticLoop(lines: string[], plan: StaticLoopPlan): void
|
|
|
401
401
|
lines.push(` let __iterEl = ${containerVar}.children[${childIndexExpr}]`)
|
|
402
402
|
if (csrMaterialize) {
|
|
403
403
|
lines.push(` if (!__iterEl) {`)
|
|
404
|
-
// SVG-rooted item templates parse inside a synthetic
|
|
405
|
-
// descend one extra level (#2219) — `template.innerHTML`
|
|
406
|
-
// the HTML namespace and the materialized elements
|
|
407
|
-
//
|
|
408
|
-
//
|
|
409
|
-
//
|
|
410
|
-
//
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
404
|
+
// SVG/MathML-rooted item templates parse inside a synthetic namespace
|
|
405
|
+
// wrap and descend one extra level (#2219, #1096) — `template.innerHTML`
|
|
406
|
+
// alone parses in the HTML namespace and the materialized elements
|
|
407
|
+
// would never draw. Reads the wrap decision AND descent path through
|
|
408
|
+
// the same single door as the reactive clone emitters
|
|
409
|
+
// (`namespaceWrapForTemplate`, #2662 Copilot review) so a future
|
|
410
|
+
// namespace or fragment-rule tweak can't update one path and miss the
|
|
411
|
+
// other; HTML-rooted templates keep byte-identical
|
|
412
|
+
// output. Multi-root fragments use the fragment-aware predicate so a
|
|
413
|
+
// `<svg>`/`<math>`-container-first fragment isn't over-wrapped (#2233
|
|
414
|
+
// Copilot review).
|
|
415
|
+
const { wrapTag, childPath } = csrMaterialize.bodyIsMultiRoot
|
|
416
|
+
? multiRootNamespaceWrapForTemplate(csrMaterialize.itemTemplate)
|
|
417
|
+
: namespaceWrapForTemplate(csrMaterialize.itemTemplate)
|
|
418
|
+
const itemHtml = wrapHtmlForNamespace(csrMaterialize.itemTemplate, wrapTag)
|
|
415
419
|
if (csrMaterialize.bodyIsMultiRoot) {
|
|
416
420
|
// Multi-root: clone every top-level sibling of the per-item template and
|
|
417
421
|
// insert them in order. `__iterEl` is the first root (the one reactive
|
|
@@ -420,7 +424,7 @@ export function stringifyStaticLoop(lines: string[], plan: StaticLoopPlan): void
|
|
|
420
424
|
lines.push(` __mtpl.innerHTML = \`${itemHtml}\``)
|
|
421
425
|
lines.push(` const __anchor = ${containerVar}.children[${childIndexExpr}] ?? null`)
|
|
422
426
|
lines.push(` let __first = null`)
|
|
423
|
-
lines.push(` let __sib = __mtpl.content${
|
|
427
|
+
lines.push(` let __sib = __mtpl.content${childPath}.firstElementChild`)
|
|
424
428
|
lines.push(` while (__sib) {`)
|
|
425
429
|
lines.push(` const __next = __sib.nextElementSibling`)
|
|
426
430
|
lines.push(` const __cloned = __sib.cloneNode(true)`)
|
|
@@ -432,7 +436,7 @@ export function stringifyStaticLoop(lines: string[], plan: StaticLoopPlan): void
|
|
|
432
436
|
} else {
|
|
433
437
|
lines.push(` const __tpl = document.createElement('template')`)
|
|
434
438
|
lines.push(` __tpl.innerHTML = \`${itemHtml}\``)
|
|
435
|
-
lines.push(` const __cloned = __tpl.content${
|
|
439
|
+
lines.push(` const __cloned = __tpl.content${childPath}`)
|
|
436
440
|
lines.push(` if (__cloned) {`)
|
|
437
441
|
lines.push(` const __anchor = ${containerVar}.children[${childIndexExpr}] ?? null`)
|
|
438
442
|
lines.push(` ${containerVar}.insertBefore(__cloned, __anchor)`)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Helpers for emitting code that parses a template literal into a DOM
|
|
3
|
-
* element clone, while preserving the SVG namespace when the loop
|
|
4
|
-
* root is
|
|
3
|
+
* element clone, while preserving the SVG/MathML namespace when the loop
|
|
4
|
+
* body root is a foreign-content element.
|
|
5
5
|
*
|
|
6
6
|
* Background (#135): the standard pattern
|
|
7
7
|
* `const __tpl = document.createElement('template')`
|
|
@@ -14,10 +14,14 @@
|
|
|
14
14
|
* Editor block when a new edge `<path>` was appended via mapArray and
|
|
15
15
|
* never showed up on the canvas.
|
|
16
16
|
*
|
|
17
|
-
* Fix: when the template's root tag is an SVG
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* extra level to get the
|
|
17
|
+
* Fix: when the template's root tag is an SVG (or, #1096, MathML) element,
|
|
18
|
+
* wrap the parsed markup in the matching synthetic namespace root
|
|
19
|
+
* (`<svg>` / `<math>`) so the HTML5 parser walks into foreign content and
|
|
20
|
+
* assigns the correct namespace, then descend one extra level to get the
|
|
21
|
+
* real root. The two namespaces share every byte of this machinery —
|
|
22
|
+
* only the wrap tag name differs — so `detectRootNamespaceWrapTag` and
|
|
23
|
+
* `namespaceWrapForTemplate` below are the single door every emitter
|
|
24
|
+
* (in this file and its consumers) reads the wrap decision through.
|
|
21
25
|
*/
|
|
22
26
|
|
|
23
27
|
import { findInterpolationEnd, findTopLevelTemplateLiterals } from '../../../scanner/js-scanner.ts'
|
|
@@ -37,73 +41,140 @@ const SVG_ROOT_TAGS = new Set([
|
|
|
37
41
|
])
|
|
38
42
|
|
|
39
43
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
+
* MathML root tags (#1096 — port of the SVG fix above). `math` itself is
|
|
45
|
+
* included alongside the element vocabulary, mirroring how `svg` is
|
|
46
|
+
* included in `SVG_ROOT_TAGS`: a template whose bare root is the
|
|
47
|
+
* namespace container tag also needs the parser nudge (unlike the
|
|
48
|
+
* MULTI-ROOT fragment case in `multiRootTemplateNeedsNamespaceWrap`,
|
|
49
|
+
* where a container-first fragment already parses correctly on its own).
|
|
50
|
+
*/
|
|
51
|
+
const MATHML_ROOT_TAGS = new Set([
|
|
52
|
+
'math',
|
|
53
|
+
'mrow', 'mfrac', 'msup', 'msub', 'msubsup', 'mn', 'mi', 'mo', 'mtext',
|
|
54
|
+
'munder', 'mover', 'munderover', 'mtable', 'mtr', 'mtd',
|
|
55
|
+
'msqrt', 'mroot', 'mstyle', 'merror', 'mpadded', 'mphantom', 'menclose',
|
|
56
|
+
'semantics', 'annotation', 'annotation-xml',
|
|
57
|
+
])
|
|
58
|
+
|
|
59
|
+
/** Namespaces whose root tags need the synthetic-wrap parser nudge. */
|
|
60
|
+
export type NamespaceWrapTag = 'svg' | 'math'
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Look up which foreign-content namespace (if any) a single tag name
|
|
64
|
+
* belongs to. Case-insensitive fallback mirrors `templateRootIsSvg`'s
|
|
65
|
+
* original comment: SVG/MathML element names are case-sensitive in JSX
|
|
66
|
+
* (e.g. `linearGradient`, `annotation-xml`) but the canonical lower-case
|
|
67
|
+
* set is checked first, then the lower-cased tag, so both spellings match.
|
|
68
|
+
*/
|
|
69
|
+
function namespaceForTag(tag: string): NamespaceWrapTag | null {
|
|
70
|
+
if (SVG_ROOT_TAGS.has(tag) || SVG_ROOT_TAGS.has(tag.toLowerCase())) return 'svg'
|
|
71
|
+
if (MATHML_ROOT_TAGS.has(tag) || MATHML_ROOT_TAGS.has(tag.toLowerCase())) return 'math'
|
|
72
|
+
return null
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Decide whether a template literal needs foreign-content (SVG or MathML)
|
|
77
|
+
* parsing, and which. Looks at the first opening tag in the literal. The
|
|
78
|
+
* check is purely lexical so that interpolations inside attribute values
|
|
79
|
+
* do not confuse it.
|
|
44
80
|
*
|
|
45
81
|
* Three shapes are recognised:
|
|
46
|
-
* 1. Direct element root — `<circle .../>`
|
|
82
|
+
* 1. Direct element root — `<circle .../>` / `<mrow>...</mrow>`
|
|
47
83
|
* 2. Conditional body (#1088) — `${cond ? `<circle .../>` : `<rect .../>`}`
|
|
48
|
-
* where every result-position template literal
|
|
49
|
-
*
|
|
50
|
-
* bodies; without the wrap the cloned
|
|
51
|
-
* namespace and renders nothing.
|
|
84
|
+
* where every result-position template literal (recursively) resolves
|
|
85
|
+
* to the SAME namespace. The compiler emits this shape for
|
|
86
|
+
* `.map(s => cond ? <a/> : <b/>)` bodies; without the wrap the cloned
|
|
87
|
+
* element ends up in the xhtml namespace and renders nothing.
|
|
52
88
|
* 3. Reactive-conditional body — a branch wrapped in `<!--bf-cond-start:sX-->`
|
|
53
89
|
* / `<!--bf-cond-end:sX-->` markers (emitted for nested reactive
|
|
54
90
|
* conditionals). The check skips leading HTML comments and recurses
|
|
55
91
|
* into the inner `${...}`.
|
|
56
92
|
*
|
|
57
|
-
* Mixed-namespace branches (
|
|
58
|
-
* to no-wrap so the user sees the same broken
|
|
59
|
-
* a silent over-wrap
|
|
93
|
+
* Mixed-namespace branches (HTML+SVG, HTML+MathML, or SVG+MathML)
|
|
94
|
+
* intentionally fall through to no-wrap so the user sees the same broken
|
|
95
|
+
* output as before instead of a silent over-wrap that would drag one
|
|
96
|
+
* branch into the wrong foreign-content namespace.
|
|
60
97
|
*/
|
|
61
|
-
export function
|
|
98
|
+
export function detectRootNamespaceWrapTag(template: string): NamespaceWrapTag | null {
|
|
62
99
|
const stripped = stripLeadingNonContent(template)
|
|
63
100
|
|
|
64
101
|
// Shape 1: direct element root.
|
|
65
102
|
const m = stripped.match(/^<\s*([A-Za-z][A-Za-z0-9-]*)/)
|
|
66
|
-
if (m)
|
|
67
|
-
// SVG element names are case-sensitive in JSX (e.g., `linearGradient`)
|
|
68
|
-
// and arrive lowercased to the renderer for the kebab-cased forms;
|
|
69
|
-
// match case-insensitively against the canonical lower-case set, but
|
|
70
|
-
// keep the canonical name's casing in the lookup table so JSX names
|
|
71
|
-
// like `clipPath` still match.
|
|
72
|
-
const tag = m[1]
|
|
73
|
-
if (SVG_ROOT_TAGS.has(tag)) return true
|
|
74
|
-
return SVG_ROOT_TAGS.has(tag.toLowerCase())
|
|
75
|
-
}
|
|
103
|
+
if (m) return namespaceForTag(m[1])
|
|
76
104
|
|
|
77
105
|
// Shapes 2 & 3: single `${...}` interpolation whose result-position
|
|
78
|
-
// template literals all (recursively) resolve to
|
|
79
|
-
// #1088).
|
|
106
|
+
// template literals all (recursively) resolve to the SAME namespace
|
|
107
|
+
// (Option A in #1088, generalised to MathML in #1096).
|
|
80
108
|
const branches = extractConditionalBranchTemplates(stripped)
|
|
81
|
-
if (branches === null || branches.length === 0) return
|
|
82
|
-
|
|
109
|
+
if (branches === null || branches.length === 0) return null
|
|
110
|
+
let result: NamespaceWrapTag | null | undefined
|
|
111
|
+
for (const branch of branches) {
|
|
112
|
+
const branchNs = detectRootNamespaceWrapTag(branch)
|
|
113
|
+
if (result === undefined) {
|
|
114
|
+
result = branchNs
|
|
115
|
+
} else if (result !== branchNs) {
|
|
116
|
+
return null
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return result ?? null
|
|
83
120
|
}
|
|
84
121
|
|
|
85
122
|
/**
|
|
86
123
|
* Wrap decision for MULTI-ROOT (fragment) templates, where the synthetic
|
|
87
|
-
*
|
|
124
|
+
* namespace wrap swallows every sibling root at once (#2233 Copilot
|
|
125
|
+
* review, generalised to MathML in #1096).
|
|
88
126
|
*
|
|
89
|
-
* `
|
|
90
|
-
* templates that's exact, but a fragment whose first root is
|
|
91
|
-
* CONTAINER (`<><svg/><span/></>`)
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
127
|
+
* `detectRootNamespaceWrapTag` inspects only the FIRST root tag. For
|
|
128
|
+
* single-root templates that's exact, but a fragment whose first root is
|
|
129
|
+
* the namespace CONTAINER itself (`<><svg/><span/></>`, `<><math/><span/></>`)
|
|
130
|
+
* doesn't need the wrap at all — the HTML parser enters foreign content at
|
|
131
|
+
* `<svg>`/`<math>` on its own — and wrapping would drag the HTML siblings
|
|
132
|
+
* into the foreign namespace (`<span>` becomes an SVGUnknownElement/etc.,
|
|
133
|
+
* silently undrawn). So container-first fragments skip the wrap; only
|
|
134
|
+
* leaf-rooted fragments (`<line>`, `<circle>`, `<mrow>`, ...) get it.
|
|
96
135
|
*
|
|
97
|
-
* Known edge (degenerate, pre-existing):
|
|
98
|
-
*
|
|
99
|
-
* in the HTML namespace — exactly the pre-#2219
|
|
100
|
-
* shape correctly needs a scan of every top-level
|
|
101
|
-
* parser until a real component hits it.
|
|
136
|
+
* Known edge (degenerate, pre-existing): a container-first fragment with
|
|
137
|
+
* LEAF siblings of the same namespace (`<><svg/><line/></>`) leaves the
|
|
138
|
+
* bare leaf siblings in the HTML namespace — exactly the pre-#2219
|
|
139
|
+
* behavior. Deciding that shape correctly needs a scan of every top-level
|
|
140
|
+
* root tag; not worth the parser until a real component hits it.
|
|
102
141
|
*/
|
|
103
|
-
export function
|
|
142
|
+
export function multiRootTemplateNeedsNamespaceWrap(template: string): NamespaceWrapTag | null {
|
|
104
143
|
const m = stripLeadingNonContent(template).match(/^<\s*([A-Za-z][A-Za-z0-9-]*)/)
|
|
105
|
-
if (m
|
|
106
|
-
|
|
144
|
+
if (m) {
|
|
145
|
+
const tagLower = m[1].toLowerCase()
|
|
146
|
+
if (tagLower === 'svg' || tagLower === 'math') return null
|
|
147
|
+
}
|
|
148
|
+
return detectRootNamespaceWrapTag(template)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Single door for the "wrap tag + descent path" pair every namespace-aware
|
|
153
|
+
* clone site needs. Consumers outside this file (`inner-loop.ts`,
|
|
154
|
+
* `loop-child-arm.ts`, `loop.ts`) read the wrap decision through this
|
|
155
|
+
* function instead of re-deriving `isSvg ? '<svg>' : ...` locally — keeps
|
|
156
|
+
* the SVG/MathML wrap code path in exactly one place (#1096).
|
|
157
|
+
*/
|
|
158
|
+
export function namespaceWrapForTemplate(template: string): { wrapTag: NamespaceWrapTag | null; childPath: string } {
|
|
159
|
+
const wrapTag = detectRootNamespaceWrapTag(template)
|
|
160
|
+
return {
|
|
161
|
+
wrapTag,
|
|
162
|
+
childPath: wrapTag ? '.firstElementChild.firstElementChild' : '.firstElementChild',
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** `namespaceWrapForTemplate`, but for the multi-root fragment predicate. */
|
|
167
|
+
export function multiRootNamespaceWrapForTemplate(template: string): { wrapTag: NamespaceWrapTag | null; childPath: string } {
|
|
168
|
+
const wrapTag = multiRootTemplateNeedsNamespaceWrap(template)
|
|
169
|
+
return {
|
|
170
|
+
wrapTag,
|
|
171
|
+
childPath: wrapTag ? '.firstElementChild' : '',
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Wrap `html` in the namespace's synthetic root tag, or return it as-is. */
|
|
176
|
+
export function wrapHtmlForNamespace(html: string, wrapTag: NamespaceWrapTag | null): string {
|
|
177
|
+
return wrapTag ? `<${wrapTag}>${html}</${wrapTag}>` : html
|
|
107
178
|
}
|
|
108
179
|
|
|
109
180
|
/**
|
|
@@ -159,14 +230,13 @@ function extractConditionalBranchTemplates(template: string): string[] | null {
|
|
|
159
230
|
*
|
|
160
231
|
* ` const __tpl = document.createElement('template'); __tpl.innerHTML = \`${template}\`; return __tpl.content.firstElementChild.cloneNode(true) `
|
|
161
232
|
*
|
|
162
|
-
* For SVG roots, the `innerHTML` is wrapped in
|
|
163
|
-
* traversal descends one extra level.
|
|
233
|
+
* For SVG/MathML roots, the `innerHTML` is wrapped in the matching
|
|
234
|
+
* namespace root tag and the traversal descends one extra level.
|
|
164
235
|
*/
|
|
165
236
|
export function emitTemplateCloneInline(template: string): string {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
169
|
-
return `const __tpl = document.createElement('template'); __tpl.innerHTML = \`${template}\`; return __tpl.content.firstElementChild.cloneNode(true)`
|
|
237
|
+
const { wrapTag, childPath } = namespaceWrapForTemplate(template)
|
|
238
|
+
const html = wrapHtmlForNamespace(template, wrapTag)
|
|
239
|
+
return `const __tpl = document.createElement('template'); __tpl.innerHTML = \`${html}\`; return __tpl.content${childPath}.cloneNode(true)`
|
|
170
240
|
}
|
|
171
241
|
|
|
172
242
|
/**
|
|
@@ -177,13 +247,14 @@ export function emitTemplateCloneInline(template: string): string {
|
|
|
177
247
|
* STATIC-ONLY skeleton produced by `buildLoopSkeletonTemplate` (dynamic attrs
|
|
178
248
|
* omitted, text markers empty) — never the per-row interpolated `template`.
|
|
179
249
|
*
|
|
180
|
-
*
|
|
181
|
-
* `
|
|
182
|
-
* the interpolated template, so the same wrap decision
|
|
250
|
+
* Namespace wrap mirrors `emitTemplateCloneLines` (#135 / #1088 / #1096):
|
|
251
|
+
* `detectRootNamespaceWrapTag` is re-checked against the skeleton (same
|
|
252
|
+
* root tag as the interpolated template, so the same wrap decision
|
|
253
|
+
* applies).
|
|
183
254
|
*/
|
|
184
255
|
export function emitHoistedTemplateDecl(lines: string[], indent: string, tplVar: string, skeletonTemplate: string): void {
|
|
185
|
-
const
|
|
186
|
-
const html =
|
|
256
|
+
const { wrapTag } = namespaceWrapForTemplate(skeletonTemplate)
|
|
257
|
+
const html = wrapHtmlForNamespace(skeletonTemplate, wrapTag)
|
|
187
258
|
lines.push(`${indent}const ${tplVar} = document.createElement('template')`)
|
|
188
259
|
lines.push(`${indent}${tplVar}.innerHTML = \`${html}\``)
|
|
189
260
|
}
|
|
@@ -194,9 +265,7 @@ export function emitHoistedTemplateDecl(lines: string[], indent: string, tplVar:
|
|
|
194
265
|
* `emitTemplateCloneInline` / `emitTemplateCloneLines` parse-and-clone.
|
|
195
266
|
*/
|
|
196
267
|
export function hoistedCloneExpr(tplVar: string, skeletonTemplate: string): string {
|
|
197
|
-
return
|
|
198
|
-
? `${tplVar}.content.firstElementChild.firstElementChild.cloneNode(true)`
|
|
199
|
-
: `${tplVar}.content.firstElementChild.cloneNode(true)`
|
|
268
|
+
return `${tplVar}.content${namespaceWrapForTemplate(skeletonTemplate).childPath}.cloneNode(true)`
|
|
200
269
|
}
|
|
201
270
|
|
|
202
271
|
/**
|
|
@@ -204,17 +273,12 @@ export function hoistedCloneExpr(tplVar: string, skeletonTemplate: string): stri
|
|
|
204
273
|
* Returns three statements with no trailing newlines.
|
|
205
274
|
*/
|
|
206
275
|
export function emitTemplateCloneLines(template: string, indent: string): string[] {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
`${indent}const __tpl = document.createElement('template')`,
|
|
210
|
-
`${indent}__tpl.innerHTML = \`<svg>${template}</svg>\``,
|
|
211
|
-
`${indent}return __tpl.content.firstElementChild.firstElementChild.cloneNode(true)`,
|
|
212
|
-
]
|
|
213
|
-
}
|
|
276
|
+
const { wrapTag, childPath } = namespaceWrapForTemplate(template)
|
|
277
|
+
const html = wrapHtmlForNamespace(template, wrapTag)
|
|
214
278
|
return [
|
|
215
279
|
`${indent}const __tpl = document.createElement('template')`,
|
|
216
|
-
`${indent}__tpl.innerHTML = \`${
|
|
217
|
-
`${indent}return __tpl.content.
|
|
280
|
+
`${indent}__tpl.innerHTML = \`${html}\``,
|
|
281
|
+
`${indent}return __tpl.content${childPath}.cloneNode(true)`,
|
|
218
282
|
]
|
|
219
283
|
}
|
|
220
284
|
|
|
@@ -312,13 +376,13 @@ export function emitMultiRootTemplateCloneLines(
|
|
|
312
376
|
varEl: string,
|
|
313
377
|
varExtras: string,
|
|
314
378
|
): string[] {
|
|
315
|
-
const
|
|
316
|
-
// Wrap in
|
|
317
|
-
// descend one level to pick up the per-item roots.
|
|
318
|
-
const innerHtmlExpr =
|
|
379
|
+
const { wrapTag, childPath } = multiRootNamespaceWrapForTemplate(template)
|
|
380
|
+
// Wrap in the namespace's root tag so the parser walks into foreign
|
|
381
|
+
// content; we then descend one level to pick up the per-item roots.
|
|
382
|
+
const innerHtmlExpr = `\`${wrapHtmlForNamespace(template, wrapTag)}\``
|
|
319
383
|
// `parent` is the element whose direct children are the per-item roots
|
|
320
|
-
// (the
|
|
321
|
-
const parentExpr =
|
|
384
|
+
// (the namespace wrap for SVG/MathML, the template's content for HTML).
|
|
385
|
+
const parentExpr = `__tpl.content${childPath}`
|
|
322
386
|
return [
|
|
323
387
|
`${indent}const __tpl = document.createElement('template')`,
|
|
324
388
|
`${indent}__tpl.innerHTML = ${innerHtmlExpr}`,
|
|
@@ -203,7 +203,11 @@ export function emitRegistrationAndHydration(
|
|
|
203
203
|
// Build ComponentDef object for hydrate()
|
|
204
204
|
const defParts: string[] = [`init: init${name}`]
|
|
205
205
|
if (canGenerateStaticTemplate(_ir.root, propNamesForStaticCheck, inlinableConstants, unsafeLocalNames)) {
|
|
206
|
-
|
|
206
|
+
// `ctx.dynamicElements` is the claim-plan-'markup' membership
|
|
207
|
+
// (#2651) — see `generateCsrTemplate`'s identical derivation below
|
|
208
|
+
// for why this is reused as-is rather than re-derived.
|
|
209
|
+
const markupSlotIds = new Set(ctx.dynamicElements.map(e => e.slotId))
|
|
210
|
+
const templateHtml = irToComponentTemplate(_ir.root, inlinableConstants, restSpreadNames, ctx.propsObjectName, markupSlotIds)
|
|
207
211
|
if (templateHtml) {
|
|
208
212
|
defParts.push(buildTemplateDefPart(ctx, templateHtml))
|
|
209
213
|
}
|
|
@@ -321,9 +321,22 @@ function escapeAttrValueExpr(valExpr: string): string {
|
|
|
321
321
|
* escaped, so this is applied only at the four text-marker emit sites.
|
|
322
322
|
* Hono escapes text content with the same set as attribute values
|
|
323
323
|
* (`& " ' < >`), so `escapeText` delegates to the same operation.
|
|
324
|
+
*
|
|
325
|
+
* `isMarkup` (#2651) switches to `escapeTextOrMarkup` — `escapeText`'s
|
|
326
|
+
* strict superset that additionally unwraps a `bfMarkup()`-branded value
|
|
327
|
+
* raw instead of escaping it. Two of the four text-marker call sites
|
|
328
|
+
* (`irToComponentTemplateWithOpts`, `generateCsrTemplateWithOpts`) pass
|
|
329
|
+
* `true` for a slot `ctx.dynamicElements` also claims — the same
|
|
330
|
+
* membership `emit-reactive.ts` uses to pick the 'markup' writer kind for
|
|
331
|
+
* this slot's REACTIVE update, so the initial-render escape and the
|
|
332
|
+
* reactive-update escape agree on whether the slot may carry raw markup.
|
|
333
|
+
* The other two (`irToHtmlTemplate`, `irToPlaceholderTemplate`) build
|
|
334
|
+
* loop-item / conditional-branch HTML, which the reactive side always
|
|
335
|
+
* treats as plain text (`__bfText`) regardless — they never pass `true`,
|
|
336
|
+
* so they keep calling `escapeText` byte-for-byte as before.
|
|
324
337
|
*/
|
|
325
|
-
function escapeTextSlotExpr(innerExpr: string): string {
|
|
326
|
-
return
|
|
338
|
+
function escapeTextSlotExpr(innerExpr: string, isMarkup = false): string {
|
|
339
|
+
return `${isMarkup ? 'escapeTextOrMarkup' : 'escapeText'}(${innerExpr})`
|
|
327
340
|
}
|
|
328
341
|
|
|
329
342
|
/**
|
|
@@ -901,7 +914,20 @@ export function irToHtmlTemplate(node: IRNode, restSpreadNames?: Set<string>, lo
|
|
|
901
914
|
case 'jsx-children': {
|
|
902
915
|
const hoistedRecurse = (n: IRNode): string => irToHtmlTemplate(n, restSpreadNames, loopDepth, loopParams, branchSlotsVar, true)
|
|
903
916
|
const childHtml = p.value.children.map(c => hoistedRecurse(c)).join('')
|
|
904
|
-
|
|
917
|
+
// Brand the assembled HTML (#2651) — every text segment inside
|
|
918
|
+
// `childHtml` was already escaped node-by-node during
|
|
919
|
+
// `hoistedRecurse`'s own build, so the concatenated result is
|
|
920
|
+
// safe to splice raw; the child's own template evaluates this
|
|
921
|
+
// prop through `escapeTextOrMarkup`/`escapeTextOrNode`, which
|
|
922
|
+
// trust the brand and skip re-escaping it. EXCEPT an explicit
|
|
923
|
+
// `children={<jsx/>}` prop (out of scope, unchanged): the
|
|
924
|
+
// child's `{children}` interpolation is the bare-passthrough
|
|
925
|
+
// door (`escapeTextSlotExpr`'s own docstring), never routed
|
|
926
|
+
// through an escape/unwrap call, so a branded object here
|
|
927
|
+
// would stringify to `[object Object]` instead of unwrapping.
|
|
928
|
+
return p.name === 'children'
|
|
929
|
+
? `${quotePropName(p.name)}: \`${childHtml}\``
|
|
930
|
+
: `${quotePropName(p.name)}: bfMarkup(\`${childHtml}\`)`
|
|
905
931
|
}
|
|
906
932
|
case 'literal':
|
|
907
933
|
return `${quotePropName(p.name)}: ${JSON.stringify(p.value.value)}`
|
|
@@ -1478,6 +1504,25 @@ export function irChildrenToJsExpr(children: IRNode[]): string {
|
|
|
1478
1504
|
return `[${exprs.join(', ')}]`
|
|
1479
1505
|
}
|
|
1480
1506
|
|
|
1507
|
+
/**
|
|
1508
|
+
* Narrow gate for branding a nested `jsx-children` getter's value with
|
|
1509
|
+
* `bfMarkup()` (#2651) — the `irNodeToJsExprs` counterpart of
|
|
1510
|
+
* `collect-elements.ts`'s `isSingleElementJsxChildren` (duplicated rather
|
|
1511
|
+
* than imported: `collect-elements.ts` already imports FROM this file, so
|
|
1512
|
+
* the reverse import would cycle). Same shape, same reasoning: a
|
|
1513
|
+
* `jsx-children` prop's value is always a single stored node
|
|
1514
|
+
* (`AttrValueOf.jsxChildren([...])`); only when that node is a lone
|
|
1515
|
+
* `'element'` does `irChildrenToJsExpr` reduce it to ONE HTML-string
|
|
1516
|
+
* template literal — the shape `bfMarkup` + `escapeTextOrNode` /
|
|
1517
|
+
* `escapeTextOrMarkup` have a proven contract for. A `'fragment'` (multiple
|
|
1518
|
+
* children) or `'conditional'` node reduces to an array literal or a
|
|
1519
|
+
* nested ternary instead — left unbranded here for the same reason as the
|
|
1520
|
+
* `collect-elements.ts` twin.
|
|
1521
|
+
*/
|
|
1522
|
+
function isSingleElementJsxChildren(nodes: IRNode[]): boolean {
|
|
1523
|
+
return nodes.length === 1 && nodes[0].type === 'element'
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1481
1526
|
function irNodeToJsExprs(node: IRNode): string[] {
|
|
1482
1527
|
switch (node.type) {
|
|
1483
1528
|
case 'component': {
|
|
@@ -1488,8 +1533,19 @@ function irNodeToJsExprs(node: IRNode): string[] {
|
|
|
1488
1533
|
return `${quotePropName(p.name)}: ${attrValueToString(p.value) ?? 'undefined'}`
|
|
1489
1534
|
}
|
|
1490
1535
|
switch (p.value.kind) {
|
|
1491
|
-
case 'jsx-children':
|
|
1492
|
-
|
|
1536
|
+
case 'jsx-children': {
|
|
1537
|
+
// Brand (#2651) only the lone-'element' shape, and never an
|
|
1538
|
+
// explicit `children={<jsx/>}` prop — see
|
|
1539
|
+
// `isSingleElementJsxChildren` (this file) for the shape
|
|
1540
|
+
// argument and the `collect-elements.ts` twin for the
|
|
1541
|
+
// `children`-exclusion argument (bare-passthrough consumer,
|
|
1542
|
+
// no unwrap call).
|
|
1543
|
+
const jsxExpr = irChildrenToJsExpr(p.value.children)
|
|
1544
|
+
const wrapped = (p.name !== 'children' && isSingleElementJsxChildren(p.value.children))
|
|
1545
|
+
? `bfMarkup(${jsxExpr})`
|
|
1546
|
+
: jsxExpr
|
|
1547
|
+
return `get ${quotePropName(p.name)}() { return ${wrapped} }`
|
|
1548
|
+
}
|
|
1493
1549
|
case 'literal':
|
|
1494
1550
|
return `get ${quotePropName(p.name)}() { return ${JSON.stringify(p.value.value)} }`
|
|
1495
1551
|
case 'boolean-shorthand':
|
|
@@ -1644,6 +1700,20 @@ export interface TemplateOptions {
|
|
|
1644
1700
|
* the template phase agree on which children defer (dropped-prop fix).
|
|
1645
1701
|
*/
|
|
1646
1702
|
deferredChildSlots?: ReadonlySet<string>
|
|
1703
|
+
/**
|
|
1704
|
+
* Slot ids of top-level, non-conditional dynamic expressions
|
|
1705
|
+
* (`ctx.dynamicElements`, populated by `collectElements` — the exact set
|
|
1706
|
+
* `emit-reactive.ts`'s `emitDynamicTextUpdates` claims `kind: 'markup'`
|
|
1707
|
+
* for on the REACTIVE side, #2651). When a text-marker expression's
|
|
1708
|
+
* `slotId` is a member, `escapeTextSlotExpr` emits `escapeTextOrMarkup`
|
|
1709
|
+
* instead of `escapeText` so a `bfMarkup()`-branded prop value (a JSX
|
|
1710
|
+
* element passed at a non-`children` component prop position) reaches
|
|
1711
|
+
* the initial-render template raw, matching what the reactive writer
|
|
1712
|
+
* already does via `escapeTextOrNode`. Computed once per component from
|
|
1713
|
+
* `ctx.dynamicElements` by `irToComponentTemplate` / `generateCsrTemplate`
|
|
1714
|
+
* — never re-derived here.
|
|
1715
|
+
*/
|
|
1716
|
+
markupSlotIds?: ReadonlySet<string>
|
|
1647
1717
|
}
|
|
1648
1718
|
|
|
1649
1719
|
/**
|
|
@@ -1659,9 +1729,10 @@ export function irToComponentTemplate(
|
|
|
1659
1729
|
node: IRNode,
|
|
1660
1730
|
inlinableConstants?: Map<string, string>,
|
|
1661
1731
|
restSpreadNames?: Set<string>,
|
|
1662
|
-
propsObjectName?: string | null
|
|
1732
|
+
propsObjectName?: string | null,
|
|
1733
|
+
markupSlotIds?: ReadonlySet<string>
|
|
1663
1734
|
): string {
|
|
1664
|
-
return irToComponentTemplateWithOpts(node, { inlinableConstants, restSpreadNames, propsObjectName, loopDepth: -1 })
|
|
1735
|
+
return irToComponentTemplateWithOpts(node, { inlinableConstants, restSpreadNames, propsObjectName, loopDepth: -1, markupSlotIds })
|
|
1665
1736
|
}
|
|
1666
1737
|
|
|
1667
1738
|
function irToComponentTemplateWithOpts(node: IRNode, opts: TemplateOptions): string {
|
|
@@ -1829,7 +1900,8 @@ function irToComponentTemplateWithOpts(node: IRNode, opts: TemplateOptions): str
|
|
|
1829
1900
|
? `Array.isArray(${wrapped}) ? ${wrapped}.join('') : (${wrapped} ?? '')`
|
|
1830
1901
|
: wrapped
|
|
1831
1902
|
if (node.slotId) {
|
|
1832
|
-
|
|
1903
|
+
const isMarkup = opts.markupSlotIds?.has(node.slotId) ?? false
|
|
1904
|
+
return `<!--bf:${node.slotId}-->\${${node.joinArrayChild ? value : escapeTextSlotExpr(wrapped, isMarkup)}}<!--/-->`
|
|
1833
1905
|
}
|
|
1834
1906
|
return `\${${value}}`
|
|
1835
1907
|
}
|
|
@@ -1873,7 +1945,12 @@ function irToComponentTemplateWithOpts(node: IRNode, opts: TemplateOptions): str
|
|
|
1873
1945
|
case 'jsx-children': {
|
|
1874
1946
|
const hoistedRecurse = (n: IRNode): string => irToComponentTemplateWithOpts(n, { ...opts, inHoistedChildren: true })
|
|
1875
1947
|
const childHtml = p.value.children.map(c => hoistedRecurse(c)).join('')
|
|
1876
|
-
|
|
1948
|
+
// Brand the assembled HTML (#2651), except an explicit
|
|
1949
|
+
// `children={<jsx/>}` prop — see the identical
|
|
1950
|
+
// `irToHtmlTemplate` case above for both arguments.
|
|
1951
|
+
return p.name === 'children'
|
|
1952
|
+
? `${quotePropName(p.name)}: \`${childHtml}\``
|
|
1953
|
+
: `${quotePropName(p.name)}: bfMarkup(\`${childHtml}\`)`
|
|
1877
1954
|
}
|
|
1878
1955
|
case 'literal':
|
|
1879
1956
|
return `${quotePropName(p.name)}: ${JSON.stringify(p.value.value)}`
|
|
@@ -2075,7 +2152,14 @@ export function generateCsrTemplate(
|
|
|
2075
2152
|
}
|
|
2076
2153
|
}
|
|
2077
2154
|
const effectiveUnsafeLocalNames = mergeCsrNullUnsafe(ctx, unsafeLocalNames)
|
|
2078
|
-
|
|
2155
|
+
// `ctx.dynamicElements` (populated by `collectElements`, before any
|
|
2156
|
+
// template-string build runs) IS the claim-plan-'markup' membership
|
|
2157
|
+
// `emit-reactive.ts` claims for these same slot ids on the reactive
|
|
2158
|
+
// side (#2651) — every entry is already non-conditional / non-`@client`
|
|
2159
|
+
// by construction (`collectElements`'s `expression` visitor only pushes
|
|
2160
|
+
// here). Reused as-is, not re-derived.
|
|
2161
|
+
const markupSlotIds = new Set(ctx.dynamicElements.map(e => e.slotId))
|
|
2162
|
+
return generateCsrTemplateWithOpts(node, { inlinableConstants, restSpreadNames, propsObjectName, csrEnv, unsafeLocalNames: effectiveUnsafeLocalNames, deferredChildSlots, loopDepth: -1, markupSlotIds })
|
|
2079
2163
|
}
|
|
2080
2164
|
|
|
2081
2165
|
/**
|
|
@@ -2440,7 +2524,8 @@ function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): strin
|
|
|
2440
2524
|
? `Array.isArray(${expr}) ? ${expr}.join('') : (${expr} ?? '')`
|
|
2441
2525
|
: expr
|
|
2442
2526
|
if (node.slotId) {
|
|
2443
|
-
|
|
2527
|
+
const isMarkup = opts.markupSlotIds?.has(node.slotId) ?? false
|
|
2528
|
+
return `<!--bf:${node.slotId}-->\${${node.joinArrayChild ? value : escapeTextSlotExpr(expr, isMarkup)}}<!--/-->`
|
|
2444
2529
|
}
|
|
2445
2530
|
return `\${${value}}`
|
|
2446
2531
|
}
|
|
@@ -2499,7 +2584,13 @@ function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): strin
|
|
|
2499
2584
|
case 'jsx-children': {
|
|
2500
2585
|
const hoistedRecurse = (n: IRNode): string => generateCsrTemplateWithOpts(n, { ...opts, inHoistedChildren: true })
|
|
2501
2586
|
const childHtml = p.value.children.map(c => hoistedRecurse(c)).join('')
|
|
2502
|
-
|
|
2587
|
+
// Brand the assembled HTML (#2651), except an explicit
|
|
2588
|
+
// `children={<jsx/>}` prop — see the identical
|
|
2589
|
+
// `irToHtmlTemplate` case (top of this file) for both
|
|
2590
|
+
// arguments.
|
|
2591
|
+
return p.name === 'children'
|
|
2592
|
+
? `${quotePropName(p.name)}: \`${childHtml}\``
|
|
2593
|
+
: `${quotePropName(p.name)}: bfMarkup(\`${childHtml}\`)`
|
|
2503
2594
|
}
|
|
2504
2595
|
case 'literal':
|
|
2505
2596
|
return `${quotePropName(p.name)}: ${JSON.stringify(p.value.value)}`
|
|
@@ -20,6 +20,10 @@ export const RUNTIME_IMPORT_CANDIDATES = [
|
|
|
20
20
|
'createPortal',
|
|
21
21
|
'provideContext', 'createContext', 'useContext',
|
|
22
22
|
'forwardProps', 'applyRestAttrs', 'splitProps', 'spreadAttrs', 'styleToCss', 'escapeAttr', 'escapeText', 'escapeTextOrNode',
|
|
23
|
+
// JSX-element-as-non-children-prop markup brand (#2651) — `bfMarkup` wraps
|
|
24
|
+
// the compiler-built HTML at the producer (renderChild / initChild props);
|
|
25
|
+
// `escapeTextOrMarkup` unwraps it at the claim-plan-'markup' template slot.
|
|
26
|
+
'bfMarkup', 'escapeTextOrMarkup',
|
|
23
27
|
'qsa', 'qsaItem', 'qsaChildScope', 'qsaChildScopes', 'upsertChildItem', '__slot', '__bfSlot', '__bfText',
|
|
24
28
|
// Claim-plan interpreter (slot unification A2/A3, spec/slot-unification.md)
|
|
25
29
|
// — the "one claim mechanism" that replaced `patchSlotRange` and
|
|
@@ -259,7 +259,12 @@ function generateTemplateOnlyMount(ir: ComponentIR, ctx: ClientJsContext): strin
|
|
|
259
259
|
let templateHtml: string | undefined
|
|
260
260
|
|
|
261
261
|
if (canGenerateStaticTemplate(ir.root, propNamesForStaticCheck, inlinableConstants, unsafeLocalNames)) {
|
|
262
|
-
|
|
262
|
+
// `ctx.dynamicElements` is always empty on this template-only path
|
|
263
|
+
// (`needsClientJs(ctx)` gates it) — passed through anyway so this
|
|
264
|
+
// matches `emitRegistrationAndHydration`'s derivation byte-for-byte
|
|
265
|
+
// rather than special-casing "no markup slots here" (#2651).
|
|
266
|
+
const markupSlotIds = new Set(ctx.dynamicElements.map(e => e.slotId))
|
|
267
|
+
templateHtml = irToComponentTemplate(ir.root, inlinableConstants, restSpreadNames, ctx.propsObjectName, markupSlotIds)
|
|
263
268
|
}
|
|
264
269
|
|
|
265
270
|
// CSR fallback: when static template generation fails (e.g., components with
|