@spintax/core 0.5.0 → 0.5.2

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
@@ -631,6 +631,7 @@ function checkPlurals(text, idx, locale, knownVariables, out) {
631
631
  var UNRESOLVED_AT_PLURAL_TIME = /\[|\{(?!\?)/u;
632
632
  var ANY_BRACKET = /[[\]{}]/u;
633
633
  var FORM_EXPANSION_PASSES = 51;
634
+ var FORM_EXPANSION_MAX_GROWTH = 64 * 1024;
634
635
  function expandFormsForCounting(formsRaw, defs, macros, hostNames) {
635
636
  const unknown = (directMacroSpintax = false) => ({
636
637
  forms: 0,
@@ -638,17 +639,23 @@ function expandFormsForCounting(formsRaw, defs, macros, hostNames) {
638
639
  directMacroSpintax
639
640
  });
640
641
  const seenMacro = /* @__PURE__ */ new Set();
642
+ const refsOf = (text2) => [...text2.matchAll(/%(\w+)%/gu)].map((m) => (m[1] ?? "").toLowerCase());
641
643
  const walkMacros = (source) => {
642
- for (const m of source.matchAll(/%(\w+)%/gu)) {
643
- const name = (m[1] ?? "").toLowerCase();
644
+ const stack = [{ refs: refsOf(source), i: 0 }];
645
+ while (stack.length > 0) {
646
+ const frame = stack[stack.length - 1];
647
+ if (frame.i >= frame.refs.length) {
648
+ stack.pop();
649
+ continue;
650
+ }
651
+ const name = frame.refs[frame.i++];
644
652
  if (defs[name] !== void 0 || hostNames.has(name)) continue;
645
653
  const macro = macros[name];
646
654
  if (macro === void 0 || seenMacro.has(name)) continue;
647
655
  seenMacro.add(name);
648
656
  if (/\{\?/u.test(macro)) return "opaque";
649
657
  if (ANY_BRACKET.test(macro)) return "brackets";
650
- const deeper = walkMacros(macro);
651
- if (deeper !== "clean") return deeper;
658
+ stack.push({ refs: refsOf(macro), i: 0 });
652
659
  }
653
660
  return "clean";
654
661
  };
@@ -656,28 +663,33 @@ function expandFormsForCounting(formsRaw, defs, macros, hostNames) {
656
663
  if (verbatim === "brackets") return unknown(true);
657
664
  if (verbatim === "opaque") return unknown();
658
665
  let text = formsRaw;
666
+ const budget = formsRaw.length + FORM_EXPANSION_MAX_GROWTH;
659
667
  for (let pass = 0; pass < FORM_EXPANSION_PASSES; pass++) {
660
668
  let sawReference = false;
661
669
  let bailed = false;
662
- text = text.replace(/%(\w+)%/gu, (whole, raw) => {
670
+ const parts = [];
671
+ let total = 0;
672
+ let cursor = 0;
673
+ const re = /%(\w+)%/gu;
674
+ let m;
675
+ while ((m = re.exec(text)) !== null) {
663
676
  sawReference = true;
664
- const name = raw.toLowerCase();
665
- if (hostNames.has(name)) {
666
- bailed = true;
667
- return whole;
668
- }
669
- const value = defs[name] ?? macros[name];
670
- if (value === void 0) {
671
- bailed = true;
672
- return whole;
673
- }
674
- if (ANY_BRACKET.test(value)) {
677
+ const name = (m[1] ?? "").toLowerCase();
678
+ const value = hostNames.has(name) ? void 0 : defs[name] ?? macros[name];
679
+ if (value === void 0 || ANY_BRACKET.test(value)) {
675
680
  bailed = true;
676
- return whole;
681
+ break;
677
682
  }
678
- return value;
679
- });
683
+ parts.push(text.slice(cursor, m.index), value);
684
+ total += m.index - cursor + value.length;
685
+ cursor = m.index + m[0].length;
686
+ if (total > budget) return unknown();
687
+ }
680
688
  if (bailed) return unknown();
689
+ parts.push(text.slice(cursor));
690
+ total += text.length - cursor;
691
+ if (total > budget) return unknown();
692
+ text = parts.join("");
681
693
  if (!sawReference) {
682
694
  return { forms: text.split("|").length, unresolved: false, directMacroSpintax: false };
683
695
  }
@@ -897,6 +909,7 @@ var NotImplementedError = class extends SpintaxError {
897
909
 
898
910
  // src/internal/render.ts
899
911
  var MAX_VARIABLE_DEPTH = 50;
912
+ var MAX_EXPANSION_CHARS = 1024 * 1024;
900
913
  var INCLUDE_LINE_RE = /^[ \t]*#include[ \t\n\r\f\x0B]+"([^"]+)"[ \t\n\r\f\x0B]*$/gmu;
901
914
  function renderAst(ast, ctx) {
902
915
  const base = buildVars(ast.setDefs, ctx.runtimeContext);
@@ -904,7 +917,8 @@ function renderAst(ast, ctx) {
904
917
  rng: ctx.rng,
905
918
  locale: ctx.locale,
906
919
  depth: 0,
907
- onPluralError: ctx.onPluralError
920
+ onPluralError: ctx.onPluralError,
921
+ budget: { left: MAX_EXPANSION_CHARS }
908
922
  };
909
923
  const vars = Object.keys(ast.defDefs).length > 0 ? { ...base, ...rollDefinitions(ast.defDefs, base, ctx.runtimeContext, walkOpts) } : base;
910
924
  const text = renderNodes(ast.nodes, { ...walkOpts, vars });
@@ -1017,6 +1031,8 @@ function resolveVariable(name, opts) {
1017
1031
  const value = opts.vars[name.toLowerCase()];
1018
1032
  if (value === void 0) return `%${name}%`;
1019
1033
  if (opts.depth >= MAX_VARIABLE_DEPTH || !/[{[%]/u.test(value)) return value;
1034
+ if (opts.budget.left <= 0) return `%${name}%`;
1035
+ opts.budget.left -= value.length;
1020
1036
  return renderNodes(parseSequence(value), { ...opts, depth: opts.depth + 1 });
1021
1037
  }
1022
1038
  function expandVarsOnly(text, opts) {
@@ -1026,6 +1042,8 @@ function expandVarsOnly(text, opts) {
1026
1042
  out = out.replace(/%(\w+)%/gu, (m, name) => {
1027
1043
  const value = opts.vars[name.toLowerCase()];
1028
1044
  if (value === void 0) return m;
1045
+ if (opts.budget.left <= 0) return m;
1046
+ opts.budget.left -= value.length;
1029
1047
  changed = true;
1030
1048
  return value;
1031
1049
  });