从原理到生产的完整指南
${svgSource}`const blob = new Blob([htmlSource], { signal: ac.signal。
{ signal: ac.signal })svg.addEventListener('pointerup'。
px, minW, dy)state.startVb = curVb.slice()}}const onPointerMove = e => {if (!state.pointers.has(e.pointerId)) returnstate.pointers.set(e.pointerId, Safari uses lines)let delta = e.deltaYif (e.deltaMode === 1) delta *= 16else if (e.deltaMode === 2) delta *= 400// Continuous zoom factor: smoother than fixed 1.1/0.9 stepsconst zoomFactor = Math.pow(1.001, onPointerUpOrCancel, factor, initVb)// Disable default gestures to prevent scroll chaining and pinch-zoom penetration in Chromesvg.style.touchAction = 'none'// Cache BoundingClientRect, px, { passive: false,lastPointerY: 0}const onPointerDown = e => {if (e.pointerType === 'mouse' e.button !== 0) returnsvg.setPointerCapture(e.pointerId)const curVb = wrap.__mermaidCurViewBoxstate.pointers.set(e.pointerId, e => {e.preventDefault()e.stopPropagation()const svg = wrap.__mermaidOriginalSvg || wrap.querySelector('svg')if (!svg) returnconst initViewBox = wrap.__mermaidInitViewBoxopenSvgInNewTab({ source: svg, svg)if (true) initMermaidGestures(item)item.__mermaidOriginalSvg = svgif (true) attachMermaidViewerButton(item)}const handleError = err => {console.error(`[mermaid] render failed for block #${index}:`, loadMermaid)})() ,lastPointerX: 0,。
maxW)out[3] = clamp(out[3], dy)if (!dist) returnconst factor = state.startDist / distconst cx = (pts[0].x + pts[1].x) / 2const cy = (pts[0].y + pts[1].y) / 2const px = curVb[0] + (cx - rect.left) * (curVb[2] / rect.width)const py = curVb[1] + (cy - rect.top) * (curVb[3] / rect.height)setCurVb(zoomAtPoint(state.startVb。
y: e.clientY })const curVb = wrap.__mermaidCurViewBoxconst rect = getRect()if (state.pointers.size === 1 state.startVb) {const p = state.pointers.values().next().valueconst dxClient = p.x - state.lastPointerXconst dyClient = p.y - state.lastPointerYstate.lastPointerX = p.xstate.lastPointerY = p.yconst dx = dxClient * (curVb[2] / rect.width)const dy = dyClient * (curVb[3] / rect.height)setCurVb([curVb[0] - dx, e)}if (!config.theme) {config.theme = theme}const mermaidThemeConfig = `%%{init: ${JSON.stringify(config)}}%%\n`const mermaidID = `mermaid-${index}`const mermaidDefinition = mermaidThemeConfig + mermaidSrc.textContentconst renderMermaid = svg => {mermaidSrc.insertAdjacentHTML('afterend', maxH)return out}// Throttle DOM updates using requestAnimationFramelet pendingVb = nulllet rafId = nullconst applyVb = () => {if (pendingVb) {wrap.__mermaidCurViewBox = pendingVbsetSvgViewBox(svg, capture: true })const getRect = () => {if (rectDirty) {cachedRect = svg.getBoundingClientRect()rectDirty = false}return cachedRect}// Precompute clamp bounds from initial viewBoxconst minW = initVb[2] * 0.1const maxW = initVb[2] * 10const minH = initVb[3] * 0.1const maxH = initVb[3] * 10const clampVb = vb => {const out = vb.slice()out[2] = clamp(out[2], markRectDirty, onWheel, h] } const initMermaidGestures = wrap => {// Clean up previous event listeners and pending framesif (wrap.__mermaidAbortController) {wrap.__mermaidAbortController.abort()if (wrap.__mermaidRafId) cancelAnimationFrame(wrap.__mermaidRafId)}const ac = new AbortController()wrap.__mermaidAbortController = acconst svg = wrap.querySelector('svg')if (!svg) return// Ensure viewBox exists so gestures always workconst initVb = getSvgViewBox(svg)wrap.__mermaidInitViewBox = initVbwrap.__mermaidCurViewBox = initVb.slice()setSvgViewBox(svg, zoomFactor, loadMermaid, curVb[3]])return}if (state.pointers.size === 2 state.startVb state.startDist > 0) {const pts = [...state.pointers.values()]const dx = pts[0].x - pts[1].xconst dy = pts[0].y - pts[1].yconst dist = Math.hypot(dx, curVb[2]。
py))}}const onPointerUpOrCancel = e => {// Release PointerCapture to avoid event capture anomaliesif (svg.hasPointerCapture svg.hasPointerCapture(e.pointerId)) {svg.releasePointerCapture(e.pointerId)}state.pointers.delete(e.pointerId)if (state.pointers.size === 0) {state.startVb = nullstate.startDist = 0} else if (state.pointers.size === 1) {const p = state.pointers.values().next().valuestate.lastPointerX = p.xstate.lastPointerY = p.y}}const onWheel = e => {// Prevent event bubbling from triggering external scrolle.preventDefault()e.stopPropagation()// Normalize deltaY across deltaMode (Chrome uses pixels, 5000) } const attachMermaidViewerButton = wrap => {let btn = wrap.querySelector('.mermaid-open-btn')if (!btn) {btn = document.createElement('button')btn.type = 'button'btn.className = 'mermaid-open-btn'wrap.appendChild(btn)}btn.innerHTML = ''if (!btn.__mermaidViewerBound) {btn.addEventListener('click', 'mermaid')window.loadMermaid ? runMermaidFn() : btf.getScript('https://cdn.jsdelivr.net/npm/mermaid@11.16.0/dist/mermaid.min.js').then(runMermaidFn) } btf.addGlobalFn('encrypt', err)const errorEl = document.createElement('div')errorEl.className = 'mermaid-error'errorEl.textContent = `Mermaid render error: ${err.message || err}`mermaidSrc.insertAdjacentElement('afterend'。
initViewBox })})btn.__mermaidViewerBound = true} } // Zoom around a point (px。
startVb: null, minH, factor, { signal: ac.signal })svg.addEventListener('wheel', w, index) => {const mermaidSrc = item.firstElementChild// Clean up event listeners before removing old SVGif (item.__mermaidAbortController) {item.__mermaidAbortController.abort()}const oldSvg = item.querySelector('svg')if (oldSvg) oldSvg.remove()let config = {}try {config = mermaidSrc.dataset.config ? JSON.parse(mermaidSrc.dataset.config) : {}} catch (e) {console.warn('[mermaid] failed to parse dataset.config:', { signal: ac.signal })svg.addEventListener('pointermove', y: e.clientY })if (state.pointers.size === 1) {state.startVb = curVb.slice()state.lastPointerX = e.clientXstate.lastPointerY = e.clientY} else if (state.pointers.size === 2) {const pts = [...state.pointers.values()]const dx = pts[0].x - pts[1].xconst dy = pts[0].y - pts[1].ystate.startDist = Math.hypot(dx。
ny, { type: 'text/html;charset=utf-8' })const url = URL.createObjectURL(blob)window.open(url, delta)const curVb = wrap.__mermaidCurViewBoxconst rect = getRect()const px = curVb[0] + (e.clientX - rect.left) * (curVb[2] / rect.width)const py = curVb[1] + (e.clientY - rect.top) * (curVb[3] / rect.height)setCurVb(zoomAtPoint(curVb, { signal: ac.signal })window.addEventListener('scroll',startDist: 0, '_blank', py) in the SVG viewport (in viewBox coordinates) const zoomAtPoint = (vb, pendingVb)pendingVb = null}rafId = nullwrap.__mermaidRafId = null}const setCurVb = vb => {pendingVb = clampVb(vb)if (!rafId) {rafId = requestAnimationFrame(applyVb)wrap.__mermaidRafId = rafId}}const state = {pointers: new Map(), { signal: ac.signal })svg.addEventListener('pointercancel', px, onPointerDown, { x: e.clientX, py) => {const w = vb[2] * factorconst h = vb[3] * factorconst nx = px - (px - vb[0]) * factorconst ny = py - (py - vb[1]) * factorreturn [nx, runMermaidFn, markRectDirty, onDblClick, 'noopener')setTimeout(() => URL.revokeObjectURL(url), onPointerMove, throttled on scroll to reduce reflowlet cachedRect = svg.getBoundingClientRect()let rectDirty = falseconst markRectDirty = () => { rectDirty = true }window.addEventListener('resize'。
curVb[1] - dy, 'mermaid') window.pjax ? loadMermaid() : document.addEventListener('DOMContentLoaded', mermaidDefinition)// mermaid v9 and v10 compatibilityif (typeof renderFn === 'string') {renderMermaid(renderFn)} else {renderFn.then(({ svg }) => renderMermaid(svg)).catch(handleError)}} catch (err) {handleError(err)}}) } const codeToMermaid = $article => {const codeMermaidEle = $article.querySelectorAll('pre > code.mermaid')if (codeMermaidEle.length === 0) returncodeMermaidEle.forEach(ele => {const preEle = document.createElement('pre')preEle.className = 'mermaid-src'preEle.hidden = truepreEle.textContent = ele.textContentconst newEle = document.createElement('div')newEle.className = 'mermaid-wrap'newEle.appendChild(preEle)ele.parentNode.replaceWith(newEle)}) } const loadMermaid = () => {const $article = document.getElementById('article-container')if (!$article) returnif (true) codeToMermaid($article)const $mermaid = $article.querySelectorAll('.mermaid-wrap')if ($mermaid.length === 0) returnconst runMermaidFn = () => runMermaid($mermaid)btf.addGlobalFn('themeChange'。
{ signal: ac.signal }) } const runMermaid = ele => {window.loadMermaid = trueconst theme = document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'default'ele.forEach((item, { x: e.clientX。
signal: ac.signal })svg.addEventListener('dblclick'。
py))}const onDblClick = () => {const init = wrap.__mermaidInitViewBoxif (init) setCurVb(init)}svg.addEventListener('pointerdown', errorEl)}try {const renderFn = mermaid.render(mermaidID, onPointerUpOrCancel。
评论列表