form0-core 0.1.4 → 0.1.6
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 +1 -1
- package/src/engine/calculation.js +31 -3
- package/src/engine/context-resolver.js +51 -6
- package/src/engine/evaluator.js +18 -6
- package/src/engine/form-engine.js +3 -1
- package/src/index.js +1 -0
- package/src/utilities/calculation-editor.js +178 -6
- package/src/utilities/calculation-expression-utils.js +15 -0
package/package.json
CHANGED
|
@@ -9,7 +9,8 @@ export function evaluateCalculatedFields(
|
|
|
9
9
|
helpers,
|
|
10
10
|
securityConfig,
|
|
11
11
|
contextResolver = null,
|
|
12
|
-
warningSystem = null
|
|
12
|
+
warningSystem = null,
|
|
13
|
+
runtimeDiagnostics = null
|
|
13
14
|
) {
|
|
14
15
|
const fields = flattenFields(schema.elements);
|
|
15
16
|
|
|
@@ -40,10 +41,37 @@ export function evaluateCalculatedFields(
|
|
|
40
41
|
context,
|
|
41
42
|
securityConfig,
|
|
42
43
|
false,
|
|
43
|
-
schema
|
|
44
|
+
schema,
|
|
45
|
+
{
|
|
46
|
+
suppressConsoleWarning: Array.isArray(runtimeDiagnostics),
|
|
47
|
+
onError: (error) => {
|
|
48
|
+
if (!Array.isArray(runtimeDiagnostics)) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
runtimeDiagnostics.push({
|
|
53
|
+
fieldName: field.data_name,
|
|
54
|
+
message:
|
|
55
|
+
error instanceof Error && error.message
|
|
56
|
+
? error.message
|
|
57
|
+
: 'Unknown calculation runtime error.',
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
}
|
|
44
61
|
);
|
|
45
62
|
} catch (e) {
|
|
46
|
-
|
|
63
|
+
if (Array.isArray(runtimeDiagnostics)) {
|
|
64
|
+
runtimeDiagnostics.push({
|
|
65
|
+
fieldName: field.data_name,
|
|
66
|
+
message:
|
|
67
|
+
e instanceof Error && e.message
|
|
68
|
+
? e.message
|
|
69
|
+
: 'Unknown calculation runtime error.',
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
if (!Array.isArray(runtimeDiagnostics)) {
|
|
73
|
+
console.warn(`Calculation failed for ${field.data_name}:`, e.message);
|
|
74
|
+
}
|
|
47
75
|
}
|
|
48
76
|
}
|
|
49
77
|
});
|
|
@@ -93,7 +93,13 @@ export class ContextResolver {
|
|
|
93
93
|
|
|
94
94
|
// Main form fields (no parent RepeatableSection) are always accessible
|
|
95
95
|
if (fieldInfo.parentPath.length === 0) {
|
|
96
|
-
return this.buildAccessInfo(
|
|
96
|
+
return this.buildAccessInfo(
|
|
97
|
+
'accessible',
|
|
98
|
+
'main_form',
|
|
99
|
+
fieldName,
|
|
100
|
+
fieldInfo,
|
|
101
|
+
executionContext
|
|
102
|
+
);
|
|
97
103
|
}
|
|
98
104
|
|
|
99
105
|
// RepeatableSection field access depends on execution context
|
|
@@ -246,7 +252,10 @@ export class ContextResolver {
|
|
|
246
252
|
* @returns {boolean} True if they share the same RepeatableSection context
|
|
247
253
|
*/
|
|
248
254
|
haveSameRepeatableSectionContext(parentPath1, parentPath2) {
|
|
249
|
-
return
|
|
255
|
+
return (
|
|
256
|
+
this.resolveRepeatableSectionAccessCode(parentPath1, parentPath2) !==
|
|
257
|
+
'different_repeatable_section'
|
|
258
|
+
);
|
|
250
259
|
}
|
|
251
260
|
|
|
252
261
|
resolveRepeatableSectionRelationship(
|
|
@@ -271,7 +280,10 @@ export class ContextResolver {
|
|
|
271
280
|
return contextPath.length === 0 ? 'main_form' : 'same_repeatable_section';
|
|
272
281
|
}
|
|
273
282
|
|
|
274
|
-
if (
|
|
283
|
+
if (
|
|
284
|
+
targetPath.length < contextPath.length &&
|
|
285
|
+
ContextResolver.isPrefix(targetPath, contextPath)
|
|
286
|
+
) {
|
|
275
287
|
return targetPath.length === 0 ? 'main_form' : 'ancestor_repeatable_context';
|
|
276
288
|
}
|
|
277
289
|
|
|
@@ -293,7 +305,12 @@ export class ContextResolver {
|
|
|
293
305
|
level,
|
|
294
306
|
code,
|
|
295
307
|
message: `Field '${fieldName}' is not accessible from current context`,
|
|
296
|
-
suggestion: this.buildRestrictedAccessSuggestion(
|
|
308
|
+
suggestion: this.buildRestrictedAccessSuggestion(
|
|
309
|
+
executionContext,
|
|
310
|
+
fieldName,
|
|
311
|
+
fieldInfo,
|
|
312
|
+
code
|
|
313
|
+
),
|
|
297
314
|
fieldInfo,
|
|
298
315
|
};
|
|
299
316
|
}
|
|
@@ -303,7 +320,7 @@ export class ContextResolver {
|
|
|
303
320
|
|
|
304
321
|
if (contextType === 'calculation') {
|
|
305
322
|
if (code === 'different_repeatable_section') {
|
|
306
|
-
return `CalculatedField can only access fields in the same RepeatableSection or ancestor contexts. Field '${fieldName}' is in RepeatableSection: ${fieldInfo.parentPath
|
|
323
|
+
return `CalculatedField can only access fields in the same RepeatableSection or ancestor contexts. Field '${fieldName}' is in RepeatableSection: ${this.formatRepeatableSectionPath(fieldInfo.parentPath)}`;
|
|
307
324
|
}
|
|
308
325
|
}
|
|
309
326
|
|
|
@@ -322,6 +339,33 @@ export class ContextResolver {
|
|
|
322
339
|
return 'Check the field access rules for your current context';
|
|
323
340
|
}
|
|
324
341
|
|
|
342
|
+
formatRepeatableSectionPath(parentPath) {
|
|
343
|
+
if (!Array.isArray(parentPath) || parentPath.length === 0) {
|
|
344
|
+
return 'main form';
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return parentPath
|
|
348
|
+
.map((_, index) => this.formatRepeatableSectionSegment(parentPath.slice(0, index + 1)))
|
|
349
|
+
.join(' -> ');
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
formatRepeatableSectionSegment(currentPath) {
|
|
353
|
+
for (const treeInfo of this.repeatableSectionTree.values()) {
|
|
354
|
+
if (!ContextResolver.pathsEqual(treeInfo.currentPath, currentPath)) {
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const dataName = treeInfo.field?.data_name || treeInfo.preferredKey;
|
|
359
|
+
if (dataName === treeInfo.preferredKey) {
|
|
360
|
+
return dataName;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return `${dataName} (${treeInfo.preferredKey})`;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return currentPath[currentPath.length - 1];
|
|
367
|
+
}
|
|
368
|
+
|
|
325
369
|
static isPrefix(prefix, fullPath) {
|
|
326
370
|
if (prefix.length === 0) {
|
|
327
371
|
return true;
|
|
@@ -381,7 +425,8 @@ export class ContextResolver {
|
|
|
381
425
|
? `Field '${fieldName}' does not exist in the form schema`
|
|
382
426
|
: `Field '${fieldName}' is not accessible from current context`),
|
|
383
427
|
suggestion:
|
|
384
|
-
accessInfo.suggestion ||
|
|
428
|
+
accessInfo.suggestion ||
|
|
429
|
+
this.generateAccessSuggestion(executionContext, fieldName, fieldInfo),
|
|
385
430
|
fieldContext: fieldInfo
|
|
386
431
|
? {
|
|
387
432
|
parentPath: fieldInfo.parentPath,
|
package/src/engine/evaluator.js
CHANGED
|
@@ -3,19 +3,26 @@ import { __setEvalContext, __clearEvalContext } from '../builtins/control/eval.j
|
|
|
3
3
|
import { __setDataNamesContext, __clearDataNamesContext } from '../builtins/schema/datanames.js';
|
|
4
4
|
import { validateExpression, createSecureContext, withTimeout } from '../security/validation.js';
|
|
5
5
|
import { DEFAULT_SECURITY_CONFIG } from '../security/config.js';
|
|
6
|
+
import {
|
|
7
|
+
isMultilineCalculationExpression,
|
|
8
|
+
normalizeInlineCalculationExpression,
|
|
9
|
+
} from '../utilities/calculation-expression-utils.js';
|
|
6
10
|
|
|
7
11
|
export function runExpression(
|
|
8
12
|
expr,
|
|
9
13
|
context = {},
|
|
10
14
|
securityConfig = DEFAULT_SECURITY_CONFIG,
|
|
11
15
|
includeEventBuiltins = false,
|
|
12
|
-
schema = null
|
|
16
|
+
schema = null,
|
|
17
|
+
options = {}
|
|
13
18
|
) {
|
|
14
19
|
try {
|
|
15
20
|
// Validate expression based on security mode
|
|
16
21
|
const validation = validateExpression(expr, securityConfig, includeEventBuiltins);
|
|
17
22
|
if (!validation.valid) {
|
|
18
|
-
|
|
23
|
+
if (!options.suppressConsoleWarning) {
|
|
24
|
+
console.warn('[form0] Expression validation failed:', validation.reason);
|
|
25
|
+
}
|
|
19
26
|
return null;
|
|
20
27
|
}
|
|
21
28
|
|
|
@@ -34,8 +41,7 @@ export function runExpression(
|
|
|
34
41
|
|
|
35
42
|
try {
|
|
36
43
|
// Handle both expressions and multi-line code (Windows-safe)
|
|
37
|
-
const isMultiLine =
|
|
38
|
-
expr.includes('\r\n') || expr.includes('\n') || expr.includes('function');
|
|
44
|
+
const isMultiLine = isMultilineCalculationExpression(expr);
|
|
39
45
|
|
|
40
46
|
if (isMultiLine) {
|
|
41
47
|
// Execute as code block (recompile each time for now)
|
|
@@ -46,7 +52,8 @@ export function runExpression(
|
|
|
46
52
|
return consumed.called ? consumed.value : result;
|
|
47
53
|
} else {
|
|
48
54
|
// Execute as expression (existing behavior)
|
|
49
|
-
const
|
|
55
|
+
const inlineExpression = normalizeInlineCalculationExpression(expr);
|
|
56
|
+
const fn = new Function(...keys, `return (${inlineExpression});`);
|
|
50
57
|
const result = fn(...values);
|
|
51
58
|
const consumed = __consumeResult();
|
|
52
59
|
return consumed.called ? consumed.value : result;
|
|
@@ -69,7 +76,12 @@ export function runExpression(
|
|
|
69
76
|
|
|
70
77
|
return executeExpression();
|
|
71
78
|
} catch (e) {
|
|
72
|
-
|
|
79
|
+
if (typeof options.onError === 'function') {
|
|
80
|
+
options.onError(e);
|
|
81
|
+
}
|
|
82
|
+
if (!options.suppressConsoleWarning) {
|
|
83
|
+
console.warn('[form0] Expression evaluation failed:', e.message);
|
|
84
|
+
}
|
|
73
85
|
return null;
|
|
74
86
|
}
|
|
75
87
|
}
|
|
@@ -19,6 +19,7 @@ export function createFormEngine({
|
|
|
19
19
|
helpers = {},
|
|
20
20
|
security = DEFAULT_SECURITY_CONFIG,
|
|
21
21
|
warningSystem = null,
|
|
22
|
+
runtimeDiagnostics = null,
|
|
22
23
|
}) {
|
|
23
24
|
const { schema: preparedSchema, buildingPlanMeta } = expandBuildingPlanSchema(schema);
|
|
24
25
|
|
|
@@ -126,7 +127,8 @@ export function createFormEngine({
|
|
|
126
127
|
allHelpers,
|
|
127
128
|
security,
|
|
128
129
|
contextResolver,
|
|
129
|
-
sharedWarningSystem
|
|
130
|
+
sharedWarningSystem,
|
|
131
|
+
runtimeDiagnostics
|
|
130
132
|
);
|
|
131
133
|
evaluateRequirement(form, values, required);
|
|
132
134
|
evaluateVisibility(form, values, visible);
|
package/src/index.js
CHANGED
|
@@ -4,8 +4,14 @@ import {
|
|
|
4
4
|
CALCULATION_BUILTIN_DEFINITIONS,
|
|
5
5
|
} from '../builtins/registry.js';
|
|
6
6
|
import { ContextResolver } from '../engine/context-resolver.js';
|
|
7
|
+
import { createFormEngine } from '../engine/form-engine.js';
|
|
8
|
+
import { WarningSystem } from '../engine/warning-system.js';
|
|
7
9
|
import { DEFAULT_SECURITY_CONFIG } from '../security/config.js';
|
|
8
10
|
import { validateExpression } from '../security/validation.js';
|
|
11
|
+
import {
|
|
12
|
+
isMultilineCalculationExpression,
|
|
13
|
+
normalizeInlineCalculationExpression,
|
|
14
|
+
} from './calculation-expression-utils.js';
|
|
9
15
|
import { flattenFields } from './field-helpers.js';
|
|
10
16
|
|
|
11
17
|
const COMMON_GLOBAL_FUNCTIONS = new Set([
|
|
@@ -181,9 +187,7 @@ function toCalculationReferenceDefinition(field, resolver, executionContext) {
|
|
|
181
187
|
type: typeof field.type === 'string' ? field.type : 'UnknownField',
|
|
182
188
|
description: typeof field.description === 'string' ? field.description : null,
|
|
183
189
|
displayStyle:
|
|
184
|
-
field.display &&
|
|
185
|
-
typeof field.display === 'object' &&
|
|
186
|
-
typeof field.display.style === 'string'
|
|
190
|
+
field.display && typeof field.display === 'object' && typeof field.display.style === 'string'
|
|
187
191
|
? field.display.style
|
|
188
192
|
: typeof field.display === 'string'
|
|
189
193
|
? field.display
|
|
@@ -214,15 +218,15 @@ function addIssue(issues, issueKeys, issue) {
|
|
|
214
218
|
}
|
|
215
219
|
|
|
216
220
|
function validateSyntax(expression) {
|
|
217
|
-
const isMultiLine =
|
|
218
|
-
expression.includes('\r\n') || expression.includes('\n') || expression.includes('function');
|
|
221
|
+
const isMultiLine = isMultilineCalculationExpression(expression);
|
|
219
222
|
|
|
220
223
|
if (isMultiLine) {
|
|
221
224
|
new Function(expression);
|
|
222
225
|
return;
|
|
223
226
|
}
|
|
224
227
|
|
|
225
|
-
|
|
228
|
+
const inlineExpression = normalizeInlineCalculationExpression(expression);
|
|
229
|
+
new Function(`return (${inlineExpression});`);
|
|
226
230
|
}
|
|
227
231
|
|
|
228
232
|
function createSchemaFilteredSecurityConfig(securityConfig) {
|
|
@@ -236,6 +240,62 @@ function createSchemaFilteredSecurityConfig(securityConfig) {
|
|
|
236
240
|
};
|
|
237
241
|
}
|
|
238
242
|
|
|
243
|
+
function deepClone(value) {
|
|
244
|
+
if (typeof structuredClone === 'function') {
|
|
245
|
+
return structuredClone(value);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return JSON.parse(JSON.stringify(value));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function cloneSimulationSchema(schema) {
|
|
252
|
+
if (schema && schema.form && Array.isArray(schema.form.elements)) {
|
|
253
|
+
return deepClone(schema);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
form: deepClone(normalizeFormSchema(schema)),
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function findFieldByDataName(elements, fieldDataName) {
|
|
262
|
+
if (!Array.isArray(elements)) {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
for (const element of elements) {
|
|
267
|
+
if (!element || typeof element !== 'object') {
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (element.data_name === fieldDataName) {
|
|
272
|
+
return element;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (Array.isArray(element.elements)) {
|
|
276
|
+
const nestedMatch = findFieldByDataName(element.elements, fieldDataName);
|
|
277
|
+
if (nestedMatch) {
|
|
278
|
+
return nestedMatch;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function sanitizeCollectedWarnings(warnings) {
|
|
287
|
+
if (!Array.isArray(warnings)) {
|
|
288
|
+
return [];
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return warnings.map((warning) => ({
|
|
292
|
+
message: warning?.message || '',
|
|
293
|
+
suggestion: warning?.suggestion || null,
|
|
294
|
+
context: warning?.context || null,
|
|
295
|
+
fieldContext: warning?.fieldContext || null,
|
|
296
|
+
}));
|
|
297
|
+
}
|
|
298
|
+
|
|
239
299
|
/**
|
|
240
300
|
* @typedef {'stable' | 'advanced' | 'unavailable'} CalculationBuiltinStatus
|
|
241
301
|
*
|
|
@@ -341,6 +401,7 @@ export function analyzeCalculationExpression({
|
|
|
341
401
|
const usedBuiltinNames = new Set();
|
|
342
402
|
const referencedFields = [];
|
|
343
403
|
const referencedFieldNames = new Set();
|
|
404
|
+
const setResultCalls = [];
|
|
344
405
|
|
|
345
406
|
const normalizedExpression = typeof expression === 'string' ? expression : '';
|
|
346
407
|
const form = normalizeFormSchema(schema);
|
|
@@ -437,6 +498,10 @@ export function analyzeCalculationExpression({
|
|
|
437
498
|
usedBuiltins.push(call.name);
|
|
438
499
|
}
|
|
439
500
|
|
|
501
|
+
if (call.name === 'SETRESULT') {
|
|
502
|
+
setResultCalls.push(call);
|
|
503
|
+
}
|
|
504
|
+
|
|
440
505
|
if (builtinMetadata.status === 'unavailable') {
|
|
441
506
|
addIssue(
|
|
442
507
|
issues,
|
|
@@ -482,6 +547,22 @@ export function analyzeCalculationExpression({
|
|
|
482
547
|
);
|
|
483
548
|
}
|
|
484
549
|
|
|
550
|
+
if (setResultCalls.length > 1) {
|
|
551
|
+
const duplicateSetResultCall = setResultCalls[1];
|
|
552
|
+
addIssue(
|
|
553
|
+
issues,
|
|
554
|
+
issueKeys,
|
|
555
|
+
createIssue({
|
|
556
|
+
code: 'multiple_setresult_calls',
|
|
557
|
+
severity: 'error',
|
|
558
|
+
message: 'SETRESULT() can only be used once in a CalculatedField expression.',
|
|
559
|
+
symbol: duplicateSetResultCall.name,
|
|
560
|
+
index: duplicateSetResultCall.index,
|
|
561
|
+
length: duplicateSetResultCall.length,
|
|
562
|
+
})
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
|
|
485
566
|
for (const reference of extractFieldReferenceMatches(normalizedExpression)) {
|
|
486
567
|
if (!referencedFieldNames.has(reference.fieldName)) {
|
|
487
568
|
referencedFieldNames.add(reference.fieldName);
|
|
@@ -532,3 +613,94 @@ export function analyzeCalculationExpression({
|
|
|
532
613
|
referencedFields,
|
|
533
614
|
};
|
|
534
615
|
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Simulate a CalculatedField expression against form0-core's real evaluation engine.
|
|
619
|
+
*
|
|
620
|
+
* @param {Object} options
|
|
621
|
+
* @param {Object} options.schema
|
|
622
|
+
* @param {string} options.fieldDataName
|
|
623
|
+
* @param {string} options.expression
|
|
624
|
+
* @param {Object} [options.values]
|
|
625
|
+
* @param {Object} [options.security]
|
|
626
|
+
* @returns {{
|
|
627
|
+
* result: unknown,
|
|
628
|
+
* runtimeError: string | null,
|
|
629
|
+
* warnings: Array<{
|
|
630
|
+
* message: string,
|
|
631
|
+
* suggestion: string | null,
|
|
632
|
+
* context: Object | null,
|
|
633
|
+
* fieldContext: Object | null,
|
|
634
|
+
* }>,
|
|
635
|
+
* }}
|
|
636
|
+
*/
|
|
637
|
+
export function simulateCalculationExpression({
|
|
638
|
+
schema,
|
|
639
|
+
fieldDataName,
|
|
640
|
+
expression,
|
|
641
|
+
values = {},
|
|
642
|
+
security = DEFAULT_SECURITY_CONFIG,
|
|
643
|
+
}) {
|
|
644
|
+
const simulationSchema = cloneSimulationSchema(schema);
|
|
645
|
+
const form = normalizeFormSchema(simulationSchema);
|
|
646
|
+
const targetField = findFieldByDataName(form.elements, fieldDataName);
|
|
647
|
+
|
|
648
|
+
if (!targetField) {
|
|
649
|
+
return {
|
|
650
|
+
result: null,
|
|
651
|
+
runtimeError: `Field '${fieldDataName}' does not exist in the form schema.`,
|
|
652
|
+
warnings: [],
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
if (targetField.type !== 'CalculatedField') {
|
|
657
|
+
return {
|
|
658
|
+
result: null,
|
|
659
|
+
runtimeError: `Field '${fieldDataName}' is not a CalculatedField.`,
|
|
660
|
+
warnings: [],
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
targetField.calculate = typeof expression === 'string' ? expression : '';
|
|
665
|
+
|
|
666
|
+
const warningSystem = new WarningSystem({
|
|
667
|
+
enableCollection: true,
|
|
668
|
+
enableConsoleWarnings: false,
|
|
669
|
+
});
|
|
670
|
+
const runtimeDiagnostics = [];
|
|
671
|
+
|
|
672
|
+
try {
|
|
673
|
+
const engine = createFormEngine({
|
|
674
|
+
schema: simulationSchema,
|
|
675
|
+
initialValues:
|
|
676
|
+
values && typeof values === 'object' && !Array.isArray(values) ? values : {},
|
|
677
|
+
security,
|
|
678
|
+
warningSystem,
|
|
679
|
+
runtimeDiagnostics,
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
engine.eval();
|
|
683
|
+
|
|
684
|
+
const engineState = engine.getState();
|
|
685
|
+
const simulatedResult = engineState.values[fieldDataName];
|
|
686
|
+
const runtimeError =
|
|
687
|
+
runtimeDiagnostics.find((diagnostic) => diagnostic.fieldName === fieldDataName)?.message ||
|
|
688
|
+
runtimeDiagnostics[0]?.message ||
|
|
689
|
+
null;
|
|
690
|
+
|
|
691
|
+
return {
|
|
692
|
+
result: simulatedResult === undefined ? null : simulatedResult,
|
|
693
|
+
runtimeError,
|
|
694
|
+
warnings: sanitizeCollectedWarnings(warningSystem.getCollectedWarnings()),
|
|
695
|
+
};
|
|
696
|
+
} catch (error) {
|
|
697
|
+
return {
|
|
698
|
+
result: null,
|
|
699
|
+
runtimeError:
|
|
700
|
+
error instanceof Error && error.message
|
|
701
|
+
? error.message
|
|
702
|
+
: 'Unknown calculation simulation error.',
|
|
703
|
+
warnings: sanitizeCollectedWarnings(warningSystem.getCollectedWarnings()),
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function isMultilineCalculationExpression(expression) {
|
|
2
|
+
const normalizedExpression = typeof expression === 'string' ? expression : '';
|
|
3
|
+
|
|
4
|
+
return (
|
|
5
|
+
normalizedExpression.includes('\r\n') ||
|
|
6
|
+
normalizedExpression.includes('\n') ||
|
|
7
|
+
normalizedExpression.includes('function')
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function normalizeInlineCalculationExpression(expression) {
|
|
12
|
+
const normalizedExpression = typeof expression === 'string' ? expression : '';
|
|
13
|
+
|
|
14
|
+
return normalizedExpression.replace(/;+\s*$/u, '');
|
|
15
|
+
}
|