@spintax/core 0.6.1 → 0.8.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 +463 -0
- package/README.md +11 -5
- package/dist/index.cjs +883 -282
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -1
- package/dist/index.d.ts +14 -1
- package/dist/index.js +883 -282
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/internal/ast.ts
|
|
2
|
-
var AST_VERSION =
|
|
2
|
+
var AST_VERSION = 4;
|
|
3
3
|
function walk(nodes, visit) {
|
|
4
4
|
const stack = [{ list: nodes, i: 0 }];
|
|
5
5
|
while (stack.length > 0) {
|
|
@@ -34,6 +34,15 @@ function isParsedAst(value) {
|
|
|
34
34
|
return typeof value === "object" && value !== null && value.astVersion === AST_VERSION && typeof value.source === "string" && Array.isArray(value.nodes);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
// src/internal/name-map.ts
|
|
38
|
+
function nameMap(...sources) {
|
|
39
|
+
const map = /* @__PURE__ */ Object.create(null);
|
|
40
|
+
for (const source of sources) {
|
|
41
|
+
for (const name of Object.keys(source)) map[name] = source[name];
|
|
42
|
+
}
|
|
43
|
+
return map;
|
|
44
|
+
}
|
|
45
|
+
|
|
37
46
|
// src/internal/neutralize.ts
|
|
38
47
|
var STRUCTURAL = ["{", "}", "[", "]", "%", "#"];
|
|
39
48
|
var SENTINEL_BASE = 57344;
|
|
@@ -59,18 +68,300 @@ function stripSentinels(text) {
|
|
|
59
68
|
return text.replace(RESTORE_RE, "");
|
|
60
69
|
}
|
|
61
70
|
|
|
62
|
-
// src/internal/
|
|
63
|
-
var
|
|
64
|
-
var
|
|
71
|
+
// src/internal/pairs.ts
|
|
72
|
+
var BRACE_OPEN = 123;
|
|
73
|
+
var BRACE_CLOSE = 125;
|
|
74
|
+
var BRACKET_OPEN = 91;
|
|
75
|
+
var BRACKET_CLOSE = 93;
|
|
76
|
+
function matchPairs(text, open, close) {
|
|
77
|
+
const pairs = new Int32Array(text.length).fill(-1);
|
|
78
|
+
const stack = [];
|
|
79
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
80
|
+
const code = text.charCodeAt(i);
|
|
81
|
+
if (code === open) {
|
|
82
|
+
stack.push(i);
|
|
83
|
+
} else if (code === close) {
|
|
84
|
+
const opener = stack.pop();
|
|
85
|
+
if (opener !== void 0) pairs[opener] = i;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return pairs;
|
|
89
|
+
}
|
|
90
|
+
function matchAnyPairs(text) {
|
|
91
|
+
const pairs = new Int32Array(text.length).fill(-1);
|
|
92
|
+
const stack = [];
|
|
93
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
94
|
+
const code = text.charCodeAt(i);
|
|
95
|
+
if (code === BRACE_OPEN || code === BRACKET_OPEN) {
|
|
96
|
+
stack.push(i);
|
|
97
|
+
} else if (code === BRACE_CLOSE || code === BRACKET_CLOSE) {
|
|
98
|
+
const opener = stack.pop();
|
|
99
|
+
if (opener !== void 0) pairs[opener] = i;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return pairs;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// src/internal/text-index.ts
|
|
106
|
+
var PIPE = 124;
|
|
107
|
+
var COLON = 58;
|
|
108
|
+
var QUOTE = 34;
|
|
109
|
+
var LT = 60;
|
|
110
|
+
var GT = 62;
|
|
111
|
+
var BANG = 33;
|
|
112
|
+
var QUESTION = 63;
|
|
65
113
|
var CONDITIONAL_NAME_RE = /[A-Za-z_]\w*/y;
|
|
114
|
+
var REFERENCE_SCAN_RE = /%(\w+)%/gu;
|
|
115
|
+
var CLOSING_TAG_RE = /<\/([a-z][a-z0-9-]*)[ \t\n\x0B\f\r]*>/giu;
|
|
116
|
+
function foldTagName(name) {
|
|
117
|
+
return name.toLowerCase().split(LONG_S).join("s");
|
|
118
|
+
}
|
|
119
|
+
var LONG_S = String.fromCharCode(383);
|
|
120
|
+
var rereadSpans = /* @__PURE__ */ new WeakMap();
|
|
121
|
+
function lowerBound(list, x) {
|
|
122
|
+
let lo = 0;
|
|
123
|
+
let hi = list.length;
|
|
124
|
+
while (lo < hi) {
|
|
125
|
+
const mid = lo + hi >>> 1;
|
|
126
|
+
if (list[mid] < x) lo = mid + 1;
|
|
127
|
+
else hi = mid;
|
|
128
|
+
}
|
|
129
|
+
return lo;
|
|
130
|
+
}
|
|
131
|
+
var TextIndex = class {
|
|
132
|
+
text;
|
|
133
|
+
braces = null;
|
|
134
|
+
brackets = null;
|
|
135
|
+
any = null;
|
|
136
|
+
pipeBrace = null;
|
|
137
|
+
pipeBracket = null;
|
|
138
|
+
pipesByDepth = null;
|
|
139
|
+
colons = null;
|
|
140
|
+
quotes = null;
|
|
141
|
+
gtByQuoteParity = null;
|
|
142
|
+
closingTags = null;
|
|
143
|
+
angles = null;
|
|
144
|
+
heads = null;
|
|
145
|
+
headCloseMin = null;
|
|
146
|
+
headTreeSize = 0;
|
|
147
|
+
definedReferences = /* @__PURE__ */ new WeakMap();
|
|
148
|
+
constructor(text) {
|
|
149
|
+
this.text = text;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* The closer matching the `{` or `[` at `i` within a span ending at `end`, or -1.
|
|
153
|
+
*
|
|
154
|
+
* The table pairs the whole text, and a span agrees with it: an opener inside a span is closed there
|
|
155
|
+
* exactly when the closer the whole text pairs it with lies inside too, because every opener pushed
|
|
156
|
+
* after it is pushed after the span begins.
|
|
157
|
+
*/
|
|
158
|
+
closerWithin(i, end) {
|
|
159
|
+
const table = this.text.charCodeAt(i) === BRACE_OPEN ? this.braces ??= matchPairs(this.text, BRACE_OPEN, BRACE_CLOSE) : this.brackets ??= matchPairs(this.text, BRACKET_OPEN, BRACKET_CLOSE);
|
|
160
|
+
const close = table[i];
|
|
161
|
+
return close !== -1 && close < end ? close : -1;
|
|
162
|
+
}
|
|
163
|
+
/** {@link matchAnyPairs} of the text: one stack for both bracket kinds, as a clamped depth counts. */
|
|
164
|
+
anyPairs() {
|
|
165
|
+
return this.any ??= matchAnyPairs(this.text);
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* The pipes of `[start, end)` that `splitTopLevel` splits at, in order.
|
|
169
|
+
*
|
|
170
|
+
* Its two counters are signed and start at zero, so a pipe at `p` splits exactly when the text before
|
|
171
|
+
* it holds as many `{` as `}` and as many `[` as `]` counted from `start` — when both running totals
|
|
172
|
+
* over the whole text read the same at `p` as at `start`. The pipes are grouped by those two totals
|
|
173
|
+
* once, and a span takes the group of its start.
|
|
174
|
+
*/
|
|
175
|
+
topLevelPipes(start, end) {
|
|
176
|
+
if (this.pipesByDepth === null) this.indexPipes();
|
|
177
|
+
const braceDepth = this.pipeBrace;
|
|
178
|
+
const bracketDepth = this.pipeBracket;
|
|
179
|
+
const group = this.pipesByDepth.get(braceDepth[start])?.get(bracketDepth[start]);
|
|
180
|
+
if (group === void 0) return [];
|
|
181
|
+
const out = [];
|
|
182
|
+
for (let k = lowerBound(group, start); k < group.length && group[k] < end; k += 1) {
|
|
183
|
+
out.push(group[k]);
|
|
184
|
+
}
|
|
185
|
+
return out;
|
|
186
|
+
}
|
|
187
|
+
indexPipes() {
|
|
188
|
+
const n = this.text.length;
|
|
189
|
+
const braceDepth = new Int32Array(n + 1);
|
|
190
|
+
const bracketDepth = new Int32Array(n + 1);
|
|
191
|
+
const groups = /* @__PURE__ */ new Map();
|
|
192
|
+
let brace = 0;
|
|
193
|
+
let bracket = 0;
|
|
194
|
+
for (let i = 0; i < n; i += 1) {
|
|
195
|
+
braceDepth[i] = brace;
|
|
196
|
+
bracketDepth[i] = bracket;
|
|
197
|
+
const code = this.text.charCodeAt(i);
|
|
198
|
+
if (code === BRACE_OPEN) brace += 1;
|
|
199
|
+
else if (code === BRACE_CLOSE) brace -= 1;
|
|
200
|
+
else if (code === BRACKET_OPEN) bracket += 1;
|
|
201
|
+
else if (code === BRACKET_CLOSE) bracket -= 1;
|
|
202
|
+
else if (code === PIPE) {
|
|
203
|
+
let byBracket = groups.get(brace);
|
|
204
|
+
if (byBracket === void 0) groups.set(brace, byBracket = /* @__PURE__ */ new Map());
|
|
205
|
+
const list = byBracket.get(bracket);
|
|
206
|
+
if (list === void 0) byBracket.set(bracket, [i]);
|
|
207
|
+
else list.push(i);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
braceDepth[n] = brace;
|
|
211
|
+
bracketDepth[n] = bracket;
|
|
212
|
+
this.pipeBrace = braceDepth;
|
|
213
|
+
this.pipeBracket = bracketDepth;
|
|
214
|
+
this.pipesByDepth = groups;
|
|
215
|
+
}
|
|
216
|
+
/** The first `:` in `[from, end)`, or -1. */
|
|
217
|
+
colonWithin(from, end) {
|
|
218
|
+
const colons = this.colons ??= this.positionsOf(COLON);
|
|
219
|
+
const k = lowerBound(colons, from);
|
|
220
|
+
return k < colons.length && colons[k] < end ? colons[k] : -1;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* The `>` that ends a `<config>` whose `<` is at `lt`, before `end`, or -1: the first one with an even
|
|
224
|
+
* number of `"` between the `<` and it — the quote toggle `findConfigEnd` keeps, as a parity.
|
|
225
|
+
*/
|
|
226
|
+
configEnd(lt, end) {
|
|
227
|
+
const quotes = this.quotes ??= this.positionsOf(QUOTE);
|
|
228
|
+
if (this.gtByQuoteParity === null) {
|
|
229
|
+
const even = [];
|
|
230
|
+
const odd = [];
|
|
231
|
+
let seen = 0;
|
|
232
|
+
for (let i = 0; i < this.text.length; i += 1) {
|
|
233
|
+
const code = this.text.charCodeAt(i);
|
|
234
|
+
if (code === QUOTE) seen += 1;
|
|
235
|
+
else if (code === GT) (seen % 2 === 0 ? even : odd).push(i);
|
|
236
|
+
}
|
|
237
|
+
this.gtByQuoteParity = [even, odd];
|
|
238
|
+
}
|
|
239
|
+
const parity = lowerBound(quotes, lt + 1) % 2;
|
|
240
|
+
const candidates = this.gtByQuoteParity[parity];
|
|
241
|
+
const k = lowerBound(candidates, lt + 1);
|
|
242
|
+
return k < candidates.length && candidates[k] < end ? candidates[k] : -1;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Does a closing tag of `tagName` lie wholly inside `[from, end)`? Closing tags cannot overlap — each
|
|
246
|
+
* starts with the only `<` it holds — so one scan of the whole text finds every one a scan of the span
|
|
247
|
+
* would, and the first one starting at or after `from` is the one that ends soonest.
|
|
248
|
+
*/
|
|
249
|
+
hasClosingTag(tagName, from, end) {
|
|
250
|
+
if (this.closingTags === null) {
|
|
251
|
+
const tags = /* @__PURE__ */ new Map();
|
|
252
|
+
for (const m of this.text.matchAll(CLOSING_TAG_RE)) {
|
|
253
|
+
const name = foldTagName(m[1] ?? "");
|
|
254
|
+
let entry2 = tags.get(name);
|
|
255
|
+
if (entry2 === void 0) tags.set(name, entry2 = { starts: [], ends: [] });
|
|
256
|
+
entry2.starts.push(m.index ?? 0);
|
|
257
|
+
entry2.ends.push((m.index ?? 0) + m[0].length);
|
|
258
|
+
}
|
|
259
|
+
this.closingTags = tags;
|
|
260
|
+
}
|
|
261
|
+
const entry = this.closingTags.get(tagName);
|
|
262
|
+
if (entry === void 0) return false;
|
|
263
|
+
const k = lowerBound(entry.starts, from);
|
|
264
|
+
return k < entry.starts.length && entry.ends[k] <= end;
|
|
265
|
+
}
|
|
266
|
+
/** The last `<` or `>` in `[floor, at]`, or -1. */
|
|
267
|
+
lastAngleWithin(floor, at) {
|
|
268
|
+
if (this.angles === null) {
|
|
269
|
+
const angles = [];
|
|
270
|
+
for (let i = 0; i < this.text.length; i += 1) {
|
|
271
|
+
const code = this.text.charCodeAt(i);
|
|
272
|
+
if (code === LT || code === GT) angles.push(i);
|
|
273
|
+
}
|
|
274
|
+
this.angles = angles;
|
|
275
|
+
}
|
|
276
|
+
const k = lowerBound(this.angles, at + 1) - 1;
|
|
277
|
+
return k >= 0 && this.angles[k] >= floor ? this.angles[k] : -1;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Would the renderer's conditional pass over `[start, end)` resolve anything? Exactly when a `{?` in
|
|
281
|
+
* the span opens a well-formed head and its brace closes inside the span: the pass reaches the first
|
|
282
|
+
* such one in order, because every `{?` before it is one it steps over.
|
|
283
|
+
*/
|
|
284
|
+
hasConditionalWithin(start, end) {
|
|
285
|
+
if (this.heads === null) this.indexHeads();
|
|
286
|
+
const heads = this.heads;
|
|
287
|
+
let lo = lowerBound(heads, start) + this.headTreeSize;
|
|
288
|
+
let hi = lowerBound(heads, end) + this.headTreeSize;
|
|
289
|
+
const tree = this.headCloseMin;
|
|
290
|
+
let min = Infinity;
|
|
291
|
+
while (lo < hi) {
|
|
292
|
+
if (lo & 1) min = Math.min(min, tree[lo++]);
|
|
293
|
+
if (hi & 1) min = Math.min(min, tree[--hi]);
|
|
294
|
+
lo >>= 1;
|
|
295
|
+
hi >>= 1;
|
|
296
|
+
}
|
|
297
|
+
return min < end;
|
|
298
|
+
}
|
|
299
|
+
indexHeads() {
|
|
300
|
+
const heads = [];
|
|
301
|
+
const closes = [];
|
|
302
|
+
const braces = this.braces ??= matchPairs(this.text, BRACE_OPEN, BRACE_CLOSE);
|
|
303
|
+
for (let at = this.text.indexOf("{?"); at !== -1; at = this.text.indexOf("{?", at + 1)) {
|
|
304
|
+
let p = at + 2;
|
|
305
|
+
if (this.text.charCodeAt(p) === BANG) p += 1;
|
|
306
|
+
CONDITIONAL_NAME_RE.lastIndex = p;
|
|
307
|
+
const name = CONDITIONAL_NAME_RE.exec(this.text);
|
|
308
|
+
if (name === null || this.text.charCodeAt(p + name[0].length) !== QUESTION) continue;
|
|
309
|
+
heads.push(at);
|
|
310
|
+
closes.push(braces[at]);
|
|
311
|
+
}
|
|
312
|
+
let size = 1;
|
|
313
|
+
while (size < heads.length) size *= 2;
|
|
314
|
+
const tree = new Float64Array(2 * size).fill(Infinity);
|
|
315
|
+
closes.forEach((close, k) => {
|
|
316
|
+
tree[size + k] = close === -1 ? Infinity : close;
|
|
317
|
+
});
|
|
318
|
+
for (let k = size - 1; k >= 1; k -= 1) tree[k] = Math.min(tree[2 * k], tree[2 * k + 1]);
|
|
319
|
+
this.heads = heads;
|
|
320
|
+
this.headCloseMin = tree;
|
|
321
|
+
this.headTreeSize = size;
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Would expansion substitute anything in `[start, end)` — is a reference there to a name `vars`
|
|
325
|
+
* defines? A reference cannot straddle a span of a construct's content (a bracket bounds it on both
|
|
326
|
+
* sides), so the references one scan of the whole text finds are the ones a scan of the span would.
|
|
327
|
+
*/
|
|
328
|
+
hasDefinedReferenceWithin(start, end, vars) {
|
|
329
|
+
let positions = this.definedReferences.get(vars);
|
|
330
|
+
if (positions === void 0) {
|
|
331
|
+
positions = [];
|
|
332
|
+
for (const m of this.text.matchAll(REFERENCE_SCAN_RE)) {
|
|
333
|
+
if (vars[(m[1] ?? "").toLowerCase()] !== void 0) positions.push(m.index ?? 0);
|
|
334
|
+
}
|
|
335
|
+
this.definedReferences.set(vars, positions);
|
|
336
|
+
}
|
|
337
|
+
const k = lowerBound(positions, start);
|
|
338
|
+
return k < positions.length && positions[k] < end;
|
|
339
|
+
}
|
|
340
|
+
positionsOf(code) {
|
|
341
|
+
const ch = String.fromCharCode(code);
|
|
342
|
+
const out = [];
|
|
343
|
+
for (let i = this.text.indexOf(ch); i !== -1; i = this.text.indexOf(ch, i + 1)) out.push(i);
|
|
344
|
+
return out;
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
// src/internal/parser.ts
|
|
349
|
+
var VARIABLE_RE = /%(\w+)%/y;
|
|
350
|
+
var PERCENT = 37;
|
|
351
|
+
var QUESTION2 = 63;
|
|
352
|
+
var PIPE2 = 124;
|
|
353
|
+
var LT2 = 60;
|
|
354
|
+
var GT2 = 62;
|
|
355
|
+
var DIRECTIVE_RE = /^[ \t]*#(set|def)[ \t]+%(\w+)%[ \t]*=[ \t]*((?:.*[^ \t\n\r\u2028\u2029])?)[ \t]*\r?$/gmu;
|
|
356
|
+
var CONDITIONAL_NAME_RE2 = /[A-Za-z_]\w*/y;
|
|
66
357
|
var PLURAL_PREFIX = "plural ";
|
|
67
358
|
function parseTemplate(src) {
|
|
68
359
|
const { body, setDefs, defDefs } = extractDirectives(stripComments(stripSentinels(src)));
|
|
69
360
|
return { astVersion: AST_VERSION, source: src, setDefs, defDefs, nodes: parseSequence(body) };
|
|
70
361
|
}
|
|
71
362
|
function extractDirectives(text) {
|
|
72
|
-
const setDefs =
|
|
73
|
-
const defDefs =
|
|
363
|
+
const setDefs = nameMap();
|
|
364
|
+
const defDefs = nameMap();
|
|
74
365
|
const occurrences = [];
|
|
75
366
|
let cursorOffset = 0;
|
|
76
367
|
let cursorLine = 1;
|
|
@@ -100,16 +391,36 @@ function extractDirectives(text) {
|
|
|
100
391
|
return { body: stripped.replace(/\n{3,}/gu, "\n\n"), setDefs, defDefs, occurrences };
|
|
101
392
|
}
|
|
102
393
|
function stripComments(text) {
|
|
103
|
-
|
|
394
|
+
let out = "";
|
|
395
|
+
let from = 0;
|
|
396
|
+
for (; ; ) {
|
|
397
|
+
const open = text.indexOf("/#", from);
|
|
398
|
+
if (open === -1) break;
|
|
399
|
+
const close = text.indexOf("#/", open + 2);
|
|
400
|
+
if (close === -1) break;
|
|
401
|
+
out += text.slice(from, open);
|
|
402
|
+
from = close + 2;
|
|
403
|
+
}
|
|
404
|
+
return from === 0 ? text : out + text.slice(from);
|
|
104
405
|
}
|
|
105
406
|
function parseSequence(text) {
|
|
106
|
-
const
|
|
107
|
-
const
|
|
407
|
+
const index = new TextIndex(text);
|
|
408
|
+
const frame = (start, end) => ({
|
|
409
|
+
start,
|
|
410
|
+
end,
|
|
411
|
+
i: start,
|
|
412
|
+
literal: start,
|
|
413
|
+
nodes: [],
|
|
414
|
+
plan: null,
|
|
415
|
+
parts: []
|
|
416
|
+
});
|
|
417
|
+
const stack = [frame(0, text.length)];
|
|
108
418
|
while (stack.length > 0) {
|
|
109
419
|
const f = stack[stack.length - 1];
|
|
110
420
|
if (f.plan !== null) {
|
|
111
|
-
if (f.parts.length < f.plan.
|
|
112
|
-
|
|
421
|
+
if (f.parts.length < f.plan.spans.length) {
|
|
422
|
+
const span2 = f.plan.spans[f.parts.length];
|
|
423
|
+
stack.push(frame(span2[0], span2[1]));
|
|
113
424
|
continue;
|
|
114
425
|
}
|
|
115
426
|
f.nodes.push(f.plan.build(f.parts));
|
|
@@ -117,39 +428,35 @@ function parseSequence(text) {
|
|
|
117
428
|
f.parts = [];
|
|
118
429
|
continue;
|
|
119
430
|
}
|
|
120
|
-
const flushLiteral = () => {
|
|
121
|
-
if (f.literal
|
|
122
|
-
f.nodes.push({ type: "literal", value: f.literal });
|
|
123
|
-
f.literal = "";
|
|
124
|
-
}
|
|
431
|
+
const flushLiteral = (to) => {
|
|
432
|
+
if (to > f.literal) f.nodes.push({ type: "literal", value: text.slice(f.literal, to) });
|
|
125
433
|
};
|
|
126
434
|
let planned = null;
|
|
127
|
-
while (f.i < f.
|
|
128
|
-
const
|
|
129
|
-
if (
|
|
130
|
-
const close =
|
|
131
|
-
|
|
132
|
-
if (end === -1) {
|
|
133
|
-
f.literal += ch;
|
|
435
|
+
while (f.i < f.end && planned === null) {
|
|
436
|
+
const code = text.charCodeAt(f.i);
|
|
437
|
+
if (code === BRACE_OPEN || code === BRACKET_OPEN) {
|
|
438
|
+
const close = index.closerWithin(f.i, f.end);
|
|
439
|
+
if (close === -1) {
|
|
134
440
|
f.i += 1;
|
|
135
441
|
continue;
|
|
136
442
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
f.
|
|
443
|
+
flushLiteral(f.i);
|
|
444
|
+
planned = code === BRACE_OPEN ? planBraceConstruct(index, f.i + 1, close) : planPermutation(index, f.i + 1, close);
|
|
445
|
+
f.i = close + 1;
|
|
446
|
+
f.literal = f.i;
|
|
141
447
|
continue;
|
|
142
448
|
}
|
|
143
|
-
if (
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
f.
|
|
148
|
-
f.
|
|
449
|
+
if (code === PERCENT) {
|
|
450
|
+
VARIABLE_RE.lastIndex = f.i;
|
|
451
|
+
const m = VARIABLE_RE.exec(text);
|
|
452
|
+
if (m !== null && VARIABLE_RE.lastIndex <= f.end) {
|
|
453
|
+
flushLiteral(f.i);
|
|
454
|
+
f.nodes.push({ type: "variable", name: m[1] });
|
|
455
|
+
f.i = VARIABLE_RE.lastIndex;
|
|
456
|
+
f.literal = f.i;
|
|
149
457
|
continue;
|
|
150
458
|
}
|
|
151
459
|
}
|
|
152
|
-
f.literal += ch;
|
|
153
460
|
f.i += 1;
|
|
154
461
|
}
|
|
155
462
|
if (planned !== null) {
|
|
@@ -157,7 +464,7 @@ function parseSequence(text) {
|
|
|
157
464
|
else f.plan = planned;
|
|
158
465
|
continue;
|
|
159
466
|
}
|
|
160
|
-
flushLiteral();
|
|
467
|
+
flushLiteral(f.end);
|
|
161
468
|
stack.pop();
|
|
162
469
|
const parent = stack[stack.length - 1];
|
|
163
470
|
if (parent !== void 0) parent.parts.push(f.nodes);
|
|
@@ -165,104 +472,130 @@ function parseSequence(text) {
|
|
|
165
472
|
}
|
|
166
473
|
return [];
|
|
167
474
|
}
|
|
168
|
-
function
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
475
|
+
function spansBetween(start, end, pipes) {
|
|
476
|
+
const spans = [];
|
|
477
|
+
let from = start;
|
|
478
|
+
for (const pipe of pipes) {
|
|
479
|
+
spans.push([from, pipe]);
|
|
480
|
+
from = pipe + 1;
|
|
481
|
+
}
|
|
482
|
+
spans.push([from, end]);
|
|
483
|
+
return spans;
|
|
484
|
+
}
|
|
485
|
+
function planBraceConstruct(index, start, end) {
|
|
486
|
+
const { text } = index;
|
|
487
|
+
if (start < end && text.charCodeAt(start) === QUESTION2) {
|
|
488
|
+
const head = recognizeConditional(text, start, end, index.anyPairs());
|
|
489
|
+
if (head !== null) {
|
|
490
|
+
const then = [head.bodyStart, head.sepIndex < 0 ? end : head.sepIndex];
|
|
491
|
+
const otherwise = head.sepIndex < 0 ? [end, end] : [head.sepIndex + 1, end];
|
|
172
492
|
return {
|
|
173
|
-
|
|
493
|
+
spans: [then, otherwise],
|
|
174
494
|
build: (children) => ({
|
|
175
495
|
type: "conditional",
|
|
176
|
-
name:
|
|
177
|
-
inverted:
|
|
496
|
+
name: head.name,
|
|
497
|
+
inverted: head.inverted,
|
|
178
498
|
then: children[0] ?? [],
|
|
179
499
|
else: children[1] ?? []
|
|
180
500
|
})
|
|
181
501
|
};
|
|
182
502
|
}
|
|
183
|
-
} else if (
|
|
184
|
-
|
|
503
|
+
} else if (end - start >= PLURAL_PREFIX.length && text.startsWith(PLURAL_PREFIX, start)) {
|
|
504
|
+
const colon = index.colonWithin(start + PLURAL_PREFIX.length, end);
|
|
505
|
+
if (colon !== -1) {
|
|
506
|
+
return {
|
|
507
|
+
node: {
|
|
508
|
+
type: "plural",
|
|
509
|
+
countRaw: text.slice(start + PLURAL_PREFIX.length, colon),
|
|
510
|
+
formsRaw: text.slice(colon + 1, end)
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
}
|
|
185
514
|
}
|
|
186
515
|
return {
|
|
187
|
-
|
|
188
|
-
build: (children) =>
|
|
516
|
+
spans: spansBetween(start, end, index.topLevelPipes(start, end)),
|
|
517
|
+
build: (children) => {
|
|
518
|
+
if (!needsTextualReread(children)) return { type: "enumeration", options: children };
|
|
519
|
+
const node = { type: "enumeration", options: children, raw: text.slice(start, end) };
|
|
520
|
+
rereadSpans.set(node, { index, start, end });
|
|
521
|
+
return node;
|
|
522
|
+
}
|
|
189
523
|
};
|
|
190
524
|
}
|
|
191
|
-
function
|
|
192
|
-
|
|
193
|
-
|
|
525
|
+
function needsTextualReread(lists) {
|
|
526
|
+
return lists.some((list) => list.some((node) => node.type === "variable" || node.type === "conditional"));
|
|
527
|
+
}
|
|
528
|
+
var REFERENCE_RE = /%\w+%/u;
|
|
529
|
+
function holdsConditional(text) {
|
|
530
|
+
if (!text.includes("{?")) return false;
|
|
531
|
+
const opens = [];
|
|
532
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
533
|
+
const ch = text.charAt(i);
|
|
534
|
+
if (ch === "{") {
|
|
535
|
+
opens.push(i);
|
|
536
|
+
} else if (ch === "}") {
|
|
537
|
+
const open = opens.pop();
|
|
538
|
+
if (open !== void 0 && text.charAt(open + 1) === "?" && recognizeConditional(text, open + 1, i) !== null) {
|
|
539
|
+
return true;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
return false;
|
|
544
|
+
}
|
|
545
|
+
var holdsTextForReread = (text) => REFERENCE_RE.test(text) || holdsConditional(text);
|
|
546
|
+
function planPermutation(index, start, end) {
|
|
547
|
+
const { text } = index;
|
|
548
|
+
const { config, contentStart } = extractPermutationConfig(index, start, end);
|
|
549
|
+
const { spans, separators } = permutationElements(index, spansBetween(contentStart, end, index.topLevelPipes(contentStart, end)));
|
|
550
|
+
const header = text.slice(start, contentStart);
|
|
551
|
+
const textNeedsReread = holdsTextForReread(header) || separators.some((sep) => sep !== null && holdsTextForReread(sep));
|
|
194
552
|
return {
|
|
195
|
-
|
|
196
|
-
build: (children) =>
|
|
197
|
-
|
|
198
|
-
config,
|
|
199
|
-
|
|
200
|
-
|
|
553
|
+
spans,
|
|
554
|
+
build: (children) => {
|
|
555
|
+
const options = children.map((nodes, i) => ({ nodes, separator: separators[i] ?? null }));
|
|
556
|
+
if (!textNeedsReread && !needsTextualReread(children)) return { type: "permutation", config, options };
|
|
557
|
+
const node = { type: "permutation", config, options, raw: text.slice(start, end) };
|
|
558
|
+
rereadSpans.set(node, { index, start, end });
|
|
559
|
+
return node;
|
|
560
|
+
}
|
|
201
561
|
};
|
|
202
562
|
}
|
|
203
|
-
function recognizeConditional(text, contentStart, contentEnd) {
|
|
563
|
+
function recognizeConditional(text, contentStart, contentEnd, anyPairs) {
|
|
204
564
|
let p = contentStart + 1;
|
|
205
565
|
let inverted = false;
|
|
206
566
|
if (text.charAt(p) === "!") {
|
|
207
567
|
inverted = true;
|
|
208
568
|
p += 1;
|
|
209
569
|
}
|
|
210
|
-
|
|
211
|
-
const name =
|
|
570
|
+
CONDITIONAL_NAME_RE2.lastIndex = p;
|
|
571
|
+
const name = CONDITIONAL_NAME_RE2.exec(text)?.[0];
|
|
212
572
|
if (name === void 0 || p + name.length > contentEnd) return null;
|
|
213
573
|
p += name.length;
|
|
214
574
|
if (text.charAt(p) !== "?") return null;
|
|
215
575
|
p += 1;
|
|
216
|
-
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
thenRaw: sep < 0 ? body : body.slice(0, sep),
|
|
227
|
-
elseRaw: sep < 0 ? "" : body.slice(sep + 1)
|
|
228
|
-
};
|
|
229
|
-
}
|
|
230
|
-
function parsePlural(afterPrefix) {
|
|
231
|
-
const colon = afterPrefix.indexOf(":");
|
|
232
|
-
return { type: "plural", countRaw: afterPrefix.slice(0, colon), formsRaw: afterPrefix.slice(colon + 1) };
|
|
233
|
-
}
|
|
234
|
-
var CONFIG_KEY_RE = /\b(?:minsize|maxsize|sep|lastsep)\s*=/i;
|
|
235
|
-
var MINSIZE_RE = /minsize\s*=\s*(\d+)/i;
|
|
236
|
-
var MAXSIZE_RE = /maxsize\s*=\s*(\d+)/i;
|
|
237
|
-
var SEP_RE = /(?<!last)sep\s*=\s*"([^"]*)"/i;
|
|
238
|
-
var LASTSEP_RE = /lastsep\s*=\s*"([^"]*)"/i;
|
|
239
|
-
var HTML_TAG_RE = /^([a-zA-Z][a-zA-Z0-9-]*)(?:\s+[^>]*)?\/?$/;
|
|
240
|
-
var PER_ELEM_HTML_RE = /^[a-zA-Z][a-zA-Z0-9]*\s/;
|
|
576
|
+
const sepIndex = anyPairs === void 0 ? firstTopLevelPipe(text, p, contentEnd) : firstTopLevelPipeByPairs(text, p, contentEnd, anyPairs);
|
|
577
|
+
return { name, inverted, bodyStart: p, sepIndex };
|
|
578
|
+
}
|
|
579
|
+
var CONFIG_KEY_RE = /\b(?:minsize|maxsize|sep|lastsep)[ \t\n\x0B\f\r]*=/i;
|
|
580
|
+
var MINSIZE_RE = /minsize[ \t\n\x0B\f\r]*=[ \t\n\x0B\f\r]*(\d+)/i;
|
|
581
|
+
var MAXSIZE_RE = /maxsize[ \t\n\x0B\f\r]*=[ \t\n\x0B\f\r]*(\d+)/i;
|
|
582
|
+
var SEP_RE = /(?<!last)sep[ \t\n\x0B\f\r]*=[ \t\n\x0B\f\r]*"([^"]*)"/i;
|
|
583
|
+
var LASTSEP_RE = /lastsep[ \t\n\x0B\f\r]*=[ \t\n\x0B\f\r]*"([^"]*)"/i;
|
|
584
|
+
var HTML_TAG_RE = /^([a-zA-Z][a-zA-Z0-9-]*)(?:[ \t\n\x0B\f\r][^>]*)?\/?$/;
|
|
585
|
+
var PER_ELEM_HTML_RE = /^[a-zA-Z][a-zA-Z0-9]*[ \t\n\x0B\f\r]/;
|
|
241
586
|
function defaultPermConfig() {
|
|
242
587
|
return { minsize: null, maxsize: null, sep: " ", lastsep: null };
|
|
243
588
|
}
|
|
244
|
-
function extractPermutationConfig(
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}
|
|
249
|
-
const
|
|
250
|
-
if (
|
|
251
|
-
const configStr =
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
return { config: defaultPermConfig(), content };
|
|
255
|
-
}
|
|
256
|
-
return { config: parseConfigString(configStr), content: remaining };
|
|
257
|
-
}
|
|
258
|
-
function findConfigEnd(text) {
|
|
259
|
-
let inQuote = false;
|
|
260
|
-
for (let i = 1; i < text.length; i += 1) {
|
|
261
|
-
const ch = text.charAt(i);
|
|
262
|
-
if (ch === '"') inQuote = !inQuote;
|
|
263
|
-
if (ch === ">" && !inQuote) return i;
|
|
264
|
-
}
|
|
265
|
-
return -1;
|
|
589
|
+
function extractPermutationConfig(index, start, end) {
|
|
590
|
+
const { text } = index;
|
|
591
|
+
let lt = start;
|
|
592
|
+
while (lt < end && isPhpTrimChar(text.charCodeAt(lt))) lt += 1;
|
|
593
|
+
if (lt === end || text.charCodeAt(lt) !== LT2) return { config: defaultPermConfig(), contentStart: start };
|
|
594
|
+
const gt = index.configEnd(lt, end);
|
|
595
|
+
if (gt === -1) return { config: defaultPermConfig(), contentStart: start };
|
|
596
|
+
const configStr = text.slice(lt + 1, gt);
|
|
597
|
+
if (looksLikeHtmlStartTag(index, configStr, gt + 1, end)) return { config: defaultPermConfig(), contentStart: start };
|
|
598
|
+
return { config: parseConfigString(configStr), contentStart: gt + 1 };
|
|
266
599
|
}
|
|
267
600
|
function parseConfigString(str) {
|
|
268
601
|
if (!CONFIG_KEY_RE.test(str)) {
|
|
@@ -275,58 +608,54 @@ function parseConfigString(str) {
|
|
|
275
608
|
lastsep: strGroup(LASTSEP_RE.exec(str))
|
|
276
609
|
};
|
|
277
610
|
}
|
|
278
|
-
function looksLikeHtmlStartTag(tagText,
|
|
611
|
+
function looksLikeHtmlStartTag(index, tagText, from, end) {
|
|
279
612
|
const trimmed = phpTrim(tagText);
|
|
280
613
|
if (trimmed === "") return false;
|
|
281
614
|
const m = HTML_TAG_RE.exec(trimmed);
|
|
282
615
|
if (!m) return false;
|
|
283
616
|
if (trimmed.endsWith("/")) return true;
|
|
284
|
-
|
|
285
|
-
return new RegExp(`</${escapeRegExp(tagName)}\\s*>`, "iu").test(remaining);
|
|
617
|
+
return index.hasClosingTag((m[1] ?? "").toLowerCase(), from, end);
|
|
286
618
|
}
|
|
287
|
-
function permutationElements(
|
|
288
|
-
const
|
|
619
|
+
function permutationElements(index, parts) {
|
|
620
|
+
const { text } = index;
|
|
621
|
+
const spans = [];
|
|
289
622
|
const separators = [];
|
|
290
623
|
let pendingSep = null;
|
|
291
|
-
|
|
292
|
-
let
|
|
624
|
+
parts.forEach(([partStart, partEnd], i) => {
|
|
625
|
+
let textEnd = partEnd;
|
|
293
626
|
let trailingSep = null;
|
|
294
|
-
if (i <
|
|
295
|
-
const extracted = extractTrailingSep(
|
|
627
|
+
if (i < parts.length - 1) {
|
|
628
|
+
const extracted = extractTrailingSep(index, partStart, partEnd);
|
|
296
629
|
if (extracted) {
|
|
297
|
-
|
|
630
|
+
textEnd = extracted.textEnd;
|
|
298
631
|
trailingSep = extracted.sep;
|
|
299
632
|
}
|
|
300
633
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
634
|
+
let a = partStart;
|
|
635
|
+
let b = textEnd;
|
|
636
|
+
while (a < b && isPhpTrimChar(text.charCodeAt(a))) a += 1;
|
|
637
|
+
while (b > a && isPhpTrimChar(text.charCodeAt(b - 1))) b -= 1;
|
|
638
|
+
if (b > a) {
|
|
639
|
+
spans.push([a, b]);
|
|
304
640
|
separators.push(pendingSep);
|
|
305
641
|
}
|
|
306
642
|
pendingSep = trailingSep;
|
|
307
643
|
});
|
|
308
|
-
return {
|
|
309
|
-
}
|
|
310
|
-
function extractTrailingSep(
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
openPos = i;
|
|
319
|
-
break;
|
|
320
|
-
}
|
|
321
|
-
if (ch === ">") return null;
|
|
322
|
-
}
|
|
323
|
-
if (openPos === -1) return null;
|
|
324
|
-
const inner = trimmed.slice(openPos + 1, len - 1);
|
|
644
|
+
return { spans, separators };
|
|
645
|
+
}
|
|
646
|
+
function extractTrailingSep(index, start, end) {
|
|
647
|
+
const { text } = index;
|
|
648
|
+
let len = end;
|
|
649
|
+
while (len > start && isPhpTrimChar(text.charCodeAt(len - 1))) len -= 1;
|
|
650
|
+
if (len === start || text.charCodeAt(len - 1) !== GT2) return null;
|
|
651
|
+
const openPos = index.lastAngleWithin(start, len - 2);
|
|
652
|
+
if (openPos === -1 || text.charCodeAt(openPos) === GT2) return null;
|
|
653
|
+
const inner = text.slice(openPos + 1, len - 1);
|
|
325
654
|
const innerTrimmed = phpTrim(inner);
|
|
326
655
|
if (innerTrimmed.startsWith("/") || innerTrimmed.endsWith("/") || PER_ELEM_HTML_RE.test(innerTrimmed)) {
|
|
327
656
|
return null;
|
|
328
657
|
}
|
|
329
|
-
return {
|
|
658
|
+
return { textEnd: openPos, sep: inner };
|
|
330
659
|
}
|
|
331
660
|
function intGroup(m) {
|
|
332
661
|
return m && m[1] !== void 0 ? Number.parseInt(m[1], 10) : null;
|
|
@@ -334,52 +663,21 @@ function intGroup(m) {
|
|
|
334
663
|
function strGroup(m) {
|
|
335
664
|
return m && m[1] !== void 0 ? m[1] : null;
|
|
336
665
|
}
|
|
337
|
-
function
|
|
338
|
-
return
|
|
666
|
+
function isPhpTrimChar(code) {
|
|
667
|
+
return code === 32 || code === 9 || code === 10 || code === 13 || code === 0 || code === 11;
|
|
339
668
|
}
|
|
340
|
-
var PHP_LTRIM_RE = /^[ \t\n\r\0\x0B]+/u;
|
|
341
|
-
var PHP_RTRIM_RE = /[ \t\n\r\0\x0B]+$/u;
|
|
342
669
|
function phpTrim(s) {
|
|
343
|
-
return
|
|
670
|
+
return phpRtrim(phpLtrim(s));
|
|
344
671
|
}
|
|
345
672
|
function phpLtrim(s) {
|
|
346
|
-
|
|
673
|
+
let start = 0;
|
|
674
|
+
while (start < s.length && isPhpTrimChar(s.charCodeAt(start))) start += 1;
|
|
675
|
+
return start === 0 ? s : s.slice(start);
|
|
347
676
|
}
|
|
348
677
|
function phpRtrim(s) {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
let depth = 0;
|
|
353
|
-
for (let i = openPos; i < text.length; i += 1) {
|
|
354
|
-
const ch = text.charAt(i);
|
|
355
|
-
if (ch === open) {
|
|
356
|
-
depth += 1;
|
|
357
|
-
} else if (ch === close) {
|
|
358
|
-
depth -= 1;
|
|
359
|
-
if (depth === 0) return i;
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
return -1;
|
|
363
|
-
}
|
|
364
|
-
function splitTopLevel(inner) {
|
|
365
|
-
const parts = [];
|
|
366
|
-
let brace = 0;
|
|
367
|
-
let bracket = 0;
|
|
368
|
-
let cur = "";
|
|
369
|
-
for (const ch of inner) {
|
|
370
|
-
if (ch === "{") brace += 1;
|
|
371
|
-
else if (ch === "}") brace -= 1;
|
|
372
|
-
else if (ch === "[") bracket += 1;
|
|
373
|
-
else if (ch === "]") bracket -= 1;
|
|
374
|
-
if (ch === "|" && brace === 0 && bracket === 0) {
|
|
375
|
-
parts.push(cur);
|
|
376
|
-
cur = "";
|
|
377
|
-
} else {
|
|
378
|
-
cur += ch;
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
parts.push(cur);
|
|
382
|
-
return parts;
|
|
678
|
+
let end = s.length;
|
|
679
|
+
while (end > 0 && isPhpTrimChar(s.charCodeAt(end - 1))) end -= 1;
|
|
680
|
+
return end === s.length ? s : s.slice(0, end);
|
|
383
681
|
}
|
|
384
682
|
function firstTopLevelPipe(body, from = 0, to = body.length) {
|
|
385
683
|
let depth = 0;
|
|
@@ -395,27 +693,35 @@ function firstTopLevelPipe(body, from = 0, to = body.length) {
|
|
|
395
693
|
}
|
|
396
694
|
return -1;
|
|
397
695
|
}
|
|
696
|
+
function firstTopLevelPipeByPairs(body, from, to, anyPairs) {
|
|
697
|
+
let j = from;
|
|
698
|
+
while (j < to) {
|
|
699
|
+
const code = body.charCodeAt(j);
|
|
700
|
+
if (code === BRACE_OPEN || code === BRACKET_OPEN) {
|
|
701
|
+
const close = anyPairs[j];
|
|
702
|
+
if (close === -1 || close >= to) return -1;
|
|
703
|
+
j = close + 1;
|
|
704
|
+
} else if (code === PIPE2) {
|
|
705
|
+
return j;
|
|
706
|
+
} else {
|
|
707
|
+
j += 1;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
return -1;
|
|
711
|
+
}
|
|
398
712
|
|
|
399
713
|
// src/internal/plurals.ts
|
|
400
714
|
var PLURAL_PREFIX2 = "{plural ";
|
|
401
715
|
function findPluralBlocks(text) {
|
|
402
716
|
const blocks = [];
|
|
717
|
+
let close = null;
|
|
403
718
|
let i = 0;
|
|
404
719
|
while (i < text.length) {
|
|
405
720
|
const start = text.indexOf(PLURAL_PREFIX2, i);
|
|
406
721
|
if (start === -1) break;
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
const ch = text.charAt(j);
|
|
411
|
-
if (ch === "{") depth += 1;
|
|
412
|
-
else if (ch === "}") {
|
|
413
|
-
depth -= 1;
|
|
414
|
-
if (depth === 0) break;
|
|
415
|
-
}
|
|
416
|
-
j += 1;
|
|
417
|
-
}
|
|
418
|
-
if (depth !== 0) {
|
|
722
|
+
close ??= matchPairs(text, BRACE_OPEN, BRACE_CLOSE);
|
|
723
|
+
const j = close[start];
|
|
724
|
+
if (j === -1) {
|
|
419
725
|
i = start + PLURAL_PREFIX2.length;
|
|
420
726
|
continue;
|
|
421
727
|
}
|
|
@@ -578,12 +884,19 @@ function checkDirectives(text, out) {
|
|
|
578
884
|
}
|
|
579
885
|
}
|
|
580
886
|
}
|
|
887
|
+
var CONFIG_KEY_PRESENT_RE = /(?<!\w)\w+[ \t\n\x0B\f\r]*=/;
|
|
888
|
+
var CONFIG_KEYS_RE = /(?<!\w)(\w+)[ \t\n\x0B\f\r]*=/gu;
|
|
581
889
|
function checkPermutationConfigs(text, idx, out) {
|
|
582
|
-
for (
|
|
583
|
-
const
|
|
584
|
-
if (
|
|
585
|
-
const configBase =
|
|
586
|
-
|
|
890
|
+
for (let from = 0; ; ) {
|
|
891
|
+
const open = text.indexOf("[<", from);
|
|
892
|
+
if (open === -1) break;
|
|
893
|
+
const configBase = open + 2;
|
|
894
|
+
const gt = text.indexOf(">", configBase);
|
|
895
|
+
if (gt === -1) break;
|
|
896
|
+
from = gt + 1;
|
|
897
|
+
const configStr = text.slice(configBase, gt);
|
|
898
|
+
if (!CONFIG_KEY_PRESENT_RE.test(configStr)) continue;
|
|
899
|
+
for (const km of configStr.matchAll(CONFIG_KEYS_RE)) {
|
|
587
900
|
const key = (km[1] ?? "").toLowerCase();
|
|
588
901
|
if (!KNOWN_CONFIG_KEYS.has(key)) {
|
|
589
902
|
out.push(err(
|
|
@@ -593,7 +906,7 @@ function checkPermutationConfigs(text, idx, out) {
|
|
|
593
906
|
));
|
|
594
907
|
}
|
|
595
908
|
}
|
|
596
|
-
const min = /minsize\
|
|
909
|
+
const min = /minsize[ \t\n\x0B\f\r]*=[ \t\n\x0B\f\r]*([^;> \t\n\x0B\f\r]+)/i.exec(configStr);
|
|
597
910
|
if (min && !/^\d+$/.test(min[1] ?? "")) {
|
|
598
911
|
out.push(err(
|
|
599
912
|
"permutation.minsize-not-integer",
|
|
@@ -601,7 +914,7 @@ function checkPermutationConfigs(text, idx, out) {
|
|
|
601
914
|
{ ...span(idx, configBase + min.index, min[0].length), data: { value: min[1] } }
|
|
602
915
|
));
|
|
603
916
|
}
|
|
604
|
-
const max = /maxsize\
|
|
917
|
+
const max = /maxsize[ \t\n\x0B\f\r]*=[ \t\n\x0B\f\r]*([^;> \t\n\x0B\f\r]+)/i.exec(configStr);
|
|
605
918
|
if (max && !/^\d+$/.test(max[1] ?? "")) {
|
|
606
919
|
out.push(err(
|
|
607
920
|
"permutation.maxsize-not-integer",
|
|
@@ -924,6 +1237,13 @@ function addAll(target, text, re, fold) {
|
|
|
924
1237
|
}
|
|
925
1238
|
}
|
|
926
1239
|
|
|
1240
|
+
// src/internal/charclass.ts
|
|
1241
|
+
var UCP_SPACE = "\\t\\n\\x0B\\f\\r \\x85\\xA0\\u1680\\u180E\\u2000-\\u200A\\u2028\\u2029\\u202F\\u205F\\u3000";
|
|
1242
|
+
function isUcpSpace(code) {
|
|
1243
|
+
return code >= 9 && code <= 13 || code === 32 || code === 133 || code === 160 || code === 5760 || code === 6158 || code >= 8192 && code <= 8202 || code === 8232 || code === 8233 || code === 8239 || code === 8287 || code === 12288;
|
|
1244
|
+
}
|
|
1245
|
+
var UCP_WORD = "\\p{L}\\p{N}\\p{Mn}\\p{Pc}";
|
|
1246
|
+
|
|
927
1247
|
// src/internal/errors.ts
|
|
928
1248
|
var SpintaxError = class extends Error {
|
|
929
1249
|
constructor(message) {
|
|
@@ -953,6 +1273,120 @@ var NotImplementedError = class extends SpintaxError {
|
|
|
953
1273
|
}
|
|
954
1274
|
};
|
|
955
1275
|
|
|
1276
|
+
// src/internal/fragment.ts
|
|
1277
|
+
var FLAT_BELOW = 128;
|
|
1278
|
+
var Join = class {
|
|
1279
|
+
constructor(pieces) {
|
|
1280
|
+
this.pieces = pieces;
|
|
1281
|
+
let length = 0;
|
|
1282
|
+
for (const piece of pieces) length += lengthOf(piece);
|
|
1283
|
+
this.length = length;
|
|
1284
|
+
let lead = 0;
|
|
1285
|
+
for (const piece of pieces) {
|
|
1286
|
+
const pieceLead = leadOf(piece);
|
|
1287
|
+
lead += pieceLead;
|
|
1288
|
+
if (pieceLead < lengthOf(piece)) break;
|
|
1289
|
+
}
|
|
1290
|
+
this.lead = lead;
|
|
1291
|
+
let trail = 0;
|
|
1292
|
+
if (lead < length) {
|
|
1293
|
+
for (let k = pieces.length - 1; k >= 0; k -= 1) {
|
|
1294
|
+
const piece = pieces[k];
|
|
1295
|
+
const pieceTrail = trailOf(piece);
|
|
1296
|
+
trail += pieceTrail;
|
|
1297
|
+
if (pieceTrail < lengthOf(piece)) break;
|
|
1298
|
+
}
|
|
1299
|
+
} else {
|
|
1300
|
+
trail = length;
|
|
1301
|
+
}
|
|
1302
|
+
this.trail = trail;
|
|
1303
|
+
}
|
|
1304
|
+
pieces;
|
|
1305
|
+
length;
|
|
1306
|
+
lead;
|
|
1307
|
+
trail;
|
|
1308
|
+
};
|
|
1309
|
+
var Window = class {
|
|
1310
|
+
constructor(base, from, to) {
|
|
1311
|
+
this.base = base;
|
|
1312
|
+
this.from = from;
|
|
1313
|
+
this.to = to;
|
|
1314
|
+
this.length = to - from;
|
|
1315
|
+
}
|
|
1316
|
+
base;
|
|
1317
|
+
from;
|
|
1318
|
+
to;
|
|
1319
|
+
length;
|
|
1320
|
+
lead = 0;
|
|
1321
|
+
trail = 0;
|
|
1322
|
+
};
|
|
1323
|
+
function lengthOf(fragment) {
|
|
1324
|
+
return fragment.length;
|
|
1325
|
+
}
|
|
1326
|
+
function leadOf(fragment) {
|
|
1327
|
+
if (typeof fragment !== "string") return fragment.lead;
|
|
1328
|
+
let k = 0;
|
|
1329
|
+
while (k < fragment.length && isPhpTrimChar(fragment.charCodeAt(k))) k += 1;
|
|
1330
|
+
return k;
|
|
1331
|
+
}
|
|
1332
|
+
function trailOf(fragment) {
|
|
1333
|
+
if (typeof fragment !== "string") return fragment.trail;
|
|
1334
|
+
let k = fragment.length;
|
|
1335
|
+
while (k > 0 && isPhpTrimChar(fragment.charCodeAt(k - 1))) k -= 1;
|
|
1336
|
+
return fragment.length - k;
|
|
1337
|
+
}
|
|
1338
|
+
function joinFragments(pieces) {
|
|
1339
|
+
if (pieces.length === 0) return "";
|
|
1340
|
+
if (pieces.length === 1) return pieces[0];
|
|
1341
|
+
let total = 0;
|
|
1342
|
+
let flat = true;
|
|
1343
|
+
for (const piece of pieces) {
|
|
1344
|
+
total += lengthOf(piece);
|
|
1345
|
+
if (typeof piece !== "string") flat = false;
|
|
1346
|
+
}
|
|
1347
|
+
if (flat && total < FLAT_BELOW) return pieces.join("");
|
|
1348
|
+
return new Join(pieces);
|
|
1349
|
+
}
|
|
1350
|
+
function trimFragment(fragment) {
|
|
1351
|
+
if (typeof fragment === "string") {
|
|
1352
|
+
let start = 0;
|
|
1353
|
+
while (start < fragment.length && isPhpTrimChar(fragment.charCodeAt(start))) start += 1;
|
|
1354
|
+
let end = fragment.length;
|
|
1355
|
+
while (end > start && isPhpTrimChar(fragment.charCodeAt(end - 1))) end -= 1;
|
|
1356
|
+
return start === 0 && end === fragment.length ? fragment : fragment.slice(start, end);
|
|
1357
|
+
}
|
|
1358
|
+
if (fragment.lead === fragment.length) return "";
|
|
1359
|
+
if (fragment.lead === 0 && fragment.trail === 0) return fragment;
|
|
1360
|
+
return new Window(fragment, fragment.lead, fragment.length - fragment.trail);
|
|
1361
|
+
}
|
|
1362
|
+
function flatten(fragment) {
|
|
1363
|
+
if (typeof fragment === "string") return fragment;
|
|
1364
|
+
const out = [];
|
|
1365
|
+
const stack = [[fragment, 0, fragment.length]];
|
|
1366
|
+
while (stack.length > 0) {
|
|
1367
|
+
const [piece, from, to] = stack.pop();
|
|
1368
|
+
if (from >= to) continue;
|
|
1369
|
+
if (typeof piece === "string") {
|
|
1370
|
+
out.push(from === 0 && to === piece.length ? piece : piece.slice(from, to));
|
|
1371
|
+
} else if (piece instanceof Window) {
|
|
1372
|
+
stack.push([piece.base, piece.from + from, piece.from + to]);
|
|
1373
|
+
} else {
|
|
1374
|
+
const wanted = [];
|
|
1375
|
+
let offset = 0;
|
|
1376
|
+
for (const child of piece.pieces) {
|
|
1377
|
+
const length = lengthOf(child);
|
|
1378
|
+
const a = Math.max(from, offset);
|
|
1379
|
+
const b = Math.min(to, offset + length);
|
|
1380
|
+
if (a < b) wanted.push([child, a - offset, b - offset]);
|
|
1381
|
+
offset += length;
|
|
1382
|
+
if (offset >= to) break;
|
|
1383
|
+
}
|
|
1384
|
+
for (let k = wanted.length - 1; k >= 0; k -= 1) stack.push(wanted[k]);
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
return out.join("");
|
|
1388
|
+
}
|
|
1389
|
+
|
|
956
1390
|
// src/internal/render.ts
|
|
957
1391
|
var MAX_VARIABLE_DEPTH = 50;
|
|
958
1392
|
var MAX_EXPANSION_CHARS = 1024 * 1024;
|
|
@@ -966,7 +1400,7 @@ function renderAst(ast, ctx) {
|
|
|
966
1400
|
onPluralError: ctx.onPluralError,
|
|
967
1401
|
budget: ctx.budget
|
|
968
1402
|
};
|
|
969
|
-
const vars = Object.keys(ast.defDefs).length > 0 ?
|
|
1403
|
+
const vars = Object.keys(ast.defDefs).length > 0 ? nameMap(base, rollDefinitions(ast.defDefs, base, ctx.runtimeContext, walkOpts)) : base;
|
|
970
1404
|
const text = renderNodes(ast.nodes, { ...walkOpts, vars });
|
|
971
1405
|
return ctx.resolver ? resolveIncludes(text, ctx) : text;
|
|
972
1406
|
}
|
|
@@ -990,7 +1424,7 @@ function resolveIncludes(text, ctx) {
|
|
|
990
1424
|
});
|
|
991
1425
|
}
|
|
992
1426
|
function buildVars(setDefs, context) {
|
|
993
|
-
const vars =
|
|
1427
|
+
const vars = nameMap();
|
|
994
1428
|
for (const [name, value] of Object.entries(setDefs)) {
|
|
995
1429
|
vars[name] = value;
|
|
996
1430
|
}
|
|
@@ -1001,16 +1435,17 @@ function buildVars(setDefs, context) {
|
|
|
1001
1435
|
}
|
|
1002
1436
|
function rollDefinitions(defDefs, vars, context, opts) {
|
|
1003
1437
|
const outranked = new Set(Object.keys(context).map((key) => key.toLowerCase()));
|
|
1004
|
-
const rolled =
|
|
1005
|
-
const
|
|
1438
|
+
const rolled = nameMap();
|
|
1439
|
+
const definitions = nameMap(defDefs);
|
|
1440
|
+
const aliases = nameMap();
|
|
1006
1441
|
for (const [name, value] of Object.entries(vars)) {
|
|
1007
|
-
if (name in
|
|
1442
|
+
if (name in definitions && !outranked.has(name)) continue;
|
|
1008
1443
|
aliases[name] = value;
|
|
1009
1444
|
}
|
|
1010
|
-
for (const name of orderDefinitions(
|
|
1445
|
+
for (const name of orderDefinitions(definitions, aliases)) {
|
|
1011
1446
|
if (outranked.has(name)) continue;
|
|
1012
|
-
const value =
|
|
1013
|
-
rolled[name] = renderNodes(parseSequence(value), { ...opts, vars:
|
|
1447
|
+
const value = definitions[name] ?? "";
|
|
1448
|
+
rolled[name] = renderNodes(parseSequence(value), { ...opts, vars: nameMap(vars, rolled) });
|
|
1014
1449
|
}
|
|
1015
1450
|
return rolled;
|
|
1016
1451
|
}
|
|
@@ -1050,6 +1485,9 @@ function directReferences(text) {
|
|
|
1050
1485
|
return [...text.matchAll(/%(\w+)%/gu)].map((match) => (match[1] ?? "").toLowerCase());
|
|
1051
1486
|
}
|
|
1052
1487
|
function renderNodes(nodes, opts) {
|
|
1488
|
+
return flatten(renderFragment(nodes, opts));
|
|
1489
|
+
}
|
|
1490
|
+
function renderFragment(nodes, opts) {
|
|
1053
1491
|
const frame = (list) => ({ nodes: list, i: 0, out: [], pending: null });
|
|
1054
1492
|
const stack = [frame(nodes)];
|
|
1055
1493
|
while (stack.length > 0) {
|
|
@@ -1064,7 +1502,7 @@ function renderNodes(nodes, opts) {
|
|
|
1064
1502
|
continue;
|
|
1065
1503
|
}
|
|
1066
1504
|
if (f.i >= f.nodes.length) {
|
|
1067
|
-
const text = f.out
|
|
1505
|
+
const text = joinFragments(f.out);
|
|
1068
1506
|
stack.pop();
|
|
1069
1507
|
const parent = stack[stack.length - 1];
|
|
1070
1508
|
if (parent === void 0) return text;
|
|
@@ -1074,11 +1512,12 @@ function renderNodes(nodes, opts) {
|
|
|
1074
1512
|
const node = f.nodes[f.i];
|
|
1075
1513
|
f.i += 1;
|
|
1076
1514
|
const step = renderNode(node, opts);
|
|
1077
|
-
if (typeof step === "string") f.out.push(step);
|
|
1515
|
+
if (typeof step === "string" || !("lists" in step)) f.out.push(step);
|
|
1078
1516
|
else f.pending = step;
|
|
1079
1517
|
}
|
|
1080
1518
|
return "";
|
|
1081
1519
|
}
|
|
1520
|
+
var single = (list) => ({ lists: [list], done: [], assemble: (parts) => parts[0] ?? "" });
|
|
1082
1521
|
function renderNode(node, opts) {
|
|
1083
1522
|
switch (node.type) {
|
|
1084
1523
|
case "literal":
|
|
@@ -1086,7 +1525,7 @@ function renderNode(node, opts) {
|
|
|
1086
1525
|
case "variable":
|
|
1087
1526
|
return resolveVariable(node.name, opts);
|
|
1088
1527
|
case "enumeration":
|
|
1089
|
-
return renderEnumeration(node
|
|
1528
|
+
return renderEnumeration(node, opts);
|
|
1090
1529
|
case "permutation":
|
|
1091
1530
|
return renderPermutation(node, opts);
|
|
1092
1531
|
case "conditional":
|
|
@@ -1100,15 +1539,20 @@ function randomInt(rng, min, max) {
|
|
|
1100
1539
|
}
|
|
1101
1540
|
function resolveVariable(name, opts) {
|
|
1102
1541
|
const value = opts.vars[name.toLowerCase()];
|
|
1103
|
-
if (value === void 0) return `%${name}%`;
|
|
1104
|
-
if (opts.depth >= MAX_VARIABLE_DEPTH || !/[{[%]/u.test(value)) return value;
|
|
1542
|
+
if (value === void 0 || opts.frozen === true) return `%${name}%`;
|
|
1105
1543
|
if (opts.budget.left <= 0) return `%${name}%`;
|
|
1106
1544
|
opts.budget.left -= value.length;
|
|
1107
|
-
|
|
1545
|
+
if (opts.depth >= MAX_VARIABLE_DEPTH || !/[{[%]/u.test(value)) return value;
|
|
1546
|
+
return renderFragment(parseSequence(value), { ...opts, depth: opts.depth + 1 });
|
|
1108
1547
|
}
|
|
1109
|
-
function
|
|
1548
|
+
function passesLeft(opts) {
|
|
1549
|
+
return Math.max(1, MAX_VARIABLE_DEPTH - opts.depth + 1);
|
|
1550
|
+
}
|
|
1551
|
+
function expandVarsFixpoint(text, opts, passes) {
|
|
1552
|
+
if (opts.frozen === true) return { text, changed: false, converged: true };
|
|
1110
1553
|
let out = text;
|
|
1111
|
-
|
|
1554
|
+
let changedAny = false;
|
|
1555
|
+
for (let i = 0; i < passes; i += 1) {
|
|
1112
1556
|
let changed = false;
|
|
1113
1557
|
out = out.replace(/%(\w+)%/gu, (m, name) => {
|
|
1114
1558
|
const value = opts.vars[name.toLowerCase()];
|
|
@@ -1118,22 +1562,34 @@ function expandVarsOnly(text, opts) {
|
|
|
1118
1562
|
changed = true;
|
|
1119
1563
|
return value;
|
|
1120
1564
|
});
|
|
1121
|
-
if (!changed)
|
|
1565
|
+
if (!changed) return { text: out, changed: changedAny, converged: true };
|
|
1566
|
+
changedAny = true;
|
|
1122
1567
|
}
|
|
1123
|
-
return out;
|
|
1568
|
+
return { text: out, changed: changedAny, converged: false };
|
|
1124
1569
|
}
|
|
1570
|
+
var NON_SPACE_RE = new RegExp(`[^${UCP_SPACE}]`, "u");
|
|
1571
|
+
var truthinessByVars = /* @__PURE__ */ new WeakMap();
|
|
1125
1572
|
function conditionalTakesThen(name, inverted, opts) {
|
|
1126
|
-
const
|
|
1127
|
-
|
|
1573
|
+
const key = name.toLowerCase();
|
|
1574
|
+
let known = truthinessByVars.get(opts.vars);
|
|
1575
|
+
if (known === void 0) truthinessByVars.set(opts.vars, known = /* @__PURE__ */ new Map());
|
|
1576
|
+
let baseTruthy = known.get(key);
|
|
1577
|
+
if (baseTruthy === void 0) {
|
|
1578
|
+
const value = opts.vars[key];
|
|
1579
|
+
baseTruthy = value !== void 0 && NON_SPACE_RE.test(value);
|
|
1580
|
+
known.set(key, baseTruthy);
|
|
1581
|
+
}
|
|
1128
1582
|
return inverted ? !baseTruthy : baseTruthy;
|
|
1129
1583
|
}
|
|
1130
1584
|
function renderConditional(node, opts) {
|
|
1131
|
-
|
|
1132
|
-
return { lists: [truthy ? node.then : node.else], done: [], assemble: (parts) => parts[0] ?? "" };
|
|
1585
|
+
return single(conditionalTakesThen(node.name, node.inverted, opts) ? node.then : node.else);
|
|
1133
1586
|
}
|
|
1134
|
-
function
|
|
1587
|
+
function resolveConditionalsInText(text, opts) {
|
|
1135
1588
|
if (!text.includes("{?")) return text;
|
|
1136
|
-
const close =
|
|
1589
|
+
const close = matchPairs(text, BRACE_OPEN, BRACE_CLOSE);
|
|
1590
|
+
const any = matchAnyPairs(text);
|
|
1591
|
+
const heads = [];
|
|
1592
|
+
for (let at = text.indexOf("{?"); at !== -1; at = text.indexOf("{?", at + 1)) heads.push(at);
|
|
1137
1593
|
const out = [];
|
|
1138
1594
|
const pending = [[0, text.length]];
|
|
1139
1595
|
while (pending.length > 0) {
|
|
@@ -1141,13 +1597,14 @@ function resolveCountConditionals(text, opts) {
|
|
|
1141
1597
|
const segEnd = segment[1];
|
|
1142
1598
|
let i = segment[0];
|
|
1143
1599
|
while (i < segEnd) {
|
|
1144
|
-
const
|
|
1600
|
+
const next = lowerBound(heads, i);
|
|
1601
|
+
const open = next < heads.length ? heads[next] : -1;
|
|
1145
1602
|
if (open === -1 || open + 1 >= segEnd) {
|
|
1146
1603
|
out.push(text.slice(i, segEnd));
|
|
1147
1604
|
break;
|
|
1148
1605
|
}
|
|
1149
1606
|
const shut = close[open] ?? -1;
|
|
1150
|
-
const head = shut === -1 || shut >= segEnd ? null : recognizeConditional(text, open + 1, shut);
|
|
1607
|
+
const head = shut === -1 || shut >= segEnd ? null : recognizeConditional(text, open + 1, shut, any);
|
|
1151
1608
|
if (head === null) {
|
|
1152
1609
|
out.push(text.slice(i, Math.min(open + 2, segEnd)));
|
|
1153
1610
|
i = open + 2;
|
|
@@ -1163,23 +1620,12 @@ function resolveCountConditionals(text, opts) {
|
|
|
1163
1620
|
}
|
|
1164
1621
|
return out.join("");
|
|
1165
1622
|
}
|
|
1166
|
-
function matchBraces(text) {
|
|
1167
|
-
const close = new Int32Array(text.length).fill(-1);
|
|
1168
|
-
const opens = [];
|
|
1169
|
-
for (let i = 0; i < text.length; i += 1) {
|
|
1170
|
-
const ch = text.charAt(i);
|
|
1171
|
-
if (ch === "{") {
|
|
1172
|
-
opens.push(i);
|
|
1173
|
-
} else if (ch === "}") {
|
|
1174
|
-
const open = opens.pop();
|
|
1175
|
-
if (open !== void 0) close[open] = i;
|
|
1176
|
-
}
|
|
1177
|
-
}
|
|
1178
|
-
return close;
|
|
1179
|
-
}
|
|
1180
1623
|
function renderPlural(node, opts) {
|
|
1181
|
-
const
|
|
1182
|
-
const
|
|
1624
|
+
const passes = passesLeft(opts);
|
|
1625
|
+
const countPass = expandVarsFixpoint(node.countRaw, opts, passes);
|
|
1626
|
+
const formsPass = expandVarsFixpoint(node.formsRaw, opts, passes);
|
|
1627
|
+
const countRaw = resolveConditionalsInText(countPass.text, opts);
|
|
1628
|
+
const formsRaw = formsPass.text;
|
|
1183
1629
|
const base = normalizeBaseLang(opts.locale);
|
|
1184
1630
|
const report = (issue) => opts.onPluralError?.(issue);
|
|
1185
1631
|
if (/[{}[\]]/u.test(formsRaw)) {
|
|
@@ -1191,7 +1637,7 @@ function renderPlural(node, opts) {
|
|
|
1191
1637
|
});
|
|
1192
1638
|
return fullwidthVerbatim(countRaw, formsRaw);
|
|
1193
1639
|
}
|
|
1194
|
-
const count =
|
|
1640
|
+
const count = phpTrim(countRaw);
|
|
1195
1641
|
if (!/^-?\d+$/u.test(count)) {
|
|
1196
1642
|
report({
|
|
1197
1643
|
code: "plural.count",
|
|
@@ -1201,7 +1647,7 @@ function renderPlural(node, opts) {
|
|
|
1201
1647
|
});
|
|
1202
1648
|
return "";
|
|
1203
1649
|
}
|
|
1204
|
-
const forms = formsRaw.split("|").map((f) =>
|
|
1650
|
+
const forms = formsRaw.split("|").map((f) => phpTrim(f));
|
|
1205
1651
|
if (forms.length !== pluralArity(base)) {
|
|
1206
1652
|
report({
|
|
1207
1653
|
code: "plural.arity",
|
|
@@ -1214,7 +1660,8 @@ function renderPlural(node, opts) {
|
|
|
1214
1660
|
return fullwidthVerbatim(countRaw, formsRaw);
|
|
1215
1661
|
}
|
|
1216
1662
|
const picked = pluralFor(base, Number.parseInt(count, 10), forms);
|
|
1217
|
-
|
|
1663
|
+
if (!formsPass.converged) return renderFragment(parseSequence(picked), { ...opts, frozen: true });
|
|
1664
|
+
return single(parseSequence(picked));
|
|
1218
1665
|
}
|
|
1219
1666
|
function rawConstruct(countRaw, formsRaw) {
|
|
1220
1667
|
return `{plural ${countRaw}:${formsRaw}}`;
|
|
@@ -1222,13 +1669,33 @@ function rawConstruct(countRaw, formsRaw) {
|
|
|
1222
1669
|
function fullwidthVerbatim(countRaw, formsRaw) {
|
|
1223
1670
|
return rawConstruct(countRaw, formsRaw).replace(/\{/gu, "\uFF5B").replace(/\}/gu, "\uFF5D");
|
|
1224
1671
|
}
|
|
1225
|
-
function renderEnumeration(
|
|
1672
|
+
function renderEnumeration(node, opts) {
|
|
1673
|
+
const spliced = node.raw === void 0 ? null : spliceConstruct(node, node.raw, "{", "}", opts);
|
|
1674
|
+
if (spliced !== null) return spliced;
|
|
1675
|
+
const { options } = node;
|
|
1226
1676
|
if (options.length === 0) return "";
|
|
1227
1677
|
const picked = options[randomInt(opts.rng, 0, options.length - 1)];
|
|
1228
1678
|
if (!picked) return "";
|
|
1229
|
-
return
|
|
1679
|
+
return single(picked);
|
|
1680
|
+
}
|
|
1681
|
+
function spliceConstruct(node, raw, open, close, opts) {
|
|
1682
|
+
if (opts.frozen === true) return null;
|
|
1683
|
+
const span2 = rereadSpans.get(node);
|
|
1684
|
+
if (span2 !== void 0 && !rereadCouldChange(span2, opts)) return null;
|
|
1685
|
+
const expanded = expandVarsFixpoint(resolveConditionalsInText(raw, opts), opts, passesLeft(opts));
|
|
1686
|
+
const body = resolveConditionalsInText(expanded.text, opts);
|
|
1687
|
+
if (body === raw) return null;
|
|
1688
|
+
const nodes = parseSequence(open + body + close);
|
|
1689
|
+
if (!expanded.converged) return renderFragment(nodes, { ...opts, frozen: true });
|
|
1690
|
+
return single(nodes);
|
|
1691
|
+
}
|
|
1692
|
+
function rereadCouldChange({ index, start, end }, opts) {
|
|
1693
|
+
if (index.hasConditionalWithin(start, end)) return true;
|
|
1694
|
+
return !(opts.budget.left <= 0) && index.hasDefinedReferenceWithin(start, end, opts.vars);
|
|
1230
1695
|
}
|
|
1231
1696
|
function renderPermutation(node, opts) {
|
|
1697
|
+
const spliced = node.raw === void 0 ? null : spliceConstruct(node, node.raw, "[", "]", opts);
|
|
1698
|
+
if (spliced !== null) return spliced;
|
|
1232
1699
|
if (node.options.length === 0) return "";
|
|
1233
1700
|
return {
|
|
1234
1701
|
lists: node.options.map((o) => o.nodes),
|
|
@@ -1237,7 +1704,11 @@ function renderPermutation(node, opts) {
|
|
|
1237
1704
|
};
|
|
1238
1705
|
}
|
|
1239
1706
|
function assemblePermutation(node, rendered, opts) {
|
|
1240
|
-
const elements =
|
|
1707
|
+
const elements = [];
|
|
1708
|
+
node.options.forEach((o, i) => {
|
|
1709
|
+
const text = trimFragment(rendered[i] ?? "");
|
|
1710
|
+
if (lengthOf(text) !== 0) elements.push({ text, sep: o.separator });
|
|
1711
|
+
});
|
|
1241
1712
|
const total = elements.length;
|
|
1242
1713
|
if (total === 0) return "";
|
|
1243
1714
|
const { config } = node;
|
|
@@ -1276,25 +1747,20 @@ function joinWithSeparators(elements, globalSep, globalLastsep) {
|
|
|
1276
1747
|
const count = elements.length;
|
|
1277
1748
|
if (count === 0) return "";
|
|
1278
1749
|
if (count === 1) return elements[0].text;
|
|
1279
|
-
|
|
1750
|
+
const pieces = [elements[0].text];
|
|
1280
1751
|
for (let i = 1; i < count; i += 1) {
|
|
1281
1752
|
const el = elements[i];
|
|
1282
1753
|
const sep = el.sep ?? (i === count - 1 ? globalLastsep : globalSep);
|
|
1283
|
-
|
|
1754
|
+
pieces.push(padSeparator(sep), el.text);
|
|
1284
1755
|
}
|
|
1285
|
-
return
|
|
1756
|
+
return joinFragments(pieces);
|
|
1286
1757
|
}
|
|
1287
1758
|
function padSeparator(sep) {
|
|
1288
|
-
const trimmed =
|
|
1759
|
+
const trimmed = phpTrim(sep);
|
|
1289
1760
|
if (trimmed === "") return sep;
|
|
1290
1761
|
if (/^\p{L}+$/u.test(trimmed)) return ` ${trimmed} `;
|
|
1291
1762
|
return sep;
|
|
1292
1763
|
}
|
|
1293
|
-
var PHP_LTRIM_RE2 = /^[ \t\n\r\0\x0B]+/u;
|
|
1294
|
-
var PHP_RTRIM_RE2 = /[ \t\n\r\0\x0B]+$/u;
|
|
1295
|
-
function phpTrim2(s) {
|
|
1296
|
-
return s.replace(PHP_LTRIM_RE2, "").replace(PHP_RTRIM_RE2, "");
|
|
1297
|
-
}
|
|
1298
1764
|
|
|
1299
1765
|
// src/internal/postprocess.ts
|
|
1300
1766
|
var SINGLE_ABBREVS = [
|
|
@@ -1347,41 +1813,179 @@ var SINGLE_ABBREVS = [
|
|
|
1347
1813
|
"Ave",
|
|
1348
1814
|
"Blvd"
|
|
1349
1815
|
];
|
|
1350
|
-
var WS =
|
|
1816
|
+
var WS = UCP_SPACE;
|
|
1351
1817
|
var S = `[${WS}]`;
|
|
1352
|
-
var
|
|
1818
|
+
var AFTER_NON_WORD = `(?<![${UCP_WORD}])`;
|
|
1819
|
+
var WORD_BOUNDARY = `(?:(?<=[${UCP_WORD}])(?![${UCP_WORD}])|(?<![${UCP_WORD}])(?=[${UCP_WORD}]))`;
|
|
1820
|
+
var LABEL = "(?:xn--)?[\\p{L}\\p{N}]+(?:-[\\p{L}\\p{N}]+)*";
|
|
1821
|
+
var DOMAIN_PART = `(?:${LABEL}\\.)+(?:xn--[a-z0-9\\-]{2,59}|[\\p{L}][\\p{L}\\p{N}-]{1,62})`;
|
|
1353
1822
|
var URI_BODY = `[^\\x00${WS}<>"')\\]]`;
|
|
1354
1823
|
var URI_RE = new RegExp(`(?:(?:https?|ftp):\\/\\/|(?:mailto|tel):)${URI_BODY}+`, "giu");
|
|
1355
1824
|
var MAILTEL_PREFIX_RE = /^(?:mailto|tel):/iu;
|
|
1356
|
-
var
|
|
1357
|
-
var
|
|
1825
|
+
var DOMAIN_AT_RE = new RegExp(`${DOMAIN_PART}${WORD_BOUNDARY}`, "iuy");
|
|
1826
|
+
var DOMAIN_SCAN_RE = new RegExp(`${AFTER_NON_WORD}(?:(${DOMAIN_PART}${WORD_BOUNDARY})|(?:${LABEL}\\.)*${LABEL})`, "giu");
|
|
1358
1827
|
var DECIMAL_RE = /\b\d+\.\d+\b/gu;
|
|
1359
|
-
var MULTI_ABBR_RE = new RegExp(
|
|
1828
|
+
var MULTI_ABBR_RE = new RegExp(`${AFTER_NON_WORD}(?:\\p{L}{1,2}\\.${S}*){2,}`, "gu");
|
|
1360
1829
|
var SINGLE_ABBR_RE = new RegExp(`(?<![\\p{L}\\p{N}])(?:${SINGLE_ABBREVS.join("|")})\\.(?=${S}|$|<)`, "giu");
|
|
1361
|
-
|
|
1830
|
+
function isEmailLocalChar(code) {
|
|
1831
|
+
return code >= 97 && code <= 122 || code >= 65 && code <= 90 || code >= 48 && code <= 57 || code === 46 || code === 95 || code === 37 || code === 43 || code === 45 || code === 383 || code === 8490;
|
|
1832
|
+
}
|
|
1833
|
+
function shieldEmails(text, shield) {
|
|
1834
|
+
let out = "";
|
|
1835
|
+
let emitted = 0;
|
|
1836
|
+
let pos = 0;
|
|
1837
|
+
for (let at = text.indexOf("@"); at !== -1; at = text.indexOf("@", Math.max(at + 1, pos))) {
|
|
1838
|
+
let start = at;
|
|
1839
|
+
while (start > pos && isEmailLocalChar(text.charCodeAt(start - 1))) start -= 1;
|
|
1840
|
+
if (start === at) continue;
|
|
1841
|
+
DOMAIN_AT_RE.lastIndex = at + 1;
|
|
1842
|
+
if (DOMAIN_AT_RE.exec(text) !== null) {
|
|
1843
|
+
out += text.slice(emitted, start) + shield(text.slice(start, DOMAIN_AT_RE.lastIndex));
|
|
1844
|
+
emitted = pos = DOMAIN_AT_RE.lastIndex;
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
return emitted === 0 ? text : out + text.slice(emitted);
|
|
1848
|
+
}
|
|
1849
|
+
var DOMAIN_DOT_RE = /\.[\p{L}\p{N}]/u;
|
|
1850
|
+
function shieldDomains(text, shield) {
|
|
1851
|
+
if (!DOMAIN_DOT_RE.test(text)) return text;
|
|
1852
|
+
let out = "";
|
|
1853
|
+
let emitted = 0;
|
|
1854
|
+
DOMAIN_SCAN_RE.lastIndex = 0;
|
|
1855
|
+
for (let m = DOMAIN_SCAN_RE.exec(text); m !== null; m = DOMAIN_SCAN_RE.exec(text)) {
|
|
1856
|
+
if (m[1] !== void 0) {
|
|
1857
|
+
out += text.slice(emitted, m.index) + shield(m[1]);
|
|
1858
|
+
emitted = m.index + m[1].length;
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
return emitted === 0 ? text : out + text.slice(emitted);
|
|
1862
|
+
}
|
|
1863
|
+
function trailingPunctuationStart(value) {
|
|
1864
|
+
let cut = value.length;
|
|
1865
|
+
while (cut > 0 && ".,;:!".includes(value.charAt(cut - 1))) cut -= 1;
|
|
1866
|
+
return cut;
|
|
1867
|
+
}
|
|
1362
1868
|
var SENTENCE_OPENERS = "\xBF\xA1";
|
|
1363
1869
|
var LEAD = `(?:<[^>]+>|[${SENTENCE_OPENERS}]|${S})*`;
|
|
1364
|
-
var SPACE_BEFORE_PUNCT_RE = new RegExp(
|
|
1365
|
-
var SPACE_AFTER_COMMA_RE = new RegExp(`([,;:])(?!\\
|
|
1366
|
-
var SPACE_AFTER_SENTENCE_RE = new RegExp(`([.!?]+)(?![.!?])(?!\\
|
|
1870
|
+
var SPACE_BEFORE_PUNCT_RE = new RegExp(`(?<!${S})${S}+([,;:!?.])`, "gu");
|
|
1871
|
+
var SPACE_AFTER_COMMA_RE = new RegExp(`([,;:])(?!\\p{Nd})(?!${S}|$|<)`, "gu");
|
|
1872
|
+
var SPACE_AFTER_SENTENCE_RE = new RegExp(`(?<![.!?])([.!?]+)(?![.!?])(?!\\p{Nd})(?!${S}|$|<)`, "gu");
|
|
1367
1873
|
var SPACE_AFTER_OPENER_RE = new RegExp(`([${SENTENCE_OPENERS}])${S}+`, "gu");
|
|
1368
1874
|
var CAP_FIRST_RE = new RegExp(`^(${LEAD})(\\p{Ll})`, "u");
|
|
1369
|
-
var CAP_AFTER_SENTENCE_RE = new RegExp(`([.!?\u2026])(${LEAD})(\\p{Ll})`, "gu");
|
|
1370
|
-
var CAP_AFTER_BLOCK_RE = new RegExp(
|
|
1371
|
-
`(<\\/?(?:p|h[1-6]|li|blockquote|div|td|th)[^>]*>${LEAD})(\\p{Ll})`,
|
|
1372
|
-
"giu"
|
|
1373
|
-
);
|
|
1374
|
-
var CAP_AFTER_BREAK_RE = new RegExp(`(\\n${LEAD})(\\p{Ll})`, "gu");
|
|
1375
1875
|
var up = (ch) => ch.toUpperCase();
|
|
1876
|
+
function indexLeads(text) {
|
|
1877
|
+
const n = text.length;
|
|
1878
|
+
const leadEnd = new Int32Array(n + 1);
|
|
1879
|
+
leadEnd[n] = n;
|
|
1880
|
+
let gt = -1;
|
|
1881
|
+
for (let i = n - 1; i >= 0; i -= 1) {
|
|
1882
|
+
const code = text.charCodeAt(i);
|
|
1883
|
+
let tokenEnd = -1;
|
|
1884
|
+
if (code === 191 || code === 161 || isUcpSpace(code)) {
|
|
1885
|
+
tokenEnd = i + 1;
|
|
1886
|
+
} else if (code === 60 && gt > i + 1) {
|
|
1887
|
+
tokenEnd = gt + 1;
|
|
1888
|
+
}
|
|
1889
|
+
leadEnd[i] = tokenEnd === -1 ? i : leadEnd[tokenEnd];
|
|
1890
|
+
if (code === 62) gt = i;
|
|
1891
|
+
}
|
|
1892
|
+
return { leadEnd };
|
|
1893
|
+
}
|
|
1894
|
+
var LOWER_RE = /^\p{Ll}/u;
|
|
1895
|
+
var LOWER_CASELESS_RE = /^\p{Ll}/iu;
|
|
1896
|
+
var BLOCK_TAG_NAME_RE = /<\/?(?:p|h[1-6]|li|blockquote|div|td|th)/iuy;
|
|
1897
|
+
var SENTENCE_END_SCAN_RE = new RegExp(`[.!?${String.fromCharCode(8230)}]`, "g");
|
|
1898
|
+
var LEAD_WALK = 32;
|
|
1899
|
+
function leadEndFrom(text, i, leads) {
|
|
1900
|
+
let j = i;
|
|
1901
|
+
for (let steps = 0; steps < LEAD_WALK && j < text.length; steps += 1) {
|
|
1902
|
+
const code = text.charCodeAt(j);
|
|
1903
|
+
if (code === 60) return leads().leadEnd[j];
|
|
1904
|
+
if (code !== 191 && code !== 161 && !isUcpSpace(code)) return j;
|
|
1905
|
+
j += 1;
|
|
1906
|
+
}
|
|
1907
|
+
return j < text.length ? leads().leadEnd[j] : j;
|
|
1908
|
+
}
|
|
1909
|
+
function capitalizeAfter(text, lower, next, context) {
|
|
1910
|
+
const leads = () => {
|
|
1911
|
+
if (context.leads === null || context.leads.leadEnd.length !== text.length + 1) context.leads = indexLeads(text);
|
|
1912
|
+
return context.leads;
|
|
1913
|
+
};
|
|
1914
|
+
let out = "";
|
|
1915
|
+
let emitted = 0;
|
|
1916
|
+
let from = 0;
|
|
1917
|
+
for (let boundary = next(text, from); boundary !== null; boundary = next(text, from)) {
|
|
1918
|
+
from = boundary.resume;
|
|
1919
|
+
const at = leadEndFrom(text, boundary.leadStart, leads);
|
|
1920
|
+
const cp = text.codePointAt(at);
|
|
1921
|
+
if (cp === void 0) continue;
|
|
1922
|
+
const ch = String.fromCodePoint(cp);
|
|
1923
|
+
if (!lower.test(ch)) continue;
|
|
1924
|
+
out += text.slice(emitted, at) + up(ch);
|
|
1925
|
+
emitted = at + ch.length;
|
|
1926
|
+
from = emitted;
|
|
1927
|
+
}
|
|
1928
|
+
return emitted === 0 ? text : out + text.slice(emitted);
|
|
1929
|
+
}
|
|
1930
|
+
var afterSentenceEnd = (text, from) => {
|
|
1931
|
+
SENTENCE_END_SCAN_RE.lastIndex = from;
|
|
1932
|
+
const m = SENTENCE_END_SCAN_RE.exec(text);
|
|
1933
|
+
return m === null ? null : { leadStart: m.index + 1, resume: m.index + 1 };
|
|
1934
|
+
};
|
|
1935
|
+
function afterBlockTag() {
|
|
1936
|
+
let gtFrom = -1;
|
|
1937
|
+
let gt = -1;
|
|
1938
|
+
return (text, from) => {
|
|
1939
|
+
for (let lt = text.indexOf("<", from); lt !== -1; lt = text.indexOf("<", lt + 1)) {
|
|
1940
|
+
BLOCK_TAG_NAME_RE.lastIndex = lt;
|
|
1941
|
+
if (!BLOCK_TAG_NAME_RE.test(text)) continue;
|
|
1942
|
+
const nameEnd = BLOCK_TAG_NAME_RE.lastIndex;
|
|
1943
|
+
if (gtFrom === -1 || nameEnd < gtFrom || gt !== -1 && nameEnd > gt) {
|
|
1944
|
+
gtFrom = nameEnd;
|
|
1945
|
+
gt = text.indexOf(">", nameEnd);
|
|
1946
|
+
}
|
|
1947
|
+
if (gt === -1) continue;
|
|
1948
|
+
return { leadStart: gt + 1, resume: lt + 1 };
|
|
1949
|
+
}
|
|
1950
|
+
return null;
|
|
1951
|
+
};
|
|
1952
|
+
}
|
|
1953
|
+
var afterLineBreak = (text, from) => {
|
|
1954
|
+
const at = text.indexOf("\n", from);
|
|
1955
|
+
return at === -1 ? null : { leadStart: at + 1, resume: at + 1 };
|
|
1956
|
+
};
|
|
1376
1957
|
var SHIELD_PREFIXES = ["URL", "URI", "EMAIL", "DOM", "NUM", "ABBR"];
|
|
1377
1958
|
var RESTORE_RE2 = new RegExp(`\\x00(?:${SHIELD_PREFIXES.join("|")})_\\d+\\x00`, "gu");
|
|
1378
1959
|
function restore(text, input, placeholders) {
|
|
1379
1960
|
if (!input.includes("\0")) {
|
|
1380
1961
|
return text.replace(RESTORE_RE2, (key) => placeholders.get(key) ?? key);
|
|
1381
1962
|
}
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1963
|
+
return restoreAsTheLoopDoes(text, placeholders);
|
|
1964
|
+
}
|
|
1965
|
+
function restoreAsTheLoopDoes(text, placeholders) {
|
|
1966
|
+
const gaps = text.split("\0");
|
|
1967
|
+
const rank = /* @__PURE__ */ new Map();
|
|
1968
|
+
for (const key of placeholders.keys()) rank.set(key, rank.size);
|
|
1969
|
+
const byRank = [];
|
|
1970
|
+
for (let j = 1; j < gaps.length - 1; j += 1) {
|
|
1971
|
+
const r = rank.get(`\0${gaps[j]}\0`);
|
|
1972
|
+
if (r !== void 0) (byRank[r] ??= []).push(j);
|
|
1973
|
+
}
|
|
1974
|
+
const taken = new Uint8Array(gaps.length);
|
|
1975
|
+
const replaced = new Uint8Array(gaps.length);
|
|
1976
|
+
for (const candidates of byRank) {
|
|
1977
|
+
for (const j of candidates ?? []) {
|
|
1978
|
+
if (taken[j - 1] === 1 || taken[j] === 1) continue;
|
|
1979
|
+
taken[j - 1] = 1;
|
|
1980
|
+
taken[j] = 1;
|
|
1981
|
+
replaced[j] = 1;
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
let out = gaps[0];
|
|
1985
|
+
for (let j = 1; j < gaps.length; j += 1) {
|
|
1986
|
+
if (taken[j - 1] !== 1) out += "\0";
|
|
1987
|
+
const gap = gaps[j];
|
|
1988
|
+
out += replaced[j] === 1 ? placeholders.get(`\0${gap}\0`) : gap;
|
|
1385
1989
|
}
|
|
1386
1990
|
return out;
|
|
1387
1991
|
}
|
|
@@ -1395,21 +1999,17 @@ function postProcess(input) {
|
|
|
1395
1999
|
return key;
|
|
1396
2000
|
};
|
|
1397
2001
|
const storeWithTrailingPunct = (value, prefix) => {
|
|
1398
|
-
const
|
|
1399
|
-
if (
|
|
1400
|
-
|
|
1401
|
-
const body = value.slice(0, value.length - suffix.length);
|
|
1402
|
-
return body === "" ? suffix : store(body, prefix) + suffix;
|
|
1403
|
-
}
|
|
1404
|
-
return store(value, prefix);
|
|
2002
|
+
const cut = trailingPunctuationStart(value);
|
|
2003
|
+
if (cut === value.length) return store(value, prefix);
|
|
2004
|
+
return cut === 0 ? value : store(value.slice(0, cut), prefix) + value.slice(cut);
|
|
1405
2005
|
};
|
|
1406
2006
|
let text = input;
|
|
1407
2007
|
text = text.replace(
|
|
1408
2008
|
URI_RE,
|
|
1409
2009
|
(m) => storeWithTrailingPunct(m, MAILTEL_PREFIX_RE.test(m) ? "URI" : "URL")
|
|
1410
2010
|
);
|
|
1411
|
-
text = text
|
|
1412
|
-
text = text
|
|
2011
|
+
text = shieldEmails(text, (m) => store(m, "EMAIL"));
|
|
2012
|
+
text = shieldDomains(text, (m) => store(m, "DOM"));
|
|
1413
2013
|
text = text.replace(DECIMAL_RE, (m) => store(m, "NUM"));
|
|
1414
2014
|
text = text.replace(MULTI_ABBR_RE, (m) => store(m, "ABBR"));
|
|
1415
2015
|
text = text.replace(SINGLE_ABBR_RE, (m) => store(m, "ABBR"));
|
|
@@ -1419,9 +2019,10 @@ function postProcess(input) {
|
|
|
1419
2019
|
text = text.replace(SPACE_AFTER_SENTENCE_RE, "$1 ");
|
|
1420
2020
|
text = text.replace(SPACE_AFTER_OPENER_RE, "$1");
|
|
1421
2021
|
text = text.replace(CAP_FIRST_RE, (_m, lead, ch) => lead + up(ch));
|
|
1422
|
-
|
|
1423
|
-
text = text
|
|
1424
|
-
text = text
|
|
2022
|
+
const leadContext = { leads: null };
|
|
2023
|
+
text = capitalizeAfter(text, LOWER_RE, afterSentenceEnd, leadContext);
|
|
2024
|
+
text = capitalizeAfter(text, LOWER_CASELESS_RE, afterBlockTag(), leadContext);
|
|
2025
|
+
text = capitalizeAfter(text, LOWER_RE, afterLineBreak, leadContext);
|
|
1425
2026
|
return restore(text, input, placeholders).trim();
|
|
1426
2027
|
}
|
|
1427
2028
|
|