@bytechain.cn/colamd 1.5.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 (157) hide show
  1. package/.github/workflows/release.yml +66 -0
  2. package/.trae/documents/fix-mermaid-colors-and-sankey.md +50 -0
  3. package/CLAUDE.md +87 -0
  4. package/LICENSE +21 -0
  5. package/README.md +540 -0
  6. package/README_CN.md +543 -0
  7. package/demo.md +486 -0
  8. package/dist/main/index.js +735 -0
  9. package/dist/preload/index.js +71 -0
  10. package/dist/renderer/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  11. package/dist/renderer/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  12. package/dist/renderer/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  13. package/dist/renderer/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  14. package/dist/renderer/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  15. package/dist/renderer/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  16. package/dist/renderer/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  17. package/dist/renderer/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  18. package/dist/renderer/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  19. package/dist/renderer/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  20. package/dist/renderer/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  21. package/dist/renderer/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  22. package/dist/renderer/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  23. package/dist/renderer/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  24. package/dist/renderer/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  25. package/dist/renderer/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  26. package/dist/renderer/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  27. package/dist/renderer/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  28. package/dist/renderer/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  29. package/dist/renderer/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  30. package/dist/renderer/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  31. package/dist/renderer/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  32. package/dist/renderer/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  33. package/dist/renderer/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  34. package/dist/renderer/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  35. package/dist/renderer/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  36. package/dist/renderer/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  37. package/dist/renderer/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  38. package/dist/renderer/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  39. package/dist/renderer/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  40. package/dist/renderer/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  41. package/dist/renderer/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  42. package/dist/renderer/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  43. package/dist/renderer/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  44. package/dist/renderer/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  45. package/dist/renderer/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  46. package/dist/renderer/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  47. package/dist/renderer/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  48. package/dist/renderer/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  49. package/dist/renderer/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  50. package/dist/renderer/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  51. package/dist/renderer/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  52. package/dist/renderer/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  53. package/dist/renderer/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  54. package/dist/renderer/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  55. package/dist/renderer/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  56. package/dist/renderer/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  57. package/dist/renderer/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  58. package/dist/renderer/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  59. package/dist/renderer/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  60. package/dist/renderer/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  61. package/dist/renderer/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  62. package/dist/renderer/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  63. package/dist/renderer/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  64. package/dist/renderer/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  65. package/dist/renderer/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  66. package/dist/renderer/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  67. package/dist/renderer/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  68. package/dist/renderer/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  69. package/dist/renderer/assets/arc-tTbbM8LO.js +131 -0
  70. package/dist/renderer/assets/architectureDiagram-3BPJPVTR-CEgYow6c.js +8720 -0
  71. package/dist/renderer/assets/blockDiagram-GPEHLZMM-LHyVtPwW.js +3825 -0
  72. package/dist/renderer/assets/c4Diagram-AAUBKEIU-C1P1eJrf.js +2482 -0
  73. package/dist/renderer/assets/channel-upve91Tq.js +7 -0
  74. package/dist/renderer/assets/chunk-2J33WTMH-lag2vhq9.js +24 -0
  75. package/dist/renderer/assets/chunk-4BX2VUAB-BXJ8Ggh-.js +16 -0
  76. package/dist/renderer/assets/chunk-55IACEB6-CiBpxRa1.js +13 -0
  77. package/dist/renderer/assets/chunk-727SXJPM-ODeKQFXC.js +2016 -0
  78. package/dist/renderer/assets/chunk-AQP2D5EJ-BK7xJolB.js +1953 -0
  79. package/dist/renderer/assets/chunk-FMBD7UC4-BxpCZPtz.js +19 -0
  80. package/dist/renderer/assets/chunk-ND2GUHAM-CqqaU9Ue.js +116 -0
  81. package/dist/renderer/assets/chunk-QZHKN3VN-Biq_K124.js +19 -0
  82. package/dist/renderer/assets/classDiagram-4FO5ZUOK-Cq95X99o.js +23 -0
  83. package/dist/renderer/assets/classDiagram-v2-Q7XG4LA2-Cq95X99o.js +23 -0
  84. package/dist/renderer/assets/cose-bilkent-S5V4N54A-XasiD0bu.js +4942 -0
  85. package/dist/renderer/assets/cytoscape.esm-CpHeHM5e.js +30269 -0
  86. package/dist/renderer/assets/dagre-BM42HDAG-Nq84Gfx4.js +705 -0
  87. package/dist/renderer/assets/defaultLocale-B2RvLBDe.js +206 -0
  88. package/dist/renderer/assets/diagram-2AECGRRQ-DwuB1GWt.js +301 -0
  89. package/dist/renderer/assets/diagram-5GNKFQAL-C2tgeI1h.js +169 -0
  90. package/dist/renderer/assets/diagram-KO2AKTUF-D5KzjNBc.js +632 -0
  91. package/dist/renderer/assets/diagram-LMA3HP47-C12xHS1c.js +212 -0
  92. package/dist/renderer/assets/diagram-OG6HWLK6-CnxI9oEa.js +851 -0
  93. package/dist/renderer/assets/erDiagram-TEJ5UH35-D_uPaKwn.js +1227 -0
  94. package/dist/renderer/assets/flowDiagram-I6XJVG4X-B6q_1-tE.js +2332 -0
  95. package/dist/renderer/assets/ganttDiagram-6RSMTGT7-CFo7ifF9.js +3720 -0
  96. package/dist/renderer/assets/gitGraphDiagram-PVQCEYII-WSexHTnq.js +1373 -0
  97. package/dist/renderer/assets/graph-DyX_9f6d.js +1988 -0
  98. package/dist/renderer/assets/index-DW7LS8C1.js +72292 -0
  99. package/dist/renderer/assets/index-dyHEFYvY.css +2184 -0
  100. package/dist/renderer/assets/infoDiagram-5YYISTIA-DaeJdLRq.js +31 -0
  101. package/dist/renderer/assets/init-ZxktEp_H.js +16 -0
  102. package/dist/renderer/assets/ishikawaDiagram-YF4QCWOH-DDCZc35f.js +967 -0
  103. package/dist/renderer/assets/journeyDiagram-JHISSGLW-BEdmpAgl.js +1255 -0
  104. package/dist/renderer/assets/kanban-definition-UN3LZRKU-BEFtQcFb.js +1052 -0
  105. package/dist/renderer/assets/layout-CAJgQHdw.js +2610 -0
  106. package/dist/renderer/assets/linear-B2ggJ8Am.js +340 -0
  107. package/dist/renderer/assets/mindmap-definition-RKZ34NQL-DSxVgHB5.js +1180 -0
  108. package/dist/renderer/assets/ordinal-DSZU4PqD.js +76 -0
  109. package/dist/renderer/assets/pieDiagram-4H26LBE5-CwYoJBuL.js +246 -0
  110. package/dist/renderer/assets/quadrantDiagram-W4KKPZXB-CST9Fvg9.js +1344 -0
  111. package/dist/renderer/assets/requirementDiagram-4Y6WPE33-DtrH52jS.js +1204 -0
  112. package/dist/renderer/assets/sankeyDiagram-5OEKKPKP-ca1tPzJ_.js +1274 -0
  113. package/dist/renderer/assets/sequenceDiagram-3UESZ5HK-Dfp1EJZ7.js +4514 -0
  114. package/dist/renderer/assets/stateDiagram-AJRCARHV-Bha2QoNB.js +450 -0
  115. package/dist/renderer/assets/stateDiagram-v2-BHNVJYJU-DWgFUYu1.js +21 -0
  116. package/dist/renderer/assets/timeline-definition-PNZ67QCA-C3h_-OTj.js +1596 -0
  117. package/dist/renderer/assets/vennDiagram-CIIHVFJN-DFzjSrZi.js +2486 -0
  118. package/dist/renderer/assets/wardley-L42UT6IY-Cx-VbqoS.js +30699 -0
  119. package/dist/renderer/assets/wardleyDiagram-YWT4CUSO-S2D9XqX6.js +975 -0
  120. package/dist/renderer/assets/xychartDiagram-2RQKCTM6-Cfxigbts.js +1932 -0
  121. package/dist/renderer/index.html +19 -0
  122. package/docs/agent-diff-view.md +48 -0
  123. package/electron-builder.yml +57 -0
  124. package/electron.vite.config.ts +30 -0
  125. package/package.json +40 -0
  126. package/resources/entitlements.mac.plist +12 -0
  127. package/resources/icon.icns +0 -0
  128. package/resources/icon.png +0 -0
  129. package/resources/icon.svg +23 -0
  130. package/resources/templates/slides/icon.png +0 -0
  131. package/resources/templates/slides/slides-template.md +74 -0
  132. package/resources/templates/slides/template.html +535 -0
  133. package/scripts/afterPack.js +13 -0
  134. package/src/main/index.ts +881 -0
  135. package/src/preload/index.ts +110 -0
  136. package/src/renderer/editor/editor.ts +204 -0
  137. package/src/renderer/editor/html-view.ts +15 -0
  138. package/src/renderer/editor/plugins/index.ts +76 -0
  139. package/src/renderer/editor/plugins/math-plugin.ts +297 -0
  140. package/src/renderer/editor/plugins/mermaid-plugin-custom.css +431 -0
  141. package/src/renderer/editor/plugins/mermaid-plugin-dark.css +428 -0
  142. package/src/renderer/editor/plugins/mermaid-plugin-elegant.css +443 -0
  143. package/src/renderer/editor/plugins/mermaid-plugin-newsprint.css +208 -0
  144. package/src/renderer/editor/plugins/mermaid-plugin.css +111 -0
  145. package/src/renderer/editor/plugins/mermaid-plugin.ts +679 -0
  146. package/src/renderer/env.d.ts +7 -0
  147. package/src/renderer/index.html +18 -0
  148. package/src/renderer/main.ts +303 -0
  149. package/src/renderer/themes/base.css +509 -0
  150. package/src/renderer/themes/theme-manager.ts +40 -0
  151. package/themes/README.md +280 -0
  152. package/themes/elegant.css +664 -0
  153. package/themes/guizang.css +732 -0
  154. package/tsconfig.json +14 -0
  155. package/tsconfig.main.json +11 -0
  156. package/tsconfig.preload.json +11 -0
  157. package/tsconfig.renderer.json +12 -0
