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.

Potentially problematic release.


This version of sketchmark might be problematic. Click here for more details.

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