sketchmark 1.1.5 → 1.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.
package/dist/index.js CHANGED
@@ -144,10 +144,16 @@ function tokenize$1(src) {
144
144
  val += "\n";
145
145
  else if (esc === "t")
146
146
  val += "\t";
147
+ else if (esc === "r")
148
+ val += "\r";
147
149
  else if (esc === "\\")
148
150
  val += "\\";
151
+ else if (esc === q)
152
+ val += q;
153
+ else if (esc)
154
+ val += `\\${esc}`;
149
155
  else
150
- val += esc;
156
+ val += "\\";
151
157
  }
152
158
  else
153
159
  val += src[i];
@@ -224,6 +230,47 @@ function tokenize$1(src) {
224
230
  return tokens;
225
231
  }
226
232
 
233
+ function pluginMessage(plugin, stage, error) {
234
+ const detail = error instanceof Error ? error.message : String(error);
235
+ return `Plugin "${plugin.name}" ${stage} failed: ${detail}`;
236
+ }
237
+ function applyPluginPreprocessors(source, plugins = []) {
238
+ let nextSource = source;
239
+ for (const plugin of plugins) {
240
+ if (!plugin.preprocess)
241
+ continue;
242
+ try {
243
+ const transformed = plugin.preprocess(nextSource);
244
+ if (typeof transformed !== "string") {
245
+ throw new Error("preprocess must return a string");
246
+ }
247
+ nextSource = transformed;
248
+ }
249
+ catch (error) {
250
+ throw new Error(pluginMessage(plugin, "preprocess", error));
251
+ }
252
+ }
253
+ return nextSource;
254
+ }
255
+ function applyPluginAstTransforms(ast, plugins = []) {
256
+ let nextAst = ast;
257
+ for (const plugin of plugins) {
258
+ if (!plugin.transformAst)
259
+ continue;
260
+ try {
261
+ const transformed = plugin.transformAst(nextAst);
262
+ if (!transformed || transformed.kind !== "diagram") {
263
+ throw new Error('transformAst must return a DiagramAST with kind="diagram"');
264
+ }
265
+ nextAst = transformed;
266
+ }
267
+ catch (error) {
268
+ throw new Error(pluginMessage(plugin, "transformAst", error));
269
+ }
270
+ }
271
+ return nextAst;
272
+ }
273
+
227
274
  // ============================================================
228
275
  // sketchmark - Parser (Tokens -> DiagramAST)
229
276
  // ============================================================
