@spintax/core 0.5.2 → 0.6.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.js CHANGED
@@ -748,9 +748,14 @@ function checkVariableReferences(text, idx, known, out) {
748
748
  for (const m of value.matchAll(/%(\w+)%/gu)) refs.push((m[1] ?? "").toLowerCase());
749
749
  refsOf.set(name, refs);
750
750
  }
751
- const reachesCycle = namesThatReachACycle(defs, refsOf);
751
+ const { reaches: reachesCycle, via } = namesThatReachACycle(defs, refsOf);
752
752
  for (const name of defs.keys()) {
753
- detectCycle(name, defs, refsOf, reachesCycle, defPos.get(name) ?? somewhere, out);
753
+ if (!reachesCycle.has(name)) continue;
754
+ out.push(err(
755
+ "variable.circular-reference",
756
+ `Circular variable reference: ${cyclePath(name, via)}.`,
757
+ { ...defPos.get(name) ?? somewhere, data: { name } }
758
+ ));
754
759
  }
755
760
  const body = text.replace(/^[ \t]*#(?:set|def)[ \t]+%\w+%[ \t]*=[ \t]*.*?$/gmu, (m) => m.replace(/[^\n]/g, " "));
756
761
  const seen = /* @__PURE__ */ new Set();
@@ -774,6 +779,12 @@ function namesThatReachACycle(defs, refsOf) {
774
779
  const BLACK = 2;
775
780
  const color = /* @__PURE__ */ new Map();
776
781
  const reaches = /* @__PURE__ */ new Set();
782
+ const via = /* @__PURE__ */ new Map();
783
+ const mark = (name, witness) => {
784
+ if (reaches.has(name)) return;
785
+ reaches.add(name);
786
+ via.set(name, witness);
787
+ };
777
788
  for (const root of defs.keys()) {
778
789
  if (color.has(root)) continue;
779
790
  const stack = [{ name: root, refs: refsOf.get(root) ?? [], i: 0 }];
@@ -785,9 +796,9 @@ function namesThatReachACycle(defs, refsOf) {
785
796
  top.i += 1;
786
797
  if (ref === top.name || !defs.has(ref)) continue;
787
798
  const c = color.get(ref);
788
- if (c === GREY) reaches.add(top.name);
799
+ if (c === GREY) mark(top.name, ref);
789
800
  else if (c === BLACK) {
790
- if (reaches.has(ref)) reaches.add(top.name);
801
+ if (reaches.has(ref)) mark(top.name, ref);
791
802
  } else {
792
803
  stack.push({ name: ref, refs: refsOf.get(ref) ?? [], i: 0 });
793
804
  color.set(ref, GREY);
@@ -795,42 +806,41 @@ function namesThatReachACycle(defs, refsOf) {
795
806
  } else {
796
807
  stack.pop();
797
808
  color.set(top.name, BLACK);
798
- if (stack.length > 0 && reaches.has(top.name)) reaches.add(stack[stack.length - 1].name);
809
+ if (stack.length > 0 && reaches.has(top.name)) mark(stack[stack.length - 1].name, top.name);
799
810
  }
800
811
  }
801
812
  }
802
- return reaches;
803
- }
804
- function detectCycle(root, defs, refsOf, reachesCycle, rootPos, out) {
805
- if (!reachesCycle.has(root)) return;
806
- const path = [root];
807
- const onPath = /* @__PURE__ */ new Set([root]);
808
- const stack = [{ name: root, refs: refsOf.get(root) ?? [], i: 0 }];
809
- const leave = (frame) => {
810
- stack.pop();
811
- onPath.delete(frame.name);
812
- path.pop();
813
- };
814
- while (stack.length > 0) {
815
- const top = stack[stack.length - 1];
816
- if (top.i >= top.refs.length) {
817
- leave(top);
818
- continue;
813
+ return { reaches, via };
814
+ }
815
+ var CYCLE_PATH_LIMIT = 8;
816
+ function cyclePath(name, via) {
817
+ const seen = /* @__PURE__ */ new Set([name]);
818
+ const shown = [name];
819
+ let current = name;
820
+ for (; ; ) {
821
+ const next = via.get(current);
822
+ if (next === void 0) break;
823
+ if (seen.has(next)) {
824
+ shown.push(next);
825
+ return shown.join(" \u2192 ");
819
826
  }
820
- const ref = top.refs[top.i];
821
- top.i += 1;
822
- if (ref === top.name) continue;
823
- if (onPath.has(ref)) {
824
- out.push(err("variable.circular-reference", `Circular variable reference: ${[...path, ref].join(" \u2192 ")}.`, rootPos));
825
- leave(top);
826
- continue;
827
- }
828
- if (defs.has(ref) && reachesCycle.has(ref)) {
829
- stack.push({ name: ref, refs: refsOf.get(ref) ?? [], i: 0 });
830
- onPath.add(ref);
831
- path.push(ref);
827
+ if (shown.length >= CYCLE_PATH_LIMIT) {
828
+ let more = 0;
829
+ let walk2 = next;
830
+ while (!seen.has(walk2)) {
831
+ seen.add(walk2);
832
+ more += 1;
833
+ const step = via.get(walk2);
834
+ if (step === void 0) break;
835
+ walk2 = step;
836
+ }
837
+ return `${shown.join(" \u2192 ")} \u2192 \u2026 (${more} more)`;
832
838
  }
839
+ seen.add(next);
840
+ shown.push(next);
841
+ current = next;
833
842
  }
843
+ return shown.join(" \u2192 ");
834
844
  }
835
845
  function checkIncludeTargets(text, idx, known, out) {
836
846
  const set = new Set(known);
@@ -918,7 +928,7 @@ function renderAst(ast, ctx) {
918
928
  locale: ctx.locale,
919
929
  depth: 0,
920
930
  onPluralError: ctx.onPluralError,
921
- budget: { left: MAX_EXPANSION_CHARS }
931
+ budget: ctx.budget
922
932
  };
923
933
  const vars = Object.keys(ast.defDefs).length > 0 ? { ...base, ...rollDefinitions(ast.defDefs, base, ctx.runtimeContext, walkOpts) } : base;
924
934
  const text = renderNodes(ast.nodes, { ...walkOpts, vars });
@@ -1358,7 +1368,10 @@ function renderWith(input, rng, opts = {}) {
1358
1368
  resolver: opts.includeResolver,
1359
1369
  maxDepth: opts.maxDepth ?? DEFAULT_MAX_DEPTH,
1360
1370
  includeStack: [],
1361
- onPluralError: opts.onPluralError
1371
+ onPluralError: opts.onPluralError,
1372
+ // One allowance for the whole call. Created here rather than in renderAst because
1373
+ // includes re-enter renderAst, and a budget made there is a budget per subtree.
1374
+ budget: { left: MAX_EXPANSION_CHARS }
1362
1375
  };
1363
1376
  const ast = resolveAst(input);
1364
1377
  let out = renderAst(ast, ctx);