制測(cè)試深度解析:text/plain 快照如何保證嵌套塊復(fù)制后的 Markdown 輸出)
前端富文本UI組件AI 應(yīng)用【免費(fèi)下載鏈接】BlockNoteA React Rich Text Editor thats block-based (Notion style) and extensible. Built on top of Prosemirror and Tiptap.項(xiàng)目地址https://gitcode.com/gh_mirrors/bl/BlockNote點(diǎn)擊查看免費(fèi)下載BlockNote 在復(fù)制/剪切時(shí)會(huì)在剪貼板寫(xiě)入三種數(shù)據(jù)格式blocknote/html內(nèi)部往返格式、text/html供富文本應(yīng)用粘貼以及text/plain供純文本與 Markdown 應(yīng)用粘貼。本文以tests/src/unit/core/clipboard/copy/__snapshots__/text/plain/multipleChildren.md這份快照文件為入口拆解 BlockNote 復(fù)制嵌套子塊時(shí)text/plain輸出的 Markdown 語(yǔ)義并沿測(cè)試用例、執(zhí)行器與序列化源碼的調(diào)用鏈還原從選區(qū)到剪貼板純文本的完整實(shí)現(xiàn)原理。讀完你既能讀懂這套快照測(cè)試的斷言方式也能理解 BlockNote 復(fù)制功能的三格式剪貼板架構(gòu)與 Markdown 序列化細(xì)節(jié)??煺瘴募亩ㄎ粡?fù)制功能的 text/plain 期望輸出multipleChildren.md是 BlockNote 單元測(cè)試體系中的一份文件快照f(shuō)ile snapshot它記錄的是多子塊multiple children復(fù)制場(chǎng)景下寫(xiě)往剪貼板text/plain位置的預(yù)期內(nèi)容。全文僅 5 行Nested Paragraph 1 Nested Paragraph 2 Nested Paragraph 3這段輸出的含義是當(dāng)用戶復(fù)制一個(gè)父段落下的三個(gè)嵌套子段落時(shí)BlockNote 生成的純文本是一個(gè)扁平的 Markdown 段落序列——每段文本之間以空行分隔既不保留嵌套層級(jí)縮進(jìn)也不附加任何列表或縮進(jìn)標(biāo)記。這與text/html保留嵌套結(jié)構(gòu)的行為形成鮮明對(duì)比是理解 BlockNote 剪貼板格式設(shè)計(jì)的關(guān)鍵細(xì)節(jié)。快照文件存放在 tests/src/unit/core/clipboard/copy/snapshots/text/plain/ 目錄下與basicBlocks.md、styledText.md、tableRow.md等 20 份快照并列共同覆蓋基礎(chǔ)塊、樣式文本、表格、圖片、代碼塊等復(fù)制場(chǎng)景的純文本輸出契約。對(duì)應(yīng)測(cè)試用例multipleChildren 的文檔結(jié)構(gòu)與選區(qū)快照并非憑空生成它由 copyTestInstances.ts 中名為multipleChildren的用例驅(qū)動(dòng)。該用例以PartialBlock樹(shù)描述測(cè)試文檔一個(gè)父段落paragraph內(nèi)容 Paragraph 1其下嵌套三個(gè)子段落內(nèi)容分別為 Nested Paragraph 1、Nested Paragraph 2、Nested Paragraph 3。選區(qū)由getCopySelection函數(shù)構(gòu)造它借助getPosOfTextNode定位文本節(jié)點(diǎn)位置const startPos getPosOfTextNode(doc, Nested Paragraph 1); const endPos getPosOfTextNode(doc, Nested Paragraph 3, true); return TextSelection.create(doc, startPos, endPos);也就是說(shuō)測(cè)試模擬的是用戶從第一個(gè)嵌套段落的開(kāi)頭框選到第三個(gè)嵌套段落的結(jié)尾這一真實(shí)操作選區(qū)內(nèi)恰好包含三個(gè)兄弟子塊BlockNote 中嵌套塊以blockGroup包裹它們都是父blockContainer的 children。選區(qū)起點(diǎn)與終點(diǎn)均由TextSelection描述屬于純文本選區(qū)而非節(jié)點(diǎn)選區(qū)。同一文檔結(jié)構(gòu)在 copyTestInstances.ts 中被childToParent、partialChildToParent、childrenToNextParent等用例復(fù)用分別驗(yàn)證跨父子層級(jí)、部分選區(qū)、跨到下一個(gè)父塊等不同邊界的復(fù)制行為multipleChildren專(zhuān)注驗(yàn)證純子塊序列這一最樸素的情形。執(zhí)行器快照如何被斷言快照比對(duì)發(fā)生在執(zhí)行器 copyTestExecutors.ts 的testCopyMarkdown中export const testCopyMarkdown async B, I, S( editor: BlockNoteEditorB, I, S, testCase: CopyTestCaseB, I, S, ) { initTestEditor(editor, testCase.document, testCase.getCopySelection); const { markdown } selectedFragmentToHTML(editor.prosemirrorView, editor); await expect(markdown).toMatchFileSnapshot( ./__snapshots__/text/plain/${testCase.name}.md, ); };流程分三步initTestEditor載入用例文檔并應(yīng)用getCopySelection構(gòu)造的選區(qū)調(diào)用selectedFragmentToHTML從 ProseMirror 視圖中取出markdown字段用toMatchFileSnapshot將實(shí)際輸出與__snapshots__/text/plain/multipleChildren.md逐字符比對(duì)不匹配即測(cè)試失敗。值得注意的是copyTestInstancesMarkdown是通過(guò)copyTestInstancesHTML.map(...)映射生成的——即所有 HTML 用例的選區(qū)與文檔完全復(fù)用只是執(zhí)行器換成testCopyMarkdown從而保證兩種剪貼板格式在相同選區(qū)下各自擁有一份獨(dú)立契約HTML 快照在__snapshots__/text/html/純文本快照在__snapshots__/text/plain/??煺瘴募?md后綴存儲(chǔ)表明text/plain位置的內(nèi)容并非原始選區(qū)文本而是 GFM 兼容的 Markdown。底層原理selectedFragmentToHTML 的三格式產(chǎn)出selectedFragmentToHTML是復(fù)制功能的樞紐實(shí)現(xiàn)在 copyExtension.ts。它一次返回三個(gè)字段return { clipboardHTML, externalHTML, markdown };clipboardHTML使用 ProseMirror 默認(rèn)的view.serializeForClipboard(...)保留完整內(nèi)部結(jié)構(gòu)與屬性供 BlockNote 內(nèi)部粘貼往返MIME 類(lèi)型為blocknote/htmlexternalHTML通過(guò)fragmentToExternalHTML將選區(qū)轉(zhuǎn)換為面向外部應(yīng)用的語(yǔ)義化 HTMLMIME 類(lèi)型為text/htmlmarkdown即本快照斷言的目標(biāo)MIME 類(lèi)型為text/plain。三者的寫(xiě)入發(fā)生在copyToClipboard中copyExtension.tsevent.clipboardData!.setData(blocknote/html, clipboardHTML); event.clipboardData!.setData(text/html, externalHTML); event.clipboardData!.setData(text/plain, markdown);這段邏輯掛在 ProseMirror 插件的handleDOMEvents.copy/cut上并在dragstart中同樣寫(xiě)入dataTransfer。因此用戶在 BlockNote 中執(zhí)行復(fù)制、剪切或拖拽時(shí)剪貼板都會(huì)帶上這三種格式粘貼回 BlockNote 用blocknote/html粘貼到 Word/Google Docs 等富文本應(yīng)用用text/html粘貼到純文本編輯器、終端或筆記軟件則使用text/plain的 Markdown。markdown字段的生成在 copyExtension.tsconst isPurelyInsideCodeBlock $from.sameParent($to) parentBlockSpec?.implementation.meta?.code true; const markdown isPurelyInsideCodeBlock ? view.state.doc.textBetween($from.pos, $to.pos) : cleanHTMLToMarkdown(externalHTML);即僅當(dāng)選區(qū)完整落在代碼塊內(nèi)部時(shí)text/plain直接輸出原始選中文本保留換行、不轉(zhuǎn)義反引號(hào)確保代碼塊內(nèi)容可原樣回貼其余一切場(chǎng)景都走cleanHTMLToMarkdown(externalHTML)先把選區(qū)轉(zhuǎn)成 external HTML再將該 HTML 轉(zhuǎn)換為 Markdown。multipleChildren用例選區(qū)落在普通段落上因此走的是HTML → Markdown轉(zhuǎn)換鏈路。Markdown 序列化鏈路從 external HTML 到 GFMcleanHTMLToMarkdown定義于 markdownExporter.ts它在轉(zhuǎn)換前先移除 external HTML 導(dǎo)出器為保住空塊而注入的占位字符EMPTY_BLOCK_PLACEHOLDER避免空段落粘貼后出現(xiàn)殘留字符然后把干凈的 HTML 交給htmlToMarkdown。真正的序列化器是 htmlToMarkdown.ts它沒(méi)有采用 unified/rehype-remark 管線而是基于 DOM 的手寫(xiě)實(shí)現(xiàn)先用臨時(shí)div解析 HTML瀏覽器與 JSDOM 環(huán)境通用再對(duì)節(jié)點(diǎn)樹(shù)做遞歸序列化其中p標(biāo)簽輸出為段落文本 \n\n見(jiàn)serializeParagraphh1–h6輸出為#–######標(biāo)題serializeHeadingul/ol輸出*/1.列表serializeUnorderedList/serializeOrderedListpre輸出帶圍欄的代碼塊serializeCodeBlocktable輸出 GFM 表格serializeTablehr輸出***。序列化上下文SerializeContext攜帶indent與inListItem兩個(gè)狀態(tài)前者用于列表嵌套縮進(jìn)后者用于抑制列表項(xiàng)內(nèi)連續(xù)段落產(chǎn)生的多余空行避免把緊湊列表意外變成寬松列表loose list。把這份輸出契約放回multipleChildren.md即可對(duì)號(hào)入座三個(gè)嵌套段落對(duì)應(yīng)的三個(gè)p節(jié)點(diǎn)各被序列化為一行文本加一個(gè)空行最終得到三段以空行分隔的扁平段落——快照正是serializeParagraph行為的直接證據(jù)。嵌套與扁平化的取舍與其他快照的橫向?qū)φ瞻裮ultipleChildren.md與同目錄其他快照對(duì)照能更清楚地看出text/plain的策略是內(nèi)容完整、結(jié)構(gòu)扁平childToParent.md 輸出Paragraph 1與Nested Paragraph 1兩個(gè)扁平段落說(shuō)明跨層級(jí)選區(qū)同樣不保留嵌套層級(jí)childrenToNextParent.md 輸出 4 個(gè)扁平段落3 個(gè)嵌套段落 下一個(gè)父塊跨塊邊界同樣被壓平basicBlocks.md 則展示了完整語(yǔ)法覆蓋# Heading 1、1.有序列表、*無(wú)序列表、* [ ]任務(wù)列表、javascript 圍欄代碼塊、GFM 表格以及***分隔線。這些快照共同構(gòu)成 text/plain 的黃金樣本集嵌套關(guān)系在純文本語(yǔ)境下沒(méi)有等價(jià)表達(dá)因此 BlockNote 選擇按文檔順序扁平展開(kāi)而塊類(lèi)型標(biāo)題、列表、代碼、表格則通過(guò)標(biāo)準(zhǔn) Markdown 語(yǔ)法保留。這保證用戶把 BlockNote 內(nèi)容粘貼進(jìn) Typora、Obsidian、GitHub Issue 或任意 Markdown 編輯器時(shí)得到的都是語(yǔ)義正確的文本而非帶縮進(jìn)的原始 HTML 或丟失結(jié)構(gòu)的裸文本。小結(jié)一份 5 行快照背后的完整工程multipleChildren.md雖只有 5 行但它錨定了 BlockNote 復(fù)制功能的一條關(guān)鍵契約嵌套塊復(fù)制到純文本剪貼板時(shí)輸出為扁平、空行分隔的 Markdown 段落。支撐這一契約的是完整的三格式剪貼板架構(gòu)blocknote/htmltext/htmltext/plain、selectedFragmentToHTML的統(tǒng)一選區(qū)處理、代碼塊特殊分支以及 DOM 驅(qū)動(dòng)的htmlToMarkdown序列化器。對(duì)于想要擴(kuò)展 BlockNote 剪貼板行為或?yàn)槠湫略鰤K類(lèi)型貢獻(xiàn)快照用例的開(kāi)發(fā)者copyTestInstances.ts 與 copyTestExecutors.ts 是理解測(cè)試約定的最佳起點(diǎn)copyExtension.ts 與 htmlToMarkdown.ts 則是深入實(shí)現(xiàn)細(xì)節(jié)的必讀源碼。贊分享前端富文本UI組件AI 應(yīng)用【免費(fèi)下載鏈接】BlockNoteA React Rich Text Editor thats block-based (Notion style) and extensible. Built on top of Prosemirror and Tiptap.項(xiàng)目地址https://gitcode.com/gh_mirrors/bl/BlockNote點(diǎn)擊查看免費(fèi)下載相關(guān)推薦amis 邊框?qū)挾裙ぞ哳?lèi)全解析border / border-t / border-r 等 24 個(gè) Class 的用法與 SCSS 實(shí)現(xiàn)原理amis 邊框?qū)挾裙ぞ哳?lèi)全解析border / border t / border r 等 24 個(gè) Class 的用法與 SCSS 實(shí)現(xiàn)原理 導(dǎo)讀 在 am前端富文本UI組件AI 應(yīng)用Data-Science-For-Beginners 數(shù)據(jù)集分類(lèi)作業(yè)詳解從結(jié)構(gòu)、價(jià)值到來(lái)源的三維度數(shù)據(jù)判定方法Data Science For Beginners 數(shù)據(jù)集分類(lèi)作業(yè)詳解從結(jié)構(gòu)、價(jià)值到來(lái)源的三維度數(shù)據(jù)判定方法 本文以 Data Science For Be前端富文本UI組件AI 應(yīng)用在 OpenSandbox 中運(yùn)行 Claude CodePython SDK 注入 CLI、Headless 調(diào)用與會(huì)話恢復(fù)的完整實(shí)踐在 OpenSandbox 中運(yùn)行 Claude CodePython SDK 注入 CLI、Headless 調(diào)用與會(huì)話恢復(fù)的完整實(shí)踐 本文以 OpenSa前端富文本UI組件AI 應(yīng)用上一篇htty錯(cuò)誤處理指南如何快速解決常見(jiàn)的HTTP交互問(wèn)題 下一篇5分鐘上手FarPlaneTwo從安裝到實(shí)現(xiàn)100000方塊渲染距離的完整指南創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考