Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

@lexical/html

Package Overview
Dependencies
Maintainers
6
Versions
588
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@lexical/html - npm Package Compare versions

Comparing version
0.44.1-nightly.20260519.0
to
0.45.0
+22
dist/compileDOMRenderConfigOverrides.d.ts
import { type EditorDOMRenderConfig, InitialEditorConfig, Klass, type LexicalNode } from 'lexical';
import { AnyDOMRenderMatch, DOMRenderConfig } from './types';
interface TypeRecord {
readonly klass: Klass<LexicalNode>;
readonly types: {
[NodeAndSubclasses in string]?: boolean;
};
}
type TypeTree = {
[NodeType in string]?: TypeRecord;
};
export declare function buildTypeTree(editorConfig: Pick<InitialEditorConfig, 'nodes'>): TypeTree;
type TypeRender<T> = {
[NodeType in string]?: T[];
};
type AnyRender<T> = readonly [(node: LexicalNode) => boolean, T] | readonly ['types', TypeRender<T>];
type PreEditorDOMRenderConfig = {
[K in keyof EditorDOMRenderConfig]: AnyRender<AnyDOMRenderMatch[K]>[];
};
export declare function precompileDOMRenderConfigOverrides(editorConfig: Pick<InitialEditorConfig, 'nodes'>, overrides: DOMRenderConfig['overrides']): PreEditorDOMRenderConfig;
export declare function compileDOMRenderConfigOverrides(editorConfig: Pick<InitialEditorConfig, 'nodes' | 'dom'>, { overrides }: Pick<DOMRenderConfig, 'overrides'>): EditorDOMRenderConfig;
export {};
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
export declare const DOMRenderExtensionName = "@lexical/html/DOM";
export declare const DOMRenderContextSymbol: unique symbol;
export declare const DOMImportExtensionName = "@lexical/html/DOMImport";
export declare const DOMImportContextSymbol: unique symbol;
export declare const ALWAYS_TRUE: () => true;
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import type { AnyContextConfigPairOrUpdater, AnyContextSymbol, ContextConfig, ContextConfigPair, ContextConfigUpdater, ContextRecord } from './types';
import { type LexicalEditor } from 'lexical';
type WithContext<Ctx extends AnyContextSymbol> = {
[K in Ctx]?: undefined | ContextRecord<Ctx>;
};
/**
* @experimental
*
* The LexicalEditor with context
*/
export type EditorContext = {
editor: LexicalEditor;
} & WithContext<AnyContextSymbol>;
/**
* @experimental
*
* @param contextRecord The ContextRecord
* @param cfg The configuration
* @returns The value or defaultValue of cfg
*/
export declare function getContextValue<Ctx extends AnyContextSymbol, V>(contextRecord: undefined | ContextRecord<Ctx>, cfg: ContextConfig<Ctx, V>): V;
/**
* @experimental
*
* Read and delete cfg from this layer of context
*
* @param contextRecord The ContextRecord
* @param cfg The configuration
* @returns The value of the configuration that was removed
*/
export declare function popOwnContextValue<Ctx extends AnyContextSymbol, V>(contextRecord: ContextRecord<Ctx>, cfg: ContextConfig<Ctx, V>): undefined | V;
/**
* @experimental
*
* Get the value without a default
*
* @param contextRecord The ContextRecord
* @param cfg The configuration
* @returns The current value in this context or `undefined` if not set
*/
export declare function getOwnContextValue<Ctx extends AnyContextSymbol, V>(contextRecord: ContextRecord<Ctx>, cfg: ContextConfig<Ctx, V>): undefined | V;
/**
* @experimental
*
* @param sym The symbol for this ContextRecord (e.g. DOMRenderContextSymbol)
* @param editor The editor
* @returns The current context or undefined
*/
export declare function getContextRecord<Ctx extends AnyContextSymbol>(sym: Ctx, editor: LexicalEditor): undefined | ContextRecord<Ctx>;
/**
* Construct a new context from a parent context and pairs
*
* @param pairs The pairs and updaters to build the context from
* @param parent The parent context
* @returns The new context
*/
export declare function contextFromPairs<Ctx extends AnyContextSymbol>(pairs: readonly AnyContextConfigPairOrUpdater<Ctx>[], parent: undefined | ContextRecord<Ctx>): undefined | ContextRecord<Ctx>;
/**
* Create a context config pair that sets a value in the render context.
* @experimental
*/
export declare function contextValue<Ctx extends AnyContextSymbol, V>(cfg: ContextConfig<Ctx, V>, value: V): ContextConfigPair<Ctx, V>;
/**
* Create a context config updater that transforms a value in the render context.
* @experimental
*/
export declare function contextUpdater<Ctx extends AnyContextSymbol, V>(cfg: ContextConfig<Ctx, V>, updater: (prev: V) => V): ContextConfigUpdater<Ctx, V>;
/**
* @internal
* @experimental
* @__NO_SIDE_EFFECTS__
*/
export declare function $withFullContext<Ctx extends AnyContextSymbol, T>(sym: Ctx, contextRecord: ContextRecord<Ctx>, f: () => T, editor?: LexicalEditor): T;
/**
* @internal
* @experimental
* @__NO_SIDE_EFFECTS__
*/
export declare function $withContext<Ctx extends AnyContextSymbol>(sym: Ctx, $defaults?: (editor: LexicalEditor) => undefined | ContextRecord<Ctx>): (cfg: readonly AnyContextConfigPairOrUpdater<Ctx>[], editor?: LexicalEditor) => (<T>(f: () => T) => T);
/**
* @experimental
* @internal
* @__NO_SIDE_EFFECTS__
*/
export declare function createContextState<Tag extends symbol, V>(tag: Tag, name: string, getDefaultValue: () => V, isEqual?: (a: V, b: V) => boolean): ContextConfig<Tag, V>;
export {};
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import type { DOMOverrideOptions, DOMRenderMatch, DOMRenderMatchConfig, NodeMatch } from './types';
import type { LexicalNode } from 'lexical';
/**
* A convenience function for type inference when constructing DOM overrides for
* use with {@link DOMRenderExtension}.
*
* The optional `options` argument controls *whether* the override is installed
* based only on render context — `disabledForEditor` gates residency in the
* editor's render pipeline (reconciliation), `disabledForSession` gates
* participation in a single export/generate session. See {@link DOMOverrideOptions}.
*
* @experimental
* @__NO_SIDE_EFFECTS__
*/
export declare function domOverride(nodes: '*', config: DOMRenderMatchConfig<LexicalNode>, options?: DOMOverrideOptions): DOMRenderMatch<LexicalNode>;
export declare function domOverride<T extends LexicalNode>(nodes: readonly NodeMatch<T>[], config: DOMRenderMatchConfig<T>, options?: DOMOverrideOptions): DOMRenderMatch<T>;
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import type { DOMRenderConfig, DOMRenderExtensionOutput } from './types';
import type { InitialEditorConfig } from 'lexical';
/** @internal The result returned from {@link DOMRenderExtension}'s `init`. */
interface DOMRenderInitResult {
/**
* The `nodes` and base `dom` captured from the editor config before `dom`
* is overwritten with the compiled config — the only fields the runtime
* needs to recompile.
*/
initialEditorConfig: Pick<InitialEditorConfig, 'nodes' | 'dom'>;
}
/**
* @experimental
*
* An extension that allows overriding the render and export behavior for an
* editor. This is highly experimental and subject to change from one version
* to the next.
**/
export declare const DOMRenderExtension: import("lexical").LexicalExtension<DOMRenderConfig, "@lexical/html/DOM", DOMRenderExtensionOutput, DOMRenderInitResult>;
export {};
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import type { AnyDOMRenderMatch, AnyRenderStateConfigPairOrUpdater, ContextRecord, DOMRenderRuntime, RenderStateConfig } from './types';
import type { EditorDOMRenderConfig, InitialEditorConfig, LexicalEditor } from 'lexical';
import { DOMRenderContextSymbol } from './constants';
type RenderContextRecord = ContextRecord<typeof DOMRenderContextSymbol>;
/**
* The mutable, writable editor-level context record. Reads of a render state
* during reconciliation (and as the base layer of a session) fall through to
* this record, and it is the layer the `disabledForEditor` predicates read.
*
* @internal
*/
export declare function createEditorContextRecord(contextDefaults: readonly AnyRenderStateConfigPairOrUpdater[]): RenderContextRecord;
/**
* Filter the configured overrides down to those that are resident in the
* editor's render config, removing any whose `disabledForEditor` predicate
* returns `true` for the given editor context.
*
* @internal
*/
export declare function filterEditorInstalled(overrides: readonly AnyDOMRenderMatch[], record: RenderContextRecord): AnyDOMRenderMatch[];
/**
* Per-editor runtime backing {@link DOMRenderExtension}'s conditional
* overrides and imperative editor context. See {@link DOMRenderRuntime}.
*
* @internal
*/
export declare class DOMRenderRuntimeImpl implements DOMRenderRuntime {
readonly editor: LexicalEditor;
/**
* The `nodes` and base `dom` captured at `init` (before `dom` was
* overwritten with the compiled config) — the clean base for every recompile.
*/
readonly initialEditorConfig: Pick<InitialEditorConfig, 'nodes' | 'dom'>;
readonly overrides: readonly AnyDOMRenderMatch[];
readonly editorContext: RenderContextRecord;
readonly hasSessionGates: boolean;
installed: readonly AnyDOMRenderMatch[];
/** Memoized session configs keyed by the set of session-disabled overrides. */
private readonly sessionCache;
constructor(editor: LexicalEditor, initialEditorConfig: Pick<InitialEditorConfig, 'nodes' | 'dom'>, overrides: readonly AnyDOMRenderMatch[], editorContext: RenderContextRecord);
setContextValue<V>(cfg: RenderStateConfig<V>, value: V): void;
getSessionConfig(): EditorDOMRenderConfig;
}
export {};
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import type { AnyDOMImportRule, DOMImportFn } from './types';
import { type Predicate } from './sel';
/** @internal */
export interface CompiledRule {
readonly name: string;
readonly predicate: Predicate;
readonly $import: DOMImportFn<Node, Record<string, RegExpMatchArray>>;
}
/** @internal */
export interface CompiledDispatch {
/** All rules in registration order. Index = registration order. */
readonly rules: readonly CompiledRule[];
/**
* For each (uppercased) HTML tag name, the ordered list of rule indices
* considered when dispatching that tag. Includes interleaved wildcard
* element rules so a single iteration handles both.
*/
readonly byTag: ReadonlyMap<string, readonly number[]>;
/** Indices of rules whose match has no tag restriction. */
readonly wildcardIndices: readonly number[];
/** Indices of rules whose match is `sel.text()`. */
readonly textIndices: readonly number[];
/** Indices of rules whose match is `sel.comment()`. */
readonly commentIndices: readonly number[];
}
/**
* Compile an ordered list of {@link DOMImportRule}s into the dispatch tables
* used by the import runtime. The rule at index 0 is the highest-priority
* (`mergeConfig` prepends partial.rules so later-merged extensions land
* first).
*
* @internal
*/
export declare function compileImportRules(rules: readonly AnyDOMImportRule[]): CompiledDispatch;
/**
* Look up the (already interleaved) rule indices relevant to `node`. Element
* nodes hit `byTag` (with wildcards merged in) or fall back to the wildcard
* bucket if no tag-specific rules exist; text and comment nodes use their
* own buckets.
*
* @internal
*/
export declare function getDispatchIndices(dispatch: CompiledDispatch, node: Node): readonly number[];
/**
* Bundles {@link CoreImportRules} into a {@link DOMImportExtension}-aware
* extension. Depend on this from your editor (directly or via richer
* extensions like `RichTextImportExtension`) to get the equivalent of the
* legacy core `importDOM` behavior for `<p>`, `<span>`, `<b>`,
* `<strong>`, `<em>`, `<i>`, `<code>`, `<mark>`, `<s>`, `<sub>`, `<sup>`,
* `<u>`, `<br>`, and `#text`.
*
* @experimental
*/
export declare const CoreImportExtension: import("lexical").LexicalExtension<import("lexical").ExtensionConfigBase, "@lexical/html/CoreImport", unknown, unknown>;
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import { type ElementFormatType } from 'lexical';
/**
* True if `value` is a non-empty {@link ElementFormatType} (matches one of
* the supported `text-align` / legacy `align`-attribute values).
*
* @internal
*/
export declare function isAlignmentValue(value: string): value is ElementFormatType;
/**
* Rules covering the {@link ParagraphNode}, {@link TextNode},
* {@link LineBreakNode}, and {@link TabNode} cases that the legacy
* `importDOM` machinery in `@lexical/lexical` handled. Intended to be
* registered as a dependency of every editor that uses
* {@link DOMImportExtension}.
*
* @experimental
*/
export declare const CoreImportRules: (import("./types").DOMImportRule<import("./types").ElementSelectorBuilder<HTMLElement | HTMLSpanElement, Record<string, never>>> | import("./types").DOMImportRule<import("./types").CompiledSelector<Text, Record<string, RegExpMatchArray>>>)[];
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import type { CompiledSelector, DOMImportFn, DOMImportRule } from './types';
/**
* Identity helper that infers a rule's matched node type and capture map
* from its `match` selector and threads them into the `$import` signature.
* Usage:
*
* ```ts
* defineImportRule({
* name: '@lexical/list/li',
* match: sel.tag('li'),
* $import: (ctx, el, $next) => {
* // el: HTMLLIElement
* return [$createListItemNode()];
* },
* });
* ```
*
* @experimental
* @__NO_SIDE_EFFECTS__
*/
export declare function defineImportRule<const S extends CompiledSelector>(rule: {
readonly name?: string;
readonly match: S;
readonly $import: DOMImportFn<S extends CompiledSelector<infer N, Record<string, RegExpMatchArray>> ? N : Node, S extends CompiledSelector<Node, infer C> ? C : Record<string, never>>;
}): DOMImportRule<S>;
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import type { AnyDOMImportRule } from './types';
import { type CompiledDispatch } from './compileImportRules';
/**
* Opaque handle for a pre-compiled set of overlay rules. Produce one with
* {@link defineOverlayRules} and pass it to
* {@link DOMImportContext.$importChildren} via
* {@link ImportChildrenOpts.rules}.
*
* To merge two or more overlays into a single one, pass them (alongside
* raw {@link DOMImportRule}s if desired) to a fresh
* {@link defineOverlayRules} — earlier arguments are higher priority.
*
* The internal shape is intentionally not part of the public API: it's a
* compiled dispatch table tagged with `__type` so callers cannot pass a
* raw rule array where a compiled overlay is expected.
*
* @experimental
*/
export interface CompiledOverlayRules {
readonly __type: 'CompiledOverlayRules';
/** @internal */
readonly dispatch: CompiledDispatch;
/**
* @internal — flattened source rules retained so an overlay can be
* recompiled when it is passed to another {@link defineOverlayRules}
* call or as part of {@link DOMImportConfig.rules}.
*/
readonly rules: readonly AnyDOMImportRule[];
}
/**
* An entry accepted everywhere rules are configured (overlay
* definitions, {@link DOMImportConfig.rules}). Either a single
* {@link DOMImportRule} or a {@link CompiledOverlayRules} produced by
* a previous {@link defineOverlayRules} call — passing the latter
* inlines the overlay's rules at this position in priority order.
*
* @experimental
*/
export type DOMImportRuleEntry = AnyDOMImportRule | CompiledOverlayRules;
/** @internal */
export declare function flattenRuleEntries(entries: readonly DOMImportRuleEntry[]): AnyDOMImportRule[];
/**
* Pre-compile a set of {@link DOMImportRuleEntry}s into a
* {@link CompiledOverlayRules} handle that can be installed via
* `ctx.$importChildren(el, {rules: …})`.
*
* Entries can be raw {@link DOMImportRule}s or other
* {@link CompiledOverlayRules} (the latter are inlined at their
* position in priority order, so the same call composes any number of
* overlays). Earlier entries are higher priority.
*
* Overlay rules installed as a raw array would be re-compiled on every
* `$importChildren` call. For overlays that are reused (e.g. a GitHub
* code-table rule that wraps every matching table), call this once at
* module scope so the dispatch table is built up front.
*
* @experimental
*/
export declare function defineOverlayRules(entries: readonly DOMImportRuleEntry[]): CompiledOverlayRules;
import type { DOMImportRuleEntry } from './defineOverlayRules';
import type { DOMImportExtensionOutput, DOMPreprocessFn, GenerateNodesFromDOMOptions, ImportContextPairOrUpdater } from './types';
import { type LexicalNode } from 'lexical';
/**
* Configuration for {@link DOMImportExtension}.
*
* @experimental
*/
export interface DOMImportConfig {
/**
* The set of rules contributed by this extension and its dependencies.
* Entries can be raw {@link DOMImportRule}s or a
* {@link CompiledOverlayRules} produced by {@link defineOverlayRules}
* (the latter is inlined in priority order — useful for libraries
* that already publish a compiled overlay).
*
* Rules are dispatched in priority order: rules contributed by
* extensions merged later (i.e. closer to the editor root) run first
* and may call `$next()` to delegate to lower-priority rules.
*
* `mergeConfig` prepends `partial.rules` to existing `rules`, so later
* configuration carries higher priority.
*/
readonly rules: readonly DOMImportRuleEntry[];
/**
* Default context pairs applied to every `$generateNodesFromDOM` call.
* Per-call overrides can be supplied via
* {@link GenerateNodesFromDOMOptions.context}.
*/
readonly contextDefaults: readonly ImportContextPairOrUpdater[];
/**
* Functions run in order on the DOM before walking begins, mutating in
* place. The default config registers
* {@link $inlineStylesFromStyleSheets} (resolves `<style>` rules to
* inline styles so the rules' style-driven matchers see them); apps
* append additional preprocessors (e.g. strip unsafe elements,
* normalize attributes, resolve relative URLs).
*
* `mergeConfig` appends, so each contributing extension's preprocessors
* run in dependency order. Per-call preprocessors registered via
* {@link GenerateNodesFromDOMOptions.preprocess} run AFTER these.
*/
readonly preprocess: readonly DOMPreprocessFn[];
}
/**
* Lowest-priority catch-all rule used as the default `config.rules` entry
* for {@link DOMImportExtension}: descends into the element's children
* and returns whatever they produced. With no other matching rule, an
* element vanishes and its contents are inserted in its place — the
* legacy `$createNodesFromDOM` hoisting behavior, but now expressed as a
* regular rule that apps can override (e.g. with a `sel.any()` rule that
* captures and discards unknown elements).
*
* @experimental
*/
export declare const DefaultHoistRule: import("./types").DOMImportRule<import("./types").ElementSelectorBuilder<HTMLElement, Record<string, never>>>;
/**
* @experimental
*
* Extension-based replacement for the legacy `importDOM` / `DOMConversion`
* machinery. Rules are contributed via configuration (see
* {@link DOMImportConfig.rules}), compiled into a tag-bucketed dispatcher at
* editor build time, and consumed via the extension's
* {@link DOMImportExtensionOutput.$generateNodesFromDOM} output.
*
* The legacy `$generateNodesFromDOM` continues to work in parallel; the
* intent is to migrate node packages over to this extension incrementally.
*/
export declare const DOMImportExtension: import("lexical").LexicalExtension<DOMImportConfig, "@lexical/html/DOMImport", DOMImportExtensionOutput, void>;
/**
* Look up the editor's {@link DOMImportExtension} and run its
* `$generateNodesFromDOM`. Designed as a drop-in replacement for the
* legacy `$generateNodesFromDOM(editor, dom)` signature so it can be
* supplied to `ClipboardImportExtension.$generateNodesFromDOM` (or any
* other consumer that wants to route through the extension pipeline).
*
* Throws if the editor was not built with {@link DOMImportExtension} as a
* dependency.
*
* @experimental
*/
export declare function $generateNodesFromDOMViaExtension(dom: Document | ParentNode, options?: GenerateNodesFromDOMOptions): LexicalNode[];
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
/**
* Import rules for {@link HorizontalRuleNode}.
*
* @experimental
*/
export declare const HorizontalRuleImportRules: import("./types").DOMImportRule<import("./types").ElementSelectorBuilder<HTMLHRElement, Record<string, never>>>[];
/**
* Bundles {@link HorizontalRuleImportRules} (plus
* {@link CoreImportExtension}) into a single dependency. The legacy
* {@link HorizontalRuleExtension.importDOM} continues to work in parallel;
* depend on this extension to opt into the new pipeline.
*
* Lives in `@lexical/html` (not `@lexical/extension`) because
* {@link DOMImportExtension} itself is in `@lexical/html`, and
* `@lexical/extension` is upstream of `@lexical/html` in the dependency
* graph — same arrangement as {@link CoreImportExtension}.
*
* @experimental
*/
export declare const HorizontalRuleImportExtension: import("lexical").LexicalExtension<import("lexical").ExtensionConfigBase, "@lexical/html/HorizontalRuleImport", unknown, unknown>;
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import type { ContextRecord } from '../types';
import type { CompiledOverlayRules } from './defineOverlayRules';
import type { ImportContextPairOrUpdater, ImportSession, ImportStateConfig } from './types';
import { type LexicalEditor } from 'lexical';
import { DOMImportContextSymbol } from '../constants';
type ImportContextRecord = ContextRecord<typeof DOMImportContextSymbol>;
/**
* Create an import context state. The phantom symbol prevents accidental
* use of a render-context state in an import context (and vice versa).
*
* Note: to support the value-or-updater pattern, `V` cannot be a function
* type; wrap it in an array or object if needed.
*
* `getDefaultValue` is called **once at state creation** and the result is
* shared between every session that reads the state without first writing
* a value. Defaults must therefore be immutable (primitives, frozen
* objects, or read-only arrays / records). If your state needs mutable
* per-session storage, lazily initialize it inside your rule (e.g.
* `if (!ctx.session.has(cfg)) ctx.session.set(cfg, new …())`).
*
* @experimental
* @__NO_SIDE_EFFECTS__
*/
export declare function createImportState<V>(name: string, getDefaultValue: () => V, isEqual?: (a: V, b: V) => boolean): ImportStateConfig<V>;
/**
* The kind of operation that produced this import. Lets rules adapt
* their behavior (e.g. preserve more whitespace on `'paste'`).
* Defaults to `'unknown'`. Apps that need a different vocabulary can
* define their own {@link ImportStateConfig} with whatever value type
* they want.
*
* @experimental
*/
export type ImportSourceKind = 'paste' | 'unknown';
/**
* Built-in import-context state identifying how this import was initiated.
* Callers of `$generateNodesFromDOM` should set it via the `context` option.
*
* @experimental
*/
export declare const ImportSource: ImportStateConfig<ImportSourceKind>;
/**
* Built-in import-context state holding the {@link DataTransfer} the
* import was sourced from, if any. `null` outside paste/drop flows.
*
* The clipboard import pipeline passes the original `DataTransfer`
* through to its per-MIME-type handler stack (see
* {@link ImportMimeTypeFunction}); handlers that route HTML through
* the {@link DOMImportExtension} pipeline should forward it into the
* walk via `context: [contextValue(ImportSourceDataTransfer,
* dataTransfer)]` so rules and preprocessors can call
* `ctx.get(ImportSourceDataTransfer)` to inspect companion MIME types
* (e.g. an `'application/rtf'` alternative or an attached
* `'application/x-officedrawing'` payload), the file list, or any
* custom drag-and-drop slot.
*
* Use sparingly: the safer pattern is to decide *which* MIME-type
* payload to walk in the clipboard handler stack and hand a finalized
* DOM to the rules; only fall back to peeking at `ImportSourceDataTransfer`
* when the source-detection signal genuinely lives in a companion
* slot.
*
* @experimental
*/
export declare const ImportSourceDataTransfer: ImportStateConfig<DataTransfer | null>;
/**
* Built-in import-context state holding the bit-packed
* {@link TextFormatType} formats that should apply to {@link TextNode}s
* produced during the current subtree. Used by inline-format wrappers
* (`<b>`, `<i>`, `<u>`, …) to propagate formatting through the context
* record instead of via the legacy `forChild` chain.
*
* @experimental
*/
export declare const ImportTextFormat: ImportStateConfig<number>;
/**
* Built-in import-context state holding a parsed CSS-style record
* (the {@link getStyleObjectFromCSS} shape) that should apply to
* {@link TextNode}s produced during the current subtree. Mirrors the
* format-bit propagation in {@link ImportTextFormat} for properties
* that don't fit into the format bit mask — `color`, `font-family`,
* `font-size`, etc.
*
* Ancestor rules that contribute a style branch the context with a
* merged record; the core `#text` rule materializes the non-empty
* record to a CSS string and calls `setStyle` on the new TextNode.
* Once TextNode adopts a parsed style record, the materialization
* step will go away.
*
* @experimental
*/
export declare const ImportTextStyle: ImportStateConfig<Readonly<Record<string, string>>>;
/**
* Determines whether a given DOM element should be treated as preserving
* whitespace (i.e. text content under it is not collapsed and is split on
* `\n` / `\t` into `LineBreakNode` / `TabNode`). The default matches the
* legacy behavior: the element itself is `<pre>` or its inline
* `white-space` style begins with `'pre'`.
*
* @experimental
*/
export type IsPreserveWhitespaceDom = (node: Node) => boolean;
/**
* Determines whether a given DOM node sits on the same visual line as its
* adjacent text siblings, governing whether leading/trailing whitespace in
* a `#text` is collapsed against neighbors. The default consults
* {@link isInlineDomNode} from `lexical` (style.display or a fixed inline
* tag-name set) and additionally treats elements with an explicit
* non-inline `display` style as block.
*
* @experimental
*/
export type IsInlineForWhitespace = (node: Node) => boolean;
/**
* Configuration for the core text whitespace-collapse logic. Override via
* {@link ImportWhitespaceConfig} either as a `contextDefaults` entry on
* the {@link DOMImportExtension} or per-call on `$generateNodesFromDOM`'s
* `context` option.
*
* @experimental
*/
export interface WhitespaceImportConfig {
/** See {@link IsPreserveWhitespaceDom}. */
readonly preservesWhitespace: IsPreserveWhitespaceDom;
/** See {@link IsInlineForWhitespace}. */
readonly isInline: IsInlineForWhitespace;
}
/**
* Default {@link WhitespaceImportConfig.preservesWhitespace}: matches
* `<pre>` and any element with `white-space: pre*`.
*
* @experimental
*/
export declare function defaultPreservesWhitespace(node: Node): boolean;
/**
* Default {@link WhitespaceImportConfig.isInline}: treats an element as
* inline iff its inline `display` style is `inline*` OR (no explicit
* non-inline display) its nodeName is a known inline tag (`isInlineDomNode`).
* Text nodes are always inline; comments and other non-elements are not.
*
* @experimental
*/
export declare function defaultIsInline(node: Node): boolean;
/**
* Built-in import-context state controlling text-node whitespace handling
* (collapse vs. preserve, what counts as an inline sibling). Override per
* editor via {@link DOMImportConfig.contextDefaults} or per call via
* {@link GenerateNodesFromDOMOptions.context}.
*
* @experimental
*/
export declare const ImportWhitespaceConfig: ImportStateConfig<WhitespaceImportConfig>;
/**
* Built-in session slot for runtime overlay rules that should be in
* effect for the entire walk. A preprocessor writes here when it wants
* to conditionally install handling for a particular paste source
* (e.g. "if the Microsoft Word generator meta tag is present, push the
* Word-paste overlay"). Each entry contributes an overlay dispatcher
* to the runtime's overlay stack; later array entries are higher
* priority. Use `ctx.session.update(ImportOverlays, prev => […])` to
* append.
*
* This is the walk-wide counterpart to
* `$importChildren({rules: …})` (which scopes an overlay to one
* subtree): write to {@link ImportOverlays} when the overlay should
* apply for the whole document; use `$importChildren`'s `rules` when
* the overlay should only apply for a deeper region.
*
* @experimental
*/
export declare const ImportOverlays: ImportStateConfig<readonly CompiledOverlayRules[]>;
/**
* The session IS the root-layer {@link ContextRecord} of the walk. Reads
* fall through the prototype chain to the editor's `contextDefaults`,
* writes mutate the record's own properties, and any branch pushed by
* `$importChildren({context})` sits above this layer and can shadow
* (but does not overwrite) slots.
*
* @internal
*/
export declare class ImportSessionImpl implements ImportSession {
readonly record: ImportContextRecord;
constructor(record: ImportContextRecord);
get<V>(cfg: ImportStateConfig<V>): V;
set<V>(cfg: ImportStateConfig<V>, value: V): void;
update<V>(cfg: ImportStateConfig<V>, updater: (prev: V) => V): void;
has<V>(cfg: ImportStateConfig<V>): boolean;
}
/**
* Read an import context value during an import operation.
* @experimental
*/
export declare function $getImportContextValue<V>(cfg: ImportStateConfig<V>, editor?: LexicalEditor): V;
/**
* Run `f` with the given context pairs applied on top of the editor's
* current import context.
*
* @experimental
*/
export declare const $withImportContext: (cfg: readonly ImportContextPairOrUpdater[], editor?: LexicalEditor) => <T>(f: () => T) => T;
export {};
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import { parseSelector } from './parseCss';
/**
* Combinator-and-parser-based builder for {@link CompiledSelector}s. The
* runtime shape returned by these factory methods is opaque; consumers
* should never inspect or construct selector objects directly.
*
* @experimental
*/
export declare const sel: {
readonly any: () => import("./types").ElementSelectorBuilder<HTMLElement>;
readonly comment: () => import("./types").CompiledSelector<Comment>;
/**
* Parse a reduced CSS-selector subset and return a builder you can chain
* combinator methods off of.
*/
readonly css: typeof parseSelector;
readonly tag: <const Tags extends readonly string[]>(...tags: Tags) => import("./types").ElementSelectorBuilder<Tags[number] extends keyof HTMLElementTagNameMap ? HTMLElementTagNameMap[Tags[number]] : HTMLElement>;
readonly text: () => import("./types").CompiledSelector<Text>;
};
export { CoreImportExtension } from './CoreImportExtension';
export { CoreImportRules } from './coreImportRules';
export { defineImportRule } from './defineImportRule';
export { type CompiledOverlayRules, defineOverlayRules, type DOMImportRuleEntry, } from './defineOverlayRules';
export { $generateNodesFromDOMViaExtension, type DOMImportConfig, DOMImportExtension, } from './DOMImportExtension';
export { HorizontalRuleImportExtension, HorizontalRuleImportRules, } from './HorizontalRuleImportExtension';
export { $getImportContextValue, $withImportContext, createImportState, defaultIsInline, defaultPreservesWhitespace, ImportOverlays, ImportSource, ImportSourceDataTransfer, type ImportSourceKind, ImportTextFormat, ImportTextStyle, ImportWhitespaceConfig, type IsInlineForWhitespace, type IsPreserveWhitespaceDom, type WhitespaceImportConfig, } from './ImportContext';
export { $inlineStylesFromStyleSheets } from './inlineStylesFromStyleSheets';
export { parseSelector } from './parseCss';
export { $distributeInlineWrapper, $isBlockLevel, BlockSchema, InlineSchema, NestedBlockSchema, RootSchema, } from './schemas';
export { isElementOfTag } from './sel';
export type { AnyDOMImportRule, AttrMatchOptions, CapturesOfSelector, ChildSchema, CompiledSelector, DOMImportContext, DOMImportExtensionOutput, DOMImportFn, DOMImportRule, DOMPreprocessContext, DOMPreprocessFn, ElementSelectorBuilder, GenerateNodesFromDOMOptions, ImportChildrenOpts, ImportContextPairOrUpdater, ImportNodeOpts, ImportSession, ImportStateConfig, NodeOfSelector, StyleMatchOptions, } from './types';
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import type { DOMPreprocessFn } from './types';
/**
* Inlines CSS rules from `<style>` tags onto matching elements as inline
* styles.
*
* Used by apps like Excel that generate HTML where styles live in
* class-based `<style>` rules (e.g. `.xl65 { background: #FFFF00; color:
* blue; }`) rather than inline styles. Since Lexical's import converters
* read inline styles, we resolve stylesheet rules into inline styles
* before conversion.
*
* Mutates the DOM in-place. Original inline styles always take
* precedence over stylesheet rules (matching CSS specificity behavior).
*
* No-op for {@link ParentNode}s that are not {@link Document}s — only a
* full document carries `styleSheets` we can iterate.
*
* @experimental
*/
export declare const $inlineStylesFromStyleSheets: DOMPreprocessFn;
export declare function $inlineStylesFromStyleSheetsDOM(dom: Document | ParentNode): void;
import type { ElementSelectorBuilder } from './types';
/**
* Parse a reduced CSS-selector subset and return a {@link CompiledSelector}.
* Supported:
* - Tag (`p`), wildcard (`*`).
* - Tag list (`h1, h2, h3`).
* - Class (`.foo`, `.foo.bar`).
* - ID (`#foo`).
* - Attribute presence (`[name]`).
* - Attribute equality (`[name="value"]`, `[name=value]`).
*
* Anything outside the subset (regex attribute, inline-style match,
* combinators, pseudo-classes) is intentionally rejected — chain combinator
* methods off the returned builder instead.
*
* @experimental
*/
export declare function parseSelector(source: string): ElementSelectorBuilder<HTMLElement>;
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import type { ImportSession } from './types';
import { type LexicalEditor, type LexicalNode } from 'lexical';
import { type CompiledDispatch } from './compileImportRules';
/**
* Top-level walker for a compiled dispatcher. Iterates the DOM children of
* `dom` (using the document body if a {@link Document} is passed) and
* applies `RootSchema` to the produced lexical nodes so runs of inlines are
* wrapped in paragraphs — same shape as the legacy `$generateNodesFromDOM`.
*
* @internal
*/
export declare function $runImport(dispatch: CompiledDispatch, editor: LexicalEditor, dom: Document | ParentNode, session: ImportSession): LexicalNode[];
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import type { ChildSchema } from './types';
import { type ElementNode, type LexicalNode } from 'lexical';
/**
* True if the node fills a block slot at the root or inside another
* block — covers both ElementNode-style blocks (paragraph, heading,
* quote) and block-level DecoratorNodes (HorizontalRuleNode,
* ImageNode-as-block, etc.). Used by {@link BlockSchema},
* {@link RootSchema}, and {@link NestedBlockSchema}.
*
* @experimental
*/
export declare function $isBlockLevel(node: LexicalNode): boolean;
/**
* Distribute an inline wrapper (`LinkNode`, `MarkNode`, …) across a
* heterogeneous run of children produced by `$importChildren`, lifting
* any block children to the top level while keeping the wrapper around
* the leaf inline content.
*
* Use from a rule whose DOM source is an inline element that the
* browser permitted to enclose block elements — the canonical case is
* `<a href="…"><h1>title</h1><div>body</div></a>`, which a link rule
* wants to surface as two block siblings (heading + paragraph), each
* with its own link wrapping the original inline content. Schemas
* can't express this because they reason about a parent's children
* only — they cannot lift the parent out of itself.
*
* For each top-level child:
* - **Inline children** are collected into runs; each run is wrapped
* in a single fresh wrapper (from `$makeWrapper()`).
* - **Block children** are descended into: their own children are
* recursively distributed with `$makeWrapper`, then re-attached so
* the block keeps its position at the top level.
*
* The returned list will contain a mix of blocks and wrapped inline
* runs. The enclosing schema (typically {@link BlockSchema}) will
* then package those inline wrappers into paragraphs as usual.
*
* @experimental
*/
export declare function $distributeInlineWrapper(children: readonly LexicalNode[], $makeWrapper: () => ElementNode): LexicalNode[];
/**
* Apply a {@link ChildSchema} to a flat list of children produced by
* `$importChildren`. Walks the list once, partitions into accepted vs.
* rejected runs, packages or drops rejected runs, then runs `$finalize`.
*
* @internal
*/
export declare function $applySchema(schema: ChildSchema, children: LexicalNode[], parent: LexicalNode | null, domParent: Node | null): LexicalNode[];
/**
* Default schema for block-level positions (root of the document, the body
* of a block element node). Accepts block lexical nodes; packages runs of
* inline children into fresh paragraph nodes.
*
* @experimental
*/
export declare const BlockSchema: ChildSchema;
/**
* Schema for inline-only positions (the body of an inline lexical node such
* as a link). Accepts non-block lexical nodes; runs of block children are
* dropped (`onReject: 'drop'` is the default).
*
* @experimental
*/
export declare const InlineSchema: ChildSchema;
/**
* Schema for nested block positions — the equivalent of the legacy
* `ArtificialNode__DO_NOT_USE` flow used when a block DOM element appears
* inside another block lexical ancestor. Accepts block nodes; runs of inline
* children are emitted with a line break between consecutive runs (instead
* of being wrapped in a paragraph, which would introduce an extra level of
* nesting).
*
* @experimental
*/
export declare const NestedBlockSchema: ChildSchema;
/**
* Schema for the topmost level of `$generateNodesFromDOM`. Identical to
* {@link BlockSchema}; aliased for clarity at the entry point and so it can
* be overridden separately in the future (e.g. to synthesize a `ListNode`
* around runs of orphan `ListItemNode`s).
*
* @experimental
*/
export declare const RootSchema: ChildSchema;
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import type { AttrMatchOptions, CompiledSelector, ElementSelectorBuilder } from './types';
/**
* @internal
*
* A predicate that may write into the per-invocation `captures` map. Returns
* `true` if the rule matches; `false` otherwise.
*/
export type Predicate = (node: Node, captures: Record<string, RegExpMatchArray>) => boolean;
/** @internal */
export type SelectorKind = 'element' | 'text' | 'comment';
/** @internal The runtime shape of a {@link CompiledSelector}. */
export interface SelectorImpl {
readonly kind: SelectorKind;
/**
* Uppercased tag names this selector is restricted to. Empty for wildcard
* element selectors and for text / comment selectors (dispatched by
* `kind`).
*/
readonly tags: ReadonlySet<string>;
/** Composed predicate run against a candidate node. */
readonly predicate: Predicate;
}
/** @internal */
export declare function getSelectorImpl(sel: CompiledSelector): SelectorImpl;
/**
* @internal
*
* Build a selector value from a tag set and a predicate list. Used by the
* combinator API and the CSS parser.
*/
export declare function buildSelector(tags: ReadonlySet<string>, predicates: readonly Predicate[]): ElementSelectorBuilder<HTMLElement>;
/** @internal */
export declare function buildClassAllPredicate(classes: readonly string[]): Predicate;
/** @internal */
export declare function buildClassAnyPredicate(classes: readonly string[]): Predicate;
/** @internal */
export declare function buildAttrPredicate(name: string, value: unknown, options?: AttrMatchOptions): Predicate;
/**
* Combinator API for building {@link CompiledSelector}s. The public
* `sel` is augmented from this in `./index.ts` (where the CSS parser is
* available without a circular import); consumers outside `@lexical/html`
* should always import the public `sel` from the package root.
*
* @internal
*/
export declare const selBase: {
/** Match any {@link HTMLElement}. */
readonly any: () => ElementSelectorBuilder<HTMLElement>;
/** Match DOM {@link Comment} nodes. */
readonly comment: () => CompiledSelector<Comment>;
/**
* Match by tag name(s). With one literal tag the element type is narrowed
* (e.g. `'a' → HTMLAnchorElement`); with multiple, it is the union of
* their `HTMLElementTagNameMap` entries.
*/
readonly tag: <const Tags extends readonly string[]>(...tags: Tags) => ElementSelectorBuilder<Tags[number] extends keyof HTMLElementTagNameMap ? HTMLElementTagNameMap[Tags[number]] : HTMLElement>;
/** Match DOM {@link Text} nodes. */
readonly text: () => CompiledSelector<Text>;
};
/**
* Cross-frame-safe replacement for `node instanceof HTMLXxxElement`. Returns
* true when `node` is an HTMLElement whose `nodeName` equals `tag` (compared
* case-insensitively).
*
* @experimental
*/
export declare function isElementOfTag<T extends keyof HTMLElementTagNameMap>(node: Node, tag: T): node is HTMLElementTagNameMap[T];
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import type { DOMImportContextSymbol } from '../constants';
import type { AnyContextConfigPairOrUpdater, ContextConfig, ContextRecord } from '../types';
import type { CompiledOverlayRules } from './defineOverlayRules';
import type { LexicalNode } from 'lexical';
/**
* Phantom-typed branding so consumers cannot construct or mutate a
* {@link CompiledSelector} directly; the only way to obtain one is via the
* {@link sel} builder or {@link parseSelector}. The actual runtime shape is
* an internal implementation detail (see `./sel`).
*
* @experimental
*/
export declare const NodeBrand: unique symbol;
/** @experimental */
export declare const CaptureBrand: unique symbol;
/**
* An opaque, compiled selector used as the `match` field of a
* {@link DOMImportRule}. The two phantom type parameters carry the matched
* Node subtype (`N`) and a record of named regex captures (`C`) so the
* importer body gets correctly-typed `ctx` and `node` arguments without
* casts.
*
* @experimental
*/
export interface CompiledSelector<N extends Node = Node, C extends Record<string, RegExpMatchArray> = Record<string, RegExpMatchArray>> {
readonly [NodeBrand]?: N;
readonly [CaptureBrand]?: C;
}
/**
* The Node subtype matched by a selector (e.g. `HTMLAnchorElement` for
* `sel.tag('a')`, `Text` for `sel.text()`).
*
* @experimental
*/
export type NodeOfSelector<S> = S extends CompiledSelector<infer N, Record<string, RegExpMatchArray>> ? N : Node;
/**
* The named-capture map for a selector.
*
* @experimental
*/
export type CapturesOfSelector<S> = S extends CompiledSelector<Node, infer C> ? C : Record<string, never>;
/**
* Options bag for {@link ElementSelectorBuilder.attr} when the value is a
* regex. Future options will be added here without breaking existing
* call-sites.
*
* @experimental
*/
export interface AttrMatchOptions<K extends string = string> {
/**
* If provided, the {@link RegExpMatchArray} from the successful match is
* stored on `ctx.captures[capture]` for the importer to consume — saving
* a second regex execution.
*/
readonly capture?: K;
}
/**
* Options bag for {@link ElementSelectorBuilder.styleAny} when the value is a
* regex. See {@link AttrMatchOptions} for capture semantics.
*
* @experimental
*/
export interface StyleMatchOptions<K extends string = string> {
readonly capture?: K;
}
/**
* Fluent builder for an element selector. The two type parameters carry the
* matched element type and the named-capture map; each call refines them.
*
* The builder itself implements {@link CompiledSelector} so it can be used
* directly as the `match` field of a rule — no `.build()` call needed.
*
* @experimental
*/
export interface ElementSelectorBuilder<E extends HTMLElement, C extends Record<string, RegExpMatchArray> = Record<string, never>> extends CompiledSelector<E, C> {
/** Require every listed class to be present on the element. */
classAll(...classes: readonly string[]): ElementSelectorBuilder<E, C>;
/** Require at least one of the listed classes to be present. */
classAny(...classes: readonly string[]): ElementSelectorBuilder<E, C>;
/** Require the attribute to be present (any value). */
attr(name: string, value: true): ElementSelectorBuilder<E, C>;
/** Require the attribute to equal the given string. */
attr(name: string, value: string): ElementSelectorBuilder<E, C>;
/**
* Require the attribute to match the given regex. With
* `{capture: 'name'}` the match result is exposed on
* `ctx.captures.name`.
*/
attr<const O extends AttrMatchOptions>(name: string, value: RegExp, options?: O): ElementSelectorBuilder<E, O extends {
capture: infer K;
} ? C & Record<K & string, RegExpMatchArray> : C>;
/** Require the inline-style declaration to equal `value`. */
styleAny(prop: string, value: string): ElementSelectorBuilder<E, C>;
/** Require the inline-style declaration to match `value`. */
styleAny<const O extends StyleMatchOptions>(prop: string, value: RegExp, options?: O): ElementSelectorBuilder<E, O extends {
capture: infer K;
} ? C & Record<K & string, RegExpMatchArray> : C>;
}
/**
* Argument to {@link DOMImportContext.branch} / `$importChildren({context})`
* — see {@link ContextConfigPair} / {@link ContextConfigUpdater}.
*
* @experimental
*/
export type ImportContextPairOrUpdater = AnyContextConfigPairOrUpdater<typeof DOMImportContextSymbol>;
/**
* A typed context-state key for the import pipeline. Create with
* {@link createImportState}.
*
* @experimental
*/
export type ImportStateConfig<V> = ContextConfig<typeof DOMImportContextSymbol, V>;
/**
* A mutable, document-order-shared store for the import pipeline. Lets a
* rule visited early in the document write information that rules visited
* later can read — e.g. parse `<style>` or `<meta>` and influence
* subsequent matching.
*
* Implemented as the root-layer {@link ContextRecord} of the import walk:
* `ctx.session.set(cfg, v)` mutates the slot on that root record, and
* every unshadowed `ctx.get(cfg)` read in any branch picks it up. A
* `$importChildren({context: [...]})` branch that explicitly writes the
* same slot shadows the session value for the duration of that branch.
*
* @experimental
*/
export interface ImportSession {
/** Read the current value, returning the config's default if unset. */
get<V>(cfg: ImportStateConfig<V>): V;
/** Write `value` into the slot. */
set<V>(cfg: ImportStateConfig<V>, value: V): void;
/** Read-modify-write. */
update<V>(cfg: ImportStateConfig<V>, updater: (prev: V) => V): void;
/** Returns `true` if the slot has been written since session creation. */
has<V>(cfg: ImportStateConfig<V>): boolean;
}
/**
* Context exposed to a rule's `$import` function. Mirrors the existing render
* context (see {@link RenderContext}) but is import-scoped.
*
* @experimental
*/
export interface DOMImportContext<C extends Record<string, RegExpMatchArray> = Record<string, never>> {
/** Captures from this rule's selector. Fresh per rule invocation. */
readonly captures: Readonly<C>;
/**
* Mutable, document-order-shared store. Use to make information from
* earlier-visited nodes available to later-visited ones (e.g. a
* `<style>` or `<meta>` at the top of the document influencing how
* later elements are interpreted). One {@link ImportSession} instance
* is created per top-level `$generateNodesFromDOM` call and is shared
* across all recursive `$importChildren` / `$importOne` invocations.
*/
readonly session: ImportSession;
/** Read a typed context value. */
get<V>(cfg: ImportStateConfig<V>): V;
/**
* Recursively import every child of `parent` and return the produced
* lexical nodes, optionally enforcing a {@link ChildSchema} and/or
* branching the import context for the duration of the call (via
* `opts.context`).
*/
$importChildren(parent: ParentNode, opts?: ImportChildrenOpts): LexicalNode[];
/**
* Recursively import a single DOM node.
*/
$importOne(node: Node, opts?: ImportNodeOpts): LexicalNode[];
}
/**
* Options accepted by {@link DOMImportContext.$importChildren}. The combination
* of `schema`, `$onChild`, and `$after` is sufficient to express every
* child-handling pattern in the legacy `forChild` / `after` / wrap-continuous
* machinery.
*
* @experimental
*/
export interface ImportChildrenOpts {
/**
* How to validate and (re)package produced children. Defaults to whichever
* schema the parent's importer passed; the top-level entry uses
* {@link BlockSchema}.
*/
readonly schema?: ChildSchema;
/**
* Called for each produced lexical child immediately after its rule
* returned, with the chance to substitute or drop it. Equivalent to the
* old `forChild` hook but scoped to one `$importChildren` call.
*/
readonly $onChild?: (child: LexicalNode) => LexicalNode | null | undefined;
/**
* Called once with the full child array after all DOM children have been
* recursively imported but before {@link ChildSchema.$packageRun} is
* applied. Equivalent to the old `after` hook.
*/
readonly $after?: (children: LexicalNode[]) => LexicalNode[];
/** Context overrides scoped to the children traversal. */
readonly context?: readonly ImportContextPairOrUpdater[];
/**
* Additional {@link DOMImportRule}s active only for this children
* traversal (and any nested `$importChildren` calls that don't push
* their own overlay). The overlay is checked BEFORE the main
* dispatcher, so its rules take precedence; calling `$next()` from an
* overlay rule falls through to the next overlay-or-main rule.
*
* Use this to scope cost-bearing rules to where they apply. For
* example, a GitHub code-table rule installs an overlay that
* unwraps `<tr>` / `<td>` inside the table, without paying that
* predicate cost on every other `<tr>` / `<td>` paste.
*
* The value must be produced by
* {@link defineOverlayRules}; this forces the dispatcher to be
* compiled once at module scope and reused across
* `$importChildren` calls, instead of being recompiled per invocation.
*/
readonly rules?: CompiledOverlayRules;
}
/** @experimental */
export interface ImportNodeOpts {
readonly context?: readonly ImportContextPairOrUpdater[];
}
/**
* A {@link ChildSchema} encodes which lexical nodes a parent accepts as
* children and how to package or reject the rest. The legacy
* `wrapContinuousInlines` / `ArtificialNode__DO_NOT_USE` logic is the
* `BlockSchema` and `NestedBlockSchema` cases of this primitive.
*
* A schema only controls how the *children* are assembled before being
* appended to the parent the calling rule already chose. It cannot
* change the parent itself. Cases where the parent's shape needs to
* change in response to its children — e.g. an inline `<a>` that
* encloses a block `<h1>`, which must be lifted so the heading takes
* the link's place and the link is redistributed onto the heading's
* inline contents — belong in the rule body, not the schema. See the
* "Lifting blocks out of an inline parent" section of the docs.
*
* @experimental
*/
export interface ChildSchema {
/** Optional name for debug output. */
readonly name?: string;
/**
* Returns `true` if `child` is a valid child of `parent` in this position.
*/
$accepts(child: LexicalNode, parent: LexicalNode | null): boolean;
/**
* Package a maximal run of non-accepted siblings into zero or more
* accepted nodes. The returned nodes replace the rejected run at the
* same position in the child list and are not re-checked against
* `$accepts` — the caller is trusted to return valid children. If
* omitted, or if it returns an empty array, {@link ChildSchema.onReject}
* is consulted instead.
*/
$packageRun?(rejected: LexicalNode[], parent: LexicalNode | null, domParent: Node | null): LexicalNode[];
/**
* Fallback strategy for a run of non-accepted children when
* `$packageRun` is missing or returns an empty array:
*
* - `'drop'` (default) — silently discards the run. Use when the
* schema is strict and rejected content is meaningless in this
* position (e.g. text between table rows).
* - `'hoist'` — emits the rejected nodes unchanged at the same
* position in the assembled child list. The caller's parent then
* receives them as-is, which is only useful if the calling rule
* intends to surface mixed content to *its* parent (a less common
* pattern; usually `$packageRun` should re-shape the run first).
*
* `'hoist'` does NOT lift the run all the way up out of the calling
* rule's parent — that requires the rule itself to detect the
* situation and emit a different parent structure (see the
* "Lifting blocks out of an inline parent" section).
*/
readonly onReject?: 'hoist' | 'drop';
/**
* Final pass over the assembled child list (after `$packageRun`). Returns
* the children to actually attach. Use to enforce structural invariants
* (e.g. drop empty runs, pad short table rows).
*/
$finalize?(children: LexicalNode[], parent: LexicalNode | null): LexicalNode[];
}
/**
* The middleware signature of an import rule. Call `$next()` to delegate to
* the next-matching rule for this node (returning its result, which may then
* be inspected or wrapped); return `[]` to drop the node.
*
* @experimental
*/
export type DOMImportFn<E extends Node, C extends Record<string, RegExpMatchArray> = Record<string, never>> = (ctx: DOMImportContext<C>, node: E, $next: () => readonly LexicalNode[]) => readonly LexicalNode[];
/**
* An importer for a DOM node, dispatched by `match` and implemented by
* `$import`.
*
* @experimental
*/
export interface DOMImportRule<S extends CompiledSelector = CompiledSelector> {
/**
* Optional identifier surfaced in dev-mode logs, error messages, and
* introspection devtools. Convention: `'@scope/package/rule-id'` for
* library rules.
*/
readonly name?: string;
/** A {@link CompiledSelector} produced by the {@link sel} builder. */
readonly match: S;
/** Middleware that converts the matched DOM node into lexical nodes. */
readonly $import: DOMImportFn<NodeOfSelector<S>, CapturesOfSelector<S>>;
}
/** @experimental */
export type AnyDOMImportRule = DOMImportRule<any>;
/**
* Context exposed to a {@link DOMPreprocessFn}. Lets the preprocessor:
*
* - Write to the per-import {@link ImportSession} (the same
* `ctx.session` rules see during the walk). Writes mutate the
* root-layer context record, so they are visible to every scoped
* `ctx.get(cfg)` read that hasn't been shadowed by a branch.
*
* @experimental
*/
export interface DOMPreprocessContext {
/**
* Document-order-shared store, same instance as `ctx.session` in
* rules. Use to pass info from the DOM preprocess phase (e.g.
* inspected `<meta>` tags, collected `<style>` text) to the
* importer rules.
*/
readonly session: ImportSession;
}
/**
* A middleware step in the DOM-preprocess chain. Runs before walking
* begins and may:
*
* - Mutate the input DOM in place (e.g. inline stylesheets, strip
* unsafe elements, normalize attributes).
* - Write to {@link DOMPreprocessContext.session} for rules to read
* (and for unshadowed scoped reads to pick up).
* - Call `$next()` to defer to the next-lower preprocessor in the
* stack; omit the call to short-circuit and skip the rest.
*
* The preprocess phase runs inside the same editor read / update
* context as the walk that follows, so a preprocess function may call
* `$`-prefixed Lexical APIs (e.g. `$getState`, `$getRoot`) as needed.
* The `$next` parameter is named with a `$` prefix to make that
* editor-context expectation visible to readers and lints.
*
* Append-style merge applies: an extension's preprocessors are appended
* to the existing stack, so later-registered preprocessors run first
* and may delegate to earlier (lower-priority) ones via `$next()`. Same
* convention as {@link ExportMimeTypeFunction} on the export side.
*
* @experimental
*/
export type DOMPreprocessFn = (dom: Document | ParentNode, ctx: DOMPreprocessContext, $next: () => void) => void;
/**
* Per-call options to the extension's `$generateNodesFromDOM`.
*
* @experimental
*/
export interface GenerateNodesFromDOMOptions {
/**
* Context pairs/updaters applied for the duration of this import only —
* use to communicate per-call info such as the {@link ImportSource}.
*/
readonly context?: readonly ImportContextPairOrUpdater[];
/**
* Additional preprocessors to run on this call only, on top of the
* extension's configured {@link DOMImportConfig.preprocess}. Per-call
* preprocessors run AFTER the configured ones.
*/
readonly preprocess?: readonly DOMPreprocessFn[];
}
/**
* Output of {@link DOMImportExtension}.
*
* @experimental
*/
export interface DOMImportExtensionOutput {
/**
* Convert a {@link Document} or {@link ParentNode} into lexical nodes,
* using the dispatcher compiled from this extension's configured
* {@link DOMImportRule}s.
*
* Must be called within an `editor.update()` or `editor.read()` because
* the importers may invoke `$create...` helpers.
*/
$generateNodesFromDOM(dom: Document | ParentNode, options?: GenerateNodesFromDOMOptions): LexicalNode[];
/** @internal */
readonly defaults: undefined | ContextRecord<typeof DOMImportContextSymbol>;
}
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import type { BaseSelection, LexicalEditor, LexicalNode } from 'lexical';
export { contextUpdater, contextValue } from './ContextRecord';
export { domOverride } from './domOverride';
export { DOMRenderExtension } from './DOMRenderExtension';
export type { AnyDOMImportRule, AttrMatchOptions, CapturesOfSelector, ChildSchema, CompiledOverlayRules, CompiledSelector, DOMImportContext, DOMImportExtensionOutput, DOMImportFn, DOMImportRule, DOMImportRuleEntry, DOMPreprocessContext, DOMPreprocessFn, ElementSelectorBuilder, GenerateNodesFromDOMOptions, ImportChildrenOpts, ImportContextPairOrUpdater, ImportNodeOpts, ImportSession, ImportStateConfig, NodeOfSelector, StyleMatchOptions, } from './import';
export { $distributeInlineWrapper, $generateNodesFromDOMViaExtension, $getImportContextValue, $inlineStylesFromStyleSheets, $isBlockLevel, $withImportContext, BlockSchema, CoreImportExtension, CoreImportRules, createImportState, defaultIsInline, defaultPreservesWhitespace, defineImportRule, defineOverlayRules, type DOMImportConfig, DOMImportExtension, HorizontalRuleImportExtension, HorizontalRuleImportRules, ImportOverlays, ImportSource, ImportSourceDataTransfer, type ImportSourceKind, ImportTextFormat, ImportTextStyle, ImportWhitespaceConfig, InlineSchema, isElementOfTag, type IsInlineForWhitespace, type IsPreserveWhitespaceDom, NestedBlockSchema, parseSelector, RootSchema, sel, type WhitespaceImportConfig, } from './import';
export { $getRenderContextValue, $getSessionDOMRenderConfig, $setRenderContextValue, $updateRenderContextValue, $withRenderContext, createRenderState, RenderContextExport, RenderContextRoot, } from './RenderContext';
export type { AnyDOMRenderMatch, AnyRenderStateConfig, AnyRenderStateConfigPairOrUpdater, ContextPairOrUpdater, DOMOverrideOptions, DOMRenderConfig, DOMRenderExtensionOutput, DOMRenderMatch, DOMRenderMatchConfig, NodeMatch, RenderContextReader, } from './types';
/**
* How you parse your html string to get a document is left up to you. In the browser you can use the native
* DOMParser API to generate a document (see clipboard.ts), but to use in a headless environment you can use JSDom
* or an equivalent library and pass in the document here.
*/
export declare function $generateNodesFromDOM(editor: LexicalEditor, dom: Document | ParentNode): Array<LexicalNode>;
/**
* Generate DOM nodes from the editor state into the given container element,
* using the editor's {@link EditorDOMRenderConfig}.
* @experimental
*/
export declare function $generateDOMFromNodes<T extends HTMLElement | DocumentFragment>(container: T, selection?: null | BaseSelection, editor?: LexicalEditor): T;
/**
* Generate DOM nodes from a root node into the given container element,
* including the root node itself. Uses the editor's {@link EditorDOMRenderConfig}.
* @experimental
*/
export declare function $generateDOMFromRoot<T extends HTMLElement | DocumentFragment>(container: T, root?: LexicalNode): T;
/**
* Generate an HTML string from the editor's current state (or `selection`
* if provided).
*
* Must be called inside an active editor scope — i.e. `editor.update(...)`,
* `editor.read(...)`, or `editor.getEditorState().read(callback, {editor})`.
* The legacy `editor.getEditorState().read(callback)` call (without the
* `{editor}` option) does not set an active editor and is not supported;
* `editor.read(...)` is the drop-in replacement.
*/
export declare function $generateHtmlFromNodes(editor: LexicalEditor, selection?: BaseSelection | null): string;

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
'use strict'
const LexicalHtml = process.env.NODE_ENV !== 'production' ? require('./LexicalHtml.dev.js') : require('./LexicalHtml.prod.js');
module.exports = LexicalHtml;

Sorry, the diff of this file is not supported yet

/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import * as modDev from './LexicalHtml.dev.mjs';
import * as modProd from './LexicalHtml.prod.mjs';
const mod = process.env.NODE_ENV !== 'production' ? modDev : modProd;
export const $distributeInlineWrapper = mod.$distributeInlineWrapper;
export const $generateDOMFromNodes = mod.$generateDOMFromNodes;
export const $generateDOMFromRoot = mod.$generateDOMFromRoot;
export const $generateHtmlFromNodes = mod.$generateHtmlFromNodes;
export const $generateNodesFromDOM = mod.$generateNodesFromDOM;
export const $generateNodesFromDOMViaExtension = mod.$generateNodesFromDOMViaExtension;
export const $getImportContextValue = mod.$getImportContextValue;
export const $getRenderContextValue = mod.$getRenderContextValue;
export const $getSessionDOMRenderConfig = mod.$getSessionDOMRenderConfig;
export const $inlineStylesFromStyleSheets = mod.$inlineStylesFromStyleSheets;
export const $isBlockLevel = mod.$isBlockLevel;
export const $setRenderContextValue = mod.$setRenderContextValue;
export const $updateRenderContextValue = mod.$updateRenderContextValue;
export const $withImportContext = mod.$withImportContext;
export const $withRenderContext = mod.$withRenderContext;
export const BlockSchema = mod.BlockSchema;
export const CoreImportExtension = mod.CoreImportExtension;
export const CoreImportRules = mod.CoreImportRules;
export const DOMImportExtension = mod.DOMImportExtension;
export const DOMRenderExtension = mod.DOMRenderExtension;
export const HorizontalRuleImportExtension = mod.HorizontalRuleImportExtension;
export const HorizontalRuleImportRules = mod.HorizontalRuleImportRules;
export const ImportOverlays = mod.ImportOverlays;
export const ImportSource = mod.ImportSource;
export const ImportSourceDataTransfer = mod.ImportSourceDataTransfer;
export const ImportTextFormat = mod.ImportTextFormat;
export const ImportTextStyle = mod.ImportTextStyle;
export const ImportWhitespaceConfig = mod.ImportWhitespaceConfig;
export const InlineSchema = mod.InlineSchema;
export const NestedBlockSchema = mod.NestedBlockSchema;
export const RenderContextExport = mod.RenderContextExport;
export const RenderContextRoot = mod.RenderContextRoot;
export const RootSchema = mod.RootSchema;
export const contextUpdater = mod.contextUpdater;
export const contextValue = mod.contextValue;
export const createImportState = mod.createImportState;
export const createRenderState = mod.createRenderState;
export const defaultIsInline = mod.defaultIsInline;
export const defaultPreservesWhitespace = mod.defaultPreservesWhitespace;
export const defineImportRule = mod.defineImportRule;
export const defineOverlayRules = mod.defineOverlayRules;
export const domOverride = mod.domOverride;
export const isElementOfTag = mod.isElementOfTag;
export const parseSelector = mod.parseSelector;
export const sel = mod.sel;
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
const mod = await (process.env.NODE_ENV !== 'production' ? import('./LexicalHtml.dev.mjs') : import('./LexicalHtml.prod.mjs'));
export const $distributeInlineWrapper = mod.$distributeInlineWrapper;
export const $generateDOMFromNodes = mod.$generateDOMFromNodes;
export const $generateDOMFromRoot = mod.$generateDOMFromRoot;
export const $generateHtmlFromNodes = mod.$generateHtmlFromNodes;
export const $generateNodesFromDOM = mod.$generateNodesFromDOM;
export const $generateNodesFromDOMViaExtension = mod.$generateNodesFromDOMViaExtension;
export const $getImportContextValue = mod.$getImportContextValue;
export const $getRenderContextValue = mod.$getRenderContextValue;
export const $getSessionDOMRenderConfig = mod.$getSessionDOMRenderConfig;
export const $inlineStylesFromStyleSheets = mod.$inlineStylesFromStyleSheets;
export const $isBlockLevel = mod.$isBlockLevel;
export const $setRenderContextValue = mod.$setRenderContextValue;
export const $updateRenderContextValue = mod.$updateRenderContextValue;
export const $withImportContext = mod.$withImportContext;
export const $withRenderContext = mod.$withRenderContext;
export const BlockSchema = mod.BlockSchema;
export const CoreImportExtension = mod.CoreImportExtension;
export const CoreImportRules = mod.CoreImportRules;
export const DOMImportExtension = mod.DOMImportExtension;
export const DOMRenderExtension = mod.DOMRenderExtension;
export const HorizontalRuleImportExtension = mod.HorizontalRuleImportExtension;
export const HorizontalRuleImportRules = mod.HorizontalRuleImportRules;
export const ImportOverlays = mod.ImportOverlays;
export const ImportSource = mod.ImportSource;
export const ImportSourceDataTransfer = mod.ImportSourceDataTransfer;
export const ImportTextFormat = mod.ImportTextFormat;
export const ImportTextStyle = mod.ImportTextStyle;
export const ImportWhitespaceConfig = mod.ImportWhitespaceConfig;
export const InlineSchema = mod.InlineSchema;
export const NestedBlockSchema = mod.NestedBlockSchema;
export const RenderContextExport = mod.RenderContextExport;
export const RenderContextRoot = mod.RenderContextRoot;
export const RootSchema = mod.RootSchema;
export const contextUpdater = mod.contextUpdater;
export const contextValue = mod.contextValue;
export const createImportState = mod.createImportState;
export const createRenderState = mod.createRenderState;
export const defaultIsInline = mod.defaultIsInline;
export const defaultPreservesWhitespace = mod.defaultPreservesWhitespace;
export const defineImportRule = mod.defineImportRule;
export const defineOverlayRules = mod.defineOverlayRules;
export const domOverride = mod.domOverride;
export const isElementOfTag = mod.isElementOfTag;
export const parseSelector = mod.parseSelector;
export const sel = mod.sel;
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
"use strict";var e=require("@lexical/selection"),t=require("lexical"),n=require("@lexical/utils"),o=require("@lexical/extension");function r(e,...t){const n=new URL("https://lexical.dev/docs/error"),o=new URLSearchParams;o.append("code",e);for(const e of t)o.append("v",e);throw n.search=o.toString(),Error(`Minified Lexical error #${e}; visit ${n.toString()} for the full message or use the non-minified dev environment for full errors and additional helpful warnings.`)}let s;function i(e,t){const{key:n}=t;return e&&n in e?e[n]:t.defaultValue}function c(e){return s&&s.editor===e?s:void 0}function l(e,t){const n=c(t);return n&&n[e]}function u(e,t){if("cfg"in t){const{cfg:n,updater:o}=t;return[n,o(i(e,n))]}return t}function a(e,t){let n=t;for(const o of e){const[e,r]=u(n,o),s=e.key;if(n===t&&i(n,e)===r)continue;const c=n===t||void 0===n?f(t):n;c[s]=r,n=c}return n}function f(e){return Object.create(e||null)}function d(e,t){return[e,t]}function p(e,n,o,r=t.$getEditor()){const i=s,l=c(r);try{return s={...l,editor:r,[e]:n},o()}finally{s=i}}function h(e,n=()=>{}){return(o,r=t.$getEditor())=>t=>{const s=c(r),i=s&&s[e],l=a(o,i||n(r));return l&&l!==i?p(e,l,t,r):t()}}function m(e,n,o,r){return Object.assign(t.createState(Symbol(n),{isEqual:r,parse:o}),{[e]:!0})}const g=(e,t,n)=>{x(e),n()};function x(e){if(!t.isDOMDocumentNode(e))return;const o=e;if(null===o.querySelector("style"))return;const r=new Map;function s(e){let t=r.get(e);if(void 0===t){t=new Set;for(let n=0;n<e.style.length;n++)t.add(e.style[n]);r.set(e,t)}return t}try{for(const e of Array.from(o.styleSheets)){let r;try{r=e.cssRules}catch(e){continue}for(const e of Array.from(r)){if(!n.objectKlassEquals(e,CSSStyleRule))continue;let r;try{r=o.querySelectorAll(e.selectorText)}catch(e){continue}for(const n of Array.from(r)){if(!t.isHTMLElement(n))continue;const o=s(n);for(let t=0;t<e.style.length;t++){const r=e.style[t];o.has(r)||n.style.setProperty(r,e.style.getPropertyValue(r),e.style.getPropertyPriority(r))}}}}}catch(e){}}const y="@lexical/html/DOM",$=Symbol.for("@lexical/html/DOMExportContext"),S="@lexical/html/DOMImport",E=Symbol.for("@lexical/html/DOMImportContext"),D=()=>!0;function C(e,t,n){return m($,e,t,n)}const I=C("root",Boolean),N=C("isExport",Boolean);function O(e){const t=o.getPeerDependencyFromEditor(e,y);return t?t.output.defaults:void 0}function v(e){const t=o.getPeerDependencyFromEditor(e,y);return t?t.output.runtime:void 0}function M(e=t.$getEditor()){const n=v(e);return n?n.getSessionConfig():t.$getEditorDOMRenderConfig(e)}const R=h($,O);function T(e){return t=>t instanceof e}function b(e,{nodes:t}){if("*"===t)return D;let n={};const o=[];for(const s of t)if("getType"in s){const t=s.getType();if(n){const o=e[t];void 0===o&&r(339,s.name,t),n=Object.assign(n,o.types)}o.push(T(s))}else n=void 0,o.push(s);return n||(1===o.length?o[0]:e=>{for(const t of o)if(t(e))return!0;return!1})}function k(e){return(t,n,o)=>e(t,o)}function w(e){return(t,n,o,r)=>e(t,n,r)}function L(e){return(t,n,o,r,s)=>e(t,n,o,s)}function A(e){return(t,n,o,r,s,i)=>e(t,n,o,r,i)}function _(e,t){return(n,o)=>{const r=()=>e(n,o),s=t(n);return s?s(n,r,o):r()}}function F(e,t){return(n,o,r)=>{const s=()=>e(n,o,r),i=t(n);return i?i(n,o,s,r):s()}}const P=F,H=w;function W(e,t){return(n,o,r,s)=>{const i=()=>e(n,o,r,s),c=t(n);return c?c(n,o,r,i,s):i()}}function B(e,t){return(n,o,r,s,i)=>{const c=()=>e(n,o,r,s,i),l=t(n);return l?l(n,o,r,s,c,i):c()}}function U(e,t){return(n,o,r,s)=>{e(n,o,r,s);const i=t(n);i&&i(n,o,r,s)}}function j(e,t,n,o,r){let s=n[t];for(const n of e[t])if("function"==typeof n[0]){const[e,t]=n;s=o(s,n=>e(n)&&t||void 0)}else{const e=n[1],t={};for(const n in e){const r=e[n];r&&(t[n]=r.reduce((e,t)=>o(e,()=>t),s))}s=o(s,e=>{const n=t[e.getType()];return n&&r(n)})}n[t]=s}function z(e,t,n,o){if(!o)return;const r=e[t];if("function"==typeof n)r.push([n,o]);else{const e=r[r.length-1];let t;e&&"types"===e[0]?t=e[1]:(t={},r.push(["types",t]));for(const e in n){const n=t[e]||[];t[e]=n,n.push(o)}}}function V(e){return"*"===e.nodes}function G(e,n){const r=function(e){const n={},{nodes:r}=o.getKnownTypesAndNodes(e);for(const e of r)n[e.getType()]={klass:e,types:{}};for(const e of Object.values(n))if(e){const o=e.klass.getType();for(let{klass:r}=e;t.$isLexicalNode(r.prototype);r=Object.getPrototypeOf(r)){const{ownNodeType:e}=t.getStaticNodeConfig(r),s=e&&n[e];s&&(s.types[o]=!0)}}return n}(e),s={$createDOM:[],$decorateDOM:[],$exportDOM:[],$extractWithChild:[],$getDOMSlot:[],$shouldExclude:[],$shouldInclude:[],$updateDOM:[]};for(const e of function(e){const n=[],o=[],r=[];for(const s of e)if(V(s))n.push(s);else if(Array.isArray(s.nodes))for(const e of s.nodes)t.$isLexicalNode(e.prototype)?r.push(1===s.nodes.length?s:{...s,nodes:[e]}):o.push(1===s.nodes.length?s:{...s,nodes:[e]});const s=new Map,i=e=>{let n=s.get(e);if(void 0===n){n=0;for(let o=e;t.$isLexicalNode(o.prototype);o=Object.getPrototypeOf(o))n++;s.set(e,n)}return n};return r.sort((e,t)=>i(e.nodes[0])-i(t.nodes[0])),[...r,...o,...n]}(n)){const t=b(r,e);for(const n in s){z(s,n,t,e[n])}}return s}function q(e){return e}function K(e,{overrides:n}){const o=G(e,n),r={...t.DEFAULT_EDITOR_DOM_CONFIG,...e.dom};return j(o,"$createDOM",r,_,k),j(o,"$exportDOM",r,_,k),j(o,"$extractWithChild",r,B,A),j(o,"$getDOMSlot",r,P,H),j(o,"$shouldExclude",r,F,w),j(o,"$shouldInclude",r,F,w),j(o,"$updateDOM",r,W,L),j(o,"$decorateDOM",r,U,q),r}function J(e){return{get:t=>i(e,t)}}function Q(e){const t=Object.create(null);return a(e,t)||t}function Y(e,t){const n=J(t);return e.filter(e=>!(e.disabledForEditor&&e.disabledForEditor(n)))}function Z(e){if("*"===e.nodes)return()=>!0;const n=e.nodes.map(e=>{const n=e;return t.$isLexicalNode(n.prototype)?e=>e instanceof n:e});return e=>n.some(t=>t(e))}class X{editor;initialEditorConfig;overrides;editorContext;hasSessionGates;installed;sessionCache=new Map;constructor(e,t,n,o){this.editor=e,this.initialEditorConfig=t,this.overrides=n,this.editorContext=o,this.installed=Y(n,o),this.hasSessionGates=n.some(e=>e.disabledForSession)}setContextValue(e,n){const o=this.installed;this.editorContext[e.key]=n;const r=Y(this.overrides,this.editorContext);if(function(e,t){if(e.length!==t.length)return!1;for(let n=0;n<e.length;n++)if(e[n]!==t[n])return!1;return!0}(o,r))return;const s=function(e,t){const n=new Set(e),o=new Set(t),r=[];for(const t of e)o.has(t)||r.push(t);for(const e of t)n.has(e)||r.push(e);return r}(o,r);this.installed=r,this.sessionCache.clear();const i=K(this.initialEditorConfig,{overrides:r});this.editor._config.dom=i;const c=function(e){const t=[];for(const n of e)(n.$createDOM||n.$getDOMSlot||n.$decorateDOM)&&t.push(Z(n));return 0===t.length?null:e=>t.some(t=>t(e))}(s);if(!c)return;const l=i.$updateDOM;i.$updateDOM=(e,t,n,o)=>!!c(e)||l(e,t,n,o),this.editor.update(t.$fullReconcile,{discrete:!0}),i.$updateDOM=l}getSessionConfig(){const e=this.editor._config.dom||t.DEFAULT_EDITOR_DOM_CONFIG;if(!this.hasSessionGates)return e;const n=J(l($,this.editor)||this.editorContext),o=[],r=[];if(this.installed.forEach((e,t)=>{e.disabledForSession&&e.disabledForSession(n)?o.push(String(t)):r.push(e)}),0===o.length)return e;const s=o.join(",");let i=this.sessionCache.get(s);return i||(i=K(this.initialEditorConfig,{overrides:r}),this.sessionCache.set(s,i)),i}}const ee=t.defineExtension({build(e,t,n){const{initialEditorConfig:o}=n.getInitResult(),r=Q(t.contextDefaults);return{defaults:r,runtime:new X(e,o,t.overrides,r)}},config:{contextDefaults:[],overrides:[]},html:{export:new Map([[t.RootNode,()=>{const e=document.createElement("div");return e.role="textbox",{element:e}}]])},init(e,t){const n={dom:e.dom,nodes:e.nodes},o=Q(t.contextDefaults),r=Y(t.overrides,o);return e.dom=K(e,{overrides:r}),{initialEditorConfig:n}},mergeConfig(e,n){const o=t.shallowMergeConfig(e,n);for(const t of["overrides","contextDefaults"])n[t]&&(o[t]=[...e[t],...n[t]]);return o},name:y}),te=Symbol.for("@lexical/html/SelectorImpl");function ne(e,n){const o={kind:"element",predicate:(s=n,0===s.length?t.isHTMLElement:1===s.length?s[0]:(e,t)=>{for(const n of s)if(!n(e,t))return!1;return!0}),tags:e};var s;const i=t=>ne(e,[...n,t]);return{[te]:o,attr:(e,t,n)=>i(se(e,t,n)),classAll:(...e)=>i(re(e)),classAny:(...e)=>i(function(e){const n=oe(e);if(0===n.length)return()=>!1;return e=>{if(!t.isHTMLElement(e))return!1;const o=e.classList;for(const e of n)if(o.contains(e))return!0;return!1}}(e)),styleAny:(e,n,o)=>i(function(e,n,o){if("string"==typeof n)return o=>t.isHTMLElement(o)&&o.style.getPropertyValue(e)===n;if(n instanceof RegExp){const r=o&&o.capture,s=n;return(n,o)=>{if(!t.isHTMLElement(n))return!1;const i=n.style.getPropertyValue(e);if(!i)return!1;const c=i.match(s);return null!==c&&(void 0!==r&&(o[r]=c),!0)}}r(362,JSON.stringify(e))}(e,n,o))}}function oe(e){const t=[];for(const n of e)n&&t.push(n);return t}function re(e){const n=oe(e);return 0===n.length?()=>!0:e=>{if(!t.isHTMLElement(e))return!1;const o=e.classList;for(const e of n)if(!o.contains(e))return!1;return!0}}function se(e,n,o){if(!0===n)return n=>t.isHTMLElement(n)&&n.hasAttribute(e);if("string"==typeof n)return o=>t.isHTMLElement(o)&&o.getAttribute(e)===n;if(n instanceof RegExp){const r=o&&o.capture,s=n;return(n,o)=>{if(!t.isHTMLElement(n))return!1;const i=n.getAttribute(e);if(null==i)return!1;const c=i.match(s);return null!==c&&(void 0!==r&&(o[r]=c),!0)}}r(361,JSON.stringify(e))}const ie={kind:"text",predicate:t.isDOMTextNode,tags:new Set},ce={[te]:ie},le={kind:"comment",predicate:e=>8===e.nodeType,tags:new Set},ue={[te]:le},ae={any:()=>ne(new Set,[]),comment:()=>ue,tag(...e){e.length>0||r(363);const t=new Set;for(const n of e)t.add(n.toUpperCase());return ne(t,[])},text:()=>ce};const fe=/[A-Za-z0-9_-]/;class de{constructor(e,t){this.source=e,this.pos=t}peek(e=0){return this.source[this.pos+e]||""}consume(){return this.source[this.pos++]||""}eof(){return this.pos>=this.source.length}skipWhitespace(){for(;!this.eof()&&/\s/.test(this.peek());)this.pos++}readIdent(){const e=this.pos;for(;!this.eof()&&fe.test(this.peek());)this.pos++;return this.source.slice(e,this.pos)}readQuoted(){const e=this.consume();this.assert('"'===e||"'"===e,"expected quote");const t=this.pos;for(;!this.eof()&&this.peek()!==e;)"\\"===this.peek()?this.pos+=2:this.pos++;this.assert(!this.eof(),"unterminated string");const n=this.source.slice(t,this.pos);return this.pos++,n.replace(/\\(.)/g,"$1")}assert(e,t){e||r(364,String(this.pos+1),t,this.source)}}function pe(e){const t=new Set,n=[],o=[];if(e.skipWhitespace(),"*"===e.peek())e.consume();else if(fe.test(e.peek())){const n=e.readIdent();n&&t.add(n.toUpperCase())}for(;!e.eof();){const t=e.peek();if("."===t){e.consume();const t=e.readIdent();e.assert(""!==t,'expected class name after "."'),o.push(t)}else if("#"===t){e.consume();const t=e.readIdent();e.assert(""!==t,'expected id after "#"'),n.push(se("id",t))}else{if("["!==t)break;{e.consume(),e.skipWhitespace();const t=e.readIdent();e.assert(""!==t,'expected attribute name after "["'),e.skipWhitespace();let o=!0;if("="===e.peek()){e.consume(),e.skipWhitespace();const t=e.peek();'"'===t||"'"===t?o=e.readQuoted():(o=e.readIdent(),e.assert(""!==o,"expected attribute value")),e.skipWhitespace()}e.assert("]"===e.peek(),'expected "]"'),e.consume(),n.push(se(t,o))}}}return o.length>0&&n.push(re(o)),{predicates:n,tags:t}}function he(e){const t=new de(e,0),n=[];for(;;){const e=pe(t);if(n.push(e),t.skipWhitespace(),t.eof())break;t.assert(","===t.peek(),'expected "," (selector lists are the only supported combinator)'),t.consume(),t.skipWhitespace()}if(1===n.length)return ne(n[0].tags,n[0].predicates);const o=new Set;for(const e of n)for(const t of e.tags)o.add(t);return ne(o,[(e,t)=>{for(const o of n){const n=e.nodeName;if(o.tags.size>0&&!o.tags.has(n))continue;let r=!0;for(const n of o.predicates)if(!n(e,t)){r=!1;break}if(r)return!0}return!1}])}function me(e){return e}function ge(e,t,n){return m(E,e,t,n)}const xe=ge("importSource",()=>"unknown"),ye=ge("importSourceDataTransfer",()=>null),$e=ge("textFormat",()=>0),Se=ge("textStyle",()=>({}));function Ee(e){if(!t.isHTMLElement(e))return!1;if("PRE"===e.nodeName)return!0;const n=e.style.whiteSpace;return"string"==typeof n&&n.startsWith("pre")}function De(e){if(t.isDOMTextNode(e))return!0;if(!t.isHTMLElement(e))return!1;const n=e.style.display;return n?n.startsWith("inline"):!t.isBlockDomNode(e)&&t.isInlineDomNode(e)}const Ce=ge("whitespaceConfig",()=>({isInline:De,preservesWhitespace:Ee})),Ie=ge("importOverlays",()=>[]);class Ne{constructor(e){this.record=e}get(e){return i(this.record,e)}set(e,t){this.record[e.key]=t}update(e,t){this.record[e.key]=t(i(this.record,e))}has(e){return Object.prototype.hasOwnProperty.call(this.record,e.key)}}function Oe(e){const t=o.getPeerDependencyFromEditor(e,S);return t?t.output.defaults:void 0}function ve(e,n=t.$getEditor()){return i(function(e){return l(E,e)||Oe(e)}(n),e)}const Me=h(E,Oe),Re=ae,Te=new Set(["center","end","justify","left","right","start"]);function be(e){return Te.has(e)}const ke={B:{fontWeight:"bold"},EM:{fontStyle:"italic"},I:{fontStyle:"italic"},S:{textDecoration:"line-through"},STRONG:{fontWeight:"bold"},SUB:{verticalAlign:"sub"},SUP:{verticalAlign:"super"},U:{textDecoration:"underline"}},we={CODE:t.IS_CODE,MARK:t.IS_HIGHLIGHT};const Le=new Set(["font-weight","font-style","text-decoration","vertical-align"]);const Ae={$import:(e,n)=>{const o=e.get($e),r=ke[n.nodeName],s=function(e){return{fontStyle:e.style.fontStyle,fontWeight:e.style.fontWeight,textDecoration:e.style.textDecoration,verticalAlign:e.style.verticalAlign}}(n),i=r?(c=r,{fontStyle:(l=s).fontStyle||c.fontStyle,fontWeight:l.fontWeight||c.fontWeight,textDecoration:l.textDecoration||c.textDecoration,verticalAlign:l.verticalAlign||c.verticalAlign}):s;var c,l;let u=(a=o,f=function(e){let n=0,o=0;const{fontWeight:r,fontStyle:s,textDecoration:i,verticalAlign:c}=e;if("700"===r||"bold"===r?n|=t.IS_BOLD:"normal"!==r&&"400"!==r||(o|=t.IS_BOLD),"italic"===s?n|=t.IS_ITALIC:"normal"===s&&(o|=t.IS_ITALIC),i){const e=i.split(" ");e.includes("underline")&&(n|=t.IS_UNDERLINE),e.includes("line-through")&&(n|=t.IS_STRIKETHROUGH),e.includes("none")&&(o|=t.IS_UNDERLINE|t.IS_STRIKETHROUGH)}return"sub"===c?(n|=t.IS_SUBSCRIPT,o|=t.IS_SUPERSCRIPT):"super"===c?(n|=t.IS_SUPERSCRIPT,o|=t.IS_SUBSCRIPT):"baseline"===c&&(o|=t.IS_SUBSCRIPT|t.IS_SUPERSCRIPT),{clear:o,set:n}}(i),a&~f.clear|f.set);var a,f;const p=we[n.nodeName];return p&&(u|=p),u===o?e.$importChildren(n):e.$importChildren(n,{context:[d($e,u)]})},match:Re.tag("b","strong","em","i","code","mark","s","sub","sup","u","span"),name:"@lexical/html/inline-format"};function _e(e,n,o){let r=e;for(;;){let e=null;for(;null===(e=n?r.nextSibling:r.previousSibling);){const e=r.parentNode;if(null===e)return null;r=e}if(r=e,!o.isInline(r))return null;let s=r;for(;null!==(s=n?r.firstChild:r.lastChild);)r=s;if(t.isDOMTextNode(r))return r;if("BR"===r.nodeName)return null}}function Fe(e,n){return 0!==n&&t.$isTextNode(e)?e.setFormat(n):e}function Pe(e,n){if(t.$isTextNode(e)){const t=function(e){let t="";for(const n in e)Le.has(n)||(t+=`${n}: ${e[n]}; `);return t.trimEnd()}(n);""!==t&&e.setStyle(t)}return e}const He={$import:(e,n)=>{const o=e.get($e),r=e.get(Se),s=e.get(Ce);if(function(e,t){let n=e.parentNode;for(;null!==n;){if(t.preservesWhitespace(n))return!0;n=n.parentNode}return!1}(n,s)){const e=t.$generateNodesFromRawText(n.textContent||"");for(const t of e)Fe(t,o),Pe(t,r);return e}const i=function(e,t){let n=(e.textContent||"").replace(/\r/g,"").replace(/[ \t\n]+/g," ");if(0===n.length)return"";if(" "===n[0]){let o=e,r=!0;for(;null!==o&&null!==(o=_e(o,!1,t));){const e=o.textContent||"";if(e.length>0){/[ \t\n]$/.test(e)&&(n=n.slice(1)),r=!1;break}}r&&(n=n.slice(1))}if(n.length>0&&" "===n[n.length-1]){let o=e,r=!0;for(;null!==o&&null!==(o=_e(o,!0,t));)if((o.textContent||"").replace(/^( |\t|\r?\n)+/,"").length>0){r=!1;break}r&&(n=n.slice(0,-1))}return n}(n,s);if(""===i)return[];const c=t.$createTextNode(i);return Fe(c,o),Pe(c,r),[c]},match:Re.text(),name:"@lexical/html/#text"},We={$import:()=>[],match:Re.tag("script","style"),name:"@lexical/html/script-style-ignore"},Be={$import:()=>[t.$createLineBreakNode()],match:Re.tag("br"),name:"@lexical/html/br"},Ue=[We,{$import:(e,n)=>{const o=t.$createParagraphNode();if(t.$setFormatFromDOM(o,n),t.setNodeIndentFromDOM(n,o),""===o.getFormatType()){const e=n.getAttribute("align");e&&be(e)&&o.setFormat(e)}return t.$setDirectionFromDOM(o,n),[o.splice(0,0,e.$importChildren(n))]},match:Re.tag("p"),name:"@lexical/html/p"},He,Be,Ae];function je(e,t){const n=[];let o=0,r=0;for(;o<e.length&&r<t.length;)e[o]<=t[r]?n.push(e[o++]):n.push(t[r++]);for(;o<e.length;)n.push(e[o++]);for(;r<t.length;)n.push(t[r++]);return n}function ze(e){const t=[],n=new Map,o=[],s=[],i=[],c=new Set;e.forEach((e,l)=>{const u=function(e){const t=e[te];return void 0===t&&r(360),t}(e.match),a=e.name||function(e,t){if("text"===e.kind)return`#text@${t}`;if("comment"===e.kind)return`#comment@${t}`;if(0===e.tags.size)return`*@${t}`;const n=Array.from(e.tags).join(",").toLowerCase();return`${n}@${t}`}(u,l);if(e.name&&c.add(e.name),t.push({$import:e.$import,name:a,predicate:u.predicate}),"text"===u.kind)s.push(l);else if("comment"===u.kind)i.push(l);else if(0===u.tags.size)o.push(l);else for(const e of u.tags){let t=n.get(e);t||(t=[],n.set(e,t)),t.push(l)}});const l=new Map;if(0===o.length)for(const[e,t]of n)l.set(e,t);else for(const[e,t]of n)l.set(e,je(t,o));return{byTag:l,commentIndices:i,rules:t,textIndices:s,wildcardIndices:o}}function Ve(e,n){return t.isDOMTextNode(n)?e.textIndices:8===n.nodeType?e.commentIndices:t.isHTMLElement(n)?e.byTag.get(n.nodeName)||e.wildcardIndices:Ge}const Ge=Object.freeze([]);function qe(e){const t=[];for(const n of e)if(Ke(n))for(const e of n.rules)t.push(e);else t.push(n);return t}function Ke(e){return"object"==typeof e&&null!==e&&"__type"in e&&"CompiledOverlayRules"===e.__type}function Je(e){return t.$isBlockElementNode(e)||t.$isDecoratorNode(e)&&!e.isInline()}function Qe(e,n,o){const r=t.$createParagraphNode();if(t.isHTMLElement(o)){const e=o.style.textAlign;be(e)&&r.setFormat(e)}return[r.splice(0,0,e)]}const Ye={$accepts:Je,$packageRun:Qe,name:"BlockSchema"},Ze={$accepts:e=>!Je(e),name:"InlineSchema"},Xe={$accepts:Je,$packageRun:e=>e,name:"NestedBlockSchema"},et={$accepts:Je,$packageRun:Qe,name:"RootSchema"},tt=Object.freeze({});function nt(e,t){const n={$importChildren:(t,n)=>function(e,t,n){const o=n&&n.rules?n.rules.dispatch:void 0;o&&e.overlays.push(o);try{const o=()=>ot(e,t,n);return n&&n.context?Me(n.context,e.editor)(o):o()}finally{o&&e.overlays.pop()}}(e,t,n),$importOne:(t,n)=>rt(e,t,n),captures:t,get:t=>ve(t,e.editor),session:e.session};return n}function ot(e,t,n){const o=n&&n.$onChild,r=[];for(const n of Array.from(t.childNodes)){const t=rt(e,n,void 0);for(const e of t){const t=o?o(e):e;null!=t&&r.push(t)}}const s=n&&n.$after?n.$after(r):r,i=n&&n.schema;return i?function(e,t,n,o){const r=[];let s=null;const i=()=>{if(null===s)return;const t=s;if(s=null,e.$packageRun){const s=e.$packageRun(t,n,o);if(s.length>0){for(const e of s)r.push(e);return}}if("hoist"===e.onReject)for(const e of t)r.push(e)};for(const o of t)e.$accepts(o,n)?(i(),r.push(o)):(null===s&&(s=[]),s.push(o));return i(),e.$finalize?e.$finalize(r,n):r}(i,s,null,t):s}function rt(e,t,n){const o=()=>function(e,t){const n=function(e,t){const n=[];for(let o=e.overlays.length-1;o>=0;o--){const r=e.overlays[o],s=Ve(r,t);s.length>0&&n.push({dispatch:r,indices:s})}const o=Ve(e.dispatch,t);o.length>0&&n.push({dispatch:e.dispatch,indices:o});return n}(e,t);if(0===n.length)return st(e,t);let o=0,r=0;const s=()=>{for(;o<n.length;){const{dispatch:i,indices:c}=n[o];for(;r<c.length;){const n=c[r++],o=i.rules[n],l={};if(o.predicate(t,l)){const n=nt(e,0===Object.keys(l).length?tt:l);try{return o.$import(n,t,s)}catch(e){throw e}}}o++,r=0}return st(e,t)};return s()}(e,t);return n&&n.context?Me(n.context,e.editor)(o):o()}function st(e,t){if(0===t.childNodes.length)return[];const n=[];for(const o of Array.from(t.childNodes)){const t=rt(e,o,void 0);for(const e of t)n.push(e)}return n}const it={$import:(e,t)=>e.$importChildren(t),match:ae.any(),name:"@lexical/html/default-hoist"},ct=t.defineExtension({build(e,n){const o=ze(qe(n.rules)),r=a(n.contextDefaults,void 0),s=n.preprocess;return{$generateNodesFromDOM:(n,i)=>{const c=i&&i.context?a(i.context,r):r,l=void 0!==c&&c!==r?c:Object.create(r||null),u=new Ne(l),f={session:u};return function(e,t,n){let o=e.length-1;const r=()=>{for(;o>=0;)return void(0,e[o--])(t,n,r)};r()}(i&&i.preprocess?[...s,...i.preprocess]:s,n,f),p(E,l,()=>function(e,n,o,r){return ot({dispatch:e,editor:n,overlays:r.get(Ie).map(e=>e.dispatch),session:r},t.isDOMDocumentNode(o)?o.body:o,{schema:et})}(o,e,n,u),e)},defaults:r}},config:{contextDefaults:[],preprocess:[g],rules:[it]},mergeConfig:(e,n)=>t.shallowMergeConfig(e,{...n,...n.contextDefaults&&{contextDefaults:[...e.contextDefaults,...n.contextDefaults]},...n.preprocess&&{preprocess:[...e.preprocess,...n.preprocess]},...n.rules&&{rules:[...n.rules,...e.rules]}}),name:S});const lt=t.defineExtension({dependencies:[t.configExtension(ct,{rules:Ue})],name:"@lexical/html/CoreImport"}),ut=[{$import:()=>[o.$createHorizontalRuleNode()],match:ae.tag("hr"),name:"@lexical/html/hr"}],at=t.defineExtension({dependencies:[lt,o.HorizontalRuleExtension,t.configExtension(ct,{rules:ut})],name:"@lexical/html/HorizontalRuleImport"}),ft={any:ae.any,comment:ae.comment,css:he,tag:ae.tag,text:ae.text},dt=new Set(["STYLE","SCRIPT"]);function pt(e,n=null,o=t.$getEditor()){return R([d(N,!0)],o)(()=>{const r=t.$getRoot(),s=M(o),i=e.append.bind(e);for(const e of r.getChildren())ht(o,e,i,n,s);return e})}function ht(n,o,r,s=null,i=t.$getEditorDOMRenderConfig(n)){let c=i.$shouldInclude(o,s,n);const l=i.$shouldExclude(o,s,n);let u=o;null!==s&&t.$isTextNode(o)&&(u=e.$sliceSelectedTextNodeContent(s,o,"clone"));const a=i.$exportDOM(u,n),{element:f,after:d,append:p,$getChildNodes:h}=a;if(!f)return!1;const m=document.createDocumentFragment(),g=h?h():t.$isElementNode(u)?u.getChildren():[],x=m.append.bind(m);for(const e of g){const t=ht(n,e,x,s,i);!c&&t&&i.$extractWithChild(o,e,s,"html",n)&&(c=!0)}if(c&&!l){if((t.isHTMLElement(f)||t.isDocumentFragment(f))&&(p?p(m):f.append(m)),r(f),d){const e=d.call(u,f);e&&(t.isDocumentFragment(f)?f.replaceChildren(e):f.replaceWith(e))}}else r(m);return c}function mt(e,n,o,r,s=new Map,i){const c=[];if(dt.has(e.nodeName))return c;let l=null;const u=function(e,t){const{nodeName:n}=e,o=t._htmlConversions.get(n.toLowerCase());let r=null;if(void 0!==o)for(const t of o){const n=t(e);null!==n&&(null===r||(r.priority||0)<=(n.priority||0))&&(r=n)}return null!==r?r.conversion:null}(e,n),a=u?u(e):null;let f=null;if(null!==a){f=a.after;const t=a.node;if(l=Array.isArray(t)?t[t.length-1]:t,null!==l){for(const[,e]of s)if(l=e(l,i),!l)break;l&&c.push(...Array.isArray(t)?t:[l])}null!=a.forChild&&s.set(e.nodeName,a.forChild)}const d=e.childNodes;let p=[];const h=(null==l||!t.$isRootOrShadowRoot(l))&&(null!=l&&t.$isBlockElementNode(l)||r);for(let e=0;e<d.length;e++)p.push(...mt(d[e],n,o,h,new Map(s),l));if(null!=f&&(p=f(p)),t.isBlockDomNode(e)&&(p=gt(e,p,h?()=>{const e=new t.ArtificialNode__DO_NOT_USE;return o.push(e),e}:t.$createParagraphNode)),null==l)if(p.length>0)for(const e of p)c.push(e);else t.isBlockDomNode(e)&&function(e){if(null==e.nextSibling||null==e.previousSibling)return!1;return t.isInlineDomNode(e.nextSibling)&&t.isInlineDomNode(e.previousSibling)}(e)&&c.push(t.$createLineBreakNode());else t.$isElementNode(l)&&l.append(...p);return c}function gt(e,n,o){const r=e.style.textAlign,s=[];let i=[];for(let e=0;e<n.length;e++){const c=n[e];if(t.$isBlockElementNode(c))r&&!c.getFormat()&&c.setFormat(r),s.push(c);else if(i.push(c),e===n.length-1||e<n.length-1&&t.$isBlockElementNode(n[e+1])){const e=o();e.setFormat(r),e.append(...i),s.push(e),i=[]}}return s}exports.$distributeInlineWrapper=function e(n,o){const r=[];let s=[];const i=()=>{0!==s.length&&(r.push(o().splice(0,0,s)),s=[])};for(const c of n)if(Je(c)){if(i(),t.$isElementNode(c)){const t=e(c.getChildren(),o);c.splice(0,c.getChildrenSize(),t)}r.push(c)}else s.push(c);return i(),r},exports.$generateDOMFromNodes=pt,exports.$generateDOMFromRoot=function(e,n=t.$getRoot()){const o=t.$getEditor();return R([d(N,!0),d(I,!0)],o)(()=>{const t=M(o),r=e.append.bind(e);return ht(o,n,r,null,t),e})},exports.$generateHtmlFromNodes=function(e,t=null){return("undefined"==typeof document||"undefined"==typeof window&&void 0===global.window)&&r(338),pt(document.createElement("div"),t,e).innerHTML},exports.$generateNodesFromDOM=function(e,n){x(n);const o=t.isDOMDocumentNode(n)?n.body.childNodes:n.childNodes,r=[],s=[];for(const t of o)if(!dt.has(t.nodeName)){const n=mt(t,e,s,!1);if(null!==n)for(const e of n)r.push(e)}return function(e){for(const n of e)n.getParent()&&n.getNextSibling()instanceof t.ArtificialNode__DO_NOT_USE&&n.insertAfter(t.$createLineBreakNode());for(const t of e){const e=t.getParent();e&&e.splice(t.getIndexWithinParent(),1,t.getChildren())}}(s),r},exports.$generateNodesFromDOMViaExtension=function(e,t){return o.$getExtensionOutput(ct).$generateNodesFromDOM(e,t)},exports.$getImportContextValue=ve,exports.$getRenderContextValue=function(e,n=t.$getEditor()){return i(function(e){return l($,e)||O(e)}(n),e)},exports.$getSessionDOMRenderConfig=M,exports.$inlineStylesFromStyleSheets=g,exports.$isBlockLevel=Je,exports.$setRenderContextValue=function(e,n,o=t.$getEditor()){const r=v(o);r&&r.setContextValue(e,n)},exports.$updateRenderContextValue=function(e,n,o=t.$getEditor()){const r=v(o);r&&r.setContextValue(e,n(i(r.editorContext,e)))},exports.$withImportContext=Me,exports.$withRenderContext=R,exports.BlockSchema=Ye,exports.CoreImportExtension=lt,exports.CoreImportRules=Ue,exports.DOMImportExtension=ct,exports.DOMRenderExtension=ee,exports.HorizontalRuleImportExtension=at,exports.HorizontalRuleImportRules=ut,exports.ImportOverlays=Ie,exports.ImportSource=xe,exports.ImportSourceDataTransfer=ye,exports.ImportTextFormat=$e,exports.ImportTextStyle=Se,exports.ImportWhitespaceConfig=Ce,exports.InlineSchema=Ze,exports.NestedBlockSchema=Xe,exports.RenderContextExport=N,exports.RenderContextRoot=I,exports.RootSchema=et,exports.contextUpdater=function(e,t){return{cfg:e,updater:t}},exports.contextValue=d,exports.createImportState=ge,exports.createRenderState=C,exports.defaultIsInline=De,exports.defaultPreservesWhitespace=Ee,exports.defineImportRule=me,exports.defineOverlayRules=function(e){const t=qe(e);return{__type:"CompiledOverlayRules",dispatch:ze(t),rules:t}},exports.domOverride=function(e,t,n){return{...t,...n,nodes:e}},exports.isElementOfTag=function(e,n){return t.isHTMLElement(e)&&e.nodeName===n.toUpperCase()},exports.parseSelector=he,exports.sel=ft;
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import{$sliceSelectedTextNodeContent as t}from"@lexical/selection";import{$getEditor as e,createState as n,isDOMDocumentNode as o,isHTMLElement as r,$getEditorDOMRenderConfig as s,DEFAULT_EDITOR_DOM_CONFIG as i,$isLexicalNode as c,getStaticNodeConfig as l,$fullReconcile as u,defineExtension as f,shallowMergeConfig as a,RootNode as p,isDOMTextNode as d,isBlockDomNode as h,isInlineDomNode as g,IS_HIGHLIGHT as m,IS_CODE as y,$generateNodesFromRawText as x,$createTextNode as S,$createLineBreakNode as $,$createParagraphNode as v,$setFormatFromDOM as b,setNodeIndentFromDOM as C,$setDirectionFromDOM as k,$isTextNode as O,IS_BOLD as w,IS_ITALIC as D,IS_UNDERLINE as M,IS_STRIKETHROUGH as A,IS_SUBSCRIPT as N,IS_SUPERSCRIPT as I,$isElementNode as E,$isBlockElementNode as W,$isDecoratorNode as R,configExtension as j,$getRoot as T,isDocumentFragment as F,$isRootOrShadowRoot as P,ArtificialNode__DO_NOT_USE as z}from"lexical";import{objectKlassEquals as _}from"@lexical/utils";import{getPeerDependencyFromEditor as L,getKnownTypesAndNodes as U,$getExtensionOutput as B,HorizontalRuleExtension as V,$createHorizontalRuleNode as q}from"@lexical/extension";function G(t,...e){const n=new URL("https://lexical.dev/docs/error"),o=new URLSearchParams;o.append("code",t);for(const t of e)o.append("v",t);throw n.search=o.toString(),Error(`Minified Lexical error #${t}; visit ${n.toString()} for the full message or use the non-minified dev environment for full errors and additional helpful warnings.`)}let H;function J(t,e){const{key:n}=e;return t&&n in t?t[n]:e.defaultValue}function Q(t){return H&&H.editor===t?H:void 0}function K(t,e){const n=Q(e);return n&&n[t]}function Y(t,e){if("cfg"in e){const{cfg:n,updater:o}=e;return[n,o(J(t,n))]}return e}function Z(t,e){let n=e;for(const o of t){const[t,r]=Y(n,o),s=t.key;if(n===e&&J(n,t)===r)continue;const i=n===e||void 0===n?X(e):n;i[s]=r,n=i}return n}function X(t){return Object.create(t||null)}function tt(t,e){return[t,e]}function et(t,e){return{cfg:t,updater:e}}function nt(t,n,o,r=e()){const s=H,i=Q(r);try{return H={...i,editor:r,[t]:n},o()}finally{H=s}}function ot(t,n=()=>{}){return(o,r=e())=>e=>{const s=Q(r),i=s&&s[t],c=Z(o,i||n(r));return c&&c!==i?nt(t,c,e,r):e()}}function rt(t,e,o,r){return Object.assign(n(Symbol(e),{isEqual:r,parse:o}),{[t]:!0})}const st=(t,e,n)=>{it(t),n()};function it(t){if(!o(t))return;const e=t;if(null===e.querySelector("style"))return;const n=new Map;function s(t){let e=n.get(t);if(void 0===e){e=new Set;for(let n=0;n<t.style.length;n++)e.add(t.style[n]);n.set(t,e)}return e}try{for(const t of Array.from(e.styleSheets)){let n;try{n=t.cssRules}catch(t){continue}for(const t of Array.from(n)){if(!_(t,CSSStyleRule))continue;let n;try{n=e.querySelectorAll(t.selectorText)}catch(t){continue}for(const e of Array.from(n)){if(!r(e))continue;const n=s(e);for(let o=0;o<t.style.length;o++){const r=t.style[o];n.has(r)||e.style.setProperty(r,t.style.getPropertyValue(r),t.style.getPropertyPriority(r))}}}}}catch(t){}}const ct="@lexical/html/DOM",lt=Symbol.for("@lexical/html/DOMExportContext"),ut="@lexical/html/DOMImport",ft=Symbol.for("@lexical/html/DOMImportContext"),at=()=>!0;function pt(t,e,n){return rt(lt,t,e,n)}const dt=pt("root",Boolean),ht=pt("isExport",Boolean);function gt(t){const e=L(t,ct);return e?e.output.defaults:void 0}function mt(t,n=e()){return J(function(t){return K(lt,t)||gt(t)}(n),t)}function yt(t){const e=L(t,ct);return e?e.output.runtime:void 0}function xt(t,n,o=e()){const r=yt(o);r&&r.setContextValue(t,n)}function St(t,n,o=e()){const r=yt(o);r&&r.setContextValue(t,n(J(r.editorContext,t)))}function $t(t=e()){const n=yt(t);return n?n.getSessionConfig():s(t)}const vt=ot(lt,gt);function bt(t,e,n){return{...e,...n,nodes:t}}function Ct(t){return e=>e instanceof t}function kt(t,{nodes:e}){if("*"===e)return at;let n={};const o=[];for(const r of e)if("getType"in r){const e=r.getType();if(n){const o=t[e];void 0===o&&G(339,r.name,e),n=Object.assign(n,o.types)}o.push(Ct(r))}else n=void 0,o.push(r);return n||(1===o.length?o[0]:t=>{for(const e of o)if(e(t))return!0;return!1})}function Ot(t){return(e,n,o)=>t(e,o)}function wt(t){return(e,n,o,r)=>t(e,n,r)}function Dt(t){return(e,n,o,r,s)=>t(e,n,o,s)}function Mt(t){return(e,n,o,r,s,i)=>t(e,n,o,r,i)}function At(t,e){return(n,o)=>{const r=()=>t(n,o),s=e(n);return s?s(n,r,o):r()}}function Nt(t,e){return(n,o,r)=>{const s=()=>t(n,o,r),i=e(n);return i?i(n,o,s,r):s()}}const It=Nt,Et=wt;function Wt(t,e){return(n,o,r,s)=>{const i=()=>t(n,o,r,s),c=e(n);return c?c(n,o,r,i,s):i()}}function Rt(t,e){return(n,o,r,s,i)=>{const c=()=>t(n,o,r,s,i),l=e(n);return l?l(n,o,r,s,c,i):c()}}function jt(t,e){return(n,o,r,s)=>{t(n,o,r,s);const i=e(n);i&&i(n,o,r,s)}}function Tt(t,e,n,o,r){let s=n[e];for(const n of t[e])if("function"==typeof n[0]){const[t,e]=n;s=o(s,n=>t(n)&&e||void 0)}else{const t=n[1],e={};for(const n in t){const r=t[n];r&&(e[n]=r.reduce((t,e)=>o(t,()=>e),s))}s=o(s,t=>{const n=e[t.getType()];return n&&r(n)})}n[e]=s}function Ft(t,e,n,o){if(!o)return;const r=t[e];if("function"==typeof n)r.push([n,o]);else{const t=r[r.length-1];let e;t&&"types"===t[0]?e=t[1]:(e={},r.push(["types",e]));for(const t in n){const n=e[t]||[];e[t]=n,n.push(o)}}}function Pt(t){return"*"===t.nodes}function zt(t,e){const n=function(t){const e={},{nodes:n}=U(t);for(const t of n)e[t.getType()]={klass:t,types:{}};for(const t of Object.values(e))if(t){const n=t.klass.getType();for(let{klass:o}=t;c(o.prototype);o=Object.getPrototypeOf(o)){const{ownNodeType:t}=l(o),r=t&&e[t];r&&(r.types[n]=!0)}}return e}(t),o={$createDOM:[],$decorateDOM:[],$exportDOM:[],$extractWithChild:[],$getDOMSlot:[],$shouldExclude:[],$shouldInclude:[],$updateDOM:[]};for(const t of function(t){const e=[],n=[],o=[];for(const r of t)if(Pt(r))e.push(r);else if(Array.isArray(r.nodes))for(const t of r.nodes)c(t.prototype)?o.push(1===r.nodes.length?r:{...r,nodes:[t]}):n.push(1===r.nodes.length?r:{...r,nodes:[t]});const r=new Map,s=t=>{let e=r.get(t);if(void 0===e){e=0;for(let n=t;c(n.prototype);n=Object.getPrototypeOf(n))e++;r.set(t,e)}return e};return o.sort((t,e)=>s(t.nodes[0])-s(e.nodes[0])),[...o,...n,...e]}(e)){const e=kt(n,t);for(const n in o){Ft(o,n,e,t[n])}}return o}function _t(t){return t}function Lt(t,{overrides:e}){const n=zt(t,e),o={...i,...t.dom};return Tt(n,"$createDOM",o,At,Ot),Tt(n,"$exportDOM",o,At,Ot),Tt(n,"$extractWithChild",o,Rt,Mt),Tt(n,"$getDOMSlot",o,It,Et),Tt(n,"$shouldExclude",o,Nt,wt),Tt(n,"$shouldInclude",o,Nt,wt),Tt(n,"$updateDOM",o,Wt,Dt),Tt(n,"$decorateDOM",o,jt,_t),o}function Ut(t){return{get:e=>J(t,e)}}function Bt(t){const e=Object.create(null);return Z(t,e)||e}function Vt(t,e){const n=Ut(e);return t.filter(t=>!(t.disabledForEditor&&t.disabledForEditor(n)))}function qt(t){if("*"===t.nodes)return()=>!0;const e=t.nodes.map(t=>{const e=t;return c(e.prototype)?t=>t instanceof e:t});return t=>e.some(e=>e(t))}class Gt{editor;initialEditorConfig;overrides;editorContext;hasSessionGates;installed;sessionCache=new Map;constructor(t,e,n,o){this.editor=t,this.initialEditorConfig=e,this.overrides=n,this.editorContext=o,this.installed=Vt(n,o),this.hasSessionGates=n.some(t=>t.disabledForSession)}setContextValue(t,e){const n=this.installed;this.editorContext[t.key]=e;const o=Vt(this.overrides,this.editorContext);if(function(t,e){if(t.length!==e.length)return!1;for(let n=0;n<t.length;n++)if(t[n]!==e[n])return!1;return!0}(n,o))return;const r=function(t,e){const n=new Set(t),o=new Set(e),r=[];for(const e of t)o.has(e)||r.push(e);for(const t of e)n.has(t)||r.push(t);return r}(n,o);this.installed=o,this.sessionCache.clear();const s=Lt(this.initialEditorConfig,{overrides:o});this.editor._config.dom=s;const i=function(t){const e=[];for(const n of t)(n.$createDOM||n.$getDOMSlot||n.$decorateDOM)&&e.push(qt(n));return 0===e.length?null:t=>e.some(e=>e(t))}(r);if(!i)return;const c=s.$updateDOM;s.$updateDOM=(t,e,n,o)=>!!i(t)||c(t,e,n,o),this.editor.update(u,{discrete:!0}),s.$updateDOM=c}getSessionConfig(){const t=this.editor._config.dom||i;if(!this.hasSessionGates)return t;const e=Ut(K(lt,this.editor)||this.editorContext),n=[],o=[];if(this.installed.forEach((t,r)=>{t.disabledForSession&&t.disabledForSession(e)?n.push(String(r)):o.push(t)}),0===n.length)return t;const r=n.join(",");let s=this.sessionCache.get(r);return s||(s=Lt(this.initialEditorConfig,{overrides:o}),this.sessionCache.set(r,s)),s}}const Ht=f({build(t,e,n){const{initialEditorConfig:o}=n.getInitResult(),r=Bt(e.contextDefaults);return{defaults:r,runtime:new Gt(t,o,e.overrides,r)}},config:{contextDefaults:[],overrides:[]},html:{export:new Map([[p,()=>{const t=document.createElement("div");return t.role="textbox",{element:t}}]])},init(t,e){const n={dom:t.dom,nodes:t.nodes},o=Bt(e.contextDefaults),r=Vt(e.overrides,o);return t.dom=Lt(t,{overrides:r}),{initialEditorConfig:n}},mergeConfig(t,e){const n=a(t,e);for(const o of["overrides","contextDefaults"])e[o]&&(n[o]=[...t[o],...e[o]]);return n},name:ct}),Jt=Symbol.for("@lexical/html/SelectorImpl");function Qt(t,e){const n={kind:"element",predicate:(o=e,0===o.length?r:1===o.length?o[0]:(t,e)=>{for(const n of o)if(!n(t,e))return!1;return!0}),tags:t};var o;const s=n=>Qt(t,[...e,n]);return{[Jt]:n,attr:(t,e,n)=>s(Zt(t,e,n)),classAll:(...t)=>s(Yt(t)),classAny:(...t)=>s(function(t){const e=Kt(t);if(0===e.length)return()=>!1;return t=>{if(!r(t))return!1;const n=t.classList;for(const t of e)if(n.contains(t))return!0;return!1}}(t)),styleAny:(t,e,n)=>s(function(t,e,n){if("string"==typeof e)return n=>r(n)&&n.style.getPropertyValue(t)===e;if(e instanceof RegExp){const o=n&&n.capture,s=e;return(e,n)=>{if(!r(e))return!1;const i=e.style.getPropertyValue(t);if(!i)return!1;const c=i.match(s);return null!==c&&(void 0!==o&&(n[o]=c),!0)}}G(362,JSON.stringify(t))}(t,e,n))}}function Kt(t){const e=[];for(const n of t)n&&e.push(n);return e}function Yt(t){const e=Kt(t);return 0===e.length?()=>!0:t=>{if(!r(t))return!1;const n=t.classList;for(const t of e)if(!n.contains(t))return!1;return!0}}function Zt(t,e,n){if(!0===e)return e=>r(e)&&e.hasAttribute(t);if("string"==typeof e)return n=>r(n)&&n.getAttribute(t)===e;if(e instanceof RegExp){const o=n&&n.capture,s=e;return(e,n)=>{if(!r(e))return!1;const i=e.getAttribute(t);if(null==i)return!1;const c=i.match(s);return null!==c&&(void 0!==o&&(n[o]=c),!0)}}G(361,JSON.stringify(t))}const Xt={kind:"text",predicate:d,tags:new Set},te={[Jt]:Xt},ee={kind:"comment",predicate:t=>8===t.nodeType,tags:new Set},ne={[Jt]:ee},oe={any:()=>Qt(new Set,[]),comment:()=>ne,tag(...t){t.length>0||G(363);const e=new Set;for(const n of t)e.add(n.toUpperCase());return Qt(e,[])},text:()=>te};function re(t,e){return r(t)&&t.nodeName===e.toUpperCase()}const se=/[A-Za-z0-9_-]/;class ie{constructor(t,e){this.source=t,this.pos=e}peek(t=0){return this.source[this.pos+t]||""}consume(){return this.source[this.pos++]||""}eof(){return this.pos>=this.source.length}skipWhitespace(){for(;!this.eof()&&/\s/.test(this.peek());)this.pos++}readIdent(){const t=this.pos;for(;!this.eof()&&se.test(this.peek());)this.pos++;return this.source.slice(t,this.pos)}readQuoted(){const t=this.consume();this.assert('"'===t||"'"===t,"expected quote");const e=this.pos;for(;!this.eof()&&this.peek()!==t;)"\\"===this.peek()?this.pos+=2:this.pos++;this.assert(!this.eof(),"unterminated string");const n=this.source.slice(e,this.pos);return this.pos++,n.replace(/\\(.)/g,"$1")}assert(t,e){t||G(364,String(this.pos+1),e,this.source)}}function ce(t){const e=new Set,n=[],o=[];if(t.skipWhitespace(),"*"===t.peek())t.consume();else if(se.test(t.peek())){const n=t.readIdent();n&&e.add(n.toUpperCase())}for(;!t.eof();){const e=t.peek();if("."===e){t.consume();const e=t.readIdent();t.assert(""!==e,'expected class name after "."'),o.push(e)}else if("#"===e){t.consume();const e=t.readIdent();t.assert(""!==e,'expected id after "#"'),n.push(Zt("id",e))}else{if("["!==e)break;{t.consume(),t.skipWhitespace();const e=t.readIdent();t.assert(""!==e,'expected attribute name after "["'),t.skipWhitespace();let o=!0;if("="===t.peek()){t.consume(),t.skipWhitespace();const e=t.peek();'"'===e||"'"===e?o=t.readQuoted():(o=t.readIdent(),t.assert(""!==o,"expected attribute value")),t.skipWhitespace()}t.assert("]"===t.peek(),'expected "]"'),t.consume(),n.push(Zt(e,o))}}}return o.length>0&&n.push(Yt(o)),{predicates:n,tags:e}}function le(t){const e=new ie(t,0),n=[];for(;;){const t=ce(e);if(n.push(t),e.skipWhitespace(),e.eof())break;e.assert(","===e.peek(),'expected "," (selector lists are the only supported combinator)'),e.consume(),e.skipWhitespace()}if(1===n.length)return Qt(n[0].tags,n[0].predicates);const o=new Set;for(const t of n)for(const e of t.tags)o.add(e);return Qt(o,[(t,e)=>{for(const o of n){const n=t.nodeName;if(o.tags.size>0&&!o.tags.has(n))continue;let r=!0;for(const n of o.predicates)if(!n(t,e)){r=!1;break}if(r)return!0}return!1}])}function ue(t){return t}function fe(t,e,n){return rt(ft,t,e,n)}const ae=fe("importSource",()=>"unknown"),pe=fe("importSourceDataTransfer",()=>null),de=fe("textFormat",()=>0),he=fe("textStyle",()=>({}));function ge(t){if(!r(t))return!1;if("PRE"===t.nodeName)return!0;const e=t.style.whiteSpace;return"string"==typeof e&&e.startsWith("pre")}function me(t){if(d(t))return!0;if(!r(t))return!1;const e=t.style.display;return e?e.startsWith("inline"):!h(t)&&g(t)}const ye=fe("whitespaceConfig",()=>({isInline:me,preservesWhitespace:ge})),xe=fe("importOverlays",()=>[]);class Se{constructor(t){this.record=t}get(t){return J(this.record,t)}set(t,e){this.record[t.key]=e}update(t,e){this.record[t.key]=e(J(this.record,t))}has(t){return Object.prototype.hasOwnProperty.call(this.record,t.key)}}function $e(t){const e=L(t,ut);return e?e.output.defaults:void 0}function ve(t,n=e()){return J(function(t){return K(ft,t)||$e(t)}(n),t)}const be=ot(ft,$e),Ce=oe,ke=new Set(["center","end","justify","left","right","start"]);function Oe(t){return ke.has(t)}const we={B:{fontWeight:"bold"},EM:{fontStyle:"italic"},I:{fontStyle:"italic"},S:{textDecoration:"line-through"},STRONG:{fontWeight:"bold"},SUB:{verticalAlign:"sub"},SUP:{verticalAlign:"super"},U:{textDecoration:"underline"}},De={CODE:y,MARK:m};const Me=new Set(["font-weight","font-style","text-decoration","vertical-align"]);const Ae={$import:(t,e)=>{const n=t.get(de),o=we[e.nodeName],r=function(t){return{fontStyle:t.style.fontStyle,fontWeight:t.style.fontWeight,textDecoration:t.style.textDecoration,verticalAlign:t.style.verticalAlign}}(e),s=o?(i=o,{fontStyle:(c=r).fontStyle||i.fontStyle,fontWeight:c.fontWeight||i.fontWeight,textDecoration:c.textDecoration||i.textDecoration,verticalAlign:c.verticalAlign||i.verticalAlign}):r;var i,c;let l=(u=n,f=function(t){let e=0,n=0;const{fontWeight:o,fontStyle:r,textDecoration:s,verticalAlign:i}=t;if("700"===o||"bold"===o?e|=w:"normal"!==o&&"400"!==o||(n|=w),"italic"===r?e|=D:"normal"===r&&(n|=D),s){const t=s.split(" ");t.includes("underline")&&(e|=M),t.includes("line-through")&&(e|=A),t.includes("none")&&(n|=M|A)}return"sub"===i?(e|=N,n|=I):"super"===i?(e|=I,n|=N):"baseline"===i&&(n|=N|I),{clear:n,set:e}}(s),u&~f.clear|f.set);var u,f;const a=De[e.nodeName];return a&&(l|=a),l===n?t.$importChildren(e):t.$importChildren(e,{context:[tt(de,l)]})},match:Ce.tag("b","strong","em","i","code","mark","s","sub","sup","u","span"),name:"@lexical/html/inline-format"};function Ne(t,e,n){let o=t;for(;;){let t=null;for(;null===(t=e?o.nextSibling:o.previousSibling);){const t=o.parentNode;if(null===t)return null;o=t}if(o=t,!n.isInline(o))return null;let r=o;for(;null!==(r=e?o.firstChild:o.lastChild);)o=r;if(d(o))return o;if("BR"===o.nodeName)return null}}function Ie(t,e){return 0!==e&&O(t)?t.setFormat(e):t}function Ee(t,e){if(O(t)){const n=function(t){let e="";for(const n in t)Me.has(n)||(e+=`${n}: ${t[n]}; `);return e.trimEnd()}(e);""!==n&&t.setStyle(n)}return t}const We={$import:(t,e)=>{const n=t.get(de),o=t.get(he),r=t.get(ye);if(function(t,e){let n=t.parentNode;for(;null!==n;){if(e.preservesWhitespace(n))return!0;n=n.parentNode}return!1}(e,r)){const t=x(e.textContent||"");for(const e of t)Ie(e,n),Ee(e,o);return t}const s=function(t,e){let n=(t.textContent||"").replace(/\r/g,"").replace(/[ \t\n]+/g," ");if(0===n.length)return"";if(" "===n[0]){let o=t,r=!0;for(;null!==o&&null!==(o=Ne(o,!1,e));){const t=o.textContent||"";if(t.length>0){/[ \t\n]$/.test(t)&&(n=n.slice(1)),r=!1;break}}r&&(n=n.slice(1))}if(n.length>0&&" "===n[n.length-1]){let o=t,r=!0;for(;null!==o&&null!==(o=Ne(o,!0,e));)if((o.textContent||"").replace(/^( |\t|\r?\n)+/,"").length>0){r=!1;break}r&&(n=n.slice(0,-1))}return n}(e,r);if(""===s)return[];const i=S(s);return Ie(i,n),Ee(i,o),[i]},match:Ce.text(),name:"@lexical/html/#text"},Re={$import:()=>[],match:Ce.tag("script","style"),name:"@lexical/html/script-style-ignore"},je={$import:()=>[$()],match:Ce.tag("br"),name:"@lexical/html/br"},Te=[Re,{$import:(t,e)=>{const n=v();if(b(n,e),C(e,n),""===n.getFormatType()){const t=e.getAttribute("align");t&&Oe(t)&&n.setFormat(t)}return k(n,e),[n.splice(0,0,t.$importChildren(e))]},match:Ce.tag("p"),name:"@lexical/html/p"},We,je,Ae];function Fe(t,e){const n=[];let o=0,r=0;for(;o<t.length&&r<e.length;)t[o]<=e[r]?n.push(t[o++]):n.push(e[r++]);for(;o<t.length;)n.push(t[o++]);for(;r<e.length;)n.push(e[r++]);return n}function Pe(t){const e=[],n=new Map,o=[],r=[],s=[],i=new Set;t.forEach((t,c)=>{const l=function(t){const e=t[Jt];return void 0===e&&G(360),e}(t.match),u=t.name||function(t,e){if("text"===t.kind)return`#text@${e}`;if("comment"===t.kind)return`#comment@${e}`;if(0===t.tags.size)return`*@${e}`;const n=Array.from(t.tags).join(",").toLowerCase();return`${n}@${e}`}(l,c);if(t.name&&i.add(t.name),e.push({$import:t.$import,name:u,predicate:l.predicate}),"text"===l.kind)r.push(c);else if("comment"===l.kind)s.push(c);else if(0===l.tags.size)o.push(c);else for(const t of l.tags){let e=n.get(t);e||(e=[],n.set(t,e)),e.push(c)}});const c=new Map;if(0===o.length)for(const[t,e]of n)c.set(t,e);else for(const[t,e]of n)c.set(t,Fe(e,o));return{byTag:c,commentIndices:s,rules:e,textIndices:r,wildcardIndices:o}}function ze(t,e){return d(e)?t.textIndices:8===e.nodeType?t.commentIndices:r(e)?t.byTag.get(e.nodeName)||t.wildcardIndices:_e}const _e=Object.freeze([]);function Le(t){const e=[];for(const n of t)if(Ue(n))for(const t of n.rules)e.push(t);else e.push(n);return e}function Ue(t){return"object"==typeof t&&null!==t&&"__type"in t&&"CompiledOverlayRules"===t.__type}function Be(t){const e=Le(t);return{__type:"CompiledOverlayRules",dispatch:Pe(e),rules:e}}function Ve(t){return W(t)||R(t)&&!t.isInline()}function qe(t,e){const n=[];let o=[];const r=()=>{0!==o.length&&(n.push(e().splice(0,0,o)),o=[])};for(const s of t)if(Ve(s)){if(r(),E(s)){const t=qe(s.getChildren(),e);s.splice(0,s.getChildrenSize(),t)}n.push(s)}else o.push(s);return r(),n}function Ge(t,e,n){const o=v();if(r(n)){const t=n.style.textAlign;Oe(t)&&o.setFormat(t)}return[o.splice(0,0,t)]}const He={$accepts:Ve,$packageRun:Ge,name:"BlockSchema"},Je={$accepts:t=>!Ve(t),name:"InlineSchema"},Qe={$accepts:Ve,$packageRun:t=>t,name:"NestedBlockSchema"},Ke={$accepts:Ve,$packageRun:Ge,name:"RootSchema"},Ye=Object.freeze({});function Ze(t,e){const n={$importChildren:(e,n)=>function(t,e,n){const o=n&&n.rules?n.rules.dispatch:void 0;o&&t.overlays.push(o);try{const o=()=>Xe(t,e,n);return n&&n.context?be(n.context,t.editor)(o):o()}finally{o&&t.overlays.pop()}}(t,e,n),$importOne:(e,n)=>tn(t,e,n),captures:e,get:e=>ve(e,t.editor),session:t.session};return n}function Xe(t,e,n){const o=n&&n.$onChild,r=[];for(const n of Array.from(e.childNodes)){const e=tn(t,n,void 0);for(const t of e){const e=o?o(t):t;null!=e&&r.push(e)}}const s=n&&n.$after?n.$after(r):r,i=n&&n.schema;return i?function(t,e,n,o){const r=[];let s=null;const i=()=>{if(null===s)return;const e=s;if(s=null,t.$packageRun){const s=t.$packageRun(e,n,o);if(s.length>0){for(const t of s)r.push(t);return}}if("hoist"===t.onReject)for(const t of e)r.push(t)};for(const o of e)t.$accepts(o,n)?(i(),r.push(o)):(null===s&&(s=[]),s.push(o));return i(),t.$finalize?t.$finalize(r,n):r}(i,s,null,e):s}function tn(t,e,n){const o=()=>function(t,e){const n=function(t,e){const n=[];for(let o=t.overlays.length-1;o>=0;o--){const r=t.overlays[o],s=ze(r,e);s.length>0&&n.push({dispatch:r,indices:s})}const o=ze(t.dispatch,e);o.length>0&&n.push({dispatch:t.dispatch,indices:o});return n}(t,e);if(0===n.length)return en(t,e);let o=0,r=0;const s=()=>{for(;o<n.length;){const{dispatch:i,indices:c}=n[o];for(;r<c.length;){const n=c[r++],o=i.rules[n],l={};if(o.predicate(e,l)){const n=Ze(t,0===Object.keys(l).length?Ye:l);try{return o.$import(n,e,s)}catch(t){throw t}}}o++,r=0}return en(t,e)};return s()}(t,e);return n&&n.context?be(n.context,t.editor)(o):o()}function en(t,e){if(0===e.childNodes.length)return[];const n=[];for(const o of Array.from(e.childNodes)){const e=tn(t,o,void 0);for(const t of e)n.push(t)}return n}const nn={$import:(t,e)=>t.$importChildren(e),match:oe.any(),name:"@lexical/html/default-hoist"},on=f({build(t,e){const n=Pe(Le(e.rules)),r=Z(e.contextDefaults,void 0),s=e.preprocess;return{$generateNodesFromDOM:(e,i)=>{const c=i&&i.context?Z(i.context,r):r,l=void 0!==c&&c!==r?c:Object.create(r||null),u=new Se(l),f={session:u};return function(t,e,n){let o=t.length-1;const r=()=>{for(;o>=0;)return void(0,t[o--])(e,n,r)};r()}(i&&i.preprocess?[...s,...i.preprocess]:s,e,f),nt(ft,l,()=>function(t,e,n,r){return Xe({dispatch:t,editor:e,overlays:r.get(xe).map(t=>t.dispatch),session:r},o(n)?n.body:n,{schema:Ke})}(n,t,e,u),t)},defaults:r}},config:{contextDefaults:[],preprocess:[st],rules:[nn]},mergeConfig:(t,e)=>a(t,{...e,...e.contextDefaults&&{contextDefaults:[...t.contextDefaults,...e.contextDefaults]},...e.preprocess&&{preprocess:[...t.preprocess,...e.preprocess]},...e.rules&&{rules:[...e.rules,...t.rules]}}),name:ut});function rn(t,e){return B(on).$generateNodesFromDOM(t,e)}const sn=f({dependencies:[j(on,{rules:Te})],name:"@lexical/html/CoreImport"}),cn=[{$import:()=>[q()],match:oe.tag("hr"),name:"@lexical/html/hr"}],ln=f({dependencies:[sn,V,j(on,{rules:cn})],name:"@lexical/html/HorizontalRuleImport"}),un={any:oe.any,comment:oe.comment,css:le,tag:oe.tag,text:oe.text},fn=new Set(["STYLE","SCRIPT"]);function an(t,e){it(e);const n=o(e)?e.body.childNodes:e.childNodes,r=[],s=[];for(const e of n)if(!fn.has(e.nodeName)){const n=mn(e,t,s,!1);if(null!==n)for(const t of n)r.push(t)}return function(t){for(const e of t)e.getParent()&&e.getNextSibling()instanceof z&&e.insertAfter($());for(const e of t){const t=e.getParent();t&&t.splice(e.getIndexWithinParent(),1,e.getChildren())}}(s),r}function pn(t,n=null,o=e()){return vt([tt(ht,!0)],o)(()=>{const e=T(),r=$t(o),s=t.append.bind(t);for(const t of e.getChildren())gn(o,t,s,n,r);return t})}function dn(t,n=T()){const o=e();return vt([tt(ht,!0),tt(dt,!0)],o)(()=>{const e=$t(o),r=t.append.bind(t);return gn(o,n,r,null,e),t})}function hn(t,e=null){return("undefined"==typeof document||"undefined"==typeof window&&void 0===global.window)&&G(338),pn(document.createElement("div"),e,t).innerHTML}function gn(e,n,o,i=null,c=s(e)){let l=c.$shouldInclude(n,i,e);const u=c.$shouldExclude(n,i,e);let f=n;null!==i&&O(n)&&(f=t(i,n,"clone"));const a=c.$exportDOM(f,e),{element:p,after:d,append:h,$getChildNodes:g}=a;if(!p)return!1;const m=document.createDocumentFragment(),y=g?g():E(f)?f.getChildren():[],x=m.append.bind(m);for(const t of y){const o=gn(e,t,x,i,c);!l&&o&&c.$extractWithChild(n,t,i,"html",e)&&(l=!0)}if(l&&!u){if((r(p)||F(p))&&(h?h(m):p.append(m)),o(p),d){const t=d.call(f,p);t&&(F(p)?p.replaceChildren(t):p.replaceWith(t))}}else o(m);return l}function mn(t,e,n,o,r=new Map,s){const i=[];if(fn.has(t.nodeName))return i;let c=null;const l=function(t,e){const{nodeName:n}=t,o=e._htmlConversions.get(n.toLowerCase());let r=null;if(void 0!==o)for(const e of o){const n=e(t);null!==n&&(null===r||(r.priority||0)<=(n.priority||0))&&(r=n)}return null!==r?r.conversion:null}(t,e),u=l?l(t):null;let f=null;if(null!==u){f=u.after;const e=u.node;if(c=Array.isArray(e)?e[e.length-1]:e,null!==c){for(const[,t]of r)if(c=t(c,s),!c)break;c&&i.push(...Array.isArray(e)?e:[c])}null!=u.forChild&&r.set(t.nodeName,u.forChild)}const a=t.childNodes;let p=[];const d=(null==c||!P(c))&&(null!=c&&W(c)||o);for(let t=0;t<a.length;t++)p.push(...mn(a[t],e,n,d,new Map(r),c));if(null!=f&&(p=f(p)),h(t)&&(p=yn(t,p,d?()=>{const t=new z;return n.push(t),t}:v)),null==c)if(p.length>0)for(const t of p)i.push(t);else h(t)&&function(t){if(null==t.nextSibling||null==t.previousSibling)return!1;return g(t.nextSibling)&&g(t.previousSibling)}(t)&&i.push($());else E(c)&&c.append(...p);return i}function yn(t,e,n){const o=t.style.textAlign,r=[];let s=[];for(let t=0;t<e.length;t++){const i=e[t];if(W(i))o&&!i.getFormat()&&i.setFormat(o),r.push(i);else if(s.push(i),t===e.length-1||t<e.length-1&&W(e[t+1])){const t=n();t.setFormat(o),t.append(...s),r.push(t),s=[]}}return r}export{qe as $distributeInlineWrapper,pn as $generateDOMFromNodes,dn as $generateDOMFromRoot,hn as $generateHtmlFromNodes,an as $generateNodesFromDOM,rn as $generateNodesFromDOMViaExtension,ve as $getImportContextValue,mt as $getRenderContextValue,$t as $getSessionDOMRenderConfig,st as $inlineStylesFromStyleSheets,Ve as $isBlockLevel,xt as $setRenderContextValue,St as $updateRenderContextValue,be as $withImportContext,vt as $withRenderContext,He as BlockSchema,sn as CoreImportExtension,Te as CoreImportRules,on as DOMImportExtension,Ht as DOMRenderExtension,ln as HorizontalRuleImportExtension,cn as HorizontalRuleImportRules,xe as ImportOverlays,ae as ImportSource,pe as ImportSourceDataTransfer,de as ImportTextFormat,he as ImportTextStyle,ye as ImportWhitespaceConfig,Je as InlineSchema,Qe as NestedBlockSchema,ht as RenderContextExport,dt as RenderContextRoot,Ke as RootSchema,et as contextUpdater,tt as contextValue,fe as createImportState,pt as createRenderState,me as defaultIsInline,ge as defaultPreservesWhitespace,ue as defineImportRule,Be as defineOverlayRules,bt as domOverride,re as isElementOfTag,le as parseSelector,un as sel};
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import type { EditorDOMRenderConfig } from 'lexical';
import { LexicalEditor } from 'lexical';
import { AnyRenderStateConfigPairOrUpdater, RenderStateConfig } from './types';
/**
* Create a context state to be used during render.
*
* Note that to support the ValueOrUpdater pattern you can not use a
* function for V (but you may wrap it in an array or object).
*
* @experimental
* @__NO_SIDE_EFFECTS__
*/
export declare function createRenderState<V>(name: string, getDefaultValue: () => V, isEqual?: (a: V, b: V) => boolean): RenderStateConfig<V>;
/**
* Render context state that is true if the export was initiated from the root of the document.
* @experimental
*/
export declare const RenderContextRoot: RenderStateConfig<boolean>;
/**
* Render context state that is true if this is an export operation ($generateHtmlFromNodes).
* @experimental
*/
export declare const RenderContextExport: RenderStateConfig<boolean>;
/**
* Get a render context value during a DOM render or export operation.
* @experimental
*/
export declare function $getRenderContextValue<V>(cfg: RenderStateConfig<V>, editor?: LexicalEditor): V;
/**
* Imperatively set a value in the persistent editor render context.
*
* Unlike {@link $withRenderContext} (which scopes values to a callback), this
* persists on the editor. If the change flips any override's
* `disabledForEditor` result, the resident render config is recompiled and the
* affected nodes are re-rendered. No-op if {@link DOMRenderExtension} is not
* installed.
*
* @experimental
*/
export declare function $setRenderContextValue<V>(cfg: RenderStateConfig<V>, value: V, editor?: LexicalEditor): void;
/**
* Imperatively update a value in the persistent editor render context with an
* updater function. See {@link $setRenderContextValue}.
*
* @experimental
*/
export declare function $updateRenderContextValue<V>(cfg: RenderStateConfig<V>, updater: (prev: V) => V, editor?: LexicalEditor): void;
/**
* Resolve the {@link EditorDOMRenderConfig} to use for the current
* export/generate session, applying any `disabledForSession` overrides against
* the active session context. Falls back to the editor's resident config when
* {@link DOMRenderExtension} is not installed.
*
* @experimental
*/
export declare function $getSessionDOMRenderConfig(editor?: LexicalEditor): EditorDOMRenderConfig;
/**
* Execute a callback within a render context with the given config pairs.
* @experimental
*/
export declare const $withRenderContext: (cfg: readonly AnyRenderStateConfigPairOrUpdater[], editor?: LexicalEditor) => <T>(f: () => T) => T;
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import type { DOMImportContextSymbol, DOMRenderContextSymbol } from './constants';
import type { BaseSelection, DOMExportOutput, DOMSlotForNode, EditorDOMRenderConfig, Klass, LexicalEditor, LexicalNode, StateConfig } from 'lexical';
/**
* @experimental
*
* Any ContextSymbol for {@link ContextConfig} (DOM render or DOM import).
*/
export type AnyContextSymbol = typeof DOMRenderContextSymbol | typeof DOMImportContextSymbol;
/**
* @experimental
*
* Context with a phantom type for its purpose (such as {@link DOMRenderContextSymbol}).
*
* A ContextRecord is a data structure used in the export and import pipelines
* to allow for information to be passed throughout the chain without explicit
* argument passing, e.g. to specify whether the intended use case for HTML
* export is for serialization or for clipboard copy.
*/
export type ContextRecord<_K extends symbol> = Record<string | symbol, unknown>;
/**
* @experimental
*
* A data structure much like StateConfig (they share implementation details)
* but for managing context during an export or import pipeline rather than
* individual node state.
*/
export type ContextConfig<Sym extends symbol, V> = StateConfig<symbol, V> & {
readonly [K in Sym]?: true;
};
/**
* @experimental
*
* Update the context at `cfg` with updater, constructed with {@link contextUpdater}
*/
export type ContextConfigUpdater<Ctx extends AnyContextSymbol, V> = {
readonly cfg: ContextConfig<Ctx, V>;
/**
* @param prev The current or default value
* @returns The new value
*/
readonly updater: (prev: V) => V;
};
/**
* @experimental
*
* Set the the context at `cfg` to a specific value, constructed with {@link contextValue}
*/
export type ContextConfigPair<Ctx extends AnyContextSymbol, V> = readonly [
ContextConfig<Ctx, V>,
V
];
/**
* @experimental
*
* Set or update a context value, constructed with {@link contextValue} or {@link contextUpdater}
*/
export type ContextPairOrUpdater<Ctx extends AnyContextSymbol, V> = ContextConfigPair<Ctx, V> | ContextConfigUpdater<Ctx, V>;
/** @experimental */
export type AnyContextConfigPairOrUpdater<Ctx extends AnyContextSymbol> = ContextPairOrUpdater<Ctx, any>;
/** @experimental */
export interface DOMRenderExtensionOutput {
/** @internal */
defaults: undefined | ContextRecord<typeof DOMRenderContextSymbol>;
/** @internal */
runtime: DOMRenderRuntime;
}
/**
* @experimental
*
* A read-only view of a render context layer, passed to the
* {@link DOMOverrideOptions} predicates so they can decide whether an
* override should be installed based only on context values.
*/
export interface RenderContextReader {
get<V>(cfg: RenderStateConfig<V>): V;
}
/**
* @experimental
* @internal
*
* Per-editor runtime state for {@link DOMRenderExtension} that backs the
* imperative editor context ({@link createRenderState} writes via
* `$setRenderContextValue`) and the conditional install of overrides.
*/
export interface DOMRenderRuntime {
/**
* The mutable, persistent editor-level context record. Reads of a
* {@link RenderStateConfig} during reconciliation (and as the base layer
* during a session) fall through to this record. It is also the layer
* that {@link DOMOverrideOptions.disabledForEditor} predicates read from.
*/
readonly editorContext: ContextRecord<typeof DOMRenderContextSymbol>;
/**
* Imperatively set a value in the editor context. If the change flips any
* override's `disabledForEditor` result, the resident render config is
* recompiled and the affected nodes are re-rendered (recreating DOM for
* structural overrides).
*/
setContextValue<V>(cfg: RenderStateConfig<V>, value: V): void;
/**
* Resolve the {@link EditorDOMRenderConfig} for the current export/generate
* session, applying any {@link DOMOverrideOptions.disabledForSession}
* predicates against the active session context. Returns the resident
* config when no session gating applies.
*/
getSessionConfig(): EditorDOMRenderConfig;
}
/**
* @experimental
*
* Options for {@link domOverride} controlling *whether* an override is
* installed, based only on render context. Both predicates default to
* "not disabled".
*/
export interface DOMOverrideOptions {
/**
* Gate residency in the editor's render config (used by reconciliation and
* as the base for export/generate). Evaluated against the persistent editor
* context at compile time, and re-evaluated when that context changes via
* `$setRenderContextValue`; a change recompiles the config and re-renders
* affected nodes. Return `true` to remove the override. Default: not disabled.
*/
disabledForEditor?: (ctx: RenderContextReader) => boolean;
/**
* Gate participation in a single export/generate session. Evaluated once at
* the start of each session against that session's context. Has no effect on
* live reconciliation (which is not a session). Return `true` to remove the
* override for that session. Default: not disabled.
*/
disabledForSession?: (ctx: RenderContextReader) => boolean;
}
/**
* @experimental
*
* Context configuration for render context, created with {@link createRenderState}
*/
export type RenderStateConfig<V> = ContextConfig<typeof DOMRenderContextSymbol, V>;
/**
* @experimental
*
* Any setter or updater for {@link RenderStateConfig}
*/
export type AnyRenderStateConfigPairOrUpdater = AnyContextConfigPairOrUpdater<typeof DOMRenderContextSymbol>;
/**
* @experimental
*
* Any {@link RenderStateConfig}
*/
export type AnyRenderStateConfig = RenderStateConfig<any>;
/**
* @experimental
*
* Configuration for {@link DOMRenderExtension}
*/
export interface DOMRenderConfig {
/**
* {@link DOMRenderMatch} overrides to customize node behavior,
* the final priority of these will be based on the following criteria:
*
* - Wildcards (`'*'`) have highest priority
* - Predicates (`$isParagraphNode`) have next priority
* - Subclasses have higher priority (e.g. `ParagraphNode` before `ElementNode`)
* - Extensions closer to the root have higher priority
* - Extensions depended on later have higher priority
* - Overrides defined later have higher priority
*/
overrides: AnyDOMRenderMatch[];
/**
* Default context to provide in all exports, the configurations are created
* with {@link createRenderState} and should be created at the module-level.
*
* Only specify these if overriding the default value globally, since each
* configuration has a built-in default value that will be used if not
* already present in the context.
*/
contextDefaults: AnyRenderStateConfigPairOrUpdater[];
}
/**
* @experimental
* Any {@link DOMRenderMatch}
*/
export type AnyDOMRenderMatch = DOMRenderMatch<any>;
/**
* @experimental
*
* Match a node (and any subclass of that node) by its LexicalNode class,
* or with a guard (e.g. `ElementNode` or `$isElementNode`).
*
* Note that using the class compiles to significantly more efficient code
* than using a guard.
*/
export type NodeMatch<T extends LexicalNode> = Klass<T> | ((node: LexicalNode) => node is T);
/**
* @experimental
*
* Used to define overrides for the render and export
* behavior for nodes matching the `nodes` predicate.
*
* All of these overrides are in a middleware style where you may use the
* result of `$next()` to enhance the result of the default implementation
* (or a lower priority override) by calling it and manipulating the result,
* or you may choose not to call `$next()` to entirely replace the behavior.
*
* It is not permitted to update the lexical editor state during any of
* these calls, you should only be doing read-only operations.
*/
export interface DOMRenderMatch<T extends LexicalNode> {
/**
* '*' for all nodes, or an array of `NodeClass | $isNodeGuard` to match
* nodes more specifically. Using classes is more efficient, but will
* also target subclasses.
*/
readonly nodes: '*' | readonly NodeMatch<T>[];
/**
* Control where an ElementNode's children are inserted into the DOM,
* this is useful to add a wrapping node or accessory nodes before or
* after the children. The root of the node returned by createDOM must
* still be exactly one HTMLElement.
*
* Generally you will call `$next()` to get a slot and then use its methods
* to create a new one. The slot type is narrowed via {@link DOMSlotForNode}:
* for `ElementNode` it resolves to {@link ElementDOMSlot} with
* children-management semantics; for non-Element nodes the base
* {@link DOMSlot} pointing at the keyed DOM.
*
* @param node The LexicalNode
* @param dom The rendered HTMLElement
* @param $next Call the next implementation
* @param editor The editor
* @returns The slot for this node
*/
$getDOMSlot?: (node: T, dom: HTMLElement, $next: () => DOMSlotForNode<T>, editor: LexicalEditor) => DOMSlotForNode<T>;
/**
* Called during the reconciliation process to determine which nodes
* to insert into the DOM for this Lexical Node. This is also the default
* implementation of `$exportDOM` for most nodes.
*
* This method must return exactly one `HTMLElement`.
*
* Nested elements are not supported except with `DecoratorNode`
* (which have unmanaged contents) or `ElementNode` using an appropriate
* `$getDOMSlot` return value.
*
* @param node The LexicalNode
* @param $next Call the next implementation
* @param editor The editor
* @returns The HTMLElement for this node to be rendered in the editor
*/
$createDOM?: (node: T, $next: () => HTMLElement, editor: LexicalEditor) => HTMLElement;
/**
* Called when a node changes and should update the DOM
* in whatever way is necessary to make it align with any changes that might
* have happened during the update.
*
* Returning `true` here will cause lexical to unmount and recreate the DOM
* node (by calling `$createDOM`). You would need to do this if the element
* tag changes, for instance.
*
* @param nextNode The current version of this node
* @param prevNode The previous version of this node
* @param dom The previously rendered HTMLElement for this node
* @param $next Call the next implementation
* @param editor The editor
* @returns `false` if no update needed or was performed in-place, `true` if `$createDOM` should be called to re-create the node
*/
$updateDOM?: (nextNode: T, prevNode: T, dom: HTMLElement, $next: () => boolean, editor: LexicalEditor) => boolean;
/**
* Called after a node is created or updated and should make any in-place
* updates to the DOM in whatever way is necessary to make it align with
* any changes that might have happened during the `$createDOM` or
* `$updateDOM`. This also runs after any children have been reconciled.
*
* Use this when you have code that you would need to duplicate in both
* methods, or if there is a need to ensure that the children are also
* reconciled before performing this in-place update.
*
* Unlike other overrides, all applicable `$decorateDOM` functions are
* called unconditionally. There is no `$next` argument, because there
* are no known use cases for avoiding the next implementation and due
* to the void return value it would be error-prone and add boilerplate
* to require calling it.
*
* The ordering here is equivalent to an implicit `$next` call *first*.
*
* @param nextNode The current version of this node
* @param prevNode The previous version of this node if `$updateDOM` returned `false`, or `null` if `$createDOM` was just called
* @param dom The previously rendered `HTMLElement` for this node
* @param editor The editor
*/
$decorateDOM?: (nextNode: T, prevNode: null | T, dom: HTMLElement, editor: LexicalEditor) => void;
/**
* Controls how the this node is serialized to HTML. This is important for
* copy and paste between Lexical and non-Lexical editors, or Lexical
* editors with different namespaces, in which case the primary transfer
* format is HTML. It's also important if you're serializing to HTML for
* any other reason via {@link @lexical/html!$generateHtmlFromNodes}.
*
* @param node The LexicalNode
* @param $next Call the next implementation
* @param editor The editor
* @returns A {@link DOMExportOutput} structure that defines how the node should be exported to HTML
*/
$exportDOM?: (node: T, $next: () => DOMExportOutput, editor: LexicalEditor) => DOMExportOutput;
/**
* Equivalent to `ElementNode.excludeFromCopy`, if it returns `true` this
* lexical node will not be exported to DOM (but if it's an `ElementNode`
* its children may still be inserted in its place).
*
* Has higher precedence than `$shouldInclude` and `$extractWithChild`.
*
* @param node The LexicalNode
* @param selection The current selection
* @param $next The next implementation
* @param editor The editor
* @returns true to exclude this node, false otherwise
*/
$shouldExclude?: (node: T, selection: null | BaseSelection, $next: () => boolean, editor: LexicalEditor) => boolean;
/**
* Return `true` if this node should be included in the export, typically based
* on the current selection (all nodes by default are included when there
* is no selection).
*
* The default implementation is equivalent to
* `selection ? node.isSelected(selection) : true`.
*
* This has lower precedence than `$extractWithChild` and `$shouldExclude`.
*
* @param node The current node
* @param selection The current selection
* @param $next The next implementation
* @param editor The editor
* @returns `true` if this node should be included in the export, `false` otherwise
*/
$shouldInclude?: (node: T, selection: null | BaseSelection, $next: () => boolean, editor: LexicalEditor) => boolean;
/**
* Return `true` if this node should be included in the export based on
* `childNode`, even if it would not otherwise be included based on its
* `$shouldInclude` result.
*
* Typically used to ensure that required wrapping nodes are always
* present with its children, e.g. a ListNode when some of its ListItemNode
* children are selected.
*
* This has higher precedence than `$extractWithChild` and lower precedence
* than `$shouldExclude`.
*
* @param node The lexical node
* @param childNode A child of this lexical node
* @param selection The current selection
* @param destination Currently always `'html'`
* @param $next The next implementation
* @param editor The editor
* @returns true if this
*/
$extractWithChild?: (node: T, childNode: LexicalNode, selection: null | BaseSelection, destination: 'clone' | 'html', $next: () => boolean, editor: LexicalEditor) => boolean;
/**
* Set via {@link domOverride}'s options argument, not directly. See
* {@link DOMOverrideOptions.disabledForEditor}.
*/
disabledForEditor?: (ctx: RenderContextReader) => boolean;
/**
* Set via {@link domOverride}'s options argument, not directly. See
* {@link DOMOverrideOptions.disabledForSession}.
*/
disabledForSession?: (ctx: RenderContextReader) => boolean;
}
/**
* @experimental
*
* The hook fields of a {@link DOMRenderMatch} — i.e. without `nodes` or the
* {@link DOMOverrideOptions} predicates, which are passed separately to
* {@link domOverride}.
*/
export type DOMRenderMatchConfig<T extends LexicalNode> = Omit<DOMRenderMatch<T>, 'nodes' | keyof DOMOverrideOptions>;
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import {getKnownTypesAndNodes} from '@lexical/extension';
import invariant from '@lexical/internal/invariant';
import {
$isLexicalNode,
DEFAULT_EDITOR_DOM_CONFIG,
type EditorDOMRenderConfig,
getStaticNodeConfig,
InitialEditorConfig,
Klass,
LexicalEditor,
type LexicalNode,
} from 'lexical';
import {ALWAYS_TRUE} from './constants';
import {AnyDOMRenderMatch, DOMRenderConfig, DOMRenderMatch} from './types';
interface TypeRecord {
readonly klass: Klass<LexicalNode>;
readonly types: {[NodeAndSubclasses in string]?: boolean};
}
type TypeTree = {
[NodeType in string]?: TypeRecord;
};
export function buildTypeTree(
editorConfig: Pick<InitialEditorConfig, 'nodes'>,
): TypeTree {
const t: TypeTree = {};
const {nodes} = getKnownTypesAndNodes(editorConfig);
for (const klass of nodes) {
const type = klass.getType();
t[type] = {klass, types: {}};
}
for (const baseRec of Object.values(t)) {
if (baseRec) {
const baseType = baseRec.klass.getType();
for (
let {klass} = baseRec;
$isLexicalNode(klass.prototype);
klass = Object.getPrototypeOf(klass)
) {
const {ownNodeType} = getStaticNodeConfig(klass);
const superRec = ownNodeType && t[ownNodeType];
if (superRec) {
superRec.types[baseType] = true;
}
}
}
}
return t;
}
type PredicateOrTypes =
| ((node: LexicalNode) => boolean)
| {[NodeType in string]?: true};
type TypeRender<T> = {[NodeType in string]?: T[]};
type AnyRender<T> =
| readonly [(node: LexicalNode) => boolean, T]
| readonly ['types', TypeRender<T>];
type PreEditorDOMRenderConfig = {
[K in keyof EditorDOMRenderConfig]: AnyRender<AnyDOMRenderMatch[K]>[];
};
function buildNodePredicate<T extends LexicalNode>(klass: Klass<T>) {
return (node: LexicalNode): node is T => node instanceof klass;
}
function getPredicate(
typeTree: TypeTree,
{nodes}: DOMRenderMatch<LexicalNode>,
): {[NodeType in string]?: true} | ((node: LexicalNode) => boolean) {
if (nodes === '*') {
return ALWAYS_TRUE;
}
let types: undefined | {[NodeType in string]?: true} = {};
const predicates: ((node: LexicalNode) => boolean)[] = [];
for (const klassOrPredicate of nodes) {
if ('getType' in klassOrPredicate) {
const type = klassOrPredicate.getType();
if (types) {
const tree = typeTree[type];
invariant(
tree !== undefined,
'Node class %s with type %s not registered in editor',
klassOrPredicate.name,
type,
);
types = Object.assign(types, tree.types);
}
predicates.push(buildNodePredicate(klassOrPredicate));
} else {
types = undefined;
predicates.push(klassOrPredicate);
}
}
if (types) {
return types;
} else if (predicates.length === 1) {
return predicates[0];
}
return (node: LexicalNode): boolean => {
for (const predicate of predicates) {
if (predicate(node)) {
return true;
}
}
return false;
};
}
function makePrerender(): PreEditorDOMRenderConfig {
return {
$createDOM: [],
$decorateDOM: [],
$exportDOM: [],
$extractWithChild: [],
$getDOMSlot: [],
$shouldExclude: [],
$shouldInclude: [],
$updateDOM: [],
};
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type AccFn<T, N extends LexicalNode, Args extends any[]> = (
node: N,
...rest: [...Args, editor: LexicalEditor]
) => T;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type GetOverrideFn<T, N extends LexicalNode, Args extends any[]> = (
n: N,
) => undefined | OverrideFn<T, N, Args>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type OverrideFn<T, N extends LexicalNode, Args extends any[]> = (
node: N,
...rest: [...Args, $next: () => T, editor: LexicalEditor]
) => T;
function ignoreNext2<T, N extends LexicalNode>(
acc: AccFn<T, N, []>,
): OverrideFn<T, N, []> {
return (node: N, _$next: () => T, editor: LexicalEditor) => acc(node, editor);
}
function ignoreNext3<T, N extends LexicalNode, A>(
acc: AccFn<T, N, [A]>,
): OverrideFn<T, N, [A]> {
return (node: N, a: A, _$next: () => T, editor: LexicalEditor) =>
acc(node, a, editor);
}
function ignoreNext4<T, N extends LexicalNode, A, B>(
acc: AccFn<T, N, [A, B]>,
): OverrideFn<T, N, [A, B]> {
return (node: N, a: A, b: B, _$next: () => T, editor: LexicalEditor) =>
acc(node, a, b, editor);
}
function ignoreNext5<T, N extends LexicalNode, A, B, C>(
acc: AccFn<T, N, [A, B, C]>,
): OverrideFn<T, N, [A, B, C]> {
return (node: N, a: A, b: B, c: C, _$next: () => T, editor: LexicalEditor) =>
acc(node, a, b, c, editor);
}
function merge2<T, N extends LexicalNode>(
$acc: AccFn<T, N, []>,
$getOverride: GetOverrideFn<T, N, []>,
): typeof $acc {
return (node, editor) => {
const $next = () => $acc(node, editor);
const $override = $getOverride(node);
return $override ? $override(node, $next, editor) : $next();
};
}
function merge3<T, N extends LexicalNode, A>(
acc: AccFn<T, N, [A]>,
$getOverride: GetOverrideFn<T, N, [A]>,
): typeof acc {
return (node, a, editor) => {
const $next = () => acc(node, a, editor);
const $override = $getOverride(node);
return $override ? $override(node, a, $next, editor) : $next();
};
}
const merge3GetDOMSlot = merge3 as (
acc: EditorDOMRenderConfig['$getDOMSlot'],
$getOverride: (n: LexicalNode) => DOMRenderMatch<LexicalNode>['$getDOMSlot'],
) => EditorDOMRenderConfig['$getDOMSlot'];
const ignoreNext3GetDOMSlot = ignoreNext3 as (
fn: EditorDOMRenderConfig['$getDOMSlot'],
) => DOMRenderMatch<LexicalNode>['$getDOMSlot'];
function merge4<T, N extends LexicalNode, A, B>(
$acc: AccFn<T, N, [A, B]>,
$getOverride: GetOverrideFn<T, N, [A, B]>,
): typeof $acc {
return (node, a, b, editor) => {
const $next = () => $acc(node, a, b, editor);
const $override = $getOverride(node);
return $override ? $override(node, a, b, $next, editor) : $next();
};
}
function merge5<T, N extends LexicalNode, A, B, C>(
acc: AccFn<T, N, [A, B, C]>,
$getOverride: GetOverrideFn<T, N, [A, B, C]>,
): typeof acc {
return (node, a, b, c, editor) => {
const $next = () => acc(node, a, b, c, editor);
const $override = $getOverride(node);
return $override ? $override(node, a, b, c, $next, editor) : $next();
};
}
function sequence4<N extends LexicalNode, A, B>(
$acc: AccFn<void, N, [A, B]>,
$getOverride: (n: N) => undefined | AccFn<void, N, [A, B]>,
): typeof $acc {
return (node, a, b, editor) => {
$acc(node, a, b, editor);
const $override = $getOverride(node);
if ($override) {
$override(node, a, b, editor);
}
};
}
function compilePrerenderKey<K extends keyof PreEditorDOMRenderConfig>(
prerender: PreEditorDOMRenderConfig,
k: K,
defaults: EditorDOMRenderConfig,
mergeFunction: (
$acc: EditorDOMRenderConfig[K],
$getOverride: (node: LexicalNode) => AnyDOMRenderMatch[K],
) => typeof $acc,
ignoreNextFunction: (fn: EditorDOMRenderConfig[K]) => AnyDOMRenderMatch[K],
): void {
let acc = defaults[k];
for (const pair of prerender[k]) {
if (typeof pair[0] === 'function') {
const [$predicate, $override] = pair;
acc = mergeFunction(
acc,
node => ($predicate(node) && $override) || undefined,
);
} else {
const typeOverrides = pair[1];
const compiled: Record<string, undefined | EditorDOMRenderConfig[K]> = {};
for (const type in typeOverrides) {
const arr = typeOverrides[type];
if (arr) {
compiled[type] = arr.reduce(
($acc, $override) => mergeFunction($acc, () => $override),
acc,
);
}
}
acc = mergeFunction(acc, node => {
const f = compiled[node.getType()];
return f && ignoreNextFunction(f);
});
}
}
defaults[k] = acc;
}
function addOverride<K extends keyof PreEditorDOMRenderConfig>(
prerender: PreEditorDOMRenderConfig,
k: K,
predicateOrTypes: PredicateOrTypes,
override: AnyDOMRenderMatch[K],
): void {
if (!override) {
return;
}
const arr = prerender[k];
if (typeof predicateOrTypes === 'function') {
arr.push([predicateOrTypes, override]);
} else {
const last = arr[arr.length - 1];
let types: TypeRender<AnyDOMRenderMatch[K]>;
if (last && last[0] === 'types') {
types = last[1];
} else {
types = {};
arr.push(['types', types]);
}
for (const type in predicateOrTypes) {
const typeArr = types[type] || [];
types[type] = typeArr;
typeArr.push(override);
}
}
}
type NormalizedDOMRenderMatch<T> = Omit<
DOMRenderMatch<LexicalNode>,
'nodes'
> & {
nodes: T;
};
function isWildcard(
override: DOMRenderMatch<LexicalNode>,
): override is NormalizedDOMRenderMatch<'*'> {
return override.nodes === '*';
}
function sortedOverrides(
overrides: DOMRenderMatch<LexicalNode>[],
): DOMRenderMatch<LexicalNode>[] {
const byWildcard: NormalizedDOMRenderMatch<'*'>[] = [];
const byPredicate: NormalizedDOMRenderMatch<
[(node: LexicalNode) => node is LexicalNode]
>[] = [];
const byNode: NormalizedDOMRenderMatch<[Klass<LexicalNode>]>[] = [];
for (const override of overrides) {
if (isWildcard(override)) {
byWildcard.push(override);
} else if (Array.isArray(override.nodes)) {
for (const klassOrPredicate of override.nodes) {
if ($isLexicalNode(klassOrPredicate.prototype)) {
byNode.push(
override.nodes.length === 1
? (override as NormalizedDOMRenderMatch<[Klass<LexicalNode>]>)
: {...override, nodes: [klassOrPredicate]},
);
} else {
byPredicate.push(
override.nodes.length === 1
? (override as NormalizedDOMRenderMatch<
[(node: LexicalNode) => node is LexicalNode]
>)
: {...override, nodes: [klassOrPredicate]},
);
}
}
}
}
const depths = new Map<Klass<LexicalNode>, number>();
const depthOf = (klass: Klass<LexicalNode>): number => {
let depth = depths.get(klass);
if (depth === undefined) {
depth = 0;
for (
let k: Klass<LexicalNode> = klass;
$isLexicalNode(k.prototype);
k = Object.getPrototypeOf(k)
) {
depth++;
}
depths.set(klass, depth);
}
return depth;
};
byNode.sort((a, b) => depthOf(a.nodes[0]) - depthOf(b.nodes[0]));
return [...byNode, ...byPredicate, ...byWildcard];
}
export function precompileDOMRenderConfigOverrides(
editorConfig: Pick<InitialEditorConfig, 'nodes'>,
overrides: DOMRenderConfig['overrides'],
): PreEditorDOMRenderConfig {
const typeTree = buildTypeTree(editorConfig);
const prerender = makePrerender();
for (const override of sortedOverrides(overrides)) {
const predicateOrTypes = getPredicate(typeTree, override);
for (const k_ in prerender) {
const k = k_ as keyof typeof prerender;
addOverride(prerender, k, predicateOrTypes, override[k]);
}
}
return prerender;
}
function identity<T>(v: T) {
return v;
}
export function compileDOMRenderConfigOverrides(
editorConfig: Pick<InitialEditorConfig, 'nodes' | 'dom'>,
{overrides}: Pick<DOMRenderConfig, 'overrides'>,
): EditorDOMRenderConfig {
const prerender = precompileDOMRenderConfigOverrides(editorConfig, overrides);
const dom = {
...DEFAULT_EDITOR_DOM_CONFIG,
...editorConfig.dom,
};
compilePrerenderKey(prerender, '$createDOM', dom, merge2, ignoreNext2);
compilePrerenderKey(prerender, '$exportDOM', dom, merge2, ignoreNext2);
compilePrerenderKey(prerender, '$extractWithChild', dom, merge5, ignoreNext5);
compilePrerenderKey(
prerender,
'$getDOMSlot',
dom,
merge3GetDOMSlot,
ignoreNext3GetDOMSlot,
);
compilePrerenderKey(prerender, '$shouldExclude', dom, merge3, ignoreNext3);
compilePrerenderKey(prerender, '$shouldInclude', dom, merge3, ignoreNext3);
compilePrerenderKey(prerender, '$updateDOM', dom, merge4, ignoreNext4);
compilePrerenderKey(prerender, '$decorateDOM', dom, sequence4, identity);
return dom;
}
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
export const DOMRenderExtensionName = '@lexical/html/DOM';
export const DOMRenderContextSymbol = Symbol.for(
'@lexical/html/DOMExportContext',
);
export const DOMImportExtensionName = '@lexical/html/DOMImport';
export const DOMImportContextSymbol = Symbol.for(
'@lexical/html/DOMImportContext',
);
export const ALWAYS_TRUE = () => true as const;
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import type {
AnyContextConfigPairOrUpdater,
AnyContextSymbol,
ContextConfig,
ContextConfigPair,
ContextConfigUpdater,
ContextRecord,
} from './types';
import {$getEditor, createState, type LexicalEditor} from 'lexical';
let activeContext: undefined | EditorContext;
type WithContext<Ctx extends AnyContextSymbol> = {
[K in Ctx]?: undefined | ContextRecord<Ctx>;
};
/**
* @experimental
*
* The LexicalEditor with context
*/
export type EditorContext = {
editor: LexicalEditor;
} & WithContext<AnyContextSymbol>;
/**
* @experimental
*
* @param contextRecord The ContextRecord
* @param cfg The configuration
* @returns The value or defaultValue of cfg
*/
export function getContextValue<Ctx extends AnyContextSymbol, V>(
contextRecord: undefined | ContextRecord<Ctx>,
cfg: ContextConfig<Ctx, V>,
): V {
const {key} = cfg;
return contextRecord && key in contextRecord
? (contextRecord[key] as V)
: cfg.defaultValue;
}
/**
* @experimental
*
* Read and delete cfg from this layer of context
*
* @param contextRecord The ContextRecord
* @param cfg The configuration
* @returns The value of the configuration that was removed
*/
export function popOwnContextValue<Ctx extends AnyContextSymbol, V>(
contextRecord: ContextRecord<Ctx>,
cfg: ContextConfig<Ctx, V>,
): undefined | V {
const rval = getOwnContextValue(contextRecord, cfg);
delete contextRecord[cfg.key];
return rval;
}
/**
* @experimental
*
* Get the value without a default
*
* @param contextRecord The ContextRecord
* @param cfg The configuration
* @returns The current value in this context or `undefined` if not set
*/
export function getOwnContextValue<Ctx extends AnyContextSymbol, V>(
contextRecord: ContextRecord<Ctx>,
cfg: ContextConfig<Ctx, V>,
): undefined | V {
const {key} = cfg;
return key in contextRecord ? (contextRecord[key] as V) : undefined;
}
function getEditorContext(editor: LexicalEditor): undefined | EditorContext {
return activeContext && activeContext.editor === editor
? activeContext
: undefined;
}
/**
* @experimental
*
* @param sym The symbol for this ContextRecord (e.g. DOMRenderContextSymbol)
* @param editor The editor
* @returns The current context or undefined
*/
export function getContextRecord<Ctx extends AnyContextSymbol>(
sym: Ctx,
editor: LexicalEditor,
): undefined | ContextRecord<Ctx> {
const editorContext = getEditorContext(editor);
return editorContext && editorContext[sym];
}
function toPair<Ctx extends AnyContextSymbol, V>(
contextRecord: undefined | ContextRecord<Ctx>,
pairOrUpdater: ContextConfigPair<Ctx, V> | ContextConfigUpdater<Ctx, V>,
): ContextConfigPair<Ctx, V> {
if ('cfg' in pairOrUpdater) {
const {cfg, updater} = pairOrUpdater;
return [cfg, updater(getContextValue(contextRecord, cfg))];
}
return pairOrUpdater;
}
/**
* Construct a new context from a parent context and pairs
*
* @param pairs The pairs and updaters to build the context from
* @param parent The parent context
* @returns The new context
*/
export function contextFromPairs<Ctx extends AnyContextSymbol>(
pairs: readonly AnyContextConfigPairOrUpdater<Ctx>[],
parent: undefined | ContextRecord<Ctx>,
): undefined | ContextRecord<Ctx> {
let rval = parent;
for (const pairOrUpdater of pairs) {
const [k, v] = toPair(rval, pairOrUpdater);
const key = k.key;
if (rval === parent && getContextValue(rval, k) === v) {
continue;
}
// If we haven't branched away from `parent` yet, create a fresh child
// context so we never mutate the caller's parent record. Subsequent
// pairs in this loop accumulate into the same child. Inside the loop
// `rval` is non-null after the first iteration, since createChildContext
// never returns null/undefined.
const ctx: ContextRecord<Ctx> =
rval === parent || rval === undefined ? createChildContext(parent) : rval;
ctx[key] = v;
rval = ctx;
}
return rval;
}
function createChildContext<Ctx extends AnyContextSymbol>(
parent: undefined | ContextRecord<Ctx>,
): ContextRecord<Ctx> {
return Object.create(parent || null);
}
/**
* Create a context config pair that sets a value in the render context.
* @experimental
*/
export function contextValue<Ctx extends AnyContextSymbol, V>(
cfg: ContextConfig<Ctx, V>,
value: V,
): ContextConfigPair<Ctx, V> {
return [cfg, value];
}
/**
* Create a context config updater that transforms a value in the render context.
* @experimental
*/
export function contextUpdater<Ctx extends AnyContextSymbol, V>(
cfg: ContextConfig<Ctx, V>,
updater: (prev: V) => V,
): ContextConfigUpdater<Ctx, V> {
return {cfg, updater};
}
/**
* @internal
* @experimental
* @__NO_SIDE_EFFECTS__
*/
export function $withFullContext<Ctx extends AnyContextSymbol, T>(
sym: Ctx,
contextRecord: ContextRecord<Ctx>,
f: () => T,
editor: LexicalEditor = $getEditor(),
): T {
const prevDOMContext = activeContext;
const parentEditorContext = getEditorContext(editor);
try {
activeContext = {...parentEditorContext, editor, [sym]: contextRecord};
return f();
} finally {
activeContext = prevDOMContext;
}
}
/**
* @internal
* @experimental
* @__NO_SIDE_EFFECTS__
*/
export function $withContext<Ctx extends AnyContextSymbol>(
sym: Ctx,
$defaults: (editor: LexicalEditor) => undefined | ContextRecord<Ctx> = () =>
undefined,
) {
return (
cfg: readonly AnyContextConfigPairOrUpdater<Ctx>[],
editor = $getEditor(),
): (<T>(f: () => T) => T) => {
return f => {
const parentEditorContext = getEditorContext(editor);
const parentContextRecord =
parentEditorContext && parentEditorContext[sym];
const contextRecord = contextFromPairs(
cfg,
parentContextRecord || $defaults(editor),
);
if (!contextRecord || contextRecord === parentContextRecord) {
return f();
}
return $withFullContext(sym, contextRecord, f, editor);
};
};
}
/**
* @experimental
* @internal
* @__NO_SIDE_EFFECTS__
*/
export function createContextState<Tag extends symbol, V>(
tag: Tag,
name: string,
getDefaultValue: () => V,
isEqual?: (a: V, b: V) => boolean,
): ContextConfig<Tag, V> {
return Object.assign(
createState(Symbol(name), {isEqual, parse: getDefaultValue}),
{[tag]: true} as const,
);
}
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import type {
AnyDOMRenderMatch,
DOMOverrideOptions,
DOMRenderMatch,
DOMRenderMatchConfig,
NodeMatch,
} from './types';
import type {LexicalNode} from 'lexical';
/**
* A convenience function for type inference when constructing DOM overrides for
* use with {@link DOMRenderExtension}.
*
* The optional `options` argument controls *whether* the override is installed
* based only on render context — `disabledForEditor` gates residency in the
* editor's render pipeline (reconciliation), `disabledForSession` gates
* participation in a single export/generate session. See {@link DOMOverrideOptions}.
*
* @experimental
* @__NO_SIDE_EFFECTS__
*/
export function domOverride(
nodes: '*',
config: DOMRenderMatchConfig<LexicalNode>,
options?: DOMOverrideOptions,
): DOMRenderMatch<LexicalNode>;
export function domOverride<T extends LexicalNode>(
nodes: readonly NodeMatch<T>[],
config: DOMRenderMatchConfig<T>,
options?: DOMOverrideOptions,
): DOMRenderMatch<T>;
export function domOverride(
nodes: AnyDOMRenderMatch['nodes'],
config: Omit<AnyDOMRenderMatch, 'nodes' | keyof DOMOverrideOptions>,
options?: DOMOverrideOptions,
): AnyDOMRenderMatch {
return {...config, ...options, nodes};
}
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import type {DOMRenderConfig, DOMRenderExtensionOutput} from './types';
import type {InitialEditorConfig} from 'lexical';
import {defineExtension, RootNode, shallowMergeConfig} from 'lexical';
import {compileDOMRenderConfigOverrides} from './compileDOMRenderConfigOverrides';
import {DOMRenderExtensionName} from './constants';
import {
createEditorContextRecord,
DOMRenderRuntimeImpl,
filterEditorInstalled,
} from './DOMRenderRuntime';
/** @internal The result returned from {@link DOMRenderExtension}'s `init`. */
interface DOMRenderInitResult {
/**
* The `nodes` and base `dom` captured from the editor config before `dom`
* is overwritten with the compiled config — the only fields the runtime
* needs to recompile.
*/
initialEditorConfig: Pick<InitialEditorConfig, 'nodes' | 'dom'>;
}
/**
* @experimental
*
* An extension that allows overriding the render and export behavior for an
* editor. This is highly experimental and subject to change from one version
* to the next.
**/
export const DOMRenderExtension = defineExtension<
DOMRenderConfig,
typeof DOMRenderExtensionName,
DOMRenderExtensionOutput,
DOMRenderInitResult
>({
build(editor, config, state) {
const {initialEditorConfig} = state.getInitResult();
const editorContext = createEditorContextRecord(config.contextDefaults);
const runtime = new DOMRenderRuntimeImpl(
editor,
initialEditorConfig,
config.overrides,
editorContext,
);
return {defaults: editorContext, runtime};
},
config: {
contextDefaults: [],
overrides: [],
},
html: {
// Define a RootNode export for $generateDOMFromRoot
export: new Map([
[
RootNode,
() => {
const element = document.createElement('div');
element.role = 'textbox';
return {element};
},
],
]),
},
init(editorConfig, config) {
// Capture the user's base `dom` (before we overwrite it) and `nodes` so the
// runtime can recompile from scratch when overrides toggle.
const initialEditorConfig: DOMRenderInitResult['initialEditorConfig'] = {
dom: editorConfig.dom,
nodes: editorConfig.nodes,
};
const editorContext = createEditorContextRecord(config.contextDefaults);
const installed = filterEditorInstalled(config.overrides, editorContext);
editorConfig.dom = compileDOMRenderConfigOverrides(editorConfig, {
overrides: installed,
});
return {initialEditorConfig};
},
mergeConfig(config, partial) {
const merged = shallowMergeConfig(config, partial);
for (const k of ['overrides', 'contextDefaults'] as const) {
if (partial[k]) {
(merged[k] as unknown[]) = [...config[k], ...partial[k]];
}
}
return merged;
},
name: DOMRenderExtensionName,
});
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import type {
AnyDOMRenderMatch,
AnyRenderStateConfigPairOrUpdater,
ContextRecord,
DOMRenderRuntime,
RenderContextReader,
RenderStateConfig,
} from './types';
import type {
EditorDOMRenderConfig,
InitialEditorConfig,
Klass,
LexicalEditor,
LexicalNode,
} from 'lexical';
import {
$fullReconcile,
$isLexicalNode,
DEFAULT_EDITOR_DOM_CONFIG,
} from 'lexical';
import {compileDOMRenderConfigOverrides} from './compileDOMRenderConfigOverrides';
import {DOMRenderContextSymbol} from './constants';
import {
contextFromPairs,
getContextRecord,
getContextValue,
} from './ContextRecord';
type RenderContextRecord = ContextRecord<typeof DOMRenderContextSymbol>;
function makeReader(record: RenderContextRecord): RenderContextReader {
return {
get<V>(cfg: RenderStateConfig<V>): V {
return getContextValue(record, cfg);
},
};
}
/**
* The mutable, writable editor-level context record. Reads of a render state
* during reconciliation (and as the base layer of a session) fall through to
* this record, and it is the layer the `disabledForEditor` predicates read.
*
* @internal
*/
export function createEditorContextRecord(
contextDefaults: readonly AnyRenderStateConfigPairOrUpdater[],
): RenderContextRecord {
const parent = Object.create(null) as RenderContextRecord;
return contextFromPairs(contextDefaults, parent) || parent;
}
/**
* Filter the configured overrides down to those that are resident in the
* editor's render config, removing any whose `disabledForEditor` predicate
* returns `true` for the given editor context.
*
* @internal
*/
export function filterEditorInstalled(
overrides: readonly AnyDOMRenderMatch[],
record: RenderContextRecord,
): AnyDOMRenderMatch[] {
const reader = makeReader(record);
return overrides.filter(
o => !(o.disabledForEditor && o.disabledForEditor(reader)),
);
}
function sameOverrides(
a: readonly AnyDOMRenderMatch[],
b: readonly AnyDOMRenderMatch[],
): boolean {
if (a.length !== b.length) {
return false;
}
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) {
return false;
}
}
return true;
}
function symmetricDiff(
prev: readonly AnyDOMRenderMatch[],
next: readonly AnyDOMRenderMatch[],
): AnyDOMRenderMatch[] {
const prevSet = new Set(prev);
const nextSet = new Set(next);
const changed: AnyDOMRenderMatch[] = [];
for (const o of prev) {
if (!nextSet.has(o)) {
changed.push(o);
}
}
for (const o of next) {
if (!prevSet.has(o)) {
changed.push(o);
}
}
return changed;
}
/**
* Build a predicate matching the nodes an override targets — `'*'` matches
* everything, a node class matches by `instanceof`, and a guard is used as-is.
*/
function nodeMatcher(o: AnyDOMRenderMatch): (node: LexicalNode) => boolean {
if (o.nodes === '*') {
return () => true;
}
const matchers = o.nodes.map(match => {
const klass = match as Klass<LexicalNode>;
return $isLexicalNode(klass.prototype)
? (node: LexicalNode) => node instanceof klass
: (match as (node: LexicalNode) => boolean);
});
return node => matchers.some(f => f(node));
}
/**
* Build a predicate matching the nodes whose DOM must be recreated for the
* given override change, or `null` when no live re-render is needed.
*
* `$createDOM`/`$getDOMSlot` produce the element and slot, and `$decorateDOM`
* may add DOM that only a fresh `$createDOM` can revert — so toggling any of
* them recreates the affected nodes. `$updateDOM` is diff-driven and applies on
* the next node update, and export-only hooks ($exportDOM/$shouldInclude/…)
* don't touch the live DOM, so neither needs a re-render. Recreating every
* affected node is the simple, always-correct choice; toggles are rare, so the
* cost is acceptable and can be optimized later if needed.
*/
function recreatePredicate(
changed: readonly AnyDOMRenderMatch[],
): ((node: LexicalNode) => boolean) | null {
const matchers: ((node: LexicalNode) => boolean)[] = [];
for (const o of changed) {
if (o.$createDOM || o.$getDOMSlot || o.$decorateDOM) {
matchers.push(nodeMatcher(o));
}
}
return matchers.length === 0 ? null : node => matchers.some(f => f(node));
}
/**
* Per-editor runtime backing {@link DOMRenderExtension}'s conditional
* overrides and imperative editor context. See {@link DOMRenderRuntime}.
*
* @internal
*/
export class DOMRenderRuntimeImpl implements DOMRenderRuntime {
readonly editor: LexicalEditor;
/**
* The `nodes` and base `dom` captured at `init` (before `dom` was
* overwritten with the compiled config) — the clean base for every recompile.
*/
readonly initialEditorConfig: Pick<InitialEditorConfig, 'nodes' | 'dom'>;
readonly overrides: readonly AnyDOMRenderMatch[];
readonly editorContext: RenderContextRecord;
readonly hasSessionGates: boolean;
installed: readonly AnyDOMRenderMatch[];
/** Memoized session configs keyed by the set of session-disabled overrides. */
private readonly sessionCache = new Map<string, EditorDOMRenderConfig>();
constructor(
editor: LexicalEditor,
initialEditorConfig: Pick<InitialEditorConfig, 'nodes' | 'dom'>,
overrides: readonly AnyDOMRenderMatch[],
editorContext: RenderContextRecord,
) {
this.editor = editor;
this.initialEditorConfig = initialEditorConfig;
this.overrides = overrides;
this.editorContext = editorContext;
this.installed = filterEditorInstalled(overrides, editorContext);
this.hasSessionGates = overrides.some(o => o.disabledForSession);
}
setContextValue<V>(cfg: RenderStateConfig<V>, value: V): void {
const prev = this.installed;
this.editorContext[cfg.key] = value;
const next = filterEditorInstalled(this.overrides, this.editorContext);
if (sameOverrides(prev, next)) {
return;
}
const changed = symmetricDiff(prev, next);
this.installed = next;
this.sessionCache.clear();
const dom = compileDOMRenderConfigOverrides(this.initialEditorConfig, {
overrides: next as AnyDOMRenderMatch[],
});
this.editor._config.dom = dom;
const recreate = recreatePredicate(changed);
if (!recreate) {
// $updateDOM-only or export-only change: the recompiled config is enough.
return;
}
// Re-render through a full reconcile, which reuses the existing node
// instances (no node-map mutation, so no spurious mutation/collaboration
// changes). The affected nodes must be unmounted and recreated — the removed
// override may have produced or decorated DOM that only a fresh $createDOM
// reverts — so install a transient $updateDOM that reports a recreate for
// matching nodes.
//
// This mutates the (shared) active config, so the reconcile MUST run and
// finish synchronously before the original is restored on the next line —
// hence `discrete`, and hence this must not be called from within an
// editor.update (where the commit would defer). A deferred update would
// either restore the wrapper before the reconcile reads it (no recreate) or
// leave it armed across a window where an unrelated reconcile would
// spuriously recreate matching nodes. No history tag is needed: a full
// reconcile marks no nodes dirty, which history merges/discards without
// pushing.
const base = dom.$updateDOM;
dom.$updateDOM = (nextNode, prevNode, el, editor) =>
recreate(nextNode) ? true : base(nextNode, prevNode, el, editor);
this.editor.update($fullReconcile, {discrete: true});
dom.$updateDOM = base;
}
getSessionConfig(): EditorDOMRenderConfig {
const resident = this.editor._config.dom || DEFAULT_EDITOR_DOM_CONFIG;
if (!this.hasSessionGates) {
return resident;
}
const reader = makeReader(
getContextRecord(DOMRenderContextSymbol, this.editor) ||
this.editorContext,
);
const disabledKeys: string[] = [];
const sessionSet: AnyDOMRenderMatch[] = [];
this.installed.forEach((o, i) => {
if (o.disabledForSession && o.disabledForSession(reader)) {
disabledKeys.push(String(i));
} else {
sessionSet.push(o);
}
});
if (disabledKeys.length === 0) {
return resident;
}
const key = disabledKeys.join(',');
let cfg = this.sessionCache.get(key);
if (!cfg) {
cfg = compileDOMRenderConfigOverrides(this.initialEditorConfig, {
overrides: sessionSet,
});
this.sessionCache.set(key, cfg);
}
return cfg;
}
}
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import type {AnyDOMImportRule, DOMImportFn} from './types';
import {isDOMTextNode, isHTMLElement} from 'lexical';
import {getSelectorImpl, type Predicate, type SelectorImpl} from './sel';
const __DEV__ = process.env.NODE_ENV !== 'production';
/** @internal */
export interface CompiledRule {
readonly name: string;
readonly predicate: Predicate;
readonly $import: DOMImportFn<Node, Record<string, RegExpMatchArray>>;
}
/** @internal */
export interface CompiledDispatch {
/** All rules in registration order. Index = registration order. */
readonly rules: readonly CompiledRule[];
/**
* For each (uppercased) HTML tag name, the ordered list of rule indices
* considered when dispatching that tag. Includes interleaved wildcard
* element rules so a single iteration handles both.
*/
readonly byTag: ReadonlyMap<string, readonly number[]>;
/** Indices of rules whose match has no tag restriction. */
readonly wildcardIndices: readonly number[];
/** Indices of rules whose match is `sel.text()`. */
readonly textIndices: readonly number[];
/** Indices of rules whose match is `sel.comment()`. */
readonly commentIndices: readonly number[];
}
function mergeSortedAsc(a: readonly number[], b: readonly number[]): number[] {
const out: number[] = [];
let i = 0;
let j = 0;
while (i < a.length && j < b.length) {
if (a[i] <= b[j]) {
out.push(a[i++]);
} else {
out.push(b[j++]);
}
}
while (i < a.length) {
out.push(a[i++]);
}
while (j < b.length) {
out.push(b[j++]);
}
return out;
}
/**
* Compile an ordered list of {@link DOMImportRule}s into the dispatch tables
* used by the import runtime. The rule at index 0 is the highest-priority
* (`mergeConfig` prepends partial.rules so later-merged extensions land
* first).
*
* @internal
*/
export function compileImportRules(
rules: readonly AnyDOMImportRule[],
): CompiledDispatch {
const compiled: CompiledRule[] = [];
const byTag = new Map<string, number[]>();
const wildcardIndices: number[] = [];
const textIndices: number[] = [];
const commentIndices: number[] = [];
const seenNames = new Set<string>();
rules.forEach((rule, i) => {
const sel = getSelectorImpl(rule.match);
const name = rule.name || defaultRuleName(sel, i);
if (__DEV__ && typeof rule.name === 'string' && seenNames.has(rule.name)) {
console.warn(
`[lexical] duplicate DOMImportRule name "${rule.name}" — keep names unique to aid debugging.`,
);
}
if (rule.name) {
seenNames.add(rule.name);
}
compiled.push({
$import: rule.$import as DOMImportFn<
Node,
Record<string, RegExpMatchArray>
>,
name,
predicate: sel.predicate,
});
if (sel.kind === 'text') {
textIndices.push(i);
} else if (sel.kind === 'comment') {
commentIndices.push(i);
} else if (sel.tags.size === 0) {
wildcardIndices.push(i);
} else {
for (const tag of sel.tags) {
let list = byTag.get(tag);
if (!list) {
list = [];
byTag.set(tag, list);
}
list.push(i);
}
}
});
// Interleave wildcard-element indices into each tag's list in registration
// (ascending-index) order, so iterating a tag bucket visits both tag-
// specific and wildcard rules in the same priority sequence.
const finalByTag = new Map<string, readonly number[]>();
if (wildcardIndices.length === 0) {
for (const [tag, list] of byTag) {
finalByTag.set(tag, list);
}
} else {
for (const [tag, list] of byTag) {
finalByTag.set(tag, mergeSortedAsc(list, wildcardIndices));
}
}
return {
byTag: finalByTag,
commentIndices,
rules: compiled,
textIndices,
wildcardIndices,
};
}
function defaultRuleName(sel: SelectorImpl, index: number): string {
if (sel.kind === 'text') {
return `#text@${index}`;
}
if (sel.kind === 'comment') {
return `#comment@${index}`;
}
if (sel.tags.size === 0) {
return `*@${index}`;
}
const tagList = Array.from(sel.tags).join(',').toLowerCase();
return `${tagList}@${index}`;
}
/**
* Look up the (already interleaved) rule indices relevant to `node`. Element
* nodes hit `byTag` (with wildcards merged in) or fall back to the wildcard
* bucket if no tag-specific rules exist; text and comment nodes use their
* own buckets.
*
* @internal
*/
export function getDispatchIndices(
dispatch: CompiledDispatch,
node: Node,
): readonly number[] {
if (isDOMTextNode(node)) {
return dispatch.textIndices;
}
if (node.nodeType === 8 /* COMMENT_NODE */) {
return dispatch.commentIndices;
}
if (isHTMLElement(node)) {
return dispatch.byTag.get(node.nodeName) || dispatch.wildcardIndices;
}
return EMPTY_INDICES;
}
const EMPTY_INDICES: readonly number[] = Object.freeze([]);
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import {configExtension, defineExtension} from 'lexical';
import {CoreImportRules} from './coreImportRules';
import {DOMImportExtension} from './DOMImportExtension';
/**
* Bundles {@link CoreImportRules} into a {@link DOMImportExtension}-aware
* extension. Depend on this from your editor (directly or via richer
* extensions like `RichTextImportExtension`) to get the equivalent of the
* legacy core `importDOM` behavior for `<p>`, `<span>`, `<b>`,
* `<strong>`, `<em>`, `<i>`, `<code>`, `<mark>`, `<s>`, `<sub>`, `<sup>`,
* `<u>`, `<br>`, and `#text`.
*
* @experimental
*/
export const CoreImportExtension = defineExtension({
dependencies: [configExtension(DOMImportExtension, {rules: CoreImportRules})],
name: '@lexical/html/CoreImport',
});
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import {
$createLineBreakNode,
$createParagraphNode,
$createTextNode,
$generateNodesFromRawText,
$isTextNode,
$setDirectionFromDOM,
$setFormatFromDOM,
type ElementFormatType,
IS_BOLD,
IS_CODE,
IS_HIGHLIGHT,
IS_ITALIC,
IS_STRIKETHROUGH,
IS_SUBSCRIPT,
IS_SUPERSCRIPT,
IS_UNDERLINE,
isDOMTextNode,
type LexicalNode,
setNodeIndentFromDOM,
} from 'lexical';
import {contextValue} from '../ContextRecord';
import {defineImportRule} from './defineImportRule';
import {
ImportTextFormat,
ImportTextStyle,
ImportWhitespaceConfig,
type WhitespaceImportConfig,
} from './ImportContext';
import {selBase} from './sel';
const sel = selBase;
const ALIGNMENT_VALUES: ReadonlySet<string> = new Set<ElementFormatType>([
'center',
'end',
'justify',
'left',
'right',
'start',
]);
/**
* True if `value` is a non-empty {@link ElementFormatType} (matches one of
* the supported `text-align` / legacy `align`-attribute values).
*
* @internal
*/
export function isAlignmentValue(value: string): value is ElementFormatType {
return ALIGNMENT_VALUES.has(value);
}
/**
* A pair of bitmasks describing which {@link TextFormatType} bits to set
* and which to clear when descending into an element. The clear pass
* matters for cases the legacy OR-merge mishandled, e.g. `<b
* style="font-weight: normal">` clearing an inherited bold, or `<sub>` /
* `<sup>` clearing each other.
*/
interface FormatOverride {
readonly set: number;
readonly clear: number;
}
/**
* The small subset of inline-style properties that affect text formatting
* during import. Modeled as a plain object so tag-implicit defaults and
* the element's own inline `style` can be merged with `{...defaults,
* ...override-if-set}` semantics rather than relying on CSSStyleDeclaration.
*/
interface FormatStyle {
fontWeight?: string;
fontStyle?: string;
textDecoration?: string;
verticalAlign?: string;
}
/**
* Default style implied by each inline format tag. `<b>`/`<strong>` set
* font-weight, `<sub>` sets vertical-align, etc. Any of these can be
* overridden by the element's own inline `style` (so `<b
* style="font-weight: normal">` ends up with `fontWeight: 'normal'` in
* the effective style).
*/
const TAG_DEFAULT_STYLE: Record<string, FormatStyle> = {
B: {fontWeight: 'bold'},
EM: {fontStyle: 'italic'},
I: {fontStyle: 'italic'},
S: {textDecoration: 'line-through'},
STRONG: {fontWeight: 'bold'},
SUB: {verticalAlign: 'sub'},
SUP: {verticalAlign: 'super'},
U: {textDecoration: 'underline'},
};
/**
* Tags whose effect on TextFormat has no CSS analog (so the style-merge
* path can't reach them). Applied as a pure "set" override.
*/
const TAG_ONLY_SET: Record<string, number> = {
CODE: IS_CODE,
MARK: IS_HIGHLIGHT,
};
function readElementFormatStyle(el: HTMLElement): FormatStyle {
return {
fontStyle: el.style.fontStyle,
fontWeight: el.style.fontWeight,
textDecoration: el.style.textDecoration,
verticalAlign: el.style.verticalAlign,
};
}
function mergeStyles(
defaults: FormatStyle,
override: FormatStyle,
): FormatStyle {
return {
fontStyle: override.fontStyle || defaults.fontStyle,
fontWeight: override.fontWeight || defaults.fontWeight,
textDecoration: override.textDecoration || defaults.textDecoration,
verticalAlign: override.verticalAlign || defaults.verticalAlign,
};
}
/**
* The CSS property names {@link styleFormatOverride} reads — these are
* "owned" by {@link ImportTextFormat} (the bit mask). When the
* {@link ImportTextStyle} record is materialized onto a TextNode's
* inline style by {@link styleObjectToCSS}, these are skipped so the
* bit-mask side is the single source of truth and the same property
* doesn't end up in both places (where the inline-style version would
* shadow the format's themed CSS).
*/
const FORMAT_BIT_STYLE_PROPS: ReadonlySet<string> = new Set([
'font-weight',
'font-style',
'text-decoration',
'vertical-align',
]);
/**
* Translate a {@link FormatStyle} into a {@link FormatOverride}. Explicit
* "non-decorating" values (`font-weight: normal`, `text-decoration: none`,
* `vertical-align: baseline`) produce `clear` bits, so an inner element
* can remove a format inherited from its ancestors.
*/
function styleFormatOverride(style: FormatStyle): FormatOverride {
let set = 0;
let clear = 0;
const {fontWeight, fontStyle, textDecoration, verticalAlign} = style;
if (fontWeight === '700' || fontWeight === 'bold') {
set |= IS_BOLD;
} else if (fontWeight === 'normal' || fontWeight === '400') {
clear |= IS_BOLD;
}
if (fontStyle === 'italic') {
set |= IS_ITALIC;
} else if (fontStyle === 'normal') {
clear |= IS_ITALIC;
}
if (textDecoration) {
const parts = textDecoration.split(' ');
if (parts.includes('underline')) {
set |= IS_UNDERLINE;
}
if (parts.includes('line-through')) {
set |= IS_STRIKETHROUGH;
}
if (parts.includes('none')) {
clear |= IS_UNDERLINE | IS_STRIKETHROUGH;
}
}
if (verticalAlign === 'sub') {
set |= IS_SUBSCRIPT;
clear |= IS_SUPERSCRIPT;
} else if (verticalAlign === 'super') {
set |= IS_SUPERSCRIPT;
clear |= IS_SUBSCRIPT;
} else if (verticalAlign === 'baseline') {
clear |= IS_SUBSCRIPT | IS_SUPERSCRIPT;
}
return {clear, set};
}
function applyFormatOverride(format: number, ov: FormatOverride): number {
return (format & ~ov.clear) | ov.set;
}
/**
* Unified rule for inline-format-bearing tags and `<span>`. The element's
* effective style is its tag's {@link TAG_DEFAULT_STYLE} merged with its
* inline `style` (element's own style wins for any property it sets), and
* the resulting style is translated into a {@link FormatOverride}. Tags
* with no CSS analog (`<code>`, `<mark>`) contribute their bit as a pure
* `set` override.
*
* This shape lets:
* - `<b style="font-weight: normal">` clear an inherited IS_BOLD.
* - `<sub><sup>x</sup></sub>` resolve to IS_SUPERSCRIPT only (sub/sup
* mutex via the vertical-align clear logic).
* - `<span style="text-decoration: none">` strip inherited underline /
* line-through.
*/
const InlineFormatRule = defineImportRule({
$import: (ctx, el) => {
const inherited = ctx.get(ImportTextFormat);
const tagDefault = TAG_DEFAULT_STYLE[el.nodeName];
const elStyle = readElementFormatStyle(el);
const effective = tagDefault ? mergeStyles(tagDefault, elStyle) : elStyle;
let merged = applyFormatOverride(inherited, styleFormatOverride(effective));
const tagOnly = TAG_ONLY_SET[el.nodeName];
if (tagOnly) {
merged |= tagOnly;
}
if (merged === inherited) {
return ctx.$importChildren(el);
}
return ctx.$importChildren(el, {
context: [contextValue(ImportTextFormat, merged)],
});
},
match: sel.tag(
'b',
'strong',
'em',
'i',
'code',
'mark',
's',
'sub',
'sup',
'u',
'span',
),
name: '@lexical/html/inline-format',
});
/**
* Walk up the DOM ancestor chain to determine whether `node` is inside an
* element whose whitespace should be preserved, per the supplied
* {@link WhitespaceImportConfig.preservesWhitespace} predicate. Pure
* ancestor walk, no caching.
*/
function isInsidePreserveWhitespace(
node: Node,
wsConfig: WhitespaceImportConfig,
): boolean {
let current: Node | null = node.parentNode;
while (current !== null) {
if (wsConfig.preservesWhitespace(current)) {
return true;
}
current = current.parentNode;
}
return false;
}
function findAdjacentTextOnLine(
text: Text,
forward: boolean,
wsConfig: WhitespaceImportConfig,
): Text | null {
let node: Node = text;
while (true) {
let sibling: Node | null = null;
while (
(sibling = forward ? node.nextSibling : node.previousSibling) === null
) {
const parent: Node | null = node.parentNode;
if (parent === null) {
return null;
}
node = parent;
}
node = sibling;
if (!wsConfig.isInline(node)) {
return null;
}
let descendant: Node | null = node;
while ((descendant = forward ? node.firstChild : node.lastChild) !== null) {
node = descendant;
}
if (isDOMTextNode(node)) {
return node;
}
if (node.nodeName === 'BR') {
return null;
}
}
}
function collapseWhitespace(
textNode: Text,
wsConfig: WhitespaceImportConfig,
): string {
let textContent = (textNode.textContent || '')
.replace(/\r/g, '')
.replace(/[ \t\n]+/g, ' ');
if (textContent.length === 0) {
return '';
}
if (textContent[0] === ' ') {
let neighbor: Text | null = textNode;
let isStartOfLine = true;
while (
neighbor !== null &&
(neighbor = findAdjacentTextOnLine(neighbor, false, wsConfig)) !== null
) {
const neighborContent = neighbor.textContent || '';
if (neighborContent.length > 0) {
if (/[ \t\n]$/.test(neighborContent)) {
textContent = textContent.slice(1);
}
isStartOfLine = false;
break;
}
}
if (isStartOfLine) {
textContent = textContent.slice(1);
}
}
if (textContent.length > 0 && textContent[textContent.length - 1] === ' ') {
let neighbor: Text | null = textNode;
let isEndOfLine = true;
while (
neighbor !== null &&
(neighbor = findAdjacentTextOnLine(neighbor, true, wsConfig)) !== null
) {
const neighborContent = (neighbor.textContent || '').replace(
/^( |\t|\r?\n)+/,
'',
);
if (neighborContent.length > 0) {
isEndOfLine = false;
break;
}
}
if (isEndOfLine) {
textContent = textContent.slice(0, -1);
}
}
return textContent;
}
function $applyFormat(node: LexicalNode, format: number): LexicalNode {
return format !== 0 && $isTextNode(node) ? node.setFormat(format) : node;
}
/**
* Inverse of {@link getStyleObjectFromCSS}: serialize a parsed style
* record back into a CSS declaration string suitable for
* `TextNode.setStyle`. Returns the empty string for an empty record.
*/
function styleObjectToCSS(style: Readonly<Record<string, string>>): string {
let css = '';
for (const prop in style) {
if (FORMAT_BIT_STYLE_PROPS.has(prop)) {
// Owned by ImportTextFormat (bit mask) — skip so the format-bit
// CSS is the single source of truth on the rendered TextNode.
continue;
}
css += `${prop}: ${style[prop]}; `;
}
return css.trimEnd();
}
function $applyTextStyle(
node: LexicalNode,
style: Readonly<Record<string, string>>,
): LexicalNode {
if ($isTextNode(node)) {
const css = styleObjectToCSS(style);
if (css !== '') {
node.setStyle(css);
}
}
return node;
}
/**
* `#text` rule. Inside a `<pre>` ancestor, preserve whitespace and split
* on `\n` and `\t` into `LineBreakNode`/`TabNode` siblings. Otherwise
* collapse whitespace using the same neighbor-aware rules as the legacy
* `$convertTextDOMNode`.
*/
const TextRule = defineImportRule({
$import: (ctx, el) => {
const format = ctx.get(ImportTextFormat);
const style = ctx.get(ImportTextStyle);
const wsConfig = ctx.get(ImportWhitespaceConfig);
if (isInsidePreserveWhitespace(el, wsConfig)) {
const out = $generateNodesFromRawText(el.textContent || '');
for (const node of out) {
$applyFormat(node, format);
$applyTextStyle(node, style);
}
return out;
}
const collapsed = collapseWhitespace(el, wsConfig);
if (collapsed === '') {
return [];
}
const text = $createTextNode(collapsed);
$applyFormat(text, format);
$applyTextStyle(text, style);
return [text];
},
match: sel.text(),
name: '@lexical/html/#text',
});
/**
* Drop `<style>` and `<script>` and skip descending into them — matches
* the legacy `IGNORE_TAGS` set, but as a regular rule so apps can register
* a higher-priority `<style>` rule to capture stylesheet text into the
* import session for later use.
*/
const IgnoreScriptStyleRule = defineImportRule({
$import: () => [],
match: sel.tag('script', 'style'),
name: '@lexical/html/script-style-ignore',
});
const LineBreakRule = defineImportRule({
$import: () => [$createLineBreakNode()],
match: sel.tag('br'),
name: '@lexical/html/br',
});
/**
* `<p>` rule. Re-applies format, indent, direction, and the legacy
* `align` attribute fallback.
*/
const ParagraphRule = defineImportRule({
$import: (ctx, el) => {
const p = $createParagraphNode();
$setFormatFromDOM(p, el);
setNodeIndentFromDOM(el, p);
if (p.getFormatType() === '') {
const align = el.getAttribute('align');
if (align && isAlignmentValue(align)) {
p.setFormat(align);
}
}
$setDirectionFromDOM(p, el);
// We deliberately pass no schema: paragraphs accept any inline run as-is.
// The enclosing context (root / block) is responsible for ensuring the
// paragraph itself is a valid block child.
return [p.splice(0, 0, ctx.$importChildren(el))];
},
match: sel.tag('p'),
name: '@lexical/html/p',
});
/**
* Rules covering the {@link ParagraphNode}, {@link TextNode},
* {@link LineBreakNode}, and {@link TabNode} cases that the legacy
* `importDOM` machinery in `@lexical/lexical` handled. Intended to be
* registered as a dependency of every editor that uses
* {@link DOMImportExtension}.
*
* @experimental
*/
export const CoreImportRules = [
IgnoreScriptStyleRule,
ParagraphRule,
TextRule,
LineBreakRule,
InlineFormatRule,
];
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import type {CompiledSelector, DOMImportFn, DOMImportRule} from './types';
/**
* Identity helper that infers a rule's matched node type and capture map
* from its `match` selector and threads them into the `$import` signature.
* Usage:
*
* ```ts
* defineImportRule({
* name: '@lexical/list/li',
* match: sel.tag('li'),
* $import: (ctx, el, $next) => {
* // el: HTMLLIElement
* return [$createListItemNode()];
* },
* });
* ```
*
* @experimental
* @__NO_SIDE_EFFECTS__
*/
export function defineImportRule<const S extends CompiledSelector>(rule: {
readonly name?: string;
readonly match: S;
readonly $import: DOMImportFn<
S extends CompiledSelector<infer N, Record<string, RegExpMatchArray>>
? N
: Node,
S extends CompiledSelector<Node, infer C> ? C : Record<string, never>
>;
}): DOMImportRule<S> {
return rule as DOMImportRule<S>;
}
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import type {AnyDOMImportRule} from './types';
import {type CompiledDispatch, compileImportRules} from './compileImportRules';
/**
* Opaque handle for a pre-compiled set of overlay rules. Produce one with
* {@link defineOverlayRules} and pass it to
* {@link DOMImportContext.$importChildren} via
* {@link ImportChildrenOpts.rules}.
*
* To merge two or more overlays into a single one, pass them (alongside
* raw {@link DOMImportRule}s if desired) to a fresh
* {@link defineOverlayRules} — earlier arguments are higher priority.
*
* The internal shape is intentionally not part of the public API: it's a
* compiled dispatch table tagged with `__type` so callers cannot pass a
* raw rule array where a compiled overlay is expected.
*
* @experimental
*/
export interface CompiledOverlayRules {
readonly __type: 'CompiledOverlayRules';
/** @internal */
readonly dispatch: CompiledDispatch;
/**
* @internal — flattened source rules retained so an overlay can be
* recompiled when it is passed to another {@link defineOverlayRules}
* call or as part of {@link DOMImportConfig.rules}.
*/
readonly rules: readonly AnyDOMImportRule[];
}
/**
* An entry accepted everywhere rules are configured (overlay
* definitions, {@link DOMImportConfig.rules}). Either a single
* {@link DOMImportRule} or a {@link CompiledOverlayRules} produced by
* a previous {@link defineOverlayRules} call — passing the latter
* inlines the overlay's rules at this position in priority order.
*
* @experimental
*/
export type DOMImportRuleEntry = AnyDOMImportRule | CompiledOverlayRules;
/** @internal */
export function flattenRuleEntries(
entries: readonly DOMImportRuleEntry[],
): AnyDOMImportRule[] {
const out: AnyDOMImportRule[] = [];
for (const entry of entries) {
if (isCompiledOverlayRules(entry)) {
for (const r of entry.rules) {
out.push(r);
}
} else {
out.push(entry);
}
}
return out;
}
function isCompiledOverlayRules(
entry: DOMImportRuleEntry,
): entry is CompiledOverlayRules {
return (
typeof entry === 'object' &&
entry !== null &&
'__type' in entry &&
entry.__type === 'CompiledOverlayRules'
);
}
/**
* Pre-compile a set of {@link DOMImportRuleEntry}s into a
* {@link CompiledOverlayRules} handle that can be installed via
* `ctx.$importChildren(el, {rules: …})`.
*
* Entries can be raw {@link DOMImportRule}s or other
* {@link CompiledOverlayRules} (the latter are inlined at their
* position in priority order, so the same call composes any number of
* overlays). Earlier entries are higher priority.
*
* Overlay rules installed as a raw array would be re-compiled on every
* `$importChildren` call. For overlays that are reused (e.g. a GitHub
* code-table rule that wraps every matching table), call this once at
* module scope so the dispatch table is built up front.
*
* @experimental
*/
export function defineOverlayRules(
entries: readonly DOMImportRuleEntry[],
): CompiledOverlayRules {
const rules = flattenRuleEntries(entries);
return {
__type: 'CompiledOverlayRules',
dispatch: compileImportRules(rules),
rules,
};
}
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import type {ContextRecord} from '../types';
import type {DOMImportRuleEntry} from './defineOverlayRules';
import type {
DOMImportExtensionOutput,
DOMPreprocessContext,
DOMPreprocessFn,
GenerateNodesFromDOMOptions,
ImportContextPairOrUpdater,
} from './types';
import {$getExtensionOutput} from '@lexical/extension';
import {defineExtension, type LexicalNode, shallowMergeConfig} from 'lexical';
import {DOMImportContextSymbol, DOMImportExtensionName} from '../constants';
import {$withFullContext, contextFromPairs} from '../ContextRecord';
import {type CompiledDispatch, compileImportRules} from './compileImportRules';
import {defineImportRule} from './defineImportRule';
import {flattenRuleEntries} from './defineOverlayRules';
import {ImportSessionImpl} from './ImportContext';
import {$inlineStylesFromStyleSheets} from './inlineStylesFromStyleSheets';
import {$runImport} from './runImport';
import {selBase} from './sel';
/**
* Configuration for {@link DOMImportExtension}.
*
* @experimental
*/
export interface DOMImportConfig {
/**
* The set of rules contributed by this extension and its dependencies.
* Entries can be raw {@link DOMImportRule}s or a
* {@link CompiledOverlayRules} produced by {@link defineOverlayRules}
* (the latter is inlined in priority order — useful for libraries
* that already publish a compiled overlay).
*
* Rules are dispatched in priority order: rules contributed by
* extensions merged later (i.e. closer to the editor root) run first
* and may call `$next()` to delegate to lower-priority rules.
*
* `mergeConfig` prepends `partial.rules` to existing `rules`, so later
* configuration carries higher priority.
*/
readonly rules: readonly DOMImportRuleEntry[];
/**
* Default context pairs applied to every `$generateNodesFromDOM` call.
* Per-call overrides can be supplied via
* {@link GenerateNodesFromDOMOptions.context}.
*/
readonly contextDefaults: readonly ImportContextPairOrUpdater[];
/**
* Functions run in order on the DOM before walking begins, mutating in
* place. The default config registers
* {@link $inlineStylesFromStyleSheets} (resolves `<style>` rules to
* inline styles so the rules' style-driven matchers see them); apps
* append additional preprocessors (e.g. strip unsafe elements,
* normalize attributes, resolve relative URLs).
*
* `mergeConfig` appends, so each contributing extension's preprocessors
* run in dependency order. Per-call preprocessors registered via
* {@link GenerateNodesFromDOMOptions.preprocess} run AFTER these.
*/
readonly preprocess: readonly DOMPreprocessFn[];
}
/**
* Drive a stack of {@link DOMPreprocessFn}s top-to-bottom: the highest-
* index fn runs first and may call `$next()` to defer to the next-lower
* one. Matches the export-side `callExportMimeTypeFunctionStack` shape.
*/
function $runPreprocessStack(
stack: readonly DOMPreprocessFn[],
dom: Document | ParentNode,
ctx: DOMPreprocessContext,
): void {
let i = stack.length - 1;
const $next = () => {
while (i >= 0) {
const cur = stack[i--];
cur(dom, ctx, $next);
return;
}
};
$next();
}
/**
* Lowest-priority catch-all rule used as the default `config.rules` entry
* for {@link DOMImportExtension}: descends into the element's children
* and returns whatever they produced. With no other matching rule, an
* element vanishes and its contents are inserted in its place — the
* legacy `$createNodesFromDOM` hoisting behavior, but now expressed as a
* regular rule that apps can override (e.g. with a `sel.any()` rule that
* captures and discards unknown elements).
*
* @experimental
*/
export const DefaultHoistRule = defineImportRule({
$import: (ctx, el) => ctx.$importChildren(el),
match: selBase.any(),
name: '@lexical/html/default-hoist',
});
/**
* @experimental
*
* Extension-based replacement for the legacy `importDOM` / `DOMConversion`
* machinery. Rules are contributed via configuration (see
* {@link DOMImportConfig.rules}), compiled into a tag-bucketed dispatcher at
* editor build time, and consumed via the extension's
* {@link DOMImportExtensionOutput.$generateNodesFromDOM} output.
*
* The legacy `$generateNodesFromDOM` continues to work in parallel; the
* intent is to migrate node packages over to this extension incrementally.
*/
export const DOMImportExtension = defineExtension<
DOMImportConfig,
typeof DOMImportExtensionName,
DOMImportExtensionOutput,
void
>({
build(editor, config) {
const dispatch: CompiledDispatch = compileImportRules(
flattenRuleEntries(config.rules),
);
const defaults = contextFromPairs(config.contextDefaults, undefined);
const configPreprocess = config.preprocess;
return {
$generateNodesFromDOM: (
dom: Document | ParentNode,
options?: GenerateNodesFromDOMOptions,
) => {
// The session record IS the root layer of the walk's context.
// Start with per-call options.context applied on top of the
// editor's contextDefaults, then ensure we have a *fresh*
// mutable child (never the shared defaults record) so
// session.set writes never leak into the editor's config.
const fromOpts =
options && options.context
? contextFromPairs(options.context, defaults)
: defaults;
const sessionRecord: ContextRecord<typeof DOMImportContextSymbol> =
fromOpts !== undefined && fromOpts !== defaults
? fromOpts
: Object.create(defaults || null);
const session = new ImportSessionImpl(sessionRecord);
const preprocessCtx: DOMPreprocessContext = {session};
// Stack of preprocessors: config-level first, then per-call.
// Top of stack (last in array) runs first; `next()` defers to
// the next-lower one. Matches the GetClipboardDataExtension
// convention so app-registered preprocessors can wrap built-in
// ones via `next()`. Preprocess writes via `ctx.session.set`
// mutate the session record directly.
const stack: readonly DOMPreprocessFn[] =
options && options.preprocess
? [...configPreprocess, ...options.preprocess]
: configPreprocess;
$runPreprocessStack(stack, dom, preprocessCtx);
return $withFullContext(
DOMImportContextSymbol,
sessionRecord,
() => $runImport(dispatch, editor, dom, session),
editor,
);
},
defaults,
};
},
config: {
contextDefaults: [],
preprocess: [$inlineStylesFromStyleSheets],
rules: [DefaultHoistRule],
},
mergeConfig(config, partial) {
return shallowMergeConfig(config, {
...partial,
...(partial.contextDefaults && {
contextDefaults: [
...config.contextDefaults,
...partial.contextDefaults,
],
}),
...(partial.preprocess && {
preprocess: [...config.preprocess, ...partial.preprocess],
}),
...(partial.rules && {
rules: [...partial.rules, ...config.rules],
}),
});
},
name: DOMImportExtensionName,
});
/**
* Look up the editor's {@link DOMImportExtension} and run its
* `$generateNodesFromDOM`. Designed as a drop-in replacement for the
* legacy `$generateNodesFromDOM(editor, dom)` signature so it can be
* supplied to `ClipboardImportExtension.$generateNodesFromDOM` (or any
* other consumer that wants to route through the extension pipeline).
*
* Throws if the editor was not built with {@link DOMImportExtension} as a
* dependency.
*
* @experimental
*/
export function $generateNodesFromDOMViaExtension(
dom: Document | ParentNode,
options?: GenerateNodesFromDOMOptions,
): LexicalNode[] {
return $getExtensionOutput(DOMImportExtension).$generateNodesFromDOM(
dom,
options,
);
}
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import {
$createHorizontalRuleNode,
HorizontalRuleExtension,
} from '@lexical/extension';
import {configExtension, defineExtension} from 'lexical';
import {CoreImportExtension} from './CoreImportExtension';
import {defineImportRule} from './defineImportRule';
import {DOMImportExtension} from './DOMImportExtension';
import {selBase} from './sel';
const HorizontalRuleRule = defineImportRule({
$import: () => [$createHorizontalRuleNode()],
match: selBase.tag('hr'),
name: '@lexical/html/hr',
});
/**
* Import rules for {@link HorizontalRuleNode}.
*
* @experimental
*/
export const HorizontalRuleImportRules = [HorizontalRuleRule];
/**
* Bundles {@link HorizontalRuleImportRules} (plus
* {@link CoreImportExtension}) into a single dependency. The legacy
* {@link HorizontalRuleExtension.importDOM} continues to work in parallel;
* depend on this extension to opt into the new pipeline.
*
* Lives in `@lexical/html` (not `@lexical/extension`) because
* {@link DOMImportExtension} itself is in `@lexical/html`, and
* `@lexical/extension` is upstream of `@lexical/html` in the dependency
* graph — same arrangement as {@link CoreImportExtension}.
*
* @experimental
*/
export const HorizontalRuleImportExtension = defineExtension({
dependencies: [
CoreImportExtension,
HorizontalRuleExtension,
configExtension(DOMImportExtension, {rules: HorizontalRuleImportRules}),
],
name: '@lexical/html/HorizontalRuleImport',
});
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import type {ContextRecord} from '../types';
import type {CompiledOverlayRules} from './defineOverlayRules';
import type {DOMImportExtension} from './DOMImportExtension';
import type {
ImportContextPairOrUpdater,
ImportSession,
ImportStateConfig,
} from './types';
import {getPeerDependencyFromEditor} from '@lexical/extension';
import {
$getEditor,
isBlockDomNode,
isDOMTextNode,
isHTMLElement,
isInlineDomNode,
type LexicalEditor,
} from 'lexical';
import {DOMImportContextSymbol, DOMImportExtensionName} from '../constants';
import {
$withContext,
createContextState,
getContextRecord,
getContextValue,
} from '../ContextRecord';
type ImportContextRecord = ContextRecord<typeof DOMImportContextSymbol>;
/**
* Create an import context state. The phantom symbol prevents accidental
* use of a render-context state in an import context (and vice versa).
*
* Note: to support the value-or-updater pattern, `V` cannot be a function
* type; wrap it in an array or object if needed.
*
* `getDefaultValue` is called **once at state creation** and the result is
* shared between every session that reads the state without first writing
* a value. Defaults must therefore be immutable (primitives, frozen
* objects, or read-only arrays / records). If your state needs mutable
* per-session storage, lazily initialize it inside your rule (e.g.
* `if (!ctx.session.has(cfg)) ctx.session.set(cfg, new …())`).
*
* @experimental
* @__NO_SIDE_EFFECTS__
*/
export function createImportState<V>(
name: string,
getDefaultValue: () => V,
isEqual?: (a: V, b: V) => boolean,
): ImportStateConfig<V> {
return createContextState(
DOMImportContextSymbol,
name,
getDefaultValue,
isEqual,
);
}
/**
* The kind of operation that produced this import. Lets rules adapt
* their behavior (e.g. preserve more whitespace on `'paste'`).
* Defaults to `'unknown'`. Apps that need a different vocabulary can
* define their own {@link ImportStateConfig} with whatever value type
* they want.
*
* @experimental
*/
export type ImportSourceKind = 'paste' | 'unknown';
/**
* Built-in import-context state identifying how this import was initiated.
* Callers of `$generateNodesFromDOM` should set it via the `context` option.
*
* @experimental
*/
export const ImportSource: ImportStateConfig<ImportSourceKind> =
createImportState<ImportSourceKind>('importSource', () => 'unknown');
/**
* Built-in import-context state holding the {@link DataTransfer} the
* import was sourced from, if any. `null` outside paste/drop flows.
*
* The clipboard import pipeline passes the original `DataTransfer`
* through to its per-MIME-type handler stack (see
* {@link ImportMimeTypeFunction}); handlers that route HTML through
* the {@link DOMImportExtension} pipeline should forward it into the
* walk via `context: [contextValue(ImportSourceDataTransfer,
* dataTransfer)]` so rules and preprocessors can call
* `ctx.get(ImportSourceDataTransfer)` to inspect companion MIME types
* (e.g. an `'application/rtf'` alternative or an attached
* `'application/x-officedrawing'` payload), the file list, or any
* custom drag-and-drop slot.
*
* Use sparingly: the safer pattern is to decide *which* MIME-type
* payload to walk in the clipboard handler stack and hand a finalized
* DOM to the rules; only fall back to peeking at `ImportSourceDataTransfer`
* when the source-detection signal genuinely lives in a companion
* slot.
*
* @experimental
*/
export const ImportSourceDataTransfer: ImportStateConfig<DataTransfer | null> =
createImportState<DataTransfer | null>(
'importSourceDataTransfer',
() => null,
);
/**
* Built-in import-context state holding the bit-packed
* {@link TextFormatType} formats that should apply to {@link TextNode}s
* produced during the current subtree. Used by inline-format wrappers
* (`<b>`, `<i>`, `<u>`, …) to propagate formatting through the context
* record instead of via the legacy `forChild` chain.
*
* @experimental
*/
export const ImportTextFormat: ImportStateConfig<number> = createImportState(
'textFormat',
() => 0,
);
/**
* Built-in import-context state holding a parsed CSS-style record
* (the {@link getStyleObjectFromCSS} shape) that should apply to
* {@link TextNode}s produced during the current subtree. Mirrors the
* format-bit propagation in {@link ImportTextFormat} for properties
* that don't fit into the format bit mask — `color`, `font-family`,
* `font-size`, etc.
*
* Ancestor rules that contribute a style branch the context with a
* merged record; the core `#text` rule materializes the non-empty
* record to a CSS string and calls `setStyle` on the new TextNode.
* Once TextNode adopts a parsed style record, the materialization
* step will go away.
*
* @experimental
*/
export const ImportTextStyle: ImportStateConfig<
Readonly<Record<string, string>>
> = createImportState<Readonly<Record<string, string>>>(
'textStyle',
() => ({}),
);
/**
* Determines whether a given DOM element should be treated as preserving
* whitespace (i.e. text content under it is not collapsed and is split on
* `\n` / `\t` into `LineBreakNode` / `TabNode`). The default matches the
* legacy behavior: the element itself is `<pre>` or its inline
* `white-space` style begins with `'pre'`.
*
* @experimental
*/
export type IsPreserveWhitespaceDom = (node: Node) => boolean;
/**
* Determines whether a given DOM node sits on the same visual line as its
* adjacent text siblings, governing whether leading/trailing whitespace in
* a `#text` is collapsed against neighbors. The default consults
* {@link isInlineDomNode} from `lexical` (style.display or a fixed inline
* tag-name set) and additionally treats elements with an explicit
* non-inline `display` style as block.
*
* @experimental
*/
export type IsInlineForWhitespace = (node: Node) => boolean;
/**
* Configuration for the core text whitespace-collapse logic. Override via
* {@link ImportWhitespaceConfig} either as a `contextDefaults` entry on
* the {@link DOMImportExtension} or per-call on `$generateNodesFromDOM`'s
* `context` option.
*
* @experimental
*/
export interface WhitespaceImportConfig {
/** See {@link IsPreserveWhitespaceDom}. */
readonly preservesWhitespace: IsPreserveWhitespaceDom;
/** See {@link IsInlineForWhitespace}. */
readonly isInline: IsInlineForWhitespace;
}
/**
* Default {@link WhitespaceImportConfig.preservesWhitespace}: matches
* `<pre>` and any element with `white-space: pre*`.
*
* @experimental
*/
export function defaultPreservesWhitespace(node: Node): boolean {
if (!isHTMLElement(node)) {
return false;
}
if (node.nodeName === 'PRE') {
return true;
}
const ws = node.style.whiteSpace;
return typeof ws === 'string' && ws.startsWith('pre');
}
/**
* Default {@link WhitespaceImportConfig.isInline}: treats an element as
* inline iff its inline `display` style is `inline*` OR (no explicit
* non-inline display) its nodeName is a known inline tag (`isInlineDomNode`).
* Text nodes are always inline; comments and other non-elements are not.
*
* @experimental
*/
export function defaultIsInline(node: Node): boolean {
if (isDOMTextNode(node)) {
return true;
}
if (!isHTMLElement(node)) {
return false;
}
const display = node.style.display;
if (display) {
return display.startsWith('inline');
}
if (isBlockDomNode(node)) {
return false;
}
return isInlineDomNode(node);
}
/**
* Built-in import-context state controlling text-node whitespace handling
* (collapse vs. preserve, what counts as an inline sibling). Override per
* editor via {@link DOMImportConfig.contextDefaults} or per call via
* {@link GenerateNodesFromDOMOptions.context}.
*
* @experimental
*/
export const ImportWhitespaceConfig: ImportStateConfig<WhitespaceImportConfig> =
createImportState<WhitespaceImportConfig>('whitespaceConfig', () => ({
isInline: defaultIsInline,
preservesWhitespace: defaultPreservesWhitespace,
}));
/**
* Built-in session slot for runtime overlay rules that should be in
* effect for the entire walk. A preprocessor writes here when it wants
* to conditionally install handling for a particular paste source
* (e.g. "if the Microsoft Word generator meta tag is present, push the
* Word-paste overlay"). Each entry contributes an overlay dispatcher
* to the runtime's overlay stack; later array entries are higher
* priority. Use `ctx.session.update(ImportOverlays, prev => […])` to
* append.
*
* This is the walk-wide counterpart to
* `$importChildren({rules: …})` (which scopes an overlay to one
* subtree): write to {@link ImportOverlays} when the overlay should
* apply for the whole document; use `$importChildren`'s `rules` when
* the overlay should only apply for a deeper region.
*
* @experimental
*/
export const ImportOverlays: ImportStateConfig<
readonly CompiledOverlayRules[]
> = createImportState<readonly CompiledOverlayRules[]>(
'importOverlays',
() => [],
);
/**
* The session IS the root-layer {@link ContextRecord} of the walk. Reads
* fall through the prototype chain to the editor's `contextDefaults`,
* writes mutate the record's own properties, and any branch pushed by
* `$importChildren({context})` sits above this layer and can shadow
* (but does not overwrite) slots.
*
* @internal
*/
export class ImportSessionImpl implements ImportSession {
constructor(readonly record: ImportContextRecord) {}
get<V>(cfg: ImportStateConfig<V>): V {
return getContextValue(this.record, cfg);
}
set<V>(cfg: ImportStateConfig<V>, value: V): void {
this.record[cfg.key] = value;
}
update<V>(cfg: ImportStateConfig<V>, updater: (prev: V) => V): void {
this.record[cfg.key] = updater(getContextValue(this.record, cfg));
}
has<V>(cfg: ImportStateConfig<V>): boolean {
return Object.prototype.hasOwnProperty.call(this.record, cfg.key);
}
}
function getDefaultImportContext(
editor: LexicalEditor,
): undefined | ContextRecord<typeof DOMImportContextSymbol> {
const dep = getPeerDependencyFromEditor<typeof DOMImportExtension>(
editor,
DOMImportExtensionName,
);
return dep ? dep.output.defaults : undefined;
}
function getImportContext(
editor: LexicalEditor,
): undefined | ContextRecord<typeof DOMImportContextSymbol> {
return (
getContextRecord(DOMImportContextSymbol, editor) ||
getDefaultImportContext(editor)
);
}
/**
* Read an import context value during an import operation.
* @experimental
*/
export function $getImportContextValue<V>(
cfg: ImportStateConfig<V>,
editor: LexicalEditor = $getEditor(),
): V {
return getContextValue(getImportContext(editor), cfg);
}
/**
* Run `f` with the given context pairs applied on top of the editor's
* current import context.
*
* @experimental
*/
export const $withImportContext: (
cfg: readonly ImportContextPairOrUpdater[],
editor?: LexicalEditor,
) => <T>(f: () => T) => T = $withContext(
DOMImportContextSymbol,
getDefaultImportContext,
);
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import {parseSelector} from './parseCss';
import {selBase} from './sel';
/**
* Combinator-and-parser-based builder for {@link CompiledSelector}s. The
* runtime shape returned by these factory methods is opaque; consumers
* should never inspect or construct selector objects directly.
*
* @experimental
*/
export const sel = {
any: selBase.any,
comment: selBase.comment,
/**
* Parse a reduced CSS-selector subset and return a builder you can chain
* combinator methods off of.
*/
css: parseSelector,
tag: selBase.tag,
text: selBase.text,
} as const;
export {CoreImportExtension} from './CoreImportExtension';
export {CoreImportRules} from './coreImportRules';
export {defineImportRule} from './defineImportRule';
export {
type CompiledOverlayRules,
defineOverlayRules,
type DOMImportRuleEntry,
} from './defineOverlayRules';
export {
$generateNodesFromDOMViaExtension,
type DOMImportConfig,
DOMImportExtension,
} from './DOMImportExtension';
export {
HorizontalRuleImportExtension,
HorizontalRuleImportRules,
} from './HorizontalRuleImportExtension';
export {
$getImportContextValue,
$withImportContext,
createImportState,
defaultIsInline,
defaultPreservesWhitespace,
ImportOverlays,
ImportSource,
ImportSourceDataTransfer,
type ImportSourceKind,
ImportTextFormat,
ImportTextStyle,
ImportWhitespaceConfig,
type IsInlineForWhitespace,
type IsPreserveWhitespaceDom,
type WhitespaceImportConfig,
} from './ImportContext';
export {$inlineStylesFromStyleSheets} from './inlineStylesFromStyleSheets';
export {parseSelector} from './parseCss';
export {
$distributeInlineWrapper,
$isBlockLevel,
BlockSchema,
InlineSchema,
NestedBlockSchema,
RootSchema,
} from './schemas';
export {isElementOfTag} from './sel';
export type {
AnyDOMImportRule,
AttrMatchOptions,
CapturesOfSelector,
ChildSchema,
CompiledSelector,
DOMImportContext,
DOMImportExtensionOutput,
DOMImportFn,
DOMImportRule,
DOMPreprocessContext,
DOMPreprocessFn,
ElementSelectorBuilder,
GenerateNodesFromDOMOptions,
ImportChildrenOpts,
ImportContextPairOrUpdater,
ImportNodeOpts,
ImportSession,
ImportStateConfig,
NodeOfSelector,
StyleMatchOptions,
} from './types';
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import type {DOMPreprocessFn} from './types';
import {objectKlassEquals} from '@lexical/utils';
import {isDOMDocumentNode, isHTMLElement} from 'lexical';
/**
* Inlines CSS rules from `<style>` tags onto matching elements as inline
* styles.
*
* Used by apps like Excel that generate HTML where styles live in
* class-based `<style>` rules (e.g. `.xl65 { background: #FFFF00; color:
* blue; }`) rather than inline styles. Since Lexical's import converters
* read inline styles, we resolve stylesheet rules into inline styles
* before conversion.
*
* Mutates the DOM in-place. Original inline styles always take
* precedence over stylesheet rules (matching CSS specificity behavior).
*
* No-op for {@link ParentNode}s that are not {@link Document}s — only a
* full document carries `styleSheets` we can iterate.
*
* @experimental
*/
export const $inlineStylesFromStyleSheets: DOMPreprocessFn = (
dom,
_ctx,
$next,
) => {
$inlineStylesFromStyleSheetsDOM(dom);
$next();
};
export function $inlineStylesFromStyleSheetsDOM(
dom: Document | ParentNode,
): void {
if (!isDOMDocumentNode(dom)) {
return;
}
const doc = dom;
if (doc.querySelector('style') === null) {
return;
}
const originalInlineStyles = new Map<HTMLElement, Set<string>>();
function getOriginalInlineProps(el: HTMLElement): Set<string> {
let props = originalInlineStyles.get(el);
if (props === undefined) {
props = new Set<string>();
for (let i = 0; i < el.style.length; i++) {
props.add(el.style[i]);
}
originalInlineStyles.set(el, props);
}
return props;
}
try {
for (const sheet of Array.from(doc.styleSheets)) {
let rules: CSSRuleList;
try {
rules = sheet.cssRules;
} catch {
continue;
}
for (const rule of Array.from(rules)) {
if (!objectKlassEquals(rule, CSSStyleRule)) {
continue;
}
let elements: NodeListOf<Element>;
try {
elements = doc.querySelectorAll(rule.selectorText);
} catch {
continue;
}
for (const el of Array.from(elements)) {
if (!isHTMLElement(el)) {
continue;
}
const originalProps = getOriginalInlineProps(el);
for (let i = 0; i < rule.style.length; i++) {
const prop = rule.style[i];
if (!originalProps.has(prop)) {
el.style.setProperty(
prop,
rule.style.getPropertyValue(prop),
rule.style.getPropertyPriority(prop),
);
}
}
}
}
}
} catch {
// styleSheets API not supported in this environment
}
}
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import type {Predicate} from './sel';
import type {ElementSelectorBuilder} from './types';
import invariant from '@lexical/internal/invariant';
import {buildAttrPredicate, buildClassAllPredicate, buildSelector} from './sel';
const IDENT_CHAR = /[A-Za-z0-9_-]/;
class Cursor {
constructor(
public readonly source: string,
public pos: number,
) {}
peek(offset = 0): string {
return this.source[this.pos + offset] || '';
}
consume(): string {
return this.source[this.pos++] || '';
}
eof(): boolean {
return this.pos >= this.source.length;
}
skipWhitespace(): void {
while (!this.eof() && /\s/.test(this.peek())) {
this.pos++;
}
}
readIdent(): string {
const start = this.pos;
while (!this.eof() && IDENT_CHAR.test(this.peek())) {
this.pos++;
}
return this.source.slice(start, this.pos);
}
readQuoted(): string {
const quote = this.consume();
this.assert(quote === '"' || quote === "'", 'expected quote');
const start = this.pos;
while (!this.eof() && this.peek() !== quote) {
if (this.peek() === '\\') {
this.pos += 2;
} else {
this.pos++;
}
}
this.assert(!this.eof(), 'unterminated string');
const value = this.source.slice(start, this.pos);
this.pos++; // consume closing quote
return value.replace(/\\(.)/g, '$1');
}
/**
* `invariant(cond, fmt, …)`-flavored assertion that also surfaces the
* cursor's position context. Use for parse-time errors so a malformed
* CSS selector gets a useful, position-annotated message.
*/
assert(cond: boolean, msg: string): asserts cond {
invariant(
cond,
'invalid CSS selector at col %s: %s in %s',
String(this.pos + 1),
msg,
this.source,
);
}
}
interface ParsedSimpleSelector {
readonly tags: Set<string>;
readonly predicates: Predicate[];
}
function parseSimpleSelector(c: Cursor): ParsedSimpleSelector {
const tags = new Set<string>();
const predicates: Predicate[] = [];
const classes: string[] = [];
c.skipWhitespace();
// Optional tag or '*'
if (c.peek() === '*') {
c.consume();
} else if (IDENT_CHAR.test(c.peek())) {
const tag = c.readIdent();
if (tag) {
tags.add(tag.toUpperCase());
}
}
// Zero or more refinements: .class, #id, [attr]
while (!c.eof()) {
const ch = c.peek();
if (ch === '.') {
c.consume();
const cls = c.readIdent();
c.assert(cls !== '', 'expected class name after "."');
classes.push(cls);
} else if (ch === '#') {
c.consume();
const id = c.readIdent();
c.assert(id !== '', 'expected id after "#"');
predicates.push(buildAttrPredicate('id', id));
} else if (ch === '[') {
c.consume();
c.skipWhitespace();
const name = c.readIdent();
c.assert(name !== '', 'expected attribute name after "["');
c.skipWhitespace();
let value: true | string = true;
if (c.peek() === '=') {
c.consume();
c.skipWhitespace();
const next = c.peek();
if (next === '"' || next === "'") {
value = c.readQuoted();
} else {
value = c.readIdent();
c.assert(value !== '', 'expected attribute value');
}
c.skipWhitespace();
}
c.assert(c.peek() === ']', 'expected "]"');
c.consume();
predicates.push(buildAttrPredicate(name, value));
} else {
break;
}
}
if (classes.length > 0) {
predicates.push(buildClassAllPredicate(classes));
}
return {predicates, tags};
}
/**
* Parse a reduced CSS-selector subset and return a {@link CompiledSelector}.
* Supported:
* - Tag (`p`), wildcard (`*`).
* - Tag list (`h1, h2, h3`).
* - Class (`.foo`, `.foo.bar`).
* - ID (`#foo`).
* - Attribute presence (`[name]`).
* - Attribute equality (`[name="value"]`, `[name=value]`).
*
* Anything outside the subset (regex attribute, inline-style match,
* combinators, pseudo-classes) is intentionally rejected — chain combinator
* methods off the returned builder instead.
*
* @experimental
*/
export function parseSelector(
source: string,
): ElementSelectorBuilder<HTMLElement> {
const c: Cursor = new Cursor(source, 0);
const groups: ParsedSimpleSelector[] = [];
while (true) {
const group = parseSimpleSelector(c);
if (group.tags.size === 0 && group.predicates.length === 0) {
// Empty group with neither tag nor refinement — only OK if it came
// from the lone `*` (which produces zero tags but no preds either).
// We accept this as "wildcard element".
}
groups.push(group);
c.skipWhitespace();
if (c.eof()) {
break;
}
c.assert(
c.peek() === ',',
'expected "," (selector lists are the only supported combinator)',
);
c.consume();
c.skipWhitespace();
}
if (groups.length === 1) {
return buildSelector(groups[0].tags, groups[0].predicates);
}
// Comma-separated list. Merge tag sets and OR-combine the per-group
// refinement predicates so that each candidate node satisfies *some*
// group entirely.
const tags = new Set<string>();
for (const g of groups) {
for (const t of g.tags) {
tags.add(t);
}
}
const orPredicate: Predicate = (node, captures) => {
for (const g of groups) {
const upper = node.nodeName;
if (g.tags.size > 0 && !g.tags.has(upper)) {
continue;
}
let ok = true;
for (const p of g.predicates) {
if (!p(node, captures)) {
ok = false;
break;
}
}
if (ok) {
return true;
}
}
return false;
};
return buildSelector(tags, [orPredicate]);
}
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import type {
ChildSchema,
DOMImportContext,
ImportChildrenOpts,
ImportNodeOpts,
ImportSession,
ImportStateConfig,
} from './types';
import {isDOMDocumentNode, type LexicalEditor, type LexicalNode} from 'lexical';
import {
type CompiledDispatch,
type CompiledRule,
getDispatchIndices,
} from './compileImportRules';
import {
$getImportContextValue,
$withImportContext,
ImportOverlays,
} from './ImportContext';
import {$applySchema, RootSchema} from './schemas';
const __DEV__ = process.env.NODE_ENV !== 'production';
const NO_CAPTURES: Record<string, RegExpMatchArray> = Object.freeze(
{} as Record<string, RegExpMatchArray>,
);
interface Runtime {
readonly dispatch: CompiledDispatch;
readonly editor: LexicalEditor;
readonly session: ImportSession;
/**
* Stack of overlay dispatchers installed via `$importChildren({rules})`.
* The most recently pushed overlay is at the highest index and is
* tried first; rules within an overlay are dispatched in their own
* registration order. When all overlay rules for a node have been
* exhausted (or have called `$next()` to defer), the main dispatcher
* is consulted. This lets app rules scope cost-bearing predicates to
* the subtrees where they apply.
*/
readonly overlays: CompiledDispatch[];
}
function makeContext(
runtime: Runtime,
captures: Readonly<Record<string, RegExpMatchArray>>,
): DOMImportContext<Record<string, RegExpMatchArray>> {
const ctx: DOMImportContext<Record<string, RegExpMatchArray>> = {
$importChildren: (parent, opts) =>
$importChildrenInternal(runtime, parent, opts),
$importOne: (node, opts) => $importOneInternal(runtime, node, opts),
captures,
get<V>(cfg: ImportStateConfig<V>): V {
return $getImportContextValue(cfg, runtime.editor);
},
session: runtime.session,
};
return ctx;
}
function $importChildrenInternal(
runtime: Runtime,
parent: ParentNode,
opts: ImportChildrenOpts | undefined,
): LexicalNode[] {
const overlay = opts && opts.rules ? opts.rules.dispatch : undefined;
if (overlay) {
runtime.overlays.push(overlay);
}
try {
const run = () => $importChildrenRun(runtime, parent, opts);
return opts && opts.context
? $withImportContext(opts.context, runtime.editor)(run)
: run();
} finally {
if (overlay) {
runtime.overlays.pop();
}
}
}
function $importChildrenRun(
runtime: Runtime,
parent: ParentNode,
opts: ImportChildrenOpts | undefined,
): LexicalNode[] {
const onChild = opts && opts.$onChild;
const collected: LexicalNode[] = [];
for (const child of Array.from(parent.childNodes)) {
const produced = $importOneInternal(runtime, child, undefined);
for (const lex of produced) {
const result = onChild ? onChild(lex) : lex;
if (result != null) {
collected.push(result);
}
}
}
const afterApplied = opts && opts.$after ? opts.$after(collected) : collected;
const schema: ChildSchema | undefined = opts && opts.schema;
if (!schema) {
return afterApplied;
}
return $applySchema(schema, afterApplied, null, parent);
}
function $importOneInternal(
runtime: Runtime,
node: Node,
opts: ImportNodeOpts | undefined,
): LexicalNode[] {
const run = () => $dispatch(runtime, node);
const out =
opts && opts.context
? $withImportContext(opts.context, runtime.editor)(run)
: run();
// Surface to callers as a mutable array per the DOMImportContext contract.
return out as LexicalNode[];
}
/**
* Build the candidate (dispatch, indices) list for `node`. Overlays are
* tried first in top-of-stack order; the main dispatcher comes last. The
* `$next()` chain walks through all of them in sequence — an overlay rule
* can defer to a lower overlay rule, or all the way through to a main
* rule, just by calling `$next()`.
*/
function getCandidates(
runtime: Runtime,
node: Node,
): readonly {dispatch: CompiledDispatch; indices: readonly number[]}[] {
const candidates: {
dispatch: CompiledDispatch;
indices: readonly number[];
}[] = [];
for (let i = runtime.overlays.length - 1; i >= 0; i--) {
const d = runtime.overlays[i];
const idx = getDispatchIndices(d, node);
if (idx.length > 0) {
candidates.push({dispatch: d, indices: idx});
}
}
const mainIdx = getDispatchIndices(runtime.dispatch, node);
if (mainIdx.length > 0) {
candidates.push({dispatch: runtime.dispatch, indices: mainIdx});
}
return candidates;
}
function $dispatch(runtime: Runtime, node: Node): readonly LexicalNode[] {
const candidates = getCandidates(runtime, node);
if (candidates.length === 0) {
return $hoistChildrenOf(runtime, node);
}
let groupCursor = 0;
let ruleCursor = 0;
const $next = (): readonly LexicalNode[] => {
while (groupCursor < candidates.length) {
const {dispatch, indices} = candidates[groupCursor];
while (ruleCursor < indices.length) {
const idx = indices[ruleCursor++];
const rule: CompiledRule = dispatch.rules[idx];
const captures: Record<string, RegExpMatchArray> = {};
if (rule.predicate(node, captures)) {
const ctx = makeContext(
runtime,
Object.keys(captures).length === 0 ? NO_CAPTURES : captures,
);
try {
return rule.$import(ctx, node, $next);
} catch (e) {
if (__DEV__) {
console.error(
`[lexical] DOM import rule "${rule.name}" threw on node`,
node,
e,
);
}
throw e;
}
}
}
groupCursor++;
ruleCursor = 0;
}
return $hoistChildrenOf(runtime, node);
};
return $next();
}
/**
* Fallback when no rule matched and `$next()` was called past the end of the
* chain: hoist the element's children to take its place, recursively. Pure
* elements with no rule become invisible, matching the legacy
* `$createNodesFromDOM` hoisting behavior.
*/
function $hoistChildrenOf(runtime: Runtime, node: Node): LexicalNode[] {
if (node.childNodes.length === 0) {
return [];
}
const collected: LexicalNode[] = [];
for (const child of Array.from(node.childNodes)) {
const produced = $importOneInternal(runtime, child, undefined);
for (const lex of produced) {
collected.push(lex);
}
}
return collected;
}
/**
* Top-level walker for a compiled dispatcher. Iterates the DOM children of
* `dom` (using the document body if a {@link Document} is passed) and
* applies `RootSchema` to the produced lexical nodes so runs of inlines are
* wrapped in paragraphs — same shape as the legacy `$generateNodesFromDOM`.
*
* @internal
*/
export function $runImport(
dispatch: CompiledDispatch,
editor: LexicalEditor,
dom: Document | ParentNode,
session: ImportSession,
): LexicalNode[] {
// Prime the overlay stack with any overlays a preprocess wrote to
// ImportOverlays. These remain in effect for the entire walk; nested
// `$importChildren({rules})` calls push on top.
const installed = session.get(ImportOverlays);
const runtime: Runtime = {
dispatch,
editor,
overlays: installed.map(o => o.dispatch),
session,
};
const rootParent: ParentNode = isDOMDocumentNode(dom) ? dom.body : dom;
return $importChildrenRun(runtime, rootParent, {schema: RootSchema});
}
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import type {ChildSchema} from './types';
import {
$createParagraphNode,
$isBlockElementNode,
$isDecoratorNode,
$isElementNode,
type ElementNode,
isHTMLElement,
type LexicalNode,
} from 'lexical';
import {isAlignmentValue} from './coreImportRules';
/**
* True if the node fills a block slot at the root or inside another
* block — covers both ElementNode-style blocks (paragraph, heading,
* quote) and block-level DecoratorNodes (HorizontalRuleNode,
* ImageNode-as-block, etc.). Used by {@link BlockSchema},
* {@link RootSchema}, and {@link NestedBlockSchema}.
*
* @experimental
*/
export function $isBlockLevel(node: LexicalNode): boolean {
return (
$isBlockElementNode(node) || ($isDecoratorNode(node) && !node.isInline())
);
}
/**
* Distribute an inline wrapper (`LinkNode`, `MarkNode`, …) across a
* heterogeneous run of children produced by `$importChildren`, lifting
* any block children to the top level while keeping the wrapper around
* the leaf inline content.
*
* Use from a rule whose DOM source is an inline element that the
* browser permitted to enclose block elements — the canonical case is
* `<a href="…"><h1>title</h1><div>body</div></a>`, which a link rule
* wants to surface as two block siblings (heading + paragraph), each
* with its own link wrapping the original inline content. Schemas
* can't express this because they reason about a parent's children
* only — they cannot lift the parent out of itself.
*
* For each top-level child:
* - **Inline children** are collected into runs; each run is wrapped
* in a single fresh wrapper (from `$makeWrapper()`).
* - **Block children** are descended into: their own children are
* recursively distributed with `$makeWrapper`, then re-attached so
* the block keeps its position at the top level.
*
* The returned list will contain a mix of blocks and wrapped inline
* runs. The enclosing schema (typically {@link BlockSchema}) will
* then package those inline wrappers into paragraphs as usual.
*
* @experimental
*/
export function $distributeInlineWrapper(
children: readonly LexicalNode[],
$makeWrapper: () => ElementNode,
): LexicalNode[] {
const out: LexicalNode[] = [];
let inlineRun: LexicalNode[] = [];
const flushInline = () => {
if (inlineRun.length === 0) {
return;
}
out.push($makeWrapper().splice(0, 0, inlineRun));
inlineRun = [];
};
for (const child of children) {
if ($isBlockLevel(child)) {
flushInline();
// Recursively distribute the wrapper into the block's own
// children. A block DecoratorNode (no children) is left alone.
if ($isElementNode(child)) {
const wrapped = $distributeInlineWrapper(
child.getChildren(),
$makeWrapper,
);
child.splice(0, child.getChildrenSize(), wrapped);
}
out.push(child);
} else {
inlineRun.push(child);
}
}
flushInline();
return out;
}
/**
* Apply a {@link ChildSchema} to a flat list of children produced by
* `$importChildren`. Walks the list once, partitions into accepted vs.
* rejected runs, packages or drops rejected runs, then runs `$finalize`.
*
* @internal
*/
export function $applySchema(
schema: ChildSchema,
children: LexicalNode[],
parent: LexicalNode | null,
domParent: Node | null,
): LexicalNode[] {
const out: LexicalNode[] = [];
let run: LexicalNode[] | null = null;
const flushRun = () => {
if (run === null) {
return;
}
const rejected = run;
run = null;
if (schema.$packageRun) {
const packaged = schema.$packageRun(rejected, parent, domParent);
if (packaged.length > 0) {
for (const n of packaged) {
out.push(n);
}
return;
}
}
// No $packageRun (or it returned []) — apply onReject. 'drop' (default)
// discards the run. 'hoist' lets it through unchanged at this level.
if (schema.onReject === 'hoist') {
for (const n of rejected) {
out.push(n);
}
}
};
for (const child of children) {
if (schema.$accepts(child, parent)) {
flushRun();
out.push(child);
} else {
if (run === null) {
run = [];
}
run.push(child);
}
}
flushRun();
return schema.$finalize ? schema.$finalize(out, parent) : out;
}
/**
* Wrap a run of inline lexical nodes in a fresh paragraph, propagating the
* `text-align` of `domParent` as the paragraph's format type (matching the
* legacy `wrapContinuousInlines` behavior).
*/
function $paragraphPackageRun(
run: LexicalNode[],
_parent: LexicalNode | null,
domParent: Node | null,
): LexicalNode[] {
const paragraph = $createParagraphNode();
if (isHTMLElement(domParent)) {
const textAlign = domParent.style.textAlign;
if (isAlignmentValue(textAlign)) {
paragraph.setFormat(textAlign);
}
}
return [paragraph.splice(0, 0, run)];
}
/**
* Default schema for block-level positions (root of the document, the body
* of a block element node). Accepts block lexical nodes; packages runs of
* inline children into fresh paragraph nodes.
*
* @experimental
*/
export const BlockSchema: ChildSchema = {
$accepts: $isBlockLevel,
$packageRun: $paragraphPackageRun,
name: 'BlockSchema',
};
/**
* Schema for inline-only positions (the body of an inline lexical node such
* as a link). Accepts non-block lexical nodes; runs of block children are
* dropped (`onReject: 'drop'` is the default).
*
* @experimental
*/
export const InlineSchema: ChildSchema = {
$accepts: child => !$isBlockLevel(child),
name: 'InlineSchema',
};
/**
* Schema for nested block positions — the equivalent of the legacy
* `ArtificialNode__DO_NOT_USE` flow used when a block DOM element appears
* inside another block lexical ancestor. Accepts block nodes; runs of inline
* children are emitted with a line break between consecutive runs (instead
* of being wrapped in a paragraph, which would introduce an extra level of
* nesting).
*
* @experimental
*/
export const NestedBlockSchema: ChildSchema = {
$accepts: $isBlockLevel,
/**
* Pass an inline run through unchanged. Because the schema iterator only
* groups *maximal* rejected runs (each separated from the next by an
* accepted block child), the legacy "linebreak between adjacent inline
* groups" case never arises — adjacent inline siblings are already
* coalesced into one run.
*/
$packageRun: run => run,
name: 'NestedBlockSchema',
};
/**
* Schema for the topmost level of `$generateNodesFromDOM`. Identical to
* {@link BlockSchema}; aliased for clarity at the entry point and so it can
* be overridden separately in the future (e.g. to synthesize a `ListNode`
* around runs of orphan `ListItemNode`s).
*
* @experimental
*/
export const RootSchema: ChildSchema = {
$accepts: $isBlockLevel,
$packageRun: $paragraphPackageRun,
name: 'RootSchema',
};
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import type {
AttrMatchOptions,
CompiledSelector,
ElementSelectorBuilder,
StyleMatchOptions,
} from './types';
import invariant from '@lexical/internal/invariant';
import {isDOMTextNode, isHTMLElement} from 'lexical';
/**
* @internal
*
* A predicate that may write into the per-invocation `captures` map. Returns
* `true` if the rule matches; `false` otherwise.
*/
export type Predicate = (
node: Node,
captures: Record<string, RegExpMatchArray>,
) => boolean;
/** @internal */
export type SelectorKind = 'element' | 'text' | 'comment';
/** @internal The runtime shape of a {@link CompiledSelector}. */
export interface SelectorImpl {
readonly kind: SelectorKind;
/**
* Uppercased tag names this selector is restricted to. Empty for wildcard
* element selectors and for text / comment selectors (dispatched by
* `kind`).
*/
readonly tags: ReadonlySet<string>;
/** Composed predicate run against a candidate node. */
readonly predicate: Predicate;
}
const IMPL = Symbol.for('@lexical/html/SelectorImpl');
/** @internal */
export function getSelectorImpl(sel: CompiledSelector): SelectorImpl {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const impl = (sel as any)[IMPL] as SelectorImpl | undefined;
invariant(
impl !== undefined,
'match must be a CompiledSelector produced by sel.* or sel.css(); received a raw object.',
);
return impl;
}
function combinePredicates(preds: readonly Predicate[]): Predicate {
if (preds.length === 0) {
return isHTMLElement;
}
if (preds.length === 1) {
return preds[0];
}
return (node, captures) => {
for (const p of preds) {
if (!p(node, captures)) {
return false;
}
}
return true;
};
}
/**
* @internal
*
* Build a selector value from a tag set and a predicate list. Used by the
* combinator API and the CSS parser.
*/
export function buildSelector(
tags: ReadonlySet<string>,
predicates: readonly Predicate[],
): ElementSelectorBuilder<HTMLElement> {
const impl: SelectorImpl = {
kind: 'element',
predicate: combinePredicates(predicates),
tags,
};
const refine = (additional: Predicate) =>
buildSelector(tags, [...predicates, additional]);
const builder = {
[IMPL]: impl,
attr: (name: string, value: unknown, options?: AttrMatchOptions) =>
refine(buildAttrPredicate(name, value, options)),
classAll: (...classes: readonly string[]) =>
refine(buildClassAllPredicate(classes)),
classAny: (...classes: readonly string[]) =>
refine(buildClassAnyPredicate(classes)),
styleAny: (prop: string, value: unknown, options?: StyleMatchOptions) =>
refine(buildStylePredicate(prop, value, options)),
};
// The runtime is fully type-erased; cast to satisfy the surface.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return builder as any;
}
function normalizeClassList(classes: readonly string[]): readonly string[] {
const out: string[] = [];
for (const c of classes) {
if (c) {
out.push(c);
}
}
return out;
}
/** @internal */
export function buildClassAllPredicate(classes: readonly string[]): Predicate {
const ns = normalizeClassList(classes);
if (ns.length === 0) {
return () => true;
}
return node => {
if (!isHTMLElement(node)) {
return false;
}
const cl = node.classList;
for (const c of ns) {
if (!cl.contains(c)) {
return false;
}
}
return true;
};
}
/** @internal */
export function buildClassAnyPredicate(classes: readonly string[]): Predicate {
const ns = normalizeClassList(classes);
if (ns.length === 0) {
return () => false;
}
return node => {
if (!isHTMLElement(node)) {
return false;
}
const cl = node.classList;
for (const c of ns) {
if (cl.contains(c)) {
return true;
}
}
return false;
};
}
/** @internal */
export function buildAttrPredicate(
name: string,
value: unknown,
options?: AttrMatchOptions,
): Predicate {
if (value === true) {
return node => isHTMLElement(node) && node.hasAttribute(name);
}
if (typeof value === 'string') {
return node => isHTMLElement(node) && node.getAttribute(name) === value;
}
if (value instanceof RegExp) {
const capture = options && options.capture;
const re = value;
return (node, captures) => {
if (!isHTMLElement(node)) {
return false;
}
const v = node.getAttribute(name);
if (v == null) {
return false;
}
const m = v.match(re);
if (m === null) {
return false;
}
if (capture !== undefined) {
captures[capture] = m;
}
return true;
};
}
invariant(
false,
'sel.attr(%s, ...) requires true, a string, or a RegExp',
JSON.stringify(name),
);
}
function buildStylePredicate(
prop: string,
value: unknown,
options?: StyleMatchOptions,
): Predicate {
if (typeof value === 'string') {
return node =>
isHTMLElement(node) && node.style.getPropertyValue(prop) === value;
}
if (value instanceof RegExp) {
const capture = options && options.capture;
const re = value;
return (node, captures) => {
if (!isHTMLElement(node)) {
return false;
}
const v = node.style.getPropertyValue(prop);
if (!v) {
return false;
}
const m = v.match(re);
if (m === null) {
return false;
}
if (capture !== undefined) {
captures[capture] = m;
}
return true;
};
}
invariant(
false,
'sel.styleAny(%s, ...) requires a string or a RegExp',
JSON.stringify(prop),
);
}
const TEXT_SELECTOR_IMPL: SelectorImpl = {
kind: 'text',
predicate: isDOMTextNode,
tags: new Set(),
};
// The `as` cast is needed because `CompiledSelector` is an opaque
// branded interface — neither the object literal nor a typed const can
// declare the internal `IMPL` symbol without exposing it.
const TEXT_SELECTOR = {[IMPL]: TEXT_SELECTOR_IMPL} as CompiledSelector<Text>;
const COMMENT_SELECTOR_IMPL: SelectorImpl = {
kind: 'comment',
predicate: node => node.nodeType === 8 /* COMMENT_NODE */,
tags: new Set(),
};
const COMMENT_SELECTOR = {
[IMPL]: COMMENT_SELECTOR_IMPL,
} as CompiledSelector<Comment>;
/**
* Combinator API for building {@link CompiledSelector}s. The public
* `sel` is augmented from this in `./index.ts` (where the CSS parser is
* available without a circular import); consumers outside `@lexical/html`
* should always import the public `sel` from the package root.
*
* @internal
*/
export const selBase = {
/** Match any {@link HTMLElement}. */
any(): ElementSelectorBuilder<HTMLElement> {
return buildSelector(new Set(), []);
},
/** Match DOM {@link Comment} nodes. */
comment(): CompiledSelector<Comment> {
return COMMENT_SELECTOR;
},
/**
* Match by tag name(s). With one literal tag the element type is narrowed
* (e.g. `'a' → HTMLAnchorElement`); with multiple, it is the union of
* their `HTMLElementTagNameMap` entries.
*/
tag<const Tags extends readonly string[]>(
...tags: Tags
): ElementSelectorBuilder<
Tags[number] extends keyof HTMLElementTagNameMap
? HTMLElementTagNameMap[Tags[number]]
: HTMLElement
> {
invariant(tags.length > 0, 'sel.tag() requires at least one tag name');
const upper = new Set<string>();
for (const t of tags) {
upper.add(t.toUpperCase());
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return buildSelector(upper, []) as any;
},
/** Match DOM {@link Text} nodes. */
text(): CompiledSelector<Text> {
return TEXT_SELECTOR;
},
} as const;
/**
* Cross-frame-safe replacement for `node instanceof HTMLXxxElement`. Returns
* true when `node` is an HTMLElement whose `nodeName` equals `tag` (compared
* case-insensitively).
*
* @experimental
*/
export function isElementOfTag<T extends keyof HTMLElementTagNameMap>(
node: Node,
tag: T,
): node is HTMLElementTagNameMap[T] {
return isHTMLElement(node) && node.nodeName === tag.toUpperCase();
}
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import type {DOMImportContextSymbol} from '../constants';
import type {
AnyContextConfigPairOrUpdater,
ContextConfig,
ContextRecord,
} from '../types';
import type {CompiledOverlayRules} from './defineOverlayRules';
import type {LexicalNode} from 'lexical';
/**
* Phantom-typed branding so consumers cannot construct or mutate a
* {@link CompiledSelector} directly; the only way to obtain one is via the
* {@link sel} builder or {@link parseSelector}. The actual runtime shape is
* an internal implementation detail (see `./sel`).
*
* @experimental
*/
export declare const NodeBrand: unique symbol;
/** @experimental */
export declare const CaptureBrand: unique symbol;
/**
* An opaque, compiled selector used as the `match` field of a
* {@link DOMImportRule}. The two phantom type parameters carry the matched
* Node subtype (`N`) and a record of named regex captures (`C`) so the
* importer body gets correctly-typed `ctx` and `node` arguments without
* casts.
*
* @experimental
*/
export interface CompiledSelector<
N extends Node = Node,
C extends Record<string, RegExpMatchArray> = Record<string, RegExpMatchArray>,
> {
readonly [NodeBrand]?: N;
readonly [CaptureBrand]?: C;
}
/**
* The Node subtype matched by a selector (e.g. `HTMLAnchorElement` for
* `sel.tag('a')`, `Text` for `sel.text()`).
*
* @experimental
*/
export type NodeOfSelector<S> =
S extends CompiledSelector<infer N, Record<string, RegExpMatchArray>>
? N
: Node;
/**
* The named-capture map for a selector.
*
* @experimental
*/
export type CapturesOfSelector<S> =
S extends CompiledSelector<Node, infer C> ? C : Record<string, never>;
/**
* Options bag for {@link ElementSelectorBuilder.attr} when the value is a
* regex. Future options will be added here without breaking existing
* call-sites.
*
* @experimental
*/
export interface AttrMatchOptions<K extends string = string> {
/**
* If provided, the {@link RegExpMatchArray} from the successful match is
* stored on `ctx.captures[capture]` for the importer to consume — saving
* a second regex execution.
*/
readonly capture?: K;
}
/**
* Options bag for {@link ElementSelectorBuilder.styleAny} when the value is a
* regex. See {@link AttrMatchOptions} for capture semantics.
*
* @experimental
*/
export interface StyleMatchOptions<K extends string = string> {
readonly capture?: K;
}
/**
* Fluent builder for an element selector. The two type parameters carry the
* matched element type and the named-capture map; each call refines them.
*
* The builder itself implements {@link CompiledSelector} so it can be used
* directly as the `match` field of a rule — no `.build()` call needed.
*
* @experimental
*/
export interface ElementSelectorBuilder<
E extends HTMLElement,
C extends Record<string, RegExpMatchArray> = Record<string, never>,
> extends CompiledSelector<E, C> {
/** Require every listed class to be present on the element. */
classAll(...classes: readonly string[]): ElementSelectorBuilder<E, C>;
/** Require at least one of the listed classes to be present. */
classAny(...classes: readonly string[]): ElementSelectorBuilder<E, C>;
/** Require the attribute to be present (any value). */
attr(name: string, value: true): ElementSelectorBuilder<E, C>;
/** Require the attribute to equal the given string. */
attr(name: string, value: string): ElementSelectorBuilder<E, C>;
/**
* Require the attribute to match the given regex. With
* `{capture: 'name'}` the match result is exposed on
* `ctx.captures.name`.
*/
attr<const O extends AttrMatchOptions>(
name: string,
value: RegExp,
options?: O,
): ElementSelectorBuilder<
E,
O extends {capture: infer K} ? C & Record<K & string, RegExpMatchArray> : C
>;
/** Require the inline-style declaration to equal `value`. */
styleAny(prop: string, value: string): ElementSelectorBuilder<E, C>;
/** Require the inline-style declaration to match `value`. */
styleAny<const O extends StyleMatchOptions>(
prop: string,
value: RegExp,
options?: O,
): ElementSelectorBuilder<
E,
O extends {capture: infer K} ? C & Record<K & string, RegExpMatchArray> : C
>;
}
/**
* Argument to {@link DOMImportContext.branch} / `$importChildren({context})`
* — see {@link ContextConfigPair} / {@link ContextConfigUpdater}.
*
* @experimental
*/
export type ImportContextPairOrUpdater = AnyContextConfigPairOrUpdater<
typeof DOMImportContextSymbol
>;
/**
* A typed context-state key for the import pipeline. Create with
* {@link createImportState}.
*
* @experimental
*/
export type ImportStateConfig<V> = ContextConfig<
typeof DOMImportContextSymbol,
V
>;
/**
* A mutable, document-order-shared store for the import pipeline. Lets a
* rule visited early in the document write information that rules visited
* later can read — e.g. parse `<style>` or `<meta>` and influence
* subsequent matching.
*
* Implemented as the root-layer {@link ContextRecord} of the import walk:
* `ctx.session.set(cfg, v)` mutates the slot on that root record, and
* every unshadowed `ctx.get(cfg)` read in any branch picks it up. A
* `$importChildren({context: [...]})` branch that explicitly writes the
* same slot shadows the session value for the duration of that branch.
*
* @experimental
*/
export interface ImportSession {
/** Read the current value, returning the config's default if unset. */
get<V>(cfg: ImportStateConfig<V>): V;
/** Write `value` into the slot. */
set<V>(cfg: ImportStateConfig<V>, value: V): void;
/** Read-modify-write. */
update<V>(cfg: ImportStateConfig<V>, updater: (prev: V) => V): void;
/** Returns `true` if the slot has been written since session creation. */
has<V>(cfg: ImportStateConfig<V>): boolean;
}
/**
* Context exposed to a rule's `$import` function. Mirrors the existing render
* context (see {@link RenderContext}) but is import-scoped.
*
* @experimental
*/
export interface DOMImportContext<
C extends Record<string, RegExpMatchArray> = Record<string, never>,
> {
/** Captures from this rule's selector. Fresh per rule invocation. */
readonly captures: Readonly<C>;
/**
* Mutable, document-order-shared store. Use to make information from
* earlier-visited nodes available to later-visited ones (e.g. a
* `<style>` or `<meta>` at the top of the document influencing how
* later elements are interpreted). One {@link ImportSession} instance
* is created per top-level `$generateNodesFromDOM` call and is shared
* across all recursive `$importChildren` / `$importOne` invocations.
*/
readonly session: ImportSession;
/** Read a typed context value. */
get<V>(cfg: ImportStateConfig<V>): V;
/**
* Recursively import every child of `parent` and return the produced
* lexical nodes, optionally enforcing a {@link ChildSchema} and/or
* branching the import context for the duration of the call (via
* `opts.context`).
*/
$importChildren(parent: ParentNode, opts?: ImportChildrenOpts): LexicalNode[];
/**
* Recursively import a single DOM node.
*/
$importOne(node: Node, opts?: ImportNodeOpts): LexicalNode[];
}
/**
* Options accepted by {@link DOMImportContext.$importChildren}. The combination
* of `schema`, `$onChild`, and `$after` is sufficient to express every
* child-handling pattern in the legacy `forChild` / `after` / wrap-continuous
* machinery.
*
* @experimental
*/
export interface ImportChildrenOpts {
/**
* How to validate and (re)package produced children. Defaults to whichever
* schema the parent's importer passed; the top-level entry uses
* {@link BlockSchema}.
*/
readonly schema?: ChildSchema;
/**
* Called for each produced lexical child immediately after its rule
* returned, with the chance to substitute or drop it. Equivalent to the
* old `forChild` hook but scoped to one `$importChildren` call.
*/
readonly $onChild?: (child: LexicalNode) => LexicalNode | null | undefined;
/**
* Called once with the full child array after all DOM children have been
* recursively imported but before {@link ChildSchema.$packageRun} is
* applied. Equivalent to the old `after` hook.
*/
readonly $after?: (children: LexicalNode[]) => LexicalNode[];
/** Context overrides scoped to the children traversal. */
readonly context?: readonly ImportContextPairOrUpdater[];
/**
* Additional {@link DOMImportRule}s active only for this children
* traversal (and any nested `$importChildren` calls that don't push
* their own overlay). The overlay is checked BEFORE the main
* dispatcher, so its rules take precedence; calling `$next()` from an
* overlay rule falls through to the next overlay-or-main rule.
*
* Use this to scope cost-bearing rules to where they apply. For
* example, a GitHub code-table rule installs an overlay that
* unwraps `<tr>` / `<td>` inside the table, without paying that
* predicate cost on every other `<tr>` / `<td>` paste.
*
* The value must be produced by
* {@link defineOverlayRules}; this forces the dispatcher to be
* compiled once at module scope and reused across
* `$importChildren` calls, instead of being recompiled per invocation.
*/
readonly rules?: CompiledOverlayRules;
}
/** @experimental */
export interface ImportNodeOpts {
readonly context?: readonly ImportContextPairOrUpdater[];
}
/**
* A {@link ChildSchema} encodes which lexical nodes a parent accepts as
* children and how to package or reject the rest. The legacy
* `wrapContinuousInlines` / `ArtificialNode__DO_NOT_USE` logic is the
* `BlockSchema` and `NestedBlockSchema` cases of this primitive.
*
* A schema only controls how the *children* are assembled before being
* appended to the parent the calling rule already chose. It cannot
* change the parent itself. Cases where the parent's shape needs to
* change in response to its children — e.g. an inline `<a>` that
* encloses a block `<h1>`, which must be lifted so the heading takes
* the link's place and the link is redistributed onto the heading's
* inline contents — belong in the rule body, not the schema. See the
* "Lifting blocks out of an inline parent" section of the docs.
*
* @experimental
*/
export interface ChildSchema {
/** Optional name for debug output. */
readonly name?: string;
/**
* Returns `true` if `child` is a valid child of `parent` in this position.
*/
$accepts(child: LexicalNode, parent: LexicalNode | null): boolean;
/**
* Package a maximal run of non-accepted siblings into zero or more
* accepted nodes. The returned nodes replace the rejected run at the
* same position in the child list and are not re-checked against
* `$accepts` — the caller is trusted to return valid children. If
* omitted, or if it returns an empty array, {@link ChildSchema.onReject}
* is consulted instead.
*/
$packageRun?(
rejected: LexicalNode[],
parent: LexicalNode | null,
domParent: Node | null,
): LexicalNode[];
/**
* Fallback strategy for a run of non-accepted children when
* `$packageRun` is missing or returns an empty array:
*
* - `'drop'` (default) — silently discards the run. Use when the
* schema is strict and rejected content is meaningless in this
* position (e.g. text between table rows).
* - `'hoist'` — emits the rejected nodes unchanged at the same
* position in the assembled child list. The caller's parent then
* receives them as-is, which is only useful if the calling rule
* intends to surface mixed content to *its* parent (a less common
* pattern; usually `$packageRun` should re-shape the run first).
*
* `'hoist'` does NOT lift the run all the way up out of the calling
* rule's parent — that requires the rule itself to detect the
* situation and emit a different parent structure (see the
* "Lifting blocks out of an inline parent" section).
*/
readonly onReject?: 'hoist' | 'drop';
/**
* Final pass over the assembled child list (after `$packageRun`). Returns
* the children to actually attach. Use to enforce structural invariants
* (e.g. drop empty runs, pad short table rows).
*/
$finalize?(
children: LexicalNode[],
parent: LexicalNode | null,
): LexicalNode[];
}
/**
* The middleware signature of an import rule. Call `$next()` to delegate to
* the next-matching rule for this node (returning its result, which may then
* be inspected or wrapped); return `[]` to drop the node.
*
* @experimental
*/
export type DOMImportFn<
E extends Node,
C extends Record<string, RegExpMatchArray> = Record<string, never>,
> = (
ctx: DOMImportContext<C>,
node: E,
$next: () => readonly LexicalNode[],
) => readonly LexicalNode[];
/**
* An importer for a DOM node, dispatched by `match` and implemented by
* `$import`.
*
* @experimental
*/
export interface DOMImportRule<S extends CompiledSelector = CompiledSelector> {
/**
* Optional identifier surfaced in dev-mode logs, error messages, and
* introspection devtools. Convention: `'@scope/package/rule-id'` for
* library rules.
*/
readonly name?: string;
/** A {@link CompiledSelector} produced by the {@link sel} builder. */
readonly match: S;
/** Middleware that converts the matched DOM node into lexical nodes. */
readonly $import: DOMImportFn<NodeOfSelector<S>, CapturesOfSelector<S>>;
}
/** @experimental */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type AnyDOMImportRule = DOMImportRule<any>;
/**
* Context exposed to a {@link DOMPreprocessFn}. Lets the preprocessor:
*
* - Write to the per-import {@link ImportSession} (the same
* `ctx.session` rules see during the walk). Writes mutate the
* root-layer context record, so they are visible to every scoped
* `ctx.get(cfg)` read that hasn't been shadowed by a branch.
*
* @experimental
*/
export interface DOMPreprocessContext {
/**
* Document-order-shared store, same instance as `ctx.session` in
* rules. Use to pass info from the DOM preprocess phase (e.g.
* inspected `<meta>` tags, collected `<style>` text) to the
* importer rules.
*/
readonly session: ImportSession;
}
/**
* A middleware step in the DOM-preprocess chain. Runs before walking
* begins and may:
*
* - Mutate the input DOM in place (e.g. inline stylesheets, strip
* unsafe elements, normalize attributes).
* - Write to {@link DOMPreprocessContext.session} for rules to read
* (and for unshadowed scoped reads to pick up).
* - Call `$next()` to defer to the next-lower preprocessor in the
* stack; omit the call to short-circuit and skip the rest.
*
* The preprocess phase runs inside the same editor read / update
* context as the walk that follows, so a preprocess function may call
* `$`-prefixed Lexical APIs (e.g. `$getState`, `$getRoot`) as needed.
* The `$next` parameter is named with a `$` prefix to make that
* editor-context expectation visible to readers and lints.
*
* Append-style merge applies: an extension's preprocessors are appended
* to the existing stack, so later-registered preprocessors run first
* and may delegate to earlier (lower-priority) ones via `$next()`. Same
* convention as {@link ExportMimeTypeFunction} on the export side.
*
* @experimental
*/
export type DOMPreprocessFn = (
dom: Document | ParentNode,
ctx: DOMPreprocessContext,
$next: () => void,
) => void;
/**
* Per-call options to the extension's `$generateNodesFromDOM`.
*
* @experimental
*/
export interface GenerateNodesFromDOMOptions {
/**
* Context pairs/updaters applied for the duration of this import only —
* use to communicate per-call info such as the {@link ImportSource}.
*/
readonly context?: readonly ImportContextPairOrUpdater[];
/**
* Additional preprocessors to run on this call only, on top of the
* extension's configured {@link DOMImportConfig.preprocess}. Per-call
* preprocessors run AFTER the configured ones.
*/
readonly preprocess?: readonly DOMPreprocessFn[];
}
/**
* Output of {@link DOMImportExtension}.
*
* @experimental
*/
export interface DOMImportExtensionOutput {
/**
* Convert a {@link Document} or {@link ParentNode} into lexical nodes,
* using the dispatcher compiled from this extension's configured
* {@link DOMImportRule}s.
*
* Must be called within an `editor.update()` or `editor.read()` because
* the importers may invoke `$create...` helpers.
*/
$generateNodesFromDOM(
dom: Document | ParentNode,
options?: GenerateNodesFromDOMOptions,
): LexicalNode[];
/** @internal */
readonly defaults: undefined | ContextRecord<typeof DOMImportContextSymbol>;
}
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import type {
BaseSelection,
DOMChildConversion,
DOMConversion,
DOMConversionFn,
EditorDOMRenderConfig,
ElementFormatType,
LexicalEditor,
LexicalNode,
} from 'lexical';
import invariant from '@lexical/internal/invariant';
import {$sliceSelectedTextNodeContent} from '@lexical/selection';
import {
$createLineBreakNode,
$createParagraphNode,
$getEditor,
$getEditorDOMRenderConfig,
$getRoot,
$isBlockElementNode,
$isElementNode,
$isRootOrShadowRoot,
$isTextNode,
ArtificialNode__DO_NOT_USE,
ElementNode,
isBlockDomNode,
isDocumentFragment,
isDOMDocumentNode,
isHTMLElement,
isInlineDomNode,
} from 'lexical';
import {contextValue} from './ContextRecord';
import {$inlineStylesFromStyleSheetsDOM} from './import/inlineStylesFromStyleSheets';
import {
$getSessionDOMRenderConfig,
$withRenderContext,
RenderContextExport,
RenderContextRoot,
} from './RenderContext';
export {contextUpdater, contextValue} from './ContextRecord';
export {domOverride} from './domOverride';
export {DOMRenderExtension} from './DOMRenderExtension';
export type {
AnyDOMImportRule,
AttrMatchOptions,
CapturesOfSelector,
ChildSchema,
CompiledOverlayRules,
CompiledSelector,
DOMImportContext,
DOMImportExtensionOutput,
DOMImportFn,
DOMImportRule,
DOMImportRuleEntry,
DOMPreprocessContext,
DOMPreprocessFn,
ElementSelectorBuilder,
GenerateNodesFromDOMOptions,
ImportChildrenOpts,
ImportContextPairOrUpdater,
ImportNodeOpts,
ImportSession,
ImportStateConfig,
NodeOfSelector,
StyleMatchOptions,
} from './import';
export {
$distributeInlineWrapper,
$generateNodesFromDOMViaExtension,
$getImportContextValue,
$inlineStylesFromStyleSheets,
$isBlockLevel,
$withImportContext,
BlockSchema,
CoreImportExtension,
CoreImportRules,
createImportState,
defaultIsInline,
defaultPreservesWhitespace,
defineImportRule,
defineOverlayRules,
type DOMImportConfig,
DOMImportExtension,
HorizontalRuleImportExtension,
HorizontalRuleImportRules,
ImportOverlays,
ImportSource,
ImportSourceDataTransfer,
type ImportSourceKind,
ImportTextFormat,
ImportTextStyle,
ImportWhitespaceConfig,
InlineSchema,
isElementOfTag,
type IsInlineForWhitespace,
type IsPreserveWhitespaceDom,
NestedBlockSchema,
parseSelector,
RootSchema,
sel,
type WhitespaceImportConfig,
} from './import';
export {
$getRenderContextValue,
$getSessionDOMRenderConfig,
$setRenderContextValue,
$updateRenderContextValue,
$withRenderContext,
createRenderState,
RenderContextExport,
RenderContextRoot,
} from './RenderContext';
export type {
AnyDOMRenderMatch,
AnyRenderStateConfig,
AnyRenderStateConfigPairOrUpdater,
ContextPairOrUpdater,
DOMOverrideOptions,
DOMRenderConfig,
DOMRenderExtensionOutput,
DOMRenderMatch,
DOMRenderMatchConfig,
NodeMatch,
RenderContextReader,
} from './types';
const IGNORE_TAGS = new Set(['STYLE', 'SCRIPT']);
/**
* How you parse your html string to get a document is left up to you. In the browser you can use the native
* DOMParser API to generate a document (see clipboard.ts), but to use in a headless environment you can use JSDom
* or an equivalent library and pass in the document here.
*/
export function $generateNodesFromDOM(
editor: LexicalEditor,
dom: Document | ParentNode,
): Array<LexicalNode> {
$inlineStylesFromStyleSheetsDOM(dom);
const elements = isDOMDocumentNode(dom)
? dom.body.childNodes
: dom.childNodes;
const lexicalNodes: Array<LexicalNode> = [];
const allArtificialNodes: Array<ArtificialNode__DO_NOT_USE> = [];
for (const element of elements) {
if (!IGNORE_TAGS.has(element.nodeName)) {
const lexicalNode = $createNodesFromDOM(
element,
editor,
allArtificialNodes,
false,
);
if (lexicalNode !== null) {
for (const node of lexicalNode) {
lexicalNodes.push(node);
}
}
}
}
$unwrapArtificialNodes(allArtificialNodes);
return lexicalNodes;
}
/**
* Generate DOM nodes from the editor state into the given container element,
* using the editor's {@link EditorDOMRenderConfig}.
* @experimental
*/
export function $generateDOMFromNodes<T extends HTMLElement | DocumentFragment>(
container: T,
selection: null | BaseSelection = null,
editor: LexicalEditor = $getEditor(),
): T {
return $withRenderContext(
[contextValue(RenderContextExport, true)],
editor,
)(() => {
const root = $getRoot();
const domConfig = $getSessionDOMRenderConfig(editor);
const parentElementAppend = container.append.bind(container);
for (const topLevelNode of root.getChildren()) {
$appendNodesToHTML(
editor,
topLevelNode,
parentElementAppend,
selection,
domConfig,
);
}
return container;
});
}
/**
* Generate DOM nodes from a root node into the given container element,
* including the root node itself. Uses the editor's {@link EditorDOMRenderConfig}.
* @experimental
*/
export function $generateDOMFromRoot<T extends HTMLElement | DocumentFragment>(
container: T,
root: LexicalNode = $getRoot(),
): T {
const editor = $getEditor();
return $withRenderContext(
[
contextValue(RenderContextExport, true),
contextValue(RenderContextRoot, true),
],
editor,
)(() => {
const selection = null;
const domConfig = $getSessionDOMRenderConfig(editor);
const parentElementAppend = container.append.bind(container);
$appendNodesToHTML(editor, root, parentElementAppend, selection, domConfig);
return container;
});
}
/**
* Generate an HTML string from the editor's current state (or `selection`
* if provided).
*
* Must be called inside an active editor scope — i.e. `editor.update(...)`,
* `editor.read(...)`, or `editor.getEditorState().read(callback, {editor})`.
* The legacy `editor.getEditorState().read(callback)` call (without the
* `{editor}` option) does not set an active editor and is not supported;
* `editor.read(...)` is the drop-in replacement.
*/
export function $generateHtmlFromNodes(
editor: LexicalEditor,
selection: BaseSelection | null = null,
): string {
if (
typeof document === 'undefined' ||
(typeof window === 'undefined' && typeof global.window === 'undefined')
) {
invariant(
false,
'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.',
);
}
return $generateDOMFromNodes(document.createElement('div'), selection, editor)
.innerHTML;
}
function $appendNodesToHTML(
editor: LexicalEditor,
currentNode: LexicalNode,
parentElementAppend: (element: Node) => void,
selection: BaseSelection | null = null,
domConfig: EditorDOMRenderConfig = $getEditorDOMRenderConfig(editor),
): boolean {
let shouldInclude = domConfig.$shouldInclude(currentNode, selection, editor);
const shouldExclude = domConfig.$shouldExclude(
currentNode,
selection,
editor,
);
let target = currentNode;
if (selection !== null && $isTextNode(currentNode)) {
target = $sliceSelectedTextNodeContent(selection, currentNode, 'clone');
}
const exportProps = domConfig.$exportDOM(target, editor);
const {element, after, append, $getChildNodes} = exportProps;
if (!element) {
return false;
}
const fragment = document.createDocumentFragment();
const children = $getChildNodes
? $getChildNodes()
: $isElementNode(target)
? target.getChildren()
: [];
const fragmentAppend = fragment.append.bind(fragment);
for (const childNode of children) {
const shouldIncludeChild = $appendNodesToHTML(
editor,
childNode,
fragmentAppend,
selection,
domConfig,
);
if (
!shouldInclude &&
shouldIncludeChild &&
domConfig.$extractWithChild(
currentNode,
childNode,
selection,
'html',
editor,
)
) {
shouldInclude = true;
}
}
if (shouldInclude && !shouldExclude) {
if (isHTMLElement(element) || isDocumentFragment(element)) {
if (append) {
append(fragment);
} else {
element.append(fragment);
}
}
parentElementAppend(element);
if (after) {
const newElement = after.call(target, element);
if (newElement) {
if (isDocumentFragment(element)) {
element.replaceChildren(newElement);
} else {
element.replaceWith(newElement);
}
}
}
} else {
parentElementAppend(fragment);
}
return shouldInclude;
}
function getConversionFunction(
domNode: Node,
editor: LexicalEditor,
): DOMConversionFn | null {
const {nodeName} = domNode;
const cachedConversions = editor._htmlConversions.get(nodeName.toLowerCase());
let currentConversion: DOMConversion | null = null;
if (cachedConversions !== undefined) {
for (const cachedConversion of cachedConversions) {
const domConversion = cachedConversion(domNode);
if (
domConversion !== null &&
(currentConversion === null ||
// Given equal priority, prefer the last registered importer
// which is typically an application custom node or HTMLConfig['import']
(currentConversion.priority || 0) <= (domConversion.priority || 0))
) {
currentConversion = domConversion;
}
}
}
return currentConversion !== null ? currentConversion.conversion : null;
}
function $createNodesFromDOM(
node: Node,
editor: LexicalEditor,
allArtificialNodes: Array<ArtificialNode__DO_NOT_USE>,
hasBlockAncestorLexicalNode: boolean,
forChildMap: Map<string, DOMChildConversion> = new Map(),
parentLexicalNode?: LexicalNode | null | undefined,
): Array<LexicalNode> {
const lexicalNodes: Array<LexicalNode> = [];
if (IGNORE_TAGS.has(node.nodeName)) {
return lexicalNodes;
}
let currentLexicalNode = null;
const transformFunction = getConversionFunction(node, editor);
const transformOutput = transformFunction
? transformFunction(node as HTMLElement)
: null;
let postTransform = null;
if (transformOutput !== null) {
postTransform = transformOutput.after;
const transformNodes = transformOutput.node;
currentLexicalNode = Array.isArray(transformNodes)
? transformNodes[transformNodes.length - 1]
: transformNodes;
if (currentLexicalNode !== null) {
for (const [, forChildFunction] of forChildMap) {
currentLexicalNode = forChildFunction(
currentLexicalNode,
parentLexicalNode,
);
if (!currentLexicalNode) {
break;
}
}
if (currentLexicalNode) {
lexicalNodes.push(
...(Array.isArray(transformNodes)
? transformNodes
: [currentLexicalNode]),
);
}
}
if (transformOutput.forChild != null) {
forChildMap.set(node.nodeName, transformOutput.forChild);
}
}
// If the DOM node doesn't have a transformer, we don't know what
// to do with it but we still need to process any childNodes.
const children = node.childNodes;
let childLexicalNodes = [];
const hasBlockAncestorLexicalNodeForChildren =
currentLexicalNode != null && $isRootOrShadowRoot(currentLexicalNode)
? false
: (currentLexicalNode != null &&
$isBlockElementNode(currentLexicalNode)) ||
hasBlockAncestorLexicalNode;
for (let i = 0; i < children.length; i++) {
childLexicalNodes.push(
...$createNodesFromDOM(
children[i],
editor,
allArtificialNodes,
hasBlockAncestorLexicalNodeForChildren,
new Map(forChildMap),
currentLexicalNode,
),
);
}
if (postTransform != null) {
childLexicalNodes = postTransform(childLexicalNodes);
}
if (isBlockDomNode(node)) {
if (!hasBlockAncestorLexicalNodeForChildren) {
childLexicalNodes = wrapContinuousInlines(
node,
childLexicalNodes,
$createParagraphNode,
);
} else {
childLexicalNodes = wrapContinuousInlines(node, childLexicalNodes, () => {
const artificialNode = new ArtificialNode__DO_NOT_USE();
allArtificialNodes.push(artificialNode);
return artificialNode;
});
}
}
if (currentLexicalNode == null) {
if (childLexicalNodes.length > 0) {
// If it hasn't been converted to a LexicalNode, we hoist its children
// up to the same level as it.
for (const childNode of childLexicalNodes) {
lexicalNodes.push(childNode);
}
} else {
if (isBlockDomNode(node) && isDomNodeBetweenTwoInlineNodes(node)) {
// Empty block dom node that hasnt been converted, we replace it with a linebreak if its between inline nodes
lexicalNodes.push($createLineBreakNode());
}
}
} else {
if ($isElementNode(currentLexicalNode)) {
// If the current node is a ElementNode after conversion,
// we can append all the children to it.
currentLexicalNode.append(...childLexicalNodes);
}
}
return lexicalNodes;
}
function wrapContinuousInlines(
domNode: Node,
nodes: Array<LexicalNode>,
createWrapperFn: () => ElementNode,
): Array<LexicalNode> {
const textAlign = (domNode as HTMLElement).style
.textAlign as ElementFormatType;
const out: Array<LexicalNode> = [];
let continuousInlines: Array<LexicalNode> = [];
// wrap contiguous inline child nodes in para
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if ($isBlockElementNode(node)) {
if (textAlign && !node.getFormat()) {
node.setFormat(textAlign);
}
out.push(node);
} else {
continuousInlines.push(node);
if (
i === nodes.length - 1 ||
(i < nodes.length - 1 && $isBlockElementNode(nodes[i + 1]))
) {
const wrapper = createWrapperFn();
wrapper.setFormat(textAlign);
wrapper.append(...continuousInlines);
out.push(wrapper);
continuousInlines = [];
}
}
}
return out;
}
function $unwrapArtificialNodes(
allArtificialNodes: Array<ArtificialNode__DO_NOT_USE>,
) {
// Replace artificial node with its children, inserting a linebreak
// between adjacent artificial nodes
for (const node of allArtificialNodes) {
if (
node.getParent() &&
node.getNextSibling() instanceof ArtificialNode__DO_NOT_USE
) {
node.insertAfter($createLineBreakNode());
}
}
for (const node of allArtificialNodes) {
const parent = node.getParent();
if (parent) {
parent.splice(node.getIndexWithinParent(), 1, node.getChildren());
}
}
}
function isDomNodeBetweenTwoInlineNodes(node: Node): boolean {
if (node.nextSibling == null || node.previousSibling == null) {
return false;
}
return (
isInlineDomNode(node.nextSibling) && isInlineDomNode(node.previousSibling)
);
}
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import type {EditorDOMRenderConfig} from 'lexical';
import {getPeerDependencyFromEditor} from '@lexical/extension';
import {$getEditor, $getEditorDOMRenderConfig, LexicalEditor} from 'lexical';
import {DOMRenderContextSymbol, DOMRenderExtensionName} from './constants';
import {
$withContext,
createContextState,
getContextRecord,
getContextValue,
} from './ContextRecord';
import {DOMRenderExtension} from './DOMRenderExtension';
import {
AnyRenderStateConfigPairOrUpdater,
ContextRecord,
RenderStateConfig,
} from './types';
/**
* Create a context state to be used during render.
*
* Note that to support the ValueOrUpdater pattern you can not use a
* function for V (but you may wrap it in an array or object).
*
* @experimental
* @__NO_SIDE_EFFECTS__
*/
export function createRenderState<V>(
name: string,
getDefaultValue: () => V,
isEqual?: (a: V, b: V) => boolean,
): RenderStateConfig<V> {
return createContextState(
DOMRenderContextSymbol,
name,
getDefaultValue,
isEqual,
);
}
/**
* Render context state that is true if the export was initiated from the root of the document.
* @experimental
*/
export const RenderContextRoot = createRenderState('root', Boolean);
/**
* Render context state that is true if this is an export operation ($generateHtmlFromNodes).
* @experimental
*/
export const RenderContextExport = createRenderState('isExport', Boolean);
function getDefaultRenderContext(
editor: LexicalEditor,
): undefined | ContextRecord<typeof DOMRenderContextSymbol> {
const dep = getPeerDependencyFromEditor<typeof DOMRenderExtension>(
editor,
DOMRenderExtensionName,
);
return dep ? dep.output.defaults : undefined;
}
function getRenderContext(
editor: LexicalEditor,
): undefined | ContextRecord<typeof DOMRenderContextSymbol> {
return (
getContextRecord(DOMRenderContextSymbol, editor) ||
getDefaultRenderContext(editor)
);
}
/**
* Get a render context value during a DOM render or export operation.
* @experimental
*/
export function $getRenderContextValue<V>(
cfg: RenderStateConfig<V>,
editor: LexicalEditor = $getEditor(),
): V {
return getContextValue(getRenderContext(editor), cfg);
}
function getRuntime(editor: LexicalEditor) {
const dep = getPeerDependencyFromEditor<typeof DOMRenderExtension>(
editor,
DOMRenderExtensionName,
);
return dep ? dep.output.runtime : undefined;
}
/**
* Imperatively set a value in the persistent editor render context.
*
* Unlike {@link $withRenderContext} (which scopes values to a callback), this
* persists on the editor. If the change flips any override's
* `disabledForEditor` result, the resident render config is recompiled and the
* affected nodes are re-rendered. No-op if {@link DOMRenderExtension} is not
* installed.
*
* @experimental
*/
export function $setRenderContextValue<V>(
cfg: RenderStateConfig<V>,
value: V,
editor: LexicalEditor = $getEditor(),
): void {
const runtime = getRuntime(editor);
if (runtime) {
runtime.setContextValue(cfg, value);
}
}
/**
* Imperatively update a value in the persistent editor render context with an
* updater function. See {@link $setRenderContextValue}.
*
* @experimental
*/
export function $updateRenderContextValue<V>(
cfg: RenderStateConfig<V>,
updater: (prev: V) => V,
editor: LexicalEditor = $getEditor(),
): void {
const runtime = getRuntime(editor);
if (runtime) {
runtime.setContextValue(
cfg,
updater(getContextValue(runtime.editorContext, cfg)),
);
}
}
/**
* Resolve the {@link EditorDOMRenderConfig} to use for the current
* export/generate session, applying any `disabledForSession` overrides against
* the active session context. Falls back to the editor's resident config when
* {@link DOMRenderExtension} is not installed.
*
* @experimental
*/
export function $getSessionDOMRenderConfig(
editor: LexicalEditor = $getEditor(),
): EditorDOMRenderConfig {
const runtime = getRuntime(editor);
return runtime
? runtime.getSessionConfig()
: $getEditorDOMRenderConfig(editor);
}
/**
* Execute a callback within a render context with the given config pairs.
* @experimental
*/
export const $withRenderContext: (
cfg: readonly AnyRenderStateConfigPairOrUpdater[],
editor?: LexicalEditor,
) => <T>(f: () => T) => T = $withContext(
DOMRenderContextSymbol,
getDefaultRenderContext,
);
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import type {DOMImportContextSymbol, DOMRenderContextSymbol} from './constants';
import type {
BaseSelection,
DOMExportOutput,
DOMSlotForNode,
EditorDOMRenderConfig,
Klass,
LexicalEditor,
LexicalNode,
StateConfig,
} from 'lexical';
/**
* @experimental
*
* Any ContextSymbol for {@link ContextConfig} (DOM render or DOM import).
*/
export type AnyContextSymbol =
| typeof DOMRenderContextSymbol
| typeof DOMImportContextSymbol;
/**
* @experimental
*
* Context with a phantom type for its purpose (such as {@link DOMRenderContextSymbol}).
*
* A ContextRecord is a data structure used in the export and import pipelines
* to allow for information to be passed throughout the chain without explicit
* argument passing, e.g. to specify whether the intended use case for HTML
* export is for serialization or for clipboard copy.
*/
export type ContextRecord<_K extends symbol> = Record<string | symbol, unknown>;
/**
* @experimental
*
* A data structure much like StateConfig (they share implementation details)
* but for managing context during an export or import pipeline rather than
* individual node state.
*/
export type ContextConfig<Sym extends symbol, V> = StateConfig<symbol, V> & {
readonly [K in Sym]?: true;
};
/**
* @experimental
*
* Update the context at `cfg` with updater, constructed with {@link contextUpdater}
*/
export type ContextConfigUpdater<Ctx extends AnyContextSymbol, V> = {
readonly cfg: ContextConfig<Ctx, V>;
/**
* @param prev The current or default value
* @returns The new value
*/
readonly updater: (prev: V) => V;
};
/**
* @experimental
*
* Set the the context at `cfg` to a specific value, constructed with {@link contextValue}
*/
export type ContextConfigPair<Ctx extends AnyContextSymbol, V> = readonly [
ContextConfig<Ctx, V>,
V,
];
/**
* @experimental
*
* Set or update a context value, constructed with {@link contextValue} or {@link contextUpdater}
*/
export type ContextPairOrUpdater<Ctx extends AnyContextSymbol, V> =
| ContextConfigPair<Ctx, V>
| ContextConfigUpdater<Ctx, V>;
/** @experimental */
export type AnyContextConfigPairOrUpdater<Ctx extends AnyContextSymbol> =
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ContextPairOrUpdater<Ctx, any>;
/** @experimental */
export interface DOMRenderExtensionOutput {
/** @internal */
defaults: undefined | ContextRecord<typeof DOMRenderContextSymbol>;
/** @internal */
runtime: DOMRenderRuntime;
}
/**
* @experimental
*
* A read-only view of a render context layer, passed to the
* {@link DOMOverrideOptions} predicates so they can decide whether an
* override should be installed based only on context values.
*/
export interface RenderContextReader {
get<V>(cfg: RenderStateConfig<V>): V;
}
/**
* @experimental
* @internal
*
* Per-editor runtime state for {@link DOMRenderExtension} that backs the
* imperative editor context ({@link createRenderState} writes via
* `$setRenderContextValue`) and the conditional install of overrides.
*/
export interface DOMRenderRuntime {
/**
* The mutable, persistent editor-level context record. Reads of a
* {@link RenderStateConfig} during reconciliation (and as the base layer
* during a session) fall through to this record. It is also the layer
* that {@link DOMOverrideOptions.disabledForEditor} predicates read from.
*/
readonly editorContext: ContextRecord<typeof DOMRenderContextSymbol>;
/**
* Imperatively set a value in the editor context. If the change flips any
* override's `disabledForEditor` result, the resident render config is
* recompiled and the affected nodes are re-rendered (recreating DOM for
* structural overrides).
*/
setContextValue<V>(cfg: RenderStateConfig<V>, value: V): void;
/**
* Resolve the {@link EditorDOMRenderConfig} for the current export/generate
* session, applying any {@link DOMOverrideOptions.disabledForSession}
* predicates against the active session context. Returns the resident
* config when no session gating applies.
*/
getSessionConfig(): EditorDOMRenderConfig;
}
/**
* @experimental
*
* Options for {@link domOverride} controlling *whether* an override is
* installed, based only on render context. Both predicates default to
* "not disabled".
*/
export interface DOMOverrideOptions {
/**
* Gate residency in the editor's render config (used by reconciliation and
* as the base for export/generate). Evaluated against the persistent editor
* context at compile time, and re-evaluated when that context changes via
* `$setRenderContextValue`; a change recompiles the config and re-renders
* affected nodes. Return `true` to remove the override. Default: not disabled.
*/
disabledForEditor?: (ctx: RenderContextReader) => boolean;
/**
* Gate participation in a single export/generate session. Evaluated once at
* the start of each session against that session's context. Has no effect on
* live reconciliation (which is not a session). Return `true` to remove the
* override for that session. Default: not disabled.
*/
disabledForSession?: (ctx: RenderContextReader) => boolean;
}
/**
* @experimental
*
* Context configuration for render context, created with {@link createRenderState}
*/
export type RenderStateConfig<V> = ContextConfig<
typeof DOMRenderContextSymbol,
V
>;
/**
* @experimental
*
* Any setter or updater for {@link RenderStateConfig}
*/
export type AnyRenderStateConfigPairOrUpdater = AnyContextConfigPairOrUpdater<
typeof DOMRenderContextSymbol
>;
/**
* @experimental
*
* Any {@link RenderStateConfig}
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type AnyRenderStateConfig = RenderStateConfig<any>;
/**
* @experimental
*
* Configuration for {@link DOMRenderExtension}
*/
export interface DOMRenderConfig {
/**
* {@link DOMRenderMatch} overrides to customize node behavior,
* the final priority of these will be based on the following criteria:
*
* - Wildcards (`'*'`) have highest priority
* - Predicates (`$isParagraphNode`) have next priority
* - Subclasses have higher priority (e.g. `ParagraphNode` before `ElementNode`)
* - Extensions closer to the root have higher priority
* - Extensions depended on later have higher priority
* - Overrides defined later have higher priority
*/
overrides: AnyDOMRenderMatch[];
/**
* Default context to provide in all exports, the configurations are created
* with {@link createRenderState} and should be created at the module-level.
*
* Only specify these if overriding the default value globally, since each
* configuration has a built-in default value that will be used if not
* already present in the context.
*/
contextDefaults: AnyRenderStateConfigPairOrUpdater[];
}
/**
* @experimental
* Any {@link DOMRenderMatch}
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type AnyDOMRenderMatch = DOMRenderMatch<any>;
/**
* @experimental
*
* Match a node (and any subclass of that node) by its LexicalNode class,
* or with a guard (e.g. `ElementNode` or `$isElementNode`).
*
* Note that using the class compiles to significantly more efficient code
* than using a guard.
*/
export type NodeMatch<T extends LexicalNode> =
| Klass<T>
| ((node: LexicalNode) => node is T);
/**
* @experimental
*
* Used to define overrides for the render and export
* behavior for nodes matching the `nodes` predicate.
*
* All of these overrides are in a middleware style where you may use the
* result of `$next()` to enhance the result of the default implementation
* (or a lower priority override) by calling it and manipulating the result,
* or you may choose not to call `$next()` to entirely replace the behavior.
*
* It is not permitted to update the lexical editor state during any of
* these calls, you should only be doing read-only operations.
*/
export interface DOMRenderMatch<T extends LexicalNode> {
/**
* '*' for all nodes, or an array of `NodeClass | $isNodeGuard` to match
* nodes more specifically. Using classes is more efficient, but will
* also target subclasses.
*/
readonly nodes: '*' | readonly NodeMatch<T>[];
/**
* Control where an ElementNode's children are inserted into the DOM,
* this is useful to add a wrapping node or accessory nodes before or
* after the children. The root of the node returned by createDOM must
* still be exactly one HTMLElement.
*
* Generally you will call `$next()` to get a slot and then use its methods
* to create a new one. The slot type is narrowed via {@link DOMSlotForNode}:
* for `ElementNode` it resolves to {@link ElementDOMSlot} with
* children-management semantics; for non-Element nodes the base
* {@link DOMSlot} pointing at the keyed DOM.
*
* @param node The LexicalNode
* @param dom The rendered HTMLElement
* @param $next Call the next implementation
* @param editor The editor
* @returns The slot for this node
*/
$getDOMSlot?: (
node: T,
dom: HTMLElement,
$next: () => DOMSlotForNode<T>,
editor: LexicalEditor,
) => DOMSlotForNode<T>;
/**
* Called during the reconciliation process to determine which nodes
* to insert into the DOM for this Lexical Node. This is also the default
* implementation of `$exportDOM` for most nodes.
*
* This method must return exactly one `HTMLElement`.
*
* Nested elements are not supported except with `DecoratorNode`
* (which have unmanaged contents) or `ElementNode` using an appropriate
* `$getDOMSlot` return value.
*
* @param node The LexicalNode
* @param $next Call the next implementation
* @param editor The editor
* @returns The HTMLElement for this node to be rendered in the editor
*/
$createDOM?: (
node: T,
$next: () => HTMLElement,
editor: LexicalEditor,
) => HTMLElement;
/**
* Called when a node changes and should update the DOM
* in whatever way is necessary to make it align with any changes that might
* have happened during the update.
*
* Returning `true` here will cause lexical to unmount and recreate the DOM
* node (by calling `$createDOM`). You would need to do this if the element
* tag changes, for instance.
*
* @param nextNode The current version of this node
* @param prevNode The previous version of this node
* @param dom The previously rendered HTMLElement for this node
* @param $next Call the next implementation
* @param editor The editor
* @returns `false` if no update needed or was performed in-place, `true` if `$createDOM` should be called to re-create the node
*/
$updateDOM?: (
nextNode: T,
prevNode: T,
dom: HTMLElement,
$next: () => boolean,
editor: LexicalEditor,
) => boolean;
/**
* Called after a node is created or updated and should make any in-place
* updates to the DOM in whatever way is necessary to make it align with
* any changes that might have happened during the `$createDOM` or
* `$updateDOM`. This also runs after any children have been reconciled.
*
* Use this when you have code that you would need to duplicate in both
* methods, or if there is a need to ensure that the children are also
* reconciled before performing this in-place update.
*
* Unlike other overrides, all applicable `$decorateDOM` functions are
* called unconditionally. There is no `$next` argument, because there
* are no known use cases for avoiding the next implementation and due
* to the void return value it would be error-prone and add boilerplate
* to require calling it.
*
* The ordering here is equivalent to an implicit `$next` call *first*.
*
* @param nextNode The current version of this node
* @param prevNode The previous version of this node if `$updateDOM` returned `false`, or `null` if `$createDOM` was just called
* @param dom The previously rendered `HTMLElement` for this node
* @param editor The editor
*/
$decorateDOM?: (
nextNode: T,
prevNode: null | T,
dom: HTMLElement,
editor: LexicalEditor,
) => void;
/**
* Controls how the this node is serialized to HTML. This is important for
* copy and paste between Lexical and non-Lexical editors, or Lexical
* editors with different namespaces, in which case the primary transfer
* format is HTML. It's also important if you're serializing to HTML for
* any other reason via {@link @lexical/html!$generateHtmlFromNodes}.
*
* @param node The LexicalNode
* @param $next Call the next implementation
* @param editor The editor
* @returns A {@link DOMExportOutput} structure that defines how the node should be exported to HTML
*/
$exportDOM?: (
node: T,
$next: () => DOMExportOutput,
editor: LexicalEditor,
) => DOMExportOutput;
/**
* Equivalent to `ElementNode.excludeFromCopy`, if it returns `true` this
* lexical node will not be exported to DOM (but if it's an `ElementNode`
* its children may still be inserted in its place).
*
* Has higher precedence than `$shouldInclude` and `$extractWithChild`.
*
* @param node The LexicalNode
* @param selection The current selection
* @param $next The next implementation
* @param editor The editor
* @returns true to exclude this node, false otherwise
*/
$shouldExclude?: (
node: T,
selection: null | BaseSelection,
$next: () => boolean,
editor: LexicalEditor,
) => boolean;
/**
* Return `true` if this node should be included in the export, typically based
* on the current selection (all nodes by default are included when there
* is no selection).
*
* The default implementation is equivalent to
* `selection ? node.isSelected(selection) : true`.
*
* This has lower precedence than `$extractWithChild` and `$shouldExclude`.
*
* @param node The current node
* @param selection The current selection
* @param $next The next implementation
* @param editor The editor
* @returns `true` if this node should be included in the export, `false` otherwise
*/
$shouldInclude?: (
node: T,
selection: null | BaseSelection,
$next: () => boolean,
editor: LexicalEditor,
) => boolean;
/**
* Return `true` if this node should be included in the export based on
* `childNode`, even if it would not otherwise be included based on its
* `$shouldInclude` result.
*
* Typically used to ensure that required wrapping nodes are always
* present with its children, e.g. a ListNode when some of its ListItemNode
* children are selected.
*
* This has higher precedence than `$extractWithChild` and lower precedence
* than `$shouldExclude`.
*
* @param node The lexical node
* @param childNode A child of this lexical node
* @param selection The current selection
* @param destination Currently always `'html'`
* @param $next The next implementation
* @param editor The editor
* @returns true if this
*/
$extractWithChild?: (
node: T,
childNode: LexicalNode,
selection: null | BaseSelection,
destination: 'clone' | 'html',
$next: () => boolean,
editor: LexicalEditor,
) => boolean;
/**
* Set via {@link domOverride}'s options argument, not directly. See
* {@link DOMOverrideOptions.disabledForEditor}.
*/
disabledForEditor?: (ctx: RenderContextReader) => boolean;
/**
* Set via {@link domOverride}'s options argument, not directly. See
* {@link DOMOverrideOptions.disabledForSession}.
*/
disabledForSession?: (ctx: RenderContextReader) => boolean;
}
/**
* @experimental
*
* The hook fields of a {@link DOMRenderMatch} — i.e. without `nodes` or the
* {@link DOMOverrideOptions} predicates, which are passed separately to
* {@link domOverride}.
*/
export type DOMRenderMatchConfig<T extends LexicalNode> = Omit<
DOMRenderMatch<T>,
'nodes' | keyof DOMOverrideOptions
>;
+33
-18

@@ -11,5 +11,5 @@ {

"license": "MIT",
"version": "0.44.1-nightly.20260519.0",
"main": "LexicalHtml.js",
"types": "index.d.ts",
"version": "0.45.0",
"main": "./dist/LexicalHtml.js",
"types": "./dist/index.d.ts",
"repository": {

@@ -21,26 +21,41 @@ "type": "git",

"dependencies": {
"@lexical/extension": "0.44.1-nightly.20260519.0",
"@lexical/selection": "0.44.1-nightly.20260519.0",
"@lexical/utils": "0.44.1-nightly.20260519.0",
"lexical": "0.44.1-nightly.20260519.0"
"@lexical/extension": "0.45.0",
"@lexical/internal": "0.45.0",
"@lexical/selection": "0.45.0",
"@lexical/utils": "0.45.0",
"lexical": "0.45.0"
},
"module": "LexicalHtml.mjs",
"module": "./dist/LexicalHtml.mjs",
"sideEffects": false,
"exports": {
".": {
"source": "./src/index.ts",
"import": {
"types": "./index.d.ts",
"development": "./LexicalHtml.dev.mjs",
"production": "./LexicalHtml.prod.mjs",
"node": "./LexicalHtml.node.mjs",
"default": "./LexicalHtml.mjs"
"types": "./dist/index.d.ts",
"development": "./dist/LexicalHtml.dev.mjs",
"production": "./dist/LexicalHtml.prod.mjs",
"node": "./dist/LexicalHtml.node.mjs",
"default": "./dist/LexicalHtml.mjs"
},
"require": {
"types": "./index.d.ts",
"development": "./LexicalHtml.dev.js",
"production": "./LexicalHtml.prod.js",
"default": "./LexicalHtml.js"
"types": "./dist/index.d.ts",
"development": "./dist/LexicalHtml.dev.js",
"production": "./dist/LexicalHtml.prod.js",
"default": "./dist/LexicalHtml.js"
}
}
}
},
"files": [
"dist",
"src",
"!src/__tests__",
"!src/__bench__",
"!src/__mocks__",
"!src/**/*.test.ts",
"!src/**/*.test.tsx",
"!src/**/*.bench.ts",
"!src/**/*.bench.tsx",
"README.md",
"LICENSE"
]
}
import { type EditorDOMRenderConfig, InitialEditorConfig, Klass, type LexicalNode } from 'lexical';
import { AnyDOMRenderMatch, DOMRenderConfig } from './types';
interface TypeRecord {
readonly klass: Klass<LexicalNode>;
readonly types: {
[NodeAndSubclasses in string]?: boolean;
};
}
type TypeTree = {
[NodeType in string]?: TypeRecord;
};
export declare function buildTypeTree(editorConfig: Pick<InitialEditorConfig, 'nodes'>): TypeTree;
type TypeRender<T> = {
[NodeType in string]?: T[];
};
type AnyRender<T> = readonly [(node: LexicalNode) => boolean, T] | readonly ['types', TypeRender<T>];
type PreEditorDOMRenderConfig = {
[K in keyof EditorDOMRenderConfig]: AnyRender<AnyDOMRenderMatch[K]>[];
};
export declare function precompileDOMRenderConfigOverrides(editorConfig: Pick<InitialEditorConfig, 'nodes'>, overrides: DOMRenderConfig['overrides']): PreEditorDOMRenderConfig;
export declare function compileDOMRenderConfigOverrides(editorConfig: InitialEditorConfig, { overrides }: DOMRenderConfig): EditorDOMRenderConfig;
export {};
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
export declare const DOMRenderExtensionName = "@lexical/html/DOM";
export declare const DOMRenderContextSymbol: unique symbol;
export declare const ALWAYS_TRUE: () => true;
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import type { AnyContextConfigPairOrUpdater, AnyContextSymbol, ContextConfig, ContextConfigPair, ContextConfigUpdater, ContextRecord } from './types';
import { type LexicalEditor } from 'lexical';
type WithContext<Ctx extends AnyContextSymbol> = {
[K in Ctx]?: undefined | ContextRecord<Ctx>;
};
/**
* @experimental
*
* The LexicalEditor with context
*/
export type EditorContext = {
editor: LexicalEditor;
} & WithContext<AnyContextSymbol>;
/**
* @experimental
*
* @param contextRecord The ContextRecord
* @param cfg The configuration
* @returns The value or defaultValue of cfg
*/
export declare function getContextValue<Ctx extends AnyContextSymbol, V>(contextRecord: undefined | ContextRecord<Ctx>, cfg: ContextConfig<Ctx, V>): V;
/**
* @experimental
*
* Read and delete cfg from this layer of context
*
* @param contextRecord The ContextRecord
* @param cfg The configuration
* @returns The value of the configuration that was removed
*/
export declare function popOwnContextValue<Ctx extends AnyContextSymbol, V>(contextRecord: ContextRecord<Ctx>, cfg: ContextConfig<Ctx, V>): undefined | V;
/**
* @experimental
*
* Get the value without a default
*
* @param contextRecord The ContextRecord
* @param cfg The configuration
* @returns The current value in this context or `undefined` if not set
*/
export declare function getOwnContextValue<Ctx extends AnyContextSymbol, V>(contextRecord: ContextRecord<Ctx>, cfg: ContextConfig<Ctx, V>): undefined | V;
/**
* @experimental
*
* @param sym The symbol for this ContextRecord (e.g. DOMRenderContextSymbol)
* @param editor The editor
* @returns The current context or undefined
*/
export declare function getContextRecord<Ctx extends AnyContextSymbol>(sym: Ctx, editor: LexicalEditor): undefined | ContextRecord<Ctx>;
/**
* Construct a new context from a parent context and pairs
*
* @param pairs The pairs and updaters to build the context from
* @param parent The parent context
* @returns The new context
*/
export declare function contextFromPairs<Ctx extends AnyContextSymbol>(pairs: readonly AnyContextConfigPairOrUpdater<Ctx>[], parent: undefined | ContextRecord<Ctx>): undefined | ContextRecord<Ctx>;
/**
* Create a context config pair that sets a value in the render context.
* @experimental
*/
export declare function contextValue<Ctx extends AnyContextSymbol, V>(cfg: ContextConfig<Ctx, V>, value: V): ContextConfigPair<Ctx, V>;
/**
* Create a context config updater that transforms a value in the render context.
* @experimental
*/
export declare function contextUpdater<Ctx extends AnyContextSymbol, V>(cfg: ContextConfig<Ctx, V>, updater: (prev: V) => V): ContextConfigUpdater<Ctx, V>;
/**
* @internal
* @experimental
* @__NO_SIDE_EFFECTS__
*/
export declare function $withFullContext<Ctx extends AnyContextSymbol, T>(sym: Ctx, contextRecord: ContextRecord<Ctx>, f: () => T, editor?: LexicalEditor): T;
/**
* @internal
* @experimental
* @__NO_SIDE_EFFECTS__
*/
export declare function $withContext<Ctx extends AnyContextSymbol>(sym: Ctx, $defaults?: (editor: LexicalEditor) => undefined | ContextRecord<Ctx>): (cfg: readonly AnyContextConfigPairOrUpdater<Ctx>[], editor?: LexicalEditor) => (<T>(f: () => T) => T);
/**
* @experimental
* @internal
* @__NO_SIDE_EFFECTS__
*/
export declare function createContextState<Tag extends symbol, V>(tag: Tag, name: string, getDefaultValue: () => V, isEqual?: (a: V, b: V) => boolean): ContextConfig<Tag, V>;
export {};
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import type { DOMRenderMatch, NodeMatch } from './types';
import type { LexicalNode } from 'lexical';
/**
* A convenience function for type inference when constructing DOM overrides for
* use with {@link DOMRenderExtension}.
*
* @experimental
* @__NO_SIDE_EFFECTS__
*/
export declare function domOverride(nodes: '*', config: Omit<DOMRenderMatch<LexicalNode>, 'nodes'>): DOMRenderMatch<LexicalNode>;
export declare function domOverride<T extends LexicalNode>(nodes: readonly NodeMatch<T>[], config: Omit<DOMRenderMatch<T>, 'nodes'>): DOMRenderMatch<T>;
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import type { DOMRenderConfig, DOMRenderExtensionOutput } from './types';
/**
* @experimental
*
* An extension that allows overriding the render and export behavior for an
* editor. This is highly experimental and subject to change from one version
* to the next.
**/
export declare const DOMRenderExtension: import("lexical").LexicalExtension<DOMRenderConfig, "@lexical/html/DOM", DOMRenderExtensionOutput, void>;
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import type { BaseSelection, LexicalEditor, LexicalNode } from 'lexical';
export { contextUpdater, contextValue } from './ContextRecord';
export { domOverride } from './domOverride';
export { DOMRenderExtension } from './DOMRenderExtension';
export { $getRenderContextValue, $withRenderContext, createRenderState, RenderContextExport, RenderContextRoot, } from './RenderContext';
export type { AnyDOMRenderMatch, AnyRenderStateConfig, AnyRenderStateConfigPairOrUpdater, ContextPairOrUpdater, DOMRenderConfig, DOMRenderExtensionOutput, DOMRenderMatch, NodeMatch, } from './types';
/**
* How you parse your html string to get a document is left up to you. In the browser you can use the native
* DOMParser API to generate a document (see clipboard.ts), but to use in a headless environment you can use JSDom
* or an equivalent library and pass in the document here.
*/
export declare function $generateNodesFromDOM(editor: LexicalEditor, dom: Document | ParentNode): Array<LexicalNode>;
/**
* Generate DOM nodes from the editor state into the given container element,
* using the editor's {@link EditorDOMRenderConfig}.
* @experimental
*/
export declare function $generateDOMFromNodes<T extends HTMLElement | DocumentFragment>(container: T, selection?: null | BaseSelection, editor?: LexicalEditor): T;
/**
* Generate DOM nodes from a root node into the given container element,
* including the root node itself. Uses the editor's {@link EditorDOMRenderConfig}.
* @experimental
*/
export declare function $generateDOMFromRoot<T extends HTMLElement | DocumentFragment>(container: T, root?: LexicalNode): T;
export declare function $generateHtmlFromNodes(editor: LexicalEditor, selection?: BaseSelection | null): string;
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
'use strict';
var selection = require('@lexical/selection');
var lexical = require('lexical');
var extension = require('@lexical/extension');
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
// Do not require this module directly! Use normal `invariant` calls.
function formatDevErrorMessage(message) {
throw new Error(message);
}
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
let activeContext;
/**
* @experimental
*
* The LexicalEditor with context
*/
/**
* @experimental
*
* @param contextRecord The ContextRecord
* @param cfg The configuration
* @returns The value or defaultValue of cfg
*/
function getContextValue(contextRecord, cfg) {
const {
key
} = cfg;
return contextRecord && key in contextRecord ? contextRecord[key] : cfg.defaultValue;
}
function getEditorContext(editor) {
return activeContext && activeContext.editor === editor ? activeContext : undefined;
}
/**
* @experimental
*
* @param sym The symbol for this ContextRecord (e.g. DOMRenderContextSymbol)
* @param editor The editor
* @returns The current context or undefined
*/
function getContextRecord(sym, editor) {
const editorContext = getEditorContext(editor);
return editorContext && editorContext[sym];
}
function toPair(contextRecord, pairOrUpdater) {
if ('cfg' in pairOrUpdater) {
const {
cfg,
updater
} = pairOrUpdater;
return [cfg, updater(getContextValue(contextRecord, cfg))];
}
return pairOrUpdater;
}
/**
* Construct a new context from a parent context and pairs
*
* @param pairs The pairs and updaters to build the context from
* @param parent The parent context
* @returns The new context
*/
function contextFromPairs(pairs, parent) {
let rval = parent;
for (const pairOrUpdater of pairs) {
const [k, v] = toPair(rval, pairOrUpdater);
const key = k.key;
if (rval === parent && getContextValue(rval, k) === v) {
continue;
}
const ctx = rval || createChildContext(parent);
ctx[key] = v;
rval = ctx;
}
return rval;
}
function createChildContext(parent) {
return Object.create(parent || null);
}
/**
* Create a context config pair that sets a value in the render context.
* @experimental
*/
function contextValue(cfg, value) {
return [cfg, value];
}
/**
* Create a context config updater that transforms a value in the render context.
* @experimental
*/
function contextUpdater(cfg, updater) {
return {
cfg,
updater
};
}
/**
* @internal
* @experimental
* @__NO_SIDE_EFFECTS__
*/
function $withFullContext(sym, contextRecord, f, editor = lexical.$getEditor()) {
const prevDOMContext = activeContext;
const parentEditorContext = getEditorContext(editor);
try {
activeContext = {
...parentEditorContext,
editor,
[sym]: contextRecord
};
return f();
} finally {
activeContext = prevDOMContext;
}
}
/**
* @internal
* @experimental
* @__NO_SIDE_EFFECTS__
*/
function $withContext(sym, $defaults = () => undefined) {
return (cfg, editor = lexical.$getEditor()) => {
return f => {
const parentEditorContext = getEditorContext(editor);
const parentContextRecord = parentEditorContext && parentEditorContext[sym];
const contextRecord = contextFromPairs(cfg, parentContextRecord || $defaults(editor));
if (!contextRecord || contextRecord === parentContextRecord) {
return f();
}
return $withFullContext(sym, contextRecord, f, editor);
};
};
}
/**
* @experimental
* @internal
* @__NO_SIDE_EFFECTS__
*/
function createContextState(tag, name, getDefaultValue, isEqual) {
return Object.assign(lexical.createState(Symbol(name), {
isEqual,
parse: getDefaultValue
}), {
[tag]: true
});
}
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
const DOMRenderExtensionName = '@lexical/html/DOM';
const DOMRenderContextSymbol = Symbol.for('@lexical/html/DOMExportContext');
const ALWAYS_TRUE = () => true;
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
/**
* Create a context state to be used during render.
*
* Note that to support the ValueOrUpdater pattern you can not use a
* function for V (but you may wrap it in an array or object).
*
* @experimental
* @__NO_SIDE_EFFECTS__
*/
function createRenderState(name, getDefaultValue, isEqual) {
return createContextState(DOMRenderContextSymbol, name, getDefaultValue, isEqual);
}
/**
* Render context state that is true if the export was initiated from the root of the document.
* @experimental
*/
const RenderContextRoot = createRenderState('root', Boolean);
/**
* Render context state that is true if this is an export operation ($generateHtmlFromNodes).
* @experimental
*/
const RenderContextExport = createRenderState('isExport', Boolean);
function getDefaultRenderContext(editor) {
const dep = extension.getPeerDependencyFromEditor(editor, DOMRenderExtensionName);
return dep ? dep.output.defaults : undefined;
}
function getRenderContext(editor) {
return getContextRecord(DOMRenderContextSymbol, editor) || getDefaultRenderContext(editor);
}
/**
* Get a render context value during a DOM render or export operation.
* @experimental
*/
function $getRenderContextValue(cfg, editor = lexical.$getEditor()) {
return getContextValue(getRenderContext(editor), cfg);
}
/**
* Execute a callback within a render context with the given config pairs.
* @experimental
*/
const $withRenderContext = $withContext(DOMRenderContextSymbol, getDefaultRenderContext);
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
/**
* A convenience function for type inference when constructing DOM overrides for
* use with {@link DOMRenderExtension}.
*
* @experimental
* @__NO_SIDE_EFFECTS__
*/
function domOverride(nodes, config) {
return {
...config,
nodes
};
}
function buildTypeTree(editorConfig) {
const t = {};
const {
nodes
} = extension.getKnownTypesAndNodes(editorConfig);
for (const klass of nodes) {
const type = klass.getType();
t[type] = {
klass,
types: {}
};
}
for (const baseRec of Object.values(t)) {
if (baseRec) {
const baseType = baseRec.klass.getType();
for (let {
klass
} = baseRec; lexical.$isLexicalNode(klass.prototype); klass = Object.getPrototypeOf(klass)) {
const {
ownNodeType
} = lexical.getStaticNodeConfig(klass);
const superRec = ownNodeType && t[ownNodeType];
if (superRec) {
superRec.types[baseType] = true;
}
}
}
}
return t;
}
function buildNodePredicate(klass) {
return node => node instanceof klass;
}
function getPredicate(typeTree, {
nodes
}) {
if (nodes === '*') {
return ALWAYS_TRUE;
}
let types = {};
const predicates = [];
for (const klassOrPredicate of nodes) {
if ('getType' in klassOrPredicate) {
const type = klassOrPredicate.getType();
if (types) {
const tree = typeTree[type];
if (!(tree !== undefined)) {
formatDevErrorMessage(`Node class ${klassOrPredicate.name} with type ${type} not registered in editor`);
}
types = Object.assign(types, tree.types);
}
predicates.push(buildNodePredicate(klassOrPredicate));
} else {
types = undefined;
predicates.push(klassOrPredicate);
}
}
if (types) {
return types;
} else if (predicates.length === 1) {
return predicates[0];
}
return node => {
for (const predicate of predicates) {
if (predicate(node)) {
return true;
}
}
return false;
};
}
function makePrerender() {
return {
$createDOM: [],
$decorateDOM: [],
$exportDOM: [],
$extractWithChild: [],
$getDOMSlot: [],
$shouldExclude: [],
$shouldInclude: [],
$updateDOM: []
};
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function ignoreNext2(acc) {
return (node, _$next, editor) => acc(node, editor);
}
function ignoreNext3(acc) {
return (node, a, _$next, editor) => acc(node, a, editor);
}
function ignoreNext4(acc) {
return (node, a, b, _$next, editor) => acc(node, a, b, editor);
}
function ignoreNext5(acc) {
return (node, a, b, c, _$next, editor) => acc(node, a, b, c, editor);
}
function merge2($acc, $getOverride) {
return (node, editor) => {
const $next = () => $acc(node, editor);
const $override = $getOverride(node);
return $override ? $override(node, $next, editor) : $next();
};
}
function merge3(acc, $getOverride) {
return (node, a, editor) => {
const $next = () => acc(node, a, editor);
const $override = $getOverride(node);
return $override ? $override(node, a, $next, editor) : $next();
};
}
function merge4($acc, $getOverride) {
return (node, a, b, editor) => {
const $next = () => $acc(node, a, b, editor);
const $override = $getOverride(node);
return $override ? $override(node, a, b, $next, editor) : $next();
};
}
function merge5(acc, $getOverride) {
return (node, a, b, c, editor) => {
const $next = () => acc(node, a, b, c, editor);
const $override = $getOverride(node);
return $override ? $override(node, a, b, c, $next, editor) : $next();
};
}
function sequence4($acc, $getOverride) {
return (node, a, b, editor) => {
$acc(node, a, b, editor);
const $override = $getOverride(node);
if ($override) {
$override(node, a, b, editor);
}
};
}
function compilePrerenderKey(prerender, k, defaults, mergeFunction, ignoreNextFunction) {
let acc = defaults[k];
for (const pair of prerender[k]) {
if (typeof pair[0] === 'function') {
const [$predicate, $override] = pair;
acc = mergeFunction(acc, node => $predicate(node) && $override || undefined);
} else {
const typeOverrides = pair[1];
const compiled = {};
for (const type in typeOverrides) {
const arr = typeOverrides[type];
if (arr) {
compiled[type] = arr.reduce(($acc, $override) => mergeFunction($acc, () => $override), acc);
}
}
acc = mergeFunction(acc, node => {
const f = compiled[node.getType()];
return f && ignoreNextFunction(f);
});
}
}
defaults[k] = acc;
}
function addOverride(prerender, k, predicateOrTypes, override) {
if (!override) {
return;
}
const arr = prerender[k];
if (typeof predicateOrTypes === 'function') {
arr.push([predicateOrTypes, override]);
} else {
const last = arr[arr.length - 1];
let types;
if (last && last[0] === 'types') {
types = last[1];
} else {
types = {};
arr.push(['types', types]);
}
for (const type in predicateOrTypes) {
const typeArr = types[type] || [];
types[type] = typeArr;
typeArr.push(override);
}
}
}
function isWildcard(override) {
return override.nodes === '*';
}
function sortedOverrides(overrides) {
const byWildcard = [];
const byPredicate = [];
const byNode = [];
for (const override of overrides) {
if (isWildcard(override)) {
byWildcard.push(override);
} else if (Array.isArray(override.nodes)) {
for (const klassOrPredicate of override.nodes) {
if (lexical.$isLexicalNode(klassOrPredicate.prototype)) {
byNode.push(override.nodes.length === 1 ? override : {
...override,
nodes: [klassOrPredicate]
});
} else {
byPredicate.push(override.nodes.length === 1 ? override : {
...override,
nodes: [klassOrPredicate]
});
}
}
}
}
const depths = new Map();
const depthOf = klass => {
let depth = depths.get(klass);
if (depth === undefined) {
depth = 0;
for (let k = klass; lexical.$isLexicalNode(k.prototype); k = Object.getPrototypeOf(k)) {
depth++;
}
depths.set(klass, depth);
}
return depth;
};
byNode.sort((a, b) => depthOf(a.nodes[0]) - depthOf(b.nodes[0]));
return [...byNode, ...byPredicate, ...byWildcard];
}
function precompileDOMRenderConfigOverrides(editorConfig, overrides) {
const typeTree = buildTypeTree(editorConfig);
const prerender = makePrerender();
for (const override of sortedOverrides(overrides)) {
const predicateOrTypes = getPredicate(typeTree, override);
for (const k_ in prerender) {
const k = k_;
addOverride(prerender, k, predicateOrTypes, override[k]);
}
}
return prerender;
}
function identity(v) {
return v;
}
function compileDOMRenderConfigOverrides(editorConfig, {
overrides
}) {
const prerender = precompileDOMRenderConfigOverrides(editorConfig, overrides);
const dom = {
...lexical.DEFAULT_EDITOR_DOM_CONFIG,
...editorConfig.dom
};
compilePrerenderKey(prerender, '$createDOM', dom, merge2, ignoreNext2);
compilePrerenderKey(prerender, '$exportDOM', dom, merge2, ignoreNext2);
compilePrerenderKey(prerender, '$extractWithChild', dom, merge5, ignoreNext5);
compilePrerenderKey(prerender, '$getDOMSlot', dom, merge3, ignoreNext3);
compilePrerenderKey(prerender, '$shouldExclude', dom, merge3, ignoreNext3);
compilePrerenderKey(prerender, '$shouldInclude', dom, merge3, ignoreNext3);
compilePrerenderKey(prerender, '$updateDOM', dom, merge4, ignoreNext4);
compilePrerenderKey(prerender, '$decorateDOM', dom, sequence4, identity);
return dom;
}
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
/**
* @experimental
*
* An extension that allows overriding the render and export behavior for an
* editor. This is highly experimental and subject to change from one version
* to the next.
**/
const DOMRenderExtension = lexical.defineExtension({
build(editor, config, state) {
return {
defaults: contextFromPairs(config.contextDefaults, undefined)
};
},
config: {
contextDefaults: [],
overrides: []
},
html: {
// Define a RootNode export for $generateDOMFromRoot
export: new Map([[lexical.RootNode, () => {
const element = document.createElement('div');
element.role = 'textbox';
return {
element
};
}]])
},
init(editorConfig, config) {
editorConfig.dom = compileDOMRenderConfigOverrides(editorConfig, config);
},
mergeConfig(config, partial) {
const merged = lexical.shallowMergeConfig(config, partial);
for (const k of ['overrides', 'contextDefaults']) {
if (partial[k]) {
merged[k] = [...config[k], ...partial[k]];
}
}
return merged;
},
name: DOMRenderExtensionName
});
function isStyleRule(rule) {
return rule.constructor.name === CSSStyleRule.name;
}
/**
* Inlines CSS rules from <style> tags onto matching elements as inline styles.
* This is needed because apps like Excel generate HTML where styles live in
* class-based <style> rules (e.g. `.xl65 { background: #FFFF00; color: blue; }`)
* rather than inline styles. Since Lexical's import converters read inline styles,
* we resolve stylesheet rules into inline styles before conversion.
*
* Mutates the DOM in-place. Original inline styles always take precedence over
* stylesheet rules (matching CSS specificity behavior).
*/
function inlineStylesFromStyleSheets(doc) {
if (doc.querySelector('style') === null) {
return;
}
const originalInlineStyles = new Map();
function getOriginalInlineProps(el) {
let props = originalInlineStyles.get(el);
if (props === undefined) {
props = new Set();
for (let i = 0; i < el.style.length; i++) {
props.add(el.style[i]);
}
originalInlineStyles.set(el, props);
}
return props;
}
try {
for (const sheet of Array.from(doc.styleSheets)) {
let rules;
try {
rules = sheet.cssRules;
} catch (_unused) {
continue;
}
for (const rule of Array.from(rules)) {
if (!isStyleRule(rule)) {
continue;
}
let elements;
try {
elements = doc.querySelectorAll(rule.selectorText);
} catch (_unused2) {
continue;
}
for (const el of Array.from(elements)) {
if (!lexical.isHTMLElement(el)) {
continue;
}
const originalProps = getOriginalInlineProps(el);
for (let i = 0; i < rule.style.length; i++) {
const prop = rule.style[i];
if (!originalProps.has(prop)) {
el.style.setProperty(prop, rule.style.getPropertyValue(prop), rule.style.getPropertyPriority(prop));
}
}
}
}
}
} catch (_unused3) {
// styleSheets API not supported in this environment
}
}
const IGNORE_TAGS = new Set(['STYLE', 'SCRIPT']);
/**
* How you parse your html string to get a document is left up to you. In the browser you can use the native
* DOMParser API to generate a document (see clipboard.ts), but to use in a headless environment you can use JSDom
* or an equivalent library and pass in the document here.
*/
function $generateNodesFromDOM(editor, dom) {
if (lexical.isDOMDocumentNode(dom)) {
inlineStylesFromStyleSheets(dom);
}
const elements = lexical.isDOMDocumentNode(dom) ? dom.body.childNodes : dom.childNodes;
const lexicalNodes = [];
const allArtificialNodes = [];
for (const element of elements) {
if (!IGNORE_TAGS.has(element.nodeName)) {
const lexicalNode = $createNodesFromDOM(element, editor, allArtificialNodes, false);
if (lexicalNode !== null) {
for (const node of lexicalNode) {
lexicalNodes.push(node);
}
}
}
}
$unwrapArtificialNodes(allArtificialNodes);
return lexicalNodes;
}
/**
* Generate DOM nodes from the editor state into the given container element,
* using the editor's {@link EditorDOMRenderConfig}.
* @experimental
*/
function $generateDOMFromNodes(container, selection = null, editor = lexical.$getEditor()) {
return $withRenderContext([contextValue(RenderContextExport, true)], editor)(() => {
const root = lexical.$getRoot();
const domConfig = lexical.$getEditorDOMRenderConfig(editor);
const parentElementAppend = container.append.bind(container);
for (const topLevelNode of root.getChildren()) {
$appendNodesToHTML(editor, topLevelNode, parentElementAppend, selection, domConfig);
}
return container;
});
}
/**
* Generate DOM nodes from a root node into the given container element,
* including the root node itself. Uses the editor's {@link EditorDOMRenderConfig}.
* @experimental
*/
function $generateDOMFromRoot(container, root = lexical.$getRoot()) {
const editor = lexical.$getEditor();
return $withRenderContext([contextValue(RenderContextExport, true), contextValue(RenderContextRoot, true)], editor)(() => {
const selection = null;
const domConfig = lexical.$getEditorDOMRenderConfig(editor);
const parentElementAppend = container.append.bind(container);
$appendNodesToHTML(editor, root, parentElementAppend, selection, domConfig);
return container;
});
}
function $generateHtmlFromNodes(editor, selection = null) {
if (typeof document === 'undefined' || typeof window === 'undefined' && typeof global.window === 'undefined') {
{
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.`);
}
}
return $generateDOMFromNodes(document.createElement('div'), selection, editor).innerHTML;
}
function $appendNodesToHTML(editor, currentNode, parentElementAppend, selection$1 = null, domConfig = lexical.$getEditorDOMRenderConfig(editor)) {
let shouldInclude = domConfig.$shouldInclude(currentNode, selection$1, editor);
const shouldExclude = domConfig.$shouldExclude(currentNode, selection$1, editor);
let target = currentNode;
if (selection$1 !== null && lexical.$isTextNode(currentNode)) {
target = selection.$sliceSelectedTextNodeContent(selection$1, currentNode, 'clone');
}
const exportProps = domConfig.$exportDOM(target, editor);
const {
element,
after,
append,
$getChildNodes
} = exportProps;
if (!element) {
return false;
}
const fragment = document.createDocumentFragment();
const children = $getChildNodes ? $getChildNodes() : lexical.$isElementNode(target) ? target.getChildren() : [];
const fragmentAppend = fragment.append.bind(fragment);
for (const childNode of children) {
const shouldIncludeChild = $appendNodesToHTML(editor, childNode, fragmentAppend, selection$1, domConfig);
if (!shouldInclude && shouldIncludeChild && domConfig.$extractWithChild(currentNode, childNode, selection$1, 'html', editor)) {
shouldInclude = true;
}
}
if (shouldInclude && !shouldExclude) {
if (lexical.isHTMLElement(element) || lexical.isDocumentFragment(element)) {
if (append) {
append(fragment);
} else {
element.append(fragment);
}
}
parentElementAppend(element);
if (after) {
const newElement = after.call(target, element);
if (newElement) {
if (lexical.isDocumentFragment(element)) {
element.replaceChildren(newElement);
} else {
element.replaceWith(newElement);
}
}
}
} else {
parentElementAppend(fragment);
}
return shouldInclude;
}
function getConversionFunction(domNode, editor) {
const {
nodeName
} = domNode;
const cachedConversions = editor._htmlConversions.get(nodeName.toLowerCase());
let currentConversion = null;
if (cachedConversions !== undefined) {
for (const cachedConversion of cachedConversions) {
const domConversion = cachedConversion(domNode);
if (domConversion !== null && (currentConversion === null ||
// Given equal priority, prefer the last registered importer
// which is typically an application custom node or HTMLConfig['import']
(currentConversion.priority || 0) <= (domConversion.priority || 0))) {
currentConversion = domConversion;
}
}
}
return currentConversion !== null ? currentConversion.conversion : null;
}
function $createNodesFromDOM(node, editor, allArtificialNodes, hasBlockAncestorLexicalNode, forChildMap = new Map(), parentLexicalNode) {
const lexicalNodes = [];
if (IGNORE_TAGS.has(node.nodeName)) {
return lexicalNodes;
}
let currentLexicalNode = null;
const transformFunction = getConversionFunction(node, editor);
const transformOutput = transformFunction ? transformFunction(node) : null;
let postTransform = null;
if (transformOutput !== null) {
postTransform = transformOutput.after;
const transformNodes = transformOutput.node;
currentLexicalNode = Array.isArray(transformNodes) ? transformNodes[transformNodes.length - 1] : transformNodes;
if (currentLexicalNode !== null) {
for (const [, forChildFunction] of forChildMap) {
currentLexicalNode = forChildFunction(currentLexicalNode, parentLexicalNode);
if (!currentLexicalNode) {
break;
}
}
if (currentLexicalNode) {
lexicalNodes.push(...(Array.isArray(transformNodes) ? transformNodes : [currentLexicalNode]));
}
}
if (transformOutput.forChild != null) {
forChildMap.set(node.nodeName, transformOutput.forChild);
}
}
// If the DOM node doesn't have a transformer, we don't know what
// to do with it but we still need to process any childNodes.
const children = node.childNodes;
let childLexicalNodes = [];
const hasBlockAncestorLexicalNodeForChildren = currentLexicalNode != null && lexical.$isRootOrShadowRoot(currentLexicalNode) ? false : currentLexicalNode != null && lexical.$isBlockElementNode(currentLexicalNode) || hasBlockAncestorLexicalNode;
for (let i = 0; i < children.length; i++) {
childLexicalNodes.push(...$createNodesFromDOM(children[i], editor, allArtificialNodes, hasBlockAncestorLexicalNodeForChildren, new Map(forChildMap), currentLexicalNode));
}
if (postTransform != null) {
childLexicalNodes = postTransform(childLexicalNodes);
}
if (lexical.isBlockDomNode(node)) {
if (!hasBlockAncestorLexicalNodeForChildren) {
childLexicalNodes = wrapContinuousInlines(node, childLexicalNodes, lexical.$createParagraphNode);
} else {
childLexicalNodes = wrapContinuousInlines(node, childLexicalNodes, () => {
const artificialNode = new lexical.ArtificialNode__DO_NOT_USE();
allArtificialNodes.push(artificialNode);
return artificialNode;
});
}
}
if (currentLexicalNode == null) {
if (childLexicalNodes.length > 0) {
// If it hasn't been converted to a LexicalNode, we hoist its children
// up to the same level as it.
for (const childNode of childLexicalNodes) {
lexicalNodes.push(childNode);
}
} else {
if (lexical.isBlockDomNode(node) && isDomNodeBetweenTwoInlineNodes(node)) {
// Empty block dom node that hasnt been converted, we replace it with a linebreak if its between inline nodes
lexicalNodes.push(lexical.$createLineBreakNode());
}
}
} else {
if (lexical.$isElementNode(currentLexicalNode)) {
// If the current node is a ElementNode after conversion,
// we can append all the children to it.
currentLexicalNode.append(...childLexicalNodes);
}
}
return lexicalNodes;
}
function wrapContinuousInlines(domNode, nodes, createWrapperFn) {
const textAlign = domNode.style.textAlign;
const out = [];
let continuousInlines = [];
// wrap contiguous inline child nodes in para
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if (lexical.$isBlockElementNode(node)) {
if (textAlign && !node.getFormat()) {
node.setFormat(textAlign);
}
out.push(node);
} else {
continuousInlines.push(node);
if (i === nodes.length - 1 || i < nodes.length - 1 && lexical.$isBlockElementNode(nodes[i + 1])) {
const wrapper = createWrapperFn();
wrapper.setFormat(textAlign);
wrapper.append(...continuousInlines);
out.push(wrapper);
continuousInlines = [];
}
}
}
return out;
}
function $unwrapArtificialNodes(allArtificialNodes) {
// Replace artificial node with its children, inserting a linebreak
// between adjacent artificial nodes
for (const node of allArtificialNodes) {
if (node.getParent() && node.getNextSibling() instanceof lexical.ArtificialNode__DO_NOT_USE) {
node.insertAfter(lexical.$createLineBreakNode());
}
}
for (const node of allArtificialNodes) {
const parent = node.getParent();
if (parent) {
parent.splice(node.getIndexWithinParent(), 1, node.getChildren());
}
}
}
function isDomNodeBetweenTwoInlineNodes(node) {
if (node.nextSibling == null || node.previousSibling == null) {
return false;
}
return lexical.isInlineDomNode(node.nextSibling) && lexical.isInlineDomNode(node.previousSibling);
}
exports.$generateDOMFromNodes = $generateDOMFromNodes;
exports.$generateDOMFromRoot = $generateDOMFromRoot;
exports.$generateHtmlFromNodes = $generateHtmlFromNodes;
exports.$generateNodesFromDOM = $generateNodesFromDOM;
exports.$getRenderContextValue = $getRenderContextValue;
exports.$withRenderContext = $withRenderContext;
exports.DOMRenderExtension = DOMRenderExtension;
exports.RenderContextExport = RenderContextExport;
exports.RenderContextRoot = RenderContextRoot;
exports.contextUpdater = contextUpdater;
exports.contextValue = contextValue;
exports.createRenderState = createRenderState;
exports.domOverride = domOverride;
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import { $sliceSelectedTextNodeContent } from '@lexical/selection';
import { $getEditor, createState, DEFAULT_EDITOR_DOM_CONFIG, $isLexicalNode, getStaticNodeConfig, defineExtension, shallowMergeConfig, RootNode, $getRoot, $getEditorDOMRenderConfig, isDOMDocumentNode, $isTextNode, $isElementNode, isHTMLElement, isDocumentFragment, $isRootOrShadowRoot, $isBlockElementNode, isBlockDomNode, $createLineBreakNode, ArtificialNode__DO_NOT_USE, isInlineDomNode, $createParagraphNode } from 'lexical';
import { getPeerDependencyFromEditor, getKnownTypesAndNodes } from '@lexical/extension';
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
// Do not require this module directly! Use normal `invariant` calls.
function formatDevErrorMessage(message) {
throw new Error(message);
}
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
let activeContext;
/**
* @experimental
*
* The LexicalEditor with context
*/
/**
* @experimental
*
* @param contextRecord The ContextRecord
* @param cfg The configuration
* @returns The value or defaultValue of cfg
*/
function getContextValue(contextRecord, cfg) {
const {
key
} = cfg;
return contextRecord && key in contextRecord ? contextRecord[key] : cfg.defaultValue;
}
function getEditorContext(editor) {
return activeContext && activeContext.editor === editor ? activeContext : undefined;
}
/**
* @experimental
*
* @param sym The symbol for this ContextRecord (e.g. DOMRenderContextSymbol)
* @param editor The editor
* @returns The current context or undefined
*/
function getContextRecord(sym, editor) {
const editorContext = getEditorContext(editor);
return editorContext && editorContext[sym];
}
function toPair(contextRecord, pairOrUpdater) {
if ('cfg' in pairOrUpdater) {
const {
cfg,
updater
} = pairOrUpdater;
return [cfg, updater(getContextValue(contextRecord, cfg))];
}
return pairOrUpdater;
}
/**
* Construct a new context from a parent context and pairs
*
* @param pairs The pairs and updaters to build the context from
* @param parent The parent context
* @returns The new context
*/
function contextFromPairs(pairs, parent) {
let rval = parent;
for (const pairOrUpdater of pairs) {
const [k, v] = toPair(rval, pairOrUpdater);
const key = k.key;
if (rval === parent && getContextValue(rval, k) === v) {
continue;
}
const ctx = rval || createChildContext(parent);
ctx[key] = v;
rval = ctx;
}
return rval;
}
function createChildContext(parent) {
return Object.create(parent || null);
}
/**
* Create a context config pair that sets a value in the render context.
* @experimental
*/
function contextValue(cfg, value) {
return [cfg, value];
}
/**
* Create a context config updater that transforms a value in the render context.
* @experimental
*/
function contextUpdater(cfg, updater) {
return {
cfg,
updater
};
}
/**
* @internal
* @experimental
* @__NO_SIDE_EFFECTS__
*/
function $withFullContext(sym, contextRecord, f, editor = $getEditor()) {
const prevDOMContext = activeContext;
const parentEditorContext = getEditorContext(editor);
try {
activeContext = {
...parentEditorContext,
editor,
[sym]: contextRecord
};
return f();
} finally {
activeContext = prevDOMContext;
}
}
/**
* @internal
* @experimental
* @__NO_SIDE_EFFECTS__
*/
function $withContext(sym, $defaults = () => undefined) {
return (cfg, editor = $getEditor()) => {
return f => {
const parentEditorContext = getEditorContext(editor);
const parentContextRecord = parentEditorContext && parentEditorContext[sym];
const contextRecord = contextFromPairs(cfg, parentContextRecord || $defaults(editor));
if (!contextRecord || contextRecord === parentContextRecord) {
return f();
}
return $withFullContext(sym, contextRecord, f, editor);
};
};
}
/**
* @experimental
* @internal
* @__NO_SIDE_EFFECTS__
*/
function createContextState(tag, name, getDefaultValue, isEqual) {
return Object.assign(createState(Symbol(name), {
isEqual,
parse: getDefaultValue
}), {
[tag]: true
});
}
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
const DOMRenderExtensionName = '@lexical/html/DOM';
const DOMRenderContextSymbol = Symbol.for('@lexical/html/DOMExportContext');
const ALWAYS_TRUE = () => true;
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
/**
* Create a context state to be used during render.
*
* Note that to support the ValueOrUpdater pattern you can not use a
* function for V (but you may wrap it in an array or object).
*
* @experimental
* @__NO_SIDE_EFFECTS__
*/
function createRenderState(name, getDefaultValue, isEqual) {
return createContextState(DOMRenderContextSymbol, name, getDefaultValue, isEqual);
}
/**
* Render context state that is true if the export was initiated from the root of the document.
* @experimental
*/
const RenderContextRoot = createRenderState('root', Boolean);
/**
* Render context state that is true if this is an export operation ($generateHtmlFromNodes).
* @experimental
*/
const RenderContextExport = createRenderState('isExport', Boolean);
function getDefaultRenderContext(editor) {
const dep = getPeerDependencyFromEditor(editor, DOMRenderExtensionName);
return dep ? dep.output.defaults : undefined;
}
function getRenderContext(editor) {
return getContextRecord(DOMRenderContextSymbol, editor) || getDefaultRenderContext(editor);
}
/**
* Get a render context value during a DOM render or export operation.
* @experimental
*/
function $getRenderContextValue(cfg, editor = $getEditor()) {
return getContextValue(getRenderContext(editor), cfg);
}
/**
* Execute a callback within a render context with the given config pairs.
* @experimental
*/
const $withRenderContext = $withContext(DOMRenderContextSymbol, getDefaultRenderContext);
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
/**
* A convenience function for type inference when constructing DOM overrides for
* use with {@link DOMRenderExtension}.
*
* @experimental
* @__NO_SIDE_EFFECTS__
*/
function domOverride(nodes, config) {
return {
...config,
nodes
};
}
function buildTypeTree(editorConfig) {
const t = {};
const {
nodes
} = getKnownTypesAndNodes(editorConfig);
for (const klass of nodes) {
const type = klass.getType();
t[type] = {
klass,
types: {}
};
}
for (const baseRec of Object.values(t)) {
if (baseRec) {
const baseType = baseRec.klass.getType();
for (let {
klass
} = baseRec; $isLexicalNode(klass.prototype); klass = Object.getPrototypeOf(klass)) {
const {
ownNodeType
} = getStaticNodeConfig(klass);
const superRec = ownNodeType && t[ownNodeType];
if (superRec) {
superRec.types[baseType] = true;
}
}
}
}
return t;
}
function buildNodePredicate(klass) {
return node => node instanceof klass;
}
function getPredicate(typeTree, {
nodes
}) {
if (nodes === '*') {
return ALWAYS_TRUE;
}
let types = {};
const predicates = [];
for (const klassOrPredicate of nodes) {
if ('getType' in klassOrPredicate) {
const type = klassOrPredicate.getType();
if (types) {
const tree = typeTree[type];
if (!(tree !== undefined)) {
formatDevErrorMessage(`Node class ${klassOrPredicate.name} with type ${type} not registered in editor`);
}
types = Object.assign(types, tree.types);
}
predicates.push(buildNodePredicate(klassOrPredicate));
} else {
types = undefined;
predicates.push(klassOrPredicate);
}
}
if (types) {
return types;
} else if (predicates.length === 1) {
return predicates[0];
}
return node => {
for (const predicate of predicates) {
if (predicate(node)) {
return true;
}
}
return false;
};
}
function makePrerender() {
return {
$createDOM: [],
$decorateDOM: [],
$exportDOM: [],
$extractWithChild: [],
$getDOMSlot: [],
$shouldExclude: [],
$shouldInclude: [],
$updateDOM: []
};
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function ignoreNext2(acc) {
return (node, _$next, editor) => acc(node, editor);
}
function ignoreNext3(acc) {
return (node, a, _$next, editor) => acc(node, a, editor);
}
function ignoreNext4(acc) {
return (node, a, b, _$next, editor) => acc(node, a, b, editor);
}
function ignoreNext5(acc) {
return (node, a, b, c, _$next, editor) => acc(node, a, b, c, editor);
}
function merge2($acc, $getOverride) {
return (node, editor) => {
const $next = () => $acc(node, editor);
const $override = $getOverride(node);
return $override ? $override(node, $next, editor) : $next();
};
}
function merge3(acc, $getOverride) {
return (node, a, editor) => {
const $next = () => acc(node, a, editor);
const $override = $getOverride(node);
return $override ? $override(node, a, $next, editor) : $next();
};
}
function merge4($acc, $getOverride) {
return (node, a, b, editor) => {
const $next = () => $acc(node, a, b, editor);
const $override = $getOverride(node);
return $override ? $override(node, a, b, $next, editor) : $next();
};
}
function merge5(acc, $getOverride) {
return (node, a, b, c, editor) => {
const $next = () => acc(node, a, b, c, editor);
const $override = $getOverride(node);
return $override ? $override(node, a, b, c, $next, editor) : $next();
};
}
function sequence4($acc, $getOverride) {
return (node, a, b, editor) => {
$acc(node, a, b, editor);
const $override = $getOverride(node);
if ($override) {
$override(node, a, b, editor);
}
};
}
function compilePrerenderKey(prerender, k, defaults, mergeFunction, ignoreNextFunction) {
let acc = defaults[k];
for (const pair of prerender[k]) {
if (typeof pair[0] === 'function') {
const [$predicate, $override] = pair;
acc = mergeFunction(acc, node => $predicate(node) && $override || undefined);
} else {
const typeOverrides = pair[1];
const compiled = {};
for (const type in typeOverrides) {
const arr = typeOverrides[type];
if (arr) {
compiled[type] = arr.reduce(($acc, $override) => mergeFunction($acc, () => $override), acc);
}
}
acc = mergeFunction(acc, node => {
const f = compiled[node.getType()];
return f && ignoreNextFunction(f);
});
}
}
defaults[k] = acc;
}
function addOverride(prerender, k, predicateOrTypes, override) {
if (!override) {
return;
}
const arr = prerender[k];
if (typeof predicateOrTypes === 'function') {
arr.push([predicateOrTypes, override]);
} else {
const last = arr[arr.length - 1];
let types;
if (last && last[0] === 'types') {
types = last[1];
} else {
types = {};
arr.push(['types', types]);
}
for (const type in predicateOrTypes) {
const typeArr = types[type] || [];
types[type] = typeArr;
typeArr.push(override);
}
}
}
function isWildcard(override) {
return override.nodes === '*';
}
function sortedOverrides(overrides) {
const byWildcard = [];
const byPredicate = [];
const byNode = [];
for (const override of overrides) {
if (isWildcard(override)) {
byWildcard.push(override);
} else if (Array.isArray(override.nodes)) {
for (const klassOrPredicate of override.nodes) {
if ($isLexicalNode(klassOrPredicate.prototype)) {
byNode.push(override.nodes.length === 1 ? override : {
...override,
nodes: [klassOrPredicate]
});
} else {
byPredicate.push(override.nodes.length === 1 ? override : {
...override,
nodes: [klassOrPredicate]
});
}
}
}
}
const depths = new Map();
const depthOf = klass => {
let depth = depths.get(klass);
if (depth === undefined) {
depth = 0;
for (let k = klass; $isLexicalNode(k.prototype); k = Object.getPrototypeOf(k)) {
depth++;
}
depths.set(klass, depth);
}
return depth;
};
byNode.sort((a, b) => depthOf(a.nodes[0]) - depthOf(b.nodes[0]));
return [...byNode, ...byPredicate, ...byWildcard];
}
function precompileDOMRenderConfigOverrides(editorConfig, overrides) {
const typeTree = buildTypeTree(editorConfig);
const prerender = makePrerender();
for (const override of sortedOverrides(overrides)) {
const predicateOrTypes = getPredicate(typeTree, override);
for (const k_ in prerender) {
const k = k_;
addOverride(prerender, k, predicateOrTypes, override[k]);
}
}
return prerender;
}
function identity(v) {
return v;
}
function compileDOMRenderConfigOverrides(editorConfig, {
overrides
}) {
const prerender = precompileDOMRenderConfigOverrides(editorConfig, overrides);
const dom = {
...DEFAULT_EDITOR_DOM_CONFIG,
...editorConfig.dom
};
compilePrerenderKey(prerender, '$createDOM', dom, merge2, ignoreNext2);
compilePrerenderKey(prerender, '$exportDOM', dom, merge2, ignoreNext2);
compilePrerenderKey(prerender, '$extractWithChild', dom, merge5, ignoreNext5);
compilePrerenderKey(prerender, '$getDOMSlot', dom, merge3, ignoreNext3);
compilePrerenderKey(prerender, '$shouldExclude', dom, merge3, ignoreNext3);
compilePrerenderKey(prerender, '$shouldInclude', dom, merge3, ignoreNext3);
compilePrerenderKey(prerender, '$updateDOM', dom, merge4, ignoreNext4);
compilePrerenderKey(prerender, '$decorateDOM', dom, sequence4, identity);
return dom;
}
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
/**
* @experimental
*
* An extension that allows overriding the render and export behavior for an
* editor. This is highly experimental and subject to change from one version
* to the next.
**/
const DOMRenderExtension = defineExtension({
build(editor, config, state) {
return {
defaults: contextFromPairs(config.contextDefaults, undefined)
};
},
config: {
contextDefaults: [],
overrides: []
},
html: {
// Define a RootNode export for $generateDOMFromRoot
export: new Map([[RootNode, () => {
const element = document.createElement('div');
element.role = 'textbox';
return {
element
};
}]])
},
init(editorConfig, config) {
editorConfig.dom = compileDOMRenderConfigOverrides(editorConfig, config);
},
mergeConfig(config, partial) {
const merged = shallowMergeConfig(config, partial);
for (const k of ['overrides', 'contextDefaults']) {
if (partial[k]) {
merged[k] = [...config[k], ...partial[k]];
}
}
return merged;
},
name: DOMRenderExtensionName
});
function isStyleRule(rule) {
return rule.constructor.name === CSSStyleRule.name;
}
/**
* Inlines CSS rules from <style> tags onto matching elements as inline styles.
* This is needed because apps like Excel generate HTML where styles live in
* class-based <style> rules (e.g. `.xl65 { background: #FFFF00; color: blue; }`)
* rather than inline styles. Since Lexical's import converters read inline styles,
* we resolve stylesheet rules into inline styles before conversion.
*
* Mutates the DOM in-place. Original inline styles always take precedence over
* stylesheet rules (matching CSS specificity behavior).
*/
function inlineStylesFromStyleSheets(doc) {
if (doc.querySelector('style') === null) {
return;
}
const originalInlineStyles = new Map();
function getOriginalInlineProps(el) {
let props = originalInlineStyles.get(el);
if (props === undefined) {
props = new Set();
for (let i = 0; i < el.style.length; i++) {
props.add(el.style[i]);
}
originalInlineStyles.set(el, props);
}
return props;
}
try {
for (const sheet of Array.from(doc.styleSheets)) {
let rules;
try {
rules = sheet.cssRules;
} catch (_unused) {
continue;
}
for (const rule of Array.from(rules)) {
if (!isStyleRule(rule)) {
continue;
}
let elements;
try {
elements = doc.querySelectorAll(rule.selectorText);
} catch (_unused2) {
continue;
}
for (const el of Array.from(elements)) {
if (!isHTMLElement(el)) {
continue;
}
const originalProps = getOriginalInlineProps(el);
for (let i = 0; i < rule.style.length; i++) {
const prop = rule.style[i];
if (!originalProps.has(prop)) {
el.style.setProperty(prop, rule.style.getPropertyValue(prop), rule.style.getPropertyPriority(prop));
}
}
}
}
}
} catch (_unused3) {
// styleSheets API not supported in this environment
}
}
const IGNORE_TAGS = new Set(['STYLE', 'SCRIPT']);
/**
* How you parse your html string to get a document is left up to you. In the browser you can use the native
* DOMParser API to generate a document (see clipboard.ts), but to use in a headless environment you can use JSDom
* or an equivalent library and pass in the document here.
*/
function $generateNodesFromDOM(editor, dom) {
if (isDOMDocumentNode(dom)) {
inlineStylesFromStyleSheets(dom);
}
const elements = isDOMDocumentNode(dom) ? dom.body.childNodes : dom.childNodes;
const lexicalNodes = [];
const allArtificialNodes = [];
for (const element of elements) {
if (!IGNORE_TAGS.has(element.nodeName)) {
const lexicalNode = $createNodesFromDOM(element, editor, allArtificialNodes, false);
if (lexicalNode !== null) {
for (const node of lexicalNode) {
lexicalNodes.push(node);
}
}
}
}
$unwrapArtificialNodes(allArtificialNodes);
return lexicalNodes;
}
/**
* Generate DOM nodes from the editor state into the given container element,
* using the editor's {@link EditorDOMRenderConfig}.
* @experimental
*/
function $generateDOMFromNodes(container, selection = null, editor = $getEditor()) {
return $withRenderContext([contextValue(RenderContextExport, true)], editor)(() => {
const root = $getRoot();
const domConfig = $getEditorDOMRenderConfig(editor);
const parentElementAppend = container.append.bind(container);
for (const topLevelNode of root.getChildren()) {
$appendNodesToHTML(editor, topLevelNode, parentElementAppend, selection, domConfig);
}
return container;
});
}
/**
* Generate DOM nodes from a root node into the given container element,
* including the root node itself. Uses the editor's {@link EditorDOMRenderConfig}.
* @experimental
*/
function $generateDOMFromRoot(container, root = $getRoot()) {
const editor = $getEditor();
return $withRenderContext([contextValue(RenderContextExport, true), contextValue(RenderContextRoot, true)], editor)(() => {
const selection = null;
const domConfig = $getEditorDOMRenderConfig(editor);
const parentElementAppend = container.append.bind(container);
$appendNodesToHTML(editor, root, parentElementAppend, selection, domConfig);
return container;
});
}
function $generateHtmlFromNodes(editor, selection = null) {
if (typeof document === 'undefined' || typeof window === 'undefined' && typeof global.window === 'undefined') {
{
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.`);
}
}
return $generateDOMFromNodes(document.createElement('div'), selection, editor).innerHTML;
}
function $appendNodesToHTML(editor, currentNode, parentElementAppend, selection = null, domConfig = $getEditorDOMRenderConfig(editor)) {
let shouldInclude = domConfig.$shouldInclude(currentNode, selection, editor);
const shouldExclude = domConfig.$shouldExclude(currentNode, selection, editor);
let target = currentNode;
if (selection !== null && $isTextNode(currentNode)) {
target = $sliceSelectedTextNodeContent(selection, currentNode, 'clone');
}
const exportProps = domConfig.$exportDOM(target, editor);
const {
element,
after,
append,
$getChildNodes
} = exportProps;
if (!element) {
return false;
}
const fragment = document.createDocumentFragment();
const children = $getChildNodes ? $getChildNodes() : $isElementNode(target) ? target.getChildren() : [];
const fragmentAppend = fragment.append.bind(fragment);
for (const childNode of children) {
const shouldIncludeChild = $appendNodesToHTML(editor, childNode, fragmentAppend, selection, domConfig);
if (!shouldInclude && shouldIncludeChild && domConfig.$extractWithChild(currentNode, childNode, selection, 'html', editor)) {
shouldInclude = true;
}
}
if (shouldInclude && !shouldExclude) {
if (isHTMLElement(element) || isDocumentFragment(element)) {
if (append) {
append(fragment);
} else {
element.append(fragment);
}
}
parentElementAppend(element);
if (after) {
const newElement = after.call(target, element);
if (newElement) {
if (isDocumentFragment(element)) {
element.replaceChildren(newElement);
} else {
element.replaceWith(newElement);
}
}
}
} else {
parentElementAppend(fragment);
}
return shouldInclude;
}
function getConversionFunction(domNode, editor) {
const {
nodeName
} = domNode;
const cachedConversions = editor._htmlConversions.get(nodeName.toLowerCase());
let currentConversion = null;
if (cachedConversions !== undefined) {
for (const cachedConversion of cachedConversions) {
const domConversion = cachedConversion(domNode);
if (domConversion !== null && (currentConversion === null ||
// Given equal priority, prefer the last registered importer
// which is typically an application custom node or HTMLConfig['import']
(currentConversion.priority || 0) <= (domConversion.priority || 0))) {
currentConversion = domConversion;
}
}
}
return currentConversion !== null ? currentConversion.conversion : null;
}
function $createNodesFromDOM(node, editor, allArtificialNodes, hasBlockAncestorLexicalNode, forChildMap = new Map(), parentLexicalNode) {
const lexicalNodes = [];
if (IGNORE_TAGS.has(node.nodeName)) {
return lexicalNodes;
}
let currentLexicalNode = null;
const transformFunction = getConversionFunction(node, editor);
const transformOutput = transformFunction ? transformFunction(node) : null;
let postTransform = null;
if (transformOutput !== null) {
postTransform = transformOutput.after;
const transformNodes = transformOutput.node;
currentLexicalNode = Array.isArray(transformNodes) ? transformNodes[transformNodes.length - 1] : transformNodes;
if (currentLexicalNode !== null) {
for (const [, forChildFunction] of forChildMap) {
currentLexicalNode = forChildFunction(currentLexicalNode, parentLexicalNode);
if (!currentLexicalNode) {
break;
}
}
if (currentLexicalNode) {
lexicalNodes.push(...(Array.isArray(transformNodes) ? transformNodes : [currentLexicalNode]));
}
}
if (transformOutput.forChild != null) {
forChildMap.set(node.nodeName, transformOutput.forChild);
}
}
// If the DOM node doesn't have a transformer, we don't know what
// to do with it but we still need to process any childNodes.
const children = node.childNodes;
let childLexicalNodes = [];
const hasBlockAncestorLexicalNodeForChildren = currentLexicalNode != null && $isRootOrShadowRoot(currentLexicalNode) ? false : currentLexicalNode != null && $isBlockElementNode(currentLexicalNode) || hasBlockAncestorLexicalNode;
for (let i = 0; i < children.length; i++) {
childLexicalNodes.push(...$createNodesFromDOM(children[i], editor, allArtificialNodes, hasBlockAncestorLexicalNodeForChildren, new Map(forChildMap), currentLexicalNode));
}
if (postTransform != null) {
childLexicalNodes = postTransform(childLexicalNodes);
}
if (isBlockDomNode(node)) {
if (!hasBlockAncestorLexicalNodeForChildren) {
childLexicalNodes = wrapContinuousInlines(node, childLexicalNodes, $createParagraphNode);
} else {
childLexicalNodes = wrapContinuousInlines(node, childLexicalNodes, () => {
const artificialNode = new ArtificialNode__DO_NOT_USE();
allArtificialNodes.push(artificialNode);
return artificialNode;
});
}
}
if (currentLexicalNode == null) {
if (childLexicalNodes.length > 0) {
// If it hasn't been converted to a LexicalNode, we hoist its children
// up to the same level as it.
for (const childNode of childLexicalNodes) {
lexicalNodes.push(childNode);
}
} else {
if (isBlockDomNode(node) && isDomNodeBetweenTwoInlineNodes(node)) {
// Empty block dom node that hasnt been converted, we replace it with a linebreak if its between inline nodes
lexicalNodes.push($createLineBreakNode());
}
}
} else {
if ($isElementNode(currentLexicalNode)) {
// If the current node is a ElementNode after conversion,
// we can append all the children to it.
currentLexicalNode.append(...childLexicalNodes);
}
}
return lexicalNodes;
}
function wrapContinuousInlines(domNode, nodes, createWrapperFn) {
const textAlign = domNode.style.textAlign;
const out = [];
let continuousInlines = [];
// wrap contiguous inline child nodes in para
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if ($isBlockElementNode(node)) {
if (textAlign && !node.getFormat()) {
node.setFormat(textAlign);
}
out.push(node);
} else {
continuousInlines.push(node);
if (i === nodes.length - 1 || i < nodes.length - 1 && $isBlockElementNode(nodes[i + 1])) {
const wrapper = createWrapperFn();
wrapper.setFormat(textAlign);
wrapper.append(...continuousInlines);
out.push(wrapper);
continuousInlines = [];
}
}
}
return out;
}
function $unwrapArtificialNodes(allArtificialNodes) {
// Replace artificial node with its children, inserting a linebreak
// between adjacent artificial nodes
for (const node of allArtificialNodes) {
if (node.getParent() && node.getNextSibling() instanceof ArtificialNode__DO_NOT_USE) {
node.insertAfter($createLineBreakNode());
}
}
for (const node of allArtificialNodes) {
const parent = node.getParent();
if (parent) {
parent.splice(node.getIndexWithinParent(), 1, node.getChildren());
}
}
}
function isDomNodeBetweenTwoInlineNodes(node) {
if (node.nextSibling == null || node.previousSibling == null) {
return false;
}
return isInlineDomNode(node.nextSibling) && isInlineDomNode(node.previousSibling);
}
export { $generateDOMFromNodes, $generateDOMFromRoot, $generateHtmlFromNodes, $generateNodesFromDOM, $getRenderContextValue, $withRenderContext, DOMRenderExtension, RenderContextExport, RenderContextRoot, contextUpdater, contextValue, createRenderState, domOverride };
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
'use strict'
const LexicalHtml = process.env.NODE_ENV !== 'production' ? require('./LexicalHtml.dev.js') : require('./LexicalHtml.prod.js');
module.exports = LexicalHtml;

Sorry, the diff of this file is not supported yet

/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import * as modDev from './LexicalHtml.dev.mjs';
import * as modProd from './LexicalHtml.prod.mjs';
const mod = process.env.NODE_ENV !== 'production' ? modDev : modProd;
export const $generateDOMFromNodes = mod.$generateDOMFromNodes;
export const $generateDOMFromRoot = mod.$generateDOMFromRoot;
export const $generateHtmlFromNodes = mod.$generateHtmlFromNodes;
export const $generateNodesFromDOM = mod.$generateNodesFromDOM;
export const $getRenderContextValue = mod.$getRenderContextValue;
export const $withRenderContext = mod.$withRenderContext;
export const DOMRenderExtension = mod.DOMRenderExtension;
export const RenderContextExport = mod.RenderContextExport;
export const RenderContextRoot = mod.RenderContextRoot;
export const contextUpdater = mod.contextUpdater;
export const contextValue = mod.contextValue;
export const createRenderState = mod.createRenderState;
export const domOverride = mod.domOverride;
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
const mod = await (process.env.NODE_ENV !== 'production' ? import('./LexicalHtml.dev.mjs') : import('./LexicalHtml.prod.mjs'));
export const $generateDOMFromNodes = mod.$generateDOMFromNodes;
export const $generateDOMFromRoot = mod.$generateDOMFromRoot;
export const $generateHtmlFromNodes = mod.$generateHtmlFromNodes;
export const $generateNodesFromDOM = mod.$generateNodesFromDOM;
export const $getRenderContextValue = mod.$getRenderContextValue;
export const $withRenderContext = mod.$withRenderContext;
export const DOMRenderExtension = mod.DOMRenderExtension;
export const RenderContextExport = mod.RenderContextExport;
export const RenderContextRoot = mod.RenderContextRoot;
export const contextUpdater = mod.contextUpdater;
export const contextValue = mod.contextValue;
export const createRenderState = mod.createRenderState;
export const domOverride = mod.domOverride;
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
"use strict";var e=require("@lexical/selection"),t=require("lexical"),n=require("@lexical/extension");function o(e,...t){const n=new URL("https://lexical.dev/docs/error"),o=new URLSearchParams;o.append("code",e);for(const e of t)o.append("v",e);throw n.search=o.toString(),Error(`Minified Lexical error #${e}; visit ${n.toString()} for the full message or use the non-minified dev environment for full errors and additional helpful warnings.`)}let r;function s(e,t){const{key:n}=t;return e&&n in e?e[n]:t.defaultValue}function i(e){return r&&r.editor===e?r:void 0}function c(e,t){if("cfg"in t){const{cfg:n,updater:o}=t;return[n,o(s(e,n))]}return t}function l(e,t){let n=t;for(const o of e){const[e,r]=c(n,o),i=e.key;if(n===t&&s(n,e)===r)continue;const l=n||u(t);l[i]=r,n=l}return n}function u(e){return Object.create(e||null)}function f(e,t){return[e,t]}const d="@lexical/html/DOM",a=Symbol.for("@lexical/html/DOMExportContext"),p=()=>!0;function g(e,n,o){return function(e,n,o,r){return Object.assign(t.createState(Symbol(n),{isEqual:r,parse:o}),{[e]:!0})}(a,e,n,o)}const h=g("root",Boolean),m=g("isExport",Boolean);function y(e){const t=n.getPeerDependencyFromEditor(e,d);return t?t.output.defaults:void 0}function x(e){return function(e,t){const n=i(t);return n&&n[e]}(a,e)||y(e)}const $=function(e,n=()=>{}){return(o,s=t.$getEditor())=>c=>{const u=i(s),f=u&&u[e],d=l(o,f||n(s));return d&&d!==f?function(e,n,o,s=t.$getEditor()){const c=r,l=i(s);try{return r={...l,editor:s,[e]:n},o()}finally{r=c}}(e,d,c,s):c()}}(a,y);function D(e){return t=>t instanceof e}function N(e,{nodes:t}){if("*"===t)return p;let n={};const r=[];for(const s of t)if("getType"in s){const t=s.getType();if(n){const r=e[t];void 0===r&&o(339,s.name,t),n=Object.assign(n,r.types)}r.push(D(s))}else n=void 0,r.push(s);return n||(1===r.length?r[0]:e=>{for(const t of r)if(t(e))return!0;return!1})}function O(e){return(t,n,o)=>e(t,o)}function E(e){return(t,n,o,r)=>e(t,n,r)}function M(e){return(t,n,o,r,s)=>e(t,n,o,s)}function S(e){return(t,n,o,r,s,i)=>e(t,n,o,r,i)}function v(e,t){return(n,o)=>{const r=()=>e(n,o),s=t(n);return s?s(n,r,o):r()}}function C(e,t){return(n,o,r)=>{const s=()=>e(n,o,r),i=t(n);return i?i(n,o,s,r):s()}}function w(e,t){return(n,o,r,s)=>{const i=()=>e(n,o,r,s),c=t(n);return c?c(n,o,r,i,s):i()}}function R(e,t){return(n,o,r,s,i)=>{const c=()=>e(n,o,r,s,i),l=t(n);return l?l(n,o,r,s,c,i):c()}}function b(e,t){return(n,o,r,s)=>{e(n,o,r,s);const i=t(n);i&&i(n,o,r,s)}}function T(e,t,n,o,r){let s=n[t];for(const n of e[t])if("function"==typeof n[0]){const[e,t]=n;s=o(s,n=>e(n)&&t||void 0)}else{const e=n[1],t={};for(const n in e){const r=e[n];r&&(t[n]=r.reduce((e,t)=>o(e,()=>t),s))}s=o(s,e=>{const n=t[e.getType()];return n&&r(n)})}n[t]=s}function A(e,t,n,o){if(!o)return;const r=e[t];if("function"==typeof n)r.push([n,o]);else{const e=r[r.length-1];let t;e&&"types"===e[0]?t=e[1]:(t={},r.push(["types",t]));for(const e in n){const n=t[e]||[];t[e]=n,n.push(o)}}}function L(e){return"*"===e.nodes}function k(e,o){const r=function(e){const o={},{nodes:r}=n.getKnownTypesAndNodes(e);for(const e of r)o[e.getType()]={klass:e,types:{}};for(const e of Object.values(o))if(e){const n=e.klass.getType();for(let{klass:r}=e;t.$isLexicalNode(r.prototype);r=Object.getPrototypeOf(r)){const{ownNodeType:e}=t.getStaticNodeConfig(r),s=e&&o[e];s&&(s.types[n]=!0)}}return o}(e),s={$createDOM:[],$decorateDOM:[],$exportDOM:[],$extractWithChild:[],$getDOMSlot:[],$shouldExclude:[],$shouldInclude:[],$updateDOM:[]};for(const e of function(e){const n=[],o=[],r=[];for(const s of e)if(L(s))n.push(s);else if(Array.isArray(s.nodes))for(const e of s.nodes)t.$isLexicalNode(e.prototype)?r.push(1===s.nodes.length?s:{...s,nodes:[e]}):o.push(1===s.nodes.length?s:{...s,nodes:[e]});const s=new Map,i=e=>{let n=s.get(e);if(void 0===n){n=0;for(let o=e;t.$isLexicalNode(o.prototype);o=Object.getPrototypeOf(o))n++;s.set(e,n)}return n};return r.sort((e,t)=>i(e.nodes[0])-i(t.nodes[0])),[...r,...o,...n]}(o)){const t=N(r,e);for(const n in s){A(s,n,t,e[n])}}return s}function F(e){return e}const P=t.defineExtension({build:(e,t,n)=>({defaults:l(t.contextDefaults,void 0)}),config:{contextDefaults:[],overrides:[]},html:{export:new Map([[t.RootNode,()=>{const e=document.createElement("div");return e.role="textbox",{element:e}}]])},init(e,n){e.dom=function(e,{overrides:n}){const o=k(e,n),r={...t.DEFAULT_EDITOR_DOM_CONFIG,...e.dom};return T(o,"$createDOM",r,v,O),T(o,"$exportDOM",r,v,O),T(o,"$extractWithChild",r,R,S),T(o,"$getDOMSlot",r,C,E),T(o,"$shouldExclude",r,C,E),T(o,"$shouldInclude",r,C,E),T(o,"$updateDOM",r,w,M),T(o,"$decorateDOM",r,b,F),r}(e,n)},mergeConfig(e,n){const o=t.shallowMergeConfig(e,n);for(const t of["overrides","contextDefaults"])n[t]&&(o[t]=[...e[t],...n[t]]);return o},name:d});function _(e){return e.constructor.name===CSSStyleRule.name}const B=new Set(["STYLE","SCRIPT"]);function I(e,n=null,o=t.$getEditor()){return $([f(m,!0)],o)(()=>{const r=t.$getRoot(),s=t.$getEditorDOMRenderConfig(o),i=e.append.bind(e);for(const e of r.getChildren())j(o,e,i,n,s);return e})}function j(n,o,r,s=null,i=t.$getEditorDOMRenderConfig(n)){let c=i.$shouldInclude(o,s,n);const l=i.$shouldExclude(o,s,n);let u=o;null!==s&&t.$isTextNode(o)&&(u=e.$sliceSelectedTextNodeContent(s,o,"clone"));const f=i.$exportDOM(u,n),{element:d,after:a,append:p,$getChildNodes:g}=f;if(!d)return!1;const h=document.createDocumentFragment(),m=g?g():t.$isElementNode(u)?u.getChildren():[],y=h.append.bind(h);for(const e of m){const t=j(n,e,y,s,i);!c&&t&&i.$extractWithChild(o,e,s,"html",n)&&(c=!0)}if(c&&!l){if((t.isHTMLElement(d)||t.isDocumentFragment(d))&&(p?p(h):d.append(h)),r(d),a){const e=a.call(u,d);e&&(t.isDocumentFragment(d)?d.replaceChildren(e):d.replaceWith(e))}}else r(h);return c}function q(e,n,o,r,s=new Map,i){const c=[];if(B.has(e.nodeName))return c;let l=null;const u=function(e,t){const{nodeName:n}=e,o=t._htmlConversions.get(n.toLowerCase());let r=null;if(void 0!==o)for(const t of o){const n=t(e);null!==n&&(null===r||(r.priority||0)<=(n.priority||0))&&(r=n)}return null!==r?r.conversion:null}(e,n),f=u?u(e):null;let d=null;if(null!==f){d=f.after;const t=f.node;if(l=Array.isArray(t)?t[t.length-1]:t,null!==l){for(const[,e]of s)if(l=e(l,i),!l)break;l&&c.push(...Array.isArray(t)?t:[l])}null!=f.forChild&&s.set(e.nodeName,f.forChild)}const a=e.childNodes;let p=[];const g=(null==l||!t.$isRootOrShadowRoot(l))&&(null!=l&&t.$isBlockElementNode(l)||r);for(let e=0;e<a.length;e++)p.push(...q(a[e],n,o,g,new Map(s),l));if(null!=d&&(p=d(p)),t.isBlockDomNode(e)&&(p=U(e,p,g?()=>{const e=new t.ArtificialNode__DO_NOT_USE;return o.push(e),e}:t.$createParagraphNode)),null==l)if(p.length>0)for(const e of p)c.push(e);else t.isBlockDomNode(e)&&function(e){if(null==e.nextSibling||null==e.previousSibling)return!1;return t.isInlineDomNode(e.nextSibling)&&t.isInlineDomNode(e.previousSibling)}(e)&&c.push(t.$createLineBreakNode());else t.$isElementNode(l)&&l.append(...p);return c}function U(e,n,o){const r=e.style.textAlign,s=[];let i=[];for(let e=0;e<n.length;e++){const c=n[e];if(t.$isBlockElementNode(c))r&&!c.getFormat()&&c.setFormat(r),s.push(c);else if(i.push(c),e===n.length-1||e<n.length-1&&t.$isBlockElementNode(n[e+1])){const e=o();e.setFormat(r),e.append(...i),s.push(e),i=[]}}return s}exports.$generateDOMFromNodes=I,exports.$generateDOMFromRoot=function(e,n=t.$getRoot()){const o=t.$getEditor();return $([f(m,!0),f(h,!0)],o)(()=>{const r=t.$getEditorDOMRenderConfig(o),s=e.append.bind(e);return j(o,n,s,null,r),e})},exports.$generateHtmlFromNodes=function(e,t=null){return("undefined"==typeof document||"undefined"==typeof window&&void 0===global.window)&&o(338),I(document.createElement("div"),t,e).innerHTML},exports.$generateNodesFromDOM=function(e,n){t.isDOMDocumentNode(n)&&function(e){if(null===e.querySelector("style"))return;const n=new Map;function o(e){let t=n.get(e);if(void 0===t){t=new Set;for(let n=0;n<e.style.length;n++)t.add(e.style[n]);n.set(e,t)}return t}try{for(const n of Array.from(e.styleSheets)){let r;try{r=n.cssRules}catch(e){continue}for(const n of Array.from(r)){if(!_(n))continue;let r;try{r=e.querySelectorAll(n.selectorText)}catch(e){continue}for(const e of Array.from(r)){if(!t.isHTMLElement(e))continue;const r=o(e);for(let t=0;t<n.style.length;t++){const o=n.style[t];r.has(o)||e.style.setProperty(o,n.style.getPropertyValue(o),n.style.getPropertyPriority(o))}}}}}catch(e){}}(n);const o=t.isDOMDocumentNode(n)?n.body.childNodes:n.childNodes,r=[],s=[];for(const t of o)if(!B.has(t.nodeName)){const n=q(t,e,s,!1);if(null!==n)for(const e of n)r.push(e)}return function(e){for(const n of e)n.getParent()&&n.getNextSibling()instanceof t.ArtificialNode__DO_NOT_USE&&n.insertAfter(t.$createLineBreakNode());for(const t of e){const e=t.getParent();e&&e.splice(t.getIndexWithinParent(),1,t.getChildren())}}(s),r},exports.$getRenderContextValue=function(e,n=t.$getEditor()){return s(x(n),e)},exports.$withRenderContext=$,exports.DOMRenderExtension=P,exports.RenderContextExport=m,exports.RenderContextRoot=h,exports.contextUpdater=function(e,t){return{cfg:e,updater:t}},exports.contextValue=f,exports.createRenderState=g,exports.domOverride=function(e,t){return{...t,nodes:e}};
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import{$sliceSelectedTextNodeContent as t}from"@lexical/selection";import{$getEditor as n,createState as e,DEFAULT_EDITOR_DOM_CONFIG as o,$isLexicalNode as r,getStaticNodeConfig as c,defineExtension as s,shallowMergeConfig as u,RootNode as i,$getRoot as l,$getEditorDOMRenderConfig as f,isDOMDocumentNode as d,$isTextNode as a,$isElementNode as p,isHTMLElement as h,isDocumentFragment as y,$isRootOrShadowRoot as g,$isBlockElementNode as m,isBlockDomNode as x,$createLineBreakNode as v,ArtificialNode__DO_NOT_USE as $,isInlineDomNode as S,$createParagraphNode as b}from"lexical";import{getPeerDependencyFromEditor as O,getKnownTypesAndNodes as M}from"@lexical/extension";function w(t,...n){const e=new URL("https://lexical.dev/docs/error"),o=new URLSearchParams;o.append("code",t);for(const t of n)o.append("v",t);throw e.search=o.toString(),Error(`Minified Lexical error #${t}; visit ${e.toString()} for the full message or use the non-minified dev environment for full errors and additional helpful warnings.`)}let D;function C(t,n){const{key:e}=n;return t&&e in t?t[e]:n.defaultValue}function A(t){return D&&D.editor===t?D:void 0}function P(t,n){if("cfg"in n){const{cfg:e,updater:o}=n;return[e,o(C(t,e))]}return n}function E(t,n){let e=n;for(const o of t){const[t,r]=P(e,o),c=t.key;if(e===n&&C(e,t)===r)continue;const s=e||N(n);s[c]=r,e=s}return e}function N(t){return Object.create(t||null)}function T(t,n){return[t,n]}function j(t,n){return{cfg:t,updater:n}}const k="@lexical/html/DOM",L=Symbol.for("@lexical/html/DOMExportContext"),I=()=>!0;function R(t,n,o){return function(t,n,o,r){return Object.assign(e(Symbol(n),{isEqual:r,parse:o}),{[t]:!0})}(L,t,n,o)}const W=R("root",Boolean),F=R("isExport",Boolean);function q(t){const n=O(t,k);return n?n.output.defaults:void 0}function B(t){return function(t,n){const e=A(n);return e&&e[t]}(L,t)||q(t)}function U(t,e=n()){return C(B(e),t)}const V=function(t,e=()=>{}){return(o,r=n())=>c=>{const s=A(r),u=s&&s[t],i=E(o,u||e(r));return i&&i!==u?function(t,e,o,r=n()){const c=D,s=A(r);try{return D={...s,editor:r,[t]:e},o()}finally{D=c}}(t,i,c,r):c()}}(L,q);function H(t,n){return{...n,nodes:t}}function Y(t){return n=>n instanceof t}function _(t,{nodes:n}){if("*"===n)return I;let e={};const o=[];for(const r of n)if("getType"in r){const n=r.getType();if(e){const o=t[n];void 0===o&&w(339,r.name,n),e=Object.assign(e,o.types)}o.push(Y(r))}else e=void 0,o.push(r);return e||(1===o.length?o[0]:t=>{for(const n of o)if(n(t))return!0;return!1})}function z(t){return(n,e,o)=>t(n,o)}function G(t){return(n,e,o,r)=>t(n,e,r)}function J(t){return(n,e,o,r,c)=>t(n,e,o,c)}function K(t){return(n,e,o,r,c,s)=>t(n,e,o,r,s)}function Q(t,n){return(e,o)=>{const r=()=>t(e,o),c=n(e);return c?c(e,r,o):r()}}function X(t,n){return(e,o,r)=>{const c=()=>t(e,o,r),s=n(e);return s?s(e,o,c,r):c()}}function Z(t,n){return(e,o,r,c)=>{const s=()=>t(e,o,r,c),u=n(e);return u?u(e,o,r,s,c):s()}}function tt(t,n){return(e,o,r,c,s)=>{const u=()=>t(e,o,r,c,s),i=n(e);return i?i(e,o,r,c,u,s):u()}}function nt(t,n){return(e,o,r,c)=>{t(e,o,r,c);const s=n(e);s&&s(e,o,r,c)}}function et(t,n,e,o,r){let c=e[n];for(const e of t[n])if("function"==typeof e[0]){const[t,n]=e;c=o(c,e=>t(e)&&n||void 0)}else{const t=e[1],n={};for(const e in t){const r=t[e];r&&(n[e]=r.reduce((t,n)=>o(t,()=>n),c))}c=o(c,t=>{const e=n[t.getType()];return e&&r(e)})}e[n]=c}function ot(t,n,e,o){if(!o)return;const r=t[n];if("function"==typeof e)r.push([e,o]);else{const t=r[r.length-1];let n;t&&"types"===t[0]?n=t[1]:(n={},r.push(["types",n]));for(const t in e){const e=n[t]||[];n[t]=e,e.push(o)}}}function rt(t){return"*"===t.nodes}function ct(t,n){const e=function(t){const n={},{nodes:e}=M(t);for(const t of e)n[t.getType()]={klass:t,types:{}};for(const t of Object.values(n))if(t){const e=t.klass.getType();for(let{klass:o}=t;r(o.prototype);o=Object.getPrototypeOf(o)){const{ownNodeType:t}=c(o),r=t&&n[t];r&&(r.types[e]=!0)}}return n}(t),o={$createDOM:[],$decorateDOM:[],$exportDOM:[],$extractWithChild:[],$getDOMSlot:[],$shouldExclude:[],$shouldInclude:[],$updateDOM:[]};for(const t of function(t){const n=[],e=[],o=[];for(const c of t)if(rt(c))n.push(c);else if(Array.isArray(c.nodes))for(const t of c.nodes)r(t.prototype)?o.push(1===c.nodes.length?c:{...c,nodes:[t]}):e.push(1===c.nodes.length?c:{...c,nodes:[t]});const c=new Map,s=t=>{let n=c.get(t);if(void 0===n){n=0;for(let e=t;r(e.prototype);e=Object.getPrototypeOf(e))n++;c.set(t,n)}return n};return o.sort((t,n)=>s(t.nodes[0])-s(n.nodes[0])),[...o,...e,...n]}(n)){const n=_(e,t);for(const e in o){ot(o,e,n,t[e])}}return o}function st(t){return t}const ut=s({build:(t,n,e)=>({defaults:E(n.contextDefaults,void 0)}),config:{contextDefaults:[],overrides:[]},html:{export:new Map([[i,()=>{const t=document.createElement("div");return t.role="textbox",{element:t}}]])},init(t,n){t.dom=function(t,{overrides:n}){const e=ct(t,n),r={...o,...t.dom};return et(e,"$createDOM",r,Q,z),et(e,"$exportDOM",r,Q,z),et(e,"$extractWithChild",r,tt,K),et(e,"$getDOMSlot",r,X,G),et(e,"$shouldExclude",r,X,G),et(e,"$shouldInclude",r,X,G),et(e,"$updateDOM",r,Z,J),et(e,"$decorateDOM",r,nt,st),r}(t,n)},mergeConfig(t,n){const e=u(t,n);for(const o of["overrides","contextDefaults"])n[o]&&(e[o]=[...t[o],...n[o]]);return e},name:k});function it(t){return t.constructor.name===CSSStyleRule.name}const lt=new Set(["STYLE","SCRIPT"]);function ft(t,n){d(n)&&function(t){if(null===t.querySelector("style"))return;const n=new Map;function e(t){let e=n.get(t);if(void 0===e){e=new Set;for(let n=0;n<t.style.length;n++)e.add(t.style[n]);n.set(t,e)}return e}try{for(const n of Array.from(t.styleSheets)){let o;try{o=n.cssRules}catch(t){continue}for(const n of Array.from(o)){if(!it(n))continue;let o;try{o=t.querySelectorAll(n.selectorText)}catch(t){continue}for(const t of Array.from(o)){if(!h(t))continue;const o=e(t);for(let e=0;e<n.style.length;e++){const r=n.style[e];o.has(r)||t.style.setProperty(r,n.style.getPropertyValue(r),n.style.getPropertyPriority(r))}}}}}catch(t){}}(n);const e=d(n)?n.body.childNodes:n.childNodes,o=[],r=[];for(const n of e)if(!lt.has(n.nodeName)){const e=yt(n,t,r,!1);if(null!==e)for(const t of e)o.push(t)}return function(t){for(const n of t)n.getParent()&&n.getNextSibling()instanceof $&&n.insertAfter(v());for(const n of t){const t=n.getParent();t&&t.splice(n.getIndexWithinParent(),1,n.getChildren())}}(r),o}function dt(t,e=null,o=n()){return V([T(F,!0)],o)(()=>{const n=l(),r=f(o),c=t.append.bind(t);for(const t of n.getChildren())ht(o,t,c,e,r);return t})}function at(t,e=l()){const o=n();return V([T(F,!0),T(W,!0)],o)(()=>{const n=f(o),r=t.append.bind(t);return ht(o,e,r,null,n),t})}function pt(t,n=null){return("undefined"==typeof document||"undefined"==typeof window&&void 0===global.window)&&w(338),dt(document.createElement("div"),n,t).innerHTML}function ht(n,e,o,r=null,c=f(n)){let s=c.$shouldInclude(e,r,n);const u=c.$shouldExclude(e,r,n);let i=e;null!==r&&a(e)&&(i=t(r,e,"clone"));const l=c.$exportDOM(i,n),{element:d,after:g,append:m,$getChildNodes:x}=l;if(!d)return!1;const v=document.createDocumentFragment(),$=x?x():p(i)?i.getChildren():[],S=v.append.bind(v);for(const t of $){const o=ht(n,t,S,r,c);!s&&o&&c.$extractWithChild(e,t,r,"html",n)&&(s=!0)}if(s&&!u){if((h(d)||y(d))&&(m?m(v):d.append(v)),o(d),g){const t=g.call(i,d);t&&(y(d)?d.replaceChildren(t):d.replaceWith(t))}}else o(v);return s}function yt(t,n,e,o,r=new Map,c){const s=[];if(lt.has(t.nodeName))return s;let u=null;const i=function(t,n){const{nodeName:e}=t,o=n._htmlConversions.get(e.toLowerCase());let r=null;if(void 0!==o)for(const n of o){const e=n(t);null!==e&&(null===r||(r.priority||0)<=(e.priority||0))&&(r=e)}return null!==r?r.conversion:null}(t,n),l=i?i(t):null;let f=null;if(null!==l){f=l.after;const n=l.node;if(u=Array.isArray(n)?n[n.length-1]:n,null!==u){for(const[,t]of r)if(u=t(u,c),!u)break;u&&s.push(...Array.isArray(n)?n:[u])}null!=l.forChild&&r.set(t.nodeName,l.forChild)}const d=t.childNodes;let a=[];const h=(null==u||!g(u))&&(null!=u&&m(u)||o);for(let t=0;t<d.length;t++)a.push(...yt(d[t],n,e,h,new Map(r),u));if(null!=f&&(a=f(a)),x(t)&&(a=gt(t,a,h?()=>{const t=new $;return e.push(t),t}:b)),null==u)if(a.length>0)for(const t of a)s.push(t);else x(t)&&function(t){if(null==t.nextSibling||null==t.previousSibling)return!1;return S(t.nextSibling)&&S(t.previousSibling)}(t)&&s.push(v());else p(u)&&u.append(...a);return s}function gt(t,n,e){const o=t.style.textAlign,r=[];let c=[];for(let t=0;t<n.length;t++){const s=n[t];if(m(s))o&&!s.getFormat()&&s.setFormat(o),r.push(s);else if(c.push(s),t===n.length-1||t<n.length-1&&m(n[t+1])){const t=e();t.setFormat(o),t.append(...c),r.push(t),c=[]}}return r}export{dt as $generateDOMFromNodes,at as $generateDOMFromRoot,pt as $generateHtmlFromNodes,ft as $generateNodesFromDOM,U as $getRenderContextValue,V as $withRenderContext,ut as DOMRenderExtension,F as RenderContextExport,W as RenderContextRoot,j as contextUpdater,T as contextValue,R as createRenderState,H as domOverride};
import { LexicalEditor } from 'lexical';
import { AnyRenderStateConfigPairOrUpdater, RenderStateConfig } from './types';
/**
* Create a context state to be used during render.
*
* Note that to support the ValueOrUpdater pattern you can not use a
* function for V (but you may wrap it in an array or object).
*
* @experimental
* @__NO_SIDE_EFFECTS__
*/
export declare function createRenderState<V>(name: string, getDefaultValue: () => V, isEqual?: (a: V, b: V) => boolean): RenderStateConfig<V>;
/**
* Render context state that is true if the export was initiated from the root of the document.
* @experimental
*/
export declare const RenderContextRoot: RenderStateConfig<boolean>;
/**
* Render context state that is true if this is an export operation ($generateHtmlFromNodes).
* @experimental
*/
export declare const RenderContextExport: RenderStateConfig<boolean>;
/**
* Get a render context value during a DOM render or export operation.
* @experimental
*/
export declare function $getRenderContextValue<V>(cfg: RenderStateConfig<V>, editor?: LexicalEditor): V;
/**
* Execute a callback within a render context with the given config pairs.
* @experimental
*/
export declare const $withRenderContext: (cfg: readonly AnyRenderStateConfigPairOrUpdater[], editor?: LexicalEditor) => <T>(f: () => T) => T;
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import type { DOMRenderContextSymbol } from './constants';
import type { BaseSelection, DOMExportOutput, ElementDOMSlot, Klass, LexicalEditor, LexicalNode, StateConfig } from 'lexical';
/**
* @experimental
*
* Any ContextSymbol for {@link ContextConfig} (currently only {@link DOMRenderContextSymbol})
*/
export type AnyContextSymbol = typeof DOMRenderContextSymbol;
/**
* @experimental
*
* Context with a phantom type for its purpose (such as {@link DOMRenderContextSymbol}).
*
* A ContextRecord is a data structure used in the export and import pipelines
* to allow for information to be passed throughout the chain without explicit
* argument passing, e.g. to specify whether the intended use case for HTML
* export is for serialization or for clipboard copy.
*/
export type ContextRecord<_K extends symbol> = Record<string | symbol, unknown>;
/**
* @experimental
*
* A data structure much like StateConfig (they share implementation details)
* but for managing context during an export or import pipeline rather than
* individual node state.
*/
export type ContextConfig<Sym extends symbol, V> = StateConfig<symbol, V> & {
readonly [K in Sym]?: true;
};
/**
* @experimental
*
* Update the context at `cfg` with updater, constructed with {@link contextUpdater}
*/
export type ContextConfigUpdater<Ctx extends AnyContextSymbol, V> = {
readonly cfg: ContextConfig<Ctx, V>;
/**
* @param prev The current or default value
* @returns The new value
*/
readonly updater: (prev: V) => V;
};
/**
* @experimental
*
* Set the the context at `cfg` to a specific value, constructed with {@link contextValue}
*/
export type ContextConfigPair<Ctx extends AnyContextSymbol, V> = readonly [
ContextConfig<Ctx, V>,
V
];
/**
* @experimental
*
* Set or update a context value, constructed with {@link contextValue} or {@link contextUpdater}
*/
export type ContextPairOrUpdater<Ctx extends AnyContextSymbol, V> = ContextConfigPair<Ctx, V> | ContextConfigUpdater<Ctx, V>;
/** @experimental */
export type AnyContextConfigPairOrUpdater<Ctx extends AnyContextSymbol> = ContextPairOrUpdater<Ctx, any>;
/** @experimental */
export interface DOMRenderExtensionOutput {
/** @internal */
defaults: undefined | ContextRecord<typeof DOMRenderContextSymbol>;
}
/**
* @experimental
*
* Context configuration for render context, created with {@link createRenderState}
*/
export type RenderStateConfig<V> = ContextConfig<typeof DOMRenderContextSymbol, V>;
/**
* @experimental
*
* Any setter or updater for {@link RenderStateConfig}
*/
export type AnyRenderStateConfigPairOrUpdater = AnyContextConfigPairOrUpdater<typeof DOMRenderContextSymbol>;
/**
* @experimental
*
* Any {@link RenderStateConfig}
*/
export type AnyRenderStateConfig = RenderStateConfig<any>;
/**
* @experimental
*
* Configuration for {@link DOMRenderExtension}
*/
export interface DOMRenderConfig {
/**
* {@link DOMRenderMatch} overrides to customize node behavior,
* the final priority of these will be based on the following criteria:
*
* - Wildcards (`'*'`) have highest priority
* - Predicates (`$isParagraphNode`) have next priority
* - Subclasses have higher priority (e.g. `ParagraphNode` before `ElementNode`)
* - Extensions closer to the root have higher priority
* - Extensions depended on later have higher priority
* - Overrides defined later have higher priority
*/
overrides: AnyDOMRenderMatch[];
/**
* Default context to provide in all exports, the configurations are created
* with {@link createRenderState} and should be created at the module-level.
*
* Only specify these if overriding the default value globally, since each
* configuration has a built-in default value that will be used if not
* already present in the context.
*/
contextDefaults: AnyRenderStateConfigPairOrUpdater[];
}
/**
* @experimental
* Any {@link DOMRenderMatch}
*/
export type AnyDOMRenderMatch = DOMRenderMatch<any>;
/**
* @experimental
*
* Match a node (and any subclass of that node) by its LexicalNode class,
* or with a guard (e.g. `ElementNode` or `$isElementNode`).
*
* Note that using the class compiles to significantly more efficient code
* than using a guard.
*/
export type NodeMatch<T extends LexicalNode> = Klass<T> | ((node: LexicalNode) => node is T);
/**
* @experimental
*
* Used to define overrides for the render and export
* behavior for nodes matching the `nodes` predicate.
*
* All of these overrides are in a middleware style where you may use the
* result of `$next()` to enhance the result of the default implementation
* (or a lower priority override) by calling it and manipulating the result,
* or you may choose not to call `$next()` to entirely replace the behavior.
*
* It is not permitted to update the lexical editor state during any of
* these calls, you should only be doing read-only operations.
*/
export interface DOMRenderMatch<T extends LexicalNode> {
/**
* '*' for all nodes, or an array of `NodeClass | $isNodeGuard` to match
* nodes more specifically. Using classes is more efficient, but will
* also target subclasses.
*/
readonly nodes: '*' | readonly NodeMatch<T>[];
/**
* Control where an ElementNode's children are inserted into the DOM,
* this is useful to add a wrapping node or accessory nodes before or
* after the children. The root of the node returned by createDOM must
* still be exactly one HTMLElement.
*
* Generally you will call `$next()` to get an ElementDOMSlot and then use
* its methods to create a new one.
*
* @param node The LexicalNode
* @param dom The rendered HTMLElement
* @param $next Call the next implementation
* @param editor The editor
* @returns The `ElementDOMSlot` for this node
*/
$getDOMSlot?: <N extends LexicalNode>(node: N, dom: HTMLElement, $next: () => ElementDOMSlot<HTMLElement>, editor: LexicalEditor) => ElementDOMSlot<HTMLElement>;
/**
* Called during the reconciliation process to determine which nodes
* to insert into the DOM for this Lexical Node. This is also the default
* implementation of `$exportDOM` for most nodes.
*
* This method must return exactly one `HTMLElement`.
*
* Nested elements are not supported except with `DecoratorNode`
* (which have unmanaged contents) or `ElementNode` using an appropriate
* `$getDOMSlot` return value.
*
* @param node The LexicalNode
* @param $next Call the next implementation
* @param editor The editor
* @returns The HTMLElement for this node to be rendered in the editor
*/
$createDOM?: (node: T, $next: () => HTMLElement, editor: LexicalEditor) => HTMLElement;
/**
* Called when a node changes and should update the DOM
* in whatever way is necessary to make it align with any changes that might
* have happened during the update.
*
* Returning `true` here will cause lexical to unmount and recreate the DOM
* node (by calling `$createDOM`). You would need to do this if the element
* tag changes, for instance.
*
* @param nextNode The current version of this node
* @param prevNode The previous version of this node
* @param dom The previously rendered HTMLElement for this node
* @param $next Call the next implementation
* @param editor The editor
* @returns `false` if no update needed or was performed in-place, `true` if `$createDOM` should be called to re-create the node
*/
$updateDOM?: (nextNode: T, prevNode: T, dom: HTMLElement, $next: () => boolean, editor: LexicalEditor) => boolean;
/**
* Called after a node is created or updated and should make any in-place
* updates to the DOM in whatever way is necessary to make it align with
* any changes that might have happened during the `$createDOM` or
* `$updateDOM`. This also runs after any children have been reconciled.
*
* Use this when you have code that you would need to duplicate in both
* methods, or if there is a need to ensure that the children are also
* reconciled before performing this in-place update.
*
* Unlike other overrides, all applicable `$decorateDOM` functions are
* called unconditionally. There is no `$next` argument, because there
* are no known use cases for avoiding the next implementation and due
* to the void return value it would be error-prone and add boilerplate
* to require calling it.
*
* The ordering here is equivalent to an implicit `$next` call *first*.
*
* @param nextNode The current version of this node
* @param prevNode The previous version of this node if `$updateDOM` returned `false`, or `null` if `$createDOM` was just called
* @param dom The previously rendered `HTMLElement` for this node
* @param editor The editor
*/
$decorateDOM?: (nextNode: T, prevNode: null | T, dom: HTMLElement, editor: LexicalEditor) => void;
/**
* Controls how the this node is serialized to HTML. This is important for
* copy and paste between Lexical and non-Lexical editors, or Lexical
* editors with different namespaces, in which case the primary transfer
* format is HTML. It's also important if you're serializing to HTML for
* any other reason via {@link @lexical/html!$generateHtmlFromNodes}.
*
* @param node The LexicalNode
* @param $next Call the next implementation
* @param editor The editor
* @returns A {@link DOMExportOutput} structure that defines how the node should be exported to HTML
*/
$exportDOM?: (node: T, $next: () => DOMExportOutput, editor: LexicalEditor) => DOMExportOutput;
/**
* Equivalent to `ElementNode.excludeFromCopy`, if it returns `true` this
* lexical node will not be exported to DOM (but if it's an `ElementNode`
* its children may still be inserted in its place).
*
* Has higher precedence than `$shouldInclude` and `$extractWithChild`.
*
* @param node The LexicalNode
* @param selection The current selection
* @param $next The next implementation
* @param editor The editor
* @returns true to exclude this node, false otherwise
*/
$shouldExclude?: (node: T, selection: null | BaseSelection, $next: () => boolean, editor: LexicalEditor) => boolean;
/**
* Return `true` if this node should be included in the export, typically based
* on the current selection (all nodes by default are included when there
* is no selection).
*
* The default implementation is equivalent to
* `selection ? node.isSelected(selection) : true`.
*
* This has lower precedence than `$extractWithChild` and `$shouldExclude`.
*
* @param node The current node
* @param selection The current selection
* @param $next The next implementation
* @param editor The editor
* @returns `true` if this node should be included in the export, `false` otherwise
*/
$shouldInclude?: (node: T, selection: null | BaseSelection, $next: () => boolean, editor: LexicalEditor) => boolean;
/**
* Return `true` if this node should be included in the export based on
* `childNode`, even if it would not otherwise be included based on its
* `$shouldInclude` result.
*
* Typically used to ensure that required wrapping nodes are always
* present with its children, e.g. a ListNode when some of its ListItemNode
* children are selected.
*
* This has higher precedence than `$extractWithChild` and lower precedence
* than `$shouldExclude`.
*
* @param node The lexical node
* @param childNode A child of this lexical node
* @param selection The current selection
* @param destination Currently always `'html'`
* @param $next The next implementation
* @param editor The editor
* @returns true if this
*/
$extractWithChild?: (node: T, childNode: LexicalNode, selection: null | BaseSelection, destination: 'clone' | 'html', $next: () => boolean, editor: LexicalEditor) => boolean;
}