Uninote
Uninote
用户根目录
brdr
common
programming
docs
tests
后端试题
问题讨论

HTML 粘贴与图片本地化实现

返回文档导航 · 用户可见行为

调用链与模块职责

编辑器 paste 事件
  → handleEditorPaste:选择应用内 Markdown、HTML、单图或普通文本路径
  → parseClipboardHtml / htmlToMarkdown:解析并转换 HTML
  → pasteHtml:保存目标、导入图片、检查文档状态、插入正文
  → preload.importHtmlImages → images:import-html IPC
  → importHtmlImages:下载或解码图片,写入 assets/,返回路径映射
  • src/renderer/runtime/editor/events.ts 在编辑器捕获阶段接收 paste,并同步决定是否拦截浏览器默认粘贴。
  • src/renderer/editor/html-to-markdown.mts 负责 HTML 解析、节点转换和可导入图片地址收集。
  • src/renderer/runtime/editor/file-actions.ts 负责异步图片导入、状态检查和一次性正文替换。
  • src/preload.ts 暴露 importHtmlImages(documentPath, sources);主进程在 src/main/ipc-handlers.ts 注册 images:import-html。
  • src/main/html-image-import.ts 读取、验证并保存图片;src/main/file-service.ts 负责生成不冲突的附件文件名及相对路径。
  • src/main.ts 向 IPC 处理器注入 Electron net.fetch,测试可注入替代实现。

