@spintax/core 0.7.0 → 0.9.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",
@@ -797,11 +1098,12 @@ function checkVariableReferences(text, idx, known, out) {
797
1098
  refsOf.set(name, refs);
798
1099
  }
799
1100
  const { reaches: reachesCycle, via } = namesThatReachACycle(defs, refsOf);
1101
+ const lengths = routeLengths(via);
800
1102
  for (const name of defs.keys()) {
801
1103
  if (!reachesCycle.has(name)) continue;
802
1104
  out.push(err(
803
1105
  "variable.circular-reference",
804
- `Circular variable reference: ${cyclePath(name, via)}.`,
1106
+ `Circular variable reference: ${cyclePath(name, via, lengths)}.`,
805
1107
  { ...defPos.get(name) ?? somewhere, data: { name } }
806
1108
  ));
807
1109
  }
@@ -861,7 +1163,7 @@ function namesThatReachACycle(defs, refsOf) {
861
1163
  return { reaches, via };
862
1164
  }
863
1165
  var CYCLE_PATH_LIMIT = 8;
864
- function cyclePath(name, via) {
1166
+ function cyclePath(name, via, lengths) {
865
1167
  const seen = /* @__PURE__ */ new Set([name]);
866
1168
  const shown = [name];
867
1169
  let current = name;
@@ -873,16 +1175,7 @@ function cyclePath(name, via) {
873
1175
  return shown.join(" \u2192 ");
874
1176
  }
875
1177
  if (shown.length >= CYCLE_PATH_LIMIT) {
876
- let more = 0;
877
- let walk2 = next;
878
- while (!seen.has(walk2)) {
879
- seen.add(walk2);
880
- more += 1;
881
- const step = via.get(walk2);
882
- if (step === void 0) break;
883
- walk2 = step;
884
- }
885
- return `${shown.join(" \u2192 ")} \u2192 \u2026 (${more} more)`;
1178
+ return `${shown.join(" \u2192 ")} \u2192 \u2026 (${lengths.get(name) - shown.length} more)`;
886
1179
  }
887
1180
  seen.add(next);
888
1181
  shown.push(next);
@@ -890,6 +1183,33 @@ function cyclePath(name, via) {
890
1183
  }
891
1184
  return shown.join(" \u2192 ");
892
1185
  }
1186
+ function routeLengths(via) {
1187
+ const lengths = /* @__PURE__ */ new Map();
1188
+ for (const start of via.keys()) {
1189
+ if (lengths.has(start)) continue;
1190
+ const walk2 = [];
1191
+ const onWalk = /* @__PURE__ */ new Map();
1192
+ let node = start;
1193
+ while (node !== void 0 && !lengths.has(node) && !onWalk.has(node)) {
1194
+ onWalk.set(node, walk2.length);
1195
+ walk2.push(node);
1196
+ node = via.get(node);
1197
+ }
1198
+ let tailEnd = walk2.length;
1199
+ let length = node === void 0 ? 0 : lengths.get(node) ?? 0;
1200
+ if (node !== void 0 && onWalk.has(node)) {
1201
+ const from = onWalk.get(node);
1202
+ length = walk2.length - from;
1203
+ for (let k = from; k < walk2.length; k += 1) lengths.set(walk2[k], length);
1204
+ tailEnd = from;
1205
+ }
1206
+ for (let k = tailEnd - 1; k >= 0; k -= 1) {
1207
+ length += 1;
1208
+ lengths.set(walk2[k], length);
1209
+ }
1210
+ }
1211
+ return lengths;
1212
+ }
893
1213
  function checkIncludeTargets(text, idx, known, out) {
894
1214
  const set = new Set(known);
895
1215
  INCLUDE_RE.lastIndex = 0;
@@ -936,6 +1256,13 @@ function addAll(target, text, re, fold) {
936
1256
  }
937
1257
  }
938
1258
 
1259
+ // src/internal/charclass.ts
1260
+ var UCP_SPACE = "\\t\\n\\x0B\\f\\r \\x85\\xA0\\u1680\\u180E\\u2000-\\u200A\\u2028\\u2029\\u202F\\u205F\\u3000";
1261
+ function isUcpSpace(code) {
1262
+ 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;
1263
+ }
1264
+ var UCP_WORD = "\\p{L}\\p{N}\\p{Mn}\\p{Pc}";
1265
+
939
1266
  // src/internal/errors.ts
940
1267
  var SpintaxError = class extends Error {
941
1268
  constructor(message) {
@@ -965,6 +1292,120 @@ var NotImplementedError = class extends SpintaxError {
965
1292
  }
966
1293
  };
967
1294
 
1295
+ // src/internal/fragment.ts
1296
+ var FLAT_BELOW = 128;
1297
+ var Join = class {
1298
+ constructor(pieces) {
1299
+ this.pieces = pieces;
1300
+ let length = 0;
1301
+ for (const piece of pieces) length += lengthOf(piece);
1302
+ this.length = length;
1303
+ let lead = 0;
1304
+ for (const piece of pieces) {
1305
+ const pieceLead = leadOf(piece);
1306
+ lead += pieceLead;
1307
+ if (pieceLead < lengthOf(piece)) break;
1308
+ }
1309
+ this.lead = lead;
1310
+ let trail = 0;
1311
+ if (lead < length) {
1312
+ for (let k = pieces.length - 1; k >= 0; k -= 1) {
1313
+ const piece = pieces[k];
1314
+ const pieceTrail = trailOf(piece);
1315
+ trail += pieceTrail;
1316
+ if (pieceTrail < lengthOf(piece)) break;
1317
+ }
1318
+ } else {
1319
+ trail = length;
1320
+ }
1321
+ this.trail = trail;
1322
+ }
1323
+ pieces;
1324
+ length;
1325
+ lead;
1326
+ trail;
1327
+ };
1328
+ var Window = class {
1329
+ constructor(base, from, to) {
1330
+ this.base = base;
1331
+ this.from = from;
1332
+ this.to = to;
1333
+ this.length = to - from;
1334
+ }
1335
+ base;
1336
+ from;
1337
+ to;
1338
+ length;
1339
+ lead = 0;
1340
+ trail = 0;
1341
+ };
1342
+ function lengthOf(fragment) {
1343
+ return fragment.length;
1344
+ }
1345
+ function leadOf(fragment) {
1346
+ if (typeof fragment !== "string") return fragment.lead;
1347
+ let k = 0;
1348
+ while (k < fragment.length && isPhpTrimChar(fragment.charCodeAt(k))) k += 1;
1349
+ return k;
1350
+ }
1351
+ function trailOf(fragment) {
1352
+ if (typeof fragment !== "string") return fragment.trail;
1353
+ let k = fragment.length;
1354
+ while (k > 0 && isPhpTrimChar(fragment.charCodeAt(k - 1))) k -= 1;
1355
+ return fragment.length - k;
1356
+ }
1357
+ function joinFragments(pieces) {
1358
+ if (pieces.length === 0) return "";
1359
+ if (pieces.length === 1) return pieces[0];
1360
+ let total = 0;
1361
+ let flat = true;
1362
+ for (const piece of pieces) {
1363
+ total += lengthOf(piece);
1364
+ if (typeof piece !== "string") flat = false;
1365
+ }
1366
+ if (flat && total < FLAT_BELOW) return pieces.join("");
1367
+ return new Join(pieces);
1368
+ }
1369
+ function trimFragment(fragment) {
1370
+ if (typeof fragment === "string") {
1371
+ let start = 0;
1372
+ while (start < fragment.length && isPhpTrimChar(fragment.charCodeAt(start))) start += 1;
1373
+ let end = fragment.length;
1374
+ while (end > start && isPhpTrimChar(fragment.charCodeAt(end - 1))) end -= 1;
1375
+ return start === 0 && end === fragment.length ? fragment : fragment.slice(start, end);
1376
+ }
1377
+ if (fragment.lead === fragment.length) return "";
1378
+ if (fragment.lead === 0 && fragment.trail === 0) return fragment;
1379
+ return new Window(fragment, fragment.lead, fragment.length - fragment.trail);
1380
+ }
1381
+ function flatten(fragment) {
1382
+ if (typeof fragment === "string") return fragment;
1383
+ const out = [];
1384
+ const stack = [[fragment, 0, fragment.length]];
1385
+ while (stack.length > 0) {
1386
+ const [piece, from, to] = stack.pop();
1387
+ if (from >= to) continue;
1388
+ if (typeof piece === "string") {
1389
+ out.push(from === 0 && to === piece.length ? piece : piece.slice(from, to));
1390
+ } else if (piece instanceof Window) {
1391
+ stack.push([piece.base, piece.from + from, piece.from + to]);
1392
+ } else {
1393
+ const wanted = [];
1394
+ let offset = 0;
1395
+ for (const child of piece.pieces) {
1396
+ const length = lengthOf(child);
1397
+ const a = Math.max(from, offset);
1398
+ const b = Math.min(to, offset + length);
1399
+ if (a < b) wanted.push([child, a - offset, b - offset]);
1400
+ offset += length;
1401
+ if (offset >= to) break;
1402
+ }
1403
+ for (let k = wanted.length - 1; k >= 0; k -= 1) stack.push(wanted[k]);
1404
+ }
1405
+ }
1406
+ return out.join("");
1407
+ }
1408
+
968
1409
  // src/internal/render.ts
969
1410
  var MAX_VARIABLE_DEPTH = 50;
970
1411
  var MAX_EXPANSION_CHARS = 1024 * 1024;
@@ -978,7 +1419,7 @@ function renderAst(ast, ctx) {
978
1419
  onPluralError: ctx.onPluralError,
979
1420
  budget: ctx.budget
980
1421
  };
981
- const vars = Object.keys(ast.defDefs).length > 0 ? { ...base, ...rollDefinitions(ast.defDefs, base, ctx.runtimeContext, walkOpts) } : base;
1422
+ const vars = Object.keys(ast.defDefs).length > 0 ? nameMap(base, rollDefinitions(ast.defDefs, base, ctx.runtimeContext, walkOpts)) : base;
982
1423
  const text = renderNodes(ast.nodes, { ...walkOpts, vars });
983
1424
  return ctx.resolver ? resolveIncludes(text, ctx) : text;
984
1425
  }
@@ -1002,7 +1443,7 @@ function resolveIncludes(text, ctx) {
1002
1443
  });
