鸿蒙 PC Markdown 编辑器系统浏览器与图片查看器独立验收

鸿蒙 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 的剩余闸门。把生成、落盘、发现、打开和内容正确拆成多个证据点,才能让"导出完成"成为可复现的工程结论,而不是一个按钮存在的描述。

相关推荐
红烧大青虫2 小时前
HarmonyOS开发实战:小分享-WaterFlow 瀑布流布局实现模板墙
华为·harmonyos·鸿蒙
程序员黑豆2 小时前
鸿蒙应用开发:Stack堆叠组件实战——实现微信消息角标效果
前端·harmonyos
Catrice02 小时前
HarmonyOS ArkTS 实战:实现一个心情日记与情绪追踪应用
华为·harmonyos
●VON3 小时前
鸿蒙 PC Markdown 编辑器通信架构:受限 ArkTS-JavaScript Bridge
华为·架构·编辑器·harmonyos·鸿蒙
一缕清烟在人间3 小时前
HarmonyOS开发实战:小分享-TextEditPage文字编辑器——Header+TextArea+工具栏
后端·华为·harmonyos·鸿蒙
2501_918582374 小时前
HarmonyOS应用开发实战:小事记 - 应用包结构:HAP/HSP/HAR 的三层架构与 deliveryWithInstall 策略
华为·架构·harmonyos·鸿蒙
不肥嘟嘟右卫门4 小时前
鸿蒙原生ArkTS布局方式之Scroll+Column+Sticky粘性布局深度解析
华为·harmonyos
宁&沉沦4 小时前
Chrome 扩展 Manifest 字段版本支持一览(全量)
前端·后端·编辑器
通问AI5 小时前
华为昇腾950 vs 英伟达GB300:技术规格对比与CUDA生态迁移路径分析
华为