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.
Files changed (207) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +169 -0
  3. package/bin/md-redline +255 -0
  4. package/bin/test-windows.ps1 +70 -0
  5. package/dist/assets/_baseFor-Ck08IaSF.js +1 -0
  6. package/dist/assets/arc-DI2g9LXK.js +1 -0
  7. package/dist/assets/architecture-YZFGNWBL-BDgMfc-b.js +1 -0
  8. package/dist/assets/architectureDiagram-Q4EWVU46-Dg1hcUEa.js +36 -0
  9. package/dist/assets/array-DOVTz2Mq.js +1 -0
  10. package/dist/assets/blockDiagram-DXYQGD6D-BAXkTCAk.js +132 -0
  11. package/dist/assets/c4Diagram-AHTNJAMY-BIkgwQSx.js +10 -0
  12. package/dist/assets/channel-DPCihw7y.js +1 -0
  13. package/dist/assets/chunk-2KRD3SAO-Dc_tBGsw.js +1 -0
  14. package/dist/assets/chunk-336JU56O-Dhi-ID9Y.js +2 -0
  15. package/dist/assets/chunk-426QAEUC-DnFdrNMW.js +1 -0
  16. package/dist/assets/chunk-4BX2VUAB-Z63FkGov.js +1 -0
  17. package/dist/assets/chunk-4TB4RGXK-BAiBlfyy.js +206 -0
  18. package/dist/assets/chunk-55IACEB6-BXDWXbxy.js +1 -0
  19. package/dist/assets/chunk-5FUZZQ4R-C72e1c_O.js +62 -0
  20. package/dist/assets/chunk-5PVQY5BW-BBHW_uCu.js +2 -0
  21. package/dist/assets/chunk-67CJDMHE-3Cf_D9m6.js +1 -0
  22. package/dist/assets/chunk-7N4EOEYR-DAXUXJ2c.js +1 -0
  23. package/dist/assets/chunk-AA7GKIK3-Dr7fOryc.js +1 -0
  24. package/dist/assets/chunk-BSJP7CBP-BmsSs1Nt.js +1 -0
  25. package/dist/assets/chunk-CIAEETIT-QDzV-X_Y.js +1 -0
  26. package/dist/assets/chunk-EDXVE4YY-C25WFHxY.js +1 -0
  27. package/dist/assets/chunk-ENJZ2VHE-_OzxcZOU.js +10 -0
  28. package/dist/assets/chunk-FMBD7UC4-CjsTKY4u.js +15 -0
  29. package/dist/assets/chunk-FOC6F5B3-g-xaH5nc.js +1 -0
  30. package/dist/assets/chunk-ICPOFSXX-iKiUSjDK.js +121 -0
  31. package/dist/assets/chunk-K5T4RW27-CKR-lPBN.js +94 -0
  32. package/dist/assets/chunk-KGLVRYIC-DRccT-B_.js +1 -0
  33. package/dist/assets/chunk-LIHQZDEY-DTbMwMXj.js +1 -0
  34. package/dist/assets/chunk-ORNJ4GCN-DlerdcWX.js +1 -0
  35. package/dist/assets/chunk-OYMX7WX6-Dekv1on2.js +231 -0
  36. package/dist/assets/chunk-QZHKN3VN-BHu0RdKl.js +1 -0
  37. package/dist/assets/chunk-U2HBQHQK-BvtlVHAg.js +70 -0
  38. package/dist/assets/chunk-X2U36JSP-BI_g8mub.js +1 -0
  39. package/dist/assets/chunk-XPW4576I-B39JkmSE.js +32 -0
  40. package/dist/assets/chunk-YZCP3GAM-BfPcXRm2.js +1 -0
  41. package/dist/assets/chunk-ZZ45TVLE-Bg4q68wZ.js +1 -0
  42. package/dist/assets/classDiagram-6PBFFD2Q-p73p727_.js +1 -0
  43. package/dist/assets/classDiagram-v2-HSJHXN6E-C4Ftpivp.js +1 -0
  44. package/dist/assets/clone-CI9aUwHe.js +1 -0
  45. package/dist/assets/cose-bilkent-S5V4N54A-7BpAeDh5.js +1 -0
  46. package/dist/assets/cytoscape.esm-DoTFyJaN.js +321 -0
  47. package/dist/assets/dagre-CilMRazv.js +1 -0
  48. package/dist/assets/dagre-KV5264BT-DDMqpjkB.js +4 -0
  49. package/dist/assets/defaultLocale-Ck2Xxk-C.js +1 -0
  50. package/dist/assets/diagram-5BDNPKRD-BFeyfnCx.js +10 -0
  51. package/dist/assets/diagram-G4DWMVQ6-DoqT-PtF.js +24 -0
  52. package/dist/assets/diagram-MMDJMWI5-BPV6KADk.js +43 -0
  53. package/dist/assets/diagram-TYMM5635-okvcTBtl.js +24 -0
  54. package/dist/assets/dist-C_eddq6m.js +1 -0
  55. package/dist/assets/erDiagram-SMLLAGMA-Dl-Ixy8n.js +85 -0
  56. package/dist/assets/flatten-B8XIuT0x.js +1 -0
  57. package/dist/assets/flowDiagram-DWJPFMVM-CsqWAx5r.js +162 -0
  58. package/dist/assets/ganttDiagram-T4ZO3ILL-mIt6zVeF.js +292 -0
  59. package/dist/assets/gitGraph-7Q5UKJZL-COXHGMvj.js +1 -0
  60. package/dist/assets/gitGraphDiagram-UUTBAWPF-syVqZJX_.js +106 -0
  61. package/dist/assets/graphlib-Bpd0q3yO.js +1 -0
  62. package/dist/assets/index-BoggyWS0.css +2 -0
  63. package/dist/assets/index-aLvjHQW4.js +104 -0
  64. package/dist/assets/info-OMHHGYJF-B-0wfxwL.js +1 -0
  65. package/dist/assets/infoDiagram-42DDH7IO-C0_uqsVa.js +2 -0
  66. package/dist/assets/init-Bft5Ffpj.js +1 -0
  67. package/dist/assets/isEmpty-BrFi5AqV.js +1 -0
  68. package/dist/assets/ishikawaDiagram-UXIWVN3A-CTjFbDBV.js +70 -0
  69. package/dist/assets/journeyDiagram-VCZTEJTY-BDBcej1q.js +139 -0
  70. package/dist/assets/kanban-definition-6JOO6SKY-Ylgzakw7.js +89 -0
  71. package/dist/assets/katex-Uj9wLT16.js +265 -0
  72. package/dist/assets/line-CRxEwpOv.js +1 -0
  73. package/dist/assets/linear-PDPfFByd.js +1 -0
  74. package/dist/assets/mermaid-parser.core-CY-XNOOy.js +4 -0
  75. package/dist/assets/mermaid.core-BPlTADIX.js +11 -0
  76. package/dist/assets/mindmap-definition-QFDTVHPH-TefzJnBM.js +96 -0
  77. package/dist/assets/ordinal-DIg8h6NI.js +1 -0
  78. package/dist/assets/packet-4T2RLAQJ-BW1T_A-C.js +1 -0
  79. package/dist/assets/path-DfRbCp9y.js +1 -0
  80. package/dist/assets/pie-ZZUOXDRM-DkKU-SFu.js +1 -0
  81. package/dist/assets/pieDiagram-DEJITSTG-BCXuaeEy.js +30 -0
  82. package/dist/assets/quadrantDiagram-34T5L4WZ-VSBAicWL.js +7 -0
  83. package/dist/assets/radar-PYXPWWZC-CYvTacKJ.js +1 -0
  84. package/dist/assets/reduce-CV2X8n1a.js +1 -0
  85. package/dist/assets/requirementDiagram-MS252O5E-4NeL9Z6J.js +84 -0
  86. package/dist/assets/rough.esm-Bbn_-PMU.js +1 -0
  87. package/dist/assets/sankeyDiagram-XADWPNL6-DMBSDnrH.js +10 -0
  88. package/dist/assets/sequenceDiagram-FGHM5R23-DVpzcZUi.js +157 -0
  89. package/dist/assets/src-PKe5NtkK.js +1 -0
  90. package/dist/assets/stateDiagram-FHFEXIEX-BkHTlCjL.js +1 -0
  91. package/dist/assets/stateDiagram-v2-QKLJ7IA2-nMeWu9fP.js +1 -0
  92. package/dist/assets/timeline-definition-GMOUNBTQ-CyLt92nf.js +120 -0
  93. package/dist/assets/treeView-SZITEDCU-BUgcJ4eR.js +1 -0
  94. package/dist/assets/treemap-W4RFUUIX-BIWGQ4Pw.js +1 -0
  95. package/dist/assets/vennDiagram-DHZGUBPP-BCK0xB_m.js +34 -0
  96. package/dist/assets/wardley-RL74JXVD-DMZZRlby.js +1 -0
  97. package/dist/assets/wardleyDiagram-NUSXRM2D-BisBgfsF.js +20 -0
  98. package/dist/assets/xychartDiagram-5P7HB3ND-D_REDciv.js +7 -0
  99. package/dist/favicon.svg +15 -0
  100. package/dist/index.html +14 -0
  101. package/dist/screenshot.png +0 -0
  102. package/index.html +13 -0
  103. package/package.json +105 -0
  104. package/public/favicon.svg +15 -0
  105. package/public/screenshot.png +0 -0
  106. package/server/index.test.ts +814 -0
  107. package/server/index.ts +736 -0
  108. package/server/preferences.test.ts +126 -0
  109. package/server/preferences.ts +76 -0
  110. package/src/App.tsx +1620 -0
  111. package/src/components/ActionButton.tsx +41 -0
  112. package/src/components/CommandPalette.tsx +191 -0
  113. package/src/components/CommentCard.tsx +556 -0
  114. package/src/components/CommentForm.tsx +285 -0
  115. package/src/components/CommentSidebar.tsx +428 -0
  116. package/src/components/ConfirmDialog.tsx +64 -0
  117. package/src/components/ContextMenu.tsx +220 -0
  118. package/src/components/DragHandles.tsx +48 -0
  119. package/src/components/FileExplorer.tsx +251 -0
  120. package/src/components/FileOpener.tsx +304 -0
  121. package/src/components/IconButton.tsx +32 -0
  122. package/src/components/KeyboardShortcutsPanel.tsx +136 -0
  123. package/src/components/MarkdownViewer.tsx +682 -0
  124. package/src/components/RawView.tsx +798 -0
  125. package/src/components/SearchBar.tsx +129 -0
  126. package/src/components/Separator.tsx +7 -0
  127. package/src/components/SettingsPanel.tsx +813 -0
  128. package/src/components/SplitIconButton.tsx +133 -0
  129. package/src/components/TabBar.tsx +594 -0
  130. package/src/components/TableOfContents.tsx +70 -0
  131. package/src/components/ThemeSelector.tsx +159 -0
  132. package/src/components/Toast.tsx +99 -0
  133. package/src/components/Toolbar.tsx +161 -0
  134. package/src/components/iconButtonVariants.ts +19 -0
  135. package/src/components/rawView.test.ts +291 -0
  136. package/src/contexts/SettingsContext.tsx +120 -0
  137. package/src/hooks/useAuthor.test.ts +58 -0
  138. package/src/hooks/useAuthor.ts +69 -0
  139. package/src/hooks/useAutoResize.ts +20 -0
  140. package/src/hooks/useCommentCardTriggers.ts +20 -0
  141. package/src/hooks/useComments.test.ts +773 -0
  142. package/src/hooks/useComments.ts +332 -0
  143. package/src/hooks/useContextMenu.ts +48 -0
  144. package/src/hooks/useContextMenuItems.ts +392 -0
  145. package/src/hooks/useDiffSnapshot.test.ts +130 -0
  146. package/src/hooks/useDiffSnapshot.ts +67 -0
  147. package/src/hooks/useDragHandles.ts +417 -0
  148. package/src/hooks/useFileWatcher.ts +45 -0
  149. package/src/hooks/useHeadingTracking.ts +84 -0
  150. package/src/hooks/useMermaidRenderer.ts +75 -0
  151. package/src/hooks/useModalState.ts +22 -0
  152. package/src/hooks/usePageVisible.test.ts +69 -0
  153. package/src/hooks/usePageVisible.ts +19 -0
  154. package/src/hooks/usePaneLayout.test.ts +108 -0
  155. package/src/hooks/usePaneLayout.ts +102 -0
  156. package/src/hooks/useRecentFiles.test.ts +103 -0
  157. package/src/hooks/useRecentFiles.ts +99 -0
  158. package/src/hooks/useResizablePanel.test.ts +84 -0
  159. package/src/hooks/useResizablePanel.ts +118 -0
  160. package/src/hooks/useSearch.test.ts +72 -0
  161. package/src/hooks/useSearch.ts +53 -0
  162. package/src/hooks/useSelection.ts +48 -0
  163. package/src/hooks/useSessionPersistence.test.ts +59 -0
  164. package/src/hooks/useSessionPersistence.ts +43 -0
  165. package/src/hooks/useTabs.test.ts +127 -0
  166. package/src/hooks/useTabs.ts +561 -0
  167. package/src/hooks/useThemePersistence.ts +41 -0
  168. package/src/hooks/useToast.ts +27 -0
  169. package/src/index.css +1047 -0
  170. package/src/lib/agent-prompts.test.ts +34 -0
  171. package/src/lib/agent-prompts.ts +68 -0
  172. package/src/lib/comment-editor-state.ts +6 -0
  173. package/src/lib/comment-parser.test.ts +1959 -0
  174. package/src/lib/comment-parser.ts +1021 -0
  175. package/src/lib/diff.test.ts +164 -0
  176. package/src/lib/diff.ts +139 -0
  177. package/src/lib/heading-slugs.test.ts +85 -0
  178. package/src/lib/heading-slugs.ts +44 -0
  179. package/src/lib/http.test.ts +43 -0
  180. package/src/lib/http.ts +29 -0
  181. package/src/lib/mermaid-highlights.test.ts +517 -0
  182. package/src/lib/mermaid-highlights.ts +936 -0
  183. package/src/lib/mermaid-renderer.test.ts +114 -0
  184. package/src/lib/mermaid-renderer.ts +89 -0
  185. package/src/lib/path-utils.test.ts +17 -0
  186. package/src/lib/path-utils.ts +7 -0
  187. package/src/lib/platform.test.ts +58 -0
  188. package/src/lib/platform.ts +14 -0
  189. package/src/lib/preferences-client.test.ts +177 -0
  190. package/src/lib/preferences-client.ts +94 -0
  191. package/src/lib/selection-resolver.test.ts +118 -0
  192. package/src/lib/selection-resolver.ts +37 -0
  193. package/src/lib/settings.test.ts +152 -0
  194. package/src/lib/settings.ts +78 -0
  195. package/src/lib/shortcut-label.tsx +18 -0
  196. package/src/lib/themes.ts +21 -0
  197. package/src/lib/visible-text.test.ts +86 -0
  198. package/src/lib/visible-text.ts +77 -0
  199. package/src/main.tsx +22 -0
  200. package/src/markdown/pipeline.test.ts +82 -0
  201. package/src/markdown/pipeline.ts +33 -0
  202. package/src/types.test.ts +43 -0
  203. package/src/types.ts +46 -0
  204. package/tsconfig.app.json +28 -0
  205. package/tsconfig.json +7 -0
  206. package/tsconfig.node.json +26 -0
  207. package/vite.config.ts +50 -0
