@stll/template-conditions 0.5.4 → 0.6.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.d.ts CHANGED
@@ -181,9 +181,14 @@ type MarkerMeta = {
181
181
  } | {
182
182
  kind: "if";
183
183
  expr: string;
184
+ /** Filters on a tag whose whole body is one field path configure THAT
185
+ * boolean: a question the document only asks has no value marker to
186
+ * carry a chain. A tag holding a real expression carries none. */
187
+ filters: readonly FilterCall[];
184
188
  } | {
185
189
  kind: "elif";
186
190
  expr: string;
191
+ filters: readonly FilterCall[];
187
192
  } | {
188
193
  kind: "else";
189
194
  } | {
@@ -525,17 +530,24 @@ type ConditionTagOptions = {
525
530
  kind: "if" | "elif";
526
531
  /** The `{% if %}` expression, already serialized. */
527
532
  expression: string;
533
+ /** The chain the boolean this tag names carries. Only a tag whose whole
534
+ * expression is one field path reads one back, so an expression tag writes
535
+ * none. */
536
+ filters: readonly FilterCall[];
528
537
  /** The placement the tag already carried. */
529
538
  prefix: MarkerPrefix;
530
539
  };
531
540
  /**
532
- * One `{% if %}` / `{% elif %}` opener carrying its expression.
541
+ * One `{% if %}` / `{% elif %}` opener carrying its expression, and the field
542
+ * configuration of the boolean it asks for.
533
543
  *
534
544
  * A rule that decides whether a block shows belongs in the tag that shows it:
535
545
  * the tag is the only place a document has for it, so a builder's rule is
536
- * serialized here rather than stored beside the document under a name.
546
+ * serialized here rather than stored beside the document under a name. The
547
+ * same is true of what to call the question and how to decide it: a condition
548
+ * the document never prints has no value marker to hold that chain either.
537
549
  */
538
- export declare const renderConditionTag: ({ expression, kind, prefix }: ConditionTagOptions) => string;
550
+ export declare const renderConditionTag: ({ expression, filters, kind, prefix }: ConditionTagOptions) => string;
539
551
  type ForOpenerOptions = {
540
552
  alias: string;
541
553
  path: string;
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
  });
@@ -208,7 +216,7 @@ const RESERVED_WORDS = new Set(CONDITION_RESERVED_WORDS);
208
216
  const isConditionKeyword = (raw) => raw !== "true" && raw !== "false" && RESERVED_WORDS.has(raw);
209
217
  /** The keyword alternation of the tokenizer, derived from the same list. */
210
218
  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("|");
219
+ const COMPARE_SYMBOL_PATTERN = Object.keys(COMPARE_SYMBOL_TO_OP).toSorted((left, right) => right.length - left.length).map(escapeRegexLiteral).join("|");
212
220
  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
221
  const STARTS_WITH_DIGIT_RE = /^-?\d/u;
214
222
  const isCompareSymbol = (raw) => Object.hasOwn(COMPARE_SYMBOL_TO_OP, raw);
