md-redline 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.
- package/LICENSE +21 -0
- package/README.md +169 -0
- package/bin/md-redline +255 -0
- package/bin/test-windows.ps1 +70 -0
- package/dist/assets/_baseFor-Ck08IaSF.js +1 -0
- package/dist/assets/arc-DI2g9LXK.js +1 -0
- package/dist/assets/architecture-YZFGNWBL-BDgMfc-b.js +1 -0
- package/dist/assets/architectureDiagram-Q4EWVU46-Dg1hcUEa.js +36 -0
- package/dist/assets/array-DOVTz2Mq.js +1 -0
- package/dist/assets/blockDiagram-DXYQGD6D-BAXkTCAk.js +132 -0
- package/dist/assets/c4Diagram-AHTNJAMY-BIkgwQSx.js +10 -0
- package/dist/assets/channel-DPCihw7y.js +1 -0
- package/dist/assets/chunk-2KRD3SAO-Dc_tBGsw.js +1 -0
- package/dist/assets/chunk-336JU56O-Dhi-ID9Y.js +2 -0
- package/dist/assets/chunk-426QAEUC-DnFdrNMW.js +1 -0
- package/dist/assets/chunk-4BX2VUAB-Z63FkGov.js +1 -0
- package/dist/assets/chunk-4TB4RGXK-BAiBlfyy.js +206 -0
- package/dist/assets/chunk-55IACEB6-BXDWXbxy.js +1 -0
- package/dist/assets/chunk-5FUZZQ4R-C72e1c_O.js +62 -0
- package/dist/assets/chunk-5PVQY5BW-BBHW_uCu.js +2 -0
- package/dist/assets/chunk-67CJDMHE-3Cf_D9m6.js +1 -0
- package/dist/assets/chunk-7N4EOEYR-DAXUXJ2c.js +1 -0
- package/dist/assets/chunk-AA7GKIK3-Dr7fOryc.js +1 -0
- package/dist/assets/chunk-BSJP7CBP-BmsSs1Nt.js +1 -0
- package/dist/assets/chunk-CIAEETIT-QDzV-X_Y.js +1 -0
- package/dist/assets/chunk-EDXVE4YY-C25WFHxY.js +1 -0
- package/dist/assets/chunk-ENJZ2VHE-_OzxcZOU.js +10 -0
- package/dist/assets/chunk-FMBD7UC4-CjsTKY4u.js +15 -0
- package/dist/assets/chunk-FOC6F5B3-g-xaH5nc.js +1 -0
- package/dist/assets/chunk-ICPOFSXX-iKiUSjDK.js +121 -0
- package/dist/assets/chunk-K5T4RW27-CKR-lPBN.js +94 -0
- package/dist/assets/chunk-KGLVRYIC-DRccT-B_.js +1 -0
- package/dist/assets/chunk-LIHQZDEY-DTbMwMXj.js +1 -0
- package/dist/assets/chunk-ORNJ4GCN-DlerdcWX.js +1 -0
- package/dist/assets/chunk-OYMX7WX6-Dekv1on2.js +231 -0
- package/dist/assets/chunk-QZHKN3VN-BHu0RdKl.js +1 -0
- package/dist/assets/chunk-U2HBQHQK-BvtlVHAg.js +70 -0
- package/dist/assets/chunk-X2U36JSP-BI_g8mub.js +1 -0
- package/dist/assets/chunk-XPW4576I-B39JkmSE.js +32 -0
- package/dist/assets/chunk-YZCP3GAM-BfPcXRm2.js +1 -0
- package/dist/assets/chunk-ZZ45TVLE-Bg4q68wZ.js +1 -0
- package/dist/assets/classDiagram-6PBFFD2Q-p73p727_.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-C4Ftpivp.js +1 -0
- package/dist/assets/clone-CI9aUwHe.js +1 -0
- package/dist/assets/cose-bilkent-S5V4N54A-7BpAeDh5.js +1 -0
- package/dist/assets/cytoscape.esm-DoTFyJaN.js +321 -0
- package/dist/assets/dagre-CilMRazv.js +1 -0
- package/dist/assets/dagre-KV5264BT-DDMqpjkB.js +4 -0
- package/dist/assets/defaultLocale-Ck2Xxk-C.js +1 -0
- package/dist/assets/diagram-5BDNPKRD-BFeyfnCx.js +10 -0
- package/dist/assets/diagram-G4DWMVQ6-DoqT-PtF.js +24 -0
- package/dist/assets/diagram-MMDJMWI5-BPV6KADk.js +43 -0
- package/dist/assets/diagram-TYMM5635-okvcTBtl.js +24 -0
- package/dist/assets/dist-C_eddq6m.js +1 -0
- package/dist/assets/erDiagram-SMLLAGMA-Dl-Ixy8n.js +85 -0
- package/dist/assets/flatten-B8XIuT0x.js +1 -0
- package/dist/assets/flowDiagram-DWJPFMVM-CsqWAx5r.js +162 -0
- package/dist/assets/ganttDiagram-T4ZO3ILL-mIt6zVeF.js +292 -0
- package/dist/assets/gitGraph-7Q5UKJZL-COXHGMvj.js +1 -0
- package/dist/assets/gitGraphDiagram-UUTBAWPF-syVqZJX_.js +106 -0
- package/dist/assets/graphlib-Bpd0q3yO.js +1 -0
- package/dist/assets/index-BoggyWS0.css +2 -0
- package/dist/assets/index-aLvjHQW4.js +104 -0
- package/dist/assets/info-OMHHGYJF-B-0wfxwL.js +1 -0
- package/dist/assets/infoDiagram-42DDH7IO-C0_uqsVa.js +2 -0
- package/dist/assets/init-Bft5Ffpj.js +1 -0
- package/dist/assets/isEmpty-BrFi5AqV.js +1 -0
- package/dist/assets/ishikawaDiagram-UXIWVN3A-CTjFbDBV.js +70 -0
- package/dist/assets/journeyDiagram-VCZTEJTY-BDBcej1q.js +139 -0
- package/dist/assets/kanban-definition-6JOO6SKY-Ylgzakw7.js +89 -0
- package/dist/assets/katex-Uj9wLT16.js +265 -0
- package/dist/assets/line-CRxEwpOv.js +1 -0
- package/dist/assets/linear-PDPfFByd.js +1 -0
- package/dist/assets/mermaid-parser.core-CY-XNOOy.js +4 -0
- package/dist/assets/mermaid.core-BPlTADIX.js +11 -0
- package/dist/assets/mindmap-definition-QFDTVHPH-TefzJnBM.js +96 -0
- package/dist/assets/ordinal-DIg8h6NI.js +1 -0
- package/dist/assets/packet-4T2RLAQJ-BW1T_A-C.js +1 -0
- package/dist/assets/path-DfRbCp9y.js +1 -0
- package/dist/assets/pie-ZZUOXDRM-DkKU-SFu.js +1 -0
- package/dist/assets/pieDiagram-DEJITSTG-BCXuaeEy.js +30 -0
- package/dist/assets/quadrantDiagram-34T5L4WZ-VSBAicWL.js +7 -0
- package/dist/assets/radar-PYXPWWZC-CYvTacKJ.js +1 -0
- package/dist/assets/reduce-CV2X8n1a.js +1 -0
- package/dist/assets/requirementDiagram-MS252O5E-4NeL9Z6J.js +84 -0
- package/dist/assets/rough.esm-Bbn_-PMU.js +1 -0
- package/dist/assets/sankeyDiagram-XADWPNL6-DMBSDnrH.js +10 -0
- package/dist/assets/sequenceDiagram-FGHM5R23-DVpzcZUi.js +157 -0
- package/dist/assets/src-PKe5NtkK.js +1 -0
- package/dist/assets/stateDiagram-FHFEXIEX-BkHTlCjL.js +1 -0
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-nMeWu9fP.js +1 -0
- package/dist/assets/timeline-definition-GMOUNBTQ-CyLt92nf.js +120 -0
- package/dist/assets/treeView-SZITEDCU-BUgcJ4eR.js +1 -0
- package/dist/assets/treemap-W4RFUUIX-BIWGQ4Pw.js +1 -0
- package/dist/assets/vennDiagram-DHZGUBPP-BCK0xB_m.js +34 -0
- package/dist/assets/wardley-RL74JXVD-DMZZRlby.js +1 -0
- package/dist/assets/wardleyDiagram-NUSXRM2D-BisBgfsF.js +20 -0
- package/dist/assets/xychartDiagram-5P7HB3ND-D_REDciv.js +7 -0
- package/dist/favicon.svg +15 -0
- package/dist/index.html +14 -0
- package/dist/screenshot.png +0 -0
- package/index.html +13 -0
- package/package.json +105 -0
- package/public/favicon.svg +15 -0
- package/public/screenshot.png +0 -0
- package/server/index.test.ts +814 -0
- package/server/index.ts +736 -0
- package/server/preferences.test.ts +126 -0
- package/server/preferences.ts +76 -0
- package/src/App.tsx +1620 -0
- package/src/components/ActionButton.tsx +41 -0
- package/src/components/CommandPalette.tsx +191 -0
- package/src/components/CommentCard.tsx +556 -0
- package/src/components/CommentForm.tsx +285 -0
- package/src/components/CommentSidebar.tsx +428 -0
- package/src/components/ConfirmDialog.tsx +64 -0
- package/src/components/ContextMenu.tsx +220 -0
- package/src/components/DragHandles.tsx +48 -0
- package/src/components/FileExplorer.tsx +251 -0
- package/src/components/FileOpener.tsx +304 -0
- package/src/components/IconButton.tsx +32 -0
- package/src/components/KeyboardShortcutsPanel.tsx +136 -0
- package/src/components/MarkdownViewer.tsx +682 -0
- package/src/components/RawView.tsx +798 -0
- package/src/components/SearchBar.tsx +129 -0
- package/src/components/Separator.tsx +7 -0
- package/src/components/SettingsPanel.tsx +813 -0
- package/src/components/SplitIconButton.tsx +133 -0
- package/src/components/TabBar.tsx +594 -0
- package/src/components/TableOfContents.tsx +70 -0
- package/src/components/ThemeSelector.tsx +159 -0
- package/src/components/Toast.tsx +99 -0
- package/src/components/Toolbar.tsx +161 -0
- package/src/components/iconButtonVariants.ts +19 -0
- package/src/components/rawView.test.ts +291 -0
- package/src/contexts/SettingsContext.tsx +120 -0
- package/src/hooks/useAuthor.test.ts +58 -0
- package/src/hooks/useAuthor.ts +69 -0
- package/src/hooks/useAutoResize.ts +20 -0
- package/src/hooks/useCommentCardTriggers.ts +20 -0
- package/src/hooks/useComments.test.ts +773 -0
- package/src/hooks/useComments.ts +332 -0
- package/src/hooks/useContextMenu.ts +48 -0
- package/src/hooks/useContextMenuItems.ts +392 -0
- package/src/hooks/useDiffSnapshot.test.ts +130 -0
- package/src/hooks/useDiffSnapshot.ts +67 -0
- package/src/hooks/useDragHandles.ts +417 -0
- package/src/hooks/useFileWatcher.ts +45 -0
- package/src/hooks/useHeadingTracking.ts +84 -0
- package/src/hooks/useMermaidRenderer.ts +75 -0
- package/src/hooks/useModalState.ts +22 -0
- package/src/hooks/usePageVisible.test.ts +69 -0
- package/src/hooks/usePageVisible.ts +19 -0
- package/src/hooks/usePaneLayout.test.ts +108 -0
- package/src/hooks/usePaneLayout.ts +102 -0
- package/src/hooks/useRecentFiles.test.ts +103 -0
- package/src/hooks/useRecentFiles.ts +99 -0
- package/src/hooks/useResizablePanel.test.ts +84 -0
- package/src/hooks/useResizablePanel.ts +118 -0
- package/src/hooks/useSearch.test.ts +72 -0
- package/src/hooks/useSearch.ts +53 -0
- package/src/hooks/useSelection.ts +48 -0
- package/src/hooks/useSessionPersistence.test.ts +59 -0
- package/src/hooks/useSessionPersistence.ts +43 -0
- package/src/hooks/useTabs.test.ts +127 -0
- package/src/hooks/useTabs.ts +561 -0
- package/src/hooks/useThemePersistence.ts +41 -0
- package/src/hooks/useToast.ts +27 -0
- package/src/index.css +1047 -0
- package/src/lib/agent-prompts.test.ts +34 -0
- package/src/lib/agent-prompts.ts +68 -0
- package/src/lib/comment-editor-state.ts +6 -0
- package/src/lib/comment-parser.test.ts +1959 -0
- package/src/lib/comment-parser.ts +1021 -0
- package/src/lib/diff.test.ts +164 -0
- package/src/lib/diff.ts +139 -0
- package/src/lib/heading-slugs.test.ts +85 -0
- package/src/lib/heading-slugs.ts +44 -0
- package/src/lib/http.test.ts +43 -0
- package/src/lib/http.ts +29 -0
- package/src/lib/mermaid-highlights.test.ts +517 -0
- package/src/lib/mermaid-highlights.ts +936 -0
- package/src/lib/mermaid-renderer.test.ts +114 -0
- package/src/lib/mermaid-renderer.ts +89 -0
- package/src/lib/path-utils.test.ts +17 -0
- package/src/lib/path-utils.ts +7 -0
- package/src/lib/platform.test.ts +58 -0
- package/src/lib/platform.ts +14 -0
- package/src/lib/preferences-client.test.ts +177 -0
- package/src/lib/preferences-client.ts +94 -0
- package/src/lib/selection-resolver.test.ts +118 -0
- package/src/lib/selection-resolver.ts +37 -0
- package/src/lib/settings.test.ts +152 -0
- package/src/lib/settings.ts +78 -0
- package/src/lib/shortcut-label.tsx +18 -0
- package/src/lib/themes.ts +21 -0
- package/src/lib/visible-text.test.ts +86 -0
- package/src/lib/visible-text.ts +77 -0
- package/src/main.tsx +22 -0
- package/src/markdown/pipeline.test.ts +82 -0
- package/src/markdown/pipeline.ts +33 -0
- package/src/types.test.ts +43 -0
- package/src/types.ts +46 -0
- package/tsconfig.app.json +28 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +26 -0
- package/vite.config.ts +50 -0
|
@@ -0,0 +1,936 @@
|
|
|
1
|
+
export interface MermaidHighlightTheme {
|
|
2
|
+
background: string;
|
|
3
|
+
activeBackground: string;
|
|
4
|
+
color: string;
|
|
5
|
+
underline: string;
|
|
6
|
+
activeUnderline: string;
|
|
7
|
+
ring: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const MERMAID_BASE_D_ATTR = 'data-mermaid-base-d';
|
|
11
|
+
const MERMAID_BASE_TRANSFORM_ATTR = 'data-mermaid-base-transform';
|
|
12
|
+
const MERMAID_BASE_VIEWBOX_ATTR = 'data-mermaid-base-view-box';
|
|
13
|
+
const MERMAID_MIN_RANK_GAP = 32;
|
|
14
|
+
|
|
15
|
+
interface TranslateTransform {
|
|
16
|
+
x: number;
|
|
17
|
+
y: number;
|
|
18
|
+
suffix: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface MermaidNodeLayoutInfo {
|
|
22
|
+
bbox: DOMRect | SVGRect;
|
|
23
|
+
el: SVGGElement;
|
|
24
|
+
transform: TranslateTransform;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface MermaidRankLayout {
|
|
28
|
+
centerY: number;
|
|
29
|
+
items: MermaidNodeLayoutInfo[];
|
|
30
|
+
shift: number;
|
|
31
|
+
top: number;
|
|
32
|
+
bottom: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface Point {
|
|
36
|
+
x: number;
|
|
37
|
+
y: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type MermaidNodeGeometry =
|
|
41
|
+
| {
|
|
42
|
+
center: Point;
|
|
43
|
+
kind: 'rect';
|
|
44
|
+
height: number;
|
|
45
|
+
width: number;
|
|
46
|
+
}
|
|
47
|
+
| {
|
|
48
|
+
center: Point;
|
|
49
|
+
kind: 'circle';
|
|
50
|
+
radius: number;
|
|
51
|
+
}
|
|
52
|
+
| {
|
|
53
|
+
center: Point;
|
|
54
|
+
kind: 'ellipse';
|
|
55
|
+
rx: number;
|
|
56
|
+
ry: number;
|
|
57
|
+
}
|
|
58
|
+
| {
|
|
59
|
+
center: Point;
|
|
60
|
+
kind: 'polygon';
|
|
61
|
+
points: Point[];
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export function getMermaidHighlightTheme(rootStyles: CSSStyleDeclaration): MermaidHighlightTheme {
|
|
65
|
+
return {
|
|
66
|
+
background: rootStyles.getPropertyValue('--theme-comment-bg-opaque').trim(),
|
|
67
|
+
activeBackground:
|
|
68
|
+
rootStyles.getPropertyValue('--theme-comment-bg-hover-opaque').trim() ||
|
|
69
|
+
rootStyles.getPropertyValue('--theme-comment-bg-opaque').trim(),
|
|
70
|
+
color: rootStyles.getPropertyValue('--theme-text').trim(),
|
|
71
|
+
underline: rootStyles.getPropertyValue('--theme-comment-underline').trim(),
|
|
72
|
+
activeUnderline:
|
|
73
|
+
rootStyles.getPropertyValue('--theme-comment-underline-active').trim() ||
|
|
74
|
+
rootStyles.getPropertyValue('--theme-comment-underline').trim(),
|
|
75
|
+
ring: rootStyles.getPropertyValue('--theme-comment-ring').trim(),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function applyMermaidHighlightStyles(
|
|
80
|
+
el: HTMLElement,
|
|
81
|
+
theme: MermaidHighlightTheme,
|
|
82
|
+
active: boolean,
|
|
83
|
+
) {
|
|
84
|
+
el.style.backgroundColor = active ? theme.activeBackground : theme.background;
|
|
85
|
+
el.style.backgroundImage = 'none';
|
|
86
|
+
el.style.color = theme.color;
|
|
87
|
+
el.style.textDecoration = 'none';
|
|
88
|
+
el.style.borderBottom = `2px solid ${active ? theme.activeUnderline : theme.underline}`;
|
|
89
|
+
el.style.borderRadius = '2px';
|
|
90
|
+
el.style.cursor = 'pointer';
|
|
91
|
+
el.style.display = 'inline';
|
|
92
|
+
el.style.lineHeight = 'inherit';
|
|
93
|
+
el.style.padding = '0';
|
|
94
|
+
el.style.transition = 'none';
|
|
95
|
+
el.style.whiteSpace = 'pre-wrap';
|
|
96
|
+
el.style.wordBreak = 'break-word';
|
|
97
|
+
el.style.overflowWrap = 'break-word';
|
|
98
|
+
el.style.maxWidth = '100%';
|
|
99
|
+
el.style.outline = active ? `1px solid ${theme.ring}` : 'none';
|
|
100
|
+
el.style.outlineOffset = '0';
|
|
101
|
+
el.style.boxDecorationBreak = 'clone';
|
|
102
|
+
el.style.setProperty('-webkit-box-decoration-break', 'clone');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function getRenderedLabelHeight(contentRoot: HTMLElement) {
|
|
106
|
+
const rootRect = contentRoot.getBoundingClientRect();
|
|
107
|
+
let height = Math.max(rootRect.height, contentRoot.scrollHeight, contentRoot.offsetHeight);
|
|
108
|
+
const rootTop = rootRect.top;
|
|
109
|
+
|
|
110
|
+
for (const descendant of contentRoot.querySelectorAll('*')) {
|
|
111
|
+
const rect = descendant.getBoundingClientRect();
|
|
112
|
+
height = Math.max(height, rect.bottom - rootTop);
|
|
113
|
+
if (descendant instanceof HTMLElement) {
|
|
114
|
+
height = Math.max(height, descendant.scrollHeight, descendant.offsetHeight);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return Math.ceil(height);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function applyWrappedLabelTextStyles(contentRoot: HTMLElement, width: number) {
|
|
122
|
+
contentRoot.style.maxWidth = `${width}px`;
|
|
123
|
+
contentRoot.style.width = `${width}px`;
|
|
124
|
+
contentRoot.style.whiteSpace = 'pre-wrap';
|
|
125
|
+
contentRoot.style.overflowWrap = 'break-word';
|
|
126
|
+
contentRoot.style.wordBreak = 'break-word';
|
|
127
|
+
|
|
128
|
+
const blockEls = contentRoot.querySelectorAll('p, span, div');
|
|
129
|
+
for (const blockEl of blockEls) {
|
|
130
|
+
const el = blockEl as HTMLElement;
|
|
131
|
+
el.style.maxWidth = '100%';
|
|
132
|
+
el.style.whiteSpace = 'pre-wrap';
|
|
133
|
+
el.style.overflowWrap = 'break-word';
|
|
134
|
+
el.style.wordBreak = 'break-word';
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function rememberBaseAttribute(el: Element, attribute: string, storageAttribute: string) {
|
|
139
|
+
const stored = el.getAttribute(storageAttribute);
|
|
140
|
+
if (stored != null) return stored;
|
|
141
|
+
|
|
142
|
+
const current = el.getAttribute(attribute);
|
|
143
|
+
if (current != null) {
|
|
144
|
+
el.setAttribute(storageAttribute, current);
|
|
145
|
+
}
|
|
146
|
+
return current;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function parseTranslateTransform(transform: string | null): TranslateTransform | null {
|
|
150
|
+
if (!transform) return null;
|
|
151
|
+
|
|
152
|
+
const match = transform.match(
|
|
153
|
+
/^translate\(\s*([-+]?\d*\.?\d+(?:e[-+]?\d+)?)\s*(?:,|\s)\s*([-+]?\d*\.?\d+(?:e[-+]?\d+)?)\s*\)\s*(.*)$/i,
|
|
154
|
+
);
|
|
155
|
+
if (!match) return null;
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
x: Number.parseFloat(match[1]),
|
|
159
|
+
y: Number.parseFloat(match[2]),
|
|
160
|
+
suffix: match[3]?.trim() ?? '',
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function formatTranslateTransform(transform: TranslateTransform) {
|
|
165
|
+
return `translate(${transform.x}, ${transform.y})${transform.suffix ? ` ${transform.suffix}` : ''}`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function applyTranslate(point: Point, transform: TranslateTransform | null) {
|
|
169
|
+
return {
|
|
170
|
+
x: point.x + (transform?.x ?? 0),
|
|
171
|
+
y: point.y + (transform?.y ?? 0),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function distanceSquared(a: Point, b: Point) {
|
|
176
|
+
const dx = a.x - b.x;
|
|
177
|
+
const dy = a.y - b.y;
|
|
178
|
+
return dx * dx + dy * dy;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function parsePolygonPoints(points: string | null) {
|
|
182
|
+
if (!points) return [];
|
|
183
|
+
|
|
184
|
+
return points
|
|
185
|
+
.trim()
|
|
186
|
+
.split(/\s+/)
|
|
187
|
+
.map((pair) => pair.split(',').map((value) => Number.parseFloat(value)))
|
|
188
|
+
.filter((pair) => pair.length === 2 && pair.every((value) => Number.isFinite(value)))
|
|
189
|
+
.map(([x, y]) => ({ x, y }));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function pointInPolygon(point: Point, polygon: Point[]) {
|
|
193
|
+
let inside = false;
|
|
194
|
+
|
|
195
|
+
for (
|
|
196
|
+
let index = 0, prevIndex = polygon.length - 1;
|
|
197
|
+
index < polygon.length;
|
|
198
|
+
prevIndex = index, index += 1
|
|
199
|
+
) {
|
|
200
|
+
const current = polygon[index];
|
|
201
|
+
const previous = polygon[prevIndex];
|
|
202
|
+
const intersects =
|
|
203
|
+
current.y > point.y !== previous.y > point.y &&
|
|
204
|
+
point.x <
|
|
205
|
+
((previous.x - current.x) * (point.y - current.y)) / (previous.y - current.y) + current.x;
|
|
206
|
+
if (intersects) {
|
|
207
|
+
inside = !inside;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return inside;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function pointInsideGeometry(geometry: MermaidNodeGeometry, point: Point) {
|
|
215
|
+
switch (geometry.kind) {
|
|
216
|
+
case 'rect':
|
|
217
|
+
return (
|
|
218
|
+
point.x >= geometry.center.x - geometry.width / 2 &&
|
|
219
|
+
point.x <= geometry.center.x + geometry.width / 2 &&
|
|
220
|
+
point.y >= geometry.center.y - geometry.height / 2 &&
|
|
221
|
+
point.y <= geometry.center.y + geometry.height / 2
|
|
222
|
+
);
|
|
223
|
+
case 'circle':
|
|
224
|
+
return distanceSquared(point, geometry.center) <= geometry.radius * geometry.radius;
|
|
225
|
+
case 'ellipse': {
|
|
226
|
+
const dx = point.x - geometry.center.x;
|
|
227
|
+
const dy = point.y - geometry.center.y;
|
|
228
|
+
if (geometry.rx === 0 || geometry.ry === 0) return false;
|
|
229
|
+
return (dx * dx) / (geometry.rx * geometry.rx) + (dy * dy) / (geometry.ry * geometry.ry) <= 1;
|
|
230
|
+
}
|
|
231
|
+
case 'polygon':
|
|
232
|
+
return pointInPolygon(point, geometry.points);
|
|
233
|
+
default:
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function intersectRect(geometry: Extract<MermaidNodeGeometry, { kind: 'rect' }>, point: Point) {
|
|
239
|
+
let halfWidth = geometry.width / 2;
|
|
240
|
+
let halfHeight = geometry.height / 2;
|
|
241
|
+
const dx = point.x - geometry.center.x;
|
|
242
|
+
const dy = point.y - geometry.center.y;
|
|
243
|
+
|
|
244
|
+
if (Math.abs(dy) * halfWidth > Math.abs(dx) * halfHeight) {
|
|
245
|
+
if (dy < 0) halfHeight = -halfHeight;
|
|
246
|
+
return {
|
|
247
|
+
x: geometry.center.x + (dy === 0 ? 0 : (halfHeight * dx) / dy),
|
|
248
|
+
y: geometry.center.y + halfHeight,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (dx < 0) halfWidth = -halfWidth;
|
|
253
|
+
return {
|
|
254
|
+
x: geometry.center.x + halfWidth,
|
|
255
|
+
y: geometry.center.y + (dx === 0 ? 0 : (halfWidth * dy) / dx),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function intersectCircle(geometry: Extract<MermaidNodeGeometry, { kind: 'circle' }>, point: Point) {
|
|
260
|
+
const dx = point.x - geometry.center.x;
|
|
261
|
+
const dy = point.y - geometry.center.y;
|
|
262
|
+
const length = Math.hypot(dx, dy);
|
|
263
|
+
if (length === 0) return null;
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
x: geometry.center.x + (dx / length) * geometry.radius,
|
|
267
|
+
y: geometry.center.y + (dy / length) * geometry.radius,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function intersectEllipse(
|
|
272
|
+
geometry: Extract<MermaidNodeGeometry, { kind: 'ellipse' }>,
|
|
273
|
+
point: Point,
|
|
274
|
+
) {
|
|
275
|
+
const dx = point.x - geometry.center.x;
|
|
276
|
+
const dy = point.y - geometry.center.y;
|
|
277
|
+
const scale = Math.sqrt(
|
|
278
|
+
(dx * dx) / (geometry.rx * geometry.rx) + (dy * dy) / (geometry.ry * geometry.ry),
|
|
279
|
+
);
|
|
280
|
+
if (!Number.isFinite(scale) || scale === 0) return null;
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
x: geometry.center.x + dx / scale,
|
|
284
|
+
y: geometry.center.y + dy / scale,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function intersectLineSegments(start: Point, end: Point, edgeStart: Point, edgeEnd: Point) {
|
|
289
|
+
const denominator =
|
|
290
|
+
(end.x - start.x) * (edgeEnd.y - edgeStart.y) - (end.y - start.y) * (edgeEnd.x - edgeStart.x);
|
|
291
|
+
if (Math.abs(denominator) < 0.000001) return null;
|
|
292
|
+
|
|
293
|
+
const startToEdgeX = edgeStart.x - start.x;
|
|
294
|
+
const startToEdgeY = edgeStart.y - start.y;
|
|
295
|
+
const lineProgress =
|
|
296
|
+
(startToEdgeX * (edgeEnd.y - edgeStart.y) - startToEdgeY * (edgeEnd.x - edgeStart.x)) /
|
|
297
|
+
denominator;
|
|
298
|
+
const edgeProgress =
|
|
299
|
+
(startToEdgeX * (end.y - start.y) - startToEdgeY * (end.x - start.x)) / denominator;
|
|
300
|
+
|
|
301
|
+
if (lineProgress < 0 || lineProgress > 1 || edgeProgress < 0 || edgeProgress > 1) {
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
point: {
|
|
307
|
+
x: start.x + (end.x - start.x) * lineProgress,
|
|
308
|
+
y: start.y + (end.y - start.y) * lineProgress,
|
|
309
|
+
},
|
|
310
|
+
progress: lineProgress,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function intersectPolygon(
|
|
315
|
+
geometry: Extract<MermaidNodeGeometry, { kind: 'polygon' }>,
|
|
316
|
+
point: Point,
|
|
317
|
+
) {
|
|
318
|
+
const intersections = geometry.points
|
|
319
|
+
.map((current, index) => {
|
|
320
|
+
const next = geometry.points[(index + 1) % geometry.points.length];
|
|
321
|
+
return intersectLineSegments(geometry.center, point, current, next);
|
|
322
|
+
})
|
|
323
|
+
.filter(
|
|
324
|
+
(intersection): intersection is { point: Point; progress: number } => intersection != null,
|
|
325
|
+
)
|
|
326
|
+
.sort((a, b) => a.progress - b.progress);
|
|
327
|
+
|
|
328
|
+
return intersections[0]?.point ?? null;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function intersectNodeGeometry(geometry: MermaidNodeGeometry, point: Point) {
|
|
332
|
+
switch (geometry.kind) {
|
|
333
|
+
case 'rect':
|
|
334
|
+
return intersectRect(geometry, point);
|
|
335
|
+
case 'circle':
|
|
336
|
+
return intersectCircle(geometry, point);
|
|
337
|
+
case 'ellipse':
|
|
338
|
+
return intersectEllipse(geometry, point);
|
|
339
|
+
case 'polygon':
|
|
340
|
+
return intersectPolygon(geometry, point);
|
|
341
|
+
default:
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function getNodeGeometry(node: SVGGElement): MermaidNodeGeometry | null {
|
|
347
|
+
const nodeTransform = parseTranslateTransform(node.getAttribute('transform'));
|
|
348
|
+
if (!nodeTransform) return null;
|
|
349
|
+
|
|
350
|
+
const polygon = node.querySelector('polygon.label-container, polygon');
|
|
351
|
+
if (polygon instanceof SVGPolygonElement) {
|
|
352
|
+
const polygonTransform = parseTranslateTransform(polygon.getAttribute('transform'));
|
|
353
|
+
const points = parsePolygonPoints(polygon.getAttribute('points'))
|
|
354
|
+
.map((point) => applyTranslate(point, polygonTransform))
|
|
355
|
+
.map((point) => applyTranslate(point, nodeTransform));
|
|
356
|
+
if (points.length >= 3) {
|
|
357
|
+
return {
|
|
358
|
+
kind: 'polygon',
|
|
359
|
+
center: { x: nodeTransform.x, y: nodeTransform.y },
|
|
360
|
+
points,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const rect = node.querySelector('rect.label-container, rect.basic.label-container');
|
|
366
|
+
if (rect instanceof SVGRectElement) {
|
|
367
|
+
const rectTransform = parseTranslateTransform(rect.getAttribute('transform'));
|
|
368
|
+
const x = Number.parseFloat(rect.getAttribute('x') || '0') + (rectTransform?.x ?? 0);
|
|
369
|
+
const y = Number.parseFloat(rect.getAttribute('y') || '0') + (rectTransform?.y ?? 0);
|
|
370
|
+
const width = Number.parseFloat(rect.getAttribute('width') || '0');
|
|
371
|
+
const height = Number.parseFloat(rect.getAttribute('height') || '0');
|
|
372
|
+
if (Number.isFinite(width) && Number.isFinite(height) && width > 0 && height > 0) {
|
|
373
|
+
return {
|
|
374
|
+
kind: 'rect',
|
|
375
|
+
center: {
|
|
376
|
+
x: nodeTransform.x + x + width / 2,
|
|
377
|
+
y: nodeTransform.y + y + height / 2,
|
|
378
|
+
},
|
|
379
|
+
width,
|
|
380
|
+
height,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const circle = node.querySelector('circle');
|
|
386
|
+
if (circle instanceof SVGCircleElement) {
|
|
387
|
+
const circleTransform = parseTranslateTransform(circle.getAttribute('transform'));
|
|
388
|
+
const radius = Number.parseFloat(circle.getAttribute('r') || '0');
|
|
389
|
+
if (Number.isFinite(radius) && radius > 0) {
|
|
390
|
+
return {
|
|
391
|
+
kind: 'circle',
|
|
392
|
+
center: {
|
|
393
|
+
x:
|
|
394
|
+
nodeTransform.x +
|
|
395
|
+
Number.parseFloat(circle.getAttribute('cx') || '0') +
|
|
396
|
+
(circleTransform?.x ?? 0),
|
|
397
|
+
y:
|
|
398
|
+
nodeTransform.y +
|
|
399
|
+
Number.parseFloat(circle.getAttribute('cy') || '0') +
|
|
400
|
+
(circleTransform?.y ?? 0),
|
|
401
|
+
},
|
|
402
|
+
radius,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const ellipse = node.querySelector('ellipse');
|
|
408
|
+
if (ellipse instanceof SVGEllipseElement) {
|
|
409
|
+
const ellipseTransform = parseTranslateTransform(ellipse.getAttribute('transform'));
|
|
410
|
+
const rx = Number.parseFloat(ellipse.getAttribute('rx') || '0');
|
|
411
|
+
const ry = Number.parseFloat(ellipse.getAttribute('ry') || '0');
|
|
412
|
+
if (Number.isFinite(rx) && Number.isFinite(ry) && rx > 0 && ry > 0) {
|
|
413
|
+
return {
|
|
414
|
+
kind: 'ellipse',
|
|
415
|
+
center: {
|
|
416
|
+
x:
|
|
417
|
+
nodeTransform.x +
|
|
418
|
+
Number.parseFloat(ellipse.getAttribute('cx') || '0') +
|
|
419
|
+
(ellipseTransform?.x ?? 0),
|
|
420
|
+
y:
|
|
421
|
+
nodeTransform.y +
|
|
422
|
+
Number.parseFloat(ellipse.getAttribute('cy') || '0') +
|
|
423
|
+
(ellipseTransform?.y ?? 0),
|
|
424
|
+
},
|
|
425
|
+
rx,
|
|
426
|
+
ry,
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function findClosestNodeGeometry(geometries: MermaidNodeGeometry[], point: Point) {
|
|
435
|
+
return geometries.reduce<MermaidNodeGeometry | null>((closest, geometry) => {
|
|
436
|
+
if (closest == null) return geometry;
|
|
437
|
+
|
|
438
|
+
return distanceSquared(geometry.center, point) < distanceSquared(closest.center, point)
|
|
439
|
+
? geometry
|
|
440
|
+
: closest;
|
|
441
|
+
}, null);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function getMarkerForwardExtent(path: SVGPathElement) {
|
|
445
|
+
const markerUrl = path.getAttribute('marker-end');
|
|
446
|
+
const markerIdMatch = markerUrl?.match(/url\(#([^)]+)\)/);
|
|
447
|
+
const markerId = markerIdMatch?.[1];
|
|
448
|
+
if (!markerId) return 0;
|
|
449
|
+
|
|
450
|
+
const marker = path.ownerSVGElement?.querySelector(
|
|
451
|
+
`marker#${CSS.escape(markerId)}`,
|
|
452
|
+
) as SVGMarkerElement | null;
|
|
453
|
+
if (!marker) return 0;
|
|
454
|
+
|
|
455
|
+
const viewBox = marker.viewBox?.baseVal;
|
|
456
|
+
const refX = marker.refX?.baseVal?.value ?? Number.parseFloat(marker.getAttribute('refX') || '0');
|
|
457
|
+
let maxX = Number.isFinite(viewBox?.width) ? viewBox!.x + viewBox!.width : Number.NaN;
|
|
458
|
+
|
|
459
|
+
for (const child of marker.children) {
|
|
460
|
+
if (!(child instanceof SVGGraphicsElement)) continue;
|
|
461
|
+
try {
|
|
462
|
+
const bbox = child.getBBox();
|
|
463
|
+
if (Number.isFinite(bbox.width)) {
|
|
464
|
+
maxX = Number.isFinite(maxX) ? Math.max(maxX, bbox.x + bbox.width) : bbox.x + bbox.width;
|
|
465
|
+
}
|
|
466
|
+
} catch {
|
|
467
|
+
// Ignore marker children that cannot report bounds.
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (!Number.isFinite(maxX)) return 0;
|
|
472
|
+
|
|
473
|
+
const markerWidth =
|
|
474
|
+
marker.markerWidth?.baseVal?.value ??
|
|
475
|
+
Number.parseFloat(marker.getAttribute('markerWidth') || '0');
|
|
476
|
+
const scaleX =
|
|
477
|
+
viewBox && viewBox.width > 0 && Number.isFinite(markerWidth) && markerWidth > 0
|
|
478
|
+
? markerWidth / viewBox.width
|
|
479
|
+
: 1;
|
|
480
|
+
|
|
481
|
+
return Math.max(0, (maxX - refX) * scaleX);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
export function parseViewBox(viewBox: string | null) {
|
|
485
|
+
if (!viewBox) return null;
|
|
486
|
+
const parts = viewBox
|
|
487
|
+
.trim()
|
|
488
|
+
.split(/[\s,]+/)
|
|
489
|
+
.map((part) => Number.parseFloat(part));
|
|
490
|
+
|
|
491
|
+
if (parts.length !== 4 || parts.some((part) => !Number.isFinite(part))) {
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return { minX: parts[0], minY: parts[1], width: parts[2], height: parts[3] };
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function collectNodeRanks(root: SVGGElement) {
|
|
499
|
+
const nodeEls = root.querySelectorAll(':scope > g.nodes > g.node');
|
|
500
|
+
const ranks: MermaidRankLayout[] = [];
|
|
501
|
+
|
|
502
|
+
for (const nodeEl of nodeEls) {
|
|
503
|
+
const el = nodeEl as SVGGElement;
|
|
504
|
+
const baseTransform = rememberBaseAttribute(el, 'transform', MERMAID_BASE_TRANSFORM_ATTR);
|
|
505
|
+
const transform = parseTranslateTransform(baseTransform);
|
|
506
|
+
if (!transform) continue;
|
|
507
|
+
|
|
508
|
+
let bbox: DOMRect | SVGRect;
|
|
509
|
+
try {
|
|
510
|
+
bbox = el.getBBox();
|
|
511
|
+
} catch {
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
if (!Number.isFinite(bbox.height) || bbox.height <= 0) continue;
|
|
515
|
+
|
|
516
|
+
const top = transform.y + bbox.y;
|
|
517
|
+
const bottom = top + bbox.height;
|
|
518
|
+
const rank = ranks.find((candidate) => Math.abs(candidate.centerY - transform.y) < 1);
|
|
519
|
+
const info: MermaidNodeLayoutInfo = { el, transform, bbox };
|
|
520
|
+
|
|
521
|
+
if (rank) {
|
|
522
|
+
rank.items.push(info);
|
|
523
|
+
rank.top = Math.min(rank.top, top);
|
|
524
|
+
rank.bottom = Math.max(rank.bottom, bottom);
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
ranks.push({
|
|
529
|
+
centerY: transform.y,
|
|
530
|
+
items: [info],
|
|
531
|
+
shift: 0,
|
|
532
|
+
top,
|
|
533
|
+
bottom,
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
ranks.sort((a, b) => a.centerY - b.centerY);
|
|
538
|
+
return ranks;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function getRankShiftResolver(ranks: MermaidRankLayout[]) {
|
|
542
|
+
if (ranks.length === 0) {
|
|
543
|
+
return () => 0;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
return (y: number) => {
|
|
547
|
+
if (y <= ranks[0].centerY) return ranks[0].shift;
|
|
548
|
+
|
|
549
|
+
for (let index = 0; index < ranks.length - 1; index += 1) {
|
|
550
|
+
const current = ranks[index];
|
|
551
|
+
const next = ranks[index + 1];
|
|
552
|
+
if (y <= next.centerY) {
|
|
553
|
+
const span = next.centerY - current.centerY;
|
|
554
|
+
if (span <= 0) return next.shift;
|
|
555
|
+
const ratio = (y - current.centerY) / span;
|
|
556
|
+
return current.shift + (next.shift - current.shift) * ratio;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
return ranks[ranks.length - 1].shift;
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
interface PathCommand {
|
|
565
|
+
cmd: string;
|
|
566
|
+
values: number[];
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
export function parsePathCommands(pathData: string) {
|
|
570
|
+
const commands: PathCommand[] = [];
|
|
571
|
+
const commandRe = /([AaCcHhLlMmQqSsTtVvZz])([^AaCcHhLlMmQqSsTtVvZz]*)/g;
|
|
572
|
+
const numberRe = /[-+]?\d*\.?\d+(?:e[-+]?\d+)?/gi;
|
|
573
|
+
|
|
574
|
+
for (const match of pathData.matchAll(commandRe)) {
|
|
575
|
+
const cmd = match[1];
|
|
576
|
+
const values = (match[2].match(numberRe) || []).map((value) => Number.parseFloat(value));
|
|
577
|
+
commands.push({ cmd, values });
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
return commands;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
export function formatPathCommands(commands: PathCommand[]) {
|
|
584
|
+
return commands
|
|
585
|
+
.map(({ cmd, values }) => (values.length > 0 ? `${cmd}${values.join(' ')}` : cmd))
|
|
586
|
+
.join('');
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
export function transformPathY(pathData: string, transformY: (y: number) => number) {
|
|
590
|
+
const commands = parsePathCommands(pathData);
|
|
591
|
+
if (commands.some(({ cmd }) => cmd !== cmd.toUpperCase())) {
|
|
592
|
+
return pathData;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
for (const command of commands) {
|
|
596
|
+
switch (command.cmd) {
|
|
597
|
+
case 'M':
|
|
598
|
+
case 'L':
|
|
599
|
+
case 'T':
|
|
600
|
+
for (let index = 1; index < command.values.length; index += 2) {
|
|
601
|
+
command.values[index] = transformY(command.values[index]);
|
|
602
|
+
}
|
|
603
|
+
break;
|
|
604
|
+
case 'C':
|
|
605
|
+
for (let index = 0; index < command.values.length; index += 6) {
|
|
606
|
+
command.values[index + 1] = transformY(command.values[index + 1]);
|
|
607
|
+
command.values[index + 3] = transformY(command.values[index + 3]);
|
|
608
|
+
command.values[index + 5] = transformY(command.values[index + 5]);
|
|
609
|
+
}
|
|
610
|
+
break;
|
|
611
|
+
case 'S':
|
|
612
|
+
case 'Q':
|
|
613
|
+
for (let index = 0; index < command.values.length; index += 4) {
|
|
614
|
+
command.values[index + 1] = transformY(command.values[index + 1]);
|
|
615
|
+
command.values[index + 3] = transformY(command.values[index + 3]);
|
|
616
|
+
}
|
|
617
|
+
break;
|
|
618
|
+
case 'V':
|
|
619
|
+
for (let index = 0; index < command.values.length; index += 1) {
|
|
620
|
+
command.values[index] = transformY(command.values[index]);
|
|
621
|
+
}
|
|
622
|
+
break;
|
|
623
|
+
case 'A':
|
|
624
|
+
for (let index = 0; index < command.values.length; index += 7) {
|
|
625
|
+
command.values[index + 6] = transformY(command.values[index + 6]);
|
|
626
|
+
}
|
|
627
|
+
break;
|
|
628
|
+
default:
|
|
629
|
+
break;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
return formatPathCommands(commands);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function stabilizeMermaidRenderedLayout(container: HTMLElement) {
|
|
637
|
+
const svgs = container.querySelectorAll('.mermaid-block .mermaid-svg svg');
|
|
638
|
+
for (const svgEl of svgs) {
|
|
639
|
+
const svg = svgEl as SVGSVGElement;
|
|
640
|
+
const root = svg.querySelector('g.root');
|
|
641
|
+
const edgePaths = root?.querySelector(':scope > g.edgePaths');
|
|
642
|
+
const edgeLabels = root?.querySelector(':scope > g.edgeLabels');
|
|
643
|
+
if (!root || !edgePaths || !edgeLabels) continue;
|
|
644
|
+
|
|
645
|
+
const ranks = collectNodeRanks(root as SVGGElement);
|
|
646
|
+
if (ranks.length === 0) continue;
|
|
647
|
+
|
|
648
|
+
let previousBottom = Number.NEGATIVE_INFINITY;
|
|
649
|
+
let cumulativeShift = 0;
|
|
650
|
+
for (const rank of ranks) {
|
|
651
|
+
const shiftedTop = rank.top + cumulativeShift;
|
|
652
|
+
const additionalShift = Number.isFinite(previousBottom)
|
|
653
|
+
? Math.max(0, previousBottom + MERMAID_MIN_RANK_GAP - shiftedTop)
|
|
654
|
+
: 0;
|
|
655
|
+
cumulativeShift += additionalShift;
|
|
656
|
+
rank.shift = cumulativeShift;
|
|
657
|
+
previousBottom = rank.bottom + cumulativeShift;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
const shiftAtY = getRankShiftResolver(ranks);
|
|
661
|
+
|
|
662
|
+
for (const rank of ranks) {
|
|
663
|
+
for (const item of rank.items) {
|
|
664
|
+
item.el.setAttribute(
|
|
665
|
+
'transform',
|
|
666
|
+
formatTranslateTransform({
|
|
667
|
+
...item.transform,
|
|
668
|
+
y: item.transform.y + rank.shift,
|
|
669
|
+
}),
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
const edgeLabelEls = edgeLabels.querySelectorAll(':scope > g.edgeLabel');
|
|
675
|
+
for (const edgeLabelEl of edgeLabelEls) {
|
|
676
|
+
const el = edgeLabelEl as SVGGElement;
|
|
677
|
+
const baseTransform = rememberBaseAttribute(el, 'transform', MERMAID_BASE_TRANSFORM_ATTR);
|
|
678
|
+
const transform = parseTranslateTransform(baseTransform);
|
|
679
|
+
if (!transform) continue;
|
|
680
|
+
|
|
681
|
+
el.setAttribute(
|
|
682
|
+
'transform',
|
|
683
|
+
formatTranslateTransform({
|
|
684
|
+
...transform,
|
|
685
|
+
y: transform.y + shiftAtY(transform.y),
|
|
686
|
+
}),
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const edgePathEls = edgePaths.querySelectorAll('path');
|
|
691
|
+
for (const edgePathEl of edgePathEls) {
|
|
692
|
+
const el = edgePathEl as SVGPathElement;
|
|
693
|
+
const basePathData = rememberBaseAttribute(el, 'd', MERMAID_BASE_D_ATTR);
|
|
694
|
+
if (!basePathData) continue;
|
|
695
|
+
el.setAttribute(
|
|
696
|
+
'd',
|
|
697
|
+
transformPathY(basePathData, (y) => y + shiftAtY(y)),
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
const baseViewBox = parseViewBox(
|
|
702
|
+
rememberBaseAttribute(svg, 'viewBox', MERMAID_BASE_VIEWBOX_ATTR),
|
|
703
|
+
);
|
|
704
|
+
if (baseViewBox) {
|
|
705
|
+
svg.setAttribute(
|
|
706
|
+
'viewBox',
|
|
707
|
+
`${baseViewBox.minX} ${baseViewBox.minY} ${baseViewBox.width} ${baseViewBox.height + cumulativeShift}`,
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
export function stabilizeMermaidSvgSizing(container: HTMLElement) {
|
|
714
|
+
const svgs = container.querySelectorAll('.mermaid-block .mermaid-svg svg');
|
|
715
|
+
for (const svgEl of svgs) {
|
|
716
|
+
const svg = svgEl as SVGSVGElement;
|
|
717
|
+
const wrapper = svg.parentElement as HTMLElement | null;
|
|
718
|
+
const viewBox = svg.viewBox?.baseVal;
|
|
719
|
+
if (!wrapper || !viewBox || !viewBox.width || !viewBox.height) continue;
|
|
720
|
+
|
|
721
|
+
svg.style.width = '100%';
|
|
722
|
+
svg.style.maxWidth = `${viewBox.width}px`;
|
|
723
|
+
svg.style.minWidth = '0';
|
|
724
|
+
svg.style.height = 'auto';
|
|
725
|
+
svg.style.overflow = 'visible';
|
|
726
|
+
svg.setAttribute('preserveAspectRatio', 'xMidYMin meet');
|
|
727
|
+
wrapper.scrollLeft = 0;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function findPathPointOutsideGeometry(
|
|
732
|
+
path: SVGPathElement,
|
|
733
|
+
totalLength: number,
|
|
734
|
+
geometry: MermaidNodeGeometry,
|
|
735
|
+
) {
|
|
736
|
+
const sampleOffsets = [24, 48, 96, Math.max(0, totalLength - 1)].filter(
|
|
737
|
+
(offset, index, offsets) =>
|
|
738
|
+
offset > 0 && offset < totalLength && offsets.indexOf(offset) === index,
|
|
739
|
+
);
|
|
740
|
+
|
|
741
|
+
for (const offset of sampleOffsets) {
|
|
742
|
+
const samplePoint = path.getPointAtLength(Math.max(0, totalLength - offset));
|
|
743
|
+
const point = { x: samplePoint.x, y: samplePoint.y };
|
|
744
|
+
if (!pointInsideGeometry(geometry, point)) {
|
|
745
|
+
return point;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
return null;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// Mermaid computes edge intersections before we resize HTML labels. Once wrapped labels
|
|
753
|
+
// settle, we rebuild only the visible endpoint overlay so arrowheads stay aligned to the
|
|
754
|
+
// current node boundary without repainting the full connector above the node boxes.
|
|
755
|
+
function buildArrowEndpointSegment(
|
|
756
|
+
path: SVGPathElement,
|
|
757
|
+
geometry: MermaidNodeGeometry | null,
|
|
758
|
+
totalLength: number,
|
|
759
|
+
segmentLength = 1.5,
|
|
760
|
+
) {
|
|
761
|
+
const originalEnd = path.getPointAtLength(totalLength);
|
|
762
|
+
const endPoint = { x: originalEnd.x, y: originalEnd.y };
|
|
763
|
+
const externalPoint = geometry ? findPathPointOutsideGeometry(path, totalLength, geometry) : null;
|
|
764
|
+
const targetEnd =
|
|
765
|
+
geometry && externalPoint
|
|
766
|
+
? (intersectNodeGeometry(geometry, externalPoint) ?? endPoint)
|
|
767
|
+
: endPoint;
|
|
768
|
+
const directionPoint =
|
|
769
|
+
externalPoint ?? path.getPointAtLength(Math.max(0, totalLength - segmentLength));
|
|
770
|
+
const dx = targetEnd.x - directionPoint.x;
|
|
771
|
+
const dy = targetEnd.y - directionPoint.y;
|
|
772
|
+
const length = Math.hypot(dx, dy);
|
|
773
|
+
const markerForwardExtent = getMarkerForwardExtent(path);
|
|
774
|
+
|
|
775
|
+
if (!Number.isFinite(length) || length === 0) {
|
|
776
|
+
return `M${targetEnd.x},${targetEnd.y}L${targetEnd.x},${targetEnd.y}`;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
const endX = targetEnd.x - (dx / length) * markerForwardExtent;
|
|
780
|
+
const endY = targetEnd.y - (dy / length) * markerForwardExtent;
|
|
781
|
+
return `M${endX - (dx / length) * segmentLength},${endY - (dy / length) * segmentLength}L${endX},${endY}`;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function stabilizeMermaidArrowEndpoints(container: HTMLElement) {
|
|
785
|
+
const svgs = container.querySelectorAll('.mermaid-block .mermaid-svg svg');
|
|
786
|
+
for (const svgEl of svgs) {
|
|
787
|
+
const root = svgEl.querySelector('g.root');
|
|
788
|
+
const edgePaths = root?.querySelector(':scope > g.edgePaths');
|
|
789
|
+
const nodes = root?.querySelector(':scope > g.nodes');
|
|
790
|
+
const edgeLabels = root?.querySelector(':scope > g.edgeLabels');
|
|
791
|
+
if (!root || !edgePaths || !nodes || !edgeLabels) continue;
|
|
792
|
+
|
|
793
|
+
root.querySelector(':scope > g.edgeEndpointOverlays')?.remove();
|
|
794
|
+
|
|
795
|
+
const overlayGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
|
796
|
+
overlayGroup.setAttribute('class', 'edgeEndpointOverlays');
|
|
797
|
+
overlayGroup.setAttribute('pointer-events', 'none');
|
|
798
|
+
const nodeGeometries = Array.from(nodes.querySelectorAll(':scope > g.node'))
|
|
799
|
+
.map((node) => getNodeGeometry(node as SVGGElement))
|
|
800
|
+
.filter((geometry): geometry is MermaidNodeGeometry => geometry != null);
|
|
801
|
+
|
|
802
|
+
const paths = edgePaths.querySelectorAll('path[marker-end]');
|
|
803
|
+
for (const pathEl of paths) {
|
|
804
|
+
const path = pathEl as SVGPathElement;
|
|
805
|
+
const totalLength = path.getTotalLength();
|
|
806
|
+
if (!Number.isFinite(totalLength) || totalLength <= 0) continue;
|
|
807
|
+
|
|
808
|
+
const endPoint = path.getPointAtLength(totalLength);
|
|
809
|
+
const targetGeometry = findClosestNodeGeometry(nodeGeometries, {
|
|
810
|
+
x: endPoint.x,
|
|
811
|
+
y: endPoint.y,
|
|
812
|
+
});
|
|
813
|
+
const overlayPath = path.cloneNode(false) as SVGPathElement;
|
|
814
|
+
overlayPath.removeAttribute('id');
|
|
815
|
+
overlayPath.setAttribute('d', buildArrowEndpointSegment(path, targetGeometry, totalLength));
|
|
816
|
+
overlayPath.classList.add('mermaid-edge-endpoint-overlay');
|
|
817
|
+
overlayPath.setAttribute('data-source-edge-id', path.id);
|
|
818
|
+
overlayPath.setAttribute('style', 'stroke: transparent !important; fill: none !important;');
|
|
819
|
+
overlayGroup.appendChild(overlayPath);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
root.appendChild(nodes);
|
|
823
|
+
if (overlayGroup.childNodes.length > 0) {
|
|
824
|
+
root.appendChild(overlayGroup);
|
|
825
|
+
}
|
|
826
|
+
root.appendChild(edgeLabels);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
export function stabilizeMermaidLabelLayout(container: HTMLElement) {
|
|
831
|
+
const labels = container.querySelectorAll('.mermaid-block .node .label foreignObject');
|
|
832
|
+
for (const label of labels) {
|
|
833
|
+
const foreignObject = label as SVGForeignObjectElement;
|
|
834
|
+
const width = Number.parseFloat(foreignObject.getAttribute('width') || '');
|
|
835
|
+
if (!Number.isFinite(width) || width <= 0) continue;
|
|
836
|
+
|
|
837
|
+
const contentRoot = foreignObject.firstElementChild as HTMLElement | null;
|
|
838
|
+
if (!contentRoot) continue;
|
|
839
|
+
|
|
840
|
+
applyWrappedLabelTextStyles(contentRoot, width);
|
|
841
|
+
|
|
842
|
+
const measuredHeight = getRenderedLabelHeight(contentRoot);
|
|
843
|
+
if (!Number.isFinite(measuredHeight) || measuredHeight <= 0) continue;
|
|
844
|
+
|
|
845
|
+
const labelGroup = foreignObject.parentElement as SVGGElement | null;
|
|
846
|
+
const nodeGroup = labelGroup?.closest('.node') as SVGGElement | null;
|
|
847
|
+
const labelRect = nodeGroup?.querySelector(
|
|
848
|
+
'rect.label-container, rect.basic.label-container',
|
|
849
|
+
) as SVGRectElement | null;
|
|
850
|
+
|
|
851
|
+
foreignObject.setAttribute('height', String(measuredHeight));
|
|
852
|
+
|
|
853
|
+
if (labelGroup) {
|
|
854
|
+
labelGroup.setAttribute('transform', `translate(${-width / 2}, ${-measuredHeight / 2})`);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
if (labelRect) {
|
|
858
|
+
const paddedHeight = measuredHeight + 30;
|
|
859
|
+
const paddedWidth = width + 60;
|
|
860
|
+
labelRect.setAttribute('width', String(paddedWidth));
|
|
861
|
+
labelRect.setAttribute('height', String(paddedHeight));
|
|
862
|
+
labelRect.setAttribute('x', String(-paddedWidth / 2));
|
|
863
|
+
labelRect.setAttribute('y', String(-paddedHeight / 2));
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
const edgeLabels = container.querySelectorAll('.mermaid-block g.edgeLabel foreignObject');
|
|
868
|
+
for (const label of edgeLabels) {
|
|
869
|
+
const foreignObject = label as SVGForeignObjectElement;
|
|
870
|
+
const width = Number.parseFloat(foreignObject.getAttribute('width') || '');
|
|
871
|
+
const currentHeight = Number.parseFloat(foreignObject.getAttribute('height') || '');
|
|
872
|
+
if (!Number.isFinite(width) || width <= 0) continue;
|
|
873
|
+
|
|
874
|
+
const contentRoot = foreignObject.firstElementChild as HTMLElement | null;
|
|
875
|
+
if (!contentRoot) continue;
|
|
876
|
+
|
|
877
|
+
applyWrappedLabelTextStyles(contentRoot, width);
|
|
878
|
+
|
|
879
|
+
const measuredHeight = getRenderedLabelHeight(contentRoot);
|
|
880
|
+
const targetHeight = Math.max(
|
|
881
|
+
Number.isFinite(currentHeight) ? currentHeight : 0,
|
|
882
|
+
measuredHeight,
|
|
883
|
+
);
|
|
884
|
+
if (!Number.isFinite(targetHeight) || targetHeight <= 0) continue;
|
|
885
|
+
|
|
886
|
+
foreignObject.setAttribute('height', String(targetHeight));
|
|
887
|
+
|
|
888
|
+
const labelGroup = foreignObject.parentElement as SVGGElement | null;
|
|
889
|
+
if (labelGroup) {
|
|
890
|
+
labelGroup.setAttribute('transform', `translate(${-width / 2}, ${-targetHeight / 2})`);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
export function scheduleMermaidLayoutStabilization(container: HTMLElement) {
|
|
896
|
+
let disposed = false;
|
|
897
|
+
const frameIds: number[] = [];
|
|
898
|
+
const timeoutIds: number[] = [];
|
|
899
|
+
|
|
900
|
+
const run = () => {
|
|
901
|
+
if (disposed || !container.isConnected) return;
|
|
902
|
+
stabilizeMermaidLabelLayout(container);
|
|
903
|
+
stabilizeMermaidRenderedLayout(container);
|
|
904
|
+
stabilizeMermaidSvgSizing(container);
|
|
905
|
+
stabilizeMermaidArrowEndpoints(container);
|
|
906
|
+
};
|
|
907
|
+
|
|
908
|
+
const queueFramePasses = (remaining: number) => {
|
|
909
|
+
if (remaining <= 0) return;
|
|
910
|
+
const frameId = window.requestAnimationFrame(() => {
|
|
911
|
+
run();
|
|
912
|
+
queueFramePasses(remaining - 1);
|
|
913
|
+
});
|
|
914
|
+
frameIds.push(frameId);
|
|
915
|
+
};
|
|
916
|
+
|
|
917
|
+
run();
|
|
918
|
+
queueFramePasses(2);
|
|
919
|
+
timeoutIds.push(window.setTimeout(run, 50));
|
|
920
|
+
timeoutIds.push(window.setTimeout(run, 200));
|
|
921
|
+
if ('fonts' in document) {
|
|
922
|
+
void document.fonts.ready.then(() => {
|
|
923
|
+
run();
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
return () => {
|
|
928
|
+
disposed = true;
|
|
929
|
+
for (const frameId of frameIds) {
|
|
930
|
+
window.cancelAnimationFrame(frameId);
|
|
931
|
+
}
|
|
932
|
+
for (const timeoutId of timeoutIds) {
|
|
933
|
+
window.clearTimeout(timeoutId);
|
|
934
|
+
}
|
|
935
|
+
};
|
|
936
|
+
}
|