1003
1444
  }
1004
1445
  function buildVars(setDefs, context) {
1005
- const vars = {};
1446
+ const vars = nameMap();
1006
1447
  for (const [name, value] of Object.entries(setDefs)) {
1007
1448
  vars[name] = value;
1008
1449
  }
@@ -1013,48 +1454,68 @@ function buildVars(setDefs, context) {
1013
1454
  }
1014
1455
  function rollDefinitions(defDefs, vars, context, opts) {
1015
1456
  const outranked = new Set(Object.keys(context).map((key) => key.toLowerCase()));
1016
- const rolled = {};
1017
- const aliases = {};
1457
+ const rolled = nameMap();
1458
+ const definitions = nameMap(defDefs);
1459
+ const aliases = nameMap();
1018
1460
  for (const [name, value] of Object.entries(vars)) {
1019
- if (name in defDefs && !outranked.has(name)) continue;
1461
+ if (name in definitions && !outranked.has(name)) continue;
1020
1462
  aliases[name] = value;
1021
1463
  }
1022
- for (const name of orderDefinitions(defDefs, aliases)) {
1464
+ const visible = nameMap(vars);
1465
+ for (const name of orderDefinitions(definitions, aliases)) {
1023
1466
  if (outranked.has(name)) continue;
1024
- const value = defDefs[name] ?? "";
1025
- rolled[name] = renderNodes(parseSequence(value), { ...opts, vars: { ...vars, ...rolled } });
1467
+ const value = renderNodes(parseSequence(definitions[name] ?? ""), { ...opts, vars: visible });
1468
+ rolled[name] = value;
1469
+ visible[name] = value;
1470
+ truthinessByVars.delete(visible);
1026
1471
  }
1027
1472
  return rolled;
1028
1473
  }
1029
1474
  function orderDefinitions(defDefs, aliases) {
1030
1475
  const names = Object.keys(defDefs);
1031
- const blocked = /* @__PURE__ */ new Map();
1476
+ const position = new Map(names.map((name, i) => [name, i]));
1477
+ const unplaced = /* @__PURE__ */ new Map();
1478
+ const waitingOn = /* @__PURE__ */ new Map();
1032
1479
  for (const name of names) {
1033
- const reached = referencedNames(defDefs[name] ?? "", aliases);
1034
- blocked.set(name, new Set(names.filter((candidate) => reached.has(candidate))));
1480
+ let count = 0;
1481
+ for (const dep of referencedNames(defDefs[name] ?? "", aliases)) {
1482
+ if (dep === name || !position.has(dep)) continue;
1483
+ count += 1;
1484
+ const waiting = waitingOn.get(dep);
1485
+ if (waiting === void 0) waitingOn.set(dep, [name]);
1486
+ else waiting.push(name);
1487
+ }
1488
+ unplaced.set(name, count);
1035
1489
  }
1036
1490
  const ordered = [];
1037
- let pending = names;
1038
- while (pending.length > 0) {
1039
- const ready = pending.filter((name) => {
1040
- const deps = blocked.get(name);
1041
- return !deps || ![...deps].some((dep) => dep !== name && pending.includes(dep));
1042
- });
1043
- if (ready.length === 0) return [...ordered, ...pending];
1044
- ordered.push(...ready);
1045
- pending = pending.filter((name) => !ready.includes(name));
1491
+ let round = names.filter((name) => unplaced.get(name) === 0);
1492
+ while (round.length > 0) {
1493
+ const next = [];
1494
+ for (const name of round) {
1495
+ ordered.push(name);
1496
+ for (const waiting of waitingOn.get(name) ?? []) {
1497
+ const left = unplaced.get(waiting) - 1;
1498
+ unplaced.set(waiting, left);
1499
+ if (left === 0) next.push(waiting);
1500
+ }
1501
+ }
1502
+ round = next.sort((a, b) => position.get(a) - position.get(b));
1503
+ }
1504
+ if (ordered.length < names.length) {
1505
+ const placed = new Set(ordered);
1506
+ for (const name of names) if (!placed.has(name)) ordered.push(name);
1046
1507
  }
1047
1508
  return ordered;
1048
1509
  }
1049
1510
  function referencedNames(value, aliases) {
1050
1511
  const seen = /* @__PURE__ */ new Set();
1051
1512
  const queue = directReferences(value);
1052
- while (queue.length > 0) {
1053
- const name = queue.shift();
1513
+ for (let i = 0; i < queue.length; i += 1) {
1514
+ const name = queue[i];
1054
1515
  if (seen.has(name)) continue;
1055
1516
  seen.add(name);
1056
1517
  const alias = aliases[name];
1057
- if (alias !== void 0) queue.push(...directReferences(alias));
1518
+ if (alias !== void 0) for (const ref of directReferences(alias)) queue.push(ref);
1058
1519
  }
1059
1520
  return seen;
1060
1521
  }
@@ -1062,6 +1523,9 @@ function directReferences(text) {
1062
1523
  return [...text.matchAll(/%(\w+)%/gu)].map((match) => (match[1] ?? "").toLowerCase());
1063
1524
  }
1064
1525
  function renderNodes(nodes, opts) {
1526
+ return flatten(renderFragment(nodes, opts));
1527
+ }
1528
+ function renderFragment(nodes, opts) {
1065
1529
  const frame = (list) => ({ nodes: list, i: 0, out: [], pending: null });
1066
1530
  const stack = [frame(nodes)];
1067
1531
  while (stack.length > 0) {
@@ -1076,7 +1540,7 @@ function renderNodes(nodes, opts) {
1076
1540
  continue;
1077
1541
  }
1078
1542
  if (f.i >= f.nodes.length) {
1079
- const text = f.out.join("");
1543
+ const text = joinFragments(f.out);
1080
1544
  stack.pop();
1081
1545
  const parent = stack[stack.length - 1];
1082
1546
  if (parent === void 0) return text;
@@ -1086,11 +1550,12 @@ function renderNodes(nodes, opts) {
1086
1550
  const node = f.nodes[f.i];
1087
1551
  f.i += 1;
1088
1552
  const step = renderNode(node, opts);
1089
- if (typeof step === "string") f.out.push(step);
1553
+ if (typeof step === "string" || !("lists" in step)) f.out.push(step);
1090
1554
  else f.pending = step;
1091
1555
  }
1092
1556
  return "";
1093
1557
  }
1558
+ var single = (list) => ({ lists: [list], done: [], assemble: (parts) => parts[0] ?? "" });
1094
1559
  function renderNode(node, opts) {
1095
1560
  switch (node.type) {
1096
1561
  case "literal":
@@ -1116,7 +1581,7 @@ function resolveVariable(name, opts) {
1116
1581
  if (opts.budget.left <= 0) return `%${name}%`;
1117
1582
  opts.budget.left -= value.length;
1118
1583
  if (opts.depth >= MAX_VARIABLE_DEPTH || !/[{[%]/u.test(value)) return value;
1119
- return renderNodes(parseSequence(value), { ...opts, depth: opts.depth + 1 });
1584
+ return renderFragment(parseSequence(value), { ...opts, depth: opts.depth + 1 });
1120
1585
  }
1121
1586
  function passesLeft(opts) {
1122
1587
  return Math.max(1, MAX_VARIABLE_DEPTH - opts.depth + 1);
@@ -1140,18 +1605,29 @@ function expandVarsFixpoint(text, opts, passes) {
1140
1605
  }
1141
1606
  return { text: out, changed: changedAny, converged: false };
1142
1607
  }
1608
+ var NON_SPACE_RE = new RegExp(`[^${UCP_SPACE}]`, "u");
1609
+ var truthinessByVars = /* @__PURE__ */ new WeakMap();
1143
1610
  function conditionalTakesThen(name, inverted, opts) {
1144
- const value = opts.vars[name.toLowerCase()];
1145
- const baseTruthy = value !== void 0 && /\S/u.test(value);
1611
+ const key = name.toLowerCase();
1612
+ let known = truthinessByVars.get(opts.vars);
1613
+ if (known === void 0) truthinessByVars.set(opts.vars, known = /* @__PURE__ */ new Map());
1614
+ let baseTruthy = known.get(key);
1615
+ if (baseTruthy === void 0) {
1616
+ const value = opts.vars[key];
1617
+ baseTruthy = value !== void 0 && NON_SPACE_RE.test(value);
1618
+ known.set(key, baseTruthy);
1619
+ }
1146
1620
  return inverted ? !baseTruthy : baseTruthy;
1147
1621
  }
1148
1622
  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] ?? "" };
1623
+ return single(conditionalTakesThen(node.name, node.inverted, opts) ? node.then : node.else);
1151
1624
  }
1152
1625
  function resolveConditionalsInText(text, opts) {
1153
1626
  if (!text.includes("{?")) return text;
1154
- const close = matchBraces(text);
1627
+ const close = matchPairs(text, BRACE_OPEN, BRACE_CLOSE);
1628
+ const any = matchAnyPairs(text);
1629
+ const heads = [];
1630
+ for (let at = text.indexOf("{?"); at !== -1; at = text.indexOf("{?", at + 1)) heads.push(at);
1155
1631
  const out = [];
1156
1632
  const pending = [[0, text.length]];
1157
1633
  while (pending.length > 0) {
@@ -1159,13 +1635,14 @@ function resolveConditionalsInText(text, opts) {
1159
1635
  const segEnd = segment[1];
1160
1636
  let i = segment[0];
1161
1637
  while (i < segEnd) {
1162
- const open = text.indexOf("{?", i);
1638
+ const next = lowerBound(heads, i);
1639
+ const open = next < heads.length ? heads[next] : -1;
1163
1640
  if (open === -1 || open + 1 >= segEnd) {
1164
1641
  out.push(text.slice(i, segEnd));
1165
1642
  break;
1166
1643
  }
1167
1644
  const shut = close[open] ?? -1;
1168
- const head = shut === -1 || shut >= segEnd ? null : recognizeConditional(text, open + 1, shut);
1645
+ const head = shut === -1 || shut >= segEnd ? null : recognizeConditional(text, open + 1, shut, any);
1169
1646
  if (head === null) {
1170
1647
  out.push(text.slice(i, Math.min(open + 2, segEnd)));
1171
1648
  i = open + 2;
@@ -1181,20 +1658,6 @@ function resolveConditionalsInText(text, opts) {
1181
1658
  }
1182
1659
  return out.join("");
1183
1660
  }
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
1661
  function renderPlural(node, opts) {
1199
1662
  const passes = passesLeft(opts);
1200
1663
  const countPass = expandVarsFixpoint(node.countRaw, opts, passes);
@@ -1212,7 +1675,7 @@ function renderPlural(node, opts) {
1212
1675
  });
1213
1676
  return fullwidthVerbatim(countRaw, formsRaw);
1214
1677
  }
1215
- const count = phpTrim2(countRaw);
1678
+ const count = phpTrim(countRaw);
1216
1679
  if (!/^-?\d+$/u.test(count)) {
1217
1680
  report({
1218
1681
  code: "plural.count",
@@ -1222,7 +1685,7 @@ function renderPlural(node, opts) {
1222
1685
  });
1223
1686
  return "";
1224
1687
  }
1225
- const forms = formsRaw.split("|").map((f) => phpTrim2(f));
1688
+ const forms = formsRaw.split("|").map((f) => phpTrim(f));
1226
1689
  if (forms.length !== pluralArity(base)) {
1227
1690
  report({
1228
1691
  code: "plural.arity",
@@ -1235,8 +1698,8 @@ function renderPlural(node, opts) {
1235
1698
  return fullwidthVerbatim(countRaw, formsRaw);
1236
1699
  }
1237
1700
  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] ?? "" };
1701
+ if (!formsPass.converged) return renderFragment(parseSequence(picked), { ...opts, frozen: true });
1702
+ return single(parseSequence(picked));
1240
1703
  }
1241
1704
  function rawConstruct(countRaw, formsRaw) {
1242
1705
  return `{plural ${countRaw}:${formsRaw}}`;
@@ -1245,25 +1708,31 @@ function fullwidthVerbatim(countRaw, formsRaw) {
1245
1708
  return rawConstruct(countRaw, formsRaw).replace(/\{/gu, "\uFF5B").replace(/\}/gu, "\uFF5D");
1246
1709
  }
1247
1710
  function renderEnumeration(node, opts) {
1248
- const spliced = node.raw === void 0 ? null : spliceConstruct(node.raw, "{", "}", opts);
1711
+ const spliced = node.raw === void 0 ? null : spliceConstruct(node, node.raw, "{", "}", opts);
1249
1712
  if (spliced !== null) return spliced;
1250
1713
  const { options } = node;
1251
1714
  if (options.length === 0) return "";
1252
1715
  const picked = options[randomInt(opts.rng, 0, options.length - 1)];
1253
1716
  if (!picked) return "";
1254
- return { lists: [picked], done: [], assemble: (parts) => parts[0] ?? "" };
1717
+ return single(picked);
1255
1718
  }
1256
- function spliceConstruct(raw, open, close, opts) {
1719
+ function spliceConstruct(node, raw, open, close, opts) {
1257
1720
  if (opts.frozen === true) return null;
1721
+ const span2 = rereadSpans.get(node);
1722
+ if (span2 !== void 0 && !rereadCouldChange(span2, opts)) return null;
1258
1723
  const expanded = expandVarsFixpoint(resolveConditionalsInText(raw, opts), opts, passesLeft(opts));
1259
1724
  const body = resolveConditionalsInText(expanded.text, opts);
1260
1725
  if (body === raw) return null;
1261
1726
  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] ?? "" };
1727
+ if (!expanded.converged) return renderFragment(nodes, { ...opts, frozen: true });
1728
+ return single(nodes);
1729
+ }
1730
+ function rereadCouldChange({ index, start, end }, opts) {
1731
+ if (index.hasConditionalWithin(start, end)) return true;
1732
+ return !(opts.budget.left <= 0) && index.hasDefinedReferenceWithin(start, end, opts.vars);
1264
1733
  }
1265
1734
  function renderPermutation(node, opts) {
1266
- const spliced = node.raw === void 0 ? null : spliceConstruct(node.raw, "[", "]", opts);
1735
+ const spliced = node.raw === void 0 ? null : spliceConstruct(node, node.raw, "[", "]", opts);
1267
1736
  if (spliced !== null) return spliced;
1268
1737
  if (node.options.length === 0) return "";
1269
1738
  return {
@@ -1273,7 +1742,11 @@ function renderPermutation(node, opts) {
1273
1742
  };
1274
1743
  }
1275
1744
  function assemblePermutation(node, rendered, opts) {
1276
- const elements = node.options.map((o, i) => ({ text: rendered[i] ?? "", sep: o.separator }));
1745
+ const elements = [];
1746
+ node.options.forEach((o, i) => {
1747
+ const text = trimFragment(rendered[i] ?? "");
1748
+ if (lengthOf(text) !== 0) elements.push({ text, sep: o.separator });
1749
+ });
1277
1750
  const total = elements.length;
1278
1751
  if (total === 0) return "";
1279
1752
  const { config } = node;
@@ -1312,25 +1785,20 @@ function joinWithSeparators(elements, globalSep, globalLastsep) {
1312
1785
  const count = elements.length;
1313
1786
  if (count === 0) return "";
1314
1787
  if (count === 1) return elements[0].text;
1315
- let out = elements[0].text;
1788
+ const pieces = [elements[0].text];
1316
1789
  for (let i = 1; i < count; i += 1) {
1317
1790
  const el = elements[i];
1318
1791
  const sep = el.sep ?? (i === count - 1 ? globalLastsep : globalSep);
1319
- out += padSeparator(sep) + el.text;
1792
+ pieces.push(padSeparator(sep), el.text);
1320
1793
  }
1321
- return out;
1794
+ return joinFragments(pieces);
1322
1795
  }
1323
1796
  function padSeparator(sep) {
1324
- const trimmed = phpTrim2(sep);
1797
+ const trimmed = phpTrim(sep);
1325
1798
  if (trimmed === "") return sep;
1326
1799
  if (/^\p{L}+$/u.test(trimmed)) return ` ${trimmed} `;
1327
1800
  return sep;
1328
1801
  }
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
1802
 
1335
1803
  // src/internal/postprocess.ts
1336
1804
  var SINGLE_ABBREVS = [
@@ -1383,41 +1851,182 @@ var SINGLE_ABBREVS = [
1383
1851
  "Ave",
1384
1852
  "Blvd"
1385
1853
  ];
1386
- var WS = " \\t\\r\\n\\f\\x0B";
1854
+ var WS = UCP_SPACE;
1387
1855
  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})";
1856
+ var AFTER_NON_WORD = `(?<![${UCP_WORD}])`;
1857
+ var WORD_BOUNDARY = `(?:(?<=[${UCP_WORD}])(?![${UCP_WORD}])|(?<![${UCP_WORD}])(?=[${UCP_WORD}]))`;
1858
+ var XN = "[xX][nN]--";
1859
+ var LABEL = `(?:${XN})?[\\p{L}\\p{N}]+(?:-[\\p{L}\\p{N}]+)*`;
1860
+ var TLD_LOWER = "\\p{Ll}\\p{Lm}\\p{Lo}";
1861
+ var TLD_UPPER = "\\p{Lu}\\p{Lt}\\p{Lm}\\p{Lo}";
1862
+ var TLD = `(?:${XN}[a-zA-Z0-9\\-\\u017F\\u212A]{2,59}|[${TLD_LOWER}][${TLD_LOWER}\\p{N}-]{1,62}|[${TLD_UPPER}][${TLD_UPPER}\\p{N}-]{1,62})`;
1863
+ var DOMAIN_PART = `(?:${LABEL}\\.)+${TLD}`;
1389
1864
  var URI_BODY = `[^\\x00${WS}<>"')\\]]`;
1390
1865
  var URI_RE = new RegExp(`(?:(?:https?|ftp):\\/\\/|(?:mailto|tel):)${URI_BODY}+`, "giu");
1391
1866
  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");
1867
+ var DOMAIN_AT_RE = new RegExp(`${DOMAIN_PART}${WORD_BOUNDARY}`, "uy");
1868
+ var DOMAIN_SCAN_RE = new RegExp(`${AFTER_NON_WORD}(?:(${DOMAIN_PART}${WORD_BOUNDARY})|(?:${LABEL}\\.)*${LABEL})`, "gu");
1394
1869
  var DECIMAL_RE = /\b\d+\.\d+\b/gu;
1395
- var MULTI_ABBR_RE = new RegExp(`\\b(?:\\p{L}{1,2}\\.${S}*){2,}`, "gu");
1870
+ var MULTI_ABBR_RE = new RegExp(`${AFTER_NON_WORD}(?:\\p{L}{1,2}\\.${S}*){2,}`, "gu");
1396
1871
  var SINGLE_ABBR_RE = new RegExp(`(?<![\\p{L}\\p{N}])(?:${SINGLE_ABBREVS.join("|")})\\.(?=${S}|$|<)`, "giu");
1397
- var TRAILING_PUNCT_RE = /([.,;:!]+)$/u;
1872
+ function isEmailLocalChar(code) {
1873
+ 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;
1874
+ }
1875
+ function shieldEmails(text, shield) {
1876
+ let out = "";
1877
+ let emitted = 0;
1878
+ let pos = 0;
1879
+ for (let at = text.indexOf("@"); at !== -1; at = text.indexOf("@", Math.max(at + 1, pos))) {
1880
+ let start = at;
1881
+ while (start > pos && isEmailLocalChar(text.charCodeAt(start - 1))) start -= 1;
1882
+ if (start === at) continue;
1883
+ DOMAIN_AT_RE.lastIndex = at + 1;
1884
+ if (DOMAIN_AT_RE.exec(text) !== null) {
1885
+ out += text.slice(emitted, start) + shield(text.slice(start, DOMAIN_AT_RE.lastIndex));
1886
+ emitted = pos = DOMAIN_AT_RE.lastIndex;
1887
+ }
1888
+ }
1889
+ return emitted === 0 ? text : out + text.slice(emitted);
1890
+ }
1891
+ var DOMAIN_DOT_RE = /\.[\p{L}\p{N}]/u;
1892
+ function shieldDomains(text, shield) {
1893
+ if (!DOMAIN_DOT_RE.test(text)) return text;
1894
+ let out = "";
1895
+ let emitted = 0;
1896
+ DOMAIN_SCAN_RE.lastIndex = 0;
1897
+ for (let m = DOMAIN_SCAN_RE.exec(text); m !== null; m = DOMAIN_SCAN_RE.exec(text)) {
1898
+ if (m[1] !== void 0) {
1899
+ out += text.slice(emitted, m.index) + shield(m[1]);
1900
+ emitted = m.index + m[1].length;
1901
+ }
1902
+ }
1903
+ return emitted === 0 ? text : out + text.slice(emitted);
1904
+ }
1905
+ function trailingPunctuationStart(value) {
1906
+ let cut = value.length;
1907
+ while (cut > 0 && ".,;:!".includes(value.charAt(cut - 1))) cut -= 1;
1908
+ return cut;
1909
+ }
1398
1910
  var SENTENCE_OPENERS = "\xBF\xA1";
1399
1911
  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");
1912
+ var SPACE_BEFORE_PUNCT_RE = new RegExp(`(?<!${S})${S}+([,;:!?.])`, "gu");
1913
+ var SPACE_AFTER_COMMA_RE = new RegExp(`([,;:])(?!\\p{Nd})(?!${S}|$|<)`, "gu");
1914
+ var SPACE_AFTER_SENTENCE_RE = new RegExp(`(?<![.!?])([.!?]+)(?![.!?])(?!\\p{Nd})(?!${S}|$|<)`, "gu");
1403
1915
  var SPACE_AFTER_OPENER_RE = new RegExp(`([${SENTENCE_OPENERS}])${S}+`, "gu");
1404
1916
  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
1917
  var up = (ch) => ch.toUpperCase();
1918
+ function indexLeads(text) {
1919
+ const n = text.length;
1920
+ const leadEnd = new Int32Array(n + 1);
1921
+ leadEnd[n] = n;
1922
+ let gt = -1;
1923
+ for (let i = n - 1; i >= 0; i -= 1) {
1924
+ const code = text.charCodeAt(i);
1925
+ let tokenEnd = -1;
1926
+ if (code === 191 || code === 161 || isUcpSpace(code)) {
1927
+ tokenEnd = i + 1;
1928
+ } else if (code === 60 && gt > i + 1) {
1929
+ tokenEnd = gt + 1;
1930
+ }
1931
+ leadEnd[i] = tokenEnd === -1 ? i : leadEnd[tokenEnd];
1932
+ if (code === 62) gt = i;
1933
+ }
1934
+ return { leadEnd };
1935
+ }
1936
+ var LOWER_RE = /^\p{Ll}/u;
1937
+ var BLOCK_TAG_NAME_RE = /<\/?(?:p|h[1-6]|li|blockquote|div|td|th)/iuy;
1938
+ var SENTENCE_END_SCAN_RE = new RegExp(`[.!?${String.fromCharCode(8230)}]`, "g");
1939
+ var LEAD_WALK = 32;
1940
+ function leadEndFrom(text, i, leads) {
1941
+ let j = i;
1942
+ for (let steps = 0; steps < LEAD_WALK && j < text.length; steps += 1) {
1943
+ const code = text.charCodeAt(j);
1944
+ if (code === 60) return leads().leadEnd[j];
1945
+ if (code !== 191 && code !== 161 && !isUcpSpace(code)) return j;
1946
+ j += 1;
1947
+ }
1948
+ return j < text.length ? leads().leadEnd[j] : j;
1949
+ }
1950
+ function capitalizeAfter(text, lower, next, context) {
1951
+ const leads = () => {
1952
+ if (context.leads === null || context.leads.leadEnd.length !== text.length + 1) context.leads = indexLeads(text);
1953
+ return context.leads;
1954
+ };
1955
+ let out = "";
1956
+ let emitted = 0;
1957
+ let from = 0;
1958
+ for (let boundary = next(text, from); boundary !== null; boundary = next(text, from)) {
1959
+ from = boundary.resume;
1960
+ const at = leadEndFrom(text, boundary.leadStart, leads);
1961
+ const cp = text.codePointAt(at);
1962
+ if (cp === void 0) continue;
1963
+ const ch = String.fromCodePoint(cp);
1964
+ if (!lower.test(ch)) continue;
1965
+ out += text.slice(emitted, at) + up(ch);
1966
+ emitted = at + ch.length;
1967
+ from = emitted;
1968
+ }
1969
+ return emitted === 0 ? text : out + text.slice(emitted);
1970
+ }
1971
+ var afterSentenceEnd = (text, from) => {
1972
+ SENTENCE_END_SCAN_RE.lastIndex = from;
1973
+ const m = SENTENCE_END_SCAN_RE.exec(text);
1974
+ return m === null ? null : { leadStart: m.index + 1, resume: m.index + 1 };
1975
+ };
1976
+ function afterBlockTag() {
1977
+ let gtFrom = -1;
1978
+ let gt = -1;
1979
+ return (text, from) => {
1980
+ for (let lt = text.indexOf("<", from); lt !== -1; lt = text.indexOf("<", lt + 1)) {
1981
+ BLOCK_TAG_NAME_RE.lastIndex = lt;
1982
+ if (!BLOCK_TAG_NAME_RE.test(text)) continue;
1983
+ const nameEnd = BLOCK_TAG_NAME_RE.lastIndex;
1984
+ if (gtFrom === -1 || nameEnd < gtFrom || gt !== -1 && nameEnd > gt) {
1985
+ gtFrom = nameEnd;
1986
+ gt = text.indexOf(">", nameEnd);
1987
+ }
1988
+ if (gt === -1) continue;
1989
+ return { leadStart: gt + 1, resume: lt + 1 };
1990
+ }
1991
+ return null;
1992
+ };
1993
+ }
1994
+ var afterLineBreak = (text, from) => {
1995
+ const at = text.indexOf("\n", from);
1996
+ return at === -1 ? null : { leadStart: at + 1, resume: at + 1 };
1997
+ };
1412
1998
  var SHIELD_PREFIXES = ["URL", "URI", "EMAIL", "DOM", "NUM", "ABBR"];
1413
1999
  var RESTORE_RE2 = new RegExp(`\\x00(?:${SHIELD_PREFIXES.join("|")})_\\d+\\x00`, "gu");
1414
2000
  function restore(text, input, placeholders) {
1415
2001
  if (!input.includes("\0")) {
1416
2002
  return text.replace(RESTORE_RE2, (key) => placeholders.get(key) ?? key);
1417
2003
  }
1418
- let out = text;
1419
- for (const [key, value] of placeholders) {
1420
- out = out.split(key).join(value);
2004
+ return restoreAsTheLoopDoes(text, placeholders);
2005
+ }
2006
+ function restoreAsTheLoopDoes(text, placeholders) {
2007
+ const gaps = text.split("\0");
2008
+ const rank = /* @__PURE__ */ new Map();
2009
+ for (const key of placeholders.keys()) rank.set(key, rank.size);
2010
+ const byRank = [];
2011
+ for (let j = 1; j < gaps.length - 1; j += 1) {
2012
+ const r = rank.get(`\0${gaps[j]}\0`);
2013
+ if (r !== void 0) (byRank[r] ??= []).push(j);
2014
+ }
2015
+ const taken = new Uint8Array(gaps.length);
2016
+ const replaced = new Uint8Array(gaps.length);
2017
+ for (const candidates of byRank) {
2018
+ for (const j of candidates ?? []) {
2019
+ if (taken[j - 1] === 1 || taken[j] === 1) continue;
2020
+ taken[j - 1] = 1;
2021
+ taken[j] = 1;
2022
+ replaced[j] = 1;
2023
+ }
2024
+ }
2025
+ let out = gaps[0];
2026
+ for (let j = 1; j < gaps.length; j += 1) {
2027
+ if (taken[j - 1] !== 1) out += "\0";
2028
+ const gap = gaps[j];
2029
+ out += replaced[j] === 1 ? placeholders.get(`\0${gap}\0`) : gap;
1421
2030
  }
1422
2031
  return out;
1423
2032
  }
@@ -1431,21 +2040,17 @@ function postProcess(input) {
1431
2040
  return key;
1432
2041
  };
1433
2042
  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);
2043
+ const cut = trailingPunctuationStart(value);
2044
+ if (cut === value.length) return store(value, prefix);
2045
+ return cut === 0 ? value : store(value.slice(0, cut), prefix) + value.slice(cut);
1441
2046
  };
1442
2047
  let text = input;
1443
2048
  text = text.replace(
1444
2049
  URI_RE,
1445
2050
  (m) => storeWithTrailingPunct(m, MAILTEL_PREFIX_RE.test(m) ? "URI" : "URL")
1446
2051
  );
1447
- text = text.replace(EMAIL_RE, (m) => store(m, "EMAIL"));
1448
- text = text.replace(DOMAIN_RE, (m) => store(m, "DOM"));
2052
+ text = shieldEmails(text, (m) => store(m, "EMAIL"));
2053
+ text = shieldDomains(text, (m) => store(m, "DOM"));
1449
2054
  text = text.replace(DECIMAL_RE, (m) => store(m, "NUM"));
1450
2055
  text = text.replace(MULTI_ABBR_RE, (m) => store(m, "ABBR"));
1451
2056
  text = text.replace(SINGLE_ABBR_RE, (m) => store(m, "ABBR"));
@@ -1455,9 +2060,10 @@ function postProcess(input) {
1455
2060
  text = text.replace(SPACE_AFTER_SENTENCE_RE, "$1 ");
1456
2061
  text = text.replace(SPACE_AFTER_OPENER_RE, "$1");
1457
2062
  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));
2063
+ const leadContext = { leads: null };
2064
+ text = capitalizeAfter(text, LOWER_RE, afterSentenceEnd, leadContext);
2065
+ text = capitalizeAfter(text, LOWER_RE, afterBlockTag(), leadContext);
2066
+ text = capitalizeAfter(text, LOWER_RE, afterLineBreak, leadContext);
1461
2067
  return restore(text, input, placeholders).trim();
1462
2068
  }
1463
2069