@mermaid-js/mermaid-cli 11.14.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/README.md +17 -11
- package/dist/index.html +0 -2
- package/dist-types/src/index.d.ts +19 -5
- package/dist-types/src/index.d.ts.map +1 -1
- package/dist-types/src/puppeteerIntercept.d.ts +17 -14
- package/dist-types/src/puppeteerIntercept.d.ts.map +1 -1
- package/dist-types/src/version.d.ts +1 -1
- package/dist-types/src/version.d.ts.map +1 -1
- package/package.json +22 -18
- package/src/cli.js +5 -3
- package/src/index.js +626 -303
- package/src/puppeteerIntercept.js +101 -54
- package/src/version.js +1 -1
- package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/assets/fa-brands-400-BdzBFuGj.woff2 +0 -0
- package/dist/assets/fa-regular-400-DQuI-phE.woff2 +0 -0
- package/dist/assets/fa-solid-900-BLm1ImsD.woff2 +0 -0
- package/dist/assets/index-DxLypzbC.js +0 -10
- package/src/index.js.bak +0 -540
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 ``
|
|
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 {``} 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 ``
|
|
402
|
-
} else {
|
|
403
|
-
return ``
|
|
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 }
|