sketchmark 1.1.6 → 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.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { parse, ParseError } from "./parser";
2
2
  export type { DiagramAST } from "./parser";
3
+ export type { ParseOptions, SketchmarkPlugin } from "./plugins";
3
4
  export { buildSceneGraph, nodeMap, groupMap, markdownMap } from "./scene";
4
5
  export type { SceneGraph, SceneNode, SceneEdge, SceneGroup, SceneMarkdown, } from "./scene";
5
6
  export { layout, connPoint } from "./layout";
@@ -11,7 +12,7 @@ export { AnimationController, ANIMATION_CSS } from "./animation";
11
12
  export type { AnimationEvent, AnimationEventType } from "./animation";
12
13
  export { exportSVG, exportPNG, exportCanvasPNG, exportHTML, exportGIF, exportMP4, getSVGBlob, svgToPNGDataURL, } from "./export";
13
14
  export type { ExportFormat, ExportOptions } from "./export";
14
- export type { NodeShape, EdgeConnector, LayoutType, AlignItems, JustifyContent, AnimationAction, AnimationTrigger, StyleProps, StepPace, ASTNode, ASTEdge, ASTGroup, ASTStep, ASTBeat, ASTStepItem, ASTChart, ASTTable, GroupChildRef, RootItemRef, ASTMarkdown, } from "./ast/types";
15
+ export type { NodeShape, EdgeConnector, EdgeAnchor, LayoutType, AlignItems, JustifyContent, AnimationAction, AnimationTrigger, StyleProps, StepPace, ASTNode, ASTEdge, ASTGroup, ASTStep, ASTBeat, ASTStepItem, ASTChart, ASTTable, GroupChildRef, RootItemRef, ASTMarkdown, } from "./ast/types";
15
16
  export { hashStr, clamp, lerp, parseHex, sleep, throttle, debounce, EventEmitter, } from "./utils";
16
17
  export { render } from "./render";
