@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.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/internal/ast.ts
2
- var AST_VERSION = 3;
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,116 +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) => hasDirectReference(children) ? { type: "enumeration", options: children, raw: content } : { 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 hasDirectReference(lists) {
192
- const stack = [...lists];
193
- while (stack.length > 0) {
194
- const list = stack.pop();
195
- for (const node of list) {
196
- if (node.type === "variable") return true;
197
- if (node.type === "conditional") stack.push(node.then, node.else);
525
+ function needsTextualReread(lists) {
526
+ return lists.some((list) => list.some((node) => node.type === "variable" || node.type === "conditional"));
527
+ }
528
+ var REFERENCE_RE = /%\w+%/u;
529
+ function holdsConditional(text) {
530
+ if (!text.includes("{?")) return false;
531
+ const opens = [];
532
+ for (let i = 0; i < text.length; i += 1) {
533
+ const ch = text.charAt(i);
534
+ if (ch === "{") {
535
+ opens.push(i);
536
+ } else if (ch === "}") {
537
+ const open = opens.pop();
538
+ if (open !== void 0 && text.charAt(open + 1) === "?" && recognizeConditional(text, open + 1, i) !== null) {
539
+ return true;
540
+ }
198
541
  }
199
542
  }
200
543
  return false;
201
544
  }
202
- var REFERENCE_RE = /%\w+%/u;
203
- function planPermutation(rawInner) {
204
- const { config, content } = extractPermutationConfig(rawInner);
205
- const { texts, separators } = permutationElements(splitTopLevel(content));
206
- const separatorHasRef = REFERENCE_RE.test(config.sep) || config.lastsep !== null && REFERENCE_RE.test(config.lastsep) || separators.some((sep) => sep !== null && REFERENCE_RE.test(sep));
545
+ var holdsTextForReread = (text) => REFERENCE_RE.test(text) || holdsConditional(text);
546
+ function planPermutation(index, start, end) {
547
+ const { text } = index;
548
+ const { config, contentStart } = extractPermutationConfig(index, start, end);
549
+ const { spans, separators } = permutationElements(index, spansBetween(contentStart, end, index.topLevelPipes(contentStart, end)));
550
+ const header = text.slice(start, contentStart);
551
+ const textNeedsReread = holdsTextForReread(header) || separators.some((sep) => sep !== null && holdsTextForReread(sep));
207
552
  return {
208
- texts,
553
+ spans,
209
554
  build: (children) => {
210
555
  const options = children.map((nodes, i) => ({ nodes, separator: separators[i] ?? null }));
211
- return separatorHasRef || hasDirectReference(children) ? { type: "permutation", config, options, raw: rawInner } : { type: "permutation", config, options };
556
+ if (!textNeedsReread && !needsTextualReread(children)) return { type: "permutation", config, options };
557
+ const node = { type: "permutation", config, options, raw: text.slice(start, end) };
558
+ rereadSpans.set(node, { index, start, end });
559
+ return node;
212
560
  }
213
561
  };
214
562
  }
215
- function recognizeConditional(text, contentStart, contentEnd) {
563
+ function recognizeConditional(text, contentStart, contentEnd, anyPairs) {
216
564
  let p = contentStart + 1;
217
565
  let inverted = false;
218
566
  if (text.charAt(p) === "!") {
219
567
  inverted = true;
220
568
  p += 1;
221
569
  }
222
- CONDITIONAL_NAME_RE.lastIndex = p;
223
- const name = CONDITIONAL_NAME_RE.exec(text)?.[0];
570
+ CONDITIONAL_NAME_RE2.lastIndex = p;
571
+ const name = CONDITIONAL_NAME_RE2.exec(text)?.[0];
224
572
  if (name === void 0 || p + name.length > contentEnd) return null;
225
573
  p += name.length;
226
574
  if (text.charAt(p) !== "?") return null;
227
575
  p += 1;
228
- return { name, inverted, bodyStart: p, sepIndex: firstTopLevelPipe(text, p, contentEnd) };
229
- }
230
- function splitConditional(content) {
231
- const head = recognizeConditional(content, 0, content.length);
232
- if (head === null) return null;
233
- const body = content.slice(head.bodyStart);
234
- const sep = head.sepIndex < 0 ? -1 : head.sepIndex - head.bodyStart;
235
- return {
236
- name: head.name,
237
- inverted: head.inverted,
238
- thenRaw: sep < 0 ? body : body.slice(0, sep),
239
- elseRaw: sep < 0 ? "" : body.slice(sep + 1)
240
- };
241
- }
242
- function parsePlural(afterPrefix) {
243
- const colon = afterPrefix.indexOf(":");
244
- return { type: "plural", countRaw: afterPrefix.slice(0, colon), formsRaw: afterPrefix.slice(colon + 1) };
245
- }
246
- var CONFIG_KEY_RE = /\b(?:minsize|maxsize|sep|lastsep)\s*=/i;
247
- var MINSIZE_RE = /minsize\s*=\s*(\d+)/i;
248
- var MAXSIZE_RE = /maxsize\s*=\s*(\d+)/i;
249
- var SEP_RE = /(?<!last)sep\s*=\s*"([^"]*)"/i;
250
- var LASTSEP_RE = /lastsep\s*=\s*"([^"]*)"/i;
251
- var HTML_TAG_RE = /^([a-zA-Z][a-zA-Z0-9-]*)(?:\s+[^>]*)?\/?$/;
252
- var PER_ELEM_HTML_RE = /^[a-zA-Z][a-zA-Z0-9]*\s/;
576
+ const sepIndex = anyPairs === void 0 ? firstTopLevelPipe(text, p, contentEnd) : firstTopLevelPipeByPairs(text, p, contentEnd, anyPairs);
577
+ return { name, inverted, bodyStart: p, sepIndex };
578
+ }
579
+ var CONFIG_KEY_RE = /\b(?:minsize|maxsize|sep|lastsep)[ \t\n\x0B\f\r]*=/i;
580
+ var MINSIZE_RE = /minsize[ \t\n\x0B\f\r]*=[ \t\n\x0B\f\r]*(\d+)/i;
581
+ var MAXSIZE_RE = /maxsize[ \t\n\x0B\f\r]*=[ \t\n\x0B\f\r]*(\d+)/i;
582
+ var SEP_RE = /(?<!last)sep[ \t\n\x0B\f\r]*=[ \t\n\x0B\f\r]*"([^"]*)"/i;
583
+ var LASTSEP_RE = /lastsep[ \t\n\x0B\f\r]*=[ \t\n\x0B\f\r]*"([^"]*)"/i;
584
+ var HTML_TAG_RE = /^([a-zA-Z][a-zA-Z0-9-]*)(?:[ \t\n\x0B\f\r][^>]*)?\/?$/;
585
+ var PER_ELEM_HTML_RE = /^[a-zA-Z][a-zA-Z0-9]*[ \t\n\x0B\f\r]/;
253
586
  function defaultPermConfig() {
254
587
  return { minsize: null, maxsize: null, sep: " ", lastsep: null };
255
588
  }
