@spintax/core 0.5.3 → 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 +86 -0
- package/dist/index.cjs +191 -114
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +191 -114
- 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;
|
|
154
|
+
}
|
|
155
|
+
if (planned !== null) {
|
|
156
|
+
if ("node" in planned) f.nodes.push(planned.node);
|
|
157
|
+
else f.plan = planned;
|
|
158
|
+
continue;
|
|
137
159
|
}
|
|
138
|
-
|
|
139
|
-
|
|
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);
|
|
@@ -748,9 +784,14 @@ function checkVariableReferences(text, idx, known, out) {
|
|
|
748
784
|
for (const m of value.matchAll(/%(\w+)%/gu)) refs.push((m[1] ?? "").toLowerCase());
|
|
749
785
|
refsOf.set(name, refs);
|
|
750
786
|
}
|
|
751
|
-
const reachesCycle = namesThatReachACycle(defs, refsOf);
|
|
787
|
+
const { reaches: reachesCycle, via } = namesThatReachACycle(defs, refsOf);
|
|
752
788
|
for (const name of defs.keys()) {
|
|
753
|
-
|
|
789
|
+
if (!reachesCycle.has(name)) continue;
|
|
790
|
+
out.push(err(
|
|
791
|
+
"variable.circular-reference",
|
|
792
|
+
`Circular variable reference: ${cyclePath(name, via)}.`,
|
|
793
|
+
{ ...defPos.get(name) ?? somewhere, data: { name } }
|
|
794
|
+
));
|
|
754
795
|
}
|
|
755
796
|
const body = text.replace(/^[ \t]*#(?:set|def)[ \t]+%\w+%[ \t]*=[ \t]*.*?$/gmu, (m) => m.replace(/[^\n]/g, " "));
|
|
756
797
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -774,6 +815,12 @@ function namesThatReachACycle(defs, refsOf) {
|
|
|
774
815
|
const BLACK = 2;
|
|
775
816
|
const color = /* @__PURE__ */ new Map();
|
|
776
817
|
const reaches = /* @__PURE__ */ new Set();
|
|
818
|
+
const via = /* @__PURE__ */ new Map();
|
|
819
|
+
const mark = (name, witness) => {
|
|
820
|
+
if (reaches.has(name)) return;
|
|
821
|
+
reaches.add(name);
|
|
822
|
+
via.set(name, witness);
|
|
823
|
+
};
|
|
777
824
|
for (const root of defs.keys()) {
|
|
778
825
|
if (color.has(root)) continue;
|
|
779
826
|
const stack = [{ name: root, refs: refsOf.get(root) ?? [], i: 0 }];
|
|
@@ -785,9 +832,9 @@ function namesThatReachACycle(defs, refsOf) {
|
|
|
785
832
|
top.i += 1;
|
|
786
833
|
if (ref === top.name || !defs.has(ref)) continue;
|
|
787
834
|
const c = color.get(ref);
|
|
788
|
-
if (c === GREY)
|
|
835
|
+
if (c === GREY) mark(top.name, ref);
|
|
789
836
|
else if (c === BLACK) {
|
|
790
|
-
if (reaches.has(ref))
|
|
837
|
+
if (reaches.has(ref)) mark(top.name, ref);
|
|
791
838
|
} else {
|
|
792
839
|
stack.push({ name: ref, refs: refsOf.get(ref) ?? [], i: 0 });
|
|
793
840
|
color.set(ref, GREY);
|
|
@@ -795,42 +842,41 @@ function namesThatReachACycle(defs, refsOf) {
|
|
|
795
842
|
} else {
|
|
796
843
|
stack.pop();
|
|
797
844
|
color.set(top.name, BLACK);
|
|
798
|
-
if (stack.length > 0 && reaches.has(top.name))
|
|
845
|
+
if (stack.length > 0 && reaches.has(top.name)) mark(stack[stack.length - 1].name, top.name);
|
|
799
846
|
}
|
|
800
847
|
}
|
|
801
848
|
}
|
|
802
|
-
return reaches;
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
const
|
|
807
|
-
const
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
const top = stack[stack.length - 1];
|
|
816
|
-
if (top.i >= top.refs.length) {
|
|
817
|
-
leave(top);
|
|
818
|
-
continue;
|
|
849
|
+
return { reaches, via };
|
|
850
|
+
}
|
|
851
|
+
var CYCLE_PATH_LIMIT = 8;
|
|
852
|
+
function cyclePath(name, via) {
|
|
853
|
+
const seen = /* @__PURE__ */ new Set([name]);
|
|
854
|
+
const shown = [name];
|
|
855
|
+
let current = name;
|
|
856
|
+
for (; ; ) {
|
|
857
|
+
const next = via.get(current);
|
|
858
|
+
if (next === void 0) break;
|
|
859
|
+
if (seen.has(next)) {
|
|
860
|
+
shown.push(next);
|
|
861
|
+
return shown.join(" \u2192 ");
|
|
819
862
|
}
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
path.push(ref);
|
|
863
|
+
if (shown.length >= CYCLE_PATH_LIMIT) {
|
|
864
|
+
let more = 0;
|
|
865
|
+
let walk2 = next;
|
|
866
|
+
while (!seen.has(walk2)) {
|
|
867
|
+
seen.add(walk2);
|
|
868
|
+
more += 1;
|
|
869
|
+
const step = via.get(walk2);
|
|
870
|
+
if (step === void 0) break;
|
|
871
|
+
walk2 = step;
|
|
872
|
+
}
|
|
873
|
+
return `${shown.join(" \u2192 ")} \u2192 \u2026 (${more} more)`;
|
|
832
874
|
}
|
|
875
|
+
seen.add(next);
|
|
876
|
+
shown.push(next);
|
|
877
|
+
current = next;
|
|
833
878
|
}
|
|
879
|
+
return shown.join(" \u2192 ");
|
|
834
880
|
}
|
|
835
881
|
function checkIncludeTargets(text, idx, known, out) {
|
|
836
882
|
const set = new Set(known);
|
|
@@ -1004,9 +1050,34 @@ function directReferences(text) {
|
|
|
1004
1050
|
return [...text.matchAll(/%(\w+)%/gu)].map((match) => (match[1] ?? "").toLowerCase());
|
|
1005
1051
|
}
|
|
1006
1052
|
function renderNodes(nodes, opts) {
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
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 "";
|
|
1010
1081
|
}
|
|
1011
1082
|
function renderNode(node, opts) {
|
|
1012
1083
|
switch (node.type) {
|
|
@@ -1058,7 +1129,7 @@ function conditionalTakesThen(name, inverted, opts) {
|
|
|
1058
1129
|
}
|
|
1059
1130
|
function renderConditional(node, opts) {
|
|
1060
1131
|
const truthy = conditionalTakesThen(node.name, node.inverted, opts);
|
|
1061
|
-
return
|
|
1132
|
+
return { lists: [truthy ? node.then : node.else], done: [], assemble: (parts) => parts[0] ?? "" };
|
|
1062
1133
|
}
|
|
1063
1134
|
function resolveCountConditionals(text, opts) {
|
|
1064
1135
|
if (!text.includes("{?")) return text;
|
|
@@ -1143,7 +1214,7 @@ function renderPlural(node, opts) {
|
|
|
1143
1214
|
return fullwidthVerbatim(countRaw, formsRaw);
|
|
1144
1215
|
}
|
|
1145
1216
|
const picked = pluralFor(base, Number.parseInt(count, 10), forms);
|
|
1146
|
-
return
|
|
1217
|
+
return { lists: [parseSequence(picked)], done: [], assemble: (parts) => parts[0] ?? "" };
|
|
1147
1218
|
}
|
|
1148
1219
|
function rawConstruct(countRaw, formsRaw) {
|
|
1149
1220
|
return `{plural ${countRaw}:${formsRaw}}`;
|
|
@@ -1154,13 +1225,19 @@ function fullwidthVerbatim(countRaw, formsRaw) {
|
|
|
1154
1225
|
function renderEnumeration(options, opts) {
|
|
1155
1226
|
if (options.length === 0) return "";
|
|
1156
1227
|
const picked = options[randomInt(opts.rng, 0, options.length - 1)];
|
|
1157
|
-
|
|
1228
|
+
if (!picked) return "";
|
|
1229
|
+
return { lists: [picked], done: [], assemble: (parts) => parts[0] ?? "" };
|
|
1158
1230
|
}
|
|
1159
1231
|
function renderPermutation(node, opts) {
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
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 }));
|
|
1164
1241
|
const total = elements.length;
|
|
1165
1242
|
if (total === 0) return "";
|
|
1166
1243
|
const { config } = node;
|