@fieldia/core 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/LICENSE +21 -0
- package/README.md +31 -0
- package/package.json +40 -0
- package/schema/page.schema.json +2142 -0
- package/src/index.d.mts +15 -0
- package/src/index.d.ts +15 -0
- package/src/index.js +46 -0
- package/src/index.js.map +1 -0
- package/src/index.mjs +14 -0
- package/src/index.mjs.map +1 -0
- package/src/lib/expression/evaluateModifier.d.mts +55 -0
- package/src/lib/expression/evaluateModifier.d.ts +55 -0
- package/src/lib/expression/evaluateModifier.js +121 -0
- package/src/lib/expression/evaluateModifier.js.map +1 -0
- package/src/lib/expression/evaluateModifier.mjs +117 -0
- package/src/lib/expression/evaluateModifier.mjs.map +1 -0
- package/src/lib/expression/evaluator.d.mts +23 -0
- package/src/lib/expression/evaluator.d.ts +23 -0
- package/src/lib/expression/evaluator.js +122 -0
- package/src/lib/expression/evaluator.js.map +1 -0
- package/src/lib/expression/evaluator.mjs +118 -0
- package/src/lib/expression/evaluator.mjs.map +1 -0
- package/src/lib/expression/extractFieldNames.d.mts +41 -0
- package/src/lib/expression/extractFieldNames.d.ts +41 -0
- package/src/lib/expression/extractFieldNames.js +97 -0
- package/src/lib/expression/extractFieldNames.js.map +1 -0
- package/src/lib/expression/extractFieldNames.mjs +93 -0
- package/src/lib/expression/extractFieldNames.mjs.map +1 -0
- package/src/lib/expression/modifier.d.mts +14 -0
- package/src/lib/expression/modifier.d.ts +14 -0
- package/src/lib/expression/modifier.js +55 -0
- package/src/lib/expression/modifier.js.map +1 -0
- package/src/lib/expression/modifier.mjs +52 -0
- package/src/lib/expression/modifier.mjs.map +1 -0
- package/src/lib/expression/parser.d.mts +109 -0
- package/src/lib/expression/parser.d.ts +109 -0
- package/src/lib/expression/parser.js +317 -0
- package/src/lib/expression/parser.js.map +1 -0
- package/src/lib/expression/parser.mjs +313 -0
- package/src/lib/expression/parser.mjs.map +1 -0
- package/src/lib/expression/tokenizer.d.mts +31 -0
- package/src/lib/expression/tokenizer.d.ts +31 -0
- package/src/lib/expression/tokenizer.js +100 -0
- package/src/lib/expression/tokenizer.js.map +1 -0
- package/src/lib/expression/tokenizer.mjs +96 -0
- package/src/lib/expression/tokenizer.mjs.map +1 -0
- package/src/lib/format/field.d.mts +967 -0
- package/src/lib/format/field.d.ts +967 -0
- package/src/lib/format/field.js +147 -0
- package/src/lib/format/field.js.map +1 -0
- package/src/lib/format/field.mjs +144 -0
- package/src/lib/format/field.mjs.map +1 -0
- package/src/lib/format/json-schema.d.mts +6 -0
- package/src/lib/format/json-schema.d.ts +6 -0
- package/src/lib/format/json-schema.js +14 -0
- package/src/lib/format/json-schema.js.map +1 -0
- package/src/lib/format/json-schema.mjs +11 -0
- package/src/lib/format/json-schema.mjs.map +1 -0
- package/src/lib/format/json.d.mts +7 -0
- package/src/lib/format/json.d.ts +7 -0
- package/src/lib/format/json.js +7 -0
- package/src/lib/format/json.js.map +1 -0
- package/src/lib/format/json.mjs +4 -0
- package/src/lib/format/json.mjs.map +1 -0
- package/src/lib/format/layout.d.mts +521 -0
- package/src/lib/format/layout.d.ts +521 -0
- package/src/lib/format/layout.js +150 -0
- package/src/lib/format/layout.js.map +1 -0
- package/src/lib/format/layout.mjs +147 -0
- package/src/lib/format/layout.mjs.map +1 -0
- package/src/lib/format/layout.types.d.mts +22 -0
- package/src/lib/format/layout.types.d.ts +22 -0
- package/src/lib/format/layout.types.js +3 -0
- package/src/lib/format/layout.types.js.map +1 -0
- package/src/lib/format/layout.types.mjs +2 -0
- package/src/lib/format/layout.types.mjs.map +1 -0
- package/src/lib/format/page.d.mts +511 -0
- package/src/lib/format/page.d.ts +511 -0
- package/src/lib/format/page.js +27 -0
- package/src/lib/format/page.js.map +1 -0
- package/src/lib/format/page.mjs +24 -0
- package/src/lib/format/page.mjs.map +1 -0
- package/src/lib/format/validate.d.mts +23 -0
- package/src/lib/format/validate.d.ts +23 -0
- package/src/lib/format/validate.js +210 -0
- package/src/lib/format/validate.js.map +1 -0
- package/src/lib/format/validate.mjs +207 -0
- package/src/lib/format/validate.mjs.map +1 -0
- package/src/lib/record/check.d.mts +14 -0
- package/src/lib/record/check.d.ts +14 -0
- package/src/lib/record/check.js +167 -0
- package/src/lib/record/check.js.map +1 -0
- package/src/lib/record/check.mjs +163 -0
- package/src/lib/record/check.mjs.map +1 -0
- package/src/lib/record/data-source.d.mts +109 -0
- package/src/lib/record/data-source.d.ts +109 -0
- package/src/lib/record/data-source.js +3 -0
- package/src/lib/record/data-source.js.map +1 -0
- package/src/lib/record/data-source.mjs +2 -0
- package/src/lib/record/data-source.mjs.map +1 -0
- package/src/lib/record/form.d.mts +106 -0
- package/src/lib/record/form.d.ts +106 -0
- package/src/lib/record/form.js +545 -0
- package/src/lib/record/form.js.map +1 -0
- package/src/lib/record/form.mjs +542 -0
- package/src/lib/record/form.mjs.map +1 -0
- package/src/lib/record/memory-data-source.d.mts +28 -0
- package/src/lib/record/memory-data-source.d.ts +28 -0
- package/src/lib/record/memory-data-source.js +132 -0
- package/src/lib/record/memory-data-source.js.map +1 -0
- package/src/lib/record/memory-data-source.mjs +129 -0
- package/src/lib/record/memory-data-source.mjs.map +1 -0
- package/src/lib/record/messages.d.mts +35 -0
- package/src/lib/record/messages.d.ts +35 -0
- package/src/lib/record/messages.js +111 -0
- package/src/lib/record/messages.js.map +1 -0
- package/src/lib/record/messages.mjs +107 -0
- package/src/lib/record/messages.mjs.map +1 -0
- package/src/lib/record/scheduler.d.mts +12 -0
- package/src/lib/record/scheduler.d.ts +12 -0
- package/src/lib/record/scheduler.js +16 -0
- package/src/lib/record/scheduler.js.map +1 -0
- package/src/lib/record/scheduler.mjs +12 -0
- package/src/lib/record/scheduler.mjs.map +1 -0
- package/src/lib/record/values.d.mts +54 -0
- package/src/lib/record/values.d.ts +54 -0
- package/src/lib/record/values.js +78 -0
- package/src/lib/record/values.js.map +1 -0
- package/src/lib/record/values.mjs +71 -0
- package/src/lib/record/values.mjs.map +1 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AST evaluator for modifiers, ported from the React form engine.
|
|
3
|
+
*
|
|
4
|
+
* Python semantics where the React version used JavaScript's: an empty list
|
|
5
|
+
* or string is false, and an empty value is never smaller or larger than
|
|
6
|
+
* anything — modifiers come from backends that think in Python.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Evaluate an AST node against a context object
|
|
10
|
+
*
|
|
11
|
+
* @param ast - The AST node to evaluate
|
|
12
|
+
* @param context - Object containing field values
|
|
13
|
+
* @returns The result of evaluation
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* const ast = { type: 'BinaryOp', operator: '==', left: {...}, right: {...} };
|
|
17
|
+
* const context = { state: 'draft', amount: 100 };
|
|
18
|
+
* evaluate(ast, context); // Returns boolean result
|
|
19
|
+
*/
|
|
20
|
+
export function evaluate(ast, context) {
|
|
21
|
+
switch (ast.type) {
|
|
22
|
+
case 'BinaryOp':
|
|
23
|
+
return evaluateBinaryOp(ast, context);
|
|
24
|
+
case 'UnaryOp':
|
|
25
|
+
return evaluateUnaryOp(ast, context);
|
|
26
|
+
case 'Literal':
|
|
27
|
+
return ast.value;
|
|
28
|
+
case 'Identifier':
|
|
29
|
+
return getNestedValue(context, ast.name);
|
|
30
|
+
default:
|
|
31
|
+
throw new Error(`Unknown AST node type: ${ast.type}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Evaluate binary operations
|
|
36
|
+
*/
|
|
37
|
+
function evaluateBinaryOp(node, context) {
|
|
38
|
+
const left = normalize(evaluate(node.left, context));
|
|
39
|
+
const right = normalize(evaluate(node.right, context));
|
|
40
|
+
const operator = node.operator.toLowerCase();
|
|
41
|
+
const ordered = left !== null && right !== null;
|
|
42
|
+
switch (operator) {
|
|
43
|
+
// Comparison operators
|
|
44
|
+
case '==':
|
|
45
|
+
return left === right;
|
|
46
|
+
case '!=':
|
|
47
|
+
case '<>':
|
|
48
|
+
return left !== right;
|
|
49
|
+
case '<':
|
|
50
|
+
return ordered && left < right;
|
|
51
|
+
case '>':
|
|
52
|
+
return ordered && left > right;
|
|
53
|
+
case '<=':
|
|
54
|
+
return ordered && left <= right;
|
|
55
|
+
case '>=':
|
|
56
|
+
return ordered && left >= right;
|
|
57
|
+
// In operators
|
|
58
|
+
case 'in':
|
|
59
|
+
return Array.isArray(right) && right.includes(left);
|
|
60
|
+
case 'not in':
|
|
61
|
+
return Array.isArray(right) && !right.includes(left);
|
|
62
|
+
// Logical operators (use truthiness)
|
|
63
|
+
case 'and':
|
|
64
|
+
return truthy(left) && truthy(right);
|
|
65
|
+
case 'or':
|
|
66
|
+
return truthy(left) || truthy(right);
|
|
67
|
+
default:
|
|
68
|
+
throw new Error(`Unknown binary operator: ${node.operator}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Evaluate unary operations
|
|
73
|
+
*/
|
|
74
|
+
function evaluateUnaryOp(node, context) {
|
|
75
|
+
const operand = evaluate(node.operand, context);
|
|
76
|
+
const operator = node.operator.toLowerCase();
|
|
77
|
+
switch (operator) {
|
|
78
|
+
case 'not':
|
|
79
|
+
return !truthy(operand);
|
|
80
|
+
default:
|
|
81
|
+
throw new Error(`Unknown unary operator: ${node.operator}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/** A missing value compares like None. */
|
|
85
|
+
function normalize(value) {
|
|
86
|
+
return value === undefined ? null : value;
|
|
87
|
+
}
|
|
88
|
+
/** Python's truth test: empty lists, strings and objects, 0 and None are false. */
|
|
89
|
+
export function truthy(value) {
|
|
90
|
+
if (Array.isArray(value))
|
|
91
|
+
return value.length > 0;
|
|
92
|
+
if (value !== null && typeof value === 'object')
|
|
93
|
+
return Object.keys(value).length > 0;
|
|
94
|
+
return Boolean(value);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Get a nested value from an object using dot notation
|
|
98
|
+
*
|
|
99
|
+
* @param obj - The object to traverse
|
|
100
|
+
* @param path - Dot-separated path (e.g., 'partner_id.country_id.name')
|
|
101
|
+
* @returns The value at the path, or undefined if not found
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* getNestedValue({ a: { b: { c: 5 } } }, 'a.b.c') // Returns 5
|
|
105
|
+
* getNestedValue({ a: null }, 'a.b.c') // Returns undefined
|
|
106
|
+
*/
|
|
107
|
+
function getNestedValue(obj, path) {
|
|
108
|
+
const parts = path.split('.');
|
|
109
|
+
let current = obj;
|
|
110
|
+
for (const part of parts) {
|
|
111
|
+
if (current === null || current === undefined) {
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
current = current[part];
|
|
115
|
+
}
|
|
116
|
+
return current;
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=evaluator.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"evaluator.mjs","sourceRoot":"","sources":["../../../../../../libs/core/src/lib/expression/evaluator.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract field names from modifier expressions
|
|
3
|
+
*
|
|
4
|
+
* Used to determine which form fields need to be watched for modifier evaluation.
|
|
5
|
+
* This enables targeted subscriptions instead of watching all form values.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* extractFieldNames("state == 'draft'") // ['state']
|
|
10
|
+
* extractFieldNames("state == 'draft' and amount > 100") // ['state', 'amount']
|
|
11
|
+
* extractFieldNames("partner_id.country_id.code == 'US'") // ['partner_id']
|
|
12
|
+
* extractFieldNames(true) // []
|
|
13
|
+
* extractFieldNames(undefined) // []
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Extract all field names referenced in a modifier expression
|
|
18
|
+
*
|
|
19
|
+
* @param expression - Modifier expression (string, boolean, or undefined)
|
|
20
|
+
* @returns Array of root field names to watch
|
|
21
|
+
*/
|
|
22
|
+
export declare function extractFieldNames(expression: string | boolean | undefined): string[];
|
|
23
|
+
/**
|
|
24
|
+
* Extract all field names from a modifiers object
|
|
25
|
+
*
|
|
26
|
+
* @param modifiers - Object containing invisible/readonly/required expressions
|
|
27
|
+
* @returns Array of unique root field names to watch
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```typescript
|
|
31
|
+
* extractFieldNamesFromModifiers({
|
|
32
|
+
* invisible: "state == 'draft'",
|
|
33
|
+
* readonly: "amount > 100"
|
|
34
|
+
* }) // ['state', 'amount']
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export declare function extractFieldNamesFromModifiers(modifiers: Record<string, string | boolean | undefined> | {
|
|
38
|
+
invisible?: string | boolean;
|
|
39
|
+
readonly?: string | boolean;
|
|
40
|
+
required?: string | boolean;
|
|
41
|
+
}): string[];
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract field names from modifier expressions
|
|
3
|
+
*
|
|
4
|
+
* Used to determine which form fields need to be watched for modifier evaluation.
|
|
5
|
+
* This enables targeted subscriptions instead of watching all form values.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* extractFieldNames("state == 'draft'") // ['state']
|
|
10
|
+
* extractFieldNames("state == 'draft' and amount > 100") // ['state', 'amount']
|
|
11
|
+
* extractFieldNames("partner_id.country_id.code == 'US'") // ['partner_id']
|
|
12
|
+
* extractFieldNames(true) // []
|
|
13
|
+
* extractFieldNames(undefined) // []
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Extract all field names referenced in a modifier expression
|
|
18
|
+
*
|
|
19
|
+
* @param expression - Modifier expression (string, boolean, or undefined)
|
|
20
|
+
* @returns Array of root field names to watch
|
|
21
|
+
*/
|
|
22
|
+
export declare function extractFieldNames(expression: string | boolean | undefined): string[];
|
|
23
|
+
/**
|
|
24
|
+
* Extract all field names from a modifiers object
|
|
25
|
+
*
|
|
26
|
+
* @param modifiers - Object containing invisible/readonly/required expressions
|
|
27
|
+
* @returns Array of unique root field names to watch
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```typescript
|
|
31
|
+
* extractFieldNamesFromModifiers({
|
|
32
|
+
* invisible: "state == 'draft'",
|
|
33
|
+
* readonly: "amount > 100"
|
|
34
|
+
* }) // ['state', 'amount']
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export declare function extractFieldNamesFromModifiers(modifiers: Record<string, string | boolean | undefined> | {
|
|
38
|
+
invisible?: string | boolean;
|
|
39
|
+
readonly?: string | boolean;
|
|
40
|
+
required?: string | boolean;
|
|
41
|
+
}): string[];
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Extract field names from modifier expressions
|
|
4
|
+
*
|
|
5
|
+
* Used to determine which form fields need to be watched for modifier evaluation.
|
|
6
|
+
* This enables targeted subscriptions instead of watching all form values.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* extractFieldNames("state == 'draft'") // ['state']
|
|
11
|
+
* extractFieldNames("state == 'draft' and amount > 100") // ['state', 'amount']
|
|
12
|
+
* extractFieldNames("partner_id.country_id.code == 'US'") // ['partner_id']
|
|
13
|
+
* extractFieldNames(true) // []
|
|
14
|
+
* extractFieldNames(undefined) // []
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.extractFieldNames = extractFieldNames;
|
|
19
|
+
exports.extractFieldNamesFromModifiers = extractFieldNamesFromModifiers;
|
|
20
|
+
const tokenizer_1 = require("./tokenizer");
|
|
21
|
+
/**
|
|
22
|
+
* Extract all field names referenced in a modifier expression
|
|
23
|
+
*
|
|
24
|
+
* @param expression - Modifier expression (string, boolean, or undefined)
|
|
25
|
+
* @returns Array of root field names to watch
|
|
26
|
+
*/
|
|
27
|
+
function extractFieldNames(expression) {
|
|
28
|
+
// Non-string expressions don't reference fields
|
|
29
|
+
if (typeof expression !== 'string' || expression.trim() === '') {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const tokens = (0, tokenizer_1.tokenize)(expression);
|
|
34
|
+
const fields = new Set();
|
|
35
|
+
// Keywords that look like identifiers but aren't field names
|
|
36
|
+
const keywords = new Set(['null', 'true', 'false', 'True', 'False', 'None']);
|
|
37
|
+
tokens.forEach(token => {
|
|
38
|
+
if (token.type === 'IDENTIFIER') {
|
|
39
|
+
// Skip literal keywords
|
|
40
|
+
if (keywords.has(token.value)) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
// For nested fields like 'partner_id.country_id.name',
|
|
44
|
+
// we only need to watch the root field 'partner_id'
|
|
45
|
+
const rootField = token.value.split('.')[0];
|
|
46
|
+
fields.add(rootField);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
return Array.from(fields);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// An expression that cannot be read reads no fields. validatePage reports
|
|
53
|
+
// it, so a validated page never reaches this branch.
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// CRITICAL: Cache to prevent creating new arrays when the result is the same
|
|
58
|
+
// WeakMap ensures memory is freed when modifiers object is garbage collected
|
|
59
|
+
const extractionCache = new WeakMap();
|
|
60
|
+
// Stable empty array for when there are no fields to extract
|
|
61
|
+
const EMPTY_FIELDS_ARRAY = Object.freeze([]);
|
|
62
|
+
/**
|
|
63
|
+
* Extract all field names from a modifiers object
|
|
64
|
+
*
|
|
65
|
+
* @param modifiers - Object containing invisible/readonly/required expressions
|
|
66
|
+
* @returns Array of unique root field names to watch
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```typescript
|
|
70
|
+
* extractFieldNamesFromModifiers({
|
|
71
|
+
* invisible: "state == 'draft'",
|
|
72
|
+
* readonly: "amount > 100"
|
|
73
|
+
* }) // ['state', 'amount']
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
function extractFieldNamesFromModifiers(modifiers) {
|
|
77
|
+
// Check cache first for stable reference
|
|
78
|
+
const cached = extractionCache.get(modifiers);
|
|
79
|
+
if (cached !== undefined) {
|
|
80
|
+
return cached;
|
|
81
|
+
}
|
|
82
|
+
const allFields = new Set();
|
|
83
|
+
Object.values(modifiers).forEach(expression => {
|
|
84
|
+
const fields = extractFieldNames(expression);
|
|
85
|
+
fields.forEach(field => allFields.add(field));
|
|
86
|
+
});
|
|
87
|
+
// Return stable empty array if no fields
|
|
88
|
+
if (allFields.size === 0) {
|
|
89
|
+
extractionCache.set(modifiers, EMPTY_FIELDS_ARRAY);
|
|
90
|
+
return EMPTY_FIELDS_ARRAY;
|
|
91
|
+
}
|
|
92
|
+
// Freeze the result array to ensure immutability
|
|
93
|
+
const result = Object.freeze(Array.from(allFields));
|
|
94
|
+
extractionCache.set(modifiers, result);
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=extractFieldNames.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extractFieldNames.js","sourceRoot":"","sources":["../../../../../../libs/core/src/lib/expression/extractFieldNames.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;GAcG;;AAUH,8CAiCC;AAuBD,wEA0BC;AA1FD,2CAAuC;AAEvC;;;;;GAKG;AACH,SAAgB,iBAAiB,CAAC,UAAwC;IACxE,gDAAgD;IAChD,IAAI,OAAO,UAAU,KAAK,QAAQ,IAAI,UAAU,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QAC/D,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAA,oBAAQ,EAAC,UAAU,CAAC,CAAC;QACpC,MAAM,MAAM,GAAG,IAAI,GAAG,EAAU,CAAC;QAEjC,6DAA6D;QAC7D,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC;QAE7E,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE;YACrB,IAAI,KAAK,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;gBAChC,wBAAwB;gBACxB,IAAI,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;oBAC9B,OAAO;gBACT,CAAC;gBAED,uDAAuD;gBACvD,oDAAoD;gBACpD,MAAM,SAAS,GAAG,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;gBAC5C,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;YACxB,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,OAAO,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC5B,CAAC;IAAC,MAAM,CAAC;QACP,0EAA0E;QAC1E,qDAAqD;QACrD,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,6EAA6E;AAC7E,6EAA6E;AAC7E,MAAM,eAAe,GAAG,IAAI,OAAO,EAAoB,CAAC;AAExD,6DAA6D;AAC7D,MAAM,kBAAkB,GAAa,MAAM,CAAC,MAAM,CAAC,EAAE,CAAwB,CAAC;AAE9E;;;;;;;;;;;;;GAaG;AACH,SAAgB,8BAA8B,CAC5C,SAAoJ;IAEpJ,yCAAyC;IACzC,MAAM,MAAM,GAAG,eAAe,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IAC9C,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,MAAM,SAAS,GAAG,IAAI,GAAG,EAAU,CAAC;IAEpC,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE;QAC5C,MAAM,MAAM,GAAG,iBAAiB,CAAC,UAAU,CAAC,CAAC;QAC7C,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC;IAChD,CAAC,CAAC,CAAC;IAEH,yCAAyC;IACzC,IAAI,SAAS,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;QACzB,eAAe,CAAC,GAAG,CAAC,SAAS,EAAE,kBAAkB,CAAC,CAAC;QACnD,OAAO,kBAAkB,CAAC;IAC5B,CAAC;IAED,iDAAiD;IACjD,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAa,CAAC;IAChE,eAAe,CAAC,GAAG,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;IACvC,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract field names from modifier expressions
|
|
3
|
+
*
|
|
4
|
+
* Used to determine which form fields need to be watched for modifier evaluation.
|
|
5
|
+
* This enables targeted subscriptions instead of watching all form values.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* extractFieldNames("state == 'draft'") // ['state']
|
|
10
|
+
* extractFieldNames("state == 'draft' and amount > 100") // ['state', 'amount']
|
|
11
|
+
* extractFieldNames("partner_id.country_id.code == 'US'") // ['partner_id']
|
|
12
|
+
* extractFieldNames(true) // []
|
|
13
|
+
* extractFieldNames(undefined) // []
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
import { tokenize } from './tokenizer.mjs';
|
|
17
|
+
/**
|
|
18
|
+
* Extract all field names referenced in a modifier expression
|
|
19
|
+
*
|
|
20
|
+
* @param expression - Modifier expression (string, boolean, or undefined)
|
|
21
|
+
* @returns Array of root field names to watch
|
|
22
|
+
*/
|
|
23
|
+
export function extractFieldNames(expression) {
|
|
24
|
+
// Non-string expressions don't reference fields
|
|
25
|
+
if (typeof expression !== 'string' || expression.trim() === '') {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
const tokens = tokenize(expression);
|
|
30
|
+
const fields = new Set();
|
|
31
|
+
// Keywords that look like identifiers but aren't field names
|
|
32
|
+
const keywords = new Set(['null', 'true', 'false', 'True', 'False', 'None']);
|
|
33
|
+
tokens.forEach(token => {
|
|
34
|
+
if (token.type === 'IDENTIFIER') {
|
|
35
|
+
// Skip literal keywords
|
|
36
|
+
if (keywords.has(token.value)) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
// For nested fields like 'partner_id.country_id.name',
|
|
40
|
+
// we only need to watch the root field 'partner_id'
|
|
41
|
+
const rootField = token.value.split('.')[0];
|
|
42
|
+
fields.add(rootField);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
return Array.from(fields);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// An expression that cannot be read reads no fields. validatePage reports
|
|
49
|
+
// it, so a validated page never reaches this branch.
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// CRITICAL: Cache to prevent creating new arrays when the result is the same
|
|
54
|
+
// WeakMap ensures memory is freed when modifiers object is garbage collected
|
|
55
|
+
const extractionCache = new WeakMap();
|
|
56
|
+
// Stable empty array for when there are no fields to extract
|
|
57
|
+
const EMPTY_FIELDS_ARRAY = Object.freeze([]);
|
|
58
|
+
/**
|
|
59
|
+
* Extract all field names from a modifiers object
|
|
60
|
+
*
|
|
61
|
+
* @param modifiers - Object containing invisible/readonly/required expressions
|
|
62
|
+
* @returns Array of unique root field names to watch
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```typescript
|
|
66
|
+
* extractFieldNamesFromModifiers({
|
|
67
|
+
* invisible: "state == 'draft'",
|
|
68
|
+
* readonly: "amount > 100"
|
|
69
|
+
* }) // ['state', 'amount']
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
export function extractFieldNamesFromModifiers(modifiers) {
|
|
73
|
+
// Check cache first for stable reference
|
|
74
|
+
const cached = extractionCache.get(modifiers);
|
|
75
|
+
if (cached !== undefined) {
|
|
76
|
+
return cached;
|
|
77
|
+
}
|
|
78
|
+
const allFields = new Set();
|
|
79
|
+
Object.values(modifiers).forEach(expression => {
|
|
80
|
+
const fields = extractFieldNames(expression);
|
|
81
|
+
fields.forEach(field => allFields.add(field));
|
|
82
|
+
});
|
|
83
|
+
// Return stable empty array if no fields
|
|
84
|
+
if (allFields.size === 0) {
|
|
85
|
+
extractionCache.set(modifiers, EMPTY_FIELDS_ARRAY);
|
|
86
|
+
return EMPTY_FIELDS_ARRAY;
|
|
87
|
+
}
|
|
88
|
+
// Freeze the result array to ensure immutability
|
|
89
|
+
const result = Object.freeze(Array.from(allFields));
|
|
90
|
+
extractionCache.set(modifiers, result);
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=extractFieldNames.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extractFieldNames.mjs","sourceRoot":"","sources":["../../../../../../libs/core/src/lib/expression/extractFieldNames.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEvC;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAAC,UAAwC;IACxE,gDAAgD;IAChD,IAAI,OAAO,UAAU,KAAK,QAAQ,IAAI,UAAU,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QAC/D,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,QAAQ,CAAC,UAAU,CAAC,CAAC;QACpC,MAAM,MAAM,GAAG,IAAI,GAAG,EAAU,CAAC;QAEjC,6DAA6D;QAC7D,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC;QAE7E,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE;YACrB,IAAI,KAAK,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;gBAChC,wBAAwB;gBACxB,IAAI,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;oBAC9B,OAAO;gBACT,CAAC;gBAED,uDAAuD;gBACvD,oDAAoD;gBACpD,MAAM,SAAS,GAAG,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;gBAC5C,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;YACxB,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,OAAO,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC5B,CAAC;IAAC,MAAM,CAAC;QACP,0EAA0E;QAC1E,qDAAqD;QACrD,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,6EAA6E;AAC7E,6EAA6E;AAC7E,MAAM,eAAe,GAAG,IAAI,OAAO,EAAoB,CAAC;AAExD,6DAA6D;AAC7D,MAAM,kBAAkB,GAAa,MAAM,CAAC,MAAM,CAAC,EAAE,CAAwB,CAAC;AAE9E;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,8BAA8B,CAC5C,SAAoJ;IAEpJ,yCAAyC;IACzC,MAAM,MAAM,GAAG,eAAe,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IAC9C,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,MAAM,SAAS,GAAG,IAAI,GAAG,EAAU,CAAC;IAEpC,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE;QAC5C,MAAM,MAAM,GAAG,iBAAiB,CAAC,UAAU,CAAC,CAAC;QAC7C,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC;IAChD,CAAC,CAAC,CAAC;IAEH,yCAAyC;IACzC,IAAI,SAAS,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;QACzB,eAAe,CAAC,GAAG,CAAC,SAAS,EAAE,kBAAkB,CAAC,CAAC;QACnD,OAAO,kBAAkB,CAAC;IAC5B,CAAC;IAED,iDAAiD;IACjD,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAa,CAAC;IAChE,eAAe,CAAC,GAAG,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;IACvC,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Modifier } from '../format/layout.mjs';
|
|
2
|
+
/** A modifier read once, ready to evaluate against a record's values. */
|
|
3
|
+
export interface CompiledModifier {
|
|
4
|
+
readonly source: Modifier | undefined;
|
|
5
|
+
/** The fields it reads, by their first name, in order of appearance. */
|
|
6
|
+
readonly fields: readonly string[];
|
|
7
|
+
evaluate(values: Readonly<Record<string, unknown>>): boolean;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Read a modifier. `true`, `false` and a missing modifier become constants;
|
|
11
|
+
* an expression is parsed now, so a mistake surfaces here and not on the
|
|
12
|
+
* first keystroke. Throws when the expression cannot be read.
|
|
13
|
+
*/
|
|
14
|
+
export declare function compileModifier(modifier: Modifier | undefined): CompiledModifier;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Modifier } from '../format/layout';
|
|
2
|
+
/** A modifier read once, ready to evaluate against a record's values. */
|
|
3
|
+
export interface CompiledModifier {
|
|
4
|
+
readonly source: Modifier | undefined;
|
|
5
|
+
/** The fields it reads, by their first name, in order of appearance. */
|
|
6
|
+
readonly fields: readonly string[];
|
|
7
|
+
evaluate(values: Readonly<Record<string, unknown>>): boolean;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Read a modifier. `true`, `false` and a missing modifier become constants;
|
|
11
|
+
* an expression is parsed now, so a mistake surfaces here and not on the
|
|
12
|
+
* first keystroke. Throws when the expression cannot be read.
|
|
13
|
+
*/
|
|
14
|
+
export declare function compileModifier(modifier: Modifier | undefined): CompiledModifier;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.compileModifier = compileModifier;
|
|
4
|
+
const evaluator_1 = require("./evaluator");
|
|
5
|
+
const parser_1 = require("./parser");
|
|
6
|
+
const tokenizer_1 = require("./tokenizer");
|
|
7
|
+
/**
|
|
8
|
+
* Read a modifier. `true`, `false` and a missing modifier become constants;
|
|
9
|
+
* an expression is parsed now, so a mistake surfaces here and not on the
|
|
10
|
+
* first keystroke. Throws when the expression cannot be read.
|
|
11
|
+
*/
|
|
12
|
+
function compileModifier(modifier) {
|
|
13
|
+
if (typeof modifier !== 'string') {
|
|
14
|
+
const value = modifier === true;
|
|
15
|
+
return { source: modifier, fields: [], evaluate: () => value };
|
|
16
|
+
}
|
|
17
|
+
const ast = new parser_1.ExpressionParser((0, tokenizer_1.tokenize)(modifier)).parse();
|
|
18
|
+
return {
|
|
19
|
+
source: modifier,
|
|
20
|
+
fields: fieldsRead(ast),
|
|
21
|
+
evaluate(values) {
|
|
22
|
+
try {
|
|
23
|
+
return (0, evaluator_1.truthy)((0, evaluator_1.evaluate)(ast, values));
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function fieldsRead(ast) {
|
|
32
|
+
const names = [];
|
|
33
|
+
const visit = (node) => {
|
|
34
|
+
switch (node.type) {
|
|
35
|
+
case 'Identifier': {
|
|
36
|
+
const root = node.name.split('.')[0];
|
|
37
|
+
if (!names.includes(root))
|
|
38
|
+
names.push(root);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
case 'BinaryOp':
|
|
42
|
+
visit(node.left);
|
|
43
|
+
visit(node.right);
|
|
44
|
+
return;
|
|
45
|
+
case 'UnaryOp':
|
|
46
|
+
visit(node.operand);
|
|
47
|
+
return;
|
|
48
|
+
default:
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
visit(ast);
|
|
53
|
+
return names;
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=modifier.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"modifier.js","sourceRoot":"","sources":["../../../../../../libs/core/src/lib/expression/modifier.ts"],"names":[],"mappings":";;AAkBA,0CAiBC;AAlCD,2CAA+C;AAC/C,qCAA0D;AAC1D,2CAAuC;AAUvC;;;;GAIG;AACH,SAAgB,eAAe,CAAC,QAA8B;IAC5D,IAAI,OAAO,QAAQ,KAAK,QAAQ,EAAE,CAAC;QACjC,MAAM,KAAK,GAAG,QAAQ,KAAK,IAAI,CAAC;QAChC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC,KAAK,EAAE,CAAC;IACjE,CAAC;IACD,MAAM,GAAG,GAAG,IAAI,yBAAgB,CAAC,IAAA,oBAAQ,EAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC;IAC7D,OAAO;QACL,MAAM,EAAE,QAAQ;QAChB,MAAM,EAAE,UAAU,CAAC,GAAG,CAAC;QACvB,QAAQ,CAAC,MAAM;YACb,IAAI,CAAC;gBACH,OAAO,IAAA,kBAAM,EAAC,IAAA,oBAAQ,EAAC,GAAG,EAAE,MAAM,CAAC,CAAC,CAAC;YACvC,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,KAAK,CAAC;YACf,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC;AAED,SAAS,UAAU,CAAC,GAAY;IAC9B,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,MAAM,KAAK,GAAG,CAAC,IAAa,EAAQ,EAAE;QACpC,QAAQ,IAAI,CAAC,IAAI,EAAE,CAAC;YAClB,KAAK,YAAY,CAAC,CAAC,CAAC;gBAClB,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;gBACrC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC;oBAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBAC5C,OAAO;YACT,CAAC;YACD,KAAK,UAAU;gBACb,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBACjB,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;gBAClB,OAAO;YACT,KAAK,SAAS;gBACZ,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;gBACpB,OAAO;YACT;gBACE,OAAO;QACX,CAAC;IACH,CAAC,CAAC;IACF,KAAK,CAAC,GAAG,CAAC,CAAC;IACX,OAAO,KAAK,CAAC;AACf,CAAC"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { evaluate, truthy } from './evaluator.mjs';
|
|
2
|
+
import { ExpressionParser } from './parser.mjs';
|
|
3
|
+
import { tokenize } from './tokenizer.mjs';
|
|
4
|
+
/**
|
|
5
|
+
* Read a modifier. `true`, `false` and a missing modifier become constants;
|
|
6
|
+
* an expression is parsed now, so a mistake surfaces here and not on the
|
|
7
|
+
* first keystroke. Throws when the expression cannot be read.
|
|
8
|
+
*/
|
|
9
|
+
export function compileModifier(modifier) {
|
|
10
|
+
if (typeof modifier !== 'string') {
|
|
11
|
+
const value = modifier === true;
|
|
12
|
+
return { source: modifier, fields: [], evaluate: () => value };
|
|
13
|
+
}
|
|
14
|
+
const ast = new ExpressionParser(tokenize(modifier)).parse();
|
|
15
|
+
return {
|
|
16
|
+
source: modifier,
|
|
17
|
+
fields: fieldsRead(ast),
|
|
18
|
+
evaluate(values) {
|
|
19
|
+
try {
|
|
20
|
+
return truthy(evaluate(ast, values));
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function fieldsRead(ast) {
|
|
29
|
+
const names = [];
|
|
30
|
+
const visit = (node) => {
|
|
31
|
+
switch (node.type) {
|
|
32
|
+
case 'Identifier': {
|
|
33
|
+
const root = node.name.split('.')[0];
|
|
34
|
+
if (!names.includes(root))
|
|
35
|
+
names.push(root);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
case 'BinaryOp':
|
|
39
|
+
visit(node.left);
|
|
40
|
+
visit(node.right);
|
|
41
|
+
return;
|
|
42
|
+
case 'UnaryOp':
|
|
43
|
+
visit(node.operand);
|
|
44
|
+
return;
|
|
45
|
+
default:
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
visit(ast);
|
|
50
|
+
return names;
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=modifier.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"modifier.mjs","sourceRoot":"","sources":["../../../../../../libs/core/src/lib/expression/modifier.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EAAE,gBAAgB,EAAgB,MAAM,UAAU,CAAC;AAC1D,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAUvC;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,QAA8B;IAC5D,IAAI,OAAO,QAAQ,KAAK,QAAQ,EAAE,CAAC;QACjC,MAAM,KAAK,GAAG,QAAQ,KAAK,IAAI,CAAC;QAChC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC,KAAK,EAAE,CAAC;IACjE,CAAC;IACD,MAAM,GAAG,GAAG,IAAI,gBAAgB,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC;IAC7D,OAAO;QACL,MAAM,EAAE,QAAQ;QAChB,MAAM,EAAE,UAAU,CAAC,GAAG,CAAC;QACvB,QAAQ,CAAC,MAAM;YACb,IAAI,CAAC;gBACH,OAAO,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC,CAAC;YACvC,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,KAAK,CAAC;YACf,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC;AAED,SAAS,UAAU,CAAC,GAAY;IAC9B,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,MAAM,KAAK,GAAG,CAAC,IAAa,EAAQ,EAAE;QACpC,QAAQ,IAAI,CAAC,IAAI,EAAE,CAAC;YAClB,KAAK,YAAY,CAAC,CAAC,CAAC;gBAClB,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;gBACrC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC;oBAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBAC5C,OAAO;YACT,CAAC;YACD,KAAK,UAAU;gBACb,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBACjB,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;gBAClB,OAAO;YACT,KAAK,SAAS;gBACZ,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;gBACpB,OAAO;YACT;gBACE,OAAO;QACX,CAAC;IACH,CAAC,CAAC;IACF,KAAK,CAAC,GAAG,CAAC,CAAC;IACX,OAAO,KAAK,CAAC;AACf,CAAC"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Expression parser for modifiers, ported from the React form engine.
|
|
3
|
+
*
|
|
4
|
+
* Recursive descent parser that converts tokens into an Abstract Syntax Tree (AST).
|
|
5
|
+
* Implements Python-like operator precedence:
|
|
6
|
+
* 1. OR (lowest precedence)
|
|
7
|
+
* 2. AND
|
|
8
|
+
* 3. NOT
|
|
9
|
+
* 4. Comparison operators (==, !=, <, >, <=, >=, in, not in)
|
|
10
|
+
* 5. Values/Identifiers (highest precedence)
|
|
11
|
+
*/
|
|
12
|
+
import { type Token } from './tokenizer.mjs';
|
|
13
|
+
/**
|
|
14
|
+
* AST Node Types
|
|
15
|
+
*/
|
|
16
|
+
export type ASTNode = BinaryOpNode | UnaryOpNode | LiteralNode | IdentifierNode;
|
|
17
|
+
export interface BinaryOpNode {
|
|
18
|
+
type: 'BinaryOp';
|
|
19
|
+
operator: string;
|
|
20
|
+
left: ASTNode;
|
|
21
|
+
right: ASTNode;
|
|
22
|
+
}
|
|
23
|
+
export interface UnaryOpNode {
|
|
24
|
+
type: 'UnaryOp';
|
|
25
|
+
operator: string;
|
|
26
|
+
operand: ASTNode;
|
|
27
|
+
}
|
|
28
|
+
export interface LiteralNode {
|
|
29
|
+
type: 'Literal';
|
|
30
|
+
value: unknown;
|
|
31
|
+
}
|
|
32
|
+
export interface IdentifierNode {
|
|
33
|
+
type: 'Identifier';
|
|
34
|
+
name: string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Recursive Descent Expression Parser
|
|
38
|
+
*/
|
|
39
|
+
export declare class ExpressionParser {
|
|
40
|
+
private tokens;
|
|
41
|
+
private position;
|
|
42
|
+
constructor(tokens: Token[]);
|
|
43
|
+
/**
|
|
44
|
+
* Parse tokens into an AST
|
|
45
|
+
*/
|
|
46
|
+
parse(): ASTNode;
|
|
47
|
+
/**
|
|
48
|
+
* Parse OR expressions (lowest precedence)
|
|
49
|
+
* or_expr: and_expr ('or' and_expr)*
|
|
50
|
+
*/
|
|
51
|
+
private parseOr;
|
|
52
|
+
/**
|
|
53
|
+
* Parse AND expressions
|
|
54
|
+
* and_expr: not_expr ('and' not_expr)*
|
|
55
|
+
*/
|
|
56
|
+
private parseAnd;
|
|
57
|
+
/**
|
|
58
|
+
* Parse NOT expressions
|
|
59
|
+
* not_expr: 'not' comparison | comparison
|
|
60
|
+
*/
|
|
61
|
+
private parseNot;
|
|
62
|
+
/**
|
|
63
|
+
* Parse comparison expressions
|
|
64
|
+
* comparison: value (comp_op value)?
|
|
65
|
+
* comp_op: '==' | '!=' | '<>' | '<' | '>' | '<=' | '>=' | 'in' | 'not in'
|
|
66
|
+
*/
|
|
67
|
+
private parseComparison;
|
|
68
|
+
/**
|
|
69
|
+
* Parse value expressions (highest precedence)
|
|
70
|
+
* value: '(' or_expr ')' | literal | identifier
|
|
71
|
+
*/
|
|
72
|
+
private parseValue;
|
|
73
|
+
/**
|
|
74
|
+
* Check if current token matches any of the given types/values
|
|
75
|
+
*/
|
|
76
|
+
private match;
|
|
77
|
+
/**
|
|
78
|
+
* Match logical operators (case-insensitive)
|
|
79
|
+
*/
|
|
80
|
+
private matchLogical;
|
|
81
|
+
/**
|
|
82
|
+
* Match comparison operators (case-insensitive)
|
|
83
|
+
*/
|
|
84
|
+
private matchOperator;
|
|
85
|
+
/**
|
|
86
|
+
* Check if current token has given type
|
|
87
|
+
*/
|
|
88
|
+
private check;
|
|
89
|
+
/**
|
|
90
|
+
* Consume current token and advance
|
|
91
|
+
*/
|
|
92
|
+
private advance;
|
|
93
|
+
/**
|
|
94
|
+
* Get current token without advancing
|
|
95
|
+
*/
|
|
96
|
+
private peek;
|
|
97
|
+
/**
|
|
98
|
+
* Get previous token
|
|
99
|
+
*/
|
|
100
|
+
private previous;
|
|
101
|
+
/**
|
|
102
|
+
* Check if we've consumed all tokens
|
|
103
|
+
*/
|
|
104
|
+
private isAtEnd;
|
|
105
|
+
/**
|
|
106
|
+
* Consume a token of specific type/value or throw error
|
|
107
|
+
*/
|
|
108
|
+
private consume;
|
|
109
|
+
}
|