@mermaid-js/mermaid-cli 11.15.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/src/index.js CHANGED
@@ -1,47 +1,71 @@
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'
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";
13
13
 
14
14
  // __dirname is not available in ESM modules by default
15
- const __dirname = url.fileURLToPath(new url.URL('.', import.meta.url))
15
+ const __dirname = url.fileURLToPath(new url.URL(".", import.meta.url));
16
16
 
17
17
  /**
18
18
  * CSS paths to embed in the page.
19
19
  */
20
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
- })
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
+ });
27
27
 
28
28
  /**
29
29
  * ESM bundles. Our interceptor doesn't support loading ESM modules that load
30
30
  * other modules using relative paths, so these have to no `dependencies`.
31
31
  */
32
- const mermaidESMPath = path.resolve(path.dirname(url.fileURLToPath(resolve('mermaid', import.meta.url))), 'mermaid.esm.mjs')
33
- const elkESMPath = path.resolve(path.dirname(url.fileURLToPath(resolve('@mermaid-js/layout-elk', import.meta.url))), 'mermaid-layout-elk.esm.mjs')
34
- 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
+ );
35
48
 
36
49
  /** @type {string | undefined} Path to `@mermaid-js/layout-tidy-tree`, if it is installed */
37
- let tidyTreeESMPath
50
+ let tidyTreeESMPath;
38
51
  try {
39
- tidyTreeESMPath = path.resolve(path.dirname(url.fileURLToPath(resolve('@mermaid-js/layout-tidy-tree', import.meta.url))), 'mermaid-layout-tidy-tree.esm.mjs')
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
+ );
40
60
  } catch (error) {
41
- if (error instanceof Error && 'code' in error && error.code === 'ERR_MODULE_NOT_FOUND') {
61
+ if (
62
+ error instanceof Error &&
63
+ "code" in error &&
64
+ error.code === "ERR_MODULE_NOT_FOUND"
65
+ ) {
42
66
  // optional dependency, this is normal
43
67
  } else {
44
- throw error
68
+ throw error;
45
69
  }
46
70
  }
47
71
 
@@ -51,19 +75,19 @@ try {
51
75
  * @param {string} message - The message to print to `stderr`.
52
76
  * @returns {never} Quits Node.JS, so never returns.
53
77
  */
54
- const error = message => {
55
- console.error(chalk.red(`\n${message}\n`))
56
- process.exit(1)
57
- }
78
+ const error = (message) => {
79
+ console.error(chalk.red(`\n${message}\n`));
80
+ process.exit(1);
81
+ };
58
82
 
59
83
  /**
60
84
  * Prints a warning to stderr.
61
85
  *
62
86
  * @param {string} message - The message to print to `stderr`.
63
87
  */
64
- const warn = message => {
65
- console.warn(chalk.yellow(`\n${message}\n`))
66
- }
88
+ const warn = (message) => {
89
+ console.warn(chalk.yellow(`\n${message}\n`));
90
+ };
67
91
 
68
92
  /**
69
93
  * Checks if the given file exists.
@@ -72,11 +96,11 @@ const warn = message => {
72
96
  * @returns {never | void} If the file doesn't exist, closes Node.JS with
73
97
  * exit code 1.
74
98
  */
75
- const checkConfigFile = file => {
99
+ const checkConfigFile = (file) => {
76
100
  if (!fs.existsSync(file)) {
77
- error(`Configuration file "${file}" doesn't exist`)
101
+ error(`Configuration file "${file}" doesn't exist`);
78
102
  }
79
- }
103
+ };
80
104
 
81
105
  /**
82
106
  * Gets the data in the given file.
@@ -85,31 +109,31 @@ const checkConfigFile = file => {
85
109
  * If `undefined`, reads from `stdin` instead.
86
110
  * @returns {Promise<string>} The contents of `inputFile` parsed as `utf8`.
87
111
  */
88
- async function getInputData (inputFile) {
112
+ async function getInputData(inputFile) {
89
113
  // if an input file has been specified using '-i', it takes precedence over
90
114
  // piping from stdin
91
- if (typeof inputFile !== 'undefined') {
92
- return await fs.promises.readFile(inputFile, 'utf-8')
115
+ if (typeof inputFile !== "undefined") {
116
+ return await fs.promises.readFile(inputFile, "utf-8");
93
117
  }
94
118
 
95
119
  return await new Promise((resolve, reject) => {
96
- let data = ''
97
- process.stdin.on('readable', function () {
98
- const chunk = process.stdin.read()
120
+ let data = "";
121
+ process.stdin.on("readable", function () {
122
+ const chunk = process.stdin.read();
99
123
 
100
124
  if (chunk !== null) {
101
- data += chunk
125
+ data += chunk;
102
126
  }
103
- })
127
+ });
104
128
 
105
- process.stdin.on('error', function (err) {
106
- reject(err)
107
- })
129
+ process.stdin.on("error", function (err) {
130
+ reject(err);
131
+ });
108
132
 
109
- process.stdin.on('end', function () {
110
- resolve(data)
111
- })
112
- })
133
+ process.stdin.on("end", function () {
134
+ resolve(data);
135
+ });
136
+ });
113
137
  }
114
138
 
115
139
  /**
@@ -121,12 +145,12 @@ async function getInputData (inputFile) {
121
145
  * @throws {InvalidArgumentError} If the arg is not valid.
122
146
  * @see https://github.com/tj/commander.js/wiki/Class:-Option#argparserfn
123
147
  */
124
- function parseCommanderInt (value, _unused) {
125
- const parsedValue = parseInt(value, 10)
148
+ function parseCommanderInt(value, _unused) {
149
+ const parsedValue = parseInt(value, 10);
126
150
  if (isNaN(parsedValue) || parsedValue < 1) {
127
- throw new InvalidArgumentError('Not a positive integer.')
151
+ throw new InvalidArgumentError("Not a positive integer.");
128
152
  }
129
- return parsedValue
153
+ return parsedValue;
130
154
  }
131
155
 
132
156
  /**
@@ -137,102 +161,189 @@ function parseCommanderInt (value, _unused) {
137
161
  * @returns {number} The value parsed as a number.
138
162
  * @see https://github.com/tj/commander.js/wiki/Class:-Option#argparserfn
139
163
  */
140
- function parseCommanderFloat (value, _unused) {
141
- const parsedValue = parseFloat(value)
164
+ function parseCommanderFloat(value, _unused) {
165
+ const parsedValue = parseFloat(value);
142
166
  if (isNaN(parsedValue) || parsedValue <= 0) {
143
- throw new InvalidArgumentError('Not a positive number.')
167
+ throw new InvalidArgumentError("Not a positive number.");
144
168
  }
145
- return parsedValue
169
+ return parsedValue;
146
170
  }
147
171
 
148
- async function cli () {
149
- const commander = new Command()
172
+ async function cli() {
173
+ const commander = new Command();
150
174
  commander
151
175
  .version(version)
152
- .addOption(new Option('-t, --theme [theme]', 'Theme of the chart').choices(['default', 'forest', 'dark', 'neutral']).default('default'))
153
- .addOption(new Option('-w, --width [width]', 'Width of the page').argParser(parseCommanderInt).default(800))
154
- .addOption(new Option('-H, --height [height]', 'Height of the page').argParser(parseCommanderInt).default(600))
155
- .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.')
156
- .option('-o, --output [output]', 'Output file. It should be either md, svg, png, pdf or use `-` to output to stdout. Optional. Default: input + ".svg"')
157
- .option('-a, --artefacts [artefacts]', 'Output artefacts path. Only used with Markdown input file. Optional. Default: output directory')
158
- .addOption(new Option('-j, --jobs <jobs>', 'Number of parallel jobs to run when rendering multiple diagrams. Defaults to half the available CPUs.').argParser(parseCommanderInt).default(
159
- Math.floor(os.availableParallelism() / 2) || 1
160
- ))
161
- .addOption(new Option('-e, --outputFormat [format]', 'Output format for the generated image.').choices(['svg', 'png', 'pdf']).default(null, 'Loaded from the output file extension'))
162
- .addOption(new Option('-b, --backgroundColor [backgroundColor]', 'Background color for pngs/svgs (not pdfs). Example: transparent, red, \'#F0F0F0\'.').default('white'))
163
- .option('-c, --configFile [configFile]', 'JSON configuration file for mermaid.')
164
- .option('-C, --cssFile [cssFile]', 'CSS file for the page.')
165
- .option('-I, --svgId [svgId]', 'The id attribute for the SVG element to be rendered.')
166
- .addOption(new Option('-s, --scale [scale]', 'Puppeteer scale factor').argParser(parseCommanderFloat).default(1))
167
- .option('-f, --pdfFit', 'Scale PDF to fit chart')
168
- .option('-q, --quiet', 'Suppress log output')
169
- .option('-p --puppeteerConfigFile [puppeteerConfigFile]', 'JSON configuration file for puppeteer.')
170
- .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.', [])
171
- .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.', [])
172
- .parse(process.argv)
173
-
174
- const options = commander.opts()
175
-
176
- let { theme, width, height, input, output, outputFormat, backgroundColor, configFile, cssFile, svgId, puppeteerConfigFile, scale, pdfFit, quiet, iconPacks, iconPacksNamesAndUrls, artefacts, jobs } = 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;
177
279
 
178
280
  // check input file
179
281
  if (!input) {
180
- warn('No input file specified, reading from stdin. ' +
181
- 'If you want to specify an input file, please use `-i <input>.` ' +
182
- 'You can use `-i -` to read from stdin and to suppress this warning.'
183
- )
184
- } 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 === "-") {
185
288
  // `--input -` means read from stdin, but suppress the above warning
186
- input = undefined
289
+ input = undefined;
187
290
  } else if (!fs.existsSync(input)) {
188
- error(`Input file "${input}" doesn't exist`)
291
+ error(`Input file "${input}" doesn't exist`);
189
292
  }
190
293
 
191
294
  // check output file
192
295
  if (!output) {
193
- // if an input file is defined, it should take precedence, otherwise, input is
194
- // coming from stdin and just name the file out.svg, if it hasn't been
195
- // 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
196
299
  if (outputFormat) {
197
- output = input ? (`${input}.${outputFormat}`) : `out.${outputFormat}`
300
+ output = input ? `${input}.${outputFormat}` : `out.${outputFormat}`;
198
301
  } else {
199
- output = input ? (`${input}.svg`) : 'out.svg'
302
+ output = input ? `${input}.svg` : "out.svg";
200
303
  }
201
- } else if (output === '-') {
304
+ } else if (output === "-") {
202
305
  // `--output -` means write to stdout.
203
- output = '/dev/stdout'
204
- quiet = true
306
+ output = "/dev/stdout";
307
+ quiet = true;
205
308
 
206
309
  if (!outputFormat) {
207
- outputFormat = 'svg'
208
- warn('No output format specified, using svg. ' +
209
- 'If you want to specify an output format and suppress this warning, ' +
210
- 'please use `-e <format>.` '
211
- )
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
+ );
212
316
  }
213
317
  } else if (!/\.(?:svg|png|pdf|md|markdown)$/.test(output)) {
214
- 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
+ );
215
321
  }
216
322
 
217
323
  if (artefacts) {
218
324
  if (!input || !/\.(?:md|markdown)$/.test(input)) {
219
- 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
+ );
220
328
  }
221
329
  if (!fs.existsSync(artefacts)) {
222
- fs.mkdirSync(artefacts, { recursive: true })
330
+ fs.mkdirSync(artefacts, { recursive: true });
223
331
  }
224
332
  }
225
333
 
226
- const outputDir = path.dirname(output)
227
- if (output !== '/dev/stdout' && !fs.existsSync(outputDir)) {
228
- 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`);
229
337
  }
230
338
 
231
339
  // check config files
232
- let mermaidConfig = { theme }
340
+ let mermaidConfig = { theme };
233
341
  if (configFile) {
234
- checkConfigFile(configFile)
235
- 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
+ );
236
347
  }
237
348
 
238
349
  let puppeteerConfig = /** @type {import('puppeteer').LaunchOptions} */ ({
@@ -241,34 +352,42 @@ async function cli () {
241
352
  * but still works. In Puppeteer v22, it uses the `chrome-headless-shell` package,
242
353
  * which is much faster than the regular headless mode.
243
354
  */
244
- headless: 'shell'
245
- })
355
+ headless: "shell",
356
+ });
246
357
  if (puppeteerConfigFile) {
247
- checkConfigFile(puppeteerConfigFile)
248
- 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
+ );
249
363
  }
250
364
 
251
365
  // check cssFile
252
- let myCSS
366
+ let myCSS;
253
367
  if (cssFile) {
254
368
  if (!fs.existsSync(cssFile)) {
255
- error(`CSS file "${cssFile}" doesn't exist`)
369
+ error(`CSS file "${cssFile}" doesn't exist`);
256
370
  }
257
- myCSS = fs.readFileSync(cssFile, 'utf-8')
371
+ myCSS = fs.readFileSync(cssFile, "utf-8");
258
372
  }
259
373
 
260
- await run(
261
- input, output, {
262
- puppeteerConfig,
263
- quiet,
264
- outputFormat,
265
- limiter: pLimit(jobs),
266
- parseMMDOptions: {
267
- mermaidConfig, backgroundColor, myCSS, pdfFit, viewport: { width, height, deviceScaleFactor: scale }, svgId, iconPacks, iconPacksNamesAndUrls
268
- },
269
- artefacts
270
- }
271
- )
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
+ });
272
391
  }
