@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/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
- for (const n of nodes) {
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
- for (const opt of n.options) walk(opt, visit);
17
+ children = n.options;
9
18
  break;
10
19
  case "conditional":
11
- walk(n.then, visit);
12
- walk(n.else, visit);
20
+ children = [n.then, n.else];
13
21
  break;
14
22
  case "permutation":
15
- for (const opt of n.options) walk(opt.nodes, visit);
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 nodes = [];
94
- let literal = "";
95
- let i = 0;
96
- const flushLiteral = () => {
97
- if (literal !== "") {
98
- nodes.push({ type: "literal", value: literal });
99
- literal = "";
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
- const content = text.slice(i + 1, end);
112
- flushLiteral();
113
- nodes.push(parseBraceConstruct(content));
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
- if (ch === "[") {
118
- const end = findMatchingClose(text, i, "[", "]");
119
- if (end === -1) {
120
- literal += ch;
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
- flushLiteral();
125
- nodes.push(parsePermutation(text.slice(i + 1, end)));
126
- i = end + 1;
127
- continue;
128
- }
129
- if (ch === "%") {
130
- const name = VARIABLE_RE.exec(text.slice(i))?.[1];
131
- if (name !== void 0) {
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
- nodes.push({ type: "variable", name });
134
- i += name.length + 2;
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
- literal += ch;
139
- i += 1;
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
- flushLiteral();
142
- return nodes;
166
+ return [];
143
167
  }
144
- function parseBraceConstruct(content) {
168
+ function planBraceConstruct(content) {
145
169
  if (content.charAt(0) === "?") {
146
- const cond = tryParseConditional(content);
147
- if (cond) return cond;
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 { type: "enumeration", options: splitTopLevel(content).map((o) => parseSequence(o)) };
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 extractPerElementSeparators(rawParts) {
254
- const options = [];
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
- options.push({ nodes: parseSequence(trimmed), separator: pendingSep });
303
+ texts.push(trimmed);
304
+ separators.push(pendingSep);
269
305
  }
270
306
  pendingSep = trailingSep;
271
307
  });
272
- return options;
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
- let out = "";
1018
- for (const node of nodes) out += renderNode(node, opts);
1019
- return out;
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 renderNodes(truthy ? node.then : node.else, opts);
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 renderNodes(parseSequence(picked), opts);
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
- return picked ? renderNodes(picked, opts) : "";
1228
+ if (!picked) return "";
1229
+ return { lists: [picked], done: [], assemble: (parts) => parts[0] ?? "" };
1168
1230
  }
1169
1231
  function renderPermutation(node, opts) {
1170
- const elements = node.options.map((o) => ({
1171
- text: renderNodes(o.nodes, opts),
1172
- sep: o.separator
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;