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