@lexical/html 0.44.1-nightly.20260519.0 → 0.45.1-dev.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 +3289 -0
- package/dist/LexicalHtml.dev.mjs +3242 -0
- package/{LexicalHtml.js.flow → dist/LexicalHtml.js.flow} +16 -16
- package/dist/LexicalHtml.mjs +57 -0
- package/dist/LexicalHtml.node.mjs +55 -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 +28 -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 +106 -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 +52 -0
- package/src/import/ImportContext.ts +339 -0
- package/src/import/compileImportRules.ts +178 -0
- package/src/import/coreImportRules.ts +545 -0
- package/src/import/defineImportRule.ts +40 -0
- package/src/import/defineOverlayRules.ts +105 -0
- package/src/import/index.ts +97 -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 +280 -0
- package/src/import/sel.ts +314 -0
- package/src/import/types.ts +471 -0
- package/src/index.ts +561 -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,545 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
import {
|
|
9
|
+
$createLineBreakNode,
|
|
10
|
+
$createParagraphNode,
|
|
11
|
+
$createTextNode,
|
|
12
|
+
$generateNodesFromRawText,
|
|
13
|
+
$isTextNode,
|
|
14
|
+
$setDirectionFromDOM,
|
|
15
|
+
$setFormatFromDOM,
|
|
16
|
+
type ElementFormatType,
|
|
17
|
+
IS_BOLD,
|
|
18
|
+
IS_CODE,
|
|
19
|
+
IS_HIGHLIGHT,
|
|
20
|
+
IS_ITALIC,
|
|
21
|
+
IS_STRIKETHROUGH,
|
|
22
|
+
IS_SUBSCRIPT,
|
|
23
|
+
IS_SUPERSCRIPT,
|
|
24
|
+
IS_UNDERLINE,
|
|
25
|
+
isBlockDomNode,
|
|
26
|
+
isDOMTextNode,
|
|
27
|
+
isLastChildInBlockNode,
|
|
28
|
+
isOnlyChildInBlockNode,
|
|
29
|
+
type LexicalNode,
|
|
30
|
+
setNodeIndentFromDOM,
|
|
31
|
+
} from 'lexical';
|
|
32
|
+
|
|
33
|
+
import {contextValue} from '../ContextRecord';
|
|
34
|
+
import {defineImportRule} from './defineImportRule';
|
|
35
|
+
import {
|
|
36
|
+
ImportTextFormat,
|
|
37
|
+
ImportTextStyle,
|
|
38
|
+
ImportWhitespaceConfig,
|
|
39
|
+
type WhitespaceImportConfig,
|
|
40
|
+
} from './ImportContext';
|
|
41
|
+
import {$propagateTextAlignToBlockChildren, BlockSchema} from './schemas';
|
|
42
|
+
import {selBase} from './sel';
|
|
43
|
+
|
|
44
|
+
const sel = selBase;
|
|
45
|
+
|
|
46
|
+
const ALIGNMENT_VALUES: ReadonlySet<string> = new Set<ElementFormatType>([
|
|
47
|
+
'center',
|
|
48
|
+
'end',
|
|
49
|
+
'justify',
|
|
50
|
+
'left',
|
|
51
|
+
'right',
|
|
52
|
+
'start',
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* True if `value` is a non-empty {@link ElementFormatType} (matches one of
|
|
57
|
+
* the supported `text-align` / legacy `align`-attribute values).
|
|
58
|
+
*
|
|
59
|
+
* @internal
|
|
60
|
+
*/
|
|
61
|
+
export function isAlignmentValue(value: string): value is ElementFormatType {
|
|
62
|
+
return ALIGNMENT_VALUES.has(value);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* A pair of bitmasks describing which {@link TextFormatType} bits to set
|
|
67
|
+
* and which to clear when descending into an element. The clear pass
|
|
68
|
+
* matters for cases the legacy OR-merge mishandled, e.g. `<b
|
|
69
|
+
* style="font-weight: normal">` clearing an inherited bold, or `<sub>` /
|
|
70
|
+
* `<sup>` clearing each other.
|
|
71
|
+
*/
|
|
72
|
+
interface FormatOverride {
|
|
73
|
+
readonly set: number;
|
|
74
|
+
readonly clear: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The small subset of inline-style properties that affect text formatting
|
|
79
|
+
* during import. Modeled as a plain object so tag-implicit defaults and
|
|
80
|
+
* the element's own inline `style` can be merged with `{...defaults,
|
|
81
|
+
* ...override-if-set}` semantics rather than relying on CSSStyleDeclaration.
|
|
82
|
+
*/
|
|
83
|
+
interface FormatStyle {
|
|
84
|
+
fontWeight?: string;
|
|
85
|
+
fontStyle?: string;
|
|
86
|
+
textDecoration?: string;
|
|
87
|
+
verticalAlign?: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Default style implied by each inline format tag. `<b>`/`<strong>` set
|
|
92
|
+
* font-weight, `<sub>` sets vertical-align, etc. Any of these can be
|
|
93
|
+
* overridden by the element's own inline `style` (so `<b
|
|
94
|
+
* style="font-weight: normal">` ends up with `fontWeight: 'normal'` in
|
|
95
|
+
* the effective style).
|
|
96
|
+
*/
|
|
97
|
+
const TAG_DEFAULT_STYLE: Record<string, FormatStyle> = {
|
|
98
|
+
B: {fontWeight: 'bold'},
|
|
99
|
+
EM: {fontStyle: 'italic'},
|
|
100
|
+
I: {fontStyle: 'italic'},
|
|
101
|
+
S: {textDecoration: 'line-through'},
|
|
102
|
+
STRONG: {fontWeight: 'bold'},
|
|
103
|
+
SUB: {verticalAlign: 'sub'},
|
|
104
|
+
SUP: {verticalAlign: 'super'},
|
|
105
|
+
U: {textDecoration: 'underline'},
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Tags whose effect on TextFormat has no CSS analog (so the style-merge
|
|
110
|
+
* path can't reach them). Applied as a pure "set" override.
|
|
111
|
+
*/
|
|
112
|
+
const TAG_ONLY_SET: Record<string, number> = {
|
|
113
|
+
CODE: IS_CODE,
|
|
114
|
+
MARK: IS_HIGHLIGHT,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
function readElementFormatStyle(el: HTMLElement): FormatStyle {
|
|
118
|
+
return {
|
|
119
|
+
fontStyle: el.style.fontStyle,
|
|
120
|
+
fontWeight: el.style.fontWeight,
|
|
121
|
+
textDecoration: el.style.textDecoration,
|
|
122
|
+
verticalAlign: el.style.verticalAlign,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function mergeStyles(
|
|
127
|
+
defaults: FormatStyle,
|
|
128
|
+
override: FormatStyle,
|
|
129
|
+
): FormatStyle {
|
|
130
|
+
return {
|
|
131
|
+
fontStyle: override.fontStyle || defaults.fontStyle,
|
|
132
|
+
fontWeight: override.fontWeight || defaults.fontWeight,
|
|
133
|
+
textDecoration: override.textDecoration || defaults.textDecoration,
|
|
134
|
+
verticalAlign: override.verticalAlign || defaults.verticalAlign,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* The CSS property names {@link styleFormatOverride} reads — these are
|
|
140
|
+
* "owned" by {@link ImportTextFormat} (the bit mask). When the
|
|
141
|
+
* {@link ImportTextStyle} record is materialized onto a TextNode's
|
|
142
|
+
* inline style by {@link styleObjectToCSS}, these are skipped so the
|
|
143
|
+
* bit-mask side is the single source of truth and the same property
|
|
144
|
+
* doesn't end up in both places (where the inline-style version would
|
|
145
|
+
* shadow the format's themed CSS).
|
|
146
|
+
*/
|
|
147
|
+
const FORMAT_BIT_STYLE_PROPS: ReadonlySet<string> = new Set([
|
|
148
|
+
'font-weight',
|
|
149
|
+
'font-style',
|
|
150
|
+
'text-decoration',
|
|
151
|
+
'vertical-align',
|
|
152
|
+
]);
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Translate a {@link FormatStyle} into a {@link FormatOverride}. Explicit
|
|
156
|
+
* "non-decorating" values (`font-weight: normal`, `text-decoration: none`,
|
|
157
|
+
* `vertical-align: baseline`) produce `clear` bits, so an inner element
|
|
158
|
+
* can remove a format inherited from its ancestors.
|
|
159
|
+
*/
|
|
160
|
+
function styleFormatOverride(style: FormatStyle): FormatOverride {
|
|
161
|
+
let set = 0;
|
|
162
|
+
let clear = 0;
|
|
163
|
+
|
|
164
|
+
const {fontWeight, fontStyle, textDecoration, verticalAlign} = style;
|
|
165
|
+
|
|
166
|
+
if (fontWeight === '700' || fontWeight === 'bold') {
|
|
167
|
+
set |= IS_BOLD;
|
|
168
|
+
} else if (fontWeight === 'normal' || fontWeight === '400') {
|
|
169
|
+
clear |= IS_BOLD;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (fontStyle === 'italic') {
|
|
173
|
+
set |= IS_ITALIC;
|
|
174
|
+
} else if (fontStyle === 'normal') {
|
|
175
|
+
clear |= IS_ITALIC;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (textDecoration) {
|
|
179
|
+
const parts = textDecoration.split(' ');
|
|
180
|
+
if (parts.includes('underline')) {
|
|
181
|
+
set |= IS_UNDERLINE;
|
|
182
|
+
}
|
|
183
|
+
if (parts.includes('line-through')) {
|
|
184
|
+
set |= IS_STRIKETHROUGH;
|
|
185
|
+
}
|
|
186
|
+
if (parts.includes('none')) {
|
|
187
|
+
clear |= IS_UNDERLINE | IS_STRIKETHROUGH;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (verticalAlign === 'sub') {
|
|
192
|
+
set |= IS_SUBSCRIPT;
|
|
193
|
+
clear |= IS_SUPERSCRIPT;
|
|
194
|
+
} else if (verticalAlign === 'super') {
|
|
195
|
+
set |= IS_SUPERSCRIPT;
|
|
196
|
+
clear |= IS_SUBSCRIPT;
|
|
197
|
+
} else if (verticalAlign === 'baseline') {
|
|
198
|
+
clear |= IS_SUBSCRIPT | IS_SUPERSCRIPT;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return {clear, set};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function applyFormatOverride(format: number, ov: FormatOverride): number {
|
|
205
|
+
return (format & ~ov.clear) | ov.set;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Unified rule for inline-format-bearing tags and `<span>`. The element's
|
|
210
|
+
* effective style is its tag's {@link TAG_DEFAULT_STYLE} merged with its
|
|
211
|
+
* inline `style` (element's own style wins for any property it sets), and
|
|
212
|
+
* the resulting style is translated into a {@link FormatOverride}. Tags
|
|
213
|
+
* with no CSS analog (`<code>`, `<mark>`) contribute their bit as a pure
|
|
214
|
+
* `set` override.
|
|
215
|
+
*
|
|
216
|
+
* This shape lets:
|
|
217
|
+
* - `<b style="font-weight: normal">` clear an inherited IS_BOLD.
|
|
218
|
+
* - `<sub><sup>x</sup></sub>` resolve to IS_SUPERSCRIPT only (sub/sup
|
|
219
|
+
* mutex via the vertical-align clear logic).
|
|
220
|
+
* - `<span style="text-decoration: none">` strip inherited underline /
|
|
221
|
+
* line-through.
|
|
222
|
+
*/
|
|
223
|
+
const InlineFormatRule = defineImportRule({
|
|
224
|
+
$import: (ctx, el) => {
|
|
225
|
+
const inherited = ctx.get(ImportTextFormat);
|
|
226
|
+
const tagDefault = TAG_DEFAULT_STYLE[el.nodeName];
|
|
227
|
+
const elStyle = readElementFormatStyle(el);
|
|
228
|
+
const effective = tagDefault ? mergeStyles(tagDefault, elStyle) : elStyle;
|
|
229
|
+
let merged = applyFormatOverride(inherited, styleFormatOverride(effective));
|
|
230
|
+
const tagOnly = TAG_ONLY_SET[el.nodeName];
|
|
231
|
+
if (tagOnly) {
|
|
232
|
+
merged |= tagOnly;
|
|
233
|
+
}
|
|
234
|
+
if (merged === inherited) {
|
|
235
|
+
return ctx.$importChildren(el);
|
|
236
|
+
}
|
|
237
|
+
return ctx.$importChildren(el, {
|
|
238
|
+
context: [contextValue(ImportTextFormat, merged)],
|
|
239
|
+
});
|
|
240
|
+
},
|
|
241
|
+
match: sel.tag(
|
|
242
|
+
'b',
|
|
243
|
+
'strong',
|
|
244
|
+
'em',
|
|
245
|
+
'i',
|
|
246
|
+
'code',
|
|
247
|
+
'mark',
|
|
248
|
+
's',
|
|
249
|
+
'sub',
|
|
250
|
+
'sup',
|
|
251
|
+
'u',
|
|
252
|
+
'span',
|
|
253
|
+
),
|
|
254
|
+
name: '@lexical/html/inline-format',
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Walk up the DOM ancestor chain to determine whether `node` is inside an
|
|
259
|
+
* element whose whitespace should be preserved, per the supplied
|
|
260
|
+
* {@link WhitespaceImportConfig.preservesWhitespace} predicate. Pure
|
|
261
|
+
* ancestor walk, no caching.
|
|
262
|
+
*/
|
|
263
|
+
function isInsidePreserveWhitespace(
|
|
264
|
+
node: Node,
|
|
265
|
+
wsConfig: WhitespaceImportConfig,
|
|
266
|
+
): boolean {
|
|
267
|
+
let current: Node | null = node.parentNode;
|
|
268
|
+
while (current !== null) {
|
|
269
|
+
if (wsConfig.preservesWhitespace(current)) {
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
current = current.parentNode;
|
|
273
|
+
}
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function findAdjacentTextOnLine(
|
|
278
|
+
text: Text,
|
|
279
|
+
forward: boolean,
|
|
280
|
+
wsConfig: WhitespaceImportConfig,
|
|
281
|
+
): Text | null {
|
|
282
|
+
let node: Node = text;
|
|
283
|
+
while (true) {
|
|
284
|
+
let sibling: Node | null = null;
|
|
285
|
+
while (
|
|
286
|
+
(sibling = forward ? node.nextSibling : node.previousSibling) === null
|
|
287
|
+
) {
|
|
288
|
+
const parent: Node | null = node.parentNode;
|
|
289
|
+
if (parent === null) {
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
node = parent;
|
|
293
|
+
}
|
|
294
|
+
node = sibling;
|
|
295
|
+
if (!wsConfig.isInline(node)) {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
let descendant: Node | null = node;
|
|
299
|
+
while ((descendant = forward ? node.firstChild : node.lastChild) !== null) {
|
|
300
|
+
node = descendant;
|
|
301
|
+
}
|
|
302
|
+
if (isDOMTextNode(node)) {
|
|
303
|
+
return node;
|
|
304
|
+
}
|
|
305
|
+
if (node.nodeName === 'BR') {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function collapseWhitespace(
|
|
312
|
+
textNode: Text,
|
|
313
|
+
wsConfig: WhitespaceImportConfig,
|
|
314
|
+
): string {
|
|
315
|
+
let textContent = (textNode.textContent || '')
|
|
316
|
+
.replace(/\r/g, '')
|
|
317
|
+
.replace(/[ \t\n]+/g, ' ');
|
|
318
|
+
if (textContent.length === 0) {
|
|
319
|
+
return '';
|
|
320
|
+
}
|
|
321
|
+
if (textContent[0] === ' ') {
|
|
322
|
+
let neighbor: Text | null = textNode;
|
|
323
|
+
let isStartOfLine = true;
|
|
324
|
+
while (
|
|
325
|
+
neighbor !== null &&
|
|
326
|
+
(neighbor = findAdjacentTextOnLine(neighbor, false, wsConfig)) !== null
|
|
327
|
+
) {
|
|
328
|
+
const neighborContent = neighbor.textContent || '';
|
|
329
|
+
if (neighborContent.length > 0) {
|
|
330
|
+
if (/[ \t\n]$/.test(neighborContent)) {
|
|
331
|
+
textContent = textContent.slice(1);
|
|
332
|
+
}
|
|
333
|
+
isStartOfLine = false;
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (isStartOfLine) {
|
|
338
|
+
textContent = textContent.slice(1);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
if (textContent.length > 0 && textContent[textContent.length - 1] === ' ') {
|
|
342
|
+
let neighbor: Text | null = textNode;
|
|
343
|
+
let isEndOfLine = true;
|
|
344
|
+
while (
|
|
345
|
+
neighbor !== null &&
|
|
346
|
+
(neighbor = findAdjacentTextOnLine(neighbor, true, wsConfig)) !== null
|
|
347
|
+
) {
|
|
348
|
+
const neighborContent = (neighbor.textContent || '').replace(
|
|
349
|
+
/^( |\t|\r?\n)+/,
|
|
350
|
+
'',
|
|
351
|
+
);
|
|
352
|
+
if (neighborContent.length > 0) {
|
|
353
|
+
isEndOfLine = false;
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
if (isEndOfLine) {
|
|
358
|
+
textContent = textContent.slice(0, -1);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return textContent;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function $applyFormat(node: LexicalNode, format: number): LexicalNode {
|
|
365
|
+
return format !== 0 && $isTextNode(node) ? node.setFormat(format) : node;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Inverse of {@link getStyleObjectFromCSS}: serialize a parsed style
|
|
370
|
+
* record back into a CSS declaration string suitable for
|
|
371
|
+
* `TextNode.setStyle`. Returns the empty string for an empty record.
|
|
372
|
+
*/
|
|
373
|
+
function styleObjectToCSS(style: Readonly<Record<string, string>>): string {
|
|
374
|
+
let css = '';
|
|
375
|
+
for (const prop in style) {
|
|
376
|
+
if (FORMAT_BIT_STYLE_PROPS.has(prop)) {
|
|
377
|
+
// Owned by ImportTextFormat (bit mask) — skip so the format-bit
|
|
378
|
+
// CSS is the single source of truth on the rendered TextNode.
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
css += `${prop}: ${style[prop]}; `;
|
|
382
|
+
}
|
|
383
|
+
return css.trimEnd();
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function $applyTextStyle(
|
|
387
|
+
node: LexicalNode,
|
|
388
|
+
style: Readonly<Record<string, string>>,
|
|
389
|
+
): LexicalNode {
|
|
390
|
+
if ($isTextNode(node)) {
|
|
391
|
+
const css = styleObjectToCSS(style);
|
|
392
|
+
if (css !== '') {
|
|
393
|
+
node.setStyle(css);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return node;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* `#text` rule. Inside a `<pre>` ancestor, preserve whitespace and split
|
|
401
|
+
* on `\n` and `\t` into `LineBreakNode`/`TabNode` siblings. Otherwise
|
|
402
|
+
* collapse whitespace using the same neighbor-aware rules as the legacy
|
|
403
|
+
* `$convertTextDOMNode`.
|
|
404
|
+
*/
|
|
405
|
+
const TextRule = defineImportRule({
|
|
406
|
+
$import: (ctx, el) => {
|
|
407
|
+
const format = ctx.get(ImportTextFormat);
|
|
408
|
+
const style = ctx.get(ImportTextStyle);
|
|
409
|
+
const wsConfig = ctx.get(ImportWhitespaceConfig);
|
|
410
|
+
if (isInsidePreserveWhitespace(el, wsConfig)) {
|
|
411
|
+
const out = $generateNodesFromRawText(el.textContent || '');
|
|
412
|
+
for (const node of out) {
|
|
413
|
+
$applyFormat(node, format);
|
|
414
|
+
$applyTextStyle(node, style);
|
|
415
|
+
}
|
|
416
|
+
return out;
|
|
417
|
+
}
|
|
418
|
+
const collapsed = collapseWhitespace(el, wsConfig);
|
|
419
|
+
if (collapsed === '') {
|
|
420
|
+
return [];
|
|
421
|
+
}
|
|
422
|
+
const text = $createTextNode(collapsed);
|
|
423
|
+
$applyFormat(text, format);
|
|
424
|
+
$applyTextStyle(text, style);
|
|
425
|
+
return [text];
|
|
426
|
+
},
|
|
427
|
+
match: sel.text(),
|
|
428
|
+
name: '@lexical/html/#text',
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Drop `<style>` and `<script>` and skip descending into them — matches
|
|
433
|
+
* the legacy `IGNORE_TAGS` set, but as a regular rule so apps can register
|
|
434
|
+
* a higher-priority `<style>` rule to capture stylesheet text into the
|
|
435
|
+
* import session for later use.
|
|
436
|
+
*/
|
|
437
|
+
const IgnoreScriptStyleRule = defineImportRule({
|
|
438
|
+
$import: () => [],
|
|
439
|
+
match: sel.tag('script', 'style'),
|
|
440
|
+
name: '@lexical/html/script-style-ignore',
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
const LineBreakRule = defineImportRule({
|
|
444
|
+
// Mirror the legacy LineBreakNode.importDOM filter: stray `<br>` that
|
|
445
|
+
// are the sole or trailing child of a block parent (e.g. Apple's
|
|
446
|
+
// `<br class="Apple-interchange-newline">` clipboard sentinel, or the
|
|
447
|
+
// trailing `<br>` browsers insert after the last text in a `<div>`)
|
|
448
|
+
// would otherwise survive as a LineBreakNode and tack an extra blank
|
|
449
|
+
// line onto the imported content.
|
|
450
|
+
$import: (_ctx, el) =>
|
|
451
|
+
isOnlyChildInBlockNode(el) || isLastChildInBlockNode(el)
|
|
452
|
+
? []
|
|
453
|
+
: [$createLineBreakNode()],
|
|
454
|
+
match: sel.tag('br'),
|
|
455
|
+
name: '@lexical/html/br',
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* `<p>` rule. Re-applies format, indent, direction, and the legacy
|
|
460
|
+
* `align` attribute fallback.
|
|
461
|
+
*/
|
|
462
|
+
const ParagraphRule = defineImportRule({
|
|
463
|
+
$import: (ctx, el) => {
|
|
464
|
+
const p = $createParagraphNode();
|
|
465
|
+
$setFormatFromDOM(p, el);
|
|
466
|
+
setNodeIndentFromDOM(el, p);
|
|
467
|
+
if (p.getFormatType() === '') {
|
|
468
|
+
const align = el.getAttribute('align');
|
|
469
|
+
if (align && isAlignmentValue(align)) {
|
|
470
|
+
p.setFormat(align);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
$setDirectionFromDOM(p, el);
|
|
474
|
+
// We deliberately pass no schema: paragraphs accept any inline run as-is.
|
|
475
|
+
// The enclosing context (root / block) is responsible for ensuring the
|
|
476
|
+
// paragraph itself is a valid block child.
|
|
477
|
+
return [p.splice(0, 0, ctx.$importChildren(el))];
|
|
478
|
+
},
|
|
479
|
+
match: sel.tag('p'),
|
|
480
|
+
name: '@lexical/html/p',
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Transparent block-container rule for any unconverted block-level DOM
|
|
485
|
+
* element — `<div>`, but also `<section>`, `<article>`, `<header>`,
|
|
486
|
+
* `<figure>`, … (everything {@link isBlockDomNode} recognizes via the
|
|
487
|
+
* legacy `BLOCK_TAG_RE`). Without it these would fall through to the
|
|
488
|
+
* dispatcher's `$hoistChildrenOf` / `DefaultHoistRule` fallback, which
|
|
489
|
+
* transparently lifts children up to the enclosing context. That works
|
|
490
|
+
* structurally, but (a) two sibling `<section>`s collapse into a single
|
|
491
|
+
* paragraph instead of two, and (b) any `text-align` set on the element
|
|
492
|
+
* is lost because the synthesized paragraph (built by the enclosing
|
|
493
|
+
* schema) sees the *grandparent* as `domParent`.
|
|
494
|
+
*
|
|
495
|
+
* The rule is registered as a `sel.any()` wildcard and defers (via
|
|
496
|
+
* `$next()`) for non-block elements so inline tags still reach the inline
|
|
497
|
+
* rules. Higher-priority tag rules (`<p>`, `<li>`, `<td>`, headings, …)
|
|
498
|
+
* are dispatched first and never reach here.
|
|
499
|
+
*
|
|
500
|
+
* The element's children run through {@link BlockSchema} so each inline
|
|
501
|
+
* run becomes its own `ParagraphNode` (with the element's `text-align`
|
|
502
|
+
* picked up via {@link $paragraphPackageRun}'s `domParent`), and any
|
|
503
|
+
* pre-existing block children get the same alignment applied via
|
|
504
|
+
* {@link $propagateTextAlignToBlockChildren}. The resulting block-level
|
|
505
|
+
* nodes are what the enclosing context sees — at the root a sibling
|
|
506
|
+
* paragraph is the natural shape; inside a block lexical container the
|
|
507
|
+
* container rule (e.g. {@link ListItemRule}) collapses paragraph
|
|
508
|
+
* children back into inline-with-line-break form. That way both `<p>`
|
|
509
|
+
* and transparent blocks (`<div>`, `<section>`, …) project to the same
|
|
510
|
+
* `ParagraphNode` intermediate, and there is no need for a marker node
|
|
511
|
+
* to distinguish them.
|
|
512
|
+
*/
|
|
513
|
+
const TransparentBlockRule = defineImportRule({
|
|
514
|
+
$import: (ctx, el, $next) => {
|
|
515
|
+
if (!isBlockDomNode(el)) {
|
|
516
|
+
// Inline element with no dedicated rule — let the inline rules (or
|
|
517
|
+
// the default hoist) handle it.
|
|
518
|
+
return $next();
|
|
519
|
+
}
|
|
520
|
+
return $propagateTextAlignToBlockChildren(
|
|
521
|
+
ctx.$importChildren(el, {schema: BlockSchema}),
|
|
522
|
+
el,
|
|
523
|
+
);
|
|
524
|
+
},
|
|
525
|
+
match: sel.any(),
|
|
526
|
+
name: '@lexical/html/transparent-block',
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Rules covering the {@link ParagraphNode}, {@link TextNode},
|
|
531
|
+
* {@link LineBreakNode}, and {@link TabNode} cases that the legacy
|
|
532
|
+
* `importDOM` machinery in `@lexical/lexical` handled. Intended to be
|
|
533
|
+
* registered as a dependency of every editor that uses
|
|
534
|
+
* {@link DOMImportExtension}.
|
|
535
|
+
*
|
|
536
|
+
* @experimental
|
|
537
|
+
*/
|
|
538
|
+
export const CoreImportRules = [
|
|
539
|
+
IgnoreScriptStyleRule,
|
|
540
|
+
ParagraphRule,
|
|
541
|
+
TransparentBlockRule,
|
|
542
|
+
TextRule,
|
|
543
|
+
LineBreakRule,
|
|
544
|
+
InlineFormatRule,
|
|
545
|
+
];
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
import type {CompiledSelector, DOMImportFn, DOMImportRule} from './types';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Identity helper that infers a rule's matched node type and capture map
|
|
12
|
+
* from its `match` selector and threads them into the `$import` signature.
|
|
13
|
+
* Usage:
|
|
14
|
+
*
|
|
15
|
+
* ```ts
|
|
16
|
+
* defineImportRule({
|
|
17
|
+
* name: '@lexical/list/li',
|
|
18
|
+
* match: sel.tag('li'),
|
|
19
|
+
* $import: (ctx, el, $next) => {
|
|
20
|
+
* // el: HTMLLIElement
|
|
21
|
+
* return [$createListItemNode()];
|
|
22
|
+
* },
|
|
23
|
+
* });
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* @experimental
|
|
27
|
+
* @__NO_SIDE_EFFECTS__
|
|
28
|
+
*/
|
|
29
|
+
export function defineImportRule<const S extends CompiledSelector>(rule: {
|
|
30
|
+
readonly name?: string;
|
|
31
|
+
readonly match: S;
|
|
32
|
+
readonly $import: DOMImportFn<
|
|
33
|
+
S extends CompiledSelector<infer N, Record<string, RegExpMatchArray>>
|
|
34
|
+
? N
|
|
35
|
+
: Node,
|
|
36
|
+
S extends CompiledSelector<Node, infer C> ? C : Record<string, never>
|
|
37
|
+
>;
|
|
38
|
+
}): DOMImportRule<S> {
|
|
39
|
+
return rule as DOMImportRule<S>;
|
|
40
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
import type {AnyDOMImportRule} from './types';
|
|
9
|
+
|
|
10
|
+
import {type CompiledDispatch, compileImportRules} from './compileImportRules';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Opaque handle for a pre-compiled set of overlay rules. Produce one with
|
|
14
|
+
* {@link defineOverlayRules} and pass it to
|
|
15
|
+
* {@link DOMImportContext.$importChildren} via
|
|
16
|
+
* {@link ImportChildrenOpts.rules}.
|
|
17
|
+
*
|
|
18
|
+
* To merge two or more overlays into a single one, pass them (alongside
|
|
19
|
+
* raw {@link DOMImportRule}s if desired) to a fresh
|
|
20
|
+
* {@link defineOverlayRules} — earlier arguments are higher priority.
|
|
21
|
+
*
|
|
22
|
+
* The internal shape is intentionally not part of the public API: it's a
|
|
23
|
+
* compiled dispatch table tagged with `__type` so callers cannot pass a
|
|
24
|
+
* raw rule array where a compiled overlay is expected.
|
|
25
|
+
*
|
|
26
|
+
* @experimental
|
|
27
|
+
*/
|
|
28
|
+
export interface CompiledOverlayRules {
|
|
29
|
+
readonly __type: 'CompiledOverlayRules';
|
|
30
|
+
/** @internal */
|
|
31
|
+
readonly dispatch: CompiledDispatch;
|
|
32
|
+
/**
|
|
33
|
+
* @internal — flattened source rules retained so an overlay can be
|
|
34
|
+
* recompiled when it is passed to another {@link defineOverlayRules}
|
|
35
|
+
* call or as part of {@link DOMImportConfig.rules}.
|
|
36
|
+
*/
|
|
37
|
+
readonly rules: readonly AnyDOMImportRule[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* An entry accepted everywhere rules are configured (overlay
|
|
42
|
+
* definitions, {@link DOMImportConfig.rules}). Either a single
|
|
43
|
+
* {@link DOMImportRule} or a {@link CompiledOverlayRules} produced by
|
|
44
|
+
* a previous {@link defineOverlayRules} call — passing the latter
|
|
45
|
+
* inlines the overlay's rules at this position in priority order.
|
|
46
|
+
*
|
|
47
|
+
* @experimental
|
|
48
|
+
*/
|
|
49
|
+
export type DOMImportRuleEntry = AnyDOMImportRule | CompiledOverlayRules;
|
|
50
|
+
|
|
51
|
+
/** @internal */
|
|
52
|
+
export function flattenRuleEntries(
|
|
53
|
+
entries: readonly DOMImportRuleEntry[],
|
|
54
|
+
): AnyDOMImportRule[] {
|
|
55
|
+
const out: AnyDOMImportRule[] = [];
|
|
56
|
+
for (const entry of entries) {
|
|
57
|
+
if (isCompiledOverlayRules(entry)) {
|
|
58
|
+
for (const r of entry.rules) {
|
|
59
|
+
out.push(r);
|
|
60
|
+
}
|
|
61
|
+
} else {
|
|
62
|
+
out.push(entry);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function isCompiledOverlayRules(
|
|
69
|
+
entry: DOMImportRuleEntry,
|
|
70
|
+
): entry is CompiledOverlayRules {
|
|
71
|
+
return (
|
|
72
|
+
typeof entry === 'object' &&
|
|
73
|
+
entry !== null &&
|
|
74
|
+
'__type' in entry &&
|
|
75
|
+
entry.__type === 'CompiledOverlayRules'
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Pre-compile a set of {@link DOMImportRuleEntry}s into a
|
|
81
|
+
* {@link CompiledOverlayRules} handle that can be installed via
|
|
82
|
+
* `ctx.$importChildren(el, {rules: …})`.
|
|
83
|
+
*
|
|
84
|
+
* Entries can be raw {@link DOMImportRule}s or other
|
|
85
|
+
* {@link CompiledOverlayRules} (the latter are inlined at their
|
|
86
|
+
* position in priority order, so the same call composes any number of
|
|
87
|
+
* overlays). Earlier entries are higher priority.
|
|
88
|
+
*
|
|
89
|
+
* Overlay rules installed as a raw array would be re-compiled on every
|
|
90
|
+
* `$importChildren` call. For overlays that are reused (e.g. a GitHub
|
|
91
|
+
* code-table rule that wraps every matching table), call this once at
|
|
92
|
+
* module scope so the dispatch table is built up front.
|
|
93
|
+
*
|
|
94
|
+
* @experimental
|
|
95
|
+
*/
|
|
96
|
+
export function defineOverlayRules(
|
|
97
|
+
entries: readonly DOMImportRuleEntry[],
|
|
98
|
+
): CompiledOverlayRules {
|
|
99
|
+
const rules = flattenRuleEntries(entries);
|
|
100
|
+
return {
|
|
101
|
+
__type: 'CompiledOverlayRules',
|
|
102
|
+
dispatch: compileImportRules(rules),
|
|
103
|
+
rules,
|
|
104
|
+
};
|
|
105
|
+
}
|