@spintax/core 0.7.0 → 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 +361 -0
- package/README.md +7 -2
- package/dist/index.cjs +847 -282
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -5
- package/dist/index.d.ts +10 -5
- package/dist/index.js +847 -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,116 +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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
+
}
|
|
198
541
|
}
|
|
199
542
|
}
|
|
200
543
|
return false;
|
|
201
544
|
}
|
|
202
|
-
var
|
|
203
|
-
function planPermutation(
|
|
204
|
-
const {
|
|
205
|
-
const {
|
|
206
|
-
const
|
|
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));
|
|
207
552
|
return {
|
|
208
|
-
|
|
553
|
+
spans,
|
|
209
554
|
build: (children) => {
|
|
210
555
|
const options = children.map((nodes, i) => ({ nodes, separator: separators[i] ?? null }));
|
|
211
|
-
|
|
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;
|
|
212
560
|
}
|
|
213
561
|
};
|
|
214
562
|
}
|
|
215
|
-
function recognizeConditional(text, contentStart, contentEnd) {
|
|
563
|
+
function recognizeConditional(text, contentStart, contentEnd, anyPairs) {
|
|
216
564
|
let p = contentStart + 1;
|
|
217
565
|
let inverted = false;
|
|
218
566
|
if (text.charAt(p) === "!") {
|
|
219
567
|
inverted = true;
|
|
220
568
|
p += 1;
|
|
221
569
|
}
|
|
222
|
-
|
|
223
|
-
const name =
|
|
570
|
+
CONDITIONAL_NAME_RE2.lastIndex = p;
|
|
571
|
+
const name = CONDITIONAL_NAME_RE2.exec(text)?.[0];
|
|
224
572
|
if (name === void 0 || p + name.length > contentEnd) return null;
|
|
225
573
|
p += name.length;
|
|
226
574
|
if (text.charAt(p) !== "?") return null;
|
|
227
575
|
p += 1;
|
|
228
|
-
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
thenRaw: sep < 0 ? body : body.slice(0, sep),
|
|
239
|
-
elseRaw: sep < 0 ? "" : body.slice(sep + 1)
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
function parsePlural(afterPrefix) {
|
|
243
|
-
const colon = afterPrefix.indexOf(":");
|
|
244
|
-
return { type: "plural", countRaw: afterPrefix.slice(0, colon), formsRaw: afterPrefix.slice(colon + 1) };
|
|
245
|
-
}
|
|
246
|
-
var CONFIG_KEY_RE = /\b(?:minsize|maxsize|sep|lastsep)\s*=/i;
|
|
247
|
-
var MINSIZE_RE = /minsize\s*=\s*(\d+)/i;
|
|
248
|
-
var MAXSIZE_RE = /maxsize\s*=\s*(\d+)/i;
|
|
249
|
-
var SEP_RE = /(?<!last)sep\s*=\s*"([^"]*)"/i;
|
|
250
|
-
var LASTSEP_RE = /lastsep\s*=\s*"([^"]*)"/i;
|
|
251
|
-
var HTML_TAG_RE = /^([a-zA-Z][a-zA-Z0-9-]*)(?:\s+[^>]*)?\/?$/;
|
|
252
|
-
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]/;
|
|
253
586
|
function defaultPermConfig() {
|
|
254
587
|
return { minsize: null, maxsize: null, sep: " ", lastsep: null };
|
|
255
588
|
}
|
|
256
|
-
function extractPermutationConfig(
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
}
|
|
261
|
-
const
|
|
262
|
-
if (
|
|
263
|
-
const configStr =
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
return { config: defaultPermConfig(), content };
|
|
267
|
-
}
|
|
268
|
-
return { config: parseConfigString(configStr), content: remaining };
|
|
269
|
-
}
|
|
270
|
-
function findConfigEnd(text) {
|
|
271
|
-
let inQuote = false;
|
|
272
|
-
for (let i = 1; i < text.length; i += 1) {
|
|
273
|
-
const ch = text.charAt(i);
|
|
274
|
-
if (ch === '"') inQuote = !inQuote;
|
|
275
|
-
if (ch === ">" && !inQuote) return i;
|
|
276
|
-
}
|
|
277
|
-
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 };
|
|
278
599
|
}
|
|
279
600
|
function parseConfigString(str) {
|
|
280
601
|
if (!CONFIG_KEY_RE.test(str)) {
|
|
@@ -287,58 +608,54 @@ function parseConfigString(str) {
|
|
|
287
608
|
lastsep: strGroup(LASTSEP_RE.exec(str))
|
|
288
609
|
};
|
|
289
610
|
}
|
|
290
|
-
function looksLikeHtmlStartTag(tagText,
|
|
611
|
+
function looksLikeHtmlStartTag(index, tagText, from, end) {
|
|
291
612
|
const trimmed = phpTrim(tagText);
|
|
292
613
|
if (trimmed === "") return false;
|
|
293
614
|
const m = HTML_TAG_RE.exec(trimmed);
|
|
294
615
|
if (!m) return false;
|
|
295
616
|
if (trimmed.endsWith("/")) return true;
|
|
296
|
-
|
|
297
|
-
return new RegExp(`</${escapeRegExp(tagName)}\\s*>`, "iu").test(remaining);
|
|
617
|
+
return index.hasClosingTag((m[1] ?? "").toLowerCase(), from, end);
|
|
298
618
|
}
|
|
299
|
-
function permutationElements(
|
|
300
|
-
const
|
|
619
|
+
function permutationElements(index, parts) {
|
|
620
|
+
const { text } = index;
|
|
621
|
+
const spans = [];
|
|
301
622
|
const separators = [];
|
|
302
623
|
let pendingSep = null;
|
|
303
|
-
|
|
304
|
-
let
|
|
624
|
+
parts.forEach(([partStart, partEnd], i) => {
|
|
625
|
+
let textEnd = partEnd;
|
|
305
626
|
let trailingSep = null;
|
|
306
|
-
if (i <
|
|
307
|
-
const extracted = extractTrailingSep(
|
|
627
|
+
if (i < parts.length - 1) {
|
|
628
|
+
const extracted = extractTrailingSep(index, partStart, partEnd);
|
|
308
629
|
if (extracted) {
|
|
309
|
-
|
|
630
|
+
textEnd = extracted.textEnd;
|
|
310
631
|
trailingSep = extracted.sep;
|
|
311
632
|
}
|
|
312
633
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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]);
|
|
316
640
|
separators.push(pendingSep);
|
|
317
641
|
}
|
|
318
642
|
pendingSep = trailingSep;
|
|
319
643
|
});
|
|
320
|
-
return {
|
|
321
|
-
}
|
|
322
|
-
function extractTrailingSep(
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
openPos = i;
|
|
331
|
-
break;
|
|
332
|
-
}
|
|
333
|
-
if (ch === ">") return null;
|
|
334
|
-
}
|
|
335
|
-
if (openPos === -1) return null;
|
|
336
|
-
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);
|
|
337
654
|
const innerTrimmed = phpTrim(inner);
|
|
338
655
|
if (innerTrimmed.startsWith("/") || innerTrimmed.endsWith("/") || PER_ELEM_HTML_RE.test(innerTrimmed)) {
|
|
339
656
|
return null;
|
|
340
657
|
}
|
|
341
|
-
return {
|
|
658
|
+
return { textEnd: openPos, sep: inner };
|
|
342
659
|
}
|
|
343
660
|
function intGroup(m) {
|
|
344
661
|
return m && m[1] !== void 0 ? Number.parseInt(m[1], 10) : null;
|
|
@@ -346,52 +663,21 @@ function intGroup(m) {
|
|
|
346
663
|
function strGroup(m) {
|
|
347
664
|
return m && m[1] !== void 0 ? m[1] : null;
|
|
348
665
|
}
|
|
349
|
-
function
|
|
350
|
-
return
|
|
666
|
+
function isPhpTrimChar(code) {
|
|
667
|
+
return code === 32 || code === 9 || code === 10 || code === 13 || code === 0 || code === 11;
|
|
351
668
|
}
|
|
352
|
-
var PHP_LTRIM_RE = /^[ \t\n\r\0\x0B]+/u;
|
|
353
|
-
var PHP_RTRIM_RE = /[ \t\n\r\0\x0B]+$/u;
|
|
354
669
|
function phpTrim(s) {
|
|
355
|
-
return
|
|
670
|
+
return phpRtrim(phpLtrim(s));
|
|
356
671
|
}
|
|
357
672
|
function phpLtrim(s) {
|
|
358
|
-
|
|
673
|
+
let start = 0;
|
|
674
|
+
while (start < s.length && isPhpTrimChar(s.charCodeAt(start))) start += 1;
|
|
675
|
+
return start === 0 ? s : s.slice(start);
|
|
359
676
|
}
|
|
360
677
|
function phpRtrim(s) {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
let depth = 0;
|
|
365
|
-
for (let i = openPos; i < text.length; i += 1) {
|
|
366
|
-
const ch = text.charAt(i);
|
|
367
|
-
if (ch === open) {
|
|
368
|
-
depth += 1;
|
|
369
|
-
} else if (ch === close) {
|
|
370
|
-
depth -= 1;
|
|
371
|
-
if (depth === 0) return i;
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
return -1;
|
|
375
|
-
}
|
|
376
|
-
function splitTopLevel(inner) {
|
|
377
|
-
const parts = [];
|
|
378
|
-
let brace = 0;
|
|
379
|
-
let bracket = 0;
|
|
380
|
-
let cur = "";
|
|
381
|
-
for (const ch of inner) {
|
|
382
|
-
if (ch === "{") brace += 1;
|
|
383
|
-
else if (ch === "}") brace -= 1;
|
|
384
|
-
else if (ch === "[") bracket += 1;
|
|
385
|
-
else if (ch === "]") bracket -= 1;
|
|
386
|
-
if (ch === "|" && brace === 0 && bracket === 0) {
|
|
387
|
-
parts.push(cur);
|
|
388
|
-
cur = "";
|
|
389
|
-
} else {
|
|
390
|
-
cur += ch;
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
parts.push(cur);
|
|
394
|
-
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);
|
|
395
681
|
}
|
|
396
682
|
function firstTopLevelPipe(body, from = 0, to = body.length) {
|
|
397
683
|
let depth = 0;
|
|
@@ -407,27 +693,35 @@ function firstTopLevelPipe(body, from = 0, to = body.length) {
|
|
|
407
693
|
}
|
|
408
694
|
return -1;
|
|
409
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
|
+
}
|
|
410
712
|
|
|
411
713
|
// src/internal/plurals.ts
|
|
412
714
|
var PLURAL_PREFIX2 = "{plural ";
|
|
413
715
|
function findPluralBlocks(text) {
|
|
414
716
|
const blocks = [];
|
|
717
|
+
let close = null;
|
|
415
718
|
let i = 0;
|
|
416
719
|
while (i < text.length) {
|
|
417
720
|
const start = text.indexOf(PLURAL_PREFIX2, i);
|
|
418
721
|
if (start === -1) break;
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
const ch = text.charAt(j);
|
|
423
|
-
if (ch === "{") depth += 1;
|
|
424
|
-
else if (ch === "}") {
|
|
425
|
-
depth -= 1;
|
|
426
|
-
if (depth === 0) break;
|
|
427
|
-
}
|
|
428
|
-
j += 1;
|
|
429
|
-
}
|
|
430
|
-
if (depth !== 0) {
|
|
722
|
+
close ??= matchPairs(text, BRACE_OPEN, BRACE_CLOSE);
|
|
723
|
+
const j = close[start];
|
|
724
|
+
if (j === -1) {
|
|
431
725
|
i = start + PLURAL_PREFIX2.length;
|
|
432
726
|
continue;
|
|
433
727
|
}
|
|
@@ -590,12 +884,19 @@ function checkDirectives(text, out) {
|
|
|
590
884
|
}
|
|
591
885
|
}
|
|
592
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;
|
|
593
889
|
function checkPermutationConfigs(text, idx, out) {
|
|
594
|
-
for (
|
|
595
|
-
const
|
|
596
|
-
if (
|
|
597
|
-
const configBase =
|
|
598
|
-
|
|
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)) {
|
|
599
900
|
const key = (km[1] ?? "").toLowerCase();
|
|
600
901
|
if (!KNOWN_CONFIG_KEYS.has(key)) {
|
|
601
902
|
out.push(err(
|
|
@@ -605,7 +906,7 @@ function checkPermutationConfigs(text, idx, out) {
|
|
|
605
906
|
));
|
|
606
907
|
}
|
|
607
908
|
}
|
|
608
|
-
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);
|
|
609
910
|
if (min && !/^\d+$/.test(min[1] ?? "")) {
|
|
610
911
|
out.push(err(
|
|
611
912
|
"permutation.minsize-not-integer",
|
|
@@ -613,7 +914,7 @@ function checkPermutationConfigs(text, idx, out) {
|
|
|
613
914
|
{ ...span(idx, configBase + min.index, min[0].length), data: { value: min[1] } }
|
|
614
915
|
));
|
|
615
916
|
}
|
|
616
|
-
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);
|
|
617
918
|
if (max && !/^\d+$/.test(max[1] ?? "")) {
|
|
618
919
|
out.push(err(
|
|
619
920
|
"permutation.maxsize-not-integer",
|
|
@@ -936,6 +1237,13 @@ function addAll(target, text, re, fold) {
|
|
|
936
1237
|
}
|
|
937
1238
|
}
|
|
938
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
|
+
|
|
939
1247
|
// src/internal/errors.ts
|
|
940
1248
|
var SpintaxError = class extends Error {
|
|
941
1249
|
constructor(message) {
|
|
@@ -965,6 +1273,120 @@ var NotImplementedError = class extends SpintaxError {
|
|
|
965
1273
|
}
|
|
966
1274
|
};
|
|
967
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
|
+
|
|
968
1390
|
// src/internal/render.ts
|
|
969
1391
|
var MAX_VARIABLE_DEPTH = 50;
|
|
970
1392
|
var MAX_EXPANSION_CHARS = 1024 * 1024;
|
|
@@ -978,7 +1400,7 @@ function renderAst(ast, ctx) {
|
|
|
978
1400
|
onPluralError: ctx.onPluralError,
|
|
979
1401
|
budget: ctx.budget
|
|
980
1402
|
};
|
|
981
|
-
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;
|
|
982
1404
|
const text = renderNodes(ast.nodes, { ...walkOpts, vars });
|
|
983
1405
|
return ctx.resolver ? resolveIncludes(text, ctx) : text;
|
|
984
1406
|
}
|
|
@@ -1002,7 +1424,7 @@ function resolveIncludes(text, ctx) {
|
|
|
1002
1424
|
});
|
|
1003
1425
|
}
|
|
1004
1426
|
function buildVars(setDefs, context) {
|
|
1005
|
-
const vars =
|
|
1427
|
+
const vars = nameMap();
|
|
1006
1428
|
for (const [name, value] of Object.entries(setDefs)) {
|
|
1007
1429
|
vars[name] = value;
|
|
1008
1430
|
}
|
|
@@ -1013,16 +1435,17 @@ function buildVars(setDefs, context) {
|
|
|
1013
1435
|
}
|
|
1014
1436
|
function rollDefinitions(defDefs, vars, context, opts) {
|
|
1015
1437
|
const outranked = new Set(Object.keys(context).map((key) => key.toLowerCase()));
|
|
1016
|
-
const rolled =
|
|
1017
|
-
const
|
|
1438
|
+
const rolled = nameMap();
|
|
1439
|
+
const definitions = nameMap(defDefs);
|
|
1440
|
+
const aliases = nameMap();
|
|
1018
1441
|
for (const [name, value] of Object.entries(vars)) {
|
|
1019
|
-
if (name in
|
|
1442
|
+
if (name in definitions && !outranked.has(name)) continue;
|
|
1020
1443
|
aliases[name] = value;
|
|
1021
1444
|
}
|
|
1022
|
-
for (const name of orderDefinitions(
|
|
1445
|
+
for (const name of orderDefinitions(definitions, aliases)) {
|
|
1023
1446
|
if (outranked.has(name)) continue;
|
|
1024
|
-
const value =
|
|
1025
|
-
rolled[name] = renderNodes(parseSequence(value), { ...opts, vars:
|
|
1447
|
+
const value = definitions[name] ?? "";
|
|
1448
|
+
rolled[name] = renderNodes(parseSequence(value), { ...opts, vars: nameMap(vars, rolled) });
|
|
1026
1449
|
}
|
|
1027
1450
|
return rolled;
|
|
1028
1451
|
}
|
|
@@ -1062,6 +1485,9 @@ function directReferences(text) {
|
|
|
1062
1485
|
return [...text.matchAll(/%(\w+)%/gu)].map((match) => (match[1] ?? "").toLowerCase());
|
|
1063
1486
|
}
|
|
1064
1487
|
function renderNodes(nodes, opts) {
|
|
1488
|
+
return flatten(renderFragment(nodes, opts));
|
|
1489
|
+
}
|
|
1490
|
+
function renderFragment(nodes, opts) {
|
|
1065
1491
|
const frame = (list) => ({ nodes: list, i: 0, out: [], pending: null });
|
|
1066
1492
|
const stack = [frame(nodes)];
|
|
1067
1493
|
while (stack.length > 0) {
|
|
@@ -1076,7 +1502,7 @@ function renderNodes(nodes, opts) {
|
|
|
1076
1502
|
continue;
|
|
1077
1503
|
}
|
|
1078
1504
|
if (f.i >= f.nodes.length) {
|
|
1079
|
-
const text = f.out
|
|
1505
|
+
const text = joinFragments(f.out);
|
|
1080
1506
|
stack.pop();
|
|
1081
1507
|
const parent = stack[stack.length - 1];
|
|
1082
1508
|
if (parent === void 0) return text;
|
|
@@ -1086,11 +1512,12 @@ function renderNodes(nodes, opts) {
|
|
|
1086
1512
|
const node = f.nodes[f.i];
|
|
1087
1513
|
f.i += 1;
|
|
1088
1514
|
const step = renderNode(node, opts);
|
|
1089
|
-
if (typeof step === "string") f.out.push(step);
|
|
1515
|
+
if (typeof step === "string" || !("lists" in step)) f.out.push(step);
|
|
1090
1516
|
else f.pending = step;
|
|
1091
1517
|
}
|
|
1092
1518
|
return "";
|
|
1093
1519
|
}
|
|
1520
|
+
var single = (list) => ({ lists: [list], done: [], assemble: (parts) => parts[0] ?? "" });
|
|
1094
1521
|
function renderNode(node, opts) {
|
|
1095
1522
|
switch (node.type) {
|
|
1096
1523
|
case "literal":
|
|
@@ -1116,7 +1543,7 @@ function resolveVariable(name, opts) {
|
|
|
1116
1543
|
if (opts.budget.left <= 0) return `%${name}%`;
|
|
1117
1544
|
opts.budget.left -= value.length;
|
|
1118
1545
|
if (opts.depth >= MAX_VARIABLE_DEPTH || !/[{[%]/u.test(value)) return value;
|
|
1119
|
-
return
|
|
1546
|
+
return renderFragment(parseSequence(value), { ...opts, depth: opts.depth + 1 });
|
|
1120
1547
|
}
|
|
1121
1548
|
function passesLeft(opts) {
|
|
1122
1549
|
return Math.max(1, MAX_VARIABLE_DEPTH - opts.depth + 1);
|
|
@@ -1140,18 +1567,29 @@ function expandVarsFixpoint(text, opts, passes) {
|
|
|
1140
1567
|
}
|
|
1141
1568
|
return { text: out, changed: changedAny, converged: false };
|
|
1142
1569
|
}
|
|
1570
|
+
var NON_SPACE_RE = new RegExp(`[^${UCP_SPACE}]`, "u");
|
|
1571
|
+
var truthinessByVars = /* @__PURE__ */ new WeakMap();
|
|
1143
1572
|
function conditionalTakesThen(name, inverted, opts) {
|
|
1144
|
-
const
|
|
1145
|
-
|
|
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
|
+
}
|
|
1146
1582
|
return inverted ? !baseTruthy : baseTruthy;
|
|
1147
1583
|
}
|
|
1148
1584
|
function renderConditional(node, opts) {
|
|
1149
|
-
|
|
1150
|
-
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);
|
|
1151
1586
|
}
|
|
1152
1587
|
function resolveConditionalsInText(text, opts) {
|
|
1153
1588
|
if (!text.includes("{?")) return text;
|
|
1154
|
-
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);
|
|
1155
1593
|
const out = [];
|
|
1156
1594
|
const pending = [[0, text.length]];
|
|
1157
1595
|
while (pending.length > 0) {
|
|
@@ -1159,13 +1597,14 @@ function resolveConditionalsInText(text, opts) {
|
|
|
1159
1597
|
const segEnd = segment[1];
|
|
1160
1598
|
let i = segment[0];
|
|
1161
1599
|
while (i < segEnd) {
|
|
1162
|
-
const
|
|
1600
|
+
const next = lowerBound(heads, i);
|
|
1601
|
+
const open = next < heads.length ? heads[next] : -1;
|
|
1163
1602
|
if (open === -1 || open + 1 >= segEnd) {
|
|
1164
1603
|
out.push(text.slice(i, segEnd));
|
|
1165
1604
|
break;
|
|
1166
1605
|
}
|
|
1167
1606
|
const shut = close[open] ?? -1;
|
|
1168
|
-
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);
|
|
1169
1608
|
if (head === null) {
|
|
1170
1609
|
out.push(text.slice(i, Math.min(open + 2, segEnd)));
|
|
1171
1610
|
i = open + 2;
|
|
@@ -1181,20 +1620,6 @@ function resolveConditionalsInText(text, opts) {
|
|
|
1181
1620
|
}
|
|
1182
1621
|
return out.join("");
|
|
1183
1622
|
}
|
|
1184
|
-
function matchBraces(text) {
|
|
1185
|
-
const close = new Int32Array(text.length).fill(-1);
|
|
1186
|
-
const opens = [];
|
|
1187
|
-
for (let i = 0; i < text.length; i += 1) {
|
|
1188
|
-
const ch = text.charAt(i);
|
|
1189
|
-
if (ch === "{") {
|
|
1190
|
-
opens.push(i);
|
|
1191
|
-
} else if (ch === "}") {
|
|
1192
|
-
const open = opens.pop();
|
|
1193
|
-
if (open !== void 0) close[open] = i;
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
1196
|
-
return close;
|
|
1197
|
-
}
|
|
1198
1623
|
function renderPlural(node, opts) {
|
|
1199
1624
|
const passes = passesLeft(opts);
|
|
1200
1625
|
const countPass = expandVarsFixpoint(node.countRaw, opts, passes);
|
|
@@ -1212,7 +1637,7 @@ function renderPlural(node, opts) {
|
|
|
1212
1637
|
});
|
|
1213
1638
|
return fullwidthVerbatim(countRaw, formsRaw);
|
|
1214
1639
|
}
|
|
1215
|
-
const count =
|
|
1640
|
+
const count = phpTrim(countRaw);
|
|
1216
1641
|
if (!/^-?\d+$/u.test(count)) {
|
|
1217
1642
|
report({
|
|
1218
1643
|
code: "plural.count",
|
|
@@ -1222,7 +1647,7 @@ function renderPlural(node, opts) {
|
|
|
1222
1647
|
});
|
|
1223
1648
|
return "";
|
|
1224
1649
|
}
|
|
1225
|
-
const forms = formsRaw.split("|").map((f) =>
|
|
1650
|
+
const forms = formsRaw.split("|").map((f) => phpTrim(f));
|
|
1226
1651
|
if (forms.length !== pluralArity(base)) {
|
|
1227
1652
|
report({
|
|
1228
1653
|
code: "plural.arity",
|
|
@@ -1235,8 +1660,8 @@ function renderPlural(node, opts) {
|
|
|
1235
1660
|
return fullwidthVerbatim(countRaw, formsRaw);
|
|
1236
1661
|
}
|
|
1237
1662
|
const picked = pluralFor(base, Number.parseInt(count, 10), forms);
|
|
1238
|
-
if (!formsPass.converged) return
|
|
1239
|
-
return
|
|
1663
|
+
if (!formsPass.converged) return renderFragment(parseSequence(picked), { ...opts, frozen: true });
|
|
1664
|
+
return single(parseSequence(picked));
|
|
1240
1665
|
}
|
|
1241
1666
|
function rawConstruct(countRaw, formsRaw) {
|
|
1242
1667
|
return `{plural ${countRaw}:${formsRaw}}`;
|
|
@@ -1245,25 +1670,31 @@ function fullwidthVerbatim(countRaw, formsRaw) {
|
|
|
1245
1670
|
return rawConstruct(countRaw, formsRaw).replace(/\{/gu, "\uFF5B").replace(/\}/gu, "\uFF5D");
|
|
1246
1671
|
}
|
|
1247
1672
|
function renderEnumeration(node, opts) {
|
|
1248
|
-
const spliced = node.raw === void 0 ? null : spliceConstruct(node.raw, "{", "}", opts);
|
|
1673
|
+
const spliced = node.raw === void 0 ? null : spliceConstruct(node, node.raw, "{", "}", opts);
|
|
1249
1674
|
if (spliced !== null) return spliced;
|
|
1250
1675
|
const { options } = node;
|
|
1251
1676
|
if (options.length === 0) return "";
|
|
1252
1677
|
const picked = options[randomInt(opts.rng, 0, options.length - 1)];
|
|
1253
1678
|
if (!picked) return "";
|
|
1254
|
-
return
|
|
1679
|
+
return single(picked);
|
|
1255
1680
|
}
|
|
1256
|
-
function spliceConstruct(raw, open, close, opts) {
|
|
1681
|
+
function spliceConstruct(node, raw, open, close, opts) {
|
|
1257
1682
|
if (opts.frozen === true) return null;
|
|
1683
|
+
const span2 = rereadSpans.get(node);
|
|
1684
|
+
if (span2 !== void 0 && !rereadCouldChange(span2, opts)) return null;
|
|
1258
1685
|
const expanded = expandVarsFixpoint(resolveConditionalsInText(raw, opts), opts, passesLeft(opts));
|
|
1259
1686
|
const body = resolveConditionalsInText(expanded.text, opts);
|
|
1260
1687
|
if (body === raw) return null;
|
|
1261
1688
|
const nodes = parseSequence(open + body + close);
|
|
1262
|
-
if (!expanded.converged) return
|
|
1263
|
-
return
|
|
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);
|
|
1264
1695
|
}
|
|
1265
1696
|
function renderPermutation(node, opts) {
|
|
1266
|
-
const spliced = node.raw === void 0 ? null : spliceConstruct(node.raw, "[", "]", opts);
|
|
1697
|
+
const spliced = node.raw === void 0 ? null : spliceConstruct(node, node.raw, "[", "]", opts);
|
|
1267
1698
|
if (spliced !== null) return spliced;
|
|
1268
1699
|
if (node.options.length === 0) return "";
|
|
1269
1700
|
return {
|
|
@@ -1273,7 +1704,11 @@ function renderPermutation(node, opts) {
|
|
|
1273
1704
|
};
|
|
1274
1705
|
}
|
|
1275
1706
|
function assemblePermutation(node, rendered, opts) {
|
|
1276
|
-
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
|
+
});
|
|
1277
1712
|
const total = elements.length;
|
|
1278
1713
|
if (total === 0) return "";
|
|
1279
1714
|
const { config } = node;
|
|
@@ -1312,25 +1747,20 @@ function joinWithSeparators(elements, globalSep, globalLastsep) {
|
|
|
1312
1747
|
const count = elements.length;
|
|
1313
1748
|
if (count === 0) return "";
|
|
1314
1749
|
if (count === 1) return elements[0].text;
|
|
1315
|
-
|
|
1750
|
+
const pieces = [elements[0].text];
|
|
1316
1751
|
for (let i = 1; i < count; i += 1) {
|
|
1317
1752
|
const el = elements[i];
|
|
1318
1753
|
const sep = el.sep ?? (i === count - 1 ? globalLastsep : globalSep);
|
|
1319
|
-
|
|
1754
|
+
pieces.push(padSeparator(sep), el.text);
|
|
1320
1755
|
}
|
|
1321
|
-
return
|
|
1756
|
+
return joinFragments(pieces);
|
|
1322
1757
|
}
|
|
1323
1758
|
function padSeparator(sep) {
|
|
1324
|
-
const trimmed =
|
|
1759
|
+
const trimmed = phpTrim(sep);
|
|
1325
1760
|
if (trimmed === "") return sep;
|
|
1326
1761
|
if (/^\p{L}+$/u.test(trimmed)) return ` ${trimmed} `;
|
|
1327
1762
|
return sep;
|
|
1328
1763
|
}
|
|
1329
|
-
var PHP_LTRIM_RE2 = /^[ \t\n\r\0\x0B]+/u;
|
|
1330
|
-
var PHP_RTRIM_RE2 = /[ \t\n\r\0\x0B]+$/u;
|
|
1331
|
-
function phpTrim2(s) {
|
|
1332
|
-
return s.replace(PHP_LTRIM_RE2, "").replace(PHP_RTRIM_RE2, "");
|
|
1333
|
-
}
|
|
1334
1764
|
|
|
1335
1765
|
// src/internal/postprocess.ts
|
|
1336
1766
|
var SINGLE_ABBREVS = [
|
|
@@ -1383,41 +1813,179 @@ var SINGLE_ABBREVS = [
|
|
|
1383
1813
|
"Ave",
|
|
1384
1814
|
"Blvd"
|
|
1385
1815
|
];
|
|
1386
|
-
var WS =
|
|
1816
|
+
var WS = UCP_SPACE;
|
|
1387
1817
|
var S = `[${WS}]`;
|
|
1388
|
-
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})`;
|
|
1389
1822
|
var URI_BODY = `[^\\x00${WS}<>"')\\]]`;
|
|
1390
1823
|
var URI_RE = new RegExp(`(?:(?:https?|ftp):\\/\\/|(?:mailto|tel):)${URI_BODY}+`, "giu");
|
|
1391
1824
|
var MAILTEL_PREFIX_RE = /^(?:mailto|tel):/iu;
|
|
1392
|
-
var
|
|
1393
|
-
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");
|
|
1394
1827
|
var DECIMAL_RE = /\b\d+\.\d+\b/gu;
|
|
1395
|
-
var MULTI_ABBR_RE = new RegExp(
|
|
1828
|
+
var MULTI_ABBR_RE = new RegExp(`${AFTER_NON_WORD}(?:\\p{L}{1,2}\\.${S}*){2,}`, "gu");
|
|
1396
1829
|
var SINGLE_ABBR_RE = new RegExp(`(?<![\\p{L}\\p{N}])(?:${SINGLE_ABBREVS.join("|")})\\.(?=${S}|$|<)`, "giu");
|
|
1397
|
-
|
|
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
|
+
}
|
|
1398
1868
|
var SENTENCE_OPENERS = "\xBF\xA1";
|
|
1399
1869
|
var LEAD = `(?:<[^>]+>|[${SENTENCE_OPENERS}]|${S})*`;
|
|
1400
|
-
var SPACE_BEFORE_PUNCT_RE = new RegExp(
|
|
1401
|
-
var SPACE_AFTER_COMMA_RE = new RegExp(`([,;:])(?!\\
|
|
1402
|
-
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");
|
|
1403
1873
|
var SPACE_AFTER_OPENER_RE = new RegExp(`([${SENTENCE_OPENERS}])${S}+`, "gu");
|
|
1404
1874
|
var CAP_FIRST_RE = new RegExp(`^(${LEAD})(\\p{Ll})`, "u");
|
|
1405
|
-
var CAP_AFTER_SENTENCE_RE = new RegExp(`([.!?\u2026])(${LEAD})(\\p{Ll})`, "gu");
|
|
1406
|
-
var CAP_AFTER_BLOCK_RE = new RegExp(
|
|
1407
|
-
`(<\\/?(?:p|h[1-6]|li|blockquote|div|td|th)[^>]*>${LEAD})(\\p{Ll})`,
|
|
1408
|
-
"giu"
|
|
1409
|
-
);
|
|
1410
|
-
var CAP_AFTER_BREAK_RE = new RegExp(`(\\n${LEAD})(\\p{Ll})`, "gu");
|
|
1411
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
|
+
};
|
|
1412
1957
|
var SHIELD_PREFIXES = ["URL", "URI", "EMAIL", "DOM", "NUM", "ABBR"];
|
|
1413
1958
|
var RESTORE_RE2 = new RegExp(`\\x00(?:${SHIELD_PREFIXES.join("|")})_\\d+\\x00`, "gu");
|
|
1414
1959
|
function restore(text, input, placeholders) {
|
|
1415
1960
|
if (!input.includes("\0")) {
|
|
1416
1961
|
return text.replace(RESTORE_RE2, (key) => placeholders.get(key) ?? key);
|
|
1417
1962
|
}
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
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;
|
|
1421
1989
|
}
|
|
1422
1990
|
return out;
|
|
1423
1991
|
}
|
|
@@ -1431,21 +1999,17 @@ function postProcess(input) {
|
|
|
1431
1999
|
return key;
|
|
1432
2000
|
};
|
|
1433
2001
|
const storeWithTrailingPunct = (value, prefix) => {
|
|
1434
|
-
const
|
|
1435
|
-
if (
|
|
1436
|
-
|
|
1437
|
-
const body = value.slice(0, value.length - suffix.length);
|
|
1438
|
-
return body === "" ? suffix : store(body, prefix) + suffix;
|
|
1439
|
-
}
|
|
1440
|
-
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);
|
|
1441
2005
|
};
|
|
1442
2006
|
let text = input;
|
|
1443
2007
|
text = text.replace(
|
|
1444
2008
|
URI_RE,
|
|
1445
2009
|
(m) => storeWithTrailingPunct(m, MAILTEL_PREFIX_RE.test(m) ? "URI" : "URL")
|
|
1446
2010
|
);
|
|
1447
|
-
text = text
|
|
1448
|
-
text = text
|
|
2011
|
+
text = shieldEmails(text, (m) => store(m, "EMAIL"));
|
|
2012
|
+
text = shieldDomains(text, (m) => store(m, "DOM"));
|
|
1449
2013
|
text = text.replace(DECIMAL_RE, (m) => store(m, "NUM"));
|
|
1450
2014
|
text = text.replace(MULTI_ABBR_RE, (m) => store(m, "ABBR"));
|
|
1451
2015
|
text = text.replace(SINGLE_ABBR_RE, (m) => store(m, "ABBR"));
|
|
@@ -1455,9 +2019,10 @@ function postProcess(input) {
|
|
|
1455
2019
|
text = text.replace(SPACE_AFTER_SENTENCE_RE, "$1 ");
|
|
1456
2020
|
text = text.replace(SPACE_AFTER_OPENER_RE, "$1");
|
|
1457
2021
|
text = text.replace(CAP_FIRST_RE, (_m, lead, ch) => lead + up(ch));
|
|
1458
|
-
|
|
1459
|
-
text = text
|
|
1460
|
-
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);
|
|
1461
2026
|
return restore(text, input, placeholders).trim();
|
|
1462
2027
|
}
|
|
1463
2028
|
|