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