@@ -0,0 +1,517 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { describe, expect, it } from 'vitest';
4
+ import {
5
+ applyMermaidHighlightStyles,
6
+ formatPathCommands,
7
+ formatTranslateTransform,
8
+ getMermaidHighlightTheme,
9
+ parsePathCommands,
10
+ parsePolygonPoints,
11
+ parseTranslateTransform,
12
+ parseViewBox,
13
+ pointInPolygon,
14
+ transformPathY,
15
+ } from './mermaid-highlights';
16
+
17
+ describe('parseTranslateTransform', () => {
18
+ it('parses "translate(10, 20)" with comma separator', () => {
19
+ expect(parseTranslateTransform('translate(10, 20)')).toEqual({
20
+ x: 10,
21
+ y: 20,
22
+ suffix: '',
23
+ });
24
+ });
25
+
26
+ it('parses "translate(10 20) scale(2)" with space separator and suffix', () => {
27
+ expect(parseTranslateTransform('translate(10 20) scale(2)')).toEqual({
28
+ x: 10,
29
+ y: 20,
30
+ suffix: 'scale(2)',
31
+ });
32
+ });
33
+
34
+ it('returns null for non-translate transforms', () => {
35
+ expect(parseTranslateTransform('scale(2)')).toBeNull();
36
+ expect(parseTranslateTransform('rotate(45)')).toBeNull();
37
+ });
38
+
39
+ it('returns null for null input', () => {
40
+ expect(parseTranslateTransform(null)).toBeNull();
41
+ });
42
+
43
+ it('returns null for empty string', () => {
44
+ expect(parseTranslateTransform('')).toBeNull();
45
+ });
46
+
47
+ it('handles negative numbers', () => {
48
+ expect(parseTranslateTransform('translate(-15, -25.5)')).toEqual({
49
+ x: -15,
50
+ y: -25.5,
51
+ suffix: '',
52
+ });
53
+ });
54
+
55
+ it('handles scientific notation numbers', () => {
56
+ expect(parseTranslateTransform('translate(1e2, 2.5e-1)')).toEqual({
57
+ x: 100,
58
+ y: 0.25,
59
+ suffix: '',
60
+ });
61
+ });
62
+
63
+ it('handles positive sign prefix', () => {
64
+ expect(parseTranslateTransform('translate(+10, +20)')).toEqual({
65
+ x: 10,
66
+ y: 20,
67
+ suffix: '',
68
+ });
69
+ });
70
+
71
+ it('handles decimal numbers without leading digit', () => {
72
+ expect(parseTranslateTransform('translate(.5, .75)')).toEqual({
73
+ x: 0.5,
74
+ y: 0.75,
75
+ suffix: '',
76
+ });
77
+ });
78
+ });
79
+
80
+ describe('formatTranslateTransform', () => {
81
+ it('formats { x: 10, y: 20, suffix: "" } to "translate(10, 20)"', () => {
82
+ expect(formatTranslateTransform({ x: 10, y: 20, suffix: '' })).toBe('translate(10, 20)');
83
+ });
84
+
85
+ it('appends suffix when present', () => {
86
+ expect(formatTranslateTransform({ x: 5, y: 15, suffix: 'scale(2)' })).toBe(
87
+ 'translate(5, 15) scale(2)',
88
+ );
89
+ });
90
+
91
+ it('round-trips through parse and format', () => {
92
+ const input = 'translate(42.5, -10.3)';
93
+ const parsed = parseTranslateTransform(input);
94
+ expect(parsed).not.toBeNull();
95
+ expect(formatTranslateTransform(parsed!)).toBe('translate(42.5, -10.3)');
96
+ });
97
+
98
+ it('round-trips with suffix', () => {
99
+ const input = 'translate(10, 20) scale(0.5)';
100
+ const parsed = parseTranslateTransform(input);
101
+ expect(parsed).not.toBeNull();
102
+ expect(formatTranslateTransform(parsed!)).toBe('translate(10, 20) scale(0.5)');
103
+ });
104
+ });
105
+
106
+ describe('parsePolygonPoints', () => {
107
+ it('parses "10,20 30,40" to point array', () => {
108
+ expect(parsePolygonPoints('10,20 30,40')).toEqual([
109
+ { x: 10, y: 20 },
110
+ { x: 30, y: 40 },
111
+ ]);
112
+ });
113
+
114
+ it('returns empty array for null', () => {
115
+ expect(parsePolygonPoints(null)).toEqual([]);
116
+ });
117
+
118
+ it('returns empty array for empty string', () => {
119
+ expect(parsePolygonPoints('')).toEqual([]);
120
+ });
121
+
122
+ it('filters out malformed pairs', () => {
123
+ expect(parsePolygonPoints('10,20 bad 30,40')).toEqual([
124
+ { x: 10, y: 20 },
125
+ { x: 30, y: 40 },
126
+ ]);
127
+ });
128
+
129
+ it('handles multiple whitespace separators', () => {
130
+ expect(parsePolygonPoints(' 10,20 30,40 50,60 ')).toEqual([
131
+ { x: 10, y: 20 },
132
+ { x: 30, y: 40 },
133
+ { x: 50, y: 60 },
134
+ ]);
135
+ });
136
+
137
+ it('parses negative coordinates', () => {
138
+ expect(parsePolygonPoints('-10,-20 30,-40')).toEqual([
139
+ { x: -10, y: -20 },
140
+ { x: 30, y: -40 },
141
+ ]);
142
+ });
143
+ });
144
+
145
+ describe('pointInPolygon', () => {
146
+ // Unit square: (0,0), (10,0), (10,10), (0,10)
147
+ const square = [
148
+ { x: 0, y: 0 },
149
+ { x: 10, y: 0 },
150
+ { x: 10, y: 10 },
151
+ { x: 0, y: 10 },
152
+ ];
153
+
154
+ it('returns true for a point inside a square polygon', () => {
155
+ expect(pointInPolygon({ x: 5, y: 5 }, square)).toBe(true);
156
+ });
157
+
158
+ it('returns false for a point outside', () => {
159
+ expect(pointInPolygon({ x: 15, y: 5 }, square)).toBe(false);
160
+ expect(pointInPolygon({ x: -1, y: 5 }, square)).toBe(false);
161
+ expect(pointInPolygon({ x: 5, y: -1 }, square)).toBe(false);
162
+ expect(pointInPolygon({ x: 5, y: 11 }, square)).toBe(false);
163
+ });
164
+
165
+ it('returns true for a point near the center of a triangle', () => {
166
+ const triangle = [
167
+ { x: 0, y: 0 },
168
+ { x: 10, y: 0 },
169
+ { x: 5, y: 10 },
170
+ ];
171
+ expect(pointInPolygon({ x: 5, y: 3 }, triangle)).toBe(true);
172
+ });
173
+
174
+ it('returns false for a point clearly outside a triangle', () => {
175
+ const triangle = [
176
+ { x: 0, y: 0 },
177
+ { x: 10, y: 0 },
178
+ { x: 5, y: 10 },
179
+ ];
180
+ expect(pointInPolygon({ x: 0, y: 10 }, triangle)).toBe(false);
181
+ });
182
+
183
+ it('returns false for an empty polygon', () => {
184
+ expect(pointInPolygon({ x: 5, y: 5 }, [])).toBe(false);
185
+ });
186
+ });
187
+
188
+ describe('parsePathCommands', () => {
189
+ it('parses "M0 0L10 20" into commands', () => {
190
+ expect(parsePathCommands('M0 0L10 20')).toEqual([
191
+ { cmd: 'M', values: [0, 0] },
192
+ { cmd: 'L', values: [10, 20] },
193
+ ]);
194
+ });
195
+
196
+ it('handles C (cubic bezier) commands', () => {
197
+ const result = parsePathCommands('C10 20 30 40 50 60');
198
+ expect(result).toEqual([{ cmd: 'C', values: [10, 20, 30, 40, 50, 60] }]);
199
+ });
200
+
201
+ it('handles S (smooth cubic) commands', () => {
202
+ const result = parsePathCommands('S10 20 30 40');
203
+ expect(result).toEqual([{ cmd: 'S', values: [10, 20, 30, 40] }]);
204
+ });
205
+
206
+ it('handles Q (quadratic) commands', () => {
207
+ const result = parsePathCommands('Q10 20 30 40');
208
+ expect(result).toEqual([{ cmd: 'Q', values: [10, 20, 30, 40] }]);
209
+ });
210
+
211
+ it('handles V (vertical line) commands', () => {
212
+ const result = parsePathCommands('V100');
213
+ expect(result).toEqual([{ cmd: 'V', values: [100] }]);
214
+ });
215
+
216
+ it('handles H (horizontal line) commands', () => {
217
+ const result = parsePathCommands('H200');
218
+ expect(result).toEqual([{ cmd: 'H', values: [200] }]);
219
+ });
220
+
221
+ it('handles Z (close path) with no values', () => {
222
+ const result = parsePathCommands('M0 0L10 10Z');
223
+ expect(result).toEqual([
224
+ { cmd: 'M', values: [0, 0] },
225
+ { cmd: 'L', values: [10, 10] },
226
+ { cmd: 'Z', values: [] },
227
+ ]);
228
+ });
229
+
230
+ it('handles A (arc) commands', () => {
231
+ const result = parsePathCommands('A25 25 0 0 1 50 75');
232
+ expect(result).toEqual([{ cmd: 'A', values: [25, 25, 0, 0, 1, 50, 75] }]);
233
+ });
234
+
235
+ it('handles negative numbers in path data', () => {
236
+ const result = parsePathCommands('M-10 -20L30 -40');
237
+ expect(result).toEqual([
238
+ { cmd: 'M', values: [-10, -20] },
239
+ { cmd: 'L', values: [30, -40] },
240
+ ]);
241
+ });
242
+
243
+ it('handles scientific notation in path data', () => {
244
+ const result = parsePathCommands('M1e2 2.5e-1');
245
+ expect(result).toEqual([{ cmd: 'M', values: [100, 0.25] }]);
246
+ });
247
+
248
+ it('parses complex multi-command paths', () => {
249
+ const result = parsePathCommands('M10 20L30 40C50 60 70 80 90 100Z');
250
+ expect(result).toHaveLength(4);
251
+ expect(result[0].cmd).toBe('M');
252
+ expect(result[1].cmd).toBe('L');
253
+ expect(result[2].cmd).toBe('C');
254
+ expect(result[3].cmd).toBe('Z');
255
+ });
256
+ });
257
+
258
+ describe('formatPathCommands', () => {
259
+ it('formats commands back to a path string', () => {
260
+ const commands = [
261
+ { cmd: 'M', values: [0, 0] },
262
+ { cmd: 'L', values: [10, 20] },
263
+ ];
264
+ expect(formatPathCommands(commands)).toBe('M0 0L10 20');
265
+ });
266
+
267
+ it('formats Z with no values', () => {
268
+ const commands = [
269
+ { cmd: 'M', values: [0, 0] },
270
+ { cmd: 'Z', values: [] },
271
+ ];
272
+ expect(formatPathCommands(commands)).toBe('M0 0Z');
273
+ });
274
+
275
+ it('round-trips with parsePathCommands', () => {
276
+ const original = 'M10 20L30 40C50 60 70 80 90 100Z';
277
+ const parsed = parsePathCommands(original);
278
+ const formatted = formatPathCommands(parsed);
279
+ // Re-parse to verify equivalence (whitespace may differ)
280
+ expect(parsePathCommands(formatted)).toEqual(parsed);
281
+ });
282
+
283
+ it('round-trips a complex path with arcs', () => {
284
+ const original = 'M0 0A25 25 0 0 1 50 75L100 200Z';
285
+ const parsed = parsePathCommands(original);
286
+ const formatted = formatPathCommands(parsed);
287
+ expect(parsePathCommands(formatted)).toEqual(parsed);
288
+ });
289
+ });
290
+
291
+ describe('transformPathY', () => {
292
+ const addTen = (y: number) => y + 10;
293
+
294
+ it('transforms M command Y values', () => {
295
+ const result = transformPathY('M0 5', addTen);
296
+ const parsed = parsePathCommands(result);
297
+ expect(parsed[0].values).toEqual([0, 15]);
298
+ });
299
+
300
+ it('transforms L command Y values', () => {
301
+ const result = transformPathY('M0 0L10 20', addTen);
302
+ const parsed = parsePathCommands(result);
303
+ expect(parsed[0].values).toEqual([0, 10]); // M: y 0 -> 10
304
+ expect(parsed[1].values).toEqual([10, 30]); // L: y 20 -> 30
305
+ });
306
+
307
+ it('transforms C command Y values at indices 1, 3, 5', () => {
308
+ const result = transformPathY('C10 20 30 40 50 60', addTen);
309
+ const parsed = parsePathCommands(result);
310
+ // indices 1, 3, 5 should have +10
311
+ expect(parsed[0].values).toEqual([10, 30, 30, 50, 50, 70]);
312
+ });
313
+
314
+ it('transforms S command Y values at indices 1 and 3', () => {
315
+ const result = transformPathY('S10 20 30 40', addTen);
316
+ const parsed = parsePathCommands(result);
317
+ expect(parsed[0].values).toEqual([10, 30, 30, 50]);
318
+ });
319
+
320
+ it('transforms Q command Y values at indices 1 and 3', () => {
321
+ const result = transformPathY('Q10 20 30 40', addTen);
322
+ const parsed = parsePathCommands(result);
323
+ expect(parsed[0].values).toEqual([10, 30, 30, 50]);
324
+ });
325
+
326
+ it('transforms V command values', () => {
327
+ const result = transformPathY('V50', addTen);
328
+ const parsed = parsePathCommands(result);
329
+ expect(parsed[0].values).toEqual([60]);
330
+ });
331
+
332
+ it('transforms A command Y value at index 6', () => {
333
+ const result = transformPathY('A25 25 0 0 1 50 75', addTen);
334
+ const parsed = parsePathCommands(result);
335
+ // Only index 6 (the final Y) should be transformed
336
+ expect(parsed[0].values).toEqual([25, 25, 0, 0, 1, 50, 85]);
337
+ });
338
+
339
+ it('returns original path for relative (lowercase) commands', () => {
340
+ const input = 'm0 5l10 20';
341
+ expect(transformPathY(input, addTen)).toBe(input);
342
+ });
343
+
344
+ it('returns original path when mix of absolute and relative commands', () => {
345
+ const input = 'M0 5l10 20';
346
+ expect(transformPathY(input, addTen)).toBe(input);
347
+ });
348
+
349
+ it('does not transform H command values (horizontal)', () => {
350
+ const result = transformPathY('H100', addTen);
351
+ const parsed = parsePathCommands(result);
352
+ expect(parsed[0].values).toEqual([100]); // H is unchanged
353
+ });
354
+
355
+ it('does not transform Z command', () => {
356
+ const result = transformPathY('M0 0L10 10Z', addTen);
357
+ const parsed = parsePathCommands(result);
358
+ expect(parsed[2].cmd).toBe('Z');
359
+ expect(parsed[2].values).toEqual([]);
360
+ });
361
+ });
362
+
363
+ describe('parseViewBox', () => {
364
+ it('parses "0 0 100 200"', () => {
365
+ expect(parseViewBox('0 0 100 200')).toEqual({
366
+ minX: 0,
367
+ minY: 0,
368
+ width: 100,
369
+ height: 200,
370
+ });
371
+ });
372
+
373
+ it('parses with comma separators', () => {
374
+ expect(parseViewBox('10,20,300,400')).toEqual({
375
+ minX: 10,
376
+ minY: 20,
377
+ width: 300,
378
+ height: 400,
379
+ });
380
+ });
381
+
382
+ it('parses negative min values', () => {
383
+ expect(parseViewBox('-50 -100 500 600')).toEqual({
384
+ minX: -50,
385
+ minY: -100,
386
+ width: 500,
387
+ height: 600,
388
+ });
389
+ });
390
+
391
+ it('returns null for null input', () => {
392
+ expect(parseViewBox(null)).toBeNull();
393
+ });
394
+
395
+ it('returns null for empty string', () => {
396
+ expect(parseViewBox('')).toBeNull();
397
+ });
398
+
399
+ it('returns null for too few values', () => {
400
+ expect(parseViewBox('0 0 100')).toBeNull();
401
+ });
402
+
403
+ it('returns null for too many values', () => {
404
+ expect(parseViewBox('0 0 100 200 300')).toBeNull();
405
+ });
406
+
407
+ it('returns null for non-numeric values', () => {
408
+ expect(parseViewBox('a b c d')).toBeNull();
409
+ });
410
+
411
+ it('handles extra whitespace', () => {
412
+ expect(parseViewBox(' 0 0 100 200 ')).toEqual({
413
+ minX: 0,
414
+ minY: 0,
415
+ width: 100,
416
+ height: 200,
417
+ });
418
+ });
419
+
420
+ it('parses fractional values', () => {
421
+ expect(parseViewBox('0.5 1.5 200.25 300.75')).toEqual({
422
+ minX: 0.5,
423
+ minY: 1.5,
424
+ width: 200.25,
425
+ height: 300.75,
426
+ });
427
+ });
428
+ });
429
+
430
+ describe('getMermaidHighlightTheme', () => {
431
+ it('reads CSS custom properties from rootStyles', () => {
432
+ const properties: Record<string, string> = {
433
+ '--theme-comment-bg-opaque': '#fff3cd',
434
+ '--theme-comment-bg-hover-opaque': '#ffeaa7',
435
+ '--theme-text': '#212529',
436
+ '--theme-comment-underline': '#f0ad4e',
437
+ '--theme-comment-underline-active': '#e08e0b',
438
+ '--theme-comment-ring': '#fd7e14',
439
+ };
440
+
441
+ const mockStyles = {
442
+ getPropertyValue: (prop: string) => properties[prop] ?? '',
443
+ } as CSSStyleDeclaration;
444
+
445
+ const theme = getMermaidHighlightTheme(mockStyles);
446
+ expect(theme).toEqual({
447
+ background: '#fff3cd',
448
+ activeBackground: '#ffeaa7',
449
+ color: '#212529',
450
+ underline: '#f0ad4e',
451
+ activeUnderline: '#e08e0b',
452
+ ring: '#fd7e14',
453
+ });
454
+ });
455
+
456
+ it('falls back for missing active/hover properties', () => {
457
+ const properties: Record<string, string> = {
458
+ '--theme-comment-bg-opaque': '#fff3cd',
459
+ '--theme-text': '#212529',
460
+ '--theme-comment-underline': '#f0ad4e',
461
+ '--theme-comment-ring': '#fd7e14',
462
+ };
463
+
464
+ const mockStyles = {
465
+ getPropertyValue: (prop: string) => properties[prop] ?? '',
466
+ } as CSSStyleDeclaration;
467
+
468
+ const theme = getMermaidHighlightTheme(mockStyles);
469
+ expect(theme.activeBackground).toBe('#fff3cd'); // falls back to background
470
+ expect(theme.activeUnderline).toBe('#f0ad4e'); // falls back to underline
471
+ });
472
+ });
473
+
474
+ describe('applyMermaidHighlightStyles', () => {
475
+ const theme = {
476
+ background: '#fff3cd',
477
+ activeBackground: '#ffeaa7',
478
+ color: '#212529',
479
+ underline: '#f0ad4e',
480
+ activeUnderline: '#e08e0b',
481
+ ring: '#fd7e14',
482
+ };
483
+
484
+ it('sets expected inline styles for active=true', () => {
485
+ const el = document.createElement('span');
486
+ applyMermaidHighlightStyles(el, theme, true);
487
+
488
+ // jsdom normalizes hex to rgb(), so compare against normalized values
489
+ expect(el.style.backgroundColor).toBe('rgb(255, 234, 167)'); // activeBackground
490
+ expect(el.style.backgroundImage).toBe('none');
491
+ expect(el.style.color).toBe('rgb(33, 37, 41)');
492
+ expect(el.style.textDecoration).toBe('none');
493
+ expect(el.style.borderRadius).toBe('2px');
494
+ expect(el.style.cursor).toBe('pointer');
495
+ expect(el.style.display).toBe('inline');
496
+ expect(el.style.whiteSpace).toBe('pre-wrap');
497
+ expect(el.style.wordBreak).toBe('break-word');
498
+ expect(el.style.maxWidth).toBe('100%');
499
+ // Verify active state uses ring for outline (not 'none')
500
+ expect(el.style.outline).not.toBe('none');
501
+ });
502
+
503
+ it('sets expected inline styles for active=false', () => {
504
+ const el = document.createElement('span');
505
+ applyMermaidHighlightStyles(el, theme, false);
506
+
507
+ expect(el.style.backgroundColor).toBe('rgb(255, 243, 205)'); // background
508
+ expect(el.style.outline).toBe('none');
509
+ });
510
+
511
+ it('sets box-decoration-break and webkit prefix', () => {
512
+ const el = document.createElement('span');
513
+ applyMermaidHighlightStyles(el, theme, false);
514
+
515
+ expect(el.style.boxDecorationBreak).toBe('clone');
516
+ });
517
+ });