@spintax/core 0.6.0 → 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/CHANGELOG.md +44 -0
- package/dist/index.cjs +147 -80
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +147 -80
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,19 +1,32 @@
|
|
|
1
1
|
// src/internal/ast.ts
|
|
2
2
|
var AST_VERSION = 2;
|
|
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,102 @@ 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) => ({ type: "enumeration", options: children })
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function planPermutation(rawInner) {
|
|
192
|
+
const { config, content } = extractPermutationConfig(rawInner);
|
|
193
|
+
const { texts, separators } = permutationElements(splitTopLevel(content));
|
|
194
|
+
return {
|
|
195
|
+
texts,
|
|
196
|
+
build: (children) => ({
|
|
197
|
+
type: "permutation",
|
|
198
|
+
config,
|
|
199
|
+
options: children.map((nodes, i) => ({ nodes, separator: separators[i] ?? null }))
|
|
200
|
+
})
|
|
201
|
+
};
|
|
152
202
|
}
|
|
153
203
|
function recognizeConditional(text, contentStart, contentEnd) {
|
|
154
204
|
let p = contentStart + 1;
|
|
@@ -177,17 +227,6 @@ function splitConditional(content) {
|
|
|
177
227
|
elseRaw: sep < 0 ? "" : body.slice(sep + 1)
|
|
178
228
|
};
|
|
179
229
|
}
|
|
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
230
|
function parsePlural(afterPrefix) {
|
|
192
231
|
const colon = afterPrefix.indexOf(":");
|
|
193
232
|
return { type: "plural", countRaw: afterPrefix.slice(0, colon), formsRaw: afterPrefix.slice(colon + 1) };
|
|
@@ -202,11 +241,6 @@ var PER_ELEM_HTML_RE = /^[a-zA-Z][a-zA-Z0-9]*\s/;
|
|
|
202
241
|
function defaultPermConfig() {
|
|
203
242
|
return { minsize: null, maxsize: null, sep: " ", lastsep: null };
|
|
204
243
|
}
|
|
205
|
-
function parsePermutation(rawInner) {
|
|
206
|
-
const { config, content } = extractPermutationConfig(rawInner);
|
|
207
|
-
const options = extractPerElementSeparators(splitTopLevel(content));
|
|
208
|
-
return { type: "permutation", config, options };
|
|
209
|
-
}
|
|
210
244
|
function extractPermutationConfig(content) {
|
|
211
245
|
const trimmed = phpLtrim(content);
|
|
212
246
|
if (trimmed === "" || trimmed.charAt(0) !== "<") {
|
|
@@ -250,8 +284,9 @@ function looksLikeHtmlStartTag(tagText, remaining) {
|
|
|
250
284
|
const tagName = (m[1] ?? "").toLowerCase();
|
|
251
285
|
return new RegExp(`</${escapeRegExp(tagName)}\\s*>`, "iu").test(remaining);
|
|
252
286
|
}
|
|
253
|
-
function
|
|
254
|
-
const
|
|
287
|
+
function permutationElements(rawParts) {
|
|
288
|
+
const texts = [];
|
|
289
|
+
const separators = [];
|
|
255
290
|
let pendingSep = null;
|
|
256
291
|
rawParts.forEach((part, i) => {
|
|
257
292
|
let text = part;
|
|
@@ -265,11 +300,12 @@ function extractPerElementSeparators(rawParts) {
|
|
|
265
300
|
}
|
|
266
301
|
const trimmed = phpTrim(text);
|
|
267
302
|
if (trimmed !== "") {
|
|
268
|
-
|
|
303
|
+
texts.push(trimmed);
|
|
304
|
+
separators.push(pendingSep);
|
|
269
305
|
}
|
|
270
306
|
pendingSep = trailingSep;
|
|
271
307
|
});
|
|
272
|
-
return
|
|
308
|
+
return { texts, separators };
|
|
273
309
|
}
|
|
274
310
|
function extractTrailingSep(part) {
|
|
275
311
|
const trimmed = phpRtrim(part);
|
|
@@ -1014,9 +1050,34 @@ function directReferences(text) {
|
|
|
1014
1050
|
return [...text.matchAll(/%(\w+)%/gu)].map((match) => (match[1] ?? "").toLowerCase());
|
|
1015
1051
|
}
|
|
1016
1052
|
function renderNodes(nodes, opts) {
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1053
|
+
const frame = (list) => ({ nodes: list, i: 0, out: [], pending: null });
|
|
1054
|
+
const stack = [frame(nodes)];
|
|
1055
|
+
while (stack.length > 0) {
|
|
1056
|
+
const f = stack[stack.length - 1];
|
|
1057
|
+
if (f.pending !== null) {
|
|
1058
|
+
if (f.pending.done.length < f.pending.lists.length) {
|
|
1059
|
+
stack.push(frame(f.pending.lists[f.pending.done.length]));
|
|
1060
|
+
continue;
|
|
1061
|
+
}
|
|
1062
|
+
f.out.push(f.pending.assemble(f.pending.done));
|
|
1063
|
+
f.pending = null;
|
|
1064
|
+
continue;
|
|
1065
|
+
}
|
|
1066
|
+
if (f.i >= f.nodes.length) {
|
|
1067
|
+
const text = f.out.join("");
|
|
1068
|
+
stack.pop();
|
|
1069
|
+
const parent = stack[stack.length - 1];
|
|
1070
|
+
if (parent === void 0) return text;
|
|
1071
|
+
parent.pending.done.push(text);
|
|
1072
|
+
continue;
|
|
1073
|
+
}
|
|
1074
|
+
const node = f.nodes[f.i];
|
|
1075
|
+
f.i += 1;
|
|
1076
|
+
const step = renderNode(node, opts);
|
|
1077
|
+
if (typeof step === "string") f.out.push(step);
|
|
1078
|
+
else f.pending = step;
|
|
1079
|
+
}
|
|
1080
|
+
return "";
|
|
1020
1081
|
}
|
|
1021
1082
|
function renderNode(node, opts) {
|
|
1022
1083
|
switch (node.type) {
|
|
@@ -1068,7 +1129,7 @@ function conditionalTakesThen(name, inverted, opts) {
|
|
|
1068
1129
|
}
|
|
1069
1130
|
function renderConditional(node, opts) {
|
|
1070
1131
|
const truthy = conditionalTakesThen(node.name, node.inverted, opts);
|
|
1071
|
-
return
|
|
1132
|
+
return { lists: [truthy ? node.then : node.else], done: [], assemble: (parts) => parts[0] ?? "" };
|
|
1072
1133
|
}
|
|
1073
1134
|
function resolveCountConditionals(text, opts) {
|
|
1074
1135
|
if (!text.includes("{?")) return text;
|
|
@@ -1153,7 +1214,7 @@ function renderPlural(node, opts) {
|
|
|
1153
1214
|
return fullwidthVerbatim(countRaw, formsRaw);
|
|
1154
1215
|
}
|
|
1155
1216
|
const picked = pluralFor(base, Number.parseInt(count, 10), forms);
|
|
1156
|
-
return
|
|
1217
|
+
return { lists: [parseSequence(picked)], done: [], assemble: (parts) => parts[0] ?? "" };
|
|
1157
1218
|
}
|
|
1158
1219
|
function rawConstruct(countRaw, formsRaw) {
|
|
1159
1220
|
return `{plural ${countRaw}:${formsRaw}}`;
|
|
@@ -1164,13 +1225,19 @@ function fullwidthVerbatim(countRaw, formsRaw) {
|
|
|
1164
1225
|
function renderEnumeration(options, opts) {
|
|
1165
1226
|
if (options.length === 0) return "";
|
|
1166
1227
|
const picked = options[randomInt(opts.rng, 0, options.length - 1)];
|
|
1167
|
-
|
|
1228
|
+
if (!picked) return "";
|
|
1229
|
+
return { lists: [picked], done: [], assemble: (parts) => parts[0] ?? "" };
|
|
1168
1230
|
}
|
|
1169
1231
|
function renderPermutation(node, opts) {
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1232
|
+
if (node.options.length === 0) return "";
|
|
1233
|
+
return {
|
|
1234
|
+
lists: node.options.map((o) => o.nodes),
|
|
1235
|
+
done: [],
|
|
1236
|
+
assemble: (parts) => assemblePermutation(node, parts, opts)
|
|
1237
|
+
};
|
|
1238
|
+
}
|
|
1239
|
+
function assemblePermutation(node, rendered, opts) {
|
|
1240
|
+
const elements = node.options.map((o, i) => ({ text: rendered[i] ?? "", sep: o.separator }));
|
|
1174
1241
|
const total = elements.length;
|
|
1175
1242
|
if (total === 0) return "";
|
|
1176
1243
|
const { config } = node;
|