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);