鸿蒙 PC Markdown 编辑器离线专业渲染管线

📅 2026/7/21 22:24:34
鸿蒙 PC Markdown 编辑器离线专业渲染管线
鸿蒙 PC Markdown 编辑器离线专业渲染管线数学公式、流程图和代码高亮经常被归为“Markdown 预览插件”但在桌面编辑器里它们实际上共同构成了一条不可信内容处理管线。输入来自用户文档解析器和渲染器来自不同生态输出最终进入具备 DOM 能力的 ArkWeb。只要其中一个环节把“文档内容”误当成“应用配置”或“可信 HTML”离线编辑器也可能出现脚本注入、界面阻断、内存失控和异步结果串文档等问题。本文讨论一套面向鸿蒙 PC 的工程实现使用 markdown-it 识别扩展语法使用 KaTeX 生成公式使用 Mermaid 生成图表使用 Highlight.js 处理代码着色再以分层净化、资源上限和代际取消把它们收敛为可验证的预览能力。文章中的代码来自 OhMarkdown 的真实实现仓库地址为 https://gitcode.com/VON-/codex_md_oh对应功能提交为f133bbe。本文只讨论已经落地并验证的应用内预览不把尚待开发的专业 HTML、PDF 和图片导出描述为现成功能。专业渲染不是三个互不相关的插件公式、图表和代码块表面上是三种视觉组件输入与风险却完全不同。公式输入是一段 TeX 风格表达式渲染结果含普通 HTML、MathML、大量类名和受控的内联几何样式Mermaid 输入近似声明式程序渲染器会解析图类型、计算布局并生成 SVG代码高亮输入本应是纯文本输出只需要一层带类名的span。如果对三类结果使用同一个宽松 HTML 白名单就会把最复杂输出需要的权限错误地授予最简单输出。因此实现没有设计一个“任意插件返回 HTML”的通用接口而是定义三个固定类型math、mermaid和code。每个类型有独立的源码长度上限、输出净化策略、错误表现和降级路径。这个选择看似保守却直接减少了权限交叉代码高亮不能借用 SVG 能力Mermaid 不能借用普通 HTML 的表单或外部对象能力公式也不能通过 KaTeX 的可信扩展插入任意标签。这条管线的顺序是Markdown 解析阶段只产生带源码行号的安全占位节点基础 HTML 先经过一次 DOMPurify占位节点进入对应本地渲染器渲染结果再按类型净化最后才替换当前节点。任何一步失败都只替换当前占位不清空整个预览区域也不回写编辑器文档。先明确语法契约OhMarkdown 接受$...$行内公式、$$...$$块公式、语言名为mermaid的围栏代码块以及其他带语言名的代码围栏。公式规则不是对 Markdown 文本做全局正则替换而是安装到 markdown-it 的行内和块级规则链。这样可以让 Markdown 自己处理转义、代码围栏和块边界避免公式识别穿过不应该进入的区域。下面是实际的规则安装入口。它在 GFM 任务列表插件之后执行最终覆盖围栏渲染函数把 Mermaid 与普通代码块转换成不同占位结构constmarkdownRenderernewMarkdownIt({html:false,linkify:true,typographer:false,breaks:false});markdownRenderer.use(taskLists,{enabled:false,label:true,labelAfter:true});installProfessionalMarkdownRules(markdownRenderer);constprofessionalPreviewEnhancernewProfessionalPreviewEnhancer();语法契约需要克制。没有实现任意 TeX 宏配置没有允许 Markdown 覆盖 Mermaid 全局配置也没有自动猜测未知代码语言。未知语言仍然是合法代码块只是保持纯文本显示。这样做保证源码可迁移文件继续是标准围栏与常见数学扩展不需要写入 OhMarkdown 私有节点、缓存 ID 或序列化后的 SVG。行内公式为什么不能只靠一个正则最简单的公式实现通常是把/$([^$])$/一类表达式套到整篇文档上。它会迅速遇到货币符号、转义美元符、连续美元符、代码区块和跨行内容。真实实现使用游标查找结尾并对候选字符做转义与空白判断。开头后面不能是空白结尾前面也不能是空白$$不会误入行内规则奇数个反斜线表示美元符已经转义。functionfindInlineMathEnd(source:string,from:number):number{letcursorfrom;while(cursorsource.length){constcandidatesource.indexOf($,cursor);if(candidate0){return-1;}if(!isEscaped(source,candidate)source[candidate-1]!$source[candidate1]!$!/\s/.test(source[candidate-1]??)){returncandidate;}cursorcandidate1;}return-1;}规则只把公式原文放进code classprofessional-render-source并记录data-source-line。此时公式仍然是转义后的文本不是 KaTeX 输出。基础 Markdown HTML 经过 DOMPurify 后增强器才读取textContent。这个顺序避免了把公式字符串拼回 HTML 属性也让错误公式仍有确定的来源位置。行内 token 本身没有完整块级行号所以实现还记录 token 在段落内容中的偏移再统计偏移前的换行数。这样多行段落中的行内公式不会全部错误地指向段落第一行。定位并不追求 TeX 子表达式的列号它承诺的是用户点击错误后能回到包含该公式的 Markdown 行这一粒度对桌面修复流程足够稳定。块公式要尊重 Markdown 的行边界块公式支持同一行$$...$$也支持起始和结束标记分别占行。解析器逐行寻找只在行尾留下空白的关闭标记没有找到关闭标记时返回false让后续 Markdown 规则继续处理而不是吞掉文档剩余部分。这一点直接决定错误隔离是否可信一个忘记闭合的块公式不应该把后面几十页都变成公式源码。块 token 的map保存开始和结束行渲染占位时取开始行作为错误位置。内容在进入 KaTeX 前只做首尾空白整理不改写内部换行、反斜线或宏文本。文档模式切换不会重新序列化 token保存和恢复仍然只面对 CodeMirror 中的原始 Markdown。KaTeX 的可信边界KaTeX 提供的trust选项决定某些可能产生链接、HTML 或外部资源的命令是否可信。桌面编辑器打开的文档不等于可信配置因此实现固定trust: false同时启用抛错和资源上限constrenderedkatex.renderToString(source,{displayMode:element.dataset.displayModeblock,output:htmlAndMathml,throwOnError:true,trust:false,maxExpand:1000,maxSize:20,strict:(errorCode):error|ignoreerrorCodehtmlExtension?error:ignore});output: htmlAndMathml同时服务视觉显示和辅助技术。HTML 部分负责稳定排版MathML 为能够理解数学语义的工具提供结构信息。throwOnError: true不意味着整篇预览抛出异常而是把错误交给当前公式节点的try/catch由应用生成本地化错误按钮。maxExpand防止宏展开失控maxSize限制异常尺寸命令单公式 20,000 字符上限在进入引擎前进一步截断攻击面。KaTeX 输出仍然不能因为来自成熟库就跳过净化。实现允许公式必需的 HTML、MathML 与 SVG profile但显式禁止script、style、iframe、object、embed和form。KaTeX 用于几何布局的元素级style属性由白名单保留因为trust: false和 HTML 扩展拒绝已经限制输入能力整个style标签仍被禁止。这里的重点是区分“库为了排版生成的受控样式属性”和“文档作者注入的活动标签”。Mermaid 必须把文档当作程序输入Mermaid 比普通 Markdown 扩展更接近一个小型语言运行时。它不仅解析文本还会根据图类型加载实现、计算布局、生成标识符和 SVG。安全配置不能由文档决定否则作者可以使用初始化指令把应用的strict改为更宽松模式。因此实现不只是设置默认值还在解析前拒绝任何%%{...}配置指令。mermaid.initialize({startOnLoad:false,securityLevel:strict,suppressErrorRendering:true,maxTextSize:MAX_MERMAID_CHARACTERS,maxEdges:500,htmlLabels:false,theme:options.themedark?dark:neutral,fontFamily:HarmonyOS Sans, Noto Sans SC, sans-serif,secure:[secure,securityLevel,startOnLoad,maxTextSize,maxEdges,suppressErrorRendering,theme,themeCSS,themeVariables,htmlLabels,fontFamily]});startOnLoad: false避免 Mermaid 自己扫描全页应用只处理已经由 markdown-it 标记的节点。securityLevel: strict禁止点击回调等宽松能力suppressErrorRendering: true防止引擎把错误 SVG 写到页面其他位置。主题与字体由应用传入文档不能重定义。每张图先parse成功后才render单图失败只把当前figure替换为错误按钮。图表数量限制为 24单图文本限制为 50,000 字符边数限制为 500。这些数字不是性能承诺而是预览保护线。达到上限时已经完成的正文和前序图表继续显示超出部分明确报错。真正的大规模图表性能仍需要在 Release 真机上测量不能用开发机一次成功就取消保护。SVG 生成后仍要二次净化严格模式是必要条件不是最终输出白名单。Mermaid 及其依赖会随版本更新输出结构也可能变化。应用在拿到 SVG 字符串后再次执行 DOMPurifySVG profile 只保留绘图需要的元素显式移除活动或可跳转节点functionsanitizeMermaidSvg(svg:string):string{returnString(DOMPurify.sanitize(svg,{USE_PROFILES:{svg:true,svgFilters:true},FORBID_TAGS:[script,foreignObject,iframe,object,embed,a],FORBID_ATTR:[href,xlink:href]}));}为什么连a也移除因为预览中的普通 Markdown 链接已经有受限的本地导航协议会交给 ArkTS 重新解析工作区边界。允许 Mermaid SVG 自带链接会产生第二条难以统一审计的导航通道。图表阅读是 G3-07 的目标图内可执行点击不是目标删除它比增加新的 Bridge 特例更可靠。净化后还会检查结果是否真的包含svg。如果白名单把异常输出清空就显示可定位错误不把空白区域伪装成成功。成功节点增加roleimg和本地化aria-label让图表至少具备来源行和类型语义更细的节点级无障碍描述仍属于后续可用性专项。代码高亮的最小权限原则代码高亮不需要 HTML、MathML 或 SVG。Highlight.js 返回的内容只需要span和class所以净化白名单可以非常窄functionsanitizeHighlightedCode(html:string):string{returnString(DOMPurify.sanitize(html,{ALLOWED_TAGS:[span],ALLOWED_ATTR:[class]}));}引擎通过import(highlight.js/lib/common)延迟加载常用语言集合。只有预览里出现普通代码块时才执行加载没有代码的文档不会运行语言注册逻辑。语言名先经过长度和字符集限制再调用getLanguage。已知语言使用显式highlight(source, { language, ignoreIllegals: true })未知语言不做自动探测原样显示为安全文本。不自动探测是桌面编辑器中的重要取舍。探测会让大代码块在多种语法间反复评分也可能把普通日志错误标成某种语言。Markdown 围栏已经提供了作者声明应用尊重声明没有声明就保持纯文本。单代码块超过 200,000 字符时同样回退不阻止正文和编辑操作。测试语料故意把字符串scriptalert(1)/script放在 TypeScript 代码中。最终预览可以看到完整字符串但 DOM 中没有script节点。这个断言比肉眼看到尖括号更可靠因为安全目标是“文本仍在活动节点不存在”。异步渲染最容易出现串文档KaTeX、Mermaid 和 Highlight.js 都以动态导入进入增强器。用户可能在依赖加载或图布局期间切换标签、打开新文档、修改源码、改变主题或切回源码模式。如果旧 Promise 完成后仍写 DOM就会出现很危险的错觉标签标题是文档 B预览内容却来自文档 A。实现使用单调递增的generation作为渲染代际。每次增强或取消都会产生新代际异步步骤在写入前同时检查代际和节点是否仍连接在当前 DOMexportclassProfessionalPreviewEnhancer{privategeneration0;cancel():void{this.generation1;}enhance(root:HTMLElement,options:ProfessionalRenderOptions):void{constgenerationthis.generation1;this.generationgeneration;voidPromise.allSettled([this.enhanceMath(root,generation,options),this.enhanceCode(root,generation,options),this.enhanceMermaid(root,generation,options)]);}privateisActive(element:HTMLElement,generation:number):boolean{returnthis.generationgenerationelement.isConnected;}}代际检查既发生在模块加载后也发生在 Mermaid 的parse和render之后。Promise.allSettled让三类增强互不阻断但真正的错误仍由各类型转换为局部 UI。Playwright 回归会先触发 Mermaid 渲染再立即切到一份只有标题的新文档等待旧任务可能完成后确认页面仍是新标题且不存在旧图表节点。错误界面必须能进入修复流程只显示“渲染失败”会让用户在长文档里继续寻找问题。占位节点记录源行错误按钮标题显示“公式错误 · 第 N 行”或“图表错误 · 第 N 行”详情截断到 240 字符并以纯文本写入。点击按钮调用统一的navigateToSourceLine切换源码模式限制行号到当前文档合法范围再把 CodeMirror 选区移动到该行。错误详情不直接拼入innerHTML。即使第三方解析器把用户输入包含在错误消息中textContent也会保证它只是文本。按钮具备键盘焦点、悬停状态和足够高度错误区使用独立的红色语义变量深色模式下使用另一组对比色而不是简单反色。代码高亮失败选择不同降级代码原文仍在节点记录error状态和标题不用错误按钮替换整段代码。原因是代码本身就是用户需要阅读的内容高亮只是增强公式和图表的源码通常不等同于可读结果因此错误按钮更合适。主题切换为什么需要重新渲染代码高亮主要依赖 CSS 变量主题切换后可以直接换色KaTeX 大多继承文字颜色Mermaid 却会把主题颜色写进生成 SVG。只修改根节点data-theme会留下浅色图表嵌在深色预览中的不一致。因此setTheme在预览或分栏模式重新执行renderPreview在源码模式则只标记预览为脏并取消旧任务等用户真正打开预览时再渲染。语言切换也采用相同策略因为错误标题和图表辅助名称必须使用当前界面语言。重绘不会重建 CodeMirror 编辑状态不改变撤销历史、脏标记或保存基线。主题和语言属于视图状态Markdown 文档仍然只有一个事实来源。鸿蒙 PC 窗口中的布局约束桌面窗口可以自由缩放公式、SVG 和长代码行不能把整个工作区撑宽。块公式、图表和代码块都设置max-width: 100%与局部横向滚动。Mermaid 容器有稳定边框和背景SVG 使用width: auto; max-width: 100%; height: auto短图居中复杂图只在自己的容器滚动。错误按钮使用grid-template-columns: minmax(0, 1fr)详情允许断词避免解析器返回长标识符后撑破侧栏。代码块保留overflow-x: auto不把源代码强制折行成难以复制的形式。页面在 1440 x 900 最终产物测量中scrollWidth与clientWidth都是 1440没有全页横向溢出。下图来自最终 Debug HAP 在 HarmonyOS MateBook Pro 2in1 模拟器中的真实运行界面。文档同时包含公式、Mermaid 流程图和 TypeScript 代码三类结果在应用内部同屏显示截图分辨率为 3120 x 2080SHA-256 为8ee32d7a0e2235837118dd5ee3532160e9b1c48471d09fae667de2b98fe7b519。它记录的是 ArkUI 工作台承载 ArkWeb 的最终应用不是单独浏览器页面或设计稿。CSP 与离线资源必须同时成立“运行时不请求 CDN”和“产物没有外链”是两个不同检查。最终editor/index.html是一个本地单文件脚本、样式和 KaTeX 字体全部内联自动化断言不存在script src和外部样式link。页面 CSP 继续使用connect-src none即使某个新依赖未来尝试建立网络连接也会先被页面策略阻断。应用没有申请互联网权限ArkWeb Bridge 也没有因为专业渲染新增方法。公式、图表和高亮完全在 Web 侧处理不需要把文档交给原生服务更不需要上传远端。离线不是宣传用语而是可以从依赖打包、CSP、权限清单和模拟器断网路径分别审计的属性。开发服务器会使用 WebSocket 热更新并从本地地址加载字体因此严格生产 CSP 不适合用开发服务器控制台来判断最终资源状态。测试同时覆盖开发交互和最终单 HTML安全与性能测量直接打开构建产物得到控制台错误 0Playwright 功能回归仍由本地测试服务运行。把两类环境分开可以避免将开发工具请求误认为产品网络依赖。包体增长需要公开记录Mermaid 支持多种图类型完整本地运行时明显增大包体。G3-06 的 Debug HAP 约为 1.61 MiB加入专业渲染后最终 Debug HAP 为 6,638,278 字节。单 HTML 为 5,842,276 字节系统 gzip 后为 2,212,681 字节。这个增长不能被“离线能力”四个字掩盖它会影响安装包、冷启动解析和内存峰值。依赖审查发现 Mermaid 自身依赖 KaTeX。如果应用直接使用不同的大版本就会在node_modules和打包图里留下两套公式引擎。最终将直接依赖统一为 KaTeX0.16.47与 Mermaid 的兼容范围合并移除一份重复包。与去重前相比原始单 HTML 减少约 266 KiBgzip 减少约 78 KiB。Highlight.js 使用lib/common而不是全语言全集控制常用语言集合。当前没有为了继续减包而自行裁剪 Mermaid 内部图类型注册表因为这会进入更难维护的私有组合路径并可能让“支持 Mermaid”的语义变得含糊。更进一步的按图类型拆分需要独立兼容性语料和 Release 测量在没有证据前不把复杂构建技巧混进当前纵切。测试必须覆盖正确结果和失败形态专业渲染的测试不能只截一张成功页面。自动化包含四组关键回归正确与错误公式同文档确认两个公式成功、一个公式失败且后续正文存在正确 Mermaid 与配置注入图同文档确认一张 SVG 成功、危险指令被拒绝已知与未知代码语言同文档确认 TypeScript 有语义类名、未知语言保持纯文本快速切换文档确认旧 Promise 不会写回。Mermaid 安全断言直接查询最终 DOM 中script、foreignObject和a的数量而不是只查字符串。代码注入断言确认尖括号文本仍然可见且脚本节点为 0。生产包测试检查外链标签与 CSP。全量 Playwright 最终为38/38包含恢复、图片、搜索、链接、多标签、换行和导出基础回归避免专业预览破坏已有编辑闭环。鸿蒙原生 ohosTest 最终为8/8Failure 和 Error 均为 0。它不测试 Web 渲染细节而是确认 Web 包增大后字节保真、恢复、图片落盘、TaskPool 搜索、链接解析和大纲服务仍可运行。不同测试层有不同责任不能因为模拟器截图成功就省略浏览器 DOM 安全断言也不能因为浏览器通过就声称 HAP 已验证。一次小语料性能测量能说明什么在 1440 x 900 的无头 Chromium 中直接打开最终离线单 HTML输入一条公式、一张流程图和一个 TypeScript 代码块。三类节点首次全部进入ready用时 83 ms依赖已加载后的深色主题重绘用时 19 ms控制台错误为 0。这个结果证明当前小语料没有明显阻断也为后续回归提供同一测量口径。它不能证明复杂文档的 P95不能替代鸿蒙 PC 真机也不能直接与其他编辑器比较。Mermaid 图布局成本与节点、边和图类型有关数百公式还涉及 DOM 数量和字体排版代码高亮成本与单块长度相关。项目把 24 图和各类字符上限作为防护同时把 Release 真机冷启动、内存峰值和复杂图压力留到 Beta 质量评审统一执行。这种诚实边界本身是产品工程的一部分。性能数据只有设备、构建模式、语料和计时起止都明确时才可复用。开发机 83 ms 是本轮回归证据不是“比竞品快多少”的营销结论。与导出管线的边界应用内预览采用异步 DOM 增强而现有基础 HTML 导出直接调用同步 Markdown 净化函数打印准备也不会等待 Mermaid 布局。因而 G3-07 完成不代表带公式和图表的导出已经与预览一致。把当前预览 DOM 粗暴复制进导出同样不够需要处理 KaTeX 字体、自包含 SVG、主题样式、图片资源、打印就绪信号和输出后的安全净化。下一阶段应建立可等待的渲染完成协议让 HTML、PDF 和图片输出消费同一份经过安全收敛的专业结果同时保持导出 CSP 和资源内联。失败图表在导出中是显示错误占位、保留源码还是阻断操作也需要明确产品策略。本文刻意保留这个边界避免用一张成功预览截图冒充输出闭环。为什么不做通用插件系统一个通用 Markdown 插件 API 看起来可以统一公式、图表和未来扩展但它同时需要定义插件权限、生命周期、异步取消、输出净化、资源访问、版本兼容和故障隔离。当前只有三个已确认类型且它们的权限差异很大。现在创建插件体系会把安全边界从三个可审查分支扩大成任意第三方代码入口。独立ProfessionalPreviewEnhancer是模块级抽取不是插件平台。它解决真实存在的复杂度主文件不再同时容纳三个引擎的动态导入、安全配置和错误处理单一调用方仍然清晰测试可以通过稳定的data-professional-kind与data-render-state观察结果。没有数据库、事件总线或新的跨模块状态框架仍然符合既定 Level 2 与 D2 边界。对鸿蒙 PC 产品力的实际价值专业 Markdown 文档经常把论证、架构和实现放在同一页公式表达模型流程图表达关系代码块表达可执行细节。如果用户必须在浏览器插件、在线图表服务和多个窗口之间切换桌面编辑器的离线与专注价值就被削弱。OhMarkdown 现在可以在鸿蒙 PC 应用内部直接阅读这三类内容同时保留标准 Markdown 源码。真正的优势不是“支持列表里多了三个勾”而是失败成本更低。坏公式不会让正文空白坏图表不会影响前后段落未知语言不会丢代码切换标签不会串内容文档无法把安全级别改成宽松模式应用不需要网络权限。错误还能回到源行用户可以立刻修复而不是打开开发者工具寻找堆栈。当前竞争优势记分卡把这项能力记为 3 分而不是 4 分。原因很具体已有代码、自动化、最终离线产物和 HarmonyOS 2in1 模拟器证据但还缺鸿蒙 PC 真机 Release 压力、统一竞品语料和专业导出闭环。产品要做大优势必须建立在可复测证据上而不是在阶段尚未完成时提前使用绝对表述。结语鸿蒙 PC Markdown 编辑器的专业渲染本质上是一条受限的编译与展示管线。可靠实现需要同时解决语法边界、可信配置、输出净化、资源上限、异步竞争、错误定位、主题重绘、离线打包和设备布局。KaTeX、Mermaid 与 Highlight.js 提供成熟领域能力应用负责把它们放进可审计的产品边界。提交f133bbe已经完成应用内预览这一闭环最终单 HTML 完全本地公式输出兼顾视觉与 MathMLMermaid 使用严格安全级别并二次净化代码高亮按需加载且未知语言安全回退错误局部可定位迟到结果不能覆盖新文档。MateBook Pro 2in1 模拟器中的真实 HAP 已同屏显示三类内容Playwright38/38与 ohosTest8/8通过。下一步不是继续堆更多语法而是把同一份安全渲染结果带入自包含 HTML、系统 PDF、图片和系统分享并用真机 Release 数据审视包体、启动和复杂文档成本。只有输出一致性和 Beta 质量证据完成后这条专业渲染管线才会从“可靠预览能力”进一步变成完整的鸿蒙 PC 内容交付能力。