17
18
  export type { RenderOptions, DiagramInstance } from "./render";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC7C,YAAY,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,EAAE,eAAe,EAAE,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC1E,YAAY,EACV,UAAU,EACV,SAAS,EACT,SAAS,EACT,UAAU,EACV,aAAa,GACd,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAG7C,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC1D,YAAY,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AACzD,OAAO,EACL,cAAc,EACd,eAAe,EACf,kBAAkB,GACnB,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAG/D,OAAO,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjE,YAAY,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAGtE,OAAO,EACL,SAAS,EACT,SAAS,EACT,eAAe,EACf,UAAU,EACV,SAAS,EACT,SAAS,EACT,UAAU,EACV,eAAe,GAChB,MAAM,UAAU,CAAC;AAClB,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAG5D,YAAY,EACV,SAAS,EACT,aAAa,EACb,UAAU,EACV,UAAU,EACV,cAAc,EACd,eAAe,EACf,gBAAgB,EAChB,UAAU,EACV,QAAQ,EACR,OAAO,EACP,OAAO,EACP,QAAQ,EACR,OAAO,EACP,OAAO,EACP,WAAW,EACX,QAAQ,EACR,QAAQ,EACR,aAAa,EACb,WAAW,EACX,WAAW,GACZ,MAAM,aAAa,CAAC;AAGrB,OAAO,EACL,OAAO,EACP,KAAK,EACL,IAAI,EACJ,QAAQ,EACR,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,YAAY,GACb,MAAM,SAAS,CAAC;AAGjB,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,YAAY,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAG/D,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/C,YAAY,EACV,uBAAuB,EACvB,sBAAsB,EACtB,iCAAiC,GAClC,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/C,YAAY,EACV,uBAAuB,EACvB,sBAAsB,GACvB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAC7C,YAAY,EACV,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,YAAY,CAAC;AAGpB,OAAO,EACL,QAAQ,EACR,cAAc,EACd,gBAAgB,EAChB,UAAU,EACV,WAAW,GACZ,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC7C,YAAY,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC3C,YAAY,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAChE,OAAO,EAAE,eAAe,EAAE,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC1E,YAAY,EACV,UAAU,EACV,SAAS,EACT,SAAS,EACT,UAAU,EACV,aAAa,GACd,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAG7C,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC1D,YAAY,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AACzD,OAAO,EACL,cAAc,EACd,eAAe,EACf,kBAAkB,GACnB,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAG/D,OAAO,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjE,YAAY,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAGtE,OAAO,EACL,SAAS,EACT,SAAS,EACT,eAAe,EACf,UAAU,EACV,SAAS,EACT,SAAS,EACT,UAAU,EACV,eAAe,GAChB,MAAM,UAAU,CAAC;AAClB,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAG5D,YAAY,EACV,SAAS,EACT,aAAa,EACb,UAAU,EACV,UAAU,EACV,UAAU,EACV,cAAc,EACd,eAAe,EACf,gBAAgB,EAChB,UAAU,EACV,QAAQ,EACR,OAAO,EACP,OAAO,EACP,QAAQ,EACR,OAAO,EACP,OAAO,EACP,WAAW,EACX,QAAQ,EACR,QAAQ,EACR,aAAa,EACb,WAAW,EACX,WAAW,GACZ,MAAM,aAAa,CAAC;AAGrB,OAAO,EACL,OAAO,EACP,KAAK,EACL,IAAI,EACJ,QAAQ,EACR,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,YAAY,GACb,MAAM,SAAS,CAAC;AAGjB,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,YAAY,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAG/D,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/C,YAAY,EACV,uBAAuB,EACvB,sBAAsB,EACtB,iCAAiC,GAClC,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/C,YAAY,EACV,uBAAuB,EACvB,sBAAsB,GACvB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAC7C,YAAY,EACV,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,YAAY,CAAC;AAGpB,OAAO,EACL,QAAQ,EACR,cAAc,EACd,gBAAgB,EAChB,UAAU,EACV,WAAW,GACZ,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC"}
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,
@@ -509,6 +558,7 @@ function parse(src) {
509
558
  ...(props.dy ? { dy: parseFloat(props.dy) } : {}),
510
559
  ...(props.factor ? { factor: parseFloat(props.factor) } : {}),
511
560
  ...(props.theme ? { theme: props.theme } : {}),
561
+ ...(meta ? { meta } : {}),
512
562
  style: propsToStyle(props),
513
563
  };
514
564
  if (props.url)
@@ -533,12 +583,14 @@ function parse(src) {
533
583
  j = 2;
534
584
  }
535
585
  Object.assign(props, parseSimpleProps(toks, j));
