mermaid-to-excalidraw-cli 0.1.1
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 +97 -0
- package/bin/mmd2excalidraw.mjs +392 -0
- package/package.json +49 -0
- package/vendor/markdown-to-text/index.mjs +80 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# mermaid-to-excalidraw-cli
|
|
2
|
+
|
|
3
|
+
[](LICENSE)
|
|
4
|
+
[](https://github.com/ayurkin/mermaid-to-excalidraw-cli/issues)
|
|
5
|
+
|
|
6
|
+
Convert Mermaid (`.mmd`) diagrams into Excalidraw (`.excalidraw`) using a headless Chromium runtime for accurate layout and text metrics.
|
|
7
|
+
|
|
8
|
+
## Why
|
|
9
|
+
|
|
10
|
+
- Mermaid is easy to edit as text.
|
|
11
|
+
- Excalidraw is great for visual docs and hand edits.
|
|
12
|
+
- This tool keeps Mermaid as the source of truth while generating readable Excalidraw output.
|
|
13
|
+
|
|
14
|
+
## Features
|
|
15
|
+
|
|
16
|
+
- High-fidelity conversion using real browser rendering (Playwright + Chromium)
|
|
17
|
+
- Converts single files or entire folders (recursive)
|
|
18
|
+
- Deterministic output suitable for version control
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install
|
|
24
|
+
npx playwright install chromium
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
For global usage:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm link
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
After publishing to npm:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm install -g mermaid-to-excalidraw-cli
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
mmd2excalidraw <input> [output]
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Examples
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# Convert a single file
|
|
49
|
+
mmd2excalidraw docs/architecture/service-overview.mmd
|
|
50
|
+
|
|
51
|
+
# Convert all .mmd files in a directory (recursive)
|
|
52
|
+
mmd2excalidraw docs/architecture
|
|
53
|
+
|
|
54
|
+
# Convert directory and write outputs to a separate folder
|
|
55
|
+
mmd2excalidraw docs/architecture -o docs/architecture
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Options
|
|
59
|
+
|
|
60
|
+
- `--font-size <number>`: Mermaid font size (default: 16)
|
|
61
|
+
- `--curve <linear|basis>`: Flowchart curve style (default: linear)
|
|
62
|
+
- `--max-edges <number>`: Max edges (default: 500)
|
|
63
|
+
- `--max-text-size <number>`: Max text size (default: 50000)
|
|
64
|
+
- `-o, --output <path>`: Output file or directory
|
|
65
|
+
- `-h, --help`: Show help
|
|
66
|
+
|
|
67
|
+
## Output
|
|
68
|
+
|
|
69
|
+
The CLI writes Excalidraw scene JSON with both shapes and text elements, ready to open in Excalidraw or the VS Code extension.
|
|
70
|
+
|
|
71
|
+
## Troubleshooting
|
|
72
|
+
|
|
73
|
+
- **Playwright error / Chromium missing**
|
|
74
|
+
Run `npx playwright install chromium`.
|
|
75
|
+
|
|
76
|
+
- **Conversion hangs**
|
|
77
|
+
Ensure no firewall blocks local loopback access.
|
|
78
|
+
|
|
79
|
+
## Development
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
node bin/mmd2excalidraw.mjs --help
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Publish
|
|
86
|
+
|
|
87
|
+
1. Update `version` in `package.json`
|
|
88
|
+
2. `npm publish`
|
|
89
|
+
|
|
90
|
+
## License
|
|
91
|
+
|
|
92
|
+
MIT
|
|
93
|
+
|
|
94
|
+
## Credits
|
|
95
|
+
|
|
96
|
+
This CLI is built on top of the official Excalidraw Mermaid converter:
|
|
97
|
+
https://github.com/excalidraw/mermaid-to-excalidraw
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
import http from "node:http";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { chromium } from "playwright";
|
|
9
|
+
|
|
10
|
+
const DEFAULTS = {
|
|
11
|
+
fontSize: 16,
|
|
12
|
+
curve: "linear",
|
|
13
|
+
maxEdges: 500,
|
|
14
|
+
maxTextSize: 50000,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function printHelp() {
|
|
18
|
+
const helpText = `Usage:
|
|
19
|
+
mmd2excalidraw <input> [output]
|
|
20
|
+
|
|
21
|
+
Options:
|
|
22
|
+
-o, --output <path> Output file or directory
|
|
23
|
+
--font-size <number> Mermaid font size (default: ${DEFAULTS.fontSize})
|
|
24
|
+
--curve <linear|basis> Mermaid flowchart curve (default: ${DEFAULTS.curve})
|
|
25
|
+
--max-edges <number> Max edges (default: ${DEFAULTS.maxEdges})
|
|
26
|
+
--max-text-size <number> Max text size (default: ${DEFAULTS.maxTextSize})
|
|
27
|
+
-h, --help Show this help
|
|
28
|
+
|
|
29
|
+
Notes:
|
|
30
|
+
Requires Playwright with Chromium installed:
|
|
31
|
+
npx playwright install chromium
|
|
32
|
+
|
|
33
|
+
Examples:
|
|
34
|
+
mmd2excalidraw docs/architecture/service-overview.mmd
|
|
35
|
+
mmd2excalidraw docs/architecture -o docs/architecture
|
|
36
|
+
`;
|
|
37
|
+
console.log(helpText);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function parseArgs(argv) {
|
|
41
|
+
const options = { ...DEFAULTS };
|
|
42
|
+
let output = null;
|
|
43
|
+
const positionals = [];
|
|
44
|
+
|
|
45
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
46
|
+
const arg = argv[i];
|
|
47
|
+
if (arg === "-h" || arg === "--help") {
|
|
48
|
+
printHelp();
|
|
49
|
+
process.exit(0);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (arg === "-o" || arg === "--output") {
|
|
53
|
+
output = argv[i + 1];
|
|
54
|
+
i += 1;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (arg === "--font-size") {
|
|
59
|
+
options.fontSize = Number.parseInt(argv[i + 1], 10);
|
|
60
|
+
i += 1;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (arg === "--curve") {
|
|
65
|
+
options.curve = argv[i + 1] || DEFAULTS.curve;
|
|
66
|
+
i += 1;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (arg === "--max-edges") {
|
|
71
|
+
options.maxEdges = Number.parseInt(argv[i + 1], 10);
|
|
72
|
+
i += 1;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (arg === "--max-text-size") {
|
|
77
|
+
options.maxTextSize = Number.parseInt(argv[i + 1], 10);
|
|
78
|
+
i += 1;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (arg.startsWith("--")) {
|
|
83
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
positionals.push(arg);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (positionals.length === 0) {
|
|
90
|
+
printHelp();
|
|
91
|
+
throw new Error("Missing input path.");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
input: positionals[0],
|
|
96
|
+
output: output ?? positionals[1] ?? null,
|
|
97
|
+
options,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function getPackageRoot() {
|
|
102
|
+
const scriptPath = fileURLToPath(import.meta.url);
|
|
103
|
+
return path.resolve(path.dirname(scriptPath), "..");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function getContentType(filePath) {
|
|
107
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
108
|
+
switch (ext) {
|
|
109
|
+
case ".js":
|
|
110
|
+
case ".mjs":
|
|
111
|
+
return "text/javascript";
|
|
112
|
+
case ".css":
|
|
113
|
+
return "text/css";
|
|
114
|
+
case ".json":
|
|
115
|
+
return "application/json";
|
|
116
|
+
case ".svg":
|
|
117
|
+
return "image/svg+xml";
|
|
118
|
+
case ".map":
|
|
119
|
+
return "application/json";
|
|
120
|
+
default:
|
|
121
|
+
return "application/octet-stream";
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function createServer() {
|
|
126
|
+
const packageRoot = getPackageRoot();
|
|
127
|
+
const moduleRoots = new Map([
|
|
128
|
+
[
|
|
129
|
+
"/modules/mermaid-to-excalidraw/",
|
|
130
|
+
path.join(
|
|
131
|
+
packageRoot,
|
|
132
|
+
"node_modules",
|
|
133
|
+
"@excalidraw",
|
|
134
|
+
"mermaid-to-excalidraw",
|
|
135
|
+
"dist"
|
|
136
|
+
),
|
|
137
|
+
],
|
|
138
|
+
[
|
|
139
|
+
"/modules/mermaid/",
|
|
140
|
+
path.join(packageRoot, "node_modules", "mermaid", "dist"),
|
|
141
|
+
],
|
|
142
|
+
[
|
|
143
|
+
"/modules/nanoid/",
|
|
144
|
+
path.join(packageRoot, "node_modules", "nanoid"),
|
|
145
|
+
],
|
|
146
|
+
[
|
|
147
|
+
"/modules/markdown-to-text/",
|
|
148
|
+
path.join(packageRoot, "vendor", "markdown-to-text"),
|
|
149
|
+
],
|
|
150
|
+
[
|
|
151
|
+
"/excalidraw/",
|
|
152
|
+
path.join(
|
|
153
|
+
packageRoot,
|
|
154
|
+
"node_modules",
|
|
155
|
+
"@excalidraw",
|
|
156
|
+
"excalidraw",
|
|
157
|
+
"dist"
|
|
158
|
+
),
|
|
159
|
+
],
|
|
160
|
+
[
|
|
161
|
+
"/react/",
|
|
162
|
+
path.join(packageRoot, "node_modules", "react", "umd"),
|
|
163
|
+
],
|
|
164
|
+
[
|
|
165
|
+
"/react-dom/",
|
|
166
|
+
path.join(packageRoot, "node_modules", "react-dom", "umd"),
|
|
167
|
+
],
|
|
168
|
+
]);
|
|
169
|
+
|
|
170
|
+
const importMap = {
|
|
171
|
+
imports: {
|
|
172
|
+
mermaid: "/modules/mermaid/mermaid.esm.mjs",
|
|
173
|
+
nanoid: "/modules/nanoid/index.browser.js",
|
|
174
|
+
"@excalidraw/markdown-to-text":
|
|
175
|
+
"/modules/markdown-to-text/index.mjs",
|
|
176
|
+
"@excalidraw/mermaid-to-excalidraw":
|
|
177
|
+
"/modules/mermaid-to-excalidraw/index.js",
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const html = `<!doctype html>
|
|
182
|
+
<html>
|
|
183
|
+
<head>
|
|
184
|
+
<meta charset="utf-8" />
|
|
185
|
+
<script type="importmap">${JSON.stringify(importMap)}</script>
|
|
186
|
+
<script src="/react/react.production.min.js"></script>
|
|
187
|
+
<script src="/react-dom/react-dom.production.min.js"></script>
|
|
188
|
+
<script src="/excalidraw/excalidraw.production.min.js"></script>
|
|
189
|
+
<script type="module">
|
|
190
|
+
import { parseMermaidToExcalidraw } from "@excalidraw/mermaid-to-excalidraw";
|
|
191
|
+
window.parseMermaidToExcalidraw = parseMermaidToExcalidraw;
|
|
192
|
+
</script>
|
|
193
|
+
</head>
|
|
194
|
+
<body></body>
|
|
195
|
+
</html>`;
|
|
196
|
+
|
|
197
|
+
const server = http.createServer(async (req, res) => {
|
|
198
|
+
try {
|
|
199
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
200
|
+
const pathname = url.pathname;
|
|
201
|
+
|
|
202
|
+
if (pathname === "/" || pathname === "/index.html") {
|
|
203
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
204
|
+
res.end(html);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
for (const [prefix, root] of moduleRoots.entries()) {
|
|
209
|
+
if (!pathname.startsWith(prefix)) {
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const relativePath = pathname.slice(prefix.length);
|
|
214
|
+
const safePath = path
|
|
215
|
+
.normalize(relativePath)
|
|
216
|
+
.replace(/^([/\\]*\.\.)+/, "");
|
|
217
|
+
const filePath = path.join(root, safePath);
|
|
218
|
+
const stat = await fs.stat(filePath).catch(() => null);
|
|
219
|
+
if (!stat || !stat.isFile()) {
|
|
220
|
+
res.writeHead(404);
|
|
221
|
+
res.end("Not found");
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const fileBuffer = await fs.readFile(filePath);
|
|
226
|
+
res.writeHead(200, { "Content-Type": getContentType(filePath) });
|
|
227
|
+
res.end(fileBuffer);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
res.writeHead(404);
|
|
232
|
+
res.end("Not found");
|
|
233
|
+
} catch (error) {
|
|
234
|
+
res.writeHead(500);
|
|
235
|
+
res.end("Server error");
|
|
236
|
+
console.error(error);
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
return new Promise((resolve, reject) => {
|
|
241
|
+
server.listen(0, "127.0.0.1", () => {
|
|
242
|
+
const address = server.address();
|
|
243
|
+
if (!address || typeof address === "string") {
|
|
244
|
+
reject(new Error("Failed to start server."));
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
resolve({ server, url: `http://127.0.0.1:${address.port}` });
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function listMermaidFiles(dir) {
|
|
253
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
254
|
+
const files = [];
|
|
255
|
+
|
|
256
|
+
for (const entry of entries) {
|
|
257
|
+
const fullPath = path.join(dir, entry.name);
|
|
258
|
+
if (entry.isDirectory()) {
|
|
259
|
+
files.push(...(await listMermaidFiles(fullPath)));
|
|
260
|
+
} else if (entry.isFile() && entry.name.endsWith(".mmd")) {
|
|
261
|
+
files.push(fullPath);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return files;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function resolveOutputPath(inputPath, outputPath) {
|
|
269
|
+
if (!outputPath) {
|
|
270
|
+
return inputPath.replace(/\.mmd$/, ".excalidraw");
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
const stat = await fs.stat(outputPath);
|
|
275
|
+
if (stat.isDirectory()) {
|
|
276
|
+
return path.join(
|
|
277
|
+
outputPath,
|
|
278
|
+
path.basename(inputPath).replace(/\.mmd$/, ".excalidraw")
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
} catch {
|
|
282
|
+
// Output does not exist yet; treat as file path.
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return outputPath;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async function convertMermaidFile(page, inputPath, outputPath, options) {
|
|
289
|
+
const mermaid = await fs.readFile(inputPath, "utf8");
|
|
290
|
+
|
|
291
|
+
const { elements, files } = await page.evaluate(
|
|
292
|
+
async ({ mermaidText, opts }) => {
|
|
293
|
+
const config = {
|
|
294
|
+
flowchart: { curve: opts.curve },
|
|
295
|
+
themeVariables: { fontSize: `${opts.fontSize}px` },
|
|
296
|
+
maxEdges: opts.maxEdges,
|
|
297
|
+
maxTextSize: opts.maxTextSize,
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const { elements, files } = await window.parseMermaidToExcalidraw(
|
|
301
|
+
mermaidText,
|
|
302
|
+
config
|
|
303
|
+
);
|
|
304
|
+
const convertedElements = window.ExcalidrawLib.convertToExcalidrawElements(
|
|
305
|
+
elements,
|
|
306
|
+
{ regenerateIds: false }
|
|
307
|
+
);
|
|
308
|
+
return { elements: convertedElements, files: files ?? {} };
|
|
309
|
+
},
|
|
310
|
+
{ mermaidText: mermaid, opts: options }
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
const scene = {
|
|
314
|
+
type: "excalidraw",
|
|
315
|
+
version: 2,
|
|
316
|
+
source: "https://excalidraw.com",
|
|
317
|
+
elements,
|
|
318
|
+
appState: { viewBackgroundColor: "#ffffff" },
|
|
319
|
+
files: files ?? {},
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
323
|
+
await fs.writeFile(outputPath, JSON.stringify(scene, null, 2), "utf8");
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async function run() {
|
|
327
|
+
const { input, output, options } = parseArgs(process.argv.slice(2));
|
|
328
|
+
const inputPath = path.resolve(process.cwd(), input);
|
|
329
|
+
const outputPath = output ? path.resolve(process.cwd(), output) : null;
|
|
330
|
+
|
|
331
|
+
const stats = await fs.stat(inputPath);
|
|
332
|
+
const { server, url } = await createServer();
|
|
333
|
+
const browser = await chromium.launch({ headless: true });
|
|
334
|
+
const page = await browser.newPage();
|
|
335
|
+
page.on("console", (msg) => {
|
|
336
|
+
console.log(`[browser:${msg.type()}] ${msg.text()}`);
|
|
337
|
+
});
|
|
338
|
+
page.on("pageerror", (error) => {
|
|
339
|
+
console.error(`[browser:error] ${error.message}`);
|
|
340
|
+
});
|
|
341
|
+
page.on("requestfailed", (request) => {
|
|
342
|
+
console.error(`[browser:requestfailed] ${request.url()} ${request.failure()?.errorText}`);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
try {
|
|
346
|
+
await page.goto(url, { waitUntil: "load" });
|
|
347
|
+
await page.waitForFunction(
|
|
348
|
+
() =>
|
|
349
|
+
window.parseMermaidToExcalidraw &&
|
|
350
|
+
window.ExcalidrawLib?.convertToExcalidrawElements,
|
|
351
|
+
{ timeout: 60000 }
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
if (stats.isDirectory()) {
|
|
355
|
+
const mermaidFiles = await listMermaidFiles(inputPath);
|
|
356
|
+
if (mermaidFiles.length === 0) {
|
|
357
|
+
console.log(`No .mmd files found under ${inputPath}`);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
for (const filePath of mermaidFiles) {
|
|
362
|
+
const relative = path.relative(inputPath, filePath);
|
|
363
|
+
const targetBase = outputPath ?? inputPath;
|
|
364
|
+
const targetPath = path
|
|
365
|
+
.join(targetBase, relative)
|
|
366
|
+
.replace(/\.mmd$/, ".excalidraw");
|
|
367
|
+
console.log(`Converting ${relative}`);
|
|
368
|
+
await convertMermaidFile(page, filePath, targetPath, options);
|
|
369
|
+
}
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (!inputPath.endsWith(".mmd")) {
|
|
374
|
+
throw new Error("Input file must have .mmd extension.");
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const finalOutput = await resolveOutputPath(inputPath, outputPath);
|
|
378
|
+
console.log(`Converting ${path.basename(inputPath)}`);
|
|
379
|
+
await convertMermaidFile(page, inputPath, finalOutput, options);
|
|
380
|
+
} finally {
|
|
381
|
+
await page.close();
|
|
382
|
+
await browser.close();
|
|
383
|
+
server.close();
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
run()
|
|
388
|
+
.then(() => process.exit(0))
|
|
389
|
+
.catch((error) => {
|
|
390
|
+
console.error(error.message || error);
|
|
391
|
+
process.exit(1);
|
|
392
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mermaid-to-excalidraw-cli",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Convert Mermaid (.mmd) to Excalidraw (.excalidraw) using headless Chromium.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Aleksandr Yurkin <alx.yurkin@gmail.com>",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"bin": {
|
|
9
|
+
"mmd2excalidraw": "bin/mmd2excalidraw.mjs"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"start": "node bin/mmd2excalidraw.mjs --help"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"bin",
|
|
16
|
+
"vendor",
|
|
17
|
+
"README.md",
|
|
18
|
+
"LICENSE"
|
|
19
|
+
],
|
|
20
|
+
"keywords": [
|
|
21
|
+
"mermaid-to-excalidraw",
|
|
22
|
+
"mermaid",
|
|
23
|
+
"excalidraw",
|
|
24
|
+
"diagram",
|
|
25
|
+
"cli",
|
|
26
|
+
"converter"
|
|
27
|
+
],
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+ssh://git@github.com/ayurkin/mermaid-to-excalidraw-cli.git"
|
|
31
|
+
},
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/ayurkin/mermaid-to-excalidraw-cli/issues"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://github.com/ayurkin/mermaid-to-excalidraw-cli",
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@excalidraw/excalidraw": "0.17.1-7381-cdf6d3e",
|
|
44
|
+
"@excalidraw/mermaid-to-excalidraw": "1.1.4",
|
|
45
|
+
"playwright": "^1.50.0",
|
|
46
|
+
"react": "^18.2.0",
|
|
47
|
+
"react-dom": "^18.2.0"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
export const removeMarkdown = (markdown, options = { listUnicodeChar: "" }) => {
|
|
2
|
+
options = options || {};
|
|
3
|
+
options.listUnicodeChar = Object.prototype.hasOwnProperty.call(
|
|
4
|
+
options,
|
|
5
|
+
"listUnicodeChar"
|
|
6
|
+
)
|
|
7
|
+
? options.listUnicodeChar
|
|
8
|
+
: false;
|
|
9
|
+
options.stripListLeaders = Object.prototype.hasOwnProperty.call(
|
|
10
|
+
options,
|
|
11
|
+
"stripListLeaders"
|
|
12
|
+
)
|
|
13
|
+
? options.stripListLeaders
|
|
14
|
+
: true;
|
|
15
|
+
options.gfm = Object.prototype.hasOwnProperty.call(options, "gfm")
|
|
16
|
+
? options.gfm
|
|
17
|
+
: true;
|
|
18
|
+
options.useImgAltText = Object.prototype.hasOwnProperty.call(
|
|
19
|
+
options,
|
|
20
|
+
"useImgAltText"
|
|
21
|
+
)
|
|
22
|
+
? options.useImgAltText
|
|
23
|
+
: true;
|
|
24
|
+
options.preserveLinks = Object.prototype.hasOwnProperty.call(
|
|
25
|
+
options,
|
|
26
|
+
"preserveLinks"
|
|
27
|
+
)
|
|
28
|
+
? options.preserveLinks
|
|
29
|
+
: false;
|
|
30
|
+
|
|
31
|
+
let output = markdown || "";
|
|
32
|
+
|
|
33
|
+
output = output.replace(/^(-\s*?|\*\s*?|_\s*?){3,}\s*$/gm, "");
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
if (options.stripListLeaders) {
|
|
37
|
+
if (options.listUnicodeChar) {
|
|
38
|
+
output = output.replace(
|
|
39
|
+
/^([\s\t]*)([\*\-\+]|\d+\.)\s+/gm,
|
|
40
|
+
`${options.listUnicodeChar} $1`
|
|
41
|
+
);
|
|
42
|
+
} else {
|
|
43
|
+
output = output.replace(/^([\s\t]*)([\*\-\+]|\d+\.)\s+/gm, "$1");
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (options.gfm) {
|
|
47
|
+
output = output
|
|
48
|
+
.replace(/\n={2,}/g, "\n")
|
|
49
|
+
.replace(/~{3}.*\n/g, "")
|
|
50
|
+
.replace(/~~/g, "")
|
|
51
|
+
.replace(/`{3}.*\n/g, "");
|
|
52
|
+
}
|
|
53
|
+
if (options.preserveLinks) {
|
|
54
|
+
output = output.replace(/\[(.*?)\][\[\(](.*?)[\]\)]/g, "$1 ($2)");
|
|
55
|
+
}
|
|
56
|
+
output = output
|
|
57
|
+
.replace(/<[^>]*>/g, "")
|
|
58
|
+
.replace(/^[=\-]{2,}\s*$/g, "")
|
|
59
|
+
.replace(/\[\^.+?\](\: .*?$)?/g, "")
|
|
60
|
+
.replace(/\s{0,2}\[.*?\]: .*?$/g, "")
|
|
61
|
+
.replace(/\!\[(.*?)\][\[\(].*?[\]\)]/g, options.useImgAltText ? "$1" : "")
|
|
62
|
+
.replace(/\[(.*?)\][\[\(].*?[\]\)]/g, "$1")
|
|
63
|
+
.replace(/^\s{0,3}>\s?/g, "")
|
|
64
|
+
.replace(/(^|\n)\s{0,3}>\s?/g, "\n\n")
|
|
65
|
+
.replace(/^\s{1,2}\[(.*?)\]: (\S+)( \".*?\")?\s*$/g, "")
|
|
66
|
+
.replace(
|
|
67
|
+
/^(\n)?\s{0,}#{1,6}\s+| {0,}(\n)?\s{0,}#{0,} {0,}(\n)?\s{0,}$/gm,
|
|
68
|
+
"$1$2$3"
|
|
69
|
+
)
|
|
70
|
+
.replace(/([\*_]{1,3})(\S.*?\S{0,1})\1/g, "$2")
|
|
71
|
+
.replace(/([\*_]{1,3})(\S.*?\S{0,1})\1/g, "$2")
|
|
72
|
+
.replace(/(`{3,})(.*?)\1/gm, "$2")
|
|
73
|
+
.replace(/`(.+?)`/g, "$1")
|
|
74
|
+
.replace(/\n{2,}/g, "\n\n");
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.error(error);
|
|
77
|
+
return markdown;
|
|
78
|
+
}
|
|
79
|
+
return output;
|
|
80
|
+
};
|