-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathindex.js
234 lines (217 loc) · 7.15 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
const { readFile } = require("fs/promises");
const puppeteer = require("puppeteer");
const PLUGIN_NAME = "remark-mermaid-dataurl";
/**
* @typedef {{[key: string]: any}} MermaidCliKwargs CLI options to pass to `@mermaid-js/mermaid-cli`
*
* For example, `--cssFile my-css-file.css` can be converted to `{"cssFile": "my-css-file.css"}`
*/
/**
* Adds custom `remark-mermaid-dataurl` defaults to a mermaid config file.
*
* Sets `useMaxWidth` to `false` by default for better Markdown SVGs.
*
* @param {{[x: string]: any}} mermaidConfig - Warning, this is modified by this function.
* @returns {object} The input object with some default vars modified.
*/
function addDefaultConfig(mermaidConfig) {
const GRAPHS_TO_DISABLE_MAX_WIDTH = [
"flowchart",
"sequence",
"gantt",
"journey",
"class",
"state",
"er",
"pie",
"requirement",
"c4",
];
for (const graphType of GRAPHS_TO_DISABLE_MAX_WIDTH) {
mermaidConfig[graphType] = {
// if this is true (default), SVG will use up the entire width of the markdown
// document, which is much much too wide
useMaxWidth: false,
...mermaidConfig[graphType],
};
}
return mermaidConfig;
}
/**
* Converts CLI args to {@link parseMMD} options.
*
* Required for backwards compatibility.
*
* @param {{[key: string]: any}} kwargs Args passed to mmdc in format `--key value`
* @returns {Promise<import("@mermaid-js/mermaid-cli").ParseMDDOptions>} Options to pass to parseMMD.
*/
async function convertMermaidKwargsToParseMMDOpts({
theme,
width = 800,
height = 600,
backgroundColor,
configFile,
cssFile,
scale,
pdfFit,
}) {
let mermaidConfig = { theme };
if (configFile) {
if (typeof configFile !== "object") {
mermaidConfig = {
...mermaidConfig,
...JSON.parse(await readFile(configFile, { encoding: "utf8" })),
};
} else {
mermaidConfig = { ...mermaidConfig, ...configFile };
}
}
let myCSS;
if (cssFile) {
myCSS = await readFile(cssFile, { encoding: "utf8" });
}
return {
mermaidConfig: addDefaultConfig(mermaidConfig),
backgroundColor,
myCSS,
pdfFit,
viewport: { width, height, deviceScaleFactor: scale },
};
}
/**
* Converts a string to a base64 string
*
* @param {string} string - The string to convert.
*/
function btoa(string) {
return Buffer.from(string).toString("base64");
}
/**
* Creates a data URL.
*
* @param {string} data - The data to convert.
* @param {string} mimeType - The MIME-type of the data.
* @param {boolean} [base64] - If `true`, use base64 encoding instead of URI encoding.
* (Better for encoding binary data).
* @returns {string} The dataurl.
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URLs
*/
function dataUrl(data, mimeType, base64 = false) {
if (base64) {
return `data:${mimeType};base64,${btoa(data)}`;
} else {
return `data:${mimeType},${encodeURIComponent(data)}`;
}
}
/**
* Transforms the given Mermaid Node.
*
* @param {import("mdast").Code} node - The Mermaid code-block.
* @param {import("vfile").VFile} file - The VFile to report errors to.
* @param {number} index - Index of node in `parent` node.
* @param {import("mdast").Parent} parent - The parent node.
* @param {object} options - Options.
* @param {MermaidCliKwargs} options.mermaidCli - kwargs to pass to mermaid cli.
* @param {puppeteer.Browser} options.browser - Puppeteer browser to use.
*/
async function transformMermaidNode(
node,
file,
index,
parent,
{ mermaidCli, browser },
) {
const { lang, value, position } = node;
try {
const { renderMermaid } = await import("@mermaid-js/mermaid-cli");
const { setSvgBbox, validSVG } = await import("./src/svg.mjs");
const { title, desc, data } = await renderMermaid(
browser,
value,
"svg",
await convertMermaidKwargsToParseMMDOpts(mermaidCli),
);
let svgString = new TextDecoder().decode(data);
// attempts to convert the whatever mermaid-cli returned into a valid SVG
// or throws an error if it can't
svgString = validSVG(svgString);
// replace width=100% with actual width in px
svgString = setSvgBbox(svgString);
/** @type {import("mdast").Image} */
const newNode = {
type: "image",
title: title ?? "Diagram generated via mermaid",
url: dataUrl(svgString, "image/svg+xml;charset=UTF-8"),
alt: desc ?? "Diagram generated via mermaid",
};
file.info(`${lang} code block replaced with graph`, position, PLUGIN_NAME);
// replace old node with current node
parent.children[index] = newNode;
} catch (error) {
const errorError = error instanceof Error ? error : new Error(`${error}`);
file.fail(errorError, position, PLUGIN_NAME);
}
}
/**
* Remark plugin that converts mermaid codeblocks into self-contained SVG dataurls.
* @param {Object} options
* @param {Object} [options.mermaidCli] Options to pass to mermaid-cli
* @param {Object | string} [options.mermaidCli.configFile] - If set, a path to
* a JSON configuration file for mermaid.
* If this is an object, it will be automatically converted to a JSON config
* file and passed to mermaid-cli.
* @param {import("puppeteer").LaunchOptions | string} [options.mermaidCli.puppeteerConfigFile] - If set,
* a path to a JSON configuration file for mermaid CLI's puppeteer instance.
* If this is an object, it will be automatically converted to a JSON config
* file and passed to mermaid-cli.
*/
function remarkMermaid({ mermaidCli = {} } = {}) {
const options = { mermaidCli };
/**
* Look for all code nodes that have the language mermaid,
* and replace them with images with data urls.
*
* @param {import("mdast").Root} tree The Markdown Tree
* @param {import("vfile").VFile} file The virtual file.
* @returns {Promise<void>}
*/
return async function (tree, file) {
/** @type {Array<Promise<void>>} */
const promises = []; // keep track of promises since visit isn't async
const { visit } = await import("unist-util-visit");
let puppeteerConfigFile = mermaidCli.puppeteerConfigFile ?? {};
if (typeof puppeteerConfigFile === "string") {
puppeteerConfigFile = /** @type {import("puppeteer").LaunchOptions} */ (
JSON.parse(await readFile(puppeteerConfigFile, { encoding: "utf8" }))
);
}
const browser = await puppeteer.launch(puppeteerConfigFile);
try {
// @ts-ignore There's some issue with TypeScript here
visit(tree, (node, index, parent) => {
// If this codeblock is not mermaid, bail.
if (node.type !== "code" || node.lang !== "mermaid") {
return node;
}
promises.push(
transformMermaidNode(
node,
file,
// We know these values are never `null`, since a Code block will
// never be the Root of a mdast, so there will always be a Parent
/** @type {number} */ (index),
/** @type {import("mdast").Parent} */ (parent),
{
...options,
browser,
},
),
);
});
await Promise.all(promises);
} finally {
await browser.close();
}
};
}
module.exports = remarkMermaid;