@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.bak DELETED
@@ -1,540 +0,0 @@
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
-
10
- // __dirname is not available in ESM modules by default
11
- const __dirname = url.fileURLToPath(new url.URL('.', import.meta.url))
12
-
13
- /**
14
- * Mermaid.js IFFE path.
15
- *
16
- * Importing this in a browser adds a global `mermaid` object.
17
- */
18
- const mermaidIIFEPath = path.resolve(path.dirname(url.fileURLToPath(resolve('mermaid', import.meta.url))), 'mermaid.js')
19
- const zenumlIIFEPath = path.resolve(path.dirname(url.fileURLToPath(resolve('@mermaid-js/mermaid-zenuml', import.meta.url))), 'mermaid-zenuml.js')
20
-
21
- /**
22
- * Prints an error to stderr, then closes with exit code 1
23
- *
24
- * @param {string} message - The message to print to `stderr`.
25
- * @returns {never} Quits Node.JS, so never returns.
26
- */
27
- const error = message => {
28
- console.error(chalk.red(`\n${message}\n`))
29
- process.exit(1)
30
- }
31
-
32
- /**
33
- * Prints a warning to stderr.
34
- *
35
- * @param {string} message - The message to print to `stderr`.
36
- */
37
- const warn = message => {
38
- console.warn(chalk.yellow(`\n${message}\n`))
39
- }
40
-
41
- /**
42
- * Checks if the given file exists.
43
- *
44
- * @param {string} file - The file to check.
45
- * @returns {never | void} If the file doesn't exist, closes Node.JS with
46
- * exit code 1.
47
- */
48
- const checkConfigFile = file => {
49
- if (!fs.existsSync(file)) {
50
- error(`Configuration file "${file}" doesn't exist`)
51
- }
52
- }
53
-
54
- /**
55
- * Gets the data in the given file.
56
- *
57
- * @param {string | undefined} inputFile - The file to read.
58
- * If `undefined`, reads from `stdin` instead.
59
- * @returns {Promise<string>} The contents of `inputFile` parsed as `utf8`.
60
- */
61
- async function getInputData (inputFile) {
62
- // if an input file has been specified using '-i', it takes precedence over
63
- // piping from stdin
64
- if (typeof inputFile !== 'undefined') {
65
- return await fs.promises.readFile(inputFile, 'utf-8')
66
- }
67
-
68
- return await new Promise((resolve, reject) => {
69
- let data = ''
70
- process.stdin.on('readable', function () {
71
- const chunk = process.stdin.read()
72
-
73
- if (chunk !== null) {
74
- data += chunk
75
- }
76
- })
77
-
78
- process.stdin.on('error', function (err) {
79
- reject(err)
80
- })
81
-
82
- process.stdin.on('end', function () {
83
- resolve(data)
84
- })
85
- })
86
- }
87
-
88
- /**
89
- * Commander parser that converts a string to an integer.
90
- *
91
- * @param {string} value - The value from commander.
92
- * @param {*} _unused - Unused.
93
- * @returns {number} The value parsed as a number.
94
- * @throws {InvalidArgumentError} If the arg is not valid.
95
- * @see https://github.com/tj/commander.js/wiki/Class:-Option#argparserfn
96
- */
97
- function parseCommanderInt (value, _unused) {
98
- const parsedValue = parseInt(value, 10)
99
- if (isNaN(parsedValue) || parsedValue < 1) {
100
- throw new InvalidArgumentError('Not a positive integer.')
101
- }
102
- return parsedValue
103
- }
104
-
105
- async function cli () {
106
- const commander = new Command()
107
- commander
108
- .version(version)
109
- .addOption(new Option('-t, --theme [theme]', 'Theme of the chart').choices(['default', 'forest', 'dark', 'neutral']).default('default'))
110
- .addOption(new Option('-w, --width [width]', 'Width of the page').argParser(parseCommanderInt).default(800))
111
- .addOption(new Option('-H, --height [height]', 'Height of the page').argParser(parseCommanderInt).default(600))
112
- .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.')
113
- .option('-o, --output [output]', 'Output file. It should be either md, svg, png, pdf or use `-` to output to stdout. Optional. Default: input + ".svg"')
114
- .option('-a, --artefacts [artefacts]', 'Output artefacts path. Only used with Markdown input file. Optional. Default: output directory')
115
- .addOption(new Option('-e, --outputFormat [format]', 'Output format for the generated image.').choices(['svg', 'png', 'pdf']).default(null, 'Loaded from the output file extension'))
116
- .addOption(new Option('-b, --backgroundColor [backgroundColor]', 'Background color for pngs/svgs (not pdfs). Example: transparent, red, \'#F0F0F0\'.').default('white'))
117
- .option('-c, --configFile [configFile]', 'JSON configuration file for mermaid.')
118
- .option('-C, --cssFile [cssFile]', 'CSS file for the page.')
119
- .option('-I, --svgId [svgId]', 'The id attribute for the SVG element to be rendered.')
120
- .addOption(new Option('-s, --scale [scale]', 'Puppeteer scale factor').argParser(parseCommanderInt).default(1))
121
- .option('-f, --pdfFit', 'Scale PDF to fit chart')
122
- .option('-q, --quiet', 'Suppress log output')
123
- .option('-p --puppeteerConfigFile [puppeteerConfigFile]', 'JSON configuration file for puppeteer.')
124
- .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.', [])
125
- .parse(process.argv)
126
-
127
- const options = commander.opts()
128
-
129
- let { theme, width, height, input, output, outputFormat, backgroundColor, configFile, cssFile, svgId, puppeteerConfigFile, scale, pdfFit, quiet, iconPacks, artefacts } = options
130
-
131
- // check input file
132
- if (!input) {
133
- warn('No input file specified, reading from stdin. ' +
134
- 'If you want to specify an input file, please use `-i <input>.` ' +
135
- 'You can use `-i -` to read from stdin and to suppress this warning.'
136
- )
137
- } else if (input === '-') {
138
- // `--input -` means read from stdin, but suppress the above warning
139
- input = undefined
140
- } else if (!fs.existsSync(input)) {
141
- error(`Input file "${input}" doesn't exist`)
142
- }
143
-
144
- // check output file
145
- if (!output) {
146
- // if an input file is defined, it should take precedence, otherwise, input is
147
- // coming from stdin and just name the file out.svg, if it hasn't been
148
- // specified with the '-o' option
149
- if (outputFormat) {
150
- output = input ? (`${input}.${outputFormat}`) : `out.${outputFormat}`
151
- } else {
152
- output = input ? (`${input}.svg`) : 'out.svg'
153
- }
154
- } else if (output === '-') {
155
- // `--output -` means write to stdout.
156
- output = '/dev/stdout'
157
- quiet = true
158
-
159
- if (!outputFormat) {
160
- outputFormat = 'svg'
161
- warn('No output format specified, using svg. ' +
162
- 'If you want to specify an output format and suppress this warning, ' +
163
- 'please use `-e <format>.` '
164
- )
165
- }
166
- } else if (!/\.(?:svg|png|pdf|md|markdown)$/.test(output)) {
167
- error('Output file must end with ".md"/".markdown", ".svg", ".png" or ".pdf"')
168
- }
169
-
170
- if (artefacts) {
171
- if (!input || !/\.(?:md|markdown)$/.test(input)) {
172
- error('Artefacts [-a|--artefacts] path can only be used with Markdown input file')
173
- }
174
- if (!fs.existsSync(artefacts)) {
175
- fs.mkdirSync(artefacts, { recursive: true })
176
- }
177
- }
178
-
179
- const outputDir = path.dirname(output)
180
- if (output !== '/dev/stdout' && !fs.existsSync(outputDir)) {
181
- error(`Output directory "${outputDir}/" doesn't exist`)
182
- }
183
-
184
- // check config files
185
- let mermaidConfig = { theme }
186
- if (configFile) {
187
- checkConfigFile(configFile)
188
- mermaidConfig = Object.assign(mermaidConfig, JSON.parse(fs.readFileSync(configFile, 'utf-8')))
189
- }
190
-
191
- let puppeteerConfig = /** @type {import('puppeteer').PuppeteerLaunchOptions} */ ({
192
- /*
193
- * `headless: 'shell'` is not officially supported in Puppeteer v19, v20, v21,
194
- * but still works. In Puppeteer v22, it uses the `chrome-headless-shell` package,
195
- * which is much faster than the regular headless mode.
196
- */
197
- headless: 'shell'
198
- })
199
- if (puppeteerConfigFile) {
200
- checkConfigFile(puppeteerConfigFile)
201
- puppeteerConfig = Object.assign(puppeteerConfig, JSON.parse(fs.readFileSync(puppeteerConfigFile, 'utf-8')))
202
- }
203
-
204
- // check cssFile
205
- let myCSS
206
- if (cssFile) {
207
- if (!fs.existsSync(cssFile)) {
208
- error(`CSS file "${cssFile}" doesn't exist`)
209
- }
210
- myCSS = fs.readFileSync(cssFile, 'utf-8')
211
- }
212
-
213
- await run(
214
- input, output, {
215
- puppeteerConfig,
216
- quiet,
217
- outputFormat,
218
- parseMMDOptions: {
219
- mermaidConfig, backgroundColor, myCSS, pdfFit, viewport: { width, height, deviceScaleFactor: scale }, svgId, iconPacks
220
- },
221
- artefacts
222
- }
223
- )
224
- }
225
-
226
- /**
227
- * @typedef {Object} ParseMDDOptions Options to pass to {@link parseMMD}
228
- * @property {import("puppeteer").Viewport} [viewport] - Puppeteer viewport (e.g. `width`, `height`, `deviceScaleFactor`)
229
- * @property {string | "transparent"} [backgroundColor] - Background color.
230
- * @property {Parameters<import("mermaid")["default"]["initialize"]>[0]} [mermaidConfig] - Mermaid config.
231
- * @property {CSSStyleDeclaration["cssText"]} [myCSS] - Optional CSS text.
232
- * @property {boolean} [pdfFit] - If set, scale PDF to fit chart.
233
- * @property {string} [svgId] - The id attribute for the SVG element to be rendered.
234
- * @property {string[]} [iconPacks] - Icon packages to use.
235
- */
236
-
237
- /**
238
- * Render a mermaid diagram.
239
- *
240
- * @param {import("puppeteer").Browser | import("puppeteer").BrowserContext} browser - Puppeteer Browser
241
- * @param {string} definition - Mermaid diagram definition
242
- * @param {"svg" | "png" | "pdf"} outputFormat - Mermaid output format.
243
- * @param {ParseMDDOptions} [opt] - Options, see {@link ParseMDDOptions} for details.
244
- * @returns {Promise<{title: string | null, desc: string | null, data: Uint8Array}>} The output file in bytes,
245
- * with optional metadata.
246
- */
247
- async function renderMermaid (browser, definition, outputFormat, { viewport, backgroundColor = 'white', mermaidConfig = {}, myCSS, pdfFit, svgId, iconPacks = [] } = {}) {
248
- const page = await browser.newPage()
249
- page.on('console', (msg) => {
250
- console.warn(msg.text())
251
- })
252
- try {
253
- if (viewport) {
254
- await page.setViewport(viewport)
255
- }
256
- const mermaidHTMLPath = path.join(__dirname, '..', 'dist', 'index.html')
257
- await page.goto(url.pathToFileURL(mermaidHTMLPath).href)
258
- await page.$eval('body', (body, backgroundColor) => {
259
- body.style.background = backgroundColor
260
- }, backgroundColor)
261
- await Promise.all([
262
- page.addScriptTag({ path: mermaidIIFEPath }),
263
- page.addScriptTag({ path: zenumlIIFEPath })
264
- ])
265
- const metadata = await page.$eval('#container', async (container, definition, mermaidConfig, myCSS, backgroundColor, svgId, iconPacks) => {
266
- await Promise.all(Array.from(document.fonts, (font) => font.load()))
267
-
268
- /**
269
- * @typedef {Object} GlobalThisWithMermaid
270
- * We've already imported these modules in our `index.html` file (or by running `page.addScriptTag`),
271
- * so that they get correctly bundled.
272
- * @property {import("mermaid")["default"]} mermaid Already imported mermaid instance
273
- * @property {import("@mermaid-js/mermaid-zenuml")["default"]} mermaid-zenuml Already imported mermaid-zenuml instance
274
- * @property {import("@mermaid-js/layout-elk")["default"]} elkLayouts Already imported mermaid-elkLayouts instance
275
- */
276
- const { mermaid, 'mermaid-zenuml': zenuml, elkLayouts } = /** @type {GlobalThisWithMermaid & typeof globalThis} */ (globalThis)
277
-
278
- await mermaid.registerExternalDiagrams([zenuml])
279
- mermaid.registerLayoutLoaders(elkLayouts)
280
- // lazy load icon packs
281
- mermaid.registerIconPacks(
282
- iconPacks.map((icon) => ({
283
- name: icon.split('/')[1],
284
- loader: () =>
285
- fetch(`https://unpkg.com/${icon}/icons.json`)
286
- .then((res) => res.json())
287
- .catch(() => error(`Failed to fetch icon: ${icon}`))
288
- }))
289
- )
290
- mermaid.initialize({ startOnLoad: false, ...mermaidConfig })
291
- // should throw an error if mmd diagram is invalid
292
- const { svg: svgText } = await mermaid.render(svgId || 'my-svg', definition, container)
293
- container.innerHTML = svgText
294
-
295
- const svg = container.getElementsByTagName?.('svg')?.[0]
296
- if (svg?.style) {
297
- svg.style.backgroundColor = backgroundColor
298
- } else {
299
- warn('svg not found. Not applying background color.')
300
- }
301
- if (myCSS) {
302
- // add CSS as a <svg>...<style>... element
303
- // see https://developer.mozilla.org/en-US/docs/Web/API/SVGStyleElement
304
- const style = document.createElementNS('http://www.w3.org/2000/svg', 'style')
305
- style.appendChild(document.createTextNode(myCSS))
306
- svg.appendChild(style)
307
- }
308
-
309
- // Finds SVG metadata for accessibility purposes
310
- /** SVG title */
311
- let title = null
312
- // If <title> exists, it must be the first child Node,
313
- // see https://www.w3.org/TR/SVG11/struct.html#DescriptionAndTitleElements
314
- /* global SVGTitleElement, SVGDescElement */ // These exist in browser-based code
315
- if (svg.firstChild instanceof SVGTitleElement) {
316
- title = svg.firstChild.textContent
317
- }
318
- /** SVG description. According to SVG spec, we should use the first one we find */
319
- let desc = null
320
- for (const svgNode of svg.children) {
321
- if (svgNode instanceof SVGDescElement) {
322
- desc = svgNode.textContent
323
- }
324
- }
325
- return {
326
- title, desc
327
- }
328
- }, definition, mermaidConfig, myCSS, backgroundColor, svgId, iconPacks)
329
-
330
- if (outputFormat === 'svg') {
331
- const svgXML = await page.$eval('svg', (svg) => {
332
- // SVG might have HTML <foreignObject> that are not valid XML
333
- // E.g. <br> must be replaced with <br/>
334
- // Luckily the DOM Web API has the XMLSerializer for this
335
- // eslint-disable-next-line no-undef
336
- const xmlSerializer = new XMLSerializer()
337
- return xmlSerializer.serializeToString(svg)
338
- })
339
- return {
340
- ...metadata,
341
- data: new TextEncoder().encode(svgXML)
342
- }
343
- } else if (outputFormat === 'png') {
344
- const clip = await page.$eval('svg', svg => {
345
- const react = svg.getBoundingClientRect()
346
- return { x: Math.floor(react.left), y: Math.floor(react.top), width: Math.ceil(react.width), height: Math.ceil(react.height) }
347
- })
348
- await page.setViewport({ ...viewport, width: clip.x + clip.width, height: clip.y + clip.height })
349
- return {
350
- ...metadata,
351
- data: await page.screenshot({ clip, omitBackground: backgroundColor === 'transparent' })
352
- }
353
- } else { // pdf
354
- if (pdfFit) {
355
- const clip = await page.$eval('svg', svg => {
356
- const react = svg.getBoundingClientRect()
357
- return { x: react.left, y: react.top, width: react.width, height: react.height }
358
- })
359
- return {
360
- ...metadata,
361
- data: await page.pdf({
362
- omitBackground: backgroundColor === 'transparent',
363
- width: (Math.ceil(clip.width) + clip.x * 2) + 'px',
364
- height: (Math.ceil(clip.height) + clip.y * 2) + 'px',
365
- pageRanges: '1-1'
366
- })
367
- }
368
- } else {
369
- return {
370
- ...metadata,
371
- data: await page.pdf({
372
- omitBackground: backgroundColor === 'transparent'
373
- })
374
- }
375
- }
376
- }
377
- } finally {
378
- await page.close()
379
- }
380
- }
381
-
382
- /**
383
- * @typedef {object} MarkdownImageProps Markdown image properties
384
- * Used to create a markdown image that looks like `![alt](url "title")`
385
- * @property {string} url - Path to image.
386
- * @property {string} alt - Image alt text, required.
387
- * @property {string | null} [title] - Optional image title text.
388
- */
389
-
390
- /**
391
- * Creates a markdown image syntax.
392
- *
393
- * @param {MarkdownImageProps} params - Parameters.
394
- * @returns {`![${string}](${string})`} The markdown image text.
395
- */
396
- function markdownImage ({ url, title, alt }) {
397
- // we can't use String.prototype.replaceAll since it's not supported in Node v14
398
- const altEscaped = alt.replace(/[[\]\\]/g, '\\$&')
399
- if (title) {
400
- const titleEscaped = title.replace(/["\\]/g, '\\$&')
401
- return `![${altEscaped}](${url} "${titleEscaped}")`
402
- } else {
403
- return `![${altEscaped}](${url})`
404
- }
405
- }
406
-
407
- /**
408
- * Renders a mermaid diagram or mermaid markdown file.
409
- *
410
- * @param {`${string}.${"md" | "markdown"}` | string | undefined} input - If this ends with `.md`/`.markdown`,
411
- * path to a markdown file containing mermaid.
412
- * If this is a string, loads the mermaid definition from the given file.
413
- * If this is `undefined`, loads the mermaid definition from stdin.
414
- * @param {`${string}.${"md" | "markdown" | "svg" | "png" | "pdf"}` | "/dev/stdout"} output - Path to the output file.
415
- * @param {Object} [opts] - Options
416
- * @param {import("puppeteer").LaunchOptions} [opts.puppeteerConfig] - Puppeteer launch options.
417
- * @param {boolean} [opts.quiet] - If set, suppress log output.
418
- * @param {"svg" | "png" | "pdf"} [opts.outputFormat] - Mermaid output format.
419
- * @param {string} [opts.artefacts] - Path to the artefacts directory.
420
- * Defaults to `output` extension. Overrides `output` extension if set.
421
- * @param {ParseMDDOptions} [opts.parseMMDOptions] - Options to pass to {@link parseMMDOptions}.
422
- */
423
- async function run (input, output, { puppeteerConfig = {}, quiet = false, outputFormat, parseMMDOptions, artefacts } = {}) {
424
- /**
425
- * Logs the given message to stdout, unless `quiet` is set to `true`.
426
- *
427
- * @param {string} message - The message to maybe log.
428
- */
429
- const info = message => {
430
- if (!quiet) {
431
- console.info(message)
432
- }
433
- }
434
-
435
- // TODO: should we use a Markdown parser like remark instead of rolling our own parser?
436
- const mermaidChartsInMarkdown = /^[^\S\n]*[`:]{3}(?:mermaid)([^\S\n]*\r?\n([\s\S]*?))[`:]{3}[^\S\n]*$/
437
- const mermaidChartsInMarkdownRegexGlobal = new RegExp(mermaidChartsInMarkdown, 'gm')
438
- /**
439
- * @type {puppeteer.Browser | undefined}
440
- * Lazy-loaded browser instance, only created when needed.
441
- */
442
- let browser
443
- try {
444
- if (!outputFormat) {
445
- const outputFormatFromFilename =
446
- /**
447
- * @type {"md" | "markdown" | "svg" | "png" | "pdf"}
448
- */ (path.extname(output).replace('.', ''))
449
- if (outputFormatFromFilename === 'md' || outputFormatFromFilename === 'markdown') {
450
- // fallback to svg in case no outputFormat is given and output file is MD
451
- outputFormat = 'svg'
452
- } else {
453
- outputFormat = outputFormatFromFilename
454
- }
455
- }
456
- if (!/(?:svg|png|pdf)$/.test(outputFormat)) {
457
- throw new Error('Output format must be one of "svg", "png" or "pdf"')
458
- }
459
-
460
- const definition = await getInputData(input)
461
- if (input && /\.(md|markdown)$/.test(input)) {
462
- if (output === '/dev/stdout') {
463
- throw new Error('Cannot use `stdout` with markdown input')
464
- }
465
-
466
- const imagePromises = []
467
- for (const mermaidCodeblockMatch of definition.matchAll(mermaidChartsInMarkdownRegexGlobal)) {
468
- if (browser === undefined) {
469
- browser = await puppeteer.launch(puppeteerConfig)
470
- }
471
- const mermaidDefinition = mermaidCodeblockMatch[2]
472
-
473
- /** Output can be either a template image file, or a `.md` output file.
474
- * If it is a template image file, use that to created numbered diagrams
475
- * I.e. if "out.png", use "out-1.png", "out-2.png", etc
476
- * If it is an output `.md` file, use that to base .svg numbered diagrams on
477
- * I.e. if "out.md". use "out-1.svg", "out-2.svg", etc
478
- * @type {string}
479
- */
480
- let outputFile = output.replace(
481
- /(\.(md|markdown|png|svg|pdf))$/,
482
- `-${imagePromises.length + 1}$1`
483
- ).replace(/\.(md|markdown)$/, `.${outputFormat}`)
484
-
485
- if (artefacts) {
486
- outputFile = path.resolve(artefacts, path.basename(outputFile))
487
- }
488
-
489
- const outputFileRelative = `./${path.relative(path.dirname(path.resolve(output)), path.resolve(outputFile))}`
490
-
491
- const imagePromise = (async () => {
492
- const { title, desc, data } = await renderMermaid(browser, mermaidDefinition, outputFormat, parseMMDOptions)
493
- await fs.promises.writeFile(outputFile, data)
494
- info(` ✅ ${outputFileRelative}`)
495
-
496
- return {
497
- url: outputFileRelative,
498
- title,
499
- alt: desc
500
- }
501
- })()
502
- imagePromises.push(imagePromise)
503
- }
504
-
505
- if (imagePromises.length) {
506
- info(`Found ${imagePromises.length} mermaid charts in Markdown input`)
507
- } else {
508
- info('No mermaid charts found in Markdown input')
509
- }
510
-
511
- const images = await Promise.all(imagePromises)
512
-
513
- if (/\.(md|markdown)$/.test(output)) {
514
- const outDefinition = definition.replace(mermaidChartsInMarkdownRegexGlobal, (_mermaidMd) => {
515
- // pop first image from front of array
516
- const { url, title, alt } =
517
- /**
518
- * @type {MarkdownImageProps} We use the same regex,
519
- * so we will never try to get too many objects from the array.
520
- * (aka `images.shift()` will never return `undefined`)
521
- */ (images.shift())
522
- return markdownImage({ url, title, alt: alt || 'diagram' })
523
- })
524
- await fs.promises.writeFile(output, outDefinition, 'utf-8')
525
- info(` ✅ ${output}`)
526
- }
527
- } else {
528
- info('Generating single mermaid chart')
529
- browser = await puppeteer.launch(puppeteerConfig)
530
- const { data } = await renderMermaid(browser, definition, outputFormat, parseMMDOptions)
531
- await output !== '/dev/stdout'
532
- ? fs.promises.writeFile(output, data)
533
- : process.stdout.write(data)
534
- }
535
- } finally {
536
- await browser?.close?.()
537
- }
538
- }
539
-
540
- export { run, renderMermaid, cli, error }