@spintax/core 0.12.0 → 0.14.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.d.cts CHANGED
@@ -29,8 +29,12 @@
29
29
  * 4 — `raw` is kept on more constructs: one holding a `{?…}` conditional, and a permutation with a
30
30
  * reference anywhere in its `<config>` (#80). The shape is 3's, but an `Ast` cached by version 3
31
31
  * lacks `raw` exactly where the new rule needs it, and would render `[{?f?a|b|x}|c]` with a raw `|`.
32
+ *
33
+ * 5 — `/! … !/` verbatim spans (#92, ADR 0001) are encoded into the text as tokens at parse time.
34
+ * The shape is 4's, but an `Ast` parsed by version 4 carries no tokens, and would spin `/!{a|b}!/`
35
+ * instead of emitting it.
32
36
  */
33
- declare const AST_VERSION = 4;
37
+ declare const AST_VERSION = 5;
34
38
  /** Opaque public handle (re-exported as `Ast` from the package index). */
35
39
  interface Ast {
36
40
  readonly astVersion: typeof AST_VERSION;
@@ -95,7 +99,8 @@ interface PluralIssue {
95
99
  * lets the deterministic parity fixtures assert exact output.
96
100
  *
97
101
  * Order: strip stray sentinels → build vars, roll #def, tree-walk (+ #include) →
98
- * cosmetic post-process (if on) → mandatory neutralize safety-restore (always).
102
+ * cosmetic post-process (if on) → mandatory neutralize safety-restore (always) → decode the
103
+ * `/! … !/` spans (always).
99
104
  */
100
105
 
101
106
  /** Default for {@link PipelineOptions.maxDepth} (#include + parse-nesting guard). */
package/dist/index.d.ts CHANGED
@@ -29,8 +29,12 @@
29
29
  * 4 — `raw` is kept on more constructs: one holding a `{?…}` conditional, and a permutation with a
30
30
  * reference anywhere in its `<config>` (#80). The shape is 3's, but an `Ast` cached by version 3
31
31
  * lacks `raw` exactly where the new rule needs it, and would render `[{?f?a|b|x}|c]` with a raw `|`.
32
+ *
33
+ * 5 — `/! … !/` verbatim spans (#92, ADR 0001) are encoded into the text as tokens at parse time.
34
+ * The shape is 4's, but an `Ast` parsed by version 4 carries no tokens, and would spin `/!{a|b}!/`
35
+ * instead of emitting it.
32
36
  */
33
- declare const AST_VERSION = 4;
37
+ declare const AST_VERSION = 5;
34
38
  /** Opaque public handle (re-exported as `Ast` from the package index). */
35
39
  interface Ast {
36
40
  readonly astVersion: typeof AST_VERSION;
@@ -95,7 +99,8 @@ interface PluralIssue {
95
99
  * lets the deterministic parity fixtures assert exact output.
96
100
  *
97
101
  * Order: strip stray sentinels → build vars, roll #def, tree-walk (+ #include) →
98
- * cosmetic post-process (if on) → mandatory neutralize safety-restore (always).
102
+ * cosmetic post-process (if on) → mandatory neutralize safety-restore (always) → decode the
103
+ * `/! … !/` spans (always).
99
104
  */
100
105
 
101
106
  /** Default for {@link PipelineOptions.maxDepth} (#include + parse-nesting guard). */
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/internal/ast.ts
2
- var AST_VERSION = 4;
2
+ var AST_VERSION = 5;
3
3
  function walk(nodes, visit) {
4
4
  const stack = [{ list: nodes, i: 0 }];
5
5
  while (stack.length > 0) {
@@ -43,31 +43,6 @@ function nameMap(...sources) {
43
43
  return map;
44
44
  }
45
45
 
46
- // src/internal/neutralize.ts
47
- var STRUCTURAL = ["{", "}", "[", "]", "%", "#"];
48
- var SENTINEL_BASE = 57344;
49
- var SHIELD = /* @__PURE__ */ new Map();
50
- var RESTORE = /* @__PURE__ */ new Map();
51
- STRUCTURAL.forEach((ch, i) => {
52
- const sentinel = String.fromCharCode(SENTINEL_BASE + i);
53
- SHIELD.set(ch, sentinel);
54
- RESTORE.set(sentinel, ch);
55
- });
56
- var SHIELD_RE = /[{}[\]%#]/gu;
57
- var RESTORE_RE = new RegExp(
58
- `[${String.fromCharCode(SENTINEL_BASE)}-${String.fromCharCode(SENTINEL_BASE + STRUCTURAL.length - 1)}]`,
59
- "gu"
60
- );
61
- function neutralize(value) {
62
- return value.replace(SHIELD_RE, (ch) => SHIELD.get(ch) ?? ch);
63
- }
64
- function safetyRestore(text) {
65
- return text.replace(RESTORE_RE, (ch) => RESTORE.get(ch) ?? ch);
66
- }
67
- function stripSentinels(text) {
68
- return text.replace(RESTORE_RE, "");
69
- }
70
-
71
46
  // src/internal/pairs.ts
72
47
  var BRACE_OPEN = 123;
73
48
  var BRACE_CLOSE = 125;
@@ -345,6 +320,126 @@ var TextIndex = class {
345
320
  }
346
321
  };
347
322
 
323
+ // src/internal/verbatim.ts
324
+ var OPEN = 983040;
325
+ var CLOSE = 983041;
326
+ var DIGIT = 987136;
327
+ var RESERVED_RE = /[-\u{F0000}-\u{F1FFF}]/gu;
328
+ var SPAN_RANGE_RE = /[\u{F0000}-\u{F1FFF}]/gu;
329
+ var TOKEN_RE = /\u{F0000}((?:[\u{F1000}-\u{F1FFF}]{2})*)\u{F0001}/gu;
330
+ var SPAN_HIGH_RE = /[\uDB80-\uDB87]/;
331
+ function encodeVerbatim(content) {
332
+ let out = String.fromCodePoint(OPEN);
333
+ for (const ch of content) {
334
+ const c = ch.codePointAt(0);
335
+ out += String.fromCodePoint(DIGIT + (c >> 12), DIGIT + (c & 4095));
336
+ }
337
+ return out + String.fromCodePoint(CLOSE);
338
+ }
339
+ function decodeVerbatim(text) {
340
+ if (!SPAN_HIGH_RE.test(text)) return text;
341
+ TOKEN_RE.lastIndex = 0;
342
+ return text.replace(TOKEN_RE, (_m, digits) => {
343
+ let out = "";
344
+ const units = [...digits];
345
+ for (let i = 0; i + 1 < units.length; i += 2) {
346
+ out += String.fromCodePoint(units[i].codePointAt(0) - DIGIT << 12 | units[i + 1].codePointAt(0) - DIGIT);
347
+ }
348
+ return out;
349
+ });
350
+ }
351
+ function budgetLength(value) {
352
+ return SPAN_HIGH_RE.test(value) ? decodeVerbatim(value).length : value.length;
353
+ }
354
+ function stripSpanRange(value) {
355
+ return SPAN_HIGH_RE.test(value) ? value.replace(SPAN_RANGE_RE, "") : value;
356
+ }
357
+ function projectSource(src) {
358
+ const outStarts = [];
359
+ const srcStarts = [];
360
+ const srcEnds = [];
361
+ const tokens = [];
362
+ let out = "";
363
+ let spans = 0;
364
+ let unclosed = -1;
365
+ const copy = (start, end) => {
366
+ if (start >= end) return;
367
+ const chunk = src.slice(start, end);
368
+ let from2 = 0;
369
+ for (const m of chunk.matchAll(RESERVED_RE)) {
370
+ if (m.index > from2) push(start + from2, start + m.index, chunk.slice(from2, m.index), false);
371
+ from2 = m.index + m[0].length;
372
+ }
373
+ if (from2 < chunk.length) push(start + from2, end, chunk.slice(from2), false);
374
+ };
375
+ const push = (srcStart, srcEnd, piece, token) => {
376
+ outStarts.push(out.length);
377
+ srcStarts.push(srcStart);
378
+ srcEnds.push(srcEnd);
379
+ tokens.push(token);
380
+ out += piece;
381
+ };
382
+ let from = 0;
383
+ let at = 0;
384
+ let nextComment = -2;
385
+ let nextSpan = -2;
386
+ let commentCloserGone = false;
387
+ let spanCloserGone = false;
388
+ for (; ; ) {
389
+ if (nextComment !== -1 && nextComment < at) nextComment = src.indexOf("/#", at);
390
+ if (nextSpan !== -1 && nextSpan < at) nextSpan = src.indexOf("/!", at);
391
+ if (nextComment === -1 && nextSpan === -1) break;
392
+ const isSpan = nextComment === -1 || nextSpan !== -1 && nextSpan < nextComment;
393
+ const open = isSpan ? nextSpan : nextComment;
394
+ const gone = isSpan ? spanCloserGone : commentCloserGone;
395
+ const close = gone ? -1 : src.indexOf(isSpan ? "!/" : "#/", open + 2);
396
+ if (close === -1) {
397
+ if (isSpan) {
398
+ spanCloserGone = true;
399
+ if (unclosed === -1) unclosed = open;
400
+ } else {
401
+ commentCloserGone = true;
402
+ }
403
+ at = open + 2;
404
+ continue;
405
+ }
406
+ copy(from, open);
407
+ if (isSpan) {
408
+ spans += 1;
409
+ if (close > open + 2) push(open, close + 2, encodeVerbatim(src.slice(open + 2, close)), true);
410
+ }
411
+ from = at = close + 2;
412
+ }
413
+ copy(from, src.length);
414
+ const segmentAt = (offset) => {
415
+ let lo = 0;
416
+ let hi = outStarts.length - 1;
417
+ if (hi < 0 || outStarts[0] > offset) return -1;
418
+ while (lo < hi) {
419
+ const mid = lo + hi + 1 >> 1;
420
+ if (outStarts[mid] <= offset) lo = mid;
421
+ else hi = mid - 1;
422
+ }
423
+ return lo;
424
+ };
425
+ const toSource = (offset) => {
426
+ const at2 = segmentAt(offset);
427
+ if (at2 === -1) return 0;
428
+ const delta = offset - outStarts[at2];
429
+ if (delta <= 0) return srcStarts[at2];
430
+ if (tokens[at2]) return srcEnds[at2];
431
+ return Math.min(srcStarts[at2] + delta, srcEnds[at2]);
432
+ };
433
+ const toSourceEnd = (offset) => {
434
+ if (offset <= 0) return toSource(0);
435
+ const at2 = segmentAt(offset - 1);
436
+ if (at2 === -1) return 0;
437
+ if (tokens[at2]) return srcEnds[at2];
438
+ return Math.min(srcStarts[at2] + (offset - outStarts[at2]), srcEnds[at2]);
439
+ };
440
+ return { text: out, spans, unclosed, toSource, toSourceEnd };
441
+ }
442
+
348
443
  // src/internal/parser.ts
349
444
  var VARIABLE_RE = /%(\w+)%/y;
350
445
  var PERCENT = 37;
@@ -356,7 +451,7 @@ var DIRECTIVE_RE = /^[ \t]*#(set|def)[ \t]+%(\w+)%[ \t]*=[ \t]*((?:.*[^ \t\n\r\u
356
451
  var CONDITIONAL_NAME_RE2 = /[A-Za-z_]\w*/y;
357
452
  var PLURAL_PREFIX = "plural ";
358
453
  function parseTemplate(src) {
359
- const { body, setDefs, defDefs } = extractDirectives(stripComments(stripSentinels(src)));
454
+ const { body, setDefs, defDefs } = extractDirectives(projectSource(src).text);
360
455
  return { astVersion: AST_VERSION, source: src, setDefs, defDefs, nodes: parseSequence(body) };
361
456
  }
362
457
  function extractDirectives(text) {
@@ -390,19 +485,6 @@ function extractDirectives(text) {
390
485
  );
391
486
  return { body: stripped.replace(/\n{3,}/gu, "\n\n"), setDefs, defDefs, occurrences };
392
487
  }
393
- function stripComments(text) {
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);
405
- }
406
488
  function parseSequence(text) {
407
489
  const index = new TextIndex(text);
408
490
  const frame = (start, end) => ({
@@ -787,7 +869,8 @@ var KNOWN_CONFIG_KEYS = /* @__PURE__ */ new Set(["minsize", "maxsize", "sep", "l
787
869
  var INCLUDE_RE = /^[ \t]*#include[ \t\n\r\f\x0B]+"([^"]+)"[ \t\n\r\f\x0B]*$/gmu;
788
870
  function validateTemplate(src, opts = {}) {
789
871
  const diagnostics = [];
790
- const text = stripComments(src);
872
+ const projection = projectSource(src);
873
+ const text = projection.text;
791
874
  const idx = buildLineIndex(text);
792
875
  checkBrackets(text, diagnostics);
793
876
  checkDirectives(text, diagnostics);
@@ -797,7 +880,62 @@ function validateTemplate(src, opts = {}) {
797
880
  if (opts.knownIncludes && opts.knownIncludes.length > 0) {
798
881
  checkIncludeTargets(text, idx, opts.knownIncludes, diagnostics);
799
882
  }
800
- return diagnostics;
883
+ const srcIdx = text === src ? idx : buildLineIndex(src);
884
+ const remap = { text, idx, src, srcIdx, toSource: projection.toSource, toSourceEnd: projection.toSourceEnd };
885
+ const placed = text === src ? diagnostics : diagnostics.map((d) => toSourcePositions(d, remap));
886
+ if (projection.unclosed !== -1) {
887
+ placed.push(warn("verbatim.unclosed", "Unclosed '/!' \u2014 the text after it is parsed as spintax.", span(srcIdx, projection.unclosed, 2)));
888
+ }
889
+ return placed;
890
+ }
891
+ function toSourcePositions(d, r) {
892
+ const codePoints = d.code.startsWith("bracket.");
893
+ const move = (line, column) => {
894
+ const offset = r.toSource(lineColToOffset(r, line, column, codePoints));
895
+ return lineColAt(r, offset, codePoints);
896
+ };
897
+ const start = move(d.line, d.column);
898
+ const data = d.data === void 0 ? void 0 : Object.fromEntries(
899
+ Object.entries(d.data).map(([key, value]) => [key, typeof value === "string" ? decodeVerbatim(value) : value])
900
+ );
901
+ const moved = { ...d, message: decodeVerbatim(d.message), line: start.line, column: start.column, ...data === void 0 ? {} : { data } };
902
+ if (d.endLine !== void 0 && d.endColumn !== void 0) {
903
+ const end = lineColAt(r, r.toSourceEnd(lineColToOffset(r, d.endLine, d.endColumn, codePoints)), codePoints);
904
+ return { ...moved, endLine: end.line, endColumn: end.column };
905
+ }
906
+ return moved;
907
+ }
908
+ function astralPrefix(text) {
909
+ const prefix = new Uint32Array(text.length + 1);
910
+ for (let i = 0; i < text.length; i += 1) {
911
+ const c = text.charCodeAt(i);
912
+ const pair = c >= 55296 && c <= 56319 && (text.charCodeAt(i + 1) & 64512) === 56320;
913
+ prefix[i + 1] = prefix[i] + (pair ? 1 : 0);
914
+ }
915
+ return prefix;
916
+ }
917
+ function codePointsBetween(prefix, from, to) {
918
+ return to - from - (prefix[to] - prefix[from]);
919
+ }
920
+ function lineColToOffset(r, line, column, codePoints) {
921
+ const start = r.idx.starts[Math.min(Math.max(line, 1), r.idx.starts.length) - 1] ?? 0;
922
+ if (!codePoints) return Math.min(start + column - 1, r.text.length);
923
+ const prefix = r.textAstral ??= astralPrefix(r.text);
924
+ let lo = start;
925
+ let hi = Math.min(start + 2 * (column - 1), r.text.length);
926
+ while (lo < hi) {
927
+ const mid = lo + hi >> 1;
928
+ if (codePointsBetween(prefix, start, mid) < column - 1) lo = mid + 1;
929
+ else hi = mid;
930
+ }
931
+ return lo;
932
+ }
933
+ function lineColAt(r, offset, codePoints) {
934
+ const pos = offsetToLineCol(r.srcIdx, offset);
935
+ if (!codePoints) return pos;
936
+ const start = r.srcIdx.starts[pos.line - 1] ?? 0;
937
+ const prefix = r.srcAstral ??= astralPrefix(r.src);
938
+ return { line: pos.line, column: codePointsBetween(prefix, start, Math.min(offset, r.src.length)) + 1 };
801
939
  }
802
940
  function err(code, message, pos) {
803
941
  return { severity: "error", code, message, ...pos };
@@ -1223,13 +1361,14 @@ function checkIncludeTargets(text, idx, known, out) {
1223
1361
  const set = new Set(known);
1224
1362
  INCLUDE_RE.lastIndex = 0;
1225
1363
  for (const m of text.matchAll(INCLUDE_RE)) {
1226
- const ref = m[1] ?? "";
1364
+ const raw = m[1] ?? "";
1365
+ const ref = decodeVerbatim(raw);
1227
1366
  if (!set.has(ref)) {
1228
1367
  const refOffset = (m.index ?? 0) + m[0].indexOf('"') + 1;
1229
1368
  out.push(err(
1230
1369
  "include.unknown-target",
1231
1370
  `#include target '${ref}' does not match any known template.`,
1232
- { ...span(idx, refOffset, ref.length), data: { target: ref } }
1371
+ { ...span(idx, refOffset, raw.length), data: { target: ref } }
1233
1372
  ));
1234
1373
  }
1235
1374
  }
@@ -1243,10 +1382,10 @@ var INCLUDE_RE2 = /^[ \t]*#include[ \t\n\r\f\x0B]+"([^"]+)"[ \t\n\r\f\x0B]*$/gmu
1243
1382
  var VARIABLE_RE2 = /%(\w+)%/gu;
1244
1383
  var CONDITIONAL_REF_RE = /\{\?!?([A-Za-z_]\w*)\?/gu;
1245
1384
  function extractFromSource(src) {
1246
- const text = stripComments(src);
1385
+ const text = projectSource(src).text;
1247
1386
  const sets = collect(text, SET_DEF_RE, true);
1248
1387
  const defs = collect(text, DEF_DEF_RE, true);
1249
- const includes = collect(text, INCLUDE_RE2, false);
1388
+ const includes = [...new Set(collect(text, INCLUDE_RE2, false).map(decodeVerbatim))];
1250
1389
  const body = text.replace(DEFINITION_LHS_RE, "");
1251
1390
  const refs = /* @__PURE__ */ new Set();
1252
1391
  addAll(refs, body, VARIABLE_RE2, true);
@@ -1465,7 +1604,8 @@ function renderAst(ast, ctx) {
1465
1604
  }
1466
1605
  function resolveIncludes(text, ctx) {
1467
1606
  INCLUDE_LINE_RE.lastIndex = 0;
1468
- return text.replace(INCLUDE_LINE_RE, (_m, ref) => {
1607
+ return text.replace(INCLUDE_LINE_RE, (_m, raw) => {
1608
+ const ref = decodeVerbatim(raw);
1469
1609
  if (ctx.includeStack.includes(ref) || ctx.includeStack.length >= ctx.maxDepth) return "";
1470
1610
  let included;
1471
1611
  try {
@@ -1619,7 +1759,7 @@ function resolveVariable(name, opts) {
1619
1759
  const value = opts.vars[name.toLowerCase()];
1620
1760
  if (value === void 0 || opts.frozen === true) return `%${name}%`;
1621
1761
  if (opts.budget.left <= 0) return `%${name}%`;
1622
- opts.budget.left -= value.length;
1762
+ opts.budget.left -= budgetLength(value);
1623
1763
  if (opts.depth >= MAX_VARIABLE_DEPTH || !/[{[%]/u.test(value)) return value;
1624
1764
  return renderFragment(parseSequence(value), { ...opts, depth: opts.depth + 1 });
1625
1765
  }
@@ -1636,7 +1776,7 @@ function expandVarsFixpoint(text, opts, passes) {
1636
1776
  const value = opts.vars[name.toLowerCase()];
1637
1777
  if (value === void 0) return m;
1638
1778
  if (opts.budget.left <= 0) return m;
1639
- opts.budget.left -= value.length;
1779
+ opts.budget.left -= budgetLength(value);
1640
1780
  changed = true;
1641
1781
  return value;
1642
1782
  });
@@ -1917,7 +2057,7 @@ var TLD_UPPER = "\\p{Lu}\\p{Lt}\\p{Lm}\\p{Lo}";
1917
2057
  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})`;
1918
2058
  var DOMAIN_PART = `(?:${LABEL}\\.)+${TLD}`;
1919
2059
  var URI_BODY = `[^\\x00${WS}<>"')\\]]`;
1920
- var URI_RE = new RegExp(`(?:(?:https?|ftp):\\/\\/|(?:mailto|tel):)${URI_BODY}+`, "giu");
2060
+ var URI_RE = new RegExp(`(?:(?:https?|ftp):\\/\\/${URI_BODY}*|(?:mailto|tel):${URI_BODY}+)`, "giu");
1921
2061
  var MAILTEL_PREFIX_RE = /^(?:mailto|tel):/iu;
1922
2062
  var DOMAIN_AT_RE = new RegExp(`${DOMAIN_PART}${WORD_BOUNDARY}`, "uy");
1923
2063
  var DOMAIN_SCAN_RE = new RegExp(`${AFTER_NON_WORD}(?:(${DOMAIN_PART}${WORD_BOUNDARY})|(?:${LABEL}\\.)*${LABEL})`, "gu");
@@ -1966,12 +2106,15 @@ var SENTENCE_OPENERS = "\xBF\xA1";
1966
2106
  var LEAD = `(?:<[^>]+>|[${SENTENCE_OPENERS}]|${S})*`;
1967
2107
  var QUOTES = `"'\xAB\xBB\u2039\u203A\u201C\u201D\u2018\u2019`;
1968
2108
  var CLOSER = `[)\\]]|[${QUOTES}]+(?=${S}|$|<|\\/?>|[.,;:!?\u2026)\\]\u2014\u2013])`;
2109
+ var DOT_TOKEN_RE = new RegExp(`(?<=^|${S}|[(\\[{>/\\\\${QUOTES}])\\.[\\p{L}\\p{N}][\\p{L}\\p{N}_.\\-]*`, "gu");
1969
2110
  var SPACE_BEFORE_PUNCT_RE = new RegExp(`(?<!${S})${S}+([,;:!?.])`, "gu");
1970
2111
  var SPACE_AFTER_COMMA_RE = new RegExp(`([,;:])(?!\\p{Nd})(?!${S}|$|<|${CLOSER})`, "gu");
1971
2112
  var SPACE_AFTER_SENTENCE_RE = new RegExp(`(?<![.!?])([.!?]+)(?![.!?])(?!\\p{Nd})(?!${S}|$|<|${CLOSER})`, "gu");
1972
2113
  var SPACE_AFTER_OPENER_RE = new RegExp(`([${SENTENCE_OPENERS}])${S}+`, "gu");
1973
2114
  var CAP_FIRST_RE = new RegExp(`^(${LEAD})(\\p{Ll})`, "u");
1974
2115
  var up = (ch) => ch.toUpperCase();
2116
+ var upTurkic = (ch) => ch === "i" ? "\u0130" : ch.toUpperCase();
2117
+ var TURKIC_LANGS = /* @__PURE__ */ new Set(["tr", "az"]);
1975
2118
  function indexLeads(text) {
1976
2119
  const n = text.length;
1977
2120
  const leadEnd = new Int32Array(n + 1);
@@ -2004,7 +2147,7 @@ function leadEndFrom(text, i, leads) {
2004
2147
  }
2005
2148
  return j < text.length ? leads().leadEnd[j] : j;
2006
2149
  }
2007
- function capitalizeAfter(text, lower, next, context) {
2150
+ function capitalizeAfter(text, lower, next, context, upper) {
2008
2151
  const leads = () => {
2009
2152
  if (context.leads === null || context.leads.leadEnd.length !== text.length + 1) context.leads = indexLeads(text);
2010
2153
  return context.leads;
@@ -2019,7 +2162,7 @@ function capitalizeAfter(text, lower, next, context) {
2019
2162
  if (cp === void 0) continue;
2020
2163
  const ch = String.fromCodePoint(cp);
2021
2164
  if (!lower.test(ch)) continue;
2022
- out += text.slice(emitted, at) + up(ch);
2165
+ out += text.slice(emitted, at) + upper(ch);
2023
2166
  emitted = at + ch.length;
2024
2167
  from = emitted;
2025
2168
  }
@@ -2052,11 +2195,11 @@ var afterLineBreak = (text, from) => {
2052
2195
  const at = text.indexOf("\n", from);
2053
2196
  return at === -1 ? null : { leadStart: at + 1, resume: at + 1 };
2054
2197
  };
2055
- var SHIELD_PREFIXES = ["URL", "URI", "EMAIL", "DOM", "NUM", "ABBR"];
2056
- var RESTORE_RE2 = new RegExp(`\\x00(?:${SHIELD_PREFIXES.join("|")})_\\d+\\x00`, "gu");
2198
+ var SHIELD_PREFIXES = ["URL", "URI", "EMAIL", "DOTTOK", "DOM", "NUM", "ABBR"];
2199
+ var RESTORE_RE = new RegExp(`\\x00(?:${SHIELD_PREFIXES.join("|")})_\\d+\\x00`, "gu");
2057
2200
  function restore(text, input, placeholders) {
2058
2201
  if (!input.includes("\0")) {
2059
- return text.replace(RESTORE_RE2, (key) => placeholders.get(key) ?? key);
2202
+ return text.replace(RESTORE_RE, (key) => placeholders.get(key) ?? key);
2060
2203
  }
2061
2204
  return restoreAsTheLoopDoes(text, placeholders);
2062
2205
  }
@@ -2087,7 +2230,8 @@ function restoreAsTheLoopDoes(text, placeholders) {
2087
2230
  }
2088
2231
  return out;
2089
2232
  }
2090
- function postProcess(input) {
2233
+ function postProcess(input, locale = "") {
2234
+ const upper = TURKIC_LANGS.has(normalizeBaseLang(locale)) ? upTurkic : up;
2091
2235
  const placeholders = /* @__PURE__ */ new Map();
2092
2236
  let counter = 0;
2093
2237
  const store = (value, prefix) => {
@@ -2107,6 +2251,7 @@ function postProcess(input) {
2107
2251
  (m) => storeWithTrailingPunct(m, MAILTEL_PREFIX_RE.test(m) ? "URI" : "URL")
2108
2252
  );
2109
2253
  text = shieldEmails(text, (m) => store(m, "EMAIL"));
2254
+ text = text.replace(DOT_TOKEN_RE, (m) => storeWithTrailingPunct(m, "DOTTOK"));
2110
2255
  text = shieldDomains(text, (m) => store(m, "DOM"));
2111
2256
  text = text.replace(DECIMAL_RE, (m) => store(m, "NUM"));
2112
2257
  text = text.replace(MULTI_ABBR_RE, (m) => store(m, "ABBR"));
@@ -2116,19 +2261,41 @@ function postProcess(input) {
2116
2261
  text = text.replace(SPACE_AFTER_COMMA_RE, "$1 ");
2117
2262
  text = text.replace(SPACE_AFTER_SENTENCE_RE, "$1 ");
2118
2263
  text = text.replace(SPACE_AFTER_OPENER_RE, "$1");
2119
- text = text.replace(CAP_FIRST_RE, (_m, lead, ch) => lead + up(ch));
2264
+ text = text.replace(CAP_FIRST_RE, (_m, lead, ch) => lead + upper(ch));
2120
2265
  const leadContext = { leads: null };
2121
- text = capitalizeAfter(text, LOWER_RE, afterSentenceEnd, leadContext);
2122
- text = capitalizeAfter(text, LOWER_RE, afterBlockTag(), leadContext);
2123
- text = capitalizeAfter(text, LOWER_RE, afterLineBreak, leadContext);
2266
+ text = capitalizeAfter(text, LOWER_RE, afterSentenceEnd, leadContext, upper);
2267
+ text = capitalizeAfter(text, LOWER_RE, afterBlockTag(), leadContext, upper);
2268
+ text = capitalizeAfter(text, LOWER_RE, afterLineBreak, leadContext, upper);
2124
2269
  return restore(text, input, placeholders).trim();
2125
2270
  }
2126
2271
 
2272
+ // src/internal/neutralize.ts
2273
+ var STRUCTURAL = ["{", "}", "[", "]", "%", "#"];
2274
+ var SENTINEL_BASE = 57344;
2275
+ var SHIELD = /* @__PURE__ */ new Map();
2276
+ var RESTORE = /* @__PURE__ */ new Map();
2277
+ STRUCTURAL.forEach((ch, i) => {
2278
+ const sentinel = String.fromCharCode(SENTINEL_BASE + i);
2279
+ SHIELD.set(ch, sentinel);
2280
+ RESTORE.set(sentinel, ch);
2281
+ });
2282
+ var SHIELD_RE = /[{}[\]%#]/gu;
2283
+ var RESTORE_RE2 = new RegExp(
2284
+ `[${String.fromCharCode(SENTINEL_BASE)}-${String.fromCharCode(SENTINEL_BASE + STRUCTURAL.length - 1)}]`,
2285
+ "gu"
2286
+ );
2287
+ function neutralize(value) {
2288
+ return value.replace(SHIELD_RE, (ch) => SHIELD.get(ch) ?? ch);
2289
+ }
2290
+ function safetyRestore(text) {
2291
+ return text.replace(RESTORE_RE2, (ch) => RESTORE.get(ch) ?? ch);
2292
+ }
2293
+
2127
2294
  // src/internal/pipeline.ts
2128
2295
  var DEFAULT_MAX_DEPTH = 20;
2129
2296
  function renderWith(input, rng, opts = {}) {
2130
2297
  const ctx = {
2131
- runtimeContext: opts.context ?? {},
2298
+ runtimeContext: runtimeValues(opts.context),
2132
2299
  rng,
2133
2300
  locale: opts.locale ?? "",
2134
2301
  resolver: opts.includeResolver,
@@ -2141,8 +2308,14 @@ function renderWith(input, rng, opts = {}) {
2141
2308
  };
2142
2309
  const ast = resolveAst(input);
2143
2310
  let out = renderAst(ast, ctx);
2144
- if (opts.postProcess !== false) out = postProcess(out);
2145
- return safetyRestore(out);
2311
+ if (opts.postProcess !== false) out = postProcess(out, ctx.locale);
2312
+ return decodeVerbatim(safetyRestore(out));
2313
+ }
2314
+ function runtimeValues(context) {
2315
+ if (context === void 0) return {};
2316
+ const entries = Object.entries(context);
2317
+ if (entries.every(([, value]) => stripSpanRange(value) === value)) return context;
2318
+ return Object.fromEntries(entries.map(([key, value]) => [key, stripSpanRange(value)]));
2146
2319
  }
2147
2320
  function resolveAst(input) {
2148
2321
  if (typeof input === "string") return parseTemplate(input);
@@ -2224,6 +2397,7 @@ function countConstructs(ast, includeCount) {
2224
2397
  });
2225
2398
  counts.set = Object.keys(ast.setDefs).length;
2226
2399
  counts.include = includeCount;
2400
+ counts.verbatim = projectSource(ast.source).spans;
2227
2401
  return counts;
2228
2402
  }
2229
2403
  function neutralize2(value) {