@@ -945,6 +953,32 @@ const readPrefix = (token) => token === "p" || token === "tr" ? PREFIX_BY_TOKEN[
945
953
  const TAG_RE = /^(?<tag>[\p{L}_][\p{L}\p{N}_]*)\b(?<rest>[\s\S]*)$/u;
946
954
  const FOR_RE = /^(?<alias>[\p{L}_][\p{L}\p{N}_-]*)\s+in\s+(?<path>[\p{L}\p{N}_.-]+)\s*(?<filters>\|[\s\S]*)?$/u;
947
955
  const CALL_RE = /^(?<name>[\p{L}_][\p{L}\p{N}_]*)\s*\(/u;
956
+ const CONDITION_RE = /^(?<path>[\p{L}\p{N}_.-]+)\s*(?<filters>\|[\s\S]*)$/u;
957
+ /**
958
+ * Read an `{% if %}` / `{% elif %}` body: one field path plus the chain that
959
+ * configures the boolean it names, or an expression the author wrote.
960
+ *
961
+ * A tail the filter catalogue does not read is part of that expression
962
+ * (`items|length > 0`), never a configuration, so the body stays verbatim
963
+ * rather than being rejected: a tag the grammar refuses would take its whole
964
+ * block with it.
965
+ */
966
+ const readConditionBody = (rest) => {
967
+ const match = CONDITION_RE.exec(rest);
968
+ if (!match) return {
969
+ expr: rest,
970
+ filters: []
971
+ };
972
+ const chain = scanFilterChain(match.groups?.["filters"] ?? "");
973
+ if (chain === null || "unknownFilter" in chain || chain.filters.length === 0) return {
974
+ expr: rest,
975
+ filters: []
976
+ };
977
+ return {
978
+ expr: match.groups?.["path"] ?? rest,
979
+ filters: chain.filters
980
+ };
981
+ };
948
982
  /** Classify a `{% ... %}` tag. */
949
983
  const classifyStatement = (inner) => {
950
984
  const match = TAG_RE.exec(inner);
@@ -952,14 +986,22 @@ const classifyStatement = (inner) => {
952
986
  const tag = match.groups?.["tag"] ?? "";
953
987
  const rest = (match.groups?.["rest"] ?? "").trim();
954
988
  switch (tag) {
955
- case "if": return {
956
- kind: "if",
957
- expr: rest
958
- };
959
- case "elif": return {
960
- kind: "elif",
961
- expr: rest
962
- };
989
+ case "if": {
990
+ const { expr, filters } = readConditionBody(rest);
991
+ return {
992
+ kind: "if",
993
+ expr,
994
+ filters
995
+ };
996
+ }
997
+ case "elif": {
998
+ const { expr, filters } = readConditionBody(rest);
999
+ return {
1000
+ kind: "elif",
1001
+ expr,
1002
+ filters
1003
+ };
1004
+ }
963
1005
  case "else": return rest === "" ? { kind: "else" } : null;
964
1006
  case "endif": return rest === "" ? { kind: "endif" } : null;
965
1007
  case "endfor": return rest === "" ? { kind: "endfor" } : null;
@@ -1466,13 +1508,20 @@ const PREFIX_TOKEN = {
1466
1508
  row: "tr"
1467
1509
  };
1468
1510
  /**
1469
- * One `{% if %}` / `{% elif %}` opener carrying its expression.
1511
+ * One `{% if %}` / `{% elif %}` opener carrying its expression, and the field
1512
+ * configuration of the boolean it asks for.
1470
1513
  *
1471
1514
  * A rule that decides whether a block shows belongs in the tag that shows it:
1472
1515
  * the tag is the only place a document has for it, so a builder's rule is
1473
- * serialized here rather than stored beside the document under a name.
1516
+ * serialized here rather than stored beside the document under a name. The
1517
+ * same is true of what to call the question and how to decide it: a condition
1518
+ * the document never prints has no value marker to hold that chain either.
1474
1519
  */
1475
- const renderConditionTag = ({ expression, kind, prefix }) => `{%${PREFIX_TOKEN[prefix]} ${kind} ${expression} %}`;
1520
+ const renderConditionTag = ({ expression, filters, kind, prefix }) => {
1521
+ const chain = renderFilterChain(filters);
1522
+ const head = `{%${PREFIX_TOKEN[prefix]} ${kind} ${expression}`;
1523
+ return chain === "" ? `${head} %}` : `${head} | ${chain} %}`;
1524
+ };
1476
1525
  /** One `{% for %}` opener, keeping its placement prefix. */
1477
1526
  const renderForOpener = ({ alias, filters, path, prefix }) => {
1478
1527
  const chain = renderFilterChain(filters);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stll/template-conditions",
3
- "version": "0.5.4",
3
+ "version": "0.6.2",
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 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",