Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions PluginPack/client/copy-mermaid.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copies the prebuilt mermaid UMD bundle into dist/js, so the packaging step
// (see .github/workflows/build.yml) deploys it to assets/markdown next to
// markdown.min.js, the same way the highlightjs runtime is handled.
//
// Why a copy and not a webpack entry (as used for highlightjs): mermaid loads
// its diagram types with dynamic import(), so bundling it would emit lazy chunks
// that the runtime script loader never fetches. The published UMD build already
// inlines everything and assigns window.mermaid, so a plain copy is correct.
const fs = require('fs');
const path = require('path');

const candidates = [
path.resolve(__dirname, '../node_modules/mermaid/dist/mermaid.min.js'),
path.resolve(__dirname, 'node_modules/mermaid/dist/mermaid.min.js'),
];

const src = candidates.find(p => fs.existsSync(p));
if (!src) {
console.error('mermaid.min.js not found in node_modules. Run "yarn install" first.');
process.exit(1);
}

const outDir = path.resolve(__dirname, '../dist/js');
fs.mkdirSync(outDir, { recursive: true });
fs.copyFileSync(src, path.join(outDir, 'mermaid.min.js'));
console.log('copied mermaid.min.js to dist/js/mermaid.min.js');
1 change: 1 addition & 0 deletions PluginPack/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"highlight.js": "^11.11.1",
"markdown-it": "^14.1.0",
"markdown-it-emoji": "^3.0.0",
"mermaid": "^11.4.0",
"qrcode": "^1.5.4"
},
"browserslist": {
Expand Down
74 changes: 74 additions & 0 deletions PluginPack/client/src/plugins/markdown-it-mermaid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import MarkdownIt from 'markdown-it'
import { MarkdownRenderContext } from '../Misc/MarkdownRenderContext'
import { importCss, importScript } from '../Misc/DynamicLoad'

// Mermaid renders on the client. Unlike plantuml (@mdit/plugin-plantuml, which
// produces a server-side <img>), the "mermaid" fenced code block is turned into a
// <pre class="mermaid"> holder and the mermaid runtime converts it to SVG after
// the HTML is inserted into the DOM. markdown.ts runs MarkdownRenderContext.postRender
// callbacks right after "container.innerHTML = html", which is the hook used here.

// mermaid is initialized once per session (theme included).
let initialized = false;

// The dark stylesheet is chosen in C# (Settings.DarkModeCssFile, default
// ".../markdown/markdown-dark.css") and injected as a <link>. There is no DOM theme
// class, so the theme is inferred from the injected stylesheet. A custom dark CSS
// whose name does not contain "markdown-dark" falls back to the light theme.
function isDarkTheme(): boolean {
const links = document.querySelectorAll('link[rel="stylesheet"]');
for (const link of Array.from(links)) {
if (/markdown-dark/i.test((link as HTMLLinkElement).href || '')) {
return true;
}
}
return false;
}

async function runMermaid() {
const nodes = document.querySelectorAll('pre.mermaid');
if (nodes.length === 0) {
return;
}
// importCss/importScript are idempotent, so the runtime is fetched only for the
// first previewed document that actually contains a diagram.
importCss(["markdown/plugin-mermaid/mermaid.css"]);
await importScript(["markdown/mermaid.min.js"]);

// The prebuilt UMD bundle assigns window.mermaid.
const mermaid = (window as any).mermaid;
if (mermaid == null) {
return;
}
if (!initialized) {
mermaid.initialize({
startOnLoad: false,
theme: isDarkTheme() ? 'dark' : 'default'
});
initialized = true;
}
// suppressErrors keeps one invalid diagram from breaking the whole preview.
await mermaid.run({ querySelector: 'pre.mermaid', suppressErrors: true });
}

export default function markdownItMermaid(md: MarkdownIt) {
const context = MarkdownRenderContext;
const defaultFence = md.renderer.rules.fence!;
// context.postRender is reset per render (markdown.ts) and this plugin is
// registered per render (a new MarkdownIt each time), so schedule at most one
// run per render, and only when a mermaid block is actually present.
let scheduled = false;

md.renderer.rules.fence = function (tokens, idx, options, env, self) {
const token = tokens[idx];
const info = token.info.trim().split(/\s+/g)[0];
if (info !== 'mermaid') {
return defaultFence(tokens, idx, options, env, self);
}
if (!scheduled) {
scheduled = true;
context.postRender.push(runMermaid);
}
return `<pre class="mermaid">${md.utils.escapeHtml(token.content)}</pre>`;
};
}
4 changes: 4 additions & 0 deletions PluginPack/client/src/plugins/markdown-it-pluginpack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import MarkdownIt from 'markdown-it'
import markdownItTaskLists from './markdown-it-task-lists'
import markdownItEmbedQrcode from './markdown-it-embed-qrcode'
import markdownItEmbedPano360 from './markdown-it-embed-pano360'
import markdownItMermaid from './markdown-it-mermaid'
import { importCss, importScript } from '../Misc/DynamicLoad'

export async function markdownItPluginPack(enabled: string[], md: MarkdownIt) {
Expand Down Expand Up @@ -128,6 +129,9 @@ export async function markdownItPluginPack(enabled: string[], md: MarkdownIt) {
md.use(plantuml, {
});
}
if (enabled.includes("mermaid")) {
md.use(markdownItMermaid);
}
if (enabled.includes("ruby")) {
md.use(ruby, {});
}
Expand Down
3 changes: 2 additions & 1 deletion PluginPack/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"scripts": {
"build:app": "webpack --config ./client/app.webpack.config.js",
"build:highlightjs": "webpack --config ./client/highlightjs.webpack.config.js",
"build:all": "concurrently \"yarn build:highlightjs\" \"yarn build:app\""
"build:mermaid": "node ./client/copy-mermaid.js",
"build:all": "concurrently \"yarn build:highlightjs\" \"yarn build:app\" \"yarn build:mermaid\""
},
"dependencies": {},
"devDependencies": {
Expand Down
Loading
Loading