@barefootjs/xslate 0.18.5 → 0.19.0
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/index.js +83 -6
- 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 +28 -9
- package/dist/adapter/props/prop-classes.d.ts.map +1 -1
- package/dist/adapter/xslate-adapter.d.ts +41 -0
- package/dist/adapter/xslate-adapter.d.ts.map +1 -1
- package/dist/build.js +83 -6
- package/dist/conformance-pins.d.ts.map +1 -1
- package/dist/index.js +85 -15
- 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 +136 -0
- package/src/adapter/lib/static-value.ts +44 -0
- package/src/adapter/props/prop-classes.ts +33 -10
- package/src/adapter/xslate-adapter.ts +102 -4
- package/src/conformance-pins.ts +26 -28
- package/src/render-divergences.ts +6 -1
- package/src/test-render.ts +13 -139
|
@@ -73,6 +73,12 @@ 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'
|
|
@@ -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 {
|
|
@@ -240,6 +247,17 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
240
247
|
*/
|
|
241
248
|
private localConstants: IRMetadata['localConstants'] = []
|
|
242
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
|
+
|
|
243
261
|
/**
|
|
244
262
|
* Optional, no-default props that are `undef` when the caller omits them.
|
|
245
263
|
* Their bare-reference attribute emission is guarded with Kolon `defined` so
|
|
@@ -272,6 +290,7 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
272
290
|
// Per-compile prop classifications (see `props/prop-classes.ts`).
|
|
273
291
|
this.booleanTypedProps = collectBooleanTypedProps(ir)
|
|
274
292
|
this.localConstants = ir.metadata.localConstants ?? []
|
|
293
|
+
this.staticLoopSourceBoundNames = collectLoopBoundNames(ir)
|
|
275
294
|
this.nullableOptionalProps = collectNullableOptionalProps(ir)
|
|
276
295
|
this.stringValueNames = collectStringValueNames(ir)
|
|
277
296
|
this.moduleStringConsts = collectModuleStringConsts(ir.metadata.localConstants)
|
|
@@ -494,7 +513,8 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
494
513
|
renderElement(element: IRElement): string {
|
|
495
514
|
const tag = element.tag
|
|
496
515
|
const attrs = this.renderAttributes(element)
|
|
497
|
-
const
|
|
516
|
+
const dangerousHtml = this.renderDangerousInnerHtml(element)
|
|
517
|
+
const children = dangerousHtml !== null ? dangerousHtml : this.renderChildren(element.children)
|
|
498
518
|
|
|
499
519
|
let hydrationAttrs = ''
|
|
500
520
|
if (element.needsScope) {
|
|
@@ -529,6 +549,28 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
529
549
|
return `<${tag}${attrs}${hydrationAttrs}>${children}</${tag}>`
|
|
530
550
|
}
|
|
531
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
|
+
|
|
532
574
|
// ===========================================================================
|
|
533
575
|
// Expression Rendering
|
|
534
576
|
// ===========================================================================
|
|
@@ -541,7 +583,12 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
541
583
|
return ''
|
|
542
584
|
}
|
|
543
585
|
|
|
544
|
-
|
|
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)
|
|
545
592
|
|
|
546
593
|
if (expr.slotId) {
|
|
547
594
|
return `<: $bf.text_start("${expr.slotId}") | mark_raw :><: ${perlExpr} :><: $bf.text_end() | mark_raw :>`
|
|
@@ -687,8 +734,27 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
687
734
|
// this adapter's test corpus only because the widened destructure gate
|
|
688
735
|
// (#2087 Phase A/B) no longer refuses this fixture's `([emoji, users])
|
|
689
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
|
+
|
|
690
756
|
const arrayName = loop.array.trim()
|
|
691
|
-
if (/^[A-Za-z_$][\w$]*$/.test(arrayName)) {
|
|
757
|
+
if (staticArray === null && /^[A-Za-z_$][\w$]*$/.test(arrayName)) {
|
|
692
758
|
const arrayConst = (this.localConstants ?? []).find(c => c.name === arrayName)
|
|
693
759
|
if (arrayConst && !arrayConst.isModule && this._resolveLiteralConst(arrayName) === null) {
|
|
694
760
|
this.errors.push({
|
|
@@ -704,7 +770,7 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
704
770
|
}
|
|
705
771
|
}
|
|
706
772
|
|
|
707
|
-
const rawArray = this.convertExpressionToKolon(loop.array)
|
|
773
|
+
const rawArray = staticArray ?? this.convertExpressionToKolon(loop.array)
|
|
708
774
|
// Apply sort if present: wrap the loop array in the shared `$bf.sort`
|
|
709
775
|
// helper, binding the sorted result to a per-iteration local so the
|
|
710
776
|
// helper runs once.
|
|
@@ -1344,6 +1410,12 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
1344
1410
|
// the unsupported-expression lowering is never reached for a deferred
|
|
1345
1411
|
// predicate (no BF101 / BF102). #1966
|
|
1346
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
|
|
1347
1419
|
// Rewrite JSX special-prop names to their HTML-attribute counterparts.
|
|
1348
1420
|
let attrName: string
|
|
1349
1421
|
if (attr.name === 'className') attrName = 'class'
|
|
@@ -1672,8 +1744,19 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
1672
1744
|
* single-quoted string literal (`const totalPages = 5`, #1897
|
|
1673
1745
|
* pagination) — function-scope consts never reach the per-render
|
|
1674
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`.
|
|
1675
1757
|
*/
|
|
1676
1758
|
private _resolveLiteralConst(name: string): string | null {
|
|
1759
|
+
if (this.staticLoopSourceBoundNames.has(name)) return null
|
|
1677
1760
|
const c = (this.localConstants ?? []).find(lc => lc.name === name)
|
|
1678
1761
|
if (c?.value === undefined) return null
|
|
1679
1762
|
const v = c.value.trim()
|
|
@@ -1683,7 +1766,22 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
1683
1766
|
return null
|
|
1684
1767
|
}
|
|
1685
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
|
+
*/
|
|
1686
1783
|
private _resolveStaticRecordLiteral(objectName: string, key: string): string | null {
|
|
1784
|
+
if (this.staticLoopSourceBoundNames.has(objectName)) return null
|
|
1687
1785
|
const hit = lookupStaticRecordLiteral(objectName, key, this.localConstants)
|
|
1688
1786
|
if (!hit) return null
|
|
1689
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,15 +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' }],
|
|
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' }],
|
|
118
116
|
}
|
|
@@ -14,4 +14,9 @@
|
|
|
14
14
|
|
|
15
15
|
import type { RenderDivergences } from '@barefootjs/jsx'
|
|
16
16
|
|
|
17
|
-
export const renderDivergences: RenderDivergences = {
|
|
17
|
+
export const renderDivergences: RenderDivergences = {
|
|
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 }))`.
|
|
22
|
+
}
|
package/src/test-render.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* than the Mojo harness's literal `test_<sN>`.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import { compileJSX, extractSsrDefaults, importsSearchParams } from '@barefootjs/jsx'
|
|
20
|
+
import { compileJSX, extractSsrDefaults, importsSearchParams, evaluateSignalInit, tryEvaluateSignalInit } from '@barefootjs/jsx'
|
|
21
21
|
import type { ComponentIR } from '@barefootjs/jsx'
|
|
22
22
|
import { mkdir, rm } from 'node:fs/promises'
|
|
23
23
|
import { resolve } from 'node:path'
|
|
@@ -135,8 +135,15 @@ export async function renderXslateComponent(options: RenderOptions): Promise<str
|
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
// Compile parent source.
|
|
139
|
-
|
|
138
|
+
// Compile parent source. `siblingTemplatesRegistered: Boolean(components)`
|
|
139
|
+
// matches this harness's real behavior — every sibling child template is registered
|
|
140
|
+
// alongside the parent before rendering, so a loop-body cross-template
|
|
141
|
+
// call resolves at render time (#2205).
|
|
142
|
+
const result = compileJSX(source, 'component.tsx', {
|
|
143
|
+
adapter,
|
|
144
|
+
outputIR: true,
|
|
145
|
+
siblingTemplatesRegistered: Boolean(components),
|
|
146
|
+
})
|
|
140
147
|
|
|
141
148
|
const errors = result.errors.filter(e => e.severity === 'error')
|
|
142
149
|
if (errors.length > 0) {
|
|
@@ -467,9 +474,9 @@ function buildPerlProps(
|
|
|
467
474
|
for (const param of ir.metadata.propsParams) {
|
|
468
475
|
if (props && param.name in props) continue
|
|
469
476
|
if (param.defaultValue) {
|
|
470
|
-
const
|
|
471
|
-
if (
|
|
472
|
-
entries.push(`${param.name} => ${
|
|
477
|
+
const result = tryEvaluateSignalInit(param.defaultValue.trim(), props)
|
|
478
|
+
if (result.ok) {
|
|
479
|
+
entries.push(`${param.name} => ${toPerlLiteral(result.value)}`)
|
|
473
480
|
continue
|
|
474
481
|
}
|
|
475
482
|
}
|
|
@@ -547,119 +554,6 @@ function buildPerlProps(
|
|
|
547
554
|
return `{${entries.join(', ')}}`
|
|
548
555
|
}
|
|
549
556
|
|
|
550
|
-
/**
|
|
551
|
-
* Evaluate a signal initializer expression using provided props.
|
|
552
|
-
* Handles: props.initial ?? 0, props.value, literal values.
|
|
553
|
-
*/
|
|
554
|
-
export function evaluateSignalInit(
|
|
555
|
-
expr: string,
|
|
556
|
-
props?: Record<string, unknown>,
|
|
557
|
-
): unknown {
|
|
558
|
-
const nullishMatch = expr.match(/^props\.(\w+)\s*\?\?\s*(.+)$/)
|
|
559
|
-
if (nullishMatch) {
|
|
560
|
-
const propName = nullishMatch[1]
|
|
561
|
-
const defaultExpr = nullishMatch[2].trim()
|
|
562
|
-
if (props && propName in props) return props[propName]
|
|
563
|
-
return parseLiteral(defaultExpr)
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
const propsMatch = expr.match(/^props\.(\w+)$/)
|
|
567
|
-
if (propsMatch) {
|
|
568
|
-
if (props && propsMatch[1] in props) return props[propsMatch[1]]
|
|
569
|
-
return null
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
return parseLiteral(expr)
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
function parseLiteral(expr: string): unknown {
|
|
576
|
-
if (/^-?\d+(\.\d+)?$/.test(expr)) return Number(expr)
|
|
577
|
-
if (expr === 'true') return true
|
|
578
|
-
if (expr === 'false') return false
|
|
579
|
-
if (expr === '[]') return []
|
|
580
|
-
|
|
581
|
-
{
|
|
582
|
-
const t = expr.trim()
|
|
583
|
-
if (t.startsWith('[') && t.endsWith(']')) {
|
|
584
|
-
const inner = t.slice(1, -1).trim()
|
|
585
|
-
if (!inner) return []
|
|
586
|
-
const out: unknown[] = []
|
|
587
|
-
for (const seg of splitTopLevelCommas(inner)) {
|
|
588
|
-
if (!seg.trim()) continue
|
|
589
|
-
const parsed = parseLiteral(seg.trim())
|
|
590
|
-
if (parsed === null && seg.trim() !== 'null') return null
|
|
591
|
-
out.push(parsed)
|
|
592
|
-
}
|
|
593
|
-
return out
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
const stringMatch = expr.match(/^(['"])(.*)\1$/s)
|
|
598
|
-
if (stringMatch) return unescapeJsString(stringMatch[2])
|
|
599
|
-
|
|
600
|
-
const trimmed = expr.trim()
|
|
601
|
-
if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
|
|
602
|
-
const inner = trimmed.slice(1, -1).trim()
|
|
603
|
-
if (!inner) return {}
|
|
604
|
-
const obj: Record<string, unknown> = {}
|
|
605
|
-
for (const pair of splitTopLevelCommas(inner)) {
|
|
606
|
-
if (!pair.trim()) continue
|
|
607
|
-
const colonIdx = pair.indexOf(':')
|
|
608
|
-
if (colonIdx < 0) return null
|
|
609
|
-
let key = pair.slice(0, colonIdx).trim()
|
|
610
|
-
const val = pair.slice(colonIdx + 1).trim()
|
|
611
|
-
const keyMatch = key.match(/^(['"])(.*)\1$/s)
|
|
612
|
-
if (keyMatch) key = unescapeJsString(keyMatch[2])
|
|
613
|
-
const parsedVal = parseLiteral(val)
|
|
614
|
-
if (parsedVal === null && val !== 'null') return null
|
|
615
|
-
obj[key] = parsedVal
|
|
616
|
-
}
|
|
617
|
-
return obj
|
|
618
|
-
}
|
|
619
|
-
return null
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
function splitTopLevelCommas(inner: string): string[] {
|
|
623
|
-
const segments: string[] = []
|
|
624
|
-
let depth = 0
|
|
625
|
-
let start = 0
|
|
626
|
-
let quote: string | null = null
|
|
627
|
-
for (let i = 0; i < inner.length; i++) {
|
|
628
|
-
const c = inner[i]
|
|
629
|
-
if (quote) {
|
|
630
|
-
if (c === quote) {
|
|
631
|
-
let backslashes = 0
|
|
632
|
-
for (let j = i - 1; j >= 0 && inner[j] === '\\'; j--) backslashes++
|
|
633
|
-
if (backslashes % 2 === 0) quote = null
|
|
634
|
-
}
|
|
635
|
-
continue
|
|
636
|
-
}
|
|
637
|
-
if (c === '"' || c === "'") {
|
|
638
|
-
quote = c
|
|
639
|
-
continue
|
|
640
|
-
}
|
|
641
|
-
if (c === '{' || c === '[') depth++
|
|
642
|
-
else if (c === '}' || c === ']') depth--
|
|
643
|
-
else if (c === ',' && depth === 0) {
|
|
644
|
-
segments.push(inner.slice(start, i))
|
|
645
|
-
start = i + 1
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
segments.push(inner.slice(start))
|
|
649
|
-
return segments
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
function unescapeJsString(s: string): string {
|
|
653
|
-
return s.replace(/\\(.)/g, (_, c) => {
|
|
654
|
-
switch (c) {
|
|
655
|
-
case 'n': return '\n'
|
|
656
|
-
case 'r': return '\r'
|
|
657
|
-
case 't': return '\t'
|
|
658
|
-
case '0': return '\0'
|
|
659
|
-
default: return c
|
|
660
|
-
}
|
|
661
|
-
})
|
|
662
|
-
}
|
|
663
557
|
|
|
664
558
|
/** Perl single-quoted string escape: `'` AND `\` need escaping. */
|
|
665
559
|
function perlSingleQuote(s: string): string {
|
|
@@ -685,23 +579,3 @@ function toPerlLiteral(value: unknown): string {
|
|
|
685
579
|
return 'undef'
|
|
686
580
|
}
|
|
687
581
|
|
|
688
|
-
/**
|
|
689
|
-
* Convert a JS literal value to a Perl literal.
|
|
690
|
-
* Handles: numbers, strings, booleans, empty arrays, props.xxx ?? default.
|
|
691
|
-
*/
|
|
692
|
-
function jsToPerlValue(jsValue: string): string | null {
|
|
693
|
-
const v = jsValue.trim()
|
|
694
|
-
|
|
695
|
-
if (/^-?\d+(\.\d+)?$/.test(v)) return v
|
|
696
|
-
if (/^['"].*['"]$/.test(v)) return v
|
|
697
|
-
if (v === 'true') return '1'
|
|
698
|
-
if (v === 'false') return '0'
|
|
699
|
-
if (v === '[]') return '[]'
|
|
700
|
-
|
|
701
|
-
const nullishMatch = v.match(/\?\?\s*(.+)$/)
|
|
702
|
-
if (nullishMatch) return jsToPerlValue(nullishMatch[1])
|
|
703
|
-
|
|
704
|
-
if (v.startsWith('props.')) return 'undef'
|
|
705
|
-
|
|
706
|
-
return null
|
|
707
|
-
}
|