From e68d10d7d9451fd31a105342c8cf100c0dacf92c Mon Sep 17 00:00:00 2001 From: Cell <1024@lruihao.cn> Date: Wed, 29 Apr 2026 16:42:15 +0800 Subject: [PATCH] refactor(mermaid): performance optimization and support pan and zoom etc. (#737) * perf(mermaid): optimize rendering by deferring non-critical tasks and enhancing viewport observation * feat(mermaid): add panzoom and download svg actions for mermaid * feat(mermaid): enhance diagram controls and add fullscreen action for code tabs * feat(i18n): update translation keys for diagram and tab actions in multiple languages * fix(mermaid): move mermaid-neutral inside diagram-container in wrapper mode Agent-Logs-Url: https://github.com/hugo-fixit/FixIt/sessions/e79e837e-be0b-445c-8bd3-8c03cdce2651 Co-authored-by: Lruihao <33419593+Lruihao@users.noreply.github.com> * fix(mermaid): update download filename for SVG export in diagram controls * refactor(mermaid): refactor rendering logic and enhance task queue for improved performance --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> --- .github/copilot-instructions.md | 1 - .vscode/extensions.json | 3 +- apps/test/content/posts/codeblock-test.md | 169 ++++ .../content/posts/mermaid-diagrams-test.md | 13 +- assets/data/cdn/jsdelivr.yml | 2 + assets/data/cdn/unpkg.yml | 2 + assets/js/lib/mermaid.js | 739 ++++++++++++++++-- assets/js/theme.js | 70 +- assets/lib/panzoom/panzoom.min.js | 16 + assets/scss/core/_common.scss | 8 + assets/scss/core/_reboot.scss | 2 +- assets/scss/pages/single/_code.scss | 57 +- .../pages/single/shortcodes/_mermaid.scss | 94 ++- hugo.toml | 6 + i18n/de.toml | 17 +- i18n/en.toml | 17 +- i18n/es.toml | 17 +- i18n/fr.toml | 17 +- i18n/hi.toml | 17 +- i18n/it.toml | 17 +- i18n/ja.toml | 17 +- i18n/ko.toml | 17 +- i18n/pl.toml | 17 +- i18n/pt-BR.toml | 17 +- i18n/ro.toml | 17 +- i18n/ru.toml | 17 +- i18n/sr.toml | 17 +- i18n/vi.toml | 17 +- i18n/zh-CN.toml | 17 +- i18n/zh-TW.toml | 17 +- layouts/_partials/function/code-header.html | 2 +- layouts/_partials/layouts/assets.html | 4 + layouts/_partials/plugin/mermaid.html | 63 +- librarybot.yml | 6 + 34 files changed, 1352 insertions(+), 177 deletions(-) create mode 100644 assets/lib/panzoom/panzoom.min.js diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index cb4a36cb..4d5da897 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -85,7 +85,6 @@ FixIt/ ```scss @include border-radius; - @include transition(all 0.2s ease); ``` ### 代码质量要求 diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 4fbd2285..ce684cdd 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -3,6 +3,7 @@ "budparr.language-hugo-vscode", "kaellarkin.hugo-shortcode-syntax", "lruihao.hugo-partials-refs", - "tamasfe.even-better-toml" + "tamasfe.even-better-toml", + "casualjim.gotemplate" ] } diff --git a/apps/test/content/posts/codeblock-test.md b/apps/test/content/posts/codeblock-test.md index 312fec0a..dc2be99a 100644 --- a/apps/test/content/posts/codeblock-test.md +++ b/apps/test/content/posts/codeblock-test.md @@ -192,6 +192,175 @@ mode = 'classic' wrapper = true ``` +## Mermaid + +```mermaid +flowchart TD + A[Start] --> B{Is it?} + B -->|Yes| C[OK] + C --> D[Rethink] + D --> B + B ---->|No| E[End] +``` + +## ECharts + +```echarts +[title] +text = "Summary Line Chart" +top = "2%" +left = "center" + +[tooltip] +trigger = "axis" + +[legend] +data = [ + "Email Marketing", + "Affiliate Advertising", + "Video Advertising", + "Direct View", + "Search Engine" +] +top = "10%" + +[grid] +left = "5%" +right = "5%" +bottom = "5%" +top = "20%" +containLabel = true + +[toolbox] +left = "left" +[toolbox.feature] +[toolbox.feature.saveAsImage] +title = "Save as Image" + +[xAxis] +type = "category" +boundaryGap = false +data = [ + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + "Sunday" +] + +[yAxis] +type = "value" + +[[series]] +name = "Email Marketing" +type = "line" +stack = "Total" +data = [ + 120.0, + 132.0, + 101.0, + 134.0, + 90.0, + 230.0, + 210.0 +] + +[[series]] +name = "Affiliate Advertising" +type = "line" +stack = "Total" +data = [ + 220.0, + 182.0, + 191.0, + 234.0, + 290.0, + 330.0, + 310.0 +] + +[[series]] +name = "Video Advertising" +type = "line" +stack = "Total" +data = [ + 150.0, + 232.0, + 201.0, + 154.0, + 190.0, + 330.0, + 410.0 +] + +[[series]] +name = "Direct View" +type = "line" +stack = "Total" +data = [ + 320.0, + 332.0, + 301.0, + 334.0, + 390.0, + 330.0, + 320.0 +] + +[[series]] +name = "Search Engine" +type = "line" +stack = "Total" +data = [ + 820.0, + 932.0, + 901.0, + 934.0, + 1290.0, + 1330.0, + 1320.0 +] +``` + +## Timeline + +```timeline +events: + - timestamp: 2024-07-11 + content: Success + - timestamp: 2024-07-13 + content: Approved + - timestamp: 2024-07-15 + content: Event start +``` + +## JSON + +```json +{ + "name": "FixIt", + "version": "0.4.0", + "description": "A clean, elegant but advanced blog theme for Hugo", + "keywords": ["Hugo", "theme", "FixIt"] +} +``` + +## File Tree + +{{< file-tree >}} +- name: src + type: dir + children: + - name: index.ts + type: file + - name: app.ts + type: file +- name: package.json + type: file +{{< /file-tree >}} + ## Code block inside other blocks > [!NOTE]+ diff --git a/apps/test/content/posts/mermaid-diagrams-test.md b/apps/test/content/posts/mermaid-diagrams-test.md index b4bd0581..a5d24e6c 100644 --- a/apps/test/content/posts/mermaid-diagrams-test.md +++ b/apps/test/content/posts/mermaid-diagrams-test.md @@ -13,6 +13,17 @@ tags: The FixIt theme supports two ways to use Mermaid: code block syntax and shortcode syntax. +```mermaid +--- +config: + theme: 'forest' +--- + graph TD + a --> b +``` + + + ## Flowchart ```mermaid @@ -24,8 +35,6 @@ flowchart TD B ---->|No| E[End] ``` - - ## Sequence Diagram ```mermaid diff --git a/assets/data/cdn/jsdelivr.yml b/assets/data/cdn/jsdelivr.yml index 4bdd03b3..30f00462 100644 --- a/assets/data/cdn/jsdelivr.yml +++ b/assets/data/cdn/jsdelivr.yml @@ -63,6 +63,8 @@ libFiles: paceJS: pace-js@1.2.4 # pangu.js@4.0.7 https://github.com/vinta/pangu.js panguJS: pangu@4.0.7/dist/browser/pangu.min.js + # @panzoom/panzoom@4.6.2 https://github.com/timmywil/panzoom + panzoomJS: '@panzoom/panzoom@4.6.2/dist/panzoom.min.js' # sharer.js@0.5.1 https://github.com/ellisonleao/sharer.js sharerJS: sharer.js@0.5.1/sharer.min.js # @github/tab-container-element@4.8.2 https://github.com/github/tab-container-element diff --git a/assets/data/cdn/unpkg.yml b/assets/data/cdn/unpkg.yml index dcd850b8..c2cf196c 100644 --- a/assets/data/cdn/unpkg.yml +++ b/assets/data/cdn/unpkg.yml @@ -63,6 +63,8 @@ libFiles: paceJS: pace-js@1.2.4 # pangu.js@4.0.7 https://github.com/vinta/pangu.js panguJS: pangu@4.0.7/dist/browser/pangu.min.js + # @panzoom/panzoom@4.6.2 https://github.com/timmywil/panzoom + panzoomJS: '@panzoom/panzoom@4.6.2/dist/panzoom.min.js' # sharer.js@0.5.1 https://github.com/ellisonleao/sharer.js sharerJS: sharer.js@0.5.1/sharer.min.js # @github/tab-container-element@4.8.2 https://github.com/github/tab-container-element diff --git a/assets/js/lib/mermaid.js b/assets/js/lib/mermaid.js index da0f015a..d53b4d47 100644 --- a/assets/js/lib/mermaid.js +++ b/assets/js/lib/mermaid.js @@ -1,6 +1,7 @@ {{- /* Page level params is not supported */ -}} {{- $mermaid := .Site.Params.mermaid -}} {{- $mermaidCDN := $mermaid.cdn | default "https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.esm.min.mjs" -}} +{{- $wrapped := $mermaid.wrapper | default true -}} import mermaid from "{{ $mermaidCDN }}" {{- with $mermaid.zenuml }} @@ -16,109 +17,675 @@ await mermaid.registerExternalDiagrams([zenuml]) {{- end }} const loaders = [] for(const item of {{ $loadersArr }}) { - if(Array.isArray(item)) { - loaders.push(...item) - } else { - loaders.push(item) - } + if(Array.isArray(item)) { + loaders.push(...item) + } else { + loaders.push(item) + } } mermaid.registerLayoutLoaders(loaders) mermaid.startOnLoad = false; -const mermaidConfig = {{ $mermaid | jsonify }} -let processing = false -let delayTask = null +const config = {{ $mermaid | jsonify }} +let tabContainerEventBound = false +let mermaidThemeRerenderBound = false +let mermaidContainerObserver = null +let mermaidRenderLock = Promise.resolve() +let mermaidConfigKey = '' +let mermaidIdSeq = 0 +{{- if $wrapped }} +let panzoomInstances = null +let panzoomWheelHosts = null +let panzoomEventBound = false +let mermaidThemeSyncBound = false +let mermaidPanzoomGroups = null +{{- end }} /** - * Get current/next theme - * @param {Boolean} [next] whether to get the next theme - * @returns + * Waits for the next animation frame. + * @returns {Promise} */ -function getTheme(next) { - const darkMode = document.documentElement.dataset.theme === 'dark' - return (next ? !darkMode : darkMode) - ? mermaidConfig.themes[1] - : mermaidConfig.themes[0] +function nextFrame() { + return new Promise((resolve) => requestAnimationFrame(resolve)) +} + +{{- if $wrapped }} +/** + * Gets (and initializes) the panzoom shared state for a Mermaid diagram container. + * + * The group is used to keep zoom/pan transform in sync across theme layers + * (e.g. `.mermaid` vs `.mermaid-dark`) within the same `.diagram-container`. + * + * @param {SVGElement} svg + */ +function getMermaidPanzoomGroup(svg) { + if (!svg?.closest?.('.diagram-view')) return null + const container = svg?.closest?.('.diagram-container') + if (!container) return null + if (!mermaidPanzoomGroups) mermaidPanzoomGroups = new WeakMap() + let group = mermaidPanzoomGroups.get(container) + if (!group) { + group = { container, transform: null, svgs: new Set(), lastSvg: null } + mermaidPanzoomGroups.set(container, group) + } + const svgs = container.querySelectorAll('.mermaid svg, .mermaid-dark svg') + svgs.forEach((s) => group.svgs.add(s)) + return group } /** - * Initialize Mermaid by loading the specified theme - * @param {String} [theme] specific theme to load - * @param {Boolean} [darkMode] whether to use dark mode - * @param {String} [selector] specific selector for the elements to process - * @returns + * Applies a saved pan/zoom transform via CSS transform. + * @param {SVGElement} svg + * @param {any} transform + * @param {number} transform.x + * @param {number} transform.y + * @param {number} transform.scale */ -async function loadMermaid({ theme, darkMode, selector }) { - if (processing) { - console.warn('Mermaid is still processing, delaying the reload.') - delayTask = () => loadMermaid({ theme, darkMode, selector }) +function applyPanzoomTransformToSvg(svg, transform) { + if (!svg || !transform) return + svg.style.transform = `scale(${transform.scale}) translate(${transform.x}px, ${transform.y}px)` +} + +/** + * Applies a saved pan/zoom transform via Panzoom instance methods. + * @param {any} instance + * @param {any} transform + * @param {number} transform.x + * @param {number} transform.y + * @param {number} transform.scale + */ +function applyPanzoomTransform(instance, transform) { + if (!instance || !transform) return + try { instance.zoom(transform.scale, { animate: false, force: true }) } catch {} + try { instance.pan(transform.x, transform.y, { animate: false, force: true }) } catch {} +} +{{- end }} + +/** + * Schedule task at idle time to avoid blocking critical rendering. + * @param {Function} task + * @param {number} [timeout=0] - Timeout in milliseconds for the task to run. + */ +function runWhenIdle(task, timeout = 0) { + if (typeof window.requestIdleCallback === 'function') { + window.requestIdleCallback(task, { timeout }) return } - const isDarkMode = darkMode ?? (document.documentElement.dataset.theme === 'dark') - const currentTheme = theme ?? getTheme() - const querySelector = selector ?? (isDarkMode ? '.mermaid-dark' : '.mermaid') - const nodes = Array.from(document.querySelectorAll(querySelector)) - .filter((node) => { - const p = node.closest('.tab-panel') - if (!p) return true - const style = getComputedStyle(p) - return style.height !== 'auto' || style.width !== 'auto' - }) - - if (!nodes.length) return - - console.time(`Loaded [${currentTheme}]`) - processing = true - nodes.forEach((node) => { - node.toggleAttribute('data-processing', true) - }) - - // https://mermaid.js.org/config/schema-docs/config.html - mermaid.initialize({ - startOnLoad: false, - darkMode: isDarkMode, - theme: currentTheme, - securityLevel: mermaidConfig.securitylevel, - look: mermaidConfig.look, - layout: mermaidConfig.layout, - fontFamily: mermaidConfig.fontfamily, - altFontFamily: mermaidConfig.fontfamily - }) - // skip data-processed elements in Mermaid run function, so this won't re-render - await mermaid.run({ - nodes, - suppressErrors: true, - postRenderCallback: (svgId) => { - document.getElementById(svgId).parentElement.toggleAttribute('data-processing', false) - }, - }) - processing = false - console.timeEnd(`Loaded [${currentTheme}]`) - - if (delayTask && typeof delayTask === 'function') { - delayTask(); - delayTask = null; - // console.log('Delayed task executed'); - } + window.setTimeout(task, 80) } -async function initMermaid() { - const mermaidElements = document.querySelectorAll('.mermaid, .mermaid-dark, .mermaid-neutral') +/** + * Checks whether an element is in the viewport (vertically). + * @param {Element} node + * @returns {boolean} + */ +function isNodeInViewport(node) { + const rect = node.getBoundingClientRect() + return rect.top < window.innerHeight && rect.bottom > 0 +} + +/** + * Checks whether a node is currently visible and should be rendered first. + * @param {Element} node + * @returns {boolean} + */ +function isNodeVisuallyActive(node) { + if (!isRenderContextActive(node)) return false + const nodeStyle = getComputedStyle(node) + if (nodeStyle.display === 'none' || nodeStyle.visibility === 'hidden') return false + const rect = node.getBoundingClientRect() + return rect.width > 0 && rect.height > 0 +} + +/** + * Checks whether an element is inside an active renderable context. + * + * This is stricter than "is in the DOM": it excludes content hidden by + * tab panels, diagram/code toggles, or common hidden containers. + * + * @param {Element} el + * @returns {boolean} + */ +function isRenderContextActive(el) { + if (!el?.isConnected) return false + if (el.closest?.('[hidden], .d-none, [aria-hidden="true"]')) return false + const diagramView = el.closest?.('.diagram-view') + if (diagramView?.closest?.('.diagram-tabs') && !diagramView.classList.contains('active')) return false + const panel = el.closest?.('.tab-panel') + if (panel?.hidden) return false + return true +} + +/** + * Check whether the current effective theme is dark. + * In auto mode, this follows the system color scheme preference. + * @returns {boolean} Whether dark mode is currently active. + */ +function isDarkMode() { + const themeMode = document.documentElement.dataset.themeMode || 'auto'; + return themeMode === 'auto' + ? window.matchMedia('(prefers-color-scheme: dark)').matches + : themeMode === 'dark'; +} + +/** + * Gets Mermaid theme name for the current effective color scheme. + * `.mermaid` uses themes[0] (default), `.mermaid-dark` uses themes[1] (dark). + * @returns {string} + */ +function getTheme() { + return isDarkMode() ? config.themes[1] : config.themes[0] +} + +/** + * Mermaid uses global runtime config; `initialize()` changes subsequent `render()` behavior. + * This helper serializes all render work to avoid concurrent initialize/render cross-talk. + * @param {Function} task + * @returns {Promise} + */ +function withMermaidLock(task) { + mermaidRenderLock = mermaidRenderLock.then(task, task) + return mermaidRenderLock +} + +/** + * Initializes Mermaid only when (theme, darkMode) changes to reduce work and keep it stable. + * @param {string} theme + * @param {boolean} darkMode + */ +function ensureMermaidInitialized(theme, darkMode) { + const key = `${String(theme)}|${darkMode ? '1' : '0'}` + if (key === mermaidConfigKey) return + mermaidConfigKey = key + mermaid.initialize({ + startOnLoad: false, + darkMode, + theme, + securityLevel: config.securitylevel, + look: config.look, + layout: config.layout, + fontFamily: config.fontfamily, + altFontFamily: config.fontfamily + }) +} + +/** + * Renders a single Mermaid host element (`.mermaid`, `.mermaid-dark`, `.mermaid-neutral`). + * + * Notes: + * - Source uses `textContent` to avoid HTML entities (e.g. `>`) breaking Mermaid parsing. + * - Uses `data-processing` / `data-processed` as state flags. + * - Emits `fixit:mermaid-rendered` to hook Panzoom and other post-render behaviors. + * + * @param {Element} el + * @param {Object} options + * @param {string} options.theme + * @param {boolean} options.darkMode + */ +async function renderMermaidElement(el, { theme, darkMode } = {}) { + if (!el || el.hasAttribute('data-processed') || el.hasAttribute('data-processing')) return + if (!isRenderContextActive(el)) return + const source = el.textContent.trim() + if (!source) { + el.setAttribute('data-processed', '') + return + } + + await withMermaidLock(async () => { + if (!el || el.hasAttribute('data-processed') || !isRenderContextActive(el)) return + if (!el.hasAttribute('data-processing')) el.setAttribute('data-processing', '') + ensureMermaidInitialized(theme, darkMode) + const id = `fixit-mermaid-${++mermaidIdSeq}` + try { + const result = await mermaid.render(id, source) + const svg = result?.svg || '' + el.innerHTML = svg + if (typeof result?.bindFunctions === 'function') { + try { result.bindFunctions(el) } catch {} + } + const svgEl = el.querySelector('svg') + if (svgEl && !svgEl.id) svgEl.id = id + el.removeAttribute('data-processing') + el.setAttribute('data-processed', '') + const svgId = svgEl?.id || id + if (svgId) { + const group = svgEl ? getMermaidPanzoomGroup(svgEl) : null + if (svgEl && group?.transform) applyPanzoomTransformToSvg(svgEl, group.transform) + document.dispatchEvent(new CustomEvent('fixit:mermaid-rendered', { detail: { svgId } })) + } + } catch (e) { + el.removeAttribute('data-processing') + console.warn('FixIt Mermaid render failed:', e) + } + }) +} + +function collectMermaidContainers(root = document) { + return Array.from(root.querySelectorAll('.diagram-container')) + .filter((container) => container && container.querySelector('.mermaid, .mermaid-dark, .mermaid-neutral')) +} + +/** + * Core rule: render only when the active layer is visible and not rendered yet. + * - "Active layer" means `.mermaid` in light mode or `.mermaid-dark` in dark mode. + * - We treat presence of a rendered `` as "already rendered" even if flags are missing. + */ +async function renderActiveLayerInContainer(container) { + if (!container) return + const el = container.querySelector(isDarkMode() ? '.mermaid-dark' : '.mermaid') + if (!el) return + if (el.hasAttribute('data-processed') || el.querySelector('svg')) return + if (!isNodeVisuallyActive(el)) return + const darkMode = isDarkMode() + await renderMermaidElement(el, { theme: getTheme(), darkMode }) +} + +/** + * Neutral theme is not part of the live light/dark switching UI. + * Render it in idle time for print / fallback scenarios. + */ +function scheduleNeutralRender() { + runWhenIdle(() => { + const neutralNodes = Array.from(document.querySelectorAll('.mermaid-neutral')) + .filter((el) => el && !el.hasAttribute('data-processed')) + .filter(isRenderContextActive) + neutralNodes.forEach((el) => { + renderMermaidElement(el, { theme: 'neutral', darkMode: false }) + }) + }) +} + +/** + * Lazy rendering via IntersectionObserver. + * When a `.diagram-container` gets close to viewport, attempt to render its active layer. + */ +function bindMermaidIntersectionObserver() { + if (mermaidContainerObserver) return + mermaidContainerObserver = new IntersectionObserver((entries) => { + entries.forEach((entry) => { + if (!entry.isIntersecting) return + const container = entry.target + if (!container || !isRenderContextActive(container)) return + renderActiveLayerInContainer(container) + }) + }, { + root: null, + // 200px margin to allow partial visibility + rootMargin: '200px 0px', + threshold: 0.01, + }) +} + +/** + * Registers all Mermaid containers under root into the observer. + * Idempotent via `data-mermaid-observed`. + */ +function observeMermaidContainers(root = document, refresh = false) { + bindMermaidIntersectionObserver() + const containers = collectMermaidContainers(root) + containers.forEach((container) => { + if (!container || container.hasAttribute('data-mermaid-observed')) return + container.setAttribute('data-mermaid-observed', '') + mermaidContainerObserver.observe(container) + }) + if (!refresh) return + containers.forEach((container) => { + if (!container) return + mermaidContainerObserver.unobserve(container) + mermaidContainerObserver.observe(container) + }) +} + +{{- if $wrapped }} +/** + * Gets the currently visible SVG for a diagram block that contains multiple Mermaid layers. + * @param {Element} root + * @returns {SVGElement|null} + */ +function getActiveMermaidSvg(root) { + const nodes = root?.querySelectorAll?.('.mermaid, .mermaid-dark, .mermaid-neutral') + if (!nodes?.length) return null + const visible = Array.from(nodes).find((el) => { + const style = getComputedStyle(el) + if (style.display === 'none' || style.visibility === 'hidden') return false + const rect = el.getBoundingClientRect() + return rect.width > 0 && rect.height > 0 + }) + return visible?.querySelector?.('svg') || null +} + +/** + * Enables pan/zoom for a Mermaid SVG inside `.diagram-view`. + * Requires `window.Panzoom`. + * + * @param {SVGElement} svg + * @returns {void} + */ +function bindMermaidPanzoom(svg) { + if (!svg) return + if (!svg.closest?.('.diagram-view')) return + if (!svg.closest?.('.mermaid, .mermaid-dark')) return + if (!panzoomInstances) panzoomInstances = new WeakMap() + if (!panzoomWheelHosts) panzoomWheelHosts = new WeakSet() + if (panzoomInstances.has(svg)) return + if (typeof window.Panzoom !== 'function') return + + const group = getMermaidPanzoomGroup(svg) + + const panzoom = window.Panzoom(svg, { + maxScale: 6, + minScale: 0.2, + step: 0.1, + }) + panzoomInstances.set(svg, panzoom) + + if (group?.transform) { + applyPanzoomTransform(panzoom, group.transform) + } + + const host = svg.closest('.mermaid, .mermaid-dark') + if (!host) return + if (panzoomWheelHosts.has(host)) return + panzoomWheelHosts.add(host) + + host.addEventListener('pointerdown', () => { + const currentSvg = host.querySelector('svg') + if (group && currentSvg) group.lastSvg = currentSvg + }) + + host.addEventListener('wheel', (event) => { + if (!event.ctrlKey) return + const currentSvg = host.querySelector('svg') + const instance = currentSvg && panzoomInstances.get(currentSvg) + if (!instance) return + if (group && currentSvg) group.lastSvg = currentSvg + event.preventDefault() + instance.zoomWithWheel(event) + }, { passive: false }) +} + +/** + * Initializes diagram tabs (`.diagram-tabs[data-diagram="mermaid"]`) controls: + * - Diagram/Code tab switching + * - Zoom/reset/download buttons wiring + * - Ensures Mermaid renders when the diagram tab is active + */ +function initDiagramControls() { + document.querySelectorAll('.diagram-tabs[data-diagram="mermaid"]:not([data-diagram-init])').forEach((tabs) => { + const actions = tabs.querySelector('.tabs-actions') + const diagramTabBtn = tabs.querySelector('.tab-item[data-tab="diagram"]') + const codeTabBtn = tabs.querySelector('.tab-item[data-tab="code"]') + const diagramBlock = tabs.querySelector('.diagram-view') + const codeBlock = Array.from(tabs.querySelectorAll('.tabs-content > .code-block')) + .find((block) => !block.classList.contains('diagram-view')) + if (!actions || !diagramTabBtn || !codeTabBtn || !diagramBlock || !codeBlock) return + + const zoomInBtn = tabs.querySelector('.diagram-zoom-in-btn') + const zoomOutBtn = tabs.querySelector('.diagram-zoom-out-btn') + const resetBtn = tabs.querySelector('.diagram-reset-btn') + const downloadBtn = tabs.querySelector('.diagram-download-btn') + tabs.dataset.diagramInit = 'true' + + const diagramActionButtons = [zoomOutBtn, zoomInBtn, resetBtn, downloadBtn].filter(Boolean) + + const movedActionRestore = new WeakMap() + let codeActionCache = [] + + const moveToActions = (elements) => { + elements.forEach((el) => { + if (!el || el.parentElement === actions) return + movedActionRestore.set(el, { parent: el.parentElement, next: el.nextSibling }) + if (pinnedFullscreenBtn && pinnedFullscreenBtn.parentElement === actions) { + actions.insertBefore(el, pinnedFullscreenBtn) + } else { + actions.appendChild(el) + } + }) + } + + const restoreMoved = (elements) => { + elements.forEach((el) => { + const restore = movedActionRestore.get(el) + if (!restore?.parent) return + if (restore.next && restore.next.parentNode === restore.parent) { + restore.parent.insertBefore(el, restore.next) + } else { + restore.parent.appendChild(el) + } + movedActionRestore.delete(el) + }) + } + + const pinnedFullscreenBtn = codeBlock.querySelector('.code-header .fullscreen-btn') + if (pinnedFullscreenBtn && pinnedFullscreenBtn.parentElement !== actions) { + actions.appendChild(pinnedFullscreenBtn) + } + + const collectCodeActions = () => { + const codeHeader = codeBlock.querySelector('.code-header') + if (codeHeader) { + return Array.from(codeHeader.querySelectorAll('.action-btn')) + .filter((btn) => btn !== pinnedFullscreenBtn) + } + return Array.from(codeBlock.querySelectorAll('.code-copy-btn')) + } + + const getCodeActions = () => { + if (codeActionCache.length && codeActionCache.some((el) => el.isConnected)) return codeActionCache + codeActionCache = collectCodeActions() + return codeActionCache + } + + const setDiagramActionsVisible = (visible) => { + diagramActionButtons.forEach((btn) => { + btn.style.display = visible ? '' : 'none' + }) + } + + const switchTo = (target) => { + const showDiagram = target === 'diagram' + diagramTabBtn.classList.toggle('active', showDiagram) + codeTabBtn.classList.toggle('active', !showDiagram) + diagramBlock.classList.toggle('active', showDiagram) + codeBlock.classList.toggle('active', !showDiagram) + const codeActions = getCodeActions() + if (showDiagram) { + setDiagramActionsVisible(true) + restoreMoved(codeActions) + // observeMermaidContainers(tabs, true) + } else { + setDiagramActionsVisible(false) + moveToActions(codeActions) + } + } + + const resolvePanzoom = () => { + const svg = getActiveMermaidSvg(diagramBlock) + return svg && panzoomInstances ? panzoomInstances.get(svg) : null + } + + diagramTabBtn.addEventListener('click', () => switchTo('diagram')) + codeTabBtn.addEventListener('click', () => switchTo('code')) + + zoomInBtn?.addEventListener('click', () => { + const panzoom = resolvePanzoom() + if (!panzoom?.zoomIn) return + try { panzoom.zoomIn({ animate: true }) } catch { panzoom.zoomIn() } + }) + zoomOutBtn?.addEventListener('click', () => { + const panzoom = resolvePanzoom() + if (!panzoom?.zoomOut) return + try { panzoom.zoomOut({ animate: true }) } catch { panzoom.zoomOut() } + }) + resetBtn?.addEventListener('click', () => { + const panzoom = resolvePanzoom() + if (!panzoom?.reset) return + try { panzoom.reset({ animate: true }) } catch { panzoom.reset() } + }) + downloadBtn?.addEventListener('click', () => { + const svg = getActiveMermaidSvg(diagramBlock) + if (!svg) return + const clonedSvg = svg.cloneNode(true) + clonedSvg.style.removeProperty('transform') + const xml = new XMLSerializer().serializeToString(clonedSvg) + const blob = new Blob([xml], { type: 'image/svg+xml;charset=utf-8' }) + const url = URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = url + link.download = diagramBlock.dataset.filename.split('.')[0] + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + URL.revokeObjectURL(url) + }) + }) +} + +/** + * Binds a listener to attach Panzoom after Mermaid renders an SVG. + * @returns {void} + */ +function bindRenderedPanzoom() { + if (panzoomEventBound) return + panzoomEventBound = true + document.addEventListener('fixit:mermaid-rendered', (event) => { + const svgId = event?.detail?.svgId + if (!svgId) return + bindMermaidPanzoom(document.getElementById(svgId)) + }) +} +{{- end }} + +/** + * Binds a listener so Mermaid diagrams inside a tab panel render after tab switches. + * @returns {void} + */ +function bindTabContainerChanged() { + if (tabContainerEventBound) return + tabContainerEventBound = true + document.addEventListener('tab-container-changed', async (event) => { + const panel = event?.panel || event?.detail?.relatedTarget + if (!panel) return + // Wait for layout/visibility to settle after tab switch before measuring visibility. + await nextFrame() + await nextFrame() + observeMermaidContainers(panel, true) + }) +} + +{{- if $wrapped }} +/** + * Binds theme switch synchronization for Panzoom transforms across Mermaid layers. + * @returns {void} + */ +function bindThemeSync() { + if (mermaidThemeSyncBound) return + mermaidThemeSyncBound = true + + const getActivePanzoomSvg = (container) => { + const nodes = container?.querySelectorAll?.('.mermaid, .mermaid-dark') + if (!nodes?.length) return null + const visible = Array.from(nodes).find((el) => { + const style = getComputedStyle(el) + if (style.display === 'none' || style.visibility === 'hidden') return false + const rect = el.getBoundingClientRect() + return rect.width > 0 && rect.height > 0 + }) + return visible?.querySelector?.('svg') || null + } + + const safeGetTransformFromSvg = (svg) => { + const instance = svg && panzoomInstances?.get?.(svg) + if (instance?.getPan && instance?.getScale) { + try { + const pan = instance.getPan() + const scale = instance.getScale() + if (pan && typeof scale === 'number') return { x: pan.x, y: pan.y, scale } + } catch {} + } + return null + } + + const resolveSourceSvg = (container) => { + const active = getActivePanzoomSvg(container) + const nowDark = isDarkMode() + const fallback = nowDark + ? container.querySelector('.mermaid svg') + : container.querySelector('.mermaid-dark svg') + const group = active ? getMermaidPanzoomGroup(active) : null + const last = group?.lastSvg + if (last?.isConnected) return last + return fallback || active + } + + const sync = () => { + if (!mermaidPanzoomGroups) return + document.querySelectorAll('.diagram-view .diagram-container').forEach((container) => { + const currentActive = getActivePanzoomSvg(container) + if (!currentActive) return + const group = getMermaidPanzoomGroup(currentActive) + if (!group) return + + const sourceSvg = resolveSourceSvg(container) + const sourceTransform = safeGetTransformFromSvg(sourceSvg) + if (sourceTransform) group.transform = sourceTransform + + if (!group.transform) return + + const instance = panzoomInstances?.get?.(currentActive) + if (instance) { + applyPanzoomTransform(instance, group.transform) + } else { + applyPanzoomTransformToSvg(currentActive, group.transform) + } + }) + } + + if (window.fixit?.switchThemeEventSet?.add) { + window.fixit.switchThemeEventSet.add(async () => { + await nextFrame() + await nextFrame() + sync() + }) + } +} +{{- end }} + +/** + * Initializes Mermaid rendering, events, lazy loading and diagram controls. + * @returns {Promise} + */ +async function init() { + const mermaidElements = document.querySelectorAll('.mermaid, .mermaid-dark, .mermaid-neutral') if (!mermaidElements.length) return - console.log( - `%c💫 FixIt Mermaid`, - 'color: #FF3670; font-weight: bold; font-size: 16px; text-shadow: 1px 1px 2px rgba(0,0,0,0.1);', - ) - const currentTheme = getTheme() - const nextTheme = getTheme(true) - const darkMode = document.documentElement.dataset.theme === 'dark' - await loadMermaid({ theme: currentTheme, darkMode }) - await loadMermaid({ theme: nextTheme, darkMode: !darkMode }) - await loadMermaid({ theme: 'neutral', darkMode: false, selector: '.mermaid-neutral' }) + console.log( + `%c💫 FixIt Mermaid`, + 'color: #FF3670; font-weight: bold; font-size: 16px; text-shadow: 1px 1px 2px rgba(0,0,0,0.1);', + ) + // Core: observe containers and render only when they become visible. + observeMermaidContainers() + // Neutral is non-critical, defer to idle time. + scheduleNeutralRender() + + bindTabContainerChanged() +{{- if $wrapped }} + initDiagramControls() + bindRenderedPanzoom() + bindThemeSync() +{{- end }} + + if (!mermaidThemeRerenderBound && window.fixit?.switchThemeEventSet?.add) { + mermaidThemeRerenderBound = true + window.fixit.switchThemeEventSet.add(async () => { + await nextFrame() + await nextFrame() + // Theme switch doesn't change container intersection, so refresh IO to trigger callbacks. + observeMermaidContainers(document, true) + }) + } } window.FixItMermaid = { - config: mermaidConfig, - load: loadMermaid, - init: initMermaid, + config, + init, } window.mermaid = mermaid diff --git a/assets/js/theme.js b/assets/js/theme.js index 6986e852..7abdfc6d 100644 --- a/assets/js/theme.js +++ b/assets/js/theme.js @@ -606,47 +606,64 @@ class FixIt { }, false); } + _getCodeFullscreenTarget(codeBlock) { + return codeBlock.closest('.code-tabs') || codeBlock; + } + _setCodeFullscreenState(codeBlock, show) { - const fullscreenBtn = codeBlock.querySelector('.code-header .fullscreen-btn'); - const hasExpandBtn = !!codeBlock.querySelector('.code-expand-btn'); - if (show && hasExpandBtn) { + const target = this._getCodeFullscreenTarget(codeBlock); + const expandBtn = codeBlock.querySelector('.code-expand-btn'); + + if (show && expandBtn) { codeBlock.dataset.fullscreenExpanded = codeBlock.classList.contains('is-expanded') ? 'true' : 'false'; codeBlock.classList.add('is-expanded'); } - if (!show && codeBlock.classList.contains('is-fullscreen')) { - codeBlock.classList.add('instant-height'); - if (hasExpandBtn && codeBlock.dataset.fullscreenExpanded === 'false') { + + if (!show && target.classList.contains('is-fullscreen')) { + target.classList.add('instant-height'); + window.requestAnimationFrame(() => target.classList.remove('instant-height')); + + if (expandBtn && codeBlock.dataset.fullscreenExpanded === 'false') { codeBlock.classList.remove('is-expanded'); } delete codeBlock.dataset.fullscreenExpanded; - window.requestAnimationFrame(() => { - codeBlock.classList.remove('instant-height'); - }); } - codeBlock.classList.toggle('is-fullscreen', show); - fullscreenBtn?.toggleAttribute('data-fullscreen', show); - const hasFullscreenCode = !!document.querySelector('.code-block.highlight.is-fullscreen'); - document.documentElement.style.overflow = hasFullscreenCode ? 'hidden' : ''; + + // update button tooltip + target.classList.toggle('is-fullscreen', show); + const btn = target.querySelector('.tabs-actions .fullscreen-btn') + || codeBlock.querySelector('.code-header .fullscreen-btn'); + if (!btn) return; + const exitTitle = btn.dataset.exitTitle || btn.getAttribute('data-exit-title') || btn.title; + const originalTitle = btn.dataset.ctOriginalTitle || btn.dataset.ctTitle || btn.title; + btn.dataset.ctOriginalTitle = originalTitle; + btn.dataset.ctTitle = show ? exitTitle : originalTitle; + const instance = window.CellTooltip.getOrCreateInstance(btn); + instance.hide(); } closeCodeFullscreen() { - const $activeCodeBlock = document.querySelector('.code-block.highlight.is-fullscreen'); - if (!$activeCodeBlock) return; - this._setCodeFullscreenState($activeCodeBlock, false); + const $activeTabs = document.querySelector('.code-tabs.is-fullscreen'); + if ($activeTabs) { + const $activeBlock = $activeTabs.querySelector('.code-block.active') || $activeTabs.querySelector('.code-block'); + if ($activeBlock) this._setCodeFullscreenState($activeBlock, false); + return; + } + const $activeBlock = document.querySelector('.code-block.highlight.is-fullscreen'); + if ($activeBlock) this._setCodeFullscreenState($activeBlock, false); } initFullscreenCode(codeBlock) { const fullscreenBtn = codeBlock.querySelector('.code-header .fullscreen-btn'); if (!fullscreenBtn) return; fullscreenBtn.addEventListener('click', () => { - const isFullscreen = codeBlock.classList.contains('is-fullscreen'); - if (isFullscreen) { - this._setCodeFullscreenState(codeBlock, false); - return; + const target = this._getCodeFullscreenTarget(codeBlock); + const show = !target.classList.contains('is-fullscreen'); + if (show) { + this.closeCodeFullscreen(); + codeBlock.classList.remove('is-collapsed'); } - this.closeCodeFullscreen(); - codeBlock.classList.remove('is-collapsed'); - this._setCodeFullscreenState(codeBlock, true); + this._setCodeFullscreenState(codeBlock, show); }, false); if (!this._codeFullscreenOnEsc) { this._codeFullscreenOnEsc = (event) => { @@ -1567,7 +1584,6 @@ class FixIt { initTabEvents(target = document) { target.addEventListener('tab-container-changed', () => { FileTree.updateLineHeight(target); - window.FixItMermaid?.init?.(); }, false); } @@ -1612,7 +1628,9 @@ class FixIt { placement: 'right', }); // code block action buttons tooltip - window.CellTooltip.initAll('.action-btn[title]'); + window.CellTooltip.initAll('.action-btn[title]', { + placement: 'bottom', + }); // footnote refs tooltip this.initFootnotes(); } @@ -1922,6 +1940,8 @@ class FixIt { if (printConfig.expandCode) { // revert code tabs to code blocks for better printing support forEach($content.querySelectorAll('.code-tabs'), ($codeTabs) => { + // skip diagrams + if ($codeTabs.dataset.diagram) return; // restore action buttons to the active tab's code-header before reverting const $actions = $codeTabs.querySelector('.tabs-actions'); const $activeBlock = $codeTabs.querySelector('.code-block.active'); diff --git a/assets/lib/panzoom/panzoom.min.js b/assets/lib/panzoom/panzoom.min.js new file mode 100644 index 00000000..6bf074fd --- /dev/null +++ b/assets/lib/panzoom/panzoom.min.js @@ -0,0 +1,16 @@ +/** +* Panzoom 4.6.2 for panning and zooming elements using CSS transforms +* Copyright Timmy Willison and other contributors +* https://github.com/timmywil/panzoom/blob/main/MIT-License.txt +*/ +((e,t)=>{"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).Panzoom=t()})(this,function(){"undefined"!=typeof window&&(window.NodeList&&!NodeList.prototype.forEach&&(NodeList.prototype.forEach=Array.prototype.forEach),"function"!=typeof window.CustomEvent)&&(window.CustomEvent=function(e,t){t=t||{bubbles:!1,cancelable:!1,detail:null};var n=document.createEvent("CustomEvent");return n.initCustomEvent(e,t.bubbles,t.cancelable,t.detail),n});let i="undefined"!=typeof document&&!!document.documentMode,a;let l=["webkit","moz","ms"],s={};function z(e){if(s[e])return s[e];var t=a=a||document.createElement("div").style;if(e in t)return s[e]=e;var n=e[0].toUpperCase()+e.slice(1);let o=l.length;for(;o--;){var r=""+l[o]+n;if(r in t)return s[e]=r}}function r(e,t){return parseFloat(t[z(e)])||0}function c(e,t,n=window.getComputedStyle(e)){var o="border"===t?"Width":"";return{left:r(t+"Left"+o,n),right:r(t+"Right"+o,n),top:r(t+"Top"+o,n),bottom:r(t+"Bottom"+o,n)}}function X(e,t,n){e.style[z(t)]=n}function Y(e){let t=e.parentNode;t&&1===t.nodeType||(t=document.documentElement);var n=window.getComputedStyle(e),o=window.getComputedStyle(t),r=e.getBoundingClientRect(),a=t.getBoundingClientRect();return{elem:{style:n,width:r.width,height:r.height,top:r.top,bottom:r.bottom,left:r.left,right:r.right,margin:c(e,"margin",n),border:c(e,"border",n)},parent:{style:o,width:a.width,height:a.height,top:a.top,bottom:a.bottom,left:a.left,right:a.right,padding:c(t,"padding",o),border:c(t,"border",o)}}}let C={down:"mousedown",move:"mousemove",up:"mouseup mouseleave"};function N(e,t,n,o){C[e].split(" ").forEach(e=>{t.addEventListener(e,n,o)})}function T(e,t,n){C[e].split(" ").forEach(e=>{t.removeEventListener(e,n)})}function $(e,t){let n=e.length;for(;n--;)if(e[n].pointerId===t.pointerId)return n;return-1}function L(e,t){let n;if(t.touches){n=0;for(var o of t.touches)o.pointerId=n++,L(e,o)}else-1<(n=$(e,t))&&e.splice(n,1),e.push(t)}function V(e){let t=(e=e.slice(0)).pop();for(var n;n=e.pop();)t={clientX:(n.clientX-t.clientX)/2+t.clientX,clientY:(n.clientY-t.clientY)/2+t.clientY};return t}function G(e){var t;return e.length<2?0:(t=e[0],e=e[1],Math.sqrt(Math.pow(Math.abs(e.clientX-t.clientX),2)+Math.pow(Math.abs(e.clientY-t.clientY),2)))}"undefined"!=typeof window&&("function"==typeof window.PointerEvent?C={down:"pointerdown",move:"pointermove",up:"pointerup pointerleave pointercancel"}:"function"==typeof window.TouchEvent&&(C={down:"touchstart",move:"touchmove",up:"touchend touchcancel"}));let I=/^http:[\w\.\/]+svg$/; +/** + * Panzoom for panning and zooming elements using CSS transforms + * https://github.com/timmywil/panzoom + * + * Copyright Timmy Willison and other contributors + * Released under the MIT license + * https://github.com/timmywil/panzoom/blob/main/MIT-License.txt + * + */ +let R={animate:!1,canvas:!1,cursor:"move",disablePan:!1,disableZoom:!1,disableXAxis:!1,disableYAxis:!1,duration:200,easing:"ease-in-out",exclude:[],excludeClass:"panzoom-exclude",handleStartEvent:e=>{e.preventDefault(),e.stopPropagation()},maxScale:4,minScale:.125,overflow:"hidden",panOnlyWhenZoomed:!1,pinchAndPan:!1,relative:!1,setTransform:function(e,{x:t,y:n,scale:o,isSVG:r},a){X(e,"transform",`scale(${o}) translate(${t}px, ${n}px)`),r&&i&&(o=window.getComputedStyle(e).getPropertyValue("transform"),e.setAttribute("transform",o))},startX:0,startY:0,startScale:1,step:.3,touchAction:"none"};function e(d,p){if(!d)throw new Error("Panzoom requires an element as an argument");if(1!==d.nodeType)throw new Error("Panzoom requires an element with a nodeType of 1");if(!(e=>{let t=e;for(;t&&t.parentNode;){if(t.parentNode===document)return 1;t=t.parentNode instanceof ShadowRoot?t.parentNode.host:t.parentNode}})(d))throw new Error("Panzoom should be called on elements that have been attached to the DOM");p=Object.assign(Object.assign({},R),p);e=d;let c=I.test(e.namespaceURI)&&"svg"!==e.nodeName.toLowerCase();var e;let n=d.parentNode;n.style.overflow=p.overflow,n.style.userSelect="none",n.style.touchAction=p.touchAction,(p.canvas?n:d).style.cursor=p.cursor,d.style.userSelect="none",d.style.touchAction=p.touchAction,X(d,"transformOrigin","string"==typeof p.origin?p.origin:c?"0 0":"50% 50%");let u=0,m=0,f=1,a=!1;function r(e,t,n){n.silent||(n=new CustomEvent(e,{detail:t}),d.dispatchEvent(n))}function l(t,n,e){let o={x:u,y:m,scale:f,isSVG:c,originalEvent:e};return requestAnimationFrame(()=>{var e;"boolean"==typeof n.animate&&(n.animate?(e=n,X(d,"transition",`${z("transform")} ${e.duration}ms `+e.easing)):X(d,"transition","none")),n.setTransform(d,o,n),r(t,o,n),r("panzoomchange",o,n)}),o}function s(e,t,n,o){var r,a,i,l,s=Object.assign(Object.assign({},p),o),c={x:u,y:m,opts:s};return(null!=o&&o.force||!(s.disablePan||s.panOnlyWhenZoomed&&f===s.startScale))&&(e=parseFloat(e),t=parseFloat(t),s.disableXAxis||(c.x=(s.relative?u:0)+e),s.disableYAxis||(c.y=(s.relative?m:0)+t),s.contain&&(e=((t=(e=(o=Y(d)).elem.width/f)*n)-e)/2,a=((r=(a=o.elem.height/f)*n)-a)/2,"inside"===s.contain?(i=(-o.elem.margin.left-o.parent.padding.left+e)/n,l=(o.parent.width-t-o.parent.padding.left-o.elem.margin.left-o.parent.border.left-o.parent.border.right+e)/n,c.x=Math.max(Math.min(c.x,l),i),l=(-o.elem.margin.top-o.parent.padding.top+a)/n,i=(o.parent.height-r-o.parent.padding.top-o.elem.margin.top-o.parent.border.top-o.parent.border.bottom+a)/n,c.y=Math.max(Math.min(c.y,i),l)):"outside"===s.contain&&(i=(-(t-o.parent.width)-o.parent.padding.left-o.parent.border.left-o.parent.border.right+e)/n,l=(e-o.parent.padding.left)/n,c.x=Math.max(Math.min(c.x,l),i),t=(-(r-o.parent.height)-o.parent.padding.top-o.parent.border.top-o.parent.border.bottom+a)/n,e=(a-o.parent.padding.top)/n,c.y=Math.max(Math.min(c.y,e),t))),s.roundPixels)&&(c.x=Math.round(c.x),c.y=Math.round(c.y)),c}function h(n,o){var r,a=Object.assign(Object.assign({},p),o),i={scale:f,opts:a};if(null!=o&&o.force||!a.disableZoom){let e=p.minScale,t=p.maxScale;a.contain&&(a=(o=Y(d)).elem.width/f,r=o.elem.height/f,1{i(p.startX,p.startY,{animate:!1,force:!0})});let y,b,w,x,O,E,S=[];function o(e){((t,n)=>{for(let e=t;null!=e;e=e.parentNode)if(o=e,r=n.excludeClass,1===o.nodeType&&-1<` ${o=o,(o.getAttribute("class")||"").trim()} `.indexOf(` ${r} `)||-1({x:u,y:m}),getScale:()=>f,getOptions:()=>{var e,t=p,n={};for(e in t)t.hasOwnProperty(e)&&(n[e]=t[e]);return n},handleDown:o,handleMove:M,handleUp:A,pan:i,reset:function(e){var e=Object.assign(Object.assign(Object.assign({},p),{animate:!0,force:!0}),e),t=(f=h(e.startScale,e).scale,s(e.startX,e.startY,f,e));return u=t.x,m=t.y,l("panzoomreset",e)},resetStyle:function(){n.style.overflow="",n.style.userSelect="",n.style.touchAction="",n.style.cursor="",d.style.cursor="",d.style.userSelect="",d.style.touchAction="",X(d,"transformOrigin","")},setOptions:function(e={}){for(var t in e)e.hasOwnProperty(t)&&(p[t]=e[t]);(e.hasOwnProperty("cursor")||e.hasOwnProperty("canvas"))&&(n.style.cursor=d.style.cursor="",(p.canvas?n:d).style.cursor=p.cursor),e.hasOwnProperty("overflow")&&(n.style.overflow=e.overflow),e.hasOwnProperty("touchAction")&&(n.style.touchAction=e.touchAction,d.style.touchAction=e.touchAction)},setStyle:(e,t)=>X(d,e,t),zoom:g,zoomIn:function(e){return t(!0,e)},zoomOut:function(e){return t(!1,e)},zoomToPoint:v,zoomWithWheel:function(e,t){e.preventDefault();var t=Object.assign(Object.assign(Object.assign({},p),t),{animate:!1}),n=(0===e.deltaY&&e.deltaX?e.deltaX:e.deltaY)<0?1:-1;return v(h(f*Math.exp(n*t.step/3),t).scale,e,t,e)}}}return e.defaultOptions=R,e}); \ No newline at end of file diff --git a/assets/scss/core/_common.scss b/assets/scss/core/_common.scss index 0c9c80e9..590c6e8b 100644 --- a/assets/scss/core/_common.scss +++ b/assets/scss/core/_common.scss @@ -59,6 +59,14 @@ @include border-radius(full); } +.is-fullscreen { + .fa-expand { + --fa: "\f066"; + font-weight: 900; + color: fi-var(global-link-hover-color); + } +} + // auto adapt to dark mode by inverting the color and hue .auto-dark-mode { @include dark-mode { diff --git a/assets/scss/core/_reboot.scss b/assets/scss/core/_reboot.scss index 6fdc2567..2de9d3b5 100644 --- a/assets/scss/core/_reboot.scss +++ b/assets/scss/core/_reboot.scss @@ -20,7 +20,7 @@ container-type: scroll-state; container-name: scroller; - &:has(dialog[open]) { + &:has(dialog[open], .is-fullscreen) { overflow: hidden; } diff --git a/assets/scss/pages/single/_code.scss b/assets/scss/pages/single/_code.scss index abf31521..b84b7ce8 100644 --- a/assets/scss/pages/single/_code.scss +++ b/assets/scss/pages/single/_code.scss @@ -137,10 +137,6 @@ pre { @include box-shadow(hover); } - &.instant-height .code-wrapper { - transition: height 0s !important; - } - .code-wrapper { overflow: hidden; height: auto; @@ -256,12 +252,6 @@ pre { color: fi-var(global-link-hover-color); } - .fullscreen-btn[data-fullscreen] .fa-expand { - --fa: "\f066"; - font-weight: 900; - color: fi-var(global-link-hover-color); - } - @each $type, $text in $code-type-map { &.language-#{to-lower-case($type)} { // e.g., --fi-code-type: "JavaScript"; @@ -635,9 +625,9 @@ pre { position: absolute; right: 0; top: 50%; - transform: translateY(-50%); + translate: 0 -50%; width: 1px; - height: 36%; + height: fi-var(code-block-font-size); background-color: fi-var(global-border-color-weight); } @@ -696,10 +686,6 @@ pre { font-weight: 900; color: fi-var(global-link-hover-color); } - - &.fullscreen-btn { - display: none; - } } } @@ -725,4 +711,43 @@ pre { } } } + + &.is-fullscreen { + position: fixed; + inset: 0; + margin: 0 !important; + display: flex; + flex-direction: column; + overflow: hidden; + @include z-index(fixed, 1); + @include border-radius(0); + + .tabs-header { + top: 0 !important; + @include border-radius(0); + } + + .tabs-content { + flex: 1; + min-height: 0; + + .code-block.active { + height: 100%; + } + + .code-wrapper { + overflow: auto; + height: calc(100dvh - 1.4em - 0.8rem); + } + } + + .code-expand-btn { + display: none; + } + } +} + +// prevent transition when code block height changed by fullscreen toggle +.instant-height .code-wrapper { + transition: height 0s !important; } diff --git a/assets/scss/pages/single/shortcodes/_mermaid.scss b/assets/scss/pages/single/shortcodes/_mermaid.scss index ab4247ea..663d5a17 100644 --- a/assets/scss/pages/single/shortcodes/_mermaid.scss +++ b/assets/scss/pages/single/shortcodes/_mermaid.scss @@ -1,13 +1,16 @@ -.mermaid, -.mermaid-dark, -.mermaid-neutral { +@mixin mermaid-base { position: relative; + box-sizing: border-box; margin: 0; overflow: hidden !important; &[data-processed] { - text-align: center; background-color: transparent; + + svg { + display: block; + margin-inline: auto; + } } &:not([data-processed])::before { @@ -20,36 +23,87 @@ } } +@mixin mermaid-hidden-layer { + position: absolute; + inset: 0; + width: 100%; + visibility: hidden; + pointer-events: none; +} + +@mixin mermaid-visible-layer { + position: relative; + inset: auto; + visibility: visible; + pointer-events: auto; +} + +:is(.mermaid, .mermaid-dark, .mermaid-neutral) { + @include mermaid-base; +} + +.diagram-tabs { + .tabs-actions .fullscreen-btn { + position: relative; + margin-inline-start: 4px; + + &::before { + content: ''; + position: absolute; + left: 0; + top: 50%; + width: 1px; + translate: -2px -50%; + height: fi-var(code-block-font-size); + background-color: fi-var(global-border-color-weight); + } + } + + .diagram-container { + margin-block: 0; + } + + &.is-fullscreen { + .diagram-container { + height: 100%; + } + + :is(.mermaid, .mermaid-dark)[data-processed] { + height: 100%; + } + } +} + +:is(.mermaid-dark, .mermaid-neutral) { + @include mermaid-hidden-layer; +} + .mermaid { @include dark-mode { - height: 0; - padding: 0; + @include mermaid-hidden-layer; } } .mermaid-dark { - height: 0; - padding: 0; - @include dark-mode { - height: auto; - padding: 0.5rem 0.75rem; + @include mermaid-visible-layer; } } -// for print view -.mermaid-neutral { - height: 0; - padding: 0; -} - @include media('print') { + .diagram-tabs { + border: none !important; + background-color: transparent !important; + + .tabs-header { + display: none !important; + } + } .mermaid, .mermaid-dark { - display: none !important; + display: none; } .mermaid-neutral { - height: auto; - padding: 0.5rem 0.75rem; + @include mermaid-visible-layer; } } diff --git a/hugo.toml b/hugo.toml index 9da5cca4..b8bee35f 100644 --- a/hugo.toml +++ b/hugo.toml @@ -841,6 +841,12 @@ loop = false # FixIt 0.2.15 | NEW Mermaid config # See http://fixit.lruihao.cn/documentation/content-management/diagrams-support/mermaid/ [params.mermaid] +# FixIt 1.0.0 | NEW whether to enable the Mermaid wrapper +# When enabled, Mermaid diagrams are wrapped with diagram tabs and actions: +# - pan/zoom (drag & ctrl+wheel) +# - reset view +# - download as SVG +wrapper = true # FixIt 0.4.0 | NEW Mermaid ESM module CDN source # default is https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.esm.min.mjs cdn = "" diff --git a/i18n/de.toml b/i18n/de.toml index ea6143f4..cadbff01 100644 --- a/i18n/de.toml +++ b/i18n/de.toml @@ -105,8 +105,9 @@ toggleLineNumbers = "Zeilennummern umschalten" toggleLineWrap = "Zeilenumbruch umschalten" toggleCodeEditable = "Codeblock editierbar umschalten" downloadCode = "Code herunterladen" -toggleCodeFullscreen = "Codeblock-Vollbild umschalten" expandCode = "Codeblock erweitern oder reduzieren" +fullscreen = "Vollbild" +exitFullscreen = "Vollbild verlassen" cookieconsentMessage = "Diese Website verwendet Cookies, um Ihre Erfahrung zu verbessern." cookieconsentDismiss = "Zustimmen" cookieconsentLink = "Erfahren Sie mehr" @@ -236,3 +237,17 @@ changed = "GEÄNDERT" deleted = "GELÖSCHT" deprecated = "VERALTET" # === shortcodes/version.html === + +# === diagram actions === +[diagram] +zoomIn = "Vergrößern" +zoomOut = "Verkleinern" +reset = "Zurücksetzen" +download = "SVG herunterladen" +# === diagram actions === + +# === tabs related === +[tabs] +diagram = "Diagramm" +code = "Code" +# === tabs related === diff --git a/i18n/en.toml b/i18n/en.toml index dfeaf6ed..4cfc8463 100644 --- a/i18n/en.toml +++ b/i18n/en.toml @@ -105,8 +105,9 @@ toggleLineNumbers = "Toggle line numbers" toggleLineWrap = "Toggle code wrap" toggleCodeEditable = "Toggle code block editable" downloadCode = "Download code" -toggleCodeFullscreen = "Toggle code block fullscreen" expandCode = "Expand or collapse code block" +fullscreen = "Fullscreen" +exitFullscreen = "Exit fullscreen" cookieconsentMessage = "This website uses Cookies to improve your experience." cookieconsentDismiss = "Got it!" cookieconsentLink = "Learn more" @@ -236,3 +237,17 @@ changed = "CHANGED" deleted = "DELETED" deprecated = "DEPRECATED" # === shortcodes/version.html === + +# === diagram actions === +[diagram] +zoomIn = "Zoom in" +zoomOut = "Zoom out" +reset = "Reset" +download = "Download SVG" +# === diagram actions === + +# === tabs related === +[tabs] +diagram = "Diagram" +code = "Code" +# === tabs related === diff --git a/i18n/es.toml b/i18n/es.toml index b8799ca6..1949ecb7 100644 --- a/i18n/es.toml +++ b/i18n/es.toml @@ -105,8 +105,9 @@ toggleLineNumbers = "Alternar números de línea" toggleLineWrap = "Alternar ajuste de línea" toggleCodeEditable = "Alternar bloque de código editable" downloadCode = "Descargar código" -toggleCodeFullscreen = "Alternar pantalla completa del bloque de código" expandCode = "Expandir o contraer bloque de código" +fullscreen = "Pantalla completa" +exitFullscreen = "Salir de pantalla completa" cookieconsentMessage = "Este sitio web utiliza Cookies para mejorar su experiencia." cookieconsentDismiss = "De acuerdo" cookieconsentLink = "Aprende más" @@ -236,3 +237,17 @@ changed = "MODIFICADO" deleted = "ELIMINADO" deprecated = "OBSOLETO" # === shortcodes/version.html === + +# === diagram actions === +[diagram] +zoomIn = "Acercar" +zoomOut = "Alejar" +reset = "Restablecer" +download = "Descargar SVG" +# === diagram actions === + +# === tabs related === +[tabs] +diagram = "Diagrama" +code = "Código" +# === tabs related === diff --git a/i18n/fr.toml b/i18n/fr.toml index c15f1f05..d63a691c 100644 --- a/i18n/fr.toml +++ b/i18n/fr.toml @@ -105,8 +105,9 @@ toggleLineNumbers = "Basculer les numéros de ligne" toggleLineWrap = "Basculer le retour à la ligne" toggleCodeEditable = "Basculer le bloc de code modifiable" downloadCode = "Télécharger le code" -toggleCodeFullscreen = "Basculer le plein écran du bloc de code" expandCode = "Développer ou réduire le bloc de code" +fullscreen = "Plein écran" +exitFullscreen = "Quitter le plein écran" cookieconsentMessage = "Ce site Web utilise des Cookies pour améliorer votre expérience." cookieconsentDismiss = "J'ai compris !" cookieconsentLink = "En apprendre plus" @@ -236,3 +237,17 @@ changed = "MODIFIÉ" deleted = "SUPPRIMÉ" deprecated = "OBSOLÈTE" # === shortcodes/version.html === + +# === diagram actions === +[diagram] +zoomIn = "Zoom avant" +zoomOut = "Zoom arrière" +reset = "Réinitialiser" +download = "Télécharger le SVG" +# === diagram actions === + +# === tabs related === +[tabs] +diagram = "Diagramme" +code = "Code" +# === tabs related === diff --git a/i18n/hi.toml b/i18n/hi.toml index 9ffe4e8a..70d1cf48 100644 --- a/i18n/hi.toml +++ b/i18n/hi.toml @@ -106,8 +106,9 @@ toggleLineNumbers = "लाइन नंबर टॉगल करें" toggleLineWrap = "लाइन रैप टॉगल करें" toggleCodeEditable = "कोड ब्लॉक संपादन योग्य टॉगल करें" downloadCode = "कोड डाउनलोड करें" -toggleCodeFullscreen = "कोड ब्लॉक फ़ुलस्क्रीन टॉगल करें" expandCode = "कोड ब्लॉक का विस्तार या संकुचित करें" +fullscreen = "पूर्ण स्क्रीन" +exitFullscreen = "पूर्ण स्क्रीन से बाहर निकलें" cookieconsentMessage = "यह वेबसाइट आपके अनुभव को बेहतर बनाने के लिए कुकीज़ का उपयोग करती है।" cookieconsentDismiss = "समझ गया!" cookieconsentLink = "और अधिक जानें" @@ -237,3 +238,17 @@ changed = "बदला हुआ" deleted = "हटाए गए" deprecated = "अप्रचलित" # === shortcodes/version.html === + +# === diagram actions === +[diagram] +zoomIn = "ज़ूम इन" +zoomOut = "ज़ूम आउट" +reset = "रीसेट" +download = "SVG डाउनलोड करें" +# === diagram actions === + +# === tabs related === +[tabs] +diagram = "आरेख" +code = "कोड" +# === tabs related === diff --git a/i18n/it.toml b/i18n/it.toml index 6659c0f4..119d9faa 100644 --- a/i18n/it.toml +++ b/i18n/it.toml @@ -105,8 +105,9 @@ toggleLineNumbers = "Attiva/disattiva i numeri di linea" toggleLineWrap = "Attiva/disattiva il ritorno a capo del codice" toggleCodeEditable = "Attiva/disattiva la modifica del blocco di codice" downloadCode = "Scarica codice" -toggleCodeFullscreen = "Attiva/disattiva schermo intero del blocco di codice" expandCode = "Espandi o riduci il blocco di codice" +fullscreen = "Schermo intero" +exitFullscreen = "Esci da schermo intero" cookieconsentMessage = "Questo sito Web utilizza i Cookies per migliorare la tua esperienza." cookieconsentDismiss = "Essere d'accordo" cookieconsentLink = "Per saperne di più" @@ -236,3 +237,17 @@ changed = "CAMBIATO" deleted = "CANCELLATO" deprecated = "DEPRECATO" # === shortcodes/version.html === + +# === diagram actions === +[diagram] +zoomIn = "Zoom avanti" +zoomOut = "Zoom indietro" +reset = "Reimposta" +download = "Scarica SVG" +# === diagram actions === + +# === tabs related === +[tabs] +diagram = "Diagramma" +code = "Codice" +# === tabs related === diff --git a/i18n/ja.toml b/i18n/ja.toml index 6ead33a1..ba30cabc 100644 --- a/i18n/ja.toml +++ b/i18n/ja.toml @@ -103,8 +103,9 @@ toggleLineNumbers = "行番号の切り替え" toggleLineWrap = "自動折り返しの切り替え" toggleCodeEditable = "コードブロックの編集切り替え" downloadCode = "コードをダウンロード" -toggleCodeFullscreen = "コードブロックの全画面表示を切り替え" expandCode = "コードブロックの展開/折りたたみ" +fullscreen = "全画面" +exitFullscreen = "全画面を終了" cookieconsentMessage = "このウェブサイトはクッキーを使用して体験を向上させます。" cookieconsentDismiss = "同意する" cookieconsentLink = "詳細はこちら" @@ -232,3 +233,17 @@ changed = "変更" deleted = "削除" deprecated = "非推奨" # === shortcodes/version.html === + +# === diagram actions === +[diagram] +zoomIn = "拡大" +zoomOut = "縮小" +reset = "リセット" +download = "SVG をダウンロード" +# === diagram actions === + +# === tabs related === +[tabs] +diagram = "図" +code = "コード" +# === tabs related === diff --git a/i18n/ko.toml b/i18n/ko.toml index 610d60b9..0aeb3cbd 100644 --- a/i18n/ko.toml +++ b/i18n/ko.toml @@ -103,8 +103,9 @@ toggleLineNumbers = "줄 번호 전환" toggleLineWrap = "자동 줄 바꿈 전환" toggleCodeEditable = "코드 블록 편집 전환" downloadCode = "코드 다운로드" -toggleCodeFullscreen = "코드 블록 전체 화면 전환" expandCode = "코드 블록 확장/축소" +fullscreen = "전체 화면" +exitFullscreen = "전체 화면 종료" cookieconsentMessage = "이 웹사이트는 쿠키를 사용하여 경험을 향상시킵니다." cookieconsentDismiss = "동의" cookieconsentLink = "자세히 알아보기" @@ -232,3 +233,17 @@ changed = "변경됨" deleted = "삭제됨" deprecated = "더 이상 사용되지 않음" # === shortcodes/version.html === + +# === diagram actions === +[diagram] +zoomIn = "확대" +zoomOut = "축소" +reset = "재설정" +download = "SVG 다운로드" +# === diagram actions === + +# === tabs related === +[tabs] +diagram = "다이어그램" +code = "코드" +# === tabs related === diff --git a/i18n/pl.toml b/i18n/pl.toml index 8d9f2e08..1706d5c2 100644 --- a/i18n/pl.toml +++ b/i18n/pl.toml @@ -105,8 +105,9 @@ toggleLineNumbers = "Przełącz numery linii" toggleLineWrap = "Przełącz zawijanie linii" toggleCodeEditable = "Przełącz edytowalny blok kodu" downloadCode = "Pobierz kod" -toggleCodeFullscreen = "Przełącz pełny ekran bloku kodu" expandCode = "Rozwiń lub zwiń blok kodu" +fullscreen = "Pełny ekran" +exitFullscreen = "Wyjdź z pełnego ekranu" cookieconsentMessage = "Ta strona korzysta z plików Cookies, aby poprawić komfort użytkowania." cookieconsentDismiss = "Zgodzić się" cookieconsentLink = "Ucz się więcej" @@ -236,3 +237,17 @@ changed = "Zmieniono" deleted = "Usunięte" deprecated = "Przestarzałe" # === shortcodes/version.html === + +# === diagram actions === +[diagram] +zoomIn = "Powiększ" +zoomOut = "Pomniejsz" +reset = "Resetuj" +download = "Pobierz SVG" +# === diagram actions === + +# === tabs related === +[tabs] +diagram = "Diagram" +code = "Kod" +# === tabs related === diff --git a/i18n/pt-BR.toml b/i18n/pt-BR.toml index e7c4e221..7b0258b5 100644 --- a/i18n/pt-BR.toml +++ b/i18n/pt-BR.toml @@ -106,8 +106,9 @@ toggleLineNumbers = "Alternar números de linha" toggleLineWrap = "Alternar quebra de linha" toggleCodeEditable = "Alternar bloco de código editável" downloadCode = "Baixar código" -toggleCodeFullscreen = "Alternar tela cheia do bloco de código" expandCode = "Expandir ou recolher bloco de código" +fullscreen = "Tela cheia" +exitFullscreen = "Sair da tela cheia" cookieconsentMessage = "Este site usa Cookies para melhorar sua experiência." cookieconsentDismiss = "Aceitar" cookieconsentLink = "Saber mais" @@ -237,3 +238,17 @@ changed = "ALTERADO" deleted = "EXCLUÍDO" deprecated = "OBSOLETO" # === shortcodes/version.html === + +# === diagram actions === +[diagram] +zoomIn = "Ampliar" +zoomOut = "Reduzir" +reset = "Redefinir" +download = "Baixar SVG" +# === diagram actions === + +# === tabs related === +[tabs] +diagram = "Diagrama" +code = "Código" +# === tabs related === diff --git a/i18n/ro.toml b/i18n/ro.toml index a63f7c9a..19bea2bb 100644 --- a/i18n/ro.toml +++ b/i18n/ro.toml @@ -105,8 +105,9 @@ toggleLineNumbers = "Comută numerele de linie" toggleLineWrap = "Comută înfășurarea liniei" toggleCodeEditable = "Comută blocul de cod editabil" downloadCode = "Descarcă codul" -toggleCodeFullscreen = "Comută ecran complet pentru blocul de cod" expandCode = "Extinde sau restrânge blocul de cod" +fullscreen = "Ecran complet" +exitFullscreen = "Ieși din ecran complet" cookieconsentMessage = "Acest site web utilizează Cookies pentru a vă îmbunătăți experiența." cookieconsentDismiss = "De acord" cookieconsentLink = "Aflați mai multe" @@ -236,3 +237,17 @@ changed = "SCHIMBAT" deleted = "ȘTERS" deprecated = "DEPRECIAȚI" # === shortcodes/version.html === + +# === diagram actions === +[diagram] +zoomIn = "Mărește" +zoomOut = "Micșorează" +reset = "Resetează" +download = "Descarcă SVG" +# === diagram actions === + +# === tabs related === +[tabs] +diagram = "Diagramă" +code = "Cod" +# === tabs related === diff --git a/i18n/ru.toml b/i18n/ru.toml index 5584354d..98d27c1a 100644 --- a/i18n/ru.toml +++ b/i18n/ru.toml @@ -105,8 +105,9 @@ toggleLineNumbers = "Переключить номера строк" toggleLineWrap = "Переключить перенос строк" toggleCodeEditable = "Переключить редактируемый блок кода" downloadCode = "Скачать код" -toggleCodeFullscreen = "Переключить полноэкранный режим блока кода" expandCode = "Развернуть или свернуть блок кода" +fullscreen = "Полный экран" +exitFullscreen = "Выйти из полного экрана" cookieconsentMessage = "Этот сайт использует Cookies для улучшения вашего опыта." cookieconsentDismiss = "Согласен" cookieconsentLink = "Учить больше" @@ -236,3 +237,17 @@ changed = "ИЗМЕНЕН" deleted = "УДАЛЕН" deprecated = "УСТАРЕЛ" # === shortcodes/version.html === + +# === diagram actions === +[diagram] +zoomIn = "Увеличить" +zoomOut = "Уменьшить" +reset = "Сбросить" +download = "Скачать SVG" +# === diagram actions === + +# === tabs related === +[tabs] +diagram = "Диаграмма" +code = "Код" +# === tabs related === diff --git a/i18n/sr.toml b/i18n/sr.toml index d48c1f75..ac6427e8 100644 --- a/i18n/sr.toml +++ b/i18n/sr.toml @@ -105,8 +105,9 @@ toggleLineNumbers = "Прекидач за бројеве линија" toggleLineWrap = "Прекидач за оматање линија" toggleCodeEditable = "Пребаци уређивање блока кода" downloadCode = "Преузми код" -toggleCodeFullscreen = "Пребаци цео екран блока кода" expandCode = "Прошири или смањи блок кода" +fullscreen = "Цео екран" +exitFullscreen = "Изађи из целог екрана" cookieconsentMessage = "Ова веб локација користи Cookies да би побољшала ваше искуство." cookieconsentDismiss = "Договорити се" cookieconsentLink = "Сазнајте више" @@ -236,3 +237,17 @@ changed = "ПРОМЕЊЕНО" deleted = "ОБРИСАНО" deprecated = "ЗАСТАРЕЛО" # === shortcodes/version.html === + +# === diagram actions === +[diagram] +zoomIn = "Увећај" +zoomOut = "Умањи" +reset = "Ресетуј" +download = "Преузми SVG" +# === diagram actions === + +# === tabs related === +[tabs] +diagram = "Дијаграм" +code = "Код" +# === tabs related === diff --git a/i18n/vi.toml b/i18n/vi.toml index f3c4f0da..2a19accc 100644 --- a/i18n/vi.toml +++ b/i18n/vi.toml @@ -104,8 +104,9 @@ toggleLineNumbers = "Chuyển đổi số dòng" toggleLineWrap = "Chuyển đổi xuống dòng" toggleCodeEditable = "Chuyển đổi khối mã có thể chỉnh sửa" downloadCode = "Tải mã" -toggleCodeFullscreen = "Chuyển đổi toàn màn hình khối mã" expandCode = "Mở rộng hoặc thu gọn khối mã" +fullscreen = "Toàn màn hình" +exitFullscreen = "Thoát toàn màn hình" cookieconsentMessage = "Trang web này sử dụng Cookies để cải thiện trải nghiệm của bạn." cookieconsentDismiss = "Đã hiểu!" cookieconsentLink = "Tìm hiểu thêm" @@ -235,3 +236,17 @@ changed = "THAY ĐỔI" deleted = "XOÁ" deprecated = "KHÔNG CÒN SỬ DỤNG" # === shortcodes/version.html === + +# === diagram actions === +[diagram] +zoomIn = "Phóng to" +zoomOut = "Thu nhỏ" +reset = "Đặt lại" +download = "Tải SVG" +# === diagram actions === + +# === tabs related === +[tabs] +diagram = "Sơ đồ" +code = "Mã" +# === tabs related === diff --git a/i18n/zh-CN.toml b/i18n/zh-CN.toml index 7de671f6..26861632 100644 --- a/i18n/zh-CN.toml +++ b/i18n/zh-CN.toml @@ -103,8 +103,9 @@ toggleLineNumbers = "切换行号" toggleLineWrap = "切换自动换行" toggleCodeEditable = "切换可编辑代码块" downloadCode = "下载代码" -toggleCodeFullscreen = "切换代码块全屏" expandCode = "展开或折叠代码块" +fullscreen = "全屏" +exitFullscreen = "退出全屏" cookieconsentMessage = "本网站使用 Cookies 来改善你的浏览体验。" cookieconsentDismiss = "同意" cookieconsentLink = "了解更多" @@ -232,3 +233,17 @@ changed = "更改" deleted = "删除" deprecated = "弃用" # === shortcodes/version.html === + +# === diagram actions === +[diagram] +zoomIn = "放大" +zoomOut = "缩小" +reset = "重置" +download = "下载 SVG" +# === diagram actions === + +# === tabs related === +[tabs] +diagram = "图表" +code = "代码" +# === tabs related === diff --git a/i18n/zh-TW.toml b/i18n/zh-TW.toml index c6ea1281..6298431d 100644 --- a/i18n/zh-TW.toml +++ b/i18n/zh-TW.toml @@ -103,8 +103,9 @@ toggleLineNumbers = "切換行號" toggleLineWrap = "切換自動換行" toggleCodeEditable = "切換可編輯程式碼區塊" downloadCode = "下載程式碼" -toggleCodeFullscreen = "切換程式碼區塊全螢幕" expandCode = "展開或摺疊程式碼區塊" +fullscreen = "全螢幕" +exitFullscreen = "退出全螢幕" cookieconsentMessage = "本網站使用 Cookies 來改善你的流覽體驗。" cookieconsentDismiss = "同意" cookieconsentLink = "瞭解更多" @@ -232,3 +233,17 @@ changed = "更改" deleted = "刪除" deprecated = "棄用" # === shortcodes/version.html === + +# === diagram actions === +[diagram] +zoomIn = "放大" +zoomOut = "縮小" +reset = "重置" +download = "下載 SVG" +# === diagram actions === + +# === tabs related === +[tabs] +diagram = "圖表" +code = "程式碼" +# === tabs related === diff --git a/layouts/_partials/function/code-header.html b/layouts/_partials/function/code-header.html index 9604c18d..a41c6d59 100644 --- a/layouts/_partials/function/code-header.html +++ b/layouts/_partials/function/code-header.html @@ -38,7 +38,7 @@ {{- end -}} {{- if $config.fullscreen -}} {{- $fullscreenIcon := dict "Class" "fa-solid fa-expand" | partial "plugin/icon.html" -}} - {{- $fullscreenBtn = printf `` (T "assets.toggleCodeFullscreen") (T "assets.toggleCodeFullscreen") $fullscreenIcon -}} + {{- $fullscreenBtn = printf `` (T "assets.fullscreen") (T "assets.fullscreen") (T "assets.exitFullscreen") $fullscreenIcon -}} {{- end -}} {{- with $config.name -}} {{- $titleEl = replace $titleEl `` (printf `` .) -}} diff --git a/layouts/_partials/layouts/assets.html b/layouts/_partials/layouts/assets.html index d7f4245f..aa41afb6 100644 --- a/layouts/_partials/layouts/assets.html +++ b/layouts/_partials/layouts/assets.html @@ -348,6 +348,10 @@ FixIt theme assets partial {{- /* Mermaid plugin */ -}} {{- if .Store.Get "hasMermaid" | and (not $isArchivesOrOffline) -}} + {{- if .Site.Params.mermaid.wrapper -}} + {{- $source := $cdn.panzoomJS | default "lib/panzoom/panzoom.min.js" -}} + {{- dict "Source" $source "Fingerprint" $fingerprint "Defer" true | dict "Page" $ "Data" | partial "store/script.html" -}} + {{- end -}} {{- $options := dict "Source" "js/lib/mermaid.js" "Template" "js/lib/mermaid.js" -}} {{- $options = dict "Context" . "Minify" hugo.IsProduction "Fingerprint" $fingerprint "Attr" `type="module"` | merge $options -}} {{- $options | dict "Page" $ "Data" | partial "store/script.html" -}} diff --git a/layouts/_partials/plugin/mermaid.html b/layouts/_partials/plugin/mermaid.html index b7be8510..f904c448 100644 --- a/layouts/_partials/plugin/mermaid.html +++ b/layouts/_partials/plugin/mermaid.html @@ -1,9 +1,56 @@ {{- /* Mermaid support for code fences extended and shortcodes. */ -}} -
-
{{ .Inner | safeHTML }}
-
{{ .Inner | safeHTML }}
-
{{ .Inner | safeHTML }}
- {{- dict "Label" "Copy Mermaid code" | partial "plugin/diagram-copy-btn.html" -}} - -
-{{- /* EOF */ -}} +{{- $code := strings.TrimSpace .Inner -}} +{{- $wrapped := .Options.wrapper | default site.Params.mermaid.wrapper -}} + +{{- if $wrapped -}} + {{- $labelDiagram := T "tabs.diagram" -}} + {{- $labelCode := T "tabs.code" -}} + {{- $labelZoomIn := T "diagram.zoomIn" -}} + {{- $labelZoomOut := T "diagram.zoomOut" -}} + {{- $labelReset := T "diagram.reset" -}} + {{- $labelDownload := T "diagram.download" -}} + {{- $filename := .Options.filename | default "mermaid.mmd" -}} + {{- $codeAttrs := printf `{copyable=true, fullscreen=true, filename="%s", class="diagram-code"}` $filename -}} + {{- $codeFence := printf "```text %s\n%s\n```" $codeAttrs $code -}} + +
+
+
+ {{ $labelDiagram }} + {{ $labelCode }} +
+
+ + + + +
+
+
+
+
+
{{ $code | safeHTML }}
+
{{ $code | safeHTML }}
+
{{ $code | safeHTML }}
+
+
+ {{- $codeFence | page.RenderString (dict "display" "block") -}} +
+
+{{- else -}} +
+
{{ $code | safeHTML }}
+
{{ $code | safeHTML }}
+
{{ $code | safeHTML }}
+ {{- dict "Label" "Copy Mermaid code" | partial "plugin/diagram-copy-btn.html" -}} + +
+{{- end -}} diff --git a/librarybot.yml b/librarybot.yml index eea7efb7..4e03ea9e 100644 --- a/librarybot.yml +++ b/librarybot.yml @@ -131,6 +131,12 @@ libraries: items: - from: dist/browser/pangu.umd.js to: pangu/pangu.umd.js + - npm: '@panzoom/panzoom' + version: 4.6.2 + local: + items: + - from: dist/panzoom.min.js + to: panzoom/panzoom.min.js - npm: sharer.js version: 0.5.1 local: