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>
This commit is contained in:
Cell
2026-04-29 16:42:15 +08:00
committed by GitHub
parent 5fe681ccc5
commit e68d10d7d9
34 changed files with 1352 additions and 177 deletions
-1
View File
@@ -85,7 +85,6 @@ FixIt/
```scss
@include border-radius;
@include transition(all 0.2s ease);
```
### 代码质量要求
+2 -1
View File
@@ -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"
]
}
+169
View File
@@ -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]+
@@ -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
```
<!--more-->
## Flowchart
```mermaid
@@ -24,8 +35,6 @@ flowchart TD
B ---->|No| E[End]
```
<!--more-->
## Sequence Diagram
```mermaid
+2
View File
@@ -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
+2
View File
@@ -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
+653 -86
View File
@@ -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<void>}
*/
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<any>}
*/
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. `&gt;`) 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 `<svg>` 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<void>}
*/
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
+45 -25
View File
@@ -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');
File diff suppressed because one or more lines are too long
+8
View File
@@ -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 {
+1 -1
View File
@@ -20,7 +20,7 @@
container-type: scroll-state;
container-name: scroller;
&:has(dialog[open]) {
&:has(dialog[open], .is-fullscreen) {
overflow: hidden;
}
+41 -16
View File
@@ -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;
}
@@ -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;
}
}
+6
View File
@@ -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 = ""
+16 -1
View File
@@ -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 ===
+16 -1
View File
@@ -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 ===
+16 -1
View File
@@ -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 ===
+16 -1
View File
@@ -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 ===
+16 -1
View File
@@ -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 ===
+16 -1
View File
@@ -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 ===
+16 -1
View File
@@ -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 ===
+16 -1
View File
@@ -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 ===
+16 -1
View File
@@ -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 ===
+16 -1
View File
@@ -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 ===
+16 -1
View File
@@ -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 ===
+16 -1
View File
@@ -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 ===
+16 -1
View File
@@ -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 ===
+16 -1
View File
@@ -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 ===
+16 -1
View File
@@ -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 ===
+16 -1
View File
@@ -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 ===
+1 -1
View File
@@ -38,7 +38,7 @@
{{- end -}}
{{- if $config.fullscreen -}}
{{- $fullscreenIcon := dict "Class" "fa-solid fa-expand" | partial "plugin/icon.html" -}}
{{- $fullscreenBtn = printf `<button type="button" class="action-btn fullscreen-btn" aria-label="%s" title="%s">%s</button>` (T "assets.toggleCodeFullscreen") (T "assets.toggleCodeFullscreen") $fullscreenIcon -}}
{{- $fullscreenBtn = printf `<button type="button" class="action-btn fullscreen-btn" aria-label="%s" title="%s" data-exit-title="%s">%s</button>` (T "assets.fullscreen") (T "assets.fullscreen") (T "assets.exitFullscreen") $fullscreenIcon -}}
{{- end -}}
{{- with $config.name -}}
{{- $titleEl = replace $titleEl `<span class="code-title">` (printf `<span class="code-title" data-name="%s">` .) -}}
+4
View File
@@ -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" -}}
+55 -8
View File
@@ -1,9 +1,56 @@
{{- /* Mermaid support for code fences extended and shortcodes. */ -}}
<div class="diagram-container">
<pre class="mermaid">{{ .Inner | safeHTML }}</pre>
<pre class="mermaid-dark">{{ .Inner | safeHTML }}</pre>
<pre class="mermaid-neutral">{{ .Inner | safeHTML }}</pre>
{{- dict "Label" "Copy Mermaid code" | partial "plugin/diagram-copy-btn.html" -}}
<template><pre>{{ .Inner | safeHTML }}</pre></template>
</div>
{{- /* 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 -}}
<div class="code-tabs diagram-tabs" data-diagram="mermaid">
<div class="tabs-header">
<div class="tabs-items">
<span class="tab-item active" data-tab="diagram" title="{{ $labelDiagram }}">{{ $labelDiagram }}</span>
<span class="tab-item" data-tab="code" title="{{ $labelCode }}">{{ $labelCode }}</span>
</div>
<div class="tabs-actions">
<button type="button" class="action-btn diagram-zoom-out-btn" aria-label="{{ $labelZoomOut }}" title="{{ $labelZoomOut }}">
{{- dict "Class" "fa-solid fa-magnifying-glass-minus" | partial "plugin/icon.html" -}}
</button>
<button type="button" class="action-btn diagram-zoom-in-btn" aria-label="{{ $labelZoomIn }}" title="{{ $labelZoomIn }}">
{{- dict "Class" "fa-solid fa-magnifying-glass-plus" | partial "plugin/icon.html" -}}
</button>
<button type="button" class="action-btn diagram-reset-btn" aria-label="{{ $labelReset }}" title="{{ $labelReset }}">
{{- dict "Class" "fa-solid fa-arrows-rotate" | partial "plugin/icon.html" -}}
</button>
<button type="button" class="action-btn diagram-download-btn" aria-label="{{ $labelDownload }}" title="{{ $labelDownload }}">
{{- dict "Class" "fa-solid fa-download" | partial "plugin/icon.html" -}}
</button>
</div>
</div>
<div class="tabs-content">
<div class="code-block diagram-view active" data-filename="{{ $filename }}">
<div class="diagram-container">
<pre class="mermaid">{{ $code | safeHTML }}</pre>
<pre class="mermaid-dark">{{ $code | safeHTML }}</pre>
<pre class="mermaid-neutral">{{ $code | safeHTML }}</pre>
</div>
</div>
{{- $codeFence | page.RenderString (dict "display" "block") -}}
</div>
</div>
{{- else -}}
<div class="diagram-container">
<pre class="mermaid">{{ $code | safeHTML }}</pre>
<pre class="mermaid-dark">{{ $code | safeHTML }}</pre>
<pre class="mermaid-neutral">{{ $code | safeHTML }}</pre>
{{- dict "Label" "Copy Mermaid code" | partial "plugin/diagram-copy-btn.html" -}}
<template><pre>{{ $code | safeHTML }}</pre></template>
</div>
{{- end -}}
+6
View File
@@ -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: