@mermaid-js/mermaid-cli 9.1.5 → 9.1.7

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/package.json CHANGED
@@ -1,42 +1,52 @@
1
1
  {
2
2
  "name": "@mermaid-js/mermaid-cli",
3
- "version": "9.1.5",
3
+ "version": "9.1.7",
4
4
  "description": "Command-line interface for mermaid",
5
5
  "license": "MIT",
6
6
  "repository": "git@github.com:mermaid-js/mermaid-cli.git",
7
+ "type": "module",
7
8
  "author": "Tyler Long <tyler4long@gmail.com>",
8
9
  "bin": {
9
- "mmdc": "./index.bundle.js"
10
+ "mmdc": "./src/cli.js"
10
11
  },
12
+ "engines": {
13
+ "node": ">=14.1.0"
14
+ },
15
+ "exports": "./src/index.js",
11
16
  "scripts": {
12
- "upgrade": "yarn-upgrade-all && source copy_modules.sh",
13
- "prepublishOnly": "babel ./src/index.js --out-file ./index.bundle.js",
14
- "test": "jest"
17
+ "upgrade": "yarn-upgrade-all && sh copy_modules.sh",
18
+ "prepare": "sh copy_modules.sh",
19
+ "prepack": "sh copy_modules.sh",
20
+ "test": "standard && yarn node --experimental-vm-modules $(yarn bin jest)",
21
+ "lint": "standard",
22
+ "lint-fix": "standard --fix"
15
23
  },
16
24
  "dependencies": {
17
- "chalk": "^4.1.0",
25
+ "chalk": "^5.0.1",
18
26
  "commander": "^9.0.0",
19
- "puppeteer": "^16.1.0"
27
+ "puppeteer": "^18.0.5"
20
28
  },
21
29
  "devDependencies": {
22
- "@babel/cli": "^7.0.0",
23
- "@babel/core": "^7.0.0",
24
- "@babel/preset-env": "^7.0.0",
25
30
  "@fortawesome/fontawesome-free-webfonts": "^1.0.9",
26
31
  "mermaid": "^9.1.2",
27
- "jest": "^28.1.2",
32
+ "jest": "^29.0.1",
28
33
  "standard": "^17.0.0",
29
34
  "yarn-upgrade-all": "^0.7.0"
30
35
  },
31
36
  "files": [
32
- "index.bundle.js",
37
+ "src/",
33
38
  "mermaid.min.js",
34
39
  "index.html",
35
40
  "fontawesome/*"
36
41
  ],
42
+ "jest": {
43
+ "moduleNameMapper": {
44
+ "#(.*)": "<rootDir>/node_modules/$1"
45
+ }
46
+ },
37
47
  "standard": {
38
48
  "ignore": [
39
- "index.bundle.js"
49
+ "mermaid.min.js"
40
50
  ]
41
51
  }
42
52
  }
