@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.
Files changed (66) hide show
  1. package/{DOMRenderExtension.d.ts → dist/DOMRenderExtension.d.ts} +12 -1
  2. package/dist/DOMRenderRuntime.d.ts +51 -0
  3. package/dist/LexicalHtml.dev.js +3289 -0
  4. package/dist/LexicalHtml.dev.mjs +3242 -0
  5. package/{LexicalHtml.js.flow → dist/LexicalHtml.js.flow} +16 -16
  6. package/dist/LexicalHtml.mjs +57 -0
  7. package/dist/LexicalHtml.node.mjs +55 -0
  8. package/dist/LexicalHtml.prod.js +9 -0
  9. package/dist/LexicalHtml.prod.mjs +9 -0
  10. package/dist/RenderContext.d.ts +68 -0
  11. package/{compileDOMRenderConfigOverrides.d.ts → dist/compileDOMRenderConfigOverrides.d.ts} +1 -1
  12. package/{constants.d.ts → dist/constants.d.ts} +2 -0
  13. package/dist/domOverride.d.ts +23 -0
  14. package/dist/import/CoreImportExtension.d.ts +11 -0
  15. package/dist/import/DOMImportExtension.d.ts +82 -0
  16. package/dist/import/HorizontalRuleImportExtension.d.ts +28 -0
  17. package/dist/import/ImportContext.d.ts +208 -0
  18. package/dist/import/compileImportRules.d.ts +50 -0
  19. package/dist/import/coreImportRules.d.ts +25 -0
  20. package/dist/import/defineImportRule.d.ts +32 -0
  21. package/dist/import/defineOverlayRules.d.ts +66 -0
  22. package/dist/import/index.d.ts +38 -0
  23. package/dist/import/inlineStylesFromStyleSheets.d.ts +28 -0
  24. package/dist/import/parseCss.d.ts +18 -0
  25. package/dist/import/runImport.d.ts +19 -0
  26. package/dist/import/schemas.d.ts +106 -0
  27. package/dist/import/sel.d.ts +74 -0
  28. package/dist/import/types.d.ts +394 -0
  29. package/dist/index.d.ts +44 -0
  30. package/{types.d.ts → dist/types.d.ts} +96 -8
  31. package/package.json +33 -18
  32. package/src/ContextRecord.ts +243 -0
  33. package/src/DOMRenderExtension.ts +96 -0
  34. package/src/DOMRenderRuntime.ts +265 -0
  35. package/src/RenderContext.ts +168 -0
  36. package/src/compileDOMRenderConfigOverrides.ts +416 -0
  37. package/src/constants.ts +18 -0
  38. package/src/domOverride.ts +46 -0
  39. package/src/import/CoreImportExtension.ts +26 -0
  40. package/src/import/DOMImportExtension.ts +221 -0
  41. package/src/import/HorizontalRuleImportExtension.ts +52 -0
  42. package/src/import/ImportContext.ts +339 -0
  43. package/src/import/compileImportRules.ts +178 -0
  44. package/src/import/coreImportRules.ts +545 -0
  45. package/src/import/defineImportRule.ts +40 -0
  46. package/src/import/defineOverlayRules.ts +105 -0
  47. package/src/import/index.ts +97 -0
  48. package/src/import/inlineStylesFromStyleSheets.ts +104 -0
  49. package/src/import/parseCss.ts +219 -0
  50. package/src/import/runImport.ts +245 -0
  51. package/src/import/schemas.ts +280 -0
  52. package/src/import/sel.ts +314 -0
  53. package/src/import/types.ts +471 -0
  54. package/src/index.ts +561 -0
  55. package/src/types.ts +470 -0
  56. package/LexicalHtml.dev.js +0 -914
  57. package/LexicalHtml.dev.mjs +0 -900
  58. package/LexicalHtml.mjs +0 -24
  59. package/LexicalHtml.node.mjs +0 -22
  60. package/LexicalHtml.prod.js +0 -9
  61. package/LexicalHtml.prod.mjs +0 -9
  62. package/RenderContext.d.ts +0 -32
  63. package/domOverride.d.ts +0 -18
  64. package/index.d.ts +0 -32
  65. /package/{ContextRecord.d.ts → dist/ContextRecord.d.ts} +0 -0
  66. /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
+ }