fix(richtext-lexical): add missing line-breaks to plaintext conversion (#11951)

### What?
Adds line-breaks after headings, lists, list items, tables, table rows,
and table cells when converting lexical content to plaintext.

### Why?
Currently text from those nodes is concatenated without a separator.

### How?
Adds handling for these nodes to the plain text converter.
This commit is contained in:
Tobias Odendahl
2025-05-02 20:24:24 +02:00
committed by GitHub
parent 1ef1c5564d
commit ddf40d59ac
2 changed files with 161 additions and 3 deletions

View File

@@ -5,6 +5,12 @@ import type {
SerializedParagraphNode,
SerializedTextNode,
SerializedLineBreakNode,
SerializedHeadingNode,
SerializedListItemNode,
SerializedListNode,
SerializedTableRowNode,
SerializedTableNode,
SerializedTableCellNode,
} from '../../../nodeTypes.js'
import { convertLexicalToPlaintext } from './sync/index.js'
@@ -51,7 +57,83 @@ function paragraphNode(children: DefaultNodeTypes[]): SerializedParagraphNode {
}
}
function rootNode(nodes: DefaultNodeTypes[]): DefaultTypedEditorState {
function headingNode(children: DefaultNodeTypes[]): SerializedHeadingNode {
return {
type: 'heading',
children,
direction: 'ltr',
format: '',
indent: 0,
textFormat: 0,
tag: 'h1',
version: 1,
}
}
function listItemNode(children: DefaultNodeTypes[]): SerializedListItemNode {
return {
type: 'listitem',
children,
checked: false,
direction: 'ltr',
format: '',
indent: 0,
value: 0,
version: 1,
}
}
function listNode(children: DefaultNodeTypes[]): SerializedListNode {
return {
type: 'list',
children,
direction: 'ltr',
format: '',
indent: 0,
listType: 'bullet',
start: 0,
tag: 'ul',
version: 1,
}
}
function tableNode(children: (DefaultNodeTypes | SerializedTableRowNode)[]): SerializedTableNode {
return {
type: 'table',
children,
direction: 'ltr',
format: '',
indent: 0,
version: 1,
}
}
function tableRowNode(
children: (DefaultNodeTypes | SerializedTableCellNode)[],
): SerializedTableRowNode {
return {
type: 'tablerow',
children,
direction: 'ltr',
format: '',
indent: 0,
version: 1,
}
}
function tableCellNode(children: DefaultNodeTypes[]): SerializedTableCellNode {
return {
type: 'tablecell',
children,
direction: 'ltr',
format: '',
indent: 0,
headerState: 0,
version: 1,
}
}
function rootNode(nodes: (DefaultNodeTypes | SerializedTableNode)[]): DefaultTypedEditorState {
return {
root: {
type: 'root',
@@ -72,7 +154,6 @@ describe('convertLexicalToPlaintext', () => {
data,
})
console.log('plaintext', plaintext)
expect(plaintext).toBe('Basic Text')
})
@@ -111,4 +192,67 @@ describe('convertLexicalToPlaintext', () => {
expect(plaintext).toBe('Basic Text\tNext Line')
})
it('ensure new lines are added between paragraphs', () => {
const data: DefaultTypedEditorState = rootNode([
paragraphNode([textNode('Basic text')]),
paragraphNode([textNode('Next block-node')]),
])
const plaintext = convertLexicalToPlaintext({
data,
})
expect(plaintext).toBe('Basic text\n\nNext block-node')
})
it('ensure new lines are added between heading nodes', () => {
const data: DefaultTypedEditorState = rootNode([
headingNode([textNode('Basic text')]),
headingNode([textNode('Next block-node')]),
])
const plaintext = convertLexicalToPlaintext({
data,
})
expect(plaintext).toBe('Basic text\n\nNext block-node')
})
it('ensure new lines are added between list items and lists', () => {
const data: DefaultTypedEditorState = rootNode([
listNode([listItemNode([textNode('First item')]), listItemNode([textNode('Second item')])]),
listNode([listItemNode([textNode('Next list')])]),
])
const plaintext = convertLexicalToPlaintext({
data,
})
expect(plaintext).toBe('First item\nSecond item\n\nNext list')
})
it('ensure new lines are added between tables, table rows, and table cells', () => {
const data: DefaultTypedEditorState = rootNode([
tableNode([
tableRowNode([
tableCellNode([textNode('Cell 1, Row 1')]),
tableCellNode([textNode('Cell 2, Row 1')]),
]),
tableRowNode([
tableCellNode([textNode('Cell 1, Row 2')]),
tableCellNode([textNode('Cell 2, Row 2')]),
]),
]),
tableNode([tableRowNode([tableCellNode([textNode('Cell in Table 2')])])]),
])
const plaintext = convertLexicalToPlaintext({
data,
})
expect(plaintext).toBe(
'Cell 1, Row 1 | Cell 2, Row 1\nCell 1, Row 2 | Cell 2, Row 2\n\nCell in Table 2',
)
})
})

View File

@@ -86,11 +86,25 @@ export function convertLexicalNodesToPlaintext({
}
} else {
// Default plaintext converter heuristic
if (node.type === 'paragraph') {
if (
node.type === 'paragraph' ||
node.type === 'heading' ||
node.type === 'list' ||
node.type === 'table'
) {
if (plainTextArray?.length) {
// Only add a new line if there is already text in the array
plainTextArray.push('\n\n')
}
} else if (node.type === 'listitem' || node.type === 'tablerow') {
if (plainTextArray?.length) {
// Only add a new line if there is already text in the array
plainTextArray.push('\n')
}
} else if (node.type === 'tablecell') {
if (plainTextArray?.length) {
plainTextArray.push(' | ')
}
} else if (node.type === 'linebreak') {
plainTextArray.push('\n')
} else if (node.type === 'tab') {