@markdy/core 1.4.2 → 1.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +3 -1
  2. package/dist/index.js +188 -30
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -4,13 +4,15 @@
4
4
  <a href="https://markdy.com/playground/"><img src="https://img.shields.io/badge/⚡_Live_Studio-markdy.com%2Fplayground-3b82f6?style=for-the-badge" alt="Live Studio" /></a>
5
5
  <a href="https://markdy.com/docs/"><img src="https://img.shields.io/badge/📖_Docs-Documentation-10b981?style=for-the-badge" alt="Documentation" /></a>
6
6
  <a href="https://marketplace.visualstudio.com/items?itemName=hoangyell.markdy-vscode"><img src="https://img.shields.io/badge/🔌_VS_Code-Extension-8b5cf6?style=for-the-badge" alt="VS Code Extension" /></a>
7
+ <a href="https://github.com/sponsors/HoangYell"><img src="https://img.shields.io/badge/💖_Sponsor-Support_Markdy-ea4aaa?style=for-the-badge" alt="Sponsor Markdy" /></a>
7
8
  </p>
8
9
 
9
10
  The zero-dependency parser, dynamic port multiplexer, and AST routing engine for [MarkdyScript](https://markdy.com/docs/) — a diagram-native DSL for animated architecture diagrams that AI agents generate reliably.
10
11
 
11
12
  > 🚀 **Try it live**: Test MarkdyScript in the browser at **[markdy.com/playground](https://markdy.com/playground/)**
12
13
  > 📚 **Documentation**: Complete syntax guide and examples at **[markdy.com/docs](https://markdy.com/docs/)**
13
- > 🌟 **Architecture Blueprints**: 30+ canonical production diagrams at **[markdy.com/examples](https://markdy.com/examples/)**
14
+ > 🌟 **Architecture Blueprints**: 30+ canonical production diagrams at **[markdy.com/examples](https://markdy.com/examples/)**
15
+ > 💼 **Enterprise & Commercial**: Free under MIT. To support development or request custom architecture blueprints, explore **[GitHub Sponsors](https://github.com/sponsors/HoangYell)**.
14
16
 
15
17
  ## Features
16
18
 
package/dist/index.js CHANGED
@@ -521,6 +521,7 @@ var FLAT = {
521
521
  next_beat: CONTROLS.next_beat,
522
522
  keyboard: INTERACTION.keyboard,
523
523
  shortcuts: INTERACTION.shortcuts,
524
+ fit: CONTROLS.fit,
524
525
  fitView: CONTROLS.fitView,
525
526
  fit_view: CONTROLS.fit_view,
526
527
  fitViewButton: CONTROLS.fitViewButton,
@@ -642,6 +643,7 @@ function resolvePlayer(config = {}, overrides = {}) {
642
643
  const chrome = config.chrome ?? {};
643
644
  const controlsAllowed = overrides.controls !== false;
644
645
  const hostControlDefault = overrides.controls === true;
646
+ const hasControlsConfig = config.controls !== void 0 || overrideControls !== void 0;
645
647
  const resolveControl = (value, fallback = hostControlDefault) => controlsAllowed && (value ?? fallback);
646
648
  const requestedControls = {
647
649
  play: resolveControl(configuredControls.play),
@@ -659,11 +661,13 @@ function resolvePlayer(config = {}, overrides = {}) {
659
661
  code: resolveControl(configuredControls.code, false),
660
662
  theme: resolveControl(configuredControls.theme, hostControlDefault)
661
663
  };
662
- const interactionOn = overrides.interactiveViewport !== false && (overrides.interactiveViewport === true || config.interaction !== void 0 || overrides.controls === true);
664
+ const hasExplicitInteraction = config.interaction !== void 0;
665
+ const fallbackGesture = overrides.interactiveViewport === true || overrides.controls === true;
666
+ const interactionOn = overrides.interactiveViewport === true || overrides.controls === true || overrides.interactiveViewport !== false && hasExplicitInteraction;
663
667
  const gestures = {
664
- zoom: interactionOn && (interaction.zoom ?? true),
665
- pan: interactionOn && (interaction.pan ?? true),
666
- doubleClickToReset: interactionOn && (interaction.doubleClickToReset ?? true)
668
+ zoom: interactionOn && (interaction.zoom ?? fallbackGesture),
669
+ pan: interactionOn && (interaction.pan ?? fallbackGesture),
670
+ doubleClickToReset: interactionOn && (interaction.doubleClickToReset ?? fallbackGesture)
667
671
  };
668
672
  const interactionEnabled = gestures.zoom || gestures.pan || gestures.doubleClickToReset;
669
673
  const configuredSpeeds = configuredControls.speeds?.length ? configuredControls.speeds : [0.25, 1];
@@ -692,7 +696,7 @@ function resolvePlayer(config = {}, overrides = {}) {
692
696
  interaction: {
693
697
  ...gestures,
694
698
  enabled: interactionEnabled,
695
- clickToPlay: overrides.clickToPlay ?? (interactionOn && (interaction.clickToPlay ?? true)),
699
+ clickToPlay: overrides.clickToPlay ?? (interaction.clickToPlay ?? true),
696
700
  keyboard: interactionOn && (interaction.keyboard ?? false)
697
701
  },
698
702
  chrome: {
@@ -1023,17 +1027,32 @@ function dedupeEdges(edges) {
1023
1027
  function assignRanks(nodeIds, edges, direction) {
1024
1028
  const ranks = /* @__PURE__ */ new Map();
1025
1029
  for (const id of nodeIds) ranks.set(id, 0);
1026
- const forward = edges.filter((e) => e.kind !== "response" && !e.selfLoop);
1027
- for (let sweep = 0; sweep < nodeIds.length; sweep++) {
1028
- let changed = false;
1029
- for (const e of forward) {
1030
- const next = (ranks.get(e.from) ?? 0) + 1;
1031
- if (next > (ranks.get(e.to) ?? 0)) {
1032
- ranks.set(e.to, next);
1033
- changed = true;
1034
- }
1030
+ const outgoing = new Map(nodeIds.map((id) => [id, []]));
1031
+ const indegree = new Map(nodeIds.map((id) => [id, 0]));
1032
+ for (const edge of edges) {
1033
+ if (edge.kind === "response" || edge.selfLoop || !outgoing.has(edge.from) || !outgoing.has(edge.to)) continue;
1034
+ outgoing.get(edge.from).push(edge.to);
1035
+ indegree.set(edge.to, indegree.get(edge.to) + 1);
1036
+ }
1037
+ const queue = nodeIds.filter((id) => indegree.get(id) === 0);
1038
+ const processed = /* @__PURE__ */ new Set();
1039
+ let cursor = 0;
1040
+ let cycleRoot = 0;
1041
+ while (processed.size < nodeIds.length) {
1042
+ if (cursor === queue.length) {
1043
+ while (processed.has(nodeIds[cycleRoot])) cycleRoot++;
1044
+ queue.push(nodeIds[cycleRoot]);
1045
+ }
1046
+ const id = queue[cursor++];
1047
+ if (processed.has(id)) continue;
1048
+ processed.add(id);
1049
+ for (const target of outgoing.get(id)) {
1050
+ if (processed.has(target)) continue;
1051
+ ranks.set(target, Math.max(ranks.get(target), ranks.get(id) + 1));
1052
+ const remaining = indegree.get(target) - 1;
1053
+ indegree.set(target, remaining);
1054
+ if (remaining === 0) queue.push(target);
1035
1055
  }
1036
- if (!changed) break;
1037
1056
  }
1038
1057
  if (direction === "RL" || direction === "BT") {
1039
1058
  const max = Math.max(...ranks.values(), 0);
@@ -1047,7 +1066,7 @@ function snapGrid(n, step = 4) {
1047
1066
  function layoutRanked(ast, edges, opts) {
1048
1067
  const nodeIds = Object.keys(ast.nodes);
1049
1068
  const dtype = diagramType(ast);
1050
- const direction = opts.forceVertical ? "TB" : ast.meta.direction;
1069
+ const direction = opts.forceVertical ? ast.meta.direction === "BT" ? "BT" : "TB" : ast.meta.direction;
1051
1070
  const ranks = assignRanks(nodeIds, edges, direction);
1052
1071
  const byRank = /* @__PURE__ */ new Map();
1053
1072
  for (const id of nodeIds) {
@@ -1072,6 +1091,7 @@ function layoutRanked(ast, edges, opts) {
1072
1091
  }
1073
1092
  const rankOrder = /* @__PURE__ */ new Map();
1074
1093
  const sortedRanks = [...byRank.keys()].sort((a, b) => a - b);
1094
+ if (direction === "RL" || direction === "BT") sortedRanks.reverse();
1075
1095
  for (const r of sortedRanks) {
1076
1096
  const ids = byRank.get(r);
1077
1097
  ids.sort((a, b) => {
@@ -1091,6 +1111,39 @@ function layoutRanked(ast, edges, opts) {
1091
1111
  rankOrder.set(id, idx);
1092
1112
  });
1093
1113
  }
1114
+ const neighbors = new Map(nodeIds.map((id) => [id, []]));
1115
+ for (const edge of edges) {
1116
+ if (edge.kind === "response" || edge.selfLoop || !neighbors.has(edge.from) || !neighbors.has(edge.to)) continue;
1117
+ neighbors.get(edge.from).push(edge.to);
1118
+ neighbors.get(edge.to).push(edge.from);
1119
+ }
1120
+ for (let sweep = 0; sweep < 4; sweep++) {
1121
+ let changed = false;
1122
+ const order = sweep % 2 === 0 ? [...sortedRanks].reverse() : sortedRanks;
1123
+ for (const rank of order) {
1124
+ const ids = byRank.get(rank);
1125
+ for (let index = 0; index < ids.length - 1; index++) {
1126
+ const left = ids[index];
1127
+ const right = ids[index + 1];
1128
+ if (nodeGroupIndex.get(left) !== nodeGroupIndex.get(right)) continue;
1129
+ let improvement = 0;
1130
+ for (const leftNeighbor of neighbors.get(left)) {
1131
+ for (const rightNeighbor of neighbors.get(right)) {
1132
+ const neighborRank = ranks.get(leftNeighbor);
1133
+ if (neighborRank === rank || neighborRank !== ranks.get(rightNeighbor)) continue;
1134
+ improvement += Math.sign(rankOrder.get(leftNeighbor) - rankOrder.get(rightNeighbor));
1135
+ }
1136
+ }
1137
+ if (improvement <= 0) continue;
1138
+ ids[index] = right;
1139
+ ids[index + 1] = left;
1140
+ rankOrder.set(right, index);
1141
+ rankOrder.set(left, index + 1);
1142
+ changed = true;
1143
+ }
1144
+ }
1145
+ if (!changed) break;
1146
+ }
1094
1147
  const isVertical = direction === "TB" || direction === "BT";
1095
1148
  const hasTitle = Boolean(ast.meta.title && ast.meta.title.trim().length > 0);
1096
1149
  const hasBeatCaptions = ast.beats.some((b) => b.label && b.label.trim().length > 0);
@@ -1225,6 +1278,7 @@ function layoutRanked(ast, edges, opts) {
1225
1278
  function layoutTree(ast, edges) {
1226
1279
  const nodeIds = Object.keys(ast.nodes);
1227
1280
  if (nodeIds.length === 0) return [];
1281
+ edges = cycleSafeEdges(nodeIds, edges);
1228
1282
  const nodeDims = /* @__PURE__ */ new Map();
1229
1283
  for (const id of nodeIds) {
1230
1284
  nodeDims.set(id, computeNodeDimensions(ast.nodes[id]));
@@ -1395,19 +1449,21 @@ function cycleSafeEdges(nodeIds, edges) {
1395
1449
  }
1396
1450
  const visited = /* @__PURE__ */ new Set();
1397
1451
  const safe = [];
1398
- const visit = (id) => {
1399
- if (visited.has(id)) return;
1400
- visited.add(id);
1401
- for (const edge of outgoing.get(id) ?? []) {
1402
- if (visited.has(edge.to)) continue;
1403
- safe.push(edge);
1404
- visit(edge.to);
1452
+ const roots = nodeIds.filter((id) => !incoming.has(id));
1453
+ for (const root of [...roots, ...nodeIds]) {
1454
+ if (visited.has(root)) continue;
1455
+ const stack = [{ id: root }];
1456
+ while (stack.length > 0) {
1457
+ const current = stack.pop();
1458
+ if (visited.has(current.id)) continue;
1459
+ visited.add(current.id);
1460
+ if (current.edge) safe.push(current.edge);
1461
+ const children = outgoing.get(current.id);
1462
+ for (let index = children.length - 1; index >= 0; index--) {
1463
+ stack.push({ id: children[index].to, edge: children[index] });
1464
+ }
1405
1465
  }
1406
- };
1407
- for (const id of nodeIds) {
1408
- if (!incoming.has(id)) visit(id);
1409
1466
  }
1410
- for (const id of nodeIds) visit(id);
1411
1467
  return safe;
1412
1468
  }
1413
1469
  function layoutConstellation(ast) {
@@ -2549,7 +2605,8 @@ function computeAdaptiveDimensions(ast, edges) {
2549
2605
  const children = /* @__PURE__ */ new Map();
2550
2606
  const hasParent = /* @__PURE__ */ new Set();
2551
2607
  for (const id of nodeIds) children.set(id, []);
2552
- const treeEdges = effectiveEdges.some((e) => e.structural) ? effectiveEdges.filter((e) => e.structural) : effectiveEdges;
2608
+ const treeCandidates = effectiveEdges.some((e) => e.structural) ? effectiveEdges.filter((e) => e.structural) : effectiveEdges;
2609
+ const treeEdges = cycleSafeEdges(nodeIds, treeCandidates);
2553
2610
  for (const e of treeEdges) {
2554
2611
  if (ast.nodes[e.from] && ast.nodes[e.to] && e.from !== e.to) {
2555
2612
  children.get(e.from)?.push(e.to);
@@ -5257,6 +5314,97 @@ function buildSmoothSvgPath(start, waypoints, end, cornerRadius = 0) {
5257
5314
  d += ` L ${end.x} ${end.y}`;
5258
5315
  return d;
5259
5316
  }
5317
+ function routeAroundObstacles(sourceBox, targetBox, points, options) {
5318
+ if (!options.obstacles?.length) return points;
5319
+ const margin = options.margin ?? 20;
5320
+ const obstacles = [sourceBox, targetBox, ...options.obstacles];
5321
+ const intersections = (route) => {
5322
+ let hits = 0;
5323
+ for (let index = 1; index < route.length; index++) {
5324
+ const start2 = route[index - 1];
5325
+ const end2 = route[index];
5326
+ if (start2.x === end2.x && start2.y === end2.y) continue;
5327
+ for (const box of obstacles) {
5328
+ if (start2.y === end2.y && start2.y > box.y && start2.y < box.y + box.height && Math.max(start2.x, end2.x) > box.x && Math.min(start2.x, end2.x) < box.x + box.width) hits++;
5329
+ else if (start2.x === end2.x && start2.x > box.x && start2.x < box.x + box.width && Math.max(start2.y, end2.y) > box.y && Math.min(start2.y, end2.y) < box.y + box.height) hits++;
5330
+ }
5331
+ }
5332
+ return hits;
5333
+ };
5334
+ if (intersections(points) === 0) return points;
5335
+ const start = points[0];
5336
+ const end = points[points.length - 1];
5337
+ const stub = (point, port) => {
5338
+ let distance = margin;
5339
+ for (const box of obstacles) {
5340
+ let clearance = Number.POSITIVE_INFINITY;
5341
+ if (point.y > box.y && point.y < box.y + box.height) {
5342
+ if (port === "right") clearance = box.x - point.x;
5343
+ if (port === "left") clearance = point.x - box.x - box.width;
5344
+ }
5345
+ if (point.x > box.x && point.x < box.x + box.width) {
5346
+ if (port === "bottom") clearance = box.y - point.y;
5347
+ if (port === "top") clearance = point.y - box.y - box.height;
5348
+ }
5349
+ if (clearance >= 0) distance = Math.min(distance, clearance / 2);
5350
+ }
5351
+ return {
5352
+ x: point.x + (port === "right" ? distance : port === "left" ? -distance : 0),
5353
+ y: point.y + (port === "bottom" ? distance : port === "top" ? -distance : 0)
5354
+ };
5355
+ };
5356
+ const sourceStub = stub(start, options.sourcePort);
5357
+ const targetStub = stub(end, options.targetPort);
5358
+ const corridorsX = /* @__PURE__ */ new Set();
5359
+ const corridorsY = /* @__PURE__ */ new Set();
5360
+ for (const box of obstacles) {
5361
+ corridorsX.add(box.x - margin);
5362
+ corridorsX.add(box.x + box.width + margin);
5363
+ corridorsY.add(box.y - margin);
5364
+ corridorsY.add(box.y + box.height + margin);
5365
+ }
5366
+ const candidates = [
5367
+ points,
5368
+ [start, sourceStub, { x: sourceStub.x, y: targetStub.y }, targetStub, end],
5369
+ [start, sourceStub, { x: targetStub.x, y: sourceStub.y }, targetStub, end]
5370
+ ];
5371
+ for (const corridor of corridorsX) {
5372
+ candidates.push([
5373
+ start,
5374
+ sourceStub,
5375
+ { x: corridor, y: sourceStub.y },
5376
+ { x: corridor, y: targetStub.y },
5377
+ targetStub,
5378
+ end
5379
+ ]);
5380
+ }
5381
+ for (const corridor of corridorsY) {
5382
+ candidates.push([
5383
+ start,
5384
+ sourceStub,
5385
+ { x: sourceStub.x, y: corridor },
5386
+ { x: targetStub.x, y: corridor },
5387
+ targetStub,
5388
+ end
5389
+ ]);
5390
+ }
5391
+ let best = points;
5392
+ let bestHits = Number.POSITIVE_INFINITY;
5393
+ let bestLength = Number.POSITIVE_INFINITY;
5394
+ for (const candidate of candidates) {
5395
+ const hits = intersections(candidate);
5396
+ let length = 0;
5397
+ for (let index = 1; index < candidate.length; index++) {
5398
+ length += Math.abs(candidate[index].x - candidate[index - 1].x) + Math.abs(candidate[index].y - candidate[index - 1].y);
5399
+ }
5400
+ if (hits < bestHits || hits === bestHits && length < bestLength) {
5401
+ best = candidate;
5402
+ bestHits = hits;
5403
+ bestLength = length;
5404
+ }
5405
+ }
5406
+ return best.filter((point, index) => index === 0 || point.x !== best[index - 1].x || point.y !== best[index - 1].y);
5407
+ }
5260
5408
  function routeOrthogonalEdge(sourceBox, targetBox, options = {}) {
5261
5409
  const isSelfLoop = sourceBox === targetBox || sourceBox.x === targetBox.x && sourceBox.y === targetBox.y && sourceBox.width === targetBox.width && sourceBox.height === targetBox.height;
5262
5410
  const MARGIN = options.margin ?? 20;
@@ -5299,6 +5447,11 @@ function routeOrthogonalEdge(sourceBox, targetBox, options = {}) {
5299
5447
  end2 = { x: rightX, y: endY };
5300
5448
  waypoints2 = [{ x: apexX, y: startY }, { x: apexX, y: endY }];
5301
5449
  }
5450
+ waypoints2 = routeAroundObstacles(sourceBox, targetBox, [start2, ...waypoints2, end2], {
5451
+ ...options,
5452
+ sourcePort: port,
5453
+ targetPort: port
5454
+ }).slice(1, -1);
5302
5455
  const svgPathData2 = buildSmoothSvgPath(start2, waypoints2, end2, options.cornerRadius ?? 6);
5303
5456
  return {
5304
5457
  sourcePort: port,
@@ -5381,13 +5534,18 @@ function routeOrthogonalEdge(sourceBox, targetBox, options = {}) {
5381
5534
  }
5382
5535
  waypoints.push(p2);
5383
5536
  }
5384
- const svgPathData = buildSmoothSvgPath(start, waypoints, end, options.cornerRadius ?? 6);
5537
+ const routedWaypoints = routeAroundObstacles(sourceBox, targetBox, [start, ...waypoints, end], {
5538
+ ...options,
5539
+ sourcePort,
5540
+ targetPort
5541
+ }).slice(1, -1);
5542
+ const svgPathData = buildSmoothSvgPath(start, routedWaypoints, end, options.cornerRadius ?? 6);
5385
5543
  return {
5386
5544
  sourcePort,
5387
5545
  targetPort,
5388
5546
  startPoint: start,
5389
5547
  endPoint: end,
5390
- waypoints,
5548
+ waypoints: routedWaypoints,
5391
5549
  svgPathData
5392
5550
  };
5393
5551
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markdy/core",
3
- "version": "1.4.2",
3
+ "version": "1.4.4",
4
4
  "description": "Diagram-native MarkdyScript parser and compiler (auto-layout, edge routing, cue scheduling) — zero runtime dependencies.",
5
5
  "license": "MIT",
6
6
  "type": "module",