@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 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 type {
10
- BaseSelection,
11
- DOMChildConversion,
12
- DOMConversion,
13
- DOMConversionFn,
14
- ElementFormatType,
15
- LexicalEditor,
16
- LexicalNode,
17
- } from 'lexical';
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
- export function $generateNodesFromDOM(
43
- editor: LexicalEditor,
44
- dom: Document | ParentNode,
45
- ): Array<LexicalNode> {
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
- export function $generateHtmlFromNodes(
70
- editor: LexicalEditor,
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
- function $appendNodesToHTML(
95
- editor: LexicalEditor,
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
- const {element, after} = exportOutput;
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
- editor,
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
- function getConversionFunction(
171
- domNode: Node,
172
- editor: LexicalEditor,
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
- domConversion !== null &&
185
- (currentConversion === null ||
186
- // Given equal priority, prefer the last registered importer
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
- function $createNodesFromDOM(
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
- function wrapContinuousInlines(
322
- domNode: Node,
323
- nodes: Array<LexicalNode>,
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
@@ -6,6 +6,6 @@
6
6
  *
7
7
  */
8
8
 
9
- 'use strict';
10
-
11
- module.exports = require('./dist/LexicalHtml.js');
9
+ 'use strict'
10
+ const LexicalHtml = process.env.NODE_ENV !== 'production' ? require('./LexicalHtml.dev.js') : require('./LexicalHtml.prod.js');
11
+ module.exports = LexicalHtml;
@@ -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.0",
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.0",
21
- "@lexical/utils": "0.38.0",
22
- "lexical": "0.38.0"
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
- });