diff --git a/packages/editor/src/serialization/html/deserializeFromHtml.ts b/packages/editor/src/serialization/html/deserializeFromHtml.ts index 6275c3a6a9..753edaf82c 100644 --- a/packages/editor/src/serialization/html/deserializeFromHtml.ts +++ b/packages/editor/src/serialization/html/deserializeFromHtml.ts @@ -14,8 +14,6 @@ import { PARAGRAPH_ELEMENT_TYPE, type ParagraphElement } from "../../plugins/par import { SECTION_ELEMENT_TYPE } from "../../plugins/section/sectionTypes"; import { isElementOfType } from "../../utils/isElementType"; -// TODO: This entire file should be refactored and reconsidered. Our current deserialization is too complex. - const createDefaultNoop = (): Descendant[] => { return [ { @@ -29,16 +27,7 @@ const createEmptyValue = (): Descendant[] => { return [ { type: SECTION_ELEMENT_TYPE, - children: [ - { - type: PARAGRAPH_ELEMENT_TYPE, - children: [ - { - text: "", - }, - ], - }, - ], + children: [{ type: PARAGRAPH_ELEMENT_TYPE, children: [{ text: "" }] }], }, ]; }; @@ -74,11 +63,7 @@ export const deserializeFromHtml = ( if (rule.deserialize) { // Already checked that nodeType === 1 -> el must be of type HTMLElement. const ret = rule.deserialize(el as HTMLElement, children, rule.options); - if (ret === undefined) { - continue; - } else { - return ret; - } + if (ret !== undefined) return ret; } } @@ -89,70 +74,57 @@ export const deserializeFromHtml = ( options.noop ? `
${html}
` : html, "text/html", ); - const nodes = Array.from(document.body.children).map(deserialize); - - const normalizedNodes = nodes - .map((n) => { - const node = Node.isNodeList(n) ? n[0] : n; - return node ? wrapMixedChildren(node, options.blocks, options.inlines) : undefined; - }) - .filter((n) => !!n); - return normalizedNodes; + return Array.from(document.body.children).flatMap((el) => { + const n = deserialize(el); + const node = Node.isNodeList(n) ? n[0] : n; + return node ? [wrapMixedChildren(node, options.blocks, options.inlines)] : []; + }); }; const addEmptyTextNodes = (node: Element) => { - const children = node.children; - let lastWasText = false; - - // Iterating in reverse ensures that we add empty text nodes only when necessary - for (let i = 0; i < children.length; i++) { - const child = children[i]; - const currentIsText = Node.isText(child); - - if (!currentIsText && !lastWasText) { - children.splice(i, 0, { text: "" }); - i++; // Skip next iteration since we inserted a new child + const withTextNodes = node.children.reduce((acc, child) => { + if (!Node.isText(child) && (acc.length === 0 || !Node.isText(acc[acc.length - 1]))) { + acc.push({ text: "" }); } - lastWasText = currentIsText; - } + acc.push(child); + return acc; + }, []); - // Ensure the last child is a text node - if (!Node.isText(children[children.length - 1])) { - children.push({ text: "" }); + if (!Node.isText(withTextNodes[withTextNodes.length - 1])) { + withTextNodes.push({ text: "" }); } + + node.children = withTextNodes as Element["children"]; }; const addEmptyParagraphs = (node: Element, blocks: ElementType[]) => { - const children = node.children; - let lastWasBlock = false; - - for (let i = 0; i < children.length; i++) { - const child = children[i]; - const currentIsBlock = isElementOfType(child, blocks); - - if (currentIsBlock && (i === 0 || lastWasBlock)) { - children.splice(i, 0, { type: "paragraph", children: [{ text: "" }] }); - i++; // Skip next iteration since we inserted a new paragraph + const withParagraphs = node.children.reduce((acc, child) => { + if (isElementOfType(child, blocks) && (acc.length === 0 || isElementOfType(acc[acc.length - 1], blocks))) { + acc.push({ type: PARAGRAPH_ELEMENT_TYPE, children: [{ text: "" }] }); } + acc.push(child); + return acc; + }, []); - lastWasBlock = currentIsBlock; + if (isElementOfType(withParagraphs[withParagraphs.length - 1], blocks)) { + withParagraphs.push({ type: PARAGRAPH_ELEMENT_TYPE, children: [{ text: "" }] }); } - // Ensure the last child is a paragraph if needed - if (isElementOfType(children[children.length - 1], blocks)) { - children.push({ type: "paragraph", children: [{ text: "" }] }); - } + node.children = withParagraphs as Element["children"]; }; +const isBlockElement = (node: Descendant, inlines: ElementType[]) => + Node.isElement(node) && !inlines.includes(node.type); + const wrapMixedChildren = (node: Descendant, blocks: ElementType[], inlines: ElementType[]): Descendant => { if (!Node.isElement(node)) return node; const children = node.children; - const blockChildren = children.filter((child) => Node.isElement(child) && !inlines.includes(child.type)); - const mixed = !!blockChildren.length && blockChildren.length !== children.length; + const hasBlockChildren = children.some((c) => isBlockElement(c, inlines)); + const mixed = hasBlockChildren && children.some((c) => !isBlockElement(c, inlines)); if (!mixed) { node.children = children.map((child) => wrapMixedChildren(child, blocks, inlines)); - if (!blockChildren.length && !!children.length) { + if (!hasBlockChildren && !!children.length) { addEmptyTextNodes(node); } else { addEmptyParagraphs(node, blocks); @@ -165,24 +137,21 @@ const wrapMixedChildren = (node: Descendant, blocks: ElementType[], inlines: Ele let openWrapperBlock: ParagraphElement | null = null; for (const child of children) { if (Node.isText(child) || (Node.isElement(child) && inlines.includes(child.type))) { - // TODO: Consider trimming - if (Node.string(child) === "" || Node.string(child) === " ") { + if (Node.string(child).trim() === "") { continue; } if (!openWrapperBlock) { - openWrapperBlock = slatejsx("element", { type: "paragraph" }, []) as ParagraphElement; + openWrapperBlock = slatejsx("element", { type: PARAGRAPH_ELEMENT_TYPE }, []) as ParagraphElement; cleanNodes.push(openWrapperBlock); } openWrapperBlock.children.push(child); } else { openWrapperBlock = null; - if (child.type === "paragraph" && child.children.length === 0) continue; + if (child.type === PARAGRAPH_ELEMENT_TYPE && child.children.length === 0) continue; cleanNodes.push(child); } } - addEmptyParagraphs(node, blocks); - // Process the cleaned-up nodes recursively node.children = cleanNodes.map((child) => wrapMixedChildren(child, blocks, inlines)); return node;