@@ -310,9 +357,10 @@ function isValueToken(t) {
310
357
  function isPropKeyToken(t) {
311
358
  return !!t && (t.type === "IDENT" || t.type === "KEYWORD");
312
359
  }
313
- function parse(src) {
360
+ function parse(src, options = {}) {
314
361
  resetUid();
315
- const tokens = tokenize$1(src).filter((t) => t.type !== "NEWLINE" || t.value === "\n");
362
+ const preparedSource = applyPluginPreprocessors(src, options.plugins);
363
+ const tokens = tokenize$1(preparedSource).filter((t) => t.type !== "NEWLINE" || t.value === "\n");
316
364
  const flat = [];
317
365
  let lastNL = false;
318
366
  for (const t of tokens) {
@@ -495,6 +543,7 @@ function parse(src) {
495
543
  const toks = lineTokens();
496
544
  const id = requireExplicitId(keywordTok, toks);
497
545
  const props = parseSimpleProps(toks, 1);
546
+ const meta = extractNodeMeta(props);
498
547
  const node = {
499
548
  kind: "node",
500
549
  id,
@@ -502,11 +551,14 @@ function parse(src) {
502
551
  label: props.label || "",
503
552
  ...(props.width ? { width: parseFloat(props.width) } : {}),
504
553
  ...(props.height ? { height: parseFloat(props.height) } : {}),
554
+ ...(props.x ? { x: parseFloat(props.x) } : {}),
555
+ ...(props.y ? { y: parseFloat(props.y) } : {}),
505
556
  ...(props.deg ? { deg: parseFloat(props.deg) } : {}),
506
557
  ...(props.dx ? { dx: parseFloat(props.dx) } : {}),
507
558
  ...(props.dy ? { dy: parseFloat(props.dy) } : {}),
508
559
  ...(props.factor ? { factor: parseFloat(props.factor) } : {}),
509
560
  ...(props.theme ? { theme: props.theme } : {}),
561
+ ...(meta ? { meta } : {}),
510
562
  style: propsToStyle(props),
511
563
  };
512
564
  if (props.url)
@@ -531,17 +583,32 @@ function parse(src) {
531
583
  j = 2;
532
584
  }
533
585
  Object.assign(props, parseSimpleProps(toks, j));
586
+ const meta = extractNodeMeta(props);
534
587
  return {
535
588
  kind: "node",
536
589
  id,
537
590
  shape: "note",
538
591
  label: (props.label ?? "").replace(/\\n/g, "\n"),
539
592
  theme: props.theme,
593
+ ...(meta ? { meta } : {}),
540
594
  style: propsToStyle(props),
541
595
  ...(props.width ? { width: parseFloat(props.width) } : {}),
542
596
  ...(props.height ? { height: parseFloat(props.height) } : {}),
597
+ ...(props.x ? { x: parseFloat(props.x) } : {}),
598
+ ...(props.y ? { y: parseFloat(props.y) } : {}),
599
+ ...(props.deg ? { deg: parseFloat(props.deg) } : {}),
600
+ ...(props.dx ? { dx: parseFloat(props.dx) } : {}),
601
+ ...(props.dy ? { dy: parseFloat(props.dy) } : {}),
602
+ ...(props.factor ? { factor: parseFloat(props.factor) } : {}),
543
603
  };
544
604
  }
605
+ function extractNodeMeta(props) {
606
+ const meta = {};
607
+ if (props["animation-parent"]) {
608
+ meta.animationParent = props["animation-parent"];
609
+ }
610
+ return Object.keys(meta).length ? meta : undefined;
611
+ }
545
612
  function parseGroup() {
546
613
  const keywordTok = cur();
547
614
  skip();
@@ -579,6 +646,8 @@ function parse(src) {
579
646
  justify: props.justify,
580
647
  theme: props.theme,
581
648
  style: propsToStyle(props),
649
+ x: props.x !== undefined ? parseFloat(props.x) : undefined,
650
+ y: props.y !== undefined ? parseFloat(props.y) : undefined,
582
651
  width: props.width !== undefined ? parseFloat(props.width) : undefined,
583
652
  height: props.height !== undefined ? parseFloat(props.height) : undefined,
584
653
  };
@@ -612,6 +681,8 @@ function parse(src) {
612
681
  to: toTok.value,
613
682
  connector: connector,
614
683
  label: props.label,
684
+ fromAnchor: props["anchor-from"],
685
+ toAnchor: props["anchor-to"],
615
686
  dashed,
616
687
  bidirectional,
617
688
  style: propsToStyle(props),
@@ -750,6 +821,8 @@ function parse(src) {
750
821
  chartType: chartType.replace("-chart", ""),
751
822
  label: props.label ?? props.title,
752
823
  data: { headers, rows },
824
+ x: props.x ? parseFloat(props.x) : undefined,
825
+ y: props.y ? parseFloat(props.y) : undefined,
753
826
  width: props.width ? parseFloat(props.width) : undefined,
754
827
  height: props.height ? parseFloat(props.height) : undefined,
755
828
  theme: props.theme,
@@ -775,6 +848,8 @@ function parse(src) {
775
848
  id,
776
849
  label: props.label ?? "",
777
850
  rows: [],
851
+ x: props.x ? parseFloat(props.x) : undefined,
852
+ y: props.y ? parseFloat(props.y) : undefined,
778
853
  theme: props.theme,
779
854
  style: propsToStyle(props),
780
855
  };
@@ -830,6 +905,8 @@ function parse(src) {
830
905
  kind: "markdown",
831
906
  id,
832
907
  content: content.trim(),
908
+ x: props.x ? parseFloat(props.x) : undefined,
909
+ y: props.y ? parseFloat(props.y) : undefined,
833
910
  width: props.width ? parseFloat(props.width) : undefined,
834
911
  height: props.height ? parseFloat(props.height) : undefined,
835
912
  theme: props.theme,
@@ -919,6 +996,7 @@ function parse(src) {
919
996
  registerAuthoredId(grp.id, "group", t);
920
997
  if (isBare) {
921
998
  grp.label = "";
999
+ grp.padding = grp.padding ?? 0;
922
1000
  grp.style = {
923
1001
  ...grp.style,
924
1002
  fill: grp.style?.fill ?? "none",
@@ -1089,7 +1167,7 @@ function parse(src) {
1089
1167
  node.style = { ...ast.styles[node.id], ...node.style };
1090
1168
  }
1091
1169
  }
1092
- return ast;
1170
+ return applyPluginAstTransforms(ast, options.plugins);
1093
1171
  }
1094
1172
 
1095
1173
  // ============================================================
@@ -3438,6 +3516,8 @@ function buildSceneGraph(ast) {
3438
3516
  groupId: nodeParentById.get(n.id),
3439
3517
  width: n.width,
3440
3518
  height: n.height,
3519
+ authoredX: n.x,
3520
+ authoredY: n.y,
3441
3521
  deg: n.deg,
3442
3522
  dx: n.dx,
3443
3523
  dy: n.dy,
@@ -3466,6 +3546,8 @@ function buildSceneGraph(ast) {
3466
3546
  align: (g.align ?? "start"),
3467
3547
  justify: (g.justify ?? "start"),
3468
3548
  style: { ...ast.styles[g.id], ...themeStyle, ...g.style },
3549
+ authoredX: g.x,
3550
+ authoredY: g.y,
3469
3551
  width: g.width,
3470
3552
  height: g.height,
3471
3553
  x: 0,
@@ -3485,6 +3567,8 @@ function buildSceneGraph(ast) {
3485
3567
  headerH: TABLE.headerH,
3486
3568
  labelH: TABLE.labelH,
3487
3569
  style: { ...ast.styles[t.id], ...themeStyle, ...t.style },
3570
+ authoredX: t.x,
3571
+ authoredY: t.y,
3488
3572
  x: 0,
3489
3573
  y: 0,
3490
3574
  w: 0,
@@ -3499,6 +3583,8 @@ function buildSceneGraph(ast) {
3499
3583
  label: c.label,
3500
3584
  data: c.data,
3501
3585
  style: { ...ast.styles[c.id], ...themeStyle, ...c.style },
3586
+ authoredX: c.x,
3587
+ authoredY: c.y,
3502
3588
  x: 0,
3503
3589
  y: 0,
3504
3590
  w: c.width ?? CHART.defaultW,
@@ -3514,6 +3600,8 @@ function buildSceneGraph(ast) {
3514
3600
  style: { ...ast.styles[m.id], ...themeStyle, ...m.style },
3515
3601
  width: m.width,
3516
3602
  height: m.height,
3603
+ authoredX: m.x,
3604
+ authoredY: m.y,
3517
3605
  x: 0,
3518
3606
  y: 0,
3519
3607
  w: 0,
@@ -3526,6 +3614,8 @@ function buildSceneGraph(ast) {
3526
3614
  to: e.to,
3527
3615
  connector: e.connector,
3528
3616
  label: e.label,
3617
+ fromAnchor: e.fromAnchor,
3618
+ toAnchor: e.toAnchor,
3529
3619
  dashed: e.dashed ?? false,
3530
3620
  bidirectional: e.bidirectional ?? false,
3531
3621
  style: e.style ?? {},
@@ -4103,28 +4193,13 @@ function connMeta(connector) {
4103
4193
  return { arrowAt: "start", dashed };
4104
4194
  return { arrowAt: "end", dashed };
4105
4195
  }
4106
- // ── Generic rect connection point ────────────────────────────────────────
4107
- function rectConnPoint$1(rx, ry, rw, rh, ox, oy) {
4108
- const cx = rx + rw / 2, cy = ry + rh / 2;
4109
- const dx = ox - cx, dy = oy - cy;
4110
- if (Math.abs(dx) < 0.01 && Math.abs(dy) < 0.01)
4111
- return [cx, cy];
4112
- const hw = rw / 2 - 2, hh = rh / 2 - 2;
4113
- const tx = Math.abs(dx) > 0.01 ? hw / Math.abs(dx) : 1e9;
4114
- const ty = Math.abs(dy) > 0.01 ? hh / Math.abs(dy) : 1e9;
4115
- const t = Math.min(tx, ty);
4116
- return [cx + t * dx, cy + t * dy];
4117
- }
4118
4196
  // ── Resolve an endpoint entity by ID across all maps ─────────────────────
4119
4197
  function resolveEndpoint(id, nm, tm, gm, cm) {
4120
4198
  return nm.get(id) ?? tm.get(id) ?? gm.get(id) ?? cm.get(id) ?? null;
4121
4199
  }
4122
4200
  // ── Get connection point for any entity ──────────────────────────────────
4123
- function getConnPoint(src, dstCX, dstCY) {
4124
- if ("shape" in src && src.shape) {
4125
- return connPoint(src, { x: dstCX - 1, y: dstCY - 1, w: 2, h: 2});
4126
- }
4127
- return rectConnPoint$1(src.x, src.y, src.w, src.h, dstCX, dstCY);
4201
+ function getConnPoint(src, dstCX, dstCY, anchor) {
4202
+ return anchoredConnPoint(src, anchor, dstCX, dstCY);
4128
4203
  }
4129
4204
  // ── Group depth (for paint order) ────────────────────────────────────────
4130
4205
  function groupDepth(g, gm) {
@@ -4361,6 +4436,12 @@ function iW(r, em) {
4361
4436
  function iH(r, em) {
4362
4437
  return em.get(r.id).h;
4363
4438
  }
4439
+ function iAuthX(r, em) {
4440
+ return em.get(r.id).authoredX ?? 0;
4441
+ }
4442
+ function iAuthY(r, em) {
4443
+ return em.get(r.id).authoredY ?? 0;
4444
+ }
4364
4445
  function setPos(r, x, y, em) {
4365
4446
  const e = em.get(r.id);
4366
4447
  e.x = Math.round(x);
@@ -4407,6 +4488,12 @@ function measure(g, gm, tm, cm, mdm, em) {
4407
4488
  g.w = cols * cellW + (cols - 1) * gap + pad * 2;
4408
4489
  g.h = rows * cellH + (rows - 1) * gap + pad * 2 + labelH;
4409
4490
  }
4491
+ else if (layout === "absolute") {
4492
+ const maxRight = Math.max(0, ...kids.map((r) => iAuthX(r, em) + iW(r, em)));
4493
+ const maxBottom = Math.max(0, ...kids.map((r) => iAuthY(r, em) + iH(r, em)));
4494
+ g.w = maxRight + pad * 2;
4495
+ g.h = maxBottom + pad * 2 + labelH;
4496
+ }
4410
4497
  else {
4411
4498
  // column (default)
4412
4499
  g.w = Math.max(...ws) + pad * 2;
@@ -4497,6 +4584,11 @@ function place(g, gm, em) {
4497
4584
  setPos(ref, contentX + (i % cols) * (cellW + gap), contentY + Math.floor(i / cols) * (cellH + gap), em);
4498
4585
  });
4499
4586
  }
4587
+ else if (layout === "absolute") {
4588
+ kids.forEach((ref) => {
4589
+ setPos(ref, contentX + iAuthX(ref, em), contentY + iAuthY(ref, em), em);
4590
+ });
4591
+ }
4500
4592
  else {
4501
4593
  // column (default)
4502
4594
  const ws = kids.map((r) => iW(r, em));
@@ -4542,6 +4634,50 @@ function connPoint(n, other) {
4542
4634
  const t = Math.min(tx, ty);
4543
4635
  return [cx + t * dx, cy + t * dy];
4544
4636
  }
4637
+ function clampInset(value) {
4638
+ return Math.max(2, value);
4639
+ }
4640
+ function anchoredConnPoint(entity, anchor, otherCX, otherCY) {
4641
+ if (!anchor) {
4642
+ if (entity.shape && otherCX != null && otherCY != null) {
4643
+ return connPoint(entity, { x: otherCX - 1, y: otherCY - 1, w: 2, h: 2});
4644
+ }
4645
+ if (otherCX != null && otherCY != null) {
4646
+ return rectConnPoint(entity.x, entity.y, entity.w, entity.h, otherCX, otherCY);
4647
+ }
4648
+ return [entity.x + entity.w / 2, entity.y + entity.h / 2];
4649
+ }
4650
+ const insetX = clampInset(Math.min(10, entity.w / 2));
4651
+ const insetY = clampInset(Math.min(10, entity.h / 2));
4652
+ const left = entity.x + insetX;
4653
+ const right = entity.x + entity.w - insetX;
4654
+ const top = entity.y + insetY;
4655
+ const bottom = entity.y + entity.h - insetY;
4656
+ const cx = entity.x + entity.w / 2;
4657
+ const cy = entity.y + entity.h / 2;
4658
+ switch (anchor) {
4659
+ case "top":
4660
+ return [cx, top];
4661
+ case "right":
4662
+ return [right, cy];
4663
+ case "bottom":
4664
+ return [cx, bottom];
4665
+ case "left":
4666
+ return [left, cy];
4667
+ case "center":
4668
+ return [cx, cy];
4669
+ case "top-left":
4670
+ return [left, top];
4671
+ case "top-right":
4672
+ return [right, top];
4673
+ case "bottom-left":
4674
+ return [left, bottom];
4675
+ case "bottom-right":
4676
+ return [right, bottom];
4677
+ default:
4678
+ return [cx, cy];
4679
+ }
4680
+ }
4545
4681
  function rectConnPoint(rx, ry, rw, rh, ox, oy) {
4546
4682
  const cx = rx + rw / 2, cy = ry + rh / 2;
4547
4683
  const dx = ox - cx, dy = oy - cy;
@@ -4573,17 +4709,6 @@ function routeEdges(sg) {
4573
4709
  return c;
4574
4710
  return null;
4575
4711
  }
4576
- function connPt(src, dstCX, dstCY) {
4577
- // SceneNode has a .shape field; use the existing connPoint for it
4578
- if ("shape" in src && src.shape) {
4579
- return connPoint(src, {
4580
- x: dstCX - 1,
4581
- y: dstCY - 1,
4582
- w: 2,
4583
- h: 2});
4584
- }
4585
- return rectConnPoint(src.x, src.y, src.w, src.h, dstCX, dstCY);
4586
- }
4587
4712
  for (const e of sg.edges) {
4588
4713
  const src = resolve(e.from);
4589
4714
  const dst = resolve(e.to);
@@ -4593,7 +4718,10 @@ function routeEdges(sg) {
4593
4718
  }
4594
4719
  const dstCX = dst.x + dst.w / 2, dstCY = dst.y + dst.h / 2;
4595
4720
  const srcCX = src.x + src.w / 2, srcCY = src.y + src.h / 2;
4596
- e.points = [connPt(src, dstCX, dstCY), connPt(dst, srcCX, srcCY)];
4721
+ e.points = [
4722
+ anchoredConnPoint(src, e.fromAnchor, dstCX, dstCY),
4723
+ anchoredConnPoint(dst, e.toAnchor, srcCX, srcCY),
4724
+ ];
4597
4725
  }
4598
4726
  }
4599
4727
  function computeBounds(sg, margin) {
@@ -4679,6 +4807,7 @@ function layout(sg) {
4679
4807
  const rootCols = Number(sg.config["columns"] ?? 1);
4680
4808
  const useGrid = rootLayout === "grid" && rootCols > 0;
4681
4809
  const useColumn = rootLayout === "column";
4810
+ const useAbsolute = rootLayout === "absolute";
4682
4811
  if (useGrid) {
4683
4812
  // ── Grid: per-row heights, per-column widths (no wasted space) ──
4684
4813
  const cols = rootCols;
@@ -4710,6 +4839,13 @@ function layout(sg) {
4710
4839
  e.y = rowY[Math.floor(idx / cols)];
4711
4840
  });
4712
4841
  }
4842
+ else if (useAbsolute) {
4843
+ for (const ref of rootOrder) {
4844
+ const e = em.get(ref.id);
4845
+ e.x = MARGIN + (e.authoredX ?? 0);
4846
+ e.y = MARGIN + (e.authoredY ?? 0);
4847
+ }
4848
+ }
4713
4849
  else {
4714
4850
  // ── Row or Column linear flow ──────────────────────────
4715
4851
  let pos = MARGIN;
@@ -7726,8 +7862,8 @@ function renderToSVG(sg, container, options = {}) {
7726
7862
  continue;
7727
7863
  const dstCX = dst.x + dst.w / 2, dstCY = dst.y + dst.h / 2;
7728
7864
  const srcCX = src.x + src.w / 2, srcCY = src.y + src.h / 2;
7729
- const [x1, y1] = getConnPoint(src, dstCX, dstCY);
7730
- const [x2, y2] = getConnPoint(dst, srcCX, srcCY);
7865
+ const [x1, y1] = getConnPoint(src, dstCX, dstCY, e.fromAnchor);
7866
+ const [x2, y2] = getConnPoint(dst, srcCX, srcCY, e.toAnchor);
7731
7867
  const eg = mkGroup(`edge-${e.from}-${e.to}`, "eg");
7732
7868
  if (e.style?.opacity != null)
7733
7869
  eg.setAttribute("opacity", String(e.style.opacity));
@@ -7804,6 +7940,8 @@ function renderToSVG(sg, container, options = {}) {
7804
7940
  ng.dataset.h = String(n.h);
7805
7941
  if (n.pathData)
7806
7942
  ng.dataset.pathData = n.pathData;
7943
+ if (n.meta?.animationParent)
7944
+ ng.dataset.animationParent = n.meta.animationParent;
7807
7945
  if (n.style?.opacity != null)
7808
7946
  ng.setAttribute("opacity", String(n.style.opacity));
7809
7947
  // ── Static transform (deg, dx, dy, factor) ──────────
@@ -8457,8 +8595,8 @@ function renderToCanvas(sg, canvas, options = {}) {
8457
8595
  continue;
8458
8596
  const dstCX = dst.x + dst.w / 2, dstCY = dst.y + dst.h / 2;
8459
8597
  const srcCX = src.x + src.w / 2, srcCY = src.y + src.h / 2;
8460
- const [x1, y1] = getConnPoint(src, dstCX, dstCY);
8461
- const [x2, y2] = getConnPoint(dst, srcCX, srcCY);
8598
+ const [x1, y1] = getConnPoint(src, dstCX, dstCY, e.fromAnchor);
8599
+ const [x2, y2] = getConnPoint(dst, srcCX, srcCY, e.toAnchor);
8462
8600
  if (e.style?.opacity != null)
8463
8601
  ctx.globalAlpha = Number(e.style.opacity);
8464
8602
  const ecol = String(e.style?.stroke ?? palette.edgeStroke);
@@ -9315,6 +9453,13 @@ class AnimationController {
9315
9453
  this.drawTargetNodes.delete(`node-${s.target}`);
9316
9454
  }
9317
9455
  }
9456
+ this._relatedElementIdsByPrimaryId = this._buildRelatedElementIndex();
9457
+ for (const nodeId of Array.from(this.drawTargetNodes)) {
9458
+ const relatedIds = this._relatedElementIdsByPrimaryId.get(nodeId);
9459
+ if (!relatedIds)
9460
+ continue;
9461
+ relatedIds.forEach((id) => this.drawTargetNodes.add(id));
9462
+ }
9318
9463
  this._drawStepIndexByElementId = this._buildDrawStepIndex();
9319
9464
  const { parentGroupByElementId, groupDescendantIds } = this._buildGroupVisibilityIndex();
9320
9465
  this._parentGroupByElementId = parentGroupByElementId;
@@ -9345,10 +9490,30 @@ class AnimationController {
9345
9490
  const el = resolveNonEdgeDrawEl(this.svg, step.target);
9346
9491
  if (el && !drawStepIndexByElementId.has(el.id)) {
9347
9492
  drawStepIndexByElementId.set(el.id, stepIndex);
9493
+ this._relatedElementIdsByPrimaryId.get(el.id)?.forEach((relatedId) => {
9494
+ if (!drawStepIndexByElementId.has(relatedId)) {
9495
+ drawStepIndexByElementId.set(relatedId, stepIndex);
9496
+ }
9497
+ });
9348
9498
  }
9349
9499
  });
9350
9500
  return drawStepIndexByElementId;
9351
9501
  }
9502
+ _buildRelatedElementIndex() {
9503
+ const relatedElementIdsByPrimaryId = new Map();
9504
+ this.svg.querySelectorAll(POSITIONABLE_SELECTOR).forEach((el) => {
9505
+ const animationParent = el.dataset.animationParent;
9506
+ if (!animationParent)
9507
+ return;
9508
+ const primaryEl = resolveNonEdgeDrawEl(this.svg, animationParent);
9509
+ if (!primaryEl || primaryEl.id === el.id)
9510
+ return;
9511
+ const related = relatedElementIdsByPrimaryId.get(primaryEl.id) ?? new Set();
9512
+ related.add(el.id);
9513
+ relatedElementIdsByPrimaryId.set(primaryEl.id, related);
9514
+ });
9515
+ return relatedElementIdsByPrimaryId;
9516
+ }
9352
9517
  _buildGroupVisibilityIndex() {
9353
9518
  const parentGroupByElementId = new Map();
9354
9519
  const directChildIdsByGroup = new Map();
@@ -9427,10 +9592,18 @@ class AnimationController {
9427
9592
  const el = resolveEl(this.svg, target);
9428
9593
  if (!el)
9429
9594
  return [];
9430
- if (!el.id.startsWith("group-"))
9431
- return [el];
9595
+ if (!el.id.startsWith("group-")) {
9596
+ const ids = new Set([el.id]);
9597
+ this._relatedElementIdsByPrimaryId.get(el.id)?.forEach((id) => ids.add(id));
9598
+ return Array.from(ids)
9599
+ .map((id) => getEl(this.svg, id))
9600
+ .filter((candidate) => candidate != null);
9601
+ }
9432
9602
  const ids = new Set([el.id]);
9433
9603
  this._groupDescendantIds.get(el.id)?.forEach((id) => ids.add(id));
9604
+ Array.from(ids).forEach((id) => {
9605
+ this._relatedElementIdsByPrimaryId.get(id)?.forEach((relatedId) => ids.add(relatedId));
9606
+ });
9434
9607
  return Array.from(ids)
9435
9608
  .map((id) => getEl(this.svg, id))
9436
9609
  .filter((candidate) => candidate != null);
@@ -9839,9 +10012,11 @@ class AnimationController {
9839
10012
  // ── highlight ────────────────────────────────────────────
9840
10013
  _doHighlight(target) {
9841
10014
  this.svg
9842
- .querySelectorAll(".ng.hl, .tg.hl, .ntg.hl, .cg.hl, .eg.hl")
10015
+ .querySelectorAll(".ng.hl, .gg.hl, .tg.hl, .ntg.hl, .cg.hl, .mdg.hl, .eg.hl")
9843
10016
  .forEach((e) => e.classList.remove("hl"));
9844
- resolveEl(this.svg, target)?.classList.add("hl");
10017
+ for (const el of this._resolveCascadeTargets(target)) {
10018
+ el.classList.add("hl");
10019
+ }
9845
10020
  }
9846
10021
  // ── fade / unfade ─────────────────────────────────────────
9847
10022
  _doFade(target, doFade) {
@@ -9877,8 +10052,8 @@ class AnimationController {
9877
10052
  }
9878
10053
  // ── move ──────────────────────────────────────────────────
9879
10054
  _doMove(target, step, silent) {
9880
- const el = resolveEl(this.svg, target);
9881
- if (!el)
10055
+ const targets = this._resolveCascadeTargets(target);
10056
+ if (!targets.length)
9882
10057
  return;
9883
10058
  const cur = this._transforms.get(target) ?? {
9884
10059
  tx: 0,
@@ -9891,12 +10066,14 @@ class AnimationController {
9891
10066
  tx: cur.tx + (step.dx ?? 0),
9892
10067
  ty: cur.ty + (step.dy ?? 0),
9893
10068
  });
9894
- this._writeTransform(el, target, silent, step.duration ?? 420);
10069
+ for (const el of targets) {
10070
+ this._writeTransform(el, target, silent, step.duration ?? 420);
10071
+ }
9895
10072
  }
9896
10073
  // ── scale ─────────────────────────────────────────────────
9897
10074
  _doScale(target, step, silent) {
9898
- const el = resolveEl(this.svg, target);
9899
- if (!el)
10075
+ const targets = this._resolveCascadeTargets(target);
10076
+ if (!targets.length)
9900
10077
  return;
9901
10078
  const cur = this._transforms.get(target) ?? {
9902
10079
  tx: 0,
@@ -9905,12 +10082,14 @@ class AnimationController {
9905
10082
  rotate: 0,
9906
10083
  };
9907
10084
  this._transforms.set(target, { ...cur, scale: step.factor ?? 1 });
9908
- this._writeTransform(el, target, silent, step.duration ?? 350);
10085
+ for (const el of targets) {
10086
+ this._writeTransform(el, target, silent, step.duration ?? 350);
10087
+ }
9909
10088
  }
9910
10089
  // ── rotate ────────────────────────────────────────────────
9911
10090
  _doRotate(target, step, silent) {
9912
- const el = resolveEl(this.svg, target);
9913
- if (!el)
10091
+ const targets = this._resolveCascadeTargets(target);
10092
+ if (!targets.length)
9914
10093
  return;
9915
10094
  const cur = this._transforms.get(target) ?? {
9916
10095
  tx: 0,
@@ -9922,7 +10101,9 @@ class AnimationController {
9922
10101
  ...cur,
9923
10102
  rotate: cur.rotate + (step.deg ?? 0),
9924
10103
  });
9925
- this._writeTransform(el, target, silent, step.duration ?? 400);
10104
+ for (const el of targets) {
10105
+ this._writeTransform(el, target, silent, step.duration ?? 400);
10106
+ }
9926
10107
  }
9927
10108
  _doDraw(step, silent) {
9928
10109
  const { target } = step;
@@ -10066,18 +10247,20 @@ class AnimationController {
10066
10247
  return;
10067
10248
  }
10068
10249
  // ── Node draw ──────────────────────────────────────
10069
- const nodeEl = getNodeEl(this.svg, target);
10070
- if (!nodeEl)
10250
+ const nodeEls = this._resolveCascadeTargets(target).filter((el) => el.classList.contains("ng"));
10251
+ if (!nodeEls.length)
10071
10252
  return;
10072
- showDrawEl(nodeEl);
10073
- if (silent) {
10074
- revealNodeInstant(nodeEl);
10075
- }
10076
- else {
10077
- if (!nodeGuidePathEl(nodeEl) && !nodeEl.querySelector("path")?.style.strokeDasharray) {
10078
- prepareNodeForDraw(nodeEl);
10253
+ for (const nodeEl of nodeEls) {
10254
+ showDrawEl(nodeEl);
10255
+ if (silent) {
10256
+ revealNodeInstant(nodeEl);
10257
+ }
10258
+ else {
10259
+ if (!nodeGuidePathEl(nodeEl) && !nodeEl.querySelector("path")?.style.strokeDasharray) {
10260
+ prepareNodeForDraw(nodeEl);
10261
+ }
10262
+ animateNodeDraw(nodeEl, step.duration ?? ANIMATION.nodeStrokeDur, step.duration ?? ANIMATION.textRevealMs);
10079
10263
  }
10080
- animateNodeDraw(nodeEl, step.duration ?? ANIMATION.nodeStrokeDur, step.duration ?? ANIMATION.textRevealMs);
10081
10264
  }
10082
10265
  }
10083
10266
  // ── erase ─────────────────────────────────────────────────
@@ -10098,45 +10281,44 @@ class AnimationController {
10098
10281
  }
10099
10282
  // ── pulse ─────────────────────────────────────────────────
10100
10283
  _doPulse(target, duration = 500) {
10101
- resolveEl(this.svg, target)?.animate([
10102
- { filter: "brightness(1)" },
10103
- { filter: "brightness(1.6)" },
10104
- { filter: "brightness(1)" },
10105
- ], { duration, iterations: 3 });
10284
+ for (const el of this._resolveCascadeTargets(target)) {
10285
+ el.animate([
10286
+ { filter: "brightness(1)" },
10287
+ { filter: "brightness(1.6)" },
10288
+ { filter: "brightness(1)" },
10289
+ ], { duration, iterations: 3 });
10290
+ }
10106
10291
  }
10107
10292
  // ── color ─────────────────────────────────────────────────
10108
10293
  _doColor(target, color) {
10109
10294
  if (!color)
10110
10295
  return;
10111
- const el = resolveEl(this.svg, target);
10112
- if (!el)
10113
- return;
10114
- // edge color stroke
10115
- if (parseEdgeTarget(target)) {
10116
- el.querySelectorAll("path, line, polyline").forEach((p) => {
10117
- p.style.stroke = color;
10118
- });
10119
- el.querySelectorAll("polygon").forEach((p) => {
10120
- p.style.fill = color;
10121
- p.style.stroke = color;
10122
- });
10123
- return;
10124
- }
10125
- // everything else — color fill
10126
- let hit = false;
10127
- el.querySelectorAll("path, rect, ellipse, polygon").forEach((c) => {
10128
- const attrFill = c.getAttribute("fill");
10129
- if (attrFill === "none")
10130
- return;
10131
- if (attrFill === null && c.tagName === "path")
10132
- return;
10133
- c.style.fill = color;
10134
- hit = true;
10135
- });
10136
- if (!hit) {
10137
- el.querySelectorAll("text").forEach((t) => {
10138
- t.style.fill = color;
10296
+ for (const el of this._resolveCascadeTargets(target)) {
10297
+ if (parseEdgeTarget(target)) {
10298
+ el.querySelectorAll("path, line, polyline").forEach((p) => {
10299
+ p.style.stroke = color;
10300
+ });
10301
+ el.querySelectorAll("polygon").forEach((p) => {
10302
+ p.style.fill = color;
10303
+ p.style.stroke = color;
10304
+ });
10305
+ continue;
10306
+ }
10307
+ let hit = false;
10308
+ el.querySelectorAll("path, rect, ellipse, polygon").forEach((c) => {
10309
+ const attrFill = c.getAttribute("fill");
10310
+ if (attrFill === "none")
10311
+ return;
10312
+ if (attrFill === null && c.tagName === "path")
10313
+ return;
10314
+ c.style.fill = color;
10315
+ hit = true;
10139
10316
  });
10317
+ if (!hit) {
10318
+ el.querySelectorAll("text").forEach((t) => {
10319
+ t.style.fill = color;
10320
+ });
10321
+ }
10140
10322
  }
10141
10323
  }
10142
10324
  // ── narration ───────────────────────────────────────────
@@ -10730,7 +10912,7 @@ class EventEmitter {
10730
10912
  }
10731
10913
 
10732
10914
  function render(options) {
10733
- const { container: rawContainer, dsl, renderer = "svg", injectCSS = true, tts, svgOptions = {}, canvasOptions = {}, onNodeClick, onReady, } = options;
10915
+ const { container: rawContainer, dsl, plugins, renderer = "svg", injectCSS = true, tts, svgOptions = {}, canvasOptions = {}, onNodeClick, onReady, } = options;
10734
10916
  if (injectCSS && !document.getElementById("ai-diagram-css")) {
10735
10917
  const style = document.createElement("style");
10736
10918
  style.id = "ai-diagram-css";
@@ -10746,7 +10928,7 @@ function render(options) {
10746
10928
  else {
10747
10929
  el = rawContainer;
10748
10930
  }
10749
- const ast = parse(dsl);
10931
+ const ast = parse(dsl, { plugins });
10750
10932
  const scene = buildSceneGraph(ast);
10751
10933
  layout(scene);
10752
10934
  let svg;
@@ -11090,6 +11272,7 @@ class SketchmarkCanvas {
11090
11272
  const instance = render({
11091
11273
  container: this.diagramWrap,
11092
11274
  dsl: this.dsl,
11275
+ plugins: this.options.plugins,
11093
11276
  renderer: this.renderer,
11094
11277
  svgOptions: { interactive: true, showTitle: true, theme: this.options.svgOptions?.theme ?? this.theme, ...this.options.svgOptions },
11095
11278
  canvasOptions: this.options.canvasOptions,
@@ -12167,6 +12350,7 @@ class SketchmarkEmbed {
12167
12350
  const instance = render({
12168
12351
  container: this.diagramWrap,
12169
12352
  dsl: this.dsl,
12353
+ plugins: this.options.plugins,
12170
12354
  renderer: "svg",
12171
12355
  svgOptions: {
12172
12356
  showTitle: true,