@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.
Files changed (77) hide show
  1. package/README.md +17 -11
  2. package/dist/index.html +0 -2
  3. package/dist-types/src/index.d.ts +19 -5
  4. package/dist-types/src/index.d.ts.map +1 -1
  5. package/dist-types/src/puppeteerIntercept.d.ts +17 -14
  6. package/dist-types/src/puppeteerIntercept.d.ts.map +1 -1
  7. package/dist-types/src/version.d.ts +1 -1
  8. package/dist-types/src/version.d.ts.map +1 -1
  9. package/package.json +22 -18
  10. package/src/cli.js +5 -3
  11. package/src/index.js +626 -303
  12. package/src/puppeteerIntercept.js +101 -54
  13. package/src/version.js +1 -1
  14. package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  15. package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  16. package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  17. package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  18. package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  19. package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  20. package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  21. package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  22. package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  23. package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  24. package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  25. package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  26. package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  27. package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  28. package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  29. package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  30. package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  31. package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  32. package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  33. package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  34. package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  35. package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  36. package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  37. package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  38. package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  39. package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  40. package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  41. package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  42. package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  43. package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  44. package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  45. package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  46. package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  47. package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  48. package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  49. package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  50. package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  51. package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  52. package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  53. package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  54. package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  55. package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  56. package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  57. package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  58. package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  59. package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  60. package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  61. package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  62. package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  63. package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  64. package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  65. package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  66. package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  67. package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  68. package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  69. package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  70. package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  71. package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  72. package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  73. package/dist/assets/fa-brands-400-BdzBFuGj.woff2 +0 -0
  74. package/dist/assets/fa-regular-400-DQuI-phE.woff2 +0 -0
  75. package/dist/assets/fa-solid-900-BLm1ImsD.woff2 +0 -0
  76. package/dist/assets/index-DxLypzbC.js +0 -10
  77. package/src/index.js.bak +0 -540
