@barefootjs/mojolicious 0.5.3 → 0.6.1
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 +91 -4
- package/dist/adapter/mojo-adapter.d.ts.map +1 -1
- package/dist/build.js +91 -4
- package/dist/index.js +91 -4
- package/lib/BarefootJS.pm +288 -0
- package/package.json +3 -3
- package/src/__tests__/mojo-adapter.test.ts +180 -20
- package/src/adapter/mojo-adapter.ts +183 -13
|
@@ -56,7 +56,7 @@ import { isAriaBooleanAttr, isBooleanResultExpr } from './boolean-result'
|
|
|
56
56
|
* the `IRNodeEmitter` interface.
|
|
57
57
|
*/
|
|
58
58
|
type MojoRenderCtx = Record<string, never>
|
|
59
|
-
import type { ParsedExpr, ParsedStatement, SortComparator, TemplatePart } from '@barefootjs/jsx'
|
|
59
|
+
import type { ParsedExpr, ParsedStatement, SortComparator, ReduceOp, FlatDepth, FlatMapOp, TemplatePart } from '@barefootjs/jsx'
|
|
60
60
|
import { BF_SLOT, BF_COND } from '@barefootjs/shared'
|
|
61
61
|
|
|
62
62
|
interface PrimitiveSpec {
|
|
@@ -1295,9 +1295,10 @@ function renderArrayMethod(
|
|
|
1295
1295
|
// arr.join(sep) → join(sep, @{arr}). The default `${obj}->{join}`
|
|
1296
1296
|
// hash-lookup fallback would emit invalid Perl, which is why the
|
|
1297
1297
|
// IR carves out a dedicated method node instead of routing
|
|
1298
|
-
// through the generic call dispatcher.
|
|
1298
|
+
// through the generic call dispatcher. `.join()` defaults the
|
|
1299
|
+
// separator to `,` (JS) and ignores any extra argument.
|
|
1299
1300
|
const obj = emit(object)
|
|
1300
|
-
const sep = emit(args[0])
|
|
1301
|
+
const sep = args.length >= 1 ? emit(args[0]) : `','`
|
|
1301
1302
|
return `join(${sep}, @{${obj}})`
|
|
1302
1303
|
}
|
|
1303
1304
|
case 'includes': {
|
|
@@ -1333,9 +1334,10 @@ function renderArrayMethod(
|
|
|
1333
1334
|
// `.at(i)` with negative-index support — `.at(-1)` is the
|
|
1334
1335
|
// last element. The Mojo helper wraps the same `length + i`
|
|
1335
1336
|
// arithmetic the Go `bf_at` does so the lowering stays
|
|
1336
|
-
// symmetric across adapters.
|
|
1337
|
+
// symmetric across adapters. `.at()` with no argument is `.at(0)`
|
|
1338
|
+
// (the first element); extra arguments are ignored.
|
|
1337
1339
|
const obj = emit(object)
|
|
1338
|
-
const idx = emit(args[0])
|
|
1340
|
+
const idx = args.length >= 1 ? emit(args[0]) : '0'
|
|
1339
1341
|
return `bf->at(${obj}, ${idx})`
|
|
1340
1342
|
}
|
|
1341
1343
|
case 'concat': {
|
|
@@ -1343,20 +1345,26 @@ function renderArrayMethod(
|
|
|
1343
1345
|
// ref so the result composes with `.join(...)` / other
|
|
1344
1346
|
// array-shape methods downstream (the canonical Tier A
|
|
1345
1347
|
// conformance fixture chains `.concat(...).join(' ')`).
|
|
1348
|
+
// `.concat()` with no argument is a shallow copy — indistinguishable
|
|
1349
|
+
// from the receiver in an SSR snapshot, so it lowers to the receiver.
|
|
1350
|
+
if (args.length === 0) {
|
|
1351
|
+
return emit(object)
|
|
1352
|
+
}
|
|
1346
1353
|
const a = emit(object)
|
|
1347
1354
|
const b = emit(args[0])
|
|
1348
1355
|
return `bf->concat(${a}, ${b})`
|
|
1349
1356
|
}
|
|
1350
1357
|
case 'slice': {
|
|
1351
|
-
// `.slice(start)` / `.slice(start, end)`. The Mojo
|
|
1352
|
-
// mirrors the Go arithmetic (negative-index normalisation,
|
|
1353
|
-
// out-of-bounds clamping, empty result on start >= end).
|
|
1354
|
-
//
|
|
1355
|
-
//
|
|
1356
|
-
//
|
|
1358
|
+
// `.slice()` / `.slice(start)` / `.slice(start, end)`. The Mojo
|
|
1359
|
+
// helper mirrors the Go arithmetic (negative-index normalisation,
|
|
1360
|
+
// out-of-bounds clamping, empty result on start >= end). A
|
|
1361
|
+
// missing `start` defaults to 0 (full copy); an absent `end`
|
|
1362
|
+
// lowers as `undef`, which the helper treats as "to length". JS
|
|
1363
|
+
// ignores a third+ argument. Returns a new ARRAY ref so the
|
|
1364
|
+
// result composes with `.join(...)` downstream.
|
|
1357
1365
|
const recv = emit(object)
|
|
1358
|
-
const start = emit(args[0])
|
|
1359
|
-
const end = args.length
|
|
1366
|
+
const start = args.length >= 1 ? emit(args[0]) : '0'
|
|
1367
|
+
const end = args.length >= 2 ? emit(args[1]) : 'undef'
|
|
1360
1368
|
return `bf->slice(${recv}, ${start}, ${end})`
|
|
1361
1369
|
}
|
|
1362
1370
|
case 'reverse':
|
|
@@ -1388,6 +1396,86 @@ function renderArrayMethod(
|
|
|
1388
1396
|
const recv = emit(object)
|
|
1389
1397
|
return `bf->trim(${recv})`
|
|
1390
1398
|
}
|
|
1399
|
+
case 'split': {
|
|
1400
|
+
// `.split()` / `.split(sep)` / `.split(sep, limit)` — string →
|
|
1401
|
+
// ARRAY ref via `bf->split`. With no separator the helper returns
|
|
1402
|
+
// the whole string as a single element; otherwise it quotemetas
|
|
1403
|
+
// the separator (literal match, not regex) and keeps trailing
|
|
1404
|
+
// empties (`-1`), staying byte-equal with Go's `bf_split`. The
|
|
1405
|
+
// optional `limit` caps the pieces; JS ignores a third+ argument.
|
|
1406
|
+
// See #1448 Tier B.
|
|
1407
|
+
const recv = emit(object)
|
|
1408
|
+
if (args.length === 0) {
|
|
1409
|
+
return `bf->split(${recv})`
|
|
1410
|
+
}
|
|
1411
|
+
const sep = emit(args[0])
|
|
1412
|
+
if (args.length === 1) {
|
|
1413
|
+
return `bf->split(${recv}, ${sep})`
|
|
1414
|
+
}
|
|
1415
|
+
const limit = emit(args[1])
|
|
1416
|
+
return `bf->split(${recv}, ${sep}, ${limit})`
|
|
1417
|
+
}
|
|
1418
|
+
case 'startsWith':
|
|
1419
|
+
case 'endsWith': {
|
|
1420
|
+
// `.startsWith(prefix, position?)` / `.endsWith(suffix,
|
|
1421
|
+
// endPosition?)` — string → boolean. The Perl helpers
|
|
1422
|
+
// (`bf->starts_with` / `bf->ends_with`) do a `substr`-anchored
|
|
1423
|
+
// comparison so the search string is matched literally (no regex
|
|
1424
|
+
// metachar surprises) and undef receivers stay quiet. The optional
|
|
1425
|
+
// second argument re-anchors the test; JS ignores a third+
|
|
1426
|
+
// argument. See #1448 Tier B.
|
|
1427
|
+
const fn = method === 'startsWith' ? 'starts_with' : 'ends_with'
|
|
1428
|
+
const recv = emit(object)
|
|
1429
|
+
const arg = emit(args[0])
|
|
1430
|
+
if (args.length >= 2) {
|
|
1431
|
+
return `bf->${fn}(${recv}, ${arg}, ${emit(args[1])})`
|
|
1432
|
+
}
|
|
1433
|
+
return `bf->${fn}(${recv}, ${arg})`
|
|
1434
|
+
}
|
|
1435
|
+
case 'replace': {
|
|
1436
|
+
// `.replace(old, new)` — string-pattern form, first occurrence.
|
|
1437
|
+
// The `bf->replace` helper splices via index/substr (not `s///`)
|
|
1438
|
+
// so both the pattern and the replacement are literal — no Perl
|
|
1439
|
+
// regex metacharacters and no `$1` / `$&` interpolation in the
|
|
1440
|
+
// replacement, keeping it byte-equal with Go's `bf_replace`. The
|
|
1441
|
+
// regex-pattern form is refused upstream at the parser. See
|
|
1442
|
+
// #1448 Tier B.
|
|
1443
|
+
const recv = emit(object)
|
|
1444
|
+
const oldS = emit(args[0])
|
|
1445
|
+
const newS = emit(args[1])
|
|
1446
|
+
return `bf->replace(${recv}, ${oldS}, ${newS})`
|
|
1447
|
+
}
|
|
1448
|
+
case 'repeat': {
|
|
1449
|
+
// `.repeat(n)` — string repeated `n` times. The `bf->repeat`
|
|
1450
|
+
// helper wraps Perl's `x` operator with the same negative-count
|
|
1451
|
+
// → "" clamp and integer truncation Go's `bf_repeat` applies, so
|
|
1452
|
+
// the two adapters stay byte-equal. Full JS arity: the no-argument
|
|
1453
|
+
// form is `repeat(0)` → ""; a second+ argument is ignored.
|
|
1454
|
+
// See #1448 Tier B.
|
|
1455
|
+
const recv = emit(object)
|
|
1456
|
+
const count = args.length === 0 ? '0' : emit(args[0])
|
|
1457
|
+
return `bf->repeat(${recv}, ${count})`
|
|
1458
|
+
}
|
|
1459
|
+
case 'padStart':
|
|
1460
|
+
case 'padEnd': {
|
|
1461
|
+
// `.padStart(target, pad?)` / `.padEnd(target, pad?)`. The
|
|
1462
|
+
// `bf->pad_*` helpers default the pad to a single space when the
|
|
1463
|
+
// arg is omitted and measure length in characters, matching Go's
|
|
1464
|
+
// rune-based `bf_pad_*`. Full JS arity: the no-argument form is
|
|
1465
|
+
// `padStart(0)` → the receiver unchanged; a third+ argument is
|
|
1466
|
+
// ignored. See #1448 Tier B.
|
|
1467
|
+
const fn = method === 'padStart' ? 'pad_start' : 'pad_end'
|
|
1468
|
+
const recv = emit(object)
|
|
1469
|
+
if (args.length === 0) {
|
|
1470
|
+
return `bf->${fn}(${recv}, 0)`
|
|
1471
|
+
}
|
|
1472
|
+
const target = emit(args[0])
|
|
1473
|
+
if (args.length === 1) {
|
|
1474
|
+
return `bf->${fn}(${recv}, ${target})`
|
|
1475
|
+
}
|
|
1476
|
+
const pad = emit(args[1])
|
|
1477
|
+
return `bf->${fn}(${recv}, ${target}, ${pad})`
|
|
1478
|
+
}
|
|
1391
1479
|
default: {
|
|
1392
1480
|
// TS-level exhaustiveness guard. If this throws at runtime, the
|
|
1393
1481
|
// IR was constructed against a newer `ArrayMethod` variant that
|
|
@@ -1445,6 +1533,64 @@ function renderSortMethod(recv: string, c: SortComparator): string {
|
|
|
1445
1533
|
return `bf->sort(${recv}, { keys => [${keyHashes.join(', ')}] })`
|
|
1446
1534
|
}
|
|
1447
1535
|
|
|
1536
|
+
/**
|
|
1537
|
+
* Render a `.reduce(fn, init)` arithmetic fold (#1448 Tier C) as a
|
|
1538
|
+
* `bf->reduce(...)` call. The structured `ReduceOp` maps to the Perl
|
|
1539
|
+
* helper's options hash:
|
|
1540
|
+
*
|
|
1541
|
+
* bf->reduce($recv, { op => '+', key_kind => 'field', key => 'duration',
|
|
1542
|
+
* type => 'numeric', init => 0 })
|
|
1543
|
+
*
|
|
1544
|
+
* A numeric init passes through as a bare Perl number (`0`, `-1`); a
|
|
1545
|
+
* string init (concat fold) is re-quoted from its literal contents.
|
|
1546
|
+
*/
|
|
1547
|
+
function renderReduceMethod(recv: string, op: ReduceOp, direction: 'left' | 'right'): string {
|
|
1548
|
+
const keyEntry =
|
|
1549
|
+
op.key.kind === 'self'
|
|
1550
|
+
? `key_kind => 'self'`
|
|
1551
|
+
: `key_kind => 'field', key => '${op.key.field}'`
|
|
1552
|
+
// `op.init` is the decoded seed value. A numeric seed is already a
|
|
1553
|
+
// canonical decimal literal Perl reads directly; a concat seed is the
|
|
1554
|
+
// string contents, embedded in a single-quoted Perl literal. The `'`
|
|
1555
|
+
// escape is REQUIRED: a seed decoded from a double-quoted JS literal
|
|
1556
|
+
// (e.g. `"a'b"`) is escape-free yet contains an apostrophe. A literal
|
|
1557
|
+
// backslash can't occur (it would need a `\\` escape, which the parser
|
|
1558
|
+
// refuses), but escaping it too keeps this self-contained.
|
|
1559
|
+
const init =
|
|
1560
|
+
op.type === 'string'
|
|
1561
|
+
? `'${op.init.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`
|
|
1562
|
+
: op.init
|
|
1563
|
+
// `direction` is "left" (reduce) or "right" (reduceRight); the Perl
|
|
1564
|
+
// helper reverses the list for "right". Only observable for concat.
|
|
1565
|
+
return `bf->reduce(${recv}, { op => '${op.op}', ${keyEntry}, type => '${op.type}', init => ${init}, direction => '${direction}' })`
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
// `.flat(depth?)` → `bf->flat($recv, $depth)`. The `Infinity` form lowers
|
|
1569
|
+
// to the `-1` sentinel (flatten fully); a finite depth flattens that many
|
|
1570
|
+
// levels (`0` = shallow copy). See `sub flat` in BarefootJS.pm. (#1448)
|
|
1571
|
+
function renderFlatMethod(recv: string, depth: FlatDepth): string {
|
|
1572
|
+
const d = depth === 'infinity' ? -1 : depth
|
|
1573
|
+
return `bf->flat(${recv}, ${d})`
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
// `.flatMap(i => i)` / `.flatMap(i => i.field)` → `bf->flat_map($recv,
|
|
1577
|
+
// 'self'|'field', 'field')`, and the array-literal tuple form
|
|
1578
|
+
// `i => [i.a, i.b]` → `bf->flat_map_tuple($recv, ['field','a'], ...)`
|
|
1579
|
+
// (one arrayref per leaf). The field key is the raw JS prop name (Perl
|
|
1580
|
+
// hashes are keyed by it), mirroring `bf->reduce`. See `sub flat_map` /
|
|
1581
|
+
// `sub flat_map_tuple` in BarefootJS.pm.
|
|
1582
|
+
function renderFlatMapMethod(recv: string, op: FlatMapOp): string {
|
|
1583
|
+
const proj = op.projection
|
|
1584
|
+
if (proj.kind === 'tuple') {
|
|
1585
|
+
const specs = proj.elements
|
|
1586
|
+
.map(l => (l.kind === 'self' ? `['self', '']` : `['field', '${l.field}']`))
|
|
1587
|
+
.join(', ')
|
|
1588
|
+
return `bf->flat_map_tuple(${recv}, ${specs})`
|
|
1589
|
+
}
|
|
1590
|
+
if (proj.kind === 'self') return `bf->flat_map(${recv}, 'self', '')`
|
|
1591
|
+
return `bf->flat_map(${recv}, 'field', '${proj.field}')`
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1448
1594
|
/** True when `type` is the `string` primitive. */
|
|
1449
1595
|
function isStringTypeInfo(type: TypeInfo | undefined): boolean {
|
|
1450
1596
|
return type?.kind === 'primitive' && type.primitive === 'string'
|
|
@@ -1627,6 +1773,18 @@ class MojoFilterEmitter implements ParsedExprEmitter {
|
|
|
1627
1773
|
return renderSortMethod(emit(object), comparator)
|
|
1628
1774
|
}
|
|
1629
1775
|
|
|
1776
|
+
reduceMethod(method: 'reduce' | 'reduceRight', object: ParsedExpr, reduceOp: ReduceOp, emit: (e: ParsedExpr) => string): string {
|
|
1777
|
+
return renderReduceMethod(emit(object), reduceOp, method === 'reduceRight' ? 'right' : 'left')
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
flatMethod(object: ParsedExpr, depth: FlatDepth, emit: (e: ParsedExpr) => string): string {
|
|
1781
|
+
return renderFlatMethod(emit(object), depth)
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
flatMapMethod(object: ParsedExpr, op: FlatMapOp, emit: (e: ParsedExpr) => string): string {
|
|
1785
|
+
return renderFlatMapMethod(emit(object), op)
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1630
1788
|
conditional(_test: ParsedExpr, _consequent: ParsedExpr, _alternate: ParsedExpr): string {
|
|
1631
1789
|
return '1'
|
|
1632
1790
|
}
|
|
@@ -1808,6 +1966,18 @@ class MojoTopLevelEmitter implements ParsedExprEmitter {
|
|
|
1808
1966
|
return renderSortMethod(emit(object), comparator)
|
|
1809
1967
|
}
|
|
1810
1968
|
|
|
1969
|
+
reduceMethod(method: 'reduce' | 'reduceRight', object: ParsedExpr, reduceOp: ReduceOp, emit: (e: ParsedExpr) => string): string {
|
|
1970
|
+
return renderReduceMethod(emit(object), reduceOp, method === 'reduceRight' ? 'right' : 'left')
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
flatMethod(object: ParsedExpr, depth: FlatDepth, emit: (e: ParsedExpr) => string): string {
|
|
1974
|
+
return renderFlatMethod(emit(object), depth)
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
flatMapMethod(object: ParsedExpr, op: FlatMapOp, emit: (e: ParsedExpr) => string): string {
|
|
1978
|
+
return renderFlatMapMethod(emit(object), op)
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1811
1981
|
conditional(
|
|
1812
1982
|
test: ParsedExpr,
|
|
1813
1983
|
consequent: ParsedExpr,
|