@spintax/core 0.2.0 → 0.3.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/CHANGELOG.md +97 -0
- package/README.md +5 -4
- package/dist/index.cjs +182 -91
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +12 -3
- package/dist/index.d.ts +12 -3
- package/dist/index.js +182 -91
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -15,8 +15,14 @@
|
|
|
15
15
|
* the AST here.
|
|
16
16
|
* Permutation's `<config>` + per-element separators are fully parsed (PR-11b).
|
|
17
17
|
*/
|
|
18
|
-
/**
|
|
19
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Bumped only on a breaking change to the node shape (independent of syntax v1).
|
|
20
|
+
*
|
|
21
|
+
* 2 — `ParsedAst` gained `defDefs`. An `Ast` cached by an older version carries no `#def` map, so
|
|
22
|
+
* rendering it would silently drop every definition; the version guard turns that into an
|
|
23
|
+
* `AstVersionError` instead.
|
|
24
|
+
*/
|
|
25
|
+
declare const AST_VERSION = 2;
|
|
20
26
|
/** Opaque public handle (re-exported as `Ast` from the package index). */
|
|
21
27
|
interface Ast {
|
|
22
28
|
readonly astVersion: typeof AST_VERSION;
|
|
@@ -75,7 +81,7 @@ interface PluralIssue {
|
|
|
75
81
|
* (which injects a fixture's `rng` strategy). Keeping the rng a parameter is what
|
|
76
82
|
* lets the deterministic parity fixtures assert exact output.
|
|
77
83
|
*
|
|
78
|
-
* Order: strip stray sentinels →
|
|
84
|
+
* Order: strip stray sentinels → build vars, roll #def, tree-walk (+ #include) →
|
|
79
85
|
* cosmetic post-process (if on) → mandatory neutralize safety-restore (always).
|
|
80
86
|
*/
|
|
81
87
|
|
|
@@ -166,7 +172,10 @@ interface Diagnostic {
|
|
|
166
172
|
}
|
|
167
173
|
interface ExtractResult {
|
|
168
174
|
refs: string[];
|
|
175
|
+
/** Names defined by `#set` — macros, re-picked at every reference. */
|
|
169
176
|
sets: string[];
|
|
177
|
+
/** Names defined by `#def` — rolled once per render and held. */
|
|
178
|
+
defs: string[];
|
|
170
179
|
includes: string[];
|
|
171
180
|
}
|
|
172
181
|
interface Analysis extends ExtractResult {
|
package/dist/index.d.ts
CHANGED
|
@@ -15,8 +15,14 @@
|
|
|
15
15
|
* the AST here.
|
|
16
16
|
* Permutation's `<config>` + per-element separators are fully parsed (PR-11b).
|
|
17
17
|
*/
|
|
18
|
-
/**
|
|
19
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Bumped only on a breaking change to the node shape (independent of syntax v1).
|
|
20
|
+
*
|
|
21
|
+
* 2 — `ParsedAst` gained `defDefs`. An `Ast` cached by an older version carries no `#def` map, so
|
|
22
|
+
* rendering it would silently drop every definition; the version guard turns that into an
|
|
23
|
+
* `AstVersionError` instead.
|
|
24
|
+
*/
|
|
25
|
+
declare const AST_VERSION = 2;
|
|
20
26
|
/** Opaque public handle (re-exported as `Ast` from the package index). */
|
|
21
27
|
interface Ast {
|
|
22
28
|
readonly astVersion: typeof AST_VERSION;
|
|
@@ -75,7 +81,7 @@ interface PluralIssue {
|
|
|
75
81
|
* (which injects a fixture's `rng` strategy). Keeping the rng a parameter is what
|
|
76
82
|
* lets the deterministic parity fixtures assert exact output.
|
|
77
83
|
*
|
|
78
|
-
* Order: strip stray sentinels →
|
|
84
|
+
* Order: strip stray sentinels → build vars, roll #def, tree-walk (+ #include) →
|
|
79
85
|
* cosmetic post-process (if on) → mandatory neutralize safety-restore (always).
|
|
80
86
|
*/
|
|
81
87
|
|
|
@@ -166,7 +172,10 @@ interface Diagnostic {
|
|
|
166
172
|
}
|
|
167
173
|
interface ExtractResult {
|
|
168
174
|
refs: string[];
|
|
175
|
+
/** Names defined by `#set` — macros, re-picked at every reference. */
|
|
169
176
|
sets: string[];
|
|
177
|
+
/** Names defined by `#def` — rolled once per render and held. */
|
|
178
|
+
defs: string[];
|
|
170
179
|
includes: string[];
|
|
171
180
|
}
|
|
172
181
|
interface Analysis extends ExtractResult {
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/internal/ast.ts
|
|
2
|
-
var AST_VERSION =
|
|
2
|
+
var AST_VERSION = 2;
|
|
3
3
|
function walk(nodes, visit) {
|
|
4
4
|
for (const n of nodes) {
|
|
5
5
|
visit(n);
|
|
@@ -21,23 +21,66 @@ function isParsedAst(value) {
|
|
|
21
21
|
return typeof value === "object" && value !== null && value.astVersion === AST_VERSION && typeof value.source === "string" && Array.isArray(value.nodes);
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
// src/internal/neutralize.ts
|
|
25
|
+
var STRUCTURAL = ["{", "}", "[", "]", "%", "#"];
|
|
26
|
+
var SENTINEL_BASE = 57344;
|
|
27
|
+
var SHIELD = /* @__PURE__ */ new Map();
|
|
28
|
+
var RESTORE = /* @__PURE__ */ new Map();
|
|
29
|
+
STRUCTURAL.forEach((ch, i) => {
|
|
30
|
+
const sentinel = String.fromCharCode(SENTINEL_BASE + i);
|
|
31
|
+
SHIELD.set(ch, sentinel);
|
|
32
|
+
RESTORE.set(sentinel, ch);
|
|
33
|
+
});
|
|
34
|
+
var SHIELD_RE = /[{}[\]%#]/gu;
|
|
35
|
+
var RESTORE_RE = new RegExp(
|
|
36
|
+
`[${String.fromCharCode(SENTINEL_BASE)}-${String.fromCharCode(SENTINEL_BASE + STRUCTURAL.length - 1)}]`,
|
|
37
|
+
"gu"
|
|
38
|
+
);
|
|
39
|
+
function neutralize(value) {
|
|
40
|
+
return value.replace(SHIELD_RE, (ch) => SHIELD.get(ch) ?? ch);
|
|
41
|
+
}
|
|
42
|
+
function safetyRestore(text) {
|
|
43
|
+
return text.replace(RESTORE_RE, (ch) => RESTORE.get(ch) ?? ch);
|
|
44
|
+
}
|
|
45
|
+
function stripSentinels(text) {
|
|
46
|
+
return text.replace(RESTORE_RE, "");
|
|
47
|
+
}
|
|
48
|
+
|
|
24
49
|
// src/internal/parser.ts
|
|
25
50
|
var VARIABLE_RE = /^%(\w+)%/;
|
|
26
|
-
var
|
|
51
|
+
var DIRECTIVE_RE = /^[ \t]*#(set|def)[ \t]+%(\w+)%[ \t]*=[ \t]*(.*?)[ \t]*\r?$/gmu;
|
|
27
52
|
var CONDITIONAL_NAME_RE = /^[A-Za-z_]\w*/;
|
|
28
53
|
var PLURAL_PREFIX = "plural ";
|
|
29
54
|
function parseTemplate(src) {
|
|
30
|
-
const { body, setDefs } =
|
|
31
|
-
return { astVersion: AST_VERSION, source: src, setDefs, nodes: parseSequence(body) };
|
|
55
|
+
const { body, setDefs, defDefs } = extractDirectives(stripComments(stripSentinels(src)));
|
|
56
|
+
return { astVersion: AST_VERSION, source: src, setDefs, defDefs, nodes: parseSequence(body) };
|
|
32
57
|
}
|
|
33
|
-
function
|
|
58
|
+
function extractDirectives(text) {
|
|
34
59
|
const setDefs = {};
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
60
|
+
const defDefs = {};
|
|
61
|
+
const occurrences = [];
|
|
62
|
+
DIRECTIVE_RE.lastIndex = 0;
|
|
63
|
+
const stripped = text.replace(
|
|
64
|
+
DIRECTIVE_RE,
|
|
65
|
+
(full, kind, rawName, value, offset) => {
|
|
66
|
+
const name = rawName.toLowerCase();
|
|
67
|
+
occurrences.push({
|
|
68
|
+
kind,
|
|
69
|
+
name,
|
|
70
|
+
value,
|
|
71
|
+
line: countLines(text, offset)
|
|
72
|
+
});
|
|
73
|
+
if (kind === "def") defDefs[name] = value;
|
|
74
|
+
else setDefs[name] = value;
|
|
75
|
+
return "";
|
|
76
|
+
}
|
|
77
|
+
);
|
|
78
|
+
return { body: stripped.replace(/\n{3,}/gu, "\n\n"), setDefs, defDefs, occurrences };
|
|
79
|
+
}
|
|
80
|
+
function countLines(text, offset) {
|
|
81
|
+
let line = 1;
|
|
82
|
+
for (let i = 0; i < offset; i += 1) if (text[i] === "\n") line += 1;
|
|
83
|
+
return line;
|
|
41
84
|
}
|
|
42
85
|
function stripComments(text) {
|
|
43
86
|
return text.replace(/\/#[\s\S]*?#\//g, "");
|
|
@@ -376,7 +419,7 @@ function validateTemplate(src, opts = {}) {
|
|
|
376
419
|
const diagnostics = [];
|
|
377
420
|
const text = stripComments(src);
|
|
378
421
|
checkBrackets(text, diagnostics);
|
|
379
|
-
|
|
422
|
+
checkDirectives(text, diagnostics);
|
|
380
423
|
checkPermutationConfigs(text, diagnostics);
|
|
381
424
|
checkPlurals(text, opts.locale, diagnostics);
|
|
382
425
|
checkVariableReferences(text, opts.knownVariables, diagnostics);
|
|
@@ -435,17 +478,43 @@ function checkBrackets(text, out) {
|
|
|
435
478
|
out.push(err("bracket.unclosed", `Unclosed '${unclosed.char}'.`, { line: unclosed.line, column: unclosed.column, endLine: unclosed.line, endColumn: unclosed.column + 1, data: { bracket: unclosed.char } }));
|
|
436
479
|
}
|
|
437
480
|
}
|
|
438
|
-
function
|
|
481
|
+
function checkDirectives(text, out) {
|
|
439
482
|
const lines = text.split("\n");
|
|
440
483
|
lines.forEach((lineText, idx) => {
|
|
441
484
|
const trimmed = lineText.replace(/^[ \t]+/, "");
|
|
442
|
-
|
|
443
|
-
|
|
485
|
+
const kind = ["#set", "#def"].find(
|
|
486
|
+
(candidate) => trimmed.startsWith(`${candidate} `) || trimmed.startsWith(`${candidate} `)
|
|
487
|
+
);
|
|
488
|
+
if (!kind) return;
|
|
489
|
+
DIRECTIVE_RE.lastIndex = 0;
|
|
490
|
+
if (!DIRECTIVE_RE.test(trimmed)) {
|
|
444
491
|
const column = lineText.length - trimmed.length + 1;
|
|
445
492
|
const line = idx + 1;
|
|
446
|
-
|
|
493
|
+
const code = kind === "#def" ? "def.malformed" : "set.malformed";
|
|
494
|
+
out.push(err(code, `Malformed ${kind}. Expected: ${kind} %name% = value`, { line, column, endLine: line, endColumn: lineText.length + 1 }));
|
|
447
495
|
}
|
|
448
496
|
});
|
|
497
|
+
const { occurrences } = extractDirectives(text);
|
|
498
|
+
const seen = /* @__PURE__ */ new Map();
|
|
499
|
+
for (const occurrence of occurrences) {
|
|
500
|
+
const first = seen.get(occurrence.name);
|
|
501
|
+
if (first !== void 0) {
|
|
502
|
+
out.push(err(
|
|
503
|
+
"definition.duplicate-name",
|
|
504
|
+
`Variable '${occurrence.name}' is defined more than once (first on line ${first}). A name belongs to one directive, once.`,
|
|
505
|
+
{ line: occurrence.line, column: 1 }
|
|
506
|
+
));
|
|
507
|
+
} else {
|
|
508
|
+
seen.set(occurrence.name, occurrence.line);
|
|
509
|
+
}
|
|
510
|
+
if (occurrence.kind === "def" && /#include\b/u.test(occurrence.value)) {
|
|
511
|
+
out.push(err(
|
|
512
|
+
"def.include-in-value",
|
|
513
|
+
`#include cannot appear in a #def value ('${occurrence.name}'): includes resolve after the value is frozen. Use #set, or put the #include in the body.`,
|
|
514
|
+
{ line: occurrence.line, column: 1 }
|
|
515
|
+
));
|
|
516
|
+
}
|
|
517
|
+
}
|
|
449
518
|
}
|
|
450
519
|
function checkPermutationConfigs(text, out) {
|
|
451
520
|
for (const m of text.matchAll(/\[<([^>]*?)>/gu)) {
|
|
@@ -483,10 +552,20 @@ function checkPermutationConfigs(text, out) {
|
|
|
483
552
|
function checkPlurals(text, locale, out) {
|
|
484
553
|
const base = locale && locale !== "" ? normalizeBaseLang(locale) : "";
|
|
485
554
|
const arity = base !== "" ? pluralArity(base) : 0;
|
|
555
|
+
const tainted = macroTaintedNames(text);
|
|
486
556
|
for (const block of findPluralBlocks(text)) {
|
|
487
557
|
const at = span(text, block.start, block.end - block.start);
|
|
558
|
+
for (const m of block.countSlot.matchAll(/%(\w+)%/gu)) {
|
|
559
|
+
const name = (m[1] ?? "").toLowerCase();
|
|
560
|
+
if (!tainted.has(name)) continue;
|
|
561
|
+
out.push(err(
|
|
562
|
+
"plural.count-macro",
|
|
563
|
+
`{plural ...}: the count '${m[1]}' is a #set macro, so it is still unresolved spintax when the plural is decided and the block renders empty. Define it with #def instead.`,
|
|
564
|
+
at
|
|
565
|
+
));
|
|
566
|
+
}
|
|
488
567
|
if (/[{}[\]]/.test(block.formsRaw)) {
|
|
489
|
-
out.push(err("plural.nested-brackets", "{plural ...}: forms must not contain nested spintax brackets.", at));
|
|
568
|
+
out.push(err("plural.nested-brackets", "{plural ...}: forms must not contain nested spintax brackets. Extract via #def first \u2014 a #set is substituted verbatim and would put the brackets straight back.", at));
|
|
490
569
|
continue;
|
|
491
570
|
}
|
|
492
571
|
if (arity > 0) {
|
|
@@ -501,11 +580,33 @@ function checkPlurals(text, locale, out) {
|
|
|
501
580
|
}
|
|
502
581
|
}
|
|
503
582
|
}
|
|
583
|
+
var UNRESOLVED_AT_PLURAL_TIME = /\[|\{(?!\?)/u;
|
|
584
|
+
function macroTaintedNames(text) {
|
|
585
|
+
const macros = extractDirectives(text).setDefs;
|
|
586
|
+
const tainted = /* @__PURE__ */ new Set();
|
|
587
|
+
for (const [name, value] of Object.entries(macros)) {
|
|
588
|
+
if (UNRESOLVED_AT_PLURAL_TIME.test(value)) tainted.add(name);
|
|
589
|
+
}
|
|
590
|
+
let grew = true;
|
|
591
|
+
while (grew) {
|
|
592
|
+
grew = false;
|
|
593
|
+
for (const [name, value] of Object.entries(macros)) {
|
|
594
|
+
if (tainted.has(name)) continue;
|
|
595
|
+
for (const m of value.matchAll(/%(\w+)%/gu)) {
|
|
596
|
+
if (!tainted.has((m[1] ?? "").toLowerCase())) continue;
|
|
597
|
+
tainted.add(name);
|
|
598
|
+
grew = true;
|
|
599
|
+
break;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
return tainted;
|
|
604
|
+
}
|
|
504
605
|
function checkVariableReferences(text, known, out) {
|
|
505
606
|
const knownSet = new Set((known ?? []).map((n) => n.toLowerCase()));
|
|
506
607
|
const defs = /* @__PURE__ */ new Map();
|
|
507
608
|
const defPos = /* @__PURE__ */ new Map();
|
|
508
|
-
for (const m of text.matchAll(/^[ \t]*#set[ \t]+%(\w+)%[ \t]*=[ \t]*(.*?)$/gmu)) {
|
|
609
|
+
for (const m of text.matchAll(/^[ \t]*#(?:set|def)[ \t]+%(\w+)%[ \t]*=[ \t]*(.*?)$/gmu)) {
|
|
509
610
|
const name = (m[1] ?? "").toLowerCase();
|
|
510
611
|
defs.set(name, m[2] ?? "");
|
|
511
612
|
const nameOffset = (m.index ?? 0) + m[0].indexOf("%");
|
|
@@ -520,7 +621,7 @@ function checkVariableReferences(text, known, out) {
|
|
|
520
621
|
for (const name of defs.keys()) {
|
|
521
622
|
detectCycle(name, defs, [name], defPos.get(name) ?? somewhere, out);
|
|
522
623
|
}
|
|
523
|
-
const body = text.replace(/^[ \t]*#set[ \t]+%\w+%[ \t]*=[ \t]*.*?$/gmu, (m) => m.replace(/[^\n]/g, " "));
|
|
624
|
+
const body = text.replace(/^[ \t]*#(?:set|def)[ \t]+%\w+%[ \t]*=[ \t]*.*?$/gmu, (m) => m.replace(/[^\n]/g, " "));
|
|
524
625
|
const seen = /* @__PURE__ */ new Set();
|
|
525
626
|
const undefinedAt = (name, offset, length) => {
|
|
526
627
|
const key = name.toLowerCase();
|
|
@@ -567,19 +668,21 @@ function checkIncludeTargets(text, known, out) {
|
|
|
567
668
|
|
|
568
669
|
// src/internal/extract.ts
|
|
569
670
|
var SET_DEF_RE = /^[ \t]*#set[ \t]+%(\w+)%[ \t]*=/gmu;
|
|
570
|
-
var
|
|
671
|
+
var DEF_DEF_RE = /^[ \t]*#def[ \t]+%(\w+)%[ \t]*=/gmu;
|
|
672
|
+
var DEFINITION_LHS_RE = /^[ \t]*#(?:set|def)[ \t]+%\w+%[ \t]*=/gmu;
|
|
571
673
|
var INCLUDE_RE2 = /^[ \t]*#include[ \t\n\r\f\x0B]+"([^"]+)"[ \t\n\r\f\x0B]*$/gmu;
|
|
572
674
|
var VARIABLE_RE2 = /%(\w+)%/gu;
|
|
573
675
|
var CONDITIONAL_REF_RE = /\{\?!?([A-Za-z_]\w*)\?/gu;
|
|
574
676
|
function extractFromSource(src) {
|
|
575
677
|
const text = stripComments(src);
|
|
576
678
|
const sets = collect(text, SET_DEF_RE, true);
|
|
679
|
+
const defs = collect(text, DEF_DEF_RE, true);
|
|
577
680
|
const includes = collect(text, INCLUDE_RE2, false);
|
|
578
|
-
const body = text.replace(
|
|
681
|
+
const body = text.replace(DEFINITION_LHS_RE, "");
|
|
579
682
|
const refs = /* @__PURE__ */ new Set();
|
|
580
683
|
addAll(refs, body, VARIABLE_RE2, true);
|
|
581
684
|
addAll(refs, body, CONDITIONAL_REF_RE, true);
|
|
582
|
-
return { refs: [...refs], sets, includes };
|
|
685
|
+
return { refs: [...refs], sets, defs, includes };
|
|
583
686
|
}
|
|
584
687
|
function collect(text, re, fold) {
|
|
585
688
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -622,43 +725,19 @@ var NotImplementedError = class extends SpintaxError {
|
|
|
622
725
|
}
|
|
623
726
|
};
|
|
624
727
|
|
|
625
|
-
// src/internal/neutralize.ts
|
|
626
|
-
var STRUCTURAL = ["{", "}", "[", "]", "%", "#"];
|
|
627
|
-
var SENTINEL_BASE = 57344;
|
|
628
|
-
var SHIELD = /* @__PURE__ */ new Map();
|
|
629
|
-
var RESTORE = /* @__PURE__ */ new Map();
|
|
630
|
-
STRUCTURAL.forEach((ch, i) => {
|
|
631
|
-
const sentinel = String.fromCharCode(SENTINEL_BASE + i);
|
|
632
|
-
SHIELD.set(ch, sentinel);
|
|
633
|
-
RESTORE.set(sentinel, ch);
|
|
634
|
-
});
|
|
635
|
-
var SHIELD_RE = /[{}[\]%#]/gu;
|
|
636
|
-
var RESTORE_RE = new RegExp(
|
|
637
|
-
`[${String.fromCharCode(SENTINEL_BASE)}-${String.fromCharCode(SENTINEL_BASE + STRUCTURAL.length - 1)}]`,
|
|
638
|
-
"gu"
|
|
639
|
-
);
|
|
640
|
-
function neutralize(value) {
|
|
641
|
-
return value.replace(SHIELD_RE, (ch) => SHIELD.get(ch) ?? ch);
|
|
642
|
-
}
|
|
643
|
-
function safetyRestore(text) {
|
|
644
|
-
return text.replace(RESTORE_RE, (ch) => RESTORE.get(ch) ?? ch);
|
|
645
|
-
}
|
|
646
|
-
function stripSentinels(text) {
|
|
647
|
-
return text.replace(RESTORE_RE, "");
|
|
648
|
-
}
|
|
649
|
-
|
|
650
728
|
// src/internal/render.ts
|
|
651
729
|
var MAX_VARIABLE_DEPTH = 50;
|
|
652
730
|
var INCLUDE_LINE_RE = /^[ \t]*#include[ \t\n\r\f\x0B]+"([^"]+)"[ \t\n\r\f\x0B]*$/gmu;
|
|
653
731
|
function renderAst(ast, ctx) {
|
|
654
|
-
const
|
|
655
|
-
const
|
|
656
|
-
vars,
|
|
732
|
+
const base = buildVars(ast.setDefs, ctx.runtimeContext);
|
|
733
|
+
const walkOpts = {
|
|
657
734
|
rng: ctx.rng,
|
|
658
735
|
locale: ctx.locale,
|
|
659
736
|
depth: 0,
|
|
660
737
|
onPluralError: ctx.onPluralError
|
|
661
|
-
}
|
|
738
|
+
};
|
|
739
|
+
const vars = Object.keys(ast.defDefs).length > 0 ? { ...base, ...rollDefinitions(ast.defDefs, base, ctx.runtimeContext, walkOpts) } : base;
|
|
740
|
+
const text = renderNodes(ast.nodes, { ...walkOpts, vars });
|
|
662
741
|
return ctx.resolver ? resolveIncludes(text, ctx) : text;
|
|
663
742
|
}
|
|
664
743
|
function resolveIncludes(text, ctx) {
|
|
@@ -672,7 +751,7 @@ function resolveIncludes(text, ctx) {
|
|
|
672
751
|
throw new IncludeResolverError(`includeResolver threw for "${ref}"`, { cause });
|
|
673
752
|
}
|
|
674
753
|
if (included === null) return "";
|
|
675
|
-
return renderAst(parseTemplate(
|
|
754
|
+
return renderAst(parseTemplate(included), {
|
|
676
755
|
...ctx,
|
|
677
756
|
runtimeContext: ctx.runtimeContext,
|
|
678
757
|
// child inherits runtime, not parent #set
|
|
@@ -680,33 +759,65 @@ function resolveIncludes(text, ctx) {
|
|
|
680
759
|
});
|
|
681
760
|
});
|
|
682
761
|
}
|
|
683
|
-
function buildVars(setDefs, context
|
|
762
|
+
function buildVars(setDefs, context) {
|
|
684
763
|
const vars = {};
|
|
685
764
|
for (const [name, value] of Object.entries(setDefs)) {
|
|
686
|
-
vars[name] =
|
|
765
|
+
vars[name] = value;
|
|
687
766
|
}
|
|
688
767
|
for (const [name, value] of Object.entries(context)) {
|
|
689
768
|
vars[name.toLowerCase()] = value;
|
|
690
769
|
}
|
|
691
770
|
return vars;
|
|
692
771
|
}
|
|
693
|
-
function
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
772
|
+
function rollDefinitions(defDefs, vars, context, opts) {
|
|
773
|
+
const outranked = new Set(Object.keys(context).map((key) => key.toLowerCase()));
|
|
774
|
+
const rolled = {};
|
|
775
|
+
const aliases = {};
|
|
776
|
+
for (const [name, value] of Object.entries(vars)) {
|
|
777
|
+
if (name in defDefs && !outranked.has(name)) continue;
|
|
778
|
+
aliases[name] = value;
|
|
779
|
+
}
|
|
780
|
+
for (const name of orderDefinitions(defDefs, aliases)) {
|
|
781
|
+
if (outranked.has(name)) continue;
|
|
782
|
+
const value = defDefs[name] ?? "";
|
|
783
|
+
rolled[name] = renderNodes(parseSequence(value), { ...opts, vars: { ...vars, ...rolled } });
|
|
784
|
+
}
|
|
785
|
+
return rolled;
|
|
786
|
+
}
|
|
787
|
+
function orderDefinitions(defDefs, aliases) {
|
|
788
|
+
const names = Object.keys(defDefs);
|
|
789
|
+
const blocked = /* @__PURE__ */ new Map();
|
|
790
|
+
for (const name of names) {
|
|
791
|
+
const reached = referencedNames(defDefs[name] ?? "", aliases);
|
|
792
|
+
blocked.set(name, new Set(names.filter((candidate) => reached.has(candidate))));
|
|
793
|
+
}
|
|
794
|
+
const ordered = [];
|
|
795
|
+
let pending = names;
|
|
796
|
+
while (pending.length > 0) {
|
|
797
|
+
const ready = pending.filter((name) => {
|
|
798
|
+
const deps = blocked.get(name);
|
|
799
|
+
return !deps || ![...deps].some((dep) => dep !== name && pending.includes(dep));
|
|
706
800
|
});
|
|
707
|
-
if (
|
|
801
|
+
if (ready.length === 0) return [...ordered, ...pending];
|
|
802
|
+
ordered.push(...ready);
|
|
803
|
+
pending = pending.filter((name) => !ready.includes(name));
|
|
708
804
|
}
|
|
709
|
-
return
|
|
805
|
+
return ordered;
|
|
806
|
+
}
|
|
807
|
+
function referencedNames(value, aliases) {
|
|
808
|
+
const seen = /* @__PURE__ */ new Set();
|
|
809
|
+
const queue = directReferences(value);
|
|
810
|
+
while (queue.length > 0) {
|
|
811
|
+
const name = queue.shift();
|
|
812
|
+
if (seen.has(name)) continue;
|
|
813
|
+
seen.add(name);
|
|
814
|
+
const alias = aliases[name];
|
|
815
|
+
if (alias !== void 0) queue.push(...directReferences(alias));
|
|
816
|
+
}
|
|
817
|
+
return seen;
|
|
818
|
+
}
|
|
819
|
+
function directReferences(text) {
|
|
820
|
+
return [...text.matchAll(/%(\w+)%/gu)].map((match) => (match[1] ?? "").toLowerCase());
|
|
710
821
|
}
|
|
711
822
|
function renderNodes(nodes, opts) {
|
|
712
823
|
let out = "";
|
|
@@ -766,7 +877,7 @@ function renderPlural(node, opts) {
|
|
|
766
877
|
if (/[{}[\]]/u.test(formsRaw)) {
|
|
767
878
|
report({
|
|
768
879
|
code: "plural.nested-brackets",
|
|
769
|
-
message: "Plural form slot contains nested spintax brackets; extract via #set
|
|
880
|
+
message: "Plural form slot contains nested spintax brackets; extract via #def first \u2014 a #set is substituted verbatim and would put the brackets straight back.",
|
|
770
881
|
construct: rawConstruct(countRaw, formsRaw),
|
|
771
882
|
locale: base
|
|
772
883
|
});
|
|
@@ -865,26 +976,6 @@ function padSeparator(sep) {
|
|
|
865
976
|
if (/^\p{L}+$/u.test(trimmed)) return ` ${trimmed} `;
|
|
866
977
|
return sep;
|
|
867
978
|
}
|
|
868
|
-
function splitTopLevelPipes(inner) {
|
|
869
|
-
const parts = [];
|
|
870
|
-
let brace = 0;
|
|
871
|
-
let bracket = 0;
|
|
872
|
-
let cur = "";
|
|
873
|
-
for (const ch of inner) {
|
|
874
|
-
if (ch === "{") brace += 1;
|
|
875
|
-
else if (ch === "}") brace -= 1;
|
|
876
|
-
else if (ch === "[") bracket += 1;
|
|
877
|
-
else if (ch === "]") bracket -= 1;
|
|
878
|
-
if (ch === "|" && brace === 0 && bracket === 0) {
|
|
879
|
-
parts.push(cur);
|
|
880
|
-
cur = "";
|
|
881
|
-
} else {
|
|
882
|
-
cur += ch;
|
|
883
|
-
}
|
|
884
|
-
}
|
|
885
|
-
parts.push(cur);
|
|
886
|
-
return parts;
|
|
887
|
-
}
|
|
888
979
|
var PHP_LTRIM_RE2 = /^[ \t\n\r\0\x0B]+/u;
|
|
889
980
|
var PHP_RTRIM_RE2 = /[ \t\n\r\0\x0B]+$/u;
|
|
890
981
|
function phpTrim2(s) {
|
|
@@ -1020,7 +1111,7 @@ function renderWith(input, rng, opts = {}) {
|
|
|
1020
1111
|
includeStack: [],
|
|
1021
1112
|
onPluralError: opts.onPluralError
|
|
1022
1113
|
};
|
|
1023
|
-
const ast = resolveAst(
|
|
1114
|
+
const ast = resolveAst(input);
|
|
1024
1115
|
let out = renderAst(ast, ctx);
|
|
1025
1116
|
if (opts.postProcess !== false) out = postProcess(out);
|
|
1026
1117
|
return safetyRestore(out);
|