@spintax/core 0.4.0 → 0.5.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 +114 -0
- package/dist/index.cjs +172 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +172 -26
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,120 @@
|
|
|
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.5.1 — 2026-08-18
|
|
7
|
+
|
|
8
|
+
Two crashes in the 0.5.0 form-counting path, both reachable from template text, both found by
|
|
9
|
+
review rather than by a failing test. **Upgrade from 0.5.0.**
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **A 62-character template could take `validate()` out with an out-of-memory crash.**
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
#set %a% = %b% %b%
|
|
17
|
+
#set %b% = %a% %a%
|
|
18
|
+
{plural 2: one|%a%}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Counting plural forms expands definitions, and the expansion was bounded by 51 *passes* with
|
|
22
|
+
nothing bounding the *size*. That template doubles the text on every pass, so 51 passes is 2^51.
|
|
23
|
+
Every engine in the family died on it — this one and the Python port with an out-of-memory
|
|
24
|
+
crash, both PHP engines with a memory fatal. A non-cyclic doubling chain does the same, so the
|
|
25
|
+
circular-reference guard never got a chance.
|
|
26
|
+
|
|
27
|
+
The expansion now stops at 64 KB and reports the count as unknowable, which is the same silence
|
|
28
|
+
this validator already uses for every input it cannot pin down. A form list is a handful of
|
|
29
|
+
plural forms; nothing legitimate approaches the ceiling.
|
|
30
|
+
|
|
31
|
+
`validate` is exposed by public HTTP surfaces — this was a remote denial of service, not a
|
|
32
|
+
local footgun.
|
|
33
|
+
|
|
34
|
+
- **`validate()` threw `RangeError` on a long `#set` chain.** The macro walk added in 0.5.0 was
|
|
35
|
+
recursive: roughly 9000 links (a 205 KB template) exhausted the call stack, while the Python
|
|
36
|
+
port gave up at ~1000 links (20 KB) and both PHP engines answered normally — one input, three
|
|
37
|
+
behaviours. The walk is iterative now.
|
|
38
|
+
|
|
39
|
+
It is a pre-order depth-first walk in source order, and it has to stay one: the first non-clean
|
|
40
|
+
answer wins, so a graph holding both an opaque macro and a brackety one gives different
|
|
41
|
+
diagnostics depending on which is met first. Checked by leaving the PHP engine recursive and
|
|
42
|
+
diffing 300 generated macro graphs against it — no mismatches.
|
|
43
|
+
|
|
44
|
+
### Notes
|
|
45
|
+
|
|
46
|
+
No API change and no verdict change for any template that was not crashing. Both fixes are
|
|
47
|
+
mirrored into `spintax/core`, `spintax-core` and the WordPress plugin, and pinned by two corpus
|
|
48
|
+
fixtures whose real assertion is that the engine answers at all — a runner that hangs fails them.
|
|
49
|
+
|
|
50
|
+
## 0.5.0 — 2026-08-18
|
|
51
|
+
|
|
52
|
+
Two plural fixes, both cross-engine, both found by measuring one engine against another rather
|
|
53
|
+
than by a failing test. Minor rather than patch: validation output and render output both change.
|
|
54
|
+
|
|
55
|
+
### Fixed
|
|
56
|
+
|
|
57
|
+
- **`validate` counted plural forms before `%variable%` expansion; `render` counts them after**
|
|
58
|
+
(issue #66, found by the Object Pascal port and confirmed in all five engines).
|
|
59
|
+
|
|
60
|
+
`#def %tail% = few|many` with `{plural 2: one|%tail%}` under `ru` renders correctly as three
|
|
61
|
+
forms, and every validator reported `plural.arity` for the two it could see in the source. The
|
|
62
|
+
count now substitutes definition values first — every reference per pass, as the renderer does —
|
|
63
|
+
and splits the result.
|
|
64
|
+
|
|
65
|
+
Only where the count is **provably invariant**. A value carrying any bracket — all four, and
|
|
66
|
+
conditionals too — suppresses the count-based verdicts rather than guessing: `{a|b}` really does
|
|
67
|
+
always freeze to one form, but `{?flag?a|b|c}` freezes as `a` or as `b|c` and the two cannot be
|
|
68
|
+
told apart without evaluating the construct. Predicting the roll was tried first and produced a
|
|
69
|
+
fresh crop of false errors. Silence on an unknowable input is the trade.
|
|
70
|
+
|
|
71
|
+
One case is not a prediction: a `#set` named **directly** in the form slot is substituted
|
|
72
|
+
verbatim and is still spintax when the plural is decided, so its brackets keep earning
|
|
73
|
+
`plural.nested-brackets` — through a `#def` it is rolled first and earns nothing.
|
|
74
|
+
|
|
75
|
+
- **A conditional in the plural COUNT slot deleted the block, silently.**
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
#set %flag% =
|
|
79
|
+
#set %n% = {?flag?1|2}
|
|
80
|
+
start {plural %n%: one|two} end
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
rendered `start end` — no fallback braces, no diagnostic, `validate` returning `[]`. Both PHP
|
|
84
|
+
engines have always rendered `start two end`: they run the conditional stage over the whole text
|
|
85
|
+
before plurals, so a plain number reaches the slot. This engine expanded variables into the raw
|
|
86
|
+
slot and left constructs literal, so the conditional failed the numeric test and the block was
|
|
87
|
+
erased. `plural.count-macro` exempts conditionals *because* they resolve before plurals — the
|
|
88
|
+
validator was written to a renderer behaviour that was not implemented here.
|
|
89
|
+
|
|
90
|
+
The branch is substituted, never rendered: enumerations resolve AFTER plurals, so a branch
|
|
91
|
+
yielding `{a|b}` still reaches the numeric test intact and still erases the block, exactly as the
|
|
92
|
+
plugin does. Rendering it would invent a count no engine has.
|
|
93
|
+
|
|
94
|
+
The **form** slot is deliberately untouched. There the engines genuinely disagree — both PHP
|
|
95
|
+
renderers resolve a conditional that expansion introduces inside a form list, this engine and the
|
|
96
|
+
Python one do not — and picking a side is issue #67, not a bug fix.
|
|
97
|
+
|
|
98
|
+
### Performance
|
|
99
|
+
|
|
100
|
+
- The count-slot pass is linear and iterative. Written recursively it made `render()` throw
|
|
101
|
+
`RangeError` at ~9000 levels of nesting, breaking the §9.2 promise that render never throws on
|
|
102
|
+
content; matching braces per `{?` made it quadratic, and an unbalanced count slot is legal input
|
|
103
|
+
(only the whole `{plural …}` block has to balance, and the slot is cut at the first `:`), so a
|
|
104
|
+
78 KB template cost 3 seconds. Both were reachable from template text through a public endpoint.
|
|
105
|
+
78 KB now costs 42 ms and 1 MB costs 372 ms.
|
|
106
|
+
|
|
107
|
+
Deep *balanced* nesting is still super-linear, in every engine of the family — the reference
|
|
108
|
+
implementation scans the body per level too. Bounding input size is a host job (§9.3); the
|
|
109
|
+
reference Worker now refuses a template over 8192 characters with 413, the same cap the hosted
|
|
110
|
+
MCP server has always applied.
|
|
111
|
+
|
|
112
|
+
### Notes
|
|
113
|
+
|
|
114
|
+
Mirrored where each engine needed it: the counting fix into `spintax/core`, the WordPress plugin
|
|
115
|
+
and `spintax-core` (Python); the count-slot fix into the Python engine only, the PHP engines
|
|
116
|
+
having been right all along. Pinned by 20 new corpus fixtures — 12 for the counting rule, 8 for the
|
|
117
|
+
count slot, of which 4 are negative controls — taking the corpus from 235 to 255, green in
|
|
118
|
+
TypeScript, Python and both PHP engines.
|
|
119
|
+
|
|
6
120
|
## 0.4.0 — 2026-08-17
|
|
7
121
|
|
|
8
122
|
### Added
|
package/dist/index.cjs
CHANGED
|
@@ -51,7 +51,7 @@ function stripSentinels(text) {
|
|
|
51
51
|
// src/internal/parser.ts
|
|
52
52
|
var VARIABLE_RE = /^%(\w+)%/;
|
|
53
53
|
var DIRECTIVE_RE = /^[ \t]*#(set|def)[ \t]+%(\w+)%[ \t]*=[ \t]*(.*?)[ \t]*\r?$/gmu;
|
|
54
|
-
var CONDITIONAL_NAME_RE =
|
|
54
|
+
var CONDITIONAL_NAME_RE = /[A-Za-z_]\w*/y;
|
|
55
55
|
var PLURAL_PREFIX = "plural ";
|
|
56
56
|
function parseTemplate(src) {
|
|
57
57
|
const { body, setDefs, defDefs } = extractDirectives(stripComments(stripSentinels(src)));
|
|
@@ -152,28 +152,42 @@ function parseBraceConstruct(content) {
|
|
|
152
152
|
}
|
|
153
153
|
return { type: "enumeration", options: splitTopLevel(content).map((o) => parseSequence(o)) };
|
|
154
154
|
}
|
|
155
|
-
function
|
|
156
|
-
let p = 1;
|
|
155
|
+
function recognizeConditional(text, contentStart, contentEnd) {
|
|
156
|
+
let p = contentStart + 1;
|
|
157
157
|
let inverted = false;
|
|
158
|
-
if (
|
|
158
|
+
if (text.charAt(p) === "!") {
|
|
159
159
|
inverted = true;
|
|
160
160
|
p += 1;
|
|
161
161
|
}
|
|
162
|
-
|
|
163
|
-
|
|
162
|
+
CONDITIONAL_NAME_RE.lastIndex = p;
|
|
163
|
+
const name = CONDITIONAL_NAME_RE.exec(text)?.[0];
|
|
164
|
+
if (name === void 0 || p + name.length > contentEnd) return null;
|
|
164
165
|
p += name.length;
|
|
165
|
-
if (
|
|
166
|
+
if (text.charAt(p) !== "?") return null;
|
|
166
167
|
p += 1;
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const
|
|
168
|
+
return { name, inverted, bodyStart: p, sepIndex: firstTopLevelPipe(text, p, contentEnd) };
|
|
169
|
+
}
|
|
170
|
+
function splitConditional(content) {
|
|
171
|
+
const head = recognizeConditional(content, 0, content.length);
|
|
172
|
+
if (head === null) return null;
|
|
173
|
+
const body = content.slice(head.bodyStart);
|
|
174
|
+
const sep = head.sepIndex < 0 ? -1 : head.sepIndex - head.bodyStart;
|
|
175
|
+
return {
|
|
176
|
+
name: head.name,
|
|
177
|
+
inverted: head.inverted,
|
|
178
|
+
thenRaw: sep < 0 ? body : body.slice(0, sep),
|
|
179
|
+
elseRaw: sep < 0 ? "" : body.slice(sep + 1)
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
function tryParseConditional(content) {
|
|
183
|
+
const parts = splitConditional(content);
|
|
184
|
+
if (parts === null) return null;
|
|
171
185
|
return {
|
|
172
186
|
type: "conditional",
|
|
173
|
-
name,
|
|
174
|
-
inverted,
|
|
175
|
-
then: parseSequence(thenRaw),
|
|
176
|
-
else: parseSequence(elseRaw)
|
|
187
|
+
name: parts.name,
|
|
188
|
+
inverted: parts.inverted,
|
|
189
|
+
then: parseSequence(parts.thenRaw),
|
|
190
|
+
else: parseSequence(parts.elseRaw)
|
|
177
191
|
};
|
|
178
192
|
}
|
|
179
193
|
function parsePlural(afterPrefix) {
|
|
@@ -333,9 +347,9 @@ function splitTopLevel(inner) {
|
|
|
333
347
|
parts.push(cur);
|
|
334
348
|
return parts;
|
|
335
349
|
}
|
|
336
|
-
function firstTopLevelPipe(body) {
|
|
350
|
+
function firstTopLevelPipe(body, from = 0, to = body.length) {
|
|
337
351
|
let depth = 0;
|
|
338
|
-
for (let j =
|
|
352
|
+
for (let j = from; j < to; j += 1) {
|
|
339
353
|
const ch = body.charAt(j);
|
|
340
354
|
if (ch === "{" || ch === "[") {
|
|
341
355
|
depth += 1;
|
|
@@ -429,7 +443,7 @@ function validateTemplate(src, opts = {}) {
|
|
|
429
443
|
checkBrackets(text, diagnostics);
|
|
430
444
|
checkDirectives(text, diagnostics);
|
|
431
445
|
checkPermutationConfigs(text, idx, diagnostics);
|
|
432
|
-
checkPlurals(text, idx, opts.locale, diagnostics);
|
|
446
|
+
checkPlurals(text, idx, opts.locale, opts.knownVariables, diagnostics);
|
|
433
447
|
checkVariableReferences(text, idx, opts.knownVariables, diagnostics);
|
|
434
448
|
if (opts.knownIncludes && opts.knownIncludes.length > 0) {
|
|
435
449
|
checkIncludeTargets(text, idx, opts.knownIncludes, diagnostics);
|
|
@@ -563,10 +577,12 @@ function checkPermutationConfigs(text, idx, out) {
|
|
|
563
577
|
}
|
|
564
578
|
}
|
|
565
579
|
}
|
|
566
|
-
function checkPlurals(text, idx, locale, out) {
|
|
580
|
+
function checkPlurals(text, idx, locale, knownVariables, out) {
|
|
567
581
|
const base = locale && locale !== "" ? normalizeBaseLang(locale) : "";
|
|
568
582
|
const arity = base !== "" ? pluralArity(base) : 0;
|
|
569
583
|
const tainted = macroTaintedNames(text);
|
|
584
|
+
const { setDefs: macros, defDefs: defs } = extractDirectives(text);
|
|
585
|
+
const hostNames = new Set((knownVariables ?? []).map((n) => n.toLowerCase()));
|
|
570
586
|
for (const block of findPluralBlocks(text)) {
|
|
571
587
|
const at = span(idx, block.start, block.end - block.start);
|
|
572
588
|
for (const m of block.countSlot.matchAll(/%(\w+)%/gu)) {
|
|
@@ -582,7 +598,13 @@ function checkPlurals(text, idx, locale, out) {
|
|
|
582
598
|
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));
|
|
583
599
|
continue;
|
|
584
600
|
}
|
|
585
|
-
const
|
|
601
|
+
const expanded = expandFormsForCounting(block.formsRaw, defs, macros, hostNames);
|
|
602
|
+
if (expanded.directMacroSpintax) {
|
|
603
|
+
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));
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
if (expanded.unresolved) continue;
|
|
607
|
+
const count = expanded.forms;
|
|
586
608
|
if (arity > 0) {
|
|
587
609
|
if (count !== arity) {
|
|
588
610
|
out.push(err(
|
|
@@ -592,15 +614,90 @@ function checkPlurals(text, idx, locale, out) {
|
|
|
592
614
|
));
|
|
593
615
|
}
|
|
594
616
|
} else if (count !== DEFAULT_PLURAL_ARITY) {
|
|
617
|
+
const unusable = locale !== void 0 && locale !== "";
|
|
595
618
|
out.push(warn(
|
|
596
619
|
"plural.locale-missing",
|
|
597
|
-
`{plural ...}: ${count} forms, but no locale was supplied. render defaults to ${DEFAULT_PLURAL_ARITY} forms and leaves this block unresolved \u2014 pass the locale you will render with.`,
|
|
598
|
-
{
|
|
620
|
+
unusable ? `{plural ...}: ${count} forms, and the locale '${locale}' does not normalize to a language the engine knows, so render falls back to ${DEFAULT_PLURAL_ARITY} forms and leaves this block unresolved \u2014 pass a base tag such as 'en' or 'ru'.` : `{plural ...}: ${count} forms, but no locale was supplied. render defaults to ${DEFAULT_PLURAL_ARITY} forms and leaves this block unresolved \u2014 pass the locale you will render with.`,
|
|
621
|
+
{
|
|
622
|
+
...at,
|
|
623
|
+
data: {
|
|
624
|
+
got: count,
|
|
625
|
+
defaultArity: DEFAULT_PLURAL_ARITY,
|
|
626
|
+
...unusable ? { locale } : {}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
599
629
|
));
|
|
600
630
|
}
|
|
601
631
|
}
|
|
602
632
|
}
|
|
603
633
|
var UNRESOLVED_AT_PLURAL_TIME = /\[|\{(?!\?)/u;
|
|
634
|
+
var ANY_BRACKET = /[[\]{}]/u;
|
|
635
|
+
var FORM_EXPANSION_PASSES = 51;
|
|
636
|
+
var FORM_EXPANSION_MAX_GROWTH = 64 * 1024;
|
|
637
|
+
function expandFormsForCounting(formsRaw, defs, macros, hostNames) {
|
|
638
|
+
const unknown = (directMacroSpintax = false) => ({
|
|
639
|
+
forms: 0,
|
|
640
|
+
unresolved: true,
|
|
641
|
+
directMacroSpintax
|
|
642
|
+
});
|
|
643
|
+
const seenMacro = /* @__PURE__ */ new Set();
|
|
644
|
+
const refsOf = (text2) => [...text2.matchAll(/%(\w+)%/gu)].map((m) => (m[1] ?? "").toLowerCase());
|
|
645
|
+
const walkMacros = (source) => {
|
|
646
|
+
const stack = [{ refs: refsOf(source), i: 0 }];
|
|
647
|
+
while (stack.length > 0) {
|
|
648
|
+
const frame = stack[stack.length - 1];
|
|
649
|
+
if (frame.i >= frame.refs.length) {
|
|
650
|
+
stack.pop();
|
|
651
|
+
continue;
|
|
652
|
+
}
|
|
653
|
+
const name = frame.refs[frame.i++];
|
|
654
|
+
if (defs[name] !== void 0 || hostNames.has(name)) continue;
|
|
655
|
+
const macro = macros[name];
|
|
656
|
+
if (macro === void 0 || seenMacro.has(name)) continue;
|
|
657
|
+
seenMacro.add(name);
|
|
658
|
+
if (/\{\?/u.test(macro)) return "opaque";
|
|
659
|
+
if (ANY_BRACKET.test(macro)) return "brackets";
|
|
660
|
+
stack.push({ refs: refsOf(macro), i: 0 });
|
|
661
|
+
}
|
|
662
|
+
return "clean";
|
|
663
|
+
};
|
|
664
|
+
const verbatim = walkMacros(formsRaw);
|
|
665
|
+
if (verbatim === "brackets") return unknown(true);
|
|
666
|
+
if (verbatim === "opaque") return unknown();
|
|
667
|
+
let text = formsRaw;
|
|
668
|
+
const budget = formsRaw.length + FORM_EXPANSION_MAX_GROWTH;
|
|
669
|
+
for (let pass = 0; pass < FORM_EXPANSION_PASSES; pass++) {
|
|
670
|
+
let sawReference = false;
|
|
671
|
+
let bailed = false;
|
|
672
|
+
const parts = [];
|
|
673
|
+
let total = 0;
|
|
674
|
+
let cursor = 0;
|
|
675
|
+
const re = /%(\w+)%/gu;
|
|
676
|
+
let m;
|
|
677
|
+
while ((m = re.exec(text)) !== null) {
|
|
678
|
+
sawReference = true;
|
|
679
|
+
const name = (m[1] ?? "").toLowerCase();
|
|
680
|
+
const value = hostNames.has(name) ? void 0 : defs[name] ?? macros[name];
|
|
681
|
+
if (value === void 0 || ANY_BRACKET.test(value)) {
|
|
682
|
+
bailed = true;
|
|
683
|
+
break;
|
|
684
|
+
}
|
|
685
|
+
parts.push(text.slice(cursor, m.index), value);
|
|
686
|
+
total += m.index - cursor + value.length;
|
|
687
|
+
cursor = m.index + m[0].length;
|
|
688
|
+
if (total > budget) return unknown();
|
|
689
|
+
}
|
|
690
|
+
if (bailed) return unknown();
|
|
691
|
+
parts.push(text.slice(cursor));
|
|
692
|
+
total += text.length - cursor;
|
|
693
|
+
if (total > budget) return unknown();
|
|
694
|
+
text = parts.join("");
|
|
695
|
+
if (!sawReference) {
|
|
696
|
+
return { forms: text.split("|").length, unresolved: false, directMacroSpintax: false };
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
return unknown();
|
|
700
|
+
}
|
|
604
701
|
function macroTaintedNames(text) {
|
|
605
702
|
const macros = extractDirectives(text).setDefs;
|
|
606
703
|
const tainted = /* @__PURE__ */ new Set();
|
|
@@ -950,14 +1047,63 @@ function expandVarsOnly(text, opts) {
|
|
|
950
1047
|
}
|
|
951
1048
|
return out;
|
|
952
1049
|
}
|
|
953
|
-
function
|
|
954
|
-
const value = opts.vars[
|
|
1050
|
+
function conditionalTakesThen(name, inverted, opts) {
|
|
1051
|
+
const value = opts.vars[name.toLowerCase()];
|
|
955
1052
|
const baseTruthy = value !== void 0 && /\S/u.test(value);
|
|
956
|
-
|
|
1053
|
+
return inverted ? !baseTruthy : baseTruthy;
|
|
1054
|
+
}
|
|
1055
|
+
function renderConditional(node, opts) {
|
|
1056
|
+
const truthy = conditionalTakesThen(node.name, node.inverted, opts);
|
|
957
1057
|
return renderNodes(truthy ? node.then : node.else, opts);
|
|
958
1058
|
}
|
|
1059
|
+
function resolveCountConditionals(text, opts) {
|
|
1060
|
+
if (!text.includes("{?")) return text;
|
|
1061
|
+
const close = matchBraces(text);
|
|
1062
|
+
const out = [];
|
|
1063
|
+
const pending = [[0, text.length]];
|
|
1064
|
+
while (pending.length > 0) {
|
|
1065
|
+
const segment = pending.pop();
|
|
1066
|
+
const segEnd = segment[1];
|
|
1067
|
+
let i = segment[0];
|
|
1068
|
+
while (i < segEnd) {
|
|
1069
|
+
const open = text.indexOf("{?", i);
|
|
1070
|
+
if (open === -1 || open + 1 >= segEnd) {
|
|
1071
|
+
out.push(text.slice(i, segEnd));
|
|
1072
|
+
break;
|
|
1073
|
+
}
|
|
1074
|
+
const shut = close[open] ?? -1;
|
|
1075
|
+
const head = shut === -1 || shut >= segEnd ? null : recognizeConditional(text, open + 1, shut);
|
|
1076
|
+
if (head === null) {
|
|
1077
|
+
out.push(text.slice(i, Math.min(open + 2, segEnd)));
|
|
1078
|
+
i = open + 2;
|
|
1079
|
+
continue;
|
|
1080
|
+
}
|
|
1081
|
+
out.push(text.slice(i, open));
|
|
1082
|
+
const branchEnd = head.sepIndex < 0 ? shut : head.sepIndex;
|
|
1083
|
+
const [from, to] = conditionalTakesThen(head.name, head.inverted, opts) ? [head.bodyStart, branchEnd] : [head.sepIndex < 0 ? shut : head.sepIndex + 1, shut];
|
|
1084
|
+
pending.push([shut + 1, segEnd]);
|
|
1085
|
+
pending.push([from, to]);
|
|
1086
|
+
break;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
return out.join("");
|
|
1090
|
+
}
|
|
1091
|
+
function matchBraces(text) {
|
|
1092
|
+
const close = new Int32Array(text.length).fill(-1);
|
|
1093
|
+
const opens = [];
|
|
1094
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
1095
|
+
const ch = text.charAt(i);
|
|
1096
|
+
if (ch === "{") {
|
|
1097
|
+
opens.push(i);
|
|
1098
|
+
} else if (ch === "}") {
|
|
1099
|
+
const open = opens.pop();
|
|
1100
|
+
if (open !== void 0) close[open] = i;
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
return close;
|
|
1104
|
+
}
|
|
959
1105
|
function renderPlural(node, opts) {
|
|
960
|
-
const countRaw = expandVarsOnly(node.countRaw, opts);
|
|
1106
|
+
const countRaw = resolveCountConditionals(expandVarsOnly(node.countRaw, opts), opts);
|
|
961
1107
|
const formsRaw = expandVarsOnly(node.formsRaw, opts);
|
|
962
1108
|
const base = normalizeBaseLang(opts.locale);
|
|
963
1109
|
const report = (issue) => opts.onPluralError?.(issue);
|