@mermaid-js/mermaid-cli 9.1.3 → 9.1.6
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 +14 -0
- package/mermaid.min.js +1 -1
- package/package.json +24 -13
- package/src/cli.js +6 -0
- package/src/index.js +315 -0
- package/index.bundle.js +0 -336
package/package.json
CHANGED
|
@@ -1,41 +1,52 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mermaid-js/mermaid-cli",
|
|
3
|
-
"version": "9.1.
|
|
3
|
+
"version": "9.1.6",
|
|
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
|
-
"
|
|
20
|
-
"puppeteer": "^14.1.0"
|
|
27
|
+
"puppeteer": "^16.1.0"
|
|
21
28
|
},
|
|
22
29
|
"devDependencies": {
|
|
23
|
-
"@babel/cli": "^7.0.0",
|
|
24
|
-
"@babel/core": "^7.0.0",
|
|
25
|
-
"@babel/preset-env": "^7.0.0",
|
|
26
30
|
"@fortawesome/fontawesome-free-webfonts": "^1.0.9",
|
|
31
|
+
"mermaid": "^9.1.2",
|
|
32
|
+
"jest": "^28.1.2",
|
|
27
33
|
"standard": "^17.0.0",
|
|
28
34
|
"yarn-upgrade-all": "^0.7.0"
|
|
29
35
|
},
|
|
30
36
|
"files": [
|
|
31
|
-
"
|
|
37
|
+
"src/",
|
|
32
38
|
"mermaid.min.js",
|
|
33
39
|
"index.html",
|
|
34
40
|
"fontawesome/*"
|
|
35
41
|
],
|
|
42
|
+
"jest": {
|
|
43
|
+
"moduleNameMapper": {
|
|
44
|
+
"#(.*)": "<rootDir>/node_modules/$1"
|
|
45
|
+
}
|
|
46
|
+
},
|
|
36
47
|
"standard": {
|
|
37
48
|
"ignore": [
|
|
38
|
-
"
|
|
49
|
+
"mermaid.min.js"
|
|
39
50
|
]
|
|
40
51
|
}
|
|
41
52
|
}
|
package/src/cli.js
ADDED
package/src/index.js
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
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
|
+
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
|
+
async function cli () {
|
|
69
|
+
// TODO: This is currently unindented to make `git diff` easier for PR reviewers.
|
|
70
|
+
const commander = new Command()
|
|
71
|
+
commander
|
|
72
|
+
.version(pkg.version)
|
|
73
|
+
.option('-t, --theme [theme]', 'Theme of the chart, could be default, forest, dark or neutral. Optional. Default: default', /^default|forest|dark|neutral$/, 'default')
|
|
74
|
+
.option('-w, --width [width]', 'Width of the page. Optional. Default: 800', /^\d+$/, '800')
|
|
75
|
+
.option('-H, --height [height]', 'Height of the page. Optional. Default: 600', /^\d+$/, '600')
|
|
76
|
+
.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
|
+
.option('-o, --output [output]', 'Output file. It should be either md, svg, png or pdf. Optional. Default: input + ".svg"')
|
|
78
|
+
.option('-b, --backgroundColor [backgroundColor]', 'Background color for pngs/svgs (not pdfs). Example: transparent, red, \'#F0F0F0\'. Optional. Default: white')
|
|
79
|
+
.option('-c, --configFile [configFile]', 'JSON configuration file for mermaid. Optional')
|
|
80
|
+
.option('-C, --cssFile [cssFile]', 'CSS file for the page. Optional')
|
|
81
|
+
.option('-s, --scale [scale]', 'Puppeteer scale factor, default 1. Optional')
|
|
82
|
+
.option('-f, --pdfFit [pdfFit]', 'Scale PDF to fit chart')
|
|
83
|
+
.option('-q, --quiet', 'Suppress log output')
|
|
84
|
+
.option('-p --puppeteerConfigFile [puppeteerConfigFile]', 'JSON configuration file for puppeteer. Optional')
|
|
85
|
+
.parse(process.argv)
|
|
86
|
+
|
|
87
|
+
const options = commander.opts()
|
|
88
|
+
|
|
89
|
+
let { theme, width, height, input, output, backgroundColor, configFile, cssFile, puppeteerConfigFile, scale, pdfFit, quiet } = options
|
|
90
|
+
|
|
91
|
+
// check input file
|
|
92
|
+
if (!(input || inputPipedFromStdin())) {
|
|
93
|
+
console.error(chalk.red('\nPlease specify input file: -i <input>\n'))
|
|
94
|
+
// Log to stderr, and return with error exitCode
|
|
95
|
+
commander.help({ error: true })
|
|
96
|
+
}
|
|
97
|
+
if (input && !fs.existsSync(input)) {
|
|
98
|
+
error(`Input file "${input}" doesn't exist`)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// check output file
|
|
102
|
+
if (!output) {
|
|
103
|
+
// if an input file is defined, it should take precedence, otherwise, input is
|
|
104
|
+
// coming from stdin and just name the file out.svg, if it hasn't been
|
|
105
|
+
// specified with the '-o' option
|
|
106
|
+
output = input ? (input + '.svg') : 'out.svg'
|
|
107
|
+
}
|
|
108
|
+
if (!/\.(?:svg|png|pdf|md)$/.test(output)) {
|
|
109
|
+
error('Output file must end with ".md", ".svg", ".png" or ".pdf"')
|
|
110
|
+
}
|
|
111
|
+
const outputDir = path.dirname(output)
|
|
112
|
+
if (!fs.existsSync(outputDir)) {
|
|
113
|
+
error(`Output directory "${outputDir}/" doesn't exist`)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// check config files
|
|
117
|
+
let mermaidConfig = { theme }
|
|
118
|
+
if (configFile) {
|
|
119
|
+
checkConfigFile(configFile)
|
|
120
|
+
mermaidConfig = Object.assign(mermaidConfig, JSON.parse(fs.readFileSync(configFile, 'utf-8')))
|
|
121
|
+
}
|
|
122
|
+
let puppeteerConfig = {}
|
|
123
|
+
if (puppeteerConfigFile) {
|
|
124
|
+
checkConfigFile(puppeteerConfigFile)
|
|
125
|
+
puppeteerConfig = JSON.parse(fs.readFileSync(puppeteerConfigFile, 'utf-8'))
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// check cssFile
|
|
129
|
+
let myCSS
|
|
130
|
+
if (cssFile) {
|
|
131
|
+
if (!fs.existsSync(cssFile)) {
|
|
132
|
+
error(`CSS file "${cssFile}" doesn't exist`)
|
|
133
|
+
}
|
|
134
|
+
myCSS = fs.readFileSync(cssFile, 'utf-8')
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// normalize args
|
|
138
|
+
width = parseInt(width)
|
|
139
|
+
height = parseInt(height)
|
|
140
|
+
backgroundColor = backgroundColor || 'white'
|
|
141
|
+
const deviceScaleFactor = parseInt(scale || 1, 10)
|
|
142
|
+
|
|
143
|
+
await run(
|
|
144
|
+
input, output, {
|
|
145
|
+
puppeteerConfig,
|
|
146
|
+
quiet,
|
|
147
|
+
parseMMDOptions: {
|
|
148
|
+
mermaidConfig, backgroundColor, myCSS, pdfFit, viewport: { width, height, deviceScaleFactor }
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* @typedef {Object} ParseMDDOptions Options to pass to {@link parseMMD}
|
|
156
|
+
* @property {puppeteer.Viewport} [viewport] - Puppeteer viewport (e.g. `width`, `height`, `deviceScaleFactor`)
|
|
157
|
+
* @property {string | "transparent"} [backgroundColor] - Background color.
|
|
158
|
+
* @property {Parameters<import("mermaid").Mermaid["initialize"]>[0]} [mermaidConfig] - Mermaid config.
|
|
159
|
+
* @property {CSSStyleDeclaration["cssText"]} [myCSS] - Optional CSS text.
|
|
160
|
+
* @property {boolean} pdfFit - If set, scale PDF to fit chart.
|
|
161
|
+
*/
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Parse and render a mermaid diagram.
|
|
165
|
+
*
|
|
166
|
+
* @param {puppeteer.Browser} browser - Puppeteer Browser
|
|
167
|
+
* @param {string} definition - Mermaid diagram definition
|
|
168
|
+
* @param {"svg" | "png" | "pdf"} outputFormat - Mermaid output format.
|
|
169
|
+
* @param {ParseMDDOptions} [opt] - Options, see {@link ParseMDDOptions} for details.
|
|
170
|
+
* @returns {Promise<Buffer>} The output file in bytes.
|
|
171
|
+
*/
|
|
172
|
+
async function parseMMD (browser, definition, outputFormat, { viewport, backgroundColor = 'white', mermaidConfig = {}, myCSS, pdfFit } = {}) {
|
|
173
|
+
const page = await browser.newPage()
|
|
174
|
+
if (viewport) {
|
|
175
|
+
await page.setViewport(viewport)
|
|
176
|
+
}
|
|
177
|
+
const mermaidHTMLPath = path.join(__dirname, '..', 'index.html')
|
|
178
|
+
await page.goto(url.pathToFileURL(mermaidHTMLPath))
|
|
179
|
+
await page.$eval('body', (body, backgroundColor) => {
|
|
180
|
+
body.style.background = backgroundColor
|
|
181
|
+
}, backgroundColor)
|
|
182
|
+
await page.$eval('#container', (container, definition, mermaidConfig, myCSS, backgroundColor) => {
|
|
183
|
+
container.textContent = definition
|
|
184
|
+
window.mermaid.initialize(mermaidConfig)
|
|
185
|
+
// should throw an error if mmd diagram is invalid
|
|
186
|
+
try {
|
|
187
|
+
window.mermaid.initThrowsErrors(undefined, container)
|
|
188
|
+
} catch (error) {
|
|
189
|
+
if (error instanceof Error) {
|
|
190
|
+
// mermaid-js doesn't currently throws JS Errors, but let's leave this
|
|
191
|
+
// here in case it does in the future
|
|
192
|
+
throw error
|
|
193
|
+
} else {
|
|
194
|
+
throw new Error(error?.message ?? 'Unknown mermaid render error')
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const svg = container.getElementsByTagName?.('svg')?.[0]
|
|
199
|
+
if (svg?.style) {
|
|
200
|
+
svg.style.backgroundColor = backgroundColor
|
|
201
|
+
} else {
|
|
202
|
+
warn('svg not found. Not applying background color.')
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
if (myCSS) {
|
|
206
|
+
// add CSS as a <svg>...<style>... element
|
|
207
|
+
// see https://developer.mozilla.org/en-US/docs/Web/API/SVGStyleElement
|
|
208
|
+
const style = document.createElementNS('http://www.w3.org/2000/svg', 'style')
|
|
209
|
+
style.appendChild(document.createTextNode(myCSS))
|
|
210
|
+
svg.appendChild(style)
|
|
211
|
+
}
|
|
212
|
+
}, definition, mermaidConfig, myCSS, backgroundColor)
|
|
213
|
+
|
|
214
|
+
if (outputFormat === 'svg') {
|
|
215
|
+
const svg = await page.$eval('#container', (container) => {
|
|
216
|
+
return container.innerHTML
|
|
217
|
+
})
|
|
218
|
+
const svgXML = convertToValidXML(svg)
|
|
219
|
+
return Buffer.from(svgXML, 'utf8')
|
|
220
|
+
} else if (outputFormat === 'png') {
|
|
221
|
+
const clip = await page.$eval('svg', svg => {
|
|
222
|
+
const react = svg.getBoundingClientRect()
|
|
223
|
+
return { x: Math.floor(react.left), y: Math.floor(react.top), width: Math.ceil(react.width), height: Math.ceil(react.height) }
|
|
224
|
+
})
|
|
225
|
+
await page.setViewport({ ...viewport, width: clip.x + clip.width, height: clip.y + clip.height })
|
|
226
|
+
return await page.screenshot({ clip, omitBackground: backgroundColor === 'transparent' })
|
|
227
|
+
} else { // pdf
|
|
228
|
+
if (pdfFit) {
|
|
229
|
+
const clip = await page.$eval('svg', svg => {
|
|
230
|
+
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'
|
|
242
|
+
})
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Renders a mermaid diagram or mermaid markdown file.
|
|
249
|
+
*
|
|
250
|
+
* @param {`${string}.md` | string} [input] - If this ends with `.md`, path to a markdown file containing mermaid.
|
|
251
|
+
* If this is a string, loads the mermaid definition from the given file.
|
|
252
|
+
* If this is `undefined`, loads the mermaid definition from stdin.
|
|
253
|
+
* @param {`${string}.${"md" | "svg" | "png" | "pdf"}`} output - Path to the output file.
|
|
254
|
+
* @param {Object} [opts] - Options
|
|
255
|
+
* @param {puppeteer.LaunchOptions} [opts.puppeteerConfig] - Puppeteer launch options.
|
|
256
|
+
* @param {boolean} [opts.quiet] - If set, suppress log output.
|
|
257
|
+
* @param {ParseMDDOptions} [opts.parseMMDOptions] - Options to pass to {@link parseMMDOptions}.
|
|
258
|
+
*/
|
|
259
|
+
async function run (input, output, { puppeteerConfig = {}, quiet = false, parseMMDOptions } = {}) {
|
|
260
|
+
const info = message => {
|
|
261
|
+
if (!quiet) {
|
|
262
|
+
console.info(message)
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const mermaidChartsInMarkdown = /^```(?:mermaid)(\r?\n([\s\S]*?))```$/
|
|
267
|
+
const mermaidChartsInMarkdownRegexGlobal = new RegExp(mermaidChartsInMarkdown, 'gm')
|
|
268
|
+
const mermaidChartsInMarkdownRegex = new RegExp(mermaidChartsInMarkdown)
|
|
269
|
+
const browser = await puppeteer.launch(puppeteerConfig)
|
|
270
|
+
try {
|
|
271
|
+
// TODO: indent this (currently unindented to make `git diff` smaller)
|
|
272
|
+
const definition = await getInputData(input)
|
|
273
|
+
if (/\.md$/.test(input)) {
|
|
274
|
+
const diagrams = []
|
|
275
|
+
const outDefinition = definition.replace(mermaidChartsInMarkdownRegexGlobal, (mermaidMd) => {
|
|
276
|
+
const md = mermaidChartsInMarkdownRegex.exec(mermaidMd)[1]
|
|
277
|
+
|
|
278
|
+
// Output can be either a template image file, or a `.md` output file.
|
|
279
|
+
// If it is a template image file, use that to created numbered diagrams
|
|
280
|
+
// I.e. if "out.png", use "out-1.png", "out-2.png", etc
|
|
281
|
+
// If it is an output `.md` file, use that to base .svg numbered diagrams on
|
|
282
|
+
// I.e. if "out.md". use "out-1.svg", "out-2.svg", etc
|
|
283
|
+
const outputFile = output.replace(/(\.(md|png|svg|pdf))$/, `-${diagrams.length + 1}$1`).replace(/(\.md)$/, '.svg')
|
|
284
|
+
const outputFileRelative = `./${path.relative(path.dirname(path.resolve(output)), path.resolve(outputFile))}`
|
|
285
|
+
diagrams.push([outputFile, md])
|
|
286
|
+
return ``
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
if (diagrams.length) {
|
|
290
|
+
info(`Found ${diagrams.length} mermaid charts in Markdown input`)
|
|
291
|
+
await Promise.all(diagrams.map(async ([imgFile, md]) => {
|
|
292
|
+
const data = await parseMMD(browser, md, path.extname(imgFile).replace('.', ''), parseMMDOptions)
|
|
293
|
+
await fs.promises.writeFile(imgFile, data)
|
|
294
|
+
info(` ✅ ${imgFile}`)
|
|
295
|
+
})
|
|
296
|
+
)
|
|
297
|
+
} else {
|
|
298
|
+
info('No mermaid charts found in Markdown input')
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (/\.md$/.test(output)) {
|
|
302
|
+
await fs.promises.writeFile(output, outDefinition, 'utf-8')
|
|
303
|
+
info(` ✅ ${output}`)
|
|
304
|
+
}
|
|
305
|
+
} else {
|
|
306
|
+
info('Generating single mermaid chart')
|
|
307
|
+
const data = await parseMMD(browser, definition, path.extname(output).replace('.', ''), parseMMDOptions)
|
|
308
|
+
await fs.promises.writeFile(output, data)
|
|
309
|
+
}
|
|
310
|
+
} finally {
|
|
311
|
+
await browser.close()
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export { run, parseMMD, cli, error }
|