鸿蒙 PC Markdown 编辑器导出架构安全 HTML 与系统 PDF本文讨论如何让预览、独立 HTML 和打印页面共享安全内容源并把 PDF 生成交给鸿蒙系统打印能力。完整示例代码https://gitcode.com/VON-/codex_md_oh。导出不能绕过预览安全策略Markdown 编辑器的导出功能很容易形成第二条 HTML 渲染链路。如果预览使用 DOMPurify而导出直接拼接 Markdown 渲染结果同一份恶意文档就可能在预览中安全、导出后却执行脚本。OhMarkdown 让预览和导出共用同一个净化函数functionsanitizeMarkdown(content:string):string{constunsafeHtmlmarkdownRenderer.render(content);returnDOMPurify.sanitize(unsafeHtml,{USE_PROFILES:{html:true},FORBID_TAGS:[style,iframe,object,embed,form],FORBID_ATTR:[style]});}functionrenderPreview(content:string):void{preview.innerHTMLsanitizeMarkdown(content);}代码来源web-editor/src/main.ts共享净化函数的意义不只是减少重复代码更重要的是让预览与导出的安全规则只有一个事实来源。生成可离线阅读的独立 HTML导出的 HTML 不引用在线脚本、样式表或字体。标题先经过字符转义正文使用净化后的 HTML页面内置严格 CSPfunctionexportHtml(title:string):string{constsafeTitleescapeHtmlText(title.trim()||OhMarkdown document);constbodysanitizeMarkdown(editor.state.sliceDoc());return!doctype html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 meta http-equivContent-Security-Policy contentdefault-src none; style-src unsafe-inline; img-src data: file:; font-src data:; base-uri none; form-action none title${safeTitle}/title style${EXPORT_STYLES}/style /head body${body}/body /html;}代码来源web-editor/src/main.ts独立 HTML 可以直接归档、发送或离线打开同时不会依赖应用安装目录中的 Web 资源。用系统能力完成 PDF 路径鸿蒙侧不自行实现 PDF 排版引擎而是让 ArkWeb 准备打印页面再交给系统 Print KitprivateasyncprintDocument():Promisevoid{constcontextthis.getHostContext();if(!context||this.operationInProgress||!this.editorReady){return;}if(this.largeDocumentMode){this.operationStatusPrint is unavailable in large file mode;return;}this.operationInProgresstrue;this.operationStatusPreparing print...;try{constprintReadyawaitthis.editorController.runJavaScript(window.OhMarkdownEditor?.preparePrint() true);if(printReady!true){thrownewError(The editor preview is not ready for printing.);}this.editorController.setPrintBackground(true);constadapterthis.editorController.createWebPrintDocumentAdapter(this.getExportName(pdf));awaitprint.print(this.getExportName(pdf),adapter,{},context);this.operationStatusPrint dialog opened;}catch(error){this.operationStatusPrint failed:${errorinstanceofError?error.message:String(error)};}finally{this.operationInProgressfalse;}}代码来源entry/src/main/ets/shared/ui/WorkspaceShell.ets打印媒体样式会隐藏 CodeMirror只输出经过净化的预览正文。这样 PDF 与 HTML 预览使用相同内容来源也避免把编辑器行号、光标或工具栏打印进去。鸿蒙 PC 模拟器截图下图来自鸿蒙 MateBook Pro 2in1 模拟器。左侧导出面板已经提供Export HTML与Print / Save PDF两条命令按钮状态会随大文件保护模式和当前操作状态变化。验证结果Playwright 已覆盖标题注入、script标签、javascript:链接、本地相对图片、打印媒体 DOM 和生产单 HTML 包。当前自动化套件共 14 项全部通过。设备侧已经确认导出面板可正常显示系统 PDF 文件的字体、分页、表格和代码块效果仍需用实际产物继续验收。先定义两种导出的不同用途HTML 与 PDF 都来自 Markdown但使用场景不同。独立 HTML 适合离线归档、在浏览器阅读、进一步发布或交给其他工具处理PDF 适合版式固定、审阅、打印和跨设备分享。两条路径不应只是更换扩展名。HTML 需要考虑资源是否自包含、CSP、字符编码、标题、相对图片和浏览器兼容。PDF 需要考虑纸张、分页、页边距、代码块断行、表格宽度、链接和字体。共同点是正文必须来自同一份当前 Markdown并执行与应用预览一致的安全净化。导出功能的正确性还包括时序用户点击时取得哪一个 revision、导出期间继续输入怎么办、文件选择器取消后是否保持当前文档、失败时是否留下半成品。不能只验证最终出现一个文件。一份内容三种呈现源码、预览和导出之间应形成单向数据流CodeMirror EditorState.doc ↓ sliceDoc() markdown-ithtml: false ↓ DOMPurify 额外禁用规则 ↓ 安全 HTML 片段 ↙ ↘ 应用预览 独立 HTML / 打印页面安全 HTML 片段是派生结果不回写 Markdown。独立 HTML 在它外面添加文档结构、CSP 和样式打印页面使用打印媒体样式应用预览则增加受控链接行为。三个出口共享正文不共享所有外围行为。如果每个出口各自初始化一套 markdown-it 与 DOMPurify 配置规则迟早会漂移。共享函数使安全修复同时覆盖所有路径测试再确认外围模板没有重新引入风险。标题和正文必须分别处理正文经过 Markdown 渲染与 DOM 净化文档标题却进入title不能直接使用同一处理方式。文件名可能包含、、、引号甚至看似闭合标签的文本因此必须做 HTML 文本转义。标题还涉及产品规则空标题使用稳定默认值文件扩展名是否移除超长标题是否截断控制字符和路径分隔符如何处理。显示标题、导出文件名和 HTMLtitle是三个不同上下文各自需要校验不能把一个“安全字符串”在所有位置复用。正文中的代码块、链接文字和普通文本由渲染器负责转义。禁止原始 HTML 后用户写入的标签会作为文本显示DOMPurify则防止插件或渲染器输出危险结构。独立 HTML 的最小完整结构可离线阅读的 HTML 至少包含 doctype、语言、UTF-8、viewport、标题、CSP、样式和正文。UTF-8 声明应靠近文档开头避免浏览器在遇到中文前错误猜测编码。导出模板的 CSP 比应用页面更严格没有脚本需求所以default-src none只允许内联样式、data/file 图片和 data 字体。base-uri none防止基础地址改变form-action none阻止表单提交。即使净化器未来遗漏某个节点独立页面仍缺少脚本和网络能力。样式应全部内联不能引用应用安装目录或在线 CDN。这样用户把 HTML 移到另一台离线设备仍可阅读也不会因为主题服务不可用而丢失版式。字体优先使用系统字体栈避免把大型字体文件嵌入每一份文档。相对图片是导出的关键边界Markdown 常见![图](./assets/a.png)。在应用预览中相对路径需要基于当前文档目录解析在独立 HTML 中用户把文件移动后路径可能失效。可选策略包括保留相对路径要求 HTML 与资源目录一起移动。把图片复制到导出目录并重写路径。对小图片转为 data URI生成单文件 HTML。打包为目录或归档文件保留结构。每种策略都有代价。data URI 会显著放大 HTML 和内存复制资源需要目录写权限与重名处理保留相对路径最简单但不完全自包含。实现前应明确产品承诺避免界面写“独立 HTML”却让大量图片失效。本地资源解析必须沿用户授权目录进行不能为了图片方便开启 ArkWeb 任意文件访问。路径规范化后要拒绝越过授权根目录的../符号链接和特殊 URI 提供方也需要单独验证。为什么 PDF 交给系统 Print Kit自行在 ArkTS 中生成 PDF 意味着实现文本排版、字体嵌入、分页、图片、链接和多语言这会形成一套与 HTML 预览不同的渲染引擎。利用 ArkWeb 的打印适配器可以让已经验证的 HTML/CSS 版式进入鸿蒙系统打印链路并使用系统提供的预览、打印机或保存 PDF 能力。这并不意味着 PDF 自动正确。系统 Print Kit 与 ArkWeb 的组合仍要验证中文字体是否嵌入或可替代、长代码行是否截断、表格是否超出纸张、分页处标题是否孤立、背景色是否输出、链接是否可点击。系统版本和打印服务差异也可能改变结果。原生侧的职责是准备打印、启用需要的背景、创建WebPrintDocumentAdapter并交给系统。应用不应把打印页面截图后拼成 PDF那会丢失可选文本、链接和清晰矢量排版。打印媒体样式要与屏幕样式分离屏幕界面包含编辑器、工具栏、状态栏和滚动容器打印只需要正文。preparePrint()先确保预览为最新内容打印 CSS 再隐藏非正文区域、移除固定高度和滚动并设置纸面适合的字体、行高和边距。典型打印规则应考虑pre和code保留空白但允许超长内容换行或明确横向处理。表格宽度不超过页面必要时缩小字号或允许单元格断词。图片使用max-width: 100%不超过可打印区域。标题避免单独留在页尾可使用分页控制属性。引用、列表和代码块不要在不合理位置被拆开。链接颜色在黑白打印中仍有可读性可选择显示 URL。打印 CSS 需要用实际 PDF 检查浏览器页面截图无法体现分页算法。revision 决定导出的一致性用户点击导出时Web 侧从当前EditorState取得文本快照。若导出或系统选择器耗时较长用户可能继续编辑。导出的内容应明确对应点击时 revision而不是在保存文件时再次读取变化后的文档。界面可以在导出期间允许继续编辑只要导出状态显示“正在导出先前快照”也可以短暂锁定导出命令避免重复任务。无论选择哪种不能把导出完成误当作当前文档保存完成导出文件和 Markdown 源文件是两种独立持久化结果。文件选择器取消属于正常结果不显示失败也不改变文档脏状态。写出 HTML 时应校验完整 UTF-8 字节、truncate 和 fsync若系统 URI 不支持原子替换可采用与普通文档相似的备份或新文件写入策略。导出安全测试矩阵测试HTML 预期PDF/打印预期script不存在且不执行不出现在活动打印 DOMjavascript:链接被移除或不可导航PDF 不含危险动作标题闭合标签title中安全转义打印任务名安全中文与 emojiUTF-8 正确显示字体与字符不缺失长代码行HTML 可滚动/换行不被静默裁掉宽表格屏幕可读页面内可读或有明确策略相对图片按资源策略解析实际图片进入 PDF网络图片默认不产生隐私请求不在打印时偷偷联网10MiB 文档受导出保护策略限制不触发不可控峰值选择器取消不生成文件不报错系统取消后回到编辑器自动化负责字符串、DOM 和模板鸿蒙设备负责系统选择器、Print Kit、实际 PDF 文件和返回后的焦点状态。PDF 验收应打开产物逐页检查而不是只确认打印 API 返回成功。导出后的文件也需要可验证HTML 可以重新读取并用解析器检查标题、CSP、节点和外链还可以在断网浏览器中打开确认内容、样式和中文。PDF 可以记录页数、文件大小和哈希再用阅读器检查文本是否可选择、搜索是否可用、图片与链接是否正确。对于阶段报告建议保留固定导出夹具标题、中文段落、多级列表、引用、表格、代码块、相对图片、长链接和分页压力内容。每次改动渲染器、净化器或打印 CSS 后生成同一组产物比较。不要把导出的用户文档提交到公共仓库。测试夹具应使用无敏感内容的专用文档真实用户文件只在本地验证并在完成后按策略清理。HTML 与 PDF 导出验收清单导出正文来自当前 CodeMirror 快照不从预览 DOM 反向读取 Markdown。预览、HTML、打印共享同一渲染和净化函数。标题按 HTML 文本、文件名和打印任务名分别校验。独立 HTML 包含 UTF-8、严格 CSP 和完整内联样式不依赖 CDN。相对图片有明确的授权、复制、嵌入或保留策略。Print Kit 使用安全预览页面不把编辑器控件打印进去。中文字体、代码块、表格、图片和分页在真实 PDF 中验收。导出与源文件保存状态分离取消和失败不会改变脏状态。大文档导出有内存与输出大小保护。自动化、模拟器和真机 PDF 证据分别记录。安全导出的核心不是生成一种新格式而是在不改变 Markdown 事实源的前提下把同一份净化内容放进适合浏览器或系统打印的可靠容器。内容链路统一、外围策略分离才能同时控制安全、版式和长期维护成本。导出样式需要稳定的设计令牌预览和导出可以共享内容规则但屏幕与纸面样式不应复制整套应用 CSS。独立 HTML 只需要正文排版令牌正文宽度、字体栈、字号、行高、前景/背景、标题间距、代码块、引用、表格和链接。工具栏、活动栏和交互动画不应进入导出。把这些值集中到EXPORT_STYLES或可生成的主题模块可以避免散落模板字符串。更改主题时用固定夹具比较标题层级、中文行高、代码对比度和表格边框。用户选择浅色/深色导出时仍需保证打印可读不能直接套屏幕深色背景浪费墨粉。样式选择器限定在正文元素不使用会影响外部浏览器页面的通配重置。独立 HTML 虽然自成文档清晰作用域仍方便未来嵌入知识库或打印容器。语义 HTML 比视觉相似更重要Markdown 标题应输出 h1 到 h6列表使用 ol/ul代码块使用 pre/code引用使用 blockquote表格保留 th/td。把所有内容渲染成 div 加 class 可能看起来相同却损失屏幕阅读器、浏览器大纲、复制和打印语义。导出测试不仅检查文本还检查节点层级。标题顺序错误应在编辑提示或导出报告中指出但不能擅自改变用户源码。图片必须有 alt 文本没有时保持空 alt 以符合装饰图语义。链接文字与目标分离打印时可选择附加 URL。语言属性当前使用zh-CN适合中文文章未来可根据文档设置选择不应通过扫描少量字符自动猜测并频繁变化。代码块语言 class也要经过白名单。元数据和隐私边界独立 HTML 可以包含标题、生成器版本和可选创建时间但默认不嵌入本地绝对 URI、用户名、设备型号、工作区路径和恢复信息。PDF 的文档属性同样需要检查系统 Print Kit 可能写入打印任务名或应用名。用户若需要作者、关键词和描述应通过明确导出设置或 Markdown front matter 读取。front matter 也属于不可信输入进入 meta 属性前按对应上下文转义不能把任意字段原样拼进 head。时间戳会让相同内容每次导出哈希不同。可复现归档模式可以省略动态时间只保留用户明确内容普通分享模式则允许记录生成时间。模式差异要可见。Front matter 的处理策略YAML/TOML front matter 常用于静态网站。编辑器可以选择在预览中隐藏并用标题等字段配置导出也可以按代码块显示。无论哪种都应使用成熟解析器并限制别名、嵌套和输入大小避免手写字符串切分和解析复杂度攻击。正文渲染器不能把 front matter 未转义值直接插入 HTML。title经过文本转义语言和主题只接受枚举未知字段忽略或保留在源码。解析失败时不阻止源码编辑导出明确提示而不是猜测。front matter 是否从导出正文移除属于产品语义应提供稳定规则不能在预览和 PDF 中表现不同。资源打包需要冲突与容量策略当独立 HTML 复制相对图片时两个不同目录可能有同名image.png。直接放到同一 assets 目录会覆盖。可以保留相对层级、按内容哈希命名或生成映射表并重写引用。路径必须规范化且在授权根内。复制前计算总资源大小和单文件上限显示预计产物超过上限时允许保留链接、跳过或取消不在内存中一次加载所有图片。写入中断需要清理临时导出目录或用版本化目录提交避免留下看似完整的半成品。资源 MIME 类型通过内容与可信扩展判断不只信文件名。SVG、HTML 和未知类型默认拒绝或作为下载附件防止独立页面重新引入活动内容。PDF 分页的工程化验证分页问题需要构造专用夹具页面末尾连续标题、跨页表格、长引用、多页代码块、超宽图片和中英文混排。每个版本生成 PDF检查页数、关键文字所在页和是否裁切。完全像素比较容易受系统字体和 Print Kit 版本影响可以组合结构断言与人工视觉评审。关键规则如break-inside: avoid在不同引擎中支持程度不同应以鸿蒙 ArkWeb 实测为准。为了避免一整段过大导致巨量空白不应对所有容器强制禁止分页而是针对标题与短块使用。页眉页脚、页码和纸张尺寸交给系统或提供有限设置。不同地区默认 A4/Letter应在打印预览中可见不在应用中假定唯一纸型。导出任务的取消、进度和清理HTML 文本生成很快但资源复制和系统打印可能较长。任务状态至少区分准备内容、选择目标、写入资源、等待系统打印和完成。系统界面接管后应用不要显示虚假的百分比只说明正在等待。用户取消时停止尚未开始的工作关闭句柄并清理临时文件。已经交给系统的打印任务按 Print Kit 能力取消不能假定应用按钮一定能撤回。重复导出使用独立 requestId旧任务完成不会覆盖新任务状态。失败后保留 Markdown 编辑状态与恢复快照。导出失败绝不影响源文件保存也不清除 dirty。错误说明哪一步失败以及是否留下目标文件方便用户清理或重试。面向知识库和发布平台的扩展未来可能导出静态站点片段、博客格式或带资源的目录。应在安全内容片段之上增加适配器每个适配器只负责外围模板和资源规则不复制 Markdown 渲染与净化核心。平台特有短代码、组件或脚本默认不能进入通用安全 HTML。若用户主动选择某平台格式输出作为代码文件并明确风险不在应用内部执行。发布到网络服务还涉及账户、令牌和隐私应与离线导出分离成独立权限模块。保持核心导出无网络、无脚本、可审计可以让高级发布能力增长时仍有一个可靠基线。导出格式需要兼容性版本独立 HTML 模板、CSS 和资源策略会随产品演进。可以在 meta 或生成器信息中记录非敏感格式版本便于后续判断旧文件采用哪套规则版本不应要求在线运行库旧导出仍是普通静态页面。改变标题结构、代码 class 或资源目录时用固定夹具比较若会破坏用户自定义处理工具应进入发布说明。PDF 由系统生成记录应用、ArkWeb 和系统版本用于复现版式差异。可访问导出也要设备验收HTML 用键盘浏览、屏幕阅读器读取标题和图片替代文本PDF 检查文本可选、阅读顺序、标题书签和链接。Print Kit 是否保留完整语义需要实测若当前 PDF 只是视觉输出就在能力说明中准确标注并规划改进。高对比度与放大阅读不应依赖固定像素宽度。正文设置合理最大宽度但允许浏览器缩放代码块在小屏有可操作滚动。导出是离开应用后的长期文档可访问性不能只由应用工作台承担。导出回归需要真实打开产物生成字符串通过断言后还要把 HTML 写入系统 URI、关闭应用、在外部浏览器断网打开把 PDF 保存后用系统阅读器翻页、搜索和复制。这样才能覆盖编码、文件截断、资源路径和系统关联。测试产物使用固定无敏感内容记录哈希和截图。系统 API 返回成功但文件为空、旧尾部未截断或浏览器 CSP 报错都只有在重新打开时暴露。导出闭环与源 Markdown 保存闭环同样需要“写出后再读取”的证据。