273
392
 
274
393
  /**
@@ -292,174 +411,259 @@ async function cli () {
292
411
  * @returns {Promise<{title: string | null, desc: string | null, data: Uint8Array}>} The output file in bytes,
293
412
  * with optional metadata.
294
413
  */
295
- async function renderMermaid (browser, definition, outputFormat, { viewport, backgroundColor = 'white', mermaidConfig = {}, myCSS, pdfFit, svgId, iconPacks = [], iconPacksNamesAndUrls = [] } = {}) {
296
- const page = await browser.newPage()
297
- page.on('console', (msg) => {
298
- console.warn(msg.text())
299
- })
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
+ });
300
433
  try {
301
434
  if (viewport) {
302
- await page.setViewport(viewport)
435
+ await page.setViewport(viewport);
303
436
  }
304
- const mermaidHTMLPath = path.join(__dirname, '..', 'dist', 'index.html')
305
- await page.goto(url.pathToFileURL(mermaidHTMLPath).href)
306
- await page.$eval('body', (body, backgroundColor) => {
307
- body.style.background = backgroundColor
308
- }, backgroundColor)
309
-
310
- const interceptor = new Interceptor()
311
- const mermaidUrl = await interceptor.fileUrlToInterceptUrl(url.pathToFileURL(mermaidESMPath))
312
- const elkUrl = await interceptor.fileUrlToInterceptUrl(url.pathToFileURL(elkESMPath))
313
- const zenumlUrl = await interceptor.fileUrlToInterceptUrl(url.pathToFileURL(zenumlESMPath))
314
- const tidyTreeESMUrl = tidyTreeESMPath ? await interceptor.fileUrlToInterceptUrl(url.pathToFileURL(tidyTreeESMPath)) : undefined
315
-
316
- page.on('request', interceptor.interceptRequestHandler)
317
- await page.setRequestInterception(true)
318
-
319
- await Promise.all(Object.entries(cssImports).map(async ([cssImport, { level }]) => {
320
- const interceptUrl = await interceptor.fileUrlToInterceptUrl(new URL(resolve(cssImport, import.meta.url)), {
321
- allowParentDirectoryLevel: level
322
- })
323
- await page.addStyleTag({
324
- url: interceptUrl
325
- })
326
- }))
327
-
328
- const metadata = await page.$eval('#container', async (container, { definition, mermaidConfig, myCSS, backgroundColor, svgId, iconPacks, iconPacksNamesAndUrls, elkUrl, mermaidUrl, zenumlUrl, tidyTreeESMUrl }) => {
329
- const { default: mermaid } = await import(mermaidUrl)
330
- /** @type {typeof import('@mermaid-js/layout-elk')} */
331
- const { default: elkLayouts } = await import(elkUrl)
332
- /** @type {typeof import('@mermaid-js/mermaid-zenuml')} */
333
- const { default: zenuml } = await import(zenumlUrl)
334
- // @ts-ignore -- @mermaid-js/layout-tidy-tree is an optionalDependency and might not be installed
335
- /** @type {typeof import('@mermaid-js/layout-tidy-tree') | {default: undefined}} */
336
- const { default: tidyTree } = tidyTreeESMUrl ? await import(tidyTreeESMUrl) : { default: undefined }
337
- await Promise.all(Array.from(document.fonts, (font) => font.load()))
338
-
339
- await mermaid.registerExternalDiagrams([zenuml])
340
- mermaid.registerLayoutLoaders([
341
- ...elkLayouts,
342
- ...(tidyTree ?? [])
343
- ])
344
- // lazy load icon packs
345
-
346
- mermaid.registerIconPacks(
347
- iconPacks.map((icon) => ({
348
- name: icon.split('/')[1],
349
- loader: () =>
350
- fetch(`https://unpkg.com/${icon}/icons.json`)
351
- .then((res) => res.json())
352
- .catch(() => error(`Failed to fetch icon: ${icon}`))
353
- }))
354
- )
355
-
356
- mermaid.registerIconPacks(
357
- iconPacksNamesAndUrls.map((iconPackInfo) => {
358
- const packName = iconPackInfo.split('#')[0]
359
- const packUrl = iconPackInfo.split('#')[1]
360
-
361
- return ({
362
- 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],
363
517
  loader: () =>
364
- fetch(packUrl)
518
+ fetch(`https://unpkg.com/${icon}/icons.json`)
365
519
  .then((res) => res.json())
366
- .catch(() => {
367
- error(`Failed to fetch icon: ${iconPackInfo}`)
368
- })
369
- }
370
- )
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);
371
564
  }
372
- )
373
- )
374
- mermaid.initialize({ startOnLoad: false, ...mermaidConfig })
375
- // should throw an error if mmd diagram is invalid
376
- const { svg: svgText } = await mermaid.render(svgId || 'my-svg', definition, container)
377
- container.innerHTML = svgText
378
-
379
- const svg = container.getElementsByTagName?.('svg')?.[0]
380
- if (svg?.style) {
381
- svg.style.backgroundColor = backgroundColor
382
- } else {
383
- warn('svg not found. Not applying background color.')
384
- }
385
- if (myCSS) {
386
- // add CSS as a <svg>...<style>... element
387
- // see https://developer.mozilla.org/en-US/docs/Web/API/SVGStyleElement
388
- const style = document.createElementNS('http://www.w3.org/2000/svg', 'style')
389
- style.appendChild(document.createTextNode(myCSS))
390
- svg.appendChild(style)
391
- }
392
565
 
393
- // Finds SVG metadata for accessibility purposes
394
- /** SVG title */
395
- let title = null
396
- // If <title> exists, it must be the first child Node,
397
- // see https://www.w3.org/TR/SVG11/struct.html#DescriptionAndTitleElements
398
- /* global SVGTitleElement, SVGDescElement */ // These exist in browser-based code
399
- if (svg.firstChild instanceof SVGTitleElement) {
400
- title = svg.firstChild.textContent
401
- }
402
- /** SVG description. According to SVG spec, we should use the first one we find */
403
- let desc = null
404
- for (const svgNode of svg.children) {
405
- if (svgNode instanceof SVGDescElement) {
406
- 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;
407
573
  }
408
- }
409
- return {
410
- title, desc
411
- }
412
- }, { definition, mermaidConfig, myCSS, backgroundColor, svgId, iconPacks, iconPacksNamesAndUrls, elkUrl, mermaidUrl, zenumlUrl, tidyTreeESMUrl })
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
+ );
413
600
 
