☰
BlockNote 剪贴板快照深度解析:跨父子边界选择(childrenToNextParent)的 Markdown 复制输出
2026/9/25 6:11:19 网站建设 项目流程
  • 前端
  • 富文本
  • UI组件
  • AI 应用

【免费下载链接】BlockNote

A React Rich Text Editor that's block-based (Notion style) and extensible. Built on top of Prosemirror and Tiptap.

项目地址:https://gitcode.com/gh_mirrors/bl/BlockNote
点击查看免费下载

本篇文章以 BlockNote 仓库中 tests/src/unit/core/clipboard/copy/snapshots/text/plain/childrenToNextParent.md 这一测试快照文件为切入点,围绕它所属的复制(copy)测试体系,完整讲解 BlockNote 在用户选中"嵌套子块 + 其后的兄弟父块"这一跨父子边界范围时,text/plain剪贴板数据(Markdown 形式)是如何生成的。读完本文,你将掌握 BlockNote 复制测试的完整链路(测试用例定义 → 编辑器初始化 → 选区构造 → 片段序列化 → 快照断言),理解嵌套块复制时 Markdown 输出"每块一行、块间空行分隔"的底层原理,并能够依据快照文件反推、验证 BlockNote 的复制行为。

快照文件是什么:一个 Markdown 剪贴板输出的黄金基准

打开 childrenToNextParent.md,文件内容非常简短:

Nested Paragraph 1 Nested Paragraph 2 Nested Paragraph 3 Paragraph 2

它不是一个普通文档,而是BlockNote 复制测试的期望输出快照(snapshot):当用户在编辑器内复制一段跨越嵌套层级的选择范围时,写入系统剪贴板text/plain数据应恰好等于上述文本。快照文件由 vitest 的toMatchFileSnapshot断言自动比对(见 copyTestExecutors.ts),任何复制逻辑的改动导致输出变化,测试都会失败并提示差异,从而锁定剪贴板行为不被无意破坏。

与它同目录的 HTML 快照 childrenToNextParent.html 记录了同一选择范围在text/html剪贴板中的数据:

<p>Nested Paragraph 1</p> <p>Nested Paragraph 2</p> <p>Nested Paragraph 3</p> <p>Paragraph 2</p>

HTML 与 Markdown 两种快照一一对应,共同构成"同一选区、多格式输出"的验证矩阵。

测试用例定义:childrenToNextParent 的文档与选区

快照对应的测试用例定义在 copyTestInstances.ts 中,名为childrenToNextParent。它先用PartialBlock数组描述测试文档结构:

{ testCase: { name: "childrenToNextParent", document: [ { type: "paragraph", content: "Paragraph 1", children: [ { type: "paragraph", content: "Nested Paragraph 1" }, { type: "paragraph", content: "Nested Paragraph 2" }, { type: "paragraph", content: "Nested Paragraph 3" }, ], }, { type: "paragraph", content: "Paragraph 2", }, ], getCopySelection: (doc) => { const startPos = getPosOfTextNode(doc, "Nested Paragraph 1"); const endPos = getPosOfTextNode(doc, "Paragraph 2", true); return TextSelection.create(doc, startPos, endPos); }, }, executeTest: testCopyHTML, }

该文档对应一棵典型的 Notion 风格嵌套树:

  • 顶级块Paragraph 1(父块),内含三个子块Nested Paragraph 1/2/3;
  • 顶级块Paragraph 2(父块的兄弟块)。

选区(selection)跨越了父子边界:起点是第一个嵌套子块Nested Paragraph 1的文本开头,终点是顶级兄弟块Paragraph 2的文本末尾。这就是测试名 "childrenToNextParent"(子块到下一个父块)的含义——它专门验证选中内容同时覆盖"某父块的多个子块"与"下一个父块本身"这一场景,确保复制出的块列表结构正确、顺序正确、不丢块。

选区的定位依赖测试工具函数 getPosOfTextNode:默认返回目标文本节点前的位置,传true时返回节点之后的位置(pos + node.nodeSize),以此构造TextSelection.create(doc, startPos, endPos)。该函数通过 ProseMirror 的doc.descendants遍历查找文本内容完全匹配的节点。

测试执行管线:从文档到快照的三步链路

整个复制测试由 runTests.test.ts 驱动。它对copyTestInstancesHTML与copyTestInstancesMarkdown两组用例分别运行"Copy tests (HTML)"与"Copy tests (Markdown)"两组 describe。其中 Markdown 组的关键在于:copyTestInstancesMarkdown并非重新定义文档,而是直接复用 HTML 组的同一批测试用例,仅把执行器换成testCopyMarkdown(见 copyTestInstances.ts)——同一选区在两种剪贴板格式下被分别验证。

testCopyMarkdown执行器(copyTestExecutors.ts)的调用链如下:

export const testCopyMarkdown = async (editor, testCase) => { initTestEditor(editor, testCase.document, testCase.getCopySelection); const { markdown } = selectedFragmentToHTML(editor.prosemirrorView, editor); await expect(markdown).toMatchFileSnapshot( `./__snapshots__/text/plain/${testCase.name}.md`, ); };

三个步骤环环相扣:

  1. 初始化编辑器与选区:initTestEditor 重置 mock 块 ID 计数器(__TEST_OPTIONS.mockID = 0,保证快照中块 ID 稳定可复现),通过editor.replaceBlocks载入测试文档,再在editor.transact事务中调用getCopySelection(tr.doc)设置选区;
  2. 生成剪贴板数据:调用核心 APIselectedFragmentToHTML,取出其中markdown字段;
  3. 快照断言:toMatchFileSnapshot将实际输出与__snapshots__/text/plain/${name}.md比对,即与本文主角childrenToNextParent.md比对。

