鸿蒙 PC Markdown 编辑器系统浏览器与图片查看器独立验收
仓库地址:https://gitcode.com/VON-/codex_md_oh
代码基线:导出实现 6c823eb,第三阶段质量基线 941a1dc,当前主线 6c9d88f。
导出完成不等于导出可用
桌面 Markdown 编辑器的导出链路很容易在应用内部形成"自证":编辑器生成一段 HTML 字符串,单元测试确认字符串里存在标题,于是工程报告写成 HTML 导出完成;ArkWeb 截得 PixelMap,方法没有抛异常,于是 PNG 也被写成完成。这样的验证只能证明生产者认为自己交付了数据,不能证明操作系统、浏览器和图片查看器能够把数据当成合法文件消费。
鸿蒙 PC 上更可靠的验收必须跨越应用边界。用户在 OhMarkdown 中发起导出,系统 DocumentSave 选择器决定目标位置,Core File Kit 负责落盘,随后用户通过文件管理器找到产物,再交给系统浏览器或图片查看器独立打开。只有这条链路走通,文件扩展名、URI 权限、字节写入、格式编码、资源自包含和外部读取才能同时获得证据。
本轮验收把 HTML 与 PNG 分开执行。HTML 在系统浏览器中显示标题、正文、代码块、公式和流程图;PNG 在系统图片查看器中显示完整预览画面。两者都不依赖 OhMarkdown 当前进程继续提供内存对象,因此比应用内预览截图更接近真实交付。
系统选择器是写入授权的起点
HarmonyOS 应用不应该先假设一个桌面绝对路径,再通过字符串拼接写文件。OhMarkdown 使用 DocumentViewPicker.save,让用户选择保存位置,同时由系统返回应用可写的 URI。建议文件名只是建议,最终位置和名称仍由用户控制。
ts
async function pickExportUri(context: Context, suggestedName: string,
suffixChoice: string): Promise<string | undefined> {
const options = new picker.DocumentSaveOptions();
options.newFileNames = [suggestedName];
options.fileSuffixChoices = [suffixChoice];
const documentPicker = new picker.DocumentViewPicker(context);
const selectedUris = await documentPicker.save(options);
return selectedUris.length > 0 ? selectedUris[0] : undefined;
}
export function pickHtmlExportUri(context: Context,
suggestedName: string): Promise<string | undefined> {
return pickExportUri(context, suggestedName, 'HTML|.html');
}
这段代码的关键并不是弹出一个对话框,而是把授权与路径选择合并为一次系统交互。用户取消时返回 undefined,调用方必须停止写入,不能把取消当成失败,也不能悄悄改写应用缓存目录。PC 产品需要尊重这种可预期性:保存位置、重名决策和文件可见性都应由系统文件体验统一管理。
文件名净化是跨文件系统兼容问题
Markdown 标签名可能包含斜杠、冒号、问号、控制字符或很长的标题。这些内容适合显示,不一定适合作为桌面文件名。导出服务先移除扩展名,再替换文件系统不接受的字符,折叠空白并限制长度;全部被清理后退回 Untitled。
ts
export function sanitizeExportBaseName(documentName: string): string {
const lastDot: number = documentName.lastIndexOf('.');
const source: string = lastDot > 0 ? documentName.slice(0, lastDot) : documentName;
const normalized: string = source
.replace(/[\u0000-\u001F\u007F/\\:*?"<>|]/g, '-')
.replace(/\s+/g, ' ')
.replace(/[- ]{2,}/g, '-')
.replace(/^[. -]+|[. -]+$/g, '')
.slice(0, 120);
return normalized.length > 0 ? normalized : 'Untitled';
}
净化逻辑不能用来改变正文,也不能把显示名称永久覆盖。它只服务导出建议名。这样的边界避免"为了保存一个文件,文档标签也被改名"的状态串扰,也降低不同文件系统和外部应用面对异常名称时的兼容风险。
自包含 HTML 才能离开应用运行
普通预览依赖 ArkWeb 内部的 CSS、字体、Blob 图片和运行时脚本。若直接把预览 DOM 的一小段 innerHTML 写入文件,外部浏览器打开后往往只剩裸文本:相对样式路径无法访问,Blob URL 已失效,KaTeX 字体和 Mermaid 图形也可能丢失。
OhMarkdown 的 HTML 导出先完成专业渲染,再把样式、KaTeX 字体、图片 Data URL 和已净化的图表结果写进单个文档。外部文件不需要连接网络,也不要求 OhMarkdown 保持运行。输出中的脚本不承担二次渲染,导出结果更接近静态出版物,而不是一份需要执行代码才能成立的网页工程。
ts
return `<!doctype html>
<html lang="${escapeHtmlAttribute(locale)}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="Content-Security-Policy"
content="default-src 'none'; img-src data:; font-src data:; style-src 'unsafe-inline'">
<title>${escapeHtml(documentName)}</title>
<style>${EXPORT_STYLES}</style>
</head>
<body><main>
${body}
</main></body>
</html>`;
严格 CSP 明确拒绝默认外部资源,只允许 Data URL 图片、Data URL 字体和内联样式。这里允许 unsafe-inline 的对象是样式,不是脚本。浏览器即使离线打开文件,也不会因为正文中存在可疑标签而主动访问第三方地址。
安全净化要发生在导出之前
Markdown 的优势是可以表达链接、图片和结构化内容,但它也意味着正文来自不完全可信的本地文件。导出不是绕过预览安全策略的后门。原始 HTML 默认关闭,Markdown 渲染结果继续经过 DOMPurify,Mermaid 使用严格安全级别,最终 SVG 再净化;链接只保留允许协议,图片在授权边界内读取并转换。
这种顺序很重要:先解析语义,再增强公式、图表和高亮,最后形成静态导出。若先拼完整 HTML 再用宽松正则删除 <script>,事件属性、危险 URL、SVG 外部引用和嵌套标签很容易漏掉。安全策略应该由结构化解析器与白名单承担,不让文章正文控制导出页面能力。
PNG 导出不是浏览器截图快捷键
PNG 的目标是把当前专业预览转成一个可以被系统图片应用消费的位图。它不等同于截取当前窗口,因为窗口中可能只有预览的一部分,还可能带着工具栏、侧栏和光标。OhMarkdown 先切换到专用导出捕获状态,计算预览内容的完整宽高,然后由 ArkWeb 绘制并交给 ImageKit 编码。
Web 侧明确给出平台边界:宽或高超过 16000 像素时拒绝继续。这个限制不是产品随意缩短长文档,而是防止位图面积、RGBA 内存和编码过程在桌面设备上形成不可控峰值。超长文章应该选择 HTML 或 PDF,而不是生成一张无法可靠打开的巨图。
ts
function getImageExportMetrics(): string {
if (!imageExportLayoutReady) return '';
const width = Math.ceil(Math.max(preview.scrollWidth,
preview.getBoundingClientRect().width));
const height = Math.ceil(Math.max(preview.scrollHeight,
preview.getBoundingClientRect().height));
return JSON.stringify({
width,
height,
maximum: 16000,
supported: width <= 16000 && height <= 16000
});
}
PixelMap 必须完整编码并同步
拿到 PixelMap 后,原生侧不能把"调用 pack"当成结束。目标 URI 可能已存在旧内容,因此先截断;ImagePacker 以 PNG、质量 100 写入;fsync 保证数据同步;最后无论成功或失败都释放 packer、关闭文件句柄。
ts
export async function writePngPixelMap(targetUri: string,
pixelMap: image.PixelMap): Promise<void> {
const file = await fileIo.open(targetUri, fileIo.OpenMode.READ_WRITE);
const packer = image.createImagePacker();
try {
await fileIo.truncate(file.fd, 0);
await packer.packToFile(pixelMap, file.fd, {
format: 'image/png', quality: 100
});
await fileIo.fsync(file.fd);
} finally {
await packer.release();
await fileIo.close(file);
}
}
若不截断,一个比旧文件更短的新 PNG 可能保留尾部字节;若不在 finally 释放资源,连续导出会积累句柄和原生内存。桌面编辑器的可靠性往往体现在这些不显眼的结束动作上。
HTML 的独立查看结果
下图不是 OhMarkdown 内部预览,而是导出 HTML 经系统文件管理器转交后,在鸿蒙 PC 模拟器的独立浏览器窗口中打开的结果。页面已经离开编辑器运行上下文,仍能保持排版、公式、流程图与代码样式。

这张证据可以支持四个结论:系统选择器确实创建了外部可见文件;浏览器能够识别 HTML;静态资源已经自包含;浏览器不依赖编辑器当前标签继续渲染。它不能单独证明所有恶意语料都安全,也不能证明不同系统浏览器版本完全一致,因此安全回归和兼容矩阵仍要单独维护。
PNG 的独立查看结果
下图来自鸿蒙 PC 模拟器的系统图片查看器。产物由 OhMarkdown 的 ArkWeb 捕获、PixelMap 绘制和 ImagePacker 编码产生,查看器能够独立解码并显示。

图片查看器成功打开,比仅检查文件后缀更有价值。.png 名称并不能保证内部是合法 PNG,文件大小大于零也不能保证 IEND、色彩与像素数据完整。独立解码至少覆盖了格式识别和主要图像数据。但它仍不是逐像素基准测试,后续可以加入固定语料的尺寸、像素抽样和视觉差异比较。
为什么通过文件管理器重新进入
应用在写入后若立即复用手中的 URI 读取,仍可能沿用原授权或缓存,无法证明普通桌面用户能找到产物。验收特意回到文件管理器,再从用户可见目录打开文件,是为了验证"产物存在于用户心智模型中"。
这一步还能暴露建议名称、扩展名、关联应用和重名策略问题。一个技术上可读但被保存到不可发现目录的文件,对 PC 用户并不算可用;一个扩展名错误、每次都需要手动选择应用的产物,也会让导出工作流显得不完整。系统文件管理器是导出功能的重要组成,而不是无关的外部工具。
HDC URI 与用户 URI 的边界
设备调试期间可以通过 HDC 观察应用沙箱、拉取日志或定位测试产物,但 HDC 能访问并不代表普通用户有同等权限。正式验收使用系统选择器返回的用户 URI,不把调试 shell 路径当作产品路径。
相反,调试工具仍适合确认字节数、文件头、HAP 哈希和日志时序。两类证据互补:HDC 回答工程人员能否复现,文件管理器与外部应用回答用户能否完成任务。混淆两者会出现"开发机上能拉出来,所以用户可以导出"的错误结论。
取消和失败必须保持原文安全
用户在 DocumentSave 中取消时,不应生成半文件,也不应改变当前文档的脏标记。浏览器或图片编码失败时,编辑器内容、标签会话和原保存基线也不能被导出动作覆盖。导出是只读消费当前快照,不是保存命令的另一种名称。
错误信息需要区分选择器取消、目标不可写、渲染失败、尺寸超限和系统服务不可用。只有可操作的失败才有恢复价值:用户可以缩短图片、选择 HTML、换目录或稍后重试。统一弹出"导出失败"会把真实系统约束隐藏起来,也不利于测试定位。
大文档保护与导出互斥
当正文达到五兆字符保护阈值,OhMarkdown 保持源码编辑,但禁用预览、HTML、PNG、PDF 和链接增强。这不是因为导出功能不重要,而是因为大文档渲染会同时复制正文、HTML、DOM、位图和字体资源,内存峰值可能远高于源文件本身。
桌面产品应该在动作开始前明确禁用,而不是运行很久后崩溃。用户仍可安全保存 Markdown 原文,再使用适合批处理的外部工具。保护模式和 16000 像素上限共同构成资源预算:前者控制文本与 DOM,后者控制图像面积。
PDF 为什么仍不能写成完成
当前应用已接入 HarmonyOS 打印适配器,模拟器能够进入系统打印预览;但测试环境没有可用的"另存为 PDF"服务,无法得到一个由独立 PDF 阅读器打开的最终文件。因此 G3-08 仍保持进行中,文章只确认打印链路接入和预览出现,不把预览界面等同于 PDF 产物。
真正的 PDF 验收至少需要:系统服务生成文件、文件管理器可发现、阅读器可打开、分页与长表格合理、中文字体和公式不丢失、链接策略符合预期。缺少其中最后产物的证据,就不能为了阶段数字提前宣告完成。
分享目标为什么仍是系统依赖
Markdown 分享已经将当前内存快照写入缓存,并通过只读 URI 发起系统隐式分享。模拟器能显示分享面板和无兼容目标时的恢复路径,但没有安装一个声明接收 Markdown 文件的兼容应用,所以无法证明接收方成功读取正文。
这与 HTML/PNG 独立查看不同:浏览器和图片查看器是现成消费方,Markdown 分享接收方不在当前镜像能力矩阵内。应用侧不能通过把文件改成 text/plain 来伪造兼容,因为那会改变产品语义和目标筛选。后续应在真机或装有兼容应用的镜像上完成端到端证据。
可复现验收清单
HTML 的标准任务是:准备含中文标题、相对图片、公式、Mermaid、代码块和外部链接的文档;导出到用户目录;关闭或切走 OhMarkdown;从文件管理器进入;使用浏览器打开;断网后刷新;检查控制台无外部资源请求。PNG 任务使用同一正文,另检查查看器识别、完整高度、清晰度和导出后编辑模式恢复。
失败任务同样必须执行:取消选择器、只读目标、非法建议名、超长预览、损坏图片、重复导出和快速连续触发。每个任务记录代码提交、HAP SHA-256、模拟器或真机型号、系统版本、目标应用版本和截图路径。没有这些元数据,截图很快就会失去回归价值。
对鸿蒙 PC 产品优势的意义
用户真正关心的不是编辑器内部有多少导出函数,而是文件能否离开应用继续工作。自包含、安全、离线、系统选择器和独立查看器共同形成可携带性。对于文档工具,这种可携带性比专有格式锁定更能建立长期信任。
OhMarkdown 当前 HTML 与 PNG 已跨过"应用内生成"到"系统外消费"的关键门槛,同时保留 PDF 与分享的真实缺口。优势不是把所有状态涂成绿色,而是对每种格式建立独立证据、失败边界和替代路径。这样后续扩展主题、模板、分页和批量导出时,仍能沿用同一验收模型。
结论
鸿蒙 PC Markdown 编辑器的导出验收必须跨越 ArkWeb、ArkUI、Core File Kit、DocumentSave、文件管理器和外部消费应用。OhMarkdown 已验证自包含 HTML 由系统浏览器独立打开,PNG 由系统图片查看器独立解码,文件在编辑器进程之外仍然成立。
本轮结果明确通过 HTML 与 PNG 独立查看,不扩大为 PDF 和 Markdown 分享成功。系统 PDF 服务与兼容分享接收方仍是 G3-08 的剩余闸门。把生成、落盘、发现、打开和内容正确拆成多个证据点,才能让"导出完成"成为可复现的工程结论,而不是一个按钮存在的描述。