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:
@@ -5,6 +5,12 @@ import type {
|
|||||||
SerializedParagraphNode,
|
SerializedParagraphNode,
|
||||||
SerializedTextNode,
|
SerializedTextNode,
|
||||||
SerializedLineBreakNode,
|
SerializedLineBreakNode,
|
||||||
|
SerializedHeadingNode,
|
||||||
|
SerializedListItemNode,
|
||||||
|
SerializedListNode,
|
||||||
|
SerializedTableRowNode,
|
||||||
|
SerializedTableNode,
|
||||||
|
SerializedTableCellNode,
|
||||||
} from '../../../nodeTypes.js'
|
} from '../../../nodeTypes.js'
|
||||||
import { convertLexicalToPlaintext } from './sync/index.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 {
|
return {
|
||||||
root: {
|
root: {
|
||||||
type: 'root',
|
type: 'root',
|
||||||
@@ -72,7 +154,6 @@ describe('convertLexicalToPlaintext', () => {
|
|||||||
data,
|
data,
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log('plaintext', plaintext)
|
|
||||||
expect(plaintext).toBe('Basic Text')
|
expect(plaintext).toBe('Basic Text')
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -111,4 +192,67 @@ describe('convertLexicalToPlaintext', () => {
|
|||||||
|
|
||||||
expect(plaintext).toBe('Basic Text\tNext Line')
|
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',
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -86,11 +86,25 @@ export function convertLexicalNodesToPlaintext({
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Default plaintext converter heuristic
|
// Default plaintext converter heuristic
|
||||||
if (node.type === 'paragraph') {
|
if (
|
||||||
|
node.type === 'paragraph' ||
|
||||||
|
node.type === 'heading' ||
|
||||||
|
node.type === 'list' ||
|
||||||
|
node.type === 'table'
|
||||||
|
) {
|
||||||
if (plainTextArray?.length) {
|
if (plainTextArray?.length) {
|
||||||
// Only add a new line if there is already text in the array
|
// Only add a new line if there is already text in the array
|
||||||
plainTextArray.push('\n\n')
|
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') {
|
} else if (node.type === 'linebreak') {
|
||||||
plainTextArray.push('\n')
|
plainTextArray.push('\n')
|
||||||
} else if (node.type === 'tab') {
|
} else if (node.type === 'tab') {
|
||||||
|
|||||||
Reference in New Issue
Block a user