[lexical-html] Feature: Support copy pasting block and inline nodes properly (#5857)

This commit is contained in:
Sherry
2024-05-03 10:18:51 +08:00
committed by GitHub
parent a0bb9b056a
commit 0b9ef956a4
12 changed files with 545 additions and 43 deletions

View File

@ -168,13 +168,7 @@ export class CodeNode extends ElementNode {
const td = node as HTMLTableCellElement;
const table: HTMLTableElement | null = td.closest('table');
if (isGitHubCodeCell(td)) {
return {
conversion: convertTableCellElement,
priority: 3,
};
}
if (table && isGitHubCodeTable(table)) {
if (isGitHubCodeCell(td) || (table && isGitHubCodeTable(table))) {
// Return a no-op if it's a table cell in a code table, but not a code line.
// Otherwise it'll fall back to the T
return {
@ -348,13 +342,6 @@ function convertDivElement(domNode: Node): DOMConversionOutput {
};
}
return {
after: (childLexicalNodes) => {
const domParent = domNode.parentNode;
if (domParent != null && domNode !== domParent.lastChild) {
childLexicalNodes.push($createLineBreakNode());
}
return childLexicalNodes;
},
node: isCode ? $createCodeNode() : null,
};
}
@ -367,22 +354,6 @@ function convertCodeNoop(): DOMConversionOutput {
return {node: null};
}
function convertTableCellElement(domNode: Node): DOMConversionOutput {
// domNode is a <td> since we matched it by nodeName
const cell = domNode as HTMLTableCellElement;
return {
after: (childLexicalNodes) => {
if (cell.parentNode && cell.parentNode.nextSibling) {
// Append newline between code lines
childLexicalNodes.push($createLineBreakNode());
}
return childLexicalNodes;
},
node: null,
};
}
function isCodeElement(div: HTMLElement): boolean {
return div.style.fontFamily.match('monospace') !== null;
}

View File

@ -19,8 +19,18 @@ import {
$cloneWithProperties,
$sliceSelectedTextNodeContent,
} from '@lexical/selection';
import {isHTMLElement} from '@lexical/utils';
import {$getRoot, $isElementNode, $isTextNode} from 'lexical';
import {isBlockDomNode, isHTMLElement} from '@lexical/utils';
import {
$createLineBreakNode,
$createParagraphNode,
$getRoot,
$isBlockElementNode,
$isElementNode,
$isRootOrShadowRoot,
$isTextNode,
ArtificialNode__DO_NOT_USE,
ElementNode,
} from 'lexical';
/**
* How you parse your html string to get a document is left up to you. In the browser you can use the native
@ -33,15 +43,22 @@ export function $generateNodesFromDOM(
): Array<LexicalNode> {
const elements = dom.body ? dom.body.childNodes : [];
let lexicalNodes: Array<LexicalNode> = [];
const allArtificialNodes: Array<ArtificialNode__DO_NOT_USE> = [];
for (let i = 0; i < elements.length; i++) {
const element = elements[i];
if (!IGNORE_TAGS.has(element.nodeName)) {
const lexicalNode = $createNodesFromDOM(element, editor);
const lexicalNode = $createNodesFromDOM(
element,
editor,
allArtificialNodes,
false,
);
if (lexicalNode !== null) {
lexicalNodes = lexicalNodes.concat(lexicalNode);
}
}
}
unwrapArtificalNodes(allArtificialNodes);
return lexicalNodes;
}
@ -161,7 +178,6 @@ function getConversionFunction(
if (cachedConversions !== undefined) {
for (const cachedConversion of cachedConversions) {
const domConversion = cachedConversion(domNode);
if (
domConversion !== null &&
(currentConversion === null ||
@ -180,6 +196,8 @@ const IGNORE_TAGS = new Set(['STYLE', 'SCRIPT']);
function $createNodesFromDOM(
node: Node,
editor: LexicalEditor,
allArtificialNodes: Array<ArtificialNode__DO_NOT_USE>,
hasBlockAncestorLexicalNode: boolean,
forChildMap: Map<string, DOMChildConversion> = new Map(),
parentLexicalNode?: LexicalNode | null | undefined,
): Array<LexicalNode> {
@ -234,11 +252,20 @@ function $createNodesFromDOM(
const children = node.childNodes;
let childLexicalNodes = [];
const hasBlockAncestorLexicalNodeForChildren =
currentLexicalNode != null && $isRootOrShadowRoot(currentLexicalNode)
? false
: (currentLexicalNode != null &&
$isBlockElementNode(currentLexicalNode)) ||
hasBlockAncestorLexicalNode;
for (let i = 0; i < children.length; i++) {
childLexicalNodes.push(
...$createNodesFromDOM(
children[i],
editor,
allArtificialNodes,
hasBlockAncestorLexicalNodeForChildren,
new Map(forChildMap),
currentLexicalNode,
),
@ -249,6 +276,22 @@ function $createNodesFromDOM(
childLexicalNodes = postTransform(childLexicalNodes);
}
if (isBlockDomNode(node)) {
if (!hasBlockAncestorLexicalNodeForChildren) {
childLexicalNodes = wrapContinuousInlines(
node,
childLexicalNodes,
$createParagraphNode,
);
} else {
childLexicalNodes = wrapContinuousInlines(node, childLexicalNodes, () => {
const artificialNode = new ArtificialNode__DO_NOT_USE();
allArtificialNodes.push(artificialNode);
return artificialNode;
});
}
}
if (currentLexicalNode == null) {
// If it hasn't been converted to a LexicalNode, we hoist its children
// up to the same level as it.
@ -263,3 +306,49 @@ function $createNodesFromDOM(
return lexicalNodes;
}
function wrapContinuousInlines(
domNode: Node,
nodes: Array<LexicalNode>,
createWrapperFn: () => ElementNode,
): Array<LexicalNode> {
const out: Array<LexicalNode> = [];
let continuousInlines: Array<LexicalNode> = [];
// wrap contiguous inline child nodes in para
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if ($isBlockElementNode(node)) {
out.push(node);
} else {
continuousInlines.push(node);
if (
i === nodes.length - 1 ||
(i < nodes.length - 1 && $isBlockElementNode(nodes[i + 1]))
) {
const wrapper = createWrapperFn();
wrapper.append(...continuousInlines);
out.push(wrapper);
continuousInlines = [];
}
}
}
return out;
}
function unwrapArtificalNodes(
allArtificialNodes: Array<ArtificialNode__DO_NOT_USE>,
) {
for (const node of allArtificialNodes) {
if (node.getNextSibling() instanceof ArtificialNode__DO_NOT_USE) {
node.insertAfter($createLineBreakNode());
}
}
// Replace artificial node with it's children
for (const node of allArtificialNodes) {
const children = node.getChildren();
for (const child of children) {
node.insertBefore(child);
}
node.remove();
}
}

View File

@ -1027,7 +1027,11 @@ test.describe('CodeBlock', () => {
<p
class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr"
dir="ltr">
<span data-lexical-text="true">XDS_RICH_TEXT_AREA</span>
<span
class="PlaygroundEditorTheme__textStrikethrough"
data-lexical-text="true">
XDS_RICH_TEXT_AREA
</span>
</p>
</td>
<td class="PlaygroundEditorTheme__tableCell">
@ -1120,7 +1124,7 @@ test.describe('CodeBlock', () => {
{
expectedHTML: EXPECTED_HTML_GOOGLE_SPREADSHEET,
name: 'Google Spreadsheet',
pastedHTML: `<google-sheets-html-origin><style type="text/css"><!--td {border: 1px solid #cccccc;}br {mso-data-placement:same-cell;}--></style><table xmlns="http://www.w3.org/1999/xhtml" cellspacing="0" cellpadding="0" dir="ltr" border="1" style="table-layout:fixed;font-size:10pt;font-family:Arial;width:0px;border-collapse:collapse;border:none" data-sheets-root="1"><colgroup><col width="100"/><col width="210"/><col width="100"/></colgroup><tbody><tr style="height:21px;"><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;font-weight:bold;" data-sheets-value="{&quot;1&quot;:2,&quot;2&quot;:&quot;Surface&quot;}">Surface</td><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;font-style:italic;" data-sheets-value="{&quot;1&quot;:2,&quot;2&quot;:&quot;MWP_WORK_LS_COMPOSER&quot;}">MWP_WORK_LS_COMPOSER</td><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;text-decoration:underline;text-align:right;" data-sheets-value="{&quot;1&quot;:3,&quot;3&quot;:77349}">77349</td></tr><tr style="height:21px;"><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;" data-sheets-value="{&quot;1&quot;:2,&quot;2&quot;:&quot;Lexical&quot;}">Lexical</td><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;" data-sheets-value="{&quot;1&quot;:2,&quot;2&quot;:&quot;XDS_RICH_TEXT_AREA&quot;}">XDS_RICH_TEXT_AREA</td><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;" data-sheets-value="{&quot;1&quot;:2,&quot;2&quot;:&quot;sdvd sdfvsfs&quot;}" data-sheets-textstyleruns="{&quot;1&quot;:0}{&quot;1&quot;:5,&quot;2&quot;:{&quot;5&quot;:1}}"><span style="font-size:10pt;font-family:Arial;font-style:normal;">sdvd </span><span style="font-size:10pt;font-family:Arial;font-weight:bold;font-style:normal;">sdfvsfs</span></td></tr></tbody></table>`,
pastedHTML: `<google-sheets-html-origin><style type="text/css"><!--td {border: 1px solid #cccccc;}br {mso-data-placement:same-cell;}--></style><table xmlns="http://www.w3.org/1999/xhtml" cellspacing="0" cellpadding="0" dir="ltr" border="1" style="table-layout:fixed;font-size:10pt;font-family:Arial;width:0px;border-collapse:collapse;border:none" data-sheets-root="1"><colgroup><col width="100"/><col width="189"/><col width="171"/></colgroup><tbody><tr style="height:21px;"><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;font-weight:bold;" data-sheets-value="{&quot;1&quot;:2,&quot;2&quot;:&quot;Surface&quot;}">Surface</td><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;font-style:italic;" data-sheets-value="{&quot;1&quot;:2,&quot;2&quot;:&quot;MWP_WORK_LS_COMPOSER&quot;}">MWP_WORK_LS_COMPOSER</td><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;text-decoration:underline;text-align:right;" data-sheets-value="{&quot;1&quot;:3,&quot;3&quot;:77349}">77349</td></tr><tr style="height:21px;"><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;" data-sheets-value="{&quot;1&quot;:2,&quot;2&quot;:&quot;Lexical&quot;}">Lexical</td><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;text-decoration:line-through;" data-sheets-value="{&quot;1&quot;:2,&quot;2&quot;:&quot;XDS_RICH_TEXT_AREA&quot;}">XDS_RICH_TEXT_AREA</td><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;" data-sheets-value="{&quot;1&quot;:2,&quot;2&quot;:&quot;sdvd sdfvsfs&quot;}" data-sheets-textstyleruns="{&quot;1&quot;:0}{&quot;1&quot;:5,&quot;2&quot;:{&quot;5&quot;:1}}"><span style="font-size:10pt;font-family:Arial;font-style:normal;">sdvd </span><span style="font-size:10pt;font-family:Arial;font-weight:bold;font-style:normal;">sdfvsfs</span></td></tr></tbody></table>`,
},
];

View File

@ -275,4 +275,229 @@ test.describe('HTML CopyAndPaste', () => {
`,
);
});
test('Copy + paste single div', async ({page, isPlainText}) => {
test.skip(isPlainText);
await focusEditor(page);
const clipboard = {
'text/html': `
123
<div>
456
</div>`,
};
await pasteFromClipboard(page, clipboard);
await assertHTML(
page,
html`
<p class="PlaygroundEditorTheme__paragraph">
<span data-lexical-text="true">123</span>
</p>
<p class="PlaygroundEditorTheme__paragraph">
<span data-lexical-text="true">456</span>
</p>
`,
);
await assertSelection(page, {
anchorOffset: 3,
anchorPath: [1, 0, 0],
focusOffset: 3,
focusPath: [1, 0, 0],
});
});
test('Copy + paste nested divs', async ({page, isPlainText}) => {
test.skip(isPlainText);
await focusEditor(page);
const clipboard = {
'text/html': html`
<div>
a
<div>
b b
<div>
c
<div>
<div></div>
z
</div>
</div>
d e
</div>
fg
</div>
`,
};
await pasteFromClipboard(page, clipboard);
await assertHTML(
page,
html`
<p
class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr"
dir="ltr">
<span data-lexical-text="true">a</span>
</p>
<p
class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr"
dir="ltr">
<span data-lexical-text="true">b b</span>
</p>
<p
class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr"
dir="ltr">
<span data-lexical-text="true">c</span>
</p>
<p
class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr"
dir="ltr">
<span data-lexical-text="true">z</span>
</p>
<p
class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr"
dir="ltr">
<span data-lexical-text="true">d e</span>
</p>
<p
class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr"
dir="ltr">
<span data-lexical-text="true">fg</span>
</p>
`,
);
await assertSelection(page, {
anchorOffset: 2,
anchorPath: [5, 0, 0],
focusOffset: 2,
focusPath: [5, 0, 0],
});
});
test('Copy + paste nested div in a span', async ({page, isPlainText}) => {
test.skip(isPlainText);
await focusEditor(page);
const clipboard = {
'text/html': html`
<span>
123
<div>456</div>
</span>
`,
};
await pasteFromClipboard(page, clipboard);
await assertHTML(
page,
html`
<p class="PlaygroundEditorTheme__paragraph">
<span data-lexical-text="true">123</span>
</p>
<p class="PlaygroundEditorTheme__paragraph">
<span data-lexical-text="true">456</span>
</p>
`,
);
await assertSelection(page, {
anchorOffset: 3,
anchorPath: [1, 0, 0],
focusOffset: 3,
focusPath: [1, 0, 0],
});
});
test('Copy + paste nested span in a div', async ({page, isPlainText}) => {
test.skip(isPlainText);
await focusEditor(page);
const clipboard = {
'text/html': html`
<div>
<span>
123
<div>456</div>
</span>
</div>
`,
};
await pasteFromClipboard(page, clipboard);
await assertHTML(
page,
html`
<p class="PlaygroundEditorTheme__paragraph">
<span data-lexical-text="true">123</span>
</p>
<p class="PlaygroundEditorTheme__paragraph">
<span data-lexical-text="true">456</span>
</p>
`,
);
await assertSelection(page, {
anchorOffset: 3,
anchorPath: [1, 0, 0],
focusOffset: 3,
focusPath: [1, 0, 0],
});
});
test('Copy + paste multiple nested spans and divs', async ({
page,
isPlainText,
}) => {
test.skip(isPlainText);
await focusEditor(page);
const clipboard = {
'text/html': html`
<div>
a b
<span>
c d
<span>e</span>
</span>
<div>
f
<span>g h</span>
</div>
</div>
`,
};
await pasteFromClipboard(page, clipboard);
await assertHTML(
page,
html`
<p
class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr"
dir="ltr">
<span data-lexical-text="true">a b c d e</span>
</p>
<p
class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr"
dir="ltr">
<span data-lexical-text="true">f g h</span>
</p>
`,
);
await assertSelection(page, {
anchorOffset: 5,
anchorPath: [1, 0, 0],
focusOffset: 5,
focusPath: [1, 0, 0],
});
});
});

View File

@ -416,4 +416,61 @@ test.describe('HTML Lists CopyAndPaste', () => {
`,
);
});
test('Copy + paste a nested divs in a list', async ({page, isPlainText}) => {
test.skip(isPlainText);
await focusEditor(page);
const clipboard = {
'text/html': html`
<ol>
<li>
1
<div>2</div>
3
</li>
<li>
A
<div>B</div>
C
</li>
</ol>
`,
};
await pasteFromClipboard(page, clipboard);
await assertHTML(
page,
html`
<ol class="PlaygroundEditorTheme__ol1">
<li value="1" class="PlaygroundEditorTheme__listItem">
<span data-lexical-text="true">1</span>
<br />
<span data-lexical-text="true">2</span>
<br />
<span data-lexical-text="true">3</span>
</li>
<li
value="2"
class="PlaygroundEditorTheme__listItem PlaygroundEditorTheme__ltr"
dir="ltr">
<span data-lexical-text="true">A</span>
<br />
<span data-lexical-text="true">B</span>
<br />
<span data-lexical-text="true">C</span>
</li>
</ol>
`,
);
await assertSelection(page, {
anchorOffset: 1,
anchorPath: [0, 1, 4, 0],
focusOffset: 1,
focusPath: [0, 1, 4, 0],
});
});
});

View File

@ -471,4 +471,104 @@ test.describe('HTML Tables CopyAndPaste', () => {
`,
);
});
test('Copy + paste nested block and inline html in a table', async ({
page,
isPlainText,
isCollab,
}) => {
test.skip(isPlainText);
test.fixme(
isCollab,
'Table selection styles are not properly synced to the right hand frame',
);
await focusEditor(page);
const clipboard = {
'text/html': html`
123
<table>
<tbody>
<tr>
<td>
<span>456<span>
</td>
<td>
789
<div>
000
</div>
</td>
</tr>
<tr>
<td>
ABC
<div>
000
<div>
000
</div>
</div>
</td>
<td>
DEF
</td>
</tr>
</tbody>
</table>
`,
};
await pasteFromClipboard(page, clipboard);
await assertHTML(
page,
html`
<p class="PlaygroundEditorTheme__paragraph">
<span data-lexical-text="true">123</span>
</p>
<table class="PlaygroundEditorTheme__table">
<tr>
<td class="PlaygroundEditorTheme__tableCell">
<p class="PlaygroundEditorTheme__paragraph">
<span data-lexical-text="true">456</span>
</p>
</td>
<td class="PlaygroundEditorTheme__tableCell">
<p class="PlaygroundEditorTheme__paragraph">
<span data-lexical-text="true">789</span>
</p>
<p class="PlaygroundEditorTheme__paragraph">
<span data-lexical-text="true">000</span>
</p>
</td>
</tr>
<tr>
<td class="PlaygroundEditorTheme__tableCell">
<p
class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr"
dir="ltr">
<span data-lexical-text="true">ABC</span>
</p>
<p class="PlaygroundEditorTheme__paragraph">
<span data-lexical-text="true">000</span>
</p>
<p class="PlaygroundEditorTheme__paragraph">
<span data-lexical-text="true">000</span>
</p>
</td>
<td class="PlaygroundEditorTheme__tableCell">
<p
class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr"
dir="ltr">
<span data-lexical-text="true">DEF</span>
</p>
</td>
</tr>
</table>
`,
);
});
});

View File

@ -44,7 +44,13 @@ import normalizeClassNames from 'shared/normalizeClassNames';
export {default as markSelection} from './markSelection';
export {default as mergeRegister} from './mergeRegister';
export {default as positionNodeOnRange} from './positionNodeOnRange';
export {$splitNode, isHTMLAnchorElement, isHTMLElement} from 'lexical';
export {
$splitNode,
isBlockDomNode,
isHTMLAnchorElement,
isHTMLElement,
isInlineDomNode,
} from 'lexical';
// Hotfix to export these with inlined types #5918
export const CAN_USE_BEFORE_INPUT: boolean = CAN_USE_BEFORE_INPUT_;
export const CAN_USE_DOM: boolean = CAN_USE_DOM_;

View File

@ -37,6 +37,7 @@ import {
getDOMSelection,
markAllNodesAsDirty,
} from './LexicalUtils';
import {ArtificialNode__DO_NOT_USE} from './nodes/ArtificialNode';
import {DecoratorNode} from './nodes/LexicalDecoratorNode';
import {LineBreakNode} from './nodes/LexicalLineBreakNode';
import {ParagraphNode} from './nodes/LexicalParagraphNode';
@ -421,6 +422,7 @@ export function createEditor(editorConfig?: CreateEditorArgs): LexicalEditor {
LineBreakNode,
TabNode,
ParagraphNode,
ArtificialNode__DO_NOT_USE,
...(config.nodes || []),
];
const {onError, html} = config;

View File

@ -1584,6 +1584,32 @@ export function isHTMLElement(x: Node | EventTarget): x is HTMLElement {
return x.nodeType === 1;
}
/**
*
* @param node - the Dom Node to check
* @returns if the Dom Node is an inline node
*/
export function isInlineDomNode(node: Node) {
const inlineNodes = new RegExp(
/^(a|abbr|acronym|b|cite|code|del|em|i|ins|kbd|label|output|q|ruby|s|samp|span|strong|sub|sup|time|u|tt|var|#text)$/,
'i',
);
return node.nodeName.match(inlineNodes) !== null;
}
/**
*
* @param node - the Dom Node to check
* @returns if the Dom Node is a block node
*/
export function isBlockDomNode(node: Node) {
const blockNodes = new RegExp(
/^(address|article|aside|blockquote|canvas|dd|div|dl|dt|fieldset|figcaption|figure|footer|form|h1|h2|h3|h4|h5|h6|header|hr|li|main|nav|noscript|ol|p|pre|section|table|td|tfoot|ul|video)$/,
'i',
);
return node.nodeName.match(blockNodes) !== null;
}
/**
* This function is for internal use of the library.
* Please do not use it as it may change in the future.

View File

@ -157,11 +157,14 @@ export {
$setSelection,
$splitNode,
getNearestEditorFromDOMNode,
isBlockDomNode,
isHTMLAnchorElement,
isHTMLElement,
isInlineDomNode,
isSelectionCapturedInDecoratorInput,
isSelectionWithinEditor,
} from './LexicalUtils';
export {ArtificialNode__DO_NOT_USE} from './nodes/ArtificialNode';
export {$isDecoratorNode, DecoratorNode} from './nodes/LexicalDecoratorNode';
export {$isElementNode, ElementNode} from './nodes/LexicalElementNode';
export type {SerializedLineBreakNode} from './nodes/LexicalLineBreakNode';

View File

@ -0,0 +1,23 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import type {EditorConfig} from 'lexical';
import {ElementNode} from './LexicalElementNode';
// TODO: Cleanup ArtificialNode__DO_NOT_USE #5966
export class ArtificialNode__DO_NOT_USE extends ElementNode {
static getType(): string {
return 'artificial';
}
createDOM(config: EditorConfig): HTMLElement {
// this isnt supposed to be used and is not used anywhere but defining it to appease the API
const dom = document.createElement('div');
return dom;
}
}

View File

@ -62,6 +62,7 @@ import {
getCachedClassNameArray,
internalMarkSiblingsAsDirty,
isHTMLElement,
isInlineDomNode,
toggleTextFormatType,
} from '../LexicalUtils';
import {$createLineBreakNode} from './LexicalLineBreakNode';
@ -1261,11 +1262,6 @@ function convertTextDOMNode(domNode: Node): DOMConversionOutput {
return {node: $createTextNode(textContent)};
}
const inlineParents = new RegExp(
/^(a|abbr|acronym|b|cite|code|del|em|i|ins|kbd|label|output|q|ruby|s|samp|span|strong|sub|sup|time|u|tt|var)$/,
'i',
);
function findTextInLine(text: Text, forward: boolean): null | Text {
let node: Node = text;
// eslint-disable-next-line no-constant-condition
@ -1284,7 +1280,7 @@ function findTextInLine(text: Text, forward: boolean): null | Text {
if (node.nodeType === DOM_ELEMENT_TYPE) {
const display = (node as HTMLElement).style.display;
if (
(display === '' && node.nodeName.match(inlineParents) === null) ||
(display === '' && !isInlineDomNode(node)) ||
(display !== '' && !display.startsWith('inline'))
) {
return null;