@barefootjs/mojolicious 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 +151 -34
- package/dist/adapter/lib/constants.d.ts.map +1 -1
- package/dist/adapter/lib/static-value.d.ts +17 -0
- package/dist/adapter/lib/static-value.d.ts.map +1 -0
- package/dist/adapter/mojo-adapter.d.ts +23 -0
- package/dist/adapter/mojo-adapter.d.ts.map +1 -1
- package/dist/adapter/props/prop-classes.d.ts +23 -6
- package/dist/adapter/props/prop-classes.d.ts.map +1 -1
- package/dist/build.js +151 -34
- package/dist/conformance-pins.d.ts.map +1 -1
- package/dist/index.js +154 -56
- package/dist/render-divergences.d.ts.map +1 -1
- package/dist/test-render.d.ts +0 -5
- package/dist/test-render.d.ts.map +1 -1
- package/lib/BarefootJS/Backend/Mojo.pm +1 -1
- package/lib/Mojolicious/Plugin/BarefootJS/DevReload.pm +1 -1
- package/lib/Mojolicious/Plugin/BarefootJS.pm +1 -1
- package/package.json +3 -3
- package/src/__tests__/mojo-adapter.test.ts +186 -5
- package/src/adapter/expr/array-method.ts +19 -0
- package/src/adapter/expr/emitters.ts +18 -3
- package/src/adapter/lib/constants.ts +3 -0
- package/src/adapter/lib/static-value.ts +47 -0
- package/src/adapter/mojo-adapter.ts +148 -16
- package/src/adapter/props/prop-classes.ts +27 -6
- package/src/conformance-pins.ts +26 -33
- package/src/render-divergences.ts +4 -20
- package/src/test-render.ts +21 -210
- package/src/__tests__/evaluate-signal-init.test.ts +0 -35
|
@@ -195,6 +195,166 @@ function Box({ other }: { other?: object }) {
|
|
|
195
195
|
})
|
|
196
196
|
})
|
|
197
197
|
|
|
198
|
+
// #2221: same class of hazard as the Twig-family `_resolveLiteralConst`
|
|
199
|
+
// flat-lookup bug, but this adapter's story is different. `resolveLiteralConst`
|
|
200
|
+
// / `resolveStaticRecordLiteral` (mojo-adapter.ts) already guard against it —
|
|
201
|
+
// they consult `loopBoundNames`, a LIVE ref-counted map that
|
|
202
|
+
// `renderLoop` populates/depopulates as it descends/ascends into each loop
|
|
203
|
+
// body (#1749), not a static whole-component set like the Twig family's
|
|
204
|
+
// `collectLoopBoundNames(ir)`. That makes the guard scope-PRECISE rather
|
|
205
|
+
// than coarse: a name loop-bound only inside one loop still inlines fine
|
|
206
|
+
// at a genuinely separate, non-shadowed occurrence elsewhere in the
|
|
207
|
+
// component (see the third test below) — the Twig-family's documented
|
|
208
|
+
// coarse trade-off (a same-named const anywhere else in the component also
|
|
209
|
+
// stops inlining) does not apply here. So no `staticLoopSourceBoundNames`
|
|
210
|
+
// field was added; the existing live tracking already covers this call
|
|
211
|
+
// site and is strictly more precise.
|
|
212
|
+
//
|
|
213
|
+
// The ONE actual gap found: `emitSpread`'s bare-identifier local-const
|
|
214
|
+
// spread resolution (mojo-adapter.ts, the `this.localConstants.find(...)`
|
|
215
|
+
// call keyed by `trimmed`, `{...attrs}` → `{ … }` hashref, #checkbox/icon)
|
|
216
|
+
// read `this.localConstants` directly with no `loopBoundNames` guard at
|
|
217
|
+
// all — a loop param named the same as an outer conditional-object const
|
|
218
|
+
// (`.map((attrs) => <li {...attrs} />)` shadowing `const attrs = cond ?
|
|
219
|
+
// {…} : {}`) incorrectly forwarded the outer object's literal hashref
|
|
220
|
+
// instead of falling through to the per-iteration `$attrs` value. Fixed
|
|
221
|
+
// with the same `loopBoundNames` guard as the other two call sites.
|
|
222
|
+
//
|
|
223
|
+
// Not covered here (upstream, shared-compiler hazard, out of this
|
|
224
|
+
// package's scope): `key={label}` shadowed by an enclosing loop param of
|
|
225
|
+
// the same name is folded to the OUTER const's literal at IR-generation
|
|
226
|
+
// time (`tryResolveIdentifierAsTemplateLiteral` → `findLocalConst` in
|
|
227
|
+
// `packages/jsx/src/jsx-to-ir.ts`), before any adapter runs — so this
|
|
228
|
+
// adapter (and every other adapter, including Hono's native JSX
|
|
229
|
+
// re-emission) still renders a `key`/`data-key` value shadowed this way
|
|
230
|
+
// as the outer literal, unconditionally, every iteration.
|
|
231
|
+
describe('MojoAdapter - const inlining vs loop-param shadowing (#2221)', () => {
|
|
232
|
+
test('a loop param shadowing an outer literal const emits the identifier, not the const value', () => {
|
|
233
|
+
const { template } = compileAndGenerate(`
|
|
234
|
+
function Widget() {
|
|
235
|
+
const label: string = 'x'
|
|
236
|
+
return <ul>{[2, 5].map((label) => <li key={label}>{1 + label}</li>)}</ul>
|
|
237
|
+
}
|
|
238
|
+
`)
|
|
239
|
+
expect(template).toContain('1 + $label')
|
|
240
|
+
expect(template).not.toContain("1 + 'x'")
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
test('a numeric const shadowed by a loop param emits the identifier too', () => {
|
|
244
|
+
const { template } = compileAndGenerate(`
|
|
245
|
+
function Widget() {
|
|
246
|
+
const count = 7
|
|
247
|
+
return <ul>{[2, 5].map((count) => <li key={count}>{1 + count}</li>)}</ul>
|
|
248
|
+
}
|
|
249
|
+
`)
|
|
250
|
+
expect(template).toContain('1 + $count')
|
|
251
|
+
expect(template).not.toContain('1 + 7')
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
test('a literal const NOT shadowed by any loop still inlines (#1897 pin)', () => {
|
|
255
|
+
const { template } = compileAndGenerate(`
|
|
256
|
+
function Widget({ values }: { values: number[] }) {
|
|
257
|
+
const totalPages = 5
|
|
258
|
+
return <div>
|
|
259
|
+
<p>Page 1 of {1 + totalPages}</p>
|
|
260
|
+
<ul>{values.map((v) => <li key={v}>{v}</li>)}</ul>
|
|
261
|
+
</div>
|
|
262
|
+
}
|
|
263
|
+
`)
|
|
264
|
+
expect(template).toContain('1 + 5')
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
// Unlike the Twig-family's coarse-but-safe `collectLoopBoundNames(ir)`
|
|
268
|
+
// exclusion, this adapter's LIVE `loopBoundNames` tracking is scoped to
|
|
269
|
+
// the actual render position: a name loop-bound ONLY inside the `.map`
|
|
270
|
+
// callback still inlines correctly at a separate, non-shadowed
|
|
271
|
+
// occurrence outside the loop — no accepted trade-off here.
|
|
272
|
+
test('a const referenced outside the loop whose name is loop-bound elsewhere still inlines (more precise than Twig family)', () => {
|
|
273
|
+
const { template } = compileAndGenerate(`
|
|
274
|
+
function Widget({ values }: { values: number[] }) {
|
|
275
|
+
const label: string = 'x'
|
|
276
|
+
return <div>
|
|
277
|
+
<p>{1 + label}</p>
|
|
278
|
+
<ul>{values.map((label) => <li key={label}>{2 + label}</li>)}</ul>
|
|
279
|
+
</div>
|
|
280
|
+
}
|
|
281
|
+
`)
|
|
282
|
+
expect(template).toContain("1 + 'x'")
|
|
283
|
+
expect(template).toContain('2 + $label')
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
// The actual gap this issue found in this adapter: `emitSpread`'s
|
|
287
|
+
// bare-identifier local-const resolution (`{...attrs}` → the outer
|
|
288
|
+
// conditional object's hashref) had no `loopBoundNames` guard.
|
|
289
|
+
test('a loop param shadowing an outer conditional-object const spread emits the loop var, not the outer hashref', () => {
|
|
290
|
+
const { template } = compileAndGenerate(`
|
|
291
|
+
function Widget({ items }: { items: object[] }) {
|
|
292
|
+
const attrs = true ? { 'data-on': 'outer' } : {}
|
|
293
|
+
return <ul>{items.map((attrs) => <li {...attrs} />)}</ul>
|
|
294
|
+
}
|
|
295
|
+
`)
|
|
296
|
+
expect(template).toContain('bf->spread_attrs($attrs)')
|
|
297
|
+
expect(template).not.toContain("'data-on' => 'outer'")
|
|
298
|
+
})
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
// #2237: the record-literal sibling of #2221's `resolveLiteralConst` bug —
|
|
302
|
+
// `resolveStaticRecordLiteral` (`IDENT.key` on a module-scope object-literal
|
|
303
|
+
// const, e.g. `variantClasses.ghost` — #1896/#1897) is confirmed reproducible
|
|
304
|
+
// on the Twig-family adapters (flat `objectName` lookup with no notion of AST
|
|
305
|
+
// scope, so an enclosing loop callback's own param of the same name resolved
|
|
306
|
+
// to the OUTER const's member value at every iteration). This adapter's
|
|
307
|
+
// `resolveStaticRecordLiteral` already guards against it (mojo-adapter.ts:
|
|
308
|
+
// `if (this.loopBoundNames?.has?.(objectName)) return null`) — the same LIVE,
|
|
309
|
+
// ref-counted `loopBoundNames` map `resolveLiteralConst` consults (#1749),
|
|
310
|
+
// scope-precise rather than the Twig family's coarse whole-component set.
|
|
311
|
+
// Pinned here (mirroring the #2221 scope-precision pin above) rather than
|
|
312
|
+
// fixed, since no code change was needed.
|
|
313
|
+
describe('MojoAdapter - record-literal member lookup vs loop-param shadowing (#2237)', () => {
|
|
314
|
+
test('a loop param shadowing an outer module object const emits the member access, not the outer literal', () => {
|
|
315
|
+
const { template } = compileAndGenerate(`
|
|
316
|
+
const cfg = { x: 'outer-lit' }
|
|
317
|
+
function Widget({ rows }: { rows: { x: string }[] }) {
|
|
318
|
+
return <ul>{rows.map((cfg) => <li key={cfg.x}>{cfg.x}</li>)}</ul>
|
|
319
|
+
}
|
|
320
|
+
`)
|
|
321
|
+
// The loop body must reference the per-iteration member access...
|
|
322
|
+
expect(template).toContain('$cfg->{x}')
|
|
323
|
+
// ...never the outer const's hard-coded value.
|
|
324
|
+
expect(template).not.toContain("'outer-lit'")
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
test('a module object const NOT shadowed by any loop still inlines (variantClasses.ghost shape, #1896/#1897 pin)', () => {
|
|
328
|
+
const { template } = compileAndGenerate(`
|
|
329
|
+
const variantClasses = { solid: 'bg-solid', ghost: 'bg-ghost' }
|
|
330
|
+
function Widget({ variant }: { variant: 'solid' | 'ghost' }) {
|
|
331
|
+
return <div>{variantClasses.ghost}</div>
|
|
332
|
+
}
|
|
333
|
+
`)
|
|
334
|
+
expect(template).toContain("'bg-ghost'")
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
// Unlike the Twig-family's coarse-but-safe `staticLoopSourceBoundNames`
|
|
338
|
+
// exclusion, this adapter's LIVE `loopBoundNames` tracking is scoped to
|
|
339
|
+
// the actual render position: an object name loop-bound ONLY inside the
|
|
340
|
+
// `.map` callback still inlines its member lookup correctly at a
|
|
341
|
+
// separate, non-shadowed occurrence outside the loop — no accepted
|
|
342
|
+
// trade-off here.
|
|
343
|
+
test('an object name loop-bound only inside the loop still inlines its member lookup outside it (more precise than Twig family)', () => {
|
|
344
|
+
const { template } = compileAndGenerate(`
|
|
345
|
+
const cfg = { x: 'outer-lit' }
|
|
346
|
+
function Widget({ rows }: { rows: { x: string }[] }) {
|
|
347
|
+
return <div>
|
|
348
|
+
<p>{cfg.x}</p>
|
|
349
|
+
<ul>{rows.map((cfg) => <li key={cfg.x}>{cfg.x}</li>)}</ul>
|
|
350
|
+
</div>
|
|
351
|
+
}
|
|
352
|
+
`)
|
|
353
|
+
expect(template).toContain("<p><%= 'outer-lit' %></p>")
|
|
354
|
+
expect(template).toContain('$cfg->{x}')
|
|
355
|
+
})
|
|
356
|
+
})
|
|
357
|
+
|
|
198
358
|
describe('MojoAdapter - Record<staticKeys,scalar>[propKey] spread value (#checkbox icon)', () => {
|
|
199
359
|
// `const sizeMap: Record<IconSize, number> = { sm: 16, ... }` indexed by
|
|
200
360
|
// a prop inside a conditional-spread object value lowers to an inline
|
|
@@ -1163,7 +1323,7 @@ export function Foo(props: { v: string }) {
|
|
|
1163
1323
|
// list rather than replace.
|
|
1164
1324
|
const a = new MojoAdapter()
|
|
1165
1325
|
const keys = Object.keys(a.templatePrimitives ?? {}).sort()
|
|
1166
|
-
expect(keys).toEqual(['JSON.stringify', 'Math.ceil', 'Math.floor', 'Math.round', 'Number', 'String'])
|
|
1326
|
+
expect(keys).toEqual(['JSON.stringify', 'Math.abs', 'Math.ceil', 'Math.floor', 'Math.max', 'Math.min', 'Math.round', 'Number', 'String'])
|
|
1167
1327
|
})
|
|
1168
1328
|
|
|
1169
1329
|
test('unregistered identifier-path callee is NOT accepted', () => {
|
|
@@ -1406,7 +1566,7 @@ describe('MojoAdapter - #1448 Tier C .flat(depth?)', () => {
|
|
|
1406
1566
|
function emitFlat(expr: string): string {
|
|
1407
1567
|
const a = new MojoAdapter()
|
|
1408
1568
|
const ir = compileToIR(`
|
|
1409
|
-
function C({ rows }: { rows:
|
|
1569
|
+
function C({ rows }: { rows: { x: string }[][] }) {
|
|
1410
1570
|
return <div>{${expr}}</div>
|
|
1411
1571
|
}
|
|
1412
1572
|
export { C }
|
|
@@ -1526,9 +1686,10 @@ export { C }
|
|
|
1526
1686
|
expect(t).toContain(`"property":"name"`)
|
|
1527
1687
|
})
|
|
1528
1688
|
|
|
1529
|
-
// The function-reference `.map(format)`
|
|
1530
|
-
//
|
|
1531
|
-
//
|
|
1689
|
+
// The function-reference `.map(format)` case is now covered cross-adapter
|
|
1690
|
+
// by the `array-map-function-reference` shared fixture — `format` resolves
|
|
1691
|
+
// to its declaration (#2206) and the fixture compiles clean rather than
|
|
1692
|
+
// refusing with BF101.
|
|
1532
1693
|
})
|
|
1533
1694
|
|
|
1534
1695
|
describe('MojoAdapter - #1448 Tier C .flatMap(field projection)', () => {
|
|
@@ -1902,3 +2063,23 @@ export function C() {
|
|
|
1902
2063
|
expect(deferred.template).not.toContain('data-x')
|
|
1903
2064
|
})
|
|
1904
2065
|
})
|
|
2066
|
+
|
|
2067
|
+
describe('MojoAdapter - named-slot capture identifier safety (#2168 jsx-element-prop)', () => {
|
|
2068
|
+
// A JSX-valued prop under a hyphenated name (`data-slot`, a valid JSX
|
|
2069
|
+
// attribute name) must not leak into the `begin %>...<% end` capture
|
|
2070
|
+
// variable — Perl variable tokens can't contain `-`. The capture
|
|
2071
|
+
// variable is purely counter-based (never derived from the prop name);
|
|
2072
|
+
// the hash KEY passed to `render_child` still carries the real name,
|
|
2073
|
+
// quoted via `perlHashKey`.
|
|
2074
|
+
test('a hyphenated prop name does not appear in the capture variable', () => {
|
|
2075
|
+
const { template } = compileAndGenerate(`
|
|
2076
|
+
function Card(props) { return null }
|
|
2077
|
+
export function Parent() {
|
|
2078
|
+
return <Card data-slot={<strong>Title</strong>}>text</Card>
|
|
2079
|
+
}
|
|
2080
|
+
`)
|
|
2081
|
+
expect(template).toContain('<% my $bf_prop_0 = begin %>')
|
|
2082
|
+
expect(template).toContain("'data-slot' => $bf_prop_0")
|
|
2083
|
+
expect(template).not.toContain('$bf_prop_data')
|
|
2084
|
+
})
|
|
2085
|
+
})
|
|
@@ -139,6 +139,15 @@ export function renderArrayMethod(
|
|
|
139
139
|
const recv = emit(object)
|
|
140
140
|
return `bf->trim(${recv})`
|
|
141
141
|
}
|
|
142
|
+
case 'trimStart':
|
|
143
|
+
case 'trimEnd': {
|
|
144
|
+
// `.trimStart()` / `.trimEnd()` — the one-sided siblings of
|
|
145
|
+
// `.trim()` (#2183 follow-up). Dedicated `bf->trim_start` /
|
|
146
|
+
// `bf->trim_end` helpers, not `bf->trim` with a flag.
|
|
147
|
+
const fn = method === 'trimStart' ? 'trim_start' : 'trim_end'
|
|
148
|
+
const recv = emit(object)
|
|
149
|
+
return `bf->${fn}(${recv})`
|
|
150
|
+
}
|
|
142
151
|
case 'toFixed': {
|
|
143
152
|
// `.toFixed(digits?)` — Number → fixed-decimal string. `bf->to_fixed`
|
|
144
153
|
// mirrors JS rounding + zero-padding (default 0 digits). #1897.
|
|
@@ -195,6 +204,16 @@ export function renderArrayMethod(
|
|
|
195
204
|
const newS = emit(args[1])
|
|
196
205
|
return `bf->replace(${recv}, ${oldS}, ${newS})`
|
|
197
206
|
}
|
|
207
|
+
case 'replaceAll': {
|
|
208
|
+
// `.replaceAll(old, new)` — string-pattern form, EVERY occurrence,
|
|
209
|
+
// via the dedicated `bf->replace_all` helper (not `bf->replace`
|
|
210
|
+
// with a flag) — the regex-pattern form is refused upstream at
|
|
211
|
+
// the parser, same as `.replace`. See #2182.
|
|
212
|
+
const recv = emit(object)
|
|
213
|
+
const oldS = emit(args[0])
|
|
214
|
+
const newS = emit(args[1])
|
|
215
|
+
return `bf->replace_all(${recv}, ${oldS}, ${newS})`
|
|
216
|
+
}
|
|
198
217
|
case 'repeat': {
|
|
199
218
|
// `.repeat(n)` — string repeated `n` times. The `bf->repeat`
|
|
200
219
|
// helper wraps Perl's `x` operator with the same negative-count
|
|
@@ -105,7 +105,7 @@ export class MojoFilterEmitter implements ParsedExprEmitter {
|
|
|
105
105
|
return String(value)
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
member(object: ParsedExpr, property: string, _computed: boolean, emit: (e: ParsedExpr) => string): string {
|
|
108
|
+
member(object: ParsedExpr, property: string, _computed: boolean, _optional: boolean, emit: (e: ParsedExpr) => string): string {
|
|
109
109
|
// `.length` on a higher-order result (e.g.
|
|
110
110
|
// `x.tags.filter(t => t.active).length > 0` inside the outer
|
|
111
111
|
// filter predicate, #1443). The higher-order emit produces an
|
|
@@ -322,7 +322,7 @@ export class MojoTopLevelEmitter implements ParsedExprEmitter {
|
|
|
322
322
|
return String(value)
|
|
323
323
|
}
|
|
324
324
|
|
|
325
|
-
member(object: ParsedExpr, property: string, _computed: boolean, emit: (e: ParsedExpr) => string): string {
|
|
325
|
+
member(object: ParsedExpr, property: string, _computed: boolean, _optional: boolean, emit: (e: ParsedExpr) => string): string {
|
|
326
326
|
// `props.x` flattens to the bare `$x` the Mojo SSR caller binds each
|
|
327
327
|
// prop to (props arrive as individual `my $x = ...` vars, not a
|
|
328
328
|
// `$props` hashref).
|
|
@@ -338,7 +338,22 @@ export class MojoTopLevelEmitter implements ParsedExprEmitter {
|
|
|
338
338
|
if (staticValue !== null) return staticValue
|
|
339
339
|
}
|
|
340
340
|
const obj = emit(object)
|
|
341
|
-
if (property === 'length')
|
|
341
|
+
if (property === 'length') {
|
|
342
|
+
// `.length` dispatches on receiver type: a STRING receiver needs
|
|
343
|
+
// Perl's scalar `length($x)`, while the array lowering
|
|
344
|
+
// `scalar(@{$x})` dereferences the value as an array ref and
|
|
345
|
+
// returns 0 for a scalar string (the `string-length-text`
|
|
346
|
+
// divergence). The receiver is string-typed when it's a known
|
|
347
|
+
// string prop/getter (`isStringTypedOperand`) or a bare
|
|
348
|
+
// identifier bound to one — the same `_isStringValueName` witness
|
|
349
|
+
// the `eq`/concat lowering already consults.
|
|
350
|
+
const isStr = (e: ParsedExpr) => isStringTypedOperand(e, n => this.ctx._isStringValueName(n))
|
|
351
|
+
const isStringReceiver =
|
|
352
|
+
isStr(object) ||
|
|
353
|
+
(object.kind === 'identifier' && this.ctx._isStringValueName(object.name))
|
|
354
|
+
if (isStringReceiver) return `length(${obj})`
|
|
355
|
+
return `scalar(@{${obj}})`
|
|
356
|
+
}
|
|
342
357
|
return `${obj}->{${property}}`
|
|
343
358
|
}
|
|
344
359
|
|
|
@@ -26,6 +26,9 @@ export const MOJO_TEMPLATE_PRIMITIVES: Record<string, PrimitiveSpec> = {
|
|
|
26
26
|
'Math.floor': { arity: 1, emit: (args) => `bf->floor(${args[0]})` },
|
|
27
27
|
'Math.ceil': { arity: 1, emit: (args) => `bf->ceil(${args[0]})` },
|
|
28
28
|
'Math.round': { arity: 1, emit: (args) => `bf->round(${args[0]})` },
|
|
29
|
+
'Math.min': { arity: 2, emit: (args) => `bf->min(${args[0]}, ${args[1]})` },
|
|
30
|
+
'Math.max': { arity: 2, emit: (args) => `bf->max(${args[0]}, ${args[1]})` },
|
|
31
|
+
'Math.abs': { arity: 1, emit: (args) => `bf->abs(${args[0]})` },
|
|
29
32
|
}
|
|
30
33
|
|
|
31
34
|
/**
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serialize a compile-time-evaluated JS value (`@barefootjs/jsx`'s
|
|
3
|
+
* `evaluateStaticLiteral`/`resolveStaticLoopSource`, #2208) into a native
|
|
4
|
+
* Perl literal. Used to inline a fully-static loop source (an inline array
|
|
5
|
+
* literal, or a function-scope local const with a static initializer)
|
|
6
|
+
* directly in the loop-bound expression, rather than requiring a bound
|
|
7
|
+
* template variable.
|
|
8
|
+
*
|
|
9
|
+
* Booleans deliberately return `null` (defer to the caller's BF101
|
|
10
|
+
* refusal) rather than baking `1`/`''` — Perl has no boolean literal, and
|
|
11
|
+
* that would diverge from JS's `String(true) === "true"` at render.
|
|
12
|
+
*
|
|
13
|
+
* Returns `null` for a value this adapter can't represent as a literal —
|
|
14
|
+
* the caller falls back to its existing BF101 refusal instead of guessing.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { perlHashKey } from './perl-naming.ts'
|
|
18
|
+
|
|
19
|
+
function escapePerlSingleQuote(s: string): string {
|
|
20
|
+
return s.replace(/\\/g, '\\\\').replace(/'/g, "\\'")
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function staticValueToPerl(value: unknown): string | null {
|
|
24
|
+
if (value === null || value === undefined) return 'undef'
|
|
25
|
+
if (typeof value === 'boolean') return null
|
|
26
|
+
if (typeof value === 'number') return String(value)
|
|
27
|
+
if (typeof value === 'string') return `'${escapePerlSingleQuote(value)}'`
|
|
28
|
+
if (Array.isArray(value)) {
|
|
29
|
+
const items: string[] = []
|
|
30
|
+
for (const el of value) {
|
|
31
|
+
const serialized = staticValueToPerl(el)
|
|
32
|
+
if (serialized === null) return null
|
|
33
|
+
items.push(serialized)
|
|
34
|
+
}
|
|
35
|
+
return `[${items.join(', ')}]`
|
|
36
|
+
}
|
|
37
|
+
if (typeof value === 'object') {
|
|
38
|
+
const entries: string[] = []
|
|
39
|
+
for (const [key, val] of Object.entries(value as Record<string, unknown>)) {
|
|
40
|
+
const serialized = staticValueToPerl(val)
|
|
41
|
+
if (serialized === null) return null
|
|
42
|
+
entries.push(`${perlHashKey(key)} => ${serialized}`)
|
|
43
|
+
}
|
|
44
|
+
return `{ ${entries.join(', ')} }`
|
|
45
|
+
}
|
|
46
|
+
return null
|
|
47
|
+
}
|
|
@@ -59,10 +59,15 @@ import {
|
|
|
59
59
|
isValidHelperId,
|
|
60
60
|
sortComparatorFromArrow,
|
|
61
61
|
isLowerableLoopDestructure,
|
|
62
|
+
isDangerousInnerHtmlAttr,
|
|
63
|
+
resolveDangerousInnerHtml,
|
|
64
|
+
dangerousInnerHtmlMetacharViolation,
|
|
65
|
+
dangerousInnerHtmlDiagnostic,
|
|
66
|
+
resolveStaticLoopSource,
|
|
62
67
|
} from '@barefootjs/jsx'
|
|
63
68
|
import { isAriaBooleanAttr, isBooleanResultExpr } from './boolean-result.ts'
|
|
64
69
|
import type { ParsedExpr, LoweringMatcher } from '@barefootjs/jsx'
|
|
65
|
-
import { BF_SLOT, BF_COND, BF_REGION } from '@barefootjs/shared'
|
|
70
|
+
import { BF_SLOT, BF_COND, BF_REGION, escapeHtml } from '@barefootjs/shared'
|
|
66
71
|
|
|
67
72
|
import type { MojoRenderCtx } from './lib/types.ts'
|
|
68
73
|
import { MOJO_PRIMITIVE_EMIT_MAP } from './lib/constants.ts'
|
|
@@ -72,6 +77,7 @@ import {
|
|
|
72
77
|
collectRootScopeNodes,
|
|
73
78
|
} from './lib/ir-scope.ts'
|
|
74
79
|
import { renderSortMethod, renderSortEval } from './expr/array-method.ts'
|
|
80
|
+
import { staticValueToPerl } from './lib/static-value.ts'
|
|
75
81
|
import { MojoFilterEmitter, MojoTopLevelEmitter } from './expr/emitters.ts'
|
|
76
82
|
import type { MojoEmitContext, MojoSpreadContext, MojoMemoContext } from './emit-context.ts'
|
|
77
83
|
import {
|
|
@@ -165,6 +171,14 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
165
171
|
private options: Required<MojoAdapterOptions>
|
|
166
172
|
private errors: CompilerError[] = []
|
|
167
173
|
private inLoop: boolean = false
|
|
174
|
+
/**
|
|
175
|
+
* `IRLoop.depth` of the loop currently being rendered (save/restore
|
|
176
|
+
* around `renderChildren(loop.children)`, mirroring `inLoop` above).
|
|
177
|
+
* `renderAttributes` reads this to derive the `key` → `data-key`/
|
|
178
|
+
* `data-key-N` suffix — the depth is IR-computed (jsx-to-ir.ts), not
|
|
179
|
+
* re-derived here (#2168 nested-loop-outer-binding).
|
|
180
|
+
*/
|
|
181
|
+
private currentLoopKeyDepth = 0
|
|
168
182
|
/**
|
|
169
183
|
* SolidJS-style props identifier (`function(props: P)`) and the
|
|
170
184
|
* analyzer-extracted prop names. Stashed at `generate()` entry so
|
|
@@ -407,6 +421,14 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
407
421
|
* quoted string literal (`const totalPages = 5`, #1897 pagination) —
|
|
408
422
|
* function-scope consts never reach the per-render stash, so a bare
|
|
409
423
|
* `$totalPages` faults under strict mode.
|
|
424
|
+
*
|
|
425
|
+
* The `loopBoundNames` guard also covers the #2221 hazard (a loop
|
|
426
|
+
* callback's own param shadowing this outer const's name): unlike the
|
|
427
|
+
* Twig-family adapters' coarse, whole-component `collectLoopBoundNames(ir)`
|
|
428
|
+
* static set, this adapter's `loopBoundNames` is a LIVE ref-counted map
|
|
429
|
+
* `renderLoop` populates/depopulates as it descends/ascends into each
|
|
430
|
+
* loop body (#1749) — so it's already scope-precise for this call site;
|
|
431
|
+
* no separate `staticLoopSourceBoundNames`-style field is needed here.
|
|
410
432
|
*/
|
|
411
433
|
private resolveLiteralConst(name: string): string | null {
|
|
412
434
|
if (this.loopBoundNames?.has?.(name)) return null
|
|
@@ -478,7 +500,9 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
478
500
|
}
|
|
479
501
|
|
|
480
502
|
emitText(node: IRText): string {
|
|
481
|
-
|
|
503
|
+
// IRText carries the entity-DECODED value (Phase 1 decodes JSX
|
|
504
|
+
// character references); re-escape for direct HTML emission.
|
|
505
|
+
return escapeHtml(node.value)
|
|
482
506
|
}
|
|
483
507
|
|
|
484
508
|
emitExpression(node: IRExpression): string {
|
|
@@ -607,7 +631,8 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
607
631
|
renderElement(element: IRElement): string {
|
|
608
632
|
const tag = element.tag
|
|
609
633
|
const attrs = this.renderAttributes(element)
|
|
610
|
-
const
|
|
634
|
+
const dangerousHtml = this.renderDangerousInnerHtml(element)
|
|
635
|
+
const children = dangerousHtml !== null ? dangerousHtml : this.renderChildren(element.children)
|
|
611
636
|
|
|
612
637
|
let hydrationAttrs = ''
|
|
613
638
|
if (element.needsScope) {
|
|
@@ -643,6 +668,28 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
643
668
|
return `<${tag}${attrs}${hydrationAttrs}>${children}</${tag}>`
|
|
644
669
|
}
|
|
645
670
|
|
|
671
|
+
/**
|
|
672
|
+
* `dangerouslySetInnerHTML={{ __html: '...' }}` (#2207) — see the Blade
|
|
673
|
+
* adapter's identical helper for the full rationale. `null` means the
|
|
674
|
+
* attribute is absent (caller falls through to normal `renderChildren`);
|
|
675
|
+
* a non-`null` string (possibly `''`) replaces the children outright.
|
|
676
|
+
*/
|
|
677
|
+
private renderDangerousInnerHtml(element: IRElement): string | null {
|
|
678
|
+
const resolution = resolveDangerousInnerHtml(element)
|
|
679
|
+
if (!resolution) return null
|
|
680
|
+
if (resolution.kind === 'dynamic') {
|
|
681
|
+
this.errors.push(dangerousInnerHtmlDiagnostic(resolution.expr, resolution.loc))
|
|
682
|
+
return ''
|
|
683
|
+
}
|
|
684
|
+
const violation = dangerousInnerHtmlMetacharViolation(resolution.html, this.name)
|
|
685
|
+
if (violation) {
|
|
686
|
+
const attr = element.attrs.find(isDangerousInnerHtmlAttr)!
|
|
687
|
+
this.errors.push(dangerousInnerHtmlDiagnostic(`{ __html: ${JSON.stringify(resolution.html)} }`, attr.loc, violation))
|
|
688
|
+
return ''
|
|
689
|
+
}
|
|
690
|
+
return resolution.html
|
|
691
|
+
}
|
|
692
|
+
|
|
646
693
|
// ===========================================================================
|
|
647
694
|
// Expression Rendering
|
|
648
695
|
// ===========================================================================
|
|
@@ -655,7 +702,12 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
655
702
|
return ''
|
|
656
703
|
}
|
|
657
704
|
|
|
658
|
-
|
|
705
|
+
// Thread the IR-carried `.parsed` tree through (mirrors go-template's
|
|
706
|
+
// `convertExpressionToGo(expr.expr, classify, expr.parsed)`) so a
|
|
707
|
+
// resolved bare-identifier `.map`/`.filter`/… callback
|
|
708
|
+
// (`resolveCallbackMethodFunctionReferences`, #2206) isn't lost to a
|
|
709
|
+
// fresh, unresolved re-parse of the raw string.
|
|
710
|
+
const perlExpr = this.convertExpressionToPerl(expr.expr, expr.parsed)
|
|
659
711
|
|
|
660
712
|
if (expr.slotId) {
|
|
661
713
|
return `<%== bf->text_start("${expr.slotId}") %><%= ${perlExpr} %><%== bf->text_end %>`
|
|
@@ -800,8 +852,24 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
800
852
|
// reachable in this adapter's test corpus only because the widened
|
|
801
853
|
// destructure gate (#2087 Phase A/B) no longer refuses this fixture's
|
|
802
854
|
// `([emoji, users]) => ...` param first.
|
|
855
|
+
// #2208: a loop source that is a fully-static array literal — either
|
|
856
|
+
// inline (`[{ label: 'Alpha' }, ...].map(...)`) or a bare identifier
|
|
857
|
+
// bound to a FUNCTION-scope local const whose initializer has no
|
|
858
|
+
// prop/signal/function-call dependency — inlines as a native Perl
|
|
859
|
+
// arrayref/hashref literal below, the same way a module-scope const's
|
|
860
|
+
// value is already seeded. A runtime-computed local (#2069, e.g.
|
|
861
|
+
// `Object.entries(props.tags).filter(...)`) still refuses below.
|
|
862
|
+
// `isNameShadowed` guards a DIFFERENT, enclosing loop's own callback
|
|
863
|
+
// param shadowing this identifier (fable review) — reuses the same
|
|
864
|
+
// live `loopBoundNames` ref-counted tracking `resolveModuleStringConst`
|
|
865
|
+
// already consults for this hazard class (#1749).
|
|
866
|
+
const staticItems = resolveStaticLoopSource(loop.arrayParsed, this.localConstants, {
|
|
867
|
+
isNameShadowed: name => this.loopBoundNames.has(name),
|
|
868
|
+
})
|
|
869
|
+
const staticArray = staticItems !== null ? staticValueToPerl(staticItems) : null
|
|
870
|
+
|
|
803
871
|
const arrayName = loop.array.trim()
|
|
804
|
-
if (/^[A-Za-z_$][\w$]*$/.test(arrayName)) {
|
|
872
|
+
if (staticArray === null && /^[A-Za-z_$][\w$]*$/.test(arrayName)) {
|
|
805
873
|
const arrayConst = (this.localConstants ?? []).find(c => c.name === arrayName)
|
|
806
874
|
if (arrayConst && !arrayConst.isModule && this.resolveLiteralConst(arrayName) === null) {
|
|
807
875
|
this.errors.push({
|
|
@@ -817,7 +885,7 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
817
885
|
}
|
|
818
886
|
}
|
|
819
887
|
|
|
820
|
-
const rawArray = this.convertExpressionToPerl(loop.array)
|
|
888
|
+
const rawArray = staticArray ?? this.convertExpressionToPerl(loop.array)
|
|
821
889
|
// Apply sort if present (#1448 Tier B): wrap the loop array in the
|
|
822
890
|
// shared sort helper. The same `renderSortEval` / `renderSortMethod`
|
|
823
891
|
// pair feeds both this loop-chain hoist and the emitter's
|
|
@@ -846,17 +914,22 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
846
914
|
// the whole body (children + key + filter) so a same-named loop variable
|
|
847
915
|
// isn't replaced by the const literal (#1749 review). Ref-counted for
|
|
848
916
|
// nested loops; released after the body lines are assembled below.
|
|
849
|
-
const loopBound = loop.
|
|
850
|
-
? [param]
|
|
851
|
-
:
|
|
852
|
-
? [
|
|
853
|
-
:
|
|
917
|
+
const loopBound = loop.objectIteration === 'entries'
|
|
918
|
+
? [param, loop.index ?? '_k']
|
|
919
|
+
: loop.objectIteration === 'keys' || loop.objectIteration === 'values' || loop.iterationShape === 'keys'
|
|
920
|
+
? [param]
|
|
921
|
+
: supportableDestructure
|
|
922
|
+
? ['__bf_item', ...(loop.paramBindings ?? []).map(b => b.name), loop.index ?? '_i']
|
|
923
|
+
: [param, loop.index ?? '_i']
|
|
854
924
|
for (const n of loopBound) {
|
|
855
925
|
this.loopBoundNames.set(n, (this.loopBoundNames.get(n) ?? 0) + 1)
|
|
856
926
|
}
|
|
857
927
|
const prevInLoop = this.inLoop
|
|
858
928
|
this.inLoop = true
|
|
929
|
+
const prevLoopKeyDepth = this.currentLoopKeyDepth
|
|
930
|
+
this.currentLoopKeyDepth = loop.depth
|
|
859
931
|
const renderedChildren = this.renderChildren(loop.children)
|
|
932
|
+
this.currentLoopKeyDepth = prevLoopKeyDepth
|
|
860
933
|
this.inLoop = prevInLoop
|
|
861
934
|
|
|
862
935
|
// Whole-item conditional (#1665): prepend an always-present
|
|
@@ -918,6 +991,21 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
918
991
|
}
|
|
919
992
|
lines.push(`% my $${sortedHoist} = ${sorted};`)
|
|
920
993
|
}
|
|
994
|
+
if (loop.objectIteration) {
|
|
995
|
+
// `objectIteration` (#2168 object-entries-map): a Perl hash has no
|
|
996
|
+
// native insertion-order guarantee (unlike Ruby's `Hash`/Python's
|
|
997
|
+
// `dict`) — this codebase's own runtime already works around this
|
|
998
|
+
// elsewhere with `sort keys %$hash` (`BarefootJS.pm`'s
|
|
999
|
+
// `spread_attrs`/`_style_to_css`), so this reuses that exact
|
|
1000
|
+
// convention for a deterministic, alphabetically-sorted iteration
|
|
1001
|
+
// (not JS insertion order — a documented known limitation for
|
|
1002
|
+
// out-of-alphabetical-order data, same as Go/Rust/Xslate).
|
|
1003
|
+
const keyVar = loop.objectIteration === 'values' ? '$__bf_k' : `$${loop.index ?? param}`
|
|
1004
|
+
lines.push(`% for my ${keyVar} (sort keys %{${array}}) {`)
|
|
1005
|
+
if (loop.objectIteration === 'entries' || loop.objectIteration === 'values') {
|
|
1006
|
+
lines.push(`% my $${param} = ${array}->{${keyVar}};`)
|
|
1007
|
+
}
|
|
1008
|
+
} else {
|
|
921
1009
|
lines.push(`% for my ${indexVar} (0..$#{${array}}) {`)
|
|
922
1010
|
if (loop.iterationShape !== 'keys') {
|
|
923
1011
|
if (supportableDestructure) {
|
|
@@ -951,6 +1039,7 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
951
1039
|
lines.push(`% my $${param} = ${array}->[${indexVar}];`)
|
|
952
1040
|
}
|
|
953
1041
|
}
|
|
1042
|
+
}
|
|
954
1043
|
|
|
955
1044
|
// Handle filter().map() pattern by wrapping children in if-condition
|
|
956
1045
|
if (loop.filterPredicate) {
|
|
@@ -1049,10 +1138,36 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
1049
1138
|
|
|
1050
1139
|
renderComponent(comp: IRComponent): string {
|
|
1051
1140
|
const propParts: string[] = []
|
|
1141
|
+
// Named JSX-valued props OTHER than the reserved `children`
|
|
1142
|
+
// (`header={<strong>Title</strong>}`, #2168 jsx-element-prop) get the
|
|
1143
|
+
// same `begin %>…<% end` capture as the reserved children slot below,
|
|
1144
|
+
// just keyed by the prop's own name. `render_child` (BarefootJS.pm)
|
|
1145
|
+
// materializes every prop value that's a CODE ref — not only
|
|
1146
|
+
// `children` — into the Mojo::ByteStream the capture block produces,
|
|
1147
|
+
// so the child's read of the slot back out (`<%= $header %>`) sees an
|
|
1148
|
+
// already-safe ByteStream and Mojo::Template's auto-escape passes it
|
|
1149
|
+
// through unescaped, the same way it already does for `children`.
|
|
1150
|
+
const namedSlotCaptures: string[] = []
|
|
1052
1151
|
for (const p of comp.props) {
|
|
1053
1152
|
// Skip callback props (onXxx) and `ref` — both are client-only for
|
|
1054
1153
|
// SSR (Hono renders neither; the client JS wires them at hydration).
|
|
1055
1154
|
if ((p.name.match(/^on[A-Z]/) || p.name === 'ref') && p.value.kind === 'expression') continue
|
|
1155
|
+
if (p.value.kind === 'jsx-children' && p.name !== 'children') {
|
|
1156
|
+
const prevInLoop = this.inLoop
|
|
1157
|
+
this.inLoop = false
|
|
1158
|
+
const slotBody = this.renderChildren(p.value.children)
|
|
1159
|
+
this.inLoop = prevInLoop
|
|
1160
|
+
// Purely counter-based — NOT derived from `p.name` or `comp.slotId`.
|
|
1161
|
+
// A JSX prop name can contain characters (`data-slot`) that aren't a
|
|
1162
|
+
// valid Perl variable token, and `comp.slotId` alone would collide
|
|
1163
|
+
// across two named-slot props on the same component invocation
|
|
1164
|
+
// (unlike the reserved children slot, there's only ever one of
|
|
1165
|
+
// those per invocation).
|
|
1166
|
+
const varName = `$bf_prop_${this.childrenCaptureCounter++}`
|
|
1167
|
+
namedSlotCaptures.push(`<% my ${varName} = begin %>${slotBody}<% end %>`)
|
|
1168
|
+
propParts.push(`${perlHashKey(p.name)} => ${varName}`)
|
|
1169
|
+
continue
|
|
1170
|
+
}
|
|
1056
1171
|
const lowered = emitAttrValue(p.value, this.componentPropEmitter, p.name)
|
|
1057
1172
|
if (lowered) propParts.push(lowered)
|
|
1058
1173
|
}
|
|
@@ -1090,9 +1205,9 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
1090
1205
|
const childrenBody = this.renderChildren(effectiveChildren)
|
|
1091
1206
|
this.inLoop = prevInLoop
|
|
1092
1207
|
const varName = `$bf_children_${comp.slotId ?? 'c' + this.childrenCaptureCounter++}`
|
|
1093
|
-
return
|
|
1208
|
+
return `${namedSlotCaptures.join('')}<% my ${varName} = begin %>${childrenBody}<% end %><%== bf->render_child('${tplName}'${propsStr}, children => ${varName}) %>`
|
|
1094
1209
|
}
|
|
1095
|
-
return
|
|
1210
|
+
return `${namedSlotCaptures.join('')}<%== bf->render_child('${tplName}'${propsStr}) %>`
|
|
1096
1211
|
}
|
|
1097
1212
|
|
|
1098
1213
|
private childrenCaptureCounter = 0
|
|
@@ -1178,7 +1293,7 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
1178
1293
|
* template). Routed through the shared dispatcher (#1290 step 2).
|
|
1179
1294
|
*/
|
|
1180
1295
|
private readonly elementAttrEmitter: AttrValueEmitter = {
|
|
1181
|
-
emitLiteral: (value, name) => `${name}="${value.value}"`,
|
|
1296
|
+
emitLiteral: (value, name) => `${name}="${escapeHtml(value.value)}"`,
|
|
1182
1297
|
emitExpression: (value, name) => {
|
|
1183
1298
|
// `style={{ … }}` object literal → a CSS string with dynamic values
|
|
1184
1299
|
// interpolated, instead of refusing the bare object with BF101 (#1322).
|
|
@@ -1354,7 +1469,15 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
1354
1469
|
// initializer text and route through the same conditional-spread
|
|
1355
1470
|
// lowering. Only function-scope (`!isModule`) consts whose value is
|
|
1356
1471
|
// NOT itself a bare identifier (loop guard) are considered.
|
|
1357
|
-
|
|
1472
|
+
//
|
|
1473
|
+
// `loopBoundNames` guard (#2221): an enclosing `.map()` callback's
|
|
1474
|
+
// own param can shadow this outer const's name (`.map((sizeAttrs)
|
|
1475
|
+
// => <li {...sizeAttrs} />)`) — without the guard this forwarded
|
|
1476
|
+
// the OUTER const's hashref at every iteration instead of the
|
|
1477
|
+
// per-item `$sizeAttrs` value. Same live ref-counted map
|
|
1478
|
+
// `resolveLiteralConst` / `resolveStaticRecordLiteral` already
|
|
1479
|
+
// consult for this hazard class (#1749).
|
|
1480
|
+
if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(trimmed) && !this.loopBoundNames.has(trimmed)) {
|
|
1358
1481
|
const localConst = this.localConstants.find(
|
|
1359
1482
|
c => c.name === trimmed && !c.isModule,
|
|
1360
1483
|
)
|
|
@@ -1431,6 +1554,12 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
1431
1554
|
// the unsupported-expression lowering is never reached for a deferred
|
|
1432
1555
|
// predicate (no BF101 / BF102). #1966
|
|
1433
1556
|
if (attr.clientOnly) continue
|
|
1557
|
+
// `dangerouslySetInnerHTML` never renders as an HTML attribute — it's
|
|
1558
|
+
// handled by `renderDangerousInnerHtml` instead, which replaces the
|
|
1559
|
+
// element's children. Skip it here so its `{ __html: ... }` object
|
|
1560
|
+
// literal never reaches the generic object-literal BF101 refusal
|
|
1561
|
+
// (which would double-report alongside the purpose-built one).
|
|
1562
|
+
if (isDangerousInnerHtmlAttr(attr)) continue
|
|
1434
1563
|
// Rewrite JSX special-prop names to their HTML-attribute
|
|
1435
1564
|
// counterparts (#1475). `className` → `class` was already
|
|
1436
1565
|
// wired in; the `key` → `data-key` rewrite matches the
|
|
@@ -1440,7 +1569,10 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
1440
1569
|
// rewrite happens at attribute-emit time.
|
|
1441
1570
|
let attrName: string
|
|
1442
1571
|
if (attr.name === 'className') attrName = 'class'
|
|
1443
|
-
else if (attr.name === 'key')
|
|
1572
|
+
else if (attr.name === 'key') {
|
|
1573
|
+
const depth = this.currentLoopKeyDepth
|
|
1574
|
+
attrName = depth > 0 ? `data-key-${depth}` : 'data-key'
|
|
1575
|
+
}
|
|
1444
1576
|
else attrName = attr.name
|
|
1445
1577
|
const lowered = emitAttrValue(attr.value, this.elementAttrEmitter, attrName)
|
|
1446
1578
|
if (lowered) parts.push(lowered)
|