package/src/cli.js ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { cli, error } from './index.js'
4
+
5
+ process.title = 'mmdc'
6
+ cli().catch((exception) => error(exception instanceof Error ? exception.stack : exception))
package/src/index.js ADDED
@@ -0,0 +1,411 @@
1
+ import { Command } from 'commander'
2
+ import chalk from 'chalk'
3
+ import fs from 'fs'
4
+ import path from 'path'
5
+ import puppeteer from 'puppeteer'
6
+ import url from 'url'
7
+
8
+ // importing JSON is still experimental in Node.JS https://nodejs.org/docs/latest-v16.x/api/esm.html#json-modules
9
+ import { createRequire } from 'module'
10
+ const require = createRequire(import.meta.url)
11
+ const pkg = require('../package.json')
12
+ // __dirname is not available in ESM modules by default
13
+ const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
14
+
15
+ const error = message => {
16
+ console.error(chalk.red(`\n${message}\n`))
17
+ process.exit(1)
18
+ }
19
+
20
+ const warn = message => {
21
+ console.log(chalk.yellow(`\n${message}\n`))
22
+ }
23
+
24
+ const checkConfigFile = file => {
25
+ if (!fs.existsSync(file)) {
26
+ error(`Configuration file "${file}" doesn't exist`)
27
+ }
28
+ }
29
+
30
+ const inputPipedFromStdin = () => fs.fstatSync(0).isFIFO()
31
+
32
+ const getInputData = async inputFile => new Promise((resolve, reject) => {
33
+ // if an input file has been specified using '-i', it takes precedence over
34
+ // piping from stdin
35
+ if (typeof inputFile !== 'undefined') {
36
+ return fs.readFile(inputFile, 'utf-8', (err, data) => {
37
+ if (err) {
38
+ return reject(err)
39
+ }
40
+
41
+ return resolve(data)
42
+ })
43
+ }
44
+
45
+ let data = ''
46
+ process.stdin.on('readable', function () {
47
+ const chunk = this.read()
48
+
49
+ if (chunk !== null) {
50
+ data += chunk
51
+ }
52
+ })
53
+
54
+ process.stdin.on('error', function (err) {
55
+ reject(err)
56
+ })
57
+
58
+ process.stdin.on('end', function () {
59
+ resolve(data)
60
+ })
61
+ })
62
+
63
+ async function cli () {
64
+ const commander = new Command()
65
+ commander
66
+ .version(pkg.version)
67
+ .option('-t, --theme [theme]', 'Theme of the chart, could be default, forest, dark or neutral. Optional. Default: default', /^default|forest|dark|neutral$/, 'default')
68
+ .option('-w, --width [width]', 'Width of the page. Optional. Default: 800', /^\d+$/, '800')
69
+ .option('-H, --height [height]', 'Height of the page. Optional. Default: 600', /^\d+$/, '600')
70
+ .option('-i, --input <input>', 'Input mermaid file. Files ending in .md will be treated as Markdown and all charts (e.g. ```mermaid (...)```) will be extracted and generated. Required.')
71
+ .option('-o, --output [output]', 'Output file. It should be either md, svg, png or pdf. Optional. Default: input + ".svg"')
72
+ .option('-e, --outputFormat <format>', 'Output format for the generated image. It should be either svg, png or pdf. Optional. Default: output file extension')
73
+ .option('-b, --backgroundColor [backgroundColor]', 'Background color for pngs/svgs (not pdfs). Example: transparent, red, \'#F0F0F0\'. Optional. Default: white')
74
+ .option('-c, --configFile [configFile]', 'JSON configuration file for mermaid. Optional')
75
+ .option('-C, --cssFile [cssFile]', 'CSS file for the page. Optional')
76
+ .option('-s, --scale [scale]', 'Puppeteer scale factor, default 1. Optional')
77
+ .option('-f, --pdfFit [pdfFit]', 'Scale PDF to fit chart')
78
+ .option('-q, --quiet', 'Suppress log output')
79
+ .option('-p --puppeteerConfigFile [puppeteerConfigFile]', 'JSON configuration file for puppeteer. Optional')
80
+ .parse(process.argv)
81
+
82
+ const options = commander.opts()
83
+
84
+ let { theme, width, height, input, output, outputFormat, backgroundColor, configFile, cssFile, puppeteerConfigFile, scale, pdfFit, quiet } = options
85
+
86
+ // check input file
87
+ if (!(input || inputPipedFromStdin())) {
88
+ console.error(chalk.red('\nPlease specify input file: -i <input>\n'))
89
+ // Log to stderr, and return with error exitCode
90
+ commander.help({ error: true })
91
+ }
92
+ if (input && !fs.existsSync(input)) {
93
+ error(`Input file "${input}" doesn't exist`)
94
+ }
95
+
96
+ // check output file
97
+ if (!output) {
98
+ // if an input file is defined, it should take precedence, otherwise, input is
99
+ // coming from stdin and just name the file out.svg, if it hasn't been
100
+ // specified with the '-o' option
101
+ output = input ? (input + '.svg') : 'out.svg'
102
+ }
103
+ if (!/\.(?:svg|png|pdf|md)$/.test(output)) {
104
+ error('Output file must end with ".md", ".svg", ".png" or ".pdf"')
105
+ }
106
+ const outputDir = path.dirname(output)
107
+ if (!fs.existsSync(outputDir)) {
108
+ error(`Output directory "${outputDir}/" doesn't exist`)
109
+ }
110
+
111
+ // check config files
112
+ let mermaidConfig = { theme }
113
+ if (configFile) {
114
+ checkConfigFile(configFile)
115
+ mermaidConfig = Object.assign(mermaidConfig, JSON.parse(fs.readFileSync(configFile, 'utf-8')))
116
+ }
117
+ let puppeteerConfig = {}
118
+ if (puppeteerConfigFile) {
119
+ checkConfigFile(puppeteerConfigFile)
120
+ puppeteerConfig = JSON.parse(fs.readFileSync(puppeteerConfigFile, 'utf-8'))
121
+ }
122
+
123
+ // check cssFile
124
+ let myCSS
125
+ if (cssFile) {
126
+ if (!fs.existsSync(cssFile)) {
127
+ error(`CSS file "${cssFile}" doesn't exist`)
128
+ }
129
+ myCSS = fs.readFileSync(cssFile, 'utf-8')
130
+ }
131
+
132
+ // normalize args
133
+ width = parseInt(width)
134
+ height = parseInt(height)
135
+ backgroundColor = backgroundColor || 'white'
136
+ const deviceScaleFactor = parseInt(scale || 1, 10)
137
+
138
+ await run(
139
+ input, output, {
140
+ puppeteerConfig,
141
+ quiet,
142
+ outputFormat,
143
+ parseMMDOptions: {
144
+ mermaidConfig, backgroundColor, myCSS, pdfFit, viewport: { width, height, deviceScaleFactor }
145
+ }
146
+ }
147
+ )
148
+ }
149
+
150
+ /**
151
+ * @typedef {Object} ParseMDDOptions Options to pass to {@link parseMMD}
152
+ * @property {puppeteer.Viewport} [viewport] - Puppeteer viewport (e.g. `width`, `height`, `deviceScaleFactor`)
153
+ * @property {string | "transparent"} [backgroundColor] - Background color.
154
+ * @property {Parameters<import("mermaid").Mermaid["initialize"]>[0]} [mermaidConfig] - Mermaid config.
155
+ * @property {CSSStyleDeclaration["cssText"]} [myCSS] - Optional CSS text.
156
+ * @property {boolean} pdfFit - If set, scale PDF to fit chart.
157
+ */
158
+
159
+ /**
160
+ * Parse and render a mermaid diagram.
161
+ *
162
+ * @deprecated Prefer {@link renderMermaid}, as it also returns useful metadata.
163
+ *
164
+ * @param {puppeteer.Browser} browser - Puppeteer Browser
165
+ * @param {string} definition - Mermaid diagram definition
166
+ * @param {"svg" | "png" | "pdf"} outputFormat - Mermaid output format.
167
+ * @param {ParseMDDOptions} [opt] - Options, see {@link ParseMDDOptions} for details.
168
+ *
169
+ * @returns {Promise<Buffer>} The output file in bytes.
170
+ */
171
+ async function parseMMD (...args) {
172
+ const { data } = await renderMermaid(...args)
173
+ return data
174
+ }
175
+
176
+ /**
177
+ * Render a mermaid diagram.
178
+ *
179
+ * @param {puppeteer.Browser} browser - Puppeteer Browser
180
+ * @param {string} definition - Mermaid diagram definition
181
+ * @param {"svg" | "png" | "pdf"} outputFormat - Mermaid output format.
182
+ * @param {ParseMDDOptions} [opt] - Options, see {@link ParseMDDOptions} for details.
183
+ * @returns {Promise<{title?: string, desc?: string, data: Buffer}>} The output file in bytes,
184
+ * with optional metadata.
185
+ */
186
+ async function renderMermaid (browser, definition, outputFormat, { viewport, backgroundColor = 'white', mermaidConfig = {}, myCSS, pdfFit } = {}) {
187
+ const page = await browser.newPage()
188
+ try {
189
+ if (viewport) {
190
+ await page.setViewport(viewport)
191
+ }
192
+ const mermaidHTMLPath = path.join(__dirname, '..', 'index.html')
193
+ await page.goto(url.pathToFileURL(mermaidHTMLPath))
194
+ await page.$eval('body', (body, backgroundColor) => {
195
+ body.style.background = backgroundColor
196
+ }, backgroundColor)
197
+ const metadata = await page.$eval('#container', (container, definition, mermaidConfig, myCSS, backgroundColor) => {
198
+ container.textContent = definition
199
+ window.mermaid.initialize(mermaidConfig)
200
+ // should throw an error if mmd diagram is invalid
201
+ try {
202
+ window.mermaid.initThrowsErrors(undefined, container)
203
+ } catch (error) {
204
+ if (error instanceof Error) {
205
+ // mermaid-js doesn't currently throws JS Errors, but let's leave this
206
+ // here in case it does in the future
207
+ throw error
208
+ } else {
209
+ throw new Error(error?.message ?? 'Unknown mermaid render error')
210
+ }
211
+ }
212
+
213
+ const svg = container.getElementsByTagName?.('svg')?.[0]
214
+ if (svg?.style) {
215
+ svg.style.backgroundColor = backgroundColor
216
+ } else {
217
+ warn('svg not found. Not applying background color.')
218
+ }
219
+ if (myCSS) {
220
+ // add CSS as a <svg>...<style>... element
221
+ // see https://developer.mozilla.org/en-US/docs/Web/API/SVGStyleElement
222
+ const style = document.createElementNS('http://www.w3.org/2000/svg', 'style')
223
+ style.appendChild(document.createTextNode(myCSS))
224
+ svg.appendChild(style)
225
+ }
226
+
227
+ // Finds SVG metadata for accessibility purposes
228
+ /** SVG title */
229
+ let title = null
230
+ // If <title> exists, it must be the first child Node,
231
+ // see https://www.w3.org/TR/SVG11/struct.html#DescriptionAndTitleElements
232
+ /* global SVGTitleElement, SVGDescElement */ // These exist in browser-based code
233
+ if (svg.firstChild instanceof SVGTitleElement) {
234
+ title = svg.firstChild.textContent
235
+ }
236
+ /** SVG description. According to SVG spec, we should use the first one we find */
237
+ let desc = null
238
+ for (const svgNode of svg.children) {
239
+ if (svgNode instanceof SVGDescElement) {
240
+ desc = svgNode.textContent
241
+ }
242
+ }
243
+ return {
244
+ title, desc
245
+ }
246
+ }, definition, mermaidConfig, myCSS, backgroundColor)
247
+
248
+ if (outputFormat === 'svg') {
249
+ const svgXML = await page.$eval('svg', (svg) => {
250
+ // SVG might have HTML <foreignObject> that are not valid XML
251
+ // E.g. <br> must be replaced with <br/>
252
+ // Luckily the DOM Web API has the XMLSerializer for this
253
+ // eslint-disable-next-line no-undef
254
+ const xmlSerializer = new XMLSerializer()
255
+ return xmlSerializer.serializeToString(svg)
256
+ })
257
+ return {
258
+ ...metadata,
259
+ data: Buffer.from(svgXML, 'utf8')
260
+ }
261
+ } else if (outputFormat === 'png') {
262
+ const clip = await page.$eval('svg', svg => {
263
+ const react = svg.getBoundingClientRect()
264
+ return { x: Math.floor(react.left), y: Math.floor(react.top), width: Math.ceil(react.width), height: Math.ceil(react.height) }
265
+ })
266
+ await page.setViewport({ ...viewport, width: clip.x + clip.width, height: clip.y + clip.height })
267
+ return {
268
+ ...metadata,
269
+ data: await page.screenshot({ clip, omitBackground: backgroundColor === 'transparent' })
270
+ }
271
+ } else { // pdf
272
+ if (pdfFit) {
273
+ const clip = await page.$eval('svg', svg => {
274
+ const react = svg.getBoundingClientRect()
275
+ return { x: react.left, y: react.top, width: react.width, height: react.height }
276
+ })
277
+ return {
278
+ ...metadata,
279
+ data: await page.pdf({
280
+ omitBackground: backgroundColor === 'transparent',
281
+ width: (Math.ceil(clip.width) + clip.x * 2) + 'px',
282
+ height: (Math.ceil(clip.height) + clip.y * 2) + 'px',
283
+ pageRanges: '1-1'
284
+ })
285
+ }
286
+ } else {
287
+ return {
288
+ ...metadata,
289
+ data: await page.pdf({
290
+ omitBackground: backgroundColor === 'transparent'
291
+ })
292
+ }
293
+ }
294
+ }
295
+ } finally {
296
+ await page.close()
297
+ }
298
+ }
299
+
300
+ /**
301
+ * Creates a markdown image syntax.
302
+ *
303
+ * @param {object} params - Parameters.
304
+ * @param {string} params.url - Path to image.
305
+ * @param {string} params.alt - Image alt text, required.
306
+ * @param {string} [params.title] - Image title text.
307
+ * @returns {`![${string}](${string})`} The markdown image text.
308
+ */
309
+ function markdownImage ({ url, title, alt }) {
310
+ // we can't use String.prototype.replaceAll since it's not supported in Node v14
311
+ const altEscaped = alt.replace(/[[\]\\]/g, '\\$&')
312
+ if (title) {
313
+ const titleEscaped = title.replace(/["\\]/g, '\\$&')
314
+ return `![${altEscaped}](${url} "${titleEscaped}")`
315
+ } else {
316
+ return `![${altEscaped}](${url})`
317
+ }
318
+ }
319
+
320
+ /**
321
+ * Renders a mermaid diagram or mermaid markdown file.
322
+ *
323
+ * @param {`${string}.md` | string} [input] - If this ends with `.md`, path to a markdown file containing mermaid.
324
+ * If this is a string, loads the mermaid definition from the given file.
325
+ * If this is `undefined`, loads the mermaid definition from stdin.
326
+ * @param {`${string}.${"md" | "svg" | "png" | "pdf"}`} output - Path to the output file.
327
+ * @param {Object} [opts] - Options
328
+ * @param {puppeteer.LaunchOptions} [opts.puppeteerConfig] - Puppeteer launch options.
329
+ * @param {boolean} [opts.quiet] - If set, suppress log output.
330
+ * @param {"svg" | "png" | "pdf"} [opts.outputFormat] - Mermaid output format.
331
+ * Defaults to `output` extension. Overrides `output` extension if set.
332
+ * @param {ParseMDDOptions} [opts.parseMMDOptions] - Options to pass to {@link parseMMDOptions}.
333
+ */
334
+ async function run (input, output, { puppeteerConfig = {}, quiet = false, outputFormat, parseMMDOptions } = {}) {
335
+ const info = message => {
336
+ if (!quiet) {
337
+ console.info(message)
338
+ }
339
+ }
340
+
341
+ const mermaidChartsInMarkdown = /^\s*```(?:mermaid)(\r?\n([\s\S]*?))```\s*$/
342
+ const mermaidChartsInMarkdownRegexGlobal = new RegExp(mermaidChartsInMarkdown, 'gm')
343
+ const browser = await puppeteer.launch(puppeteerConfig)
344
+ try {
345
+ if (!outputFormat) {
346
+ outputFormat = path.extname(output).replace('.', '')
347
+ }
348
+ if (outputFormat === 'md') {
349
+ // fallback to svg in case no outputFormat is given and output file is MD
350
+ outputFormat = 'svg'
351
+ }
352
+ if (!/(?:svg|png|pdf)$/.test(outputFormat)) {
353
+ throw new Error('Output format must be one of "svg", "png" or "pdf"')
354
+ }
355
+
356
+ const definition = await getInputData(input)
357
+ if (/\.md$/.test(input)) {
358
+ const imagePromises = []
359
+ for (const mermaidCodeblockMatch of definition.matchAll(mermaidChartsInMarkdownRegexGlobal)) {
360
+ const mermaidDefinition = mermaidCodeblockMatch[1]
361
+
362
+ // Output can be either a template image file, or a `.md` output file.
363
+ // If it is a template image file, use that to created numbered diagrams
364
+ // I.e. if "out.png", use "out-1.png", "out-2.png", etc
365
+ // If it is an output `.md` file, use that to base .svg numbered diagrams on
366
+ // I.e. if "out.md". use "out-1.svg", "out-2.svg", etc
367
+ const outputFile = output.replace(/(\.(md|png|svg|pdf))$/, `-${imagePromises.length + 1}$1`).replace(/(\.md)$/, `.${outputFormat}`)
368
+ const outputFileRelative = `./${path.relative(path.dirname(path.resolve(output)), path.resolve(outputFile))}`
369
+
370
+ const imagePromise = (async () => {
371
+ const { title, desc, data } = await renderMermaid(browser, mermaidDefinition, outputFormat, parseMMDOptions)
372
+ await fs.promises.writeFile(outputFile, data)
373
+ info(` ✅ ${outputFileRelative}`)
374
+
375
+ return {
376
+ url: outputFileRelative,
377
+ title,
378
+ alt: desc
379
+ }
380
+ })()
381
+ imagePromises.push(imagePromise)
382
+ }
383
+
384
+ if (imagePromises.length) {
385
+ info(`Found ${imagePromises.length} mermaid charts in Markdown input`)
386
+ } else {
387
+ info('No mermaid charts found in Markdown input')
388
+ }
389
+
390
+ const images = await Promise.all(imagePromises)
391
+
392
+ if (/\.md$/.test(output)) {
393
+ const outDefinition = definition.replace(mermaidChartsInMarkdownRegexGlobal, (_mermaidMd) => {
394
+ // pop first image from front of array
395
+ const { url, title, alt } = images.shift()
396
+ return markdownImage({ url, title, alt: alt || 'diagram' })
397
+ })
398
+ await fs.promises.writeFile(output, outDefinition, 'utf-8')
399
+ info(` ✅ ${output}`)
400
+ }
401
+ } else {
402
+ info('Generating single mermaid chart')
403
+ const data = await parseMMD(browser, definition, outputFormat, parseMMDOptions)
404
+ await fs.promises.writeFile(output, data)
405
+ }
406
+ } finally {
407
+ await browser.close()
408
+ }
409
+ }
410
+
411
+ export { run, renderMermaid, parseMMD, cli, error }