@revisium/formula 0.6.2 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -53,6 +53,31 @@ evaluate('max(max, 0)', { max: 10 });
53
53
  evaluate('max(max - field.min, 0)', { max: 100, field: { min: 20 } });
54
54
  // 80
55
55
 
56
+ // AST serialization (inverse of parseFormula)
57
+ import { parseFormula, serializeAst, replaceDependencies } from '@revisium/formula';
58
+
59
+ serializeAst(parseFormula('(a + b) * 2').ast);
60
+ // "(a + b) * 2"
61
+
62
+ serializeAst(parseFormula('a + b * 2').ast);
63
+ // "a + b * 2" (no unnecessary parentheses)
64
+
65
+ serializeAst(parseFormula('items[*].price').ast);
66
+ // "items[*].price"
67
+
68
+ // Dependency replacement
69
+
70
+ const ast = parseFormula('price * quantity + 10').ast;
71
+ const newAst = replaceDependencies(ast, { price: 'cost', quantity: 'qty' });
72
+ serializeAst(newAst);
73
+ // "cost * qty + 10"
74
+
75
+ // Replace with different path types
76
+ const ast2 = parseFormula('items[*].price * 2').ast;
77
+ const newAst2 = replaceDependencies(ast2, { 'items[*].price': 'products[*].cost' });
78
+ serializeAst(newAst2);
79
+ // "products[*].cost * 2"
80
+
56
81
  // Type inference
57
82
  import { inferFormulaType } from '@revisium/formula';
58
83
 
