enhance: update mermaid to v12 (#39331)

Update to [mermaid
v12](https://github.com/mermaid-js/mermaid/releases#release-mermaid@12.0.0),
ELK is now the default layout so the plugin and all supporting code is
gone.

Layout switching to previous `dagre` layout via frontmatter works as
expected. Diagrams use the new `neo` default look which renders them
slightly different (smaller boxes) then the `classic` look that was
default in v11.

The `neo` look got some tweaks to remove shadows and gradient to look
better. Also did some related refactors and mermaid dragging now works
on touch devices too.
This commit is contained in:
silverwind
2026-09-18 01:28:51 +02:00
committed by GitHub
parent c0c573f7af
commit 2b10f77e42
7 changed files with 107 additions and 278 deletions
+1 -2
View File
@@ -27,7 +27,6 @@
"@github/text-expander-element": "2.9.4",
"@lezer/highlight": "1.2.3",
"@mcaptcha/vanilla-glue": "0.1.0-rc2",
"@mermaid-js/layout-elk": "0.2.3",
"@primer/octicons": "19.36.0",
"@replit/codemirror-indentation-markers": "6.5.3",
"@replit/codemirror-lang-nix": "6.0.1",
@@ -51,7 +50,7 @@
"jquery": "4.0.0",
"js-yaml": "5.4.1",
"katex": "0.18.7",
"mermaid": "11.17.2",
"mermaid": "12.0.0",
"online-3d-viewer": "0.18.0",
"pdfobject": "2.3.1",
"postcss": "8.5.28",
+61 -40
View File
@@ -229,9 +229,6 @@ importers:
'@mcaptcha/vanilla-glue':
specifier: 0.1.0-rc2
version: 0.1.0-rc2(supports-color@10.2.2)
'@mermaid-js/layout-elk':
specifier: 0.2.3
version: 0.2.3(mermaid@11.17.2)
'@primer/octicons':
specifier: 19.36.0
version: 19.36.0
@@ -302,8 +299,8 @@ importers:
specifier: 0.18.7
version: 0.18.7
mermaid:
specifier: 11.17.2
version: 11.17.2
specifier: 12.0.0
version: 12.0.0
online-3d-viewer:
specifier: 0.18.0
version: 0.18.0
@@ -520,9 +517,21 @@ packages:
'@cacheable/utils@2.5.0':
resolution: {integrity: sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==}
'@chevrotain/cst-dts-gen@11.1.2':
resolution: {integrity: sha512-XTsjvDVB5nDZBQB8o0o/0ozNelQtn2KrUVteIHSlPd2VAV2utEb6JzyCJaJ8tGxACR4RiBNWy5uYUHX2eji88Q==}
'@chevrotain/gast@11.1.2':
resolution: {integrity: sha512-Z9zfXR5jNZb1Hlsd/p+4XWeUFugrHirq36bKzPWDSIacV+GPSVXdk+ahVWZTwjhNwofAWg/sZg58fyucKSQx5g==}
'@chevrotain/regexp-to-ast@11.1.2':
resolution: {integrity: sha512-nMU3Uj8naWer7xpZTYJdxbAs6RIv/dxYzkYU8GSwgUtcAAlzjcPfX1w+RKRcYG8POlzMeayOQ/znfwxEGo5ulw==}
'@chevrotain/types@11.1.2':
resolution: {integrity: sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==}
'@chevrotain/utils@11.1.2':
resolution: {integrity: sha512-4mudFAQ6H+MqBTfqLmU7G1ZwRzCLfJEooL/fsF6rCX5eePMbGhoy5n4g+G4vlh2muDcsCTJtL+uKbOzWxs5LHA==}
'@citation-js/core@0.7.21':
resolution: {integrity: sha512-Vobv2/Yfnn6C6BVO/pvj7madQ7Mfzl83/jAWwixbemGF6ZThhGMz8++FD9hWHyHXDMYuLGa6fK68c2VsolZmTA==}
engines: {node: '>=16.0.0'}
@@ -1060,13 +1069,9 @@ packages:
'@mcaptcha/vanilla-glue@0.1.0-rc2':
resolution: {integrity: sha512-LDjn9lrKioJ3zwaQOfql7PXsnxCAHg7b1rPw7G0OxpvVE7xLB/a40SHfIIiocce2VS9TPI4MbcKm5pcuy8fU5g==}
'@mermaid-js/layout-elk@0.2.3':
resolution: {integrity: sha512-m46RVGfvKK1PSMdAFGDz2nksI4jusEkFxvf0lb48bYWmqLQxZpjtpruiw29MbOy9rlWJtuMf9EzG9fAF8CVZbg==}
peerDependencies:
mermaid: ^11.0.2
'@mermaid-js/parser@1.2.1':
resolution: {integrity: sha512-n12NohV3mrUyUL2o93IgG/ifeW9FTyeJn3zDxkhwa8MJ9Fxg3HQMlA3RiGmD/3UnJvheztkjjQAjA2T4LmUcpw==}
'@mermaid-js/parser@2.0.0':
resolution: {integrity: sha512-K8BeapFUfrfxbRAUQAG5oBOCwo1+bNWzaVnPxTCutkfrTBn6T/j91FIhqxWJ32SUeQ9T3iy1zcmPZ5ROZEvDrg==}
engines: {node: '>=22.12.0'}
'@napi-rs/wasm-runtime@1.1.6':
resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==}
@@ -2047,6 +2052,9 @@ packages:
peerDependencies:
chart.js: '>=3.2.0'
chevrotain@11.1.2:
resolution: {integrity: sha512-opLQzEVriiH1uUQ4Kctsd49bRoFDXGGSC4GUqj7pGyxM3RehRhvTlZJc1FL/Flew2p5uwxa1tUDWKzI4wNM8pg==}
chokidar@3.6.0:
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
engines: {node: '>= 8.10.0'}
@@ -2464,8 +2472,8 @@ packages:
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
engines: {node: '>= 4'}
dompurify@3.4.7:
resolution: {integrity: sha512-2jBxDJY4RR06tQNy4w5FlFH7kfxsQZlufd0sbv+chfHCxeJwrFw2baUDsSwvBISD4K4RDbd0PTfy3uNXsR6siA==}
dompurify@3.4.15:
resolution: {integrity: sha512-EUBjM+B+lkDE41iE82DDSCfkoPGfXx8IxFxPMjNzm/Uk4xDet77rTN9wqlxlVg71kK7XGuUMv6wUxJUwwv+Xyw==}
domutils@3.2.2:
resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
@@ -2766,9 +2774,6 @@ packages:
fast-uri@3.1.2:
resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==}
fastdom@1.0.12:
resolution: {integrity: sha512-LB+xjSTEbjHE1cWsxu+tN2Xqr1kpi+V9aADI7sVM5ZMaXyYGPHULQMzpJMYqOTULK/73pUkWVzzObFRBkPr+hg==}
fastest-levenshtein@1.0.16:
resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==}
engines: {node: '>= 4.9.1'}
@@ -3399,6 +3404,9 @@ packages:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'}
lodash-es@4.17.23:
resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==}
lodash-es@4.18.1:
resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==}
@@ -3469,8 +3477,9 @@ packages:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
mermaid@11.17.2:
resolution: {integrity: sha512-V6K3C8EBdEsPFZXSKMJe6ppQOENxuHARr9GvHX4hh47lAbhMRD9qf4oEK7LoaRQxULMa80/qt5gHO73aCleBBg==}
mermaid@12.0.0:
resolution: {integrity: sha512-/wQXC9iBxoGV8p3erbvaXs9h77VyLDBH6GdayVjj3hEcSQhFU4N1WUhUppotCEqlIxI2pRMwjwBSwTB1MfZBgQ==}
engines: {node: '>=22.12.0'}
micromark-core-commonmark@2.0.3:
resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==}
@@ -4131,9 +4140,6 @@ packages:
resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==}
engines: {node: '>= 0.4'}
strictdom@1.0.1:
resolution: {integrity: sha512-cEmp9QeXXRmjj/rVp9oyiqcvyocWab/HaoN4+bwFeZ7QzykJD6L3yD4v12K1x0tHpqRqVpJevN3gW7kyM39Bqg==}
string-width@4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'}
@@ -4692,8 +4698,23 @@ snapshots:
hashery: 1.5.1
keyv: 5.6.0
'@chevrotain/cst-dts-gen@11.1.2':
dependencies:
'@chevrotain/gast': 11.1.2
'@chevrotain/types': 11.1.2
lodash-es: 4.17.23
'@chevrotain/gast@11.1.2':
dependencies:
'@chevrotain/types': 11.1.2
lodash-es: 4.17.23
'@chevrotain/regexp-to-ast@11.1.2': {}
'@chevrotain/types@11.1.2': {}
'@chevrotain/utils@11.1.2': {}
'@citation-js/core@0.7.21':
dependencies:
'@citation-js/date': 0.5.1
@@ -5374,13 +5395,7 @@ snapshots:
- supports-color
- utf-8-validate
'@mermaid-js/layout-elk@0.2.3(mermaid@11.17.2)':
dependencies:
d3: 7.9.0
elkjs: 0.9.3
mermaid: 11.17.2
'@mermaid-js/parser@1.2.1':
'@mermaid-js/parser@2.0.0':
dependencies:
'@chevrotain/types': 11.1.2
@@ -6428,6 +6443,15 @@ snapshots:
chart.js: 4.5.1
hammerjs: 2.0.8
chevrotain@11.1.2:
dependencies:
'@chevrotain/cst-dts-gen': 11.1.2
'@chevrotain/gast': 11.1.2
'@chevrotain/regexp-to-ast': 11.1.2
'@chevrotain/types': 11.1.2
'@chevrotain/utils': 11.1.2
lodash-es: 4.17.23
chokidar@3.6.0:
dependencies:
anymatch: 3.1.3
@@ -6853,7 +6877,7 @@ snapshots:
dependencies:
domelementtype: 2.3.0
dompurify@3.4.7:
dompurify@3.4.15:
optionalDependencies:
'@types/trusted-types': 2.0.7
@@ -7289,10 +7313,6 @@ snapshots:
fast-uri@3.1.2: {}
fastdom@1.0.12:
dependencies:
strictdom: 1.0.1
fastest-levenshtein@1.0.16: {}
fastq@1.20.1:
@@ -7927,6 +7947,8 @@ snapshots:
dependencies:
p-locate: 5.0.0
lodash-es@4.17.23: {}
lodash-es@4.18.1: {}
lodash.truncate@4.4.2: {}
@@ -8009,13 +8031,14 @@ snapshots:
merge2@1.4.1: {}
mermaid@11.17.2:
mermaid@12.0.0:
dependencies:
'@braintree/sanitize-url': 7.1.2
'@iconify/utils': 3.1.3
'@mermaid-js/parser': 1.2.1
'@mermaid-js/parser': 2.0.0
'@types/d3': 7.4.3
'@upsetjs/venn.js': 2.0.0
chevrotain: 11.1.2
cytoscape: 3.34.1
cytoscape-cose-bilkent: 4.1.0(cytoscape@3.34.1)
cytoscape-fcose: 2.2.0(cytoscape@3.34.1)
@@ -8023,9 +8046,9 @@ snapshots:
d3-sankey: 0.12.3
dagre-d3-es: 7.0.14
dayjs: 1.11.23
dompurify: 3.4.7
dompurify: 3.4.15
elkjs: 0.9.3
es-toolkit: 1.47.0
fastdom: 1.0.12
katex: 0.16.47
khroma: 2.1.0
marked: 16.4.2
@@ -8799,8 +8822,6 @@ snapshots:
es-errors: 1.3.0
internal-slot: 1.1.0
strictdom@1.0.1: {}
string-width@4.2.3:
dependencies:
emoji-regex: 8.0.0
+1 -1
View File
@@ -427,7 +427,7 @@ html[data-gitea-theme-dark="false"] .markup img[src*="#gh-dark-mode-only"] {
display: block;
border: none;
width: 100%;
height: var(--height-loading); /* actual height is set in JS after loading */
height: var(--height-loading); /* actual height is set in JS */
overflow: hidden;
color-scheme: normal; /* match the value inside the iframe to allow it to become transparent */
}
+1
View File
@@ -55,6 +55,7 @@ form.single-button-form.is-loading .button {
.markup pre.is-loading,
.editor-loading.is-loading {
height: var(--height-loading);
overflow: hidden;
}
.markup .is-loading > * {
-59
View File
@@ -1,59 +0,0 @@
import {sourceNeedsElk} from './mermaid.ts';
import {dedent} from '../utils/testhelper.ts';
test('MermaidConfigLayoutCheck', () => {
expect(sourceNeedsElk(dedent(`
flowchart TB
elk --> B
`))).toEqual(false);
expect(sourceNeedsElk(dedent(`
---
config:
layout : elk
---
flowchart TB
A --> B
`))).toEqual(true);
expect(sourceNeedsElk(dedent(`
---
config:
layout: elk.layered
---
flowchart TB
A --> B
`))).toEqual(true);
expect(sourceNeedsElk(`
%%{ init : { "flowchart": { "defaultRenderer": "elk" } } }%%
flowchart TB
A --> B
`)).toEqual(true);
expect(sourceNeedsElk(dedent(`
---
config:
layout: 123
---
%%{ init : { "class": { "defaultRenderer": "elk.any" } } }%%
flowchart TB
A --> B
`))).toEqual(true);
expect(sourceNeedsElk(`
%%{init:{
"layout" : "elk.layered"
}}%%
flowchart TB
A --> B
`)).toEqual(true);
expect(sourceNeedsElk(`
%%{ initialize: {
'layout' : 'elk.layered'
}}%%
flowchart TB
A --> B
`)).toEqual(true);
});
+43 -160
View File
@@ -1,203 +1,98 @@
import {isDarkTheme} from '../utils.ts';
import {displayError} from './common.ts';
import {createElementFromAttrs, createElementFromHTML, queryElems} from '../utils/dom.ts';
import {createElementFromAttrs, createElementFromHTML, isPlainClick, queryElems} from '../utils/dom.ts';
import {html, htmlRaw} from '../utils/html.ts';
import {load as loadYaml} from 'js-yaml';
import type {MermaidConfig} from 'mermaid';
import {svg} from '../svg.ts';
const {mermaidMaxSourceCharacters} = window.config;
function getIframeCss(): string {
return `
html, body { height: 100%; }
body { margin: 0; padding: 0; overflow: hidden; }
#mermaid { display: block; margin: 0 auto; }
`;
}
function isSourceTooLarge(source: string) {
return mermaidMaxSourceCharacters >= 0 && source.length > mermaidMaxSourceCharacters;
}
function parseYamlInitConfig(source: string): MermaidConfig | null {
// ref: https://github.com/mermaid-js/mermaid/blob/develop/packages/mermaid/src/diagram-api/regexes.ts
const yamlFrontMatterRegex = /^---\s*[\n\r](.*?)[\n\r]---\s*[\n\r]+/s;
const frontmatter = (yamlFrontMatterRegex.exec(source) || [])[1];
if (!frontmatter) return null;
try {
return (loadYaml(frontmatter) as {config: MermaidConfig})?.config;
} catch {
console.error('invalid or unsupported mermaid init YAML config', frontmatter);
}
return null;
}
function parseJsonInitConfig(source: string): MermaidConfig | null {
// https://mermaid.js.org/config/directives.html#declaring-directives
// Do as dirty as mermaid does: https://github.com/mermaid-js/mermaid/blob/develop/packages/mermaid/src/utils.ts
// It can even accept invalid JSON string like:
// %%{initialize: { 'logLevel': 'fatal', "theme":'dark', 'startOnLoad': true } }%%
const jsonInitConfigRegex = /%%\{\s*(init|initialize)\s*:\s*(.*?)\}%%/s;
const jsonInitText = (jsonInitConfigRegex.exec(source) || [])[2];
if (!jsonInitText) return null;
try {
const processed = jsonInitText.trim().replace(/'/g, '"');
return JSON.parse(processed);
} catch {
console.error('invalid or unsupported mermaid init JSON config', jsonInitText);
}
return null;
}
function configValueIsElk(layoutOrRenderer: string | undefined) {
if (typeof layoutOrRenderer !== 'string') return false;
return layoutOrRenderer === 'elk' || layoutOrRenderer.startsWith('elk.');
}
function configContainsElk(config: MermaidConfig | null) {
if (!config) return false;
// Check the layout from the following properties:
// * config.layout
// * config.{any-diagram-config}.defaultRenderer
// Although only a few diagram types like "flowchart" support "defaultRenderer",
// as long as there is no side effect, here do a general check for all properties of "config", for ease of maintenance
return configValueIsElk(config.layout) || Object.values(config).some((diagCfg) => configValueIsElk(diagCfg?.defaultRenderer));
}
export function sourceNeedsElk(source: string) {
if (isSourceTooLarge(source)) return false;
const configYaml = parseYamlInitConfig(source), configJson = parseJsonInitConfig(source);
return configContainsElk(configYaml) || configContainsElk(configJson);
}
async function loadMermaid(needElkRender: boolean) {
const mermaidPromise = import('mermaid');
const elkPromise = needElkRender ? import('@mermaid-js/layout-elk') : null;
const results = await Promise.all([mermaidPromise, elkPromise]);
return {
mermaid: results[0].default,
elkLayouts: results[1]?.default,
};
}
function initMermaidViewController(viewController: Element, dragElement: SVGSVGElement) {
let inited = false, isDragging = false;
let currentScale = 1, initLeft = 0, lastLeft = 0, lastTop = 0, lastPageX = 0, lastPageY = 0;
const resetView = () => {
currentScale = 1;
lastLeft = initLeft;
lastTop = 0;
dragElement.style.left = `${lastLeft}px`;
dragElement.style.top = `${lastTop}px`;
dragElement.style.position = 'absolute';
dragElement.style.margin = '0';
};
const initAbsolutePosition = () => {
if (inited) return;
// if we need to drag or zoom, use absolute position and get the current "left" from the "margin: auto" layout.
inited = true;
const container = dragElement.parentElement!;
initLeft = container.getBoundingClientRect().width / 2 - dragElement.getBoundingClientRect().width / 2;
resetView();
let scale = 1, left = 0, top = 0, lastPageX = 0, lastPageY = 0;
let dragPointerId: number | null = null;
const applyTransform = () => {
dragElement.style.transform = `translate(${left}px, ${top}px) scale(${scale})`;
dragElement.style.touchAction = scale === 1 ? '' : 'none'; // touch scrolls the page until zoomed, then pans the diagram
};
for (const el of viewController.querySelectorAll('[data-control-action]')) {
el.addEventListener('click', () => {
initAbsolutePosition();
switch (el.getAttribute('data-control-action')) {
case 'zoom-in':
currentScale *= 1.2;
scale *= 1.2;
break;
case 'zoom-out':
currentScale /= 1.2;
scale /= 1.2;
break;
case 'reset':
resetView();
scale = 1;
left = top = 0;
break;
}
dragElement.style.transform = `scale(${currentScale})`;
applyTransform();
});
}
dragElement.addEventListener('mousedown', (e) => {
if (e.button !== 0 || e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return; // only left mouse button can drag
const target = e.target as Element;
dragElement.addEventListener('pointerdown', (e) => {
if (dragPointerId !== null || !isPlainClick(e) || (e.pointerType !== 'mouse' && scale === 1)) return;
// don't start the drag if the click is on an interactive element (e.g.: link, button) or text element
if (target.closest('div, p, a, span, button, input, text')) return;
initAbsolutePosition();
isDragging = true;
if ((e.target as Element).closest('div, p, a, span, button, input, text')) return;
e.preventDefault(); // prevent text selection while dragging
dragPointerId = e.pointerId;
dragElement.setPointerCapture(e.pointerId);
lastPageX = e.pageX;
lastPageY = e.pageY;
dragElement.style.cursor = 'grabbing';
});
dragElement.ownerDocument.addEventListener('mousemove', (e) => {
if (!isDragging) return;
lastLeft = e.pageX - lastPageX + lastLeft;
lastTop = e.pageY - lastPageY + lastTop;
dragElement.style.left = `${lastLeft}px`;
dragElement.style.top = `${lastTop}px`;
dragElement.addEventListener('pointermove', (e) => {
if (e.pointerId !== dragPointerId) return;
left += e.pageX - lastPageX;
top += e.pageY - lastPageY;
lastPageX = e.pageX;
lastPageY = e.pageY;
applyTransform();
});
dragElement.ownerDocument.addEventListener('mouseup', () => {
if (!isDragging) return;
isDragging = false;
const endDrag = (e: PointerEvent) => {
if (e.pointerId !== dragPointerId) return;
dragPointerId = null;
dragElement.style.removeProperty('cursor');
});
};
dragElement.addEventListener('pointerup', endDrag);
dragElement.addEventListener('pointercancel', endDrag);
}
let elkLayoutsRegistered = false;
export async function initMarkupCodeMermaid(elMarkup: HTMLElement): Promise<void> {
// .markup code.language-mermaid
const mermaidBlocks: Array<{source: string, parentContainer: Element}> = [];
const attrMermaidRendered = 'data-markup-mermaid-rendered';
let needElkRender = false;
for (const elCodeBlock of queryElems(elMarkup, 'code.language-mermaid')) {
const parentContainer = elCodeBlock.closest('pre.code-block')!; // it must exist, if no, there must be a bug
if (parentContainer.hasAttribute(attrMermaidRendered)) continue;
parentContainer.setAttribute(attrMermaidRendered, 'true');
const source = elCodeBlock.textContent ?? '';
needElkRender = needElkRender || sourceNeedsElk(source);
const source = elCodeBlock.textContent;
if (mermaidMaxSourceCharacters >= 0 && source.length > mermaidMaxSourceCharacters) {
displayError(parentContainer, new Error(`Mermaid source of ${source.length} characters exceeds the maximum allowed length of ${mermaidMaxSourceCharacters}.`));
continue;
}
mermaidBlocks.push({source, parentContainer});
}
if (!mermaidBlocks.length) return;
const {mermaid, elkLayouts} = await loadMermaid(needElkRender);
if (elkLayouts && !elkLayoutsRegistered) {
mermaid.registerLayoutLoaders(elkLayouts);
elkLayoutsRegistered = true;
}
const {default: mermaid} = await import('mermaid');
mermaid.initialize({
startOnLoad: false,
theme: isDarkTheme() ? 'dark' : 'neutral', // TODO: maybe it should use "darkMode" to adopt more user-specified theme instead of just "dark" or "neutral"
look: 'neo',
themeVariables: {dropShadow: 'none', useGradient: false}, // hide ugly shadows and gradients from neo
themeCSS: '[filter] { filter: none; }', // sequence diagrams hardcode the neo drop shadow as an attribute
securityLevel: 'strict',
suppressErrorRendering: true,
});
const iframeStyleText = getIframeCss();
const applyMermaidIframeHeight = (iframe: HTMLIFrameElement, height: number) => {
if (!height) return;
// use a min-height to make sure the buttons won't overlap.
iframe.style.height = `${Math.max(height, 85)}px`;
};
// mermaid is a globally shared instance, its document also says "Multiple calls to this function will be enqueued to run serially."
// so here we just simply render the mermaid blocks one by one, no need to do "Promise.all" concurrently
for (const block of mermaidBlocks) {
const {source, parentContainer} = block;
if (isSourceTooLarge(source)) {
displayError(parentContainer, new Error(`Mermaid source of ${source.length} characters exceeds the maximum allowed length of ${mermaidMaxSourceCharacters}.`));
continue;
}
for (const {source, parentContainer} of mermaidBlocks) {
try {
// render the mermaid diagram to svg text, and parse it to a DOM node
const {svg: svgText, bindFunctions} = await mermaid.render('mermaid', source, parentContainer);
@@ -214,30 +109,18 @@ export async function initMarkupCodeMermaid(elMarkup: HTMLElement): Promise<void
// create an iframe to sandbox the svg with styles, and set correct height by reading svg's viewBox height
const iframe = document.createElement('iframe');
iframe.classList.add('markup-content-iframe', 'is-loading');
// the styles are not ready, so don't really render anything before the "load" event, to avoid flicker of unstyled content
iframe.srcdoc = html`<html><head></head><body></body></html>`;
// although the "viewBox" is optional, mermaid's output should always have a correct viewBox with width and height
const iframeHeightFromViewBox = Math.ceil(svgNode.viewBox?.baseVal?.height ?? 0);
applyMermaidIframeHeight(iframe, iframeHeightFromViewBox);
iframe.classList.add('markup-content-iframe', 'tw-invisible');
iframe.srcdoc = '<style>body { margin: 0; overflow: hidden; } #mermaid { display: block; margin: 0 auto; }</style>';
const viewBoxHeight = svgNode.viewBox.baseVal?.height;
if (viewBoxHeight) iframe.style.height = `${Math.max(Math.ceil(viewBoxHeight), 85)}px`; // min-height keeps the buttons from overlapping
// the iframe will be fully reloaded if its DOM context is changed (e.g.: moved in the DOM tree).
// to avoid unnecessary reloading, we should insert the iframe to its final position only once.
iframe.addEventListener('load', () => {
// same origin, so we can operate "iframe head/body" and all elements directly
const style = document.createElement('style');
style.textContent = iframeStyleText;
iframe.contentDocument!.head.append(style);
const iframeBody = iframe.contentDocument!.body;
iframeBody.append(svgNode);
bindFunctions?.(iframeBody); // follow "mermaid.render" doc, attach event handlers to the svg's container
// according to mermaid, the viewBox height should always exist, here just a fallback for unknown cases.
// and keep in mind: clientHeight can be 0 if the element is hidden (display: none).
if (!iframeHeightFromViewBox) applyMermaidIframeHeight(iframe, iframeBody.clientHeight);
iframe.classList.remove('is-loading');
iframe.classList.remove('tw-invisible');
initMermaidViewController(viewController, svgNode);
});
-16
View File
@@ -12,22 +12,6 @@ export function captureNavigations() {
return navigations;
}
/** strip common indentation from a string and trim it */
export function dedent(str: string) {
const match = str.match(/^[ \t]*(?=\S)/gm);
if (!match) return str;
let minIndent = Infinity;
for (const indent of match) {
minIndent = Math.min(minIndent, indent.length);
}
if (minIndent === 0 || minIndent === Infinity) {
return str;
}
return str.replace(new RegExp(`^[ \\t]{${minIndent}}`, 'gm'), '').trim();
}
export function normalizeTestHtml(s: string) {
const lines = s.replace(/>\s+</g, '>\n<').trim().split('\n');
for (let i = 0; i < lines.length; i++) {