-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
/
mermaid.ts
80 lines (75 loc) · 3.12 KB
/
mermaid.ts
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
import { exec } from 'node:child_process';
import { createHash } from 'node:crypto';
import { readFile, writeFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import { promisify } from 'node:util';
import type { Plugin } from 'vite';
import { mkdir } from '../../src/utils/fs';
import { getFilesInDirectory } from './helpers';
const execPromise = promisify(exec);
const graphsDirectory = new URL('graphs/', import.meta.url);
const mermaidRegExp = /^```mermaid\n([\S\s]*?)\n```/gm;
const greaterThanRegExp = />/g;
const styleTagRegExp = /<style>[\S\s]*?<\/style>/gm;
const configFileURL = new URL('mermaid.config.json', import.meta.url);
const puppeteerConfigFileURL = new URL('puppeteer-config.json', import.meta.url);
export function renderMermaidGraphsPlugin(): Plugin {
const existingGraphFileNamesPromise = mkdir(graphsDirectory, { recursive: true })
.then(() => getFilesInDirectory(graphsDirectory))
.then(files => new Set(files.filter(name => name.endsWith('.svg'))));
const existingGraphsByName = new Map<string, Promise<string>>();
async function renderGraph(codeBlock: string, outFile: string) {
const existingGraphFileNames = await existingGraphFileNamesPromise;
const outFileURL = new URL(outFile, graphsDirectory);
if (!existingGraphFileNames.has(outFile)) {
const inFileURL = new URL(`${outFile}.mmd`, graphsDirectory);
await writeFile(inFileURL, codeBlock);
const { stdout, stderr } = await execPromise(
`npx mmdc --configFile ${fileURLToPath(
configFileURL
)} --puppeteerConfigFile ${fileURLToPath(puppeteerConfigFileURL)} --input ${fileURLToPath(
inFileURL
)} --output ${fileURLToPath(outFileURL)}`
);
if (stderr.trim()) console.log(stderr.trim());
if (stdout.trim()) console.log(stdout.trim());
}
const outFileContent = await readFile(outFileURL, 'utf8');
// Styles need to be placed top-level, so we extract them and then
// prepend them, separated with a line-break
const extractedStyles: string[] = [];
const baseGraph = outFileContent
// We need to replace some HTML entities
.replace(greaterThanRegExp, '>')
.replace(styleTagRegExp, styleTag => {
extractedStyles.push(styleTag);
return '';
});
console.log('Extracted styles from mermaid chart:', extractedStyles.length);
return `${extractedStyles.join('')}\n${baseGraph}`;
}
return {
enforce: 'pre',
name: 'render-mermaid-charts',
async transform(code, id) {
if (id.endsWith('.md')) {
const renderedGraphs: string[] = [];
const mermaidCodeBlocks: string[] = [];
let match: RegExpExecArray | null = null;
while ((match = mermaidRegExp.exec(code)) !== null) {
mermaidCodeBlocks.push(match[1]);
}
await Promise.all(
mermaidCodeBlocks.map(async (codeBlock, index) => {
const outFile = `${createHash('sha256').update(codeBlock).digest('base64url')}.svg`;
if (!existingGraphsByName.has(outFile)) {
existingGraphsByName.set(outFile, renderGraph(codeBlock, outFile));
}
renderedGraphs[index] = await existingGraphsByName.get(outFile)!;
})
);
return code.replace(mermaidRegExp, () => renderedGraphs.shift()!);
}
}
};
}