@aglyn/plugins-logic 1.0.0-beta.244 → 1.0.0-beta.247
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/package.json +8 -8
- package/src/lib/components/formula-builder-dialog.component.d.ts +33 -0
- package/src/lib/components/formula-builder-dialog.component.js +331 -0
- package/src/lib/components/formula-builder-dialog.component.js.map +1 -0
- package/src/lib/components/formula-builder.component.d.ts +30 -0
- package/src/lib/components/formula-builder.component.js +62 -0
- package/src/lib/components/formula-builder.component.js.map +1 -0
- package/src/lib/components/host-functions-card.component.js +71 -10
- package/src/lib/components/host-functions-card.component.js.map +1 -1
- package/src/lib/components/host-variables-card.component.js +14 -9
- package/src/lib/components/host-variables-card.component.js.map +1 -1
- package/src/lib/components/structured-value-editor.component.d.ts +30 -0
- package/src/lib/components/structured-value-editor.component.js +189 -0
- package/src/lib/components/structured-value-editor.component.js.map +1 -0
- package/src/lib/model/formula-names.d.ts +41 -0
- package/src/lib/model/formula-names.js +82 -0
- package/src/lib/model/formula-names.js.map +1 -0
- package/src/lib/model/formula.d.ts +72 -0
- package/src/lib/model/formula.js +264 -0
- package/src/lib/model/formula.js.map +1 -0
- package/src/lib/model/structured-value.d.ts +51 -0
- package/src/lib/model/structured-value.js +135 -0
- package/src/lib/model/structured-value.js.map +1 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* A function step's expression, built by picking an operation and its values
|
|
19
|
+
* instead of typing it (AGL-3757). What comes out is the SAME expression an
|
|
20
|
+
* author could have typed — the evaluator, the stored definition and every
|
|
21
|
+
* page that runs it are untouched; this only writes the text.
|
|
22
|
+
*/
|
|
23
|
+
export type FormulaOperation = 'value' | 'add' | 'subtract' | 'multiply' | 'divide' | 'min' | 'max' | 'round' | 'roundDown' | 'roundUp' | 'absolute' | 'currency' | 'number' | 'join';
|
|
24
|
+
/** One value an operation works on. */
|
|
25
|
+
export type FormulaOperand =
|
|
26
|
+
/** A parameter, a local variable, a site variable or a dictionary member. */
|
|
27
|
+
{
|
|
28
|
+
source: 'name';
|
|
29
|
+
name: string;
|
|
30
|
+
} | {
|
|
31
|
+
source: 'number';
|
|
32
|
+
value: string;
|
|
33
|
+
} | {
|
|
34
|
+
source: 'text';
|
|
35
|
+
value: string;
|
|
36
|
+
}
|
|
37
|
+
/** Whatever the box holds already, so formulas can be layered. */
|
|
38
|
+
| {
|
|
39
|
+
source: 'current';
|
|
40
|
+
};
|
|
41
|
+
export interface FormulaDraft {
|
|
42
|
+
operation: FormulaOperation;
|
|
43
|
+
operands: FormulaOperand[];
|
|
44
|
+
/** Decimal places for round, currency and number. */
|
|
45
|
+
places?: number;
|
|
46
|
+
/** The sign written before a currency amount. */
|
|
47
|
+
symbol?: string;
|
|
48
|
+
/** What goes between joined texts. */
|
|
49
|
+
separator?: string;
|
|
50
|
+
}
|
|
51
|
+
interface FormulaOperationSpec {
|
|
52
|
+
label: string;
|
|
53
|
+
/** How many values it takes at least; `variadic` ones take more. */
|
|
54
|
+
minOperands: number;
|
|
55
|
+
variadic?: boolean;
|
|
56
|
+
/** What each value box is called, in order; the last repeats. */
|
|
57
|
+
operandLabels: string[];
|
|
58
|
+
places?: boolean;
|
|
59
|
+
}
|
|
60
|
+
export declare const FORMULA_OPERATIONS: Record<FormulaOperation, FormulaOperationSpec>;
|
|
61
|
+
/** The operations, in the order the picker lists them. */
|
|
62
|
+
export declare const FORMULA_OPERATION_ORDER: FormulaOperation[];
|
|
63
|
+
/** The label for a value box at `index` of `operation`. */
|
|
64
|
+
export declare function formulaOperandLabel(operation: FormulaOperation, index: number): string;
|
|
65
|
+
/**
|
|
66
|
+
* The expression a draft writes, or `null` while a value it needs is blank.
|
|
67
|
+
* `current` is what the box held when the builder opened.
|
|
68
|
+
*/
|
|
69
|
+
export declare function buildFormula(draft: FormulaDraft, current?: string): string | null;
|
|
70
|
+
/** A fresh draft for `operation`, carrying over the values already chosen. */
|
|
71
|
+
export declare function formulaDraftFor(operation: FormulaOperation, previous?: FormulaDraft, current?: string): FormulaDraft;
|
|
72
|
+
export {};
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ /**
|
|
17
|
+
* A function step's expression, built by picking an operation and its values
|
|
18
|
+
* instead of typing it (AGL-3757). What comes out is the SAME expression an
|
|
19
|
+
* author could have typed — the evaluator, the stored definition and every
|
|
20
|
+
* page that runs it are untouched; this only writes the text.
|
|
21
|
+
*/ export const FORMULA_OPERATIONS = {
|
|
22
|
+
value: {
|
|
23
|
+
label: 'Use a value',
|
|
24
|
+
minOperands: 1,
|
|
25
|
+
operandLabels: [
|
|
26
|
+
'Value'
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
add: {
|
|
30
|
+
label: 'Add',
|
|
31
|
+
minOperands: 2,
|
|
32
|
+
variadic: true,
|
|
33
|
+
operandLabels: [
|
|
34
|
+
'Value',
|
|
35
|
+
'Plus'
|
|
36
|
+
]
|
|
37
|
+
},
|
|
38
|
+
subtract: {
|
|
39
|
+
label: 'Subtract',
|
|
40
|
+
minOperands: 2,
|
|
41
|
+
operandLabels: [
|
|
42
|
+
'Value',
|
|
43
|
+
'Minus'
|
|
44
|
+
]
|
|
45
|
+
},
|
|
46
|
+
multiply: {
|
|
47
|
+
label: 'Multiply',
|
|
48
|
+
minOperands: 2,
|
|
49
|
+
variadic: true,
|
|
50
|
+
operandLabels: [
|
|
51
|
+
'Value',
|
|
52
|
+
'Times'
|
|
53
|
+
]
|
|
54
|
+
},
|
|
55
|
+
divide: {
|
|
56
|
+
label: 'Divide',
|
|
57
|
+
minOperands: 2,
|
|
58
|
+
operandLabels: [
|
|
59
|
+
'Value',
|
|
60
|
+
'Divided by'
|
|
61
|
+
]
|
|
62
|
+
},
|
|
63
|
+
min: {
|
|
64
|
+
label: 'Smallest of',
|
|
65
|
+
minOperands: 2,
|
|
66
|
+
variadic: true,
|
|
67
|
+
operandLabels: [
|
|
68
|
+
'Value',
|
|
69
|
+
'Or'
|
|
70
|
+
]
|
|
71
|
+
},
|
|
72
|
+
max: {
|
|
73
|
+
label: 'Largest of',
|
|
74
|
+
minOperands: 2,
|
|
75
|
+
variadic: true,
|
|
76
|
+
operandLabels: [
|
|
77
|
+
'Value',
|
|
78
|
+
'Or'
|
|
79
|
+
]
|
|
80
|
+
},
|
|
81
|
+
round: {
|
|
82
|
+
label: 'Round',
|
|
83
|
+
minOperands: 1,
|
|
84
|
+
operandLabels: [
|
|
85
|
+
'Value'
|
|
86
|
+
],
|
|
87
|
+
places: true
|
|
88
|
+
},
|
|
89
|
+
roundDown: {
|
|
90
|
+
label: 'Round down',
|
|
91
|
+
minOperands: 1,
|
|
92
|
+
operandLabels: [
|
|
93
|
+
'Value'
|
|
94
|
+
]
|
|
95
|
+
},
|
|
96
|
+
roundUp: {
|
|
97
|
+
label: 'Round up',
|
|
98
|
+
minOperands: 1,
|
|
99
|
+
operandLabels: [
|
|
100
|
+
'Value'
|
|
101
|
+
]
|
|
102
|
+
},
|
|
103
|
+
absolute: {
|
|
104
|
+
label: 'Without its minus sign',
|
|
105
|
+
minOperands: 1,
|
|
106
|
+
operandLabels: [
|
|
107
|
+
'Value'
|
|
108
|
+
]
|
|
109
|
+
},
|
|
110
|
+
currency: {
|
|
111
|
+
label: 'Format as money',
|
|
112
|
+
minOperands: 1,
|
|
113
|
+
operandLabels: [
|
|
114
|
+
'Amount'
|
|
115
|
+
],
|
|
116
|
+
places: true
|
|
117
|
+
},
|
|
118
|
+
number: {
|
|
119
|
+
label: 'Format with thousands separators',
|
|
120
|
+
minOperands: 1,
|
|
121
|
+
operandLabels: [
|
|
122
|
+
'Number'
|
|
123
|
+
],
|
|
124
|
+
places: true
|
|
125
|
+
},
|
|
126
|
+
join: {
|
|
127
|
+
label: 'Join text',
|
|
128
|
+
minOperands: 2,
|
|
129
|
+
variadic: true,
|
|
130
|
+
operandLabels: [
|
|
131
|
+
'Text',
|
|
132
|
+
'Then'
|
|
133
|
+
]
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
/** The operations, in the order the picker lists them. */ export const FORMULA_OPERATION_ORDER = Object.keys(FORMULA_OPERATIONS);
|
|
137
|
+
/** The label for a value box at `index` of `operation`. */ export function formulaOperandLabel(operation, index) {
|
|
138
|
+
const labels = FORMULA_OPERATIONS[operation].operandLabels;
|
|
139
|
+
return labels[Math.min(index, labels.length - 1)];
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* A text as the evaluator reads it. Its strings have no escapes, so the
|
|
143
|
+
* quote the text does not contain is used; a text holding both loses its
|
|
144
|
+
* double quotes rather than writing an expression that cannot be read.
|
|
145
|
+
*/ function quoteText(value) {
|
|
146
|
+
if (!value.includes("'")) return `'${value}'`;
|
|
147
|
+
if (!value.includes('"')) return `"${value}"`;
|
|
148
|
+
return `'${value.replace(/"/g, '')}'`;
|
|
149
|
+
}
|
|
150
|
+
const SIMPLE_EXPRESSION = /^\s*(?:-?[0-9]*\.?[0-9]+|[a-zA-Z_][a-zA-Z0-9_.]*)\s*$/;
|
|
151
|
+
/**
|
|
152
|
+
* One operand as expression text, or `null` while it is still blank. `bare`
|
|
153
|
+
* is a built-in's argument, which its own parentheses already delimit.
|
|
154
|
+
*/ function operandText(operand, current, bare = false) {
|
|
155
|
+
switch(operand.source){
|
|
156
|
+
case 'name':
|
|
157
|
+
return operand.name ? operand.name : null;
|
|
158
|
+
case 'number':
|
|
159
|
+
{
|
|
160
|
+
const text = operand.value.trim();
|
|
161
|
+
if (!text) return null;
|
|
162
|
+
const parsed = Number(text);
|
|
163
|
+
return Number.isFinite(parsed) ? String(parsed) : null;
|
|
164
|
+
}
|
|
165
|
+
case 'text':
|
|
166
|
+
return quoteText(operand.value);
|
|
167
|
+
case 'current':
|
|
168
|
+
{
|
|
169
|
+
const text = current.trim();
|
|
170
|
+
if (!text) return null;
|
|
171
|
+
return bare || SIMPLE_EXPRESSION.test(text) ? text : `(${text})`;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function placesOf(draft, fallback) {
|
|
176
|
+
var _draft_places;
|
|
177
|
+
const places = Number((_draft_places = draft.places) != null ? _draft_places : fallback);
|
|
178
|
+
return Number.isInteger(places) && places >= 0 && places <= 6 ? places : fallback;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* The expression a draft writes, or `null` while a value it needs is blank.
|
|
182
|
+
* `current` is what the box held when the builder opened.
|
|
183
|
+
*/ export function buildFormula(draft, current = '') {
|
|
184
|
+
const spec = FORMULA_OPERATIONS[draft.operation];
|
|
185
|
+
const operands = draft.operands.slice(0, spec.variadic ? draft.operands.length : spec.minOperands);
|
|
186
|
+
if (operands.length < spec.minOperands) return null;
|
|
187
|
+
const arithmetic = [
|
|
188
|
+
'add',
|
|
189
|
+
'subtract',
|
|
190
|
+
'multiply',
|
|
191
|
+
'divide',
|
|
192
|
+
'join'
|
|
193
|
+
].includes(draft.operation);
|
|
194
|
+
const parts = operands.map((operand)=>operandText(operand, current, !arithmetic && draft.operation !== 'value'));
|
|
195
|
+
if (parts.some((part)=>part === null)) return null;
|
|
196
|
+
const values = parts;
|
|
197
|
+
const [first] = values;
|
|
198
|
+
switch(draft.operation){
|
|
199
|
+
case 'value':
|
|
200
|
+
return first;
|
|
201
|
+
case 'add':
|
|
202
|
+
return values.join(' + ');
|
|
203
|
+
case 'subtract':
|
|
204
|
+
return values.join(' - ');
|
|
205
|
+
case 'multiply':
|
|
206
|
+
return values.join(' * ');
|
|
207
|
+
case 'divide':
|
|
208
|
+
return values.join(' / ');
|
|
209
|
+
case 'min':
|
|
210
|
+
return `min(${values.join(', ')})`;
|
|
211
|
+
case 'max':
|
|
212
|
+
return `max(${values.join(', ')})`;
|
|
213
|
+
case 'round':
|
|
214
|
+
return `round(${first}, ${placesOf(draft, 0)})`;
|
|
215
|
+
case 'roundDown':
|
|
216
|
+
return `floor(${first})`;
|
|
217
|
+
case 'roundUp':
|
|
218
|
+
return `ceil(${first})`;
|
|
219
|
+
case 'absolute':
|
|
220
|
+
return `abs(${first})`;
|
|
221
|
+
case 'currency':
|
|
222
|
+
var _draft_symbol;
|
|
223
|
+
return `${quoteText((_draft_symbol = draft.symbol) != null ? _draft_symbol : '$')} + format(${first}, ${placesOf(draft, 2)})`;
|
|
224
|
+
case 'number':
|
|
225
|
+
return `format(${first}, ${placesOf(draft, 0)})`;
|
|
226
|
+
case 'join':
|
|
227
|
+
{
|
|
228
|
+
var _draft_separator;
|
|
229
|
+
// A leading '' keeps two numbers joined as text rather than added.
|
|
230
|
+
const separator = (_draft_separator = draft.separator) != null ? _draft_separator : ' ';
|
|
231
|
+
return separator ? values.join(` + ${quoteText(separator)} + `) : [
|
|
232
|
+
'\'\'',
|
|
233
|
+
...values
|
|
234
|
+
].join(' + ');
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
/** A fresh draft for `operation`, carrying over the values already chosen. */ export function formulaDraftFor(operation, previous, current = '') {
|
|
239
|
+
const spec = FORMULA_OPERATIONS[operation];
|
|
240
|
+
const blank = (index)=>index === 0 && current.trim() ? {
|
|
241
|
+
source: 'current'
|
|
242
|
+
} : operation === 'join' ? {
|
|
243
|
+
source: 'text',
|
|
244
|
+
value: ''
|
|
245
|
+
} : {
|
|
246
|
+
source: 'name',
|
|
247
|
+
name: ''
|
|
248
|
+
};
|
|
249
|
+
const operands = Array.from({
|
|
250
|
+
length: spec.minOperands
|
|
251
|
+
}, (_, index)=>{
|
|
252
|
+
var _ref;
|
|
253
|
+
return (_ref = previous == null ? void 0 : previous.operands[index]) != null ? _ref : blank(index);
|
|
254
|
+
});
|
|
255
|
+
return {
|
|
256
|
+
operation,
|
|
257
|
+
operands,
|
|
258
|
+
places: previous == null ? void 0 : previous.places,
|
|
259
|
+
symbol: previous == null ? void 0 : previous.symbol,
|
|
260
|
+
separator: previous == null ? void 0 : previous.separator
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
//# sourceMappingURL=formula.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/logic/src/lib/model/formula.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * A function step's expression, built by picking an operation and its values\n * instead of typing it (AGL-3757). What comes out is the SAME expression an\n * author could have typed — the evaluator, the stored definition and every\n * page that runs it are untouched; this only writes the text.\n */\n\nexport type FormulaOperation =\n | 'value'\n | 'add'\n | 'subtract'\n | 'multiply'\n | 'divide'\n | 'min'\n | 'max'\n | 'round'\n | 'roundDown'\n | 'roundUp'\n | 'absolute'\n | 'currency'\n | 'number'\n | 'join'\n\n/** One value an operation works on. */\nexport type FormulaOperand =\n /** A parameter, a local variable, a site variable or a dictionary member. */\n | { source: 'name'; name: string }\n | { source: 'number'; value: string }\n | { source: 'text'; value: string }\n /** Whatever the box holds already, so formulas can be layered. */\n | { source: 'current' }\n\nexport interface FormulaDraft {\n operation: FormulaOperation\n operands: FormulaOperand[]\n /** Decimal places for round, currency and number. */\n places?: number\n /** The sign written before a currency amount. */\n symbol?: string\n /** What goes between joined texts. */\n separator?: string\n}\n\ninterface FormulaOperationSpec {\n label: string\n /** How many values it takes at least; `variadic` ones take more. */\n minOperands: number\n variadic?: boolean\n /** What each value box is called, in order; the last repeats. */\n operandLabels: string[]\n places?: boolean\n}\n\nexport const FORMULA_OPERATIONS: Record<FormulaOperation, FormulaOperationSpec> = {\n value: { label: 'Use a value', minOperands: 1, operandLabels: ['Value'] },\n add: {\n label: 'Add',\n minOperands: 2,\n variadic: true,\n operandLabels: ['Value', 'Plus'],\n },\n subtract: {\n label: 'Subtract',\n minOperands: 2,\n operandLabels: ['Value', 'Minus'],\n },\n multiply: {\n label: 'Multiply',\n minOperands: 2,\n variadic: true,\n operandLabels: ['Value', 'Times'],\n },\n divide: {\n label: 'Divide',\n minOperands: 2,\n operandLabels: ['Value', 'Divided by'],\n },\n min: {\n label: 'Smallest of',\n minOperands: 2,\n variadic: true,\n operandLabels: ['Value', 'Or'],\n },\n max: {\n label: 'Largest of',\n minOperands: 2,\n variadic: true,\n operandLabels: ['Value', 'Or'],\n },\n round: {\n label: 'Round',\n minOperands: 1,\n operandLabels: ['Value'],\n places: true,\n },\n roundDown: { label: 'Round down', minOperands: 1, operandLabels: ['Value'] },\n roundUp: { label: 'Round up', minOperands: 1, operandLabels: ['Value'] },\n absolute: {\n label: 'Without its minus sign',\n minOperands: 1,\n operandLabels: ['Value'],\n },\n currency: {\n label: 'Format as money',\n minOperands: 1,\n operandLabels: ['Amount'],\n places: true,\n },\n number: {\n label: 'Format with thousands separators',\n minOperands: 1,\n operandLabels: ['Number'],\n places: true,\n },\n join: {\n label: 'Join text',\n minOperands: 2,\n variadic: true,\n operandLabels: ['Text', 'Then'],\n },\n}\n\n/** The operations, in the order the picker lists them. */\nexport const FORMULA_OPERATION_ORDER = Object.keys(\n FORMULA_OPERATIONS,\n) as FormulaOperation[]\n\n/** The label for a value box at `index` of `operation`. */\nexport function formulaOperandLabel(\n operation: FormulaOperation,\n index: number,\n): string {\n const labels = FORMULA_OPERATIONS[operation].operandLabels\n return labels[Math.min(index, labels.length - 1)]\n}\n\n/**\n * A text as the evaluator reads it. Its strings have no escapes, so the\n * quote the text does not contain is used; a text holding both loses its\n * double quotes rather than writing an expression that cannot be read.\n */\nfunction quoteText(value: string): string {\n if (!value.includes(\"'\")) return `'${value}'`\n if (!value.includes('\"')) return `\"${value}\"`\n return `'${value.replace(/\"/g, '')}'`\n}\n\nconst SIMPLE_EXPRESSION = /^\\s*(?:-?[0-9]*\\.?[0-9]+|[a-zA-Z_][a-zA-Z0-9_.]*)\\s*$/\n\n/**\n * One operand as expression text, or `null` while it is still blank. `bare`\n * is a built-in's argument, which its own parentheses already delimit.\n */\nfunction operandText(\n operand: FormulaOperand,\n current: string,\n bare = false,\n): string | null {\n switch (operand.source) {\n case 'name':\n return operand.name ? operand.name : null\n case 'number': {\n const text = operand.value.trim()\n if (!text) return null\n const parsed = Number(text)\n return Number.isFinite(parsed) ? String(parsed) : null\n }\n case 'text':\n return quoteText(operand.value)\n case 'current': {\n const text = current.trim()\n if (!text) return null\n return bare || SIMPLE_EXPRESSION.test(text) ? text : `(${text})`\n }\n }\n}\n\nfunction placesOf(draft: FormulaDraft, fallback: number): number {\n const places = Number(draft.places ?? fallback)\n return Number.isInteger(places) && places >= 0 && places <= 6 ? places : fallback\n}\n\n/**\n * The expression a draft writes, or `null` while a value it needs is blank.\n * `current` is what the box held when the builder opened.\n */\nexport function buildFormula(draft: FormulaDraft, current = ''): string | null {\n const spec = FORMULA_OPERATIONS[draft.operation]\n const operands = draft.operands.slice(\n 0,\n spec.variadic ? draft.operands.length : spec.minOperands,\n )\n if (operands.length < spec.minOperands) return null\n const arithmetic = ['add', 'subtract', 'multiply', 'divide', 'join'].includes(\n draft.operation,\n )\n const parts = operands.map((operand) =>\n operandText(operand, current, !arithmetic && draft.operation !== 'value'),\n )\n if (parts.some((part) => part === null)) return null\n const values = parts as string[]\n const [first] = values\n switch (draft.operation) {\n case 'value':\n return first\n case 'add':\n return values.join(' + ')\n case 'subtract':\n return values.join(' - ')\n case 'multiply':\n return values.join(' * ')\n case 'divide':\n return values.join(' / ')\n case 'min':\n return `min(${values.join(', ')})`\n case 'max':\n return `max(${values.join(', ')})`\n case 'round':\n return `round(${first}, ${placesOf(draft, 0)})`\n case 'roundDown':\n return `floor(${first})`\n case 'roundUp':\n return `ceil(${first})`\n case 'absolute':\n return `abs(${first})`\n case 'currency':\n return `${quoteText(draft.symbol ?? '$')} + format(${first}, ${placesOf(draft, 2)})`\n case 'number':\n return `format(${first}, ${placesOf(draft, 0)})`\n case 'join': {\n // A leading '' keeps two numbers joined as text rather than added.\n const separator = draft.separator ?? ' '\n return separator\n ? values.join(` + ${quoteText(separator)} + `)\n : ['\\'\\'', ...values].join(' + ')\n }\n }\n}\n\n/** A fresh draft for `operation`, carrying over the values already chosen. */\nexport function formulaDraftFor(\n operation: FormulaOperation,\n previous?: FormulaDraft,\n current = '',\n): FormulaDraft {\n const spec = FORMULA_OPERATIONS[operation]\n const blank = (index: number): FormulaOperand =>\n index === 0 && current.trim()\n ? { source: 'current' }\n : operation === 'join'\n ? { source: 'text', value: '' }\n : { source: 'name', name: '' }\n const operands = Array.from(\n { length: spec.minOperands },\n (_, index) => previous?.operands[index] ?? blank(index),\n )\n return {\n operation,\n operands,\n places: previous?.places,\n symbol: previous?.symbol,\n separator: previous?.separator,\n }\n}\n"],"names":["FORMULA_OPERATIONS","value","label","minOperands","operandLabels","add","variadic","subtract","multiply","divide","min","max","round","places","roundDown","roundUp","absolute","currency","number","join","FORMULA_OPERATION_ORDER","Object","keys","formulaOperandLabel","operation","index","labels","Math","length","quoteText","includes","replace","SIMPLE_EXPRESSION","operandText","operand","current","bare","source","name","text","trim","parsed","Number","isFinite","String","test","placesOf","draft","fallback","isInteger","buildFormula","spec","operands","slice","arithmetic","parts","map","some","part","values","first","symbol","separator","formulaDraftFor","previous","blank","Array","from","_"],"mappings":"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"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* A dictionary or collection variable, as rows an author fills in rather than
|
|
19
|
+
* JSON they type (AGL-3757). The stored value is unchanged — the same JSON
|
|
20
|
+
* string `formatVariableValue` and a function's `name.member` read — so this
|
|
21
|
+
* module only translates between that string and the rows the editor shows.
|
|
22
|
+
*/
|
|
23
|
+
/** What one entry holds; `kept` is a nested value the rows cannot edit. */
|
|
24
|
+
export type StructuredValueKind = 'text' | 'number' | 'boolean' | 'kept';
|
|
25
|
+
export interface StructuredValueRow {
|
|
26
|
+
/** The dictionary key; ignored for a collection. */
|
|
27
|
+
key: string;
|
|
28
|
+
kind: StructuredValueKind;
|
|
29
|
+
/** Text as shown in the box; `'true'`/`'false'` for a true/false entry. */
|
|
30
|
+
value: string;
|
|
31
|
+
/** The original nested value, carried through untouched for a `kept` row. */
|
|
32
|
+
kept?: unknown;
|
|
33
|
+
}
|
|
34
|
+
export interface ParsedStructuredValue {
|
|
35
|
+
rows: StructuredValueRow[];
|
|
36
|
+
/**
|
|
37
|
+
* True when a stored value could not be read as the expected shape — an
|
|
38
|
+
* old hand-typed value with a stray comma, or a list stored on a
|
|
39
|
+
* dictionary. The editor starts from what it could read and says so.
|
|
40
|
+
*/
|
|
41
|
+
unreadable: boolean;
|
|
42
|
+
}
|
|
43
|
+
/** The rows a stored dictionary or collection value reads as. */
|
|
44
|
+
export declare function parseStructuredValue(raw: string | undefined, shape: 'dictionary' | 'collection'): ParsedStructuredValue;
|
|
45
|
+
/**
|
|
46
|
+
* The JSON string rows store as. A dictionary drops rows without a key and
|
|
47
|
+
* keeps the LAST row of a repeated key, as reading the JSON back would.
|
|
48
|
+
*/
|
|
49
|
+
export declare function serializeStructuredValue(rows: readonly StructuredValueRow[], shape: 'dictionary' | 'collection'): string;
|
|
50
|
+
/** The dictionary keys used more than once, which the editor flags. */
|
|
51
|
+
export declare function duplicateStructuredKeys(rows: readonly StructuredValueRow[]): Set<string>;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ /**
|
|
17
|
+
* A dictionary or collection variable, as rows an author fills in rather than
|
|
18
|
+
* JSON they type (AGL-3757). The stored value is unchanged — the same JSON
|
|
19
|
+
* string `formatVariableValue` and a function's `name.member` read — so this
|
|
20
|
+
* module only translates between that string and the rows the editor shows.
|
|
21
|
+
*/ /** What one entry holds; `kept` is a nested value the rows cannot edit. */ function rowFor(key, value) {
|
|
22
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
23
|
+
return {
|
|
24
|
+
key,
|
|
25
|
+
kind: 'number',
|
|
26
|
+
value: String(value)
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
if (typeof value === 'boolean') {
|
|
30
|
+
return {
|
|
31
|
+
key,
|
|
32
|
+
kind: 'boolean',
|
|
33
|
+
value: value ? 'true' : 'false'
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
if (typeof value === 'string') return {
|
|
37
|
+
key,
|
|
38
|
+
kind: 'text',
|
|
39
|
+
value
|
|
40
|
+
};
|
|
41
|
+
if (value === null || value === undefined) {
|
|
42
|
+
return {
|
|
43
|
+
key,
|
|
44
|
+
kind: 'text',
|
|
45
|
+
value: ''
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
key,
|
|
50
|
+
kind: 'kept',
|
|
51
|
+
value: JSON.stringify(value),
|
|
52
|
+
kept: value
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/** The rows a stored dictionary or collection value reads as. */ export function parseStructuredValue(raw, shape) {
|
|
56
|
+
const text = String(raw != null ? raw : '').trim();
|
|
57
|
+
if (!text) return {
|
|
58
|
+
rows: [],
|
|
59
|
+
unreadable: false
|
|
60
|
+
};
|
|
61
|
+
let parsed;
|
|
62
|
+
try {
|
|
63
|
+
parsed = JSON.parse(text);
|
|
64
|
+
} catch (unused) {
|
|
65
|
+
return {
|
|
66
|
+
rows: [],
|
|
67
|
+
unreadable: true
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (shape === 'collection') {
|
|
71
|
+
if (!Array.isArray(parsed)) return {
|
|
72
|
+
rows: [],
|
|
73
|
+
unreadable: true
|
|
74
|
+
};
|
|
75
|
+
return {
|
|
76
|
+
rows: parsed.map((item)=>rowFor('', item)),
|
|
77
|
+
unreadable: false
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
81
|
+
return {
|
|
82
|
+
rows: [],
|
|
83
|
+
unreadable: true
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
rows: Object.entries(parsed).map(([key, value])=>rowFor(key, value)),
|
|
88
|
+
unreadable: false
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* One row's stored value. A number box that does not hold a number stores
|
|
93
|
+
* nothing rather than a string a calculation would then trip over.
|
|
94
|
+
*/ function rowValue(row) {
|
|
95
|
+
switch(row.kind){
|
|
96
|
+
case 'number':
|
|
97
|
+
{
|
|
98
|
+
const text = row.value.trim();
|
|
99
|
+
const parsed = Number(text);
|
|
100
|
+
return text !== '' && Number.isFinite(parsed) ? parsed : null;
|
|
101
|
+
}
|
|
102
|
+
case 'boolean':
|
|
103
|
+
return row.value === 'true';
|
|
104
|
+
case 'kept':
|
|
105
|
+
return row.kept;
|
|
106
|
+
case 'text':
|
|
107
|
+
default:
|
|
108
|
+
return row.value;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* The JSON string rows store as. A dictionary drops rows without a key and
|
|
113
|
+
* keeps the LAST row of a repeated key, as reading the JSON back would.
|
|
114
|
+
*/ export function serializeStructuredValue(rows, shape) {
|
|
115
|
+
if (shape === 'collection') return JSON.stringify(rows.map(rowValue));
|
|
116
|
+
const entries = {};
|
|
117
|
+
for (const row of rows){
|
|
118
|
+
const key = row.key.trim();
|
|
119
|
+
if (key) entries[key] = rowValue(row);
|
|
120
|
+
}
|
|
121
|
+
return JSON.stringify(entries);
|
|
122
|
+
}
|
|
123
|
+
/** The dictionary keys used more than once, which the editor flags. */ export function duplicateStructuredKeys(rows) {
|
|
124
|
+
const seen = new Set();
|
|
125
|
+
const duplicates = new Set();
|
|
126
|
+
for (const row of rows){
|
|
127
|
+
const key = row.key.trim();
|
|
128
|
+
if (!key) continue;
|
|
129
|
+
if (seen.has(key)) duplicates.add(key);
|
|
130
|
+
seen.add(key);
|
|
131
|
+
}
|
|
132
|
+
return duplicates;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
//# sourceMappingURL=structured-value.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/logic/src/lib/model/structured-value.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * A dictionary or collection variable, as rows an author fills in rather than\n * JSON they type (AGL-3757). The stored value is unchanged — the same JSON\n * string `formatVariableValue` and a function's `name.member` read — so this\n * module only translates between that string and the rows the editor shows.\n */\n\n/** What one entry holds; `kept` is a nested value the rows cannot edit. */\nexport type StructuredValueKind = 'text' | 'number' | 'boolean' | 'kept'\n\nexport interface StructuredValueRow {\n /** The dictionary key; ignored for a collection. */\n key: string\n kind: StructuredValueKind\n /** Text as shown in the box; `'true'`/`'false'` for a true/false entry. */\n value: string\n /** The original nested value, carried through untouched for a `kept` row. */\n kept?: unknown\n}\n\nexport interface ParsedStructuredValue {\n rows: StructuredValueRow[]\n /**\n * True when a stored value could not be read as the expected shape — an\n * old hand-typed value with a stray comma, or a list stored on a\n * dictionary. The editor starts from what it could read and says so.\n */\n unreadable: boolean\n}\n\nfunction rowFor(key: string, value: unknown): StructuredValueRow {\n if (typeof value === 'number' && Number.isFinite(value)) {\n return { key, kind: 'number', value: String(value) }\n }\n if (typeof value === 'boolean') {\n return { key, kind: 'boolean', value: value ? 'true' : 'false' }\n }\n if (typeof value === 'string') return { key, kind: 'text', value }\n if (value === null || value === undefined) {\n return { key, kind: 'text', value: '' }\n }\n return { key, kind: 'kept', value: JSON.stringify(value), kept: value }\n}\n\n/** The rows a stored dictionary or collection value reads as. */\nexport function parseStructuredValue(\n raw: string | undefined,\n shape: 'dictionary' | 'collection',\n): ParsedStructuredValue {\n const text = String(raw ?? '').trim()\n if (!text) return { rows: [], unreadable: false }\n let parsed: unknown\n try {\n parsed = JSON.parse(text)\n } catch {\n return { rows: [], unreadable: true }\n }\n if (shape === 'collection') {\n if (!Array.isArray(parsed)) return { rows: [], unreadable: true }\n return { rows: parsed.map((item) => rowFor('', item)), unreadable: false }\n }\n if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {\n return { rows: [], unreadable: true }\n }\n return {\n rows: Object.entries(parsed as Record<string, unknown>).map(\n ([key, value]) => rowFor(key, value),\n ),\n unreadable: false,\n }\n}\n\n/**\n * One row's stored value. A number box that does not hold a number stores\n * nothing rather than a string a calculation would then trip over.\n */\nfunction rowValue(row: StructuredValueRow): unknown {\n switch (row.kind) {\n case 'number': {\n const text = row.value.trim()\n const parsed = Number(text)\n return text !== '' && Number.isFinite(parsed) ? parsed : null\n }\n case 'boolean':\n return row.value === 'true'\n case 'kept':\n return row.kept\n case 'text':\n default:\n return row.value\n }\n}\n\n/**\n * The JSON string rows store as. A dictionary drops rows without a key and\n * keeps the LAST row of a repeated key, as reading the JSON back would.\n */\nexport function serializeStructuredValue(\n rows: readonly StructuredValueRow[],\n shape: 'dictionary' | 'collection',\n): string {\n if (shape === 'collection') return JSON.stringify(rows.map(rowValue))\n const entries: Record<string, unknown> = {}\n for (const row of rows) {\n const key = row.key.trim()\n if (key) entries[key] = rowValue(row)\n }\n return JSON.stringify(entries)\n}\n\n/** The dictionary keys used more than once, which the editor flags. */\nexport function duplicateStructuredKeys(\n rows: readonly StructuredValueRow[],\n): Set<string> {\n const seen = new Set<string>()\n const duplicates = new Set<string>()\n for (const row of rows) {\n const key = row.key.trim()\n if (!key) continue\n if (seen.has(key)) duplicates.add(key)\n seen.add(key)\n }\n return duplicates\n}\n"],"names":["rowFor","key","value","Number","isFinite","kind","String","undefined","JSON","stringify","kept","parseStructuredValue","raw","shape","text","trim","rows","unreadable","parsed","parse","Array","isArray","map","item","Object","entries","rowValue","row","serializeStructuredValue","duplicateStructuredKeys","seen","Set","duplicates","has","add"],"mappings":"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"}
|