@barefootjs/xslate 0.18.4 → 0.18.7
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 +131 -19
- package/dist/adapter/lib/constants.d.ts.map +1 -1
- package/dist/adapter/lib/static-value.d.ts +18 -0
- package/dist/adapter/lib/static-value.d.ts.map +1 -0
- package/dist/adapter/props/prop-classes.d.ts +25 -7
- package/dist/adapter/props/prop-classes.d.ts.map +1 -1
- package/dist/adapter/xslate-adapter.d.ts +49 -0
- package/dist/adapter/xslate-adapter.d.ts.map +1 -1
- package/dist/build.js +131 -19
- package/dist/conformance-pins.d.ts.map +1 -1
- package/dist/index.js +134 -39
- package/dist/render-divergences.d.ts.map +1 -1
- package/lib/BarefootJS/Backend/Xslate.pm +1 -1
- package/package.json +3 -3
- package/src/__tests__/xslate-adapter.test.ts +139 -0
- package/src/adapter/expr/array-method.ts +19 -0
- package/src/adapter/expr/emitters.ts +2 -2
- package/src/adapter/lib/constants.ts +3 -0
- package/src/adapter/lib/static-value.ts +44 -0
- package/src/adapter/props/prop-classes.ts +29 -7
- package/src/adapter/xslate-adapter.ts +178 -15
- package/src/conformance-pins.ts +26 -33
- package/src/render-divergences.ts +4 -16
- package/src/test-render.ts +13 -139
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serialize a compile-time-evaluated JS value (`@barefootjs/jsx`'s
|
|
3
|
+
* `evaluateStaticLiteral`/`resolveStaticLoopSource`, #2208) into a native
|
|
4
|
+
* Text::Xslate (Kolon) literal. Used to inline a fully-static loop source
|
|
5
|
+
* (an inline array literal, or a function-scope local const with a static
|
|
6
|
+
* initializer) directly in a `for EXPR -> $item { ... }` header, rather
|
|
7
|
+
* than requiring a bound template variable.
|
|
8
|
+
*
|
|
9
|
+
* Booleans deliberately return `null` (defer to the caller's BF101
|
|
10
|
+
* refusal) rather than baking a `1`/absent-value stand-in — Kolon has no
|
|
11
|
+
* native boolean literal in this position, and guessing one would diverge
|
|
12
|
+
* from JS's `String(true) === "true"` at render.
|
|
13
|
+
*
|
|
14
|
+
* Returns `null` for a value this adapter can't represent as a literal —
|
|
15
|
+
* the caller falls back to its existing BF101 refusal instead of guessing.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { escapeKolonSingleQuoted, kolonHashKey } from './kolon-naming.ts'
|
|
19
|
+
|
|
20
|
+
export function staticValueToKolon(value: unknown): string | null {
|
|
21
|
+
if (value === null || value === undefined) return 'nil'
|
|
22
|
+
if (typeof value === 'boolean') return null
|
|
23
|
+
if (typeof value === 'number') return String(value)
|
|
24
|
+
if (typeof value === 'string') return `'${escapeKolonSingleQuoted(value)}'`
|
|
25
|
+
if (Array.isArray(value)) {
|
|
26
|
+
const items: string[] = []
|
|
27
|
+
for (const el of value) {
|
|
28
|
+
const serialized = staticValueToKolon(el)
|
|
29
|
+
if (serialized === null) return null
|
|
30
|
+
items.push(serialized)
|
|
31
|
+
}
|
|
32
|
+
return `[${items.join(', ')}]`
|
|
33
|
+
}
|
|
34
|
+
if (typeof value === 'object') {
|
|
35
|
+
const entries: string[] = []
|
|
36
|
+
for (const [key, val] of Object.entries(value as Record<string, unknown>)) {
|
|
37
|
+
const serialized = staticValueToKolon(val)
|
|
38
|
+
if (serialized === null) return null
|
|
39
|
+
entries.push(`${kolonHashKey(key)} => ${serialized}`)
|
|
40
|
+
}
|
|
41
|
+
return `{ ${entries.join(', ')} }`
|
|
42
|
+
}
|
|
43
|
+
return null
|
|
44
|
+
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* adapter's `props/prop-types.ts`. No adapter instance state.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import type
|
|
10
|
+
import { collectLoopBoundNames, type ComponentIR } from '@barefootjs/jsx'
|
|
11
11
|
import { isStringTypeInfo, isBareStringLiteral } from '../value/parsed-literal.ts'
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -43,12 +43,30 @@ export function collectNullableOptionalProps(ir: ComponentIR): Set<string> {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
|
-
* String-typed signals
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
46
|
+
* String-typed signals, props, and same-file local consts (#2212). A
|
|
47
|
+
* signal is string-typed when its inferred type is `string` (or,
|
|
48
|
+
* defensively, when its initial value is a bare string literal); a prop
|
|
49
|
+
* when its annotated type is `string`; a local const the same way. Consumed
|
|
50
|
+
* by `isStringConcatBinary`/`isStringTypedOperand` (`@barefootjs/jsx`) to
|
|
51
|
+
* pick Kolon's `~` over JS `+`'s numeric fallback (#2163, #2212) —
|
|
52
|
+
* including now for a bare identifier operand, not just a prop/getter/
|
|
53
|
+
* literal. In the Mojo adapter this ALSO drives `eq`/`ne` selection for
|
|
54
|
+
* string equality; the Kolon emitters don't consume that distinction
|
|
55
|
+
* (Kolon's `==`/`!=` compare strings and numbers correctly), so that half
|
|
56
|
+
* of this set is carried only for parity with the Mojo adapter.
|
|
57
|
+
*
|
|
58
|
+
* Excludes any name bound as a `.map()`/`.filter()` loop callback's item
|
|
59
|
+
* or index parameter ANYWHERE in the component (Fable review, #2212): the
|
|
60
|
+
* lookup below is a flat, scope-blind `Set<string>` with no notion of a
|
|
61
|
+
* loop param shadowing an outer string-typed binding of the same name
|
|
62
|
+
* (`items.map((name) => 1 + name)` inside a component that also has a
|
|
63
|
+
* string `name` prop) — left unguarded, that shadowed `name` would be
|
|
64
|
+
* misdetected as string-typed and `1 + name` would silently lower to `~`
|
|
65
|
+
* instead of staying numeric `+`. Subtracting loop-bound names is coarse
|
|
66
|
+
* (it also suppresses a genuinely non-shadowed same-named string
|
|
67
|
+
* elsewhere in the component) but safe: the suppressed case just falls
|
|
68
|
+
* back to today's numeric `+` — the same, already-accepted residual as an
|
|
69
|
+
* unresolvable operand — never silently-wrong output.
|
|
52
70
|
*/
|
|
53
71
|
export function collectStringValueNames(ir: ComponentIR): Set<string> {
|
|
54
72
|
const names = new Set<string>()
|
|
@@ -60,5 +78,9 @@ export function collectStringValueNames(ir: ComponentIR): Set<string> {
|
|
|
60
78
|
for (const p of ir.metadata.propsParams) {
|
|
61
79
|
if (isStringTypeInfo(p.type)) names.add(p.name)
|
|
62
80
|
}
|
|
81
|
+
for (const c of ir.metadata.localConstants) {
|
|
82
|
+
if (isStringTypeInfo(c.type ?? undefined) || isBareStringLiteral(c.value)) names.add(c.name)
|
|
83
|
+
}
|
|
84
|
+
for (const bound of collectLoopBoundNames(ir)) names.delete(bound)
|
|
63
85
|
return names
|
|
64
86
|
}
|
|
@@ -73,11 +73,17 @@ import {
|
|
|
73
73
|
isValidHelperId,
|
|
74
74
|
sortComparatorFromArrow,
|
|
75
75
|
isLowerableLoopDestructure,
|
|
76
|
+
isDangerousInnerHtmlAttr,
|
|
77
|
+
resolveDangerousInnerHtml,
|
|
78
|
+
dangerousInnerHtmlMetacharViolation,
|
|
79
|
+
dangerousInnerHtmlDiagnostic,
|
|
80
|
+
resolveStaticLoopSource,
|
|
81
|
+
collectLoopBoundNames,
|
|
76
82
|
} from '@barefootjs/jsx'
|
|
77
83
|
import { isAriaBooleanAttr, isBooleanResultExpr } from './boolean-result.ts'
|
|
78
84
|
import ts from 'typescript'
|
|
79
85
|
import type { ParsedExpr, LoweringMatcher } from '@barefootjs/jsx'
|
|
80
|
-
import { BF_SLOT, BF_COND, BF_REGION } from '@barefootjs/shared'
|
|
86
|
+
import { BF_SLOT, BF_COND, BF_REGION, escapeHtml } from '@barefootjs/shared'
|
|
81
87
|
|
|
82
88
|
import type { XslateRenderCtx } from './lib/types.ts'
|
|
83
89
|
import { XSLATE_PRIMITIVE_EMIT_MAP } from './lib/constants.ts'
|
|
@@ -87,6 +93,7 @@ import {
|
|
|
87
93
|
collectRootScopeNodes,
|
|
88
94
|
} from './lib/ir-scope.ts'
|
|
89
95
|
import { renderSortMethod, renderSortEval } from './expr/array-method.ts'
|
|
96
|
+
import { staticValueToKolon } from './lib/static-value.ts'
|
|
90
97
|
import { XslateFilterEmitter, XslateTopLevelEmitter } from './expr/emitters.ts'
|
|
91
98
|
import type { XslateEmitContext, XslateSpreadContext, XslateMemoContext } from './emit-context.ts'
|
|
92
99
|
import {
|
|
@@ -176,6 +183,14 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
176
183
|
private options: Required<XslateAdapterOptions>
|
|
177
184
|
private errors: CompilerError[] = []
|
|
178
185
|
private inLoop: boolean = false
|
|
186
|
+
/**
|
|
187
|
+
* `IRLoop.depth` of the loop currently being rendered (save/restore
|
|
188
|
+
* around `renderChildren(loop.children)`, mirroring `inLoop` above).
|
|
189
|
+
* `renderAttributes` reads this to derive the `key` → `data-key`/
|
|
190
|
+
* `data-key-N` suffix — the depth is IR-computed (jsx-to-ir.ts), not
|
|
191
|
+
* re-derived here (#2168 nested-loop-outer-binding).
|
|
192
|
+
*/
|
|
193
|
+
private currentLoopKeyDepth = 0
|
|
179
194
|
/**
|
|
180
195
|
* SolidJS-style props identifier (`function(props: P)`) and the
|
|
181
196
|
* analyzer-extracted prop names. Stashed at `generate()` entry so the
|
|
@@ -232,6 +247,17 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
232
247
|
*/
|
|
233
248
|
private localConstants: IRMetadata['localConstants'] = []
|
|
234
249
|
|
|
250
|
+
/**
|
|
251
|
+
* Every name a `.map()`/`.filter()` loop callback binds as its item/index
|
|
252
|
+
* parameter anywhere in the component (#2208 fable review). A static
|
|
253
|
+
* loop-SOURCE name (e.g. a function-scope `const items = [...]`) must
|
|
254
|
+
* never resolve through `resolveStaticLoopSource` at a use site where a
|
|
255
|
+
* DIFFERENT, enclosing loop's own callback param shadows it — same
|
|
256
|
+
* shadowing hazard, and same coarse-but-safe mitigation, as #2212's
|
|
257
|
+
* `collectLoopBoundNames` use in `collectStringValueNames`.
|
|
258
|
+
*/
|
|
259
|
+
private staticLoopSourceBoundNames: Set<string> = new Set()
|
|
260
|
+
|
|
235
261
|
/**
|
|
236
262
|
* Optional, no-default props that are `undef` when the caller omits them.
|
|
237
263
|
* Their bare-reference attribute emission is guarded with Kolon `defined` so
|
|
@@ -264,6 +290,7 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
264
290
|
// Per-compile prop classifications (see `props/prop-classes.ts`).
|
|
265
291
|
this.booleanTypedProps = collectBooleanTypedProps(ir)
|
|
266
292
|
this.localConstants = ir.metadata.localConstants ?? []
|
|
293
|
+
this.staticLoopSourceBoundNames = collectLoopBoundNames(ir)
|
|
267
294
|
this.nullableOptionalProps = collectNullableOptionalProps(ir)
|
|
268
295
|
this.stringValueNames = collectStringValueNames(ir)
|
|
269
296
|
this.moduleStringConsts = collectModuleStringConsts(ir.metadata.localConstants)
|
|
@@ -375,7 +402,9 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
375
402
|
}
|
|
376
403
|
|
|
377
404
|
emitText(node: IRText): string {
|
|
378
|
-
|
|
405
|
+
// IRText carries the entity-DECODED value (Phase 1 decodes JSX
|
|
406
|
+
// character references); re-escape for direct HTML emission.
|
|
407
|
+
return escapeHtml(node.value)
|
|
379
408
|
}
|
|
380
409
|
|
|
381
410
|
emitExpression(node: IRExpression): string {
|
|
@@ -484,7 +513,8 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
484
513
|
renderElement(element: IRElement): string {
|
|
485
514
|
const tag = element.tag
|
|
486
515
|
const attrs = this.renderAttributes(element)
|
|
487
|
-
const
|
|
516
|
+
const dangerousHtml = this.renderDangerousInnerHtml(element)
|
|
517
|
+
const children = dangerousHtml !== null ? dangerousHtml : this.renderChildren(element.children)
|
|
488
518
|
|
|
489
519
|
let hydrationAttrs = ''
|
|
490
520
|
if (element.needsScope) {
|
|
@@ -519,6 +549,28 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
519
549
|
return `<${tag}${attrs}${hydrationAttrs}>${children}</${tag}>`
|
|
520
550
|
}
|
|
521
551
|
|
|
552
|
+
/**
|
|
553
|
+
* `dangerouslySetInnerHTML={{ __html: '...' }}` (#2207) — see the Blade
|
|
554
|
+
* adapter's identical helper for the full rationale. `null` means the
|
|
555
|
+
* attribute is absent (caller falls through to normal `renderChildren`);
|
|
556
|
+
* a non-`null` string (possibly `''`) replaces the children outright.
|
|
557
|
+
*/
|
|
558
|
+
private renderDangerousInnerHtml(element: IRElement): string | null {
|
|
559
|
+
const resolution = resolveDangerousInnerHtml(element)
|
|
560
|
+
if (!resolution) return null
|
|
561
|
+
if (resolution.kind === 'dynamic') {
|
|
562
|
+
this.errors.push(dangerousInnerHtmlDiagnostic(resolution.expr, resolution.loc))
|
|
563
|
+
return ''
|
|
564
|
+
}
|
|
565
|
+
const violation = dangerousInnerHtmlMetacharViolation(resolution.html, this.name)
|
|
566
|
+
if (violation) {
|
|
567
|
+
const attr = element.attrs.find(isDangerousInnerHtmlAttr)!
|
|
568
|
+
this.errors.push(dangerousInnerHtmlDiagnostic(`{ __html: ${JSON.stringify(resolution.html)} }`, attr.loc, violation))
|
|
569
|
+
return ''
|
|
570
|
+
}
|
|
571
|
+
return resolution.html
|
|
572
|
+
}
|
|
573
|
+
|
|
522
574
|
// ===========================================================================
|
|
523
575
|
// Expression Rendering
|
|
524
576
|
// ===========================================================================
|
|
@@ -531,7 +583,12 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
531
583
|
return ''
|
|
532
584
|
}
|
|
533
585
|
|
|
534
|
-
|
|
586
|
+
// Thread the IR-carried `.parsed` tree through (mirrors go-template's
|
|
587
|
+
// `convertExpressionToGo(expr.expr, classify, expr.parsed)`) so a
|
|
588
|
+
// resolved bare-identifier `.map`/`.filter`/… callback
|
|
589
|
+
// (`resolveCallbackMethodFunctionReferences`, #2206) isn't lost to a
|
|
590
|
+
// fresh, unresolved re-parse of the raw string.
|
|
591
|
+
const perlExpr = this.convertExpressionToKolon(expr.expr, expr.parsed)
|
|
535
592
|
|
|
536
593
|
if (expr.slotId) {
|
|
537
594
|
return `<: $bf.text_start("${expr.slotId}") | mark_raw :><: ${perlExpr} :><: $bf.text_end() | mark_raw :>`
|
|
@@ -677,8 +734,27 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
677
734
|
// this adapter's test corpus only because the widened destructure gate
|
|
678
735
|
// (#2087 Phase A/B) no longer refuses this fixture's `([emoji, users])
|
|
679
736
|
// => ...` param first.
|
|
737
|
+
// #2208: a loop source that is a fully-static array literal — either
|
|
738
|
+
// inline (`[{ label: 'Alpha' }, ...].map(...)`) or a bare identifier
|
|
739
|
+
// bound to a FUNCTION-scope local const whose initializer has no
|
|
740
|
+
// prop/signal/function-call dependency — inlines as a native Kolon
|
|
741
|
+
// array/hash literal below, the same way a module-scope const's value
|
|
742
|
+
// is already seeded. A runtime-computed local (#2069, e.g.
|
|
743
|
+
// `Object.entries(props.tags).filter(...)`) still refuses below.
|
|
744
|
+
// `isNameShadowed` guards a DIFFERENT, enclosing loop's own callback
|
|
745
|
+
// param shadowing this identifier (fable review) — never resolve the
|
|
746
|
+
// static const in that case. `rawArray` then falls through to the
|
|
747
|
+
// bare identifier expression below, same as before #2208 — which
|
|
748
|
+
// still trips the pre-existing BF101 gate for an unresolvable local
|
|
749
|
+
// const reference (a loud, conservative refusal, not a silent wrong
|
|
750
|
+
// value).
|
|
751
|
+
const staticItems = resolveStaticLoopSource(loop.arrayParsed, this.localConstants, {
|
|
752
|
+
isNameShadowed: name => this.staticLoopSourceBoundNames.has(name),
|
|
753
|
+
})
|
|
754
|
+
const staticArray = staticItems !== null ? staticValueToKolon(staticItems) : null
|
|
755
|
+
|
|
680
756
|
const arrayName = loop.array.trim()
|
|
681
|
-
if (/^[A-Za-z_$][\w$]*$/.test(arrayName)) {
|
|
757
|
+
if (staticArray === null && /^[A-Za-z_$][\w$]*$/.test(arrayName)) {
|
|
682
758
|
const arrayConst = (this.localConstants ?? []).find(c => c.name === arrayName)
|
|
683
759
|
if (arrayConst && !arrayConst.isModule && this._resolveLiteralConst(arrayName) === null) {
|
|
684
760
|
this.errors.push({
|
|
@@ -694,7 +770,7 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
694
770
|
}
|
|
695
771
|
}
|
|
696
772
|
|
|
697
|
-
const rawArray = this.convertExpressionToKolon(loop.array)
|
|
773
|
+
const rawArray = staticArray ?? this.convertExpressionToKolon(loop.array)
|
|
698
774
|
// Apply sort if present: wrap the loop array in the shared `$bf.sort`
|
|
699
775
|
// helper, binding the sorted result to a per-iteration local so the
|
|
700
776
|
// helper runs once.
|
|
@@ -724,9 +800,13 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
724
800
|
// For `keys`-shape iterations the callback param IS the index. We iterate
|
|
725
801
|
// the array but bind the loop var to a throwaway and expose the index as
|
|
726
802
|
// `$param`. Kolon's `$~loopvar.index` provides the 0-based index.
|
|
727
|
-
const loopVar = loop.
|
|
728
|
-
? '
|
|
729
|
-
:
|
|
803
|
+
const loopVar = loop.objectIteration === 'entries'
|
|
804
|
+
? '__bf_pair'
|
|
805
|
+
: loop.objectIteration
|
|
806
|
+
? param
|
|
807
|
+
: loop.iterationShape === 'keys'
|
|
808
|
+
? '__bf_item'
|
|
809
|
+
: supportableDestructure ? '__bf_item' : param
|
|
730
810
|
|
|
731
811
|
// Index alias: when an explicit `index` param is present (`.map((x, i) =>
|
|
732
812
|
// ...)`) or the iteration is `keys`-shaped, expose it via a `: my` Kolon
|
|
@@ -746,7 +826,16 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
746
826
|
// `segments` (empty at the loop root, per the `LoopParamBinding` jsdoc) —
|
|
747
827
|
// NOT the same as a fixed binding's full-accessor segments.
|
|
748
828
|
const indexLocalLines: string[] = []
|
|
749
|
-
if (loop.
|
|
829
|
+
if (loop.objectIteration === 'entries') {
|
|
830
|
+
// `key`/`value` bind off the `.kv()` pair (see the for-header below)
|
|
831
|
+
// — no derived `.index` local needed, unlike the array
|
|
832
|
+
// `iterationShape` cases.
|
|
833
|
+
indexLocalLines.push(`: my $${loop.index ?? param} = $${loopVar}.key;`)
|
|
834
|
+
indexLocalLines.push(`: my $${param} = $${loopVar}.value;`)
|
|
835
|
+
} else if (loop.objectIteration) {
|
|
836
|
+
// 'keys'/'values': `.keys()`/`.values()` already yield the bound
|
|
837
|
+
// value directly — no derived local needed either.
|
|
838
|
+
} else if (loop.iterationShape === 'keys') {
|
|
750
839
|
indexLocalLines.push(`: my $${param} = $~${loopVar}.index;`)
|
|
751
840
|
} else if (loop.index) {
|
|
752
841
|
indexLocalLines.push(`: my $${loop.index} = $~${loopVar}.index;`)
|
|
@@ -769,10 +858,13 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
769
858
|
|
|
770
859
|
const prevInLoop = this.inLoop
|
|
771
860
|
this.inLoop = true
|
|
861
|
+
const prevLoopKeyDepth = this.currentLoopKeyDepth
|
|
862
|
+
this.currentLoopKeyDepth = loop.depth
|
|
772
863
|
// Re-render children now that inLoop is set (so nested components use the
|
|
773
864
|
// loop-child naming convention). renderedChildren above was computed with
|
|
774
865
|
// the previous flag; recompute under the loop flag.
|
|
775
866
|
const childrenUnderLoop = this.renderChildren(loop.children)
|
|
867
|
+
this.currentLoopKeyDepth = prevLoopKeyDepth
|
|
776
868
|
this.inLoop = prevInLoop
|
|
777
869
|
void renderedChildren
|
|
778
870
|
|
|
@@ -789,7 +881,21 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
789
881
|
// Scoped per-call-site marker so sibling `.map()`s under the same parent
|
|
790
882
|
// each get their own reconciliation range.
|
|
791
883
|
lines.push(`<: $bf.comment("loop:${loop.markerId}") | mark_raw :>`)
|
|
792
|
-
|
|
884
|
+
// `objectIteration` (#2168 object-entries-map): Kolon has no built-in
|
|
885
|
+
// hash-destructure `for` target, so `'entries'` iterates `.kv()`
|
|
886
|
+
// (yielding `{key, value}` pair objects, unpacked via the `: my`
|
|
887
|
+
// locals above) while `'keys'`/`'values'` iterate `.keys()`/`.values()`
|
|
888
|
+
// directly. All three are alphabetically sorted by Text::Xslate itself
|
|
889
|
+
// (verified empirically) — not JS insertion order, a documented known
|
|
890
|
+
// limitation for out-of-alphabetical-order data, same as Go/Rust.
|
|
891
|
+
const forHeader = loop.objectIteration === 'entries'
|
|
892
|
+
? `: for ${array}.kv() -> $${loopVar} {`
|
|
893
|
+
: loop.objectIteration === 'keys'
|
|
894
|
+
? `: for ${array}.keys() -> $${loopVar} {`
|
|
895
|
+
: loop.objectIteration === 'values'
|
|
896
|
+
? `: for ${array}.values() -> $${loopVar} {`
|
|
897
|
+
: `: for ${array} -> $${loopVar} {`
|
|
898
|
+
lines.push(forHeader)
|
|
793
899
|
for (const il of indexLocalLines) lines.push(il)
|
|
794
900
|
|
|
795
901
|
// Handle filter().map() pattern by wrapping children in if-condition
|
|
@@ -932,11 +1038,33 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
932
1038
|
type Segment = { kind: 'entries'; parts: string[] } | { kind: 'spread'; expr: string }
|
|
933
1039
|
const segments: Segment[] = [{ kind: 'entries', parts: [] }]
|
|
934
1040
|
const currentEntries = () => this.componentPropSegmentEntries(segments)
|
|
1041
|
+
// Named JSX-valued props OTHER than the reserved `children`
|
|
1042
|
+
// (`header={<strong>Title</strong>}`, #2168 jsx-element-prop) each get
|
|
1043
|
+
// their own macro, prepended to the final returned string below —
|
|
1044
|
+
// same mechanism as the reserved children macro, just keyed by the
|
|
1045
|
+
// prop's own name instead of `children`.
|
|
1046
|
+
const namedSlotMacros: string[] = []
|
|
935
1047
|
|
|
936
1048
|
for (const p of comp.props) {
|
|
937
1049
|
// Skip callback props (onXxx) and `ref` — both are client-only for
|
|
938
1050
|
// SSR (Hono renders neither; the client JS wires them at hydration).
|
|
939
1051
|
if ((p.name.match(/^on[A-Z]/) || p.name === 'ref') && p.value.kind === 'expression') continue
|
|
1052
|
+
if (p.value.kind === 'jsx-children' && p.name !== 'children') {
|
|
1053
|
+
const prevInLoop = this.inLoop
|
|
1054
|
+
this.inLoop = false
|
|
1055
|
+
const slotBody = this.renderChildren(p.value.children)
|
|
1056
|
+
this.inLoop = prevInLoop
|
|
1057
|
+
// Purely counter-based — NOT derived from `p.name` or `comp.slotId`.
|
|
1058
|
+
// A JSX prop name can contain characters (`data-slot`) that aren't a
|
|
1059
|
+
// valid Kolon macro identifier, and `comp.slotId` alone would
|
|
1060
|
+
// collide across two named-slot props on the same component
|
|
1061
|
+
// invocation (unlike the reserved children slot, there's only ever
|
|
1062
|
+
// one of those per invocation).
|
|
1063
|
+
const macroName = `bf_prop_${this.childrenCaptureCounter++}`
|
|
1064
|
+
namedSlotMacros.push(`<: macro ${macroName} -> () { :>${slotBody}<: } :>`)
|
|
1065
|
+
currentEntries().push(`${kolonHashKey(p.name)} => ${macroName}()`)
|
|
1066
|
+
continue
|
|
1067
|
+
}
|
|
940
1068
|
if (p.value.kind === 'spread') {
|
|
941
1069
|
const trimmed = p.value.expr.trim()
|
|
942
1070
|
// SolidJS-style props identifier (`function(props: P)`) has no
|
|
@@ -996,12 +1124,12 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
996
1124
|
const macroName = `bf_children_${comp.slotId ?? 'c' + this.childrenCaptureCounter++}`
|
|
997
1125
|
currentEntries().push(`children => ${macroName}()`)
|
|
998
1126
|
const dict = this.combineComponentPropSegments(segments)
|
|
999
|
-
return
|
|
1127
|
+
return `${namedSlotMacros.join('')}<: macro ${macroName} -> () { :>${childrenBody}<: } :><: $bf.render_child('${tplName}', ${dict}) | mark_raw :>`
|
|
1000
1128
|
}
|
|
1001
1129
|
|
|
1002
1130
|
const isEmpty = segments.every(s => s.kind === 'entries' && s.parts.length === 0)
|
|
1003
1131
|
const hashEntries = isEmpty ? '' : `, ${this.combineComponentPropSegments(segments)}`
|
|
1004
|
-
return
|
|
1132
|
+
return `${namedSlotMacros.join('')}<: $bf.render_child('${tplName}'${hashEntries}) | mark_raw :>`
|
|
1005
1133
|
}
|
|
1006
1134
|
|
|
1007
1135
|
private childrenCaptureCounter = 0
|
|
@@ -1085,7 +1213,7 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
1085
1213
|
* AttrValue lowering for intrinsic-element attributes (Kolon).
|
|
1086
1214
|
*/
|
|
1087
1215
|
private readonly elementAttrEmitter: AttrValueEmitter = {
|
|
1088
|
-
emitLiteral: (value, name) => `${name}="${value.value}"`,
|
|
1216
|
+
emitLiteral: (value, name) => `${name}="${escapeHtml(value.value)}"`,
|
|
1089
1217
|
emitExpression: (value, name) => {
|
|
1090
1218
|
// `style={{ … }}` object literal → a CSS string with dynamic values
|
|
1091
1219
|
// interpolated, instead of refusing the bare object with BF101 (#1322).
|
|
@@ -1282,10 +1410,19 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
1282
1410
|
// the unsupported-expression lowering is never reached for a deferred
|
|
1283
1411
|
// predicate (no BF101 / BF102). #1966
|
|
1284
1412
|
if (attr.clientOnly) continue
|
|
1413
|
+
// `dangerouslySetInnerHTML` never renders as an HTML attribute — it's
|
|
1414
|
+
// handled by `renderDangerousInnerHtml` instead, which replaces the
|
|
1415
|
+
// element's children. Skip it here so its `{ __html: ... }` object
|
|
1416
|
+
// literal never reaches the generic object-literal BF101 refusal
|
|
1417
|
+
// (which would double-report alongside the purpose-built one).
|
|
1418
|
+
if (isDangerousInnerHtmlAttr(attr)) continue
|
|
1285
1419
|
// Rewrite JSX special-prop names to their HTML-attribute counterparts.
|
|
1286
1420
|
let attrName: string
|
|
1287
1421
|
if (attr.name === 'className') attrName = 'class'
|
|
1288
|
-
else if (attr.name === 'key')
|
|
1422
|
+
else if (attr.name === 'key') {
|
|
1423
|
+
const depth = this.currentLoopKeyDepth
|
|
1424
|
+
attrName = depth > 0 ? `data-key-${depth}` : 'data-key'
|
|
1425
|
+
}
|
|
1289
1426
|
else attrName = attr.name
|
|
1290
1427
|
const lowered = emitAttrValue(attr.value, this.elementAttrEmitter, attrName)
|
|
1291
1428
|
if (lowered) parts.push(lowered)
|
|
@@ -1607,8 +1744,19 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
1607
1744
|
* single-quoted string literal (`const totalPages = 5`, #1897
|
|
1608
1745
|
* pagination) — function-scope consts never reach the per-render
|
|
1609
1746
|
* stash, so a bare `$totalPages` renders empty.
|
|
1747
|
+
*
|
|
1748
|
+
* The lookup is a flat name match with no notion of AST scope, so a
|
|
1749
|
+
* name that any loop callback binds as its item/index param never
|
|
1750
|
+
* inlines (#2221) — the occurrence may be the loop's own (shadowing)
|
|
1751
|
+
* binding, and substituting the outer const's value there renders every
|
|
1752
|
+
* iteration with the same hard-coded literal. Coarse (a genuinely
|
|
1753
|
+
* non-shadowed same-named const elsewhere in the component also stops
|
|
1754
|
+
* inlining, falling back to the bare identifier) but safe — the same
|
|
1755
|
+
* trade-off as #2212's `collectLoopBoundNames` use in
|
|
1756
|
+
* `collectStringValueNames`.
|
|
1610
1757
|
*/
|
|
1611
1758
|
private _resolveLiteralConst(name: string): string | null {
|
|
1759
|
+
if (this.staticLoopSourceBoundNames.has(name)) return null
|
|
1612
1760
|
const c = (this.localConstants ?? []).find(lc => lc.name === name)
|
|
1613
1761
|
if (c?.value === undefined) return null
|
|
1614
1762
|
const v = c.value.trim()
|
|
@@ -1618,7 +1766,22 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
1618
1766
|
return null
|
|
1619
1767
|
}
|
|
1620
1768
|
|
|
1769
|
+
/**
|
|
1770
|
+
* Resolve `IDENT.key` where `IDENT` is a module-scope object-literal const
|
|
1771
|
+
* (`variantClasses.ghost`, #1896/#1897) to the looked-up scalar.
|
|
1772
|
+
*
|
|
1773
|
+
* The lookup is a flat name match on `objectName` with no notion of AST
|
|
1774
|
+
* scope, so an enclosing loop callback's own param of the same name
|
|
1775
|
+
* (`.map((cfg) => <li>{cfg.x}</li>)` shadowing a module `const cfg = {…}`)
|
|
1776
|
+
* still resolved to the OUTER const's member value at every iteration
|
|
1777
|
+
* (#2237) — the sibling hazard to #2221's `_resolveLiteralConst`. Same
|
|
1778
|
+
* coarse-but-safe `staticLoopSourceBoundNames` guard: any name a loop
|
|
1779
|
+
* binds anywhere in the component never inlines, falling back to the bare
|
|
1780
|
+
* `$cfg.x` member expression (which an Xslate `: for` loop binds
|
|
1781
|
+
* correctly at the shadowed occurrences).
|
|
1782
|
+
*/
|
|
1621
1783
|
private _resolveStaticRecordLiteral(objectName: string, key: string): string | null {
|
|
1784
|
+
if (this.staticLoopSourceBoundNames.has(objectName)) return null
|
|
1622
1785
|
const hit = lookupStaticRecordLiteral(objectName, key, this.localConstants)
|
|
1623
1786
|
if (!hit) return null
|
|
1624
1787
|
return hit.kind === 'number'
|
package/src/conformance-pins.ts
CHANGED
|
@@ -9,17 +9,17 @@
|
|
|
9
9
|
import type { ConformancePins } from '@barefootjs/jsx'
|
|
10
10
|
|
|
11
11
|
export const conformancePins: ConformancePins = {
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
'
|
|
12
|
+
// `todo-app` / `todo-app-ssr` no longer pinned (#2205) — the conformance
|
|
13
|
+
// harness now passes `siblingTemplatesRegistered: true` for fixtures with
|
|
14
|
+
// sibling `components`, matching `bf build`'s real semantics, so the
|
|
15
|
+
// BF103 loop-body cross-template check no longer fires spuriously. (Both
|
|
16
|
+
// fixtures are still skipped on this adapter via `render-divergences.ts`
|
|
17
|
+
// — #2209 — for an unrelated signal-seeding gap.)
|
|
18
|
+
// `static-array-children` no longer pinned (#2208) — `items`'s
|
|
19
|
+
// array-literal initializer is now recognized as fully-static
|
|
20
|
+
// (`resolveStaticLoopSource`) and inlined as a native Kolon array/hash
|
|
21
|
+
// literal in the `for EXPR -> $item` header, the same way a module-scope
|
|
22
|
+
// const's value is already seeded.
|
|
23
23
|
// `([emoji, users]) => ...` / `([id, t]) => ...` are plain array-index
|
|
24
24
|
// (tuple) destructures, no rest — #2087 Phase B's `segments`-walking
|
|
25
25
|
// accessor lowers both to `$__bf_item[0]` / `$__bf_item[1]` `: my` locals
|
|
@@ -32,12 +32,11 @@ export const conformancePins: ConformancePins = {
|
|
|
32
32
|
// simply unreachable before because BF104 refused the destructure shape
|
|
33
33
|
// first.
|
|
34
34
|
'static-array-from-props': [{ code: 'BF101', severity: 'error' }],
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
],
|
|
35
|
+
// The BF101 above fires; BF104 no longer does (see above), and BF103
|
|
36
|
+
// (sibling-imported `<Tag>` child component in the loop body) no longer
|
|
37
|
+
// does either now that the conformance harness passes
|
|
38
|
+
// `siblingTemplatesRegistered: true` (#2205).
|
|
39
|
+
'static-array-from-props-with-component': [{ code: 'BF101', severity: 'error' }],
|
|
41
40
|
// #1310 / #2087: rest destructure in .map() callback. All four shapes now
|
|
42
41
|
// lower via #2087 Phase B's `segments`-walking accessor:
|
|
43
42
|
// - object-rest read via member access (`rest-destructure-object-in-map`):
|
|
@@ -104,20 +103,14 @@ export const conformancePins: ConformancePins = {
|
|
|
104
103
|
// `find_last_index` via the same Kolon-lambda mechanism as `.filter` /
|
|
105
104
|
// `.every` / `.some`, so they render. Only the NESTED-in-a-predicate form
|
|
106
105
|
// above is refused (#2038).
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
|
|
117
|
-
'dangerous-inner-html': [{ code: 'BF101', severity: 'error' }],
|
|
118
|
-
// Edge-case sweep (Priority 12): `.replaceAll` has no lowering yet —
|
|
119
|
-
// only first-occurrence `.replace` is wired to the runtime helpers.
|
|
120
|
-
// Refused with BF101 rather than reusing the first-only lowering,
|
|
121
|
-
// which would silently change semantics.
|
|
122
|
-
'string-replaceall': [{ code: 'BF101', severity: 'error' }],
|
|
106
|
+
// `array-map-function-reference` no longer pinned — a bare-identifier
|
|
107
|
+
// `.map(format)` callback now resolves one hop to its declaration
|
|
108
|
+
// (`resolveCallbackMethodFunctionReferences`, #2206), the same mechanism
|
|
109
|
+
// #2090 established for `.sort(fnref)`.
|
|
110
|
+
// `dangerous-inner-html` no longer pinned — a compile-time string-literal
|
|
111
|
+
// `dangerouslySetInnerHTML={{ __html: '...' }}` is spliced directly into
|
|
112
|
+
// the template as trusted raw text (`resolveDangerousInnerHtml`, #2207).
|
|
113
|
+
// A dynamic/signal-derived value still refuses with BF101 — see the
|
|
114
|
+
// `dangerous-inner-html-dynamic` fixture/pin below (tracked: #2215).
|
|
115
|
+
'dangerous-inner-html-dynamic': [{ code: 'BF101', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2215' }],
|
|
123
116
|
}
|
|
@@ -15,20 +15,8 @@
|
|
|
15
15
|
import type { RenderDivergences } from '@barefootjs/jsx'
|
|
16
16
|
|
|
17
17
|
export const renderDivergences: RenderDivergences = {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
'static-attr-escape':
|
|
23
|
-
'static attribute values are not HTML-escaped (`title="Fish & Chips"` emitted raw; Hono escapes)',
|
|
24
|
-
'object-entries-map':
|
|
25
|
-
'`Object.entries(prop).map(([k, v]) => …)` renders an EMPTY list — the object-shaped prop silently produces zero iterations',
|
|
26
|
-
'nested-loop-outer-binding':
|
|
27
|
-
'nested-loop inner items carry `data-key` where the reference emits the depth-suffixed `data-key-1`',
|
|
28
|
-
'jsx-element-prop':
|
|
29
|
-
'a JSX element passed as a NON-children prop renders an empty slot — the element value is silently dropped',
|
|
30
|
-
'string-slice':
|
|
31
|
-
'`.slice()` on a STRING misfires through the array slice helper',
|
|
32
|
-
'string-trim-sided':
|
|
33
|
-
'`.trimStart()` / `.trimEnd()` render empty (no lowering)',
|
|
18
|
+
// `todo-app` / `todo-app-ssr` no longer diverge (#2209) — the shared
|
|
19
|
+
// `evaluateSignalInit` (`@barefootjs/jsx`, sandboxed real-JS evaluation
|
|
20
|
+
// instead of a fixed regex-shape catalogue) now correctly seeds `todos`
|
|
21
|
+
// from `(props.initialTodos ?? []).map(t => ({ ...t, editing: false }))`.
|
|
34
22
|
}
|