2 * Copyright (c) Meta Platforms, Inc. and affiliates.
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
18 import {$sliceSelectedTextNodeContent} from '@lexical/selection';
19 import {isBlockDomNode, isHTMLElement} from '@lexical/utils';
29 ArtificialNode__DO_NOT_USE,
35 * How you parse your html string to get a document is left up to you. In the browser you can use the native
36 * DOMParser API to generate a document (see clipboard.ts), but to use in a headless environment you can use JSDom
37 * or an equivalent library and pass in the document here.
39 export function $generateNodesFromDOM(
40 editor: LexicalEditor,
42 ): Array<LexicalNode> {
43 const elements = dom.body ? dom.body.childNodes : [];
44 let lexicalNodes: Array<LexicalNode> = [];
45 const allArtificialNodes: Array<ArtificialNode__DO_NOT_USE> = [];
46 for (let i = 0; i < elements.length; i++) {
47 const element = elements[i];
48 if (!IGNORE_TAGS.has(element.nodeName)) {
49 const lexicalNode = $createNodesFromDOM(
55 if (lexicalNode !== null) {
56 lexicalNodes = lexicalNodes.concat(lexicalNode);
61 $unwrapArtificalNodes(allArtificialNodes);
66 export function $generateHtmlFromNodes(
67 editor: LexicalEditor,
68 selection?: BaseSelection | null,
71 typeof document === 'undefined' ||
72 (typeof window === 'undefined' && typeof global.window === 'undefined')
75 'To use $generateHtmlFromNodes in headless mode please initialize a headless browser implementation such as JSDom before calling this function.',
79 const container = document.createElement('div');
80 const root = $getRoot();
81 const topLevelChildren = root.getChildren();
83 for (let i = 0; i < topLevelChildren.length; i++) {
84 const topLevelNode = topLevelChildren[i];
85 $appendNodesToHTML(editor, topLevelNode, container, selection);
89 for (const node of container.childNodes) {
90 if ("outerHTML" in node) {
91 nodeCode.push(node.outerHTML)
93 const wrap = document.createElement('div');
94 wrap.appendChild(node.cloneNode(true));
95 nodeCode.push(wrap.innerHTML);
99 return nodeCode.join('\n');
102 function $appendNodesToHTML(
103 editor: LexicalEditor,
104 currentNode: LexicalNode,
105 parentElement: HTMLElement | DocumentFragment,
106 selection: BaseSelection | null = null,
109 selection !== null ? currentNode.isSelected(selection) : true;
110 const shouldExclude =
111 $isElementNode(currentNode) && currentNode.excludeFromCopy('html');
112 let target = currentNode;
114 if (selection !== null) {
115 let clone = $cloneWithProperties(currentNode);
117 $isTextNode(clone) && selection !== null
118 ? $sliceSelectedTextNodeContent(selection, clone)
122 const children = $isElementNode(target) ? target.getChildren() : [];
123 const registeredNode = editor._nodes.get(target.getType());
126 // Use HTMLConfig overrides, if available.
127 if (registeredNode && registeredNode.exportDOM !== undefined) {
128 exportOutput = registeredNode.exportDOM(editor, target);
130 exportOutput = target.exportDOM(editor);
133 const {element, after} = exportOutput;
139 const fragment = document.createDocumentFragment();
141 for (let i = 0; i < children.length; i++) {
142 const childNode = children[i];
143 const shouldIncludeChild = $appendNodesToHTML(
152 $isElementNode(currentNode) &&
153 shouldIncludeChild &&
154 currentNode.extractWithChild(childNode, selection, 'html')
156 shouldInclude = true;
160 if (shouldInclude && !shouldExclude) {
161 if (isHTMLElement(element)) {
162 element.append(fragment);
164 parentElement.append(element);
167 const newElement = after.call(target, element);
169 element.replaceWith(newElement);
173 parentElement.append(fragment);
176 return shouldInclude;
179 function getConversionFunction(
181 editor: LexicalEditor,
182 ): DOMConversionFn | null {
183 const {nodeName} = domNode;
185 const cachedConversions = editor._htmlConversions.get(nodeName.toLowerCase());
187 let currentConversion: DOMConversion | null = null;
189 if (cachedConversions !== undefined) {
190 for (const cachedConversion of cachedConversions) {
191 const domConversion = cachedConversion(domNode);
193 domConversion !== null &&
194 (currentConversion === null ||
195 (currentConversion.priority || 0) < (domConversion.priority || 0))
197 currentConversion = domConversion;
202 return currentConversion !== null ? currentConversion.conversion : null;
205 const IGNORE_TAGS = new Set(['STYLE', 'SCRIPT']);
207 function $createNodesFromDOM(
209 editor: LexicalEditor,
210 allArtificialNodes: Array<ArtificialNode__DO_NOT_USE>,
211 hasBlockAncestorLexicalNode: boolean,
212 forChildMap: Map<string, DOMChildConversion> = new Map(),
213 parentLexicalNode?: LexicalNode | null | undefined,
214 ): Array<LexicalNode> {
215 let lexicalNodes: Array<LexicalNode> = [];
217 if (IGNORE_TAGS.has(node.nodeName)) {
221 let currentLexicalNode = null;
222 const transformFunction = getConversionFunction(node, editor);
223 const transformOutput = transformFunction
224 ? transformFunction(node as HTMLElement)
226 let postTransform = null;
228 if (transformOutput !== null) {
229 postTransform = transformOutput.after;
230 const transformNodes = transformOutput.node;
232 if (transformNodes === 'ignore') {
236 currentLexicalNode = Array.isArray(transformNodes)
237 ? transformNodes[transformNodes.length - 1]
240 if (currentLexicalNode !== null) {
241 for (const [, forChildFunction] of forChildMap) {
242 currentLexicalNode = forChildFunction(
247 if (!currentLexicalNode) {
252 if (currentLexicalNode) {
254 ...(Array.isArray(transformNodes)
256 : [currentLexicalNode]),
261 if (transformOutput.forChild != null) {
262 forChildMap.set(node.nodeName, transformOutput.forChild);
266 // If the DOM node doesn't have a transformer, we don't know what
267 // to do with it but we still need to process any childNodes.
268 const children = node.childNodes;
269 let childLexicalNodes = [];
271 const hasBlockAncestorLexicalNodeForChildren =
272 currentLexicalNode != null && $isRootOrShadowRoot(currentLexicalNode)
274 : (currentLexicalNode != null &&
275 $isBlockElementNode(currentLexicalNode)) ||
276 hasBlockAncestorLexicalNode;
278 for (let i = 0; i < children.length; i++) {
279 childLexicalNodes.push(
280 ...$createNodesFromDOM(
284 hasBlockAncestorLexicalNodeForChildren,
285 new Map(forChildMap),
291 if (postTransform != null) {
292 childLexicalNodes = postTransform(childLexicalNodes);
295 if (isBlockDomNode(node)) {
296 if (!hasBlockAncestorLexicalNodeForChildren) {
297 childLexicalNodes = wrapContinuousInlines(
300 $createParagraphNode,
303 childLexicalNodes = wrapContinuousInlines(node, childLexicalNodes, () => {
304 const artificialNode = new ArtificialNode__DO_NOT_USE();
305 allArtificialNodes.push(artificialNode);
306 return artificialNode;
311 if (currentLexicalNode == null) {
312 if (childLexicalNodes.length > 0) {
313 // If it hasn't been converted to a LexicalNode, we hoist its children
314 // up to the same level as it.
315 lexicalNodes = lexicalNodes.concat(childLexicalNodes);
317 if (isBlockDomNode(node) && isDomNodeBetweenTwoInlineNodes(node)) {
318 // Empty block dom node that hasnt been converted, we replace it with a linebreak if its between inline nodes
319 lexicalNodes = lexicalNodes.concat($createLineBreakNode());
323 if ($isElementNode(currentLexicalNode)) {
324 // If the current node is a ElementNode after conversion,
325 // we can append all the children to it.
326 currentLexicalNode.append(...childLexicalNodes);
333 function wrapContinuousInlines(
335 nodes: Array<LexicalNode>,
336 createWrapperFn: () => ElementNode,
337 ): Array<LexicalNode> {
338 const out: Array<LexicalNode> = [];
339 let continuousInlines: Array<LexicalNode> = [];
340 // wrap contiguous inline child nodes in para
341 for (let i = 0; i < nodes.length; i++) {
342 const node = nodes[i];
343 if ($isBlockElementNode(node)) {
346 continuousInlines.push(node);
348 i === nodes.length - 1 ||
349 (i < nodes.length - 1 && $isBlockElementNode(nodes[i + 1]))
351 const wrapper = createWrapperFn();
352 wrapper.append(...continuousInlines);
354 continuousInlines = [];
361 function $unwrapArtificalNodes(
362 allArtificialNodes: Array<ArtificialNode__DO_NOT_USE>,
364 for (const node of allArtificialNodes) {
365 if (node.getNextSibling() instanceof ArtificialNode__DO_NOT_USE) {
366 node.insertAfter($createLineBreakNode());
369 // Replace artificial node with it's children
370 for (const node of allArtificialNodes) {
371 const children = node.getChildren();
372 for (const child of children) {
373 node.insertBefore(child);
379 function isDomNodeBetweenTwoInlineNodes(node: Node): boolean {
380 if (node.nextSibling == null || node.previousSibling == null) {
384 isInlineDomNode(node.nextSibling) && isInlineDomNode(node.previousSibling)