@lexical/html 0.44.1-nightly.20260518.0 → 0.45.0
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/{DOMRenderExtension.d.ts → dist/DOMRenderExtension.d.ts} +12 -1
- package/dist/DOMRenderRuntime.d.ts +51 -0
- package/dist/LexicalHtml.dev.js +3192 -0
- package/dist/LexicalHtml.dev.mjs +3146 -0
- package/{LexicalHtml.js.flow → dist/LexicalHtml.js.flow} +16 -16
- package/dist/LexicalHtml.mjs +56 -0
- package/dist/LexicalHtml.node.mjs +54 -0
- package/dist/LexicalHtml.prod.js +9 -0
- package/dist/LexicalHtml.prod.mjs +9 -0
- package/dist/RenderContext.d.ts +68 -0
- package/{compileDOMRenderConfigOverrides.d.ts → dist/compileDOMRenderConfigOverrides.d.ts} +1 -1
- package/{constants.d.ts → dist/constants.d.ts} +2 -0
- package/dist/domOverride.d.ts +23 -0
- package/dist/import/CoreImportExtension.d.ts +11 -0
- package/dist/import/DOMImportExtension.d.ts +82 -0
- package/dist/import/HorizontalRuleImportExtension.d.ts +27 -0
- package/dist/import/ImportContext.d.ts +208 -0
- package/dist/import/compileImportRules.d.ts +50 -0
- package/dist/import/coreImportRules.d.ts +25 -0
- package/dist/import/defineImportRule.d.ts +32 -0
- package/dist/import/defineOverlayRules.d.ts +66 -0
- package/dist/import/index.d.ts +38 -0
- package/dist/import/inlineStylesFromStyleSheets.d.ts +28 -0
- package/dist/import/parseCss.d.ts +18 -0
- package/dist/import/runImport.d.ts +19 -0
- package/dist/import/schemas.d.ts +91 -0
- package/dist/import/sel.d.ts +74 -0
- package/dist/import/types.d.ts +394 -0
- package/dist/index.d.ts +44 -0
- package/{types.d.ts → dist/types.d.ts} +96 -8
- package/package.json +33 -18
- package/src/ContextRecord.ts +243 -0
- package/src/DOMRenderExtension.ts +96 -0
- package/src/DOMRenderRuntime.ts +265 -0
- package/src/RenderContext.ts +168 -0
- package/src/compileDOMRenderConfigOverrides.ts +416 -0
- package/src/constants.ts +18 -0
- package/src/domOverride.ts +46 -0
- package/src/import/CoreImportExtension.ts +26 -0
- package/src/import/DOMImportExtension.ts +221 -0
- package/src/import/HorizontalRuleImportExtension.ts +53 -0
- package/src/import/ImportContext.ts +339 -0
- package/src/import/compileImportRules.ts +178 -0
- package/src/import/coreImportRules.ts +485 -0
- package/src/import/defineImportRule.ts +40 -0
- package/src/import/defineOverlayRules.ts +105 -0
- package/src/import/index.ts +96 -0
- package/src/import/inlineStylesFromStyleSheets.ts +104 -0
- package/src/import/parseCss.ts +219 -0
- package/src/import/runImport.ts +245 -0
- package/src/import/schemas.ts +236 -0
- package/src/import/sel.ts +314 -0
- package/src/import/types.ts +471 -0
- package/src/index.ts +555 -0
- package/src/types.ts +470 -0
- package/LexicalHtml.dev.js +0 -914
- package/LexicalHtml.dev.mjs +0 -900
- package/LexicalHtml.mjs +0 -24
- package/LexicalHtml.node.mjs +0 -22
- package/LexicalHtml.prod.js +0 -9
- package/LexicalHtml.prod.mjs +0 -9
- package/RenderContext.d.ts +0 -32
- package/domOverride.d.ts +0 -18
- package/index.d.ts +0 -32
- /package/{ContextRecord.d.ts → dist/ContextRecord.d.ts} +0 -0
- /package/{LexicalHtml.js → dist/LexicalHtml.js} +0 -0
|
@@ -0,0 +1,3192 @@
|
|
|
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 lexical = require('lexical');
|
|
13
|
+
var utils = require('@lexical/utils');
|
|
14
|
+
var extension = require('@lexical/extension');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
18
|
+
*
|
|
19
|
+
* This source code is licensed under the MIT license found in the
|
|
20
|
+
* LICENSE file in the root directory of this source tree.
|
|
21
|
+
*
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
// Do not require this module directly! Use normal `invariant` calls.
|
|
25
|
+
|
|
26
|
+
function formatDevErrorMessage(message) {
|
|
27
|
+
throw new Error(message);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
32
|
+
*
|
|
33
|
+
* This source code is licensed under the MIT license found in the
|
|
34
|
+
* LICENSE file in the root directory of this source tree.
|
|
35
|
+
*
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
let activeContext;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @experimental
|
|
42
|
+
*
|
|
43
|
+
* The LexicalEditor with context
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @experimental
|
|
48
|
+
*
|
|
49
|
+
* @param contextRecord The ContextRecord
|
|
50
|
+
* @param cfg The configuration
|
|
51
|
+
* @returns The value or defaultValue of cfg
|
|
52
|
+
*/
|
|
53
|
+
function getContextValue(contextRecord, cfg) {
|
|
54
|
+
const {
|
|
55
|
+
key
|
|
56
|
+
} = cfg;
|
|
57
|
+
return contextRecord && key in contextRecord ? contextRecord[key] : cfg.defaultValue;
|
|
58
|
+
}
|
|
59
|
+
function getEditorContext(editor) {
|
|
60
|
+
return activeContext && activeContext.editor === editor ? activeContext : undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @experimental
|
|
65
|
+
*
|
|
66
|
+
* @param sym The symbol for this ContextRecord (e.g. DOMRenderContextSymbol)
|
|
67
|
+
* @param editor The editor
|
|
68
|
+
* @returns The current context or undefined
|
|
69
|
+
*/
|
|
70
|
+
function getContextRecord(sym, editor) {
|
|
71
|
+
const editorContext = getEditorContext(editor);
|
|
72
|
+
return editorContext && editorContext[sym];
|
|
73
|
+
}
|
|
74
|
+
function toPair(contextRecord, pairOrUpdater) {
|
|
75
|
+
if ('cfg' in pairOrUpdater) {
|
|
76
|
+
const {
|
|
77
|
+
cfg,
|
|
78
|
+
updater
|
|
79
|
+
} = pairOrUpdater;
|
|
80
|
+
return [cfg, updater(getContextValue(contextRecord, cfg))];
|
|
81
|
+
}
|
|
82
|
+
return pairOrUpdater;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Construct a new context from a parent context and pairs
|
|
87
|
+
*
|
|
88
|
+
* @param pairs The pairs and updaters to build the context from
|
|
89
|
+
* @param parent The parent context
|
|
90
|
+
* @returns The new context
|
|
91
|
+
*/
|
|
92
|
+
function contextFromPairs(pairs, parent) {
|
|
93
|
+
let rval = parent;
|
|
94
|
+
for (const pairOrUpdater of pairs) {
|
|
95
|
+
const [k, v] = toPair(rval, pairOrUpdater);
|
|
96
|
+
const key = k.key;
|
|
97
|
+
if (rval === parent && getContextValue(rval, k) === v) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
// If we haven't branched away from `parent` yet, create a fresh child
|
|
101
|
+
// context so we never mutate the caller's parent record. Subsequent
|
|
102
|
+
// pairs in this loop accumulate into the same child. Inside the loop
|
|
103
|
+
// `rval` is non-null after the first iteration, since createChildContext
|
|
104
|
+
// never returns null/undefined.
|
|
105
|
+
const ctx = rval === parent || rval === undefined ? createChildContext(parent) : rval;
|
|
106
|
+
ctx[key] = v;
|
|
107
|
+
rval = ctx;
|
|
108
|
+
}
|
|
109
|
+
return rval;
|
|
110
|
+
}
|
|
111
|
+
function createChildContext(parent) {
|
|
112
|
+
return Object.create(parent || null);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Create a context config pair that sets a value in the render context.
|
|
117
|
+
* @experimental
|
|
118
|
+
*/
|
|
119
|
+
function contextValue(cfg, value) {
|
|
120
|
+
return [cfg, value];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Create a context config updater that transforms a value in the render context.
|
|
125
|
+
* @experimental
|
|
126
|
+
*/
|
|
127
|
+
function contextUpdater(cfg, updater) {
|
|
128
|
+
return {
|
|
129
|
+
cfg,
|
|
130
|
+
updater
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* @internal
|
|
136
|
+
* @experimental
|
|
137
|
+
* @__NO_SIDE_EFFECTS__
|
|
138
|
+
*/
|
|
139
|
+
function $withFullContext(sym, contextRecord, f, editor = lexical.$getEditor()) {
|
|
140
|
+
const prevDOMContext = activeContext;
|
|
141
|
+
const parentEditorContext = getEditorContext(editor);
|
|
142
|
+
try {
|
|
143
|
+
activeContext = {
|
|
144
|
+
...parentEditorContext,
|
|
145
|
+
editor,
|
|
146
|
+
[sym]: contextRecord
|
|
147
|
+
};
|
|
148
|
+
return f();
|
|
149
|
+
} finally {
|
|
150
|
+
activeContext = prevDOMContext;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* @internal
|
|
156
|
+
* @experimental
|
|
157
|
+
* @__NO_SIDE_EFFECTS__
|
|
158
|
+
*/
|
|
159
|
+
function $withContext(sym, $defaults = () => undefined) {
|
|
160
|
+
return (cfg, editor = lexical.$getEditor()) => {
|
|
161
|
+
return f => {
|
|
162
|
+
const parentEditorContext = getEditorContext(editor);
|
|
163
|
+
const parentContextRecord = parentEditorContext && parentEditorContext[sym];
|
|
164
|
+
const contextRecord = contextFromPairs(cfg, parentContextRecord || $defaults(editor));
|
|
165
|
+
if (!contextRecord || contextRecord === parentContextRecord) {
|
|
166
|
+
return f();
|
|
167
|
+
}
|
|
168
|
+
return $withFullContext(sym, contextRecord, f, editor);
|
|
169
|
+
};
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* @experimental
|
|
175
|
+
* @internal
|
|
176
|
+
* @__NO_SIDE_EFFECTS__
|
|
177
|
+
*/
|
|
178
|
+
function createContextState(tag, name, getDefaultValue, isEqual) {
|
|
179
|
+
return Object.assign(lexical.createState(Symbol(name), {
|
|
180
|
+
isEqual,
|
|
181
|
+
parse: getDefaultValue
|
|
182
|
+
}), {
|
|
183
|
+
[tag]: true
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
189
|
+
*
|
|
190
|
+
* This source code is licensed under the MIT license found in the
|
|
191
|
+
* LICENSE file in the root directory of this source tree.
|
|
192
|
+
*
|
|
193
|
+
*/
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Inlines CSS rules from `<style>` tags onto matching elements as inline
|
|
198
|
+
* styles.
|
|
199
|
+
*
|
|
200
|
+
* Used by apps like Excel that generate HTML where styles live in
|
|
201
|
+
* class-based `<style>` rules (e.g. `.xl65 { background: #FFFF00; color:
|
|
202
|
+
* blue; }`) rather than inline styles. Since Lexical's import converters
|
|
203
|
+
* read inline styles, we resolve stylesheet rules into inline styles
|
|
204
|
+
* before conversion.
|
|
205
|
+
*
|
|
206
|
+
* Mutates the DOM in-place. Original inline styles always take
|
|
207
|
+
* precedence over stylesheet rules (matching CSS specificity behavior).
|
|
208
|
+
*
|
|
209
|
+
* No-op for {@link ParentNode}s that are not {@link Document}s — only a
|
|
210
|
+
* full document carries `styleSheets` we can iterate.
|
|
211
|
+
*
|
|
212
|
+
* @experimental
|
|
213
|
+
*/
|
|
214
|
+
const $inlineStylesFromStyleSheets = (dom, _ctx, $next) => {
|
|
215
|
+
$inlineStylesFromStyleSheetsDOM(dom);
|
|
216
|
+
$next();
|
|
217
|
+
};
|
|
218
|
+
function $inlineStylesFromStyleSheetsDOM(dom) {
|
|
219
|
+
if (!lexical.isDOMDocumentNode(dom)) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const doc = dom;
|
|
223
|
+
if (doc.querySelector('style') === null) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const originalInlineStyles = new Map();
|
|
227
|
+
function getOriginalInlineProps(el) {
|
|
228
|
+
let props = originalInlineStyles.get(el);
|
|
229
|
+
if (props === undefined) {
|
|
230
|
+
props = new Set();
|
|
231
|
+
for (let i = 0; i < el.style.length; i++) {
|
|
232
|
+
props.add(el.style[i]);
|
|
233
|
+
}
|
|
234
|
+
originalInlineStyles.set(el, props);
|
|
235
|
+
}
|
|
236
|
+
return props;
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
239
|
+
for (const sheet of Array.from(doc.styleSheets)) {
|
|
240
|
+
let rules;
|
|
241
|
+
try {
|
|
242
|
+
rules = sheet.cssRules;
|
|
243
|
+
} catch (_unused) {
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
for (const rule of Array.from(rules)) {
|
|
247
|
+
if (!utils.objectKlassEquals(rule, CSSStyleRule)) {
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
let elements;
|
|
251
|
+
try {
|
|
252
|
+
elements = doc.querySelectorAll(rule.selectorText);
|
|
253
|
+
} catch (_unused2) {
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
for (const el of Array.from(elements)) {
|
|
257
|
+
if (!lexical.isHTMLElement(el)) {
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
const originalProps = getOriginalInlineProps(el);
|
|
261
|
+
for (let i = 0; i < rule.style.length; i++) {
|
|
262
|
+
const prop = rule.style[i];
|
|
263
|
+
if (!originalProps.has(prop)) {
|
|
264
|
+
el.style.setProperty(prop, rule.style.getPropertyValue(prop), rule.style.getPropertyPriority(prop));
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
} catch (_unused3) {
|
|
271
|
+
// styleSheets API not supported in this environment
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
277
|
+
*
|
|
278
|
+
* This source code is licensed under the MIT license found in the
|
|
279
|
+
* LICENSE file in the root directory of this source tree.
|
|
280
|
+
*
|
|
281
|
+
*/
|
|
282
|
+
const DOMRenderExtensionName = '@lexical/html/DOM';
|
|
283
|
+
const DOMRenderContextSymbol = Symbol.for('@lexical/html/DOMExportContext');
|
|
284
|
+
const DOMImportExtensionName = '@lexical/html/DOMImport';
|
|
285
|
+
const DOMImportContextSymbol = Symbol.for('@lexical/html/DOMImportContext');
|
|
286
|
+
const ALWAYS_TRUE = () => true;
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
290
|
+
*
|
|
291
|
+
* This source code is licensed under the MIT license found in the
|
|
292
|
+
* LICENSE file in the root directory of this source tree.
|
|
293
|
+
*
|
|
294
|
+
*/
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Create a context state to be used during render.
|
|
298
|
+
*
|
|
299
|
+
* Note that to support the ValueOrUpdater pattern you can not use a
|
|
300
|
+
* function for V (but you may wrap it in an array or object).
|
|
301
|
+
*
|
|
302
|
+
* @experimental
|
|
303
|
+
* @__NO_SIDE_EFFECTS__
|
|
304
|
+
*/
|
|
305
|
+
function createRenderState(name, getDefaultValue, isEqual) {
|
|
306
|
+
return createContextState(DOMRenderContextSymbol, name, getDefaultValue, isEqual);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Render context state that is true if the export was initiated from the root of the document.
|
|
311
|
+
* @experimental
|
|
312
|
+
*/
|
|
313
|
+
const RenderContextRoot = createRenderState('root', Boolean);
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Render context state that is true if this is an export operation ($generateHtmlFromNodes).
|
|
317
|
+
* @experimental
|
|
318
|
+
*/
|
|
319
|
+
const RenderContextExport = createRenderState('isExport', Boolean);
|
|
320
|
+
function getDefaultRenderContext(editor) {
|
|
321
|
+
const dep = extension.getPeerDependencyFromEditor(editor, DOMRenderExtensionName);
|
|
322
|
+
return dep ? dep.output.defaults : undefined;
|
|
323
|
+
}
|
|
324
|
+
function getRenderContext(editor) {
|
|
325
|
+
return getContextRecord(DOMRenderContextSymbol, editor) || getDefaultRenderContext(editor);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Get a render context value during a DOM render or export operation.
|
|
330
|
+
* @experimental
|
|
331
|
+
*/
|
|
332
|
+
function $getRenderContextValue(cfg, editor = lexical.$getEditor()) {
|
|
333
|
+
return getContextValue(getRenderContext(editor), cfg);
|
|
334
|
+
}
|
|
335
|
+
function getRuntime(editor) {
|
|
336
|
+
const dep = extension.getPeerDependencyFromEditor(editor, DOMRenderExtensionName);
|
|
337
|
+
return dep ? dep.output.runtime : undefined;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Imperatively set a value in the persistent editor render context.
|
|
342
|
+
*
|
|
343
|
+
* Unlike {@link $withRenderContext} (which scopes values to a callback), this
|
|
344
|
+
* persists on the editor. If the change flips any override's
|
|
345
|
+
* `disabledForEditor` result, the resident render config is recompiled and the
|
|
346
|
+
* affected nodes are re-rendered. No-op if {@link DOMRenderExtension} is not
|
|
347
|
+
* installed.
|
|
348
|
+
*
|
|
349
|
+
* @experimental
|
|
350
|
+
*/
|
|
351
|
+
function $setRenderContextValue(cfg, value, editor = lexical.$getEditor()) {
|
|
352
|
+
const runtime = getRuntime(editor);
|
|
353
|
+
if (runtime) {
|
|
354
|
+
runtime.setContextValue(cfg, value);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Imperatively update a value in the persistent editor render context with an
|
|
360
|
+
* updater function. See {@link $setRenderContextValue}.
|
|
361
|
+
*
|
|
362
|
+
* @experimental
|
|
363
|
+
*/
|
|
364
|
+
function $updateRenderContextValue(cfg, updater, editor = lexical.$getEditor()) {
|
|
365
|
+
const runtime = getRuntime(editor);
|
|
366
|
+
if (runtime) {
|
|
367
|
+
runtime.setContextValue(cfg, updater(getContextValue(runtime.editorContext, cfg)));
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Resolve the {@link EditorDOMRenderConfig} to use for the current
|
|
373
|
+
* export/generate session, applying any `disabledForSession` overrides against
|
|
374
|
+
* the active session context. Falls back to the editor's resident config when
|
|
375
|
+
* {@link DOMRenderExtension} is not installed.
|
|
376
|
+
*
|
|
377
|
+
* @experimental
|
|
378
|
+
*/
|
|
379
|
+
function $getSessionDOMRenderConfig(editor = lexical.$getEditor()) {
|
|
380
|
+
const runtime = getRuntime(editor);
|
|
381
|
+
return runtime ? runtime.getSessionConfig() : lexical.$getEditorDOMRenderConfig(editor);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Execute a callback within a render context with the given config pairs.
|
|
386
|
+
* @experimental
|
|
387
|
+
*/
|
|
388
|
+
const $withRenderContext = $withContext(DOMRenderContextSymbol, getDefaultRenderContext);
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
392
|
+
*
|
|
393
|
+
* This source code is licensed under the MIT license found in the
|
|
394
|
+
* LICENSE file in the root directory of this source tree.
|
|
395
|
+
*
|
|
396
|
+
*/
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* A convenience function for type inference when constructing DOM overrides for
|
|
400
|
+
* use with {@link DOMRenderExtension}.
|
|
401
|
+
*
|
|
402
|
+
* The optional `options` argument controls *whether* the override is installed
|
|
403
|
+
* based only on render context — `disabledForEditor` gates residency in the
|
|
404
|
+
* editor's render pipeline (reconciliation), `disabledForSession` gates
|
|
405
|
+
* participation in a single export/generate session. See {@link DOMOverrideOptions}.
|
|
406
|
+
*
|
|
407
|
+
* @experimental
|
|
408
|
+
* @__NO_SIDE_EFFECTS__
|
|
409
|
+
*/
|
|
410
|
+
|
|
411
|
+
function domOverride(nodes, config, options) {
|
|
412
|
+
return {
|
|
413
|
+
...config,
|
|
414
|
+
...options,
|
|
415
|
+
nodes
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function buildTypeTree(editorConfig) {
|
|
420
|
+
const t = {};
|
|
421
|
+
const {
|
|
422
|
+
nodes
|
|
423
|
+
} = extension.getKnownTypesAndNodes(editorConfig);
|
|
424
|
+
for (const klass of nodes) {
|
|
425
|
+
const type = klass.getType();
|
|
426
|
+
t[type] = {
|
|
427
|
+
klass,
|
|
428
|
+
types: {}
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
for (const baseRec of Object.values(t)) {
|
|
432
|
+
if (baseRec) {
|
|
433
|
+
const baseType = baseRec.klass.getType();
|
|
434
|
+
for (let {
|
|
435
|
+
klass
|
|
436
|
+
} = baseRec; lexical.$isLexicalNode(klass.prototype); klass = Object.getPrototypeOf(klass)) {
|
|
437
|
+
const {
|
|
438
|
+
ownNodeType
|
|
439
|
+
} = lexical.getStaticNodeConfig(klass);
|
|
440
|
+
const superRec = ownNodeType && t[ownNodeType];
|
|
441
|
+
if (superRec) {
|
|
442
|
+
superRec.types[baseType] = true;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return t;
|
|
448
|
+
}
|
|
449
|
+
function buildNodePredicate(klass) {
|
|
450
|
+
return node => node instanceof klass;
|
|
451
|
+
}
|
|
452
|
+
function getPredicate(typeTree, {
|
|
453
|
+
nodes
|
|
454
|
+
}) {
|
|
455
|
+
if (nodes === '*') {
|
|
456
|
+
return ALWAYS_TRUE;
|
|
457
|
+
}
|
|
458
|
+
let types = {};
|
|
459
|
+
const predicates = [];
|
|
460
|
+
for (const klassOrPredicate of nodes) {
|
|
461
|
+
if ('getType' in klassOrPredicate) {
|
|
462
|
+
const type = klassOrPredicate.getType();
|
|
463
|
+
if (types) {
|
|
464
|
+
const tree = typeTree[type];
|
|
465
|
+
if (!(tree !== undefined)) {
|
|
466
|
+
formatDevErrorMessage(`Node class ${klassOrPredicate.name} with type ${type} not registered in editor`);
|
|
467
|
+
}
|
|
468
|
+
types = Object.assign(types, tree.types);
|
|
469
|
+
}
|
|
470
|
+
predicates.push(buildNodePredicate(klassOrPredicate));
|
|
471
|
+
} else {
|
|
472
|
+
types = undefined;
|
|
473
|
+
predicates.push(klassOrPredicate);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
if (types) {
|
|
477
|
+
return types;
|
|
478
|
+
} else if (predicates.length === 1) {
|
|
479
|
+
return predicates[0];
|
|
480
|
+
}
|
|
481
|
+
return node => {
|
|
482
|
+
for (const predicate of predicates) {
|
|
483
|
+
if (predicate(node)) {
|
|
484
|
+
return true;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return false;
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
function makePrerender() {
|
|
491
|
+
return {
|
|
492
|
+
$createDOM: [],
|
|
493
|
+
$decorateDOM: [],
|
|
494
|
+
$exportDOM: [],
|
|
495
|
+
$extractWithChild: [],
|
|
496
|
+
$getDOMSlot: [],
|
|
497
|
+
$shouldExclude: [],
|
|
498
|
+
$shouldInclude: [],
|
|
499
|
+
$updateDOM: []
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
504
|
+
|
|
505
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
506
|
+
|
|
507
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
508
|
+
|
|
509
|
+
function ignoreNext2(acc) {
|
|
510
|
+
return (node, _$next, editor) => acc(node, editor);
|
|
511
|
+
}
|
|
512
|
+
function ignoreNext3(acc) {
|
|
513
|
+
return (node, a, _$next, editor) => acc(node, a, editor);
|
|
514
|
+
}
|
|
515
|
+
function ignoreNext4(acc) {
|
|
516
|
+
return (node, a, b, _$next, editor) => acc(node, a, b, editor);
|
|
517
|
+
}
|
|
518
|
+
function ignoreNext5(acc) {
|
|
519
|
+
return (node, a, b, c, _$next, editor) => acc(node, a, b, c, editor);
|
|
520
|
+
}
|
|
521
|
+
function merge2($acc, $getOverride) {
|
|
522
|
+
return (node, editor) => {
|
|
523
|
+
const $next = () => $acc(node, editor);
|
|
524
|
+
const $override = $getOverride(node);
|
|
525
|
+
return $override ? $override(node, $next, editor) : $next();
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
function merge3(acc, $getOverride) {
|
|
529
|
+
return (node, a, editor) => {
|
|
530
|
+
const $next = () => acc(node, a, editor);
|
|
531
|
+
const $override = $getOverride(node);
|
|
532
|
+
return $override ? $override(node, a, $next, editor) : $next();
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
const merge3GetDOMSlot = merge3;
|
|
536
|
+
const ignoreNext3GetDOMSlot = ignoreNext3;
|
|
537
|
+
function merge4($acc, $getOverride) {
|
|
538
|
+
return (node, a, b, editor) => {
|
|
539
|
+
const $next = () => $acc(node, a, b, editor);
|
|
540
|
+
const $override = $getOverride(node);
|
|
541
|
+
return $override ? $override(node, a, b, $next, editor) : $next();
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
function merge5(acc, $getOverride) {
|
|
545
|
+
return (node, a, b, c, editor) => {
|
|
546
|
+
const $next = () => acc(node, a, b, c, editor);
|
|
547
|
+
const $override = $getOverride(node);
|
|
548
|
+
return $override ? $override(node, a, b, c, $next, editor) : $next();
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
function sequence4($acc, $getOverride) {
|
|
552
|
+
return (node, a, b, editor) => {
|
|
553
|
+
$acc(node, a, b, editor);
|
|
554
|
+
const $override = $getOverride(node);
|
|
555
|
+
if ($override) {
|
|
556
|
+
$override(node, a, b, editor);
|
|
557
|
+
}
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
function compilePrerenderKey(prerender, k, defaults, mergeFunction, ignoreNextFunction) {
|
|
561
|
+
let acc = defaults[k];
|
|
562
|
+
for (const pair of prerender[k]) {
|
|
563
|
+
if (typeof pair[0] === 'function') {
|
|
564
|
+
const [$predicate, $override] = pair;
|
|
565
|
+
acc = mergeFunction(acc, node => $predicate(node) && $override || undefined);
|
|
566
|
+
} else {
|
|
567
|
+
const typeOverrides = pair[1];
|
|
568
|
+
const compiled = {};
|
|
569
|
+
for (const type in typeOverrides) {
|
|
570
|
+
const arr = typeOverrides[type];
|
|
571
|
+
if (arr) {
|
|
572
|
+
compiled[type] = arr.reduce(($acc, $override) => mergeFunction($acc, () => $override), acc);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
acc = mergeFunction(acc, node => {
|
|
576
|
+
const f = compiled[node.getType()];
|
|
577
|
+
return f && ignoreNextFunction(f);
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
defaults[k] = acc;
|
|
582
|
+
}
|
|
583
|
+
function addOverride(prerender, k, predicateOrTypes, override) {
|
|
584
|
+
if (!override) {
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
const arr = prerender[k];
|
|
588
|
+
if (typeof predicateOrTypes === 'function') {
|
|
589
|
+
arr.push([predicateOrTypes, override]);
|
|
590
|
+
} else {
|
|
591
|
+
const last = arr[arr.length - 1];
|
|
592
|
+
let types;
|
|
593
|
+
if (last && last[0] === 'types') {
|
|
594
|
+
types = last[1];
|
|
595
|
+
} else {
|
|
596
|
+
types = {};
|
|
597
|
+
arr.push(['types', types]);
|
|
598
|
+
}
|
|
599
|
+
for (const type in predicateOrTypes) {
|
|
600
|
+
const typeArr = types[type] || [];
|
|
601
|
+
types[type] = typeArr;
|
|
602
|
+
typeArr.push(override);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
function isWildcard(override) {
|
|
607
|
+
return override.nodes === '*';
|
|
608
|
+
}
|
|
609
|
+
function sortedOverrides(overrides) {
|
|
610
|
+
const byWildcard = [];
|
|
611
|
+
const byPredicate = [];
|
|
612
|
+
const byNode = [];
|
|
613
|
+
for (const override of overrides) {
|
|
614
|
+
if (isWildcard(override)) {
|
|
615
|
+
byWildcard.push(override);
|
|
616
|
+
} else if (Array.isArray(override.nodes)) {
|
|
617
|
+
for (const klassOrPredicate of override.nodes) {
|
|
618
|
+
if (lexical.$isLexicalNode(klassOrPredicate.prototype)) {
|
|
619
|
+
byNode.push(override.nodes.length === 1 ? override : {
|
|
620
|
+
...override,
|
|
621
|
+
nodes: [klassOrPredicate]
|
|
622
|
+
});
|
|
623
|
+
} else {
|
|
624
|
+
byPredicate.push(override.nodes.length === 1 ? override : {
|
|
625
|
+
...override,
|
|
626
|
+
nodes: [klassOrPredicate]
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
const depths = new Map();
|
|
633
|
+
const depthOf = klass => {
|
|
634
|
+
let depth = depths.get(klass);
|
|
635
|
+
if (depth === undefined) {
|
|
636
|
+
depth = 0;
|
|
637
|
+
for (let k = klass; lexical.$isLexicalNode(k.prototype); k = Object.getPrototypeOf(k)) {
|
|
638
|
+
depth++;
|
|
639
|
+
}
|
|
640
|
+
depths.set(klass, depth);
|
|
641
|
+
}
|
|
642
|
+
return depth;
|
|
643
|
+
};
|
|
644
|
+
byNode.sort((a, b) => depthOf(a.nodes[0]) - depthOf(b.nodes[0]));
|
|
645
|
+
return [...byNode, ...byPredicate, ...byWildcard];
|
|
646
|
+
}
|
|
647
|
+
function precompileDOMRenderConfigOverrides(editorConfig, overrides) {
|
|
648
|
+
const typeTree = buildTypeTree(editorConfig);
|
|
649
|
+
const prerender = makePrerender();
|
|
650
|
+
for (const override of sortedOverrides(overrides)) {
|
|
651
|
+
const predicateOrTypes = getPredicate(typeTree, override);
|
|
652
|
+
for (const k_ in prerender) {
|
|
653
|
+
const k = k_;
|
|
654
|
+
addOverride(prerender, k, predicateOrTypes, override[k]);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
return prerender;
|
|
658
|
+
}
|
|
659
|
+
function identity(v) {
|
|
660
|
+
return v;
|
|
661
|
+
}
|
|
662
|
+
function compileDOMRenderConfigOverrides(editorConfig, {
|
|
663
|
+
overrides
|
|
664
|
+
}) {
|
|
665
|
+
const prerender = precompileDOMRenderConfigOverrides(editorConfig, overrides);
|
|
666
|
+
const dom = {
|
|
667
|
+
...lexical.DEFAULT_EDITOR_DOM_CONFIG,
|
|
668
|
+
...editorConfig.dom
|
|
669
|
+
};
|
|
670
|
+
compilePrerenderKey(prerender, '$createDOM', dom, merge2, ignoreNext2);
|
|
671
|
+
compilePrerenderKey(prerender, '$exportDOM', dom, merge2, ignoreNext2);
|
|
672
|
+
compilePrerenderKey(prerender, '$extractWithChild', dom, merge5, ignoreNext5);
|
|
673
|
+
compilePrerenderKey(prerender, '$getDOMSlot', dom, merge3GetDOMSlot, ignoreNext3GetDOMSlot);
|
|
674
|
+
compilePrerenderKey(prerender, '$shouldExclude', dom, merge3, ignoreNext3);
|
|
675
|
+
compilePrerenderKey(prerender, '$shouldInclude', dom, merge3, ignoreNext3);
|
|
676
|
+
compilePrerenderKey(prerender, '$updateDOM', dom, merge4, ignoreNext4);
|
|
677
|
+
compilePrerenderKey(prerender, '$decorateDOM', dom, sequence4, identity);
|
|
678
|
+
return dom;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
683
|
+
*
|
|
684
|
+
* This source code is licensed under the MIT license found in the
|
|
685
|
+
* LICENSE file in the root directory of this source tree.
|
|
686
|
+
*
|
|
687
|
+
*/
|
|
688
|
+
|
|
689
|
+
function makeReader(record) {
|
|
690
|
+
return {
|
|
691
|
+
get(cfg) {
|
|
692
|
+
return getContextValue(record, cfg);
|
|
693
|
+
}
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* The mutable, writable editor-level context record. Reads of a render state
|
|
699
|
+
* during reconciliation (and as the base layer of a session) fall through to
|
|
700
|
+
* this record, and it is the layer the `disabledForEditor` predicates read.
|
|
701
|
+
*
|
|
702
|
+
* @internal
|
|
703
|
+
*/
|
|
704
|
+
function createEditorContextRecord(contextDefaults) {
|
|
705
|
+
const parent = Object.create(null);
|
|
706
|
+
return contextFromPairs(contextDefaults, parent) || parent;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* Filter the configured overrides down to those that are resident in the
|
|
711
|
+
* editor's render config, removing any whose `disabledForEditor` predicate
|
|
712
|
+
* returns `true` for the given editor context.
|
|
713
|
+
*
|
|
714
|
+
* @internal
|
|
715
|
+
*/
|
|
716
|
+
function filterEditorInstalled(overrides, record) {
|
|
717
|
+
const reader = makeReader(record);
|
|
718
|
+
return overrides.filter(o => !(o.disabledForEditor && o.disabledForEditor(reader)));
|
|
719
|
+
}
|
|
720
|
+
function sameOverrides(a, b) {
|
|
721
|
+
if (a.length !== b.length) {
|
|
722
|
+
return false;
|
|
723
|
+
}
|
|
724
|
+
for (let i = 0; i < a.length; i++) {
|
|
725
|
+
if (a[i] !== b[i]) {
|
|
726
|
+
return false;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
return true;
|
|
730
|
+
}
|
|
731
|
+
function symmetricDiff(prev, next) {
|
|
732
|
+
const prevSet = new Set(prev);
|
|
733
|
+
const nextSet = new Set(next);
|
|
734
|
+
const changed = [];
|
|
735
|
+
for (const o of prev) {
|
|
736
|
+
if (!nextSet.has(o)) {
|
|
737
|
+
changed.push(o);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
for (const o of next) {
|
|
741
|
+
if (!prevSet.has(o)) {
|
|
742
|
+
changed.push(o);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
return changed;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* Build a predicate matching the nodes an override targets — `'*'` matches
|
|
750
|
+
* everything, a node class matches by `instanceof`, and a guard is used as-is.
|
|
751
|
+
*/
|
|
752
|
+
function nodeMatcher(o) {
|
|
753
|
+
if (o.nodes === '*') {
|
|
754
|
+
return () => true;
|
|
755
|
+
}
|
|
756
|
+
const matchers = o.nodes.map(match => {
|
|
757
|
+
const klass = match;
|
|
758
|
+
return lexical.$isLexicalNode(klass.prototype) ? node => node instanceof klass : match;
|
|
759
|
+
});
|
|
760
|
+
return node => matchers.some(f => f(node));
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* Build a predicate matching the nodes whose DOM must be recreated for the
|
|
765
|
+
* given override change, or `null` when no live re-render is needed.
|
|
766
|
+
*
|
|
767
|
+
* `$createDOM`/`$getDOMSlot` produce the element and slot, and `$decorateDOM`
|
|
768
|
+
* may add DOM that only a fresh `$createDOM` can revert — so toggling any of
|
|
769
|
+
* them recreates the affected nodes. `$updateDOM` is diff-driven and applies on
|
|
770
|
+
* the next node update, and export-only hooks ($exportDOM/$shouldInclude/…)
|
|
771
|
+
* don't touch the live DOM, so neither needs a re-render. Recreating every
|
|
772
|
+
* affected node is the simple, always-correct choice; toggles are rare, so the
|
|
773
|
+
* cost is acceptable and can be optimized later if needed.
|
|
774
|
+
*/
|
|
775
|
+
function recreatePredicate(changed) {
|
|
776
|
+
const matchers = [];
|
|
777
|
+
for (const o of changed) {
|
|
778
|
+
if (o.$createDOM || o.$getDOMSlot || o.$decorateDOM) {
|
|
779
|
+
matchers.push(nodeMatcher(o));
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
return matchers.length === 0 ? null : node => matchers.some(f => f(node));
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* Per-editor runtime backing {@link DOMRenderExtension}'s conditional
|
|
787
|
+
* overrides and imperative editor context. See {@link DOMRenderRuntime}.
|
|
788
|
+
*
|
|
789
|
+
* @internal
|
|
790
|
+
*/
|
|
791
|
+
class DOMRenderRuntimeImpl {
|
|
792
|
+
editor;
|
|
793
|
+
/**
|
|
794
|
+
* The `nodes` and base `dom` captured at `init` (before `dom` was
|
|
795
|
+
* overwritten with the compiled config) — the clean base for every recompile.
|
|
796
|
+
*/
|
|
797
|
+
initialEditorConfig;
|
|
798
|
+
overrides;
|
|
799
|
+
editorContext;
|
|
800
|
+
hasSessionGates;
|
|
801
|
+
installed;
|
|
802
|
+
|
|
803
|
+
/** Memoized session configs keyed by the set of session-disabled overrides. */
|
|
804
|
+
sessionCache = new Map();
|
|
805
|
+
constructor(editor, initialEditorConfig, overrides, editorContext) {
|
|
806
|
+
this.editor = editor;
|
|
807
|
+
this.initialEditorConfig = initialEditorConfig;
|
|
808
|
+
this.overrides = overrides;
|
|
809
|
+
this.editorContext = editorContext;
|
|
810
|
+
this.installed = filterEditorInstalled(overrides, editorContext);
|
|
811
|
+
this.hasSessionGates = overrides.some(o => o.disabledForSession);
|
|
812
|
+
}
|
|
813
|
+
setContextValue(cfg, value) {
|
|
814
|
+
const prev = this.installed;
|
|
815
|
+
this.editorContext[cfg.key] = value;
|
|
816
|
+
const next = filterEditorInstalled(this.overrides, this.editorContext);
|
|
817
|
+
if (sameOverrides(prev, next)) {
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
const changed = symmetricDiff(prev, next);
|
|
821
|
+
this.installed = next;
|
|
822
|
+
this.sessionCache.clear();
|
|
823
|
+
const dom = compileDOMRenderConfigOverrides(this.initialEditorConfig, {
|
|
824
|
+
overrides: next
|
|
825
|
+
});
|
|
826
|
+
this.editor._config.dom = dom;
|
|
827
|
+
const recreate = recreatePredicate(changed);
|
|
828
|
+
if (!recreate) {
|
|
829
|
+
// $updateDOM-only or export-only change: the recompiled config is enough.
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// Re-render through a full reconcile, which reuses the existing node
|
|
834
|
+
// instances (no node-map mutation, so no spurious mutation/collaboration
|
|
835
|
+
// changes). The affected nodes must be unmounted and recreated — the removed
|
|
836
|
+
// override may have produced or decorated DOM that only a fresh $createDOM
|
|
837
|
+
// reverts — so install a transient $updateDOM that reports a recreate for
|
|
838
|
+
// matching nodes.
|
|
839
|
+
//
|
|
840
|
+
// This mutates the (shared) active config, so the reconcile MUST run and
|
|
841
|
+
// finish synchronously before the original is restored on the next line —
|
|
842
|
+
// hence `discrete`, and hence this must not be called from within an
|
|
843
|
+
// editor.update (where the commit would defer). A deferred update would
|
|
844
|
+
// either restore the wrapper before the reconcile reads it (no recreate) or
|
|
845
|
+
// leave it armed across a window where an unrelated reconcile would
|
|
846
|
+
// spuriously recreate matching nodes. No history tag is needed: a full
|
|
847
|
+
// reconcile marks no nodes dirty, which history merges/discards without
|
|
848
|
+
// pushing.
|
|
849
|
+
const base = dom.$updateDOM;
|
|
850
|
+
dom.$updateDOM = (nextNode, prevNode, el, editor) => recreate(nextNode) ? true : base(nextNode, prevNode, el, editor);
|
|
851
|
+
this.editor.update(lexical.$fullReconcile, {
|
|
852
|
+
discrete: true
|
|
853
|
+
});
|
|
854
|
+
dom.$updateDOM = base;
|
|
855
|
+
}
|
|
856
|
+
getSessionConfig() {
|
|
857
|
+
const resident = this.editor._config.dom || lexical.DEFAULT_EDITOR_DOM_CONFIG;
|
|
858
|
+
if (!this.hasSessionGates) {
|
|
859
|
+
return resident;
|
|
860
|
+
}
|
|
861
|
+
const reader = makeReader(getContextRecord(DOMRenderContextSymbol, this.editor) || this.editorContext);
|
|
862
|
+
const disabledKeys = [];
|
|
863
|
+
const sessionSet = [];
|
|
864
|
+
this.installed.forEach((o, i) => {
|
|
865
|
+
if (o.disabledForSession && o.disabledForSession(reader)) {
|
|
866
|
+
disabledKeys.push(String(i));
|
|
867
|
+
} else {
|
|
868
|
+
sessionSet.push(o);
|
|
869
|
+
}
|
|
870
|
+
});
|
|
871
|
+
if (disabledKeys.length === 0) {
|
|
872
|
+
return resident;
|
|
873
|
+
}
|
|
874
|
+
const key = disabledKeys.join(',');
|
|
875
|
+
let cfg = this.sessionCache.get(key);
|
|
876
|
+
if (!cfg) {
|
|
877
|
+
cfg = compileDOMRenderConfigOverrides(this.initialEditorConfig, {
|
|
878
|
+
overrides: sessionSet
|
|
879
|
+
});
|
|
880
|
+
this.sessionCache.set(key, cfg);
|
|
881
|
+
}
|
|
882
|
+
return cfg;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
/**
|
|
887
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
888
|
+
*
|
|
889
|
+
* This source code is licensed under the MIT license found in the
|
|
890
|
+
* LICENSE file in the root directory of this source tree.
|
|
891
|
+
*
|
|
892
|
+
*/
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
/** @internal The result returned from {@link DOMRenderExtension}'s `init`. */
|
|
896
|
+
|
|
897
|
+
/**
|
|
898
|
+
* @experimental
|
|
899
|
+
*
|
|
900
|
+
* An extension that allows overriding the render and export behavior for an
|
|
901
|
+
* editor. This is highly experimental and subject to change from one version
|
|
902
|
+
* to the next.
|
|
903
|
+
**/
|
|
904
|
+
const DOMRenderExtension = lexical.defineExtension({
|
|
905
|
+
build(editor, config, state) {
|
|
906
|
+
const {
|
|
907
|
+
initialEditorConfig
|
|
908
|
+
} = state.getInitResult();
|
|
909
|
+
const editorContext = createEditorContextRecord(config.contextDefaults);
|
|
910
|
+
const runtime = new DOMRenderRuntimeImpl(editor, initialEditorConfig, config.overrides, editorContext);
|
|
911
|
+
return {
|
|
912
|
+
defaults: editorContext,
|
|
913
|
+
runtime
|
|
914
|
+
};
|
|
915
|
+
},
|
|
916
|
+
config: {
|
|
917
|
+
contextDefaults: [],
|
|
918
|
+
overrides: []
|
|
919
|
+
},
|
|
920
|
+
html: {
|
|
921
|
+
// Define a RootNode export for $generateDOMFromRoot
|
|
922
|
+
export: new Map([[lexical.RootNode, () => {
|
|
923
|
+
const element = document.createElement('div');
|
|
924
|
+
element.role = 'textbox';
|
|
925
|
+
return {
|
|
926
|
+
element
|
|
927
|
+
};
|
|
928
|
+
}]])
|
|
929
|
+
},
|
|
930
|
+
init(editorConfig, config) {
|
|
931
|
+
// Capture the user's base `dom` (before we overwrite it) and `nodes` so the
|
|
932
|
+
// runtime can recompile from scratch when overrides toggle.
|
|
933
|
+
const initialEditorConfig = {
|
|
934
|
+
dom: editorConfig.dom,
|
|
935
|
+
nodes: editorConfig.nodes
|
|
936
|
+
};
|
|
937
|
+
const editorContext = createEditorContextRecord(config.contextDefaults);
|
|
938
|
+
const installed = filterEditorInstalled(config.overrides, editorContext);
|
|
939
|
+
editorConfig.dom = compileDOMRenderConfigOverrides(editorConfig, {
|
|
940
|
+
overrides: installed
|
|
941
|
+
});
|
|
942
|
+
return {
|
|
943
|
+
initialEditorConfig
|
|
944
|
+
};
|
|
945
|
+
},
|
|
946
|
+
mergeConfig(config, partial) {
|
|
947
|
+
const merged = lexical.shallowMergeConfig(config, partial);
|
|
948
|
+
for (const k of ['overrides', 'contextDefaults']) {
|
|
949
|
+
if (partial[k]) {
|
|
950
|
+
merged[k] = [...config[k], ...partial[k]];
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
return merged;
|
|
954
|
+
},
|
|
955
|
+
name: DOMRenderExtensionName
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
/**
|
|
959
|
+
* @internal
|
|
960
|
+
*
|
|
961
|
+
* A predicate that may write into the per-invocation `captures` map. Returns
|
|
962
|
+
* `true` if the rule matches; `false` otherwise.
|
|
963
|
+
*/
|
|
964
|
+
|
|
965
|
+
/** @internal */
|
|
966
|
+
|
|
967
|
+
/** @internal The runtime shape of a {@link CompiledSelector}. */
|
|
968
|
+
|
|
969
|
+
const IMPL = Symbol.for('@lexical/html/SelectorImpl');
|
|
970
|
+
|
|
971
|
+
/** @internal */
|
|
972
|
+
function getSelectorImpl(sel) {
|
|
973
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
974
|
+
const impl = sel[IMPL];
|
|
975
|
+
if (!(impl !== undefined)) {
|
|
976
|
+
formatDevErrorMessage(`match must be a CompiledSelector produced by sel.* or sel.css(); received a raw object.`);
|
|
977
|
+
}
|
|
978
|
+
return impl;
|
|
979
|
+
}
|
|
980
|
+
function combinePredicates(preds) {
|
|
981
|
+
if (preds.length === 0) {
|
|
982
|
+
return lexical.isHTMLElement;
|
|
983
|
+
}
|
|
984
|
+
if (preds.length === 1) {
|
|
985
|
+
return preds[0];
|
|
986
|
+
}
|
|
987
|
+
return (node, captures) => {
|
|
988
|
+
for (const p of preds) {
|
|
989
|
+
if (!p(node, captures)) {
|
|
990
|
+
return false;
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
return true;
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
/**
|
|
998
|
+
* @internal
|
|
999
|
+
*
|
|
1000
|
+
* Build a selector value from a tag set and a predicate list. Used by the
|
|
1001
|
+
* combinator API and the CSS parser.
|
|
1002
|
+
*/
|
|
1003
|
+
function buildSelector(tags, predicates) {
|
|
1004
|
+
const impl = {
|
|
1005
|
+
kind: 'element',
|
|
1006
|
+
predicate: combinePredicates(predicates),
|
|
1007
|
+
tags
|
|
1008
|
+
};
|
|
1009
|
+
const refine = additional => buildSelector(tags, [...predicates, additional]);
|
|
1010
|
+
const builder = {
|
|
1011
|
+
[IMPL]: impl,
|
|
1012
|
+
attr: (name, value, options) => refine(buildAttrPredicate(name, value, options)),
|
|
1013
|
+
classAll: (...classes) => refine(buildClassAllPredicate(classes)),
|
|
1014
|
+
classAny: (...classes) => refine(buildClassAnyPredicate(classes)),
|
|
1015
|
+
styleAny: (prop, value, options) => refine(buildStylePredicate(prop, value, options))
|
|
1016
|
+
};
|
|
1017
|
+
// The runtime is fully type-erased; cast to satisfy the surface.
|
|
1018
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1019
|
+
return builder;
|
|
1020
|
+
}
|
|
1021
|
+
function normalizeClassList(classes) {
|
|
1022
|
+
const out = [];
|
|
1023
|
+
for (const c of classes) {
|
|
1024
|
+
if (c) {
|
|
1025
|
+
out.push(c);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
return out;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
/** @internal */
|
|
1032
|
+
function buildClassAllPredicate(classes) {
|
|
1033
|
+
const ns = normalizeClassList(classes);
|
|
1034
|
+
if (ns.length === 0) {
|
|
1035
|
+
return () => true;
|
|
1036
|
+
}
|
|
1037
|
+
return node => {
|
|
1038
|
+
if (!lexical.isHTMLElement(node)) {
|
|
1039
|
+
return false;
|
|
1040
|
+
}
|
|
1041
|
+
const cl = node.classList;
|
|
1042
|
+
for (const c of ns) {
|
|
1043
|
+
if (!cl.contains(c)) {
|
|
1044
|
+
return false;
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
return true;
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
/** @internal */
|
|
1052
|
+
function buildClassAnyPredicate(classes) {
|
|
1053
|
+
const ns = normalizeClassList(classes);
|
|
1054
|
+
if (ns.length === 0) {
|
|
1055
|
+
return () => false;
|
|
1056
|
+
}
|
|
1057
|
+
return node => {
|
|
1058
|
+
if (!lexical.isHTMLElement(node)) {
|
|
1059
|
+
return false;
|
|
1060
|
+
}
|
|
1061
|
+
const cl = node.classList;
|
|
1062
|
+
for (const c of ns) {
|
|
1063
|
+
if (cl.contains(c)) {
|
|
1064
|
+
return true;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
return false;
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
/** @internal */
|
|
1072
|
+
function buildAttrPredicate(name, value, options) {
|
|
1073
|
+
if (value === true) {
|
|
1074
|
+
return node => lexical.isHTMLElement(node) && node.hasAttribute(name);
|
|
1075
|
+
}
|
|
1076
|
+
if (typeof value === 'string') {
|
|
1077
|
+
return node => lexical.isHTMLElement(node) && node.getAttribute(name) === value;
|
|
1078
|
+
}
|
|
1079
|
+
if (value instanceof RegExp) {
|
|
1080
|
+
const capture = options && options.capture;
|
|
1081
|
+
const re = value;
|
|
1082
|
+
return (node, captures) => {
|
|
1083
|
+
if (!lexical.isHTMLElement(node)) {
|
|
1084
|
+
return false;
|
|
1085
|
+
}
|
|
1086
|
+
const v = node.getAttribute(name);
|
|
1087
|
+
if (v == null) {
|
|
1088
|
+
return false;
|
|
1089
|
+
}
|
|
1090
|
+
const m = v.match(re);
|
|
1091
|
+
if (m === null) {
|
|
1092
|
+
return false;
|
|
1093
|
+
}
|
|
1094
|
+
if (capture !== undefined) {
|
|
1095
|
+
captures[capture] = m;
|
|
1096
|
+
}
|
|
1097
|
+
return true;
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
{
|
|
1101
|
+
formatDevErrorMessage(`sel.attr(${JSON.stringify(name)}, ...) requires true, a string, or a RegExp`);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
function buildStylePredicate(prop, value, options) {
|
|
1105
|
+
if (typeof value === 'string') {
|
|
1106
|
+
return node => lexical.isHTMLElement(node) && node.style.getPropertyValue(prop) === value;
|
|
1107
|
+
}
|
|
1108
|
+
if (value instanceof RegExp) {
|
|
1109
|
+
const capture = options && options.capture;
|
|
1110
|
+
const re = value;
|
|
1111
|
+
return (node, captures) => {
|
|
1112
|
+
if (!lexical.isHTMLElement(node)) {
|
|
1113
|
+
return false;
|
|
1114
|
+
}
|
|
1115
|
+
const v = node.style.getPropertyValue(prop);
|
|
1116
|
+
if (!v) {
|
|
1117
|
+
return false;
|
|
1118
|
+
}
|
|
1119
|
+
const m = v.match(re);
|
|
1120
|
+
if (m === null) {
|
|
1121
|
+
return false;
|
|
1122
|
+
}
|
|
1123
|
+
if (capture !== undefined) {
|
|
1124
|
+
captures[capture] = m;
|
|
1125
|
+
}
|
|
1126
|
+
return true;
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
{
|
|
1130
|
+
formatDevErrorMessage(`sel.styleAny(${JSON.stringify(prop)}, ...) requires a string or a RegExp`);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
const TEXT_SELECTOR_IMPL = {
|
|
1134
|
+
kind: 'text',
|
|
1135
|
+
predicate: lexical.isDOMTextNode,
|
|
1136
|
+
tags: new Set()
|
|
1137
|
+
};
|
|
1138
|
+
|
|
1139
|
+
// The `as` cast is needed because `CompiledSelector` is an opaque
|
|
1140
|
+
// branded interface — neither the object literal nor a typed const can
|
|
1141
|
+
// declare the internal `IMPL` symbol without exposing it.
|
|
1142
|
+
const TEXT_SELECTOR = {
|
|
1143
|
+
[IMPL]: TEXT_SELECTOR_IMPL
|
|
1144
|
+
};
|
|
1145
|
+
const COMMENT_SELECTOR_IMPL = {
|
|
1146
|
+
kind: 'comment',
|
|
1147
|
+
predicate: node => node.nodeType === 8 /* COMMENT_NODE */,
|
|
1148
|
+
tags: new Set()
|
|
1149
|
+
};
|
|
1150
|
+
const COMMENT_SELECTOR = {
|
|
1151
|
+
[IMPL]: COMMENT_SELECTOR_IMPL
|
|
1152
|
+
};
|
|
1153
|
+
|
|
1154
|
+
/**
|
|
1155
|
+
* Combinator API for building {@link CompiledSelector}s. The public
|
|
1156
|
+
* `sel` is augmented from this in `./index.ts` (where the CSS parser is
|
|
1157
|
+
* available without a circular import); consumers outside `@lexical/html`
|
|
1158
|
+
* should always import the public `sel` from the package root.
|
|
1159
|
+
*
|
|
1160
|
+
* @internal
|
|
1161
|
+
*/
|
|
1162
|
+
const selBase = {
|
|
1163
|
+
/** Match any {@link HTMLElement}. */
|
|
1164
|
+
any() {
|
|
1165
|
+
return buildSelector(new Set(), []);
|
|
1166
|
+
},
|
|
1167
|
+
/** Match DOM {@link Comment} nodes. */
|
|
1168
|
+
comment() {
|
|
1169
|
+
return COMMENT_SELECTOR;
|
|
1170
|
+
},
|
|
1171
|
+
/**
|
|
1172
|
+
* Match by tag name(s). With one literal tag the element type is narrowed
|
|
1173
|
+
* (e.g. `'a' → HTMLAnchorElement`); with multiple, it is the union of
|
|
1174
|
+
* their `HTMLElementTagNameMap` entries.
|
|
1175
|
+
*/
|
|
1176
|
+
tag(...tags) {
|
|
1177
|
+
if (!(tags.length > 0)) {
|
|
1178
|
+
formatDevErrorMessage(`sel.tag() requires at least one tag name`);
|
|
1179
|
+
}
|
|
1180
|
+
const upper = new Set();
|
|
1181
|
+
for (const t of tags) {
|
|
1182
|
+
upper.add(t.toUpperCase());
|
|
1183
|
+
}
|
|
1184
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1185
|
+
return buildSelector(upper, []);
|
|
1186
|
+
},
|
|
1187
|
+
/** Match DOM {@link Text} nodes. */
|
|
1188
|
+
text() {
|
|
1189
|
+
return TEXT_SELECTOR;
|
|
1190
|
+
}
|
|
1191
|
+
};
|
|
1192
|
+
|
|
1193
|
+
/**
|
|
1194
|
+
* Cross-frame-safe replacement for `node instanceof HTMLXxxElement`. Returns
|
|
1195
|
+
* true when `node` is an HTMLElement whose `nodeName` equals `tag` (compared
|
|
1196
|
+
* case-insensitively).
|
|
1197
|
+
*
|
|
1198
|
+
* @experimental
|
|
1199
|
+
*/
|
|
1200
|
+
function isElementOfTag(node, tag) {
|
|
1201
|
+
return lexical.isHTMLElement(node) && node.nodeName === tag.toUpperCase();
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
const IDENT_CHAR = /[A-Za-z0-9_-]/;
|
|
1205
|
+
class Cursor {
|
|
1206
|
+
constructor(source, pos) {
|
|
1207
|
+
this.source = source;
|
|
1208
|
+
this.pos = pos;
|
|
1209
|
+
}
|
|
1210
|
+
peek(offset = 0) {
|
|
1211
|
+
return this.source[this.pos + offset] || '';
|
|
1212
|
+
}
|
|
1213
|
+
consume() {
|
|
1214
|
+
return this.source[this.pos++] || '';
|
|
1215
|
+
}
|
|
1216
|
+
eof() {
|
|
1217
|
+
return this.pos >= this.source.length;
|
|
1218
|
+
}
|
|
1219
|
+
skipWhitespace() {
|
|
1220
|
+
while (!this.eof() && /\s/.test(this.peek())) {
|
|
1221
|
+
this.pos++;
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
readIdent() {
|
|
1225
|
+
const start = this.pos;
|
|
1226
|
+
while (!this.eof() && IDENT_CHAR.test(this.peek())) {
|
|
1227
|
+
this.pos++;
|
|
1228
|
+
}
|
|
1229
|
+
return this.source.slice(start, this.pos);
|
|
1230
|
+
}
|
|
1231
|
+
readQuoted() {
|
|
1232
|
+
const quote = this.consume();
|
|
1233
|
+
this.assert(quote === '"' || quote === "'", 'expected quote');
|
|
1234
|
+
const start = this.pos;
|
|
1235
|
+
while (!this.eof() && this.peek() !== quote) {
|
|
1236
|
+
if (this.peek() === '\\') {
|
|
1237
|
+
this.pos += 2;
|
|
1238
|
+
} else {
|
|
1239
|
+
this.pos++;
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
this.assert(!this.eof(), 'unterminated string');
|
|
1243
|
+
const value = this.source.slice(start, this.pos);
|
|
1244
|
+
this.pos++; // consume closing quote
|
|
1245
|
+
return value.replace(/\\(.)/g, '$1');
|
|
1246
|
+
}
|
|
1247
|
+
/**
|
|
1248
|
+
* `invariant(cond, fmt, …)`-flavored assertion that also surfaces the
|
|
1249
|
+
* cursor's position context. Use for parse-time errors so a malformed
|
|
1250
|
+
* CSS selector gets a useful, position-annotated message.
|
|
1251
|
+
*/
|
|
1252
|
+
assert(cond, msg) {
|
|
1253
|
+
if (!cond) {
|
|
1254
|
+
formatDevErrorMessage(`invalid CSS selector at col ${String(this.pos + 1)}: ${msg} in ${this.source}`);
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
function parseSimpleSelector(c) {
|
|
1259
|
+
const tags = new Set();
|
|
1260
|
+
const predicates = [];
|
|
1261
|
+
const classes = [];
|
|
1262
|
+
c.skipWhitespace();
|
|
1263
|
+
|
|
1264
|
+
// Optional tag or '*'
|
|
1265
|
+
if (c.peek() === '*') {
|
|
1266
|
+
c.consume();
|
|
1267
|
+
} else if (IDENT_CHAR.test(c.peek())) {
|
|
1268
|
+
const tag = c.readIdent();
|
|
1269
|
+
if (tag) {
|
|
1270
|
+
tags.add(tag.toUpperCase());
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
// Zero or more refinements: .class, #id, [attr]
|
|
1275
|
+
while (!c.eof()) {
|
|
1276
|
+
const ch = c.peek();
|
|
1277
|
+
if (ch === '.') {
|
|
1278
|
+
c.consume();
|
|
1279
|
+
const cls = c.readIdent();
|
|
1280
|
+
c.assert(cls !== '', 'expected class name after "."');
|
|
1281
|
+
classes.push(cls);
|
|
1282
|
+
} else if (ch === '#') {
|
|
1283
|
+
c.consume();
|
|
1284
|
+
const id = c.readIdent();
|
|
1285
|
+
c.assert(id !== '', 'expected id after "#"');
|
|
1286
|
+
predicates.push(buildAttrPredicate('id', id));
|
|
1287
|
+
} else if (ch === '[') {
|
|
1288
|
+
c.consume();
|
|
1289
|
+
c.skipWhitespace();
|
|
1290
|
+
const name = c.readIdent();
|
|
1291
|
+
c.assert(name !== '', 'expected attribute name after "["');
|
|
1292
|
+
c.skipWhitespace();
|
|
1293
|
+
let value = true;
|
|
1294
|
+
if (c.peek() === '=') {
|
|
1295
|
+
c.consume();
|
|
1296
|
+
c.skipWhitespace();
|
|
1297
|
+
const next = c.peek();
|
|
1298
|
+
if (next === '"' || next === "'") {
|
|
1299
|
+
value = c.readQuoted();
|
|
1300
|
+
} else {
|
|
1301
|
+
value = c.readIdent();
|
|
1302
|
+
c.assert(value !== '', 'expected attribute value');
|
|
1303
|
+
}
|
|
1304
|
+
c.skipWhitespace();
|
|
1305
|
+
}
|
|
1306
|
+
c.assert(c.peek() === ']', 'expected "]"');
|
|
1307
|
+
c.consume();
|
|
1308
|
+
predicates.push(buildAttrPredicate(name, value));
|
|
1309
|
+
} else {
|
|
1310
|
+
break;
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
if (classes.length > 0) {
|
|
1314
|
+
predicates.push(buildClassAllPredicate(classes));
|
|
1315
|
+
}
|
|
1316
|
+
return {
|
|
1317
|
+
predicates,
|
|
1318
|
+
tags
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
/**
|
|
1323
|
+
* Parse a reduced CSS-selector subset and return a {@link CompiledSelector}.
|
|
1324
|
+
* Supported:
|
|
1325
|
+
* - Tag (`p`), wildcard (`*`).
|
|
1326
|
+
* - Tag list (`h1, h2, h3`).
|
|
1327
|
+
* - Class (`.foo`, `.foo.bar`).
|
|
1328
|
+
* - ID (`#foo`).
|
|
1329
|
+
* - Attribute presence (`[name]`).
|
|
1330
|
+
* - Attribute equality (`[name="value"]`, `[name=value]`).
|
|
1331
|
+
*
|
|
1332
|
+
* Anything outside the subset (regex attribute, inline-style match,
|
|
1333
|
+
* combinators, pseudo-classes) is intentionally rejected — chain combinator
|
|
1334
|
+
* methods off the returned builder instead.
|
|
1335
|
+
*
|
|
1336
|
+
* @experimental
|
|
1337
|
+
*/
|
|
1338
|
+
function parseSelector(source) {
|
|
1339
|
+
const c = new Cursor(source, 0);
|
|
1340
|
+
const groups = [];
|
|
1341
|
+
while (true) {
|
|
1342
|
+
const group = parseSimpleSelector(c);
|
|
1343
|
+
groups.push(group);
|
|
1344
|
+
c.skipWhitespace();
|
|
1345
|
+
if (c.eof()) {
|
|
1346
|
+
break;
|
|
1347
|
+
}
|
|
1348
|
+
c.assert(c.peek() === ',', 'expected "," (selector lists are the only supported combinator)');
|
|
1349
|
+
c.consume();
|
|
1350
|
+
c.skipWhitespace();
|
|
1351
|
+
}
|
|
1352
|
+
if (groups.length === 1) {
|
|
1353
|
+
return buildSelector(groups[0].tags, groups[0].predicates);
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
// Comma-separated list. Merge tag sets and OR-combine the per-group
|
|
1357
|
+
// refinement predicates so that each candidate node satisfies *some*
|
|
1358
|
+
// group entirely.
|
|
1359
|
+
const tags = new Set();
|
|
1360
|
+
for (const g of groups) {
|
|
1361
|
+
for (const t of g.tags) {
|
|
1362
|
+
tags.add(t);
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
const orPredicate = (node, captures) => {
|
|
1366
|
+
for (const g of groups) {
|
|
1367
|
+
const upper = node.nodeName;
|
|
1368
|
+
if (g.tags.size > 0 && !g.tags.has(upper)) {
|
|
1369
|
+
continue;
|
|
1370
|
+
}
|
|
1371
|
+
let ok = true;
|
|
1372
|
+
for (const p of g.predicates) {
|
|
1373
|
+
if (!p(node, captures)) {
|
|
1374
|
+
ok = false;
|
|
1375
|
+
break;
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
if (ok) {
|
|
1379
|
+
return true;
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
return false;
|
|
1383
|
+
};
|
|
1384
|
+
return buildSelector(tags, [orPredicate]);
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
/**
|
|
1388
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
1389
|
+
*
|
|
1390
|
+
* This source code is licensed under the MIT license found in the
|
|
1391
|
+
* LICENSE file in the root directory of this source tree.
|
|
1392
|
+
*
|
|
1393
|
+
*/
|
|
1394
|
+
|
|
1395
|
+
/**
|
|
1396
|
+
* Identity helper that infers a rule's matched node type and capture map
|
|
1397
|
+
* from its `match` selector and threads them into the `$import` signature.
|
|
1398
|
+
* Usage:
|
|
1399
|
+
*
|
|
1400
|
+
* ```ts
|
|
1401
|
+
* defineImportRule({
|
|
1402
|
+
* name: '@lexical/list/li',
|
|
1403
|
+
* match: sel.tag('li'),
|
|
1404
|
+
* $import: (ctx, el, $next) => {
|
|
1405
|
+
* // el: HTMLLIElement
|
|
1406
|
+
* return [$createListItemNode()];
|
|
1407
|
+
* },
|
|
1408
|
+
* });
|
|
1409
|
+
* ```
|
|
1410
|
+
*
|
|
1411
|
+
* @experimental
|
|
1412
|
+
* @__NO_SIDE_EFFECTS__
|
|
1413
|
+
*/
|
|
1414
|
+
function defineImportRule(rule) {
|
|
1415
|
+
return rule;
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
/**
|
|
1419
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
1420
|
+
*
|
|
1421
|
+
* This source code is licensed under the MIT license found in the
|
|
1422
|
+
* LICENSE file in the root directory of this source tree.
|
|
1423
|
+
*
|
|
1424
|
+
*/
|
|
1425
|
+
|
|
1426
|
+
/**
|
|
1427
|
+
* Create an import context state. The phantom symbol prevents accidental
|
|
1428
|
+
* use of a render-context state in an import context (and vice versa).
|
|
1429
|
+
*
|
|
1430
|
+
* Note: to support the value-or-updater pattern, `V` cannot be a function
|
|
1431
|
+
* type; wrap it in an array or object if needed.
|
|
1432
|
+
*
|
|
1433
|
+
* `getDefaultValue` is called **once at state creation** and the result is
|
|
1434
|
+
* shared between every session that reads the state without first writing
|
|
1435
|
+
* a value. Defaults must therefore be immutable (primitives, frozen
|
|
1436
|
+
* objects, or read-only arrays / records). If your state needs mutable
|
|
1437
|
+
* per-session storage, lazily initialize it inside your rule (e.g.
|
|
1438
|
+
* `if (!ctx.session.has(cfg)) ctx.session.set(cfg, new …())`).
|
|
1439
|
+
*
|
|
1440
|
+
* @experimental
|
|
1441
|
+
* @__NO_SIDE_EFFECTS__
|
|
1442
|
+
*/
|
|
1443
|
+
function createImportState(name, getDefaultValue, isEqual) {
|
|
1444
|
+
return createContextState(DOMImportContextSymbol, name, getDefaultValue, isEqual);
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
/**
|
|
1448
|
+
* The kind of operation that produced this import. Lets rules adapt
|
|
1449
|
+
* their behavior (e.g. preserve more whitespace on `'paste'`).
|
|
1450
|
+
* Defaults to `'unknown'`. Apps that need a different vocabulary can
|
|
1451
|
+
* define their own {@link ImportStateConfig} with whatever value type
|
|
1452
|
+
* they want.
|
|
1453
|
+
*
|
|
1454
|
+
* @experimental
|
|
1455
|
+
*/
|
|
1456
|
+
|
|
1457
|
+
/**
|
|
1458
|
+
* Built-in import-context state identifying how this import was initiated.
|
|
1459
|
+
* Callers of `$generateNodesFromDOM` should set it via the `context` option.
|
|
1460
|
+
*
|
|
1461
|
+
* @experimental
|
|
1462
|
+
*/
|
|
1463
|
+
const ImportSource = createImportState('importSource', () => 'unknown');
|
|
1464
|
+
|
|
1465
|
+
/**
|
|
1466
|
+
* Built-in import-context state holding the {@link DataTransfer} the
|
|
1467
|
+
* import was sourced from, if any. `null` outside paste/drop flows.
|
|
1468
|
+
*
|
|
1469
|
+
* The clipboard import pipeline passes the original `DataTransfer`
|
|
1470
|
+
* through to its per-MIME-type handler stack (see
|
|
1471
|
+
* {@link ImportMimeTypeFunction}); handlers that route HTML through
|
|
1472
|
+
* the {@link DOMImportExtension} pipeline should forward it into the
|
|
1473
|
+
* walk via `context: [contextValue(ImportSourceDataTransfer,
|
|
1474
|
+
* dataTransfer)]` so rules and preprocessors can call
|
|
1475
|
+
* `ctx.get(ImportSourceDataTransfer)` to inspect companion MIME types
|
|
1476
|
+
* (e.g. an `'application/rtf'` alternative or an attached
|
|
1477
|
+
* `'application/x-officedrawing'` payload), the file list, or any
|
|
1478
|
+
* custom drag-and-drop slot.
|
|
1479
|
+
*
|
|
1480
|
+
* Use sparingly: the safer pattern is to decide *which* MIME-type
|
|
1481
|
+
* payload to walk in the clipboard handler stack and hand a finalized
|
|
1482
|
+
* DOM to the rules; only fall back to peeking at `ImportSourceDataTransfer`
|
|
1483
|
+
* when the source-detection signal genuinely lives in a companion
|
|
1484
|
+
* slot.
|
|
1485
|
+
*
|
|
1486
|
+
* @experimental
|
|
1487
|
+
*/
|
|
1488
|
+
const ImportSourceDataTransfer = createImportState('importSourceDataTransfer', () => null);
|
|
1489
|
+
|
|
1490
|
+
/**
|
|
1491
|
+
* Built-in import-context state holding the bit-packed
|
|
1492
|
+
* {@link TextFormatType} formats that should apply to {@link TextNode}s
|
|
1493
|
+
* produced during the current subtree. Used by inline-format wrappers
|
|
1494
|
+
* (`<b>`, `<i>`, `<u>`, …) to propagate formatting through the context
|
|
1495
|
+
* record instead of via the legacy `forChild` chain.
|
|
1496
|
+
*
|
|
1497
|
+
* @experimental
|
|
1498
|
+
*/
|
|
1499
|
+
const ImportTextFormat = createImportState('textFormat', () => 0);
|
|
1500
|
+
|
|
1501
|
+
/**
|
|
1502
|
+
* Built-in import-context state holding a parsed CSS-style record
|
|
1503
|
+
* (the {@link getStyleObjectFromCSS} shape) that should apply to
|
|
1504
|
+
* {@link TextNode}s produced during the current subtree. Mirrors the
|
|
1505
|
+
* format-bit propagation in {@link ImportTextFormat} for properties
|
|
1506
|
+
* that don't fit into the format bit mask — `color`, `font-family`,
|
|
1507
|
+
* `font-size`, etc.
|
|
1508
|
+
*
|
|
1509
|
+
* Ancestor rules that contribute a style branch the context with a
|
|
1510
|
+
* merged record; the core `#text` rule materializes the non-empty
|
|
1511
|
+
* record to a CSS string and calls `setStyle` on the new TextNode.
|
|
1512
|
+
* Once TextNode adopts a parsed style record, the materialization
|
|
1513
|
+
* step will go away.
|
|
1514
|
+
*
|
|
1515
|
+
* @experimental
|
|
1516
|
+
*/
|
|
1517
|
+
const ImportTextStyle = createImportState('textStyle', () => ({}));
|
|
1518
|
+
|
|
1519
|
+
/**
|
|
1520
|
+
* Determines whether a given DOM element should be treated as preserving
|
|
1521
|
+
* whitespace (i.e. text content under it is not collapsed and is split on
|
|
1522
|
+
* `\n` / `\t` into `LineBreakNode` / `TabNode`). The default matches the
|
|
1523
|
+
* legacy behavior: the element itself is `<pre>` or its inline
|
|
1524
|
+
* `white-space` style begins with `'pre'`.
|
|
1525
|
+
*
|
|
1526
|
+
* @experimental
|
|
1527
|
+
*/
|
|
1528
|
+
|
|
1529
|
+
/**
|
|
1530
|
+
* Determines whether a given DOM node sits on the same visual line as its
|
|
1531
|
+
* adjacent text siblings, governing whether leading/trailing whitespace in
|
|
1532
|
+
* a `#text` is collapsed against neighbors. The default consults
|
|
1533
|
+
* {@link isInlineDomNode} from `lexical` (style.display or a fixed inline
|
|
1534
|
+
* tag-name set) and additionally treats elements with an explicit
|
|
1535
|
+
* non-inline `display` style as block.
|
|
1536
|
+
*
|
|
1537
|
+
* @experimental
|
|
1538
|
+
*/
|
|
1539
|
+
|
|
1540
|
+
/**
|
|
1541
|
+
* Configuration for the core text whitespace-collapse logic. Override via
|
|
1542
|
+
* {@link ImportWhitespaceConfig} either as a `contextDefaults` entry on
|
|
1543
|
+
* the {@link DOMImportExtension} or per-call on `$generateNodesFromDOM`'s
|
|
1544
|
+
* `context` option.
|
|
1545
|
+
*
|
|
1546
|
+
* @experimental
|
|
1547
|
+
*/
|
|
1548
|
+
|
|
1549
|
+
/**
|
|
1550
|
+
* Default {@link WhitespaceImportConfig.preservesWhitespace}: matches
|
|
1551
|
+
* `<pre>` and any element with `white-space: pre*`.
|
|
1552
|
+
*
|
|
1553
|
+
* @experimental
|
|
1554
|
+
*/
|
|
1555
|
+
function defaultPreservesWhitespace(node) {
|
|
1556
|
+
if (!lexical.isHTMLElement(node)) {
|
|
1557
|
+
return false;
|
|
1558
|
+
}
|
|
1559
|
+
if (node.nodeName === 'PRE') {
|
|
1560
|
+
return true;
|
|
1561
|
+
}
|
|
1562
|
+
const ws = node.style.whiteSpace;
|
|
1563
|
+
return typeof ws === 'string' && ws.startsWith('pre');
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
/**
|
|
1567
|
+
* Default {@link WhitespaceImportConfig.isInline}: treats an element as
|
|
1568
|
+
* inline iff its inline `display` style is `inline*` OR (no explicit
|
|
1569
|
+
* non-inline display) its nodeName is a known inline tag (`isInlineDomNode`).
|
|
1570
|
+
* Text nodes are always inline; comments and other non-elements are not.
|
|
1571
|
+
*
|
|
1572
|
+
* @experimental
|
|
1573
|
+
*/
|
|
1574
|
+
function defaultIsInline(node) {
|
|
1575
|
+
if (lexical.isDOMTextNode(node)) {
|
|
1576
|
+
return true;
|
|
1577
|
+
}
|
|
1578
|
+
if (!lexical.isHTMLElement(node)) {
|
|
1579
|
+
return false;
|
|
1580
|
+
}
|
|
1581
|
+
const display = node.style.display;
|
|
1582
|
+
if (display) {
|
|
1583
|
+
return display.startsWith('inline');
|
|
1584
|
+
}
|
|
1585
|
+
if (lexical.isBlockDomNode(node)) {
|
|
1586
|
+
return false;
|
|
1587
|
+
}
|
|
1588
|
+
return lexical.isInlineDomNode(node);
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
/**
|
|
1592
|
+
* Built-in import-context state controlling text-node whitespace handling
|
|
1593
|
+
* (collapse vs. preserve, what counts as an inline sibling). Override per
|
|
1594
|
+
* editor via {@link DOMImportConfig.contextDefaults} or per call via
|
|
1595
|
+
* {@link GenerateNodesFromDOMOptions.context}.
|
|
1596
|
+
*
|
|
1597
|
+
* @experimental
|
|
1598
|
+
*/
|
|
1599
|
+
const ImportWhitespaceConfig = createImportState('whitespaceConfig', () => ({
|
|
1600
|
+
isInline: defaultIsInline,
|
|
1601
|
+
preservesWhitespace: defaultPreservesWhitespace
|
|
1602
|
+
}));
|
|
1603
|
+
|
|
1604
|
+
/**
|
|
1605
|
+
* Built-in session slot for runtime overlay rules that should be in
|
|
1606
|
+
* effect for the entire walk. A preprocessor writes here when it wants
|
|
1607
|
+
* to conditionally install handling for a particular paste source
|
|
1608
|
+
* (e.g. "if the Microsoft Word generator meta tag is present, push the
|
|
1609
|
+
* Word-paste overlay"). Each entry contributes an overlay dispatcher
|
|
1610
|
+
* to the runtime's overlay stack; later array entries are higher
|
|
1611
|
+
* priority. Use `ctx.session.update(ImportOverlays, prev => […])` to
|
|
1612
|
+
* append.
|
|
1613
|
+
*
|
|
1614
|
+
* This is the walk-wide counterpart to
|
|
1615
|
+
* `$importChildren({rules: …})` (which scopes an overlay to one
|
|
1616
|
+
* subtree): write to {@link ImportOverlays} when the overlay should
|
|
1617
|
+
* apply for the whole document; use `$importChildren`'s `rules` when
|
|
1618
|
+
* the overlay should only apply for a deeper region.
|
|
1619
|
+
*
|
|
1620
|
+
* @experimental
|
|
1621
|
+
*/
|
|
1622
|
+
const ImportOverlays = createImportState('importOverlays', () => []);
|
|
1623
|
+
|
|
1624
|
+
/**
|
|
1625
|
+
* The session IS the root-layer {@link ContextRecord} of the walk. Reads
|
|
1626
|
+
* fall through the prototype chain to the editor's `contextDefaults`,
|
|
1627
|
+
* writes mutate the record's own properties, and any branch pushed by
|
|
1628
|
+
* `$importChildren({context})` sits above this layer and can shadow
|
|
1629
|
+
* (but does not overwrite) slots.
|
|
1630
|
+
*
|
|
1631
|
+
* @internal
|
|
1632
|
+
*/
|
|
1633
|
+
class ImportSessionImpl {
|
|
1634
|
+
constructor(record) {
|
|
1635
|
+
this.record = record;
|
|
1636
|
+
}
|
|
1637
|
+
get(cfg) {
|
|
1638
|
+
return getContextValue(this.record, cfg);
|
|
1639
|
+
}
|
|
1640
|
+
set(cfg, value) {
|
|
1641
|
+
this.record[cfg.key] = value;
|
|
1642
|
+
}
|
|
1643
|
+
update(cfg, updater) {
|
|
1644
|
+
this.record[cfg.key] = updater(getContextValue(this.record, cfg));
|
|
1645
|
+
}
|
|
1646
|
+
has(cfg) {
|
|
1647
|
+
return Object.prototype.hasOwnProperty.call(this.record, cfg.key);
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
function getDefaultImportContext(editor) {
|
|
1651
|
+
const dep = extension.getPeerDependencyFromEditor(editor, DOMImportExtensionName);
|
|
1652
|
+
return dep ? dep.output.defaults : undefined;
|
|
1653
|
+
}
|
|
1654
|
+
function getImportContext(editor) {
|
|
1655
|
+
return getContextRecord(DOMImportContextSymbol, editor) || getDefaultImportContext(editor);
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
/**
|
|
1659
|
+
* Read an import context value during an import operation.
|
|
1660
|
+
* @experimental
|
|
1661
|
+
*/
|
|
1662
|
+
function $getImportContextValue(cfg, editor = lexical.$getEditor()) {
|
|
1663
|
+
return getContextValue(getImportContext(editor), cfg);
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
/**
|
|
1667
|
+
* Run `f` with the given context pairs applied on top of the editor's
|
|
1668
|
+
* current import context.
|
|
1669
|
+
*
|
|
1670
|
+
* @experimental
|
|
1671
|
+
*/
|
|
1672
|
+
const $withImportContext = $withContext(DOMImportContextSymbol, getDefaultImportContext);
|
|
1673
|
+
|
|
1674
|
+
/**
|
|
1675
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
1676
|
+
*
|
|
1677
|
+
* This source code is licensed under the MIT license found in the
|
|
1678
|
+
* LICENSE file in the root directory of this source tree.
|
|
1679
|
+
*
|
|
1680
|
+
*/
|
|
1681
|
+
const sel$1 = selBase;
|
|
1682
|
+
const ALIGNMENT_VALUES = new Set(['center', 'end', 'justify', 'left', 'right', 'start']);
|
|
1683
|
+
|
|
1684
|
+
/**
|
|
1685
|
+
* True if `value` is a non-empty {@link ElementFormatType} (matches one of
|
|
1686
|
+
* the supported `text-align` / legacy `align`-attribute values).
|
|
1687
|
+
*
|
|
1688
|
+
* @internal
|
|
1689
|
+
*/
|
|
1690
|
+
function isAlignmentValue(value) {
|
|
1691
|
+
return ALIGNMENT_VALUES.has(value);
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
/**
|
|
1695
|
+
* A pair of bitmasks describing which {@link TextFormatType} bits to set
|
|
1696
|
+
* and which to clear when descending into an element. The clear pass
|
|
1697
|
+
* matters for cases the legacy OR-merge mishandled, e.g. `<b
|
|
1698
|
+
* style="font-weight: normal">` clearing an inherited bold, or `<sub>` /
|
|
1699
|
+
* `<sup>` clearing each other.
|
|
1700
|
+
*/
|
|
1701
|
+
|
|
1702
|
+
/**
|
|
1703
|
+
* The small subset of inline-style properties that affect text formatting
|
|
1704
|
+
* during import. Modeled as a plain object so tag-implicit defaults and
|
|
1705
|
+
* the element's own inline `style` can be merged with `{...defaults,
|
|
1706
|
+
* ...override-if-set}` semantics rather than relying on CSSStyleDeclaration.
|
|
1707
|
+
*/
|
|
1708
|
+
|
|
1709
|
+
/**
|
|
1710
|
+
* Default style implied by each inline format tag. `<b>`/`<strong>` set
|
|
1711
|
+
* font-weight, `<sub>` sets vertical-align, etc. Any of these can be
|
|
1712
|
+
* overridden by the element's own inline `style` (so `<b
|
|
1713
|
+
* style="font-weight: normal">` ends up with `fontWeight: 'normal'` in
|
|
1714
|
+
* the effective style).
|
|
1715
|
+
*/
|
|
1716
|
+
const TAG_DEFAULT_STYLE = {
|
|
1717
|
+
B: {
|
|
1718
|
+
fontWeight: 'bold'
|
|
1719
|
+
},
|
|
1720
|
+
EM: {
|
|
1721
|
+
fontStyle: 'italic'
|
|
1722
|
+
},
|
|
1723
|
+
I: {
|
|
1724
|
+
fontStyle: 'italic'
|
|
1725
|
+
},
|
|
1726
|
+
S: {
|
|
1727
|
+
textDecoration: 'line-through'
|
|
1728
|
+
},
|
|
1729
|
+
STRONG: {
|
|
1730
|
+
fontWeight: 'bold'
|
|
1731
|
+
},
|
|
1732
|
+
SUB: {
|
|
1733
|
+
verticalAlign: 'sub'
|
|
1734
|
+
},
|
|
1735
|
+
SUP: {
|
|
1736
|
+
verticalAlign: 'super'
|
|
1737
|
+
},
|
|
1738
|
+
U: {
|
|
1739
|
+
textDecoration: 'underline'
|
|
1740
|
+
}
|
|
1741
|
+
};
|
|
1742
|
+
|
|
1743
|
+
/**
|
|
1744
|
+
* Tags whose effect on TextFormat has no CSS analog (so the style-merge
|
|
1745
|
+
* path can't reach them). Applied as a pure "set" override.
|
|
1746
|
+
*/
|
|
1747
|
+
const TAG_ONLY_SET = {
|
|
1748
|
+
CODE: lexical.IS_CODE,
|
|
1749
|
+
MARK: lexical.IS_HIGHLIGHT
|
|
1750
|
+
};
|
|
1751
|
+
function readElementFormatStyle(el) {
|
|
1752
|
+
return {
|
|
1753
|
+
fontStyle: el.style.fontStyle,
|
|
1754
|
+
fontWeight: el.style.fontWeight,
|
|
1755
|
+
textDecoration: el.style.textDecoration,
|
|
1756
|
+
verticalAlign: el.style.verticalAlign
|
|
1757
|
+
};
|
|
1758
|
+
}
|
|
1759
|
+
function mergeStyles(defaults, override) {
|
|
1760
|
+
return {
|
|
1761
|
+
fontStyle: override.fontStyle || defaults.fontStyle,
|
|
1762
|
+
fontWeight: override.fontWeight || defaults.fontWeight,
|
|
1763
|
+
textDecoration: override.textDecoration || defaults.textDecoration,
|
|
1764
|
+
verticalAlign: override.verticalAlign || defaults.verticalAlign
|
|
1765
|
+
};
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
/**
|
|
1769
|
+
* The CSS property names {@link styleFormatOverride} reads — these are
|
|
1770
|
+
* "owned" by {@link ImportTextFormat} (the bit mask). When the
|
|
1771
|
+
* {@link ImportTextStyle} record is materialized onto a TextNode's
|
|
1772
|
+
* inline style by {@link styleObjectToCSS}, these are skipped so the
|
|
1773
|
+
* bit-mask side is the single source of truth and the same property
|
|
1774
|
+
* doesn't end up in both places (where the inline-style version would
|
|
1775
|
+
* shadow the format's themed CSS).
|
|
1776
|
+
*/
|
|
1777
|
+
const FORMAT_BIT_STYLE_PROPS = new Set(['font-weight', 'font-style', 'text-decoration', 'vertical-align']);
|
|
1778
|
+
|
|
1779
|
+
/**
|
|
1780
|
+
* Translate a {@link FormatStyle} into a {@link FormatOverride}. Explicit
|
|
1781
|
+
* "non-decorating" values (`font-weight: normal`, `text-decoration: none`,
|
|
1782
|
+
* `vertical-align: baseline`) produce `clear` bits, so an inner element
|
|
1783
|
+
* can remove a format inherited from its ancestors.
|
|
1784
|
+
*/
|
|
1785
|
+
function styleFormatOverride(style) {
|
|
1786
|
+
let set = 0;
|
|
1787
|
+
let clear = 0;
|
|
1788
|
+
const {
|
|
1789
|
+
fontWeight,
|
|
1790
|
+
fontStyle,
|
|
1791
|
+
textDecoration,
|
|
1792
|
+
verticalAlign
|
|
1793
|
+
} = style;
|
|
1794
|
+
if (fontWeight === '700' || fontWeight === 'bold') {
|
|
1795
|
+
set |= lexical.IS_BOLD;
|
|
1796
|
+
} else if (fontWeight === 'normal' || fontWeight === '400') {
|
|
1797
|
+
clear |= lexical.IS_BOLD;
|
|
1798
|
+
}
|
|
1799
|
+
if (fontStyle === 'italic') {
|
|
1800
|
+
set |= lexical.IS_ITALIC;
|
|
1801
|
+
} else if (fontStyle === 'normal') {
|
|
1802
|
+
clear |= lexical.IS_ITALIC;
|
|
1803
|
+
}
|
|
1804
|
+
if (textDecoration) {
|
|
1805
|
+
const parts = textDecoration.split(' ');
|
|
1806
|
+
if (parts.includes('underline')) {
|
|
1807
|
+
set |= lexical.IS_UNDERLINE;
|
|
1808
|
+
}
|
|
1809
|
+
if (parts.includes('line-through')) {
|
|
1810
|
+
set |= lexical.IS_STRIKETHROUGH;
|
|
1811
|
+
}
|
|
1812
|
+
if (parts.includes('none')) {
|
|
1813
|
+
clear |= lexical.IS_UNDERLINE | lexical.IS_STRIKETHROUGH;
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
if (verticalAlign === 'sub') {
|
|
1817
|
+
set |= lexical.IS_SUBSCRIPT;
|
|
1818
|
+
clear |= lexical.IS_SUPERSCRIPT;
|
|
1819
|
+
} else if (verticalAlign === 'super') {
|
|
1820
|
+
set |= lexical.IS_SUPERSCRIPT;
|
|
1821
|
+
clear |= lexical.IS_SUBSCRIPT;
|
|
1822
|
+
} else if (verticalAlign === 'baseline') {
|
|
1823
|
+
clear |= lexical.IS_SUBSCRIPT | lexical.IS_SUPERSCRIPT;
|
|
1824
|
+
}
|
|
1825
|
+
return {
|
|
1826
|
+
clear,
|
|
1827
|
+
set
|
|
1828
|
+
};
|
|
1829
|
+
}
|
|
1830
|
+
function applyFormatOverride(format, ov) {
|
|
1831
|
+
return format & ~ov.clear | ov.set;
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
/**
|
|
1835
|
+
* Unified rule for inline-format-bearing tags and `<span>`. The element's
|
|
1836
|
+
* effective style is its tag's {@link TAG_DEFAULT_STYLE} merged with its
|
|
1837
|
+
* inline `style` (element's own style wins for any property it sets), and
|
|
1838
|
+
* the resulting style is translated into a {@link FormatOverride}. Tags
|
|
1839
|
+
* with no CSS analog (`<code>`, `<mark>`) contribute their bit as a pure
|
|
1840
|
+
* `set` override.
|
|
1841
|
+
*
|
|
1842
|
+
* This shape lets:
|
|
1843
|
+
* - `<b style="font-weight: normal">` clear an inherited IS_BOLD.
|
|
1844
|
+
* - `<sub><sup>x</sup></sub>` resolve to IS_SUPERSCRIPT only (sub/sup
|
|
1845
|
+
* mutex via the vertical-align clear logic).
|
|
1846
|
+
* - `<span style="text-decoration: none">` strip inherited underline /
|
|
1847
|
+
* line-through.
|
|
1848
|
+
*/
|
|
1849
|
+
const InlineFormatRule = defineImportRule({
|
|
1850
|
+
$import: (ctx, el) => {
|
|
1851
|
+
const inherited = ctx.get(ImportTextFormat);
|
|
1852
|
+
const tagDefault = TAG_DEFAULT_STYLE[el.nodeName];
|
|
1853
|
+
const elStyle = readElementFormatStyle(el);
|
|
1854
|
+
const effective = tagDefault ? mergeStyles(tagDefault, elStyle) : elStyle;
|
|
1855
|
+
let merged = applyFormatOverride(inherited, styleFormatOverride(effective));
|
|
1856
|
+
const tagOnly = TAG_ONLY_SET[el.nodeName];
|
|
1857
|
+
if (tagOnly) {
|
|
1858
|
+
merged |= tagOnly;
|
|
1859
|
+
}
|
|
1860
|
+
if (merged === inherited) {
|
|
1861
|
+
return ctx.$importChildren(el);
|
|
1862
|
+
}
|
|
1863
|
+
return ctx.$importChildren(el, {
|
|
1864
|
+
context: [contextValue(ImportTextFormat, merged)]
|
|
1865
|
+
});
|
|
1866
|
+
},
|
|
1867
|
+
match: sel$1.tag('b', 'strong', 'em', 'i', 'code', 'mark', 's', 'sub', 'sup', 'u', 'span'),
|
|
1868
|
+
name: '@lexical/html/inline-format'
|
|
1869
|
+
});
|
|
1870
|
+
|
|
1871
|
+
/**
|
|
1872
|
+
* Walk up the DOM ancestor chain to determine whether `node` is inside an
|
|
1873
|
+
* element whose whitespace should be preserved, per the supplied
|
|
1874
|
+
* {@link WhitespaceImportConfig.preservesWhitespace} predicate. Pure
|
|
1875
|
+
* ancestor walk, no caching.
|
|
1876
|
+
*/
|
|
1877
|
+
function isInsidePreserveWhitespace(node, wsConfig) {
|
|
1878
|
+
let current = node.parentNode;
|
|
1879
|
+
while (current !== null) {
|
|
1880
|
+
if (wsConfig.preservesWhitespace(current)) {
|
|
1881
|
+
return true;
|
|
1882
|
+
}
|
|
1883
|
+
current = current.parentNode;
|
|
1884
|
+
}
|
|
1885
|
+
return false;
|
|
1886
|
+
}
|
|
1887
|
+
function findAdjacentTextOnLine(text, forward, wsConfig) {
|
|
1888
|
+
let node = text;
|
|
1889
|
+
while (true) {
|
|
1890
|
+
let sibling = null;
|
|
1891
|
+
while ((sibling = forward ? node.nextSibling : node.previousSibling) === null) {
|
|
1892
|
+
const parent = node.parentNode;
|
|
1893
|
+
if (parent === null) {
|
|
1894
|
+
return null;
|
|
1895
|
+
}
|
|
1896
|
+
node = parent;
|
|
1897
|
+
}
|
|
1898
|
+
node = sibling;
|
|
1899
|
+
if (!wsConfig.isInline(node)) {
|
|
1900
|
+
return null;
|
|
1901
|
+
}
|
|
1902
|
+
let descendant = node;
|
|
1903
|
+
while ((descendant = forward ? node.firstChild : node.lastChild) !== null) {
|
|
1904
|
+
node = descendant;
|
|
1905
|
+
}
|
|
1906
|
+
if (lexical.isDOMTextNode(node)) {
|
|
1907
|
+
return node;
|
|
1908
|
+
}
|
|
1909
|
+
if (node.nodeName === 'BR') {
|
|
1910
|
+
return null;
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
function collapseWhitespace(textNode, wsConfig) {
|
|
1915
|
+
let textContent = (textNode.textContent || '').replace(/\r/g, '').replace(/[ \t\n]+/g, ' ');
|
|
1916
|
+
if (textContent.length === 0) {
|
|
1917
|
+
return '';
|
|
1918
|
+
}
|
|
1919
|
+
if (textContent[0] === ' ') {
|
|
1920
|
+
let neighbor = textNode;
|
|
1921
|
+
let isStartOfLine = true;
|
|
1922
|
+
while (neighbor !== null && (neighbor = findAdjacentTextOnLine(neighbor, false, wsConfig)) !== null) {
|
|
1923
|
+
const neighborContent = neighbor.textContent || '';
|
|
1924
|
+
if (neighborContent.length > 0) {
|
|
1925
|
+
if (/[ \t\n]$/.test(neighborContent)) {
|
|
1926
|
+
textContent = textContent.slice(1);
|
|
1927
|
+
}
|
|
1928
|
+
isStartOfLine = false;
|
|
1929
|
+
break;
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
if (isStartOfLine) {
|
|
1933
|
+
textContent = textContent.slice(1);
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
if (textContent.length > 0 && textContent[textContent.length - 1] === ' ') {
|
|
1937
|
+
let neighbor = textNode;
|
|
1938
|
+
let isEndOfLine = true;
|
|
1939
|
+
while (neighbor !== null && (neighbor = findAdjacentTextOnLine(neighbor, true, wsConfig)) !== null) {
|
|
1940
|
+
const neighborContent = (neighbor.textContent || '').replace(/^( |\t|\r?\n)+/, '');
|
|
1941
|
+
if (neighborContent.length > 0) {
|
|
1942
|
+
isEndOfLine = false;
|
|
1943
|
+
break;
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
if (isEndOfLine) {
|
|
1947
|
+
textContent = textContent.slice(0, -1);
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
return textContent;
|
|
1951
|
+
}
|
|
1952
|
+
function $applyFormat(node, format) {
|
|
1953
|
+
return format !== 0 && lexical.$isTextNode(node) ? node.setFormat(format) : node;
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
/**
|
|
1957
|
+
* Inverse of {@link getStyleObjectFromCSS}: serialize a parsed style
|
|
1958
|
+
* record back into a CSS declaration string suitable for
|
|
1959
|
+
* `TextNode.setStyle`. Returns the empty string for an empty record.
|
|
1960
|
+
*/
|
|
1961
|
+
function styleObjectToCSS(style) {
|
|
1962
|
+
let css = '';
|
|
1963
|
+
for (const prop in style) {
|
|
1964
|
+
if (FORMAT_BIT_STYLE_PROPS.has(prop)) {
|
|
1965
|
+
// Owned by ImportTextFormat (bit mask) — skip so the format-bit
|
|
1966
|
+
// CSS is the single source of truth on the rendered TextNode.
|
|
1967
|
+
continue;
|
|
1968
|
+
}
|
|
1969
|
+
css += `${prop}: ${style[prop]}; `;
|
|
1970
|
+
}
|
|
1971
|
+
return css.trimEnd();
|
|
1972
|
+
}
|
|
1973
|
+
function $applyTextStyle(node, style) {
|
|
1974
|
+
if (lexical.$isTextNode(node)) {
|
|
1975
|
+
const css = styleObjectToCSS(style);
|
|
1976
|
+
if (css !== '') {
|
|
1977
|
+
node.setStyle(css);
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
return node;
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
/**
|
|
1984
|
+
* `#text` rule. Inside a `<pre>` ancestor, preserve whitespace and split
|
|
1985
|
+
* on `\n` and `\t` into `LineBreakNode`/`TabNode` siblings. Otherwise
|
|
1986
|
+
* collapse whitespace using the same neighbor-aware rules as the legacy
|
|
1987
|
+
* `$convertTextDOMNode`.
|
|
1988
|
+
*/
|
|
1989
|
+
const TextRule = defineImportRule({
|
|
1990
|
+
$import: (ctx, el) => {
|
|
1991
|
+
const format = ctx.get(ImportTextFormat);
|
|
1992
|
+
const style = ctx.get(ImportTextStyle);
|
|
1993
|
+
const wsConfig = ctx.get(ImportWhitespaceConfig);
|
|
1994
|
+
if (isInsidePreserveWhitespace(el, wsConfig)) {
|
|
1995
|
+
const out = lexical.$generateNodesFromRawText(el.textContent || '');
|
|
1996
|
+
for (const node of out) {
|
|
1997
|
+
$applyFormat(node, format);
|
|
1998
|
+
$applyTextStyle(node, style);
|
|
1999
|
+
}
|
|
2000
|
+
return out;
|
|
2001
|
+
}
|
|
2002
|
+
const collapsed = collapseWhitespace(el, wsConfig);
|
|
2003
|
+
if (collapsed === '') {
|
|
2004
|
+
return [];
|
|
2005
|
+
}
|
|
2006
|
+
const text = lexical.$createTextNode(collapsed);
|
|
2007
|
+
$applyFormat(text, format);
|
|
2008
|
+
$applyTextStyle(text, style);
|
|
2009
|
+
return [text];
|
|
2010
|
+
},
|
|
2011
|
+
match: sel$1.text(),
|
|
2012
|
+
name: '@lexical/html/#text'
|
|
2013
|
+
});
|
|
2014
|
+
|
|
2015
|
+
/**
|
|
2016
|
+
* Drop `<style>` and `<script>` and skip descending into them — matches
|
|
2017
|
+
* the legacy `IGNORE_TAGS` set, but as a regular rule so apps can register
|
|
2018
|
+
* a higher-priority `<style>` rule to capture stylesheet text into the
|
|
2019
|
+
* import session for later use.
|
|
2020
|
+
*/
|
|
2021
|
+
const IgnoreScriptStyleRule = defineImportRule({
|
|
2022
|
+
$import: () => [],
|
|
2023
|
+
match: sel$1.tag('script', 'style'),
|
|
2024
|
+
name: '@lexical/html/script-style-ignore'
|
|
2025
|
+
});
|
|
2026
|
+
const LineBreakRule = defineImportRule({
|
|
2027
|
+
$import: () => [lexical.$createLineBreakNode()],
|
|
2028
|
+
match: sel$1.tag('br'),
|
|
2029
|
+
name: '@lexical/html/br'
|
|
2030
|
+
});
|
|
2031
|
+
|
|
2032
|
+
/**
|
|
2033
|
+
* `<p>` rule. Re-applies format, indent, direction, and the legacy
|
|
2034
|
+
* `align` attribute fallback.
|
|
2035
|
+
*/
|
|
2036
|
+
const ParagraphRule = defineImportRule({
|
|
2037
|
+
$import: (ctx, el) => {
|
|
2038
|
+
const p = lexical.$createParagraphNode();
|
|
2039
|
+
lexical.$setFormatFromDOM(p, el);
|
|
2040
|
+
lexical.setNodeIndentFromDOM(el, p);
|
|
2041
|
+
if (p.getFormatType() === '') {
|
|
2042
|
+
const align = el.getAttribute('align');
|
|
2043
|
+
if (align && isAlignmentValue(align)) {
|
|
2044
|
+
p.setFormat(align);
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
lexical.$setDirectionFromDOM(p, el);
|
|
2048
|
+
// We deliberately pass no schema: paragraphs accept any inline run as-is.
|
|
2049
|
+
// The enclosing context (root / block) is responsible for ensuring the
|
|
2050
|
+
// paragraph itself is a valid block child.
|
|
2051
|
+
return [p.splice(0, 0, ctx.$importChildren(el))];
|
|
2052
|
+
},
|
|
2053
|
+
match: sel$1.tag('p'),
|
|
2054
|
+
name: '@lexical/html/p'
|
|
2055
|
+
});
|
|
2056
|
+
|
|
2057
|
+
/**
|
|
2058
|
+
* Rules covering the {@link ParagraphNode}, {@link TextNode},
|
|
2059
|
+
* {@link LineBreakNode}, and {@link TabNode} cases that the legacy
|
|
2060
|
+
* `importDOM` machinery in `@lexical/lexical` handled. Intended to be
|
|
2061
|
+
* registered as a dependency of every editor that uses
|
|
2062
|
+
* {@link DOMImportExtension}.
|
|
2063
|
+
*
|
|
2064
|
+
* @experimental
|
|
2065
|
+
*/
|
|
2066
|
+
const CoreImportRules = [IgnoreScriptStyleRule, ParagraphRule, TextRule, LineBreakRule, InlineFormatRule];
|
|
2067
|
+
|
|
2068
|
+
/**
|
|
2069
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2070
|
+
*
|
|
2071
|
+
* This source code is licensed under the MIT license found in the
|
|
2072
|
+
* LICENSE file in the root directory of this source tree.
|
|
2073
|
+
*
|
|
2074
|
+
*/
|
|
2075
|
+
|
|
2076
|
+
|
|
2077
|
+
/** @internal */
|
|
2078
|
+
|
|
2079
|
+
/** @internal */
|
|
2080
|
+
|
|
2081
|
+
function mergeSortedAsc(a, b) {
|
|
2082
|
+
const out = [];
|
|
2083
|
+
let i = 0;
|
|
2084
|
+
let j = 0;
|
|
2085
|
+
while (i < a.length && j < b.length) {
|
|
2086
|
+
if (a[i] <= b[j]) {
|
|
2087
|
+
out.push(a[i++]);
|
|
2088
|
+
} else {
|
|
2089
|
+
out.push(b[j++]);
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
while (i < a.length) {
|
|
2093
|
+
out.push(a[i++]);
|
|
2094
|
+
}
|
|
2095
|
+
while (j < b.length) {
|
|
2096
|
+
out.push(b[j++]);
|
|
2097
|
+
}
|
|
2098
|
+
return out;
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
/**
|
|
2102
|
+
* Compile an ordered list of {@link DOMImportRule}s into the dispatch tables
|
|
2103
|
+
* used by the import runtime. The rule at index 0 is the highest-priority
|
|
2104
|
+
* (`mergeConfig` prepends partial.rules so later-merged extensions land
|
|
2105
|
+
* first).
|
|
2106
|
+
*
|
|
2107
|
+
* @internal
|
|
2108
|
+
*/
|
|
2109
|
+
function compileImportRules(rules) {
|
|
2110
|
+
const compiled = [];
|
|
2111
|
+
const byTag = new Map();
|
|
2112
|
+
const wildcardIndices = [];
|
|
2113
|
+
const textIndices = [];
|
|
2114
|
+
const commentIndices = [];
|
|
2115
|
+
const seenNames = new Set();
|
|
2116
|
+
rules.forEach((rule, i) => {
|
|
2117
|
+
const sel = getSelectorImpl(rule.match);
|
|
2118
|
+
const name = rule.name || defaultRuleName(sel, i);
|
|
2119
|
+
if (typeof rule.name === 'string' && seenNames.has(rule.name)) {
|
|
2120
|
+
console.warn(`[lexical] duplicate DOMImportRule name "${rule.name}" — keep names unique to aid debugging.`);
|
|
2121
|
+
}
|
|
2122
|
+
if (rule.name) {
|
|
2123
|
+
seenNames.add(rule.name);
|
|
2124
|
+
}
|
|
2125
|
+
compiled.push({
|
|
2126
|
+
$import: rule.$import,
|
|
2127
|
+
name,
|
|
2128
|
+
predicate: sel.predicate
|
|
2129
|
+
});
|
|
2130
|
+
if (sel.kind === 'text') {
|
|
2131
|
+
textIndices.push(i);
|
|
2132
|
+
} else if (sel.kind === 'comment') {
|
|
2133
|
+
commentIndices.push(i);
|
|
2134
|
+
} else if (sel.tags.size === 0) {
|
|
2135
|
+
wildcardIndices.push(i);
|
|
2136
|
+
} else {
|
|
2137
|
+
for (const tag of sel.tags) {
|
|
2138
|
+
let list = byTag.get(tag);
|
|
2139
|
+
if (!list) {
|
|
2140
|
+
list = [];
|
|
2141
|
+
byTag.set(tag, list);
|
|
2142
|
+
}
|
|
2143
|
+
list.push(i);
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
});
|
|
2147
|
+
|
|
2148
|
+
// Interleave wildcard-element indices into each tag's list in registration
|
|
2149
|
+
// (ascending-index) order, so iterating a tag bucket visits both tag-
|
|
2150
|
+
// specific and wildcard rules in the same priority sequence.
|
|
2151
|
+
const finalByTag = new Map();
|
|
2152
|
+
if (wildcardIndices.length === 0) {
|
|
2153
|
+
for (const [tag, list] of byTag) {
|
|
2154
|
+
finalByTag.set(tag, list);
|
|
2155
|
+
}
|
|
2156
|
+
} else {
|
|
2157
|
+
for (const [tag, list] of byTag) {
|
|
2158
|
+
finalByTag.set(tag, mergeSortedAsc(list, wildcardIndices));
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
return {
|
|
2162
|
+
byTag: finalByTag,
|
|
2163
|
+
commentIndices,
|
|
2164
|
+
rules: compiled,
|
|
2165
|
+
textIndices,
|
|
2166
|
+
wildcardIndices
|
|
2167
|
+
};
|
|
2168
|
+
}
|
|
2169
|
+
function defaultRuleName(sel, index) {
|
|
2170
|
+
if (sel.kind === 'text') {
|
|
2171
|
+
return `#text@${index}`;
|
|
2172
|
+
}
|
|
2173
|
+
if (sel.kind === 'comment') {
|
|
2174
|
+
return `#comment@${index}`;
|
|
2175
|
+
}
|
|
2176
|
+
if (sel.tags.size === 0) {
|
|
2177
|
+
return `*@${index}`;
|
|
2178
|
+
}
|
|
2179
|
+
const tagList = Array.from(sel.tags).join(',').toLowerCase();
|
|
2180
|
+
return `${tagList}@${index}`;
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2183
|
+
/**
|
|
2184
|
+
* Look up the (already interleaved) rule indices relevant to `node`. Element
|
|
2185
|
+
* nodes hit `byTag` (with wildcards merged in) or fall back to the wildcard
|
|
2186
|
+
* bucket if no tag-specific rules exist; text and comment nodes use their
|
|
2187
|
+
* own buckets.
|
|
2188
|
+
*
|
|
2189
|
+
* @internal
|
|
2190
|
+
*/
|
|
2191
|
+
function getDispatchIndices(dispatch, node) {
|
|
2192
|
+
if (lexical.isDOMTextNode(node)) {
|
|
2193
|
+
return dispatch.textIndices;
|
|
2194
|
+
}
|
|
2195
|
+
if (node.nodeType === 8 /* COMMENT_NODE */) {
|
|
2196
|
+
return dispatch.commentIndices;
|
|
2197
|
+
}
|
|
2198
|
+
if (lexical.isHTMLElement(node)) {
|
|
2199
|
+
return dispatch.byTag.get(node.nodeName) || dispatch.wildcardIndices;
|
|
2200
|
+
}
|
|
2201
|
+
return EMPTY_INDICES;
|
|
2202
|
+
}
|
|
2203
|
+
const EMPTY_INDICES = Object.freeze([]);
|
|
2204
|
+
|
|
2205
|
+
/**
|
|
2206
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2207
|
+
*
|
|
2208
|
+
* This source code is licensed under the MIT license found in the
|
|
2209
|
+
* LICENSE file in the root directory of this source tree.
|
|
2210
|
+
*
|
|
2211
|
+
*/
|
|
2212
|
+
|
|
2213
|
+
|
|
2214
|
+
/**
|
|
2215
|
+
* Opaque handle for a pre-compiled set of overlay rules. Produce one with
|
|
2216
|
+
* {@link defineOverlayRules} and pass it to
|
|
2217
|
+
* {@link DOMImportContext.$importChildren} via
|
|
2218
|
+
* {@link ImportChildrenOpts.rules}.
|
|
2219
|
+
*
|
|
2220
|
+
* To merge two or more overlays into a single one, pass them (alongside
|
|
2221
|
+
* raw {@link DOMImportRule}s if desired) to a fresh
|
|
2222
|
+
* {@link defineOverlayRules} — earlier arguments are higher priority.
|
|
2223
|
+
*
|
|
2224
|
+
* The internal shape is intentionally not part of the public API: it's a
|
|
2225
|
+
* compiled dispatch table tagged with `__type` so callers cannot pass a
|
|
2226
|
+
* raw rule array where a compiled overlay is expected.
|
|
2227
|
+
*
|
|
2228
|
+
* @experimental
|
|
2229
|
+
*/
|
|
2230
|
+
|
|
2231
|
+
/**
|
|
2232
|
+
* An entry accepted everywhere rules are configured (overlay
|
|
2233
|
+
* definitions, {@link DOMImportConfig.rules}). Either a single
|
|
2234
|
+
* {@link DOMImportRule} or a {@link CompiledOverlayRules} produced by
|
|
2235
|
+
* a previous {@link defineOverlayRules} call — passing the latter
|
|
2236
|
+
* inlines the overlay's rules at this position in priority order.
|
|
2237
|
+
*
|
|
2238
|
+
* @experimental
|
|
2239
|
+
*/
|
|
2240
|
+
|
|
2241
|
+
/** @internal */
|
|
2242
|
+
function flattenRuleEntries(entries) {
|
|
2243
|
+
const out = [];
|
|
2244
|
+
for (const entry of entries) {
|
|
2245
|
+
if (isCompiledOverlayRules(entry)) {
|
|
2246
|
+
for (const r of entry.rules) {
|
|
2247
|
+
out.push(r);
|
|
2248
|
+
}
|
|
2249
|
+
} else {
|
|
2250
|
+
out.push(entry);
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
return out;
|
|
2254
|
+
}
|
|
2255
|
+
function isCompiledOverlayRules(entry) {
|
|
2256
|
+
return typeof entry === 'object' && entry !== null && '__type' in entry && entry.__type === 'CompiledOverlayRules';
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
/**
|
|
2260
|
+
* Pre-compile a set of {@link DOMImportRuleEntry}s into a
|
|
2261
|
+
* {@link CompiledOverlayRules} handle that can be installed via
|
|
2262
|
+
* `ctx.$importChildren(el, {rules: …})`.
|
|
2263
|
+
*
|
|
2264
|
+
* Entries can be raw {@link DOMImportRule}s or other
|
|
2265
|
+
* {@link CompiledOverlayRules} (the latter are inlined at their
|
|
2266
|
+
* position in priority order, so the same call composes any number of
|
|
2267
|
+
* overlays). Earlier entries are higher priority.
|
|
2268
|
+
*
|
|
2269
|
+
* Overlay rules installed as a raw array would be re-compiled on every
|
|
2270
|
+
* `$importChildren` call. For overlays that are reused (e.g. a GitHub
|
|
2271
|
+
* code-table rule that wraps every matching table), call this once at
|
|
2272
|
+
* module scope so the dispatch table is built up front.
|
|
2273
|
+
*
|
|
2274
|
+
* @experimental
|
|
2275
|
+
*/
|
|
2276
|
+
function defineOverlayRules(entries) {
|
|
2277
|
+
const rules = flattenRuleEntries(entries);
|
|
2278
|
+
return {
|
|
2279
|
+
__type: 'CompiledOverlayRules',
|
|
2280
|
+
dispatch: compileImportRules(rules),
|
|
2281
|
+
rules
|
|
2282
|
+
};
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
/**
|
|
2286
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2287
|
+
*
|
|
2288
|
+
* This source code is licensed under the MIT license found in the
|
|
2289
|
+
* LICENSE file in the root directory of this source tree.
|
|
2290
|
+
*
|
|
2291
|
+
*/
|
|
2292
|
+
|
|
2293
|
+
|
|
2294
|
+
/**
|
|
2295
|
+
* True if the node fills a block slot at the root or inside another
|
|
2296
|
+
* block — covers both ElementNode-style blocks (paragraph, heading,
|
|
2297
|
+
* quote) and block-level DecoratorNodes (HorizontalRuleNode,
|
|
2298
|
+
* ImageNode-as-block, etc.). Used by {@link BlockSchema},
|
|
2299
|
+
* {@link RootSchema}, and {@link NestedBlockSchema}.
|
|
2300
|
+
*
|
|
2301
|
+
* @experimental
|
|
2302
|
+
*/
|
|
2303
|
+
function $isBlockLevel(node) {
|
|
2304
|
+
return lexical.$isBlockElementNode(node) || lexical.$isDecoratorNode(node) && !node.isInline();
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
/**
|
|
2308
|
+
* Distribute an inline wrapper (`LinkNode`, `MarkNode`, …) across a
|
|
2309
|
+
* heterogeneous run of children produced by `$importChildren`, lifting
|
|
2310
|
+
* any block children to the top level while keeping the wrapper around
|
|
2311
|
+
* the leaf inline content.
|
|
2312
|
+
*
|
|
2313
|
+
* Use from a rule whose DOM source is an inline element that the
|
|
2314
|
+
* browser permitted to enclose block elements — the canonical case is
|
|
2315
|
+
* `<a href="…"><h1>title</h1><div>body</div></a>`, which a link rule
|
|
2316
|
+
* wants to surface as two block siblings (heading + paragraph), each
|
|
2317
|
+
* with its own link wrapping the original inline content. Schemas
|
|
2318
|
+
* can't express this because they reason about a parent's children
|
|
2319
|
+
* only — they cannot lift the parent out of itself.
|
|
2320
|
+
*
|
|
2321
|
+
* For each top-level child:
|
|
2322
|
+
* - **Inline children** are collected into runs; each run is wrapped
|
|
2323
|
+
* in a single fresh wrapper (from `$makeWrapper()`).
|
|
2324
|
+
* - **Block children** are descended into: their own children are
|
|
2325
|
+
* recursively distributed with `$makeWrapper`, then re-attached so
|
|
2326
|
+
* the block keeps its position at the top level.
|
|
2327
|
+
*
|
|
2328
|
+
* The returned list will contain a mix of blocks and wrapped inline
|
|
2329
|
+
* runs. The enclosing schema (typically {@link BlockSchema}) will
|
|
2330
|
+
* then package those inline wrappers into paragraphs as usual.
|
|
2331
|
+
*
|
|
2332
|
+
* @experimental
|
|
2333
|
+
*/
|
|
2334
|
+
function $distributeInlineWrapper(children, $makeWrapper) {
|
|
2335
|
+
const out = [];
|
|
2336
|
+
let inlineRun = [];
|
|
2337
|
+
const flushInline = () => {
|
|
2338
|
+
if (inlineRun.length === 0) {
|
|
2339
|
+
return;
|
|
2340
|
+
}
|
|
2341
|
+
out.push($makeWrapper().splice(0, 0, inlineRun));
|
|
2342
|
+
inlineRun = [];
|
|
2343
|
+
};
|
|
2344
|
+
for (const child of children) {
|
|
2345
|
+
if ($isBlockLevel(child)) {
|
|
2346
|
+
flushInline();
|
|
2347
|
+
// Recursively distribute the wrapper into the block's own
|
|
2348
|
+
// children. A block DecoratorNode (no children) is left alone.
|
|
2349
|
+
if (lexical.$isElementNode(child)) {
|
|
2350
|
+
const wrapped = $distributeInlineWrapper(child.getChildren(), $makeWrapper);
|
|
2351
|
+
child.splice(0, child.getChildrenSize(), wrapped);
|
|
2352
|
+
}
|
|
2353
|
+
out.push(child);
|
|
2354
|
+
} else {
|
|
2355
|
+
inlineRun.push(child);
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
flushInline();
|
|
2359
|
+
return out;
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
/**
|
|
2363
|
+
* Apply a {@link ChildSchema} to a flat list of children produced by
|
|
2364
|
+
* `$importChildren`. Walks the list once, partitions into accepted vs.
|
|
2365
|
+
* rejected runs, packages or drops rejected runs, then runs `$finalize`.
|
|
2366
|
+
*
|
|
2367
|
+
* @internal
|
|
2368
|
+
*/
|
|
2369
|
+
function $applySchema(schema, children, parent, domParent) {
|
|
2370
|
+
const out = [];
|
|
2371
|
+
let run = null;
|
|
2372
|
+
const flushRun = () => {
|
|
2373
|
+
if (run === null) {
|
|
2374
|
+
return;
|
|
2375
|
+
}
|
|
2376
|
+
const rejected = run;
|
|
2377
|
+
run = null;
|
|
2378
|
+
if (schema.$packageRun) {
|
|
2379
|
+
const packaged = schema.$packageRun(rejected, parent, domParent);
|
|
2380
|
+
if (packaged.length > 0) {
|
|
2381
|
+
for (const n of packaged) {
|
|
2382
|
+
out.push(n);
|
|
2383
|
+
}
|
|
2384
|
+
return;
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
// No $packageRun (or it returned []) — apply onReject. 'drop' (default)
|
|
2388
|
+
// discards the run. 'hoist' lets it through unchanged at this level.
|
|
2389
|
+
if (schema.onReject === 'hoist') {
|
|
2390
|
+
for (const n of rejected) {
|
|
2391
|
+
out.push(n);
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
};
|
|
2395
|
+
for (const child of children) {
|
|
2396
|
+
if (schema.$accepts(child, parent)) {
|
|
2397
|
+
flushRun();
|
|
2398
|
+
out.push(child);
|
|
2399
|
+
} else {
|
|
2400
|
+
if (run === null) {
|
|
2401
|
+
run = [];
|
|
2402
|
+
}
|
|
2403
|
+
run.push(child);
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
flushRun();
|
|
2407
|
+
return schema.$finalize ? schema.$finalize(out, parent) : out;
|
|
2408
|
+
}
|
|
2409
|
+
|
|
2410
|
+
/**
|
|
2411
|
+
* Wrap a run of inline lexical nodes in a fresh paragraph, propagating the
|
|
2412
|
+
* `text-align` of `domParent` as the paragraph's format type (matching the
|
|
2413
|
+
* legacy `wrapContinuousInlines` behavior).
|
|
2414
|
+
*/
|
|
2415
|
+
function $paragraphPackageRun(run, _parent, domParent) {
|
|
2416
|
+
const paragraph = lexical.$createParagraphNode();
|
|
2417
|
+
if (lexical.isHTMLElement(domParent)) {
|
|
2418
|
+
const textAlign = domParent.style.textAlign;
|
|
2419
|
+
if (isAlignmentValue(textAlign)) {
|
|
2420
|
+
paragraph.setFormat(textAlign);
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
return [paragraph.splice(0, 0, run)];
|
|
2424
|
+
}
|
|
2425
|
+
|
|
2426
|
+
/**
|
|
2427
|
+
* Default schema for block-level positions (root of the document, the body
|
|
2428
|
+
* of a block element node). Accepts block lexical nodes; packages runs of
|
|
2429
|
+
* inline children into fresh paragraph nodes.
|
|
2430
|
+
*
|
|
2431
|
+
* @experimental
|
|
2432
|
+
*/
|
|
2433
|
+
const BlockSchema = {
|
|
2434
|
+
$accepts: $isBlockLevel,
|
|
2435
|
+
$packageRun: $paragraphPackageRun,
|
|
2436
|
+
name: 'BlockSchema'
|
|
2437
|
+
};
|
|
2438
|
+
|
|
2439
|
+
/**
|
|
2440
|
+
* Schema for inline-only positions (the body of an inline lexical node such
|
|
2441
|
+
* as a link). Accepts non-block lexical nodes; runs of block children are
|
|
2442
|
+
* dropped (`onReject: 'drop'` is the default).
|
|
2443
|
+
*
|
|
2444
|
+
* @experimental
|
|
2445
|
+
*/
|
|
2446
|
+
const InlineSchema = {
|
|
2447
|
+
$accepts: child => !$isBlockLevel(child),
|
|
2448
|
+
name: 'InlineSchema'
|
|
2449
|
+
};
|
|
2450
|
+
|
|
2451
|
+
/**
|
|
2452
|
+
* Schema for nested block positions — the equivalent of the legacy
|
|
2453
|
+
* `ArtificialNode__DO_NOT_USE` flow used when a block DOM element appears
|
|
2454
|
+
* inside another block lexical ancestor. Accepts block nodes; runs of inline
|
|
2455
|
+
* children are emitted with a line break between consecutive runs (instead
|
|
2456
|
+
* of being wrapped in a paragraph, which would introduce an extra level of
|
|
2457
|
+
* nesting).
|
|
2458
|
+
*
|
|
2459
|
+
* @experimental
|
|
2460
|
+
*/
|
|
2461
|
+
const NestedBlockSchema = {
|
|
2462
|
+
$accepts: $isBlockLevel,
|
|
2463
|
+
/**
|
|
2464
|
+
* Pass an inline run through unchanged. Because the schema iterator only
|
|
2465
|
+
* groups *maximal* rejected runs (each separated from the next by an
|
|
2466
|
+
* accepted block child), the legacy "linebreak between adjacent inline
|
|
2467
|
+
* groups" case never arises — adjacent inline siblings are already
|
|
2468
|
+
* coalesced into one run.
|
|
2469
|
+
*/
|
|
2470
|
+
$packageRun: run => run,
|
|
2471
|
+
name: 'NestedBlockSchema'
|
|
2472
|
+
};
|
|
2473
|
+
|
|
2474
|
+
/**
|
|
2475
|
+
* Schema for the topmost level of `$generateNodesFromDOM`. Identical to
|
|
2476
|
+
* {@link BlockSchema}; aliased for clarity at the entry point and so it can
|
|
2477
|
+
* be overridden separately in the future (e.g. to synthesize a `ListNode`
|
|
2478
|
+
* around runs of orphan `ListItemNode`s).
|
|
2479
|
+
*
|
|
2480
|
+
* @experimental
|
|
2481
|
+
*/
|
|
2482
|
+
const RootSchema = {
|
|
2483
|
+
$accepts: $isBlockLevel,
|
|
2484
|
+
$packageRun: $paragraphPackageRun,
|
|
2485
|
+
name: 'RootSchema'
|
|
2486
|
+
};
|
|
2487
|
+
|
|
2488
|
+
/**
|
|
2489
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2490
|
+
*
|
|
2491
|
+
* This source code is licensed under the MIT license found in the
|
|
2492
|
+
* LICENSE file in the root directory of this source tree.
|
|
2493
|
+
*
|
|
2494
|
+
*/
|
|
2495
|
+
|
|
2496
|
+
const NO_CAPTURES = Object.freeze({});
|
|
2497
|
+
function makeContext(runtime, captures) {
|
|
2498
|
+
const ctx = {
|
|
2499
|
+
$importChildren: (parent, opts) => $importChildrenInternal(runtime, parent, opts),
|
|
2500
|
+
$importOne: (node, opts) => $importOneInternal(runtime, node, opts),
|
|
2501
|
+
captures,
|
|
2502
|
+
get(cfg) {
|
|
2503
|
+
return $getImportContextValue(cfg, runtime.editor);
|
|
2504
|
+
},
|
|
2505
|
+
session: runtime.session
|
|
2506
|
+
};
|
|
2507
|
+
return ctx;
|
|
2508
|
+
}
|
|
2509
|
+
function $importChildrenInternal(runtime, parent, opts) {
|
|
2510
|
+
const overlay = opts && opts.rules ? opts.rules.dispatch : undefined;
|
|
2511
|
+
if (overlay) {
|
|
2512
|
+
runtime.overlays.push(overlay);
|
|
2513
|
+
}
|
|
2514
|
+
try {
|
|
2515
|
+
const run = () => $importChildrenRun(runtime, parent, opts);
|
|
2516
|
+
return opts && opts.context ? $withImportContext(opts.context, runtime.editor)(run) : run();
|
|
2517
|
+
} finally {
|
|
2518
|
+
if (overlay) {
|
|
2519
|
+
runtime.overlays.pop();
|
|
2520
|
+
}
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
function $importChildrenRun(runtime, parent, opts) {
|
|
2524
|
+
const onChild = opts && opts.$onChild;
|
|
2525
|
+
const collected = [];
|
|
2526
|
+
for (const child of Array.from(parent.childNodes)) {
|
|
2527
|
+
const produced = $importOneInternal(runtime, child, undefined);
|
|
2528
|
+
for (const lex of produced) {
|
|
2529
|
+
const result = onChild ? onChild(lex) : lex;
|
|
2530
|
+
if (result != null) {
|
|
2531
|
+
collected.push(result);
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
const afterApplied = opts && opts.$after ? opts.$after(collected) : collected;
|
|
2536
|
+
const schema = opts && opts.schema;
|
|
2537
|
+
if (!schema) {
|
|
2538
|
+
return afterApplied;
|
|
2539
|
+
}
|
|
2540
|
+
return $applySchema(schema, afterApplied, null, parent);
|
|
2541
|
+
}
|
|
2542
|
+
function $importOneInternal(runtime, node, opts) {
|
|
2543
|
+
const run = () => $dispatch(runtime, node);
|
|
2544
|
+
const out = opts && opts.context ? $withImportContext(opts.context, runtime.editor)(run) : run();
|
|
2545
|
+
// Surface to callers as a mutable array per the DOMImportContext contract.
|
|
2546
|
+
return out;
|
|
2547
|
+
}
|
|
2548
|
+
|
|
2549
|
+
/**
|
|
2550
|
+
* Build the candidate (dispatch, indices) list for `node`. Overlays are
|
|
2551
|
+
* tried first in top-of-stack order; the main dispatcher comes last. The
|
|
2552
|
+
* `$next()` chain walks through all of them in sequence — an overlay rule
|
|
2553
|
+
* can defer to a lower overlay rule, or all the way through to a main
|
|
2554
|
+
* rule, just by calling `$next()`.
|
|
2555
|
+
*/
|
|
2556
|
+
function getCandidates(runtime, node) {
|
|
2557
|
+
const candidates = [];
|
|
2558
|
+
for (let i = runtime.overlays.length - 1; i >= 0; i--) {
|
|
2559
|
+
const d = runtime.overlays[i];
|
|
2560
|
+
const idx = getDispatchIndices(d, node);
|
|
2561
|
+
if (idx.length > 0) {
|
|
2562
|
+
candidates.push({
|
|
2563
|
+
dispatch: d,
|
|
2564
|
+
indices: idx
|
|
2565
|
+
});
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
const mainIdx = getDispatchIndices(runtime.dispatch, node);
|
|
2569
|
+
if (mainIdx.length > 0) {
|
|
2570
|
+
candidates.push({
|
|
2571
|
+
dispatch: runtime.dispatch,
|
|
2572
|
+
indices: mainIdx
|
|
2573
|
+
});
|
|
2574
|
+
}
|
|
2575
|
+
return candidates;
|
|
2576
|
+
}
|
|
2577
|
+
function $dispatch(runtime, node) {
|
|
2578
|
+
const candidates = getCandidates(runtime, node);
|
|
2579
|
+
if (candidates.length === 0) {
|
|
2580
|
+
return $hoistChildrenOf(runtime, node);
|
|
2581
|
+
}
|
|
2582
|
+
let groupCursor = 0;
|
|
2583
|
+
let ruleCursor = 0;
|
|
2584
|
+
const $next = () => {
|
|
2585
|
+
while (groupCursor < candidates.length) {
|
|
2586
|
+
const {
|
|
2587
|
+
dispatch,
|
|
2588
|
+
indices
|
|
2589
|
+
} = candidates[groupCursor];
|
|
2590
|
+
while (ruleCursor < indices.length) {
|
|
2591
|
+
const idx = indices[ruleCursor++];
|
|
2592
|
+
const rule = dispatch.rules[idx];
|
|
2593
|
+
const captures = {};
|
|
2594
|
+
if (rule.predicate(node, captures)) {
|
|
2595
|
+
const ctx = makeContext(runtime, Object.keys(captures).length === 0 ? NO_CAPTURES : captures);
|
|
2596
|
+
try {
|
|
2597
|
+
return rule.$import(ctx, node, $next);
|
|
2598
|
+
} catch (e) {
|
|
2599
|
+
{
|
|
2600
|
+
console.error(`[lexical] DOM import rule "${rule.name}" threw on node`, node, e);
|
|
2601
|
+
}
|
|
2602
|
+
throw e;
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
groupCursor++;
|
|
2607
|
+
ruleCursor = 0;
|
|
2608
|
+
}
|
|
2609
|
+
return $hoistChildrenOf(runtime, node);
|
|
2610
|
+
};
|
|
2611
|
+
return $next();
|
|
2612
|
+
}
|
|
2613
|
+
|
|
2614
|
+
/**
|
|
2615
|
+
* Fallback when no rule matched and `$next()` was called past the end of the
|
|
2616
|
+
* chain: hoist the element's children to take its place, recursively. Pure
|
|
2617
|
+
* elements with no rule become invisible, matching the legacy
|
|
2618
|
+
* `$createNodesFromDOM` hoisting behavior.
|
|
2619
|
+
*/
|
|
2620
|
+
function $hoistChildrenOf(runtime, node) {
|
|
2621
|
+
if (node.childNodes.length === 0) {
|
|
2622
|
+
return [];
|
|
2623
|
+
}
|
|
2624
|
+
const collected = [];
|
|
2625
|
+
for (const child of Array.from(node.childNodes)) {
|
|
2626
|
+
const produced = $importOneInternal(runtime, child, undefined);
|
|
2627
|
+
for (const lex of produced) {
|
|
2628
|
+
collected.push(lex);
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
return collected;
|
|
2632
|
+
}
|
|
2633
|
+
|
|
2634
|
+
/**
|
|
2635
|
+
* Top-level walker for a compiled dispatcher. Iterates the DOM children of
|
|
2636
|
+
* `dom` (using the document body if a {@link Document} is passed) and
|
|
2637
|
+
* applies `RootSchema` to the produced lexical nodes so runs of inlines are
|
|
2638
|
+
* wrapped in paragraphs — same shape as the legacy `$generateNodesFromDOM`.
|
|
2639
|
+
*
|
|
2640
|
+
* @internal
|
|
2641
|
+
*/
|
|
2642
|
+
function $runImport(dispatch, editor, dom, session) {
|
|
2643
|
+
// Prime the overlay stack with any overlays a preprocess wrote to
|
|
2644
|
+
// ImportOverlays. These remain in effect for the entire walk; nested
|
|
2645
|
+
// `$importChildren({rules})` calls push on top.
|
|
2646
|
+
const installed = session.get(ImportOverlays);
|
|
2647
|
+
const runtime = {
|
|
2648
|
+
dispatch,
|
|
2649
|
+
editor,
|
|
2650
|
+
overlays: installed.map(o => o.dispatch),
|
|
2651
|
+
session
|
|
2652
|
+
};
|
|
2653
|
+
const rootParent = lexical.isDOMDocumentNode(dom) ? dom.body : dom;
|
|
2654
|
+
return $importChildrenRun(runtime, rootParent, {
|
|
2655
|
+
schema: RootSchema
|
|
2656
|
+
});
|
|
2657
|
+
}
|
|
2658
|
+
|
|
2659
|
+
/**
|
|
2660
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2661
|
+
*
|
|
2662
|
+
* This source code is licensed under the MIT license found in the
|
|
2663
|
+
* LICENSE file in the root directory of this source tree.
|
|
2664
|
+
*
|
|
2665
|
+
*/
|
|
2666
|
+
|
|
2667
|
+
|
|
2668
|
+
/**
|
|
2669
|
+
* Configuration for {@link DOMImportExtension}.
|
|
2670
|
+
*
|
|
2671
|
+
* @experimental
|
|
2672
|
+
*/
|
|
2673
|
+
|
|
2674
|
+
/**
|
|
2675
|
+
* Drive a stack of {@link DOMPreprocessFn}s top-to-bottom: the highest-
|
|
2676
|
+
* index fn runs first and may call `$next()` to defer to the next-lower
|
|
2677
|
+
* one. Matches the export-side `callExportMimeTypeFunctionStack` shape.
|
|
2678
|
+
*/
|
|
2679
|
+
function $runPreprocessStack(stack, dom, ctx) {
|
|
2680
|
+
let i = stack.length - 1;
|
|
2681
|
+
const $next = () => {
|
|
2682
|
+
while (i >= 0) {
|
|
2683
|
+
const cur = stack[i--];
|
|
2684
|
+
cur(dom, ctx, $next);
|
|
2685
|
+
return;
|
|
2686
|
+
}
|
|
2687
|
+
};
|
|
2688
|
+
$next();
|
|
2689
|
+
}
|
|
2690
|
+
|
|
2691
|
+
/**
|
|
2692
|
+
* Lowest-priority catch-all rule used as the default `config.rules` entry
|
|
2693
|
+
* for {@link DOMImportExtension}: descends into the element's children
|
|
2694
|
+
* and returns whatever they produced. With no other matching rule, an
|
|
2695
|
+
* element vanishes and its contents are inserted in its place — the
|
|
2696
|
+
* legacy `$createNodesFromDOM` hoisting behavior, but now expressed as a
|
|
2697
|
+
* regular rule that apps can override (e.g. with a `sel.any()` rule that
|
|
2698
|
+
* captures and discards unknown elements).
|
|
2699
|
+
*
|
|
2700
|
+
* @experimental
|
|
2701
|
+
*/
|
|
2702
|
+
const DefaultHoistRule = defineImportRule({
|
|
2703
|
+
$import: (ctx, el) => ctx.$importChildren(el),
|
|
2704
|
+
match: selBase.any(),
|
|
2705
|
+
name: '@lexical/html/default-hoist'
|
|
2706
|
+
});
|
|
2707
|
+
|
|
2708
|
+
/**
|
|
2709
|
+
* @experimental
|
|
2710
|
+
*
|
|
2711
|
+
* Extension-based replacement for the legacy `importDOM` / `DOMConversion`
|
|
2712
|
+
* machinery. Rules are contributed via configuration (see
|
|
2713
|
+
* {@link DOMImportConfig.rules}), compiled into a tag-bucketed dispatcher at
|
|
2714
|
+
* editor build time, and consumed via the extension's
|
|
2715
|
+
* {@link DOMImportExtensionOutput.$generateNodesFromDOM} output.
|
|
2716
|
+
*
|
|
2717
|
+
* The legacy `$generateNodesFromDOM` continues to work in parallel; the
|
|
2718
|
+
* intent is to migrate node packages over to this extension incrementally.
|
|
2719
|
+
*/
|
|
2720
|
+
const DOMImportExtension = lexical.defineExtension({
|
|
2721
|
+
build(editor, config) {
|
|
2722
|
+
const dispatch = compileImportRules(flattenRuleEntries(config.rules));
|
|
2723
|
+
const defaults = contextFromPairs(config.contextDefaults, undefined);
|
|
2724
|
+
const configPreprocess = config.preprocess;
|
|
2725
|
+
return {
|
|
2726
|
+
$generateNodesFromDOM: (dom, options) => {
|
|
2727
|
+
// The session record IS the root layer of the walk's context.
|
|
2728
|
+
// Start with per-call options.context applied on top of the
|
|
2729
|
+
// editor's contextDefaults, then ensure we have a *fresh*
|
|
2730
|
+
// mutable child (never the shared defaults record) so
|
|
2731
|
+
// session.set writes never leak into the editor's config.
|
|
2732
|
+
const fromOpts = options && options.context ? contextFromPairs(options.context, defaults) : defaults;
|
|
2733
|
+
const sessionRecord = fromOpts !== undefined && fromOpts !== defaults ? fromOpts : Object.create(defaults || null);
|
|
2734
|
+
const session = new ImportSessionImpl(sessionRecord);
|
|
2735
|
+
const preprocessCtx = {
|
|
2736
|
+
session
|
|
2737
|
+
};
|
|
2738
|
+
// Stack of preprocessors: config-level first, then per-call.
|
|
2739
|
+
// Top of stack (last in array) runs first; `next()` defers to
|
|
2740
|
+
// the next-lower one. Matches the GetClipboardDataExtension
|
|
2741
|
+
// convention so app-registered preprocessors can wrap built-in
|
|
2742
|
+
// ones via `next()`. Preprocess writes via `ctx.session.set`
|
|
2743
|
+
// mutate the session record directly.
|
|
2744
|
+
const stack = options && options.preprocess ? [...configPreprocess, ...options.preprocess] : configPreprocess;
|
|
2745
|
+
$runPreprocessStack(stack, dom, preprocessCtx);
|
|
2746
|
+
return $withFullContext(DOMImportContextSymbol, sessionRecord, () => $runImport(dispatch, editor, dom, session), editor);
|
|
2747
|
+
},
|
|
2748
|
+
defaults
|
|
2749
|
+
};
|
|
2750
|
+
},
|
|
2751
|
+
config: {
|
|
2752
|
+
contextDefaults: [],
|
|
2753
|
+
preprocess: [$inlineStylesFromStyleSheets],
|
|
2754
|
+
rules: [DefaultHoistRule]
|
|
2755
|
+
},
|
|
2756
|
+
mergeConfig(config, partial) {
|
|
2757
|
+
return lexical.shallowMergeConfig(config, {
|
|
2758
|
+
...partial,
|
|
2759
|
+
...(partial.contextDefaults && {
|
|
2760
|
+
contextDefaults: [...config.contextDefaults, ...partial.contextDefaults]
|
|
2761
|
+
}),
|
|
2762
|
+
...(partial.preprocess && {
|
|
2763
|
+
preprocess: [...config.preprocess, ...partial.preprocess]
|
|
2764
|
+
}),
|
|
2765
|
+
...(partial.rules && {
|
|
2766
|
+
rules: [...partial.rules, ...config.rules]
|
|
2767
|
+
})
|
|
2768
|
+
});
|
|
2769
|
+
},
|
|
2770
|
+
name: DOMImportExtensionName
|
|
2771
|
+
});
|
|
2772
|
+
|
|
2773
|
+
/**
|
|
2774
|
+
* Look up the editor's {@link DOMImportExtension} and run its
|
|
2775
|
+
* `$generateNodesFromDOM`. Designed as a drop-in replacement for the
|
|
2776
|
+
* legacy `$generateNodesFromDOM(editor, dom)` signature so it can be
|
|
2777
|
+
* supplied to `ClipboardImportExtension.$generateNodesFromDOM` (or any
|
|
2778
|
+
* other consumer that wants to route through the extension pipeline).
|
|
2779
|
+
*
|
|
2780
|
+
* Throws if the editor was not built with {@link DOMImportExtension} as a
|
|
2781
|
+
* dependency.
|
|
2782
|
+
*
|
|
2783
|
+
* @experimental
|
|
2784
|
+
*/
|
|
2785
|
+
function $generateNodesFromDOMViaExtension(dom, options) {
|
|
2786
|
+
return extension.$getExtensionOutput(DOMImportExtension).$generateNodesFromDOM(dom, options);
|
|
2787
|
+
}
|
|
2788
|
+
|
|
2789
|
+
/**
|
|
2790
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2791
|
+
*
|
|
2792
|
+
* This source code is licensed under the MIT license found in the
|
|
2793
|
+
* LICENSE file in the root directory of this source tree.
|
|
2794
|
+
*
|
|
2795
|
+
*/
|
|
2796
|
+
|
|
2797
|
+
/**
|
|
2798
|
+
* Bundles {@link CoreImportRules} into a {@link DOMImportExtension}-aware
|
|
2799
|
+
* extension. Depend on this from your editor (directly or via richer
|
|
2800
|
+
* extensions like `RichTextImportExtension`) to get the equivalent of the
|
|
2801
|
+
* legacy core `importDOM` behavior for `<p>`, `<span>`, `<b>`,
|
|
2802
|
+
* `<strong>`, `<em>`, `<i>`, `<code>`, `<mark>`, `<s>`, `<sub>`, `<sup>`,
|
|
2803
|
+
* `<u>`, `<br>`, and `#text`.
|
|
2804
|
+
*
|
|
2805
|
+
* @experimental
|
|
2806
|
+
*/
|
|
2807
|
+
const CoreImportExtension = lexical.defineExtension({
|
|
2808
|
+
dependencies: [lexical.configExtension(DOMImportExtension, {
|
|
2809
|
+
rules: CoreImportRules
|
|
2810
|
+
})],
|
|
2811
|
+
name: '@lexical/html/CoreImport'
|
|
2812
|
+
});
|
|
2813
|
+
|
|
2814
|
+
/**
|
|
2815
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2816
|
+
*
|
|
2817
|
+
* This source code is licensed under the MIT license found in the
|
|
2818
|
+
* LICENSE file in the root directory of this source tree.
|
|
2819
|
+
*
|
|
2820
|
+
*/
|
|
2821
|
+
|
|
2822
|
+
const HorizontalRuleRule = defineImportRule({
|
|
2823
|
+
$import: () => [extension.$createHorizontalRuleNode()],
|
|
2824
|
+
match: selBase.tag('hr'),
|
|
2825
|
+
name: '@lexical/html/hr'
|
|
2826
|
+
});
|
|
2827
|
+
|
|
2828
|
+
/**
|
|
2829
|
+
* Import rules for {@link HorizontalRuleNode}.
|
|
2830
|
+
*
|
|
2831
|
+
* @experimental
|
|
2832
|
+
*/
|
|
2833
|
+
const HorizontalRuleImportRules = [HorizontalRuleRule];
|
|
2834
|
+
|
|
2835
|
+
/**
|
|
2836
|
+
* Bundles {@link HorizontalRuleImportRules} (plus
|
|
2837
|
+
* {@link CoreImportExtension}) into a single dependency. The legacy
|
|
2838
|
+
* {@link HorizontalRuleExtension.importDOM} continues to work in parallel;
|
|
2839
|
+
* depend on this extension to opt into the new pipeline.
|
|
2840
|
+
*
|
|
2841
|
+
* Lives in `@lexical/html` (not `@lexical/extension`) because
|
|
2842
|
+
* {@link DOMImportExtension} itself is in `@lexical/html`, and
|
|
2843
|
+
* `@lexical/extension` is upstream of `@lexical/html` in the dependency
|
|
2844
|
+
* graph — same arrangement as {@link CoreImportExtension}.
|
|
2845
|
+
*
|
|
2846
|
+
* @experimental
|
|
2847
|
+
*/
|
|
2848
|
+
const HorizontalRuleImportExtension = lexical.defineExtension({
|
|
2849
|
+
dependencies: [CoreImportExtension, extension.HorizontalRuleExtension, lexical.configExtension(DOMImportExtension, {
|
|
2850
|
+
rules: HorizontalRuleImportRules
|
|
2851
|
+
})],
|
|
2852
|
+
name: '@lexical/html/HorizontalRuleImport'
|
|
2853
|
+
});
|
|
2854
|
+
|
|
2855
|
+
/**
|
|
2856
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2857
|
+
*
|
|
2858
|
+
* This source code is licensed under the MIT license found in the
|
|
2859
|
+
* LICENSE file in the root directory of this source tree.
|
|
2860
|
+
*
|
|
2861
|
+
*/
|
|
2862
|
+
|
|
2863
|
+
/**
|
|
2864
|
+
* Combinator-and-parser-based builder for {@link CompiledSelector}s. The
|
|
2865
|
+
* runtime shape returned by these factory methods is opaque; consumers
|
|
2866
|
+
* should never inspect or construct selector objects directly.
|
|
2867
|
+
*
|
|
2868
|
+
* @experimental
|
|
2869
|
+
*/
|
|
2870
|
+
const sel = {
|
|
2871
|
+
any: selBase.any,
|
|
2872
|
+
comment: selBase.comment,
|
|
2873
|
+
/**
|
|
2874
|
+
* Parse a reduced CSS-selector subset and return a builder you can chain
|
|
2875
|
+
* combinator methods off of.
|
|
2876
|
+
*/
|
|
2877
|
+
css: parseSelector,
|
|
2878
|
+
tag: selBase.tag,
|
|
2879
|
+
text: selBase.text
|
|
2880
|
+
};
|
|
2881
|
+
|
|
2882
|
+
const IGNORE_TAGS = new Set(['STYLE', 'SCRIPT']);
|
|
2883
|
+
|
|
2884
|
+
/**
|
|
2885
|
+
* How you parse your html string to get a document is left up to you. In the browser you can use the native
|
|
2886
|
+
* DOMParser API to generate a document (see clipboard.ts), but to use in a headless environment you can use JSDom
|
|
2887
|
+
* or an equivalent library and pass in the document here.
|
|
2888
|
+
*/
|
|
2889
|
+
function $generateNodesFromDOM(editor, dom) {
|
|
2890
|
+
$inlineStylesFromStyleSheetsDOM(dom);
|
|
2891
|
+
const elements = lexical.isDOMDocumentNode(dom) ? dom.body.childNodes : dom.childNodes;
|
|
2892
|
+
const lexicalNodes = [];
|
|
2893
|
+
const allArtificialNodes = [];
|
|
2894
|
+
for (const element of elements) {
|
|
2895
|
+
if (!IGNORE_TAGS.has(element.nodeName)) {
|
|
2896
|
+
const lexicalNode = $createNodesFromDOM(element, editor, allArtificialNodes, false);
|
|
2897
|
+
if (lexicalNode !== null) {
|
|
2898
|
+
for (const node of lexicalNode) {
|
|
2899
|
+
lexicalNodes.push(node);
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
2903
|
+
}
|
|
2904
|
+
$unwrapArtificialNodes(allArtificialNodes);
|
|
2905
|
+
return lexicalNodes;
|
|
2906
|
+
}
|
|
2907
|
+
|
|
2908
|
+
/**
|
|
2909
|
+
* Generate DOM nodes from the editor state into the given container element,
|
|
2910
|
+
* using the editor's {@link EditorDOMRenderConfig}.
|
|
2911
|
+
* @experimental
|
|
2912
|
+
*/
|
|
2913
|
+
function $generateDOMFromNodes(container, selection = null, editor = lexical.$getEditor()) {
|
|
2914
|
+
return $withRenderContext([contextValue(RenderContextExport, true)], editor)(() => {
|
|
2915
|
+
const root = lexical.$getRoot();
|
|
2916
|
+
const domConfig = $getSessionDOMRenderConfig(editor);
|
|
2917
|
+
const parentElementAppend = container.append.bind(container);
|
|
2918
|
+
for (const topLevelNode of root.getChildren()) {
|
|
2919
|
+
$appendNodesToHTML(editor, topLevelNode, parentElementAppend, selection, domConfig);
|
|
2920
|
+
}
|
|
2921
|
+
return container;
|
|
2922
|
+
});
|
|
2923
|
+
}
|
|
2924
|
+
|
|
2925
|
+
/**
|
|
2926
|
+
* Generate DOM nodes from a root node into the given container element,
|
|
2927
|
+
* including the root node itself. Uses the editor's {@link EditorDOMRenderConfig}.
|
|
2928
|
+
* @experimental
|
|
2929
|
+
*/
|
|
2930
|
+
function $generateDOMFromRoot(container, root = lexical.$getRoot()) {
|
|
2931
|
+
const editor = lexical.$getEditor();
|
|
2932
|
+
return $withRenderContext([contextValue(RenderContextExport, true), contextValue(RenderContextRoot, true)], editor)(() => {
|
|
2933
|
+
const selection = null;
|
|
2934
|
+
const domConfig = $getSessionDOMRenderConfig(editor);
|
|
2935
|
+
const parentElementAppend = container.append.bind(container);
|
|
2936
|
+
$appendNodesToHTML(editor, root, parentElementAppend, selection, domConfig);
|
|
2937
|
+
return container;
|
|
2938
|
+
});
|
|
2939
|
+
}
|
|
2940
|
+
|
|
2941
|
+
/**
|
|
2942
|
+
* Generate an HTML string from the editor's current state (or `selection`
|
|
2943
|
+
* if provided).
|
|
2944
|
+
*
|
|
2945
|
+
* Must be called inside an active editor scope — i.e. `editor.update(...)`,
|
|
2946
|
+
* `editor.read(...)`, or `editor.getEditorState().read(callback, {editor})`.
|
|
2947
|
+
* The legacy `editor.getEditorState().read(callback)` call (without the
|
|
2948
|
+
* `{editor}` option) does not set an active editor and is not supported;
|
|
2949
|
+
* `editor.read(...)` is the drop-in replacement.
|
|
2950
|
+
*/
|
|
2951
|
+
function $generateHtmlFromNodes(editor, selection = null) {
|
|
2952
|
+
if (typeof document === 'undefined' || typeof window === 'undefined' && typeof global.window === 'undefined') {
|
|
2953
|
+
{
|
|
2954
|
+
formatDevErrorMessage(`To use $generateHtmlFromNodes in headless mode please initialize a headless browser implementation such as JSDom or use withDOM from @lexical/headless/dom before calling this function.`);
|
|
2955
|
+
}
|
|
2956
|
+
}
|
|
2957
|
+
return $generateDOMFromNodes(document.createElement('div'), selection, editor).innerHTML;
|
|
2958
|
+
}
|
|
2959
|
+
function $appendNodesToHTML(editor, currentNode, parentElementAppend, selection$1 = null, domConfig = lexical.$getEditorDOMRenderConfig(editor)) {
|
|
2960
|
+
let shouldInclude = domConfig.$shouldInclude(currentNode, selection$1, editor);
|
|
2961
|
+
const shouldExclude = domConfig.$shouldExclude(currentNode, selection$1, editor);
|
|
2962
|
+
let target = currentNode;
|
|
2963
|
+
if (selection$1 !== null && lexical.$isTextNode(currentNode)) {
|
|
2964
|
+
target = selection.$sliceSelectedTextNodeContent(selection$1, currentNode, 'clone');
|
|
2965
|
+
}
|
|
2966
|
+
const exportProps = domConfig.$exportDOM(target, editor);
|
|
2967
|
+
const {
|
|
2968
|
+
element,
|
|
2969
|
+
after,
|
|
2970
|
+
append,
|
|
2971
|
+
$getChildNodes
|
|
2972
|
+
} = exportProps;
|
|
2973
|
+
if (!element) {
|
|
2974
|
+
return false;
|
|
2975
|
+
}
|
|
2976
|
+
const fragment = document.createDocumentFragment();
|
|
2977
|
+
const children = $getChildNodes ? $getChildNodes() : lexical.$isElementNode(target) ? target.getChildren() : [];
|
|
2978
|
+
const fragmentAppend = fragment.append.bind(fragment);
|
|
2979
|
+
for (const childNode of children) {
|
|
2980
|
+
const shouldIncludeChild = $appendNodesToHTML(editor, childNode, fragmentAppend, selection$1, domConfig);
|
|
2981
|
+
if (!shouldInclude && shouldIncludeChild && domConfig.$extractWithChild(currentNode, childNode, selection$1, 'html', editor)) {
|
|
2982
|
+
shouldInclude = true;
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
if (shouldInclude && !shouldExclude) {
|
|
2986
|
+
if (lexical.isHTMLElement(element) || lexical.isDocumentFragment(element)) {
|
|
2987
|
+
if (append) {
|
|
2988
|
+
append(fragment);
|
|
2989
|
+
} else {
|
|
2990
|
+
element.append(fragment);
|
|
2991
|
+
}
|
|
2992
|
+
}
|
|
2993
|
+
parentElementAppend(element);
|
|
2994
|
+
if (after) {
|
|
2995
|
+
const newElement = after.call(target, element);
|
|
2996
|
+
if (newElement) {
|
|
2997
|
+
if (lexical.isDocumentFragment(element)) {
|
|
2998
|
+
element.replaceChildren(newElement);
|
|
2999
|
+
} else {
|
|
3000
|
+
element.replaceWith(newElement);
|
|
3001
|
+
}
|
|
3002
|
+
}
|
|
3003
|
+
}
|
|
3004
|
+
} else {
|
|
3005
|
+
parentElementAppend(fragment);
|
|
3006
|
+
}
|
|
3007
|
+
return shouldInclude;
|
|
3008
|
+
}
|
|
3009
|
+
function getConversionFunction(domNode, editor) {
|
|
3010
|
+
const {
|
|
3011
|
+
nodeName
|
|
3012
|
+
} = domNode;
|
|
3013
|
+
const cachedConversions = editor._htmlConversions.get(nodeName.toLowerCase());
|
|
3014
|
+
let currentConversion = null;
|
|
3015
|
+
if (cachedConversions !== undefined) {
|
|
3016
|
+
for (const cachedConversion of cachedConversions) {
|
|
3017
|
+
const domConversion = cachedConversion(domNode);
|
|
3018
|
+
if (domConversion !== null && (currentConversion === null ||
|
|
3019
|
+
// Given equal priority, prefer the last registered importer
|
|
3020
|
+
// which is typically an application custom node or HTMLConfig['import']
|
|
3021
|
+
(currentConversion.priority || 0) <= (domConversion.priority || 0))) {
|
|
3022
|
+
currentConversion = domConversion;
|
|
3023
|
+
}
|
|
3024
|
+
}
|
|
3025
|
+
}
|
|
3026
|
+
return currentConversion !== null ? currentConversion.conversion : null;
|
|
3027
|
+
}
|
|
3028
|
+
function $createNodesFromDOM(node, editor, allArtificialNodes, hasBlockAncestorLexicalNode, forChildMap = new Map(), parentLexicalNode) {
|
|
3029
|
+
const lexicalNodes = [];
|
|
3030
|
+
if (IGNORE_TAGS.has(node.nodeName)) {
|
|
3031
|
+
return lexicalNodes;
|
|
3032
|
+
}
|
|
3033
|
+
let currentLexicalNode = null;
|
|
3034
|
+
const transformFunction = getConversionFunction(node, editor);
|
|
3035
|
+
const transformOutput = transformFunction ? transformFunction(node) : null;
|
|
3036
|
+
let postTransform = null;
|
|
3037
|
+
if (transformOutput !== null) {
|
|
3038
|
+
postTransform = transformOutput.after;
|
|
3039
|
+
const transformNodes = transformOutput.node;
|
|
3040
|
+
currentLexicalNode = Array.isArray(transformNodes) ? transformNodes[transformNodes.length - 1] : transformNodes;
|
|
3041
|
+
if (currentLexicalNode !== null) {
|
|
3042
|
+
for (const [, forChildFunction] of forChildMap) {
|
|
3043
|
+
currentLexicalNode = forChildFunction(currentLexicalNode, parentLexicalNode);
|
|
3044
|
+
if (!currentLexicalNode) {
|
|
3045
|
+
break;
|
|
3046
|
+
}
|
|
3047
|
+
}
|
|
3048
|
+
if (currentLexicalNode) {
|
|
3049
|
+
lexicalNodes.push(...(Array.isArray(transformNodes) ? transformNodes : [currentLexicalNode]));
|
|
3050
|
+
}
|
|
3051
|
+
}
|
|
3052
|
+
if (transformOutput.forChild != null) {
|
|
3053
|
+
forChildMap.set(node.nodeName, transformOutput.forChild);
|
|
3054
|
+
}
|
|
3055
|
+
}
|
|
3056
|
+
|
|
3057
|
+
// If the DOM node doesn't have a transformer, we don't know what
|
|
3058
|
+
// to do with it but we still need to process any childNodes.
|
|
3059
|
+
const children = node.childNodes;
|
|
3060
|
+
let childLexicalNodes = [];
|
|
3061
|
+
const hasBlockAncestorLexicalNodeForChildren = currentLexicalNode != null && lexical.$isRootOrShadowRoot(currentLexicalNode) ? false : currentLexicalNode != null && lexical.$isBlockElementNode(currentLexicalNode) || hasBlockAncestorLexicalNode;
|
|
3062
|
+
for (let i = 0; i < children.length; i++) {
|
|
3063
|
+
childLexicalNodes.push(...$createNodesFromDOM(children[i], editor, allArtificialNodes, hasBlockAncestorLexicalNodeForChildren, new Map(forChildMap), currentLexicalNode));
|
|
3064
|
+
}
|
|
3065
|
+
if (postTransform != null) {
|
|
3066
|
+
childLexicalNodes = postTransform(childLexicalNodes);
|
|
3067
|
+
}
|
|
3068
|
+
if (lexical.isBlockDomNode(node)) {
|
|
3069
|
+
if (!hasBlockAncestorLexicalNodeForChildren) {
|
|
3070
|
+
childLexicalNodes = wrapContinuousInlines(node, childLexicalNodes, lexical.$createParagraphNode);
|
|
3071
|
+
} else {
|
|
3072
|
+
childLexicalNodes = wrapContinuousInlines(node, childLexicalNodes, () => {
|
|
3073
|
+
const artificialNode = new lexical.ArtificialNode__DO_NOT_USE();
|
|
3074
|
+
allArtificialNodes.push(artificialNode);
|
|
3075
|
+
return artificialNode;
|
|
3076
|
+
});
|
|
3077
|
+
}
|
|
3078
|
+
}
|
|
3079
|
+
if (currentLexicalNode == null) {
|
|
3080
|
+
if (childLexicalNodes.length > 0) {
|
|
3081
|
+
// If it hasn't been converted to a LexicalNode, we hoist its children
|
|
3082
|
+
// up to the same level as it.
|
|
3083
|
+
for (const childNode of childLexicalNodes) {
|
|
3084
|
+
lexicalNodes.push(childNode);
|
|
3085
|
+
}
|
|
3086
|
+
} else {
|
|
3087
|
+
if (lexical.isBlockDomNode(node) && isDomNodeBetweenTwoInlineNodes(node)) {
|
|
3088
|
+
// Empty block dom node that hasnt been converted, we replace it with a linebreak if its between inline nodes
|
|
3089
|
+
lexicalNodes.push(lexical.$createLineBreakNode());
|
|
3090
|
+
}
|
|
3091
|
+
}
|
|
3092
|
+
} else {
|
|
3093
|
+
if (lexical.$isElementNode(currentLexicalNode)) {
|
|
3094
|
+
// If the current node is a ElementNode after conversion,
|
|
3095
|
+
// we can append all the children to it.
|
|
3096
|
+
currentLexicalNode.append(...childLexicalNodes);
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
3099
|
+
return lexicalNodes;
|
|
3100
|
+
}
|
|
3101
|
+
function wrapContinuousInlines(domNode, nodes, createWrapperFn) {
|
|
3102
|
+
const textAlign = domNode.style.textAlign;
|
|
3103
|
+
const out = [];
|
|
3104
|
+
let continuousInlines = [];
|
|
3105
|
+
// wrap contiguous inline child nodes in para
|
|
3106
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
3107
|
+
const node = nodes[i];
|
|
3108
|
+
if (lexical.$isBlockElementNode(node)) {
|
|
3109
|
+
if (textAlign && !node.getFormat()) {
|
|
3110
|
+
node.setFormat(textAlign);
|
|
3111
|
+
}
|
|
3112
|
+
out.push(node);
|
|
3113
|
+
} else {
|
|
3114
|
+
continuousInlines.push(node);
|
|
3115
|
+
if (i === nodes.length - 1 || i < nodes.length - 1 && lexical.$isBlockElementNode(nodes[i + 1])) {
|
|
3116
|
+
const wrapper = createWrapperFn();
|
|
3117
|
+
wrapper.setFormat(textAlign);
|
|
3118
|
+
wrapper.append(...continuousInlines);
|
|
3119
|
+
out.push(wrapper);
|
|
3120
|
+
continuousInlines = [];
|
|
3121
|
+
}
|
|
3122
|
+
}
|
|
3123
|
+
}
|
|
3124
|
+
return out;
|
|
3125
|
+
}
|
|
3126
|
+
function $unwrapArtificialNodes(allArtificialNodes) {
|
|
3127
|
+
// Replace artificial node with its children, inserting a linebreak
|
|
3128
|
+
// between adjacent artificial nodes
|
|
3129
|
+
for (const node of allArtificialNodes) {
|
|
3130
|
+
if (node.getParent() && node.getNextSibling() instanceof lexical.ArtificialNode__DO_NOT_USE) {
|
|
3131
|
+
node.insertAfter(lexical.$createLineBreakNode());
|
|
3132
|
+
}
|
|
3133
|
+
}
|
|
3134
|
+
for (const node of allArtificialNodes) {
|
|
3135
|
+
const parent = node.getParent();
|
|
3136
|
+
if (parent) {
|
|
3137
|
+
parent.splice(node.getIndexWithinParent(), 1, node.getChildren());
|
|
3138
|
+
}
|
|
3139
|
+
}
|
|
3140
|
+
}
|
|
3141
|
+
function isDomNodeBetweenTwoInlineNodes(node) {
|
|
3142
|
+
if (node.nextSibling == null || node.previousSibling == null) {
|
|
3143
|
+
return false;
|
|
3144
|
+
}
|
|
3145
|
+
return lexical.isInlineDomNode(node.nextSibling) && lexical.isInlineDomNode(node.previousSibling);
|
|
3146
|
+
}
|
|
3147
|
+
|
|
3148
|
+
exports.$distributeInlineWrapper = $distributeInlineWrapper;
|
|
3149
|
+
exports.$generateDOMFromNodes = $generateDOMFromNodes;
|
|
3150
|
+
exports.$generateDOMFromRoot = $generateDOMFromRoot;
|
|
3151
|
+
exports.$generateHtmlFromNodes = $generateHtmlFromNodes;
|
|
3152
|
+
exports.$generateNodesFromDOM = $generateNodesFromDOM;
|
|
3153
|
+
exports.$generateNodesFromDOMViaExtension = $generateNodesFromDOMViaExtension;
|
|
3154
|
+
exports.$getImportContextValue = $getImportContextValue;
|
|
3155
|
+
exports.$getRenderContextValue = $getRenderContextValue;
|
|
3156
|
+
exports.$getSessionDOMRenderConfig = $getSessionDOMRenderConfig;
|
|
3157
|
+
exports.$inlineStylesFromStyleSheets = $inlineStylesFromStyleSheets;
|
|
3158
|
+
exports.$isBlockLevel = $isBlockLevel;
|
|
3159
|
+
exports.$setRenderContextValue = $setRenderContextValue;
|
|
3160
|
+
exports.$updateRenderContextValue = $updateRenderContextValue;
|
|
3161
|
+
exports.$withImportContext = $withImportContext;
|
|
3162
|
+
exports.$withRenderContext = $withRenderContext;
|
|
3163
|
+
exports.BlockSchema = BlockSchema;
|
|
3164
|
+
exports.CoreImportExtension = CoreImportExtension;
|
|
3165
|
+
exports.CoreImportRules = CoreImportRules;
|
|
3166
|
+
exports.DOMImportExtension = DOMImportExtension;
|
|
3167
|
+
exports.DOMRenderExtension = DOMRenderExtension;
|
|
3168
|
+
exports.HorizontalRuleImportExtension = HorizontalRuleImportExtension;
|
|
3169
|
+
exports.HorizontalRuleImportRules = HorizontalRuleImportRules;
|
|
3170
|
+
exports.ImportOverlays = ImportOverlays;
|
|
3171
|
+
exports.ImportSource = ImportSource;
|
|
3172
|
+
exports.ImportSourceDataTransfer = ImportSourceDataTransfer;
|
|
3173
|
+
exports.ImportTextFormat = ImportTextFormat;
|
|
3174
|
+
exports.ImportTextStyle = ImportTextStyle;
|
|
3175
|
+
exports.ImportWhitespaceConfig = ImportWhitespaceConfig;
|
|
3176
|
+
exports.InlineSchema = InlineSchema;
|
|
3177
|
+
exports.NestedBlockSchema = NestedBlockSchema;
|
|
3178
|
+
exports.RenderContextExport = RenderContextExport;
|
|
3179
|
+
exports.RenderContextRoot = RenderContextRoot;
|
|
3180
|
+
exports.RootSchema = RootSchema;
|
|
3181
|
+
exports.contextUpdater = contextUpdater;
|
|
3182
|
+
exports.contextValue = contextValue;
|
|
3183
|
+
exports.createImportState = createImportState;
|
|
3184
|
+
exports.createRenderState = createRenderState;
|
|
3185
|
+
exports.defaultIsInline = defaultIsInline;
|
|
3186
|
+
exports.defaultPreservesWhitespace = defaultPreservesWhitespace;
|
|
3187
|
+
exports.defineImportRule = defineImportRule;
|
|
3188
|
+
exports.defineOverlayRules = defineOverlayRules;
|
|
3189
|
+
exports.domOverride = domOverride;
|
|
3190
|
+
exports.isElementOfTag = isElementOfTag;
|
|
3191
|
+
exports.parseSelector = parseSelector;
|
|
3192
|
+
exports.sel = sel;
|