sketchmark 1.0.3 → 1.1.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.
@@ -228,7 +228,7 @@ var AIDiagram = (function (exports) {
228
228
  }
229
229
 
230
230
  // ============================================================
231
- // sketchmark Parser (Tokens DiagramAST)
231
+ // sketchmark - Parser (Tokens -> DiagramAST)
232
232
  // ============================================================
233
233
  let _uid = 0;
234
234
  function uid(prefix) {
@@ -287,15 +287,16 @@ var AIDiagram = (function (exports) {
287
287
  s.textAlign = p["text-align"];
288
288
  if (p.padding)
289
289
  s.padding = parseFloat(p.padding);
290
- if (p["vertical-align"])
290
+ if (p["vertical-align"]) {
291
291
  s.verticalAlign = p["vertical-align"];
292
+ }
292
293
  if (p["line-height"])
293
294
  s.lineHeight = parseFloat(p["line-height"]);
294
295
  if (p["letter-spacing"])
295
296
  s.letterSpacing = parseFloat(p["letter-spacing"]);
296
297
  if (p.font)
297
298
  s.font = p.font;
298
- const dashVal = p["dash"] || p["stroke-dash"];
299
+ const dashVal = p.dash || p["stroke-dash"];
299
300
  if (dashVal) {
300
301
  const parts = dashVal
301
302
  .split(",")
@@ -306,10 +307,15 @@ var AIDiagram = (function (exports) {
306
307
  }
307
308
  return s;
308
309
  }
310
+ function isValueToken(t) {
311
+ return !!t && (t.type === "IDENT" || t.type === "STRING" || t.type === "KEYWORD");
312
+ }
313
+ function isPropKeyToken(t) {
314
+ return !!t && (t.type === "IDENT" || t.type === "KEYWORD");
315
+ }
309
316
  function parse(src) {
310
317
  resetUid();
311
318
  const tokens = tokenize$1(src).filter((t) => t.type !== "NEWLINE" || t.value === "\n");
312
- // Collapse multiple consecutive NEWLINEs into one
313
319
  const flat = [];
314
320
  let lastNL = false;
315
321
  for (const t of tokens) {
@@ -338,11 +344,9 @@ var AIDiagram = (function (exports) {
338
344
  config: {},
339
345
  rootOrder: [],
340
346
  };
341
- const nodeIds = new Set();
342
- const tableIds = new Set();
343
- const chartIds = new Set();
344
- const groupIds = new Set();
345
- const markdownIds = new Set();
347
+ const authoredEntityKinds = new Map();
348
+ const unresolvedGroupItems = new Map();
349
+ const groupTokens = new Map();
346
350
  let i = 0;
347
351
  const cur = () => flat[i] ?? flat[flat.length - 1];
348
352
  const peek1 = () => flat[i + 1] ?? flat[flat.length - 1];
@@ -351,7 +355,6 @@ var AIDiagram = (function (exports) {
351
355
  while (cur().type === "NEWLINE")
352
356
  skip();
353
357
  };
354
- // Consume until EOL, return all tokens
355
358
  function lineTokens() {
356
359
  const acc = [];
357
360
  while (cur().type !== "NEWLINE" && cur().type !== "EOF") {
@@ -362,24 +365,104 @@ var AIDiagram = (function (exports) {
362
365
  skip();
363
366
  return acc;
364
367
  }
368
+ function requireExplicitId(keywordTok, toks) {
369
+ const first = toks[0];
370
+ if (!isValueToken(first) || toks[1]?.type === "EQUALS") {
371
+ throw new ParseError(`${keywordTok.value} requires an explicit id before properties`, keywordTok.line, keywordTok.col);
372
+ }
373
+ return first.value;
374
+ }
375
+ function parseSimpleProps(toks, startIndex) {
376
+ const props = {};
377
+ let j = startIndex;
378
+ while (j < toks.length - 1) {
379
+ const key = toks[j];
380
+ const eq = toks[j + 1];
381
+ if (isPropKeyToken(key) && eq?.type === "EQUALS" && j + 2 < toks.length) {
382
+ props[key.value] = toks[j + 2].value;
383
+ j += 3;
384
+ }
385
+ else {
386
+ j++;
387
+ }
388
+ }
389
+ return props;
390
+ }
391
+ function parseGroupProps(toks, startIndex) {
392
+ const props = {};
393
+ const itemIds = [];
394
+ let j = startIndex;
395
+ while (j < toks.length) {
396
+ const key = toks[j];
397
+ const eq = toks[j + 1];
398
+ if (!isPropKeyToken(key) || eq?.type !== "EQUALS") {
399
+ j++;
400
+ continue;
401
+ }
402
+ if (key.value === "items") {
403
+ const open = toks[j + 2];
404
+ if (open?.type !== "LBRACKET") {
405
+ throw new ParseError(`items must use bracket syntax like items=[a,b]`, key.line, key.col);
406
+ }
407
+ j += 3;
408
+ while (j < toks.length && toks[j].type !== "RBRACKET") {
409
+ const tok = toks[j];
410
+ if (tok.type === "COMMA") {
411
+ j++;
412
+ continue;
413
+ }
414
+ if (!isValueToken(tok)) {
415
+ const invalidTok = toks[j];
416
+ throw new ParseError(`items can only contain ids like items=[a,b]`, invalidTok.line, invalidTok.col);
417
+ }
418
+ itemIds.push(tok.value);
419
+ j++;
420
+ if (toks[j]?.type === "COMMA") {
421
+ j++;
422
+ }
423
+ else if (toks[j] && toks[j].type !== "RBRACKET") {
424
+ throw new ParseError(`Expected ',' or ']' in items list`, toks[j].line, toks[j].col);
425
+ }
426
+ }
427
+ if (toks[j]?.type !== "RBRACKET") {
428
+ throw new ParseError(`Unterminated items list; expected ']'`, key.line, key.col);
429
+ }
430
+ j++;
431
+ continue;
432
+ }
433
+ if (j + 2 < toks.length) {
434
+ props[key.value] = toks[j + 2].value;
435
+ j += 3;
436
+ }
437
+ else {
438
+ j++;
439
+ }
440
+ }
441
+ return { props, itemIds };
442
+ }
443
+ function registerAuthoredId(id, kind, tok) {
444
+ const existing = authoredEntityKinds.get(id);
445
+ if (existing) {
446
+ throw new ParseError(`Duplicate id "${id}" already declared as a ${existing}`, tok.line, tok.col);
447
+ }
448
+ authoredEntityKinds.set(id, kind);
449
+ }
365
450
  function parseDataArray() {
366
451
  const rows = [];
367
452
  while (cur().type !== "LBRACKET" && cur().type !== "EOF")
368
453
  skip();
369
- skip(); // outer [
454
+ skip();
370
455
  skipNL();
371
456
  while (cur().type !== "RBRACKET" && cur().type !== "EOF") {
372
457
  skipNL();
373
458
  if (cur().type === "RBRACKET" || cur().type === "EOF")
374
- break; // ← ADD THIS LINE
459
+ break;
375
460
  if (cur().type === "LBRACKET") {
376
461
  skip();
377
462
  const row = [];
378
463
  while (cur().type !== "RBRACKET" && cur().type !== "EOF") {
379
464
  const v = cur();
380
- if (v.type === "STRING" ||
381
- v.type === "IDENT" ||
382
- v.type === "KEYWORD") {
465
+ if (v.type === "STRING" || v.type === "IDENT" || v.type === "KEYWORD") {
383
466
  row.push(v.value);
384
467
  skip();
385
468
  }
@@ -390,8 +473,9 @@ var AIDiagram = (function (exports) {
390
473
  else if (v.type === "COMMA" || v.type === "NEWLINE") {
391
474
  skip();
392
475
  }
393
- else
476
+ else {
394
477
  break;
478
+ }
395
479
  }
396
480
  if (cur().type === "RBRACKET")
397
481
  skip();
@@ -400,46 +484,25 @@ var AIDiagram = (function (exports) {
400
484
  else if (cur().type === "COMMA" || cur().type === "NEWLINE") {
401
485
  skip();
402
486
  }
403
- else
487
+ else {
404
488
  skip();
489
+ }
405
490
  }
406
491
  if (cur().type === "RBRACKET")
407
492
  skip();
408
493
  return rows;
409
494
  }
410
- function parseNode(shape, groupId) {
411
- skip(); // shape keyword
495
+ function parseNode(shape) {
496
+ const keywordTok = cur();
497
+ skip();
412
498
  const toks = lineTokens();
413
- let id = groupId ? groupId + "_" + uid(shape) : uid(shape);
414
- const props = {};
415
- let j = 0;
416
- // First token may be the node id
417
- if (j < toks.length &&
418
- (toks[j].type === "IDENT" || toks[j].type === "STRING")) {
419
- id = toks[j++].value;
420
- }
421
- // Remaining tokens are key=value pairs
422
- while (j < toks.length) {
423
- const t = toks[j];
424
- if ((t.type === "IDENT" || t.type === "KEYWORD") &&
425
- j + 1 < toks.length &&
426
- toks[j + 1].type === "EQUALS") {
427
- const key = t.value;
428
- j += 2;
429
- if (j < toks.length) {
430
- props[key] = toks[j].value;
431
- j++;
432
- }
433
- }
434
- else
435
- j++;
436
- }
499
+ const id = requireExplicitId(keywordTok, toks);
500
+ const props = parseSimpleProps(toks, 1);
437
501
  const node = {
438
502
  kind: "node",
439
503
  id,
440
504
  shape,
441
505
  label: props.label || "",
442
- ...(groupId ? { groupId } : {}),
443
506
  ...(props.width ? { width: parseFloat(props.width) } : {}),
444
507
  ...(props.height ? { height: parseFloat(props.height) } : {}),
445
508
  ...(props.deg ? { deg: parseFloat(props.deg) } : {}),
@@ -457,113 +520,56 @@ var AIDiagram = (function (exports) {
457
520
  node.pathData = props.value;
458
521
  return node;
459
522
  }
460
- function parseEdge(fromId, connector, rest) {
461
- const toTok = rest.shift();
462
- if (!toTok)
463
- throw new ParseError("Expected edge target", 0, 0);
464
- const toId = toTok.value;
465
- const props = {};
466
- let j = 0;
467
- while (j < rest.length) {
468
- const t = rest[j];
469
- if ((t.type === "IDENT" || t.type === "KEYWORD") &&
470
- j + 1 < rest.length &&
471
- rest[j + 1].type === "EQUALS") {
472
- const key = t.value;
473
- j += 2;
474
- if (j < rest.length) {
475
- props[key] = rest[j].value;
476
- j++;
477
- }
478
- }
479
- else
480
- j++;
481
- }
482
- const dashed = connector.includes("--") ||
483
- connector.includes(".-") ||
484
- connector.includes("-.");
485
- const bidir = connector.includes("<") && connector.includes(">");
486
- return {
487
- kind: "edge",
488
- id: uid("edge"),
489
- from: fromId,
490
- to: toId,
491
- connector: connector,
492
- label: props.label,
493
- dashed,
494
- bidirectional: bidir,
495
- style: propsToStyle(props),
496
- };
497
- }
498
- // ── parseNote → returns ASTNode with shape='note' ────────
499
- function parseNote(groupId) {
500
- skip(); // 'note'
523
+ function parseNote() {
524
+ const keywordTok = cur();
525
+ skip();
501
526
  const toks = lineTokens();
502
- let id = groupId ? groupId + "_" + uid("note") : uid("note");
503
- if (toks[0])
504
- id = toks[0].value;
527
+ const id = requireExplicitId(keywordTok, toks);
505
528
  const props = {};
506
529
  let j = 1;
507
- // Backward compat: second token is a bare/quoted string → label
508
530
  if (toks[1] &&
509
531
  (toks[1].type === "STRING" ||
510
532
  (toks[1].type === "IDENT" && toks[2]?.type !== "EQUALS"))) {
511
533
  props.label = toks[1].value;
512
534
  j = 2;
513
535
  }
514
- // Parse remaining key=value props
515
- while (j < toks.length - 1) {
516
- const k = toks[j];
517
- const eq = toks[j + 1];
518
- if (eq && eq.type === "EQUALS" && j + 2 < toks.length) {
519
- props[k.value] = toks[j + 2].value;
520
- j += 3;
521
- }
522
- else
523
- j++;
524
- }
525
- // Support multiline via literal \n in label string
526
- const rawLabel = props.label ?? "";
536
+ Object.assign(props, parseSimpleProps(toks, j));
527
537
  return {
528
538
  kind: "node",
529
539
  id,
530
540
  shape: "note",
531
- label: rawLabel.replace(/\\n/g, "\n"),
532
- groupId,
541
+ label: (props.label ?? "").replace(/\\n/g, "\n"),
533
542
  theme: props.theme,
534
543
  style: propsToStyle(props),
535
544
  ...(props.width ? { width: parseFloat(props.width) } : {}),
536
545
  ...(props.height ? { height: parseFloat(props.height) } : {}),
537
546
  };
538
547
  }
539
- // ── parseGroup ───────────────────────────────────────────
540
- function parseGroup(parentGroupId) {
541
- skip(); // 'group'
548
+ function parseGroup() {
549
+ const keywordTok = cur();
550
+ skip();
542
551
  const toks = lineTokens();
543
- let id = uid("group");
544
- if (toks[0])
545
- id = toks[0].value;
552
+ if (toks.some((t) => t.type === "LBRACE" || t.type === "RBRACE")) {
553
+ throw new ParseError(`Nested group blocks were removed. Use ${keywordTok.value} <id> items=[...] instead.`, keywordTok.line, keywordTok.col);
554
+ }
555
+ const id = requireExplicitId(keywordTok, toks);
546
556
  const props = {};
547
557
  let j = 1;
548
- // Backward compat: second token is a quoted/bare string (old label syntax)
549
558
  if (toks[1] &&
550
559
  (toks[1].type === "STRING" ||
551
560
  (toks[1].type === "IDENT" && toks[2]?.type !== "EQUALS"))) {
552
561
  props.label = toks[1].value;
553
562
  j = 2;
554
563
  }
555
- // Parse remaining key=value props
556
- while (j < toks.length - 1) {
557
- const k = toks[j];
558
- const eq = toks[j + 1];
559
- if (eq && eq.type === "EQUALS" && j + 2 < toks.length) {
560
- props[k.value] = toks[j + 2].value;
561
- j += 3;
562
- }
563
- else
564
- j++;
564
+ const parsed = parseGroupProps(toks, j);
565
+ Object.assign(props, parsed.props);
566
+ skipNL();
567
+ if (cur().type === "LBRACE") {
568
+ throw new ParseError(`Nested group blocks were removed. Use ${keywordTok.value} ${id} items=[...] instead.`, cur().line, cur().col);
565
569
  }
566
- const group = {
570
+ unresolvedGroupItems.set(id, parsed.itemIds);
571
+ groupTokens.set(id, keywordTok);
572
+ return {
567
573
  kind: "group",
568
574
  id,
569
575
  label: props.label ?? "",
@@ -576,109 +582,43 @@ var AIDiagram = (function (exports) {
576
582
  justify: props.justify,
577
583
  theme: props.theme,
578
584
  style: propsToStyle(props),
579
- width: props.width !== undefined ? parseFloat(props.width) : undefined, // ← add
585
+ width: props.width !== undefined ? parseFloat(props.width) : undefined,
580
586
  height: props.height !== undefined ? parseFloat(props.height) : undefined,
581
587
  };
582
- skipNL();
583
- if (cur().type === "LBRACE") {
584
- skip();
585
- skipNL();
586
- }
587
- while (cur().type !== "RBRACE" &&
588
- cur().value !== "end" &&
589
- cur().type !== "EOF") {
590
- skipNL();
591
- if (cur().type === "RBRACE")
592
- break;
593
- const v = cur().value;
594
- // ── Nested group ──────────────────────────────────
595
- if (v === "group" || v === "bare") {
596
- const isBare = v === "bare";
597
- const nested = parseGroup();
598
- if (isBare) {
599
- nested.label = "";
600
- nested.style = {
601
- ...nested.style,
602
- fill: nested.style?.fill ?? "none",
603
- stroke: nested.style?.stroke ?? "none",
604
- strokeWidth: nested.style?.strokeWidth ?? 0,
605
- };
606
- }
607
- ast.groups.push(nested);
608
- groupIds.add(nested.id);
609
- group.children.push({ kind: "group", id: nested.id });
610
- continue;
611
- }
612
- // ── Table ─────────────────────────────────────────
613
- if (v === "table") {
614
- const tbl = parseTable();
615
- ast.tables.push(tbl);
616
- tableIds.add(tbl.id);
617
- group.children.push({ kind: "table", id: tbl.id });
618
- continue;
619
- }
620
- // ── Note (parsed as node with shape='note') ──────
621
- if (v === "note") {
622
- const note = parseNote(id);
623
- ast.nodes.push(note);
624
- nodeIds.add(note.id);
625
- group.children.push({ kind: "node", id: note.id });
626
- continue;
627
- }
628
- // ── Markdown ───────────────────────────────────────
629
- if (v === "markdown") {
630
- const md = parseMarkdown(id);
631
- ast.markdowns.push(md);
632
- markdownIds.add(md.id);
633
- group.children.push({ kind: "markdown", id: md.id });
634
- continue;
635
- }
636
- if (v === "bare") {
637
- // treat exactly like 'group' but inject defaults
638
- const grp = parseGroup(); // reuse parseGroup
639
- grp.label = "";
640
- grp.style = { ...grp.style, stroke: "none", fill: "none" };
641
- // rest is identical to group handling
642
- }
643
- // ── Chart ──────────────────────────────────────────
644
- if (CHART_TYPES.includes(v)) {
645
- const chart = parseChart(v);
646
- ast.charts.push(chart);
647
- chartIds.add(chart.id);
648
- group.children.push({ kind: "chart", id: chart.id });
649
- continue;
650
- }
651
- // ── Node shape ────────────────────────────────────
652
- if (SHAPES$1.includes(v)) {
653
- const node = parseNode(v, id);
654
- if (!nodeIds.has(node.id)) {
655
- nodeIds.add(node.id);
656
- ast.nodes.push(node);
657
- }
658
- group.children.push({ kind: "node", id: node.id });
659
- continue;
588
+ }
589
+ function parseEdge(fromId, connector, rest) {
590
+ const toTok = rest.shift();
591
+ if (!toTok)
592
+ throw new ParseError("Expected edge target", 0, 0);
593
+ const props = {};
594
+ let j = 0;
595
+ while (j < rest.length) {
596
+ const t = rest[j];
597
+ if ((t.type === "IDENT" || t.type === "KEYWORD") &&
598
+ j + 1 < rest.length &&
599
+ rest[j + 1].type === "EQUALS") {
600
+ props[t.value] = rest[j + 2]?.value ?? "";
601
+ j += 3;
660
602
  }
661
- // ── Edge inside group ─────────────────────────────
662
- if (cur().type === "IDENT" ||
663
- cur().type === "STRING" ||
664
- cur().type === "KEYWORD") {
665
- const nextTok = flat[i + 1];
666
- if (nextTok && nextTok.type === "ARROW") {
667
- const lineToks = lineTokens();
668
- if (lineToks.length >= 3 && lineToks[1].type === "ARROW") {
669
- const fromId = lineToks[0].value;
670
- const conn = lineToks[1].value;
671
- const edge = parseEdge(fromId, conn, lineToks.slice(2));
672
- ast.edges.push(edge);
673
- }
674
- continue;
675
- }
603
+ else {
604
+ j++;
676
605
  }
677
- skip();
678
606
  }
679
- if (cur().type === "RBRACE")
680
- skip();
681
- return group;
607
+ const dashed = connector.includes("--") ||
608
+ connector.includes(".-") ||
609
+ connector.includes("-.");
610
+ const bidirectional = connector.includes("<") && connector.includes(">");
611
+ return {
612
+ kind: "edge",
613
+ id: uid("edge"),
614
+ from: fromId,
615
+ to: toTok.value,
616
+ connector: connector,
617
+ label: props.label,
618
+ dashed,
619
+ bidirectional,
620
+ style: propsToStyle(props),
621
+ };
682
622
  }
683
623
  function parseStep() {
684
624
  skip();
@@ -689,12 +629,10 @@ var AIDiagram = (function (exports) {
689
629
  target = `${toks[1].value}${toks[2].value}${toks[3].value}`;
690
630
  }
691
631
  const step = { kind: "step", action, target };
692
- // narrate: text is the value, not a target
693
632
  if (action === "narrate") {
694
633
  step.target = "";
695
634
  step.value = toks[1]?.value ?? "";
696
635
  }
697
- // bracket: needs two targets
698
636
  if (action === "bracket" && toks.length >= 3) {
699
637
  step.target = toks[1]?.value ?? "";
700
638
  step.target2 = toks[2]?.value ?? "";
@@ -704,7 +642,6 @@ var AIDiagram = (function (exports) {
704
642
  const k = toks[j]?.value;
705
643
  const eq = toks[j + 1];
706
644
  const vt = toks[j + 2];
707
- // key=value form
708
645
  if (eq?.type === "EQUALS" && vt) {
709
646
  if (k === "dx") {
710
647
  step.dx = parseFloat(vt.value);
@@ -736,12 +673,7 @@ var AIDiagram = (function (exports) {
736
673
  j += 2;
737
674
  continue;
738
675
  }
739
- if (k === "fill") {
740
- step.value = vt.value;
741
- j += 2;
742
- continue;
743
- }
744
- if (k === "color") {
676
+ if (k === "fill" || k === "color") {
745
677
  step.value = vt.value;
746
678
  j += 2;
747
679
  continue;
@@ -752,7 +684,6 @@ var AIDiagram = (function (exports) {
752
684
  continue;
753
685
  }
754
686
  }
755
- // bare key value (legacy)
756
687
  if (k === "delay" && eq?.type === "NUMBER") {
757
688
  step.delay = parseFloat(eq.value);
758
689
  j++;
@@ -766,89 +697,16 @@ var AIDiagram = (function (exports) {
766
697
  if (k === "trigger") {
767
698
  step.trigger = eq?.value;
768
699
  j++;
769
- continue;
770
700
  }
771
701
  }
772
702
  return step;
773
703
  }
774
- // function parseStep(): ASTStep {
775
- // skip(); // 'step'
776
- // const toks = lineTokens();
777
- // const action = (toks[0]?.value ?? "highlight") as AnimationAction;
778
- // let target = toks[1]?.value ?? "";
779
- // if (toks[2]?.type === "ARROW" && toks[3]) {
780
- // target = `${toks[1].value}${toks[2].value}${toks[3].value}`;
781
- // }
782
- // const step: ASTStep = { kind: "step", action, target };
783
- // for (let j = 2; j < toks.length - 1; j++) {
784
- // const k = toks[j].value;
785
- // const eq = toks[j + 1];
786
- // const vt = toks[j + 2];
787
- // // key=value form (dx=50, dy=-80, duration=600)
788
- // if (eq?.type === "EQUALS" && vt) {
789
- // if (k === "dx") {
790
- // step.dx = parseFloat(vt.value);
791
- // j += 2;
792
- // continue;
793
- // }
794
- // if (k === "dy") {
795
- // step.dy = parseFloat(vt.value);
796
- // j += 2;
797
- // continue;
798
- // }
799
- // if (k === "duration") {
800
- // step.duration = parseFloat(vt.value);
801
- // j += 2;
802
- // continue;
803
- // }
804
- // if (k === "delay") {
805
- // step.delay = parseFloat(vt.value);
806
- // j += 2;
807
- // continue;
808
- // }
809
- // }
810
- // // bare key value form (legacy: delay 500, duration 400)
811
- // if (k === "delay" && eq?.type === "NUMBER") {
812
- // step.delay = parseFloat(eq.value);
813
- // j++;
814
- // }
815
- // if (k === "duration" && eq?.type === "NUMBER") {
816
- // step.duration = parseFloat(eq.value);
817
- // j++;
818
- // }
819
- // if (k === "trigger") {
820
- // step.trigger = eq?.value as AnimationTrigger;
821
- // j++;
822
- // }
823
- // if (k === "factor") {
824
- // step.factor = parseFloat(vt.value);
825
- // j += 2;
826
- // continue;
827
- // }
828
- // if (k === "deg") {
829
- // step.deg = parseFloat(vt.value);
830
- // j += 2;
831
- // continue;
832
- // }
833
- // }
834
- // return step;
835
- // }
836
704
  function parseChart(chartType) {
705
+ const keywordTok = cur();
837
706
  skip();
838
707
  const toks = lineTokens();
839
- const id = toks[0]?.value ?? uid("chart");
840
- const props = {};
841
- let j = 1;
842
- while (j < toks.length - 1) {
843
- const k = toks[j];
844
- const eq = toks[j + 1];
845
- if (eq?.type === "EQUALS" && j + 2 < toks.length) {
846
- props[k.value] = toks[j + 2].value;
847
- j += 3;
848
- }
849
- else
850
- j++;
851
- }
708
+ const id = requireExplicitId(keywordTok, toks);
709
+ const props = parseSimpleProps(toks, 1);
852
710
  let dataRows = [];
853
711
  skipNL();
854
712
  while (cur().type !== "EOF" && cur().value !== "end") {
@@ -861,30 +719,31 @@ var AIDiagram = (function (exports) {
861
719
  }
862
720
  else if ((cur().type === "IDENT" || cur().type === "KEYWORD") &&
863
721
  peek1().type === "EQUALS") {
864
- const k = cur().value;
722
+ const key = cur().value;
865
723
  skip();
866
724
  skip();
867
- props[k] = cur().value;
725
+ props[key] = cur().value;
868
726
  skip();
869
727
  }
870
728
  else if (SHAPES$1.includes(v) ||
871
729
  v === "step" ||
872
730
  v === "group" ||
873
- v === "note" || // ← ADD
731
+ v === "bare" ||
732
+ v === "note" ||
874
733
  v === "table" ||
875
- v === "config" || // ← ADD
876
- v === "theme" || // ← ADD
734
+ v === "config" ||
735
+ v === "theme" ||
877
736
  v === "style" ||
878
737
  v === "markdown" ||
879
738
  CHART_TYPES.includes(v)) {
880
739
  break;
881
740
  }
882
741
  else if (peek1().type === "ARROW") {
883
- // ← ADD THIS WHOLE BLOCK
884
742
  break;
885
743
  }
886
- else
744
+ else {
887
745
  skip();
746
+ }
888
747
  }
889
748
  const headers = dataRows[0]?.map(String) ?? [];
890
749
  const rows = dataRows.slice(1);
@@ -901,29 +760,19 @@ var AIDiagram = (function (exports) {
901
760
  };
902
761
  }
903
762
  function parseTable() {
904
- skip(); // 'table'
763
+ const keywordTok = cur();
764
+ skip();
905
765
  const toks = lineTokens();
906
- let id = uid("table");
907
- if (toks[0])
908
- id = toks[0].value;
766
+ const id = requireExplicitId(keywordTok, toks);
909
767
  const props = {};
910
768
  let j = 1;
911
- // label="..." or bare second token
912
769
  if (toks[1] &&
913
770
  (toks[1].type === "STRING" ||
914
771
  (toks[1].type === "IDENT" && toks[2]?.type !== "EQUALS"))) {
915
772
  props.label = toks[1].value;
916
773
  j = 2;
917
774
  }
918
- while (j < toks.length - 1) {
919
- const k = toks[j], eq = toks[j + 1];
920
- if (eq?.type === "EQUALS" && j + 2 < toks.length) {
921
- props[k.value] = toks[j + 2].value;
922
- j += 3;
923
- }
924
- else
925
- j++;
926
- }
775
+ Object.assign(props, parseSimpleProps(toks, j));
927
776
  const table = {
928
777
  kind: "table",
929
778
  id,
@@ -950,7 +799,8 @@ var AIDiagram = (function (exports) {
950
799
  while (cur().type !== "NEWLINE" && cur().type !== "EOF") {
951
800
  if (cur().type === "STRING" ||
952
801
  cur().type === "IDENT" ||
953
- cur().type === "NUMBER") {
802
+ cur().type === "NUMBER" ||
803
+ cur().type === "KEYWORD") {
954
804
  cells.push(cur().value);
955
805
  }
956
806
  skip();
@@ -959,31 +809,20 @@ var AIDiagram = (function (exports) {
959
809
  skip();
960
810
  table.rows.push({ kind: v === "header" ? "header" : "data", cells });
961
811
  }
962
- else
812
+ else {
963
813
  skip();
814
+ }
964
815
  }
965
816
  if (cur().type === "RBRACE")
966
817
  skip();
967
818
  return table;
968
819
  }
969
- // ── parseMarkdown ─────────────────────────────────────────
970
- function parseMarkdown(groupId) {
971
- skip(); // 'markdown'
820
+ function parseMarkdown() {
821
+ const keywordTok = cur();
822
+ skip();
972
823
  const toks = lineTokens();
973
- let id = groupId ? groupId + "_" + uid("md") : uid("md");
974
- if (toks[0])
975
- id = toks[0].value;
976
- const props = {};
977
- let j = 1;
978
- while (j < toks.length - 1) {
979
- const k = toks[j], eq = toks[j + 1];
980
- if (eq?.type === "EQUALS" && j + 2 < toks.length) {
981
- props[k.value] = toks[j + 2].value;
982
- j += 3;
983
- }
984
- else
985
- j++;
986
- }
824
+ const id = requireExplicitId(keywordTok, toks);
825
+ const props = parseSimpleProps(toks, 1);
987
826
  skipNL();
988
827
  let content = "";
989
828
  if (cur().type === "STRING_BLOCK") {
@@ -1000,7 +839,6 @@ var AIDiagram = (function (exports) {
1000
839
  style: propsToStyle(props),
1001
840
  };
1002
841
  }
1003
- // ── Main parse loop ─────────────────────────────────────
1004
842
  skipNL();
1005
843
  if (cur().value === "diagram")
1006
844
  skip();
@@ -1019,19 +857,16 @@ var AIDiagram = (function (exports) {
1019
857
  }
1020
858
  if (v === "end")
1021
859
  break;
1022
- // direction — silently ignored (removed from engine)
1023
860
  if (v === "direction") {
1024
861
  lineTokens();
1025
862
  continue;
1026
863
  }
1027
- // layout
1028
864
  if (v === "layout") {
1029
865
  skip();
1030
866
  ast.layout = cur().value ?? "column";
1031
867
  skip();
1032
868
  continue;
1033
869
  }
1034
- // title
1035
870
  if (v === "title") {
1036
871
  skip();
1037
872
  const toks = lineTokens();
@@ -1041,14 +876,10 @@ var AIDiagram = (function (exports) {
1041
876
  ast.title = toks[idx + 2]?.value ?? "";
1042
877
  }
1043
878
  else {
1044
- ast.title = toks
1045
- .map((t2) => t2.value)
1046
- .join(" ")
1047
- .replace(/"/g, "");
879
+ ast.title = toks.map((t2) => t2.value).join(" ").replace(/"/g, "");
1048
880
  }
1049
881
  continue;
1050
882
  }
1051
- // description
1052
883
  if (v === "description") {
1053
884
  skip();
1054
885
  ast.description = lineTokens()
@@ -1057,65 +888,38 @@ var AIDiagram = (function (exports) {
1057
888
  .replace(/"/g, "");
1058
889
  continue;
1059
890
  }
1060
- // config
1061
891
  if (v === "config") {
1062
892
  skip();
1063
- const k = cur().value;
893
+ const key = cur().value;
1064
894
  skip();
1065
895
  if (cur().type === "EQUALS")
1066
896
  skip();
1067
- const cv = cur().value;
897
+ const value = cur().value;
1068
898
  skip();
1069
- ast.config[k] = cv;
899
+ ast.config[key] = value;
1070
900
  continue;
1071
901
  }
1072
- // style
1073
902
  if (v === "style") {
1074
903
  skip();
1075
904
  const targetId = cur().value;
1076
905
  skip();
1077
- const lineToks = lineTokens();
1078
- const p = {};
1079
- let j = 0;
1080
- while (j < lineToks.length - 1) {
1081
- const k2 = lineToks[j];
1082
- const eq = lineToks[j + 1];
1083
- if (eq.type === "EQUALS") {
1084
- p[k2.value] = lineToks[j + 2]?.value ?? "";
1085
- j += 3;
1086
- }
1087
- else
1088
- j++;
1089
- }
1090
- ast.styles[targetId] = { ...ast.styles[targetId], ...propsToStyle(p) };
906
+ const props = parseSimpleProps(lineTokens(), 0);
907
+ ast.styles[targetId] = { ...ast.styles[targetId], ...propsToStyle(props) };
1091
908
  continue;
1092
909
  }
1093
- // theme
1094
910
  if (v === "theme") {
1095
911
  skip();
1096
912
  const toks = lineTokens();
1097
913
  const themeId = toks[0]?.value;
1098
914
  if (!themeId)
1099
915
  continue;
1100
- const props = {};
1101
- let j = 1;
1102
- while (j < toks.length - 1) {
1103
- const k2 = toks[j];
1104
- const eq = toks[j + 1];
1105
- if (eq && eq.type === "EQUALS" && j + 2 < toks.length) {
1106
- props[k2.value] = toks[j + 2].value;
1107
- j += 3;
1108
- }
1109
- else
1110
- j++;
1111
- }
1112
- ast.themes[themeId] = propsToStyle(props);
916
+ ast.themes[themeId] = propsToStyle(parseSimpleProps(toks, 1));
1113
917
  continue;
1114
918
  }
1115
- // group
1116
919
  if (v === "group" || v === "bare") {
1117
920
  const isBare = v === "bare";
1118
921
  const grp = parseGroup();
922
+ registerAuthoredId(grp.id, "group", t);
1119
923
  if (isBare) {
1120
924
  grp.label = "";
1121
925
  grp.style = {
@@ -1126,36 +930,34 @@ var AIDiagram = (function (exports) {
1126
930
  };
1127
931
  }
1128
932
  ast.groups.push(grp);
1129
- groupIds.add(grp.id);
1130
933
  ast.rootOrder.push({ kind: "group", id: grp.id });
1131
934
  continue;
1132
935
  }
1133
- // table
1134
936
  if (v === "table") {
1135
937
  const tbl = parseTable();
938
+ registerAuthoredId(tbl.id, "table", t);
1136
939
  ast.tables.push(tbl);
1137
- tableIds.add(tbl.id);
1138
940
  ast.rootOrder.push({ kind: "table", id: tbl.id });
1139
941
  continue;
1140
942
  }
1141
- // note (parsed as node with shape='note')
1142
943
  if (v === "note") {
1143
944
  const note = parseNote();
945
+ registerAuthoredId(note.id, "node", t);
1144
946
  ast.nodes.push(note);
1145
- nodeIds.add(note.id);
1146
947
  ast.rootOrder.push({ kind: "node", id: note.id });
1147
948
  continue;
1148
949
  }
1149
- // beat { ... } — parallel steps
1150
950
  if (v === "beat") {
1151
- skip(); // 'beat'
951
+ skip();
1152
952
  skipNL();
1153
953
  if (cur().type === "LBRACE") {
1154
954
  skip();
1155
955
  skipNL();
1156
956
  }
1157
957
  const children = [];
1158
- while (cur().type !== "RBRACE" && cur().value !== "end" && cur().type !== "EOF") {
958
+ while (cur().type !== "RBRACE" &&
959
+ cur().value !== "end" &&
960
+ cur().type !== "EOF") {
1159
961
  skipNL();
1160
962
  if (cur().type === "RBRACE")
1161
963
  break;
@@ -1171,69 +973,120 @@ var AIDiagram = (function (exports) {
1171
973
  ast.steps.push({ kind: "beat", children });
1172
974
  continue;
1173
975
  }
1174
- // step
1175
976
  if (v === "step") {
1176
977
  ast.steps.push(parseStep());
1177
978
  continue;
1178
979
  }
1179
- // charts
1180
980
  if (CHART_TYPES.includes(v)) {
1181
981
  const chart = parseChart(v);
982
+ registerAuthoredId(chart.id, "chart", t);
1182
983
  ast.charts.push(chart);
1183
- chartIds.add(chart.id);
1184
- ast.rootOrder.push({ kind: "chart", id: chart.id }); // ← ADD
984
+ ast.rootOrder.push({ kind: "chart", id: chart.id });
1185
985
  continue;
1186
986
  }
1187
987
  if (v === "markdown") {
1188
988
  const md = parseMarkdown();
989
+ registerAuthoredId(md.id, "markdown", t);
1189
990
  ast.markdowns.push(md);
1190
- markdownIds.add(md.id);
1191
991
  ast.rootOrder.push({ kind: "markdown", id: md.id });
1192
992
  continue;
1193
993
  }
1194
- // edge: A -> B (MUST come before shape check)
1195
994
  if (t.type === "IDENT" || t.type === "STRING" || t.type === "KEYWORD") {
1196
995
  const nextTok = flat[i + 1];
1197
996
  if (nextTok && nextTok.type === "ARROW") {
1198
997
  const lineToks = lineTokens();
1199
998
  if (lineToks.length >= 3 && lineToks[1].type === "ARROW") {
1200
999
  const fromId = lineToks[0].value;
1201
- const conn = lineToks[1].value;
1202
- const edge = parseEdge(fromId, conn, lineToks.slice(2));
1203
- ast.edges.push(edge);
1204
- // Auto-create implied nodes if they don't exist yet
1205
- for (const nid of [fromId, edge.to]) {
1206
- if (!nodeIds.has(nid) &&
1207
- !tableIds.has(nid) &&
1208
- !chartIds.has(nid) &&
1209
- !groupIds.has(nid)) {
1210
- nodeIds.add(nid);
1211
- ast.nodes.push({
1212
- kind: "node",
1213
- id: nid,
1214
- shape: "box",
1215
- label: nid,
1216
- style: {},
1217
- });
1218
- }
1219
- }
1000
+ const connector = lineToks[1].value;
1001
+ ast.edges.push(parseEdge(fromId, connector, lineToks.slice(2)));
1220
1002
  continue;
1221
1003
  }
1222
1004
  }
1223
1005
  }
1224
- // node shapes — only reached if NOT followed by an arrow
1225
1006
  if (SHAPES$1.includes(v)) {
1226
1007
  const node = parseNode(v);
1227
- if (!nodeIds.has(node.id)) {
1228
- nodeIds.add(node.id);
1229
- ast.nodes.push(node);
1230
- ast.rootOrder.push({ kind: "node", id: node.id });
1231
- }
1008
+ registerAuthoredId(node.id, "node", t);
1009
+ ast.nodes.push(node);
1010
+ ast.rootOrder.push({ kind: "node", id: node.id });
1232
1011
  continue;
1233
1012
  }
1234
1013
  skip();
1235
1014
  }
1236
- // Merge global styles into node styles
1015
+ const allKnownIds = new Set(authoredEntityKinds.keys());
1016
+ for (const edge of ast.edges) {
1017
+ for (const id of [edge.from, edge.to]) {
1018
+ if (allKnownIds.has(id))
1019
+ continue;
1020
+ allKnownIds.add(id);
1021
+ ast.nodes.push({
1022
+ kind: "node",
1023
+ id,
1024
+ shape: "box",
1025
+ label: id,
1026
+ style: {},
1027
+ });
1028
+ }
1029
+ }
1030
+ const entityKindById = new Map();
1031
+ ast.nodes.forEach((node) => entityKindById.set(node.id, "node"));
1032
+ ast.groups.forEach((group) => entityKindById.set(group.id, "group"));
1033
+ ast.tables.forEach((table) => entityKindById.set(table.id, "table"));
1034
+ ast.charts.forEach((chart) => entityKindById.set(chart.id, "chart"));
1035
+ ast.markdowns.forEach((md) => entityKindById.set(md.id, "markdown"));
1036
+ for (const group of ast.groups) {
1037
+ const itemIds = unresolvedGroupItems.get(group.id) ?? [];
1038
+ group.children = itemIds.map((itemId) => {
1039
+ if (itemId === group.id) {
1040
+ const tok = groupTokens.get(group.id) ?? cur();
1041
+ throw new ParseError(`Group "${group.id}" cannot include itself in items=[...]`, tok.line, tok.col);
1042
+ }
1043
+ const kind = entityKindById.get(itemId);
1044
+ if (!kind) {
1045
+ const tok = groupTokens.get(group.id) ?? cur();
1046
+ throw new ParseError(`Group "${group.id}" references unknown item "${itemId}" in items=[...]`, tok.line, tok.col);
1047
+ }
1048
+ return { kind, id: itemId };
1049
+ });
1050
+ }
1051
+ const parentByItemId = new Map();
1052
+ for (const group of ast.groups) {
1053
+ for (const child of group.children) {
1054
+ const existingParent = parentByItemId.get(child.id);
1055
+ if (existingParent) {
1056
+ const tok = groupTokens.get(group.id) ?? cur();
1057
+ throw new ParseError(`Item "${child.id}" cannot belong to both "${existingParent}" and "${group.id}"`, tok.line, tok.col);
1058
+ }
1059
+ parentByItemId.set(child.id, group.id);
1060
+ }
1061
+ }
1062
+ const groupsById = new Map(ast.groups.map((group) => [group.id, group]));
1063
+ const visiting = new Set();
1064
+ const visited = new Set();
1065
+ const stack = [];
1066
+ function visitGroup(groupId) {
1067
+ if (visiting.has(groupId)) {
1068
+ const start = stack.indexOf(groupId);
1069
+ const cycle = (start >= 0 ? stack.slice(start) : stack).concat(groupId);
1070
+ const tok = groupTokens.get(groupId) ?? cur();
1071
+ throw new ParseError(`Group cycle detected: ${cycle.join(" -> ")}`, tok.line, tok.col);
1072
+ }
1073
+ if (visited.has(groupId))
1074
+ return;
1075
+ visiting.add(groupId);
1076
+ stack.push(groupId);
1077
+ const group = groupsById.get(groupId);
1078
+ if (group) {
1079
+ for (const child of group.children) {
1080
+ if (child.kind === "group")
1081
+ visitGroup(child.id);
1082
+ }
1083
+ }
1084
+ stack.pop();
1085
+ visiting.delete(groupId);
1086
+ visited.add(groupId);
1087
+ }
1088
+ for (const group of ast.groups)
1089
+ visitGroup(group.id);
1237
1090
  for (const node of ast.nodes) {
1238
1091
  if (ast.styles[node.id]) {
1239
1092
  node.style = { ...ast.styles[node.id], ...node.style };
@@ -3568,6 +3421,16 @@ var AIDiagram = (function (exports) {
3568
3421
  // ============================================================
3569
3422
  // ── Build scene graph from AST ────────────────────────────
3570
3423
  function buildSceneGraph(ast) {
3424
+ const nodeParentById = new Map();
3425
+ const groupParentById = new Map();
3426
+ for (const g of ast.groups) {
3427
+ for (const child of g.children) {
3428
+ if (child.kind === "node")
3429
+ nodeParentById.set(child.id, g.id);
3430
+ if (child.kind === "group")
3431
+ groupParentById.set(child.id, g.id);
3432
+ }
3433
+ }
3571
3434
  const nodes = ast.nodes.map((n) => {
3572
3435
  const themeStyle = n.theme ? (ast.themes[n.theme] ?? {}) : {};
3573
3436
  return {
@@ -3575,7 +3438,7 @@ var AIDiagram = (function (exports) {
3575
3438
  shape: n.shape,
3576
3439
  label: n.label,
3577
3440
  style: { ...ast.styles[n.id], ...themeStyle, ...n.style },
3578
- groupId: n.groupId,
3441
+ groupId: nodeParentById.get(n.id),
3579
3442
  width: n.width,
3580
3443
  height: n.height,
3581
3444
  deg: n.deg,
@@ -3597,7 +3460,7 @@ var AIDiagram = (function (exports) {
3597
3460
  return {
3598
3461
  id: g.id,
3599
3462
  label: g.label,
3600
- parentId: undefined, // set below
3463
+ parentId: groupParentById.get(g.id),
3601
3464
  children: g.children,
3602
3465
  layout: (g.layout ?? "column"),
3603
3466
  columns: g.columns ?? 1,
@@ -3645,28 +3508,21 @@ var AIDiagram = (function (exports) {
3645
3508
  h: c.height ?? CHART.defaultH,
3646
3509
  };
3647
3510
  });
3648
- const markdowns = (ast.markdowns ?? []).map(m => {
3511
+ const markdowns = (ast.markdowns ?? []).map((m) => {
3649
3512
  const themeStyle = m.theme ? (ast.themes[m.theme] ?? {}) : {};
3650
3513
  return {
3651
3514
  id: m.id,
3652
3515
  content: m.content,
3653
3516
  lines: parseMarkdownContent(m.content),
3654
- style: { ...themeStyle, ...m.style },
3517
+ style: { ...ast.styles[m.id], ...themeStyle, ...m.style },
3655
3518
  width: m.width,
3656
3519
  height: m.height,
3657
- x: 0, y: 0, w: 0, h: 0,
3520
+ x: 0,
3521
+ y: 0,
3522
+ w: 0,
3523
+ h: 0,
3658
3524
  };
3659
3525
  });
3660
- // Set parentId for nested groups
3661
- for (const g of groups) {
3662
- for (const child of g.children) {
3663
- if (child.kind === "group") {
3664
- const nested = groups.find((gg) => gg.id === child.id);
3665
- if (nested)
3666
- nested.parentId = g.id;
3667
- }
3668
- }
3669
- }
3670
3526
  const edges = ast.edges.map((e) => ({
3671
3527
  id: e.id,
3672
3528
  from: e.from,
@@ -4793,15 +4649,30 @@ var AIDiagram = (function (exports) {
4793
4649
  // 4. Build root order
4794
4650
  // sg.rootOrder preserves DSL declaration order.
4795
4651
  // Fall back: groups, then nodes, then tables.
4796
- const rootOrder = sg.rootOrder?.length
4797
- ? sg.rootOrder
4798
- : [
4799
- ...rootGroups.map((g) => ({ kind: "group", id: g.id })),
4800
- ...rootNodes.map((n) => ({ kind: "node", id: n.id })),
4801
- ...rootTables.map((t) => ({ kind: "table", id: t.id })),
4802
- ...rootCharts.map((c) => ({ kind: "chart", id: c.id })),
4803
- ...rootMarkdowns.map((m) => ({ kind: "markdown", id: m.id })),
4804
- ];
4652
+ const defaultRootOrder = [
4653
+ ...rootGroups.map((g) => ({ kind: "group", id: g.id })),
4654
+ ...rootNodes.map((n) => ({ kind: "node", id: n.id })),
4655
+ ...rootTables.map((t) => ({ kind: "table", id: t.id })),
4656
+ ...rootCharts.map((c) => ({ kind: "chart", id: c.id })),
4657
+ ...rootMarkdowns.map((m) => ({ kind: "markdown", id: m.id })),
4658
+ ];
4659
+ const rootOrderSource = sg.rootOrder?.length ? sg.rootOrder : defaultRootOrder;
4660
+ const rootOrder = rootOrderSource.filter((ref) => {
4661
+ switch (ref.kind) {
4662
+ case "group":
4663
+ return !nestedGroupIds.has(ref.id);
4664
+ case "node":
4665
+ return !groupedNodeIds.has(ref.id);
4666
+ case "table":
4667
+ return !groupedTableIds.has(ref.id);
4668
+ case "chart":
4669
+ return !groupedChartIds.has(ref.id);
4670
+ case "markdown":
4671
+ return !groupedMarkdownIds.has(ref.id);
4672
+ default:
4673
+ return true;
4674
+ }
4675
+ });
4805
4676
  // 5. Root-level layout
4806
4677
  // sg.layout:
4807
4678
  // 'row' → items flow left to right (default)