mirror of
https://github.com/go-gitea/gitea.git
synced 2026-09-18 11:53:42 +00:00
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:
+1
-2
@@ -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",
|
||||
|
||||
Generated
+61
-40
@@ -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
|
||||
|
||||
@@ -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 */
|
||||
}
|
||||
|
||||
@@ -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 > * {
|
||||
|
||||
@@ -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
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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++) {
|
||||
|
||||
Reference in New Issue
Block a user