@mermaid-js/mermaid-cli 11.15.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-types/src/index.d.ts +5 -6
- package/dist-types/src/index.d.ts.map +1 -1
- package/dist-types/src/puppeteerIntercept.d.ts +10 -11
- 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 +10 -12
- package/src/cli.js +5 -3
- package/src/index.js +586 -342
- package/src/puppeteerIntercept.js +82 -66
- package/src/version.js +1 -1
- package/src/index.js.bak +0 -540
package/src/index.js
CHANGED
|
@@ -1,47 +1,71 @@
|
|
|
1
|
-
import { Command, Option, InvalidArgumentError } from
|
|
2
|
-
import chalk from
|
|
3
|
-
import fs from
|
|
4
|
-
import { resolve } from
|
|
5
|
-
import os from
|
|
6
|
-
import path from
|
|
7
|
-
import pLimit from
|
|
8
|
-
import puppeteer from
|
|
9
|
-
import url from
|
|
10
|
-
import { promisify } from
|
|
11
|
-
import { version } from
|
|
12
|
-
import { Interceptor } from
|
|
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 os from "node:os";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import pLimit from "p-limit";
|
|
8
|
+
import puppeteer from "puppeteer";
|
|
9
|
+
import url from "url";
|
|
10
|
+
import { promisify } from "node:util";
|
|
11
|
+
import { version } from "./version.js";
|
|
12
|
+
import { Interceptor } from "./puppeteerIntercept.js";
|
|
13
13
|
|
|
14
14
|
// __dirname is not available in ESM modules by default
|
|
15
|
-
const __dirname = url.fileURLToPath(new url.URL(
|
|
15
|
+
const __dirname = url.fileURLToPath(new url.URL(".", import.meta.url));
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* CSS paths to embed in the page.
|
|
19
19
|
*/
|
|
20
20
|
const cssImports = /** @type {const} */ ({
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
})
|
|
21
|
+
"@fortawesome/fontawesome-free/css/brands.css": { level: 2 },
|
|
22
|
+
"@fortawesome/fontawesome-free/css/regular.css": { level: 2 },
|
|
23
|
+
"@fortawesome/fontawesome-free/css/solid.css": { level: 2 },
|
|
24
|
+
"@fortawesome/fontawesome-free/css/fontawesome.css": { level: 2 },
|
|
25
|
+
"katex/dist/katex.css": { level: 2 },
|
|
26
|
+
});
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
29
|
* ESM bundles. Our interceptor doesn't support loading ESM modules that load
|
|
30
30
|
* other modules using relative paths, so these have to no `dependencies`.
|
|
31
31
|
*/
|
|
32
|
-
const mermaidESMPath = path.resolve(
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
const mermaidESMPath = path.resolve(
|
|
33
|
+
path.dirname(url.fileURLToPath(resolve("mermaid", import.meta.url))),
|
|
34
|
+
"mermaid.esm.mjs",
|
|
35
|
+
);
|
|
36
|
+
const elkESMPath = path.resolve(
|
|
37
|
+
path.dirname(
|
|
38
|
+
url.fileURLToPath(resolve("@mermaid-js/layout-elk", import.meta.url)),
|
|
39
|
+
),
|
|
40
|
+
"mermaid-layout-elk.esm.mjs",
|
|
41
|
+
);
|
|
42
|
+
const zenumlESMPath = path.resolve(
|
|
43
|
+
path.dirname(
|
|
44
|
+
url.fileURLToPath(resolve("@mermaid-js/mermaid-zenuml", import.meta.url)),
|
|
45
|
+
),
|
|
46
|
+
"mermaid-zenuml.esm.mjs",
|
|
47
|
+
);
|
|
35
48
|
|
|
36
49
|
/** @type {string | undefined} Path to `@mermaid-js/layout-tidy-tree`, if it is installed */
|
|
37
|
-
let tidyTreeESMPath
|
|
50
|
+
let tidyTreeESMPath;
|
|
38
51
|
try {
|
|
39
|
-
tidyTreeESMPath = path.resolve(
|
|
52
|
+
tidyTreeESMPath = path.resolve(
|
|
53
|
+
path.dirname(
|
|
54
|
+
url.fileURLToPath(
|
|
55
|
+
resolve("@mermaid-js/layout-tidy-tree", import.meta.url),
|
|
56
|
+
),
|
|
57
|
+
),
|
|
58
|
+
"mermaid-layout-tidy-tree.esm.mjs",
|
|
59
|
+
);
|
|
40
60
|
} catch (error) {
|
|
41
|
-
if (
|
|
61
|
+
if (
|
|
62
|
+
error instanceof Error &&
|
|
63
|
+
"code" in error &&
|
|
64
|
+
error.code === "ERR_MODULE_NOT_FOUND"
|
|
65
|
+
) {
|
|
42
66
|
// optional dependency, this is normal
|
|
43
67
|
} else {
|
|
44
|
-
throw error
|
|
68
|
+
throw error;
|
|
45
69
|
}
|
|
46
70
|
}
|
|
47
71
|
|
|
@@ -51,19 +75,19 @@ try {
|
|
|
51
75
|
* @param {string} message - The message to print to `stderr`.
|
|
52
76
|
* @returns {never} Quits Node.JS, so never returns.
|
|
53
77
|
*/
|
|
54
|
-
const error = message => {
|
|
55
|
-
console.error(chalk.red(`\n${message}\n`))
|
|
56
|
-
process.exit(1)
|
|
57
|
-
}
|
|
78
|
+
const error = (message) => {
|
|
79
|
+
console.error(chalk.red(`\n${message}\n`));
|
|
80
|
+
process.exit(1);
|
|
81
|
+
};
|
|
58
82
|
|
|
59
83
|
/**
|
|
60
84
|
* Prints a warning to stderr.
|
|
61
85
|
*
|
|
62
86
|
* @param {string} message - The message to print to `stderr`.
|
|
63
87
|
*/
|
|
64
|
-
const warn = message => {
|
|
65
|
-
console.warn(chalk.yellow(`\n${message}\n`))
|
|
66
|
-
}
|
|
88
|
+
const warn = (message) => {
|
|
89
|
+
console.warn(chalk.yellow(`\n${message}\n`));
|
|
90
|
+
};
|
|
67
91
|
|
|
68
92
|
/**
|
|
69
93
|
* Checks if the given file exists.
|
|
@@ -72,11 +96,11 @@ const warn = message => {
|
|
|
72
96
|
* @returns {never | void} If the file doesn't exist, closes Node.JS with
|
|
73
97
|
* exit code 1.
|
|
74
98
|
*/
|
|
75
|
-
const checkConfigFile = file => {
|
|
99
|
+
const checkConfigFile = (file) => {
|
|
76
100
|
if (!fs.existsSync(file)) {
|
|
77
|
-
error(`Configuration file "${file}" doesn't exist`)
|
|
101
|
+
error(`Configuration file "${file}" doesn't exist`);
|
|
78
102
|
}
|
|
79
|
-
}
|
|
103
|
+
};
|
|
80
104
|
|
|
81
105
|
/**
|
|
82
106
|
* Gets the data in the given file.
|
|
@@ -85,31 +109,31 @@ const checkConfigFile = file => {
|
|
|
85
109
|
* If `undefined`, reads from `stdin` instead.
|
|
86
110
|
* @returns {Promise<string>} The contents of `inputFile` parsed as `utf8`.
|
|
87
111
|
*/
|
|
88
|
-
async function getInputData
|
|
112
|
+
async function getInputData(inputFile) {
|
|
89
113
|
// if an input file has been specified using '-i', it takes precedence over
|
|
90
114
|
// piping from stdin
|
|
91
|
-
if (typeof inputFile !==
|
|
92
|
-
return await fs.promises.readFile(inputFile,
|
|
115
|
+
if (typeof inputFile !== "undefined") {
|
|
116
|
+
return await fs.promises.readFile(inputFile, "utf-8");
|
|
93
117
|
}
|
|
94
118
|
|
|
95
119
|
return await new Promise((resolve, reject) => {
|
|
96
|
-
let data =
|
|
97
|
-
process.stdin.on(
|
|
98
|
-
const chunk = process.stdin.read()
|
|
120
|
+
let data = "";
|
|
121
|
+
process.stdin.on("readable", function () {
|
|
122
|
+
const chunk = process.stdin.read();
|
|
99
123
|
|
|
100
124
|
if (chunk !== null) {
|
|
101
|
-
data += chunk
|
|
125
|
+
data += chunk;
|
|
102
126
|
}
|
|
103
|
-
})
|
|
127
|
+
});
|
|
104
128
|
|
|
105
|
-
process.stdin.on(
|
|
106
|
-
reject(err)
|
|
107
|
-
})
|
|
129
|
+
process.stdin.on("error", function (err) {
|
|
130
|
+
reject(err);
|
|
131
|
+
});
|
|
108
132
|
|
|
109
|
-
process.stdin.on(
|
|
110
|
-
resolve(data)
|
|
111
|
-
})
|
|
112
|
-
})
|
|
133
|
+
process.stdin.on("end", function () {
|
|
134
|
+
resolve(data);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
113
137
|
}
|
|
114
138
|
|
|
115
139
|
/**
|
|
@@ -121,12 +145,12 @@ async function getInputData (inputFile) {
|
|
|
121
145
|
* @throws {InvalidArgumentError} If the arg is not valid.
|
|
122
146
|
* @see https://github.com/tj/commander.js/wiki/Class:-Option#argparserfn
|
|
123
147
|
*/
|
|
124
|
-
function parseCommanderInt
|
|
125
|
-
const parsedValue = parseInt(value, 10)
|
|
148
|
+
function parseCommanderInt(value, _unused) {
|
|
149
|
+
const parsedValue = parseInt(value, 10);
|
|
126
150
|
if (isNaN(parsedValue) || parsedValue < 1) {
|
|
127
|
-
throw new InvalidArgumentError(
|
|
151
|
+
throw new InvalidArgumentError("Not a positive integer.");
|
|
128
152
|
}
|
|
129
|
-
return parsedValue
|
|
153
|
+
return parsedValue;
|
|
130
154
|
}
|
|
131
155
|
|
|
132
156
|
/**
|
|
@@ -137,102 +161,189 @@ function parseCommanderInt (value, _unused) {
|
|
|
137
161
|
* @returns {number} The value parsed as a number.
|
|
138
162
|
* @see https://github.com/tj/commander.js/wiki/Class:-Option#argparserfn
|
|
139
163
|
*/
|
|
140
|
-
function parseCommanderFloat
|
|
141
|
-
const parsedValue = parseFloat(value)
|
|
164
|
+
function parseCommanderFloat(value, _unused) {
|
|
165
|
+
const parsedValue = parseFloat(value);
|
|
142
166
|
if (isNaN(parsedValue) || parsedValue <= 0) {
|
|
143
|
-
throw new InvalidArgumentError(
|
|
167
|
+
throw new InvalidArgumentError("Not a positive number.");
|
|
144
168
|
}
|
|
145
|
-
return parsedValue
|
|
169
|
+
return parsedValue;
|
|
146
170
|
}
|
|
147
171
|
|
|
148
|
-
async function cli
|
|
149
|
-
const commander = new Command()
|
|
172
|
+
async function cli() {
|
|
173
|
+
const commander = new Command();
|
|
150
174
|
commander
|
|
151
175
|
.version(version)
|
|
152
|
-
.addOption(
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
.
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
.addOption(
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
.option(
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
.option(
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
176
|
+
.addOption(
|
|
177
|
+
new Option("-t, --theme [theme]", "Theme of the chart")
|
|
178
|
+
.choices(["default", "forest", "dark", "neutral"])
|
|
179
|
+
.default("default"),
|
|
180
|
+
)
|
|
181
|
+
.addOption(
|
|
182
|
+
new Option("-w, --width [width]", "Width of the page")
|
|
183
|
+
.argParser(parseCommanderInt)
|
|
184
|
+
.default(800),
|
|
185
|
+
)
|
|
186
|
+
.addOption(
|
|
187
|
+
new Option("-H, --height [height]", "Height of the page")
|
|
188
|
+
.argParser(parseCommanderInt)
|
|
189
|
+
.default(600),
|
|
190
|
+
)
|
|
191
|
+
.option(
|
|
192
|
+
"-i, --input <input>",
|
|
193
|
+
"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.",
|
|
194
|
+
)
|
|
195
|
+
.option(
|
|
196
|
+
"-o, --output [output]",
|
|
197
|
+
'Output file. It should be either md, svg, png, pdf or use `-` to output to stdout. Optional. Default: input + ".svg"',
|
|
198
|
+
)
|
|
199
|
+
.option(
|
|
200
|
+
"-a, --artefacts [artefacts]",
|
|
201
|
+
"Output artefacts path. Only used with Markdown input file. Optional. Default: output directory",
|
|
202
|
+
)
|
|
203
|
+
.addOption(
|
|
204
|
+
new Option(
|
|
205
|
+
"-j, --jobs <jobs>",
|
|
206
|
+
"Number of parallel jobs to run when rendering multiple diagrams. Defaults to half the available CPUs.",
|
|
207
|
+
)
|
|
208
|
+
.argParser(parseCommanderInt)
|
|
209
|
+
.default(Math.floor(os.availableParallelism() / 2) || 1),
|
|
210
|
+
)
|
|
211
|
+
.addOption(
|
|
212
|
+
new Option(
|
|
213
|
+
"-e, --outputFormat [format]",
|
|
214
|
+
"Output format for the generated image.",
|
|
215
|
+
)
|
|
216
|
+
.choices(["svg", "png", "pdf"])
|
|
217
|
+
.default(null, "Loaded from the output file extension"),
|
|
218
|
+
)
|
|
219
|
+
.addOption(
|
|
220
|
+
new Option(
|
|
221
|
+
"-b, --backgroundColor [backgroundColor]",
|
|
222
|
+
"Background color for pngs/svgs (not pdfs). Example: transparent, red, '#F0F0F0'.",
|
|
223
|
+
).default("white"),
|
|
224
|
+
)
|
|
225
|
+
.option(
|
|
226
|
+
"-c, --configFile [configFile]",
|
|
227
|
+
"JSON configuration file for mermaid.",
|
|
228
|
+
)
|
|
229
|
+
.option("-C, --cssFile [cssFile]", "CSS file for the page.")
|
|
230
|
+
.option(
|
|
231
|
+
"-I, --svgId [svgId]",
|
|
232
|
+
"The id attribute for the SVG element to be rendered.",
|
|
233
|
+
)
|
|
234
|
+
.addOption(
|
|
235
|
+
new Option("-s, --scale [scale]", "Puppeteer scale factor")
|
|
236
|
+
.argParser(parseCommanderFloat)
|
|
237
|
+
.default(1),
|
|
238
|
+
)
|
|
239
|
+
.option("-f, --pdfFit", "Scale PDF to fit chart")
|
|
240
|
+
.option("-q, --quiet", "Suppress log output")
|
|
241
|
+
.option(
|
|
242
|
+
"-p --puppeteerConfigFile [puppeteerConfigFile]",
|
|
243
|
+
"JSON configuration file for puppeteer.",
|
|
244
|
+
)
|
|
245
|
+
.option(
|
|
246
|
+
"--iconPacks <icons...>",
|
|
247
|
+
"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.",
|
|
248
|
+
[],
|
|
249
|
+
)
|
|
250
|
+
.option(
|
|
251
|
+
"--iconPacksNamesAndUrls <prefix#iconsurl...>",
|
|
252
|
+
'Icon packs to use, e.g. azure#https://raw.githubusercontent.com/NakayamaKento/AzureIcons/refs/heads/main/icons.json where the name (prefix) of the icon pack is defined before the "#" and the url of the json definition after the "#". These should be Iconify json file formatted as IconifyJson, see https://iconify.design/docs/icons/json.html. These will be downloaded when needed.',
|
|
253
|
+
[],
|
|
254
|
+
)
|
|
255
|
+
.parse(process.argv);
|
|
256
|
+
|
|
257
|
+
const options = commander.opts();
|
|
258
|
+
|
|
259
|
+
let {
|
|
260
|
+
theme,
|
|
261
|
+
width,
|
|
262
|
+
height,
|
|
263
|
+
input,
|
|
264
|
+
output,
|
|
265
|
+
outputFormat,
|
|
266
|
+
backgroundColor,
|
|
267
|
+
configFile,
|
|
268
|
+
cssFile,
|
|
269
|
+
svgId,
|
|
270
|
+
puppeteerConfigFile,
|
|
271
|
+
scale,
|
|
272
|
+
pdfFit,
|
|
273
|
+
quiet,
|
|
274
|
+
iconPacks,
|
|
275
|
+
iconPacksNamesAndUrls,
|
|
276
|
+
artefacts,
|
|
277
|
+
jobs,
|
|
278
|
+
} = options;
|
|
177
279
|
|
|
178
280
|
// check input file
|
|
179
281
|
if (!input) {
|
|
180
|
-
warn(
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
282
|
+
warn(
|
|
283
|
+
"No input file specified, reading from stdin. " +
|
|
284
|
+
"If you want to specify an input file, please use `-i <input>.` " +
|
|
285
|
+
"You can use `-i -` to read from stdin and to suppress this warning.",
|
|
286
|
+
);
|
|
287
|
+
} else if (input === "-") {
|
|
185
288
|
// `--input -` means read from stdin, but suppress the above warning
|
|
186
|
-
input = undefined
|
|
289
|
+
input = undefined;
|
|
187
290
|
} else if (!fs.existsSync(input)) {
|
|
188
|
-
error(`Input file "${input}" doesn't exist`)
|
|
291
|
+
error(`Input file "${input}" doesn't exist`);
|
|
189
292
|
}
|
|
190
293
|
|
|
191
294
|
// check output file
|
|
192
295
|
if (!output) {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
296
|
+
// if an input file is defined, it should take precedence, otherwise, input is
|
|
297
|
+
// coming from stdin and just name the file out.svg, if it hasn't been
|
|
298
|
+
// specified with the '-o' option
|
|
196
299
|
if (outputFormat) {
|
|
197
|
-
output = input ?
|
|
300
|
+
output = input ? `${input}.${outputFormat}` : `out.${outputFormat}`;
|
|
198
301
|
} else {
|
|
199
|
-
output = input ?
|
|
302
|
+
output = input ? `${input}.svg` : "out.svg";
|
|
200
303
|
}
|
|
201
|
-
} else if (output ===
|
|
304
|
+
} else if (output === "-") {
|
|
202
305
|
// `--output -` means write to stdout.
|
|
203
|
-
output =
|
|
204
|
-
quiet = true
|
|
306
|
+
output = "/dev/stdout";
|
|
307
|
+
quiet = true;
|
|
205
308
|
|
|
206
309
|
if (!outputFormat) {
|
|
207
|
-
outputFormat =
|
|
208
|
-
warn(
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
310
|
+
outputFormat = "svg";
|
|
311
|
+
warn(
|
|
312
|
+
"No output format specified, using svg. " +
|
|
313
|
+
"If you want to specify an output format and suppress this warning, " +
|
|
314
|
+
"please use `-e <format>.` ",
|
|
315
|
+
);
|
|
212
316
|
}
|
|
213
317
|
} else if (!/\.(?:svg|png|pdf|md|markdown)$/.test(output)) {
|
|
214
|
-
error(
|
|
318
|
+
error(
|
|
319
|
+
'Output file must end with ".md"/".markdown", ".svg", ".png" or ".pdf"',
|
|
320
|
+
);
|
|
215
321
|
}
|
|
216
322
|
|
|
217
323
|
if (artefacts) {
|
|
218
324
|
if (!input || !/\.(?:md|markdown)$/.test(input)) {
|
|
219
|
-
error(
|
|
325
|
+
error(
|
|
326
|
+
"Artefacts [-a|--artefacts] path can only be used with Markdown input file",
|
|
327
|
+
);
|
|
220
328
|
}
|
|
221
329
|
if (!fs.existsSync(artefacts)) {
|
|
222
|
-
fs.mkdirSync(artefacts, { recursive: true })
|
|
330
|
+
fs.mkdirSync(artefacts, { recursive: true });
|
|
223
331
|
}
|
|
224
332
|
}
|
|
225
333
|
|
|
226
|
-
const outputDir = path.dirname(output)
|
|
227
|
-
if (output !==
|
|
228
|
-
error(`Output directory "${outputDir}/" doesn't exist`)
|
|
334
|
+
const outputDir = path.dirname(output);
|
|
335
|
+
if (output !== "/dev/stdout" && !fs.existsSync(outputDir)) {
|
|
336
|
+
error(`Output directory "${outputDir}/" doesn't exist`);
|
|
229
337
|
}
|
|
230
338
|
|
|
231
339
|
// check config files
|
|
232
|
-
let mermaidConfig = { theme }
|
|
340
|
+
let mermaidConfig = { theme };
|
|
233
341
|
if (configFile) {
|
|
234
|
-
checkConfigFile(configFile)
|
|
235
|
-
mermaidConfig = Object.assign(
|
|
342
|
+
checkConfigFile(configFile);
|
|
343
|
+
mermaidConfig = Object.assign(
|
|
344
|
+
mermaidConfig,
|
|
345
|
+
JSON.parse(fs.readFileSync(configFile, "utf-8")),
|
|
346
|
+
);
|
|
236
347
|
}
|
|
237
348
|
|
|
238
349
|
let puppeteerConfig = /** @type {import('puppeteer').LaunchOptions} */ ({
|
|
@@ -241,34 +352,42 @@ async function cli () {
|
|
|
241
352
|
* but still works. In Puppeteer v22, it uses the `chrome-headless-shell` package,
|
|
242
353
|
* which is much faster than the regular headless mode.
|
|
243
354
|
*/
|
|
244
|
-
headless:
|
|
245
|
-
})
|
|
355
|
+
headless: "shell",
|
|
356
|
+
});
|
|
246
357
|
if (puppeteerConfigFile) {
|
|
247
|
-
checkConfigFile(puppeteerConfigFile)
|
|
248
|
-
puppeteerConfig = Object.assign(
|
|
358
|
+
checkConfigFile(puppeteerConfigFile);
|
|
359
|
+
puppeteerConfig = Object.assign(
|
|
360
|
+
puppeteerConfig,
|
|
361
|
+
JSON.parse(fs.readFileSync(puppeteerConfigFile, "utf-8")),
|
|
362
|
+
);
|
|
249
363
|
}
|
|
250
364
|
|
|
251
365
|
// check cssFile
|
|
252
|
-
let myCSS
|
|
366
|
+
let myCSS;
|
|
253
367
|
if (cssFile) {
|
|
254
368
|
if (!fs.existsSync(cssFile)) {
|
|
255
|
-
error(`CSS file "${cssFile}" doesn't exist`)
|
|
369
|
+
error(`CSS file "${cssFile}" doesn't exist`);
|
|
256
370
|
}
|
|
257
|
-
myCSS = fs.readFileSync(cssFile,
|
|
371
|
+
myCSS = fs.readFileSync(cssFile, "utf-8");
|
|
258
372
|
}
|
|
259
373
|
|
|
260
|
-
await run(
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
374
|
+
await run(input, output, {
|
|
375
|
+
puppeteerConfig,
|
|
376
|
+
quiet,
|
|
377
|
+
outputFormat,
|
|
378
|
+
limiter: pLimit(jobs),
|
|
379
|
+
parseMMDOptions: {
|
|
380
|
+
mermaidConfig,
|
|
381
|
+
backgroundColor,
|
|
382
|
+
myCSS,
|
|
383
|
+
pdfFit,
|
|
384
|
+
viewport: { width, height, deviceScaleFactor: scale },
|
|
385
|
+
svgId,
|
|
386
|
+
iconPacks,
|
|
387
|
+
iconPacksNamesAndUrls,
|
|
388
|
+
},
|
|
389
|
+
artefacts,
|
|
390
|
+
});
|
|
272
391
|
}
|
|
273
392
|
|
|
274
393
|
/**
|
|
@@ -292,174 +411,259 @@ async function cli () {
|
|
|
292
411
|
* @returns {Promise<{title: string | null, desc: string | null, data: Uint8Array}>} The output file in bytes,
|
|
293
412
|
* with optional metadata.
|
|
294
413
|
*/
|
|
295
|
-
async function renderMermaid
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
414
|
+
async function renderMermaid(
|
|
415
|
+
browser,
|
|
416
|
+
definition,
|
|
417
|
+
outputFormat,
|
|
418
|
+
{
|
|
419
|
+
viewport,
|
|
420
|
+
backgroundColor = "white",
|
|
421
|
+
mermaidConfig = {},
|
|
422
|
+
myCSS,
|
|
423
|
+
pdfFit,
|
|
424
|
+
svgId,
|
|
425
|
+
iconPacks = [],
|
|
426
|
+
iconPacksNamesAndUrls = [],
|
|
427
|
+
} = {},
|
|
428
|
+
) {
|
|
429
|
+
const page = await browser.newPage();
|
|
430
|
+
page.on("console", (msg) => {
|
|
431
|
+
console.warn(msg.text());
|
|
432
|
+
});
|
|
300
433
|
try {
|
|
301
434
|
if (viewport) {
|
|
302
|
-
await page.setViewport(viewport)
|
|
435
|
+
await page.setViewport(viewport);
|
|
303
436
|
}
|
|
304
|
-
const mermaidHTMLPath = path.join(__dirname,
|
|
305
|
-
await page.goto(url.pathToFileURL(mermaidHTMLPath).href)
|
|
306
|
-
await page.$eval(
|
|
307
|
-
body
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
const
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
437
|
+
const mermaidHTMLPath = path.join(__dirname, "..", "dist", "index.html");
|
|
438
|
+
await page.goto(url.pathToFileURL(mermaidHTMLPath).href);
|
|
439
|
+
await page.$eval(
|
|
440
|
+
"body",
|
|
441
|
+
(body, backgroundColor) => {
|
|
442
|
+
body.style.background = backgroundColor;
|
|
443
|
+
},
|
|
444
|
+
backgroundColor,
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
const interceptor = new Interceptor();
|
|
448
|
+
const mermaidUrl = await interceptor.fileUrlToInterceptUrl(
|
|
449
|
+
url.pathToFileURL(mermaidESMPath),
|
|
450
|
+
);
|
|
451
|
+
const elkUrl = await interceptor.fileUrlToInterceptUrl(
|
|
452
|
+
url.pathToFileURL(elkESMPath),
|
|
453
|
+
);
|
|
454
|
+
const zenumlUrl = await interceptor.fileUrlToInterceptUrl(
|
|
455
|
+
url.pathToFileURL(zenumlESMPath),
|
|
456
|
+
);
|
|
457
|
+
const tidyTreeESMUrl = tidyTreeESMPath
|
|
458
|
+
? await interceptor.fileUrlToInterceptUrl(
|
|
459
|
+
url.pathToFileURL(tidyTreeESMPath),
|
|
460
|
+
)
|
|
461
|
+
: undefined;
|
|
462
|
+
|
|
463
|
+
page.on("request", interceptor.interceptRequestHandler);
|
|
464
|
+
await page.setRequestInterception(true);
|
|
465
|
+
|
|
466
|
+
await Promise.all(
|
|
467
|
+
Object.entries(cssImports).map(async ([cssImport, { level }]) => {
|
|
468
|
+
const interceptUrl = await interceptor.fileUrlToInterceptUrl(
|
|
469
|
+
new URL(resolve(cssImport, import.meta.url)),
|
|
470
|
+
{
|
|
471
|
+
allowParentDirectoryLevel: level,
|
|
472
|
+
},
|
|
473
|
+
);
|
|
474
|
+
await page.addStyleTag({
|
|
475
|
+
url: interceptUrl,
|
|
476
|
+
});
|
|
477
|
+
}),
|
|
478
|
+
);
|
|
479
|
+
|
|
480
|
+
const metadata = await page.$eval(
|
|
481
|
+
"#container",
|
|
482
|
+
async (
|
|
483
|
+
container,
|
|
484
|
+
{
|
|
485
|
+
definition,
|
|
486
|
+
mermaidConfig,
|
|
487
|
+
myCSS,
|
|
488
|
+
backgroundColor,
|
|
489
|
+
svgId,
|
|
490
|
+
iconPacks,
|
|
491
|
+
iconPacksNamesAndUrls,
|
|
492
|
+
elkUrl,
|
|
493
|
+
mermaidUrl,
|
|
494
|
+
zenumlUrl,
|
|
495
|
+
tidyTreeESMUrl,
|
|
496
|
+
},
|
|
497
|
+
) => {
|
|
498
|
+
const { default: mermaid } = await import(mermaidUrl);
|
|
499
|
+
/** @type {typeof import('@mermaid-js/layout-elk')} */
|
|
500
|
+
const { default: elkLayouts } = await import(elkUrl);
|
|
501
|
+
/** @type {typeof import('@mermaid-js/mermaid-zenuml')} */
|
|
502
|
+
const { default: zenuml } = await import(zenumlUrl);
|
|
503
|
+
// @ts-ignore -- @mermaid-js/layout-tidy-tree is an optionalDependency and might not be installed
|
|
504
|
+
/** @type {typeof import('@mermaid-js/layout-tidy-tree') | {default: undefined}} */
|
|
505
|
+
const { default: tidyTree } = tidyTreeESMUrl
|
|
506
|
+
? await import(tidyTreeESMUrl)
|
|
507
|
+
: { default: undefined };
|
|
508
|
+
await Promise.all(Array.from(document.fonts, (font) => font.load()));
|
|
509
|
+
|
|
510
|
+
await mermaid.registerExternalDiagrams([zenuml]);
|
|
511
|
+
mermaid.registerLayoutLoaders([...elkLayouts, ...(tidyTree ?? [])]);
|
|
512
|
+
// lazy load icon packs
|
|
513
|
+
|
|
514
|
+
mermaid.registerIconPacks(
|
|
515
|
+
iconPacks.map((icon) => ({
|
|
516
|
+
name: icon.split("/")[1],
|
|
363
517
|
loader: () =>
|
|
364
|
-
fetch(
|
|
518
|
+
fetch(`https://unpkg.com/${icon}/icons.json`)
|
|
365
519
|
.then((res) => res.json())
|
|
366
|
-
.catch(() => {
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
520
|
+
.catch(() => error(`Failed to fetch icon: ${icon}`)),
|
|
521
|
+
})),
|
|
522
|
+
);
|
|
523
|
+
|
|
524
|
+
mermaid.registerIconPacks(
|
|
525
|
+
iconPacksNamesAndUrls.map((iconPackInfo) => {
|
|
526
|
+
const packName = iconPackInfo.split("#")[0];
|
|
527
|
+
const packUrl = iconPackInfo.split("#")[1];
|
|
528
|
+
|
|
529
|
+
return {
|
|
530
|
+
name: packName,
|
|
531
|
+
loader: () =>
|
|
532
|
+
fetch(packUrl)
|
|
533
|
+
.then((res) => res.json())
|
|
534
|
+
.catch(() => {
|
|
535
|
+
error(`Failed to fetch icon: ${iconPackInfo}`);
|
|
536
|
+
}),
|
|
537
|
+
};
|
|
538
|
+
}),
|
|
539
|
+
);
|
|
540
|
+
mermaid.initialize({ startOnLoad: false, ...mermaidConfig });
|
|
541
|
+
// should throw an error if mmd diagram is invalid
|
|
542
|
+
const { svg: svgText } = await mermaid.render(
|
|
543
|
+
svgId || "my-svg",
|
|
544
|
+
definition,
|
|
545
|
+
container,
|
|
546
|
+
);
|
|
547
|
+
container.innerHTML = svgText;
|
|
548
|
+
|
|
549
|
+
const svg = container.getElementsByTagName?.("svg")?.[0];
|
|
550
|
+
if (svg?.style) {
|
|
551
|
+
svg.style.backgroundColor = backgroundColor;
|
|
552
|
+
} else {
|
|
553
|
+
warn("svg not found. Not applying background color.");
|
|
554
|
+
}
|
|
555
|
+
if (myCSS) {
|
|
556
|
+
// add CSS as a <svg>...<style>... element
|
|
557
|
+
// see https://developer.mozilla.org/en-US/docs/Web/API/SVGStyleElement
|
|
558
|
+
const style = document.createElementNS(
|
|
559
|
+
"http://www.w3.org/2000/svg",
|
|
560
|
+
"style",
|
|
561
|
+
);
|
|
562
|
+
style.appendChild(document.createTextNode(myCSS));
|
|
563
|
+
svg.appendChild(style);
|
|
371
564
|
}
|
|
372
|
-
)
|
|
373
|
-
)
|
|
374
|
-
mermaid.initialize({ startOnLoad: false, ...mermaidConfig })
|
|
375
|
-
// should throw an error if mmd diagram is invalid
|
|
376
|
-
const { svg: svgText } = await mermaid.render(svgId || 'my-svg', definition, container)
|
|
377
|
-
container.innerHTML = svgText
|
|
378
|
-
|
|
379
|
-
const svg = container.getElementsByTagName?.('svg')?.[0]
|
|
380
|
-
if (svg?.style) {
|
|
381
|
-
svg.style.backgroundColor = backgroundColor
|
|
382
|
-
} else {
|
|
383
|
-
warn('svg not found. Not applying background color.')
|
|
384
|
-
}
|
|
385
|
-
if (myCSS) {
|
|
386
|
-
// add CSS as a <svg>...<style>... element
|
|
387
|
-
// see https://developer.mozilla.org/en-US/docs/Web/API/SVGStyleElement
|
|
388
|
-
const style = document.createElementNS('http://www.w3.org/2000/svg', 'style')
|
|
389
|
-
style.appendChild(document.createTextNode(myCSS))
|
|
390
|
-
svg.appendChild(style)
|
|
391
|
-
}
|
|
392
565
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
title = svg.firstChild.textContent
|
|
401
|
-
}
|
|
402
|
-
/** SVG description. According to SVG spec, we should use the first one we find */
|
|
403
|
-
let desc = null
|
|
404
|
-
for (const svgNode of svg.children) {
|
|
405
|
-
if (svgNode instanceof SVGDescElement) {
|
|
406
|
-
desc = svgNode.textContent
|
|
566
|
+
// Finds SVG metadata for accessibility purposes
|
|
567
|
+
/** SVG title */
|
|
568
|
+
let title = null;
|
|
569
|
+
// If <title> exists, it must be the first child Node,
|
|
570
|
+
// see https://www.w3.org/TR/SVG11/struct.html#DescriptionAndTitleElements
|
|
571
|
+
if (svg.firstChild instanceof SVGTitleElement) {
|
|
572
|
+
title = svg.firstChild.textContent;
|
|
407
573
|
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
574
|
+
/** SVG description. According to SVG spec, we should use the first one we find */
|
|
575
|
+
let desc = null;
|
|
576
|
+
for (const svgNode of svg.children) {
|
|
577
|
+
if (svgNode instanceof SVGDescElement) {
|
|
578
|
+
desc = svgNode.textContent;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
return {
|
|
582
|
+
title,
|
|
583
|
+
desc,
|
|
584
|
+
};
|
|
585
|
+
},
|
|
586
|
+
{
|
|
587
|
+
definition,
|
|
588
|
+
mermaidConfig,
|
|
589
|
+
myCSS,
|
|
590
|
+
backgroundColor,
|
|
591
|
+
svgId,
|
|
592
|
+
iconPacks,
|
|
593
|
+
iconPacksNamesAndUrls,
|
|
594
|
+
elkUrl,
|
|
595
|
+
mermaidUrl,
|
|
596
|
+
zenumlUrl,
|
|
597
|
+
tidyTreeESMUrl,
|
|
598
|
+
},
|
|
599
|
+
);
|
|
413
600
|
|
|
414
|
-
if (outputFormat ===
|
|
415
|
-
const svgXML = await page.$eval(
|
|
601
|
+
if (outputFormat === "svg") {
|
|
602
|
+
const svgXML = await page.$eval("svg", (svg) => {
|
|
416
603
|
// SVG might have HTML <foreignObject> that are not valid XML
|
|
417
604
|
// E.g. <br> must be replaced with <br/>
|
|
418
605
|
// Luckily the DOM Web API has the XMLSerializer for this
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
})
|
|
606
|
+
const xmlSerializer = new XMLSerializer();
|
|
607
|
+
return xmlSerializer.serializeToString(svg);
|
|
608
|
+
});
|
|
423
609
|
return {
|
|
424
610
|
...metadata,
|
|
425
|
-
data: new TextEncoder().encode(svgXML)
|
|
426
|
-
}
|
|
427
|
-
} else if (outputFormat ===
|
|
428
|
-
const clip = await page.$eval(
|
|
429
|
-
const react = svg.getBoundingClientRect()
|
|
430
|
-
return {
|
|
431
|
-
|
|
432
|
-
|
|
611
|
+
data: new TextEncoder().encode(svgXML),
|
|
612
|
+
};
|
|
613
|
+
} else if (outputFormat === "png") {
|
|
614
|
+
const clip = await page.$eval("svg", (svg) => {
|
|
615
|
+
const react = svg.getBoundingClientRect();
|
|
616
|
+
return {
|
|
617
|
+
x: Math.floor(react.left),
|
|
618
|
+
y: Math.floor(react.top),
|
|
619
|
+
width: Math.ceil(react.width),
|
|
620
|
+
height: Math.ceil(react.height),
|
|
621
|
+
};
|
|
622
|
+
});
|
|
623
|
+
await page.setViewport({
|
|
624
|
+
...viewport,
|
|
625
|
+
width: clip.x + clip.width,
|
|
626
|
+
height: clip.y + clip.height,
|
|
627
|
+
});
|
|
433
628
|
return {
|
|
434
629
|
...metadata,
|
|
435
|
-
data: await page.screenshot({
|
|
436
|
-
|
|
437
|
-
|
|
630
|
+
data: await page.screenshot({
|
|
631
|
+
clip,
|
|
632
|
+
omitBackground: backgroundColor === "transparent",
|
|
633
|
+
}),
|
|
634
|
+
};
|
|
635
|
+
} else {
|
|
636
|
+
// pdf
|
|
438
637
|
if (pdfFit) {
|
|
439
|
-
const clip = await page.$eval(
|
|
440
|
-
const react = svg.getBoundingClientRect()
|
|
441
|
-
return {
|
|
442
|
-
|
|
638
|
+
const clip = await page.$eval("svg", (svg) => {
|
|
639
|
+
const react = svg.getBoundingClientRect();
|
|
640
|
+
return {
|
|
641
|
+
x: react.left,
|
|
642
|
+
y: react.top,
|
|
643
|
+
width: react.width,
|
|
644
|
+
height: react.height,
|
|
645
|
+
};
|
|
646
|
+
});
|
|
443
647
|
return {
|
|
444
648
|
...metadata,
|
|
445
649
|
data: await page.pdf({
|
|
446
|
-
omitBackground: backgroundColor ===
|
|
447
|
-
width:
|
|
448
|
-
height:
|
|
449
|
-
pageRanges:
|
|
450
|
-
})
|
|
451
|
-
}
|
|
650
|
+
omitBackground: backgroundColor === "transparent",
|
|
651
|
+
width: Math.ceil(clip.width) + clip.x * 2 + "px",
|
|
652
|
+
height: Math.ceil(clip.height) + clip.y * 2 + "px",
|
|
653
|
+
pageRanges: "1-1",
|
|
654
|
+
}),
|
|
655
|
+
};
|
|
452
656
|
} else {
|
|
453
657
|
return {
|
|
454
658
|
...metadata,
|
|
455
659
|
data: await page.pdf({
|
|
456
|
-
omitBackground: backgroundColor ===
|
|
457
|
-
})
|
|
458
|
-
}
|
|
660
|
+
omitBackground: backgroundColor === "transparent",
|
|
661
|
+
}),
|
|
662
|
+
};
|
|
459
663
|
}
|
|
460
664
|
}
|
|
461
665
|
} finally {
|
|
462
|
-
await page.close()
|
|
666
|
+
await page.close();
|
|
463
667
|
}
|
|
464
668
|
}
|
|
465
669
|
|
|
@@ -477,14 +681,14 @@ async function renderMermaid (browser, definition, outputFormat, { viewport, bac
|
|
|
477
681
|
* @param {MarkdownImageProps} params - Parameters.
|
|
478
682
|
* @returns {``} The markdown image text.
|
|
479
683
|
*/
|
|
480
|
-
function markdownImage
|
|
684
|
+
function markdownImage({ url, title, alt }) {
|
|
481
685
|
// we can't use String.prototype.replaceAll since it's not supported in Node v14
|
|
482
|
-
const altEscaped = alt.replace(/[[\]\\]/g,
|
|
686
|
+
const altEscaped = alt.replace(/[[\]\\]/g, "\\$&");
|
|
483
687
|
if (title) {
|
|
484
|
-
const titleEscaped = title.replace(/["\\]/g,
|
|
485
|
-
return `
|
|
688
|
+
const titleEscaped = title.replace(/["\\]/g, "\\$&");
|
|
689
|
+
return ``;
|
|
486
690
|
} else {
|
|
487
|
-
return `
|
|
691
|
+
return ``;
|
|
488
692
|
}
|
|
489
693
|
}
|
|
490
694
|
|
|
@@ -514,55 +718,76 @@ function markdownImage ({ url, title, alt }) {
|
|
|
514
718
|
* @param {Limiter} [opts.limiter] - If set, limiter function to avoid rendering too many diagrams in parallel.
|
|
515
719
|
* @param {ParseMDDOptions} [opts.parseMMDOptions] - Options to pass to {@link parseMMDOptions}.
|
|
516
720
|
*/
|
|
517
|
-
async function run
|
|
721
|
+
async function run(
|
|
722
|
+
input,
|
|
723
|
+
output,
|
|
724
|
+
{
|
|
725
|
+
browser: userPassedBrowser,
|
|
726
|
+
puppeteerConfig = {},
|
|
727
|
+
quiet = false,
|
|
728
|
+
outputFormat,
|
|
729
|
+
parseMMDOptions,
|
|
730
|
+
limiter = (x, ...args) => x(...args),
|
|
731
|
+
artefacts,
|
|
732
|
+
} = {},
|
|
733
|
+
) {
|
|
518
734
|
/**
|
|
519
735
|
* Logs the given message to stdout, unless `quiet` is set to `true`.
|
|
520
736
|
*
|
|
521
737
|
* @param {string} message - The message to maybe log.
|
|
522
738
|
*/
|
|
523
|
-
const info = message => {
|
|
739
|
+
const info = (message) => {
|
|
524
740
|
if (!quiet) {
|
|
525
|
-
console.info(message)
|
|
741
|
+
console.info(message);
|
|
526
742
|
}
|
|
527
|
-
}
|
|
743
|
+
};
|
|
528
744
|
|
|
529
745
|
// TODO: should we use a Markdown parser like remark instead of rolling our own parser?
|
|
530
|
-
const mermaidChartsInMarkdown =
|
|
531
|
-
|
|
746
|
+
const mermaidChartsInMarkdown =
|
|
747
|
+
/^[^\S\n]*[`:]{3}(?:mermaid)([^\S\n]*\r?\n([\s\S]*?))[`:]{3}[^\S\n]*$/;
|
|
748
|
+
const mermaidChartsInMarkdownRegexGlobal = new RegExp(
|
|
749
|
+
mermaidChartsInMarkdown,
|
|
750
|
+
"gm",
|
|
751
|
+
);
|
|
532
752
|
/**
|
|
533
|
-
* @type {puppeteer.Browser | undefined}
|
|
753
|
+
* @type {import('puppeteer').Browser | undefined}
|
|
534
754
|
* Lazy-loaded browser instance, only created when needed.
|
|
535
755
|
*/
|
|
536
|
-
let browser = userPassedBrowser
|
|
756
|
+
let browser = userPassedBrowser;
|
|
537
757
|
try {
|
|
538
758
|
if (!outputFormat) {
|
|
539
759
|
const outputFormatFromFilename =
|
|
540
760
|
/**
|
|
541
761
|
* @type {"md" | "markdown" | "svg" | "png" | "pdf"}
|
|
542
|
-
*/ (path.extname(output).replace(
|
|
543
|
-
if (
|
|
762
|
+
*/ (path.extname(output).replace(".", ""));
|
|
763
|
+
if (
|
|
764
|
+
outputFormatFromFilename === "md" ||
|
|
765
|
+
outputFormatFromFilename === "markdown"
|
|
766
|
+
) {
|
|
544
767
|
// fallback to svg in case no outputFormat is given and output file is MD
|
|
545
|
-
outputFormat =
|
|
768
|
+
outputFormat = "svg";
|
|
546
769
|
} else {
|
|
547
|
-
outputFormat = outputFormatFromFilename
|
|
770
|
+
outputFormat = outputFormatFromFilename;
|
|
548
771
|
}
|
|
549
772
|
}
|
|
550
773
|
if (!/(?:svg|png|pdf)$/.test(outputFormat)) {
|
|
551
|
-
throw new Error('Output format must be one of "svg", "png" or "pdf"')
|
|
774
|
+
throw new Error('Output format must be one of "svg", "png" or "pdf"');
|
|
552
775
|
}
|
|
553
776
|
|
|
554
|
-
const definition = await getInputData(input)
|
|
777
|
+
const definition = await getInputData(input);
|
|
555
778
|
if (input && /\.(md|markdown)$/.test(input)) {
|
|
556
|
-
if (output ===
|
|
557
|
-
throw new Error(
|
|
779
|
+
if (output === "/dev/stdout") {
|
|
780
|
+
throw new Error("Cannot use `stdout` with markdown input");
|
|
558
781
|
}
|
|
559
782
|
|
|
560
|
-
const imagePromises = []
|
|
561
|
-
for (const mermaidCodeblockMatch of definition.matchAll(
|
|
783
|
+
const imagePromises = [];
|
|
784
|
+
for (const mermaidCodeblockMatch of definition.matchAll(
|
|
785
|
+
mermaidChartsInMarkdownRegexGlobal,
|
|
786
|
+
)) {
|
|
562
787
|
if (browser === undefined) {
|
|
563
|
-
browser = await puppeteer.launch(puppeteerConfig)
|
|
788
|
+
browser = await puppeteer.launch(puppeteerConfig);
|
|
564
789
|
}
|
|
565
|
-
const mermaidDefinition = mermaidCodeblockMatch[2]
|
|
790
|
+
const mermaidDefinition = mermaidCodeblockMatch[2];
|
|
566
791
|
|
|
567
792
|
/** Output can be either a template image file, or a `.md` output file.
|
|
568
793
|
* If it is a template image file, use that to created numbered diagrams
|
|
@@ -571,69 +796,88 @@ async function run (input, output, { browser: userPassedBrowser, puppeteerConfig
|
|
|
571
796
|
* I.e. if "out.md". use "out-1.svg", "out-2.svg", etc
|
|
572
797
|
* @type {string}
|
|
573
798
|
*/
|
|
574
|
-
let outputFile = output
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
799
|
+
let outputFile = output
|
|
800
|
+
.replace(
|
|
801
|
+
/(\.(md|markdown|png|svg|pdf))$/,
|
|
802
|
+
`-${imagePromises.length + 1}$1`,
|
|
803
|
+
)
|
|
804
|
+
.replace(/\.(md|markdown)$/, `.${outputFormat}`);
|
|
578
805
|
|
|
579
806
|
if (artefacts) {
|
|
580
|
-
outputFile = path.resolve(artefacts, path.basename(outputFile))
|
|
807
|
+
outputFile = path.resolve(artefacts, path.basename(outputFile));
|
|
581
808
|
}
|
|
582
809
|
|
|
583
|
-
const outputFileRelative = `./${path.relative(path.dirname(path.resolve(output)), path.resolve(outputFile))}
|
|
584
|
-
|
|
585
|
-
const imagePromise = limiter(
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
810
|
+
const outputFileRelative = `./${path.relative(path.dirname(path.resolve(output)), path.resolve(outputFile))}`;
|
|
811
|
+
|
|
812
|
+
const imagePromise = limiter(
|
|
813
|
+
async (browser, outputFormat) => {
|
|
814
|
+
const { title, desc, data } = await renderMermaid(
|
|
815
|
+
browser,
|
|
816
|
+
mermaidDefinition,
|
|
817
|
+
outputFormat,
|
|
818
|
+
parseMMDOptions,
|
|
819
|
+
);
|
|
820
|
+
await fs.promises.writeFile(outputFile, data);
|
|
821
|
+
info(` ✅ ${outputFileRelative}`);
|
|
822
|
+
|
|
823
|
+
return {
|
|
824
|
+
url: outputFileRelative,
|
|
825
|
+
title,
|
|
826
|
+
alt: desc,
|
|
827
|
+
};
|
|
828
|
+
},
|
|
829
|
+
browser,
|
|
830
|
+
outputFormat,
|
|
831
|
+
);
|
|
832
|
+
imagePromises.push(imagePromise);
|
|
597
833
|
}
|
|
598
834
|
|
|
599
835
|
if (imagePromises.length) {
|
|
600
|
-
info(`Found ${imagePromises.length} mermaid charts in Markdown input`)
|
|
836
|
+
info(`Found ${imagePromises.length} mermaid charts in Markdown input`);
|
|
601
837
|
} else {
|
|
602
|
-
info(
|
|
838
|
+
info("No mermaid charts found in Markdown input");
|
|
603
839
|
}
|
|
604
840
|
|
|
605
|
-
const images = await Promise.all(imagePromises)
|
|
841
|
+
const images = await Promise.all(imagePromises);
|
|
606
842
|
|
|
607
843
|
if (/\.(md|markdown)$/.test(output)) {
|
|
608
|
-
const outDefinition = definition.replace(
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
844
|
+
const outDefinition = definition.replace(
|
|
845
|
+
mermaidChartsInMarkdownRegexGlobal,
|
|
846
|
+
(_mermaidMd) => {
|
|
847
|
+
// pop first image from front of array
|
|
848
|
+
const { url, title, alt } =
|
|
849
|
+
/**
|
|
850
|
+
* @type {MarkdownImageProps} We use the same regex,
|
|
851
|
+
* so we will never try to get too many objects from the array.
|
|
852
|
+
* (aka `images.shift()` will never return `undefined`)
|
|
853
|
+
*/ (images.shift());
|
|
854
|
+
return markdownImage({ url, title, alt: alt || "diagram" });
|
|
855
|
+
},
|
|
856
|
+
);
|
|
857
|
+
await fs.promises.writeFile(output, outDefinition, "utf-8");
|
|
858
|
+
info(` ✅ ${output}`);
|
|
620
859
|
}
|
|
621
860
|
} else {
|
|
622
|
-
info(
|
|
623
|
-
browser ??= await puppeteer.launch(puppeteerConfig)
|
|
624
|
-
const { data } = await renderMermaid(
|
|
625
|
-
|
|
626
|
-
|
|
861
|
+
info("Generating single mermaid chart");
|
|
862
|
+
browser ??= await puppeteer.launch(puppeteerConfig);
|
|
863
|
+
const { data } = await renderMermaid(
|
|
864
|
+
browser,
|
|
865
|
+
definition,
|
|
866
|
+
outputFormat,
|
|
867
|
+
parseMMDOptions,
|
|
868
|
+
);
|
|
869
|
+
if (output === "/dev/stdout") {
|
|
870
|
+
await promisify(process.stdout.write).call(process.stdout, data);
|
|
627
871
|
} else {
|
|
628
|
-
await fs.promises.writeFile(output, data)
|
|
872
|
+
await fs.promises.writeFile(output, data);
|
|
629
873
|
}
|
|
630
874
|
}
|
|
631
875
|
} finally {
|
|
632
876
|
// Don't close the browser if it was passed in by the user
|
|
633
877
|
if (browser !== userPassedBrowser) {
|
|
634
|
-
await browser?.close?.()
|
|
878
|
+
await browser?.close?.();
|
|
635
879
|
}
|
|
636
880
|
}
|
|
637
881
|
}
|
|
638
882
|
|
|
639
|
-
export { run, renderMermaid, cli, error }
|
|
883
|
+
export { run, renderMermaid, cli, error };
|