@spintax/core 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +69 -0
- package/README.md +4 -3
- package/dist/index.cjs +154 -63
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -2
- package/dist/index.d.ts +11 -2
- package/dist/index.js +154 -63
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,75 @@
|
|
|
3
3
|
All notable changes to `@spintax/core` are documented here. This project adheres to
|
|
4
4
|
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
5
5
|
|
|
6
|
+
## 0.3.0 — 2026-07-19
|
|
7
|
+
|
|
8
|
+
`#set` goes back to being a macro and a new `#def` carries roll-once. Breaking: it changes what
|
|
9
|
+
existing templates mean. Ships in lockstep with the WordPress plugin 3.0.0, `spintax/core` and the
|
|
10
|
+
OpenCart port; the corpus landed last, after both PHP engines.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- **`#set` is a macro.** The value is substituted at every `%var%` reference and whatever brackets
|
|
15
|
+
it holds resolve independently each time. Until now an enumeration-valued `#set` collapsed once at
|
|
16
|
+
set-time; that behaviour moved to `#def`.
|
|
17
|
+
|
|
18
|
+
Worth recording *why* the revert, because the reasoning is not recoverable from the diff:
|
|
19
|
+
collapse-once was the newcomer. It shipped in the plugin on 2026-07-04, was announced in one
|
|
20
|
+
changelog line, and contradicted the project's published documentation from the day it landed.
|
|
21
|
+
Macro expansion is what the engine did before that and what consumers written against those docs
|
|
22
|
+
assume.
|
|
23
|
+
|
|
24
|
+
- **`AST_VERSION` 1 → 2.** `ParsedAst` gained `defDefs`. An `Ast` cached by an older version carries
|
|
25
|
+
no definition map, so rendering it would silently drop every `#def`; the version guard turns that
|
|
26
|
+
into an `AstVersionError` instead.
|
|
27
|
+
|
|
28
|
+
- **`extract()` reports `defs`** alongside `sets`. Additive for readers, but a consumer building a
|
|
29
|
+
variable list from `sets` alone will now miss `#def` names.
|
|
30
|
+
|
|
31
|
+
- **`plural.nested-brackets` advice.** "Extract via `#set` first" was correct under collapse-once and
|
|
32
|
+
is wrong under a macro — the value is substituted verbatim and puts the brackets straight back into
|
|
33
|
+
the form slot, raising the very error it was meant to avoid. It now says `#def`.
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- **`#def %var% = value` — roll-once.** The value is rendered once per render, as if it were a
|
|
38
|
+
miniature body, and the result is held for every reference. It covers enumerations *and*
|
|
39
|
+
permutations, resolves **after** the merged context exists (so it can read globals and runtime
|
|
40
|
+
variables; a runtime variable of the same name outranks it), and resolves in dependency order —
|
|
41
|
+
an order that follows aliases **through** macro values, since a `#def` can reach another `#def` by
|
|
42
|
+
way of a `#set` expanded only at reference time.
|
|
43
|
+
|
|
44
|
+
This is where a plural counter now lives: `#def %n% = {1|4|9}` then `{plural %n%: …}` prints and
|
|
45
|
+
agrees the same number. Under `#set` the two disagree and the block is dropped.
|
|
46
|
+
|
|
47
|
+
- **Four diagnostics**: `def.malformed`, `definition.duplicate-name` (a name belongs to one
|
|
48
|
+
directive, once — this also closes silently-last-wins duplicate `#set`s), `def.include-in-value`
|
|
49
|
+
(includes resolve after a value is frozen; legal inside a `#set`), and `plural.count-macro`.
|
|
50
|
+
|
|
51
|
+
`plural.count-macro` is decided by **stage order, not bracket type**: conditionals resolve before
|
|
52
|
+
plurals and are exempt, enumerations and permutations resolve after and are not, and a nested
|
|
53
|
+
`{plural …}` resolves in the *same* pass so it is not exempt either. Taint propagates through
|
|
54
|
+
`#set` → `#set` references to a fixpoint.
|
|
55
|
+
|
|
56
|
+
### Corpus
|
|
57
|
+
|
|
58
|
+
The difference between the directives is a difference in how many RNG draws a render consumes, so
|
|
59
|
+
seeded-sequence fixtures pin it exactly and cross-engine — `set/macro-re-rolls-at-every-reference`
|
|
60
|
+
and `def/rolled-once-and-held` share a template and a sequence and differ only in whether the second
|
|
61
|
+
draw is reached. The corpus grew 138 → 160 cases, green against this engine, the WordPress plugin
|
|
62
|
+
and `spintax/core`.
|
|
63
|
+
|
|
64
|
+
The previous rule here said collapse could only be pinned with RNG-free values, and the consequence
|
|
65
|
+
was that **nothing pinned it at all**: the semantics flipped in the plugin without a single fixture
|
|
66
|
+
noticing. All-identical alternatives are not sufficient either — they render the same under both
|
|
67
|
+
semantics and would pass against an engine implementing `#def` as an alias of `#set`.
|
|
68
|
+
|
|
69
|
+
### Migration
|
|
70
|
+
|
|
71
|
+
A `#set` whose value is an enumeration or permutation *and* which is referenced more than once for
|
|
72
|
+
consistency — a plural counter, a brand name that must not vary mid-sentence — becomes `#def`. One
|
|
73
|
+
line per definition; references are untouched.
|
|
74
|
+
|
|
6
75
|
## 0.2.0 — 2026-07-18
|
|
7
76
|
|
|
8
77
|
Serbian, Croatian and Bosnian join the 3-form plural family; the plural error model and the
|
package/README.md
CHANGED
|
@@ -17,8 +17,8 @@ parse, render, validate, extract, analyze, and neutralize spintax templates.
|
|
|
17
17
|
- **Parity-verified against the [Spintax WordPress plugin](https://wordpress.org/plugins/spintax/).**
|
|
18
18
|
An *independent* TypeScript implementation (not a line-by-line port) held to the plugin's behavior
|
|
19
19
|
contract by a shared golden corpus — the **same** fixtures pass against both this engine and the
|
|
20
|
-
PHP plugin (deterministic verdicts, plural buckets, conditionals, `#set`
|
|
21
|
-
with no divergence.
|
|
20
|
+
PHP plugin (deterministic verdicts, plural buckets, conditionals, `#set`/`#def` semantics,
|
|
21
|
+
post-process), with no divergence.
|
|
22
22
|
- **MIT** licensed.
|
|
23
23
|
|
|
24
24
|
> **Status: released & stable.** Feature-complete — parse / render / validate / extract / analyze /
|
|
@@ -77,7 +77,8 @@ dedupe in the host and cap the retries: a template may simply not have N combina
|
|
|
77
77
|
| Enumeration | `{a\|b\|c}` | pick one (nestable: `{a\|{b\|c}}`) |
|
|
78
78
|
| Permutation | `[a\|b\|c]` | pick N, shuffle, join — `[<minsize=1;maxsize=2;sep=", ">a\|b\|c]` |
|
|
79
79
|
| Variable | `%var%` | substitute a context value |
|
|
80
|
-
| Local set | `#set %v% = value` | define a
|
|
80
|
+
| Local set | `#set %v% = value` | define a macro — re-picked at every use |
|
|
81
|
+
| Local def | `#def %v% = value` | define a value — picked once per render, held at every use |
|
|
81
82
|
| Conditional | `{?VAR?then\|else}` | `then` if `VAR` is truthy, else `else` |
|
|
82
83
|
| Plural | `{plural %n%: one\|few\|many}` | grammatical agreement by locale |
|
|
83
84
|
| Include | `#include "slug-or-id"` | embed another template (host-resolved) |
|
package/dist/index.cjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
// src/internal/ast.ts
|
|
4
|
-
var AST_VERSION =
|
|
4
|
+
var AST_VERSION = 2;
|
|
5
5
|
function walk(nodes, visit) {
|
|
6
6
|
for (const n of nodes) {
|
|
7
7
|
visit(n);
|
|
@@ -25,21 +25,39 @@ function isParsedAst(value) {
|
|
|
25
25
|
|
|
26
26
|
// src/internal/parser.ts
|
|
27
27
|
var VARIABLE_RE = /^%(\w+)%/;
|
|
28
|
-
var
|
|
28
|
+
var DIRECTIVE_RE = /^[ \t]*#(set|def)[ \t]+%(\w+)%[ \t]*=[ \t]*(.*?)[ \t]*\r?$/gmu;
|
|
29
29
|
var CONDITIONAL_NAME_RE = /^[A-Za-z_]\w*/;
|
|
30
30
|
var PLURAL_PREFIX = "plural ";
|
|
31
31
|
function parseTemplate(src) {
|
|
32
|
-
const { body, setDefs } =
|
|
33
|
-
return { astVersion: AST_VERSION, source: src, setDefs, nodes: parseSequence(body) };
|
|
32
|
+
const { body, setDefs, defDefs } = extractDirectives(stripComments(src));
|
|
33
|
+
return { astVersion: AST_VERSION, source: src, setDefs, defDefs, nodes: parseSequence(body) };
|
|
34
34
|
}
|
|
35
|
-
function
|
|
35
|
+
function extractDirectives(text) {
|
|
36
36
|
const setDefs = {};
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
37
|
+
const defDefs = {};
|
|
38
|
+
const occurrences = [];
|
|
39
|
+
DIRECTIVE_RE.lastIndex = 0;
|
|
40
|
+
const stripped = text.replace(
|
|
41
|
+
DIRECTIVE_RE,
|
|
42
|
+
(full, kind, rawName, value, offset) => {
|
|
43
|
+
const name = rawName.toLowerCase();
|
|
44
|
+
occurrences.push({
|
|
45
|
+
kind,
|
|
46
|
+
name,
|
|
47
|
+
value,
|
|
48
|
+
line: countLines(text, offset)
|
|
49
|
+
});
|
|
50
|
+
if (kind === "def") defDefs[name] = value;
|
|
51
|
+
else setDefs[name] = value;
|
|
52
|
+
return "";
|
|
53
|
+
}
|
|
54
|
+
);
|
|
55
|
+
return { body: stripped.replace(/\n{3,}/gu, "\n\n"), setDefs, defDefs, occurrences };
|
|
56
|
+
}
|
|
57
|
+
function countLines(text, offset) {
|
|
58
|
+
let line = 1;
|
|
59
|
+
for (let i = 0; i < offset; i += 1) if (text[i] === "\n") line += 1;
|
|
60
|
+
return line;
|
|
43
61
|
}
|
|
44
62
|
function stripComments(text) {
|
|
45
63
|
return text.replace(/\/#[\s\S]*?#\//g, "");
|
|
@@ -378,7 +396,7 @@ function validateTemplate(src, opts = {}) {
|
|
|
378
396
|
const diagnostics = [];
|
|
379
397
|
const text = stripComments(src);
|
|
380
398
|
checkBrackets(text, diagnostics);
|
|
381
|
-
|
|
399
|
+
checkDirectives(text, diagnostics);
|
|
382
400
|
checkPermutationConfigs(text, diagnostics);
|
|
383
401
|
checkPlurals(text, opts.locale, diagnostics);
|
|
384
402
|
checkVariableReferences(text, opts.knownVariables, diagnostics);
|
|
@@ -437,17 +455,43 @@ function checkBrackets(text, out) {
|
|
|
437
455
|
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 } }));
|
|
438
456
|
}
|
|
439
457
|
}
|
|
440
|
-
function
|
|
458
|
+
function checkDirectives(text, out) {
|
|
441
459
|
const lines = text.split("\n");
|
|
442
460
|
lines.forEach((lineText, idx) => {
|
|
443
461
|
const trimmed = lineText.replace(/^[ \t]+/, "");
|
|
444
|
-
|
|
445
|
-
|
|
462
|
+
const kind = ["#set", "#def"].find(
|
|
463
|
+
(candidate) => trimmed.startsWith(`${candidate} `) || trimmed.startsWith(`${candidate} `)
|
|
464
|
+
);
|
|
465
|
+
if (!kind) return;
|
|
466
|
+
DIRECTIVE_RE.lastIndex = 0;
|
|
467
|
+
if (!DIRECTIVE_RE.test(trimmed)) {
|
|
446
468
|
const column = lineText.length - trimmed.length + 1;
|
|
447
469
|
const line = idx + 1;
|
|
448
|
-
|
|
470
|
+
const code = kind === "#def" ? "def.malformed" : "set.malformed";
|
|
471
|
+
out.push(err(code, `Malformed ${kind}. Expected: ${kind} %name% = value`, { line, column, endLine: line, endColumn: lineText.length + 1 }));
|
|
449
472
|
}
|
|
450
473
|
});
|
|
474
|
+
const { occurrences } = extractDirectives(text);
|
|
475
|
+
const seen = /* @__PURE__ */ new Map();
|
|
476
|
+
for (const occurrence of occurrences) {
|
|
477
|
+
const first = seen.get(occurrence.name);
|
|
478
|
+
if (first !== void 0) {
|
|
479
|
+
out.push(err(
|
|
480
|
+
"definition.duplicate-name",
|
|
481
|
+
`Variable '${occurrence.name}' is defined more than once (first on line ${first}). A name belongs to one directive, once.`,
|
|
482
|
+
{ line: occurrence.line, column: 1 }
|
|
483
|
+
));
|
|
484
|
+
} else {
|
|
485
|
+
seen.set(occurrence.name, occurrence.line);
|
|
486
|
+
}
|
|
487
|
+
if (occurrence.kind === "def" && /#include\b/u.test(occurrence.value)) {
|
|
488
|
+
out.push(err(
|
|
489
|
+
"def.include-in-value",
|
|
490
|
+
`#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.`,
|
|
491
|
+
{ line: occurrence.line, column: 1 }
|
|
492
|
+
));
|
|
493
|
+
}
|
|
494
|
+
}
|
|
451
495
|
}
|
|
452
496
|
function checkPermutationConfigs(text, out) {
|
|
453
497
|
for (const m of text.matchAll(/\[<([^>]*?)>/gu)) {
|
|
@@ -485,10 +529,20 @@ function checkPermutationConfigs(text, out) {
|
|
|
485
529
|
function checkPlurals(text, locale, out) {
|
|
486
530
|
const base = locale && locale !== "" ? normalizeBaseLang(locale) : "";
|
|
487
531
|
const arity = base !== "" ? pluralArity(base) : 0;
|
|
532
|
+
const tainted = macroTaintedNames(text);
|
|
488
533
|
for (const block of findPluralBlocks(text)) {
|
|
489
534
|
const at = span(text, block.start, block.end - block.start);
|
|
535
|
+
for (const m of block.countSlot.matchAll(/%(\w+)%/gu)) {
|
|
536
|
+
const name = (m[1] ?? "").toLowerCase();
|
|
537
|
+
if (!tainted.has(name)) continue;
|
|
538
|
+
out.push(err(
|
|
539
|
+
"plural.count-macro",
|
|
540
|
+
`{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.`,
|
|
541
|
+
at
|
|
542
|
+
));
|
|
543
|
+
}
|
|
490
544
|
if (/[{}[\]]/.test(block.formsRaw)) {
|
|
491
|
-
out.push(err("plural.nested-brackets", "{plural ...}: forms must not contain nested spintax brackets.", at));
|
|
545
|
+
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));
|
|
492
546
|
continue;
|
|
493
547
|
}
|
|
494
548
|
if (arity > 0) {
|
|
@@ -503,11 +557,33 @@ function checkPlurals(text, locale, out) {
|
|
|
503
557
|
}
|
|
504
558
|
}
|
|
505
559
|
}
|
|
560
|
+
var UNRESOLVED_AT_PLURAL_TIME = /\[|\{(?!\?)/u;
|
|
561
|
+
function macroTaintedNames(text) {
|
|
562
|
+
const macros = extractDirectives(text).setDefs;
|
|
563
|
+
const tainted = /* @__PURE__ */ new Set();
|
|
564
|
+
for (const [name, value] of Object.entries(macros)) {
|
|
565
|
+
if (UNRESOLVED_AT_PLURAL_TIME.test(value)) tainted.add(name);
|
|
566
|
+
}
|
|
567
|
+
let grew = true;
|
|
568
|
+
while (grew) {
|
|
569
|
+
grew = false;
|
|
570
|
+
for (const [name, value] of Object.entries(macros)) {
|
|
571
|
+
if (tainted.has(name)) continue;
|
|
572
|
+
for (const m of value.matchAll(/%(\w+)%/gu)) {
|
|
573
|
+
if (!tainted.has((m[1] ?? "").toLowerCase())) continue;
|
|
574
|
+
tainted.add(name);
|
|
575
|
+
grew = true;
|
|
576
|
+
break;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
return tainted;
|
|
581
|
+
}
|
|
506
582
|
function checkVariableReferences(text, known, out) {
|
|
507
583
|
const knownSet = new Set((known ?? []).map((n) => n.toLowerCase()));
|
|
508
584
|
const defs = /* @__PURE__ */ new Map();
|
|
509
585
|
const defPos = /* @__PURE__ */ new Map();
|
|
510
|
-
for (const m of text.matchAll(/^[ \t]*#set[ \t]+%(\w+)%[ \t]*=[ \t]*(.*?)$/gmu)) {
|
|
586
|
+
for (const m of text.matchAll(/^[ \t]*#(?:set|def)[ \t]+%(\w+)%[ \t]*=[ \t]*(.*?)$/gmu)) {
|
|
511
587
|
const name = (m[1] ?? "").toLowerCase();
|
|
512
588
|
defs.set(name, m[2] ?? "");
|
|
513
589
|
const nameOffset = (m.index ?? 0) + m[0].indexOf("%");
|
|
@@ -522,7 +598,7 @@ function checkVariableReferences(text, known, out) {
|
|
|
522
598
|
for (const name of defs.keys()) {
|
|
523
599
|
detectCycle(name, defs, [name], defPos.get(name) ?? somewhere, out);
|
|
524
600
|
}
|
|
525
|
-
const body = text.replace(/^[ \t]*#set[ \t]+%\w+%[ \t]*=[ \t]*.*?$/gmu, (m) => m.replace(/[^\n]/g, " "));
|
|
601
|
+
const body = text.replace(/^[ \t]*#(?:set|def)[ \t]+%\w+%[ \t]*=[ \t]*.*?$/gmu, (m) => m.replace(/[^\n]/g, " "));
|
|
526
602
|
const seen = /* @__PURE__ */ new Set();
|
|
527
603
|
const undefinedAt = (name, offset, length) => {
|
|
528
604
|
const key = name.toLowerCase();
|
|
@@ -569,19 +645,21 @@ function checkIncludeTargets(text, known, out) {
|
|
|
569
645
|
|
|
570
646
|
// src/internal/extract.ts
|
|
571
647
|
var SET_DEF_RE = /^[ \t]*#set[ \t]+%(\w+)%[ \t]*=/gmu;
|
|
572
|
-
var
|
|
648
|
+
var DEF_DEF_RE = /^[ \t]*#def[ \t]+%(\w+)%[ \t]*=/gmu;
|
|
649
|
+
var DEFINITION_LHS_RE = /^[ \t]*#(?:set|def)[ \t]+%\w+%[ \t]*=/gmu;
|
|
573
650
|
var INCLUDE_RE2 = /^[ \t]*#include[ \t\n\r\f\x0B]+"([^"]+)"[ \t\n\r\f\x0B]*$/gmu;
|
|
574
651
|
var VARIABLE_RE2 = /%(\w+)%/gu;
|
|
575
652
|
var CONDITIONAL_REF_RE = /\{\?!?([A-Za-z_]\w*)\?/gu;
|
|
576
653
|
function extractFromSource(src) {
|
|
577
654
|
const text = stripComments(src);
|
|
578
655
|
const sets = collect(text, SET_DEF_RE, true);
|
|
656
|
+
const defs = collect(text, DEF_DEF_RE, true);
|
|
579
657
|
const includes = collect(text, INCLUDE_RE2, false);
|
|
580
|
-
const body = text.replace(
|
|
658
|
+
const body = text.replace(DEFINITION_LHS_RE, "");
|
|
581
659
|
const refs = /* @__PURE__ */ new Set();
|
|
582
660
|
addAll(refs, body, VARIABLE_RE2, true);
|
|
583
661
|
addAll(refs, body, CONDITIONAL_REF_RE, true);
|
|
584
|
-
return { refs: [...refs], sets, includes };
|
|
662
|
+
return { refs: [...refs], sets, defs, includes };
|
|
585
663
|
}
|
|
586
664
|
function collect(text, re, fold) {
|
|
587
665
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -653,14 +731,15 @@ function stripSentinels(text) {
|
|
|
653
731
|
var MAX_VARIABLE_DEPTH = 50;
|
|
654
732
|
var INCLUDE_LINE_RE = /^[ \t]*#include[ \t\n\r\f\x0B]+"([^"]+)"[ \t\n\r\f\x0B]*$/gmu;
|
|
655
733
|
function renderAst(ast, ctx) {
|
|
656
|
-
const
|
|
657
|
-
const
|
|
658
|
-
vars,
|
|
734
|
+
const base = buildVars(ast.setDefs, ctx.runtimeContext);
|
|
735
|
+
const walkOpts = {
|
|
659
736
|
rng: ctx.rng,
|
|
660
737
|
locale: ctx.locale,
|
|
661
738
|
depth: 0,
|
|
662
739
|
onPluralError: ctx.onPluralError
|
|
663
|
-
}
|
|
740
|
+
};
|
|
741
|
+
const vars = Object.keys(ast.defDefs).length > 0 ? { ...base, ...rollDefinitions(ast.defDefs, base, ctx.runtimeContext, walkOpts) } : base;
|
|
742
|
+
const text = renderNodes(ast.nodes, { ...walkOpts, vars });
|
|
664
743
|
return ctx.resolver ? resolveIncludes(text, ctx) : text;
|
|
665
744
|
}
|
|
666
745
|
function resolveIncludes(text, ctx) {
|
|
@@ -682,33 +761,65 @@ function resolveIncludes(text, ctx) {
|
|
|
682
761
|
});
|
|
683
762
|
});
|
|
684
763
|
}
|
|
685
|
-
function buildVars(setDefs, context
|
|
764
|
+
function buildVars(setDefs, context) {
|
|
686
765
|
const vars = {};
|
|
687
766
|
for (const [name, value] of Object.entries(setDefs)) {
|
|
688
|
-
vars[name] =
|
|
767
|
+
vars[name] = value;
|
|
689
768
|
}
|
|
690
769
|
for (const [name, value] of Object.entries(context)) {
|
|
691
770
|
vars[name.toLowerCase()] = value;
|
|
692
771
|
}
|
|
693
772
|
return vars;
|
|
694
773
|
}
|
|
695
|
-
function
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
774
|
+
function rollDefinitions(defDefs, vars, context, opts) {
|
|
775
|
+
const outranked = new Set(Object.keys(context).map((key) => key.toLowerCase()));
|
|
776
|
+
const rolled = {};
|
|
777
|
+
const aliases = {};
|
|
778
|
+
for (const [name, value] of Object.entries(vars)) {
|
|
779
|
+
if (name in defDefs && !outranked.has(name)) continue;
|
|
780
|
+
aliases[name] = value;
|
|
781
|
+
}
|
|
782
|
+
for (const name of orderDefinitions(defDefs, aliases)) {
|
|
783
|
+
if (outranked.has(name)) continue;
|
|
784
|
+
const value = defDefs[name] ?? "";
|
|
785
|
+
rolled[name] = renderNodes(parseSequence(value), { ...opts, vars: { ...vars, ...rolled } });
|
|
786
|
+
}
|
|
787
|
+
return rolled;
|
|
788
|
+
}
|
|
789
|
+
function orderDefinitions(defDefs, aliases) {
|
|
790
|
+
const names = Object.keys(defDefs);
|
|
791
|
+
const blocked = /* @__PURE__ */ new Map();
|
|
792
|
+
for (const name of names) {
|
|
793
|
+
const reached = referencedNames(defDefs[name] ?? "", aliases);
|
|
794
|
+
blocked.set(name, new Set(names.filter((candidate) => reached.has(candidate))));
|
|
795
|
+
}
|
|
796
|
+
const ordered = [];
|
|
797
|
+
let pending = names;
|
|
798
|
+
while (pending.length > 0) {
|
|
799
|
+
const ready = pending.filter((name) => {
|
|
800
|
+
const deps = blocked.get(name);
|
|
801
|
+
return !deps || ![...deps].some((dep) => dep !== name && pending.includes(dep));
|
|
708
802
|
});
|
|
709
|
-
if (
|
|
803
|
+
if (ready.length === 0) return [...ordered, ...pending];
|
|
804
|
+
ordered.push(...ready);
|
|
805
|
+
pending = pending.filter((name) => !ready.includes(name));
|
|
710
806
|
}
|
|
711
|
-
return
|
|
807
|
+
return ordered;
|
|
808
|
+
}
|
|
809
|
+
function referencedNames(value, aliases) {
|
|
810
|
+
const seen = /* @__PURE__ */ new Set();
|
|
811
|
+
const queue = directReferences(value);
|
|
812
|
+
while (queue.length > 0) {
|
|
813
|
+
const name = queue.shift();
|
|
814
|
+
if (seen.has(name)) continue;
|
|
815
|
+
seen.add(name);
|
|
816
|
+
const alias = aliases[name];
|
|
817
|
+
if (alias !== void 0) queue.push(...directReferences(alias));
|
|
818
|
+
}
|
|
819
|
+
return seen;
|
|
820
|
+
}
|
|
821
|
+
function directReferences(text) {
|
|
822
|
+
return [...text.matchAll(/%(\w+)%/gu)].map((match) => (match[1] ?? "").toLowerCase());
|
|
712
823
|
}
|
|
713
824
|
function renderNodes(nodes, opts) {
|
|
714
825
|
let out = "";
|
|
@@ -867,26 +978,6 @@ function padSeparator(sep) {
|
|
|
867
978
|
if (/^\p{L}+$/u.test(trimmed)) return ` ${trimmed} `;
|
|
868
979
|
return sep;
|
|
869
980
|
}
|
|
870
|
-
function splitTopLevelPipes(inner) {
|
|
871
|
-
const parts = [];
|
|
872
|
-
let brace = 0;
|
|
873
|
-
let bracket = 0;
|
|
874
|
-
let cur = "";
|
|
875
|
-
for (const ch of inner) {
|
|
876
|
-
if (ch === "{") brace += 1;
|
|
877
|
-
else if (ch === "}") brace -= 1;
|
|
878
|
-
else if (ch === "[") bracket += 1;
|
|
879
|
-
else if (ch === "]") bracket -= 1;
|
|
880
|
-
if (ch === "|" && brace === 0 && bracket === 0) {
|
|
881
|
-
parts.push(cur);
|
|
882
|
-
cur = "";
|
|
883
|
-
} else {
|
|
884
|
-
cur += ch;
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
parts.push(cur);
|
|
888
|
-
return parts;
|
|
889
|
-
}
|
|
890
981
|
var PHP_LTRIM_RE2 = /^[ \t\n\r\0\x0B]+/u;
|
|
891
982
|
var PHP_RTRIM_RE2 = /[ \t\n\r\0\x0B]+$/u;
|
|
892
983
|
function phpTrim2(s) {
|