From 52f2bc73fde33036f7537a28f16eb3a77491be27 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 30 Jan 2026 02:10:31 +0100 Subject: [PATCH 01/36] Add `layout: elk` support to mermaid --- package.json | 1 + pnpm-lock.yaml | 48 ++++++++++++++---------------------- web_src/js/markup/mermaid.ts | 6 ++++- 3 files changed, 25 insertions(+), 30 deletions(-) diff --git a/package.json b/package.json index acac860a393c8..30f0fa708dae7 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "@github/relative-time-element": "5.0.0", "@github/text-expander-element": "2.9.4", "@mcaptcha/vanilla-glue": "0.1.0-alpha-3", + "@mermaid-js/layout-elk": "0.2.0", "@primer/octicons": "19.21.2", "@resvg/resvg-wasm": "2.6.2", "@silverwind/vue3-calendar-heatmap": "2.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4e39cff537672..4313d3a71d2f0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -53,6 +53,9 @@ importers: '@mcaptcha/vanilla-glue': specifier: 0.1.0-alpha-3 version: 0.1.0-alpha-3 + '@mermaid-js/layout-elk': + specifier: 0.2.0 + version: 0.2.0(mermaid@11.12.2) '@primer/octicons': specifier: 19.21.2 version: 19.21.2 @@ -813,6 +816,11 @@ packages: '@mcaptcha/vanilla-glue@0.1.0-alpha-3': resolution: {integrity: sha512-GT6TJBgmViGXcXiT5VOr+h/6iOnThSlZuCoOWncubyTZU9R3cgU5vWPkF7G6Ob6ee2CBe3yqBxxk24CFVGTVXw==} + '@mermaid-js/layout-elk@0.2.0': + resolution: {integrity: sha512-vjjYGnCCjYlIA/rR7M//eFi0rHM6dsMyN1JQKfckpt30DTC/esrw36hcrvA2FNPHaqh3Q/SyBWzddyaky8EtUQ==} + peerDependencies: + mermaid: ^11.0.2 + '@mermaid-js/parser@0.6.3': resolution: {integrity: sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA==} @@ -936,49 +944,41 @@ packages: resolution: {integrity: sha512-qNQk0H6q1CnwS9cnvyjk9a+JN8BTbxK7K15Bb5hYfJcKTG1hfloQf6egndKauYOO0wu9ldCMPBrEP1FNIQEhaA==} cpu: [arm64] os: [linux] - libc: [glibc] '@oxc-resolver/binding-linux-arm64-musl@11.16.4': resolution: {integrity: sha512-wEXSaEaYxGGoVSbw0i2etjDDWcqErKr8xSkTdwATP798efsZmodUAcLYJhN0Nd4W35Oq6qAvFGHpKwFrrhpTrA==} cpu: [arm64] os: [linux] - libc: [musl] '@oxc-resolver/binding-linux-ppc64-gnu@11.16.4': resolution: {integrity: sha512-CUFOlpb07DVOFLoYiaTfbSBRPIhNgwc/MtlYeg3p6GJJw+kEm/vzc9lohPSjzF2MLPB5hzsJdk+L/GjrTT3UPw==} cpu: [ppc64] os: [linux] - libc: [glibc] '@oxc-resolver/binding-linux-riscv64-gnu@11.16.4': resolution: {integrity: sha512-d8It4AH8cN9ReK1hW6ZO4x3rMT0hB2LYH0RNidGogV9xtnjLRU+Y3MrCeClLyOSGCibmweJJAjnwB7AQ31GEhg==} cpu: [riscv64] os: [linux] - libc: [glibc] '@oxc-resolver/binding-linux-riscv64-musl@11.16.4': resolution: {integrity: sha512-d09dOww9iKyEHSxuOQ/Iu2aYswl0j7ExBcyy14D6lJ5ijQSP9FXcJYJsJ3yvzboO/PDEFjvRuF41f8O1skiPVg==} cpu: [riscv64] os: [linux] - libc: [musl] '@oxc-resolver/binding-linux-s390x-gnu@11.16.4': resolution: {integrity: sha512-lhjyGmUzTWHduZF3MkdUSEPMRIdExnhsqv8u1upX3A15epVn6YVwv4msFQPJl1x1wszkACPeDHGOtzHsITXGdw==} cpu: [s390x] os: [linux] - libc: [glibc] '@oxc-resolver/binding-linux-x64-gnu@11.16.4': resolution: {integrity: sha512-ZtqqiI5rzlrYBm/IMMDIg3zvvVj4WO/90Dg/zX+iA8lWaLN7K5nroXb17MQ4WhI5RqlEAgrnYDXW+hok1D9Kaw==} cpu: [x64] os: [linux] - libc: [glibc] '@oxc-resolver/binding-linux-x64-musl@11.16.4': resolution: {integrity: sha512-LM424h7aaKcMlqHnQWgTzO+GRNLyjcNnMpqm8SygEtFRVW693XS+XGXYvjORlmJtsyjo84ej1FMb3U2HE5eyjg==} cpu: [x64] os: [linux] - libc: [musl] '@oxc-resolver/binding-openharmony-arm64@11.16.4': resolution: {integrity: sha512-8w8U6A5DDWTBv3OUxSD9fNk37liZuEC5jnAc9wQRv9DeYKAXvuUtBfT09aIZ58swaci0q1WS48/CoMVEO6jdCA==} @@ -1061,79 +1061,66 @@ packages: resolution: {integrity: sha512-E8jKK87uOvLrrLN28jnAAAChNq5LeCd2mGgZF+fGF5D507WlG/Noct3lP/QzQ6MrqJ5BCKNwI9ipADB6jyiq2A==} cpu: [arm] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.56.0': resolution: {integrity: sha512-jQosa5FMYF5Z6prEpTCCmzCXz6eKr/tCBssSmQGEeozA9tkRUty/5Vx06ibaOP9RCrW1Pvb8yp3gvZhHwTDsJw==} cpu: [arm] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.56.0': resolution: {integrity: sha512-uQVoKkrC1KGEV6udrdVahASIsaF8h7iLG0U0W+Xn14ucFwi6uS539PsAr24IEF9/FoDtzMeeJXJIBo5RkbNWvQ==} cpu: [arm64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.56.0': resolution: {integrity: sha512-vLZ1yJKLxhQLFKTs42RwTwa6zkGln+bnXc8ueFGMYmBTLfNu58sl5/eXyxRa2RarTkJbXl8TKPgfS6V5ijNqEA==} cpu: [arm64] os: [linux] - libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.56.0': resolution: {integrity: sha512-FWfHOCub564kSE3xJQLLIC/hbKqHSVxy8vY75/YHHzWvbJL7aYJkdgwD/xGfUlL5UV2SB7otapLrcCj2xnF1dg==} cpu: [loong64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.56.0': resolution: {integrity: sha512-z1EkujxIh7nbrKL1lmIpqFTc/sr0u8Uk0zK/qIEFldbt6EDKWFk/pxFq3gYj4Bjn3aa9eEhYRlL3H8ZbPT1xvA==} cpu: [loong64] os: [linux] - libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.56.0': resolution: {integrity: sha512-iNFTluqgdoQC7AIE8Q34R3AuPrJGJirj5wMUErxj22deOcY7XwZRaqYmB6ZKFHoVGqRcRd0mqO+845jAibKCkw==} cpu: [ppc64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.56.0': resolution: {integrity: sha512-MtMeFVlD2LIKjp2sE2xM2slq3Zxf9zwVuw0jemsxvh1QOpHSsSzfNOTH9uYW9i1MXFxUSMmLpeVeUzoNOKBaWg==} cpu: [ppc64] os: [linux] - libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.56.0': resolution: {integrity: sha512-in+v6wiHdzzVhYKXIk5U74dEZHdKN9KH0Q4ANHOTvyXPG41bajYRsy7a8TPKbYPl34hU7PP7hMVHRvv/5aCSew==} cpu: [riscv64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.56.0': resolution: {integrity: sha512-yni2raKHB8m9NQpI9fPVwN754mn6dHQSbDTwxdr9SE0ks38DTjLMMBjrwvB5+mXrX+C0npX0CVeCUcvvvD8CNQ==} cpu: [riscv64] os: [linux] - libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.56.0': resolution: {integrity: sha512-zhLLJx9nQPu7wezbxt2ut+CI4YlXi68ndEve16tPc/iwoylWS9B3FxpLS2PkmfYgDQtosah07Mj9E0khc3Y+vQ==} cpu: [s390x] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.56.0': resolution: {integrity: sha512-MVC6UDp16ZSH7x4rtuJPAEoE1RwS8N4oK9DLHy3FTEdFoUTCFVzMfJl/BVJ330C+hx8FfprA5Wqx4FhZXkj2Kw==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-musl@4.56.0': resolution: {integrity: sha512-ZhGH1eA4Qv0lxaV00azCIS1ChedK0V32952Md3FtnxSqZTBTd6tgil4nZT5cU8B+SIw3PFYkvyR4FKo2oyZIHA==} cpu: [x64] os: [linux] - libc: [musl] '@rollup/rollup-openbsd-x64@4.56.0': resolution: {integrity: sha512-O16XcmyDeFI9879pEcmtWvD/2nyxR9mF7Gs44lf1vGGx8Vg2DRNx11aVXBEqOQhWb92WN4z7fW/q4+2NYzCbBA==} @@ -1507,49 +1494,41 @@ packages: resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} cpu: [arm64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-arm64-musl@1.11.1': resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} cpu: [arm64] os: [linux] - libc: [musl] '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} cpu: [ppc64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} cpu: [riscv64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} cpu: [riscv64] os: [linux] - libc: [musl] '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} cpu: [s390x] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-x64-gnu@1.11.1': resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} cpu: [x64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-x64-musl@1.11.1': resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} cpu: [x64] os: [linux] - libc: [musl] '@unrs/resolver-binding-wasm32-wasi@1.11.1': resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} @@ -2376,6 +2355,9 @@ packages: electron-to-chromium@1.5.278: resolution: {integrity: sha512-dQ0tM1svDRQOwxnXxm+twlGTjr9Upvt8UFWAgmLsxEzFQxhbti4VwxmMjsDxVC51Zo84swW7FVCXEV+VAkhuPw==} + elkjs@0.9.3: + resolution: {integrity: sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ==} + emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} @@ -4900,6 +4882,12 @@ snapshots: dependencies: '@mcaptcha/core-glue': 0.1.0-alpha-5 + '@mermaid-js/layout-elk@0.2.0(mermaid@11.12.2)': + dependencies: + d3: 7.9.0 + elkjs: 0.9.3 + mermaid: 11.12.2 + '@mermaid-js/parser@0.6.3': dependencies: langium: 3.3.1 @@ -6397,6 +6385,8 @@ snapshots: electron-to-chromium@1.5.278: {} + elkjs@0.9.3: {} + emoji-regex@10.6.0: {} emoji-regex@8.0.0: {} diff --git a/web_src/js/markup/mermaid.ts b/web_src/js/markup/mermaid.ts index e1c2935f23d84..4d4a7ebf13f29 100644 --- a/web_src/js/markup/mermaid.ts +++ b/web_src/js/markup/mermaid.ts @@ -13,8 +13,12 @@ body {margin: 0; padding: 0; overflow: hidden} export async function initMarkupCodeMermaid(elMarkup: HTMLElement): Promise { // .markup code.language-mermaid queryElems(elMarkup, 'code.language-mermaid', async (el) => { - const {default: mermaid} = await import(/* webpackChunkName: "mermaid" */'mermaid'); + const [{default: mermaid}, {default: elk}] = await Promise.all([ + import(/* webpackChunkName: "mermaid" */'mermaid'), + import(/* webpackChunkName: "mermaid-layout-elk" */'@mermaid-js/layout-elk'), + ]); + mermaid.registerLayoutLoaders(elk); mermaid.initialize({ startOnLoad: false, theme: isDarkTheme() ? 'dark' : 'neutral', From 8bb2500420180e54c64774875e38032162f397ae Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 30 Jan 2026 02:14:59 +0100 Subject: [PATCH 02/36] rename var --- web_src/js/markup/mermaid.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web_src/js/markup/mermaid.ts b/web_src/js/markup/mermaid.ts index 4d4a7ebf13f29..b5b2b206e9b27 100644 --- a/web_src/js/markup/mermaid.ts +++ b/web_src/js/markup/mermaid.ts @@ -13,12 +13,12 @@ body {margin: 0; padding: 0; overflow: hidden} export async function initMarkupCodeMermaid(elMarkup: HTMLElement): Promise { // .markup code.language-mermaid queryElems(elMarkup, 'code.language-mermaid', async (el) => { - const [{default: mermaid}, {default: elk}] = await Promise.all([ + const [{default: mermaid}, {default: elkLoaders}] = await Promise.all([ import(/* webpackChunkName: "mermaid" */'mermaid'), import(/* webpackChunkName: "mermaid-layout-elk" */'@mermaid-js/layout-elk'), ]); - mermaid.registerLayoutLoaders(elk); + mermaid.registerLayoutLoaders(elkLoaders); mermaid.initialize({ startOnLoad: false, theme: isDarkTheme() ? 'dark' : 'neutral', From bfb614db3795e6c425d9d77feb88ce4478ad9962 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 30 Jan 2026 02:19:56 +0100 Subject: [PATCH 03/36] use same webpackChunkName --- web_src/js/markup/mermaid.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web_src/js/markup/mermaid.ts b/web_src/js/markup/mermaid.ts index b5b2b206e9b27..9a5982b9b873b 100644 --- a/web_src/js/markup/mermaid.ts +++ b/web_src/js/markup/mermaid.ts @@ -15,7 +15,7 @@ export async function initMarkupCodeMermaid(elMarkup: HTMLElement): Promise { const [{default: mermaid}, {default: elkLoaders}] = await Promise.all([ import(/* webpackChunkName: "mermaid" */'mermaid'), - import(/* webpackChunkName: "mermaid-layout-elk" */'@mermaid-js/layout-elk'), + import(/* webpackChunkName: "mermaid" */'@mermaid-js/layout-elk'), ]); mermaid.registerLayoutLoaders(elkLoaders); From 3cde6c5f254ab0a8e8df8fe121dfcd5fe0cb9ac3 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 30 Jan 2026 03:14:55 +0100 Subject: [PATCH 04/36] add lazy-loading elk --- web_src/js/markup/mermaid.ts | 52 ++++++++++++++++++++++++++---------- 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/web_src/js/markup/mermaid.ts b/web_src/js/markup/mermaid.ts index 9a5982b9b873b..f643752903b95 100644 --- a/web_src/js/markup/mermaid.ts +++ b/web_src/js/markup/mermaid.ts @@ -3,6 +3,7 @@ import {makeCodeCopyButton} from './codecopy.ts'; import {displayError} from './common.ts'; import {queryElems} from '../utils/dom.ts'; import {html, htmlRaw} from '../utils/html.ts'; +import type {LayoutLoaderDefinition, Mermaid} from 'mermaid'; const {mermaidMaxSourceCharacters} = window.config; @@ -10,15 +11,45 @@ const iframeCss = `:root {color-scheme: normal} body {margin: 0; padding: 0; overflow: hidden} #mermaid {display: block; margin: 0 auto}`; +async function loadMermaid(sources: Array) { + const imports: Array> = [ + import(/* webpackChunkName: "mermaid" */'mermaid'), + ]; + + // crude check to detect elk layout configuration in the source, defined in either a + // YAML frontmatter block or an JSON init directive. + const sourcesContainElk = sources.some((source) => { + return /(layout|defaultRenderer).+elk/.test(source); + }); + if (sourcesContainElk) { + imports.push(import(/* webpackChunkName: "mermaid-layout-elk" */'@mermaid-js/layout-elk')); + } + + const results = await Promise.all(imports); + return { + mermaid: results[0].default as Mermaid, + elkLayouts: (results[1]?.default) as Array | undefined, + }; +} + export async function initMarkupCodeMermaid(elMarkup: HTMLElement): Promise { // .markup code.language-mermaid - queryElems(elMarkup, 'code.language-mermaid', async (el) => { - const [{default: mermaid}, {default: elkLoaders}] = await Promise.all([ - import(/* webpackChunkName: "mermaid" */'mermaid'), - import(/* webpackChunkName: "mermaid" */'@mermaid-js/layout-elk'), - ]); + const els = Array.from(queryElems(elMarkup, 'code.language-mermaid')); + const sources = Array.from(els, (el) => el.textContent); + const {mermaid, elkLayouts} = await loadMermaid(sources); + + for (const [index, el] of els.entries()) { + const source = sources[index]; + const pre = el.closest('pre')!; + + if (mermaidMaxSourceCharacters >= 0 && source.length > mermaidMaxSourceCharacters) { + displayError(pre, new Error(`Mermaid source of ${source.length} characters exceeds the maximum allowed length of ${mermaidMaxSourceCharacters}.`)); + return; + } - mermaid.registerLayoutLoaders(elkLoaders); + if (elkLayouts) { + mermaid.registerLayoutLoaders(elkLayouts); + } mermaid.initialize({ startOnLoad: false, theme: isDarkTheme() ? 'dark' : 'neutral', @@ -26,15 +57,8 @@ export async function initMarkupCodeMermaid(elMarkup: HTMLElement): Promise= 0 && source.length > mermaidMaxSourceCharacters) { - displayError(pre, new Error(`Mermaid source of ${source.length} characters exceeds the maximum allowed length of ${mermaidMaxSourceCharacters}.`)); - return; - } - try { await mermaid.parse(source); } catch (err) { @@ -87,5 +111,5 @@ export async function initMarkupCodeMermaid(elMarkup: HTMLElement): Promise Date: Fri, 30 Jan 2026 03:22:47 +0100 Subject: [PATCH 05/36] add unit tests --- web_src/js/markup/mermaid.test.ts | 46 +++++++++++++++++++++++++++++++ web_src/js/markup/mermaid.ts | 14 ++++++---- 2 files changed, 54 insertions(+), 6 deletions(-) create mode 100644 web_src/js/markup/mermaid.test.ts diff --git a/web_src/js/markup/mermaid.test.ts b/web_src/js/markup/mermaid.test.ts new file mode 100644 index 0000000000000..70bdccc7b8b15 --- /dev/null +++ b/web_src/js/markup/mermaid.test.ts @@ -0,0 +1,46 @@ +import {sourcesContainElk} from "./mermaid.ts"; + +test('sourcesContainElk', () => { + expect(sourcesContainElk([` + flowchart TB + A --> B + A --> C --> B + `])).toEqual(false); + + expect(sourcesContainElk([` + --- + config: + layout: elk + flowchart: + defaultRenderer: elk + --- + flowchart TB + A --> B + A --> C --> B + `])).toEqual(true); + + expect(sourcesContainElk([` + %%{ init: { "layout": "elk" } }%% + flowchart TB + A --> B + A --> C --> B + `])).toEqual(true); + + expect(sourcesContainElk([` + %%{ init: { "flowchart": { "defaultRenderer": "elk" } } }%% + flowchart TB + A --> B + A --> C --> B + `])).toEqual(true); + + expect(sourcesContainElk([` + %%{ + init: { + "layout": "elk", + } + }%% + flowchart TB + A --> B + A --> C --> B + `])).toEqual(true); +}); diff --git a/web_src/js/markup/mermaid.ts b/web_src/js/markup/mermaid.ts index f643752903b95..2367604353ab7 100644 --- a/web_src/js/markup/mermaid.ts +++ b/web_src/js/markup/mermaid.ts @@ -11,17 +11,19 @@ const iframeCss = `:root {color-scheme: normal} body {margin: 0; padding: 0; overflow: hidden} #mermaid {display: block; margin: 0 auto}`; +/** detect whether mermaid sources contain elk layout configuration */ +export function sourcesContainElk(sources: Array) { + return sources.some((source) => { + return /(layout|defaultRenderer).+elk/.test(source); + }); +} + async function loadMermaid(sources: Array) { const imports: Array> = [ import(/* webpackChunkName: "mermaid" */'mermaid'), ]; - // crude check to detect elk layout configuration in the source, defined in either a - // YAML frontmatter block or an JSON init directive. - const sourcesContainElk = sources.some((source) => { - return /(layout|defaultRenderer).+elk/.test(source); - }); - if (sourcesContainElk) { + if (sourcesContainElk(sources)) { imports.push(import(/* webpackChunkName: "mermaid-layout-elk" */'@mermaid-js/layout-elk')); } From 2e85fd0e6d9c0991eae217fefb076f703d2b4532 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 30 Jan 2026 03:29:12 +0100 Subject: [PATCH 06/36] fix review comments --- web_src/js/markup/mermaid.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/web_src/js/markup/mermaid.ts b/web_src/js/markup/mermaid.ts index 2367604353ab7..59036ccaaae27 100644 --- a/web_src/js/markup/mermaid.ts +++ b/web_src/js/markup/mermaid.ts @@ -14,7 +14,7 @@ body {margin: 0; padding: 0; overflow: hidden} /** detect whether mermaid sources contain elk layout configuration */ export function sourcesContainElk(sources: Array) { return sources.some((source) => { - return /(layout|defaultRenderer).+elk/.test(source); + return /(layout|defaultRenderer)[\s\S]*elk/.test(source); }); } @@ -34,23 +34,26 @@ async function loadMermaid(sources: Array) { }; } +let elkLayoutsRegistered = false; + export async function initMarkupCodeMermaid(elMarkup: HTMLElement): Promise { // .markup code.language-mermaid const els = Array.from(queryElems(elMarkup, 'code.language-mermaid')); - const sources = Array.from(els, (el) => el.textContent); + const sources = Array.from(els, (el) => el.textContent ?? ''); const {mermaid, elkLayouts} = await loadMermaid(sources); for (const [index, el] of els.entries()) { const source = sources[index]; - const pre = el.closest('pre')!; + const pre = el.closest('pre'); - if (mermaidMaxSourceCharacters >= 0 && source.length > mermaidMaxSourceCharacters) { + if (pre && mermaidMaxSourceCharacters >= 0 && source.length > mermaidMaxSourceCharacters) { displayError(pre, new Error(`Mermaid source of ${source.length} characters exceeds the maximum allowed length of ${mermaidMaxSourceCharacters}.`)); - return; + continue; } - if (elkLayouts) { + if (elkLayouts && !elkLayoutsRegistered) { mermaid.registerLayoutLoaders(elkLayouts); + elkLayoutsRegistered = true; } mermaid.initialize({ startOnLoad: false, From 67de5a30b976a0d779cf9d34f798a07b34856c68 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 30 Jan 2026 03:32:42 +0100 Subject: [PATCH 07/36] simplify loadMermaid --- web_src/js/markup/mermaid.ts | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/web_src/js/markup/mermaid.ts b/web_src/js/markup/mermaid.ts index 59036ccaaae27..751dcf779d8e8 100644 --- a/web_src/js/markup/mermaid.ts +++ b/web_src/js/markup/mermaid.ts @@ -19,18 +19,14 @@ export function sourcesContainElk(sources: Array) { } async function loadMermaid(sources: Array) { - const imports: Array> = [ - import(/* webpackChunkName: "mermaid" */'mermaid'), - ]; + const mermaidPromise = import(/* webpackChunkName: "mermaid" */'mermaid'); + const elkPromise = sourcesContainElk(sources) ? + import(/* webpackChunkName: "mermaid-layout-elk" */'@mermaid-js/layout-elk') : null; - if (sourcesContainElk(sources)) { - imports.push(import(/* webpackChunkName: "mermaid-layout-elk" */'@mermaid-js/layout-elk')); - } - - const results = await Promise.all(imports); + const results = await Promise.all([mermaidPromise, elkPromise]); return { - mermaid: results[0].default as Mermaid, - elkLayouts: (results[1]?.default) as Array | undefined, + mermaid: results[0].default, + elkLayouts: results[1]?.default, }; } From a6204c7d353cb817cfd14d1a976f31157df295e6 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 30 Jan 2026 03:33:18 +0100 Subject: [PATCH 08/36] fix lint --- web_src/js/markup/mermaid.test.ts | 2 +- web_src/js/markup/mermaid.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/web_src/js/markup/mermaid.test.ts b/web_src/js/markup/mermaid.test.ts index 70bdccc7b8b15..3fed77fe9a968 100644 --- a/web_src/js/markup/mermaid.test.ts +++ b/web_src/js/markup/mermaid.test.ts @@ -1,4 +1,4 @@ -import {sourcesContainElk} from "./mermaid.ts"; +import {sourcesContainElk} from './mermaid.ts'; test('sourcesContainElk', () => { expect(sourcesContainElk([` diff --git a/web_src/js/markup/mermaid.ts b/web_src/js/markup/mermaid.ts index 751dcf779d8e8..fe09ad6b4edf8 100644 --- a/web_src/js/markup/mermaid.ts +++ b/web_src/js/markup/mermaid.ts @@ -3,7 +3,6 @@ import {makeCodeCopyButton} from './codecopy.ts'; import {displayError} from './common.ts'; import {queryElems} from '../utils/dom.ts'; import {html, htmlRaw} from '../utils/html.ts'; -import type {LayoutLoaderDefinition, Mermaid} from 'mermaid'; const {mermaidMaxSourceCharacters} = window.config; From 271238914a174f58d1ff8bee352f8ea356301043 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 30 Jan 2026 03:34:37 +0100 Subject: [PATCH 09/36] add test --- web_src/js/markup/mermaid.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/web_src/js/markup/mermaid.test.ts b/web_src/js/markup/mermaid.test.ts index 3fed77fe9a968..d867b7d5575af 100644 --- a/web_src/js/markup/mermaid.test.ts +++ b/web_src/js/markup/mermaid.test.ts @@ -7,6 +7,12 @@ test('sourcesContainElk', () => { A --> C --> B `])).toEqual(false); + expect(sourcesContainElk([` + flowchart TB + elk --> B + elk --> C --> B + `])).toEqual(false); + expect(sourcesContainElk([` --- config: From cbd2f87825475a77fd29a6b093bde0489450ee86 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 30 Jan 2026 03:37:29 +0100 Subject: [PATCH 10/36] add return when no els --- web_src/js/markup/mermaid.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/web_src/js/markup/mermaid.ts b/web_src/js/markup/mermaid.ts index fe09ad6b4edf8..33e3e233f0f10 100644 --- a/web_src/js/markup/mermaid.ts +++ b/web_src/js/markup/mermaid.ts @@ -34,6 +34,7 @@ let elkLayoutsRegistered = false; export async function initMarkupCodeMermaid(elMarkup: HTMLElement): Promise { // .markup code.language-mermaid const els = Array.from(queryElems(elMarkup, 'code.language-mermaid')); + if (!els.length) return; const sources = Array.from(els, (el) => el.textContent ?? ''); const {mermaid, elkLayouts} = await loadMermaid(sources); From 723632f95a0150a8c415f85adf8c0cbada345f65 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 30 Jan 2026 03:38:48 +0100 Subject: [PATCH 11/36] move check --- web_src/js/markup/mermaid.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/web_src/js/markup/mermaid.ts b/web_src/js/markup/mermaid.ts index 33e3e233f0f10..3872e2d0e1ac9 100644 --- a/web_src/js/markup/mermaid.ts +++ b/web_src/js/markup/mermaid.ts @@ -42,7 +42,11 @@ export async function initMarkupCodeMermaid(elMarkup: HTMLElement): Promise= 0 && source.length > mermaidMaxSourceCharacters) { + if (!pre || pre.hasAttribute('data-render-done')) { + continue; + } + + if (mermaidMaxSourceCharacters >= 0 && source.length > mermaidMaxSourceCharacters) { displayError(pre, new Error(`Mermaid source of ${source.length} characters exceeds the maximum allowed length of ${mermaidMaxSourceCharacters}.`)); continue; } @@ -58,8 +62,6 @@ export async function initMarkupCodeMermaid(elMarkup: HTMLElement): Promise Date: Fri, 30 Jan 2026 03:50:46 +0100 Subject: [PATCH 12/36] refine regex and add tests --- web_src/js/markup/mermaid.test.ts | 45 ++++++++++++++++++++++++++++++- web_src/js/markup/mermaid.ts | 6 ++++- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/web_src/js/markup/mermaid.test.ts b/web_src/js/markup/mermaid.test.ts index d867b7d5575af..4403b6a8479e3 100644 --- a/web_src/js/markup/mermaid.test.ts +++ b/web_src/js/markup/mermaid.test.ts @@ -16,7 +16,26 @@ test('sourcesContainElk', () => { expect(sourcesContainElk([` --- config: - layout: elk + layout : elk + --- + flowchart TB + A --> B + A --> C --> B + `])).toEqual(true); + + expect(sourcesContainElk([` + --- + config: + layout: elk.layered + --- + flowchart TB + A --> B + A --> C --> B + `])).toEqual(true); + + expect(sourcesContainElk([` + --- + config: flowchart: defaultRenderer: elk --- @@ -49,4 +68,28 @@ test('sourcesContainElk', () => { A --> B A --> C --> B `])).toEqual(true); + + expect(sourcesContainElk([` + %%{ + init: { + "layout" : "elk.layered", + } + }%% + flowchart TB + A --> B + A --> C --> B + `])).toEqual(true); + + expect(sourcesContainElk([` + %%{ + init: { + "flowchart": { + "defaultRenderer": "elk", + } + } + }%% + flowchart TB + A --> B + A --> C --> B + `])).toEqual(true); }); diff --git a/web_src/js/markup/mermaid.ts b/web_src/js/markup/mermaid.ts index 3872e2d0e1ac9..a89635b1fce8d 100644 --- a/web_src/js/markup/mermaid.ts +++ b/web_src/js/markup/mermaid.ts @@ -13,7 +13,11 @@ body {margin: 0; padding: 0; overflow: hidden} /** detect whether mermaid sources contain elk layout configuration */ export function sourcesContainElk(sources: Array) { return sources.some((source) => { - return /(layout|defaultRenderer)[\s\S]*elk/.test(source); + // yaml frontmatter + if (/^\s*(layout|defaultRenderer)\s*:\s*elk/m.test(source)) return true; + // json init + if (/"(layout|defaultRenderer)"\s*:\s*"elk/.test(source)) return true; + return false; }); } From 41f0028be868fb1f7d701c44290257638770f6e4 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 30 Jan 2026 03:58:38 +0100 Subject: [PATCH 13/36] support yaml quotes --- web_src/js/markup/mermaid.test.ts | 20 ++++++++++++++++++++ web_src/js/markup/mermaid.ts | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/web_src/js/markup/mermaid.test.ts b/web_src/js/markup/mermaid.test.ts index 4403b6a8479e3..780e6dcacb3d5 100644 --- a/web_src/js/markup/mermaid.test.ts +++ b/web_src/js/markup/mermaid.test.ts @@ -33,6 +33,26 @@ test('sourcesContainElk', () => { A --> C --> B `])).toEqual(true); + expect(sourcesContainElk([` + --- + config: + "layout": "elk.layered" + --- + flowchart TB + A --> B + A --> C --> B + `])).toEqual(true); + + expect(sourcesContainElk([` + --- + config: + 'layout': 'elk.layered' + --- + flowchart TB + A --> B + A --> C --> B + `])).toEqual(true); + expect(sourcesContainElk([` --- config: diff --git a/web_src/js/markup/mermaid.ts b/web_src/js/markup/mermaid.ts index a89635b1fce8d..129856d0f33ac 100644 --- a/web_src/js/markup/mermaid.ts +++ b/web_src/js/markup/mermaid.ts @@ -14,7 +14,7 @@ body {margin: 0; padding: 0; overflow: hidden} export function sourcesContainElk(sources: Array) { return sources.some((source) => { // yaml frontmatter - if (/^\s*(layout|defaultRenderer)\s*:\s*elk/m.test(source)) return true; + if (/^['"\s]*(layout|defaultRenderer)['"\s]*:['"\s]*elk/m.test(source)) return true; // json init if (/"(layout|defaultRenderer)"\s*:\s*"elk/.test(source)) return true; return false; From 2ec40acc3fc5c06e0b25a9d6b82cbaff02e794c0 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 30 Jan 2026 04:40:49 +0100 Subject: [PATCH 14/36] use continue to attempt render other diagrams --- web_src/js/markup/mermaid.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web_src/js/markup/mermaid.ts b/web_src/js/markup/mermaid.ts index 129856d0f33ac..14e530448b773 100644 --- a/web_src/js/markup/mermaid.ts +++ b/web_src/js/markup/mermaid.ts @@ -70,7 +70,7 @@ export async function initMarkupCodeMermaid(elMarkup: HTMLElement): Promise Date: Fri, 30 Jan 2026 04:42:20 +0100 Subject: [PATCH 15/36] init once per markup element --- web_src/js/markup/mermaid.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/web_src/js/markup/mermaid.ts b/web_src/js/markup/mermaid.ts index 14e530448b773..02f2224032b78 100644 --- a/web_src/js/markup/mermaid.ts +++ b/web_src/js/markup/mermaid.ts @@ -42,6 +42,17 @@ export async function initMarkupCodeMermaid(elMarkup: HTMLElement): Promise el.textContent ?? ''); const {mermaid, elkLayouts} = await loadMermaid(sources); + if (elkLayouts && !elkLayoutsRegistered) { + mermaid.registerLayoutLoaders(elkLayouts); + elkLayoutsRegistered = true; + } + mermaid.initialize({ + startOnLoad: false, + theme: isDarkTheme() ? 'dark' : 'neutral', + securityLevel: 'strict', + suppressErrorRendering: true, + }); + for (const [index, el] of els.entries()) { const source = sources[index]; const pre = el.closest('pre'); @@ -55,17 +66,6 @@ export async function initMarkupCodeMermaid(elMarkup: HTMLElement): Promise Date: Fri, 30 Jan 2026 04:52:27 +0100 Subject: [PATCH 16/36] parallel render --- web_src/js/markup/mermaid.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/web_src/js/markup/mermaid.ts b/web_src/js/markup/mermaid.ts index 02f2224032b78..75679964297b0 100644 --- a/web_src/js/markup/mermaid.ts +++ b/web_src/js/markup/mermaid.ts @@ -53,24 +53,24 @@ export async function initMarkupCodeMermaid(elMarkup: HTMLElement): Promise { const source = sources[index]; const pre = el.closest('pre'); if (!pre || pre.hasAttribute('data-render-done')) { - continue; + return; } if (mermaidMaxSourceCharacters >= 0 && source.length > mermaidMaxSourceCharacters) { displayError(pre, new Error(`Mermaid source of ${source.length} characters exceeds the maximum allowed length of ${mermaidMaxSourceCharacters}.`)); - continue; + return; } try { await mermaid.parse(source); } catch (err) { displayError(pre, err); - continue; + return; } try { @@ -118,5 +118,5 @@ export async function initMarkupCodeMermaid(elMarkup: HTMLElement): Promise Date: Fri, 30 Jan 2026 04:56:21 +0100 Subject: [PATCH 17/36] use index from map --- web_src/js/markup/mermaid.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web_src/js/markup/mermaid.ts b/web_src/js/markup/mermaid.ts index 75679964297b0..e4dbda229aa56 100644 --- a/web_src/js/markup/mermaid.ts +++ b/web_src/js/markup/mermaid.ts @@ -53,7 +53,7 @@ export async function initMarkupCodeMermaid(elMarkup: HTMLElement): Promise { + await Promise.all(els.map(async (el, index) => { const source = sources[index]; const pre = el.closest('pre'); From 846ac7e9177dfb58ff3a55cc5268ed092686ae3f Mon Sep 17 00:00:00 2001 From: silverwind Date: Wed, 4 Feb 2026 21:19:58 +0100 Subject: [PATCH 18/36] use json and yaml parsers --- web_src/js/markup/mermaid.test.ts | 7 +++++++ web_src/js/markup/mermaid.ts | 30 ++++++++++++++++++++++++++---- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/web_src/js/markup/mermaid.test.ts b/web_src/js/markup/mermaid.test.ts index 780e6dcacb3d5..38fd8fb113ec2 100644 --- a/web_src/js/markup/mermaid.test.ts +++ b/web_src/js/markup/mermaid.test.ts @@ -71,6 +71,13 @@ test('sourcesContainElk', () => { A --> C --> B `])).toEqual(true); + expect(sourcesContainElk([` + %%{ init: { "layout": "elkx" } }%% + flowchart TB + A --> B + A --> C --> B + `])).toEqual(false); + expect(sourcesContainElk([` %%{ init: { "flowchart": { "defaultRenderer": "elk" } } }%% flowchart TB diff --git a/web_src/js/markup/mermaid.ts b/web_src/js/markup/mermaid.ts index e4dbda229aa56..7e192ed5c9fe3 100644 --- a/web_src/js/markup/mermaid.ts +++ b/web_src/js/markup/mermaid.ts @@ -3,6 +3,8 @@ import {makeCodeCopyButton} from './codecopy.ts'; import {displayError} from './common.ts'; import {queryElems} from '../utils/dom.ts'; import {html, htmlRaw} from '../utils/html.ts'; +import {load as loadYaml} from 'js-yaml'; +import type {MermaidConfig} from 'mermaid'; const {mermaidMaxSourceCharacters} = window.config; @@ -10,13 +12,33 @@ const iframeCss = `:root {color-scheme: normal} body {margin: 0; padding: 0; overflow: hidden} #mermaid {display: block; margin: 0 auto}`; +function configContainsElk(config: MermaidConfig) { + if (config?.layout === 'elk' || config?.layout?.startsWith('elk.')) return true; + return Object.values(config).some((value) => value?.defaultRenderer?.startsWith('elk')); +} + /** detect whether mermaid sources contain elk layout configuration */ export function sourcesContainElk(sources: Array) { return sources.some((source) => { - // yaml frontmatter - if (/^['"\s]*(layout|defaultRenderer)['"\s]*:['"\s]*elk/m.test(source)) return true; - // json init - if (/"(layout|defaultRenderer)"\s*:\s*"elk/.test(source)) return true; + const frontmatter = (/---\r?\n([\s\S]*?)\r?\n\s*---/.exec(source) || [])[1]; + if (frontmatter) { + try { + return configContainsElk((loadYaml(frontmatter) as {config: MermaidConfig}).config); + } catch { + return false; + } + } + const directive = (/%%\{([\s\S]*)\}%%/.exec(source) || [])[1]; + if (directive) { + const init = (/init\s*:\s*([\s\S]*)$/.exec(directive) || [])[1]; + if (init) { + try { + return configContainsElk((JSON.parse(init.trim().replace(/,(\s*[}\]])/g, '$1')) as MermaidConfig)); + } catch { + return false; + } + } + } return false; }); } From dfc36d3eed49f6e3566ff330f98915e4109e5b7b Mon Sep 17 00:00:00 2001 From: silverwind Date: Wed, 4 Feb 2026 21:25:49 +0100 Subject: [PATCH 19/36] value.defaultRenderer can only be elk as per mermaid types --- web_src/js/markup/mermaid.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web_src/js/markup/mermaid.ts b/web_src/js/markup/mermaid.ts index 7e192ed5c9fe3..87fcef8a2e5ef 100644 --- a/web_src/js/markup/mermaid.ts +++ b/web_src/js/markup/mermaid.ts @@ -14,7 +14,7 @@ body {margin: 0; padding: 0; overflow: hidden} function configContainsElk(config: MermaidConfig) { if (config?.layout === 'elk' || config?.layout?.startsWith('elk.')) return true; - return Object.values(config).some((value) => value?.defaultRenderer?.startsWith('elk')); + return Object.values(config).some((value) => value?.defaultRenderer === 'elk'); } /** detect whether mermaid sources contain elk layout configuration */ From 23f84afbcf077350f1c89602fb528704be4e1808 Mon Sep 17 00:00:00 2001 From: silverwind Date: Wed, 4 Feb 2026 21:34:22 +0100 Subject: [PATCH 20/36] use valid json --- web_src/js/markup/mermaid.test.ts | 6 +++--- web_src/js/markup/mermaid.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/web_src/js/markup/mermaid.test.ts b/web_src/js/markup/mermaid.test.ts index 38fd8fb113ec2..9c71cdd070b93 100644 --- a/web_src/js/markup/mermaid.test.ts +++ b/web_src/js/markup/mermaid.test.ts @@ -88,7 +88,7 @@ test('sourcesContainElk', () => { expect(sourcesContainElk([` %%{ init: { - "layout": "elk", + "layout": "elk" } }%% flowchart TB @@ -99,7 +99,7 @@ test('sourcesContainElk', () => { expect(sourcesContainElk([` %%{ init: { - "layout" : "elk.layered", + "layout" : "elk.layered" } }%% flowchart TB @@ -111,7 +111,7 @@ test('sourcesContainElk', () => { %%{ init: { "flowchart": { - "defaultRenderer": "elk", + "defaultRenderer": "elk" } } }%% diff --git a/web_src/js/markup/mermaid.ts b/web_src/js/markup/mermaid.ts index 87fcef8a2e5ef..1b023b1b10cc0 100644 --- a/web_src/js/markup/mermaid.ts +++ b/web_src/js/markup/mermaid.ts @@ -33,7 +33,7 @@ export function sourcesContainElk(sources: Array) { const init = (/init\s*:\s*([\s\S]*)$/.exec(directive) || [])[1]; if (init) { try { - return configContainsElk((JSON.parse(init.trim().replace(/,(\s*[}\]])/g, '$1')) as MermaidConfig)); + return configContainsElk((JSON.parse(init) as MermaidConfig)); } catch { return false; } From 26a75e152fe9de91c5e8f3963a08c017af884e2b Mon Sep 17 00:00:00 2001 From: silverwind Date: Wed, 4 Feb 2026 21:36:04 +0100 Subject: [PATCH 21/36] use for-of --- web_src/js/markup/mermaid.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/web_src/js/markup/mermaid.ts b/web_src/js/markup/mermaid.ts index 1b023b1b10cc0..a449fbf95eb4a 100644 --- a/web_src/js/markup/mermaid.ts +++ b/web_src/js/markup/mermaid.ts @@ -19,28 +19,28 @@ function configContainsElk(config: MermaidConfig) { /** detect whether mermaid sources contain elk layout configuration */ export function sourcesContainElk(sources: Array) { - return sources.some((source) => { + for (const source of sources) { const frontmatter = (/---\r?\n([\s\S]*?)\r?\n\s*---/.exec(source) || [])[1]; if (frontmatter) { try { - return configContainsElk((loadYaml(frontmatter) as {config: MermaidConfig}).config); - } catch { - return false; - } + if (configContainsElk((loadYaml(frontmatter) as {config: MermaidConfig}).config)) { + return true; + } + } catch {} } const directive = (/%%\{([\s\S]*)\}%%/.exec(source) || [])[1]; if (directive) { const init = (/init\s*:\s*([\s\S]*)$/.exec(directive) || [])[1]; if (init) { try { - return configContainsElk((JSON.parse(init) as MermaidConfig)); - } catch { - return false; - } + if (configContainsElk((JSON.parse(init) as MermaidConfig))) { + return true; + } + } catch {} } } - return false; - }); + } + return false; } async function loadMermaid(sources: Array) { From 176fd16d4f17cd483f58285f7f684c51992f3425 Mon Sep 17 00:00:00 2001 From: silverwind Date: Wed, 4 Feb 2026 21:40:49 +0100 Subject: [PATCH 22/36] add test --- web_src/js/markup/mermaid.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/web_src/js/markup/mermaid.test.ts b/web_src/js/markup/mermaid.test.ts index 9c71cdd070b93..9829d695b84fe 100644 --- a/web_src/js/markup/mermaid.test.ts +++ b/web_src/js/markup/mermaid.test.ts @@ -64,6 +64,17 @@ test('sourcesContainElk', () => { A --> C --> B `])).toEqual(true); + expect(sourcesContainElk([` + --- + config: + layout: noelk + --- + %%{ init: { "layout": "elk" } }%% + flowchart TB + A --> B + A --> C --> B + `])).toEqual(true); + expect(sourcesContainElk([` %%{ init: { "layout": "elk" } }%% flowchart TB From 69b6e3a3d1cb0d7f8abfd9cafb2057ba8f289421 Mon Sep 17 00:00:00 2001 From: silverwind Date: Wed, 4 Feb 2026 21:42:19 +0100 Subject: [PATCH 23/36] add test --- web_src/js/markup/mermaid.test.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/web_src/js/markup/mermaid.test.ts b/web_src/js/markup/mermaid.test.ts index 9829d695b84fe..00a201bec4da8 100644 --- a/web_src/js/markup/mermaid.test.ts +++ b/web_src/js/markup/mermaid.test.ts @@ -75,6 +75,16 @@ test('sourcesContainElk', () => { A --> C --> B `])).toEqual(true); + expect(sourcesContainElk([` + --- + + --- + %%{ init: { "layout": "elk" } }%% + flowchart TB + A --> B + A --> C --> B + `])).toEqual(true); + expect(sourcesContainElk([` %%{ init: { "layout": "elk" } }%% flowchart TB From c4e699c171af370010f22b49c61ebb44da85ae2d Mon Sep 17 00:00:00 2001 From: silverwind Date: Wed, 4 Feb 2026 21:46:34 +0100 Subject: [PATCH 24/36] check source limit in sourcesContainElk --- web_src/js/markup/mermaid.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/web_src/js/markup/mermaid.ts b/web_src/js/markup/mermaid.ts index a449fbf95eb4a..d9fd1924add07 100644 --- a/web_src/js/markup/mermaid.ts +++ b/web_src/js/markup/mermaid.ts @@ -12,6 +12,10 @@ const iframeCss = `:root {color-scheme: normal} body {margin: 0; padding: 0; overflow: hidden} #mermaid {display: block; margin: 0 auto}`; +function isSourceTooLarge(source: string) { + return mermaidMaxSourceCharacters >= 0 && source.length > mermaidMaxSourceCharacters; +} + function configContainsElk(config: MermaidConfig) { if (config?.layout === 'elk' || config?.layout?.startsWith('elk.')) return true; return Object.values(config).some((value) => value?.defaultRenderer === 'elk'); @@ -20,6 +24,8 @@ function configContainsElk(config: MermaidConfig) { /** detect whether mermaid sources contain elk layout configuration */ export function sourcesContainElk(sources: Array) { for (const source of sources) { + if (isSourceTooLarge(source)) continue; + const frontmatter = (/---\r?\n([\s\S]*?)\r?\n\s*---/.exec(source) || [])[1]; if (frontmatter) { try { @@ -40,6 +46,7 @@ export function sourcesContainElk(sources: Array) { } } } + return false; } @@ -83,7 +90,7 @@ export async function initMarkupCodeMermaid(elMarkup: HTMLElement): Promise= 0 && source.length > mermaidMaxSourceCharacters) { + if (isSourceTooLarge(source)) { displayError(pre, new Error(`Mermaid source of ${source.length} characters exceeds the maximum allowed length of ${mermaidMaxSourceCharacters}.`)); return; } From 28d70cc0c3c3e56bdbb7e6c177de9e87a47feddc Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Thu, 5 Feb 2026 20:39:16 +0800 Subject: [PATCH 25/36] fix --- web_src/js/markup/mermaid.test.ts | 141 ++++-------------------------- web_src/js/markup/mermaid.ts | 49 ++++++----- 2 files changed, 47 insertions(+), 143 deletions(-) diff --git a/web_src/js/markup/mermaid.test.ts b/web_src/js/markup/mermaid.test.ts index 00a201bec4da8..1edeb0b2e6e53 100644 --- a/web_src/js/markup/mermaid.test.ts +++ b/web_src/js/markup/mermaid.test.ts @@ -2,142 +2,39 @@ import {sourcesContainElk} from './mermaid.ts'; test('sourcesContainElk', () => { expect(sourcesContainElk([` - flowchart TB - A --> B - A --> C --> B - `])).toEqual(false); - - expect(sourcesContainElk([` - flowchart TB - elk --> B - elk --> C --> B - `])).toEqual(false); - - expect(sourcesContainElk([` - --- - config: - layout : elk - --- - flowchart TB - A --> B - A --> C --> B - `])).toEqual(true); - - expect(sourcesContainElk([` - --- - config: - layout: elk.layered - --- - flowchart TB - A --> B - A --> C --> B - `])).toEqual(true); - - expect(sourcesContainElk([` - --- - config: - "layout": "elk.layered" - --- - flowchart TB - A --> B - A --> C --> B - `])).toEqual(true); - - expect(sourcesContainElk([` - --- - config: - 'layout': 'elk.layered' - --- - flowchart TB - A --> B - A --> C --> B - `])).toEqual(true); - - expect(sourcesContainElk([` - --- - config: - flowchart: - defaultRenderer: elk - --- - flowchart TB - A --> B - A --> C --> B - `])).toEqual(true); - - expect(sourcesContainElk([` - --- - config: - layout: noelk - --- - %%{ init: { "layout": "elk" } }%% - flowchart TB - A --> B - A --> C --> B - `])).toEqual(true); +flowchart TB + elk --> B +`])).toEqual(false); expect(sourcesContainElk([` - --- - - --- - %%{ init: { "layout": "elk" } }%% - flowchart TB - A --> B - A --> C --> B - `])).toEqual(true); - - expect(sourcesContainElk([` - %%{ init: { "layout": "elk" } }%% - flowchart TB - A --> B - A --> C --> B - `])).toEqual(true); +--- +config: + layout : elk +--- +flowchart TB + A --> B +`.trim()])).toEqual(true); expect(sourcesContainElk([` - %%{ init: { "layout": "elkx" } }%% - flowchart TB - A --> B - A --> C --> B - `])).toEqual(false); +--- +config: + layout: elk.layered +--- +flowchart TB + A --> B +`.trim()])).toEqual(true); expect(sourcesContainElk([` %%{ init: { "flowchart": { "defaultRenderer": "elk" } } }%% flowchart TB A --> B - A --> C --> B - `])).toEqual(true); - - expect(sourcesContainElk([` - %%{ - init: { - "layout": "elk" - } - }%% - flowchart TB - A --> B - A --> C --> B `])).toEqual(true); expect(sourcesContainElk([` - %%{ - init: { + %%{init:{ "layout" : "elk.layered" - } - }%% - flowchart TB - A --> B - A --> C --> B - `])).toEqual(true); - - expect(sourcesContainElk([` - %%{ - init: { - "flowchart": { - "defaultRenderer": "elk" - } - } - }%% + }}%% flowchart TB A --> B - A --> C --> B `])).toEqual(true); }); diff --git a/web_src/js/markup/mermaid.ts b/web_src/js/markup/mermaid.ts index d9fd1924add07..67f4396435fb5 100644 --- a/web_src/js/markup/mermaid.ts +++ b/web_src/js/markup/mermaid.ts @@ -12,13 +12,34 @@ const iframeCss = `:root {color-scheme: normal} body {margin: 0; padding: 0; overflow: hidden} #mermaid {display: block; margin: 0 auto}`; +// 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 jsonInitConfigRegex = /%%\{\s*init\s*:\s*(.*?)\}%%/s; + function isSourceTooLarge(source: string) { return mermaidMaxSourceCharacters >= 0 && source.length > mermaidMaxSourceCharacters; } -function configContainsElk(config: MermaidConfig) { +function parseYamlInitConfig(source: string): MermaidConfig | null { + const frontmatter = (yamlFrontMatterRegex.exec(source) || [])[1]; + if (!frontmatter) return null; + try { + return (loadYaml(frontmatter) as {config: MermaidConfig}).config; + } catch {} + return null; +} + +function parseJsonInitConfig(source: string): MermaidConfig | null { + const json = (jsonInitConfigRegex.exec(source) || [])[1]; + try { + return JSON.parse(json); + } catch {} + return null; +} + +function configContainsElk(config: MermaidConfig | null) { if (config?.layout === 'elk' || config?.layout?.startsWith('elk.')) return true; - return Object.values(config).some((value) => value?.defaultRenderer === 'elk'); + return config?.flowchart?.defaultRenderer === 'elk'; } /** detect whether mermaid sources contain elk layout configuration */ @@ -26,25 +47,11 @@ export function sourcesContainElk(sources: Array) { for (const source of sources) { if (isSourceTooLarge(source)) continue; - const frontmatter = (/---\r?\n([\s\S]*?)\r?\n\s*---/.exec(source) || [])[1]; - if (frontmatter) { - try { - if (configContainsElk((loadYaml(frontmatter) as {config: MermaidConfig}).config)) { - return true; - } - } catch {} - } - const directive = (/%%\{([\s\S]*)\}%%/.exec(source) || [])[1]; - if (directive) { - const init = (/init\s*:\s*([\s\S]*)$/.exec(directive) || [])[1]; - if (init) { - try { - if (configContainsElk((JSON.parse(init) as MermaidConfig))) { - return true; - } - } catch {} - } - } + const yamlConfig = parseYamlInitConfig(source); + if (configContainsElk(yamlConfig)) return true; + + const jsonConfig = parseJsonInitConfig(source); + if (configContainsElk(jsonConfig)) return true; } return false; From 313c895b3d5824a43653e06382e5f8b39c4f156a Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Thu, 5 Feb 2026 20:46:26 +0800 Subject: [PATCH 26/36] fix --- web_src/js/markup/mermaid.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/web_src/js/markup/mermaid.ts b/web_src/js/markup/mermaid.ts index 67f4396435fb5..45c35a71e2484 100644 --- a/web_src/js/markup/mermaid.ts +++ b/web_src/js/markup/mermaid.ts @@ -14,6 +14,7 @@ body {margin: 0; padding: 0; overflow: hidden} // 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; +// https://mermaid.js.org/config/directives.html#declaring-directives const jsonInitConfigRegex = /%%\{\s*init\s*:\s*(.*?)\}%%/s; function isSourceTooLarge(source: string) { @@ -24,15 +25,16 @@ function parseYamlInitConfig(source: string): MermaidConfig | null { const frontmatter = (yamlFrontMatterRegex.exec(source) || [])[1]; if (!frontmatter) return null; try { - return (loadYaml(frontmatter) as {config: MermaidConfig}).config; + return (loadYaml(frontmatter) as {config: MermaidConfig})?.config; } catch {} return null; } function parseJsonInitConfig(source: string): MermaidConfig | null { - const json = (jsonInitConfigRegex.exec(source) || [])[1]; + const jsonInit = (jsonInitConfigRegex.exec(source) || [])[1]; + if (!jsonInit) return null; try { - return JSON.parse(json); + return JSON.parse(jsonInit); } catch {} return null; } From 02a8093f45023592c5ccefd51dc3f8120817a0a9 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Thu, 5 Feb 2026 20:51:53 +0800 Subject: [PATCH 27/36] fix --- web_src/js/markup/mermaid.test.ts | 2 +- web_src/js/markup/mermaid.ts | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/web_src/js/markup/mermaid.test.ts b/web_src/js/markup/mermaid.test.ts index 1edeb0b2e6e53..6ea2cc9a79938 100644 --- a/web_src/js/markup/mermaid.test.ts +++ b/web_src/js/markup/mermaid.test.ts @@ -25,7 +25,7 @@ flowchart TB `.trim()])).toEqual(true); expect(sourcesContainElk([` - %%{ init: { "flowchart": { "defaultRenderer": "elk" } } }%% + %%{ init : { "flowchart": { "defaultRenderer": "elk" } } }%% flowchart TB A --> B `])).toEqual(true); diff --git a/web_src/js/markup/mermaid.ts b/web_src/js/markup/mermaid.ts index 45c35a71e2484..2cfb7e617e055 100644 --- a/web_src/js/markup/mermaid.ts +++ b/web_src/js/markup/mermaid.ts @@ -12,16 +12,13 @@ const iframeCss = `:root {color-scheme: normal} body {margin: 0; padding: 0; overflow: hidden} #mermaid {display: block; margin: 0 auto}`; -// 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; -// https://mermaid.js.org/config/directives.html#declaring-directives -const jsonInitConfigRegex = /%%\{\s*init\s*:\s*(.*?)\}%%/s; - 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 { @@ -31,6 +28,8 @@ function parseYamlInitConfig(source: string): MermaidConfig | null { } function parseJsonInitConfig(source: string): MermaidConfig | null { + // https://mermaid.js.org/config/directives.html#declaring-directives + const jsonInitConfigRegex = /%%\{\s*init\s*:\s*(.*?)\}%%/s; const jsonInit = (jsonInitConfigRegex.exec(source) || [])[1]; if (!jsonInit) return null; try { From 2d5a66ab4847d85aae23e9e15b96e043bc615a73 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Thu, 5 Feb 2026 20:59:19 +0800 Subject: [PATCH 28/36] fix --- web_src/js/markup/mermaid.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/web_src/js/markup/mermaid.ts b/web_src/js/markup/mermaid.ts index 2cfb7e617e055..a06584cc5c777 100644 --- a/web_src/js/markup/mermaid.ts +++ b/web_src/js/markup/mermaid.ts @@ -23,18 +23,26 @@ function parseYamlInitConfig(source: string): MermaidConfig | null { if (!frontmatter) return null; try { return (loadYaml(frontmatter) as {config: MermaidConfig})?.config; - } catch {} + } 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 + // TODO: mermaid is quite hacky for the "JSON" pattern, it can have other root keys + // and it can even accept invalid JSON string like: + // %%{initialize: { 'logLevel': 'fatal', "theme":'dark', 'startOnLoad': true } }%% + // To keep things simple, Gitea only supports "init" directive with valid JSON string at the moment. const jsonInitConfigRegex = /%%\{\s*init\s*:\s*(.*?)\}%%/s; const jsonInit = (jsonInitConfigRegex.exec(source) || [])[1]; if (!jsonInit) return null; try { return JSON.parse(jsonInit); - } catch {} + } catch { + console.error('invalid or unsupported mermaid init JSON config', jsonInit); + } return null; } From a7bfdb78ef3722fdd189adae14ba02d02244504d Mon Sep 17 00:00:00 2001 From: silverwind Date: Thu, 5 Feb 2026 18:46:17 +0100 Subject: [PATCH 29/36] restore Object.values method --- web_src/js/markup/mermaid.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/web_src/js/markup/mermaid.ts b/web_src/js/markup/mermaid.ts index a06584cc5c777..a46769bcab7c1 100644 --- a/web_src/js/markup/mermaid.ts +++ b/web_src/js/markup/mermaid.ts @@ -46,9 +46,12 @@ function parseJsonInitConfig(source: string): MermaidConfig | null { return null; } +function isElk(layout: string | undefined) { + return Boolean(layout === 'elk' || layout?.startsWith?.('elk.')); +} + function configContainsElk(config: MermaidConfig | null) { - if (config?.layout === 'elk' || config?.layout?.startsWith('elk.')) return true; - return config?.flowchart?.defaultRenderer === 'elk'; + return isElk(config?.layout) || Object.values(config || {}).some((value) => isElk(value?.defaultRenderer)); } /** detect whether mermaid sources contain elk layout configuration */ From 9bb7e28f5a6516a3a47084ecf15eab128d6e5878 Mon Sep 17 00:00:00 2001 From: silverwind Date: Thu, 5 Feb 2026 18:47:26 +0100 Subject: [PATCH 30/36] rename variable --- web_src/js/markup/mermaid.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web_src/js/markup/mermaid.ts b/web_src/js/markup/mermaid.ts index a46769bcab7c1..5ab0579e1f92c 100644 --- a/web_src/js/markup/mermaid.ts +++ b/web_src/js/markup/mermaid.ts @@ -46,8 +46,8 @@ function parseJsonInitConfig(source: string): MermaidConfig | null { return null; } -function isElk(layout: string | undefined) { - return Boolean(layout === 'elk' || layout?.startsWith?.('elk.')); +function isElk(layoutOrRenderer: string | undefined) { + return Boolean(layoutOrRenderer === 'elk' || layoutOrRenderer?.startsWith?.('elk.')); } function configContainsElk(config: MermaidConfig | null) { From f68d5a0b9fbfcbb7c0833193f35e78e0cda1cd3c Mon Sep 17 00:00:00 2001 From: silverwind Date: Thu, 5 Feb 2026 18:53:54 +0100 Subject: [PATCH 31/36] add dedent test helper --- web_src/js/markup/mermaid.test.ts | 41 ++++++++++++++++--------------- web_src/js/utils/testhelper.ts | 16 ++++++++++++ 2 files changed, 37 insertions(+), 20 deletions(-) diff --git a/web_src/js/markup/mermaid.test.ts b/web_src/js/markup/mermaid.test.ts index 6ea2cc9a79938..128ee9bd06a31 100644 --- a/web_src/js/markup/mermaid.test.ts +++ b/web_src/js/markup/mermaid.test.ts @@ -1,28 +1,29 @@ import {sourcesContainElk} from './mermaid.ts'; +import {dedent} from '../utils/testhelper.ts'; test('sourcesContainElk', () => { - expect(sourcesContainElk([` -flowchart TB - elk --> B -`])).toEqual(false); + expect(sourcesContainElk([dedent(` + flowchart TB + elk --> B + `)])).toEqual(false); - expect(sourcesContainElk([` ---- -config: - layout : elk ---- -flowchart TB - A --> B -`.trim()])).toEqual(true); + expect(sourcesContainElk([dedent(` + --- + config: + layout : elk + --- + flowchart TB + A --> B + `)])).toEqual(true); - expect(sourcesContainElk([` ---- -config: - layout: elk.layered ---- -flowchart TB - A --> B -`.trim()])).toEqual(true); + expect(sourcesContainElk([dedent(` + --- + config: + layout: elk.layered + --- + flowchart TB + A --> B + `)])).toEqual(true); expect(sourcesContainElk([` %%{ init : { "flowchart": { "defaultRenderer": "elk" } } }%% diff --git a/web_src/js/utils/testhelper.ts b/web_src/js/utils/testhelper.ts index 0e0aff9fa3ce8..59eb39778c232 100644 --- a/web_src/js/utils/testhelper.ts +++ b/web_src/js/utils/testhelper.ts @@ -4,3 +4,19 @@ export function isInFrontendUnitTest() { return import.meta.env.TEST === 'true'; } + +/** 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 = Number.POSITIVE_INFINITY; + for (const indent of match) { + minIndent = Math.min(minIndent, indent.length); + } + if (minIndent === 0 || minIndent === Number.POSITIVE_INFINITY) { + return str; + } + + return str.replace(new RegExp(`^[ \\t]{${minIndent}}`, 'gm'), '').trim(); +} From d74878cc5032e663b54d54cac4365fe7ace544c3 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 6 Feb 2026 03:47:51 +0100 Subject: [PATCH 32/36] Apply suggestion from @silverwind Signed-off-by: silverwind --- web_src/js/markup/mermaid.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/web_src/js/markup/mermaid.ts b/web_src/js/markup/mermaid.ts index 5ab0579e1f92c..51ecb67d89186 100644 --- a/web_src/js/markup/mermaid.ts +++ b/web_src/js/markup/mermaid.ts @@ -46,6 +46,7 @@ function parseJsonInitConfig(source: string): MermaidConfig | null { return null; } +/** checks if either `config.layout` or `config.*.defaultRender` contains a elk layout. */ function isElk(layoutOrRenderer: string | undefined) { return Boolean(layoutOrRenderer === 'elk' || layoutOrRenderer?.startsWith?.('elk.')); } From 5766550317050764d3d29e62f22b67cac5014102 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 6 Feb 2026 03:48:34 +0100 Subject: [PATCH 33/36] Apply suggestion from @silverwind Signed-off-by: silverwind --- web_src/js/markup/mermaid.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/web_src/js/markup/mermaid.ts b/web_src/js/markup/mermaid.ts index 51ecb67d89186..5ab0579e1f92c 100644 --- a/web_src/js/markup/mermaid.ts +++ b/web_src/js/markup/mermaid.ts @@ -46,7 +46,6 @@ function parseJsonInitConfig(source: string): MermaidConfig | null { return null; } -/** checks if either `config.layout` or `config.*.defaultRender` contains a elk layout. */ function isElk(layoutOrRenderer: string | undefined) { return Boolean(layoutOrRenderer === 'elk' || layoutOrRenderer?.startsWith?.('elk.')); } From ddcd9338287d3f7fe15c6cda26918ac2daa5a47a Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 6 Feb 2026 03:48:52 +0100 Subject: [PATCH 34/36] Apply suggestion from @silverwind Signed-off-by: silverwind --- web_src/js/markup/mermaid.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/web_src/js/markup/mermaid.ts b/web_src/js/markup/mermaid.ts index 5ab0579e1f92c..013571ed141f3 100644 --- a/web_src/js/markup/mermaid.ts +++ b/web_src/js/markup/mermaid.ts @@ -50,6 +50,7 @@ function isElk(layoutOrRenderer: string | undefined) { return Boolean(layoutOrRenderer === 'elk' || layoutOrRenderer?.startsWith?.('elk.')); } +/** checks if either `config.layout` or `config.*.defaultRender` contains a elk layout. */ function configContainsElk(config: MermaidConfig | null) { return isElk(config?.layout) || Object.values(config || {}).some((value) => isElk(value?.defaultRenderer)); } From 9ed216b930f10ee1514585b79318b10ed6d2f111 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Fri, 6 Feb 2026 22:24:34 +0800 Subject: [PATCH 35/36] fix --- web_src/js/markup/mermaid.test.ts | 19 +++++++++++++++++++ web_src/js/markup/mermaid.ts | 14 ++++++++++---- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/web_src/js/markup/mermaid.test.ts b/web_src/js/markup/mermaid.test.ts index 128ee9bd06a31..69b468b9b3a7c 100644 --- a/web_src/js/markup/mermaid.test.ts +++ b/web_src/js/markup/mermaid.test.ts @@ -31,6 +31,16 @@ test('sourcesContainElk', () => { A --> B `])).toEqual(true); + expect(sourcesContainElk([` + --- + config: + layout: 123 + --- + %%{ init : { "class": { "defaultRenderer": "elk.any" } } }%% + flowchart TB + A --> B + `])).toEqual(true); + expect(sourcesContainElk([` %%{init:{ "layout" : "elk.layered" @@ -38,4 +48,13 @@ test('sourcesContainElk', () => { flowchart TB A --> B `])).toEqual(true); + + // TODO: mermaid supports invalid JSON, but we don't support it at the moment + expect(sourcesContainElk([` + %%{init:{ + 'layout' : 'elk.layered' + }}%% + flowchart TB + A --> B + `])).toEqual(false); }); diff --git a/web_src/js/markup/mermaid.ts b/web_src/js/markup/mermaid.ts index 013571ed141f3..73dd6618302c3 100644 --- a/web_src/js/markup/mermaid.ts +++ b/web_src/js/markup/mermaid.ts @@ -46,13 +46,19 @@ function parseJsonInitConfig(source: string): MermaidConfig | null { return null; } -function isElk(layoutOrRenderer: string | undefined) { - return Boolean(layoutOrRenderer === 'elk' || layoutOrRenderer?.startsWith?.('elk.')); +function configValueIsElk(layoutOrRenderer: string | undefined) { + if (typeof layoutOrRenderer !== 'string') return false; + return layoutOrRenderer === 'elk' || layoutOrRenderer.startsWith('elk.'); } -/** checks if either `config.layout` or `config.*.defaultRender` contains a elk layout. */ function configContainsElk(config: MermaidConfig | null) { - return isElk(config?.layout) || Object.values(config || {}).some((value) => isElk(value?.defaultRenderer)); + 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((value) => configValueIsElk(value?.defaultRenderer)); } /** detect whether mermaid sources contain elk layout configuration */ From bcfb75c4a3a0edc6cf7f550456270c46631905ab Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sat, 7 Feb 2026 10:20:49 +0800 Subject: [PATCH 36/36] fix --- web_src/js/markup/mermaid.test.ts | 5 ++--- web_src/js/markup/mermaid.ts | 18 +++++++++--------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/web_src/js/markup/mermaid.test.ts b/web_src/js/markup/mermaid.test.ts index 69b468b9b3a7c..19c396365878c 100644 --- a/web_src/js/markup/mermaid.test.ts +++ b/web_src/js/markup/mermaid.test.ts @@ -49,12 +49,11 @@ test('sourcesContainElk', () => { A --> B `])).toEqual(true); - // TODO: mermaid supports invalid JSON, but we don't support it at the moment expect(sourcesContainElk([` - %%{init:{ + %%{ initialize: { 'layout' : 'elk.layered' }}%% flowchart TB A --> B - `])).toEqual(false); + `])).toEqual(true); }); diff --git a/web_src/js/markup/mermaid.ts b/web_src/js/markup/mermaid.ts index 73dd6618302c3..0314a6177c47b 100644 --- a/web_src/js/markup/mermaid.ts +++ b/web_src/js/markup/mermaid.ts @@ -31,17 +31,17 @@ function parseYamlInitConfig(source: string): MermaidConfig | null { function parseJsonInitConfig(source: string): MermaidConfig | null { // https://mermaid.js.org/config/directives.html#declaring-directives - // TODO: mermaid is quite hacky for the "JSON" pattern, it can have other root keys - // and it can even accept invalid JSON string like: - // %%{initialize: { 'logLevel': 'fatal', "theme":'dark', 'startOnLoad': true } }%% - // To keep things simple, Gitea only supports "init" directive with valid JSON string at the moment. - const jsonInitConfigRegex = /%%\{\s*init\s*:\s*(.*?)\}%%/s; - const jsonInit = (jsonInitConfigRegex.exec(source) || [])[1]; - if (!jsonInit) return null; + // 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 { - return JSON.parse(jsonInit); + const processed = jsonInitText.trim().replace(/'/g, '"'); + return JSON.parse(processed); } catch { - console.error('invalid or unsupported mermaid init JSON config', jsonInit); + console.error('invalid or unsupported mermaid init JSON config', jsonInitText); } return null; }