fix(editor): should preserve format in <p> when importing html (#12275)

Closes: [BS-3485](https://linear.app/affine-design/issue/BS-3485/粘贴-html-格式的内容时,紧邻着-bold-text-的普通文本会丢失空格)

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## 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.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
donteatfriedrice
2025-05-15 00:50:10 +00:00
parent 278aa8f7a0
commit d56d46d8d6
2 changed files with 82 additions and 1 deletions

View File

@@ -2360,6 +2360,65 @@ describe('html to snapshot', () => {
expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot); expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot);
}); });
test('should preserve space in p', async () => {
const html = template(
`<p>A <b>bold text</b> followed by a <i>italic text</i></p>`
);
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 () => { test('span nested in p', async () => {
const html = template( const html = template(
`<p><span>aaa</span><span>bbb</span><span>ccc</span></p>` `<p><span>aaa</span><span>bbb</span><span>ccc</span></p>`

View File

@@ -5,6 +5,24 @@ import {
import { collapseWhiteSpace } from 'collapse-white-space'; import { collapseWhiteSpace } from 'collapse-white-space';
import type { Element } from 'hast'; 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 => { const isElement = (ast: HtmlAST): ast is Element => {
return ast.type === 'element'; return ast.type === 'element';
}; };
@@ -22,12 +40,16 @@ export const htmlTextToDeltaMatcher = HtmlASTToDeltaExtension({
return []; return [];
} }
const { options } = context; const { options } = context;
options.trim ??= true; options.trim ??= false;
if (options.pre) { if (options.pre) {
return [{ insert: ast.value }]; return [{ insert: ast.value }];
} }
if (isEmptyText(ast)) {
return [];
}
const value = options.trim const value = options.trim
? collapseWhiteSpace(ast.value, { trim: options.trim }) ? collapseWhiteSpace(ast.value, { trim: options.trim })
: collapseWhiteSpace(ast.value); : collapseWhiteSpace(ast.value);