Files
presentations/2026/celery2/assets/mermaid.js
T
2026-09-24 16:39:17 +02:00

405 lines
18 KiB
JavaScript

/*
Plugin for embedding mermaid.js diagrams in reveal.js presentations
GitHub: https://github.com/dainiak/revealjs-plugins/
Author: Alex Dainiak
Web: www.dainiak.com
Email: dainiak@gmail.com
Processing rules can be added to post-process the diagram SVG DOM subtree.
One can set the id, set or remove classes, or set attributes on the SVG elements. In any combinations:
%%% someCssSelector -> #cssIdToSet .css-class-to-set !.class-to-remove [some-property-to-set=newValue]
If the `->` arrow is used then the modifications are applied to the element itself.
If the `^->` arrow is used then the element is wrapped with a g element and modifications are applied to this g.
If the `_->` arrow is used then a single g is added as the only new child of the element and the modifications are applied to this g. All the former children are moved inside this g.
You can use [n] right after the selector to target a specific element in the list of matching elements. Works even when `:nth-child` does not.
Examples:
%% Apply .fragment class to the first of the mermaid nodes:
%%% g.node[1] -> .fragment
%% Make the "Start" node a fragment step and red
%%% rect[id*="A"] _-> .fragment .fade-up [data-fragment-index=1]
%% Remove default class and add new one
%%% #D -> !.node .end-state
*/
const RevealMermaid = {
id: 'mermaid',
init: async (reveal) => {
const katexVersion = '0.16.27';
const mermaidVersion = '11.14.0';
let options = reveal.getConfig().mermaid || {};
options = {
mathInLabels: options.mathInLabels !== false,
urls: options.urls || {
mermaid: options.urls && options.urls.mermaid || `./assets/mermaid.min.js`,
katex: options.urls && options.urls.katex || `https://cdn.jsdelivr.net/npm/katex@${katexVersion}/dist/katex.min.js`,
katexCss: options.urls && options.urls.katexCss || `https://cdn.jsdelivr.net/npm/katex@${katexVersion}/dist/katex.min.css`,
},
selectors: {
container: options.selectors && options.selectors.container || '[data-mermaid]',
script: options.selectors && options.selectors.script || 'script[type="text/mermaid"]',
},
overflowVisible: options.overflowVisible === undefined ? true : options.overflowVisible,
mermaidInit: options.mermaidInit || {
startOnLoad: false,
theme: 'auto',
suppressErrorRendering: true
},
iconPacks: options.iconPacks || [],
css: {
enabled: options?.css?.enabled !== false,
cssIndices: options?.css?.cssIndices !== false,
indexClassPrefix: options?.css?.indexClassPrefix || 'fragidx-',
debug: options?.css?.debug || false
}
};
let scriptsToLoad = [
{
url: options.urls.mermaid,
condition:
!window.mermaid
&& !document.querySelector(`script[src="${options.mermaidUrl}"]`)
}, {
url: options.urls.katex,
condition:
options.mathInLabels
&& !window.katex
&& !document.querySelector(`script[src="${options.katexUrl}"]`)
},{
url: options.urls.katexCss,
type: 'text/css',
condition:
!window.vegaEmbed
&& !window.katex
}
];
function loadScript(params) {
return new Promise((resolve) => {
if(params.condition !== undefined
&& !(params.condition === true || typeof params.condition == 'function' && params.condition.call())) {
return resolve();
}
if( params.type === undefined )
params.type = (params.url && params.url.match(/\.css[^.]*$/)) ? 'text/css' : 'text/javascript';
let element;
if( params.type === 'text/css' ){
if( params.content ){
element = document.createElement('style');
element.textContent = params.content;
}
else {
element = document.createElement('link');
element.rel = 'stylesheet';
element.type = 'text/css';
element.href = params.url;
}
}
else {
element = document.createElement('script');
element.type = params.type || 'text/javascript';
if( params.content )
element.textContent = params.content;
else
element.src = params.url;
}
if(params.content){
document.querySelector('head').appendChild(element);
resolve();
}
else {
element.onload = resolve;
document.querySelector( 'head' ).appendChild(element);
}
});
}
if(!reveal.getSlidesElement().querySelector(options.selectors.container))
return;
// Load mermaid, katex, and katex CSS in parallel (they are independent)
await Promise.all(scriptsToLoad.map(s => loadScript(s)));
if((options.mermaidInit.theme || 'auto') === 'auto') {
options.mermaidInit.theme = 'default';
if(document.querySelector(
'[href*="black.css"],[href*="league.css"],[href*="night.css"],[href*="moon.css"],[href*="dracula.css"],[href*="blood.css"]'
)) {
options.mermaidInit.theme = 'dark';
}
}
window.mermaid.initialize(options.mermaidInit);
if(options.iconPacks) {
for(let packName in options.iconPacks) {
window.mermaid.registerIconPacks([
{
name: packName,
loader: () =>
fetch(options.iconPacks[packName]).then((res) => res.json()),
},
]);
}
}
function dedentAndTrim(str) {
const TAB_WIDTH = 4;
const lines = str.split(/\r?\n/);
let minIndent = Infinity;
for (const line of lines) {
if (!line.trim()) continue;
let width = 0;
for (const char of line) {
if (char === ' ') width++;
else if (char === '\t') width += TAB_WIDTH;
else break;
}
if (width < minIndent) minIndent = width;
}
if (minIndent === Infinity) minIndent = 0;
return lines.map(line => {
let width = 0, i = 0;
for (; i < line.length; i++) {
if (width >= minIndent) break;
const char = line[i];
if (char === ' ') width++;
else if (char === '\t') width += TAB_WIDTH;
else break;
}
return ' '.repeat(Math.max(0, width - minIndent)) + line.slice(i);
}).join('\n').trim();
}
const mermaidContainers = Array.from(reveal.getSlidesElement().querySelectorAll(options.selectors.container));
const renderPromises = mermaidContainers.map(async (mermaidContainer) => {
const parent = mermaidContainer.parentNode;
const newDiv = document.createElement('div');
parent.insertBefore(newDiv, mermaidContainer);
const mermaidScript = mermaidContainer.querySelector("script");
if(mermaidScript) {
mermaidScript.textContent = dedentAndTrim(mermaidScript.textContent);
}
if(!mermaidContainer.id)
mermaidContainer.id = `mermaid-${Math.floor(Math.random() * 1000000)}`;
let graphDefinition;
const mermaidAttrValue = mermaidContainer.getAttribute('data-mermaid');
if(mermaidAttrValue && mermaidAttrValue.trim() !== '') {
try {
const response = await fetch(mermaidAttrValue.trim());
if(!response.ok)
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
graphDefinition = await response.text();
} catch(error) {
console.warn(`Mermaid Plugin: Failed to fetch diagram from "${mermaidAttrValue}":`, error);
mermaidContainer.remove();
return;
}
} else {
const scriptEl = mermaidContainer.querySelector(options.selectors.script);
if(!scriptEl) {
console.warn('Mermaid Plugin: No script element or data-mermaid path found in container', mermaidContainer);
mermaidContainer.remove();
return;
}
graphDefinition = scriptEl.innerHTML;
}
let renderRules = [];
// 1. Extract Custom CSS Logic
// Regex matches: selector + (one of ->, ^->, _->) + assignment
const ruleRegex = /^\s*%%%\s+(.+?)\s+([\^_]?->)\s+(.+?)\s*$/gm;
graphDefinition = graphDefinition.replace(ruleRegex, (match, selector, arrow, assignment) => {
renderRules.push({
selector,
assignment,
type: arrow
});
return match;
});
// 2. Handle Math in Labels
if(options.mathInLabels) {
graphDefinition = graphDefinition.replace(/\\([(\[]).*?\\([)\]])/g, (s) => {
let output = window.katex.renderToString(s.substring(2, s.length - 2), {
output: 'html',
displayMode: s[1] === '['
});
return output.replaceAll('"', "'");
});
}
// 3. Render Mermaid
try {
await window.mermaid.parse(graphDefinition, {suppressErrors: false});
} catch (error) {
console.warn(`Mermaid diagram failed to parse:\n\n${graphDefinition}\n\nError: `, error);
mermaidContainer.remove();
return;
}
let svg;
try {
({ svg } = await window.mermaid.render(mermaidContainer.id, graphDefinition));
} catch (error) {
console.warn(`Mermaid diagram failed to render:\n\n${graphDefinition}\n\nError: `, error);
mermaidContainer.remove();
return;
}
newDiv.outerHTML = svg;
const svgElement = parent.querySelector(`#${mermaidContainer.id}`);
// Copy classes and styles from container
svgElement.classList += " " + mermaidContainer.classList;
const hasStretchClass = mermaidContainer.classList.contains("r-stretch");
if(hasStretchClass) {
svgElement.style.width = "";
svgElement.style.height = "";
svgElement.style.maxWidth = "";
svgElement.style.maxHeight = "";
svgElement.style.minWidth = "";
svgElement.style.minHeight = "";
}
for (const prop of mermaidContainer.style) {
const value = mermaidContainer.style.getPropertyValue(prop);
if (value && value.trim() !== '') {
svgElement.style.setProperty(prop, value);
}
}
mermaidContainer.remove();
// 4. Handle Overflow
if(options.overflowVisible) {
let selector = options.overflowVisible === '*' ? '*' : 'foreignObject';
svgElement.querySelectorAll(selector).forEach((obj) => {
obj.setAttribute('overflow', 'visible');
});
}
// 5. Apply Custom CSS Logic
if (options.css.enabled && renderRules.length > 0) {
const svgNS = "http://www.w3.org/2000/svg";
renderRules.forEach(({ selector, assignment, type }) => {
try {
selector = selector.trim();
let indexPart = null;
if(/.*\[\d+]$/.exec(selector)) {
const digitPart = /\[\d+]$/.exec(selector)[0];
indexPart = parseInt(digitPart.match(/\d+/)[0]);
selector = selector.replace(/\[\d+]$/, '');
}
const targets = svgElement.querySelectorAll(selector);
targets.forEach((el, idx) => {
if(indexPart !== null && idx + 1 !== indexPart)
return;
if(options.css.debug) {
console.log(selector, idx + 1, type, el)
}
let modificationTarget = el;
// --- LOGIC FOR ARROW TYPES ---
if (type === '^->') {
// 1. OUTER WRAPPER
// Wraps the element in a new <g> and modifies the <g>
const wrapper = document.createElementNS(svgNS, 'g');
el.parentNode.insertBefore(wrapper, el);
wrapper.appendChild(el);
modificationTarget = wrapper;
} else if (type === '_->') {
// 2. INNER WRAPPER
// Creates a new <g> inside the element, moves all children into it,
// and modifies that new internal <g>
const innerGroup = document.createElementNS(svgNS, 'g');
// Move all existing children of 'el' into 'innerGroup'
while (el.firstChild) {
innerGroup.appendChild(el.firstChild);
}
el.appendChild(innerGroup);
modificationTarget = innerGroup;
}
// Default '->' falls through here, keeping modificationTarget = el
const tokens = assignment.match(/(\[.+?\])|(\S+)/g) || [];
tokens.forEach(token => {
if (token.startsWith('.')) {
modificationTarget.classList.add(token.substring(1));
} else if (token.startsWith('!.')) {
modificationTarget.classList.remove(token.substring(2));
} else if (token.startsWith('#')) {
modificationTarget.id = token.substring(1);
} else if (token.startsWith('[')) {
const content = token.substring(1, token.length - 1);
const eqIndex = content.indexOf('=');
if (eqIndex > -1) {
const key = content.substring(0, eqIndex).trim();
let val = content.substring(eqIndex + 1).trim();
if ((val.startsWith('"') && val.endsWith('"')) ||
(val.startsWith("'") && val.endsWith("'"))) {
val = val.substring(1, val.length - 1);
}
modificationTarget.setAttribute(key, val);
} else {
modificationTarget.setAttribute(content, '');
}
}
});
});
} catch (e) {
console.warn(`Mermaid Plugin: Failed to apply selector "${selector}".`, e);
}
});
}
if(options.css.enabled && options.css.cssIndices) {
const cssSelector = '[class*="' + options.css.indexClassPrefix + '"]';
const fragmentsWithCssIndex = svgElement.querySelectorAll(cssSelector);
for (let fragment of fragmentsWithCssIndex) {
let s = fragment.getAttribute('class');
s = s.substring(
s.indexOf(options.css.indexClassPrefix) + options.css.indexClassPrefix.length
);
s = s.substring(0, Math.max(s.indexOf(' '), s.length));
fragment.classList.add('fragment');
fragment.setAttribute('data-fragment-index', s);
}
}
if(hasStretchClass)
svgElement.style.height = '100%';
});
await Promise.all(renderPromises);
reveal.layout();
}
};