@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 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 isRecord = (v) => typeof v === "object" && v !== null;
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 (!isRecord(current)) return;
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).sort((left, right) => right.length - left.length).map(escapeRegexLiteral).join("|");
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
- children.push(parseAnd());
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
- children.push(parseNot());
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: [parseNot()]
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.0",
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 --deny-warnings --type-aware packages/template-conditions",
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.4"
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.9.0",
60
+ "fast-check": "^4.10.1",
61
61
  "tsdown": "0.23.0"
62
62
  },
63
63
  "main": "./dist/index.js",