414
- if (outputFormat === 'svg') {
415
- const svgXML = await page.$eval('svg', (svg) => {
601
+ if (outputFormat === "svg") {
602
+ const svgXML = await page.$eval("svg", (svg) => {
416
603
  // SVG might have HTML <foreignObject> that are not valid XML
417
604
  // E.g. <br> must be replaced with <br/>
418
605
  // Luckily the DOM Web API has the XMLSerializer for this
419
- // eslint-disable-next-line no-undef
420
- const xmlSerializer = new XMLSerializer()
421
- return xmlSerializer.serializeToString(svg)
422
- })
606
+ const xmlSerializer = new XMLSerializer();
607
+ return xmlSerializer.serializeToString(svg);
608
+ });
423
609
  return {
424
610
  ...metadata,
425
- data: new TextEncoder().encode(svgXML)
426
- }
427
- } else if (outputFormat === 'png') {
428
- const clip = await page.$eval('svg', svg => {
429
- const react = svg.getBoundingClientRect()
430
- return { x: Math.floor(react.left), y: Math.floor(react.top), width: Math.ceil(react.width), height: Math.ceil(react.height) }
431
- })
432
- 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
+ });
433
628
  return {
434
629
  ...metadata,
435
- data: await page.screenshot({ clip, omitBackground: backgroundColor === 'transparent' })
436
- }
437
- } else { // pdf
630
+ data: await page.screenshot({
631
+ clip,
632
+ omitBackground: backgroundColor === "transparent",
633
+ }),
634
+ };
635
+ } else {
636
+ // pdf
438
637
  if (pdfFit) {
439
- const clip = await page.$eval('svg', svg => {
440
- const react = svg.getBoundingClientRect()
441
- return { x: react.left, y: react.top, width: react.width, height: react.height }
442
- })
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
+ });
443
647
  return {
444
648
  ...metadata,
445
649
  data: await page.pdf({
446
- omitBackground: backgroundColor === 'transparent',
447
- width: (Math.ceil(clip.width) + clip.x * 2) + 'px',
448
- height: (Math.ceil(clip.height) + clip.y * 2) + 'px',
449
- pageRanges: '1-1'
450
- })
451
- }
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
+ };
452
656
  } else {
453
657
  return {
454
658
  ...metadata,
455
659
  data: await page.pdf({
456
- omitBackground: backgroundColor === 'transparent'
457
- })
458
- }
660
+ omitBackground: backgroundColor === "transparent",
661
+ }),
662
+ };
459
663
  }
