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