586
+ const meta = extractNodeMeta(props);
536
587
  return {
537
588
  kind: "node",
538
589
  id,
539
590
  shape: "note",
540
591
  label: (props.label ?? "").replace(/\\n/g, "\n"),
541
592
  theme: props.theme,
593
+ ...(meta ? { meta } : {}),
542
594
  style: propsToStyle(props),
543
595
  ...(props.width ? { width: parseFloat(props.width) } : {}),
544
596
  ...(props.height ? { height: parseFloat(props.height) } : {}),
@@ -550,6 +602,13 @@ function parse(src) {
550
602
  ...(props.factor ? { factor: parseFloat(props.factor) } : {}),
551
603
  };
552
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
+ }
553
612
  function parseGroup() {
554
613
  const keywordTok = cur();
555
614
  skip();
@@ -622,6 +681,8 @@ function parse(src) {
622
681
  to: toTok.value,
623
682
  connector: connector,
624
683
  label: props.label,
684
+ fromAnchor: props["anchor-from"],
685
+ toAnchor: props["anchor-to"],
625
686
  dashed,
626
687
  bidirectional,
627
688
  style: propsToStyle(props),
@@ -935,6 +996,7 @@ function parse(src) {
935
996
  registerAuthoredId(grp.id, "group", t);
936
997
  if (isBare) {
937
998
  grp.label = "";
999
+ grp.padding = grp.padding ?? 0;
938
1000
  grp.style = {
939
1001
  ...grp.style,
940
1002
  fill: grp.style?.fill ?? "none",
@@ -1105,7 +1167,7 @@ function parse(src) {
1105
1167
  node.style = { ...ast.styles[node.id], ...node.style };
1106
1168
  }
1107
1169
  }
1108
- return ast;
1170
+ return applyPluginAstTransforms(ast, options.plugins);
1109
1171
  }
1110
1172
 
1111
1173
  // ============================================================
@@ -3552,6 +3614,8 @@ function buildSceneGraph(ast) {
3552
3614
  to: e.to,
3553
3615
  connector: e.connector,
3554
3616
  label: e.label,
3617
+ fromAnchor: e.fromAnchor,
3618
+ toAnchor: e.toAnchor,
3555
3619
  dashed: e.dashed ?? false,
3556
3620
  bidirectional: e.bidirectional ?? false,
3557
3621
  style: e.style ?? {},
@@ -4129,28 +4193,13 @@ function connMeta(connector) {
4129
4193
  return { arrowAt: "start", dashed };
4130
4194
  return { arrowAt: "end", dashed };
4131
4195
  }
4132
- // ── Generic rect connection point ────────────────────────────────────────
4133
- function rectConnPoint$1(rx, ry, rw, rh, ox, oy) {
4134
- const cx = rx + rw / 2, cy = ry + rh / 2;
4135
- const dx = ox - cx, dy = oy - cy;
4136
- if (Math.abs(dx) < 0.01 && Math.abs(dy) < 0.01)
4137
- return [cx, cy];
4138
- const hw = rw / 2 - 2, hh = rh / 2 - 2;
4139
- const tx = Math.abs(dx) > 0.01 ? hw / Math.abs(dx) : 1e9;
4140
- const ty = Math.abs(dy) > 0.01 ? hh / Math.abs(dy) : 1e9;
4141
- const t = Math.min(tx, ty);
4142
- return [cx + t * dx, cy + t * dy];
4143
- }
4144
4196
  // ── Resolve an endpoint entity by ID across all maps ─────────────────────
4145
4197
  function resolveEndpoint(id, nm, tm, gm, cm) {
4146
4198
  return nm.get(id) ?? tm.get(id) ?? gm.get(id) ?? cm.get(id) ?? null;
4147
4199
  }
4148
4200
  // ── Get connection point for any entity ──────────────────────────────────
4149
- function getConnPoint(src, dstCX, dstCY) {
4150
- if ("shape" in src && src.shape) {
4151
- return connPoint(src, { x: dstCX - 1, y: dstCY - 1, w: 2, h: 2});
4152
- }
4153
- 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);
4154
4203
  }
4155
4204
  // ── Group depth (for paint order) ────────────────────────────────────────
4156
4205
  function groupDepth(g, gm) {
@@ -4585,6 +4634,50 @@ function connPoint(n, other) {
4585
4634
  const t = Math.min(tx, ty);
4586
4635
  return [cx + t * dx, cy + t * dy];
4587
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
+ }
4588
4681
  function rectConnPoint(rx, ry, rw, rh, ox, oy) {
4589
4682
  const cx = rx + rw / 2, cy = ry + rh / 2;
4590
4683
  const dx = ox - cx, dy = oy - cy;
@@ -4616,17 +4709,6 @@ function routeEdges(sg) {
4616
4709
  return c;
4617
4710
  return null;
4618
4711
  }
4619
- function connPt(src, dstCX, dstCY) {
4620
- // SceneNode has a .shape field; use the existing connPoint for it
4621
- if ("shape" in src && src.shape) {
4622
- return connPoint(src, {
4623
- x: dstCX - 1,
4624
- y: dstCY - 1,
4625
- w: 2,
4626
- h: 2});
4627
- }
4628
- return rectConnPoint(src.x, src.y, src.w, src.h, dstCX, dstCY);
4629
- }
4630
4712
  for (const e of sg.edges) {
4631
4713
  const src = resolve(e.from);
4632
4714
  const dst = resolve(e.to);
@@ -4636,7 +4718,10 @@ function routeEdges(sg) {
4636
4718
  }
4637
4719
  const dstCX = dst.x + dst.w / 2, dstCY = dst.y + dst.h / 2;
4638
4720
  const srcCX = src.x + src.w / 2, srcCY = src.y + src.h / 2;
4639
- 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
+ ];
4640
4725
  }
4641
4726
  }
4642
4727
  function computeBounds(sg, margin) {
@@ -7777,8 +7862,8 @@ function renderToSVG(sg, container, options = {}) {
7777
7862
  continue;
7778
7863
  const dstCX = dst.x + dst.w / 2, dstCY = dst.y + dst.h / 2;
7779
7864
  const srcCX = src.x + src.w / 2, srcCY = src.y + src.h / 2;
7780
- const [x1, y1] = getConnPoint(src, dstCX, dstCY);
7781
- 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);
7782
7867
  const eg = mkGroup(`edge-${e.from}-${e.to}`, "eg");
7783
7868
  if (e.style?.opacity != null)
7784
7869
  eg.setAttribute("opacity", String(e.style.opacity));
@@ -7855,6 +7940,8 @@ function renderToSVG(sg, container, options = {}) {
7855
7940
  ng.dataset.h = String(n.h);
7856
7941
  if (n.pathData)
7857
7942
  ng.dataset.pathData = n.pathData;
7943
+ if (n.meta?.animationParent)
7944
+ ng.dataset.animationParent = n.meta.animationParent;
7858
7945
  if (n.style?.opacity != null)
7859
7946
  ng.setAttribute("opacity", String(n.style.opacity));
7860
7947
  // ── Static transform (deg, dx, dy, factor) ──────────
@@ -8508,8 +8595,8 @@ function renderToCanvas(sg, canvas, options = {}) {
8508
8595
  continue;
8509
8596
  const dstCX = dst.x + dst.w / 2, dstCY = dst.y + dst.h / 2;
8510
8597
  const srcCX = src.x + src.w / 2, srcCY = src.y + src.h / 2;
8511
- const [x1, y1] = getConnPoint(src, dstCX, dstCY);
8512
- 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);
8513
8600
  if (e.style?.opacity != null)
8514
8601
  ctx.globalAlpha = Number(e.style.opacity);
8515
8602
  const ecol = String(e.style?.stroke ?? palette.edgeStroke);
@@ -9366,6 +9453,13 @@ class AnimationController {
9366
9453
  this.drawTargetNodes.delete(`node-${s.target}`);
9367
9454
  }
9368
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
+ }
9369
9463
  this._drawStepIndexByElementId = this._buildDrawStepIndex();
9370
9464
  const { parentGroupByElementId, groupDescendantIds } = this._buildGroupVisibilityIndex();
9371
9465
  this._parentGroupByElementId = parentGroupByElementId;
@@ -9396,10 +9490,30 @@ class AnimationController {
9396
9490
  const el = resolveNonEdgeDrawEl(this.svg, step.target);
9397
9491
  if (el && !drawStepIndexByElementId.has(el.id)) {
9398
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
+ });
9399
9498
  }
9400
9499
  });
9401
9500
  return drawStepIndexByElementId;
9402
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
+ }
9403
9517
  _buildGroupVisibilityIndex() {
9404
9518
  const parentGroupByElementId = new Map();
9405
9519
  const directChildIdsByGroup = new Map();
@@ -9478,10 +9592,18 @@ class AnimationController {
9478
9592
  const el = resolveEl(this.svg, target);
9479
9593
  if (!el)
9480
9594
  return [];
9481
- if (!el.id.startsWith("group-"))
9482
- 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
+ }
9483
9602
  const ids = new Set([el.id]);
9484
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
+ });
9485
9607
  return Array.from(ids)
