@mermaid-js/mermaid-cli 11.14.0 → 11.16.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/README.md +17 -11
- package/dist/index.html +0 -2
- package/dist-types/src/index.d.ts +19 -5
- package/dist-types/src/index.d.ts.map +1 -1
- package/dist-types/src/puppeteerIntercept.d.ts +17 -14
- package/dist-types/src/puppeteerIntercept.d.ts.map +1 -1
- package/dist-types/src/version.d.ts +1 -1
- package/dist-types/src/version.d.ts.map +1 -1
- package/package.json +22 -18
- package/src/cli.js +5 -3
- package/src/index.js +626 -303
- package/src/puppeteerIntercept.js +101 -54
- package/src/version.js +1 -1
- package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/assets/fa-brands-400-BdzBFuGj.woff2 +0 -0
- package/dist/assets/fa-regular-400-DQuI-phE.woff2 +0 -0
- package/dist/assets/fa-solid-900-BLm1ImsD.woff2 +0 -0
- package/dist/assets/index-DxLypzbC.js +0 -10
- package/src/index.js.bak +0 -540
package/src/index.js
CHANGED
|
@@ -1,23 +1,73 @@
|
|
|
1
|
-
import { Command, Option, InvalidArgumentError } from
|
|
2
|
-
import chalk from
|
|
3
|
-
import fs from
|
|
4
|
-
import { resolve } from
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import
|
|
1
|
+
import { Command, Option, InvalidArgumentError } from "commander";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import { resolve } from "import-meta-resolve";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import pLimit from "p-limit";
|
|
8
|
+
import puppeteer from "puppeteer";
|
|
9
|
+
import url from "url";
|
|
10
|
+
import { promisify } from "node:util";
|
|
11
|
+
import { version } from "./version.js";
|
|
12
|
+
import { Interceptor } from "./puppeteerIntercept.js";
|
|
10
13
|
|
|
11
14
|
// __dirname is not available in ESM modules by default
|
|
12
|
-
const __dirname = url.fileURLToPath(new url.URL(
|
|
15
|
+
const __dirname = url.fileURLToPath(new url.URL(".", import.meta.url));
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* CSS paths to embed in the page.
|
|
19
|
+
*/
|
|
20
|
+
const cssImports = /** @type {const} */ ({
|
|
21
|
+
"@fortawesome/fontawesome-free/css/brands.css": { level: 2 },
|
|
22
|
+
"@fortawesome/fontawesome-free/css/regular.css": { level: 2 },
|
|
23
|
+
"@fortawesome/fontawesome-free/css/solid.css": { level: 2 },
|
|
24
|
+
"@fortawesome/fontawesome-free/css/fontawesome.css": { level: 2 },
|
|
25
|
+
"katex/dist/katex.css": { level: 2 },
|
|
26
|
+
});
|
|
13
27
|
|
|
14
28
|
/**
|
|
15
29
|
* ESM bundles. Our interceptor doesn't support loading ESM modules that load
|
|
16
30
|
* other modules using relative paths, so these have to no `dependencies`.
|
|
17
31
|
*/
|
|
18
|
-
const mermaidESMPath = path.resolve(
|
|
19
|
-
|
|
20
|
-
|
|
32
|
+
const mermaidESMPath = path.resolve(
|
|
33
|
+
path.dirname(url.fileURLToPath(resolve("mermaid", import.meta.url))),
|
|
34
|
+
"mermaid.esm.mjs",
|
|
35
|
+
);
|
|
36
|
+
const elkESMPath = path.resolve(
|
|
37
|
+
path.dirname(
|
|
38
|
+
url.fileURLToPath(resolve("@mermaid-js/layout-elk", import.meta.url)),
|
|
39
|
+
),
|
|
40
|
+
"mermaid-layout-elk.esm.mjs",
|
|
41
|
+
);
|
|
42
|
+
const zenumlESMPath = path.resolve(
|
|
43
|
+
path.dirname(
|
|
44
|
+
url.fileURLToPath(resolve("@mermaid-js/mermaid-zenuml", import.meta.url)),
|
|
45
|
+
),
|
|
46
|
+
"mermaid-zenuml.esm.mjs",
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
/** @type {string | undefined} Path to `@mermaid-js/layout-tidy-tree`, if it is installed */
|
|
50
|
+
let tidyTreeESMPath;
|
|
51
|
+
try {
|
|
52
|
+
tidyTreeESMPath = path.resolve(
|
|
53
|
+
path.dirname(
|
|
54
|
+
url.fileURLToPath(
|
|
55
|
+
resolve("@mermaid-js/layout-tidy-tree", import.meta.url),
|
|
56
|
+
),
|
|
57
|
+
),
|
|
58
|
+
"mermaid-layout-tidy-tree.esm.mjs",
|
|
59
|
+
);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
if (
|
|
62
|
+
error instanceof Error &&
|
|
63
|
+
"code" in error &&
|
|
64
|
+
error.code === "ERR_MODULE_NOT_FOUND"
|
|
65
|
+
) {
|
|
66
|
+
// optional dependency, this is normal
|
|
67
|
+
} else {
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
21
71
|
|
|
22
72
|
/**
|
|
23
73
|
* Prints an error to stderr, then closes with exit code 1
|
|
@@ -25,19 +75,19 @@ const zenumlESMPath = path.resolve(path.dirname(url.fileURLToPath(resolve('@merm
|
|
|
25
75
|
* @param {string} message - The message to print to `stderr`.
|
|
26
76
|
* @returns {never} Quits Node.JS, so never returns.
|
|
27
77
|
*/
|
|
28
|
-
const error = message => {
|
|
29
|
-
console.error(chalk.red(`\n${message}\n`))
|
|
30
|
-
process.exit(1)
|
|
31
|
-
}
|
|
78
|
+
const error = (message) => {
|
|
79
|
+
console.error(chalk.red(`\n${message}\n`));
|
|
80
|
+
process.exit(1);
|
|
81
|
+
};
|
|
32
82
|
|
|
33
83
|
/**
|
|
34
84
|
* Prints a warning to stderr.
|
|
35
85
|
*
|
|
36
86
|
* @param {string} message - The message to print to `stderr`.
|
|
37
87
|
*/
|
|
38
|
-
const warn = message => {
|
|
39
|
-
console.warn(chalk.yellow(`\n${message}\n`))
|
|
40
|
-
}
|
|
88
|
+
const warn = (message) => {
|
|
89
|
+
console.warn(chalk.yellow(`\n${message}\n`));
|
|
90
|
+
};
|
|
41
91
|
|
|
42
92
|
/**
|
|
43
93
|
* Checks if the given file exists.
|
|
@@ -46,11 +96,11 @@ const warn = message => {
|
|
|
46
96
|
* @returns {never | void} If the file doesn't exist, closes Node.JS with
|
|
47
97
|
* exit code 1.
|
|
48
98
|
*/
|
|
49
|
-
const checkConfigFile = file => {
|
|
99
|
+
const checkConfigFile = (file) => {
|
|
50
100
|
if (!fs.existsSync(file)) {
|
|
51
|
-
error(`Configuration file "${file}" doesn't exist`)
|
|
101
|
+
error(`Configuration file "${file}" doesn't exist`);
|
|
52
102
|
}
|
|
53
|
-
}
|
|
103
|
+
};
|
|
54
104
|
|
|
55
105
|
/**
|
|
56
106
|
* Gets the data in the given file.
|
|
@@ -59,31 +109,31 @@ const checkConfigFile = file => {
|
|
|
59
109
|
* If `undefined`, reads from `stdin` instead.
|
|
60
110
|
* @returns {Promise<string>} The contents of `inputFile` parsed as `utf8`.
|
|
61
111
|
*/
|
|
62
|
-
async function getInputData
|
|
112
|
+
async function getInputData(inputFile) {
|
|
63
113
|
// if an input file has been specified using '-i', it takes precedence over
|
|
64
114
|
// piping from stdin
|
|
65
|
-
if (typeof inputFile !==
|
|
66
|
-
return await fs.promises.readFile(inputFile,
|
|
115
|
+
if (typeof inputFile !== "undefined") {
|
|
116
|
+
return await fs.promises.readFile(inputFile, "utf-8");
|
|
67
117
|
}
|
|
68
118
|
|
|
69
119
|
return await new Promise((resolve, reject) => {
|
|
70
|
-
let data =
|
|
71
|
-
process.stdin.on(
|
|
72
|
-
const chunk = process.stdin.read()
|
|
120
|
+
let data = "";
|
|
121
|
+
process.stdin.on("readable", function () {
|
|
122
|
+
const chunk = process.stdin.read();
|
|
73
123
|
|
|
74
124
|
if (chunk !== null) {
|
|
75
|
-
data += chunk
|
|
125
|
+
data += chunk;
|
|
76
126
|
}
|
|
77
|
-
})
|
|
127
|
+
});
|
|
78
128
|
|
|
79
|
-
process.stdin.on(
|
|
80
|
-
reject(err)
|
|
81
|
-
})
|
|
129
|
+
process.stdin.on("error", function (err) {
|
|
130
|
+
reject(err);
|
|
131
|
+
});
|
|
82
132
|
|
|
83
|
-
process.stdin.on(
|
|
84
|
-
resolve(data)
|
|
85
|
-
})
|
|
86
|
-
})
|
|
133
|
+
process.stdin.on("end", function () {
|
|
134
|
+
resolve(data);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
87
137
|
}
|
|
88
138
|
|
|
89
139
|
/**
|
|
@@ -95,99 +145,205 @@ async function getInputData (inputFile) {
|
|
|
95
145
|
* @throws {InvalidArgumentError} If the arg is not valid.
|
|
96
146
|
* @see https://github.com/tj/commander.js/wiki/Class:-Option#argparserfn
|
|
97
147
|
*/
|
|
98
|
-
function parseCommanderInt
|
|
99
|
-
const parsedValue = parseInt(value, 10)
|
|
148
|
+
function parseCommanderInt(value, _unused) {
|
|
149
|
+
const parsedValue = parseInt(value, 10);
|
|
100
150
|
if (isNaN(parsedValue) || parsedValue < 1) {
|
|
101
|
-
throw new InvalidArgumentError(
|
|
151
|
+
throw new InvalidArgumentError("Not a positive integer.");
|
|
102
152
|
}
|
|
103
|
-
return parsedValue
|
|
153
|
+
return parsedValue;
|
|
104
154
|
}
|
|
105
155
|
|
|
106
|
-
|
|
107
|
-
|
|
156
|
+
/**
|
|
157
|
+
* Commander parser that converts a string to a float.
|
|
158
|
+
*
|
|
159
|
+
* @param {string} value - The value from commander.
|
|
160
|
+
* @param {*} _unused - Unused.
|
|
161
|
+
* @returns {number} The value parsed as a number.
|
|
162
|
+
* @see https://github.com/tj/commander.js/wiki/Class:-Option#argparserfn
|
|
163
|
+
*/
|
|
164
|
+
function parseCommanderFloat(value, _unused) {
|
|
165
|
+
const parsedValue = parseFloat(value);
|
|
166
|
+
if (isNaN(parsedValue) || parsedValue <= 0) {
|
|
167
|
+
throw new InvalidArgumentError("Not a positive number.");
|
|
168
|
+
}
|
|
169
|
+
return parsedValue;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function cli() {
|
|
173
|
+
const commander = new Command();
|
|
108
174
|
commander
|
|
109
175
|
.version(version)
|
|
110
|
-
.addOption(
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
.
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
.
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
.option(
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
176
|
+
.addOption(
|
|
177
|
+
new Option("-t, --theme [theme]", "Theme of the chart")
|
|
178
|
+
.choices(["default", "forest", "dark", "neutral"])
|
|
179
|
+
.default("default"),
|
|
180
|
+
)
|
|
181
|
+
.addOption(
|
|
182
|
+
new Option("-w, --width [width]", "Width of the page")
|
|
183
|
+
.argParser(parseCommanderInt)
|
|
184
|
+
.default(800),
|
|
185
|
+
)
|
|
186
|
+
.addOption(
|
|
187
|
+
new Option("-H, --height [height]", "Height of the page")
|
|
188
|
+
.argParser(parseCommanderInt)
|
|
189
|
+
.default(600),
|
|
190
|
+
)
|
|
191
|
+
.option(
|
|
192
|
+
"-i, --input <input>",
|
|
193
|
+
"Input mermaid file. Files ending in .md will be treated as Markdown and all charts (e.g. ```mermaid (...)``` or :::mermaid (...):::) will be extracted and generated. Use `-` to read from stdin.",
|
|
194
|
+
)
|
|
195
|
+
.option(
|
|
196
|
+
"-o, --output [output]",
|
|
197
|
+
'Output file. It should be either md, svg, png, pdf or use `-` to output to stdout. Optional. Default: input + ".svg"',
|
|
198
|
+
)
|
|
199
|
+
.option(
|
|
200
|
+
"-a, --artefacts [artefacts]",
|
|
201
|
+
"Output artefacts path. Only used with Markdown input file. Optional. Default: output directory",
|
|
202
|
+
)
|
|
203
|
+
.addOption(
|
|
204
|
+
new Option(
|
|
205
|
+
"-j, --jobs <jobs>",
|
|
206
|
+
"Number of parallel jobs to run when rendering multiple diagrams. Defaults to half the available CPUs.",
|
|
207
|
+
)
|
|
208
|
+
.argParser(parseCommanderInt)
|
|
209
|
+
.default(Math.floor(os.availableParallelism() / 2) || 1),
|
|
210
|
+
)
|
|
211
|
+
.addOption(
|
|
212
|
+
new Option(
|
|
213
|
+
"-e, --outputFormat [format]",
|
|
214
|
+
"Output format for the generated image.",
|
|
215
|
+
)
|
|
216
|
+
.choices(["svg", "png", "pdf"])
|
|
217
|
+
.default(null, "Loaded from the output file extension"),
|
|
218
|
+
)
|
|
219
|
+
.addOption(
|
|
220
|
+
new Option(
|
|
221
|
+
"-b, --backgroundColor [backgroundColor]",
|
|
222
|
+
"Background color for pngs/svgs (not pdfs). Example: transparent, red, '#F0F0F0'.",
|
|
223
|
+
).default("white"),
|
|
224
|
+
)
|
|
225
|
+
.option(
|
|
226
|
+
"-c, --configFile [configFile]",
|
|
227
|
+
"JSON configuration file for mermaid.",
|
|
228
|
+
)
|
|
229
|
+
.option("-C, --cssFile [cssFile]", "CSS file for the page.")
|
|
230
|
+
.option(
|
|
231
|
+
"-I, --svgId [svgId]",
|
|
232
|
+
"The id attribute for the SVG element to be rendered.",
|
|
233
|
+
)
|
|
234
|
+
.addOption(
|
|
235
|
+
new Option("-s, --scale [scale]", "Puppeteer scale factor")
|
|
236
|
+
.argParser(parseCommanderFloat)
|
|
237
|
+
.default(1),
|
|
238
|
+
)
|
|
239
|
+
.option("-f, --pdfFit", "Scale PDF to fit chart")
|
|
240
|
+
.option("-q, --quiet", "Suppress log output")
|
|
241
|
+
.option(
|
|
242
|
+
"-p --puppeteerConfigFile [puppeteerConfigFile]",
|
|
243
|
+
"JSON configuration file for puppeteer.",
|
|
244
|
+
)
|
|
245
|
+
.option(
|
|
246
|
+
"--iconPacks <icons...>",
|
|
247
|
+
"Icon packs to use, e.g. @iconify-json/logos. These should be Iconify NPM packages that expose a icons.json file, see https://iconify.design/docs/icons/json.html. These will be downloaded from https://unkpg.com when needed.",
|
|
248
|
+
[],
|
|
249
|
+
)
|
|
250
|
+
.option(
|
|
251
|
+
"--iconPacksNamesAndUrls <prefix#iconsurl...>",
|
|
252
|
+
'Icon packs to use, e.g. azure#https://raw.githubusercontent.com/NakayamaKento/AzureIcons/refs/heads/main/icons.json where the name (prefix) of the icon pack is defined before the "#" and the url of the json definition after the "#". These should be Iconify json file formatted as IconifyJson, see https://iconify.design/docs/icons/json.html. These will be downloaded when needed.',
|
|
253
|
+
[],
|
|
254
|
+
)
|
|
255
|
+
.parse(process.argv);
|
|
256
|
+
|
|
257
|
+
const options = commander.opts();
|
|
258
|
+
|
|
259
|
+
let {
|
|
260
|
+
theme,
|
|
261
|
+
width,
|
|
262
|
+
height,
|
|
263
|
+
input,
|
|
264
|
+
output,
|
|
265
|
+
outputFormat,
|
|
266
|
+
backgroundColor,
|
|
267
|
+
configFile,
|
|
268
|
+
cssFile,
|
|
269
|
+
svgId,
|
|
270
|
+
puppeteerConfigFile,
|
|
271
|
+
scale,
|
|
272
|
+
pdfFit,
|
|
273
|
+
quiet,
|
|
274
|
+
iconPacks,
|
|
275
|
+
iconPacksNamesAndUrls,
|
|
276
|
+
artefacts,
|
|
277
|
+
jobs,
|
|
278
|
+
} = options;
|
|
132
279
|
|
|
133
280
|
// check input file
|
|
134
281
|
if (!input) {
|
|
135
|
-
warn(
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
282
|
+
warn(
|
|
283
|
+
"No input file specified, reading from stdin. " +
|
|
284
|
+
"If you want to specify an input file, please use `-i <input>.` " +
|
|
285
|
+
"You can use `-i -` to read from stdin and to suppress this warning.",
|
|
286
|
+
);
|
|
287
|
+
} else if (input === "-") {
|
|
140
288
|
// `--input -` means read from stdin, but suppress the above warning
|
|
141
|
-
input = undefined
|
|
289
|
+
input = undefined;
|
|
142
290
|
} else if (!fs.existsSync(input)) {
|
|
143
|
-
error(`Input file "${input}" doesn't exist`)
|
|
291
|
+
error(`Input file "${input}" doesn't exist`);
|
|
144
292
|
}
|
|
145
293
|
|
|
146
294
|
// check output file
|
|
147
295
|
if (!output) {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
296
|
+
// if an input file is defined, it should take precedence, otherwise, input is
|
|
297
|
+
// coming from stdin and just name the file out.svg, if it hasn't been
|
|
298
|
+
// specified with the '-o' option
|
|
151
299
|
if (outputFormat) {
|
|
152
|
-
output = input ?
|
|
300
|
+
output = input ? `${input}.${outputFormat}` : `out.${outputFormat}`;
|
|
153
301
|
} else {
|
|
154
|
-
output = input ?
|
|
302
|
+
output = input ? `${input}.svg` : "out.svg";
|
|
155
303
|
}
|
|
156
|
-
} else if (output ===
|
|
304
|
+
} else if (output === "-") {
|
|
157
305
|
// `--output -` means write to stdout.
|
|
158
|
-
output =
|
|
159
|
-
quiet = true
|
|
306
|
+
output = "/dev/stdout";
|
|
307
|
+
quiet = true;
|
|
160
308
|
|
|
161
309
|
if (!outputFormat) {
|
|
162
|
-
outputFormat =
|
|
163
|
-
warn(
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
310
|
+
outputFormat = "svg";
|
|
311
|
+
warn(
|
|
312
|
+
"No output format specified, using svg. " +
|
|
313
|
+
"If you want to specify an output format and suppress this warning, " +
|
|
314
|
+
"please use `-e <format>.` ",
|
|
315
|
+
);
|
|
167
316
|
}
|
|
168
317
|
} else if (!/\.(?:svg|png|pdf|md|markdown)$/.test(output)) {
|
|
169
|
-
error(
|
|
318
|
+
error(
|
|
319
|
+
'Output file must end with ".md"/".markdown", ".svg", ".png" or ".pdf"',
|
|
320
|
+
);
|
|
170
321
|
}
|
|
171
322
|
|
|
172
323
|
if (artefacts) {
|
|
173
324
|
if (!input || !/\.(?:md|markdown)$/.test(input)) {
|
|
174
|
-
error(
|
|
325
|
+
error(
|
|
326
|
+
"Artefacts [-a|--artefacts] path can only be used with Markdown input file",
|
|
327
|
+
);
|
|
175
328
|
}
|
|
176
329
|
if (!fs.existsSync(artefacts)) {
|
|
177
|
-
fs.mkdirSync(artefacts, { recursive: true })
|
|
330
|
+
fs.mkdirSync(artefacts, { recursive: true });
|
|
178
331
|
}
|
|
179
332
|
}
|
|
180
333
|
|
|
181
|
-
const outputDir = path.dirname(output)
|
|
182
|
-
if (output !==
|
|
183
|
-
error(`Output directory "${outputDir}/" doesn't exist`)
|
|
334
|
+
const outputDir = path.dirname(output);
|
|
335
|
+
if (output !== "/dev/stdout" && !fs.existsSync(outputDir)) {
|
|
336
|
+
error(`Output directory "${outputDir}/" doesn't exist`);
|
|
184
337
|
}
|
|
185
338
|
|
|
186
339
|
// check config files
|
|
187
|
-
let mermaidConfig = { theme }
|
|
340
|
+
let mermaidConfig = { theme };
|
|
188
341
|
if (configFile) {
|
|
189
|
-
checkConfigFile(configFile)
|
|
190
|
-
mermaidConfig = Object.assign(
|
|
342
|
+
checkConfigFile(configFile);
|
|
343
|
+
mermaidConfig = Object.assign(
|
|
344
|
+
mermaidConfig,
|
|
345
|
+
JSON.parse(fs.readFileSync(configFile, "utf-8")),
|
|
346
|
+
);
|
|
191
347
|
}
|
|
192
348
|
|
|
193
349
|
let puppeteerConfig = /** @type {import('puppeteer').LaunchOptions} */ ({
|
|
@@ -196,33 +352,42 @@ async function cli () {
|
|
|
196
352
|
* but still works. In Puppeteer v22, it uses the `chrome-headless-shell` package,
|
|
197
353
|
* which is much faster than the regular headless mode.
|
|
198
354
|
*/
|
|
199
|
-
headless:
|
|
200
|
-
})
|
|
355
|
+
headless: "shell",
|
|
356
|
+
});
|
|
201
357
|
if (puppeteerConfigFile) {
|
|
202
|
-
checkConfigFile(puppeteerConfigFile)
|
|
203
|
-
puppeteerConfig = Object.assign(
|
|
358
|
+
checkConfigFile(puppeteerConfigFile);
|
|
359
|
+
puppeteerConfig = Object.assign(
|
|
360
|
+
puppeteerConfig,
|
|
361
|
+
JSON.parse(fs.readFileSync(puppeteerConfigFile, "utf-8")),
|
|
362
|
+
);
|
|
204
363
|
}
|
|
205
364
|
|
|
206
365
|
// check cssFile
|
|
207
|
-
let myCSS
|
|
366
|
+
let myCSS;
|
|
208
367
|
if (cssFile) {
|
|
209
368
|
if (!fs.existsSync(cssFile)) {
|
|
210
|
-
error(`CSS file "${cssFile}" doesn't exist`)
|
|
369
|
+
error(`CSS file "${cssFile}" doesn't exist`);
|
|
211
370
|
}
|
|
212
|
-
myCSS = fs.readFileSync(cssFile,
|
|
371
|
+
myCSS = fs.readFileSync(cssFile, "utf-8");
|
|
213
372
|
}
|
|
214
373
|
|
|
215
|
-
await run(
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
374
|
+
await run(input, output, {
|
|
375
|
+
puppeteerConfig,
|
|
376
|
+
quiet,
|
|
377
|
+
outputFormat,
|
|
378
|
+
limiter: pLimit(jobs),
|
|
379
|
+
parseMMDOptions: {
|
|
380
|
+
mermaidConfig,
|
|
381
|
+
backgroundColor,
|
|
382
|
+
myCSS,
|
|
383
|
+
pdfFit,
|
|
384
|
+
viewport: { width, height, deviceScaleFactor: scale },
|
|
385
|
+
svgId,
|
|
386
|
+
iconPacks,
|
|
387
|
+
iconPacksNamesAndUrls,
|
|
388
|
+
},
|
|
389
|
+
artefacts,
|
|
390
|
+
});
|
|
226
391
|
}
|
|
227
392
|
|
|
228
393
|
/**
|
|
@@ -246,156 +411,259 @@ async function cli () {
|
|
|
246
411
|
* @returns {Promise<{title: string | null, desc: string | null, data: Uint8Array}>} The output file in bytes,
|
|
247
412
|
* with optional metadata.
|
|
248
413
|
*/
|
|
249
|
-
async function renderMermaid
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
414
|
+
async function renderMermaid(
|
|
415
|
+
browser,
|
|
416
|
+
definition,
|
|
417
|
+
outputFormat,
|
|
418
|
+
{
|
|
419
|
+
viewport,
|
|
420
|
+
backgroundColor = "white",
|
|
421
|
+
mermaidConfig = {},
|
|
422
|
+
myCSS,
|
|
423
|
+
pdfFit,
|
|
424
|
+
svgId,
|
|
425
|
+
iconPacks = [],
|
|
426
|
+
iconPacksNamesAndUrls = [],
|
|
427
|
+
} = {},
|
|
428
|
+
) {
|
|
429
|
+
const page = await browser.newPage();
|
|
430
|
+
page.on("console", (msg) => {
|
|
431
|
+
console.warn(msg.text());
|
|
432
|
+
});
|
|
254
433
|
try {
|
|
255
434
|
if (viewport) {
|
|
256
|
-
await page.setViewport(viewport)
|
|
435
|
+
await page.setViewport(viewport);
|
|
257
436
|
}
|
|
258
|
-
const mermaidHTMLPath = path.join(__dirname,
|
|
259
|
-
await page.goto(url.pathToFileURL(mermaidHTMLPath).href)
|
|
260
|
-
await page.$eval(
|
|
261
|
-
body
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
437
|
+
const mermaidHTMLPath = path.join(__dirname, "..", "dist", "index.html");
|
|
438
|
+
await page.goto(url.pathToFileURL(mermaidHTMLPath).href);
|
|
439
|
+
await page.$eval(
|
|
440
|
+
"body",
|
|
441
|
+
(body, backgroundColor) => {
|
|
442
|
+
body.style.background = backgroundColor;
|
|
443
|
+
},
|
|
444
|
+
backgroundColor,
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
const interceptor = new Interceptor();
|
|
448
|
+
const mermaidUrl = await interceptor.fileUrlToInterceptUrl(
|
|
449
|
+
url.pathToFileURL(mermaidESMPath),
|
|
450
|
+
);
|
|
451
|
+
const elkUrl = await interceptor.fileUrlToInterceptUrl(
|
|
452
|
+
url.pathToFileURL(elkESMPath),
|
|
453
|
+
);
|
|
454
|
+
const zenumlUrl = await interceptor.fileUrlToInterceptUrl(
|
|
455
|
+
url.pathToFileURL(zenumlESMPath),
|
|
456
|
+
);
|
|
457
|
+
const tidyTreeESMUrl = tidyTreeESMPath
|
|
458
|
+
? await interceptor.fileUrlToInterceptUrl(
|
|
459
|
+
url.pathToFileURL(tidyTreeESMPath),
|
|
460
|
+
)
|
|
461
|
+
: undefined;
|
|
462
|
+
|
|
463
|
+
page.on("request", interceptor.interceptRequestHandler);
|
|
464
|
+
await page.setRequestInterception(true);
|
|
465
|
+
|
|
466
|
+
await Promise.all(
|
|
467
|
+
Object.entries(cssImports).map(async ([cssImport, { level }]) => {
|
|
468
|
+
const interceptUrl = await interceptor.fileUrlToInterceptUrl(
|
|
469
|
+
new URL(resolve(cssImport, import.meta.url)),
|
|
470
|
+
{
|
|
471
|
+
allowParentDirectoryLevel: level,
|
|
472
|
+
},
|
|
473
|
+
);
|
|
474
|
+
await page.addStyleTag({
|
|
475
|
+
url: interceptUrl,
|
|
476
|
+
});
|
|
477
|
+
}),
|
|
478
|
+
);
|
|
479
|
+
|
|
480
|
+
const metadata = await page.$eval(
|
|
481
|
+
"#container",
|
|
482
|
+
async (
|
|
483
|
+
container,
|
|
484
|
+
{
|
|
485
|
+
definition,
|
|
486
|
+
mermaidConfig,
|
|
487
|
+
myCSS,
|
|
488
|
+
backgroundColor,
|
|
489
|
+
svgId,
|
|
490
|
+
iconPacks,
|
|
491
|
+
iconPacksNamesAndUrls,
|
|
492
|
+
elkUrl,
|
|
493
|
+
mermaidUrl,
|
|
494
|
+
zenumlUrl,
|
|
495
|
+
tidyTreeESMUrl,
|
|
496
|
+
},
|
|
497
|
+
) => {
|
|
498
|
+
const { default: mermaid } = await import(mermaidUrl);
|
|
499
|
+
/** @type {typeof import('@mermaid-js/layout-elk')} */
|
|
500
|
+
const { default: elkLayouts } = await import(elkUrl);
|
|
501
|
+
/** @type {typeof import('@mermaid-js/mermaid-zenuml')} */
|
|
502
|
+
const { default: zenuml } = await import(zenumlUrl);
|
|
503
|
+
// @ts-ignore -- @mermaid-js/layout-tidy-tree is an optionalDependency and might not be installed
|
|
504
|
+
/** @type {typeof import('@mermaid-js/layout-tidy-tree') | {default: undefined}} */
|
|
505
|
+
const { default: tidyTree } = tidyTreeESMUrl
|
|
506
|
+
? await import(tidyTreeESMUrl)
|
|
507
|
+
: { default: undefined };
|
|
508
|
+
await Promise.all(Array.from(document.fonts, (font) => font.load()));
|
|
509
|
+
|
|
510
|
+
await mermaid.registerExternalDiagrams([zenuml]);
|
|
511
|
+
mermaid.registerLayoutLoaders([...elkLayouts, ...(tidyTree ?? [])]);
|
|
512
|
+
// lazy load icon packs
|
|
513
|
+
|
|
514
|
+
mermaid.registerIconPacks(
|
|
515
|
+
iconPacks.map((icon) => ({
|
|
516
|
+
name: icon.split("/")[1],
|
|
299
517
|
loader: () =>
|
|
300
|
-
fetch(
|
|
518
|
+
fetch(`https://unpkg.com/${icon}/icons.json`)
|
|
301
519
|
.then((res) => res.json())
|
|
302
|
-
.catch(() => {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
520
|
+
.catch(() => error(`Failed to fetch icon: ${icon}`)),
|
|
521
|
+
})),
|
|
522
|
+
);
|
|
523
|
+
|
|
524
|
+
mermaid.registerIconPacks(
|
|
525
|
+
iconPacksNamesAndUrls.map((iconPackInfo) => {
|
|
526
|
+
const packName = iconPackInfo.split("#")[0];
|
|
527
|
+
const packUrl = iconPackInfo.split("#")[1];
|
|
528
|
+
|
|
529
|
+
return {
|
|
530
|
+
name: packName,
|
|
531
|
+
loader: () =>
|
|
532
|
+
fetch(packUrl)
|
|
533
|
+
.then((res) => res.json())
|
|
534
|
+
.catch(() => {
|
|
535
|
+
error(`Failed to fetch icon: ${iconPackInfo}`);
|
|
536
|
+
}),
|
|
537
|
+
};
|
|
538
|
+
}),
|
|
539
|
+
);
|
|
540
|
+
mermaid.initialize({ startOnLoad: false, ...mermaidConfig });
|
|
541
|
+
// should throw an error if mmd diagram is invalid
|
|
542
|
+
const { svg: svgText } = await mermaid.render(
|
|
543
|
+
svgId || "my-svg",
|
|
544
|
+
definition,
|
|
545
|
+
container,
|
|
546
|
+
);
|
|
547
|
+
container.innerHTML = svgText;
|
|
548
|
+
|
|
549
|
+
const svg = container.getElementsByTagName?.("svg")?.[0];
|
|
550
|
+
if (svg?.style) {
|
|
551
|
+
svg.style.backgroundColor = backgroundColor;
|
|
552
|
+
} else {
|
|
553
|
+
warn("svg not found. Not applying background color.");
|
|
554
|
+
}
|
|
555
|
+
if (myCSS) {
|
|
556
|
+
// add CSS as a <svg>...<style>... element
|
|
557
|
+
// see https://developer.mozilla.org/en-US/docs/Web/API/SVGStyleElement
|
|
558
|
+
const style = document.createElementNS(
|
|
559
|
+
"http://www.w3.org/2000/svg",
|
|
560
|
+
"style",
|
|
561
|
+
);
|
|
562
|
+
style.appendChild(document.createTextNode(myCSS));
|
|
563
|
+
svg.appendChild(style);
|
|
307
564
|
}
|
|
308
|
-
)
|
|
309
|
-
)
|
|
310
|
-
mermaid.initialize({ startOnLoad: false, ...mermaidConfig })
|
|
311
|
-
// should throw an error if mmd diagram is invalid
|
|
312
|
-
const { svg: svgText } = await mermaid.render(svgId || 'my-svg', definition, container)
|
|
313
|
-
container.innerHTML = svgText
|
|
314
|
-
|
|
315
|
-
const svg = container.getElementsByTagName?.('svg')?.[0]
|
|
316
|
-
if (svg?.style) {
|
|
317
|
-
svg.style.backgroundColor = backgroundColor
|
|
318
|
-
} else {
|
|
319
|
-
warn('svg not found. Not applying background color.')
|
|
320
|
-
}
|
|
321
|
-
if (myCSS) {
|
|
322
|
-
// add CSS as a <svg>...<style>... element
|
|
323
|
-
// see https://developer.mozilla.org/en-US/docs/Web/API/SVGStyleElement
|
|
324
|
-
const style = document.createElementNS('http://www.w3.org/2000/svg', 'style')
|
|
325
|
-
style.appendChild(document.createTextNode(myCSS))
|
|
326
|
-
svg.appendChild(style)
|
|
327
|
-
}
|
|
328
565
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
title = svg.firstChild.textContent
|
|
337
|
-
}
|
|
338
|
-
/** SVG description. According to SVG spec, we should use the first one we find */
|
|
339
|
-
let desc = null
|
|
340
|
-
for (const svgNode of svg.children) {
|
|
341
|
-
if (svgNode instanceof SVGDescElement) {
|
|
342
|
-
desc = svgNode.textContent
|
|
566
|
+
// Finds SVG metadata for accessibility purposes
|
|
567
|
+
/** SVG title */
|
|
568
|
+
let title = null;
|
|
569
|
+
// If <title> exists, it must be the first child Node,
|
|
570
|
+
// see https://www.w3.org/TR/SVG11/struct.html#DescriptionAndTitleElements
|
|
571
|
+
if (svg.firstChild instanceof SVGTitleElement) {
|
|
572
|
+
title = svg.firstChild.textContent;
|
|
343
573
|
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
574
|
+
/** SVG description. According to SVG spec, we should use the first one we find */
|
|
575
|
+
let desc = null;
|
|
576
|
+
for (const svgNode of svg.children) {
|
|
577
|
+
if (svgNode instanceof SVGDescElement) {
|
|
578
|
+
desc = svgNode.textContent;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
return {
|
|
582
|
+
title,
|
|
583
|
+
desc,
|
|
584
|
+
};
|
|
585
|
+
},
|
|
586
|
+
{
|
|
587
|
+
definition,
|
|
588
|
+
mermaidConfig,
|
|
589
|
+
myCSS,
|
|
590
|
+
backgroundColor,
|
|
591
|
+
svgId,
|
|
592
|
+
iconPacks,
|
|
593
|
+
iconPacksNamesAndUrls,
|
|
594
|
+
elkUrl,
|
|
595
|
+
mermaidUrl,
|
|
596
|
+
zenumlUrl,
|
|
597
|
+
tidyTreeESMUrl,
|
|
598
|
+
},
|
|
599
|
+
);
|
|
349
600
|
|
|
350
|
-
if (outputFormat ===
|
|
351
|
-
const svgXML = await page.$eval(
|
|
601
|
+
if (outputFormat === "svg") {
|
|
602
|
+
const svgXML = await page.$eval("svg", (svg) => {
|
|
352
603
|
// SVG might have HTML <foreignObject> that are not valid XML
|
|
353
604
|
// E.g. <br> must be replaced with <br/>
|
|
354
605
|
// Luckily the DOM Web API has the XMLSerializer for this
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
})
|
|
606
|
+
const xmlSerializer = new XMLSerializer();
|
|
607
|
+
return xmlSerializer.serializeToString(svg);
|
|
608
|
+
});
|
|
359
609
|
return {
|
|
360
610
|
...metadata,
|
|
361
|
-
data: new TextEncoder().encode(svgXML)
|
|
362
|
-
}
|
|
363
|
-
} else if (outputFormat ===
|
|
364
|
-
const clip = await page.$eval(
|
|
365
|
-
const react = svg.getBoundingClientRect()
|
|
366
|
-
return {
|
|
367
|
-
|
|
368
|
-
|
|
611
|
+
data: new TextEncoder().encode(svgXML),
|
|
612
|
+
};
|
|
613
|
+
} else if (outputFormat === "png") {
|
|
614
|
+
const clip = await page.$eval("svg", (svg) => {
|
|
615
|
+
const react = svg.getBoundingClientRect();
|
|
616
|
+
return {
|
|
617
|
+
x: Math.floor(react.left),
|
|
618
|
+
y: Math.floor(react.top),
|
|
619
|
+
width: Math.ceil(react.width),
|
|
620
|
+
height: Math.ceil(react.height),
|
|
621
|
+
};
|
|
622
|
+
});
|
|
623
|
+
await page.setViewport({
|
|
624
|
+
...viewport,
|
|
625
|
+
width: clip.x + clip.width,
|
|
626
|
+
height: clip.y + clip.height,
|
|
627
|
+
});
|
|
369
628
|
return {
|
|
370
629
|
...metadata,
|
|
371
|
-
data: await page.screenshot({
|
|
372
|
-
|
|
373
|
-
|
|
630
|
+
data: await page.screenshot({
|
|
631
|
+
clip,
|
|
632
|
+
omitBackground: backgroundColor === "transparent",
|
|
633
|
+
}),
|
|
634
|
+
};
|
|
635
|
+
} else {
|
|
636
|
+
// pdf
|
|
374
637
|
if (pdfFit) {
|
|
375
|
-
const clip = await page.$eval(
|
|
376
|
-
const react = svg.getBoundingClientRect()
|
|
377
|
-
return {
|
|
378
|
-
|
|
638
|
+
const clip = await page.$eval("svg", (svg) => {
|
|
639
|
+
const react = svg.getBoundingClientRect();
|
|
640
|
+
return {
|
|
641
|
+
x: react.left,
|
|
642
|
+
y: react.top,
|
|
643
|
+
width: react.width,
|
|
644
|
+
height: react.height,
|
|
645
|
+
};
|
|
646
|
+
});
|
|
379
647
|
return {
|
|
380
648
|
...metadata,
|
|
381
649
|
data: await page.pdf({
|
|
382
|
-
omitBackground: backgroundColor ===
|
|
383
|
-
width:
|
|
384
|
-
height:
|
|
385
|
-
pageRanges:
|
|
386
|
-
})
|
|
387
|
-
}
|
|
650
|
+
omitBackground: backgroundColor === "transparent",
|
|
651
|
+
width: Math.ceil(clip.width) + clip.x * 2 + "px",
|
|
652
|
+
height: Math.ceil(clip.height) + clip.y * 2 + "px",
|
|
653
|
+
pageRanges: "1-1",
|
|
654
|
+
}),
|
|
655
|
+
};
|
|
388
656
|
} else {
|
|
389
657
|
return {
|
|
390
658
|
...metadata,
|
|
391
659
|
data: await page.pdf({
|
|
392
|
-
omitBackground: backgroundColor ===
|
|
393
|
-
})
|
|
394
|
-
}
|
|
660
|
+
omitBackground: backgroundColor === "transparent",
|
|
661
|
+
}),
|
|
662
|
+
};
|
|
395
663
|
}
|
|
396
664
|
}
|
|
397
665
|
} finally {
|
|
398
|
-
await page.close()
|
|
666
|
+
await page.close();
|
|
399
667
|
}
|
|
400
668
|
}
|
|
401
669
|
|
|
@@ -413,17 +681,24 @@ async function renderMermaid (browser, definition, outputFormat, { viewport, bac
|
|
|
413
681
|
* @param {MarkdownImageProps} params - Parameters.
|
|
414
682
|
* @returns {``} The markdown image text.
|
|
415
683
|
*/
|
|
416
|
-
function markdownImage
|
|
684
|
+
function markdownImage({ url, title, alt }) {
|
|
417
685
|
// we can't use String.prototype.replaceAll since it's not supported in Node v14
|
|
418
|
-
const altEscaped = alt.replace(/[[\]\\]/g,
|
|
686
|
+
const altEscaped = alt.replace(/[[\]\\]/g, "\\$&");
|
|
419
687
|
if (title) {
|
|
420
|
-
const titleEscaped = title.replace(/["\\]/g,
|
|
421
|
-
return `
|
|
688
|
+
const titleEscaped = title.replace(/["\\]/g, "\\$&");
|
|
689
|
+
return ``;
|
|
422
690
|
} else {
|
|
423
|
-
return `
|
|
691
|
+
return ``;
|
|
424
692
|
}
|
|
425
693
|
}
|
|
426
694
|
|
|
695
|
+
/**
|
|
696
|
+
* @typedef {<Arguments extends unknown[], ReturnType>(
|
|
697
|
+
* function_: (...arguments_: Arguments) => Promise<ReturnType>,
|
|
698
|
+
* ...arguments_: Arguments
|
|
699
|
+
* ) => Promise<ReturnType>} Limiter - Adapted from `p-limit` package.
|
|
700
|
+
*/
|
|
701
|
+
|
|
427
702
|
/**
|
|
428
703
|
* Renders a mermaid diagram or mermaid markdown file.
|
|
429
704
|
*
|
|
@@ -438,57 +713,81 @@ function markdownImage ({ url, title, alt }) {
|
|
|
438
713
|
* @param {"svg" | "png" | "pdf"} [opts.outputFormat] - Mermaid output format.
|
|
439
714
|
* @param {string} [opts.artefacts] - Path to the artefacts directory.
|
|
440
715
|
* Defaults to `output` extension. Overrides `output` extension if set.
|
|
716
|
+
* @param {import("puppeteer").Browser} [opts.browser] - If set, reuses the given puppeteer browser instance instead of creating a new one.
|
|
717
|
+
* This may leak cookies/cache between runs.
|
|
718
|
+
* @param {Limiter} [opts.limiter] - If set, limiter function to avoid rendering too many diagrams in parallel.
|
|
441
719
|
* @param {ParseMDDOptions} [opts.parseMMDOptions] - Options to pass to {@link parseMMDOptions}.
|
|
442
720
|
*/
|
|
443
|
-
async function run
|
|
721
|
+
async function run(
|
|
722
|
+
input,
|
|
723
|
+
output,
|
|
724
|
+
{
|
|
725
|
+
browser: userPassedBrowser,
|
|
726
|
+
puppeteerConfig = {},
|
|
727
|
+
quiet = false,
|
|
728
|
+
outputFormat,
|
|
729
|
+
parseMMDOptions,
|
|
730
|
+
limiter = (x, ...args) => x(...args),
|
|
731
|
+
artefacts,
|
|
732
|
+
} = {},
|
|
733
|
+
) {
|
|
444
734
|
/**
|
|
445
735
|
* Logs the given message to stdout, unless `quiet` is set to `true`.
|
|
446
736
|
*
|
|
447
737
|
* @param {string} message - The message to maybe log.
|
|
448
738
|
*/
|
|
449
|
-
const info = message => {
|
|
739
|
+
const info = (message) => {
|
|
450
740
|
if (!quiet) {
|
|
451
|
-
console.info(message)
|
|
741
|
+
console.info(message);
|
|
452
742
|
}
|
|
453
|
-
}
|
|
743
|
+
};
|
|
454
744
|
|
|
455
745
|
// TODO: should we use a Markdown parser like remark instead of rolling our own parser?
|
|
456
|
-
const mermaidChartsInMarkdown =
|
|
457
|
-
|
|
746
|
+
const mermaidChartsInMarkdown =
|
|
747
|
+
/^[^\S\n]*[`:]{3}(?:mermaid)([^\S\n]*\r?\n([\s\S]*?))[`:]{3}[^\S\n]*$/;
|
|
748
|
+
const mermaidChartsInMarkdownRegexGlobal = new RegExp(
|
|
749
|
+
mermaidChartsInMarkdown,
|
|
750
|
+
"gm",
|
|
751
|
+
);
|
|
458
752
|
/**
|
|
459
|
-
* @type {puppeteer.Browser | undefined}
|
|
753
|
+
* @type {import('puppeteer').Browser | undefined}
|
|
460
754
|
* Lazy-loaded browser instance, only created when needed.
|
|
461
755
|
*/
|
|
462
|
-
let browser
|
|
756
|
+
let browser = userPassedBrowser;
|
|
463
757
|
try {
|
|
464
758
|
if (!outputFormat) {
|
|
465
759
|
const outputFormatFromFilename =
|
|
466
760
|
/**
|
|
467
761
|
* @type {"md" | "markdown" | "svg" | "png" | "pdf"}
|
|
468
|
-
*/ (path.extname(output).replace(
|
|
469
|
-
if (
|
|
762
|
+
*/ (path.extname(output).replace(".", ""));
|
|
763
|
+
if (
|
|
764
|
+
outputFormatFromFilename === "md" ||
|
|
765
|
+
outputFormatFromFilename === "markdown"
|
|
766
|
+
) {
|
|
470
767
|
// fallback to svg in case no outputFormat is given and output file is MD
|
|
471
|
-
outputFormat =
|
|
768
|
+
outputFormat = "svg";
|
|
472
769
|
} else {
|
|
473
|
-
outputFormat = outputFormatFromFilename
|
|
770
|
+
outputFormat = outputFormatFromFilename;
|
|
474
771
|
}
|
|
475
772
|
}
|
|
476
773
|
if (!/(?:svg|png|pdf)$/.test(outputFormat)) {
|
|
477
|
-
throw new Error('Output format must be one of "svg", "png" or "pdf"')
|
|
774
|
+
throw new Error('Output format must be one of "svg", "png" or "pdf"');
|
|
478
775
|
}
|
|
479
776
|
|
|
480
|
-
const definition = await getInputData(input)
|
|
777
|
+
const definition = await getInputData(input);
|
|
481
778
|
if (input && /\.(md|markdown)$/.test(input)) {
|
|
482
|
-
if (output ===
|
|
483
|
-
throw new Error(
|
|
779
|
+
if (output === "/dev/stdout") {
|
|
780
|
+
throw new Error("Cannot use `stdout` with markdown input");
|
|
484
781
|
}
|
|
485
782
|
|
|
486
|
-
const imagePromises = []
|
|
487
|
-
for (const mermaidCodeblockMatch of definition.matchAll(
|
|
783
|
+
const imagePromises = [];
|
|
784
|
+
for (const mermaidCodeblockMatch of definition.matchAll(
|
|
785
|
+
mermaidChartsInMarkdownRegexGlobal,
|
|
786
|
+
)) {
|
|
488
787
|
if (browser === undefined) {
|
|
489
|
-
browser = await puppeteer.launch(puppeteerConfig)
|
|
788
|
+
browser = await puppeteer.launch(puppeteerConfig);
|
|
490
789
|
}
|
|
491
|
-
const mermaidDefinition = mermaidCodeblockMatch[2]
|
|
790
|
+
const mermaidDefinition = mermaidCodeblockMatch[2];
|
|
492
791
|
|
|
493
792
|
/** Output can be either a template image file, or a `.md` output file.
|
|
494
793
|
* If it is a template image file, use that to created numbered diagrams
|
|
@@ -497,64 +796,88 @@ async function run (input, output, { puppeteerConfig = {}, quiet = false, output
|
|
|
497
796
|
* I.e. if "out.md". use "out-1.svg", "out-2.svg", etc
|
|
498
797
|
* @type {string}
|
|
499
798
|
*/
|
|
500
|
-
let outputFile = output
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
799
|
+
let outputFile = output
|
|
800
|
+
.replace(
|
|
801
|
+
/(\.(md|markdown|png|svg|pdf))$/,
|
|
802
|
+
`-${imagePromises.length + 1}$1`,
|
|
803
|
+
)
|
|
804
|
+
.replace(/\.(md|markdown)$/, `.${outputFormat}`);
|
|
504
805
|
|
|
505
806
|
if (artefacts) {
|
|
506
|
-
outputFile = path.resolve(artefacts, path.basename(outputFile))
|
|
807
|
+
outputFile = path.resolve(artefacts, path.basename(outputFile));
|
|
507
808
|
}
|
|
508
809
|
|
|
509
|
-
const outputFileRelative = `./${path.relative(path.dirname(path.resolve(output)), path.resolve(outputFile))}
|
|
510
|
-
|
|
511
|
-
const imagePromise = (
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
810
|
+
const outputFileRelative = `./${path.relative(path.dirname(path.resolve(output)), path.resolve(outputFile))}`;
|
|
811
|
+
|
|
812
|
+
const imagePromise = limiter(
|
|
813
|
+
async (browser, outputFormat) => {
|
|
814
|
+
const { title, desc, data } = await renderMermaid(
|
|
815
|
+
browser,
|
|
816
|
+
mermaidDefinition,
|
|
817
|
+
outputFormat,
|
|
818
|
+
parseMMDOptions,
|
|
819
|
+
);
|
|
820
|
+
await fs.promises.writeFile(outputFile, data);
|
|
821
|
+
info(` ✅ ${outputFileRelative}`);
|
|
822
|
+
|
|
823
|
+
return {
|
|
824
|
+
url: outputFileRelative,
|
|
825
|
+
title,
|
|
826
|
+
alt: desc,
|
|
827
|
+
};
|
|
828
|
+
},
|
|
829
|
+
browser,
|
|
830
|
+
outputFormat,
|
|
831
|
+
);
|
|
832
|
+
imagePromises.push(imagePromise);
|
|
523
833
|
}
|
|
524
834
|
|
|
525
835
|
if (imagePromises.length) {
|
|
526
|
-
info(`Found ${imagePromises.length} mermaid charts in Markdown input`)
|
|
836
|
+
info(`Found ${imagePromises.length} mermaid charts in Markdown input`);
|
|
527
837
|
} else {
|
|
528
|
-
info(
|
|
838
|
+
info("No mermaid charts found in Markdown input");
|
|
529
839
|
}
|
|
530
840
|
|
|
531
|
-
const images = await Promise.all(imagePromises)
|
|
841
|
+
const images = await Promise.all(imagePromises);
|
|
532
842
|
|
|
533
843
|
if (/\.(md|markdown)$/.test(output)) {
|
|
534
|
-
const outDefinition = definition.replace(
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
844
|
+
const outDefinition = definition.replace(
|
|
845
|
+
mermaidChartsInMarkdownRegexGlobal,
|
|
846
|
+
(_mermaidMd) => {
|
|
847
|
+
// pop first image from front of array
|
|
848
|
+
const { url, title, alt } =
|
|
849
|
+
/**
|
|
850
|
+
* @type {MarkdownImageProps} We use the same regex,
|
|
851
|
+
* so we will never try to get too many objects from the array.
|
|
852
|
+
* (aka `images.shift()` will never return `undefined`)
|
|
853
|
+
*/ (images.shift());
|
|
854
|
+
return markdownImage({ url, title, alt: alt || "diagram" });
|
|
855
|
+
},
|
|
856
|
+
);
|
|
857
|
+
await fs.promises.writeFile(output, outDefinition, "utf-8");
|
|
858
|
+
info(` ✅ ${output}`);
|
|
546
859
|
}
|
|
547
860
|
} else {
|
|
548
|
-
info(
|
|
549
|
-
browser
|
|
550
|
-
const { data } = await renderMermaid(
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
861
|
+
info("Generating single mermaid chart");
|
|
862
|
+
browser ??= await puppeteer.launch(puppeteerConfig);
|
|
863
|
+
const { data } = await renderMermaid(
|
|
864
|
+
browser,
|
|
865
|
+
definition,
|
|
866
|
+
outputFormat,
|
|
867
|
+
parseMMDOptions,
|
|
868
|
+
);
|
|
869
|
+
if (output === "/dev/stdout") {
|
|
870
|
+
await promisify(process.stdout.write).call(process.stdout, data);
|
|
871
|
+
} else {
|
|
872
|
+
await fs.promises.writeFile(output, data);
|
|
873
|
+
}
|
|
554
874
|
}
|
|
555
875
|
} finally {
|
|
556
|
-
|
|
876
|
+
// Don't close the browser if it was passed in by the user
|
|
877
|
+
if (browser !== userPassedBrowser) {
|
|
878
|
+
await browser?.close?.();
|
|
879
|
+
}
|
|
557
880
|
}
|
|
558
881
|
}
|
|
559
882
|
|
|
560
|
-
export { run, renderMermaid, cli, error }
|
|
883
|
+
export { run, renderMermaid, cli, error };
|