鸿蒙 PC Markdown 编辑器 GFM 渲染:表格、删除线、自动链接与任务列表

发布时间:2026/7/26 4:48:19
鸿蒙 PC Markdown 编辑器 GFM 渲染:表格、删除线、自动链接与任务列表 鸿蒙 PC Markdown 编辑器 GFM 渲染表格、删除线、自动链接与任务列表Markdown 预览并不是把井号替换成标题标签。桌面用户从 GitHub、GitCode 和团队文档仓库带来的文件通常包含表格、删除线、裸 URL 和任务列表如果编辑器只支持最小语法源码可以打开预览却会丢失信息。反过来如果为了兼容而允许任意 HTML嵌入式 ArkWeb 又会扩大脚本和导航风险。本文基于鸿蒙 PC Markdown 编辑器 OhMarkdown分析如何用 markdown-it 建立 GFM 常用语法基线用固定版本任务列表插件补齐复选框再通过 DOMPurify 和受限链接行为保持本地预览安全。完整代码位于 https://gitcode.com/VON-/codex_md_oh本文对应提交3a9146e。先明确 GFM 范围“支持 GFM”容易成为模糊宣传。GitHub Flavored Markdown 包含一组规范扩展具体解析库又可能默认打开部分能力。OhMarkdown 当前把验收范围写成四个可测试点管道表格生成table、thead、tbody和单元格结构。~~内容~~生成删除线。裸https://URL 自动成为链接。- [ ]与- [x]生成只读任务复选框并保留勾选状态。这四项覆盖日常项目说明、技术方案和任务记录但不等于支持所有 GitHub 页面特性。脚注、警告块、数学公式、Mermaid、仓库相对链接解析和语法高亮都需要独立设计。把范围拆成 DOM 结果比一句“兼容 GFM”更容易持续回归。markdown-it 的基础配置渲染器在 Web 内核启动时创建一次constmarkdownRenderernewMarkdownIt({html:false,linkify:true,typographer:false,breaks:false});markdownRenderer.use(taskLists,{enabled:false,label:true,labelAfter:true});html: false表示源码中的原生 HTML 不参与渲染。用户输入script、iframe或div style...时不会直接成为活动 DOM。这是第一道安全边界也让 Markdown 文件在不同平台上的表现更可预测。需要支持安全 HTML 子集时应单独定义标签和属性白名单而不是把开关改为 true 后完全依赖浏览器。linkify: true打开裸链接识别因此正文里的https://example.com不必写成[链接](...)。typographer: false避免渲染器自动替换引号、破折号和符号技术文档中的字符应尽量忠于源码。breaks: false保持标准段落换行语义单个源码换行不会无条件生成br。markdown-it 默认已经支持表格和删除线规则因此不需要再装两个插件。任务列表不是核心规则由markdown-it-task-lists增加。依赖在package.json中固定为2.1.1{dependencies:{markdown-it:^14.3.0,markdown-it-task-lists:2.1.1}}任务列表插件固定精确版本是为了降低构建结果随补丁发布变化的风险。解析内核自身目前允许兼容范围升级锁文件仍固定实际安装版本。涉及输出 DOM 的依赖升级必须重新跑安全与结构测试不能只看 TypeScript 编译。为什么任务复选框默认只读插件配置enabled: false会生成禁用复选框。预览区的职责是阅读渲染结果不直接修改源码。若允许用户点击预览中的任务项应用必须把 DOM 节点反向映射到源码偏移修改[ ]为[x]处理重复条目、嵌套列表和编辑期间偏移变化还要把变更放进 CodeMirror 撤销历史。在没有完整双向映射前让复选框可点击会制造假交互界面看似勾选源码和保存文件却没变化。只读复选框诚实表达当前能力同时保留勾选视觉状态。label: true和labelAfter: true让插件生成可关联标签结构文字位于复选框之后。即使复选框禁用语义结构仍有利于可访问性和样式。未来开放交互时也不必重新修改输出形态。表格需要结构与样式共同完成解析器把如下源码转换为表格| 名称 | 状态 | | --- | --- | | 鸿蒙 PC | 完成 |仅生成 HTML 还不够。浏览器默认表格没有清晰边框长内容也可能撑破预览列。当前样式建立紧凑的桌面阅读结构#preview table{border-collapse:collapse;}#preview th, #preview td{padding:7px 12px;border:1px solid #dce1e4;}深色主题覆盖边框:root[data-themedark] #preview th, :root[data-themedark] #preview td{border-color:#465057;}后续还需补充宽表格的横向滚动策略。当前table没有外层滚动容器超长单元格可能挤压布局。可靠方案通常是在渲染后给表格包裹容器或设置display: block; overflow-x: auto但这会影响表格布局算法。应在真实宽表、中文长词、代码字段和窄窗口上验证后再选。表格对齐标记、空单元格、转义管道和行内代码里的管道也是需要版本化语料覆盖的边界。只测三列表格能证明规则启动不能证明复杂文档完全正确。删除线与自动链接的语义删除线输入~~旧内容~~预期生成s旧内容/s。这项功能实现简单仍需要测试因为解析器选项或版本升级可能关闭相关规则。删除线在变更记录、废弃方案和任务说明中很常见若渲染成两个波浪号会明显降低文档可读性。裸 URL 由linkify转换。预览渲染后应用会统一处理所有链接preview.querySelectorAllHTMLAnchorElement(a).forEach((link){link.target_blank;link.relnoopener noreferrer;link.addEventListener(click,(event)event.preventDefault());});设置_blank和noopener noreferrer是标准的外部链接隔离但当前还会阻止默认点击因此预览不会直接从本地编辑器导航到外部页面。代码保留链接视觉与 DOM 语义用户能够识别 URL后续可由原生层接管点击并经过协议白名单确认后调用系统浏览器。如果只依赖target_blankArkWeb 可能创建新窗口或离开应用上下文如果完全删除href导出的 HTML 又失去链接。当前渲染链把“生成安全链接结构”和“应用内是否允许导航”分开处理。所有扩展输出仍要经过净化markdown-it 配置html: false已经阻止源码原生 HTML但插件和链接规则仍会产生 HTML。应用将渲染结果统一交给 DOMPurifyfunctionsanitizeMarkdown(content:string):string{constunsafeHtmlmarkdownRenderer.render(content);returnDOMPurify.sanitize(unsafeHtml,{USE_PROFILES:{html:true},FORBID_TAGS:[style,iframe,object,embed,form],FORBID_ATTR:[style]});}安全净化必须位于所有渲染规则之后。如果先净化 Markdown 源码再让插件生成 HTML插件输出绕过了最终白名单。当前流程固定为 Markdown 到 HTML、HTML 净化、写入 DOM。禁止内联 style 能防止文档覆盖编辑器 UI、制造不可见链接或使用 CSS 读取行为。禁止 iframe、object、embed 和 form 缩小嵌入内容与提交能力。DOMPurify 还会处理危险协议属性导出测试明确断言不存在hrefjavascript:...。任务列表插件需要的input和label在 HTML profile 中保留但复选框禁用。每次新增 Markdown 插件都要检查它输出哪些标签和属性确认净化后功能仍在、安全边界没有被放宽。插件兼容不是“页面看起来有内容”还要验证净化前后 DOM。预览更新采用脏标记编辑器有源码、分栏和预览三种模式。源码模式下不需要每次按键都渲染隐藏预览只把previewDirty设为 true。进入分栏或预览时再生成functionsetMode(mode:ViewMode):void{if(largeDocumentModemode!source){return;}currentModemode;workspace.dataset.modecurrentMode;if(mode!sourcepreviewDirty){renderPreview(editor.state.sliceDoc());}if(mode!preview){window.requestAnimationFrame(()editor.focus());}}分栏和预览可见时编辑变更会刷新渲染纯源码时延迟。这个策略减少后台 DOM 构建特别适合用户长时间专注源码输入。五兆字符以上文档直接进入大文档保护只允许源码模式避免 markdown-it 和 DOMPurify 对超大文本建立庞大 DOM。renderPreview每次替换整个innerHTMLfunctionrenderPreview(content:string):void{preview.innerHTMLsanitizeMarkdown(content);// 重新约束链接previewDirtyfalse;}全量渲染实现简单输出确定但长文档频繁输入可能产生性能压力和滚动位置变化。后续可以做节流、按块 diff 或增量 token 渲染不过任何优化都必须保留净化边界不能把未经净化的局部片段直接插入 DOM。GFM 样式也要适配深色与窄窗口任务列表去掉普通列表圆点复选框使用固定尺寸和强调色#preview .contains-task-list{padding-left:0;list-style:none;}#preview .task-list-item{list-style:none;}#preview .task-list-item-checkbox{width:15px;height:15px;margin:0 8px 0 0;vertical-align:-2px;accent-color:#087a63;}固定尺寸防止浏览器默认控件在不同系统缩放下挤压行高vertical-align让复选框与文本基线协调。真正的鸿蒙 PC 适配还要测试系统字体放大和高 DPI固定十五像素是否足够可点并不重要因为当前禁用但视觉可辨识度仍重要。分栏在宽窗口使用两列窄于 760 像素后变成上下两行。GFM 表格和代码块需要在两种布局中都不把容器撑破。预览图片使用max-width: 100%代码块使用overflow: auto。表格的窄窗口策略仍是后续重点。鸿蒙 PC 模拟器中的预览链路下图来自 MateBook Pro 2in1 模拟器。源码编辑区与经过 markdown-it、DOMPurify 处理的预览同屏标题层级、边框、排版和同步开关均在真实 ArkWeb 容器中运行。这张应用截图证明的是完整预览链已经进入鸿蒙 PC 工作台而 GFM 四项由自动化 DOM 断言覆盖。正式发布前还应在模拟器准备包含表格、删除线、裸链接和任务列表的专用文档补充一张四项同屏截图当前文章不把普通标题截图伪装成 GFM 四项视觉证据。自动化测试直接检查 DOMPlaywright 用例一次构造四类语法constsource| 名称 | 状态 |\n| --- | --- |\n| 鸿蒙 PC | 完成 |\n\n~~旧内容~~\n\nhttps://example.com\n\n- [ ] 待办\n- [x] 完成;host.OhMarkdownEditor.setDocument(content);host.OhMarkdownEditor.setMode(preview);断言不是截图比对而是结构与属性awaitexpect(page.locator(#preview table)).toHaveCount(1);awaitexpect(page.locator(#preview s)).toHaveText(旧内容);awaitexpect(page.locator(#preview a)).toHaveAttribute(href,https://example.com);awaitexpect(page.locator(#preview .task-list-item-checkbox)).toHaveCount(2);awaitexpect(page.locator(#preview .task-list-item-checkbox).first()).toBeDisabled();awaitexpect(page.locator(#preview .task-list-item-checkbox).nth(1)).toBeChecked();表格存在证明规则启用删除线文本证明 token 正确链接 href 证明 linkify 结果两个 checkbox 的 disabled 与 checked 同时验证只读语义和勾选状态。比起只断言预览包含文字这些条件更接近功能契约。安全测试另外输入script和javascript:链接确认脚本没有进入 DOM、全局变量没有执行、导出 HTML 不含危险协议。功能测试与安全测试必须同时存在因为新增插件可能让一种 GFM 语法正确却改变净化输出。版本化语料的作用项目在test-fixtures/markdown/gfm-baseline.md保存稳定语料。测试代码内的最小字符串适合快速断言版本化文件适合人工预览、模拟器截图和未来差异比较。两者职责不同。语料应逐步增加对齐表格、空任务、嵌套任务、删除线跨行边界、带括号 URL、中文域名、转义波浪号、代码块中的任务标记、表格单元格内链接。每次修复解析差异时先加入最小复现再升级依赖避免“新版本看起来更强”却破坏旧文档。GFM 与 CommonMark 基线要分开。普通段落、标题、列表、引用和代码围栏属于基础语义表格和任务列表属于扩展。测试失败时能快速判断是核心解析退化还是插件变化。导出与预览必须共享渲染语义HTML 导出调用同一个sanitizeMarkdownfunctionexportHtml(title:string):string{constsafeTitleescapeHtmlText(title.trim()||OhMarkdown document);constbodysanitizeMarkdown(editor.state.sliceDoc());return!doctype html...${body}.../html;}因此表格、删除线、自动链接和任务列表在应用预览与导出 HTML 中使用同一解析器和净化策略。若导出另建一套 Markdown 库用户会遇到“应用里正确、导出后不同”的问题。打印 PDF也先准备同一预览 DOM再交给系统打印适配器。共享语义不代表样式完全相同。导出 HTML内嵌独立 CSS不依赖应用资源表格有边框但任务列表样式目前需要检查是否完整进入导出样式。任何新增 GFM 展示规则都要同步考虑应用 CSS、导出 CSS 和打印媒体。当前边界OhMarkdown 当前没有语法高亮代码块没有 Mermaid、数学公式或脚注插件没有允许预览直接勾选任务也不会自动打开外部链接。相对图片和链接的工作区基准 URI 仍需进一步完善。大文档模式禁用预览和导出避免不受控内存占用。这些边界应当公开而不是通过不断安装插件掩盖。每个插件都会增加包体、供应链、DOM 输出和安全审查成本。产品要优于现有编辑器不是插件数量最多而是在承诺语法上渲染一致、离线可用、导出一致、升级可回归。结语GFM 渲染是一条完整管线markdown-it 提供基础表格、删除线和链接识别固定版本插件生成只读任务列表DOMPurify 对所有输出做最终净化CSS 在亮暗主题和分栏布局中建立可读样式Playwright 用结构断言锁定行为导出与打印复用相同语义。当这条管线的范围、风险和测试都明确后用户打开来自 GitCode 的 Markdown 文件才能相信表格不会散、任务状态不会丢、链接不会劫持应用、导出不会换一种语法。对鸿蒙 PC 编辑器而言这比单纯展示一块 HTML 预览更接近真正的文档兼容能力。