- 前端
- 富文本
- UI组件
- AI 应用
【免费下载链接】BlockNote
A React Rich Text Editor that's block-based (Notion style) and extensible. Built on top of Prosemirror and Tiptap.
本篇文章以 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`, ); };三个步骤环环相扣:
- 初始化编辑器与选区:initTestEditor 重置 mock 块 ID 计数器(
__TEST_OPTIONS.mockID = 0,保证快照中块 ID 稳定可复现),通过editor.replaceBlocks载入测试文档,再在editor.transact事务中调用getCopySelection(tr.doc)设置选区; - 生成剪贴板数据:调用核心 API
selectedFragmentToHTML,取出其中markdown字段; - 快照断言:
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 做了两件事:
- 移除
EMPTY_BLOCK_PLACEHOLDER——外部 HTML 导出器会用占位字符填充空的内联内容块以保证 HTML 往返不丢块,但 Markdown 不需要它,必须剔除,否则会出现"幽灵字符"(源码注释明确说明这一点); - 调用
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.
相关推荐
BlockNote 剪贴板复制测试深度解析:text/plain 快照如何保证嵌套块复制后的 Markdown 输出
BlockNote 剪贴板复制测试深度解析:text/plain 快照如何保证嵌套块复制后的 Markdown 输出 BlockNote 在复制/剪切时会在剪贴
前端富文本UI组件AI 应用BlockNote 嵌套块复制的剪贴板输出解析:从 `childrenToNextParentsChildren` 测试快照看 text/plain Markdown 链路
BlockNote 嵌套块复制的剪贴板输出解析:从 childrenToNextParentsChildren 测试快照看 text/plain Markdow
前端富文本UI组件AI 应用BlockNote 代码块部分选择的剪贴板复制:text/plain 快照与源码级解析
BlockNote 代码块部分选择的剪贴板复制:text/plain 快照与源码级解析 BlockNote 是一款基于 ProseMirror 与 Tiptap
前端富文本UI组件AI 应用
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考