@spintax/core 0.1.1 → 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/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, line = 1, column = 1) {
383
- return { severity: "error", code, message, line, column };
382
+ function err(code, message, pos) {
383
+ return { severity: "error", code, message, ...pos };
384
384
  }
385
- function warn(code, message, line = 1, column = 1) {
386
- return { severity: "warning", code, message, line, column };
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
- out.push(err("set.malformed", "Malformed #set. Expected: #set %name% = value", idx + 1));
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 line = countLines(text, m.index ?? 0);
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("permutation.unknown-key", `Unknown permutation config key: '${km[1]}'.`, line));
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("permutation.minsize-not-integer", `minsize must be a positive integer, got '${min[1]}'.`, line));
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("permutation.maxsize-not-integer", `maxsize must be a positive integer, got '${max[1]}'.`, line));
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 line = countLines(text, block.start);
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.", line));
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("plural.arity", `{plural ...}: expected ${arity} forms, got ${count}.`, line));
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
- defs.set((m[1] ?? "").toLowerCase(), m[2] ?? "");
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 refs = /* @__PURE__ */ new Set();
479
- for (const m of body.matchAll(/%(\w+)%/gu)) refs.add((m[1] ?? "").toLowerCase());
480
- for (const m of body.matchAll(/\{\?!?([A-Za-z_]\w*)\?/gu)) refs.add((m[1] ?? "").toLowerCase());
481
- for (const ref of refs) {
482
- if (!defs.has(ref) && !knownSet.has(ref)) {
483
- out.push(warn("variable.undefined", `Variable '${ref}' is not defined \u2014 may be a runtime variable.`));
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
- out.push(
506
- err("include.unknown-target", `#include target '${ref}' does not match any known template.`, countLines(text, m.index ?? 0))
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;