@spintax/core 0.6.1 → 0.7.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.d.cts CHANGED
@@ -21,8 +21,12 @@
21
21
  * 2 — `ParsedAst` gained `defDefs`. An `Ast` cached by an older version carries no `#def` map, so
22
22
  * rendering it would silently drop every definition; the version guard turns that into an
23
23
  * `AstVersionError` instead.
24
+ *
25
+ * 3 — `EnumerationNode` and `PermutationNode` gained `raw`, the body a direct `%var%` reference
26
+ * is spliced into at render time. An `Ast` cached by version 2 carries no `raw`, so rendering it
27
+ * would silently keep a pipe-joined value as ONE option — the defect 0.7.0 fixed.
24
28
  */
25
- declare const AST_VERSION = 2;
29
+ declare const AST_VERSION = 3;
26
30
  /** Opaque public handle (re-exported as `Ast` from the package index). */
27
31
  interface Ast {
28
32
  readonly astVersion: typeof AST_VERSION;
@@ -42,6 +46,10 @@ interface Ast {
42
46
  * - conditionals test truthiness against the raw var map; plurals resolve the
43
47
  * count (vars already expanded) then pick the bucket, lenient fullwidth
44
48
  * fallback (Stage 6d, after vars).
49
+ * - a `%var%` that sits DIRECTLY in an enumeration/permutation body is spliced as
50
+ * TEXT and the construct re-read (spliceConstruct, 0.7.0): a `|` inside such a
51
+ * value separates options, exactly as in the plugin, whose expansion runs before
52
+ * any bracket is read. Every other construct keeps the tree it was parsed into.
45
53
  * - #include (post-tree string pass) and post-process are later PRs.
46
54
  *
47
55
  * RNG note: cross-engine RNG-sequence parity is a non-goal (§3.2). Enumerations
package/dist/index.d.ts CHANGED
@@ -21,8 +21,12 @@
21
21
  * 2 — `ParsedAst` gained `defDefs`. An `Ast` cached by an older version carries no `#def` map, so
22
22
  * rendering it would silently drop every definition; the version guard turns that into an
23
23
  * `AstVersionError` instead.
24
+ *
25
+ * 3 — `EnumerationNode` and `PermutationNode` gained `raw`, the body a direct `%var%` reference
26
+ * is spliced into at render time. An `Ast` cached by version 2 carries no `raw`, so rendering it
27
+ * would silently keep a pipe-joined value as ONE option — the defect 0.7.0 fixed.
24
28
  */
25
- declare const AST_VERSION = 2;
29
+ declare const AST_VERSION = 3;
26
30
  /** Opaque public handle (re-exported as `Ast` from the package index). */
27
31
  interface Ast {
28
32
  readonly astVersion: typeof AST_VERSION;
@@ -42,6 +46,10 @@ interface Ast {
42
46
  * - conditionals test truthiness against the raw var map; plurals resolve the
43
47
  * count (vars already expanded) then pick the bucket, lenient fullwidth
44
48
  * fallback (Stage 6d, after vars).
49
+ * - a `%var%` that sits DIRECTLY in an enumeration/permutation body is spliced as
50
+ * TEXT and the construct re-read (spliceConstruct, 0.7.0): a `|` inside such a
51
+ * value separates options, exactly as in the plugin, whose expansion runs before
52
+ * any bracket is read. Every other construct keeps the tree it was parsed into.
45
53
  * - #include (post-tree string pass) and post-process are later PRs.
46
54
  *
47
55
  * RNG note: cross-engine RNG-sequence parity is a non-goal (§3.2). Enumerations
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/internal/ast.ts
2
- var AST_VERSION = 2;
2
+ var AST_VERSION = 3;
3
3
  function walk(nodes, visit) {
4
4
  const stack = [{ list: nodes, i: 0 }];
5
5
  while (stack.length > 0) {
@@ -185,19 +185,31 @@ function planBraceConstruct(content) {
185
185
  }
186
186
  return {
187
187
  texts: splitTopLevel(content),
188
- build: (children) => ({ type: "enumeration", options: children })
188
+ build: (children) => hasDirectReference(children) ? { type: "enumeration", options: children, raw: content } : { type: "enumeration", options: children }
189
189
  };
190
190
  }
191
+ function hasDirectReference(lists) {
192
+ const stack = [...lists];
193
+ while (stack.length > 0) {
194
+ const list = stack.pop();
195
+ for (const node of list) {
196
+ if (node.type === "variable") return true;
197
+ if (node.type === "conditional") stack.push(node.then, node.else);
198
+ }
199
+ }
200
+ return false;
201
+ }
202
+ var REFERENCE_RE = /%\w+%/u;
191
203
  function planPermutation(rawInner) {
192
204
  const { config, content } = extractPermutationConfig(rawInner);
193
205
  const { texts, separators } = permutationElements(splitTopLevel(content));
206
+ const separatorHasRef = REFERENCE_RE.test(config.sep) || config.lastsep !== null && REFERENCE_RE.test(config.lastsep) || separators.some((sep) => sep !== null && REFERENCE_RE.test(sep));
194
207
  return {
195
208
  texts,
196
- build: (children) => ({
197
- type: "permutation",
198
- config,
199
- options: children.map((nodes, i) => ({ nodes, separator: separators[i] ?? null }))
200
- })
209
+ build: (children) => {
210
+ const options = children.map((nodes, i) => ({ nodes, separator: separators[i] ?? null }));
211
+ return separatorHasRef || hasDirectReference(children) ? { type: "permutation", config, options, raw: rawInner } : { type: "permutation", config, options };
212
+ }
201
213
  };
202
214
  }
203
215
  function recognizeConditional(text, contentStart, contentEnd) {
@@ -1086,7 +1098,7 @@ function renderNode(node, opts) {
1086
1098
  case "variable":
1087
1099
  return resolveVariable(node.name, opts);
1088
1100
  case "enumeration":
1089
- return renderEnumeration(node.options, opts);
1101
+ return renderEnumeration(node, opts);
1090
1102
  case "permutation":
1091
1103
  return renderPermutation(node, opts);
1092
1104
  case "conditional":
@@ -1100,15 +1112,20 @@ function randomInt(rng, min, max) {
1100
1112
  }
1101
1113
  function resolveVariable(name, opts) {
1102
1114
  const value = opts.vars[name.toLowerCase()];
1103
- if (value === void 0) return `%${name}%`;
1104
- if (opts.depth >= MAX_VARIABLE_DEPTH || !/[{[%]/u.test(value)) return value;
1115
+ if (value === void 0 || opts.frozen === true) return `%${name}%`;
1105
1116
  if (opts.budget.left <= 0) return `%${name}%`;
1106
1117
  opts.budget.left -= value.length;
1118
+ if (opts.depth >= MAX_VARIABLE_DEPTH || !/[{[%]/u.test(value)) return value;
1107
1119
  return renderNodes(parseSequence(value), { ...opts, depth: opts.depth + 1 });
1108
1120
  }
1109
- function expandVarsOnly(text, opts) {
1121
+ function passesLeft(opts) {
1122
+ return Math.max(1, MAX_VARIABLE_DEPTH - opts.depth + 1);
1123
+ }
1124
+ function expandVarsFixpoint(text, opts, passes) {
1125
+ if (opts.frozen === true) return { text, changed: false, converged: true };
1110
1126
  let out = text;
1111
- for (let i = 0; i < MAX_VARIABLE_DEPTH; i += 1) {
1127
+ let changedAny = false;
1128
+ for (let i = 0; i < passes; i += 1) {
1112
1129
  let changed = false;
1113
1130
  out = out.replace(/%(\w+)%/gu, (m, name) => {
1114
1131
  const value = opts.vars[name.toLowerCase()];
@@ -1118,9 +1135,10 @@ function expandVarsOnly(text, opts) {
1118
1135
  changed = true;
1119
1136
  return value;
1120
1137
  });
1121
- if (!changed) break;
1138
+ if (!changed) return { text: out, changed: changedAny, converged: true };
1139
+ changedAny = true;
1122
1140
  }
1123
- return out;
1141
+ return { text: out, changed: changedAny, converged: false };
1124
1142
  }
1125
1143
  function conditionalTakesThen(name, inverted, opts) {
1126
1144
  const value = opts.vars[name.toLowerCase()];
@@ -1131,7 +1149,7 @@ function renderConditional(node, opts) {
1131
1149
  const truthy = conditionalTakesThen(node.name, node.inverted, opts);
1132
1150
  return { lists: [truthy ? node.then : node.else], done: [], assemble: (parts) => parts[0] ?? "" };
1133
1151
  }
1134
- function resolveCountConditionals(text, opts) {
1152
+ function resolveConditionalsInText(text, opts) {
1135
1153
  if (!text.includes("{?")) return text;
1136
1154
  const close = matchBraces(text);
1137
1155
  const out = [];
@@ -1178,8 +1196,11 @@ function matchBraces(text) {
1178
1196
  return close;
1179
1197
  }
1180
1198
  function renderPlural(node, opts) {
1181
- const countRaw = resolveCountConditionals(expandVarsOnly(node.countRaw, opts), opts);
1182
- const formsRaw = expandVarsOnly(node.formsRaw, opts);
1199
+ const passes = passesLeft(opts);
1200
+ const countPass = expandVarsFixpoint(node.countRaw, opts, passes);
1201
+ const formsPass = expandVarsFixpoint(node.formsRaw, opts, passes);
1202
+ const countRaw = resolveConditionalsInText(countPass.text, opts);
1203
+ const formsRaw = formsPass.text;
1183
1204
  const base = normalizeBaseLang(opts.locale);
1184
1205
  const report = (issue) => opts.onPluralError?.(issue);
1185
1206
  if (/[{}[\]]/u.test(formsRaw)) {
@@ -1214,6 +1235,7 @@ function renderPlural(node, opts) {
1214
1235
  return fullwidthVerbatim(countRaw, formsRaw);
1215
1236
  }
1216
1237
  const picked = pluralFor(base, Number.parseInt(count, 10), forms);
1238
+ if (!formsPass.converged) return renderNodes(parseSequence(picked), { ...opts, frozen: true });
1217
1239
  return { lists: [parseSequence(picked)], done: [], assemble: (parts) => parts[0] ?? "" };
1218
1240
  }
1219
1241
  function rawConstruct(countRaw, formsRaw) {
@@ -1222,13 +1244,27 @@ function rawConstruct(countRaw, formsRaw) {
1222
1244
  function fullwidthVerbatim(countRaw, formsRaw) {
1223
1245
  return rawConstruct(countRaw, formsRaw).replace(/\{/gu, "\uFF5B").replace(/\}/gu, "\uFF5D");
1224
1246
  }
1225
- function renderEnumeration(options, opts) {
1247
+ function renderEnumeration(node, opts) {
1248
+ const spliced = node.raw === void 0 ? null : spliceConstruct(node.raw, "{", "}", opts);
1249
+ if (spliced !== null) return spliced;
1250
+ const { options } = node;
1226
1251
  if (options.length === 0) return "";
1227
1252
  const picked = options[randomInt(opts.rng, 0, options.length - 1)];
1228
1253
  if (!picked) return "";
1229
1254
  return { lists: [picked], done: [], assemble: (parts) => parts[0] ?? "" };
1230
1255
  }
1256
+ function spliceConstruct(raw, open, close, opts) {
1257
+ if (opts.frozen === true) return null;
1258
+ const expanded = expandVarsFixpoint(resolveConditionalsInText(raw, opts), opts, passesLeft(opts));
1259
+ const body = resolveConditionalsInText(expanded.text, opts);
1260
+ if (body === raw) return null;
1261
+ const nodes = parseSequence(open + body + close);
1262
+ if (!expanded.converged) return renderNodes(nodes, { ...opts, frozen: true });
1263
+ return { lists: [nodes], done: [], assemble: (parts) => parts[0] ?? "" };
1264
+ }
1231
1265
  function renderPermutation(node, opts) {
1266
+ const spliced = node.raw === void 0 ? null : spliceConstruct(node.raw, "[", "]", opts);
1267
+ if (spliced !== null) return spliced;
1232
1268
  if (node.options.length === 0) return "";
1233
1269
  return {
1234
1270
  lists: node.options.map((o) => o.nodes),