@lexical/html 0.38.0 → 0.38.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/LexicalHtml.dev.js +248 -0
- package/{src/index.ts → LexicalHtml.dev.mjs} +56 -191
- package/LexicalHtml.js +3 -3
- package/LexicalHtml.mjs +13 -0
- package/LexicalHtml.node.mjs +11 -0
- package/LexicalHtml.prod.js +9 -0
- package/LexicalHtml.prod.mjs +9 -0
- package/index.d.ts +15 -0
- package/package.json +4 -4
- package/src/__tests__/unit/LexicalHtml.test.ts +0 -265
- /package/{flow/LexicalHtml.js.flow → LexicalHtml.js.flow} +0 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
var selection = require('@lexical/selection');
|
|
12
|
+
var utils = require('@lexical/utils');
|
|
13
|
+
var lexical = require('lexical');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
17
|
+
*
|
|
18
|
+
* This source code is licensed under the MIT license found in the
|
|
19
|
+
* LICENSE file in the root directory of this source tree.
|
|
20
|
+
*
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* How you parse your html string to get a document is left up to you. In the browser you can use the native
|
|
26
|
+
* DOMParser API to generate a document (see clipboard.ts), but to use in a headless environment you can use JSDom
|
|
27
|
+
* or an equivalent library and pass in the document here.
|
|
28
|
+
*/
|
|
29
|
+
function $generateNodesFromDOM(editor, dom) {
|
|
30
|
+
const elements = lexical.isDOMDocumentNode(dom) ? dom.body.childNodes : dom.childNodes;
|
|
31
|
+
let lexicalNodes = [];
|
|
32
|
+
const allArtificialNodes = [];
|
|
33
|
+
for (const element of elements) {
|
|
34
|
+
if (!IGNORE_TAGS.has(element.nodeName)) {
|
|
35
|
+
const lexicalNode = $createNodesFromDOM(element, editor, allArtificialNodes, false);
|
|
36
|
+
if (lexicalNode !== null) {
|
|
37
|
+
lexicalNodes = lexicalNodes.concat(lexicalNode);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
$unwrapArtificialNodes(allArtificialNodes);
|
|
42
|
+
return lexicalNodes;
|
|
43
|
+
}
|
|
44
|
+
function $generateHtmlFromNodes(editor, selection) {
|
|
45
|
+
if (typeof document === 'undefined' || typeof window === 'undefined' && typeof global.window === 'undefined') {
|
|
46
|
+
throw new Error('To use $generateHtmlFromNodes in headless mode please initialize a headless browser implementation such as JSDom before calling this function.');
|
|
47
|
+
}
|
|
48
|
+
const container = document.createElement('div');
|
|
49
|
+
const root = lexical.$getRoot();
|
|
50
|
+
const topLevelChildren = root.getChildren();
|
|
51
|
+
for (let i = 0; i < topLevelChildren.length; i++) {
|
|
52
|
+
const topLevelNode = topLevelChildren[i];
|
|
53
|
+
$appendNodesToHTML(editor, topLevelNode, container, selection);
|
|
54
|
+
}
|
|
55
|
+
return container.innerHTML;
|
|
56
|
+
}
|
|
57
|
+
function $appendNodesToHTML(editor, currentNode, parentElement, selection$1 = null) {
|
|
58
|
+
let shouldInclude = selection$1 !== null ? currentNode.isSelected(selection$1) : true;
|
|
59
|
+
const shouldExclude = lexical.$isElementNode(currentNode) && currentNode.excludeFromCopy('html');
|
|
60
|
+
let target = currentNode;
|
|
61
|
+
if (selection$1 !== null && lexical.$isTextNode(currentNode)) {
|
|
62
|
+
target = selection.$sliceSelectedTextNodeContent(selection$1, currentNode, 'clone');
|
|
63
|
+
}
|
|
64
|
+
const children = lexical.$isElementNode(target) ? target.getChildren() : [];
|
|
65
|
+
const registeredNode = lexical.getRegisteredNode(editor, target.getType());
|
|
66
|
+
let exportOutput;
|
|
67
|
+
|
|
68
|
+
// Use HTMLConfig overrides, if available.
|
|
69
|
+
if (registeredNode && registeredNode.exportDOM !== undefined) {
|
|
70
|
+
exportOutput = registeredNode.exportDOM(editor, target);
|
|
71
|
+
} else {
|
|
72
|
+
exportOutput = target.exportDOM(editor);
|
|
73
|
+
}
|
|
74
|
+
const {
|
|
75
|
+
element,
|
|
76
|
+
after
|
|
77
|
+
} = exportOutput;
|
|
78
|
+
if (!element) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
const fragment = document.createDocumentFragment();
|
|
82
|
+
for (let i = 0; i < children.length; i++) {
|
|
83
|
+
const childNode = children[i];
|
|
84
|
+
const shouldIncludeChild = $appendNodesToHTML(editor, childNode, fragment, selection$1);
|
|
85
|
+
if (!shouldInclude && lexical.$isElementNode(currentNode) && shouldIncludeChild && currentNode.extractWithChild(childNode, selection$1, 'html')) {
|
|
86
|
+
shouldInclude = true;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (shouldInclude && !shouldExclude) {
|
|
90
|
+
if (utils.isHTMLElement(element) || lexical.isDocumentFragment(element)) {
|
|
91
|
+
element.append(fragment);
|
|
92
|
+
}
|
|
93
|
+
parentElement.append(element);
|
|
94
|
+
if (after) {
|
|
95
|
+
const newElement = after.call(target, element);
|
|
96
|
+
if (newElement) {
|
|
97
|
+
if (lexical.isDocumentFragment(element)) {
|
|
98
|
+
element.replaceChildren(newElement);
|
|
99
|
+
} else {
|
|
100
|
+
element.replaceWith(newElement);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
parentElement.append(fragment);
|
|
106
|
+
}
|
|
107
|
+
return shouldInclude;
|
|
108
|
+
}
|
|
109
|
+
function getConversionFunction(domNode, editor) {
|
|
110
|
+
const {
|
|
111
|
+
nodeName
|
|
112
|
+
} = domNode;
|
|
113
|
+
const cachedConversions = editor._htmlConversions.get(nodeName.toLowerCase());
|
|
114
|
+
let currentConversion = null;
|
|
115
|
+
if (cachedConversions !== undefined) {
|
|
116
|
+
for (const cachedConversion of cachedConversions) {
|
|
117
|
+
const domConversion = cachedConversion(domNode);
|
|
118
|
+
if (domConversion !== null && (currentConversion === null ||
|
|
119
|
+
// Given equal priority, prefer the last registered importer
|
|
120
|
+
// which is typically an application custom node or HTMLConfig['import']
|
|
121
|
+
(currentConversion.priority || 0) <= (domConversion.priority || 0))) {
|
|
122
|
+
currentConversion = domConversion;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return currentConversion !== null ? currentConversion.conversion : null;
|
|
127
|
+
}
|
|
128
|
+
const IGNORE_TAGS = new Set(['STYLE', 'SCRIPT']);
|
|
129
|
+
function $createNodesFromDOM(node, editor, allArtificialNodes, hasBlockAncestorLexicalNode, forChildMap = new Map(), parentLexicalNode) {
|
|
130
|
+
let lexicalNodes = [];
|
|
131
|
+
if (IGNORE_TAGS.has(node.nodeName)) {
|
|
132
|
+
return lexicalNodes;
|
|
133
|
+
}
|
|
134
|
+
let currentLexicalNode = null;
|
|
135
|
+
const transformFunction = getConversionFunction(node, editor);
|
|
136
|
+
const transformOutput = transformFunction ? transformFunction(node) : null;
|
|
137
|
+
let postTransform = null;
|
|
138
|
+
if (transformOutput !== null) {
|
|
139
|
+
postTransform = transformOutput.after;
|
|
140
|
+
const transformNodes = transformOutput.node;
|
|
141
|
+
currentLexicalNode = Array.isArray(transformNodes) ? transformNodes[transformNodes.length - 1] : transformNodes;
|
|
142
|
+
if (currentLexicalNode !== null) {
|
|
143
|
+
for (const [, forChildFunction] of forChildMap) {
|
|
144
|
+
currentLexicalNode = forChildFunction(currentLexicalNode, parentLexicalNode);
|
|
145
|
+
if (!currentLexicalNode) {
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (currentLexicalNode) {
|
|
150
|
+
lexicalNodes.push(...(Array.isArray(transformNodes) ? transformNodes : [currentLexicalNode]));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (transformOutput.forChild != null) {
|
|
154
|
+
forChildMap.set(node.nodeName, transformOutput.forChild);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// If the DOM node doesn't have a transformer, we don't know what
|
|
159
|
+
// to do with it but we still need to process any childNodes.
|
|
160
|
+
const children = node.childNodes;
|
|
161
|
+
let childLexicalNodes = [];
|
|
162
|
+
const hasBlockAncestorLexicalNodeForChildren = currentLexicalNode != null && lexical.$isRootOrShadowRoot(currentLexicalNode) ? false : currentLexicalNode != null && lexical.$isBlockElementNode(currentLexicalNode) || hasBlockAncestorLexicalNode;
|
|
163
|
+
for (let i = 0; i < children.length; i++) {
|
|
164
|
+
childLexicalNodes.push(...$createNodesFromDOM(children[i], editor, allArtificialNodes, hasBlockAncestorLexicalNodeForChildren, new Map(forChildMap), currentLexicalNode));
|
|
165
|
+
}
|
|
166
|
+
if (postTransform != null) {
|
|
167
|
+
childLexicalNodes = postTransform(childLexicalNodes);
|
|
168
|
+
}
|
|
169
|
+
if (utils.isBlockDomNode(node)) {
|
|
170
|
+
if (!hasBlockAncestorLexicalNodeForChildren) {
|
|
171
|
+
childLexicalNodes = wrapContinuousInlines(node, childLexicalNodes, lexical.$createParagraphNode);
|
|
172
|
+
} else {
|
|
173
|
+
childLexicalNodes = wrapContinuousInlines(node, childLexicalNodes, () => {
|
|
174
|
+
const artificialNode = new lexical.ArtificialNode__DO_NOT_USE();
|
|
175
|
+
allArtificialNodes.push(artificialNode);
|
|
176
|
+
return artificialNode;
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (currentLexicalNode == null) {
|
|
181
|
+
if (childLexicalNodes.length > 0) {
|
|
182
|
+
// If it hasn't been converted to a LexicalNode, we hoist its children
|
|
183
|
+
// up to the same level as it.
|
|
184
|
+
lexicalNodes = lexicalNodes.concat(childLexicalNodes);
|
|
185
|
+
} else {
|
|
186
|
+
if (utils.isBlockDomNode(node) && isDomNodeBetweenTwoInlineNodes(node)) {
|
|
187
|
+
// Empty block dom node that hasnt been converted, we replace it with a linebreak if its between inline nodes
|
|
188
|
+
lexicalNodes = lexicalNodes.concat(lexical.$createLineBreakNode());
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
} else {
|
|
192
|
+
if (lexical.$isElementNode(currentLexicalNode)) {
|
|
193
|
+
// If the current node is a ElementNode after conversion,
|
|
194
|
+
// we can append all the children to it.
|
|
195
|
+
currentLexicalNode.append(...childLexicalNodes);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return lexicalNodes;
|
|
199
|
+
}
|
|
200
|
+
function wrapContinuousInlines(domNode, nodes, createWrapperFn) {
|
|
201
|
+
const textAlign = domNode.style.textAlign;
|
|
202
|
+
const out = [];
|
|
203
|
+
let continuousInlines = [];
|
|
204
|
+
// wrap contiguous inline child nodes in para
|
|
205
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
206
|
+
const node = nodes[i];
|
|
207
|
+
if (lexical.$isBlockElementNode(node)) {
|
|
208
|
+
if (textAlign && !node.getFormat()) {
|
|
209
|
+
node.setFormat(textAlign);
|
|
210
|
+
}
|
|
211
|
+
out.push(node);
|
|
212
|
+
} else {
|
|
213
|
+
continuousInlines.push(node);
|
|
214
|
+
if (i === nodes.length - 1 || i < nodes.length - 1 && lexical.$isBlockElementNode(nodes[i + 1])) {
|
|
215
|
+
const wrapper = createWrapperFn();
|
|
216
|
+
wrapper.setFormat(textAlign);
|
|
217
|
+
wrapper.append(...continuousInlines);
|
|
218
|
+
out.push(wrapper);
|
|
219
|
+
continuousInlines = [];
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return out;
|
|
224
|
+
}
|
|
225
|
+
function $unwrapArtificialNodes(allArtificialNodes) {
|
|
226
|
+
for (const node of allArtificialNodes) {
|
|
227
|
+
if (node.getNextSibling() instanceof lexical.ArtificialNode__DO_NOT_USE) {
|
|
228
|
+
node.insertAfter(lexical.$createLineBreakNode());
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
// Replace artificial node with it's children
|
|
232
|
+
for (const node of allArtificialNodes) {
|
|
233
|
+
const children = node.getChildren();
|
|
234
|
+
for (const child of children) {
|
|
235
|
+
node.insertBefore(child);
|
|
236
|
+
}
|
|
237
|
+
node.remove();
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function isDomNodeBetweenTwoInlineNodes(node) {
|
|
241
|
+
if (node.nextSibling == null || node.previousSibling == null) {
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
return lexical.isInlineDomNode(node.nextSibling) && lexical.isInlineDomNode(node.previousSibling);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
exports.$generateHtmlFromNodes = $generateHtmlFromNodes;
|
|
248
|
+
exports.$generateNodesFromDOM = $generateNodesFromDOM;
|
|
@@ -6,103 +6,56 @@
|
|
|
6
6
|
*
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
9
|
+
import { $sliceSelectedTextNodeContent } from '@lexical/selection';
|
|
10
|
+
import { isHTMLElement, isBlockDomNode } from '@lexical/utils';
|
|
11
|
+
import { isDOMDocumentNode, $getRoot, $isElementNode, $isTextNode, getRegisteredNode, isDocumentFragment, $isRootOrShadowRoot, $isBlockElementNode, $createLineBreakNode, ArtificialNode__DO_NOT_USE, isInlineDomNode, $createParagraphNode } from 'lexical';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
15
|
+
*
|
|
16
|
+
* This source code is licensed under the MIT license found in the
|
|
17
|
+
* LICENSE file in the root directory of this source tree.
|
|
18
|
+
*
|
|
19
|
+
*/
|
|
18
20
|
|
|
19
|
-
import {$sliceSelectedTextNodeContent} from '@lexical/selection';
|
|
20
|
-
import {isBlockDomNode, isHTMLElement} from '@lexical/utils';
|
|
21
|
-
import {
|
|
22
|
-
$createLineBreakNode,
|
|
23
|
-
$createParagraphNode,
|
|
24
|
-
$getRoot,
|
|
25
|
-
$isBlockElementNode,
|
|
26
|
-
$isElementNode,
|
|
27
|
-
$isRootOrShadowRoot,
|
|
28
|
-
$isTextNode,
|
|
29
|
-
ArtificialNode__DO_NOT_USE,
|
|
30
|
-
ElementNode,
|
|
31
|
-
getRegisteredNode,
|
|
32
|
-
isDocumentFragment,
|
|
33
|
-
isDOMDocumentNode,
|
|
34
|
-
isInlineDomNode,
|
|
35
|
-
} from 'lexical';
|
|
36
21
|
|
|
37
22
|
/**
|
|
38
23
|
* How you parse your html string to get a document is left up to you. In the browser you can use the native
|
|
39
24
|
* DOMParser API to generate a document (see clipboard.ts), but to use in a headless environment you can use JSDom
|
|
40
25
|
* or an equivalent library and pass in the document here.
|
|
41
26
|
*/
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const elements = isDOMDocumentNode(dom)
|
|
47
|
-
? dom.body.childNodes
|
|
48
|
-
: dom.childNodes;
|
|
49
|
-
let lexicalNodes: Array<LexicalNode> = [];
|
|
50
|
-
const allArtificialNodes: Array<ArtificialNode__DO_NOT_USE> = [];
|
|
27
|
+
function $generateNodesFromDOM(editor, dom) {
|
|
28
|
+
const elements = isDOMDocumentNode(dom) ? dom.body.childNodes : dom.childNodes;
|
|
29
|
+
let lexicalNodes = [];
|
|
30
|
+
const allArtificialNodes = [];
|
|
51
31
|
for (const element of elements) {
|
|
52
32
|
if (!IGNORE_TAGS.has(element.nodeName)) {
|
|
53
|
-
const lexicalNode = $createNodesFromDOM(
|
|
54
|
-
element,
|
|
55
|
-
editor,
|
|
56
|
-
allArtificialNodes,
|
|
57
|
-
false,
|
|
58
|
-
);
|
|
33
|
+
const lexicalNode = $createNodesFromDOM(element, editor, allArtificialNodes, false);
|
|
59
34
|
if (lexicalNode !== null) {
|
|
60
35
|
lexicalNodes = lexicalNodes.concat(lexicalNode);
|
|
61
36
|
}
|
|
62
37
|
}
|
|
63
38
|
}
|
|
64
39
|
$unwrapArtificialNodes(allArtificialNodes);
|
|
65
|
-
|
|
66
40
|
return lexicalNodes;
|
|
67
41
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
selection?: BaseSelection | null,
|
|
72
|
-
): string {
|
|
73
|
-
if (
|
|
74
|
-
typeof document === 'undefined' ||
|
|
75
|
-
(typeof window === 'undefined' && typeof global.window === 'undefined')
|
|
76
|
-
) {
|
|
77
|
-
throw new Error(
|
|
78
|
-
'To use $generateHtmlFromNodes in headless mode please initialize a headless browser implementation such as JSDom before calling this function.',
|
|
79
|
-
);
|
|
42
|
+
function $generateHtmlFromNodes(editor, selection) {
|
|
43
|
+
if (typeof document === 'undefined' || typeof window === 'undefined' && typeof global.window === 'undefined') {
|
|
44
|
+
throw new Error('To use $generateHtmlFromNodes in headless mode please initialize a headless browser implementation such as JSDom before calling this function.');
|
|
80
45
|
}
|
|
81
|
-
|
|
82
46
|
const container = document.createElement('div');
|
|
83
47
|
const root = $getRoot();
|
|
84
48
|
const topLevelChildren = root.getChildren();
|
|
85
|
-
|
|
86
49
|
for (let i = 0; i < topLevelChildren.length; i++) {
|
|
87
50
|
const topLevelNode = topLevelChildren[i];
|
|
88
51
|
$appendNodesToHTML(editor, topLevelNode, container, selection);
|
|
89
52
|
}
|
|
90
|
-
|
|
91
53
|
return container.innerHTML;
|
|
92
54
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
currentNode: LexicalNode,
|
|
97
|
-
parentElement: HTMLElement | DocumentFragment,
|
|
98
|
-
selection: BaseSelection | null = null,
|
|
99
|
-
): boolean {
|
|
100
|
-
let shouldInclude =
|
|
101
|
-
selection !== null ? currentNode.isSelected(selection) : true;
|
|
102
|
-
const shouldExclude =
|
|
103
|
-
$isElementNode(currentNode) && currentNode.excludeFromCopy('html');
|
|
55
|
+
function $appendNodesToHTML(editor, currentNode, parentElement, selection = null) {
|
|
56
|
+
let shouldInclude = selection !== null ? currentNode.isSelected(selection) : true;
|
|
57
|
+
const shouldExclude = $isElementNode(currentNode) && currentNode.excludeFromCopy('html');
|
|
104
58
|
let target = currentNode;
|
|
105
|
-
|
|
106
59
|
if (selection !== null && $isTextNode(currentNode)) {
|
|
107
60
|
target = $sliceSelectedTextNodeContent(selection, currentNode, 'clone');
|
|
108
61
|
}
|
|
@@ -116,40 +69,26 @@ function $appendNodesToHTML(
|
|
|
116
69
|
} else {
|
|
117
70
|
exportOutput = target.exportDOM(editor);
|
|
118
71
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
72
|
+
const {
|
|
73
|
+
element,
|
|
74
|
+
after
|
|
75
|
+
} = exportOutput;
|
|
122
76
|
if (!element) {
|
|
123
77
|
return false;
|
|
124
78
|
}
|
|
125
|
-
|
|
126
79
|
const fragment = document.createDocumentFragment();
|
|
127
|
-
|
|
128
80
|
for (let i = 0; i < children.length; i++) {
|
|
129
81
|
const childNode = children[i];
|
|
130
|
-
const shouldIncludeChild = $appendNodesToHTML(
|
|
131
|
-
|
|
132
|
-
childNode,
|
|
133
|
-
fragment,
|
|
134
|
-
selection,
|
|
135
|
-
);
|
|
136
|
-
|
|
137
|
-
if (
|
|
138
|
-
!shouldInclude &&
|
|
139
|
-
$isElementNode(currentNode) &&
|
|
140
|
-
shouldIncludeChild &&
|
|
141
|
-
currentNode.extractWithChild(childNode, selection, 'html')
|
|
142
|
-
) {
|
|
82
|
+
const shouldIncludeChild = $appendNodesToHTML(editor, childNode, fragment, selection);
|
|
83
|
+
if (!shouldInclude && $isElementNode(currentNode) && shouldIncludeChild && currentNode.extractWithChild(childNode, selection, 'html')) {
|
|
143
84
|
shouldInclude = true;
|
|
144
85
|
}
|
|
145
86
|
}
|
|
146
|
-
|
|
147
87
|
if (shouldInclude && !shouldExclude) {
|
|
148
88
|
if (isHTMLElement(element) || isDocumentFragment(element)) {
|
|
149
89
|
element.append(fragment);
|
|
150
90
|
}
|
|
151
91
|
parentElement.append(element);
|
|
152
|
-
|
|
153
92
|
if (after) {
|
|
154
93
|
const newElement = after.call(target, element);
|
|
155
94
|
if (newElement) {
|
|
@@ -163,89 +102,52 @@ function $appendNodesToHTML(
|
|
|
163
102
|
} else {
|
|
164
103
|
parentElement.append(fragment);
|
|
165
104
|
}
|
|
166
|
-
|
|
167
105
|
return shouldInclude;
|
|
168
106
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
): DOMConversionFn | null {
|
|
174
|
-
const {nodeName} = domNode;
|
|
175
|
-
|
|
107
|
+
function getConversionFunction(domNode, editor) {
|
|
108
|
+
const {
|
|
109
|
+
nodeName
|
|
110
|
+
} = domNode;
|
|
176
111
|
const cachedConversions = editor._htmlConversions.get(nodeName.toLowerCase());
|
|
177
|
-
|
|
178
|
-
let currentConversion: DOMConversion | null = null;
|
|
179
|
-
|
|
112
|
+
let currentConversion = null;
|
|
180
113
|
if (cachedConversions !== undefined) {
|
|
181
114
|
for (const cachedConversion of cachedConversions) {
|
|
182
115
|
const domConversion = cachedConversion(domNode);
|
|
183
|
-
if (
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
// which is typically an application custom node or HTMLConfig['import']
|
|
188
|
-
(currentConversion.priority || 0) <= (domConversion.priority || 0))
|
|
189
|
-
) {
|
|
116
|
+
if (domConversion !== null && (currentConversion === null ||
|
|
117
|
+
// Given equal priority, prefer the last registered importer
|
|
118
|
+
// which is typically an application custom node or HTMLConfig['import']
|
|
119
|
+
(currentConversion.priority || 0) <= (domConversion.priority || 0))) {
|
|
190
120
|
currentConversion = domConversion;
|
|
191
121
|
}
|
|
192
122
|
}
|
|
193
123
|
}
|
|
194
|
-
|
|
195
124
|
return currentConversion !== null ? currentConversion.conversion : null;
|
|
196
125
|
}
|
|
197
|
-
|
|
198
126
|
const IGNORE_TAGS = new Set(['STYLE', 'SCRIPT']);
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
node: Node,
|
|
202
|
-
editor: LexicalEditor,
|
|
203
|
-
allArtificialNodes: Array<ArtificialNode__DO_NOT_USE>,
|
|
204
|
-
hasBlockAncestorLexicalNode: boolean,
|
|
205
|
-
forChildMap: Map<string, DOMChildConversion> = new Map(),
|
|
206
|
-
parentLexicalNode?: LexicalNode | null | undefined,
|
|
207
|
-
): Array<LexicalNode> {
|
|
208
|
-
let lexicalNodes: Array<LexicalNode> = [];
|
|
209
|
-
|
|
127
|
+
function $createNodesFromDOM(node, editor, allArtificialNodes, hasBlockAncestorLexicalNode, forChildMap = new Map(), parentLexicalNode) {
|
|
128
|
+
let lexicalNodes = [];
|
|
210
129
|
if (IGNORE_TAGS.has(node.nodeName)) {
|
|
211
130
|
return lexicalNodes;
|
|
212
131
|
}
|
|
213
|
-
|
|
214
132
|
let currentLexicalNode = null;
|
|
215
133
|
const transformFunction = getConversionFunction(node, editor);
|
|
216
|
-
const transformOutput = transformFunction
|
|
217
|
-
? transformFunction(node as HTMLElement)
|
|
218
|
-
: null;
|
|
134
|
+
const transformOutput = transformFunction ? transformFunction(node) : null;
|
|
219
135
|
let postTransform = null;
|
|
220
|
-
|
|
221
136
|
if (transformOutput !== null) {
|
|
222
137
|
postTransform = transformOutput.after;
|
|
223
138
|
const transformNodes = transformOutput.node;
|
|
224
|
-
currentLexicalNode = Array.isArray(transformNodes)
|
|
225
|
-
? transformNodes[transformNodes.length - 1]
|
|
226
|
-
: transformNodes;
|
|
227
|
-
|
|
139
|
+
currentLexicalNode = Array.isArray(transformNodes) ? transformNodes[transformNodes.length - 1] : transformNodes;
|
|
228
140
|
if (currentLexicalNode !== null) {
|
|
229
141
|
for (const [, forChildFunction] of forChildMap) {
|
|
230
|
-
currentLexicalNode = forChildFunction(
|
|
231
|
-
currentLexicalNode,
|
|
232
|
-
parentLexicalNode,
|
|
233
|
-
);
|
|
234
|
-
|
|
142
|
+
currentLexicalNode = forChildFunction(currentLexicalNode, parentLexicalNode);
|
|
235
143
|
if (!currentLexicalNode) {
|
|
236
144
|
break;
|
|
237
145
|
}
|
|
238
146
|
}
|
|
239
|
-
|
|
240
147
|
if (currentLexicalNode) {
|
|
241
|
-
lexicalNodes.push(
|
|
242
|
-
...(Array.isArray(transformNodes)
|
|
243
|
-
? transformNodes
|
|
244
|
-
: [currentLexicalNode]),
|
|
245
|
-
);
|
|
148
|
+
lexicalNodes.push(...(Array.isArray(transformNodes) ? transformNodes : [currentLexicalNode]));
|
|
246
149
|
}
|
|
247
150
|
}
|
|
248
|
-
|
|
249
151
|
if (transformOutput.forChild != null) {
|
|
250
152
|
forChildMap.set(node.nodeName, transformOutput.forChild);
|
|
251
153
|
}
|
|
@@ -255,38 +157,16 @@ function $createNodesFromDOM(
|
|
|
255
157
|
// to do with it but we still need to process any childNodes.
|
|
256
158
|
const children = node.childNodes;
|
|
257
159
|
let childLexicalNodes = [];
|
|
258
|
-
|
|
259
|
-
const hasBlockAncestorLexicalNodeForChildren =
|
|
260
|
-
currentLexicalNode != null && $isRootOrShadowRoot(currentLexicalNode)
|
|
261
|
-
? false
|
|
262
|
-
: (currentLexicalNode != null &&
|
|
263
|
-
$isBlockElementNode(currentLexicalNode)) ||
|
|
264
|
-
hasBlockAncestorLexicalNode;
|
|
265
|
-
|
|
160
|
+
const hasBlockAncestorLexicalNodeForChildren = currentLexicalNode != null && $isRootOrShadowRoot(currentLexicalNode) ? false : currentLexicalNode != null && $isBlockElementNode(currentLexicalNode) || hasBlockAncestorLexicalNode;
|
|
266
161
|
for (let i = 0; i < children.length; i++) {
|
|
267
|
-
childLexicalNodes.push(
|
|
268
|
-
...$createNodesFromDOM(
|
|
269
|
-
children[i],
|
|
270
|
-
editor,
|
|
271
|
-
allArtificialNodes,
|
|
272
|
-
hasBlockAncestorLexicalNodeForChildren,
|
|
273
|
-
new Map(forChildMap),
|
|
274
|
-
currentLexicalNode,
|
|
275
|
-
),
|
|
276
|
-
);
|
|
162
|
+
childLexicalNodes.push(...$createNodesFromDOM(children[i], editor, allArtificialNodes, hasBlockAncestorLexicalNodeForChildren, new Map(forChildMap), currentLexicalNode));
|
|
277
163
|
}
|
|
278
|
-
|
|
279
164
|
if (postTransform != null) {
|
|
280
165
|
childLexicalNodes = postTransform(childLexicalNodes);
|
|
281
166
|
}
|
|
282
|
-
|
|
283
167
|
if (isBlockDomNode(node)) {
|
|
284
168
|
if (!hasBlockAncestorLexicalNodeForChildren) {
|
|
285
|
-
childLexicalNodes = wrapContinuousInlines(
|
|
286
|
-
node,
|
|
287
|
-
childLexicalNodes,
|
|
288
|
-
$createParagraphNode,
|
|
289
|
-
);
|
|
169
|
+
childLexicalNodes = wrapContinuousInlines(node, childLexicalNodes, $createParagraphNode);
|
|
290
170
|
} else {
|
|
291
171
|
childLexicalNodes = wrapContinuousInlines(node, childLexicalNodes, () => {
|
|
292
172
|
const artificialNode = new ArtificialNode__DO_NOT_USE();
|
|
@@ -295,7 +175,6 @@ function $createNodesFromDOM(
|
|
|
295
175
|
});
|
|
296
176
|
}
|
|
297
177
|
}
|
|
298
|
-
|
|
299
178
|
if (currentLexicalNode == null) {
|
|
300
179
|
if (childLexicalNodes.length > 0) {
|
|
301
180
|
// If it hasn't been converted to a LexicalNode, we hoist its children
|
|
@@ -314,19 +193,12 @@ function $createNodesFromDOM(
|
|
|
314
193
|
currentLexicalNode.append(...childLexicalNodes);
|
|
315
194
|
}
|
|
316
195
|
}
|
|
317
|
-
|
|
318
196
|
return lexicalNodes;
|
|
319
197
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
createWrapperFn: () => ElementNode,
|
|
325
|
-
): Array<LexicalNode> {
|
|
326
|
-
const textAlign = (domNode as HTMLElement).style
|
|
327
|
-
.textAlign as ElementFormatType;
|
|
328
|
-
const out: Array<LexicalNode> = [];
|
|
329
|
-
let continuousInlines: Array<LexicalNode> = [];
|
|
198
|
+
function wrapContinuousInlines(domNode, nodes, createWrapperFn) {
|
|
199
|
+
const textAlign = domNode.style.textAlign;
|
|
200
|
+
const out = [];
|
|
201
|
+
let continuousInlines = [];
|
|
330
202
|
// wrap contiguous inline child nodes in para
|
|
331
203
|
for (let i = 0; i < nodes.length; i++) {
|
|
332
204
|
const node = nodes[i];
|
|
@@ -337,10 +209,7 @@ function wrapContinuousInlines(
|
|
|
337
209
|
out.push(node);
|
|
338
210
|
} else {
|
|
339
211
|
continuousInlines.push(node);
|
|
340
|
-
if (
|
|
341
|
-
i === nodes.length - 1 ||
|
|
342
|
-
(i < nodes.length - 1 && $isBlockElementNode(nodes[i + 1]))
|
|
343
|
-
) {
|
|
212
|
+
if (i === nodes.length - 1 || i < nodes.length - 1 && $isBlockElementNode(nodes[i + 1])) {
|
|
344
213
|
const wrapper = createWrapperFn();
|
|
345
214
|
wrapper.setFormat(textAlign);
|
|
346
215
|
wrapper.append(...continuousInlines);
|
|
@@ -351,10 +220,7 @@ function wrapContinuousInlines(
|
|
|
351
220
|
}
|
|
352
221
|
return out;
|
|
353
222
|
}
|
|
354
|
-
|
|
355
|
-
function $unwrapArtificialNodes(
|
|
356
|
-
allArtificialNodes: Array<ArtificialNode__DO_NOT_USE>,
|
|
357
|
-
) {
|
|
223
|
+
function $unwrapArtificialNodes(allArtificialNodes) {
|
|
358
224
|
for (const node of allArtificialNodes) {
|
|
359
225
|
if (node.getNextSibling() instanceof ArtificialNode__DO_NOT_USE) {
|
|
360
226
|
node.insertAfter($createLineBreakNode());
|
|
@@ -369,12 +235,11 @@ function $unwrapArtificialNodes(
|
|
|
369
235
|
node.remove();
|
|
370
236
|
}
|
|
371
237
|
}
|
|
372
|
-
|
|
373
|
-
function isDomNodeBetweenTwoInlineNodes(node: Node): boolean {
|
|
238
|
+
function isDomNodeBetweenTwoInlineNodes(node) {
|
|
374
239
|
if (node.nextSibling == null || node.previousSibling == null) {
|
|
375
240
|
return false;
|
|
376
241
|
}
|
|
377
|
-
return (
|
|
378
|
-
isInlineDomNode(node.nextSibling) && isInlineDomNode(node.previousSibling)
|
|
379
|
-
);
|
|
242
|
+
return isInlineDomNode(node.nextSibling) && isInlineDomNode(node.previousSibling);
|
|
380
243
|
}
|
|
244
|
+
|
|
245
|
+
export { $generateHtmlFromNodes, $generateNodesFromDOM };
|
package/LexicalHtml.js
CHANGED
package/LexicalHtml.mjs
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as modDev from './LexicalHtml.dev.mjs';
|
|
10
|
+
import * as modProd from './LexicalHtml.prod.mjs';
|
|
11
|
+
const mod = process.env.NODE_ENV !== 'production' ? modDev : modProd;
|
|
12
|
+
export const $generateHtmlFromNodes = mod.$generateHtmlFromNodes;
|
|
13
|
+
export const $generateNodesFromDOM = mod.$generateNodesFromDOM;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const mod = await (process.env.NODE_ENV !== 'production' ? import('./LexicalHtml.dev.mjs') : import('./LexicalHtml.prod.mjs'));
|
|
10
|
+
export const $generateHtmlFromNodes = mod.$generateHtmlFromNodes;
|
|
11
|
+
export const $generateNodesFromDOM = mod.$generateNodesFromDOM;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
"use strict";var e=require("@lexical/selection"),n=require("@lexical/utils"),t=require("lexical");function o(l,i,r,s=null){let c=null===s||i.isSelected(s);const d=t.$isElementNode(i)&&i.excludeFromCopy("html");let u=i;null!==s&&t.$isTextNode(i)&&(u=e.$sliceSelectedTextNodeContent(s,i,"clone"));const a=t.$isElementNode(u)?u.getChildren():[],f=t.getRegisteredNode(l,u.getType());let m;m=f&&void 0!==f.exportDOM?f.exportDOM(l,u):u.exportDOM(l);const{element:h,after:p}=m;if(!h)return!1;const g=document.createDocumentFragment();for(let e=0;e<a.length;e++){const n=a[e],r=o(l,n,g,s);!c&&t.$isElementNode(i)&&r&&i.extractWithChild(n,s,"html")&&(c=!0)}if(c&&!d){if((n.isHTMLElement(h)||t.isDocumentFragment(h))&&h.append(g),r.append(h),p){const e=p.call(u,h);e&&(t.isDocumentFragment(h)?h.replaceChildren(e):h.replaceWith(e))}}else r.append(g);return c}const l=new Set(["STYLE","SCRIPT"]);function i(e,o,s,c,d=new Map,u){let a=[];if(l.has(e.nodeName))return a;let f=null;const m=function(e,n){const{nodeName:t}=e,o=n._htmlConversions.get(t.toLowerCase());let l=null;if(void 0!==o)for(const n of o){const t=n(e);null!==t&&(null===l||(l.priority||0)<=(t.priority||0))&&(l=t)}return null!==l?l.conversion:null}(e,o),h=m?m(e):null;let p=null;if(null!==h){p=h.after;const n=h.node;if(f=Array.isArray(n)?n[n.length-1]:n,null!==f){for(const[,e]of d)if(f=e(f,u),!f)break;f&&a.push(...Array.isArray(n)?n:[f])}null!=h.forChild&&d.set(e.nodeName,h.forChild)}const g=e.childNodes;let N=[];const $=(null==f||!t.$isRootOrShadowRoot(f))&&(null!=f&&t.$isBlockElementNode(f)||c);for(let e=0;e<g.length;e++)N.push(...i(g[e],o,s,$,new Map(d),f));return null!=p&&(N=p(N)),n.isBlockDomNode(e)&&(N=r(e,N,$?()=>{const e=new t.ArtificialNode__DO_NOT_USE;return s.push(e),e}:t.$createParagraphNode)),null==f?N.length>0?a=a.concat(N):n.isBlockDomNode(e)&&function(e){if(null==e.nextSibling||null==e.previousSibling)return!1;return t.isInlineDomNode(e.nextSibling)&&t.isInlineDomNode(e.previousSibling)}(e)&&(a=a.concat(t.$createLineBreakNode())):t.$isElementNode(f)&&f.append(...N),a}function r(e,n,o){const l=e.style.textAlign,i=[];let r=[];for(let e=0;e<n.length;e++){const s=n[e];if(t.$isBlockElementNode(s))l&&!s.getFormat()&&s.setFormat(l),i.push(s);else if(r.push(s),e===n.length-1||e<n.length-1&&t.$isBlockElementNode(n[e+1])){const e=o();e.setFormat(l),e.append(...r),i.push(e),r=[]}}return i}exports.$generateHtmlFromNodes=function(e,n){if("undefined"==typeof document||"undefined"==typeof window&&void 0===global.window)throw new Error("To use $generateHtmlFromNodes in headless mode please initialize a headless browser implementation such as JSDom before calling this function.");const l=document.createElement("div"),i=t.$getRoot().getChildren();for(let t=0;t<i.length;t++){o(e,i[t],l,n)}return l.innerHTML},exports.$generateNodesFromDOM=function(e,n){const o=t.isDOMDocumentNode(n)?n.body.childNodes:n.childNodes;let r=[];const s=[];for(const n of o)if(!l.has(n.nodeName)){const t=i(n,e,s,!1);null!==t&&(r=r.concat(t))}return function(e){for(const n of e)n.getNextSibling()instanceof t.ArtificialNode__DO_NOT_USE&&n.insertAfter(t.$createLineBreakNode());for(const n of e){const e=n.getChildren();for(const t of e)n.insertBefore(t);n.remove()}}(s),r};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import{$sliceSelectedTextNodeContent as e}from"@lexical/selection";import{isHTMLElement as n,isBlockDomNode as t}from"@lexical/utils";import{isDOMDocumentNode as o,$getRoot as l,$isElementNode as r,$isTextNode as i,getRegisteredNode as s,isDocumentFragment as c,$isRootOrShadowRoot as u,$isBlockElementNode as f,$createLineBreakNode as a,ArtificialNode__DO_NOT_USE as d,isInlineDomNode as p,$createParagraphNode as h}from"lexical";function m(e,n){const t=o(n)?n.body.childNodes:n.childNodes;let l=[];const r=[];for(const n of t)if(!w.has(n.nodeName)){const t=y(n,e,r,!1);null!==t&&(l=l.concat(t))}return function(e){for(const n of e)n.getNextSibling()instanceof d&&n.insertAfter(a());for(const n of e){const e=n.getChildren();for(const t of e)n.insertBefore(t);n.remove()}}(r),l}function g(e,n){if("undefined"==typeof document||"undefined"==typeof window&&void 0===global.window)throw new Error("To use $generateHtmlFromNodes in headless mode please initialize a headless browser implementation such as JSDom before calling this function.");const t=document.createElement("div"),o=l().getChildren();for(let l=0;l<o.length;l++){x(e,o[l],t,n)}return t.innerHTML}function x(t,o,l,u=null){let f=null===u||o.isSelected(u);const a=r(o)&&o.excludeFromCopy("html");let d=o;null!==u&&i(o)&&(d=e(u,o,"clone"));const p=r(d)?d.getChildren():[],h=s(t,d.getType());let m;m=h&&void 0!==h.exportDOM?h.exportDOM(t,d):d.exportDOM(t);const{element:g,after:w}=m;if(!g)return!1;const y=document.createDocumentFragment();for(let e=0;e<p.length;e++){const n=p[e],l=x(t,n,y,u);!f&&r(o)&&l&&o.extractWithChild(n,u,"html")&&(f=!0)}if(f&&!a){if((n(g)||c(g))&&g.append(y),l.append(g),w){const e=w.call(d,g);e&&(c(g)?g.replaceChildren(e):g.replaceWith(e))}}else l.append(y);return f}const w=new Set(["STYLE","SCRIPT"]);function y(e,n,o,l,i=new Map,s){let c=[];if(w.has(e.nodeName))return c;let m=null;const g=function(e,n){const{nodeName:t}=e,o=n._htmlConversions.get(t.toLowerCase());let l=null;if(void 0!==o)for(const n of o){const t=n(e);null!==t&&(null===l||(l.priority||0)<=(t.priority||0))&&(l=t)}return null!==l?l.conversion:null}(e,n),x=g?g(e):null;let b=null;if(null!==x){b=x.after;const n=x.node;if(m=Array.isArray(n)?n[n.length-1]:n,null!==m){for(const[,e]of i)if(m=e(m,s),!m)break;m&&c.push(...Array.isArray(n)?n:[m])}null!=x.forChild&&i.set(e.nodeName,x.forChild)}const S=e.childNodes;let v=[];const N=(null==m||!u(m))&&(null!=m&&f(m)||l);for(let e=0;e<S.length;e++)v.push(...y(S[e],n,o,N,new Map(i),m));return null!=b&&(v=b(v)),t(e)&&(v=C(e,v,N?()=>{const e=new d;return o.push(e),e}:h)),null==m?v.length>0?c=c.concat(v):t(e)&&function(e){if(null==e.nextSibling||null==e.previousSibling)return!1;return p(e.nextSibling)&&p(e.previousSibling)}(e)&&(c=c.concat(a())):r(m)&&m.append(...v),c}function C(e,n,t){const o=e.style.textAlign,l=[];let r=[];for(let e=0;e<n.length;e++){const i=n[e];if(f(i))o&&!i.getFormat()&&i.setFormat(o),l.push(i);else if(r.push(i),e===n.length-1||e<n.length-1&&f(n[e+1])){const e=t();e.setFormat(o),e.append(...r),l.push(e),r=[]}}return l}export{g as $generateHtmlFromNodes,m as $generateNodesFromDOM};
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
import type { BaseSelection, LexicalEditor, LexicalNode } from 'lexical';
|
|
9
|
+
/**
|
|
10
|
+
* How you parse your html string to get a document is left up to you. In the browser you can use the native
|
|
11
|
+
* DOMParser API to generate a document (see clipboard.ts), but to use in a headless environment you can use JSDom
|
|
12
|
+
* or an equivalent library and pass in the document here.
|
|
13
|
+
*/
|
|
14
|
+
export declare function $generateNodesFromDOM(editor: LexicalEditor, dom: Document | ParentNode): Array<LexicalNode>;
|
|
15
|
+
export declare function $generateHtmlFromNodes(editor: LexicalEditor, selection?: BaseSelection | null): string;
|
package/package.json
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"html"
|
|
9
9
|
],
|
|
10
10
|
"license": "MIT",
|
|
11
|
-
"version": "0.38.
|
|
11
|
+
"version": "0.38.1",
|
|
12
12
|
"main": "LexicalHtml.js",
|
|
13
13
|
"types": "index.d.ts",
|
|
14
14
|
"repository": {
|
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
"directory": "packages/lexical-html"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@lexical/selection": "0.38.
|
|
21
|
-
"@lexical/utils": "0.38.
|
|
22
|
-
"lexical": "0.38.
|
|
20
|
+
"@lexical/selection": "0.38.1",
|
|
21
|
+
"@lexical/utils": "0.38.1",
|
|
22
|
+
"lexical": "0.38.1"
|
|
23
23
|
},
|
|
24
24
|
"module": "LexicalHtml.mjs",
|
|
25
25
|
"sideEffects": false,
|
|
@@ -1,265 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
-
*
|
|
4
|
-
* This source code is licensed under the MIT license found in the
|
|
5
|
-
* LICENSE file in the root directory of this source tree.
|
|
6
|
-
*
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import {CodeNode} from '@lexical/code';
|
|
10
|
-
import {createHeadlessEditor} from '@lexical/headless';
|
|
11
|
-
import {$generateHtmlFromNodes, $generateNodesFromDOM} from '@lexical/html';
|
|
12
|
-
import {LinkNode} from '@lexical/link';
|
|
13
|
-
import {ListItemNode, ListNode} from '@lexical/list';
|
|
14
|
-
import {HeadingNode, QuoteNode} from '@lexical/rich-text';
|
|
15
|
-
import {
|
|
16
|
-
$createParagraphNode,
|
|
17
|
-
$createRangeSelection,
|
|
18
|
-
$createTextNode,
|
|
19
|
-
$getRoot,
|
|
20
|
-
ParagraphNode,
|
|
21
|
-
RangeSelection,
|
|
22
|
-
} from 'lexical';
|
|
23
|
-
import {describe, expect, test} from 'vitest';
|
|
24
|
-
|
|
25
|
-
describe('HTML', () => {
|
|
26
|
-
type Input = Array<{
|
|
27
|
-
name: string;
|
|
28
|
-
html: string;
|
|
29
|
-
initializeEditorState: () => void;
|
|
30
|
-
}>;
|
|
31
|
-
|
|
32
|
-
const HTML_SERIALIZE: Input = [
|
|
33
|
-
{
|
|
34
|
-
html: '<p><br></p>',
|
|
35
|
-
initializeEditorState: () => {
|
|
36
|
-
$getRoot().append($createParagraphNode());
|
|
37
|
-
},
|
|
38
|
-
name: 'Empty editor state',
|
|
39
|
-
},
|
|
40
|
-
];
|
|
41
|
-
for (const {name, html, initializeEditorState} of HTML_SERIALIZE) {
|
|
42
|
-
test(`[Lexical -> HTML]: ${name}`, () => {
|
|
43
|
-
const editor = createHeadlessEditor({
|
|
44
|
-
nodes: [
|
|
45
|
-
HeadingNode,
|
|
46
|
-
ListNode,
|
|
47
|
-
ListItemNode,
|
|
48
|
-
QuoteNode,
|
|
49
|
-
CodeNode,
|
|
50
|
-
LinkNode,
|
|
51
|
-
],
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
editor.update(initializeEditorState, {
|
|
55
|
-
discrete: true,
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
expect(
|
|
59
|
-
editor.getEditorState().read(() => $generateHtmlFromNodes(editor)),
|
|
60
|
-
).toBe(html);
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
test(`[Lexical -> HTML]: Use provided selection`, () => {
|
|
65
|
-
const editor = createHeadlessEditor({
|
|
66
|
-
nodes: [
|
|
67
|
-
HeadingNode,
|
|
68
|
-
ListNode,
|
|
69
|
-
ListItemNode,
|
|
70
|
-
QuoteNode,
|
|
71
|
-
CodeNode,
|
|
72
|
-
LinkNode,
|
|
73
|
-
],
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
let selection: RangeSelection | null = null;
|
|
77
|
-
|
|
78
|
-
editor.update(
|
|
79
|
-
() => {
|
|
80
|
-
const root = $getRoot();
|
|
81
|
-
const p1 = $createParagraphNode();
|
|
82
|
-
const text1 = $createTextNode('Hello');
|
|
83
|
-
p1.append(text1);
|
|
84
|
-
const p2 = $createParagraphNode();
|
|
85
|
-
const text2 = $createTextNode('World');
|
|
86
|
-
p2.append(text2);
|
|
87
|
-
root.append(p1).append(p2);
|
|
88
|
-
// Root
|
|
89
|
-
// - ParagraphNode
|
|
90
|
-
// -- TextNode "Hello"
|
|
91
|
-
// - ParagraphNode
|
|
92
|
-
// -- TextNode "World"
|
|
93
|
-
p1.select(0, text1.getTextContentSize());
|
|
94
|
-
selection = $createRangeSelection();
|
|
95
|
-
selection.setTextNodeRange(text2, 0, text2, text2.getTextContentSize());
|
|
96
|
-
},
|
|
97
|
-
{
|
|
98
|
-
discrete: true,
|
|
99
|
-
},
|
|
100
|
-
);
|
|
101
|
-
|
|
102
|
-
let html = '';
|
|
103
|
-
|
|
104
|
-
editor.update(() => {
|
|
105
|
-
html = $generateHtmlFromNodes(editor, selection);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
expect(html).toBe('<span style="white-space: pre-wrap;">World</span>');
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
test(`[Lexical -> HTML]: Default selection (undefined) should serialize entire editor state`, () => {
|
|
112
|
-
const editor = createHeadlessEditor({
|
|
113
|
-
nodes: [
|
|
114
|
-
HeadingNode,
|
|
115
|
-
ListNode,
|
|
116
|
-
ListItemNode,
|
|
117
|
-
QuoteNode,
|
|
118
|
-
CodeNode,
|
|
119
|
-
LinkNode,
|
|
120
|
-
],
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
editor.update(
|
|
124
|
-
() => {
|
|
125
|
-
const root = $getRoot();
|
|
126
|
-
const p1 = $createParagraphNode();
|
|
127
|
-
const text1 = $createTextNode('Hello');
|
|
128
|
-
p1.append(text1);
|
|
129
|
-
const p2 = $createParagraphNode();
|
|
130
|
-
const text2 = $createTextNode('World');
|
|
131
|
-
p2.append(text2);
|
|
132
|
-
root.append(p1).append(p2);
|
|
133
|
-
// Root
|
|
134
|
-
// - ParagraphNode
|
|
135
|
-
// -- TextNode "Hello"
|
|
136
|
-
// - ParagraphNode
|
|
137
|
-
// -- TextNode "World"
|
|
138
|
-
p1.select(0, text1.getTextContentSize());
|
|
139
|
-
},
|
|
140
|
-
{
|
|
141
|
-
discrete: true,
|
|
142
|
-
},
|
|
143
|
-
);
|
|
144
|
-
|
|
145
|
-
let html = '';
|
|
146
|
-
|
|
147
|
-
editor.update(() => {
|
|
148
|
-
html = $generateHtmlFromNodes(editor);
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
expect(html).toBe(
|
|
152
|
-
'<p><span style="white-space: pre-wrap;">Hello</span></p><p><span style="white-space: pre-wrap;">World</span></p>',
|
|
153
|
-
);
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
test(`If alignment is set on the paragraph, don't overwrite from parent empty format`, () => {
|
|
157
|
-
const editor = createHeadlessEditor();
|
|
158
|
-
const parser = new DOMParser();
|
|
159
|
-
const rightAlignedParagraphInDiv =
|
|
160
|
-
'<div><p style="text-align: center;">Hello world!</p></div>';
|
|
161
|
-
|
|
162
|
-
editor.update(
|
|
163
|
-
() => {
|
|
164
|
-
const root = $getRoot();
|
|
165
|
-
const dom = parser.parseFromString(
|
|
166
|
-
rightAlignedParagraphInDiv,
|
|
167
|
-
'text/html',
|
|
168
|
-
);
|
|
169
|
-
const nodes = $generateNodesFromDOM(editor, dom);
|
|
170
|
-
root.append(...nodes);
|
|
171
|
-
},
|
|
172
|
-
{discrete: true},
|
|
173
|
-
);
|
|
174
|
-
|
|
175
|
-
let html = '';
|
|
176
|
-
|
|
177
|
-
editor.update(() => {
|
|
178
|
-
html = $generateHtmlFromNodes(editor);
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
expect(html).toBe(
|
|
182
|
-
'<p style="text-align: center;"><span style="white-space: pre-wrap;">Hello world!</span></p>',
|
|
183
|
-
);
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
test(`If alignment is set on the paragraph, it should take precedence over its parent block alignment`, () => {
|
|
187
|
-
const editor = createHeadlessEditor();
|
|
188
|
-
const parser = new DOMParser();
|
|
189
|
-
const rightAlignedParagraphInDiv =
|
|
190
|
-
'<div style="text-align: right;"><p style="text-align: center;">Hello world!</p></div>';
|
|
191
|
-
|
|
192
|
-
editor.update(
|
|
193
|
-
() => {
|
|
194
|
-
const root = $getRoot();
|
|
195
|
-
const dom = parser.parseFromString(
|
|
196
|
-
rightAlignedParagraphInDiv,
|
|
197
|
-
'text/html',
|
|
198
|
-
);
|
|
199
|
-
const nodes = $generateNodesFromDOM(editor, dom);
|
|
200
|
-
root.append(...nodes);
|
|
201
|
-
},
|
|
202
|
-
{discrete: true},
|
|
203
|
-
);
|
|
204
|
-
|
|
205
|
-
let html = '';
|
|
206
|
-
|
|
207
|
-
editor.update(() => {
|
|
208
|
-
html = $generateHtmlFromNodes(editor);
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
expect(html).toBe(
|
|
212
|
-
'<p style="text-align: center;"><span style="white-space: pre-wrap;">Hello world!</span></p>',
|
|
213
|
-
);
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
test('It should output correctly nodes whose export is DocumentFragment', () => {
|
|
217
|
-
const editor = createHeadlessEditor({
|
|
218
|
-
html: {
|
|
219
|
-
export: new Map([
|
|
220
|
-
[
|
|
221
|
-
ParagraphNode,
|
|
222
|
-
() => {
|
|
223
|
-
const element = document.createDocumentFragment();
|
|
224
|
-
return {
|
|
225
|
-
element,
|
|
226
|
-
};
|
|
227
|
-
},
|
|
228
|
-
],
|
|
229
|
-
]),
|
|
230
|
-
},
|
|
231
|
-
nodes: [],
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
editor.update(
|
|
235
|
-
() => {
|
|
236
|
-
const root = $getRoot();
|
|
237
|
-
const p1 = $createParagraphNode();
|
|
238
|
-
const text1 = $createTextNode('Hello');
|
|
239
|
-
p1.append(text1);
|
|
240
|
-
const p2 = $createParagraphNode();
|
|
241
|
-
const text2 = $createTextNode('World');
|
|
242
|
-
p2.append(text2);
|
|
243
|
-
root.append(p1).append(p2);
|
|
244
|
-
// Root
|
|
245
|
-
// - ParagraphNode
|
|
246
|
-
// -- TextNode "Hello"
|
|
247
|
-
// - ParagraphNode
|
|
248
|
-
// -- TextNode "World"
|
|
249
|
-
},
|
|
250
|
-
{
|
|
251
|
-
discrete: true,
|
|
252
|
-
},
|
|
253
|
-
);
|
|
254
|
-
|
|
255
|
-
let html = '';
|
|
256
|
-
|
|
257
|
-
editor.update(() => {
|
|
258
|
-
html = $generateHtmlFromNodes(editor);
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
expect(html).toBe(
|
|
262
|
-
'<span style="white-space: pre-wrap;">Hello</span><span style="white-space: pre-wrap;">World</span>',
|
|
263
|
-
);
|
|
264
|
-
});
|
|
265
|
-
});
|
|
File without changes
|