@@ -0,0 +1,735 @@
1
+ "use strict";
2
+ const electron = require("electron");
3
+ const path = require("path");
4
+ const promises = require("fs/promises");
5
+ const fs = require("fs");
6
+ const http = require("http");
7
+ const themesDir = path.join(electron.app.getPath("home"), ".colamd", "themes");
8
+ function ensureThemesDir() {
9
+ if (!fs.existsSync(themesDir)) {
10
+ promises.mkdir(themesDir, { recursive: true }).catch(() => {
11
+ });
12
+ }
13
+ }
14
+ const windowStates = /* @__PURE__ */ new Map();
15
+ let pendingFilePaths = [];
16
+ let pluginMenuItems = [];
17
+ function getState(win) {
18
+ let state = windowStates.get(win.id);
19
+ if (!state) {
20
+ state = { filePath: null, watcher: null, isInternalSave: false, debounceTimer: null, agentState: "idle", lastExternalChange: 0, agentCooldownTimer: null };
21
+ windowStates.set(win.id, state);
22
+ }
23
+ return state;
24
+ }
25
+ function getWinFromEvent(event) {
26
+ return electron.BrowserWindow.fromWebContents(event.sender);
27
+ }
28
+ function createWindow(filePath) {
29
+ const win = new electron.BrowserWindow({
30
+ width: 960,
31
+ height: 720,
32
+ minWidth: 600,
33
+ minHeight: 400,
34
+ titleBarStyle: "hiddenInset",
35
+ trafficLightPosition: { x: 16, y: 16 },
36
+ webPreferences: {
37
+ preload: path.join(__dirname, "../preload/index.js"),
38
+ contextIsolation: true,
39
+ nodeIntegration: false,
40
+ sandbox: false
41
+ }
42
+ });
43
+ const state = getState(win);
44
+ if (process.env.ELECTRON_RENDERER_URL) {
45
+ win.loadURL(process.env.ELECTRON_RENDERER_URL);
46
+ } else {
47
+ win.loadFile(path.join(__dirname, "../renderer/index.html"));
48
+ }
49
+ win.webContents.on("did-finish-load", () => {
50
+ if (filePath) {
51
+ loadFileInWindow(win, filePath);
52
+ }
53
+ });
54
+ win.on("closed", () => {
55
+ stopWatching(state);
56
+ windowStates.delete(win.id);
57
+ });
58
+ updateTitle(win);
59
+ return win;
60
+ }
61
+ function updateTitle(win) {
62
+ const state = getState(win);
63
+ const fileName = state.filePath ? path.basename(state.filePath) : "Untitled";
64
+ win.setTitle(`${fileName} — ColaMD`);
65
+ }
66
+ function suggestFileName(win, content) {
67
+ const state = getState(win);
68
+ if (state.filePath) return path.basename(state.filePath, ".md");
69
+ if (!content) return void 0;
70
+ const match = content.match(/^#\s+(.+)/m) || content.match(/^(.+)/m);
71
+ if (!match) return void 0;
72
+ return match[1].trim().replace(/[/\\:*?"<>|]/g, "").slice(0, 60) || void 0;
73
+ }
74
+ function stopWatching(state) {
75
+ if (state.watcher) {
76
+ state.watcher.close();
77
+ state.watcher = null;
78
+ }
79
+ if (state.agentCooldownTimer) {
80
+ clearTimeout(state.agentCooldownTimer);
81
+ state.agentCooldownTimer = null;
82
+ }
83
+ state.agentState = "idle";
84
+ state.lastExternalChange = 0;
85
+ }
86
+ function transitionAgentState(win, state, newState) {
87
+ if (state.agentCooldownTimer) {
88
+ clearTimeout(state.agentCooldownTimer);
89
+ state.agentCooldownTimer = null;
90
+ }
91
+ if (newState === "active") {
92
+ if (state.agentState !== "active") {
93
+ state.agentState = "active";
94
+ if (!win.isDestroyed()) win.webContents.send("agent-activity", "active");
95
+ }
96
+ state.agentCooldownTimer = setTimeout(() => {
97
+ transitionAgentState(win, state, "cooldown");
98
+ }, 3e3);
99
+ } else if (newState === "cooldown") {
100
+ state.agentState = "cooldown";
101
+ if (!win.isDestroyed()) win.webContents.send("agent-activity", "cooldown");
102
+ state.agentCooldownTimer = setTimeout(() => {
103
+ transitionAgentState(win, state, "idle");
104
+ }, 2e3);
105
+ } else {
106
+ state.agentState = "idle";
107
+ if (!win.isDestroyed()) win.webContents.send("agent-activity", "idle");
108
+ }
109
+ }
110
+ function watchFile(win, state) {
111
+ if (!state.filePath) return;
112
+ stopWatching(state);
113
+ const filePath = state.filePath;
114
+ state.watcher = fs.watch(filePath, (eventType) => {
115
+ if (eventType !== "change" || state.isInternalSave) return;
116
+ const now = Date.now();
117
+ const gap = now - state.lastExternalChange;
118
+ state.lastExternalChange = now;
119
+ if (gap > 0 && gap < 2e3) {
120
+ transitionAgentState(win, state, "active");
121
+ } else if (state.agentState === "active") {
122
+ transitionAgentState(win, state, "active");
123
+ }
124
+ if (state.debounceTimer) clearTimeout(state.debounceTimer);
125
+ state.debounceTimer = setTimeout(() => {
126
+ promises.readFile(filePath, "utf-8").then((data) => {
127
+ if (!win.isDestroyed()) win.webContents.send("file-changed", resolveImagePaths(data, filePath));
128
+ }).catch(() => {
129
+ });
130
+ }, 100);
131
+ });
132
+ }
133
+ function resolveImagePaths(content, filePath) {
134
+ const dir = path.dirname(filePath);
135
+ return content.replace(/!\[([^\]]*)\]\((?!https?:\/\/|file:\/\/|data:)([^)]+)\)/g, (_match, alt, src) => {
136
+ const abs = path.join(dir, src);
137
+ return `![${alt}](file://${abs})`;
138
+ });
139
+ }
140
+ function loadFileInWindow(win, filePath) {
141
+ promises.readFile(filePath, "utf-8").then((data) => {
142
+ const state = getState(win);
143
+ state.filePath = filePath;
144
+ watchFile(win, state);
145
+ updateTitle(win);
146
+ win.webContents.send("file-opened", { path: filePath, content: resolveImagePaths(data, filePath) });
147
+ }).catch(() => {
148
+ });
149
+ }
150
+ function findWindowForFile(filePath) {
151
+ for (const [id, state] of windowStates) {
152
+ if (state.filePath === filePath) {
153
+ return electron.BrowserWindow.fromId(id) || null;
154
+ }
155
+ }
156
+ return null;
157
+ }
158
+ function openFile(filePath) {
159
+ const existing = findWindowForFile(filePath);
160
+ if (existing) {
161
+ existing.focus();
162
+ return;
163
+ }
164
+ const emptyWin = findEmptyWindow();
165
+ if (emptyWin) {
166
+ loadFileInWindow(emptyWin, filePath);
167
+ emptyWin.focus();
168
+ return;
169
+ }
170
+ const win = createWindow(filePath);
171
+ win.focus();
172
+ }
173
+ function findEmptyWindow() {
174
+ for (const [id, state] of windowStates) {
175
+ if (!state.filePath) {
176
+ return electron.BrowserWindow.fromId(id) || null;
177
+ }
178
+ }
179
+ return null;
180
+ }
181
+ async function saveToPath(win, filePath, content) {
182
+ const state = getState(win);
183
+ try {
184
+ state.isInternalSave = true;
185
+ await promises.writeFile(filePath, content, "utf-8");
186
+ state.filePath = filePath;
187
+ watchFile(win, state);
188
+ updateTitle(win);
189
+ return true;
190
+ } catch {
191
+ return false;
192
+ } finally {
193
+ setTimeout(() => {
194
+ state.isInternalSave = false;
195
+ }, 100);
196
+ }
197
+ }
198
+ electron.ipcMain.on("open-external", (_event, url) => {
199
+ if (typeof url === "string" && (url.startsWith("https://") || url.startsWith("http://"))) {
200
+ electron.shell.openExternal(url);
201
+ }
202
+ });
203
+ electron.ipcMain.handle("open-file", async (event) => {
204
+ const win = getWinFromEvent(event);
205
+ if (!win) return null;
206
+ const result = await electron.dialog.showOpenDialog(win, {
207
+ filters: [
208
+ { name: "Markdown", extensions: ["md", "markdown", "mdown", "mkd"] },
209
+ { name: "Text", extensions: ["txt"] },
210
+ { name: "All Files", extensions: ["*"] }
211
+ ],
212
+ properties: ["openFile"]
213
+ });
214
+ if (result.canceled || result.filePaths.length === 0) return null;
215
+ const filePath = result.filePaths[0];
216
+ const state = getState(win);
217
+ if (!state.filePath) {
218
+ try {
219
+ const content = await promises.readFile(filePath, "utf-8");
220
+ state.filePath = filePath;
221
+ watchFile(win, state);
222
+ updateTitle(win);
223
+ return { path: filePath, content };
224
+ } catch {
225
+ return null;
226
+ }
227
+ } else {
228
+ openFile(filePath);
229
+ return null;
230
+ }
231
+ });
232
+ electron.ipcMain.handle("open-file-path", async (event, filePath) => {
233
+ const win = getWinFromEvent(event);
234
+ if (!win) return null;
235
+ const state = getState(win);
236
+ if (!state.filePath) {
237
+ try {
238
+ const content = await promises.readFile(filePath, "utf-8");
239
+ state.filePath = filePath;
240
+ watchFile(win, state);
241
+ updateTitle(win);
242
+ return { path: filePath, content };
243
+ } catch {
244
+ return null;
245
+ }
246
+ } else {
247
+ openFile(filePath);
248
+ return null;
249
+ }
250
+ });
251
+ electron.ipcMain.handle("save-file", async (event, content) => {
252
+ const win = getWinFromEvent(event);
253
+ if (!win) return false;
254
+ const state = getState(win);
255
+ if (!state.filePath) {
256
+ const result = await electron.dialog.showSaveDialog(win, {
257
+ defaultPath: suggestFileName(win, content),
258
+ filters: [
259
+ { name: "Markdown", extensions: ["md"] },
260
+ { name: "All Files", extensions: ["*"] }
261
+ ]
262
+ });
263
+ if (result.canceled || !result.filePath) return false;
264
+ state.filePath = result.filePath;
265
+ if (content.includes("kicker:") || content.includes("chip:")) {
266
+ const destDir = path.dirname(state.filePath);
267
+ try {
268
+ const files = await promises.readdir(slidesTemplateDir);
269
+ await Promise.all(files.filter((f) => f !== "slides-template.md").map(async (f) => {
270
+ const dest = path.join(destDir, f);
271
+ if (!fs.existsSync(dest)) await promises.copyFile(path.join(slidesTemplateDir, f), dest);
272
+ }));
273
+ } catch {
274
+ }
275
+ }
276
+ }
277
+ return saveToPath(win, state.filePath, content);
278
+ });
279
+ electron.ipcMain.handle("save-file-as", async (event, content) => {
280
+ const win = getWinFromEvent(event);
281
+ if (!win) return false;
282
+ const result = await electron.dialog.showSaveDialog(win, {
283
+ defaultPath: suggestFileName(win, content),
284
+ filters: [
285
+ { name: "Markdown", extensions: ["md"] },
286
+ { name: "All Files", extensions: ["*"] }
287
+ ]
288
+ });
289
+ if (result.canceled || !result.filePath) return false;
290
+ return saveToPath(win, result.filePath, content);
291
+ });
292
+ electron.ipcMain.handle("save-export-file", async (event, dataUrl, defaultName) => {
293
+ const win = getWinFromEvent(event);
294
+ if (!win) return false;
295
+ const ext = (defaultName.match(/\.(\w+)$/)?.[1] || "png").toLowerCase();
296
+ const result = await electron.dialog.showSaveDialog(win, {
297
+ defaultPath: defaultName,
298
+ filters: [{ name: `${ext.toUpperCase()} File`, extensions: [ext] }]
299
+ });
300
+ if (result.canceled || !result.filePath) return false;
301
+ try {
302
+ const base64 = dataUrl.replace(/^data:[^;]+;base64,/, "");
303
+ await promises.writeFile(result.filePath, Buffer.from(base64, "base64"));
304
+ return true;
305
+ } catch {
306
+ return false;
307
+ }
308
+ });
309
+ electron.ipcMain.handle("export-pdf", async (event) => {
310
+ const win = getWinFromEvent(event);
311
+ if (!win) return false;
312
+ const result = await electron.dialog.showSaveDialog(win, {
313
+ defaultPath: suggestFileName(win),
314
+ filters: [{ name: "PDF", extensions: ["pdf"] }]
315
+ });
316
+ if (result.canceled || !result.filePath) return false;
317
+ try {
318
+ const cssKey = await win.webContents.insertCSS(
319
+ "html, body { height: auto !important; overflow: visible !important; } #titlebar { display: none !important; } #editor { height: auto !important; overflow: visible !important; } #editor .ProseMirror { min-height: auto !important; } .mermaid-error { display: none !important; } .mermaid-loading { display: none !important; } svg .error-icon, svg .error-text { display: none !important; } .math-inline-raw, .math-block-raw, textarea.mermaid-source { display: none !important; }"
320
+ );
321
+ const pdfData = await win.webContents.printToPDF({
322
+ marginType: 0,
323
+ printBackground: true,
324
+ pageSize: "A4"
325
+ });
326
+ await win.webContents.removeInsertedCSS(cssKey);
327
+ await promises.writeFile(result.filePath, pdfData);
328
+ return true;
329
+ } catch {
330
+ return false;
331
+ }
332
+ });
333
+ electron.ipcMain.handle("export-html", async (event, htmlContent) => {
334
+ const win = getWinFromEvent(event);
335
+ if (!win) return false;
336
+ const result = await electron.dialog.showSaveDialog(win, {
337
+ defaultPath: suggestFileName(win),
338
+ filters: [{ name: "HTML", extensions: ["html"] }]
339
+ });
340
+ if (result.canceled || !result.filePath) return false;
341
+ try {
342
+ await promises.writeFile(result.filePath, htmlContent, "utf-8");
343
+ return true;
344
+ } catch {
345
+ return false;
346
+ }
347
+ });
348
+ const slidesTemplateDir = electron.app.isPackaged ? path.join(process.resourcesPath, "templates", "slides") : path.join(__dirname, "../../resources/templates/slides");
349
+ const slidesServers = /* @__PURE__ */ new Map();
350
+ const MIME = {
351
+ ".html": "text/html",
352
+ ".md": "text/plain",
353
+ ".css": "text/css",
354
+ ".js": "application/javascript",
355
+ ".png": "image/png",
356
+ ".jpg": "image/jpeg",
357
+ ".jpeg": "image/jpeg",
358
+ ".mp4": "video/mp4",
359
+ ".webm": "video/webm",
360
+ ".svg": "image/svg+xml",
361
+ ".ico": "image/x-icon"
362
+ };
363
+ function getOrCreateSlidesServer(dir) {
364
+ const existing = slidesServers.get(dir);
365
+ if (existing) return Promise.resolve(existing.port);
366
+ return new Promise((resolve, reject) => {
367
+ const server = http.createServer((req, res) => {
368
+ const url = req.url === "/" ? "/template.html" : req.url || "/";
369
+ const filePath = path.join(dir, url.split("?")[0]);
370
+ const ext = path.extname(filePath).toLowerCase();
371
+ const mime = MIME[ext] || "application/octet-stream";
372
+ try {
373
+ const data = fs.readFileSync(filePath);
374
+ res.writeHead(200, { "Content-Type": mime });
375
+ res.end(data);
376
+ } catch {
377
+ res.writeHead(404);
378
+ res.end("Not found");
379
+ }
380
+ });
381
+ server.listen(0, "127.0.0.1", () => {
382
+ const addr = server.address();
383
+ if (!addr || typeof addr === "string") {
384
+ reject(new Error("no port"));
385
+ return;
386
+ }
387
+ slidesServers.set(dir, { port: addr.port, server });
388
+ resolve(addr.port);
389
+ });
390
+ server.on("error", reject);
391
+ });
392
+ }
393
+ electron.ipcMain.handle("new-slides", async (event) => {
394
+ const win = getWinFromEvent(event);
395
+ if (!win) return null;
396
+ try {
397
+ const content = await promises.readFile(path.join(slidesTemplateDir, "slides-template.md"), "utf-8");
398
+ win.webContents.send("new-slides-content", content);
399
+ return true;
400
+ } catch {
401
+ return null;
402
+ }
403
+ });
404
+ electron.ipcMain.handle("open-as-slides", async (event, content) => {
405
+ const win = getWinFromEvent(event);
406
+ if (!win) return false;
407
+ const state = getState(win);
408
+ if (!state.filePath) {
409
+ const result = await electron.dialog.showSaveDialog(win, {
410
+ title: "Create New Slides",
411
+ defaultPath: "slides.md",
412
+ filters: [{ name: "Markdown", extensions: ["md"] }]
413
+ });
414
+ if (result.canceled || !result.filePath) return false;
415
+ try {
416
+ await promises.copyFile(path.join(slidesTemplateDir, "slides-template.md"), result.filePath);
417
+ loadFileInWindow(win, result.filePath);
418
+ state.filePath = result.filePath;
419
+ } catch {
420
+ return false;
421
+ }
422
+ }
423
+ if (content !== void 0 && state.filePath) {
424
+ try {
425
+ await promises.writeFile(state.filePath, content, "utf-8");
426
+ } catch {
427
+ }
428
+ }
429
+ const dir = path.dirname(state.filePath);
430
+ const mdName = path.basename(state.filePath);
431
+ const templateDest = path.join(dir, "template.html");
432
+ try {
433
+ await promises.copyFile(path.join(slidesTemplateDir, "template.html"), templateDest);
434
+ } catch {
435
+ return false;
436
+ }
437
+ if (mdName !== "slides.md") {
438
+ try {
439
+ let html = await promises.readFile(templateDest, "utf-8");
440
+ html = html.replace(/fetch\('slides\.md'\)/, `fetch('${mdName}')`);
441
+ await promises.writeFile(templateDest, html, "utf-8");
442
+ } catch {
443
+ }
444
+ }
445
+ try {
446
+ const port = await getOrCreateSlidesServer(dir);
447
+ electron.shell.openExternal(`http://127.0.0.1:${port}/template.html`);
448
+ return true;
449
+ } catch {
450
+ return false;
451
+ }
452
+ });
453
+ electron.ipcMain.handle("export-slides", async (event, content) => {
454
+ const win = getWinFromEvent(event);
455
+ if (!win) return false;
456
+ const state = getState(win);
457
+ if (!state.filePath) return false;
458
+ const srcDir = path.dirname(state.filePath);
459
+ const videoRefs = [...content.matchAll(/<!--\s*type:\s*video[^>]*src:\s*([^\s,>]+)/g)].map((m) => m[1].trim()).filter(Boolean);
460
+ const hasVideo = videoRefs.length > 0;
461
+ let destDir;
462
+ let destHtml;
463
+ if (hasVideo) {
464
+ const result = await electron.dialog.showSaveDialog(win, {
465
+ title: "Export Slides Folder",
466
+ defaultPath: path.join(srcDir, "slides-export"),
467
+ buttonLabel: "Export"
468
+ });
469
+ if (result.canceled || !result.filePath) return false;
470
+ destDir = result.filePath;
471
+ destHtml = path.join(destDir, "index.html");
472
+ await promises.mkdir(destDir, { recursive: true });
473
+ } else {
474
+ const result = await electron.dialog.showSaveDialog(win, {
475
+ title: "Export Slides",
476
+ defaultPath: path.join(srcDir, "slides.html"),
477
+ filters: [{ name: "HTML", extensions: ["html"] }]
478
+ });
479
+ if (result.canceled || !result.filePath) return false;
480
+ destDir = path.dirname(result.filePath);
481
+ destHtml = result.filePath;
482
+ }
483
+ let html = await promises.readFile(path.join(srcDir, "template.html"), "utf-8");
484
+ const escaped = content.replace(/`/g, "\\`").replace(/\$/g, "\\$");
485
+ html = html.replace(
486
+ /fetch\('[^']+'\)\s*\n?\s*\.then\(r => r\.text\(\)\)/,
487
+ `Promise.resolve(\`${escaped}\`)`
488
+ );
489
+ const imgMatches = [...content.matchAll(/!\[[^\]]*\]\((?!https?:\/\/|data:)([^)]+)\)/g)];
490
+ const inlinedImages = /* @__PURE__ */ new Map();
491
+ for (const m of imgMatches) {
492
+ const imgPath = m[1].trim();
493
+ if (inlinedImages.has(imgPath)) continue;
494
+ try {
495
+ const abs = path.join(srcDir, imgPath);
496
+ const buf = await promises.readFile(abs);
497
+ const ext = path.extname(imgPath).slice(1).toLowerCase();
498
+ const mime = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : ext === "png" ? "image/png" : ext === "gif" ? "image/gif" : ext === "webp" ? "image/webp" : ext === "svg" ? "image/svg+xml" : "image/png";
499
+ inlinedImages.set(imgPath, `data:${mime};base64,${buf.toString("base64")}`);
500
+ } catch {
501
+ }
502
+ }
503
+ for (const [src, dataUrl] of inlinedImages) {
504
+ html = html.replaceAll(`src="${src}"`, `src="${dataUrl}"`);
505
+ html = html.replaceAll(`src='${src}'`, `src='${dataUrl}'`);
506
+ }
507
+ if (hasVideo) {
508
+ for (const videoSrc of videoRefs) {
509
+ try {
510
+ await promises.copyFile(path.join(srcDir, videoSrc), path.join(destDir, videoSrc));
511
+ } catch {
512
+ }
513
+ }
514
+ }
515
+ await promises.writeFile(destHtml, html, "utf-8");
516
+ electron.shell.showItemInFolder(destHtml);
517
+ return true;
518
+ });
519
+ electron.ipcMain.handle("load-custom-theme", async (event) => {
520
+ const win = getWinFromEvent(event);
521
+ if (!win) return null;
522
+ const result = await electron.dialog.showOpenDialog(win, {
523
+ filters: [{ name: "CSS", extensions: ["css"] }],
524
+ properties: ["openFile"]
525
+ });
526
+ if (result.canceled || result.filePaths.length === 0) return null;
527
+ try {
528
+ const srcPath = result.filePaths[0];
529
+ const fileName = path.basename(srcPath);
530
+ const destPath = path.join(themesDir, fileName);
531
+ await promises.copyFile(srcPath, destPath);
532
+ const css = await promises.readFile(destPath, "utf-8");
533
+ buildMenu();
534
+ return { name: fileName, css };
535
+ } catch {
536
+ return null;
537
+ }
538
+ });
539
+ electron.ipcMain.handle("load-theme-css", async (_event, fileName) => {
540
+ try {
541
+ return await promises.readFile(path.join(themesDir, fileName), "utf-8");
542
+ } catch {
543
+ return null;
544
+ }
545
+ });
546
+ electron.ipcMain.handle("register-plugins", (_event, plugins) => {
547
+ pluginMenuItems = plugins.map((p) => ({
548
+ id: p.id,
549
+ label: p.name,
550
+ type: "checkbox",
551
+ checked: p.enabled,
552
+ click: () => sendToFocused("menu-toggle-plugin", p.id)
553
+ }));
554
+ buildMenu();
555
+ return true;
556
+ });
557
+ electron.ipcMain.handle("sync-plugin-state", (_event, id, enabled) => {
558
+ const item = pluginMenuItems.find((p) => p.id === id);
559
+ if (item) item.checked = enabled;
560
+ buildMenu();
561
+ });
562
+ function getFocusedWindow() {
563
+ return electron.BrowserWindow.getFocusedWindow();
564
+ }
565
+ function sendToFocused(channel, ...args) {
566
+ const win = getFocusedWindow();
567
+ if (win) win.webContents.send(channel, ...args);
568
+ }
569
+ function buildMenu() {
570
+ const isMac = process.platform === "darwin";
571
+ const customThemeItems = [];
572
+ try {
573
+ const files = fs.readdirSync(themesDir).filter((f) => f.endsWith(".css")).sort();
574
+ for (const file of files) {
575
+ customThemeItems.push({
576
+ label: file.replace(/\.css$/, ""),
577
+ click: async () => {
578
+ try {
579
+ const css = await promises.readFile(path.join(themesDir, file), "utf-8");
580
+ sendToFocused("set-theme", `custom:${file}`);
581
+ sendToFocused("set-custom-css", css);
582
+ } catch {
583
+ }
584
+ }
585
+ });
586
+ }
587
+ } catch {
588
+ }
589
+ const themeSubmenu = [
590
+ { label: "Light", click: () => sendToFocused("set-theme", "light") },
591
+ { label: "Dark", click: () => sendToFocused("set-theme", "dark") },
592
+ { label: "Elegant", click: () => sendToFocused("set-theme", "elegant") },
593
+ { label: "Newsprint", click: () => sendToFocused("set-theme", "newsprint") }
594
+ ];
595
+ if (customThemeItems.length > 0) {
596
+ themeSubmenu.push({ type: "separator" }, ...customThemeItems);
597
+ }
598
+ themeSubmenu.push({ type: "separator" }, {
599
+ label: "Import Theme...",
600
+ click: () => sendToFocused("menu-import-theme")
601
+ });
602
+ const template = [
603
+ ...isMac ? [{
604
+ label: "ColaMD",
605
+ submenu: [
606
+ { role: "about" },
607
+ { type: "separator" },
608
+ { role: "hide" },
609
+ { role: "hideOthers" },
610
+ { role: "unhide" },
611
+ { type: "separator" },
612
+ { role: "quit" }
613
+ ]
614
+ }] : [],
615
+ {
616
+ label: "File",
617
+ submenu: [
618
+ {
619
+ label: "New",
620
+ accelerator: "CmdOrCtrl+N",
621
+ click: () => createWindow()
622
+ },
623
+ {
624
+ label: "New Slides...",
625
+ accelerator: "CmdOrCtrl+Shift+N",
626
+ click: () => sendToFocused("menu-new-slides")
627
+ },
628
+ {
629
+ label: "Open...",
630
+ accelerator: "CmdOrCtrl+O",
631
+ click: () => sendToFocused("menu-open")
632
+ },
633
+ { type: "separator" },
634
+ {
635
+ label: "Save",
636
+ accelerator: "CmdOrCtrl+S",
637
+ click: () => sendToFocused("menu-save")
638
+ },
639
+ {
640
+ label: "Save As...",
641
+ accelerator: "CmdOrCtrl+Shift+S",
642
+ click: () => sendToFocused("menu-save-as")
643
+ },
644
+ { type: "separator" },
645
+ {
646
+ label: "Export PDF...",
647
+ click: () => sendToFocused("menu-export-pdf")
648
+ },
649
+ {
650
+ label: "Export HTML...",
651
+ click: () => sendToFocused("menu-export-html")
652
+ },
653
+ {
654
+ label: "Export Slides...",
655
+ click: () => sendToFocused("menu-export-slides")
656
+ },
657
+ {
658
+ label: "Open as Slides",
659
+ accelerator: "CmdOrCtrl+Shift+P",
660
+ click: () => sendToFocused("menu-open-as-slides")
661
+ },
662
+ { type: "separator" },
663
+ isMac ? { role: "close" } : { role: "quit" }
664
+ ]
665
+ },
666
+ {
667
+ label: "Edit",
668
+ submenu: [
669
+ { role: "undo" },
670
+ { role: "redo" },
671
+ { type: "separator" },
672
+ { role: "cut" },
673
+ { role: "copy" },
674
+ { role: "paste" },
675
+ { role: "selectAll" }
676
+ ]
677
+ },
678
+ {
679
+ label: "View",
680
+ submenu: [
681
+ { role: "resetZoom" },
682
+ { role: "zoomIn" },
683
+ { role: "zoomOut" },
684
+ { type: "separator" },
685
+ { role: "togglefullscreen" },
686
+ ...pluginMenuItems.length > 0 ? [{ type: "separator" }, { label: "Plugins", submenu: pluginMenuItems }] : []
687
+ ]
688
+ },
689
+ {
690
+ label: "Theme",
691
+ submenu: themeSubmenu
692
+ },
693
+ {
694
+ label: "Help",
695
+ submenu: [
696
+ {
697
+ label: "About ColaMD",
698
+ click: () => electron.shell.openExternal("https://github.com/marswaveai/colamd")
699
+ }
700
+ ]
701
+ }
702
+ ];
703
+ electron.Menu.setApplicationMenu(electron.Menu.buildFromTemplate(template));
704
+ }
705
+ electron.app.whenReady().then(() => {
706
+ ensureThemesDir();
707
+ buildMenu();
708
+ const args = process.argv.slice(electron.app.isPackaged ? 1 : 2);
709
+ const fileArgs = args.filter((arg) => !arg.startsWith("-"));
710
+ if (fileArgs.length > 0) {
711
+ pendingFilePaths = fileArgs;
712
+ }
713
+ if (pendingFilePaths.length > 0) {
714
+ for (const fp of pendingFilePaths) {
715
+ createWindow(fp);
716
+ }
717
+ pendingFilePaths = [];
718
+ } else {
719
+ createWindow();
720
+ }
721
+ electron.app.on("activate", () => {
722
+ if (electron.BrowserWindow.getAllWindows().length === 0) createWindow();
723
+ });
724
+ });
725
+ electron.app.on("window-all-closed", () => {
726
+ if (process.platform !== "darwin") electron.app.quit();
727
+ });
728
+ electron.app.on("open-file", (event, filePath) => {
729
+ event.preventDefault();
730
+ if (electron.app.isReady()) {
731
+ openFile(filePath);
732
+ } else {
733
+ pendingFilePaths.push(filePath);
734
+ }
735
+ });