@spintax/core 0.3.1 → 0.3.3
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 +110 -0
- package/dist/index.cjs +152 -62
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +152 -62
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -59,6 +59,15 @@ function extractDirectives(text) {
|
|
|
59
59
|
const setDefs = {};
|
|
60
60
|
const defDefs = {};
|
|
61
61
|
const occurrences = [];
|
|
62
|
+
let cursorOffset = 0;
|
|
63
|
+
let cursorLine = 1;
|
|
64
|
+
const lineAt = (offset) => {
|
|
65
|
+
for (let i = cursorOffset; i < offset; i += 1) {
|
|
66
|
+
if (text.charCodeAt(i) === 10) cursorLine += 1;
|
|
67
|
+
}
|
|
68
|
+
cursorOffset = offset;
|
|
69
|
+
return cursorLine;
|
|
70
|
+
};
|
|
62
71
|
DIRECTIVE_RE.lastIndex = 0;
|
|
63
72
|
const stripped = text.replace(
|
|
64
73
|
DIRECTIVE_RE,
|
|
@@ -68,7 +77,7 @@ function extractDirectives(text) {
|
|
|
68
77
|
kind,
|
|
69
78
|
name,
|
|
70
79
|
value,
|
|
71
|
-
line:
|
|
80
|
+
line: lineAt(offset)
|
|
72
81
|
});
|
|
73
82
|
if (kind === "def") defDefs[name] = value;
|
|
74
83
|
else setDefs[name] = value;
|
|
@@ -77,11 +86,6 @@ function extractDirectives(text) {
|
|
|
77
86
|
);
|
|
78
87
|
return { body: stripped.replace(/\n{3,}/gu, "\n\n"), setDefs, defDefs, occurrences };
|
|
79
88
|
}
|
|
80
|
-
function countLines(text, offset) {
|
|
81
|
-
let line = 1;
|
|
82
|
-
for (let i = 0; i < offset; i += 1) if (text[i] === "\n") line += 1;
|
|
83
|
-
return line;
|
|
84
|
-
}
|
|
85
89
|
function stripComments(text) {
|
|
86
90
|
return text.replace(/\/#[\s\S]*?#\//g, "");
|
|
87
91
|
}
|
|
@@ -418,13 +422,14 @@ var INCLUDE_RE = /^[ \t]*#include[ \t\n\r\f\x0B]+"([^"]+)"[ \t\n\r\f\x0B]*$/gmu;
|
|
|
418
422
|
function validateTemplate(src, opts = {}) {
|
|
419
423
|
const diagnostics = [];
|
|
420
424
|
const text = stripComments(src);
|
|
425
|
+
const idx = buildLineIndex(text);
|
|
421
426
|
checkBrackets(text, diagnostics);
|
|
422
427
|
checkDirectives(text, diagnostics);
|
|
423
|
-
checkPermutationConfigs(text, diagnostics);
|
|
424
|
-
checkPlurals(text, opts.locale, diagnostics);
|
|
425
|
-
checkVariableReferences(text, opts.knownVariables, diagnostics);
|
|
428
|
+
checkPermutationConfigs(text, idx, diagnostics);
|
|
429
|
+
checkPlurals(text, idx, opts.locale, diagnostics);
|
|
430
|
+
checkVariableReferences(text, idx, opts.knownVariables, diagnostics);
|
|
426
431
|
if (opts.knownIncludes && opts.knownIncludes.length > 0) {
|
|
427
|
-
checkIncludeTargets(text, opts.knownIncludes, diagnostics);
|
|
432
|
+
checkIncludeTargets(text, idx, opts.knownIncludes, diagnostics);
|
|
428
433
|
}
|
|
429
434
|
return diagnostics;
|
|
430
435
|
}
|
|
@@ -434,21 +439,27 @@ function err(code, message, pos) {
|
|
|
434
439
|
function warn(code, message, pos) {
|
|
435
440
|
return { severity: "warning", code, message, ...pos };
|
|
436
441
|
}
|
|
437
|
-
function
|
|
438
|
-
|
|
439
|
-
let
|
|
440
|
-
|
|
441
|
-
for (let i = 0; i < end; i += 1) {
|
|
442
|
-
if (text.charAt(i) === "\n") {
|
|
443
|
-
line += 1;
|
|
444
|
-
lineStart = i + 1;
|
|
445
|
-
}
|
|
442
|
+
function buildLineIndex(text) {
|
|
443
|
+
const starts = [0];
|
|
444
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
445
|
+
if (text.charCodeAt(i) === 10) starts.push(i + 1);
|
|
446
446
|
}
|
|
447
|
-
return {
|
|
447
|
+
return { starts, length: text.length };
|
|
448
|
+
}
|
|
449
|
+
function offsetToLineCol(idx, offset) {
|
|
450
|
+
const end = Math.min(Math.max(offset, 0), idx.length);
|
|
451
|
+
let lo = 0;
|
|
452
|
+
let hi = idx.starts.length - 1;
|
|
453
|
+
while (lo < hi) {
|
|
454
|
+
const mid = lo + hi + 1 >> 1;
|
|
455
|
+
if ((idx.starts[mid] ?? 0) <= end) lo = mid;
|
|
456
|
+
else hi = mid - 1;
|
|
457
|
+
}
|
|
458
|
+
return { line: lo + 1, column: end - (idx.starts[lo] ?? 0) + 1 };
|
|
448
459
|
}
|
|
449
|
-
function span(
|
|
450
|
-
const s = offsetToLineCol(
|
|
451
|
-
const e = offsetToLineCol(
|
|
460
|
+
function span(idx, offset, length) {
|
|
461
|
+
const s = offsetToLineCol(idx, offset);
|
|
462
|
+
const e = offsetToLineCol(idx, offset + length);
|
|
452
463
|
return { line: s.line, column: s.column, endLine: e.line, endColumn: e.column };
|
|
453
464
|
}
|
|
454
465
|
function checkBrackets(text, out) {
|
|
@@ -516,7 +527,7 @@ function checkDirectives(text, out) {
|
|
|
516
527
|
}
|
|
517
528
|
}
|
|
518
529
|
}
|
|
519
|
-
function checkPermutationConfigs(text, out) {
|
|
530
|
+
function checkPermutationConfigs(text, idx, out) {
|
|
520
531
|
for (const m of text.matchAll(/\[<([^>]*?)>/gu)) {
|
|
521
532
|
const configStr = m[1] ?? "";
|
|
522
533
|
if (!/\w+\s*=/.test(configStr)) continue;
|
|
@@ -527,7 +538,7 @@ function checkPermutationConfigs(text, out) {
|
|
|
527
538
|
out.push(err(
|
|
528
539
|
"permutation.unknown-key",
|
|
529
540
|
`Unknown permutation config key: '${km[1]}'.`,
|
|
530
|
-
{ ...span(
|
|
541
|
+
{ ...span(idx, configBase + (km.index ?? 0), (km[1] ?? "").length), data: { key: km[1] } }
|
|
531
542
|
));
|
|
532
543
|
}
|
|
533
544
|
}
|
|
@@ -536,7 +547,7 @@ function checkPermutationConfigs(text, out) {
|
|
|
536
547
|
out.push(err(
|
|
537
548
|
"permutation.minsize-not-integer",
|
|
538
549
|
`minsize must be a positive integer, got '${min[1]}'.`,
|
|
539
|
-
{ ...span(
|
|
550
|
+
{ ...span(idx, configBase + min.index, min[0].length), data: { value: min[1] } }
|
|
540
551
|
));
|
|
541
552
|
}
|
|
542
553
|
const max = /maxsize\s*=\s*([^;>\s]+)/i.exec(configStr);
|
|
@@ -544,17 +555,17 @@ function checkPermutationConfigs(text, out) {
|
|
|
544
555
|
out.push(err(
|
|
545
556
|
"permutation.maxsize-not-integer",
|
|
546
557
|
`maxsize must be a positive integer, got '${max[1]}'.`,
|
|
547
|
-
{ ...span(
|
|
558
|
+
{ ...span(idx, configBase + max.index, max[0].length), data: { value: max[1] } }
|
|
548
559
|
));
|
|
549
560
|
}
|
|
550
561
|
}
|
|
551
562
|
}
|
|
552
|
-
function checkPlurals(text, locale, out) {
|
|
563
|
+
function checkPlurals(text, idx, locale, out) {
|
|
553
564
|
const base = locale && locale !== "" ? normalizeBaseLang(locale) : "";
|
|
554
565
|
const arity = base !== "" ? pluralArity(base) : 0;
|
|
555
566
|
const tainted = macroTaintedNames(text);
|
|
556
567
|
for (const block of findPluralBlocks(text)) {
|
|
557
|
-
const at = span(
|
|
568
|
+
const at = span(idx, block.start, block.end - block.start);
|
|
558
569
|
for (const m of block.countSlot.matchAll(/%(\w+)%/gu)) {
|
|
559
570
|
const name = (m[1] ?? "").toLowerCase();
|
|
560
571
|
if (!tainted.has(name)) continue;
|
|
@@ -584,25 +595,34 @@ var UNRESOLVED_AT_PLURAL_TIME = /\[|\{(?!\?)/u;
|
|
|
584
595
|
function macroTaintedNames(text) {
|
|
585
596
|
const macros = extractDirectives(text).setDefs;
|
|
586
597
|
const tainted = /* @__PURE__ */ new Set();
|
|
598
|
+
const queue = [];
|
|
587
599
|
for (const [name, value] of Object.entries(macros)) {
|
|
588
|
-
if (UNRESOLVED_AT_PLURAL_TIME.test(value))
|
|
600
|
+
if (UNRESOLVED_AT_PLURAL_TIME.test(value)) {
|
|
601
|
+
tainted.add(name);
|
|
602
|
+
queue.push(name);
|
|
603
|
+
}
|
|
589
604
|
}
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
605
|
+
const dependents = /* @__PURE__ */ new Map();
|
|
606
|
+
for (const [name, value] of Object.entries(macros)) {
|
|
607
|
+
for (const m of value.matchAll(/%(\w+)%/gu)) {
|
|
608
|
+
const ref = (m[1] ?? "").toLowerCase();
|
|
609
|
+
const list = dependents.get(ref);
|
|
610
|
+
if (list === void 0) dependents.set(ref, [name]);
|
|
611
|
+
else list.push(name);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
while (queue.length > 0) {
|
|
615
|
+
const source = queue.pop();
|
|
616
|
+
for (const name of dependents.get(source) ?? []) {
|
|
617
|
+
if (!tainted.has(name)) {
|
|
597
618
|
tainted.add(name);
|
|
598
|
-
|
|
599
|
-
break;
|
|
619
|
+
queue.push(name);
|
|
600
620
|
}
|
|
601
621
|
}
|
|
602
622
|
}
|
|
603
623
|
return tainted;
|
|
604
624
|
}
|
|
605
|
-
function checkVariableReferences(text, known, out) {
|
|
625
|
+
function checkVariableReferences(text, idx, known, out) {
|
|
606
626
|
const knownSet = new Set((known ?? []).map((n) => n.toLowerCase()));
|
|
607
627
|
const defs = /* @__PURE__ */ new Map();
|
|
608
628
|
const defPos = /* @__PURE__ */ new Map();
|
|
@@ -610,7 +630,7 @@ function checkVariableReferences(text, known, out) {
|
|
|
610
630
|
const name = (m[1] ?? "").toLowerCase();
|
|
611
631
|
defs.set(name, m[2] ?? "");
|
|
612
632
|
const nameOffset = (m.index ?? 0) + m[0].indexOf("%");
|
|
613
|
-
defPos.set(name, span(
|
|
633
|
+
defPos.set(name, span(idx, nameOffset, name.length + 2));
|
|
614
634
|
}
|
|
615
635
|
const somewhere = { line: 1, column: 1 };
|
|
616
636
|
for (const [name, value] of defs) {
|
|
@@ -618,8 +638,15 @@ function checkVariableReferences(text, known, out) {
|
|
|
618
638
|
out.push(err("variable.self-reference", `Variable '${name}' references itself.`, defPos.get(name) ?? somewhere));
|
|
619
639
|
}
|
|
620
640
|
}
|
|
641
|
+
const refsOf = /* @__PURE__ */ new Map();
|
|
642
|
+
for (const [name, value] of defs) {
|
|
643
|
+
const refs = [];
|
|
644
|
+
for (const m of value.matchAll(/%(\w+)%/gu)) refs.push((m[1] ?? "").toLowerCase());
|
|
645
|
+
refsOf.set(name, refs);
|
|
646
|
+
}
|
|
647
|
+
const reachesCycle = namesThatReachACycle(defs, refsOf);
|
|
621
648
|
for (const name of defs.keys()) {
|
|
622
|
-
detectCycle(name, defs,
|
|
649
|
+
detectCycle(name, defs, refsOf, reachesCycle, defPos.get(name) ?? somewhere, out);
|
|
623
650
|
}
|
|
624
651
|
const body = text.replace(/^[ \t]*#(?:set|def)[ \t]+%\w+%[ \t]*=[ \t]*.*?$/gmu, (m) => m.replace(/[^\n]/g, " "));
|
|
625
652
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -630,7 +657,7 @@ function checkVariableReferences(text, known, out) {
|
|
|
630
657
|
out.push(warn(
|
|
631
658
|
"variable.undefined",
|
|
632
659
|
`Variable '${name}' is not defined \u2014 may be a runtime variable.`,
|
|
633
|
-
{ ...span(
|
|
660
|
+
{ ...span(idx, offset, length), data: { name } }
|
|
634
661
|
));
|
|
635
662
|
};
|
|
636
663
|
for (const m of body.matchAll(/%(\w+)%/gu)) undefinedAt(m[1] ?? "", m.index ?? 0, m[0].length);
|
|
@@ -638,19 +665,70 @@ function checkVariableReferences(text, known, out) {
|
|
|
638
665
|
undefinedAt(m[1] ?? "", (m.index ?? 0) + m[0].indexOf(m[1] ?? ""), (m[1] ?? "").length);
|
|
639
666
|
}
|
|
640
667
|
}
|
|
641
|
-
function
|
|
642
|
-
const
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
668
|
+
function namesThatReachACycle(defs, refsOf) {
|
|
669
|
+
const GREY = 1;
|
|
670
|
+
const BLACK = 2;
|
|
671
|
+
const color = /* @__PURE__ */ new Map();
|
|
672
|
+
const reaches = /* @__PURE__ */ new Set();
|
|
673
|
+
for (const root of defs.keys()) {
|
|
674
|
+
if (color.has(root)) continue;
|
|
675
|
+
const stack = [{ name: root, refs: refsOf.get(root) ?? [], i: 0 }];
|
|
676
|
+
color.set(root, GREY);
|
|
677
|
+
while (stack.length > 0) {
|
|
678
|
+
const top = stack[stack.length - 1];
|
|
679
|
+
if (top.i < top.refs.length) {
|
|
680
|
+
const ref = top.refs[top.i];
|
|
681
|
+
top.i += 1;
|
|
682
|
+
if (ref === top.name || !defs.has(ref)) continue;
|
|
683
|
+
const c = color.get(ref);
|
|
684
|
+
if (c === GREY) reaches.add(top.name);
|
|
685
|
+
else if (c === BLACK) {
|
|
686
|
+
if (reaches.has(ref)) reaches.add(top.name);
|
|
687
|
+
} else {
|
|
688
|
+
stack.push({ name: ref, refs: refsOf.get(ref) ?? [], i: 0 });
|
|
689
|
+
color.set(ref, GREY);
|
|
690
|
+
}
|
|
691
|
+
} else {
|
|
692
|
+
stack.pop();
|
|
693
|
+
color.set(top.name, BLACK);
|
|
694
|
+
if (stack.length > 0 && reaches.has(top.name)) reaches.add(stack[stack.length - 1].name);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
return reaches;
|
|
699
|
+
}
|
|
700
|
+
function detectCycle(root, defs, refsOf, reachesCycle, rootPos, out) {
|
|
701
|
+
if (!reachesCycle.has(root)) return;
|
|
702
|
+
const path = [root];
|
|
703
|
+
const onPath = /* @__PURE__ */ new Set([root]);
|
|
704
|
+
const stack = [{ name: root, refs: refsOf.get(root) ?? [], i: 0 }];
|
|
705
|
+
const leave = (frame) => {
|
|
706
|
+
stack.pop();
|
|
707
|
+
onPath.delete(frame.name);
|
|
708
|
+
path.pop();
|
|
709
|
+
};
|
|
710
|
+
while (stack.length > 0) {
|
|
711
|
+
const top = stack[stack.length - 1];
|
|
712
|
+
if (top.i >= top.refs.length) {
|
|
713
|
+
leave(top);
|
|
714
|
+
continue;
|
|
715
|
+
}
|
|
716
|
+
const ref = top.refs[top.i];
|
|
717
|
+
top.i += 1;
|
|
718
|
+
if (ref === top.name) continue;
|
|
719
|
+
if (onPath.has(ref)) {
|
|
720
|
+
out.push(err("variable.circular-reference", `Circular variable reference: ${[...path, ref].join(" \u2192 ")}.`, rootPos));
|
|
721
|
+
leave(top);
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
if (defs.has(ref) && reachesCycle.has(ref)) {
|
|
725
|
+
stack.push({ name: ref, refs: refsOf.get(ref) ?? [], i: 0 });
|
|
726
|
+
onPath.add(ref);
|
|
727
|
+
path.push(ref);
|
|
649
728
|
}
|
|
650
|
-
if (defs.has(ref)) detectCycle(ref, defs, [...visited, ref], rootPos, out);
|
|
651
729
|
}
|
|
652
730
|
}
|
|
653
|
-
function checkIncludeTargets(text, known, out) {
|
|
731
|
+
function checkIncludeTargets(text, idx, known, out) {
|
|
654
732
|
const set = new Set(known);
|
|
655
733
|
INCLUDE_RE.lastIndex = 0;
|
|
656
734
|
for (const m of text.matchAll(INCLUDE_RE)) {
|
|
@@ -660,7 +738,7 @@ function checkIncludeTargets(text, known, out) {
|
|
|
660
738
|
out.push(err(
|
|
661
739
|
"include.unknown-target",
|
|
662
740
|
`#include target '${ref}' does not match any known template.`,
|
|
663
|
-
{ ...span(
|
|
741
|
+
{ ...span(idx, refOffset, ref.length), data: { target: ref } }
|
|
664
742
|
));
|
|
665
743
|
}
|
|
666
744
|
}
|
|
@@ -1036,8 +1114,9 @@ var SINGLE_ABBREVS = [
|
|
|
1036
1114
|
var WS = " \\t\\r\\n\\f\\x0B";
|
|
1037
1115
|
var S = `[${WS}]`;
|
|
1038
1116
|
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})";
|
|
1039
|
-
var
|
|
1040
|
-
var
|
|
1117
|
+
var URI_BODY = `[^\\x00${WS}<>"')\\]]`;
|
|
1118
|
+
var URI_RE = new RegExp(`(?:(?:https?|ftp):\\/\\/|(?:mailto|tel):)${URI_BODY}+`, "giu");
|
|
1119
|
+
var MAILTEL_PREFIX_RE = /^(?:mailto|tel):/iu;
|
|
1041
1120
|
var EMAIL_RE = new RegExp(`[a-z0-9._%+\\-]+@${DOMAIN_PART}\\b`, "giu");
|
|
1042
1121
|
var DOMAIN_RE = new RegExp(`\\b${DOMAIN_PART}\\b`, "giu");
|
|
1043
1122
|
var DECIMAL_RE = /\b\d+\.\d+\b/gu;
|
|
@@ -1058,6 +1137,18 @@ var CAP_AFTER_BLOCK_RE = new RegExp(
|
|
|
1058
1137
|
);
|
|
1059
1138
|
var CAP_AFTER_BREAK_RE = new RegExp(`(\\n${LEAD})(\\p{Ll})`, "gu");
|
|
1060
1139
|
var up = (ch) => ch.toUpperCase();
|
|
1140
|
+
var SHIELD_PREFIXES = ["URL", "URI", "EMAIL", "DOM", "NUM", "ABBR"];
|
|
1141
|
+
var RESTORE_RE2 = new RegExp(`\\x00(?:${SHIELD_PREFIXES.join("|")})_\\d+\\x00`, "gu");
|
|
1142
|
+
function restore(text, input, placeholders) {
|
|
1143
|
+
if (!input.includes("\0")) {
|
|
1144
|
+
return text.replace(RESTORE_RE2, (key) => placeholders.get(key) ?? key);
|
|
1145
|
+
}
|
|
1146
|
+
let out = text;
|
|
1147
|
+
for (const [key, value] of placeholders) {
|
|
1148
|
+
out = out.split(key).join(value);
|
|
1149
|
+
}
|
|
1150
|
+
return out;
|
|
1151
|
+
}
|
|
1061
1152
|
function postProcess(input) {
|
|
1062
1153
|
const placeholders = /* @__PURE__ */ new Map();
|
|
1063
1154
|
let counter = 0;
|
|
@@ -1077,8 +1168,10 @@ function postProcess(input) {
|
|
|
1077
1168
|
return store(value, prefix);
|
|
1078
1169
|
};
|
|
1079
1170
|
let text = input;
|
|
1080
|
-
text = text.replace(
|
|
1081
|
-
|
|
1171
|
+
text = text.replace(
|
|
1172
|
+
URI_RE,
|
|
1173
|
+
(m) => storeWithTrailingPunct(m, MAILTEL_PREFIX_RE.test(m) ? "URI" : "URL")
|
|
1174
|
+
);
|
|
1082
1175
|
text = text.replace(EMAIL_RE, (m) => store(m, "EMAIL"));
|
|
1083
1176
|
text = text.replace(DOMAIN_RE, (m) => store(m, "DOM"));
|
|
1084
1177
|
text = text.replace(DECIMAL_RE, (m) => store(m, "NUM"));
|
|
@@ -1093,10 +1186,7 @@ function postProcess(input) {
|
|
|
1093
1186
|
text = text.replace(CAP_AFTER_SENTENCE_RE, (_m, p, gap, ch) => p + gap + up(ch));
|
|
1094
1187
|
text = text.replace(CAP_AFTER_BLOCK_RE, (_m, tag, ch) => tag + up(ch));
|
|
1095
1188
|
text = text.replace(CAP_AFTER_BREAK_RE, (_m, br, ch) => br + up(ch));
|
|
1096
|
-
|
|
1097
|
-
text = text.split(key).join(value);
|
|
1098
|
-
}
|
|
1099
|
-
return text.trim();
|
|
1189
|
+
return restore(text, input, placeholders).trim();
|
|
1100
1190
|
}
|
|
1101
1191
|
|
|
1102
1192
|
// src/internal/pipeline.ts
|