@spintax/core 0.6.0 → 0.7.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 +146 -0
- package/README.md +4 -3
- package/dist/index.cjs +195 -92
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.js +195 -92
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -21,8 +21,12 @@
|
|
|
21
21
|
* 2 — `ParsedAst` gained `defDefs`. An `Ast` cached by an older version carries no `#def` map, so
|
|
22
22
|
* rendering it would silently drop every definition; the version guard turns that into an
|
|
23
23
|
* `AstVersionError` instead.
|
|
24
|
+
*
|
|
25
|
+
* 3 — `EnumerationNode` and `PermutationNode` gained `raw`, the body a direct `%var%` reference
|
|
26
|
+
* is spliced into at render time. An `Ast` cached by version 2 carries no `raw`, so rendering it
|
|
27
|
+
* would silently keep a pipe-joined value as ONE option — the defect 0.7.0 fixed.
|
|
24
28
|
*/
|
|
25
|
-
declare const AST_VERSION =
|
|
29
|
+
declare const AST_VERSION = 3;
|
|
26
30
|
/** Opaque public handle (re-exported as `Ast` from the package index). */
|
|
27
31
|
interface Ast {
|
|
28
32
|
readonly astVersion: typeof AST_VERSION;
|
|
@@ -42,6 +46,10 @@ interface Ast {
|
|
|
42
46
|
* - conditionals test truthiness against the raw var map; plurals resolve the
|
|
43
47
|
* count (vars already expanded) then pick the bucket, lenient fullwidth
|
|
44
48
|
* fallback (Stage 6d, after vars).
|
|
49
|
+
* - a `%var%` that sits DIRECTLY in an enumeration/permutation body is spliced as
|
|
50
|
+
* TEXT and the construct re-read (spliceConstruct, 0.7.0): a `|` inside such a
|
|
51
|
+
* value separates options, exactly as in the plugin, whose expansion runs before
|
|
52
|
+
* any bracket is read. Every other construct keeps the tree it was parsed into.
|
|
45
53
|
* - #include (post-tree string pass) and post-process are later PRs.
|
|
46
54
|
*
|
|
47
55
|
* RNG note: cross-engine RNG-sequence parity is a non-goal (§3.2). Enumerations
|
package/dist/index.d.ts
CHANGED
|
@@ -21,8 +21,12 @@
|
|
|
21
21
|
* 2 — `ParsedAst` gained `defDefs`. An `Ast` cached by an older version carries no `#def` map, so
|
|
22
22
|
* rendering it would silently drop every definition; the version guard turns that into an
|
|
23
23
|
* `AstVersionError` instead.
|
|
24
|
+
*
|
|
25
|
+
* 3 — `EnumerationNode` and `PermutationNode` gained `raw`, the body a direct `%var%` reference
|
|
26
|
+
* is spliced into at render time. An `Ast` cached by version 2 carries no `raw`, so rendering it
|
|
27
|
+
* would silently keep a pipe-joined value as ONE option — the defect 0.7.0 fixed.
|
|
24
28
|
*/
|
|
25
|
-
declare const AST_VERSION =
|
|
29
|
+
declare const AST_VERSION = 3;
|
|
26
30
|
/** Opaque public handle (re-exported as `Ast` from the package index). */
|
|
27
31
|
interface Ast {
|
|
28
32
|
readonly astVersion: typeof AST_VERSION;
|
|
@@ -42,6 +46,10 @@ interface Ast {
|
|
|
42
46
|
* - conditionals test truthiness against the raw var map; plurals resolve the
|
|
43
47
|
* count (vars already expanded) then pick the bucket, lenient fullwidth
|
|
44
48
|
* fallback (Stage 6d, after vars).
|
|
49
|
+
* - a `%var%` that sits DIRECTLY in an enumeration/permutation body is spliced as
|
|
50
|
+
* TEXT and the construct re-read (spliceConstruct, 0.7.0): a `|` inside such a
|
|
51
|
+
* value separates options, exactly as in the plugin, whose expansion runs before
|
|
52
|
+
* any bracket is read. Every other construct keeps the tree it was parsed into.
|
|
45
53
|
* - #include (post-tree string pass) and post-process are later PRs.
|
|
46
54
|
*
|
|
47
55
|
* RNG note: cross-engine RNG-sequence parity is a non-goal (§3.2). Enumerations
|
package/dist/index.js
CHANGED
|
@@ -1,19 +1,32 @@
|
|
|
1
1
|
// src/internal/ast.ts
|
|
2
|
-
var AST_VERSION =
|
|
2
|
+
var AST_VERSION = 3;
|
|
3
3
|
function walk(nodes, visit) {
|
|
4
|
-
|
|
4
|
+
const stack = [{ list: nodes, i: 0 }];
|
|
5
|
+
while (stack.length > 0) {
|
|
6
|
+
const top = stack[stack.length - 1];
|
|
7
|
+
if (top.i >= top.list.length) {
|
|
8
|
+
stack.pop();
|
|
9
|
+
continue;
|
|
10
|
+
}
|
|
11
|
+
const n = top.list[top.i];
|
|
12
|
+
top.i += 1;
|
|
5
13
|
visit(n);
|
|
14
|
+
let children;
|
|
6
15
|
switch (n.type) {
|
|
7
16
|
case "enumeration":
|
|
8
|
-
|
|
17
|
+
children = n.options;
|
|
9
18
|
break;
|
|
10
19
|
case "conditional":
|
|
11
|
-
|
|
12
|
-
walk(n.else, visit);
|
|
20
|
+
children = [n.then, n.else];
|
|
13
21
|
break;
|
|
14
22
|
case "permutation":
|
|
15
|
-
|
|
23
|
+
children = n.options.map((opt) => opt.nodes);
|
|
16
24
|
break;
|
|
25
|
+
default:
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
for (let k = children.length - 1; k >= 0; k -= 1) {
|
|
29
|
+
stack.push({ list: children[k], i: 0 });
|
|
17
30
|
}
|
|
18
31
|
}
|
|
19
32
|
}
|
|
@@ -90,65 +103,114 @@ function stripComments(text) {
|
|
|
90
103
|
return text.replace(/\/#[\s\S]*?#\//g, "");
|
|
91
104
|
}
|
|
92
105
|
function parseSequence(text) {
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
};
|
|
102
|
-
while (i < text.length) {
|
|
103
|
-
const ch = text.charAt(i);
|
|
104
|
-
if (ch === "{") {
|
|
105
|
-
const end = findMatchingClose(text, i, "{", "}");
|
|
106
|
-
if (end === -1) {
|
|
107
|
-
literal += ch;
|
|
108
|
-
i += 1;
|
|
106
|
+
const frame = (t) => ({ text: t, i: 0, literal: "", nodes: [], plan: null, parts: [] });
|
|
107
|
+
const stack = [frame(text)];
|
|
108
|
+
while (stack.length > 0) {
|
|
109
|
+
const f = stack[stack.length - 1];
|
|
110
|
+
if (f.plan !== null) {
|
|
111
|
+
if (f.parts.length < f.plan.texts.length) {
|
|
112
|
+
stack.push(frame(f.plan.texts[f.parts.length]));
|
|
109
113
|
continue;
|
|
110
114
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
i = end + 1;
|
|
115
|
+
f.nodes.push(f.plan.build(f.parts));
|
|
116
|
+
f.plan = null;
|
|
117
|
+
f.parts = [];
|
|
115
118
|
continue;
|
|
116
119
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
literal
|
|
121
|
-
i += 1;
|
|
122
|
-
continue;
|
|
120
|
+
const flushLiteral = () => {
|
|
121
|
+
if (f.literal !== "") {
|
|
122
|
+
f.nodes.push({ type: "literal", value: f.literal });
|
|
123
|
+
f.literal = "";
|
|
123
124
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
125
|
+
};
|
|
126
|
+
let planned = null;
|
|
127
|
+
while (f.i < f.text.length && planned === null) {
|
|
128
|
+
const ch = f.text.charAt(f.i);
|
|
129
|
+
if (ch === "{" || ch === "[") {
|
|
130
|
+
const close = ch === "{" ? "}" : "]";
|
|
131
|
+
const end = findMatchingClose(f.text, f.i, ch, close);
|
|
132
|
+
if (end === -1) {
|
|
133
|
+
f.literal += ch;
|
|
134
|
+
f.i += 1;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const inner = f.text.slice(f.i + 1, end);
|
|
132
138
|
flushLiteral();
|
|
133
|
-
|
|
134
|
-
i
|
|
139
|
+
planned = ch === "{" ? planBraceConstruct(inner) : planPermutation(inner);
|
|
140
|
+
f.i = end + 1;
|
|
135
141
|
continue;
|
|
136
142
|
}
|
|
143
|
+
if (ch === "%") {
|
|
144
|
+
const name = VARIABLE_RE.exec(f.text.slice(f.i))?.[1];
|
|
145
|
+
if (name !== void 0) {
|
|
146
|
+
flushLiteral();
|
|
147
|
+
f.nodes.push({ type: "variable", name });
|
|
148
|
+
f.i += name.length + 2;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
f.literal += ch;
|
|
153
|
+
f.i += 1;
|
|
137
154
|
}
|
|
138
|
-
|
|
139
|
-
|
|
155
|
+
if (planned !== null) {
|
|
156
|
+
if ("node" in planned) f.nodes.push(planned.node);
|
|
157
|
+
else f.plan = planned;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
flushLiteral();
|
|
161
|
+
stack.pop();
|
|
162
|
+
const parent = stack[stack.length - 1];
|
|
163
|
+
if (parent !== void 0) parent.parts.push(f.nodes);
|
|
164
|
+
else return f.nodes;
|
|
140
165
|
}
|
|
141
|
-
|
|
142
|
-
return nodes;
|
|
166
|
+
return [];
|
|
143
167
|
}
|
|
144
|
-
function
|
|
168
|
+
function planBraceConstruct(content) {
|
|
145
169
|
if (content.charAt(0) === "?") {
|
|
146
|
-
const
|
|
147
|
-
if (
|
|
170
|
+
const parts = splitConditional(content);
|
|
171
|
+
if (parts !== null) {
|
|
172
|
+
return {
|
|
173
|
+
texts: [parts.thenRaw, parts.elseRaw],
|
|
174
|
+
build: (children) => ({
|
|
175
|
+
type: "conditional",
|
|
176
|
+
name: parts.name,
|
|
177
|
+
inverted: parts.inverted,
|
|
178
|
+
then: children[0] ?? [],
|
|
179
|
+
else: children[1] ?? []
|
|
180
|
+
})
|
|
181
|
+
};
|
|
182
|
+
}
|
|
148
183
|
} else if (content.startsWith(PLURAL_PREFIX) && content.slice(PLURAL_PREFIX.length).includes(":")) {
|
|
149
|
-
return parsePlural(content.slice(PLURAL_PREFIX.length));
|
|
184
|
+
return { node: parsePlural(content.slice(PLURAL_PREFIX.length)) };
|
|
150
185
|
}
|
|
151
|
-
return {
|
|
186
|
+
return {
|
|
187
|
+
texts: splitTopLevel(content),
|
|
188
|
+
build: (children) => hasDirectReference(children) ? { type: "enumeration", options: children, raw: content } : { type: "enumeration", options: children }
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function hasDirectReference(lists) {
|
|
192
|
+
const stack = [...lists];
|
|
193
|
+
while (stack.length > 0) {
|
|
194
|
+
const list = stack.pop();
|
|
195
|
+
for (const node of list) {
|
|
196
|
+
if (node.type === "variable") return true;
|
|
197
|
+
if (node.type === "conditional") stack.push(node.then, node.else);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
var REFERENCE_RE = /%\w+%/u;
|
|
203
|
+
function planPermutation(rawInner) {
|
|
204
|
+
const { config, content } = extractPermutationConfig(rawInner);
|
|
205
|
+
const { texts, separators } = permutationElements(splitTopLevel(content));
|
|
206
|
+
const separatorHasRef = REFERENCE_RE.test(config.sep) || config.lastsep !== null && REFERENCE_RE.test(config.lastsep) || separators.some((sep) => sep !== null && REFERENCE_RE.test(sep));
|
|
207
|
+
return {
|
|
208
|
+
texts,
|
|
209
|
+
build: (children) => {
|
|
210
|
+
const options = children.map((nodes, i) => ({ nodes, separator: separators[i] ?? null }));
|
|
211
|
+
return separatorHasRef || hasDirectReference(children) ? { type: "permutation", config, options, raw: rawInner } : { type: "permutation", config, options };
|
|
212
|
+
}
|
|
213
|
+
};
|
|
152
214
|
}
|
|
153
215
|
function recognizeConditional(text, contentStart, contentEnd) {
|
|
154
216
|
let p = contentStart + 1;
|
|
@@ -177,17 +239,6 @@ function splitConditional(content) {
|
|
|
177
239
|
elseRaw: sep < 0 ? "" : body.slice(sep + 1)
|
|
178
240
|
};
|
|
179
241
|
}
|
|
180
|
-
function tryParseConditional(content) {
|
|
181
|
-
const parts = splitConditional(content);
|
|
182
|
-
if (parts === null) return null;
|
|
183
|
-
return {
|
|
184
|
-
type: "conditional",
|
|
185
|
-
name: parts.name,
|
|
186
|
-
inverted: parts.inverted,
|
|
187
|
-
then: parseSequence(parts.thenRaw),
|
|
188
|
-
else: parseSequence(parts.elseRaw)
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
242
|
function parsePlural(afterPrefix) {
|
|
192
243
|
const colon = afterPrefix.indexOf(":");
|
|
193
244
|
return { type: "plural", countRaw: afterPrefix.slice(0, colon), formsRaw: afterPrefix.slice(colon + 1) };
|
|
@@ -202,11 +253,6 @@ var PER_ELEM_HTML_RE = /^[a-zA-Z][a-zA-Z0-9]*\s/;
|
|
|
202
253
|
function defaultPermConfig() {
|
|
203
254
|
return { minsize: null, maxsize: null, sep: " ", lastsep: null };
|
|
204
255
|
}
|
|
205
|
-
function parsePermutation(rawInner) {
|
|
206
|
-
const { config, content } = extractPermutationConfig(rawInner);
|
|
207
|
-
const options = extractPerElementSeparators(splitTopLevel(content));
|
|
208
|
-
return { type: "permutation", config, options };
|
|
209
|
-
}
|
|
210
256
|
function extractPermutationConfig(content) {
|
|
211
257
|
const trimmed = phpLtrim(content);
|
|
212
258
|
if (trimmed === "" || trimmed.charAt(0) !== "<") {
|
|
@@ -250,8 +296,9 @@ function looksLikeHtmlStartTag(tagText, remaining) {
|
|
|
250
296
|
const tagName = (m[1] ?? "").toLowerCase();
|
|
251
297
|
return new RegExp(`</${escapeRegExp(tagName)}\\s*>`, "iu").test(remaining);
|
|
252
298
|
}
|
|
253
|
-
function
|
|
254
|
-
const
|
|
299
|
+
function permutationElements(rawParts) {
|
|
300
|
+
const texts = [];
|
|
301
|
+
const separators = [];
|
|
255
302
|
let pendingSep = null;
|
|
256
303
|
rawParts.forEach((part, i) => {
|
|
257
304
|
let text = part;
|
|
@@ -265,11 +312,12 @@ function extractPerElementSeparators(rawParts) {
|
|
|
265
312
|
}
|
|
266
313
|
const trimmed = phpTrim(text);
|
|
267
314
|
if (trimmed !== "") {
|
|
268
|
-
|
|
315
|
+
texts.push(trimmed);
|
|
316
|
+
separators.push(pendingSep);
|
|
269
317
|
}
|
|
270
318
|
pendingSep = trailingSep;
|
|
271
319
|
});
|
|
272
|
-
return
|
|
320
|
+
return { texts, separators };
|
|
273
321
|
}
|
|
274
322
|
function extractTrailingSep(part) {
|
|
275
323
|
const trimmed = phpRtrim(part);
|
|
@@ -1014,9 +1062,34 @@ function directReferences(text) {
|
|
|
1014
1062
|
return [...text.matchAll(/%(\w+)%/gu)].map((match) => (match[1] ?? "").toLowerCase());
|
|
1015
1063
|
}
|
|
1016
1064
|
function renderNodes(nodes, opts) {
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1065
|
+
const frame = (list) => ({ nodes: list, i: 0, out: [], pending: null });
|
|
1066
|
+
const stack = [frame(nodes)];
|
|
1067
|
+
while (stack.length > 0) {
|
|
1068
|
+
const f = stack[stack.length - 1];
|
|
1069
|
+
if (f.pending !== null) {
|
|
1070
|
+
if (f.pending.done.length < f.pending.lists.length) {
|
|
1071
|
+
stack.push(frame(f.pending.lists[f.pending.done.length]));
|
|
1072
|
+
continue;
|
|
1073
|
+
}
|
|
1074
|
+
f.out.push(f.pending.assemble(f.pending.done));
|
|
1075
|
+
f.pending = null;
|
|
1076
|
+
continue;
|
|
1077
|
+
}
|
|
1078
|
+
if (f.i >= f.nodes.length) {
|
|
1079
|
+
const text = f.out.join("");
|
|
1080
|
+
stack.pop();
|
|
1081
|
+
const parent = stack[stack.length - 1];
|
|
1082
|
+
if (parent === void 0) return text;
|
|
1083
|
+
parent.pending.done.push(text);
|
|
1084
|
+
continue;
|
|
1085
|
+
}
|
|
1086
|
+
const node = f.nodes[f.i];
|
|
1087
|
+
f.i += 1;
|
|
1088
|
+
const step = renderNode(node, opts);
|
|
1089
|
+
if (typeof step === "string") f.out.push(step);
|
|
1090
|
+
else f.pending = step;
|
|
1091
|
+
}
|
|
1092
|
+
return "";
|
|
1020
1093
|
}
|
|
1021
1094
|
function renderNode(node, opts) {
|
|
1022
1095
|
switch (node.type) {
|
|
@@ -1025,7 +1098,7 @@ function renderNode(node, opts) {
|
|
|
1025
1098
|
case "variable":
|
|
1026
1099
|
return resolveVariable(node.name, opts);
|
|
1027
1100
|
case "enumeration":
|
|
1028
|
-
return renderEnumeration(node
|
|
1101
|
+
return renderEnumeration(node, opts);
|
|
1029
1102
|
case "permutation":
|
|
1030
1103
|
return renderPermutation(node, opts);
|
|
1031
1104
|
case "conditional":
|
|
@@ -1039,15 +1112,20 @@ function randomInt(rng, min, max) {
|
|
|
1039
1112
|
}
|
|
1040
1113
|
function resolveVariable(name, opts) {
|
|
1041
1114
|
const value = opts.vars[name.toLowerCase()];
|
|
1042
|
-
if (value === void 0) return `%${name}%`;
|
|
1043
|
-
if (opts.depth >= MAX_VARIABLE_DEPTH || !/[{[%]/u.test(value)) return value;
|
|
1115
|
+
if (value === void 0 || opts.frozen === true) return `%${name}%`;
|
|
1044
1116
|
if (opts.budget.left <= 0) return `%${name}%`;
|
|
1045
1117
|
opts.budget.left -= value.length;
|
|
1118
|
+
if (opts.depth >= MAX_VARIABLE_DEPTH || !/[{[%]/u.test(value)) return value;
|
|
1046
1119
|
return renderNodes(parseSequence(value), { ...opts, depth: opts.depth + 1 });
|
|
1047
1120
|
}
|
|
1048
|
-
function
|
|
1121
|
+
function passesLeft(opts) {
|
|
1122
|
+
return Math.max(1, MAX_VARIABLE_DEPTH - opts.depth + 1);
|
|
1123
|
+
}
|
|
1124
|
+
function expandVarsFixpoint(text, opts, passes) {
|
|
1125
|
+
if (opts.frozen === true) return { text, changed: false, converged: true };
|
|
1049
1126
|
let out = text;
|
|
1050
|
-
|
|
1127
|
+
let changedAny = false;
|
|
1128
|
+
for (let i = 0; i < passes; i += 1) {
|
|
1051
1129
|
let changed = false;
|
|
1052
1130
|
out = out.replace(/%(\w+)%/gu, (m, name) => {
|
|
1053
1131
|
const value = opts.vars[name.toLowerCase()];
|
|
@@ -1057,9 +1135,10 @@ function expandVarsOnly(text, opts) {
|
|
|
1057
1135
|
changed = true;
|
|
1058
1136
|
return value;
|
|
1059
1137
|
});
|
|
1060
|
-
if (!changed)
|
|
1138
|
+
if (!changed) return { text: out, changed: changedAny, converged: true };
|
|
1139
|
+
changedAny = true;
|
|
1061
1140
|
}
|
|
1062
|
-
return out;
|
|
1141
|
+
return { text: out, changed: changedAny, converged: false };
|
|
1063
1142
|
}
|
|
1064
1143
|
function conditionalTakesThen(name, inverted, opts) {
|
|
1065
1144
|
const value = opts.vars[name.toLowerCase()];
|
|
@@ -1068,9 +1147,9 @@ function conditionalTakesThen(name, inverted, opts) {
|
|
|
1068
1147
|
}
|
|
1069
1148
|
function renderConditional(node, opts) {
|
|
1070
1149
|
const truthy = conditionalTakesThen(node.name, node.inverted, opts);
|
|
1071
|
-
return
|
|
1150
|
+
return { lists: [truthy ? node.then : node.else], done: [], assemble: (parts) => parts[0] ?? "" };
|
|
1072
1151
|
}
|
|
1073
|
-
function
|
|
1152
|
+
function resolveConditionalsInText(text, opts) {
|
|
1074
1153
|
if (!text.includes("{?")) return text;
|
|
1075
1154
|
const close = matchBraces(text);
|
|
1076
1155
|
const out = [];
|
|
@@ -1117,8 +1196,11 @@ function matchBraces(text) {
|
|
|
1117
1196
|
return close;
|
|
1118
1197
|
}
|
|
1119
1198
|
function renderPlural(node, opts) {
|
|
1120
|
-
const
|
|
1121
|
-
const
|
|
1199
|
+
const passes = passesLeft(opts);
|
|
1200
|
+
const countPass = expandVarsFixpoint(node.countRaw, opts, passes);
|
|
1201
|
+
const formsPass = expandVarsFixpoint(node.formsRaw, opts, passes);
|
|
1202
|
+
const countRaw = resolveConditionalsInText(countPass.text, opts);
|
|
1203
|
+
const formsRaw = formsPass.text;
|
|
1122
1204
|
const base = normalizeBaseLang(opts.locale);
|
|
1123
1205
|
const report = (issue) => opts.onPluralError?.(issue);
|
|
1124
1206
|
if (/[{}[\]]/u.test(formsRaw)) {
|
|
@@ -1153,7 +1235,8 @@ function renderPlural(node, opts) {
|
|
|
1153
1235
|
return fullwidthVerbatim(countRaw, formsRaw);
|
|
1154
1236
|
}
|
|
1155
1237
|
const picked = pluralFor(base, Number.parseInt(count, 10), forms);
|
|
1156
|
-
return renderNodes(parseSequence(picked), opts);
|
|
1238
|
+
if (!formsPass.converged) return renderNodes(parseSequence(picked), { ...opts, frozen: true });
|
|
1239
|
+
return { lists: [parseSequence(picked)], done: [], assemble: (parts) => parts[0] ?? "" };
|
|
1157
1240
|
}
|
|
1158
1241
|
function rawConstruct(countRaw, formsRaw) {
|
|
1159
1242
|
return `{plural ${countRaw}:${formsRaw}}`;
|
|
@@ -1161,16 +1244,36 @@ function rawConstruct(countRaw, formsRaw) {
|
|
|
1161
1244
|
function fullwidthVerbatim(countRaw, formsRaw) {
|
|
1162
1245
|
return rawConstruct(countRaw, formsRaw).replace(/\{/gu, "\uFF5B").replace(/\}/gu, "\uFF5D");
|
|
1163
1246
|
}
|
|
1164
|
-
function renderEnumeration(
|
|
1247
|
+
function renderEnumeration(node, opts) {
|
|
1248
|
+
const spliced = node.raw === void 0 ? null : spliceConstruct(node.raw, "{", "}", opts);
|
|
1249
|
+
if (spliced !== null) return spliced;
|
|
1250
|
+
const { options } = node;
|
|
1165
1251
|
if (options.length === 0) return "";
|
|
1166
1252
|
const picked = options[randomInt(opts.rng, 0, options.length - 1)];
|
|
1167
|
-
|
|
1253
|
+
if (!picked) return "";
|
|
1254
|
+
return { lists: [picked], done: [], assemble: (parts) => parts[0] ?? "" };
|
|
1255
|
+
}
|
|
1256
|
+
function spliceConstruct(raw, open, close, opts) {
|
|
1257
|
+
if (opts.frozen === true) return null;
|
|
1258
|
+
const expanded = expandVarsFixpoint(resolveConditionalsInText(raw, opts), opts, passesLeft(opts));
|
|
1259
|
+
const body = resolveConditionalsInText(expanded.text, opts);
|
|
1260
|
+
if (body === raw) return null;
|
|
1261
|
+
const nodes = parseSequence(open + body + close);
|
|
1262
|
+
if (!expanded.converged) return renderNodes(nodes, { ...opts, frozen: true });
|
|
1263
|
+
return { lists: [nodes], done: [], assemble: (parts) => parts[0] ?? "" };
|
|
1168
1264
|
}
|
|
1169
1265
|
function renderPermutation(node, opts) {
|
|
1170
|
-
const
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1266
|
+
const spliced = node.raw === void 0 ? null : spliceConstruct(node.raw, "[", "]", opts);
|
|
1267
|
+
if (spliced !== null) return spliced;
|
|
1268
|
+
if (node.options.length === 0) return "";
|
|
1269
|
+
return {
|
|
1270
|
+
lists: node.options.map((o) => o.nodes),
|
|
1271
|
+
done: [],
|
|
1272
|
+
assemble: (parts) => assemblePermutation(node, parts, opts)
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
function assemblePermutation(node, rendered, opts) {
|
|
1276
|
+
const elements = node.options.map((o, i) => ({ text: rendered[i] ?? "", sep: o.separator }));
|
|
1174
1277
|
const total = elements.length;
|
|
1175
1278
|
if (total === 0) return "";
|
|
1176
1279
|
const { config } = node;
|