粘贴入口与优先级

  1. 只处理当前编辑器页面的粘贴事件;只读或禁用状态直接阻止编辑。
  2. 剪贴板含应用内部 Markdown 标记时,先尝试原有跨目录图片路径改写。
  3. htmlPasteConversionEnabled 默认为启用;禁用时不处理 HTML,有纯文本则交给 CodeMirror 原样粘贴,没有纯文本时沿用单图或原生粘贴路径。
  4. 剪贴板类型匹配 htmlPasteExcludedTypes 中任一条目且 text/plain 非空时,交给 CodeMirror 粘贴原始纯文本,跳过 HTML 转换和图片导入。
    • 所有用户默认过滤 vscode-editor-data;缺少此配置的现有用户同样使用默认列表。
    • 设置页的“不转换 HTML 的剪贴板类型”支持每行一个类型,可编辑或清空;显式空列表不再补回默认值。
    • 只检查来源标记是否存在,不解析其 JSON 内容;空或格式错误的元数据不影响识别。
    • 保留代码缩进、换行、首尾空白和 Markdown 语法,不自动添加代码围栏。
    • 来源标记存在但纯文本为空时,继续后续流程,避免丢失有效 HTML 或图片。
    • VS Code 同时写入纯文本、高亮 HTML 和该来源标记,见 VS Code 剪贴板实现。
  5. 剪贴板含 text/html 且转换结果非空时,阻止默认粘贴并处理完整 HTML。
  6. 没有有效 HTML、但含 image/* 二进制条目时,沿用单张剪贴板图片插入流程。
  7. 其他情况交给 CodeMirror 的普通文本粘贴;text/plain 中的 HTML 标签不会自动转换。

菜单粘贴通过 Electron 原生编辑命令进入同一事件链。 编辑器右键菜单的“粘贴为纯文本”直接插入剪贴板文本,跳过 HTML 转换和图片下载。 不携带默认来源标记的 IDE 可在提示中添加其附加剪贴板类型,也可使用“粘贴为纯文本”;不根据字体、颜色或 pre 标签猜测来源,避免误拦截网页代码示例。 同时含 HTML 正文和图片二进制条目时,HTML 路径优先,避免只插入其中一张图片而丢失正文。

HTML 转换提示

  • 在有效 HTML 进入转换路径时,读取当前应用设置中的 htmlPasteNoticeEnabled;未配置时默认启用。
  • Toast 显示“检测到 HTML 格式内容,正在尝试转换为 Markdown。”,8 秒后自动隐藏。
  • 点击 Toast 或下方“查看更多”按钮,复用自定义确认对话框。
    • 动态类型选项放在上方独立区域,每个类型一个按钮,按列排列。
    • 下方单独一行按“禁用转换”“继续提示”“转到设置”“不再提示”的顺序右对齐;窗口过窄时可横向滚动按钮区域。
    • “禁用转换”保存 htmlPasteConversionEnabled: false,从下次粘贴起跳过 HTML 自动转换;不改动提示偏好或类型列表。
    • 设置页的“启用 HTML 转 Markdown”默认开启,取消勾选可禁用,重新勾选可恢复。
      • 根据当前表单中的勾选状态,立即禁用或启用下方的转换提示开关与类型过滤列表,禁用时变灰。
      • 不清空控件值;打开设置或取消修改时同步恢复已保存的值和禁用状态。
    • 类型列表提示文案仅说明填写方式与匹配行为,不再显示默认包含的类型。
    • “不再提示”保存关闭提示的偏好。
    • “继续提示”保留提示;按 Esc 关闭对话框同样继续提示。
    • “转到设置”打开设置页并聚焦“粘贴 HTML 时显示转换提示”,可重新开启或关闭提示,并编辑下方的类型过滤列表。
    • 当前剪贴板中每个附加类型分别提供“该类型不转换”按钮,排除 text/plain、text/html 和浏览器条目指示器 Files。
    • 点击后在设置写入队列中追加对应类型,去重并保留其他条目,跨窗口同时追加不会覆盖彼此的选择;从下次粘贴起生效。
    • 添加类型不改变提示开关;只有匹配类型且具有非空纯文本的后续粘贴会跳过转换。
  • 偏好通过应用设置存储保存在本机,并通过现有设置广播更新其他窗口;不参与云端偏好同步。
  • 显示提示不会阻塞粘贴,提示开关与转换开关独立;图片下载失败提示继续独立显示。

HTML 转换

解析与输出

  • DOMParser 以 text/html 模式解析剪贴板字符串,转换器遍历解析所得的 body。
  • 转换规则参照 pjkit/tools/doc-crawler/doc-crawler.js 的 nodeToMd、listToMd、tableToMd 和 renderMarkdown;浏览器原生解析器取代参考脚本自带的 HTML 解析器。
  • 普通文本节点把连续空白压为一个空格;pre 和 code 内保留原始空白。
  • 最后清除行尾空白、将连续三行以上空行压成两行,并去掉首尾空白。
  • 转换产物是 Markdown 字符串,通过 CodeMirror 的 replaceRange 写入正文,不把剪贴板 HTML 直接插入编辑器 DOM。

标签规则

HTML Markdown 或处理方式
h1–h6、p、br、hr 标题、段落、换行、分隔线
strong、b、em、i、s、del、strike 粗体、斜体、删除线
a [文字](href);空地址或 javascript: 地址只保留文字
img ![alt](src);alt 为空时仍保留图片
code、pre 行内代码、带可识别语言标记的代码围栏
blockquote、ul、ol 引用、无序或有序列表;子列表缩进到父项下
table 首行作为表头,补齐列数,转义单元格内的管道符
常见容器标签 递归转换子节点,并规范块间空行
script、style、iframe、svg 等 跳过

带 aria-hidden="true" 或位于 role="img" 装饰节点内的内容会跳过。 剪贴板没有可靠的页面来源 URL,转换器不会猜测相对链接的基准地址。 与参考脚本相比,空 alt 的有效图片仍生成 Markdown 引用,以便按本功能要求尝试本地化。

图片导入

渲染进程到主进程

  • 转换器从非装饰性的 <img src> 收集去重后的地址。
  • 只有 http:、https: 和 data:image/ 地址进入导入流程;相对地址原样保留,也不会触发未保存文档的保存对话框。
  • 导入前通过 ensureDocumentPath() 取得 Markdown 文件路径;未保存文档需先选择保存位置。
  • 渲染进程每次最多发送前 20 个可导入地址,超过的地址保持原值并计入失败提示。
  • IPC 返回 { paths, failedCount }。
    • paths 是原 src 到 assets/... 相对路径的映射。
    • failedCount 是本次请求中未能导入的不同地址数量。
  • 主进程先用 validateMarkdownPath 和 FileAccess.assertDocument 确认目标是已授权的 Markdown 文件,再接受图片地址。

下载、验证与落盘

  • HTTP/HTTPS 图片由主进程的 net.fetch 下载;重定向逐次检查协议及地址中的用户名、密码,最多跟随 5 次。
  • 每张网络图片共享一个 15 秒超时,先检查 Content-Length,再流式读取并限制实际内容为 10 MB。
  • data:image 读取 Base64 或有效的 URI 百分号编码;编码无法解析时保留原地址,解码后的内容同样受 10 MB 限制。
  • 仅接受 PNG、JPEG、GIF、WebP 和 BMP;除检查 MIME 外,还检查文件头对应的真实格式。
  • 导入按地址逐个进行;任何一张失败都不影响其他图片。相同地址只写入一次。
  • 文件使用原 URL 文件名或 pasted-image 作为名称基底,附加随机后缀,再写入目标 Markdown 文件同级的 assets/。
  • 图片保存位置取决于目标 Markdown 文件路径。如果文档保存在项目或笔记库外,assets/ 也会位于该文件同级,而非项目根目录。
  • 不读取 HTML 中的 file: 图片,也不尝试解析没有来源基准的相对地址。

异步结果与失败回退

  • pasteHtml 在等待保存和下载前记录文档 ID、正文、编辑器适配器及原选区。
  • 等待结束后,若活动文档已切换、适配器已销毁或正文已变化,就放弃插入,避免异步结果覆盖后续编辑。
  • 图片下载失败、格式不受支持或超出数量限制时,Markdown 保留该图片的原 src,其余正文照常插入,并显示失败数量。
  • 未保存文档取消保存位置选择时,不下载图片,但仍插入转换后的正文及原图片地址。
  • 最终通过一次 replaceEditorRange 替换粘贴时的选区,因此整次 HTML 粘贴对应一个撤销单元。
  • 若转换后的 Markdown 为空,使用剪贴板 text/plain 作为回退内容。

右键下载外部图片

  • 编辑器右键菜单中的“下载外部图片”使用 Markdown 解析器寻找 HTTP/HTTPS 图片引用;普通链接、代码块、相对地址和 data:image 不进入此操作。
  • 有选区时只处理完整落在选区中的图片引用;没有选区时扫描当前文档。重复地址只下载一次。
  • 复用上述主进程下载、验证、授权和附件保存流程。进度按不同地址计数,显示已处理、总数、成功数和失败数。
  • “终止”中断当前网络请求并跳过后续地址;已完成的附件和地址替换保留。下载失败的引用保持原地址。
  • 下载完成或终止后提示成功、失败及未处理数量。若等待期间切换文档或修改正文,附件不会写回已变化的编辑器内容。

验证入口

  • node scripts/run.mts node --test test/html-image-import.test.js test/ipc-handlers.test.js:覆盖去重、网络图与 data:image、错误 MIME、文件头、大小、重定向和授权边界。
  • npm run build:editor && npm run test:smoke:html-paste:在真实 Electron 编辑器中检查格式转换、混合剪贴板、重复附件、坏图回退、相对地址、纯文本 HTML、撤销与重做。
    • 同时覆盖类型过滤默认值、多类型按钮、追加及去重、设置编辑和清空、禁用及重新启用转换、重启保留,以及 IDE 来源识别、代码及 Markdown 原文保留、空或格式错误的元数据、混合图片跳过、撤销与重做,以及没有纯文本时的 HTML 回退。
    • 测试库没有 Git 历史,打开目标文档前通过界面关闭文件差异,避免异步 Git 基线提示覆盖图片失败提示,造成时序相关的误报。
    • 断言失败时输出转换后的正文、粘贴时的通知和事件拦截状态,便于定位具体失败条件。
  • npm run test:smoke:paste:回归应用内 Markdown 跨目录粘贴和原有编辑器历史行为。

file-tree-test-cases

keyboard-mvp

点赞(0) 阅读(16) 举报
目录
标题