@lokascript/framework 2.1.0 → 2.2.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.
Files changed (52) hide show
  1. package/dist/api/index.js +111 -24
  2. package/dist/api/index.js.map +1 -1
  3. package/dist/core/index.js +133 -0
  4. package/dist/core/index.js.map +1 -1
  5. package/dist/core/pattern-matching/index.js.map +1 -1
  6. package/dist/core/tokenization/index.js +133 -0
  7. package/dist/core/tokenization/index.js.map +1 -1
  8. package/dist/core/tokenization/morphology/base-normalizer.d.ts +94 -0
  9. package/dist/core/tokenization/morphology/base-normalizer.d.ts.map +1 -0
  10. package/dist/core/tokenization/morphology/index.d.ts +3 -1
  11. package/dist/core/tokenization/morphology/index.d.ts.map +1 -1
  12. package/dist/core/types.d.ts +3 -2
  13. package/dist/core/types.d.ts.map +1 -1
  14. package/dist/core/types.js.map +1 -1
  15. package/dist/generation/index.js.map +1 -1
  16. package/dist/index.cjs +564 -46
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.ts +2 -0
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +556 -46
  21. package/dist/index.js.map +1 -1
  22. package/dist/ir/explicit-parser.d.ts +62 -7
  23. package/dist/ir/explicit-parser.d.ts.map +1 -1
  24. package/dist/ir/explicit-renderer.d.ts +18 -1
  25. package/dist/ir/explicit-renderer.d.ts.map +1 -1
  26. package/dist/ir/index.d.ts +4 -2
  27. package/dist/ir/index.d.ts.map +1 -1
  28. package/dist/ir/index.js +423 -46
  29. package/dist/ir/index.js.map +1 -1
  30. package/dist/ir/protocol-json.d.ts.map +1 -1
  31. package/dist/ir/to-runtime-ast.d.ts +53 -0
  32. package/dist/ir/to-runtime-ast.d.ts.map +1 -0
  33. package/dist/ir/types.d.ts +14 -4
  34. package/dist/ir/types.d.ts.map +1 -1
  35. package/dist/parsing/index.js.map +1 -1
  36. package/dist/testing/index.js +8671 -8261
  37. package/dist/testing/index.js.map +1 -1
  38. package/package.json +1 -1
  39. package/src/core/tokenization/morphology/base-normalizer.ts +249 -0
  40. package/src/core/tokenization/morphology/index.ts +3 -1
  41. package/src/core/types.ts +4 -2
  42. package/src/index.ts +7 -0
  43. package/src/ir/conformance.test.ts +120 -0
  44. package/src/ir/explicit-parser.test.ts +307 -1
  45. package/src/ir/explicit-parser.ts +403 -29
  46. package/src/ir/explicit-renderer.test.ts +87 -2
  47. package/src/ir/explicit-renderer.ts +57 -5
  48. package/src/ir/index.ts +13 -2
  49. package/src/ir/protocol-json.test.ts +4 -2
  50. package/src/ir/protocol-json.ts +30 -24
  51. package/src/ir/to-runtime-ast.ts +203 -0
  52. package/src/ir/types.ts +14 -4
package/dist/ir/index.js CHANGED
@@ -162,6 +162,8 @@ function isNestedCommand(value) {
162
162
  }