9486
9608
  .map((id) => getEl(this.svg, id))
9487
9609
  .filter((candidate) => candidate != null);
@@ -9890,9 +10012,11 @@ class AnimationController {
9890
10012
  // ── highlight ────────────────────────────────────────────
9891
10013
  _doHighlight(target) {
9892
10014
  this.svg
9893
- .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")
9894
10016
  .forEach((e) => e.classList.remove("hl"));
9895
- resolveEl(this.svg, target)?.classList.add("hl");
10017
+ for (const el of this._resolveCascadeTargets(target)) {
10018
+ el.classList.add("hl");
10019
+ }
9896
10020
  }
9897
10021
  // ── fade / unfade ─────────────────────────────────────────
9898
10022
  _doFade(target, doFade) {
@@ -9928,8 +10052,8 @@ class AnimationController {
9928
10052
  }
9929
10053
  // ── move ──────────────────────────────────────────────────
9930
10054
  _doMove(target, step, silent) {
9931
- const el = resolveEl(this.svg, target);
9932
- if (!el)
10055
+ const targets = this._resolveCascadeTargets(target);
10056
+ if (!targets.length)
9933
10057
  return;
9934
10058
  const cur = this._transforms.get(target) ?? {
9935
10059
  tx: 0,
@@ -9942,12 +10066,14 @@ class AnimationController {
9942
10066
  tx: cur.tx + (step.dx ?? 0),
9943
10067
  ty: cur.ty + (step.dy ?? 0),
9944
10068
  });
9945
- this._writeTransform(el, target, silent, step.duration ?? 420);
10069
+ for (const el of targets) {
10070
+ this._writeTransform(el, target, silent, step.duration ?? 420);
10071
+ }
9946
10072
  }
9947
10073
  // ── scale ─────────────────────────────────────────────────
9948
10074
  _doScale(target, step, silent) {
9949
- const el = resolveEl(this.svg, target);
9950
- if (!el)
10075
+ const targets = this._resolveCascadeTargets(target);
10076
+ if (!targets.length)
9951
10077
  return;
9952
10078
  const cur = this._transforms.get(target) ?? {
9953
10079
  tx: 0,
@@ -9956,12 +10082,14 @@ class AnimationController {
9956
10082
  rotate: 0,
9957
10083
  };
9958
10084
  this._transforms.set(target, { ...cur, scale: step.factor ?? 1 });
9959
- this._writeTransform(el, target, silent, step.duration ?? 350);
10085
+ for (const el of targets) {
10086
+ this._writeTransform(el, target, silent, step.duration ?? 350);
10087
+ }
9960
10088
  }
9961
10089
  // ── rotate ────────────────────────────────────────────────
9962
10090
  _doRotate(target, step, silent) {
9963
- const el = resolveEl(this.svg, target);
9964
- if (!el)
10091
+ const targets = this._resolveCascadeTargets(target);
10092
+ if (!targets.length)
9965
10093
  return;
9966
10094
  const cur = this._transforms.get(target) ?? {
9967
10095
  tx: 0,
@@ -9973,7 +10101,9 @@ class AnimationController {
9973
10101
  ...cur,
9974
10102
  rotate: cur.rotate + (step.deg ?? 0),
9975
10103
  });
9976
- this._writeTransform(el, target, silent, step.duration ?? 400);
10104
+ for (const el of targets) {
10105
+ this._writeTransform(el, target, silent, step.duration ?? 400);
10106
+ }
9977
10107
  }
9978
10108
  _doDraw(step, silent) {
9979
10109
  const { target } = step;
@@ -10117,18 +10247,20 @@ class AnimationController {
10117
10247
  return;
10118
10248
  }
10119
10249
  // ── Node draw ──────────────────────────────────────
10120
- const nodeEl = getNodeEl(this.svg, target);
10121
- if (!nodeEl)
10250
+ const nodeEls = this._resolveCascadeTargets(target).filter((el) => el.classList.contains("ng"));
10251
+ if (!nodeEls.length)
10122
10252
  return;
10123
- showDrawEl(nodeEl);
10124
- if (silent) {
10125
- revealNodeInstant(nodeEl);
10126
- }
10127
- else {
10128
- if (!nodeGuidePathEl(nodeEl) && !nodeEl.querySelector("path")?.style.strokeDasharray) {
10129
- 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);
10130
10263
  }
10131
- animateNodeDraw(nodeEl, step.duration ?? ANIMATION.nodeStrokeDur, step.duration ?? ANIMATION.textRevealMs);
10132
10264
  }
10133
10265
  }
10134
10266
  // ── erase ─────────────────────────────────────────────────
@@ -10149,45 +10281,44 @@ class AnimationController {
10149
10281
  }
10150
10282
  // ── pulse ─────────────────────────────────────────────────
10151
10283
  _doPulse(target, duration = 500) {
10152
- resolveEl(this.svg, target)?.animate([
10153
- { filter: "brightness(1)" },
10154
- { filter: "brightness(1.6)" },
10155
- { filter: "brightness(1)" },
10156
- ], { 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
+ }
10157
10291
  }
10158
10292
  // ── color ─────────────────────────────────────────────────
10159
10293
  _doColor(target, color) {
10160
10294
  if (!color)
10161
10295
  return;
10162
- const el = resolveEl(this.svg, target);
10163
- if (!el)
10164
- return;
10165
- // edge color stroke
10166
- if (parseEdgeTarget(target)) {
10167
- el.querySelectorAll("path, line, polyline").forEach((p) => {
10168
- p.style.stroke = color;
10169
- });
10170
- el.querySelectorAll("polygon").forEach((p) => {
10171
- p.style.fill = color;
10172
- p.style.stroke = color;
10173
- });
10174
- return;
10175
- }
10176
- // everything else — color fill
10177
- let hit = false;
10178
- el.querySelectorAll("path, rect, ellipse, polygon").forEach((c) => {
10179
- const attrFill = c.getAttribute("fill");
10180
- if (attrFill === "none")
10181
- return;
10182
- if (attrFill === null && c.tagName === "path")
10183
- return;
10184
- c.style.fill = color;
10185
- hit = true;
10186
- });
10187
- if (!hit) {
10188
- el.querySelectorAll("text").forEach((t) => {
10189
- 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;
10190
10316
  });
10317
+ if (!hit) {
10318
+ el.querySelectorAll("text").forEach((t) => {
10319
+ t.style.fill = color;
10320
+ });
10321
+ }
10191
10322
  }
10192
10323
  }
10193
10324
  // ── narration ───────────────────────────────────────────
@@ -10781,7 +10912,7 @@ class EventEmitter {
10781
10912
  }
10782
10913
 
10783
10914
  function render(options) {
10784
- 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;
10785
10916
  if (injectCSS && !document.getElementById("ai-diagram-css")) {
10786
10917
  const style = document.createElement("style");
10787
10918
  style.id = "ai-diagram-css";
@@ -10797,7 +10928,7 @@ function render(options) {
10797
10928
  else {
10798
10929
  el = rawContainer;
10799
10930
  }
10800
- const ast = parse(dsl);
10931
+ const ast = parse(dsl, { plugins });
10801
10932
  const scene = buildSceneGraph(ast);
10802
10933
  layout(scene);
10803
10934
  let svg;
@@ -11141,6 +11272,7 @@ class SketchmarkCanvas {
11141
11272
  const instance = render({
11142
11273
  container: this.diagramWrap,
11143
11274
  dsl: this.dsl,
11275
+ plugins: this.options.plugins,
11144
11276
  renderer: this.renderer,
11145
11277
  svgOptions: { interactive: true, showTitle: true, theme: this.options.svgOptions?.theme ?? this.theme, ...this.options.svgOptions },
11146
11278
  canvasOptions: this.options.canvasOptions,
@@ -12218,6 +12350,7 @@ class SketchmarkEmbed {
12218
12350
  const instance = render({
12219
12351
  container: this.diagramWrap,
12220
12352
  dsl: this.dsl,
12353
+ plugins: this.options.plugins,
12221
12354
  renderer: "svg",
12222
12355
  svgOptions: {
12223
12356
  showTitle: true,