460
664
  }
461
665
  } finally {
462
- await page.close()
666
+ await page.close();
463
667
  }
464
668
  }
465
669
 
@@ -477,14 +681,14 @@ async function renderMermaid (browser, definition, outputFormat, { viewport, bac
477
681
  * @param {MarkdownImageProps} params - Parameters.
478
682
  * @returns {`![${string}](${string})`} The markdown image text.
479
683
  */
480
- function markdownImage ({ url, title, alt }) {
684
+ function markdownImage({ url, title, alt }) {
481
685
  // we can't use String.prototype.replaceAll since it's not supported in Node v14
482
- const altEscaped = alt.replace(/[[\]\\]/g, '\\$&')
686
+ const altEscaped = alt.replace(/[[\]\\]/g, "\\$&");
483
687
  if (title) {
484
- const titleEscaped = title.replace(/["\\]/g, '\\$&')
485
- return `![${altEscaped}](${url} "${titleEscaped}")`
688
+ const titleEscaped = title.replace(/["\\]/g, "\\$&");
689
+ return `![${altEscaped}](${url} "${titleEscaped}")`;
486
690
  } else {
487
- return `![${altEscaped}](${url})`
691
+ return `![${altEscaped}](${url})`;
488
692
  }
489
693
  }
490
694
 
@@ -514,55 +718,76 @@ function markdownImage ({ url, title, alt }) {
514
718
  * @param {Limiter} [opts.limiter] - If set, limiter function to avoid rendering too many diagrams in parallel.
515
719
  * @param {ParseMDDOptions} [opts.parseMMDOptions] - Options to pass to {@link parseMMDOptions}.
516
720
  */
517
- async function run (input, output, { browser: userPassedBrowser, puppeteerConfig = {}, quiet = false, outputFormat, parseMMDOptions, limiter = (x, ...args) => x(...args), 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
+ ) {
518
734
  /**
519
735
  * Logs the given message to stdout, unless `quiet` is set to `true`.
520
736
  *
521
737
  * @param {string} message - The message to maybe log.
522
738
  */
523
- const info = message => {
739
+ const info = (message) => {
524
740
  if (!quiet) {
525
- console.info(message)
741
+ console.info(message);
526
742
  }
527
- }
743
+ };
528
744
 
529
745
  // TODO: should we use a Markdown parser like remark instead of rolling our own parser?
530
- const mermaidChartsInMarkdown = /^[^\S\n]*[`:]{3}(?:mermaid)([^\S\n]*\r?\n([\s\S]*?))[`:]{3}[^\S\n]*$/
531
- 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
+ );
532
752
  /**
533
- * @type {puppeteer.Browser | undefined}
753
+ * @type {import('puppeteer').Browser | undefined}
534
754
  * Lazy-loaded browser instance, only created when needed.
535
755
  */
536
- let browser = userPassedBrowser
756
+ let browser = userPassedBrowser;
537
757
  try {
538
758
  if (!outputFormat) {
539
759
  const outputFormatFromFilename =
540
760
  /**
541
761
  * @type {"md" | "markdown" | "svg" | "png" | "pdf"}
542
- */ (path.extname(output).replace('.', ''))
543
- if (outputFormatFromFilename === 'md' || outputFormatFromFilename === 'markdown') {
762
+ */ (path.extname(output).replace(".", ""));
763
+ if (
764
+ outputFormatFromFilename === "md" ||
765
+ outputFormatFromFilename === "markdown"
766
+ ) {
544
767
  // fallback to svg in case no outputFormat is given and output file is MD
545
- outputFormat = 'svg'
768
+ outputFormat = "svg";
546
769
  } else {
547
- outputFormat = outputFormatFromFilename
770
+ outputFormat = outputFormatFromFilename;
548
771
  }
549
772
  }
550
773
  if (!/(?:svg|png|pdf)$/.test(outputFormat)) {
551
- 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"');
552
775
  }
553
776
 
554
- const definition = await getInputData(input)
777
+ const definition = await getInputData(input);
555
778
  if (input && /\.(md|markdown)$/.test(input)) {
556
- if (output === '/dev/stdout') {
557
- throw new Error('Cannot use `stdout` with markdown input')
779
+ if (output === "/dev/stdout") {
780
+ throw new Error("Cannot use `stdout` with markdown input");
558
781
  }
559
782
 
560
- const imagePromises = []
561
- for (const mermaidCodeblockMatch of definition.matchAll(mermaidChartsInMarkdownRegexGlobal)) {
783
+ const imagePromises = [];
784
+ for (const mermaidCodeblockMatch of definition.matchAll(
785
+ mermaidChartsInMarkdownRegexGlobal,
786
+ )) {
562
787
  if (browser === undefined) {
563
- browser = await puppeteer.launch(puppeteerConfig)
788
+ browser = await puppeteer.launch(puppeteerConfig);
564
789
  }
565
- const mermaidDefinition = mermaidCodeblockMatch[2]
790
+ const mermaidDefinition = mermaidCodeblockMatch[2];
566
791
 
567
792
  /** Output can be either a template image file, or a `.md` output file.
568
793
  * If it is a template image file, use that to created numbered diagrams
@@ -571,69 +796,88 @@ async function run (input, output, { browser: userPassedBrowser, puppeteerConfig
571
796
  * I.e. if "out.md". use "out-1.svg", "out-2.svg", etc
572
797
  * @type {string}
573
798
  */
574
- let outputFile = output.replace(
575
- /(\.(md|markdown|png|svg|pdf))$/,
576
- `-${imagePromises.length + 1}$1`
577
- ).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}`);
578
805
 
579
806
  if (artefacts) {
580
- outputFile = path.resolve(artefacts, path.basename(outputFile))
807
+ outputFile = path.resolve(artefacts, path.basename(outputFile));
581
808
  }
582
809
 
583
- const outputFileRelative = `./${path.relative(path.dirname(path.resolve(output)), path.resolve(outputFile))}`
584
-
585
- const imagePromise = limiter(async (browser, outputFormat) => {
586
- const { title, desc, data } = await renderMermaid(browser, mermaidDefinition, outputFormat, parseMMDOptions)
587
- await fs.promises.writeFile(outputFile, data)
588
- info(` ✅ ${outputFileRelative}`)
589
-
590
- return {
591
- url: outputFileRelative,
592
- title,
593
- alt: desc
594
- }
595
- }, browser, outputFormat)
596
- 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);
597
833
  }
598
834
 
599
835
  if (imagePromises.length) {
600
- info(`Found ${imagePromises.length} mermaid charts in Markdown input`)
836
+ info(`Found ${imagePromises.length} mermaid charts in Markdown input`);
601
837
  } else {
602
- info('No mermaid charts found in Markdown input')
838
+ info("No mermaid charts found in Markdown input");
603
839
  }
604
840
 
605
- const images = await Promise.all(imagePromises)
841
+ const images = await Promise.all(imagePromises);
606
842
 
607
843
  if (/\.(md|markdown)$/.test(output)) {
608
- const outDefinition = definition.replace(mermaidChartsInMarkdownRegexGlobal, (_mermaidMd) => {
609
- // pop first image from front of array
610
- const { url, title, alt } =
611
- /**
612
- * @type {MarkdownImageProps} We use the same regex,
613
- * so we will never try to get too many objects from the array.
614
- * (aka `images.shift()` will never return `undefined`)
615
- */ (images.shift())
616
- return markdownImage({ url, title, alt: alt || 'diagram' })
617
- })
618
- await fs.promises.writeFile(output, outDefinition, 'utf-8')
619
- 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}`);
620
859
  }
621
860
  } else {
622
- info('Generating single mermaid chart')
623
- browser ??= await puppeteer.launch(puppeteerConfig)
624
- const { data } = await renderMermaid(browser, definition, outputFormat, parseMMDOptions)
625
- if (output === '/dev/stdout') {
626
- await promisify(process.stdout.write).call(process.stdout, 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);
627
871
  } else {
628
- await fs.promises.writeFile(output, data)
872
+ await fs.promises.writeFile(output, data);
629
873
  }
630
874
  }
631
875
  } finally {
632
876
  // Don't close the browser if it was passed in by the user
633
877
  if (browser !== userPassedBrowser) {
634
- await browser?.close?.()
878
+ await browser?.close?.();
635
879
  }
636
880
  }
637
881
  }
638
882
 
639
- export { run, renderMermaid, cli, error }
883
+ export { run, renderMermaid, cli, error };