@spintax/core 0.1.2 → 0.1.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 +25 -0
- package/README.md +1 -1
- package/dist/index.cjs +83 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +83 -40
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -328,7 +328,7 @@ function findPluralBlocks(text) {
|
|
|
328
328
|
i = j + 1;
|
|
329
329
|
continue;
|
|
330
330
|
}
|
|
331
|
-
blocks.push({ start, countSlot: inner.slice(0, colon), formsRaw: inner.slice(colon + 1) });
|
|
331
|
+
blocks.push({ start, end: j + 1, countSlot: inner.slice(0, colon), formsRaw: inner.slice(colon + 1) });
|
|
332
332
|
i = j + 1;
|
|
333
333
|
}
|
|
334
334
|
return blocks;
|
|
@@ -379,11 +379,28 @@ function validateTemplate(src, opts = {}) {
|
|
|
379
379
|
}
|
|
380
380
|
return diagnostics;
|
|
381
381
|
}
|
|
382
|
-
function err(code, message,
|
|
383
|
-
return { severity: "error", code, message,
|
|
382
|
+
function err(code, message, pos) {
|
|
383
|
+
return { severity: "error", code, message, ...pos };
|
|
384
384
|
}
|
|
385
|
-
function warn(code, message,
|
|
386
|
-
return { severity: "warning", code, message,
|
|
385
|
+
function warn(code, message, pos) {
|
|
386
|
+
return { severity: "warning", code, message, ...pos };
|
|
387
|
+
}
|
|
388
|
+
function offsetToLineCol(text, offset) {
|
|
389
|
+
let line = 1;
|
|
390
|
+
let lineStart = 0;
|
|
391
|
+
const end = Math.min(Math.max(offset, 0), text.length);
|
|
392
|
+
for (let i = 0; i < end; i += 1) {
|
|
393
|
+
if (text.charAt(i) === "\n") {
|
|
394
|
+
line += 1;
|
|
395
|
+
lineStart = i + 1;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return { line, column: end - lineStart + 1 };
|
|
399
|
+
}
|
|
400
|
+
function span(text, offset, length) {
|
|
401
|
+
const s = offsetToLineCol(text, offset);
|
|
402
|
+
const e = offsetToLineCol(text, offset + length);
|
|
403
|
+
return { line: s.line, column: s.column, endLine: e.line, endColumn: e.column };
|
|
387
404
|
}
|
|
388
405
|
function checkBrackets(text, out) {
|
|
389
406
|
const close = { "{": "}", "[": "]" };
|
|
@@ -401,15 +418,15 @@ function checkBrackets(text, out) {
|
|
|
401
418
|
} else if (ch === "}" || ch === "]") {
|
|
402
419
|
const top = stack.pop();
|
|
403
420
|
if (top === void 0) {
|
|
404
|
-
out.push(err("bracket.unexpected-closing", `Unexpected closing '${ch}'.`, line, column));
|
|
421
|
+
out.push(err("bracket.unexpected-closing", `Unexpected closing '${ch}'.`, { line, column, endLine: line, endColumn: column + 1, data: { bracket: ch } }));
|
|
405
422
|
} else if (top.expect !== ch) {
|
|
406
|
-
out.push(err("bracket.mismatched", `'${top.char}' closed by '${ch}'.`, line, column));
|
|
423
|
+
out.push(err("bracket.mismatched", `'${top.char}' closed by '${ch}'.`, { line, column, endLine: line, endColumn: column + 1, data: { open: top.char, close: ch } }));
|
|
407
424
|
}
|
|
408
425
|
}
|
|
409
426
|
column += 1;
|
|
410
427
|
}
|
|
411
428
|
for (const unclosed of stack) {
|
|
412
|
-
out.push(err("bracket.unclosed", `Unclosed '${unclosed.char}'.`, unclosed.line, unclosed.column));
|
|
429
|
+
out.push(err("bracket.unclosed", `Unclosed '${unclosed.char}'.`, { line: unclosed.line, column: unclosed.column, endLine: unclosed.line, endColumn: unclosed.column + 1, data: { bracket: unclosed.char } }));
|
|
413
430
|
}
|
|
414
431
|
}
|
|
415
432
|
function checkSetDirectives(text, out) {
|
|
@@ -418,7 +435,9 @@ function checkSetDirectives(text, out) {
|
|
|
418
435
|
const trimmed = lineText.replace(/^[ \t]+/, "");
|
|
419
436
|
if (!trimmed.startsWith("#set ") && !trimmed.startsWith("#set ")) return;
|
|
420
437
|
if (!/^#set\s+%(\w+)%\s*=\s*(.+)$/u.test(trimmed)) {
|
|
421
|
-
|
|
438
|
+
const column = lineText.length - trimmed.length + 1;
|
|
439
|
+
const line = idx + 1;
|
|
440
|
+
out.push(err("set.malformed", "Malformed #set. Expected: #set %name% = value", { line, column, endLine: line, endColumn: lineText.length + 1 }));
|
|
422
441
|
}
|
|
423
442
|
});
|
|
424
443
|
}
|
|
@@ -426,20 +445,32 @@ function checkPermutationConfigs(text, out) {
|
|
|
426
445
|
for (const m of text.matchAll(/\[<([^>]*?)>/gu)) {
|
|
427
446
|
const configStr = m[1] ?? "";
|
|
428
447
|
if (!/\w+\s*=/.test(configStr)) continue;
|
|
429
|
-
const
|
|
448
|
+
const configBase = (m.index ?? 0) + 2;
|
|
430
449
|
for (const km of configStr.matchAll(/(\w+)\s*=/gu)) {
|
|
431
450
|
const key = (km[1] ?? "").toLowerCase();
|
|
432
451
|
if (!KNOWN_CONFIG_KEYS.has(key)) {
|
|
433
|
-
out.push(err(
|
|
452
|
+
out.push(err(
|
|
453
|
+
"permutation.unknown-key",
|
|
454
|
+
`Unknown permutation config key: '${km[1]}'.`,
|
|
455
|
+
{ ...span(text, configBase + (km.index ?? 0), (km[1] ?? "").length), data: { key: km[1] } }
|
|
456
|
+
));
|
|
434
457
|
}
|
|
435
458
|
}
|
|
436
459
|
const min = /minsize\s*=\s*([^;>\s]+)/i.exec(configStr);
|
|
437
460
|
if (min && !/^\d+$/.test(min[1] ?? "")) {
|
|
438
|
-
out.push(err(
|
|
461
|
+
out.push(err(
|
|
462
|
+
"permutation.minsize-not-integer",
|
|
463
|
+
`minsize must be a positive integer, got '${min[1]}'.`,
|
|
464
|
+
{ ...span(text, configBase + min.index, min[0].length), data: { value: min[1] } }
|
|
465
|
+
));
|
|
439
466
|
}
|
|
440
467
|
const max = /maxsize\s*=\s*([^;>\s]+)/i.exec(configStr);
|
|
441
468
|
if (max && !/^\d+$/.test(max[1] ?? "")) {
|
|
442
|
-
out.push(err(
|
|
469
|
+
out.push(err(
|
|
470
|
+
"permutation.maxsize-not-integer",
|
|
471
|
+
`maxsize must be a positive integer, got '${max[1]}'.`,
|
|
472
|
+
{ ...span(text, configBase + max.index, max[0].length), data: { value: max[1] } }
|
|
473
|
+
));
|
|
443
474
|
}
|
|
444
475
|
}
|
|
445
476
|
}
|
|
@@ -447,15 +478,19 @@ function checkPlurals(text, locale, out) {
|
|
|
447
478
|
const base = locale && locale !== "" ? normalizeBaseLang(locale) : "";
|
|
448
479
|
const arity = base !== "" ? pluralArity(base) : 0;
|
|
449
480
|
for (const block of findPluralBlocks(text)) {
|
|
450
|
-
const
|
|
481
|
+
const at = span(text, block.start, block.end - block.start);
|
|
451
482
|
if (/[{}[\]]/.test(block.formsRaw)) {
|
|
452
|
-
out.push(err("plural.nested-brackets", "{plural ...}: forms must not contain nested spintax brackets.",
|
|
483
|
+
out.push(err("plural.nested-brackets", "{plural ...}: forms must not contain nested spintax brackets.", at));
|
|
453
484
|
continue;
|
|
454
485
|
}
|
|
455
486
|
if (arity > 0) {
|
|
456
487
|
const count = block.formsRaw.split("|").length;
|
|
457
488
|
if (count !== arity) {
|
|
458
|
-
out.push(err(
|
|
489
|
+
out.push(err(
|
|
490
|
+
"plural.arity",
|
|
491
|
+
`{plural ...}: expected ${arity} forms, got ${count}.`,
|
|
492
|
+
{ ...at, data: { expected: arity, got: count } }
|
|
493
|
+
));
|
|
459
494
|
}
|
|
460
495
|
}
|
|
461
496
|
}
|
|
@@ -463,37 +498,49 @@ function checkPlurals(text, locale, out) {
|
|
|
463
498
|
function checkVariableReferences(text, known, out) {
|
|
464
499
|
const knownSet = new Set((known ?? []).map((n) => n.toLowerCase()));
|
|
465
500
|
const defs = /* @__PURE__ */ new Map();
|
|
501
|
+
const defPos = /* @__PURE__ */ new Map();
|
|
466
502
|
for (const m of text.matchAll(/^[ \t]*#set[ \t]+%(\w+)%[ \t]*=[ \t]*(.*?)$/gmu)) {
|
|
467
|
-
|
|
503
|
+
const name = (m[1] ?? "").toLowerCase();
|
|
504
|
+
defs.set(name, m[2] ?? "");
|
|
505
|
+
const nameOffset = (m.index ?? 0) + m[0].indexOf("%");
|
|
506
|
+
defPos.set(name, span(text, nameOffset, name.length + 2));
|
|
468
507
|
}
|
|
508
|
+
const somewhere = { line: 1, column: 1 };
|
|
469
509
|
for (const [name, value] of defs) {
|
|
470
510
|
if (value.toLowerCase().includes(`%${name}%`)) {
|
|
471
|
-
out.push(err("variable.self-reference", `Variable '${name}' references itself
|
|
511
|
+
out.push(err("variable.self-reference", `Variable '${name}' references itself.`, defPos.get(name) ?? somewhere));
|
|
472
512
|
}
|
|
473
513
|
}
|
|
474
514
|
for (const name of defs.keys()) {
|
|
475
|
-
detectCycle(name, defs, [name], out);
|
|
515
|
+
detectCycle(name, defs, [name], defPos.get(name) ?? somewhere, out);
|
|
476
516
|
}
|
|
477
|
-
const body = text.replace(/^[ \t]*#set[ \t]+%\w+%[ \t]*=[ \t]*.*?$/gmu, "");
|
|
478
|
-
const
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
517
|
+
const body = text.replace(/^[ \t]*#set[ \t]+%\w+%[ \t]*=[ \t]*.*?$/gmu, (m) => m.replace(/[^\n]/g, " "));
|
|
518
|
+
const seen = /* @__PURE__ */ new Set();
|
|
519
|
+
const undefinedAt = (name, offset, length) => {
|
|
520
|
+
const key = name.toLowerCase();
|
|
521
|
+
if (defs.has(key) || knownSet.has(key) || seen.has(key)) return;
|
|
522
|
+
seen.add(key);
|
|
523
|
+
out.push(warn(
|
|
524
|
+
"variable.undefined",
|
|
525
|
+
`Variable '${name}' is not defined \u2014 may be a runtime variable.`,
|
|
526
|
+
{ ...span(text, offset, length), data: { name } }
|
|
527
|
+
));
|
|
528
|
+
};
|
|
529
|
+
for (const m of body.matchAll(/%(\w+)%/gu)) undefinedAt(m[1] ?? "", m.index ?? 0, m[0].length);
|
|
530
|
+
for (const m of body.matchAll(/\{\?!?([A-Za-z_]\w*)\?/gu)) {
|
|
531
|
+
undefinedAt(m[1] ?? "", (m.index ?? 0) + m[0].indexOf(m[1] ?? ""), (m[1] ?? "").length);
|
|
485
532
|
}
|
|
486
533
|
}
|
|
487
|
-
function detectCycle(current, defs, visited, out) {
|
|
534
|
+
function detectCycle(current, defs, visited, rootPos, out) {
|
|
488
535
|
const value = defs.get(current) ?? "";
|
|
489
536
|
for (const m of value.matchAll(/%(\w+)%/gu)) {
|
|
490
537
|
const ref = (m[1] ?? "").toLowerCase();
|
|
491
538
|
if (ref === current) continue;
|
|
492
539
|
if (visited.includes(ref)) {
|
|
493
|
-
out.push(err("variable.circular-reference", `Circular variable reference: ${[...visited, ref].join(" \u2192 ")}
|
|
540
|
+
out.push(err("variable.circular-reference", `Circular variable reference: ${[...visited, ref].join(" \u2192 ")}.`, rootPos));
|
|
494
541
|
return;
|
|
495
542
|
}
|
|
496
|
-
if (defs.has(ref)) detectCycle(ref, defs, [...visited, ref], out);
|
|
543
|
+
if (defs.has(ref)) detectCycle(ref, defs, [...visited, ref], rootPos, out);
|
|
497
544
|
}
|
|
498
545
|
}
|
|
499
546
|
function checkIncludeTargets(text, known, out) {
|
|
@@ -502,19 +549,15 @@ function checkIncludeTargets(text, known, out) {
|
|
|
502
549
|
for (const m of text.matchAll(INCLUDE_RE)) {
|
|
503
550
|
const ref = m[1] ?? "";
|
|
504
551
|
if (!set.has(ref)) {
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
552
|
+
const refOffset = (m.index ?? 0) + m[0].indexOf('"') + 1;
|
|
553
|
+
out.push(err(
|
|
554
|
+
"include.unknown-target",
|
|
555
|
+
`#include target '${ref}' does not match any known template.`,
|
|
556
|
+
{ ...span(text, refOffset, ref.length), data: { target: ref } }
|
|
557
|
+
));
|
|
508
558
|
}
|
|
509
559
|
}
|
|
510
560
|
}
|
|
511
|
-
function countLines(text, offset) {
|
|
512
|
-
let n = 1;
|
|
513
|
-
for (let i = 0; i < offset && i < text.length; i += 1) {
|
|
514
|
-
if (text.charAt(i) === "\n") n += 1;
|
|
515
|
-
}
|
|
516
|
-
return n;
|
|
517
|
-
}
|
|
518
561
|
|
|
519
562
|
// src/internal/extract.ts
|
|
520
563
|
var SET_DEF_RE = /^[ \t]*#set[ \t]+%(\w+)%[ \t]*=/gmu;
|