@mermaid-js/mermaid-cli 9.1.6 → 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/README.md +31 -0
- package/mermaid.min.js +1 -1
- package/package.json +3 -3
- package/src/index.js +187 -91
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mermaid-js/mermaid-cli",
|
|
3
|
-
"version": "9.1.
|
|
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",
|
|
@@ -24,12 +24,12 @@
|
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"chalk": "^5.0.1",
|
|
26
26
|
"commander": "^9.0.0",
|
|
27
|
-
"puppeteer": "^
|
|
27
|
+
"puppeteer": "^18.0.5"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@fortawesome/fontawesome-free-webfonts": "^1.0.9",
|
|
31
31
|
"mermaid": "^9.1.2",
|
|
32
|
-
"jest": "^
|
|
32
|
+
"jest": "^29.0.1",
|
|
33
33
|
"standard": "^17.0.0",
|
|
34
34
|
"yarn-upgrade-all": "^0.7.0"
|
|
35
35
|
},
|
package/src/index.js
CHANGED
|
@@ -60,13 +60,7 @@ const getInputData = async inputFile => new Promise((resolve, reject) => {
|
|
|
60
60
|
})
|
|
61
61
|
})
|
|
62
62
|
|
|
63
|
-
const convertToValidXML = html => {
|
|
64
|
-
// <br> tags in valid HTML (from innerHTML) look like <br>, but they must look like <br/> to be valid XML (such as SVG)
|
|
65
|
-
return html.replace(/<br>/gi, '<br/>')
|
|
66
|
-
}
|
|
67
|
-
|
|
68
63
|
async function cli () {
|
|
69
|
-
// TODO: This is currently unindented to make `git diff` easier for PR reviewers.
|
|
70
64
|
const commander = new Command()
|
|
71
65
|
commander
|
|
72
66
|
.version(pkg.version)
|
|
@@ -75,6 +69,7 @@ async function cli () {
|
|
|
75
69
|
.option('-H, --height [height]', 'Height of the page. Optional. Default: 600', /^\d+$/, '600')
|
|
76
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.')
|
|
77
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')
|
|
78
73
|
.option('-b, --backgroundColor [backgroundColor]', 'Background color for pngs/svgs (not pdfs). Example: transparent, red, \'#F0F0F0\'. Optional. Default: white')
|
|
79
74
|
.option('-c, --configFile [configFile]', 'JSON configuration file for mermaid. Optional')
|
|
80
75
|
.option('-C, --cssFile [cssFile]', 'CSS file for the page. Optional')
|
|
@@ -86,7 +81,7 @@ async function cli () {
|
|
|
86
81
|
|
|
87
82
|
const options = commander.opts()
|
|
88
83
|
|
|
89
|
-
let { theme, width, height, input, output, backgroundColor, configFile, cssFile, puppeteerConfigFile, scale, pdfFit, quiet } = options
|
|
84
|
+
let { theme, width, height, input, output, outputFormat, backgroundColor, configFile, cssFile, puppeteerConfigFile, scale, pdfFit, quiet } = options
|
|
90
85
|
|
|
91
86
|
// check input file
|
|
92
87
|
if (!(input || inputPipedFromStdin())) {
|
|
@@ -144,6 +139,7 @@ async function cli () {
|
|
|
144
139
|
input, output, {
|
|
145
140
|
puppeteerConfig,
|
|
146
141
|
quiet,
|
|
142
|
+
outputFormat,
|
|
147
143
|
parseMMDOptions: {
|
|
148
144
|
mermaidConfig, backgroundColor, myCSS, pdfFit, viewport: { width, height, deviceScaleFactor }
|
|
149
145
|
}
|
|
@@ -163,84 +159,161 @@ async function cli () {
|
|
|
163
159
|
/**
|
|
164
160
|
* Parse and render a mermaid diagram.
|
|
165
161
|
*
|
|
162
|
+
* @deprecated Prefer {@link renderMermaid}, as it also returns useful metadata.
|
|
163
|
+
*
|
|
166
164
|
* @param {puppeteer.Browser} browser - Puppeteer Browser
|
|
167
165
|
* @param {string} definition - Mermaid diagram definition
|
|
168
166
|
* @param {"svg" | "png" | "pdf"} outputFormat - Mermaid output format.
|
|
169
167
|
* @param {ParseMDDOptions} [opt] - Options, see {@link ParseMDDOptions} for details.
|
|
168
|
+
*
|
|
170
169
|
* @returns {Promise<Buffer>} The output file in bytes.
|
|
171
170
|
*/
|
|
172
|
-
async function parseMMD (
|
|
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 } = {}) {
|
|
173
187
|
const page = await browser.newPage()
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
body
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
|
193
216
|
} else {
|
|
194
|
-
|
|
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)
|
|
195
225
|
}
|
|
196
|
-
}
|
|
197
226
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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)
|
|
213
247
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
return {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
if (pdfFit) {
|
|
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') {
|
|
229
262
|
const clip = await page.$eval('svg', svg => {
|
|
230
263
|
const react = svg.getBoundingClientRect()
|
|
231
|
-
return { x: react.left, y: react.top, width: react.width, height: react.height }
|
|
232
|
-
})
|
|
233
|
-
return await page.pdf({
|
|
234
|
-
omitBackground: backgroundColor === 'transparent',
|
|
235
|
-
width: (Math.ceil(clip.width) + clip.x * 2) + 'px',
|
|
236
|
-
height: (Math.ceil(clip.height) + clip.y * 2) + 'px',
|
|
237
|
-
pageRanges: '1-1'
|
|
238
|
-
})
|
|
239
|
-
} else {
|
|
240
|
-
return await page.pdf({
|
|
241
|
-
omitBackground: backgroundColor === 'transparent'
|
|
264
|
+
return { x: Math.floor(react.left), y: Math.floor(react.top), width: Math.ceil(react.width), height: Math.ceil(react.height) }
|
|
242
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
|
+
}
|
|
243
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 {``} 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 ``
|
|
315
|
+
} else {
|
|
316
|
+
return ``
|
|
244
317
|
}
|
|
245
318
|
}
|
|
246
319
|
|
|
@@ -254,57 +327,80 @@ async function parseMMD (browser, definition, outputFormat, { viewport, backgrou
|
|
|
254
327
|
* @param {Object} [opts] - Options
|
|
255
328
|
* @param {puppeteer.LaunchOptions} [opts.puppeteerConfig] - Puppeteer launch options.
|
|
256
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.
|
|
257
332
|
* @param {ParseMDDOptions} [opts.parseMMDOptions] - Options to pass to {@link parseMMDOptions}.
|
|
258
333
|
*/
|
|
259
|
-
async function run (input, output, { puppeteerConfig = {}, quiet = false, parseMMDOptions } = {}) {
|
|
334
|
+
async function run (input, output, { puppeteerConfig = {}, quiet = false, outputFormat, parseMMDOptions } = {}) {
|
|
260
335
|
const info = message => {
|
|
261
336
|
if (!quiet) {
|
|
262
337
|
console.info(message)
|
|
263
338
|
}
|
|
264
339
|
}
|
|
265
340
|
|
|
266
|
-
const mermaidChartsInMarkdown =
|
|
341
|
+
const mermaidChartsInMarkdown = /^\s*```(?:mermaid)(\r?\n([\s\S]*?))```\s*$/
|
|
267
342
|
const mermaidChartsInMarkdownRegexGlobal = new RegExp(mermaidChartsInMarkdown, 'gm')
|
|
268
|
-
const mermaidChartsInMarkdownRegex = new RegExp(mermaidChartsInMarkdown)
|
|
269
343
|
const browser = await puppeteer.launch(puppeteerConfig)
|
|
270
344
|
try {
|
|
271
|
-
|
|
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
|
+
|
|
272
356
|
const definition = await getInputData(input)
|
|
273
357
|
if (/\.md$/.test(input)) {
|
|
274
|
-
const
|
|
275
|
-
const
|
|
276
|
-
const
|
|
358
|
+
const imagePromises = []
|
|
359
|
+
for (const mermaidCodeblockMatch of definition.matchAll(mermaidChartsInMarkdownRegexGlobal)) {
|
|
360
|
+
const mermaidDefinition = mermaidCodeblockMatch[1]
|
|
277
361
|
|
|
278
362
|
// Output can be either a template image file, or a `.md` output file.
|
|
279
363
|
// If it is a template image file, use that to created numbered diagrams
|
|
280
364
|
// I.e. if "out.png", use "out-1.png", "out-2.png", etc
|
|
281
365
|
// If it is an output `.md` file, use that to base .svg numbered diagrams on
|
|
282
366
|
// I.e. if "out.md". use "out-1.svg", "out-2.svg", etc
|
|
283
|
-
const outputFile = output.replace(/(\.(md|png|svg|pdf))$/, `-${
|
|
367
|
+
const outputFile = output.replace(/(\.(md|png|svg|pdf))$/, `-${imagePromises.length + 1}$1`).replace(/(\.md)$/, `.${outputFormat}`)
|
|
284
368
|
const outputFileRelative = `./${path.relative(path.dirname(path.resolve(output)), path.resolve(outputFile))}`
|
|
285
|
-
diagrams.push([outputFile, md])
|
|
286
|
-
return ``
|
|
287
|
-
})
|
|
288
369
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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`)
|
|
297
386
|
} else {
|
|
298
387
|
info('No mermaid charts found in Markdown input')
|
|
299
388
|
}
|
|
300
389
|
|
|
390
|
+
const images = await Promise.all(imagePromises)
|
|
391
|
+
|
|
301
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
|
+
})
|
|
302
398
|
await fs.promises.writeFile(output, outDefinition, 'utf-8')
|
|
303
399
|
info(` ✅ ${output}`)
|
|
304
400
|
}
|
|
305
401
|
} else {
|
|
306
402
|
info('Generating single mermaid chart')
|
|
307
|
-
const data = await parseMMD(browser, definition,
|
|
403
|
+
const data = await parseMMD(browser, definition, outputFormat, parseMMDOptions)
|
|
308
404
|
await fs.promises.writeFile(output, data)
|
|
309
405
|
}
|
|
310
406
|
} finally {
|
|
@@ -312,4 +408,4 @@ async function run (input, output, { puppeteerConfig = {}, quiet = false, parseM
|
|
|
312
408
|
}
|
|
313
409
|
}
|
|
314
410
|
|
|
315
|
-
export { run, parseMMD, cli, error }
|
|
411
|
+
export { run, renderMermaid, parseMMD, cli, error }
|