@barefootjs/mojolicious 0.18.3 → 0.18.5
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/expr/operand.d.ts +6 -17
- package/dist/adapter/expr/operand.d.ts.map +1 -1
- package/dist/adapter/index.js +88 -45
- package/dist/adapter/lib/constants.d.ts.map +1 -1
- package/dist/adapter/mojo-adapter.d.ts +8 -0
- package/dist/adapter/mojo-adapter.d.ts.map +1 -1
- package/dist/build.js +88 -45
- package/dist/conformance-pins.d.ts.map +1 -1
- package/dist/index.js +90 -64
- package/dist/render-divergences.d.ts.map +1 -1
- 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 +21 -1
- package/src/adapter/expr/array-method.ts +19 -0
- package/src/adapter/expr/emitters.ts +44 -9
- package/src/adapter/expr/operand.ts +6 -27
- package/src/adapter/lib/constants.ts +3 -0
- package/src/adapter/mojo-adapter.ts +71 -11
- package/src/conformance-pins.ts +0 -5
- package/src/render-divergences.ts +1 -32
- package/src/test-render.ts +5 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@barefootjs/mojolicious",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.5",
|
|
4
4
|
"description": "Mojolicious EP template adapter for BarefootJS - generates .html.ep files from IR",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"directory": "packages/adapter-mojolicious"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"@barefootjs/shared": "0.18.
|
|
55
|
+
"@barefootjs/shared": "0.18.5"
|
|
56
56
|
},
|
|
57
57
|
"peerDependencies": {
|
|
58
58
|
"@barefootjs/jsx": ">=0.2.0",
|
|
@@ -60,6 +60,6 @@
|
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@barefootjs/adapter-tests": "0.1.0",
|
|
63
|
-
"@barefootjs/jsx": "0.18.
|
|
63
|
+
"@barefootjs/jsx": "0.18.5"
|
|
64
64
|
}
|
|
65
65
|
}
|
|
@@ -1163,7 +1163,7 @@ export function Foo(props: { v: string }) {
|
|
|
1163
1163
|
// list rather than replace.
|
|
1164
1164
|
const a = new MojoAdapter()
|
|
1165
1165
|
const keys = Object.keys(a.templatePrimitives ?? {}).sort()
|
|
1166
|
-
expect(keys).toEqual(['JSON.stringify', 'Math.ceil', 'Math.floor', 'Math.round', 'Number', 'String'])
|
|
1166
|
+
expect(keys).toEqual(['JSON.stringify', 'Math.abs', 'Math.ceil', 'Math.floor', 'Math.max', 'Math.min', 'Math.round', 'Number', 'String'])
|
|
1167
1167
|
})
|
|
1168
1168
|
|
|
1169
1169
|
test('unregistered identifier-path callee is NOT accepted', () => {
|
|
@@ -1902,3 +1902,23 @@ export function C() {
|
|
|
1902
1902
|
expect(deferred.template).not.toContain('data-x')
|
|
1903
1903
|
})
|
|
1904
1904
|
})
|
|
1905
|
+
|
|
1906
|
+
describe('MojoAdapter - named-slot capture identifier safety (#2168 jsx-element-prop)', () => {
|
|
1907
|
+
// A JSX-valued prop under a hyphenated name (`data-slot`, a valid JSX
|
|
1908
|
+
// attribute name) must not leak into the `begin %>...<% end` capture
|
|
1909
|
+
// variable — Perl variable tokens can't contain `-`. The capture
|
|
1910
|
+
// variable is purely counter-based (never derived from the prop name);
|
|
1911
|
+
// the hash KEY passed to `render_child` still carries the real name,
|
|
1912
|
+
// quoted via `perlHashKey`.
|
|
1913
|
+
test('a hyphenated prop name does not appear in the capture variable', () => {
|
|
1914
|
+
const { template } = compileAndGenerate(`
|
|
1915
|
+
function Card(props) { return null }
|
|
1916
|
+
export function Parent() {
|
|
1917
|
+
return <Card data-slot={<strong>Title</strong>}>text</Card>
|
|
1918
|
+
}
|
|
1919
|
+
`)
|
|
1920
|
+
expect(template).toContain('<% my $bf_prop_0 = begin %>')
|
|
1921
|
+
expect(template).toContain("'data-slot' => $bf_prop_0")
|
|
1922
|
+
expect(template).not.toContain('$bf_prop_data')
|
|
1923
|
+
})
|
|
1924
|
+
})
|
|
@@ -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
|
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
* adapter only through the narrow `MojoEmitContext` seam.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import {
|
|
14
|
+
import { groupBinaryOperand,
|
|
15
|
+
isStringTypedOperand,
|
|
16
|
+
isStringConcatBinary,
|
|
15
17
|
type ParsedExprEmitter,
|
|
16
18
|
type HigherOrderMethod,
|
|
17
19
|
type ArrayMethod,
|
|
@@ -29,7 +31,7 @@ import {
|
|
|
29
31
|
|
|
30
32
|
import type { MojoEmitContext } from '../emit-context.ts'
|
|
31
33
|
import { MOJO_TEMPLATE_PRIMITIVES } from '../lib/constants.ts'
|
|
32
|
-
import { emitIndexAccessPerl
|
|
34
|
+
import { emitIndexAccessPerl } from './operand.ts'
|
|
33
35
|
import {
|
|
34
36
|
renderArrayMethod,
|
|
35
37
|
renderSortMethod,
|
|
@@ -103,7 +105,7 @@ export class MojoFilterEmitter implements ParsedExprEmitter {
|
|
|
103
105
|
return String(value)
|
|
104
106
|
}
|
|
105
107
|
|
|
106
|
-
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 {
|
|
107
109
|
// `.length` on a higher-order result (e.g.
|
|
108
110
|
// `x.tags.filter(t => t.active).length > 0` inside the outer
|
|
109
111
|
// filter predicate, #1443). The higher-order emit produces an
|
|
@@ -141,8 +143,11 @@ export class MojoFilterEmitter implements ParsedExprEmitter {
|
|
|
141
143
|
}
|
|
142
144
|
|
|
143
145
|
binary(op: string, left: ParsedExpr, right: ParsedExpr, emit: (e: ParsedExpr) => string): string {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
+
// Preserve source grouping: a compound operand re-emitted as infix
|
|
147
|
+
// text is otherwise re-parsed under THIS language's precedence —
|
|
148
|
+
// `(count() + 2) * 3` would silently become `count + 2 * 3` (#2173).
|
|
149
|
+
const l = groupBinaryOperand(left, emit(left))
|
|
150
|
+
const r = groupBinaryOperand(right, emit(right))
|
|
146
151
|
// String equality: `eq`/`ne` when EITHER operand is string-typed — a string
|
|
147
152
|
// literal, a string signal getter, or a string prop. Numeric `==`/`!=`
|
|
148
153
|
// would coerce both sides to 0 and match unrelated non-numeric strings (#1672).
|
|
@@ -154,6 +159,12 @@ export class MojoFilterEmitter implements ParsedExprEmitter {
|
|
|
154
159
|
if ((op === '!==' || op === '!=') && stringCmp) {
|
|
155
160
|
return `${l} ne ${r}`
|
|
156
161
|
}
|
|
162
|
+
// JS `+` with a string-typed operand is CONCATENATION, not addition —
|
|
163
|
+
// Perl's numeric `+` coerces `'Hello, ' + $name` to 0 (#2176). Lower
|
|
164
|
+
// to Perl's `.` concat operator.
|
|
165
|
+
if (isStringConcatBinary(op, left, right, this.isStringName)) {
|
|
166
|
+
return `${l} . ${r}`
|
|
167
|
+
}
|
|
157
168
|
const opMap: Record<string, string> = {
|
|
158
169
|
'===': '==', '!==': '!=', '>': '>', '<': '<', '>=': '>=', '<=': '<=',
|
|
159
170
|
'+': '+', '-': '-', '*': '*', '/': '/',
|
|
@@ -311,7 +322,7 @@ export class MojoTopLevelEmitter implements ParsedExprEmitter {
|
|
|
311
322
|
return String(value)
|
|
312
323
|
}
|
|
313
324
|
|
|
314
|
-
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 {
|
|
315
326
|
// `props.x` flattens to the bare `$x` the Mojo SSR caller binds each
|
|
316
327
|
// prop to (props arrive as individual `my $x = ...` vars, not a
|
|
317
328
|
// `$props` hashref).
|
|
@@ -327,7 +338,22 @@ export class MojoTopLevelEmitter implements ParsedExprEmitter {
|
|
|
327
338
|
if (staticValue !== null) return staticValue
|
|
328
339
|
}
|
|
329
340
|
const obj = emit(object)
|
|
330
|
-
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
|
+
}
|
|
331
357
|
return `${obj}->{${property}}`
|
|
332
358
|
}
|
|
333
359
|
|
|
@@ -389,8 +415,11 @@ export class MojoTopLevelEmitter implements ParsedExprEmitter {
|
|
|
389
415
|
}
|
|
390
416
|
|
|
391
417
|
binary(op: string, left: ParsedExpr, right: ParsedExpr, emit: (e: ParsedExpr) => string): string {
|
|
392
|
-
|
|
393
|
-
|
|
418
|
+
// Preserve source grouping: a compound operand re-emitted as infix
|
|
419
|
+
// text is otherwise re-parsed under THIS language's precedence —
|
|
420
|
+
// `(count() + 2) * 3` would silently become `count + 2 * 3` (#2173).
|
|
421
|
+
const l = groupBinaryOperand(left, emit(left))
|
|
422
|
+
const r = groupBinaryOperand(right, emit(right))
|
|
394
423
|
// String equality: `eq`/`ne` when EITHER operand is string-typed — a string
|
|
395
424
|
// literal (`role() === 'admin'`), a string signal getter (`sel()`), or a
|
|
396
425
|
// string prop (`props.x`). Falling back to numeric `==`/`!=` would make
|
|
@@ -404,6 +433,12 @@ export class MojoTopLevelEmitter implements ParsedExprEmitter {
|
|
|
404
433
|
if ((op === '!==' || op === '!=') && stringCmp) {
|
|
405
434
|
return `${l} ne ${r}`
|
|
406
435
|
}
|
|
436
|
+
// JS `+` with a string-typed operand is CONCATENATION, not addition —
|
|
437
|
+
// Perl's numeric `+` coerces `'Hello, ' + $name` to 0 (#2176). Lower
|
|
438
|
+
// to Perl's `.` concat operator.
|
|
439
|
+
if (isStringConcatBinary(op, left, right, n => this.ctx._isStringValueName(n))) {
|
|
440
|
+
return `${l} . ${r}`
|
|
441
|
+
}
|
|
407
442
|
const opMap: Record<string, string> = {
|
|
408
443
|
'===': '==', '!==': '!=', '>': '>', '<': '<', '>=': '>=', '<=': '<=',
|
|
409
444
|
'+': '+', '-': '-', '*': '*',
|
|
@@ -1,39 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* EP template adapter.
|
|
2
|
+
* Index-access lowering for the Mojolicious EP template adapter.
|
|
4
3
|
*
|
|
5
4
|
* Extracted from `mojo-adapter.ts` (domain-module refactor, issue #2018
|
|
6
|
-
* track D). Pure
|
|
5
|
+
* track D). Pure function over `ParsedExpr` — it takes an `isStringName`
|
|
7
6
|
* predicate (supplied by the emitter from adapter state) rather than reading
|
|
8
7
|
* adapter instance state directly.
|
|
9
8
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* integration, issue #2018 track D). `emitIndexAccessPerl` stays Mojo-specific
|
|
9
|
+
* The string-typed-operand classifier that used to live here (marked
|
|
10
|
+
* SHARED CANDIDATE) was promoted to `@barefootjs/jsx` as
|
|
11
|
+
* `isStringTypedOperand` (#2176); `emitIndexAccessPerl` stays Mojo-specific
|
|
14
12
|
* (Perl's `->[]` vs `->{}` split has no Kolon equivalent).
|
|
15
13
|
*/
|
|
16
14
|
|
|
17
|
-
import type
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Whether a comparison operand is string-typed, so JS `===`/`!==` against it
|
|
21
|
-
* must lower to Perl `eq`/`ne` instead of numeric `==`/`!=` (#1672). Covers a
|
|
22
|
-
* string literal, a string-signal getter call (`sel()`), and a string prop
|
|
23
|
-
* access (`props.x`). `isStringName` reports whether a getter/prop name is
|
|
24
|
-
* known-string. Loop-element fields (`t.id`) on untyped arrays have no known
|
|
25
|
-
* type and stay undetected — a separate, narrower gap.
|
|
26
|
-
*/
|
|
27
|
-
export function isStringTypedOperand(expr: ParsedExpr, isStringName: (n: string) => boolean): boolean {
|
|
28
|
-
if (expr.kind === 'literal' && expr.literalType === 'string') return true
|
|
29
|
-
if (expr.kind === 'call' && expr.callee.kind === 'identifier' && expr.args.length === 0) {
|
|
30
|
-
return isStringName(expr.callee.name)
|
|
31
|
-
}
|
|
32
|
-
if (expr.kind === 'member' && expr.object.kind === 'identifier' && expr.object.name === 'props') {
|
|
33
|
-
return isStringName(expr.property)
|
|
34
|
-
}
|
|
35
|
-
return false
|
|
36
|
-
}
|
|
15
|
+
import { isStringTypedOperand, type ParsedExpr } from '@barefootjs/jsx'
|
|
37
16
|
|
|
38
17
|
/**
|
|
39
18
|
* Lower `arr[index]` to a Perl deref. Perl distinguishes array
|
|
@@ -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
|
/**
|
|
@@ -62,7 +62,7 @@ import {
|
|
|
62
62
|
} from '@barefootjs/jsx'
|
|
63
63
|
import { isAriaBooleanAttr, isBooleanResultExpr } from './boolean-result.ts'
|
|
64
64
|
import type { ParsedExpr, LoweringMatcher } from '@barefootjs/jsx'
|
|
65
|
-
import { BF_SLOT, BF_COND, BF_REGION } from '@barefootjs/shared'
|
|
65
|
+
import { BF_SLOT, BF_COND, BF_REGION, escapeHtml } from '@barefootjs/shared'
|
|
66
66
|
|
|
67
67
|
import type { MojoRenderCtx } from './lib/types.ts'
|
|
68
68
|
import { MOJO_PRIMITIVE_EMIT_MAP } from './lib/constants.ts'
|
|
@@ -165,6 +165,14 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
165
165
|
private options: Required<MojoAdapterOptions>
|
|
166
166
|
private errors: CompilerError[] = []
|
|
167
167
|
private inLoop: boolean = false
|
|
168
|
+
/**
|
|
169
|
+
* `IRLoop.depth` of the loop currently being rendered (save/restore
|
|
170
|
+
* around `renderChildren(loop.children)`, mirroring `inLoop` above).
|
|
171
|
+
* `renderAttributes` reads this to derive the `key` → `data-key`/
|
|
172
|
+
* `data-key-N` suffix — the depth is IR-computed (jsx-to-ir.ts), not
|
|
173
|
+
* re-derived here (#2168 nested-loop-outer-binding).
|
|
174
|
+
*/
|
|
175
|
+
private currentLoopKeyDepth = 0
|
|
168
176
|
/**
|
|
169
177
|
* SolidJS-style props identifier (`function(props: P)`) and the
|
|
170
178
|
* analyzer-extracted prop names. Stashed at `generate()` entry so
|
|
@@ -478,7 +486,9 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
478
486
|
}
|
|
479
487
|
|
|
480
488
|
emitText(node: IRText): string {
|
|
481
|
-
|
|
489
|
+
// IRText carries the entity-DECODED value (Phase 1 decodes JSX
|
|
490
|
+
// character references); re-escape for direct HTML emission.
|
|
491
|
+
return escapeHtml(node.value)
|
|
482
492
|
}
|
|
483
493
|
|
|
484
494
|
emitExpression(node: IRExpression): string {
|
|
@@ -846,17 +856,22 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
846
856
|
// the whole body (children + key + filter) so a same-named loop variable
|
|
847
857
|
// isn't replaced by the const literal (#1749 review). Ref-counted for
|
|
848
858
|
// nested loops; released after the body lines are assembled below.
|
|
849
|
-
const loopBound = loop.
|
|
850
|
-
? [param]
|
|
851
|
-
:
|
|
852
|
-
? [
|
|
853
|
-
:
|
|
859
|
+
const loopBound = loop.objectIteration === 'entries'
|
|
860
|
+
? [param, loop.index ?? '_k']
|
|
861
|
+
: loop.objectIteration === 'keys' || loop.objectIteration === 'values' || loop.iterationShape === 'keys'
|
|
862
|
+
? [param]
|
|
863
|
+
: supportableDestructure
|
|
864
|
+
? ['__bf_item', ...(loop.paramBindings ?? []).map(b => b.name), loop.index ?? '_i']
|
|
865
|
+
: [param, loop.index ?? '_i']
|
|
854
866
|
for (const n of loopBound) {
|
|
855
867
|
this.loopBoundNames.set(n, (this.loopBoundNames.get(n) ?? 0) + 1)
|
|
856
868
|
}
|
|
857
869
|
const prevInLoop = this.inLoop
|
|
858
870
|
this.inLoop = true
|
|
871
|
+
const prevLoopKeyDepth = this.currentLoopKeyDepth
|
|
872
|
+
this.currentLoopKeyDepth = loop.depth
|
|
859
873
|
const renderedChildren = this.renderChildren(loop.children)
|
|
874
|
+
this.currentLoopKeyDepth = prevLoopKeyDepth
|
|
860
875
|
this.inLoop = prevInLoop
|
|
861
876
|
|
|
862
877
|
// Whole-item conditional (#1665): prepend an always-present
|
|
@@ -918,6 +933,21 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
918
933
|
}
|
|
919
934
|
lines.push(`% my $${sortedHoist} = ${sorted};`)
|
|
920
935
|
}
|
|
936
|
+
if (loop.objectIteration) {
|
|
937
|
+
// `objectIteration` (#2168 object-entries-map): a Perl hash has no
|
|
938
|
+
// native insertion-order guarantee (unlike Ruby's `Hash`/Python's
|
|
939
|
+
// `dict`) — this codebase's own runtime already works around this
|
|
940
|
+
// elsewhere with `sort keys %$hash` (`BarefootJS.pm`'s
|
|
941
|
+
// `spread_attrs`/`_style_to_css`), so this reuses that exact
|
|
942
|
+
// convention for a deterministic, alphabetically-sorted iteration
|
|
943
|
+
// (not JS insertion order — a documented known limitation for
|
|
944
|
+
// out-of-alphabetical-order data, same as Go/Rust/Xslate).
|
|
945
|
+
const keyVar = loop.objectIteration === 'values' ? '$__bf_k' : `$${loop.index ?? param}`
|
|
946
|
+
lines.push(`% for my ${keyVar} (sort keys %{${array}}) {`)
|
|
947
|
+
if (loop.objectIteration === 'entries' || loop.objectIteration === 'values') {
|
|
948
|
+
lines.push(`% my $${param} = ${array}->{${keyVar}};`)
|
|
949
|
+
}
|
|
950
|
+
} else {
|
|
921
951
|
lines.push(`% for my ${indexVar} (0..$#{${array}}) {`)
|
|
922
952
|
if (loop.iterationShape !== 'keys') {
|
|
923
953
|
if (supportableDestructure) {
|
|
@@ -951,6 +981,7 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
951
981
|
lines.push(`% my $${param} = ${array}->[${indexVar}];`)
|
|
952
982
|
}
|
|
953
983
|
}
|
|
984
|
+
}
|
|
954
985
|
|
|
955
986
|
// Handle filter().map() pattern by wrapping children in if-condition
|
|
956
987
|
if (loop.filterPredicate) {
|
|
@@ -1049,10 +1080,36 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
1049
1080
|
|
|
1050
1081
|
renderComponent(comp: IRComponent): string {
|
|
1051
1082
|
const propParts: string[] = []
|
|
1083
|
+
// Named JSX-valued props OTHER than the reserved `children`
|
|
1084
|
+
// (`header={<strong>Title</strong>}`, #2168 jsx-element-prop) get the
|
|
1085
|
+
// same `begin %>…<% end` capture as the reserved children slot below,
|
|
1086
|
+
// just keyed by the prop's own name. `render_child` (BarefootJS.pm)
|
|
1087
|
+
// materializes every prop value that's a CODE ref — not only
|
|
1088
|
+
// `children` — into the Mojo::ByteStream the capture block produces,
|
|
1089
|
+
// so the child's read of the slot back out (`<%= $header %>`) sees an
|
|
1090
|
+
// already-safe ByteStream and Mojo::Template's auto-escape passes it
|
|
1091
|
+
// through unescaped, the same way it already does for `children`.
|
|
1092
|
+
const namedSlotCaptures: string[] = []
|
|
1052
1093
|
for (const p of comp.props) {
|
|
1053
1094
|
// Skip callback props (onXxx) and `ref` — both are client-only for
|
|
1054
1095
|
// SSR (Hono renders neither; the client JS wires them at hydration).
|
|
1055
1096
|
if ((p.name.match(/^on[A-Z]/) || p.name === 'ref') && p.value.kind === 'expression') continue
|
|
1097
|
+
if (p.value.kind === 'jsx-children' && p.name !== 'children') {
|
|
1098
|
+
const prevInLoop = this.inLoop
|
|
1099
|
+
this.inLoop = false
|
|
1100
|
+
const slotBody = this.renderChildren(p.value.children)
|
|
1101
|
+
this.inLoop = prevInLoop
|
|
1102
|
+
// Purely counter-based — NOT derived from `p.name` or `comp.slotId`.
|
|
1103
|
+
// A JSX prop name can contain characters (`data-slot`) that aren't a
|
|
1104
|
+
// valid Perl variable token, and `comp.slotId` alone would collide
|
|
1105
|
+
// across two named-slot props on the same component invocation
|
|
1106
|
+
// (unlike the reserved children slot, there's only ever one of
|
|
1107
|
+
// those per invocation).
|
|
1108
|
+
const varName = `$bf_prop_${this.childrenCaptureCounter++}`
|
|
1109
|
+
namedSlotCaptures.push(`<% my ${varName} = begin %>${slotBody}<% end %>`)
|
|
1110
|
+
propParts.push(`${perlHashKey(p.name)} => ${varName}`)
|
|
1111
|
+
continue
|
|
1112
|
+
}
|
|
1056
1113
|
const lowered = emitAttrValue(p.value, this.componentPropEmitter, p.name)
|
|
1057
1114
|
if (lowered) propParts.push(lowered)
|
|
1058
1115
|
}
|
|
@@ -1090,9 +1147,9 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
1090
1147
|
const childrenBody = this.renderChildren(effectiveChildren)
|
|
1091
1148
|
this.inLoop = prevInLoop
|
|
1092
1149
|
const varName = `$bf_children_${comp.slotId ?? 'c' + this.childrenCaptureCounter++}`
|
|
1093
|
-
return
|
|
1150
|
+
return `${namedSlotCaptures.join('')}<% my ${varName} = begin %>${childrenBody}<% end %><%== bf->render_child('${tplName}'${propsStr}, children => ${varName}) %>`
|
|
1094
1151
|
}
|
|
1095
|
-
return
|
|
1152
|
+
return `${namedSlotCaptures.join('')}<%== bf->render_child('${tplName}'${propsStr}) %>`
|
|
1096
1153
|
}
|
|
1097
1154
|
|
|
1098
1155
|
private childrenCaptureCounter = 0
|
|
@@ -1178,7 +1235,7 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
1178
1235
|
* template). Routed through the shared dispatcher (#1290 step 2).
|
|
1179
1236
|
*/
|
|
1180
1237
|
private readonly elementAttrEmitter: AttrValueEmitter = {
|
|
1181
|
-
emitLiteral: (value, name) => `${name}="${value.value}"`,
|
|
1238
|
+
emitLiteral: (value, name) => `${name}="${escapeHtml(value.value)}"`,
|
|
1182
1239
|
emitExpression: (value, name) => {
|
|
1183
1240
|
// `style={{ … }}` object literal → a CSS string with dynamic values
|
|
1184
1241
|
// interpolated, instead of refusing the bare object with BF101 (#1322).
|
|
@@ -1440,7 +1497,10 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
1440
1497
|
// rewrite happens at attribute-emit time.
|
|
1441
1498
|
let attrName: string
|
|
1442
1499
|
if (attr.name === 'className') attrName = 'class'
|
|
1443
|
-
else if (attr.name === 'key')
|
|
1500
|
+
else if (attr.name === 'key') {
|
|
1501
|
+
const depth = this.currentLoopKeyDepth
|
|
1502
|
+
attrName = depth > 0 ? `data-key-${depth}` : 'data-key'
|
|
1503
|
+
}
|
|
1444
1504
|
else attrName = attr.name
|
|
1445
1505
|
const lowered = emitAttrValue(attr.value, this.elementAttrEmitter, attrName)
|
|
1446
1506
|
if (lowered) parts.push(lowered)
|
package/src/conformance-pins.ts
CHANGED
|
@@ -141,9 +141,4 @@ export const conformancePins: ConformancePins = {
|
|
|
141
141
|
// the shape loudly instead of emitting entity-escaped markup that
|
|
142
142
|
// silently renders tags as text.
|
|
143
143
|
'dangerous-inner-html': [{ code: 'BF101', severity: 'error' }],
|
|
144
|
-
// Edge-case sweep (Priority 12): `.replaceAll` has no lowering yet —
|
|
145
|
-
// only first-occurrence `.replace` is wired to the runtime helpers.
|
|
146
|
-
// Refused with BF101 rather than reusing the first-only lowering,
|
|
147
|
-
// which would silently change semantics.
|
|
148
|
-
'string-replaceall': [{ code: 'BF101', severity: 'error' }],
|
|
149
144
|
}
|
|
@@ -14,35 +14,4 @@
|
|
|
14
14
|
|
|
15
15
|
import type { RenderDivergences } from '@barefootjs/jsx'
|
|
16
16
|
|
|
17
|
-
export const renderDivergences: RenderDivergences = {
|
|
18
|
-
'arithmetic-text':
|
|
19
|
-
'`(count() + 2) * 3` renders 10 instead of 18 — the parenthesised sub-expression loses its grouping (silent wrong arithmetic)',
|
|
20
|
-
'string-concat-plus':
|
|
21
|
-
"`'Hello, ' + name` renders \"0\" — Perl's numeric `+` coerces the strings; JS string-concat `+` needs Perl's `.`",
|
|
22
|
-
'string-length-text':
|
|
23
|
-
'`.length` on a STRING prop diverges (array-length lowering misapplied to a scalar)',
|
|
24
|
-
'number-tofixed':
|
|
25
|
-
'the literal `¥` in template text reaches the output as U+FFFD — a UTF-8 encoding gap for non-ASCII literal text adjacent to a dynamic slot',
|
|
26
|
-
'html-entity-text':
|
|
27
|
-
'`©` in JSX literal text: Hono decodes to `©`, this adapter re-emits the raw entity — same DOM, different bytes',
|
|
28
|
-
'math-methods':
|
|
29
|
-
'Math.min/max/abs over a signal render empty (only Math.floor is in the template-primitive registry)',
|
|
30
|
-
'boolean-attr-literals':
|
|
31
|
-
'camelCase boolean alias `readOnly`: Hono SSRs `readOnly="true"`, this adapter emits bare presence',
|
|
32
|
-
'camelcase-attributes':
|
|
33
|
-
'`htmlFor` is not lowered to `for` (Hono maps it)',
|
|
34
|
-
'static-attr-escape':
|
|
35
|
-
'static attribute values are not HTML-escaped (`title="Fish & Chips"` emitted raw; Hono escapes)',
|
|
36
|
-
'svg-icon':
|
|
37
|
-
'SVG camelCase presentation attrs (`strokeWidth`, `strokeLinecap`) pass through unmapped; Hono lowers to kebab-case',
|
|
38
|
-
'object-entries-map':
|
|
39
|
-
'`Object.entries(prop).map(([k, v]) => …)` renders an EMPTY list — the object-shaped prop silently produces zero iterations',
|
|
40
|
-
'nested-loop-outer-binding':
|
|
41
|
-
'nested-loop inner items carry `data-key` where the reference emits the depth-suffixed `data-key-1`',
|
|
42
|
-
'jsx-element-prop':
|
|
43
|
-
'a JSX element passed as a NON-children prop renders an empty slot — the element value is silently dropped',
|
|
44
|
-
'string-slice':
|
|
45
|
-
'`.slice()` on a STRING misfires through the array slice helper',
|
|
46
|
-
'string-trim-sided':
|
|
47
|
-
'`.trimStart()` / `.trimEnd()` render empty (no lowering)',
|
|
48
|
-
}
|
|
17
|
+
export const renderDivergences: RenderDivergences = {}
|
package/src/test-render.ts
CHANGED
|
@@ -240,6 +240,11 @@ export async function renderMojoComponent(options: RenderOptions): Promise<strin
|
|
|
240
240
|
use strict;
|
|
241
241
|
use warnings;
|
|
242
242
|
use utf8;
|
|
243
|
+
# The template is read through :utf8, so $output holds decoded wide
|
|
244
|
+
# characters; without a matching layer on STDOUT Perl emits them as
|
|
245
|
+
# latin-1 bytes and the harness reads U+FFFD (©/¥ mangling). A real
|
|
246
|
+
# Mojolicious response encodes on the way out — this is harness-only.
|
|
247
|
+
binmode STDOUT, ':encoding(UTF-8)';
|
|
243
248
|
|
|
244
249
|
use lib '${LIB_DIR}', '${PERL_CORE_LIB_DIR}';
|
|
245
250
|
use Mojolicious;
|