@stll/template-conditions 0.6.0 → 0.6.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.d.ts +4 -1
- package/dist/index.js +40 -18
- package/package.json +4 -4
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ import { CompareNode, CompareOp, Condition, ConditionNode, ConditionNode as Cond
|
|
|
4
4
|
export declare const resolvePath: (path: string, data: Record<string, unknown>) => unknown;
|
|
5
5
|
//#endregion
|
|
6
6
|
//#region src/parse.d.ts
|
|
7
|
+
/** Maximum combined nesting of parentheses and unary negation. */
|
|
8
|
+
export declare const MAX_CONDITION_NESTING = 256;
|
|
7
9
|
/** Bare words the tokenizer reads as operators or literals; a data path can
|
|
8
10
|
* never be one of them. */
|
|
9
11
|
export declare const CONDITION_RESERVED_WORDS: readonly ["and", "or", "not", "in", "is", "defined", "true", "false"];
|
|
@@ -349,7 +351,8 @@ type InvalidMarker = {
|
|
|
349
351
|
*/
|
|
350
352
|
export declare const scanInvalidMarkers: (text: string) => InvalidMarker[];
|
|
351
353
|
/** The loop alias the codemod (and the legacy-marker message) generates for a
|
|
352
|
-
* `{{#each path}}`: the singular of the last plain-word segment, else `item`.
|
|
354
|
+
* `{{#each path}}`: the singular of the last plain-word segment, else `item`.
|
|
355
|
+
* A fresh `{% for %}` inserted by the Studio names its item by the same rule. */
|
|
353
356
|
export declare const legacyLoopAlias: (path: string) => string;
|
|
354
357
|
/** Translate an old-dialect condition expression to the Jinja surface:
|
|
355
358
|
* `!a` becomes `not a`, `a contains "x"` becomes `"x" in a`. */
|
package/dist/index.js
CHANGED
|
@@ -15,14 +15,14 @@ import { Temporal } from "temporal-polyfill/full";
|
|
|
15
15
|
* (`parties.1.name`), and rejecting arrays would silently resolve those to
|
|
16
16
|
* `undefined`.
|
|
17
17
|
*/
|
|
18
|
-
const
|
|
18
|
+
const isNonNullObject = (v) => typeof v === "object" && v !== null;
|
|
19
19
|
/** Resolve a dotted path like `company.name` against data. */
|
|
20
20
|
const resolvePath = (path, data) => {
|
|
21
21
|
if (Object.hasOwn(data, path)) return data[path];
|
|
22
22
|
const parts = path.split(".");
|
|
23
23
|
let current = data;
|
|
24
24
|
for (const part of parts) {
|
|
25
|
-
if (!
|
|
25
|
+
if (!isNonNullObject(current)) return;
|
|
26
26
|
current = current[part];
|
|
27
27
|
}
|
|
28
28
|
return current;
|
|
@@ -55,6 +55,14 @@ const NUMERIC_FUNCTION_NAMES = Object.freeze([
|
|
|
55
55
|
"ceil"
|
|
56
56
|
]);
|
|
57
57
|
const isNumericFunctionName = (name) => NUMERIC_FUNCTION_NAMES.some((candidate) => candidate === name);
|
|
58
|
+
const OPERATORS = [
|
|
59
|
+
"+",
|
|
60
|
+
"-",
|
|
61
|
+
"*",
|
|
62
|
+
"/",
|
|
63
|
+
"%"
|
|
64
|
+
];
|
|
65
|
+
const isOperator = (value) => OPERATORS.some((candidate) => candidate === value);
|
|
58
66
|
const TOKEN_RE = /(?<numeric>[0-9][0-9_]*(?:\.[0-9]+)?)|(?<ident>[\p{L}_][\p{L}\p{N}_.]*(?:-[\p{L}\p{N}_.]+)*)|(?<punct>[+\-*/%(),])|(?<invalid>\S)/gu;
|
|
59
67
|
const PRECEDENCE = {
|
|
60
68
|
"+": 1,
|
|
@@ -79,7 +87,7 @@ const tokenize$1 = (expression) => {
|
|
|
79
87
|
else if (punct === "(") tokens.push({ t: "lp" });
|
|
80
88
|
else if (punct === ")") tokens.push({ t: "rp" });
|
|
81
89
|
else if (punct === ",") tokens.push({ t: "comma" });
|
|
82
|
-
else if (punct !== void 0) tokens.push({
|
|
90
|
+
else if (punct !== void 0 && isOperator(punct)) tokens.push({
|
|
83
91
|
t: "op",
|
|
84
92
|
v: punct
|
|
85
93
|
});
|
|
@@ -183,6 +191,8 @@ const evaluateNumericExpression = (expression, data) => {
|
|
|
183
191
|
};
|
|
184
192
|
//#endregion
|
|
185
193
|
//#region src/parse.ts
|
|
194
|
+
/** Maximum combined nesting of parentheses and unary negation. */
|
|
195
|
+
const MAX_CONDITION_NESTING = 256;
|
|
186
196
|
const COMPARE_SYMBOL_TO_OP = {
|
|
187
197
|
"==": "eq",
|
|
188
198
|
"!=": "neq",
|
|
@@ -208,7 +218,7 @@ const RESERVED_WORDS = new Set(CONDITION_RESERVED_WORDS);
|
|
|
208
218
|
const isConditionKeyword = (raw) => raw !== "true" && raw !== "false" && RESERVED_WORDS.has(raw);
|
|
209
219
|
/** The keyword alternation of the tokenizer, derived from the same list. */
|
|
210
220
|
const KEYWORD_PATTERN = CONDITION_RESERVED_WORDS.filter(isConditionKeyword).map((word) => String.raw`${word}\b`).join("|");
|
|
211
|
-
const COMPARE_SYMBOL_PATTERN = Object.keys(COMPARE_SYMBOL_TO_OP).
|
|
221
|
+
const COMPARE_SYMBOL_PATTERN = Object.keys(COMPARE_SYMBOL_TO_OP).toSorted((left, right) => right.length - left.length).map(escapeRegexLiteral).join("|");
|
|
212
222
|
const NON_STRING_TOKEN_RE = new RegExp(String.raw`(?<token>${COMPARE_SYMBOL_PATTERN}|${KEYWORD_PATTERN}|[()]|-?\d[\p{N}_.]*|[\p{L}\p{N}_.]+(?:-[\p{L}\p{N}_.]+)*)`, "uy");
|
|
213
223
|
const STARTS_WITH_DIGIT_RE = /^-?\d/u;
|
|
214
224
|
const isCompareSymbol = (raw) => Object.hasOwn(COMPARE_SYMBOL_TO_OP, raw);
|
|
@@ -317,13 +327,16 @@ const parseCondition = (expression) => {
|
|
|
317
327
|
if (tokens.length === 0) return null;
|
|
318
328
|
let pos = 0;
|
|
319
329
|
const peek = () => tokens[pos];
|
|
320
|
-
const parseOr = () => {
|
|
321
|
-
const first = parseAnd();
|
|
330
|
+
const parseOr = (depth) => {
|
|
331
|
+
const first = parseAnd(depth);
|
|
332
|
+
if (first === null) return null;
|
|
322
333
|
if (peek()?.type !== "or") return first;
|
|
323
334
|
const children = [first];
|
|
324
335
|
while (peek()?.type === "or") {
|
|
325
336
|
pos += 1;
|
|
326
|
-
|
|
337
|
+
const child = parseAnd(depth);
|
|
338
|
+
if (child === null) return null;
|
|
339
|
+
children.push(child);
|
|
327
340
|
}
|
|
328
341
|
return {
|
|
329
342
|
type: "group",
|
|
@@ -331,13 +344,16 @@ const parseCondition = (expression) => {
|
|
|
331
344
|
children
|
|
332
345
|
};
|
|
333
346
|
};
|
|
334
|
-
const parseAnd = () => {
|
|
335
|
-
const first = parseNot();
|
|
347
|
+
const parseAnd = (depth) => {
|
|
348
|
+
const first = parseNot(depth);
|
|
349
|
+
if (first === null) return null;
|
|
336
350
|
if (peek()?.type !== "and") return first;
|
|
337
351
|
const children = [first];
|
|
338
352
|
while (peek()?.type === "and") {
|
|
339
353
|
pos += 1;
|
|
340
|
-
|
|
354
|
+
const child = parseNot(depth);
|
|
355
|
+
if (child === null) return null;
|
|
356
|
+
children.push(child);
|
|
341
357
|
}
|
|
342
358
|
return {
|
|
343
359
|
type: "group",
|
|
@@ -345,22 +361,27 @@ const parseCondition = (expression) => {
|
|
|
345
361
|
children
|
|
346
362
|
};
|
|
347
363
|
};
|
|
348
|
-
const parseNot = () => {
|
|
364
|
+
const parseNot = (depth) => {
|
|
349
365
|
if (peek()?.type === "not") {
|
|
366
|
+
if (depth >= 256) return null;
|
|
350
367
|
pos += 1;
|
|
368
|
+
const child = parseNot(depth + 1);
|
|
369
|
+
if (child === null) return null;
|
|
351
370
|
return {
|
|
352
371
|
type: "group",
|
|
353
372
|
combinator: "and",
|
|
354
373
|
negated: true,
|
|
355
|
-
children: [
|
|
374
|
+
children: [child]
|
|
356
375
|
};
|
|
357
376
|
}
|
|
358
|
-
return parseComparison();
|
|
377
|
+
return parseComparison(depth);
|
|
359
378
|
};
|
|
360
|
-
const parseComparison = () => {
|
|
379
|
+
const parseComparison = (depth) => {
|
|
361
380
|
if (peek()?.type === "lparen") {
|
|
381
|
+
if (depth >= 256) return null;
|
|
362
382
|
pos += 1;
|
|
363
|
-
const inner = parseOr();
|
|
383
|
+
const inner = parseOr(depth + 1);
|
|
384
|
+
if (inner === null) return null;
|
|
364
385
|
if (peek()?.type === "rparen") pos += 1;
|
|
365
386
|
return inner;
|
|
366
387
|
}
|
|
@@ -429,7 +450,7 @@ const parseCondition = (expression) => {
|
|
|
429
450
|
right: operandFromToken(rightTok)
|
|
430
451
|
};
|
|
431
452
|
};
|
|
432
|
-
return parseOr();
|
|
453
|
+
return parseOr(0);
|
|
433
454
|
};
|
|
434
455
|
//#endregion
|
|
435
456
|
//#region src/referenced-paths.ts
|
|
@@ -1175,7 +1196,8 @@ const singularize = (segment) => {
|
|
|
1175
1196
|
return segment;
|
|
1176
1197
|
};
|
|
1177
1198
|
/** The loop alias the codemod (and the legacy-marker message) generates for a
|
|
1178
|
-
* `{{#each path}}`: the singular of the last plain-word segment, else `item`.
|
|
1199
|
+
* `{{#each path}}`: the singular of the last plain-word segment, else `item`.
|
|
1200
|
+
* A fresh `{% for %}` inserted by the Studio names its item by the same rule. */
|
|
1179
1201
|
const legacyLoopAlias = (path) => {
|
|
1180
1202
|
const segment = path.split(".").at(-1) ?? "";
|
|
1181
1203
|
if (!IDENTIFIER_RE.test(segment)) return "item";
|
|
@@ -1705,4 +1727,4 @@ const evaluateCondition = (expression, data, namedConditions, _resolved = /* @__
|
|
|
1705
1727
|
return evaluateCondition$1(node, makeResolver(data, namedConditions, _resolved, _depth));
|
|
1706
1728
|
};
|
|
1707
1729
|
//#endregion
|
|
1708
|
-
export { ARRAY_FILTER_NAMES, BLOCK_DIRECTIVE_KINDS, CONDITION_RESERVED_WORDS, DATE_FORMAT_EXAMPLE_ISO, DATE_FORMAT_STYLES, DIRECTIVE_KINDS, FILTER_NAMES, LOOP_PROPERTIES, MARKER_DEFECT_KINDS, MARKER_OUTPUT_BODY, MARKER_STATEMENT_BODY, MAX_CONDITION_DEPTH, NUMERIC_FUNCTION_NAMES, arrayFiltersFromFieldConfig, assertNever, blockDirectiveLinePattern, classifyMarker, classifyMarkerDefect, clauseSlotKey, clauseSlotPattern, conditionHasFormula, conditionNodeSchema, conditionSchema, detectRowBlockPair, emptyCondition, evaluateCondition, evaluateNumericExpression, filtersFromFieldConfig, formatDate, hasBlockDirectivePattern, hasNumberingPattern, isArrayFilterName, isBlockDirectiveKind, isClauseSlotName, isFieldPath, isSafeFieldPath, legacyLoopAlias, legacyMarkerReplacement, loopPattern, markerPattern, normalizeMarkerInner, numPattern, parseCondition, placeholderPattern, qualifyLoopPath, qualifyRowScopedPath, qualifyRowScopedPlaceholder, refPattern, referencedConditionPaths, renderConditionTag, renderDeterministicFieldValue, renderFilterChain, renderForOpener, renderValueMarker, replaceOutputMarkers, resolvePath, rowScopePaths, scanInvalidMarkers, scanMarkers, serializeCondition, substitutionKey, translateLegacyExpression, unwritableFilterValues, unwritableMarkerLiteral };
|
|
1730
|
+
export { ARRAY_FILTER_NAMES, BLOCK_DIRECTIVE_KINDS, CONDITION_RESERVED_WORDS, DATE_FORMAT_EXAMPLE_ISO, DATE_FORMAT_STYLES, DIRECTIVE_KINDS, FILTER_NAMES, LOOP_PROPERTIES, MARKER_DEFECT_KINDS, MARKER_OUTPUT_BODY, MARKER_STATEMENT_BODY, MAX_CONDITION_DEPTH, MAX_CONDITION_NESTING, NUMERIC_FUNCTION_NAMES, arrayFiltersFromFieldConfig, assertNever, blockDirectiveLinePattern, classifyMarker, classifyMarkerDefect, clauseSlotKey, clauseSlotPattern, conditionHasFormula, conditionNodeSchema, conditionSchema, detectRowBlockPair, emptyCondition, evaluateCondition, evaluateNumericExpression, filtersFromFieldConfig, formatDate, hasBlockDirectivePattern, hasNumberingPattern, isArrayFilterName, isBlockDirectiveKind, isClauseSlotName, isFieldPath, isSafeFieldPath, legacyLoopAlias, legacyMarkerReplacement, loopPattern, markerPattern, normalizeMarkerInner, numPattern, parseCondition, placeholderPattern, qualifyLoopPath, qualifyRowScopedPath, qualifyRowScopedPlaceholder, refPattern, referencedConditionPaths, renderConditionTag, renderDeterministicFieldValue, renderFilterChain, renderForOpener, renderValueMarker, replaceOutputMarkers, resolvePath, rowScopePaths, scanInvalidMarkers, scanMarkers, serializeCondition, substitutionKey, translateLegacyExpression, unwritableFilterValues, unwritableMarkerLiteral };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stll/template-conditions",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3",
|
|
4
4
|
"description": "A document-template marker and condition engine: {{...}} marker grammar, parse, compute, serialize, field values.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"condition",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"test": "bun test",
|
|
44
44
|
"test:property": "bun test --preload @stll/property-testing/preload $(grep -rlF 'fc.assert' src --include='*.test.ts')",
|
|
45
45
|
"typecheck": "bun ../../packages/scripts/src/tsc-native.ts --noEmit",
|
|
46
|
-
"lint": "cd ../.. && bun --bun oxlint -c oxlint.config.ts --report-unused-disable-directives-severity=error --
|
|
46
|
+
"lint": "cd ../.. && bun --bun oxlint -c oxlint.config.ts --report-unused-disable-directives-severity=error --type-aware --type-check packages/template-conditions",
|
|
47
47
|
"lint:fix": "cd ../.. && bun --bun oxlint -c oxlint.config.ts --type-aware --fix packages/template-conditions",
|
|
48
48
|
"format": "bun ../../scripts/run-oxfmt.ts .",
|
|
49
49
|
"prepack": "bun run build"
|
|
@@ -51,13 +51,13 @@
|
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@stll/conditions": "^0.4.0",
|
|
53
53
|
"better-result": "3.0.1",
|
|
54
|
-
"temporal-polyfill": "1.0.
|
|
54
|
+
"temporal-polyfill": "1.0.5"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@stll/property-testing": "0.0.0",
|
|
58
58
|
"@stll/typescript-config": "0.0.0",
|
|
59
59
|
"bun-types": "1.4.2",
|
|
60
|
-
"fast-check": "^4.
|
|
60
|
+
"fast-check": "^4.10.1",
|
|
61
61
|
"tsdown": "0.23.0"
|
|
62
62
|
},
|
|
63
63
|
"main": "./dist/index.js",
|