package/src/index.js CHANGED
@@ -1,23 +1,73 @@
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 path from 'path'
6
- import puppeteer from 'puppeteer'
7
- import url from 'url'
8
- import { version } from './version.js'
9
- import { Interceptor } from './puppeteerIntercept.js'
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('.', import.meta.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(path.dirname(url.fileURLToPath(resolve('mermaid', import.meta.url))), 'mermaid.esm.mjs')
19
- const elkESMPath = path.resolve(path.dirname(url.fileURLToPath(resolve('@mermaid-js/layout-elk', import.meta.url))), 'mermaid-layout-elk.esm.mjs')
20
- const zenumlESMPath = path.resolve(path.dirname(url.fileURLToPath(resolve('@mermaid-js/mermaid-zenuml', import.meta.url))), 'mermaid-zenuml.esm.mjs')
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 (inputFile) {
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 !== 'undefined') {
66
- return await fs.promises.readFile(inputFile, 'utf-8')
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('readable', function () {
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('error', function (err) {
80
- reject(err)
81
- })
129
+ process.stdin.on("error", function (err) {
130
+ reject(err);
131
+ });
82
132
 
83
- process.stdin.on('end', function () {
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 (value, _unused) {
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('Not a positive integer.')
151
+ throw new InvalidArgumentError("Not a positive integer.");
102
152
  }
103
- return parsedValue
153
+ return parsedValue;
104
154
  }
105
155
 
106
- async function cli () {
107
- const commander = new Command()
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(new Option('-t, --theme [theme]', 'Theme of the chart').choices(['default', 'forest', 'dark', 'neutral']).default('default'))
111
- .addOption(new Option('-w, --width [width]', 'Width of the page').argParser(parseCommanderInt).default(800))
112
- .addOption(new Option('-H, --height [height]', 'Height of the page').argParser(parseCommanderInt).default(600))
113
- .option('-i, --input <input>', '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.')
114
- .option('-o, --output [output]', 'Output file. It should be either md, svg, png, pdf or use `-` to output to stdout. Optional. Default: input + ".svg"')
115
- .option('-a, --artefacts [artefacts]', 'Output artefacts path. Only used with Markdown input file. Optional. Default: output directory')
116
- .addOption(new Option('-e, --outputFormat [format]', 'Output format for the generated image.').choices(['svg', 'png', 'pdf']).default(null, 'Loaded from the output file extension'))
117
- .addOption(new Option('-b, --backgroundColor [backgroundColor]', 'Background color for pngs/svgs (not pdfs). Example: transparent, red, \'#F0F0F0\'.').default('white'))
118
- .option('-c, --configFile [configFile]', 'JSON configuration file for mermaid.')
119
- .option('-C, --cssFile [cssFile]', 'CSS file for the page.')
120
- .option('-I, --svgId [svgId]', 'The id attribute for the SVG element to be rendered.')
121
- .addOption(new Option('-s, --scale [scale]', 'Puppeteer scale factor').argParser(parseCommanderInt).default(1))
122
- .option('-f, --pdfFit', 'Scale PDF to fit chart')
123
- .option('-q, --quiet', 'Suppress log output')
124
- .option('-p --puppeteerConfigFile [puppeteerConfigFile]', 'JSON configuration file for puppeteer.')
125
- .option('--iconPacks <icons...>', '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.', [])
126
- .option('--iconPacksNamesAndUrls <prefix#iconsurl...>', '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.', [])
127
- .parse(process.argv)
128
-
129
- const options = commander.opts()
130
-
131
- let { theme, width, height, input, output, outputFormat, backgroundColor, configFile, cssFile, svgId, puppeteerConfigFile, scale, pdfFit, quiet, iconPacks, iconPacksNamesAndUrls, artefacts } = options
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('No input file specified, reading from stdin. ' +
136
- 'If you want to specify an input file, please use `-i <input>.` ' +
137
- 'You can use `-i -` to read from stdin and to suppress this warning.'
138
- )
139
- } else if (input === '-') {
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
- // if an input file is defined, it should take precedence, otherwise, input is
149
- // coming from stdin and just name the file out.svg, if it hasn't been
150
- // specified with the '-o' option
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 ? (`${input}.${outputFormat}`) : `out.${outputFormat}`
300
+ output = input ? `${input}.${outputFormat}` : `out.${outputFormat}`;
153
301
  } else {
154
- output = input ? (`${input}.svg`) : 'out.svg'
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 = '/dev/stdout'
159
- quiet = true
306
+ output = "/dev/stdout";
307
+ quiet = true;
160
308
 
161
309
  if (!outputFormat) {
162
- outputFormat = 'svg'
163
- warn('No output format specified, using svg. ' +
164
- 'If you want to specify an output format and suppress this warning, ' +
165
- 'please use `-e <format>.` '
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('Output file must end with ".md"/".markdown", ".svg", ".png" or ".pdf"')
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('Artefacts [-a|--artefacts] path can only be used with Markdown input file')
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 !== '/dev/stdout' && !fs.existsSync(outputDir)) {
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(mermaidConfig, JSON.parse(fs.readFileSync(configFile, 'utf-8')))
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: 'shell'
200
- })
355
+ headless: "shell",
356
+ });
201
357
  if (puppeteerConfigFile) {
202
- checkConfigFile(puppeteerConfigFile)
203
- puppeteerConfig = Object.assign(puppeteerConfig, JSON.parse(fs.readFileSync(puppeteerConfigFile, 'utf-8')))
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, 'utf-8')
371
+ myCSS = fs.readFileSync(cssFile, "utf-8");
213
372
  }
214
373
 
215
- await run(
216
- input, output, {
217
- puppeteerConfig,
218
- quiet,
219
- outputFormat,
220
- parseMMDOptions: {
221
- mermaidConfig, backgroundColor, myCSS, pdfFit, viewport: { width, height, deviceScaleFactor: scale }, svgId, iconPacks, iconPacksNamesAndUrls
222
- },
223
- artefacts
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 (browser, definition, outputFormat, { viewport, backgroundColor = 'white', mermaidConfig = {}, myCSS, pdfFit, svgId, iconPacks = [], iconPacksNamesAndUrls = [] } = {}) {
250
- const page = await browser.newPage()
251
- page.on('console', (msg) => {
252
- console.warn(msg.text())
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, '..', 'dist', 'index.html')
259
- await page.goto(url.pathToFileURL(mermaidHTMLPath).href)
260
- await page.$eval('body', (body, backgroundColor) => {
261
- body.style.background = backgroundColor
262
- }, backgroundColor)
263
-
264
- const interceptor = new Interceptor()
265
- const mermaidUrl = await interceptor.fileUrlToInterceptUrl(url.pathToFileURL(mermaidESMPath))
266
- const elkUrl = await interceptor.fileUrlToInterceptUrl(url.pathToFileURL(elkESMPath))
267
- const zenumlUrl = await interceptor.fileUrlToInterceptUrl(url.pathToFileURL(zenumlESMPath))
268
-
269
- page.on('request', interceptor.interceptRequestHandler)
270
- await page.setRequestInterception(true)
271
-
272
- const metadata = await page.$eval('#container', async (container, { definition, mermaidConfig, myCSS, backgroundColor, svgId, iconPacks, iconPacksNamesAndUrls, elkUrl, mermaidUrl, zenumlUrl }) => {
273
- const { default: mermaid } = await import(mermaidUrl)
274
- const { default: elkLayouts } = await import(elkUrl)
275
- const { default: zenuml } = await import(zenumlUrl)
276
- await Promise.all(Array.from(document.fonts, (font) => font.load()))
277
-
278
- await mermaid.registerExternalDiagrams([zenuml])
279
- mermaid.registerLayoutLoaders(elkLayouts)
280
- // lazy load icon packs
281
-
282
- mermaid.registerIconPacks(
283
- iconPacks.map((icon) => ({
284
- name: icon.split('/')[1],
285
- loader: () =>
286
- fetch(`https://unpkg.com/${icon}/icons.json`)
287
- .then((res) => res.json())
288
- .catch(() => error(`Failed to fetch icon: ${icon}`))
289
- }))
290
- )
291
-
292
- mermaid.registerIconPacks(
293
- iconPacksNamesAndUrls.map((iconPackInfo) => {
294
- const packName = iconPackInfo.split('#')[0]
295
- const packUrl = iconPackInfo.split('#')[1]
296
-
297
- return ({
298
- name: packName,
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(packUrl)
518
+ fetch(`https://unpkg.com/${icon}/icons.json`)
301
519
  .then((res) => res.json())
302
- .catch(() => {
303
- error(`Failed to fetch icon: ${iconPackInfo}`)
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
- // Finds SVG metadata for accessibility purposes
330
- /** SVG title */
331
- let title = null
332
- // If <title> exists, it must be the first child Node,
333
- // see https://www.w3.org/TR/SVG11/struct.html#DescriptionAndTitleElements
334
- /* global SVGTitleElement, SVGDescElement */ // These exist in browser-based code
335
- if (svg.firstChild instanceof SVGTitleElement) {
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
- return {
346
- title, desc
347
- }
348
- }, { definition, mermaidConfig, myCSS, backgroundColor, svgId, iconPacks, iconPacksNamesAndUrls, elkUrl, mermaidUrl, zenumlUrl })
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 === 'svg') {
351
- const svgXML = await page.$eval('svg', (svg) => {
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
- // eslint-disable-next-line no-undef
356
- const xmlSerializer = new XMLSerializer()
357
- return xmlSerializer.serializeToString(svg)
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 === 'png') {
364
- const clip = await page.$eval('svg', svg => {
365
- const react = svg.getBoundingClientRect()
366
- return { x: Math.floor(react.left), y: Math.floor(react.top), width: Math.ceil(react.width), height: Math.ceil(react.height) }
367
- })
368
- await page.setViewport({ ...viewport, width: clip.x + clip.width, height: clip.y + clip.height })
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({ clip, omitBackground: backgroundColor === 'transparent' })
372
- }
373
- } else { // pdf
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('svg', svg => {
376
- const react = svg.getBoundingClientRect()
377
- return { x: react.left, y: react.top, width: react.width, height: react.height }
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 === 'transparent',
383
- width: (Math.ceil(clip.width) + clip.x * 2) + 'px',
384
- height: (Math.ceil(clip.height) + clip.y * 2) + 'px',
385
- pageRanges: '1-1'
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 === 'transparent'
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 {`![${string}](${string})`} The markdown image text.
415
683
  */
416
- function markdownImage ({ url, title, alt }) {
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 `![${altEscaped}](${url} "${titleEscaped}")`
688
+ const titleEscaped = title.replace(/["\\]/g, "\\$&");
689
+ return `![${altEscaped}](${url} "${titleEscaped}")`;
422
690
  } else {
423
- return `![${altEscaped}](${url})`
691
+ return `![${altEscaped}](${url})`;
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 (input, output, { puppeteerConfig = {}, quiet = false, outputFormat, parseMMDOptions, artefacts } = {}) {
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 = /^[^\S\n]*[`:]{3}(?:mermaid)([^\S\n]*\r?\n([\s\S]*?))[`:]{3}[^\S\n]*$/
457
- const mermaidChartsInMarkdownRegexGlobal = new RegExp(mermaidChartsInMarkdown, 'gm')
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 (outputFormatFromFilename === 'md' || outputFormatFromFilename === 'markdown') {
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 = 'svg'
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 === '/dev/stdout') {
483
- throw new Error('Cannot use `stdout` with markdown input')
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(mermaidChartsInMarkdownRegexGlobal)) {
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.replace(
501
- /(\.(md|markdown|png|svg|pdf))$/,
502
- `-${imagePromises.length + 1}$1`
503
- ).replace(/\.(md|markdown)$/, `.${outputFormat}`)
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 = (async () => {
512
- const { title, desc, data } = await renderMermaid(browser, mermaidDefinition, outputFormat, parseMMDOptions)
513
- await fs.promises.writeFile(outputFile, data)
514
- info(` ✅ ${outputFileRelative}`)
515
-
516
- return {
517
- url: outputFileRelative,
518
- title,
519
- alt: desc
520
- }
521
- })()
522
- imagePromises.push(imagePromise)
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('No mermaid charts found in Markdown input')
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(mermaidChartsInMarkdownRegexGlobal, (_mermaidMd) => {
535
- // pop first image from front of array
536
- const { url, title, alt } =
537
- /**
538
- * @type {MarkdownImageProps} We use the same regex,
539
- * so we will never try to get too many objects from the array.
540
- * (aka `images.shift()` will never return `undefined`)
541
- */ (images.shift())
542
- return markdownImage({ url, title, alt: alt || 'diagram' })
543
- })
544
- await fs.promises.writeFile(output, outDefinition, 'utf-8')
545
- info(` ✅ ${output}`)
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('Generating single mermaid chart')
549
- browser = await puppeteer.launch(puppeteerConfig)
550
- const { data } = await renderMermaid(browser, definition, outputFormat, parseMMDOptions)
551
- await output !== '/dev/stdout'
552
- ? fs.promises.writeFile(output, data)
553
- : process.stdout.write(data)
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
- await browser?.close?.()
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 };