@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/CHANGELOG.md +102 -0
- package/README.md +4 -3
- package/dist/index.cjs +54 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.js +54 -18
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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 =
|
|
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 =
|
|
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
|
+
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
|
-
|
|
198
|
-
config,
|
|
199
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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)
|
|
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
|
|
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
|
|
1182
|
-
const
|
|
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(
|
|
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),
|