167 lines
6.3 KiB
JavaScript
167 lines
6.3 KiB
JavaScript
|
/**
|
||
|
* 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.
|
||
|
*/
|
||
|
'use strict';
|
||
|
|
||
|
var selection = require('@lexical/selection');
|
||
|
var utils = require('@lexical/utils');
|
||
|
var lexical = require('lexical');
|
||
|
|
||
|
/** @module @lexical/html */
|
||
|
|
||
|
/**
|
||
|
* How you parse your html string to get a document is left up to you. In the browser you can use the native
|
||
|
* DOMParser API to generate a document (see clipboard.ts), but to use in a headless environment you can use JSDom
|
||
|
* or an equivilant library and pass in the document here.
|
||
|
*/
|
||
|
function $generateNodesFromDOM(editor, dom) {
|
||
|
const elements = dom.body ? dom.body.childNodes : [];
|
||
|
let lexicalNodes = [];
|
||
|
for (let i = 0; i < elements.length; i++) {
|
||
|
const element = elements[i];
|
||
|
if (!IGNORE_TAGS.has(element.nodeName)) {
|
||
|
const lexicalNode = $createNodesFromDOM(element, editor);
|
||
|
if (lexicalNode !== null) {
|
||
|
lexicalNodes = lexicalNodes.concat(lexicalNode);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return lexicalNodes;
|
||
|
}
|
||
|
function $generateHtmlFromNodes(editor, selection) {
|
||
|
if (typeof document === 'undefined' || typeof window === 'undefined' && typeof global.window === 'undefined') {
|
||
|
throw new Error('To use $generateHtmlFromNodes in headless mode please initialize a headless browser implementation such as JSDom before calling this function.');
|
||
|
}
|
||
|
const container = document.createElement('div');
|
||
|
const root = lexical.$getRoot();
|
||
|
const topLevelChildren = root.getChildren();
|
||
|
for (let i = 0; i < topLevelChildren.length; i++) {
|
||
|
const topLevelNode = topLevelChildren[i];
|
||
|
$appendNodesToHTML(editor, topLevelNode, container, selection);
|
||
|
}
|
||
|
return container.innerHTML;
|
||
|
}
|
||
|
function $appendNodesToHTML(editor, currentNode, parentElement, selection$1 = null) {
|
||
|
let shouldInclude = selection$1 !== null ? currentNode.isSelected(selection$1) : true;
|
||
|
const shouldExclude = lexical.$isElementNode(currentNode) && currentNode.excludeFromCopy('html');
|
||
|
let target = currentNode;
|
||
|
if (selection$1 !== null) {
|
||
|
let clone = selection.$cloneWithProperties(currentNode);
|
||
|
clone = lexical.$isTextNode(clone) && selection$1 !== null ? selection.$sliceSelectedTextNodeContent(selection$1, clone) : clone;
|
||
|
target = clone;
|
||
|
}
|
||
|
const children = lexical.$isElementNode(target) ? target.getChildren() : [];
|
||
|
const registeredNode = editor._nodes.get(target.getType());
|
||
|
let exportOutput;
|
||
|
|
||
|
// Use HTMLConfig overrides, if available.
|
||
|
if (registeredNode && registeredNode.exportDOM !== undefined) {
|
||
|
exportOutput = registeredNode.exportDOM(editor, target);
|
||
|
} else {
|
||
|
exportOutput = target.exportDOM(editor);
|
||
|
}
|
||
|
const {
|
||
|
element,
|
||
|
after
|
||
|
} = exportOutput;
|
||
|
if (!element) {
|
||
|
return false;
|
||
|
}
|
||
|
const fragment = document.createDocumentFragment();
|
||
|
for (let i = 0; i < children.length; i++) {
|
||
|
const childNode = children[i];
|
||
|
const shouldIncludeChild = $appendNodesToHTML(editor, childNode, fragment, selection$1);
|
||
|
if (!shouldInclude && lexical.$isElementNode(currentNode) && shouldIncludeChild && currentNode.extractWithChild(childNode, selection$1, 'html')) {
|
||
|
shouldInclude = true;
|
||
|
}
|
||
|
}
|
||
|
if (shouldInclude && !shouldExclude) {
|
||
|
if (utils.isHTMLElement(element)) {
|
||
|
element.append(fragment);
|
||
|
}
|
||
|
parentElement.append(element);
|
||
|
if (after) {
|
||
|
const newElement = after.call(target, element);
|
||
|
if (newElement) element.replaceWith(newElement);
|
||
|
}
|
||
|
} else {
|
||
|
parentElement.append(fragment);
|
||
|
}
|
||
|
return shouldInclude;
|
||
|
}
|
||
|
function getConversionFunction(domNode, editor) {
|
||
|
const {
|
||
|
nodeName
|
||
|
} = domNode;
|
||
|
const cachedConversions = editor._htmlConversions.get(nodeName.toLowerCase());
|
||
|
let currentConversion = null;
|
||
|
if (cachedConversions !== undefined) {
|
||
|
for (const cachedConversion of cachedConversions) {
|
||
|
const domConversion = cachedConversion(domNode);
|
||
|
if (domConversion !== null && (currentConversion === null || (currentConversion.priority || 0) < (domConversion.priority || 0))) {
|
||
|
currentConversion = domConversion;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return currentConversion !== null ? currentConversion.conversion : null;
|
||
|
}
|
||
|
const IGNORE_TAGS = new Set(['STYLE', 'SCRIPT']);
|
||
|
function $createNodesFromDOM(node, editor, forChildMap = new Map(), parentLexicalNode) {
|
||
|
let lexicalNodes = [];
|
||
|
if (IGNORE_TAGS.has(node.nodeName)) {
|
||
|
return lexicalNodes;
|
||
|
}
|
||
|
let currentLexicalNode = null;
|
||
|
const transformFunction = getConversionFunction(node, editor);
|
||
|
const transformOutput = transformFunction ? transformFunction(node) : null;
|
||
|
let postTransform = null;
|
||
|
if (transformOutput !== null) {
|
||
|
postTransform = transformOutput.after;
|
||
|
const transformNodes = transformOutput.node;
|
||
|
currentLexicalNode = Array.isArray(transformNodes) ? transformNodes[transformNodes.length - 1] : transformNodes;
|
||
|
if (currentLexicalNode !== null) {
|
||
|
for (const [, forChildFunction] of forChildMap) {
|
||
|
currentLexicalNode = forChildFunction(currentLexicalNode, parentLexicalNode);
|
||
|
if (!currentLexicalNode) {
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
if (currentLexicalNode) {
|
||
|
lexicalNodes.push(...(Array.isArray(transformNodes) ? transformNodes : [currentLexicalNode]));
|
||
|
}
|
||
|
}
|
||
|
if (transformOutput.forChild != null) {
|
||
|
forChildMap.set(node.nodeName, transformOutput.forChild);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// If the DOM node doesn't have a transformer, we don't know what
|
||
|
// to do with it but we still need to process any childNodes.
|
||
|
const children = node.childNodes;
|
||
|
let childLexicalNodes = [];
|
||
|
for (let i = 0; i < children.length; i++) {
|
||
|
childLexicalNodes.push(...$createNodesFromDOM(children[i], editor, new Map(forChildMap), currentLexicalNode));
|
||
|
}
|
||
|
if (postTransform != null) {
|
||
|
childLexicalNodes = postTransform(childLexicalNodes);
|
||
|
}
|
||
|
if (currentLexicalNode == null) {
|
||
|
// If it hasn't been converted to a LexicalNode, we hoist its children
|
||
|
// up to the same level as it.
|
||
|
lexicalNodes = lexicalNodes.concat(childLexicalNodes);
|
||
|
} else {
|
||
|
if (lexical.$isElementNode(currentLexicalNode)) {
|
||
|
// If the current node is a ElementNode after conversion,
|
||
|
// we can append all the children to it.
|
||
|
currentLexicalNode.append(...childLexicalNodes);
|
||
|
}
|
||
|
}
|
||
|
return lexicalNodes;
|
||
|
}
|
||
|
|
||
|
exports.$generateHtmlFromNodes = $generateHtmlFromNodes;
|
||
|
exports.$generateNodesFromDOM = $generateNodesFromDOM;
|