From d56d46d8d65f70897de2d24bba3b7f084aa10a49 Mon Sep 17 00:00:00 2001 From: donteatfriedrice Date: Thu, 15 May 2025 00:50:10 +0000 Subject: [PATCH] fix(editor): should preserve format in

when importing html (#12275) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes: [BS-3485](https://linear.app/affine-design/issue/BS-3485/粘贴-html-格式的内容时,紧邻着-bold-text-的普通文本会丢失空格) ## Summary by CodeRabbit - **Bug Fixes** - Improved handling of spaces and whitespace in paragraphs when converting HTML with inline formatting, ensuring spaces are preserved as in the original content. - **Tests** - Added a new test to verify that spaces are correctly preserved in paragraphs containing bold and italic formatting during HTML conversion. --- .../src/__tests__/adapters/html.unit.spec.ts | 59 +++++++++++++++++++ .../preset/src/adapters/html/html-inline.ts | 24 +++++++- 2 files changed, 82 insertions(+), 1 deletion(-) diff --git a/blocksuite/affine/all/src/__tests__/adapters/html.unit.spec.ts b/blocksuite/affine/all/src/__tests__/adapters/html.unit.spec.ts index 1e97ac503..f2aa7b429 100644 --- a/blocksuite/affine/all/src/__tests__/adapters/html.unit.spec.ts +++ b/blocksuite/affine/all/src/__tests__/adapters/html.unit.spec.ts @@ -2360,6 +2360,65 @@ describe('html to snapshot', () => { expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot); }); + test('should preserve space in p', async () => { + const html = template( + `

A bold text followed by a italic text

` + ); + + const blockSnapshot: BlockSnapshot = { + type: 'block', + id: 'matchesReplaceMap[0]', + flavour: 'affine:note', + props: { + xywh: '[0,0,800,95]', + background: DefaultTheme.noteBackgrounColor, + index: 'a0', + hidden: false, + displayMode: NoteDisplayMode.DocAndEdgeless, + }, + children: [ + { + type: 'block', + id: 'matchesReplaceMap[1]', + flavour: 'affine:paragraph', + props: { + type: 'text', + text: { + '$blocksuite:internal:text$': true, + delta: [ + { + insert: 'A ', + }, + { + insert: 'bold text', + attributes: { + bold: true, + }, + }, + { + insert: ' followed by a ', + }, + { + insert: 'italic text', + attributes: { + italic: true, + }, + }, + ], + }, + }, + children: [], + }, + ], + }; + + const htmlAdapter = new HtmlAdapter(createJob(), provider); + const rawBlockSnapshot = await htmlAdapter.toBlockSnapshot({ + file: html, + }); + expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot); + }); + test('span nested in p', async () => { const html = template( `

aaabbbccc

` diff --git a/blocksuite/affine/inlines/preset/src/adapters/html/html-inline.ts b/blocksuite/affine/inlines/preset/src/adapters/html/html-inline.ts index fe53ae8a2..291cb3332 100644 --- a/blocksuite/affine/inlines/preset/src/adapters/html/html-inline.ts +++ b/blocksuite/affine/inlines/preset/src/adapters/html/html-inline.ts @@ -5,6 +5,24 @@ import { import { collapseWhiteSpace } from 'collapse-white-space'; import type { Element } from 'hast'; +/** + * Handle empty text nodes created by HTML parser for styling purposes. + * These nodes typically contain only whitespace/newlines, for example: + * ```json + * { + * "type": "text", + * "value": "\n\n \n \n " + * } + * ``` + * We collapse and trim the whitespace to check if the node is truly empty, + * and return an empty array in that case. + */ +const isEmptyText = (ast: HtmlAST): boolean => { + return ( + ast.type === 'text' && collapseWhiteSpace(ast.value, { trim: true }) === '' + ); +}; + const isElement = (ast: HtmlAST): ast is Element => { return ast.type === 'element'; }; @@ -22,12 +40,16 @@ export const htmlTextToDeltaMatcher = HtmlASTToDeltaExtension({ return []; } const { options } = context; - options.trim ??= true; + options.trim ??= false; if (options.pre) { return [{ insert: ast.value }]; } + if (isEmptyText(ast)) { + return []; + } + const value = options.trim ? collapseWhiteSpace(ast.value, { trim: options.trim }) : collapseWhiteSpace(ast.value);