@coldsmirk/abacus-core 0.5.0 → 0.7.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/dist/index.js CHANGED
@@ -358,8 +358,9 @@ function isZenConsistentNumber(value) {
358
358
  return context !== null && literal.digits === context.digits && literal.exponent === context.exponent;
359
359
  }
360
360
  const SUBJECT_PATTERN = /^[A-Z_$][\w$]*(?:\.[A-Z_$][\w$]*|\[\d+\])*$/i;
361
+ const SUBJECT_IDENTIFIER_PATTERN = /[A-Z_$][\w$]*/gi;
361
362
  const SUBJECT_INDEX_PATTERN = /\[(?<index>\d+)\]/g;
362
- const ZEN_RESERVED_WORDS = new Set([
363
+ const ZEN_ROOT_RESERVED_WORDS = new Set([
363
364
  "and",
364
365
  "or",
365
366
  "not",
@@ -368,8 +369,11 @@ const ZEN_RESERVED_WORDS = new Set([
368
369
  "false",
369
370
  "null"
370
371
  ]);
372
+ const ZEN_MEMBER_RESERVED_WORDS = new Set(["true", "false"]);
371
373
  function isIdentifierPath(subject) {
372
- if (!SUBJECT_PATTERN.test(subject) || (subject.match(/[A-Z_$][\w$]*/gi) ?? []).some((segment) => ZEN_RESERVED_WORDS.has(segment))) return false;
374
+ if (!SUBJECT_PATTERN.test(subject)) return false;
375
+ const [root, ...members] = subject.match(SUBJECT_IDENTIFIER_PATTERN) ?? [];
376
+ if (root === void 0 || ZEN_ROOT_RESERVED_WORDS.has(root) || members.some((member) => ZEN_MEMBER_RESERVED_WORDS.has(member))) return false;
373
377
  for (const match of subject.matchAll(SUBJECT_INDEX_PATTERN)) if (match.groups?.index === void 0 || !isZenUnsignedIntegerText(match.groups.index)) return false;
374
378
  return true;
375
379
  }
@@ -394,14 +398,17 @@ function toZenLiteral(value) {
394
398
  function encodeZenString(value) {
395
399
  const hasSingle = value.includes("'");
396
400
  const hasDouble = value.includes("\"");
397
- if (hasSingle && hasDouble) throw new ExpressionError("String contains both single and double quotes and has no ZEN literal representation");
398
- return hasSingle ? `"${value}"` : `'${value}'`;
401
+ const hasBacktick = value.includes("`");
402
+ if (!hasSingle) return `'${value}'`;
403
+ if (!hasDouble) return `"${value}"`;
404
+ if (!hasBacktick) return `\`${value}\``;
405
+ throw new ExpressionError("String contains every ZEN raw-string delimiter and has no literal representation");
399
406
  }
400
407
  function toArrayLiteral(value) {
401
408
  return isArray(value) ? toZenLiteral(value) : `[${toZenLiteral(value)}]`;
402
409
  }
403
410
  function zenIsEmpty(subject) {
404
- return `(${subject} == null or (type(${subject}) == 'string' and len(trim(${subject})) == 0) or (type(${subject}) == 'array' and len(${subject}) == 0))`;
411
+ return `(${subject} == null or (type(${subject}) == 'string' and len(trim(${subject})) == 0) or (type(${subject}) == 'array' and len(${subject}) == 0) or (type(${subject}) == 'object' and len(keys(${subject})) == 0))`;
405
412
  }
406
413
  function compileFieldCondition(subject, operator, value) {
407
414
  switch (operator) {
@@ -969,7 +976,7 @@ function tokenize(input) {
969
976
  index += 1;
970
977
  continue;
971
978
  }
972
- if (char === "'" || char === "\"") {
979
+ if (char === "'" || char === "\"" || char === "`") {
973
980
  const end = input.indexOf(char, index + 1);
974
981
  if (end === -1) return null;
975
982
  tokens.push({
@@ -1152,33 +1159,45 @@ const zhCNMessages = {
1152
1159
  expectedType: (expectedType, actualType) => `期望 \`${expectedType}\`,实际为 \`${actualType}\`。`
1153
1160
  };
1154
1161
  const localeRegistry = new Map([["en-US", enMessages], ["zh-CN", zhCNMessages]]);
1162
+ let activeBaseMessages = enMessages;
1155
1163
  let activeMessages = enMessages;
1164
+ const messageListeners = /* @__PURE__ */ new Set();
1156
1165
  function registerExpressionLocale(locale, messages) {
1157
1166
  localeRegistry.set(locale, messages);
1158
1167
  }
1159
1168
  function configureExpressionMessages({ locale, messages }) {
1160
- const base = locale === void 0 ? activeMessages : localeRegistry.get(locale) ?? activeMessages;
1161
- activeMessages = messages === void 0 ? base : {
1169
+ const base = locale === void 0 ? activeBaseMessages : localeRegistry.get(locale) ?? activeBaseMessages;
1170
+ const nextMessages = messages === void 0 ? base : {
1162
1171
  ...base,
1163
1172
  ...messages
1164
1173
  };
1174
+ activeBaseMessages = base;
1175
+ if (nextMessages === activeMessages) return;
1176
+ activeMessages = nextMessages;
1177
+ for (const listener of messageListeners) listener(activeMessages);
1165
1178
  }
1166
1179
  function getExpressionMessages() {
1167
1180
  return activeMessages;
1168
1181
  }
1182
+ function subscribeExpressionMessages(listener) {
1183
+ messageListeners.add(listener);
1184
+ return () => {
1185
+ messageListeners.delete(listener);
1186
+ };
1187
+ }
1169
1188
  function parseOffset(text) {
1170
1189
  const trimmed = text?.trim();
1171
1190
  return trimmed ? Number(trimmed) : NaN;
1172
1191
  }
1173
1192
  function extractPosition(message) {
1174
- const segments = message.split(" at ");
1175
- const last = segments.length <= 1 ? void 0 : segments.at(-1);
1176
- if (last === void 0) return null;
1177
- const [left, right] = last.replace("(", "").replace(")", "").split(", ");
1178
- const from = parseOffset(left);
1179
- if (Number.isNaN(from)) return null;
1180
- const to = parseOffset(right);
1181
- return [from, Number.isNaN(to) ? from : to];
1193
+ const positionPattern = / at (?:\(\s*(?<rangeFrom>\d+)\s*,\s*(?<rangeTo>\d+)\s*\)|(?<point>\d+))(?=\s*(?:;|$))/g;
1194
+ let position = null;
1195
+ for (const match of message.matchAll(positionPattern)) {
1196
+ const from = parseOffset(match.groups?.rangeFrom ?? match.groups?.point);
1197
+ const to = parseOffset(match.groups?.rangeTo);
1198
+ position = [from, Number.isNaN(to) ? from : to];
1199
+ }
1200
+ return position;
1182
1201
  }
1183
1202
  function normalizeDiagnostic(raw, source) {
1184
1203
  if (raw === null || raw === void 0) return null;
@@ -1298,4 +1317,293 @@ async function getTemplateDiagnostics(source) {
1298
1317
  await loadEngine();
1299
1318
  return getTemplateDiagnosticsSync(source);
1300
1319
  }
1301
- export { CONDITION_OPERATORS, CONDITION_TREE_OPERATORS, ExpressionError, ExpressionNotReadyError, MAX_CONDITION_TREE_DEPTH, analyzeTemplate, analyzeTemplateSync, analyzeTypes, analyzeTypesSync, compileBranch, compileCondition, compileConditionTree, compileGroup, conditionOperatorArity, configureEngine, configureExpressionMessages, emptyConditionGroup, enMessages, ensureConditionNodeIds, evaluate, evaluateSync, evaluateUnary, evaluateUnarySync, getCompletionItems, getCompletionItemsSync, getDiagnostics, getDiagnosticsSync, getEngineError, getEngineSync, getExpressionMessages, getTemplateDiagnostics, getTemplateDiagnosticsSync, isEngineReady, isZenRepresentableNumber, liftConditionTree, loadEngine, newConditionNodeId, parseTemplateHoles, registerExpressionLocale, resetEngine, satisfiesType, satisfiesTypeSync, selectBranch, selectBranchWith, templateHoleAt, toZenLiteral, zhCNMessages };
1320
+ function fieldType(field) {
1321
+ switch (field.type) {
1322
+ case "string": return "String";
1323
+ case "number": return "Number";
1324
+ case "integer": return "Number";
1325
+ case "boolean": return "Bool";
1326
+ case "object": return { Object: fieldRecord(field.children) };
1327
+ case "array": return { Array: field.items === null ? "Any" : fieldType(field.items) };
1328
+ case "any": return "Any";
1329
+ }
1330
+ }
1331
+ function fieldRecord(fields) {
1332
+ return Object.fromEntries(fields.filter((field) => field.name.trim() !== "" || field.preserveBlankName === true).map((field) => [field.name, fieldType(field)]));
1333
+ }
1334
+ function schemaTreeToExpressionType(tree) {
1335
+ return { Object: fieldRecord(tree.fields) };
1336
+ }
1337
+ function mergeSchemas(a, b) {
1338
+ if (a.type === "object" && b.type === "object") {
1339
+ const left = a.properties ?? {};
1340
+ const right = b.properties ?? {};
1341
+ const entries = [];
1342
+ const keys = new Set([...Object.keys(left), ...Object.keys(right)]);
1343
+ for (const key of keys) {
1344
+ const hasLeft = Object.hasOwn(left, key);
1345
+ const hasRight = Object.hasOwn(right, key);
1346
+ entries.push([key, hasLeft && hasRight ? mergeSchemas(left[key], right[key]) : hasLeft ? left[key] : right[key]]);
1347
+ }
1348
+ const properties = Object.fromEntries(entries);
1349
+ return Object.keys(properties).length > 0 ? {
1350
+ type: "object",
1351
+ properties
1352
+ } : { type: "object" };
1353
+ }
1354
+ if (a.type === "array" && b.type === "array") return a.items !== void 0 && b.items !== void 0 ? {
1355
+ type: "array",
1356
+ items: mergeSchemas(a.items, b.items)
1357
+ } : { type: "array" };
1358
+ return JSON.stringify(a) === JSON.stringify(b) ? a : {};
1359
+ }
1360
+ function inferSchema(sample) {
1361
+ if (sample === null) return {};
1362
+ if (Array.isArray(sample)) {
1363
+ if (sample.length === 0) return { type: "array" };
1364
+ return {
1365
+ type: "array",
1366
+ items: sample.map((element) => inferSchema(element)).reduce((left, right) => mergeSchemas(left, right))
1367
+ };
1368
+ }
1369
+ if (typeof sample === "string") return { type: "string" };
1370
+ if (typeof sample === "number") return { type: "number" };
1371
+ if (typeof sample === "boolean") return { type: "boolean" };
1372
+ const properties = Object.fromEntries(Object.entries(sample).map(([key, value]) => [key, inferSchema(value)]));
1373
+ return Object.keys(properties).length > 0 ? {
1374
+ type: "object",
1375
+ properties
1376
+ } : { type: "object" };
1377
+ }
1378
+ const SCHEMA_DIALECT_2020_12 = "https://json-schema.org/draft/2020-12/schema";
1379
+ const fieldIdRealm = Math.random().toString(36).slice(2, 7);
1380
+ let fieldIdCounter = 0;
1381
+ function newSchemaTreeField(overrides = {}) {
1382
+ fieldIdCounter += 1;
1383
+ return {
1384
+ id: `sf-${fieldIdRealm}-${fieldIdCounter}`,
1385
+ name: "",
1386
+ type: "string",
1387
+ required: false,
1388
+ description: "",
1389
+ children: [],
1390
+ items: null,
1391
+ ...overrides
1392
+ };
1393
+ }
1394
+ const SCALAR_TYPES = new Set([
1395
+ "string",
1396
+ "number",
1397
+ "integer",
1398
+ "boolean"
1399
+ ]);
1400
+ const ROOT_KEYS = new Set([
1401
+ "$schema",
1402
+ "type",
1403
+ "properties",
1404
+ "required",
1405
+ "description"
1406
+ ]);
1407
+ const SUBSCHEMA_KEYS = new Set([
1408
+ "type",
1409
+ "properties",
1410
+ "required",
1411
+ "items",
1412
+ "description"
1413
+ ]);
1414
+ var Unsupported = class extends Error {
1415
+ issue;
1416
+ constructor(issue) {
1417
+ super(issue.code);
1418
+ this.issue = issue;
1419
+ }
1420
+ };
1421
+ function asPlainObject(value) {
1422
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
1423
+ }
1424
+ function checkKeys(schema, allowed) {
1425
+ for (const key of Object.keys(schema)) if (!allowed.has(key)) throw new Unsupported({
1426
+ code: "unsupported-keyword",
1427
+ keyword: key
1428
+ });
1429
+ }
1430
+ function parseObjectFields(schema) {
1431
+ const properties = schema.properties === void 0 ? null : asPlainObject(schema.properties);
1432
+ if (schema.properties !== void 0 && properties === null) throw new Unsupported({ code: "invalid-properties" });
1433
+ const fields = [];
1434
+ const entries = Object.entries(properties ?? {});
1435
+ for (const [name, subschema] of entries) fields.push({
1436
+ ...parseSubschema(subschema, name),
1437
+ name,
1438
+ ...name.trim() === "" && { preserveBlankName: true }
1439
+ });
1440
+ if (schema.required !== void 0) {
1441
+ if (!Array.isArray(schema.required)) throw new Unsupported({ code: "invalid-required" });
1442
+ for (const entry of schema.required) {
1443
+ if (typeof entry !== "string") throw new Unsupported({ code: "invalid-required" });
1444
+ const field = fields.find((candidate) => candidate.name === entry);
1445
+ if (field === void 0) throw new Unsupported({
1446
+ code: "unknown-required-field",
1447
+ field: entry
1448
+ });
1449
+ field.required = true;
1450
+ }
1451
+ }
1452
+ return fields;
1453
+ }
1454
+ function parseDescription(schema) {
1455
+ if (schema.description === void 0) return "";
1456
+ if (typeof schema.description !== "string") throw new Unsupported({ code: "invalid-description" });
1457
+ return schema.description;
1458
+ }
1459
+ function ensureAbsent(schema, key, holder) {
1460
+ if (schema[key] !== void 0) throw new Unsupported({
1461
+ code: "misplaced-keyword",
1462
+ keyword: key,
1463
+ holder
1464
+ });
1465
+ }
1466
+ function parseSubschema(value, name) {
1467
+ const schema = asPlainObject(value);
1468
+ if (schema === null) throw new Unsupported({
1469
+ code: "invalid-field-definition",
1470
+ field: name
1471
+ });
1472
+ checkKeys(schema, SUBSCHEMA_KEYS);
1473
+ const { type } = schema;
1474
+ if (type !== void 0 && typeof type !== "string") throw new Unsupported({
1475
+ code: "invalid-field-type",
1476
+ field: name
1477
+ });
1478
+ const description = parseDescription(schema);
1479
+ if (type === "object" || type === void 0 && (schema.properties !== void 0 || schema.required !== void 0)) {
1480
+ ensureAbsent(schema, "items", "array");
1481
+ return newSchemaTreeField({
1482
+ type: "object",
1483
+ description,
1484
+ children: parseObjectFields(schema),
1485
+ explicitType: type === "object"
1486
+ });
1487
+ }
1488
+ if (type === "array" || type === void 0 && schema.items !== void 0) {
1489
+ ensureAbsent(schema, "properties", "object");
1490
+ ensureAbsent(schema, "required", "object");
1491
+ return newSchemaTreeField({
1492
+ type: "array",
1493
+ description,
1494
+ items: schema.items === void 0 ? null : parseSubschema(schema.items, name),
1495
+ explicitType: type === "array"
1496
+ });
1497
+ }
1498
+ if (type === void 0) {
1499
+ ensureAbsent(schema, "items", "array");
1500
+ return newSchemaTreeField({
1501
+ type: "any",
1502
+ description
1503
+ });
1504
+ }
1505
+ if (!SCALAR_TYPES.has(type)) throw new Unsupported({
1506
+ code: "unsupported-field-type",
1507
+ field: name,
1508
+ type
1509
+ });
1510
+ ensureAbsent(schema, "properties", "object");
1511
+ ensureAbsent(schema, "required", "object");
1512
+ ensureAbsent(schema, "items", "array");
1513
+ return newSchemaTreeField({
1514
+ type,
1515
+ description
1516
+ });
1517
+ }
1518
+ function parseSchemaTree(text) {
1519
+ if (text.trim() === "") return {
1520
+ ok: true,
1521
+ tree: {
1522
+ fields: [],
1523
+ dialect: false,
1524
+ description: ""
1525
+ }
1526
+ };
1527
+ let parsed;
1528
+ try {
1529
+ parsed = JSON.parse(text);
1530
+ } catch {
1531
+ return {
1532
+ ok: false,
1533
+ issue: { code: "invalid-json" }
1534
+ };
1535
+ }
1536
+ try {
1537
+ const root = asPlainObject(parsed);
1538
+ if (root === null) throw new Unsupported({ code: "root-not-object" });
1539
+ checkKeys(root, ROOT_KEYS);
1540
+ const dialect = root.$schema !== void 0;
1541
+ if (dialect && root.$schema !== "https://json-schema.org/draft/2020-12/schema") throw new Unsupported({ code: "unsupported-dialect" });
1542
+ if (root.type !== void 0 && root.type !== "object") throw new Unsupported({ code: "root-not-object" });
1543
+ return {
1544
+ ok: true,
1545
+ tree: {
1546
+ fields: parseObjectFields(root),
1547
+ dialect,
1548
+ description: parseDescription(root),
1549
+ explicitType: root.type === "object"
1550
+ }
1551
+ };
1552
+ } catch (error) {
1553
+ if (error instanceof Unsupported) return {
1554
+ ok: false,
1555
+ issue: error.issue
1556
+ };
1557
+ throw error;
1558
+ }
1559
+ }
1560
+ function serializeObjectBody(fields) {
1561
+ const named = fields.filter((field) => field.name.trim() !== "" || field.preserveBlankName === true);
1562
+ const finalByName = /* @__PURE__ */ new Map();
1563
+ const finalFields = [];
1564
+ const body = {};
1565
+ for (const field of named) finalByName.set(field.name, field);
1566
+ finalByName.forEach((field) => {
1567
+ finalFields.push(field);
1568
+ });
1569
+ if (finalFields.length > 0) body.properties = Object.fromEntries(finalFields.map((field) => [field.name, serializeField(field)]));
1570
+ const required = finalFields.filter((field) => field.required).map((field) => field.name);
1571
+ if (required.length > 0) body.required = required;
1572
+ return body;
1573
+ }
1574
+ function serializeField(field) {
1575
+ const description = field.description === "" ? {} : { description: field.description };
1576
+ switch (field.type) {
1577
+ case "any": return { ...description };
1578
+ case "object": return {
1579
+ ...field.explicitType !== false && { type: "object" },
1580
+ ...description,
1581
+ ...serializeObjectBody(field.children)
1582
+ };
1583
+ case "array": {
1584
+ const type = field.explicitType === false ? {} : { type: "array" };
1585
+ return field.items === null || field.items.type === "any" && field.items.description === "" ? {
1586
+ ...type,
1587
+ ...description
1588
+ } : {
1589
+ ...type,
1590
+ ...description,
1591
+ items: serializeField(field.items)
1592
+ };
1593
+ }
1594
+ default: return {
1595
+ type: field.type,
1596
+ ...description
1597
+ };
1598
+ }
1599
+ }
1600
+ function serializeSchemaTree(tree) {
1601
+ const root = {
1602
+ ...tree.dialect && { $schema: "https://json-schema.org/draft/2020-12/schema" },
1603
+ ...tree.explicitType !== false && { type: "object" },
1604
+ ...tree.description !== "" && { description: tree.description },
1605
+ ...serializeObjectBody(tree.fields)
1606
+ };
1607
+ return `${JSON.stringify(root, null, 2)}\n`;
1608
+ }
1609
+ export { CONDITION_OPERATORS, CONDITION_TREE_OPERATORS, ExpressionError, ExpressionNotReadyError, MAX_CONDITION_TREE_DEPTH, SCHEMA_DIALECT_2020_12, analyzeTemplate, analyzeTemplateSync, analyzeTypes, analyzeTypesSync, compileBranch, compileCondition, compileConditionTree, compileGroup, conditionOperatorArity, configureEngine, configureExpressionMessages, emptyConditionGroup, enMessages, ensureConditionNodeIds, evaluate, evaluateSync, evaluateUnary, evaluateUnarySync, getCompletionItems, getCompletionItemsSync, getDiagnostics, getDiagnosticsSync, getEngineError, getEngineSync, getExpressionMessages, getTemplateDiagnostics, getTemplateDiagnosticsSync, inferSchema, isEngineReady, isZenRepresentableNumber, liftConditionTree, loadEngine, newConditionNodeId, newSchemaTreeField, parseSchemaTree, parseTemplateHoles, registerExpressionLocale, resetEngine, satisfiesType, satisfiesTypeSync, schemaTreeToExpressionType, selectBranch, selectBranchWith, serializeSchemaTree, subscribeExpressionMessages, templateHoleAt, toZenLiteral, zhCNMessages };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coldsmirk/abacus-core",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "Framework-agnostic ZEN expression engine: compile, evaluate, and type-analyze expressions over the GoRules ZEN WASM engine.",
5
5
  "keywords": [
6
6
  "zen",