底层原理:Markdown 剪贴板数据如何从选区产生

selectedFragmentToHTML定义于 copyExtension.ts,它一次生成三种剪贴板数据,返回{ clipboardHTML, externalHTML, markdown }:

字段用途生成方式
clipboardHTML写入blocknote/html(BlockNote 内部格式)ProseMirror 默认剪贴板序列化view.serializeForClipboard
externalHTML写入text/html(外部通用 HTML)fragmentToExternalHTML走外部 HTML 导出器
markdown写入text/plain(纯文本)由externalHTML经cleanHTMLToMarkdown转换

其中markdown的生成遵循两条规则:

  • 普通场景:cleanHTMLToMarkdown(externalHTML)—— 先由外部 HTML 导出器把选中片段序列化为块级 HTML,再转换为 Markdown;
  • 纯代码块场景:当选区完全位于meta.code === true的块内(如 codeBlock)时,直接取doc.textBetween($from.pos, $to.pos)的原始文本,避免 Markdown 围栏(fences)和反斜杠转义残留(注释见 copyTestInstances.ts)。

随后实际写入剪贴板的动作发生在 copyToClipboard:event.preventDefault()阻止浏览器默认行为,依次setData("blocknote/html", clipboardHTML)、setData("text/html", externalHTML)、setData("text/plain", markdown)。因此,本快照文件实质上就是用户按 Ctrl/Cmd+C 时系统剪贴板text/plain数据的精确还原。

为何输出是"每块一行 + 空行分隔"

markdown的生成链路是externalHTML → cleanHTMLToMarkdown。cleanHTMLToMarkdown 做了两件事:

  1. 移除EMPTY_BLOCK_PLACEHOLDER——外部 HTML 导出器会用占位字符填充空的内联内容块以保证 HTML 往返不丢块,但 Markdown 不需要它,必须剔除,否则会出现"幽灵字符"(源码注释明确说明这一点);
  2. 调用htmlToMarkdown把块级 HTML 转成 Markdown。

对于本例这种全部由普通段落(paragraph)组成的选中范围,<p>...</p>会被转换为各自独立的 Markdown 段落,段落之间以空行分隔,于是得到快照中的 4 行文本 + 3 个空行。这里没有任何缩进或列表标记,因为选中块均为顶层级别的普通段落——注意Nested Paragraph 1/2/3虽是Paragraph 1的子块,但在 plain/text 序列化中它们被"扁平化"为独立的 Markdown 段落,层级信息由块结构承载而非缩进符号表达。这正是 Markdown 剪贴板输出与文档树层级之间的关键差异点。

横向对照:相邻用例验证嵌套行为

同目录下另两个快照可作为对照,进一步验证跨父边界选择的序列化行为:

  • childrenToNextParentsChildren.md 对应 childrenToNextParentsChildren 用例:Paragraph 2也有自己的三个子块(Nested Paragraph 4/5/6),选区覆盖全部六个子块加Paragraph 2,输出为 7 段连续文本,同样块间空行分隔;
  • childToParent.md 对应childToParent用例:选区从父块Paragraph 1到第一个子块Nested Paragraph 1,验证的是反向(父到子)边界。

三个用例共同覆盖了子→子(multipleChildren)、父→子(childToParent)、子→下一个父(childrenToNextParent)、子→下一个父的子(childrenToNextParentsChildren)这四种嵌套选区组合,构成完整的嵌套复制验证矩阵。

如何运行与复现该快照验证

快照所属测试运行在tests/工作区中。仓库采用 pnpm workspace 管理(见 pnpm-workspace.yaml),核心包为@blocknote/core(packages/core),测试通过 vite.config.ts 配置。可执行以下命令复现:

# 在仓库根目录安装依赖(若尚未安装) pnpm install # 运行复制相关单元测试(含 HTML 与 Markdown 两组快照断言) pnpm --filter @blocknote/test run test -- src/unit/core/clipboard/copy

运行后 vitest 会逐条执行runTests.test.ts中的用例,将selectedFragmentToHTML实际生成的 markdown 与 childrenToNextParent.md 等快照比对。若复制逻辑发生变化导致输出与快照不一致,测试会失败并生成.new后缀的差异文件,便于开发者审查是否符合预期后再决定是否更新快照。

结语:从一行快照反推整个复制子系统

childrenToNextParent.md表面上只是 7 行纯文本,但它浓缩了 BlockNote 剪贴板子系统的一条完整验证链:测试用例(copyTestInstances.ts)定义文档与跨父边界选区 → 执行器(copyTestExecutors.ts)初始化编辑器并调用核心 API → copyExtension.ts 的selectedFragmentToHTML产出三格式数据 → markdownExporter.ts 的cleanHTMLToMarkdown完成 HTML→Markdown 转换 → 快照锁定结果。阅读这类快照时,建议始终对照其 HTML 同名快照与测试用例中的document/getCopySelection,即可快速还原"什么样的选区产生什么样的剪贴板输出",这是理解 BlockNote 数据序列化行为最高效的入口。

  • 前端
  • 富文本
  • UI组件
  • AI 应用

【免费下载链接】BlockNote

A React Rich Text Editor that's block-based (Notion style) and extensible. Built on top of Prosemirror and Tiptap.

项目地址:https://gitcode.com/gh_mirrors/bl/BlockNote
点击查看免费下载

相关推荐

上一篇:如何快速获取抖音直播数据:DouyinLiveWebFetcher完整实战指南
下一篇:Origami Simulator 实时折纸模拟:从打开浏览器到折出立体模型

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询