From 5e416eccef277067a4e4b9d655044e8bc45b243c Mon Sep 17 00:00:00 2001 From: Atlas Zeng Date: Tue, 18 Aug 2026 01:22:28 +0800 Subject: [PATCH] =?UTF-8?q?feat(mermaid):=20Markdown=20=E9=A2=84=E8=A7=88?= =?UTF-8?q?=E5=AE=89=E5=85=A8=E6=B8=B2=E6=9F=93=20Mermaid=20=E5=9B=BE?= =?UTF-8?q?=E8=A1=A8=EF=BC=88strict=20=E6=B8=85=E6=B4=97=20+=20=E6=87=92?= =?UTF-8?q?=E5=8A=A0=E8=BD=BD=20chunk=20+=20=E7=82=B9=E5=87=BB=E6=94=BE?= =?UTF-8?q?=E5=A4=A7=EF=BC=89=20(#164)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(mermaid): 新增 mermaid 懒加载 chunk(构建链四处同步 + mermaid 依赖) tsdown CHUNKS / bundle-route CHUNK_NAMES / chunk-loader ChunkName / package.json files 同步注册 mermaid chunk;mermaid ^11.16.1 进依赖与 lockfile;chunk 构建注入 uuid 浏览器入口别名,绕开 node:crypto 纯度门。 * feat(mermaid): Markdown 预览安全渲染 mermaid 图表 + 点击放大弹窗 - splitMermaidBlocks 纯函数按 fence 切块,无 mermaid 文件零行为变化; - mermaid.initialize 走 securityLevel strict + htmlLabels:false,不应用 bindFunctions(静态图);渲染 SVG 经 mermaid-sanitize 二次清洗: 删 foreignObject/script/外来 HTML 元素、剥 @*/on* 属性、移除全部 href/xlink:href,仅接受 SVG 根,解析失败返回空串; - 点击图表弹出放大弹窗(滚轮缩放/拖拽平移/快捷键,借鉴 PR #75 交互, 基于已清洗 SVG 克隆重写,不新增攻击面); - 深浅色跟随重渲、失败回退错误条 + 原码、CodeBlock 同款 chrome。 * test(mermaid): splitter/sanitize 单测 + chunk 清单同步 + 挂载冒烟 diagram.md - mermaid-blocks.spec 10 例 fence 切块语义; - mermaid-sanitize.spec 8 例 XSS(foreignObject/on*/@*/href/xlink:href/ script/外来元素/畸形 XML/非 SVG 根); - bundle-route / chunk-artifact / manifest-consistency 清单加入 mermaid (chunk-artifact 补 addEventListener stub); - 挂载冒烟新增 diagram.md:client-mermaid.js 往返、SVG 节点文字、 点击弹窗 Esc 关闭、preview 模式编辑器隐藏。 * docs(mermaid): 安全渲染设计文档 + lazy-chunks 表 + README 双语更新 设计文档记录基线决策(PR #99 架构 + PR #75 点击放大)、安全模型 (strict/htmlLabels/清洗/href 剥离/无 bindFunctions)、验证与取舍。 * fix(mermaid): sanitizer 属性名大小写归一,堵住混合大小写属性绕过(CR P1) XML 属性名保留大小写,而输出会经 HTML 解析归一化(dangerouslySetInnerHTML), oNload/HREF/xlink:HREF 这类混合大小写属性此前会漏过清洗。比较前先 toLowerCase(),按原名删除;新增混合大小写事件/链接属性回归用例。 * fix(mermaid): sanitizer 元素黑名单大小写归一,堵住混合大小写元素绕过(CR P1) XML 元素名保留大小写,querySelectorAll('script') 等选择器匹配不到 ">x', + 'u', + '', + ].join('') + const out = sanitizeSvg(svg) + expect(out).not.toContain('oNload') + expect(out).not.toContain('OnClick') + expect(out).not.toContain('@Click') + expect(out).not.toContain('HREF') + expect(out).not.toContain('xlink:HREF') + expect(out).not.toContain('javascript:') + }) + + it('removes href and xlink:href regardless of scheme', () => { + const svg = [ + '', + 'x', + 'y', + '', + ].join('') + const out = sanitizeSvg(svg) + expect(out).not.toContain('href') + expect(out).not.toContain('javascript:') + expect(out).not.toContain('data:text/html') + }) + + it('removes script elements', () => { + const svg = 't' + const out = sanitizeSvg(svg) + expect(out).not.toContain('script') + expect(out).toContain('t') + }) + + it('strips mixed-case element names (HTML parsing normalizes tag casing)', () => { + const svg = [ + '', + '', + '
', + '
', + '