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