256
- function extractPermutationConfig(content) {
257
- const trimmed = phpLtrim(content);
258
- if (trimmed === "" || trimmed.charAt(0) !== "<") {
259
- return { config: defaultPermConfig(), content };
260
- }
261
- const end = findConfigEnd(trimmed);
262
- if (end === -1) return { config: defaultPermConfig(), content };
263
- const configStr = trimmed.slice(1, end);
264
- const remaining = trimmed.slice(end + 1);
265
- if (looksLikeHtmlStartTag(configStr, remaining)) {
266
- return { config: defaultPermConfig(), content };
267
- }
268
- return { config: parseConfigString(configStr), content: remaining };
269
- }
270
- function findConfigEnd(text) {
271
- let inQuote = false;
272
- for (let i = 1; i < text.length; i += 1) {
273
- const ch = text.charAt(i);
274
- if (ch === '"') inQuote = !inQuote;
275
- if (ch === ">" && !inQuote) return i;
276
- }
277
- return -1;
589
+ function extractPermutationConfig(index, start, end) {
590
+ const { text } = index;
591
+ let lt = start;
592
+ while (lt < end && isPhpTrimChar(text.charCodeAt(lt))) lt += 1;
593
+ if (lt === end || text.charCodeAt(lt) !== LT2) return { config: defaultPermConfig(), contentStart: start };
594
+ const gt = index.configEnd(lt, end);
595
+ if (gt === -1) return { config: defaultPermConfig(), contentStart: start };
596
+ const configStr = text.slice(lt + 1, gt);
597
+ if (looksLikeHtmlStartTag(index, configStr, gt + 1, end)) return { config: defaultPermConfig(), contentStart: start };
598
+ return { config: parseConfigString(configStr), contentStart: gt + 1 };
278
599
  }
279
600
  function parseConfigString(str) {
280
601
  if (!CONFIG_KEY_RE.test(str)) {
@@ -287,58 +608,54 @@ function parseConfigString(str) {
287
608
  lastsep: strGroup(LASTSEP_RE.exec(str))
288
609
  };
289
610
  }
290
- function looksLikeHtmlStartTag(tagText, remaining) {
611
+ function looksLikeHtmlStartTag(index, tagText, from, end) {
291
612
  const trimmed = phpTrim(tagText);
292
613
  if (trimmed === "") return false;
293
614
  const m = HTML_TAG_RE.exec(trimmed);
294
615
  if (!m) return false;
295
616
  if (trimmed.endsWith("/")) return true;
296
- const tagName = (m[1] ?? "").toLowerCase();
297
- return new RegExp(`</${escapeRegExp(tagName)}\\s*>`, "iu").test(remaining);
617
+ return index.hasClosingTag((m[1] ?? "").toLowerCase(), from, end);
298
618
  }
299
- function permutationElements(rawParts) {
300
- const texts = [];
619
+ function permutationElements(index, parts) {
620
+ const { text } = index;
621
+ const spans = [];
301
622
  const separators = [];
302
623
  let pendingSep = null;
303
- rawParts.forEach((part, i) => {
304
- let text = part;
624
+ parts.forEach(([partStart, partEnd], i) => {
625
+ let textEnd = partEnd;
305
626
  let trailingSep = null;
306
- if (i < rawParts.length - 1) {
307
- const extracted = extractTrailingSep(part);
627
+ if (i < parts.length - 1) {
628
+ const extracted = extractTrailingSep(index, partStart, partEnd);
308
629
  if (extracted) {
309
- text = extracted.text;
630
+ textEnd = extracted.textEnd;
310
631
  trailingSep = extracted.sep;
311
632
  }
312
633
  }
313
- const trimmed = phpTrim(text);
314
- if (trimmed !== "") {
315
- 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]);
316
640
  separators.push(pendingSep);
317
641
  }
318
642
  pendingSep = trailingSep;
319
643
  });
320
- return { texts, separators };
321
- }
322
- function extractTrailingSep(part) {
323
- const trimmed = phpRtrim(part);
324
- const len = trimmed.length;
325
- if (len === 0 || trimmed.charAt(len - 1) !== ">") return null;
326
- let openPos = -1;
327
- for (let i = len - 2; i >= 0; i -= 1) {
328
- const ch = trimmed.charAt(i);
329
- if (ch === "<") {
330
- openPos = i;
331
- break;
332
- }
333
- if (ch === ">") return null;
334
- }
335
- if (openPos === -1) return null;
336
- const inner = trimmed.slice(openPos + 1, len - 1);
644
+ return { spans, separators };
645
+ }
646
+ function extractTrailingSep(index, start, end) {
647
+ const { text } = index;
648
+ let len = end;
649
+ while (len > start && isPhpTrimChar(text.charCodeAt(len - 1))) len -= 1;
650
+ if (len === start || text.charCodeAt(len - 1) !== GT2) return null;
651
+ const openPos = index.lastAngleWithin(start, len - 2);
652
+ if (openPos === -1 || text.charCodeAt(openPos) === GT2) return null;
653
+ const inner = text.slice(openPos + 1, len - 1);
337
654
  const innerTrimmed = phpTrim(inner);
338
655
  if (innerTrimmed.startsWith("/") || innerTrimmed.endsWith("/") || PER_ELEM_HTML_RE.test(innerTrimmed)) {
339
656
  return null;
340
657
  }
341
- return { text: trimmed.slice(0, openPos), sep: inner };
658
+ return { textEnd: openPos, sep: inner };
342
659
  }
343
660
  function intGroup(m) {
344
661
  return m && m[1] !== void 0 ? Number.parseInt(m[1], 10) : null;
@@ -346,52 +663,21 @@ function intGroup(m) {
346
663
  function strGroup(m) {
347
664
  return m && m[1] !== void 0 ? m[1] : null;
348
665
  }
349
- function escapeRegExp(s) {
350
- return s.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
666
+ function isPhpTrimChar(code) {
667
+ return code === 32 || code === 9 || code === 10 || code === 13 || code === 0 || code === 11;
351
668
  }
352
- var PHP_LTRIM_RE = /^[ \t\n\r\0\x0B]+/u;
353
- var PHP_RTRIM_RE = /[ \t\n\r\0\x0B]+$/u;
354
669
  function phpTrim(s) {
355
- return s.replace(PHP_LTRIM_RE, "").replace(PHP_RTRIM_RE, "");
670
+ return phpRtrim(phpLtrim(s));
356
671
  }
357
672
  function phpLtrim(s) {
358
- 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);
359
676
  }
360
677
  function phpRtrim(s) {
361
- return s.replace(PHP_RTRIM_RE, "");
362
- }
363
- function findMatchingClose(text, openPos, open, close) {
364
- let depth = 0;
365
- for (let i = openPos; i < text.length; i += 1) {
366
- const ch = text.charAt(i);
367
- if (ch === open) {
368
- depth += 1;
369
- } else if (ch === close) {
370
- depth -= 1;
371
- if (depth === 0) return i;
372
- }
373
- }
374
- return -1;
375
- }
376
- function splitTopLevel(inner) {
377
- const parts = [];
378
- let brace = 0;
379
- let bracket = 0;
380
- let cur = "";
381
- for (const ch of inner) {
382
- if (ch === "{") brace += 1;
383
- else if (ch === "}") brace -= 1;
384
- else if (ch === "[") bracket += 1;
385
- else if (ch === "]") bracket -= 1;
386
- if (ch === "|" && brace === 0 && bracket === 0) {
387
- parts.push(cur);
388
- cur = "";
389
- } else {
390
- cur += ch;
391
- }
392
- }
393
- parts.push(cur);
394
- return parts;
678
+ let end = s.length;
679
+ while (end > 0 && isPhpTrimChar(s.charCodeAt(end - 1))) end -= 1;
680
+ return end === s.length ? s : s.slice(0, end);
395
681
  }
396
682
  function firstTopLevelPipe(body, from = 0, to = body.length) {
397
683
  let depth = 0;
@@ -407,27 +693,35 @@ function firstTopLevelPipe(body, from = 0, to = body.length) {
407
693
  }
408
694
  return -1;
409
695
  }
696
+ function firstTopLevelPipeByPairs(body, from, to, anyPairs) {
697
+ let j = from;
698
+ while (j < to) {
699
+ const code = body.charCodeAt(j);
700
+ if (code === BRACE_OPEN || code === BRACKET_OPEN) {
701
+ const close = anyPairs[j];
702
+ if (close === -1 || close >= to) return -1;
703
+ j = close + 1;
704
+ } else if (code === PIPE2) {
705
+ return j;
706
+ } else {
707
+ j += 1;
708
+ }
709
+ }
710
+ return -1;
711
+ }
410
712
 
411
713
  // src/internal/plurals.ts
412
714
  var PLURAL_PREFIX2 = "{plural ";
413
715
  function findPluralBlocks(text) {
414
716
  const blocks = [];
717
+ let close = null;
415
718
  let i = 0;
416
719
  while (i < text.length) {
417
720
  const start = text.indexOf(PLURAL_PREFIX2, i);
418
721
  if (start === -1) break;
419
- let depth = 1;
420
- let j = start + PLURAL_PREFIX2.length;
421
- while (j < text.length) {
422
- const ch = text.charAt(j);
423
- if (ch === "{") depth += 1;
424
- else if (ch === "}") {
425
- depth -= 1;
426
- if (depth === 0) break;
427
- }
428
- j += 1;
429
- }
430
- if (depth !== 0) {
722
+ close ??= matchPairs(text, BRACE_OPEN, BRACE_CLOSE);
723
+ const j = close[start];
724
+ if (j === -1) {
431
725
  i = start + PLURAL_PREFIX2.length;
432
726
  continue;
433
727
  }
@@ -590,12 +884,19 @@ function checkDirectives(text, out) {
590
884
  }
591
885
  }
592
886
  }
887
+ var CONFIG_KEY_PRESENT_RE = /(?<!\w)\w+[ \t\n\x0B\f\r]*=/;
888
+ var CONFIG_KEYS_RE = /(?<!\w)(\w+)[ \t\n\x0B\f\r]*=/gu;
593
889
  function checkPermutationConfigs(text, idx, out) {
594
- for (const m of text.matchAll(/\[<([^>]*?)>/gu)) {
595
- const configStr = m[1] ?? "";
596
- if (!/\w+\s*=/.test(configStr)) continue;
597
- const configBase = (m.index ?? 0) + 2;
598
- 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)) {
599
900
  const key = (km[1] ?? "").toLowerCase();
600
901
  if (!KNOWN_CONFIG_KEYS.has(key)) {
601
902
  out.push(err(
@@ -605,7 +906,7 @@ function checkPermutationConfigs(text, idx, out) {
605
906
  ));
606
907
  }
607
908
  }
608
- const min = /minsize\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);
609
910
  if (min && !/^\d+$/.test(min[1] ?? "")) {
610
911
  out.push(err(
611
912
  "permutation.minsize-not-integer",
@@ -613,7 +914,7 @@ function checkPermutationConfigs(text, idx, out) {
613
914
  { ...span(idx, configBase + min.index, min[0].length), data: { value: min[1] } }
614
915
  ));
615
916
  }
616
- const max = /maxsize\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);
617
918
  if (max && !/^\d+$/.test(max[1] ?? "")) {
618
919
  out.push(err(
619
920
  "permutation.maxsize-not-integer",
@@ -936,6 +1237,13 @@ function addAll(target, text, re, fold) {
936
1237
  }
937
1238
  }
938
1239
 
1240
+ // src/internal/charclass.ts
1241
+ var UCP_SPACE = "\\t\\n\\x0B\\f\\r \\x85\\xA0\\u1680\\u180E\\u2000-\\u200A\\u2028\\u2029\\u202F\\u205F\\u3000";
1242
+ function isUcpSpace(code) {
1243
+ return code >= 9 && code <= 13 || code === 32 || code === 133 || code === 160 || code === 5760 || code === 6158 || code >= 8192 && code <= 8202 || code === 8232 || code === 8233 || code === 8239 || code === 8287 || code === 12288;
1244
+ }
1245
+ var UCP_WORD = "\\p{L}\\p{N}\\p{Mn}\\p{Pc}";
1246
+
939
1247
  // src/internal/errors.ts
940
1248
  var SpintaxError = class extends Error {
941
1249
  constructor(message) {
@@ -965,6 +1273,120 @@ var NotImplementedError = class extends SpintaxError {
965
1273
  }
966
1274
  };
967
1275
 
1276
+ // src/internal/fragment.ts
1277
+ var FLAT_BELOW = 128;
1278
+ var Join = class {
1279
+ constructor(pieces) {
1280
+ this.pieces = pieces;
1281
+ let length = 0;
1282
+ for (const piece of pieces) length += lengthOf(piece);
1283
+ this.length = length;
1284
+ let lead = 0;
1285
+ for (const piece of pieces) {
1286
+ const pieceLead = leadOf(piece);
1287
+ lead += pieceLead;
1288
+ if (pieceLead < lengthOf(piece)) break;
1289
+ }
1290
+ this.lead = lead;
1291
+ let trail = 0;
1292
+ if (lead < length) {
1293
+ for (let k = pieces.length - 1; k >= 0; k -= 1) {
1294
+ const piece = pieces[k];
1295
+ const pieceTrail = trailOf(piece);
1296
+ trail += pieceTrail;
1297
+ if (pieceTrail < lengthOf(piece)) break;
1298
+ }
1299
+ } else {
1300
+ trail = length;
1301
+ }
1302
+ this.trail = trail;
1303
+ }
1304
+ pieces;
1305
+ length;
1306
+ lead;
1307
+ trail;
1308
+ };
1309
+ var Window = class {
1310
+ constructor(base, from, to) {
1311
+ this.base = base;
1312
+ this.from = from;
1313
+ this.to = to;
1314
+ this.length = to - from;
1315
+ }
1316
+ base;
1317
+ from;
1318
+ to;
1319
+ length;
1320
+ lead = 0;
1321
+ trail = 0;
1322
+ };
1323
+ function lengthOf(fragment) {
1324
+ return fragment.length;
1325
+ }
1326
+ function leadOf(fragment) {
1327
+ if (typeof fragment !== "string") return fragment.lead;
1328
+ let k = 0;
1329
+ while (k < fragment.length && isPhpTrimChar(fragment.charCodeAt(k))) k += 1;
1330
+ return k;
1331
+ }
1332
+ function trailOf(fragment) {
1333
+ if (typeof fragment !== "string") return fragment.trail;
1334
+ let k = fragment.length;
1335
+ while (k > 0 && isPhpTrimChar(fragment.charCodeAt(k - 1))) k -= 1;
1336
+ return fragment.length - k;
1337
+ }
1338
+ function joinFragments(pieces) {
1339
+ if (pieces.length === 0) return "";
1340
+ if (pieces.length === 1) return pieces[0];
1341
+ let total = 0;
1342
+ let flat = true;
1343
+ for (const piece of pieces) {
1344
+ total += lengthOf(piece);
1345
+ if (typeof piece !== "string") flat = false;
1346
+ }
1347
+ if (flat && total < FLAT_BELOW) return pieces.join("");
1348
+ return new Join(pieces);
1349
+ }
1350
+ function trimFragment(fragment) {
1351
+ if (typeof fragment === "string") {
1352
+ let start = 0;
1353
+ while (start < fragment.length && isPhpTrimChar(fragment.charCodeAt(start))) start += 1;
1354
+ let end = fragment.length;
1355
+ while (end > start && isPhpTrimChar(fragment.charCodeAt(end - 1))) end -= 1;
1356
+ return start === 0 && end === fragment.length ? fragment : fragment.slice(start, end);
1357
+ }
1358
+ if (fragment.lead === fragment.length) return "";
1359
+ if (fragment.lead === 0 && fragment.trail === 0) return fragment;
1360
+ return new Window(fragment, fragment.lead, fragment.length - fragment.trail);
1361
+ }
1362
+ function flatten(fragment) {
1363
+ if (typeof fragment === "string") return fragment;
1364
+ const out = [];
1365
+ const stack = [[fragment, 0, fragment.length]];
1366
+ while (stack.length > 0) {
1367
+ const [piece, from, to] = stack.pop();
1368
+ if (from >= to) continue;
1369
+ if (typeof piece === "string") {
1370
+ out.push(from === 0 && to === piece.length ? piece : piece.slice(from, to));
1371
+ } else if (piece instanceof Window) {
1372
+ stack.push([piece.base, piece.from + from, piece.from + to]);
1373
+ } else {
1374
+ const wanted = [];
1375
+ let offset = 0;
1376
+ for (const child of piece.pieces) {
1377
+ const length = lengthOf(child);
1378
+ const a = Math.max(from, offset);
1379
+ const b = Math.min(to, offset + length);
1380
+ if (a < b) wanted.push([child, a - offset, b - offset]);
1381
+ offset += length;
1382
+ if (offset >= to) break;
1383
+ }
1384
+ for (let k = wanted.length - 1; k >= 0; k -= 1) stack.push(wanted[k]);
1385
+ }
1386
+ }
1387
+ return out.join("");
1388
+ }
1389
+
968
1390
  // src/internal/render.ts
969
1391
  var MAX_VARIABLE_DEPTH = 50;
970
1392
  var MAX_EXPANSION_CHARS = 1024 * 1024;
@@ -978,7 +1400,7 @@ function renderAst(ast, ctx) {
978
1400
  onPluralError: ctx.onPluralError,
979
1401
  budget: ctx.budget
980
1402
  };
981
- const vars = Object.keys(ast.defDefs).length > 0 ? { ...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;
982
1404
  const text = renderNodes(ast.nodes, { ...walkOpts, vars });
983
1405
  return ctx.resolver ? resolveIncludes(text, ctx) : text;
984
1406
  }
@@ -1002,7 +1424,7 @@ function resolveIncludes(text, ctx) {
1002
1424
  });
1003
1425
  }
1004
1426
  function buildVars(setDefs, context) {
1005
- const vars = {};
1427
+ const vars = nameMap();
1006
1428
  for (const [name, value] of Object.entries(setDefs)) {
1007
1429
  vars[name] = value;
1008
1430
  }
@@ -1013,16 +1435,17 @@ function buildVars(setDefs, context) {
1013
1435
  }
1014
1436
  function rollDefinitions(defDefs, vars, context, opts) {
1015
1437
  const outranked = new Set(Object.keys(context).map((key) => key.toLowerCase()));
1016
- const rolled = {};
1017
- const aliases = {};
1438
+ const rolled = nameMap();
1439
+ const definitions = nameMap(defDefs);
1440
+ const aliases = nameMap();
1018
1441
  for (const [name, value] of Object.entries(vars)) {
1019
- if (name in defDefs && !outranked.has(name)) continue;
1442
+ if (name in definitions && !outranked.has(name)) continue;
1020
1443
  aliases[name] = value;
1021
1444
  }
1022
- for (const name of orderDefinitions(defDefs, aliases)) {
1445
+ for (const name of orderDefinitions(definitions, aliases)) {
1023
1446
  if (outranked.has(name)) continue;
1024
- const value = defDefs[name] ?? "";
1025
- 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) });
1026
1449
  }
1027
1450
  return rolled;
1028
1451
  }
@@ -1062,6 +1485,9 @@ function directReferences(text) {
1062
1485
  return [...text.matchAll(/%(\w+)%/gu)].map((match) => (match[1] ?? "").toLowerCase());
1063
1486
  }
1064
1487
  function renderNodes(nodes, opts) {
1488
+ return flatten(renderFragment(nodes, opts));
1489
+ }
1490
+ function renderFragment(nodes, opts) {
1065
1491
  const frame = (list) => ({ nodes: list, i: 0, out: [], pending: null });
1066
1492
  const stack = [frame(nodes)];
1067
1493
  while (stack.length > 0) {
@@ -1076,7 +1502,7 @@ function renderNodes(nodes, opts) {
1076
1502
  continue;
1077
1503
  }
1078
1504
  if (f.i >= f.nodes.length) {
1079
- const text = f.out.join("");
1505
+ const text = joinFragments(f.out);
1080
1506
  stack.pop();
1081
1507
  const parent = stack[stack.length - 1];
1082
1508
  if (parent === void 0) return text;
@@ -1086,11 +1512,12 @@ function renderNodes(nodes, opts) {
1086
1512
  const node = f.nodes[f.i];
1087
1513
  f.i += 1;
1088
1514
  const step = renderNode(node, opts);
1089
- if (typeof step === "string") f.out.push(step);
1515
+ if (typeof step === "string" || !("lists" in step)) f.out.push(step);
1090
1516
  else f.pending = step;
1091
1517
  }
1092
1518
  return "";
1093
1519
  }
1520
+ var single = (list) => ({ lists: [list], done: [], assemble: (parts) => parts[0] ?? "" });
1094
1521
  function renderNode(node, opts) {
1095
1522
  switch (node.type) {
1096
1523
  case "literal":
@@ -1116,7 +1543,7 @@ function resolveVariable(name, opts) {
1116
1543
  if (opts.budget.left <= 0) return `%${name}%`;
1117
1544
  opts.budget.left -= value.length;
1118
1545
  if (opts.depth >= MAX_VARIABLE_DEPTH || !/[{[%]/u.test(value)) return value;
1119
- return renderNodes(parseSequence(value), { ...opts, depth: opts.depth + 1 });
1546
+ return renderFragment(parseSequence(value), { ...opts, depth: opts.depth + 1 });
1120
1547
  }
1121
1548
  function passesLeft(opts) {
1122
1549
  return Math.max(1, MAX_VARIABLE_DEPTH - opts.depth + 1);
@@ -1140,18 +1567,29 @@ function expandVarsFixpoint(text, opts, passes) {
1140
1567
  }
1141
1568
  return { text: out, changed: changedAny, converged: false };
1142
1569
  }
1570
+ var NON_SPACE_RE = new RegExp(`[^${UCP_SPACE}]`, "u");
1571
+ var truthinessByVars = /* @__PURE__ */ new WeakMap();
1143
1572
  function conditionalTakesThen(name, inverted, opts) {
1144
- const value = opts.vars[name.toLowerCase()];
1145
- 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
+ }
1146
1582
  return inverted ? !baseTruthy : baseTruthy;
1147
1583
  }
1148
1584
  function renderConditional(node, opts) {
1149
- const truthy = conditionalTakesThen(node.name, node.inverted, opts);
1150
- return { lists: [truthy ? node.then : node.else], done: [], assemble: (parts) => parts[0] ?? "" };
1585
+ return single(conditionalTakesThen(node.name, node.inverted, opts) ? node.then : node.else);
1151
1586
  }
1152
1587
  function resolveConditionalsInText(text, opts) {
1153
1588
  if (!text.includes("{?")) return text;
1154
- const close = 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);
1155
1593
  const out = [];
1156
1594
  const pending = [[0, text.length]];
1157
1595
  while (pending.length > 0) {
@@ -1159,13 +1597,14 @@ function resolveConditionalsInText(text, opts) {
1159
1597
  const segEnd = segment[1];
1160
1598
  let i = segment[0];
1161
1599
  while (i < segEnd) {
1162
- const open = text.indexOf("{?", i);
1600
+ const next = lowerBound(heads, i);
1601
+ const open = next < heads.length ? heads[next] : -1;
1163
1602
  if (open === -1 || open + 1 >= segEnd) {
1164
1603
  out.push(text.slice(i, segEnd));
1165
1604
  break;
1166
1605
  }
1167
1606
  const shut = close[open] ?? -1;
1168
- const head = shut === -1 || shut >= segEnd ? null : recognizeConditional(text, open + 1, shut);
1607
+ const head = shut === -1 || shut >= segEnd ? null : recognizeConditional(text, open + 1, shut, any);
1169
1608
  if (head === null) {
1170
1609
  out.push(text.slice(i, Math.min(open + 2, segEnd)));
1171
1610
  i = open + 2;
@@ -1181,20 +1620,6 @@ function resolveConditionalsInText(text, opts) {
1181
1620
  }
1182
1621
  return out.join("");
1183
1622
  }
1184
- function matchBraces(text) {
1185
- const close = new Int32Array(text.length).fill(-1);
1186
- const opens = [];
1187
- for (let i = 0; i < text.length; i += 1) {
1188
- const ch = text.charAt(i);
1189
- if (ch === "{") {
1190
- opens.push(i);
1191
- } else if (ch === "}") {
1192
- const open = opens.pop();
1193
- if (open !== void 0) close[open] = i;
1194
- }
1195
- }
1196
- return close;
1197
- }
1198
1623
  function renderPlural(node, opts) {
1199
1624
  const passes = passesLeft(opts);
1200
1625
  const countPass = expandVarsFixpoint(node.countRaw, opts, passes);
@@ -1212,7 +1637,7 @@ function renderPlural(node, opts) {
1212
1637
  });
1213
1638
  return fullwidthVerbatim(countRaw, formsRaw);
1214
1639
  }
1215
- const count = phpTrim2(countRaw);
1640
+ const count = phpTrim(countRaw);
1216
1641
  if (!/^-?\d+$/u.test(count)) {
1217
1642
  report({
1218
1643
  code: "plural.count",
@@ -1222,7 +1647,7 @@ function renderPlural(node, opts) {
1222
1647
  });
1223
1648
  return "";
1224
1649
  }
1225
- const forms = formsRaw.split("|").map((f) => phpTrim2(f));
1650
+ const forms = formsRaw.split("|").map((f) => phpTrim(f));
1226
1651
  if (forms.length !== pluralArity(base)) {
1227
1652
  report({
1228
1653
  code: "plural.arity",
@@ -1235,8 +1660,8 @@ function renderPlural(node, opts) {
1235
1660
  return fullwidthVerbatim(countRaw, formsRaw);
1236
1661
  }
1237
1662
  const picked = pluralFor(base, Number.parseInt(count, 10), forms);
1238
- if (!formsPass.converged) return renderNodes(parseSequence(picked), { ...opts, frozen: true });
1239
- 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));
1240
1665
  }
1241
1666
  function rawConstruct(countRaw, formsRaw) {
1242
1667
  return `{plural ${countRaw}:${formsRaw}}`;
@@ -1245,25 +1670,31 @@ function fullwidthVerbatim(countRaw, formsRaw) {
1245
1670
  return rawConstruct(countRaw, formsRaw).replace(/\{/gu, "\uFF5B").replace(/\}/gu, "\uFF5D");
1246
1671
  }
1247
1672
  function renderEnumeration(node, opts) {
1248
- const spliced = node.raw === void 0 ? null : spliceConstruct(node.raw, "{", "}", opts);
1673
+ const spliced = node.raw === void 0 ? null : spliceConstruct(node, node.raw, "{", "}", opts);
1249
1674
  if (spliced !== null) return spliced;
1250
1675
  const { options } = node;
1251
1676
  if (options.length === 0) return "";
1252
1677
  const picked = options[randomInt(opts.rng, 0, options.length - 1)];
1253
1678
  if (!picked) return "";
1254
- return { lists: [picked], done: [], assemble: (parts) => parts[0] ?? "" };
1679
+ return single(picked);
1255
1680
  }
1256
- function spliceConstruct(raw, open, close, opts) {
1681
+ function spliceConstruct(node, raw, open, close, opts) {
1257
1682
  if (opts.frozen === true) return null;
1683
+ const span2 = rereadSpans.get(node);
1684
+ if (span2 !== void 0 && !rereadCouldChange(span2, opts)) return null;
1258
1685
  const expanded = expandVarsFixpoint(resolveConditionalsInText(raw, opts), opts, passesLeft(opts));
1259
1686
  const body = resolveConditionalsInText(expanded.text, opts);
1260
1687
  if (body === raw) return null;
1261
1688
  const nodes = parseSequence(open + body + close);
1262
- if (!expanded.converged) return renderNodes(nodes, { ...opts, frozen: true });
1263
- return { lists: [nodes], done: [], assemble: (parts) => parts[0] ?? "" };
1689
+ if (!expanded.converged) return renderFragment(nodes, { ...opts, frozen: true });
1690
+ return single(nodes);
1691
+ }
1692
+ function rereadCouldChange({ index, start, end }, opts) {
1693
+ if (index.hasConditionalWithin(start, end)) return true;
1694
+ return !(opts.budget.left <= 0) && index.hasDefinedReferenceWithin(start, end, opts.vars);
1264
1695
  }
1265
1696
  function renderPermutation(node, opts) {
1266
- const spliced = node.raw === void 0 ? null : spliceConstruct(node.raw, "[", "]", opts);
1697
+ const spliced = node.raw === void 0 ? null : spliceConstruct(node, node.raw, "[", "]", opts);
1267
1698
  if (spliced !== null) return spliced;
1268
1699
  if (node.options.length === 0) return "";
1269
1700
  return {
@@ -1273,7 +1704,11 @@ function renderPermutation(node, opts) {
1273
1704
  };
1274
1705
  }
1275
1706
  function assemblePermutation(node, rendered, opts) {
1276
- const elements = 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
+ });
1277
1712
  const total = elements.length;
1278
1713
  if (total === 0) return "";
1279
1714
  const { config } = node;
@@ -1312,25 +1747,20 @@ function joinWithSeparators(elements, globalSep, globalLastsep) {
1312
1747
  const count = elements.length;
1313
1748
  if (count === 0) return "";
1314
1749
  if (count === 1) return elements[0].text;
1315
- let out = elements[0].text;
1750
+ const pieces = [elements[0].text];
1316
1751
  for (let i = 1; i < count; i += 1) {
1317
1752
  const el = elements[i];
1318
1753
  const sep = el.sep ?? (i === count - 1 ? globalLastsep : globalSep);
1319
- out += padSeparator(sep) + el.text;
1754
+ pieces.push(padSeparator(sep), el.text);
1320
1755
  }
1321
- return out;
1756
+ return joinFragments(pieces);
1322
1757
  }
1323
1758
  function padSeparator(sep) {
1324
- const trimmed = phpTrim2(sep);
1759
+ const trimmed = phpTrim(sep);
1325
1760
  if (trimmed === "") return sep;
1326
1761
  if (/^\p{L}+$/u.test(trimmed)) return ` ${trimmed} `;
1327
1762
  return sep;
1328
1763
  }
1329
- var PHP_LTRIM_RE2 = /^[ \t\n\r\0\x0B]+/u;
1330
- var PHP_RTRIM_RE2 = /[ \t\n\r\0\x0B]+$/u;
1331
- function phpTrim2(s) {
1332
- return s.replace(PHP_LTRIM_RE2, "").replace(PHP_RTRIM_RE2, "");
1333
- }
1334
1764
 
1335
1765
  // src/internal/postprocess.ts
1336
1766
  var SINGLE_ABBREVS = [
@@ -1383,41 +1813,179 @@ var SINGLE_ABBREVS = [
1383
1813
  "Ave",
1384
1814
  "Blvd"
1385
1815
  ];
1386
- var WS = " \\t\\r\\n\\f\\x0B";
1816
+ var WS = UCP_SPACE;
1387
1817
  var S = `[${WS}]`;
1388
- 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})`;
1389
1822
  var URI_BODY = `[^\\x00${WS}<>"')\\]]`;
1390
1823
  var URI_RE = new RegExp(`(?:(?:https?|ftp):\\/\\/|(?:mailto|tel):)${URI_BODY}+`, "giu");
1391
1824
  var MAILTEL_PREFIX_RE = /^(?:mailto|tel):/iu;
1392
- var EMAIL_RE = new RegExp(`[a-z0-9._%+\\-]+@${DOMAIN_PART}\\b`, "giu");
1393
- 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");
1394
1827
  var DECIMAL_RE = /\b\d+\.\d+\b/gu;
1395
- 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");
1396
1829
  var SINGLE_ABBR_RE = new RegExp(`(?<![\\p{L}\\p{N}])(?:${SINGLE_ABBREVS.join("|")})\\.(?=${S}|$|<)`, "giu");
1397
- 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
+ }
1398
1868
  var SENTENCE_OPENERS = "\xBF\xA1";
1399
1869
  var LEAD = `(?:<[^>]+>|[${SENTENCE_OPENERS}]|${S})*`;
1400
- var SPACE_BEFORE_PUNCT_RE = new RegExp(`${S}+([,;:!?.])`, "gu");
1401
- var SPACE_AFTER_COMMA_RE = new RegExp(`([,;:])(?!\\d)(?!${S}|$|<)`, "gu");
1402
- 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");
1403
1873
  var SPACE_AFTER_OPENER_RE = new RegExp(`([${SENTENCE_OPENERS}])${S}+`, "gu");
1404
1874
  var CAP_FIRST_RE = new RegExp(`^(${LEAD})(\\p{Ll})`, "u");
1405
- var CAP_AFTER_SENTENCE_RE = new RegExp(`([.!?\u2026])(${LEAD})(\\p{Ll})`, "gu");
1406
- var CAP_AFTER_BLOCK_RE = new RegExp(
1407
- `(<\\/?(?:p|h[1-6]|li|blockquote|div|td|th)[^>]*>${LEAD})(\\p{Ll})`,
1408
- "giu"
1409
- );
1410
- var CAP_AFTER_BREAK_RE = new RegExp(`(\\n${LEAD})(\\p{Ll})`, "gu");
1411
1875
  var up = (ch) => ch.toUpperCase();
1876
+ function indexLeads(text) {
1877
+ const n = text.length;
1878
+ const leadEnd = new Int32Array(n + 1);
1879
+ leadEnd[n] = n;
1880
+ let gt = -1;
1881
+ for (let i = n - 1; i >= 0; i -= 1) {
1882
+ const code = text.charCodeAt(i);
1883
+ let tokenEnd = -1;
1884
+ if (code === 191 || code === 161 || isUcpSpace(code)) {
1885
+ tokenEnd = i + 1;
1886
+ } else if (code === 60 && gt > i + 1) {
1887
+ tokenEnd = gt + 1;
1888
+ }
1889
+ leadEnd[i] = tokenEnd === -1 ? i : leadEnd[tokenEnd];
1890
+ if (code === 62) gt = i;
1891
+ }
1892
+ return { leadEnd };
1893
+ }
1894
+ var LOWER_RE = /^\p{Ll}/u;
1895
+ var LOWER_CASELESS_RE = /^\p{Ll}/iu;
1896
+ var BLOCK_TAG_NAME_RE = /<\/?(?:p|h[1-6]|li|blockquote|div|td|th)/iuy;
1897
+ var SENTENCE_END_SCAN_RE = new RegExp(`[.!?${String.fromCharCode(8230)}]`, "g");
1898
+ var LEAD_WALK = 32;
1899
+ function leadEndFrom(text, i, leads) {
1900
+ let j = i;
1901
+ for (let steps = 0; steps < LEAD_WALK && j < text.length; steps += 1) {
1902
+ const code = text.charCodeAt(j);
1903
+ if (code === 60) return leads().leadEnd[j];
1904
+ if (code !== 191 && code !== 161 && !isUcpSpace(code)) return j;
1905
+ j += 1;
1906
+ }
1907
+ return j < text.length ? leads().leadEnd[j] : j;
1908
+ }
1909
+ function capitalizeAfter(text, lower, next, context) {
1910
+ const leads = () => {
1911
+ if (context.leads === null || context.leads.leadEnd.length !== text.length + 1) context.leads = indexLeads(text);
1912
+ return context.leads;
1913
+ };
1914
+ let out = "";
1915
+ let emitted = 0;
1916
+ let from = 0;
1917
+ for (let boundary = next(text, from); boundary !== null; boundary = next(text, from)) {
1918
+ from = boundary.resume;
1919
+ const at = leadEndFrom(text, boundary.leadStart, leads);
1920
+ const cp = text.codePointAt(at);
1921
+ if (cp === void 0) continue;
1922
+ const ch = String.fromCodePoint(cp);
1923
+ if (!lower.test(ch)) continue;
1924
+ out += text.slice(emitted, at) + up(ch);
1925
+ emitted = at + ch.length;
1926
+ from = emitted;
1927
+ }
1928
+ return emitted === 0 ? text : out + text.slice(emitted);
1929
+ }
1930
+ var afterSentenceEnd = (text, from) => {
1931
+ SENTENCE_END_SCAN_RE.lastIndex = from;
1932
+ const m = SENTENCE_END_SCAN_RE.exec(text);
1933
+ return m === null ? null : { leadStart: m.index + 1, resume: m.index + 1 };
1934
+ };
1935
+ function afterBlockTag() {
1936
+ let gtFrom = -1;
1937
+ let gt = -1;
1938
+ return (text, from) => {
1939
+ for (let lt = text.indexOf("<", from); lt !== -1; lt = text.indexOf("<", lt + 1)) {
1940
+ BLOCK_TAG_NAME_RE.lastIndex = lt;
1941
+ if (!BLOCK_TAG_NAME_RE.test(text)) continue;
1942
+ const nameEnd = BLOCK_TAG_NAME_RE.lastIndex;
1943
+ if (gtFrom === -1 || nameEnd < gtFrom || gt !== -1 && nameEnd > gt) {
1944
+ gtFrom = nameEnd;
1945
+ gt = text.indexOf(">", nameEnd);
1946
+ }
1947
+ if (gt === -1) continue;
1948
+ return { leadStart: gt + 1, resume: lt + 1 };
1949
+ }
1950
+ return null;
1951
+ };
1952
+ }
1953
+ var afterLineBreak = (text, from) => {
1954
+ const at = text.indexOf("\n", from);
1955
+ return at === -1 ? null : { leadStart: at + 1, resume: at + 1 };
1956
+ };
1412
1957
  var SHIELD_PREFIXES = ["URL", "URI", "EMAIL", "DOM", "NUM", "ABBR"];
1413
1958
  var RESTORE_RE2 = new RegExp(`\\x00(?:${SHIELD_PREFIXES.join("|")})_\\d+\\x00`, "gu");
1414
1959
  function restore(text, input, placeholders) {
1415
1960
  if (!input.includes("\0")) {
1416
1961
  return text.replace(RESTORE_RE2, (key) => placeholders.get(key) ?? key);
1417
1962
  }
1418
- let out = text;
1419
- for (const [key, value] of placeholders) {
1420
- 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;
1421
1989
  }
1422
1990
  return out;
1423
1991
  }
@@ -1431,21 +1999,17 @@ function postProcess(input) {
1431
1999
  return key;
1432
2000
  };
1433
2001
  const storeWithTrailingPunct = (value, prefix) => {
1434
- const m = TRAILING_PUNCT_RE.exec(value);
1435
- if (m) {
1436
- const suffix = m[1] ?? "";
1437
- const body = value.slice(0, value.length - suffix.length);
1438
- return body === "" ? suffix : store(body, prefix) + suffix;
1439
- }
1440
- return store(value, prefix);
2002
+ const cut = trailingPunctuationStart(value);
2003
+ if (cut === value.length) return store(value, prefix);
2004
+ return cut === 0 ? value : store(value.slice(0, cut), prefix) + value.slice(cut);
1441
2005
  };
1442
2006
  let text = input;
1443
2007
  text = text.replace(
1444
2008
  URI_RE,
1445
2009
  (m) => storeWithTrailingPunct(m, MAILTEL_PREFIX_RE.test(m) ? "URI" : "URL")
1446
2010
  );
1447
- text = text.replace(EMAIL_RE, (m) => store(m, "EMAIL"));
1448
- 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"));
1449
2013
  text = text.replace(DECIMAL_RE, (m) => store(m, "NUM"));
1450
2014
  text = text.replace(MULTI_ABBR_RE, (m) => store(m, "ABBR"));
1451
2015
  text = text.replace(SINGLE_ABBR_RE, (m) => store(m, "ABBR"));
@@ -1455,9 +2019,10 @@ function postProcess(input) {
1455
2019
  text = text.replace(SPACE_AFTER_SENTENCE_RE, "$1 ");
1456
2020
  text = text.replace(SPACE_AFTER_OPENER_RE, "$1");
1457
2021
  text = text.replace(CAP_FIRST_RE, (_m, lead, ch) => lead + up(ch));
1458
- text = text.replace(CAP_AFTER_SENTENCE_RE, (_m, p, gap, ch) => p + gap + up(ch));
1459
- text = text.replace(CAP_AFTER_BLOCK_RE, (_m, tag, ch) => tag + up(ch));
1460
- 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);
1461
2026
  return restore(text, input, placeholders).trim();
1462
2027
  }
1463
2028