个人经历生活感悟博客搭建树莓派除夕夜冰淇淋感冒鼻炎童年时光龟苓膏使用体验零食我的世界皮肤AI 生成皮肤豆包 AILUMEN游戏皮肤生成家庭西瓜个人网站CSS 美化技术折腾AI 机器人DeepSeek前端脚本公众号运维七年坚持选择比努力重要AstrBotShell 命令权限管理大语言模型Brave 浏览器macOS系统错误覆盖安装1PanelButterfly粉丝统计友链关注按钮暗色模式HexoCSS 阴影毛玻璃效果随机跳转网站默哀自定义提示条CloudflareAI 防护Dotfiles备份脚本Vercel编程工具图片占位GiscusFrontmatterHexo 教程Markdown 语法Markdown 教程常用语法OpenClawToken 消耗QQ 空间QQ社交反思微信对比数字记忆RSSHubB 站我的世界VulkanMod游戏优化帧率提升Beryl 光影Waline评论系统CDN邮箱提醒SMTP 配置环境变量光影游戏攻略MesaWorkersPages星空邮箱混淆Linux鼠须管高考高考作弊校园回忆青春论坛地狱赛跑死后世界灵魂竞赛恐怖综艺死亡体验Ubuntu 壁纸下载脚本清华镜像友谊情感回忆宠物深夜故事文件分类Linux 脚本暴太郎特摄剧超级战队剧情讨论角色分析巴巴爸爸动画博客日常纪实域名续费华莱士儿童套餐快餐体验头像解析GravatarGitHub Pages部署平台Cloudflare Pages蛋糕网络速度游戏体验Steam 客户端购物体验饥荒联机版性别误认二次元空岛生存生存模式末影龙科幻故事时间机器元气骑士游戏回忆地牢游戏儿童节大逃杀城市漫步午后时光中考梦境对话硫磺史莱姆游戏装饰我的世界攻略游戏趣味玩法AI 检测文章识别写作风格技术讨论诗云诗歌创作人工智能版权保护创作意义Shell 脚本壁纸切换个人项目Windows 广告博客活动AI数据修复广州出行技术聚会小红书中美对账跨文化交流游戏探索代理软件网络故障暴雨气候变化神奇数字马戏团甜品怀旧社会现象误判垃圾邮件网络安全本地化饮食习惯代际沟通亲情B 站小站爬虫技术Python 脚本Playwright小学回忆Mac 游戏拔刀剑外星人数据备份夏日抄袭争议材质包食品安全医院经历都市传说未解之谜大脚怪尼斯湖水怪诡异小区故事情绪管理奶茶空岛数据包GitGit 历史git show版本控制代码管理Node.js物品模型插件推荐微信支付苹果日历记账脚本流浪猫图片压缩WebP交通安全监控摄像头隐私泄露地下通道送货毒品滥用张雪峰张雪就业选择个人奋斗不文明行为泰迦奥特曼人间体分析友情Ubuntu发明潜水空气炸锅美食视频上火MacBook恋爱观AI 编辑器Kali Linux保存的快捷栏泰拉瑞亚毕业批量拉黑游戏回坑信息茧房抖音服务器明日方舟崩坏三数据包游戏更新末日生存太空旅行光纤问题魔法学校端午节屈原传统文化节日习俗驱邪避疫泡面挂面QQ 群退群短视频知乎内容质量算法推荐良师益友数据泄露Apple Music音乐平台电脑买卖诈骗文本扩散模型装修PJAXClaude CodeDeepSeek APIClash VergeDNS 配置SEO活动行订票取消旅行计划DMARCTahoe洗鼻技术写作文字园洗牙牙龈炎口腔健康研学数据恢复天文比赛二手回收网恋废弃房屋Steam 骗局转学生MkDocs未来世界科技倒退路径纠错
` const blob = new Blob([htmlSource], { type: 'text/html;charset=utf-8' }) const url = URL.createObjectURL(blob) window.open(url, '_blank', 'noopener') setTimeout(() => URL.revokeObjectURL(url), 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', e => { e.preventDefault() e.stopPropagation() const svg = wrap.__mermaidOriginalSvg || wrap.querySelector('svg') if (!svg) return const initViewBox = wrap.__mermaidInitViewBox openSvgInNewTab({ source: svg, initViewBox }) }) btn.__mermaidViewerBound = true } } // Zoom around a point (px, py) in the SVG viewport (in viewBox coordinates) const zoomAtPoint = (vb, factor, px, py) => { const w = vb[2] * factor const h = vb[3] * factor const nx = px - (px - vb[0]) * factor const ny = py - (py - vb[1]) * factor return [nx, ny, w, h] } const initMermaidGestures = wrap => { // Clean up previous event listeners and pending frames if (wrap.__mermaidAbortController) { wrap.__mermaidAbortController.abort() if (wrap.__mermaidRafId) cancelAnimationFrame(wrap.__mermaidRafId) } const ac = new AbortController() wrap.__mermaidAbortController = ac const svg = wrap.querySelector('svg') if (!svg) return // Ensure viewBox exists so gestures always work const initVb = getSvgViewBox(svg) wrap.__mermaidInitViewBox = initVb wrap.__mermaidCurViewBox = initVb.slice() setSvgViewBox(svg, initVb) // Disable default gestures to prevent scroll chaining and pinch-zoom penetration in Chrome svg.style.touchAction = 'none' // Cache BoundingClientRect, throttled on scroll to reduce reflow let cachedRect = svg.getBoundingClientRect() let rectDirty = false const markRectDirty = () => { rectDirty = true } window.addEventListener('resize', markRectDirty, { signal: ac.signal }) window.addEventListener('scroll', markRectDirty, { signal: ac.signal, capture: true }) const getRect = () => { if (rectDirty) { cachedRect = svg.getBoundingClientRect() rectDirty = false } return cachedRect } // Precompute clamp bounds from initial viewBox const minW = initVb[2] * 0.1 const maxW = initVb[2] * 10 const minH = initVb[3] * 0.1 const maxH = initVb[3] * 10 const clampVb = vb => { const out = vb.slice() out[2] = clamp(out[2], minW, maxW) out[3] = clamp(out[3], minH, maxH) return out } // Throttle DOM updates using requestAnimationFrame let pendingVb = null let rafId = null const applyVb = () => { if (pendingVb) { wrap.__mermaidCurViewBox = pendingVb setSvgViewBox(svg, pendingVb) pendingVb = null } rafId = null wrap.__mermaidRafId = null } const setCurVb = vb => { pendingVb = clampVb(vb) if (!rafId) { rafId = requestAnimationFrame(applyVb) wrap.__mermaidRafId = rafId } } const state = { pointers: new Map(), startVb: null, startDist: 0, lastPointerX: 0, lastPointerY: 0 } const onPointerDown = e => { if (e.pointerType === 'mouse' && e.button !== 0) return svg.setPointerCapture(e.pointerId) const curVb = wrap.__mermaidCurViewBox state.pointers.set(e.pointerId, { x: e.clientX, y: e.clientY }) if (state.pointers.size === 1) { state.startVb = curVb.slice() state.lastPointerX = e.clientX state.lastPointerY = e.clientY } else if (state.pointers.size === 2) { const pts = [...state.pointers.values()] const dx = pts[0].x - pts[1].x const dy = pts[0].y - pts[1].y state.startDist = Math.hypot(dx, dy) state.startVb = curVb.slice() } } const onPointerMove = e => { if (!state.pointers.has(e.pointerId)) return state.pointers.set(e.pointerId, { x: e.clientX, y: e.clientY }) const curVb = wrap.__mermaidCurViewBox const rect = getRect() if (state.pointers.size === 1 && state.startVb) { const p = state.pointers.values().next().value const dxClient = p.x - state.lastPointerX const dyClient = p.y - state.lastPointerY state.lastPointerX = p.x state.lastPointerY = p.y const dx = dxClient * (curVb[2] / rect.width) const dy = dyClient * (curVb[3] / rect.height) setCurVb([curVb[0] - dx, curVb[1] - dy, curVb[2], 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].x const dy = pts[0].y - pts[1].y const dist = Math.hypot(dx, dy) if (!dist) return const factor = state.startDist / dist const cx = (pts[0].x + pts[1].x) / 2 const cy = (pts[0].y + pts[1].y) / 2 const 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, factor, px, py)) } } const onPointerUpOrCancel = e => { // Release PointerCapture to avoid event capture anomalies if (svg.hasPointerCapture && svg.hasPointerCapture(e.pointerId)) { svg.releasePointerCapture(e.pointerId) } state.pointers.delete(e.pointerId) if (state.pointers.size === 0) { state.startVb = null state.startDist = 0 } else if (state.pointers.size === 1) { const p = state.pointers.values().next().value state.lastPointerX = p.x state.lastPointerY = p.y } } const onWheel = e => { // Prevent event bubbling from triggering external scroll e.preventDefault() e.stopPropagation() // Normalize deltaY across deltaMode (Chrome uses pixels, Safari uses lines) let delta = e.deltaY if (e.deltaMode === 1) delta *= 16 else if (e.deltaMode === 2) delta *= 400 // Continuous zoom factor: smoother than fixed 1.1/0.9 steps const zoomFactor = Math.pow(1.001, delta) const curVb = wrap.__mermaidCurViewBox const 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, zoomFactor, px, py)) } const onDblClick = () => { const init = wrap.__mermaidInitViewBox if (init) setCurVb(init) } svg.addEventListener('pointerdown', onPointerDown, { signal: ac.signal }) svg.addEventListener('pointermove', onPointerMove, { signal: ac.signal }) svg.addEventListener('pointerup', onPointerUpOrCancel, { signal: ac.signal }) svg.addEventListener('pointercancel', onPointerUpOrCancel, { signal: ac.signal }) svg.addEventListener('wheel', onWheel, { passive: false, signal: ac.signal }) svg.addEventListener('dblclick', onDblClick, { signal: ac.signal }) } const runMermaid = ele => { window.loadMermaid = true const theme = document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'default' ele.forEach((item, index) => { const mermaidSrc = item.firstElementChild // Clean up event listeners before removing old SVG if (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:', e) } if (!config.theme) { config.theme = theme } const mermaidThemeConfig = `%%{init: ${JSON.stringify(config)}}%%\n` const mermaidID = `mermaid-${index}` const mermaidDefinition = mermaidThemeConfig + mermaidSrc.textContent const renderMermaid = svg => { mermaidSrc.insertAdjacentHTML('afterend', svg) if (false) initMermaidGestures(item) item.__mermaidOriginalSvg = svg if (true) attachMermaidViewerButton(item) } const handleError = err => { console.error(`[mermaid] render failed for block #${index}:`, err) const errorEl = document.createElement('div') errorEl.className = 'mermaid-error' errorEl.textContent = `Mermaid render error: ${err.message || err}` mermaidSrc.insertAdjacentElement('afterend', errorEl) } try { const renderFn = mermaid.render(mermaidID, mermaidDefinition) // mermaid v9 and v10 compatibility if (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) return codeMermaidEle.forEach(ele => { const preEle = document.createElement('pre') preEle.className = 'mermaid-src' preEle.hidden = true preEle.textContent = ele.textContent const 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) return if (true) codeToMermaid($article) const $mermaid = $article.querySelectorAll('.mermaid-wrap') if ($mermaid.length === 0) return const runMermaidFn = () => runMermaid($mermaid) btf.addGlobalFn('themeChange', runMermaidFn, 'mermaid') window.loadMermaid ? runMermaidFn() : btf.getScript('https://cdn.jsdelivr.net/npm/mermaid@11.16.0/dist/mermaid.min.js').then(runMermaidFn) } btf.addGlobalFn('encrypt', loadMermaid, 'mermaid') window.pjax ? loadMermaid() : document.addEventListener('DOMContentLoaded', loadMermaid) })()