163
163
  function parseExplicit(input, options = {}) {
164
164
  const trimmed = input.trim();
165
+ const collect = options.collectDiagnostics === true;
166
+ const diagnostics = [];
165
167
  if (!trimmed.startsWith("[") || !trimmed.endsWith("]")) {
166
168
  throw new Error("Explicit syntax must be wrapped in brackets: [command role:value ...]");
167
169
  }
@@ -184,10 +186,30 @@ function parseExplicit(input, options = {}) {
184
186
  const enabled = token.startsWith("+");
185
187
  const flagName = token.slice(1);
186
188
  if (!flagName) {
189
+ if (collect) {
190
+ diagnostics.push({
191
+ severity: "error",
192
+ code: "EMPTY_FLAG",
193
+ message: `Empty flag name: "${token}"`,
194
+ source: "schema"
195
+ });
196
+ continue;
197
+ }
187
198
  throw new Error(`Empty flag name: "${token}"`);
188
199
  }
189
200
  if (validRoleNames && !validRoleNames.has(flagName)) {
190
201
  const roleList = [...validRoleNames].join(", ");
202
+ if (collect) {
203
+ diagnostics.push({
204
+ severity: "error",
205
+ code: "UNKNOWN_FLAG",
206
+ message: `Unknown flag "${flagName}" for command "${command}"`,
207
+ source: "schema",
208
+ suggestions: [...validRoleNames].map((r) => `+${r}`)
209
+ });
210
+ roles.set(flagName, createFlag(flagName, enabled));
211
+ continue;
212
+ }
191
213
  throw new Error(
192
214
  `Unknown flag "${flagName}" for command "${command}". Valid roles: ${roleList}`
193
215
  );
@@ -197,14 +219,33 @@ function parseExplicit(input, options = {}) {
197
219
  }
198
220
  const colonIndex = token.indexOf(":");
199
221
  if (colonIndex === -1) {
222
+ if (collect) {
223
+ diagnostics.push({
224
+ severity: "error",
225
+ code: "INVALID_ROLE_FORMAT",
226
+ message: `Invalid role format: "${token}". Expected role:value or +flag`,
227
+ source: "schema"
228
+ });
229
+ continue;
230
+ }
200
231
  throw new Error(`Invalid role format: "${token}". Expected role:value or +flag`);
201
232
  }
202
233
  const roleName = token.slice(0, colonIndex);
203
234
  if (validRoleNames && !STRUCTURAL_ROLES.has(roleName) && !validRoleNames.has(roleName)) {
204
235
  const roleList = [...validRoleNames].join(", ");
205
- throw new Error(
206
- `Unknown role "${roleName}" for command "${command}". Valid roles: ${roleList}`
207
- );
236
+ if (collect) {
237
+ diagnostics.push({
238
+ severity: "error",
239
+ code: "UNKNOWN_ROLE",
240
+ message: `Unknown role "${roleName}" for command "${command}"`,
241
+ source: "schema",
242
+ suggestions: [...validRoleNames]
243
+ });
244
+ } else {
245
+ throw new Error(
246
+ `Unknown role "${roleName}" for command "${command}". Valid roles: ${roleList}`
247
+ );
248
+ }
208
249
  }
209
250
  const role = roleName;
210
251
  const valueStr = token.slice(colonIndex + 1);
@@ -220,37 +261,67 @@ function parseExplicit(input, options = {}) {
220
261
  if (schema && command !== "on") {
221
262
  for (const roleSpec of schema.roles) {
222
263
  if (roleSpec.required && !roles.has(roleSpec.role) && !roleSpec.default) {
223
- throw new Error(
224
- `Missing required role "${roleSpec.role}" for command "${command}": ${roleSpec.description}`
225
- );
264
+ if (collect) {
265
+ diagnostics.push({
266
+ severity: "error",
267
+ code: "MISSING_REQUIRED_ROLE",
268
+ message: `Missing required role "${roleSpec.role}" for command "${command}": ${roleSpec.description}`,
269
+ source: "schema"
270
+ });
271
+ } else {
272
+ throw new Error(
273
+ `Missing required role "${roleSpec.role}" for command "${command}": ${roleSpec.description}`
274
+ );
275
+ }
226
276
  }
227
277
  }
228
278
  }
279
+ const attachDiagnostics = (node) => {
280
+ if (diagnostics.length === 0) return node;
281
+ return { ...node, diagnostics };
282
+ };
229
283
  switch (command) {
230
284
  case "on": {
231
285
  const eventValue = roles.get("event");
232
286
  if (!eventValue) {
287
+ if (collect) {
288
+ diagnostics.push({
289
+ severity: "error",
290
+ code: "MISSING_EVENT_ROLE",
291
+ message: "Event handler requires event role: [on event:click ...]",
292
+ source: "schema"
293
+ });
294
+ return attachDiagnostics(
295
+ createEventHandlerNode("on", roles, [], {
296
+ sourceLanguage: "explicit"
297
+ })
298
+ );
299
+ }
233
300
  throw new Error("Event handler requires event role: [on event:click ...]");
234
301
  }
235
302
  const body = extractStructuralBody(roles, "body", options);
236
303
  roles.delete("body");
237
- return createEventHandlerNode("on", roles, body, {
238
- sourceLanguage: "explicit"
239
- });
304
+ return attachDiagnostics(
305
+ createEventHandlerNode("on", roles, body, {
306
+ sourceLanguage: "explicit"
307
+ })
308
+ );
240
309
  }
241
310
  case "if": {
242
311
  const thenBranch = extractStructuralBody(roles, "then", options);
243
312
  const elseBranch = extractStructuralBody(roles, "else", options);
244
313
  roles.delete("then");
245
314
  roles.delete("else");
246
- return createConditionalNode(
247
- command,
248
- roles,
249
- thenBranch,
250
- elseBranch.length > 0 ? elseBranch : void 0,
251
- {
252
- sourceLanguage: "explicit"
253
- }
315
+ return attachDiagnostics(
316
+ createConditionalNode(
317
+ command,
318
+ roles,
319
+ thenBranch,
320
+ elseBranch.length > 0 ? elseBranch : void 0,
321
+ {
322
+ sourceLanguage: "explicit"
323
+ }
324
+ )
254
325
  );
255
326
  }
256
327
  case "repeat": {
@@ -276,20 +347,182 @@ function parseExplicit(input, options = {}) {
276
347
  }
277
348
  const loopVariable = variableValue?.type === "literal" && typeof variableValue.value === "string" ? variableValue.value : variableValue?.type === "expression" ? variableValue.raw : void 0;
278
349
  const indexVariable = indexVariableValue?.type === "literal" && typeof indexVariableValue.value === "string" ? indexVariableValue.value : void 0;
279
- return createLoopNode(command, roles, variant, loopBody, loopVariable, indexVariable, {
280
- sourceLanguage: "explicit"
281
- });
350
+ return attachDiagnostics(
351
+ createLoopNode(command, roles, variant, loopBody, loopVariable, indexVariable, {
352
+ sourceLanguage: "explicit"
353
+ })
354
+ );
282
355
  }
283
356
  default:
284
- return createCommandNode(command, roles, {
285
- sourceLanguage: "explicit"
286
- });
357
+ return attachDiagnostics(
358
+ createCommandNode(command, roles, {
359
+ sourceLanguage: "explicit"
360
+ })
361
+ );
287
362
  }
288
363
  }
289
364
  function isExplicitSyntax(input) {
290
365
  const trimmed = input.trim();
291
366
  return trimmed.startsWith("[") && trimmed.endsWith("]");
292
367
  }
368
+ var CHAIN_KEYWORDS = /* @__PURE__ */ new Set(["then", "and", "async", "sequential"]);
369
+ function splitCompoundSegments(input) {
370
+ const segments = [];
371
+ let chainType = null;
372
+ let current = "";
373
+ let bracketDepth = 0;
374
+ let inString = false;
375
+ let stringChar = "";
376
+ for (let i = 0; i < input.length; i++) {
377
+ const char = input[i];
378
+ if (inString) {
379
+ current += char;
380
+ if (char === stringChar && input[i - 1] !== "\\") {
381
+ inString = false;
382
+ }
383
+ continue;
384
+ }
385
+ if (char === '"' || char === "'") {
386
+ inString = true;
387
+ stringChar = char;
388
+ current += char;
389
+ continue;
390
+ }
391
+ if (char === "[") {
392
+ bracketDepth++;
393
+ current += char;
394
+ continue;
395
+ }
396
+ if (char === "]") {
397
+ bracketDepth--;
398
+ current += char;
399
+ if (bracketDepth === 0) {
400
+ const rest = input.slice(i + 1);
401
+ const pipeMatch = rest.match(/^\s*\|>\s*/);
402
+ if (pipeMatch) {
403
+ segments.push(current.trim());
404
+ current = "";
405
+ chainType ?? (chainType = "pipe");
406
+ i += pipeMatch[0].length;
407
+ continue;
408
+ }
409
+ const keywordMatch = rest.match(/^\s+(then|and|async|sequential)\s+/);
410
+ if (keywordMatch && CHAIN_KEYWORDS.has(keywordMatch[1])) {
411
+ segments.push(current.trim());
412
+ current = "";
413
+ chainType ?? (chainType = keywordMatch[1]);
414
+ i += keywordMatch[0].length;
415
+ continue;
416
+ }
417
+ }
418
+ continue;
419
+ }
420
+ current += char;
421
+ }
422
+ if (current.trim()) {
423
+ segments.push(current.trim());
424
+ }
425
+ return { segments, chainType };
426
+ }
427
+ function parseCompound(input, options) {
428
+ const trimmed = input.trim();
429
+ const { annotations, remainder } = extractAnnotations(trimmed);
430
+ const { segments, chainType } = splitCompoundSegments(remainder);
431
+ let node;
432
+ if (segments.length <= 1) {
433
+ node = parseExplicit(segments[0] || remainder, options);
434
+ } else {
435
+ const statements = segments.map((seg) => parseExplicit(seg, options));
436
+ node = createCompoundNode(statements, chainType ?? "sequential");
437
+ }
438
+ if (annotations.length > 0) {
439
+ return { ...node, annotations };
440
+ }
441
+ return node;
442
+ }
443
+ function extractAnnotations(input) {
444
+ const annotations = [];
445
+ let pos = 0;
446
+ while (pos < input.length) {
447
+ while (pos < input.length && (input[pos] === " " || input[pos] === " ")) {
448
+ pos++;
449
+ }
450
+ if (pos >= input.length || input[pos] !== "@") {
451
+ break;
452
+ }
453
+ pos++;
454
+ const nameStart = pos;
455
+ while (pos < input.length && /[A-Za-z0-9\-_]/.test(input[pos])) {
456
+ pos++;
457
+ }
458
+ const name = input.slice(nameStart, pos);
459
+ if (!name) break;
460
+ let value;
461
+ if (pos < input.length && input[pos] === "(") {
462
+ pos++;
463
+ const valueStart = pos;
464
+ if (pos < input.length && (input[pos] === '"' || input[pos] === "'")) {
465
+ const quote = input[pos];
466
+ pos++;
467
+ while (pos < input.length && input[pos] !== quote) {
468
+ if (input[pos] === "\\") pos++;
469
+ pos++;
470
+ }
471
+ pos++;
472
+ value = input.slice(valueStart + 1, pos - 1);
473
+ } else {
474
+ while (pos < input.length && input[pos] !== ")") {
475
+ pos++;
476
+ }
477
+ value = input.slice(valueStart, pos);
478
+ }
479
+ if (pos < input.length && input[pos] === ")") {
480
+ pos++;
481
+ }
482
+ }
483
+ annotations.push(value !== void 0 ? { name, value } : { name });
484
+ }
485
+ return { annotations, remainder: input.slice(pos).trim() };
486
+ }
487
+ var VERSION_HEADER_RE = /^#!lse\s+(\d+\.\d+(?:\.\d+)?)\s*$/;
488
+ function parseDocument(input, options) {
489
+ const lines = input.split("\n");
490
+ let lseVersion = "1.0";
491
+ const nodes = [];
492
+ let startLine = 0;
493
+ for (let i = 0; i < lines.length; i++) {
494
+ const trimmed = lines[i].trim();
495
+ if (!trimmed) continue;
496
+ const versionMatch = trimmed.match(VERSION_HEADER_RE);
497
+ if (versionMatch) {
498
+ lseVersion = versionMatch[1];
499
+ startLine = i + 1;
500
+ }
501
+ break;
502
+ }
503
+ for (let i = startLine; i < lines.length; i++) {
504
+ const trimmed = lines[i].trim();
505
+ if (!trimmed) continue;
506
+ if (trimmed.startsWith("//") || trimmed.startsWith("#") && !trimmed.startsWith("#!")) {
507
+ continue;
508
+ }
509
+ nodes.push(parseCompound(trimmed, options));
510
+ }
511
+ return { lseVersion, nodes };
512
+ }
513
+ function isCompoundSyntax(input) {
514
+ const { segments } = splitCompoundSegments(input.trim());
515
+ return segments.length > 1;
516
+ }
517
+ function isDocumentSyntax(input) {
518
+ const trimmed = input.trim();
519
+ if (trimmed.includes("\n")) return true;
520
+ if (VERSION_HEADER_RE.test(trimmed)) return true;
521
+ if (trimmed.startsWith("//") || trimmed.startsWith("#") && !trimmed.startsWith("#!")) {
522
+ return true;
523
+ }
524
+ return false;
525
+ }
293
526
  function extractStructuralBody(roles, roleName, options) {
294
527
  const value = roles.get(roleName);
295
528
  if (!value) return [];
@@ -391,11 +624,33 @@ function findMatchingBracket(str, start) {
391
624
 
392
625
  // src/ir/explicit-renderer.ts
393
626
  function renderExplicit(node) {
627
+ let result;
394
628
  if (node.kind === "compound") {
395
629
  const compoundNode = node;
396
630
  const renderedStatements = compoundNode.statements.map((stmt) => renderExplicit(stmt));
397
- return renderedStatements.join(` ${compoundNode.chainType} `);
631
+ result = renderedStatements.join(` ${compoundNode.chainType} `);
632
+ } else {
633
+ result = renderBracketCommand(node);
634
+ }
635
+ if (node.annotations && node.annotations.length > 0) {
636
+ const annParts = node.annotations.map(
637
+ (ann) => ann.value !== void 0 ? `@${ann.name}(${ann.value})` : `@${ann.name}`
638
+ );
639
+ return annParts.join(" ") + " " + result;
640
+ }
641
+ return result;
642
+ }
643
+ function renderDocument(envelope) {
644
+ const lines = [];
645
+ if (envelope.lseVersion && envelope.lseVersion !== "1.0") {
646
+ lines.push(`#!lse ${envelope.lseVersion}`);
398
647
  }
648
+ for (const node of envelope.nodes) {
649
+ lines.push(renderExplicit(node));
650
+ }
651
+ return lines.join("\n");
652
+ }
653
+ function renderBracketCommand(node) {
399
654
  const parts = [node.action];
400
655
  for (const [role, value] of node.roles) {
401
656
  if (value.type === "flag") {
@@ -733,12 +988,16 @@ function toProtocolJSON(node) {
733
988
  );
734
989
  }
735
990
  if (node.diagnostics && node.diagnostics.length > 0) {
736
- result.diagnostics = node.diagnostics.map((d) => ({
737
- level: d.level,
738
- role: d.role,
739
- message: d.message,
740
- code: d.code
741
- }));
991
+ result.diagnostics = node.diagnostics.map((d) => {
992
+ const json = {
993
+ level: d.severity,
994
+ message: d.message
995
+ };
996
+ if (d.code) json.code = d.code;
997
+ if (d.source) json.source = d.source;
998
+ if (d.suggestions && d.suggestions.length > 0) json.suggestions = [...d.suggestions];
999
+ return json;
1000
+ });
742
1001
  }
743
1002
  return result;
744
1003
  }
@@ -786,10 +1045,11 @@ function fromProtocolJSON(json) {
786
1045
  (a) => a.value !== void 0 ? { name: a.name, value: a.value } : { name: a.name }
787
1046
  );
788
1047
  const diagnostics = json.diagnostics?.map((d) => ({
789
- level: d.level,
790
- role: d.role,
1048
+ severity: d.level,
791
1049
  message: d.message,
792
- code: d.code
1050
+ ...d.code ? { code: d.code } : {},
1051
+ ...d.source ? { source: d.source } : {},
1052
+ ...d.suggestions && d.suggestions.length > 0 ? { suggestions: d.suggestions } : {}
793
1053
  }));
794
1054
  if (json.trigger) {
795
1055
  const eventName = json.trigger.event;
@@ -930,7 +1190,7 @@ var VALID_VALUE_TYPES2 = /* @__PURE__ */ new Set([
930
1190
  ]);
931
1191
  var VALID_CHAIN_TYPES = /* @__PURE__ */ new Set(["then", "and", "async", "sequential", "pipe"]);
932
1192
  var VALID_ASYNC_VARIANTS = /* @__PURE__ */ new Set(["all", "race"]);
933
- var VALID_DIAGNOSTIC_LEVELS = /* @__PURE__ */ new Set(["error", "warning"]);
1193
+ var VALID_DIAGNOSTIC_LEVELS = /* @__PURE__ */ new Set(["error", "warning", "info"]);
934
1194
  function validateProtocolJSON(json) {
935
1195
  const diagnostics = [];
936
1196
  if (typeof json !== "object" || json === null) {
@@ -1079,14 +1339,7 @@ function validateProtocolJSON(json) {
1079
1339
  diagnostics.push({
1080
1340
  severity: "error",
1081
1341
  code: "INVALID_DIAGNOSTIC_LEVEL",
1082
- message: `diagnostics[${i}].level must be "error" or "warning"`
1083
- });
1084
- }
1085
- if (typeof d.role !== "string") {
1086
- diagnostics.push({
1087
- severity: "error",
1088
- code: "MISSING_DIAGNOSTIC_ROLE",
1089
- message: `diagnostics[${i}] missing required field: role`
1342
+ message: `diagnostics[${i}].level must be "error", "warning", or "info"`
1090
1343
  });
1091
1344
  }
1092
1345
  if (typeof d.message !== "string") {
@@ -1096,11 +1349,11 @@ function validateProtocolJSON(json) {
1096
1349
  message: `diagnostics[${i}] missing required field: message`
1097
1350
  });
1098
1351
  }
1099
- if (typeof d.code !== "string") {
1352
+ if ("code" in d && typeof d.code !== "string") {
1100
1353
  diagnostics.push({
1101
- severity: "error",
1102
- code: "MISSING_DIAGNOSTIC_CODE",
1103
- message: `diagnostics[${i}] missing required field: code`
1354
+ severity: "warning",
1355
+ code: "INVALID_DIAGNOSTIC_CODE",
1356
+ message: `diagnostics[${i}].code must be a string if present`
1104
1357
  });
1105
1358
  }
1106
1359
  }
@@ -1535,20 +1788,144 @@ function bridgeEventModifiers(mods) {
1535
1788
  ...from !== void 0 ? { from } : {}
1536
1789
  };
1537
1790
  }
1791
+
1792
+ // src/ir/to-runtime-ast.ts
1793
+ function semanticNodeToRuntimeAST(node) {
1794
+ switch (node.kind) {
1795
+ case "event-handler":
1796
+ return convertEventHandler(node);
1797
+ case "conditional":
1798
+ return convertConditional(node);
1799
+ case "loop":
1800
+ return convertLoop(node);
1801
+ case "compound": {
1802
+ const compound = node;
1803
+ const commands = (compound.statements ?? []).map(semanticNodeToRuntimeAST);
1804
+ return {
1805
+ type: "CommandSequence",
1806
+ commands
1807
+ };
1808
+ }
1809
+ case "command":
1810
+ default:
1811
+ return convertCommand2(node);
1812
+ }
1813
+ }
1814
+ function convertEventHandler(eh) {
1815
+ const eventValue = eh.roles.get("event");
1816
+ const eventName = eventValue && "value" in eventValue ? String(eventValue.value) : "click";
1817
+ const modifiers = {};
1818
+ if (eh.eventModifiers) {
1819
+ if (eh.eventModifiers.once) modifiers.once = true;
1820
+ if (eh.eventModifiers.debounce) modifiers.debounce = eh.eventModifiers.debounce;
1821
+ if (eh.eventModifiers.throttle) modifiers.throttle = eh.eventModifiers.throttle;
1822
+ if (eh.eventModifiers.queue) modifiers.queue = eh.eventModifiers.queue;
1823
+ }
1824
+ return {
1825
+ type: "event",
1826
+ event: eventName,
1827
+ modifiers,
1828
+ body: (eh.body ?? []).map(semanticNodeToRuntimeAST)
1829
+ };
1830
+ }
1831
+ function convertCommand2(node) {
1832
+ const roles = {};
1833
+ const args = [];
1834
+ for (const [roleName, value] of node.roles) {
1835
+ const astValue = semanticValueToAST(value);
1836
+ roles[roleName] = astValue;
1837
+ args.push(astValue);
1838
+ }
1839
+ return {
1840
+ type: "command",
1841
+ name: node.action,
1842
+ args,
1843
+ roles
1844
+ };
1845
+ }
1846
+ function convertConditional(node) {
1847
+ const conditionValue = node.roles.get("condition");
1848
+ return {
1849
+ type: "command",
1850
+ name: "if",
1851
+ args: conditionValue ? [semanticValueToAST(conditionValue)] : [],
1852
+ condition: conditionValue ? semanticValueToAST(conditionValue) : void 0,
1853
+ thenBranch: (node.thenBranch ?? []).map(semanticNodeToRuntimeAST),
1854
+ elseBranch: (node.elseBranch ?? []).map(semanticNodeToRuntimeAST)
1855
+ };
1856
+ }
1857
+ function convertLoop(node) {
1858
+ const countValue = node.roles.get("patient") || node.roles.get("count");
1859
+ return {
1860
+ type: "command",
1861
+ name: "repeat",
1862
+ args: countValue ? [semanticValueToAST(countValue)] : [],
1863
+ loopVariant: node.loopVariant ?? "forever",
1864
+ body: (node.body ?? []).map(semanticNodeToRuntimeAST),
1865
+ loopVariable: node.loopVariable,
1866
+ indexVariable: node.indexVariable
1867
+ };
1868
+ }
1869
+ function semanticValueToAST(value) {
1870
+ switch (value.type) {
1871
+ case "selector":
1872
+ return { type: "selector", value: value.value };
1873
+ case "reference":
1874
+ return { type: "identifier", value: value.value };
1875
+ case "literal": {
1876
+ const lit = value;
1877
+ if (lit.dataType === "duration") {
1878
+ const str = String(lit.value);
1879
+ const match = /^(\d+(?:\.\d+)?)(ms|s)$/.exec(str);
1880
+ if (match) {
1881
+ const ms = match[2] === "s" ? parseFloat(match[1]) * 1e3 : parseFloat(match[1]);
1882
+ return { type: "literal", value: ms };
1883
+ }
1884
+ }
1885
+ return { type: "literal", value: lit.value };
1886
+ }
1887
+ case "property-path": {
1888
+ const pp = value;
1889
+ const objectAST = semanticValueToAST(pp.object);
1890
+ return {
1891
+ type: "member",
1892
+ object: objectAST,
1893
+ property: pp.property
1894
+ };
1895
+ }
1896
+ case "expression": {
1897
+ const expr = value;
1898
+ return { type: "expression", value: expr.raw };
1899
+ }
1900
+ case "flag": {
1901
+ const flag = value;
1902
+ return { type: "literal", value: flag.enabled };
1903
+ }
1904
+ default:
1905
+ return { type: "literal", value: String(value.value ?? "") };
1906
+ }
1907
+ }
1538
1908
  export {
1539
1909
  DEFAULT_REFERENCES,
1540
1910
  convertValue,
1541
1911
  fromEnvelopeJSON,
1542
1912
  fromInterchangeNode,
1543
1913
  fromProtocolJSON,
1914
+ isCompoundSyntax,
1915
+ isDocumentSyntax,
1544
1916
  isEnvelope,
1545
1917
  isExplicitSyntax,
1546
1918
  isValidReference,
1547
1919
  jsonToSemanticNode,
1920
+ parseCompound,
1921
+ parseDocument,
1548
1922
  parseExplicit,
1923
+ renderDocument,
1549
1924
  renderExplicit,
1550
1925
  renderExpr,
1551
1926
  semanticNodeToJSON,
1927
+ semanticNodeToRuntimeAST,
1928
+ semanticValueToAST,
1552
1929
  toEnvelopeJSON,
1553
1930
  toProtocolJSON,
1554
1931
  validateProtocolJSON,