@stll/template-conditions 0.0.1-placeholder.0 → 0.1.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/README.md ADDED
@@ -0,0 +1,42 @@
1
+ # @stll/template-conditions
2
+
3
+ A document-template marker and condition engine: the `{{...}}` marker grammar,
4
+ an expression parser, numeric/arithmetic compute, a condition-builder
5
+ serializer, and deterministic field-value rendering. Small, side-effect-free
6
+ functions usable on both backend (Bun) and frontend (browser).
7
+
8
+ Boolean conditions are evaluated through the canonical
9
+ [`@stll/conditions`](https://github.com/stella/stella/tree/main/packages/conditions)
10
+ AST and its single evaluator, so template `{{#if ...}}` conditionals share one
11
+ set of operators and semantics with the rest of the system. This package owns
12
+ the template surface: the marker grammar, the string parser, the named-condition
13
+ resolver, the numeric expression evaluator, and the no-code builder serializer.
14
+
15
+ ```ts
16
+ import { evaluateCondition, scanMarkers } from "@stll/template-conditions";
17
+
18
+ const branch = evaluateCondition("isCompany and signed", fillData);
19
+
20
+ for (const marker of scanMarkers(templateText)) {
21
+ // marker.kind, marker.path, ...
22
+ }
23
+ ```
24
+
25
+ ## Install
26
+
27
+ ```sh
28
+ bun add @stll/template-conditions
29
+ ```
30
+
31
+ ## Exports (`.`)
32
+
33
+ - Condition evaluation: `evaluateCondition`, `parseCondition`,
34
+ `evaluateNumericExpression`, `serializeCondition`, `resolvePath`.
35
+ - Field values: `renderDeterministicFieldValue`, `renderComposite`,
36
+ `formatDate`.
37
+ - Marker grammar: `scanMarkers`, `classifyMarker`, `markerPattern`,
38
+ `placeholderPattern`, and the directive-kind constants.
39
+
40
+ ## License
41
+
42
+ Apache-2.0
@@ -0,0 +1,248 @@
1
+ import { CompareNode, CompareOp, Condition, ConditionNode, ConditionNode as ConditionNode$1, GroupNode, Operand, PredicateNode, PredicateOp, conditionHasFormula, conditionNodeSchema, conditionSchema, emptyCondition } from "@stll/conditions";
2
+
3
+ //#region src/path.d.ts
4
+ /** Resolve a dotted path like `company.name` against data. */
5
+ declare const resolvePath: (path: string, data: Record<string, unknown>) => unknown;
6
+ //#endregion
7
+ //#region src/parse.d.ts
8
+ declare const parseCondition: (expression: string) => ConditionNode$1 | null;
9
+ //#endregion
10
+ //#region src/compute.d.ts
11
+ /**
12
+ * Arithmetic evaluator for computed template fields.
13
+ *
14
+ * The condition engine ([[evaluateCondition]]) is boolean-only by design.
15
+ * Computed fields need a value, so this is a separate, pure recursive-descent
16
+ * evaluator: `+ - * / %`, unary `-`, parentheses, numeric literals (with `_`
17
+ * separators and decimals), dotted-path variables resolved from the fill data,
18
+ * and the functions `min`, `max`, `round` (optional 2nd arg = decimal places),
19
+ * `abs`, `floor`, `ceil`.
20
+ *
21
+ * Example — rent indexed by CPI but capped at +5%/yr:
22
+ * `min(rent * (1 + index / 100), rent * 1.05)`
23
+ *
24
+ * Returns `undefined` on any parse or resolution failure (bad syntax, a
25
+ * non-numeric variable, division producing a non-finite result) so callers
26
+ * can fall back to leaving the field unfilled rather than emitting `NaN`.
27
+ */
28
+ declare const evaluateNumericExpression: (expression: string, data: Record<string, unknown>) => number | undefined;
29
+ //#endregion
30
+ //#region src/field-values.d.ts
31
+ /**
32
+ * Single source of truth for the DETERMINISTIC field-value transforms a
33
+ * template fill applies — composite (parts joined by a `{{key}}` format),
34
+ * formula (arithmetic over the other values), and locale-aware date rendering.
35
+ *
36
+ * Both the API fill engine (apps/api/src/handlers/docx) and the web live
37
+ * preview (template-studio.tsx) MUST route their rendering through
38
+ * {@link renderDeterministicFieldValue} so the preview and the generated
39
+ * document produce byte-identical strings. The api/field-value-parity.test.ts
40
+ * asserts this; reintroducing a bespoke transform on either side fails CI.
41
+ *
42
+ * Out of scope (non-deterministic / server-only, handled by callers):
43
+ * - registry lookup (needs a network hit; the preview overlays it async)
44
+ * - AI drafting / adaptation
45
+ *
46
+ * Pure: no IO, no model/provider dependency. The config types here are minimal
47
+ * STRUCTURAL shapes so the package stays free of api/web imports; callers pass
48
+ * their own field objects, which structurally satisfy these.
49
+ */
50
+ /** Locale-aware date rendering config. "iso" leaves the submitted value as-is;
51
+ * the other styles map to `Intl.DateTimeFormat` `dateStyle`. */
52
+ type FieldDateFormat = {
53
+ /** BCP-47 language tag of the document, e.g. "cs", "de", "pl". */locale: string;
54
+ style: "long" | "medium" | "short" | "iso";
55
+ };
56
+ /**
57
+ * Format an ISO date (YYYY-MM-DD) per the field's locale + style via
58
+ * `Intl.DateTimeFormat` — ICU produces the correct localized (and, in date
59
+ * contexts, correctly inflected) month names, e.g. cs long → "13. června
60
+ * 2028". The "iso" style returns the value unchanged; an invalid calendar date
61
+ * returns null.
62
+ */
63
+ declare const formatDate: (value: string, dateFormat: FieldDateFormat) => string | null;
64
+ /** Minimal structural shape of one composite field part: only the `key`
65
+ * referenced by the join format is needed to render. Callers' richer part
66
+ * types (api FieldPart, web EditablePart) structurally satisfy this. */
67
+ type PartConfig = {
68
+ key: string;
69
+ };
70
+ /**
71
+ * Render a `{{key}}` format over part values. Markers whose key has no part
72
+ * value are left as-is (a visible authoring artifact, not a render error), so
73
+ * a partially filled composite previews exactly as the fill engine renders it.
74
+ */
75
+ declare const renderComposite: (parts: readonly PartConfig[], format: string, partValues: Readonly<Record<string, string>>) => string;
76
+ /**
77
+ * Minimal STRUCTURAL config of a field, carrying only what the deterministic
78
+ * transforms read. Callers (api FieldMeta, web StudioField) structurally
79
+ * satisfy this; lookup/AI/scalar concerns are intentionally absent because the
80
+ * dispatcher does not handle them.
81
+ */
82
+ type DeterministicFieldConfig = {
83
+ path: string;
84
+ inputType?: string | undefined;
85
+ parts?: readonly PartConfig[] | undefined;
86
+ format?: string | undefined;
87
+ formula?: string | undefined;
88
+ dateFormat?: FieldDateFormat | undefined;
89
+ };
90
+ /**
91
+ * THE deterministic field-value dispatcher: returns the string the fill engine
92
+ * writes for `field` given the submitted `values`, or null when the field has
93
+ * no deterministic transform (a scalar, lookup, or AI field the CALLER renders
94
+ * itself).
95
+ *
96
+ * Dispatch order mirrors the API fill pipeline (composite → formula → date):
97
+ * - composite (parts + format present) → {@link renderComposite}
98
+ * - else formula present → {@link evaluateNumericExpression}, stringified
99
+ * - else date (inputType "date" + dateFormat) → {@link formatDate}
100
+ * - else null
101
+ *
102
+ * Composite returns null when its value is not an object of part values (a
103
+ * plain string passes through unchanged on the api side, so the caller's
104
+ * scalar path handles it). Formula and date return null when the expression /
105
+ * value does not yield a value, so the caller leaves the field as-is.
106
+ */
107
+ declare const renderDeterministicFieldValue: (field: DeterministicFieldConfig, values: Record<string, unknown>) => string | null;
108
+ //#endregion
109
+ //#region src/condition-builder.d.ts
110
+ /**
111
+ * Serialize a condition tree into an expression for `evaluateCondition`.
112
+ * Returns an empty string for an empty tree (caller treats that as "no
113
+ * condition" / always-visible).
114
+ */
115
+ declare const serializeCondition: (node: ConditionNode$1) => string;
116
+ //#endregion
117
+ //#region src/markers.d.ts
118
+ /**
119
+ * Canonical grammar for `{{...}}` template markers — the single source of truth
120
+ * shared by the fill pipeline (api), the editor highlighter (folio), and the
121
+ * read-only preview (web).
122
+ *
123
+ * Add a new directive in ONE place: append its `kind` to {@link DIRECTIVE_KINDS},
124
+ * add a `MarkerMeta` member, and teach {@link classifyMarker} to recognize it.
125
+ * Every scanner that uses {@link scanMarkers} / {@link classifyMarker} then sees
126
+ * it automatically, and every exhaustive `switch` over a marker's kind (closed
127
+ * with {@link assertNever}) fails to compile at any consumer that has not yet
128
+ * handled it. That is what makes "added a directive server-side but a recognizer
129
+ * elsewhere silently ignored it" a build error rather than a latent bug.
130
+ *
131
+ * No consumer should define its own `{{...}}` regex. The low-level pattern
132
+ * factories below exist so handlers that mutate raw XML/text reuse the exact
133
+ * same literals; recognizers should prefer the high-level scanner.
134
+ */
135
+ declare const DIRECTIVE_KINDS: readonly ["placeholder", "clause", "num", "ref", "index", "count", "if", "elseif", "else", "endif", "each", "endeach"];
136
+ type DirectiveKind = (typeof DIRECTIVE_KINDS)[number];
137
+ /** Directives that occupy their own paragraph (block-level), opener→closer. */
138
+ declare const BLOCK_DIRECTIVE_KINDS: readonly ["if", "elseif", "else", "endif", "each", "endeach"];
139
+ declare const isBlockDirectiveKind: (kind: DirectiveKind) => boolean;
140
+ type MarkerMeta = {
141
+ kind: "placeholder";
142
+ expr: string;
143
+ } | {
144
+ kind: "clause";
145
+ name: string;
146
+ version: string | undefined;
147
+ } | {
148
+ kind: "num";
149
+ key: string;
150
+ } | {
151
+ kind: "ref";
152
+ key: string;
153
+ } | {
154
+ kind: "index";
155
+ } | {
156
+ kind: "count";
157
+ } | {
158
+ kind: "if";
159
+ expr: string;
160
+ } | {
161
+ kind: "elseif";
162
+ expr: string;
163
+ } | {
164
+ kind: "else";
165
+ } | {
166
+ kind: "endif";
167
+ } | {
168
+ kind: "each";
169
+ expr: string;
170
+ } | {
171
+ kind: "endeach";
172
+ };
173
+ /** Any `{{...}}` marker; group 1 is the inner text (untrimmed — callers trim). */
174
+ declare const markerPattern: () => RegExp;
175
+ /** Inline field / clause / numbering marker (inner allows the directive sigils). */
176
+ declare const placeholderPattern: () => RegExp;
177
+ /** `{{@clause:Name}}` / `{{@clause:Name:v3}}` — `name` and `modifier` groups. */
178
+ declare const clauseSlotPattern: () => RegExp;
179
+ /** `{{@num:Key}}` — the `key` group. */
180
+ declare const numPattern: () => RegExp;
181
+ /** `{{@ref:Key}}` — the `key` group. */
182
+ declare const refPattern: () => RegExp;
183
+ /** `{{@index}}` — the 1-based position within the innermost enclosing loop. */
184
+ declare const indexPattern: () => RegExp;
185
+ /** `{{@count}}` — the item count of the innermost enclosing loop. */
186
+ declare const countPattern: () => RegExp;
187
+ /** Cheap presence test for any numbering marker (no capture). */
188
+ declare const hasNumberingPattern: () => RegExp;
189
+ /** Cheap presence test for any block directive (no capture). */
190
+ declare const hasBlockDirectivePattern: () => RegExp;
191
+ /** A whole line that is a single block directive — `tag` and `expr` groups. */
192
+ declare const blockDirectiveLinePattern: () => RegExp;
193
+ /** Whether `value` is a valid field path per the marker grammar (dotted
194
+ * segments of letters/digits/underscore/dash — no brackets or spaces). */
195
+ declare const isFieldPath: (value: string) => boolean;
196
+ /**
197
+ * Classify the inner text of a `{{...}}` marker. Returns `null` when the text is
198
+ * not a recognized directive (so a recognizer can leave stray braces alone).
199
+ */
200
+ declare const classifyMarker: (innerRaw: string) => MarkerMeta | null;
201
+ type ScannedMarker = {
202
+ /** Offset of `{{` in the source text. */start: number; /** Offset just past `}}`. */
203
+ end: number; /** The full matched marker, e.g. `{{@num:scope}}`. */
204
+ raw: string; /** The (untrimmed) inner text. */
205
+ inner: string;
206
+ meta: MarkerMeta;
207
+ };
208
+ /**
209
+ * Find every recognized `{{...}}` marker in `text`, in document order, with
210
+ * offsets and classified metadata. Unrecognized `{{...}}` spans are skipped.
211
+ */
212
+ declare const scanMarkers: (text: string) => ScannedMarker[];
213
+ /** Exhaustiveness guard — pass the discriminant in a `switch` default branch. */
214
+ declare const assertNever: (value: never) => never;
215
+ //#endregion
216
+ //#region src/index.d.ts
217
+ type NamedCondition = {
218
+ name: string; /** The `{{#if}}` expression string. Authoritative unless `node` is set. */
219
+ expression: string;
220
+ /**
221
+ * The canonical AST, set when the condition cannot round-trip through the
222
+ * expression string — i.e. it uses a `formula` operand the `{{#if}}` grammar
223
+ * has no syntax for. When present it is evaluated directly and `expression`
224
+ * is ignored.
225
+ */
226
+ node?: ConditionNode$1;
227
+ label?: string;
228
+ };
229
+ /**
230
+ * Maximum recursion depth for named condition resolution.
231
+ * Prevents stack overflow when a long chain of named
232
+ * conditions (A→B→C→...→Z) is evaluated without cycles.
233
+ */
234
+ declare const MAX_CONDITION_DEPTH = 50;
235
+ /**
236
+ * Evaluate a template `{{#if ...}}` condition. The expression is parsed into
237
+ * the canonical `@stll/conditions` AST and evaluated by the shared evaluator.
238
+ *
239
+ * Supports comparisons (`==`, `!=`, `>`, `<`, `>=`, `<=`), `contains`,
240
+ * truthiness, negation (`!`), logical operators (`and`, `or`), parentheses,
241
+ * dotted paths, numeric underscores, and named-condition references.
242
+ *
243
+ * An empty or unparseable expression is `false` ("no condition" never gates a
244
+ * branch true).
245
+ */
246
+ declare const evaluateCondition: (expression: string, data: Record<string, unknown>, namedConditions?: readonly NamedCondition[], _resolved?: ReadonlySet<string>, _depth?: number) => boolean;
247
+ //#endregion
248
+ export { BLOCK_DIRECTIVE_KINDS, type CompareNode, type CompareOp, type Condition, type ConditionNode, DIRECTIVE_KINDS, type DeterministicFieldConfig, type DirectiveKind, type FieldDateFormat, type GroupNode, MAX_CONDITION_DEPTH, type MarkerMeta, NamedCondition, type Operand, type PartConfig, type PredicateNode, type PredicateOp, type ScannedMarker, assertNever, blockDirectiveLinePattern, classifyMarker, clauseSlotPattern, conditionHasFormula, conditionNodeSchema, conditionSchema, countPattern, emptyCondition, evaluateCondition, evaluateNumericExpression, formatDate, hasBlockDirectivePattern, hasNumberingPattern, indexPattern, isBlockDirectiveKind, isFieldPath, markerPattern, numPattern, parseCondition, placeholderPattern, refPattern, renderComposite, renderDeterministicFieldValue, resolvePath, scanMarkers, serializeCondition };
package/dist/index.js ADDED
@@ -0,0 +1,715 @@
1
+ import { conditionHasFormula, conditionNodeSchema, conditionSchema, emptyCondition, evaluateCondition as evaluateCondition$1 } from "@stll/conditions";
2
+ import { panic } from "better-result";
3
+ //#region src/path.ts
4
+ /**
5
+ * Dotted-path resolution against a data record. Extracted from index.ts so both
6
+ * the condition engine (index.ts) and the arithmetic evaluator (compute.ts) can
7
+ * import it without a dependency cycle.
8
+ */
9
+ /** Narrow `unknown` to a string-keyed record. */
10
+ const isRecord = (v) => typeof v === "object" && v !== null;
11
+ /** Resolve a dotted path like `company.name` against data. */
12
+ const resolvePath = (path, data) => {
13
+ if (Object.hasOwn(data, path)) return data[path];
14
+ const parts = path.split(".");
15
+ let current = data;
16
+ for (const part of parts) {
17
+ if (!isRecord(current)) return;
18
+ current = current[part];
19
+ }
20
+ return current;
21
+ };
22
+ //#endregion
23
+ //#region src/compute.ts
24
+ /**
25
+ * Arithmetic evaluator for computed template fields.
26
+ *
27
+ * The condition engine ([[evaluateCondition]]) is boolean-only by design.
28
+ * Computed fields need a value, so this is a separate, pure recursive-descent
29
+ * evaluator: `+ - * / %`, unary `-`, parentheses, numeric literals (with `_`
30
+ * separators and decimals), dotted-path variables resolved from the fill data,
31
+ * and the functions `min`, `max`, `round` (optional 2nd arg = decimal places),
32
+ * `abs`, `floor`, `ceil`.
33
+ *
34
+ * Example — rent indexed by CPI but capped at +5%/yr:
35
+ * `min(rent * (1 + index / 100), rent * 1.05)`
36
+ *
37
+ * Returns `undefined` on any parse or resolution failure (bad syntax, a
38
+ * non-numeric variable, division producing a non-finite result) so callers
39
+ * can fall back to leaving the field unfilled rather than emitting `NaN`.
40
+ */
41
+ const TOKEN_RE$1 = /(?<numeric>[0-9][0-9_]*(?:\.[0-9]+)?)|(?<ident>[\p{L}_][\p{L}\p{N}_.]*(?:-[\p{L}\p{N}_.]+)*)|(?<punct>[+\-*/%(),])|(?<invalid>\S)/gu;
42
+ const PRECEDENCE = {
43
+ "+": 1,
44
+ "-": 1,
45
+ "*": 2,
46
+ "/": 2,
47
+ "%": 2
48
+ };
49
+ const tokenize$1 = (expression) => {
50
+ const tokens = [];
51
+ for (const match of expression.matchAll(TOKEN_RE$1)) {
52
+ const { numeric, ident, punct, invalid } = match.groups ?? {};
53
+ if (invalid !== void 0) throw new Error(`Unexpected token: ${invalid}`);
54
+ if (numeric !== void 0) tokens.push({
55
+ t: "num",
56
+ v: Number(numeric.replace(/_/gu, ""))
57
+ });
58
+ else if (ident !== void 0) tokens.push({
59
+ t: "id",
60
+ v: ident
61
+ });
62
+ else if (punct === "(") tokens.push({ t: "lp" });
63
+ else if (punct === ")") tokens.push({ t: "rp" });
64
+ else if (punct === ",") tokens.push({ t: "comma" });
65
+ else if (punct !== void 0) tokens.push({
66
+ t: "op",
67
+ v: punct
68
+ });
69
+ }
70
+ return tokens;
71
+ };
72
+ const applyOp = (op, a, b) => {
73
+ switch (op) {
74
+ case "+": return a + b;
75
+ case "-": return a - b;
76
+ case "*": return a * b;
77
+ case "/": return a / b;
78
+ default: return a % b;
79
+ }
80
+ };
81
+ const callFunction = (name, args) => {
82
+ switch (name) {
83
+ case "min": return Math.min(...args);
84
+ case "max": return Math.max(...args);
85
+ case "abs": return Math.abs(args[0] ?? NaN);
86
+ case "floor": return Math.floor(args[0] ?? NaN);
87
+ case "ceil": return Math.ceil(args[0] ?? NaN);
88
+ case "round": {
89
+ const value = args[0] ?? NaN;
90
+ const digits = args[1] ?? 0;
91
+ const factor = 10 ** Math.trunc(digits);
92
+ return Math.round(value * factor) / factor;
93
+ }
94
+ default: throw new Error(`Unknown function: ${name}`);
95
+ }
96
+ };
97
+ const evaluateNumericExpression = (expression, data) => {
98
+ try {
99
+ const tokens = tokenize$1(expression);
100
+ if (tokens.length === 0) return;
101
+ let pos = 0;
102
+ const resolveIdentifier = (path) => {
103
+ const raw = resolvePath(path, data);
104
+ const num = typeof raw === "number" ? raw : Number(raw);
105
+ if (!Number.isFinite(num)) throw new TypeError(`Non-numeric variable: ${path}`);
106
+ return num;
107
+ };
108
+ const parseAtom = () => {
109
+ const tok = tokens[pos];
110
+ if (!tok) throw new Error("Unexpected end of expression");
111
+ if (tok.t === "op" && (tok.v === "-" || tok.v === "+")) {
112
+ pos += 1;
113
+ const operand = parseAtom();
114
+ return tok.v === "-" ? -operand : operand;
115
+ }
116
+ if (tok.t === "num") {
117
+ pos += 1;
118
+ return tok.v;
119
+ }
120
+ if (tok.t === "lp") {
121
+ pos += 1;
122
+ const value = parseBinary(0);
123
+ if (tokens[pos]?.t !== "rp") throw new Error("Expected )");
124
+ pos += 1;
125
+ return value;
126
+ }
127
+ if (tok.t === "id") {
128
+ pos += 1;
129
+ if (tokens[pos]?.t === "lp") {
130
+ pos += 1;
131
+ const args = [];
132
+ if (tokens[pos]?.t !== "rp") {
133
+ args.push(parseBinary(0));
134
+ while (tokens[pos]?.t === "comma") {
135
+ pos += 1;
136
+ args.push(parseBinary(0));
137
+ }
138
+ }
139
+ if (tokens[pos]?.t !== "rp") throw new Error("Expected )");
140
+ pos += 1;
141
+ return callFunction(tok.v, args);
142
+ }
143
+ return resolveIdentifier(tok.v);
144
+ }
145
+ throw new Error("Unexpected token in expression");
146
+ };
147
+ const parseBinary = (minPrecedence) => {
148
+ let left = parseAtom();
149
+ let next = tokens[pos];
150
+ while (next?.t === "op" && (PRECEDENCE[next.v] ?? 0) >= minPrecedence) {
151
+ const op = next.v;
152
+ pos += 1;
153
+ const right = parseBinary((PRECEDENCE[op] ?? 0) + 1);
154
+ left = applyOp(op, left, right);
155
+ next = tokens[pos];
156
+ }
157
+ return left;
158
+ };
159
+ const result = parseBinary(0);
160
+ if (pos !== tokens.length) return;
161
+ return Number.isFinite(result) ? result : void 0;
162
+ } catch {
163
+ return;
164
+ }
165
+ };
166
+ //#endregion
167
+ //#region src/parse.ts
168
+ const TOKEN_RE = /(?<token>"[^"\\]*(?:\\.[^"\\]*)*"|==|!=|>=|<=|>|<|!(?!=)|and\b|or\b|contains\b|[()]|-?\d[\p{N}_.]*|[\p{L}\p{N}_.]+(?:-[\p{L}\p{N}_.]+)*)/gu;
169
+ const STARTS_WITH_DIGIT_RE = /^-?\d/u;
170
+ const COMPARE_SYMBOL_TO_OP = {
171
+ "==": "eq",
172
+ "!=": "neq",
173
+ ">": "gt",
174
+ "<": "lt",
175
+ ">=": "gte",
176
+ "<=": "lte"
177
+ };
178
+ const tokenize = (expr) => {
179
+ const tokens = [];
180
+ for (const m of expr.matchAll(TOKEN_RE)) {
181
+ const raw = m.groups?.["token"] ?? m[0];
182
+ if (raw === "and") tokens.push({ type: "and" });
183
+ else if (raw === "or") tokens.push({ type: "or" });
184
+ else if (raw === "!") tokens.push({ type: "not" });
185
+ else if (raw === "==" || raw === "!=" || raw === ">" || raw === "<" || raw === ">=" || raw === "<=" || raw === "contains") tokens.push({
186
+ type: "op",
187
+ raw
188
+ });
189
+ else if (raw === "(") tokens.push({ type: "lparen" });
190
+ else if (raw === ")") tokens.push({ type: "rparen" });
191
+ else if (raw.startsWith("\"")) tokens.push({
192
+ type: "string",
193
+ raw: raw.slice(1, -1).replace(/\\"/gu, "\"")
194
+ });
195
+ else tokens.push({
196
+ type: "value",
197
+ raw
198
+ });
199
+ }
200
+ return tokens;
201
+ };
202
+ /** Parse a numeric literal, supporting `_` separators. */
203
+ const parseNumeric = (raw) => {
204
+ if (!STARTS_WITH_DIGIT_RE.test(raw)) return;
205
+ const n = Number(raw.replace(/_/gu, ""));
206
+ return Number.isFinite(n) ? n : void 0;
207
+ };
208
+ /** A `value`/`string` token becomes a literal (string / number / boolean) or a
209
+ * dotted `path` operand. */
210
+ const operandFromToken = (token) => {
211
+ if (token.type === "string") return {
212
+ type: "literal",
213
+ value: token.raw
214
+ };
215
+ const num = parseNumeric(token.raw);
216
+ if (num !== void 0) return {
217
+ type: "literal",
218
+ value: num
219
+ };
220
+ if (token.raw === "true") return {
221
+ type: "literal",
222
+ value: true
223
+ };
224
+ if (token.raw === "false") return {
225
+ type: "literal",
226
+ value: false
227
+ };
228
+ return {
229
+ type: "path",
230
+ path: token.raw
231
+ };
232
+ };
233
+ /** `contains` carries a literal payload (`string`), not an operand: render the
234
+ * right-hand token as a string. */
235
+ const containsValue = (token) => {
236
+ const operand = operandFromToken(token);
237
+ return operand.type === "literal" ? String(operand.value) : token.raw;
238
+ };
239
+ const parseCondition = (expression) => {
240
+ const tokens = tokenize(expression);
241
+ if (tokens.length === 0) return null;
242
+ let pos = 0;
243
+ const peek = () => tokens[pos];
244
+ const parseOr = () => {
245
+ const first = parseAnd();
246
+ if (peek()?.type !== "or") return first;
247
+ const children = [first];
248
+ while (peek()?.type === "or") {
249
+ pos += 1;
250
+ children.push(parseAnd());
251
+ }
252
+ return {
253
+ type: "group",
254
+ combinator: "or",
255
+ children
256
+ };
257
+ };
258
+ const parseAnd = () => {
259
+ const first = parseNot();
260
+ if (peek()?.type !== "and") return first;
261
+ const children = [first];
262
+ while (peek()?.type === "and") {
263
+ pos += 1;
264
+ children.push(parseNot());
265
+ }
266
+ return {
267
+ type: "group",
268
+ combinator: "and",
269
+ children
270
+ };
271
+ };
272
+ const parseNot = () => {
273
+ if (peek()?.type === "not") {
274
+ pos += 1;
275
+ return {
276
+ type: "group",
277
+ combinator: "and",
278
+ negated: true,
279
+ children: [parseNot()]
280
+ };
281
+ }
282
+ return parseComparison();
283
+ };
284
+ const parseComparison = () => {
285
+ if (peek()?.type === "lparen") {
286
+ pos += 1;
287
+ const inner = parseOr();
288
+ if (peek()?.type === "rparen") pos += 1;
289
+ return inner;
290
+ }
291
+ const leftTok = peek();
292
+ if (!leftTok || leftTok.type !== "value" && leftTok.type !== "string") {
293
+ pos += 1;
294
+ return {
295
+ type: "group",
296
+ combinator: "and",
297
+ children: []
298
+ };
299
+ }
300
+ pos += 1;
301
+ const left = operandFromToken(leftTok);
302
+ const opTok = peek();
303
+ if (opTok?.type !== "op") return {
304
+ type: "predicate",
305
+ operand: left,
306
+ op: "is_truthy"
307
+ };
308
+ pos += 1;
309
+ const rightTok = peek();
310
+ if (!rightTok || rightTok.type !== "value" && rightTok.type !== "string") return {
311
+ type: "predicate",
312
+ operand: left,
313
+ op: "is_truthy"
314
+ };
315
+ pos += 1;
316
+ if (opTok.raw === "contains") return {
317
+ type: "predicate",
318
+ operand: left,
319
+ op: "contains",
320
+ value: containsValue(rightTok)
321
+ };
322
+ return {
323
+ type: "compare",
324
+ left,
325
+ op: COMPARE_SYMBOL_TO_OP[opTok.raw] ?? "eq",
326
+ right: operandFromToken(rightTok)
327
+ };
328
+ };
329
+ return parseOr();
330
+ };
331
+ //#endregion
332
+ //#region src/markers.ts
333
+ /**
334
+ * Canonical grammar for `{{...}}` template markers — the single source of truth
335
+ * shared by the fill pipeline (api), the editor highlighter (folio), and the
336
+ * read-only preview (web).
337
+ *
338
+ * Add a new directive in ONE place: append its `kind` to {@link DIRECTIVE_KINDS},
339
+ * add a `MarkerMeta` member, and teach {@link classifyMarker} to recognize it.
340
+ * Every scanner that uses {@link scanMarkers} / {@link classifyMarker} then sees
341
+ * it automatically, and every exhaustive `switch` over a marker's kind (closed
342
+ * with {@link assertNever}) fails to compile at any consumer that has not yet
343
+ * handled it. That is what makes "added a directive server-side but a recognizer
344
+ * elsewhere silently ignored it" a build error rather than a latent bug.
345
+ *
346
+ * No consumer should define its own `{{...}}` regex. The low-level pattern
347
+ * factories below exist so handlers that mutate raw XML/text reuse the exact
348
+ * same literals; recognizers should prefer the high-level scanner.
349
+ */
350
+ const DIRECTIVE_KINDS = [
351
+ "placeholder",
352
+ "clause",
353
+ "num",
354
+ "ref",
355
+ "index",
356
+ "count",
357
+ "if",
358
+ "elseif",
359
+ "else",
360
+ "endif",
361
+ "each",
362
+ "endeach"
363
+ ];
364
+ /** Directives that occupy their own paragraph (block-level), opener→closer. */
365
+ const BLOCK_DIRECTIVE_KINDS = [
366
+ "if",
367
+ "elseif",
368
+ "else",
369
+ "endif",
370
+ "each",
371
+ "endeach"
372
+ ];
373
+ const BLOCK_KIND_SET = new Set(BLOCK_DIRECTIVE_KINDS);
374
+ const isBlockDirectiveKind = (kind) => BLOCK_KIND_SET.has(kind);
375
+ /** Any `{{...}}` marker; group 1 is the inner text (untrimmed — callers trim). */
376
+ const markerPattern = () => /\{\{(?<inner>[^{}]*)\}\}/gu;
377
+ /** Inline field / clause / numbering marker (inner allows the directive sigils). */
378
+ const placeholderPattern = () => /\{\{\s*(?<name>[\p{L}\p{N}_.@:-]+)\s*\}\}/gu;
379
+ /** `{{@clause:Name}}` / `{{@clause:Name:v3}}` — `name` and `modifier` groups. */
380
+ const clauseSlotPattern = () => /\{\{\s*@clause:(?<name>[^:}\s]+)(?::(?<modifier>[^}\s]+))?\s*\}\}/gu;
381
+ /** `{{@num:Key}}` — the `key` group. */
382
+ const numPattern = () => /\{\{\s*@num:(?<key>[\p{L}\p{N}_.-]+)\s*\}\}/gu;
383
+ /** `{{@ref:Key}}` — the `key` group. */
384
+ const refPattern = () => /\{\{\s*@ref:(?<key>[\p{L}\p{N}_.-]+)\s*\}\}/gu;
385
+ /** `{{@index}}` — the 1-based position within the innermost enclosing loop. */
386
+ const indexPattern = () => /\{\{\s*@index\s*\}\}/gu;
387
+ /** `{{@count}}` — the item count of the innermost enclosing loop. */
388
+ const countPattern = () => /\{\{\s*@count\s*\}\}/gu;
389
+ /** Cheap presence test for any numbering marker (no capture). */
390
+ const hasNumberingPattern = () => /\{\{\s*@(?:num|ref):/u;
391
+ /** Cheap presence test for any block directive (no capture). */
392
+ const hasBlockDirectivePattern = () => /\{\{\s*[#/]/u;
393
+ /** A whole line that is a single block directive — `tag` and `expr` groups. */
394
+ const blockDirectiveLinePattern = () => /^\s*\{\{\s*(?<tag>#if|#elseif|#else|#each|\/if|\/each)\b(?<expr>[^{}]*)\}\}\s*$/u;
395
+ const FIELD_PATH_RE = /^[\p{L}\p{N}_.-]+$/u;
396
+ /** Whether `value` is a valid field path per the marker grammar (dotted
397
+ * segments of letters/digits/underscore/dash — no brackets or spaces). */
398
+ const isFieldPath = (value) => FIELD_PATH_RE.test(value);
399
+ const CLAUSE_INNER_RE = /^@clause:(?<name>[^:}\s]+)(?::(?<version>[^}\s]+))?$/u;
400
+ const NUM_INNER_RE = /^@num:(?<key>[\p{L}\p{N}_.-]+)$/u;
401
+ const REF_INNER_RE = /^@ref:(?<key>[\p{L}\p{N}_.-]+)$/u;
402
+ const INDEX_INNER_RE = /^@index$/u;
403
+ const COUNT_INNER_RE = /^@count$/u;
404
+ const BLOCK_INNER_RE = /^(?<token>#if|#elseif|#else|#each|\/if|\/each)\b(?<expr>.*)$/u;
405
+ /**
406
+ * Classify the inner text of a `{{...}}` marker. Returns `null` when the text is
407
+ * not a recognized directive (so a recognizer can leave stray braces alone).
408
+ */
409
+ const classifyMarker = (innerRaw) => {
410
+ const inner = innerRaw.trim();
411
+ const block = BLOCK_INNER_RE.exec(inner);
412
+ if (block) {
413
+ const token = block.groups?.["token"] ?? "";
414
+ const expr = (block.groups?.["expr"] ?? "").trim();
415
+ if (token === "#if") return {
416
+ kind: "if",
417
+ expr
418
+ };
419
+ if (token === "#elseif") return {
420
+ kind: "elseif",
421
+ expr
422
+ };
423
+ if (token === "#each") return {
424
+ kind: "each",
425
+ expr
426
+ };
427
+ if (token === "#else") return { kind: "else" };
428
+ if (token === "/if") return { kind: "endif" };
429
+ if (token === "/each") return { kind: "endeach" };
430
+ }
431
+ const clause = CLAUSE_INNER_RE.exec(inner);
432
+ if (clause) return {
433
+ kind: "clause",
434
+ name: clause.groups?.["name"] ?? "",
435
+ version: clause.groups?.["version"]
436
+ };
437
+ const num = NUM_INNER_RE.exec(inner);
438
+ if (num) return {
439
+ kind: "num",
440
+ key: num.groups?.["key"] ?? ""
441
+ };
442
+ const ref = REF_INNER_RE.exec(inner);
443
+ if (ref) return {
444
+ kind: "ref",
445
+ key: ref.groups?.["key"] ?? ""
446
+ };
447
+ if (INDEX_INNER_RE.test(inner)) return { kind: "index" };
448
+ if (COUNT_INNER_RE.test(inner)) return { kind: "count" };
449
+ if (FIELD_PATH_RE.test(inner)) return {
450
+ kind: "placeholder",
451
+ expr: inner
452
+ };
453
+ return null;
454
+ };
455
+ /**
456
+ * Find every recognized `{{...}}` marker in `text`, in document order, with
457
+ * offsets and classified metadata. Unrecognized `{{...}}` spans are skipped.
458
+ */
459
+ const scanMarkers = (text) => {
460
+ const re = markerPattern();
461
+ const out = [];
462
+ let match = re.exec(text);
463
+ while (match !== null) {
464
+ const inner = (match[1] ?? "").trim();
465
+ const meta = classifyMarker(inner);
466
+ if (meta) out.push({
467
+ start: match.index,
468
+ end: match.index + match[0].length,
469
+ raw: match[0],
470
+ inner,
471
+ meta
472
+ });
473
+ match = re.exec(text);
474
+ }
475
+ return out;
476
+ };
477
+ /** Exhaustiveness guard — pass the discriminant in a `switch` default branch. */
478
+ const assertNever = (value) => panic(`Unhandled template directive: ${JSON.stringify(value)}`);
479
+ //#endregion
480
+ //#region src/field-values.ts
481
+ /**
482
+ * Single source of truth for the DETERMINISTIC field-value transforms a
483
+ * template fill applies — composite (parts joined by a `{{key}}` format),
484
+ * formula (arithmetic over the other values), and locale-aware date rendering.
485
+ *
486
+ * Both the API fill engine (apps/api/src/handlers/docx) and the web live
487
+ * preview (template-studio.tsx) MUST route their rendering through
488
+ * {@link renderDeterministicFieldValue} so the preview and the generated
489
+ * document produce byte-identical strings. The api/field-value-parity.test.ts
490
+ * asserts this; reintroducing a bespoke transform on either side fails CI.
491
+ *
492
+ * Out of scope (non-deterministic / server-only, handled by callers):
493
+ * - registry lookup (needs a network hit; the preview overlays it async)
494
+ * - AI drafting / adaptation
495
+ *
496
+ * Pure: no IO, no model/provider dependency. The config types here are minimal
497
+ * STRUCTURAL shapes so the package stays free of api/web imports; callers pass
498
+ * their own field objects, which structurally satisfy these.
499
+ */
500
+ const ISO_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/u;
501
+ const STYLE_OPTIONS = {
502
+ long: { dateStyle: "long" },
503
+ medium: { dateStyle: "medium" },
504
+ short: { dateStyle: "short" }
505
+ };
506
+ /**
507
+ * Parse a strict YYYY-MM-DD calendar date; null when malformed or not a real
508
+ * date. UTC-anchored so the rendered day never shifts with the timezone.
509
+ * `Date` rolls out-of-range components over (2028-02-30 → March 1), so the
510
+ * round-trip comparison catches non-existent dates.
511
+ */
512
+ const parseIsoDate = (value) => {
513
+ if (!ISO_DATE_PATTERN.test(value)) return null;
514
+ const date = /* @__PURE__ */ new Date(`${value}T00:00:00Z`);
515
+ if (Number.isNaN(date.getTime()) || !date.toISOString().startsWith(value)) return null;
516
+ return date;
517
+ };
518
+ /**
519
+ * Format an ISO date (YYYY-MM-DD) per the field's locale + style via
520
+ * `Intl.DateTimeFormat` — ICU produces the correct localized (and, in date
521
+ * contexts, correctly inflected) month names, e.g. cs long → "13. června
522
+ * 2028". The "iso" style returns the value unchanged; an invalid calendar date
523
+ * returns null.
524
+ */
525
+ const formatDate = (value, dateFormat) => {
526
+ const date = parseIsoDate(value);
527
+ if (date === null) return null;
528
+ if (dateFormat.style === "iso") return value;
529
+ return new Intl.DateTimeFormat(dateFormat.locale, {
530
+ ...STYLE_OPTIONS[dateFormat.style],
531
+ timeZone: "UTC"
532
+ }).format(date);
533
+ };
534
+ /**
535
+ * Render a `{{key}}` format over part values. Markers whose key has no part
536
+ * value are left as-is (a visible authoring artifact, not a render error), so
537
+ * a partially filled composite previews exactly as the fill engine renders it.
538
+ */
539
+ const renderComposite = (parts, format, partValues) => format.replace(markerPattern(), (raw, inner) => {
540
+ const key = inner.trim();
541
+ if (!parts.some((part) => part.key === key)) return raw;
542
+ return partValues[key] ?? raw;
543
+ });
544
+ /** A plain object of part values: a non-null, non-array object. The api
545
+ * composite step accepts exactly this shape (an array or primitive is not a
546
+ * part-values object), so the dispatcher mirrors it. */
547
+ const isPartValuesObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
548
+ /** Build the `{{key}} → value` map for a composite field from its raw object
549
+ * value, keeping only string part values for declared part keys. */
550
+ const compositePartValues = (parts, raw) => {
551
+ const partValues = {};
552
+ for (const part of parts) {
553
+ const value = raw[part.key];
554
+ if (typeof value === "string") partValues[part.key] = value;
555
+ }
556
+ return partValues;
557
+ };
558
+ /**
559
+ * THE deterministic field-value dispatcher: returns the string the fill engine
560
+ * writes for `field` given the submitted `values`, or null when the field has
561
+ * no deterministic transform (a scalar, lookup, or AI field the CALLER renders
562
+ * itself).
563
+ *
564
+ * Dispatch order mirrors the API fill pipeline (composite → formula → date):
565
+ * - composite (parts + format present) → {@link renderComposite}
566
+ * - else formula present → {@link evaluateNumericExpression}, stringified
567
+ * - else date (inputType "date" + dateFormat) → {@link formatDate}
568
+ * - else null
569
+ *
570
+ * Composite returns null when its value is not an object of part values (a
571
+ * plain string passes through unchanged on the api side, so the caller's
572
+ * scalar path handles it). Formula and date return null when the expression /
573
+ * value does not yield a value, so the caller leaves the field as-is.
574
+ */
575
+ const renderDeterministicFieldValue = (field, values) => {
576
+ if (field.parts !== void 0 && field.format !== void 0) {
577
+ const raw = resolvePath(field.path, values);
578
+ if (!isPartValuesObject(raw)) return null;
579
+ return renderComposite(field.parts, field.format, compositePartValues(field.parts, raw));
580
+ }
581
+ if (field.formula !== void 0) {
582
+ const result = evaluateNumericExpression(field.formula, values);
583
+ return result === void 0 ? null : String(result);
584
+ }
585
+ if (field.inputType === "date" && field.dateFormat !== void 0) {
586
+ const raw = resolvePath(field.path, values);
587
+ if (typeof raw !== "string" || raw.trim() === "") return null;
588
+ return formatDate(raw, field.dateFormat);
589
+ }
590
+ return null;
591
+ };
592
+ //#endregion
593
+ //#region src/condition-builder.ts
594
+ const COMPARE_OP_TO_SYMBOL = {
595
+ eq: "==",
596
+ neq: "!=",
597
+ gt: ">",
598
+ lt: "<",
599
+ gte: ">=",
600
+ lte: "<="
601
+ };
602
+ /** Render a literal: strings are quoted/escaped, booleans and numbers are
603
+ * emitted bare (matching the parser's literal handling); an array literal
604
+ * joins on `,` (rare; the template builder edits scalar rules). */
605
+ const serializeLiteral = (value) => {
606
+ if (typeof value === "string") return `"${value.replace(/\\/gu, "\\\\").replace(/"/gu, "\\\"")}"`;
607
+ if (Array.isArray(value)) return `"${value.join(",")}"`;
608
+ return String(value);
609
+ };
610
+ const serializeOperand = (operand) => {
611
+ if (operand.type === "literal") return serializeLiteral(operand.value);
612
+ if (operand.type === "path") return operand.path;
613
+ if (operand.type === "formula") panic("serializeCondition: formula operand requires AST storage");
614
+ return "";
615
+ };
616
+ const serializeCompare = (node) => `${serializeOperand(node.left)} ${COMPARE_OP_TO_SYMBOL[node.op]} ${serializeOperand(node.right)}`;
617
+ const serializePredicate = (node) => {
618
+ const operand = serializeOperand(node.operand);
619
+ if (node.op === "contains") return `${operand} contains ${serializeLiteral(Array.isArray(node.value) ? node.value.join(",") : node.value ?? "")}`;
620
+ return operand;
621
+ };
622
+ const serializeGroup = (group, top) => {
623
+ const parts = group.children.map((child) => serializeNode(child, false)).filter((part) => part.length > 0);
624
+ if (parts.length === 0) return "";
625
+ const body = parts.length === 1 ? parts[0] ?? "" : parts.join(group.combinator === "and" ? " and " : " or ");
626
+ if (group.negated) return `!(${body})`;
627
+ if (parts.length === 1) return body;
628
+ return top ? body : `(${body})`;
629
+ };
630
+ const serializeNode = (node, top) => {
631
+ if (node.type === "compare") return serializeCompare(node);
632
+ if (node.type === "predicate") return serializePredicate(node);
633
+ return serializeGroup(node, top);
634
+ };
635
+ /**
636
+ * Serialize a condition tree into an expression for `evaluateCondition`.
637
+ * Returns an empty string for an empty tree (caller treats that as "no
638
+ * condition" / always-visible).
639
+ */
640
+ const serializeCondition = (node) => serializeNode(node, true);
641
+ //#endregion
642
+ //#region src/index.ts
643
+ /**
644
+ * Shared condition evaluation + marker grammar for DOCX templates. Small,
645
+ * side-effect-free functions usable on both backend (Bun) and frontend
646
+ * (browser).
647
+ *
648
+ * Boolean conditions are evaluated through the canonical `@stll/conditions`
649
+ * AST and its single evaluator, so view filters, AI-extraction gating, and
650
+ * template `{{#if ...}}` conditionals share one set of operators and semantics
651
+ * and never drift apart. This module owns the template surface: the string
652
+ * parser (`./parse`), the named-condition resolver, and the no-code builder
653
+ * serializer (`./condition-builder`).
654
+ */
655
+ /**
656
+ * Maximum recursion depth for named condition resolution.
657
+ * Prevents stack overflow when a long chain of named
658
+ * conditions (A→B→C→...→Z) is evaluated without cycles.
659
+ */
660
+ const MAX_CONDITION_DEPTH = 50;
661
+ /**
662
+ * Normalize a fill-bag value into a `ConditionValue` the shared evaluator
663
+ * accepts: arrays become string arrays (multi-select fields hold their options
664
+ * as strings, so membership tests like `parties contains "guarantor"` work
665
+ * even when an element is numeric), primitives pass through, anything else is
666
+ * stringified.
667
+ */
668
+ const toConditionValue = (raw) => {
669
+ if (raw === null || raw === void 0) return raw;
670
+ if (Array.isArray(raw)) return raw.map(String);
671
+ if (typeof raw === "string" || typeof raw === "number" || typeof raw === "boolean") return raw;
672
+ };
673
+ /**
674
+ * Build the operand resolver for one evaluation. Only `path` operands need
675
+ * resolving (the evaluator handles literals). A path is first matched against
676
+ * the named conditions: a hit evaluates that condition's expression — so
677
+ * `{{#if isCompany}}` and `{{#if isCompany and signed}}` both work — guarded
678
+ * against cycles (`resolved`) and runaway chains (`MAX_CONDITION_DEPTH`).
679
+ * Otherwise the path resolves against the fill data.
680
+ */
681
+ const makeResolver = (data, namedConditions, resolved, depth) => {
682
+ const resolve = (operand) => {
683
+ if (operand.type === "formula") return evaluateNumericExpression(operand.expr, data);
684
+ if (operand.type !== "path") return;
685
+ const { path } = operand;
686
+ const named = namedConditions?.find((c) => c.name === path);
687
+ if (named) {
688
+ if (depth >= 50 || resolved.has(path)) return false;
689
+ const next = new Set(resolved);
690
+ next.add(path);
691
+ if (named.node) return evaluateCondition$1(named.node, makeResolver(data, namedConditions, next, depth + 1));
692
+ return evaluateCondition(named.expression, data, namedConditions, next, depth + 1);
693
+ }
694
+ return toConditionValue(resolvePath(path, data));
695
+ };
696
+ return resolve;
697
+ };
698
+ /**
699
+ * Evaluate a template `{{#if ...}}` condition. The expression is parsed into
700
+ * the canonical `@stll/conditions` AST and evaluated by the shared evaluator.
701
+ *
702
+ * Supports comparisons (`==`, `!=`, `>`, `<`, `>=`, `<=`), `contains`,
703
+ * truthiness, negation (`!`), logical operators (`and`, `or`), parentheses,
704
+ * dotted paths, numeric underscores, and named-condition references.
705
+ *
706
+ * An empty or unparseable expression is `false` ("no condition" never gates a
707
+ * branch true).
708
+ */
709
+ const evaluateCondition = (expression, data, namedConditions, _resolved = /* @__PURE__ */ new Set(), _depth = 0) => {
710
+ const node = parseCondition(expression);
711
+ if (node === null) return false;
712
+ return evaluateCondition$1(node, makeResolver(data, namedConditions, _resolved, _depth));
713
+ };
714
+ //#endregion
715
+ export { BLOCK_DIRECTIVE_KINDS, DIRECTIVE_KINDS, MAX_CONDITION_DEPTH, assertNever, blockDirectiveLinePattern, classifyMarker, clauseSlotPattern, conditionHasFormula, conditionNodeSchema, conditionSchema, countPattern, emptyCondition, evaluateCondition, evaluateNumericExpression, formatDate, hasBlockDirectivePattern, hasNumberingPattern, indexPattern, isBlockDirectiveKind, isFieldPath, markerPattern, numPattern, parseCondition, placeholderPattern, refPattern, renderComposite, renderDeterministicFieldValue, resolvePath, scanMarkers, serializeCondition };
package/package.json CHANGED
@@ -1,5 +1,64 @@
1
1
  {
2
2
  "name": "@stll/template-conditions",
3
- "version": "0.0.1-placeholder.0",
4
- "description": "Placeholder to bootstrap npm trusted publishing. The real package ships via the next @stll release."
5
- }
3
+ "version": "0.1.0",
4
+ "description": "A document-template marker and condition engine: {{...}} marker grammar, parse, compute, serialize, field values.",
5
+ "keywords": [
6
+ "condition",
7
+ "field-values",
8
+ "marker",
9
+ "parse",
10
+ "placeholder",
11
+ "serialize",
12
+ "template"
13
+ ],
14
+ "homepage": "https://github.com/stella/stella/tree/main/packages/template-conditions",
15
+ "bugs": {
16
+ "url": "https://github.com/stella/stella/issues"
17
+ },
18
+ "license": "Apache-2.0",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "https://github.com/stella/stella.git",
22
+ "directory": "packages/template-conditions"
23
+ },
24
+ "files": [
25
+ "dist",
26
+ "README.md"
27
+ ],
28
+ "type": "module",
29
+ "sideEffects": false,
30
+ "exports": {
31
+ ".": {
32
+ "types": "./dist/index.d.ts",
33
+ "import": "./dist/index.js"
34
+ }
35
+ },
36
+ "publishConfig": {
37
+ "access": "public"
38
+ },
39
+ "scripts": {
40
+ "clean": "git clean -xdf dist .cache .turbo node_modules",
41
+ "build": "tsdown",
42
+ "pack:dry-run": "bun pm pack --dry-run",
43
+ "test": "bun test",
44
+ "test:property": "bun test $(grep -rlF 'fc.assert' src --include='*.test.ts')",
45
+ "typecheck": "tsgo --noEmit",
46
+ "lint": "cd ../.. && bun --bun oxlint -c oxlint.config.ts --report-unused-disable-directives-severity=error --deny-warnings --type-aware packages/template-conditions",
47
+ "lint:fix": "cd ../.. && bun --bun oxlint -c oxlint.config.ts --type-aware --fix packages/template-conditions",
48
+ "format": "oxfmt .",
49
+ "prepack": "bun run build"
50
+ },
51
+ "dependencies": {
52
+ "@stll/conditions": "^0.1.0",
53
+ "better-result": "2.9.2"
54
+ },
55
+ "devDependencies": {
56
+ "@stll/property-testing": "0.0.0",
57
+ "@stll/typescript-config": "0.0.0",
58
+ "@types/bun": "1.3.14",
59
+ "fast-check": "^4.8.0",
60
+ "tsdown": "0.22.3"
61
+ },
62
+ "main": "./dist/index.js",
63
+ "types": "./dist/index.d.ts"
64
+ }