@spintax/core 0.13.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/CHANGELOG.md +29 -0
- package/README.md +2 -1
- package/dist/index.cjs +222 -53
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -2
- package/dist/index.d.ts +7 -2
- package/dist/index.js +222 -53
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,35 @@
|
|
|
3
3
|
All notable changes to `@spintax/core` are documented here. This project adheres to
|
|
4
4
|
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
5
5
|
|
|
6
|
+
## 0.14.0 — 2026-10-19
|
|
7
|
+
|
|
8
|
+
**Breaking (syntax surface): a verbatim span, `/! … !/` (#92).** A minor on 0.x, as every syntax change
|
|
9
|
+
here. No stored output changes: the delimiters occur 0 times in 3 560 stored documents of the family's
|
|
10
|
+
products, and a render, validate and extract differential over them shows 0 changes.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`/! … !/` keeps its content exactly as written** — no spintax inside, no post-process
|
|
15
|
+
(`docs/decisions/0001-verbatim-span.md`). Markdown links (`/!/`), foreign
|
|
16
|
+
macros (`/!#file[D:\kw.txt,1,S]!/`), BBCode and prose about spintax itself (`/!{Hello|Hi}!/`) now
|
|
17
|
+
survive a render. One scanner with `/# … #/` comments: whichever opener comes first wins; the first `!/`
|
|
18
|
+
closes a span; `/!!/` is removed; an unclosed `/!` is plain text. A span works in a `#set`/`#def` value
|
|
19
|
+
(across lines, too), in an `#include` body and in an `#include` ref (the resolver gets it decoded).
|
|
20
|
+
Around a span the post-process works as usual; the span itself is neither capitalized nor trimmed.
|
|
21
|
+
- **`verbatim.unclosed` warning** for a `/!` with no closer — its text then parses as spintax. A warning:
|
|
22
|
+
the verdict does not change.
|
|
23
|
+
- `analyze().constructs.verbatim` counts spans.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- **`AST_VERSION` 5.** An `Ast` from an earlier version is refused with `AstVersionError`; it would spin a
|
|
28
|
+
span instead of emitting it.
|
|
29
|
+
- **Reserved range.** U+F0000–U+F1FFF (Supplementary Private Use Area-A, where spans are encoded during a
|
|
30
|
+
render) is stripped from template text outside spans and from every runtime `context` value. The
|
|
31
|
+
neutralize sentinels U+E000–U+E005 inside a span are content and come back unchanged.
|
|
32
|
+
- Validator positions after a comment or a span point at the author's source (they were shifted by the
|
|
33
|
+
removed comment text). Positions are not parity-gated.
|
|
34
|
+
|
|
6
35
|
## 0.13.0 — 2026-10-19
|
|
7
36
|
|
|
8
37
|
A minor: the post-process moves rendered text for a token that starts with a dot, for a bare `//`
|
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ parse, render, validate, extract, analyze, and neutralize spintax templates.
|
|
|
19
19
|
[`spintax-core`](https://pypi.org/project/spintax-core/) (Python), an Object Pascal engine,
|
|
20
20
|
[`Spintax.Core`](https://www.nuget.org/packages/Spintax.Core/) (.NET), and
|
|
21
21
|
the [Spintax WordPress plugin](https://wordpress.org/plugins/spintax/) are all held to the same
|
|
22
|
-
behavior contract by a **shared golden corpus of
|
|
22
|
+
behavior contract by a **shared golden corpus of 486 fixtures** — deterministic verdicts, plural
|
|
23
23
|
buckets, conditionals, `#set`/`#def` semantics and post-processing agree everywhere, so a
|
|
24
24
|
template is an asset you can move between runtimes, not a lock-in.
|
|
25
25
|
- **MIT** licensed.
|
|
@@ -88,6 +88,7 @@ dedupe in the host and cap the retries: a template may simply not have N combina
|
|
|
88
88
|
| Plural | `{plural %n%: one\|few\|many}` | grammatical agreement by locale — 2 forms (en), 3 (ru/uk/be, sr/hr/bs), 6 (ar: zero\|one\|two\|few\|many\|other) |
|
|
89
89
|
| Include | `#include "slug-or-id"` | embed another template (host-resolved) |
|
|
90
90
|
| Comment | `/# … #/` | stripped before rendering |
|
|
91
|
+
| Verbatim | `/! … !/` | emitted exactly as written — no spintax inside, no post-process: `/!/`, `/!#file[D:\kw.txt,1,S]!/`. The first `!/` closes it; an unclosed `/!` is plain text and a `verbatim.unclosed` warning |
|
|
91
92
|
|
|
92
93
|
## API
|
|
93
94
|
|
package/dist/index.cjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
// src/internal/ast.ts
|
|
4
|
-
var AST_VERSION =
|
|
4
|
+
var AST_VERSION = 5;
|
|
5
5
|
function walk(nodes, visit) {
|
|
6
6
|
const stack = [{ list: nodes, i: 0 }];
|
|
7
7
|
while (stack.length > 0) {
|
|
@@ -45,31 +45,6 @@ function nameMap(...sources) {
|
|
|
45
45
|
return map;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
// src/internal/neutralize.ts
|
|
49
|
-
var STRUCTURAL = ["{", "}", "[", "]", "%", "#"];
|
|
50
|
-
var SENTINEL_BASE = 57344;
|
|
51
|
-
var SHIELD = /* @__PURE__ */ new Map();
|
|
52
|
-
var RESTORE = /* @__PURE__ */ new Map();
|
|
53
|
-
STRUCTURAL.forEach((ch, i) => {
|
|
54
|
-
const sentinel = String.fromCharCode(SENTINEL_BASE + i);
|
|
55
|
-
SHIELD.set(ch, sentinel);
|
|
56
|
-
RESTORE.set(sentinel, ch);
|
|
57
|
-
});
|
|
58
|
-
var SHIELD_RE = /[{}[\]%#]/gu;
|
|
59
|
-
var RESTORE_RE = new RegExp(
|
|
60
|
-
`[${String.fromCharCode(SENTINEL_BASE)}-${String.fromCharCode(SENTINEL_BASE + STRUCTURAL.length - 1)}]`,
|
|
61
|
-
"gu"
|
|
62
|
-
);
|
|
63
|
-
function neutralize(value) {
|
|
64
|
-
return value.replace(SHIELD_RE, (ch) => SHIELD.get(ch) ?? ch);
|
|
65
|
-
}
|
|
66
|
-
function safetyRestore(text) {
|
|
67
|
-
return text.replace(RESTORE_RE, (ch) => RESTORE.get(ch) ?? ch);
|
|
68
|
-
}
|
|
69
|
-
function stripSentinels(text) {
|
|
70
|
-
return text.replace(RESTORE_RE, "");
|
|
71
|
-
}
|
|
72
|
-
|
|
73
48
|
// src/internal/pairs.ts
|
|
74
49
|
var BRACE_OPEN = 123;
|
|
75
50
|
var BRACE_CLOSE = 125;
|
|
@@ -347,6 +322,126 @@ var TextIndex = class {
|
|
|
347
322
|
}
|
|
348
323
|
};
|
|
349
324
|
|
|
325
|
+
// src/internal/verbatim.ts
|
|
326
|
+
var OPEN = 983040;
|
|
327
|
+
var CLOSE = 983041;
|
|
328
|
+
var DIGIT = 987136;
|
|
329
|
+
var RESERVED_RE = /[-\u{F0000}-\u{F1FFF}]/gu;
|
|
330
|
+
var SPAN_RANGE_RE = /[\u{F0000}-\u{F1FFF}]/gu;
|
|
331
|
+
var TOKEN_RE = /\u{F0000}((?:[\u{F1000}-\u{F1FFF}]{2})*)\u{F0001}/gu;
|
|
332
|
+
var SPAN_HIGH_RE = /[\uDB80-\uDB87]/;
|
|
333
|
+
function encodeVerbatim(content) {
|
|
334
|
+
let out = String.fromCodePoint(OPEN);
|
|
335
|
+
for (const ch of content) {
|
|
336
|
+
const c = ch.codePointAt(0);
|
|
337
|
+
out += String.fromCodePoint(DIGIT + (c >> 12), DIGIT + (c & 4095));
|
|
338
|
+
}
|
|
339
|
+
return out + String.fromCodePoint(CLOSE);
|
|
340
|
+
}
|
|
341
|
+
function decodeVerbatim(text) {
|
|
342
|
+
if (!SPAN_HIGH_RE.test(text)) return text;
|
|
343
|
+
TOKEN_RE.lastIndex = 0;
|
|
344
|
+
return text.replace(TOKEN_RE, (_m, digits) => {
|
|
345
|
+
let out = "";
|
|
346
|
+
const units = [...digits];
|
|
347
|
+
for (let i = 0; i + 1 < units.length; i += 2) {
|
|
348
|
+
out += String.fromCodePoint(units[i].codePointAt(0) - DIGIT << 12 | units[i + 1].codePointAt(0) - DIGIT);
|
|
349
|
+
}
|
|
350
|
+
return out;
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
function budgetLength(value) {
|
|
354
|
+
return SPAN_HIGH_RE.test(value) ? decodeVerbatim(value).length : value.length;
|
|
355
|
+
}
|
|
356
|
+
function stripSpanRange(value) {
|
|
357
|
+
return SPAN_HIGH_RE.test(value) ? value.replace(SPAN_RANGE_RE, "") : value;
|
|
358
|
+
}
|
|
359
|
+
function projectSource(src) {
|
|
360
|
+
const outStarts = [];
|
|
361
|
+
const srcStarts = [];
|
|
362
|
+
const srcEnds = [];
|
|
363
|
+
const tokens = [];
|
|
364
|
+
let out = "";
|
|
365
|
+
let spans = 0;
|
|
366
|
+
let unclosed = -1;
|
|
367
|
+
const copy = (start, end) => {
|
|
368
|
+
if (start >= end) return;
|
|
369
|
+
const chunk = src.slice(start, end);
|
|
370
|
+
let from2 = 0;
|
|
371
|
+
for (const m of chunk.matchAll(RESERVED_RE)) {
|
|
372
|
+
if (m.index > from2) push(start + from2, start + m.index, chunk.slice(from2, m.index), false);
|
|
373
|
+
from2 = m.index + m[0].length;
|
|
374
|
+
}
|
|
375
|
+
if (from2 < chunk.length) push(start + from2, end, chunk.slice(from2), false);
|
|
376
|
+
};
|
|
377
|
+
const push = (srcStart, srcEnd, piece, token) => {
|
|
378
|
+
outStarts.push(out.length);
|
|
379
|
+
srcStarts.push(srcStart);
|
|
380
|
+
srcEnds.push(srcEnd);
|
|
381
|
+
tokens.push(token);
|
|
382
|
+
out += piece;
|
|
383
|
+
};
|
|
384
|
+
let from = 0;
|
|
385
|
+
let at = 0;
|
|
386
|
+
let nextComment = -2;
|
|
387
|
+
let nextSpan = -2;
|
|
388
|
+
let commentCloserGone = false;
|
|
389
|
+
let spanCloserGone = false;
|
|
390
|
+
for (; ; ) {
|
|
391
|
+
if (nextComment !== -1 && nextComment < at) nextComment = src.indexOf("/#", at);
|
|
392
|
+
if (nextSpan !== -1 && nextSpan < at) nextSpan = src.indexOf("/!", at);
|
|
393
|
+
if (nextComment === -1 && nextSpan === -1) break;
|
|
394
|
+
const isSpan = nextComment === -1 || nextSpan !== -1 && nextSpan < nextComment;
|
|
395
|
+
const open = isSpan ? nextSpan : nextComment;
|
|
396
|
+
const gone = isSpan ? spanCloserGone : commentCloserGone;
|
|
397
|
+
const close = gone ? -1 : src.indexOf(isSpan ? "!/" : "#/", open + 2);
|
|
398
|
+
if (close === -1) {
|
|
399
|
+
if (isSpan) {
|
|
400
|
+
spanCloserGone = true;
|
|
401
|
+
if (unclosed === -1) unclosed = open;
|
|
402
|
+
} else {
|
|
403
|
+
commentCloserGone = true;
|
|
404
|
+
}
|
|
405
|
+
at = open + 2;
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
copy(from, open);
|
|
409
|
+
if (isSpan) {
|
|
410
|
+
spans += 1;
|
|
411
|
+
if (close > open + 2) push(open, close + 2, encodeVerbatim(src.slice(open + 2, close)), true);
|
|
412
|
+
}
|
|
413
|
+
from = at = close + 2;
|
|
414
|
+
}
|
|
415
|
+
copy(from, src.length);
|
|
416
|
+
const segmentAt = (offset) => {
|
|
417
|
+
let lo = 0;
|
|
418
|
+
let hi = outStarts.length - 1;
|
|
419
|
+
if (hi < 0 || outStarts[0] > offset) return -1;
|
|
420
|
+
while (lo < hi) {
|
|
421
|
+
const mid = lo + hi + 1 >> 1;
|
|
422
|
+
if (outStarts[mid] <= offset) lo = mid;
|
|
423
|
+
else hi = mid - 1;
|
|
424
|
+
}
|
|
425
|
+
return lo;
|
|
426
|
+
};
|
|
427
|
+
const toSource = (offset) => {
|
|
428
|
+
const at2 = segmentAt(offset);
|
|
429
|
+
if (at2 === -1) return 0;
|
|
430
|
+
const delta = offset - outStarts[at2];
|
|
431
|
+
if (delta <= 0) return srcStarts[at2];
|
|
432
|
+
if (tokens[at2]) return srcEnds[at2];
|
|
433
|
+
return Math.min(srcStarts[at2] + delta, srcEnds[at2]);
|
|
434
|
+
};
|
|
435
|
+
const toSourceEnd = (offset) => {
|
|
436
|
+
if (offset <= 0) return toSource(0);
|
|
437
|
+
const at2 = segmentAt(offset - 1);
|
|
438
|
+
if (at2 === -1) return 0;
|
|
439
|
+
if (tokens[at2]) return srcEnds[at2];
|
|
440
|
+
return Math.min(srcStarts[at2] + (offset - outStarts[at2]), srcEnds[at2]);
|
|
441
|
+
};
|
|
442
|
+
return { text: out, spans, unclosed, toSource, toSourceEnd };
|
|
443
|
+
}
|
|
444
|
+
|
|
350
445
|
// src/internal/parser.ts
|
|
351
446
|
var VARIABLE_RE = /%(\w+)%/y;
|
|
352
447
|
var PERCENT = 37;
|
|
@@ -358,7 +453,7 @@ var DIRECTIVE_RE = /^[ \t]*#(set|def)[ \t]+%(\w+)%[ \t]*=[ \t]*((?:.*[^ \t\n\r\u
|
|
|
358
453
|
var CONDITIONAL_NAME_RE2 = /[A-Za-z_]\w*/y;
|
|
359
454
|
var PLURAL_PREFIX = "plural ";
|
|
360
455
|
function parseTemplate(src) {
|
|
361
|
-
const { body, setDefs, defDefs } = extractDirectives(
|
|
456
|
+
const { body, setDefs, defDefs } = extractDirectives(projectSource(src).text);
|
|
362
457
|
return { astVersion: AST_VERSION, source: src, setDefs, defDefs, nodes: parseSequence(body) };
|
|
363
458
|
}
|
|
364
459
|
function extractDirectives(text) {
|
|
@@ -392,19 +487,6 @@ function extractDirectives(text) {
|
|
|
392
487
|
);
|
|
393
488
|
return { body: stripped.replace(/\n{3,}/gu, "\n\n"), setDefs, defDefs, occurrences };
|
|
394
489
|
}
|
|
395
|
-
function stripComments(text) {
|
|
396
|
-
let out = "";
|
|
397
|
-
let from = 0;
|
|
398
|
-
for (; ; ) {
|
|
399
|
-
const open = text.indexOf("/#", from);
|
|
400
|
-
if (open === -1) break;
|
|
401
|
-
const close = text.indexOf("#/", open + 2);
|
|
402
|
-
if (close === -1) break;
|
|
403
|
-
out += text.slice(from, open);
|
|
404
|
-
from = close + 2;
|
|
405
|
-
}
|
|
406
|
-
return from === 0 ? text : out + text.slice(from);
|
|
407
|
-
}
|
|
408
490
|
function parseSequence(text) {
|
|
409
491
|
const index = new TextIndex(text);
|
|
410
492
|
const frame = (start, end) => ({
|
|
@@ -789,7 +871,8 @@ var KNOWN_CONFIG_KEYS = /* @__PURE__ */ new Set(["minsize", "maxsize", "sep", "l
|
|
|
789
871
|
var INCLUDE_RE = /^[ \t]*#include[ \t\n\r\f\x0B]+"([^"]+)"[ \t\n\r\f\x0B]*$/gmu;
|
|
790
872
|
function validateTemplate(src, opts = {}) {
|
|
791
873
|
const diagnostics = [];
|
|
792
|
-
const
|
|
874
|
+
const projection = projectSource(src);
|
|
875
|
+
const text = projection.text;
|
|
793
876
|
const idx = buildLineIndex(text);
|
|
794
877
|
checkBrackets(text, diagnostics);
|
|
795
878
|
checkDirectives(text, diagnostics);
|
|
@@ -799,7 +882,62 @@ function validateTemplate(src, opts = {}) {
|
|
|
799
882
|
if (opts.knownIncludes && opts.knownIncludes.length > 0) {
|
|
800
883
|
checkIncludeTargets(text, idx, opts.knownIncludes, diagnostics);
|
|
801
884
|
}
|
|
802
|
-
|
|
885
|
+
const srcIdx = text === src ? idx : buildLineIndex(src);
|
|
886
|
+
const remap = { text, idx, src, srcIdx, toSource: projection.toSource, toSourceEnd: projection.toSourceEnd };
|
|
887
|
+
const placed = text === src ? diagnostics : diagnostics.map((d) => toSourcePositions(d, remap));
|
|
888
|
+
if (projection.unclosed !== -1) {
|
|
889
|
+
placed.push(warn("verbatim.unclosed", "Unclosed '/!' \u2014 the text after it is parsed as spintax.", span(srcIdx, projection.unclosed, 2)));
|
|
890
|
+
}
|
|
891
|
+
return placed;
|
|
892
|
+
}
|
|
893
|
+
function toSourcePositions(d, r) {
|
|
894
|
+
const codePoints = d.code.startsWith("bracket.");
|
|
895
|
+
const move = (line, column) => {
|
|
896
|
+
const offset = r.toSource(lineColToOffset(r, line, column, codePoints));
|
|
897
|
+
return lineColAt(r, offset, codePoints);
|
|
898
|
+
};
|
|
899
|
+
const start = move(d.line, d.column);
|
|
900
|
+
const data = d.data === void 0 ? void 0 : Object.fromEntries(
|
|
901
|
+
Object.entries(d.data).map(([key, value]) => [key, typeof value === "string" ? decodeVerbatim(value) : value])
|
|
902
|
+
);
|
|
903
|
+
const moved = { ...d, message: decodeVerbatim(d.message), line: start.line, column: start.column, ...data === void 0 ? {} : { data } };
|
|
904
|
+
if (d.endLine !== void 0 && d.endColumn !== void 0) {
|
|
905
|
+
const end = lineColAt(r, r.toSourceEnd(lineColToOffset(r, d.endLine, d.endColumn, codePoints)), codePoints);
|
|
906
|
+
return { ...moved, endLine: end.line, endColumn: end.column };
|
|
907
|
+
}
|
|
908
|
+
return moved;
|
|
909
|
+
}
|
|
910
|
+
function astralPrefix(text) {
|
|
911
|
+
const prefix = new Uint32Array(text.length + 1);
|
|
912
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
913
|
+
const c = text.charCodeAt(i);
|
|
914
|
+
const pair = c >= 55296 && c <= 56319 && (text.charCodeAt(i + 1) & 64512) === 56320;
|
|
915
|
+
prefix[i + 1] = prefix[i] + (pair ? 1 : 0);
|
|
916
|
+
}
|
|
917
|
+
return prefix;
|
|
918
|
+
}
|
|
919
|
+
function codePointsBetween(prefix, from, to) {
|
|
920
|
+
return to - from - (prefix[to] - prefix[from]);
|
|
921
|
+
}
|
|
922
|
+
function lineColToOffset(r, line, column, codePoints) {
|
|
923
|
+
const start = r.idx.starts[Math.min(Math.max(line, 1), r.idx.starts.length) - 1] ?? 0;
|
|
924
|
+
if (!codePoints) return Math.min(start + column - 1, r.text.length);
|
|
925
|
+
const prefix = r.textAstral ??= astralPrefix(r.text);
|
|
926
|
+
let lo = start;
|
|
927
|
+
let hi = Math.min(start + 2 * (column - 1), r.text.length);
|
|
928
|
+
while (lo < hi) {
|
|
929
|
+
const mid = lo + hi >> 1;
|
|
930
|
+
if (codePointsBetween(prefix, start, mid) < column - 1) lo = mid + 1;
|
|
931
|
+
else hi = mid;
|
|
932
|
+
}
|
|
933
|
+
return lo;
|
|
934
|
+
}
|
|
935
|
+
function lineColAt(r, offset, codePoints) {
|
|
936
|
+
const pos = offsetToLineCol(r.srcIdx, offset);
|
|
937
|
+
if (!codePoints) return pos;
|
|
938
|
+
const start = r.srcIdx.starts[pos.line - 1] ?? 0;
|
|
939
|
+
const prefix = r.srcAstral ??= astralPrefix(r.src);
|
|
940
|
+
return { line: pos.line, column: codePointsBetween(prefix, start, Math.min(offset, r.src.length)) + 1 };
|
|
803
941
|
}
|
|
804
942
|
function err(code, message, pos) {
|
|
805
943
|
return { severity: "error", code, message, ...pos };
|
|
@@ -1225,13 +1363,14 @@ function checkIncludeTargets(text, idx, known, out) {
|
|
|
1225
1363
|
const set = new Set(known);
|
|
1226
1364
|
INCLUDE_RE.lastIndex = 0;
|
|
1227
1365
|
for (const m of text.matchAll(INCLUDE_RE)) {
|
|
1228
|
-
const
|
|
1366
|
+
const raw = m[1] ?? "";
|
|
1367
|
+
const ref = decodeVerbatim(raw);
|
|
1229
1368
|
if (!set.has(ref)) {
|
|
1230
1369
|
const refOffset = (m.index ?? 0) + m[0].indexOf('"') + 1;
|
|
1231
1370
|
out.push(err(
|
|
1232
1371
|
"include.unknown-target",
|
|
1233
1372
|
`#include target '${ref}' does not match any known template.`,
|
|
1234
|
-
{ ...span(idx, refOffset,
|
|
1373
|
+
{ ...span(idx, refOffset, raw.length), data: { target: ref } }
|
|
1235
1374
|
));
|
|
1236
1375
|
}
|
|
1237
1376
|
}
|
|
@@ -1245,10 +1384,10 @@ var INCLUDE_RE2 = /^[ \t]*#include[ \t\n\r\f\x0B]+"([^"]+)"[ \t\n\r\f\x0B]*$/gmu
|
|
|
1245
1384
|
var VARIABLE_RE2 = /%(\w+)%/gu;
|
|
1246
1385
|
var CONDITIONAL_REF_RE = /\{\?!?([A-Za-z_]\w*)\?/gu;
|
|
1247
1386
|
function extractFromSource(src) {
|
|
1248
|
-
const text =
|
|
1387
|
+
const text = projectSource(src).text;
|
|
1249
1388
|
const sets = collect(text, SET_DEF_RE, true);
|
|
1250
1389
|
const defs = collect(text, DEF_DEF_RE, true);
|
|
1251
|
-
const includes = collect(text, INCLUDE_RE2, false);
|
|
1390
|
+
const includes = [...new Set(collect(text, INCLUDE_RE2, false).map(decodeVerbatim))];
|
|
1252
1391
|
const body = text.replace(DEFINITION_LHS_RE, "");
|
|
1253
1392
|
const refs = /* @__PURE__ */ new Set();
|
|
1254
1393
|
addAll(refs, body, VARIABLE_RE2, true);
|
|
@@ -1467,7 +1606,8 @@ function renderAst(ast, ctx) {
|
|
|
1467
1606
|
}
|
|
1468
1607
|
function resolveIncludes(text, ctx) {
|
|
1469
1608
|
INCLUDE_LINE_RE.lastIndex = 0;
|
|
1470
|
-
return text.replace(INCLUDE_LINE_RE, (_m,
|
|
1609
|
+
return text.replace(INCLUDE_LINE_RE, (_m, raw) => {
|
|
1610
|
+
const ref = decodeVerbatim(raw);
|
|
1471
1611
|
if (ctx.includeStack.includes(ref) || ctx.includeStack.length >= ctx.maxDepth) return "";
|
|
1472
1612
|
let included;
|
|
1473
1613
|
try {
|
|
@@ -1621,7 +1761,7 @@ function resolveVariable(name, opts) {
|
|
|
1621
1761
|
const value = opts.vars[name.toLowerCase()];
|
|
1622
1762
|
if (value === void 0 || opts.frozen === true) return `%${name}%`;
|
|
1623
1763
|
if (opts.budget.left <= 0) return `%${name}%`;
|
|
1624
|
-
opts.budget.left -= value
|
|
1764
|
+
opts.budget.left -= budgetLength(value);
|
|
1625
1765
|
if (opts.depth >= MAX_VARIABLE_DEPTH || !/[{[%]/u.test(value)) return value;
|
|
1626
1766
|
return renderFragment(parseSequence(value), { ...opts, depth: opts.depth + 1 });
|
|
1627
1767
|
}
|
|
@@ -1638,7 +1778,7 @@ function expandVarsFixpoint(text, opts, passes) {
|
|
|
1638
1778
|
const value = opts.vars[name.toLowerCase()];
|
|
1639
1779
|
if (value === void 0) return m;
|
|
1640
1780
|
if (opts.budget.left <= 0) return m;
|
|
1641
|
-
opts.budget.left -= value
|
|
1781
|
+
opts.budget.left -= budgetLength(value);
|
|
1642
1782
|
changed = true;
|
|
1643
1783
|
return value;
|
|
1644
1784
|
});
|
|
@@ -2058,10 +2198,10 @@ var afterLineBreak = (text, from) => {
|
|
|
2058
2198
|
return at === -1 ? null : { leadStart: at + 1, resume: at + 1 };
|
|
2059
2199
|
};
|
|
2060
2200
|
var SHIELD_PREFIXES = ["URL", "URI", "EMAIL", "DOTTOK", "DOM", "NUM", "ABBR"];
|
|
2061
|
-
var
|
|
2201
|
+
var RESTORE_RE = new RegExp(`\\x00(?:${SHIELD_PREFIXES.join("|")})_\\d+\\x00`, "gu");
|
|
2062
2202
|
function restore(text, input, placeholders) {
|
|
2063
2203
|
if (!input.includes("\0")) {
|
|
2064
|
-
return text.replace(
|
|
2204
|
+
return text.replace(RESTORE_RE, (key) => placeholders.get(key) ?? key);
|
|
2065
2205
|
}
|
|
2066
2206
|
return restoreAsTheLoopDoes(text, placeholders);
|
|
2067
2207
|
}
|
|
@@ -2131,11 +2271,33 @@ function postProcess(input, locale = "") {
|
|
|
2131
2271
|
return restore(text, input, placeholders).trim();
|
|
2132
2272
|
}
|
|
2133
2273
|
|
|
2274
|
+
// src/internal/neutralize.ts
|
|
2275
|
+
var STRUCTURAL = ["{", "}", "[", "]", "%", "#"];
|
|
2276
|
+
var SENTINEL_BASE = 57344;
|
|
2277
|
+
var SHIELD = /* @__PURE__ */ new Map();
|
|
2278
|
+
var RESTORE = /* @__PURE__ */ new Map();
|
|
2279
|
+
STRUCTURAL.forEach((ch, i) => {
|
|
2280
|
+
const sentinel = String.fromCharCode(SENTINEL_BASE + i);
|
|
2281
|
+
SHIELD.set(ch, sentinel);
|
|
2282
|
+
RESTORE.set(sentinel, ch);
|
|
2283
|
+
});
|
|
2284
|
+
var SHIELD_RE = /[{}[\]%#]/gu;
|
|
2285
|
+
var RESTORE_RE2 = new RegExp(
|
|
2286
|
+
`[${String.fromCharCode(SENTINEL_BASE)}-${String.fromCharCode(SENTINEL_BASE + STRUCTURAL.length - 1)}]`,
|
|
2287
|
+
"gu"
|
|
2288
|
+
);
|
|
2289
|
+
function neutralize(value) {
|
|
2290
|
+
return value.replace(SHIELD_RE, (ch) => SHIELD.get(ch) ?? ch);
|
|
2291
|
+
}
|
|
2292
|
+
function safetyRestore(text) {
|
|
2293
|
+
return text.replace(RESTORE_RE2, (ch) => RESTORE.get(ch) ?? ch);
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2134
2296
|
// src/internal/pipeline.ts
|
|
2135
2297
|
var DEFAULT_MAX_DEPTH = 20;
|
|
2136
2298
|
function renderWith(input, rng, opts = {}) {
|
|
2137
2299
|
const ctx = {
|
|
2138
|
-
runtimeContext: opts.context
|
|
2300
|
+
runtimeContext: runtimeValues(opts.context),
|
|
2139
2301
|
rng,
|
|
2140
2302
|
locale: opts.locale ?? "",
|
|
2141
2303
|
resolver: opts.includeResolver,
|
|
@@ -2149,7 +2311,13 @@ function renderWith(input, rng, opts = {}) {
|
|
|
2149
2311
|
const ast = resolveAst(input);
|
|
2150
2312
|
let out = renderAst(ast, ctx);
|
|
2151
2313
|
if (opts.postProcess !== false) out = postProcess(out, ctx.locale);
|
|
2152
|
-
return safetyRestore(out);
|
|
2314
|
+
return decodeVerbatim(safetyRestore(out));
|
|
2315
|
+
}
|
|
2316
|
+
function runtimeValues(context) {
|
|
2317
|
+
if (context === void 0) return {};
|
|
2318
|
+
const entries = Object.entries(context);
|
|
2319
|
+
if (entries.every(([, value]) => stripSpanRange(value) === value)) return context;
|
|
2320
|
+
return Object.fromEntries(entries.map(([key, value]) => [key, stripSpanRange(value)]));
|
|
2153
2321
|
}
|
|
2154
2322
|
function resolveAst(input) {
|
|
2155
2323
|
if (typeof input === "string") return parseTemplate(input);
|
|
@@ -2231,6 +2399,7 @@ function countConstructs(ast, includeCount) {
|
|
|
2231
2399
|
});
|
|
2232
2400
|
counts.set = Object.keys(ast.setDefs).length;
|
|
2233
2401
|
counts.include = includeCount;
|
|
2402
|
+
counts.verbatim = projectSource(ast.source).spans;
|
|
2234
2403
|
return counts;
|
|
2235
2404
|
}
|
|
2236
2405
|
function neutralize2(value) {
|