@@ -2,6 +2,16 @@ import DefaultTheme from 'vitepress/theme'
22import './style.css'
33
44let rootEnglishLabel = 'English'
5+ let mermaidViewer
6+ let mermaidViewerBody
7+ let mermaidViewerStage
8+ let mermaidObserver
9+ let mermaidViewerUrl
10+ let mermaidViewerImage
11+ let mermaidViewerNaturalWidth = 0
12+ let mermaidViewerNaturalHeight = 0
13+ let mermaidViewerFitScale = 1
14+ let mermaidViewerScale = 1
515
616function dedupeLocaleMenuLinks ( ) {
717 if ( typeof document === 'undefined' ) return
@@ -60,16 +70,257 @@ function syncRootLocaleLabel(siteData, href = '') {
6070 locales . root . label = relative . startsWith ( '/zh/' ) ? locales . zh . label : rootEnglishLabel
6171}
6272
73+ function ensureMermaidViewer ( ) {
74+ if ( typeof document === 'undefined' ) return
75+ if ( mermaidViewer && mermaidViewerBody && mermaidViewerStage ) return
76+
77+ mermaidViewer = document . createElement ( 'div' )
78+ mermaidViewer . className = 'mermaid-viewer'
79+ mermaidViewer . innerHTML = `
80+ <div class="mermaid-viewer__backdrop" data-mermaid-close="true"></div>
81+ <div class="mermaid-viewer__panel" role="dialog" aria-modal="true" aria-label="Expanded Mermaid diagram">
82+ <div class="mermaid-viewer__toolbar">
83+ <button type="button" class="mermaid-viewer__tool" aria-label="Zoom out" data-mermaid-action="zoom-out">−</button>
84+ <button type="button" class="mermaid-viewer__tool mermaid-viewer__tool--label" aria-label="Reset zoom" data-mermaid-action="reset">100%</button>
85+ <button type="button" class="mermaid-viewer__tool" aria-label="Zoom in" data-mermaid-action="zoom-in">+</button>
86+ <button type="button" class="mermaid-viewer__close" aria-label="Close diagram" data-mermaid-close="true">×</button>
87+ </div>
88+ <div class="mermaid-viewer__body">
89+ <div class="mermaid-viewer__stage"></div>
90+ </div>
91+ </div>
92+ `
93+
94+ mermaidViewerBody = mermaidViewer . querySelector ( '.mermaid-viewer__body' )
95+ mermaidViewerStage = mermaidViewer . querySelector ( '.mermaid-viewer__stage' )
96+
97+ mermaidViewer . addEventListener ( 'click' , ( event ) => {
98+ const target = event . target
99+ if ( ! ( target instanceof HTMLElement ) ) return
100+
101+ if (
102+ target . dataset . mermaidClose === 'true' ||
103+ target === mermaidViewerBody ||
104+ target === mermaidViewerStage
105+ ) {
106+ closeMermaidViewer ( )
107+ return
108+ }
109+
110+ const action = target . dataset . mermaidAction
111+ if ( action === 'zoom-in' ) {
112+ setMermaidViewerScale ( mermaidViewerScale + 0.2 )
113+ } else if ( action === 'zoom-out' ) {
114+ setMermaidViewerScale ( mermaidViewerScale - 0.2 )
115+ } else if ( action === 'reset' ) {
116+ setMermaidViewerScale ( 1 )
117+ }
118+ } )
119+
120+ document . addEventListener ( 'keydown' , ( event ) => {
121+ if ( event . key === 'Escape' && mermaidViewer ?. classList . contains ( 'is-open' ) ) {
122+ closeMermaidViewer ( )
123+ } else if ( ( event . key === '+' || event . key === '=' ) && mermaidViewer ?. classList . contains ( 'is-open' ) ) {
124+ setMermaidViewerScale ( mermaidViewerScale + 0.2 )
125+ } else if ( event . key === '-' && mermaidViewer ?. classList . contains ( 'is-open' ) ) {
126+ setMermaidViewerScale ( mermaidViewerScale - 0.2 )
127+ }
128+ } )
129+
130+ mermaidViewerBody ?. addEventListener ( 'wheel' , ( event ) => {
131+ if ( ! mermaidViewer ?. classList . contains ( 'is-open' ) ) return
132+ event . preventDefault ( )
133+ const delta = event . deltaY < 0 ? 0.12 : - 0.12
134+ setMermaidViewerScale ( mermaidViewerScale + delta )
135+ } , { passive : false } )
136+
137+ window . addEventListener ( 'resize' , ( ) => {
138+ if ( mermaidViewer ?. classList . contains ( 'is-open' ) ) {
139+ updateMermaidViewerFitScale ( )
140+ setMermaidViewerScale ( mermaidViewerScale )
141+ }
142+ } )
143+
144+ document . body . appendChild ( mermaidViewer )
145+ }
146+
147+ function serializeMermaidSvg ( svg ) {
148+ const clone = svg . cloneNode ( true )
149+ if ( ! ( clone instanceof SVGElement ) ) return null
150+
151+ if ( ! clone . getAttribute ( 'xmlns' ) ) {
152+ clone . setAttribute ( 'xmlns' , 'http://www.w3.org/2000/svg' )
153+ }
154+ if ( ! clone . getAttribute ( 'xmlns:xlink' ) ) {
155+ clone . setAttribute ( 'xmlns:xlink' , 'http://www.w3.org/1999/xlink' )
156+ }
157+
158+ const rect = svg . getBoundingClientRect ( )
159+ const viewBox = svg . viewBox ?. baseVal
160+ const rawWidth = rect . width || viewBox ?. width || Number . parseFloat ( svg . getAttribute ( 'width' ) || '' ) || 1200
161+ const rawHeight = rect . height || viewBox ?. height || Number . parseFloat ( svg . getAttribute ( 'height' ) || '' ) || 800
162+
163+ if ( ! clone . getAttribute ( 'viewBox' ) ) {
164+ clone . setAttribute ( 'viewBox' , `0 0 ${ Math . max ( 1 , Math . round ( rawWidth ) ) } ${ Math . max ( 1 , Math . round ( rawHeight ) ) } ` )
165+ }
166+
167+ clone . setAttribute ( 'width' , String ( Math . max ( 1 , Math . round ( rawWidth ) ) ) )
168+ clone . setAttribute ( 'height' , String ( Math . max ( 1 , Math . round ( rawHeight ) ) ) )
169+
170+ return {
171+ markup : new XMLSerializer ( ) . serializeToString ( clone ) ,
172+ width : Math . max ( 1 , rawWidth ) ,
173+ height : Math . max ( 1 , rawHeight )
174+ }
175+ }
176+
177+ function updateMermaidViewerFitScale ( ) {
178+ if ( ! mermaidViewerNaturalWidth || ! mermaidViewerNaturalHeight || typeof window === 'undefined' ) return
179+
180+ const maxWidth = Math . max ( 320 , window . innerWidth - 16 )
181+ const maxHeight = Math . max ( 240 , window . innerHeight - 16 )
182+ mermaidViewerFitScale = Math . max (
183+ 0.25 ,
184+ Math . min ( maxWidth / mermaidViewerNaturalWidth , maxHeight / mermaidViewerNaturalHeight )
185+ )
186+ }
187+
188+ function setMermaidViewerScale ( nextScale ) {
189+ if ( ! mermaidViewerStage || ! mermaidViewerNaturalWidth || ! mermaidViewerNaturalHeight ) return
190+
191+ mermaidViewerScale = Math . max ( 0.5 , Math . min ( nextScale , 4 ) )
192+
193+ const width = mermaidViewerNaturalWidth * mermaidViewerFitScale * mermaidViewerScale
194+ const height = mermaidViewerNaturalHeight * mermaidViewerFitScale * mermaidViewerScale
195+
196+ mermaidViewerStage . style . width = `${ width } px`
197+ mermaidViewerStage . style . height = `${ height } px`
198+
199+ const resetButton = mermaidViewer ?. querySelector ( '[data-mermaid-action="reset"]' )
200+ if ( resetButton instanceof HTMLElement ) {
201+ resetButton . textContent = `${ Math . round ( mermaidViewerScale * 100 ) } %`
202+ }
203+ }
204+
205+ function openMermaidViewer ( diagram ) {
206+ ensureMermaidViewer ( )
207+ if ( ! mermaidViewerBody || ! mermaidViewer || ! mermaidViewerStage ) return
208+
209+ const svg = diagram . querySelector ( 'svg' )
210+ if ( ! svg ) return
211+
212+ if ( mermaidViewerUrl ) {
213+ URL . revokeObjectURL ( mermaidViewerUrl )
214+ mermaidViewerUrl = undefined
215+ }
216+
217+ const serialized = serializeMermaidSvg ( svg )
218+ if ( ! serialized ) return
219+
220+ const blob = new Blob ( [ serialized . markup ] , { type : 'image/svg+xml;charset=utf-8' } )
221+ mermaidViewerUrl = URL . createObjectURL ( blob )
222+
223+ const image = document . createElement ( 'img' )
224+ image . className = 'mermaid-viewer__image'
225+ image . alt = 'Expanded Mermaid diagram'
226+ image . src = mermaidViewerUrl
227+ image . addEventListener ( 'load' , ( ) => {
228+ mermaidViewerNaturalWidth = serialized . width
229+ mermaidViewerNaturalHeight = serialized . height
230+ updateMermaidViewerFitScale ( )
231+ setMermaidViewerScale ( 1 )
232+ if ( mermaidViewerBody ) {
233+ mermaidViewerBody . scrollTo ( { top : 0 , left : 0 } )
234+ }
235+ } )
236+
237+ mermaidViewerImage = image
238+ mermaidViewerStage . replaceChildren ( image )
239+ mermaidViewer . classList . add ( 'is-open' )
240+ document . documentElement . classList . add ( 'has-mermaid-viewer' )
241+ }
242+
243+ function closeMermaidViewer ( ) {
244+ if ( ! mermaidViewer || ! mermaidViewerBody || ! mermaidViewerStage ) return
245+ mermaidViewer . classList . remove ( 'is-open' )
246+ mermaidViewerStage . replaceChildren ( )
247+ mermaidViewerStage . style . width = ''
248+ mermaidViewerStage . style . height = ''
249+ if ( mermaidViewerUrl ) {
250+ URL . revokeObjectURL ( mermaidViewerUrl )
251+ mermaidViewerUrl = undefined
252+ }
253+ mermaidViewerImage = undefined
254+ mermaidViewerNaturalWidth = 0
255+ mermaidViewerNaturalHeight = 0
256+ mermaidViewerFitScale = 1
257+ mermaidViewerScale = 1
258+ document . documentElement . classList . remove ( 'has-mermaid-viewer' )
259+ }
260+
261+ function bindMermaidZoomables ( root = document ) {
262+ if ( typeof document === 'undefined' ) return
263+ ensureMermaidViewer ( )
264+
265+ const diagrams = root . querySelectorAll ( '.vp-doc .mermaid' )
266+ for ( const diagram of diagrams ) {
267+ if ( ! ( diagram instanceof HTMLElement ) || diagram . dataset . mermaidZoomBound === 'true' ) {
268+ continue
269+ }
270+
271+ diagram . dataset . mermaidZoomBound = 'true'
272+ diagram . tabIndex = 0
273+ diagram . setAttribute ( 'role' , 'button' )
274+ diagram . setAttribute ( 'aria-label' , 'Open Mermaid diagram in fullscreen' )
275+
276+ diagram . addEventListener ( 'click' , ( ) => {
277+ openMermaidViewer ( diagram )
278+ } )
279+
280+ diagram . addEventListener ( 'keydown' , ( event ) => {
281+ if ( event . key === 'Enter' || event . key === ' ' ) {
282+ event . preventDefault ( )
283+ openMermaidViewer ( diagram )
284+ }
285+ } )
286+ }
287+ }
288+
289+ function queueMermaidBinding ( ) {
290+ if ( typeof window === 'undefined' ) return
291+ window . requestAnimationFrame ( ( ) => {
292+ window . requestAnimationFrame ( ( ) => {
293+ bindMermaidZoomables ( )
294+ } )
295+ } )
296+ }
297+
298+ function ensureMermaidObserver ( ) {
299+ if ( typeof document === 'undefined' || mermaidObserver ) return
300+ mermaidObserver = new MutationObserver ( ( ) => {
301+ bindMermaidZoomables ( )
302+ } )
303+
304+ mermaidObserver . observe ( document . body , {
305+ childList : true ,
306+ subtree : true
307+ } )
308+ }
309+
63310export default {
64311 extends : DefaultTheme ,
65312 enhanceApp ( ctx ) {
66313 syncRootLocaleLabel ( ctx . siteData )
67314 queueLocaleMenuDedupe ( )
315+ queueMermaidBinding ( )
316+ ensureMermaidObserver ( )
68317
69318 const previous = ctx . router . onAfterRouteChange
70319 ctx . router . onAfterRouteChange = async ( to ) => {
71320 syncRootLocaleLabel ( ctx . siteData , to )
72321 queueLocaleMenuDedupe ( )
322+ closeMermaidViewer ( )
323+ queueMermaidBinding ( )
73324 if ( previous ) {
74325 await previous ( to )
75326 }
0 commit comments