@graph-artifact/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +62 -0
  3. package/dist/ThemeContext.d.ts +47 -0
  4. package/dist/ThemeContext.js +81 -0
  5. package/dist/components/DiagramCanvas.d.ts +8 -0
  6. package/dist/components/DiagramCanvas.js +19 -0
  7. package/dist/components/GraphCanvas.d.ts +9 -0
  8. package/dist/components/GraphCanvas.js +104 -0
  9. package/dist/components/NodeDetail.d.ts +9 -0
  10. package/dist/components/NodeDetail.js +127 -0
  11. package/dist/components/edges/RoutedEdge.d.ts +11 -0
  12. package/dist/components/edges/RoutedEdge.js +199 -0
  13. package/dist/components/nodes/ClassNode.d.ts +5 -0
  14. package/dist/components/nodes/ClassNode.js +62 -0
  15. package/dist/components/nodes/EntityNode.d.ts +5 -0
  16. package/dist/components/nodes/EntityNode.js +57 -0
  17. package/dist/components/nodes/FlowNode.d.ts +5 -0
  18. package/dist/components/nodes/FlowNode.js +144 -0
  19. package/dist/components/nodes/SequenceNodes.d.ts +33 -0
  20. package/dist/components/nodes/SequenceNodes.js +205 -0
  21. package/dist/components/nodes/StateNode.d.ts +5 -0
  22. package/dist/components/nodes/StateNode.js +71 -0
  23. package/dist/components/nodes/SubgraphNode.d.ts +5 -0
  24. package/dist/components/nodes/SubgraphNode.js +16 -0
  25. package/dist/config.d.ts +138 -0
  26. package/dist/config.js +165 -0
  27. package/dist/core.d.ts +12 -0
  28. package/dist/core.js +7 -0
  29. package/dist/diagrams/detect.d.ts +2 -0
  30. package/dist/diagrams/detect.js +8 -0
  31. package/dist/diagrams/plugins.d.ts +2 -0
  32. package/dist/diagrams/plugins.js +45 -0
  33. package/dist/diagrams/registry.d.ts +3 -0
  34. package/dist/diagrams/registry.js +13 -0
  35. package/dist/diagrams/types.d.ts +7 -0
  36. package/dist/diagrams/types.js +1 -0
  37. package/dist/index.d.ts +1 -0
  38. package/dist/index.js +3 -0
  39. package/dist/layout/dagre/index.d.ts +31 -0
  40. package/dist/layout/dagre/index.js +224 -0
  41. package/dist/layout/dagre/nodeSizing.d.ts +32 -0
  42. package/dist/layout/dagre/nodeSizing.js +202 -0
  43. package/dist/layout/edges/buildEdges.d.ts +18 -0
  44. package/dist/layout/edges/buildEdges.js +405 -0
  45. package/dist/layout/edges/classify.d.ts +13 -0
  46. package/dist/layout/edges/classify.js +36 -0
  47. package/dist/layout/edges/diamondHandles.d.ts +23 -0
  48. package/dist/layout/edges/diamondHandles.js +108 -0
  49. package/dist/layout/edges/index.d.ts +10 -0
  50. package/dist/layout/edges/index.js +8 -0
  51. package/dist/layout/edges/paths.d.ts +57 -0
  52. package/dist/layout/edges/paths.js +279 -0
  53. package/dist/layout/index.d.ts +59 -0
  54. package/dist/layout/index.js +131 -0
  55. package/dist/layout/intersect/circle.d.ts +2 -0
  56. package/dist/layout/intersect/circle.js +14 -0
  57. package/dist/layout/intersect/diamond.d.ts +9 -0
  58. package/dist/layout/intersect/diamond.js +21 -0
  59. package/dist/layout/intersect/index.d.ts +17 -0
  60. package/dist/layout/intersect/index.js +28 -0
  61. package/dist/layout/intersect/rect.d.ts +10 -0
  62. package/dist/layout/intersect/rect.js +31 -0
  63. package/dist/layout/intersect/rectRounded.d.ts +20 -0
  64. package/dist/layout/intersect/rectRounded.js +48 -0
  65. package/dist/layout/mindmapLayout.d.ts +13 -0
  66. package/dist/layout/mindmapLayout.js +299 -0
  67. package/dist/layout/sequenceLayout.d.ts +24 -0
  68. package/dist/layout/sequenceLayout.js +414 -0
  69. package/dist/layout/subgraph.d.ts +26 -0
  70. package/dist/layout/subgraph.js +63 -0
  71. package/dist/layout/types.d.ts +34 -0
  72. package/dist/layout/types.js +8 -0
  73. package/dist/parsers/classDiagram.d.ts +2 -0
  74. package/dist/parsers/classDiagram.js +105 -0
  75. package/dist/parsers/er.d.ts +2 -0
  76. package/dist/parsers/er.js +97 -0
  77. package/dist/parsers/flowchart.d.ts +2 -0
  78. package/dist/parsers/flowchart.js +191 -0
  79. package/dist/parsers/helpers.d.ts +4 -0
  80. package/dist/parsers/helpers.js +8 -0
  81. package/dist/parsers/index.d.ts +7 -0
  82. package/dist/parsers/index.js +19 -0
  83. package/dist/parsers/mindmap.d.ts +2 -0
  84. package/dist/parsers/mindmap.js +124 -0
  85. package/dist/parsers/sequence.d.ts +18 -0
  86. package/dist/parsers/sequence.js +196 -0
  87. package/dist/parsers/state.d.ts +2 -0
  88. package/dist/parsers/state.js +68 -0
  89. package/dist/react.d.ts +7 -0
  90. package/dist/react.js +9 -0
  91. package/dist/reactDefaults.d.ts +5 -0
  92. package/dist/reactDefaults.js +37 -0
  93. package/dist/renderMarkdown.d.ts +9 -0
  94. package/dist/renderMarkdown.js +103 -0
  95. package/dist/swagger.d.ts +113 -0
  96. package/dist/swagger.js +551 -0
  97. package/dist/theme/dark.d.ts +8 -0
  98. package/dist/theme/dark.js +190 -0
  99. package/dist/theme/index.d.ts +18 -0
  100. package/dist/theme/index.js +29 -0
  101. package/dist/theme/light.d.ts +8 -0
  102. package/dist/theme/light.js +190 -0
  103. package/dist/theme/types.d.ts +97 -0
  104. package/dist/theme/types.js +7 -0
  105. package/dist/types.d.ts +235 -0
  106. package/dist/types.js +1 -0
  107. package/package.json +74 -0
@@ -0,0 +1,45 @@
1
+ import { parseFlowchart } from '../parsers/flowchart.js';
2
+ import { parseStateDiagram } from '../parsers/state.js';
3
+ import { parseErDiagram } from '../parsers/er.js';
4
+ import { parseClassDiagram } from '../parsers/classDiagram.js';
5
+ import { parseSequenceDiagram } from '../parsers/sequence.js';
6
+ import { parseMindmap } from '../parsers/mindmap.js';
7
+ const startsWith = (regex) => (syntax) => regex.test(syntax.trimStart());
8
+ export const builtinDiagramPlugins = [
9
+ {
10
+ id: 'mindmap',
11
+ detect: startsWith(/^mindmap\b/i),
12
+ parse: parseMindmap,
13
+ renderer: 'graph',
14
+ },
15
+ {
16
+ id: 'flowchart',
17
+ detect: startsWith(/^(graph|flowchart)\s+/i),
18
+ parse: parseFlowchart,
19
+ renderer: 'graph',
20
+ },
21
+ {
22
+ id: 'state',
23
+ detect: startsWith(/^stateDiagram(?:-v2)?\b/i),
24
+ parse: parseStateDiagram,
25
+ renderer: 'graph',
26
+ },
27
+ {
28
+ id: 'er',
29
+ detect: startsWith(/^erDiagram\b/i),
30
+ parse: parseErDiagram,
31
+ renderer: 'graph',
32
+ },
33
+ {
34
+ id: 'class',
35
+ detect: startsWith(/^classDiagram\b/i),
36
+ parse: parseClassDiagram,
37
+ renderer: 'graph',
38
+ },
39
+ {
40
+ id: 'sequence',
41
+ detect: startsWith(/^sequenceDiagram\b/i),
42
+ parse: parseSequenceDiagram,
43
+ renderer: 'graph',
44
+ },
45
+ ];
@@ -0,0 +1,3 @@
1
+ import type { DiagramPlugin } from './types.js';
2
+ export declare function registerDiagramPlugin(plugin: DiagramPlugin): void;
3
+ export declare function getDiagramPlugins(): readonly DiagramPlugin[];
@@ -0,0 +1,13 @@
1
+ import { builtinDiagramPlugins } from './plugins.js';
2
+ const plugins = [...builtinDiagramPlugins];
3
+ export function registerDiagramPlugin(plugin) {
4
+ const index = plugins.findIndex((p) => p.id === plugin.id);
5
+ if (index >= 0) {
6
+ plugins[index] = plugin;
7
+ return;
8
+ }
9
+ plugins.push(plugin);
10
+ }
11
+ export function getDiagramPlugins() {
12
+ return plugins;
13
+ }
@@ -0,0 +1,7 @@
1
+ import type { ParsedDiagram, DiagramType } from '../types.js';
2
+ export interface DiagramPlugin {
3
+ id: DiagramType;
4
+ detect: (syntax: string) => boolean;
5
+ parse: (syntax: string) => ParsedDiagram | null;
6
+ renderer: 'graph' | 'custom';
7
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export * from './core.js';
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ // Backward-compatible default entry points to core/headless APIs.
2
+ // Use `@graph-artifact/core/react` for React renderer components.
3
+ export * from './core.js';
@@ -0,0 +1,31 @@
1
+ /**
2
+ * dagre/ — Dagre layout algorithm.
3
+ *
4
+ * Builds a dagre graph from parsed data, runs the layout algorithm,
5
+ * and extracts node positions + edge waypoints for downstream use.
6
+ *
7
+ * Mirrors Mermaid's layout-algorithms/dagre/ pattern:
8
+ * the layout algorithm is isolated behind a clean module boundary.
9
+ */
10
+ import dagre from '@dagrejs/dagre';
11
+ import type { ParsedGraph, ParsedNode } from '../../types.js';
12
+ import type { GraphConfig } from '../../config.js';
13
+ import type { NodePosition, HandlePositions, EdgeWaypoints } from '../types.js';
14
+ export type { NodePosition, HandlePositions, EdgeWaypoints } from '../types.js';
15
+ /**
16
+ * Maps graph direction to handle positions.
17
+ * Forward edges use primary handles (e.g. Bottom→Top for TB).
18
+ * Back-edges route to side handles to avoid passing through intermediate nodes.
19
+ */
20
+ export declare function directionToHandles(direction: ParsedGraph['direction']): HandlePositions;
21
+ export declare function createDagreGraph(parsed: ParsedGraph, layoutConfig: GraphConfig['layout']): dagre.graphlib.Graph;
22
+ /**
23
+ * Extracts top-left positions from dagre's center-based coordinates.
24
+ * Returns a Map of nodeId → { x, y, width, height }.
25
+ */
26
+ export declare function extractPositions(graph: dagre.graphlib.Graph, nodes: ParsedNode[]): Map<string, NodePosition>;
27
+ /**
28
+ * Extracts dagre's computed edge waypoints for every edge.
29
+ * These waypoints route around intermediate nodes automatically.
30
+ */
31
+ export declare function extractEdgeWaypoints(graph: dagre.graphlib.Graph, parsed: ParsedGraph): EdgeWaypoints[];
@@ -0,0 +1,224 @@
1
+ /**
2
+ * dagre/ — Dagre layout algorithm.
3
+ *
4
+ * Builds a dagre graph from parsed data, runs the layout algorithm,
5
+ * and extracts node positions + edge waypoints for downstream use.
6
+ *
7
+ * Mirrors Mermaid's layout-algorithms/dagre/ pattern:
8
+ * the layout algorithm is isolated behind a clean module boundary.
9
+ */
10
+ import dagre from '@dagrejs/dagre';
11
+ import { Position } from '@xyflow/react';
12
+ import { nodeSize } from './nodeSizing.js';
13
+ // ─── Handle Positions by Direction ──────────────────────────────────────────
14
+ /**
15
+ * Maps graph direction to handle positions.
16
+ * Forward edges use primary handles (e.g. Bottom→Top for TB).
17
+ * Back-edges route to side handles to avoid passing through intermediate nodes.
18
+ */
19
+ export function directionToHandles(direction) {
20
+ switch (direction) {
21
+ case 'LR':
22
+ return {
23
+ sourcePosition: Position.Right,
24
+ targetPosition: Position.Left,
25
+ backEdgeSource: Position.Top,
26
+ backEdgeTarget: Position.Top,
27
+ };
28
+ case 'RL':
29
+ return {
30
+ sourcePosition: Position.Left,
31
+ targetPosition: Position.Right,
32
+ backEdgeSource: Position.Top,
33
+ backEdgeTarget: Position.Top,
34
+ };
35
+ case 'BT':
36
+ return {
37
+ sourcePosition: Position.Top,
38
+ targetPosition: Position.Bottom,
39
+ backEdgeSource: Position.Right,
40
+ backEdgeTarget: Position.Right,
41
+ };
42
+ case 'TB':
43
+ default:
44
+ return {
45
+ sourcePosition: Position.Bottom,
46
+ targetPosition: Position.Top,
47
+ backEdgeSource: Position.Right,
48
+ backEdgeTarget: Position.Right,
49
+ };
50
+ }
51
+ }
52
+ // ─── Dagre Graph Construction ───────────────────────────────────────────────
53
+ export function createDagreGraph(parsed, layoutConfig) {
54
+ validateParsedGraph(parsed);
55
+ const g = new dagre.graphlib.Graph({ multigraph: true, compound: true });
56
+ g.setDefaultEdgeLabel(() => ({}));
57
+ const rankSepByType = {
58
+ er: layoutConfig.erRankSep,
59
+ state: layoutConfig.stateRankSep,
60
+ flowchart: layoutConfig.rankSep,
61
+ class: layoutConfig.rankSep,
62
+ sequence: layoutConfig.rankSep,
63
+ mindmap: layoutConfig.rankSep,
64
+ gitGraph: layoutConfig.rankSep,
65
+ };
66
+ const nodeSepByType = {
67
+ er: layoutConfig.erNodeSep,
68
+ state: layoutConfig.stateNodeSep,
69
+ flowchart: layoutConfig.nodeSep,
70
+ class: layoutConfig.nodeSep,
71
+ sequence: layoutConfig.nodeSep,
72
+ mindmap: layoutConfig.nodeSep,
73
+ gitGraph: layoutConfig.nodeSep,
74
+ };
75
+ g.setGraph({
76
+ rankdir: parsed.direction,
77
+ ranksep: rankSepByType[parsed.diagramType] ?? layoutConfig.rankSep,
78
+ nodesep: nodeSepByType[parsed.diagramType] ?? layoutConfig.nodeSep,
79
+ marginx: 8,
80
+ marginy: 8,
81
+ ranker: 'network-simplex',
82
+ });
83
+ for (const node of parsed.nodes) {
84
+ const size = nodeSize(node, parsed.diagramType, layoutConfig.nodeSizing);
85
+ g.setNode(node.id, { width: size.width, height: size.height });
86
+ }
87
+ // Defensive: if a parser emits an edge endpoint that is missing from
88
+ // parsed.nodes, dagre creates an implicit node with undefined label data,
89
+ // which can crash rank assignment (network simplex). Materialize a fallback
90
+ // node shape/dimensions so layout remains stable.
91
+ const fallbackNode = {
92
+ width: layoutConfig.nodeSizing.flow.minWidth,
93
+ height: layoutConfig.nodeSizing.flow.height,
94
+ };
95
+ // Register subgraphs as compound parent nodes so dagre clusters
96
+ // their children together and adds spacing between groups.
97
+ if (parsed.subgraphs) {
98
+ const pad = layoutConfig.subgraphPadding;
99
+ for (const subgraph of parsed.subgraphs) {
100
+ g.setNode(subgraph.id, {
101
+ clusterLabelPos: 'top',
102
+ paddingTop: pad + 14, // extra for label
103
+ paddingBottom: pad,
104
+ paddingLeft: pad,
105
+ paddingRight: pad,
106
+ });
107
+ for (const nodeId of subgraph.nodeIds) {
108
+ if (!g.hasNode(nodeId))
109
+ g.setNode(nodeId, fallbackNode);
110
+ g.setParent(nodeId, subgraph.id);
111
+ }
112
+ }
113
+ }
114
+ parsed.edges.forEach((edge, i) => {
115
+ // Pass label dimensions to dagre so it spaces edges apart
116
+ // and generates better waypoints. Without this, dagre treats
117
+ // all edges as zero-width and packs waypoints tight together.
118
+ const labelWidth = edge.label ? edge.label.length * 8 + 16 : 0;
119
+ const labelHeight = edge.label ? 20 : 0;
120
+ // For inheritance edges (dagreReversed), reverse the direction in dagre
121
+ // so parents rank above children, while the visual arrow stays child→parent.
122
+ const dagreSrc = edge.dagreReversed ? edge.target : edge.source;
123
+ const dagreTgt = edge.dagreReversed ? edge.source : edge.target;
124
+ if (!g.hasNode(dagreSrc))
125
+ g.setNode(dagreSrc, fallbackNode);
126
+ if (!g.hasNode(dagreTgt))
127
+ g.setNode(dagreTgt, fallbackNode);
128
+ // Use index as edge name for multigraph — prevents overwriting when
129
+ // multiple edges exist between the same source→target pair.
130
+ g.setEdge(dagreSrc, dagreTgt, {
131
+ width: labelWidth,
132
+ height: labelHeight,
133
+ labelpos: 'c',
134
+ }, `e${i}`);
135
+ });
136
+ try {
137
+ dagre.layout(g);
138
+ }
139
+ catch (error) {
140
+ const msg = error instanceof Error ? error.message : String(error);
141
+ throw new Error(`Layout failed for diagram type '${parsed.diagramType}'. ${msg}`);
142
+ }
143
+ return g;
144
+ }
145
+ function validateParsedGraph(parsed) {
146
+ const issues = [];
147
+ const nodeIds = new Set();
148
+ for (const node of parsed.nodes) {
149
+ const id = String(node.id ?? '').trim();
150
+ if (!id) {
151
+ issues.push('Found node with empty id.');
152
+ continue;
153
+ }
154
+ if (nodeIds.has(id))
155
+ issues.push(`Duplicate node id '${id}'.`);
156
+ nodeIds.add(id);
157
+ }
158
+ parsed.edges.forEach((edge, i) => {
159
+ const src = String(edge.source ?? '').trim();
160
+ const tgt = String(edge.target ?? '').trim();
161
+ if (!src)
162
+ issues.push(`Edge #${i} has empty source.`);
163
+ if (!tgt)
164
+ issues.push(`Edge #${i} has empty target.`);
165
+ if (src && !nodeIds.has(src))
166
+ issues.push(`Edge #${i} source '${src}' does not exist.`);
167
+ if (tgt && !nodeIds.has(tgt))
168
+ issues.push(`Edge #${i} target '${tgt}' does not exist.`);
169
+ });
170
+ if (parsed.subgraphs) {
171
+ for (const sg of parsed.subgraphs) {
172
+ for (const nid of sg.nodeIds) {
173
+ const id = String(nid ?? '').trim();
174
+ if (!id) {
175
+ issues.push(`Subgraph '${sg.id}' contains empty node id.`);
176
+ continue;
177
+ }
178
+ if (!nodeIds.has(id))
179
+ issues.push(`Subgraph '${sg.id}' references missing node '${id}'.`);
180
+ }
181
+ }
182
+ }
183
+ if (issues.length > 0) {
184
+ throw new Error(`Invalid diagram graph: ${issues.join(' ')}`);
185
+ }
186
+ }
187
+ // ─── Position Extraction ────────────────────────────────────────────────────
188
+ /**
189
+ * Extracts top-left positions from dagre's center-based coordinates.
190
+ * Returns a Map of nodeId → { x, y, width, height }.
191
+ */
192
+ export function extractPositions(graph, nodes) {
193
+ const positions = new Map();
194
+ for (const node of nodes) {
195
+ const pos = graph.node(node.id);
196
+ positions.set(node.id, {
197
+ x: pos.x - pos.width / 2,
198
+ y: pos.y - pos.height / 2,
199
+ width: pos.width,
200
+ height: pos.height,
201
+ });
202
+ }
203
+ return positions;
204
+ }
205
+ // ─── Edge Waypoint Extraction ────────────────────────────────────────────────
206
+ /**
207
+ * Extracts dagre's computed edge waypoints for every edge.
208
+ * These waypoints route around intermediate nodes automatically.
209
+ */
210
+ export function extractEdgeWaypoints(graph, parsed) {
211
+ return parsed.edges.map((edge, i) => {
212
+ // Use the same source/target order we gave dagre (may be reversed for inheritance edges)
213
+ const dagreSrc = edge.dagreReversed ? edge.target : edge.source;
214
+ const dagreTgt = edge.dagreReversed ? edge.source : edge.target;
215
+ const dagreEdge = graph.edge(dagreSrc, dagreTgt, `e${i}`);
216
+ let points = dagreEdge?.points
217
+ ? dagreEdge.points.map((p) => ({ x: p.x, y: p.y }))
218
+ : [];
219
+ // Reverse waypoints back so they flow from visual source → visual target
220
+ if (edge.dagreReversed)
221
+ points = points.reverse();
222
+ return { source: edge.source, target: edge.target, points };
223
+ });
224
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * nodeSizing.ts — Per-diagram-type node dimension calculations.
3
+ *
4
+ * Each diagram type has its own sizing function. The dispatcher
5
+ * `nodeSize()` selects the right one based on DiagramType.
6
+ *
7
+ * These dimensions are the SINGLE SOURCE OF TRUTH for node sizes.
8
+ * They feed dagre for positioning, intersection math for edge routing,
9
+ * and are passed to components as layoutWidth/layoutHeight.
10
+ *
11
+ * DIMENSIONAL CONTRACT:
12
+ * Card-based nodes (ER, Class) render at their natural CSS height —
13
+ * no fixed height is set. The estimates here are used ONLY by dagre
14
+ * for node spacing and by intersection math for edge endpoints.
15
+ * Estimates should be generous (round up) to ensure dagre allocates
16
+ * enough room. React Flow measures the actual DOM height after render.
17
+ *
18
+ * Shape-based nodes (Flow, State) use layoutWidth/layoutHeight
19
+ * directly since their geometry is deterministic (circles, diamonds, etc).
20
+ *
21
+ * All sizing reads from config so consumers can tune dimensions
22
+ * via `configure({ layout: { nodeSizing: { ... } } })`.
23
+ */
24
+ import type { ParsedNode, GraphDiagramType } from '../../types.js';
25
+ import type { GraphConfig } from '../../config.js';
26
+ type NodeSizingConfig = GraphConfig['layout']['nodeSizing'];
27
+ interface Dimensions {
28
+ width: number;
29
+ height: number;
30
+ }
31
+ export declare function nodeSize(node: ParsedNode, diagramType: GraphDiagramType, sizing: NodeSizingConfig): Dimensions;
32
+ export {};
@@ -0,0 +1,202 @@
1
+ /**
2
+ * nodeSizing.ts — Per-diagram-type node dimension calculations.
3
+ *
4
+ * Each diagram type has its own sizing function. The dispatcher
5
+ * `nodeSize()` selects the right one based on DiagramType.
6
+ *
7
+ * These dimensions are the SINGLE SOURCE OF TRUTH for node sizes.
8
+ * They feed dagre for positioning, intersection math for edge routing,
9
+ * and are passed to components as layoutWidth/layoutHeight.
10
+ *
11
+ * DIMENSIONAL CONTRACT:
12
+ * Card-based nodes (ER, Class) render at their natural CSS height —
13
+ * no fixed height is set. The estimates here are used ONLY by dagre
14
+ * for node spacing and by intersection math for edge endpoints.
15
+ * Estimates should be generous (round up) to ensure dagre allocates
16
+ * enough room. React Flow measures the actual DOM height after render.
17
+ *
18
+ * Shape-based nodes (Flow, State) use layoutWidth/layoutHeight
19
+ * directly since their geometry is deterministic (circles, diamonds, etc).
20
+ *
21
+ * All sizing reads from config so consumers can tune dimensions
22
+ * via `configure({ layout: { nodeSizing: { ... } } })`.
23
+ */
24
+ import { getConfig } from '../../config.js';
25
+ function clampWidth(px) {
26
+ const { layout } = getConfig();
27
+ return Math.max(layout.nodeSizing.common.minWidth, Math.min(layout.nodeSizing.common.maxWidth, Math.ceil(px)));
28
+ }
29
+ function stripMarkdown(input) {
30
+ return input
31
+ .replace(/`([^`]+)`/g, '$1')
32
+ .replace(/\*\*([^*]+)\*\*/g, '$1')
33
+ .replace(/\*([^*]+)\*/g, '$1')
34
+ .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1')
35
+ .replace(/[_~#>|-]/g, '')
36
+ .trim();
37
+ }
38
+ function labelMetrics(label) {
39
+ const normalized = label.replace(/\\n/g, '\n');
40
+ const lines = normalized.split('\n');
41
+ const maxChars = Math.max(1, ...lines.map((line) => stripMarkdown(line).length || 1));
42
+ return { maxChars, lines: Math.max(1, lines.length) };
43
+ }
44
+ function estimatedWrappedLineCount(label, contentCharsPerLine) {
45
+ const normalized = label.replace(/\\n/g, '\n');
46
+ const lines = normalized.split('\n');
47
+ let total = 0;
48
+ const charsPerLine = Math.max(8, contentCharsPerLine);
49
+ for (const rawLine of lines) {
50
+ const clean = stripMarkdown(rawLine);
51
+ const len = Math.max(1, clean.length);
52
+ total += Math.max(1, Math.ceil(len / charsPerLine));
53
+ }
54
+ return Math.max(1, total);
55
+ }
56
+ // ─── Flowchart ──────────────────────────────────────────────────────────────
57
+ function flowchartNodeSize(node, sizing) {
58
+ const common = sizing.common;
59
+ const { maxChars } = labelMetrics(node.label);
60
+ const dynamicWidth = Math.max(sizing.flow.minWidth, maxChars * sizing.charWidth + sizing.flow.padding);
61
+ const width = clampWidth(dynamicWidth);
62
+ const contentCharsPerLine = Math.max(8, Math.floor((width - sizing.flow.padding) / Math.max(1, sizing.charWidth)));
63
+ const wrappedLines = estimatedWrappedLineCount(node.label, contentCharsPerLine);
64
+ const dynamicHeight = Math.max(sizing.flow.height, common.lineBaseHeight + wrappedLines * common.lineStepHeight);
65
+ if (node.shape === 'diamond') {
66
+ // The visual diamond is a rotated square — its bounding box is side * sqrt(2)
67
+ const baseSide = Math.max(sizing.flow.diamondSize, dynamicWidth * 0.72, dynamicHeight * 0.72);
68
+ const diagSize = Math.ceil(baseSide * Math.SQRT2);
69
+ return { width: diagSize, height: diagSize };
70
+ }
71
+ if (node.shape === 'circle') {
72
+ const size = Math.max(sizing.flow.circleSize, dynamicWidth * 0.85, dynamicHeight * 0.85);
73
+ return { width: Math.ceil(size), height: Math.ceil(size) };
74
+ }
75
+ if (node.shape === 'cylinder') {
76
+ return {
77
+ width: Math.max(sizing.flow.cylinderWidth, dynamicWidth),
78
+ height: Math.max(sizing.flow.cylinderHeight, dynamicHeight + 10),
79
+ };
80
+ }
81
+ return {
82
+ width,
83
+ height: dynamicHeight,
84
+ };
85
+ }
86
+ // ─── State Diagram ──────────────────────────────────────────────────────────
87
+ function stateNodeSize(node, sizing) {
88
+ const common = sizing.common;
89
+ const { maxChars, lines } = labelMetrics(node.label);
90
+ if (node.shape === 'circle') {
91
+ return { width: sizing.state.circleSize, height: sizing.state.circleSize };
92
+ }
93
+ if (node.shape === 'doublecircle') {
94
+ // Include border in bounding box so intersection math hits the visual boundary
95
+ const size = sizing.state.circleSize + sizing.state.doublecirclePad
96
+ + sizing.state.doublecircleBorder * 2;
97
+ return { width: size, height: size };
98
+ }
99
+ // border-box dimensions: content + padding + border extras
100
+ const textWidth = Math.max(sizing.state.minWidth, maxChars * sizing.charWidth + sizing.state.padding);
101
+ const textHeight = Math.max(sizing.state.height, common.lineBaseHeight + lines * common.lineStepHeight);
102
+ return {
103
+ width: textWidth + sizing.state.borderExtra,
104
+ height: textHeight + sizing.state.borderExtra,
105
+ };
106
+ }
107
+ // ─── ER Diagram ─────────────────────────────────────────────────────────────
108
+ function erNodeSize(node, sizing) {
109
+ const attrCount = node.attributes?.length ?? 0;
110
+ const attrs = node.attributes ?? [];
111
+ // Height breakdown (mirrors EntityNode CSS):
112
+ // headerHeight: header padding (space[2]*2) + font (size.lg) + line-height
113
+ // sectionPad: attribute wrapper padding (space[2]*2 = 8+8 = 16px)
114
+ // rowHeight: per-row padding (space[1]*2 = 4+4) + font (size.md) * line-height
115
+ // dividers: 1px borderBottom between attribute rows
116
+ // borderExtra: card border (borderWidth.sm * 2 = 1+1 = 2px)
117
+ const sectionPad = attrCount > 0 ? 16 : 0;
118
+ const dividers = attrCount > 1 ? attrCount - 1 : 0;
119
+ const badgeRoom = attrs.some((attr) => {
120
+ const t = attr.trim().split(/\s+/);
121
+ return t[2] === 'PK' || t[2] === 'FK' || t[2] === 'UK';
122
+ }) ? 44 : 0;
123
+ // Width mirrors EntityNode row composition:
124
+ // section horizontal padding (24) + type column (90) + gap (8)
125
+ // + longest name content + optional badge room.
126
+ const headerChars = Math.max(0, node.label.length);
127
+ const maxNameChars = attrs.reduce((max, attr) => {
128
+ const t = attr.trim().split(/\s+/);
129
+ return Math.max(max, (t[1] ?? '').length);
130
+ }, 0);
131
+ const contentChars = Math.max(headerChars, maxNameChars);
132
+ const estimatedWidth = Math.max(sizing.er.width, 24 + 90 + 8 + contentChars * sizing.charWidth + badgeRoom);
133
+ return {
134
+ width: clampWidth(estimatedWidth),
135
+ height: sizing.er.headerHeight
136
+ + sectionPad
137
+ + attrCount * sizing.er.rowHeight
138
+ + dividers
139
+ + sizing.er.borderExtra,
140
+ };
141
+ }
142
+ // ─── Class Diagram ──────────────────────────────────────────────────────────
143
+ function classNodeSize(node, sizing) {
144
+ const props = node.properties ?? [];
145
+ const methods = node.methods ?? [];
146
+ const hasBothSections = props.length > 0 && methods.length > 0;
147
+ const sectionCount = (props.length > 0 ? 1 : 0) + (methods.length > 0 ? 1 : 0);
148
+ const longestRowChars = Math.max(node.label.length, ...props.map((p) => p.length), ...methods.map((m) => m.length), 0);
149
+ const estimatedWidth = Math.max(sizing.class.width, 24 + longestRowChars * 7.2);
150
+ const classWidth = clampWidth(estimatedWidth);
151
+ // Estimate wrapped line count per row. The content area is the node width
152
+ // minus section padding (12px each side) and card border (1px each side).
153
+ const contentWidth = classWidth - 26;
154
+ const monoCharWidth = 6.6; // approximate width of 11px monospace chars
155
+ const charsPerLine = Math.max(8, Math.floor(contentWidth / monoCharWidth));
156
+ // Height breakdown (calibrated to match actual component rendering):
157
+ // header: padding 8+8 + font 14px + line-height ≈ 33px
158
+ // per section: wrapper padding 8 top + 8 bottom = 16px
159
+ // per row: row padding 4 top + 4 bottom = 8px
160
+ // per text line: 11px font * 1.4 lineHeight ≈ 16px
161
+ // divider: 1px border between sections
162
+ // card border: 2px (1px top + 1px bottom)
163
+ const HEADER = sizing.class.headerHeight;
164
+ const SECTION_PAD = 16;
165
+ const ROW_PAD = 8;
166
+ const LINE_H = 16;
167
+ const BORDER = 2;
168
+ function sectionHeight(rows) {
169
+ if (rows.length === 0)
170
+ return 0;
171
+ let totalLines = 0;
172
+ for (const row of rows) {
173
+ totalLines += Math.max(1, Math.ceil(row.length / charsPerLine));
174
+ }
175
+ return SECTION_PAD + rows.length * ROW_PAD + totalLines * LINE_H;
176
+ }
177
+ const height = HEADER
178
+ + sectionHeight(props)
179
+ + sectionHeight(methods)
180
+ + (hasBothSections ? 1 : 0) // divider border
181
+ + BORDER
182
+ + (sectionCount === 0 ? 30 : 0); // "(empty)" placeholder
183
+ return { width: classWidth, height };
184
+ }
185
+ // ─── Dispatcher ─────────────────────────────────────────────────────────────
186
+ // Sequence diagrams bypass dagre entirely — they never call nodeSize().
187
+ // This stub exists only to satisfy the DiagramType→function mapping.
188
+ function sequenceNodeSize() {
189
+ return { width: 150, height: 40 };
190
+ }
191
+ const sizingByType = {
192
+ flowchart: flowchartNodeSize,
193
+ state: stateNodeSize,
194
+ er: erNodeSize,
195
+ class: classNodeSize,
196
+ sequence: sequenceNodeSize,
197
+ mindmap: flowchartNodeSize,
198
+ gitGraph: flowchartNodeSize,
199
+ };
200
+ export function nodeSize(node, diagramType, sizing) {
201
+ return sizingByType[diagramType](node, sizing);
202
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * buildEdges.ts — Main edge builder.
3
+ *
4
+ * Orchestrates: intersection → path generation → handle assignment → styling
5
+ * to produce React Flow Edge objects from parsed edges + dagre waypoints.
6
+ *
7
+ * For each edge:
8
+ * 1. Takes dagre's waypoints (which route around intermediate nodes)
9
+ * 2. Computes node-boundary intersection (rect or diamond) for start/end
10
+ * 3. Shortens the end by the arrow marker size so the tip lands at the boundary
11
+ * 4. Generates a rounded SVG path through all waypoints
12
+ * 5. Passes the pre-computed path + label position as edge data
13
+ */
14
+ import type { Edge } from '@xyflow/react';
15
+ import type { ParsedEdge, ParsedGraph } from '../../types.js';
16
+ import type { Theme } from '../../ThemeContext.js';
17
+ import type { NodePosition, EdgeWaypoints } from '../types.js';
18
+ export declare function buildEdges(parsedEdges: ParsedEdge[], positions: Map<string, NodePosition>, _handles: unknown, direction: ParsedGraph['direction'], theme: Theme, diamondEdgeHandles?: Map<number, string>, edgeWaypoints?: EdgeWaypoints[], nodeShapes?: Map<string, string>): Edge[];