@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/README.md +45 -0
- package/mermaid.min.js +1 -1
- package/package.json +23 -13
- package/src/cli.js +6 -0
- package/src/index.js +411 -0
- package/index.bundle.js +0 -336
package/package.json
CHANGED
|
@@ -1,42 +1,52 @@
|
|
|
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",
|
|
7
|
+
"type": "module",
|
|
7
8
|
"author": "Tyler Long <tyler4long@gmail.com>",
|
|
8
9
|
"bin": {
|
|
9
|
-
"mmdc": "./
|
|
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 &&
|
|
13
|
-
"
|
|
14
|
-
"
|
|
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": "^
|
|
25
|
+
"chalk": "^5.0.1",
|
|
18
26
|
"commander": "^9.0.0",
|
|
19
|
-
"puppeteer": "^
|
|
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": "^
|
|
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
|
-
"
|
|
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
|
-
"
|
|
49
|
+
"mermaid.min.js"
|
|
40
50
|
]
|
|
41
51
|
}
|
|
42
52
|
}
|
package/src/cli.js
ADDED
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 {``} 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 ``
|
|
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 }
|