@@ -93,6 +118,8 @@ evaluateWithContext('price * (1 - ../discount)', {
93
118
  | `evaluate` | Evaluate expression with context |
94
119
  | `evaluateWithContext` | Evaluate with automatic `/` and `../` path resolution |
95
120
  | `inferFormulaType` | Infer return type of expression |
121
+ | `serializeAst` | Serialize AST back to expression string (inverse of `parseFormula`) |
122
+ | `replaceDependencies` | Replace dependency paths in AST (returns new AST) |
96
123
 
97
124
  ### Expression API
98
125
 
@@ -89,6 +89,7 @@ var grammarText = String.raw`Formula {
89
89
  // Primary expressions
90
90
  Primary
91
91
  = "(" Expression ")" -- paren
92
+ | "[" string "]" -- bracketedField
92
93
  | number
93
94
  | string
94
95
  | boolean
@@ -146,6 +147,9 @@ var grammarText = String.raw`Formula {
146
147
  var grammar2 = ohm__namespace.grammar(grammarText);
147
148
 
148
149
  // src/ohm/semantics/index.ts
150
+ function isSafePropertyName(name) {
151
+ return name !== "__proto__";
152
+ }
149
153
  function childrenToAST(children) {
150
154
  return children.filter((c) => "toAST" in c).map((c) => c.toAST());
151
155
  }
@@ -319,10 +323,18 @@ semantics.addOperation("toAST", {
319
323
  };
320
324
  },
321
325
  Postfix_index(obj, _lb, index, _rb) {
326
+ const indexAST = index.toAST();
327
+ if (indexAST.type === "StringLiteral") {
328
+ return {
329
+ type: "BracketedMemberExpression",
330
+ object: obj.toAST(),
331
+ property: indexAST.value
332
+ };
333
+ }
322
334
  return {
323
335
  type: "IndexExpression",
324
336
  object: obj.toAST(),
325
- index: index.toAST()
337
+ index: indexAST
326
338
  };
327
339
  },
328
340
  Postfix_wildcard(obj, _lb, _star, _rb) {
@@ -342,6 +354,10 @@ semantics.addOperation("toAST", {
342
354
  Primary_paren(_lp, expr, _rp) {
343
355
  return expr.toAST();
344
356
  },
357
+ Primary_bracketedField(_lb, str, _rb) {
358
+ const strAST = str.toAST();
359
+ return { type: "BracketedIdentifier", name: strAST.value };
360
+ },
345
361
  Primary(e) {
346
362
  return e.toAST();
347
363
  },
@@ -397,6 +413,9 @@ semantics.addOperation("dependencies", {
397
413
  identifier(_start, _rest) {
398
414
  return [this.sourceString];
399
415
  },
416
+ Primary_bracketedField(_lb, _str, _rb) {
417
+ return [this.sourceString];
418
+ },
400
419
  rootPath(_slash, _path) {
401
420
  return [this.sourceString];
402
421
  },
@@ -419,7 +438,15 @@ semantics.addOperation("dependencies", {
419
438
  Postfix_index(obj, _lb, index, _rb) {
420
439
  const objDeps = obj.dependencies();
421
440
  const indexNode = index.toAST();
422
- const getNumericIndex = (node) => {
441
+ if (indexNode.type === "StringLiteral") {
442
+ const strValue = indexNode.value;
443
+ const quote = this.sourceString.includes('"') ? '"' : "'";
444
+ if (objDeps.length === 1) {
445
+ return [`${objDeps[0]}[${quote}${strValue}${quote}]`];
446
+ }
447
+ return objDeps;
448
+ }
449
+ const getNumericIndex2 = (node) => {
423
450
  if (node.type === "NumberLiteral") {
424
451
  return node.value;
425
452
  }
@@ -428,7 +455,7 @@ semantics.addOperation("dependencies", {
428
455
  }
429
456
  return null;
430
457
  };
431
- const numericIndex = getNumericIndex(indexNode);
458
+ const numericIndex = getNumericIndex2(indexNode);
432
459
  if (objDeps.length === 1 && numericIndex !== null) {
433
460
  return [`${objDeps[0]}[${numericIndex}]`];
434
461
  }
@@ -488,6 +515,9 @@ var ARRAY_FUNCTIONS = /* @__PURE__ */ new Set([
488
515
  "includes"
489
516
  ]);
490
517
  semantics.addOperation("features", {
518
+ Primary_bracketedField(_lb, _str, _rb) {
519
+ return ["bracket_notation"];
520
+ },
491
521
  rootPath(_slash, _path) {
492
522
  const path = this.sourceString;
493
523
  const features = ["root_path"];
@@ -518,6 +548,10 @@ semantics.addOperation("features", {
518
548
  Postfix_index(obj, _lb, index, _rb) {
519
549
  const objFeatures = obj.features();
520
550
  const indexFeatures = index.features();
551
+ const indexNode = index.toAST();
552
+ if (indexNode.type === "StringLiteral") {
553
+ return [...objFeatures, ...indexFeatures, "bracket_notation"];
554
+ }
521
555
  return [...objFeatures, ...indexFeatures, "array_index"];
522
556
  },
523
557
  Postfix_wildcard(obj, _lb, _star, _rb) {
@@ -727,10 +761,17 @@ semantics.addOperation("eval(ctx)", {
727
761
  Postfix_index(obj, _lb, index, _rb) {
728
762
  const objVal = obj.eval(this.args.ctx);
729
763
  const idx = index.eval(this.args.ctx);
730
- if (idx < 0) {
731
- return objVal?.[objVal.length + idx];
764
+ if (typeof idx === "string") {
765
+ if (!isSafePropertyName(idx)) {
766
+ return void 0;
767
+ }
768
+ return objVal?.[idx];
732
769
  }
733
- return objVal?.[idx];
770
+ const numIdx = idx;
771
+ if (numIdx < 0) {
772
+ return objVal?.[objVal.length + numIdx];
773
+ }
774
+ return objVal?.[numIdx];
734
775
  },
735
776
  Postfix_wildcard(obj, _lb, _star, _rb) {
736
777
  return obj.eval(this.args.ctx);
@@ -747,6 +788,13 @@ semantics.addOperation("eval(ctx)", {
747
788
  Primary_paren(_lp, expr, _rp) {
748
789
  return expr.eval(this.args.ctx);
749
790
  },
791
+ Primary_bracketedField(_lb, str, _rb) {
792
+ const fieldName = str.eval(this.args.ctx);
793
+ if (!isSafePropertyName(fieldName)) {
794
+ return void 0;
795
+ }
796
+ return this.args.ctx[fieldName];
797
+ },
750
798
  Primary(e) {
751
799
  return e.eval(this.args.ctx);
752
800
  },
@@ -1161,6 +1209,272 @@ function inferFormulaType(expression, fieldTypes = {}) {
1161
1209
  }
1162
1210
  }
1163
1211
 
1212
+ // src/ohm/core/serialize-ast.ts
1213
+ function escapeDoubleQuoted(value) {
1214
+ let result = "";
1215
+ for (const char of value) {
1216
+ switch (char) {
1217
+ case "\\":
1218
+ result += String.raw`\\`;
1219
+ break;
1220
+ case '"':
1221
+ result += String.raw`\"`;
1222
+ break;
1223
+ case "\n":
1224
+ result += String.raw`\n`;
1225
+ break;
1226
+ case "\r":
1227
+ result += String.raw`\r`;
1228
+ break;
1229
+ case " ":
1230
+ result += String.raw`\t`;
1231
+ break;
1232
+ default:
1233
+ result += char;
1234
+ }
1235
+ }
1236
+ return result;
1237
+ }
1238
+ var PRECEDENCE = {
1239
+ "||": 1,
1240
+ "&&": 2,
1241
+ "==": 3,
1242
+ "!=": 3,
1243
+ ">": 4,
1244
+ "<": 4,
1245
+ ">=": 4,
1246
+ "<=": 4,
1247
+ "+": 5,
1248
+ "-": 5,
1249
+ "*": 6,
1250
+ "/": 6,
1251
+ "%": 6
1252
+ };
1253
+ function serializeBinaryOp(node) {
1254
+ const prec = PRECEDENCE[node.op] ?? 0;
1255
+ const wrapLeft = (child) => {
1256
+ const s = serializeNode(child);
1257
+ if (child.type === "BinaryOp") {
1258
+ const childPrec = PRECEDENCE[child.op] ?? 0;
1259
+ if (childPrec < prec) {
1260
+ return `(${s})`;
1261
+ }
1262
+ }
1263
+ if (child.type === "TernaryOp") {
1264
+ return `(${s})`;
1265
+ }
1266
+ return s;
1267
+ };
1268
+ const wrapRight = (child) => {
1269
+ const s = serializeNode(child);
1270
+ if (child.type === "BinaryOp") {
1271
+ const childPrec = PRECEDENCE[child.op] ?? 0;
1272
+ if (childPrec <= prec) {
1273
+ return `(${s})`;
1274
+ }
1275
+ }
1276
+ if (child.type === "TernaryOp") {
1277
+ return `(${s})`;
1278
+ }
1279
+ return s;
1280
+ };
1281
+ return `${wrapLeft(node.left)} ${node.op} ${wrapRight(node.right)}`;
1282
+ }
1283
+ function serializeNode(node) {
1284
+ switch (node.type) {
1285
+ case "NumberLiteral":
1286
+ return String(node.value);
1287
+ case "StringLiteral":
1288
+ return `"${escapeDoubleQuoted(node.value)}"`;
1289
+ case "BooleanLiteral":
1290
+ return String(node.value);
1291
+ case "NullLiteral":
1292
+ return "null";
1293
+ case "Identifier":
1294
+ return node.name;
1295
+ case "BracketedIdentifier":
1296
+ return `["${escapeDoubleQuoted(node.name)}"]`;
1297
+ case "RootPath":
1298
+ return node.path;
1299
+ case "RelativePath":
1300
+ return node.path;
1301
+ case "ContextToken":
1302
+ return node.name;
1303
+ case "BinaryOp":
1304
+ return serializeBinaryOp(node);
1305
+ case "UnaryOp":
1306
+ return `${node.op}${serializeUnaryArgument(node)}`;
1307
+ case "TernaryOp":
1308
+ return `${serializeTernaryCondition(node.condition)} ? ${serializeNode(node.consequent)} : ${serializeNode(node.alternate)}`;
1309
+ case "CallExpression":
1310
+ return `${serializeNode(node.callee)}(${node.arguments.map(serializeNode).join(", ")})`;
1311
+ case "MemberExpression":
1312
+ return `${serializePostfixObject(node.object)}.${node.property}`;
1313
+ case "BracketedMemberExpression":
1314
+ return `${serializePostfixObject(node.object)}["${escapeDoubleQuoted(node.property)}"]`;
1315
+ case "IndexExpression":
1316
+ return `${serializePostfixObject(node.object)}[${serializeNode(node.index)}]`;
1317
+ case "WildcardExpression":
1318
+ return `${serializePostfixObject(node.object)}[*]`;
1319
+ }
1320
+ }
1321
+ function serializeUnaryArgument(node) {
1322
+ const s = serializeNode(node.argument);
1323
+ if (node.argument.type === "BinaryOp" || node.argument.type === "TernaryOp") {
1324
+ return `(${s})`;
1325
+ }
1326
+ return s;
1327
+ }
1328
+ function serializeTernaryCondition(node) {
1329
+ const s = serializeNode(node);
1330
+ if (node.type === "TernaryOp") {
1331
+ return `(${s})`;
1332
+ }
1333
+ return s;
1334
+ }
1335
+ function serializePostfixObject(node) {
1336
+ const s = serializeNode(node);
1337
+ if (node.type === "BinaryOp" || node.type === "TernaryOp" || node.type === "UnaryOp") {
1338
+ return `(${s})`;
1339
+ }
1340
+ return s;
1341
+ }
1342
+ function serializeAst(ast) {
1343
+ return serializeNode(ast);
1344
+ }
1345
+
1346
+ // src/ohm/core/replace-dependencies.ts
1347
+ function collectDependencyPath(node) {
1348
+ switch (node.type) {
1349
+ case "Identifier":
1350
+ return node.name;
1351
+ case "BracketedIdentifier":
1352
+ return `["${node.name}"]`;
1353
+ case "RootPath":
1354
+ return node.path;
1355
+ case "RelativePath":
1356
+ return node.path;
1357
+ case "MemberExpression": {
1358
+ const objPath = collectDependencyPath(node.object);
1359
+ if (objPath === null) {
1360
+ return null;
1361
+ }
1362
+ return `${objPath}.${node.property}`;
1363
+ }
1364
+ case "BracketedMemberExpression": {
1365
+ const objPath = collectDependencyPath(node.object);
1366
+ if (objPath === null) {
1367
+ return null;
1368
+ }
1369
+ const quote = '"';
1370
+ return `${objPath}[${quote}${node.property}${quote}]`;
1371
+ }
1372
+ case "IndexExpression": {
1373
+ const objPath = collectDependencyPath(node.object);
1374
+ if (objPath === null) {
1375
+ return null;
1376
+ }
1377
+ const numericIndex = getNumericIndex(node.index);
1378
+ if (numericIndex !== null) {
1379
+ return `${objPath}[${numericIndex}]`;
1380
+ }
1381
+ return null;
1382
+ }
1383
+ case "WildcardExpression": {
1384
+ const objPath = collectDependencyPath(node.object);
1385
+ if (objPath === null) {
1386
+ return null;
1387
+ }
1388
+ return `${objPath}[*]`;
1389
+ }
1390
+ default:
1391
+ return null;
1392
+ }
1393
+ }
1394
+ function getNumericIndex(node) {
1395
+ if (node.type === "NumberLiteral") {
1396
+ return node.value;
1397
+ }
1398
+ if (node.type === "UnaryOp" && node.op === "-" && node.argument.type === "NumberLiteral") {
1399
+ return -node.argument.value;
1400
+ }
1401
+ return null;
1402
+ }
1403
+ function findMatch(node, replacements) {
1404
+ const fullPath = collectDependencyPath(node);
1405
+ if (fullPath !== null) {
1406
+ const value = replacements[fullPath];
1407
+ if (value !== void 0) {
1408
+ return value;
1409
+ }
1410
+ }
1411
+ return null;
1412
+ }
1413
+ function pathToAst(path) {
1414
+ const { ast } = parseFormula(path);
1415
+ return ast;
1416
+ }
1417
+ function replaceNode(node, replacements) {
1418
+ const replacement = findMatch(node, replacements);
1419
+ if (replacement !== null) {
1420
+ return pathToAst(replacement);
1421
+ }
1422
+ switch (node.type) {
1423
+ case "NumberLiteral":
1424
+ case "StringLiteral":
1425
+ case "BooleanLiteral":
1426
+ case "NullLiteral":
1427
+ case "Identifier":
1428
+ case "BracketedIdentifier":
1429
+ case "RootPath":
1430
+ case "RelativePath":
1431
+ case "ContextToken":
1432
+ return node;
1433
+ case "BinaryOp":
1434
+ return {
1435
+ ...node,
1436
+ left: replaceNode(node.left, replacements),
1437
+ right: replaceNode(node.right, replacements)
1438
+ };
1439
+ case "UnaryOp":
1440
+ return {
1441
+ ...node,
1442
+ argument: replaceNode(node.argument, replacements)
1443
+ };
1444
+ case "TernaryOp":
1445
+ return {
1446
+ ...node,
1447
+ condition: replaceNode(node.condition, replacements),
1448
+ consequent: replaceNode(node.consequent, replacements),
1449
+ alternate: replaceNode(node.alternate, replacements)
1450
+ };
1451
+ case "CallExpression": {
1452
+ const isSimpleFunctionCall = node.callee.type === "Identifier";
1453
+ return {
1454
+ ...node,
1455
+ callee: isSimpleFunctionCall ? node.callee : replaceNode(node.callee, replacements),
1456
+ arguments: node.arguments.map((arg) => replaceNode(arg, replacements))
1457
+ };
1458
+ }
1459
+ case "MemberExpression":
1460
+ case "BracketedMemberExpression":
1461
+ case "WildcardExpression":
1462
+ return {
1463
+ ...node,
1464
+ object: replaceNode(node.object, replacements)
1465
+ };
1466
+ case "IndexExpression":
1467
+ return {
1468
+ ...node,
1469
+ object: replaceNode(node.object, replacements),
1470
+ index: replaceNode(node.index, replacements)
1471
+ };
1472
+ }
1473
+ }
1474
+ function replaceDependencies(ast, replacements) {
1475
+ return replaceNode(ast, replacements);
1476
+ }
1477
+
1164
1478
  // src/parse-formula.ts
1165
1479
  function parseExpression(expression) {
1166
1480
  const result = parseFormula(expression);
@@ -1190,7 +1504,9 @@ exports.evaluateWithContext = evaluateWithContext;
1190
1504
  exports.inferFormulaType = inferFormulaType;
1191
1505
  exports.parseExpression = parseExpression;
1192
1506
  exports.parseFormula = parseFormula;
1507
+ exports.replaceDependencies = replaceDependencies;
1508
+ exports.serializeAst = serializeAst;
1193
1509
  exports.validateFormulaSyntax = validateFormulaSyntax;
1194
1510
  exports.validateSyntax = validateSyntax;
1195
- //# sourceMappingURL=chunk-QS3FBYM5.cjs.map
1196
- //# sourceMappingURL=chunk-QS3FBYM5.cjs.map
1511
+ //# sourceMappingURL=chunk-4FIOGFOL.cjs.map
1512
+ //# sourceMappingURL=chunk-4FIOGFOL.cjs.map