mirror of
https://gricad-gitlab.univ-grenoble-alpes.fr/tienyoun/presentations.git
synced 2026-09-26 13:31:01 +00:00
405 lines
18 KiB
JavaScript
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();
|
|
}
|
|
};
|