{"owner":"facebook","repo":"lexical","hasSkills":true,"totalSkillsCount":96,"totalTokensCount":140729,"categories":["root-instruction","claude-rule","plugin-manifest","anthropic-skill"],"hasMcp":false,"mcpConfig":null,"found":["AGENTS.md","CLAUDE.md","packages/lexical-a11y/README.md","packages/lexical-clipboard/README.md","packages/lexical-code-core/README.md","packages/lexical-code-prism/README.md","packages/lexical-code-shiki/README.md","packages/lexical-code/README.md","packages/lexical-compiler/README.md","packages/lexical-devtools-core/README.md","packages/lexical-devtools/README.md","packages/lexical-dragon/README.md","packages/lexical-eslint-plugin/README.md","packages/lexical-eslint-plugin/__tests__/integration/README.md","packages/lexical-extension/README.md","packages/lexical-fast-check/README.md","packages/lexical-file/README.md","packages/lexical-hashtag/README.md","packages/lexical-headless/README.md","packages/lexical-history/README.md","packages/lexical-html/README.md","packages/lexical-internal/README.md","packages/lexical-link/README.md","packages/lexical-list/README.md","packages/lexical-mark/README.md","packages/lexical-markdown/README.md","packages/lexical-mdast/README.md","packages/lexical-offset/README.md","packages/lexical-overflow/README.md","packages/lexical-plain-text/README.md","packages/lexical-react/README.md","packages/lexical-rich-text/README.md","packages/lexical-selection/README.md","packages/lexical-table/README.md","packages/lexical-tailwind/README.md","packages/lexical-text/README.md","packages/lexical-utils/README.md","packages/lexical-website/README.md","packages/lexical-website/docs/collaboration/_category_.json","packages/lexical-website/docs/collaboration/faq.md","packages/lexical-website/docs/collaboration/react.md","packages/lexical-website/docs/concepts/_category_.json","packages/lexical-website/docs/concepts/commands.md","packages/lexical-website/docs/concepts/compared-with-prosemirror.md","packages/lexical-website/docs/concepts/document-model.md","packages/lexical-website/docs/concepts/dom-events.md","packages/lexical-website/docs/concepts/editor-state.md","packages/lexical-website/docs/concepts/headless.md","packages/lexical-website/docs/concepts/history.md","packages/lexical-website/docs/concepts/key-management.md","packages/lexical-website/docs/concepts/keyboard-accessibility.md","packages/lexical-website/docs/concepts/listeners.md","packages/lexical-website/docs/concepts/named-slots.md","packages/lexical-website/docs/concepts/node-cloning.md","packages/lexical-website/docs/concepts/node-replacement.md","packages/lexical-website/docs/concepts/node-state.md","packages/lexical-website/docs/concepts/one-lexical-per-app.md","packages/lexical-website/docs/concepts/read-only.md","packages/lexical-website/docs/concepts/selection.md","packages/lexical-website/docs/concepts/shadow-dom.md","packages/lexical-website/docs/concepts/transforms.md","packages/lexical-website/docs/concepts/traversals.md","packages/lexical-website/docs/design.md","packages/lexical-website/docs/error.md","packages/lexical-website/docs/extensions/defining-extensions.md","packages/lexical-website/docs/extensions/design.md","packages/lexical-website/docs/extensions/included-extensions.md","packages/lexical-website/docs/extensions/intro.md","packages/lexical-website/docs/extensions/peer-dependencies.md","packages/lexical-website/docs/extensions/react.md","packages/lexical-website/docs/extensions/signals.md","packages/lexical-website/docs/faq.md","packages/lexical-website/docs/getting-started/_category_.json","packages/lexical-website/docs/getting-started/devtools.md","packages/lexical-website/docs/getting-started/supported-browsers.md","packages/lexical-website/docs/getting-started/theming.md","packages/lexical-website/docs/intro.md","packages/lexical-website/docs/maintainers-guide-link.md","packages/lexical-website/docs/maintainers-guide.md","packages/lexical-website/docs/react/_category_.json","packages/lexical-website/docs/react/create_plugin.md","packages/lexical-website/docs/react/faq.md","packages/lexical-website/docs/react/index.md","packages/lexical-website/docs/react/plugins.md","packages/lexical-website/docs/serialization/_category_.json","packages/lexical-website/docs/serialization/dom-import.md","packages/lexical-website/docs/serialization/dom-render.md","packages/lexical-website/docs/serialization/legacy.md","packages/lexical-website/docs/serialization/markdown-mdast.md","packages/lexical-website/docs/serialization/serialization.md","packages/lexical-website/docs/testing.md","packages/lexical-website/plugins/example-source/README.md","packages/lexical-website/src/data/README.md","packages/lexical-yjs/README.md","packages/lexical/README.md","packages/lexical/src/__bench__/README.md"],"skills":{"AGENTS.md":"# Lexical Agent Guide\n\nThis file provides detailed guidance for AI agents and automated tools working with the Lexical codebase.\n\n## Build, Test, and Development Commands\n\n### Building\n- `pnpm run build` - Build all packages in development mode\n- `pnpm run build-prod` - Clean and build all packages in production mode\n- `pnpm run build-release` - Build production release with error codes\n- `pnpm run build-types` - Build TypeScript type definitions and validate them\n\n### Testing\n- `pnpm run test-unit` - Run all unit tests (Vitest, Node/jsdom)\n- `pnpm run test-unit-watch` - Run unit tests in watch mode\n- `pnpm run test-browser` - Run browser-mode unit tests (Vitest + Playwright, real browser)\n- `pnpm run test-browser-watch` - Run browser-mode tests in watch mode\n- `pnpm run test-e2e-chromium` - Run E2E tests in Chromium (requires dev server running)\n- `pnpm run test-e2e-firefox` - Run E2E tests in Firefox\n- `pnpm run test-e2e-webkit` - Run E2E tests in WebKit\n- `pnpm run debug-test-e2e-chromium` - Run E2E tests in debug mode (headed)\n- `pnpm run debug-test-unit` - Debug unit tests with inspector\n\nFor E2E testing workflow:\n1. Start the dev server: `pnpm run start` (or `pnpm run dev` if you don't need collab)\n2. In another terminal: `pnpm run test-e2e-chromium`\n\n### Development Servers\n- `pnpm run start` - Start playground dev server + collab server (http://localhost:3000)\n- `pnpm run dev` - Start only the playground dev server (no collab)\n- `pnpm run start:website` - Start Docusaurus website (http://localhost:3001)\n- `pnpm run collab` - Start collab server on localhost:1234\n\n### Code Quality\n- `pnpm run lint` - Run ESLint on all files\n- `pnpm run lint:fix` - Auto-fix lint issues\n- `pnpm run prettier` - Check code formatting\n- `pnpm run prettier:fix` - Auto-fix formatting issues\n- `pnpm run flow` - Run a full foreground Flow check (does not reuse a server)\n- `pnpm run tsc` - Run TypeScript compiler\n- `pnpm run ci-check` - Run all checks (TypeScript, Flow, Prettier, ESLint)\n\n**Never commit changes to `scripts/error-codes/codes.json`.**\nThat edit is not yours to make — revert it to the state of\n`main` before staging, and never `git add` the file.\n\n### Searching and refactoring\n\nPrefer **ast-grep** over line-oriented regex (`grep`/`sed`) for anything\nstructural — matching or rewriting imports, call sites, JSX, type\nannotations, etc. Regexes miss multi-line forms (a symbol on its own line\ninside a multi-line `import { ... }` block) and produce false positives\n(`IS_APPLE` matching inside `IS_APPLE_WEBKIT`); ast-grep matches the syntax\ntree, so it does neither.\n\nIt isn't a dependency, so run it via npx (the CLI binary is `ast-grep`, not\nthe shadow-utils `sg` that may be on `PATH`):\n\n```sh\n# Find every import of something from '@lexical/utils' (ts and tsx are\n# separate grammars, so pass -l for each; add --json=compact to post-process)\nnpx --package @ast-grep/cli ast-grep run \\\n  -p \"import { \\$\\$\\$NAMES } from '@lexical/utils'\" -l ts packages\n```\n\nMetavariables (`$NAME`, `$$$LIST`) capture nodes for reporting or rewriting\nwith `--rewrite`. Reach for it whenever a change spans many files and must be\nprecise — e.g. moving symbols that `@lexical/utils` merely re-exports back to\na direct `lexical` import.\n\n### Extension source imports\n\nImport from the `@lexical/extension` barrel across package boundaries. Within\n`packages/lexical-extension`, use relative imports and re-exports. Do not\nmanually rewrite these source imports to `@lexical/extension/*`: ESLint\nenforces the source convention, and `@lexical/compiler`'s `subpathImports`\npass rewrites them in all published package builds, including www. Public\nsibling entries stay external so shared modules such as signals retain one\ninstance. Helpers with only one owning entry live under `src/internal` and\nare bundled into that entry, without exposing additional public subpaths.\nDownstream users may import either the barrel or public subpaths.\n\n### Tree-shaking annotations\n\nModule-scope calls to the side-effect-free factories (`defineExtension`,\n`configExtension`, `safeCast`, `createCommand`, `createState`,\n`defineImportRule`, etc.) need a `/* @__PURE__ */` annotation so bundlers\ncan drop unused definitions from application bundles. **Do not write those\nannotations by hand** — they are injected at build time by\n`@lexical/compiler` (`packages/lexical-compiler`), which\nruns as a Rollup plugin in `scripts/build.mjs` for the published bundles\nand as a Vite plugin in `scripts/vite/lexicalMonorepoPlugin.ts` for\neverything the monorepo builds from source. The published package is also\nwhat consumers add to their own build when they compile Lexical from its\n`source` export condition rather than from `dist`.\n\nA hand-written annotation on one of these calls is a lint error\n(`@lexical/internal/no-pure-annotation`), and the rule is autofixable, so a\nbranch written before the transform existed migrates with\n`pnpm run lint:fix`. Annotations on anything else — a third-party factory, or\na call inside a function body, where the build never injects one — are left\nalone.\n\nWhen adding a new factory of this kind, annotate its definition with\n`@__NO_SIDE_EFFECTS__`. That alone is enough for calls in the same package to\nbe annotated; add its name to `PURE_FACTORY_FUNCTIONS` in\n`packages/lexical-compiler/src/LexicalCompiler.mjs` so that\ncalls in code that imports it by package name are too. An object whose\nmethods build values and touch nothing else (like `@lexical/html`'s `sel`) is\nmarked `@lexical-pure-namespace` instead, so that `sel.tag('p')` is annotated\nthe way a factory call is.\n\nThe build runs with `strict`, which fails on a call inside one of these\ndefinitions that nothing has established is side-effect free — such a call\npins the definition into every bundle that imports the module, however well\nannotated the definition itself is. If it stops you, either make the call\nlazy (a `nodes: () => [...]` callback is not evaluated at module scope), or\ndeclare the function side-effect free so its calls are annotated too. A\nthird-party factory that cannot be declared is annotated by hand at the call\nsite, which is what the remaining hand-written annotations in the tree are.\n\nFactory calls are not the only module-scope statements that pin a module.\nesbuild and webpack keep, as a side effect, any property read at module scope\n(`navigator.userAgent`, `Date.now`, `SomeExtension.name`), any call or `new`\nthey were not told is pure (`new RegExp(...)`, `Object.freeze(...)`,\n`[...].join(',')`), an array or object spread, an `in` test, a `try`\nstatement, a mutation (`table.push(...)`, `Klass.static = ...`), a class with\na computed member (`[SOME_SYMBOL]?: number` — write `declare` on it), and an\nIIFE — together with everything the statement references. One\n`LexicalEditor.version = ...` after the class kept nearly all of `lexical` in\na bundle that imported only `createCommand` (#9120). Move such work into a\nfunction declared `@__NO_SIDE_EFFECTS__` and call it at module scope, so the\nbuild annotates the call; annotate a call to a builtin or to a third-party\nfunction (`/* @__PURE__ */ forwardRef(...)`) by hand. Two tests enforce this\nby bundling a bare `import 'pkg'` of every entry with esbuild and requiring\nthat nothing is retained: `scripts/__tests__/unit/treeShakingSource.test.ts`\non the sources (runs with `pnpm run test-unit`) and\n`scripts/__tests__/integration/tree-shaking.test.mjs` on the published\nbuilds. A failure prints the retained code; its first statement is usually\nthe culprit.\n\nA factory whose body is a trivial expression over its own arguments (like\n`safeCast`, `defineExtension`, or `configExtension`) is additionally marked\n`@lexical-inline <form>` and listed in that file's `INLINE_FACTORIES`: the\nbuild replaces calls to it with the expression it would have returned, which\nneeds no annotation at all. **The body of a marked function is reproduced by\nthe build**, so keep it trivial — `packages/lexical-compiler`'s unit\ntests compare what the transform emits against what the real function returns\nand will fail if the two drift apart.\n\n## High-Level Architecture\n\n### Core Concepts\n\nLexical is built around several key architectural concepts that work together:\n\n**Editor Instance** - Created via `createEditor()`, wires everything together. Manages the EditorState, registers listeners/commands/transforms, and handles DOM reconciliation.\n\n**EditorState** - Immutable data model representing the editor content. Contains:\n- A node tree (hierarchical structure of LexicalNodes)\n- A selection object (current cursor/selection state)\n- Fully serializable to/from JSON\n\n**`$` Functions Convention** - Functions prefixed with `$` (e.g., `$getRoot()`, `$getSelection()`) can ONLY be called within:\n- `editor.update(() => {...})` - for mutations\n- `editor.read(() => {...})` - for read-only access\n- Node transforms and command handlers (which have implicit update context)\n\nThis is similar to React hooks' restrictions but enforces synchronous context instead of call order.\n\n**Double-Buffering Updates** - When `editor.update()` is called:\n1. Current EditorState is cloned as work-in-progress\n2. Mutations modify the work-in-progress state\n3. Multiple synchronous updates are batched\n4. DOM reconciler diffs and applies changes\n5. New immutable EditorState becomes current\n\n**Node Immutability & Keys** - All nodes are recursively frozen after reconciliation. Node methods automatically call `node.getWritable()` to create mutable clones. All versions of a logical node share the same runtime-only key, allowing node methods to always reference the latest version from the active EditorState.\n\n### Monorepo Structure\n\nThis is a monorepo with packages in `packages/`:\n\n**Core Packages:**\n- `lexical` - Core framework (Editor, EditorState, base nodes, selection, updates)\n- `@lexical/react` - React bindings (LexicalComposer, plugins as components)\n- `@lexical/headless` - Headless editor for server-side/testing\n\n**Feature Packages** (extend core with nodes/commands/utilities):\n- `@lexical/rich-text` - Rich text editing (headings, quotes, etc.)\n- `@lexical/plain-text` - Plain text editing\n- `@lexical/extension` - Extend editor functionality\n- `@lexical/list` - List nodes (ordered/unordered/checklist)\n- `@lexical/table` - Table support\n- `@lexical/code` - Code block with syntax highlighting\n- `@lexical/link` - Link nodes and utilities\n- `@lexical/markdown` - Markdown import/export\n- `@lexical/html` - HTML serialization\n- `@lexical/history` - Undo/redo\n- `@lexical/yjs` - Real-time collaboration via Yjs\n- And many more...\n\n**Development Packages:**\n- `lexical-playground` - Full-featured demo application\n- `lexical-website` - Docusaurus documentation site\n\n### Key Architectural Patterns\n\n**Extensions** - Extensions should be used to add features and configuration\nto an editor. The set of extensions in an editor must be determined when the\neditor is created with `buildEditorFromExtensions`. Extensions with\nfunctionality that can be toggled on or off typically have a `disabled`\nconfiguration property and output signal that defaults to `false`. See the\nlexical-extension package and the supporting code in lexical for\nmore examples and implementation details.\n\n```tsx\nexport interface MyConfig {\n  disabled: boolean;\n}\nexport const MyExtension = defineExtension({\n  build: (_editor, config, _state) => namedSignals(config),\n  config: safeCast<MyConfig>({ disabled: false }),\n  name: '@lexical/docs/My',\n  nodes: () => [MyNode],\n  register: (editor, _config, state) => {\n    const {disabled} = state.getOutput();\n    return effect(() => {\n      if (!disabled.value) {\n        return editor.registerUpdateListener(({editorState}) => {\n          // React to updates\n        });\n      }\n    })\n  },\n})\n```\n\n**Plugin System (React)** - Plugins are a legacy pattern for React components\nto hook into the editor lifecycle, extensions should be preferred for new code:\n```jsx\nfunction MyPlugin() {\n  const [editor] = useLexicalComposerContext();\n  useEffect(() => {\n    return editor.registerUpdateListener(({editorState}) => {\n      // React to updates\n    });\n  }, [editor]);\n  return null;\n}\n```\n\n**Command Pattern** - Commands are the primary communication mechanism:\n- Create with `createCommand()`\n- Dispatch with `editor.dispatchCommand(command, payload)`\n- Handle with `editor.registerCommand(command, handler, priority)`\n- Handlers propagate by priority until one stops propagation\n\n**Node Transforms** - Registered via `editor.registerNodeTransform(NodeClass, transform)`. Called automatically during updates when nodes of that type change. Have implicit update context.\n\n**Listeners** - All `editor.register*()` methods return cleanup functions for easy unsubscription.\n\n## Type System\n\nThis codebase uses **both TypeScript and Flow**:\n- Source files are primarily TypeScript (`.ts`, `.tsx`)\n- Flow type definitions are generated in `packages/*/flow/` directories\n- Run `pnpm run flow` to check Flow types\n- Run `pnpm run tsc` to check TypeScript types\n- Both are checked in CI via `pnpm run ci-check`\n\nWhen adding/modifying APIs, types must be maintained for both systems.\n\n## Backwards Compatibility\n\n**All changes MUST be backwards compatible.** Lexical is a widely-adopted OSS library, and breaking changes ripple out to every downstream consumer.\n\n- Do NOT remove or rename existing public APIs, exported functions, types, or `$` functions. Add new APIs alongside the old ones instead.\n- Do NOT change the signature, return type, or behavior of existing public APIs in ways that could break callers. Prefer additive, optional parameters.\n- Preserve the serialization format of `EditorState` and node JSON. Serialized content produced by older versions must continue to deserialize correctly.\n- If an API genuinely must change, deprecate the old one first (keep it working, document the replacement) rather than removing it outright.\n- When in doubt, assume external code depends on the current behavior and keep it intact.\n\n## Dependencies: Lexical is a singleton within one app\n\nAn application must resolve exactly one copy of `lexical`, and one copy of each\n`@lexical/*` package it uses. The active editor and editor state are\nmodule-scope variables in `LexicalUpdates.ts`, node registration and\n`instanceof LexicalNode` compare class references, and commands are object\nidentities from `createCommand()` — none of which survive a second copy of the\nmodule. Two copies also mean two versions, which is API drift inside one editor.\n\nThe boundary is the app, not the page: several self-contained apps, each with\nits own bundled Lexical, can coexist on one page (`isLexicalEditor` is an\n`instanceof` check precisely to support that). They just cannot interoperate —\nnodes, editors, `EditorState`s, selections and commands must never be passed\nacross such a boundary, only serialized data.\n\n- Inside this monorepo, packages depend on siblings with `\"lexical\":\n  \"workspace:*\"`; `pnpm publish` rewrites that to the exact published version,\n  so every `@lexical/*` release pins the matching `lexical`. Keep that pattern\n  when adding a package — do not hand-write a version range.\n- A **library** published on top of Lexical (anything outside this repo that\n  imports `lexical` or `@lexical/*`) declares those packages in\n  `peerDependencies`, plus `devDependencies` for its own build and tests, never\n  in `dependencies`. Apps bring their own Lexical and own the version.\n- Code under `examples/` and `packages/lexical-playground` are applications, so\n  they depend on Lexical directly — that is correct and should stay.\n\nThe user-facing version of this rule lives in\n`packages/lexical-website/docs/concepts/one-lexical-per-app.md`; keep the two in\nsync when either changes.\n\n## Important Development Notes\n\n### Reconciliation and Updates\n- `editor.read(...)`  or `editor.read('force-commit', ...)` flushes pending updates first, then provides consistent reconciled state\n- `editor.read('pending', ...)` reads the pending state (like `editor.update(...)`, but read-only)\n- `editor.read('latest', ...)` reads the latest consistent reconciled state\n- Inside `editor.update()`, you see pending state (transforms/reconciliation not yet run)\n- `editor.getEditorState().read()` always uses latest reconciled state, but prefer `editor.read('latest', ...)` in new code\n- Updates can be nested: `editor.update(() => editor.update(...))` is allowed but strongly discouraged\n- Do NOT nest updates in reads, or use a force-commit in an update\n\n### Node References\nAlways access node properties/methods within read/update context. Nodes automatically resolve to their latest version via their key. Don't store node references across update boundaries.\n\n### Testing Strategy\n- **Unit tests** - Vitest, located in `packages/**/__tests__/unit/**/*.test.{ts,tsx}`.\n  Add `// @vitest-environment node` after the copyright header for tests that\n  only exercise editor state, headless behavior, or pure utilities. DOM tests\n  use jsdom with `vmThreads` on Node 24.9+, which reuses the jsdom implementation\n  while keeping each file's window and module graph isolated. Older Node\n  versions use isolated forks because their VM contexts cannot load jsdom's\n  `require(esm)` dependencies. Platform-mocked tests depend on module isolation.\n  Compiler tests run with `scripts-unit` in\n  ordinary Node workers because native build tools are not VM-realm-safe.\n  Use `DOMParser` to parse HTML in DOM tests and iframes for separate-window\n  tests. Reserve direct JSDOM construction for Node fixtures or CSSOM behavior\n  that jsdom's DOMParser lacks.\n- **Browser tests** - Vitest browser mode driven by the Playwright runner, located in\n  `packages/**/__tests__/browser/**/*.test.{ts,tsx}`. Use these for behavior that depends on\n  a real layout/selection engine instead of stubbing the missing jsdom functionality from\n  `vitest.setup.mts` (e.g. `Range.getBoundingClientRect`, the Selection API). Run with\n  `pnpm run test-browser`; the browser set is controlled by the `VITEST_BROWSER` env var\n  (comma-separated, default `chromium`). Prefer building editors with the extension APIs\n  (`buildEditorFromExtensions`, or `LexicalExtensionComposer`/`LexicalExtensionEditorComposer`\n  in React).\n  - **Do not use `using`/`Disposable` in browser tests (or any browser-facing code).**\n    Explicit Resource Management (`using`, `Symbol.dispose`, `Disposable`) is not supported\n    in WebKit/Safari yet, so the syntax throws a `SyntaxError` there. `using` is fine in unit\n    tests (jsdom/Node), but browser tests should clean up with\n    `onTestFinished(() => editor.dispose())` instead — `editor.dispose()` is a plain method\n    available on the result of `buildEditorFromExtensions`.\n- **E2E tests** - Playwright, located in `packages/lexical-playground/__tests__/e2e/**/*.spec.{ts,mjs}`\n- E2E tests require the playground dev server running\n- Use `pnpm run debug-test-e2e-chromium` to debug E2E tests with browser UI\n\n### Custom Nodes\nWhen creating custom nodes:\n1. Extend a base node class (TextNode, ElementNode, DecoratorNode). The\n   constructor must work with no arguments.\n2. Implement `$config()` with `this.config('type', {extends: BaseNode, json:\n   schema})`, where `schema` is built with `nodeSchema<YourNode>()({...})`\n   as a module-scope `const` above the class. Written inline in `$config()`,\n   it makes TypeScript skip checking the class without a diagnostic.\n   Declare every property inherent to the node in it, and declare each one\n   stored verbatim in a field with\n   `withField(stringValue(), {field: '__name'})`. Initialize that field in\n   the class (`__name: string = '';`) so it is an own property of a fresh\n   node. Use `withAccessors` only where a property is not a plain field.\n3. Do not hand-write `clone`, `importJSON`, `exportJSON` or\n   `updateFromJSON`: `$config` derives them from the schema, and it carries\n   every property declared with `withField` across a clone. A field missing\n   from the schema is not carried, so it is lost the first time a later\n   update clones the node; declare it with `withField` rather than writing\n   `afterCloneFrom`. Ad-hoc data that is not inherent to the node type\n   belongs in NodeState (`createState`), which clones on its own. Write\n   `afterCloneFrom` only for storage neither covers, such as a property\n   declared through accessors on both sides whose value is not one field\n   (one that is can name it: `setter: {field: '__ids', method: 'setIDs'}`\n   in `MarkNode`). Call `super.afterCloneFrom(prevNode)` first, and copy\n   every field of the class, schema fields included: Lexical does not add\n   to a class's own `afterCloneFrom`.\n4. Implement `createDOM()` and `updateDOM()` (and `decorate()` for a\n   DecoratorNode). Keep `exportDOM()` on the class when the default export\n   is not enough.\n5. For HTML import, prefer `defineImportRule` rules registered by the\n   extension that provides the node: list `CoreImportExtension` and\n   `configExtension(DOMImportExtension, {rules: [...]})` in its\n   `dependencies` (see `ListExtension`, and `ListImportRules` for the rules,\n   in `@lexical/list`). Both APIs are `@experimental`, and the rules only\n   apply where the editor routes HTML through the pipeline; see\n   `packages/lexical-website/docs/serialization/dom-import.md`.\n   `importDOM()`, which that page calls legacy, is not deprecated yet, but\n   its deprecation is planned.\n6. Register with an extension (`nodes: () => [YourNode]`) or editor config.\n7. Export a `$createYourNode()` factory built on `$create(YourNode)` and the\n   node's setters, and a `$isYourNode()` guard.\n\nThe full schema reference is\n`packages/lexical-website/docs/serialization/serialization.md`; the cloning\nrules are in `packages/lexical-website/docs/concepts/node-cloning.md`.\n\n### Shadow DOM and iframe realm safety\n\nLexical supports editors whose root element lives inside a Shadow DOM or an\n`<iframe>` document. The editor resolves its `window` and `document` from\n`rootElement.ownerDocument.defaultView`, so code that reaches for the **global**\n`window` or `document` will silently use the wrong realm when the editor crosses\na frame boundary. Shadow DOM adds a second hazard: the browser **retargets**\nselection and focus reads to the shadow host, hiding the real nodes.\n\nUse the shadow/iframe-aware helpers exported from `lexical` instead of the raw\nbrowser globals:\n\n| Instead of | Use | Why |\n| --- | --- | --- |\n| `window` | `element.ownerDocument.defaultView` or `getDefaultView(element)` (@internal) | Returns the window that owns the element |\n| `window.getSelection()` | `getDOMSelection(rootElement.ownerDocument.defaultView)` | Reads selection from the correct window |\n| `selection.getRangeAt(0)` | `getDOMSelectionRange(selection, rootElement)` | Unwraps retargeted shadow-DOM selection |\n| `document` in `createDOM`/`updateDOM`/`exportDOM` | `$getDocument()` | Returns the document that owns the editor root (falls back to `globalThis.document`) |\n| `document` elsewhere | `getRootOwnerDocument(rootElement)` or `element.ownerDocument` | Returns the document that owns a specific element |\n| `document.activeElement` | `getActiveElement(element)` / `getActiveElementDeep(document)` | Walks through shadow roots to find the real focused element |\n| `event.target` | `getComposedEventTarget(event)` | Returns the un-retargeted target for composed events |\n| `element.parentElement` | `getParentElement(element)` | Crosses shadow boundaries correctly |\n| `selection.anchorNode` / `focusNode` | `getDOMSelectionPoints(selection, rootElement)` | Returns un-retargeted boundary points |\n\nTwo ESLint rules enforce the globals rows at lint time:\n- `no-restricted-syntax` (error) catches all `document.*` and `window.*` member access in library sources.\n- `@lexical/no-document-in-dom-methods` (error, with autofix) catches `document.*` specifically inside `createDOM`/`updateDOM`/`exportDOM` methods and autofixes to `$getDocument().*`.\n\nThe remaining rows involve local variable properties that lint cannot reliably detect, so they must be caught in code review.\n\nFor full details on the browser platform APIs involved, see\n[Shadow DOM and iframes](packages/lexical-website/docs/concepts/shadow-dom.md).\n\n### Commits and Pull Requests\nEvery commit message — not just PR bodies — must be written to the shape of\n`.github/pull_request_template.md`, so any commit can seed a PR directly\nwithout being rewritten. This applies to every commit you author, including\none-line fixes; do not wait to be asked. Read the template rather than working\nfrom memory, and fill in its sections:\n\n- **Subject line**: `[Affected Packages] PR Type: title`, where the packages are\n  the directory names under `packages/` that the diff touches and the type is\n  one of Breaking change / Refactor / Feature / Bug Fix / Documentation Update /\n  Chore. Test-only and tooling changes are `Chore`.\n- **`## Description`**: what the current behavior is and what this change makes\n  it do. Add `Closes #<issue>` only when there is a real issue number; drop the\n  line otherwise rather than leaving the template's placeholder behind.\n- **`## Test plan`** with `### Before` and `### After` subsections: the command\n  you ran, plus the actual failing output before and passing output after.\n  Paste real output — do not describe it. If a platform or browser in the\n  matrix could not be exercised, say so explicitly under `### After`.\n\nDrop any template section that does not apply to the diff instead of carrying\nan empty heading, and treat the template's HTML comments as instructions to\nfollow, not text to copy into the message.\n\n### Commit and PR Hygiene for Agents\nThis is an open source project: never include agent-session URLs or other\nprivate/team-internal links (e.g. `https://claude.ai/code/session_...`) in\ncommit messages, PR titles, or PR bodies. Those URLs are private to the\nperson or team that ran the session and are meaningless or misleading to\neveryone else. Co-authorship attribution (e.g. `Co-Authored-By:`) is fine.\nFor Claude Code this is enforced mechanically via `attribution.sessionUrl:\nfalse` in the checked-in `.claude/settings.json`; agents from other vendors\nshould follow this rule as written.\n\n### Build System\n- Uses Rollup for bundling\n- Build script: `scripts/build.mjs`\n- Supports multiple build modes: development, production, www (Meta internal)\n- TypeScript source → compiled to ESM for npm (`dist/*.js` in the\n  `\"type\": \"module\"` packages; CommonJS consumers load it through\n  `require(esm)`, so the fork modules must stay free of top-level await) and\n  to CommonJS for www only (`--www`)\n- Package manager logic in `scripts/shared/packagesManager.mjs`\n","CLAUDE.md":"@AGENTS.md\n","packages/lexical-a11y/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-a11y%2FREADME.md -->","packages/lexical-clipboard/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-clipboard%2FREADME.md -->","packages/lexical-code-core/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-code-core%2FREADME.md -->","packages/lexical-code-prism/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-code-prism%2FREADME.md -->","packages/lexical-code-shiki/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-code-shiki%2FREADME.md -->","packages/lexical-code/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-code%2FREADME.md -->","packages/lexical-compiler/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-compiler%2FREADME.md -->","packages/lexical-devtools-core/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-devtools-core%2FREADME.md -->","packages/lexical-devtools/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-devtools%2FREADME.md -->","packages/lexical-dragon/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-dragon%2FREADME.md -->","packages/lexical-eslint-plugin/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-eslint-plugin%2FREADME.md -->","packages/lexical-eslint-plugin/__tests__/integration/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-eslint-plugin%2F__tests__%2Fintegration%2FREADME.md -->","packages/lexical-extension/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-extension%2FREADME.md -->","packages/lexical-fast-check/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-fast-check%2FREADME.md -->","packages/lexical-file/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-file%2FREADME.md -->","packages/lexical-hashtag/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-hashtag%2FREADME.md -->","packages/lexical-headless/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-headless%2FREADME.md -->","packages/lexical-history/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-history%2FREADME.md -->","packages/lexical-html/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-html%2FREADME.md -->","packages/lexical-internal/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-internal%2FREADME.md -->","packages/lexical-link/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-link%2FREADME.md -->","packages/lexical-list/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-list%2FREADME.md -->","packages/lexical-mark/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-mark%2FREADME.md -->","packages/lexical-markdown/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-markdown%2FREADME.md -->","packages/lexical-mdast/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-mdast%2FREADME.md -->","packages/lexical-offset/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-offset%2FREADME.md -->","packages/lexical-overflow/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-overflow%2FREADME.md -->","packages/lexical-plain-text/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-plain-text%2FREADME.md -->","packages/lexical-react/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-react%2FREADME.md -->","packages/lexical-rich-text/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-rich-text%2FREADME.md -->","packages/lexical-selection/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-selection%2FREADME.md -->","packages/lexical-table/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-table%2FREADME.md -->","packages/lexical-tailwind/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-tailwind%2FREADME.md -->","packages/lexical-text/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-text%2FREADME.md -->","packages/lexical-utils/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-utils%2FREADME.md -->","packages/lexical-website/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-website%2FREADME.md -->","packages/lexical-website/docs/collaboration/_category_.json":"{\n  \"label\": \"Collaboration\",\n  \"position\": 6\n}\n","packages/lexical-website/docs/collaboration/faq.md":"---\nsidebar_position: 2\n---\n\n# Collaboration FAQ\n\n## Source of truth: Lexical State, Yjs and App's DB\n\nIt's recommended to treat the Yjs model as the source of truth. You can store the document to a database for indexing.\nBut, if possible, you should never forget the Yjs model, as this is the only way clients without internet access can reliably join and sync with the server.\n\nYou can also treat the database as the source of truth. This is how it could be achieved:\n\n- Clients receive a `sessionId` when they connect to the server\n- When a client connects without an existing `sessionId`, get the content from the database and create a `sessionId`\n- When all clients disconnect, forget the room content and `sessionId` on the server after some timeout (e.g. 1 hour)\n- When a client reconnects, use that content on the server. Furthermore, get the `sessionId` from the client\n- When two clients with different `sessionId` reconnect, one of the clients should forget the room content. _In this case the client will lose content_ - although it is very unlikely if you set the forget timeout (see point 2) very high.\n\nOr, there is an ever simpler approach:\n\n- When a client connects to the server, the server populates the room content if empty\n- When all clients disconnect, the server forgets the room content after some timeout (e.g. 1 hour)\n- When a client was not able to reconnect for 40 minutes, the client must forget its local updates and start fresh (this should be enforced by the server)\n\nWhen the database is the source of truth, and if you want to be able to forget the Yjs model, you will always run into cases where clients are not able to commit changes. That's not too bad in most projects. It somehow limits you, because you can't cache the document on the client using y-indexeddb. On the other hand, it is much easier to maintain, and do Yjs upgrades. Furthermore, most people would say that SQL is a bit more reliable than Yjs.\n\n_* Based on the advice of the Yjs author - [Kevin Jahns](https://github.com/yjs/yjs/issues/82#issuecomment-328365015)_\n\n\n## Initializing `EditorState` from Yjs Document\n\nIt's achievable by leveraging headless Lexical and no-op provider for Yjs:\n\n<details>\n  <summary>createHeadlessCollaborativeEditor.ts</summary>\n\n  ```typescript\n  import type {Binding, Provider} from '@lexical/yjs';\n  import type {\n    Klass,\n    LexicalEditor,\n    LexicalNode,\n    LexicalNodeReplacement,\n    SerializedEditorState,\n  } from 'lexical';\n\n  import {createHeadlessEditor} from '@lexical/headless';\n  import {\n    createBinding,\n    syncLexicalUpdateToYjs,\n    syncYjsChangesToLexical,\n  } from '@lexical/yjs';\n  import {type YEvent, applyUpdate, Doc, Transaction} from 'yjs';\n\n  export default function headlessConvertYDocStateToLexicalJSON(\n    nodes: ReadonlyArray<Klass<LexicalNode> | LexicalNodeReplacement>,\n    yDocState: Uint8Array,\n  ): SerializedEditorState {\n    return withHeadlessCollaborationEditor(nodes, (editor, binding) => {\n      applyUpdate(binding.doc, yDocState, {isUpdateRemote: true});\n      editor.update(() => {}, {discrete: true});\n\n      return editor.getEditorState().toJSON();\n    });\n  }\n\n  /**\n   * Creates headless collaboration editor with no-op provider (since it won't\n   * connect to message distribution infra) and binding. It also sets up\n   * bi-directional synchronization between yDoc and editor\n   */\n  export function withHeadlessCollaborationEditor<T>(\n    nodes: ReadonlyArray<Klass<LexicalNode> | LexicalNodeReplacement>,\n    callback: (editor: LexicalEditor, binding: Binding, provider: Provider) => T,\n  ): T {\n    const editor = createHeadlessEditor({\n      nodes,\n    });\n\n    const id = 'main';\n    const doc = new Doc();\n    const docMap = new Map([[id, doc]]);\n    const provider = createNoOpProvider();\n    const binding = createBinding(editor, provider, id, doc, docMap);\n\n    const unsubscribe = registerCollaborationListeners(editor, provider, binding);\n\n    const res = callback(editor, binding, provider);\n\n    unsubscribe();\n\n    return res;\n  }\n\n  function registerCollaborationListeners(\n    editor: LexicalEditor,\n    provider: Provider,\n    binding: Binding,\n  ): () => void {\n    const unsubscribeUpdateListener = editor.registerUpdateListener(\n      ({\n        dirtyElements,\n        dirtyLeaves,\n        editorState,\n        normalizedNodes,\n        prevEditorState,\n        tags,\n      }) => {\n        if (tags.has('skip-collab') === false) {\n          syncLexicalUpdateToYjs(\n            binding,\n            provider,\n            prevEditorState,\n            editorState,\n            dirtyElements,\n            dirtyLeaves,\n            normalizedNodes,\n            tags,\n          );\n        }\n      },\n    );\n\n    const observer = (events: Array<YEvent<any>>, transaction: Transaction) => {\n      if (transaction.origin !== binding) {\n        syncYjsChangesToLexical(binding, provider, events, false);\n      }\n    };\n\n    binding.root.getSharedType().observeDeep(observer);\n\n    return () => {\n      unsubscribeUpdateListener();\n      binding.root.getSharedType().unobserveDeep(observer);\n    };\n  }\n\n  function createNoOpProvider(): Provider {\n    const emptyFunction = () => {};\n\n    return {\n      awareness: {\n        getLocalState: () => null,\n        getStates: () => new Map(),\n        off: emptyFunction,\n        on: emptyFunction,\n        setLocalState: emptyFunction,\n        setLocalStateField: emptyFunction,\n      },\n      connect: emptyFunction,\n      disconnect: emptyFunction,\n      off: emptyFunction,\n      on: emptyFunction,\n    };\n  }\n  ```\n</details>\n","packages/lexical-website/docs/collaboration/react.md":"---\nsidebar_position: 1\n---\n\n# React\n\nLexical provides `LexicalCollaborationPlugin` and `useCollaborationContext` hook within `@lexical/react` to accelerate creation of the collaborative React backed editors.\nThis is on top of the Yjs bindings provided by `@lexical/yjs`.\n\n\n:::tip\n\nClone [Lexical GitHub repo](https://github.com/facebook/lexical), run `pnpm i && pnpm run start` and open [`http://localhost:3000/split/?isCollab=true`](http://localhost:3000/split/?isCollab=true) to launch playground in collaborative mode.\n\n:::\n\n## Getting started\n\nThis guide is based on [examples/react-rich](https://github.com/facebook/lexical/tree/main/examples/react-rich) example.\n\n**Install minimal set of the required dependencies:**\n```bash\n$ npm i -S @lexical/react @lexical/yjs lexical react react-dom y-websocket yjs\n```\n\n:::note\n\n`y-websocket` is the only officially supported Yjs connection provider at this point. Although other providers may work just fine.\n\n:::\n\n**Get WebSocket server running:**\n\nThis allows different browser windows and different browsers to find each other and sync Lexical state. On top of this `YPERSISTENCE` allows you to save Yjs documents in between server restarts so clients can simply reconnect and keep editing.\n\n```bash\n$ HOST=localhost PORT=1234 YPERSISTENCE=./yjs-wss-db npx y-websocket\n```\n\n**Get basic collaborative Lexical setup:**\n\n```tsx\nimport { useCallback } from 'react';\n\nimport {$getRoot, $createParagraphNode, $createTextNode} from 'lexical';\nimport {LexicalCollaboration} from '@lexical/react/LexicalCollaborationContext';\nimport {LexicalComposer} from '@lexical/react/LexicalComposer';\nimport {ContentEditable} from '@lexical/react/LexicalContentEditable';\nimport {LexicalErrorBoundary} from '@lexical/react/LexicalErrorBoundary';\nimport {RichTextPlugin} from '@lexical/react/LexicalRichTextPlugin';\nimport {CollaborationPlugin} from '@lexical/react/LexicalCollaborationPlugin';\nimport * as Y from 'yjs';\nimport {$initialEditorState} from './initialEditorState';\nimport {WebsocketProvider} from 'y-websocket';\n\nfunction Editor() {\n  const initialConfig = {\n    // NOTE: This is critical for collaboration plugin to set editor state to null. It\n    // would indicate that the editor should not try to set any default state\n    // (not even empty one), and let collaboration plugin do it instead\n    editorState: null,\n    namespace: 'Demo',\n    nodes: [],\n    onError: (error: Error) => {\n      throw error;\n    },\n    theme: {},\n  };\n\n  const getDocFromMap = (id: string, yjsDocMap: Map<string, Y.Doc>): Y.Doc => {\n    let doc = yjsDocMap.get(id);\n  \n    if (doc === undefined) {\n      doc = new Y.Doc();\n      yjsDocMap.set(id, doc);\n    } else {\n      doc.load();\n    }\n\n    return doc;\n  }\n\n  const providerFactory = useCallback(\n    (id: string, yjsDocMap: Map<string, Y.Doc>) => {\n      const doc = getDocFromMap(id, yjsDocMap);\n\n      return new WebsocketProvider('ws://localhost:1234', id, doc, {\n        connect: false,\n      });\n    }, [],\n  );\n\n  return (\n    <LexicalCollaboration>\n      <LexicalComposer initialConfig={initialConfig}>\n        <RichTextPlugin\n          contentEditable={<ContentEditable className=\"editor-input\" />}\n          placeholder={<div className=\"editor-placeholder\">Enter some rich text...</div>}\n          ErrorBoundary={LexicalErrorBoundary}\n        />\n        <CollaborationPlugin\n          id=\"lexical/react-rich-collab\"\n          providerFactory={providerFactory}\n          shouldBootstrap={true}\n        />\n      </LexicalComposer>\n    </LexicalCollaboration>\n  );\n}\n```\n\n**Initial editor content:**\n\nIn a production environment, you should bootstrap the editor's initial content on the server. If bootstrapping was left to the client and two clients connected at the same time, they could both try to initialize the content resulting in document corruption.\n\nUsing the `withHeadlessCollaborationEditor` function from the [FAQ](faq.md) page, you can create a bootstrapped `Y.Doc` with the following:\n\n```tsx\nimport type {CreateEditorArgs} from 'lexical';\n\nimport {$getRoot, $createParagraphNode} from 'lexical';\nimport {Doc} from 'yjs';\n\nimport {withHeadlessCollaborationEditor} from './createHeadlessCollaborativeEditor';\n\nfunction createBootstrappedYDoc(nodes: CreateEditorArgs['nodes']): Doc {\n  return withHeadlessCollaborationEditor(nodes, (editor, binding) => {\n    editor.update(() => {\n      $getRoot().append($createParagraphNode());\n    }, {discrete: true});\n    // binding.doc is the Doc the editor synced into\n    return binding.doc;\n  });\n}\n```\n\nIf you're simply following the above example to play around in a local dev environment, then you can add the following props to `CollaborationPlugin` to initialize the editor state client-side:\n\n```tsx\n// Dev-testing only, do not use in real-world cases.\ninitialEditorState={$initialEditorState}\nshouldBootstrap={true}\n```\n\n## See it in action\n\nSource code: [examples/react-rich-collab](https://github.com/facebook/lexical/tree/main/examples/react-rich-collab)\n\n<iframe width=\"100%\" height=\"600\" src=\"https://stackblitz.com/github/facebook/lexical/tree/main/examples/react-rich-collab?embed=1&file=src%2FApp.tsx&terminalHeight=0&ctl=1&showSidebar=0&devtoolsheight=0&view=preview\" sandbox=\"allow-forms allow-modals allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin allow-scripts\"></iframe>\n\n## Sharing one Yjs document between editors\n\nBy default a binding uses the `XmlText` shared type named `root` on the Yjs\n`Doc`, so each editor needs its own document. Two options let several editors\nlive in one document instead, which is what applications like collaborative\nsticky notes or comment threads usually want:\n\n- `rootName` picks a different top-level shared type, one per editor:\n\n```jsx\n<CollaborationPlugin\n  id={`note-${noteId}`}\n  providerFactory={providerFactory}\n  rootName={`note-${noteId}`}\n  shouldBootstrap={true}\n/>\n```\n\n- `getXmlText` resolves the root yourself, for roots that are not top-level\n  shared types (for example an `XmlText` stored in a `Y.Map`). It takes\n  precedence over `rootName`:\n\n```jsx\nconst getXmlText = useCallback(\n  (doc) => doc.getMap('notes').get(noteId),\n  [noteId],\n);\n\n<CollaborationPlugin\n  id={`note-${noteId}`}\n  providerFactory={providerFactory}\n  getXmlText={getXmlText}\n  shouldBootstrap={true}\n/>\n```\n\nGive each editor its own `id`, as above: the `id` keys the document map and the\nremote-cursor highlight registry, both of which are shared across the page, so\ntwo editors mounted under one `id` overwrite each other's entries.\n\nBoth options are also available on `createYjsBinding` for non-React usage.\n\n:::warning\n\nThe root is chosen when the binding is created, and a mounted editor cannot be\nrepointed at another root: changing `rootName` or `getXmlText` afterwards has no\neffect. To switch documents, remount the **editor** — put a `key` that changes\nwith the document on the component that owns the `LexicalComposer`. Remounting\nonly the plugin is not enough: the editor would keep the previous document's\ncontent and write it into the newly selected root.\n\n:::\n\n`getXmlText` runs while the binding is constructed, before the provider has\nsynced, so it has to resolve the root from whatever the local `Doc` holds at\nthat moment — reaching into a `Y.Array` that the server has not sent yet throws.\nEither mount the editor only once the note exists locally, or have the callback\ncreate the missing shared type. Write it to be idempotent: it can run more than\nonce for one editor (React StrictMode double-invokes render, and a remount\nbuilds a new binding), and a callback that unconditionally stores a new\n`XmlText` would throw away what the previous call stored.\n\nThe returned `XmlText` is checked to be an `XmlText` that is integrated into the\ndocument; that it is not already driving another live binding is yours to\nguarantee, since a second binding over one root takes over the collab nodes\ncached on its shared types.\n\nThe experimental `CollaborationPluginV2__EXPERIMENTAL` (and\n`createBindingV2__EXPERIMENTAL`) take the same two options, except that its\nroot is an `XmlElement`, so the callback is named `getXmlElement`. Create that\nelement as `new Y.XmlElement()` without a `nodeName`, which is how the root is\nrecognized.\n\n## Building collaborative plugins\n\n[Lexical Playground](https://playground.lexical.dev/) features set of the collaboration enabled plugins that integrate with primary document via `useCollaborationContext()` hook. Notable mentions:\n\n- [`CommentPlugin`](https://github.com/facebook/lexical/tree/main/packages/lexical-playground/src/plugins/CommentPlugin) - features use of the separate provider and Yjs room to sync comments.\n- [`ImageComponent`](https://github.com/facebook/lexical/blob/main/packages/lexical-playground/src/nodes/ImageComponent.tsx) - features use of the `LexicalExtensionEditorComposer` for its nested caption editor.\n- [`PollOptionComponent`](https://github.com/facebook/lexical/blob/main/packages/lexical-playground/src/nodes/PollComponent.tsx) - showcases poll implementation using `name` from the collaboration context.\n- [`StickyComponent`](https://github.com/facebook/lexical/blob/main/packages/lexical-playground/src/nodes/StickyComponent.tsx) - features use of the `LexicalExtensionEditorComposer` for its nested editor as well as sticky note position real-time sync.\n\n:::note\n\nWhile these \"playground\" plugins aren't production ready - they serve as a great example of collaborative Lexical capabilities as well as provide a good starting point.\n\n:::\n\n## Custom node property syncing\n\n`@lexical/yjs` syncs custom node properties by constructing a fresh node instance and inspecting its **own enumerable properties**. This means every property that should be synced across peers must be assigned in the constructor, even if the initial value is `undefined`.\n\n:::warning\n\nDeclaring a class property with TypeScript's optional syntax (`foo?: Type`) does **not** guarantee the property exists as an own enumerable property on the instance. Under TypeScript's default compilation settings (target < ES2022 without `useDefineForClassFields`), such declarations produce no initialization code.\n\n:::\n\n### What to do\n\nAlways initialize every node property in the constructor. Conditional initialization such as `if (someValue !== undefined) this.__someValue = someValue` is not enough, because the default construction path still needs to create the property.\n\nFor example:\n\n```ts\n// ✅ Correct — property is guaranteed to be an own property\nclass MyNode extends ElementNode {\n  __someValue: string | undefined;\n\n  constructor(someValue?: string, key?: NodeKey) {\n    super(key);\n    this.__someValue = someValue; // explicitly initialized\n  }\n}\n\n// ❌ Incorrect — property may not exist as an own property\nclass MyNode extends ElementNode {\n  __someValue?: string; // optional syntax without initialization\n\n  constructor(key?: NodeKey) {\n    super(key);\n    // __someValue is never assigned → won't be synced via yjs\n  }\n}\n```\n\nIf you use [`NodeState`](../concepts/node-state.md) for your custom properties, this concern does not apply. `NodeState` values are synced correctly without relying on constructor-created enumerable properties.\n\n## Yjs providers\n\nSetting up the communication between clients, managing awareness information, and storing shared data for offline usage is quite a hassle. Providers manage all that for you and are the perfect starting point for your collaborative app.\n\nSee [Yjs Website](https://docs.yjs.dev/ecosystem/connection-provider) for the list of the officially endorsed providers. Although it's not an exhaustive one.\n","packages/lexical-website/docs/concepts/_category_.json":"{\n  \"label\": \"Concepts\",\n  \"position\": 3\n}\n","packages/lexical-website/docs/concepts/commands.md":"\n\n# Commands\n\nCommands are a very powerful feature of Lexical that lets you register listeners for events like `KEY_ENTER_COMMAND` or `KEY_TAB_COMMAND` and contextually react to them _wherever_ & _however_ you'd like.\n\nThis pattern is useful for building [`Toolbars`](https://github.com/facebook/lexical/blob/main/packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx) or complex extensions and nodes such as [tables](https://github.com/facebook/lexical/tree/main/packages/lexical-table) which require special handling for `selection`, `keyboard events`, and more.\n\nWhen registering a `command` you supply a `priority` and can return `true` to mark it as \"handled\", which stops other listeners from receiving the event. If a command isn't handled explicitly by you, it's likely handled by default in [`RichTextExtension`](https://github.com/facebook/lexical/blob/main/packages/lexical-rich-text/src/index.ts) or [`PlainTextExtension`](https://github.com/facebook/lexical/blob/main/packages/lexical-plain-text/src/index.ts).\n\n## `createCommand(...)`\n\nYou can view the core commands in the [`lexical` API reference](/docs/api/modules/lexical#commands). Other packages define their own, listed under Commands in each package's API reference, such as [`TOGGLE_LINK_COMMAND`](/docs/api/modules/lexical_link#toggle_link_command) in `@lexical/link`. If you need a custom command for your own use case, check out the typed `createCommand(...)` function.\n\n```js\nconst HELLO_WORLD_COMMAND: LexicalCommand<string> = createCommand('HELLO_WORLD');\n\neditor.registerCommand(\n  HELLO_WORLD_COMMAND,\n  (payload: string) => {\n    console.log(payload); // Hello World!\n    return false;\n  },\n  COMMAND_PRIORITY_EDITOR,\n);\n\neditor.dispatchCommand(HELLO_WORLD_COMMAND, 'Hello World!');\n```\n\n## `editor.dispatchCommand(...)`\n\nCommands can be dispatched from anywhere you have access to the `editor` such as a Toolbar Button, an event listener, or an extension, but most of the core commands are dispatched from [`LexicalEvents.ts`](https://github.com/facebook/lexical/blob/main/packages/lexical/src/LexicalEvents.ts).\n\nCalling `dispatchCommand` will implicitly call `editor.update` to trigger its command listeners if it was not called from inside `editor.update`.\n\nDon't dispatch a command from inside a read-only context such as\n`editor.read(fn)` or `editorState.read(fn)`. Command listeners usually change\nthe editor, so Lexical runs them in a separate writable update and\ndevelopment builds log a warning when they detect this. Dispatch after the\nread returns instead.\n\n```js\neditor.dispatchCommand(command, payload);\n```\n\nThe `payload`s are typed via the `createCommand(...)` API, but they're usually a DOM `event` for commands dispatched from an event listener.\n\nHere are some real examples from [`LexicalEvents.ts`](https://github.com/facebook/lexical/blob/main/packages/lexical/src/LexicalEvents.ts).\n\n```js\neditor.dispatchCommand(KEY_ARROW_LEFT_COMMAND, event);\n// ...\neditor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic');\n```\n\nAnd another example from the [`ToolbarPlugin`](https://github.com/facebook/lexical/blob/main/packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx) in our Playground.\n\n```js\nconst formatBulletList = () => {\n  editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND);\n};\n```\n\nWhich is later handled by [`ListExtension`](https://github.com/facebook/lexical/blob/main/packages/lexical-list/src/LexicalListExtension.ts) to insert the list into the editor.\n\n```js\neditor.registerCommand(\n  INSERT_UNORDERED_LIST_COMMAND,\n  () => {\n    $insertList('bullet');\n    return true;\n  },\n  COMMAND_PRIORITY_LOW,\n);\n```\n\n## `editor.registerCommand(...)`\n\nYou can register a command from anywhere you have access to the `editor` object, but it's important that you remember to clean up the listener with its remove listener callback when it's no longer needed.\n\nThe command listener will always be called from an `editor.update`, so you may use dollar functions. You should not use\n`editor.update` (and *never* call `editor.read(...)` or `editor.read('force-commit', ...)`) synchronously from within a command listener. It is safe to call\n`editor.read('latest', ...)` or `editor.getEditorState().read` if you need to read the previous state after updates have already been made.\n\n```js\nconst removeListener = editor.registerCommand(\n  COMMAND,\n  (payload) => boolean, // Return true to stop propagation.\n  priority,\n);\n// ...\nremoveListener(); // Cleans up the listener.\n```\n\nThe easiest way to get clean-up right is to register commands from an\n[extension](/docs/extensions/defining-extensions)'s `register` function and\nreturn the remove callback. The editor calls it when it is disposed. To\nregister several things, combine their callbacks with `mergeRegister` from\n`@lexical/utils`.\n\n```js\nimport {LinkExtension, TOGGLE_LINK_COMMAND} from '@lexical/link';\nimport {COMMAND_PRIORITY_BEFORE_EDITOR, defineExtension} from 'lexical';\n\nexport const LinkLoggerExtension = defineExtension({\n  // The command it observes is handled by LinkExtension\n  dependencies: [LinkExtension],\n  name: '@my-app/LinkLogger',\n  register: (editor) =>\n    editor.registerCommand(\n      TOGGLE_LINK_COMMAND,\n      (payload) => {\n        console.log('link', payload);\n        return false; // let the link extension handle it too\n      },\n      // LinkExtension handles this command at COMMAND_PRIORITY_EDITOR and\n      // returns true, so observe it at the BEFORE variant of that priority.\n      COMMAND_PRIORITY_BEFORE_EDITOR,\n    ),\n});\n```\n\nA listener that only observes a command, like this one, needs to run before\nthe listener that handles it. Depending on the extension that handles it and\nusing the `COMMAND_PRIORITY_BEFORE_*` variant of its priority does that\nwithout escalating to a higher priority. See\n[Priorities and ordering](#priorities-and-ordering) below.\n\nIn a legacy React plugin, return the `registerCommand` call from a\n`useEffect` instead.\n\nAnd as seen above and below, `registerCommand`'s callback can return `true` to signal to the other listeners that the command has been handled and propagation will be stopped.\n\nHere's a simplified example of handling a `KEY_TAB_COMMAND` from [`TabIndentationExtension`](https://github.com/facebook/lexical/blob/main/packages/lexical-extension/src/TabIndentationExtension.ts), which is used to dispatch a `OUTDENT_CONTENT_COMMAND` or `INDENT_CONTENT_COMMAND`.\n\n```js\neditor.registerCommand(\n  KEY_TAB_COMMAND,\n  (payload) => {\n    const event: KeyboardEvent = payload;\n    event.preventDefault();\n    return editor.dispatchCommand(\n      event.shiftKey ? OUTDENT_CONTENT_COMMAND : INDENT_CONTENT_COMMAND,\n    );\n  },\n  COMMAND_PRIORITY_EDITOR,\n);\n```\n\nNote that the same `KEY_TAB_COMMAND` command is registered by [`LexicalTableSelectionHelpers.ts`](https://github.com/facebook/lexical/blob/main/packages/lexical-table/src/LexicalTableSelectionHelpers.ts), which handles moving focus to the next or previous cell within a `TableNode`, but the priority is high (`COMMAND_PRIORITY_HIGH`) because this behavior is very important.\n\n### Priorities and ordering\n\nCommand listeners are called in the following order until a listener returns `true`:\n\n- From priority highest to lowest (critical, high, normal, low, editor)\n- All `COMMAND_PRIORITY_BEFORE_${priority}` listeners, most recently registered first\n- All `COMMAND_PRIORITY_${priority}` listeners, in registration order\n\n:::note\n\nThe `COMMAND_PRIORITY_BEFORE_*` priorities make it much easier to override\ndefault behavior without escalating the priority.\n\n:::\n\nIt is best practice to use the lowest priority possible, so most commands will\nbe registered with `COMMAND_PRIORITY_EDITOR` for the default behavior, then\ncommands to override that behavior can be registered with\n`COMMAND_PRIORITY_BEFORE_EDITOR`. The higher priorities mostly serve purposes\nsuch as being able to observe-but-not-handle events and to support legacy code\nthat predates the availability of `COMMAND_PRIORITY_BEFORE_*`.\n\nA modern lexical app will typically only need the\n`COMMAND_PRIORITY_BEFORE_EDITOR` priority since the\nlast-registered-called-first ordering is suitable for almost all use cases. The\nolder priorities without BEFORE can be considered legacy and are primarily\noffered for compatibility.\n\nHere is the full ordering of priorities, from lowest to highest:\n\n- `COMMAND_PRIORITY_EDITOR`\n- `COMMAND_PRIORITY_BEFORE_EDITOR`\n- `COMMAND_PRIORITY_LOW`\n- `COMMAND_PRIORITY_BEFORE_LOW`\n- `COMMAND_PRIORITY_NORMAL`\n- `COMMAND_PRIORITY_BEFORE_NORMAL`\n- `COMMAND_PRIORITY_HIGH`\n- `COMMAND_PRIORITY_BEFORE_HIGH`\n- `COMMAND_PRIORITY_CRITICAL`\n- `COMMAND_PRIORITY_BEFORE_CRITICAL`\n","packages/lexical-website/docs/concepts/compared-with-prosemirror.md":"# Compared with ProseMirror\n\nLexical and [ProseMirror](https://prosemirror.net) are both frameworks for\nbuilding rich text editors on top of `contentEditable`, and both keep an\nimmutable document model as the source of truth. This page collects the main\ndifferences in one place, for readers who know one and are learning the other.\n\n## Document model {#document-model}\n\nLexical's tree is shaped like the HTML it renders (see\n[Document Model](./document-model.md)). This is a deliberate difference from\nProseMirror. ProseMirror's document is also a tree,\nand blocks can contain other blocks, but the inline content of a textblock is\na flat sequence of nodes (text and inline nodes) with marks for formatting and\nlinks, and every position in the document is a single integer.\n\n| | Lexical | ProseMirror |\n| -- | -- | -- |\n| Document | Immutable tree of nodes, one `RootNode` | Immutable tree of nodes, one `doc` node |\n| Node identity and navigation | Each node has a runtime key and knows its parent and siblings | Nodes are plain values with no parent links; a resolved position supplies ancestor context |\n| Blocks | `ElementNode` subclasses (paragraph, heading, list, table, …), or a block `DecoratorNode` (image, embed, …) | Block nodes defined in the schema |\n| Inline formatting | Format flags on each `TextNode` (bold, italic, code, …) | Marks on text |\n| Links and other inline wrappers | Inline `ElementNode`s (`LinkNode`, `MarkNode`) that contain text nodes | Marks on text, like formatting |\n| Embedded content | `DecoratorNode`, inline or block, rendered by your framework (for example React) | Leaf or atom nodes, often with a custom `NodeView` |\n| Several editable regions in one node | [Named slots](./named-slots.md): regions addressed by name, like a card's `title`, each isolated so editing and selection never cross the boundary | Child nodes in the order the schema's content expression allows, optionally marked `isolating`, or a separate editor inside a `NodeView` |\n| Addressing a position | Node key plus offset (`{key, offset, type}`) | One integer counted across the whole document |\n| Allowed structure | Declared by node classes, enforced with node transforms and normalization | Declared by a schema of content expressions |\n| Applying edits | Call node and selection methods inside `editor.update()` | Build a transaction from steps that address positions or ranges |\n| Rendering | Each node's `createDOM()`/`updateDOM()`, applied by the reconciler, and optionally changed per node class by a `DOMRenderExtension` override | `toDOM` in the schema, or a `NodeView` |\n\n## Serialization {#serialization}\n\nBecause a Lexical link is an element that already contains its text, it\nexports as an `<a>` around those text nodes. A mark-based model has to find the\nadjacent text that shares a mark and group it into one element first.\n\n| Format | Lexical | ProseMirror |\n| -- | -- | -- |\n| JSON | `editorState.toJSON()`, restored with `editor.parseEditorState()` | `doc.toJSON()` and `Node.fromJSON(schema, json)` |\n| HTML export | `$generateHtmlFromNodes()`, using each node's `createDOM()` or `exportDOM()` | `DOMSerializer.fromSchema(schema)`, using `toDOM` from the schema's nodes and marks |\n| HTML import | `$generateNodesFromDOM()`, using `importDOM()` or `DOMImportExtension` rules | `DOMParser.fromSchema(schema)`, using the schema's `parseDOM` rules |\n| Markdown | `@lexical/markdown` transformers or `@lexical/mdast` | `prosemirror-markdown`: a markdown-it parser and a serializer with a function per node and mark |\n| Clipboard | Copy writes plain text, HTML, and Lexical JSON. Paste prefers the JSON, then HTML, then plain text. | Copy writes HTML and plain text. Paste parses the HTML with the schema's parse rules. |\n\nIn the JSON, a Lexical link is a `link` node with its text nodes as\n`children`, and bold is a `format` value on a `text` node, where\nProseMirror's JSON gives each text node a list of `marks`. In both editors the\ndefault HTML export reuses the definition that renders the editor\n(`createDOM()` in Lexical, `toDOM` in ProseMirror), so the two stay in step\nunless you override one. See [Serialization](../serialization/serialization.md)\nfor the details of each Lexical format.\n\n## History {#history}\n\nBoth editors group consecutive edits into one undo step. Lexical's\n[`HistoryExtension`](./history.md) merges edits made within `delay`\n(300ms by default) and keeps an unlimited stack unless you set `maxDepth`.\nProseMirror's history plugin uses `newGroupDelay: 500` and `depth: 100` by\ndefault, so `configExtension(HistoryExtension, {delay: 500, maxDepth: 100})`\nmatches its behavior.\n","packages/lexical-website/docs/concepts/document-model.md":"# Document Model\n\nAn [editor state](./editor-state.md) holds a tree of nodes. This page explains\nhow that tree is shaped.\n\n## A DOM-like tree\n\nLexical's node tree is shaped like the HTML it renders. A paragraph is an\n`ElementNode` whose children are the nodes inside it, and inline structure\nnests the same way: a link is a `LinkNode` element that contains the text\nnodes it wraps, just as an `<a>` contains its text. Each node type decides how\nit renders with `createDOM()` and `updateDOM()`, so the model maps closely to\nthe DOM, and a position in the document is a node plus an offset within it.\n\nHere is a small document with a heading, a paragraph containing bold text and\na link, a block image, and a card that has a named `title` slot as well as\nordinary body children:\n\n```mermaid\nflowchart TB\n  accTitle: The shape of a Lexical document tree\n  accDescr: The root has four children in order: a heading, a paragraph, an image, and a card. The heading contains the text \"Welcome\". The paragraph contains the text \"Read the \", a bold text node \"docs\", and a link, which contains the text \"here\". The image is a block decorator node with no children. The card has a named slot called title, which holds a paragraph containing the text \"Card title\", and one ordinary child, a paragraph containing the text \"Card body\".\n  root[\"Root\"] --> heading[\"Heading (h1)\"]\n  root --> paragraph[\"Paragraph\"]\n  root --> image[\"Image<br/>(block DecoratorNode)\"]\n  root --> card[\"Card<br/>(ElementNode)\"]\n  heading --> welcome[\"Text: 'Welcome'\"]\n  paragraph --> read[\"Text: 'Read the '\"]\n  paragraph --> docs[\"Text: 'docs'<br/>{ format: bold }\"]\n  paragraph --> link[\"Link\"]\n  link --> here[\"Text: 'here'\"]\n  card -.->|\"slot: title\"| titlePara[\"Paragraph\"]\n  titlePara --> titleText[\"Text: 'Card title'\"]\n  card --> body[\"Paragraph\"]\n  body --> bodyText[\"Text: 'Card body'\"]\n```\n\nSolid arrows are ordinary children, which form an ordered list under their\nparent and are read with `getChildren()`. The dashed arrow is a\n[named slot](./named-slots.md): a separate channel on the host, keyed by name\ninstead of position and read with `$getSlot(card, 'title')`. A slot's value\nis the root of its own isolated region, so its `getParent()` is `null` and\n`$getSlotHost()` leads back to the card. Any `ElementNode` or `DecoratorNode`\ncan host slots, so a decorator such as the image could also expose an\neditable caption this way.\n\n## Working with the tree\n\nLexical code navigates the document the way DOM code does, with methods like\n`getParent()`, `getChildren()`, and `getNextSibling()`, and each node owns the\nDOM it renders. The resemblance is structural, not one\nnode per HTML element: bold or italic text, for example, is a format on a\n`TextNode`, not a separate `<strong>` or `<em>` node. See\n[Nodes](./nodes.mdx) for the built-in node types.\n\n## Customizing rendering\n\nYou can also change how nodes render without subclassing them.\n[`DOMRenderExtension`](../serialization/dom-render.md) from `@lexical/html`\ntakes overrides for `createDOM`, `updateDOM`, and `exportDOM`, for one node\nclass or for every node, such as adding a `data-` attribute or wrapping a\nnode's children in another element. Each override calls `$next()` to get the\ndefault result and adjusts it, so overrides from several extensions compose.\nExport overrides only affect HTML export, and `createDOM` overrides also carry\nthrough to export for nodes that use the default `exportDOM`. Changes made\nwhile updating the editor's DOM don't; see\n[Lexical -> HTML](../serialization/serialization.md#lexical---html) for which\nhooks reach export.\n\nFor how the tree is converted to and from JSON, HTML, and Markdown, see\n[Serialization](../serialization/serialization.md#formats-at-a-glance). For\nhow this model differs from a mark-based editor, see\n[Compared with ProseMirror](./compared-with-prosemirror.md).\n","packages/lexical-website/docs/concepts/dom-events.md":"\n\n# Working with DOM Events\n\nSometimes, when working with Lexical, it might be necessary or useful for you to attach a DOM Event Listener to the underlying DOM nodes that Lexical controls. For instance, you might want to show a popover when a user mouses over a specific node or open a modal when they click on a node. Either of these use cases (and many others) can be accomplished via native DOM Event Listeners. There are 3 main ways that you can listen for DOM Events on nodes controlled by Lexical:\n\n## 1. Event Delegation\n\nOne way to handle events inside the editor is to set a listener on the editor root element (the contentEditable Lexical attaches to). You can do this using a [Root Listener](listeners.md).\n\n```js\nfunction myListener(event) {\n    // You may want to filter on the event target here\n    // to only include clicks on certain types of DOM Nodes.\n    alert('Nice!');\n}\n\nconst ClickExtension = defineExtension({\n  name: '@my-app/Click',\n  register: (editor) =>\n    editor.registerRootListener((rootElement) => {\n      if (rootElement === null) {\n        return;\n      }\n      // add the listener to the current root element\n      rootElement.addEventListener('click', myListener);\n      // remove the listener when the root element changes - make sure the ref\n      // to myListener is stable so the removal works and you avoid a memory leak.\n      return () => rootElement.removeEventListener('click', myListener);\n    }),\n});\n```\n\nAn extension's `register` returns a cleanup function, and the editor calls it\nwhen it is disposed, so there is no teardown to write by hand. Add\n`ClickExtension` to your editor's `dependencies` to use it.\nThis can be a simple, efficient way to handle some use cases, since it's not necessary to attach a listener to each DOM node individually.\n\nThe `addEventListener`/`removeEventListener` pairing above is common enough that the core `lexical` package exports a `registerEventListener(target, type, listener, options?)` helper. It attaches the listener and returns a dispose function that removes it, so the example above becomes:\n\n```js\nimport {defineExtension, registerEventListener} from 'lexical';\n\nconst ClickExtension = defineExtension({\n  name: '@my-app/Click',\n  register: (editor) =>\n    editor.registerRootListener((rootElement) =>\n      // registerEventListener returns the matching removeEventListener cleanup,\n      // so there's no need to write the teardown by hand.\n      rootElement === null\n        ? undefined\n        : registerEventListener(rootElement, 'click', myListener),\n    ),\n});\n```\n\nIt mirrors the `addEventListener` overloads (so the event type is strongly typed) and composes well with `mergeRegister` when you need to register several listeners at once.\n\nTo attach several listeners to the same target, `registerEventListeners(target, listeners, options?)` takes a `{type: listener}` map and returns a single dispose function that removes all of them. Each listener's event argument is still strongly typed per event type, and the optional `options` argument is shared across every listener:\n\n```js\nimport {registerEventListeners} from 'lexical';\n\nconst removeListeners = registerEventListeners(\n  rootElement,\n  {\n    click: onClick,\n    keydown: onKeyDown, // receives a KeyboardEvent\n  },\n  {capture: true}, // shared by both listeners\n);\n```\n\nBecause `options` is shared, register a group that needs different options (such as a different `capture` flag) with a separate `registerEventListeners` call and combine the results with `mergeRegister`.\n\n## 2. Directly Attach Handlers\n\nIn some cases, it may be better to attach an event handler directly to the underlying DOM node of each specific node. With this approach, you generally don't need to filter the event target in the handler, which can make it a bit simpler. It will also guarantee that your handler isn't running for events that you don't care about. This approach is implemented via a [Mutation Listener](listeners.md).\n\n```js\nconst registeredElements: WeakSet<HTMLElement> = new WeakSet();\nconst removeMutationListener = editor.registerMutationListener(nodeType, (mutations) => {\n    editor.getEditorState().read(() => {\n        for (const [key, mutation] of mutations) {\n            const element: null | HTMLElement = editor.getElementByKey(key);\n            if (\n                // Updated might be a move, so that might mean a new DOM element\n                // is created. In this case, we need to add an event listener too.\n                (mutation === 'created' || mutation === 'updated') &&\n                element !== null &&\n                !registeredElements.has(element)\n            ) {\n                registeredElements.add(element);\n                element.addEventListener('click', (event: Event) => {\n                    alert('Nice!');\n                });\n            }\n        }\n    });\n});\n\n// teardown the listener - return it from an extension's `register` so the\n// editor removes it when it is disposed.\nremoveMutationListener();\n```\nNotice that here we don't worry about cleaning up, as Lexical will dereference the underlying DOM nodes and allow the JavaScript runtime garbage collector to clean up their listeners.\n\n## 3. Use NodeEventPlugin\n\nIf you're using React, we've wrapped approach #2 up into a simple React plugin that you can render inside your composer to achieve the same effect, without worrying about the details:\n\n```jsx\n<LexicalExtensionComposer extension={appExtension}>\n    <NodeEventPlugin\n        nodeType={LinkNode}\n        eventType={'click'}\n        eventListener={(e: Event) => {\n            alert('Nice!');\n        }}\n    />\n</LexicalExtensionComposer>\n```\n\nIf the editor lives inside a shadow root or an iframe, see\n[Shadow DOM and iframes](./shadow-dom.md) for the helper to read\n`event.target` through the shadow boundary — the browser retargets it to the\nshadow host on listeners attached above the boundary, so a plain\n`event.target` won't match a node inside the editor.\n","packages/lexical-website/docs/concepts/editor-state.md":"\n\n# Editor State\n\n## Why is it necessary?\n\nWith Lexical, the source of truth is not the DOM, but rather an underlying state model\nthat Lexical maintains and associates with an editor instance.\n\nWhile HTML is great for storing rich text content it's often \"way too flexible\" when it comes to text editing.\nFor example the following lines of content will produce equal outcome:\n\n```html\n<i><b>Lexical</b></i>\n<i><b>Lex<b><b>ical</b></i>\n<b><i>Lexical</i></b>\n```\n\n<details>\n  <summary>See rendered version!</summary>\n  <div>\n    <i><b>Lexical</b></i>\n    <i><b>Lex</b><b>ical</b></i>\n    <b><i>Lexical</i></b>\n  </div>\n</details>\n\nOf course, there are ways to normalize all these variants to a single canonical form, however this would require DOM manipulation and so re-rendering of the content. And to overcome this we can use Virtual DOM, or State.\n\nOn top of that it allows to decouple content structure from content formatting. Let's look at this example stored in HTML:\n\n```html\n<p>Why did the JavaScript developer go to the bar? <b>Because he couldn't handle his <i>Promise</i>s</b></p>\n```\n\n<figure class=\"text--center\">\n  <img src=\"/img/docs/state-formatting-html.drawio.svg\" alt=\"Nested structure of the HTML state\"/>\n  <figcaption>Nested structure of the HTML state because of the formatting</figcaption>\n</figure>\n\nIn contrast, Lexical decouples structure from formatting by offsetting this information to attributes. This allows us to have canonical document structure regardless of the order in which different styles were applied.\n\nHere is the same content as a Lexical node tree, printed the way the\n[tree view](/docs/getting-started/devtools) shows it (the numbers are node keys):\n\n```text\nroot\n  └ (2) paragraph\n    ├ (3) text \"Why did the JavaScript developer go to the bar? \"\n    ├ (4) text \"Because he couldn't handle his \" { format: bold }\n    ├ (5) text \"Promise\" { format: bold, italic }\n    └ (6) text \"s\" { format: bold }\n```\n\nThe paragraph's children are a flat list of text nodes, and the formatting is a\nproperty of each one, so there is only one way to represent this content.\n\n## Understanding the Editor State\n\nYou can get the latest editor state from an editor by calling `editor.getEditorState()`.\n\nEditor states have two phases:\n\n- During an update they can be thought of as \"mutable\". See \"Updating state\" below to\n  mutate an editor state.\n- After an update, the editor state is then locked and deemed immutable from there on. This\n  editor state can therefore be thought of as a \"snapshot\".\n\nEditor states contain two core things:\n\n- The editor node tree (starting from the root node). See\n  [Document Model](./document-model.md) for how the tree is shaped.\n- The editor [selection](./selection.md) (which can be null).\n\nFor example, \"Hello world\" with a link around \"world\" and the caret at the\nend looks like this:\n\n```mermaid\nflowchart TB\n  accTitle: A simple editor state\n  accDescr: The root contains a paragraph with a \"Hello \" text node and a link. The link contains the \"world\" text node. The selection's anchor and focus both point to offset 5 in \"world\".\n  subgraph state[\"EditorState\"]\n    direction TB\n    root[\"Root\"] --> paragraph[\"Paragraph\"]\n    paragraph --> hello[\"Text: 'Hello '\"]\n    paragraph --> link[\"Link\"]\n    link --> world[\"Text: 'world'\"]\n    selection[\"Selection\"] -.->|\"caret at offset 5\"| world\n  end\n```\n\n`editor.getEditorState()` returns the latest committed state. Its `toJSON()`\nmethod serializes the document tree only; the selection and the runtime node\nkeys are not included. To restore saved content, pass the JSON to\n`editor.parseEditorState()` and the result to `editor.setEditorState()`. The\neditor must have the same node types registered. See\n[Serialization](../serialization/serialization.md) for JSON, HTML, and\nMarkdown.\n\nHere's an example of how you can initialize an editor with saved content and\nthen persist it. The saved JSON is passed as the editor's `$initialEditorState`:\n\n```js\nimport {buildEditorFromExtensions} from '@lexical/extension';\nimport {RichTextExtension} from '@lexical/rich-text';\nimport {defineExtension} from 'lexical';\n\n// Get the saved content (e.g. loaded from a backend)\nconst initialEditorState = await loadContent();\n\nconst editor = buildEditorFromExtensions(\n  defineExtension({\n    $initialEditorState: initialEditorState,\n    dependencies: [RichTextExtension],\n    name: '@my-app/editor',\n  }),\n);\neditor.setRootElement(document.getElementById('editor'));\n\n// Store the content (e.g. when the user submits a form)\nasync function onSubmit() {\n  await saveContent(JSON.stringify(editor.getEditorState()));\n}\n```\n\nWith React, pass the extension to `LexicalExtensionComposer`. A component\nrendered inside it can reach the editor with `useLexicalComposerContext()`:\n\n```jsx\nimport {LexicalExtensionComposer} from '@lexical/react/LexicalExtensionComposer';\nimport {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';\nimport {RichTextExtension} from '@lexical/rich-text';\nimport {defineExtension} from 'lexical';\nimport {useMemo} from 'react';\n\nfunction SaveButton() {\n  const [editor] = useLexicalComposerContext();\n  return (\n    <button\n      onClick={() => saveContent(JSON.stringify(editor.getEditorState()))}>\n      Save\n    </button>\n  );\n}\n\nfunction Editor({initialEditorState}) {\n  // The editor is recreated whenever this extension changes,\n  // so keep it stable for the life of the editor.\n  const extension = useMemo(\n    () =>\n      defineExtension({\n        $initialEditorState: initialEditorState,\n        dependencies: [RichTextExtension],\n        name: '@my-app/editor',\n      }),\n    [initialEditorState],\n  );\n  return (\n    <LexicalExtensionComposer extension={extension}>\n      <SaveButton />\n    </LexicalExtensionComposer>\n  );\n}\n```\n\n`$initialEditorState` is applied once, when the editor is built; changing it\nlater has no effect on that editor. See \"Updating state\" below for the proper\nway to change editor state after initialization.\n\n`$initialEditorState` accepts:\n\n- a JSON string or a parsed `SerializedEditorState` object, passed to\n  `editor.parseEditorState()` (as in the example above);\n- an `EditorState` instance, applied directly with `editor.setEditorState()`;\n- a function `(editor) => void`, run inside `editor.update(...)`;\n- `null`, which skips default initialization entirely. Use this with\n  [collaboration](/docs/collaboration/react) so that the Yjs document, not\n  Lexical, owns the initial state.\n\nOmitting the field (or passing `undefined`) seeds the root with a default empty\n`ParagraphNode`. The two are not interchangeable: `null` leaves the root with no children,\nwhile `undefined` produces a single empty line. If your `loadContent` may yield `null` or\n`undefined` for new documents, coalesce to `undefined` (e.g. `(await loadContent()) ?? undefined`)\nso the editor still gets the default paragraph rather than the collab-style uninitialized\nstate.\n\nThe legacy `LexicalComposer` takes the same values as `initialConfig.editorState`,\nexcept that a function there only runs if the root is still empty.\n\n## Updating state\n\nAll reads and writes of the document happen inside a synchronous callback:\n\n```js\nimport {$createParagraphNode, $createTextNode, $getRoot} from 'lexical';\n\neditor.update(() => {\n  const paragraph = $createParagraphNode();\n  paragraph.append($createTextNode('Hello world'));\n  $getRoot().append(paragraph);\n});\n\nconst text = editor.read('force-commit', () => $getRoot().getTextContent());\n```\n\n### The `$` function convention {#dollar-functions}\n\nFunctions whose names start with `$`, such as `$getRoot()` and\n`$getSelection()`, only work inside one of these callbacks, because they act\non the active editor state. Calling them anywhere else throws an error. The\nconvention is similar to React Hooks:\n\n| | React Hooks | Lexical `$` functions |\n| -- | -- | -- |\n| Naming | `useFunction` | `$function` |\n| Can only be called | while rendering a component | inside an update or read |\n| Can call others of the same kind | ✅ | ✅ |\n| Must be synchronous | ✅ | ✅ |\n| Must be called unconditionally, in the same order | ✅ | ❌ No such rule |\n\nCommand handlers and node transforms already run inside an update, so they\ncan call `$` functions directly.\n\nThis works because, while a callback runs, Lexical keeps the active editor and\nthe active editor state in module-level variables, and `$` functions read\nthem: `$getRoot()` returns the root of the active state, and `$getEditor()`\nreturns the active editor. Because the callback is synchronous, nothing else\ncan run in between and change them. The same mechanism enforces the\ndifference between reads and updates. Inside a read, a method that would\nchange a node throws `Cannot use method in read-only mode.`\n\nGive your own functions a `$` prefix when they call `$` functions, so it is\nclear where they can be used. The\n[`@lexical/rules-of-lexical`](/docs/packages/lexical-eslint-plugin) ESLint rule\nchecks this convention.\n\nThe same rule applies to node objects. Call node methods only inside a read or\nupdate. Every node has a key that identifies it across versions of the editor\nstate, and node methods use that key to find the latest version of the node.\nThis is why a node reference taken earlier in an update stays usable after the\nnode changes. Keys exist only at runtime: they are not serialized, and you\nshould treat them as opaque.\n\nBecause the callbacks are synchronous, do any asynchronous work (fetching\ndata, awaiting a promise) first, and then enter an update with the result.\n\nAvoid nesting one `editor.update()` inside another: the inner update does not\nrun immediately but is queued to run after the outer one. Never start an\nupdate inside a read.\n\nEach update works on a pending copy of the editor state, and updates made in\nthe same tick are committed and reconciled to the DOM together. See\n[Updates](./updates.mdx#update-lifecycle) for the full lifecycle.\n\n### Replacing the state\n\nAnother way to set state is `setEditorState` method, which replaces current state with the one passed as an argument.\n\nHere's an example of how you can set editor state from a stringified JSON:\n\n```js\nconst editorState = editor.parseEditorState(editorStateJSONString);\neditor.setEditorState(editorState);\n```\n\n:::warning\n\n`setEditorState` rejects an `EditorState` that satisfies `editorState.isEmpty()` — i.e.\nthe root is the only node and there is no selection. In a development build it throws\n`\"setEditorState: the editor state is empty. Ensure the editor state's root node never\nbecomes empty.\"`; in production it warns with the same message and recovers by appending\nan empty `ParagraphNode`, so the editor stays usable. The `EditorState` produced by\ninitializing with `editorState: null` and never appending content (typical for a\ncollaboration document before peers connect) has exactly this shape, so persisting and\nreloading such a state hits this path. Note that `parseEditorState` itself succeeds — the\ncheck lands on the apply step. To avoid relying on the recovery, guard with\n`editorState.isEmpty()` before calling `setEditorState`, or seed the document with a\n`ParagraphNode` before serializing.\n\n:::\n\n## Reading state\n\nWhich state you see depends on how you read it:\n\n- Inside `editor.update()` you see the **pending** state, where your changes\n  are visible but transforms and reconciliation may not have run yet.\n  `editor.read('pending', fn)` gives you the same view without allowing\n  changes.\n- `editor.read('force-commit', fn)` first commits any pending updates, so it\n  always sees a consistent, reconciled state. Do not call it inside an update.\n  `editor.read(fn)` with no mode does the same thing, and is kept for\n  convenience and backwards compatibility. Inside a mutation or update\n  listener it does not commit an update that another listener started (that\n  update commits after the listeners have run, so they observe commits in\n  order); use `editor.read('latest', fn)` there.\n- `editor.read('latest', fn)` reads the most recently reconciled state without\n  committing anything, so pending changes are not visible.\n- `editorState.read(fn, {editor})` reads one particular snapshot, such as the\n  `editorState` an update listener receives. Passing the `editor` keeps\n  `$getEditor()` and extension lookups working inside the callback.\n\n## State update listener\n\nIf you want to know when the editor updates so you can react to the changes, you can add an update\nlistener to the editor, as shown below:\n\n```js\neditor.registerUpdateListener(({editorState}) => {\n  // The latest EditorState can be found as `editorState`.\n  // To read the contents of the EditorState, use the following API:\n\n  editorState.read(\n    () => {\n      // Just like editor.update(), .read() expects a closure where you can use\n      // the $ prefixed helper functions.\n    },\n    // Passing the editor keeps $getEditor() and extension lookups working.\n    {editor},\n  );\n});\n```\n\n## When are Listeners, Transforms, and Commands called?\n\nThere are several types of callbacks that can be registered with the editor that are related to\nupdates of the Editor State.\n\n| Callback Type | When It's Called |\n| -- | -- |\n| Update Listener | After reconciliation |\n| Mutation Listener | After reconciliation |\n| Node Transform | During `editor.update()`, after the callback finishes, if any instances of the node type they are registered for were updated |\n| Command | As soon as the command is dispatched to the editor (called from an implicit `editor.update()`) |\n\n## Synchronous reconciliation with discrete updates\n\nWhile commit scheduling and batching are normally what we want, they can sometimes get in the way.\n\nConsider this example: you're trying to manipulate an editor state in a server context and then persist it in a database.\n\n```js\neditor.update(() => {\n  // manipulate the state...\n});\n\nsaveToDatabase(editor.getEditorState().toJSON());\n```\n\nThis code will not work as expected, because the `saveToDatabase` call will happen before the state has been committed.\nThe state that will be saved will be the same one that existed before the update.\n\nFortunately, the `discrete` option for `LexicalEditor.update` forces an update to be immediately committed.\n\n```js\neditor.update(() => {\n  // manipulate the state...\n}, {discrete: true});\n\nsaveToDatabase(editor.getEditorState().toJSON());\n```\n\n### Cloning state\n\nLexical state can be cloned, optionally with custom selection. One of the scenarios where you'd want to do it\nis setting editor's state but not forcing any selection:\n\n```js\n// Passing `null` as a selection value to prevent focusing the editor\neditor.setEditorState(editorState.clone(null));\n```\n","packages/lexical-website/docs/concepts/headless.md":"# Running Without a Browser\n\nThe DOM is only needed to show an editor on screen. Because every read and\nupdate goes through the editor state, Lexical also works where there is no\nbrowser DOM:\n\n- **With no DOM at all.** An editor that has no root element skips\n  reconciliation and DOM selection entirely, so you can build one with\n  `buildEditorFromExtensions()` and never call `setRootElement()`. Adding\n  `HeadlessExtension` from `@lexical/headless` makes that explicit: attaching a\n  root element then throws, and nested editors are headless too. Updates,\n  transforms, listeners, commands, and JSON serialization all still work,\n  which is enough to process documents on a server, apply changes from a\n  collaboration backend, or write tests.\n- **With a virtual DOM** such as happy-dom or jsdom, for the features that\n  create DOM nodes, like HTML import and export. `withDOM()` from\n  `@lexical/headless/dom` runs a callback with a temporary happy-dom window,\n  for example to convert HTML on a server.\n\n## Example: converting saved JSON to HTML on a server\n\n```ts\nimport {buildEditorFromExtensions} from '@lexical/extension';\nimport {HeadlessExtension} from '@lexical/headless';\nimport {withDOM} from '@lexical/headless/dom';\nimport {$generateHtmlFromNodes} from '@lexical/html';\nimport {RichTextExtension} from '@lexical/rich-text';\nimport {defineExtension} from 'lexical';\n\nconst editor = buildEditorFromExtensions(\n  defineExtension({\n    dependencies: [HeadlessExtension, RichTextExtension],\n    name: '@my-app/server-editor',\n  }),\n);\n\nexport function jsonToHtml(json: string): string {\n  editor.setEditorState(editor.parseEditorState(json));\n  return withDOM(() => editor.read(() => $generateHtmlFromNodes(editor)));\n}\n```\n\nThe editor must have the same node types registered as the editor that saved\nthe JSON, which is why it uses the same extensions. JSON and Markdown\nconversion do not need `withDOM()`.\n\n## `createHeadlessEditor()`\n\nBefore extensions, headless editors were created with `createHeadlessEditor()`\nfrom `@lexical/headless`. It still works, and it makes DOM-only methods such as\n`registerRootListener` and `registerMutationListener` throw. Because many\nextensions register those listeners, use `HeadlessExtension` with\n`buildEditorFromExtensions()` in new code.\n","packages/lexical-website/docs/concepts/history.md":"# History\n\nLexical adds support for Undo and Redo through the `@lexical/history` package —\n`registerHistory`, the `HistoryExtension` extension, or the `HistoryPlugin`\nReact component from `@lexical/react/LexicalHistoryPlugin`. Part of undo and redo requires supporting coalescing of certain dynamic operations such as continuous typing. The below provides background information regarding the continuous typing undo coalescing feature.\n\n**Undo Coalescing of Typed Text**\n\n**TL;DR**\nWhen working with text editors, users expect a well polished undo experience. A key part of this is a smooth, well thought out workflow when undoing continuous typing events. For this post, strict typing events may be limited to typing characters, backspacing and forward deleting. Users generally expect that continuous typing should fully undo with a single undo gesture and that they don’t require separate undo gestures for each character.\nThe algorithm that covers this is “undo coalescing of typed text” and there’s a certain degree of nuance involved. This post covers that nuance while attempting to avoid prescriptive implementation details.\n\n**What Kind of Edits Require Coalescing?**\nAs mentioned above, continuous typing should be coalesced into a single undo action. Continuous typing is a loaded phrase and we’ll get into some details below.\nThere are other types of edits that require undo coalescing such as rapidly resizing an image or rapidly changing the font size of selected text. These latter two examples fall under what might be termed “dynamic operations” and their implementation might vary quite a bit from coalescing of typing.\n\n**What Constitutes Continuous Typing?**\nFor the purposes of undo coalescing, continuous typing events are safely limited to these three actions:\n\n1. Forward typing characters (heads up, composed character typing discussed later).\n2. Deleting backwards, e.g. backspace gestures including backspace key presses.\n3. Forward deletion, e.g. forward delete gestures including Delete key presses.\n\nAny interruption to the above breaks the notion of continuous typing. Note that switching between either of these three actions may itself be considered an interruption of continuous typing or it may not. This ends up being a usability decision. In my opinion, when examining various word processors, switching between these three actions should maintain continuity, because users often make typos or corrections in the flow of typing and therefore would lean towards the whole sequence of typing, backspacing and deleting to be within a single undoable session.\nNote that Google Docs and Quip treat a switch between typing, backspacing and deleting as disruptions to the continuous typing sessions.\n\n**Start with Selected Text and Followup with Typing**\nLet’s start with some selected text. The user may have selected the text directly, as a result of pasting text or through some other sequence of steps. When followed by continuous typing, the initial selection should be included along with the following typing events as part of a single coalesced undo session.\n\n**Timeouts**\nText editing is generally a modeless user experience and so just about any event beyond the above typing actions will disrupt continuous typing. The first and foremost is a timeout.\nQuip seems to require continuous typing to occur within 1 second intervals, whereas Google Docs requires about 2 seconds. In my opinion, the timeout may stretch to as long as 3 to 5 seconds. The more time between typing events, the more chance the user has to intentionally input a block of text, a thought or concept.\nThe use of a timeout is not necessarily required, and the amount of time is certainly up for debate. Collaborative co-editing also makes use of timeouts when grouping text that is merged across clients. Longer timeouts tend to make collaboration a smoother, less disruptive experience, because receiving other client input while expressing one’s thoughts tends to be unhelpful.\nImplementers should consider the correct timeout based on their customer expectations and related collaborative features that might share this timeout.\n\n**Hard and Soft Returns**\nHard returns as generated by pressing the Return key tend to disrupt and thereby reset continuous typing.\nFor Google Docs and Quip, soft returns as generated by pressing shift+Return key do not terminate continuous typing. This makes sense in that the soft return ends text wrap but does not end the paragraph.\n\n**Changing Selections and Key Focus**\nAny change to the selection by the user should terminate coalescing. This includes:\n\n- Arrow gesture navigation.\n- Gestures to select a word, a sentence or a paragraph.\n- Any other selection change that does not move the blinking caret to the next or previous character by way of the above mentioned typing events.\n\nAdditionally, ending keyboard focus on the editing window, including moving focus to another editing window or a different view within the same window should also terminate coalescing. This also includes setting focus to modal popovers, such as, for example, a hyperlink property editor.\n\n**Composed Characters**\nCharacter composition occurs when typing diacritics, CJK and even emojis. The process usually involves at least these three phases:\n\n- Start composition.\n- Replace composed text with other text and remain composing. This step may occur many times within a single composition session.\n- Stop composition either through accepting the text or canceling the latest input.\n\nCoalescing should terminate at start composition. Replacing composed text should generally be coalesced. Stopping composition may or may not reset coalescing depending upon the implementation. Quip appears to try to coalesce multiple composed characters, but disrupts coalescing when switching between composed and non-composed characters. Google Docs appears to disrupt coalescing more frequently. Both coalesce the text replacements.\n\n**Auto Correct, Spelling Corrections and Text Substitutions**\nText substitution behaves much like composed characters. When the text is transformed, the coalescing should terminate and the transformed text becomes the first coalesced entry in the next undo session.\n\n**Copy & Pasting**\nAny actions like copy and paste typically trigger events that are not part of the 3 continuous typing actions described above. So, copy and paste also terminates coalescing.\n\n**In Conclusion**\nText undo coalescing greatly improves the usability of undo in that it allows the user to bypass all the intermediate events that went into forming or re-forming text within a paragraph. Given the modeless nature of text editing, limiting continuous typing to strictly three events allows for a robust and clean implementation. Implementers will need tight bottlenecks to trap selection changes, key focus changes as well as changes from collaborative clients. While the above is not complete by any means, I hope readers will find it helpful.\n\n## Tuning HistoryExtension for memory and long sessions\n\n`HistoryExtension` ships with config values chosen for the common case\nof a short-lived editor on a richly interactive surface. For\napplications that keep an editor mounted for a long time — chat\nthreads, long documents, anything where the user may produce thousands\nof distinct history events without reloading the page — two settings\nare worth tuning explicitly.\n\n### `maxDepth` — cap the undo stack\n\nBy default `maxDepth` is `null`, meaning the undo stack is\n**unbounded**. Every separate-history boundary (see \"What Constitutes\nContinuous Typing?\" above) pushes a `HistoryStateEntry` that retains a\nfull `EditorState` snapshot. Snapshots share a copy-on-write\n`NodeMap`, so the marginal cost of any one entry is small, but it is\nnot zero — and an unbounded stack accumulates a long tail over time.\n\nSet a finite `maxDepth` to keep the stack at a fixed length. The\nextension FIFO-evicts the oldest entries past the cap; older snapshots\nbecome unreachable and can be collected.\n\n```ts\nimport {configExtension, defineExtension} from 'lexical';\nimport {HistoryExtension} from '@lexical/history';\n\nconst editorExtension = defineExtension({\n  // ...\n  dependencies: [\n    // 100 events is a reasonable starting point that supports a deep\n    // enough undo stack for almost any interactive editing session.\n    configExtension(HistoryExtension, {maxDepth: 100}),\n  ],\n});\n```\n\nThe cap is also exposed as a writable signal on\n`HistoryExtension.output.maxDepth`, so it can be reconfigured at\nruntime (the new value applies to future pushes — the current stack\nis not retroactively trimmed when the cap is lowered).\n\n### `delay` — coalesce more aggressively\n\n`delay` is the merge window during which adjacent edits collapse into\nthe current history entry instead of pushing a new one. The default is\n`300`ms. A longer delay means fewer history events for the same\ntyping burst, which:\n\n- reduces the rate at which `undoStack` grows, multiplying the effect\n  of any `maxDepth` cap, and\n- gives the user undo gestures that match larger semantic chunks of\n  their writing.\n\nFor text-heavy editors a value in the `500`–`1000`ms range is common.\n\n```ts\nconfigExtension(HistoryExtension, {delay: 500, maxDepth: 100}),\n```\n\n### When NOT to cap history\n\nIf your editor is short-lived (mounted only for the duration of a\nmodal, page form, or single message draft) and undo is expected to\nreach back to the document's initial state, the default `maxDepth:\nnull` is fine — the stack will be discarded with the editor instance.\nThe cap matters when an editor is **long-lived** and can accumulate\nthousands of distinct history events without unmounting.\n","packages/lexical-website/docs/concepts/key-management.md":"# Key Management\n\nKeys are a fundamental concept in Lexical that enable efficient state management and node tracking. Understanding how keys work is crucial for building reliable editor implementations.\n\n## What are Keys?\n\nThe `__key` property is a unique identifier assigned to each node in the Lexical editor. These keys are:\n- Automatically generated by Lexical\n- Used to track nodes in the editor state\n- Essential for state management and updates\n- Immutable during a node's lifecycle\n\n## When to Use `__key`?\n\n### ✅ Correct Usage\n\nKeys should ONLY be used in two specific situations:\n\n1. **In Node Constructors**\n```typescript\nclass MyCustomNode extends ElementNode {\n  constructor(someData: string, key?: NodeKey) {\n    super(key); // Correctly passing key to parent constructor\n    this.__someData = someData;\n  }\n}\n```\n\n2. **In Static Clone Methods**\n```typescript\nclass MyCustomNode extends ElementNode {\n  static clone(node: MyCustomNode): MyCustomNode {\n    return new MyCustomNode(node.__someData, node.__key);\n  }\n}\n```\n\n### ❌ Incorrect Usage\n\nNever use keys in these situations:\n\n```typescript\n// ❌ Don't pass keys between different nodes\nconst newNode = new MyCustomNode(existingNode.__key);\n\n// ❌ Don't manipulate keys directly\nnode.__key = 'custom-key';\n```\n\n## How Lexical Uses Keys\n\n### Diagram\n\nThe dotted outlines show nodes that are re-used in a zero-copy fashion from one EditorState to the next\n\n```mermaid\ngraph TB\n  subgraph s0[\"Initial State\"]\n    direction TB\n    m0[\"NodeMap (v0)\"]\n    m0 -->|Key A| n0a[\"A (v0)\"]\n    m0 -->|Key B| n0b[\"B (v0)\"]\n  end\n  subgraph s1[\"Create Node C\"]\n    direction TB\n    style n1a stroke-dasharray: 5 5\n    style n1b stroke-dasharray: 5 5\n    m1[\"NodeMap (v1)\"]\n    m1 -->|Key A| n1a[\"A (v0)\"]\n    m1 -->|Key B| n1b[\"B (v0)\"]\n    m1 -->|Key C| n1c[\"C (v0)\"]\n  end\n  subgraph s2[\"Update Node A\"]\n    direction TB\n    style n2a stroke-dasharray: 5 0\n    style n2b stroke-dasharray: 5 5\n    style n2c stroke-dasharray: 5 5\n    m2[\"NodeMap (v1)\"]\n    m2 -->|Key A| n2a[\"A (v1)\"]\n    m2 -->|Key B| n2b[\"B (v0)\"]\n    m2 -->|Key C| n2c[\"C (v0)\"]\n  end\n  s0 -.-> s1 -.-> s2\n```\n\n### Node Map Structure\n\nThe EditorState maintains a `Map<NodeKey, LexicalNode>` that tracks all nodes. Nodes refer to each other using keys in their internal pointers:\n\n```typescript\n// Internal node structure (not for direct usage)\n{\n  __prev: null | NodeKey,\n  __next: null | NodeKey,\n  __parent: null | NodeKey,\n  // __first, __last and __size are only for ElementNode to track its children\n  __first: null | NodeKey,\n  __last: null | NodeKey,\n  __size: number\n}\n```\n\nThese internal pointers maintain the tree structure and should never be manipulated directly.\n\n### Key-Related APIs\n\n1. **Editor Methods**\n   ```typescript\n   // Get node by key\n   const node = $getNodeByKey(key);\n\n   // Get the DOM element a node key is reconciled to\n   const element = editor.getElementByKey(key);\n\n   // Get latest version of a node\n   const latest = node.getLatest();\n   \n   // Get mutable version for updates\n   const mutable = node.getWritable();\n   ```\n\n### Key Lifecycle\n\nNodeKeys are ephemeral and have several important characteristics:\n\n1. **Serialization**\n   - Keys are never serialized\n   - New keys are generated when deserializing (from JSON/HTML)\n   - Keys are only meaningful within their EditorState instance\n\n2. **Uniqueness**\n   - Keys are unique within an EditorState\n   - Current implementation uses serial numbers for debugging\n   - Should be treated as random and opaque values\n   - Never logically reused\n\nKeys are used internally by Lexical to:\n1. Track nodes in the editor state\n2. Manage node updates and versions\n3. Maintain referential integrity\n4. Enable efficient state updates\n\n## Common Pitfalls\n\n1. **Key Reuse**\n   ```typescript\n   // ❌ Never do this\n   function duplicateNode(node: LexicalNode) {\n     return new SameNodeType(data, node.__key);\n   }\n   ```\n\n2. **Manual Key Assignment**\n   ```typescript\n   // ❌ Never do this\n   node.__key = generateCustomKey();\n   ```\n\n3. **Incorrect Constructor/Clone Implementation**\n   ```typescript\n   // ❌ Never do this - missing key in constructor\n   class MyCustomNode extends ElementNode {\n     constructor(someData: string) {\n       super(); // Missing key parameter\n       this.__someData = someData;\n     }\n   }\n\n   // ✅ Correct implementation\n   class MyCustomNode extends ElementNode {\n     __someData: string;\n\n     constructor(someData: string, key?: NodeKey) {\n       super(key);\n       this.__someData = someData;\n     }\n     \n     static clone(node: MyCustomNode): MyCustomNode {\n       return new MyCustomNode(node.__someData, node.__key);\n     }\n\n     afterCloneFrom(prevNode: this): void {\n       super.afterCloneFrom(prevNode);\n       this.__someData = prevNode.__someData;\n     }\n   }\n   ```\n\n4. **Node Replacement**\n   ```typescript\n   // ❌ Never re-use the key when changing the node class\n   const editorConfig = {\n     nodes: [\n       CustomNodeType,\n       {\n         replace: OriginalNodeType,\n         with: (node: OriginalNodeType) => new CustomNodeType(node.__key),\n         withKlass: CustomNodeType\n       }\n     ]\n   };\n\n   // ✅ Correct: Use node replacement configuration\n   const editorConfig = {\n     nodes: [\n       CustomNodeType,\n       {\n         replace: OriginalNodeType,\n         with: (node: OriginalNodeType) => new CustomNodeType(),\n         withKlass: CustomNodeType\n       }\n     ]\n   };\n   ```\n\n   For proper node replacement, see the [Node Replacement guide](node-replacement.md).\n\n## Best Practices\n\n1. **Let Lexical Handle Keys**\n   ```typescript\n   import {$applyNodeReplacement} from 'lexical';\n\n   // Create node helper function\n   export function $createMyCustomNode(data: string): MyCustomNode {\n     return $applyNodeReplacement(new MyCustomNode(data));\n   }\n   ```\n\n## Testing Considerations\n\nWhen writing tests involving node keys:\n\n```typescript\ntest('node creation', async () => {\n  await editor.update(() => {\n    // ✅ Correct: Create nodes normally\n    const node = new MyCustomNode(\"test\");\n    \n    // ✅ Correct: Keys are automatically handled\n    expect(node.__key).toBeDefined();\n    expect(node.__key).not.toBe('');\n  });\n});\n```\n\n## Performance Impact\n\nUnderstanding key management is crucial for performance:\n\n1. Keys enable efficient node lookup (O(1))\n2. Proper key usage prevents unnecessary re-renders\n3. Lexical's key system optimizes state updates\n4. Improper key manipulation can cause performance issues\n\n## Common Questions\n\n**Q: How do I reference a node later?**\nA: Store a reference to the node. Conventionally, all node methods will use `getLatest()` or `getWritable()` which will look up the latest version of that node before reading or writing its properties, which is equivalent to using the key but is type-safe (but may cause errors if you try to use a reference to a node that no longer exists). In some situations it may be preferable to use the key directly, which is also fine.\n\n**Q: How do I ensure unique nodes?**\nA: Let Lexical handle key generation and management. Focus on node content and structure. ","packages/lexical-website/docs/concepts/keyboard-accessibility.md":"# Keyboard Accessibility\n\nLexical follows the [WAI-ARIA Authoring Practices](https://www.w3.org/WAI/ARIA/apg/) keyboard model. This page documents the contracts that plugin authors and host applications rely on, so a Lexical editor can sit inside a larger keyboard-accessible flow without trapping or surprising the user.\n\n## Tab key\n\nBy default, `Tab` inside the editor follows the browser's normal focus order — it moves to the next focusable element on the page. There is no keyboard trap.\n\nThe optional `TabIndentationExtension` (`@lexical/extension`) takes over `Tab` and uses it for block indent / outdent. Hosts that mount this extension opt in to that behavior. WCAG 2.1.2 (No Keyboard Trap) still applies — pressing `Escape` blurs the editor (see below), at which point `Tab` leaves the editor again.\n\n## Escape key\n\nPressing `Escape` while the editor has focus calls `editor.blur()` via the rich-text Escape command handler (registered at `COMMAND_PRIORITY_EDITOR`). The handler only blurs when the current selection is a range selection; with a `NodeSelection` (or no selection) it returns `false` and falls through without blurring. After blur, the next `Tab` advances to the page's next focusable element — so the editor is not a hard keyboard trap. (`TabIndentationExtension` consumes `Tab` only while a range selection is active, and its own source notes that taking over `Tab` is generally discouraged for keyboard accessibility, so treat WCAG 2.1.2 conformance as something to verify in your integration rather than a guarantee.)\n\nPlugin authors who want to intercept `Escape` (modal close, dropdown close, etc.) should register their handler at a higher priority and return `true` to stop the command chain. Common patterns already established in the codebase:\n\n| Priority | Caller | Purpose |\n| --- | --- | --- |\n| `HIGH` | Floating link editor | Close the floating UI, keep editor focus |\n| `HIGH` | Equation component (while editing) | Close the equation editor input |\n| `NORMAL` | Comment plugin | Cancel inline comment editing |\n| `LOW` | Image component | Escape out of caption / resize editing: drop the inner selection and restore the `NodeSelection` on the image decorator |\n| `EDITOR` | Rich text (default) | `editor.blur()` (range selection only) |\n\nHandlers that return `false` let the chain continue, so Escape can still fall through to the default blur even after a higher-priority handler runs.\n\nSome host UI binds its close handler directly to a DOM `keydown` listener rather than going through `KEY_ESCAPE_COMMAND` — the playground's `Modal` via `window.addEventListener('keydown', ...)` and its `DropDown` via a React `onKeyDown` on the menu. Those paths run alongside the table above; they do not participate in the editor's command priority chain.\n\n## React hooks and `@lexical/a11y` extensions\n\nThe keyboard helpers below ship as `@lexical/a11y` _extensions_ — the extension is the API and does all of the work (event listeners, focus management, the live region). The `@lexical/react` hooks (`useLexicalFocusManagerRef`, `useLexicalRovingTabIndexRef`, `useLexicalFocusTrapRef`, `useLexicalAriaLiveRegion`) are thin React adapters that only _communicate_ with their extension: each returns a `RefCallback` (or an `announce` function) that drives the extension's signals. Using a hook therefore **requires** its matching extension to be registered in the editor's extension tree — it is not an optional integration path. Non-React hosts (Svelte, Vue, vanilla DOM) use the extensions directly and skip the hooks.\n\n## Toolbar focus jump\n\nThe playground uses `useLexicalFocusManagerRef` (`@lexical/react/useLexicalFocusManagerRef`) to provide a documented shortcut for moving focus from the editor to the toolbar without tabbing through the page. The hook returns a `RefCallback` that registers the attached element as a focus-managed toolbar. It listens for `Alt+F10` inside the editor and moves focus into the toolbar — preferring the item that currently carries `tabindex=\"0\"` (the active roving item, when paired with `useLexicalRovingTabIndexRef`), and otherwise the first focusable item. While focus is in the toolbar, pressing `Escape` restores both focus and the editor's prior selection via `editor.focus()`. Multiple toolbars can be registered simultaneously. Requires `FocusManagerExtension` from `@lexical/a11y` in the editor's extension tree.\n\nThis matches the WAI-ARIA editor menubar pattern and the convention used by Word and CKEditor. Hosts that don't mount `useLexicalFocusManagerRef` get the default browser flow (Tab in, Tab out).\n\n## Toolbar arrow-key navigation\n\nToolbars that opt into `useLexicalRovingTabIndexRef` (`@lexical/react/useLexicalRovingTabIndexRef`) collapse to a single tab stop: arrow keys move between items inside the toolbar; `Tab` moves past the toolbar as a whole. Items keep `tabindex=\"-1\"` except the active one (`tabindex=\"0\"`). The hook returns a `RefCallback` — multiple containers can be registered simultaneously. Tracks the WAI-ARIA toolbar pattern. Requires `RovingTabIndexExtension` from `@lexical/a11y` in the editor's extension tree.\n\n## Modal focus trap\n\n`useLexicalFocusTrapRef` (`@lexical/react/useLexicalFocusTrapRef`) returns a `RefCallback` that cycles `Tab` / `Shift+Tab` inside a modal container and restores focus to the previously-focused element on deactivation. While active, the trap also installs a document-level `focusin` listener that pulls any focus landing outside the container back to the first focusable descendant (or the container itself) — so it recovers from the browser routing `Tab` out to its own chrome. Although several containers can be _registered_, only one trap should be _active_ at a time: concurrent traps install competing `focusin` listeners and fight over focus. Escape is **not** intercepted by the hook — modal owners handle the close key themselves, so the editor's Escape contract above stays predictable. Requires `FocusTrapExtension` from `@lexical/a11y` in the editor's extension tree.\n\n## Screen reader announcements\n\n`useLexicalAriaLiveRegion` (`@lexical/react/useLexicalAriaLiveRegion`) returns a stable `announce(message)` function. The hook itself mounts no DOM — like the others above, it is a thin adapter that **requires `AriaLiveRegionExtension` from `@lexical/a11y` in the editor's extension tree**. That extension owns the lifecycle of the visually-hidden region (`role=\"status\"`, `aria-atomic`, `aria-live` defaulting to `polite` — configurable via its `politeness` config): the region is created in the extension's `register` step and disposed on the editor's teardown, not on the hook's unmount. `EditorModeAnnounceExtension` (mode transitions via `registerEditableListener`) and `HistoryAnnounceExtension` (undo / redo via `UNDO_COMMAND` / `REDO_COMMAND`) announce through the same region. Both extensions accept optional message overrides for i18n.\n\n## Component ARIA quick reference\n\nWhere Lexical already applies ARIA attributes today (set in each component's `createDOM` / `updateDOM`, declaratively in the surrounding React tree, or applied imperatively via `setAttribute` on a managed or portaled element — as the typeahead menu and the live region do):\n\n| Component | role | aria-\\* attributes |\n| --- | --- | --- |\n| `ToolbarPlugin` (playground) | `toolbar` | `aria-label` |\n| `Modal` (playground) | `dialog` | `aria-modal`, `aria-labelledby` |\n| `LexicalListItemNode` (check list leaf) | `checkbox` | `aria-checked` |\n| `LexicalMenu` typeahead popup | `listbox` | `aria-label` |\n| Editor root (while typeahead is open) | — | `aria-controls`, `aria-activedescendant` |\n| `EquationNode` (playground) | `math` | `aria-label` |\n| `useLexicalAriaLiveRegion` region | `status` | `aria-live`, `aria-atomic` |\n\nCustom nodes follow the same pattern: set the attributes in `createDOM` and re-apply in `updateDOM` when the state they reflect changes. There is no central ARIA contract on `LexicalNode`; treat each subclass's `createDOM` as the source of truth.\n\n## Browser notes\n\nLexical relies on the standard DOM `Escape` contract — verified on Chrome and Safari (macOS, normal window). Safari in fullscreen intercepts `Escape` at the browser level to exit fullscreen; that is the browser's contract, not Lexical's.\n\n## What this page does not cover\n\n- Touch / mobile screen reader gestures. The hooks above share the same DOM contracts with mobile (TalkBack, VoiceOver iOS) but mobile has not been exercised in this set of changes.\n- High contrast / forced-colors theming. See the playground's `index.css` `@media (forced-colors: active)` block for a baseline.\n","packages/lexical-website/docs/concepts/listeners.md":"\n\n# Listeners\n\nListeners are a mechanism that lets the Editor instance inform the user when a certain operation has occurred. All listeners follow a reactive pattern where you can do an operation upon something happening in the future. All listeners also return a function that easily allows for the\nlistener to be unregistered. Below are the different listeners that Lexical supports today:\n\n## `registerUpdateListener`\n\nGet notified when Lexical commits an update to the DOM.\n\n```js\nconst removeUpdateListener = editor.registerUpdateListener(({editorState}) => {\n  // The latest EditorState can be found as `editorState`.\n  // To read the contents of the EditorState, use the following API:\n\n  editorState.read(() => {\n    // Just like editor.update(), .read() expects a closure where you can use\n    // the $ prefixed helper functions.\n  });\n});\n\n// Do not forget to unregister the listener when no longer needed!\nremoveUpdateListener();\n```\n\nThe update listener callbacks receives a single argument containing the follow properties:\n\n- `editorState` the latest updated Editor State\n- `prevEditorState` the previous Editor State\n- `tags` a Set of all tags that were passed to the update\n- `dirtyElements` a Map of the NodeKeys of ElementNodes that were marked\n  dirty, to a boolean that is true when the node was intentionally mutated\n- `dirtyLeaves` a Set of the NodeKeys of all dirty nodes that are not\n  ElementNodes\n- `mutatedNodes` a Map of LexicalNode constructors to a\n  `Map<NodeKey, NodeMutation>`, or null when no DOM was mutated. Only\n  populated when at least one MutationListener is registered\n- `normalizedNodes` (advanced) the keys of TextNode descendants that were\n  merged with a sibling by normalization\n\nOne thing to be aware of is \"waterfall\" updates. This is where you might schedule an update inside an update\nlistener, as shown below:\n\n```js\neditor.registerUpdateListener(({editorState}) => {\n  // Read the editorState and maybe get some value.\n  editorState.read(() => {\n    // ...\n  });\n\n  // Then schedule another update.\n  editor.update(() => {\n    // ...\n  });\n});\n```\n\nThe problem with this pattern is that it means we end up doing two DOM updates, when we likely could have\ndone it in a single DOM update. This can have an impact on performance, which is important in a text editor.\n\nA listener that re-enqueues work without a stop condition is worse than slow.\nThe editor keeps a cascade budget: once a run of updates re-enqueues more\nthan 100 times without yielding to the event loop, it discards the entire\npending update queue and warns. Bounded activity never reaches the limit —\nthe budget is reset from a macrotask, which a genuine runaway starves — but a\ntrue loop silently loses whatever else was queued.\n\nTo avoid this, we recommend looking into [Node Transforms](transforms.md), which allow you to listen to node changes and\ntransform them as part of the same given update, meaning no waterfalls!\n\n## `registerTextContentListener`\n\nGet notified when Lexical commits an update to the DOM and the text content of the editor changes from\nthe previous state of the editor. If the text content is the same between updates, no notifications to\nthe listeners will happen.\n\n```js\nconst removeTextContentListener = editor.registerTextContentListener(\n  (textContent) => {\n    // The latest text content of the editor!\n    console.log(textContent);\n  },\n);\n\n// Do not forget to unregister the listener when no longer needed!\nremoveTextContentListener();\n```\n\n## `registerMutationListener`\n\nGet notified when a specific type of Lexical node has been mutated. There are three states of mutation:\n\n- `created`\n- `destroyed`\n- `updated`\n\nMutation listeners are great for tracking the lifecycle of specific types of node. They can be used to\nhandle external UI state and UI features relating to specific types of node.\n\nIf any existing nodes are in the DOM, and skipInitialization is not true, the listener\nwill be called immediately with an updateTag of 'registerMutationListener' where all\nnodes have the 'created' NodeMutation. This can be controlled with the skipInitialization option,\nwhich defaults to false.\n\n```js\nconst removeMutationListener = editor.registerMutationListener(\n  MyCustomNode,\n  (mutatedNodes, { updateTags, dirtyLeaves, prevEditorState }) => {\n    // mutatedNodes is a Map where each key is the NodeKey, and the value is the state of mutation.\n    for (let [nodeKey, mutation] of mutatedNodes) {\n      console.log(nodeKey, mutation)\n    }\n  },\n  {skipInitialization: false}\n);\n\n// Do not forget to unregister the listener when no longer needed!\nremoveMutationListener();\n```\n\n## `registerEditableListener`\n\nGet notified when the editor's mode has changed. The editor's mode can be changed\nvia `editor.setEditable(boolean)`.\n\n```js\nconst removeEditableListener = editor.registerEditableListener(\n  (editable) => {\n    // The editor's mode is passed in!\n    console.log(editable);\n  },\n);\n\n// Do not forget to unregister the listener when no longer needed!\nremoveEditableListener();\n```\n\n## `registerDecoratorListener`\n\nGet notified when the editor's decorator object changes. The decorator object contains\nall `DecoratorNode` keys -> their decorated value. This is primarily used with external\nUI frameworks.\n\n```js\nconst removeDecoratorListener = editor.registerDecoratorListener(\n  (decorators) => {\n    // The editor's decorators object is passed in!\n    console.log(decorators);\n  },\n);\n\n// Do not forget to unregister the listener when no longer needed!\nremoveDecoratorListener();\n```\n\n## `registerRootListener`\n\nGet notified when the editor's root DOM element (the content editable Lexical attaches to) changes. This is primarily used to\nattach event listeners to the root element. *The root listener function is executed directly upon registration and then on any subsequent update.*\n\n```js\nconst removeRootListener = editor.registerRootListener(\n  (rootElement, prevRootElement) => {\n   //add listeners to the new root element\n   //remove listeners from the old root element\n  },\n);\n\n// Do not forget to unregister the listener when no longer needed!\nremoveRootListener();\n```\n","packages/lexical-website/docs/concepts/named-slots.md":"# Named Slots\n\n:::warning\n\nNamed slots are experimental. The APIs described here are tagged\n`@experimental`, may change without a major version bump, and the\nserialization and collab formats should be treated as unstable until the\nfeature is stabilized.\n\n:::\n\nNamed slots let a single host node (an ElementNode or a DecoratorNode) own\nseveral isolated editable regions addressed by name — a Card's `title`, a\nPullQuote's `quote` and `attribution` — inside the host's own EditorState.\nEach region takes its own caret and formatting, never merges across the\nboundary, and `Cmd+A` doesn't spill into the rest of the document.\n\nBefore named slots, the usual answer was a nested editor per region: every\nregion lives in its own EditorState, so moving nodes between regions and\nkeeping history and collab in sync all require serialization and extra\n`editor.update` passes. The other node shapes don't fit either: plain\nElementNode children share one undivided linked list (Backspace at a region\nstart merges it into the previous region), and a DecoratorNode is atomic, so\nLexical can't own selection, collab, or serialization inside it. With named\nslots, editing a slot is just editing the one tree.\n\n:::note\n\nNamed slots are the model-level generalization of the rendering concept\nbehind [`$getDOMSlot`](/docs/api/modules/lexical#getdomslot) /\n`ElementDOMSlot` in the\n[DOM rendering](../serialization/dom-render.md) documentation. Every\nElementNode already has one _unnamed_ children channel, and `$getDOMSlot`\ncontrols where that channel's content attaches in the node's DOM. Named\nslots are additional, explicitly-named channels alongside it — symmetric in\nthat each renders into a controllable location in the host's DOM — but they\nalso carry the model-level semantics the unnamed channel doesn't: isolation\n(the virtual shadow root below), a separate NodeKey map, and their own\nserialization and collaboration.\n\n:::\n\n## The Model\n\nA host keeps a second child channel, a `Map` of slot name to NodeKey,\nseparate from its ordinary linked-list children. A slot value has its slot\nhost pointer set and `getParent() === null`, with exactly one of the two\nnon-null: `getParent()` stops at the slot boundary, and you climb out only\nthrough [`$getSlotHost()`](/docs/api/modules/lexical#getslothost).\n\nThe slot link itself acts as a **virtual invisible shadow root** between the\nhost and the value. Isolation is structural rather than a convention — an\naccidental boundary crossing surfaces as a thrown invariant, not silent\ncorruption.\n\nIn the DOM, each value renders synchronously into a keyless\n`<div data-lexical-slot=\"<name>\">` container parked slots-first in the host\nDOM as a **hidden placeholder** (`display: none`), in a canonical order\nderived from the host class (see [slot order](#slot-order)). Nothing is\nvisible until the host explicitly attaches the container somewhere (see\n[Rendering](#rendering)) — mirroring how\n[`getDOMSlot`](/docs/api/modules/lexical#getdomslot) gives an element\ncontrol over where its linked-list children render.\n\n## Hosts and Values\n\nAny ElementNode or DecoratorNode can host slots. An ElementNode host can mix\nslots with ordinary children (a Card with a `title` slot plus regular body\nparagraphs); a DecoratorNode host has no children channel, so slots are its\nonly editable content (a PullQuote that is otherwise atomic).\n\nA slot value is any non-inline ElementNode or DecoratorNode. The value's\nshape decides the editing model:\n\n- **A bare block** (for example a ParagraphNode) behaves as a single-line\n  field with no extra wrapper in the model, the JSON, or the DOM: its virtual\n  scope holds exactly one block, so Enter is a no-op (hosts may map it to\n  focus movement) and a multi-block paste flattens to inline content the way\n  an `<input>` sanitizes its value — line breaks stripped, block-only\n  decorators dropped.\n- **A shadow-root container** (an ElementNode whose\n  [`isShadowRoot()`](/docs/api/modules/lexical#isshadowroot) returns\n  `true`) behaves as a multi-block region with normal block editing inside.\n\n## Declaring and Setting Slots\n\nA host class declares its slot names in [`$config()`](nodes.mdx); values are\nattached with [`$setSlot`](/docs/api/modules/lexical#setslot):\n\n```ts\nimport {\n  $create,\n  $createParagraphNode,\n  $setSlot,\n  ElementNode,\n} from 'lexical';\n\nclass CardNode extends ElementNode {\n  $config() {\n    return this.config('card', {extends: ElementNode, slots: ['title']});\n  }\n  createDOM(): HTMLElement {\n    return document.createElement('div');\n  }\n  updateDOM(): boolean {\n    return false;\n  }\n}\n\nfunction $createCardNode(): CardNode {\n  const card = $create(CardNode);\n  // Single-line title: the bare paragraph IS the slot value. An empty\n  // paragraph is an empty field; to seed default text, append a non-empty\n  // TextNode (empty TextNodes are eliminated during reconciliation).\n  $setSlot(card, 'title', $createParagraphNode());\n  // Ordinary body child, edited like any other block.\n  return card.append($createParagraphNode());\n}\n```\n\nFor a multi-block region, use a shadow-root container as the value:\n\n```ts\nclass SlotContainerNode extends ElementNode {\n  $config() {\n    return this.config('slot-container', {extends: ElementNode});\n  }\n  createDOM(): HTMLElement {\n    return document.createElement('div');\n  }\n  updateDOM(): boolean {\n    return false;\n  }\n  isShadowRoot(): boolean {\n    return true;\n  }\n}\n\n$setSlot(\n  pullQuote,\n  'quote',\n  $create(SlotContainerNode).append(\n    $createParagraphNode().append($createTextNode('First block')),\n    $createParagraphNode().append($createTextNode('Second block')),\n  ),\n);\n```\n\nThe core API surface (all exported from `lexical`):\n\n- [`$setSlot(host, name, node)`](/docs/api/modules/lexical#setslot) — place a value into a named slot, replacing\n  any existing value under that name. Move semantics, mirroring `append`:\n  the value is detached from any current parent or slot first. Throws if the\n  value is inline or if the name is one of the reserved prototype keys\n  (`__proto__`, `constructor`, `prototype`). Slotting a node into its own\n  descendant would form a cycle; this is checked (and throws) in development\n  only — in production it behaves like appending an ancestor through the\n  children channel, which is likewise unguarded.\n- [`$getSlot(host, name)`](/docs/api/modules/lexical#getslot) — the value under a name, or `null`.\n- [`$getSlotNames(host)`](/docs/api/modules/lexical#getslotnames) — the host's slot names in canonical order.\n- [`$removeSlot(host, name)`](/docs/api/modules/lexical#removeslot) — detach the value under a name (the subtree is\n  garbage-collected unless you reattach it elsewhere).\n- [`$getSlotHost(node)`](/docs/api/modules/lexical#getslothost) — the host a value is slotted into, or `null`.\n- [`$getSlotNameWithinHost(node)`](/docs/api/modules/lexical#getslotnamewithinhost) — the name of the slot\n  a value occupies on its host (the reverse direction of `$getSlotHost`), or\n  `null` when the node is not a slot value.\n- [`$getSlotFrame(node)`](/docs/api/modules/lexical#getslotframe) — the innermost slot value containing a node (the\n  \"frame\" whose virtual shadow root scopes editing), or `null` when the node\n  is not inside any slot.\n- [`$getSelectionSlotFrame(selection)`](/docs/api/modules/lexical#getselectionslotframe) — the slot frame a\n  selection lives in, or `null` when it is outside any slot. Selection-driven\n  exporters walk this frame instead of the root's children, because a\n  selection inside a slot never contains its host and a root walk would\n  export nothing. Works for every selection type, not just `RangeSelection`.\n- [`$isSlotHost(node)`](/docs/api/modules/lexical#isslothost) /\n  [`$isSlotChild(node)`](/docs/api/modules/lexical#isslotchild) — type guards\n  for the [`SlotHostNode`](/docs/api/modules/lexical#slothostnode) /\n  [`SlotChildNode`](/docs/api/modules/lexical#slotchildnode) interfaces.\n\n## Slot Order\n\nSlot order is canonical and derived, never stored. Names declared in\n`$config()` (`slots: ['quote', 'attribution']`) sort first in declaration\norder; undeclared names sort after them, lexicographically by UTF-16 code\nunit (plain JavaScript string comparison, locale-independent). `$setSlot`\nre-canonicalizes on every write, so documents normalize on load and\nconcurrent collaborative additions converge to the same order on every\nclient. If presentation order matters, declare the names.\n\n## Rendering\n\nThe reconciler always renders every slot subtree synchronously, but into a\nhidden placeholder container — visibility is the host's explicit decision.\nThere are three ways to attach a slot, all sharing the same contract\n(attaching moves the container to the target, a no-op when it is already\nthere, and reveals it; the container renders as a normal block):\n\n1. **Synchronously in-lexical**: register a `$getSlotTargetElement`\n   [`DOMRenderMatch`](/docs/api/modules/lexical_html#domrendermatch)\n   override for the host's node class (a\n   [DOM render override](../serialization/dom-render.md), not a node\n   method — this is an advanced hook). The reconciler consults it whenever\n   it creates or reconciles the slot's container and attaches/reveals\n   within the same commit — no listener or framework hop. Returning\n   `hostDom` reveals the slot in its default slots-first position:\n\n   ```ts\n   import {domOverride, DOMRenderExtension} from '@lexical/html';\n   import {configExtension, defineExtension} from 'lexical';\n\n   export const CardExtension = defineExtension({\n     dependencies: [\n       configExtension(DOMRenderExtension, {\n         overrides: [\n           domOverride([CardNode], {\n             // Reveal the title in its default slots-first position within\n             // the same commit that renders it. Returning an element from\n             // deeper inside the host's DOM attaches it there instead;\n             // $next() defers to lower-priority overrides (default: null,\n             // a hidden placeholder).\n             $getSlotTargetElement: (node, slotName, hostDom, $next, editor) =>\n               hostDom,\n           }),\n         ],\n       }),\n     ],\n     name: 'card',\n     nodes: [CardNode],\n   });\n   ```\n\n2. **Imperatively**:\n   [`mountSlotContainer(editor, nodeKey, slotName, target)`](/docs/api/modules/lexical#mountslotcontainer)\n   and\n   [`unmountSlotContainer(editor, nodeKey, container)`](/docs/api/modules/lexical#unmountslotcontainer)\n   from `lexical` are the framework-independent primitives (e.g. from a\n   [mutation listener](/docs/api/modules/lexical#registermutationlistener),\n   which fires after the commit). `mountSlotContainer` resolves the container\n   against the committed editor state\n   ([`editor.getEditorState()`](/docs/api/modules/lexical#geteditorstate)), so\n   the model it reads matches the reconciled DOM it reveals;\n   `unmountSlotContainer` takes the container you already hold and only touches\n   the DOM:\n\n   ```ts\n   import {mountSlotContainer} from 'lexical';\n\n   // e.g. inside an extension's register(editor):\n   const unregister = editor.registerMutationListener(\n     CardNode,\n     (mutations) => {\n       for (const [nodeKey, mutation] of mutations) {\n         if (mutation === 'destroyed') {\n           continue;\n         }\n         const hostDom = editor.getElementByKey(nodeKey);\n         if (hostDom !== null) {\n           // Mounting in place: the placeholder is already parked in the\n           // host DOM, so this just reveals it in its slots-first position.\n           // Any element within the host's DOM works as a target.\n           mountSlotContainer(editor, nodeKey, 'title', hostDom);\n         }\n       }\n     },\n     {skipInitialization: false},\n   );\n   ```\n\n   `unmountSlotContainer(editor, nodeKey, container)` is the reverse: it\n   hides the container and parks it back in the host DOM, for when a mount\n   target goes away while the host remains.\n\n3. **From React chrome**: the\n   [`useLexicalSlotRef`](/docs/api/modules/lexical_react_useLexicalSlotRef#uselexicalslotref)\n   hook from `@lexical/react/useLexicalSlotRef` wraps the imperative pair and returns a\n   ref that mounts a slot's container into your component — the usual choice\n   for a DecoratorNode host's `decorate()` chrome (whose containers are\n   opted back into `contentEditable` automatically, since the decorator DOM\n   is non-editable):\n\n```tsx\nimport {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';\nimport {useLexicalSlotRef} from '@lexical/react/useLexicalSlotRef';\n\nfunction PullQuoteComponent({nodeKey}: {nodeKey: NodeKey}) {\n  const [editor] = useLexicalComposerContext();\n  const quoteRef = useLexicalSlotRef<HTMLDivElement>(editor, nodeKey, 'quote');\n  const attributionRef = useLexicalSlotRef<HTMLDivElement>(\n    editor,\n    nodeKey,\n    'attribution',\n  );\n  return (\n    <blockquote>\n      <div ref={quoteRef} />\n      <div ref={attributionRef} />\n    </blockquote>\n  );\n}\n```\n\nA `contentEditable=false` ElementNode shell can host React chrome the same\nway (slot containers opt into editing whenever the host DOM is\nnon-editable): the playground's Review demo portals chrome into the host DOM,\nattaches the author slot with `useLexicalSlotRef`, drives an interactive\nstar-rating widget that persists to NodeState, and applies the identical\nhidden-then-attach technique to its `getDOMSlot` children element. Such a shell should call\n[`setDOMUnmanaged(dom)`](/docs/api/modules/lexical#setdomunmanaged) in\n`createDOM` — the portal and the\nattach moves mutate the shell's children from outside the reconciler, and\nthe marker gives the shell the same mutation-observer exemption a\nDecoratorNode's DOM has.\n\n## Editable state\n\nA slot rendered inside a non-editable host (a DecoratorNode, or a\n`contentEditable=false` element shell) would not track the editor's editable\nstate on its own, so the reconciler gives its container an explicit\n`contentEditable` that follows `editor.isEditable()`, and re-renders the islands\nwhenever `setEditable` toggles — so a read-only editor's slots are not left\neditable. No extension is required. Slots always follow the editor; there is\ncurrently no way for a slot to override its own editable state.\n\nA host that attaches its own editable island which is *not* a slot container —\nfor example the Review demo's `getDOMSlot` children element inside its\n`contentEditable=false` shell — opts it into the same behavior with\n[`$markSlotEditable(element, editor)`](/docs/api/modules/lexical#marksloteditable),\nre-applying it from `updateDOM` so an editable toggle reaches the island.\n\n## Editing Behavior\n\n- **Selection never crosses a slot boundary.** Selections are clamped to the\n  anchor's slot frame at every entry point (DOM resolution,\n  [`$setSelection`](/docs/api/modules/lexical#setselection),\n  and point mutation), so a mouse drag and a `shift+arrow` across the\n  boundary land on the same clamped result. A focus in a slot of a host is\n  moved past the whole host, or to the edge of the host's own children when\n  the selection starts inside the host.\n- **Deletion stops at the boundary.** Backspace at the start of a slot and\n  forward-Delete at its end are no-ops instead of merging across the virtual\n  shadow root.\n- **`Cmd+A` scopes to the slot frame** when the caret is inside one; outside\n  slots the default handlers keep the legacy whole-document behavior.\n  Progressive expansion (block → enclosing slot frame → document on repeated\n  presses) is provided by the opt-in\n  [`SelectBlockExtension`](/docs/api/modules/lexical_extension#selectblockextension)\n  from `@lexical/extension`.\n- **Generic block conversion skips slot values.** A slot value has no\n  parent — its up-link is `$getSlotHost` — so selection-driven block\n  converters (`$setBlocksType`, `$wrapNodes`, `$insertList` / `$removeList`,\n  the markdown block shortcuts) treat it as ineligible and do nothing rather\n  than replace it: the slot assignment is managed by the node or extension\n  that owns the slot. Calling `LexicalNode.replace` directly on a slot value\n  throws; reassign the slot with `$setSlot` on its host instead.\n- **Slots stay bound to their host through `replace`.** Replacing a slot\n  *host* (`host.replace(other)`) does not transfer its slots to the\n  replacement — slots are not necessarily portable across node types, so the\n  slot map travels with the node, never with its position. If the replaced\n  host is reattached in the same update (the\n  [`$wrapNodeInElement`](/docs/api/modules/lexical_utils#wrapnodeinelement)\n  pattern) it keeps its slots; if it stays detached, its slot subtrees are\n  garbage-collected with it. The same applies to anything built on `replace`:\n  converting a slot host with `$setBlocksType` produces a slot-less\n  replacement block. Move a slot onto another host explicitly with\n  `$setSlot`.\n- **A NodeSelection of an element carries its children.** Copy and export\n  of a whole-host NodeSelection (e.g. a chrome click that selects \"the whole\n  Card\") include the host's body children even though they aren't in the\n  selection themselves — the old shell-only output made cut silently lossy.\n  This applies only to NodeSelection; a partial RangeSelection over the host\n  keeps per-child slicing.\n\n### Traversal is intentionally asymmetric\n\nContent reads include slot subtrees, slots-first:\n[`getTextContent()`](/docs/api/modules/lexical#gettextcontent),\n[`getAllTextNodes()`](/docs/api/modules/lexical#getalltextnodes),\nand the [`$dfsWithSlots`](/docs/api/modules/lexical_utils#dfswithslots)\nfamily in `@lexical/utils` count slot content for search, copy, and\naccessibility. Navigation excludes them:\n[`getChildren()`](/docs/api/modules/lexical#getchildren),\n[`getFirstDescendant()`](/docs/api/modules/lexical#getfirstdescendant)\nand friends stay linked-list-only, so caret movement never walks into a slot\nby accident. Choose [`$dfs`](/docs/api/modules/lexical_utils#dfs) or\n[`$dfsWithSlots`](/docs/api/modules/lexical_utils#dfswithslots) depending on whether \"this subtree\" should mean the\nnavigable tree or all content.\n\n## Serialization\n\nJSON serialization is automatic in both directions. A host's slots serialize\nunder the reserved `$slots` key on\n[`SerializedLexicalNode`](/docs/api/modules/lexical#serializedlexicalnode)\n(a sibling of\nNodeState's reserved `'$'` key), keyed by slot name:\n\n```json\n{\n  \"type\": \"card\",\n  \"version\": 1,\n  \"$slots\": {\n    \"title\": {\"type\": \"paragraph\", \"children\": [], \"version\": 1}\n  },\n  \"children\": []\n}\n```\n\nParsing re-attaches each subtree with `$setSlot` and throws if `$slots`\nappears on a node that cannot host slots. The `$` prefix keeps the\nframework-owned key from colliding with a subclass that already serializes a\n`slots` property of its own.\n\nHTML serialization is opt-in per host, like NodeState: the exporter never\ndescends into slots on its own. A host's `exportDOM` can emit each slot into\na wrapper using\n[`$appendNodeToHTML`](/docs/api/modules/lexical_html#appendnodetohtml) from\n`@lexical/html`, and a\n[DOM import rule](../serialization/dom-import.md) on the host's\ndistinguishing markup maps the wrappers back through `$setSlot`.\n\n## Collaboration\n\nSlots sync on both the V1 and V2 Yjs bindings through a per-slot-diffed\n`Y.Map` stored under the reserved `__slots` attribute key on the host's shared\ntype (the channel reuses the host's `__slots` field name, which is already\nexcluded from the property sync). Hosts with declared slots create that map\neagerly, so two clients\nconcurrently setting *different* slot names for the first time merge\nper-entry instead of racing. Malformed or hostile remote entries are\nvalidated and skipped.\n\n:::caution\n\nPre-slots clients receiving slot data from upgraded peers will error rather\nthan render it. Enabling slots in long-lived shared documents should be\ngated on all participants running a slots-aware version; new clients\ntolerate unknown slot data going forward.\n\n:::\n\n## Reserved Names\n\nAdding slots reserves a few identifiers that custom node subclasses should\nnot define for their own purposes:\n\n- the `__slots` and `__slotHost` fields on ElementNode / DecoratorNode — the\n  `__slots` name is also the collab attribute key for the slots channel, so it\n  is reserved on the Yjs shared type too;\n- the `$slots` serialized JSON key.\n\n## Current Limitations\n\n- The caret / NodeCaret APIs throw across a slot boundary (\"no common\n  ancestor\"); slot-aware caret traversal is a planned follow-up.\n- Nested slots (a slot whose host is itself slotted) are only handled one\n  level deep by the runtime selection comparator.\n- The mixed-version collab caveat above.\n","packages/lexical-website/docs/concepts/node-cloning.md":"# Node Cloning\n\nUnderstanding how to properly clone and modify nodes is crucial for working with Lexical. This guide explains the different cloning mechanisms and when to use each one.\n\n## Types of Node Cloning\n\nLexical provides several ways to clone nodes, each serving a different purpose:\n\n1. `static clone()` - Internal API for state management\n2. `$copyNode()` - Public API for creating new nodes\n3. `getWritable()` - Public API for node modifications\n\n## The `clone` Method\n\n### What is `clone`?\n\nThe `clone` method is a static method that creates the next version of a node. A node that declares [`$config()`](./nodes.mdx) has one synthesized for it and need not implement it — a hand-written `clone` is only required for a node that does not use `$config()`, or whose constructor has required arguments. Either way, it's important to understand that this is an **internal API** used by Lexical's state management system.\n\n```typescript\nclass MyCustomNode extends ElementNode {\n  static clone(node: MyCustomNode): MyCustomNode {\n    // ✅ Correct implementation\n    return new MyCustomNode(node.__someData, node.__key);\n  }\n}\n```\n\n### When is `clone` Used?\n\n1. **Internal State Management**\n   - Used by Lexical internally\n   - Part of the editor's state update system\n   - Called by `getWritable()`\n\n2. **Node Mutations**\n   ```typescript\n   // Example of a node method implementation\n   class MyCustomNode extends ElementNode {\n     setData(data: string): this {\n       const self = this.getWritable();\n       self.__data = data;\n       return self;\n     }\n   }\n\n   node.setData(\"new data\");\n   ```\n\n### When NOT to Use `clone`?\n\n```typescript\n// ❌ Never do this\nfunction $duplicateNode(node: MyCustomNode) {\n  return MyCustomNode.clone(node); // Don't call clone directly\n}\n```\n\n## The `afterCloneFrom` Method\n\n### What is `afterCloneFrom`?\n\n`afterCloneFrom(prevNode)` runs on every clone, right after it is constructed,\nand copies over whatever the constructor did not. The base implementation\ncarries the node's links to its parent and siblings and its\n[NodeState](./node-state.md), and `ElementNode` and `TextNode` carry their own\nbuilt-in properties such as format, style, and indent. A property that is\ncopied neither by the constructor nor by some `afterCloneFrom` silently\nreverts to its default on the node's next write.\n\n### When you don't need it\n\nWith the modern APIs you usually don't write one at all:\n\n- **Serialization schema.** A property declared as a field in a `$config`\n  [`json` schema](./nodes.mdx#creating-custom-nodes-with-a-serialization-schema)\n  (`withField(stringValue(), {field: '__label'})`) is carried across the\n  clone for you, because the schema says where it is stored. Available in\n  Lexical v0.51.0 and later.\n- **NodeState.** State created with `createState` and stored with `$setState`\n  is carried by the base `afterCloneFrom`, so a node whose extra data lives\n  only in NodeState needs nothing.\n\n```ts\n// At module scope, above the class, so TypeScript checks it against the class\nconst calloutSchema = nodeSchema<CalloutNode>()({\n  label: withField(stringValue(), {field: '__label'}),\n});\n\nclass CalloutNode extends ElementNode {\n  __label: string = '';\n\n  // No afterCloneFrom: `__label` is declared as a field in calloutSchema, so\n  // it is carried across every clone.\n  $config() {\n    return this.config('callout', {extends: ElementNode, json: calloutSchema});\n  }\n}\n```\n\n### When you still need it\n\nOverride `afterCloneFrom` for a property that neither of those covers: a\nfield that isn't in the node's schema, one declared only through accessor\nmethods (see\n[Carrying properties across a clone](../serialization/serialization.md#carrying-properties-across-a-clone)),\nor any property of a node written without a schema. Always call\n`super.afterCloneFrom(prevNode)` first so the inherited properties and\nNodeState are still carried:\n\n```ts\nclass MyCustomNode extends ElementNode {\n  __data: string = '';\n\n  afterCloneFrom(prevNode: this): void {\n    super.afterCloneFrom(prevNode);\n    this.__data = prevNode.__data;\n  }\n}\n```\n\nLike `clone`, `afterCloneFrom` is called by Lexical; don't call it yourself\nexcept through `super`.\n\n## Using `$copyNode`\n\n### What is `$copyNode`?\n\n`$copyNode` is the public API for creating a copy of a node with a new key. Use this when you need to create a duplicate node.\nBy default, all properties and `NodeState` will be copied to the new node, and then `resetOnCopyNodeFrom` will be called to\nallow the node to optionally reset certain properties (and NodeState configured with `resetOnCopyNode: true`) to defaults\n(such as the checked state of a `ListItemNode`).\n\n```typescript\n// ✅ Correct: Using $copyNode\nconst copy = $copyNode(existingNode);\n```\n\n### When to Use `$copyNode`?\n\n1. **Creating Duplicates**\n   ```typescript\n   // ✅ Correct: Duplicating a node\n   const duplicate = $copyNode(originalNode);\n   someParent.append(duplicate);\n   ```\n\n## Using `getWritable`\n\n### What is `getWritable`?\n\n`getWritable` is an internal API used within node method implementations to get a mutable version of a node. Node consumers should use the node's public methods instead.\n\n```typescript\n// ✅ Correct: Implementation of a node method\nclass MyCustomNode extends ElementNode {\n  setData(data: string): this {\n    const self = this.getWritable();\n    self.__data = data;\n    return self;\n  }\n}\n\n// ✅ Correct: Using the node\nconst node = new MyCustomNode();\nnode.setData(\"new value\");\n```\n\n## Common Patterns\n\n### Modifying Nodes\n\n```typescript\n// ✅ Correct: Modifying a node\nfunction $updateNodeData(node: MyCustomNode, newData: string): MyCustomNode {\n  return node.setData(newData);\n}\n\n// ❌ Incorrect: Don't clone manually\nfunction $updateNodeDataWrong(node: MyCustomNode, newData: string): MyCustomNode {\n  const clone = MyCustomNode.clone(node);\n  clone.setData(newData);\n  return clone;\n}\n```\n\n### Copying Nodes\n\n```typescript\n// ✅ Correct: Copying a node\nfunction $duplicateNode(node: MyCustomNode): MyCustomNode {\n  return $copyNode(node);\n}\n\n// ❌ Incorrect: Don't use clone\nfunction $duplicateNodeWrong(node: MyCustomNode): MyCustomNode {\n  return node.constructor.clone(node);\n}\n```\n\n## Performance Considerations\n\n1. **Referential Integrity**\n   - `getWritable()` ensures proper EditorState updates with new clones\n   - Prevents \"orphaned\" nodes that won't be rendered\n\n2. **State Management**\n   ```typescript\n   class MyCustomNode extends ElementNode {\n     setData(data: string): this {\n       const self = this.getWritable();\n       self.__data = data; // Properly tracked by editor\n       return self;\n     }\n   }\n   ```\n\n## Testing\n\n```typescript\ntest('node modification', async () => {\n  await editor.update(() => {\n    const node = new MyCustomNode(\"test\");\n    \n    node.setData(\"new data\");\n    \n    // ✅ Correct: Use $copyNode for duplication\n    const copy = $copyNode(node);\n  });\n});\n```\n\n## Related Concepts\n\n- [Editor State](editor-state.md) - How cloning affects editor state\n- [Nodes](nodes.mdx) - Core concepts about Lexical nodes\n\n## Common Questions\n\n**Q: How do I duplicate a node?**\nA: Use `$copyNode(node)` to create a new copy with a new key.\n\n**Q: When should I use clone?**\nA: Never directly. Use `$copyNode()` or `getWritable()` instead. ","packages/lexical-website/docs/concepts/node-replacement.md":"# Node Replacement\n\nNode Replacement allow you to replace all instances of a given node in your editor with instances of a subclass.\n\n## Use Case\n\n:::note\n\nIn earlier versions of this documentation, \"Node Replacement\" was called \"Node Overrides\".\nWe've changed the name to match the terms used in the implementation.\n\n:::\n\n:::tip\n\nIf your use case only requires adding ad-hoc data to existing nodes, you may be able to use the [NodeState](node-state.md) API instead of subclassing and node replacement.\n\n:::\n\nSome of the most commonly used Lexical Nodes are owned and maintained by the core library. For example, ParagraphNode, HeadingNode, QuoteNode, List(Item)Node etc - these are all provided by Lexical packages, which provides an easier out-of-the-box experience for some editor features, but makes it difficult to override their behavior. For instance, if you wanted to change the behavior of ListNode, you would typically extend the class and override the methods. However, how would you tell Lexical to use *your* ListNode subclass in `ListExtension` instead of using the core ListNode? That's where Node Replacement can help.\n\n\nNode Replacement allow you to replace all instances of a given node in your editor with instances of a different node class. This can be done through the `nodes` of an extension:\n\n```ts\nconst CustomParagraphExtension = defineExtension({\n  name: '@my-app/CustomParagraph',\n  nodes: () => [\n    // Don't forget to register your custom node separately!\n    CustomParagraphNode,\n    {\n      replace: ParagraphNode,\n      with: (node: ParagraphNode) => {\n        return $createCustomParagraphNode();\n      },\n      withKlass: CustomParagraphNode,\n    },\n  ],\n});\n```\nIn the snippet above,\n- `replace`: Specifies the core node type to be replaced. \n- `with`: Defines a transformation function to replace instances of the original node to the custom node.  \n- `withKlass`: This option ensures that behaviors associated with the original node type work seamlessly with the replacement. For instance, node transforms or mutation listeners targeting ParagraphNode will also apply to CustomParagraphNode when withKlass is specified. Without this option, the custom node might not fully integrate with the editor's built-in features, leading to unexpected behavior.\n\nOnce this is done, Lexical will replace all ParagraphNode instances with CustomParagraphNode instances. One important use case for this feature is overriding the serialization behavior of core nodes. Check out the full example below.\n\n## Node Replacement Example\n\nThis example demonstrates using Node Replacement to replace all `ParagraphNode` with a `CustomParagraphNode` that overrides `createDOM`.\n\n[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/facebook/lexical/tree/main/examples/node-replacement?file=src/main.tsx)\n\n<iframe width=\"100%\" height=\"600\" src=\"https://stackblitz.com/github/facebook/lexical/tree/main/examples/node-replacement?embed=1&file=src%2FApp.tsx&terminalHeight=0&ctl=1&showSidebar=0&devtoolsheight=0&view=preview\" sandbox=\"allow-forms allow-modals allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin allow-scripts\" title=\"Node Replacement Example\"></iframe>\n","packages/lexical-website/docs/concepts/node-state.md":"# NodeState\n\nThe NodeState API introduced in v0.26.0 allows arbitrary state to be added\nad-hoc to any node in a way that participates with reconciliation, history,\nand JSON serialization.\n\n## Use Case\n\nNodeState allows your application to define keys that can be stored on\nany node with automatic JSON support.\n\n:::tip\n\nYou can even add node state to the RootNode to store document-level metadata.\n\n:::\n\nWith a combination of NodeState and other APIs such as\n[Listeners](listeners.md) or\n[Transforms](transforms.md) you can\nlikely shape the editor to meet your needs without having to do much\n[Node Customization](node-replacement.md).\n\nEven when you are subclassing nodes, using NodeState instead of additional\nproperties to store the node's data can be [more efficient](#efficiency)\nand will save you from writing a lot of boilerplate in the constructor,\nupdateFromJSON, and exportJSON.\n\n## Usage\n\n### createState\n\n[createState](/docs/api/modules/lexical#createstate)\ncreates a\n[StateConfig](/docs/api/modules/lexical#stateconfig)\nwhich defines the key and configuration for your NodeState value.\n\nThe key must be locally unique, two distinct StateConfig must not have the\nsame string key if they are to be used on the same node.\n\nTypical usage will look something like this:\n\n```ts\nconst questionState = createState('question', {\n  parse: (v) => (typeof v === 'string' ? v : ''),\n});\n```\n\nThe required `parse` function serves two purposes:\n- It provides a type-safe and runtime-safe way to parse values that were\n  serialized to JSON\n- When called with `undefined` (or any invalid value) it should return some\n  default value (which may be `undefined` or `null` or any other value you\n  choose)\n\nIn this case, the question must be a string, and the default is an\nempty string.\n\nSee the\n[createState](/docs/api/modules/lexical#createstate)\nAPI documentation for more details, there are other optional settings\nthat you may want to define particularly if the value is not a primitive\nvalue such as boolean, string, number, null, or undefined.\n\n:::tip\n\nInstead of writing parse functions by hand, use the\n[serialization schema combinators](/docs/serialization/#declarative-serialization-schemas-with-config)\nfrom `lexical`, such as `stringValue`, `numberValue`, `enumValue`,\n`arrayValue`, `objectValue`, and `nullable`. They are the same ones a node's\n`$config` JSON schema uses. A schema is itself a parse function, so it can be\npassed directly as `parse`:\n\n```ts\nimport {arrayValue, createState, stringValue} from 'lexical';\n\nconst questionState = createState('question', {parse: stringValue()});\nconst tagsState = createState('tags', {parse: arrayValue(stringValue())});\n```\n\nBecause `parse` is just a function, the combinators also compose with schema\nlibraries such as [zod](https://zod.dev/), [ArkType](https://arktype.io/),\n[Effect](https://effect.website/), and [Valibot](https://valibot.dev/). Wrap a\nlibrary's validator in a parse function that returns a default for invalid\ninput, or use `transformValue` to refine what a combinator parsed.\n\n:::\n\n### $getState\n\n[$getState](/docs/api/modules/lexical#getstate) gets the\nNodeState value from the given node, or the default if that key was never\nset on the node.\n\n```ts\nconst question = $getState(pollNode, questionState);\n```\n\nSee also\n[$getStateChange](/docs/api/modules/lexical#getstatechange)\nif you need an efficient way to determine if the state has changed on two\nversions of the same node (typically used in updateDOM, but may be useful in\nan update listener or mutation listener).\n\n### $setState\n\n[$setState](/docs/api/modules/lexical#setstate) sets the\nNodeState value on the given node.\n\n```ts\nconst question = $setState(\n  pollNode,\n  questionState,\n  'Are you planning to use NodeState?',\n);\n```\n\n:::tip\n\nThe last argument is a ValueOrUpdater, just like with React's useState\nsetters. If you use an updater function and the value does not change,\nthe node and its NodeState *won't* be marked dirty.\n\n:::\n\n## Serialization\n\nThe NodeState for a node, if any values are set to non-default values, is\nserialized to a record under a single\n[NODE_STATE_KEY](/docs/api/modules/lexical#node_state_key)\nwhich is equal to `'$'`.\n\n```json\n{\n  \"type\": \"poll\",\n  \"$\": {\n    \"question\": \"Are you planning to use NodeState?\"\n  }\n}\n```\n\n:::tip\n\nBy default, it is assumed that your parsed values are JSON serializable,\nbut for advanced use cases you may use values such as Date, Map, or Set\nthat need to be transformed before JSON serialization. See the\n[StateValueConfig](/docs/api/modules/lexical#statevalueconfig)\nAPI documentation.\n\n:::\n\n### Flat serialization with `$config`\n\nNodes that declare a `StateConfig` in [`$config`](nodes.mdx#creating-custom-nodes-with-config-and-nodestate)\nwith `flat: true` lift that key to the top level of the serialized node\ninstead of nesting it under `'$'`. This makes the JSON shape identical\nto a legacy node that stored the value as a `__property` instance\nvariable, so existing payloads continue to round-trip after the node is\nmigrated to NodeState.\n\nFor example, a `ColoredNode` that extends `TextNode` and declares a flat\n`color` state (see [Extending TextNode with `$config`](nodes.mdx#extending-textnode-with-config)):\n\n```ts\n$config() {\n  return this.config('colored', {\n    extends: TextNode,\n    stateConfigs: [{flat: true, stateConfig: colorState}],\n  });\n}\n```\n\nserializes as:\n\n```json\n{\n  \"type\": \"colored\",\n  \"text\": \"hello\",\n  \"color\": \"red\"\n}\n```\n\nA non-flat (default) state on the same node would instead appear under\n`'$'`:\n\n```json\n{\n  \"type\": \"colored\",\n  \"text\": \"hello\",\n  \"$\": {\n    \"color\": \"red\"\n  }\n}\n```\n\nIn both cases, a key is only emitted when the current value is not equal\nto the default returned by its `parse` function — see\n[Efficiency](#efficiency).\n\n:::note\n\nDon't reuse a flat key that a superclass already serializes (e.g. `text`\non `TextNode`).\n\n:::\n\n### Upgrading a legacy JSON property to NodeState\n\nA node that stores data as a `__property` instance variable with custom\n`exportJSON`/`importJSON`/`updateFromJSON` can be migrated to NodeState\nwith `flat: true` without changing its serialized JSON shape.\n\nBefore — `ColoredNode` with a `__color` property and hand-written\nserialization (see the full legacy example in\n[Extending TextNode with `$config`](nodes.mdx#extending-textnode-with-config)):\n\n```ts\nexport type SerializedColoredNode = Spread<\n  {color?: string},\n  SerializedTextNode\n>;\n\nexport class ColoredNode extends TextNode {\n  __color: string;\n\n  constructor(text: string = '', color: string = DEFAULT_COLOR, key?: NodeKey) {\n    super(text, key);\n    this.__color = color;\n  }\n\n  static getType(): string {\n    return 'colored';\n  }\n\n  static clone(node: ColoredNode): ColoredNode {\n    return new ColoredNode(node.__text, node.__color, node.__key);\n  }\n\n  static importJSON(serializedNode: SerializedColoredNode) {\n    return new ColoredNode().updateFromJSON(serializedNode);\n  }\n\n  updateFromJSON(serializedNode: SerializedColoredNode) {\n    const self = super.updateFromJSON(serializedNode);\n    self.__color =\n      typeof serializedNode.color === 'string'\n        ? serializedNode.color\n        : DEFAULT_COLOR;\n    return self;\n  }\n\n  exportJSON(): SerializedColoredNode {\n    return {\n      ...super.exportJSON(),\n      color: this.__color === DEFAULT_COLOR ? undefined : this.__color,\n    };\n  }\n}\n```\n\nAfter — same on-the-wire JSON, no hand-written serialization:\n\n```ts\nconst colorState = createState('color', {\n  parse: (v) => (typeof v === 'string' ? v : DEFAULT_COLOR),\n});\n\nexport class ColoredNode extends TextNode {\n  $config() {\n    return this.config('colored', {\n      extends: TextNode,\n      stateConfigs: [{flat: true, stateConfig: colorState}],\n    });\n  }\n}\n```\n\nNo `exportJSON`, `importJSON`, `updateFromJSON`, `clone`, or\n`afterCloneFrom` override is needed: `$config` installs `clone` and\n`importJSON`, and the base `exportJSON`/`updateFromJSON`/`afterCloneFrom`\non `LexicalNode`/`TextNode` already round-trip NodeState. Read the color\nvia `$getState(node, colorState)` and write it via\n`$setState(node, colorState, value)` instead of `node.__color`.\n\n:::tip\n\nFor a seamless migration, keep the JSON key name the same — for a flat\nstate, that's the first argument to `createState`. Migrating from a\nnon-flat NodeState to a flat NodeState also works: state that appears\nunder the `$` (`NODE_STATE_KEY`) is still parsed even when the state is\nconfigured as flat, and the flat value takes precedence when both are\npresent.\n\n:::\n\n## Efficiency\n\nNodeState uses a copy-on-write scheme to manage each node's state. If\nnone of the state has changed, then the NodeState instance will be\nshared across multiple versions of that node.\n\n:::info\n\nIn a given reconciliation cycle, the first time a Lexical node is marked dirty\nvia `getWritable` will create a new instance of that node. All properties\nof the previous version are set on the new instance. NodeState is stored\nas a single property, and no copying of the internal state is done\nuntil the NodeState itself is marked writable.\n\n:::\n\nWhen serializing to JSON, each key will only be stored if the value\nis not equal to the default value. This can save quite a lot of space\nand bandwidth.\n\nParsing and serialization is only done at network boundaries, when\nintegrating with JSON or Yjs. When a value changes from an external\nsource, it is only parsed once the first time it is read.\nValues that do not come from external sources are not parsed, and\nvalues that are not used are never parsed.\n\n## Capabilities\n\nCurrent:\n\n- Allows you to define and add state to any node\n- Serializes that state automatically in the node's JSON, supporting\n  versioning and copy+paste\n- Works with the reconciler, TextNodes with differing state will not\n  be implicitly merged\n- @lexical/yjs support, NodeState will be automatically synchronized\n  like any other property\n- NodeState values that are not used will simply pass-through, making\n  it a bit easier for situations where multiple configurations are used\n  on the same data (e.g. older and newer versions of your editor,\n  a different set of plugins based on context, etc.).\n- Pre-registration system for nodes to declare expected state\n  and serialize them as top-level properties (`flat`) with `$config`\n  (see [#7260](https://github.com/facebook/lexical/issues/7260)).\n- Can be integrated with\n  [DOMRenderExtension](../serialization/dom-render.md) for editor\n  rendering and HTML export\n- Can be integrated with\n  [DOMImportExtension](../serialization/dom-import.md) for HTML import\n\nFuture:\n\n- Does not yet support direct integration with Yjs, e.g.\n  you can not store a Y.Map as a NodeState value\n  (see [#7293](https://github.com/facebook/lexical/issues/7293))\n\n## Node State Style Example\n\nThis example demonstrates an advanced use case of storing a style object on TextNode using NodeState.\n\n[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/facebook/lexical/tree/main/examples/node-state-style?file=src/main.tsx)\n\n<iframe width=\"100%\" height=\"600\" src=\"https://stackblitz.com/github/facebook/lexical/tree/main/examples/node-state-style?embed=1&file=src%2FApp.tsx&terminalHeight=0&ctl=1&showSidebar=0&devtoolsheight=0&view=preview\" sandbox=\"allow-forms allow-modals allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin allow-scripts\" title=\"Node State Style Example\"></iframe>\n","packages/lexical-website/docs/concepts/one-lexical-per-app.md":"---\nsidebar_label: One Lexical per app\n---\n\n# One Lexical per app\n\n`lexical` is effectively a singleton **within one app**: an app must resolve\nexactly **one** copy of `lexical`, and one copy of each `@lexical/*` package it\nuses. Two copies in the same bundle do not merely waste bytes — they break the\neditor in ways whose error messages point nowhere near the real cause.\n\nThe practical consequence, covered in [Publishing a library](#publishing-a-library)\nbelow: **a library built on Lexical declares `lexical` and the `@lexical/*`\npackages it imports as `peerDependencies`, never as `dependencies`. The\napplication brings its own Lexical.**\n\n:::note\n\nThis page is about npm `peerDependencies` in your `package.json`. Extensions\nhave their own, unrelated notion of\n[peer dependencies](../extensions/peer-dependencies.md) — optional links between\nextensions inside one editor.\n\n:::\n\n## The boundary is the app, not the page\n\nSeveral self-contained apps can share a page, each with its own bundled copy of\nLexical, and that is fine — a CMS page with two independently deployed widgets\non it, or a micro-frontend setup, does not have a bug just because two copies of\n`lexical` are loaded. Lexical anticipates this: `isLexicalEditor()` is an\n`instanceof` check specifically \"to prevent issues with multiple embedded\nLexical installations\", and an editor is tagged on its own root element rather\nthan in any page-global registry, so each app's editors resolve to that app's\nLexical and ignore the others.\n\nWhat those apps cannot do is interoperate. Two copies have no compatible API\nsurface between them, so treat each as a sealed island:\n\n- do not pass nodes, editors, `EditorState` objects, selections or commands\n  across the boundary — pass serialized JSON or HTML, which is data rather than\n  identity;\n- a plugin, node package or shared editor config must be bundled with the app it\n  serves, not shared as a live import between apps;\n- `@lexical/react` context does not cross the boundary either, since the context\n  object itself comes from one copy.\n\nThe rest of this page is about the failure case: two copies **inside one app**,\nwhere code from copy A is expected to work with an editor from copy B.\n\n## Why one copy per app\n\nLexical keeps state at module scope, and identifies things by reference rather\nthan by name. Both assumptions hold only within a single copy of the module.\n\n- **The active editor is module state.** `activeEditor`, `activeEditorState`\n  and `isReadOnlyMode` live at module scope in `LexicalUpdates.ts`, and every\n  `$` function reads them. `editor.update()` in copy A sets copy A's variables;\n  a `$` function imported from copy B reads copy B's, which are still `null`\n  ([Editor State](editor-state.md)).\n- **Node classes are compared by identity.** Node registration and the\n  `instanceof LexicalNode` checks compare class references. A node that extends\n  `LexicalNode` from copy A is not, as far as copy B is concerned, a\n  `LexicalNode` at all — even though both classes have the same name and come\n  from the same published version ([Nodes](nodes.mdx)).\n- **Commands are object identities.** `createCommand()` returns a fresh `{type}`\n  object, and `registerCommand`/`dispatchCommand` match on that object, not on\n  its `type` string. Two copies of `lexical` export two distinct\n  `FORMAT_TEXT_COMMAND` objects, so a dispatch from one never reaches a listener\n  registered with the other ([Commands](commands.md)).\n\nOn top of that, two copies are usually two *versions*, which is API drift inside\na single editor: half your plugins were compiled against a different API and\nserialization format than the editor running them, and the mismatch surfaces\nonly at runtime, on the paths that happen to differ.\n\n## Symptoms of a duplicated Lexical\n\nThe errors are unhelpfully literal — a class that plainly does implement\n`getType()` is reported as not implementing it, because the check is running\nagainst the other copy:\n\n```\nLexicalNode: Node FooNode does not implement .getType().\n\nCreate node: Type heading in node HeadingNode does not match registered node\nHeadingNode with the same type\n\ncreateEditor: nodes[0] FooNode (type foo) is not a constructor that subclasses\nLexicalNode from the lexical package used by this editor (0.50.0)\n\nUnable to find an active editor state. State helpers or node methods can only be\nused synchronously during the callback of editor.update(), editor.read(), or\neditorState.read(). Detected on the page: 0 compatible editor(s) with version\n0.50.0 and incompatible editors with versions 0.49.0\n```\n\nThat last sentence is Lexical telling you directly that it found editors built\nfrom another copy — a version different from its own, or the same version marked\n`(separately built, likely a bundler configuration issue)`. Note that this\ndiagnostic scans the whole page, so a genuinely separate app that ships its own\nLexical is counted there too; it is evidence of a bug only when those editors\nwere supposed to be part of *your* app.\n\nSilent failures look like: a command that dispatches but never fires its\nlistener, `$getSelection()` returning `null` inside an update, or a custom node\nrendering as plain text after a copy/paste. Real reports of this failure mode:\n[#4225](https://github.com/facebook/lexical/issues/4225) and\n[#7819](https://github.com/facebook/lexical/issues/7819).\n\n## Publishing a library\n\nIf you publish a package that imports from `lexical` or `@lexical/*` — a plugin,\na node package, a shared editor config, an in-house wrapper in a monorepo — put\nthose packages in `peerDependencies`, and in `devDependencies` so you can still\nbuild and test:\n\n```json\n{\n  \"name\": \"my-lexical-plugin\",\n  \"peerDependencies\": {\n    \"lexical\": \">=0.50.0\",\n    \"@lexical/react\": \">=0.50.0\",\n    \"@lexical/utils\": \">=0.50.0\"\n  },\n  \"devDependencies\": {\n    \"lexical\": \"0.50.0\",\n    \"@lexical/react\": \"0.50.0\",\n    \"@lexical/utils\": \"0.50.0\"\n  }\n}\n```\n\nNotes on the ranges:\n\n- Lexical is pre-1.0, so `^0.50.0` means `>=0.50.0 <0.51.0` — a caret peer range\n  locks your users out of the next release. Prefer an open `>=` range (or an\n  explicit `>=0.50.0 <1.0.0`) so an app can upgrade Lexical without waiting for\n  you.\n- Mark a peer optional with\n  [`peerDependenciesMeta`](https://docs.npmjs.com/cli/configuring-npm/package-json#peerdependenciesmeta)\n  when the import is behind an opt-in entry point, the way this repo's own\n  packages mark `typescript` optional.\n- Do not add `lexical` to `dependencies` \"just so it installs\". That is exactly\n  what pins a second copy under `node_modules/your-lib/node_modules/lexical` as\n  soon as your range and the app's range disagree.\n\n## Application setup\n\nThe application owns the version, and every Lexical package it installs moves\ntogether:\n\n- Install `lexical` and each `@lexical/*` package you use **at the same\n  version**. Published `@lexical/*` packages depend on the exact matching\n  `lexical` version (the monorepo's `workspace:*` is rewritten to an exact\n  version at publish time), so `@lexical/react@0.50.0` beside `lexical@0.49.0`\n  guarantees a nested second copy.\n- Verify there is only one **per app**, from the app root:\n\n  ```sh\n  npm ls lexical @lexical/react     # or: pnpm why lexical / yarn why lexical\n  ```\n\n  More than one resolved version, or a path with a nested `node_modules/lexical`\n  in it, is the bug.\n- In a monorepo or a linked-package setup (`npm link`, `pnpm link`, a Yalc\n  copy), the linked library resolves Lexical from *its own* `node_modules` and\n  duplicates are the default rather than the exception. Deduplicate explicitly:\n  hoist the dependency to the workspace root, or force resolution with\n  [pnpm `overrides`](https://pnpm.io/settings#overrides), Yarn `resolutions`, or\n  `npm dedupe`.\n- Bundlers can also be told to collapse duplicates — Vite\n  [`resolve.dedupe`](https://vite.dev/config/shared-options.html#resolve-dedupe),\n  webpack `resolve.alias` pointing each package at one path:\n\n  ```js\n  // vite.config.js\n  export default {\n    resolve: {dedupe: ['lexical', '@lexical/react', '@lexical/utils']},\n  };\n  ```\n\n  Treat that as a safety net, not a fix: it collapses copies that are already\n  version-compatible, and cannot rescue two genuinely different versions.\n- Duplication does not always come from the dependency tree: importing Lexical\n  through both `import` and `require` pulls in the ESM and the CJS build of the\n  *same* version, which are still two copies. The\n  [React FAQ](../react/faq.md#lexicalcomposercontextuselexicalcomposercontext-cannot-find-a-lexicalcomposercontext)\n  covers how this one shows up in `@lexical/react`.\n","packages/lexical-website/docs/concepts/read-only.md":"\n\n# Read Mode / Edit Mode\n\nLexical supports two modes:\n\n- Read mode\n- Edit mode\n\nThe default behavior for Lexical is edit mode, or more accurately not read only mode. Under-the-hood, the main\nimplementation detail is that the `contentEditable` is being set to `\"false\"` or `\"true\"` depending on the mode.\nSpecific plugins can listen to the mode change too – allowing them to customize parts of the UI depending on the\nmode.\n\n## Setting the mode\n\nIn order to set the mode, this can be done on creation of the editor, with the\n`editable` property of your app's extension:\n\n```js\nimport {buildEditorFromExtensions} from '@lexical/extension';\nimport {RichTextExtension} from '@lexical/rich-text';\nimport {defineExtension} from 'lexical';\n\nconst editor = buildEditorFromExtensions(\n  defineExtension({\n    dependencies: [RichTextExtension],\n    editable: false,\n    name: '@my-app/editor',\n  }),\n);\n```\n\nWith React, pass the same extension to `<LexicalExtensionComposer>`:\n\n```jsx\n<LexicalExtensionComposer extension={appExtension}>\n  ...\n</LexicalExtensionComposer>\n```\n\nAfter an editor is created, the mode can be changed imperatively:\n\n```js\neditor.setEditable(true);\n```\n\n## Reading the mode\n\nIn order to find the current mode of the editor you can use:\n\n```js\nconst isEditable = editor.isEditable(); // Returns true or false\n```\n\nYou can also get notified when the editor's read only mode has changed:\n\n```js\nconst removeEditableListener = editor.registerEditableListener(\n  (isEditable) => {\n    // The editor's mode is passed in!\n    console.log(isEditable);\n  },\n);\n\n// Do not forget to unregister the listener when no longer needed!\nremoveEditableListener();\n```\n\nIn an extension, depend on\n[`WatchEditableExtension`](/docs/api/modules/lexical_extension#watcheditableextension),\nwhose output is a signal holding the current editable state, instead of\nregistering the listener yourself.\n","packages/lexical-website/docs/concepts/selection.md":"# Selection\n\n## Types of selection\n\nLexical's selection is part of the `EditorState`. This means that for every update, or change to the editor, the\nselection always remains consistent with that of the `EditorState`'s node tree.\n\nIn Lexical, there are four types of selection possible:\n\n- `RangeSelection`\n- `NodeSelection`\n- `TableSelection` (implemented in `@lexical/table`)\n- `null`\n\nIt is possible, but not generally recommended, to implement your own selection types that implement `BaseSelection`.\n\n### `RangeSelection`\n\nThis is the most common type of selection, and is a normalization of the browser's DOM Selection and Range APIs.\n`RangeSelection` consists of three main properties:\n\n- `anchor` representing a `RangeSelection` point\n- `focus` representing a `RangeSelection` point\n- `format` numeric bitwise flag, representing any active text formats\n\nBoth the `anchor` and `focus` points refer to an object that represents a specific part of the editor. The main properties of a `RangeSelection` point are:\n\n- `key` representing the `NodeKey` of the selected Lexical node\n- `offset` representing the position from within its selected Lexical node. For the `text` type this is the character, and for the `element` type this is the child index from within the `ElementNode`\n- `type` representing either `element` or `text`.\n\n### `NodeSelection`\n\nNodeSelection represents a selection of multiple arbitrary nodes. For example, three images selected at the same time.\n\n- `getNodes()` returns an array containing the selected LexicalNodes\n\n### `TableSelection`\n\nTableSelection represents a grid-like selection like tables. It stores the key of the parent node where the selection takes place and the start and end points.\n`TableSelection` consists of three main properties:\n\n- `tableKey` representing the parent node key where the selection takes place\n- `anchor` representing a `TableSelection` point\n- `focus` representing a `TableSelection` point\n\nFor example, a table where you select row = 1 col = 1 to row 2 col = 2 could be stored as follows:\n- `tableKey = 2` table key\n- `anchor = 4` table cell (key may vary)\n- `focus = 10` table cell (key may vary)\n\nNote that `anchor` and `focus` points work the same way as `RangeSelection`.\n\n### `null`\n\nThis is for when the editor doesn't have any active selection. This is common for when the editor has been blurred or when selection\nhas moved to another editor on the page. This can also happen when trying to select non-editable components within the editor space.\n\n## Working with selection\n\nSelection can be found using the `$getSelection()` helper, exported from the `lexical` package. This function can be used within\nan update, a read, or a command listener.\n\n:::tip\n\nTo walk the document from a selection point — or to write a traversal that\nhandles empty nodes and collapsed selections correctly — see\n[Node Traversals with NodeCaret](./traversals.md).\n`$caretRangeFromSelection(selection)` converts a `RangeSelection` into the\ncaret range that traversal API works with, and\n`$setSelectionFromCaretRange(range)` converts back.\n\n:::\n\n```js\nimport {$getSelection, SELECTION_CHANGE_COMMAND} from 'lexical';\n\neditor.update(() => {\n  const selection = $getSelection();\n});\n\neditorState.read(() => {\n  const selection = $getSelection();\n});\n\n// SELECTION_CHANGE_COMMAND runs before reconciliation in the pending update.\neditor.registerCommand(SELECTION_CHANGE_COMMAND, () => {\n  const selection = $getSelection();\n});\n```\n\nIn some cases you might want to create a new type of selection and set the editor selection to\nbe that. This can only be done in update or command listeners.\n\n```js\nimport {$setSelection, $createRangeSelection, $createNodeSelection} from 'lexical';\n\neditor.update(() => {\n  // Set a range selection\n  const rangeSelection = $createRangeSelection();\n  $setSelection(rangeSelection);\n\n  // You can also indirectly create a range selection, by calling some of the selection\n  // methods on Lexical nodes.\n  const someNode = $getNodeByKey(someKey);\n\n  // On element nodes, this will create a RangeSelection with type \"element\",\n  // referencing an offset relating to the child within the element.\n  // On text nodes, this will create a RangeSelection with type \"text\",\n  // referencing the text character offset.\n  someNode.select();\n  someNode.selectPrevious();\n  someNode.selectNext();\n\n  // You can use this on any node.\n  someNode.selectStart();\n  someNode.selectEnd();\n\n  // Set a node selection\n  const nodeSelection = $createNodeSelection();\n  // Add a node key to the selection.\n  nodeSelection.add(someKey);\n  $setSelection(nodeSelection);\n\n  // You can also clear selection by setting it to `null`.\n  $setSelection(null);\n});\n```\n\n## Focus\n\nYou may notice that when you issue an `editor.update` or\n`editor.dispatchCommand` then the editor can \"steal focus\" if there is\na selection and the editor is editable. This is because the Lexical\nselection is reconciled to the DOM selection during reconciliation,\nand the browser's focus follows its DOM selection.\n\nIf you want to make updates or dispatch commands to the editor without\nchanging the selection, can use the `SKIP_DOM_SELECTION_TAG` update tag\n(added in v0.22.0):\n\n```js\n// Call this from an editor.update or command listener\n$addUpdateTag(SKIP_DOM_SELECTION_TAG);\n```\n\nIf you want to add this tag during processing of a `dispatchCommand`,\nyou can wrap it in an `editor.update`:\n\n```js\n// NOTE: If you are already in a command listener or editor.update,\n//       do *not* nest a second editor.update! Nested updates have\n//       confusing semantics (dispatchCommand will re-use the\n//       current update without nesting)\neditor.update(() => {\n  $addUpdateTag(SKIP_DOM_SELECTION_TAG);\n  editor.dispatchCommand(/* … */);\n});\n```\n\n`SKIP_DOM_SELECTION_TAG` does not apply to the initial editor state setup\n(the `$initialEditorState` property of the root extension, or\n`initialConfig.editorState` with the legacy `LexicalComposer`). On first mount the editor still scrolls\nto and focuses the initial selection. To prevent that, call\n`$setSelection(null)` inside your initial state setup function:\n\n```js\nconst editor = buildEditorFromExtensions({\n  name: '[root]',\n  // ...\n  $initialEditorState: () => {\n    // ... build your initial nodes ...\n    $setSelection(null);\n  },\n});\n```\n\nIf you have to support older versions of Lexical, you can mark the editor\nas not editable during the update or dispatch.\n\n```js\n// NOTE: This code should be *outside* of your update or command listener, e.g.\n//       directly in the DOM event listener\nconst prevEditable = editor.isEditable();\neditor.setEditable(false);\neditor.update(\n  () => {\n    // run your update code or editor.dispatchCommand in here\n  }, {\n    onUpdate: () => {\n      editor.setEditable(prevEditable);\n    },\n  },\n);\n```\n\n## Reading selection across a shadow boundary\n\nIf your plugin reads the DOM selection directly — through\n`Selection.anchorNode`, `Selection.getRangeAt(0)`, or similar — and the\neditor's `contentEditable` lives inside a `ShadowRoot`, the browser retargets\nthose reads to the shadow host. See\n[Shadow DOM and iframes](./shadow-dom.md) for the shadow-aware helpers\n(`getDOMSelectionPoints`, `getDOMSelectionRange`, etc.) that return the\nun-retargeted boundary points; they fall through to the standard reads in the\nplain light DOM, so there's nothing to do until you actually mount the editor\nin a shadow tree.\n\n### Selection change timing\n\n`SELECTION_CHANGE_COMMAND` runs **before reconciliation**, in a writable update,\nfor native, programmatic, and non-range selection changes. `$getSelection()`\nreturns the pending selection; `$getPreviousSelection()` returns the selection\nfrom the last committed state. A listener can normalize the selection or edit\nnodes before that same update commits. Listeners that change the selection may\ncause another notification before commit; they must converge.\n\nThis covers programmatic changes such as `node.select()`, which notify without\nwaiting for a DOM `selectionchange` event, and non-range selections such as\n`NodeSelection` and `TableSelection`, as well as native range selections.\n\nSelection listeners use the update's normal error handling. A throwing listener\nreports through `onError` and can abort the entire pending update, including the\nedit that triggered the notification and any other edits batched into that\ncommit, just like an explicit command dispatch inside that update. Because a\nprogrammatic selection change also notifies, a listener error can abort content\nedits made in the same update. Listener edits do not have a separate rollback\nscope.\n\nThe DOM is not guaranteed to match the pending state, even for a `NodeSelection`.\nNew nodes may not have an element yet, and DOM ranges, layout, and focus may\nstill reflect the previous state. Schedule DOM-dependent work after the update\nand read the reconciled state there:\n\n```ts\neditor.registerCommand(\n  SELECTION_CHANGE_COMMAND,\n  () => {\n    $onUpdate(() => {\n      editor.read('latest', () => {\n        // Read $getSelection(), look up elements, or position floating UI here.\n      });\n    });\n    return false;\n  },\n  COMMAND_PRIORITY_LOW,\n);\n```\n\nImport `$onUpdate` and `COMMAND_PRIORITY_LOW` from `lexical`. Read selection and\nnodes inside the callback rather than capturing mutable pending objects.\n`registerUpdateListener` is another option for UI that must respond to every\ncommit, including content changes that do not change the selection. Updates\nusing `SKIP_DOM_SELECTION_TAG` still deliberately leave the browser selection\nunsynchronized.\n\n#### Update tags and local edits\n\nThe notification belongs to the pending update and inherits its tags. A listener\ncan inspect them with `$hasUpdateTag`. In particular, Yjs does not sync content\nedits back to peers when the update is tagged `COLLABORATION_TAG` or\n`HISTORIC_TAG`; history also treats `HISTORIC_TAG` as replaying an existing entry.\nThis applies to content edits made by selection listeners in those updates.\n\nListeners that only apply to local editing should skip those tagged updates. If\na remote change or undo must trigger an independent local content edit, schedule\nit after the tagged commit with `$onUpdate`, then start a fresh `editor.update`:\n\n```ts\nconst $applyLocalEdit = () => {\n  // Read the current selection and nodes, then apply a convergent local edit.\n};\n\neditor.registerCommand(\n  SELECTION_CHANGE_COMMAND,\n  () => {\n    if ($hasUpdateTag(COLLABORATION_TAG) || $hasUpdateTag(HISTORIC_TAG)) {\n      $onUpdate(() => editor.update($applyLocalEdit));\n    } else {\n      $applyLocalEdit();\n    }\n    return false;\n  },\n  COMMAND_PRIORITY_LOW,\n);\n```\n\nImport the tags and `$hasUpdateTag` from `lexical`. Re-read the state in the fresh\nupdate; it has its own collaboration and undo behavior. Calling `editor.update`\ndirectly inside the command listener queues a nested update that still belongs\nto the same tagged commit.\n\nAutomatic range and cleared-selection notifications require a connected editor\nroot. Changes committed while those notifications are skipped advance the\nselection baseline and are not replayed when the root reconnects. Non-range\nselections continue to notify in unmounted and headless editors,\nwith the same pre-reconciliation timing. Native dirty-selection notifications\nmay fire even when the selection compares equal to the previous selection.\n","packages/lexical-website/docs/concepts/shadow-dom.md":"---\nsidebar_label: Shadow DOM and iframes\n---\n\n# Shadow DOM and iframes\n\nA Lexical editor works when its `contentEditable` lives inside an **open**\n[DOM `ShadowRoot`](https://developer.mozilla.org/docs/Web/API/ShadowRoot) (for\nexample inside a [web component](https://developer.mozilla.org/docs/Web/API/Web_components))\nor inside an `<iframe>` document. Both are supported out of the box — you only\nhave to point the editor at the right root element.\n\n## Embedding in a shadow root\n\nAttach an open shadow root, render a `contentEditable` inside it, and pass that\nelement to `editor.setRootElement` (or, with `@lexical/react`, portal the\n`ContentEditable` into the shadow root — React context flows across the portal,\nso the editor is built exactly as it would be in the light DOM):\n\n```js\nconst host = document.querySelector('#editor-host');\nconst shadow = host.attachShadow({mode: 'open'});\nconst contentEditable = document.createElement('div');\ncontentEditable.contentEditable = 'true';\nshadow.appendChild(contentEditable);\n\neditor.setRootElement(contentEditable);\n```\n\nLexical detects the enclosing shadow root from the root element and resolves\nselection through it. There is nothing else to configure.\n\n### Why it needs platform support\n\nInside a shadow tree the browser **retargets** the document's selection to the\nshadow host: `Selection.anchorNode`/`focusNode` and `Selection.getRangeAt`\nreport the host element, not the node the caret is actually in, and\n`document.activeElement` reports the host rather than the focused element.\nLexical works around this using only standard platform APIs:\n\n- Selection is read with\n  [`Selection.getComposedRanges`](https://developer.mozilla.org/docs/Web/API/Selection/getComposedRanges)\n  and [`Selection.direction`](https://developer.mozilla.org/docs/Web/API/Selection/direction),\n  which return the real (un‑retargeted) boundary points.\n- Focus is read with\n  [`ShadowRoot.activeElement`](https://developer.mozilla.org/docs/Web/API/DocumentOrShadowRoot/activeElement)\n  via [`Node.getRootNode`](https://developer.mozilla.org/docs/Web/API/Node/getRootNode).\n- Caret movement and word/line deletion use the native `Selection.modify`, and\n  selection is written with `Selection.setBaseAndExtent`, both of which operate\n  on shadow‑tree nodes directly.\n- Drop targets and image-drag hit‑tests resolve through\n  [`Document.caretPositionFromPoint`](https://developer.mozilla.org/docs/Web/API/Document/caretPositionFromPoint)\n  with the `shadowRoots` option so the dropped point lands on the real node\n  rather than the retargeted host.\n\n### Browser support\n\nThe shadow-aware path requires the platform APIs listed above. Lexical falls\nback to the light‑DOM reads when those APIs are missing, so editors that don't\nlive in a shadow tree keep working on any engine the rest of Lexical supports.\n\n| Platform API | Used for | Chrome / Edge | Firefox | Safari |\n| --- | --- | --- | --- | --- |\n| `Selection.getComposedRanges` | Reading the un‑retargeted boundary points | 137+ | 142+ | 17.0+ |\n| `Selection.direction` | Mapping the composed range back onto anchor/focus | 137+ | 126+ | 17.0+ |\n| `ShadowRoot.activeElement` | Resolving the focused element through the host | All modern | All modern | All modern |\n| `Document.caretPositionFromPoint({shadowRoots})` | Shadow-aware drop / drag hit-tests | 128+ | 125+ | 18.1+ |\n\nLexical also supports the legacy variadic form of `getComposedRanges` shipped by\nSafari 17–18.1, automatically choosing the dictionary or variadic call shape\nat runtime.\n\n### Closed shadow roots\n\nA closed shadow root (`{mode: 'closed'}`) hides its contents from outside code:\n`Node.getRootNode({composed: false})` returns the host on every external probe,\nand `Selection.getComposedRanges({shadowRoots})` ignores the closed root. The\neditor inside a closed shadow root cannot read its own selection through the\nhost, so closed shadow roots are **not supported**.\n\nIf you need style/markup encapsulation without selection isolation, mount the\neditor in an open shadow root with `delegatesFocus: true` instead — outside\ncode can still observe focus and selection while CSS and markup remain\nencapsulated.\n\n### Styling\n\nShadow trees do not inherit the document's stylesheets, so your editor/theme\nCSS has to live inside the shadow root. A `<style>` element appended to the\nroot is the simplest option; a\n[constructable stylesheet](https://developer.mozilla.org/docs/Web/API/CSSStyleSheet#constructor)\nadopted via `shadowRoot.adoptedStyleSheets` scales better when one stylesheet\nserves several editor instances. The playground's `ShadowDomWrapper` clones\nthe page's existing `<link>` / `<style>` nodes into the shadow root so it can\nreuse Vite's HMR-managed stylesheets. This is a property of Shadow DOM, not\nof Lexical.\n\nCSS custom properties and inherited HTML attributes (`dir`, `lang`) cross\nthe shadow boundary on their own, so a page-level rule like `:root {\n--editor-bg: #1c1d22; }` or a host attribute like `<my-editor dir=\"rtl\"\nlang=\"ko\">` propagates into the editor without further setup. User\npreference media queries (`@media (prefers-color-scheme: dark)`,\n`(prefers-reduced-motion: reduce)`, `(forced-colors: active)`) evaluate\ninside the shadow root just like outside; place them in the shadow-mounted\nstylesheet rather than the page sheet.\n\n## Embedding in an iframe\n\nAn editor whose root element belongs to an `<iframe>` document is also\nsupported. Lexical reads the editor's `window`/`document` from the root\nelement (`rootElement.ownerDocument.defaultView`), so selection and focus are\nresolved against the iframe rather than the top‑level document:\n\n```js\nconst iframeDoc = iframe.contentDocument;\nconst contentEditable = iframeDoc.querySelector('#editor');\n\n// The editor can be built in the parent frame; once its root element is set it uses\n// the iframe's own window and document for selection and focus.\neditor.setRootElement(contentEditable);\n```\n\nSelection inside an iframe is **not** retargeted (an iframe is a separate\ndocument, not a shadow boundary), so `getComposedRanges` is not involved here —\nthe iframe's own selection already reports the correct nodes.\n\n## Shadow-aware helpers\n\nIf a plugin reads the DOM selection or the focused element directly, use the\nshadow/iframe‑aware helpers exported from `lexical` instead of\n`Selection.anchorNode` / `document.activeElement`, so it keeps working in these\ncontexts. In the plain light DOM each helper degrades to the standard read\nwithout any extra cost.\n\n| Instead of | Use |\n| --- | --- |\n| `Selection.anchorNode` / `anchorOffset` / `focusNode` / `focusOffset` | `getDOMSelectionPoints(selection, rootElement)` |\n| `selection.getRangeAt(0)` | `getDOMSelectionRange(selection, rootElement)` |\n| Both shapes from one read | `getDOMSelectionRangeAndPoints(selection, rootElement)` |\n| `selection.getComposedRanges` raw call | `getComposedStaticRange(selection, rootElement)` |\n| `document.activeElement === el` | `getActiveElement(el) === el` |\n| Deepest focused element through nested shadow trees | `getActiveElementDeep(document)` |\n| `event.target` on a `composed: true` event listening above the shadow boundary | `getComposedEventTarget(event)` |\n| `node.getRootNode() instanceof ShadowRoot` (cross‑realm‑safe) | `isDOMShadowRoot(node)` |\n| Walking up to discover enclosing shadow roots | `getDOMShadowRoots(node)` |\n\nAll nine helpers are marked `@experimental` while the surface stabilizes — the\nreturn shape may change, but the behavior in the light DOM is stable.\n\n### `getDOMSelectionPoints` read semantics\n\nIn the light DOM `getDOMSelectionPoints` returns the live `Selection` itself,\nso each property read is deferred and `$updateDOMSelection` only pays for the\nsynchronous style/layout recalculation that a `Selection.anchorNode` /\n`focusNode` read triggers when it actually needs the value. Inside a shadow\ntree the return is a snapshot taken at call time, including\n`Selection.direction` so callers can branch on it explicitly. Read the four\npoints immediately after the call rather than caching the returned reference,\nor use `points === domSelection` to detect the alias path.\n\nIf a future engine ships `getComposedRanges` without `Selection.direction`\n(no current configuration matches — every engine that ships the former\nalso ships the latter), the snapshot's `direction` is `undefined` and\nanchor/focus default to the StaticRange's tree order; callers needing strict\nbackward fidelity should check `direction !== undefined` before relying on\nthe swap.\n\n## Form association\n\nA Lexical editor inside a [form-associated custom\nelement](https://developer.mozilla.org/docs/Web/API/Web_components#form-associated_custom_elements)\nparticipates in a `<form>` like any built-in form control: submission carries\nthe editor's value, form reset clears it, and validation flows through. The\nsketch:\n\n```js\nclass LexicalEditorElement extends HTMLElement {\n  static formAssociated = true;\n\n  constructor() {\n    super();\n    this.internals = this.attachInternals();\n  }\n\n  get value() {\n    return this.editor\n      ? JSON.stringify(this.editor.getEditorState().toJSON())\n      : '';\n  }\n\n  set value(serialized) {\n    if (this.editor && serialized !== '') {\n      this.editor.setEditorState(this.editor.parseEditorState(serialized));\n    }\n  }\n\n  connectedCallback() {\n    // `delegatesFocus: true` routes `host.focus()` (and implicit focus\n    // from `<label for=\"...\">`) to the first focusable element inside\n    // the shadow root — for the editor that's the contentEditable.\n    const shadow =\n      this.shadowRoot ?? this.attachShadow({delegatesFocus: true, mode: 'open'});\n    const contentEditable = document.createElement('div');\n    contentEditable.contentEditable = 'true';\n    contentEditable.tabIndex = 0;\n    shadow.appendChild(contentEditable);\n\n    const editor = buildEditorFromExtensions(\n      defineExtension({name: 'lexical-editor-element'}),\n    );\n    editor.setRootElement(contentEditable);\n    this.editor = editor;\n\n    // Seed the form value so a submit before the user types still produces\n    // a non-empty serialized state, mirroring `<input value=\"...\">`.\n    this.internals.setFormValue(this.value);\n\n    editor.registerUpdateListener(({dirtyElements, dirtyLeaves}) => {\n      // Only fire on real content changes, not on pure selection updates —\n      // matches HTMLInputElement's `input` event contract.\n      if (dirtyElements.size === 0 && dirtyLeaves.size === 0) return;\n      this.internals.setFormValue(this.value);\n      this.dispatchEvent(new Event('input', {bubbles: true, composed: true}));\n    });\n  }\n}\n```\n\nWithout the dirty-leaf/element gate every caret move would wake the form's\n`oninput` listeners, so the `input` event matches `HTMLInputElement`'s\ncontract only after that guard. The initial `setFormValue` is what keeps a\nform submission before the user types from carrying an empty value, since\nthe update listener has not fired yet at that point. A full reference\nimplementation lives in\n[`examples/shadow-dom-web-component`](https://github.com/facebook/lexical/tree/main/examples/shadow-dom-web-component).\n\nDOM moves (re-parenting the host into a different `<form>` or list) trigger\n`disconnectedCallback` followed by `connectedCallback`, which rebuilds the\neditor against a fresh contentEditable. Round-trip the user's content the\nsame way `<input>` and `<textarea>` round-trip their `value` attribute:\ncache the serialized state in `disconnectedCallback` and replay it through\n`parseEditorState` on the next mount. The reference dev example uses a\n`pendingState` field for exactly this.\n\nThe same form-associated host can also implement the rest of the\n[form-associated callbacks](https://developer.mozilla.org/docs/Web/API/Web_components/Using_custom_elements#form-associated_callbacks):\n`formStateRestoreCallback(state, reason)` re-hydrates the editor on bfcache\nnavigation (`reason: 'restore'`) or form autocomplete restore\n(`reason: 'autocomplete'`); `formAssociatedCallback(form)` reacts to\nprogrammatic form moves; `formResetCallback()` clears the editor when the\nsurrounding form is reset; and `formDisabledCallback(isDisabled)` flips the\neditor's editable state when an ancestor `<fieldset disabled>` toggles. The\nserialized JSON `internals.setFormValue` published earlier is what comes\nback in the state-restore callback.\n\n## Common pitfalls\n\nMoving an existing editor into a shadow root exposes a handful of recurring\nmismatches between the DOM APIs you'd reach for and what they report.\n\n### Event retargeting\n\nListeners above the shadow boundary — `window`, `document`'s\n`selectionchange`, popups portaled to `document.body` — see `event.target`\nretargeted to the shadow host, not the actual clicked element. A\n`rootElement.contains(event.target)` gate therefore always rejects clicks\nthat came from the editor. Resolve through the composed path:\n\n```js\nconst target = getComposedEventTarget(event);\nif (target instanceof Node && rootElement.contains(target)) {\n  // click really did land inside the shadow-hosted editor\n}\n```\n\n### Focus probes\n\n`document.activeElement` reports the host when focus is inside an open\nshadow tree, so `document.activeElement === rootElement` is always false.\n`getActiveElement(rootElement)` reads `DocumentOrShadowRoot.activeElement`\nthrough `Node.getRootNode` instead, and `getActiveElementDeep` keeps\ndescending into nested shadow roots when an editor whose decorator embeds a\nweb component needs the innermost focused element.\n\n```js\nif (getActiveElement(rootElement) === rootElement) { ... }\n```\n\n### Outside-click handlers\n\nA typical dropdown registers `document.addEventListener('click', ...)` and\ncalls `setShowDropDown(false)` whenever `button.contains(event.target)` is\nfalse. From inside a shadow tree that check always fails (the target is the\nhost), so the dropdown closes on the very click that opened it. Compare\nagainst `getComposedEventTarget(event)` instead — the same fix the playground's\n`DropDown` and `@lexical/react`'s `DraggableBlockPlugin` use.\n\n### Drop hit-tests\n\n`document.caretRangeFromPoint` and the no-argument\n`document.caretPositionFromPoint` return the host when the pointer is over\nshadow content, so an image drop lands on the host rather than the textnode\nunder the cursor. `@lexical/clipboard`'s internal `caretFromPoint` helper switches to\n`caretPositionFromPoint(x, y, {shadowRoots})` when `rootElement` lives in a\nshadow tree, and verifies the returned offset node really did land inside\none of the requested shadow roots — engines that silently ignore the option\nfall through to the legacy paths.\n\n### Style mirror cleanup\n\nIf your shadow mount mirrors `<style>` / `<link>` nodes from `document.head`\nand watches for additions via `MutationObserver`, mirror removals too;\notherwise stylesheets removed by HMR or a runtime theme swap linger inside\nthe shadow. `ShadowDomWrapper` tracks an `original → clone` map so an\nupstream removal drops the corresponding clone, and on unmount it removes\nevery clone so React 18 StrictMode's double-mount can't accumulate\nduplicates through the persistent shadow root.\n\nDev-mode HMR has two paths that a `childList`-only observer misses.\nFirefox replaces a `<style>`'s text content in place, so the observer\nneeds `characterData + subtree` to see the mutation; Chrome and WebKit\nupdate CSS through the CSSOM (`style.sheet.replaceSync` / `insertRule`),\nwhich the DOM observer never sees at all. `ShadowDomWrapper` also\nsubscribes to Vite's `vite:afterUpdate` hook for an explicit resync of\nevery clone after each HMR pass — `import.meta.hot` is `undefined` in\nproduction builds, so the hook is a no-op outside dev.\n\n### Popover and dialog layout\n\nThe UA stylesheet defaults a closed\n[Popover](https://developer.mozilla.org/docs/Web/API/Popover_API) (or a\n`<dialog>` without `open`) to `display: none`. A base rule like\n`#my-popover { display: flex; ... }` overrides that and leaves the popover\nvisible after `hidePopover()`. Scope layout to the open state instead —\n`#my-popover:popover-open { display: flex; }` — so the closed state honors\nthe UA default. This is a popover-API gotcha, not a shadow-specific one,\nbut it surfaces here because floating UI anchored to a shadow-root\nselection (a format popover or link editor that reads coordinates through\n`getDOMSelectionRangeAndPoints`) is a common shadow integration pattern.\n\n### Observers across the shadow boundary\n\n`ResizeObserver`, `IntersectionObserver`, and `MutationObserver` observe\nnodes inside an open shadow root with no special configuration — pass the\ninner node (the contentEditable or the host) just like any other DOM\ntarget. A `MutationObserver` registered on the host does not see mutations\ninside the shadow tree, though; observe the contentEditable or a\nshadow-internal container instead. The host's `attributeChangedCallback`\ncovers the host's own attribute changes.\n\n## Migrating an existing light-DOM editor\n\nThe same code shape keeps working in a shadow root; migration is mostly\nabout removing pre-shadow workarounds. Direct selection reads\n(`Selection.anchorNode`, `Selection.getRangeAt(0)`,\n`document.querySelector('[contenteditable]')` used as an editor probe) route\nthrough `getDOMSelectionPoints` / `getDOMSelectionRange` /\n`getActiveElement`, and any `event.target` read above the shadow boundary\nbecomes `getComposedEventTarget(event)`. CSS variables inherit through the\nshadow boundary but class declarations don't, so the editor/theme CSS has\nto be adopted or cloned into the shadow root.\n\nThe focus and popup paths are the ones that surprise people in review:\noutside-click handlers, blur-then-refocus, and \"did the editor lose focus?\"\nprobes usually need `getActiveElement(rootElement)` rather than\n`document.activeElement`; floating UI that portals into `document.body` and\nresolves hover with `element.getRootNode().elementFromPoint(x, y)` keeps the\nsame call shape but has to guard the popup root narrowing against a\ndetached popup whose `getRootNode()` returns itself.\n\n## Examples\n\nRunnable examples live in the repository:\n\n- [`examples/shadow-dom`](https://github.com/facebook/lexical/tree/main/examples/shadow-dom)\n  — a React editor inside a shadow root with a light‑DOM toolbar.\n- [`examples/shadow-dom-web-component`](https://github.com/facebook/lexical/tree/main/examples/shadow-dom-web-component)\n  — a framework‑free `<lexical-editor>` custom element, form‑associated via\n  `ElementInternals`. The page also mounts one instance inside a wrapper\n  `<div>` that opens its own shadow root, exercising the multi‑level\n  walk through two nested shadow boundaries.\n- [`examples/vanilla-js-iframe`](https://github.com/facebook/lexical/tree/main/examples/vanilla-js-iframe)\n  — an editor rendered into an `<iframe>`.\n- The playground's **Render in Shadow DOM** setting toggles the same editor\n  between light and shadow mounts, useful for spot-checking your plugins\n  against both modes.","packages/lexical-website/docs/concepts/transforms.md":"\n\n# Node Transforms\n\nTransforms are the most efficient mechanism to respond to changes to the EditorState.\n\nFor example:\nUser types a character and you want to color the word blue if the word is now equal to \"congrats\".\nWe programmatically add an `@Mention` to the editor, the `@Mention` is immediately next to another `@Mention` (`@Mention@Mention`). Since we believe this makes mentions hard to read, we want to destroy/replace both mentions and render them as plain TextNode's instead.\n\n```js\nconst removeTransform = editor.registerNodeTransform(TextNode, (textNode) => {\n  if (textNode.getTextContent() === 'blue') {\n    textNode.setTextContent('green');\n  }\n});\n```\n\n## Syntax\n\n```typescript\neditor.registerNodeTransform<T extends LexicalNode>(klass: Klass<T>, listener: Transform<T>): () => void\n```\n\n## Lifecycle\n\nTransforms are executed sequentially before changes are propagated to the DOM and multiple transforms still lead to a single DOM reconciliation (the most expensive operation in Lexical's lifecycle).\n\n```mermaid\nflowchart LR\n  accTitle: Transforms lifecycle\n  accDescr: A keyboard event leads to an update function. Its changes mark nodes dirty, and the transforms registered for those nodes run one after another. If a transform changed more nodes, the transforms for those nodes run again. When nothing is left dirty, the DOM is reconciled once and the update listeners run.\n  event{{\"Keyboard event\"}} --> update[\"Update function\"]\n  update --> t1[\"transform1\"]\n  t1 --> t2[\"transform2\"]\n  t2 --> dirty{\"Nodes still dirty?\"}\n  dirty -->|\"yes\"| t1\n  dirty -->|\"no\"| reconcile[\"DOM reconciled once\"]\n  reconcile --> listeners[\"Update listeners\"]\n```\n\n:::caution Beware!\n\nWhile it is possible to achieve the same or very similar result through an [update listener](listeners.md#registerupdatelistener) followed by an update, this is highly discouraged as it triggers an additional render (the most expensive lifecycle operation).\n\nAdditionally, each cycle creates a brand new `EditorState` object which can interfere with features like undo/redo (`HistoryExtension`) if not handled correctly.\n\n```js\neditor.registerUpdateListener(() => {\n  editor.update(() => {\n    // Don't do this\n  });\n});\n```\n\n:::\n\n### Dirty Nodes\n\n*Dirty Leaves* (any LexicalNode that is not an ElementNode) and *Dirty Elements*\nare tracked separately to support the transform heuristic.\n\nInternally, there are two states for dirty nodes:\n* Intentionally Dirty nodes (leaves or elements) had `node.getWritable()` or\n  `node.markDirty()` (an alias) called on them. Maintaining the\n  tree-of-doubly-linked-lists structure of a lexical document requires that\n  this Intentionally Dirty state will propagate to immediate siblings and in\n  some cases the parent node.\n* Unintentionally Dirty Element nodes are an ancestor of a\n  dirty node that were not explicitly marked as Intentionally Dirty\n  Only elements can be Unintentionally Dirty, because\n  leaves by definition can not have children.\n\nThe reconciler works by starting at the RootNode (which is the ancestor\nof any attached node, and thus always dirty whenever any attached node\nis dirty). Intentionally Dirty nodes have their createDOM and/or updateDOM\ncalled. Dirty Elements reconcile all of their children. Thus, reconciliation\nstops at the highest node in a subtree that has no dirty nodes (unless it\nis running with a flag to do a full reconciliation which considers all\nnodes as Intentionally Dirty).\n\n:::info\n\nThe transform heuristic depends on these internal implementation details to\nfind a fixed point where no more transforms are required.\n\n:::\n\n### Transform heuristic\n\n1. We transform leaves first. If transforms generate additional dirty nodes we repeat `step 1`. The reasoning behind this is that marking a leaf as dirty marks all its parent elements as dirty too.\n2. We transform elements.\n    - If element transforms generate additional dirty nodes we repeat `step 1`.\n    - If element transforms only generate additional dirty elements we only repeat `step 2`.\n\nA node will be marked as dirty on any (or most) modifications done to it, its children, or siblings in certain cases.\n\n## Preconditions\n\nPreconditions are fundamental for transforms to prevent them from running multiple times and ultimately causing an infinite loop.\n\nTransforms are designed to run when nodes have been modified (aka Intentionally Dirty). For the most part, transforms only need to run once after the update but the sequential nature of transforms makes it possible to have order bias. Hence, transforms are run over and over until this particular type of Node is no longer marked as intentionally dirty by any of the transforms.\n\nHence, we have to make sure that the transforms do not mark the node dirty unnecessarily.\n\n```js\n// When a TextNode changes (marked as dirty) make it bold\neditor.registerNodeTransform(TextNode, textNode => {\n  // Important: Check current format state\n  if (!textNode.hasFormat('bold')) {\n    textNode.toggleFormat('bold');\n  }\n});\n```\n\nBut oftentimes, the order is not important. The below would always end up in the result of the two transforms:\n\n```js\n// Extension 1\neditor.registerNodeTransform(TextNode, textNode => {\n  // This transform runs twice but does nothing the first time because it doesn't meet the preconditions\n  if (textNode.getTextContent() === 'modified') {\n    textNode.setTextContent('re-modified');\n  }\n})\n// Extension 2\neditor.registerNodeTransform(TextNode, textNode => {\n  // This transform runs only once\n  if (textNode.getTextContent() === 'original') {\n    textNode.setTextContent('modified');\n  }\n})\n// App\neditor.registerUpdateListener(({editorState}) => {\n  const text = editorState.read(() => $getRoot().getTextContent());\n  // text === 're-modified'\n});\n```\n\n:::info\n\nThe transform heuristic considers `RootNode` to be Intentionally Dirty whenever\nany node is dirty, and it ensures that this transform is applied last after all\nother transforms. In this way, you can consider it a \"pre-update\" listener,\nwhich occurs before any DOM reconciliation has happened. As with any other\ntransform, it may get called multiple times in a given update (especially if\nyour RootNode transform makes any node dirty).\n\n:::\n\n## Transforms on parent nodes\n\nTransforms are very specific to a type of node. This applies to both the declaration (`registerNodeTransform(ImageNode)`) and the times it triggers during an update cycle.\n\n```js\n// Won't trigger\neditor.registerNodeTransform(ParagraphNode, () => {});\n// Will trigger as TextNode was marked dirty\neditor.registerNodeTransform(TextNode, () => {});\neditor.update(() => {\n  const textNode = $getNodeByKey('3');\n  textNode.setTextContent('foo');\n});\n```\n\nWhile the marked dirty rule is always true, there are some cases when it's not immediately obvious and/or we force nearby nodes to become dirty for the sake of easier transform logic:\nYou add a node to an ElementNode, the ElementNode and the newly added children are marked dirty, also its new immediate siblings\nYou remove a node, its parent is marked dirty, also the node's immediate siblings prior to being removed\nYou move a node via `replace`, rules 2 and 1 are applied.\n\n```js\neditor.registerNodeTransform(ParagraphNode, paragraph => {\n // Triggers\n});\neditor.update(() => {\n  const paragraph = $getRoot().getFirstChild();\n  paragraph.append($createTextNode('foo'));\n});\n```\n\n## registerLexicalTextEntity\n\nIt is common to have certain nodes that are created/destroyed based on their text content and siblings. For example, `#lexical` is a valid hashtag whereas `#!lexical` is not.\n\nThis is a perfectly valid case for transforms but we have gone ahead and already built a utility transform wrapper for you for this specific case:\n\n```typescript\nregisterLexicalTextEntity<T extends TextNode>(\n  editor: LexicalEditor,\n  getMatch: (text: string) => null | EntityMatch,\n  targetNode: Klass<T>,\n  createNode: (textNode: TextNode) => T,\n): Array<() => void>;\n```\n\n## Examples\n\n1. [Emojis](https://github.com/facebook/lexical/blob/main/packages/lexical-playground/src/plugins/EmojisExtension/index.ts)\n2. [AutoLink](https://github.com/facebook/lexical/blob/main/packages/lexical-playground/src/plugins/AutoLinkExtension/index.ts)\n3. [HashtagExtension](https://github.com/facebook/lexical/blob/main/packages/lexical-hashtag/src/LexicalHashtagExtension.ts)\n","packages/lexical-website/docs/concepts/traversals.md":"# Node Traversals with NodeCaret\n\nNodeCaret offers a unified and efficient way for traversing the document\ntree, making it much easier to correctly implement traversals and avoid\nedge cases around empty nodes and collapsed selections.\n\nThese new low-level functions were all designed to work together as a\nfully featured relatively lightweight API to use in the core to\nallow us to gradually address some edge cases and then simplify and shrink\nthe code. We expect higher-level utilities to be developed and shipped\nin @lexical/utils or another module at a later date. The current overhead\nshould be less than 3kB in a production environment.\n\nThe NodeCaret API was introduced in lexical v0.25.0.\n\n## Concepts\n\nThe core concept with `NodeCaret` is that you can represent any specific\npoint in the document by using an `origin` node, a `direction` that\npoints towards an adjacent node (`next` or `previous`), and a `type`\nto specify whether the arrow points towards a sibling (`sibling`) or\ntowards a child (`child`).\n\nAll of these types have a `D` type parameter that must be a `CaretDirection`, so you\ncan not accidentally mix up `next` and `previous` carets. Many of them\nalso have a `T` type parameter that encodes the type of the `origin` node.\n\n:::tip\n\nThe methods of a caret are designed to operate on nodes attached to the `origin`\nin the designated direction, not the `origin` itself. For example, this code is\na no-op because it will attach a node to the `origin`, and then remove the node\nthat was just attached.\n\n```ts\n// The origin is unaffected (other than being marked dirty)\ncaret.insert($createTextNode('no-op')).remove();\n```\n\n:::\n\n:::warning\n\nCarets are immutable, and designed for low-level usage. There is no attempt\nfor carets to automatically update based on changes to the document\n(this is a common source of bugs when working with `RangeSelection`).\nFunctions and methods that work with carets and are expected to change the\nstructure of the document will always return a possibly new caret.\n\nThe `origin` of a caret is the exact version of the object that it was\nconstructed with, all accessor methods on that origin will generally call\n`origin.getLatest()` so the operations will see the latest version.\n\n:::\n\n### NodeCaret\n\n`NodeCaret` is any `SiblingCaret` or any `ChildCaret`\n* Typically constructed with `$getChildCaretOrSelf($getSiblingCaret(origin, direction))`\n  which returns a `ChildCaret` when the origin is an `ElementNode`\n\n:::tip\n\nThis type does not include `TextPointCaret` or `TextPointCaretSlice`,\nso you will not have to consider those edge cases when you see this\nmore specific type.\n\n:::\n\n### SiblingCaret\n\n`SiblingCaret` is a caret that points towards a sibling of the origin\n\n* Constructed with `$getSiblingCaret(origin: LexicalNode, direction: CaretDirection)`\n* The `next` direction points towards the right\n* The `previous` direction points towards the left\n\n|                        | → direction: `'next'`      | ← direction: `'previous'`     |\n|------------------------|---------------------------|-------------------------------|\n| `getParentAtCaret()`   | `origin.getParent()`      | `origin.getParent()`          |\n| `getNodeAtCaret()`     | `origin.getNextSibling()` | `origin.getPreviousSibling()` |\n| `insert(node)`         | `origin.insertAfter(node)`| `origin.insertBefore(node)`   |\n\n### ChildCaret\n\n`ChildCaret` is a caret that points towards the first or last child of the origin\n\n* Constructed with `$getChildCaret(origin: ElementNode, direction: CaretDirection)`\n* The `next` direction points towards the first child\n* The `previous` direction points towards the last child\n\n|                        | ↘ direction: `'next'`      | ↙ direction: `'previous'`     |\n|------------------------|----------------------------|-------------------------------|\n| `getParentAtCaret()`   | `origin`                   | `origin`                      |\n| `getNodeAtCaret()`     | `origin.getFirstChild()`   | `origin.getLastChild()`       |\n| `insert(node)`         | `origin.splice(0, 0, node)`| `origin.append(node)`         |\n\n### PointCaret\n\n`PointCaret` is any `TextPointCaret`, `SiblingCaret` or `ChildCaret`. This\ntype can be used to represent any point in the document that `PointType` can represent.\n\n### TextPointCaret\n\n`TextPointCaret` is basically a `SiblingCaret` with a `TextNode` origin and an `offset` property\n* Constructed with `$getTextPointCaret(origin, direction, offset)`\n* The `offset` property is an absolute index into the string\n* The `next` direction implies all text content after `offset`\n* The `previous` direction implies all text content before `offset`\n* All methods that are also present on `SiblingCaret` behave in the same way\n\n### TextPointCaretSlice\n\n`TextPointCaretSlice` is a wrapper for `TextPointCaret` that provides a signed `distance`.\n\n* Constructed with `$getTextPointCaretSlice(caret, distance)`\n* `$splitTextPointCaretSlice(slice, selection?)` isolates the slice in a text node,\n  splitting at its boundaries when needed. It returns `null` for an empty slice\n  and the original node for a whole-node slice. Pass the range selection that\n  produced the slice to preserve its endpoints, including element points and\n  detached selections. Like `TextNode.splitText`, it does not treat token or\n  segmented nodes as atomic.\n* There are convenience methods like `removeTextSlice()` and `getTextContent()`,\n  so it's not generally necessary to know the implementation details here\n* `Math.min(caret.offset, caret.offset + distance)` refers to the start offset of the slice\n* `Math.max(caret.offset, caret.offset + distance)` refers to the end offset of the slice\n* The `direction` of the caret is generally ignored when working with a\n  `TextPointCaretSlice`, the slice is in absolute string coordinates\n\n:::info\n\nThe property name `distance` was chosen because `length` and `size` are\ncommonly used on other data structures in JavaScript and Lexical, and they\nare overwhelmingly non-negative. While most uses of `distance` are also\nnon-negative, in some contexts such as computer graphics it is not uncommon\nto use\n[Signed distance functions](https://en.wikipedia.org/wiki/Signed_distance_function)\nwhere the distance metric is signed.\n\nIn SDF terms, the subset of the space is `[offset, ∞)`. Any coordinate less\nthan the `offset` boundary is a negative distance; otherwise the distance is\nnon-negative.\n\n:::\n\n### CaretRange\n\n`CaretRange` contains a pair of `PointCaret` that are in the same direction. It\nis equivalent in purpose to a `RangeSelection`, and is what you would generally\nuse for depth first traversals.\n\n* Constructed with `$getCaretRange(anchor, focus)`, `$caretRangeFromSelection(selection)`,\n  or `$extendCaretToRange(anchor)`\n* The `anchor` is the start of the range, generally where the selection originated,\n  and it is \"anchored\" in place because when a selection grows or shrinks only the\n  `focus` will be moved\n* The `focus` is the end of the range, where the blinking cursor is, it's the current\n  focus of the user\n* Anchor and focus must point in the same direction. The `anchor` points towards the first\n  node *in the range* and the focus points towards the first node *not in the range*\n* The `getTextSlices()` method is essential to handle the literal edge cases where\n  the anchor and/or focus are a `TextPointCaret`. These edges are *not* included\n  in the default caret iteration of the `CaretRange`.\n\n:::warning\n\nIf you are iterating a `CaretRange` you must consider the `getTextSlices()`\nseparately, they are not included in the iteration. This is so you don't have\nto consider `TextPointCaretSlice` at every step. They are literal edge cases\nthat can only be at the start and/or end and typically have special\ntreatment (splitting instead of removing, for example).\n\n:::\n\n## Traversal Strategies\n\n<!-- when you update the example code below, please update the tests in packages/lexical/src/caret/__tests__/unit/docs-traversals.test.ts -->\n\n### Adjacent Caret Traversals\n\nThe lowest level building block for traversals with NodeCaret is the adjacent caret\ntraversal, which is supported directly by methods of NodeCaret.\n\n`getAdjacentCaret()` - Gets a `SiblingCaret` for the node attached to\n  `origin` in direction. If there is no attached node, it will return `null`\n\n`getParentCaret(rootMode)` - Gets a `SiblingCaret` for the parent node\n  of `origin` in the same direction. If there is no parent node, or the parent\n  is a root according to `rootMode`, then it will return `null`. `rootMode`\n  may be `'root'` to only return `null` for `RootNode` or `'shadowRoot'` to\n  return `null` for `RootNode` or any `ElementNode` parent where\n  `isShadowRoot()` returns true\n\n`getChildCaret()` - Gets a `ChildCaret` for this origin, or `null` if the\n  origin is not an `ElementNode`. Will return `this` if the caret is already\n  a `ChildCaret`\n\nFor example, iterating all siblings:\n\n```ts\n// Note that NodeCaret<D> already implements Iterable<NodeCaret<D>> in this\n// way, so this function is not very useful. You can just use startCaret as\n// the iterable.\nfunction *$iterSiblings<D extends CaretDirection>(\n  startCaret: NodeCaret<D>\n): Iterable<SiblingCaret<LexicalNode, D>> {\n  // Note that we start at the adjacent caret. The start caret\n  // points away from the origin node, so we do not want to\n  // trick ourselves into thinking that that origin is included.\n  for (\n    let caret = startCaret.getAdjacentCaret();\n    caret !== null;\n    caret = caret.getAdjacentCaret()\n  ) {\n    yield caret;\n  }\n}\n```\n\n### Examples\n\nGiven the following document tree, here are some examples of using the\nadjacent node traversal:\n\nRoot\n* Paragraph A\n  * Text A1\n  * Link A2\n    * Text A3\n  * Text A4\n* Paragraph B\n  * Text B1\n* Paragraph C\n\n```ts\n// The root does not have sibling nodes\nconst carets = [...$getSiblingCaret($getRoot(), 'next')];\nexpect(carets).toEqual([]);\n```\n\n```ts\n// The adjacent node to a ChildNode is its first or last child\n// and is always a SiblingNode. It does not traverse deeper.\nconst carets = [...$getChildCaret($getRoot(), 'next')];\n\n// next starts at the first child\nexpect(carets).toEqual([\n  $getSiblingCaret(paragraphA, 'next'),\n  $getSiblingCaret(paragraphB, 'next'),\n  $getSiblingCaret(paragraphC, 'next'),\n]);\n\n// previous starts at the last child\nconst prevCarets = [...$getChildCaret($getRoot(), 'previous')];\nexpect(prevCarets).toEqual([\n  $getSiblingCaret(paragraphC, 'previous'),\n  $getSiblingCaret(paragraphB, 'previous'),\n  $getSiblingCaret(paragraphA, 'previous'),\n]);\n```\n\n```ts\n// The iteration starts at the node where the head of the \"arrow\"\n// is pointing, which is away from the origin (the tail of the \"arrow\").\nconst carets = [...$getSiblingCaret(paragraphB, 'next')];\nexpect(carets).toEqual([\n  $getSiblingCaret(paragraphC, 'next'),\n]);\n\nconst prevCarets = [...$getSiblingCaret(paragraphB, 'previous')];\nexpect(prevCarets).toEqual([\n  $getSiblingCaret(paragraphA, 'previous'),\n]);\n```\n\n### Depth First Caret Traversals\n\nThe strategy to do a depth-first caret traversal is to use an adjacent caret\ntraversal and immediately use a `ChildCaret` any time that an `ElementNode`\norigin is encountered. This strategy yields all possible carets, but each\nElementNode in the traversal may be yielded once or twice (a `ChildCaret` on\nenter, and a `SiblingCaret` on leave). Allowing you to see whether an\n`ElementNode` is partially included in the range or not is one of the\nreasons that this abstraction exists.\n\n```ts\nfunction *$iterCaretsDepthFirst<D extends CaretDirection>(\n  startCaret: NodeCaret<D>\n): Iterable<NodeCaret<D>> {\n  function step(prevCaret: NodeCaret<D>): null | NodeCaret<D> {\n    // Get the adjacent SiblingCaret\n    const nextCaret = prevCaret.getAdjacentCaret();\n    return (\n      // If there is a sibling, try and get a ChildCaret from it\n      (nextCaret && nextCaret.getChildCaret()) ||\n      // Return the sibling if there is one\n      nextCaret ||\n      // Return a SiblingCaret of the parent, if there is one\n      prevCaret.getParentCaret('root')\n    );\n  }\n  // You may add an additional check here, usually some specific\n  // caret to terminate the iteration with (such as the parent caret\n  // of startCaret):\n  //\n  //  `caret !== null || caret.is(endCaret)`\n  //\n  for (\n    let caret = step(startCaret);\n    caret !== null;\n    caret = step(caret)\n  ) {\n    yield caret;\n  }\n}\n```\n\nNormally this type of iteration would be done from a `CaretRange`, where you\nwould specify a precise end caret (focus).\n\n```ts\nfunction $iterCaretsDepthFirst<D extends CaretDirection>(\n  startCaret: NodeCaret<D>,\n  endCaret?: NodeCaret<D>,\n): Iterable<NodeCaret<D>> {\n  return $getCaretRange(\n    startCaret,\n    // Use the root as the default end caret, but you might choose\n    // to use startCaret.getParentCaret('root') for example\n    endCaret || $getSiblingCaret($getRoot(), startCaret.direction)\n  );\n}\n```\n\nTo get all nodes that are entirely selected between two carets:\n\n```ts\nfunction *$iterNodesDepthFirst<D extends CaretDirection>(\n  startCaret: NodeCaret<D>,\n  endCaret: NodeCaret<D> = $getChildCaret($getRoot(), startCaret.direction),\n): Iterable<LexicalNode> {\n  const seen = new Set<NodeKey>();\n  for (const caret of $getCaretRange(startCaret, endCaret)) {\n    const {origin} = caret;\n    if ($isChildCaret(caret)) {\n      seen.add(origin.getKey());\n    } else if (!$isElementNode(origin) || seen.has(origin.getKey())) {\n      // If the origin is an element and we have not seen it as a ChildCaret\n      // then it was not entirely in the CaretRange\n      yield origin;\n    }\n  }\n}\n```\n\n### Examples\n\nGiven the following document tree, here are some examples of using the\ndepth-first node traversal (with a `CaretRange`):\n\nRoot\n* Paragraph A\n  * Text A1\n  * Link A2\n    * Text A3\n  * Text A4\n* Paragraph B\n  * Text B1\n* Paragraph C\n\n```ts\n// A full traversal of the document from root\nconst carets = [...$getCaretRange(\n  // Start with the arrow pointing towards the first child of root\n  $getChildCaret($getRoot(), 'next'),\n  // End when the arrow points away from root\n  $getSiblingCaret($getRoot(), 'next'),\n)];\nexpect(carets).toEqual([\n  $getChildCaret(paragraphA, 'next'),   // enter Paragraph A\n  $getSiblingCaret(textA1, 'next'),\n  $getChildCaret(linkA2, 'next'),       // enter Link A2\n  $getSiblingCaret(textA3, 'next'),\n  $getSiblingCaret(linkA2, 'next'),     // leave Link A2\n  $getSiblingCaret(textA4, 'next'),\n  $getSiblingCaret(paragraphA, 'next'), // leave Paragraph A\n  $getChildCaret(paragraphB, 'next'),   // enter Paragraph B\n  $getSiblingCaret(textB1, 'next'),\n  $getSiblingCaret(paragraphB, 'next'), // leave Paragraph B\n  $getChildCaret(paragraphC, 'next'),   // enter Paragraph C\n  $getSiblingCaret(paragraphC, 'next'), // leave Paragraph C\n]);\n```\n\n## Future Direction\n\nIt's expected that higher-level abstractions will be built on top of this\noutside of the core, either in @lexical/utils or a separate companion package.\nThis is just designed to be the lowest-level layer with a consistent and\ntype-safe interface. That sort of abstraction will probably look a little bit\nlike cheerio or jQuery, but for working with Lexical documents. It is not\nexpected that more abstractions will be added to the core.\n\nIn order to reduce code size and eliminate bugs, more of the core will be\nrefactored to use NodeCaret internally.\n\nOnce this happens, it's possible that the internal structure of PointType\nand/or RangeSelection may change to accommodate NodeCaret, as it is more\nresilient to document changes (only changes that directly affect the\norigin node will \"break\" the point). A simple version of this would be to\ncreate a caret any time that the point changes, and use that caret\nas a fallback if the selection would otherwise be lost.\n\nIt may be the case that NodeCaret will become the lowest level API, working\ndirectly with private LexicalNode/ElementNode internals. When/if that happens,\nthe methods on LexicalNode will remain for backwards compatibility,\nbut overriding them will not be supported. It isn't particularly safe to\noverride them as-is anyway, and these overrides are frequently the\nroot cause of bugs (e.g. parents that remove themselves after an operation\non a child, causing the point to be lost unless the caller was sophisticated\nenough to store the array of parents).\n\n## History\n\nBefore NodeCaret, Lexical's core API offered a relatively low-level DOM-like\ninterface for working with nodes and traversing them. It has accumulated\nmany functions over time for performing various kinds of traversals around\nthe tree (finding ancestors, children, depth, siblings, etc.), but most of\nthem are not implemented in a way that makes them easy to combine\nefficiently, and many of them have edge cases that are difficult to avoid\nand can't really be addressed without breaking compatibility.\n\nMany of these functions also have a lot of edge cases, particularly around\nassuming the reference nodes are inclusive. Many are also left-to-right\nbiased, don't offer an iterative version that can be aborted early or\nconsumed on the fly, etc.\n\nRefactoring many of these to use something like `PointType` would almost\nbe sufficient for many of these use cases, but the representation of\nthat type is inefficient and error-prone as any mutation to the tree\nrequires that each point be manually recomputed. `PointType` is also\ndirectionless, forcing a specific left-to-right bias into most APIs.\n`RangeSelection` can be used in many cases because a direction can\nbe inferred from any two different points, but that collapses with\na single point. It's also impractical to use `RangeSelection`\nconcurrently with mutations due to the problems with `PointType`.\n\nNodeCaret was born out of frustration with these APIs and a desire\nto unify it all in a coherent way to simplify and reduce errors in\nthe core.\n\n## Terminology\n\n### Caret\n\nThe term Caret was chosen because it is concise and specific term\nspecific to a point in a text document. Lexical is \"an extensible text editor\nframework\" so it makes sense that navigation in the document would use\nterms relevant to text. Most other terms such as Cursor or Point\nalready have meanings in Lexical and/or are less specific.\n\nSee also:\n- [Caret](https://developer.mozilla.org/en-US/docs/Glossary/Caret)\n- [Caret navigation](https://en.wikipedia.org/wiki/Caret_navigation)\n\n### Origin\n\nThe origin is the reference node for a NodeCaret. Absolute coordinates\nare determined by combining this origin node and an \"arrow\" that points\ntowards to where the adjacent node is (or could be). The \"arrow\" is\ndetermined by the `direction` and `type` of the caret.\n\nIn a way this \"arrow\" is considered to be something like a unit vector\nto indicate the direction, and adding it to an origin allows you to specify\nan absolute location relative to that origin. Unlike the other coordinate\nsystems available in Lexical, it does not need recomputing whenever\nsiblings or a parent changes, so long as the origin node is still attached.\n\nSee also:\n- [Origin](https://en.wikipedia.org/wiki/Origin_(mathematics))\n- [Unit Vector](https://en.wikipedia.org/wiki/Unit_vector)\n\n### ChildCaret / SiblingCaret\n\nThese were chosen because they match the existing methods on `ElementNode`\nand `LexicalNode` (`getFirstChild`, `getNextSibling`, etc.)\n\n### Direction\n\n`'next'` and `'previous'` were chosen for direction mostly to match the\nexisting methods such as `getNextSibling()` that exist in DOM and in Lexical.\nUsing other words such as `'left'` and `'right'` would be ambiguous since\ntext direction can be bidirectional and already uses the terms left-to-right\nand right-to-left.\n\n### Distance\n\nThe property name `distance` was chosen for `TextPointCaretSlice` because\n`length` and `size` are commonly used on other data structures in JavaScript\nand Lexical, and they are overwhelmingly non-negative. While most uses of\n`distance` are also non-negative, in some contexts such as computer graphics\nit is not uncommon to use\n[Signed distance functions](https://en.wikipedia.org/wiki/Signed_distance_function)\nwhere the distance metric is signed.\n\nIn SDF terms, the subset of the space is `[offset, ∞)`. Any coordinate less\nthan the `offset` boundary is a negative distance; otherwise the distance is\nnon-negative.\n","packages/lexical-website/docs/design.md":"# Lexical's Design\n\n> Note: this document is still a work-in-progress and things are likely still in flux.\n\nLexical was built from the ground up to be a lean and extensible text-editing framework.\n\nYou can think of Lexical as more of a text editing engine, rather than a monolithic text\neditor that provides everything out-of-the-box. It might help to think of Lexical as\na framework like React – where React provides some hooks and a reconciler, and then you\nbuild the things you need on top, rather than have them included by default.\n\nThe main concept of Lexical is the editor state. Lexical uses a double-buffering technique\nto ensure consistency and reliability. There are never more than two editor states in play.\nThe \"current\" editor state represents what you can visibly see on screen and the \"pending\"\neditor state is what is currently being constructed to be shown in the future. Once the\n\"pending\" editor state is ready, it swaps and becomes the new \"current\" editor state.\n\nLexical's core concerns itself with four main concerns:\n\n- Updates: the act of making changes to editor state\n- Node Transforms: the process of acting on ongoing updates\n- Reconciliation: the process of patching the DOM with the latest editor state\n- Listening/Commands: the process of reacting to changes that occur internally\n\nAdditionally, Lexical uses DOM mutation observers to ensure that any outside changes to\nthe editor DOM element are either reverted back to Lexical's current editor state, or are\ncommunicated as intents that cause further updates to the editor state (text changes).\n\nThis separation was done to ensure developers have a way of applying custom logic with the\nimplementation and framework of their choice. This makes it possible to use Lexical with\nReact, or any other JavaScript web framework/library. This also makes it easier to tackle\nproblems around collaboration and undo/redo, where you might have an alternative model involved.\nInstead of hoping for the text editor to support what you want, you can work with the editor\nto make what you want.\n\nThis design can make getting started a bit more complex in certain cases, but as we've been\nable to demonstrate with our `@lexical/react` plugins and hooks – you can get started, in the\nform of an out-of-the-box experience.\n","packages/lexical-website/docs/error.md":"---\nid: error\ntitle: Error Code\n---\nimport ErrorCodePage from '@site/src/components/ErrorCodePage';\n\n# Error Code\n<ErrorCodePage/>","packages/lexical-website/docs/extensions/defining-extensions.md":"# Defining Extensions\n\nAn extension is an plain JavaScript object that complies with the\n[LexicalExtension](/docs/api/modules/lexical#lexicalextension) interface.\nThe [defineExtension](/docs/api/modules/lexical#defineextension) function\nis merely an identity function to make TypeScript inference convenient.\nIt will be compiled down to the following, and likely optimized out by your\nbundler or minifier:\n\n```js\nfunction defineExtension(extension) {\n  return extension;\n}\n```\n\nExtensions *must* be stable references. It is most useful to define them at\nmodule scope, but if for some reason you have to define it inside of a\nReact component you'll need some way to make it stable such as with\n`useState`, `useMemo`, or `useRef` as appropriate.\n\n## Required Properties\n\n### name\n\nThe only required property for an extension is `name`. The name is a string\nthat should be unique to that extension your editor. It is best practice to\nuse a scope for your project or organization to ensure that reusable\nextensions don't conflict with each other.\n\nThe general practice in the lexical repository is to use the name of the\npackage as the extension name when it only makes sense to export a single\nextension, (e.g. `DragonExtension` has the name `@lexical/dragon`), or\nwith an additional path if multiple extensions are exported (e.g.\n`AutoFocusExtension` has the name `@lexical/extension/AutoFocus`).\n\n## InitialEditorConfig\n\nExtensions can specify configuration overrides for the editor, these are\ndefined by the\n[InitialEditorConfig](/docs/api/modules/lexical#initialeditorconfig)\ninterface. See the API docs for the full list of properties. Every property\nhere has some default.\n\n### Merged properties\n\nFor an extension designed to be used as a dependency, it can also be useful\nto specify properties that are merged such as\n`html` (override HTML import/export),\n`nodes` (register new nodes or node overrides with the editor),\nor `theme` (specify classes to be used for nodes provided by lexical).\n\n```ts\nexport const CodeExtension = defineExtension({\n  name: '@lexical/code',\n  nodes: () => [CodeNode, CodeHighlightNode],\n});\n```\n\n### Root properties\n\nThere are other properties that are more useful for the \"root\" extension\nthat you provide to\n[buildEditorFromExtensions](/docs/api/modules/lexical_extension#buildeditorfromextensions)\nor the `extension` prop of\n[LexicalExtensionComposer](/docs/api/modules/lexical_react_LexicalExtensionComposer#lexicalextensioncomposer)\nwhere you would specify properties that can only be meaningfully set\nonce such as `$initialEditorState`, `editable`, `onError`, `namespace` or `parentEditor`.\n\nThere is no requirement that these properties be set from any specific level\nof the extension hierarchy, or at all, but it is only meaningful to set them\nonce per editor.\n\n```ts\nconst editor = buildEditorFromExtensions(\n  defineExtension({\n    name: \"@example/basic-rich-text-editor\",\n    namespace: \"basic-rich-text-editor\",\n    dependencies: [RichTextExtension],\n    register: (editor: LexicalEditor) => {\n      console.log(\"Editor Created\");\n      return () => console.log(\"Editor Disposed\");\n    },\n  }),\n);\n```\n\n## Extension Dependencies\n\nLexical Extensions have two ways for extensions to depend on and specify\nconfiguration for each other:\n\n- [dependencies](#dependencies) are direct (by reference)\n- [peerDependencies](#peerdependencies) are indirect (by name) and optional\n\nThe exception to this rule is with `peerDependencies`. These are optional\ndependencies\n\n### dependencies\n\nThe `dependencies` property is an array of extensions by reference that your\nextension depends on. For example, if your extension uses React then it should\ndepend on the `ReactExtension`.\n\nDependencies form a directed acyclic graph. This means that there must not\nbe any circular dependencies. If extension A depends on extension B then\nthere must not be any dependency path from B to A\n(or from A to A).\n\nThis array can contain either direct references to extensions, or calls to\n[configExtension](/docs/api/modules/lexical#configextension).\n\n```ts\nexport const ExampleExtension = defineExtension({\n  name: \"@example/extension\",\n  dependencies: [\n    SomeExtension,\n    configExtension(ReactExtension, {\n      decorators: [<ExampleDecorator />]\n    }),\n  ],\n});\n```\n\n### peerDependencies\n\nThe `peerDependencies` property is an array of optional extensions by name.\nThey are not requirements, but specifying them here allows your extension\nto find them at runtime and override configuration for them if they are\nbuilt with the editor. You can use\n[declarePeerDependency](/docs/api/modules/lexical#declarepeerdependency) to\nbuild this array with type inference.\n\nSince these are optional and by name, an extension may declare a peer it does\nnot import. The graph must still be acyclic: peer edges take part in the same\ntopological sort as `dependencies`, so two extensions that peer-depend on each\nother and are both present will fail to build.\n\nThis is rarely needed in practice and is considered an advanced use case,\ntypically to avoid a direct import or dependency loop. See\n[Peer Dependencies](/docs/extensions/peer-dependencies) for more detail.\n\n### conflictsWith\n\nThe `conflictsWith` property is an array of extensions by name that are\nknown to conflict with this extension. For example, it does not make\nsense to have\n[RichTextExtension](/docs/api/modules/lexical_rich-text#richtextextension) and\n[PlainTextExtension](/docs/api/modules/lexical_plain-text#plaintextextension)\nin the same editor.\n\n```ts\nexport const PlainTextExtension = defineExtension({\n  conflictsWith: ['@lexical/rich-text'],\n  dependencies: [\n    DragonExtension,\n    NormalizeInlineElementsExtension,\n    NormalizeTripleClickSelectionExtension,\n  ],\n  name: '@lexical/plain-text',\n  register: registerPlainText,\n});\n```\n\nThis is rarely needed in practice and is considered an advanced use case,\nbut it can provide helpful early errors when it is needed.\n\n## Phases\n\nThe construction of an editor using extensions is done in sequential phases,\nand the following properties are useful ways to hook into that process at\nspecific points.\n\n### config\n\n`config` is an object that is the default configuration for your extension,\nproperties of this object can be overridden by other extensions using\n[configExtension](/docs/api/modules/lexical#configextension) or\n[declarePeerDependency](/docs/api/modules/lexical#declarepeerdependency).\n\nThis object is used in later phases to build the init and/or output.\n\nThe config phase happens before the editor is constructed.\n\n### mergeConfig\n\n`mergeConfig` is a function that can be used to merge configuration\nwith its overrides if you need something more fine-grained than a\nshallow object merge, such as concatenating arrays. For example:\n\n```ts\ninterface StringArrayConfig {\n  array: string[];\n}\nconst StringArrayExtension = defineExtension({\n  config: safeCast<StringArrayConfig>({array: []}),\n  name: \"@example/StringArray\",\n  mergeConfig(a, b) {\n    const config = shallowMergeConfig(a, b);\n    if (b.array) {\n      config.array =\n        b.array.length > 0\n          ? [...a.array, ...b.array]\n          : a.array;\n    }\n    return config;\n  },\n});\n```\n\nThe default implementation is\n[shallowMergeConfig](/docs/api/modules/lexical#shallowmergeconfig),\nmost extensions will not need to override this.\n\n### init\n\nThe init phase happens before the editor is constructed, but after\nthe merged editor configuration is available and all extensions are\nconfigured. It can be used to reference configuration\nfrom peers, compute data that should be available during\n[build](#build), and is generally a last resort for making mutations\nto any extension or editor configuration before the editor is\ncreated.\n\nThe result of this function is available in later phases.\n\nThis is rarely needed in practice and is considered an advanced use case.\n\n### build\n\nThe build phase happens after the editor is constructed — it receives the\n`LexicalEditor` — but before [register](#register), and after\n[config](#config) and [init](#init). The return value of the `build`\nphase is `output` and is available for later phases.\n\n`output` is generally how extensions provide functionality to each\nother and to the nodes in your application. A very common use case is\nto build signals from configuration with `namedSignals` so that the\nbehavior of your extension can be modified at runtime (e.g. `disabled`\nis a very common use case).\n\nOther use cases for build's `output` are to provide\nshared data structures, typed theme configuration, references to the\ncommands that the extension implements, etc.\n\n```ts\nexport interface TabIndentationConfig {\n  disabled: boolean;\n  maxIndent: null | number;\n  /**\n   * By default, indents are set on all elements for which the\n   * {@link ElementNode.canIndent} returns true. This option allows you to set\n   * indents for specific nodes without overriding the method for others.\n   */\n  $canIndent: CanIndentPredicate;\n}\nexport const TabIndentationExtension = defineExtension({\n  build(editor, config, state) {\n    return namedSignals(config);\n  },\n  config: safeCast<TabIndentationConfig>({\n    $canIndent: $defaultCanIndent,\n    disabled: false,\n    maxIndent: null,\n  }),\n  name: '@lexical/extension/TabIndentation',\n  register(editor, config, state) {\n    const {disabled, maxIndent, $canIndent} = state.getOutput();\n    return effect(() => {\n      if (!disabled.value) {\n        return registerTabIndentation(editor, maxIndent, $canIndent);\n      }\n    });\n  },\n});\n```\n\n### register\n\nThis happens after the editor has been constructed. This is where you will\nregister any commands, listeners, etc. that your extension needs. It can use\nthe result of `init` or `build` via `state.getInitResult()` and\n`state.getOutput()` respectively.\n\nThe return value is a dispose function, typically the result of `mergeRegister`.\n\n### afterRegistration\n\nThis happens after [register](#register) for every extension has been called,\nso all commands should be registered. This is the phase when the\n`$initialEditorState` is applied to the editor by `InitialStateExtension`,\nso `editor.setRootElement` should be called no sooner than this phase (it may\nof course be called after the editor is built, outside of extensions).\n\nThe return value is a dispose function, typically the result of\n`mergeRegister`.\n","packages/lexical-website/docs/extensions/design.md":"# Design Doc\n\nSee also the initial PR [#7706](https://github.com/facebook/lexical/pull/7706)\n\n## Guiding principles\n\n### Minimize boilerplate\n\nWherever possible, sensible defaults should be provided and it must be\npossible to override them. This is why a `Extension` has the `config` property\nwhich specifies the initial configuration.\n\n### Full backwards compatibility\n\nAlmost anything that you do with the editor or a legacy React plug-in should\njust work. Upgrading to use a `Extension` should be a simple change.\n\n### Minimize change required to add an Extension\n\nIdeally, adding an extension should be two or three lines of code:\n\n- Import it\n- Add it as a dependency (with config if necessary)\n- Add UI related code (only if needed, e.g. has to be rendered in a specific place in the DOM)\n\n### Avoid invalid states\n\nTypeScript goes a long way here, but it's only as good as the API design.\nFor example, splitting up the workflow for an Extension to have separate `Config`,\n`Init` and `Output` types was something that evolved over time. In earlier\nprototypes, `Config` was overloaded to maintain all of that state, but it\nrequired a lot more runtime support and type assertions to check that\nthe state was properly initialized for that phase.\n\n### Try to avoid undecipherable type errors\n\nThere's only so much that can be done to make readable error messages\nout of complex type expressions, so the API is designed to avoid them\nwhen possible. See also [microsoft/TypeScript#23689](https://github.com/microsoft/TypeScript/issues/23689).\n\n## Inspiration\n\n### [ESLint Flat Plugins](https://eslint.org/docs/latest/extend/plugin-migration-flat-config)\n\nThese have many of the same constraints as this project, although of course\nare used for a very different purpose\n\n### [MDXEditor](https://mdxeditor.dev/)\n\nThis editor is based on Lexical, but provides its own mechanism for plugins\nbased on a third party state management library. These plugins don't\nreally expose any type-safe configuration.\n\n### [@payloadcms/richtext-lexical](https://www.npmjs.com/package/@payloadcms/richtext-lexical)\n\nAnother Lexical based editor framework for use inside the\n[Payload](https://payloadcms.com/) CMS. It also doesn't really expose type-safe\nconfiguration, but it does seem to have quite a lot of features and seems\nwell-designed for laziness and SSR. Features are conceptually similar to Extensions.\n\n### [Effect](https://effect.website/)\n\nThis library is pretty much state of the art for type safety with a focus on\ncomposition and usability.\n\n### Tighter integration with LexicalEditor\n\nThis package was originally prototyped as an entirely optional feature,\nas adoption increases it's expected that more of Lexical will directly\ndepend on it.\n\n## Intentional trade-offs\n\n### No compile-time support for dependency resolution\n\nThe current theory is that it would require too much TypeScript\nin order to carry around the list of all dependency names that\nexist in the graph, and would likely add another type argument\nto `LexicalExtension`.\n\nThe features that this blocks are:\n\n- Compile-time support for detecting Extension conflicts. Detecting that two\n  extensions defined with the same name (but are not identical object\n  references, because a dependency can be shared!) would also be quite the\n  undertaking in TypeScript. This is automatically detected at runtime by\n  the builder.\n- Compile-time support for required configuration without defaults.\n  An extension can implement this at runtime in `init` or `register`.\n- Compile-time support for required peer dependencies. A use case for this\n  would be the requirement of a `ReactProviderExtension` provided by either\n  `LexicalExtensionComposer` or `ReactPluginHostExtension`. An extension can implement this\n  at runtime in `init` or `register`.\n\nGenerally speaking, all of these are already surfaced as runtime errors\nwhile building the editor with sufficient information to quickly track\ndown the root cause.\n\n## Known Missing Features\n\n### Direct support for devtools\n\nThis is a TODO, the infrastructure was designed with this in mind.\n\n### Documented patterns for RSC/SSR/Headless\n\nHaving a known peer dependency that is used to declare SSR may help\nguide future Extension development. In many cases there are decorator nodes\nthat have React dependencies that you do not want to (or can not) render\nin an SSR context. For example, RSC can not be supported anywhere that\n`React.Context` is used which is everywhere that React is currently used\nin Lexical.\n\nAnother option would be to build separate entrypoints for the SSR use case,\nwhich is more of an RSC specific strategy, but we could encourage people\nto use those import conditions even in a Vanilla JS or some other bespoke\nSSR use case. That strategy may make it hard to support including both\n\"headless\" and \"non-headless\" in the same module.\n","packages/lexical-website/docs/extensions/included-extensions.md":"# Included Extensions\n\n[@lexical/a11y](/docs/api/modules/lexical_a11y)\n\nFramework-agnostic accessibility extensions. See [Keyboard Accessibility](/docs/concepts/keyboard-accessibility) for the full contract and the matching `@lexical/react` adapters.\n\n- [AriaLiveRegionExtension](/docs/api/modules/lexical_a11y#arialiveregionextension) - Owns a visually hidden `aria-live` region and exposes a stable `announce` sink (WAI-ARIA status messages, WCAG 4.1.3)\n- [EditorModeAnnounceExtension](/docs/api/modules/lexical_a11y#editormodeannounceextension) - Announces editable / read-only transitions through the live region\n- [FocusManagerExtension](/docs/api/modules/lexical_a11y#focusmanagerextension) - Editor ↔ toolbar focus jump (Alt+F10 to the toolbar, Escape back to the editor), reference-counted per toolbar\n- [FocusTrapExtension](/docs/api/modules/lexical_a11y#focustrapextension) - Traps Tab / Shift+Tab focus inside one or more containers (modal dialogs), reference-counted per container\n- [HistoryAnnounceExtension](/docs/api/modules/lexical_a11y#historyannounceextension) - Announces undo / redo through the live region\n- [RovingTabIndexExtension](/docs/api/modules/lexical_a11y#rovingtabindexextension) - WAI-ARIA roving-tabindex pattern for toolbars / groups, reference-counted per container\n\n[@lexical/clipboard](/docs/api/modules/lexical_clipboard)\n\n- [ClipboardDOMImportExtension](/docs/api/modules/lexical_clipboard#clipboarddomimportextension) - Routes `text/html` pastes and drops through the `DOMImportExtension` pipeline (experimental)\n- [ClipboardImportExtension](/docs/api/modules/lexical_clipboard#clipboardimportextension) - Configurable per-MIME-type clipboard import for paste and drop (experimental)\n- [GetClipboardDataExtension](/docs/api/modules/lexical_clipboard#getclipboarddataextension) - Configurable serialization of the selection into clipboard MIME types for copy and drag\n\n[@lexical/code](/docs/api/modules/lexical_code)\n\n- [CodeExtension](/docs/api/modules/lexical_code#codeextension) - CodeNode (code blocks)\n- [CodeIndentExtension](/docs/api/modules/lexical_code#codeindentextension) - CodeNode tab key indentation (code blocks)\n\n[@lexical/code-prism](/docs/api/modules/lexical_code-prism)\n\n- [CodePrismExtension](/docs/api/modules/lexical_code-prism#codeprismextension) - Highlighting with Prism for CodeNode\n\n[@lexical/code-shiki](/docs/api/modules/lexical_code-shiki)\n\n- [CodeShikiExtension](/docs/api/modules/lexical_code-shiki#codeshikiextension) - Highlighting with Shiki for CodeNode\n\n[@lexical/dragon](/docs/api/modules/lexical_dragon)\n\n- [DragonExtension](/docs/api/modules/lexical_dragon#dragonextension) - Dragon (speech to text) support, included by default with `RichTextExtension` and `PlainTextExtension`\n\n[@lexical/extension](/docs/api/modules/lexical_extension)\n\n- [AutoFocusExtension](/docs/api/modules/lexical_extension#autofocusextension) - Focus the editor when it is created (e.g. on page load)\n- [ClearEditorExtension](/docs/api/modules/lexical_extension#cleareditorextension) - Implementation for the `CLEAR_EDITOR_COMMAND`\n- [ClickAfterLastBlockExtension](/docs/api/modules/lexical_extension#clickafterlastblockextension) - Inserts a paragraph when clicking below a last block that can not accommodate the click (e.g. a DecoratorNode or TableNode)\n- [DecoratorTextExtension](/docs/api/modules/lexical_extension#decoratortextextension) - DecoratorTextNode support, sets the format and CSS classes for the DOM container\n- [EditorStateExtension](/docs/api/modules/lexical_extension#editorstateextension) - Provide EditorState as a signal (alternative to `registerUpdateListener`)\n- [HMRExtension](/docs/api/modules/lexical_extension#hmrextension) - Preserves editor state, selection, editability and undo/redo history across Hot Module Replacement (HMR) cycles\n- [HorizontalRuleExtension](/docs/api/modules/lexical_extension#horizontalruleextension) - HorizontalRuleNode (`<hr>` tag)\n- [IMEExtension](/docs/api/modules/lexical_extension#imeextension) - Centralizes IME composition state as signals\n- [InitialStateExtension](/docs/api/modules/lexical_extension#initialstateextension) - Sets the initial state of the editor (always included)\n- [KeyboardShortcutsExtension](/docs/api/modules/lexical_extension#keyboardshortcutsextension) - Dispatches a table of keyboard shortcuts from a single compiled `KEY_DOWN_COMMAND` listener; the table is merged across the extension graph by name (experimental)\n- [NestedEditorExtension](/docs/api/modules/lexical_extension#nestededitorextension) - Configures an editor as a nested editor of a parent editor (theme inheritance, optionally editable state)\n- [NodeSelectionExtension](/docs/api/modules/lexical_extension#nodeselectionextension) - Tracks selection, typically for DecoratorNodes\n- [NodeSelectionDataSelectedExtension](/docs/api/modules/lexical_extension#nodeselectiondataselectedextension) - Mirrors a node's `NodeSelection` membership onto its host DOM as a `data-selected` attribute so CSS can outline selected `ElementNode` hosts; configured per node type (experimental)\n- [NormalizeInlineElementsExtension](/docs/api/modules/lexical_extension#normalizeinlineelementsextension) - Removes empty inline elements, included by default with `RichTextExtension` and `PlainTextExtension`\n- [NormalizeTripleClickSelectionExtension](/docs/api/modules/lexical_extension#normalizetripleclickselectionextension) - Corrects over-selection after triple click events, included by default with `RichTextExtension` and `PlainTextExtension`\n- [PreventSelectAllExtension](/docs/api/modules/lexical_extension#preventselectallextension) - Prevents select all (Ctrl/Cmd+A) inside input/textarea elements from selecting the editor content, included by default with `SelectBlockExtension`\n- [RootElementExtension](/docs/api/modules/lexical_extension#rootelementextension) - Exposes the editor's root element as a reactive `Signal<HTMLElement | null>`\n- [SelectBlockExtension](/docs/api/modules/lexical_extension#selectblockextension) - Select all (Ctrl/Cmd+A) selects the nearest block element first, pressing it again selects the whole document\n- [SelectionAlwaysOnDisplayExtension](/docs/api/modules/lexical_extension#selectionalwaysondisplayextension) - Highlights selected content even when the editor is not focused\n- [TabIndentationExtension](/docs/api/modules/lexical_extension#tabindentationextension) - Changes Tab key to insert tabs and indent instead of natively focusing the next field\n- [WatchEditableExtension](/docs/api/modules/lexical_extension#watcheditableextension) - Exposes the editor's editable state as a reactive `Signal<boolean>`\n\n[@lexical/hashtag](/docs/api/modules/lexical_hashtag)\n\n- [HashtagExtension](/docs/api/modules/lexical_hashtag#hashtagextension) - HashtagNode\n\n[@lexical/history](/docs/api/modules/lexical_history)\n\n- [HistoryExtension](/docs/api/modules/lexical_history#historyextension) - History support (undo/redo)\n- [SharedHistoryExtension](/docs/api/modules/lexical_history#sharedhistoryextension) - History sharing between a parent editor and its nested editors\n\n[@lexical/html](/docs/api/modules/lexical_html)\n\n- [CoreImportExtension](/docs/api/modules/lexical_html#coreimportextension) - DOM import rules for the core nodes, included implicitly by node-providing extensions (experimental)\n- [DOMImportExtension](/docs/api/modules/lexical_html#domimportextension) - Extension-based replacement for `importDOM`/`DOMConversion`, rules are compiled into a dispatcher at editor build time (experimental)\n- [DOMRenderExtension](/docs/api/modules/lexical_html#domrenderextension) - Overrides the DOM render and export behavior for an editor (experimental)\n\n[@lexical/link](/docs/api/modules/lexical_link)\n\n- [AutoLinkExtension](/docs/api/modules/lexical_link#autolinkextension) - AutoLinkNode, note that matchers have to be manually configured\n- [ClickableLinkExtension](/docs/api/modules/lexical_link#clickablelinkextension) - Toggle between lexical selection and native click to open behavior for links\n- [LinkExtension](/docs/api/modules/lexical_link#linkextension) - LinkNode (`<a>` tag)\n\n[@lexical/list](/docs/api/modules/lexical_list)\n\n- [ListExtension](/docs/api/modules/lexical_list#listextension) - ListNode, ListItemNode\n- [CheckListExtension](/docs/api/modules/lexical_list#checklistextension) - Checklist support for ListNode and ListItemNode\n\n[@lexical/mark](/docs/api/modules/lexical_mark)\n\n- [MarkExtension](/docs/api/modules/lexical_mark#markextension) - MarkNode\n\n[@lexical/mdast](/docs/api/modules/lexical_mdast)\n\nExperimental micromark/mdast-based Markdown support (every extension in this package is experimental). See [Markdown with @lexical/mdast](/docs/serialization/markdown-mdast) for the full story.\n\n- [MdastExtension](/docs/api/modules/lexical_mdast#mdastextension) - The configurable core registry, exposing both Markdown import and export\n- [MdastHeadingExtension](/docs/api/modules/lexical_mdast#mdastheadingextension) - ATX & setext headings (HeadingNode)\n- [MdastBlockquoteExtension](/docs/api/modules/lexical_mdast#mdastblockquoteextension) - Block quotes (QuoteNode)\n- [MdastRichTextExtension](/docs/api/modules/lexical_mdast#mdastrichtextextension) - Bundle of heading + blockquote\n- [MdastListExtension](/docs/api/modules/lexical_mdast#mdastlistextension) - Ordered/unordered lists (ListNode, ListItemNode)\n- [MdastTaskListExtension](/docs/api/modules/lexical_mdast#mdasttasklistextension) - GFM task lists (`- [x]`), layered on MdastListExtension\n- [MdastCodeExtension](/docs/api/modules/lexical_mdast#mdastcodeextension) - Fenced & indented code blocks (CodeNode)\n- [MdastLinkExtension](/docs/api/modules/lexical_mdast#mdastlinkextension) - Links, CommonMark autolinks, reference links (LinkNode)\n- [MdastAutolinkLiteralExtension](/docs/api/modules/lexical_mdast#mdastautolinkliteralextension) - GFM literal autolinks (bare URLs in prose)\n- [MdastHorizontalRuleExtension](/docs/api/modules/lexical_mdast#mdasthorizontalruleextension) - Thematic breaks (HorizontalRuleNode)\n- [MdastStrikethroughExtension](/docs/api/modules/lexical_mdast#mdaststrikethroughextension) - GFM strikethrough (text format)\n- [MdastTableExtension](/docs/api/modules/lexical_mdast#mdasttableextension) - GFM tables (TableNode, ...)\n- [MdastCommonMarkExtension](/docs/api/modules/lexical_mdast#mdastcommonmarkextension) - Bundle of the six CommonMark extensions\n- [MdastGfmExtension](/docs/api/modules/lexical_mdast#mdastgfmextension) - Bundle of the four GFM extensions\n- [MdastShadowRootQuoteExtension](/docs/api/modules/lexical_mdast#mdastshadowrootquoteextension) - Opt-in: blockquotes as block containers with full-fidelity nested content\n- [MdastHtmlExtension](/docs/api/modules/lexical_mdast#mdasthtmlextension) - Opt-in: raw HTML in Markdown routed through the `@lexical/html` DOM import rules (Markdown inside the tags included, GitHub-style), plus generic HTML-encoded export via `$exportViaDOM` / `rawHtmlBlock`\n- [MdastShortcutsExtension](/docs/api/modules/lexical_mdast#mdastshortcutsextension) - Streaming Markdown typing shortcuts, driven by the same grammar as import\n\n[@lexical/overflow](/docs/api/modules/lexical_overflow)\n\n- [OverflowExtension](/docs/api/modules/lexical_overflow#overflowextension) - OverflowNode\n\n[@lexical/plain-text](/docs/api/modules/lexical_plain-text)\n\n- [PlainTextExtension](/docs/api/modules/lexical_plain-text#plaintextextension) - Plain text editor, the return key creates a LineBreakNode by default (one ParagraphNode per document)\n\n`@lexical/react`\n\n- [ReactExtension](/docs/api/modules/lexical_react_ReactExtension#reactextension)\n- [ReactPluginHostExtension](/docs/api/modules/lexical_react_ReactPluginHostExtension#reactpluginhostextension)\n- [ReactProviderExtension](/docs/api/modules/lexical_react_ReactProviderExtension#reactproviderextension)\n- [TreeViewExtension](/docs/api/modules/lexical_react_TreeViewExtension#treeviewextension)\n\n[@lexical/rich-text](/docs/api/modules/lexical_rich-text)\n\n- [RichTextExtension](/docs/api/modules/lexical_rich-text#richtextextension) - Rich Text editor (QuoteNode, HeadingNode), the return key creates a ParagraphNode by default (multiple ParagraphNode per document). Includes configurable `escapeFormatTriggers` to escape text formatting (e.g. code) at text node boundaries, and `shouldHandlePasteAsFiles` to control whether files win over incidental text/HTML clipboard entries on paste\n\n[@lexical/table](/docs/api/modules/lexical_table)\n\n- [TableExtension](/docs/api/modules/lexical_table#tableextension) - TableNode, TableRowNode, TableCellNode\n\n[@lexical/tailwind](/docs/api/modules/lexical_tailwind)\n\n- [TailwindExtension](/docs/api/modules/lexical_tailwind#tailwindextension) - A theme configuration that uses Tailwind classes\n","packages/lexical-website/docs/extensions/intro.md":"# Lexical Extensions\n\nExtensions are a new convention to add configuration and behavior to a Lexical\neditor in a modular and framework-independent manner. It is easier to use and\nmore powerful than the React plug-in scheme that came before, and is framework\nagnostic.\n\nSee [Included Extensions](./included-extensions) for the list of extensions\nthat come with Lexical and its associated packages.\n\n## Use Case\n\nLexical Extensions allow complex features to be added to your editor\nin a single place. If you want to have an editor that supports check\nlists, you simply add the `CheckListExtension` to your dependencies.\nAll of its dependent configuration, registration, and dependencies on\nother extensions is encapsulated. Extensions also work the same way\nwith or without React, so you don't generally need separate\ndocumentation and exports to support both.\n\nAll of the complexity for how and when things need to happen is\nmoved to `buildEditorFromExtensions` and the definitions of the\nextensions themselves.\n\nIn contrast, using checklists without Lexical Extensions requires\n`ListNode` and `ListItemNode` to be configured, and both the `ListPlugin` and\n`CheckListPlugin` to be registered exactly one time each (or if not using\nReact: `registerList` and `registerCheckList`).\n\nCustomizing a Lexical editor without Lexical Extensions is done in two phases:\n\n* Configuration before the editor is constructed\n  (registered nodes, node replacement, theme, etc.)\n* Registration after the editor is constructed\n  (listeners, transforms, commands, react plug-ins, etc.)\n\nIn many cases, adding a feature to the editor requires *both* configuration\nand registration. These typically happen at very different places in the code,\npossibly even different files, and there are no transitive dependencies. It's\nup to you to make sure that every required snippet of configuration and\nregistration code is included exactly once in the right place. When some dependent\nconfiguration or registration is missing (or runs twice!), at best you get a runtime\nerror, but often it will partially work which is tricky to debug.\n\nAnother compelling use case for extensions is to provide functionality\nfrom one extension to another extension (e.g. collaboration with yjs),\nor have one extension enhance the capabilities of another (e.g. markdown\nimport/export, adding options to menus or toolbars, etc.).\nThis is very awkward and error-prone with legacy plug-ins since there is no\nestablished pattern for them to depend on each other, and providing this\nsort of functionality with React requires a strict hierarchy of context\nproviders. Any extension can provide output, which is equivalent to having\nextension-specific context scoped to an editor.\n\n## Core API Overview\n\nThe primary use case for extensions is to build an editor from a collection of\nextensions. Any configuration or behavior specific to your editor should be\nprovided as an extension with\n[defineExtension](/docs/api/modules/lexical#defineextension) or as an override\nof some dependency's configuration with\n[configExtension](/docs/api/modules/lexical#configextension). This is done\nwith either:\n\n* [buildEditorFromExtensions](/docs/api/modules/lexical_extension#buildeditorfromextensions) - when not using React\n* [LexicalExtensionComposer](/docs/api/modules/lexical_react_LexicalExtensionComposer) - when using React\n\nSee [Defining Extensions](./defining-extensions.md) for a detailed guide for\n`defineExtension`.\n\nThe following functions are included in the core\n[lexical](/docs/api/modules/lexical) module to ensure that defining\nextensions is lightweight and requires no dependency on the runtime\nfeatures of [@lexical/extension](/docs/api/modules/lexical_extension).\nThese functions are primarily for type inference with very minimal\nimplementations.\n\n* [defineExtension](/docs/api/modules/lexical#defineextension) - define an extension\n* [configExtension](/docs/api/modules/lexical#configextension) - provide configuration overrides for an extension\n* [declarePeerDependency](/docs/api/modules/lexical#declarepeerdependency) - declare an indirect dependency on another extension by name\n* [safeCast](/docs/api/modules/lexical#safecast) - a convenience for TypeScript casting, with more safety than `as`. Similar to using a variable with a declared type.\n* [shallowMergeConfig](/docs/api/modules/lexical#shallowmergeconfig) - the default configuration merge strategy\n\n## Advanced features\n\n- [Signals](./signals) - Can be used for extensions to change their configuration or behavior at runtime, such as dynamically enabling or disabling an extension. This is also used to facilitate some performance optimizations around subscriptions and listeners.\n- [Peer Dependencies](./peer-dependencies) - Used to declare indirect or optional dependencies between extensions, this can be used to enable some functionality or configuration only when some other extension is available. For example, an Emoji extension may declare a specific Markdown serialization for EmojiNode, but only if the Markdown extension is present.\n","packages/lexical-website/docs/extensions/peer-dependencies.md":"# Peer Dependencies\n\nPeer Dependencies are used to declare indirect or optional dependencies\nbetween extensions. This can be used to enable some functionality or\nconfiguration only when some other extension is available, or to\navoid a dependency loop. Extensions must form a directed acyclic graph,\nthere must not be a dependency path from one extension back to itself.\n\n## Use Cases\n\nGenerally the intended use case for `peerDependencies` is to provide\nfunctionality only when some other extension is present. For example,\nyou may have an extension that requires special code to interact with\nReact, but is also usable without React, so you could have\n`ReactProviderExtension` in the `peerDependencies` array.\n\nAnother example would be to provide Markdown serialization or import\nfor an extension that provides some custom node type, but only\nif the `MdastExtension` is present in that editor.\n","packages/lexical-website/docs/extensions/react.md":"# React and Lexical Extension\n\nLexical Extension vastly improved support for non-React usage, but\ndid not make any compromises when supporting React.\n\n## Minimal migration to LexicalExtensionComposer\n\nMigrating your application from `LexicalComposer` to\n`LexicalExtensionComposer` is straightforward, but has\ntwo things to consider.\n\n### Convert your `initialConfig` to an `extension`\n\nBefore:\n```tsx\nconst initialConfig = {\n  namespace: 'MyEditor',\n  nodes: [QuoteNode, HeadingNode],\n  theme,\n  editorState(editor) {\n    editor.update($initialEditorState, {tag: HISTORY_MERGE_TAG});\n  },\n  // This is the default when using Lexical Extension\n  onError(err) {\n    throw err;\n  },\n};\n```\n\nAfter:\n```tsx\n// Be careful to make sure that this is a stable reference, either by defining it\n// at the top-level of a module or by using something like `useRef` or `useState`\n// to create it. Any time this object changes, the editor will be recreated.\nconst appExtension = defineExtension({\n  name: 'MyEditor',\n  namespace: 'MyEditor',\n  nodes: () => [QuoteNode, HeadingNode],\n  theme,\n  $initialEditorState,\n});\n```\n\n### Switch from LexicalComposer to LexicalExtensionComposer\n\nBefore:\n```tsx\n<LexicalComposer initialConfig={initialConfig}>\n    <RichTextPlugin\n        contentEditable={<ContentEditable />}\n        ErrorBoundary={LexicalErrorBoundary}\n    />\n    {/* other legacy React plugins */}\n</LexicalComposer>\n```\n\nAfter:\n```tsx\n<LexicalExtensionComposer extension={appExtension} contentEditable={null}>\n    <RichTextPlugin\n        contentEditable={<ContentEditable />}\n        ErrorBoundary={LexicalErrorBoundary}\n    />\n    {/* other legacy React plugins */}\n</LexicalExtensionComposer>\n```\n\nThis is a minimal migration, in most cases you can migrate plug-in usage to\nextension dependencies. For example, by using\n[RichTextExtension](/docs/api/modules/lexical_rich-text#richtextextension) instead of\n`RichTextPlugin`:\n\n```tsx\nconst appExtension = defineExtension({\n  name: 'MyEditor',\n  namespace: 'MyEditor',\n  dependencies: [RichTextExtension],\n  theme,\n  $initialEditorState,\n});\n```\n\n```tsx\n<LexicalExtensionComposer extension={appExtension}>\n  {/* By default the contentEditable is the first child, defaulting to <ContentEditable /> */}\n  {/* other legacy React plugins */}\n</LexicalExtensionComposer>\n```\n\n## Managing the editor yourself with LexicalExtensionEditorComposer\n\n`LexicalExtensionComposer` builds its editor from `extension` and disposes it\nwhen the component unmounts. When you want to own that lifecycle instead,\nsuch as a nested editor that belongs to a node (like the caption in the\nplayground's `StickyComponent`), an editor shared with non-React code, or one\nthat should outlive the component, build the editor yourself and render it\nwith\n[`LexicalExtensionEditorComposer`](/docs/api/modules/lexical_react_LexicalExtensionEditorComposer).\n\nThe editor must include `ReactProviderExtension` and `ReactExtension`, which\n`LexicalExtensionComposer` otherwise adds for you:\n\n```tsx\nimport {buildEditorFromExtensions, defineExtension} from '@lexical/extension';\nimport {LexicalExtensionEditorComposer} from '@lexical/react/LexicalExtensionEditorComposer';\nimport {ReactExtension} from '@lexical/react/ReactExtension';\nimport {ReactProviderExtension} from '@lexical/react/ReactProviderExtension';\nimport {RichTextExtension} from '@lexical/rich-text';\n\n// Built once, at module scope, so it is not re-created on every render\nconst editor = buildEditorFromExtensions(\n  defineExtension({\n    dependencies: [ReactProviderExtension, ReactExtension, RichTextExtension],\n    name: '@my-app/editor',\n  }),\n);\n\nfunction App() {\n  return (\n    <LexicalExtensionEditorComposer initialEditor={editor}>\n      {/* The ContentEditable is rendered by ReactExtension, as with LexicalExtensionComposer */}\n      {/* other legacy React plugins */}\n    </LexicalExtensionEditorComposer>\n  );\n}\n```\n\n`LexicalExtensionEditorComposer` never disposes the editor, so call\n`editor.dispose()` yourself when you are done with it. Keep the\n`initialEditor` prop stable (module scope, `useMemo`, or state held by a\nnode), and render a given editor in only one place at a time, since an editor\nhas only one root element.\n\n## React Plug-ins (Legacy)\n\nThese just work as-is, by rendering them as children of your composer. You may\nwish to package them as an extension for easier re-use, or consider refactoring\nthem to work without React. See [Extension Decorators](#extension-decorators)\nand [Output Components](#output-components), and\n[React Independence](#react-independence) for strategies to repackage or\nrefactor your legacy plug-ins.\n\n## Extension Decorators\n\nA common use case for legacy react plug-ins is to render some React component,\nbut the position of that component in the document is not important. Some use\ncases for this are when the component is simply an effect that always returns\n`null`, or it is rendering something like a portal. There's no need for users\nto manually decide where these components are rendered, so Lexical's\nReactExtension maintains an array of these to render automatically.\n\n ```tsx\n // This is exactly how you would write a legacy React plug-in\nfunction LogEditorPlugin() {\n  const [editor] = useLexicalComposerContext()\n  useEffect(() => {\n    console.log(editor);\n  }, [editor]);\n  return null;\n}\n// This is how you would declare the plugin as an extension\n// that depends on React and renders this plug-in as a decorator\nexport const LogEditorExtension = defineExtension({\n  name: \"@example/LogEditor\",\n  dependencies: [\n    configExtension(ReactExtension, {\n      decorators: [<LogEditorPlugin />]\n    }),\n  ],\n});\n```\n\n## Output Components\n\nIn situations where it is important for the user to decide exactly where an\nextension's component is rendered in the React document, the convention is\nto provide an Output Component.\n\nHere we have the TreeViewExtension which defines an Output Component to render\nthe TreeView with the specified configuration.\n\n```tsx\nexport function TreeViewExtensionComponent(\n  props: Partial<TreeViewConfig>,\n): JSX.Element {\n  const [editor] = useLexicalComposerContext();\n  return (\n    <TreeView\n      editor={editor}\n      {...useExtensionDependency(TreeViewExtension).config}\n      {...props}\n    />\n  );\n}\n\nexport const TreeViewExtension = defineExtension({\n  build: () => ({Component: TreeViewExtensionComponent}),\n  config,\n  dependencies: [ReactExtension],\n  name: '@lexical/react/TreeView',\n});\n```\n\nThis extension component can be acquired with the\n[useExtensionComponent](/docs/api/modules/lexical_react_useExtensionComponent#useextensioncomponent)\nhook or the [ExtensionComponent](/docs/api/modules/lexical_react_ExtensionComponent#extensioncomponent) component.\n\n```tsx\nreturn (\n  <ExtensionComponent\n    lexical:extension={TreeViewExtension}\n    viewClassName=\"tree-view-output\" />\n);\n```\n\n```tsx\nconst TreeViewComponent = useExtensionComponent(TreeViewExtension);\nreturn (<TreeViewComponent viewClassName=\"tree-view-output\" />);\n```\n\n## React Independence\n\nMany legacy plug-ins are merely a `useEffect` wrapper, these tend to not\nneed React at all. For example:\n\nBefore:\n```tsx\nexport function CheckListPlugin(): null {\n  const [editor] = useLexicalComposerContext();\n  useEffect(() => {\n    return registerCheckList(editor);\n  }, [editor]);\n  return null;\n}\n```\n\nAfter:\n```tsx\nexport const CheckListExtension = defineExtension({\n  name: \"@lexical/list/CheckList\",\n  register: registerCheckList,\n});\n```\n\n## Using React extensions and plug-ins without JSX\n\n[ReactPluginHostExtension](/docs/api/modules/lexical_react_ReactPluginHostExtension#reactpluginhostextension)\nis an extension that allows you to mount the a React root at a\nspecific DOM element in your app so that applications that are\nnot natively React can still take advantage of existing legacy\nplug-ins and extensions that depend on React.\n\nWhile this does mean your application will include the React runtime, you\ndon't have to use JSX or have any other React infrastructure in your app.\n\nSee\n[extension-vanilla-react-plugin-host](https://github.com/facebook/lexical/blob/main/examples/extension-vanilla-react-plugin-host/src/main.ts)\n[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/facebook/lexical/tree/main/examples/?file=src%2Fmain.ts)\nfor usage of this extension and its API\n","packages/lexical-website/docs/extensions/signals.md":"# Signals\n\nLexical Extensions uses signals as the reactive interface that you can use to\ncommunicate with and between extensions.\n\n## Motivation\n\nAt the lowest level, communicating between parts of Lexical is done with\ncallbacks such as `registerCommand`, `registerUpdateListener`,\n`registerMutationListener`, and `addEventListener` with the DOM.\n\nHow these callbacks behave and in some cases whether they are registered\nat all depends on state local to the extension that may also be mutable,\nso would require a similar callback mechanism with a naïve implementation.\nWith legacy React plug-ins, this mechanism is `useEffect` which is very\ncoarse and doesn't really have a portable equivalent when using other\nor no framework.\n\nThe initial prototype was largely callback based as well, but this quickly\nbecomes very difficult to work with when you have to do work based on\nthe \"result\" of multiple callbacks or configuration values. Signals are\na more general solution to this problem. There's even a\n[TC39 Signals Proposal](https://github.com/tc39/proposal-signals) to add\ninfrastructure for this to the JavaScript standard.\n\nSignals also allow for optimizations by reducing redundant callbacks with\nprimitives such as `batch` or by allowing the current value to be inspected\nwith `peek` without adding any subscription. Basically, the same Signal can\nbe used either like React State when you want to run code as the value changes\nover time, or like a React Ref when you only need to know the current value\n(as is typical with event handlers).\n\nSignals also do not require you to manage a dependency array or use a\ncompiler, the subscriptions and dependencies are inferred at runtime\nas they are used. This means that the set of signals subscribed to by a\ngiven effect can change without violating any rules or generating any\nwarnings.\n\n## Implementation\n\nSince Signals are not yet standardized, and the\n[signal-polyfill](https://github.com/proposal-signals/signal-polyfill) is not\nready for production use, it was decided to leverage\n[@preact/signals-core](https://github.com/preactjs/signals). This\nimplementation is dependency-free, optimized, and well tested.\n\nFor future compatibility we *do not recommend* that you use\n`@preact/signals-core` directly, and instead use the re-exports from\n[@lexical/extension](https://lexical.dev/docs/api/modules/lexical_extension).\n\nAs of the initial release, these re-exports are:\n\n```ts\nexport {\n  batch,\n  computed,\n  effect,\n  type ReadonlySignal,\n  type Signal,\n  signal,\n  type SignalOptions,\n  untracked,\n} from '@preact/signals-core';\n```\n\n## Usage\n\nFor thorough examples on how the signal primitives work, you can read through the\n[Preact Signals](https://preactjs.com/guide/v10/signals/) documentation.\n\nLexical provides the following higher-level signal functions to make it easier\nto build extensions:\n\n### namedSignals\n\n[namedSignals](/docs/api/modules/lexical_extension#namedsignals)\nis a convenience function used to output independent signals from\neach property of a configuration object.\n\n```ts\nexport interface TabIndentationConfig {\n  disabled: boolean;\n  maxIndent: null | number;\n  /**\n   * By default, indents are set on all elements for which the\n   * {@link ElementNode.canIndent} returns true. This option allows you to set\n   * indents for specific nodes without overriding the method for others.\n   */\n  $canIndent: CanIndentPredicate;\n}\n\nexport const TabIndentationExtension = defineExtension({\n  build(editor, config, state) {\n    return namedSignals(config);\n  },\n  config: safeCast<TabIndentationConfig>({\n    $canIndent: $defaultCanIndent,\n    disabled: false,\n    maxIndent: null,\n  }),\n  name: '@lexical/extension/TabIndentation',\n  register(editor, config, state) {\n    const {disabled, maxIndent, $canIndent} = state.getOutput();\n    return effect(() => {\n      if (!disabled.value) {\n        // This register function's implementation uses peek on the maxIndent\n        // signal to get its current value when needed, no need to re-register\n        // every time this value changes.\n        return registerTabIndentation(editor, maxIndent, $canIndent);\n      }\n    });\n  },\n});\n```\n\n### watchedSignal\n\n[watchedSignal](/docs/api/modules/lexical_extension#watchedsignal)\nis equivalent to React's [useSyncExternalStore](https://react.dev/reference/react/useSyncExternalStore)\n\n```ts\nexport const EditorStateExtension = defineExtension({\n  build(editor) {\n    return watchedSignal(\n      () => editor.getEditorState(),\n      (editorStateSignal) =>\n        editor.registerUpdateListener((payload) => {\n          editorStateSignal.value = payload.editorState;\n        }),\n    );\n  },\n  name: '@lexical/extension/EditorState',\n});\n```\n","packages/lexical-website/docs/faq.md":"---\nsidebar_position: 7\n---\n\n# FAQ\n\n## Why does Lexical use the `$` prefix in the name of many of the functions?\n\nOriginally, Lexical didn't have `$` functions, instead these functions were provided to you through callback params:\n\n```js\n// Mid 2020 API\neditor.update((viewState) => {\n  const getRoot = viewState.getRoot();\n  ..\n});\neditor.addTextTransform((viewState) => {\n  const getRoot = viewState.getRoot();\n  ..\n});\n```\n\nInternally, this approach raised some negative feedback:\n\n- The `viewState` terminology was confusing. It wasn't really a `viewState`, more like a toolkit to manipulate the `EditorState`\n- For complex updates and transforms devs had to carry params around on many layers\n\nThis is when we decided to leverage the \"lexical\" scope instead to perform `EditorState` manipulation, and the `$` represents just that.\n\n```js\neditor.update(() => ...);\neditor.registerNodeTransform(FooNode, () => ...);\neditor.getEditorState().read(...);\n```\n\nIf you've used React Hooks before, you can think of `$` functions as being something that follows a similar pattern. These are functions that show their intent as to where they can or cannot be used. This makes it possible for a developer to create their own functions that give the same signal, by simply prefixing the function with the dollar.\n\nInternally, we've found this scales really well and developers get to grips with it in almost no time at all.\n\n## When does reconciliation happen?\n\nReconciliation is scheduled with\n[queueMicrotask](https://developer.mozilla.org/en-US/docs/Web/API/queueMicrotask),\nwhich means that it will happen very soon, but asynchronously. This is similar\nto something like `setTimeout(reconcile, 0)` with a bit more immediacy or\n`Promise.resolve().then(reconcile)` with less overhead. This is done so\nthat all of the updates that occur as a result of a single logical event will\nbe batched into one reconciliation.\n\nYou can force a reconciliation to take place synchronously with the discrete\noption to `editor.update` (demonstrated below).\n\n## Why do tests use `await editor.update(…)`\n\nYou may notice that many tests look like this:\n\n```js\nawait editor.update(updateA);\nawait editor.update(updateB);\n```\n\nAn astute observer would notice that this seems very strange, since\n`editor.update()` returns `void` and not `Promise<void>`. However,\nit does happen to work as you would want it to because\nthe implementation of Promise uses the same microtask queue.\n\nIt's not recommended to rely on this in browser code as it could depend on\nimplementation details of the compilers, bundlers, and VM. It's best to stick\nto using the `discrete` or the `onUpdate` callback options to be sure that\nthe reconciliation has taken place.\n\nIgnoring any other microtasks that were scheduled elsewhere,\nit is roughly equivalent to this synchronous code:\n\n```js\neditor.update(updateA, {discrete: true});\neditor.update(updateB, {discrete: true});\n```\n\nAt a high level, very roughly, the order of operations looks like this:\n\n1. `editor.update()` is called\n2. `updateA()` is called and updates the editor state\n3. `editor.update()` schedules a reconciliation microtask and returns\n4. `await` schedules a resume microtask and yields control to the task executor\n5. the reconciliation microtask runs, reconciling the editor state with the DOM\n6. the resume microtask runs\n\n## How do I listen for user text insertions?\n\nListening to text insertion events is problematic with content editables in general. It's a common source of bugs due to how\ndifferent browsers and third-party extensions interact with the DOM. Whilst it's possible to use DOM events like `input` and\n`beforeinput` to gauge some of the possible cases where a user has inserted text, these are hardly reliable and also don't\ntake into account edge-cases. Instead, Lexical prefers to consider any change as a possible user input, and as such doesn't\nmake a distinction between the cases. This is important for tools like spellcheck, browser extensions, IME, speech-to-text,\nscreen readers and other external tools that often don't reliably trigger a reliable event sequence (some don't even trigger\nany events at all!).\n\nFor those wanting to react to a text change and possibly block/alter the intent, the recommended approach is to use a node\ntransform. This also plays nicely with other sub-systems at play that might also be looking to do the same thing as you.\n\nFor those who just want to know of the changes, this can be achieved using a text content listener or an editor update listener.\n\n## How do I clear the contents of the editor?\n\nYou can go this by calling ```clear()``` on the RootNode in an update callback:\n\n```js\neditor.update(() => {\n  $getRoot().clear();\n})\n```\n\n## How do I listen to specific key down events?\n\nYou can leverage Lexical's command listening system. Lexical provides specific commands for many common keyboard operations, such as:\n\n- `KEY_ARROW_LEFT_COMMAND`\n- `KEY_ARROW_RIGHT_COMMAND`\n- `KEY_ARROW_UP_COMMAND`\n- `KEY_ARROW_DOWN_COMMAND`\n- `KEY_SPACE_COMMAND`\n- `KEY_ENTER_COMMAND`\n- `KEY_BACKSPACE_COMMAND`\n- `KEY_DELETE_COMMAND`\n- `KEY_TAB_COMMAND`\n- `KEY_ESCAPE_COMMAND`\n\n```js\nimport {KEY_ENTER_COMMAND, COMMAND_PRIORITY_LOW} from 'lexical';\n\neditor.registerCommand(KEY_ENTER_COMMAND, (event: KeyboardEvent) => {\n  // Handle enter key presses here\n  return false;\n}, COMMAND_PRIORITY_LOW);\n```\n\nYou can use the generic `KEY_DOWN_COMMAND` command to listen\nto all keydown events. Do note, that returning `true` in your listener will prevent any\nother key based commands from firing, so in most cases you'll want to return `false` from\nthe command listener.\n\n```js\nimport {KEY_DOWN_COMMAND, COMMAND_PRIORITY_LOW} from 'lexical';\n\neditor.registerCommand(KEY_DOWN_COMMAND, (event: KeyboardEvent) => {\n  // Handle event here\n  return false;\n}, COMMAND_PRIORITY_LOW)\n```\n\n:::tip\n\nIf you do return `true` from any listener with an event payload, you likely\nalso should call `event.preventDefault()` unless your command relies on the\nbrowser's native processing of that event.\n\n:::\n\n## Why do I get \"does not implement .getType()\" or \"does not match registered node\"?\n\n```\nLexicalNode: Node FooNode does not implement .getType().\nCreate node: Type heading in node HeadingNode does not match registered node HeadingNode with the same type\n```\n\nThese almost always mean your app resolved **two copies of `lexical`**, not that\nthe node is misdeclared — the `instanceof` and registration checks compare class\nreferences, and the two copies have different ones. Run `npm ls lexical` (or\n`pnpm why lexical`) from the app root: it must report exactly one version.\nSee [One Lexical per app](concepts/one-lexical-per-app.md) for the causes and\nthe fix, including why a library must declare `lexical` in `peerDependencies`.\n\n## Which module formats are published? (ESM, CommonJS, Node.js, React Native)\n\nEvery Lexical package is published as ES modules only. The packages declare\n`\"type\": \"module\"`, and their `exports` maps offer a `development` and a\n`production` build plus a `default` entry that picks one of the two at runtime\nfrom `process.env.NODE_ENV`.\n\n- **Bundlers** (webpack, Vite, Rollup, esbuild with the conditions set) resolve\n  the `development` or `production` condition and get exactly one build.\n- **Node.js** resolves `default`, which loads both builds and exports one. To\n  load a single build, pass the condition on the command line:\n  `node --conditions=production app.js` (or `--conditions=development`).\n- **CommonJS** code can `require()` the packages on Node.js 20.19 or later,\n  which loads ES modules from `require()` as long as nothing in the module\n  graph uses top-level `await` (Lexical's builds do not). Older Node.js\n  versions have to use `await import('lexical')` instead. Loading is all\n  that `require()` guarantees: Lexical's builds import their own\n  dependencies as ES modules, so a dependency that a CommonJS application\n  also loads through `require()` can end up in the application twice, once\n  per module system, with separate classes and module state. That breaks\n  any integration that hands such a dependency's objects to Lexical, for\n  example `yjs` documents given to `@lexical/yjs`, or `@preact/signals-core`\n  effects observing signals from `@lexical/extension`. Load those\n  dependencies as ES modules too (`await import('yjs')`), or move the\n  application to ES modules.\n- **React Native (Metro)** bundles the packages without extra configuration,\n  but Metro does not set the `development`/`production` conditions and does\n  not drop the build the `default` entry leaves unused, so both end up in the\n  bundle. To bundle only the one that matches the build mode, add the\n  condition for Lexical's packages in `metro.config.js`:\n\n  ```js\n  const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config');\n\n  module.exports = mergeConfig(getDefaultConfig(__dirname), {\n    resolver: {\n      resolveRequest: (context, moduleName, platform) =>\n        context.resolveRequest(\n          /^(lexical|@lexical\\/)/.test(moduleName)\n            ? {\n                ...context,\n                unstable_conditionNames: [\n                  ...context.unstable_conditionNames,\n                  context.dev ? 'development' : 'production',\n                ],\n              }\n            : context,\n          moduleName,\n          platform,\n        ),\n    },\n  });\n  ```\n\n","packages/lexical-website/docs/getting-started/_category_.json":"{\n  \"label\": \"Getting Started\",\n  \"position\": 2\n}\n","packages/lexical-website/docs/getting-started/devtools.md":"---\nsidebar_label: \"Developer Tools\"\n---\n\n# Lexical Developer Tools\n\n## Extension\n\nThe easiest way to debug websites built with Lexical is to install the Lexical Developer Tools browser extension. It is available for several popular browsers:\n\n\n[link-chrome]: https://chromewebstore.google.com/detail/lexical-developer-tools/kgljmdocanfjckcgfpcpdoklodllfdpc 'Version published on Chrome Web Store'\n[link-firefox]: https://addons.mozilla.org/en-US/firefox/addon/lexical-developer-tools/ 'Version published on Mozilla Add-ons'\n[link-safari]: https://apps.apple.com/us/app/lexical-developer-tools/id6502753400 'Version published on Mac App Store'\n\n[<img src=\"https://cdnjs.cloudflare.com/ajax/libs/browser-logos/74.1.0/chrome/chrome.svg\" width=\"48\" alt=\"Chrome\" valign=\"middle\"/>][link-chrome] [<img valign=\"middle\" src=\"https://img.shields.io/chrome-web-store/v/kgljmdocanfjckcgfpcpdoklodllfdpc?style=flat&label=%20\n\"/>][link-chrome]\n\n[<img src=\"https://cdnjs.cloudflare.com/ajax/libs/browser-logos/74.1.0/firefox/firefox.svg\" width=\"48\" alt=\"Firefox\" valign=\"middle\"/>][link-firefox] [<img valign=\"middle\" src=\"https://img.shields.io/amo/v/lexical-developer-tools.svg?label=%20\"/>][link-firefox]\n\n[<img src=\"https://cdnjs.cloudflare.com/ajax/libs/browser-logos/74.1.0/safari/safari.svg\" width=\"48\" alt=\"Safari logo\" valign=\"middle\"/>][link-safari] [<img valign=\"middle\" src=\"https://img.shields.io/itunes/v/6502753400?label=%20\"/>][link-safari]\n\nNow, if you visit a website built with Lexical (such as [https://playground.lexical.dev/](https://playground.lexical.dev/)), you will see the _Lexical_ panel in your browser Inspector window.\n\n<img src=\"/img/docs/devtools-demo.png\" width=\"460\" alt=\"Lexical Developer Tools Demo screenshot\"/>\n\n## Extension Support\n\nFeel free to reach out to us for support, or just to chat, at [EPAM-lexical-support@epam.com](mailto:EPAM-lexical-support@epam.com) or via our [community channels](/community).\n\nLegal information:\n\n- [Meta Open Source - Privacy Policy](https://opensource.facebook.com/legal/privacy/)\n- [Meta Open Source - Terms of Use](https://opensource.facebook.com/legal/terms/)\n","packages/lexical-website/docs/getting-started/supported-browsers.md":"---\nsidebar_position: 4\n---\n\n# Supported Browsers\n\n> Note: Lexical does not support Internet Explorer or legacy versions of Edge.\n\n- Firefox 115+\n- Chrome 86+ (and other browsers based on Chrome 86+ such as Android Chrome 86+, Edge 86+ and Opera 72+)\n- Safari 15+ (iOS 15+, iPad OS 15+)\n","packages/lexical-website/docs/getting-started/theming.md":"---\nsidebar_position: 3\n---\n\n# Theming\n\nLexical tries to make theming straight-forward, by providing a way of passing a customizable theming object that maps CSS class names to the editor on creation. Here's an example of a plain-text theme:\n\n```js\nconst exampleTheme = {\n  paragraph: 'editor-paragraph',\n};\n```\n\nIn your CSS, you can then add something like:\n\n```css\n.editor-placeholder {\n  color: #999;\n  overflow: hidden;\n  position: absolute;\n  top: 15px;\n  left: 15px;\n  user-select: none;\n  pointer-events: none;\n}\n\n.editor-paragraph {\n  margin: 0 0 15px 0;\n  position: relative;\n}\n```\n\nPut the theme on your root extension. The same theme configuration works with\nReact and vanilla JavaScript:\n\n```js\nimport {PlainTextExtension} from '@lexical/plain-text';\nimport {defineExtension} from 'lexical';\n\nexport const appExtension = defineExtension({\n  name: 'MyEditor',\n  namespace: 'MyEditor',\n  dependencies: [PlainTextExtension],\n  theme: exampleTheme,\n});\n```\n\nIn React, pass that stable extension to `LexicalExtensionComposer`. This example\nplaces its own `ContentEditable`, so it disables the composer's default one:\n\n```jsx\nimport {ContentEditable} from '@lexical/react/LexicalContentEditable';\nimport {LexicalExtensionComposer} from '@lexical/react/LexicalExtensionComposer';\nimport {appExtension} from './appExtension';\n\nexport default function Editor() {\n  return (\n    <LexicalExtensionComposer extension={appExtension} contentEditable={null}>\n      <div style={{position: 'relative'}}>\n        <ContentEditable\n          aria-label=\"Plain text editor\"\n          aria-placeholder=\"Enter some text...\"\n          placeholder={<div className=\"editor-placeholder\">Enter some text...</div>}\n        />\n      </div>\n    </LexicalExtensionComposer>\n  );\n}\n```\n\nIn vanilla JavaScript, build the editor from the same extension and attach it to\nyour editable element:\n\n```js\nimport {buildEditorFromExtensions} from '@lexical/extension';\nimport {appExtension} from './appExtension';\n\nconst editor = buildEditorFromExtensions(appExtension);\neditor.setRootElement(document.getElementById('editor'));\n// Call editor.dispose() when removing this editor permanently.\n```\n\nA theme supplies class names; it does not install features or include CSS. For\nexample, to use the heading and quote styles below, add `RichTextExtension` in\nplace of `PlainTextExtension`. Add the corresponding extensions for lists, links,\nand code highlighting when you need those features.\n\n## Merging extension themes\n\nEvery extension can supply a `theme`. Lexical recursively merges these themes in\ndependency order: dependencies contribute first, and extensions that depend on\nthem can override individual entries. Your root extension can therefore customize\nan extension's styles while keeping the rest of its theme, including nested\nentries such as `text` and `heading`.\n\n```js\nconst EmphasisExtension = defineExtension({\n  name: 'Emphasis',\n  theme: {\n    text: {\n      bold: 'emphasis-bold',\n      italic: 'emphasis-italic',\n    },\n  },\n});\n\nconst appExtension = defineExtension({\n  name: 'MyEditor',\n  dependencies: [PlainTextExtension, EmphasisExtension],\n  theme: {\n    paragraph: 'editor-paragraph',\n    text: {bold: 'editor-bold'},\n  },\n});\n\n// The merged theme is:\n// {\n//   paragraph: 'editor-paragraph',\n//   text: {bold: 'editor-bold', italic: 'emphasis-italic'},\n// }\n```\n\nWhen two themes set the same entry, the later value replaces the earlier one;\nclass-name strings are not concatenated. There is no need to spread a dependency's\ntheme into your own.\n\n## Available theme entries\n\nMany of the Lexical's core nodes also accept theming properties. Here's a more comprehensive theming object:\n\n```js\nconst exampleTheme = {\n  paragraph: 'editor-paragraph',\n  quote: 'editor-quote',\n  heading: {\n    h1: 'editor-heading-h1',\n    h2: 'editor-heading-h2',\n    h3: 'editor-heading-h3',\n    h4: 'editor-heading-h4',\n    h5: 'editor-heading-h5',\n    h6: 'editor-heading-h6',\n  },\n  list: {\n    nested: {\n      listitem: 'editor-nested-listitem',\n    },\n    ol: 'editor-list-ol',\n    ul: 'editor-list-ul',\n    listitem: 'editor-listItem',\n    listitemChecked: 'editor-listItemChecked',\n    listitemUnchecked: 'editor-listItemUnchecked',\n  },\n  hashtag: 'editor-hashtag',\n  image: 'editor-image',\n  link: 'editor-link',\n  text: {\n    bold: 'editor-textBold',\n    code: 'editor-textCode',\n    italic: 'editor-textItalic',\n    strikethrough: 'editor-textStrikethrough',\n    subscript: 'editor-textSubscript',\n    superscript: 'editor-textSuperscript',\n    underline: 'editor-textUnderline',\n    underlineStrikethrough: 'editor-textUnderlineStrikethrough',\n  },\n  code: 'editor-code',\n  codeHighlight: {\n    atrule: 'editor-tokenAttr',\n    attr: 'editor-tokenAttr',\n    boolean: 'editor-tokenProperty',\n    builtin: 'editor-tokenSelector',\n    cdata: 'editor-tokenComment',\n    char: 'editor-tokenSelector',\n    class: 'editor-tokenFunction',\n    'class-name': 'editor-tokenFunction',\n    comment: 'editor-tokenComment',\n    constant: 'editor-tokenProperty',\n    deleted: 'editor-tokenProperty',\n    doctype: 'editor-tokenComment',\n    entity: 'editor-tokenOperator',\n    function: 'editor-tokenFunction',\n    important: 'editor-tokenVariable',\n    inserted: 'editor-tokenSelector',\n    keyword: 'editor-tokenAttr',\n    namespace: 'editor-tokenVariable',\n    number: 'editor-tokenProperty',\n    operator: 'editor-tokenOperator',\n    prolog: 'editor-tokenComment',\n    property: 'editor-tokenProperty',\n    punctuation: 'editor-tokenPunctuation',\n    regex: 'editor-tokenVariable',\n    selector: 'editor-tokenSelector',\n    string: 'editor-tokenSelector',\n    symbol: 'editor-tokenProperty',\n    tag: 'editor-tokenProperty',\n    url: 'editor-tokenOperator',\n    variable: 'editor-tokenVariable',\n  },\n};\n```\n","packages/lexical-website/docs/intro.md":"---\nsidebar_position: 1\n---\n\n# Introduction\n\nLexical is an extensible text editor framework for the web, built for\nreliability, accessibility, and performance. It gives you a small,\ndependency-free core and a set of optional packages that you compose into the\neditor you need, from a plain-text input with mentions to a collaborative\nrich-text document editor.\n\nLexical attaches to a `contenteditable` element and keeps its own model of the\ndocument. You read and change that model with Lexical's APIs, and Lexical\ntakes care of keeping the DOM, the selection, and the browser's many\n`contenteditable` quirks in sync. Most code never touches the DOM directly;\nthe main exception is custom nodes, which define how they render.\n\n## What can you build? {#what-can-be-built-with-lexical}\n\n- Plain-text inputs that need more than a `<textarea>`: mentions, hashtags,\n  links, custom emoji.\n- Rich-text editors for comments, posts, and messages.\n- Full document editors with tables, lists, code blocks, and images, for a CMS\n  or a notes app.\n- Real-time collaborative editing with shared content and remote cursors,\n  using the [Yjs integration](./collaboration/react.md).\n\nLexical supplies the editing infrastructure. Your application supplies the\nlayout, toolbars, menus, styling, and storage.\n\nLexical is an open-source community project. It began at Meta, and its\ncontributors today include Meta engineers alongside many volunteers and\ndevelopers from other companies. It powers text editing in Meta's web products\nand at [Bloomberg](https://www.bloomberg.com), and is also the editor behind\n[Payload CMS](https://payloadcms.com),\n[Supabase](https://supabase.com),\n[Proton Docs](https://proton.me/drive/document-editor),\n[MDXEditor](https://mdxeditor.dev), [Sveltia CMS](https://sveltiacms.app),\n[Dify](https://dify.ai), [RAGFlow](https://ragflow.io),\n[DeepSeek Harness](https://deepseek.com/harness/),\n[Paperclip](https://paperclip.ing), and [Lexxy](https://lexxy.dev). To see\nwhat it can do, try the [playground](https://playground.lexical.dev).\n\n## How it fits together\n\nThe `lexical` package is the core: the editor, the editor state, the base\nnode types, selection, commands, and the DOM reconciler. Everything else is an\noptional package built on top of it, so an application only includes the\nfeatures it uses. The core is framework-agnostic, and `@lexical/react`\nprovides React bindings.\n\n```mermaid\nflowchart TB\n  accTitle: How Lexical's packages fit together\n  accDescr: Your application uses optional packages such as rich text, history, lists, tables, and React bindings. Both the application and those packages build on the lexical core, which contains the editor, the editor state, commands, and the DOM reconciler.\n  app[\"Your application\"]\n  subgraph features[\"Optional packages\"]\n    rich[\"@lexical/rich-text<br/>@lexical/plain-text\"] ~~~ more[\"@lexical/history<br/>@lexical/list<br/>@lexical/table<br/>…\"] ~~~ bindings[\"@lexical/react\"]\n  end\n  subgraph core[\"lexical (core)\"]\n    editor[\"Editor\"] ~~~ state[\"EditorState<br/>(node tree + selection)\"] ~~~ commands[\"Commands\"] ~~~ reconciler[\"DOM reconciler\"]\n  end\n  app --> features\n  app --> core\n  features --> core\n```\n\nFeatures are added to an editor as [extensions](./extensions/intro.md). An\nextension bundles everything a feature needs (its nodes, configuration,\ncommands, and listeners, plus any extensions it depends on) so it can be\nadded in one place:\n\n```ts\nimport {buildEditorFromExtensions} from '@lexical/extension';\nimport {HistoryExtension} from '@lexical/history';\nimport {RichTextExtension} from '@lexical/rich-text';\nimport {defineExtension} from 'lexical';\n\nconst editor = buildEditorFromExtensions(\n  defineExtension({\n    dependencies: [RichTextExtension, HistoryExtension],\n    name: '@my-app/editor',\n    namespace: 'my-app',\n  }),\n);\neditor.setRootElement(document.getElementById('editor'));\n```\n\nIn React, [`LexicalExtensionComposer`](./extensions/react.md) does the same\njob. The [Quick Start](/docs/getting-started/quick-start) and\n[React guide](/docs/getting-started/react) walk through a complete setup.\n\n## Core concepts {#lexicals-design}\n\nThese are the ideas the rest of the documentation builds on. Each one links to\na page with the details.\n\n### Editor {#editor-instances}\n\nThe editor wires everything together. It owns the current editor state,\nattaches to a root DOM element, and is where you register nodes, listeners,\ntransforms, and commands. You usually create it with\n`buildEditorFromExtensions` or through the React bindings rather than calling\n`createEditor()` yourself.\n\n### Editor state {#editor-states}\n\nAn [editor state](./concepts/editor-state.md) is an immutable snapshot of the\ndocument: a tree of [nodes](./concepts/nodes.mdx) under a single `RootNode`,\nplus a [selection](./concepts/selection.md). The editor state, not the DOM, is\nthe source of truth. Its `toJSON()` output is how you save and restore content.\n\n### A DOM-like document tree {#document-model}\n\nLexical's node tree is shaped like the HTML it renders. A paragraph contains\nits text, and a link is an element that contains the text it wraps. See\n[Document Model](./concepts/document-model.md), and\n[Compared with ProseMirror](./concepts/compared-with-prosemirror.md) for how\nthis differs from a mark-based editor.\n\n### Reading and updating editor state {#reading-and-updating-editor-state}\n\nAll reads and writes happen inside a synchronous callback, such as\n`editor.update(fn)` or `editor.read('force-commit', fn)`. Functions whose\nnames start with `$`, such as `$getRoot()`, only work inside those callbacks,\nin the same way React Hooks only work while a component renders. See\n[Editor State](./concepts/editor-state.md#updating-state).\n\n### Updates and the DOM reconciler {#dom-reconciler}\n\nUpdates change a pending copy of the editor state. Updates in the same tick\nare batched, and when the batch commits, the reconciler patches only the DOM\nthat belongs to changed nodes. Lexical also watches for DOM changes made\noutside it and keeps or reverts them. [Updates](./concepts/updates.mdx) walks\nthrough each phase, with an interactive example.\n\n### Commands, transforms, and listeners {#listeners-node-transforms-and-commands}\n\nMost editor behavior is built from three kinds of hooks.\n[Commands](./concepts/commands.md) carry user input and your own actions to\nhandlers in priority order. [Node transforms](./concepts/transforms.md) run\nduring an update to keep the document in shape.\n[Listeners](./concepts/listeners.md) react after an update is committed.\n\n### Serialization and running on a server\n\nContent converts to and from JSON, HTML, and Markdown; see\n[Serialization](./serialization/serialization.md). An editor with no root\nelement does no DOM work at all, so the same code can run on a server or in\ntests; see [Running Without a Browser](./concepts/headless.md).\n\n## Get started\n\n- [Quick Start](/docs/getting-started/quick-start) builds an editor without a\n  framework, and [Getting Started with React](/docs/getting-started/react) does\n  the same in React.\n- [Lexical Extensions](./extensions/intro.md) and\n  [Included Extensions](./extensions/included-extensions.md) cover how to add\n  features.\n- The [playground](https://playground.lexical.dev) shows many features working\n  together.\n","packages/lexical-website/docs/maintainers-guide-link.md":"---\nsidebar_position: 99\n---\n\n# Developing against a local Lexical checkout\n\nIf you're iterating on Lexical itself and want a downstream app to pick\nup your changes without publishing, the Lexical packages support two\nlinking workflows.\n\nBoth rely on the fact that each `packages/<name>/` directory is its own\npublishable npm package: after a build, `packages/lexical/package.json`\nplus the `dist/` it sits next to is a complete tarball. There is no\nintermediate `npm/` directory and no copy step — `pnpm pack` /\n`pnpm publish` run straight from the package root.\n\n## Option A: built artifacts (works for any consumer)\n\nBuild the artifacts, then point your downstream project at the package\ndirectory:\n\n```bash\n# in the Lexical checkout\npnpm install\npnpm run build-release   # produces dist/<Name>.{dev,prod}.js + .d.ts + .js.flow\n\n# in your downstream app\npnpm add link:/path/to/lexical/packages/lexical\npnpm add link:/path/to/lexical/packages/lexical-react   # etc.\n```\n\n`pnpm` follows the `link:` protocol by creating a real symlink into your\n`node_modules/`, so subsequent rebuilds of Lexical (e.g.\n`pnpm run build` in the Lexical checkout) become visible immediately.\nStandard `exports` resolution picks up the `development` /\n`production` conditions, so dev builds load the unminified variants and\nprod builds load the minified ones automatically. Only ESM is published;\na CommonJS consumer loads the same files through `require(esm)`\n(Node.js 20.19+).\n\nIf you prefer the dev-only loop (faster builds, no minification), use\n`pnpm run build` instead of `build-release`. The fork modules emitted\nby a dev-only build re-export the `.dev` variant unconditionally; you\nwon't be able to `pnpm publish` from such a checkout because the\npublish guard will report missing `.prod` files.\n\n`file:/path/to/...` works too if your package manager treats it as a\nsymlink (modern pnpm does); npm copies the directory into\n`node_modules` instead, which loses any live-reload benefit but is\notherwise equivalent for one-shot installs.\n\n## Option B: TypeScript source (no Lexical build needed)\n\nFor the tightest dev loop — edit a Lexical source file and have your\ndownstream bundler pick it up on the next request — opt into the\n`source` export condition. Each public package's `exports` map exposes\nits `./src/<entry>.tsx?` next to the compiled `development`/`production`/\n`default` conditions, so a bundler configured with\n`resolve.conditions: ['source', …]` will load TypeScript directly out\nof the linked package.\n\nThe only requirement is **bundler support**: your consumer's bundler has to\nbe configured to pick up the `source` condition AND to transform\n`.ts`/`.tsx` from inside `node_modules`. Vite, Rspack, and Parcel can all be\ntold to do this; webpack needs a loader override for the linked package.\n\nThere is **no aliasing and no extra `define` to set up**. Internal utilities\nthe source depends on are a real (internal) dependency, `@lexical/internal`,\nwhich resolves through normal package resolution; the React/test helpers are\npackage-internal; and the dev/prod branch is `process.env.NODE_ENV !==\n'production'`, which every mainstream bundler substitutes by default. So\n`source` mode works without any `resolve.alias` entries or build-time globals\n— including for the transitive `@lexical/*` packages.\n\nOne thing the compiled bundles have that the sources do not is the\n`/* @__PURE__ */` annotations on module-scope calls to `defineExtension`,\n`createCommand`, `createState`, and friends. Lexical's own build injects\nthem, and without them a bundler has to assume those calls have side effects\nand will keep every extension, command, and rule definition your app never\nuses. Add [`@lexical/compiler`](https://www.npmjs.com/package/@lexical/compiler)\nto your build to get the same annotations out of `source` mode — it annotates\nthe factory calls in your own code too.\n\n### Minimal Vite setup\n\n```ts\n// vite.config.ts\nimport {pureAnnotations} from '@lexical/compiler/PureAnnotations';\nimport {defineConfig} from 'vite';\n\nexport default defineConfig({\n  plugins: [pureAnnotations()],\n  resolve: {\n    conditions: ['source', 'development', 'module', 'browser', 'default'],\n  },\n});\n```\n\nBecause a `link:`ed checkout is the same version as the plugin, you can also\npass `pureAnnotations({inline: true})`, which replaces calls to the trivial\nfactories (`safeCast`, `defineExtension`, `configExtension`, ...) with the\nliteral they would have returned rather than annotating them.\n\n`@lexical/compiler/PureAnnotations` also exports the transform itself\n(`transformPureAnnotations`) for bundlers without a Rollup-style plugin API;\nsee its README for the options.\n\nThe integration fixture at\n`scripts/__tests__/integration/fixtures/lexical-link-source-mode/`\nexercises exactly this setup and is the canonical reference if you need\na fuller example.\n\n> [!NOTE]\n> `@lexical/internal` is published only so this resolution works (and for\n> direct internal consumers); the compiled `dist/` artifacts inline it, so\n> a normal `npm install` of `lexical` never executes a separate copy.\n","packages/lexical-website/docs/maintainers-guide.md":"# Maintainers' Guide\n\nThis is the grimoire of arcane knowledge covering the overall organization\nof the Lexical monorepo, including its conventions, quirks, and\nconfigurations.\n\n## Monorepo Organization\n\n### Workspaces\n\nThe monorepo is configured with\n[pnpm workspaces](https://pnpm.io/workspaces); the set of workspace\npackages is declared in the top-level `pnpm-workspace.yaml`. This mostly means that all packages share a\ntop-level `pnpm-lock.yaml` and `pnpm -C {package} run {command}` is often\nused to run a command from a nested package's package.json.\n\n### Private\n\nSome packages in the monorepo do not get published to npm, for example:\n\n* `packages/lexical-devtools` - browser extension for working with Lexical\n  sites\n* `packages/lexical-playground` - the\n  [playground.lexical.dev](https://playground.lexical.dev/) demo site\n* `packages/lexical-website` - the [lexical.dev](https://lexical.dev/)\n  docusaurus website that you may even be reading right now\nInternal runtime code shared by more than one package lives in\n`packages/lexical-internal` (`@lexical/internal`). Unlike the others above\nit **is** published, but only so its source resolves through normal package\nresolution (the `source` export condition used by linked-checkout\nconsumers); the compiled packages inline it, so it is never executed as a\nseparate runtime dependency. It is not a public API and has no semver\nguarantees — see [Developing against a local Lexical\ncheckout](./maintainers-guide-link.md).\n\nIt is required that private packages, and any other package that should not\nbe published to npm, have a `\"private\": true` property in their `package.json`.\nIf you have an in-progress package that will eventually be public, but is\nnot ready for consumption, it should probably still be set to\n`\"private\": true` otherwise the tooling will find it and publish it.\n\n## Package naming conventions\n\n### Overall\n\n| Usage | Convention |\n| -- | -- |\n| Directory name | `packages/lexical-package-name` |\n| Entrypoint | `packages/lexical-package-name/src/index.{ts,tsx}` |\n| Flow types | `packages/lexical-package/flow/LexicalPackageName.js.flow` |\n| package.json name | `@lexical/package-name` |\n| Documentation | `packages/lexical-package-name/README.md` |\n| Unit Tests | `packages/lexical-package-name/src/__tests__/unit/LexicalPackageName.test.{ts,tsx}` |\n| dist (gitignore'd build product) | `packages/lexical-package-name/dist` |\n| npm (gitignore'd prerelease product) | `packages/lexical-package-name/npm` |\n| www entrypoint | `packages/lexical-package-name/LexicalPackageName.js` |\n\n### Multiple module export (@lexical/react)\n\nInstead of having a single module, some packages may have many modules\n(currently only `@lexical/react`) that are each exported separately.\nIn that scenario, there should be no `index.ts` entrypoint file and every module\nat the top-level should be an entrypoint. All entrypoints should be a\nTypeScript file, not a subdirectory containing an index.ts file.\n\nThe [update-packages](#pnpm-run-update-packages) script will ensure that the\nexports match the files on disk.\n\n## Creating a new package\n\nThe first step in creating a new package is to create the workspace directory\nand package.json file. The example we will use is the steps that were used\nto create the `lexical-eslint-plugin`, which will be published to npm as\n`@lexical/eslint-plugin`.\n\n### Create the workspace\n\n```bash\nmkdir -p packages/lexical-eslint-plugin\n```\n\nCreate the initial `package.json` file (you can base it on an existing package\nor use the template below):\n\n<details><summary>\n\n`packages/lexical-eslint-plugin/package.json`\n</summary>\n\n```json\n{\n  \"name\": \"@lexical/eslint-plugin\",\n  \"description\": \"\",\n  \"keywords\": [\n    \"lexical\",\n    \"editor\"\n  ],\n  \"version\": \"0.14.3\",\n  \"license\": \"MIT\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/facebook/lexical.git\",\n    \"directory\": \"packages/lexical-eslint-plugin\"\n  },\n  \"main\": \"LexicalEslintPlugin.js\",\n  \"types\": \"index.d.ts\",\n  \"bugs\": {\n    \"url\": \"https://github.com/facebook/lexical/issues\"\n  },\n  \"homepage\": \"https://github.com/facebook/lexical#readme\"\n}\n```\n</details>\n\nSome next steps for this package.json before moving on:\n\n- Update the description\n- Add appropriate keywords\n\n### Create the initial source file\n\n```\nmkdir -p packages/lexical-eslint-plugin/src\ncode packages/lexical-eslint-plugin/src/index.ts\n```\n\nHere are some minimal examples of those files that you might start out with.\nI've elided the license header, the eslint header/header fixer will help you\nwith that!\n\n<details><summary>\n\n`packages/lexical-eslint-plugin/src/index.ts`\n</summary>\n\n```typescript\nimport {name, version} from '../package.json';\n\nconst plugin = {\n  meta: {name, version},\n  rules: {},\n};\n\nexport default plugin;\n```\n</details>\n\n### Run update-packages to generate boilerplate docs & config\n\n```\npnpm run update-packages\n```\n\nThis will set up the tsconfig, flow, etc. configuration to recognize your\nnew module. It will also create an initial README.md using only the\ndescription from the package.json.\n\n\n### Create an initial unit test\n\n```\nmkdir -p packages/lexical-eslint-plugin/src/__tests__/unit\ncode packages/lexical-eslint-plugin/src/__tests__/unit/LexicalEslintPlugin.test.ts\n```\n\n\n<details><summary>\n\n`packages/lexical-eslint-plugin/src/__tests__/unit/LexicalEslintPlugin.test.ts`\n</summary>\n\n```typescript\nimport plugin from '@lexical/eslint-plugin';\n\ndescribe('LexicalEslintPlugin', () => {\n  it('exports a plugin with meta and rules', () => {\n    expect(Object.keys(plugin).sort()).toMatchObject(['meta', 'rules']);\n  });\n});\n```\n</details>\n\n\n## Scripts for development\n\n### pnpm run update-packages\n\nThis script runs: update-version, update-tsconfig, update-flowconfig,\ncreate-docs, and create-www-stubs. This is safe to do at any time and will\nensure that package.json files are all at the correct versions, paths are\nset up correctly for module resolution of all public exports, and that\nvarious defaults are filled in.\n\nThese scripts can be run individually, but unless you're working on one\nof these scripts you might as well run them all.\n\n### pnpm run prepare-release\n\nThis runs `build-release` to produce all of the artifacts each public\npackage needs (the `dev`/`prod` ESM variants plus their fork modules,\n`.d.ts` declarations, and `.js.flow` stubs under\n`packages/<name>/dist/`), then runs the publish-time guard in\n`scripts/npm/prepare-release.mjs` to confirm every path the package's\n`exports`/`main`/`module`/`types` fields reference actually exists on\ndisk. The guard fails the build if e.g. you ran `pnpm run build` (dev\nonly) and then tried to publish — the `.prod.js` files would be\nmissing.\n\nOnly ESM is published to npm. Every public package is a\n`\"type\": \"module\"` package (`update-version` adds the field), so its\nbuild is plain `.js`. A CommonJS consumer gets the same files through\nNode's `require(esm)` (Node.js 20.19+), which is why the fork module that\nthe exports map's `default` condition resolves to has no top-level await.\nThe CommonJS variants are only built for www (`pnpm run build-www`).\n\nBecause a `.d.ts` in a `\"type\": \"module\"` package is an ES module\ndeclaration, the build gives the relative imports tsc emits an explicit\n`.js` extension (`from './LexicalEditor.js'`): TypeScript's `nodenext`\nresolution does not resolve extensionless relative imports in ESM, and a\nconsumer with `skipLibCheck` would silently get `any` for everything.\n`scripts/__tests__/integration/declaration-resolution.test.mjs` checks\nthat a `nodenext` consumer of every entry point still sees the types.\n\nEach package is its own publish root: `packages/<name>/` IS the\npublishable npm package after `build-release`. `pnpm publish` is run\ndirectly from that directory by `scripts/npm/release.mjs` so pnpm's\nautomatic `workspace:*` rewriting and the `files` whitelist do the\nright thing without an intermediate `npm/` copy step.\n\nThis will also update `scripts/error-codes/codes.json`, the mapping of\nproduction error codes to error messages. It's imperative to commit the\nresult of this before tagging a release.\n\n### pnpm run ci-check\n\nCheck flow, TypeScript, prettier and eslint for issues. A good command to run\nafter committing (which will auto-fix most prettier issues) and before pushing\na PR.\n\n### pnpm run flow\n\nCheck the Flow types\n\n### pnpm run tsc\n\nCheck the TypeScript types\n\n### pnpm run tsc-extension\n\nCheck the TypeScript types of the lexical-devtools extension\n\n### pnpm run test-unit\n\nRun the unit tests\n\n### pnpm run lint\n\nRun eslint\n\n### pnpm run generate-node-json\n\nRegenerate the specialized JSON serialization code for the built-in node\nclasses. A node's [serialization schema](/docs/serialization/#declarative-serialization-schemas-with-config)\nstates everything about a serialized property ahead of time — which accessor\nor field it uses, what its default is, what its domain admits — so the\ngeneric walk over that schema can be compiled into straight-line code. This\nscript does that compiling; the output is checked in.\n\nIt writes one module per package, beside the nodes it serializes:\n\n| Module | Classes |\n| --- | --- |\n| `packages/lexical/src/LexicalGeneratedJSON.ts` | ElementNode, TextNode, ParagraphNode, LineBreakNode, TabNode |\n| `packages/lexical-rich-text/src/LexicalRichTextGeneratedJSON.ts` | HeadingNode, QuoteNode |\n| `packages/lexical-link/src/LexicalLinkGeneratedJSON.ts` | LinkNode, AutoLinkNode |\n| `packages/lexical-mark/src/LexicalMarkGeneratedJSON.ts` | MarkNode |\n| `packages/lexical-list/src/LexicalListGeneratedJSON.ts` | ListNode, ListItemNode |\n| `packages/lexical-table/src/LexicalTableGeneratedJSON.ts` | TableNode, TableRowNode, TableCellNode |\n| `packages/lexical-code-core/src/LexicalCodeCoreGeneratedJSON.ts` | CodeNode, CodeHighlightNode |\n| `packages/lexical-react/src/shared/LexicalReactGeneratedJSON.ts` | DecoratorBlockNode |\n\nEach class receives its own generated code through its `$config`'s\n`generated` property, so nothing has to match code to class by type string at\nruntime. What the property holds is a factory: registration calls it with the\nclass's composed schema, and the generated code reads the lookup tables it\nneeds (`getterTable`, `setterTable`, `aliasedValue`'s) off that schema through\n`getterTableOf`, `setterTableOf` and `aliasTableOf`, so a generated module\ncarries no copy of a table and nothing about a table's contents is written at\nbuild time — only its type, so the field a value is assigned to is still\nchecked. A subclass inherits the code along with the schema when its compiled\naccessor tables are the ones the code was generated from — checked entry for\nentry at registration — and runs it over its own schema, while one that\noverrides an accessor a field stands in for, or declares a serialized property\nof its own, resolves differently and takes the schema-driven walk instead.\nGenerated exporters read `type` off the node for the same reason.\n\nThree things are worth knowing before touching it:\n\n- **The output is verified, not trusted.** The import direction is the\n  untrusted-JSON boundary, so every generated parser is run against the schema\n  it was compiled from over a corpus drawn from that schema plus a fixed set of\n  hostile values (`'__proto__'`, `'toString'`, `'1e999'`, …). A property whose\n  schema cannot be compiled faithfully takes its class out of the import half\n  rather than shipping a parser that disagrees with the walk — the script says\n  so on stdout when it happens.\n- **A stale checkout fails the tests.** `LexicalGeneratedJSON.test.ts`\n  regenerates into a temporary directory and compares every file byte for\n  byte, and separately asserts that each generated exporter agrees with the\n  schema-driven walk for both the legacy and compact forms. Change a schema\n  without rerunning this script and that test fails.\n- **Two lists, deliberately.** The script runs in two phases, because reading\n  the schemas means importing the packages and each package imports the file\n  the script writes for it. Phase one replaces every output with a valid\n  do-nothing stub from the static `MANIFEST` in\n  `scripts/shared/generateNodeJSONManifest.mjs` so the imports always succeed;\n  phase two re-enters under `tsx` and writes the real thing from `PACKAGES` in\n  `scripts/shared/generateNodeJSON.mjs`, which is also what the drift test\n  runs in-process. Adding a class means editing both lists, and the generation\n  fails loudly if they disagree.\n\nThe compact form compares each property against its default. A primitive\ndefault is a literal; a reference-typed default has no literal a value could\nbe `===`, so it gets the structural test the schema's own equality reduces to\nwhere that can be stated — MarkNode's `ids`, whose default is an empty array,\nbecomes a length test — and every emitted comparison is verified against that\nequality over a corpus, the way a parse is. A default the generator cannot\nstate that way (an object, a non-empty array, a non-finite number) takes the\nclass out of the compact half only: the script says so on stdout and the other\nforms are generated as usual. A class that carries flat NodeState is generated\nlike any other; the walk applies the state before handing the node to the\ngenerated parser, the mirror of how export appends it after the generated\nliteral.\n\n`afterCloneFrom` is generated too, and is the one direction whose fallback is\nnot the walk. A schema field is where a property is *stored*, so every class\nthat declares one gets an `afterCloneFrom` synthesized at registration —\ngenerated straight-line code when the class has some, and otherwise a loop over\nthe field names — copying the fields that class's own `$config` declared and\ndelegating the rest through `super`, which is why the emitted function covers\none class's own fields and nothing above it. A field an ancestor declares too\nis left to the ancestor, whose method has already assigned it: re-declaring an\ninherited property changes how it is serialized, not where it is stored, so a\nclass that only re-declares gets no method at all — `TabNode`, which restates\n`TextNode`'s `text`, `detail` and `mode`, is the in-tree case and simply\ninherits `TextNode`'s. Both accessor directions are read\nfor a field name, and the declared field is used rather than the one\n`resolveGetterAccessor` resolves to: an override changes how a property is\nserialized, not where it lives. A class that writes its own `afterCloneFrom` is\nleft alone and owns all of its properties, which is how `ElementNode` keeps\ncarrying `__first`/`__last`/`__size` and its slot bookkeeping; so is a property\ndeclared through accessor methods on both sides, which names no field for\nanything to copy. No in-tree node is in that second position — a property held\nin a field says so with `setter: {field, method}` and stays derived, as\n`MarkNode`'s `ids` does — so the boilerplate that remains is `ElementNode`'s\nand `CodeNode`'s, and each of those calls the generated `afterClone<Class>`\nfor its schema half and writes only the fields no schema describes.\n`ownSchemaFields` in `LexicalUtils.ts` is the single definition of that field\nlist, called by both the generator and the synthesized fallback.\n\nThe schema-to-JavaScript compiler itself lives in `@lexical/compiler`'s\n`SchemaJsonCodegen` entry point, so it is testable independently of the\ngenerator that drives it.\n\n## Scripts for release managers\n\n### pnpm run extract-codes\n\nThis will run a build that also extracts the generated error codes.json file.\n\nThis should be done, at minimum, before each release, but not in any PR as\nit would cause conflicts between serial numbers.\n\nIt's safe and probably advisable to do this more often, possibly any time a\nbranch is merged to main.\n\nThe codes.json file is also updated any time a release build is generated\nas a failsafe to ensure that these codes are up to date in a release.\nThis command runs a development build to extract the codes which is much\nfaster as it is not doing any optimization/minification steps.\n\n### pnpm run increment-version\n\nIncrement the monorepo version. The `-i` argument must be one of\n`minor` | `patch` | `prerelease`.\n\nThe postversion script will:\n- Create a local `${CHANNEL}__release` branch (the version-named\n  `${npm_package_version}__release` branch is only a remote push target,\n  and only when the channel is not `nightly`)\n- `pnpm run update-version` to update example and sub-package monorepo dependencies\n- `pnpm install` to update the pnpm-lock.yaml\n- `pnpm run update-packages` to update other generated config\n- `pnpm run extract-codes` to extract the error codes and\n  `pnpm run update-changelog` to update the changelog (both only when\n  `CHANNEL` is `latest`)\n- Create a version commit and tag from the branch\n\nThis is typically executed through the `version.yml` GitHub Workflow which\nwill also push the tag and branch.\n\n### pnpm run changelog\n\nUpdate the changelog from git history.\n\n### pnpm run release\n\n*Prerequisites:* all of the previous release manager scripts,\nplus creating a tag in git, and likely other steps.\n\nRuns prepare-release to do a full build and then uploads to npm.\n\n### pnpm run setup-trusted-publishing\n\nOne-time (idempotent) helper to register every public package with\n[npm trusted publishing](https://docs.npmjs.com/trusted-publishers).\nRe-run it whenever a new public package is added.\n\n#### Prerequisites\n\n- Node.js — whatever the repo's root `package.json#engines.node` says (currently `>=20.19.0`). Running with Node 24+ is recommended because that's what CI uses for publishes.\n- pnpm — pinned by `package.json#packageManager` (currently `pnpm@11.24.0`). Activate with [corepack](https://nodejs.org/api/corepack.html) or install directly.\n- npm CLI — **`npm ≥ 11.10`** (`npm i -g npm@latest`). The `npm trust` subcommand was added in npm 11; older versions will fail the preflight check.\n- An authenticated npm session (`npm login --registry https://registry.npmjs.org`) on a publisher account that has **account-level 2FA enabled** and write access to every `@lexical/*` package.\n\n#### Usage\n\nRun in check-only mode first:\n\n```bash\npnpm run setup-trusted-publishing\n```\n\nFor each public package in the monorepo, it queries\n`https://registry.npmjs.org` and reports whether the name is already\nclaimed. Packages that *don't* exist on the registry are listed; you\ncan re-run with `--bootstrap` to publish a deprecated\n`0.0.0-bootstrap.0` placeholder under the `bootstrap` dist-tag so the\nname can be claimed:\n\n```bash\nnpm login --registry https://registry.npmjs.org\npnpm run setup-trusted-publishing --bootstrap\n```\n\nOnce a package exists on the registry, you can configure trusted\npublishing for it programmatically by adding `--setup-trust`. This\nruns `npm trust github` under the hood (requires `npm` ≥ 11.10 and an\nauthenticated session with account-level 2FA on the publishing\naccount), and is idempotent — the script reads the existing trust\nconfiguration for each package via a read-only registry call (no OTP)\nand skips packages whose config already matches:\n\n```bash\nnpm login --registry https://registry.npmjs.org\npnpm run setup-trusted-publishing --setup-trust\n```\n\n`npm trust github` is a write operation, so each package that *does*\nneed configuring will trigger a one-time-password / web-auth prompt.\nOn the first prompt npm prints a URL; open it in a browser, sign in,\nand tick **\"Skip two-factor authentication for the next 5 minutes\"**.\nSubsequent packages in the same run will then go through without\nre-prompting. The script also inserts a small (~2 s) pause between\ncalls to stay under the registry's `E429` rate limit.\n\nFor full first-time setup of a brand-new monorepo, combine both flags:\n\n```bash\npnpm run setup-trusted-publishing --bootstrap --setup-trust\n```\n\nWhen adding a **single** new package to an existing monorepo — the common\ncase going forward — pass its name so the run only touches that package\ninstead of re-checking all 30+ already-configured ones (which just prints a\nwall of `CONFLICT` lines). The name can be the full npm name or the unscoped\nshort name, and `--package` / positional args are interchangeable and\nrepeatable:\n\n```bash\npnpm run setup-trusted-publishing --bootstrap --setup-trust @lexical/a11y\n# equivalently: --package a11y\n```\n\nUseful flags:\n\n- `--package <name>` (or a positional `<name>`, repeatable) — restrict the run to the given package(s), matched by full npm name (`@lexical/a11y`) or unscoped short name (`a11y`). Omit to process every public package.\n- `--dry-run` — print what would happen without touching the registry (works with both `--bootstrap` and `--setup-trust`)\n- `--workflow <filename>` — override the workflow filename (default `pre-release.yml`)\n- `--repo <owner/name>` — override the GitHub repo (default `facebook/lexical`)\n- `--stub-version <semver>` — override the placeholder version (default `0.0.0-bootstrap.0`)\n- `--registry <url>` — override the npm registry\n\nIn the default (check-only) mode the script also prints the npmjs.com\n`/access` URL for each existing package and the exact values to enter\nmanually, as a fallback for when `npm trust github` isn't an option.\n\n### Testing trusted publishing from a PR branch\n\nThe \"Publish to NPM\" workflow (`pre-release.yml`) exposes `ref`,\n`channel`, and `increment-version` inputs so it doubles as a test\nharness. Picking a branch in the \"Run workflow\" dropdown selects\nwhich version of the workflow files run, and the inputs determine\nwhat actually gets published. The workflow has no NPM_TOKEN secret to\nfall back on — publishes always go through OIDC trusted publishing —\nso a misconfigured trust setup fails loudly rather than silently\nfalling through to token auth.\n\nA safe end-to-end test looks like:\n\n| Input | Value |\n| -- | -- |\n| Branch (dropdown) | your PR branch |\n| `ref` | your PR branch (same value) |\n| `channel` | `dev` |\n| `increment-version` | checked |\n| `ignore-previously-published` | unchecked |\n\nWith `increment-version` on, the run bumps `package.json` to a fresh\nprerelease (e.g. `0.46.0-dev.0`), commits + tags it on a `dev__release`\nbranch on origin, and publishes the monorepo under the `dev` dist-tag\nvia OIDC. The `latest` tag is untouched, so default `npm install`\nusers are unaffected. After it succeeds:\n\n```bash\nnpm view lexical@dev version     # → the just-published prerelease\nnpm view lexical@latest version  # → unchanged\n```\n\nCleanup (the prerelease itself can't be reused, but the git refs\nshould go):\n\n```bash\ngit push --delete origin v0.46.0-dev.0 dev__release 0.46.0-dev.0__release\n```\n\nThe `increment-version=true + channel=latest` combination is refused\nby the workflow's guard job — real `latest` releases must go through\n`version.yml` first.\n\n## Continuous releases (pkg.pr.new)\n\nEvery commit to a pull request and every commit to `main` publishes all public\npackages to [pkg.pr.new](https://github.com/stackblitz-labs/pkg.pr.new) through\nthe `pkg-pr-new.yml` workflow. Nothing goes to npm: the nightly, next and latest\nchannels above are unchanged and still publish through `call-release.yml`.\n\n```sh\nnpm i https://pkg.pr.new/lexical@9372      # latest commit of PR #9372\nnpm i https://pkg.pr.new/lexical@4282a68   # a specific commit\nnpm i https://pkg.pr.new/lexical@main      # tip of main\n```\n\nThe bot comments install commands on each pull request. Previews are built\nwith `pnpm run prepare-release`, the same build as the npm channels, after\n`scripts/npm/preview-version.mjs` stamps a version in the nightly line:\n`0.52.1-pr.9372.g4282a68` for a PR and `0.52.1-main.g4282a68` for `main`, next\nto `0.52.1-nightly.20261019.0` on npm. That version is also what\n`LexicalEditor.version` reports. Dependencies between Lexical packages are\nrewritten to the same commit's pkg.pr.new URLs, so `@lexical/react` from a\npreview depends on `lexical` from that preview, not on an npm release.\n\nThe bot's comment lists npm commands, and npm dedupes those dependencies by\nname and version, so `@9372` or `@main` URLs are fine. pnpm and yarn key URL\ndependencies by URL instead: install with the long commit URLs that the\npackages use for each other, or you get more than one copy of `lexical`. pnpm\n11+ also refuses URL dependencies of URL dependencies unless\n`blockExoticSubdeps` is off. Each run's job summary lists these commands:\n\n```sh\npnpm add --config.block-exotic-subdeps=false \\\n  lexical@https://pkg.pr.new/facebook/lexical/lexical@4282a68 \\\n  @lexical/react@https://pkg.pr.new/facebook/lexical/@lexical/react@4282a68\nyarn add \\\n  lexical@https://pkg.pr.new/facebook/lexical/lexical@4282a68 \\\n  @lexical/react@https://pkg.pr.new/facebook/lexical/@lexical/react@4282a68\n```\n\nOn a cold cache, yarn 1 can fail with \"the file appears to be corrupt\" while\nit downloads the same URL for several packages at once. Run it again.\n\nThe workflow requires the [pkg.pr.new GitHub App](https://github.com/apps/pkg-pr-new)\nto be installed on the repository.\n\n## Release Procedure\n\nThis is the current release procedure for public releases, at least as of\nMay 2024 (~0.15.0).\n\nThe main branch should be \"frozen\" during this procedure (no other PRs should\nbe merged during this time). This avoids a mismatch between the contents of\nthe GitHub release (created from main in step 1) and the NPM release (created\nfrom main in step 4).\n\n1. Create a new version with the Github Actions \"Create New Release Branch\" workflow (`version.yml`)\n2. Raise a PR against version branch created by that action\n3. After PR is approved with passing tests, merge PR\n4. After PR is merged to main, publish to NPM with the Github Actions \"Publish to NPM\" workflow (`pre-release.yml`)\n5. Create a GitHub release from the tag created in step 1, manually editing the release notes\n6. Announce the release in #announcements on Discord\n\n## Release Protocol\n\n1. All PRs with breaking changes must have `[Breaking Change]` in the PR's title with documentation of what followup actions consumers of the lexical library need to be aware of.\n2. Monthly releases happen on the last week of the month, with a minor increment (eg. v0.20+1.0).\n3. Anything in between will be a patch increment (eg. 0.20.0+1), unless there is a breaking change. \n\n## Website Team Page\n\nThe [team page](https://lexical.dev/community) displays core team\nmembers, emeriti, and distinguished contributors. The `team.json` data is\ngenerated from GitHub contributor information and some predetermined decisions in\nthe script to acknowledge emeriti and historically important distinguished contributors.\n\nTo update the team page data:\n\n```bash\npnpm run update-team-data\n```\n\nThis fetches the latest contributor data from GitHub and categorizes team members\nbased on recent activity (last 12 months). See `packages/lexical-website/src/data/README.md`\nfor more details on configuration and team categorization logic.\n","packages/lexical-website/docs/react/_category_.json":"{\n  \"label\": \"React\",\n  \"position\": 5\n}\n","packages/lexical-website/docs/react/create_plugin.md":"# Creating a React Plugin\n\nIn addition to using the Lexical React plugins offered by the core library, you can make your own plugins to extend or alter Lexical's functionality to suit your own use cases.\n\nLexical's React plugin interface is simple - just create a React component and add it as a child of your LexicalComposer component:\n\n```jsx\n <LexicalComposer initialConfig={initialConfig}>\n    <MyLexicalPlugin />\n </LexicalComposer>\n```\n\nIf the Plugin introduces new nodes, they have to be registered in `initialConfig.nodes`:\n\n```js\nconst initialConfig = {\n  namespace: 'MyEditor',\n  nodes: [MyLexicalNode],\n};\n```\n\n```jsx\n <LexicalComposer initialConfig={initialConfig}>\n    <MyLexicalPlugin />\n </LexicalComposer>\n```\n\nLexicalComposer provides access to the underlying LexicalEditor instance via React Context:\n\n```jsx\n//MyLexicalPlugin.js\n\nexport function MyLexicalPlugin(props) {\n    const [editor] = useLexicalComposerContext();\n    ...\n}\n```\n\nWith access to the Editor, your plugin can extend Lexical via [Commands](../concepts/commands.md), [Transforms](../concepts/transforms.md), or other APIs. For example, the [TwitterPlugin](https://github.com/facebook/lexical/blob/0775ab929e65723433626fa8c25900941e7f232f/packages/lexical-playground/src/plugins/TwitterPlugin/index.ts#L18) embeds a tweet into the editor, fetching the data asynchronously from Twitter based on the provided Tweet ID:\n\n```jsx\nexport const INSERT_TWEET_COMMAND: LexicalCommand<string> = createCommand();\n\nexport default function TwitterPlugin(): JSX.Element | null {\n  const [editor] = useLexicalComposerContext();\n\n  useEffect(() => {\n    if (!editor.hasNodes([TweetNode])) {\n      throw new Error('TwitterPlugin: TweetNode not registered on editor (initialConfig.nodes)');\n    }\n\n    return editor.registerCommand<string>(\n      INSERT_TWEET_COMMAND,\n      (payload) => {\n        const tweetNode = $createTweetNode(payload);\n        $insertNodeToNearestRoot(tweetNode);\n\n        return true;\n      },\n      COMMAND_PRIORITY_EDITOR,\n    );\n  }, [editor]);\n\n  return null;\n}\n```\n\nTwitterPlugin is just a React component that accesses the Lexical editor via React Context (useLexicalComposerContext). Using the LexicalEditor instance, this plugin does two things:\n\n1. Verifies that there is a TweetNode registered on the editor (if you forget to register the node, you can't do #2)\n2. registers a \"command\", passing a callback that will run when that command is dispatched. The command callback creates and inserts a TweetNode in the editor.\n\nYou can see how [TwitterPlugin is used in the playground](https://github.com/facebook/lexical/blob/0775ab929e65723433626fa8c25900941e7f232f/packages/lexical-playground/src/Editor.tsx#L137). It's added as a child of a LexicalComposer component, which does the job of providing the Context necessary for access to the editor instance. To actually trigger this command callback and insert a [TweetNode](https://github.com/facebook/lexical/blob/b0fa38615c03f1c4fc7c8c5ea26412b723770e55/packages/lexical-playground/src/nodes/TweetNode.tsx#L212), we have a [button](https://github.com/facebook/lexical/blob/b0fa38615c03f1c4fc7c8c5ea26412b723770e55/packages/lexical-playground/src/plugins/ToolbarPlugin.tsx#L534) that \"dispatches\" the Tweet command we registered in the plugin.\n\nWhile the TwitterPlugin registers a command that inserts a custom node, this is only one example of what can be done with a plugin. To get a better idea of what's possible, take a look at the [plugins defined in the playground](https://github.com/facebook/lexical/tree/main/packages/lexical-playground/src/plugins).\n","packages/lexical-website/docs/react/faq.md":"# React FAQ\n\n## My app does not work in dev when using StrictMode, help!?\n\nWhen hooks are used correctly, there are no known issues with React StrictMode\nand Lexical. The first thing you should do is go through React's documentation\nto make sure that your usage of `useEffect` and other hooks follow React's\nconventions and guidelines. This is a great place to start:\n[My Effect runs twice when the component mounts](https://react.dev/reference/react/useEffect#my-effect-runs-twice-when-the-component-mounts)\n\nSome Lexical-specific concerns (which are consequences of React's\nconcurrent and StrictMode semantics, not due to anything unusual in Lexical):\n\n- In React 19, `useMemo` calls are cached across StrictMode re-renders, so\n  only one editor will be used for both renders. If you have a `useEffect`\n  call with side-effects (such as updating the document when a plug-in\n  initializes), then you should first check to make sure that this effect\n  has not already occurred (e.g. by checking the state of the document or\n  undoing the change as a cleanup function returned by the effect)\n- `LexicalComposer`'s initialConfig prop is only considered once during\n  the first render (`useMemo` is used to create the `LexicalComposerContext`\n  which includes the editor and theme)\n- If you are using an `editorState` argument in the config when creating the\n  editor, it will only be called once when the editor is created.\n- You should generally prefer to use hooks that return state such as\n  `useLexicalEditable` (`useLexicalSubscription` is a generalization of this\n  style) rather than manually registering the listeners and expecting a\n  particular sequence of triggers to be called, especially\n  when their source is an effect. Listeners are only called when state\n  changes, and in StrictMode the state may have changed during the initial\n  render. The listeners registered from your second render will not be called\n  if the change was triggered by the first render, and you will likely not\n  see the listeners triggered during the first render because those effects\n  were immediately cleaned up before the change effect occurred.\n\n## LexicalComposerContext.useLexicalComposerContext: cannot find a LexicalComposerContext\n\nThis error happens for one reason: the `useLexicalComposerContext()` hook\nwas called from a component that is not a child of a `LexicalComposer`,\n`LexicalNestedComposer`, or `LexicalComposerContext.Provider` from the same\nbuild of Lexical that the hook was imported from.\n\nThe most common root causes of this issue are:\n\n- You are trying to use `useLexicalComposerContext()` in a component that is\n  not a child of the `LexicalComposer`. If you need to do that, you need to\n  pass the context or editor up the tree with something like `EditorRefPlugin`.\n- You have multiple builds of Lexical in your project. This could be because\n  you have a dependency that has a direct dependency on some other version\n  of Lexical (these packages should have Lexical as `peerDependencies`, but\n  not all do), or because your bundler resolves Lexical to different files\n  from different places (for example the `development` build in one and the\n  `production` build in another, or a `source` condition in one and `dist`\n  in another). Resolving this generally requires overriding what\n  your package manager does in `package.json`, and/or what the bundler does in\n  some configuration file for your framework or bundler. There are a lot of\n  combinations of tools in the ecosystem (npm, pnpm, yarn, webpack, vite,\n  next.js, etc.), so the syntax of that workaround is quite dependent on\n  precisely which tools (and even versions of those tools) that your project\n  is using. See [One Lexical per app](../concepts/one-lexical-per-app.md) for\n  why one copy is required, how to confirm you have more than one, and why a\n  library should declare Lexical in `peerDependencies`.\n\n## Hot Module Replacement (HMR)\n\nDuring development, HMR re-executes modules on every code change. Because Lexical uses object identity for node class registration, command dispatch, and extension deduplication, a naive HMR cycle destroys the editor state and resets the document.\n\n### HMRExtension\n\n`@lexical/extension` exports an `HMRExtension` that preserves editor state, the selection, the editable flag, and undo/redo history across HMR cycles. It stashes a reference to the current `EditorState` in the bundler's HMR data store, and the module instance that replaces it serializes that state and rebuilds it against the new node classes. Serializing only at that point keeps the cost off of every editor update.\n\nThe editor state and the undo/redo entries are serialized together, as one family of related states, rather than one at a time. Serializing each state on its own (`toJSON()` / `parseEditorState()`) would lose two properties the editor depends on: consecutive versions of a document share nearly all of their node objects, because an update clones only what it touches, and reconciliation is a diff over `NodeKey`s. Independently parsed states share nothing and have disjoint key spaces, so the history would cost memory proportional to its depth and the first undo after a reload would rebuild the whole document instead of the part that changed. The family format stores one entry per distinct node *version* and rebuilds each one once, so what was shared comes back shared and every version of a node answers to one key — which is also what lets the selection be carried by key.\n\n```ts\nimport {buildEditorFromExtensions, configExtension, defineExtension, HMRExtension} from '@lexical/extension';\nimport {RichTextExtension} from '@lexical/rich-text';\nimport {HistoryExtension} from '@lexical/history';\n\nconst editor = buildEditorFromExtensions(\n  defineExtension({\n    name: '[root]',\n    namespace: 'my-editor',\n    dependencies: [\n      RichTextExtension,\n      HistoryExtension,\n      configExtension(HMRExtension, {hot: import.meta.hot ?? null}),\n    ],\n  }),\n);\n```\n\nThe `hot` config accepts any object with a mutable `data: Record<string, unknown>` property — Vite's `import.meta.hot` satisfies this directly. Webpack and Parcel expose `module.hot` instead, whose `data` is populated by dispose handlers and is not directly mutable, so `module.hot` cannot be passed here; those bundlers need a small adapter built on `module.hot.addDisposeHandler`. Pass `null` in production or when HMR is not available; the extension becomes a no-op.\n\nWhen `HistoryExtension` is present as a peer, undo/redo stacks are preserved automatically. The extension does not declare `HistoryExtension` as a dependency — it detects it at runtime via peer dependency lookup.\n\nEditors with distinct `namespace` values — set via `namespace` in `defineExtension` or `createEditor` — are isolated automatically, so no `id` is needed. An editor given no namespace at all is keyed without one, since the namespace `createEditor` generates in that case is a new random string on every reload; those editors all share a single key. When two editors share both the same `import.meta.hot` context and the same key, pass a stable `id` string to keep their states independent:\n\n```ts\ndefineExtension({ name: '[main]', namespace: 'shared', dependencies: [configExtension(HMRExtension, {hot: import.meta.hot ?? null, id: 'main'})] })\ndefineExtension({ name: '[sidebar]', namespace: 'shared', dependencies: [configExtension(HMRExtension, {hot: import.meta.hot ?? null, id: 'sidebar'})] })\n```\n\nA nested editor inherits its parent's namespace, so the two are not isolated from each other automatically the way independent editors are; give the nested editor a distinct `id`. In development, sharing a key with the parent is warned about.\n\nSaved state belongs to the namespace and `id` for as long as the page lives, not to the reload that produced it. An editor that is unmounted and later remounted under the same key during development restores what the previous one had rather than its own `$initialEditorState` — so two editors showing different documents need distinct namespaces even when they are never on screen at the same time.\n\n### Fast Refresh compatibility\n\nVite (and similar tools) apply React Fast Refresh — state-preserving HMR for React components — only when a module exports nothing but React components. Modules that also export hooks, classes, commands, or constants fall back to a full remount, which discards component state.\n\nSeveral `@lexical/react` plugin modules split their non-component exports into companion `*Utils` files. Consumers that import non-component values directly from the `*Utils` module get more granular HMR boundaries, since changes to the component file don't invalidate those imports. The original module re-exports these for backwards compatibility.\n\nA companion module only earns that boundary if it imports no component module itself, so shared values it needs have to live somewhere component-free too: `MenuOption` — the class a menu plugin's options extend — is exported from `@lexical/react/LexicalMenuOption` for that reason, and re-exported from the plugin modules that used to define it.\n\nValues shared between entry points also have to be defined in one, not in an internal module: an internal module is inlined into every entry point that imports it, so a command or class defined there is a different object in each of them, and commands and `instanceof` are matched by identity.\n\nIf you're building custom plugins, follow the same pattern: keep React components in one file and export hooks, constants, or classes from a separate file.\n\n### Fallback: `// @refresh reset`\n\nIf a module can't be split (e.g. it defines both a component and tightly-coupled non-component logic), you can mark it for a full refresh using your framework's directive. For example, [Next.js fast refresh](https://nextjs.org/docs/architecture/fast-refresh#tips) supports a `// @refresh reset` comment at the top of the file. This forces a full remount of all components in the file on every change.\n","packages/lexical-website/docs/react/index.md":"---\nsidebar_label: 'Introduction'\n---\n\n# Lexical + React\n\nTo make it easier for React users to implement rich-text editors, Lexical exposes a set of abstractions that aim to cover a wide range of use cases out-of-the-box.\n\n<a\n    class=\"whitespace-nowrap rounded-md bg-blue-500 px-6 py-2 text-sm font-bold text-white transition-opacity hover:text-white hover:no-underline hover:opacity-90\"\n    href=\"/docs/getting-started/react\">\n{`Getting Started Guide`}\n</a>\n\n## Supported Versions\n\nReact 18+ (tested with React 18 and React 19)\n","packages/lexical-website/docs/react/plugins.md":"# Lexical Plugins\n\n:::tip\n\nMost of these plugins are available as modern\n[Lexical Extensions](../extensions/intro.md), which do not require separate\nconfiguration and can have more efficient implementations.\nYou should prefer using extensions over plugins where available, and\n[migrate](../extensions/migration.mdx) any project to extensions with\nLexicalExtensionComposer or LexicalExtensionEditorComposer if it is still\nusing LexicalComposer.\n\n:::\n\nReact-based plugins use a Lexical editor instance from `<LexicalExtensionComposer>` or `<LexicalComposer>` (legacy) context:\n\n```js\nimport {LexicalComposer} from '@lexical/react/LexicalComposer';\nimport {PlainTextPlugin} from '@lexical/react/LexicalPlainTextPlugin';\nimport {ContentEditable} from '@lexical/react/LexicalContentEditable';\nimport {LexicalErrorBoundary} from '@lexical/react/LexicalErrorBoundary';\nimport {HistoryPlugin} from '@lexical/react/LexicalHistoryPlugin';\nimport {OnChangePlugin} from '@lexical/react/LexicalOnChangePlugin';\n```\n\n```jsx\nconst initialConfig = {\n  namespace: 'MyEditor',\n  theme,\n  onError,\n};\n\n<LexicalComposer initialConfig={initialConfig}>\n  <PlainTextPlugin\n    contentEditable={\n      <ContentEditable placeholder={<div>Enter some text...</div>} />\n    }\n    ErrorBoundary={LexicalErrorBoundary}\n  />\n  <HistoryPlugin />\n  <OnChangePlugin onChange={onChange} />\n  ...\n</LexicalComposer>;\n```\n\n> Note: Many plugins might require you to register the one or many Lexical nodes in order for the plugin to work. You can do this by passing a reference to the node to the `nodes` array in your initial editor configuration.\n\n```jsx\nconst initialConfig = {\n  namespace: 'MyEditor',\n  theme,\n  nodes: [ListNode, ListItemNode], // Pass the references to the nodes here\n  onError,\n};\n```\n\n### `LexicalPlainTextPlugin`\n\nReact wrapper for `@lexical/plain-text` that adds major features for plain text editing, including typing, deletion and copy/pasting.\n\n```jsx\n<PlainTextPlugin\n  contentEditable={\n    <ContentEditable\n      aria-placeholder={'Enter some text...'}\n      placeholder={<div>Enter some text...</div>}\n    />\n  }\n  ErrorBoundary={LexicalErrorBoundary}\n/>\n```\n\n:::tip\n\nUse [PlainTextExtension](/docs/api/modules/lexical_plain-text#plaintextextension) when using extensions\n\n:::\n\n### Configuring an error boundary for React extensions\n\nThe `ErrorBoundary` prop on `PlainTextPlugin` and `RichTextPlugin` is configured\nthrough `ReactExtension` when using `PlainTextExtension` or\n`RichTextExtension`. Add a configured `ReactExtension` to the root extension's\ndependencies:\n\n```tsx\nimport {configExtension, defineExtension} from 'lexical';\nimport {PlainTextExtension} from '@lexical/plain-text';\nimport {ReactExtension} from '@lexical/react/ReactExtension';\nimport {LexicalErrorBoundary} from '@lexical/react/LexicalErrorBoundary';\nimport type {ReactElement} from 'react';\n\ntype ErrorBoundaryProps = {\n  children: ReactElement;\n  onError: (error: Error) => void;\n};\n\nfunction CustomErrorBoundary({children, onError}: ErrorBoundaryProps) {\n  return (\n    <LexicalErrorBoundary\n      fallback={<div>Something went wrong.</div>}\n      onError={onError}>\n      {children}\n    </LexicalErrorBoundary>\n  );\n}\n\nconst editorExtension = defineExtension({\n  name: 'MyEditor',\n  dependencies: [\n    PlainTextExtension,\n    configExtension(ReactExtension, {\n      ErrorBoundary: CustomErrorBoundary,\n    }),\n  ],\n});\n```\n\nIf you do not provide `ErrorBoundary`, `ReactExtension` uses its default\n`LexicalErrorBoundary`. Replace `CustomErrorBoundary` with\n`LexicalErrorBoundary` when the default fallback and error reporting are\nsufficient.\n\n### `LexicalRichTextPlugin`\n\nReact wrapper for `@lexical/rich-text` that adds major features for rich text editing, including typing, deletion, copy/pasting, indent/outdent and bold/italic/underline/strikethrough text formatting.\n\n```jsx\n<RichTextPlugin\n  contentEditable={\n    <ContentEditable\n      aria-placeholder={'Enter some text...'}\n      placeholder={<div>Enter some text...</div>}\n    />\n  }\n  ErrorBoundary={LexicalErrorBoundary}\n/>\n```\n\n:::tip\n\nUse [RichTextExtension](/docs/api/modules/lexical_rich-text#richtextextension) when using extensions\n\n:::\n\n### `LexicalAutoFocusPlugin`\n\nAutomatically focuses the editor when the component mounts and optionally places the cursor at the start or end of the root content.\n\n```jsx\n<AutoFocusPlugin defaultSelection=\"rootEnd\" />\n```\n\n:::tip\n\nUse [AutoFocusExtension](/docs/api/modules/lexical_extension#autofocusextension) when using extensions\n\n:::\n\n### `LexicalOnChangePlugin`\n\nPlugin that calls `onChange` whenever Lexical state is updated. Using `ignoreHistoryMergeTagChange` (`true` by default) and `ignoreSelectionChange` (`false` by default) can give more granular control over changes that are causing `onChange` call.\n\n```jsx\n<OnChangePlugin onChange={onChange} />\n```\n\n:::tip\n\nWhen using extensions, subscribe to [EditorStateExtension](/docs/api/modules/lexical_extension#editorstateextension) for a signal-based equivalent\n\n:::\n\n### `LexicalHistoryPlugin`\n\nReact wrapper for `@lexical/history` that adds support for history stack management and `undo` / `redo` commands.\n\n```jsx\n<HistoryPlugin />\n```\n\n:::tip\n\nUse [HistoryExtension](/docs/api/modules/lexical_history#historyextension) when using extensions\n\n:::\n\n### `LexicalLinkPlugin`\n\nReact wrapper for `@lexical/link` that adds support for links, including `$toggleLink` command support that toggles link for selected text.\n\n```jsx\n<LinkPlugin />\n```\n\n:::tip\n\nUse [LinkExtension](/docs/api/modules/lexical_link#linkextension) when using extensions\n\n:::\n\n### `LexicalClickableLinkPlugin`\n\nMakes `LinkNode` elements in the editor clickable, navigating to the link URL on click. Set `disabled` to `true` to turn off the behavior, or `newTab` to control whether links open in a new tab.\n\n> Note: Requires `LinkNode` from `@lexical/link` to be registered.\n\n```jsx\n<ClickableLinkPlugin newTab={true} disabled={false} />\n```\n\n:::tip\n\nUse [ClickableLinkExtension](/docs/api/modules/lexical_link#clickablelinkextension) when using extensions\n\n:::\n\n### `LexicalListPlugin`\n\nReact wrapper for `@lexical/list` that adds support for lists (ordered and unordered)\n\n```jsx\n<ListPlugin />\n```\n\n:::tip\n\nUse [ListExtension](/docs/api/modules/lexical_list#listextension) when using extensions\n\n:::\n\n### `LexicalCheckListPlugin`\n\nReact wrapper for `@lexical/list` that adds support for check lists. Note that it requires some css to render check/uncheck marks. See PlaygroundEditorTheme.css for details.\n\n```jsx\n<CheckListPlugin />\n```\n\n:::tip\n\nUse [CheckListExtension](/docs/api/modules/lexical_list#checklistextension) when using extensions\n\n:::\n\n### `LexicalTablePlugin`\n\n[![See API Documentation](/img/see-api-documentation.svg)](/docs/api/modules/lexical_react_LexicalTablePlugin)\n\nReact wrapper for `@lexical/table` that adds support for tables.\n\n```jsx\n<TablePlugin />\n```\n\n:::tip\n\nUse [TableExtension](/docs/api/modules/lexical_table#tableextension) when using extensions\n\n:::\n\n### `LexicalHorizontalRulePlugin`\n\nRegisters a command handler for `INSERT_HORIZONTAL_RULE_COMMAND`. When dispatched, it inserts a `HorizontalRuleNode` at the current selection.\n\n> Note: Requires `HorizontalRuleNode` to be registered. This plugin is deprecated in favor of `HorizontalRuleExtension` from `@lexical/extension`.\n\n```jsx\nimport {HorizontalRuleNode} from '@lexical/react/LexicalHorizontalRuleNode';\n\nconst config = {nodes: [HorizontalRuleNode]};\n\n<HorizontalRulePlugin />\n```\n\n:::tip\n\nUse [HorizontalRuleExtension](/docs/api/modules/lexical_extension#horizontalruleextension) when using extensions\n\n:::\n\n### `LexicalTabIndentationPlugin`\n\nPlugin that allows tab indentation in combination with `@lexical/rich-text`.\n\n```jsx\n<TabIndentationPlugin />\n```\n\n:::tip\n\nUse [TabIndentationExtension](/docs/api/modules/lexical_extension#tabindentationextension) when using extensions\n\n:::\n\n### `LexicalHashtagPlugin`\n\nAutomatically detects hashtag patterns (e.g., `#hello`) in text as the user types and converts them into `HashtagNode` instances with distinct styling.\n\n> Note: Requires `HashtagNode` from `@lexical/hashtag` to be registered.\n\n```jsx\nimport {HashtagNode} from '@lexical/hashtag';\n\nconst config = {nodes: [HashtagNode]};\n\n<HashtagPlugin />\n```\n\n:::tip\n\nUse [HashtagExtension](/docs/api/modules/lexical_hashtag#hashtagextension) when using extensions\n\n:::\n\n### `LexicalAutoLinkPlugin`\n\nPlugin will convert text into links based on passed matchers list. In example below whenever user types url-like string it will automatically convert it into a link node\n\n> Note: Requires `AutoLinkNode` from `@lexical/link` to be registered.\n\n```jsx\nconst URL_MATCHER =\n  /((https?:\\/\\/(www\\.)?)|(www\\.))[-a-zA-Z0-9@:%._+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/;\n\nconst MATCHERS = [\n  (text) => {\n    const match = URL_MATCHER.exec(text);\n    if (match === null) {\n      return null;\n    }\n    const fullMatch = match[0];\n    return {\n      index: match.index,\n      length: fullMatch.length,\n      text: fullMatch,\n      url: fullMatch.startsWith('http') ? fullMatch : `https://${fullMatch}`,\n      // attributes: { rel: 'noreferrer', target: '_blank' }, // Optional link attributes\n    };\n  },\n];\n\n...\n\n<AutoLinkPlugin matchers={MATCHERS} />\n```\n\n:::tip\n\nUse [AutoLinkExtension](/docs/api/modules/lexical_link#autolinkextension) when using extensions\n\n:::\n\n### `LexicalAutoEmbedPlugin`\n\nWatches for pasted links that match any of the provided embed configurations (e.g., YouTube, Twitter URLs). When a match is found, it shows a menu offering to replace the link with an embedded node.\n\n> Note: Requires `LinkNode` and `AutoLinkNode` from `@lexical/link` to be registered.\n\n```jsx\nimport {LexicalAutoEmbedPlugin, AutoEmbedOption} from '@lexical/react/LexicalAutoEmbedPlugin';\nimport type {EmbedConfig} from '@lexical/react/LexicalAutoEmbedPlugin';\n\nconst YouTubeEmbedConfig = {\n  type: 'youtube',\n  parseUrl: (url) => {\n    const match = url.match(/youtube\\.com\\/watch\\?v=([a-zA-Z0-9_-]+)/);\n    return match ? {url, id: match[1]} : null;\n  },\n  insertNode: (editor, result) => {\n    // Insert your custom embed node here\n  },\n};\n\n<LexicalAutoEmbedPlugin\n  embedConfigs={[YouTubeEmbedConfig]}\n  onOpenEmbedModalForConfig={(config) => {/* open modal */}}\n  getMenuOptions={(config, embedFn, dismissFn) => [\n    new AutoEmbedOption('Embed', {onSelect: embedFn}),\n    new AutoEmbedOption('Dismiss', {onSelect: dismissFn}),\n  ]}\n/>\n```\n\nThe optional `$disableMenuFor` predicate suppresses the menu for specific locations, e.g. inside nodes that only accept inline content. It receives the matched `AutoLinkNode`; when it returns true the menu is not shown and the pasted link is left as-is. The predicate can inspect the node's ancestors, e.g. with `$getNearestNodeOfType` from `@lexical/utils`.\n\n```jsx\n<LexicalAutoEmbedPlugin\n  embedConfigs={[YouTubeEmbedConfig]}\n  $disableMenuFor={(linkNode) =>\n    $getNearestNodeOfType(linkNode, InlineOnlyNode) !== null\n  }\n  // ...\n/>\n```\n\n### `LexicalCharacterLimitPlugin`\n\nTracks the number of characters in the editor and renders a count of remaining characters. Uses `OverflowNode` to visually indicate when the limit is exceeded.\n\n> Note: Requires `OverflowNode` from `@lexical/overflow` to be registered.\n\n```jsx\nimport {CharacterLimitPlugin} from '@lexical/react/LexicalCharacterLimitPlugin';\n\n<CharacterLimitPlugin charset=\"UTF-16\" maxLength={280} />\n```\n\n:::tip\n\nUse [OverflowExtension](/docs/api/modules/lexical_overflow#overflowextension) when using extensions\n\n:::\n\n### `LexicalClearEditorPlugin`\n\nAdds `clearEditor` command support to clear editor's content.\n\n```jsx\n<ClearEditorPlugin />\n```\n\n:::tip\n\nUse [ClearEditorExtension](/docs/api/modules/lexical_extension#cleareditorextension) when using extensions\n\n:::\n\n### `LexicalMarkdownShortcutPlugin`\n\nAdds markdown shortcut support: headings, lists, code blocks, quotes, links and inline styles (bold, italic, strikethrough).\n\n```jsx\n<MarkdownShortcutPlugin />\n```\n\n### `LexicalTableOfContentsPlugin`\n\nThis plugin allows you to render a table of contents for a page from the headings from the editor. It listens to any deletions or modifications to those headings and updates the table of contents. Additionally, it's able to track any newly added headings and inserts them in the table of contents once they are created. This plugin also supports lazy loading - so you can defer adding the plugin until when the user needs it.\n\nIn order to use `TableOfContentsPlugin`, you need to pass a callback function in its children. This callback function gives you access to the up-to-date data of the table of contents. You can access this data through a single parameter for the callback which comes in the form of an array of arrays `[[headingKey, headingTextContent, headingTag], [], [], ...]`\n\n`headingKey`: Unique key that identifies the heading.\n`headingTextContent`: A string of the exact text of the heading.\n`headingTag`: The heading's tag — `'h1'` through `'h6'`. Every heading in the document is listed; the plugin does not filter by level.\n\n```jsx\n<TableOfContentsPlugin>\n  {(tableOfContentsArray) => {\n    return (\n      <MyCustomTableOfContentsPlugin tableOfContents={tableOfContentsArray} />\n    );\n  }}\n</TableOfContentsPlugin>\n```\n\n### `LexicalEditorRefPlugin`\n\nAllows you to get a ref to the underlying editor instance outside of LexicalComposer, which is convenient when you want to interact with the editor\nfrom a separate part of your application.\n\n```jsx\nconst editorRef = useRef(null);\n<EditorRefPlugin editorRef={editorRef} />;\n```\n\n### `LexicalCollaborationPlugin`\n\nIntegrates Yjs-based real-time collaborative editing into Lexical. Creates a Yjs binding between the editor state and a shared Yjs document, renders remote user cursors, and provides collaborative undo/redo history.\n\n```jsx\nimport {LexicalCollaboration} from '@lexical/react/LexicalCollaborationContext';\nimport {CollaborationPlugin} from '@lexical/react/LexicalCollaborationPlugin';\nimport {WebsocketProvider} from 'y-websocket';\nimport {Doc} from 'yjs';\n\nfunction createProvider(id, yjsDocMap) {\n  const doc = new Doc();\n  yjsDocMap.set(id, doc);\n  return new WebsocketProvider('ws://localhost:1234', id, doc);\n}\n\n<LexicalCollaboration>\n  <LexicalComposer initialConfig={initialConfig}>\n    <CollaborationPlugin\n      id=\"my-document\"\n      providerFactory={createProvider}\n      shouldBootstrap={true}\n      username=\"Alice\"\n      cursorColor=\"#FF0000\"\n    />\n  </LexicalComposer>\n</LexicalCollaboration>\n```\n\n### `LexicalDraggableBlockPlugin`\n\nAdds a draggable block handle that appears next to top-level block nodes when hovered. Users can drag and drop blocks to reorder them. A target line indicates the drop position.\n\n> Note: This plugin is experimental.\n\n```jsx\nimport {DraggableBlockPlugin_EXPERIMENTAL} from '@lexical/react/LexicalDraggableBlockPlugin';\n\nconst menuRef = useRef(null);\nconst targetLineRef = useRef(null);\n\n<DraggableBlockPlugin_EXPERIMENTAL\n  anchorElem={document.body}\n  menuRef={menuRef}\n  targetLineRef={targetLineRef}\n  menuComponent={<div ref={menuRef} className=\"drag-handle\">::</div>}\n  targetLineComponent={<div ref={targetLineRef} className=\"target-line\" />}\n  isOnMenu={(el) => menuRef.current?.contains(el) ?? false}\n/>\n```\n\n### `LexicalNodeEventPlugin`\n\nAttaches a DOM event listener on the editor root element that fires only when the event target corresponds to a node of the specified type.\n\n```jsx\nimport {NodeEventPlugin} from '@lexical/react/LexicalNodeEventPlugin';\n\n<NodeEventPlugin\n  nodeType={ImageNode}\n  eventType=\"click\"\n  eventListener={(event, editor, nodeKey) => {\n    console.log('Clicked on node:', nodeKey);\n  }}\n/>\n```\n\n### `LexicalNodeMenuPlugin`\n\nRenders a menu anchored to a specific Lexical node identified by `nodeKey`. Provides keyboard navigation (arrow keys, enter, escape) through the menu options.\n\n```jsx\nimport {LexicalNodeMenuPlugin, MenuOption} from '@lexical/react/LexicalNodeMenuPlugin';\n\n<LexicalNodeMenuPlugin\n  nodeKey={selectedNodeKey}\n  options={menuOptions}\n  onSelectOption={(option, textNode, closeMenu) => {\n    option.action();\n    closeMenu();\n  }}\n  menuRenderFn={(anchorRef, {options, selectedIndex, selectOptionAndCleanUp}) =>\n    anchorRef.current\n      ? createPortal(\n          <ul>\n            {options.map((opt, i) => (\n              <li key={opt.key} onClick={() => selectOptionAndCleanUp(opt)}>\n                {opt.title}\n              </li>\n            ))}\n          </ul>,\n          anchorRef.current,\n        )\n      : null\n  }\n  onClose={() => setSelectedNodeKey(null)}\n/>\n```\n\n### `LexicalTypeaheadMenuPlugin`\n\nProvides a typeahead/autocomplete menu that appears when the user types a trigger character (e.g., `@` for mentions, `:` for emoji). It listens to text changes, runs a trigger function against text before the cursor, and when a match is found, positions a menu at the trigger location.\n\n```jsx\nimport {\n  LexicalTypeaheadMenuPlugin,\n  useBasicTypeaheadTriggerMatch,\n} from '@lexical/react/LexicalTypeaheadMenuPlugin';\n\nconst triggerMatch = useBasicTypeaheadTriggerMatch('@', {minLength: 1});\n\n<LexicalTypeaheadMenuPlugin\n  triggerFn={triggerMatch}\n  options={mentionOptions}\n  onQueryChange={setQuery}\n  onSelectOption={(option, textNode, closeMenu) => {\n    editor.update(() => {\n      const mentionNode = $createMentionNode(option.name);\n      if (textNode) {\n        textNode.replace(mentionNode);\n      }\n      closeMenu();\n    });\n  }}\n  menuRenderFn={(anchorRef, {options, selectedIndex, selectOptionAndCleanUp}) =>\n    anchorRef.current\n      ? createPortal(\n          <ul className=\"typeahead-menu\">\n            {options.map((opt, i) => (\n              <li\n                key={opt.key}\n                className={i === selectedIndex ? 'selected' : ''}\n                onClick={() => selectOptionAndCleanUp(opt)}>\n                {opt.name}\n              </li>\n            ))}\n          </ul>,\n          anchorRef.current,\n        )\n      : null\n  }\n/>\n```\n\n### `LexicalSelectionAlwaysOnDisplay`\n\nBy default, browser text selection becomes invisible when clicking away from the editor. This plugin ensures the selection remains visible.\n\n```jsx\n<SelectionAlwaysOnDisplay />\n```\n\n:::tip\n\nUse [SelectionAlwaysOnDisplayExtension](/docs/api/modules/lexical_extension#selectionalwaysondisplayextension) when using extensions\n\n:::\n","packages/lexical-website/docs/serialization/_category_.json":"{\n  \"label\": \"Serialization\",\n  \"position\": 4\n}\n","packages/lexical-website/docs/serialization/dom-import.md":"# DOMImportExtension\n\n:::warning Experimental\n\n`DOMImportExtension` and everything described on this page are marked\n`@experimental` and may change between any two Lexical releases —\nincluding breaking renames, signature changes, or behavior changes —\nuntil the API stabilizes. We track issues and proposals in the\n[GitHub repo](https://github.com/facebook/lexical); breaking changes\nwill be called out in release notes. Apps that depend on this\npipeline should pin their Lexical version and treat upgrades as\nintentional.\n\nThe legacy static `importDOM` machinery and `$generateNodesFromDOM`\nentry are unchanged and remain the supported default for production\napps that don't want to track an experimental API.\n\nThe DOMImportExtension API was introduced in Lexical v0.45.0\n\n:::\n\nThe DOM import system in `@lexical/html` lets you convert any HTML or\nDOM tree into Lexical nodes. The legacy entry — the static\n`importDOM` declared on each node class — still works, but for new\ncode we offer an extension-based pipeline (`DOMImportExtension`) with\ntyped selectors, middleware-style rules, structural schemas,\nconfigurable whitespace handling, and a dedicated context system. It's\ndesigned for performance, ergonomics, and composability across\nextensions.\n\nThe new pipeline ships side-by-side with the legacy one in\n`@lexical/html`; the default `$generateNodesFromDOM` is unchanged. The\nnode-providing extensions (`RichTextExtension`, `ListExtension`,\n`LinkExtension`, `TableExtension`, `CodeExtension`, …) register their\nown import rules (plus the shared `CoreImportExtension` baseline), so\nan editor built from them already has a fully configured\n`DOMImportExtension` — to opt in, just read the imported nodes from\nthe extension's `$generateNodesFromDOM` output (or add\n`ClipboardDOMImportExtension` to route pastes through it).\n\n## Quick start\n\nThe smallest editor that imports HTML through the extension pipeline:\n\n```ts\nimport {\n  buildEditorFromExtensions,\n  getExtensionDependencyFromEditor,\n} from '@lexical/extension';\nimport {\n  CoreImportExtension,\n  DOMImportExtension,\n} from '@lexical/html';\nimport {$getRoot, defineExtension} from 'lexical';\n\nconst editor = buildEditorFromExtensions(\n  defineExtension({\n    name: 'app',\n    dependencies: [CoreImportExtension],\n  }),\n);\n\nconst dep = getExtensionDependencyFromEditor(editor, DOMImportExtension);\neditor.update(() => {\n  const dom = new DOMParser().parseFromString(\n    '<p>Hello <strong>world</strong></p>',\n    'text/html',\n  );\n  const nodes = dep.output.$generateNodesFromDOM(dom);\n  $getRoot().clear().splice(0, 0, nodes);\n});\n```\n\n`CoreImportExtension` ships the rules that cover `<p>`, `<span>`,\ninline format tags (`<b>`, `<strong>`, `<em>`, `<i>`, `<code>`,\n`<mark>`, `<s>`, `<sub>`, `<sup>`, `<u>`), `<br>`, and `#text` —\neverything the core lexical package's legacy `importDOM` machinery\nhandled — plus `<hr>`, gated on `HorizontalRuleNode` being registered\n(the node lives in `@lexical/extension`, which is upstream of\n`@lexical/html` and so cannot register import rules itself).\n\nThe per-tag rules ship with the extensions that provide the nodes, so\nlisting the node extensions you want is the entire import\nconfiguration:\n\n| Extension | Provides |\n| --- | --- |\n| `CoreImportExtension` (`@lexical/html`) | `<p>`, `<span>`, inline format tags, `<br>`, `#text`, `<hr>` (when `HorizontalRuleNode` is registered) |\n| `RichTextExtension` (`@lexical/rich-text`) | `<h1>`–`<h6>`, `<blockquote>`, Google Docs 26pt-title heuristic |\n| `ListExtension` (`@lexical/list`) | `<ol>`, `<ul>` (incl. checklist detection), `<li>`, GitHub task list, Joplin checkbox |\n| `LinkExtension` (`@lexical/link`) | `<a>` |\n| `TableExtension` (`@lexical/table`) | `<table>`, `<tr>`, `<td>`, `<th>` |\n| `CodeExtension` (`@lexical/code-core`) | `<pre>`, multi-line `<code>`, monospace `<div>`, GitHub raw-file-view tables |\n\nEach of these depends on `CoreImportExtension` itself, so the shared\nbaseline comes along automatically. The rules are inert unless the\neditor actually routes HTML through the pipeline (the legacy paste\npath is unchanged), and an extension's rules merge above its\ndependencies' rules, so app-registered rules still win dispatch.\n\n:::tip\n\nThe legacy entry `$generateNodesFromDOM(editor, dom)` and the new\nextension's `dep.output.$generateNodesFromDOM(dom)` both live in\n`@lexical/html`. Tell them apart by signature: the legacy takes the\neditor first, the new one takes only the DOM (plus optional\n`{context, preprocess}`).\n\n:::\n\n## Rules\n\nA rule is the unit of work in the extension. Each rule is built with\n`defineImportRule`:\n\n```ts\nimport {defineImportRule, sel} from '@lexical/html';\nimport {$createParagraphNode} from 'lexical';\n\nconst ParagraphRule = defineImportRule({\n  name: '@app/p',\n  match: sel.tag('p'),\n  $import: (ctx, el) => {\n    const p = $createParagraphNode();\n    p.splice(0, 0, ctx.$importChildren(el));\n    return [p];\n  },\n});\n```\n\nA rule has three parts:\n\n1. **`match`** — an opaque `CompiledSelector` produced by `sel.*`. The\n   dispatcher buckets rules by tag at compile time so the per-node\n   dispatch cost is bounded by the number of rules registered for\n   that tag (typically one or two).\n2. **`$import`** — middleware-style function `(ctx, node, $next) =>\n   readonly LexicalNode[]`. The element type of `node` is narrowed by\n   the selector (e.g. `sel.tag('a')` gives `HTMLAnchorElement`,\n   `sel.text()` gives `Text`). Return `[]` to drop the element, or\n   call `$next()` to defer to the next-lower matching rule.\n3. **`name`** (optional) — surfaces in dev-mode warnings and error\n   traces. Convention: `'@scope/package/rule-id'`.\n\n### Dispatch order\n\nThere is no numeric priority. `DOMImportConfig.rules` is a single flat,\nordered list, and dispatch walks it **front to back**: for a given DOM\nnode the dispatcher visits each rule whose `match` accepts that node, in\nlist order, and the first rule that returns without calling `$next()`\ndecides the outcome. \"Higher priority\" and \"earlier in the list\" are the\nsame thing.\n\nSo within one `configExtension(DOMImportExtension, {rules})` call, the\nrules run in the order you wrote them:\n\n```ts\nconfigExtension(DOMImportExtension, {\n  rules: [\n    // Tries this first\n    SpecificParagraphRule,\n    // Falls back to this via $next()\n    GenericParagraphRule,\n  ],\n})\n```\n\nCalling `$next()` from a rule walks the chain — the next matching rule\nin the list fires; if none is left, the framework's catch-all\n`DefaultHoistRule` descends into the element's children. Returning `[]`\nfrom a rule short-circuits: nothing else runs and the element is\ndropped.\n\n#### How the list is assembled\n\nMany extensions contribute to the same list. `DOMImportExtension`'s\n`mergeConfig` **prepends** each contribution to what has accumulated so\nfar, and the extension builder merges configs in dependency order — a\ndependency's contribution is merged before the contribution of the\nextension that depends on it. Two consequences:\n\n- **Each contribution stays contiguous and keeps its own order.** Your\n  `rules` array is inlined as one chunk, first entry first. Ordering\n  *within* your own rules is entirely up to you.\n- **The chunks run most-dependent first.** An extension's rules are\n  tried before the rules contributed by its dependencies. That is what\n  makes overriding work: `@lexical/rich-text` depends on\n  `CoreImportExtension`, so its `<p>` rule is reached before the core\n  one, and an app extension that depends on `@lexical/rich-text` gets to\n  go ahead of both.\n\nConcretely, for an app extension that depends on `RichTextExtension`\n(which in turn depends on `CoreImportExtension`), the compiled list is:\n\n```\n[ …rules passed to buildEditorFromExtensions… ]  ← merged last, highest priority\n[ …the app extension's rules…                 ]\n[ …@lexical/rich-text's rules…                ]\n[ …CoreImportExtension's rules…               ]\n[ DefaultHoistRule                            ]  ← the base config, always last\n```\n\nConfiguration handed straight to `buildEditorFromExtensions` is merged\nafter every extension's, so it outranks all of them. `DefaultHoistRule`\nis `DOMImportExtension`'s own default `config.rules` entry, so\neverything else lands in front of it and it is always the last rule\ntried.\n\n:::caution\n\nThe relative order of two extensions where neither transitively depends\non the other falls out of the topological sort and is not part of the\nAPI. If extension **A** must override a rule from extension **B**, say\nso structurally — make **B** a dependency of **A** — rather than relying\non the order they happen to be listed in.\n\n:::\n\n### `$next()` as a wrapper\n\nThe middleware shape is strictly more powerful than the legacy\nnumeric `priority`. A rule can call `$next()`, inspect the result,\nand transform it — perfect for cross-cutting decorators:\n\n```ts\nconst IdAttributeRule = defineImportRule({\n  name: '@app/id-decorator',\n  match: sel.any().attr('id', /\\S/),\n  $import: (ctx, el, $next) => {\n    const out = $next();\n    if (out.length === 1) {\n      $setState(out[0], idState, el.getAttribute('id')!);\n    }\n    return out;\n  },\n});\n```\n\nPlaced early in your `rules` array, this rule fires for every element\ncarrying an `id` BEFORE the tag-specific rule, calls `$next()` to get\nthe produced node, and tags it with state. No tag-specific machinery\nneeds to know about `id`.\n\n## Selectors\n\nBuild selectors with the combinator API:\n\n```ts\nsel.any()                                       // any HTMLElement\nsel.tag('a')                                    // <a> only (typed as HTMLAnchorElement)\nsel.tag('h1', 'h2', 'h3', 'h4', 'h5', 'h6')     // typed as HTMLHeadingElement\nsel.text()                                      // text nodes\nsel.comment()                                   // comment nodes\nsel.tag('li').classAll('task-list-item')        // <li class=\"task-list-item …\">\nsel.tag('span').classAny('hl', 'mark')          // <span class=\"hl|mark …\">\nsel.tag('a').attr('href', /^https:/)            // <a href> matching a regex\nsel.tag('a').attr('target', true)               // attribute present\nsel.tag('a').attr('href', '/wiki')              // exact value\nsel.tag('span').styleAny('font-size', /^(\\d+)pt/) // inline-style match\n```\n\nA CSS-subset parser is also available for terse selectors:\n\n```ts\nsel.css('p.google-docs-title')      // tag + class\nsel.css('h1, h2, h3, h4, h5, h6')   // tag list\nsel.css('img[src]')                 // attribute presence\nsel.css('a[href=\"/wiki\"]')          // attribute equality\n```\n\n`sel.css` covers the basics (tag, `.class`, `#id`, `[attr]`,\n`[attr=\"val\"]`, comma-separated lists, `*`). For anything outside that\nsubset (regex attribute, inline-style match, capturing), chain\ncombinator methods off the result:\n\n```ts\nsel.css('pre').attr('class', /(?:^|\\s)language-(\\S+)/, {capture: 'lang'})\n```\n\nThe grammar deliberately excludes descendant combinators (`>`, ` `)\nand `:not` — see [Context](#context) below for the idiomatic\nreplacements.\n\n### Captured matches\n\nPass `{capture: 'name'}` to `attr` or `styleAny` and the\n`RegExpMatchArray` for the successful match is surfaced on\n`ctx.captures.name`, fully typed:\n\n```ts\nconst CodeBlockRule = defineImportRule({\n  name: '@app/pre-code',\n  match: sel\n    .tag('pre')\n    .attr('class', /(?:^|\\s)language-(\\S+)/, {capture: 'lang'}),\n  $import: (ctx, el) => {\n    // ctx.captures.lang: RegExpMatchArray\n    const language = ctx.captures.lang[1];\n    const node = $createCodeNode(language);\n    node.splice(0, 0, ctx.$importChildren(el));\n    return [node];\n  },\n});\n```\n\nThe selector's element type and captures map both flow into `$import`'s\nsignature, so you never need to cast `el` or re-run the regex inside\nthe rule body.\n\n:::tip\n\nIf your selector becomes a runtime function (a `(node) => boolean`\nguard), the rule lands in the wildcard bucket and is consulted for\nevery element — expensive. Push as much as possible into the selector\nitself; do the remaining refinement inside the rule body, calling\n`$next()` to defer when the body decides not to handle the element.\n\n```ts\ndefineImportRule({\n  match: sel.tag('img'),  // tag bucket — cheap\n  $import: (ctx, el, $next) => {\n    if (!el.src || el.src.startsWith('data:')) {\n      return $next();      // body refinement\n    }\n    return [$createImageNode({src: el.src, alt: el.alt})];\n  },\n});\n```\n\n:::\n\n## Schemas\n\nA rule's `$import` body uses `ctx.$importChildren(parent, {schema})`\nto recursively import children. The schema enforces what node types\nare allowed in this position and how to package non-conforming runs:\n\n```ts\ninterface ChildSchema {\n  $accepts(child: LexicalNode, parent: LexicalNode | null): boolean;\n  $packageRun?(rejected: LexicalNode[], ...): LexicalNode[];\n  onReject?: 'hoist' | 'drop';\n  $finalize?(children: LexicalNode[], ...): LexicalNode[];\n}\n```\n\nThe `$`-prefixed members are invoked inside the import walk (which\nalready runs inside `editor.update`), so they can call `$is*`\npredicates and node methods on the `LexicalNode` arguments directly.\n\nBuilt-ins, all from `@lexical/html`:\n\n| Schema | Accepts | Rejected runs |\n| --- | --- | --- |\n| `BlockSchema` | Block ElementNodes and block DecoratorNodes | Wrapped in a fresh `ParagraphNode` (preserves `text-align`) |\n| `RootSchema` | Same as `BlockSchema` — aliased for clarity at the entry point | Same |\n| `InlineSchema` | Inline ElementNodes, TextNodes, inline DecoratorNodes | Dropped |\n| `NestedBlockSchema` | Block nodes | Inline runs pass through unchanged (no extra paragraph wrapping) |\n\nPer-package schemas:\n\n| Schema | Accepts | Rejected runs |\n| --- | --- | --- |\n| `ListSchema` (`@lexical/list`) | `ListItemNode`, nested `ListNode` | Wrapped in a synthetic `ListItemNode` (no intermediate paragraph) |\n| `TableSchema` (`@lexical/table`) | `TableRowNode` | If all cells, wrapped in a synthetic `TableRowNode`; else dropped |\n| `TableRowSchema` (`@lexical/table`) | `TableCellNode` | Dropped |\n\nA rule typically passes a schema when it knows its children's\nallowed type — the legacy `wrapContinuousInlines` and\n`ArtificialNode__DO_NOT_USE` cases are now `BlockSchema` and\n`NestedBlockSchema` respectively:\n\n```ts\nconst HeadingRule = defineImportRule({\n  match: sel.tag('h1', 'h2', 'h3', 'h4', 'h5', 'h6'),\n  $import: (ctx, el) => {\n    const node = $createHeadingNode(el.nodeName.toLowerCase() as HeadingTagType);\n    node.splice(0, 0, ctx.$importChildren(el, {schema: InlineSchema}));\n    return [node];\n  },\n});\n```\n\nCustom schemas implement `ChildSchema` directly. `$accepts`,\n`$packageRun`, and `$finalize` are invoked inside the editor context\nof the surrounding walk, so they receive `LexicalNode` arguments and\ncan call `$is*` predicates / node methods without setting up their\nown `editor.read`.\n\n### When `$packageRun` runs and `onReject` doesn't\n\nThe schema walks the assembled child array once, partitioning into\naccepted children (passed through individually) and maximal runs of\nnon-accepted children. For each rejected run, `$packageRun` runs\nfirst; if it's missing or returns `[]`, `onReject` decides:\n\n- `'drop'` (default) silently discards the run.\n- `'hoist'` emits the run unchanged at the same position. This\n  surfaces the mixed content to the calling rule's parent — only\n  useful when the rule's caller is prepared to receive mixed\n  children. It does NOT lift the run *out* of the calling rule's\n  parent.\n\n`$packageRun` is the right hook when the rejected content has a\nsensible re-shape that still belongs *inside* the rule's parent —\ne.g. `BlockSchema` wraps a run of inline siblings in a\n`ParagraphNode`, `TableSchema` wraps cells in a synthesized row.\n\n### Lifting blocks out of an inline parent\n\nSchemas cannot change the parent the rule already created. The\ncanonical example is an inline element with block children:\n\n```html\n<a href=\"link\"><h1>some text</h1><div>more text</div></a>\n```\n\nThe desired output is to lift each block out of the link and\nredistribute the link onto the block's inline contents:\n\n```\nHeadingNode\n└─ LinkNode(\"link\")\n   └─ TextNode(\"some text\")\nParagraphNode\n└─ LinkNode(\"link\")\n   └─ TextNode(\"more text\")\n```\n\n`InlineSchema` doesn't help here: with `onReject: 'drop'` the\nheadings are lost, with `'hoist'` they emerge unwrapped, and there's\nno `$packageRun` shape that produces \"a list of blocks each holding\nthe original inline wrapper\" because the schema sees only the\nchildren, not the parent.\n\nThis is a rule-level concern, but `@lexical/html` ships the\n`$distributeInlineWrapper(children, $makeWrapper)` helper so rules\nthat produce an inline parent (e.g. `LinkNode`, `MarkNode`) can\nopt in with one line:\n\n```ts\nimport {\n  $distributeInlineWrapper,\n  defineImportRule,\n  sel,\n} from '@lexical/html';\nimport {$createLinkNode} from '@lexical/link';\n\nconst AnchorRule = defineImportRule({\n  match: sel.tag('a'),\n  $import: (ctx, el) => {\n    const href = el.getAttribute('href') || '';\n    return $distributeInlineWrapper(ctx.$importChildren(el), () =>\n      $createLinkNode(href),\n    );\n  },\n});\n```\n\nThe helper walks the children: each block is descended into and its\ninline content is wrapped with a fresh instance from `$makeWrapper`;\neach contiguous inline run at the top level becomes its own wrapped\nnode. The enclosing `BlockSchema` then packages the inline-wrapped\nruns into paragraphs as usual.\n\n## Context\n\n`ImportContext` is the per-rule environment exposed as `ctx` to\n`$import`. It provides:\n\n- `ctx.captures` — typed regex captures from this rule's selector.\n- `ctx.get(cfg)` — read an `ImportStateConfig` value.\n- `ctx.$importChildren(parent, opts)` — recurse into children.\n- `ctx.$importOne(node, opts)` — recurse into one node.\n- `ctx.session` — per-import mutable store (see [Sessions](#sessions)).\n\n### State configs and `ctx.get`\n\n`createImportState` mints a typed key, similar to `createState` for\nnode state but scoped to the import pipeline:\n\n```ts\nimport {createImportState} from '@lexical/html';\n\nconst ImportMode = createImportState<'paste' | 'deserialize'>(\n  'app/importMode',\n  () => 'paste',\n);\n```\n\nRules read it with `ctx.get(ImportMode)`. The value is supplied\neither as a `contextDefaults` entry on the extension config (sticks\nfor every import call) or as a per-call `context` override on\n`$generateNodesFromDOM`:\n\n```ts\nimport {contextValue} from '@lexical/html';\n\ndep.output.$generateNodesFromDOM(dom, {\n  context: [contextValue(ImportMode, 'deserialize')],\n});\n```\n\nA nested `$importChildren` can also branch context for its descendants\nvia `opts.context`:\n\n```ts\nimport {contextValue, ImportTextFormat} from '@lexical/html';\nimport {IS_BOLD} from 'lexical';\n\nctx.$importChildren(el, {\n  context: [contextValue(ImportTextFormat, ctx.get(ImportTextFormat) | IS_BOLD)],\n});\n```\n\nBranched values are restored on the way back out — siblings outside\nthe branched subtree see the unchanged inherited value.\n\n### Built-in states\n\n`@lexical/html` ships several states out of the box:\n\n- **`ImportSource`** (`'paste' | 'unknown'`) — identifies how this\n  import started. Defaults to `'unknown'`; the clipboard handler sets\n  `'paste'`. Rules can branch on it to adapt behavior (e.g. preserve\n  more whitespace on `'paste'`). Apps that need a richer vocabulary\n  (e.g. `'drop'`, `'deserialize'`) can define their own\n  `ImportStateConfig` and pass it through the same `context` slot.\n- **`ImportSourceDataTransfer`** (`DataTransfer | null`) — the original\n  `DataTransfer` the paste/drop came from, surfaced so rules can peek\n  at companion MIME types or attached files. `null` outside clipboard\n  imports. See [below](#importsourcedatatransfer).\n- **`ImportTextFormat`** (`number`, a `TextFormatType` bitmask) —\n  used by the inline-format rules (`<b>`, `<strong>`, `<em>`, …) to\n  propagate format bits to descendant TextNodes. Replaces the legacy\n  `forChild` chain that the inline tags previously used. Build your\n  own format propagation by branching this state.\n- **`ImportTextStyle`** (`Readonly<Record<string, string>>`) — parsed\n  CSS-property record (e.g. `{color: 'red', 'font-family': 'serif'}`)\n  inherited by descendant TextNodes and materialized onto their\n  `style` string. Used by the core `#text` rule to pick up\n  inheritable cell/span styles like `color`. Properties already\n  covered by `ImportTextFormat` (`font-weight`, `font-style`,\n  `text-decoration`, `vertical-align`) are filtered out so they\n  remain owned by the format-bit path.\n- **`ImportOverlays`** — session slot (`readonly CompiledOverlayRules[]`)\n  holding overlays installed during the preprocess phase. The walker\n  primes its overlay stack from this list before starting, so a\n  preprocess can scope an overlay to the whole document based on a\n  signal it detected (e.g. a Word `Generator` meta tag). See\n  [Preprocessors](#preprocessors).\n- **`ImportWhitespaceConfig`** — controls text-node whitespace\n  handling (which DOM elements preserve whitespace, which count as\n  inline siblings). See below.\n\n### `ImportWhitespaceConfig`\n\nThe text-node whitespace handler walks DOM siblings to decide\nwhether trailing/leading spaces should be preserved (the neighbor is\ninline text) or collapsed (the neighbor is a block boundary). Two\npredicates govern this:\n\n```ts\ninterface WhitespaceImportConfig {\n  preservesWhitespace: (node: Node) => boolean;\n  isInline: (node: Node) => boolean;\n}\n```\n\nThe defaults match the legacy behavior: an element preserves\nwhitespace if its `nodeName` is `'PRE'` or its inline `style.whiteSpace`\nstarts with `'pre'`; an element is inline if its inline `style.display`\nstarts with `'inline'` or its tag name is in the standard inline-tag\nset used by lexical core's `isInlineDomNode` / `isBlockDomNode`\nhelpers (`a`, `abbr`, `acronym`, `b`, `cite`, `code`, `del`, `em`,\n`i`, `ins`, `kbd`, `label`, `mark`, `output`, `q`, `ruby`, `s`,\n`samp`, `span`, `strong`, `sub`, `sup`, `time`, `u`, `tt`, `var`).\nThe canonical list lives in `packages/lexical/src/LexicalUtils.ts` —\nsee the module-private `INLINE_TAG_RE` / `BLOCK_TAG_RE` constants if you\nwant to inspect the defaults.\n\nTo recognize custom tags (e.g. a custom `<tooltip>` that should be\ntreated as inline so the spaces around it survive), override the\npredicate:\n\n```ts\nimport {\n  contextValue,\n  defaultIsInline,\n  defaultPreservesWhitespace,\n  ImportWhitespaceConfig,\n} from '@lexical/html';\nimport {isHTMLElement} from 'lexical';\n\nconfigExtension(DOMImportExtension, {\n  contextDefaults: [\n    contextValue(ImportWhitespaceConfig, {\n      isInline: (node) =>\n        defaultIsInline(node) ||\n        (isHTMLElement(node) && node.nodeName === 'TOOLTIP'),\n      preservesWhitespace: defaultPreservesWhitespace,\n    }),\n  ],\n})\n```\n\nThis is the declarative replacement for setting `display: inline` on\nthe element from inside an extended TextNode importer.\n\n## Sessions\n\n`ImportSession` is the **root-layer context record** of the walk —\na mutable, document-order-shared store on `ctx`. Use it to make\ninformation from a node visited earlier in the document available\nto nodes visited later — e.g. a `<style>` / `<meta>` tag at the\ntop of the document influencing how later elements are interpreted.\n\nThe session and the scoped context share the same\n`ImportStateConfig` keys you create with `createImportState`:\n\n- `ctx.get(cfg)` reads through the current branch's prototype chain\n  (a `$importChildren({context})` branch layered above the session\n  record, falling through to the session record itself, falling\n  through to the editor's `contextDefaults`, then to the config's\n  default).\n- `ctx.session.get(cfg)` reads the same chain starting at the\n  session record (skips any active branch overlay).\n- `ctx.session.set(cfg, v)` / `update` mutate the session record's\n  own properties. Every unshadowed `ctx.get(cfg)` later in the walk\n  (including in not-yet-entered branches) picks up the new value.\n- A branch that writes `cfg` shadows the session value for the\n  duration of the branch only; on return, scoped reads see the\n  session value again.\n\n```ts\nimport {createImportState} from '@lexical/html';\n\nconst Stylesheets = createImportState<string[]>(\n  'app/stylesheets',\n  () => [],\n);\n\nconst CollectStyleSheetsRule = defineImportRule({\n  match: sel.tag('style'),\n  $import: (ctx, el) => {\n    ctx.session.update(Stylesheets, (prev) => [\n      ...prev,\n      el.textContent ?? '',\n    ]);\n    return [];\n  },\n});\n\nconst ConsumesStyleSheetsRule = defineImportRule({\n  match: sel.tag('article'),\n  $import: (ctx, el) => {\n    const sheets = ctx.session.get(Stylesheets);\n    // ... use the collected stylesheet text however the app needs\n  },\n});\n```\n\nA fresh session record is created for every `$generateNodesFromDOM`\ncall. It chains to the ambient import context when the call runs\nnested inside another import operation — a rule re-entering the walk\nfor sub-content, or raw HTML inside a `@lexical/mdast` Markdown import\n— so states layered by the outer operation stay readable; the\noutermost call chains to the editor's `contextDefaults`. Per-call\n`options.context` pairs are seeded into it before any preprocessors\nrun, and preprocess-time `ctx.session.set` writes mutate the same\nrecord (never the parent, so session writes don't leak outward).\n\n## Preprocessors\n\nBefore walking begins, `DOMImportExtension` runs a stack of\npreprocessors against the input DOM. They're middleware-shaped:\n\n```ts\ntype DOMPreprocessFn = (\n  dom: Document | ParentNode,\n  ctx: DOMPreprocessContext,\n  $next: () => void,\n) => void;\n```\n\nEach step can:\n\n- Mutate the DOM in place (e.g. inline stylesheet rules onto\n  matching elements, strip unsafe nodes, normalize attributes,\n  resolve relative URLs).\n- Write to `ctx.session` — the session IS the root layer of the\n  walk's context, so writes are visible to both `ctx.session.get`\n  and to unshadowed `ctx.get(cfg)` reads in any branch.\n- Call `$next()` to defer to the next-lower preprocessor (top of stack\n  runs first); skip the call to short-circuit.\n\nThe default config registers\n[`$inlineStylesFromStyleSheets`](#inlinestylesfromstylesheets) — the\nsame Excel-flavored CSS preprocess the legacy\n`$generateNodesFromDOM` uses. Apps append more preprocessors via\n`DOMImportConfig.preprocess` or per-call via\n`GenerateNodesFromDOMOptions.preprocess`.\n\n```ts\nconst $stripScripts: DOMPreprocessFn = (dom, _ctx, $next) => {\n  const root = 'body' in dom ? dom.body : (dom as ParentNode);\n  for (const el of Array.from(root.querySelectorAll('script'))) {\n    el.remove();\n  }\n  $next();\n};\n\nconfigExtension(DOMImportExtension, {\n  preprocess: [$stripScripts],\n})\n```\n\n### Preprocess order\n\n`preprocess` composes in the opposite array direction from\n[`rules`](#dispatch-order) but produces the same precedence. `mergeConfig`\n**appends** each contribution to the accumulated stack, and the runner\nstarts at the **last** entry and works backwards. So:\n\n- Within one contribution, the **last** entry runs first and wraps the\n  ones written before it.\n- Because configs are merged in dependency order, an extension's\n  preprocessors run before — and can wrap, via `$next()` — those of its\n  dependencies. `$inlineStylesFromStyleSheets`, sitting in the extension's\n  own base config, runs last of all, and only if every preprocessor above\n  it calls `$next()`.\n- Per-call preprocessors from `GenerateNodesFromDOMOptions.preprocess` are\n  appended on top of the configured stack, so they run **before** the\n  configured ones.\n\nSkipping `$next()` is therefore a real decision: it drops every\nlower-priority preprocessor, including the built-in stylesheet inlining.\n\n### Reading meta tags into context\n\nA common pattern: a preprocess step inspects a `<meta>` tag (or any\nDOM marker), then layers a typed context value the rest of the walk\ncan branch on.\n\n```ts\nimport {createImportState} from '@lexical/html';\n\nconst DocumentLanguage = createImportState<string>(\n  'app/documentLanguage',\n  () => 'unknown',\n);\n\nconst $readMetaLang: DOMPreprocessFn = (dom, ctx, $next) => {\n  const root = 'body' in dom ? dom.body : (dom as ParentNode);\n  const meta = root.querySelector('meta[name=\"content-language\"]');\n  if (meta && meta.getAttribute('content')) {\n    ctx.session.set(DocumentLanguage, meta.getAttribute('content')!);\n  }\n  $next();\n};\n```\n\n### `$inlineStylesFromStyleSheets`\n\nThe default preprocessor resolves CSS rules from `<style>` tags onto\nmatching elements as inline styles, so the rest of the pipeline can\nread those styles via plain `el.style.foo`. Apps like Excel paste\nHTML in this shape; the preprocess collapses the indirection so rule\nselectors and bodies don't need to chase stylesheets themselves.\nRe-exported as a `DOMPreprocessFn` for apps that want to compose\ntheir own preprocess pipeline:\n\n```ts\nimport {$inlineStylesFromStyleSheets} from '@lexical/html';\n\nconfigExtension(DOMImportExtension, {\n  preprocess: [\n    $appPreprocess,\n    // The stack runs from the END of the array, so listing the inliner\n    // last makes it run FIRST — useful when `$appPreprocess` expects the\n    // styles to already be inlined. (The copy in the extension's base\n    // config runs at the very bottom of the stack, after everything\n    // else, so re-listing it here is how you pull it forward.)\n    $inlineStylesFromStyleSheets,\n  ],\n});\n```\n\nSee [Preprocess order](#preprocess-order) for the full composition rules.\n\n## `$importChildren` rules overlay\n\n`ctx.$importChildren(parent, {rules: defineOverlayRules([...])})`\ninstalls an **overlay** dispatcher active only for the duration of\nthat children traversal (and nested `$importChildren` calls that\ndon't push their own overlay). Overlay rules are checked BEFORE the\nmain dispatcher; calling `$next()` from an overlay rule falls through\nto the next overlay-or-main rule.\n\nThis lets you scope cost-bearing rules to the subtrees where they\napply, rather than paying the predicate cost on every paste. The\nGitHub raw-file-view code-table rule in `@lexical/code-core` uses\nthe overlay to unwrap `<tr>` / `<td>` only while processing a code\ntable:\n\n```ts\n// Compile once at module scope. `defineOverlayRules` builds the same\n// tag-bucketed dispatch table the main config uses, so each\n// $importChildren call reuses it instead of recompiling.\nconst GitHubCodeTableOverlayRules = defineOverlayRules([\n  defineImportRule({\n    match: sel.tag('tr', 'td'),\n    $import: (ctx, el) => ctx.$importChildren(el),\n    name: '@lexical/code/github-code-table/unwrap',\n  }),\n]);\n\nconst GitHubCodeTableRule = defineImportRule({\n  match: sel.tag('table').classAll('js-file-line-container'),\n  $import: (ctx, el) => {\n    const node = $createCodeNode();\n    node.splice(\n      0,\n      0,\n      ctx.$importChildren(el, {rules: GitHubCodeTableOverlayRules}),\n    );\n    return [node];\n  },\n});\n```\n\nOutside the code table, the `tr` / `td` rule from `@lexical/table` is\nthe only one consulted; the overlay isn't installed unless we enter\na code-table subtree.\n\n`ImportChildrenOpts.rules` only accepts a `CompiledOverlayRules`\nproduced by `defineOverlayRules` — a raw rule array won't typecheck.\nThis is by design: a one-off overlay built inline on every walk would\nrecompile the dispatcher each time, defeating the point of the overlay\nbeing a fast-path.\n\n### Composing overlays\n\n`defineOverlayRules` accepts both raw `DOMImportRule`s and other\n`CompiledOverlayRules` in the same list (the union is\n`DOMImportRuleEntry`). Existing overlays are inlined at their position\nin priority order — earlier entries dispatch first — so any number of\noverlays compose with a single call, recompiled once.\n\n```ts\nconst allOverlays = defineOverlayRules([\n  GitHubCodeTableOverlayRules,\n  ExcelTableOverlayRules,\n  // ...plus inline rules if you want them at this priority position:\n  defineImportRule({match: sel.tag('foo'), $import: …}),\n]);\n\nctx.$importChildren(el, {rules: allOverlays});\n```\n\nThe same `DOMImportRuleEntry` union is accepted by\n`DOMImportConfig.rules` (the extension-level rule list), so a library\nthat ships a `CompiledOverlayRules` can be dropped straight into\n`configExtension(DOMImportExtension, {rules: […]})` without unpacking.\n\nThis is the definition-time complement to runtime composition (an\noverlay rule's `$import` calling\n`ctx.$importChildren(el, {rules: another})` to push a nested overlay\non top of the current stack): compose when you want a fixed merged\noverlay across an entire subtree, nest when the inner overlay should\nonly apply for a deeper region.\n\n### Walk-wide overlays installed by a preprocessor\n\nA preprocessor can install an overlay for the entire walk by writing to\nthe built-in `ImportOverlays` session slot. The runtime reads it once\nbefore walking and seeds the overlay stack from it, so rules don't have\nto bracket each subtree with `$importChildren({rules: …})`.\n\nThis is how you handle paste-source-conditional rule sets: detect the\nsource in preprocess, install only the relevant overlay, and pay\nnothing for other sources.\n\n```ts\nimport {\n  defineOverlayRules,\n  type DOMPreprocessFn,\n  ImportOverlays,\n} from '@lexical/html';\n\nconst WordPasteOverlay = defineOverlayRules([\n  WordOPRule,\n  WordListParagraphRule,\n  // …\n]);\n\nconst $installWordOverlay: DOMPreprocessFn = (dom, ctx, $next) => {\n  const meta = dom.querySelector('meta[name=\"Generator\"]');\n  if (meta && /Microsoft Word/i.test(meta.getAttribute('content') || '')) {\n    ctx.session.update(ImportOverlays, (prev) => [...prev, WordPasteOverlay]);\n  }\n  $next();\n};\n```\n\nThat is the pattern, not something you have to write for Word lists\nspecifically: `@lexical/list` ships this exact preprocess as\n`WordListImportExtension`. It is opt-in — `ListExtension` does not depend\non it, so an editor that never pastes from Word does not bundle it:\n\n```ts\nimport {WordListImportExtension} from '@lexical/list';\nimport {defineExtension} from 'lexical';\n\ndefineExtension({\n  dependencies: [WordListImportExtension],\n  name: 'my-editor',\n});\n```\n\nSee `packages/lexical-list/src/__tests__/unit/ListImportExtension.test.ts`\n(\"MS Word paste — preprocess-installed overlay\") for a worked unit\ntest, and [`dev-examples/dom-import`](https://github.com/facebook/lexical/tree/main/dev-examples/dom-import)\nfor a running editor that exercises the same overlay end-to-end (a\nreduced rich-text editor with lists, tables, code blocks, markdown\nshortcuts, and an \"Import HTML\" dialog so you can paste raw HTML\nwithout needing a real clipboard `text/html` slot). The dev-example\nalso ships a VS Code → Safari paste fixture and the matching\n`@lexical/code-core` preprocess that turns the flat sibling run of\nmonospace+pre divs into a single `CodeNode`.\n\n## ClipboardImportExtension\n\nThe clipboard's paste handler (`$insertDataTransferForRichText`)\ndelegates to `ClipboardImportExtension`. Its output owns the full\npaste-side iteration: it walks a per-MIME-type priority list, reads\nthe corresponding `DataTransfer` slot, and invokes that MIME type's\nhandler stack.\n\n`ClipboardImportConfig` mirrors `GetClipboardDataExtension`:\n\n```ts\nexport interface ClipboardImportConfig {\n  $importMimeType: ImportMimeTypeConfig;\n  priority: ImportMimeTypePriority;\n}\n```\n\n- `$importMimeType` — per-MIME-type stack of middleware functions\n  `(data, selection, $next, dataTransfer) => boolean`. Top of stack runs\n  first; `$next()` defers to the next-lower; return `true` to claim the\n  data. The `dataTransfer` is the original `DataTransfer` the paste/drop\n  came from, so a handler can peek at companion MIME types or attached\n  files (see [`ImportSourceDataTransfer`](#importsourcedatatransfer) for\n  how to surface it to rules).\n- `priority` — a per-MIME-type weight map (`Record<string, number>`,\n  lower runs first). Composable: each extension contributes weights\n  for its own MIME types without coordinating with others.\n\n`$next(data?, selection?)` can replace the data, selection, or both for the\nremaining handlers in the same MIME-type stack. Omitted arguments retain\nthe values received by the current handler, so `$next()`\nis equivalent to `$next(data, selection)`. For example, `$next(cleanedData)`\nchanges only the data, and `$next(data, otherSelection)` changes only\nthe selection. An empty string is forwarded as-is; it does not automatically\nfall back to another MIME type. These replacements do not modify the original\n`DataTransfer` or the arguments used when trying another MIME type.\n\n```ts\nconfigExtension(ClipboardImportExtension, {\n  $importMimeType: {\n    'application/vnd.myapp+json': [\n      (data, selection) => {\n        const nodes = parseMyAppFormat(data);\n        $insertGeneratedNodes($getEditor(), nodes, selection);\n        return true;\n      },\n    ],\n  },\n  // Slot between 'application/x-lexical-editor' (0) and 'text/html' (10).\n  priority: {'application/vnd.myapp+json': 5},\n})\n```\n\n### Routing pastes through `DOMImportExtension`\n\nThe easy on-switch is `ClipboardDOMImportExtension` from\n`@lexical/clipboard` — add it to your dependencies and `text/html`\npastes and drops route through the new pipeline (forwarding\n`ImportSource='paste'` and `ImportSourceDataTransfer` automatically).\nThe node extensions already registered their rules, so nothing else\nneeds to be configured:\n\n```ts\nimport {defineExtension} from 'lexical';\nimport {ClipboardDOMImportExtension} from '@lexical/clipboard';\nimport {RichTextExtension} from '@lexical/rich-text';\n\ndefineExtension({\n  name: 'app',\n  dependencies: [\n    RichTextExtension,\n    // …other node extensions (list, link, table, code, …)…\n    ClipboardDOMImportExtension,\n  ],\n});\n```\n\nApps that don't configure `ClipboardImportExtension` keep the legacy\nbehavior — `$insertDataTransferForRichText` falls back to the same\ndefaults the legacy code path uses, including the legacy\n`$generateNodesFromDOM` for HTML.\n\n### Transforming pasted HTML\n\n:::tip\n\nUse HTML transforms when integrating libraries that accept and return HTML\nstrings. For DOM-based normalization, prefer a [DOM import preprocessor](#preprocessors).\nIt works directly on the parsed DOM, avoiding an extra serialization and parse.\nSee [`WordListImportExtension`](#walk-wide-overlays-installed-by-a-preprocessor)\nand [`$inlineStylesFromStyleSheets`](#inlinestylesfromstylesheets) for examples.\n\n:::\n\nUse extension dependencies to enforce handler ordering. A transform's\nconfiguration is applied after its dependencies, so its handler runs first.\n\n**To use `ClipboardDOMImportExtension`**, declare it as a dependency of your\ntransform extension:\n\n```ts\nimport {\n  ClipboardDOMImportExtension,\n  ClipboardImportExtension,\n} from '@lexical/clipboard';\nimport {configExtension, defineExtension} from 'lexical';\nimport {normalizePastedHTML} from './normalizePastedHTML';\n\nconst TransformPastedHTMLExtension = defineExtension({\n  name: 'app/TransformPastedHTML',\n  dependencies: [\n    ClipboardDOMImportExtension,\n    configExtension(ClipboardImportExtension, {\n      $importMimeType: {\n        'text/html': [\n          (html, _selection, $next) => $next(normalizePastedHTML(html)),\n        ],\n      },\n    }),\n  ],\n});\n```\n\nAdd `TransformPastedHTMLExtension` to your root dependencies. This includes\n`ClipboardDOMImportExtension` and guarantees that the transform runs before\nits HTML importer, passing the transformed HTML to it through `$next`.\n\n**To support either importer**, declare `ClipboardDOMImportExtension` as an\noptional peer dependency:\n\n```ts\nimport {\n  type ClipboardDOMImportExtension,\n  ClipboardImportExtension,\n} from '@lexical/clipboard';\nimport {configExtension, declarePeerDependency, defineExtension} from 'lexical';\nimport {normalizePastedHTML} from './normalizePastedHTML';\n\nconst TransformPastedHTMLForEitherImporterExtension = defineExtension({\n  name: 'app/TransformPastedHTMLForEitherImporter',\n  dependencies: [\n    configExtension(ClipboardImportExtension, {\n      $importMimeType: {\n        'text/html': [\n          (html, _selection, $next) => $next(normalizePastedHTML(html)),\n        ],\n      },\n    }),\n  ],\n  peerDependencies: [\n    declarePeerDependency<typeof ClipboardDOMImportExtension>(\n      '@lexical/clipboard/DOMImport',\n    ),\n  ],\n});\n```\n\nAdd `TransformPastedHTMLForEitherImporterExtension` to your root dependencies.\nWhen the editor includes `ClipboardDOMImportExtension`, the peer dependency\nmakes this extension's clipboard configuration apply after the DOM importer's\nconfiguration, so the transform runs first. Otherwise, the transform runs\nbefore the legacy default HTML importer.\n\n### `ImportSourceDataTransfer`\n\nA builtin `ImportStateConfig<DataTransfer | null>` slot for surfacing\nthe original paste/drop `DataTransfer` to import rules and\npreprocessors. `ClipboardDOMImportExtension` forwards it via\n`context`; rules can then read it during the walk:\n\n```ts\nimport {ImportSourceDataTransfer} from '@lexical/html';\n\nconst ExcelAwareTableRule = defineImportRule({\n  match: sel.tag('table'),\n  $import: (ctx, el, $next) => {\n    const transfer = ctx.get(ImportSourceDataTransfer);\n    // Excel ships an HTML alternative alongside a real RTF payload; if\n    // we see both, we may want to defer to the RTF handler instead of\n    // parsing Excel's HTML quirks.\n    if (transfer && transfer.types.includes('application/rtf')) {\n      return $next();\n    }\n    // …handle the table…\n  },\n});\n```\n\nDefaults to `null` outside of clipboard imports (e.g. headless\n`$generateNodesFromDOM` calls). Use sparingly — the safer pattern is\nto decide *which* MIME-type payload to walk in the clipboard handler\nstack and hand a finalized DOM to the rules; only fall back to peeking\nat `ImportSourceDataTransfer` when the source-detection signal\ngenuinely lives in a companion slot.\n\n## Migrating from `importDOM`\n\nThe legacy `static importDOM(): DOMConversionMap` declaration on each\nnode class still works; the new pipeline is opt-in. When you're\nready to move a custom node, the translation is mechanical:\n\n| Legacy concept | New equivalent |\n| --- | --- |\n| `static importDOM(): DOMConversionMap` returning `{tag: () => ({conversion, priority})}` | One or more `defineImportRule({match, $import})` entries |\n| Numeric `priority` (0–4) | Position in the compiled `rules` list (earlier entry runs first; an extension's rules come ahead of its dependencies') plus `$next()` for deferring — see [Dispatch order](#dispatch-order) |\n| `forChild(node, parent)` | `ctx.$importChildren(el, {context: [...], $onChild})` |\n| `after(children)` | `ctx.$importChildren(el, {$after})` |\n| `wrapContinuousInlines` (block ancestor case) | `ctx.$importChildren(el, {schema: BlockSchema})` |\n| `ArtificialNode__DO_NOT_USE` (nested block) | `ctx.$importChildren(el, {schema: NestedBlockSchema})` |\n| Cross-tag setup via shared state | `createImportState` + `ctx.get` (scoped) or `ctx.session.get/set` (flat) |\n| Mutating the DOM before walking | `DOMPreprocessFn` in `DOMImportConfig.preprocess` |\n| `html: {import: ...}` field on a node-providing extension | `configExtension(DOMImportExtension, {rules: […]})` |\n\nA migrated rule typically replaces a forChild chain with explicit\ncontext propagation (clearer + survives across rule boundaries), and\nan `after` callback with either `$after` (when the post-processing\nruns on this rule's children) or a schema's `$packageRun` / `$finalize`\n(when the same logic belongs to anything appending to this kind of\nparent).\n\n### Importing children is explicit\n\nThe most consequential difference: the legacy `DOMConversion` pipeline\nwalked an element's children for you (via `wrapContinuousInlines` /\nthe post-conversion runner). In `DOMImportExtension`, a rule that\nwants its element's children must call `ctx.$importChildren(el)`\nitself.\n\nThis is deliberate. A rule decides:\n\n- Whether to recurse at all (skip children to drop them, or look at\n  `el` only)\n- Which schema the children must satisfy\n  (`schema: BlockSchema | InlineSchema | NestedBlockSchema | …`)\n- Whether to install a subtree-scoped overlay\n  (`rules: defineOverlayRules([...])`)\n- Where the produced children attach — usually\n  `parent.splice(0, 0, ctx.$importChildren(el))`, but a rule that\n  wants flat output can just return the array.\n\n### Concrete example: blockquote\n\nA typical legacy `importDOM` for a quote-like node:\n\n```ts\n// Legacy\nclass QuoteNode extends ElementNode {\n  static importDOM(): DOMConversionMap | null {\n    return {\n      blockquote: () => ({\n        conversion: (_el) => ({node: $createQuoteNode()}),\n        priority: 0,\n      }),\n    };\n  }\n}\n```\n\nThe migrated rule:\n\n```ts\nimport {\n  BlockSchema,\n  defineImportRule,\n  InlineSchema,\n  sel,\n} from '@lexical/html';\nimport {$createQuoteNode} from '@lexical/rich-text';\n\nconst QuoteRule = defineImportRule({\n  $import: (ctx, el) => {\n    const node = $createQuoteNode();\n    // The recursion is explicit. QuoteNode contains inline children\n    // only, so we constrain the schema; rejected blocks are dropped\n    // (InlineSchema declares no onReject, and 'drop' is the default).\n    node.splice(0, 0, ctx.$importChildren(el, {schema: InlineSchema}));\n    return [node];\n  },\n  match: sel.tag('blockquote'),\n  name: '@app/quote',\n});\n\nconfigExtension(DOMImportExtension, {rules: [QuoteRule]});\n```\n\nThe legacy pipeline would have inferred the recursion + wrapping\nbehavior from `QuoteNode`'s position in the tree and its acceptance\nrules. In the new pipeline both are stated at the call site: the rule\nasks for inline children, gets them packaged by `InlineSchema`, and\nattaches them via the normal `ElementNode.splice` primitive.\n\n### Concrete example: playground\n\nThe lexical-playground application was migrated in\n[#8590](https://github.com/facebook/lexical/pull/8590), which should\nbe a good example of what it would take to move an entire application\nfrom `importDOM` to `DOMImportExtension` (while adding test coverage).\n\n## Capabilities\n\nCurrent:\n\n- Typed selectors with element-narrowing through to `$import`'s\n  signature; no instanceof or cast boilerplate.\n- Middleware `$next()` chain replaces numeric priority.\n- Per-rule capture map (`ctx.captures`) avoids re-running regex\n  inside the rule body.\n- `ChildSchema` enforces structural invariants (block/inline/list/\n  table/code) without per-importer wrapping logic.\n- Per-call context (`ImportSource`), persistent context state\n  (`ImportTextFormat`, `ImportWhitespaceConfig`, app-defined).\n- Mutable session store for document-order-shared info.\n- DOM preprocess middleware (default: stylesheet inlining).\n- Subtree-scoped `rules` overlay for cost-bearing predicates.\n- Clipboard pipeline owns the whole `DataTransfer` iteration via\n  `ClipboardImportExtension`; per-MIME-type priority weights compose\n  across extensions.\n\nFuture:\n\n- The default pipeline (`$generateNodesFromDOM` and the static\n  `importDOM` methods on lexical-core node classes) still works\n  side-by-side. There is no plan in this iteration to flip the\n  default — both will coexist while ecosystem migrates.\n","packages/lexical-website/docs/serialization/dom-render.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-website%2Fdocs%2Fserialization%2Fdom-render.md -->","packages/lexical-website/docs/serialization/legacy.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-website%2Fdocs%2Fserialization%2Flegacy.md -->","packages/lexical-website/docs/serialization/markdown-mdast.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-website%2Fdocs%2Fserialization%2Fmarkdown-mdast.md -->","packages/lexical-website/docs/serialization/serialization.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-website%2Fdocs%2Fserialization%2Fserialization.md -->","packages/lexical-website/docs/testing.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-website%2Fdocs%2Ftesting.md -->","packages/lexical-website/plugins/example-source/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-website%2Fplugins%2Fexample-source%2FREADME.md -->","packages/lexical-website/src/data/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-website%2Fsrc%2Fdata%2FREADME.md -->","packages/lexical-yjs/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-yjs%2FREADME.md -->","packages/lexical/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical%2FREADME.md -->","packages/lexical/src/__bench__/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical%2Fsrc%2F__bench__%2FREADME.md -->"},"files":{"AGENTS.md":"# Lexical Agent Guide\n\nThis file provides detailed guidance for AI agents and automated tools working with the Lexical codebase.\n\n## Build, Test, and Development Commands\n\n### Building\n- `pnpm run build` - Build all packages in development mode\n- `pnpm run build-prod` - Clean and build all packages in production mode\n- `pnpm run build-release` - Build production release with error codes\n- `pnpm run build-types` - Build TypeScript type definitions and validate them\n\n### Testing\n- `pnpm run test-unit` - Run all unit tests (Vitest, Node/jsdom)\n- `pnpm run test-unit-watch` - Run unit tests in watch mode\n- `pnpm run test-browser` - Run browser-mode unit tests (Vitest + Playwright, real browser)\n- `pnpm run test-browser-watch` - Run browser-mode tests in watch mode\n- `pnpm run test-e2e-chromium` - Run E2E tests in Chromium (requires dev server running)\n- `pnpm run test-e2e-firefox` - Run E2E tests in Firefox\n- `pnpm run test-e2e-webkit` - Run E2E tests in WebKit\n- `pnpm run debug-test-e2e-chromium` - Run E2E tests in debug mode (headed)\n- `pnpm run debug-test-unit` - Debug unit tests with inspector\n\nFor E2E testing workflow:\n1. Start the dev server: `pnpm run start` (or `pnpm run dev` if you don't need collab)\n2. In another terminal: `pnpm run test-e2e-chromium`\n\n### Development Servers\n- `pnpm run start` - Start playground dev server + collab server (http://localhost:3000)\n- `pnpm run dev` - Start only the playground dev server (no collab)\n- `pnpm run start:website` - Start Docusaurus website (http://localhost:3001)\n- `pnpm run collab` - Start collab server on localhost:1234\n\n### Code Quality\n- `pnpm run lint` - Run ESLint on all files\n- `pnpm run lint:fix` - Auto-fix lint issues\n- `pnpm run prettier` - Check code formatting\n- `pnpm run prettier:fix` - Auto-fix formatting issues\n- `pnpm run flow` - Run a full foreground Flow check (does not reuse a server)\n- `pnpm run tsc` - Run TypeScript compiler\n- `pnpm run ci-check` - Run all checks (TypeScript, Flow, Prettier, ESLint)\n\n**Never commit changes to `scripts/error-codes/codes.json`.**\nThat edit is not yours to make — revert it to the state of\n`main` before staging, and never `git add` the file.\n\n### Searching and refactoring\n\nPrefer **ast-grep** over line-oriented regex (`grep`/`sed`) for anything\nstructural — matching or rewriting imports, call sites, JSX, type\nannotations, etc. Regexes miss multi-line forms (a symbol on its own line\ninside a multi-line `import { ... }` block) and produce false positives\n(`IS_APPLE` matching inside `IS_APPLE_WEBKIT`); ast-grep matches the syntax\ntree, so it does neither.\n\nIt isn't a dependency, so run it via npx (the CLI binary is `ast-grep`, not\nthe shadow-utils `sg` that may be on `PATH`):\n\n```sh\n# Find every import of something from '@lexical/utils' (ts and tsx are\n# separate grammars, so pass -l for each; add --json=compact to post-process)\nnpx --package @ast-grep/cli ast-grep run \\\n  -p \"import { \\$\\$\\$NAMES } from '@lexical/utils'\" -l ts packages\n```\n\nMetavariables (`$NAME`, `$$$LIST`) capture nodes for reporting or rewriting\nwith `--rewrite`. Reach for it whenever a change spans many files and must be\nprecise — e.g. moving symbols that `@lexical/utils` merely re-exports back to\na direct `lexical` import.\n\n### Extension source imports\n\nImport from the `@lexical/extension` barrel across package boundaries. Within\n`packages/lexical-extension`, use relative imports and re-exports. Do not\nmanually rewrite these source imports to `@lexical/extension/*`: ESLint\nenforces the source convention, and `@lexical/compiler`'s `subpathImports`\npass rewrites them in all published package builds, including www. Public\nsibling entries stay external so shared modules such as signals retain one\ninstance. Helpers with only one owning entry live under `src/internal` and\nare bundled into that entry, without exposing additional public subpaths.\nDownstream users may import either the barrel or public subpaths.\n\n### Tree-shaking annotations\n\nModule-scope calls to the side-effect-free factories (`defineExtension`,\n`configExtension`, `safeCast`, `createCommand`, `createState`,\n`defineImportRule`, etc.) need a `/* @__PURE__ */` annotation so bundlers\ncan drop unused definitions from application bundles. **Do not write those\nannotations by hand** — they are injected at build time by\n`@lexical/compiler` (`packages/lexical-compiler`), which\nruns as a Rollup plugin in `scripts/build.mjs` for the published bundles\nand as a Vite plugin in `scripts/vite/lexicalMonorepoPlugin.ts` for\neverything the monorepo builds from source. The published package is also\nwhat consumers add to their own build when they compile Lexical from its\n`source` export condition rather than from `dist`.\n\nA hand-written annotation on one of these calls is a lint error\n(`@lexical/internal/no-pure-annotation`), and the rule is autofixable, so a\nbranch written before the transform existed migrates with\n`pnpm run lint:fix`. Annotations on anything else — a third-party factory, or\na call inside a function body, where the build never injects one — are left\nalone.\n\nWhen adding a new factory of this kind, annotate its definition with\n`@__NO_SIDE_EFFECTS__`. That alone is enough for calls in the same package to\nbe annotated; add its name to `PURE_FACTORY_FUNCTIONS` in\n`packages/lexical-compiler/src/LexicalCompiler.mjs` so that\ncalls in code that imports it by package name are too. An object whose\nmethods build values and touch nothing else (like `@lexical/html`'s `sel`) is\nmarked `@lexical-pure-namespace` instead, so that `sel.tag('p')` is annotated\nthe way a factory call is.\n\nThe build runs with `strict`, which fails on a call inside one of these\ndefinitions that nothing has established is side-effect free — such a call\npins the definition into every bundle that imports the module, however well\nannotated the definition itself is. If it stops you, either make the call\nlazy (a `nodes: () => [...]` callback is not evaluated at module scope), or\ndeclare the function side-effect free so its calls are annotated too. A\nthird-party factory that cannot be declared is annotated by hand at the call\nsite, which is what the remaining hand-written annotations in the tree are.\n\nFactory calls are not the only module-scope statements that pin a module.\nesbuild and webpack keep, as a side effect, any property read at module scope\n(`navigator.userAgent`, `Date.now`, `SomeExtension.name`), any call or `new`\nthey were not told is pure (`new RegExp(...)`, `Object.freeze(...)`,\n`[...].join(',')`), an array or object spread, an `in` test, a `try`\nstatement, a mutation (`table.push(...)`, `Klass.static = ...`), a class with\na computed member (`[SOME_SYMBOL]?: number` — write `declare` on it), and an\nIIFE — together with everything the statement references. One\n`LexicalEditor.version = ...` after the class kept nearly all of `lexical` in\na bundle that imported only `createCommand` (#9120). Move such work into a\nfunction declared `@__NO_SIDE_EFFECTS__` and call it at module scope, so the\nbuild annotates the call; annotate a call to a builtin or to a third-party\nfunction (`/* @__PURE__ */ forwardRef(...)`) by hand. Two tests enforce this\nby bundling a bare `import 'pkg'` of every entry with esbuild and requiring\nthat nothing is retained: `scripts/__tests__/unit/treeShakingSource.test.ts`\non the sources (runs with `pnpm run test-unit`) and\n`scripts/__tests__/integration/tree-shaking.test.mjs` on the published\nbuilds. A failure prints the retained code; its first statement is usually\nthe culprit.\n\nA factory whose body is a trivial expression over its own arguments (like\n`safeCast`, `defineExtension`, or `configExtension`) is additionally marked\n`@lexical-inline <form>` and listed in that file's `INLINE_FACTORIES`: the\nbuild replaces calls to it with the expression it would have returned, which\nneeds no annotation at all. **The body of a marked function is reproduced by\nthe build**, so keep it trivial — `packages/lexical-compiler`'s unit\ntests compare what the transform emits against what the real function returns\nand will fail if the two drift apart.\n\n## High-Level Architecture\n\n### Core Concepts\n\nLexical is built around several key architectural concepts that work together:\n\n**Editor Instance** - Created via `createEditor()`, wires everything together. Manages the EditorState, registers listeners/commands/transforms, and handles DOM reconciliation.\n\n**EditorState** - Immutable data model representing the editor content. Contains:\n- A node tree (hierarchical structure of LexicalNodes)\n- A selection object (current cursor/selection state)\n- Fully serializable to/from JSON\n\n**`$` Functions Convention** - Functions prefixed with `$` (e.g., `$getRoot()`, `$getSelection()`) can ONLY be called within:\n- `editor.update(() => {...})` - for mutations\n- `editor.read(() => {...})` - for read-only access\n- Node transforms and command handlers (which have implicit update context)\n\nThis is similar to React hooks' restrictions but enforces synchronous context instead of call order.\n\n**Double-Buffering Updates** - When `editor.update()` is called:\n1. Current EditorState is cloned as work-in-progress\n2. Mutations modify the work-in-progress state\n3. Multiple synchronous updates are batched\n4. DOM reconciler diffs and applies changes\n5. New immutable EditorState becomes current\n\n**Node Immutability & Keys** - All nodes are recursively frozen after reconciliation. Node methods automatically call `node.getWritable()` to create mutable clones. All versions of a logical node share the same runtime-only key, allowing node methods to always reference the latest version from the active EditorState.\n\n### Monorepo Structure\n\nThis is a monorepo with packages in `packages/`:\n\n**Core Packages:**\n- `lexical` - Core framework (Editor, EditorState, base nodes, selection, updates)\n- `@lexical/react` - React bindings (LexicalComposer, plugins as components)\n- `@lexical/headless` - Headless editor for server-side/testing\n\n**Feature Packages** (extend core with nodes/commands/utilities):\n- `@lexical/rich-text` - Rich text editing (headings, quotes, etc.)\n- `@lexical/plain-text` - Plain text editing\n- `@lexical/extension` - Extend editor functionality\n- `@lexical/list` - List nodes (ordered/unordered/checklist)\n- `@lexical/table` - Table support\n- `@lexical/code` - Code block with syntax highlighting\n- `@lexical/link` - Link nodes and utilities\n- `@lexical/markdown` - Markdown import/export\n- `@lexical/html` - HTML serialization\n- `@lexical/history` - Undo/redo\n- `@lexical/yjs` - Real-time collaboration via Yjs\n- And many more...\n\n**Development Packages:**\n- `lexical-playground` - Full-featured demo application\n- `lexical-website` - Docusaurus documentation site\n\n### Key Architectural Patterns\n\n**Extensions** - Extensions should be used to add features and configuration\nto an editor. The set of extensions in an editor must be determined when the\neditor is created with `buildEditorFromExtensions`. Extensions with\nfunctionality that can be toggled on or off typically have a `disabled`\nconfiguration property and output signal that defaults to `false`. See the\nlexical-extension package and the supporting code in lexical for\nmore examples and implementation details.\n\n```tsx\nexport interface MyConfig {\n  disabled: boolean;\n}\nexport const MyExtension = defineExtension({\n  build: (_editor, config, _state) => namedSignals(config),\n  config: safeCast<MyConfig>({ disabled: false }),\n  name: '@lexical/docs/My',\n  nodes: () => [MyNode],\n  register: (editor, _config, state) => {\n    const {disabled} = state.getOutput();\n    return effect(() => {\n      if (!disabled.value) {\n        return editor.registerUpdateListener(({editorState}) => {\n          // React to updates\n        });\n      }\n    })\n  },\n})\n```\n\n**Plugin System (React)** - Plugins are a legacy pattern for React components\nto hook into the editor lifecycle, extensions should be preferred for new code:\n```jsx\nfunction MyPlugin() {\n  const [editor] = useLexicalComposerContext();\n  useEffect(() => {\n    return editor.registerUpdateListener(({editorState}) => {\n      // React to updates\n    });\n  }, [editor]);\n  return null;\n}\n```\n\n**Command Pattern** - Commands are the primary communication mechanism:\n- Create with `createCommand()`\n- Dispatch with `editor.dispatchCommand(command, payload)`\n- Handle with `editor.registerCommand(command, handler, priority)`\n- Handlers propagate by priority until one stops propagation\n\n**Node Transforms** - Registered via `editor.registerNodeTransform(NodeClass, transform)`. Called automatically during updates when nodes of that type change. Have implicit update context.\n\n**Listeners** - All `editor.register*()` methods return cleanup functions for easy unsubscription.\n\n## Type System\n\nThis codebase uses **both TypeScript and Flow**:\n- Source files are primarily TypeScript (`.ts`, `.tsx`)\n- Flow type definitions are generated in `packages/*/flow/` directories\n- Run `pnpm run flow` to check Flow types\n- Run `pnpm run tsc` to check TypeScript types\n- Both are checked in CI via `pnpm run ci-check`\n\nWhen adding/modifying APIs, types must be maintained for both systems.\n\n## Backwards Compatibility\n\n**All changes MUST be backwards compatible.** Lexical is a widely-adopted OSS library, and breaking changes ripple out to every downstream consumer.\n\n- Do NOT remove or rename existing public APIs, exported functions, types, or `$` functions. Add new APIs alongside the old ones instead.\n- Do NOT change the signature, return type, or behavior of existing public APIs in ways that could break callers. Prefer additive, optional parameters.\n- Preserve the serialization format of `EditorState` and node JSON. Serialized content produced by older versions must continue to deserialize correctly.\n- If an API genuinely must change, deprecate the old one first (keep it working, document the replacement) rather than removing it outright.\n- When in doubt, assume external code depends on the current behavior and keep it intact.\n\n## Dependencies: Lexical is a singleton within one app\n\nAn application must resolve exactly one copy of `lexical`, and one copy of each\n`@lexical/*` package it uses. The active editor and editor state are\nmodule-scope variables in `LexicalUpdates.ts`, node registration and\n`instanceof LexicalNode` compare class references, and commands are object\nidentities from `createCommand()` — none of which survive a second copy of the\nmodule. Two copies also mean two versions, which is API drift inside one editor.\n\nThe boundary is the app, not the page: several self-contained apps, each with\nits own bundled Lexical, can coexist on one page (`isLexicalEditor` is an\n`instanceof` check precisely to support that). They just cannot interoperate —\nnodes, editors, `EditorState`s, selections and commands must never be passed\nacross such a boundary, only serialized data.\n\n- Inside this monorepo, packages depend on siblings with `\"lexical\":\n  \"workspace:*\"`; `pnpm publish` rewrites that to the exact published version,\n  so every `@lexical/*` release pins the matching `lexical`. Keep that pattern\n  when adding a package — do not hand-write a version range.\n- A **library** published on top of Lexical (anything outside this repo that\n  imports `lexical` or `@lexical/*`) declares those packages in\n  `peerDependencies`, plus `devDependencies` for its own build and tests, never\n  in `dependencies`. Apps bring their own Lexical and own the version.\n- Code under `examples/` and `packages/lexical-playground` are applications, so\n  they depend on Lexical directly — that is correct and should stay.\n\nThe user-facing version of this rule lives in\n`packages/lexical-website/docs/concepts/one-lexical-per-app.md`; keep the two in\nsync when either changes.\n\n## Important Development Notes\n\n### Reconciliation and Updates\n- `editor.read(...)`  or `editor.read('force-commit', ...)` flushes pending updates first, then provides consistent reconciled state\n- `editor.read('pending', ...)` reads the pending state (like `editor.update(...)`, but read-only)\n- `editor.read('latest', ...)` reads the latest consistent reconciled state\n- Inside `editor.update()`, you see pending state (transforms/reconciliation not yet run)\n- `editor.getEditorState().read()` always uses latest reconciled state, but prefer `editor.read('latest', ...)` in new code\n- Updates can be nested: `editor.update(() => editor.update(...))` is allowed but strongly discouraged\n- Do NOT nest updates in reads, or use a force-commit in an update\n\n### Node References\nAlways access node properties/methods within read/update context. Nodes automatically resolve to their latest version via their key. Don't store node references across update boundaries.\n\n### Testing Strategy\n- **Unit tests** - Vitest, located in `packages/**/__tests__/unit/**/*.test.{ts,tsx}`.\n  Add `// @vitest-environment node` after the copyright header for tests that\n  only exercise editor state, headless behavior, or pure utilities. DOM tests\n  use jsdom with `vmThreads` on Node 24.9+, which reuses the jsdom implementation\n  while keeping each file's window and module graph isolated. Older Node\n  versions use isolated forks because their VM contexts cannot load jsdom's\n  `require(esm)` dependencies. Platform-mocked tests depend on module isolation.\n  Compiler tests run with `scripts-unit` in\n  ordinary Node workers because native build tools are not VM-realm-safe.\n  Use `DOMParser` to parse HTML in DOM tests and iframes for separate-window\n  tests. Reserve direct JSDOM construction for Node fixtures or CSSOM behavior\n  that jsdom's DOMParser lacks.\n- **Browser tests** - Vitest browser mode driven by the Playwright runner, located in\n  `packages/**/__tests__/browser/**/*.test.{ts,tsx}`. Use these for behavior that depends on\n  a real layout/selection engine instead of stubbing the missing jsdom functionality from\n  `vitest.setup.mts` (e.g. `Range.getBoundingClientRect`, the Selection API). Run with\n  `pnpm run test-browser`; the browser set is controlled by the `VITEST_BROWSER` env var\n  (comma-separated, default `chromium`). Prefer building editors with the extension APIs\n  (`buildEditorFromExtensions`, or `LexicalExtensionComposer`/`LexicalExtensionEditorComposer`\n  in React).\n  - **Do not use `using`/`Disposable` in browser tests (or any browser-facing code).**\n    Explicit Resource Management (`using`, `Symbol.dispose`, `Disposable`) is not supported\n    in WebKit/Safari yet, so the syntax throws a `SyntaxError` there. `using` is fine in unit\n    tests (jsdom/Node), but browser tests should clean up with\n    `onTestFinished(() => editor.dispose())` instead — `editor.dispose()` is a plain method\n    available on the result of `buildEditorFromExtensions`.\n- **E2E tests** - Playwright, located in `packages/lexical-playground/__tests__/e2e/**/*.spec.{ts,mjs}`\n- E2E tests require the playground dev server running\n- Use `pnpm run debug-test-e2e-chromium` to debug E2E tests with browser UI\n\n### Custom Nodes\nWhen creating custom nodes:\n1. Extend a base node class (TextNode, ElementNode, DecoratorNode). The\n   constructor must work with no arguments.\n2. Implement `$config()` with `this.config('type', {extends: BaseNode, json:\n   schema})`, where `schema` is built with `nodeSchema<YourNode>()({...})`\n   as a module-scope `const` above the class. Written inline in `$config()`,\n   it makes TypeScript skip checking the class without a diagnostic.\n   Declare every property inherent to the node in it, and declare each one\n   stored verbatim in a field with\n   `withField(stringValue(), {field: '__name'})`. Initialize that field in\n   the class (`__name: string = '';`) so it is an own property of a fresh\n   node. Use `withAccessors` only where a property is not a plain field.\n3. Do not hand-write `clone`, `importJSON`, `exportJSON` or\n   `updateFromJSON`: `$config` derives them from the schema, and it carries\n   every property declared with `withField` across a clone. A field missing\n   from the schema is not carried, so it is lost the first time a later\n   update clones the node; declare it with `withField` rather than writing\n   `afterCloneFrom`. Ad-hoc data that is not inherent to the node type\n   belongs in NodeState (`createState`), which clones on its own. Write\n   `afterCloneFrom` only for storage neither covers, such as a property\n   declared through accessors on both sides whose value is not one field\n   (one that is can name it: `setter: {field: '__ids', method: 'setIDs'}`\n   in `MarkNode`). Call `super.afterCloneFrom(prevNode)` first, and copy\n   every field of the class, schema fields included: Lexical does not add\n   to a class's own `afterCloneFrom`.\n4. Implement `createDOM()` and `updateDOM()` (and `decorate()` for a\n   DecoratorNode). Keep `exportDOM()` on the class when the default export\n   is not enough.\n5. For HTML import, prefer `defineImportRule` rules registered by the\n   extension that provides the node: list `CoreImportExtension` and\n   `configExtension(DOMImportExtension, {rules: [...]})` in its\n   `dependencies` (see `ListExtension`, and `ListImportRules` for the rules,\n   in `@lexical/list`). Both APIs are `@experimental`, and the rules only\n   apply where the editor routes HTML through the pipeline; see\n   `packages/lexical-website/docs/serialization/dom-import.md`.\n   `importDOM()`, which that page calls legacy, is not deprecated yet, but\n   its deprecation is planned.\n6. Register with an extension (`nodes: () => [YourNode]`) or editor config.\n7. Export a `$createYourNode()` factory built on `$create(YourNode)` and the\n   node's setters, and a `$isYourNode()` guard.\n\nThe full schema reference is\n`packages/lexical-website/docs/serialization/serialization.md`; the cloning\nrules are in `packages/lexical-website/docs/concepts/node-cloning.md`.\n\n### Shadow DOM and iframe realm safety\n\nLexical supports editors whose root element lives inside a Shadow DOM or an\n`<iframe>` document. The editor resolves its `window` and `document` from\n`rootElement.ownerDocument.defaultView`, so code that reaches for the **global**\n`window` or `document` will silently use the wrong realm when the editor crosses\na frame boundary. Shadow DOM adds a second hazard: the browser **retargets**\nselection and focus reads to the shadow host, hiding the real nodes.\n\nUse the shadow/iframe-aware helpers exported from `lexical` instead of the raw\nbrowser globals:\n\n| Instead of | Use | Why |\n| --- | --- | --- |\n| `window` | `element.ownerDocument.defaultView` or `getDefaultView(element)` (@internal) | Returns the window that owns the element |\n| `window.getSelection()` | `getDOMSelection(rootElement.ownerDocument.defaultView)` | Reads selection from the correct window |\n| `selection.getRangeAt(0)` | `getDOMSelectionRange(selection, rootElement)` | Unwraps retargeted shadow-DOM selection |\n| `document` in `createDOM`/`updateDOM`/`exportDOM` | `$getDocument()` | Returns the document that owns the editor root (falls back to `globalThis.document`) |\n| `document` elsewhere | `getRootOwnerDocument(rootElement)` or `element.ownerDocument` | Returns the document that owns a specific element |\n| `document.activeElement` | `getActiveElement(element)` / `getActiveElementDeep(document)` | Walks through shadow roots to find the real focused element |\n| `event.target` | `getComposedEventTarget(event)` | Returns the un-retargeted target for composed events |\n| `element.parentElement` | `getParentElement(element)` | Crosses shadow boundaries correctly |\n| `selection.anchorNode` / `focusNode` | `getDOMSelectionPoints(selection, rootElement)` | Returns un-retargeted boundary points |\n\nTwo ESLint rules enforce the globals rows at lint time:\n- `no-restricted-syntax` (error) catches all `document.*` and `window.*` member access in library sources.\n- `@lexical/no-document-in-dom-methods` (error, with autofix) catches `document.*` specifically inside `createDOM`/`updateDOM`/`exportDOM` methods and autofixes to `$getDocument().*`.\n\nThe remaining rows involve local variable properties that lint cannot reliably detect, so they must be caught in code review.\n\nFor full details on the browser platform APIs involved, see\n[Shadow DOM and iframes](packages/lexical-website/docs/concepts/shadow-dom.md).\n\n### Commits and Pull Requests\nEvery commit message — not just PR bodies — must be written to the shape of\n`.github/pull_request_template.md`, so any commit can seed a PR directly\nwithout being rewritten. This applies to every commit you author, including\none-line fixes; do not wait to be asked. Read the template rather than working\nfrom memory, and fill in its sections:\n\n- **Subject line**: `[Affected Packages] PR Type: title`, where the packages are\n  the directory names under `packages/` that the diff touches and the type is\n  one of Breaking change / Refactor / Feature / Bug Fix / Documentation Update /\n  Chore. Test-only and tooling changes are `Chore`.\n- **`## Description`**: what the current behavior is and what this change makes\n  it do. Add `Closes #<issue>` only when there is a real issue number; drop the\n  line otherwise rather than leaving the template's placeholder behind.\n- **`## Test plan`** with `### Before` and `### After` subsections: the command\n  you ran, plus the actual failing output before and passing output after.\n  Paste real output — do not describe it. If a platform or browser in the\n  matrix could not be exercised, say so explicitly under `### After`.\n\nDrop any template section that does not apply to the diff instead of carrying\nan empty heading, and treat the template's HTML comments as instructions to\nfollow, not text to copy into the message.\n\n### Commit and PR Hygiene for Agents\nThis is an open source project: never include agent-session URLs or other\nprivate/team-internal links (e.g. `https://claude.ai/code/session_...`) in\ncommit messages, PR titles, or PR bodies. Those URLs are private to the\nperson or team that ran the session and are meaningless or misleading to\neveryone else. Co-authorship attribution (e.g. `Co-Authored-By:`) is fine.\nFor Claude Code this is enforced mechanically via `attribution.sessionUrl:\nfalse` in the checked-in `.claude/settings.json`; agents from other vendors\nshould follow this rule as written.\n\n### Build System\n- Uses Rollup for bundling\n- Build script: `scripts/build.mjs`\n- Supports multiple build modes: development, production, www (Meta internal)\n- TypeScript source → compiled to ESM for npm (`dist/*.js` in the\n  `\"type\": \"module\"` packages; CommonJS consumers load it through\n  `require(esm)`, so the fork modules must stay free of top-level await) and\n  to CommonJS for www only (`--www`)\n- Package manager logic in `scripts/shared/packagesManager.mjs`\n","CLAUDE.md":"@AGENTS.md\n","packages/lexical-a11y/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-a11y%2FREADME.md -->","packages/lexical-clipboard/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-clipboard%2FREADME.md -->","packages/lexical-code-core/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-code-core%2FREADME.md -->","packages/lexical-code-prism/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-code-prism%2FREADME.md -->","packages/lexical-code-shiki/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-code-shiki%2FREADME.md -->","packages/lexical-code/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-code%2FREADME.md -->","packages/lexical-compiler/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-compiler%2FREADME.md -->","packages/lexical-devtools-core/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-devtools-core%2FREADME.md -->","packages/lexical-devtools/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-devtools%2FREADME.md -->","packages/lexical-dragon/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-dragon%2FREADME.md -->","packages/lexical-eslint-plugin/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-eslint-plugin%2FREADME.md -->","packages/lexical-eslint-plugin/__tests__/integration/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-eslint-plugin%2F__tests__%2Fintegration%2FREADME.md -->","packages/lexical-extension/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-extension%2FREADME.md -->","packages/lexical-fast-check/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-fast-check%2FREADME.md -->","packages/lexical-file/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-file%2FREADME.md -->","packages/lexical-hashtag/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-hashtag%2FREADME.md -->","packages/lexical-headless/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-headless%2FREADME.md -->","packages/lexical-history/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-history%2FREADME.md -->","packages/lexical-html/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-html%2FREADME.md -->","packages/lexical-internal/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-internal%2FREADME.md -->","packages/lexical-link/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-link%2FREADME.md -->","packages/lexical-list/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-list%2FREADME.md -->","packages/lexical-mark/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-mark%2FREADME.md -->","packages/lexical-markdown/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-markdown%2FREADME.md -->","packages/lexical-mdast/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-mdast%2FREADME.md -->","packages/lexical-offset/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-offset%2FREADME.md -->","packages/lexical-overflow/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-overflow%2FREADME.md -->","packages/lexical-plain-text/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-plain-text%2FREADME.md -->","packages/lexical-react/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-react%2FREADME.md -->","packages/lexical-rich-text/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-rich-text%2FREADME.md -->","packages/lexical-selection/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-selection%2FREADME.md -->","packages/lexical-table/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-table%2FREADME.md -->","packages/lexical-tailwind/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-tailwind%2FREADME.md -->","packages/lexical-text/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-text%2FREADME.md -->","packages/lexical-utils/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-utils%2FREADME.md -->","packages/lexical-website/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-website%2FREADME.md -->","packages/lexical-website/docs/collaboration/_category_.json":"{\n  \"label\": \"Collaboration\",\n  \"position\": 6\n}\n","packages/lexical-website/docs/collaboration/faq.md":"---\nsidebar_position: 2\n---\n\n# Collaboration FAQ\n\n## Source of truth: Lexical State, Yjs and App's DB\n\nIt's recommended to treat the Yjs model as the source of truth. You can store the document to a database for indexing.\nBut, if possible, you should never forget the Yjs model, as this is the only way clients without internet access can reliably join and sync with the server.\n\nYou can also treat the database as the source of truth. This is how it could be achieved:\n\n- Clients receive a `sessionId` when they connect to the server\n- When a client connects without an existing `sessionId`, get the content from the database and create a `sessionId`\n- When all clients disconnect, forget the room content and `sessionId` on the server after some timeout (e.g. 1 hour)\n- When a client reconnects, use that content on the server. Furthermore, get the `sessionId` from the client\n- When two clients with different `sessionId` reconnect, one of the clients should forget the room content. _In this case the client will lose content_ - although it is very unlikely if you set the forget timeout (see point 2) very high.\n\nOr, there is an ever simpler approach:\n\n- When a client connects to the server, the server populates the room content if empty\n- When all clients disconnect, the server forgets the room content after some timeout (e.g. 1 hour)\n- When a client was not able to reconnect for 40 minutes, the client must forget its local updates and start fresh (this should be enforced by the server)\n\nWhen the database is the source of truth, and if you want to be able to forget the Yjs model, you will always run into cases where clients are not able to commit changes. That's not too bad in most projects. It somehow limits you, because you can't cache the document on the client using y-indexeddb. On the other hand, it is much easier to maintain, and do Yjs upgrades. Furthermore, most people would say that SQL is a bit more reliable than Yjs.\n\n_* Based on the advice of the Yjs author - [Kevin Jahns](https://github.com/yjs/yjs/issues/82#issuecomment-328365015)_\n\n\n## Initializing `EditorState` from Yjs Document\n\nIt's achievable by leveraging headless Lexical and no-op provider for Yjs:\n\n<details>\n  <summary>createHeadlessCollaborativeEditor.ts</summary>\n\n  ```typescript\n  import type {Binding, Provider} from '@lexical/yjs';\n  import type {\n    Klass,\n    LexicalEditor,\n    LexicalNode,\n    LexicalNodeReplacement,\n    SerializedEditorState,\n  } from 'lexical';\n\n  import {createHeadlessEditor} from '@lexical/headless';\n  import {\n    createBinding,\n    syncLexicalUpdateToYjs,\n    syncYjsChangesToLexical,\n  } from '@lexical/yjs';\n  import {type YEvent, applyUpdate, Doc, Transaction} from 'yjs';\n\n  export default function headlessConvertYDocStateToLexicalJSON(\n    nodes: ReadonlyArray<Klass<LexicalNode> | LexicalNodeReplacement>,\n    yDocState: Uint8Array,\n  ): SerializedEditorState {\n    return withHeadlessCollaborationEditor(nodes, (editor, binding) => {\n      applyUpdate(binding.doc, yDocState, {isUpdateRemote: true});\n      editor.update(() => {}, {discrete: true});\n\n      return editor.getEditorState().toJSON();\n    });\n  }\n\n  /**\n   * Creates headless collaboration editor with no-op provider (since it won't\n   * connect to message distribution infra) and binding. It also sets up\n   * bi-directional synchronization between yDoc and editor\n   */\n  export function withHeadlessCollaborationEditor<T>(\n    nodes: ReadonlyArray<Klass<LexicalNode> | LexicalNodeReplacement>,\n    callback: (editor: LexicalEditor, binding: Binding, provider: Provider) => T,\n  ): T {\n    const editor = createHeadlessEditor({\n      nodes,\n    });\n\n    const id = 'main';\n    const doc = new Doc();\n    const docMap = new Map([[id, doc]]);\n    const provider = createNoOpProvider();\n    const binding = createBinding(editor, provider, id, doc, docMap);\n\n    const unsubscribe = registerCollaborationListeners(editor, provider, binding);\n\n    const res = callback(editor, binding, provider);\n\n    unsubscribe();\n\n    return res;\n  }\n\n  function registerCollaborationListeners(\n    editor: LexicalEditor,\n    provider: Provider,\n    binding: Binding,\n  ): () => void {\n    const unsubscribeUpdateListener = editor.registerUpdateListener(\n      ({\n        dirtyElements,\n        dirtyLeaves,\n        editorState,\n        normalizedNodes,\n        prevEditorState,\n        tags,\n      }) => {\n        if (tags.has('skip-collab') === false) {\n          syncLexicalUpdateToYjs(\n            binding,\n            provider,\n            prevEditorState,\n            editorState,\n            dirtyElements,\n            dirtyLeaves,\n            normalizedNodes,\n            tags,\n          );\n        }\n      },\n    );\n\n    const observer = (events: Array<YEvent<any>>, transaction: Transaction) => {\n      if (transaction.origin !== binding) {\n        syncYjsChangesToLexical(binding, provider, events, false);\n      }\n    };\n\n    binding.root.getSharedType().observeDeep(observer);\n\n    return () => {\n      unsubscribeUpdateListener();\n      binding.root.getSharedType().unobserveDeep(observer);\n    };\n  }\n\n  function createNoOpProvider(): Provider {\n    const emptyFunction = () => {};\n\n    return {\n      awareness: {\n        getLocalState: () => null,\n        getStates: () => new Map(),\n        off: emptyFunction,\n        on: emptyFunction,\n        setLocalState: emptyFunction,\n        setLocalStateField: emptyFunction,\n      },\n      connect: emptyFunction,\n      disconnect: emptyFunction,\n      off: emptyFunction,\n      on: emptyFunction,\n    };\n  }\n  ```\n</details>\n","packages/lexical-website/docs/collaboration/react.md":"---\nsidebar_position: 1\n---\n\n# React\n\nLexical provides `LexicalCollaborationPlugin` and `useCollaborationContext` hook within `@lexical/react` to accelerate creation of the collaborative React backed editors.\nThis is on top of the Yjs bindings provided by `@lexical/yjs`.\n\n\n:::tip\n\nClone [Lexical GitHub repo](https://github.com/facebook/lexical), run `pnpm i && pnpm run start` and open [`http://localhost:3000/split/?isCollab=true`](http://localhost:3000/split/?isCollab=true) to launch playground in collaborative mode.\n\n:::\n\n## Getting started\n\nThis guide is based on [examples/react-rich](https://github.com/facebook/lexical/tree/main/examples/react-rich) example.\n\n**Install minimal set of the required dependencies:**\n```bash\n$ npm i -S @lexical/react @lexical/yjs lexical react react-dom y-websocket yjs\n```\n\n:::note\n\n`y-websocket` is the only officially supported Yjs connection provider at this point. Although other providers may work just fine.\n\n:::\n\n**Get WebSocket server running:**\n\nThis allows different browser windows and different browsers to find each other and sync Lexical state. On top of this `YPERSISTENCE` allows you to save Yjs documents in between server restarts so clients can simply reconnect and keep editing.\n\n```bash\n$ HOST=localhost PORT=1234 YPERSISTENCE=./yjs-wss-db npx y-websocket\n```\n\n**Get basic collaborative Lexical setup:**\n\n```tsx\nimport { useCallback } from 'react';\n\nimport {$getRoot, $createParagraphNode, $createTextNode} from 'lexical';\nimport {LexicalCollaboration} from '@lexical/react/LexicalCollaborationContext';\nimport {LexicalComposer} from '@lexical/react/LexicalComposer';\nimport {ContentEditable} from '@lexical/react/LexicalContentEditable';\nimport {LexicalErrorBoundary} from '@lexical/react/LexicalErrorBoundary';\nimport {RichTextPlugin} from '@lexical/react/LexicalRichTextPlugin';\nimport {CollaborationPlugin} from '@lexical/react/LexicalCollaborationPlugin';\nimport * as Y from 'yjs';\nimport {$initialEditorState} from './initialEditorState';\nimport {WebsocketProvider} from 'y-websocket';\n\nfunction Editor() {\n  const initialConfig = {\n    // NOTE: This is critical for collaboration plugin to set editor state to null. It\n    // would indicate that the editor should not try to set any default state\n    // (not even empty one), and let collaboration plugin do it instead\n    editorState: null,\n    namespace: 'Demo',\n    nodes: [],\n    onError: (error: Error) => {\n      throw error;\n    },\n    theme: {},\n  };\n\n  const getDocFromMap = (id: string, yjsDocMap: Map<string, Y.Doc>): Y.Doc => {\n    let doc = yjsDocMap.get(id);\n  \n    if (doc === undefined) {\n      doc = new Y.Doc();\n      yjsDocMap.set(id, doc);\n    } else {\n      doc.load();\n    }\n\n    return doc;\n  }\n\n  const providerFactory = useCallback(\n    (id: string, yjsDocMap: Map<string, Y.Doc>) => {\n      const doc = getDocFromMap(id, yjsDocMap);\n\n      return new WebsocketProvider('ws://localhost:1234', id, doc, {\n        connect: false,\n      });\n    }, [],\n  );\n\n  return (\n    <LexicalCollaboration>\n      <LexicalComposer initialConfig={initialConfig}>\n        <RichTextPlugin\n          contentEditable={<ContentEditable className=\"editor-input\" />}\n          placeholder={<div className=\"editor-placeholder\">Enter some rich text...</div>}\n          ErrorBoundary={LexicalErrorBoundary}\n        />\n        <CollaborationPlugin\n          id=\"lexical/react-rich-collab\"\n          providerFactory={providerFactory}\n          shouldBootstrap={true}\n        />\n      </LexicalComposer>\n    </LexicalCollaboration>\n  );\n}\n```\n\n**Initial editor content:**\n\nIn a production environment, you should bootstrap the editor's initial content on the server. If bootstrapping was left to the client and two clients connected at the same time, they could both try to initialize the content resulting in document corruption.\n\nUsing the `withHeadlessCollaborationEditor` function from the [FAQ](faq.md) page, you can create a bootstrapped `Y.Doc` with the following:\n\n```tsx\nimport type {CreateEditorArgs} from 'lexical';\n\nimport {$getRoot, $createParagraphNode} from 'lexical';\nimport {Doc} from 'yjs';\n\nimport {withHeadlessCollaborationEditor} from './createHeadlessCollaborativeEditor';\n\nfunction createBootstrappedYDoc(nodes: CreateEditorArgs['nodes']): Doc {\n  return withHeadlessCollaborationEditor(nodes, (editor, binding) => {\n    editor.update(() => {\n      $getRoot().append($createParagraphNode());\n    }, {discrete: true});\n    // binding.doc is the Doc the editor synced into\n    return binding.doc;\n  });\n}\n```\n\nIf you're simply following the above example to play around in a local dev environment, then you can add the following props to `CollaborationPlugin` to initialize the editor state client-side:\n\n```tsx\n// Dev-testing only, do not use in real-world cases.\ninitialEditorState={$initialEditorState}\nshouldBootstrap={true}\n```\n\n## See it in action\n\nSource code: [examples/react-rich-collab](https://github.com/facebook/lexical/tree/main/examples/react-rich-collab)\n\n<iframe width=\"100%\" height=\"600\" src=\"https://stackblitz.com/github/facebook/lexical/tree/main/examples/react-rich-collab?embed=1&file=src%2FApp.tsx&terminalHeight=0&ctl=1&showSidebar=0&devtoolsheight=0&view=preview\" sandbox=\"allow-forms allow-modals allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin allow-scripts\"></iframe>\n\n## Sharing one Yjs document between editors\n\nBy default a binding uses the `XmlText` shared type named `root` on the Yjs\n`Doc`, so each editor needs its own document. Two options let several editors\nlive in one document instead, which is what applications like collaborative\nsticky notes or comment threads usually want:\n\n- `rootName` picks a different top-level shared type, one per editor:\n\n```jsx\n<CollaborationPlugin\n  id={`note-${noteId}`}\n  providerFactory={providerFactory}\n  rootName={`note-${noteId}`}\n  shouldBootstrap={true}\n/>\n```\n\n- `getXmlText` resolves the root yourself, for roots that are not top-level\n  shared types (for example an `XmlText` stored in a `Y.Map`). It takes\n  precedence over `rootName`:\n\n```jsx\nconst getXmlText = useCallback(\n  (doc) => doc.getMap('notes').get(noteId),\n  [noteId],\n);\n\n<CollaborationPlugin\n  id={`note-${noteId}`}\n  providerFactory={providerFactory}\n  getXmlText={getXmlText}\n  shouldBootstrap={true}\n/>\n```\n\nGive each editor its own `id`, as above: the `id` keys the document map and the\nremote-cursor highlight registry, both of which are shared across the page, so\ntwo editors mounted under one `id` overwrite each other's entries.\n\nBoth options are also available on `createYjsBinding` for non-React usage.\n\n:::warning\n\nThe root is chosen when the binding is created, and a mounted editor cannot be\nrepointed at another root: changing `rootName` or `getXmlText` afterwards has no\neffect. To switch documents, remount the **editor** — put a `key` that changes\nwith the document on the component that owns the `LexicalComposer`. Remounting\nonly the plugin is not enough: the editor would keep the previous document's\ncontent and write it into the newly selected root.\n\n:::\n\n`getXmlText` runs while the binding is constructed, before the provider has\nsynced, so it has to resolve the root from whatever the local `Doc` holds at\nthat moment — reaching into a `Y.Array` that the server has not sent yet throws.\nEither mount the editor only once the note exists locally, or have the callback\ncreate the missing shared type. Write it to be idempotent: it can run more than\nonce for one editor (React StrictMode double-invokes render, and a remount\nbuilds a new binding), and a callback that unconditionally stores a new\n`XmlText` would throw away what the previous call stored.\n\nThe returned `XmlText` is checked to be an `XmlText` that is integrated into the\ndocument; that it is not already driving another live binding is yours to\nguarantee, since a second binding over one root takes over the collab nodes\ncached on its shared types.\n\nThe experimental `CollaborationPluginV2__EXPERIMENTAL` (and\n`createBindingV2__EXPERIMENTAL`) take the same two options, except that its\nroot is an `XmlElement`, so the callback is named `getXmlElement`. Create that\nelement as `new Y.XmlElement()` without a `nodeName`, which is how the root is\nrecognized.\n\n## Building collaborative plugins\n\n[Lexical Playground](https://playground.lexical.dev/) features set of the collaboration enabled plugins that integrate with primary document via `useCollaborationContext()` hook. Notable mentions:\n\n- [`CommentPlugin`](https://github.com/facebook/lexical/tree/main/packages/lexical-playground/src/plugins/CommentPlugin) - features use of the separate provider and Yjs room to sync comments.\n- [`ImageComponent`](https://github.com/facebook/lexical/blob/main/packages/lexical-playground/src/nodes/ImageComponent.tsx) - features use of the `LexicalExtensionEditorComposer` for its nested caption editor.\n- [`PollOptionComponent`](https://github.com/facebook/lexical/blob/main/packages/lexical-playground/src/nodes/PollComponent.tsx) - showcases poll implementation using `name` from the collaboration context.\n- [`StickyComponent`](https://github.com/facebook/lexical/blob/main/packages/lexical-playground/src/nodes/StickyComponent.tsx) - features use of the `LexicalExtensionEditorComposer` for its nested editor as well as sticky note position real-time sync.\n\n:::note\n\nWhile these \"playground\" plugins aren't production ready - they serve as a great example of collaborative Lexical capabilities as well as provide a good starting point.\n\n:::\n\n## Custom node property syncing\n\n`@lexical/yjs` syncs custom node properties by constructing a fresh node instance and inspecting its **own enumerable properties**. This means every property that should be synced across peers must be assigned in the constructor, even if the initial value is `undefined`.\n\n:::warning\n\nDeclaring a class property with TypeScript's optional syntax (`foo?: Type`) does **not** guarantee the property exists as an own enumerable property on the instance. Under TypeScript's default compilation settings (target < ES2022 without `useDefineForClassFields`), such declarations produce no initialization code.\n\n:::\n\n### What to do\n\nAlways initialize every node property in the constructor. Conditional initialization such as `if (someValue !== undefined) this.__someValue = someValue` is not enough, because the default construction path still needs to create the property.\n\nFor example:\n\n```ts\n// ✅ Correct — property is guaranteed to be an own property\nclass MyNode extends ElementNode {\n  __someValue: string | undefined;\n\n  constructor(someValue?: string, key?: NodeKey) {\n    super(key);\n    this.__someValue = someValue; // explicitly initialized\n  }\n}\n\n// ❌ Incorrect — property may not exist as an own property\nclass MyNode extends ElementNode {\n  __someValue?: string; // optional syntax without initialization\n\n  constructor(key?: NodeKey) {\n    super(key);\n    // __someValue is never assigned → won't be synced via yjs\n  }\n}\n```\n\nIf you use [`NodeState`](../concepts/node-state.md) for your custom properties, this concern does not apply. `NodeState` values are synced correctly without relying on constructor-created enumerable properties.\n\n## Yjs providers\n\nSetting up the communication between clients, managing awareness information, and storing shared data for offline usage is quite a hassle. Providers manage all that for you and are the perfect starting point for your collaborative app.\n\nSee [Yjs Website](https://docs.yjs.dev/ecosystem/connection-provider) for the list of the officially endorsed providers. Although it's not an exhaustive one.\n","packages/lexical-website/docs/concepts/_category_.json":"{\n  \"label\": \"Concepts\",\n  \"position\": 3\n}\n","packages/lexical-website/docs/concepts/commands.md":"\n\n# Commands\n\nCommands are a very powerful feature of Lexical that lets you register listeners for events like `KEY_ENTER_COMMAND` or `KEY_TAB_COMMAND` and contextually react to them _wherever_ & _however_ you'd like.\n\nThis pattern is useful for building [`Toolbars`](https://github.com/facebook/lexical/blob/main/packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx) or complex extensions and nodes such as [tables](https://github.com/facebook/lexical/tree/main/packages/lexical-table) which require special handling for `selection`, `keyboard events`, and more.\n\nWhen registering a `command` you supply a `priority` and can return `true` to mark it as \"handled\", which stops other listeners from receiving the event. If a command isn't handled explicitly by you, it's likely handled by default in [`RichTextExtension`](https://github.com/facebook/lexical/blob/main/packages/lexical-rich-text/src/index.ts) or [`PlainTextExtension`](https://github.com/facebook/lexical/blob/main/packages/lexical-plain-text/src/index.ts).\n\n## `createCommand(...)`\n\nYou can view the core commands in the [`lexical` API reference](/docs/api/modules/lexical#commands). Other packages define their own, listed under Commands in each package's API reference, such as [`TOGGLE_LINK_COMMAND`](/docs/api/modules/lexical_link#toggle_link_command) in `@lexical/link`. If you need a custom command for your own use case, check out the typed `createCommand(...)` function.\n\n```js\nconst HELLO_WORLD_COMMAND: LexicalCommand<string> = createCommand('HELLO_WORLD');\n\neditor.registerCommand(\n  HELLO_WORLD_COMMAND,\n  (payload: string) => {\n    console.log(payload); // Hello World!\n    return false;\n  },\n  COMMAND_PRIORITY_EDITOR,\n);\n\neditor.dispatchCommand(HELLO_WORLD_COMMAND, 'Hello World!');\n```\n\n## `editor.dispatchCommand(...)`\n\nCommands can be dispatched from anywhere you have access to the `editor` such as a Toolbar Button, an event listener, or an extension, but most of the core commands are dispatched from [`LexicalEvents.ts`](https://github.com/facebook/lexical/blob/main/packages/lexical/src/LexicalEvents.ts).\n\nCalling `dispatchCommand` will implicitly call `editor.update` to trigger its command listeners if it was not called from inside `editor.update`.\n\nDon't dispatch a command from inside a read-only context such as\n`editor.read(fn)` or `editorState.read(fn)`. Command listeners usually change\nthe editor, so Lexical runs them in a separate writable update and\ndevelopment builds log a warning when they detect this. Dispatch after the\nread returns instead.\n\n```js\neditor.dispatchCommand(command, payload);\n```\n\nThe `payload`s are typed via the `createCommand(...)` API, but they're usually a DOM `event` for commands dispatched from an event listener.\n\nHere are some real examples from [`LexicalEvents.ts`](https://github.com/facebook/lexical/blob/main/packages/lexical/src/LexicalEvents.ts).\n\n```js\neditor.dispatchCommand(KEY_ARROW_LEFT_COMMAND, event);\n// ...\neditor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic');\n```\n\nAnd another example from the [`ToolbarPlugin`](https://github.com/facebook/lexical/blob/main/packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx) in our Playground.\n\n```js\nconst formatBulletList = () => {\n  editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND);\n};\n```\n\nWhich is later handled by [`ListExtension`](https://github.com/facebook/lexical/blob/main/packages/lexical-list/src/LexicalListExtension.ts) to insert the list into the editor.\n\n```js\neditor.registerCommand(\n  INSERT_UNORDERED_LIST_COMMAND,\n  () => {\n    $insertList('bullet');\n    return true;\n  },\n  COMMAND_PRIORITY_LOW,\n);\n```\n\n## `editor.registerCommand(...)`\n\nYou can register a command from anywhere you have access to the `editor` object, but it's important that you remember to clean up the listener with its remove listener callback when it's no longer needed.\n\nThe command listener will always be called from an `editor.update`, so you may use dollar functions. You should not use\n`editor.update` (and *never* call `editor.read(...)` or `editor.read('force-commit', ...)`) synchronously from within a command listener. It is safe to call\n`editor.read('latest', ...)` or `editor.getEditorState().read` if you need to read the previous state after updates have already been made.\n\n```js\nconst removeListener = editor.registerCommand(\n  COMMAND,\n  (payload) => boolean, // Return true to stop propagation.\n  priority,\n);\n// ...\nremoveListener(); // Cleans up the listener.\n```\n\nThe easiest way to get clean-up right is to register commands from an\n[extension](/docs/extensions/defining-extensions)'s `register` function and\nreturn the remove callback. The editor calls it when it is disposed. To\nregister several things, combine their callbacks with `mergeRegister` from\n`@lexical/utils`.\n\n```js\nimport {LinkExtension, TOGGLE_LINK_COMMAND} from '@lexical/link';\nimport {COMMAND_PRIORITY_BEFORE_EDITOR, defineExtension} from 'lexical';\n\nexport const LinkLoggerExtension = defineExtension({\n  // The command it observes is handled by LinkExtension\n  dependencies: [LinkExtension],\n  name: '@my-app/LinkLogger',\n  register: (editor) =>\n    editor.registerCommand(\n      TOGGLE_LINK_COMMAND,\n      (payload) => {\n        console.log('link', payload);\n        return false; // let the link extension handle it too\n      },\n      // LinkExtension handles this command at COMMAND_PRIORITY_EDITOR and\n      // returns true, so observe it at the BEFORE variant of that priority.\n      COMMAND_PRIORITY_BEFORE_EDITOR,\n    ),\n});\n```\n\nA listener that only observes a command, like this one, needs to run before\nthe listener that handles it. Depending on the extension that handles it and\nusing the `COMMAND_PRIORITY_BEFORE_*` variant of its priority does that\nwithout escalating to a higher priority. See\n[Priorities and ordering](#priorities-and-ordering) below.\n\nIn a legacy React plugin, return the `registerCommand` call from a\n`useEffect` instead.\n\nAnd as seen above and below, `registerCommand`'s callback can return `true` to signal to the other listeners that the command has been handled and propagation will be stopped.\n\nHere's a simplified example of handling a `KEY_TAB_COMMAND` from [`TabIndentationExtension`](https://github.com/facebook/lexical/blob/main/packages/lexical-extension/src/TabIndentationExtension.ts), which is used to dispatch a `OUTDENT_CONTENT_COMMAND` or `INDENT_CONTENT_COMMAND`.\n\n```js\neditor.registerCommand(\n  KEY_TAB_COMMAND,\n  (payload) => {\n    const event: KeyboardEvent = payload;\n    event.preventDefault();\n    return editor.dispatchCommand(\n      event.shiftKey ? OUTDENT_CONTENT_COMMAND : INDENT_CONTENT_COMMAND,\n    );\n  },\n  COMMAND_PRIORITY_EDITOR,\n);\n```\n\nNote that the same `KEY_TAB_COMMAND` command is registered by [`LexicalTableSelectionHelpers.ts`](https://github.com/facebook/lexical/blob/main/packages/lexical-table/src/LexicalTableSelectionHelpers.ts), which handles moving focus to the next or previous cell within a `TableNode`, but the priority is high (`COMMAND_PRIORITY_HIGH`) because this behavior is very important.\n\n### Priorities and ordering\n\nCommand listeners are called in the following order until a listener returns `true`:\n\n- From priority highest to lowest (critical, high, normal, low, editor)\n- All `COMMAND_PRIORITY_BEFORE_${priority}` listeners, most recently registered first\n- All `COMMAND_PRIORITY_${priority}` listeners, in registration order\n\n:::note\n\nThe `COMMAND_PRIORITY_BEFORE_*` priorities make it much easier to override\ndefault behavior without escalating the priority.\n\n:::\n\nIt is best practice to use the lowest priority possible, so most commands will\nbe registered with `COMMAND_PRIORITY_EDITOR` for the default behavior, then\ncommands to override that behavior can be registered with\n`COMMAND_PRIORITY_BEFORE_EDITOR`. The higher priorities mostly serve purposes\nsuch as being able to observe-but-not-handle events and to support legacy code\nthat predates the availability of `COMMAND_PRIORITY_BEFORE_*`.\n\nA modern lexical app will typically only need the\n`COMMAND_PRIORITY_BEFORE_EDITOR` priority since the\nlast-registered-called-first ordering is suitable for almost all use cases. The\nolder priorities without BEFORE can be considered legacy and are primarily\noffered for compatibility.\n\nHere is the full ordering of priorities, from lowest to highest:\n\n- `COMMAND_PRIORITY_EDITOR`\n- `COMMAND_PRIORITY_BEFORE_EDITOR`\n- `COMMAND_PRIORITY_LOW`\n- `COMMAND_PRIORITY_BEFORE_LOW`\n- `COMMAND_PRIORITY_NORMAL`\n- `COMMAND_PRIORITY_BEFORE_NORMAL`\n- `COMMAND_PRIORITY_HIGH`\n- `COMMAND_PRIORITY_BEFORE_HIGH`\n- `COMMAND_PRIORITY_CRITICAL`\n- `COMMAND_PRIORITY_BEFORE_CRITICAL`\n","packages/lexical-website/docs/concepts/compared-with-prosemirror.md":"# Compared with ProseMirror\n\nLexical and [ProseMirror](https://prosemirror.net) are both frameworks for\nbuilding rich text editors on top of `contentEditable`, and both keep an\nimmutable document model as the source of truth. This page collects the main\ndifferences in one place, for readers who know one and are learning the other.\n\n## Document model {#document-model}\n\nLexical's tree is shaped like the HTML it renders (see\n[Document Model](./document-model.md)). This is a deliberate difference from\nProseMirror. ProseMirror's document is also a tree,\nand blocks can contain other blocks, but the inline content of a textblock is\na flat sequence of nodes (text and inline nodes) with marks for formatting and\nlinks, and every position in the document is a single integer.\n\n| | Lexical | ProseMirror |\n| -- | -- | -- |\n| Document | Immutable tree of nodes, one `RootNode` | Immutable tree of nodes, one `doc` node |\n| Node identity and navigation | Each node has a runtime key and knows its parent and siblings | Nodes are plain values with no parent links; a resolved position supplies ancestor context |\n| Blocks | `ElementNode` subclasses (paragraph, heading, list, table, …), or a block `DecoratorNode` (image, embed, …) | Block nodes defined in the schema |\n| Inline formatting | Format flags on each `TextNode` (bold, italic, code, …) | Marks on text |\n| Links and other inline wrappers | Inline `ElementNode`s (`LinkNode`, `MarkNode`) that contain text nodes | Marks on text, like formatting |\n| Embedded content | `DecoratorNode`, inline or block, rendered by your framework (for example React) | Leaf or atom nodes, often with a custom `NodeView` |\n| Several editable regions in one node | [Named slots](./named-slots.md): regions addressed by name, like a card's `title`, each isolated so editing and selection never cross the boundary | Child nodes in the order the schema's content expression allows, optionally marked `isolating`, or a separate editor inside a `NodeView` |\n| Addressing a position | Node key plus offset (`{key, offset, type}`) | One integer counted across the whole document |\n| Allowed structure | Declared by node classes, enforced with node transforms and normalization | Declared by a schema of content expressions |\n| Applying edits | Call node and selection methods inside `editor.update()` | Build a transaction from steps that address positions or ranges |\n| Rendering | Each node's `createDOM()`/`updateDOM()`, applied by the reconciler, and optionally changed per node class by a `DOMRenderExtension` override | `toDOM` in the schema, or a `NodeView` |\n\n## Serialization {#serialization}\n\nBecause a Lexical link is an element that already contains its text, it\nexports as an `<a>` around those text nodes. A mark-based model has to find the\nadjacent text that shares a mark and group it into one element first.\n\n| Format | Lexical | ProseMirror |\n| -- | -- | -- |\n| JSON | `editorState.toJSON()`, restored with `editor.parseEditorState()` | `doc.toJSON()` and `Node.fromJSON(schema, json)` |\n| HTML export | `$generateHtmlFromNodes()`, using each node's `createDOM()` or `exportDOM()` | `DOMSerializer.fromSchema(schema)`, using `toDOM` from the schema's nodes and marks |\n| HTML import | `$generateNodesFromDOM()`, using `importDOM()` or `DOMImportExtension` rules | `DOMParser.fromSchema(schema)`, using the schema's `parseDOM` rules |\n| Markdown | `@lexical/markdown` transformers or `@lexical/mdast` | `prosemirror-markdown`: a markdown-it parser and a serializer with a function per node and mark |\n| Clipboard | Copy writes plain text, HTML, and Lexical JSON. Paste prefers the JSON, then HTML, then plain text. | Copy writes HTML and plain text. Paste parses the HTML with the schema's parse rules. |\n\nIn the JSON, a Lexical link is a `link` node with its text nodes as\n`children`, and bold is a `format` value on a `text` node, where\nProseMirror's JSON gives each text node a list of `marks`. In both editors the\ndefault HTML export reuses the definition that renders the editor\n(`createDOM()` in Lexical, `toDOM` in ProseMirror), so the two stay in step\nunless you override one. See [Serialization](../serialization/serialization.md)\nfor the details of each Lexical format.\n\n## History {#history}\n\nBoth editors group consecutive edits into one undo step. Lexical's\n[`HistoryExtension`](./history.md) merges edits made within `delay`\n(300ms by default) and keeps an unlimited stack unless you set `maxDepth`.\nProseMirror's history plugin uses `newGroupDelay: 500` and `depth: 100` by\ndefault, so `configExtension(HistoryExtension, {delay: 500, maxDepth: 100})`\nmatches its behavior.\n","packages/lexical-website/docs/concepts/document-model.md":"# Document Model\n\nAn [editor state](./editor-state.md) holds a tree of nodes. This page explains\nhow that tree is shaped.\n\n## A DOM-like tree\n\nLexical's node tree is shaped like the HTML it renders. A paragraph is an\n`ElementNode` whose children are the nodes inside it, and inline structure\nnests the same way: a link is a `LinkNode` element that contains the text\nnodes it wraps, just as an `<a>` contains its text. Each node type decides how\nit renders with `createDOM()` and `updateDOM()`, so the model maps closely to\nthe DOM, and a position in the document is a node plus an offset within it.\n\nHere is a small document with a heading, a paragraph containing bold text and\na link, a block image, and a card that has a named `title` slot as well as\nordinary body children:\n\n```mermaid\nflowchart TB\n  accTitle: The shape of a Lexical document tree\n  accDescr: The root has four children in order: a heading, a paragraph, an image, and a card. The heading contains the text \"Welcome\". The paragraph contains the text \"Read the \", a bold text node \"docs\", and a link, which contains the text \"here\". The image is a block decorator node with no children. The card has a named slot called title, which holds a paragraph containing the text \"Card title\", and one ordinary child, a paragraph containing the text \"Card body\".\n  root[\"Root\"] --> heading[\"Heading (h1)\"]\n  root --> paragraph[\"Paragraph\"]\n  root --> image[\"Image<br/>(block DecoratorNode)\"]\n  root --> card[\"Card<br/>(ElementNode)\"]\n  heading --> welcome[\"Text: 'Welcome'\"]\n  paragraph --> read[\"Text: 'Read the '\"]\n  paragraph --> docs[\"Text: 'docs'<br/>{ format: bold }\"]\n  paragraph --> link[\"Link\"]\n  link --> here[\"Text: 'here'\"]\n  card -.->|\"slot: title\"| titlePara[\"Paragraph\"]\n  titlePara --> titleText[\"Text: 'Card title'\"]\n  card --> body[\"Paragraph\"]\n  body --> bodyText[\"Text: 'Card body'\"]\n```\n\nSolid arrows are ordinary children, which form an ordered list under their\nparent and are read with `getChildren()`. The dashed arrow is a\n[named slot](./named-slots.md): a separate channel on the host, keyed by name\ninstead of position and read with `$getSlot(card, 'title')`. A slot's value\nis the root of its own isolated region, so its `getParent()` is `null` and\n`$getSlotHost()` leads back to the card. Any `ElementNode` or `DecoratorNode`\ncan host slots, so a decorator such as the image could also expose an\neditable caption this way.\n\n## Working with the tree\n\nLexical code navigates the document the way DOM code does, with methods like\n`getParent()`, `getChildren()`, and `getNextSibling()`, and each node owns the\nDOM it renders. The resemblance is structural, not one\nnode per HTML element: bold or italic text, for example, is a format on a\n`TextNode`, not a separate `<strong>` or `<em>` node. See\n[Nodes](./nodes.mdx) for the built-in node types.\n\n## Customizing rendering\n\nYou can also change how nodes render without subclassing them.\n[`DOMRenderExtension`](../serialization/dom-render.md) from `@lexical/html`\ntakes overrides for `createDOM`, `updateDOM`, and `exportDOM`, for one node\nclass or for every node, such as adding a `data-` attribute or wrapping a\nnode's children in another element. Each override calls `$next()` to get the\ndefault result and adjusts it, so overrides from several extensions compose.\nExport overrides only affect HTML export, and `createDOM` overrides also carry\nthrough to export for nodes that use the default `exportDOM`. Changes made\nwhile updating the editor's DOM don't; see\n[Lexical -> HTML](../serialization/serialization.md#lexical---html) for which\nhooks reach export.\n\nFor how the tree is converted to and from JSON, HTML, and Markdown, see\n[Serialization](../serialization/serialization.md#formats-at-a-glance). For\nhow this model differs from a mark-based editor, see\n[Compared with ProseMirror](./compared-with-prosemirror.md).\n","packages/lexical-website/docs/concepts/dom-events.md":"\n\n# Working with DOM Events\n\nSometimes, when working with Lexical, it might be necessary or useful for you to attach a DOM Event Listener to the underlying DOM nodes that Lexical controls. For instance, you might want to show a popover when a user mouses over a specific node or open a modal when they click on a node. Either of these use cases (and many others) can be accomplished via native DOM Event Listeners. There are 3 main ways that you can listen for DOM Events on nodes controlled by Lexical:\n\n## 1. Event Delegation\n\nOne way to handle events inside the editor is to set a listener on the editor root element (the contentEditable Lexical attaches to). You can do this using a [Root Listener](listeners.md).\n\n```js\nfunction myListener(event) {\n    // You may want to filter on the event target here\n    // to only include clicks on certain types of DOM Nodes.\n    alert('Nice!');\n}\n\nconst ClickExtension = defineExtension({\n  name: '@my-app/Click',\n  register: (editor) =>\n    editor.registerRootListener((rootElement) => {\n      if (rootElement === null) {\n        return;\n      }\n      // add the listener to the current root element\n      rootElement.addEventListener('click', myListener);\n      // remove the listener when the root element changes - make sure the ref\n      // to myListener is stable so the removal works and you avoid a memory leak.\n      return () => rootElement.removeEventListener('click', myListener);\n    }),\n});\n```\n\nAn extension's `register` returns a cleanup function, and the editor calls it\nwhen it is disposed, so there is no teardown to write by hand. Add\n`ClickExtension` to your editor's `dependencies` to use it.\nThis can be a simple, efficient way to handle some use cases, since it's not necessary to attach a listener to each DOM node individually.\n\nThe `addEventListener`/`removeEventListener` pairing above is common enough that the core `lexical` package exports a `registerEventListener(target, type, listener, options?)` helper. It attaches the listener and returns a dispose function that removes it, so the example above becomes:\n\n```js\nimport {defineExtension, registerEventListener} from 'lexical';\n\nconst ClickExtension = defineExtension({\n  name: '@my-app/Click',\n  register: (editor) =>\n    editor.registerRootListener((rootElement) =>\n      // registerEventListener returns the matching removeEventListener cleanup,\n      // so there's no need to write the teardown by hand.\n      rootElement === null\n        ? undefined\n        : registerEventListener(rootElement, 'click', myListener),\n    ),\n});\n```\n\nIt mirrors the `addEventListener` overloads (so the event type is strongly typed) and composes well with `mergeRegister` when you need to register several listeners at once.\n\nTo attach several listeners to the same target, `registerEventListeners(target, listeners, options?)` takes a `{type: listener}` map and returns a single dispose function that removes all of them. Each listener's event argument is still strongly typed per event type, and the optional `options` argument is shared across every listener:\n\n```js\nimport {registerEventListeners} from 'lexical';\n\nconst removeListeners = registerEventListeners(\n  rootElement,\n  {\n    click: onClick,\n    keydown: onKeyDown, // receives a KeyboardEvent\n  },\n  {capture: true}, // shared by both listeners\n);\n```\n\nBecause `options` is shared, register a group that needs different options (such as a different `capture` flag) with a separate `registerEventListeners` call and combine the results with `mergeRegister`.\n\n## 2. Directly Attach Handlers\n\nIn some cases, it may be better to attach an event handler directly to the underlying DOM node of each specific node. With this approach, you generally don't need to filter the event target in the handler, which can make it a bit simpler. It will also guarantee that your handler isn't running for events that you don't care about. This approach is implemented via a [Mutation Listener](listeners.md).\n\n```js\nconst registeredElements: WeakSet<HTMLElement> = new WeakSet();\nconst removeMutationListener = editor.registerMutationListener(nodeType, (mutations) => {\n    editor.getEditorState().read(() => {\n        for (const [key, mutation] of mutations) {\n            const element: null | HTMLElement = editor.getElementByKey(key);\n            if (\n                // Updated might be a move, so that might mean a new DOM element\n                // is created. In this case, we need to add an event listener too.\n                (mutation === 'created' || mutation === 'updated') &&\n                element !== null &&\n                !registeredElements.has(element)\n            ) {\n                registeredElements.add(element);\n                element.addEventListener('click', (event: Event) => {\n                    alert('Nice!');\n                });\n            }\n        }\n    });\n});\n\n// teardown the listener - return it from an extension's `register` so the\n// editor removes it when it is disposed.\nremoveMutationListener();\n```\nNotice that here we don't worry about cleaning up, as Lexical will dereference the underlying DOM nodes and allow the JavaScript runtime garbage collector to clean up their listeners.\n\n## 3. Use NodeEventPlugin\n\nIf you're using React, we've wrapped approach #2 up into a simple React plugin that you can render inside your composer to achieve the same effect, without worrying about the details:\n\n```jsx\n<LexicalExtensionComposer extension={appExtension}>\n    <NodeEventPlugin\n        nodeType={LinkNode}\n        eventType={'click'}\n        eventListener={(e: Event) => {\n            alert('Nice!');\n        }}\n    />\n</LexicalExtensionComposer>\n```\n\nIf the editor lives inside a shadow root or an iframe, see\n[Shadow DOM and iframes](./shadow-dom.md) for the helper to read\n`event.target` through the shadow boundary — the browser retargets it to the\nshadow host on listeners attached above the boundary, so a plain\n`event.target` won't match a node inside the editor.\n","packages/lexical-website/docs/concepts/editor-state.md":"\n\n# Editor State\n\n## Why is it necessary?\n\nWith Lexical, the source of truth is not the DOM, but rather an underlying state model\nthat Lexical maintains and associates with an editor instance.\n\nWhile HTML is great for storing rich text content it's often \"way too flexible\" when it comes to text editing.\nFor example the following lines of content will produce equal outcome:\n\n```html\n<i><b>Lexical</b></i>\n<i><b>Lex<b><b>ical</b></i>\n<b><i>Lexical</i></b>\n```\n\n<details>\n  <summary>See rendered version!</summary>\n  <div>\n    <i><b>Lexical</b></i>\n    <i><b>Lex</b><b>ical</b></i>\n    <b><i>Lexical</i></b>\n  </div>\n</details>\n\nOf course, there are ways to normalize all these variants to a single canonical form, however this would require DOM manipulation and so re-rendering of the content. And to overcome this we can use Virtual DOM, or State.\n\nOn top of that it allows to decouple content structure from content formatting. Let's look at this example stored in HTML:\n\n```html\n<p>Why did the JavaScript developer go to the bar? <b>Because he couldn't handle his <i>Promise</i>s</b></p>\n```\n\n<figure class=\"text--center\">\n  <img src=\"/img/docs/state-formatting-html.drawio.svg\" alt=\"Nested structure of the HTML state\"/>\n  <figcaption>Nested structure of the HTML state because of the formatting</figcaption>\n</figure>\n\nIn contrast, Lexical decouples structure from formatting by offsetting this information to attributes. This allows us to have canonical document structure regardless of the order in which different styles were applied.\n\nHere is the same content as a Lexical node tree, printed the way the\n[tree view](/docs/getting-started/devtools) shows it (the numbers are node keys):\n\n```text\nroot\n  └ (2) paragraph\n    ├ (3) text \"Why did the JavaScript developer go to the bar? \"\n    ├ (4) text \"Because he couldn't handle his \" { format: bold }\n    ├ (5) text \"Promise\" { format: bold, italic }\n    └ (6) text \"s\" { format: bold }\n```\n\nThe paragraph's children are a flat list of text nodes, and the formatting is a\nproperty of each one, so there is only one way to represent this content.\n\n## Understanding the Editor State\n\nYou can get the latest editor state from an editor by calling `editor.getEditorState()`.\n\nEditor states have two phases:\n\n- During an update they can be thought of as \"mutable\". See \"Updating state\" below to\n  mutate an editor state.\n- After an update, the editor state is then locked and deemed immutable from there on. This\n  editor state can therefore be thought of as a \"snapshot\".\n\nEditor states contain two core things:\n\n- The editor node tree (starting from the root node). See\n  [Document Model](./document-model.md) for how the tree is shaped.\n- The editor [selection](./selection.md) (which can be null).\n\nFor example, \"Hello world\" with a link around \"world\" and the caret at the\nend looks like this:\n\n```mermaid\nflowchart TB\n  accTitle: A simple editor state\n  accDescr: The root contains a paragraph with a \"Hello \" text node and a link. The link contains the \"world\" text node. The selection's anchor and focus both point to offset 5 in \"world\".\n  subgraph state[\"EditorState\"]\n    direction TB\n    root[\"Root\"] --> paragraph[\"Paragraph\"]\n    paragraph --> hello[\"Text: 'Hello '\"]\n    paragraph --> link[\"Link\"]\n    link --> world[\"Text: 'world'\"]\n    selection[\"Selection\"] -.->|\"caret at offset 5\"| world\n  end\n```\n\n`editor.getEditorState()` returns the latest committed state. Its `toJSON()`\nmethod serializes the document tree only; the selection and the runtime node\nkeys are not included. To restore saved content, pass the JSON to\n`editor.parseEditorState()` and the result to `editor.setEditorState()`. The\neditor must have the same node types registered. See\n[Serialization](../serialization/serialization.md) for JSON, HTML, and\nMarkdown.\n\nHere's an example of how you can initialize an editor with saved content and\nthen persist it. The saved JSON is passed as the editor's `$initialEditorState`:\n\n```js\nimport {buildEditorFromExtensions} from '@lexical/extension';\nimport {RichTextExtension} from '@lexical/rich-text';\nimport {defineExtension} from 'lexical';\n\n// Get the saved content (e.g. loaded from a backend)\nconst initialEditorState = await loadContent();\n\nconst editor = buildEditorFromExtensions(\n  defineExtension({\n    $initialEditorState: initialEditorState,\n    dependencies: [RichTextExtension],\n    name: '@my-app/editor',\n  }),\n);\neditor.setRootElement(document.getElementById('editor'));\n\n// Store the content (e.g. when the user submits a form)\nasync function onSubmit() {\n  await saveContent(JSON.stringify(editor.getEditorState()));\n}\n```\n\nWith React, pass the extension to `LexicalExtensionComposer`. A component\nrendered inside it can reach the editor with `useLexicalComposerContext()`:\n\n```jsx\nimport {LexicalExtensionComposer} from '@lexical/react/LexicalExtensionComposer';\nimport {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';\nimport {RichTextExtension} from '@lexical/rich-text';\nimport {defineExtension} from 'lexical';\nimport {useMemo} from 'react';\n\nfunction SaveButton() {\n  const [editor] = useLexicalComposerContext();\n  return (\n    <button\n      onClick={() => saveContent(JSON.stringify(editor.getEditorState()))}>\n      Save\n    </button>\n  );\n}\n\nfunction Editor({initialEditorState}) {\n  // The editor is recreated whenever this extension changes,\n  // so keep it stable for the life of the editor.\n  const extension = useMemo(\n    () =>\n      defineExtension({\n        $initialEditorState: initialEditorState,\n        dependencies: [RichTextExtension],\n        name: '@my-app/editor',\n      }),\n    [initialEditorState],\n  );\n  return (\n    <LexicalExtensionComposer extension={extension}>\n      <SaveButton />\n    </LexicalExtensionComposer>\n  );\n}\n```\n\n`$initialEditorState` is applied once, when the editor is built; changing it\nlater has no effect on that editor. See \"Updating state\" below for the proper\nway to change editor state after initialization.\n\n`$initialEditorState` accepts:\n\n- a JSON string or a parsed `SerializedEditorState` object, passed to\n  `editor.parseEditorState()` (as in the example above);\n- an `EditorState` instance, applied directly with `editor.setEditorState()`;\n- a function `(editor) => void`, run inside `editor.update(...)`;\n- `null`, which skips default initialization entirely. Use this with\n  [collaboration](/docs/collaboration/react) so that the Yjs document, not\n  Lexical, owns the initial state.\n\nOmitting the field (or passing `undefined`) seeds the root with a default empty\n`ParagraphNode`. The two are not interchangeable: `null` leaves the root with no children,\nwhile `undefined` produces a single empty line. If your `loadContent` may yield `null` or\n`undefined` for new documents, coalesce to `undefined` (e.g. `(await loadContent()) ?? undefined`)\nso the editor still gets the default paragraph rather than the collab-style uninitialized\nstate.\n\nThe legacy `LexicalComposer` takes the same values as `initialConfig.editorState`,\nexcept that a function there only runs if the root is still empty.\n\n## Updating state\n\nAll reads and writes of the document happen inside a synchronous callback:\n\n```js\nimport {$createParagraphNode, $createTextNode, $getRoot} from 'lexical';\n\neditor.update(() => {\n  const paragraph = $createParagraphNode();\n  paragraph.append($createTextNode('Hello world'));\n  $getRoot().append(paragraph);\n});\n\nconst text = editor.read('force-commit', () => $getRoot().getTextContent());\n```\n\n### The `$` function convention {#dollar-functions}\n\nFunctions whose names start with `$`, such as `$getRoot()` and\n`$getSelection()`, only work inside one of these callbacks, because they act\non the active editor state. Calling them anywhere else throws an error. The\nconvention is similar to React Hooks:\n\n| | React Hooks | Lexical `$` functions |\n| -- | -- | -- |\n| Naming | `useFunction` | `$function` |\n| Can only be called | while rendering a component | inside an update or read |\n| Can call others of the same kind | ✅ | ✅ |\n| Must be synchronous | ✅ | ✅ |\n| Must be called unconditionally, in the same order | ✅ | ❌ No such rule |\n\nCommand handlers and node transforms already run inside an update, so they\ncan call `$` functions directly.\n\nThis works because, while a callback runs, Lexical keeps the active editor and\nthe active editor state in module-level variables, and `$` functions read\nthem: `$getRoot()` returns the root of the active state, and `$getEditor()`\nreturns the active editor. Because the callback is synchronous, nothing else\ncan run in between and change them. The same mechanism enforces the\ndifference between reads and updates. Inside a read, a method that would\nchange a node throws `Cannot use method in read-only mode.`\n\nGive your own functions a `$` prefix when they call `$` functions, so it is\nclear where they can be used. The\n[`@lexical/rules-of-lexical`](/docs/packages/lexical-eslint-plugin) ESLint rule\nchecks this convention.\n\nThe same rule applies to node objects. Call node methods only inside a read or\nupdate. Every node has a key that identifies it across versions of the editor\nstate, and node methods use that key to find the latest version of the node.\nThis is why a node reference taken earlier in an update stays usable after the\nnode changes. Keys exist only at runtime: they are not serialized, and you\nshould treat them as opaque.\n\nBecause the callbacks are synchronous, do any asynchronous work (fetching\ndata, awaiting a promise) first, and then enter an update with the result.\n\nAvoid nesting one `editor.update()` inside another: the inner update does not\nrun immediately but is queued to run after the outer one. Never start an\nupdate inside a read.\n\nEach update works on a pending copy of the editor state, and updates made in\nthe same tick are committed and reconciled to the DOM together. See\n[Updates](./updates.mdx#update-lifecycle) for the full lifecycle.\n\n### Replacing the state\n\nAnother way to set state is `setEditorState` method, which replaces current state with the one passed as an argument.\n\nHere's an example of how you can set editor state from a stringified JSON:\n\n```js\nconst editorState = editor.parseEditorState(editorStateJSONString);\neditor.setEditorState(editorState);\n```\n\n:::warning\n\n`setEditorState` rejects an `EditorState` that satisfies `editorState.isEmpty()` — i.e.\nthe root is the only node and there is no selection. In a development build it throws\n`\"setEditorState: the editor state is empty. Ensure the editor state's root node never\nbecomes empty.\"`; in production it warns with the same message and recovers by appending\nan empty `ParagraphNode`, so the editor stays usable. The `EditorState` produced by\ninitializing with `editorState: null` and never appending content (typical for a\ncollaboration document before peers connect) has exactly this shape, so persisting and\nreloading such a state hits this path. Note that `parseEditorState` itself succeeds — the\ncheck lands on the apply step. To avoid relying on the recovery, guard with\n`editorState.isEmpty()` before calling `setEditorState`, or seed the document with a\n`ParagraphNode` before serializing.\n\n:::\n\n## Reading state\n\nWhich state you see depends on how you read it:\n\n- Inside `editor.update()` you see the **pending** state, where your changes\n  are visible but transforms and reconciliation may not have run yet.\n  `editor.read('pending', fn)` gives you the same view without allowing\n  changes.\n- `editor.read('force-commit', fn)` first commits any pending updates, so it\n  always sees a consistent, reconciled state. Do not call it inside an update.\n  `editor.read(fn)` with no mode does the same thing, and is kept for\n  convenience and backwards compatibility. Inside a mutation or update\n  listener it does not commit an update that another listener started (that\n  update commits after the listeners have run, so they observe commits in\n  order); use `editor.read('latest', fn)` there.\n- `editor.read('latest', fn)` reads the most recently reconciled state without\n  committing anything, so pending changes are not visible.\n- `editorState.read(fn, {editor})` reads one particular snapshot, such as the\n  `editorState` an update listener receives. Passing the `editor` keeps\n  `$getEditor()` and extension lookups working inside the callback.\n\n## State update listener\n\nIf you want to know when the editor updates so you can react to the changes, you can add an update\nlistener to the editor, as shown below:\n\n```js\neditor.registerUpdateListener(({editorState}) => {\n  // The latest EditorState can be found as `editorState`.\n  // To read the contents of the EditorState, use the following API:\n\n  editorState.read(\n    () => {\n      // Just like editor.update(), .read() expects a closure where you can use\n      // the $ prefixed helper functions.\n    },\n    // Passing the editor keeps $getEditor() and extension lookups working.\n    {editor},\n  );\n});\n```\n\n## When are Listeners, Transforms, and Commands called?\n\nThere are several types of callbacks that can be registered with the editor that are related to\nupdates of the Editor State.\n\n| Callback Type | When It's Called |\n| -- | -- |\n| Update Listener | After reconciliation |\n| Mutation Listener | After reconciliation |\n| Node Transform | During `editor.update()`, after the callback finishes, if any instances of the node type they are registered for were updated |\n| Command | As soon as the command is dispatched to the editor (called from an implicit `editor.update()`) |\n\n## Synchronous reconciliation with discrete updates\n\nWhile commit scheduling and batching are normally what we want, they can sometimes get in the way.\n\nConsider this example: you're trying to manipulate an editor state in a server context and then persist it in a database.\n\n```js\neditor.update(() => {\n  // manipulate the state...\n});\n\nsaveToDatabase(editor.getEditorState().toJSON());\n```\n\nThis code will not work as expected, because the `saveToDatabase` call will happen before the state has been committed.\nThe state that will be saved will be the same one that existed before the update.\n\nFortunately, the `discrete` option for `LexicalEditor.update` forces an update to be immediately committed.\n\n```js\neditor.update(() => {\n  // manipulate the state...\n}, {discrete: true});\n\nsaveToDatabase(editor.getEditorState().toJSON());\n```\n\n### Cloning state\n\nLexical state can be cloned, optionally with custom selection. One of the scenarios where you'd want to do it\nis setting editor's state but not forcing any selection:\n\n```js\n// Passing `null` as a selection value to prevent focusing the editor\neditor.setEditorState(editorState.clone(null));\n```\n","packages/lexical-website/docs/concepts/headless.md":"# Running Without a Browser\n\nThe DOM is only needed to show an editor on screen. Because every read and\nupdate goes through the editor state, Lexical also works where there is no\nbrowser DOM:\n\n- **With no DOM at all.** An editor that has no root element skips\n  reconciliation and DOM selection entirely, so you can build one with\n  `buildEditorFromExtensions()` and never call `setRootElement()`. Adding\n  `HeadlessExtension` from `@lexical/headless` makes that explicit: attaching a\n  root element then throws, and nested editors are headless too. Updates,\n  transforms, listeners, commands, and JSON serialization all still work,\n  which is enough to process documents on a server, apply changes from a\n  collaboration backend, or write tests.\n- **With a virtual DOM** such as happy-dom or jsdom, for the features that\n  create DOM nodes, like HTML import and export. `withDOM()` from\n  `@lexical/headless/dom` runs a callback with a temporary happy-dom window,\n  for example to convert HTML on a server.\n\n## Example: converting saved JSON to HTML on a server\n\n```ts\nimport {buildEditorFromExtensions} from '@lexical/extension';\nimport {HeadlessExtension} from '@lexical/headless';\nimport {withDOM} from '@lexical/headless/dom';\nimport {$generateHtmlFromNodes} from '@lexical/html';\nimport {RichTextExtension} from '@lexical/rich-text';\nimport {defineExtension} from 'lexical';\n\nconst editor = buildEditorFromExtensions(\n  defineExtension({\n    dependencies: [HeadlessExtension, RichTextExtension],\n    name: '@my-app/server-editor',\n  }),\n);\n\nexport function jsonToHtml(json: string): string {\n  editor.setEditorState(editor.parseEditorState(json));\n  return withDOM(() => editor.read(() => $generateHtmlFromNodes(editor)));\n}\n```\n\nThe editor must have the same node types registered as the editor that saved\nthe JSON, which is why it uses the same extensions. JSON and Markdown\nconversion do not need `withDOM()`.\n\n## `createHeadlessEditor()`\n\nBefore extensions, headless editors were created with `createHeadlessEditor()`\nfrom `@lexical/headless`. It still works, and it makes DOM-only methods such as\n`registerRootListener` and `registerMutationListener` throw. Because many\nextensions register those listeners, use `HeadlessExtension` with\n`buildEditorFromExtensions()` in new code.\n","packages/lexical-website/docs/concepts/history.md":"# History\n\nLexical adds support for Undo and Redo through the `@lexical/history` package —\n`registerHistory`, the `HistoryExtension` extension, or the `HistoryPlugin`\nReact component from `@lexical/react/LexicalHistoryPlugin`. Part of undo and redo requires supporting coalescing of certain dynamic operations such as continuous typing. The below provides background information regarding the continuous typing undo coalescing feature.\n\n**Undo Coalescing of Typed Text**\n\n**TL;DR**\nWhen working with text editors, users expect a well polished undo experience. A key part of this is a smooth, well thought out workflow when undoing continuous typing events. For this post, strict typing events may be limited to typing characters, backspacing and forward deleting. Users generally expect that continuous typing should fully undo with a single undo gesture and that they don’t require separate undo gestures for each character.\nThe algorithm that covers this is “undo coalescing of typed text” and there’s a certain degree of nuance involved. This post covers that nuance while attempting to avoid prescriptive implementation details.\n\n**What Kind of Edits Require Coalescing?**\nAs mentioned above, continuous typing should be coalesced into a single undo action. Continuous typing is a loaded phrase and we’ll get into some details below.\nThere are other types of edits that require undo coalescing such as rapidly resizing an image or rapidly changing the font size of selected text. These latter two examples fall under what might be termed “dynamic operations” and their implementation might vary quite a bit from coalescing of typing.\n\n**What Constitutes Continuous Typing?**\nFor the purposes of undo coalescing, continuous typing events are safely limited to these three actions:\n\n1. Forward typing characters (heads up, composed character typing discussed later).\n2. Deleting backwards, e.g. backspace gestures including backspace key presses.\n3. Forward deletion, e.g. forward delete gestures including Delete key presses.\n\nAny interruption to the above breaks the notion of continuous typing. Note that switching between either of these three actions may itself be considered an interruption of continuous typing or it may not. This ends up being a usability decision. In my opinion, when examining various word processors, switching between these three actions should maintain continuity, because users often make typos or corrections in the flow of typing and therefore would lean towards the whole sequence of typing, backspacing and deleting to be within a single undoable session.\nNote that Google Docs and Quip treat a switch between typing, backspacing and deleting as disruptions to the continuous typing sessions.\n\n**Start with Selected Text and Followup with Typing**\nLet’s start with some selected text. The user may have selected the text directly, as a result of pasting text or through some other sequence of steps. When followed by continuous typing, the initial selection should be included along with the following typing events as part of a single coalesced undo session.\n\n**Timeouts**\nText editing is generally a modeless user experience and so just about any event beyond the above typing actions will disrupt continuous typing. The first and foremost is a timeout.\nQuip seems to require continuous typing to occur within 1 second intervals, whereas Google Docs requires about 2 seconds. In my opinion, the timeout may stretch to as long as 3 to 5 seconds. The more time between typing events, the more chance the user has to intentionally input a block of text, a thought or concept.\nThe use of a timeout is not necessarily required, and the amount of time is certainly up for debate. Collaborative co-editing also makes use of timeouts when grouping text that is merged across clients. Longer timeouts tend to make collaboration a smoother, less disruptive experience, because receiving other client input while expressing one’s thoughts tends to be unhelpful.\nImplementers should consider the correct timeout based on their customer expectations and related collaborative features that might share this timeout.\n\n**Hard and Soft Returns**\nHard returns as generated by pressing the Return key tend to disrupt and thereby reset continuous typing.\nFor Google Docs and Quip, soft returns as generated by pressing shift+Return key do not terminate continuous typing. This makes sense in that the soft return ends text wrap but does not end the paragraph.\n\n**Changing Selections and Key Focus**\nAny change to the selection by the user should terminate coalescing. This includes:\n\n- Arrow gesture navigation.\n- Gestures to select a word, a sentence or a paragraph.\n- Any other selection change that does not move the blinking caret to the next or previous character by way of the above mentioned typing events.\n\nAdditionally, ending keyboard focus on the editing window, including moving focus to another editing window or a different view within the same window should also terminate coalescing. This also includes setting focus to modal popovers, such as, for example, a hyperlink property editor.\n\n**Composed Characters**\nCharacter composition occurs when typing diacritics, CJK and even emojis. The process usually involves at least these three phases:\n\n- Start composition.\n- Replace composed text with other text and remain composing. This step may occur many times within a single composition session.\n- Stop composition either through accepting the text or canceling the latest input.\n\nCoalescing should terminate at start composition. Replacing composed text should generally be coalesced. Stopping composition may or may not reset coalescing depending upon the implementation. Quip appears to try to coalesce multiple composed characters, but disrupts coalescing when switching between composed and non-composed characters. Google Docs appears to disrupt coalescing more frequently. Both coalesce the text replacements.\n\n**Auto Correct, Spelling Corrections and Text Substitutions**\nText substitution behaves much like composed characters. When the text is transformed, the coalescing should terminate and the transformed text becomes the first coalesced entry in the next undo session.\n\n**Copy & Pasting**\nAny actions like copy and paste typically trigger events that are not part of the 3 continuous typing actions described above. So, copy and paste also terminates coalescing.\n\n**In Conclusion**\nText undo coalescing greatly improves the usability of undo in that it allows the user to bypass all the intermediate events that went into forming or re-forming text within a paragraph. Given the modeless nature of text editing, limiting continuous typing to strictly three events allows for a robust and clean implementation. Implementers will need tight bottlenecks to trap selection changes, key focus changes as well as changes from collaborative clients. While the above is not complete by any means, I hope readers will find it helpful.\n\n## Tuning HistoryExtension for memory and long sessions\n\n`HistoryExtension` ships with config values chosen for the common case\nof a short-lived editor on a richly interactive surface. For\napplications that keep an editor mounted for a long time — chat\nthreads, long documents, anything where the user may produce thousands\nof distinct history events without reloading the page — two settings\nare worth tuning explicitly.\n\n### `maxDepth` — cap the undo stack\n\nBy default `maxDepth` is `null`, meaning the undo stack is\n**unbounded**. Every separate-history boundary (see \"What Constitutes\nContinuous Typing?\" above) pushes a `HistoryStateEntry` that retains a\nfull `EditorState` snapshot. Snapshots share a copy-on-write\n`NodeMap`, so the marginal cost of any one entry is small, but it is\nnot zero — and an unbounded stack accumulates a long tail over time.\n\nSet a finite `maxDepth` to keep the stack at a fixed length. The\nextension FIFO-evicts the oldest entries past the cap; older snapshots\nbecome unreachable and can be collected.\n\n```ts\nimport {configExtension, defineExtension} from 'lexical';\nimport {HistoryExtension} from '@lexical/history';\n\nconst editorExtension = defineExtension({\n  // ...\n  dependencies: [\n    // 100 events is a reasonable starting point that supports a deep\n    // enough undo stack for almost any interactive editing session.\n    configExtension(HistoryExtension, {maxDepth: 100}),\n  ],\n});\n```\n\nThe cap is also exposed as a writable signal on\n`HistoryExtension.output.maxDepth`, so it can be reconfigured at\nruntime (the new value applies to future pushes — the current stack\nis not retroactively trimmed when the cap is lowered).\n\n### `delay` — coalesce more aggressively\n\n`delay` is the merge window during which adjacent edits collapse into\nthe current history entry instead of pushing a new one. The default is\n`300`ms. A longer delay means fewer history events for the same\ntyping burst, which:\n\n- reduces the rate at which `undoStack` grows, multiplying the effect\n  of any `maxDepth` cap, and\n- gives the user undo gestures that match larger semantic chunks of\n  their writing.\n\nFor text-heavy editors a value in the `500`–`1000`ms range is common.\n\n```ts\nconfigExtension(HistoryExtension, {delay: 500, maxDepth: 100}),\n```\n\n### When NOT to cap history\n\nIf your editor is short-lived (mounted only for the duration of a\nmodal, page form, or single message draft) and undo is expected to\nreach back to the document's initial state, the default `maxDepth:\nnull` is fine — the stack will be discarded with the editor instance.\nThe cap matters when an editor is **long-lived** and can accumulate\nthousands of distinct history events without unmounting.\n","packages/lexical-website/docs/concepts/key-management.md":"# Key Management\n\nKeys are a fundamental concept in Lexical that enable efficient state management and node tracking. Understanding how keys work is crucial for building reliable editor implementations.\n\n## What are Keys?\n\nThe `__key` property is a unique identifier assigned to each node in the Lexical editor. These keys are:\n- Automatically generated by Lexical\n- Used to track nodes in the editor state\n- Essential for state management and updates\n- Immutable during a node's lifecycle\n\n## When to Use `__key`?\n\n### ✅ Correct Usage\n\nKeys should ONLY be used in two specific situations:\n\n1. **In Node Constructors**\n```typescript\nclass MyCustomNode extends ElementNode {\n  constructor(someData: string, key?: NodeKey) {\n    super(key); // Correctly passing key to parent constructor\n    this.__someData = someData;\n  }\n}\n```\n\n2. **In Static Clone Methods**\n```typescript\nclass MyCustomNode extends ElementNode {\n  static clone(node: MyCustomNode): MyCustomNode {\n    return new MyCustomNode(node.__someData, node.__key);\n  }\n}\n```\n\n### ❌ Incorrect Usage\n\nNever use keys in these situations:\n\n```typescript\n// ❌ Don't pass keys between different nodes\nconst newNode = new MyCustomNode(existingNode.__key);\n\n// ❌ Don't manipulate keys directly\nnode.__key = 'custom-key';\n```\n\n## How Lexical Uses Keys\n\n### Diagram\n\nThe dotted outlines show nodes that are re-used in a zero-copy fashion from one EditorState to the next\n\n```mermaid\ngraph TB\n  subgraph s0[\"Initial State\"]\n    direction TB\n    m0[\"NodeMap (v0)\"]\n    m0 -->|Key A| n0a[\"A (v0)\"]\n    m0 -->|Key B| n0b[\"B (v0)\"]\n  end\n  subgraph s1[\"Create Node C\"]\n    direction TB\n    style n1a stroke-dasharray: 5 5\n    style n1b stroke-dasharray: 5 5\n    m1[\"NodeMap (v1)\"]\n    m1 -->|Key A| n1a[\"A (v0)\"]\n    m1 -->|Key B| n1b[\"B (v0)\"]\n    m1 -->|Key C| n1c[\"C (v0)\"]\n  end\n  subgraph s2[\"Update Node A\"]\n    direction TB\n    style n2a stroke-dasharray: 5 0\n    style n2b stroke-dasharray: 5 5\n    style n2c stroke-dasharray: 5 5\n    m2[\"NodeMap (v1)\"]\n    m2 -->|Key A| n2a[\"A (v1)\"]\n    m2 -->|Key B| n2b[\"B (v0)\"]\n    m2 -->|Key C| n2c[\"C (v0)\"]\n  end\n  s0 -.-> s1 -.-> s2\n```\n\n### Node Map Structure\n\nThe EditorState maintains a `Map<NodeKey, LexicalNode>` that tracks all nodes. Nodes refer to each other using keys in their internal pointers:\n\n```typescript\n// Internal node structure (not for direct usage)\n{\n  __prev: null | NodeKey,\n  __next: null | NodeKey,\n  __parent: null | NodeKey,\n  // __first, __last and __size are only for ElementNode to track its children\n  __first: null | NodeKey,\n  __last: null | NodeKey,\n  __size: number\n}\n```\n\nThese internal pointers maintain the tree structure and should never be manipulated directly.\n\n### Key-Related APIs\n\n1. **Editor Methods**\n   ```typescript\n   // Get node by key\n   const node = $getNodeByKey(key);\n\n   // Get the DOM element a node key is reconciled to\n   const element = editor.getElementByKey(key);\n\n   // Get latest version of a node\n   const latest = node.getLatest();\n   \n   // Get mutable version for updates\n   const mutable = node.getWritable();\n   ```\n\n### Key Lifecycle\n\nNodeKeys are ephemeral and have several important characteristics:\n\n1. **Serialization**\n   - Keys are never serialized\n   - New keys are generated when deserializing (from JSON/HTML)\n   - Keys are only meaningful within their EditorState instance\n\n2. **Uniqueness**\n   - Keys are unique within an EditorState\n   - Current implementation uses serial numbers for debugging\n   - Should be treated as random and opaque values\n   - Never logically reused\n\nKeys are used internally by Lexical to:\n1. Track nodes in the editor state\n2. Manage node updates and versions\n3. Maintain referential integrity\n4. Enable efficient state updates\n\n## Common Pitfalls\n\n1. **Key Reuse**\n   ```typescript\n   // ❌ Never do this\n   function duplicateNode(node: LexicalNode) {\n     return new SameNodeType(data, node.__key);\n   }\n   ```\n\n2. **Manual Key Assignment**\n   ```typescript\n   // ❌ Never do this\n   node.__key = generateCustomKey();\n   ```\n\n3. **Incorrect Constructor/Clone Implementation**\n   ```typescript\n   // ❌ Never do this - missing key in constructor\n   class MyCustomNode extends ElementNode {\n     constructor(someData: string) {\n       super(); // Missing key parameter\n       this.__someData = someData;\n     }\n   }\n\n   // ✅ Correct implementation\n   class MyCustomNode extends ElementNode {\n     __someData: string;\n\n     constructor(someData: string, key?: NodeKey) {\n       super(key);\n       this.__someData = someData;\n     }\n     \n     static clone(node: MyCustomNode): MyCustomNode {\n       return new MyCustomNode(node.__someData, node.__key);\n     }\n\n     afterCloneFrom(prevNode: this): void {\n       super.afterCloneFrom(prevNode);\n       this.__someData = prevNode.__someData;\n     }\n   }\n   ```\n\n4. **Node Replacement**\n   ```typescript\n   // ❌ Never re-use the key when changing the node class\n   const editorConfig = {\n     nodes: [\n       CustomNodeType,\n       {\n         replace: OriginalNodeType,\n         with: (node: OriginalNodeType) => new CustomNodeType(node.__key),\n         withKlass: CustomNodeType\n       }\n     ]\n   };\n\n   // ✅ Correct: Use node replacement configuration\n   const editorConfig = {\n     nodes: [\n       CustomNodeType,\n       {\n         replace: OriginalNodeType,\n         with: (node: OriginalNodeType) => new CustomNodeType(),\n         withKlass: CustomNodeType\n       }\n     ]\n   };\n   ```\n\n   For proper node replacement, see the [Node Replacement guide](node-replacement.md).\n\n## Best Practices\n\n1. **Let Lexical Handle Keys**\n   ```typescript\n   import {$applyNodeReplacement} from 'lexical';\n\n   // Create node helper function\n   export function $createMyCustomNode(data: string): MyCustomNode {\n     return $applyNodeReplacement(new MyCustomNode(data));\n   }\n   ```\n\n## Testing Considerations\n\nWhen writing tests involving node keys:\n\n```typescript\ntest('node creation', async () => {\n  await editor.update(() => {\n    // ✅ Correct: Create nodes normally\n    const node = new MyCustomNode(\"test\");\n    \n    // ✅ Correct: Keys are automatically handled\n    expect(node.__key).toBeDefined();\n    expect(node.__key).not.toBe('');\n  });\n});\n```\n\n## Performance Impact\n\nUnderstanding key management is crucial for performance:\n\n1. Keys enable efficient node lookup (O(1))\n2. Proper key usage prevents unnecessary re-renders\n3. Lexical's key system optimizes state updates\n4. Improper key manipulation can cause performance issues\n\n## Common Questions\n\n**Q: How do I reference a node later?**\nA: Store a reference to the node. Conventionally, all node methods will use `getLatest()` or `getWritable()` which will look up the latest version of that node before reading or writing its properties, which is equivalent to using the key but is type-safe (but may cause errors if you try to use a reference to a node that no longer exists). In some situations it may be preferable to use the key directly, which is also fine.\n\n**Q: How do I ensure unique nodes?**\nA: Let Lexical handle key generation and management. Focus on node content and structure. ","packages/lexical-website/docs/concepts/keyboard-accessibility.md":"# Keyboard Accessibility\n\nLexical follows the [WAI-ARIA Authoring Practices](https://www.w3.org/WAI/ARIA/apg/) keyboard model. This page documents the contracts that plugin authors and host applications rely on, so a Lexical editor can sit inside a larger keyboard-accessible flow without trapping or surprising the user.\n\n## Tab key\n\nBy default, `Tab` inside the editor follows the browser's normal focus order — it moves to the next focusable element on the page. There is no keyboard trap.\n\nThe optional `TabIndentationExtension` (`@lexical/extension`) takes over `Tab` and uses it for block indent / outdent. Hosts that mount this extension opt in to that behavior. WCAG 2.1.2 (No Keyboard Trap) still applies — pressing `Escape` blurs the editor (see below), at which point `Tab` leaves the editor again.\n\n## Escape key\n\nPressing `Escape` while the editor has focus calls `editor.blur()` via the rich-text Escape command handler (registered at `COMMAND_PRIORITY_EDITOR`). The handler only blurs when the current selection is a range selection; with a `NodeSelection` (or no selection) it returns `false` and falls through without blurring. After blur, the next `Tab` advances to the page's next focusable element — so the editor is not a hard keyboard trap. (`TabIndentationExtension` consumes `Tab` only while a range selection is active, and its own source notes that taking over `Tab` is generally discouraged for keyboard accessibility, so treat WCAG 2.1.2 conformance as something to verify in your integration rather than a guarantee.)\n\nPlugin authors who want to intercept `Escape` (modal close, dropdown close, etc.) should register their handler at a higher priority and return `true` to stop the command chain. Common patterns already established in the codebase:\n\n| Priority | Caller | Purpose |\n| --- | --- | --- |\n| `HIGH` | Floating link editor | Close the floating UI, keep editor focus |\n| `HIGH` | Equation component (while editing) | Close the equation editor input |\n| `NORMAL` | Comment plugin | Cancel inline comment editing |\n| `LOW` | Image component | Escape out of caption / resize editing: drop the inner selection and restore the `NodeSelection` on the image decorator |\n| `EDITOR` | Rich text (default) | `editor.blur()` (range selection only) |\n\nHandlers that return `false` let the chain continue, so Escape can still fall through to the default blur even after a higher-priority handler runs.\n\nSome host UI binds its close handler directly to a DOM `keydown` listener rather than going through `KEY_ESCAPE_COMMAND` — the playground's `Modal` via `window.addEventListener('keydown', ...)` and its `DropDown` via a React `onKeyDown` on the menu. Those paths run alongside the table above; they do not participate in the editor's command priority chain.\n\n## React hooks and `@lexical/a11y` extensions\n\nThe keyboard helpers below ship as `@lexical/a11y` _extensions_ — the extension is the API and does all of the work (event listeners, focus management, the live region). The `@lexical/react` hooks (`useLexicalFocusManagerRef`, `useLexicalRovingTabIndexRef`, `useLexicalFocusTrapRef`, `useLexicalAriaLiveRegion`) are thin React adapters that only _communicate_ with their extension: each returns a `RefCallback` (or an `announce` function) that drives the extension's signals. Using a hook therefore **requires** its matching extension to be registered in the editor's extension tree — it is not an optional integration path. Non-React hosts (Svelte, Vue, vanilla DOM) use the extensions directly and skip the hooks.\n\n## Toolbar focus jump\n\nThe playground uses `useLexicalFocusManagerRef` (`@lexical/react/useLexicalFocusManagerRef`) to provide a documented shortcut for moving focus from the editor to the toolbar without tabbing through the page. The hook returns a `RefCallback` that registers the attached element as a focus-managed toolbar. It listens for `Alt+F10` inside the editor and moves focus into the toolbar — preferring the item that currently carries `tabindex=\"0\"` (the active roving item, when paired with `useLexicalRovingTabIndexRef`), and otherwise the first focusable item. While focus is in the toolbar, pressing `Escape` restores both focus and the editor's prior selection via `editor.focus()`. Multiple toolbars can be registered simultaneously. Requires `FocusManagerExtension` from `@lexical/a11y` in the editor's extension tree.\n\nThis matches the WAI-ARIA editor menubar pattern and the convention used by Word and CKEditor. Hosts that don't mount `useLexicalFocusManagerRef` get the default browser flow (Tab in, Tab out).\n\n## Toolbar arrow-key navigation\n\nToolbars that opt into `useLexicalRovingTabIndexRef` (`@lexical/react/useLexicalRovingTabIndexRef`) collapse to a single tab stop: arrow keys move between items inside the toolbar; `Tab` moves past the toolbar as a whole. Items keep `tabindex=\"-1\"` except the active one (`tabindex=\"0\"`). The hook returns a `RefCallback` — multiple containers can be registered simultaneously. Tracks the WAI-ARIA toolbar pattern. Requires `RovingTabIndexExtension` from `@lexical/a11y` in the editor's extension tree.\n\n## Modal focus trap\n\n`useLexicalFocusTrapRef` (`@lexical/react/useLexicalFocusTrapRef`) returns a `RefCallback` that cycles `Tab` / `Shift+Tab` inside a modal container and restores focus to the previously-focused element on deactivation. While active, the trap also installs a document-level `focusin` listener that pulls any focus landing outside the container back to the first focusable descendant (or the container itself) — so it recovers from the browser routing `Tab` out to its own chrome. Although several containers can be _registered_, only one trap should be _active_ at a time: concurrent traps install competing `focusin` listeners and fight over focus. Escape is **not** intercepted by the hook — modal owners handle the close key themselves, so the editor's Escape contract above stays predictable. Requires `FocusTrapExtension` from `@lexical/a11y` in the editor's extension tree.\n\n## Screen reader announcements\n\n`useLexicalAriaLiveRegion` (`@lexical/react/useLexicalAriaLiveRegion`) returns a stable `announce(message)` function. The hook itself mounts no DOM — like the others above, it is a thin adapter that **requires `AriaLiveRegionExtension` from `@lexical/a11y` in the editor's extension tree**. That extension owns the lifecycle of the visually-hidden region (`role=\"status\"`, `aria-atomic`, `aria-live` defaulting to `polite` — configurable via its `politeness` config): the region is created in the extension's `register` step and disposed on the editor's teardown, not on the hook's unmount. `EditorModeAnnounceExtension` (mode transitions via `registerEditableListener`) and `HistoryAnnounceExtension` (undo / redo via `UNDO_COMMAND` / `REDO_COMMAND`) announce through the same region. Both extensions accept optional message overrides for i18n.\n\n## Component ARIA quick reference\n\nWhere Lexical already applies ARIA attributes today (set in each component's `createDOM` / `updateDOM`, declaratively in the surrounding React tree, or applied imperatively via `setAttribute` on a managed or portaled element — as the typeahead menu and the live region do):\n\n| Component | role | aria-\\* attributes |\n| --- | --- | --- |\n| `ToolbarPlugin` (playground) | `toolbar` | `aria-label` |\n| `Modal` (playground) | `dialog` | `aria-modal`, `aria-labelledby` |\n| `LexicalListItemNode` (check list leaf) | `checkbox` | `aria-checked` |\n| `LexicalMenu` typeahead popup | `listbox` | `aria-label` |\n| Editor root (while typeahead is open) | — | `aria-controls`, `aria-activedescendant` |\n| `EquationNode` (playground) | `math` | `aria-label` |\n| `useLexicalAriaLiveRegion` region | `status` | `aria-live`, `aria-atomic` |\n\nCustom nodes follow the same pattern: set the attributes in `createDOM` and re-apply in `updateDOM` when the state they reflect changes. There is no central ARIA contract on `LexicalNode`; treat each subclass's `createDOM` as the source of truth.\n\n## Browser notes\n\nLexical relies on the standard DOM `Escape` contract — verified on Chrome and Safari (macOS, normal window). Safari in fullscreen intercepts `Escape` at the browser level to exit fullscreen; that is the browser's contract, not Lexical's.\n\n## What this page does not cover\n\n- Touch / mobile screen reader gestures. The hooks above share the same DOM contracts with mobile (TalkBack, VoiceOver iOS) but mobile has not been exercised in this set of changes.\n- High contrast / forced-colors theming. See the playground's `index.css` `@media (forced-colors: active)` block for a baseline.\n","packages/lexical-website/docs/concepts/listeners.md":"\n\n# Listeners\n\nListeners are a mechanism that lets the Editor instance inform the user when a certain operation has occurred. All listeners follow a reactive pattern where you can do an operation upon something happening in the future. All listeners also return a function that easily allows for the\nlistener to be unregistered. Below are the different listeners that Lexical supports today:\n\n## `registerUpdateListener`\n\nGet notified when Lexical commits an update to the DOM.\n\n```js\nconst removeUpdateListener = editor.registerUpdateListener(({editorState}) => {\n  // The latest EditorState can be found as `editorState`.\n  // To read the contents of the EditorState, use the following API:\n\n  editorState.read(() => {\n    // Just like editor.update(), .read() expects a closure where you can use\n    // the $ prefixed helper functions.\n  });\n});\n\n// Do not forget to unregister the listener when no longer needed!\nremoveUpdateListener();\n```\n\nThe update listener callbacks receives a single argument containing the follow properties:\n\n- `editorState` the latest updated Editor State\n- `prevEditorState` the previous Editor State\n- `tags` a Set of all tags that were passed to the update\n- `dirtyElements` a Map of the NodeKeys of ElementNodes that were marked\n  dirty, to a boolean that is true when the node was intentionally mutated\n- `dirtyLeaves` a Set of the NodeKeys of all dirty nodes that are not\n  ElementNodes\n- `mutatedNodes` a Map of LexicalNode constructors to a\n  `Map<NodeKey, NodeMutation>`, or null when no DOM was mutated. Only\n  populated when at least one MutationListener is registered\n- `normalizedNodes` (advanced) the keys of TextNode descendants that were\n  merged with a sibling by normalization\n\nOne thing to be aware of is \"waterfall\" updates. This is where you might schedule an update inside an update\nlistener, as shown below:\n\n```js\neditor.registerUpdateListener(({editorState}) => {\n  // Read the editorState and maybe get some value.\n  editorState.read(() => {\n    // ...\n  });\n\n  // Then schedule another update.\n  editor.update(() => {\n    // ...\n  });\n});\n```\n\nThe problem with this pattern is that it means we end up doing two DOM updates, when we likely could have\ndone it in a single DOM update. This can have an impact on performance, which is important in a text editor.\n\nA listener that re-enqueues work without a stop condition is worse than slow.\nThe editor keeps a cascade budget: once a run of updates re-enqueues more\nthan 100 times without yielding to the event loop, it discards the entire\npending update queue and warns. Bounded activity never reaches the limit —\nthe budget is reset from a macrotask, which a genuine runaway starves — but a\ntrue loop silently loses whatever else was queued.\n\nTo avoid this, we recommend looking into [Node Transforms](transforms.md), which allow you to listen to node changes and\ntransform them as part of the same given update, meaning no waterfalls!\n\n## `registerTextContentListener`\n\nGet notified when Lexical commits an update to the DOM and the text content of the editor changes from\nthe previous state of the editor. If the text content is the same between updates, no notifications to\nthe listeners will happen.\n\n```js\nconst removeTextContentListener = editor.registerTextContentListener(\n  (textContent) => {\n    // The latest text content of the editor!\n    console.log(textContent);\n  },\n);\n\n// Do not forget to unregister the listener when no longer needed!\nremoveTextContentListener();\n```\n\n## `registerMutationListener`\n\nGet notified when a specific type of Lexical node has been mutated. There are three states of mutation:\n\n- `created`\n- `destroyed`\n- `updated`\n\nMutation listeners are great for tracking the lifecycle of specific types of node. They can be used to\nhandle external UI state and UI features relating to specific types of node.\n\nIf any existing nodes are in the DOM, and skipInitialization is not true, the listener\nwill be called immediately with an updateTag of 'registerMutationListener' where all\nnodes have the 'created' NodeMutation. This can be controlled with the skipInitialization option,\nwhich defaults to false.\n\n```js\nconst removeMutationListener = editor.registerMutationListener(\n  MyCustomNode,\n  (mutatedNodes, { updateTags, dirtyLeaves, prevEditorState }) => {\n    // mutatedNodes is a Map where each key is the NodeKey, and the value is the state of mutation.\n    for (let [nodeKey, mutation] of mutatedNodes) {\n      console.log(nodeKey, mutation)\n    }\n  },\n  {skipInitialization: false}\n);\n\n// Do not forget to unregister the listener when no longer needed!\nremoveMutationListener();\n```\n\n## `registerEditableListener`\n\nGet notified when the editor's mode has changed. The editor's mode can be changed\nvia `editor.setEditable(boolean)`.\n\n```js\nconst removeEditableListener = editor.registerEditableListener(\n  (editable) => {\n    // The editor's mode is passed in!\n    console.log(editable);\n  },\n);\n\n// Do not forget to unregister the listener when no longer needed!\nremoveEditableListener();\n```\n\n## `registerDecoratorListener`\n\nGet notified when the editor's decorator object changes. The decorator object contains\nall `DecoratorNode` keys -> their decorated value. This is primarily used with external\nUI frameworks.\n\n```js\nconst removeDecoratorListener = editor.registerDecoratorListener(\n  (decorators) => {\n    // The editor's decorators object is passed in!\n    console.log(decorators);\n  },\n);\n\n// Do not forget to unregister the listener when no longer needed!\nremoveDecoratorListener();\n```\n\n## `registerRootListener`\n\nGet notified when the editor's root DOM element (the content editable Lexical attaches to) changes. This is primarily used to\nattach event listeners to the root element. *The root listener function is executed directly upon registration and then on any subsequent update.*\n\n```js\nconst removeRootListener = editor.registerRootListener(\n  (rootElement, prevRootElement) => {\n   //add listeners to the new root element\n   //remove listeners from the old root element\n  },\n);\n\n// Do not forget to unregister the listener when no longer needed!\nremoveRootListener();\n```\n","packages/lexical-website/docs/concepts/named-slots.md":"# Named Slots\n\n:::warning\n\nNamed slots are experimental. The APIs described here are tagged\n`@experimental`, may change without a major version bump, and the\nserialization and collab formats should be treated as unstable until the\nfeature is stabilized.\n\n:::\n\nNamed slots let a single host node (an ElementNode or a DecoratorNode) own\nseveral isolated editable regions addressed by name — a Card's `title`, a\nPullQuote's `quote` and `attribution` — inside the host's own EditorState.\nEach region takes its own caret and formatting, never merges across the\nboundary, and `Cmd+A` doesn't spill into the rest of the document.\n\nBefore named slots, the usual answer was a nested editor per region: every\nregion lives in its own EditorState, so moving nodes between regions and\nkeeping history and collab in sync all require serialization and extra\n`editor.update` passes. The other node shapes don't fit either: plain\nElementNode children share one undivided linked list (Backspace at a region\nstart merges it into the previous region), and a DecoratorNode is atomic, so\nLexical can't own selection, collab, or serialization inside it. With named\nslots, editing a slot is just editing the one tree.\n\n:::note\n\nNamed slots are the model-level generalization of the rendering concept\nbehind [`$getDOMSlot`](/docs/api/modules/lexical#getdomslot) /\n`ElementDOMSlot` in the\n[DOM rendering](../serialization/dom-render.md) documentation. Every\nElementNode already has one _unnamed_ children channel, and `$getDOMSlot`\ncontrols where that channel's content attaches in the node's DOM. Named\nslots are additional, explicitly-named channels alongside it — symmetric in\nthat each renders into a controllable location in the host's DOM — but they\nalso carry the model-level semantics the unnamed channel doesn't: isolation\n(the virtual shadow root below), a separate NodeKey map, and their own\nserialization and collaboration.\n\n:::\n\n## The Model\n\nA host keeps a second child channel, a `Map` of slot name to NodeKey,\nseparate from its ordinary linked-list children. A slot value has its slot\nhost pointer set and `getParent() === null`, with exactly one of the two\nnon-null: `getParent()` stops at the slot boundary, and you climb out only\nthrough [`$getSlotHost()`](/docs/api/modules/lexical#getslothost).\n\nThe slot link itself acts as a **virtual invisible shadow root** between the\nhost and the value. Isolation is structural rather than a convention — an\naccidental boundary crossing surfaces as a thrown invariant, not silent\ncorruption.\n\nIn the DOM, each value renders synchronously into a keyless\n`<div data-lexical-slot=\"<name>\">` container parked slots-first in the host\nDOM as a **hidden placeholder** (`display: none`), in a canonical order\nderived from the host class (see [slot order](#slot-order)). Nothing is\nvisible until the host explicitly attaches the container somewhere (see\n[Rendering](#rendering)) — mirroring how\n[`getDOMSlot`](/docs/api/modules/lexical#getdomslot) gives an element\ncontrol over where its linked-list children render.\n\n## Hosts and Values\n\nAny ElementNode or DecoratorNode can host slots. An ElementNode host can mix\nslots with ordinary children (a Card with a `title` slot plus regular body\nparagraphs); a DecoratorNode host has no children channel, so slots are its\nonly editable content (a PullQuote that is otherwise atomic).\n\nA slot value is any non-inline ElementNode or DecoratorNode. The value's\nshape decides the editing model:\n\n- **A bare block** (for example a ParagraphNode) behaves as a single-line\n  field with no extra wrapper in the model, the JSON, or the DOM: its virtual\n  scope holds exactly one block, so Enter is a no-op (hosts may map it to\n  focus movement) and a multi-block paste flattens to inline content the way\n  an `<input>` sanitizes its value — line breaks stripped, block-only\n  decorators dropped.\n- **A shadow-root container** (an ElementNode whose\n  [`isShadowRoot()`](/docs/api/modules/lexical#isshadowroot) returns\n  `true`) behaves as a multi-block region with normal block editing inside.\n\n## Declaring and Setting Slots\n\nA host class declares its slot names in [`$config()`](nodes.mdx); values are\nattached with [`$setSlot`](/docs/api/modules/lexical#setslot):\n\n```ts\nimport {\n  $create,\n  $createParagraphNode,\n  $setSlot,\n  ElementNode,\n} from 'lexical';\n\nclass CardNode extends ElementNode {\n  $config() {\n    return this.config('card', {extends: ElementNode, slots: ['title']});\n  }\n  createDOM(): HTMLElement {\n    return document.createElement('div');\n  }\n  updateDOM(): boolean {\n    return false;\n  }\n}\n\nfunction $createCardNode(): CardNode {\n  const card = $create(CardNode);\n  // Single-line title: the bare paragraph IS the slot value. An empty\n  // paragraph is an empty field; to seed default text, append a non-empty\n  // TextNode (empty TextNodes are eliminated during reconciliation).\n  $setSlot(card, 'title', $createParagraphNode());\n  // Ordinary body child, edited like any other block.\n  return card.append($createParagraphNode());\n}\n```\n\nFor a multi-block region, use a shadow-root container as the value:\n\n```ts\nclass SlotContainerNode extends ElementNode {\n  $config() {\n    return this.config('slot-container', {extends: ElementNode});\n  }\n  createDOM(): HTMLElement {\n    return document.createElement('div');\n  }\n  updateDOM(): boolean {\n    return false;\n  }\n  isShadowRoot(): boolean {\n    return true;\n  }\n}\n\n$setSlot(\n  pullQuote,\n  'quote',\n  $create(SlotContainerNode).append(\n    $createParagraphNode().append($createTextNode('First block')),\n    $createParagraphNode().append($createTextNode('Second block')),\n  ),\n);\n```\n\nThe core API surface (all exported from `lexical`):\n\n- [`$setSlot(host, name, node)`](/docs/api/modules/lexical#setslot) — place a value into a named slot, replacing\n  any existing value under that name. Move semantics, mirroring `append`:\n  the value is detached from any current parent or slot first. Throws if the\n  value is inline or if the name is one of the reserved prototype keys\n  (`__proto__`, `constructor`, `prototype`). Slotting a node into its own\n  descendant would form a cycle; this is checked (and throws) in development\n  only — in production it behaves like appending an ancestor through the\n  children channel, which is likewise unguarded.\n- [`$getSlot(host, name)`](/docs/api/modules/lexical#getslot) — the value under a name, or `null`.\n- [`$getSlotNames(host)`](/docs/api/modules/lexical#getslotnames) — the host's slot names in canonical order.\n- [`$removeSlot(host, name)`](/docs/api/modules/lexical#removeslot) — detach the value under a name (the subtree is\n  garbage-collected unless you reattach it elsewhere).\n- [`$getSlotHost(node)`](/docs/api/modules/lexical#getslothost) — the host a value is slotted into, or `null`.\n- [`$getSlotNameWithinHost(node)`](/docs/api/modules/lexical#getslotnamewithinhost) — the name of the slot\n  a value occupies on its host (the reverse direction of `$getSlotHost`), or\n  `null` when the node is not a slot value.\n- [`$getSlotFrame(node)`](/docs/api/modules/lexical#getslotframe) — the innermost slot value containing a node (the\n  \"frame\" whose virtual shadow root scopes editing), or `null` when the node\n  is not inside any slot.\n- [`$getSelectionSlotFrame(selection)`](/docs/api/modules/lexical#getselectionslotframe) — the slot frame a\n  selection lives in, or `null` when it is outside any slot. Selection-driven\n  exporters walk this frame instead of the root's children, because a\n  selection inside a slot never contains its host and a root walk would\n  export nothing. Works for every selection type, not just `RangeSelection`.\n- [`$isSlotHost(node)`](/docs/api/modules/lexical#isslothost) /\n  [`$isSlotChild(node)`](/docs/api/modules/lexical#isslotchild) — type guards\n  for the [`SlotHostNode`](/docs/api/modules/lexical#slothostnode) /\n  [`SlotChildNode`](/docs/api/modules/lexical#slotchildnode) interfaces.\n\n## Slot Order\n\nSlot order is canonical and derived, never stored. Names declared in\n`$config()` (`slots: ['quote', 'attribution']`) sort first in declaration\norder; undeclared names sort after them, lexicographically by UTF-16 code\nunit (plain JavaScript string comparison, locale-independent). `$setSlot`\nre-canonicalizes on every write, so documents normalize on load and\nconcurrent collaborative additions converge to the same order on every\nclient. If presentation order matters, declare the names.\n\n## Rendering\n\nThe reconciler always renders every slot subtree synchronously, but into a\nhidden placeholder container — visibility is the host's explicit decision.\nThere are three ways to attach a slot, all sharing the same contract\n(attaching moves the container to the target, a no-op when it is already\nthere, and reveals it; the container renders as a normal block):\n\n1. **Synchronously in-lexical**: register a `$getSlotTargetElement`\n   [`DOMRenderMatch`](/docs/api/modules/lexical_html#domrendermatch)\n   override for the host's node class (a\n   [DOM render override](../serialization/dom-render.md), not a node\n   method — this is an advanced hook). The reconciler consults it whenever\n   it creates or reconciles the slot's container and attaches/reveals\n   within the same commit — no listener or framework hop. Returning\n   `hostDom` reveals the slot in its default slots-first position:\n\n   ```ts\n   import {domOverride, DOMRenderExtension} from '@lexical/html';\n   import {configExtension, defineExtension} from 'lexical';\n\n   export const CardExtension = defineExtension({\n     dependencies: [\n       configExtension(DOMRenderExtension, {\n         overrides: [\n           domOverride([CardNode], {\n             // Reveal the title in its default slots-first position within\n             // the same commit that renders it. Returning an element from\n             // deeper inside the host's DOM attaches it there instead;\n             // $next() defers to lower-priority overrides (default: null,\n             // a hidden placeholder).\n             $getSlotTargetElement: (node, slotName, hostDom, $next, editor) =>\n               hostDom,\n           }),\n         ],\n       }),\n     ],\n     name: 'card',\n     nodes: [CardNode],\n   });\n   ```\n\n2. **Imperatively**:\n   [`mountSlotContainer(editor, nodeKey, slotName, target)`](/docs/api/modules/lexical#mountslotcontainer)\n   and\n   [`unmountSlotContainer(editor, nodeKey, container)`](/docs/api/modules/lexical#unmountslotcontainer)\n   from `lexical` are the framework-independent primitives (e.g. from a\n   [mutation listener](/docs/api/modules/lexical#registermutationlistener),\n   which fires after the commit). `mountSlotContainer` resolves the container\n   against the committed editor state\n   ([`editor.getEditorState()`](/docs/api/modules/lexical#geteditorstate)), so\n   the model it reads matches the reconciled DOM it reveals;\n   `unmountSlotContainer` takes the container you already hold and only touches\n   the DOM:\n\n   ```ts\n   import {mountSlotContainer} from 'lexical';\n\n   // e.g. inside an extension's register(editor):\n   const unregister = editor.registerMutationListener(\n     CardNode,\n     (mutations) => {\n       for (const [nodeKey, mutation] of mutations) {\n         if (mutation === 'destroyed') {\n           continue;\n         }\n         const hostDom = editor.getElementByKey(nodeKey);\n         if (hostDom !== null) {\n           // Mounting in place: the placeholder is already parked in the\n           // host DOM, so this just reveals it in its slots-first position.\n           // Any element within the host's DOM works as a target.\n           mountSlotContainer(editor, nodeKey, 'title', hostDom);\n         }\n       }\n     },\n     {skipInitialization: false},\n   );\n   ```\n\n   `unmountSlotContainer(editor, nodeKey, container)` is the reverse: it\n   hides the container and parks it back in the host DOM, for when a mount\n   target goes away while the host remains.\n\n3. **From React chrome**: the\n   [`useLexicalSlotRef`](/docs/api/modules/lexical_react_useLexicalSlotRef#uselexicalslotref)\n   hook from `@lexical/react/useLexicalSlotRef` wraps the imperative pair and returns a\n   ref that mounts a slot's container into your component — the usual choice\n   for a DecoratorNode host's `decorate()` chrome (whose containers are\n   opted back into `contentEditable` automatically, since the decorator DOM\n   is non-editable):\n\n```tsx\nimport {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';\nimport {useLexicalSlotRef} from '@lexical/react/useLexicalSlotRef';\n\nfunction PullQuoteComponent({nodeKey}: {nodeKey: NodeKey}) {\n  const [editor] = useLexicalComposerContext();\n  const quoteRef = useLexicalSlotRef<HTMLDivElement>(editor, nodeKey, 'quote');\n  const attributionRef = useLexicalSlotRef<HTMLDivElement>(\n    editor,\n    nodeKey,\n    'attribution',\n  );\n  return (\n    <blockquote>\n      <div ref={quoteRef} />\n      <div ref={attributionRef} />\n    </blockquote>\n  );\n}\n```\n\nA `contentEditable=false` ElementNode shell can host React chrome the same\nway (slot containers opt into editing whenever the host DOM is\nnon-editable): the playground's Review demo portals chrome into the host DOM,\nattaches the author slot with `useLexicalSlotRef`, drives an interactive\nstar-rating widget that persists to NodeState, and applies the identical\nhidden-then-attach technique to its `getDOMSlot` children element. Such a shell should call\n[`setDOMUnmanaged(dom)`](/docs/api/modules/lexical#setdomunmanaged) in\n`createDOM` — the portal and the\nattach moves mutate the shell's children from outside the reconciler, and\nthe marker gives the shell the same mutation-observer exemption a\nDecoratorNode's DOM has.\n\n## Editable state\n\nA slot rendered inside a non-editable host (a DecoratorNode, or a\n`contentEditable=false` element shell) would not track the editor's editable\nstate on its own, so the reconciler gives its container an explicit\n`contentEditable` that follows `editor.isEditable()`, and re-renders the islands\nwhenever `setEditable` toggles — so a read-only editor's slots are not left\neditable. No extension is required. Slots always follow the editor; there is\ncurrently no way for a slot to override its own editable state.\n\nA host that attaches its own editable island which is *not* a slot container —\nfor example the Review demo's `getDOMSlot` children element inside its\n`contentEditable=false` shell — opts it into the same behavior with\n[`$markSlotEditable(element, editor)`](/docs/api/modules/lexical#marksloteditable),\nre-applying it from `updateDOM` so an editable toggle reaches the island.\n\n## Editing Behavior\n\n- **Selection never crosses a slot boundary.** Selections are clamped to the\n  anchor's slot frame at every entry point (DOM resolution,\n  [`$setSelection`](/docs/api/modules/lexical#setselection),\n  and point mutation), so a mouse drag and a `shift+arrow` across the\n  boundary land on the same clamped result. A focus in a slot of a host is\n  moved past the whole host, or to the edge of the host's own children when\n  the selection starts inside the host.\n- **Deletion stops at the boundary.** Backspace at the start of a slot and\n  forward-Delete at its end are no-ops instead of merging across the virtual\n  shadow root.\n- **`Cmd+A` scopes to the slot frame** when the caret is inside one; outside\n  slots the default handlers keep the legacy whole-document behavior.\n  Progressive expansion (block → enclosing slot frame → document on repeated\n  presses) is provided by the opt-in\n  [`SelectBlockExtension`](/docs/api/modules/lexical_extension#selectblockextension)\n  from `@lexical/extension`.\n- **Generic block conversion skips slot values.** A slot value has no\n  parent — its up-link is `$getSlotHost` — so selection-driven block\n  converters (`$setBlocksType`, `$wrapNodes`, `$insertList` / `$removeList`,\n  the markdown block shortcuts) treat it as ineligible and do nothing rather\n  than replace it: the slot assignment is managed by the node or extension\n  that owns the slot. Calling `LexicalNode.replace` directly on a slot value\n  throws; reassign the slot with `$setSlot` on its host instead.\n- **Slots stay bound to their host through `replace`.** Replacing a slot\n  *host* (`host.replace(other)`) does not transfer its slots to the\n  replacement — slots are not necessarily portable across node types, so the\n  slot map travels with the node, never with its position. If the replaced\n  host is reattached in the same update (the\n  [`$wrapNodeInElement`](/docs/api/modules/lexical_utils#wrapnodeinelement)\n  pattern) it keeps its slots; if it stays detached, its slot subtrees are\n  garbage-collected with it. The same applies to anything built on `replace`:\n  converting a slot host with `$setBlocksType` produces a slot-less\n  replacement block. Move a slot onto another host explicitly with\n  `$setSlot`.\n- **A NodeSelection of an element carries its children.** Copy and export\n  of a whole-host NodeSelection (e.g. a chrome click that selects \"the whole\n  Card\") include the host's body children even though they aren't in the\n  selection themselves — the old shell-only output made cut silently lossy.\n  This applies only to NodeSelection; a partial RangeSelection over the host\n  keeps per-child slicing.\n\n### Traversal is intentionally asymmetric\n\nContent reads include slot subtrees, slots-first:\n[`getTextContent()`](/docs/api/modules/lexical#gettextcontent),\n[`getAllTextNodes()`](/docs/api/modules/lexical#getalltextnodes),\nand the [`$dfsWithSlots`](/docs/api/modules/lexical_utils#dfswithslots)\nfamily in `@lexical/utils` count slot content for search, copy, and\naccessibility. Navigation excludes them:\n[`getChildren()`](/docs/api/modules/lexical#getchildren),\n[`getFirstDescendant()`](/docs/api/modules/lexical#getfirstdescendant)\nand friends stay linked-list-only, so caret movement never walks into a slot\nby accident. Choose [`$dfs`](/docs/api/modules/lexical_utils#dfs) or\n[`$dfsWithSlots`](/docs/api/modules/lexical_utils#dfswithslots) depending on whether \"this subtree\" should mean the\nnavigable tree or all content.\n\n## Serialization\n\nJSON serialization is automatic in both directions. A host's slots serialize\nunder the reserved `$slots` key on\n[`SerializedLexicalNode`](/docs/api/modules/lexical#serializedlexicalnode)\n(a sibling of\nNodeState's reserved `'$'` key), keyed by slot name:\n\n```json\n{\n  \"type\": \"card\",\n  \"version\": 1,\n  \"$slots\": {\n    \"title\": {\"type\": \"paragraph\", \"children\": [], \"version\": 1}\n  },\n  \"children\": []\n}\n```\n\nParsing re-attaches each subtree with `$setSlot` and throws if `$slots`\nappears on a node that cannot host slots. The `$` prefix keeps the\nframework-owned key from colliding with a subclass that already serializes a\n`slots` property of its own.\n\nHTML serialization is opt-in per host, like NodeState: the exporter never\ndescends into slots on its own. A host's `exportDOM` can emit each slot into\na wrapper using\n[`$appendNodeToHTML`](/docs/api/modules/lexical_html#appendnodetohtml) from\n`@lexical/html`, and a\n[DOM import rule](../serialization/dom-import.md) on the host's\ndistinguishing markup maps the wrappers back through `$setSlot`.\n\n## Collaboration\n\nSlots sync on both the V1 and V2 Yjs bindings through a per-slot-diffed\n`Y.Map` stored under the reserved `__slots` attribute key on the host's shared\ntype (the channel reuses the host's `__slots` field name, which is already\nexcluded from the property sync). Hosts with declared slots create that map\neagerly, so two clients\nconcurrently setting *different* slot names for the first time merge\nper-entry instead of racing. Malformed or hostile remote entries are\nvalidated and skipped.\n\n:::caution\n\nPre-slots clients receiving slot data from upgraded peers will error rather\nthan render it. Enabling slots in long-lived shared documents should be\ngated on all participants running a slots-aware version; new clients\ntolerate unknown slot data going forward.\n\n:::\n\n## Reserved Names\n\nAdding slots reserves a few identifiers that custom node subclasses should\nnot define for their own purposes:\n\n- the `__slots` and `__slotHost` fields on ElementNode / DecoratorNode — the\n  `__slots` name is also the collab attribute key for the slots channel, so it\n  is reserved on the Yjs shared type too;\n- the `$slots` serialized JSON key.\n\n## Current Limitations\n\n- The caret / NodeCaret APIs throw across a slot boundary (\"no common\n  ancestor\"); slot-aware caret traversal is a planned follow-up.\n- Nested slots (a slot whose host is itself slotted) are only handled one\n  level deep by the runtime selection comparator.\n- The mixed-version collab caveat above.\n","packages/lexical-website/docs/concepts/node-cloning.md":"# Node Cloning\n\nUnderstanding how to properly clone and modify nodes is crucial for working with Lexical. This guide explains the different cloning mechanisms and when to use each one.\n\n## Types of Node Cloning\n\nLexical provides several ways to clone nodes, each serving a different purpose:\n\n1. `static clone()` - Internal API for state management\n2. `$copyNode()` - Public API for creating new nodes\n3. `getWritable()` - Public API for node modifications\n\n## The `clone` Method\n\n### What is `clone`?\n\nThe `clone` method is a static method that creates the next version of a node. A node that declares [`$config()`](./nodes.mdx) has one synthesized for it and need not implement it — a hand-written `clone` is only required for a node that does not use `$config()`, or whose constructor has required arguments. Either way, it's important to understand that this is an **internal API** used by Lexical's state management system.\n\n```typescript\nclass MyCustomNode extends ElementNode {\n  static clone(node: MyCustomNode): MyCustomNode {\n    // ✅ Correct implementation\n    return new MyCustomNode(node.__someData, node.__key);\n  }\n}\n```\n\n### When is `clone` Used?\n\n1. **Internal State Management**\n   - Used by Lexical internally\n   - Part of the editor's state update system\n   - Called by `getWritable()`\n\n2. **Node Mutations**\n   ```typescript\n   // Example of a node method implementation\n   class MyCustomNode extends ElementNode {\n     setData(data: string): this {\n       const self = this.getWritable();\n       self.__data = data;\n       return self;\n     }\n   }\n\n   node.setData(\"new data\");\n   ```\n\n### When NOT to Use `clone`?\n\n```typescript\n// ❌ Never do this\nfunction $duplicateNode(node: MyCustomNode) {\n  return MyCustomNode.clone(node); // Don't call clone directly\n}\n```\n\n## The `afterCloneFrom` Method\n\n### What is `afterCloneFrom`?\n\n`afterCloneFrom(prevNode)` runs on every clone, right after it is constructed,\nand copies over whatever the constructor did not. The base implementation\ncarries the node's links to its parent and siblings and its\n[NodeState](./node-state.md), and `ElementNode` and `TextNode` carry their own\nbuilt-in properties such as format, style, and indent. A property that is\ncopied neither by the constructor nor by some `afterCloneFrom` silently\nreverts to its default on the node's next write.\n\n### When you don't need it\n\nWith the modern APIs you usually don't write one at all:\n\n- **Serialization schema.** A property declared as a field in a `$config`\n  [`json` schema](./nodes.mdx#creating-custom-nodes-with-a-serialization-schema)\n  (`withField(stringValue(), {field: '__label'})`) is carried across the\n  clone for you, because the schema says where it is stored. Available in\n  Lexical v0.51.0 and later.\n- **NodeState.** State created with `createState` and stored with `$setState`\n  is carried by the base `afterCloneFrom`, so a node whose extra data lives\n  only in NodeState needs nothing.\n\n```ts\n// At module scope, above the class, so TypeScript checks it against the class\nconst calloutSchema = nodeSchema<CalloutNode>()({\n  label: withField(stringValue(), {field: '__label'}),\n});\n\nclass CalloutNode extends ElementNode {\n  __label: string = '';\n\n  // No afterCloneFrom: `__label` is declared as a field in calloutSchema, so\n  // it is carried across every clone.\n  $config() {\n    return this.config('callout', {extends: ElementNode, json: calloutSchema});\n  }\n}\n```\n\n### When you still need it\n\nOverride `afterCloneFrom` for a property that neither of those covers: a\nfield that isn't in the node's schema, one declared only through accessor\nmethods (see\n[Carrying properties across a clone](../serialization/serialization.md#carrying-properties-across-a-clone)),\nor any property of a node written without a schema. Always call\n`super.afterCloneFrom(prevNode)` first so the inherited properties and\nNodeState are still carried:\n\n```ts\nclass MyCustomNode extends ElementNode {\n  __data: string = '';\n\n  afterCloneFrom(prevNode: this): void {\n    super.afterCloneFrom(prevNode);\n    this.__data = prevNode.__data;\n  }\n}\n```\n\nLike `clone`, `afterCloneFrom` is called by Lexical; don't call it yourself\nexcept through `super`.\n\n## Using `$copyNode`\n\n### What is `$copyNode`?\n\n`$copyNode` is the public API for creating a copy of a node with a new key. Use this when you need to create a duplicate node.\nBy default, all properties and `NodeState` will be copied to the new node, and then `resetOnCopyNodeFrom` will be called to\nallow the node to optionally reset certain properties (and NodeState configured with `resetOnCopyNode: true`) to defaults\n(such as the checked state of a `ListItemNode`).\n\n```typescript\n// ✅ Correct: Using $copyNode\nconst copy = $copyNode(existingNode);\n```\n\n### When to Use `$copyNode`?\n\n1. **Creating Duplicates**\n   ```typescript\n   // ✅ Correct: Duplicating a node\n   const duplicate = $copyNode(originalNode);\n   someParent.append(duplicate);\n   ```\n\n## Using `getWritable`\n\n### What is `getWritable`?\n\n`getWritable` is an internal API used within node method implementations to get a mutable version of a node. Node consumers should use the node's public methods instead.\n\n```typescript\n// ✅ Correct: Implementation of a node method\nclass MyCustomNode extends ElementNode {\n  setData(data: string): this {\n    const self = this.getWritable();\n    self.__data = data;\n    return self;\n  }\n}\n\n// ✅ Correct: Using the node\nconst node = new MyCustomNode();\nnode.setData(\"new value\");\n```\n\n## Common Patterns\n\n### Modifying Nodes\n\n```typescript\n// ✅ Correct: Modifying a node\nfunction $updateNodeData(node: MyCustomNode, newData: string): MyCustomNode {\n  return node.setData(newData);\n}\n\n// ❌ Incorrect: Don't clone manually\nfunction $updateNodeDataWrong(node: MyCustomNode, newData: string): MyCustomNode {\n  const clone = MyCustomNode.clone(node);\n  clone.setData(newData);\n  return clone;\n}\n```\n\n### Copying Nodes\n\n```typescript\n// ✅ Correct: Copying a node\nfunction $duplicateNode(node: MyCustomNode): MyCustomNode {\n  return $copyNode(node);\n}\n\n// ❌ Incorrect: Don't use clone\nfunction $duplicateNodeWrong(node: MyCustomNode): MyCustomNode {\n  return node.constructor.clone(node);\n}\n```\n\n## Performance Considerations\n\n1. **Referential Integrity**\n   - `getWritable()` ensures proper EditorState updates with new clones\n   - Prevents \"orphaned\" nodes that won't be rendered\n\n2. **State Management**\n   ```typescript\n   class MyCustomNode extends ElementNode {\n     setData(data: string): this {\n       const self = this.getWritable();\n       self.__data = data; // Properly tracked by editor\n       return self;\n     }\n   }\n   ```\n\n## Testing\n\n```typescript\ntest('node modification', async () => {\n  await editor.update(() => {\n    const node = new MyCustomNode(\"test\");\n    \n    node.setData(\"new data\");\n    \n    // ✅ Correct: Use $copyNode for duplication\n    const copy = $copyNode(node);\n  });\n});\n```\n\n## Related Concepts\n\n- [Editor State](editor-state.md) - How cloning affects editor state\n- [Nodes](nodes.mdx) - Core concepts about Lexical nodes\n\n## Common Questions\n\n**Q: How do I duplicate a node?**\nA: Use `$copyNode(node)` to create a new copy with a new key.\n\n**Q: When should I use clone?**\nA: Never directly. Use `$copyNode()` or `getWritable()` instead. ","packages/lexical-website/docs/concepts/node-replacement.md":"# Node Replacement\n\nNode Replacement allow you to replace all instances of a given node in your editor with instances of a subclass.\n\n## Use Case\n\n:::note\n\nIn earlier versions of this documentation, \"Node Replacement\" was called \"Node Overrides\".\nWe've changed the name to match the terms used in the implementation.\n\n:::\n\n:::tip\n\nIf your use case only requires adding ad-hoc data to existing nodes, you may be able to use the [NodeState](node-state.md) API instead of subclassing and node replacement.\n\n:::\n\nSome of the most commonly used Lexical Nodes are owned and maintained by the core library. For example, ParagraphNode, HeadingNode, QuoteNode, List(Item)Node etc - these are all provided by Lexical packages, which provides an easier out-of-the-box experience for some editor features, but makes it difficult to override their behavior. For instance, if you wanted to change the behavior of ListNode, you would typically extend the class and override the methods. However, how would you tell Lexical to use *your* ListNode subclass in `ListExtension` instead of using the core ListNode? That's where Node Replacement can help.\n\n\nNode Replacement allow you to replace all instances of a given node in your editor with instances of a different node class. This can be done through the `nodes` of an extension:\n\n```ts\nconst CustomParagraphExtension = defineExtension({\n  name: '@my-app/CustomParagraph',\n  nodes: () => [\n    // Don't forget to register your custom node separately!\n    CustomParagraphNode,\n    {\n      replace: ParagraphNode,\n      with: (node: ParagraphNode) => {\n        return $createCustomParagraphNode();\n      },\n      withKlass: CustomParagraphNode,\n    },\n  ],\n});\n```\nIn the snippet above,\n- `replace`: Specifies the core node type to be replaced. \n- `with`: Defines a transformation function to replace instances of the original node to the custom node.  \n- `withKlass`: This option ensures that behaviors associated with the original node type work seamlessly with the replacement. For instance, node transforms or mutation listeners targeting ParagraphNode will also apply to CustomParagraphNode when withKlass is specified. Without this option, the custom node might not fully integrate with the editor's built-in features, leading to unexpected behavior.\n\nOnce this is done, Lexical will replace all ParagraphNode instances with CustomParagraphNode instances. One important use case for this feature is overriding the serialization behavior of core nodes. Check out the full example below.\n\n## Node Replacement Example\n\nThis example demonstrates using Node Replacement to replace all `ParagraphNode` with a `CustomParagraphNode` that overrides `createDOM`.\n\n[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/facebook/lexical/tree/main/examples/node-replacement?file=src/main.tsx)\n\n<iframe width=\"100%\" height=\"600\" src=\"https://stackblitz.com/github/facebook/lexical/tree/main/examples/node-replacement?embed=1&file=src%2FApp.tsx&terminalHeight=0&ctl=1&showSidebar=0&devtoolsheight=0&view=preview\" sandbox=\"allow-forms allow-modals allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin allow-scripts\" title=\"Node Replacement Example\"></iframe>\n","packages/lexical-website/docs/concepts/node-state.md":"# NodeState\n\nThe NodeState API introduced in v0.26.0 allows arbitrary state to be added\nad-hoc to any node in a way that participates with reconciliation, history,\nand JSON serialization.\n\n## Use Case\n\nNodeState allows your application to define keys that can be stored on\nany node with automatic JSON support.\n\n:::tip\n\nYou can even add node state to the RootNode to store document-level metadata.\n\n:::\n\nWith a combination of NodeState and other APIs such as\n[Listeners](listeners.md) or\n[Transforms](transforms.md) you can\nlikely shape the editor to meet your needs without having to do much\n[Node Customization](node-replacement.md).\n\nEven when you are subclassing nodes, using NodeState instead of additional\nproperties to store the node's data can be [more efficient](#efficiency)\nand will save you from writing a lot of boilerplate in the constructor,\nupdateFromJSON, and exportJSON.\n\n## Usage\n\n### createState\n\n[createState](/docs/api/modules/lexical#createstate)\ncreates a\n[StateConfig](/docs/api/modules/lexical#stateconfig)\nwhich defines the key and configuration for your NodeState value.\n\nThe key must be locally unique, two distinct StateConfig must not have the\nsame string key if they are to be used on the same node.\n\nTypical usage will look something like this:\n\n```ts\nconst questionState = createState('question', {\n  parse: (v) => (typeof v === 'string' ? v : ''),\n});\n```\n\nThe required `parse` function serves two purposes:\n- It provides a type-safe and runtime-safe way to parse values that were\n  serialized to JSON\n- When called with `undefined` (or any invalid value) it should return some\n  default value (which may be `undefined` or `null` or any other value you\n  choose)\n\nIn this case, the question must be a string, and the default is an\nempty string.\n\nSee the\n[createState](/docs/api/modules/lexical#createstate)\nAPI documentation for more details, there are other optional settings\nthat you may want to define particularly if the value is not a primitive\nvalue such as boolean, string, number, null, or undefined.\n\n:::tip\n\nInstead of writing parse functions by hand, use the\n[serialization schema combinators](/docs/serialization/#declarative-serialization-schemas-with-config)\nfrom `lexical`, such as `stringValue`, `numberValue`, `enumValue`,\n`arrayValue`, `objectValue`, and `nullable`. They are the same ones a node's\n`$config` JSON schema uses. A schema is itself a parse function, so it can be\npassed directly as `parse`:\n\n```ts\nimport {arrayValue, createState, stringValue} from 'lexical';\n\nconst questionState = createState('question', {parse: stringValue()});\nconst tagsState = createState('tags', {parse: arrayValue(stringValue())});\n```\n\nBecause `parse` is just a function, the combinators also compose with schema\nlibraries such as [zod](https://zod.dev/), [ArkType](https://arktype.io/),\n[Effect](https://effect.website/), and [Valibot](https://valibot.dev/). Wrap a\nlibrary's validator in a parse function that returns a default for invalid\ninput, or use `transformValue` to refine what a combinator parsed.\n\n:::\n\n### $getState\n\n[$getState](/docs/api/modules/lexical#getstate) gets the\nNodeState value from the given node, or the default if that key was never\nset on the node.\n\n```ts\nconst question = $getState(pollNode, questionState);\n```\n\nSee also\n[$getStateChange](/docs/api/modules/lexical#getstatechange)\nif you need an efficient way to determine if the state has changed on two\nversions of the same node (typically used in updateDOM, but may be useful in\nan update listener or mutation listener).\n\n### $setState\n\n[$setState](/docs/api/modules/lexical#setstate) sets the\nNodeState value on the given node.\n\n```ts\nconst question = $setState(\n  pollNode,\n  questionState,\n  'Are you planning to use NodeState?',\n);\n```\n\n:::tip\n\nThe last argument is a ValueOrUpdater, just like with React's useState\nsetters. If you use an updater function and the value does not change,\nthe node and its NodeState *won't* be marked dirty.\n\n:::\n\n## Serialization\n\nThe NodeState for a node, if any values are set to non-default values, is\nserialized to a record under a single\n[NODE_STATE_KEY](/docs/api/modules/lexical#node_state_key)\nwhich is equal to `'$'`.\n\n```json\n{\n  \"type\": \"poll\",\n  \"$\": {\n    \"question\": \"Are you planning to use NodeState?\"\n  }\n}\n```\n\n:::tip\n\nBy default, it is assumed that your parsed values are JSON serializable,\nbut for advanced use cases you may use values such as Date, Map, or Set\nthat need to be transformed before JSON serialization. See the\n[StateValueConfig](/docs/api/modules/lexical#statevalueconfig)\nAPI documentation.\n\n:::\n\n### Flat serialization with `$config`\n\nNodes that declare a `StateConfig` in [`$config`](nodes.mdx#creating-custom-nodes-with-config-and-nodestate)\nwith `flat: true` lift that key to the top level of the serialized node\ninstead of nesting it under `'$'`. This makes the JSON shape identical\nto a legacy node that stored the value as a `__property` instance\nvariable, so existing payloads continue to round-trip after the node is\nmigrated to NodeState.\n\nFor example, a `ColoredNode` that extends `TextNode` and declares a flat\n`color` state (see [Extending TextNode with `$config`](nodes.mdx#extending-textnode-with-config)):\n\n```ts\n$config() {\n  return this.config('colored', {\n    extends: TextNode,\n    stateConfigs: [{flat: true, stateConfig: colorState}],\n  });\n}\n```\n\nserializes as:\n\n```json\n{\n  \"type\": \"colored\",\n  \"text\": \"hello\",\n  \"color\": \"red\"\n}\n```\n\nA non-flat (default) state on the same node would instead appear under\n`'$'`:\n\n```json\n{\n  \"type\": \"colored\",\n  \"text\": \"hello\",\n  \"$\": {\n    \"color\": \"red\"\n  }\n}\n```\n\nIn both cases, a key is only emitted when the current value is not equal\nto the default returned by its `parse` function — see\n[Efficiency](#efficiency).\n\n:::note\n\nDon't reuse a flat key that a superclass already serializes (e.g. `text`\non `TextNode`).\n\n:::\n\n### Upgrading a legacy JSON property to NodeState\n\nA node that stores data as a `__property` instance variable with custom\n`exportJSON`/`importJSON`/`updateFromJSON` can be migrated to NodeState\nwith `flat: true` without changing its serialized JSON shape.\n\nBefore — `ColoredNode` with a `__color` property and hand-written\nserialization (see the full legacy example in\n[Extending TextNode with `$config`](nodes.mdx#extending-textnode-with-config)):\n\n```ts\nexport type SerializedColoredNode = Spread<\n  {color?: string},\n  SerializedTextNode\n>;\n\nexport class ColoredNode extends TextNode {\n  __color: string;\n\n  constructor(text: string = '', color: string = DEFAULT_COLOR, key?: NodeKey) {\n    super(text, key);\n    this.__color = color;\n  }\n\n  static getType(): string {\n    return 'colored';\n  }\n\n  static clone(node: ColoredNode): ColoredNode {\n    return new ColoredNode(node.__text, node.__color, node.__key);\n  }\n\n  static importJSON(serializedNode: SerializedColoredNode) {\n    return new ColoredNode().updateFromJSON(serializedNode);\n  }\n\n  updateFromJSON(serializedNode: SerializedColoredNode) {\n    const self = super.updateFromJSON(serializedNode);\n    self.__color =\n      typeof serializedNode.color === 'string'\n        ? serializedNode.color\n        : DEFAULT_COLOR;\n    return self;\n  }\n\n  exportJSON(): SerializedColoredNode {\n    return {\n      ...super.exportJSON(),\n      color: this.__color === DEFAULT_COLOR ? undefined : this.__color,\n    };\n  }\n}\n```\n\nAfter — same on-the-wire JSON, no hand-written serialization:\n\n```ts\nconst colorState = createState('color', {\n  parse: (v) => (typeof v === 'string' ? v : DEFAULT_COLOR),\n});\n\nexport class ColoredNode extends TextNode {\n  $config() {\n    return this.config('colored', {\n      extends: TextNode,\n      stateConfigs: [{flat: true, stateConfig: colorState}],\n    });\n  }\n}\n```\n\nNo `exportJSON`, `importJSON`, `updateFromJSON`, `clone`, or\n`afterCloneFrom` override is needed: `$config` installs `clone` and\n`importJSON`, and the base `exportJSON`/`updateFromJSON`/`afterCloneFrom`\non `LexicalNode`/`TextNode` already round-trip NodeState. Read the color\nvia `$getState(node, colorState)` and write it via\n`$setState(node, colorState, value)` instead of `node.__color`.\n\n:::tip\n\nFor a seamless migration, keep the JSON key name the same — for a flat\nstate, that's the first argument to `createState`. Migrating from a\nnon-flat NodeState to a flat NodeState also works: state that appears\nunder the `$` (`NODE_STATE_KEY`) is still parsed even when the state is\nconfigured as flat, and the flat value takes precedence when both are\npresent.\n\n:::\n\n## Efficiency\n\nNodeState uses a copy-on-write scheme to manage each node's state. If\nnone of the state has changed, then the NodeState instance will be\nshared across multiple versions of that node.\n\n:::info\n\nIn a given reconciliation cycle, the first time a Lexical node is marked dirty\nvia `getWritable` will create a new instance of that node. All properties\nof the previous version are set on the new instance. NodeState is stored\nas a single property, and no copying of the internal state is done\nuntil the NodeState itself is marked writable.\n\n:::\n\nWhen serializing to JSON, each key will only be stored if the value\nis not equal to the default value. This can save quite a lot of space\nand bandwidth.\n\nParsing and serialization is only done at network boundaries, when\nintegrating with JSON or Yjs. When a value changes from an external\nsource, it is only parsed once the first time it is read.\nValues that do not come from external sources are not parsed, and\nvalues that are not used are never parsed.\n\n## Capabilities\n\nCurrent:\n\n- Allows you to define and add state to any node\n- Serializes that state automatically in the node's JSON, supporting\n  versioning and copy+paste\n- Works with the reconciler, TextNodes with differing state will not\n  be implicitly merged\n- @lexical/yjs support, NodeState will be automatically synchronized\n  like any other property\n- NodeState values that are not used will simply pass-through, making\n  it a bit easier for situations where multiple configurations are used\n  on the same data (e.g. older and newer versions of your editor,\n  a different set of plugins based on context, etc.).\n- Pre-registration system for nodes to declare expected state\n  and serialize them as top-level properties (`flat`) with `$config`\n  (see [#7260](https://github.com/facebook/lexical/issues/7260)).\n- Can be integrated with\n  [DOMRenderExtension](../serialization/dom-render.md) for editor\n  rendering and HTML export\n- Can be integrated with\n  [DOMImportExtension](../serialization/dom-import.md) for HTML import\n\nFuture:\n\n- Does not yet support direct integration with Yjs, e.g.\n  you can not store a Y.Map as a NodeState value\n  (see [#7293](https://github.com/facebook/lexical/issues/7293))\n\n## Node State Style Example\n\nThis example demonstrates an advanced use case of storing a style object on TextNode using NodeState.\n\n[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/facebook/lexical/tree/main/examples/node-state-style?file=src/main.tsx)\n\n<iframe width=\"100%\" height=\"600\" src=\"https://stackblitz.com/github/facebook/lexical/tree/main/examples/node-state-style?embed=1&file=src%2FApp.tsx&terminalHeight=0&ctl=1&showSidebar=0&devtoolsheight=0&view=preview\" sandbox=\"allow-forms allow-modals allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin allow-scripts\" title=\"Node State Style Example\"></iframe>\n","packages/lexical-website/docs/concepts/one-lexical-per-app.md":"---\nsidebar_label: One Lexical per app\n---\n\n# One Lexical per app\n\n`lexical` is effectively a singleton **within one app**: an app must resolve\nexactly **one** copy of `lexical`, and one copy of each `@lexical/*` package it\nuses. Two copies in the same bundle do not merely waste bytes — they break the\neditor in ways whose error messages point nowhere near the real cause.\n\nThe practical consequence, covered in [Publishing a library](#publishing-a-library)\nbelow: **a library built on Lexical declares `lexical` and the `@lexical/*`\npackages it imports as `peerDependencies`, never as `dependencies`. The\napplication brings its own Lexical.**\n\n:::note\n\nThis page is about npm `peerDependencies` in your `package.json`. Extensions\nhave their own, unrelated notion of\n[peer dependencies](../extensions/peer-dependencies.md) — optional links between\nextensions inside one editor.\n\n:::\n\n## The boundary is the app, not the page\n\nSeveral self-contained apps can share a page, each with its own bundled copy of\nLexical, and that is fine — a CMS page with two independently deployed widgets\non it, or a micro-frontend setup, does not have a bug just because two copies of\n`lexical` are loaded. Lexical anticipates this: `isLexicalEditor()` is an\n`instanceof` check specifically \"to prevent issues with multiple embedded\nLexical installations\", and an editor is tagged on its own root element rather\nthan in any page-global registry, so each app's editors resolve to that app's\nLexical and ignore the others.\n\nWhat those apps cannot do is interoperate. Two copies have no compatible API\nsurface between them, so treat each as a sealed island:\n\n- do not pass nodes, editors, `EditorState` objects, selections or commands\n  across the boundary — pass serialized JSON or HTML, which is data rather than\n  identity;\n- a plugin, node package or shared editor config must be bundled with the app it\n  serves, not shared as a live import between apps;\n- `@lexical/react` context does not cross the boundary either, since the context\n  object itself comes from one copy.\n\nThe rest of this page is about the failure case: two copies **inside one app**,\nwhere code from copy A is expected to work with an editor from copy B.\n\n## Why one copy per app\n\nLexical keeps state at module scope, and identifies things by reference rather\nthan by name. Both assumptions hold only within a single copy of the module.\n\n- **The active editor is module state.** `activeEditor`, `activeEditorState`\n  and `isReadOnlyMode` live at module scope in `LexicalUpdates.ts`, and every\n  `$` function reads them. `editor.update()` in copy A sets copy A's variables;\n  a `$` function imported from copy B reads copy B's, which are still `null`\n  ([Editor State](editor-state.md)).\n- **Node classes are compared by identity.** Node registration and the\n  `instanceof LexicalNode` checks compare class references. A node that extends\n  `LexicalNode` from copy A is not, as far as copy B is concerned, a\n  `LexicalNode` at all — even though both classes have the same name and come\n  from the same published version ([Nodes](nodes.mdx)).\n- **Commands are object identities.** `createCommand()` returns a fresh `{type}`\n  object, and `registerCommand`/`dispatchCommand` match on that object, not on\n  its `type` string. Two copies of `lexical` export two distinct\n  `FORMAT_TEXT_COMMAND` objects, so a dispatch from one never reaches a listener\n  registered with the other ([Commands](commands.md)).\n\nOn top of that, two copies are usually two *versions*, which is API drift inside\na single editor: half your plugins were compiled against a different API and\nserialization format than the editor running them, and the mismatch surfaces\nonly at runtime, on the paths that happen to differ.\n\n## Symptoms of a duplicated Lexical\n\nThe errors are unhelpfully literal — a class that plainly does implement\n`getType()` is reported as not implementing it, because the check is running\nagainst the other copy:\n\n```\nLexicalNode: Node FooNode does not implement .getType().\n\nCreate node: Type heading in node HeadingNode does not match registered node\nHeadingNode with the same type\n\ncreateEditor: nodes[0] FooNode (type foo) is not a constructor that subclasses\nLexicalNode from the lexical package used by this editor (0.50.0)\n\nUnable to find an active editor state. State helpers or node methods can only be\nused synchronously during the callback of editor.update(), editor.read(), or\neditorState.read(). Detected on the page: 0 compatible editor(s) with version\n0.50.0 and incompatible editors with versions 0.49.0\n```\n\nThat last sentence is Lexical telling you directly that it found editors built\nfrom another copy — a version different from its own, or the same version marked\n`(separately built, likely a bundler configuration issue)`. Note that this\ndiagnostic scans the whole page, so a genuinely separate app that ships its own\nLexical is counted there too; it is evidence of a bug only when those editors\nwere supposed to be part of *your* app.\n\nSilent failures look like: a command that dispatches but never fires its\nlistener, `$getSelection()` returning `null` inside an update, or a custom node\nrendering as plain text after a copy/paste. Real reports of this failure mode:\n[#4225](https://github.com/facebook/lexical/issues/4225) and\n[#7819](https://github.com/facebook/lexical/issues/7819).\n\n## Publishing a library\n\nIf you publish a package that imports from `lexical` or `@lexical/*` — a plugin,\na node package, a shared editor config, an in-house wrapper in a monorepo — put\nthose packages in `peerDependencies`, and in `devDependencies` so you can still\nbuild and test:\n\n```json\n{\n  \"name\": \"my-lexical-plugin\",\n  \"peerDependencies\": {\n    \"lexical\": \">=0.50.0\",\n    \"@lexical/react\": \">=0.50.0\",\n    \"@lexical/utils\": \">=0.50.0\"\n  },\n  \"devDependencies\": {\n    \"lexical\": \"0.50.0\",\n    \"@lexical/react\": \"0.50.0\",\n    \"@lexical/utils\": \"0.50.0\"\n  }\n}\n```\n\nNotes on the ranges:\n\n- Lexical is pre-1.0, so `^0.50.0` means `>=0.50.0 <0.51.0` — a caret peer range\n  locks your users out of the next release. Prefer an open `>=` range (or an\n  explicit `>=0.50.0 <1.0.0`) so an app can upgrade Lexical without waiting for\n  you.\n- Mark a peer optional with\n  [`peerDependenciesMeta`](https://docs.npmjs.com/cli/configuring-npm/package-json#peerdependenciesmeta)\n  when the import is behind an opt-in entry point, the way this repo's own\n  packages mark `typescript` optional.\n- Do not add `lexical` to `dependencies` \"just so it installs\". That is exactly\n  what pins a second copy under `node_modules/your-lib/node_modules/lexical` as\n  soon as your range and the app's range disagree.\n\n## Application setup\n\nThe application owns the version, and every Lexical package it installs moves\ntogether:\n\n- Install `lexical` and each `@lexical/*` package you use **at the same\n  version**. Published `@lexical/*` packages depend on the exact matching\n  `lexical` version (the monorepo's `workspace:*` is rewritten to an exact\n  version at publish time), so `@lexical/react@0.50.0` beside `lexical@0.49.0`\n  guarantees a nested second copy.\n- Verify there is only one **per app**, from the app root:\n\n  ```sh\n  npm ls lexical @lexical/react     # or: pnpm why lexical / yarn why lexical\n  ```\n\n  More than one resolved version, or a path with a nested `node_modules/lexical`\n  in it, is the bug.\n- In a monorepo or a linked-package setup (`npm link`, `pnpm link`, a Yalc\n  copy), the linked library resolves Lexical from *its own* `node_modules` and\n  duplicates are the default rather than the exception. Deduplicate explicitly:\n  hoist the dependency to the workspace root, or force resolution with\n  [pnpm `overrides`](https://pnpm.io/settings#overrides), Yarn `resolutions`, or\n  `npm dedupe`.\n- Bundlers can also be told to collapse duplicates — Vite\n  [`resolve.dedupe`](https://vite.dev/config/shared-options.html#resolve-dedupe),\n  webpack `resolve.alias` pointing each package at one path:\n\n  ```js\n  // vite.config.js\n  export default {\n    resolve: {dedupe: ['lexical', '@lexical/react', '@lexical/utils']},\n  };\n  ```\n\n  Treat that as a safety net, not a fix: it collapses copies that are already\n  version-compatible, and cannot rescue two genuinely different versions.\n- Duplication does not always come from the dependency tree: importing Lexical\n  through both `import` and `require` pulls in the ESM and the CJS build of the\n  *same* version, which are still two copies. The\n  [React FAQ](../react/faq.md#lexicalcomposercontextuselexicalcomposercontext-cannot-find-a-lexicalcomposercontext)\n  covers how this one shows up in `@lexical/react`.\n","packages/lexical-website/docs/concepts/read-only.md":"\n\n# Read Mode / Edit Mode\n\nLexical supports two modes:\n\n- Read mode\n- Edit mode\n\nThe default behavior for Lexical is edit mode, or more accurately not read only mode. Under-the-hood, the main\nimplementation detail is that the `contentEditable` is being set to `\"false\"` or `\"true\"` depending on the mode.\nSpecific plugins can listen to the mode change too – allowing them to customize parts of the UI depending on the\nmode.\n\n## Setting the mode\n\nIn order to set the mode, this can be done on creation of the editor, with the\n`editable` property of your app's extension:\n\n```js\nimport {buildEditorFromExtensions} from '@lexical/extension';\nimport {RichTextExtension} from '@lexical/rich-text';\nimport {defineExtension} from 'lexical';\n\nconst editor = buildEditorFromExtensions(\n  defineExtension({\n    dependencies: [RichTextExtension],\n    editable: false,\n    name: '@my-app/editor',\n  }),\n);\n```\n\nWith React, pass the same extension to `<LexicalExtensionComposer>`:\n\n```jsx\n<LexicalExtensionComposer extension={appExtension}>\n  ...\n</LexicalExtensionComposer>\n```\n\nAfter an editor is created, the mode can be changed imperatively:\n\n```js\neditor.setEditable(true);\n```\n\n## Reading the mode\n\nIn order to find the current mode of the editor you can use:\n\n```js\nconst isEditable = editor.isEditable(); // Returns true or false\n```\n\nYou can also get notified when the editor's read only mode has changed:\n\n```js\nconst removeEditableListener = editor.registerEditableListener(\n  (isEditable) => {\n    // The editor's mode is passed in!\n    console.log(isEditable);\n  },\n);\n\n// Do not forget to unregister the listener when no longer needed!\nremoveEditableListener();\n```\n\nIn an extension, depend on\n[`WatchEditableExtension`](/docs/api/modules/lexical_extension#watcheditableextension),\nwhose output is a signal holding the current editable state, instead of\nregistering the listener yourself.\n","packages/lexical-website/docs/concepts/selection.md":"# Selection\n\n## Types of selection\n\nLexical's selection is part of the `EditorState`. This means that for every update, or change to the editor, the\nselection always remains consistent with that of the `EditorState`'s node tree.\n\nIn Lexical, there are four types of selection possible:\n\n- `RangeSelection`\n- `NodeSelection`\n- `TableSelection` (implemented in `@lexical/table`)\n- `null`\n\nIt is possible, but not generally recommended, to implement your own selection types that implement `BaseSelection`.\n\n### `RangeSelection`\n\nThis is the most common type of selection, and is a normalization of the browser's DOM Selection and Range APIs.\n`RangeSelection` consists of three main properties:\n\n- `anchor` representing a `RangeSelection` point\n- `focus` representing a `RangeSelection` point\n- `format` numeric bitwise flag, representing any active text formats\n\nBoth the `anchor` and `focus` points refer to an object that represents a specific part of the editor. The main properties of a `RangeSelection` point are:\n\n- `key` representing the `NodeKey` of the selected Lexical node\n- `offset` representing the position from within its selected Lexical node. For the `text` type this is the character, and for the `element` type this is the child index from within the `ElementNode`\n- `type` representing either `element` or `text`.\n\n### `NodeSelection`\n\nNodeSelection represents a selection of multiple arbitrary nodes. For example, three images selected at the same time.\n\n- `getNodes()` returns an array containing the selected LexicalNodes\n\n### `TableSelection`\n\nTableSelection represents a grid-like selection like tables. It stores the key of the parent node where the selection takes place and the start and end points.\n`TableSelection` consists of three main properties:\n\n- `tableKey` representing the parent node key where the selection takes place\n- `anchor` representing a `TableSelection` point\n- `focus` representing a `TableSelection` point\n\nFor example, a table where you select row = 1 col = 1 to row 2 col = 2 could be stored as follows:\n- `tableKey = 2` table key\n- `anchor = 4` table cell (key may vary)\n- `focus = 10` table cell (key may vary)\n\nNote that `anchor` and `focus` points work the same way as `RangeSelection`.\n\n### `null`\n\nThis is for when the editor doesn't have any active selection. This is common for when the editor has been blurred or when selection\nhas moved to another editor on the page. This can also happen when trying to select non-editable components within the editor space.\n\n## Working with selection\n\nSelection can be found using the `$getSelection()` helper, exported from the `lexical` package. This function can be used within\nan update, a read, or a command listener.\n\n:::tip\n\nTo walk the document from a selection point — or to write a traversal that\nhandles empty nodes and collapsed selections correctly — see\n[Node Traversals with NodeCaret](./traversals.md).\n`$caretRangeFromSelection(selection)` converts a `RangeSelection` into the\ncaret range that traversal API works with, and\n`$setSelectionFromCaretRange(range)` converts back.\n\n:::\n\n```js\nimport {$getSelection, SELECTION_CHANGE_COMMAND} from 'lexical';\n\neditor.update(() => {\n  const selection = $getSelection();\n});\n\neditorState.read(() => {\n  const selection = $getSelection();\n});\n\n// SELECTION_CHANGE_COMMAND runs before reconciliation in the pending update.\neditor.registerCommand(SELECTION_CHANGE_COMMAND, () => {\n  const selection = $getSelection();\n});\n```\n\nIn some cases you might want to create a new type of selection and set the editor selection to\nbe that. This can only be done in update or command listeners.\n\n```js\nimport {$setSelection, $createRangeSelection, $createNodeSelection} from 'lexical';\n\neditor.update(() => {\n  // Set a range selection\n  const rangeSelection = $createRangeSelection();\n  $setSelection(rangeSelection);\n\n  // You can also indirectly create a range selection, by calling some of the selection\n  // methods on Lexical nodes.\n  const someNode = $getNodeByKey(someKey);\n\n  // On element nodes, this will create a RangeSelection with type \"element\",\n  // referencing an offset relating to the child within the element.\n  // On text nodes, this will create a RangeSelection with type \"text\",\n  // referencing the text character offset.\n  someNode.select();\n  someNode.selectPrevious();\n  someNode.selectNext();\n\n  // You can use this on any node.\n  someNode.selectStart();\n  someNode.selectEnd();\n\n  // Set a node selection\n  const nodeSelection = $createNodeSelection();\n  // Add a node key to the selection.\n  nodeSelection.add(someKey);\n  $setSelection(nodeSelection);\n\n  // You can also clear selection by setting it to `null`.\n  $setSelection(null);\n});\n```\n\n## Focus\n\nYou may notice that when you issue an `editor.update` or\n`editor.dispatchCommand` then the editor can \"steal focus\" if there is\na selection and the editor is editable. This is because the Lexical\nselection is reconciled to the DOM selection during reconciliation,\nand the browser's focus follows its DOM selection.\n\nIf you want to make updates or dispatch commands to the editor without\nchanging the selection, can use the `SKIP_DOM_SELECTION_TAG` update tag\n(added in v0.22.0):\n\n```js\n// Call this from an editor.update or command listener\n$addUpdateTag(SKIP_DOM_SELECTION_TAG);\n```\n\nIf you want to add this tag during processing of a `dispatchCommand`,\nyou can wrap it in an `editor.update`:\n\n```js\n// NOTE: If you are already in a command listener or editor.update,\n//       do *not* nest a second editor.update! Nested updates have\n//       confusing semantics (dispatchCommand will re-use the\n//       current update without nesting)\neditor.update(() => {\n  $addUpdateTag(SKIP_DOM_SELECTION_TAG);\n  editor.dispatchCommand(/* … */);\n});\n```\n\n`SKIP_DOM_SELECTION_TAG` does not apply to the initial editor state setup\n(the `$initialEditorState` property of the root extension, or\n`initialConfig.editorState` with the legacy `LexicalComposer`). On first mount the editor still scrolls\nto and focuses the initial selection. To prevent that, call\n`$setSelection(null)` inside your initial state setup function:\n\n```js\nconst editor = buildEditorFromExtensions({\n  name: '[root]',\n  // ...\n  $initialEditorState: () => {\n    // ... build your initial nodes ...\n    $setSelection(null);\n  },\n});\n```\n\nIf you have to support older versions of Lexical, you can mark the editor\nas not editable during the update or dispatch.\n\n```js\n// NOTE: This code should be *outside* of your update or command listener, e.g.\n//       directly in the DOM event listener\nconst prevEditable = editor.isEditable();\neditor.setEditable(false);\neditor.update(\n  () => {\n    // run your update code or editor.dispatchCommand in here\n  }, {\n    onUpdate: () => {\n      editor.setEditable(prevEditable);\n    },\n  },\n);\n```\n\n## Reading selection across a shadow boundary\n\nIf your plugin reads the DOM selection directly — through\n`Selection.anchorNode`, `Selection.getRangeAt(0)`, or similar — and the\neditor's `contentEditable` lives inside a `ShadowRoot`, the browser retargets\nthose reads to the shadow host. See\n[Shadow DOM and iframes](./shadow-dom.md) for the shadow-aware helpers\n(`getDOMSelectionPoints`, `getDOMSelectionRange`, etc.) that return the\nun-retargeted boundary points; they fall through to the standard reads in the\nplain light DOM, so there's nothing to do until you actually mount the editor\nin a shadow tree.\n\n### Selection change timing\n\n`SELECTION_CHANGE_COMMAND` runs **before reconciliation**, in a writable update,\nfor native, programmatic, and non-range selection changes. `$getSelection()`\nreturns the pending selection; `$getPreviousSelection()` returns the selection\nfrom the last committed state. A listener can normalize the selection or edit\nnodes before that same update commits. Listeners that change the selection may\ncause another notification before commit; they must converge.\n\nThis covers programmatic changes such as `node.select()`, which notify without\nwaiting for a DOM `selectionchange` event, and non-range selections such as\n`NodeSelection` and `TableSelection`, as well as native range selections.\n\nSelection listeners use the update's normal error handling. A throwing listener\nreports through `onError` and can abort the entire pending update, including the\nedit that triggered the notification and any other edits batched into that\ncommit, just like an explicit command dispatch inside that update. Because a\nprogrammatic selection change also notifies, a listener error can abort content\nedits made in the same update. Listener edits do not have a separate rollback\nscope.\n\nThe DOM is not guaranteed to match the pending state, even for a `NodeSelection`.\nNew nodes may not have an element yet, and DOM ranges, layout, and focus may\nstill reflect the previous state. Schedule DOM-dependent work after the update\nand read the reconciled state there:\n\n```ts\neditor.registerCommand(\n  SELECTION_CHANGE_COMMAND,\n  () => {\n    $onUpdate(() => {\n      editor.read('latest', () => {\n        // Read $getSelection(), look up elements, or position floating UI here.\n      });\n    });\n    return false;\n  },\n  COMMAND_PRIORITY_LOW,\n);\n```\n\nImport `$onUpdate` and `COMMAND_PRIORITY_LOW` from `lexical`. Read selection and\nnodes inside the callback rather than capturing mutable pending objects.\n`registerUpdateListener` is another option for UI that must respond to every\ncommit, including content changes that do not change the selection. Updates\nusing `SKIP_DOM_SELECTION_TAG` still deliberately leave the browser selection\nunsynchronized.\n\n#### Update tags and local edits\n\nThe notification belongs to the pending update and inherits its tags. A listener\ncan inspect them with `$hasUpdateTag`. In particular, Yjs does not sync content\nedits back to peers when the update is tagged `COLLABORATION_TAG` or\n`HISTORIC_TAG`; history also treats `HISTORIC_TAG` as replaying an existing entry.\nThis applies to content edits made by selection listeners in those updates.\n\nListeners that only apply to local editing should skip those tagged updates. If\na remote change or undo must trigger an independent local content edit, schedule\nit after the tagged commit with `$onUpdate`, then start a fresh `editor.update`:\n\n```ts\nconst $applyLocalEdit = () => {\n  // Read the current selection and nodes, then apply a convergent local edit.\n};\n\neditor.registerCommand(\n  SELECTION_CHANGE_COMMAND,\n  () => {\n    if ($hasUpdateTag(COLLABORATION_TAG) || $hasUpdateTag(HISTORIC_TAG)) {\n      $onUpdate(() => editor.update($applyLocalEdit));\n    } else {\n      $applyLocalEdit();\n    }\n    return false;\n  },\n  COMMAND_PRIORITY_LOW,\n);\n```\n\nImport the tags and `$hasUpdateTag` from `lexical`. Re-read the state in the fresh\nupdate; it has its own collaboration and undo behavior. Calling `editor.update`\ndirectly inside the command listener queues a nested update that still belongs\nto the same tagged commit.\n\nAutomatic range and cleared-selection notifications require a connected editor\nroot. Changes committed while those notifications are skipped advance the\nselection baseline and are not replayed when the root reconnects. Non-range\nselections continue to notify in unmounted and headless editors,\nwith the same pre-reconciliation timing. Native dirty-selection notifications\nmay fire even when the selection compares equal to the previous selection.\n","packages/lexical-website/docs/concepts/shadow-dom.md":"---\nsidebar_label: Shadow DOM and iframes\n---\n\n# Shadow DOM and iframes\n\nA Lexical editor works when its `contentEditable` lives inside an **open**\n[DOM `ShadowRoot`](https://developer.mozilla.org/docs/Web/API/ShadowRoot) (for\nexample inside a [web component](https://developer.mozilla.org/docs/Web/API/Web_components))\nor inside an `<iframe>` document. Both are supported out of the box — you only\nhave to point the editor at the right root element.\n\n## Embedding in a shadow root\n\nAttach an open shadow root, render a `contentEditable` inside it, and pass that\nelement to `editor.setRootElement` (or, with `@lexical/react`, portal the\n`ContentEditable` into the shadow root — React context flows across the portal,\nso the editor is built exactly as it would be in the light DOM):\n\n```js\nconst host = document.querySelector('#editor-host');\nconst shadow = host.attachShadow({mode: 'open'});\nconst contentEditable = document.createElement('div');\ncontentEditable.contentEditable = 'true';\nshadow.appendChild(contentEditable);\n\neditor.setRootElement(contentEditable);\n```\n\nLexical detects the enclosing shadow root from the root element and resolves\nselection through it. There is nothing else to configure.\n\n### Why it needs platform support\n\nInside a shadow tree the browser **retargets** the document's selection to the\nshadow host: `Selection.anchorNode`/`focusNode` and `Selection.getRangeAt`\nreport the host element, not the node the caret is actually in, and\n`document.activeElement` reports the host rather than the focused element.\nLexical works around this using only standard platform APIs:\n\n- Selection is read with\n  [`Selection.getComposedRanges`](https://developer.mozilla.org/docs/Web/API/Selection/getComposedRanges)\n  and [`Selection.direction`](https://developer.mozilla.org/docs/Web/API/Selection/direction),\n  which return the real (un‑retargeted) boundary points.\n- Focus is read with\n  [`ShadowRoot.activeElement`](https://developer.mozilla.org/docs/Web/API/DocumentOrShadowRoot/activeElement)\n  via [`Node.getRootNode`](https://developer.mozilla.org/docs/Web/API/Node/getRootNode).\n- Caret movement and word/line deletion use the native `Selection.modify`, and\n  selection is written with `Selection.setBaseAndExtent`, both of which operate\n  on shadow‑tree nodes directly.\n- Drop targets and image-drag hit‑tests resolve through\n  [`Document.caretPositionFromPoint`](https://developer.mozilla.org/docs/Web/API/Document/caretPositionFromPoint)\n  with the `shadowRoots` option so the dropped point lands on the real node\n  rather than the retargeted host.\n\n### Browser support\n\nThe shadow-aware path requires the platform APIs listed above. Lexical falls\nback to the light‑DOM reads when those APIs are missing, so editors that don't\nlive in a shadow tree keep working on any engine the rest of Lexical supports.\n\n| Platform API | Used for | Chrome / Edge | Firefox | Safari |\n| --- | --- | --- | --- | --- |\n| `Selection.getComposedRanges` | Reading the un‑retargeted boundary points | 137+ | 142+ | 17.0+ |\n| `Selection.direction` | Mapping the composed range back onto anchor/focus | 137+ | 126+ | 17.0+ |\n| `ShadowRoot.activeElement` | Resolving the focused element through the host | All modern | All modern | All modern |\n| `Document.caretPositionFromPoint({shadowRoots})` | Shadow-aware drop / drag hit-tests | 128+ | 125+ | 18.1+ |\n\nLexical also supports the legacy variadic form of `getComposedRanges` shipped by\nSafari 17–18.1, automatically choosing the dictionary or variadic call shape\nat runtime.\n\n### Closed shadow roots\n\nA closed shadow root (`{mode: 'closed'}`) hides its contents from outside code:\n`Node.getRootNode({composed: false})` returns the host on every external probe,\nand `Selection.getComposedRanges({shadowRoots})` ignores the closed root. The\neditor inside a closed shadow root cannot read its own selection through the\nhost, so closed shadow roots are **not supported**.\n\nIf you need style/markup encapsulation without selection isolation, mount the\neditor in an open shadow root with `delegatesFocus: true` instead — outside\ncode can still observe focus and selection while CSS and markup remain\nencapsulated.\n\n### Styling\n\nShadow trees do not inherit the document's stylesheets, so your editor/theme\nCSS has to live inside the shadow root. A `<style>` element appended to the\nroot is the simplest option; a\n[constructable stylesheet](https://developer.mozilla.org/docs/Web/API/CSSStyleSheet#constructor)\nadopted via `shadowRoot.adoptedStyleSheets` scales better when one stylesheet\nserves several editor instances. The playground's `ShadowDomWrapper` clones\nthe page's existing `<link>` / `<style>` nodes into the shadow root so it can\nreuse Vite's HMR-managed stylesheets. This is a property of Shadow DOM, not\nof Lexical.\n\nCSS custom properties and inherited HTML attributes (`dir`, `lang`) cross\nthe shadow boundary on their own, so a page-level rule like `:root {\n--editor-bg: #1c1d22; }` or a host attribute like `<my-editor dir=\"rtl\"\nlang=\"ko\">` propagates into the editor without further setup. User\npreference media queries (`@media (prefers-color-scheme: dark)`,\n`(prefers-reduced-motion: reduce)`, `(forced-colors: active)`) evaluate\ninside the shadow root just like outside; place them in the shadow-mounted\nstylesheet rather than the page sheet.\n\n## Embedding in an iframe\n\nAn editor whose root element belongs to an `<iframe>` document is also\nsupported. Lexical reads the editor's `window`/`document` from the root\nelement (`rootElement.ownerDocument.defaultView`), so selection and focus are\nresolved against the iframe rather than the top‑level document:\n\n```js\nconst iframeDoc = iframe.contentDocument;\nconst contentEditable = iframeDoc.querySelector('#editor');\n\n// The editor can be built in the parent frame; once its root element is set it uses\n// the iframe's own window and document for selection and focus.\neditor.setRootElement(contentEditable);\n```\n\nSelection inside an iframe is **not** retargeted (an iframe is a separate\ndocument, not a shadow boundary), so `getComposedRanges` is not involved here —\nthe iframe's own selection already reports the correct nodes.\n\n## Shadow-aware helpers\n\nIf a plugin reads the DOM selection or the focused element directly, use the\nshadow/iframe‑aware helpers exported from `lexical` instead of\n`Selection.anchorNode` / `document.activeElement`, so it keeps working in these\ncontexts. In the plain light DOM each helper degrades to the standard read\nwithout any extra cost.\n\n| Instead of | Use |\n| --- | --- |\n| `Selection.anchorNode` / `anchorOffset` / `focusNode` / `focusOffset` | `getDOMSelectionPoints(selection, rootElement)` |\n| `selection.getRangeAt(0)` | `getDOMSelectionRange(selection, rootElement)` |\n| Both shapes from one read | `getDOMSelectionRangeAndPoints(selection, rootElement)` |\n| `selection.getComposedRanges` raw call | `getComposedStaticRange(selection, rootElement)` |\n| `document.activeElement === el` | `getActiveElement(el) === el` |\n| Deepest focused element through nested shadow trees | `getActiveElementDeep(document)` |\n| `event.target` on a `composed: true` event listening above the shadow boundary | `getComposedEventTarget(event)` |\n| `node.getRootNode() instanceof ShadowRoot` (cross‑realm‑safe) | `isDOMShadowRoot(node)` |\n| Walking up to discover enclosing shadow roots | `getDOMShadowRoots(node)` |\n\nAll nine helpers are marked `@experimental` while the surface stabilizes — the\nreturn shape may change, but the behavior in the light DOM is stable.\n\n### `getDOMSelectionPoints` read semantics\n\nIn the light DOM `getDOMSelectionPoints` returns the live `Selection` itself,\nso each property read is deferred and `$updateDOMSelection` only pays for the\nsynchronous style/layout recalculation that a `Selection.anchorNode` /\n`focusNode` read triggers when it actually needs the value. Inside a shadow\ntree the return is a snapshot taken at call time, including\n`Selection.direction` so callers can branch on it explicitly. Read the four\npoints immediately after the call rather than caching the returned reference,\nor use `points === domSelection` to detect the alias path.\n\nIf a future engine ships `getComposedRanges` without `Selection.direction`\n(no current configuration matches — every engine that ships the former\nalso ships the latter), the snapshot's `direction` is `undefined` and\nanchor/focus default to the StaticRange's tree order; callers needing strict\nbackward fidelity should check `direction !== undefined` before relying on\nthe swap.\n\n## Form association\n\nA Lexical editor inside a [form-associated custom\nelement](https://developer.mozilla.org/docs/Web/API/Web_components#form-associated_custom_elements)\nparticipates in a `<form>` like any built-in form control: submission carries\nthe editor's value, form reset clears it, and validation flows through. The\nsketch:\n\n```js\nclass LexicalEditorElement extends HTMLElement {\n  static formAssociated = true;\n\n  constructor() {\n    super();\n    this.internals = this.attachInternals();\n  }\n\n  get value() {\n    return this.editor\n      ? JSON.stringify(this.editor.getEditorState().toJSON())\n      : '';\n  }\n\n  set value(serialized) {\n    if (this.editor && serialized !== '') {\n      this.editor.setEditorState(this.editor.parseEditorState(serialized));\n    }\n  }\n\n  connectedCallback() {\n    // `delegatesFocus: true` routes `host.focus()` (and implicit focus\n    // from `<label for=\"...\">`) to the first focusable element inside\n    // the shadow root — for the editor that's the contentEditable.\n    const shadow =\n      this.shadowRoot ?? this.attachShadow({delegatesFocus: true, mode: 'open'});\n    const contentEditable = document.createElement('div');\n    contentEditable.contentEditable = 'true';\n    contentEditable.tabIndex = 0;\n    shadow.appendChild(contentEditable);\n\n    const editor = buildEditorFromExtensions(\n      defineExtension({name: 'lexical-editor-element'}),\n    );\n    editor.setRootElement(contentEditable);\n    this.editor = editor;\n\n    // Seed the form value so a submit before the user types still produces\n    // a non-empty serialized state, mirroring `<input value=\"...\">`.\n    this.internals.setFormValue(this.value);\n\n    editor.registerUpdateListener(({dirtyElements, dirtyLeaves}) => {\n      // Only fire on real content changes, not on pure selection updates —\n      // matches HTMLInputElement's `input` event contract.\n      if (dirtyElements.size === 0 && dirtyLeaves.size === 0) return;\n      this.internals.setFormValue(this.value);\n      this.dispatchEvent(new Event('input', {bubbles: true, composed: true}));\n    });\n  }\n}\n```\n\nWithout the dirty-leaf/element gate every caret move would wake the form's\n`oninput` listeners, so the `input` event matches `HTMLInputElement`'s\ncontract only after that guard. The initial `setFormValue` is what keeps a\nform submission before the user types from carrying an empty value, since\nthe update listener has not fired yet at that point. A full reference\nimplementation lives in\n[`examples/shadow-dom-web-component`](https://github.com/facebook/lexical/tree/main/examples/shadow-dom-web-component).\n\nDOM moves (re-parenting the host into a different `<form>` or list) trigger\n`disconnectedCallback` followed by `connectedCallback`, which rebuilds the\neditor against a fresh contentEditable. Round-trip the user's content the\nsame way `<input>` and `<textarea>` round-trip their `value` attribute:\ncache the serialized state in `disconnectedCallback` and replay it through\n`parseEditorState` on the next mount. The reference dev example uses a\n`pendingState` field for exactly this.\n\nThe same form-associated host can also implement the rest of the\n[form-associated callbacks](https://developer.mozilla.org/docs/Web/API/Web_components/Using_custom_elements#form-associated_callbacks):\n`formStateRestoreCallback(state, reason)` re-hydrates the editor on bfcache\nnavigation (`reason: 'restore'`) or form autocomplete restore\n(`reason: 'autocomplete'`); `formAssociatedCallback(form)` reacts to\nprogrammatic form moves; `formResetCallback()` clears the editor when the\nsurrounding form is reset; and `formDisabledCallback(isDisabled)` flips the\neditor's editable state when an ancestor `<fieldset disabled>` toggles. The\nserialized JSON `internals.setFormValue` published earlier is what comes\nback in the state-restore callback.\n\n## Common pitfalls\n\nMoving an existing editor into a shadow root exposes a handful of recurring\nmismatches between the DOM APIs you'd reach for and what they report.\n\n### Event retargeting\n\nListeners above the shadow boundary — `window`, `document`'s\n`selectionchange`, popups portaled to `document.body` — see `event.target`\nretargeted to the shadow host, not the actual clicked element. A\n`rootElement.contains(event.target)` gate therefore always rejects clicks\nthat came from the editor. Resolve through the composed path:\n\n```js\nconst target = getComposedEventTarget(event);\nif (target instanceof Node && rootElement.contains(target)) {\n  // click really did land inside the shadow-hosted editor\n}\n```\n\n### Focus probes\n\n`document.activeElement` reports the host when focus is inside an open\nshadow tree, so `document.activeElement === rootElement` is always false.\n`getActiveElement(rootElement)` reads `DocumentOrShadowRoot.activeElement`\nthrough `Node.getRootNode` instead, and `getActiveElementDeep` keeps\ndescending into nested shadow roots when an editor whose decorator embeds a\nweb component needs the innermost focused element.\n\n```js\nif (getActiveElement(rootElement) === rootElement) { ... }\n```\n\n### Outside-click handlers\n\nA typical dropdown registers `document.addEventListener('click', ...)` and\ncalls `setShowDropDown(false)` whenever `button.contains(event.target)` is\nfalse. From inside a shadow tree that check always fails (the target is the\nhost), so the dropdown closes on the very click that opened it. Compare\nagainst `getComposedEventTarget(event)` instead — the same fix the playground's\n`DropDown` and `@lexical/react`'s `DraggableBlockPlugin` use.\n\n### Drop hit-tests\n\n`document.caretRangeFromPoint` and the no-argument\n`document.caretPositionFromPoint` return the host when the pointer is over\nshadow content, so an image drop lands on the host rather than the textnode\nunder the cursor. `@lexical/clipboard`'s internal `caretFromPoint` helper switches to\n`caretPositionFromPoint(x, y, {shadowRoots})` when `rootElement` lives in a\nshadow tree, and verifies the returned offset node really did land inside\none of the requested shadow roots — engines that silently ignore the option\nfall through to the legacy paths.\n\n### Style mirror cleanup\n\nIf your shadow mount mirrors `<style>` / `<link>` nodes from `document.head`\nand watches for additions via `MutationObserver`, mirror removals too;\notherwise stylesheets removed by HMR or a runtime theme swap linger inside\nthe shadow. `ShadowDomWrapper` tracks an `original → clone` map so an\nupstream removal drops the corresponding clone, and on unmount it removes\nevery clone so React 18 StrictMode's double-mount can't accumulate\nduplicates through the persistent shadow root.\n\nDev-mode HMR has two paths that a `childList`-only observer misses.\nFirefox replaces a `<style>`'s text content in place, so the observer\nneeds `characterData + subtree` to see the mutation; Chrome and WebKit\nupdate CSS through the CSSOM (`style.sheet.replaceSync` / `insertRule`),\nwhich the DOM observer never sees at all. `ShadowDomWrapper` also\nsubscribes to Vite's `vite:afterUpdate` hook for an explicit resync of\nevery clone after each HMR pass — `import.meta.hot` is `undefined` in\nproduction builds, so the hook is a no-op outside dev.\n\n### Popover and dialog layout\n\nThe UA stylesheet defaults a closed\n[Popover](https://developer.mozilla.org/docs/Web/API/Popover_API) (or a\n`<dialog>` without `open`) to `display: none`. A base rule like\n`#my-popover { display: flex; ... }` overrides that and leaves the popover\nvisible after `hidePopover()`. Scope layout to the open state instead —\n`#my-popover:popover-open { display: flex; }` — so the closed state honors\nthe UA default. This is a popover-API gotcha, not a shadow-specific one,\nbut it surfaces here because floating UI anchored to a shadow-root\nselection (a format popover or link editor that reads coordinates through\n`getDOMSelectionRangeAndPoints`) is a common shadow integration pattern.\n\n### Observers across the shadow boundary\n\n`ResizeObserver`, `IntersectionObserver`, and `MutationObserver` observe\nnodes inside an open shadow root with no special configuration — pass the\ninner node (the contentEditable or the host) just like any other DOM\ntarget. A `MutationObserver` registered on the host does not see mutations\ninside the shadow tree, though; observe the contentEditable or a\nshadow-internal container instead. The host's `attributeChangedCallback`\ncovers the host's own attribute changes.\n\n## Migrating an existing light-DOM editor\n\nThe same code shape keeps working in a shadow root; migration is mostly\nabout removing pre-shadow workarounds. Direct selection reads\n(`Selection.anchorNode`, `Selection.getRangeAt(0)`,\n`document.querySelector('[contenteditable]')` used as an editor probe) route\nthrough `getDOMSelectionPoints` / `getDOMSelectionRange` /\n`getActiveElement`, and any `event.target` read above the shadow boundary\nbecomes `getComposedEventTarget(event)`. CSS variables inherit through the\nshadow boundary but class declarations don't, so the editor/theme CSS has\nto be adopted or cloned into the shadow root.\n\nThe focus and popup paths are the ones that surprise people in review:\noutside-click handlers, blur-then-refocus, and \"did the editor lose focus?\"\nprobes usually need `getActiveElement(rootElement)` rather than\n`document.activeElement`; floating UI that portals into `document.body` and\nresolves hover with `element.getRootNode().elementFromPoint(x, y)` keeps the\nsame call shape but has to guard the popup root narrowing against a\ndetached popup whose `getRootNode()` returns itself.\n\n## Examples\n\nRunnable examples live in the repository:\n\n- [`examples/shadow-dom`](https://github.com/facebook/lexical/tree/main/examples/shadow-dom)\n  — a React editor inside a shadow root with a light‑DOM toolbar.\n- [`examples/shadow-dom-web-component`](https://github.com/facebook/lexical/tree/main/examples/shadow-dom-web-component)\n  — a framework‑free `<lexical-editor>` custom element, form‑associated via\n  `ElementInternals`. The page also mounts one instance inside a wrapper\n  `<div>` that opens its own shadow root, exercising the multi‑level\n  walk through two nested shadow boundaries.\n- [`examples/vanilla-js-iframe`](https://github.com/facebook/lexical/tree/main/examples/vanilla-js-iframe)\n  — an editor rendered into an `<iframe>`.\n- The playground's **Render in Shadow DOM** setting toggles the same editor\n  between light and shadow mounts, useful for spot-checking your plugins\n  against both modes.","packages/lexical-website/docs/concepts/transforms.md":"\n\n# Node Transforms\n\nTransforms are the most efficient mechanism to respond to changes to the EditorState.\n\nFor example:\nUser types a character and you want to color the word blue if the word is now equal to \"congrats\".\nWe programmatically add an `@Mention` to the editor, the `@Mention` is immediately next to another `@Mention` (`@Mention@Mention`). Since we believe this makes mentions hard to read, we want to destroy/replace both mentions and render them as plain TextNode's instead.\n\n```js\nconst removeTransform = editor.registerNodeTransform(TextNode, (textNode) => {\n  if (textNode.getTextContent() === 'blue') {\n    textNode.setTextContent('green');\n  }\n});\n```\n\n## Syntax\n\n```typescript\neditor.registerNodeTransform<T extends LexicalNode>(klass: Klass<T>, listener: Transform<T>): () => void\n```\n\n## Lifecycle\n\nTransforms are executed sequentially before changes are propagated to the DOM and multiple transforms still lead to a single DOM reconciliation (the most expensive operation in Lexical's lifecycle).\n\n```mermaid\nflowchart LR\n  accTitle: Transforms lifecycle\n  accDescr: A keyboard event leads to an update function. Its changes mark nodes dirty, and the transforms registered for those nodes run one after another. If a transform changed more nodes, the transforms for those nodes run again. When nothing is left dirty, the DOM is reconciled once and the update listeners run.\n  event{{\"Keyboard event\"}} --> update[\"Update function\"]\n  update --> t1[\"transform1\"]\n  t1 --> t2[\"transform2\"]\n  t2 --> dirty{\"Nodes still dirty?\"}\n  dirty -->|\"yes\"| t1\n  dirty -->|\"no\"| reconcile[\"DOM reconciled once\"]\n  reconcile --> listeners[\"Update listeners\"]\n```\n\n:::caution Beware!\n\nWhile it is possible to achieve the same or very similar result through an [update listener](listeners.md#registerupdatelistener) followed by an update, this is highly discouraged as it triggers an additional render (the most expensive lifecycle operation).\n\nAdditionally, each cycle creates a brand new `EditorState` object which can interfere with features like undo/redo (`HistoryExtension`) if not handled correctly.\n\n```js\neditor.registerUpdateListener(() => {\n  editor.update(() => {\n    // Don't do this\n  });\n});\n```\n\n:::\n\n### Dirty Nodes\n\n*Dirty Leaves* (any LexicalNode that is not an ElementNode) and *Dirty Elements*\nare tracked separately to support the transform heuristic.\n\nInternally, there are two states for dirty nodes:\n* Intentionally Dirty nodes (leaves or elements) had `node.getWritable()` or\n  `node.markDirty()` (an alias) called on them. Maintaining the\n  tree-of-doubly-linked-lists structure of a lexical document requires that\n  this Intentionally Dirty state will propagate to immediate siblings and in\n  some cases the parent node.\n* Unintentionally Dirty Element nodes are an ancestor of a\n  dirty node that were not explicitly marked as Intentionally Dirty\n  Only elements can be Unintentionally Dirty, because\n  leaves by definition can not have children.\n\nThe reconciler works by starting at the RootNode (which is the ancestor\nof any attached node, and thus always dirty whenever any attached node\nis dirty). Intentionally Dirty nodes have their createDOM and/or updateDOM\ncalled. Dirty Elements reconcile all of their children. Thus, reconciliation\nstops at the highest node in a subtree that has no dirty nodes (unless it\nis running with a flag to do a full reconciliation which considers all\nnodes as Intentionally Dirty).\n\n:::info\n\nThe transform heuristic depends on these internal implementation details to\nfind a fixed point where no more transforms are required.\n\n:::\n\n### Transform heuristic\n\n1. We transform leaves first. If transforms generate additional dirty nodes we repeat `step 1`. The reasoning behind this is that marking a leaf as dirty marks all its parent elements as dirty too.\n2. We transform elements.\n    - If element transforms generate additional dirty nodes we repeat `step 1`.\n    - If element transforms only generate additional dirty elements we only repeat `step 2`.\n\nA node will be marked as dirty on any (or most) modifications done to it, its children, or siblings in certain cases.\n\n## Preconditions\n\nPreconditions are fundamental for transforms to prevent them from running multiple times and ultimately causing an infinite loop.\n\nTransforms are designed to run when nodes have been modified (aka Intentionally Dirty). For the most part, transforms only need to run once after the update but the sequential nature of transforms makes it possible to have order bias. Hence, transforms are run over and over until this particular type of Node is no longer marked as intentionally dirty by any of the transforms.\n\nHence, we have to make sure that the transforms do not mark the node dirty unnecessarily.\n\n```js\n// When a TextNode changes (marked as dirty) make it bold\neditor.registerNodeTransform(TextNode, textNode => {\n  // Important: Check current format state\n  if (!textNode.hasFormat('bold')) {\n    textNode.toggleFormat('bold');\n  }\n});\n```\n\nBut oftentimes, the order is not important. The below would always end up in the result of the two transforms:\n\n```js\n// Extension 1\neditor.registerNodeTransform(TextNode, textNode => {\n  // This transform runs twice but does nothing the first time because it doesn't meet the preconditions\n  if (textNode.getTextContent() === 'modified') {\n    textNode.setTextContent('re-modified');\n  }\n})\n// Extension 2\neditor.registerNodeTransform(TextNode, textNode => {\n  // This transform runs only once\n  if (textNode.getTextContent() === 'original') {\n    textNode.setTextContent('modified');\n  }\n})\n// App\neditor.registerUpdateListener(({editorState}) => {\n  const text = editorState.read(() => $getRoot().getTextContent());\n  // text === 're-modified'\n});\n```\n\n:::info\n\nThe transform heuristic considers `RootNode` to be Intentionally Dirty whenever\nany node is dirty, and it ensures that this transform is applied last after all\nother transforms. In this way, you can consider it a \"pre-update\" listener,\nwhich occurs before any DOM reconciliation has happened. As with any other\ntransform, it may get called multiple times in a given update (especially if\nyour RootNode transform makes any node dirty).\n\n:::\n\n## Transforms on parent nodes\n\nTransforms are very specific to a type of node. This applies to both the declaration (`registerNodeTransform(ImageNode)`) and the times it triggers during an update cycle.\n\n```js\n// Won't trigger\neditor.registerNodeTransform(ParagraphNode, () => {});\n// Will trigger as TextNode was marked dirty\neditor.registerNodeTransform(TextNode, () => {});\neditor.update(() => {\n  const textNode = $getNodeByKey('3');\n  textNode.setTextContent('foo');\n});\n```\n\nWhile the marked dirty rule is always true, there are some cases when it's not immediately obvious and/or we force nearby nodes to become dirty for the sake of easier transform logic:\nYou add a node to an ElementNode, the ElementNode and the newly added children are marked dirty, also its new immediate siblings\nYou remove a node, its parent is marked dirty, also the node's immediate siblings prior to being removed\nYou move a node via `replace`, rules 2 and 1 are applied.\n\n```js\neditor.registerNodeTransform(ParagraphNode, paragraph => {\n // Triggers\n});\neditor.update(() => {\n  const paragraph = $getRoot().getFirstChild();\n  paragraph.append($createTextNode('foo'));\n});\n```\n\n## registerLexicalTextEntity\n\nIt is common to have certain nodes that are created/destroyed based on their text content and siblings. For example, `#lexical` is a valid hashtag whereas `#!lexical` is not.\n\nThis is a perfectly valid case for transforms but we have gone ahead and already built a utility transform wrapper for you for this specific case:\n\n```typescript\nregisterLexicalTextEntity<T extends TextNode>(\n  editor: LexicalEditor,\n  getMatch: (text: string) => null | EntityMatch,\n  targetNode: Klass<T>,\n  createNode: (textNode: TextNode) => T,\n): Array<() => void>;\n```\n\n## Examples\n\n1. [Emojis](https://github.com/facebook/lexical/blob/main/packages/lexical-playground/src/plugins/EmojisExtension/index.ts)\n2. [AutoLink](https://github.com/facebook/lexical/blob/main/packages/lexical-playground/src/plugins/AutoLinkExtension/index.ts)\n3. [HashtagExtension](https://github.com/facebook/lexical/blob/main/packages/lexical-hashtag/src/LexicalHashtagExtension.ts)\n","packages/lexical-website/docs/concepts/traversals.md":"# Node Traversals with NodeCaret\n\nNodeCaret offers a unified and efficient way for traversing the document\ntree, making it much easier to correctly implement traversals and avoid\nedge cases around empty nodes and collapsed selections.\n\nThese new low-level functions were all designed to work together as a\nfully featured relatively lightweight API to use in the core to\nallow us to gradually address some edge cases and then simplify and shrink\nthe code. We expect higher-level utilities to be developed and shipped\nin @lexical/utils or another module at a later date. The current overhead\nshould be less than 3kB in a production environment.\n\nThe NodeCaret API was introduced in lexical v0.25.0.\n\n## Concepts\n\nThe core concept with `NodeCaret` is that you can represent any specific\npoint in the document by using an `origin` node, a `direction` that\npoints towards an adjacent node (`next` or `previous`), and a `type`\nto specify whether the arrow points towards a sibling (`sibling`) or\ntowards a child (`child`).\n\nAll of these types have a `D` type parameter that must be a `CaretDirection`, so you\ncan not accidentally mix up `next` and `previous` carets. Many of them\nalso have a `T` type parameter that encodes the type of the `origin` node.\n\n:::tip\n\nThe methods of a caret are designed to operate on nodes attached to the `origin`\nin the designated direction, not the `origin` itself. For example, this code is\na no-op because it will attach a node to the `origin`, and then remove the node\nthat was just attached.\n\n```ts\n// The origin is unaffected (other than being marked dirty)\ncaret.insert($createTextNode('no-op')).remove();\n```\n\n:::\n\n:::warning\n\nCarets are immutable, and designed for low-level usage. There is no attempt\nfor carets to automatically update based on changes to the document\n(this is a common source of bugs when working with `RangeSelection`).\nFunctions and methods that work with carets and are expected to change the\nstructure of the document will always return a possibly new caret.\n\nThe `origin` of a caret is the exact version of the object that it was\nconstructed with, all accessor methods on that origin will generally call\n`origin.getLatest()` so the operations will see the latest version.\n\n:::\n\n### NodeCaret\n\n`NodeCaret` is any `SiblingCaret` or any `ChildCaret`\n* Typically constructed with `$getChildCaretOrSelf($getSiblingCaret(origin, direction))`\n  which returns a `ChildCaret` when the origin is an `ElementNode`\n\n:::tip\n\nThis type does not include `TextPointCaret` or `TextPointCaretSlice`,\nso you will not have to consider those edge cases when you see this\nmore specific type.\n\n:::\n\n### SiblingCaret\n\n`SiblingCaret` is a caret that points towards a sibling of the origin\n\n* Constructed with `$getSiblingCaret(origin: LexicalNode, direction: CaretDirection)`\n* The `next` direction points towards the right\n* The `previous` direction points towards the left\n\n|                        | → direction: `'next'`      | ← direction: `'previous'`     |\n|------------------------|---------------------------|-------------------------------|\n| `getParentAtCaret()`   | `origin.getParent()`      | `origin.getParent()`          |\n| `getNodeAtCaret()`     | `origin.getNextSibling()` | `origin.getPreviousSibling()` |\n| `insert(node)`         | `origin.insertAfter(node)`| `origin.insertBefore(node)`   |\n\n### ChildCaret\n\n`ChildCaret` is a caret that points towards the first or last child of the origin\n\n* Constructed with `$getChildCaret(origin: ElementNode, direction: CaretDirection)`\n* The `next` direction points towards the first child\n* The `previous` direction points towards the last child\n\n|                        | ↘ direction: `'next'`      | ↙ direction: `'previous'`     |\n|------------------------|----------------------------|-------------------------------|\n| `getParentAtCaret()`   | `origin`                   | `origin`                      |\n| `getNodeAtCaret()`     | `origin.getFirstChild()`   | `origin.getLastChild()`       |\n| `insert(node)`         | `origin.splice(0, 0, node)`| `origin.append(node)`         |\n\n### PointCaret\n\n`PointCaret` is any `TextPointCaret`, `SiblingCaret` or `ChildCaret`. This\ntype can be used to represent any point in the document that `PointType` can represent.\n\n### TextPointCaret\n\n`TextPointCaret` is basically a `SiblingCaret` with a `TextNode` origin and an `offset` property\n* Constructed with `$getTextPointCaret(origin, direction, offset)`\n* The `offset` property is an absolute index into the string\n* The `next` direction implies all text content after `offset`\n* The `previous` direction implies all text content before `offset`\n* All methods that are also present on `SiblingCaret` behave in the same way\n\n### TextPointCaretSlice\n\n`TextPointCaretSlice` is a wrapper for `TextPointCaret` that provides a signed `distance`.\n\n* Constructed with `$getTextPointCaretSlice(caret, distance)`\n* `$splitTextPointCaretSlice(slice, selection?)` isolates the slice in a text node,\n  splitting at its boundaries when needed. It returns `null` for an empty slice\n  and the original node for a whole-node slice. Pass the range selection that\n  produced the slice to preserve its endpoints, including element points and\n  detached selections. Like `TextNode.splitText`, it does not treat token or\n  segmented nodes as atomic.\n* There are convenience methods like `removeTextSlice()` and `getTextContent()`,\n  so it's not generally necessary to know the implementation details here\n* `Math.min(caret.offset, caret.offset + distance)` refers to the start offset of the slice\n* `Math.max(caret.offset, caret.offset + distance)` refers to the end offset of the slice\n* The `direction` of the caret is generally ignored when working with a\n  `TextPointCaretSlice`, the slice is in absolute string coordinates\n\n:::info\n\nThe property name `distance` was chosen because `length` and `size` are\ncommonly used on other data structures in JavaScript and Lexical, and they\nare overwhelmingly non-negative. While most uses of `distance` are also\nnon-negative, in some contexts such as computer graphics it is not uncommon\nto use\n[Signed distance functions](https://en.wikipedia.org/wiki/Signed_distance_function)\nwhere the distance metric is signed.\n\nIn SDF terms, the subset of the space is `[offset, ∞)`. Any coordinate less\nthan the `offset` boundary is a negative distance; otherwise the distance is\nnon-negative.\n\n:::\n\n### CaretRange\n\n`CaretRange` contains a pair of `PointCaret` that are in the same direction. It\nis equivalent in purpose to a `RangeSelection`, and is what you would generally\nuse for depth first traversals.\n\n* Constructed with `$getCaretRange(anchor, focus)`, `$caretRangeFromSelection(selection)`,\n  or `$extendCaretToRange(anchor)`\n* The `anchor` is the start of the range, generally where the selection originated,\n  and it is \"anchored\" in place because when a selection grows or shrinks only the\n  `focus` will be moved\n* The `focus` is the end of the range, where the blinking cursor is, it's the current\n  focus of the user\n* Anchor and focus must point in the same direction. The `anchor` points towards the first\n  node *in the range* and the focus points towards the first node *not in the range*\n* The `getTextSlices()` method is essential to handle the literal edge cases where\n  the anchor and/or focus are a `TextPointCaret`. These edges are *not* included\n  in the default caret iteration of the `CaretRange`.\n\n:::warning\n\nIf you are iterating a `CaretRange` you must consider the `getTextSlices()`\nseparately, they are not included in the iteration. This is so you don't have\nto consider `TextPointCaretSlice` at every step. They are literal edge cases\nthat can only be at the start and/or end and typically have special\ntreatment (splitting instead of removing, for example).\n\n:::\n\n## Traversal Strategies\n\n<!-- when you update the example code below, please update the tests in packages/lexical/src/caret/__tests__/unit/docs-traversals.test.ts -->\n\n### Adjacent Caret Traversals\n\nThe lowest level building block for traversals with NodeCaret is the adjacent caret\ntraversal, which is supported directly by methods of NodeCaret.\n\n`getAdjacentCaret()` - Gets a `SiblingCaret` for the node attached to\n  `origin` in direction. If there is no attached node, it will return `null`\n\n`getParentCaret(rootMode)` - Gets a `SiblingCaret` for the parent node\n  of `origin` in the same direction. If there is no parent node, or the parent\n  is a root according to `rootMode`, then it will return `null`. `rootMode`\n  may be `'root'` to only return `null` for `RootNode` or `'shadowRoot'` to\n  return `null` for `RootNode` or any `ElementNode` parent where\n  `isShadowRoot()` returns true\n\n`getChildCaret()` - Gets a `ChildCaret` for this origin, or `null` if the\n  origin is not an `ElementNode`. Will return `this` if the caret is already\n  a `ChildCaret`\n\nFor example, iterating all siblings:\n\n```ts\n// Note that NodeCaret<D> already implements Iterable<NodeCaret<D>> in this\n// way, so this function is not very useful. You can just use startCaret as\n// the iterable.\nfunction *$iterSiblings<D extends CaretDirection>(\n  startCaret: NodeCaret<D>\n): Iterable<SiblingCaret<LexicalNode, D>> {\n  // Note that we start at the adjacent caret. The start caret\n  // points away from the origin node, so we do not want to\n  // trick ourselves into thinking that that origin is included.\n  for (\n    let caret = startCaret.getAdjacentCaret();\n    caret !== null;\n    caret = caret.getAdjacentCaret()\n  ) {\n    yield caret;\n  }\n}\n```\n\n### Examples\n\nGiven the following document tree, here are some examples of using the\nadjacent node traversal:\n\nRoot\n* Paragraph A\n  * Text A1\n  * Link A2\n    * Text A3\n  * Text A4\n* Paragraph B\n  * Text B1\n* Paragraph C\n\n```ts\n// The root does not have sibling nodes\nconst carets = [...$getSiblingCaret($getRoot(), 'next')];\nexpect(carets).toEqual([]);\n```\n\n```ts\n// The adjacent node to a ChildNode is its first or last child\n// and is always a SiblingNode. It does not traverse deeper.\nconst carets = [...$getChildCaret($getRoot(), 'next')];\n\n// next starts at the first child\nexpect(carets).toEqual([\n  $getSiblingCaret(paragraphA, 'next'),\n  $getSiblingCaret(paragraphB, 'next'),\n  $getSiblingCaret(paragraphC, 'next'),\n]);\n\n// previous starts at the last child\nconst prevCarets = [...$getChildCaret($getRoot(), 'previous')];\nexpect(prevCarets).toEqual([\n  $getSiblingCaret(paragraphC, 'previous'),\n  $getSiblingCaret(paragraphB, 'previous'),\n  $getSiblingCaret(paragraphA, 'previous'),\n]);\n```\n\n```ts\n// The iteration starts at the node where the head of the \"arrow\"\n// is pointing, which is away from the origin (the tail of the \"arrow\").\nconst carets = [...$getSiblingCaret(paragraphB, 'next')];\nexpect(carets).toEqual([\n  $getSiblingCaret(paragraphC, 'next'),\n]);\n\nconst prevCarets = [...$getSiblingCaret(paragraphB, 'previous')];\nexpect(prevCarets).toEqual([\n  $getSiblingCaret(paragraphA, 'previous'),\n]);\n```\n\n### Depth First Caret Traversals\n\nThe strategy to do a depth-first caret traversal is to use an adjacent caret\ntraversal and immediately use a `ChildCaret` any time that an `ElementNode`\norigin is encountered. This strategy yields all possible carets, but each\nElementNode in the traversal may be yielded once or twice (a `ChildCaret` on\nenter, and a `SiblingCaret` on leave). Allowing you to see whether an\n`ElementNode` is partially included in the range or not is one of the\nreasons that this abstraction exists.\n\n```ts\nfunction *$iterCaretsDepthFirst<D extends CaretDirection>(\n  startCaret: NodeCaret<D>\n): Iterable<NodeCaret<D>> {\n  function step(prevCaret: NodeCaret<D>): null | NodeCaret<D> {\n    // Get the adjacent SiblingCaret\n    const nextCaret = prevCaret.getAdjacentCaret();\n    return (\n      // If there is a sibling, try and get a ChildCaret from it\n      (nextCaret && nextCaret.getChildCaret()) ||\n      // Return the sibling if there is one\n      nextCaret ||\n      // Return a SiblingCaret of the parent, if there is one\n      prevCaret.getParentCaret('root')\n    );\n  }\n  // You may add an additional check here, usually some specific\n  // caret to terminate the iteration with (such as the parent caret\n  // of startCaret):\n  //\n  //  `caret !== null || caret.is(endCaret)`\n  //\n  for (\n    let caret = step(startCaret);\n    caret !== null;\n    caret = step(caret)\n  ) {\n    yield caret;\n  }\n}\n```\n\nNormally this type of iteration would be done from a `CaretRange`, where you\nwould specify a precise end caret (focus).\n\n```ts\nfunction $iterCaretsDepthFirst<D extends CaretDirection>(\n  startCaret: NodeCaret<D>,\n  endCaret?: NodeCaret<D>,\n): Iterable<NodeCaret<D>> {\n  return $getCaretRange(\n    startCaret,\n    // Use the root as the default end caret, but you might choose\n    // to use startCaret.getParentCaret('root') for example\n    endCaret || $getSiblingCaret($getRoot(), startCaret.direction)\n  );\n}\n```\n\nTo get all nodes that are entirely selected between two carets:\n\n```ts\nfunction *$iterNodesDepthFirst<D extends CaretDirection>(\n  startCaret: NodeCaret<D>,\n  endCaret: NodeCaret<D> = $getChildCaret($getRoot(), startCaret.direction),\n): Iterable<LexicalNode> {\n  const seen = new Set<NodeKey>();\n  for (const caret of $getCaretRange(startCaret, endCaret)) {\n    const {origin} = caret;\n    if ($isChildCaret(caret)) {\n      seen.add(origin.getKey());\n    } else if (!$isElementNode(origin) || seen.has(origin.getKey())) {\n      // If the origin is an element and we have not seen it as a ChildCaret\n      // then it was not entirely in the CaretRange\n      yield origin;\n    }\n  }\n}\n```\n\n### Examples\n\nGiven the following document tree, here are some examples of using the\ndepth-first node traversal (with a `CaretRange`):\n\nRoot\n* Paragraph A\n  * Text A1\n  * Link A2\n    * Text A3\n  * Text A4\n* Paragraph B\n  * Text B1\n* Paragraph C\n\n```ts\n// A full traversal of the document from root\nconst carets = [...$getCaretRange(\n  // Start with the arrow pointing towards the first child of root\n  $getChildCaret($getRoot(), 'next'),\n  // End when the arrow points away from root\n  $getSiblingCaret($getRoot(), 'next'),\n)];\nexpect(carets).toEqual([\n  $getChildCaret(paragraphA, 'next'),   // enter Paragraph A\n  $getSiblingCaret(textA1, 'next'),\n  $getChildCaret(linkA2, 'next'),       // enter Link A2\n  $getSiblingCaret(textA3, 'next'),\n  $getSiblingCaret(linkA2, 'next'),     // leave Link A2\n  $getSiblingCaret(textA4, 'next'),\n  $getSiblingCaret(paragraphA, 'next'), // leave Paragraph A\n  $getChildCaret(paragraphB, 'next'),   // enter Paragraph B\n  $getSiblingCaret(textB1, 'next'),\n  $getSiblingCaret(paragraphB, 'next'), // leave Paragraph B\n  $getChildCaret(paragraphC, 'next'),   // enter Paragraph C\n  $getSiblingCaret(paragraphC, 'next'), // leave Paragraph C\n]);\n```\n\n## Future Direction\n\nIt's expected that higher-level abstractions will be built on top of this\noutside of the core, either in @lexical/utils or a separate companion package.\nThis is just designed to be the lowest-level layer with a consistent and\ntype-safe interface. That sort of abstraction will probably look a little bit\nlike cheerio or jQuery, but for working with Lexical documents. It is not\nexpected that more abstractions will be added to the core.\n\nIn order to reduce code size and eliminate bugs, more of the core will be\nrefactored to use NodeCaret internally.\n\nOnce this happens, it's possible that the internal structure of PointType\nand/or RangeSelection may change to accommodate NodeCaret, as it is more\nresilient to document changes (only changes that directly affect the\norigin node will \"break\" the point). A simple version of this would be to\ncreate a caret any time that the point changes, and use that caret\nas a fallback if the selection would otherwise be lost.\n\nIt may be the case that NodeCaret will become the lowest level API, working\ndirectly with private LexicalNode/ElementNode internals. When/if that happens,\nthe methods on LexicalNode will remain for backwards compatibility,\nbut overriding them will not be supported. It isn't particularly safe to\noverride them as-is anyway, and these overrides are frequently the\nroot cause of bugs (e.g. parents that remove themselves after an operation\non a child, causing the point to be lost unless the caller was sophisticated\nenough to store the array of parents).\n\n## History\n\nBefore NodeCaret, Lexical's core API offered a relatively low-level DOM-like\ninterface for working with nodes and traversing them. It has accumulated\nmany functions over time for performing various kinds of traversals around\nthe tree (finding ancestors, children, depth, siblings, etc.), but most of\nthem are not implemented in a way that makes them easy to combine\nefficiently, and many of them have edge cases that are difficult to avoid\nand can't really be addressed without breaking compatibility.\n\nMany of these functions also have a lot of edge cases, particularly around\nassuming the reference nodes are inclusive. Many are also left-to-right\nbiased, don't offer an iterative version that can be aborted early or\nconsumed on the fly, etc.\n\nRefactoring many of these to use something like `PointType` would almost\nbe sufficient for many of these use cases, but the representation of\nthat type is inefficient and error-prone as any mutation to the tree\nrequires that each point be manually recomputed. `PointType` is also\ndirectionless, forcing a specific left-to-right bias into most APIs.\n`RangeSelection` can be used in many cases because a direction can\nbe inferred from any two different points, but that collapses with\na single point. It's also impractical to use `RangeSelection`\nconcurrently with mutations due to the problems with `PointType`.\n\nNodeCaret was born out of frustration with these APIs and a desire\nto unify it all in a coherent way to simplify and reduce errors in\nthe core.\n\n## Terminology\n\n### Caret\n\nThe term Caret was chosen because it is concise and specific term\nspecific to a point in a text document. Lexical is \"an extensible text editor\nframework\" so it makes sense that navigation in the document would use\nterms relevant to text. Most other terms such as Cursor or Point\nalready have meanings in Lexical and/or are less specific.\n\nSee also:\n- [Caret](https://developer.mozilla.org/en-US/docs/Glossary/Caret)\n- [Caret navigation](https://en.wikipedia.org/wiki/Caret_navigation)\n\n### Origin\n\nThe origin is the reference node for a NodeCaret. Absolute coordinates\nare determined by combining this origin node and an \"arrow\" that points\ntowards to where the adjacent node is (or could be). The \"arrow\" is\ndetermined by the `direction` and `type` of the caret.\n\nIn a way this \"arrow\" is considered to be something like a unit vector\nto indicate the direction, and adding it to an origin allows you to specify\nan absolute location relative to that origin. Unlike the other coordinate\nsystems available in Lexical, it does not need recomputing whenever\nsiblings or a parent changes, so long as the origin node is still attached.\n\nSee also:\n- [Origin](https://en.wikipedia.org/wiki/Origin_(mathematics))\n- [Unit Vector](https://en.wikipedia.org/wiki/Unit_vector)\n\n### ChildCaret / SiblingCaret\n\nThese were chosen because they match the existing methods on `ElementNode`\nand `LexicalNode` (`getFirstChild`, `getNextSibling`, etc.)\n\n### Direction\n\n`'next'` and `'previous'` were chosen for direction mostly to match the\nexisting methods such as `getNextSibling()` that exist in DOM and in Lexical.\nUsing other words such as `'left'` and `'right'` would be ambiguous since\ntext direction can be bidirectional and already uses the terms left-to-right\nand right-to-left.\n\n### Distance\n\nThe property name `distance` was chosen for `TextPointCaretSlice` because\n`length` and `size` are commonly used on other data structures in JavaScript\nand Lexical, and they are overwhelmingly non-negative. While most uses of\n`distance` are also non-negative, in some contexts such as computer graphics\nit is not uncommon to use\n[Signed distance functions](https://en.wikipedia.org/wiki/Signed_distance_function)\nwhere the distance metric is signed.\n\nIn SDF terms, the subset of the space is `[offset, ∞)`. Any coordinate less\nthan the `offset` boundary is a negative distance; otherwise the distance is\nnon-negative.\n","packages/lexical-website/docs/design.md":"# Lexical's Design\n\n> Note: this document is still a work-in-progress and things are likely still in flux.\n\nLexical was built from the ground up to be a lean and extensible text-editing framework.\n\nYou can think of Lexical as more of a text editing engine, rather than a monolithic text\neditor that provides everything out-of-the-box. It might help to think of Lexical as\na framework like React – where React provides some hooks and a reconciler, and then you\nbuild the things you need on top, rather than have them included by default.\n\nThe main concept of Lexical is the editor state. Lexical uses a double-buffering technique\nto ensure consistency and reliability. There are never more than two editor states in play.\nThe \"current\" editor state represents what you can visibly see on screen and the \"pending\"\neditor state is what is currently being constructed to be shown in the future. Once the\n\"pending\" editor state is ready, it swaps and becomes the new \"current\" editor state.\n\nLexical's core concerns itself with four main concerns:\n\n- Updates: the act of making changes to editor state\n- Node Transforms: the process of acting on ongoing updates\n- Reconciliation: the process of patching the DOM with the latest editor state\n- Listening/Commands: the process of reacting to changes that occur internally\n\nAdditionally, Lexical uses DOM mutation observers to ensure that any outside changes to\nthe editor DOM element are either reverted back to Lexical's current editor state, or are\ncommunicated as intents that cause further updates to the editor state (text changes).\n\nThis separation was done to ensure developers have a way of applying custom logic with the\nimplementation and framework of their choice. This makes it possible to use Lexical with\nReact, or any other JavaScript web framework/library. This also makes it easier to tackle\nproblems around collaboration and undo/redo, where you might have an alternative model involved.\nInstead of hoping for the text editor to support what you want, you can work with the editor\nto make what you want.\n\nThis design can make getting started a bit more complex in certain cases, but as we've been\nable to demonstrate with our `@lexical/react` plugins and hooks – you can get started, in the\nform of an out-of-the-box experience.\n","packages/lexical-website/docs/error.md":"---\nid: error\ntitle: Error Code\n---\nimport ErrorCodePage from '@site/src/components/ErrorCodePage';\n\n# Error Code\n<ErrorCodePage/>","packages/lexical-website/docs/extensions/defining-extensions.md":"# Defining Extensions\n\nAn extension is an plain JavaScript object that complies with the\n[LexicalExtension](/docs/api/modules/lexical#lexicalextension) interface.\nThe [defineExtension](/docs/api/modules/lexical#defineextension) function\nis merely an identity function to make TypeScript inference convenient.\nIt will be compiled down to the following, and likely optimized out by your\nbundler or minifier:\n\n```js\nfunction defineExtension(extension) {\n  return extension;\n}\n```\n\nExtensions *must* be stable references. It is most useful to define them at\nmodule scope, but if for some reason you have to define it inside of a\nReact component you'll need some way to make it stable such as with\n`useState`, `useMemo`, or `useRef` as appropriate.\n\n## Required Properties\n\n### name\n\nThe only required property for an extension is `name`. The name is a string\nthat should be unique to that extension your editor. It is best practice to\nuse a scope for your project or organization to ensure that reusable\nextensions don't conflict with each other.\n\nThe general practice in the lexical repository is to use the name of the\npackage as the extension name when it only makes sense to export a single\nextension, (e.g. `DragonExtension` has the name `@lexical/dragon`), or\nwith an additional path if multiple extensions are exported (e.g.\n`AutoFocusExtension` has the name `@lexical/extension/AutoFocus`).\n\n## InitialEditorConfig\n\nExtensions can specify configuration overrides for the editor, these are\ndefined by the\n[InitialEditorConfig](/docs/api/modules/lexical#initialeditorconfig)\ninterface. See the API docs for the full list of properties. Every property\nhere has some default.\n\n### Merged properties\n\nFor an extension designed to be used as a dependency, it can also be useful\nto specify properties that are merged such as\n`html` (override HTML import/export),\n`nodes` (register new nodes or node overrides with the editor),\nor `theme` (specify classes to be used for nodes provided by lexical).\n\n```ts\nexport const CodeExtension = defineExtension({\n  name: '@lexical/code',\n  nodes: () => [CodeNode, CodeHighlightNode],\n});\n```\n\n### Root properties\n\nThere are other properties that are more useful for the \"root\" extension\nthat you provide to\n[buildEditorFromExtensions](/docs/api/modules/lexical_extension#buildeditorfromextensions)\nor the `extension` prop of\n[LexicalExtensionComposer](/docs/api/modules/lexical_react_LexicalExtensionComposer#lexicalextensioncomposer)\nwhere you would specify properties that can only be meaningfully set\nonce such as `$initialEditorState`, `editable`, `onError`, `namespace` or `parentEditor`.\n\nThere is no requirement that these properties be set from any specific level\nof the extension hierarchy, or at all, but it is only meaningful to set them\nonce per editor.\n\n```ts\nconst editor = buildEditorFromExtensions(\n  defineExtension({\n    name: \"@example/basic-rich-text-editor\",\n    namespace: \"basic-rich-text-editor\",\n    dependencies: [RichTextExtension],\n    register: (editor: LexicalEditor) => {\n      console.log(\"Editor Created\");\n      return () => console.log(\"Editor Disposed\");\n    },\n  }),\n);\n```\n\n## Extension Dependencies\n\nLexical Extensions have two ways for extensions to depend on and specify\nconfiguration for each other:\n\n- [dependencies](#dependencies) are direct (by reference)\n- [peerDependencies](#peerdependencies) are indirect (by name) and optional\n\nThe exception to this rule is with `peerDependencies`. These are optional\ndependencies\n\n### dependencies\n\nThe `dependencies` property is an array of extensions by reference that your\nextension depends on. For example, if your extension uses React then it should\ndepend on the `ReactExtension`.\n\nDependencies form a directed acyclic graph. This means that there must not\nbe any circular dependencies. If extension A depends on extension B then\nthere must not be any dependency path from B to A\n(or from A to A).\n\nThis array can contain either direct references to extensions, or calls to\n[configExtension](/docs/api/modules/lexical#configextension).\n\n```ts\nexport const ExampleExtension = defineExtension({\n  name: \"@example/extension\",\n  dependencies: [\n    SomeExtension,\n    configExtension(ReactExtension, {\n      decorators: [<ExampleDecorator />]\n    }),\n  ],\n});\n```\n\n### peerDependencies\n\nThe `peerDependencies` property is an array of optional extensions by name.\nThey are not requirements, but specifying them here allows your extension\nto find them at runtime and override configuration for them if they are\nbuilt with the editor. You can use\n[declarePeerDependency](/docs/api/modules/lexical#declarepeerdependency) to\nbuild this array with type inference.\n\nSince these are optional and by name, an extension may declare a peer it does\nnot import. The graph must still be acyclic: peer edges take part in the same\ntopological sort as `dependencies`, so two extensions that peer-depend on each\nother and are both present will fail to build.\n\nThis is rarely needed in practice and is considered an advanced use case,\ntypically to avoid a direct import or dependency loop. See\n[Peer Dependencies](/docs/extensions/peer-dependencies) for more detail.\n\n### conflictsWith\n\nThe `conflictsWith` property is an array of extensions by name that are\nknown to conflict with this extension. For example, it does not make\nsense to have\n[RichTextExtension](/docs/api/modules/lexical_rich-text#richtextextension) and\n[PlainTextExtension](/docs/api/modules/lexical_plain-text#plaintextextension)\nin the same editor.\n\n```ts\nexport const PlainTextExtension = defineExtension({\n  conflictsWith: ['@lexical/rich-text'],\n  dependencies: [\n    DragonExtension,\n    NormalizeInlineElementsExtension,\n    NormalizeTripleClickSelectionExtension,\n  ],\n  name: '@lexical/plain-text',\n  register: registerPlainText,\n});\n```\n\nThis is rarely needed in practice and is considered an advanced use case,\nbut it can provide helpful early errors when it is needed.\n\n## Phases\n\nThe construction of an editor using extensions is done in sequential phases,\nand the following properties are useful ways to hook into that process at\nspecific points.\n\n### config\n\n`config` is an object that is the default configuration for your extension,\nproperties of this object can be overridden by other extensions using\n[configExtension](/docs/api/modules/lexical#configextension) or\n[declarePeerDependency](/docs/api/modules/lexical#declarepeerdependency).\n\nThis object is used in later phases to build the init and/or output.\n\nThe config phase happens before the editor is constructed.\n\n### mergeConfig\n\n`mergeConfig` is a function that can be used to merge configuration\nwith its overrides if you need something more fine-grained than a\nshallow object merge, such as concatenating arrays. For example:\n\n```ts\ninterface StringArrayConfig {\n  array: string[];\n}\nconst StringArrayExtension = defineExtension({\n  config: safeCast<StringArrayConfig>({array: []}),\n  name: \"@example/StringArray\",\n  mergeConfig(a, b) {\n    const config = shallowMergeConfig(a, b);\n    if (b.array) {\n      config.array =\n        b.array.length > 0\n          ? [...a.array, ...b.array]\n          : a.array;\n    }\n    return config;\n  },\n});\n```\n\nThe default implementation is\n[shallowMergeConfig](/docs/api/modules/lexical#shallowmergeconfig),\nmost extensions will not need to override this.\n\n### init\n\nThe init phase happens before the editor is constructed, but after\nthe merged editor configuration is available and all extensions are\nconfigured. It can be used to reference configuration\nfrom peers, compute data that should be available during\n[build](#build), and is generally a last resort for making mutations\nto any extension or editor configuration before the editor is\ncreated.\n\nThe result of this function is available in later phases.\n\nThis is rarely needed in practice and is considered an advanced use case.\n\n### build\n\nThe build phase happens after the editor is constructed — it receives the\n`LexicalEditor` — but before [register](#register), and after\n[config](#config) and [init](#init). The return value of the `build`\nphase is `output` and is available for later phases.\n\n`output` is generally how extensions provide functionality to each\nother and to the nodes in your application. A very common use case is\nto build signals from configuration with `namedSignals` so that the\nbehavior of your extension can be modified at runtime (e.g. `disabled`\nis a very common use case).\n\nOther use cases for build's `output` are to provide\nshared data structures, typed theme configuration, references to the\ncommands that the extension implements, etc.\n\n```ts\nexport interface TabIndentationConfig {\n  disabled: boolean;\n  maxIndent: null | number;\n  /**\n   * By default, indents are set on all elements for which the\n   * {@link ElementNode.canIndent} returns true. This option allows you to set\n   * indents for specific nodes without overriding the method for others.\n   */\n  $canIndent: CanIndentPredicate;\n}\nexport const TabIndentationExtension = defineExtension({\n  build(editor, config, state) {\n    return namedSignals(config);\n  },\n  config: safeCast<TabIndentationConfig>({\n    $canIndent: $defaultCanIndent,\n    disabled: false,\n    maxIndent: null,\n  }),\n  name: '@lexical/extension/TabIndentation',\n  register(editor, config, state) {\n    const {disabled, maxIndent, $canIndent} = state.getOutput();\n    return effect(() => {\n      if (!disabled.value) {\n        return registerTabIndentation(editor, maxIndent, $canIndent);\n      }\n    });\n  },\n});\n```\n\n### register\n\nThis happens after the editor has been constructed. This is where you will\nregister any commands, listeners, etc. that your extension needs. It can use\nthe result of `init` or `build` via `state.getInitResult()` and\n`state.getOutput()` respectively.\n\nThe return value is a dispose function, typically the result of `mergeRegister`.\n\n### afterRegistration\n\nThis happens after [register](#register) for every extension has been called,\nso all commands should be registered. This is the phase when the\n`$initialEditorState` is applied to the editor by `InitialStateExtension`,\nso `editor.setRootElement` should be called no sooner than this phase (it may\nof course be called after the editor is built, outside of extensions).\n\nThe return value is a dispose function, typically the result of\n`mergeRegister`.\n","packages/lexical-website/docs/extensions/design.md":"# Design Doc\n\nSee also the initial PR [#7706](https://github.com/facebook/lexical/pull/7706)\n\n## Guiding principles\n\n### Minimize boilerplate\n\nWherever possible, sensible defaults should be provided and it must be\npossible to override them. This is why a `Extension` has the `config` property\nwhich specifies the initial configuration.\n\n### Full backwards compatibility\n\nAlmost anything that you do with the editor or a legacy React plug-in should\njust work. Upgrading to use a `Extension` should be a simple change.\n\n### Minimize change required to add an Extension\n\nIdeally, adding an extension should be two or three lines of code:\n\n- Import it\n- Add it as a dependency (with config if necessary)\n- Add UI related code (only if needed, e.g. has to be rendered in a specific place in the DOM)\n\n### Avoid invalid states\n\nTypeScript goes a long way here, but it's only as good as the API design.\nFor example, splitting up the workflow for an Extension to have separate `Config`,\n`Init` and `Output` types was something that evolved over time. In earlier\nprototypes, `Config` was overloaded to maintain all of that state, but it\nrequired a lot more runtime support and type assertions to check that\nthe state was properly initialized for that phase.\n\n### Try to avoid undecipherable type errors\n\nThere's only so much that can be done to make readable error messages\nout of complex type expressions, so the API is designed to avoid them\nwhen possible. See also [microsoft/TypeScript#23689](https://github.com/microsoft/TypeScript/issues/23689).\n\n## Inspiration\n\n### [ESLint Flat Plugins](https://eslint.org/docs/latest/extend/plugin-migration-flat-config)\n\nThese have many of the same constraints as this project, although of course\nare used for a very different purpose\n\n### [MDXEditor](https://mdxeditor.dev/)\n\nThis editor is based on Lexical, but provides its own mechanism for plugins\nbased on a third party state management library. These plugins don't\nreally expose any type-safe configuration.\n\n### [@payloadcms/richtext-lexical](https://www.npmjs.com/package/@payloadcms/richtext-lexical)\n\nAnother Lexical based editor framework for use inside the\n[Payload](https://payloadcms.com/) CMS. It also doesn't really expose type-safe\nconfiguration, but it does seem to have quite a lot of features and seems\nwell-designed for laziness and SSR. Features are conceptually similar to Extensions.\n\n### [Effect](https://effect.website/)\n\nThis library is pretty much state of the art for type safety with a focus on\ncomposition and usability.\n\n### Tighter integration with LexicalEditor\n\nThis package was originally prototyped as an entirely optional feature,\nas adoption increases it's expected that more of Lexical will directly\ndepend on it.\n\n## Intentional trade-offs\n\n### No compile-time support for dependency resolution\n\nThe current theory is that it would require too much TypeScript\nin order to carry around the list of all dependency names that\nexist in the graph, and would likely add another type argument\nto `LexicalExtension`.\n\nThe features that this blocks are:\n\n- Compile-time support for detecting Extension conflicts. Detecting that two\n  extensions defined with the same name (but are not identical object\n  references, because a dependency can be shared!) would also be quite the\n  undertaking in TypeScript. This is automatically detected at runtime by\n  the builder.\n- Compile-time support for required configuration without defaults.\n  An extension can implement this at runtime in `init` or `register`.\n- Compile-time support for required peer dependencies. A use case for this\n  would be the requirement of a `ReactProviderExtension` provided by either\n  `LexicalExtensionComposer` or `ReactPluginHostExtension`. An extension can implement this\n  at runtime in `init` or `register`.\n\nGenerally speaking, all of these are already surfaced as runtime errors\nwhile building the editor with sufficient information to quickly track\ndown the root cause.\n\n## Known Missing Features\n\n### Direct support for devtools\n\nThis is a TODO, the infrastructure was designed with this in mind.\n\n### Documented patterns for RSC/SSR/Headless\n\nHaving a known peer dependency that is used to declare SSR may help\nguide future Extension development. In many cases there are decorator nodes\nthat have React dependencies that you do not want to (or can not) render\nin an SSR context. For example, RSC can not be supported anywhere that\n`React.Context` is used which is everywhere that React is currently used\nin Lexical.\n\nAnother option would be to build separate entrypoints for the SSR use case,\nwhich is more of an RSC specific strategy, but we could encourage people\nto use those import conditions even in a Vanilla JS or some other bespoke\nSSR use case. That strategy may make it hard to support including both\n\"headless\" and \"non-headless\" in the same module.\n","packages/lexical-website/docs/extensions/included-extensions.md":"# Included Extensions\n\n[@lexical/a11y](/docs/api/modules/lexical_a11y)\n\nFramework-agnostic accessibility extensions. See [Keyboard Accessibility](/docs/concepts/keyboard-accessibility) for the full contract and the matching `@lexical/react` adapters.\n\n- [AriaLiveRegionExtension](/docs/api/modules/lexical_a11y#arialiveregionextension) - Owns a visually hidden `aria-live` region and exposes a stable `announce` sink (WAI-ARIA status messages, WCAG 4.1.3)\n- [EditorModeAnnounceExtension](/docs/api/modules/lexical_a11y#editormodeannounceextension) - Announces editable / read-only transitions through the live region\n- [FocusManagerExtension](/docs/api/modules/lexical_a11y#focusmanagerextension) - Editor ↔ toolbar focus jump (Alt+F10 to the toolbar, Escape back to the editor), reference-counted per toolbar\n- [FocusTrapExtension](/docs/api/modules/lexical_a11y#focustrapextension) - Traps Tab / Shift+Tab focus inside one or more containers (modal dialogs), reference-counted per container\n- [HistoryAnnounceExtension](/docs/api/modules/lexical_a11y#historyannounceextension) - Announces undo / redo through the live region\n- [RovingTabIndexExtension](/docs/api/modules/lexical_a11y#rovingtabindexextension) - WAI-ARIA roving-tabindex pattern for toolbars / groups, reference-counted per container\n\n[@lexical/clipboard](/docs/api/modules/lexical_clipboard)\n\n- [ClipboardDOMImportExtension](/docs/api/modules/lexical_clipboard#clipboarddomimportextension) - Routes `text/html` pastes and drops through the `DOMImportExtension` pipeline (experimental)\n- [ClipboardImportExtension](/docs/api/modules/lexical_clipboard#clipboardimportextension) - Configurable per-MIME-type clipboard import for paste and drop (experimental)\n- [GetClipboardDataExtension](/docs/api/modules/lexical_clipboard#getclipboarddataextension) - Configurable serialization of the selection into clipboard MIME types for copy and drag\n\n[@lexical/code](/docs/api/modules/lexical_code)\n\n- [CodeExtension](/docs/api/modules/lexical_code#codeextension) - CodeNode (code blocks)\n- [CodeIndentExtension](/docs/api/modules/lexical_code#codeindentextension) - CodeNode tab key indentation (code blocks)\n\n[@lexical/code-prism](/docs/api/modules/lexical_code-prism)\n\n- [CodePrismExtension](/docs/api/modules/lexical_code-prism#codeprismextension) - Highlighting with Prism for CodeNode\n\n[@lexical/code-shiki](/docs/api/modules/lexical_code-shiki)\n\n- [CodeShikiExtension](/docs/api/modules/lexical_code-shiki#codeshikiextension) - Highlighting with Shiki for CodeNode\n\n[@lexical/dragon](/docs/api/modules/lexical_dragon)\n\n- [DragonExtension](/docs/api/modules/lexical_dragon#dragonextension) - Dragon (speech to text) support, included by default with `RichTextExtension` and `PlainTextExtension`\n\n[@lexical/extension](/docs/api/modules/lexical_extension)\n\n- [AutoFocusExtension](/docs/api/modules/lexical_extension#autofocusextension) - Focus the editor when it is created (e.g. on page load)\n- [ClearEditorExtension](/docs/api/modules/lexical_extension#cleareditorextension) - Implementation for the `CLEAR_EDITOR_COMMAND`\n- [ClickAfterLastBlockExtension](/docs/api/modules/lexical_extension#clickafterlastblockextension) - Inserts a paragraph when clicking below a last block that can not accommodate the click (e.g. a DecoratorNode or TableNode)\n- [DecoratorTextExtension](/docs/api/modules/lexical_extension#decoratortextextension) - DecoratorTextNode support, sets the format and CSS classes for the DOM container\n- [EditorStateExtension](/docs/api/modules/lexical_extension#editorstateextension) - Provide EditorState as a signal (alternative to `registerUpdateListener`)\n- [HMRExtension](/docs/api/modules/lexical_extension#hmrextension) - Preserves editor state, selection, editability and undo/redo history across Hot Module Replacement (HMR) cycles\n- [HorizontalRuleExtension](/docs/api/modules/lexical_extension#horizontalruleextension) - HorizontalRuleNode (`<hr>` tag)\n- [IMEExtension](/docs/api/modules/lexical_extension#imeextension) - Centralizes IME composition state as signals\n- [InitialStateExtension](/docs/api/modules/lexical_extension#initialstateextension) - Sets the initial state of the editor (always included)\n- [KeyboardShortcutsExtension](/docs/api/modules/lexical_extension#keyboardshortcutsextension) - Dispatches a table of keyboard shortcuts from a single compiled `KEY_DOWN_COMMAND` listener; the table is merged across the extension graph by name (experimental)\n- [NestedEditorExtension](/docs/api/modules/lexical_extension#nestededitorextension) - Configures an editor as a nested editor of a parent editor (theme inheritance, optionally editable state)\n- [NodeSelectionExtension](/docs/api/modules/lexical_extension#nodeselectionextension) - Tracks selection, typically for DecoratorNodes\n- [NodeSelectionDataSelectedExtension](/docs/api/modules/lexical_extension#nodeselectiondataselectedextension) - Mirrors a node's `NodeSelection` membership onto its host DOM as a `data-selected` attribute so CSS can outline selected `ElementNode` hosts; configured per node type (experimental)\n- [NormalizeInlineElementsExtension](/docs/api/modules/lexical_extension#normalizeinlineelementsextension) - Removes empty inline elements, included by default with `RichTextExtension` and `PlainTextExtension`\n- [NormalizeTripleClickSelectionExtension](/docs/api/modules/lexical_extension#normalizetripleclickselectionextension) - Corrects over-selection after triple click events, included by default with `RichTextExtension` and `PlainTextExtension`\n- [PreventSelectAllExtension](/docs/api/modules/lexical_extension#preventselectallextension) - Prevents select all (Ctrl/Cmd+A) inside input/textarea elements from selecting the editor content, included by default with `SelectBlockExtension`\n- [RootElementExtension](/docs/api/modules/lexical_extension#rootelementextension) - Exposes the editor's root element as a reactive `Signal<HTMLElement | null>`\n- [SelectBlockExtension](/docs/api/modules/lexical_extension#selectblockextension) - Select all (Ctrl/Cmd+A) selects the nearest block element first, pressing it again selects the whole document\n- [SelectionAlwaysOnDisplayExtension](/docs/api/modules/lexical_extension#selectionalwaysondisplayextension) - Highlights selected content even when the editor is not focused\n- [TabIndentationExtension](/docs/api/modules/lexical_extension#tabindentationextension) - Changes Tab key to insert tabs and indent instead of natively focusing the next field\n- [WatchEditableExtension](/docs/api/modules/lexical_extension#watcheditableextension) - Exposes the editor's editable state as a reactive `Signal<boolean>`\n\n[@lexical/hashtag](/docs/api/modules/lexical_hashtag)\n\n- [HashtagExtension](/docs/api/modules/lexical_hashtag#hashtagextension) - HashtagNode\n\n[@lexical/history](/docs/api/modules/lexical_history)\n\n- [HistoryExtension](/docs/api/modules/lexical_history#historyextension) - History support (undo/redo)\n- [SharedHistoryExtension](/docs/api/modules/lexical_history#sharedhistoryextension) - History sharing between a parent editor and its nested editors\n\n[@lexical/html](/docs/api/modules/lexical_html)\n\n- [CoreImportExtension](/docs/api/modules/lexical_html#coreimportextension) - DOM import rules for the core nodes, included implicitly by node-providing extensions (experimental)\n- [DOMImportExtension](/docs/api/modules/lexical_html#domimportextension) - Extension-based replacement for `importDOM`/`DOMConversion`, rules are compiled into a dispatcher at editor build time (experimental)\n- [DOMRenderExtension](/docs/api/modules/lexical_html#domrenderextension) - Overrides the DOM render and export behavior for an editor (experimental)\n\n[@lexical/link](/docs/api/modules/lexical_link)\n\n- [AutoLinkExtension](/docs/api/modules/lexical_link#autolinkextension) - AutoLinkNode, note that matchers have to be manually configured\n- [ClickableLinkExtension](/docs/api/modules/lexical_link#clickablelinkextension) - Toggle between lexical selection and native click to open behavior for links\n- [LinkExtension](/docs/api/modules/lexical_link#linkextension) - LinkNode (`<a>` tag)\n\n[@lexical/list](/docs/api/modules/lexical_list)\n\n- [ListExtension](/docs/api/modules/lexical_list#listextension) - ListNode, ListItemNode\n- [CheckListExtension](/docs/api/modules/lexical_list#checklistextension) - Checklist support for ListNode and ListItemNode\n\n[@lexical/mark](/docs/api/modules/lexical_mark)\n\n- [MarkExtension](/docs/api/modules/lexical_mark#markextension) - MarkNode\n\n[@lexical/mdast](/docs/api/modules/lexical_mdast)\n\nExperimental micromark/mdast-based Markdown support (every extension in this package is experimental). See [Markdown with @lexical/mdast](/docs/serialization/markdown-mdast) for the full story.\n\n- [MdastExtension](/docs/api/modules/lexical_mdast#mdastextension) - The configurable core registry, exposing both Markdown import and export\n- [MdastHeadingExtension](/docs/api/modules/lexical_mdast#mdastheadingextension) - ATX & setext headings (HeadingNode)\n- [MdastBlockquoteExtension](/docs/api/modules/lexical_mdast#mdastblockquoteextension) - Block quotes (QuoteNode)\n- [MdastRichTextExtension](/docs/api/modules/lexical_mdast#mdastrichtextextension) - Bundle of heading + blockquote\n- [MdastListExtension](/docs/api/modules/lexical_mdast#mdastlistextension) - Ordered/unordered lists (ListNode, ListItemNode)\n- [MdastTaskListExtension](/docs/api/modules/lexical_mdast#mdasttasklistextension) - GFM task lists (`- [x]`), layered on MdastListExtension\n- [MdastCodeExtension](/docs/api/modules/lexical_mdast#mdastcodeextension) - Fenced & indented code blocks (CodeNode)\n- [MdastLinkExtension](/docs/api/modules/lexical_mdast#mdastlinkextension) - Links, CommonMark autolinks, reference links (LinkNode)\n- [MdastAutolinkLiteralExtension](/docs/api/modules/lexical_mdast#mdastautolinkliteralextension) - GFM literal autolinks (bare URLs in prose)\n- [MdastHorizontalRuleExtension](/docs/api/modules/lexical_mdast#mdasthorizontalruleextension) - Thematic breaks (HorizontalRuleNode)\n- [MdastStrikethroughExtension](/docs/api/modules/lexical_mdast#mdaststrikethroughextension) - GFM strikethrough (text format)\n- [MdastTableExtension](/docs/api/modules/lexical_mdast#mdasttableextension) - GFM tables (TableNode, ...)\n- [MdastCommonMarkExtension](/docs/api/modules/lexical_mdast#mdastcommonmarkextension) - Bundle of the six CommonMark extensions\n- [MdastGfmExtension](/docs/api/modules/lexical_mdast#mdastgfmextension) - Bundle of the four GFM extensions\n- [MdastShadowRootQuoteExtension](/docs/api/modules/lexical_mdast#mdastshadowrootquoteextension) - Opt-in: blockquotes as block containers with full-fidelity nested content\n- [MdastHtmlExtension](/docs/api/modules/lexical_mdast#mdasthtmlextension) - Opt-in: raw HTML in Markdown routed through the `@lexical/html` DOM import rules (Markdown inside the tags included, GitHub-style), plus generic HTML-encoded export via `$exportViaDOM` / `rawHtmlBlock`\n- [MdastShortcutsExtension](/docs/api/modules/lexical_mdast#mdastshortcutsextension) - Streaming Markdown typing shortcuts, driven by the same grammar as import\n\n[@lexical/overflow](/docs/api/modules/lexical_overflow)\n\n- [OverflowExtension](/docs/api/modules/lexical_overflow#overflowextension) - OverflowNode\n\n[@lexical/plain-text](/docs/api/modules/lexical_plain-text)\n\n- [PlainTextExtension](/docs/api/modules/lexical_plain-text#plaintextextension) - Plain text editor, the return key creates a LineBreakNode by default (one ParagraphNode per document)\n\n`@lexical/react`\n\n- [ReactExtension](/docs/api/modules/lexical_react_ReactExtension#reactextension)\n- [ReactPluginHostExtension](/docs/api/modules/lexical_react_ReactPluginHostExtension#reactpluginhostextension)\n- [ReactProviderExtension](/docs/api/modules/lexical_react_ReactProviderExtension#reactproviderextension)\n- [TreeViewExtension](/docs/api/modules/lexical_react_TreeViewExtension#treeviewextension)\n\n[@lexical/rich-text](/docs/api/modules/lexical_rich-text)\n\n- [RichTextExtension](/docs/api/modules/lexical_rich-text#richtextextension) - Rich Text editor (QuoteNode, HeadingNode), the return key creates a ParagraphNode by default (multiple ParagraphNode per document). Includes configurable `escapeFormatTriggers` to escape text formatting (e.g. code) at text node boundaries, and `shouldHandlePasteAsFiles` to control whether files win over incidental text/HTML clipboard entries on paste\n\n[@lexical/table](/docs/api/modules/lexical_table)\n\n- [TableExtension](/docs/api/modules/lexical_table#tableextension) - TableNode, TableRowNode, TableCellNode\n\n[@lexical/tailwind](/docs/api/modules/lexical_tailwind)\n\n- [TailwindExtension](/docs/api/modules/lexical_tailwind#tailwindextension) - A theme configuration that uses Tailwind classes\n","packages/lexical-website/docs/extensions/intro.md":"# Lexical Extensions\n\nExtensions are a new convention to add configuration and behavior to a Lexical\neditor in a modular and framework-independent manner. It is easier to use and\nmore powerful than the React plug-in scheme that came before, and is framework\nagnostic.\n\nSee [Included Extensions](./included-extensions) for the list of extensions\nthat come with Lexical and its associated packages.\n\n## Use Case\n\nLexical Extensions allow complex features to be added to your editor\nin a single place. If you want to have an editor that supports check\nlists, you simply add the `CheckListExtension` to your dependencies.\nAll of its dependent configuration, registration, and dependencies on\nother extensions is encapsulated. Extensions also work the same way\nwith or without React, so you don't generally need separate\ndocumentation and exports to support both.\n\nAll of the complexity for how and when things need to happen is\nmoved to `buildEditorFromExtensions` and the definitions of the\nextensions themselves.\n\nIn contrast, using checklists without Lexical Extensions requires\n`ListNode` and `ListItemNode` to be configured, and both the `ListPlugin` and\n`CheckListPlugin` to be registered exactly one time each (or if not using\nReact: `registerList` and `registerCheckList`).\n\nCustomizing a Lexical editor without Lexical Extensions is done in two phases:\n\n* Configuration before the editor is constructed\n  (registered nodes, node replacement, theme, etc.)\n* Registration after the editor is constructed\n  (listeners, transforms, commands, react plug-ins, etc.)\n\nIn many cases, adding a feature to the editor requires *both* configuration\nand registration. These typically happen at very different places in the code,\npossibly even different files, and there are no transitive dependencies. It's\nup to you to make sure that every required snippet of configuration and\nregistration code is included exactly once in the right place. When some dependent\nconfiguration or registration is missing (or runs twice!), at best you get a runtime\nerror, but often it will partially work which is tricky to debug.\n\nAnother compelling use case for extensions is to provide functionality\nfrom one extension to another extension (e.g. collaboration with yjs),\nor have one extension enhance the capabilities of another (e.g. markdown\nimport/export, adding options to menus or toolbars, etc.).\nThis is very awkward and error-prone with legacy plug-ins since there is no\nestablished pattern for them to depend on each other, and providing this\nsort of functionality with React requires a strict hierarchy of context\nproviders. Any extension can provide output, which is equivalent to having\nextension-specific context scoped to an editor.\n\n## Core API Overview\n\nThe primary use case for extensions is to build an editor from a collection of\nextensions. Any configuration or behavior specific to your editor should be\nprovided as an extension with\n[defineExtension](/docs/api/modules/lexical#defineextension) or as an override\nof some dependency's configuration with\n[configExtension](/docs/api/modules/lexical#configextension). This is done\nwith either:\n\n* [buildEditorFromExtensions](/docs/api/modules/lexical_extension#buildeditorfromextensions) - when not using React\n* [LexicalExtensionComposer](/docs/api/modules/lexical_react_LexicalExtensionComposer) - when using React\n\nSee [Defining Extensions](./defining-extensions.md) for a detailed guide for\n`defineExtension`.\n\nThe following functions are included in the core\n[lexical](/docs/api/modules/lexical) module to ensure that defining\nextensions is lightweight and requires no dependency on the runtime\nfeatures of [@lexical/extension](/docs/api/modules/lexical_extension).\nThese functions are primarily for type inference with very minimal\nimplementations.\n\n* [defineExtension](/docs/api/modules/lexical#defineextension) - define an extension\n* [configExtension](/docs/api/modules/lexical#configextension) - provide configuration overrides for an extension\n* [declarePeerDependency](/docs/api/modules/lexical#declarepeerdependency) - declare an indirect dependency on another extension by name\n* [safeCast](/docs/api/modules/lexical#safecast) - a convenience for TypeScript casting, with more safety than `as`. Similar to using a variable with a declared type.\n* [shallowMergeConfig](/docs/api/modules/lexical#shallowmergeconfig) - the default configuration merge strategy\n\n## Advanced features\n\n- [Signals](./signals) - Can be used for extensions to change their configuration or behavior at runtime, such as dynamically enabling or disabling an extension. This is also used to facilitate some performance optimizations around subscriptions and listeners.\n- [Peer Dependencies](./peer-dependencies) - Used to declare indirect or optional dependencies between extensions, this can be used to enable some functionality or configuration only when some other extension is available. For example, an Emoji extension may declare a specific Markdown serialization for EmojiNode, but only if the Markdown extension is present.\n","packages/lexical-website/docs/extensions/peer-dependencies.md":"# Peer Dependencies\n\nPeer Dependencies are used to declare indirect or optional dependencies\nbetween extensions. This can be used to enable some functionality or\nconfiguration only when some other extension is available, or to\navoid a dependency loop. Extensions must form a directed acyclic graph,\nthere must not be a dependency path from one extension back to itself.\n\n## Use Cases\n\nGenerally the intended use case for `peerDependencies` is to provide\nfunctionality only when some other extension is present. For example,\nyou may have an extension that requires special code to interact with\nReact, but is also usable without React, so you could have\n`ReactProviderExtension` in the `peerDependencies` array.\n\nAnother example would be to provide Markdown serialization or import\nfor an extension that provides some custom node type, but only\nif the `MdastExtension` is present in that editor.\n","packages/lexical-website/docs/extensions/react.md":"# React and Lexical Extension\n\nLexical Extension vastly improved support for non-React usage, but\ndid not make any compromises when supporting React.\n\n## Minimal migration to LexicalExtensionComposer\n\nMigrating your application from `LexicalComposer` to\n`LexicalExtensionComposer` is straightforward, but has\ntwo things to consider.\n\n### Convert your `initialConfig` to an `extension`\n\nBefore:\n```tsx\nconst initialConfig = {\n  namespace: 'MyEditor',\n  nodes: [QuoteNode, HeadingNode],\n  theme,\n  editorState(editor) {\n    editor.update($initialEditorState, {tag: HISTORY_MERGE_TAG});\n  },\n  // This is the default when using Lexical Extension\n  onError(err) {\n    throw err;\n  },\n};\n```\n\nAfter:\n```tsx\n// Be careful to make sure that this is a stable reference, either by defining it\n// at the top-level of a module or by using something like `useRef` or `useState`\n// to create it. Any time this object changes, the editor will be recreated.\nconst appExtension = defineExtension({\n  name: 'MyEditor',\n  namespace: 'MyEditor',\n  nodes: () => [QuoteNode, HeadingNode],\n  theme,\n  $initialEditorState,\n});\n```\n\n### Switch from LexicalComposer to LexicalExtensionComposer\n\nBefore:\n```tsx\n<LexicalComposer initialConfig={initialConfig}>\n    <RichTextPlugin\n        contentEditable={<ContentEditable />}\n        ErrorBoundary={LexicalErrorBoundary}\n    />\n    {/* other legacy React plugins */}\n</LexicalComposer>\n```\n\nAfter:\n```tsx\n<LexicalExtensionComposer extension={appExtension} contentEditable={null}>\n    <RichTextPlugin\n        contentEditable={<ContentEditable />}\n        ErrorBoundary={LexicalErrorBoundary}\n    />\n    {/* other legacy React plugins */}\n</LexicalExtensionComposer>\n```\n\nThis is a minimal migration, in most cases you can migrate plug-in usage to\nextension dependencies. For example, by using\n[RichTextExtension](/docs/api/modules/lexical_rich-text#richtextextension) instead of\n`RichTextPlugin`:\n\n```tsx\nconst appExtension = defineExtension({\n  name: 'MyEditor',\n  namespace: 'MyEditor',\n  dependencies: [RichTextExtension],\n  theme,\n  $initialEditorState,\n});\n```\n\n```tsx\n<LexicalExtensionComposer extension={appExtension}>\n  {/* By default the contentEditable is the first child, defaulting to <ContentEditable /> */}\n  {/* other legacy React plugins */}\n</LexicalExtensionComposer>\n```\n\n## Managing the editor yourself with LexicalExtensionEditorComposer\n\n`LexicalExtensionComposer` builds its editor from `extension` and disposes it\nwhen the component unmounts. When you want to own that lifecycle instead,\nsuch as a nested editor that belongs to a node (like the caption in the\nplayground's `StickyComponent`), an editor shared with non-React code, or one\nthat should outlive the component, build the editor yourself and render it\nwith\n[`LexicalExtensionEditorComposer`](/docs/api/modules/lexical_react_LexicalExtensionEditorComposer).\n\nThe editor must include `ReactProviderExtension` and `ReactExtension`, which\n`LexicalExtensionComposer` otherwise adds for you:\n\n```tsx\nimport {buildEditorFromExtensions, defineExtension} from '@lexical/extension';\nimport {LexicalExtensionEditorComposer} from '@lexical/react/LexicalExtensionEditorComposer';\nimport {ReactExtension} from '@lexical/react/ReactExtension';\nimport {ReactProviderExtension} from '@lexical/react/ReactProviderExtension';\nimport {RichTextExtension} from '@lexical/rich-text';\n\n// Built once, at module scope, so it is not re-created on every render\nconst editor = buildEditorFromExtensions(\n  defineExtension({\n    dependencies: [ReactProviderExtension, ReactExtension, RichTextExtension],\n    name: '@my-app/editor',\n  }),\n);\n\nfunction App() {\n  return (\n    <LexicalExtensionEditorComposer initialEditor={editor}>\n      {/* The ContentEditable is rendered by ReactExtension, as with LexicalExtensionComposer */}\n      {/* other legacy React plugins */}\n    </LexicalExtensionEditorComposer>\n  );\n}\n```\n\n`LexicalExtensionEditorComposer` never disposes the editor, so call\n`editor.dispose()` yourself when you are done with it. Keep the\n`initialEditor` prop stable (module scope, `useMemo`, or state held by a\nnode), and render a given editor in only one place at a time, since an editor\nhas only one root element.\n\n## React Plug-ins (Legacy)\n\nThese just work as-is, by rendering them as children of your composer. You may\nwish to package them as an extension for easier re-use, or consider refactoring\nthem to work without React. See [Extension Decorators](#extension-decorators)\nand [Output Components](#output-components), and\n[React Independence](#react-independence) for strategies to repackage or\nrefactor your legacy plug-ins.\n\n## Extension Decorators\n\nA common use case for legacy react plug-ins is to render some React component,\nbut the position of that component in the document is not important. Some use\ncases for this are when the component is simply an effect that always returns\n`null`, or it is rendering something like a portal. There's no need for users\nto manually decide where these components are rendered, so Lexical's\nReactExtension maintains an array of these to render automatically.\n\n ```tsx\n // This is exactly how you would write a legacy React plug-in\nfunction LogEditorPlugin() {\n  const [editor] = useLexicalComposerContext()\n  useEffect(() => {\n    console.log(editor);\n  }, [editor]);\n  return null;\n}\n// This is how you would declare the plugin as an extension\n// that depends on React and renders this plug-in as a decorator\nexport const LogEditorExtension = defineExtension({\n  name: \"@example/LogEditor\",\n  dependencies: [\n    configExtension(ReactExtension, {\n      decorators: [<LogEditorPlugin />]\n    }),\n  ],\n});\n```\n\n## Output Components\n\nIn situations where it is important for the user to decide exactly where an\nextension's component is rendered in the React document, the convention is\nto provide an Output Component.\n\nHere we have the TreeViewExtension which defines an Output Component to render\nthe TreeView with the specified configuration.\n\n```tsx\nexport function TreeViewExtensionComponent(\n  props: Partial<TreeViewConfig>,\n): JSX.Element {\n  const [editor] = useLexicalComposerContext();\n  return (\n    <TreeView\n      editor={editor}\n      {...useExtensionDependency(TreeViewExtension).config}\n      {...props}\n    />\n  );\n}\n\nexport const TreeViewExtension = defineExtension({\n  build: () => ({Component: TreeViewExtensionComponent}),\n  config,\n  dependencies: [ReactExtension],\n  name: '@lexical/react/TreeView',\n});\n```\n\nThis extension component can be acquired with the\n[useExtensionComponent](/docs/api/modules/lexical_react_useExtensionComponent#useextensioncomponent)\nhook or the [ExtensionComponent](/docs/api/modules/lexical_react_ExtensionComponent#extensioncomponent) component.\n\n```tsx\nreturn (\n  <ExtensionComponent\n    lexical:extension={TreeViewExtension}\n    viewClassName=\"tree-view-output\" />\n);\n```\n\n```tsx\nconst TreeViewComponent = useExtensionComponent(TreeViewExtension);\nreturn (<TreeViewComponent viewClassName=\"tree-view-output\" />);\n```\n\n## React Independence\n\nMany legacy plug-ins are merely a `useEffect` wrapper, these tend to not\nneed React at all. For example:\n\nBefore:\n```tsx\nexport function CheckListPlugin(): null {\n  const [editor] = useLexicalComposerContext();\n  useEffect(() => {\n    return registerCheckList(editor);\n  }, [editor]);\n  return null;\n}\n```\n\nAfter:\n```tsx\nexport const CheckListExtension = defineExtension({\n  name: \"@lexical/list/CheckList\",\n  register: registerCheckList,\n});\n```\n\n## Using React extensions and plug-ins without JSX\n\n[ReactPluginHostExtension](/docs/api/modules/lexical_react_ReactPluginHostExtension#reactpluginhostextension)\nis an extension that allows you to mount the a React root at a\nspecific DOM element in your app so that applications that are\nnot natively React can still take advantage of existing legacy\nplug-ins and extensions that depend on React.\n\nWhile this does mean your application will include the React runtime, you\ndon't have to use JSX or have any other React infrastructure in your app.\n\nSee\n[extension-vanilla-react-plugin-host](https://github.com/facebook/lexical/blob/main/examples/extension-vanilla-react-plugin-host/src/main.ts)\n[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/facebook/lexical/tree/main/examples/?file=src%2Fmain.ts)\nfor usage of this extension and its API\n","packages/lexical-website/docs/extensions/signals.md":"# Signals\n\nLexical Extensions uses signals as the reactive interface that you can use to\ncommunicate with and between extensions.\n\n## Motivation\n\nAt the lowest level, communicating between parts of Lexical is done with\ncallbacks such as `registerCommand`, `registerUpdateListener`,\n`registerMutationListener`, and `addEventListener` with the DOM.\n\nHow these callbacks behave and in some cases whether they are registered\nat all depends on state local to the extension that may also be mutable,\nso would require a similar callback mechanism with a naïve implementation.\nWith legacy React plug-ins, this mechanism is `useEffect` which is very\ncoarse and doesn't really have a portable equivalent when using other\nor no framework.\n\nThe initial prototype was largely callback based as well, but this quickly\nbecomes very difficult to work with when you have to do work based on\nthe \"result\" of multiple callbacks or configuration values. Signals are\na more general solution to this problem. There's even a\n[TC39 Signals Proposal](https://github.com/tc39/proposal-signals) to add\ninfrastructure for this to the JavaScript standard.\n\nSignals also allow for optimizations by reducing redundant callbacks with\nprimitives such as `batch` or by allowing the current value to be inspected\nwith `peek` without adding any subscription. Basically, the same Signal can\nbe used either like React State when you want to run code as the value changes\nover time, or like a React Ref when you only need to know the current value\n(as is typical with event handlers).\n\nSignals also do not require you to manage a dependency array or use a\ncompiler, the subscriptions and dependencies are inferred at runtime\nas they are used. This means that the set of signals subscribed to by a\ngiven effect can change without violating any rules or generating any\nwarnings.\n\n## Implementation\n\nSince Signals are not yet standardized, and the\n[signal-polyfill](https://github.com/proposal-signals/signal-polyfill) is not\nready for production use, it was decided to leverage\n[@preact/signals-core](https://github.com/preactjs/signals). This\nimplementation is dependency-free, optimized, and well tested.\n\nFor future compatibility we *do not recommend* that you use\n`@preact/signals-core` directly, and instead use the re-exports from\n[@lexical/extension](https://lexical.dev/docs/api/modules/lexical_extension).\n\nAs of the initial release, these re-exports are:\n\n```ts\nexport {\n  batch,\n  computed,\n  effect,\n  type ReadonlySignal,\n  type Signal,\n  signal,\n  type SignalOptions,\n  untracked,\n} from '@preact/signals-core';\n```\n\n## Usage\n\nFor thorough examples on how the signal primitives work, you can read through the\n[Preact Signals](https://preactjs.com/guide/v10/signals/) documentation.\n\nLexical provides the following higher-level signal functions to make it easier\nto build extensions:\n\n### namedSignals\n\n[namedSignals](/docs/api/modules/lexical_extension#namedsignals)\nis a convenience function used to output independent signals from\neach property of a configuration object.\n\n```ts\nexport interface TabIndentationConfig {\n  disabled: boolean;\n  maxIndent: null | number;\n  /**\n   * By default, indents are set on all elements for which the\n   * {@link ElementNode.canIndent} returns true. This option allows you to set\n   * indents for specific nodes without overriding the method for others.\n   */\n  $canIndent: CanIndentPredicate;\n}\n\nexport const TabIndentationExtension = defineExtension({\n  build(editor, config, state) {\n    return namedSignals(config);\n  },\n  config: safeCast<TabIndentationConfig>({\n    $canIndent: $defaultCanIndent,\n    disabled: false,\n    maxIndent: null,\n  }),\n  name: '@lexical/extension/TabIndentation',\n  register(editor, config, state) {\n    const {disabled, maxIndent, $canIndent} = state.getOutput();\n    return effect(() => {\n      if (!disabled.value) {\n        // This register function's implementation uses peek on the maxIndent\n        // signal to get its current value when needed, no need to re-register\n        // every time this value changes.\n        return registerTabIndentation(editor, maxIndent, $canIndent);\n      }\n    });\n  },\n});\n```\n\n### watchedSignal\n\n[watchedSignal](/docs/api/modules/lexical_extension#watchedsignal)\nis equivalent to React's [useSyncExternalStore](https://react.dev/reference/react/useSyncExternalStore)\n\n```ts\nexport const EditorStateExtension = defineExtension({\n  build(editor) {\n    return watchedSignal(\n      () => editor.getEditorState(),\n      (editorStateSignal) =>\n        editor.registerUpdateListener((payload) => {\n          editorStateSignal.value = payload.editorState;\n        }),\n    );\n  },\n  name: '@lexical/extension/EditorState',\n});\n```\n","packages/lexical-website/docs/faq.md":"---\nsidebar_position: 7\n---\n\n# FAQ\n\n## Why does Lexical use the `$` prefix in the name of many of the functions?\n\nOriginally, Lexical didn't have `$` functions, instead these functions were provided to you through callback params:\n\n```js\n// Mid 2020 API\neditor.update((viewState) => {\n  const getRoot = viewState.getRoot();\n  ..\n});\neditor.addTextTransform((viewState) => {\n  const getRoot = viewState.getRoot();\n  ..\n});\n```\n\nInternally, this approach raised some negative feedback:\n\n- The `viewState` terminology was confusing. It wasn't really a `viewState`, more like a toolkit to manipulate the `EditorState`\n- For complex updates and transforms devs had to carry params around on many layers\n\nThis is when we decided to leverage the \"lexical\" scope instead to perform `EditorState` manipulation, and the `$` represents just that.\n\n```js\neditor.update(() => ...);\neditor.registerNodeTransform(FooNode, () => ...);\neditor.getEditorState().read(...);\n```\n\nIf you've used React Hooks before, you can think of `$` functions as being something that follows a similar pattern. These are functions that show their intent as to where they can or cannot be used. This makes it possible for a developer to create their own functions that give the same signal, by simply prefixing the function with the dollar.\n\nInternally, we've found this scales really well and developers get to grips with it in almost no time at all.\n\n## When does reconciliation happen?\n\nReconciliation is scheduled with\n[queueMicrotask](https://developer.mozilla.org/en-US/docs/Web/API/queueMicrotask),\nwhich means that it will happen very soon, but asynchronously. This is similar\nto something like `setTimeout(reconcile, 0)` with a bit more immediacy or\n`Promise.resolve().then(reconcile)` with less overhead. This is done so\nthat all of the updates that occur as a result of a single logical event will\nbe batched into one reconciliation.\n\nYou can force a reconciliation to take place synchronously with the discrete\noption to `editor.update` (demonstrated below).\n\n## Why do tests use `await editor.update(…)`\n\nYou may notice that many tests look like this:\n\n```js\nawait editor.update(updateA);\nawait editor.update(updateB);\n```\n\nAn astute observer would notice that this seems very strange, since\n`editor.update()` returns `void` and not `Promise<void>`. However,\nit does happen to work as you would want it to because\nthe implementation of Promise uses the same microtask queue.\n\nIt's not recommended to rely on this in browser code as it could depend on\nimplementation details of the compilers, bundlers, and VM. It's best to stick\nto using the `discrete` or the `onUpdate` callback options to be sure that\nthe reconciliation has taken place.\n\nIgnoring any other microtasks that were scheduled elsewhere,\nit is roughly equivalent to this synchronous code:\n\n```js\neditor.update(updateA, {discrete: true});\neditor.update(updateB, {discrete: true});\n```\n\nAt a high level, very roughly, the order of operations looks like this:\n\n1. `editor.update()` is called\n2. `updateA()` is called and updates the editor state\n3. `editor.update()` schedules a reconciliation microtask and returns\n4. `await` schedules a resume microtask and yields control to the task executor\n5. the reconciliation microtask runs, reconciling the editor state with the DOM\n6. the resume microtask runs\n\n## How do I listen for user text insertions?\n\nListening to text insertion events is problematic with content editables in general. It's a common source of bugs due to how\ndifferent browsers and third-party extensions interact with the DOM. Whilst it's possible to use DOM events like `input` and\n`beforeinput` to gauge some of the possible cases where a user has inserted text, these are hardly reliable and also don't\ntake into account edge-cases. Instead, Lexical prefers to consider any change as a possible user input, and as such doesn't\nmake a distinction between the cases. This is important for tools like spellcheck, browser extensions, IME, speech-to-text,\nscreen readers and other external tools that often don't reliably trigger a reliable event sequence (some don't even trigger\nany events at all!).\n\nFor those wanting to react to a text change and possibly block/alter the intent, the recommended approach is to use a node\ntransform. This also plays nicely with other sub-systems at play that might also be looking to do the same thing as you.\n\nFor those who just want to know of the changes, this can be achieved using a text content listener or an editor update listener.\n\n## How do I clear the contents of the editor?\n\nYou can go this by calling ```clear()``` on the RootNode in an update callback:\n\n```js\neditor.update(() => {\n  $getRoot().clear();\n})\n```\n\n## How do I listen to specific key down events?\n\nYou can leverage Lexical's command listening system. Lexical provides specific commands for many common keyboard operations, such as:\n\n- `KEY_ARROW_LEFT_COMMAND`\n- `KEY_ARROW_RIGHT_COMMAND`\n- `KEY_ARROW_UP_COMMAND`\n- `KEY_ARROW_DOWN_COMMAND`\n- `KEY_SPACE_COMMAND`\n- `KEY_ENTER_COMMAND`\n- `KEY_BACKSPACE_COMMAND`\n- `KEY_DELETE_COMMAND`\n- `KEY_TAB_COMMAND`\n- `KEY_ESCAPE_COMMAND`\n\n```js\nimport {KEY_ENTER_COMMAND, COMMAND_PRIORITY_LOW} from 'lexical';\n\neditor.registerCommand(KEY_ENTER_COMMAND, (event: KeyboardEvent) => {\n  // Handle enter key presses here\n  return false;\n}, COMMAND_PRIORITY_LOW);\n```\n\nYou can use the generic `KEY_DOWN_COMMAND` command to listen\nto all keydown events. Do note, that returning `true` in your listener will prevent any\nother key based commands from firing, so in most cases you'll want to return `false` from\nthe command listener.\n\n```js\nimport {KEY_DOWN_COMMAND, COMMAND_PRIORITY_LOW} from 'lexical';\n\neditor.registerCommand(KEY_DOWN_COMMAND, (event: KeyboardEvent) => {\n  // Handle event here\n  return false;\n}, COMMAND_PRIORITY_LOW)\n```\n\n:::tip\n\nIf you do return `true` from any listener with an event payload, you likely\nalso should call `event.preventDefault()` unless your command relies on the\nbrowser's native processing of that event.\n\n:::\n\n## Why do I get \"does not implement .getType()\" or \"does not match registered node\"?\n\n```\nLexicalNode: Node FooNode does not implement .getType().\nCreate node: Type heading in node HeadingNode does not match registered node HeadingNode with the same type\n```\n\nThese almost always mean your app resolved **two copies of `lexical`**, not that\nthe node is misdeclared — the `instanceof` and registration checks compare class\nreferences, and the two copies have different ones. Run `npm ls lexical` (or\n`pnpm why lexical`) from the app root: it must report exactly one version.\nSee [One Lexical per app](concepts/one-lexical-per-app.md) for the causes and\nthe fix, including why a library must declare `lexical` in `peerDependencies`.\n\n## Which module formats are published? (ESM, CommonJS, Node.js, React Native)\n\nEvery Lexical package is published as ES modules only. The packages declare\n`\"type\": \"module\"`, and their `exports` maps offer a `development` and a\n`production` build plus a `default` entry that picks one of the two at runtime\nfrom `process.env.NODE_ENV`.\n\n- **Bundlers** (webpack, Vite, Rollup, esbuild with the conditions set) resolve\n  the `development` or `production` condition and get exactly one build.\n- **Node.js** resolves `default`, which loads both builds and exports one. To\n  load a single build, pass the condition on the command line:\n  `node --conditions=production app.js` (or `--conditions=development`).\n- **CommonJS** code can `require()` the packages on Node.js 20.19 or later,\n  which loads ES modules from `require()` as long as nothing in the module\n  graph uses top-level `await` (Lexical's builds do not). Older Node.js\n  versions have to use `await import('lexical')` instead. Loading is all\n  that `require()` guarantees: Lexical's builds import their own\n  dependencies as ES modules, so a dependency that a CommonJS application\n  also loads through `require()` can end up in the application twice, once\n  per module system, with separate classes and module state. That breaks\n  any integration that hands such a dependency's objects to Lexical, for\n  example `yjs` documents given to `@lexical/yjs`, or `@preact/signals-core`\n  effects observing signals from `@lexical/extension`. Load those\n  dependencies as ES modules too (`await import('yjs')`), or move the\n  application to ES modules.\n- **React Native (Metro)** bundles the packages without extra configuration,\n  but Metro does not set the `development`/`production` conditions and does\n  not drop the build the `default` entry leaves unused, so both end up in the\n  bundle. To bundle only the one that matches the build mode, add the\n  condition for Lexical's packages in `metro.config.js`:\n\n  ```js\n  const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config');\n\n  module.exports = mergeConfig(getDefaultConfig(__dirname), {\n    resolver: {\n      resolveRequest: (context, moduleName, platform) =>\n        context.resolveRequest(\n          /^(lexical|@lexical\\/)/.test(moduleName)\n            ? {\n                ...context,\n                unstable_conditionNames: [\n                  ...context.unstable_conditionNames,\n                  context.dev ? 'development' : 'production',\n                ],\n              }\n            : context,\n          moduleName,\n          platform,\n        ),\n    },\n  });\n  ```\n\n","packages/lexical-website/docs/getting-started/_category_.json":"{\n  \"label\": \"Getting Started\",\n  \"position\": 2\n}\n","packages/lexical-website/docs/getting-started/devtools.md":"---\nsidebar_label: \"Developer Tools\"\n---\n\n# Lexical Developer Tools\n\n## Extension\n\nThe easiest way to debug websites built with Lexical is to install the Lexical Developer Tools browser extension. It is available for several popular browsers:\n\n\n[link-chrome]: https://chromewebstore.google.com/detail/lexical-developer-tools/kgljmdocanfjckcgfpcpdoklodllfdpc 'Version published on Chrome Web Store'\n[link-firefox]: https://addons.mozilla.org/en-US/firefox/addon/lexical-developer-tools/ 'Version published on Mozilla Add-ons'\n[link-safari]: https://apps.apple.com/us/app/lexical-developer-tools/id6502753400 'Version published on Mac App Store'\n\n[<img src=\"https://cdnjs.cloudflare.com/ajax/libs/browser-logos/74.1.0/chrome/chrome.svg\" width=\"48\" alt=\"Chrome\" valign=\"middle\"/>][link-chrome] [<img valign=\"middle\" src=\"https://img.shields.io/chrome-web-store/v/kgljmdocanfjckcgfpcpdoklodllfdpc?style=flat&label=%20\n\"/>][link-chrome]\n\n[<img src=\"https://cdnjs.cloudflare.com/ajax/libs/browser-logos/74.1.0/firefox/firefox.svg\" width=\"48\" alt=\"Firefox\" valign=\"middle\"/>][link-firefox] [<img valign=\"middle\" src=\"https://img.shields.io/amo/v/lexical-developer-tools.svg?label=%20\"/>][link-firefox]\n\n[<img src=\"https://cdnjs.cloudflare.com/ajax/libs/browser-logos/74.1.0/safari/safari.svg\" width=\"48\" alt=\"Safari logo\" valign=\"middle\"/>][link-safari] [<img valign=\"middle\" src=\"https://img.shields.io/itunes/v/6502753400?label=%20\"/>][link-safari]\n\nNow, if you visit a website built with Lexical (such as [https://playground.lexical.dev/](https://playground.lexical.dev/)), you will see the _Lexical_ panel in your browser Inspector window.\n\n<img src=\"/img/docs/devtools-demo.png\" width=\"460\" alt=\"Lexical Developer Tools Demo screenshot\"/>\n\n## Extension Support\n\nFeel free to reach out to us for support, or just to chat, at [EPAM-lexical-support@epam.com](mailto:EPAM-lexical-support@epam.com) or via our [community channels](/community).\n\nLegal information:\n\n- [Meta Open Source - Privacy Policy](https://opensource.facebook.com/legal/privacy/)\n- [Meta Open Source - Terms of Use](https://opensource.facebook.com/legal/terms/)\n","packages/lexical-website/docs/getting-started/supported-browsers.md":"---\nsidebar_position: 4\n---\n\n# Supported Browsers\n\n> Note: Lexical does not support Internet Explorer or legacy versions of Edge.\n\n- Firefox 115+\n- Chrome 86+ (and other browsers based on Chrome 86+ such as Android Chrome 86+, Edge 86+ and Opera 72+)\n- Safari 15+ (iOS 15+, iPad OS 15+)\n","packages/lexical-website/docs/getting-started/theming.md":"---\nsidebar_position: 3\n---\n\n# Theming\n\nLexical tries to make theming straight-forward, by providing a way of passing a customizable theming object that maps CSS class names to the editor on creation. Here's an example of a plain-text theme:\n\n```js\nconst exampleTheme = {\n  paragraph: 'editor-paragraph',\n};\n```\n\nIn your CSS, you can then add something like:\n\n```css\n.editor-placeholder {\n  color: #999;\n  overflow: hidden;\n  position: absolute;\n  top: 15px;\n  left: 15px;\n  user-select: none;\n  pointer-events: none;\n}\n\n.editor-paragraph {\n  margin: 0 0 15px 0;\n  position: relative;\n}\n```\n\nPut the theme on your root extension. The same theme configuration works with\nReact and vanilla JavaScript:\n\n```js\nimport {PlainTextExtension} from '@lexical/plain-text';\nimport {defineExtension} from 'lexical';\n\nexport const appExtension = defineExtension({\n  name: 'MyEditor',\n  namespace: 'MyEditor',\n  dependencies: [PlainTextExtension],\n  theme: exampleTheme,\n});\n```\n\nIn React, pass that stable extension to `LexicalExtensionComposer`. This example\nplaces its own `ContentEditable`, so it disables the composer's default one:\n\n```jsx\nimport {ContentEditable} from '@lexical/react/LexicalContentEditable';\nimport {LexicalExtensionComposer} from '@lexical/react/LexicalExtensionComposer';\nimport {appExtension} from './appExtension';\n\nexport default function Editor() {\n  return (\n    <LexicalExtensionComposer extension={appExtension} contentEditable={null}>\n      <div style={{position: 'relative'}}>\n        <ContentEditable\n          aria-label=\"Plain text editor\"\n          aria-placeholder=\"Enter some text...\"\n          placeholder={<div className=\"editor-placeholder\">Enter some text...</div>}\n        />\n      </div>\n    </LexicalExtensionComposer>\n  );\n}\n```\n\nIn vanilla JavaScript, build the editor from the same extension and attach it to\nyour editable element:\n\n```js\nimport {buildEditorFromExtensions} from '@lexical/extension';\nimport {appExtension} from './appExtension';\n\nconst editor = buildEditorFromExtensions(appExtension);\neditor.setRootElement(document.getElementById('editor'));\n// Call editor.dispose() when removing this editor permanently.\n```\n\nA theme supplies class names; it does not install features or include CSS. For\nexample, to use the heading and quote styles below, add `RichTextExtension` in\nplace of `PlainTextExtension`. Add the corresponding extensions for lists, links,\nand code highlighting when you need those features.\n\n## Merging extension themes\n\nEvery extension can supply a `theme`. Lexical recursively merges these themes in\ndependency order: dependencies contribute first, and extensions that depend on\nthem can override individual entries. Your root extension can therefore customize\nan extension's styles while keeping the rest of its theme, including nested\nentries such as `text` and `heading`.\n\n```js\nconst EmphasisExtension = defineExtension({\n  name: 'Emphasis',\n  theme: {\n    text: {\n      bold: 'emphasis-bold',\n      italic: 'emphasis-italic',\n    },\n  },\n});\n\nconst appExtension = defineExtension({\n  name: 'MyEditor',\n  dependencies: [PlainTextExtension, EmphasisExtension],\n  theme: {\n    paragraph: 'editor-paragraph',\n    text: {bold: 'editor-bold'},\n  },\n});\n\n// The merged theme is:\n// {\n//   paragraph: 'editor-paragraph',\n//   text: {bold: 'editor-bold', italic: 'emphasis-italic'},\n// }\n```\n\nWhen two themes set the same entry, the later value replaces the earlier one;\nclass-name strings are not concatenated. There is no need to spread a dependency's\ntheme into your own.\n\n## Available theme entries\n\nMany of the Lexical's core nodes also accept theming properties. Here's a more comprehensive theming object:\n\n```js\nconst exampleTheme = {\n  paragraph: 'editor-paragraph',\n  quote: 'editor-quote',\n  heading: {\n    h1: 'editor-heading-h1',\n    h2: 'editor-heading-h2',\n    h3: 'editor-heading-h3',\n    h4: 'editor-heading-h4',\n    h5: 'editor-heading-h5',\n    h6: 'editor-heading-h6',\n  },\n  list: {\n    nested: {\n      listitem: 'editor-nested-listitem',\n    },\n    ol: 'editor-list-ol',\n    ul: 'editor-list-ul',\n    listitem: 'editor-listItem',\n    listitemChecked: 'editor-listItemChecked',\n    listitemUnchecked: 'editor-listItemUnchecked',\n  },\n  hashtag: 'editor-hashtag',\n  image: 'editor-image',\n  link: 'editor-link',\n  text: {\n    bold: 'editor-textBold',\n    code: 'editor-textCode',\n    italic: 'editor-textItalic',\n    strikethrough: 'editor-textStrikethrough',\n    subscript: 'editor-textSubscript',\n    superscript: 'editor-textSuperscript',\n    underline: 'editor-textUnderline',\n    underlineStrikethrough: 'editor-textUnderlineStrikethrough',\n  },\n  code: 'editor-code',\n  codeHighlight: {\n    atrule: 'editor-tokenAttr',\n    attr: 'editor-tokenAttr',\n    boolean: 'editor-tokenProperty',\n    builtin: 'editor-tokenSelector',\n    cdata: 'editor-tokenComment',\n    char: 'editor-tokenSelector',\n    class: 'editor-tokenFunction',\n    'class-name': 'editor-tokenFunction',\n    comment: 'editor-tokenComment',\n    constant: 'editor-tokenProperty',\n    deleted: 'editor-tokenProperty',\n    doctype: 'editor-tokenComment',\n    entity: 'editor-tokenOperator',\n    function: 'editor-tokenFunction',\n    important: 'editor-tokenVariable',\n    inserted: 'editor-tokenSelector',\n    keyword: 'editor-tokenAttr',\n    namespace: 'editor-tokenVariable',\n    number: 'editor-tokenProperty',\n    operator: 'editor-tokenOperator',\n    prolog: 'editor-tokenComment',\n    property: 'editor-tokenProperty',\n    punctuation: 'editor-tokenPunctuation',\n    regex: 'editor-tokenVariable',\n    selector: 'editor-tokenSelector',\n    string: 'editor-tokenSelector',\n    symbol: 'editor-tokenProperty',\n    tag: 'editor-tokenProperty',\n    url: 'editor-tokenOperator',\n    variable: 'editor-tokenVariable',\n  },\n};\n```\n","packages/lexical-website/docs/intro.md":"---\nsidebar_position: 1\n---\n\n# Introduction\n\nLexical is an extensible text editor framework for the web, built for\nreliability, accessibility, and performance. It gives you a small,\ndependency-free core and a set of optional packages that you compose into the\neditor you need, from a plain-text input with mentions to a collaborative\nrich-text document editor.\n\nLexical attaches to a `contenteditable` element and keeps its own model of the\ndocument. You read and change that model with Lexical's APIs, and Lexical\ntakes care of keeping the DOM, the selection, and the browser's many\n`contenteditable` quirks in sync. Most code never touches the DOM directly;\nthe main exception is custom nodes, which define how they render.\n\n## What can you build? {#what-can-be-built-with-lexical}\n\n- Plain-text inputs that need more than a `<textarea>`: mentions, hashtags,\n  links, custom emoji.\n- Rich-text editors for comments, posts, and messages.\n- Full document editors with tables, lists, code blocks, and images, for a CMS\n  or a notes app.\n- Real-time collaborative editing with shared content and remote cursors,\n  using the [Yjs integration](./collaboration/react.md).\n\nLexical supplies the editing infrastructure. Your application supplies the\nlayout, toolbars, menus, styling, and storage.\n\nLexical is an open-source community project. It began at Meta, and its\ncontributors today include Meta engineers alongside many volunteers and\ndevelopers from other companies. It powers text editing in Meta's web products\nand at [Bloomberg](https://www.bloomberg.com), and is also the editor behind\n[Payload CMS](https://payloadcms.com),\n[Supabase](https://supabase.com),\n[Proton Docs](https://proton.me/drive/document-editor),\n[MDXEditor](https://mdxeditor.dev), [Sveltia CMS](https://sveltiacms.app),\n[Dify](https://dify.ai), [RAGFlow](https://ragflow.io),\n[DeepSeek Harness](https://deepseek.com/harness/),\n[Paperclip](https://paperclip.ing), and [Lexxy](https://lexxy.dev). To see\nwhat it can do, try the [playground](https://playground.lexical.dev).\n\n## How it fits together\n\nThe `lexical` package is the core: the editor, the editor state, the base\nnode types, selection, commands, and the DOM reconciler. Everything else is an\noptional package built on top of it, so an application only includes the\nfeatures it uses. The core is framework-agnostic, and `@lexical/react`\nprovides React bindings.\n\n```mermaid\nflowchart TB\n  accTitle: How Lexical's packages fit together\n  accDescr: Your application uses optional packages such as rich text, history, lists, tables, and React bindings. Both the application and those packages build on the lexical core, which contains the editor, the editor state, commands, and the DOM reconciler.\n  app[\"Your application\"]\n  subgraph features[\"Optional packages\"]\n    rich[\"@lexical/rich-text<br/>@lexical/plain-text\"] ~~~ more[\"@lexical/history<br/>@lexical/list<br/>@lexical/table<br/>…\"] ~~~ bindings[\"@lexical/react\"]\n  end\n  subgraph core[\"lexical (core)\"]\n    editor[\"Editor\"] ~~~ state[\"EditorState<br/>(node tree + selection)\"] ~~~ commands[\"Commands\"] ~~~ reconciler[\"DOM reconciler\"]\n  end\n  app --> features\n  app --> core\n  features --> core\n```\n\nFeatures are added to an editor as [extensions](./extensions/intro.md). An\nextension bundles everything a feature needs (its nodes, configuration,\ncommands, and listeners, plus any extensions it depends on) so it can be\nadded in one place:\n\n```ts\nimport {buildEditorFromExtensions} from '@lexical/extension';\nimport {HistoryExtension} from '@lexical/history';\nimport {RichTextExtension} from '@lexical/rich-text';\nimport {defineExtension} from 'lexical';\n\nconst editor = buildEditorFromExtensions(\n  defineExtension({\n    dependencies: [RichTextExtension, HistoryExtension],\n    name: '@my-app/editor',\n    namespace: 'my-app',\n  }),\n);\neditor.setRootElement(document.getElementById('editor'));\n```\n\nIn React, [`LexicalExtensionComposer`](./extensions/react.md) does the same\njob. The [Quick Start](/docs/getting-started/quick-start) and\n[React guide](/docs/getting-started/react) walk through a complete setup.\n\n## Core concepts {#lexicals-design}\n\nThese are the ideas the rest of the documentation builds on. Each one links to\na page with the details.\n\n### Editor {#editor-instances}\n\nThe editor wires everything together. It owns the current editor state,\nattaches to a root DOM element, and is where you register nodes, listeners,\ntransforms, and commands. You usually create it with\n`buildEditorFromExtensions` or through the React bindings rather than calling\n`createEditor()` yourself.\n\n### Editor state {#editor-states}\n\nAn [editor state](./concepts/editor-state.md) is an immutable snapshot of the\ndocument: a tree of [nodes](./concepts/nodes.mdx) under a single `RootNode`,\nplus a [selection](./concepts/selection.md). The editor state, not the DOM, is\nthe source of truth. Its `toJSON()` output is how you save and restore content.\n\n### A DOM-like document tree {#document-model}\n\nLexical's node tree is shaped like the HTML it renders. A paragraph contains\nits text, and a link is an element that contains the text it wraps. See\n[Document Model](./concepts/document-model.md), and\n[Compared with ProseMirror](./concepts/compared-with-prosemirror.md) for how\nthis differs from a mark-based editor.\n\n### Reading and updating editor state {#reading-and-updating-editor-state}\n\nAll reads and writes happen inside a synchronous callback, such as\n`editor.update(fn)` or `editor.read('force-commit', fn)`. Functions whose\nnames start with `$`, such as `$getRoot()`, only work inside those callbacks,\nin the same way React Hooks only work while a component renders. See\n[Editor State](./concepts/editor-state.md#updating-state).\n\n### Updates and the DOM reconciler {#dom-reconciler}\n\nUpdates change a pending copy of the editor state. Updates in the same tick\nare batched, and when the batch commits, the reconciler patches only the DOM\nthat belongs to changed nodes. Lexical also watches for DOM changes made\noutside it and keeps or reverts them. [Updates](./concepts/updates.mdx) walks\nthrough each phase, with an interactive example.\n\n### Commands, transforms, and listeners {#listeners-node-transforms-and-commands}\n\nMost editor behavior is built from three kinds of hooks.\n[Commands](./concepts/commands.md) carry user input and your own actions to\nhandlers in priority order. [Node transforms](./concepts/transforms.md) run\nduring an update to keep the document in shape.\n[Listeners](./concepts/listeners.md) react after an update is committed.\n\n### Serialization and running on a server\n\nContent converts to and from JSON, HTML, and Markdown; see\n[Serialization](./serialization/serialization.md). An editor with no root\nelement does no DOM work at all, so the same code can run on a server or in\ntests; see [Running Without a Browser](./concepts/headless.md).\n\n## Get started\n\n- [Quick Start](/docs/getting-started/quick-start) builds an editor without a\n  framework, and [Getting Started with React](/docs/getting-started/react) does\n  the same in React.\n- [Lexical Extensions](./extensions/intro.md) and\n  [Included Extensions](./extensions/included-extensions.md) cover how to add\n  features.\n- The [playground](https://playground.lexical.dev) shows many features working\n  together.\n","packages/lexical-website/docs/maintainers-guide-link.md":"---\nsidebar_position: 99\n---\n\n# Developing against a local Lexical checkout\n\nIf you're iterating on Lexical itself and want a downstream app to pick\nup your changes without publishing, the Lexical packages support two\nlinking workflows.\n\nBoth rely on the fact that each `packages/<name>/` directory is its own\npublishable npm package: after a build, `packages/lexical/package.json`\nplus the `dist/` it sits next to is a complete tarball. There is no\nintermediate `npm/` directory and no copy step — `pnpm pack` /\n`pnpm publish` run straight from the package root.\n\n## Option A: built artifacts (works for any consumer)\n\nBuild the artifacts, then point your downstream project at the package\ndirectory:\n\n```bash\n# in the Lexical checkout\npnpm install\npnpm run build-release   # produces dist/<Name>.{dev,prod}.js + .d.ts + .js.flow\n\n# in your downstream app\npnpm add link:/path/to/lexical/packages/lexical\npnpm add link:/path/to/lexical/packages/lexical-react   # etc.\n```\n\n`pnpm` follows the `link:` protocol by creating a real symlink into your\n`node_modules/`, so subsequent rebuilds of Lexical (e.g.\n`pnpm run build` in the Lexical checkout) become visible immediately.\nStandard `exports` resolution picks up the `development` /\n`production` conditions, so dev builds load the unminified variants and\nprod builds load the minified ones automatically. Only ESM is published;\na CommonJS consumer loads the same files through `require(esm)`\n(Node.js 20.19+).\n\nIf you prefer the dev-only loop (faster builds, no minification), use\n`pnpm run build` instead of `build-release`. The fork modules emitted\nby a dev-only build re-export the `.dev` variant unconditionally; you\nwon't be able to `pnpm publish` from such a checkout because the\npublish guard will report missing `.prod` files.\n\n`file:/path/to/...` works too if your package manager treats it as a\nsymlink (modern pnpm does); npm copies the directory into\n`node_modules` instead, which loses any live-reload benefit but is\notherwise equivalent for one-shot installs.\n\n## Option B: TypeScript source (no Lexical build needed)\n\nFor the tightest dev loop — edit a Lexical source file and have your\ndownstream bundler pick it up on the next request — opt into the\n`source` export condition. Each public package's `exports` map exposes\nits `./src/<entry>.tsx?` next to the compiled `development`/`production`/\n`default` conditions, so a bundler configured with\n`resolve.conditions: ['source', …]` will load TypeScript directly out\nof the linked package.\n\nThe only requirement is **bundler support**: your consumer's bundler has to\nbe configured to pick up the `source` condition AND to transform\n`.ts`/`.tsx` from inside `node_modules`. Vite, Rspack, and Parcel can all be\ntold to do this; webpack needs a loader override for the linked package.\n\nThere is **no aliasing and no extra `define` to set up**. Internal utilities\nthe source depends on are a real (internal) dependency, `@lexical/internal`,\nwhich resolves through normal package resolution; the React/test helpers are\npackage-internal; and the dev/prod branch is `process.env.NODE_ENV !==\n'production'`, which every mainstream bundler substitutes by default. So\n`source` mode works without any `resolve.alias` entries or build-time globals\n— including for the transitive `@lexical/*` packages.\n\nOne thing the compiled bundles have that the sources do not is the\n`/* @__PURE__ */` annotations on module-scope calls to `defineExtension`,\n`createCommand`, `createState`, and friends. Lexical's own build injects\nthem, and without them a bundler has to assume those calls have side effects\nand will keep every extension, command, and rule definition your app never\nuses. Add [`@lexical/compiler`](https://www.npmjs.com/package/@lexical/compiler)\nto your build to get the same annotations out of `source` mode — it annotates\nthe factory calls in your own code too.\n\n### Minimal Vite setup\n\n```ts\n// vite.config.ts\nimport {pureAnnotations} from '@lexical/compiler/PureAnnotations';\nimport {defineConfig} from 'vite';\n\nexport default defineConfig({\n  plugins: [pureAnnotations()],\n  resolve: {\n    conditions: ['source', 'development', 'module', 'browser', 'default'],\n  },\n});\n```\n\nBecause a `link:`ed checkout is the same version as the plugin, you can also\npass `pureAnnotations({inline: true})`, which replaces calls to the trivial\nfactories (`safeCast`, `defineExtension`, `configExtension`, ...) with the\nliteral they would have returned rather than annotating them.\n\n`@lexical/compiler/PureAnnotations` also exports the transform itself\n(`transformPureAnnotations`) for bundlers without a Rollup-style plugin API;\nsee its README for the options.\n\nThe integration fixture at\n`scripts/__tests__/integration/fixtures/lexical-link-source-mode/`\nexercises exactly this setup and is the canonical reference if you need\na fuller example.\n\n> [!NOTE]\n> `@lexical/internal` is published only so this resolution works (and for\n> direct internal consumers); the compiled `dist/` artifacts inline it, so\n> a normal `npm install` of `lexical` never executes a separate copy.\n","packages/lexical-website/docs/maintainers-guide.md":"# Maintainers' Guide\n\nThis is the grimoire of arcane knowledge covering the overall organization\nof the Lexical monorepo, including its conventions, quirks, and\nconfigurations.\n\n## Monorepo Organization\n\n### Workspaces\n\nThe monorepo is configured with\n[pnpm workspaces](https://pnpm.io/workspaces); the set of workspace\npackages is declared in the top-level `pnpm-workspace.yaml`. This mostly means that all packages share a\ntop-level `pnpm-lock.yaml` and `pnpm -C {package} run {command}` is often\nused to run a command from a nested package's package.json.\n\n### Private\n\nSome packages in the monorepo do not get published to npm, for example:\n\n* `packages/lexical-devtools` - browser extension for working with Lexical\n  sites\n* `packages/lexical-playground` - the\n  [playground.lexical.dev](https://playground.lexical.dev/) demo site\n* `packages/lexical-website` - the [lexical.dev](https://lexical.dev/)\n  docusaurus website that you may even be reading right now\nInternal runtime code shared by more than one package lives in\n`packages/lexical-internal` (`@lexical/internal`). Unlike the others above\nit **is** published, but only so its source resolves through normal package\nresolution (the `source` export condition used by linked-checkout\nconsumers); the compiled packages inline it, so it is never executed as a\nseparate runtime dependency. It is not a public API and has no semver\nguarantees — see [Developing against a local Lexical\ncheckout](./maintainers-guide-link.md).\n\nIt is required that private packages, and any other package that should not\nbe published to npm, have a `\"private\": true` property in their `package.json`.\nIf you have an in-progress package that will eventually be public, but is\nnot ready for consumption, it should probably still be set to\n`\"private\": true` otherwise the tooling will find it and publish it.\n\n## Package naming conventions\n\n### Overall\n\n| Usage | Convention |\n| -- | -- |\n| Directory name | `packages/lexical-package-name` |\n| Entrypoint | `packages/lexical-package-name/src/index.{ts,tsx}` |\n| Flow types | `packages/lexical-package/flow/LexicalPackageName.js.flow` |\n| package.json name | `@lexical/package-name` |\n| Documentation | `packages/lexical-package-name/README.md` |\n| Unit Tests | `packages/lexical-package-name/src/__tests__/unit/LexicalPackageName.test.{ts,tsx}` |\n| dist (gitignore'd build product) | `packages/lexical-package-name/dist` |\n| npm (gitignore'd prerelease product) | `packages/lexical-package-name/npm` |\n| www entrypoint | `packages/lexical-package-name/LexicalPackageName.js` |\n\n### Multiple module export (@lexical/react)\n\nInstead of having a single module, some packages may have many modules\n(currently only `@lexical/react`) that are each exported separately.\nIn that scenario, there should be no `index.ts` entrypoint file and every module\nat the top-level should be an entrypoint. All entrypoints should be a\nTypeScript file, not a subdirectory containing an index.ts file.\n\nThe [update-packages](#pnpm-run-update-packages) script will ensure that the\nexports match the files on disk.\n\n## Creating a new package\n\nThe first step in creating a new package is to create the workspace directory\nand package.json file. The example we will use is the steps that were used\nto create the `lexical-eslint-plugin`, which will be published to npm as\n`@lexical/eslint-plugin`.\n\n### Create the workspace\n\n```bash\nmkdir -p packages/lexical-eslint-plugin\n```\n\nCreate the initial `package.json` file (you can base it on an existing package\nor use the template below):\n\n<details><summary>\n\n`packages/lexical-eslint-plugin/package.json`\n</summary>\n\n```json\n{\n  \"name\": \"@lexical/eslint-plugin\",\n  \"description\": \"\",\n  \"keywords\": [\n    \"lexical\",\n    \"editor\"\n  ],\n  \"version\": \"0.14.3\",\n  \"license\": \"MIT\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/facebook/lexical.git\",\n    \"directory\": \"packages/lexical-eslint-plugin\"\n  },\n  \"main\": \"LexicalEslintPlugin.js\",\n  \"types\": \"index.d.ts\",\n  \"bugs\": {\n    \"url\": \"https://github.com/facebook/lexical/issues\"\n  },\n  \"homepage\": \"https://github.com/facebook/lexical#readme\"\n}\n```\n</details>\n\nSome next steps for this package.json before moving on:\n\n- Update the description\n- Add appropriate keywords\n\n### Create the initial source file\n\n```\nmkdir -p packages/lexical-eslint-plugin/src\ncode packages/lexical-eslint-plugin/src/index.ts\n```\n\nHere are some minimal examples of those files that you might start out with.\nI've elided the license header, the eslint header/header fixer will help you\nwith that!\n\n<details><summary>\n\n`packages/lexical-eslint-plugin/src/index.ts`\n</summary>\n\n```typescript\nimport {name, version} from '../package.json';\n\nconst plugin = {\n  meta: {name, version},\n  rules: {},\n};\n\nexport default plugin;\n```\n</details>\n\n### Run update-packages to generate boilerplate docs & config\n\n```\npnpm run update-packages\n```\n\nThis will set up the tsconfig, flow, etc. configuration to recognize your\nnew module. It will also create an initial README.md using only the\ndescription from the package.json.\n\n\n### Create an initial unit test\n\n```\nmkdir -p packages/lexical-eslint-plugin/src/__tests__/unit\ncode packages/lexical-eslint-plugin/src/__tests__/unit/LexicalEslintPlugin.test.ts\n```\n\n\n<details><summary>\n\n`packages/lexical-eslint-plugin/src/__tests__/unit/LexicalEslintPlugin.test.ts`\n</summary>\n\n```typescript\nimport plugin from '@lexical/eslint-plugin';\n\ndescribe('LexicalEslintPlugin', () => {\n  it('exports a plugin with meta and rules', () => {\n    expect(Object.keys(plugin).sort()).toMatchObject(['meta', 'rules']);\n  });\n});\n```\n</details>\n\n\n## Scripts for development\n\n### pnpm run update-packages\n\nThis script runs: update-version, update-tsconfig, update-flowconfig,\ncreate-docs, and create-www-stubs. This is safe to do at any time and will\nensure that package.json files are all at the correct versions, paths are\nset up correctly for module resolution of all public exports, and that\nvarious defaults are filled in.\n\nThese scripts can be run individually, but unless you're working on one\nof these scripts you might as well run them all.\n\n### pnpm run prepare-release\n\nThis runs `build-release` to produce all of the artifacts each public\npackage needs (the `dev`/`prod` ESM variants plus their fork modules,\n`.d.ts` declarations, and `.js.flow` stubs under\n`packages/<name>/dist/`), then runs the publish-time guard in\n`scripts/npm/prepare-release.mjs` to confirm every path the package's\n`exports`/`main`/`module`/`types` fields reference actually exists on\ndisk. The guard fails the build if e.g. you ran `pnpm run build` (dev\nonly) and then tried to publish — the `.prod.js` files would be\nmissing.\n\nOnly ESM is published to npm. Every public package is a\n`\"type\": \"module\"` package (`update-version` adds the field), so its\nbuild is plain `.js`. A CommonJS consumer gets the same files through\nNode's `require(esm)` (Node.js 20.19+), which is why the fork module that\nthe exports map's `default` condition resolves to has no top-level await.\nThe CommonJS variants are only built for www (`pnpm run build-www`).\n\nBecause a `.d.ts` in a `\"type\": \"module\"` package is an ES module\ndeclaration, the build gives the relative imports tsc emits an explicit\n`.js` extension (`from './LexicalEditor.js'`): TypeScript's `nodenext`\nresolution does not resolve extensionless relative imports in ESM, and a\nconsumer with `skipLibCheck` would silently get `any` for everything.\n`scripts/__tests__/integration/declaration-resolution.test.mjs` checks\nthat a `nodenext` consumer of every entry point still sees the types.\n\nEach package is its own publish root: `packages/<name>/` IS the\npublishable npm package after `build-release`. `pnpm publish` is run\ndirectly from that directory by `scripts/npm/release.mjs` so pnpm's\nautomatic `workspace:*` rewriting and the `files` whitelist do the\nright thing without an intermediate `npm/` copy step.\n\nThis will also update `scripts/error-codes/codes.json`, the mapping of\nproduction error codes to error messages. It's imperative to commit the\nresult of this before tagging a release.\n\n### pnpm run ci-check\n\nCheck flow, TypeScript, prettier and eslint for issues. A good command to run\nafter committing (which will auto-fix most prettier issues) and before pushing\na PR.\n\n### pnpm run flow\n\nCheck the Flow types\n\n### pnpm run tsc\n\nCheck the TypeScript types\n\n### pnpm run tsc-extension\n\nCheck the TypeScript types of the lexical-devtools extension\n\n### pnpm run test-unit\n\nRun the unit tests\n\n### pnpm run lint\n\nRun eslint\n\n### pnpm run generate-node-json\n\nRegenerate the specialized JSON serialization code for the built-in node\nclasses. A node's [serialization schema](/docs/serialization/#declarative-serialization-schemas-with-config)\nstates everything about a serialized property ahead of time — which accessor\nor field it uses, what its default is, what its domain admits — so the\ngeneric walk over that schema can be compiled into straight-line code. This\nscript does that compiling; the output is checked in.\n\nIt writes one module per package, beside the nodes it serializes:\n\n| Module | Classes |\n| --- | --- |\n| `packages/lexical/src/LexicalGeneratedJSON.ts` | ElementNode, TextNode, ParagraphNode, LineBreakNode, TabNode |\n| `packages/lexical-rich-text/src/LexicalRichTextGeneratedJSON.ts` | HeadingNode, QuoteNode |\n| `packages/lexical-link/src/LexicalLinkGeneratedJSON.ts` | LinkNode, AutoLinkNode |\n| `packages/lexical-mark/src/LexicalMarkGeneratedJSON.ts` | MarkNode |\n| `packages/lexical-list/src/LexicalListGeneratedJSON.ts` | ListNode, ListItemNode |\n| `packages/lexical-table/src/LexicalTableGeneratedJSON.ts` | TableNode, TableRowNode, TableCellNode |\n| `packages/lexical-code-core/src/LexicalCodeCoreGeneratedJSON.ts` | CodeNode, CodeHighlightNode |\n| `packages/lexical-react/src/shared/LexicalReactGeneratedJSON.ts` | DecoratorBlockNode |\n\nEach class receives its own generated code through its `$config`'s\n`generated` property, so nothing has to match code to class by type string at\nruntime. What the property holds is a factory: registration calls it with the\nclass's composed schema, and the generated code reads the lookup tables it\nneeds (`getterTable`, `setterTable`, `aliasedValue`'s) off that schema through\n`getterTableOf`, `setterTableOf` and `aliasTableOf`, so a generated module\ncarries no copy of a table and nothing about a table's contents is written at\nbuild time — only its type, so the field a value is assigned to is still\nchecked. A subclass inherits the code along with the schema when its compiled\naccessor tables are the ones the code was generated from — checked entry for\nentry at registration — and runs it over its own schema, while one that\noverrides an accessor a field stands in for, or declares a serialized property\nof its own, resolves differently and takes the schema-driven walk instead.\nGenerated exporters read `type` off the node for the same reason.\n\nThree things are worth knowing before touching it:\n\n- **The output is verified, not trusted.** The import direction is the\n  untrusted-JSON boundary, so every generated parser is run against the schema\n  it was compiled from over a corpus drawn from that schema plus a fixed set of\n  hostile values (`'__proto__'`, `'toString'`, `'1e999'`, …). A property whose\n  schema cannot be compiled faithfully takes its class out of the import half\n  rather than shipping a parser that disagrees with the walk — the script says\n  so on stdout when it happens.\n- **A stale checkout fails the tests.** `LexicalGeneratedJSON.test.ts`\n  regenerates into a temporary directory and compares every file byte for\n  byte, and separately asserts that each generated exporter agrees with the\n  schema-driven walk for both the legacy and compact forms. Change a schema\n  without rerunning this script and that test fails.\n- **Two lists, deliberately.** The script runs in two phases, because reading\n  the schemas means importing the packages and each package imports the file\n  the script writes for it. Phase one replaces every output with a valid\n  do-nothing stub from the static `MANIFEST` in\n  `scripts/shared/generateNodeJSONManifest.mjs` so the imports always succeed;\n  phase two re-enters under `tsx` and writes the real thing from `PACKAGES` in\n  `scripts/shared/generateNodeJSON.mjs`, which is also what the drift test\n  runs in-process. Adding a class means editing both lists, and the generation\n  fails loudly if they disagree.\n\nThe compact form compares each property against its default. A primitive\ndefault is a literal; a reference-typed default has no literal a value could\nbe `===`, so it gets the structural test the schema's own equality reduces to\nwhere that can be stated — MarkNode's `ids`, whose default is an empty array,\nbecomes a length test — and every emitted comparison is verified against that\nequality over a corpus, the way a parse is. A default the generator cannot\nstate that way (an object, a non-empty array, a non-finite number) takes the\nclass out of the compact half only: the script says so on stdout and the other\nforms are generated as usual. A class that carries flat NodeState is generated\nlike any other; the walk applies the state before handing the node to the\ngenerated parser, the mirror of how export appends it after the generated\nliteral.\n\n`afterCloneFrom` is generated too, and is the one direction whose fallback is\nnot the walk. A schema field is where a property is *stored*, so every class\nthat declares one gets an `afterCloneFrom` synthesized at registration —\ngenerated straight-line code when the class has some, and otherwise a loop over\nthe field names — copying the fields that class's own `$config` declared and\ndelegating the rest through `super`, which is why the emitted function covers\none class's own fields and nothing above it. A field an ancestor declares too\nis left to the ancestor, whose method has already assigned it: re-declaring an\ninherited property changes how it is serialized, not where it is stored, so a\nclass that only re-declares gets no method at all — `TabNode`, which restates\n`TextNode`'s `text`, `detail` and `mode`, is the in-tree case and simply\ninherits `TextNode`'s. Both accessor directions are read\nfor a field name, and the declared field is used rather than the one\n`resolveGetterAccessor` resolves to: an override changes how a property is\nserialized, not where it lives. A class that writes its own `afterCloneFrom` is\nleft alone and owns all of its properties, which is how `ElementNode` keeps\ncarrying `__first`/`__last`/`__size` and its slot bookkeeping; so is a property\ndeclared through accessor methods on both sides, which names no field for\nanything to copy. No in-tree node is in that second position — a property held\nin a field says so with `setter: {field, method}` and stays derived, as\n`MarkNode`'s `ids` does — so the boilerplate that remains is `ElementNode`'s\nand `CodeNode`'s, and each of those calls the generated `afterClone<Class>`\nfor its schema half and writes only the fields no schema describes.\n`ownSchemaFields` in `LexicalUtils.ts` is the single definition of that field\nlist, called by both the generator and the synthesized fallback.\n\nThe schema-to-JavaScript compiler itself lives in `@lexical/compiler`'s\n`SchemaJsonCodegen` entry point, so it is testable independently of the\ngenerator that drives it.\n\n## Scripts for release managers\n\n### pnpm run extract-codes\n\nThis will run a build that also extracts the generated error codes.json file.\n\nThis should be done, at minimum, before each release, but not in any PR as\nit would cause conflicts between serial numbers.\n\nIt's safe and probably advisable to do this more often, possibly any time a\nbranch is merged to main.\n\nThe codes.json file is also updated any time a release build is generated\nas a failsafe to ensure that these codes are up to date in a release.\nThis command runs a development build to extract the codes which is much\nfaster as it is not doing any optimization/minification steps.\n\n### pnpm run increment-version\n\nIncrement the monorepo version. The `-i` argument must be one of\n`minor` | `patch` | `prerelease`.\n\nThe postversion script will:\n- Create a local `${CHANNEL}__release` branch (the version-named\n  `${npm_package_version}__release` branch is only a remote push target,\n  and only when the channel is not `nightly`)\n- `pnpm run update-version` to update example and sub-package monorepo dependencies\n- `pnpm install` to update the pnpm-lock.yaml\n- `pnpm run update-packages` to update other generated config\n- `pnpm run extract-codes` to extract the error codes and\n  `pnpm run update-changelog` to update the changelog (both only when\n  `CHANNEL` is `latest`)\n- Create a version commit and tag from the branch\n\nThis is typically executed through the `version.yml` GitHub Workflow which\nwill also push the tag and branch.\n\n### pnpm run changelog\n\nUpdate the changelog from git history.\n\n### pnpm run release\n\n*Prerequisites:* all of the previous release manager scripts,\nplus creating a tag in git, and likely other steps.\n\nRuns prepare-release to do a full build and then uploads to npm.\n\n### pnpm run setup-trusted-publishing\n\nOne-time (idempotent) helper to register every public package with\n[npm trusted publishing](https://docs.npmjs.com/trusted-publishers).\nRe-run it whenever a new public package is added.\n\n#### Prerequisites\n\n- Node.js — whatever the repo's root `package.json#engines.node` says (currently `>=20.19.0`). Running with Node 24+ is recommended because that's what CI uses for publishes.\n- pnpm — pinned by `package.json#packageManager` (currently `pnpm@11.24.0`). Activate with [corepack](https://nodejs.org/api/corepack.html) or install directly.\n- npm CLI — **`npm ≥ 11.10`** (`npm i -g npm@latest`). The `npm trust` subcommand was added in npm 11; older versions will fail the preflight check.\n- An authenticated npm session (`npm login --registry https://registry.npmjs.org`) on a publisher account that has **account-level 2FA enabled** and write access to every `@lexical/*` package.\n\n#### Usage\n\nRun in check-only mode first:\n\n```bash\npnpm run setup-trusted-publishing\n```\n\nFor each public package in the monorepo, it queries\n`https://registry.npmjs.org` and reports whether the name is already\nclaimed. Packages that *don't* exist on the registry are listed; you\ncan re-run with `--bootstrap` to publish a deprecated\n`0.0.0-bootstrap.0` placeholder under the `bootstrap` dist-tag so the\nname can be claimed:\n\n```bash\nnpm login --registry https://registry.npmjs.org\npnpm run setup-trusted-publishing --bootstrap\n```\n\nOnce a package exists on the registry, you can configure trusted\npublishing for it programmatically by adding `--setup-trust`. This\nruns `npm trust github` under the hood (requires `npm` ≥ 11.10 and an\nauthenticated session with account-level 2FA on the publishing\naccount), and is idempotent — the script reads the existing trust\nconfiguration for each package via a read-only registry call (no OTP)\nand skips packages whose config already matches:\n\n```bash\nnpm login --registry https://registry.npmjs.org\npnpm run setup-trusted-publishing --setup-trust\n```\n\n`npm trust github` is a write operation, so each package that *does*\nneed configuring will trigger a one-time-password / web-auth prompt.\nOn the first prompt npm prints a URL; open it in a browser, sign in,\nand tick **\"Skip two-factor authentication for the next 5 minutes\"**.\nSubsequent packages in the same run will then go through without\nre-prompting. The script also inserts a small (~2 s) pause between\ncalls to stay under the registry's `E429` rate limit.\n\nFor full first-time setup of a brand-new monorepo, combine both flags:\n\n```bash\npnpm run setup-trusted-publishing --bootstrap --setup-trust\n```\n\nWhen adding a **single** new package to an existing monorepo — the common\ncase going forward — pass its name so the run only touches that package\ninstead of re-checking all 30+ already-configured ones (which just prints a\nwall of `CONFLICT` lines). The name can be the full npm name or the unscoped\nshort name, and `--package` / positional args are interchangeable and\nrepeatable:\n\n```bash\npnpm run setup-trusted-publishing --bootstrap --setup-trust @lexical/a11y\n# equivalently: --package a11y\n```\n\nUseful flags:\n\n- `--package <name>` (or a positional `<name>`, repeatable) — restrict the run to the given package(s), matched by full npm name (`@lexical/a11y`) or unscoped short name (`a11y`). Omit to process every public package.\n- `--dry-run` — print what would happen without touching the registry (works with both `--bootstrap` and `--setup-trust`)\n- `--workflow <filename>` — override the workflow filename (default `pre-release.yml`)\n- `--repo <owner/name>` — override the GitHub repo (default `facebook/lexical`)\n- `--stub-version <semver>` — override the placeholder version (default `0.0.0-bootstrap.0`)\n- `--registry <url>` — override the npm registry\n\nIn the default (check-only) mode the script also prints the npmjs.com\n`/access` URL for each existing package and the exact values to enter\nmanually, as a fallback for when `npm trust github` isn't an option.\n\n### Testing trusted publishing from a PR branch\n\nThe \"Publish to NPM\" workflow (`pre-release.yml`) exposes `ref`,\n`channel`, and `increment-version` inputs so it doubles as a test\nharness. Picking a branch in the \"Run workflow\" dropdown selects\nwhich version of the workflow files run, and the inputs determine\nwhat actually gets published. The workflow has no NPM_TOKEN secret to\nfall back on — publishes always go through OIDC trusted publishing —\nso a misconfigured trust setup fails loudly rather than silently\nfalling through to token auth.\n\nA safe end-to-end test looks like:\n\n| Input | Value |\n| -- | -- |\n| Branch (dropdown) | your PR branch |\n| `ref` | your PR branch (same value) |\n| `channel` | `dev` |\n| `increment-version` | checked |\n| `ignore-previously-published` | unchecked |\n\nWith `increment-version` on, the run bumps `package.json` to a fresh\nprerelease (e.g. `0.46.0-dev.0`), commits + tags it on a `dev__release`\nbranch on origin, and publishes the monorepo under the `dev` dist-tag\nvia OIDC. The `latest` tag is untouched, so default `npm install`\nusers are unaffected. After it succeeds:\n\n```bash\nnpm view lexical@dev version     # → the just-published prerelease\nnpm view lexical@latest version  # → unchanged\n```\n\nCleanup (the prerelease itself can't be reused, but the git refs\nshould go):\n\n```bash\ngit push --delete origin v0.46.0-dev.0 dev__release 0.46.0-dev.0__release\n```\n\nThe `increment-version=true + channel=latest` combination is refused\nby the workflow's guard job — real `latest` releases must go through\n`version.yml` first.\n\n## Continuous releases (pkg.pr.new)\n\nEvery commit to a pull request and every commit to `main` publishes all public\npackages to [pkg.pr.new](https://github.com/stackblitz-labs/pkg.pr.new) through\nthe `pkg-pr-new.yml` workflow. Nothing goes to npm: the nightly, next and latest\nchannels above are unchanged and still publish through `call-release.yml`.\n\n```sh\nnpm i https://pkg.pr.new/lexical@9372      # latest commit of PR #9372\nnpm i https://pkg.pr.new/lexical@4282a68   # a specific commit\nnpm i https://pkg.pr.new/lexical@main      # tip of main\n```\n\nThe bot comments install commands on each pull request. Previews are built\nwith `pnpm run prepare-release`, the same build as the npm channels, after\n`scripts/npm/preview-version.mjs` stamps a version in the nightly line:\n`0.52.1-pr.9372.g4282a68` for a PR and `0.52.1-main.g4282a68` for `main`, next\nto `0.52.1-nightly.20261019.0` on npm. That version is also what\n`LexicalEditor.version` reports. Dependencies between Lexical packages are\nrewritten to the same commit's pkg.pr.new URLs, so `@lexical/react` from a\npreview depends on `lexical` from that preview, not on an npm release.\n\nThe bot's comment lists npm commands, and npm dedupes those dependencies by\nname and version, so `@9372` or `@main` URLs are fine. pnpm and yarn key URL\ndependencies by URL instead: install with the long commit URLs that the\npackages use for each other, or you get more than one copy of `lexical`. pnpm\n11+ also refuses URL dependencies of URL dependencies unless\n`blockExoticSubdeps` is off. Each run's job summary lists these commands:\n\n```sh\npnpm add --config.block-exotic-subdeps=false \\\n  lexical@https://pkg.pr.new/facebook/lexical/lexical@4282a68 \\\n  @lexical/react@https://pkg.pr.new/facebook/lexical/@lexical/react@4282a68\nyarn add \\\n  lexical@https://pkg.pr.new/facebook/lexical/lexical@4282a68 \\\n  @lexical/react@https://pkg.pr.new/facebook/lexical/@lexical/react@4282a68\n```\n\nOn a cold cache, yarn 1 can fail with \"the file appears to be corrupt\" while\nit downloads the same URL for several packages at once. Run it again.\n\nThe workflow requires the [pkg.pr.new GitHub App](https://github.com/apps/pkg-pr-new)\nto be installed on the repository.\n\n## Release Procedure\n\nThis is the current release procedure for public releases, at least as of\nMay 2024 (~0.15.0).\n\nThe main branch should be \"frozen\" during this procedure (no other PRs should\nbe merged during this time). This avoids a mismatch between the contents of\nthe GitHub release (created from main in step 1) and the NPM release (created\nfrom main in step 4).\n\n1. Create a new version with the Github Actions \"Create New Release Branch\" workflow (`version.yml`)\n2. Raise a PR against version branch created by that action\n3. After PR is approved with passing tests, merge PR\n4. After PR is merged to main, publish to NPM with the Github Actions \"Publish to NPM\" workflow (`pre-release.yml`)\n5. Create a GitHub release from the tag created in step 1, manually editing the release notes\n6. Announce the release in #announcements on Discord\n\n## Release Protocol\n\n1. All PRs with breaking changes must have `[Breaking Change]` in the PR's title with documentation of what followup actions consumers of the lexical library need to be aware of.\n2. Monthly releases happen on the last week of the month, with a minor increment (eg. v0.20+1.0).\n3. Anything in between will be a patch increment (eg. 0.20.0+1), unless there is a breaking change. \n\n## Website Team Page\n\nThe [team page](https://lexical.dev/community) displays core team\nmembers, emeriti, and distinguished contributors. The `team.json` data is\ngenerated from GitHub contributor information and some predetermined decisions in\nthe script to acknowledge emeriti and historically important distinguished contributors.\n\nTo update the team page data:\n\n```bash\npnpm run update-team-data\n```\n\nThis fetches the latest contributor data from GitHub and categorizes team members\nbased on recent activity (last 12 months). See `packages/lexical-website/src/data/README.md`\nfor more details on configuration and team categorization logic.\n","packages/lexical-website/docs/react/_category_.json":"{\n  \"label\": \"React\",\n  \"position\": 5\n}\n","packages/lexical-website/docs/react/create_plugin.md":"# Creating a React Plugin\n\nIn addition to using the Lexical React plugins offered by the core library, you can make your own plugins to extend or alter Lexical's functionality to suit your own use cases.\n\nLexical's React plugin interface is simple - just create a React component and add it as a child of your LexicalComposer component:\n\n```jsx\n <LexicalComposer initialConfig={initialConfig}>\n    <MyLexicalPlugin />\n </LexicalComposer>\n```\n\nIf the Plugin introduces new nodes, they have to be registered in `initialConfig.nodes`:\n\n```js\nconst initialConfig = {\n  namespace: 'MyEditor',\n  nodes: [MyLexicalNode],\n};\n```\n\n```jsx\n <LexicalComposer initialConfig={initialConfig}>\n    <MyLexicalPlugin />\n </LexicalComposer>\n```\n\nLexicalComposer provides access to the underlying LexicalEditor instance via React Context:\n\n```jsx\n//MyLexicalPlugin.js\n\nexport function MyLexicalPlugin(props) {\n    const [editor] = useLexicalComposerContext();\n    ...\n}\n```\n\nWith access to the Editor, your plugin can extend Lexical via [Commands](../concepts/commands.md), [Transforms](../concepts/transforms.md), or other APIs. For example, the [TwitterPlugin](https://github.com/facebook/lexical/blob/0775ab929e65723433626fa8c25900941e7f232f/packages/lexical-playground/src/plugins/TwitterPlugin/index.ts#L18) embeds a tweet into the editor, fetching the data asynchronously from Twitter based on the provided Tweet ID:\n\n```jsx\nexport const INSERT_TWEET_COMMAND: LexicalCommand<string> = createCommand();\n\nexport default function TwitterPlugin(): JSX.Element | null {\n  const [editor] = useLexicalComposerContext();\n\n  useEffect(() => {\n    if (!editor.hasNodes([TweetNode])) {\n      throw new Error('TwitterPlugin: TweetNode not registered on editor (initialConfig.nodes)');\n    }\n\n    return editor.registerCommand<string>(\n      INSERT_TWEET_COMMAND,\n      (payload) => {\n        const tweetNode = $createTweetNode(payload);\n        $insertNodeToNearestRoot(tweetNode);\n\n        return true;\n      },\n      COMMAND_PRIORITY_EDITOR,\n    );\n  }, [editor]);\n\n  return null;\n}\n```\n\nTwitterPlugin is just a React component that accesses the Lexical editor via React Context (useLexicalComposerContext). Using the LexicalEditor instance, this plugin does two things:\n\n1. Verifies that there is a TweetNode registered on the editor (if you forget to register the node, you can't do #2)\n2. registers a \"command\", passing a callback that will run when that command is dispatched. The command callback creates and inserts a TweetNode in the editor.\n\nYou can see how [TwitterPlugin is used in the playground](https://github.com/facebook/lexical/blob/0775ab929e65723433626fa8c25900941e7f232f/packages/lexical-playground/src/Editor.tsx#L137). It's added as a child of a LexicalComposer component, which does the job of providing the Context necessary for access to the editor instance. To actually trigger this command callback and insert a [TweetNode](https://github.com/facebook/lexical/blob/b0fa38615c03f1c4fc7c8c5ea26412b723770e55/packages/lexical-playground/src/nodes/TweetNode.tsx#L212), we have a [button](https://github.com/facebook/lexical/blob/b0fa38615c03f1c4fc7c8c5ea26412b723770e55/packages/lexical-playground/src/plugins/ToolbarPlugin.tsx#L534) that \"dispatches\" the Tweet command we registered in the plugin.\n\nWhile the TwitterPlugin registers a command that inserts a custom node, this is only one example of what can be done with a plugin. To get a better idea of what's possible, take a look at the [plugins defined in the playground](https://github.com/facebook/lexical/tree/main/packages/lexical-playground/src/plugins).\n","packages/lexical-website/docs/react/faq.md":"# React FAQ\n\n## My app does not work in dev when using StrictMode, help!?\n\nWhen hooks are used correctly, there are no known issues with React StrictMode\nand Lexical. The first thing you should do is go through React's documentation\nto make sure that your usage of `useEffect` and other hooks follow React's\nconventions and guidelines. This is a great place to start:\n[My Effect runs twice when the component mounts](https://react.dev/reference/react/useEffect#my-effect-runs-twice-when-the-component-mounts)\n\nSome Lexical-specific concerns (which are consequences of React's\nconcurrent and StrictMode semantics, not due to anything unusual in Lexical):\n\n- In React 19, `useMemo` calls are cached across StrictMode re-renders, so\n  only one editor will be used for both renders. If you have a `useEffect`\n  call with side-effects (such as updating the document when a plug-in\n  initializes), then you should first check to make sure that this effect\n  has not already occurred (e.g. by checking the state of the document or\n  undoing the change as a cleanup function returned by the effect)\n- `LexicalComposer`'s initialConfig prop is only considered once during\n  the first render (`useMemo` is used to create the `LexicalComposerContext`\n  which includes the editor and theme)\n- If you are using an `editorState` argument in the config when creating the\n  editor, it will only be called once when the editor is created.\n- You should generally prefer to use hooks that return state such as\n  `useLexicalEditable` (`useLexicalSubscription` is a generalization of this\n  style) rather than manually registering the listeners and expecting a\n  particular sequence of triggers to be called, especially\n  when their source is an effect. Listeners are only called when state\n  changes, and in StrictMode the state may have changed during the initial\n  render. The listeners registered from your second render will not be called\n  if the change was triggered by the first render, and you will likely not\n  see the listeners triggered during the first render because those effects\n  were immediately cleaned up before the change effect occurred.\n\n## LexicalComposerContext.useLexicalComposerContext: cannot find a LexicalComposerContext\n\nThis error happens for one reason: the `useLexicalComposerContext()` hook\nwas called from a component that is not a child of a `LexicalComposer`,\n`LexicalNestedComposer`, or `LexicalComposerContext.Provider` from the same\nbuild of Lexical that the hook was imported from.\n\nThe most common root causes of this issue are:\n\n- You are trying to use `useLexicalComposerContext()` in a component that is\n  not a child of the `LexicalComposer`. If you need to do that, you need to\n  pass the context or editor up the tree with something like `EditorRefPlugin`.\n- You have multiple builds of Lexical in your project. This could be because\n  you have a dependency that has a direct dependency on some other version\n  of Lexical (these packages should have Lexical as `peerDependencies`, but\n  not all do), or because your bundler resolves Lexical to different files\n  from different places (for example the `development` build in one and the\n  `production` build in another, or a `source` condition in one and `dist`\n  in another). Resolving this generally requires overriding what\n  your package manager does in `package.json`, and/or what the bundler does in\n  some configuration file for your framework or bundler. There are a lot of\n  combinations of tools in the ecosystem (npm, pnpm, yarn, webpack, vite,\n  next.js, etc.), so the syntax of that workaround is quite dependent on\n  precisely which tools (and even versions of those tools) that your project\n  is using. See [One Lexical per app](../concepts/one-lexical-per-app.md) for\n  why one copy is required, how to confirm you have more than one, and why a\n  library should declare Lexical in `peerDependencies`.\n\n## Hot Module Replacement (HMR)\n\nDuring development, HMR re-executes modules on every code change. Because Lexical uses object identity for node class registration, command dispatch, and extension deduplication, a naive HMR cycle destroys the editor state and resets the document.\n\n### HMRExtension\n\n`@lexical/extension` exports an `HMRExtension` that preserves editor state, the selection, the editable flag, and undo/redo history across HMR cycles. It stashes a reference to the current `EditorState` in the bundler's HMR data store, and the module instance that replaces it serializes that state and rebuilds it against the new node classes. Serializing only at that point keeps the cost off of every editor update.\n\nThe editor state and the undo/redo entries are serialized together, as one family of related states, rather than one at a time. Serializing each state on its own (`toJSON()` / `parseEditorState()`) would lose two properties the editor depends on: consecutive versions of a document share nearly all of their node objects, because an update clones only what it touches, and reconciliation is a diff over `NodeKey`s. Independently parsed states share nothing and have disjoint key spaces, so the history would cost memory proportional to its depth and the first undo after a reload would rebuild the whole document instead of the part that changed. The family format stores one entry per distinct node *version* and rebuilds each one once, so what was shared comes back shared and every version of a node answers to one key — which is also what lets the selection be carried by key.\n\n```ts\nimport {buildEditorFromExtensions, configExtension, defineExtension, HMRExtension} from '@lexical/extension';\nimport {RichTextExtension} from '@lexical/rich-text';\nimport {HistoryExtension} from '@lexical/history';\n\nconst editor = buildEditorFromExtensions(\n  defineExtension({\n    name: '[root]',\n    namespace: 'my-editor',\n    dependencies: [\n      RichTextExtension,\n      HistoryExtension,\n      configExtension(HMRExtension, {hot: import.meta.hot ?? null}),\n    ],\n  }),\n);\n```\n\nThe `hot` config accepts any object with a mutable `data: Record<string, unknown>` property — Vite's `import.meta.hot` satisfies this directly. Webpack and Parcel expose `module.hot` instead, whose `data` is populated by dispose handlers and is not directly mutable, so `module.hot` cannot be passed here; those bundlers need a small adapter built on `module.hot.addDisposeHandler`. Pass `null` in production or when HMR is not available; the extension becomes a no-op.\n\nWhen `HistoryExtension` is present as a peer, undo/redo stacks are preserved automatically. The extension does not declare `HistoryExtension` as a dependency — it detects it at runtime via peer dependency lookup.\n\nEditors with distinct `namespace` values — set via `namespace` in `defineExtension` or `createEditor` — are isolated automatically, so no `id` is needed. An editor given no namespace at all is keyed without one, since the namespace `createEditor` generates in that case is a new random string on every reload; those editors all share a single key. When two editors share both the same `import.meta.hot` context and the same key, pass a stable `id` string to keep their states independent:\n\n```ts\ndefineExtension({ name: '[main]', namespace: 'shared', dependencies: [configExtension(HMRExtension, {hot: import.meta.hot ?? null, id: 'main'})] })\ndefineExtension({ name: '[sidebar]', namespace: 'shared', dependencies: [configExtension(HMRExtension, {hot: import.meta.hot ?? null, id: 'sidebar'})] })\n```\n\nA nested editor inherits its parent's namespace, so the two are not isolated from each other automatically the way independent editors are; give the nested editor a distinct `id`. In development, sharing a key with the parent is warned about.\n\nSaved state belongs to the namespace and `id` for as long as the page lives, not to the reload that produced it. An editor that is unmounted and later remounted under the same key during development restores what the previous one had rather than its own `$initialEditorState` — so two editors showing different documents need distinct namespaces even when they are never on screen at the same time.\n\n### Fast Refresh compatibility\n\nVite (and similar tools) apply React Fast Refresh — state-preserving HMR for React components — only when a module exports nothing but React components. Modules that also export hooks, classes, commands, or constants fall back to a full remount, which discards component state.\n\nSeveral `@lexical/react` plugin modules split their non-component exports into companion `*Utils` files. Consumers that import non-component values directly from the `*Utils` module get more granular HMR boundaries, since changes to the component file don't invalidate those imports. The original module re-exports these for backwards compatibility.\n\nA companion module only earns that boundary if it imports no component module itself, so shared values it needs have to live somewhere component-free too: `MenuOption` — the class a menu plugin's options extend — is exported from `@lexical/react/LexicalMenuOption` for that reason, and re-exported from the plugin modules that used to define it.\n\nValues shared between entry points also have to be defined in one, not in an internal module: an internal module is inlined into every entry point that imports it, so a command or class defined there is a different object in each of them, and commands and `instanceof` are matched by identity.\n\nIf you're building custom plugins, follow the same pattern: keep React components in one file and export hooks, constants, or classes from a separate file.\n\n### Fallback: `// @refresh reset`\n\nIf a module can't be split (e.g. it defines both a component and tightly-coupled non-component logic), you can mark it for a full refresh using your framework's directive. For example, [Next.js fast refresh](https://nextjs.org/docs/architecture/fast-refresh#tips) supports a `// @refresh reset` comment at the top of the file. This forces a full remount of all components in the file on every change.\n","packages/lexical-website/docs/react/index.md":"---\nsidebar_label: 'Introduction'\n---\n\n# Lexical + React\n\nTo make it easier for React users to implement rich-text editors, Lexical exposes a set of abstractions that aim to cover a wide range of use cases out-of-the-box.\n\n<a\n    class=\"whitespace-nowrap rounded-md bg-blue-500 px-6 py-2 text-sm font-bold text-white transition-opacity hover:text-white hover:no-underline hover:opacity-90\"\n    href=\"/docs/getting-started/react\">\n{`Getting Started Guide`}\n</a>\n\n## Supported Versions\n\nReact 18+ (tested with React 18 and React 19)\n","packages/lexical-website/docs/react/plugins.md":"# Lexical Plugins\n\n:::tip\n\nMost of these plugins are available as modern\n[Lexical Extensions](../extensions/intro.md), which do not require separate\nconfiguration and can have more efficient implementations.\nYou should prefer using extensions over plugins where available, and\n[migrate](../extensions/migration.mdx) any project to extensions with\nLexicalExtensionComposer or LexicalExtensionEditorComposer if it is still\nusing LexicalComposer.\n\n:::\n\nReact-based plugins use a Lexical editor instance from `<LexicalExtensionComposer>` or `<LexicalComposer>` (legacy) context:\n\n```js\nimport {LexicalComposer} from '@lexical/react/LexicalComposer';\nimport {PlainTextPlugin} from '@lexical/react/LexicalPlainTextPlugin';\nimport {ContentEditable} from '@lexical/react/LexicalContentEditable';\nimport {LexicalErrorBoundary} from '@lexical/react/LexicalErrorBoundary';\nimport {HistoryPlugin} from '@lexical/react/LexicalHistoryPlugin';\nimport {OnChangePlugin} from '@lexical/react/LexicalOnChangePlugin';\n```\n\n```jsx\nconst initialConfig = {\n  namespace: 'MyEditor',\n  theme,\n  onError,\n};\n\n<LexicalComposer initialConfig={initialConfig}>\n  <PlainTextPlugin\n    contentEditable={\n      <ContentEditable placeholder={<div>Enter some text...</div>} />\n    }\n    ErrorBoundary={LexicalErrorBoundary}\n  />\n  <HistoryPlugin />\n  <OnChangePlugin onChange={onChange} />\n  ...\n</LexicalComposer>;\n```\n\n> Note: Many plugins might require you to register the one or many Lexical nodes in order for the plugin to work. You can do this by passing a reference to the node to the `nodes` array in your initial editor configuration.\n\n```jsx\nconst initialConfig = {\n  namespace: 'MyEditor',\n  theme,\n  nodes: [ListNode, ListItemNode], // Pass the references to the nodes here\n  onError,\n};\n```\n\n### `LexicalPlainTextPlugin`\n\nReact wrapper for `@lexical/plain-text` that adds major features for plain text editing, including typing, deletion and copy/pasting.\n\n```jsx\n<PlainTextPlugin\n  contentEditable={\n    <ContentEditable\n      aria-placeholder={'Enter some text...'}\n      placeholder={<div>Enter some text...</div>}\n    />\n  }\n  ErrorBoundary={LexicalErrorBoundary}\n/>\n```\n\n:::tip\n\nUse [PlainTextExtension](/docs/api/modules/lexical_plain-text#plaintextextension) when using extensions\n\n:::\n\n### Configuring an error boundary for React extensions\n\nThe `ErrorBoundary` prop on `PlainTextPlugin` and `RichTextPlugin` is configured\nthrough `ReactExtension` when using `PlainTextExtension` or\n`RichTextExtension`. Add a configured `ReactExtension` to the root extension's\ndependencies:\n\n```tsx\nimport {configExtension, defineExtension} from 'lexical';\nimport {PlainTextExtension} from '@lexical/plain-text';\nimport {ReactExtension} from '@lexical/react/ReactExtension';\nimport {LexicalErrorBoundary} from '@lexical/react/LexicalErrorBoundary';\nimport type {ReactElement} from 'react';\n\ntype ErrorBoundaryProps = {\n  children: ReactElement;\n  onError: (error: Error) => void;\n};\n\nfunction CustomErrorBoundary({children, onError}: ErrorBoundaryProps) {\n  return (\n    <LexicalErrorBoundary\n      fallback={<div>Something went wrong.</div>}\n      onError={onError}>\n      {children}\n    </LexicalErrorBoundary>\n  );\n}\n\nconst editorExtension = defineExtension({\n  name: 'MyEditor',\n  dependencies: [\n    PlainTextExtension,\n    configExtension(ReactExtension, {\n      ErrorBoundary: CustomErrorBoundary,\n    }),\n  ],\n});\n```\n\nIf you do not provide `ErrorBoundary`, `ReactExtension` uses its default\n`LexicalErrorBoundary`. Replace `CustomErrorBoundary` with\n`LexicalErrorBoundary` when the default fallback and error reporting are\nsufficient.\n\n### `LexicalRichTextPlugin`\n\nReact wrapper for `@lexical/rich-text` that adds major features for rich text editing, including typing, deletion, copy/pasting, indent/outdent and bold/italic/underline/strikethrough text formatting.\n\n```jsx\n<RichTextPlugin\n  contentEditable={\n    <ContentEditable\n      aria-placeholder={'Enter some text...'}\n      placeholder={<div>Enter some text...</div>}\n    />\n  }\n  ErrorBoundary={LexicalErrorBoundary}\n/>\n```\n\n:::tip\n\nUse [RichTextExtension](/docs/api/modules/lexical_rich-text#richtextextension) when using extensions\n\n:::\n\n### `LexicalAutoFocusPlugin`\n\nAutomatically focuses the editor when the component mounts and optionally places the cursor at the start or end of the root content.\n\n```jsx\n<AutoFocusPlugin defaultSelection=\"rootEnd\" />\n```\n\n:::tip\n\nUse [AutoFocusExtension](/docs/api/modules/lexical_extension#autofocusextension) when using extensions\n\n:::\n\n### `LexicalOnChangePlugin`\n\nPlugin that calls `onChange` whenever Lexical state is updated. Using `ignoreHistoryMergeTagChange` (`true` by default) and `ignoreSelectionChange` (`false` by default) can give more granular control over changes that are causing `onChange` call.\n\n```jsx\n<OnChangePlugin onChange={onChange} />\n```\n\n:::tip\n\nWhen using extensions, subscribe to [EditorStateExtension](/docs/api/modules/lexical_extension#editorstateextension) for a signal-based equivalent\n\n:::\n\n### `LexicalHistoryPlugin`\n\nReact wrapper for `@lexical/history` that adds support for history stack management and `undo` / `redo` commands.\n\n```jsx\n<HistoryPlugin />\n```\n\n:::tip\n\nUse [HistoryExtension](/docs/api/modules/lexical_history#historyextension) when using extensions\n\n:::\n\n### `LexicalLinkPlugin`\n\nReact wrapper for `@lexical/link` that adds support for links, including `$toggleLink` command support that toggles link for selected text.\n\n```jsx\n<LinkPlugin />\n```\n\n:::tip\n\nUse [LinkExtension](/docs/api/modules/lexical_link#linkextension) when using extensions\n\n:::\n\n### `LexicalClickableLinkPlugin`\n\nMakes `LinkNode` elements in the editor clickable, navigating to the link URL on click. Set `disabled` to `true` to turn off the behavior, or `newTab` to control whether links open in a new tab.\n\n> Note: Requires `LinkNode` from `@lexical/link` to be registered.\n\n```jsx\n<ClickableLinkPlugin newTab={true} disabled={false} />\n```\n\n:::tip\n\nUse [ClickableLinkExtension](/docs/api/modules/lexical_link#clickablelinkextension) when using extensions\n\n:::\n\n### `LexicalListPlugin`\n\nReact wrapper for `@lexical/list` that adds support for lists (ordered and unordered)\n\n```jsx\n<ListPlugin />\n```\n\n:::tip\n\nUse [ListExtension](/docs/api/modules/lexical_list#listextension) when using extensions\n\n:::\n\n### `LexicalCheckListPlugin`\n\nReact wrapper for `@lexical/list` that adds support for check lists. Note that it requires some css to render check/uncheck marks. See PlaygroundEditorTheme.css for details.\n\n```jsx\n<CheckListPlugin />\n```\n\n:::tip\n\nUse [CheckListExtension](/docs/api/modules/lexical_list#checklistextension) when using extensions\n\n:::\n\n### `LexicalTablePlugin`\n\n[![See API Documentation](/img/see-api-documentation.svg)](/docs/api/modules/lexical_react_LexicalTablePlugin)\n\nReact wrapper for `@lexical/table` that adds support for tables.\n\n```jsx\n<TablePlugin />\n```\n\n:::tip\n\nUse [TableExtension](/docs/api/modules/lexical_table#tableextension) when using extensions\n\n:::\n\n### `LexicalHorizontalRulePlugin`\n\nRegisters a command handler for `INSERT_HORIZONTAL_RULE_COMMAND`. When dispatched, it inserts a `HorizontalRuleNode` at the current selection.\n\n> Note: Requires `HorizontalRuleNode` to be registered. This plugin is deprecated in favor of `HorizontalRuleExtension` from `@lexical/extension`.\n\n```jsx\nimport {HorizontalRuleNode} from '@lexical/react/LexicalHorizontalRuleNode';\n\nconst config = {nodes: [HorizontalRuleNode]};\n\n<HorizontalRulePlugin />\n```\n\n:::tip\n\nUse [HorizontalRuleExtension](/docs/api/modules/lexical_extension#horizontalruleextension) when using extensions\n\n:::\n\n### `LexicalTabIndentationPlugin`\n\nPlugin that allows tab indentation in combination with `@lexical/rich-text`.\n\n```jsx\n<TabIndentationPlugin />\n```\n\n:::tip\n\nUse [TabIndentationExtension](/docs/api/modules/lexical_extension#tabindentationextension) when using extensions\n\n:::\n\n### `LexicalHashtagPlugin`\n\nAutomatically detects hashtag patterns (e.g., `#hello`) in text as the user types and converts them into `HashtagNode` instances with distinct styling.\n\n> Note: Requires `HashtagNode` from `@lexical/hashtag` to be registered.\n\n```jsx\nimport {HashtagNode} from '@lexical/hashtag';\n\nconst config = {nodes: [HashtagNode]};\n\n<HashtagPlugin />\n```\n\n:::tip\n\nUse [HashtagExtension](/docs/api/modules/lexical_hashtag#hashtagextension) when using extensions\n\n:::\n\n### `LexicalAutoLinkPlugin`\n\nPlugin will convert text into links based on passed matchers list. In example below whenever user types url-like string it will automatically convert it into a link node\n\n> Note: Requires `AutoLinkNode` from `@lexical/link` to be registered.\n\n```jsx\nconst URL_MATCHER =\n  /((https?:\\/\\/(www\\.)?)|(www\\.))[-a-zA-Z0-9@:%._+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/;\n\nconst MATCHERS = [\n  (text) => {\n    const match = URL_MATCHER.exec(text);\n    if (match === null) {\n      return null;\n    }\n    const fullMatch = match[0];\n    return {\n      index: match.index,\n      length: fullMatch.length,\n      text: fullMatch,\n      url: fullMatch.startsWith('http') ? fullMatch : `https://${fullMatch}`,\n      // attributes: { rel: 'noreferrer', target: '_blank' }, // Optional link attributes\n    };\n  },\n];\n\n...\n\n<AutoLinkPlugin matchers={MATCHERS} />\n```\n\n:::tip\n\nUse [AutoLinkExtension](/docs/api/modules/lexical_link#autolinkextension) when using extensions\n\n:::\n\n### `LexicalAutoEmbedPlugin`\n\nWatches for pasted links that match any of the provided embed configurations (e.g., YouTube, Twitter URLs). When a match is found, it shows a menu offering to replace the link with an embedded node.\n\n> Note: Requires `LinkNode` and `AutoLinkNode` from `@lexical/link` to be registered.\n\n```jsx\nimport {LexicalAutoEmbedPlugin, AutoEmbedOption} from '@lexical/react/LexicalAutoEmbedPlugin';\nimport type {EmbedConfig} from '@lexical/react/LexicalAutoEmbedPlugin';\n\nconst YouTubeEmbedConfig = {\n  type: 'youtube',\n  parseUrl: (url) => {\n    const match = url.match(/youtube\\.com\\/watch\\?v=([a-zA-Z0-9_-]+)/);\n    return match ? {url, id: match[1]} : null;\n  },\n  insertNode: (editor, result) => {\n    // Insert your custom embed node here\n  },\n};\n\n<LexicalAutoEmbedPlugin\n  embedConfigs={[YouTubeEmbedConfig]}\n  onOpenEmbedModalForConfig={(config) => {/* open modal */}}\n  getMenuOptions={(config, embedFn, dismissFn) => [\n    new AutoEmbedOption('Embed', {onSelect: embedFn}),\n    new AutoEmbedOption('Dismiss', {onSelect: dismissFn}),\n  ]}\n/>\n```\n\nThe optional `$disableMenuFor` predicate suppresses the menu for specific locations, e.g. inside nodes that only accept inline content. It receives the matched `AutoLinkNode`; when it returns true the menu is not shown and the pasted link is left as-is. The predicate can inspect the node's ancestors, e.g. with `$getNearestNodeOfType` from `@lexical/utils`.\n\n```jsx\n<LexicalAutoEmbedPlugin\n  embedConfigs={[YouTubeEmbedConfig]}\n  $disableMenuFor={(linkNode) =>\n    $getNearestNodeOfType(linkNode, InlineOnlyNode) !== null\n  }\n  // ...\n/>\n```\n\n### `LexicalCharacterLimitPlugin`\n\nTracks the number of characters in the editor and renders a count of remaining characters. Uses `OverflowNode` to visually indicate when the limit is exceeded.\n\n> Note: Requires `OverflowNode` from `@lexical/overflow` to be registered.\n\n```jsx\nimport {CharacterLimitPlugin} from '@lexical/react/LexicalCharacterLimitPlugin';\n\n<CharacterLimitPlugin charset=\"UTF-16\" maxLength={280} />\n```\n\n:::tip\n\nUse [OverflowExtension](/docs/api/modules/lexical_overflow#overflowextension) when using extensions\n\n:::\n\n### `LexicalClearEditorPlugin`\n\nAdds `clearEditor` command support to clear editor's content.\n\n```jsx\n<ClearEditorPlugin />\n```\n\n:::tip\n\nUse [ClearEditorExtension](/docs/api/modules/lexical_extension#cleareditorextension) when using extensions\n\n:::\n\n### `LexicalMarkdownShortcutPlugin`\n\nAdds markdown shortcut support: headings, lists, code blocks, quotes, links and inline styles (bold, italic, strikethrough).\n\n```jsx\n<MarkdownShortcutPlugin />\n```\n\n### `LexicalTableOfContentsPlugin`\n\nThis plugin allows you to render a table of contents for a page from the headings from the editor. It listens to any deletions or modifications to those headings and updates the table of contents. Additionally, it's able to track any newly added headings and inserts them in the table of contents once they are created. This plugin also supports lazy loading - so you can defer adding the plugin until when the user needs it.\n\nIn order to use `TableOfContentsPlugin`, you need to pass a callback function in its children. This callback function gives you access to the up-to-date data of the table of contents. You can access this data through a single parameter for the callback which comes in the form of an array of arrays `[[headingKey, headingTextContent, headingTag], [], [], ...]`\n\n`headingKey`: Unique key that identifies the heading.\n`headingTextContent`: A string of the exact text of the heading.\n`headingTag`: The heading's tag — `'h1'` through `'h6'`. Every heading in the document is listed; the plugin does not filter by level.\n\n```jsx\n<TableOfContentsPlugin>\n  {(tableOfContentsArray) => {\n    return (\n      <MyCustomTableOfContentsPlugin tableOfContents={tableOfContentsArray} />\n    );\n  }}\n</TableOfContentsPlugin>\n```\n\n### `LexicalEditorRefPlugin`\n\nAllows you to get a ref to the underlying editor instance outside of LexicalComposer, which is convenient when you want to interact with the editor\nfrom a separate part of your application.\n\n```jsx\nconst editorRef = useRef(null);\n<EditorRefPlugin editorRef={editorRef} />;\n```\n\n### `LexicalCollaborationPlugin`\n\nIntegrates Yjs-based real-time collaborative editing into Lexical. Creates a Yjs binding between the editor state and a shared Yjs document, renders remote user cursors, and provides collaborative undo/redo history.\n\n```jsx\nimport {LexicalCollaboration} from '@lexical/react/LexicalCollaborationContext';\nimport {CollaborationPlugin} from '@lexical/react/LexicalCollaborationPlugin';\nimport {WebsocketProvider} from 'y-websocket';\nimport {Doc} from 'yjs';\n\nfunction createProvider(id, yjsDocMap) {\n  const doc = new Doc();\n  yjsDocMap.set(id, doc);\n  return new WebsocketProvider('ws://localhost:1234', id, doc);\n}\n\n<LexicalCollaboration>\n  <LexicalComposer initialConfig={initialConfig}>\n    <CollaborationPlugin\n      id=\"my-document\"\n      providerFactory={createProvider}\n      shouldBootstrap={true}\n      username=\"Alice\"\n      cursorColor=\"#FF0000\"\n    />\n  </LexicalComposer>\n</LexicalCollaboration>\n```\n\n### `LexicalDraggableBlockPlugin`\n\nAdds a draggable block handle that appears next to top-level block nodes when hovered. Users can drag and drop blocks to reorder them. A target line indicates the drop position.\n\n> Note: This plugin is experimental.\n\n```jsx\nimport {DraggableBlockPlugin_EXPERIMENTAL} from '@lexical/react/LexicalDraggableBlockPlugin';\n\nconst menuRef = useRef(null);\nconst targetLineRef = useRef(null);\n\n<DraggableBlockPlugin_EXPERIMENTAL\n  anchorElem={document.body}\n  menuRef={menuRef}\n  targetLineRef={targetLineRef}\n  menuComponent={<div ref={menuRef} className=\"drag-handle\">::</div>}\n  targetLineComponent={<div ref={targetLineRef} className=\"target-line\" />}\n  isOnMenu={(el) => menuRef.current?.contains(el) ?? false}\n/>\n```\n\n### `LexicalNodeEventPlugin`\n\nAttaches a DOM event listener on the editor root element that fires only when the event target corresponds to a node of the specified type.\n\n```jsx\nimport {NodeEventPlugin} from '@lexical/react/LexicalNodeEventPlugin';\n\n<NodeEventPlugin\n  nodeType={ImageNode}\n  eventType=\"click\"\n  eventListener={(event, editor, nodeKey) => {\n    console.log('Clicked on node:', nodeKey);\n  }}\n/>\n```\n\n### `LexicalNodeMenuPlugin`\n\nRenders a menu anchored to a specific Lexical node identified by `nodeKey`. Provides keyboard navigation (arrow keys, enter, escape) through the menu options.\n\n```jsx\nimport {LexicalNodeMenuPlugin, MenuOption} from '@lexical/react/LexicalNodeMenuPlugin';\n\n<LexicalNodeMenuPlugin\n  nodeKey={selectedNodeKey}\n  options={menuOptions}\n  onSelectOption={(option, textNode, closeMenu) => {\n    option.action();\n    closeMenu();\n  }}\n  menuRenderFn={(anchorRef, {options, selectedIndex, selectOptionAndCleanUp}) =>\n    anchorRef.current\n      ? createPortal(\n          <ul>\n            {options.map((opt, i) => (\n              <li key={opt.key} onClick={() => selectOptionAndCleanUp(opt)}>\n                {opt.title}\n              </li>\n            ))}\n          </ul>,\n          anchorRef.current,\n        )\n      : null\n  }\n  onClose={() => setSelectedNodeKey(null)}\n/>\n```\n\n### `LexicalTypeaheadMenuPlugin`\n\nProvides a typeahead/autocomplete menu that appears when the user types a trigger character (e.g., `@` for mentions, `:` for emoji). It listens to text changes, runs a trigger function against text before the cursor, and when a match is found, positions a menu at the trigger location.\n\n```jsx\nimport {\n  LexicalTypeaheadMenuPlugin,\n  useBasicTypeaheadTriggerMatch,\n} from '@lexical/react/LexicalTypeaheadMenuPlugin';\n\nconst triggerMatch = useBasicTypeaheadTriggerMatch('@', {minLength: 1});\n\n<LexicalTypeaheadMenuPlugin\n  triggerFn={triggerMatch}\n  options={mentionOptions}\n  onQueryChange={setQuery}\n  onSelectOption={(option, textNode, closeMenu) => {\n    editor.update(() => {\n      const mentionNode = $createMentionNode(option.name);\n      if (textNode) {\n        textNode.replace(mentionNode);\n      }\n      closeMenu();\n    });\n  }}\n  menuRenderFn={(anchorRef, {options, selectedIndex, selectOptionAndCleanUp}) =>\n    anchorRef.current\n      ? createPortal(\n          <ul className=\"typeahead-menu\">\n            {options.map((opt, i) => (\n              <li\n                key={opt.key}\n                className={i === selectedIndex ? 'selected' : ''}\n                onClick={() => selectOptionAndCleanUp(opt)}>\n                {opt.name}\n              </li>\n            ))}\n          </ul>,\n          anchorRef.current,\n        )\n      : null\n  }\n/>\n```\n\n### `LexicalSelectionAlwaysOnDisplay`\n\nBy default, browser text selection becomes invisible when clicking away from the editor. This plugin ensures the selection remains visible.\n\n```jsx\n<SelectionAlwaysOnDisplay />\n```\n\n:::tip\n\nUse [SelectionAlwaysOnDisplayExtension](/docs/api/modules/lexical_extension#selectionalwaysondisplayextension) when using extensions\n\n:::\n","packages/lexical-website/docs/serialization/_category_.json":"{\n  \"label\": \"Serialization\",\n  \"position\": 4\n}\n","packages/lexical-website/docs/serialization/dom-import.md":"# DOMImportExtension\n\n:::warning Experimental\n\n`DOMImportExtension` and everything described on this page are marked\n`@experimental` and may change between any two Lexical releases —\nincluding breaking renames, signature changes, or behavior changes —\nuntil the API stabilizes. We track issues and proposals in the\n[GitHub repo](https://github.com/facebook/lexical); breaking changes\nwill be called out in release notes. Apps that depend on this\npipeline should pin their Lexical version and treat upgrades as\nintentional.\n\nThe legacy static `importDOM` machinery and `$generateNodesFromDOM`\nentry are unchanged and remain the supported default for production\napps that don't want to track an experimental API.\n\nThe DOMImportExtension API was introduced in Lexical v0.45.0\n\n:::\n\nThe DOM import system in `@lexical/html` lets you convert any HTML or\nDOM tree into Lexical nodes. The legacy entry — the static\n`importDOM` declared on each node class — still works, but for new\ncode we offer an extension-based pipeline (`DOMImportExtension`) with\ntyped selectors, middleware-style rules, structural schemas,\nconfigurable whitespace handling, and a dedicated context system. It's\ndesigned for performance, ergonomics, and composability across\nextensions.\n\nThe new pipeline ships side-by-side with the legacy one in\n`@lexical/html`; the default `$generateNodesFromDOM` is unchanged. The\nnode-providing extensions (`RichTextExtension`, `ListExtension`,\n`LinkExtension`, `TableExtension`, `CodeExtension`, …) register their\nown import rules (plus the shared `CoreImportExtension` baseline), so\nan editor built from them already has a fully configured\n`DOMImportExtension` — to opt in, just read the imported nodes from\nthe extension's `$generateNodesFromDOM` output (or add\n`ClipboardDOMImportExtension` to route pastes through it).\n\n## Quick start\n\nThe smallest editor that imports HTML through the extension pipeline:\n\n```ts\nimport {\n  buildEditorFromExtensions,\n  getExtensionDependencyFromEditor,\n} from '@lexical/extension';\nimport {\n  CoreImportExtension,\n  DOMImportExtension,\n} from '@lexical/html';\nimport {$getRoot, defineExtension} from 'lexical';\n\nconst editor = buildEditorFromExtensions(\n  defineExtension({\n    name: 'app',\n    dependencies: [CoreImportExtension],\n  }),\n);\n\nconst dep = getExtensionDependencyFromEditor(editor, DOMImportExtension);\neditor.update(() => {\n  const dom = new DOMParser().parseFromString(\n    '<p>Hello <strong>world</strong></p>',\n    'text/html',\n  );\n  const nodes = dep.output.$generateNodesFromDOM(dom);\n  $getRoot().clear().splice(0, 0, nodes);\n});\n```\n\n`CoreImportExtension` ships the rules that cover `<p>`, `<span>`,\ninline format tags (`<b>`, `<strong>`, `<em>`, `<i>`, `<code>`,\n`<mark>`, `<s>`, `<sub>`, `<sup>`, `<u>`), `<br>`, and `#text` —\neverything the core lexical package's legacy `importDOM` machinery\nhandled — plus `<hr>`, gated on `HorizontalRuleNode` being registered\n(the node lives in `@lexical/extension`, which is upstream of\n`@lexical/html` and so cannot register import rules itself).\n\nThe per-tag rules ship with the extensions that provide the nodes, so\nlisting the node extensions you want is the entire import\nconfiguration:\n\n| Extension | Provides |\n| --- | --- |\n| `CoreImportExtension` (`@lexical/html`) | `<p>`, `<span>`, inline format tags, `<br>`, `#text`, `<hr>` (when `HorizontalRuleNode` is registered) |\n| `RichTextExtension` (`@lexical/rich-text`) | `<h1>`–`<h6>`, `<blockquote>`, Google Docs 26pt-title heuristic |\n| `ListExtension` (`@lexical/list`) | `<ol>`, `<ul>` (incl. checklist detection), `<li>`, GitHub task list, Joplin checkbox |\n| `LinkExtension` (`@lexical/link`) | `<a>` |\n| `TableExtension` (`@lexical/table`) | `<table>`, `<tr>`, `<td>`, `<th>` |\n| `CodeExtension` (`@lexical/code-core`) | `<pre>`, multi-line `<code>`, monospace `<div>`, GitHub raw-file-view tables |\n\nEach of these depends on `CoreImportExtension` itself, so the shared\nbaseline comes along automatically. The rules are inert unless the\neditor actually routes HTML through the pipeline (the legacy paste\npath is unchanged), and an extension's rules merge above its\ndependencies' rules, so app-registered rules still win dispatch.\n\n:::tip\n\nThe legacy entry `$generateNodesFromDOM(editor, dom)` and the new\nextension's `dep.output.$generateNodesFromDOM(dom)` both live in\n`@lexical/html`. Tell them apart by signature: the legacy takes the\neditor first, the new one takes only the DOM (plus optional\n`{context, preprocess}`).\n\n:::\n\n## Rules\n\nA rule is the unit of work in the extension. Each rule is built with\n`defineImportRule`:\n\n```ts\nimport {defineImportRule, sel} from '@lexical/html';\nimport {$createParagraphNode} from 'lexical';\n\nconst ParagraphRule = defineImportRule({\n  name: '@app/p',\n  match: sel.tag('p'),\n  $import: (ctx, el) => {\n    const p = $createParagraphNode();\n    p.splice(0, 0, ctx.$importChildren(el));\n    return [p];\n  },\n});\n```\n\nA rule has three parts:\n\n1. **`match`** — an opaque `CompiledSelector` produced by `sel.*`. The\n   dispatcher buckets rules by tag at compile time so the per-node\n   dispatch cost is bounded by the number of rules registered for\n   that tag (typically one or two).\n2. **`$import`** — middleware-style function `(ctx, node, $next) =>\n   readonly LexicalNode[]`. The element type of `node` is narrowed by\n   the selector (e.g. `sel.tag('a')` gives `HTMLAnchorElement`,\n   `sel.text()` gives `Text`). Return `[]` to drop the element, or\n   call `$next()` to defer to the next-lower matching rule.\n3. **`name`** (optional) — surfaces in dev-mode warnings and error\n   traces. Convention: `'@scope/package/rule-id'`.\n\n### Dispatch order\n\nThere is no numeric priority. `DOMImportConfig.rules` is a single flat,\nordered list, and dispatch walks it **front to back**: for a given DOM\nnode the dispatcher visits each rule whose `match` accepts that node, in\nlist order, and the first rule that returns without calling `$next()`\ndecides the outcome. \"Higher priority\" and \"earlier in the list\" are the\nsame thing.\n\nSo within one `configExtension(DOMImportExtension, {rules})` call, the\nrules run in the order you wrote them:\n\n```ts\nconfigExtension(DOMImportExtension, {\n  rules: [\n    // Tries this first\n    SpecificParagraphRule,\n    // Falls back to this via $next()\n    GenericParagraphRule,\n  ],\n})\n```\n\nCalling `$next()` from a rule walks the chain — the next matching rule\nin the list fires; if none is left, the framework's catch-all\n`DefaultHoistRule` descends into the element's children. Returning `[]`\nfrom a rule short-circuits: nothing else runs and the element is\ndropped.\n\n#### How the list is assembled\n\nMany extensions contribute to the same list. `DOMImportExtension`'s\n`mergeConfig` **prepends** each contribution to what has accumulated so\nfar, and the extension builder merges configs in dependency order — a\ndependency's contribution is merged before the contribution of the\nextension that depends on it. Two consequences:\n\n- **Each contribution stays contiguous and keeps its own order.** Your\n  `rules` array is inlined as one chunk, first entry first. Ordering\n  *within* your own rules is entirely up to you.\n- **The chunks run most-dependent first.** An extension's rules are\n  tried before the rules contributed by its dependencies. That is what\n  makes overriding work: `@lexical/rich-text` depends on\n  `CoreImportExtension`, so its `<p>` rule is reached before the core\n  one, and an app extension that depends on `@lexical/rich-text` gets to\n  go ahead of both.\n\nConcretely, for an app extension that depends on `RichTextExtension`\n(which in turn depends on `CoreImportExtension`), the compiled list is:\n\n```\n[ …rules passed to buildEditorFromExtensions… ]  ← merged last, highest priority\n[ …the app extension's rules…                 ]\n[ …@lexical/rich-text's rules…                ]\n[ …CoreImportExtension's rules…               ]\n[ DefaultHoistRule                            ]  ← the base config, always last\n```\n\nConfiguration handed straight to `buildEditorFromExtensions` is merged\nafter every extension's, so it outranks all of them. `DefaultHoistRule`\nis `DOMImportExtension`'s own default `config.rules` entry, so\neverything else lands in front of it and it is always the last rule\ntried.\n\n:::caution\n\nThe relative order of two extensions where neither transitively depends\non the other falls out of the topological sort and is not part of the\nAPI. If extension **A** must override a rule from extension **B**, say\nso structurally — make **B** a dependency of **A** — rather than relying\non the order they happen to be listed in.\n\n:::\n\n### `$next()` as a wrapper\n\nThe middleware shape is strictly more powerful than the legacy\nnumeric `priority`. A rule can call `$next()`, inspect the result,\nand transform it — perfect for cross-cutting decorators:\n\n```ts\nconst IdAttributeRule = defineImportRule({\n  name: '@app/id-decorator',\n  match: sel.any().attr('id', /\\S/),\n  $import: (ctx, el, $next) => {\n    const out = $next();\n    if (out.length === 1) {\n      $setState(out[0], idState, el.getAttribute('id')!);\n    }\n    return out;\n  },\n});\n```\n\nPlaced early in your `rules` array, this rule fires for every element\ncarrying an `id` BEFORE the tag-specific rule, calls `$next()` to get\nthe produced node, and tags it with state. No tag-specific machinery\nneeds to know about `id`.\n\n## Selectors\n\nBuild selectors with the combinator API:\n\n```ts\nsel.any()                                       // any HTMLElement\nsel.tag('a')                                    // <a> only (typed as HTMLAnchorElement)\nsel.tag('h1', 'h2', 'h3', 'h4', 'h5', 'h6')     // typed as HTMLHeadingElement\nsel.text()                                      // text nodes\nsel.comment()                                   // comment nodes\nsel.tag('li').classAll('task-list-item')        // <li class=\"task-list-item …\">\nsel.tag('span').classAny('hl', 'mark')          // <span class=\"hl|mark …\">\nsel.tag('a').attr('href', /^https:/)            // <a href> matching a regex\nsel.tag('a').attr('target', true)               // attribute present\nsel.tag('a').attr('href', '/wiki')              // exact value\nsel.tag('span').styleAny('font-size', /^(\\d+)pt/) // inline-style match\n```\n\nA CSS-subset parser is also available for terse selectors:\n\n```ts\nsel.css('p.google-docs-title')      // tag + class\nsel.css('h1, h2, h3, h4, h5, h6')   // tag list\nsel.css('img[src]')                 // attribute presence\nsel.css('a[href=\"/wiki\"]')          // attribute equality\n```\n\n`sel.css` covers the basics (tag, `.class`, `#id`, `[attr]`,\n`[attr=\"val\"]`, comma-separated lists, `*`). For anything outside that\nsubset (regex attribute, inline-style match, capturing), chain\ncombinator methods off the result:\n\n```ts\nsel.css('pre').attr('class', /(?:^|\\s)language-(\\S+)/, {capture: 'lang'})\n```\n\nThe grammar deliberately excludes descendant combinators (`>`, ` `)\nand `:not` — see [Context](#context) below for the idiomatic\nreplacements.\n\n### Captured matches\n\nPass `{capture: 'name'}` to `attr` or `styleAny` and the\n`RegExpMatchArray` for the successful match is surfaced on\n`ctx.captures.name`, fully typed:\n\n```ts\nconst CodeBlockRule = defineImportRule({\n  name: '@app/pre-code',\n  match: sel\n    .tag('pre')\n    .attr('class', /(?:^|\\s)language-(\\S+)/, {capture: 'lang'}),\n  $import: (ctx, el) => {\n    // ctx.captures.lang: RegExpMatchArray\n    const language = ctx.captures.lang[1];\n    const node = $createCodeNode(language);\n    node.splice(0, 0, ctx.$importChildren(el));\n    return [node];\n  },\n});\n```\n\nThe selector's element type and captures map both flow into `$import`'s\nsignature, so you never need to cast `el` or re-run the regex inside\nthe rule body.\n\n:::tip\n\nIf your selector becomes a runtime function (a `(node) => boolean`\nguard), the rule lands in the wildcard bucket and is consulted for\nevery element — expensive. Push as much as possible into the selector\nitself; do the remaining refinement inside the rule body, calling\n`$next()` to defer when the body decides not to handle the element.\n\n```ts\ndefineImportRule({\n  match: sel.tag('img'),  // tag bucket — cheap\n  $import: (ctx, el, $next) => {\n    if (!el.src || el.src.startsWith('data:')) {\n      return $next();      // body refinement\n    }\n    return [$createImageNode({src: el.src, alt: el.alt})];\n  },\n});\n```\n\n:::\n\n## Schemas\n\nA rule's `$import` body uses `ctx.$importChildren(parent, {schema})`\nto recursively import children. The schema enforces what node types\nare allowed in this position and how to package non-conforming runs:\n\n```ts\ninterface ChildSchema {\n  $accepts(child: LexicalNode, parent: LexicalNode | null): boolean;\n  $packageRun?(rejected: LexicalNode[], ...): LexicalNode[];\n  onReject?: 'hoist' | 'drop';\n  $finalize?(children: LexicalNode[], ...): LexicalNode[];\n}\n```\n\nThe `$`-prefixed members are invoked inside the import walk (which\nalready runs inside `editor.update`), so they can call `$is*`\npredicates and node methods on the `LexicalNode` arguments directly.\n\nBuilt-ins, all from `@lexical/html`:\n\n| Schema | Accepts | Rejected runs |\n| --- | --- | --- |\n| `BlockSchema` | Block ElementNodes and block DecoratorNodes | Wrapped in a fresh `ParagraphNode` (preserves `text-align`) |\n| `RootSchema` | Same as `BlockSchema` — aliased for clarity at the entry point | Same |\n| `InlineSchema` | Inline ElementNodes, TextNodes, inline DecoratorNodes | Dropped |\n| `NestedBlockSchema` | Block nodes | Inline runs pass through unchanged (no extra paragraph wrapping) |\n\nPer-package schemas:\n\n| Schema | Accepts | Rejected runs |\n| --- | --- | --- |\n| `ListSchema` (`@lexical/list`) | `ListItemNode`, nested `ListNode` | Wrapped in a synthetic `ListItemNode` (no intermediate paragraph) |\n| `TableSchema` (`@lexical/table`) | `TableRowNode` | If all cells, wrapped in a synthetic `TableRowNode`; else dropped |\n| `TableRowSchema` (`@lexical/table`) | `TableCellNode` | Dropped |\n\nA rule typically passes a schema when it knows its children's\nallowed type — the legacy `wrapContinuousInlines` and\n`ArtificialNode__DO_NOT_USE` cases are now `BlockSchema` and\n`NestedBlockSchema` respectively:\n\n```ts\nconst HeadingRule = defineImportRule({\n  match: sel.tag('h1', 'h2', 'h3', 'h4', 'h5', 'h6'),\n  $import: (ctx, el) => {\n    const node = $createHeadingNode(el.nodeName.toLowerCase() as HeadingTagType);\n    node.splice(0, 0, ctx.$importChildren(el, {schema: InlineSchema}));\n    return [node];\n  },\n});\n```\n\nCustom schemas implement `ChildSchema` directly. `$accepts`,\n`$packageRun`, and `$finalize` are invoked inside the editor context\nof the surrounding walk, so they receive `LexicalNode` arguments and\ncan call `$is*` predicates / node methods without setting up their\nown `editor.read`.\n\n### When `$packageRun` runs and `onReject` doesn't\n\nThe schema walks the assembled child array once, partitioning into\naccepted children (passed through individually) and maximal runs of\nnon-accepted children. For each rejected run, `$packageRun` runs\nfirst; if it's missing or returns `[]`, `onReject` decides:\n\n- `'drop'` (default) silently discards the run.\n- `'hoist'` emits the run unchanged at the same position. This\n  surfaces the mixed content to the calling rule's parent — only\n  useful when the rule's caller is prepared to receive mixed\n  children. It does NOT lift the run *out* of the calling rule's\n  parent.\n\n`$packageRun` is the right hook when the rejected content has a\nsensible re-shape that still belongs *inside* the rule's parent —\ne.g. `BlockSchema` wraps a run of inline siblings in a\n`ParagraphNode`, `TableSchema` wraps cells in a synthesized row.\n\n### Lifting blocks out of an inline parent\n\nSchemas cannot change the parent the rule already created. The\ncanonical example is an inline element with block children:\n\n```html\n<a href=\"link\"><h1>some text</h1><div>more text</div></a>\n```\n\nThe desired output is to lift each block out of the link and\nredistribute the link onto the block's inline contents:\n\n```\nHeadingNode\n└─ LinkNode(\"link\")\n   └─ TextNode(\"some text\")\nParagraphNode\n└─ LinkNode(\"link\")\n   └─ TextNode(\"more text\")\n```\n\n`InlineSchema` doesn't help here: with `onReject: 'drop'` the\nheadings are lost, with `'hoist'` they emerge unwrapped, and there's\nno `$packageRun` shape that produces \"a list of blocks each holding\nthe original inline wrapper\" because the schema sees only the\nchildren, not the parent.\n\nThis is a rule-level concern, but `@lexical/html` ships the\n`$distributeInlineWrapper(children, $makeWrapper)` helper so rules\nthat produce an inline parent (e.g. `LinkNode`, `MarkNode`) can\nopt in with one line:\n\n```ts\nimport {\n  $distributeInlineWrapper,\n  defineImportRule,\n  sel,\n} from '@lexical/html';\nimport {$createLinkNode} from '@lexical/link';\n\nconst AnchorRule = defineImportRule({\n  match: sel.tag('a'),\n  $import: (ctx, el) => {\n    const href = el.getAttribute('href') || '';\n    return $distributeInlineWrapper(ctx.$importChildren(el), () =>\n      $createLinkNode(href),\n    );\n  },\n});\n```\n\nThe helper walks the children: each block is descended into and its\ninline content is wrapped with a fresh instance from `$makeWrapper`;\neach contiguous inline run at the top level becomes its own wrapped\nnode. The enclosing `BlockSchema` then packages the inline-wrapped\nruns into paragraphs as usual.\n\n## Context\n\n`ImportContext` is the per-rule environment exposed as `ctx` to\n`$import`. It provides:\n\n- `ctx.captures` — typed regex captures from this rule's selector.\n- `ctx.get(cfg)` — read an `ImportStateConfig` value.\n- `ctx.$importChildren(parent, opts)` — recurse into children.\n- `ctx.$importOne(node, opts)` — recurse into one node.\n- `ctx.session` — per-import mutable store (see [Sessions](#sessions)).\n\n### State configs and `ctx.get`\n\n`createImportState` mints a typed key, similar to `createState` for\nnode state but scoped to the import pipeline:\n\n```ts\nimport {createImportState} from '@lexical/html';\n\nconst ImportMode = createImportState<'paste' | 'deserialize'>(\n  'app/importMode',\n  () => 'paste',\n);\n```\n\nRules read it with `ctx.get(ImportMode)`. The value is supplied\neither as a `contextDefaults` entry on the extension config (sticks\nfor every import call) or as a per-call `context` override on\n`$generateNodesFromDOM`:\n\n```ts\nimport {contextValue} from '@lexical/html';\n\ndep.output.$generateNodesFromDOM(dom, {\n  context: [contextValue(ImportMode, 'deserialize')],\n});\n```\n\nA nested `$importChildren` can also branch context for its descendants\nvia `opts.context`:\n\n```ts\nimport {contextValue, ImportTextFormat} from '@lexical/html';\nimport {IS_BOLD} from 'lexical';\n\nctx.$importChildren(el, {\n  context: [contextValue(ImportTextFormat, ctx.get(ImportTextFormat) | IS_BOLD)],\n});\n```\n\nBranched values are restored on the way back out — siblings outside\nthe branched subtree see the unchanged inherited value.\n\n### Built-in states\n\n`@lexical/html` ships several states out of the box:\n\n- **`ImportSource`** (`'paste' | 'unknown'`) — identifies how this\n  import started. Defaults to `'unknown'`; the clipboard handler sets\n  `'paste'`. Rules can branch on it to adapt behavior (e.g. preserve\n  more whitespace on `'paste'`). Apps that need a richer vocabulary\n  (e.g. `'drop'`, `'deserialize'`) can define their own\n  `ImportStateConfig` and pass it through the same `context` slot.\n- **`ImportSourceDataTransfer`** (`DataTransfer | null`) — the original\n  `DataTransfer` the paste/drop came from, surfaced so rules can peek\n  at companion MIME types or attached files. `null` outside clipboard\n  imports. See [below](#importsourcedatatransfer).\n- **`ImportTextFormat`** (`number`, a `TextFormatType` bitmask) —\n  used by the inline-format rules (`<b>`, `<strong>`, `<em>`, …) to\n  propagate format bits to descendant TextNodes. Replaces the legacy\n  `forChild` chain that the inline tags previously used. Build your\n  own format propagation by branching this state.\n- **`ImportTextStyle`** (`Readonly<Record<string, string>>`) — parsed\n  CSS-property record (e.g. `{color: 'red', 'font-family': 'serif'}`)\n  inherited by descendant TextNodes and materialized onto their\n  `style` string. Used by the core `#text` rule to pick up\n  inheritable cell/span styles like `color`. Properties already\n  covered by `ImportTextFormat` (`font-weight`, `font-style`,\n  `text-decoration`, `vertical-align`) are filtered out so they\n  remain owned by the format-bit path.\n- **`ImportOverlays`** — session slot (`readonly CompiledOverlayRules[]`)\n  holding overlays installed during the preprocess phase. The walker\n  primes its overlay stack from this list before starting, so a\n  preprocess can scope an overlay to the whole document based on a\n  signal it detected (e.g. a Word `Generator` meta tag). See\n  [Preprocessors](#preprocessors).\n- **`ImportWhitespaceConfig`** — controls text-node whitespace\n  handling (which DOM elements preserve whitespace, which count as\n  inline siblings). See below.\n\n### `ImportWhitespaceConfig`\n\nThe text-node whitespace handler walks DOM siblings to decide\nwhether trailing/leading spaces should be preserved (the neighbor is\ninline text) or collapsed (the neighbor is a block boundary). Two\npredicates govern this:\n\n```ts\ninterface WhitespaceImportConfig {\n  preservesWhitespace: (node: Node) => boolean;\n  isInline: (node: Node) => boolean;\n}\n```\n\nThe defaults match the legacy behavior: an element preserves\nwhitespace if its `nodeName` is `'PRE'` or its inline `style.whiteSpace`\nstarts with `'pre'`; an element is inline if its inline `style.display`\nstarts with `'inline'` or its tag name is in the standard inline-tag\nset used by lexical core's `isInlineDomNode` / `isBlockDomNode`\nhelpers (`a`, `abbr`, `acronym`, `b`, `cite`, `code`, `del`, `em`,\n`i`, `ins`, `kbd`, `label`, `mark`, `output`, `q`, `ruby`, `s`,\n`samp`, `span`, `strong`, `sub`, `sup`, `time`, `u`, `tt`, `var`).\nThe canonical list lives in `packages/lexical/src/LexicalUtils.ts` —\nsee the module-private `INLINE_TAG_RE` / `BLOCK_TAG_RE` constants if you\nwant to inspect the defaults.\n\nTo recognize custom tags (e.g. a custom `<tooltip>` that should be\ntreated as inline so the spaces around it survive), override the\npredicate:\n\n```ts\nimport {\n  contextValue,\n  defaultIsInline,\n  defaultPreservesWhitespace,\n  ImportWhitespaceConfig,\n} from '@lexical/html';\nimport {isHTMLElement} from 'lexical';\n\nconfigExtension(DOMImportExtension, {\n  contextDefaults: [\n    contextValue(ImportWhitespaceConfig, {\n      isInline: (node) =>\n        defaultIsInline(node) ||\n        (isHTMLElement(node) && node.nodeName === 'TOOLTIP'),\n      preservesWhitespace: defaultPreservesWhitespace,\n    }),\n  ],\n})\n```\n\nThis is the declarative replacement for setting `display: inline` on\nthe element from inside an extended TextNode importer.\n\n## Sessions\n\n`ImportSession` is the **root-layer context record** of the walk —\na mutable, document-order-shared store on `ctx`. Use it to make\ninformation from a node visited earlier in the document available\nto nodes visited later — e.g. a `<style>` / `<meta>` tag at the\ntop of the document influencing how later elements are interpreted.\n\nThe session and the scoped context share the same\n`ImportStateConfig` keys you create with `createImportState`:\n\n- `ctx.get(cfg)` reads through the current branch's prototype chain\n  (a `$importChildren({context})` branch layered above the session\n  record, falling through to the session record itself, falling\n  through to the editor's `contextDefaults`, then to the config's\n  default).\n- `ctx.session.get(cfg)` reads the same chain starting at the\n  session record (skips any active branch overlay).\n- `ctx.session.set(cfg, v)` / `update` mutate the session record's\n  own properties. Every unshadowed `ctx.get(cfg)` later in the walk\n  (including in not-yet-entered branches) picks up the new value.\n- A branch that writes `cfg` shadows the session value for the\n  duration of the branch only; on return, scoped reads see the\n  session value again.\n\n```ts\nimport {createImportState} from '@lexical/html';\n\nconst Stylesheets = createImportState<string[]>(\n  'app/stylesheets',\n  () => [],\n);\n\nconst CollectStyleSheetsRule = defineImportRule({\n  match: sel.tag('style'),\n  $import: (ctx, el) => {\n    ctx.session.update(Stylesheets, (prev) => [\n      ...prev,\n      el.textContent ?? '',\n    ]);\n    return [];\n  },\n});\n\nconst ConsumesStyleSheetsRule = defineImportRule({\n  match: sel.tag('article'),\n  $import: (ctx, el) => {\n    const sheets = ctx.session.get(Stylesheets);\n    // ... use the collected stylesheet text however the app needs\n  },\n});\n```\n\nA fresh session record is created for every `$generateNodesFromDOM`\ncall. It chains to the ambient import context when the call runs\nnested inside another import operation — a rule re-entering the walk\nfor sub-content, or raw HTML inside a `@lexical/mdast` Markdown import\n— so states layered by the outer operation stay readable; the\noutermost call chains to the editor's `contextDefaults`. Per-call\n`options.context` pairs are seeded into it before any preprocessors\nrun, and preprocess-time `ctx.session.set` writes mutate the same\nrecord (never the parent, so session writes don't leak outward).\n\n## Preprocessors\n\nBefore walking begins, `DOMImportExtension` runs a stack of\npreprocessors against the input DOM. They're middleware-shaped:\n\n```ts\ntype DOMPreprocessFn = (\n  dom: Document | ParentNode,\n  ctx: DOMPreprocessContext,\n  $next: () => void,\n) => void;\n```\n\nEach step can:\n\n- Mutate the DOM in place (e.g. inline stylesheet rules onto\n  matching elements, strip unsafe nodes, normalize attributes,\n  resolve relative URLs).\n- Write to `ctx.session` — the session IS the root layer of the\n  walk's context, so writes are visible to both `ctx.session.get`\n  and to unshadowed `ctx.get(cfg)` reads in any branch.\n- Call `$next()` to defer to the next-lower preprocessor (top of stack\n  runs first); skip the call to short-circuit.\n\nThe default config registers\n[`$inlineStylesFromStyleSheets`](#inlinestylesfromstylesheets) — the\nsame Excel-flavored CSS preprocess the legacy\n`$generateNodesFromDOM` uses. Apps append more preprocessors via\n`DOMImportConfig.preprocess` or per-call via\n`GenerateNodesFromDOMOptions.preprocess`.\n\n```ts\nconst $stripScripts: DOMPreprocessFn = (dom, _ctx, $next) => {\n  const root = 'body' in dom ? dom.body : (dom as ParentNode);\n  for (const el of Array.from(root.querySelectorAll('script'))) {\n    el.remove();\n  }\n  $next();\n};\n\nconfigExtension(DOMImportExtension, {\n  preprocess: [$stripScripts],\n})\n```\n\n### Preprocess order\n\n`preprocess` composes in the opposite array direction from\n[`rules`](#dispatch-order) but produces the same precedence. `mergeConfig`\n**appends** each contribution to the accumulated stack, and the runner\nstarts at the **last** entry and works backwards. So:\n\n- Within one contribution, the **last** entry runs first and wraps the\n  ones written before it.\n- Because configs are merged in dependency order, an extension's\n  preprocessors run before — and can wrap, via `$next()` — those of its\n  dependencies. `$inlineStylesFromStyleSheets`, sitting in the extension's\n  own base config, runs last of all, and only if every preprocessor above\n  it calls `$next()`.\n- Per-call preprocessors from `GenerateNodesFromDOMOptions.preprocess` are\n  appended on top of the configured stack, so they run **before** the\n  configured ones.\n\nSkipping `$next()` is therefore a real decision: it drops every\nlower-priority preprocessor, including the built-in stylesheet inlining.\n\n### Reading meta tags into context\n\nA common pattern: a preprocess step inspects a `<meta>` tag (or any\nDOM marker), then layers a typed context value the rest of the walk\ncan branch on.\n\n```ts\nimport {createImportState} from '@lexical/html';\n\nconst DocumentLanguage = createImportState<string>(\n  'app/documentLanguage',\n  () => 'unknown',\n);\n\nconst $readMetaLang: DOMPreprocessFn = (dom, ctx, $next) => {\n  const root = 'body' in dom ? dom.body : (dom as ParentNode);\n  const meta = root.querySelector('meta[name=\"content-language\"]');\n  if (meta && meta.getAttribute('content')) {\n    ctx.session.set(DocumentLanguage, meta.getAttribute('content')!);\n  }\n  $next();\n};\n```\n\n### `$inlineStylesFromStyleSheets`\n\nThe default preprocessor resolves CSS rules from `<style>` tags onto\nmatching elements as inline styles, so the rest of the pipeline can\nread those styles via plain `el.style.foo`. Apps like Excel paste\nHTML in this shape; the preprocess collapses the indirection so rule\nselectors and bodies don't need to chase stylesheets themselves.\nRe-exported as a `DOMPreprocessFn` for apps that want to compose\ntheir own preprocess pipeline:\n\n```ts\nimport {$inlineStylesFromStyleSheets} from '@lexical/html';\n\nconfigExtension(DOMImportExtension, {\n  preprocess: [\n    $appPreprocess,\n    // The stack runs from the END of the array, so listing the inliner\n    // last makes it run FIRST — useful when `$appPreprocess` expects the\n    // styles to already be inlined. (The copy in the extension's base\n    // config runs at the very bottom of the stack, after everything\n    // else, so re-listing it here is how you pull it forward.)\n    $inlineStylesFromStyleSheets,\n  ],\n});\n```\n\nSee [Preprocess order](#preprocess-order) for the full composition rules.\n\n## `$importChildren` rules overlay\n\n`ctx.$importChildren(parent, {rules: defineOverlayRules([...])})`\ninstalls an **overlay** dispatcher active only for the duration of\nthat children traversal (and nested `$importChildren` calls that\ndon't push their own overlay). Overlay rules are checked BEFORE the\nmain dispatcher; calling `$next()` from an overlay rule falls through\nto the next overlay-or-main rule.\n\nThis lets you scope cost-bearing rules to the subtrees where they\napply, rather than paying the predicate cost on every paste. The\nGitHub raw-file-view code-table rule in `@lexical/code-core` uses\nthe overlay to unwrap `<tr>` / `<td>` only while processing a code\ntable:\n\n```ts\n// Compile once at module scope. `defineOverlayRules` builds the same\n// tag-bucketed dispatch table the main config uses, so each\n// $importChildren call reuses it instead of recompiling.\nconst GitHubCodeTableOverlayRules = defineOverlayRules([\n  defineImportRule({\n    match: sel.tag('tr', 'td'),\n    $import: (ctx, el) => ctx.$importChildren(el),\n    name: '@lexical/code/github-code-table/unwrap',\n  }),\n]);\n\nconst GitHubCodeTableRule = defineImportRule({\n  match: sel.tag('table').classAll('js-file-line-container'),\n  $import: (ctx, el) => {\n    const node = $createCodeNode();\n    node.splice(\n      0,\n      0,\n      ctx.$importChildren(el, {rules: GitHubCodeTableOverlayRules}),\n    );\n    return [node];\n  },\n});\n```\n\nOutside the code table, the `tr` / `td` rule from `@lexical/table` is\nthe only one consulted; the overlay isn't installed unless we enter\na code-table subtree.\n\n`ImportChildrenOpts.rules` only accepts a `CompiledOverlayRules`\nproduced by `defineOverlayRules` — a raw rule array won't typecheck.\nThis is by design: a one-off overlay built inline on every walk would\nrecompile the dispatcher each time, defeating the point of the overlay\nbeing a fast-path.\n\n### Composing overlays\n\n`defineOverlayRules` accepts both raw `DOMImportRule`s and other\n`CompiledOverlayRules` in the same list (the union is\n`DOMImportRuleEntry`). Existing overlays are inlined at their position\nin priority order — earlier entries dispatch first — so any number of\noverlays compose with a single call, recompiled once.\n\n```ts\nconst allOverlays = defineOverlayRules([\n  GitHubCodeTableOverlayRules,\n  ExcelTableOverlayRules,\n  // ...plus inline rules if you want them at this priority position:\n  defineImportRule({match: sel.tag('foo'), $import: …}),\n]);\n\nctx.$importChildren(el, {rules: allOverlays});\n```\n\nThe same `DOMImportRuleEntry` union is accepted by\n`DOMImportConfig.rules` (the extension-level rule list), so a library\nthat ships a `CompiledOverlayRules` can be dropped straight into\n`configExtension(DOMImportExtension, {rules: […]})` without unpacking.\n\nThis is the definition-time complement to runtime composition (an\noverlay rule's `$import` calling\n`ctx.$importChildren(el, {rules: another})` to push a nested overlay\non top of the current stack): compose when you want a fixed merged\noverlay across an entire subtree, nest when the inner overlay should\nonly apply for a deeper region.\n\n### Walk-wide overlays installed by a preprocessor\n\nA preprocessor can install an overlay for the entire walk by writing to\nthe built-in `ImportOverlays` session slot. The runtime reads it once\nbefore walking and seeds the overlay stack from it, so rules don't have\nto bracket each subtree with `$importChildren({rules: …})`.\n\nThis is how you handle paste-source-conditional rule sets: detect the\nsource in preprocess, install only the relevant overlay, and pay\nnothing for other sources.\n\n```ts\nimport {\n  defineOverlayRules,\n  type DOMPreprocessFn,\n  ImportOverlays,\n} from '@lexical/html';\n\nconst WordPasteOverlay = defineOverlayRules([\n  WordOPRule,\n  WordListParagraphRule,\n  // …\n]);\n\nconst $installWordOverlay: DOMPreprocessFn = (dom, ctx, $next) => {\n  const meta = dom.querySelector('meta[name=\"Generator\"]');\n  if (meta && /Microsoft Word/i.test(meta.getAttribute('content') || '')) {\n    ctx.session.update(ImportOverlays, (prev) => [...prev, WordPasteOverlay]);\n  }\n  $next();\n};\n```\n\nThat is the pattern, not something you have to write for Word lists\nspecifically: `@lexical/list` ships this exact preprocess as\n`WordListImportExtension`. It is opt-in — `ListExtension` does not depend\non it, so an editor that never pastes from Word does not bundle it:\n\n```ts\nimport {WordListImportExtension} from '@lexical/list';\nimport {defineExtension} from 'lexical';\n\ndefineExtension({\n  dependencies: [WordListImportExtension],\n  name: 'my-editor',\n});\n```\n\nSee `packages/lexical-list/src/__tests__/unit/ListImportExtension.test.ts`\n(\"MS Word paste — preprocess-installed overlay\") for a worked unit\ntest, and [`dev-examples/dom-import`](https://github.com/facebook/lexical/tree/main/dev-examples/dom-import)\nfor a running editor that exercises the same overlay end-to-end (a\nreduced rich-text editor with lists, tables, code blocks, markdown\nshortcuts, and an \"Import HTML\" dialog so you can paste raw HTML\nwithout needing a real clipboard `text/html` slot). The dev-example\nalso ships a VS Code → Safari paste fixture and the matching\n`@lexical/code-core` preprocess that turns the flat sibling run of\nmonospace+pre divs into a single `CodeNode`.\n\n## ClipboardImportExtension\n\nThe clipboard's paste handler (`$insertDataTransferForRichText`)\ndelegates to `ClipboardImportExtension`. Its output owns the full\npaste-side iteration: it walks a per-MIME-type priority list, reads\nthe corresponding `DataTransfer` slot, and invokes that MIME type's\nhandler stack.\n\n`ClipboardImportConfig` mirrors `GetClipboardDataExtension`:\n\n```ts\nexport interface ClipboardImportConfig {\n  $importMimeType: ImportMimeTypeConfig;\n  priority: ImportMimeTypePriority;\n}\n```\n\n- `$importMimeType` — per-MIME-type stack of middleware functions\n  `(data, selection, $next, dataTransfer) => boolean`. Top of stack runs\n  first; `$next()` defers to the next-lower; return `true` to claim the\n  data. The `dataTransfer` is the original `DataTransfer` the paste/drop\n  came from, so a handler can peek at companion MIME types or attached\n  files (see [`ImportSourceDataTransfer`](#importsourcedatatransfer) for\n  how to surface it to rules).\n- `priority` — a per-MIME-type weight map (`Record<string, number>`,\n  lower runs first). Composable: each extension contributes weights\n  for its own MIME types without coordinating with others.\n\n`$next(data?, selection?)` can replace the data, selection, or both for the\nremaining handlers in the same MIME-type stack. Omitted arguments retain\nthe values received by the current handler, so `$next()`\nis equivalent to `$next(data, selection)`. For example, `$next(cleanedData)`\nchanges only the data, and `$next(data, otherSelection)` changes only\nthe selection. An empty string is forwarded as-is; it does not automatically\nfall back to another MIME type. These replacements do not modify the original\n`DataTransfer` or the arguments used when trying another MIME type.\n\n```ts\nconfigExtension(ClipboardImportExtension, {\n  $importMimeType: {\n    'application/vnd.myapp+json': [\n      (data, selection) => {\n        const nodes = parseMyAppFormat(data);\n        $insertGeneratedNodes($getEditor(), nodes, selection);\n        return true;\n      },\n    ],\n  },\n  // Slot between 'application/x-lexical-editor' (0) and 'text/html' (10).\n  priority: {'application/vnd.myapp+json': 5},\n})\n```\n\n### Routing pastes through `DOMImportExtension`\n\nThe easy on-switch is `ClipboardDOMImportExtension` from\n`@lexical/clipboard` — add it to your dependencies and `text/html`\npastes and drops route through the new pipeline (forwarding\n`ImportSource='paste'` and `ImportSourceDataTransfer` automatically).\nThe node extensions already registered their rules, so nothing else\nneeds to be configured:\n\n```ts\nimport {defineExtension} from 'lexical';\nimport {ClipboardDOMImportExtension} from '@lexical/clipboard';\nimport {RichTextExtension} from '@lexical/rich-text';\n\ndefineExtension({\n  name: 'app',\n  dependencies: [\n    RichTextExtension,\n    // …other node extensions (list, link, table, code, …)…\n    ClipboardDOMImportExtension,\n  ],\n});\n```\n\nApps that don't configure `ClipboardImportExtension` keep the legacy\nbehavior — `$insertDataTransferForRichText` falls back to the same\ndefaults the legacy code path uses, including the legacy\n`$generateNodesFromDOM` for HTML.\n\n### Transforming pasted HTML\n\n:::tip\n\nUse HTML transforms when integrating libraries that accept and return HTML\nstrings. For DOM-based normalization, prefer a [DOM import preprocessor](#preprocessors).\nIt works directly on the parsed DOM, avoiding an extra serialization and parse.\nSee [`WordListImportExtension`](#walk-wide-overlays-installed-by-a-preprocessor)\nand [`$inlineStylesFromStyleSheets`](#inlinestylesfromstylesheets) for examples.\n\n:::\n\nUse extension dependencies to enforce handler ordering. A transform's\nconfiguration is applied after its dependencies, so its handler runs first.\n\n**To use `ClipboardDOMImportExtension`**, declare it as a dependency of your\ntransform extension:\n\n```ts\nimport {\n  ClipboardDOMImportExtension,\n  ClipboardImportExtension,\n} from '@lexical/clipboard';\nimport {configExtension, defineExtension} from 'lexical';\nimport {normalizePastedHTML} from './normalizePastedHTML';\n\nconst TransformPastedHTMLExtension = defineExtension({\n  name: 'app/TransformPastedHTML',\n  dependencies: [\n    ClipboardDOMImportExtension,\n    configExtension(ClipboardImportExtension, {\n      $importMimeType: {\n        'text/html': [\n          (html, _selection, $next) => $next(normalizePastedHTML(html)),\n        ],\n      },\n    }),\n  ],\n});\n```\n\nAdd `TransformPastedHTMLExtension` to your root dependencies. This includes\n`ClipboardDOMImportExtension` and guarantees that the transform runs before\nits HTML importer, passing the transformed HTML to it through `$next`.\n\n**To support either importer**, declare `ClipboardDOMImportExtension` as an\noptional peer dependency:\n\n```ts\nimport {\n  type ClipboardDOMImportExtension,\n  ClipboardImportExtension,\n} from '@lexical/clipboard';\nimport {configExtension, declarePeerDependency, defineExtension} from 'lexical';\nimport {normalizePastedHTML} from './normalizePastedHTML';\n\nconst TransformPastedHTMLForEitherImporterExtension = defineExtension({\n  name: 'app/TransformPastedHTMLForEitherImporter',\n  dependencies: [\n    configExtension(ClipboardImportExtension, {\n      $importMimeType: {\n        'text/html': [\n          (html, _selection, $next) => $next(normalizePastedHTML(html)),\n        ],\n      },\n    }),\n  ],\n  peerDependencies: [\n    declarePeerDependency<typeof ClipboardDOMImportExtension>(\n      '@lexical/clipboard/DOMImport',\n    ),\n  ],\n});\n```\n\nAdd `TransformPastedHTMLForEitherImporterExtension` to your root dependencies.\nWhen the editor includes `ClipboardDOMImportExtension`, the peer dependency\nmakes this extension's clipboard configuration apply after the DOM importer's\nconfiguration, so the transform runs first. Otherwise, the transform runs\nbefore the legacy default HTML importer.\n\n### `ImportSourceDataTransfer`\n\nA builtin `ImportStateConfig<DataTransfer | null>` slot for surfacing\nthe original paste/drop `DataTransfer` to import rules and\npreprocessors. `ClipboardDOMImportExtension` forwards it via\n`context`; rules can then read it during the walk:\n\n```ts\nimport {ImportSourceDataTransfer} from '@lexical/html';\n\nconst ExcelAwareTableRule = defineImportRule({\n  match: sel.tag('table'),\n  $import: (ctx, el, $next) => {\n    const transfer = ctx.get(ImportSourceDataTransfer);\n    // Excel ships an HTML alternative alongside a real RTF payload; if\n    // we see both, we may want to defer to the RTF handler instead of\n    // parsing Excel's HTML quirks.\n    if (transfer && transfer.types.includes('application/rtf')) {\n      return $next();\n    }\n    // …handle the table…\n  },\n});\n```\n\nDefaults to `null` outside of clipboard imports (e.g. headless\n`$generateNodesFromDOM` calls). Use sparingly — the safer pattern is\nto decide *which* MIME-type payload to walk in the clipboard handler\nstack and hand a finalized DOM to the rules; only fall back to peeking\nat `ImportSourceDataTransfer` when the source-detection signal\ngenuinely lives in a companion slot.\n\n## Migrating from `importDOM`\n\nThe legacy `static importDOM(): DOMConversionMap` declaration on each\nnode class still works; the new pipeline is opt-in. When you're\nready to move a custom node, the translation is mechanical:\n\n| Legacy concept | New equivalent |\n| --- | --- |\n| `static importDOM(): DOMConversionMap` returning `{tag: () => ({conversion, priority})}` | One or more `defineImportRule({match, $import})` entries |\n| Numeric `priority` (0–4) | Position in the compiled `rules` list (earlier entry runs first; an extension's rules come ahead of its dependencies') plus `$next()` for deferring — see [Dispatch order](#dispatch-order) |\n| `forChild(node, parent)` | `ctx.$importChildren(el, {context: [...], $onChild})` |\n| `after(children)` | `ctx.$importChildren(el, {$after})` |\n| `wrapContinuousInlines` (block ancestor case) | `ctx.$importChildren(el, {schema: BlockSchema})` |\n| `ArtificialNode__DO_NOT_USE` (nested block) | `ctx.$importChildren(el, {schema: NestedBlockSchema})` |\n| Cross-tag setup via shared state | `createImportState` + `ctx.get` (scoped) or `ctx.session.get/set` (flat) |\n| Mutating the DOM before walking | `DOMPreprocessFn` in `DOMImportConfig.preprocess` |\n| `html: {import: ...}` field on a node-providing extension | `configExtension(DOMImportExtension, {rules: […]})` |\n\nA migrated rule typically replaces a forChild chain with explicit\ncontext propagation (clearer + survives across rule boundaries), and\nan `after` callback with either `$after` (when the post-processing\nruns on this rule's children) or a schema's `$packageRun` / `$finalize`\n(when the same logic belongs to anything appending to this kind of\nparent).\n\n### Importing children is explicit\n\nThe most consequential difference: the legacy `DOMConversion` pipeline\nwalked an element's children for you (via `wrapContinuousInlines` /\nthe post-conversion runner). In `DOMImportExtension`, a rule that\nwants its element's children must call `ctx.$importChildren(el)`\nitself.\n\nThis is deliberate. A rule decides:\n\n- Whether to recurse at all (skip children to drop them, or look at\n  `el` only)\n- Which schema the children must satisfy\n  (`schema: BlockSchema | InlineSchema | NestedBlockSchema | …`)\n- Whether to install a subtree-scoped overlay\n  (`rules: defineOverlayRules([...])`)\n- Where the produced children attach — usually\n  `parent.splice(0, 0, ctx.$importChildren(el))`, but a rule that\n  wants flat output can just return the array.\n\n### Concrete example: blockquote\n\nA typical legacy `importDOM` for a quote-like node:\n\n```ts\n// Legacy\nclass QuoteNode extends ElementNode {\n  static importDOM(): DOMConversionMap | null {\n    return {\n      blockquote: () => ({\n        conversion: (_el) => ({node: $createQuoteNode()}),\n        priority: 0,\n      }),\n    };\n  }\n}\n```\n\nThe migrated rule:\n\n```ts\nimport {\n  BlockSchema,\n  defineImportRule,\n  InlineSchema,\n  sel,\n} from '@lexical/html';\nimport {$createQuoteNode} from '@lexical/rich-text';\n\nconst QuoteRule = defineImportRule({\n  $import: (ctx, el) => {\n    const node = $createQuoteNode();\n    // The recursion is explicit. QuoteNode contains inline children\n    // only, so we constrain the schema; rejected blocks are dropped\n    // (InlineSchema declares no onReject, and 'drop' is the default).\n    node.splice(0, 0, ctx.$importChildren(el, {schema: InlineSchema}));\n    return [node];\n  },\n  match: sel.tag('blockquote'),\n  name: '@app/quote',\n});\n\nconfigExtension(DOMImportExtension, {rules: [QuoteRule]});\n```\n\nThe legacy pipeline would have inferred the recursion + wrapping\nbehavior from `QuoteNode`'s position in the tree and its acceptance\nrules. In the new pipeline both are stated at the call site: the rule\nasks for inline children, gets them packaged by `InlineSchema`, and\nattaches them via the normal `ElementNode.splice` primitive.\n\n### Concrete example: playground\n\nThe lexical-playground application was migrated in\n[#8590](https://github.com/facebook/lexical/pull/8590), which should\nbe a good example of what it would take to move an entire application\nfrom `importDOM` to `DOMImportExtension` (while adding test coverage).\n\n## Capabilities\n\nCurrent:\n\n- Typed selectors with element-narrowing through to `$import`'s\n  signature; no instanceof or cast boilerplate.\n- Middleware `$next()` chain replaces numeric priority.\n- Per-rule capture map (`ctx.captures`) avoids re-running regex\n  inside the rule body.\n- `ChildSchema` enforces structural invariants (block/inline/list/\n  table/code) without per-importer wrapping logic.\n- Per-call context (`ImportSource`), persistent context state\n  (`ImportTextFormat`, `ImportWhitespaceConfig`, app-defined).\n- Mutable session store for document-order-shared info.\n- DOM preprocess middleware (default: stylesheet inlining).\n- Subtree-scoped `rules` overlay for cost-bearing predicates.\n- Clipboard pipeline owns the whole `DataTransfer` iteration via\n  `ClipboardImportExtension`; per-MIME-type priority weights compose\n  across extensions.\n\nFuture:\n\n- The default pipeline (`$generateNodesFromDOM` and the static\n  `importDOM` methods on lexical-core node classes) still works\n  side-by-side. There is no plan in this iteration to flip the\n  default — both will coexist while ecosystem migrates.\n","packages/lexical-website/docs/serialization/dom-render.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-website%2Fdocs%2Fserialization%2Fdom-render.md -->","packages/lexical-website/docs/serialization/legacy.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-website%2Fdocs%2Fserialization%2Flegacy.md -->","packages/lexical-website/docs/serialization/markdown-mdast.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-website%2Fdocs%2Fserialization%2Fmarkdown-mdast.md -->","packages/lexical-website/docs/serialization/serialization.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-website%2Fdocs%2Fserialization%2Fserialization.md -->","packages/lexical-website/docs/testing.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-website%2Fdocs%2Ftesting.md -->","packages/lexical-website/plugins/example-source/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-website%2Fplugins%2Fexample-source%2FREADME.md -->","packages/lexical-website/src/data/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-website%2Fsrc%2Fdata%2FREADME.md -->","packages/lexical-yjs/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-yjs%2FREADME.md -->","packages/lexical/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical%2FREADME.md -->","packages/lexical/src/__bench__/README.md":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical%2Fsrc%2F__bench__%2FREADME.md -->"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/AGENTS.md","title":"AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# Lexical Agent Guide\n\nThis file provides detailed guidance for AI agents and automated tools working with the Lexical codebase.\n\n## Build, Test, and Development Commands\n\n### Building\n- `pnpm run build` - Build all packages in development mode\n- `pnpm run build-prod` - Clean and build all packages in production mode\n- `pnpm run build-release` - Build production release with error codes\n- `pnpm run build-types` - Build TypeScript type definitions and validate them\n\n### Testing\n- `pnpm run test-unit` - Run all unit tests (Vitest, Node/jsdom)\n- `pnpm run test-unit-watch` - Run unit tests in watch mode\n- `pnpm run test-browser` - Run browser-mode unit tests (Vitest + Playwright, real browser)\n- `pnpm run test-browser-watch` - Run browser-mode tests in watch mode\n- `pnpm run test-e2e-chromium` - Run E2E tests in Chromium (requires dev server running)\n- `pnpm run test-e2e-firefox` - Run E2E tests in Firefox\n- `pnpm run test-e2e-webkit` - Run E2E tests in WebKit\n- `pnpm run debug-test-e2e-chromium` - Run E2E tests in debug mode (headed)\n- `pnpm run debug-test-unit` - Debug unit tests with inspector\n\nFor E2E testing workflow:\n1. Start the dev server: `pnpm run start` (or `pnpm run dev` if you don't need collab)\n2. In another terminal: `pnpm run test-e2e-chromium`\n\n### Development Servers\n- `pnpm run start` - Start playground dev server + collab server (http://localhost:3000)\n- `pnpm run dev` - Start only the playground dev server (no collab)\n- `pnpm run start:website` - Start Docusaurus website (http://localhost:3001)\n- `pnpm run collab` - Start collab server on localhost:1234\n\n### Code Quality\n- `pnpm run lint` - Run ESLint on all files\n- `pnpm run lint:fix` - Auto-fix lint issues\n- `pnpm run prettier` - Check code formatting\n- `pnpm run prettier:fix` - Auto-fix formatting issues\n- `pnpm run flow` - Run a full foreground Flow check (does not reuse a server)\n- `pnpm run tsc` - Run TypeScript compiler\n- `pnpm run ci-check` - Run all checks (TypeScript, Flow, Prettier, ESLint)\n\n**Never commit changes to `scripts/error-codes/codes.json`.**\nThat edit is not yours to make — revert it to the state of\n`main` before staging, and never `git add` the file.\n\n### Searching and refactoring\n\nPrefer **ast-grep** over line-oriented regex (`grep`/`sed`) for anything\nstructural — matching or rewriting imports, call sites, JSX, type\nannotations, etc. Regexes miss multi-line forms (a symbol on its own line\ninside a multi-line `import { ... }` block) and produce false positives\n(`IS_APPLE` matching inside `IS_APPLE_WEBKIT`); ast-grep matches the syntax\ntree, so it does neither.\n\nIt isn't a dependency, so run it via npx (the CLI binary is `ast-grep`, not\nthe shadow-utils `sg` that may be on `PATH`):\n\n```sh\n# Find every import of something from '@lexical/utils' (ts and tsx are\n# separate grammars, so pass -l for each; add --json=compact to post-process)\nnpx --package @ast-grep/cli ast-grep run \\\n  -p \"import { \\$\\$\\$NAMES } from '@lexical/utils'\" -l ts packages\n```\n\nMetavariables (`$NAME`, `$$$LIST`) capture nodes for reporting or rewriting\nwith `--rewrite`. Reach for it whenever a change spans many files and must be\nprecise — e.g. moving symbols that `@lexical/utils` merely re-exports back to\na direct `lexical` import.\n\n### Extension source imports\n\nImport from the `@lexical/extension` barrel across package boundaries. Within\n`packages/lexical-extension`, use relative imports and re-exports. Do not\nmanually rewrite these source imports to `@lexical/extension/*`: ESLint\nenforces the source convention, and `@lexical/compiler`'s `subpathImports`\npass rewrites them in all published package builds, including www. Public\nsibling entries stay external so shared modules such as signals retain one\ninstance. Helpers with only one owning entry live under `src/internal` and\nare bundled into that entry, without exposing additional public subpaths.\nDownstream users may import either the barrel or public subpaths.\n\n### Tree-shaking annotations\n\nModule-scope calls to the side-effect-free factories (`defineExtension`,\n`configExtension`, `safeCast`, `createCommand`, `createState`,\n`defineImportRule`, etc.) need a `/* @__PURE__ */` annotation so bundlers\ncan drop unused definitions from application bundles. **Do not write those\nannotations by hand** — they are injected at build time by\n`@lexical/compiler` (`packages/lexical-compiler`), which\nruns as a Rollup plugin in `scripts/build.mjs` for the published bundles\nand as a Vite plugin in `scripts/vite/lexicalMonorepoPlugin.ts` for\neverything the monorepo builds from source. The published package is also\nwhat consumers add to their own build when they compile Lexical from its\n`source` export condition rather than from `dist`.\n\nA hand-written annotation on one of these calls is a lint error\n(`@lexical/internal/no-pure-annotation`), and the rule is autofixable, so a\nbranch written before the transform existed migrates with\n`pnpm run lint:fix`. Annotations on anything else — a third-party factory, or\na call inside a function body, where the build never injects one — are left\nalone.\n\nWhen adding a new factory of this kind, annotate its definition with\n`@__NO_SIDE_EFFECTS__`. That alone is enough for calls in the same package to\nbe annotated; add its name to `PURE_FACTORY_FUNCTIONS` in\n`packages/lexical-compiler/src/LexicalCompiler.mjs` so that\ncalls in code that imports it by package name are too. An object whose\nmethods build values and touch nothing else (like `@lexical/html`'s `sel`) is\nmarked `@lexical-pure-namespace` instead, so that `sel.tag('p')` is annotated\nthe way a factory call is.\n\nThe build runs with `strict`, which fails on a call inside one of these\ndefinitions that nothing has established is side-effect free — such a call\npins the definition into every bundle that imports the module, however well\nannotated the definition itself is. If it stops you, either make the call\nlazy (a `nodes: () => [...]` callback is not evaluated at module scope), or\ndeclare the function side-effect free so its calls are annotated too. A\nthird-party factory that cannot be declared is annotated by hand at the call\nsite, which is what the remaining hand-written annotations in the tree are.\n\nFactory calls are not the only module-scope statements that pin a module.\nesbuild and webpack keep, as a side effect, any property read at module scope\n(`navigator.userAgent`, `Date.now`, `SomeExtension.name`), any call or `new`\nthey were not told is pure (`new RegExp(...)`, `Object.freeze(...)`,\n`[...].join(',')`), an array or object spread, an `in` test, a `try`\nstatement, a mutation (`table.push(...)`, `Klass.static = ...`), a class with\na computed member (`[SOME_SYMBOL]?: number` — write `declare` on it), and an\nIIFE — together with everything the statement references. One\n`LexicalEditor.version = ...` after the class kept nearly all of `lexical` in\na bundle that imported only `createCommand` (#9120). Move such work into a\nfunction declared `@__NO_SIDE_EFFECTS__` and call it at module scope, so the\nbuild annotates the call; annotate a call to a builtin or to a third-party\nfunction (`/* @__PURE__ */ forwardRef(...)`) by hand. Two tests enforce this\nby bundling a bare `import 'pkg'` of every entry with esbuild and requiring\nthat nothing is retained: `scripts/__tests__/unit/treeShakingSource.test.ts`\non the sources (runs with `pnpm run test-unit`) and\n`scripts/__tests__/integration/tree-shaking.test.mjs` on the published\nbuilds. A failure prints the retained code; its first statement is usually\nthe culprit.\n\nA factory whose body is a trivial expression over its own arguments (like\n`safeCast`, `defineExtension`, or `configExtension`) is additionally marked\n`@lexical-inline <form>` and listed in that file's `INLINE_FACTORIES`: the\nbuild replaces calls to it with the expression it would have returned, which\nneeds no annotation at all. **The body of a marked function is reproduced by\nthe build**, so keep it trivial — `packages/lexical-compiler`'s unit\ntests compare what the transform emits against what the real function returns\nand will fail if the two drift apart.\n\n## High-Level Architecture\n\n### Core Concepts\n\nLexical is built around several key architectural concepts that work together:\n\n**Editor Instance** - Created via `createEditor()`, wires everything together. Manages the EditorState, registers listeners/commands/transforms, and handles DOM reconciliation.\n\n**EditorState** - Immutable data model representing the editor content. Contains:\n- A node tree (hierarchical structure of LexicalNodes)\n- A selection object (current cursor/selection state)\n- Fully serializable to/from JSON\n\n**`$` Functions Convention** - Functions prefixed with `$` (e.g., `$getRoot()`, `$getSelection()`) can ONLY be called within:\n- `editor.update(() => {...})` - for mutations\n- `editor.read(() => {...})` - for read-only access\n- Node transforms and command handlers (which have implicit update context)\n\nThis is similar to React hooks' restrictions but enforces synchronous context instead of call order.\n\n**Double-Buffering Updates** - When `editor.update()` is called:\n1. Current EditorState is cloned as work-in-progress\n2. Mutations modify the work-in-progress state\n3. Multiple synchronous updates are batched\n4. DOM reconciler diffs and applies changes\n5. New immutable EditorState becomes current\n\n**Node Immutability & Keys** - All nodes are recursively frozen after reconciliation. Node methods automatically call `node.getWritable()` to create mutable clones. All versions of a logical node share the same runtime-only key, allowing node methods to always reference the latest version from the active EditorState.\n\n### Monorepo Structure\n\nThis is a monorepo with packages in `packages/`:\n\n**Core Packages:**\n- `lexical` - Core framework (Editor, EditorState, base nodes, selection, updates)\n- `@lexical/react` - React bindings (LexicalComposer, plugins as components)\n- `@lexical/headless` - Headless editor for server-side/testing\n\n**Feature Packages** (extend core with nodes/commands/utilities):\n- `@lexical/rich-text` - Rich text editing (headings, quotes, etc.)\n- `@lexical/plain-text` - Plain text editing\n- `@lexical/extension` - Extend editor functionality\n- `@lexical/list` - List nodes (ordered/unordered/checklist)\n- `@lexical/table` - Table support\n- `@lexical/code` - Code block with syntax highlighting\n- `@lexical/link` - Link nodes and utilities\n- `@lexical/markdown` - Markdown import/export\n- `@lexical/html` - HTML serialization\n- `@lexical/history` - Undo/redo\n- `@lexical/yjs` - Real-time collaboration via Yjs\n- And many more...\n\n**Development Packages:**\n- `lexical-playground` - Full-featured demo application\n- `lexical-website` - Docusaurus documentation site\n\n### Key Architectural Patterns\n\n**Extensions** - Extensions should be used to add features and configuration\nto an editor. The set of extensions in an editor must be determined when the\neditor is created with `buildEditorFromExtensions`. Extensions with\nfunctionality that can be toggled on or off typically have a `disabled`\nconfiguration property and output signal that defaults to `false`. See the\nlexical-extension package and the supporting code in lexical for\nmore examples and implementation details.\n\n```tsx\nexport interface MyConfig {\n  disabled: boolean;\n}\nexport const MyExtension = defineExtension({\n  build: (_editor, config, _state) => namedSignals(config),\n  config: safeCast<MyConfig>({ disabled: false }),\n  name: '@lexical/docs/My',\n  nodes: () => [MyNode],\n  register: (editor, _config, state) => {\n    const {disabled} = state.getOutput();\n    return effect(() => {\n      if (!disabled.value) {\n        return editor.registerUpdateListener(({editorState}) => {\n          // React to updates\n        });\n      }\n    })\n  },\n})\n```\n\n**Plugin System (React)** - Plugins are a legacy pattern for React components\nto hook into the editor lifecycle, extensions should be preferred for new code:\n```jsx\nfunction MyPlugin() {\n  const [editor] = useLexicalComposerContext();\n  useEffect(() => {\n    return editor.registerUpdateListener(({editorState}) => {\n      // React to updates\n    });\n  }, [editor]);\n  return null;\n}\n```\n\n**Command Pattern** - Commands are the primary communication mechanism:\n- Create with `createCommand()`\n- Dispatch with `editor.dispatchCommand(command, payload)`\n- Handle with `editor.registerCommand(command, handler, priority)`\n- Handlers propagate by priority until one stops propagation\n\n**Node Transforms** - Registered via `editor.registerNodeTransform(NodeClass, transform)`. Called automatically during updates when nodes of that type change. Have implicit update context.\n\n**Listeners** - All `editor.register*()` methods return cleanup functions for easy unsubscription.\n\n## Type System\n\nThis codebase uses **both TypeScript and Flow**:\n- Source files are primarily TypeScript (`.ts`, `.tsx`)\n- Flow type definitions are generated in `packages/*/flow/` directories\n- Run `pnpm run flow` to check Flow types\n- Run `pnpm run tsc` to check TypeScript types\n- Both are checked in CI via `pnpm run ci-check`\n\nWhen adding/modifying APIs, types must be maintained for both systems.\n\n## Backwards Compatibility\n\n**All changes MUST be backwards compatible.** Lexical is a widely-adopted OSS library, and breaking changes ripple out to every downstream consumer.\n\n- Do NOT remove or rename existing public APIs, exported functions, types, or `$` functions. Add new APIs alongside the old ones instead.\n- Do NOT change the signature, return type, or behavior of existing public APIs in ways that could break callers. Prefer additive, optional parameters.\n- Preserve the serialization format of `EditorState` and node JSON. Serialized content produced by older versions must continue to deserialize correctly.\n- If an API genuinely must change, deprecate the old one first (keep it working, document the replacement) rather than removing it outright.\n- When in doubt, assume external code depends on the current behavior and keep it intact.\n\n## Dependencies: Lexical is a singleton within one app\n\nAn application must resolve exactly one copy of `lexical`, and one copy of each\n`@lexical/*` package it uses. The active editor and editor state are\nmodule-scope variables in `LexicalUpdates.ts`, node registration and\n`instanceof LexicalNode` compare class references, and commands are object\nidentities from `createCommand()` — none of which survive a second copy of the\nmodule. Two copies also mean two versions, which is API drift inside one editor.\n\nThe boundary is the app, not the page: several self-contained apps, each with\nits own bundled Lexical, can coexist on one page (`isLexicalEditor` is an\n`instanceof` check precisely to support that). They just cannot interoperate —\nnodes, editors, `EditorState`s, selections and commands must never be passed\nacross such a boundary, only serialized data.\n\n- Inside this monorepo, packages depend on siblings with `\"lexical\":\n  \"workspace:*\"`; `pnpm publish` rewrites that to the exact published version,\n  so every `@lexical/*` release pins the matching `lexical`. Keep that pattern\n  when adding a package — do not hand-write a version range.\n- A **library** published on top of Lexical (anything outside this repo that\n  imports `lexical` or `@lexical/*`) declares those packages in\n  `peerDependencies`, plus `devDependencies` for its own build and tests, never\n  in `dependencies`. Apps bring their own Lexical and own the version.\n- Code under `examples/` and `packages/lexical-playground` are applications, so\n  they depend on Lexical directly — that is correct and should stay.\n\nThe user-facing version of this rule lives in\n`packages/lexical-website/docs/concepts/one-lexical-per-app.md`; keep the two in\nsync when either changes.\n\n## Important Development Notes\n\n### Reconciliation and Updates\n- `editor.read(...)`  or `editor.read('force-commit', ...)` flushes pending updates first, then provides consistent reconciled state\n- `editor.read('pending', ...)` reads the pending state (like `editor.update(...)`, but read-only)\n- `editor.read('latest', ...)` reads the latest consistent reconciled state\n- Inside `editor.update()`, you see pending state (transforms/reconciliation not yet run)\n- `editor.getEditorState().read()` always uses latest reconciled state, but prefer `editor.read('latest', ...)` in new code\n- Updates can be nested: `editor.update(() => editor.update(...))` is allowed but strongly discouraged\n- Do NOT nest updates in reads, or use a force-commit in an update\n\n### Node References\nAlways access node properties/methods within read/update context. Nodes automatically resolve to their latest version via their key. Don't store node references across update boundaries.\n\n### Testing Strategy\n- **Unit tests** - Vitest, located in `packages/**/__tests__/unit/**/*.test.{ts,tsx}`.\n  Add `// @vitest-environment node` after the copyright header for tests that\n  only exercise editor state, headless behavior, or pure utilities. DOM tests\n  use jsdom with `vmThreads` on Node 24.9+, which reuses the jsdom implementation\n  while keeping each file's window and module graph isolated. Older Node\n  versions use isolated forks because their VM contexts cannot load jsdom's\n  `require(esm)` dependencies. Platform-mocked tests depend on module isolation.\n  Compiler tests run with `scripts-unit` in\n  ordinary Node workers because native build tools are not VM-realm-safe.\n  Use `DOMParser` to parse HTML in DOM tests and iframes for separate-window\n  tests. Reserve direct JSDOM construction for Node fixtures or CSSOM behavior\n  that jsdom's DOMParser lacks.\n- **Browser tests** - Vitest browser mode driven by the Playwright runner, located in\n  `packages/**/__tests__/browser/**/*.test.{ts,tsx}`. Use these for behavior that depends on\n  a real layout/selection engine instead of stubbing the missing jsdom functionality from\n  `vitest.setup.mts` (e.g. `Range.getBoundingClientRect`, the Selection API). Run with\n  `pnpm run test-browser`; the browser set is controlled by the `VITEST_BROWSER` env var\n  (comma-separated, default `chromium`). Prefer building editors with the extension APIs\n  (`buildEditorFromExtensions`, or `LexicalExtensionComposer`/`LexicalExtensionEditorComposer`\n  in React).\n  - **Do not use `using`/`Disposable` in browser tests (or any browser-facing code).**\n    Explicit Resource Management (`using`, `Symbol.dispose`, `Disposable`) is not supported\n    in WebKit/Safari yet, so the syntax throws a `SyntaxError` there. `using` is fine in unit\n    tests (jsdom/Node), but browser tests should clean up with\n    `onTestFinished(() => editor.dispose())` instead — `editor.dispose()` is a plain method\n    available on the result of `buildEditorFromExtensions`.\n- **E2E tests** - Playwright, located in `packages/lexical-playground/__tests__/e2e/**/*.spec.{ts,mjs}`\n- E2E tests require the playground dev server running\n- Use `pnpm run debug-test-e2e-chromium` to debug E2E tests with browser UI\n\n### Custom Nodes\nWhen creating custom nodes:\n1. Extend a base node class (TextNode, ElementNode, DecoratorNode). The\n   constructor must work with no arguments.\n2. Implement `$config()` with `this.config('type', {extends: BaseNode, json:\n   schema})`, where `schema` is built with `nodeSchema<YourNode>()({...})`\n   as a module-scope `const` above the class. Written inline in `$config()`,\n   it makes TypeScript skip checking the class without a diagnostic.\n   Declare every property inherent to the node in it, and declare each one\n   stored verbatim in a field with\n   `withField(stringValue(), {field: '__name'})`. Initialize that field in\n   the class (`__name: string = '';`) so it is an own property of a fresh\n   node. Use `withAccessors` only where a property is not a plain field.\n3. Do not hand-write `clone`, `importJSON`, `exportJSON` or\n   `updateFromJSON`: `$config` derives them from the schema, and it carries\n   every property declared with `withField` across a clone. A field missing\n   from the schema is not carried, so it is lost the first time a later\n   update clones the node; declare it with `withField` rather than writing\n   `afterCloneFrom`. Ad-hoc data that is not inherent to the node type\n   belongs in NodeState (`createState`), which clones on its own. Write\n   `afterCloneFrom` only for storage neither covers, such as a property\n   declared through accessors on both sides whose value is not one field\n   (one that is can name it: `setter: {field: '__ids', method: 'setIDs'}`\n   in `MarkNode`). Call `super.afterCloneFrom(prevNode)` first, and copy\n   every field of the class, schema fields included: Lexical does not add\n   to a class's own `afterCloneFrom`.\n4. Implement `createDOM()` and `updateDOM()` (and `decorate()` for a\n   DecoratorNode). Keep `exportDOM()` on the class when the default export\n   is not enough.\n5. For HTML import, prefer `defineImportRule` rules registered by the\n   extension that provides the node: list `CoreImportExtension` and\n   `configExtension(DOMImportExtension, {rules: [...]})` in its\n   `dependencies` (see `ListExtension`, and `ListImportRules` for the rules,\n   in `@lexical/list`). Both APIs are `@experimental`, and the rules only\n   apply where the editor routes HTML through the pipeline; see\n   `packages/lexical-website/docs/serialization/dom-import.md`.\n   `importDOM()`, which that page calls legacy, is not deprecated yet, but\n   its deprecation is planned.\n6. Register with an extension (`nodes: () => [YourNode]`) or editor config.\n7. Export a `$createYourNode()` factory built on `$create(YourNode)` and the\n   node's setters, and a `$isYourNode()` guard.\n\nThe full schema reference is\n`packages/lexical-website/docs/serialization/serialization.md`; the cloning\nrules are in `packages/lexical-website/docs/concepts/node-cloning.md`.\n\n### Shadow DOM and iframe realm safety\n\nLexical supports editors whose root element lives inside a Shadow DOM or an\n`<iframe>` document. The editor resolves its `window` and `document` from\n`rootElement.ownerDocument.defaultView`, so code that reaches for the **global**\n`window` or `document` will silently use the wrong realm when the editor crosses\na frame boundary. Shadow DOM adds a second hazard: the browser **retargets**\nselection and focus reads to the shadow host, hiding the real nodes.\n\nUse the shadow/iframe-aware helpers exported from `lexical` instead of the raw\nbrowser globals:\n\n| Instead of | Use | Why |\n| --- | --- | --- |\n| `window` | `element.ownerDocument.defaultView` or `getDefaultView(element)` (@internal) | Returns the window that owns the element |\n| `window.getSelection()` | `getDOMSelection(rootElement.ownerDocument.defaultView)` | Reads selection from the correct window |\n| `selection.getRangeAt(0)` | `getDOMSelectionRange(selection, rootElement)` | Unwraps retargeted shadow-DOM selection |\n| `document` in `createDOM`/`updateDOM`/`exportDOM` | `$getDocument()` | Returns the document that owns the editor root (falls back to `globalThis.document`) |\n| `document` elsewhere | `getRootOwnerDocument(rootElement)` or `element.ownerDocument` | Returns the document that owns a specific element |\n| `document.activeElement` | `getActiveElement(element)` / `getActiveElementDeep(document)` | Walks through shadow roots to find the real focused element |\n| `event.target` | `getComposedEventTarget(event)` | Returns the un-retargeted target for composed events |\n| `element.parentElement` | `getParentElement(element)` | Crosses shadow boundaries correctly |\n| `selection.anchorNode` / `focusNode` | `getDOMSelectionPoints(selection, rootElement)` | Returns un-retargeted boundary points |\n\nTwo ESLint rules enforce the globals rows at lint time:\n- `no-restricted-syntax` (error) catches all `document.*` and `window.*` member access in library sources.\n- `@lexical/no-document-in-dom-methods` (error, with autofix) catches `document.*` specifically inside `createDOM`/`updateDOM`/`exportDOM` methods and autofixes to `$getDocument().*`.\n\nThe remaining rows involve local variable properties that lint cannot reliably detect, so they must be caught in code review.\n\nFor full details on the browser platform APIs involved, see\n[Shadow DOM and iframes](packages/lexical-website/docs/concepts/shadow-dom.md).\n\n### Commits and Pull Requests\nEvery commit message — not just PR bodies — must be written to the shape of\n`.github/pull_request_template.md`, so any commit can seed a PR directly\nwithout being rewritten. This applies to every commit you author, including\none-line fixes; do not wait to be asked. Read the template rather than working\nfrom memory, and fill in its sections:\n\n- **Subject line**: `[Affected Packages] PR Type: title`, where the packages are\n  the directory names under `packages/` that the diff touches and the type is\n  one of Breaking change / Refactor / Feature / Bug Fix / Documentation Update /\n  Chore. Test-only and tooling changes are `Chore`.\n- **`## Description`**: what the current behavior is and what this change makes\n  it do. Add `Closes #<issue>` only when there is a real issue number; drop the\n  line otherwise rather than leaving the template's placeholder behind.\n- **`## Test plan`** with `### Before` and `### After` subsections: the command\n  you ran, plus the actual failing output before and passing output after.\n  Paste real output — do not describe it. If a platform or browser in the\n  matrix could not be exercised, say so explicitly under `### After`.\n\nDrop any template section that does not apply to the diff instead of carrying\nan empty heading, and treat the template's HTML comments as instructions to\nfollow, not text to copy into the message.\n\n### Commit and PR Hygiene for Agents\nThis is an open source project: never include agent-session URLs or other\nprivate/team-internal links (e.g. `https://claude.ai/code/session_...`) in\ncommit messages, PR titles, or PR bodies. Those URLs are private to the\nperson or team that ran the session and are meaningless or misleading to\neveryone else. Co-authorship attribution (e.g. `Co-Authored-By:`) is fine.\nFor Claude Code this is enforced mechanically via `attribution.sessionUrl:\nfalse` in the checked-in `.claude/settings.json`; agents from other vendors\nshould follow this rule as written.\n\n### Build System\n- Uses Rollup for bundling\n- Build script: `scripts/build.mjs`\n- Supports multiple build modes: development, production, www (Meta internal)\n- TypeScript source → compiled to ESM for npm (`dist/*.js` in the\n  `\"type\": \"module\"` packages; CommonJS consumers load it through\n  `require(esm)`, so the fork modules must stay free of top-level await) and\n  to CommonJS for www only (`--www`)\n- Package manager logic in `scripts/shared/packagesManager.mjs`\n","isInternal":false,"tokens":6294,"sizeBytes":26716},{"name":"CLAUDE.md","path":"CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/CLAUDE.md","title":"Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"@AGENTS.md\n","isInternal":false,"tokens":5,"sizeBytes":11},{"name":"_category_.json","path":"packages/lexical-website/docs/collaboration/_category_.json","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-website/docs/collaboration/_category_.json","title":"Collaboration Skill","category":"anthropic-skill","format":"json","content":"{\n  \"label\": \"Collaboration\",\n  \"position\": 6\n}\n","frontmatter":{"label":"Collaboration","position":6},"isInternal":false,"tokens":18,"sizeBytes":48},{"name":"faq.md","path":"packages/lexical-website/docs/collaboration/faq.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-website/docs/collaboration/faq.md","title":"Collaboration Skill","category":"anthropic-skill","format":"markdown","content":"---\nsidebar_position: 2\n---\n\n# Collaboration FAQ\n\n## Source of truth: Lexical State, Yjs and App's DB\n\nIt's recommended to treat the Yjs model as the source of truth. You can store the document to a database for indexing.\nBut, if possible, you should never forget the Yjs model, as this is the only way clients without internet access can reliably join and sync with the server.\n\nYou can also treat the database as the source of truth. This is how it could be achieved:\n\n- Clients receive a `sessionId` when they connect to the server\n- When a client connects without an existing `sessionId`, get the content from the database and create a `sessionId`\n- When all clients disconnect, forget the room content and `sessionId` on the server after some timeout (e.g. 1 hour)\n- When a client reconnects, use that content on the server. Furthermore, get the `sessionId` from the client\n- When two clients with different `sessionId` reconnect, one of the clients should forget the room content. _In this case the client will lose content_ - although it is very unlikely if you set the forget timeout (see point 2) very high.\n\nOr, there is an ever simpler approach:\n\n- When a client connects to the server, the server populates the room content if empty\n- When all clients disconnect, the server forgets the room content after some timeout (e.g. 1 hour)\n- When a client was not able to reconnect for 40 minutes, the client must forget its local updates and start fresh (this should be enforced by the server)\n\nWhen the database is the source of truth, and if you want to be able to forget the Yjs model, you will always run into cases where clients are not able to commit changes. That's not too bad in most projects. It somehow limits you, because you can't cache the document on the client using y-indexeddb. On the other hand, it is much easier to maintain, and do Yjs upgrades. Furthermore, most people would say that SQL is a bit more reliable than Yjs.\n\n_* Based on the advice of the Yjs author - [Kevin Jahns](https://github.com/yjs/yjs/issues/82#issuecomment-328365015)_\n\n\n## Initializing `EditorState` from Yjs Document\n\nIt's achievable by leveraging headless Lexical and no-op provider for Yjs:\n\n<details>\n  <summary>createHeadlessCollaborativeEditor.ts</summary>\n\n  ```typescript\n  import type {Binding, Provider} from '@lexical/yjs';\n  import type {\n    Klass,\n    LexicalEditor,\n    LexicalNode,\n    LexicalNodeReplacement,\n    SerializedEditorState,\n  } from 'lexical';\n\n  import {createHeadlessEditor} from '@lexical/headless';\n  import {\n    createBinding,\n    syncLexicalUpdateToYjs,\n    syncYjsChangesToLexical,\n  } from '@lexical/yjs';\n  import {type YEvent, applyUpdate, Doc, Transaction} from 'yjs';\n\n  export default function headlessConvertYDocStateToLexicalJSON(\n    nodes: ReadonlyArray<Klass<LexicalNode> | LexicalNodeReplacement>,\n    yDocState: Uint8Array,\n  ): SerializedEditorState {\n    return withHeadlessCollaborationEditor(nodes, (editor, binding) => {\n      applyUpdate(binding.doc, yDocState, {isUpdateRemote: true});\n      editor.update(() => {}, {discrete: true});\n\n      return editor.getEditorState().toJSON();\n    });\n  }\n\n  /**\n   * Creates headless collaboration editor with no-op provider (since it won't\n   * connect to message distribution infra) and binding. It also sets up\n   * bi-directional synchronization between yDoc and editor\n   */\n  export function withHeadlessCollaborationEditor<T>(\n    nodes: ReadonlyArray<Klass<LexicalNode> | LexicalNodeReplacement>,\n    callback: (editor: LexicalEditor, binding: Binding, provider: Provider) => T,\n  ): T {\n    const editor = createHeadlessEditor({\n      nodes,\n    });\n\n    const id = 'main';\n    const doc = new Doc();\n    const docMap = new Map([[id, doc]]);\n    const provider = createNoOpProvider();\n    const binding = createBinding(editor, provider, id, doc, docMap);\n\n    const unsubscribe = registerCollaborationListeners(editor, provider, binding);\n\n    const res = callback(editor, binding, provider);\n\n    unsubscribe();\n\n    return res;\n  }\n\n  function registerCollaborationListeners(\n    editor: LexicalEditor,\n    provider: Provider,\n    binding: Binding,\n  ): () => void {\n    const unsubscribeUpdateListener = editor.registerUpdateListener(\n      ({\n        dirtyElements,\n        dirtyLeaves,\n        editorState,\n        normalizedNodes,\n        prevEditorState,\n        tags,\n      }) => {\n        if (tags.has('skip-collab') === false) {\n          syncLexicalUpdateToYjs(\n            binding,\n            provider,\n            prevEditorState,\n            editorState,\n            dirtyElements,\n            dirtyLeaves,\n            normalizedNodes,\n            tags,\n          );\n        }\n      },\n    );\n\n    const observer = (events: Array<YEvent<any>>, transaction: Transaction) => {\n      if (transaction.origin !== binding) {\n        syncYjsChangesToLexical(binding, provider, events, false);\n      }\n    };\n\n    binding.root.getSharedType().observeDeep(observer);\n\n    return () => {\n      unsubscribeUpdateListener();\n      binding.root.getSharedType().unobserveDeep(observer);\n    };\n  }\n\n  function createNoOpProvider(): Provider {\n    const emptyFunction = () => {};\n\n    return {\n      awareness: {\n        getLocalState: () => null,\n        getStates: () => new Map(),\n        off: emptyFunction,\n        on: emptyFunction,\n        setLocalState: emptyFunction,\n        setLocalStateField: emptyFunction,\n      },\n      connect: emptyFunction,\n      disconnect: emptyFunction,\n      off: emptyFunction,\n      on: emptyFunction,\n    };\n  }\n  ```\n</details>\n","frontmatter":{"sidebar_position":2},"isInternal":false,"tokens":1279,"sizeBytes":5579},{"name":"react.md","path":"packages/lexical-website/docs/collaboration/react.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-website/docs/collaboration/react.md","title":"Collaboration Skill","category":"anthropic-skill","format":"markdown","content":"---\nsidebar_position: 1\n---\n\n# React\n\nLexical provides `LexicalCollaborationPlugin` and `useCollaborationContext` hook within `@lexical/react` to accelerate creation of the collaborative React backed editors.\nThis is on top of the Yjs bindings provided by `@lexical/yjs`.\n\n\n:::tip\n\nClone [Lexical GitHub repo](https://github.com/facebook/lexical), run `pnpm i && pnpm run start` and open [`http://localhost:3000/split/?isCollab=true`](http://localhost:3000/split/?isCollab=true) to launch playground in collaborative mode.\n\n:::\n\n## Getting started\n\nThis guide is based on [examples/react-rich](https://github.com/facebook/lexical/tree/main/examples/react-rich) example.\n\n**Install minimal set of the required dependencies:**\n```bash\n$ npm i -S @lexical/react @lexical/yjs lexical react react-dom y-websocket yjs\n```\n\n:::note\n\n`y-websocket` is the only officially supported Yjs connection provider at this point. Although other providers may work just fine.\n\n:::\n\n**Get WebSocket server running:**\n\nThis allows different browser windows and different browsers to find each other and sync Lexical state. On top of this `YPERSISTENCE` allows you to save Yjs documents in between server restarts so clients can simply reconnect and keep editing.\n\n```bash\n$ HOST=localhost PORT=1234 YPERSISTENCE=./yjs-wss-db npx y-websocket\n```\n\n**Get basic collaborative Lexical setup:**\n\n```tsx\nimport { useCallback } from 'react';\n\nimport {$getRoot, $createParagraphNode, $createTextNode} from 'lexical';\nimport {LexicalCollaboration} from '@lexical/react/LexicalCollaborationContext';\nimport {LexicalComposer} from '@lexical/react/LexicalComposer';\nimport {ContentEditable} from '@lexical/react/LexicalContentEditable';\nimport {LexicalErrorBoundary} from '@lexical/react/LexicalErrorBoundary';\nimport {RichTextPlugin} from '@lexical/react/LexicalRichTextPlugin';\nimport {CollaborationPlugin} from '@lexical/react/LexicalCollaborationPlugin';\nimport * as Y from 'yjs';\nimport {$initialEditorState} from './initialEditorState';\nimport {WebsocketProvider} from 'y-websocket';\n\nfunction Editor() {\n  const initialConfig = {\n    // NOTE: This is critical for collaboration plugin to set editor state to null. It\n    // would indicate that the editor should not try to set any default state\n    // (not even empty one), and let collaboration plugin do it instead\n    editorState: null,\n    namespace: 'Demo',\n    nodes: [],\n    onError: (error: Error) => {\n      throw error;\n    },\n    theme: {},\n  };\n\n  const getDocFromMap = (id: string, yjsDocMap: Map<string, Y.Doc>): Y.Doc => {\n    let doc = yjsDocMap.get(id);\n  \n    if (doc === undefined) {\n      doc = new Y.Doc();\n      yjsDocMap.set(id, doc);\n    } else {\n      doc.load();\n    }\n\n    return doc;\n  }\n\n  const providerFactory = useCallback(\n    (id: string, yjsDocMap: Map<string, Y.Doc>) => {\n      const doc = getDocFromMap(id, yjsDocMap);\n\n      return new WebsocketProvider('ws://localhost:1234', id, doc, {\n        connect: false,\n      });\n    }, [],\n  );\n\n  return (\n    <LexicalCollaboration>\n      <LexicalComposer initialConfig={initialConfig}>\n        <RichTextPlugin\n          contentEditable={<ContentEditable className=\"editor-input\" />}\n          placeholder={<div className=\"editor-placeholder\">Enter some rich text...</div>}\n          ErrorBoundary={LexicalErrorBoundary}\n        />\n        <CollaborationPlugin\n          id=\"lexical/react-rich-collab\"\n          providerFactory={providerFactory}\n          shouldBootstrap={true}\n        />\n      </LexicalComposer>\n    </LexicalCollaboration>\n  );\n}\n```\n\n**Initial editor content:**\n\nIn a production environment, you should bootstrap the editor's initial content on the server. If bootstrapping was left to the client and two clients connected at the same time, they could both try to initialize the content resulting in document corruption.\n\nUsing the `withHeadlessCollaborationEditor` function from the [FAQ](faq.md) page, you can create a bootstrapped `Y.Doc` with the following:\n\n```tsx\nimport type {CreateEditorArgs} from 'lexical';\n\nimport {$getRoot, $createParagraphNode} from 'lexical';\nimport {Doc} from 'yjs';\n\nimport {withHeadlessCollaborationEditor} from './createHeadlessCollaborativeEditor';\n\nfunction createBootstrappedYDoc(nodes: CreateEditorArgs['nodes']): Doc {\n  return withHeadlessCollaborationEditor(nodes, (editor, binding) => {\n    editor.update(() => {\n      $getRoot().append($createParagraphNode());\n    }, {discrete: true});\n    // binding.doc is the Doc the editor synced into\n    return binding.doc;\n  });\n}\n```\n\nIf you're simply following the above example to play around in a local dev environment, then you can add the following props to `CollaborationPlugin` to initialize the editor state client-side:\n\n```tsx\n// Dev-testing only, do not use in real-world cases.\ninitialEditorState={$initialEditorState}\nshouldBootstrap={true}\n```\n\n## See it in action\n\nSource code: [examples/react-rich-collab](https://github.com/facebook/lexical/tree/main/examples/react-rich-collab)\n\n<iframe width=\"100%\" height=\"600\" src=\"https://stackblitz.com/github/facebook/lexical/tree/main/examples/react-rich-collab?embed=1&file=src%2FApp.tsx&terminalHeight=0&ctl=1&showSidebar=0&devtoolsheight=0&view=preview\" sandbox=\"allow-forms allow-modals allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin allow-scripts\"></iframe>\n\n## Sharing one Yjs document between editors\n\nBy default a binding uses the `XmlText` shared type named `root` on the Yjs\n`Doc`, so each editor needs its own document. Two options let several editors\nlive in one document instead, which is what applications like collaborative\nsticky notes or comment threads usually want:\n\n- `rootName` picks a different top-level shared type, one per editor:\n\n```jsx\n<CollaborationPlugin\n  id={`note-${noteId}`}\n  providerFactory={providerFactory}\n  rootName={`note-${noteId}`}\n  shouldBootstrap={true}\n/>\n```\n\n- `getXmlText` resolves the root yourself, for roots that are not top-level\n  shared types (for example an `XmlText` stored in a `Y.Map`). It takes\n  precedence over `rootName`:\n\n```jsx\nconst getXmlText = useCallback(\n  (doc) => doc.getMap('notes').get(noteId),\n  [noteId],\n);\n\n<CollaborationPlugin\n  id={`note-${noteId}`}\n  providerFactory={providerFactory}\n  getXmlText={getXmlText}\n  shouldBootstrap={true}\n/>\n```\n\nGive each editor its own `id`, as above: the `id` keys the document map and the\nremote-cursor highlight registry, both of which are shared across the page, so\ntwo editors mounted under one `id` overwrite each other's entries.\n\nBoth options are also available on `createYjsBinding` for non-React usage.\n\n:::warning\n\nThe root is chosen when the binding is created, and a mounted editor cannot be\nrepointed at another root: changing `rootName` or `getXmlText` afterwards has no\neffect. To switch documents, remount the **editor** — put a `key` that changes\nwith the document on the component that owns the `LexicalComposer`. Remounting\nonly the plugin is not enough: the editor would keep the previous document's\ncontent and write it into the newly selected root.\n\n:::\n\n`getXmlText` runs while the binding is constructed, before the provider has\nsynced, so it has to resolve the root from whatever the local `Doc` holds at\nthat moment — reaching into a `Y.Array` that the server has not sent yet throws.\nEither mount the editor only once the note exists locally, or have the callback\ncreate the missing shared type. Write it to be idempotent: it can run more than\nonce for one editor (React StrictMode double-invokes render, and a remount\nbuilds a new binding), and a callback that unconditionally stores a new\n`XmlText` would throw away what the previous call stored.\n\nThe returned `XmlText` is checked to be an `XmlText` that is integrated into the\ndocument; that it is not already driving another live binding is yours to\nguarantee, since a second binding over one root takes over the collab nodes\ncached on its shared types.\n\nThe experimental `CollaborationPluginV2__EXPERIMENTAL` (and\n`createBindingV2__EXPERIMENTAL`) take the same two options, except that its\nroot is an `XmlElement`, so the callback is named `getXmlElement`. Create that\nelement as `new Y.XmlElement()` without a `nodeName`, which is how the root is\nrecognized.\n\n## Building collaborative plugins\n\n[Lexical Playground](https://playground.lexical.dev/) features set of the collaboration enabled plugins that integrate with primary document via `useCollaborationContext()` hook. Notable mentions:\n\n- [`CommentPlugin`](https://github.com/facebook/lexical/tree/main/packages/lexical-playground/src/plugins/CommentPlugin) - features use of the separate provider and Yjs room to sync comments.\n- [`ImageComponent`](https://github.com/facebook/lexical/blob/main/packages/lexical-playground/src/nodes/ImageComponent.tsx) - features use of the `LexicalExtensionEditorComposer` for its nested caption editor.\n- [`PollOptionComponent`](https://github.com/facebook/lexical/blob/main/packages/lexical-playground/src/nodes/PollComponent.tsx) - showcases poll implementation using `name` from the collaboration context.\n- [`StickyComponent`](https://github.com/facebook/lexical/blob/main/packages/lexical-playground/src/nodes/StickyComponent.tsx) - features use of the `LexicalExtensionEditorComposer` for its nested editor as well as sticky note position real-time sync.\n\n:::note\n\nWhile these \"playground\" plugins aren't production ready - they serve as a great example of collaborative Lexical capabilities as well as provide a good starting point.\n\n:::\n\n## Custom node property syncing\n\n`@lexical/yjs` syncs custom node properties by constructing a fresh node instance and inspecting its **own enumerable properties**. This means every property that should be synced across peers must be assigned in the constructor, even if the initial value is `undefined`.\n\n:::warning\n\nDeclaring a class property with TypeScript's optional syntax (`foo?: Type`) does **not** guarantee the property exists as an own enumerable property on the instance. Under TypeScript's default compilation settings (target < ES2022 without `useDefineForClassFields`), such declarations produce no initialization code.\n\n:::\n\n### What to do\n\nAlways initialize every node property in the constructor. Conditional initialization such as `if (someValue !== undefined) this.__someValue = someValue` is not enough, because the default construction path still needs to create the property.\n\nFor example:\n\n```ts\n// ✅ Correct — property is guaranteed to be an own property\nclass MyNode extends ElementNode {\n  __someValue: string | undefined;\n\n  constructor(someValue?: string, key?: NodeKey) {\n    super(key);\n    this.__someValue = someValue; // explicitly initialized\n  }\n}\n\n// ❌ Incorrect — property may not exist as an own property\nclass MyNode extends ElementNode {\n  __someValue?: string; // optional syntax without initialization\n\n  constructor(key?: NodeKey) {\n    super(key);\n    // __someValue is never assigned → won't be synced via yjs\n  }\n}\n```\n\nIf you use [`NodeState`](../concepts/node-state.md) for your custom properties, this concern does not apply. `NodeState` values are synced correctly without relying on constructor-created enumerable properties.\n\n## Yjs providers\n\nSetting up the communication between clients, managing awareness information, and storing shared data for offline usage is quite a hassle. Providers manage all that for you and are the perfect starting point for your collaborative app.\n\nSee [Yjs Website](https://docs.yjs.dev/ecosystem/connection-provider) for the list of the officially endorsed providers. Although it's not an exhaustive one.\n","frontmatter":{"sidebar_position":1},"isInternal":false,"tokens":2689,"sizeBytes":11650},{"name":"_category_.json","path":"packages/lexical-website/docs/concepts/_category_.json","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-website/docs/concepts/_category_.json","title":"Concepts Skill","category":"anthropic-skill","format":"json","content":"{\n  \"label\": \"Concepts\",\n  \"position\": 3\n}\n","frontmatter":{"label":"Concepts","position":3},"isInternal":false,"tokens":17,"sizeBytes":43},{"name":"commands.md","path":"packages/lexical-website/docs/concepts/commands.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-website/docs/concepts/commands.md","title":"Concepts Skill","category":"anthropic-skill","format":"markdown","content":"\n\n# Commands\n\nCommands are a very powerful feature of Lexical that lets you register listeners for events like `KEY_ENTER_COMMAND` or `KEY_TAB_COMMAND` and contextually react to them _wherever_ & _however_ you'd like.\n\nThis pattern is useful for building [`Toolbars`](https://github.com/facebook/lexical/blob/main/packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx) or complex extensions and nodes such as [tables](https://github.com/facebook/lexical/tree/main/packages/lexical-table) which require special handling for `selection`, `keyboard events`, and more.\n\nWhen registering a `command` you supply a `priority` and can return `true` to mark it as \"handled\", which stops other listeners from receiving the event. If a command isn't handled explicitly by you, it's likely handled by default in [`RichTextExtension`](https://github.com/facebook/lexical/blob/main/packages/lexical-rich-text/src/index.ts) or [`PlainTextExtension`](https://github.com/facebook/lexical/blob/main/packages/lexical-plain-text/src/index.ts).\n\n## `createCommand(...)`\n\nYou can view the core commands in the [`lexical` API reference](/docs/api/modules/lexical#commands). Other packages define their own, listed under Commands in each package's API reference, such as [`TOGGLE_LINK_COMMAND`](/docs/api/modules/lexical_link#toggle_link_command) in `@lexical/link`. If you need a custom command for your own use case, check out the typed `createCommand(...)` function.\n\n```js\nconst HELLO_WORLD_COMMAND: LexicalCommand<string> = createCommand('HELLO_WORLD');\n\neditor.registerCommand(\n  HELLO_WORLD_COMMAND,\n  (payload: string) => {\n    console.log(payload); // Hello World!\n    return false;\n  },\n  COMMAND_PRIORITY_EDITOR,\n);\n\neditor.dispatchCommand(HELLO_WORLD_COMMAND, 'Hello World!');\n```\n\n## `editor.dispatchCommand(...)`\n\nCommands can be dispatched from anywhere you have access to the `editor` such as a Toolbar Button, an event listener, or an extension, but most of the core commands are dispatched from [`LexicalEvents.ts`](https://github.com/facebook/lexical/blob/main/packages/lexical/src/LexicalEvents.ts).\n\nCalling `dispatchCommand` will implicitly call `editor.update` to trigger its command listeners if it was not called from inside `editor.update`.\n\nDon't dispatch a command from inside a read-only context such as\n`editor.read(fn)` or `editorState.read(fn)`. Command listeners usually change\nthe editor, so Lexical runs them in a separate writable update and\ndevelopment builds log a warning when they detect this. Dispatch after the\nread returns instead.\n\n```js\neditor.dispatchCommand(command, payload);\n```\n\nThe `payload`s are typed via the `createCommand(...)` API, but they're usually a DOM `event` for commands dispatched from an event listener.\n\nHere are some real examples from [`LexicalEvents.ts`](https://github.com/facebook/lexical/blob/main/packages/lexical/src/LexicalEvents.ts).\n\n```js\neditor.dispatchCommand(KEY_ARROW_LEFT_COMMAND, event);\n// ...\neditor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic');\n```\n\nAnd another example from the [`ToolbarPlugin`](https://github.com/facebook/lexical/blob/main/packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx) in our Playground.\n\n```js\nconst formatBulletList = () => {\n  editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND);\n};\n```\n\nWhich is later handled by [`ListExtension`](https://github.com/facebook/lexical/blob/main/packages/lexical-list/src/LexicalListExtension.ts) to insert the list into the editor.\n\n```js\neditor.registerCommand(\n  INSERT_UNORDERED_LIST_COMMAND,\n  () => {\n    $insertList('bullet');\n    return true;\n  },\n  COMMAND_PRIORITY_LOW,\n);\n```\n\n## `editor.registerCommand(...)`\n\nYou can register a command from anywhere you have access to the `editor` object, but it's important that you remember to clean up the listener with its remove listener callback when it's no longer needed.\n\nThe command listener will always be called from an `editor.update`, so you may use dollar functions. You should not use\n`editor.update` (and *never* call `editor.read(...)` or `editor.read('force-commit', ...)`) synchronously from within a command listener. It is safe to call\n`editor.read('latest', ...)` or `editor.getEditorState().read` if you need to read the previous state after updates have already been made.\n\n```js\nconst removeListener = editor.registerCommand(\n  COMMAND,\n  (payload) => boolean, // Return true to stop propagation.\n  priority,\n);\n// ...\nremoveListener(); // Cleans up the listener.\n```\n\nThe easiest way to get clean-up right is to register commands from an\n[extension](/docs/extensions/defining-extensions)'s `register` function and\nreturn the remove callback. The editor calls it when it is disposed. To\nregister several things, combine their callbacks with `mergeRegister` from\n`@lexical/utils`.\n\n```js\nimport {LinkExtension, TOGGLE_LINK_COMMAND} from '@lexical/link';\nimport {COMMAND_PRIORITY_BEFORE_EDITOR, defineExtension} from 'lexical';\n\nexport const LinkLoggerExtension = defineExtension({\n  // The command it observes is handled by LinkExtension\n  dependencies: [LinkExtension],\n  name: '@my-app/LinkLogger',\n  register: (editor) =>\n    editor.registerCommand(\n      TOGGLE_LINK_COMMAND,\n      (payload) => {\n        console.log('link', payload);\n        return false; // let the link extension handle it too\n      },\n      // LinkExtension handles this command at COMMAND_PRIORITY_EDITOR and\n      // returns true, so observe it at the BEFORE variant of that priority.\n      COMMAND_PRIORITY_BEFORE_EDITOR,\n    ),\n});\n```\n\nA listener that only observes a command, like this one, needs to run before\nthe listener that handles it. Depending on the extension that handles it and\nusing the `COMMAND_PRIORITY_BEFORE_*` variant of its priority does that\nwithout escalating to a higher priority. See\n[Priorities and ordering](#priorities-and-ordering) below.\n\nIn a legacy React plugin, return the `registerCommand` call from a\n`useEffect` instead.\n\nAnd as seen above and below, `registerCommand`'s callback can return `true` to signal to the other listeners that the command has been handled and propagation will be stopped.\n\nHere's a simplified example of handling a `KEY_TAB_COMMAND` from [`TabIndentationExtension`](https://github.com/facebook/lexical/blob/main/packages/lexical-extension/src/TabIndentationExtension.ts), which is used to dispatch a `OUTDENT_CONTENT_COMMAND` or `INDENT_CONTENT_COMMAND`.\n\n```js\neditor.registerCommand(\n  KEY_TAB_COMMAND,\n  (payload) => {\n    const event: KeyboardEvent = payload;\n    event.preventDefault();\n    return editor.dispatchCommand(\n      event.shiftKey ? OUTDENT_CONTENT_COMMAND : INDENT_CONTENT_COMMAND,\n    );\n  },\n  COMMAND_PRIORITY_EDITOR,\n);\n```\n\nNote that the same `KEY_TAB_COMMAND` command is registered by [`LexicalTableSelectionHelpers.ts`](https://github.com/facebook/lexical/blob/main/packages/lexical-table/src/LexicalTableSelectionHelpers.ts), which handles moving focus to the next or previous cell within a `TableNode`, but the priority is high (`COMMAND_PRIORITY_HIGH`) because this behavior is very important.\n\n### Priorities and ordering\n\nCommand listeners are called in the following order until a listener returns `true`:\n\n- From priority highest to lowest (critical, high, normal, low, editor)\n- All `COMMAND_PRIORITY_BEFORE_${priority}` listeners, most recently registered first\n- All `COMMAND_PRIORITY_${priority}` listeners, in registration order\n\n:::note\n\nThe `COMMAND_PRIORITY_BEFORE_*` priorities make it much easier to override\ndefault behavior without escalating the priority.\n\n:::\n\nIt is best practice to use the lowest priority possible, so most commands will\nbe registered with `COMMAND_PRIORITY_EDITOR` for the default behavior, then\ncommands to override that behavior can be registered with\n`COMMAND_PRIORITY_BEFORE_EDITOR`. The higher priorities mostly serve purposes\nsuch as being able to observe-but-not-handle events and to support legacy code\nthat predates the availability of `COMMAND_PRIORITY_BEFORE_*`.\n\nA modern lexical app will typically only need the\n`COMMAND_PRIORITY_BEFORE_EDITOR` priority since the\nlast-registered-called-first ordering is suitable for almost all use cases. The\nolder priorities without BEFORE can be considered legacy and are primarily\noffered for compatibility.\n\nHere is the full ordering of priorities, from lowest to highest:\n\n- `COMMAND_PRIORITY_EDITOR`\n- `COMMAND_PRIORITY_BEFORE_EDITOR`\n- `COMMAND_PRIORITY_LOW`\n- `COMMAND_PRIORITY_BEFORE_LOW`\n- `COMMAND_PRIORITY_NORMAL`\n- `COMMAND_PRIORITY_BEFORE_NORMAL`\n- `COMMAND_PRIORITY_HIGH`\n- `COMMAND_PRIORITY_BEFORE_HIGH`\n- `COMMAND_PRIORITY_CRITICAL`\n- `COMMAND_PRIORITY_BEFORE_CRITICAL`\n","isInternal":false,"tokens":1849,"sizeBytes":8664},{"name":"compared-with-prosemirror.md","path":"packages/lexical-website/docs/concepts/compared-with-prosemirror.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-website/docs/concepts/compared-with-prosemirror.md","title":"Concepts Skill","category":"anthropic-skill","format":"markdown","content":"# Compared with ProseMirror\n\nLexical and [ProseMirror](https://prosemirror.net) are both frameworks for\nbuilding rich text editors on top of `contentEditable`, and both keep an\nimmutable document model as the source of truth. This page collects the main\ndifferences in one place, for readers who know one and are learning the other.\n\n## Document model {#document-model}\n\nLexical's tree is shaped like the HTML it renders (see\n[Document Model](./document-model.md)). This is a deliberate difference from\nProseMirror. ProseMirror's document is also a tree,\nand blocks can contain other blocks, but the inline content of a textblock is\na flat sequence of nodes (text and inline nodes) with marks for formatting and\nlinks, and every position in the document is a single integer.\n\n| | Lexical | ProseMirror |\n| -- | -- | -- |\n| Document | Immutable tree of nodes, one `RootNode` | Immutable tree of nodes, one `doc` node |\n| Node identity and navigation | Each node has a runtime key and knows its parent and siblings | Nodes are plain values with no parent links; a resolved position supplies ancestor context |\n| Blocks | `ElementNode` subclasses (paragraph, heading, list, table, …), or a block `DecoratorNode` (image, embed, …) | Block nodes defined in the schema |\n| Inline formatting | Format flags on each `TextNode` (bold, italic, code, …) | Marks on text |\n| Links and other inline wrappers | Inline `ElementNode`s (`LinkNode`, `MarkNode`) that contain text nodes | Marks on text, like formatting |\n| Embedded content | `DecoratorNode`, inline or block, rendered by your framework (for example React) | Leaf or atom nodes, often with a custom `NodeView` |\n| Several editable regions in one node | [Named slots](./named-slots.md): regions addressed by name, like a card's `title`, each isolated so editing and selection never cross the boundary | Child nodes in the order the schema's content expression allows, optionally marked `isolating`, or a separate editor inside a `NodeView` |\n| Addressing a position | Node key plus offset (`{key, offset, type}`) | One integer counted across the whole document |\n| Allowed structure | Declared by node classes, enforced with node transforms and normalization | Declared by a schema of content expressions |\n| Applying edits | Call node and selection methods inside `editor.update()` | Build a transaction from steps that address positions or ranges |\n| Rendering | Each node's `createDOM()`/`updateDOM()`, applied by the reconciler, and optionally changed per node class by a `DOMRenderExtension` override | `toDOM` in the schema, or a `NodeView` |\n\n## Serialization {#serialization}\n\nBecause a Lexical link is an element that already contains its text, it\nexports as an `<a>` around those text nodes. A mark-based model has to find the\nadjacent text that shares a mark and group it into one element first.\n\n| Format | Lexical | ProseMirror |\n| -- | -- | -- |\n| JSON | `editorState.toJSON()`, restored with `editor.parseEditorState()` | `doc.toJSON()` and `Node.fromJSON(schema, json)` |\n| HTML export | `$generateHtmlFromNodes()`, using each node's `createDOM()` or `exportDOM()` | `DOMSerializer.fromSchema(schema)`, using `toDOM` from the schema's nodes and marks |\n| HTML import | `$generateNodesFromDOM()`, using `importDOM()` or `DOMImportExtension` rules | `DOMParser.fromSchema(schema)`, using the schema's `parseDOM` rules |\n| Markdown | `@lexical/markdown` transformers or `@lexical/mdast` | `prosemirror-markdown`: a markdown-it parser and a serializer with a function per node and mark |\n| Clipboard | Copy writes plain text, HTML, and Lexical JSON. Paste prefers the JSON, then HTML, then plain text. | Copy writes HTML and plain text. Paste parses the HTML with the schema's parse rules. |\n\nIn the JSON, a Lexical link is a `link` node with its text nodes as\n`children`, and bold is a `format` value on a `text` node, where\nProseMirror's JSON gives each text node a list of `marks`. In both editors the\ndefault HTML export reuses the definition that renders the editor\n(`createDOM()` in Lexical, `toDOM` in ProseMirror), so the two stay in step\nunless you override one. See [Serialization](../serialization/serialization.md)\nfor the details of each Lexical format.\n\n## History {#history}\n\nBoth editors group consecutive edits into one undo step. Lexical's\n[`HistoryExtension`](./history.md) merges edits made within `delay`\n(300ms by default) and keeps an unlimited stack unless you set `maxDepth`.\nProseMirror's history plugin uses `newGroupDelay: 500` and `depth: 100` by\ndefault, so `configExtension(HistoryExtension, {delay: 500, maxDepth: 100})`\nmatches its behavior.\n","isInternal":false,"tokens":1091,"sizeBytes":4640},{"name":"document-model.md","path":"packages/lexical-website/docs/concepts/document-model.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-website/docs/concepts/document-model.md","title":"Concepts Skill","category":"anthropic-skill","format":"markdown","content":"# Document Model\n\nAn [editor state](./editor-state.md) holds a tree of nodes. This page explains\nhow that tree is shaped.\n\n## A DOM-like tree\n\nLexical's node tree is shaped like the HTML it renders. A paragraph is an\n`ElementNode` whose children are the nodes inside it, and inline structure\nnests the same way: a link is a `LinkNode` element that contains the text\nnodes it wraps, just as an `<a>` contains its text. Each node type decides how\nit renders with `createDOM()` and `updateDOM()`, so the model maps closely to\nthe DOM, and a position in the document is a node plus an offset within it.\n\nHere is a small document with a heading, a paragraph containing bold text and\na link, a block image, and a card that has a named `title` slot as well as\nordinary body children:\n\n```mermaid\nflowchart TB\n  accTitle: The shape of a Lexical document tree\n  accDescr: The root has four children in order: a heading, a paragraph, an image, and a card. The heading contains the text \"Welcome\". The paragraph contains the text \"Read the \", a bold text node \"docs\", and a link, which contains the text \"here\". The image is a block decorator node with no children. The card has a named slot called title, which holds a paragraph containing the text \"Card title\", and one ordinary child, a paragraph containing the text \"Card body\".\n  root[\"Root\"] --> heading[\"Heading (h1)\"]\n  root --> paragraph[\"Paragraph\"]\n  root --> image[\"Image<br/>(block DecoratorNode)\"]\n  root --> card[\"Card<br/>(ElementNode)\"]\n  heading --> welcome[\"Text: 'Welcome'\"]\n  paragraph --> read[\"Text: 'Read the '\"]\n  paragraph --> docs[\"Text: 'docs'<br/>{ format: bold }\"]\n  paragraph --> link[\"Link\"]\n  link --> here[\"Text: 'here'\"]\n  card -.->|\"slot: title\"| titlePara[\"Paragraph\"]\n  titlePara --> titleText[\"Text: 'Card title'\"]\n  card --> body[\"Paragraph\"]\n  body --> bodyText[\"Text: 'Card body'\"]\n```\n\nSolid arrows are ordinary children, which form an ordered list under their\nparent and are read with `getChildren()`. The dashed arrow is a\n[named slot](./named-slots.md): a separate channel on the host, keyed by name\ninstead of position and read with `$getSlot(card, 'title')`. A slot's value\nis the root of its own isolated region, so its `getParent()` is `null` and\n`$getSlotHost()` leads back to the card. Any `ElementNode` or `DecoratorNode`\ncan host slots, so a decorator such as the image could also expose an\neditable caption this way.\n\n## Working with the tree\n\nLexical code navigates the document the way DOM code does, with methods like\n`getParent()`, `getChildren()`, and `getNextSibling()`, and each node owns the\nDOM it renders. The resemblance is structural, not one\nnode per HTML element: bold or italic text, for example, is a format on a\n`TextNode`, not a separate `<strong>` or `<em>` node. See\n[Nodes](./nodes.mdx) for the built-in node types.\n\n## Customizing rendering\n\nYou can also change how nodes render without subclassing them.\n[`DOMRenderExtension`](../serialization/dom-render.md) from `@lexical/html`\ntakes overrides for `createDOM`, `updateDOM`, and `exportDOM`, for one node\nclass or for every node, such as adding a `data-` attribute or wrapping a\nnode's children in another element. Each override calls `$next()` to get the\ndefault result and adjusts it, so overrides from several extensions compose.\nExport overrides only affect HTML export, and `createDOM` overrides also carry\nthrough to export for nodes that use the default `exportDOM`. Changes made\nwhile updating the editor's DOM don't; see\n[Lexical -> HTML](../serialization/serialization.md#lexical---html) for which\nhooks reach export.\n\nFor how the tree is converted to and from JSON, HTML, and Markdown, see\n[Serialization](../serialization/serialization.md#formats-at-a-glance). For\nhow this model differs from a mark-based editor, see\n[Compared with ProseMirror](./compared-with-prosemirror.md).\n","isInternal":false,"tokens":949,"sizeBytes":3859},{"name":"dom-events.md","path":"packages/lexical-website/docs/concepts/dom-events.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-website/docs/concepts/dom-events.md","title":"Concepts Skill","category":"anthropic-skill","format":"markdown","content":"\n\n# Working with DOM Events\n\nSometimes, when working with Lexical, it might be necessary or useful for you to attach a DOM Event Listener to the underlying DOM nodes that Lexical controls. For instance, you might want to show a popover when a user mouses over a specific node or open a modal when they click on a node. Either of these use cases (and many others) can be accomplished via native DOM Event Listeners. There are 3 main ways that you can listen for DOM Events on nodes controlled by Lexical:\n\n## 1. Event Delegation\n\nOne way to handle events inside the editor is to set a listener on the editor root element (the contentEditable Lexical attaches to). You can do this using a [Root Listener](listeners.md).\n\n```js\nfunction myListener(event) {\n    // You may want to filter on the event target here\n    // to only include clicks on certain types of DOM Nodes.\n    alert('Nice!');\n}\n\nconst ClickExtension = defineExtension({\n  name: '@my-app/Click',\n  register: (editor) =>\n    editor.registerRootListener((rootElement) => {\n      if (rootElement === null) {\n        return;\n      }\n      // add the listener to the current root element\n      rootElement.addEventListener('click', myListener);\n      // remove the listener when the root element changes - make sure the ref\n      // to myListener is stable so the removal works and you avoid a memory leak.\n      return () => rootElement.removeEventListener('click', myListener);\n    }),\n});\n```\n\nAn extension's `register` returns a cleanup function, and the editor calls it\nwhen it is disposed, so there is no teardown to write by hand. Add\n`ClickExtension` to your editor's `dependencies` to use it.\nThis can be a simple, efficient way to handle some use cases, since it's not necessary to attach a listener to each DOM node individually.\n\nThe `addEventListener`/`removeEventListener` pairing above is common enough that the core `lexical` package exports a `registerEventListener(target, type, listener, options?)` helper. It attaches the listener and returns a dispose function that removes it, so the example above becomes:\n\n```js\nimport {defineExtension, registerEventListener} from 'lexical';\n\nconst ClickExtension = defineExtension({\n  name: '@my-app/Click',\n  register: (editor) =>\n    editor.registerRootListener((rootElement) =>\n      // registerEventListener returns the matching removeEventListener cleanup,\n      // so there's no need to write the teardown by hand.\n      rootElement === null\n        ? undefined\n        : registerEventListener(rootElement, 'click', myListener),\n    ),\n});\n```\n\nIt mirrors the `addEventListener` overloads (so the event type is strongly typed) and composes well with `mergeRegister` when you need to register several listeners at once.\n\nTo attach several listeners to the same target, `registerEventListeners(target, listeners, options?)` takes a `{type: listener}` map and returns a single dispose function that removes all of them. Each listener's event argument is still strongly typed per event type, and the optional `options` argument is shared across every listener:\n\n```js\nimport {registerEventListeners} from 'lexical';\n\nconst removeListeners = registerEventListeners(\n  rootElement,\n  {\n    click: onClick,\n    keydown: onKeyDown, // receives a KeyboardEvent\n  },\n  {capture: true}, // shared by both listeners\n);\n```\n\nBecause `options` is shared, register a group that needs different options (such as a different `capture` flag) with a separate `registerEventListeners` call and combine the results with `mergeRegister`.\n\n## 2. Directly Attach Handlers\n\nIn some cases, it may be better to attach an event handler directly to the underlying DOM node of each specific node. With this approach, you generally don't need to filter the event target in the handler, which can make it a bit simpler. It will also guarantee that your handler isn't running for events that you don't care about. This approach is implemented via a [Mutation Listener](listeners.md).\n\n```js\nconst registeredElements: WeakSet<HTMLElement> = new WeakSet();\nconst removeMutationListener = editor.registerMutationListener(nodeType, (mutations) => {\n    editor.getEditorState().read(() => {\n        for (const [key, mutation] of mutations) {\n            const element: null | HTMLElement = editor.getElementByKey(key);\n            if (\n                // Updated might be a move, so that might mean a new DOM element\n                // is created. In this case, we need to add an event listener too.\n                (mutation === 'created' || mutation === 'updated') &&\n                element !== null &&\n                !registeredElements.has(element)\n            ) {\n                registeredElements.add(element);\n                element.addEventListener('click', (event: Event) => {\n                    alert('Nice!');\n                });\n            }\n        }\n    });\n});\n\n// teardown the listener - return it from an extension's `register` so the\n// editor removes it when it is disposed.\nremoveMutationListener();\n```\nNotice that here we don't worry about cleaning up, as Lexical will dereference the underlying DOM nodes and allow the JavaScript runtime garbage collector to clean up their listeners.\n\n## 3. Use NodeEventPlugin\n\nIf you're using React, we've wrapped approach #2 up into a simple React plugin that you can render inside your composer to achieve the same effect, without worrying about the details:\n\n```jsx\n<LexicalExtensionComposer extension={appExtension}>\n    <NodeEventPlugin\n        nodeType={LinkNode}\n        eventType={'click'}\n        eventListener={(e: Event) => {\n            alert('Nice!');\n        }}\n    />\n</LexicalExtensionComposer>\n```\n\nIf the editor lives inside a shadow root or an iframe, see\n[Shadow DOM and iframes](./shadow-dom.md) for the helper to read\n`event.target` through the shadow boundary — the browser retargets it to the\nshadow host on listeners attached above the boundary, so a plain\n`event.target` won't match a node inside the editor.\n","isInternal":false,"tokens":1266,"sizeBytes":5987},{"name":"editor-state.md","path":"packages/lexical-website/docs/concepts/editor-state.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-website/docs/concepts/editor-state.md","title":"Concepts Skill","category":"anthropic-skill","format":"markdown","content":"\n\n# Editor State\n\n## Why is it necessary?\n\nWith Lexical, the source of truth is not the DOM, but rather an underlying state model\nthat Lexical maintains and associates with an editor instance.\n\nWhile HTML is great for storing rich text content it's often \"way too flexible\" when it comes to text editing.\nFor example the following lines of content will produce equal outcome:\n\n```html\n<i><b>Lexical</b></i>\n<i><b>Lex<b><b>ical</b></i>\n<b><i>Lexical</i></b>\n```\n\n<details>\n  <summary>See rendered version!</summary>\n  <div>\n    <i><b>Lexical</b></i>\n    <i><b>Lex</b><b>ical</b></i>\n    <b><i>Lexical</i></b>\n  </div>\n</details>\n\nOf course, there are ways to normalize all these variants to a single canonical form, however this would require DOM manipulation and so re-rendering of the content. And to overcome this we can use Virtual DOM, or State.\n\nOn top of that it allows to decouple content structure from content formatting. Let's look at this example stored in HTML:\n\n```html\n<p>Why did the JavaScript developer go to the bar? <b>Because he couldn't handle his <i>Promise</i>s</b></p>\n```\n\n<figure class=\"text--center\">\n  <img src=\"/img/docs/state-formatting-html.drawio.svg\" alt=\"Nested structure of the HTML state\"/>\n  <figcaption>Nested structure of the HTML state because of the formatting</figcaption>\n</figure>\n\nIn contrast, Lexical decouples structure from formatting by offsetting this information to attributes. This allows us to have canonical document structure regardless of the order in which different styles were applied.\n\nHere is the same content as a Lexical node tree, printed the way the\n[tree view](/docs/getting-started/devtools) shows it (the numbers are node keys):\n\n```text\nroot\n  └ (2) paragraph\n    ├ (3) text \"Why did the JavaScript developer go to the bar? \"\n    ├ (4) text \"Because he couldn't handle his \" { format: bold }\n    ├ (5) text \"Promise\" { format: bold, italic }\n    └ (6) text \"s\" { format: bold }\n```\n\nThe paragraph's children are a flat list of text nodes, and the formatting is a\nproperty of each one, so there is only one way to represent this content.\n\n## Understanding the Editor State\n\nYou can get the latest editor state from an editor by calling `editor.getEditorState()`.\n\nEditor states have two phases:\n\n- During an update they can be thought of as \"mutable\". See \"Updating state\" below to\n  mutate an editor state.\n- After an update, the editor state is then locked and deemed immutable from there on. This\n  editor state can therefore be thought of as a \"snapshot\".\n\nEditor states contain two core things:\n\n- The editor node tree (starting from the root node). See\n  [Document Model](./document-model.md) for how the tree is shaped.\n- The editor [selection](./selection.md) (which can be null).\n\nFor example, \"Hello world\" with a link around \"world\" and the caret at the\nend looks like this:\n\n```mermaid\nflowchart TB\n  accTitle: A simple editor state\n  accDescr: The root contains a paragraph with a \"Hello \" text node and a link. The link contains the \"world\" text node. The selection's anchor and focus both point to offset 5 in \"world\".\n  subgraph state[\"EditorState\"]\n    direction TB\n    root[\"Root\"] --> paragraph[\"Paragraph\"]\n    paragraph --> hello[\"Text: 'Hello '\"]\n    paragraph --> link[\"Link\"]\n    link --> world[\"Text: 'world'\"]\n    selection[\"Selection\"] -.->|\"caret at offset 5\"| world\n  end\n```\n\n`editor.getEditorState()` returns the latest committed state. Its `toJSON()`\nmethod serializes the document tree only; the selection and the runtime node\nkeys are not included. To restore saved content, pass the JSON to\n`editor.parseEditorState()` and the result to `editor.setEditorState()`. The\neditor must have the same node types registered. See\n[Serialization](../serialization/serialization.md) for JSON, HTML, and\nMarkdown.\n\nHere's an example of how you can initialize an editor with saved content and\nthen persist it. The saved JSON is passed as the editor's `$initialEditorState`:\n\n```js\nimport {buildEditorFromExtensions} from '@lexical/extension';\nimport {RichTextExtension} from '@lexical/rich-text';\nimport {defineExtension} from 'lexical';\n\n// Get the saved content (e.g. loaded from a backend)\nconst initialEditorState = await loadContent();\n\nconst editor = buildEditorFromExtensions(\n  defineExtension({\n    $initialEditorState: initialEditorState,\n    dependencies: [RichTextExtension],\n    name: '@my-app/editor',\n  }),\n);\neditor.setRootElement(document.getElementById('editor'));\n\n// Store the content (e.g. when the user submits a form)\nasync function onSubmit() {\n  await saveContent(JSON.stringify(editor.getEditorState()));\n}\n```\n\nWith React, pass the extension to `LexicalExtensionComposer`. A component\nrendered inside it can reach the editor with `useLexicalComposerContext()`:\n\n```jsx\nimport {LexicalExtensionComposer} from '@lexical/react/LexicalExtensionComposer';\nimport {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';\nimport {RichTextExtension} from '@lexical/rich-text';\nimport {defineExtension} from 'lexical';\nimport {useMemo} from 'react';\n\nfunction SaveButton() {\n  const [editor] = useLexicalComposerContext();\n  return (\n    <button\n      onClick={() => saveContent(JSON.stringify(editor.getEditorState()))}>\n      Save\n    </button>\n  );\n}\n\nfunction Editor({initialEditorState}) {\n  // The editor is recreated whenever this extension changes,\n  // so keep it stable for the life of the editor.\n  const extension = useMemo(\n    () =>\n      defineExtension({\n        $initialEditorState: initialEditorState,\n        dependencies: [RichTextExtension],\n        name: '@my-app/editor',\n      }),\n    [initialEditorState],\n  );\n  return (\n    <LexicalExtensionComposer extension={extension}>\n      <SaveButton />\n    </LexicalExtensionComposer>\n  );\n}\n```\n\n`$initialEditorState` is applied once, when the editor is built; changing it\nlater has no effect on that editor. See \"Updating state\" below for the proper\nway to change editor state after initialization.\n\n`$initialEditorState` accepts:\n\n- a JSON string or a parsed `SerializedEditorState` object, passed to\n  `editor.parseEditorState()` (as in the example above);\n- an `EditorState` instance, applied directly with `editor.setEditorState()`;\n- a function `(editor) => void`, run inside `editor.update(...)`;\n- `null`, which skips default initialization entirely. Use this with\n  [collaboration](/docs/collaboration/react) so that the Yjs document, not\n  Lexical, owns the initial state.\n\nOmitting the field (or passing `undefined`) seeds the root with a default empty\n`ParagraphNode`. The two are not interchangeable: `null` leaves the root with no children,\nwhile `undefined` produces a single empty line. If your `loadContent` may yield `null` or\n`undefined` for new documents, coalesce to `undefined` (e.g. `(await loadContent()) ?? undefined`)\nso the editor still gets the default paragraph rather than the collab-style uninitialized\nstate.\n\nThe legacy `LexicalComposer` takes the same values as `initialConfig.editorState`,\nexcept that a function there only runs if the root is still empty.\n\n## Updating state\n\nAll reads and writes of the document happen inside a synchronous callback:\n\n```js\nimport {$createParagraphNode, $createTextNode, $getRoot} from 'lexical';\n\neditor.update(() => {\n  const paragraph = $createParagraphNode();\n  paragraph.append($createTextNode('Hello world'));\n  $getRoot().append(paragraph);\n});\n\nconst text = editor.read('force-commit', () => $getRoot().getTextContent());\n```\n\n### The `$` function convention {#dollar-functions}\n\nFunctions whose names start with `$`, such as `$getRoot()` and\n`$getSelection()`, only work inside one of these callbacks, because they act\non the active editor state. Calling them anywhere else throws an error. The\nconvention is similar to React Hooks:\n\n| | React Hooks | Lexical `$` functions |\n| -- | -- | -- |\n| Naming | `useFunction` | `$function` |\n| Can only be called | while rendering a component | inside an update or read |\n| Can call others of the same kind | ✅ | ✅ |\n| Must be synchronous | ✅ | ✅ |\n| Must be called unconditionally, in the same order | ✅ | ❌ No such rule |\n\nCommand handlers and node transforms already run inside an update, so they\ncan call `$` functions directly.\n\nThis works because, while a callback runs, Lexical keeps the active editor and\nthe active editor state in module-level variables, and `$` functions read\nthem: `$getRoot()` returns the root of the active state, and `$getEditor()`\nreturns the active editor. Because the callback is synchronous, nothing else\ncan run in between and change them. The same mechanism enforces the\ndifference between reads and updates. Inside a read, a method that would\nchange a node throws `Cannot use method in read-only mode.`\n\nGive your own functions a `$` prefix when they call `$` functions, so it is\nclear where they can be used. The\n[`@lexical/rules-of-lexical`](/docs/packages/lexical-eslint-plugin) ESLint rule\nchecks this convention.\n\nThe same rule applies to node objects. Call node methods only inside a read or\nupdate. Every node has a key that identifies it across versions of the editor\nstate, and node methods use that key to find the latest version of the node.\nThis is why a node reference taken earlier in an update stays usable after the\nnode changes. Keys exist only at runtime: they are not serialized, and you\nshould treat them as opaque.\n\nBecause the callbacks are synchronous, do any asynchronous work (fetching\ndata, awaiting a promise) first, and then enter an update with the result.\n\nAvoid nesting one `editor.update()` inside another: the inner update does not\nrun immediately but is queued to run after the outer one. Never start an\nupdate inside a read.\n\nEach update works on a pending copy of the editor state, and updates made in\nthe same tick are committed and reconciled to the DOM together. See\n[Updates](./updates.mdx#update-lifecycle) for the full lifecycle.\n\n### Replacing the state\n\nAnother way to set state is `setEditorState` method, which replaces current state with the one passed as an argument.\n\nHere's an example of how you can set editor state from a stringified JSON:\n\n```js\nconst editorState = editor.parseEditorState(editorStateJSONString);\neditor.setEditorState(editorState);\n```\n\n:::warning\n\n`setEditorState` rejects an `EditorState` that satisfies `editorState.isEmpty()` — i.e.\nthe root is the only node and there is no selection. In a development build it throws\n`\"setEditorState: the editor state is empty. Ensure the editor state's root node never\nbecomes empty.\"`; in production it warns with the same message and recovers by appending\nan empty `ParagraphNode`, so the editor stays usable. The `EditorState` produced by\ninitializing with `editorState: null` and never appending content (typical for a\ncollaboration document before peers connect) has exactly this shape, so persisting and\nreloading such a state hits this path. Note that `parseEditorState` itself succeeds — the\ncheck lands on the apply step. To avoid relying on the recovery, guard with\n`editorState.isEmpty()` before calling `setEditorState`, or seed the document with a\n`ParagraphNode` before serializing.\n\n:::\n\n## Reading state\n\nWhich state you see depends on how you read it:\n\n- Inside `editor.update()` you see the **pending** state, where your changes\n  are visible but transforms and reconciliation may not have run yet.\n  `editor.read('pending', fn)` gives you the same view without allowing\n  changes.\n- `editor.read('force-commit', fn)` first commits any pending updates, so it\n  always sees a consistent, reconciled state. Do not call it inside an update.\n  `editor.read(fn)` with no mode does the same thing, and is kept for\n  convenience and backwards compatibility. Inside a mutation or update\n  listener it does not commit an update that another listener started (that\n  update commits after the listeners have run, so they observe commits in\n  order); use `editor.read('latest', fn)` there.\n- `editor.read('latest', fn)` reads the most recently reconciled state without\n  committing anything, so pending changes are not visible.\n- `editorState.read(fn, {editor})` reads one particular snapshot, such as the\n  `editorState` an update listener receives. Passing the `editor` keeps\n  `$getEditor()` and extension lookups working inside the callback.\n\n## State update listener\n\nIf you want to know when the editor updates so you can react to the changes, you can add an update\nlistener to the editor, as shown below:\n\n```js\neditor.registerUpdateListener(({editorState}) => {\n  // The latest EditorState can be found as `editorState`.\n  // To read the contents of the EditorState, use the following API:\n\n  editorState.read(\n    () => {\n      // Just like editor.update(), .read() expects a closure where you can use\n      // the $ prefixed helper functions.\n    },\n    // Passing the editor keeps $getEditor() and extension lookups working.\n    {editor},\n  );\n});\n```\n\n## When are Listeners, Transforms, and Commands called?\n\nThere are several types of callbacks that can be registered with the editor that are related to\nupdates of the Editor State.\n\n| Callback Type | When It's Called |\n| -- | -- |\n| Update Listener | After reconciliation |\n| Mutation Listener | After reconciliation |\n| Node Transform | During `editor.update()`, after the callback finishes, if any instances of the node type they are registered for were updated |\n| Command | As soon as the command is dispatched to the editor (called from an implicit `editor.update()`) |\n\n## Synchronous reconciliation with discrete updates\n\nWhile commit scheduling and batching are normally what we want, they can sometimes get in the way.\n\nConsider this example: you're trying to manipulate an editor state in a server context and then persist it in a database.\n\n```js\neditor.update(() => {\n  // manipulate the state...\n});\n\nsaveToDatabase(editor.getEditorState().toJSON());\n```\n\nThis code will not work as expected, because the `saveToDatabase` call will happen before the state has been committed.\nThe state that will be saved will be the same one that existed before the update.\n\nFortunately, the `discrete` option for `LexicalEditor.update` forces an update to be immediately committed.\n\n```js\neditor.update(() => {\n  // manipulate the state...\n}, {discrete: true});\n\nsaveToDatabase(editor.getEditorState().toJSON());\n```\n\n### Cloning state\n\nLexical state can be cloned, optionally with custom selection. One of the scenarios where you'd want to do it\nis setting editor's state but not forcing any selection:\n\n```js\n// Passing `null` as a selection value to prevent focusing the editor\neditor.setEditorState(editorState.clone(null));\n```\n","isInternal":false,"tokens":3370,"sizeBytes":14692},{"name":"headless.md","path":"packages/lexical-website/docs/concepts/headless.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-website/docs/concepts/headless.md","title":"Concepts Skill","category":"anthropic-skill","format":"markdown","content":"# Running Without a Browser\n\nThe DOM is only needed to show an editor on screen. Because every read and\nupdate goes through the editor state, Lexical also works where there is no\nbrowser DOM:\n\n- **With no DOM at all.** An editor that has no root element skips\n  reconciliation and DOM selection entirely, so you can build one with\n  `buildEditorFromExtensions()` and never call `setRootElement()`. Adding\n  `HeadlessExtension` from `@lexical/headless` makes that explicit: attaching a\n  root element then throws, and nested editors are headless too. Updates,\n  transforms, listeners, commands, and JSON serialization all still work,\n  which is enough to process documents on a server, apply changes from a\n  collaboration backend, or write tests.\n- **With a virtual DOM** such as happy-dom or jsdom, for the features that\n  create DOM nodes, like HTML import and export. `withDOM()` from\n  `@lexical/headless/dom` runs a callback with a temporary happy-dom window,\n  for example to convert HTML on a server.\n\n## Example: converting saved JSON to HTML on a server\n\n```ts\nimport {buildEditorFromExtensions} from '@lexical/extension';\nimport {HeadlessExtension} from '@lexical/headless';\nimport {withDOM} from '@lexical/headless/dom';\nimport {$generateHtmlFromNodes} from '@lexical/html';\nimport {RichTextExtension} from '@lexical/rich-text';\nimport {defineExtension} from 'lexical';\n\nconst editor = buildEditorFromExtensions(\n  defineExtension({\n    dependencies: [HeadlessExtension, RichTextExtension],\n    name: '@my-app/server-editor',\n  }),\n);\n\nexport function jsonToHtml(json: string): string {\n  editor.setEditorState(editor.parseEditorState(json));\n  return withDOM(() => editor.read(() => $generateHtmlFromNodes(editor)));\n}\n```\n\nThe editor must have the same node types registered as the editor that saved\nthe JSON, which is why it uses the same extensions. JSON and Markdown\nconversion do not need `withDOM()`.\n\n## `createHeadlessEditor()`\n\nBefore extensions, headless editors were created with `createHeadlessEditor()`\nfrom `@lexical/headless`. It still works, and it makes DOM-only methods such as\n`registerRootListener` and `registerMutationListener` throw. Because many\nextensions register those listeners, use `HeadlessExtension` with\n`buildEditorFromExtensions()` in new code.\n","isInternal":false,"tokens":517,"sizeBytes":2291},{"name":"history.md","path":"packages/lexical-website/docs/concepts/history.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-website/docs/concepts/history.md","title":"Concepts Skill","category":"anthropic-skill","format":"markdown","content":"# History\n\nLexical adds support for Undo and Redo through the `@lexical/history` package —\n`registerHistory`, the `HistoryExtension` extension, or the `HistoryPlugin`\nReact component from `@lexical/react/LexicalHistoryPlugin`. Part of undo and redo requires supporting coalescing of certain dynamic operations such as continuous typing. The below provides background information regarding the continuous typing undo coalescing feature.\n\n**Undo Coalescing of Typed Text**\n\n**TL;DR**\nWhen working with text editors, users expect a well polished undo experience. A key part of this is a smooth, well thought out workflow when undoing continuous typing events. For this post, strict typing events may be limited to typing characters, backspacing and forward deleting. Users generally expect that continuous typing should fully undo with a single undo gesture and that they don’t require separate undo gestures for each character.\nThe algorithm that covers this is “undo coalescing of typed text” and there’s a certain degree of nuance involved. This post covers that nuance while attempting to avoid prescriptive implementation details.\n\n**What Kind of Edits Require Coalescing?**\nAs mentioned above, continuous typing should be coalesced into a single undo action. Continuous typing is a loaded phrase and we’ll get into some details below.\nThere are other types of edits that require undo coalescing such as rapidly resizing an image or rapidly changing the font size of selected text. These latter two examples fall under what might be termed “dynamic operations” and their implementation might vary quite a bit from coalescing of typing.\n\n**What Constitutes Continuous Typing?**\nFor the purposes of undo coalescing, continuous typing events are safely limited to these three actions:\n\n1. Forward typing characters (heads up, composed character typing discussed later).\n2. Deleting backwards, e.g. backspace gestures including backspace key presses.\n3. Forward deletion, e.g. forward delete gestures including Delete key presses.\n\nAny interruption to the above breaks the notion of continuous typing. Note that switching between either of these three actions may itself be considered an interruption of continuous typing or it may not. This ends up being a usability decision. In my opinion, when examining various word processors, switching between these three actions should maintain continuity, because users often make typos or corrections in the flow of typing and therefore would lean towards the whole sequence of typing, backspacing and deleting to be within a single undoable session.\nNote that Google Docs and Quip treat a switch between typing, backspacing and deleting as disruptions to the continuous typing sessions.\n\n**Start with Selected Text and Followup with Typing**\nLet’s start with some selected text. The user may have selected the text directly, as a result of pasting text or through some other sequence of steps. When followed by continuous typing, the initial selection should be included along with the following typing events as part of a single coalesced undo session.\n\n**Timeouts**\nText editing is generally a modeless user experience and so just about any event beyond the above typing actions will disrupt continuous typing. The first and foremost is a timeout.\nQuip seems to require continuous typing to occur within 1 second intervals, whereas Google Docs requires about 2 seconds. In my opinion, the timeout may stretch to as long as 3 to 5 seconds. The more time between typing events, the more chance the user has to intentionally input a block of text, a thought or concept.\nThe use of a timeout is not necessarily required, and the amount of time is certainly up for debate. Collaborative co-editing also makes use of timeouts when grouping text that is merged across clients. Longer timeouts tend to make collaboration a smoother, less disruptive experience, because receiving other client input while expressing one’s thoughts tends to be unhelpful.\nImplementers should consider the correct timeout based on their customer expectations and related collaborative features that might share this timeout.\n\n**Hard and Soft Returns**\nHard returns as generated by pressing the Return key tend to disrupt and thereby reset continuous typing.\nFor Google Docs and Quip, soft returns as generated by pressing shift+Return key do not terminate continuous typing. This makes sense in that the soft return ends text wrap but does not end the paragraph.\n\n**Changing Selections and Key Focus**\nAny change to the selection by the user should terminate coalescing. This includes:\n\n- Arrow gesture navigation.\n- Gestures to select a word, a sentence or a paragraph.\n- Any other selection change that does not move the blinking caret to the next or previous character by way of the above mentioned typing events.\n\nAdditionally, ending keyboard focus on the editing window, including moving focus to another editing window or a different view within the same window should also terminate coalescing. This also includes setting focus to modal popovers, such as, for example, a hyperlink property editor.\n\n**Composed Characters**\nCharacter composition occurs when typing diacritics, CJK and even emojis. The process usually involves at least these three phases:\n\n- Start composition.\n- Replace composed text with other text and remain composing. This step may occur many times within a single composition session.\n- Stop composition either through accepting the text or canceling the latest input.\n\nCoalescing should terminate at start composition. Replacing composed text should generally be coalesced. Stopping composition may or may not reset coalescing depending upon the implementation. Quip appears to try to coalesce multiple composed characters, but disrupts coalescing when switching between composed and non-composed characters. Google Docs appears to disrupt coalescing more frequently. Both coalesce the text replacements.\n\n**Auto Correct, Spelling Corrections and Text Substitutions**\nText substitution behaves much like composed characters. When the text is transformed, the coalescing should terminate and the transformed text becomes the first coalesced entry in the next undo session.\n\n**Copy & Pasting**\nAny actions like copy and paste typically trigger events that are not part of the 3 continuous typing actions described above. So, copy and paste also terminates coalescing.\n\n**In Conclusion**\nText undo coalescing greatly improves the usability of undo in that it allows the user to bypass all the intermediate events that went into forming or re-forming text within a paragraph. Given the modeless nature of text editing, limiting continuous typing to strictly three events allows for a robust and clean implementation. Implementers will need tight bottlenecks to trap selection changes, key focus changes as well as changes from collaborative clients. While the above is not complete by any means, I hope readers will find it helpful.\n\n## Tuning HistoryExtension for memory and long sessions\n\n`HistoryExtension` ships with config values chosen for the common case\nof a short-lived editor on a richly interactive surface. For\napplications that keep an editor mounted for a long time — chat\nthreads, long documents, anything where the user may produce thousands\nof distinct history events without reloading the page — two settings\nare worth tuning explicitly.\n\n### `maxDepth` — cap the undo stack\n\nBy default `maxDepth` is `null`, meaning the undo stack is\n**unbounded**. Every separate-history boundary (see \"What Constitutes\nContinuous Typing?\" above) pushes a `HistoryStateEntry` that retains a\nfull `EditorState` snapshot. Snapshots share a copy-on-write\n`NodeMap`, so the marginal cost of any one entry is small, but it is\nnot zero — and an unbounded stack accumulates a long tail over time.\n\nSet a finite `maxDepth` to keep the stack at a fixed length. The\nextension FIFO-evicts the oldest entries past the cap; older snapshots\nbecome unreachable and can be collected.\n\n```ts\nimport {configExtension, defineExtension} from 'lexical';\nimport {HistoryExtension} from '@lexical/history';\n\nconst editorExtension = defineExtension({\n  // ...\n  dependencies: [\n    // 100 events is a reasonable starting point that supports a deep\n    // enough undo stack for almost any interactive editing session.\n    configExtension(HistoryExtension, {maxDepth: 100}),\n  ],\n});\n```\n\nThe cap is also exposed as a writable signal on\n`HistoryExtension.output.maxDepth`, so it can be reconfigured at\nruntime (the new value applies to future pushes — the current stack\nis not retroactively trimmed when the cap is lowered).\n\n### `delay` — coalesce more aggressively\n\n`delay` is the merge window during which adjacent edits collapse into\nthe current history entry instead of pushing a new one. The default is\n`300`ms. A longer delay means fewer history events for the same\ntyping burst, which:\n\n- reduces the rate at which `undoStack` grows, multiplying the effect\n  of any `maxDepth` cap, and\n- gives the user undo gestures that match larger semantic chunks of\n  their writing.\n\nFor text-heavy editors a value in the `500`–`1000`ms range is common.\n\n```ts\nconfigExtension(HistoryExtension, {delay: 500, maxDepth: 100}),\n```\n\n### When NOT to cap history\n\nIf your editor is short-lived (mounted only for the duration of a\nmodal, page form, or single message draft) and undo is expected to\nreach back to the document's initial state, the default `maxDepth:\nnull` is fine — the stack will be discarded with the editor instance.\nThe cap matters when an editor is **long-lived** and can accumulate\nthousands of distinct history events without unmounting.\n","isInternal":false,"tokens":1944,"sizeBytes":9707},{"name":"key-management.md","path":"packages/lexical-website/docs/concepts/key-management.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-website/docs/concepts/key-management.md","title":"Concepts Skill","category":"anthropic-skill","format":"markdown","content":"# Key Management\n\nKeys are a fundamental concept in Lexical that enable efficient state management and node tracking. Understanding how keys work is crucial for building reliable editor implementations.\n\n## What are Keys?\n\nThe `__key` property is a unique identifier assigned to each node in the Lexical editor. These keys are:\n- Automatically generated by Lexical\n- Used to track nodes in the editor state\n- Essential for state management and updates\n- Immutable during a node's lifecycle\n\n## When to Use `__key`?\n\n### ✅ Correct Usage\n\nKeys should ONLY be used in two specific situations:\n\n1. **In Node Constructors**\n```typescript\nclass MyCustomNode extends ElementNode {\n  constructor(someData: string, key?: NodeKey) {\n    super(key); // Correctly passing key to parent constructor\n    this.__someData = someData;\n  }\n}\n```\n\n2. **In Static Clone Methods**\n```typescript\nclass MyCustomNode extends ElementNode {\n  static clone(node: MyCustomNode): MyCustomNode {\n    return new MyCustomNode(node.__someData, node.__key);\n  }\n}\n```\n\n### ❌ Incorrect Usage\n\nNever use keys in these situations:\n\n```typescript\n// ❌ Don't pass keys between different nodes\nconst newNode = new MyCustomNode(existingNode.__key);\n\n// ❌ Don't manipulate keys directly\nnode.__key = 'custom-key';\n```\n\n## How Lexical Uses Keys\n\n### Diagram\n\nThe dotted outlines show nodes that are re-used in a zero-copy fashion from one EditorState to the next\n\n```mermaid\ngraph TB\n  subgraph s0[\"Initial State\"]\n    direction TB\n    m0[\"NodeMap (v0)\"]\n    m0 -->|Key A| n0a[\"A (v0)\"]\n    m0 -->|Key B| n0b[\"B (v0)\"]\n  end\n  subgraph s1[\"Create Node C\"]\n    direction TB\n    style n1a stroke-dasharray: 5 5\n    style n1b stroke-dasharray: 5 5\n    m1[\"NodeMap (v1)\"]\n    m1 -->|Key A| n1a[\"A (v0)\"]\n    m1 -->|Key B| n1b[\"B (v0)\"]\n    m1 -->|Key C| n1c[\"C (v0)\"]\n  end\n  subgraph s2[\"Update Node A\"]\n    direction TB\n    style n2a stroke-dasharray: 5 0\n    style n2b stroke-dasharray: 5 5\n    style n2c stroke-dasharray: 5 5\n    m2[\"NodeMap (v1)\"]\n    m2 -->|Key A| n2a[\"A (v1)\"]\n    m2 -->|Key B| n2b[\"B (v0)\"]\n    m2 -->|Key C| n2c[\"C (v0)\"]\n  end\n  s0 -.-> s1 -.-> s2\n```\n\n### Node Map Structure\n\nThe EditorState maintains a `Map<NodeKey, LexicalNode>` that tracks all nodes. Nodes refer to each other using keys in their internal pointers:\n\n```typescript\n// Internal node structure (not for direct usage)\n{\n  __prev: null | NodeKey,\n  __next: null | NodeKey,\n  __parent: null | NodeKey,\n  // __first, __last and __size are only for ElementNode to track its children\n  __first: null | NodeKey,\n  __last: null | NodeKey,\n  __size: number\n}\n```\n\nThese internal pointers maintain the tree structure and should never be manipulated directly.\n\n### Key-Related APIs\n\n1. **Editor Methods**\n   ```typescript\n   // Get node by key\n   const node = $getNodeByKey(key);\n\n   // Get the DOM element a node key is reconciled to\n   const element = editor.getElementByKey(key);\n\n   // Get latest version of a node\n   const latest = node.getLatest();\n   \n   // Get mutable version for updates\n   const mutable = node.getWritable();\n   ```\n\n### Key Lifecycle\n\nNodeKeys are ephemeral and have several important characteristics:\n\n1. **Serialization**\n   - Keys are never serialized\n   - New keys are generated when deserializing (from JSON/HTML)\n   - Keys are only meaningful within their EditorState instance\n\n2. **Uniqueness**\n   - Keys are unique within an EditorState\n   - Current implementation uses serial numbers for debugging\n   - Should be treated as random and opaque values\n   - Never logically reused\n\nKeys are used internally by Lexical to:\n1. Track nodes in the editor state\n2. Manage node updates and versions\n3. Maintain referential integrity\n4. Enable efficient state updates\n\n## Common Pitfalls\n\n1. **Key Reuse**\n   ```typescript\n   // ❌ Never do this\n   function duplicateNode(node: LexicalNode) {\n     return new SameNodeType(data, node.__key);\n   }\n   ```\n\n2. **Manual Key Assignment**\n   ```typescript\n   // ❌ Never do this\n   node.__key = generateCustomKey();\n   ```\n\n3. **Incorrect Constructor/Clone Implementation**\n   ```typescript\n   // ❌ Never do this - missing key in constructor\n   class MyCustomNode extends ElementNode {\n     constructor(someData: string) {\n       super(); // Missing key parameter\n       this.__someData = someData;\n     }\n   }\n\n   // ✅ Correct implementation\n   class MyCustomNode extends ElementNode {\n     __someData: string;\n\n     constructor(someData: string, key?: NodeKey) {\n       super(key);\n       this.__someData = someData;\n     }\n     \n     static clone(node: MyCustomNode): MyCustomNode {\n       return new MyCustomNode(node.__someData, node.__key);\n     }\n\n     afterCloneFrom(prevNode: this): void {\n       super.afterCloneFrom(prevNode);\n       this.__someData = prevNode.__someData;\n     }\n   }\n   ```\n\n4. **Node Replacement**\n   ```typescript\n   // ❌ Never re-use the key when changing the node class\n   const editorConfig = {\n     nodes: [\n       CustomNodeType,\n       {\n         replace: OriginalNodeType,\n         with: (node: OriginalNodeType) => new CustomNodeType(node.__key),\n         withKlass: CustomNodeType\n       }\n     ]\n   };\n\n   // ✅ Correct: Use node replacement configuration\n   const editorConfig = {\n     nodes: [\n       CustomNodeType,\n       {\n         replace: OriginalNodeType,\n         with: (node: OriginalNodeType) => new CustomNodeType(),\n         withKlass: CustomNodeType\n       }\n     ]\n   };\n   ```\n\n   For proper node replacement, see the [Node Replacement guide](node-replacement.md).\n\n## Best Practices\n\n1. **Let Lexical Handle Keys**\n   ```typescript\n   import {$applyNodeReplacement} from 'lexical';\n\n   // Create node helper function\n   export function $createMyCustomNode(data: string): MyCustomNode {\n     return $applyNodeReplacement(new MyCustomNode(data));\n   }\n   ```\n\n## Testing Considerations\n\nWhen writing tests involving node keys:\n\n```typescript\ntest('node creation', async () => {\n  await editor.update(() => {\n    // ✅ Correct: Create nodes normally\n    const node = new MyCustomNode(\"test\");\n    \n    // ✅ Correct: Keys are automatically handled\n    expect(node.__key).toBeDefined();\n    expect(node.__key).not.toBe('');\n  });\n});\n```\n\n## Performance Impact\n\nUnderstanding key management is crucial for performance:\n\n1. Keys enable efficient node lookup (O(1))\n2. Proper key usage prevents unnecessary re-renders\n3. Lexical's key system optimizes state updates\n4. Improper key manipulation can cause performance issues\n\n## Common Questions\n\n**Q: How do I reference a node later?**\nA: Store a reference to the node. Conventionally, all node methods will use `getLatest()` or `getWritable()` which will look up the latest version of that node before reading or writing its properties, which is equivalent to using the key but is type-safe (but may cause errors if you try to use a reference to a node that no longer exists). In some situations it may be preferable to use the key directly, which is also fine.\n\n**Q: How do I ensure unique nodes?**\nA: Let Lexical handle key generation and management. Focus on node content and structure. ","isInternal":false,"tokens":1792,"sizeBytes":7121},{"name":"keyboard-accessibility.md","path":"packages/lexical-website/docs/concepts/keyboard-accessibility.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-website/docs/concepts/keyboard-accessibility.md","title":"Concepts Skill","category":"anthropic-skill","format":"markdown","content":"# Keyboard Accessibility\n\nLexical follows the [WAI-ARIA Authoring Practices](https://www.w3.org/WAI/ARIA/apg/) keyboard model. This page documents the contracts that plugin authors and host applications rely on, so a Lexical editor can sit inside a larger keyboard-accessible flow without trapping or surprising the user.\n\n## Tab key\n\nBy default, `Tab` inside the editor follows the browser's normal focus order — it moves to the next focusable element on the page. There is no keyboard trap.\n\nThe optional `TabIndentationExtension` (`@lexical/extension`) takes over `Tab` and uses it for block indent / outdent. Hosts that mount this extension opt in to that behavior. WCAG 2.1.2 (No Keyboard Trap) still applies — pressing `Escape` blurs the editor (see below), at which point `Tab` leaves the editor again.\n\n## Escape key\n\nPressing `Escape` while the editor has focus calls `editor.blur()` via the rich-text Escape command handler (registered at `COMMAND_PRIORITY_EDITOR`). The handler only blurs when the current selection is a range selection; with a `NodeSelection` (or no selection) it returns `false` and falls through without blurring. After blur, the next `Tab` advances to the page's next focusable element — so the editor is not a hard keyboard trap. (`TabIndentationExtension` consumes `Tab` only while a range selection is active, and its own source notes that taking over `Tab` is generally discouraged for keyboard accessibility, so treat WCAG 2.1.2 conformance as something to verify in your integration rather than a guarantee.)\n\nPlugin authors who want to intercept `Escape` (modal close, dropdown close, etc.) should register their handler at a higher priority and return `true` to stop the command chain. Common patterns already established in the codebase:\n\n| Priority | Caller | Purpose |\n| --- | --- | --- |\n| `HIGH` | Floating link editor | Close the floating UI, keep editor focus |\n| `HIGH` | Equation component (while editing) | Close the equation editor input |\n| `NORMAL` | Comment plugin | Cancel inline comment editing |\n| `LOW` | Image component | Escape out of caption / resize editing: drop the inner selection and restore the `NodeSelection` on the image decorator |\n| `EDITOR` | Rich text (default) | `editor.blur()` (range selection only) |\n\nHandlers that return `false` let the chain continue, so Escape can still fall through to the default blur even after a higher-priority handler runs.\n\nSome host UI binds its close handler directly to a DOM `keydown` listener rather than going through `KEY_ESCAPE_COMMAND` — the playground's `Modal` via `window.addEventListener('keydown', ...)` and its `DropDown` via a React `onKeyDown` on the menu. Those paths run alongside the table above; they do not participate in the editor's command priority chain.\n\n## React hooks and `@lexical/a11y` extensions\n\nThe keyboard helpers below ship as `@lexical/a11y` _extensions_ — the extension is the API and does all of the work (event listeners, focus management, the live region). The `@lexical/react` hooks (`useLexicalFocusManagerRef`, `useLexicalRovingTabIndexRef`, `useLexicalFocusTrapRef`, `useLexicalAriaLiveRegion`) are thin React adapters that only _communicate_ with their extension: each returns a `RefCallback` (or an `announce` function) that drives the extension's signals. Using a hook therefore **requires** its matching extension to be registered in the editor's extension tree — it is not an optional integration path. Non-React hosts (Svelte, Vue, vanilla DOM) use the extensions directly and skip the hooks.\n\n## Toolbar focus jump\n\nThe playground uses `useLexicalFocusManagerRef` (`@lexical/react/useLexicalFocusManagerRef`) to provide a documented shortcut for moving focus from the editor to the toolbar without tabbing through the page. The hook returns a `RefCallback` that registers the attached element as a focus-managed toolbar. It listens for `Alt+F10` inside the editor and moves focus into the toolbar — preferring the item that currently carries `tabindex=\"0\"` (the active roving item, when paired with `useLexicalRovingTabIndexRef`), and otherwise the first focusable item. While focus is in the toolbar, pressing `Escape` restores both focus and the editor's prior selection via `editor.focus()`. Multiple toolbars can be registered simultaneously. Requires `FocusManagerExtension` from `@lexical/a11y` in the editor's extension tree.\n\nThis matches the WAI-ARIA editor menubar pattern and the convention used by Word and CKEditor. Hosts that don't mount `useLexicalFocusManagerRef` get the default browser flow (Tab in, Tab out).\n\n## Toolbar arrow-key navigation\n\nToolbars that opt into `useLexicalRovingTabIndexRef` (`@lexical/react/useLexicalRovingTabIndexRef`) collapse to a single tab stop: arrow keys move between items inside the toolbar; `Tab` moves past the toolbar as a whole. Items keep `tabindex=\"-1\"` except the active one (`tabindex=\"0\"`). The hook returns a `RefCallback` — multiple containers can be registered simultaneously. Tracks the WAI-ARIA toolbar pattern. Requires `RovingTabIndexExtension` from `@lexical/a11y` in the editor's extension tree.\n\n## Modal focus trap\n\n`useLexicalFocusTrapRef` (`@lexical/react/useLexicalFocusTrapRef`) returns a `RefCallback` that cycles `Tab` / `Shift+Tab` inside a modal container and restores focus to the previously-focused element on deactivation. While active, the trap also installs a document-level `focusin` listener that pulls any focus landing outside the container back to the first focusable descendant (or the container itself) — so it recovers from the browser routing `Tab` out to its own chrome. Although several containers can be _registered_, only one trap should be _active_ at a time: concurrent traps install competing `focusin` listeners and fight over focus. Escape is **not** intercepted by the hook — modal owners handle the close key themselves, so the editor's Escape contract above stays predictable. Requires `FocusTrapExtension` from `@lexical/a11y` in the editor's extension tree.\n\n## Screen reader announcements\n\n`useLexicalAriaLiveRegion` (`@lexical/react/useLexicalAriaLiveRegion`) returns a stable `announce(message)` function. The hook itself mounts no DOM — like the others above, it is a thin adapter that **requires `AriaLiveRegionExtension` from `@lexical/a11y` in the editor's extension tree**. That extension owns the lifecycle of the visually-hidden region (`role=\"status\"`, `aria-atomic`, `aria-live` defaulting to `polite` — configurable via its `politeness` config): the region is created in the extension's `register` step and disposed on the editor's teardown, not on the hook's unmount. `EditorModeAnnounceExtension` (mode transitions via `registerEditableListener`) and `HistoryAnnounceExtension` (undo / redo via `UNDO_COMMAND` / `REDO_COMMAND`) announce through the same region. Both extensions accept optional message overrides for i18n.\n\n## Component ARIA quick reference\n\nWhere Lexical already applies ARIA attributes today (set in each component's `createDOM` / `updateDOM`, declaratively in the surrounding React tree, or applied imperatively via `setAttribute` on a managed or portaled element — as the typeahead menu and the live region do):\n\n| Component | role | aria-\\* attributes |\n| --- | --- | --- |\n| `ToolbarPlugin` (playground) | `toolbar` | `aria-label` |\n| `Modal` (playground) | `dialog` | `aria-modal`, `aria-labelledby` |\n| `LexicalListItemNode` (check list leaf) | `checkbox` | `aria-checked` |\n| `LexicalMenu` typeahead popup | `listbox` | `aria-label` |\n| Editor root (while typeahead is open) | — | `aria-controls`, `aria-activedescendant` |\n| `EquationNode` (playground) | `math` | `aria-label` |\n| `useLexicalAriaLiveRegion` region | `status` | `aria-live`, `aria-atomic` |\n\nCustom nodes follow the same pattern: set the attributes in `createDOM` and re-apply in `updateDOM` when the state they reflect changes. There is no central ARIA contract on `LexicalNode`; treat each subclass's `createDOM` as the source of truth.\n\n## Browser notes\n\nLexical relies on the standard DOM `Escape` contract — verified on Chrome and Safari (macOS, normal window). Safari in fullscreen intercepts `Escape` at the browser level to exit fullscreen; that is the browser's contract, not Lexical's.\n\n## What this page does not cover\n\n- Touch / mobile screen reader gestures. The hooks above share the same DOM contracts with mobile (TalkBack, VoiceOver iOS) but mobile has not been exercised in this set of changes.\n- High contrast / forced-colors theming. See the playground's `index.css` `@media (forced-colors: active)` block for a baseline.\n","isInternal":false,"tokens":1980,"sizeBytes":8645},{"name":"listeners.md","path":"packages/lexical-website/docs/concepts/listeners.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-website/docs/concepts/listeners.md","title":"Concepts Skill","category":"anthropic-skill","format":"markdown","content":"\n\n# Listeners\n\nListeners are a mechanism that lets the Editor instance inform the user when a certain operation has occurred. All listeners follow a reactive pattern where you can do an operation upon something happening in the future. All listeners also return a function that easily allows for the\nlistener to be unregistered. Below are the different listeners that Lexical supports today:\n\n## `registerUpdateListener`\n\nGet notified when Lexical commits an update to the DOM.\n\n```js\nconst removeUpdateListener = editor.registerUpdateListener(({editorState}) => {\n  // The latest EditorState can be found as `editorState`.\n  // To read the contents of the EditorState, use the following API:\n\n  editorState.read(() => {\n    // Just like editor.update(), .read() expects a closure where you can use\n    // the $ prefixed helper functions.\n  });\n});\n\n// Do not forget to unregister the listener when no longer needed!\nremoveUpdateListener();\n```\n\nThe update listener callbacks receives a single argument containing the follow properties:\n\n- `editorState` the latest updated Editor State\n- `prevEditorState` the previous Editor State\n- `tags` a Set of all tags that were passed to the update\n- `dirtyElements` a Map of the NodeKeys of ElementNodes that were marked\n  dirty, to a boolean that is true when the node was intentionally mutated\n- `dirtyLeaves` a Set of the NodeKeys of all dirty nodes that are not\n  ElementNodes\n- `mutatedNodes` a Map of LexicalNode constructors to a\n  `Map<NodeKey, NodeMutation>`, or null when no DOM was mutated. Only\n  populated when at least one MutationListener is registered\n- `normalizedNodes` (advanced) the keys of TextNode descendants that were\n  merged with a sibling by normalization\n\nOne thing to be aware of is \"waterfall\" updates. This is where you might schedule an update inside an update\nlistener, as shown below:\n\n```js\neditor.registerUpdateListener(({editorState}) => {\n  // Read the editorState and maybe get some value.\n  editorState.read(() => {\n    // ...\n  });\n\n  // Then schedule another update.\n  editor.update(() => {\n    // ...\n  });\n});\n```\n\nThe problem with this pattern is that it means we end up doing two DOM updates, when we likely could have\ndone it in a single DOM update. This can have an impact on performance, which is important in a text editor.\n\nA listener that re-enqueues work without a stop condition is worse than slow.\nThe editor keeps a cascade budget: once a run of updates re-enqueues more\nthan 100 times without yielding to the event loop, it discards the entire\npending update queue and warns. Bounded activity never reaches the limit —\nthe budget is reset from a macrotask, which a genuine runaway starves — but a\ntrue loop silently loses whatever else was queued.\n\nTo avoid this, we recommend looking into [Node Transforms](transforms.md), which allow you to listen to node changes and\ntransform them as part of the same given update, meaning no waterfalls!\n\n## `registerTextContentListener`\n\nGet notified when Lexical commits an update to the DOM and the text content of the editor changes from\nthe previous state of the editor. If the text content is the same between updates, no notifications to\nthe listeners will happen.\n\n```js\nconst removeTextContentListener = editor.registerTextContentListener(\n  (textContent) => {\n    // The latest text content of the editor!\n    console.log(textContent);\n  },\n);\n\n// Do not forget to unregister the listener when no longer needed!\nremoveTextContentListener();\n```\n\n## `registerMutationListener`\n\nGet notified when a specific type of Lexical node has been mutated. There are three states of mutation:\n\n- `created`\n- `destroyed`\n- `updated`\n\nMutation listeners are great for tracking the lifecycle of specific types of node. They can be used to\nhandle external UI state and UI features relating to specific types of node.\n\nIf any existing nodes are in the DOM, and skipInitialization is not true, the listener\nwill be called immediately with an updateTag of 'registerMutationListener' where all\nnodes have the 'created' NodeMutation. This can be controlled with the skipInitialization option,\nwhich defaults to false.\n\n```js\nconst removeMutationListener = editor.registerMutationListener(\n  MyCustomNode,\n  (mutatedNodes, { updateTags, dirtyLeaves, prevEditorState }) => {\n    // mutatedNodes is a Map where each key is the NodeKey, and the value is the state of mutation.\n    for (let [nodeKey, mutation] of mutatedNodes) {\n      console.log(nodeKey, mutation)\n    }\n  },\n  {skipInitialization: false}\n);\n\n// Do not forget to unregister the listener when no longer needed!\nremoveMutationListener();\n```\n\n## `registerEditableListener`\n\nGet notified when the editor's mode has changed. The editor's mode can be changed\nvia `editor.setEditable(boolean)`.\n\n```js\nconst removeEditableListener = editor.registerEditableListener(\n  (editable) => {\n    // The editor's mode is passed in!\n    console.log(editable);\n  },\n);\n\n// Do not forget to unregister the listener when no longer needed!\nremoveEditableListener();\n```\n\n## `registerDecoratorListener`\n\nGet notified when the editor's decorator object changes. The decorator object contains\nall `DecoratorNode` keys -> their decorated value. This is primarily used with external\nUI frameworks.\n\n```js\nconst removeDecoratorListener = editor.registerDecoratorListener(\n  (decorators) => {\n    // The editor's decorators object is passed in!\n    console.log(decorators);\n  },\n);\n\n// Do not forget to unregister the listener when no longer needed!\nremoveDecoratorListener();\n```\n\n## `registerRootListener`\n\nGet notified when the editor's root DOM element (the content editable Lexical attaches to) changes. This is primarily used to\nattach event listeners to the root element. *The root listener function is executed directly upon registration and then on any subsequent update.*\n\n```js\nconst removeRootListener = editor.registerRootListener(\n  (rootElement, prevRootElement) => {\n   //add listeners to the new root element\n   //remove listeners from the old root element\n  },\n);\n\n// Do not forget to unregister the listener when no longer needed!\nremoveRootListener();\n```\n","isInternal":false,"tokens":1320,"sizeBytes":6143},{"name":"named-slots.md","path":"packages/lexical-website/docs/concepts/named-slots.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-website/docs/concepts/named-slots.md","title":"Concepts Skill","category":"anthropic-skill","format":"markdown","content":"# Named Slots\n\n:::warning\n\nNamed slots are experimental. The APIs described here are tagged\n`@experimental`, may change without a major version bump, and the\nserialization and collab formats should be treated as unstable until the\nfeature is stabilized.\n\n:::\n\nNamed slots let a single host node (an ElementNode or a DecoratorNode) own\nseveral isolated editable regions addressed by name — a Card's `title`, a\nPullQuote's `quote` and `attribution` — inside the host's own EditorState.\nEach region takes its own caret and formatting, never merges across the\nboundary, and `Cmd+A` doesn't spill into the rest of the document.\n\nBefore named slots, the usual answer was a nested editor per region: every\nregion lives in its own EditorState, so moving nodes between regions and\nkeeping history and collab in sync all require serialization and extra\n`editor.update` passes. The other node shapes don't fit either: plain\nElementNode children share one undivided linked list (Backspace at a region\nstart merges it into the previous region), and a DecoratorNode is atomic, so\nLexical can't own selection, collab, or serialization inside it. With named\nslots, editing a slot is just editing the one tree.\n\n:::note\n\nNamed slots are the model-level generalization of the rendering concept\nbehind [`$getDOMSlot`](/docs/api/modules/lexical#getdomslot) /\n`ElementDOMSlot` in the\n[DOM rendering](../serialization/dom-render.md) documentation. Every\nElementNode already has one _unnamed_ children channel, and `$getDOMSlot`\ncontrols where that channel's content attaches in the node's DOM. Named\nslots are additional, explicitly-named channels alongside it — symmetric in\nthat each renders into a controllable location in the host's DOM — but they\nalso carry the model-level semantics the unnamed channel doesn't: isolation\n(the virtual shadow root below), a separate NodeKey map, and their own\nserialization and collaboration.\n\n:::\n\n## The Model\n\nA host keeps a second child channel, a `Map` of slot name to NodeKey,\nseparate from its ordinary linked-list children. A slot value has its slot\nhost pointer set and `getParent() === null`, with exactly one of the two\nnon-null: `getParent()` stops at the slot boundary, and you climb out only\nthrough [`$getSlotHost()`](/docs/api/modules/lexical#getslothost).\n\nThe slot link itself acts as a **virtual invisible shadow root** between the\nhost and the value. Isolation is structural rather than a convention — an\naccidental boundary crossing surfaces as a thrown invariant, not silent\ncorruption.\n\nIn the DOM, each value renders synchronously into a keyless\n`<div data-lexical-slot=\"<name>\">` container parked slots-first in the host\nDOM as a **hidden placeholder** (`display: none`), in a canonical order\nderived from the host class (see [slot order](#slot-order)). Nothing is\nvisible until the host explicitly attaches the container somewhere (see\n[Rendering](#rendering)) — mirroring how\n[`getDOMSlot`](/docs/api/modules/lexical#getdomslot) gives an element\ncontrol over where its linked-list children render.\n\n## Hosts and Values\n\nAny ElementNode or DecoratorNode can host slots. An ElementNode host can mix\nslots with ordinary children (a Card with a `title` slot plus regular body\nparagraphs); a DecoratorNode host has no children channel, so slots are its\nonly editable content (a PullQuote that is otherwise atomic).\n\nA slot value is any non-inline ElementNode or DecoratorNode. The value's\nshape decides the editing model:\n\n- **A bare block** (for example a ParagraphNode) behaves as a single-line\n  field with no extra wrapper in the model, the JSON, or the DOM: its virtual\n  scope holds exactly one block, so Enter is a no-op (hosts may map it to\n  focus movement) and a multi-block paste flattens to inline content the way\n  an `<input>` sanitizes its value — line breaks stripped, block-only\n  decorators dropped.\n- **A shadow-root container** (an ElementNode whose\n  [`isShadowRoot()`](/docs/api/modules/lexical#isshadowroot) returns\n  `true`) behaves as a multi-block region with normal block editing inside.\n\n## Declaring and Setting Slots\n\nA host class declares its slot names in [`$config()`](nodes.mdx); values are\nattached with [`$setSlot`](/docs/api/modules/lexical#setslot):\n\n```ts\nimport {\n  $create,\n  $createParagraphNode,\n  $setSlot,\n  ElementNode,\n} from 'lexical';\n\nclass CardNode extends ElementNode {\n  $config() {\n    return this.config('card', {extends: ElementNode, slots: ['title']});\n  }\n  createDOM(): HTMLElement {\n    return document.createElement('div');\n  }\n  updateDOM(): boolean {\n    return false;\n  }\n}\n\nfunction $createCardNode(): CardNode {\n  const card = $create(CardNode);\n  // Single-line title: the bare paragraph IS the slot value. An empty\n  // paragraph is an empty field; to seed default text, append a non-empty\n  // TextNode (empty TextNodes are eliminated during reconciliation).\n  $setSlot(card, 'title', $createParagraphNode());\n  // Ordinary body child, edited like any other block.\n  return card.append($createParagraphNode());\n}\n```\n\nFor a multi-block region, use a shadow-root container as the value:\n\n```ts\nclass SlotContainerNode extends ElementNode {\n  $config() {\n    return this.config('slot-container', {extends: ElementNode});\n  }\n  createDOM(): HTMLElement {\n    return document.createElement('div');\n  }\n  updateDOM(): boolean {\n    return false;\n  }\n  isShadowRoot(): boolean {\n    return true;\n  }\n}\n\n$setSlot(\n  pullQuote,\n  'quote',\n  $create(SlotContainerNode).append(\n    $createParagraphNode().append($createTextNode('First block')),\n    $createParagraphNode().append($createTextNode('Second block')),\n  ),\n);\n```\n\nThe core API surface (all exported from `lexical`):\n\n- [`$setSlot(host, name, node)`](/docs/api/modules/lexical#setslot) — place a value into a named slot, replacing\n  any existing value under that name. Move semantics, mirroring `append`:\n  the value is detached from any current parent or slot first. Throws if the\n  value is inline or if the name is one of the reserved prototype keys\n  (`__proto__`, `constructor`, `prototype`). Slotting a node into its own\n  descendant would form a cycle; this is checked (and throws) in development\n  only — in production it behaves like appending an ancestor through the\n  children channel, which is likewise unguarded.\n- [`$getSlot(host, name)`](/docs/api/modules/lexical#getslot) — the value under a name, or `null`.\n- [`$getSlotNames(host)`](/docs/api/modules/lexical#getslotnames) — the host's slot names in canonical order.\n- [`$removeSlot(host, name)`](/docs/api/modules/lexical#removeslot) — detach the value under a name (the subtree is\n  garbage-collected unless you reattach it elsewhere).\n- [`$getSlotHost(node)`](/docs/api/modules/lexical#getslothost) — the host a value is slotted into, or `null`.\n- [`$getSlotNameWithinHost(node)`](/docs/api/modules/lexical#getslotnamewithinhost) — the name of the slot\n  a value occupies on its host (the reverse direction of `$getSlotHost`), or\n  `null` when the node is not a slot value.\n- [`$getSlotFrame(node)`](/docs/api/modules/lexical#getslotframe) — the innermost slot value containing a node (the\n  \"frame\" whose virtual shadow root scopes editing), or `null` when the node\n  is not inside any slot.\n- [`$getSelectionSlotFrame(selection)`](/docs/api/modules/lexical#getselectionslotframe) — the slot frame a\n  selection lives in, or `null` when it is outside any slot. Selection-driven\n  exporters walk this frame instead of the root's children, because a\n  selection inside a slot never contains its host and a root walk would\n  export nothing. Works for every selection type, not just `RangeSelection`.\n- [`$isSlotHost(node)`](/docs/api/modules/lexical#isslothost) /\n  [`$isSlotChild(node)`](/docs/api/modules/lexical#isslotchild) — type guards\n  for the [`SlotHostNode`](/docs/api/modules/lexical#slothostnode) /\n  [`SlotChildNode`](/docs/api/modules/lexical#slotchildnode) interfaces.\n\n## Slot Order\n\nSlot order is canonical and derived, never stored. Names declared in\n`$config()` (`slots: ['quote', 'attribution']`) sort first in declaration\norder; undeclared names sort after them, lexicographically by UTF-16 code\nunit (plain JavaScript string comparison, locale-independent). `$setSlot`\nre-canonicalizes on every write, so documents normalize on load and\nconcurrent collaborative additions converge to the same order on every\nclient. If presentation order matters, declare the names.\n\n## Rendering\n\nThe reconciler always renders every slot subtree synchronously, but into a\nhidden placeholder container — visibility is the host's explicit decision.\nThere are three ways to attach a slot, all sharing the same contract\n(attaching moves the container to the target, a no-op when it is already\nthere, and reveals it; the container renders as a normal block):\n\n1. **Synchronously in-lexical**: register a `$getSlotTargetElement`\n   [`DOMRenderMatch`](/docs/api/modules/lexical_html#domrendermatch)\n   override for the host's node class (a\n   [DOM render override](../serialization/dom-render.md), not a node\n   method — this is an advanced hook). The reconciler consults it whenever\n   it creates or reconciles the slot's container and attaches/reveals\n   within the same commit — no listener or framework hop. Returning\n   `hostDom` reveals the slot in its default slots-first position:\n\n   ```ts\n   import {domOverride, DOMRenderExtension} from '@lexical/html';\n   import {configExtension, defineExtension} from 'lexical';\n\n   export const CardExtension = defineExtension({\n     dependencies: [\n       configExtension(DOMRenderExtension, {\n         overrides: [\n           domOverride([CardNode], {\n             // Reveal the title in its default slots-first position within\n             // the same commit that renders it. Returning an element from\n             // deeper inside the host's DOM attaches it there instead;\n             // $next() defers to lower-priority overrides (default: null,\n             // a hidden placeholder).\n             $getSlotTargetElement: (node, slotName, hostDom, $next, editor) =>\n               hostDom,\n           }),\n         ],\n       }),\n     ],\n     name: 'card',\n     nodes: [CardNode],\n   });\n   ```\n\n2. **Imperatively**:\n   [`mountSlotContainer(editor, nodeKey, slotName, target)`](/docs/api/modules/lexical#mountslotcontainer)\n   and\n   [`unmountSlotContainer(editor, nodeKey, container)`](/docs/api/modules/lexical#unmountslotcontainer)\n   from `lexical` are the framework-independent primitives (e.g. from a\n   [mutation listener](/docs/api/modules/lexical#registermutationlistener),\n   which fires after the commit). `mountSlotContainer` resolves the container\n   against the committed editor state\n   ([`editor.getEditorState()`](/docs/api/modules/lexical#geteditorstate)), so\n   the model it reads matches the reconciled DOM it reveals;\n   `unmountSlotContainer` takes the container you already hold and only touches\n   the DOM:\n\n   ```ts\n   import {mountSlotContainer} from 'lexical';\n\n   // e.g. inside an extension's register(editor):\n   const unregister = editor.registerMutationListener(\n     CardNode,\n     (mutations) => {\n       for (const [nodeKey, mutation] of mutations) {\n         if (mutation === 'destroyed') {\n           continue;\n         }\n         const hostDom = editor.getElementByKey(nodeKey);\n         if (hostDom !== null) {\n           // Mounting in place: the placeholder is already parked in the\n           // host DOM, so this just reveals it in its slots-first position.\n           // Any element within the host's DOM works as a target.\n           mountSlotContainer(editor, nodeKey, 'title', hostDom);\n         }\n       }\n     },\n     {skipInitialization: false},\n   );\n   ```\n\n   `unmountSlotContainer(editor, nodeKey, container)` is the reverse: it\n   hides the container and parks it back in the host DOM, for when a mount\n   target goes away while the host remains.\n\n3. **From React chrome**: the\n   [`useLexicalSlotRef`](/docs/api/modules/lexical_react_useLexicalSlotRef#uselexicalslotref)\n   hook from `@lexical/react/useLexicalSlotRef` wraps the imperative pair and returns a\n   ref that mounts a slot's container into your component — the usual choice\n   for a DecoratorNode host's `decorate()` chrome (whose containers are\n   opted back into `contentEditable` automatically, since the decorator DOM\n   is non-editable):\n\n```tsx\nimport {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';\nimport {useLexicalSlotRef} from '@lexical/react/useLexicalSlotRef';\n\nfunction PullQuoteComponent({nodeKey}: {nodeKey: NodeKey}) {\n  const [editor] = useLexicalComposerContext();\n  const quoteRef = useLexicalSlotRef<HTMLDivElement>(editor, nodeKey, 'quote');\n  const attributionRef = useLexicalSlotRef<HTMLDivElement>(\n    editor,\n    nodeKey,\n    'attribution',\n  );\n  return (\n    <blockquote>\n      <div ref={quoteRef} />\n      <div ref={attributionRef} />\n    </blockquote>\n  );\n}\n```\n\nA `contentEditable=false` ElementNode shell can host React chrome the same\nway (slot containers opt into editing whenever the host DOM is\nnon-editable): the playground's Review demo portals chrome into the host DOM,\nattaches the author slot with `useLexicalSlotRef`, drives an interactive\nstar-rating widget that persists to NodeState, and applies the identical\nhidden-then-attach technique to its `getDOMSlot` children element. Such a shell should call\n[`setDOMUnmanaged(dom)`](/docs/api/modules/lexical#setdomunmanaged) in\n`createDOM` — the portal and the\nattach moves mutate the shell's children from outside the reconciler, and\nthe marker gives the shell the same mutation-observer exemption a\nDecoratorNode's DOM has.\n\n## Editable state\n\nA slot rendered inside a non-editable host (a DecoratorNode, or a\n`contentEditable=false` element shell) would not track the editor's editable\nstate on its own, so the reconciler gives its container an explicit\n`contentEditable` that follows `editor.isEditable()`, and re-renders the islands\nwhenever `setEditable` toggles — so a read-only editor's slots are not left\neditable. No extension is required. Slots always follow the editor; there is\ncurrently no way for a slot to override its own editable state.\n\nA host that attaches its own editable island which is *not* a slot container —\nfor example the Review demo's `getDOMSlot` children element inside its\n`contentEditable=false` shell — opts it into the same behavior with\n[`$markSlotEditable(element, editor)`](/docs/api/modules/lexical#marksloteditable),\nre-applying it from `updateDOM` so an editable toggle reaches the island.\n\n## Editing Behavior\n\n- **Selection never crosses a slot boundary.** Selections are clamped to the\n  anchor's slot frame at every entry point (DOM resolution,\n  [`$setSelection`](/docs/api/modules/lexical#setselection),\n  and point mutation), so a mouse drag and a `shift+arrow` across the\n  boundary land on the same clamped result. A focus in a slot of a host is\n  moved past the whole host, or to the edge of the host's own children when\n  the selection starts inside the host.\n- **Deletion stops at the boundary.** Backspace at the start of a slot and\n  forward-Delete at its end are no-ops instead of merging across the virtual\n  shadow root.\n- **`Cmd+A` scopes to the slot frame** when the caret is inside one; outside\n  slots the default handlers keep the legacy whole-document behavior.\n  Progressive expansion (block → enclosing slot frame → document on repeated\n  presses) is provided by the opt-in\n  [`SelectBlockExtension`](/docs/api/modules/lexical_extension#selectblockextension)\n  from `@lexical/extension`.\n- **Generic block conversion skips slot values.** A slot value has no\n  parent — its up-link is `$getSlotHost` — so selection-driven block\n  converters (`$setBlocksType`, `$wrapNodes`, `$insertList` / `$removeList`,\n  the markdown block shortcuts) treat it as ineligible and do nothing rather\n  than replace it: the slot assignment is managed by the node or extension\n  that owns the slot. Calling `LexicalNode.replace` directly on a slot value\n  throws; reassign the slot with `$setSlot` on its host instead.\n- **Slots stay bound to their host through `replace`.** Replacing a slot\n  *host* (`host.replace(other)`) does not transfer its slots to the\n  replacement — slots are not necessarily portable across node types, so the\n  slot map travels with the node, never with its position. If the replaced\n  host is reattached in the same update (the\n  [`$wrapNodeInElement`](/docs/api/modules/lexical_utils#wrapnodeinelement)\n  pattern) it keeps its slots; if it stays detached, its slot subtrees are\n  garbage-collected with it. The same applies to anything built on `replace`:\n  converting a slot host with `$setBlocksType` produces a slot-less\n  replacement block. Move a slot onto another host explicitly with\n  `$setSlot`.\n- **A NodeSelection of an element carries its children.** Copy and export\n  of a whole-host NodeSelection (e.g. a chrome click that selects \"the whole\n  Card\") include the host's body children even though they aren't in the\n  selection themselves — the old shell-only output made cut silently lossy.\n  This applies only to NodeSelection; a partial RangeSelection over the host\n  keeps per-child slicing.\n\n### Traversal is intentionally asymmetric\n\nContent reads include slot subtrees, slots-first:\n[`getTextContent()`](/docs/api/modules/lexical#gettextcontent),\n[`getAllTextNodes()`](/docs/api/modules/lexical#getalltextnodes),\nand the [`$dfsWithSlots`](/docs/api/modules/lexical_utils#dfswithslots)\nfamily in `@lexical/utils` count slot content for search, copy, and\naccessibility. Navigation excludes them:\n[`getChildren()`](/docs/api/modules/lexical#getchildren),\n[`getFirstDescendant()`](/docs/api/modules/lexical#getfirstdescendant)\nand friends stay linked-list-only, so caret movement never walks into a slot\nby accident. Choose [`$dfs`](/docs/api/modules/lexical_utils#dfs) or\n[`$dfsWithSlots`](/docs/api/modules/lexical_utils#dfswithslots) depending on whether \"this subtree\" should mean the\nnavigable tree or all content.\n\n## Serialization\n\nJSON serialization is automatic in both directions. A host's slots serialize\nunder the reserved `$slots` key on\n[`SerializedLexicalNode`](/docs/api/modules/lexical#serializedlexicalnode)\n(a sibling of\nNodeState's reserved `'$'` key), keyed by slot name:\n\n```json\n{\n  \"type\": \"card\",\n  \"version\": 1,\n  \"$slots\": {\n    \"title\": {\"type\": \"paragraph\", \"children\": [], \"version\": 1}\n  },\n  \"children\": []\n}\n```\n\nParsing re-attaches each subtree with `$setSlot` and throws if `$slots`\nappears on a node that cannot host slots. The `$` prefix keeps the\nframework-owned key from colliding with a subclass that already serializes a\n`slots` property of its own.\n\nHTML serialization is opt-in per host, like NodeState: the exporter never\ndescends into slots on its own. A host's `exportDOM` can emit each slot into\na wrapper using\n[`$appendNodeToHTML`](/docs/api/modules/lexical_html#appendnodetohtml) from\n`@lexical/html`, and a\n[DOM import rule](../serialization/dom-import.md) on the host's\ndistinguishing markup maps the wrappers back through `$setSlot`.\n\n## Collaboration\n\nSlots sync on both the V1 and V2 Yjs bindings through a per-slot-diffed\n`Y.Map` stored under the reserved `__slots` attribute key on the host's shared\ntype (the channel reuses the host's `__slots` field name, which is already\nexcluded from the property sync). Hosts with declared slots create that map\neagerly, so two clients\nconcurrently setting *different* slot names for the first time merge\nper-entry instead of racing. Malformed or hostile remote entries are\nvalidated and skipped.\n\n:::caution\n\nPre-slots clients receiving slot data from upgraded peers will error rather\nthan render it. Enabling slots in long-lived shared documents should be\ngated on all participants running a slots-aware version; new clients\ntolerate unknown slot data going forward.\n\n:::\n\n## Reserved Names\n\nAdding slots reserves a few identifiers that custom node subclasses should\nnot define for their own purposes:\n\n- the `__slots` and `__slotHost` fields on ElementNode / DecoratorNode — the\n  `__slots` name is also the collab attribute key for the slots channel, so it\n  is reserved on the Yjs shared type too;\n- the `$slots` serialized JSON key.\n\n## Current Limitations\n\n- The caret / NodeCaret APIs throw across a slot boundary (\"no common\n  ancestor\"); slot-aware caret traversal is a planned follow-up.\n- Nested slots (a slot whose host is itself slotted) are only handled one\n  level deep by the runtime selection comparator.\n- The mixed-version collab caveat above.\n","isInternal":false,"tokens":4854,"sizeBytes":20630},{"name":"node-cloning.md","path":"packages/lexical-website/docs/concepts/node-cloning.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-website/docs/concepts/node-cloning.md","title":"Concepts Skill","category":"anthropic-skill","format":"markdown","content":"# Node Cloning\n\nUnderstanding how to properly clone and modify nodes is crucial for working with Lexical. This guide explains the different cloning mechanisms and when to use each one.\n\n## Types of Node Cloning\n\nLexical provides several ways to clone nodes, each serving a different purpose:\n\n1. `static clone()` - Internal API for state management\n2. `$copyNode()` - Public API for creating new nodes\n3. `getWritable()` - Public API for node modifications\n\n## The `clone` Method\n\n### What is `clone`?\n\nThe `clone` method is a static method that creates the next version of a node. A node that declares [`$config()`](./nodes.mdx) has one synthesized for it and need not implement it — a hand-written `clone` is only required for a node that does not use `$config()`, or whose constructor has required arguments. Either way, it's important to understand that this is an **internal API** used by Lexical's state management system.\n\n```typescript\nclass MyCustomNode extends ElementNode {\n  static clone(node: MyCustomNode): MyCustomNode {\n    // ✅ Correct implementation\n    return new MyCustomNode(node.__someData, node.__key);\n  }\n}\n```\n\n### When is `clone` Used?\n\n1. **Internal State Management**\n   - Used by Lexical internally\n   - Part of the editor's state update system\n   - Called by `getWritable()`\n\n2. **Node Mutations**\n   ```typescript\n   // Example of a node method implementation\n   class MyCustomNode extends ElementNode {\n     setData(data: string): this {\n       const self = this.getWritable();\n       self.__data = data;\n       return self;\n     }\n   }\n\n   node.setData(\"new data\");\n   ```\n\n### When NOT to Use `clone`?\n\n```typescript\n// ❌ Never do this\nfunction $duplicateNode(node: MyCustomNode) {\n  return MyCustomNode.clone(node); // Don't call clone directly\n}\n```\n\n## The `afterCloneFrom` Method\n\n### What is `afterCloneFrom`?\n\n`afterCloneFrom(prevNode)` runs on every clone, right after it is constructed,\nand copies over whatever the constructor did not. The base implementation\ncarries the node's links to its parent and siblings and its\n[NodeState](./node-state.md), and `ElementNode` and `TextNode` carry their own\nbuilt-in properties such as format, style, and indent. A property that is\ncopied neither by the constructor nor by some `afterCloneFrom` silently\nreverts to its default on the node's next write.\n\n### When you don't need it\n\nWith the modern APIs you usually don't write one at all:\n\n- **Serialization schema.** A property declared as a field in a `$config`\n  [`json` schema](./nodes.mdx#creating-custom-nodes-with-a-serialization-schema)\n  (`withField(stringValue(), {field: '__label'})`) is carried across the\n  clone for you, because the schema says where it is stored. Available in\n  Lexical v0.51.0 and later.\n- **NodeState.** State created with `createState` and stored with `$setState`\n  is carried by the base `afterCloneFrom`, so a node whose extra data lives\n  only in NodeState needs nothing.\n\n```ts\n// At module scope, above the class, so TypeScript checks it against the class\nconst calloutSchema = nodeSchema<CalloutNode>()({\n  label: withField(stringValue(), {field: '__label'}),\n});\n\nclass CalloutNode extends ElementNode {\n  __label: string = '';\n\n  // No afterCloneFrom: `__label` is declared as a field in calloutSchema, so\n  // it is carried across every clone.\n  $config() {\n    return this.config('callout', {extends: ElementNode, json: calloutSchema});\n  }\n}\n```\n\n### When you still need it\n\nOverride `afterCloneFrom` for a property that neither of those covers: a\nfield that isn't in the node's schema, one declared only through accessor\nmethods (see\n[Carrying properties across a clone](../serialization/serialization.md#carrying-properties-across-a-clone)),\nor any property of a node written without a schema. Always call\n`super.afterCloneFrom(prevNode)` first so the inherited properties and\nNodeState are still carried:\n\n```ts\nclass MyCustomNode extends ElementNode {\n  __data: string = '';\n\n  afterCloneFrom(prevNode: this): void {\n    super.afterCloneFrom(prevNode);\n    this.__data = prevNode.__data;\n  }\n}\n```\n\nLike `clone`, `afterCloneFrom` is called by Lexical; don't call it yourself\nexcept through `super`.\n\n## Using `$copyNode`\n\n### What is `$copyNode`?\n\n`$copyNode` is the public API for creating a copy of a node with a new key. Use this when you need to create a duplicate node.\nBy default, all properties and `NodeState` will be copied to the new node, and then `resetOnCopyNodeFrom` will be called to\nallow the node to optionally reset certain properties (and NodeState configured with `resetOnCopyNode: true`) to defaults\n(such as the checked state of a `ListItemNode`).\n\n```typescript\n// ✅ Correct: Using $copyNode\nconst copy = $copyNode(existingNode);\n```\n\n### When to Use `$copyNode`?\n\n1. **Creating Duplicates**\n   ```typescript\n   // ✅ Correct: Duplicating a node\n   const duplicate = $copyNode(originalNode);\n   someParent.append(duplicate);\n   ```\n\n## Using `getWritable`\n\n### What is `getWritable`?\n\n`getWritable` is an internal API used within node method implementations to get a mutable version of a node. Node consumers should use the node's public methods instead.\n\n```typescript\n// ✅ Correct: Implementation of a node method\nclass MyCustomNode extends ElementNode {\n  setData(data: string): this {\n    const self = this.getWritable();\n    self.__data = data;\n    return self;\n  }\n}\n\n// ✅ Correct: Using the node\nconst node = new MyCustomNode();\nnode.setData(\"new value\");\n```\n\n## Common Patterns\n\n### Modifying Nodes\n\n```typescript\n// ✅ Correct: Modifying a node\nfunction $updateNodeData(node: MyCustomNode, newData: string): MyCustomNode {\n  return node.setData(newData);\n}\n\n// ❌ Incorrect: Don't clone manually\nfunction $updateNodeDataWrong(node: MyCustomNode, newData: string): MyCustomNode {\n  const clone = MyCustomNode.clone(node);\n  clone.setData(newData);\n  return clone;\n}\n```\n\n### Copying Nodes\n\n```typescript\n// ✅ Correct: Copying a node\nfunction $duplicateNode(node: MyCustomNode): MyCustomNode {\n  return $copyNode(node);\n}\n\n// ❌ Incorrect: Don't use clone\nfunction $duplicateNodeWrong(node: MyCustomNode): MyCustomNode {\n  return node.constructor.clone(node);\n}\n```\n\n## Performance Considerations\n\n1. **Referential Integrity**\n   - `getWritable()` ensures proper EditorState updates with new clones\n   - Prevents \"orphaned\" nodes that won't be rendered\n\n2. **State Management**\n   ```typescript\n   class MyCustomNode extends ElementNode {\n     setData(data: string): this {\n       const self = this.getWritable();\n       self.__data = data; // Properly tracked by editor\n       return self;\n     }\n   }\n   ```\n\n## Testing\n\n```typescript\ntest('node modification', async () => {\n  await editor.update(() => {\n    const node = new MyCustomNode(\"test\");\n    \n    node.setData(\"new data\");\n    \n    // ✅ Correct: Use $copyNode for duplication\n    const copy = $copyNode(node);\n  });\n});\n```\n\n## Related Concepts\n\n- [Editor State](editor-state.md) - How cloning affects editor state\n- [Nodes](nodes.mdx) - Core concepts about Lexical nodes\n\n## Common Questions\n\n**Q: How do I duplicate a node?**\nA: Use `$copyNode(node)` to create a new copy with a new key.\n\n**Q: When should I use clone?**\nA: Never directly. Use `$copyNode()` or `getWritable()` instead. ","isInternal":false,"tokens":1768,"sizeBytes":7286},{"name":"node-replacement.md","path":"packages/lexical-website/docs/concepts/node-replacement.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-website/docs/concepts/node-replacement.md","title":"Concepts Skill","category":"anthropic-skill","format":"markdown","content":"# Node Replacement\n\nNode Replacement allow you to replace all instances of a given node in your editor with instances of a subclass.\n\n## Use Case\n\n:::note\n\nIn earlier versions of this documentation, \"Node Replacement\" was called \"Node Overrides\".\nWe've changed the name to match the terms used in the implementation.\n\n:::\n\n:::tip\n\nIf your use case only requires adding ad-hoc data to existing nodes, you may be able to use the [NodeState](node-state.md) API instead of subclassing and node replacement.\n\n:::\n\nSome of the most commonly used Lexical Nodes are owned and maintained by the core library. For example, ParagraphNode, HeadingNode, QuoteNode, List(Item)Node etc - these are all provided by Lexical packages, which provides an easier out-of-the-box experience for some editor features, but makes it difficult to override their behavior. For instance, if you wanted to change the behavior of ListNode, you would typically extend the class and override the methods. However, how would you tell Lexical to use *your* ListNode subclass in `ListExtension` instead of using the core ListNode? That's where Node Replacement can help.\n\n\nNode Replacement allow you to replace all instances of a given node in your editor with instances of a different node class. This can be done through the `nodes` of an extension:\n\n```ts\nconst CustomParagraphExtension = defineExtension({\n  name: '@my-app/CustomParagraph',\n  nodes: () => [\n    // Don't forget to register your custom node separately!\n    CustomParagraphNode,\n    {\n      replace: ParagraphNode,\n      with: (node: ParagraphNode) => {\n        return $createCustomParagraphNode();\n      },\n      withKlass: CustomParagraphNode,\n    },\n  ],\n});\n```\nIn the snippet above,\n- `replace`: Specifies the core node type to be replaced. \n- `with`: Defines a transformation function to replace instances of the original node to the custom node.  \n- `withKlass`: This option ensures that behaviors associated with the original node type work seamlessly with the replacement. For instance, node transforms or mutation listeners targeting ParagraphNode will also apply to CustomParagraphNode when withKlass is specified. Without this option, the custom node might not fully integrate with the editor's built-in features, leading to unexpected behavior.\n\nOnce this is done, Lexical will replace all ParagraphNode instances with CustomParagraphNode instances. One important use case for this feature is overriding the serialization behavior of core nodes. Check out the full example below.\n\n## Node Replacement Example\n\nThis example demonstrates using Node Replacement to replace all `ParagraphNode` with a `CustomParagraphNode` that overrides `createDOM`.\n\n[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/facebook/lexical/tree/main/examples/node-replacement?file=src/main.tsx)\n\n<iframe width=\"100%\" height=\"600\" src=\"https://stackblitz.com/github/facebook/lexical/tree/main/examples/node-replacement?embed=1&file=src%2FApp.tsx&terminalHeight=0&ctl=1&showSidebar=0&devtoolsheight=0&view=preview\" sandbox=\"allow-forms allow-modals allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin allow-scripts\" title=\"Node Replacement Example\"></iframe>\n","isInternal":false,"tokens":699,"sizeBytes":3272},{"name":"node-state.md","path":"packages/lexical-website/docs/concepts/node-state.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-website/docs/concepts/node-state.md","title":"Concepts Skill","category":"anthropic-skill","format":"markdown","content":"# NodeState\n\nThe NodeState API introduced in v0.26.0 allows arbitrary state to be added\nad-hoc to any node in a way that participates with reconciliation, history,\nand JSON serialization.\n\n## Use Case\n\nNodeState allows your application to define keys that can be stored on\nany node with automatic JSON support.\n\n:::tip\n\nYou can even add node state to the RootNode to store document-level metadata.\n\n:::\n\nWith a combination of NodeState and other APIs such as\n[Listeners](listeners.md) or\n[Transforms](transforms.md) you can\nlikely shape the editor to meet your needs without having to do much\n[Node Customization](node-replacement.md).\n\nEven when you are subclassing nodes, using NodeState instead of additional\nproperties to store the node's data can be [more efficient](#efficiency)\nand will save you from writing a lot of boilerplate in the constructor,\nupdateFromJSON, and exportJSON.\n\n## Usage\n\n### createState\n\n[createState](/docs/api/modules/lexical#createstate)\ncreates a\n[StateConfig](/docs/api/modules/lexical#stateconfig)\nwhich defines the key and configuration for your NodeState value.\n\nThe key must be locally unique, two distinct StateConfig must not have the\nsame string key if they are to be used on the same node.\n\nTypical usage will look something like this:\n\n```ts\nconst questionState = createState('question', {\n  parse: (v) => (typeof v === 'string' ? v : ''),\n});\n```\n\nThe required `parse` function serves two purposes:\n- It provides a type-safe and runtime-safe way to parse values that were\n  serialized to JSON\n- When called with `undefined` (or any invalid value) it should return some\n  default value (which may be `undefined` or `null` or any other value you\n  choose)\n\nIn this case, the question must be a string, and the default is an\nempty string.\n\nSee the\n[createState](/docs/api/modules/lexical#createstate)\nAPI documentation for more details, there are other optional settings\nthat you may want to define particularly if the value is not a primitive\nvalue such as boolean, string, number, null, or undefined.\n\n:::tip\n\nInstead of writing parse functions by hand, use the\n[serialization schema combinators](/docs/serialization/#declarative-serialization-schemas-with-config)\nfrom `lexical`, such as `stringValue`, `numberValue`, `enumValue`,\n`arrayValue`, `objectValue`, and `nullable`. They are the same ones a node's\n`$config` JSON schema uses. A schema is itself a parse function, so it can be\npassed directly as `parse`:\n\n```ts\nimport {arrayValue, createState, stringValue} from 'lexical';\n\nconst questionState = createState('question', {parse: stringValue()});\nconst tagsState = createState('tags', {parse: arrayValue(stringValue())});\n```\n\nBecause `parse` is just a function, the combinators also compose with schema\nlibraries such as [zod](https://zod.dev/), [ArkType](https://arktype.io/),\n[Effect](https://effect.website/), and [Valibot](https://valibot.dev/). Wrap a\nlibrary's validator in a parse function that returns a default for invalid\ninput, or use `transformValue` to refine what a combinator parsed.\n\n:::\n\n### $getState\n\n[$getState](/docs/api/modules/lexical#getstate) gets the\nNodeState value from the given node, or the default if that key was never\nset on the node.\n\n```ts\nconst question = $getState(pollNode, questionState);\n```\n\nSee also\n[$getStateChange](/docs/api/modules/lexical#getstatechange)\nif you need an efficient way to determine if the state has changed on two\nversions of the same node (typically used in updateDOM, but may be useful in\nan update listener or mutation listener).\n\n### $setState\n\n[$setState](/docs/api/modules/lexical#setstate) sets the\nNodeState value on the given node.\n\n```ts\nconst question = $setState(\n  pollNode,\n  questionState,\n  'Are you planning to use NodeState?',\n);\n```\n\n:::tip\n\nThe last argument is a ValueOrUpdater, just like with React's useState\nsetters. If you use an updater function and the value does not change,\nthe node and its NodeState *won't* be marked dirty.\n\n:::\n\n## Serialization\n\nThe NodeState for a node, if any values are set to non-default values, is\nserialized to a record under a single\n[NODE_STATE_KEY](/docs/api/modules/lexical#node_state_key)\nwhich is equal to `'$'`.\n\n```json\n{\n  \"type\": \"poll\",\n  \"$\": {\n    \"question\": \"Are you planning to use NodeState?\"\n  }\n}\n```\n\n:::tip\n\nBy default, it is assumed that your parsed values are JSON serializable,\nbut for advanced use cases you may use values such as Date, Map, or Set\nthat need to be transformed before JSON serialization. See the\n[StateValueConfig](/docs/api/modules/lexical#statevalueconfig)\nAPI documentation.\n\n:::\n\n### Flat serialization with `$config`\n\nNodes that declare a `StateConfig` in [`$config`](nodes.mdx#creating-custom-nodes-with-config-and-nodestate)\nwith `flat: true` lift that key to the top level of the serialized node\ninstead of nesting it under `'$'`. This makes the JSON shape identical\nto a legacy node that stored the value as a `__property` instance\nvariable, so existing payloads continue to round-trip after the node is\nmigrated to NodeState.\n\nFor example, a `ColoredNode` that extends `TextNode` and declares a flat\n`color` state (see [Extending TextNode with `$config`](nodes.mdx#extending-textnode-with-config)):\n\n```ts\n$config() {\n  return this.config('colored', {\n    extends: TextNode,\n    stateConfigs: [{flat: true, stateConfig: colorState}],\n  });\n}\n```\n\nserializes as:\n\n```json\n{\n  \"type\": \"colored\",\n  \"text\": \"hello\",\n  \"color\": \"red\"\n}\n```\n\nA non-flat (default) state on the same node would instead appear under\n`'$'`:\n\n```json\n{\n  \"type\": \"colored\",\n  \"text\": \"hello\",\n  \"$\": {\n    \"color\": \"red\"\n  }\n}\n```\n\nIn both cases, a key is only emitted when the current value is not equal\nto the default returned by its `parse` function — see\n[Efficiency](#efficiency).\n\n:::note\n\nDon't reuse a flat key that a superclass already serializes (e.g. `text`\non `TextNode`).\n\n:::\n\n### Upgrading a legacy JSON property to NodeState\n\nA node that stores data as a `__property` instance variable with custom\n`exportJSON`/`importJSON`/`updateFromJSON` can be migrated to NodeState\nwith `flat: true` without changing its serialized JSON shape.\n\nBefore — `ColoredNode` with a `__color` property and hand-written\nserialization (see the full legacy example in\n[Extending TextNode with `$config`](nodes.mdx#extending-textnode-with-config)):\n\n```ts\nexport type SerializedColoredNode = Spread<\n  {color?: string},\n  SerializedTextNode\n>;\n\nexport class ColoredNode extends TextNode {\n  __color: string;\n\n  constructor(text: string = '', color: string = DEFAULT_COLOR, key?: NodeKey) {\n    super(text, key);\n    this.__color = color;\n  }\n\n  static getType(): string {\n    return 'colored';\n  }\n\n  static clone(node: ColoredNode): ColoredNode {\n    return new ColoredNode(node.__text, node.__color, node.__key);\n  }\n\n  static importJSON(serializedNode: SerializedColoredNode) {\n    return new ColoredNode().updateFromJSON(serializedNode);\n  }\n\n  updateFromJSON(serializedNode: SerializedColoredNode) {\n    const self = super.updateFromJSON(serializedNode);\n    self.__color =\n      typeof serializedNode.color === 'string'\n        ? serializedNode.color\n        : DEFAULT_COLOR;\n    return self;\n  }\n\n  exportJSON(): SerializedColoredNode {\n    return {\n      ...super.exportJSON(),\n      color: this.__color === DEFAULT_COLOR ? undefined : this.__color,\n    };\n  }\n}\n```\n\nAfter — same on-the-wire JSON, no hand-written serialization:\n\n```ts\nconst colorState = createState('color', {\n  parse: (v) => (typeof v === 'string' ? v : DEFAULT_COLOR),\n});\n\nexport class ColoredNode extends TextNode {\n  $config() {\n    return this.config('colored', {\n      extends: TextNode,\n      stateConfigs: [{flat: true, stateConfig: colorState}],\n    });\n  }\n}\n```\n\nNo `exportJSON`, `importJSON`, `updateFromJSON`, `clone`, or\n`afterCloneFrom` override is needed: `$config` installs `clone` and\n`importJSON`, and the base `exportJSON`/`updateFromJSON`/`afterCloneFrom`\non `LexicalNode`/`TextNode` already round-trip NodeState. Read the color\nvia `$getState(node, colorState)` and write it via\n`$setState(node, colorState, value)` instead of `node.__color`.\n\n:::tip\n\nFor a seamless migration, keep the JSON key name the same — for a flat\nstate, that's the first argument to `createState`. Migrating from a\nnon-flat NodeState to a flat NodeState also works: state that appears\nunder the `$` (`NODE_STATE_KEY`) is still parsed even when the state is\nconfigured as flat, and the flat value takes precedence when both are\npresent.\n\n:::\n\n## Efficiency\n\nNodeState uses a copy-on-write scheme to manage each node's state. If\nnone of the state has changed, then the NodeState instance will be\nshared across multiple versions of that node.\n\n:::info\n\nIn a given reconciliation cycle, the first time a Lexical node is marked dirty\nvia `getWritable` will create a new instance of that node. All properties\nof the previous version are set on the new instance. NodeState is stored\nas a single property, and no copying of the internal state is done\nuntil the NodeState itself is marked writable.\n\n:::\n\nWhen serializing to JSON, each key will only be stored if the value\nis not equal to the default value. This can save quite a lot of space\nand bandwidth.\n\nParsing and serialization is only done at network boundaries, when\nintegrating with JSON or Yjs. When a value changes from an external\nsource, it is only parsed once the first time it is read.\nValues that do not come from external sources are not parsed, and\nvalues that are not used are never parsed.\n\n## Capabilities\n\nCurrent:\n\n- Allows you to define and add state to any node\n- Serializes that state automatically in the node's JSON, supporting\n  versioning and copy+paste\n- Works with the reconciler, TextNodes with differing state will not\n  be implicitly merged\n- @lexical/yjs support, NodeState will be automatically synchronized\n  like any other property\n- NodeState values that are not used will simply pass-through, making\n  it a bit easier for situations where multiple configurations are used\n  on the same data (e.g. older and newer versions of your editor,\n  a different set of plugins based on context, etc.).\n- Pre-registration system for nodes to declare expected state\n  and serialize them as top-level properties (`flat`) with `$config`\n  (see [#7260](https://github.com/facebook/lexical/issues/7260)).\n- Can be integrated with\n  [DOMRenderExtension](../serialization/dom-render.md) for editor\n  rendering and HTML export\n- Can be integrated with\n  [DOMImportExtension](../serialization/dom-import.md) for HTML import\n\nFuture:\n\n- Does not yet support direct integration with Yjs, e.g.\n  you can not store a Y.Map as a NodeState value\n  (see [#7293](https://github.com/facebook/lexical/issues/7293))\n\n## Node State Style Example\n\nThis example demonstrates an advanced use case of storing a style object on TextNode using NodeState.\n\n[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/facebook/lexical/tree/main/examples/node-state-style?file=src/main.tsx)\n\n<iframe width=\"100%\" height=\"600\" src=\"https://stackblitz.com/github/facebook/lexical/tree/main/examples/node-state-style?embed=1&file=src%2FApp.tsx&terminalHeight=0&ctl=1&showSidebar=0&devtoolsheight=0&view=preview\" sandbox=\"allow-forms allow-modals allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin allow-scripts\" title=\"Node State Style Example\"></iframe>\n","isInternal":false,"tokens":2790,"sizeBytes":11450},{"name":"one-lexical-per-app.md","path":"packages/lexical-website/docs/concepts/one-lexical-per-app.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-website/docs/concepts/one-lexical-per-app.md","title":"Concepts Skill","category":"anthropic-skill","format":"markdown","content":"---\nsidebar_label: One Lexical per app\n---\n\n# One Lexical per app\n\n`lexical` is effectively a singleton **within one app**: an app must resolve\nexactly **one** copy of `lexical`, and one copy of each `@lexical/*` package it\nuses. Two copies in the same bundle do not merely waste bytes — they break the\neditor in ways whose error messages point nowhere near the real cause.\n\nThe practical consequence, covered in [Publishing a library](#publishing-a-library)\nbelow: **a library built on Lexical declares `lexical` and the `@lexical/*`\npackages it imports as `peerDependencies`, never as `dependencies`. The\napplication brings its own Lexical.**\n\n:::note\n\nThis page is about npm `peerDependencies` in your `package.json`. Extensions\nhave their own, unrelated notion of\n[peer dependencies](../extensions/peer-dependencies.md) — optional links between\nextensions inside one editor.\n\n:::\n\n## The boundary is the app, not the page\n\nSeveral self-contained apps can share a page, each with its own bundled copy of\nLexical, and that is fine — a CMS page with two independently deployed widgets\non it, or a micro-frontend setup, does not have a bug just because two copies of\n`lexical` are loaded. Lexical anticipates this: `isLexicalEditor()` is an\n`instanceof` check specifically \"to prevent issues with multiple embedded\nLexical installations\", and an editor is tagged on its own root element rather\nthan in any page-global registry, so each app's editors resolve to that app's\nLexical and ignore the others.\n\nWhat those apps cannot do is interoperate. Two copies have no compatible API\nsurface between them, so treat each as a sealed island:\n\n- do not pass nodes, editors, `EditorState` objects, selections or commands\n  across the boundary — pass serialized JSON or HTML, which is data rather than\n  identity;\n- a plugin, node package or shared editor config must be bundled with the app it\n  serves, not shared as a live import between apps;\n- `@lexical/react` context does not cross the boundary either, since the context\n  object itself comes from one copy.\n\nThe rest of this page is about the failure case: two copies **inside one app**,\nwhere code from copy A is expected to work with an editor from copy B.\n\n## Why one copy per app\n\nLexical keeps state at module scope, and identifies things by reference rather\nthan by name. Both assumptions hold only within a single copy of the module.\n\n- **The active editor is module state.** `activeEditor`, `activeEditorState`\n  and `isReadOnlyMode` live at module scope in `LexicalUpdates.ts`, and every\n  `$` function reads them. `editor.update()` in copy A sets copy A's variables;\n  a `$` function imported from copy B reads copy B's, which are still `null`\n  ([Editor State](editor-state.md)).\n- **Node classes are compared by identity.** Node registration and the\n  `instanceof LexicalNode` checks compare class references. A node that extends\n  `LexicalNode` from copy A is not, as far as copy B is concerned, a\n  `LexicalNode` at all — even though both classes have the same name and come\n  from the same published version ([Nodes](nodes.mdx)).\n- **Commands are object identities.** `createCommand()` returns a fresh `{type}`\n  object, and `registerCommand`/`dispatchCommand` match on that object, not on\n  its `type` string. Two copies of `lexical` export two distinct\n  `FORMAT_TEXT_COMMAND` objects, so a dispatch from one never reaches a listener\n  registered with the other ([Commands](commands.md)).\n\nOn top of that, two copies are usually two *versions*, which is API drift inside\na single editor: half your plugins were compiled against a different API and\nserialization format than the editor running them, and the mismatch surfaces\nonly at runtime, on the paths that happen to differ.\n\n## Symptoms of a duplicated Lexical\n\nThe errors are unhelpfully literal — a class that plainly does implement\n`getType()` is reported as not implementing it, because the check is running\nagainst the other copy:\n\n```\nLexicalNode: Node FooNode does not implement .getType().\n\nCreate node: Type heading in node HeadingNode does not match registered node\nHeadingNode with the same type\n\ncreateEditor: nodes[0] FooNode (type foo) is not a constructor that subclasses\nLexicalNode from the lexical package used by this editor (0.50.0)\n\nUnable to find an active editor state. State helpers or node methods can only be\nused synchronously during the callback of editor.update(), editor.read(), or\neditorState.read(). Detected on the page: 0 compatible editor(s) with version\n0.50.0 and incompatible editors with versions 0.49.0\n```\n\nThat last sentence is Lexical telling you directly that it found editors built\nfrom another copy — a version different from its own, or the same version marked\n`(separately built, likely a bundler configuration issue)`. Note that this\ndiagnostic scans the whole page, so a genuinely separate app that ships its own\nLexical is counted there too; it is evidence of a bug only when those editors\nwere supposed to be part of *your* app.\n\nSilent failures look like: a command that dispatches but never fires its\nlistener, `$getSelection()` returning `null` inside an update, or a custom node\nrendering as plain text after a copy/paste. Real reports of this failure mode:\n[#4225](https://github.com/facebook/lexical/issues/4225) and\n[#7819](https://github.com/facebook/lexical/issues/7819).\n\n## Publishing a library\n\nIf you publish a package that imports from `lexical` or `@lexical/*` — a plugin,\na node package, a shared editor config, an in-house wrapper in a monorepo — put\nthose packages in `peerDependencies`, and in `devDependencies` so you can still\nbuild and test:\n\n```json\n{\n  \"name\": \"my-lexical-plugin\",\n  \"peerDependencies\": {\n    \"lexical\": \">=0.50.0\",\n    \"@lexical/react\": \">=0.50.0\",\n    \"@lexical/utils\": \">=0.50.0\"\n  },\n  \"devDependencies\": {\n    \"lexical\": \"0.50.0\",\n    \"@lexical/react\": \"0.50.0\",\n    \"@lexical/utils\": \"0.50.0\"\n  }\n}\n```\n\nNotes on the ranges:\n\n- Lexical is pre-1.0, so `^0.50.0` means `>=0.50.0 <0.51.0` — a caret peer range\n  locks your users out of the next release. Prefer an open `>=` range (or an\n  explicit `>=0.50.0 <1.0.0`) so an app can upgrade Lexical without waiting for\n  you.\n- Mark a peer optional with\n  [`peerDependenciesMeta`](https://docs.npmjs.com/cli/configuring-npm/package-json#peerdependenciesmeta)\n  when the import is behind an opt-in entry point, the way this repo's own\n  packages mark `typescript` optional.\n- Do not add `lexical` to `dependencies` \"just so it installs\". That is exactly\n  what pins a second copy under `node_modules/your-lib/node_modules/lexical` as\n  soon as your range and the app's range disagree.\n\n## Application setup\n\nThe application owns the version, and every Lexical package it installs moves\ntogether:\n\n- Install `lexical` and each `@lexical/*` package you use **at the same\n  version**. Published `@lexical/*` packages depend on the exact matching\n  `lexical` version (the monorepo's `workspace:*` is rewritten to an exact\n  version at publish time), so `@lexical/react@0.50.0` beside `lexical@0.49.0`\n  guarantees a nested second copy.\n- Verify there is only one **per app**, from the app root:\n\n  ```sh\n  npm ls lexical @lexical/react     # or: pnpm why lexical / yarn why lexical\n  ```\n\n  More than one resolved version, or a path with a nested `node_modules/lexical`\n  in it, is the bug.\n- In a monorepo or a linked-package setup (`npm link`, `pnpm link`, a Yalc\n  copy), the linked library resolves Lexical from *its own* `node_modules` and\n  duplicates are the default rather than the exception. Deduplicate explicitly:\n  hoist the dependency to the workspace root, or force resolution with\n  [pnpm `overrides`](https://pnpm.io/settings#overrides), Yarn `resolutions`, or\n  `npm dedupe`.\n- Bundlers can also be told to collapse duplicates — Vite\n  [`resolve.dedupe`](https://vite.dev/config/shared-options.html#resolve-dedupe),\n  webpack `resolve.alias` pointing each package at one path:\n\n  ```js\n  // vite.config.js\n  export default {\n    resolve: {dedupe: ['lexical', '@lexical/react', '@lexical/utils']},\n  };\n  ```\n\n  Treat that as a safety net, not a fix: it collapses copies that are already\n  version-compatible, and cannot rescue two genuinely different versions.\n- Duplication does not always come from the dependency tree: importing Lexical\n  through both `import` and `require` pulls in the ESM and the CJS build of the\n  *same* version, which are still two copies. The\n  [React FAQ](../react/faq.md#lexicalcomposercontextuselexicalcomposercontext-cannot-find-a-lexicalcomposercontext)\n  covers how this one shows up in `@lexical/react`.\n","frontmatter":{"sidebar_label":"One Lexical per app"},"isInternal":false,"tokens":2113,"sizeBytes":8637},{"name":"read-only.md","path":"packages/lexical-website/docs/concepts/read-only.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-website/docs/concepts/read-only.md","title":"Concepts Skill","category":"anthropic-skill","format":"markdown","content":"\n\n# Read Mode / Edit Mode\n\nLexical supports two modes:\n\n- Read mode\n- Edit mode\n\nThe default behavior for Lexical is edit mode, or more accurately not read only mode. Under-the-hood, the main\nimplementation detail is that the `contentEditable` is being set to `\"false\"` or `\"true\"` depending on the mode.\nSpecific plugins can listen to the mode change too – allowing them to customize parts of the UI depending on the\nmode.\n\n## Setting the mode\n\nIn order to set the mode, this can be done on creation of the editor, with the\n`editable` property of your app's extension:\n\n```js\nimport {buildEditorFromExtensions} from '@lexical/extension';\nimport {RichTextExtension} from '@lexical/rich-text';\nimport {defineExtension} from 'lexical';\n\nconst editor = buildEditorFromExtensions(\n  defineExtension({\n    dependencies: [RichTextExtension],\n    editable: false,\n    name: '@my-app/editor',\n  }),\n);\n```\n\nWith React, pass the same extension to `<LexicalExtensionComposer>`:\n\n```jsx\n<LexicalExtensionComposer extension={appExtension}>\n  ...\n</LexicalExtensionComposer>\n```\n\nAfter an editor is created, the mode can be changed imperatively:\n\n```js\neditor.setEditable(true);\n```\n\n## Reading the mode\n\nIn order to find the current mode of the editor you can use:\n\n```js\nconst isEditable = editor.isEditable(); // Returns true or false\n```\n\nYou can also get notified when the editor's read only mode has changed:\n\n```js\nconst removeEditableListener = editor.registerEditableListener(\n  (isEditable) => {\n    // The editor's mode is passed in!\n    console.log(isEditable);\n  },\n);\n\n// Do not forget to unregister the listener when no longer needed!\nremoveEditableListener();\n```\n\nIn an extension, depend on\n[`WatchEditableExtension`](/docs/api/modules/lexical_extension#watcheditableextension),\nwhose output is a signal holding the current editable state, instead of\nregistering the listener yourself.\n","isInternal":false,"tokens":424,"sizeBytes":1892},{"name":"selection.md","path":"packages/lexical-website/docs/concepts/selection.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-website/docs/concepts/selection.md","title":"Concepts Skill","category":"anthropic-skill","format":"markdown","content":"# Selection\n\n## Types of selection\n\nLexical's selection is part of the `EditorState`. This means that for every update, or change to the editor, the\nselection always remains consistent with that of the `EditorState`'s node tree.\n\nIn Lexical, there are four types of selection possible:\n\n- `RangeSelection`\n- `NodeSelection`\n- `TableSelection` (implemented in `@lexical/table`)\n- `null`\n\nIt is possible, but not generally recommended, to implement your own selection types that implement `BaseSelection`.\n\n### `RangeSelection`\n\nThis is the most common type of selection, and is a normalization of the browser's DOM Selection and Range APIs.\n`RangeSelection` consists of three main properties:\n\n- `anchor` representing a `RangeSelection` point\n- `focus` representing a `RangeSelection` point\n- `format` numeric bitwise flag, representing any active text formats\n\nBoth the `anchor` and `focus` points refer to an object that represents a specific part of the editor. The main properties of a `RangeSelection` point are:\n\n- `key` representing the `NodeKey` of the selected Lexical node\n- `offset` representing the position from within its selected Lexical node. For the `text` type this is the character, and for the `element` type this is the child index from within the `ElementNode`\n- `type` representing either `element` or `text`.\n\n### `NodeSelection`\n\nNodeSelection represents a selection of multiple arbitrary nodes. For example, three images selected at the same time.\n\n- `getNodes()` returns an array containing the selected LexicalNodes\n\n### `TableSelection`\n\nTableSelection represents a grid-like selection like tables. It stores the key of the parent node where the selection takes place and the start and end points.\n`TableSelection` consists of three main properties:\n\n- `tableKey` representing the parent node key where the selection takes place\n- `anchor` representing a `TableSelection` point\n- `focus` representing a `TableSelection` point\n\nFor example, a table where you select row = 1 col = 1 to row 2 col = 2 could be stored as follows:\n- `tableKey = 2` table key\n- `anchor = 4` table cell (key may vary)\n- `focus = 10` table cell (key may vary)\n\nNote that `anchor` and `focus` points work the same way as `RangeSelection`.\n\n### `null`\n\nThis is for when the editor doesn't have any active selection. This is common for when the editor has been blurred or when selection\nhas moved to another editor on the page. This can also happen when trying to select non-editable components within the editor space.\n\n## Working with selection\n\nSelection can be found using the `$getSelection()` helper, exported from the `lexical` package. This function can be used within\nan update, a read, or a command listener.\n\n:::tip\n\nTo walk the document from a selection point — or to write a traversal that\nhandles empty nodes and collapsed selections correctly — see\n[Node Traversals with NodeCaret](./traversals.md).\n`$caretRangeFromSelection(selection)` converts a `RangeSelection` into the\ncaret range that traversal API works with, and\n`$setSelectionFromCaretRange(range)` converts back.\n\n:::\n\n```js\nimport {$getSelection, SELECTION_CHANGE_COMMAND} from 'lexical';\n\neditor.update(() => {\n  const selection = $getSelection();\n});\n\neditorState.read(() => {\n  const selection = $getSelection();\n});\n\n// SELECTION_CHANGE_COMMAND runs before reconciliation in the pending update.\neditor.registerCommand(SELECTION_CHANGE_COMMAND, () => {\n  const selection = $getSelection();\n});\n```\n\nIn some cases you might want to create a new type of selection and set the editor selection to\nbe that. This can only be done in update or command listeners.\n\n```js\nimport {$setSelection, $createRangeSelection, $createNodeSelection} from 'lexical';\n\neditor.update(() => {\n  // Set a range selection\n  const rangeSelection = $createRangeSelection();\n  $setSelection(rangeSelection);\n\n  // You can also indirectly create a range selection, by calling some of the selection\n  // methods on Lexical nodes.\n  const someNode = $getNodeByKey(someKey);\n\n  // On element nodes, this will create a RangeSelection with type \"element\",\n  // referencing an offset relating to the child within the element.\n  // On text nodes, this will create a RangeSelection with type \"text\",\n  // referencing the text character offset.\n  someNode.select();\n  someNode.selectPrevious();\n  someNode.selectNext();\n\n  // You can use this on any node.\n  someNode.selectStart();\n  someNode.selectEnd();\n\n  // Set a node selection\n  const nodeSelection = $createNodeSelection();\n  // Add a node key to the selection.\n  nodeSelection.add(someKey);\n  $setSelection(nodeSelection);\n\n  // You can also clear selection by setting it to `null`.\n  $setSelection(null);\n});\n```\n\n## Focus\n\nYou may notice that when you issue an `editor.update` or\n`editor.dispatchCommand` then the editor can \"steal focus\" if there is\na selection and the editor is editable. This is because the Lexical\nselection is reconciled to the DOM selection during reconciliation,\nand the browser's focus follows its DOM selection.\n\nIf you want to make updates or dispatch commands to the editor without\nchanging the selection, can use the `SKIP_DOM_SELECTION_TAG` update tag\n(added in v0.22.0):\n\n```js\n// Call this from an editor.update or command listener\n$addUpdateTag(SKIP_DOM_SELECTION_TAG);\n```\n\nIf you want to add this tag during processing of a `dispatchCommand`,\nyou can wrap it in an `editor.update`:\n\n```js\n// NOTE: If you are already in a command listener or editor.update,\n//       do *not* nest a second editor.update! Nested updates have\n//       confusing semantics (dispatchCommand will re-use the\n//       current update without nesting)\neditor.update(() => {\n  $addUpdateTag(SKIP_DOM_SELECTION_TAG);\n  editor.dispatchCommand(/* … */);\n});\n```\n\n`SKIP_DOM_SELECTION_TAG` does not apply to the initial editor state setup\n(the `$initialEditorState` property of the root extension, or\n`initialConfig.editorState` with the legacy `LexicalComposer`). On first mount the editor still scrolls\nto and focuses the initial selection. To prevent that, call\n`$setSelection(null)` inside your initial state setup function:\n\n```js\nconst editor = buildEditorFromExtensions({\n  name: '[root]',\n  // ...\n  $initialEditorState: () => {\n    // ... build your initial nodes ...\n    $setSelection(null);\n  },\n});\n```\n\nIf you have to support older versions of Lexical, you can mark the editor\nas not editable during the update or dispatch.\n\n```js\n// NOTE: This code should be *outside* of your update or command listener, e.g.\n//       directly in the DOM event listener\nconst prevEditable = editor.isEditable();\neditor.setEditable(false);\neditor.update(\n  () => {\n    // run your update code or editor.dispatchCommand in here\n  }, {\n    onUpdate: () => {\n      editor.setEditable(prevEditable);\n    },\n  },\n);\n```\n\n## Reading selection across a shadow boundary\n\nIf your plugin reads the DOM selection directly — through\n`Selection.anchorNode`, `Selection.getRangeAt(0)`, or similar — and the\neditor's `contentEditable` lives inside a `ShadowRoot`, the browser retargets\nthose reads to the shadow host. See\n[Shadow DOM and iframes](./shadow-dom.md) for the shadow-aware helpers\n(`getDOMSelectionPoints`, `getDOMSelectionRange`, etc.) that return the\nun-retargeted boundary points; they fall through to the standard reads in the\nplain light DOM, so there's nothing to do until you actually mount the editor\nin a shadow tree.\n\n### Selection change timing\n\n`SELECTION_CHANGE_COMMAND` runs **before reconciliation**, in a writable update,\nfor native, programmatic, and non-range selection changes. `$getSelection()`\nreturns the pending selection; `$getPreviousSelection()` returns the selection\nfrom the last committed state. A listener can normalize the selection or edit\nnodes before that same update commits. Listeners that change the selection may\ncause another notification before commit; they must converge.\n\nThis covers programmatic changes such as `node.select()`, which notify without\nwaiting for a DOM `selectionchange` event, and non-range selections such as\n`NodeSelection` and `TableSelection`, as well as native range selections.\n\nSelection listeners use the update's normal error handling. A throwing listener\nreports through `onError` and can abort the entire pending update, including the\nedit that triggered the notification and any other edits batched into that\ncommit, just like an explicit command dispatch inside that update. Because a\nprogrammatic selection change also notifies, a listener error can abort content\nedits made in the same update. Listener edits do not have a separate rollback\nscope.\n\nThe DOM is not guaranteed to match the pending state, even for a `NodeSelection`.\nNew nodes may not have an element yet, and DOM ranges, layout, and focus may\nstill reflect the previous state. Schedule DOM-dependent work after the update\nand read the reconciled state there:\n\n```ts\neditor.registerCommand(\n  SELECTION_CHANGE_COMMAND,\n  () => {\n    $onUpdate(() => {\n      editor.read('latest', () => {\n        // Read $getSelection(), look up elements, or position floating UI here.\n      });\n    });\n    return false;\n  },\n  COMMAND_PRIORITY_LOW,\n);\n```\n\nImport `$onUpdate` and `COMMAND_PRIORITY_LOW` from `lexical`. Read selection and\nnodes inside the callback rather than capturing mutable pending objects.\n`registerUpdateListener` is another option for UI that must respond to every\ncommit, including content changes that do not change the selection. Updates\nusing `SKIP_DOM_SELECTION_TAG` still deliberately leave the browser selection\nunsynchronized.\n\n#### Update tags and local edits\n\nThe notification belongs to the pending update and inherits its tags. A listener\ncan inspect them with `$hasUpdateTag`. In particular, Yjs does not sync content\nedits back to peers when the update is tagged `COLLABORATION_TAG` or\n`HISTORIC_TAG`; history also treats `HISTORIC_TAG` as replaying an existing entry.\nThis applies to content edits made by selection listeners in those updates.\n\nListeners that only apply to local editing should skip those tagged updates. If\na remote change or undo must trigger an independent local content edit, schedule\nit after the tagged commit with `$onUpdate`, then start a fresh `editor.update`:\n\n```ts\nconst $applyLocalEdit = () => {\n  // Read the current selection and nodes, then apply a convergent local edit.\n};\n\neditor.registerCommand(\n  SELECTION_CHANGE_COMMAND,\n  () => {\n    if ($hasUpdateTag(COLLABORATION_TAG) || $hasUpdateTag(HISTORIC_TAG)) {\n      $onUpdate(() => editor.update($applyLocalEdit));\n    } else {\n      $applyLocalEdit();\n    }\n    return false;\n  },\n  COMMAND_PRIORITY_LOW,\n);\n```\n\nImport the tags and `$hasUpdateTag` from `lexical`. Re-read the state in the fresh\nupdate; it has its own collaboration and undo behavior. Calling `editor.update`\ndirectly inside the command listener queues a nested update that still belongs\nto the same tagged commit.\n\nAutomatic range and cleared-selection notifications require a connected editor\nroot. Changes committed while those notifications are skipped advance the\nselection baseline and are not replayed when the root reconnects. Non-range\nselections continue to notify in unmounted and headless editors,\nwith the same pre-reconciliation timing. Native dirty-selection notifications\nmay fire even when the selection compares equal to the previous selection.\n","isInternal":false,"tokens":2513,"sizeBytes":11388},{"name":"shadow-dom.md","path":"packages/lexical-website/docs/concepts/shadow-dom.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-website/docs/concepts/shadow-dom.md","title":"Concepts Skill","category":"anthropic-skill","format":"markdown","content":"---\nsidebar_label: Shadow DOM and iframes\n---\n\n# Shadow DOM and iframes\n\nA Lexical editor works when its `contentEditable` lives inside an **open**\n[DOM `ShadowRoot`](https://developer.mozilla.org/docs/Web/API/ShadowRoot) (for\nexample inside a [web component](https://developer.mozilla.org/docs/Web/API/Web_components))\nor inside an `<iframe>` document. Both are supported out of the box — you only\nhave to point the editor at the right root element.\n\n## Embedding in a shadow root\n\nAttach an open shadow root, render a `contentEditable` inside it, and pass that\nelement to `editor.setRootElement` (or, with `@lexical/react`, portal the\n`ContentEditable` into the shadow root — React context flows across the portal,\nso the editor is built exactly as it would be in the light DOM):\n\n```js\nconst host = document.querySelector('#editor-host');\nconst shadow = host.attachShadow({mode: 'open'});\nconst contentEditable = document.createElement('div');\ncontentEditable.contentEditable = 'true';\nshadow.appendChild(contentEditable);\n\neditor.setRootElement(contentEditable);\n```\n\nLexical detects the enclosing shadow root from the root element and resolves\nselection through it. There is nothing else to configure.\n\n### Why it needs platform support\n\nInside a shadow tree the browser **retargets** the document's selection to the\nshadow host: `Selection.anchorNode`/`focusNode` and `Selection.getRangeAt`\nreport the host element, not the node the caret is actually in, and\n`document.activeElement` reports the host rather than the focused element.\nLexical works around this using only standard platform APIs:\n\n- Selection is read with\n  [`Selection.getComposedRanges`](https://developer.mozilla.org/docs/Web/API/Selection/getComposedRanges)\n  and [`Selection.direction`](https://developer.mozilla.org/docs/Web/API/Selection/direction),\n  which return the real (un‑retargeted) boundary points.\n- Focus is read with\n  [`ShadowRoot.activeElement`](https://developer.mozilla.org/docs/Web/API/DocumentOrShadowRoot/activeElement)\n  via [`Node.getRootNode`](https://developer.mozilla.org/docs/Web/API/Node/getRootNode).\n- Caret movement and word/line deletion use the native `Selection.modify`, and\n  selection is written with `Selection.setBaseAndExtent`, both of which operate\n  on shadow‑tree nodes directly.\n- Drop targets and image-drag hit‑tests resolve through\n  [`Document.caretPositionFromPoint`](https://developer.mozilla.org/docs/Web/API/Document/caretPositionFromPoint)\n  with the `shadowRoots` option so the dropped point lands on the real node\n  rather than the retargeted host.\n\n### Browser support\n\nThe shadow-aware path requires the platform APIs listed above. Lexical falls\nback to the light‑DOM reads when those APIs are missing, so editors that don't\nlive in a shadow tree keep working on any engine the rest of Lexical supports.\n\n| Platform API | Used for | Chrome / Edge | Firefox | Safari |\n| --- | --- | --- | --- | --- |\n| `Selection.getComposedRanges` | Reading the un‑retargeted boundary points | 137+ | 142+ | 17.0+ |\n| `Selection.direction` | Mapping the composed range back onto anchor/focus | 137+ | 126+ | 17.0+ |\n| `ShadowRoot.activeElement` | Resolving the focused element through the host | All modern | All modern | All modern |\n| `Document.caretPositionFromPoint({shadowRoots})` | Shadow-aware drop / drag hit-tests | 128+ | 125+ | 18.1+ |\n\nLexical also supports the legacy variadic form of `getComposedRanges` shipped by\nSafari 17–18.1, automatically choosing the dictionary or variadic call shape\nat runtime.\n\n### Closed shadow roots\n\nA closed shadow root (`{mode: 'closed'}`) hides its contents from outside code:\n`Node.getRootNode({composed: false})` returns the host on every external probe,\nand `Selection.getComposedRanges({shadowRoots})` ignores the closed root. The\neditor inside a closed shadow root cannot read its own selection through the\nhost, so closed shadow roots are **not supported**.\n\nIf you need style/markup encapsulation without selection isolation, mount the\neditor in an open shadow root with `delegatesFocus: true` instead — outside\ncode can still observe focus and selection while CSS and markup remain\nencapsulated.\n\n### Styling\n\nShadow trees do not inherit the document's stylesheets, so your editor/theme\nCSS has to live inside the shadow root. A `<style>` element appended to the\nroot is the simplest option; a\n[constructable stylesheet](https://developer.mozilla.org/docs/Web/API/CSSStyleSheet#constructor)\nadopted via `shadowRoot.adoptedStyleSheets` scales better when one stylesheet\nserves several editor instances. The playground's `ShadowDomWrapper` clones\nthe page's existing `<link>` / `<style>` nodes into the shadow root so it can\nreuse Vite's HMR-managed stylesheets. This is a property of Shadow DOM, not\nof Lexical.\n\nCSS custom properties and inherited HTML attributes (`dir`, `lang`) cross\nthe shadow boundary on their own, so a page-level rule like `:root {\n--editor-bg: #1c1d22; }` or a host attribute like `<my-editor dir=\"rtl\"\nlang=\"ko\">` propagates into the editor without further setup. User\npreference media queries (`@media (prefers-color-scheme: dark)`,\n`(prefers-reduced-motion: reduce)`, `(forced-colors: active)`) evaluate\ninside the shadow root just like outside; place them in the shadow-mounted\nstylesheet rather than the page sheet.\n\n## Embedding in an iframe\n\nAn editor whose root element belongs to an `<iframe>` document is also\nsupported. Lexical reads the editor's `window`/`document` from the root\nelement (`rootElement.ownerDocument.defaultView`), so selection and focus are\nresolved against the iframe rather than the top‑level document:\n\n```js\nconst iframeDoc = iframe.contentDocument;\nconst contentEditable = iframeDoc.querySelector('#editor');\n\n// The editor can be built in the parent frame; once its root element is set it uses\n// the iframe's own window and document for selection and focus.\neditor.setRootElement(contentEditable);\n```\n\nSelection inside an iframe is **not** retargeted (an iframe is a separate\ndocument, not a shadow boundary), so `getComposedRanges` is not involved here —\nthe iframe's own selection already reports the correct nodes.\n\n## Shadow-aware helpers\n\nIf a plugin reads the DOM selection or the focused element directly, use the\nshadow/iframe‑aware helpers exported from `lexical` instead of\n`Selection.anchorNode` / `document.activeElement`, so it keeps working in these\ncontexts. In the plain light DOM each helper degrades to the standard read\nwithout any extra cost.\n\n| Instead of | Use |\n| --- | --- |\n| `Selection.anchorNode` / `anchorOffset` / `focusNode` / `focusOffset` | `getDOMSelectionPoints(selection, rootElement)` |\n| `selection.getRangeAt(0)` | `getDOMSelectionRange(selection, rootElement)` |\n| Both shapes from one read | `getDOMSelectionRangeAndPoints(selection, rootElement)` |\n| `selection.getComposedRanges` raw call | `getComposedStaticRange(selection, rootElement)` |\n| `document.activeElement === el` | `getActiveElement(el) === el` |\n| Deepest focused element through nested shadow trees | `getActiveElementDeep(document)` |\n| `event.target` on a `composed: true` event listening above the shadow boundary | `getComposedEventTarget(event)` |\n| `node.getRootNode() instanceof ShadowRoot` (cross‑realm‑safe) | `isDOMShadowRoot(node)` |\n| Walking up to discover enclosing shadow roots | `getDOMShadowRoots(node)` |\n\nAll nine helpers are marked `@experimental` while the surface stabilizes — the\nreturn shape may change, but the behavior in the light DOM is stable.\n\n### `getDOMSelectionPoints` read semantics\n\nIn the light DOM `getDOMSelectionPoints` returns the live `Selection` itself,\nso each property read is deferred and `$updateDOMSelection` only pays for the\nsynchronous style/layout recalculation that a `Selection.anchorNode` /\n`focusNode` read triggers when it actually needs the value. Inside a shadow\ntree the return is a snapshot taken at call time, including\n`Selection.direction` so callers can branch on it explicitly. Read the four\npoints immediately after the call rather than caching the returned reference,\nor use `points === domSelection` to detect the alias path.\n\nIf a future engine ships `getComposedRanges` without `Selection.direction`\n(no current configuration matches — every engine that ships the former\nalso ships the latter), the snapshot's `direction` is `undefined` and\nanchor/focus default to the StaticRange's tree order; callers needing strict\nbackward fidelity should check `direction !== undefined` before relying on\nthe swap.\n\n## Form association\n\nA Lexical editor inside a [form-associated custom\nelement](https://developer.mozilla.org/docs/Web/API/Web_components#form-associated_custom_elements)\nparticipates in a `<form>` like any built-in form control: submission carries\nthe editor's value, form reset clears it, and validation flows through. The\nsketch:\n\n```js\nclass LexicalEditorElement extends HTMLElement {\n  static formAssociated = true;\n\n  constructor() {\n    super();\n    this.internals = this.attachInternals();\n  }\n\n  get value() {\n    return this.editor\n      ? JSON.stringify(this.editor.getEditorState().toJSON())\n      : '';\n  }\n\n  set value(serialized) {\n    if (this.editor && serialized !== '') {\n      this.editor.setEditorState(this.editor.parseEditorState(serialized));\n    }\n  }\n\n  connectedCallback() {\n    // `delegatesFocus: true` routes `host.focus()` (and implicit focus\n    // from `<label for=\"...\">`) to the first focusable element inside\n    // the shadow root — for the editor that's the contentEditable.\n    const shadow =\n      this.shadowRoot ?? this.attachShadow({delegatesFocus: true, mode: 'open'});\n    const contentEditable = document.createElement('div');\n    contentEditable.contentEditable = 'true';\n    contentEditable.tabIndex = 0;\n    shadow.appendChild(contentEditable);\n\n    const editor = buildEditorFromExtensions(\n      defineExtension({name: 'lexical-editor-element'}),\n    );\n    editor.setRootElement(contentEditable);\n    this.editor = editor;\n\n    // Seed the form value so a submit before the user types still produces\n    // a non-empty serialized state, mirroring `<input value=\"...\">`.\n    this.internals.setFormValue(this.value);\n\n    editor.registerUpdateListener(({dirtyElements, dirtyLeaves}) => {\n      // Only fire on real content changes, not on pure selection updates —\n      // matches HTMLInputElement's `input` event contract.\n      if (dirtyElements.size === 0 && dirtyLeaves.size === 0) return;\n      this.internals.setFormValue(this.value);\n      this.dispatchEvent(new Event('input', {bubbles: true, composed: true}));\n    });\n  }\n}\n```\n\nWithout the dirty-leaf/element gate every caret move would wake the form's\n`oninput` listeners, so the `input` event matches `HTMLInputElement`'s\ncontract only after that guard. The initial `setFormValue` is what keeps a\nform submission before the user types from carrying an empty value, since\nthe update listener has not fired yet at that point. A full reference\nimplementation lives in\n[`examples/shadow-dom-web-component`](https://github.com/facebook/lexical/tree/main/examples/shadow-dom-web-component).\n\nDOM moves (re-parenting the host into a different `<form>` or list) trigger\n`disconnectedCallback` followed by `connectedCallback`, which rebuilds the\neditor against a fresh contentEditable. Round-trip the user's content the\nsame way `<input>` and `<textarea>` round-trip their `value` attribute:\ncache the serialized state in `disconnectedCallback` and replay it through\n`parseEditorState` on the next mount. The reference dev example uses a\n`pendingState` field for exactly this.\n\nThe same form-associated host can also implement the rest of the\n[form-associated callbacks](https://developer.mozilla.org/docs/Web/API/Web_components/Using_custom_elements#form-associated_callbacks):\n`formStateRestoreCallback(state, reason)` re-hydrates the editor on bfcache\nnavigation (`reason: 'restore'`) or form autocomplete restore\n(`reason: 'autocomplete'`); `formAssociatedCallback(form)` reacts to\nprogrammatic form moves; `formResetCallback()` clears the editor when the\nsurrounding form is reset; and `formDisabledCallback(isDisabled)` flips the\neditor's editable state when an ancestor `<fieldset disabled>` toggles. The\nserialized JSON `internals.setFormValue` published earlier is what comes\nback in the state-restore callback.\n\n## Common pitfalls\n\nMoving an existing editor into a shadow root exposes a handful of recurring\nmismatches between the DOM APIs you'd reach for and what they report.\n\n### Event retargeting\n\nListeners above the shadow boundary — `window`, `document`'s\n`selectionchange`, popups portaled to `document.body` — see `event.target`\nretargeted to the shadow host, not the actual clicked element. A\n`rootElement.contains(event.target)` gate therefore always rejects clicks\nthat came from the editor. Resolve through the composed path:\n\n```js\nconst target = getComposedEventTarget(event);\nif (target instanceof Node && rootElement.contains(target)) {\n  // click really did land inside the shadow-hosted editor\n}\n```\n\n### Focus probes\n\n`document.activeElement` reports the host when focus is inside an open\nshadow tree, so `document.activeElement === rootElement` is always false.\n`getActiveElement(rootElement)` reads `DocumentOrShadowRoot.activeElement`\nthrough `Node.getRootNode` instead, and `getActiveElementDeep` keeps\ndescending into nested shadow roots when an editor whose decorator embeds a\nweb component needs the innermost focused element.\n\n```js\nif (getActiveElement(rootElement) === rootElement) { ... }\n```\n\n### Outside-click handlers\n\nA typical dropdown registers `document.addEventListener('click', ...)` and\ncalls `setShowDropDown(false)` whenever `button.contains(event.target)` is\nfalse. From inside a shadow tree that check always fails (the target is the\nhost), so the dropdown closes on the very click that opened it. Compare\nagainst `getComposedEventTarget(event)` instead — the same fix the playground's\n`DropDown` and `@lexical/react`'s `DraggableBlockPlugin` use.\n\n### Drop hit-tests\n\n`document.caretRangeFromPoint` and the no-argument\n`document.caretPositionFromPoint` return the host when the pointer is over\nshadow content, so an image drop lands on the host rather than the textnode\nunder the cursor. `@lexical/clipboard`'s internal `caretFromPoint` helper switches to\n`caretPositionFromPoint(x, y, {shadowRoots})` when `rootElement` lives in a\nshadow tree, and verifies the returned offset node really did land inside\none of the requested shadow roots — engines that silently ignore the option\nfall through to the legacy paths.\n\n### Style mirror cleanup\n\nIf your shadow mount mirrors `<style>` / `<link>` nodes from `document.head`\nand watches for additions via `MutationObserver`, mirror removals too;\notherwise stylesheets removed by HMR or a runtime theme swap linger inside\nthe shadow. `ShadowDomWrapper` tracks an `original → clone` map so an\nupstream removal drops the corresponding clone, and on unmount it removes\nevery clone so React 18 StrictMode's double-mount can't accumulate\nduplicates through the persistent shadow root.\n\nDev-mode HMR has two paths that a `childList`-only observer misses.\nFirefox replaces a `<style>`'s text content in place, so the observer\nneeds `characterData + subtree` to see the mutation; Chrome and WebKit\nupdate CSS through the CSSOM (`style.sheet.replaceSync` / `insertRule`),\nwhich the DOM observer never sees at all. `ShadowDomWrapper` also\nsubscribes to Vite's `vite:afterUpdate` hook for an explicit resync of\nevery clone after each HMR pass — `import.meta.hot` is `undefined` in\nproduction builds, so the hook is a no-op outside dev.\n\n### Popover and dialog layout\n\nThe UA stylesheet defaults a closed\n[Popover](https://developer.mozilla.org/docs/Web/API/Popover_API) (or a\n`<dialog>` without `open`) to `display: none`. A base rule like\n`#my-popover { display: flex; ... }` overrides that and leaves the popover\nvisible after `hidePopover()`. Scope layout to the open state instead —\n`#my-popover:popover-open { display: flex; }` — so the closed state honors\nthe UA default. This is a popover-API gotcha, not a shadow-specific one,\nbut it surfaces here because floating UI anchored to a shadow-root\nselection (a format popover or link editor that reads coordinates through\n`getDOMSelectionRangeAndPoints`) is a common shadow integration pattern.\n\n### Observers across the shadow boundary\n\n`ResizeObserver`, `IntersectionObserver`, and `MutationObserver` observe\nnodes inside an open shadow root with no special configuration — pass the\ninner node (the contentEditable or the host) just like any other DOM\ntarget. A `MutationObserver` registered on the host does not see mutations\ninside the shadow tree, though; observe the contentEditable or a\nshadow-internal container instead. The host's `attributeChangedCallback`\ncovers the host's own attribute changes.\n\n## Migrating an existing light-DOM editor\n\nThe same code shape keeps working in a shadow root; migration is mostly\nabout removing pre-shadow workarounds. Direct selection reads\n(`Selection.anchorNode`, `Selection.getRangeAt(0)`,\n`document.querySelector('[contenteditable]')` used as an editor probe) route\nthrough `getDOMSelectionPoints` / `getDOMSelectionRange` /\n`getActiveElement`, and any `event.target` read above the shadow boundary\nbecomes `getComposedEventTarget(event)`. CSS variables inherit through the\nshadow boundary but class declarations don't, so the editor/theme CSS has\nto be adopted or cloned into the shadow root.\n\nThe focus and popup paths are the ones that surprise people in review:\noutside-click handlers, blur-then-refocus, and \"did the editor lose focus?\"\nprobes usually need `getActiveElement(rootElement)` rather than\n`document.activeElement`; floating UI that portals into `document.body` and\nresolves hover with `element.getRootNode().elementFromPoint(x, y)` keeps the\nsame call shape but has to guard the popup root narrowing against a\ndetached popup whose `getRootNode()` returns itself.\n\n## Examples\n\nRunnable examples live in the repository:\n\n- [`examples/shadow-dom`](https://github.com/facebook/lexical/tree/main/examples/shadow-dom)\n  — a React editor inside a shadow root with a light‑DOM toolbar.\n- [`examples/shadow-dom-web-component`](https://github.com/facebook/lexical/tree/main/examples/shadow-dom-web-component)\n  — a framework‑free `<lexical-editor>` custom element, form‑associated via\n  `ElementInternals`. The page also mounts one instance inside a wrapper\n  `<div>` that opens its own shadow root, exercising the multi‑level\n  walk through two nested shadow boundaries.\n- [`examples/vanilla-js-iframe`](https://github.com/facebook/lexical/tree/main/examples/vanilla-js-iframe)\n  — an editor rendered into an `<iframe>`.\n- The playground's **Render in Shadow DOM** setting toggles the same editor\n  between light and shadow mounts, useful for spot-checking your plugins\n  against both modes.","frontmatter":{"sidebar_label":"Shadow DOM and iframes"},"isInternal":false,"tokens":4273,"sizeBytes":18976},{"name":"transforms.md","path":"packages/lexical-website/docs/concepts/transforms.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-website/docs/concepts/transforms.md","title":"Concepts Skill","category":"anthropic-skill","format":"markdown","content":"\n\n# Node Transforms\n\nTransforms are the most efficient mechanism to respond to changes to the EditorState.\n\nFor example:\nUser types a character and you want to color the word blue if the word is now equal to \"congrats\".\nWe programmatically add an `@Mention` to the editor, the `@Mention` is immediately next to another `@Mention` (`@Mention@Mention`). Since we believe this makes mentions hard to read, we want to destroy/replace both mentions and render them as plain TextNode's instead.\n\n```js\nconst removeTransform = editor.registerNodeTransform(TextNode, (textNode) => {\n  if (textNode.getTextContent() === 'blue') {\n    textNode.setTextContent('green');\n  }\n});\n```\n\n## Syntax\n\n```typescript\neditor.registerNodeTransform<T extends LexicalNode>(klass: Klass<T>, listener: Transform<T>): () => void\n```\n\n## Lifecycle\n\nTransforms are executed sequentially before changes are propagated to the DOM and multiple transforms still lead to a single DOM reconciliation (the most expensive operation in Lexical's lifecycle).\n\n```mermaid\nflowchart LR\n  accTitle: Transforms lifecycle\n  accDescr: A keyboard event leads to an update function. Its changes mark nodes dirty, and the transforms registered for those nodes run one after another. If a transform changed more nodes, the transforms for those nodes run again. When nothing is left dirty, the DOM is reconciled once and the update listeners run.\n  event{{\"Keyboard event\"}} --> update[\"Update function\"]\n  update --> t1[\"transform1\"]\n  t1 --> t2[\"transform2\"]\n  t2 --> dirty{\"Nodes still dirty?\"}\n  dirty -->|\"yes\"| t1\n  dirty -->|\"no\"| reconcile[\"DOM reconciled once\"]\n  reconcile --> listeners[\"Update listeners\"]\n```\n\n:::caution Beware!\n\nWhile it is possible to achieve the same or very similar result through an [update listener](listeners.md#registerupdatelistener) followed by an update, this is highly discouraged as it triggers an additional render (the most expensive lifecycle operation).\n\nAdditionally, each cycle creates a brand new `EditorState` object which can interfere with features like undo/redo (`HistoryExtension`) if not handled correctly.\n\n```js\neditor.registerUpdateListener(() => {\n  editor.update(() => {\n    // Don't do this\n  });\n});\n```\n\n:::\n\n### Dirty Nodes\n\n*Dirty Leaves* (any LexicalNode that is not an ElementNode) and *Dirty Elements*\nare tracked separately to support the transform heuristic.\n\nInternally, there are two states for dirty nodes:\n* Intentionally Dirty nodes (leaves or elements) had `node.getWritable()` or\n  `node.markDirty()` (an alias) called on them. Maintaining the\n  tree-of-doubly-linked-lists structure of a lexical document requires that\n  this Intentionally Dirty state will propagate to immediate siblings and in\n  some cases the parent node.\n* Unintentionally Dirty Element nodes are an ancestor of a\n  dirty node that were not explicitly marked as Intentionally Dirty\n  Only elements can be Unintentionally Dirty, because\n  leaves by definition can not have children.\n\nThe reconciler works by starting at the RootNode (which is the ancestor\nof any attached node, and thus always dirty whenever any attached node\nis dirty). Intentionally Dirty nodes have their createDOM and/or updateDOM\ncalled. Dirty Elements reconcile all of their children. Thus, reconciliation\nstops at the highest node in a subtree that has no dirty nodes (unless it\nis running with a flag to do a full reconciliation which considers all\nnodes as Intentionally Dirty).\n\n:::info\n\nThe transform heuristic depends on these internal implementation details to\nfind a fixed point where no more transforms are required.\n\n:::\n\n### Transform heuristic\n\n1. We transform leaves first. If transforms generate additional dirty nodes we repeat `step 1`. The reasoning behind this is that marking a leaf as dirty marks all its parent elements as dirty too.\n2. We transform elements.\n    - If element transforms generate additional dirty nodes we repeat `step 1`.\n    - If element transforms only generate additional dirty elements we only repeat `step 2`.\n\nA node will be marked as dirty on any (or most) modifications done to it, its children, or siblings in certain cases.\n\n## Preconditions\n\nPreconditions are fundamental for transforms to prevent them from running multiple times and ultimately causing an infinite loop.\n\nTransforms are designed to run when nodes have been modified (aka Intentionally Dirty). For the most part, transforms only need to run once after the update but the sequential nature of transforms makes it possible to have order bias. Hence, transforms are run over and over until this particular type of Node is no longer marked as intentionally dirty by any of the transforms.\n\nHence, we have to make sure that the transforms do not mark the node dirty unnecessarily.\n\n```js\n// When a TextNode changes (marked as dirty) make it bold\neditor.registerNodeTransform(TextNode, textNode => {\n  // Important: Check current format state\n  if (!textNode.hasFormat('bold')) {\n    textNode.toggleFormat('bold');\n  }\n});\n```\n\nBut oftentimes, the order is not important. The below would always end up in the result of the two transforms:\n\n```js\n// Extension 1\neditor.registerNodeTransform(TextNode, textNode => {\n  // This transform runs twice but does nothing the first time because it doesn't meet the preconditions\n  if (textNode.getTextContent() === 'modified') {\n    textNode.setTextContent('re-modified');\n  }\n})\n// Extension 2\neditor.registerNodeTransform(TextNode, textNode => {\n  // This transform runs only once\n  if (textNode.getTextContent() === 'original') {\n    textNode.setTextContent('modified');\n  }\n})\n// App\neditor.registerUpdateListener(({editorState}) => {\n  const text = editorState.read(() => $getRoot().getTextContent());\n  // text === 're-modified'\n});\n```\n\n:::info\n\nThe transform heuristic considers `RootNode` to be Intentionally Dirty whenever\nany node is dirty, and it ensures that this transform is applied last after all\nother transforms. In this way, you can consider it a \"pre-update\" listener,\nwhich occurs before any DOM reconciliation has happened. As with any other\ntransform, it may get called multiple times in a given update (especially if\nyour RootNode transform makes any node dirty).\n\n:::\n\n## Transforms on parent nodes\n\nTransforms are very specific to a type of node. This applies to both the declaration (`registerNodeTransform(ImageNode)`) and the times it triggers during an update cycle.\n\n```js\n// Won't trigger\neditor.registerNodeTransform(ParagraphNode, () => {});\n// Will trigger as TextNode was marked dirty\neditor.registerNodeTransform(TextNode, () => {});\neditor.update(() => {\n  const textNode = $getNodeByKey('3');\n  textNode.setTextContent('foo');\n});\n```\n\nWhile the marked dirty rule is always true, there are some cases when it's not immediately obvious and/or we force nearby nodes to become dirty for the sake of easier transform logic:\nYou add a node to an ElementNode, the ElementNode and the newly added children are marked dirty, also its new immediate siblings\nYou remove a node, its parent is marked dirty, also the node's immediate siblings prior to being removed\nYou move a node via `replace`, rules 2 and 1 are applied.\n\n```js\neditor.registerNodeTransform(ParagraphNode, paragraph => {\n // Triggers\n});\neditor.update(() => {\n  const paragraph = $getRoot().getFirstChild();\n  paragraph.append($createTextNode('foo'));\n});\n```\n\n## registerLexicalTextEntity\n\nIt is common to have certain nodes that are created/destroyed based on their text content and siblings. For example, `#lexical` is a valid hashtag whereas `#!lexical` is not.\n\nThis is a perfectly valid case for transforms but we have gone ahead and already built a utility transform wrapper for you for this specific case:\n\n```typescript\nregisterLexicalTextEntity<T extends TextNode>(\n  editor: LexicalEditor,\n  getMatch: (text: string) => null | EntityMatch,\n  targetNode: Klass<T>,\n  createNode: (textNode: TextNode) => T,\n): Array<() => void>;\n```\n\n## Examples\n\n1. [Emojis](https://github.com/facebook/lexical/blob/main/packages/lexical-playground/src/plugins/EmojisExtension/index.ts)\n2. [AutoLink](https://github.com/facebook/lexical/blob/main/packages/lexical-playground/src/plugins/AutoLinkExtension/index.ts)\n3. [HashtagExtension](https://github.com/facebook/lexical/blob/main/packages/lexical-hashtag/src/LexicalHashtagExtension.ts)\n","isInternal":false,"tokens":1851,"sizeBytes":8352},{"name":"traversals.md","path":"packages/lexical-website/docs/concepts/traversals.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-website/docs/concepts/traversals.md","title":"Concepts Skill","category":"anthropic-skill","format":"markdown","content":"# Node Traversals with NodeCaret\n\nNodeCaret offers a unified and efficient way for traversing the document\ntree, making it much easier to correctly implement traversals and avoid\nedge cases around empty nodes and collapsed selections.\n\nThese new low-level functions were all designed to work together as a\nfully featured relatively lightweight API to use in the core to\nallow us to gradually address some edge cases and then simplify and shrink\nthe code. We expect higher-level utilities to be developed and shipped\nin @lexical/utils or another module at a later date. The current overhead\nshould be less than 3kB in a production environment.\n\nThe NodeCaret API was introduced in lexical v0.25.0.\n\n## Concepts\n\nThe core concept with `NodeCaret` is that you can represent any specific\npoint in the document by using an `origin` node, a `direction` that\npoints towards an adjacent node (`next` or `previous`), and a `type`\nto specify whether the arrow points towards a sibling (`sibling`) or\ntowards a child (`child`).\n\nAll of these types have a `D` type parameter that must be a `CaretDirection`, so you\ncan not accidentally mix up `next` and `previous` carets. Many of them\nalso have a `T` type parameter that encodes the type of the `origin` node.\n\n:::tip\n\nThe methods of a caret are designed to operate on nodes attached to the `origin`\nin the designated direction, not the `origin` itself. For example, this code is\na no-op because it will attach a node to the `origin`, and then remove the node\nthat was just attached.\n\n```ts\n// The origin is unaffected (other than being marked dirty)\ncaret.insert($createTextNode('no-op')).remove();\n```\n\n:::\n\n:::warning\n\nCarets are immutable, and designed for low-level usage. There is no attempt\nfor carets to automatically update based on changes to the document\n(this is a common source of bugs when working with `RangeSelection`).\nFunctions and methods that work with carets and are expected to change the\nstructure of the document will always return a possibly new caret.\n\nThe `origin` of a caret is the exact version of the object that it was\nconstructed with, all accessor methods on that origin will generally call\n`origin.getLatest()` so the operations will see the latest version.\n\n:::\n\n### NodeCaret\n\n`NodeCaret` is any `SiblingCaret` or any `ChildCaret`\n* Typically constructed with `$getChildCaretOrSelf($getSiblingCaret(origin, direction))`\n  which returns a `ChildCaret` when the origin is an `ElementNode`\n\n:::tip\n\nThis type does not include `TextPointCaret` or `TextPointCaretSlice`,\nso you will not have to consider those edge cases when you see this\nmore specific type.\n\n:::\n\n### SiblingCaret\n\n`SiblingCaret` is a caret that points towards a sibling of the origin\n\n* Constructed with `$getSiblingCaret(origin: LexicalNode, direction: CaretDirection)`\n* The `next` direction points towards the right\n* The `previous` direction points towards the left\n\n|                        | → direction: `'next'`      | ← direction: `'previous'`     |\n|------------------------|---------------------------|-------------------------------|\n| `getParentAtCaret()`   | `origin.getParent()`      | `origin.getParent()`          |\n| `getNodeAtCaret()`     | `origin.getNextSibling()` | `origin.getPreviousSibling()` |\n| `insert(node)`         | `origin.insertAfter(node)`| `origin.insertBefore(node)`   |\n\n### ChildCaret\n\n`ChildCaret` is a caret that points towards the first or last child of the origin\n\n* Constructed with `$getChildCaret(origin: ElementNode, direction: CaretDirection)`\n* The `next` direction points towards the first child\n* The `previous` direction points towards the last child\n\n|                        | ↘ direction: `'next'`      | ↙ direction: `'previous'`     |\n|------------------------|----------------------------|-------------------------------|\n| `getParentAtCaret()`   | `origin`                   | `origin`                      |\n| `getNodeAtCaret()`     | `origin.getFirstChild()`   | `origin.getLastChild()`       |\n| `insert(node)`         | `origin.splice(0, 0, node)`| `origin.append(node)`         |\n\n### PointCaret\n\n`PointCaret` is any `TextPointCaret`, `SiblingCaret` or `ChildCaret`. This\ntype can be used to represent any point in the document that `PointType` can represent.\n\n### TextPointCaret\n\n`TextPointCaret` is basically a `SiblingCaret` with a `TextNode` origin and an `offset` property\n* Constructed with `$getTextPointCaret(origin, direction, offset)`\n* The `offset` property is an absolute index into the string\n* The `next` direction implies all text content after `offset`\n* The `previous` direction implies all text content before `offset`\n* All methods that are also present on `SiblingCaret` behave in the same way\n\n### TextPointCaretSlice\n\n`TextPointCaretSlice` is a wrapper for `TextPointCaret` that provides a signed `distance`.\n\n* Constructed with `$getTextPointCaretSlice(caret, distance)`\n* `$splitTextPointCaretSlice(slice, selection?)` isolates the slice in a text node,\n  splitting at its boundaries when needed. It returns `null` for an empty slice\n  and the original node for a whole-node slice. Pass the range selection that\n  produced the slice to preserve its endpoints, including element points and\n  detached selections. Like `TextNode.splitText`, it does not treat token or\n  segmented nodes as atomic.\n* There are convenience methods like `removeTextSlice()` and `getTextContent()`,\n  so it's not generally necessary to know the implementation details here\n* `Math.min(caret.offset, caret.offset + distance)` refers to the start offset of the slice\n* `Math.max(caret.offset, caret.offset + distance)` refers to the end offset of the slice\n* The `direction` of the caret is generally ignored when working with a\n  `TextPointCaretSlice`, the slice is in absolute string coordinates\n\n:::info\n\nThe property name `distance` was chosen because `length` and `size` are\ncommonly used on other data structures in JavaScript and Lexical, and they\nare overwhelmingly non-negative. While most uses of `distance` are also\nnon-negative, in some contexts such as computer graphics it is not uncommon\nto use\n[Signed distance functions](https://en.wikipedia.org/wiki/Signed_distance_function)\nwhere the distance metric is signed.\n\nIn SDF terms, the subset of the space is `[offset, ∞)`. Any coordinate less\nthan the `offset` boundary is a negative distance; otherwise the distance is\nnon-negative.\n\n:::\n\n### CaretRange\n\n`CaretRange` contains a pair of `PointCaret` that are in the same direction. It\nis equivalent in purpose to a `RangeSelection`, and is what you would generally\nuse for depth first traversals.\n\n* Constructed with `$getCaretRange(anchor, focus)`, `$caretRangeFromSelection(selection)`,\n  or `$extendCaretToRange(anchor)`\n* The `anchor` is the start of the range, generally where the selection originated,\n  and it is \"anchored\" in place because when a selection grows or shrinks only the\n  `focus` will be moved\n* The `focus` is the end of the range, where the blinking cursor is, it's the current\n  focus of the user\n* Anchor and focus must point in the same direction. The `anchor` points towards the first\n  node *in the range* and the focus points towards the first node *not in the range*\n* The `getTextSlices()` method is essential to handle the literal edge cases where\n  the anchor and/or focus are a `TextPointCaret`. These edges are *not* included\n  in the default caret iteration of the `CaretRange`.\n\n:::warning\n\nIf you are iterating a `CaretRange` you must consider the `getTextSlices()`\nseparately, they are not included in the iteration. This is so you don't have\nto consider `TextPointCaretSlice` at every step. They are literal edge cases\nthat can only be at the start and/or end and typically have special\ntreatment (splitting instead of removing, for example).\n\n:::\n\n## Traversal Strategies\n\n<!-- when you update the example code below, please update the tests in packages/lexical/src/caret/__tests__/unit/docs-traversals.test.ts -->\n\n### Adjacent Caret Traversals\n\nThe lowest level building block for traversals with NodeCaret is the adjacent caret\ntraversal, which is supported directly by methods of NodeCaret.\n\n`getAdjacentCaret()` - Gets a `SiblingCaret` for the node attached to\n  `origin` in direction. If there is no attached node, it will return `null`\n\n`getParentCaret(rootMode)` - Gets a `SiblingCaret` for the parent node\n  of `origin` in the same direction. If there is no parent node, or the parent\n  is a root according to `rootMode`, then it will return `null`. `rootMode`\n  may be `'root'` to only return `null` for `RootNode` or `'shadowRoot'` to\n  return `null` for `RootNode` or any `ElementNode` parent where\n  `isShadowRoot()` returns true\n\n`getChildCaret()` - Gets a `ChildCaret` for this origin, or `null` if the\n  origin is not an `ElementNode`. Will return `this` if the caret is already\n  a `ChildCaret`\n\nFor example, iterating all siblings:\n\n```ts\n// Note that NodeCaret<D> already implements Iterable<NodeCaret<D>> in this\n// way, so this function is not very useful. You can just use startCaret as\n// the iterable.\nfunction *$iterSiblings<D extends CaretDirection>(\n  startCaret: NodeCaret<D>\n): Iterable<SiblingCaret<LexicalNode, D>> {\n  // Note that we start at the adjacent caret. The start caret\n  // points away from the origin node, so we do not want to\n  // trick ourselves into thinking that that origin is included.\n  for (\n    let caret = startCaret.getAdjacentCaret();\n    caret !== null;\n    caret = caret.getAdjacentCaret()\n  ) {\n    yield caret;\n  }\n}\n```\n\n### Examples\n\nGiven the following document tree, here are some examples of using the\nadjacent node traversal:\n\nRoot\n* Paragraph A\n  * Text A1\n  * Link A2\n    * Text A3\n  * Text A4\n* Paragraph B\n  * Text B1\n* Paragraph C\n\n```ts\n// The root does not have sibling nodes\nconst carets = [...$getSiblingCaret($getRoot(), 'next')];\nexpect(carets).toEqual([]);\n```\n\n```ts\n// The adjacent node to a ChildNode is its first or last child\n// and is always a SiblingNode. It does not traverse deeper.\nconst carets = [...$getChildCaret($getRoot(), 'next')];\n\n// next starts at the first child\nexpect(carets).toEqual([\n  $getSiblingCaret(paragraphA, 'next'),\n  $getSiblingCaret(paragraphB, 'next'),\n  $getSiblingCaret(paragraphC, 'next'),\n]);\n\n// previous starts at the last child\nconst prevCarets = [...$getChildCaret($getRoot(), 'previous')];\nexpect(prevCarets).toEqual([\n  $getSiblingCaret(paragraphC, 'previous'),\n  $getSiblingCaret(paragraphB, 'previous'),\n  $getSiblingCaret(paragraphA, 'previous'),\n]);\n```\n\n```ts\n// The iteration starts at the node where the head of the \"arrow\"\n// is pointing, which is away from the origin (the tail of the \"arrow\").\nconst carets = [...$getSiblingCaret(paragraphB, 'next')];\nexpect(carets).toEqual([\n  $getSiblingCaret(paragraphC, 'next'),\n]);\n\nconst prevCarets = [...$getSiblingCaret(paragraphB, 'previous')];\nexpect(prevCarets).toEqual([\n  $getSiblingCaret(paragraphA, 'previous'),\n]);\n```\n\n### Depth First Caret Traversals\n\nThe strategy to do a depth-first caret traversal is to use an adjacent caret\ntraversal and immediately use a `ChildCaret` any time that an `ElementNode`\norigin is encountered. This strategy yields all possible carets, but each\nElementNode in the traversal may be yielded once or twice (a `ChildCaret` on\nenter, and a `SiblingCaret` on leave). Allowing you to see whether an\n`ElementNode` is partially included in the range or not is one of the\nreasons that this abstraction exists.\n\n```ts\nfunction *$iterCaretsDepthFirst<D extends CaretDirection>(\n  startCaret: NodeCaret<D>\n): Iterable<NodeCaret<D>> {\n  function step(prevCaret: NodeCaret<D>): null | NodeCaret<D> {\n    // Get the adjacent SiblingCaret\n    const nextCaret = prevCaret.getAdjacentCaret();\n    return (\n      // If there is a sibling, try and get a ChildCaret from it\n      (nextCaret && nextCaret.getChildCaret()) ||\n      // Return the sibling if there is one\n      nextCaret ||\n      // Return a SiblingCaret of the parent, if there is one\n      prevCaret.getParentCaret('root')\n    );\n  }\n  // You may add an additional check here, usually some specific\n  // caret to terminate the iteration with (such as the parent caret\n  // of startCaret):\n  //\n  //  `caret !== null || caret.is(endCaret)`\n  //\n  for (\n    let caret = step(startCaret);\n    caret !== null;\n    caret = step(caret)\n  ) {\n    yield caret;\n  }\n}\n```\n\nNormally this type of iteration would be done from a `CaretRange`, where you\nwould specify a precise end caret (focus).\n\n```ts\nfunction $iterCaretsDepthFirst<D extends CaretDirection>(\n  startCaret: NodeCaret<D>,\n  endCaret?: NodeCaret<D>,\n): Iterable<NodeCaret<D>> {\n  return $getCaretRange(\n    startCaret,\n    // Use the root as the default end caret, but you might choose\n    // to use startCaret.getParentCaret('root') for example\n    endCaret || $getSiblingCaret($getRoot(), startCaret.direction)\n  );\n}\n```\n\nTo get all nodes that are entirely selected between two carets:\n\n```ts\nfunction *$iterNodesDepthFirst<D extends CaretDirection>(\n  startCaret: NodeCaret<D>,\n  endCaret: NodeCaret<D> = $getChildCaret($getRoot(), startCaret.direction),\n): Iterable<LexicalNode> {\n  const seen = new Set<NodeKey>();\n  for (const caret of $getCaretRange(startCaret, endCaret)) {\n    const {origin} = caret;\n    if ($isChildCaret(caret)) {\n      seen.add(origin.getKey());\n    } else if (!$isElementNode(origin) || seen.has(origin.getKey())) {\n      // If the origin is an element and we have not seen it as a ChildCaret\n      // then it was not entirely in the CaretRange\n      yield origin;\n    }\n  }\n}\n```\n\n### Examples\n\nGiven the following document tree, here are some examples of using the\ndepth-first node traversal (with a `CaretRange`):\n\nRoot\n* Paragraph A\n  * Text A1\n  * Link A2\n    * Text A3\n  * Text A4\n* Paragraph B\n  * Text B1\n* Paragraph C\n\n```ts\n// A full traversal of the document from root\nconst carets = [...$getCaretRange(\n  // Start with the arrow pointing towards the first child of root\n  $getChildCaret($getRoot(), 'next'),\n  // End when the arrow points away from root\n  $getSiblingCaret($getRoot(), 'next'),\n)];\nexpect(carets).toEqual([\n  $getChildCaret(paragraphA, 'next'),   // enter Paragraph A\n  $getSiblingCaret(textA1, 'next'),\n  $getChildCaret(linkA2, 'next'),       // enter Link A2\n  $getSiblingCaret(textA3, 'next'),\n  $getSiblingCaret(linkA2, 'next'),     // leave Link A2\n  $getSiblingCaret(textA4, 'next'),\n  $getSiblingCaret(paragraphA, 'next'), // leave Paragraph A\n  $getChildCaret(paragraphB, 'next'),   // enter Paragraph B\n  $getSiblingCaret(textB1, 'next'),\n  $getSiblingCaret(paragraphB, 'next'), // leave Paragraph B\n  $getChildCaret(paragraphC, 'next'),   // enter Paragraph C\n  $getSiblingCaret(paragraphC, 'next'), // leave Paragraph C\n]);\n```\n\n## Future Direction\n\nIt's expected that higher-level abstractions will be built on top of this\noutside of the core, either in @lexical/utils or a separate companion package.\nThis is just designed to be the lowest-level layer with a consistent and\ntype-safe interface. That sort of abstraction will probably look a little bit\nlike cheerio or jQuery, but for working with Lexical documents. It is not\nexpected that more abstractions will be added to the core.\n\nIn order to reduce code size and eliminate bugs, more of the core will be\nrefactored to use NodeCaret internally.\n\nOnce this happens, it's possible that the internal structure of PointType\nand/or RangeSelection may change to accommodate NodeCaret, as it is more\nresilient to document changes (only changes that directly affect the\norigin node will \"break\" the point). A simple version of this would be to\ncreate a caret any time that the point changes, and use that caret\nas a fallback if the selection would otherwise be lost.\n\nIt may be the case that NodeCaret will become the lowest level API, working\ndirectly with private LexicalNode/ElementNode internals. When/if that happens,\nthe methods on LexicalNode will remain for backwards compatibility,\nbut overriding them will not be supported. It isn't particularly safe to\noverride them as-is anyway, and these overrides are frequently the\nroot cause of bugs (e.g. parents that remove themselves after an operation\non a child, causing the point to be lost unless the caller was sophisticated\nenough to store the array of parents).\n\n## History\n\nBefore NodeCaret, Lexical's core API offered a relatively low-level DOM-like\ninterface for working with nodes and traversing them. It has accumulated\nmany functions over time for performing various kinds of traversals around\nthe tree (finding ancestors, children, depth, siblings, etc.), but most of\nthem are not implemented in a way that makes them easy to combine\nefficiently, and many of them have edge cases that are difficult to avoid\nand can't really be addressed without breaking compatibility.\n\nMany of these functions also have a lot of edge cases, particularly around\nassuming the reference nodes are inclusive. Many are also left-to-right\nbiased, don't offer an iterative version that can be aborted early or\nconsumed on the fly, etc.\n\nRefactoring many of these to use something like `PointType` would almost\nbe sufficient for many of these use cases, but the representation of\nthat type is inefficient and error-prone as any mutation to the tree\nrequires that each point be manually recomputed. `PointType` is also\ndirectionless, forcing a specific left-to-right bias into most APIs.\n`RangeSelection` can be used in many cases because a direction can\nbe inferred from any two different points, but that collapses with\na single point. It's also impractical to use `RangeSelection`\nconcurrently with mutations due to the problems with `PointType`.\n\nNodeCaret was born out of frustration with these APIs and a desire\nto unify it all in a coherent way to simplify and reduce errors in\nthe core.\n\n## Terminology\n\n### Caret\n\nThe term Caret was chosen because it is concise and specific term\nspecific to a point in a text document. Lexical is \"an extensible text editor\nframework\" so it makes sense that navigation in the document would use\nterms relevant to text. Most other terms such as Cursor or Point\nalready have meanings in Lexical and/or are less specific.\n\nSee also:\n- [Caret](https://developer.mozilla.org/en-US/docs/Glossary/Caret)\n- [Caret navigation](https://en.wikipedia.org/wiki/Caret_navigation)\n\n### Origin\n\nThe origin is the reference node for a NodeCaret. Absolute coordinates\nare determined by combining this origin node and an \"arrow\" that points\ntowards to where the adjacent node is (or could be). The \"arrow\" is\ndetermined by the `direction` and `type` of the caret.\n\nIn a way this \"arrow\" is considered to be something like a unit vector\nto indicate the direction, and adding it to an origin allows you to specify\nan absolute location relative to that origin. Unlike the other coordinate\nsystems available in Lexical, it does not need recomputing whenever\nsiblings or a parent changes, so long as the origin node is still attached.\n\nSee also:\n- [Origin](https://en.wikipedia.org/wiki/Origin_(mathematics))\n- [Unit Vector](https://en.wikipedia.org/wiki/Unit_vector)\n\n### ChildCaret / SiblingCaret\n\nThese were chosen because they match the existing methods on `ElementNode`\nand `LexicalNode` (`getFirstChild`, `getNextSibling`, etc.)\n\n### Direction\n\n`'next'` and `'previous'` were chosen for direction mostly to match the\nexisting methods such as `getNextSibling()` that exist in DOM and in Lexical.\nUsing other words such as `'left'` and `'right'` would be ambiguous since\ntext direction can be bidirectional and already uses the terms left-to-right\nand right-to-left.\n\n### Distance\n\nThe property name `distance` was chosen for `TextPointCaretSlice` because\n`length` and `size` are commonly used on other data structures in JavaScript\nand Lexical, and they are overwhelmingly non-negative. While most uses of\n`distance` are also non-negative, in some contexts such as computer graphics\nit is not uncommon to use\n[Signed distance functions](https://en.wikipedia.org/wiki/Signed_distance_function)\nwhere the distance metric is signed.\n\nIn SDF terms, the subset of the space is `[offset, ∞)`. Any coordinate less\nthan the `offset` boundary is a negative distance; otherwise the distance is\nnon-negative.\n","isInternal":false,"tokens":4941,"sizeBytes":20271},{"name":"design.md","path":"packages/lexical-website/docs/design.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-website/docs/design.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"# Lexical's Design\n\n> Note: this document is still a work-in-progress and things are likely still in flux.\n\nLexical was built from the ground up to be a lean and extensible text-editing framework.\n\nYou can think of Lexical as more of a text editing engine, rather than a monolithic text\neditor that provides everything out-of-the-box. It might help to think of Lexical as\na framework like React – where React provides some hooks and a reconciler, and then you\nbuild the things you need on top, rather than have them included by default.\n\nThe main concept of Lexical is the editor state. Lexical uses a double-buffering technique\nto ensure consistency and reliability. There are never more than two editor states in play.\nThe \"current\" editor state represents what you can visibly see on screen and the \"pending\"\neditor state is what is currently being constructed to be shown in the future. Once the\n\"pending\" editor state is ready, it swaps and becomes the new \"current\" editor state.\n\nLexical's core concerns itself with four main concerns:\n\n- Updates: the act of making changes to editor state\n- Node Transforms: the process of acting on ongoing updates\n- Reconciliation: the process of patching the DOM with the latest editor state\n- Listening/Commands: the process of reacting to changes that occur internally\n\nAdditionally, Lexical uses DOM mutation observers to ensure that any outside changes to\nthe editor DOM element are either reverted back to Lexical's current editor state, or are\ncommunicated as intents that cause further updates to the editor state (text changes).\n\nThis separation was done to ensure developers have a way of applying custom logic with the\nimplementation and framework of their choice. This makes it possible to use Lexical with\nReact, or any other JavaScript web framework/library. This also makes it easier to tackle\nproblems around collaboration and undo/redo, where you might have an alternative model involved.\nInstead of hoping for the text editor to support what you want, you can work with the editor\nto make what you want.\n\nThis design can make getting started a bit more complex in certain cases, but as we've been\nable to demonstrate with our `@lexical/react` plugins and hooks – you can get started, in the\nform of an out-of-the-box experience.\n","isInternal":false,"tokens":479,"sizeBytes":2294},{"name":"error.md","path":"packages/lexical-website/docs/error.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-website/docs/error.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"---\nid: error\ntitle: Error Code\n---\nimport ErrorCodePage from '@site/src/components/ErrorCodePage';\n\n# Error Code\n<ErrorCodePage/>","frontmatter":{"id":"error","title":"Error Code"},"isInternal":false,"tokens":31,"sizeBytes":130},{"name":"defining-extensions.md","path":"packages/lexical-website/docs/extensions/defining-extensions.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-website/docs/extensions/defining-extensions.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"# Defining Extensions\n\nAn extension is an plain JavaScript object that complies with the\n[LexicalExtension](/docs/api/modules/lexical#lexicalextension) interface.\nThe [defineExtension](/docs/api/modules/lexical#defineextension) function\nis merely an identity function to make TypeScript inference convenient.\nIt will be compiled down to the following, and likely optimized out by your\nbundler or minifier:\n\n```js\nfunction defineExtension(extension) {\n  return extension;\n}\n```\n\nExtensions *must* be stable references. It is most useful to define them at\nmodule scope, but if for some reason you have to define it inside of a\nReact component you'll need some way to make it stable such as with\n`useState`, `useMemo`, or `useRef` as appropriate.\n\n## Required Properties\n\n### name\n\nThe only required property for an extension is `name`. The name is a string\nthat should be unique to that extension your editor. It is best practice to\nuse a scope for your project or organization to ensure that reusable\nextensions don't conflict with each other.\n\nThe general practice in the lexical repository is to use the name of the\npackage as the extension name when it only makes sense to export a single\nextension, (e.g. `DragonExtension` has the name `@lexical/dragon`), or\nwith an additional path if multiple extensions are exported (e.g.\n`AutoFocusExtension` has the name `@lexical/extension/AutoFocus`).\n\n## InitialEditorConfig\n\nExtensions can specify configuration overrides for the editor, these are\ndefined by the\n[InitialEditorConfig](/docs/api/modules/lexical#initialeditorconfig)\ninterface. See the API docs for the full list of properties. Every property\nhere has some default.\n\n### Merged properties\n\nFor an extension designed to be used as a dependency, it can also be useful\nto specify properties that are merged such as\n`html` (override HTML import/export),\n`nodes` (register new nodes or node overrides with the editor),\nor `theme` (specify classes to be used for nodes provided by lexical).\n\n```ts\nexport const CodeExtension = defineExtension({\n  name: '@lexical/code',\n  nodes: () => [CodeNode, CodeHighlightNode],\n});\n```\n\n### Root properties\n\nThere are other properties that are more useful for the \"root\" extension\nthat you provide to\n[buildEditorFromExtensions](/docs/api/modules/lexical_extension#buildeditorfromextensions)\nor the `extension` prop of\n[LexicalExtensionComposer](/docs/api/modules/lexical_react_LexicalExtensionComposer#lexicalextensioncomposer)\nwhere you would specify properties that can only be meaningfully set\nonce such as `$initialEditorState`, `editable`, `onError`, `namespace` or `parentEditor`.\n\nThere is no requirement that these properties be set from any specific level\nof the extension hierarchy, or at all, but it is only meaningful to set them\nonce per editor.\n\n```ts\nconst editor = buildEditorFromExtensions(\n  defineExtension({\n    name: \"@example/basic-rich-text-editor\",\n    namespace: \"basic-rich-text-editor\",\n    dependencies: [RichTextExtension],\n    register: (editor: LexicalEditor) => {\n      console.log(\"Editor Created\");\n      return () => console.log(\"Editor Disposed\");\n    },\n  }),\n);\n```\n\n## Extension Dependencies\n\nLexical Extensions have two ways for extensions to depend on and specify\nconfiguration for each other:\n\n- [dependencies](#dependencies) are direct (by reference)\n- [peerDependencies](#peerdependencies) are indirect (by name) and optional\n\nThe exception to this rule is with `peerDependencies`. These are optional\ndependencies\n\n### dependencies\n\nThe `dependencies` property is an array of extensions by reference that your\nextension depends on. For example, if your extension uses React then it should\ndepend on the `ReactExtension`.\n\nDependencies form a directed acyclic graph. This means that there must not\nbe any circular dependencies. If extension A depends on extension B then\nthere must not be any dependency path from B to A\n(or from A to A).\n\nThis array can contain either direct references to extensions, or calls to\n[configExtension](/docs/api/modules/lexical#configextension).\n\n```ts\nexport const ExampleExtension = defineExtension({\n  name: \"@example/extension\",\n  dependencies: [\n    SomeExtension,\n    configExtension(ReactExtension, {\n      decorators: [<ExampleDecorator />]\n    }),\n  ],\n});\n```\n\n### peerDependencies\n\nThe `peerDependencies` property is an array of optional extensions by name.\nThey are not requirements, but specifying them here allows your extension\nto find them at runtime and override configuration for them if they are\nbuilt with the editor. You can use\n[declarePeerDependency](/docs/api/modules/lexical#declarepeerdependency) to\nbuild this array with type inference.\n\nSince these are optional and by name, an extension may declare a peer it does\nnot import. The graph must still be acyclic: peer edges take part in the same\ntopological sort as `dependencies`, so two extensions that peer-depend on each\nother and are both present will fail to build.\n\nThis is rarely needed in practice and is considered an advanced use case,\ntypically to avoid a direct import or dependency loop. See\n[Peer Dependencies](/docs/extensions/peer-dependencies) for more detail.\n\n### conflictsWith\n\nThe `conflictsWith` property is an array of extensions by name that are\nknown to conflict with this extension. For example, it does not make\nsense to have\n[RichTextExtension](/docs/api/modules/lexical_rich-text#richtextextension) and\n[PlainTextExtension](/docs/api/modules/lexical_plain-text#plaintextextension)\nin the same editor.\n\n```ts\nexport const PlainTextExtension = defineExtension({\n  conflictsWith: ['@lexical/rich-text'],\n  dependencies: [\n    DragonExtension,\n    NormalizeInlineElementsExtension,\n    NormalizeTripleClickSelectionExtension,\n  ],\n  name: '@lexical/plain-text',\n  register: registerPlainText,\n});\n```\n\nThis is rarely needed in practice and is considered an advanced use case,\nbut it can provide helpful early errors when it is needed.\n\n## Phases\n\nThe construction of an editor using extensions is done in sequential phases,\nand the following properties are useful ways to hook into that process at\nspecific points.\n\n### config\n\n`config` is an object that is the default configuration for your extension,\nproperties of this object can be overridden by other extensions using\n[configExtension](/docs/api/modules/lexical#configextension) or\n[declarePeerDependency](/docs/api/modules/lexical#declarepeerdependency).\n\nThis object is used in later phases to build the init and/or output.\n\nThe config phase happens before the editor is constructed.\n\n### mergeConfig\n\n`mergeConfig` is a function that can be used to merge configuration\nwith its overrides if you need something more fine-grained than a\nshallow object merge, such as concatenating arrays. For example:\n\n```ts\ninterface StringArrayConfig {\n  array: string[];\n}\nconst StringArrayExtension = defineExtension({\n  config: safeCast<StringArrayConfig>({array: []}),\n  name: \"@example/StringArray\",\n  mergeConfig(a, b) {\n    const config = shallowMergeConfig(a, b);\n    if (b.array) {\n      config.array =\n        b.array.length > 0\n          ? [...a.array, ...b.array]\n          : a.array;\n    }\n    return config;\n  },\n});\n```\n\nThe default implementation is\n[shallowMergeConfig](/docs/api/modules/lexical#shallowmergeconfig),\nmost extensions will not need to override this.\n\n### init\n\nThe init phase happens before the editor is constructed, but after\nthe merged editor configuration is available and all extensions are\nconfigured. It can be used to reference configuration\nfrom peers, compute data that should be available during\n[build](#build), and is generally a last resort for making mutations\nto any extension or editor configuration before the editor is\ncreated.\n\nThe result of this function is available in later phases.\n\nThis is rarely needed in practice and is considered an advanced use case.\n\n### build\n\nThe build phase happens after the editor is constructed — it receives the\n`LexicalEditor` — but before [register](#register), and after\n[config](#config) and [init](#init). The return value of the `build`\nphase is `output` and is available for later phases.\n\n`output` is generally how extensions provide functionality to each\nother and to the nodes in your application. A very common use case is\nto build signals from configuration with `namedSignals` so that the\nbehavior of your extension can be modified at runtime (e.g. `disabled`\nis a very common use case).\n\nOther use cases for build's `output` are to provide\nshared data structures, typed theme configuration, references to the\ncommands that the extension implements, etc.\n\n```ts\nexport interface TabIndentationConfig {\n  disabled: boolean;\n  maxIndent: null | number;\n  /**\n   * By default, indents are set on all elements for which the\n   * {@link ElementNode.canIndent} returns true. This option allows you to set\n   * indents for specific nodes without overriding the method for others.\n   */\n  $canIndent: CanIndentPredicate;\n}\nexport const TabIndentationExtension = defineExtension({\n  build(editor, config, state) {\n    return namedSignals(config);\n  },\n  config: safeCast<TabIndentationConfig>({\n    $canIndent: $defaultCanIndent,\n    disabled: false,\n    maxIndent: null,\n  }),\n  name: '@lexical/extension/TabIndentation',\n  register(editor, config, state) {\n    const {disabled, maxIndent, $canIndent} = state.getOutput();\n    return effect(() => {\n      if (!disabled.value) {\n        return registerTabIndentation(editor, maxIndent, $canIndent);\n      }\n    });\n  },\n});\n```\n\n### register\n\nThis happens after the editor has been constructed. This is where you will\nregister any commands, listeners, etc. that your extension needs. It can use\nthe result of `init` or `build` via `state.getInitResult()` and\n`state.getOutput()` respectively.\n\nThe return value is a dispose function, typically the result of `mergeRegister`.\n\n### afterRegistration\n\nThis happens after [register](#register) for every extension has been called,\nso all commands should be registered. This is the phase when the\n`$initialEditorState` is applied to the editor by `InitialStateExtension`,\nso `editor.setRootElement` should be called no sooner than this phase (it may\nof course be called after the editor is built, outside of extensions).\n\nThe return value is a dispose function, typically the result of\n`mergeRegister`.\n","isInternal":false,"tokens":2269,"sizeBytes":10354},{"name":"design.md","path":"packages/lexical-website/docs/extensions/design.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-website/docs/extensions/design.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"# Design Doc\n\nSee also the initial PR [#7706](https://github.com/facebook/lexical/pull/7706)\n\n## Guiding principles\n\n### Minimize boilerplate\n\nWherever possible, sensible defaults should be provided and it must be\npossible to override them. This is why a `Extension` has the `config` property\nwhich specifies the initial configuration.\n\n### Full backwards compatibility\n\nAlmost anything that you do with the editor or a legacy React plug-in should\njust work. Upgrading to use a `Extension` should be a simple change.\n\n### Minimize change required to add an Extension\n\nIdeally, adding an extension should be two or three lines of code:\n\n- Import it\n- Add it as a dependency (with config if necessary)\n- Add UI related code (only if needed, e.g. has to be rendered in a specific place in the DOM)\n\n### Avoid invalid states\n\nTypeScript goes a long way here, but it's only as good as the API design.\nFor example, splitting up the workflow for an Extension to have separate `Config`,\n`Init` and `Output` types was something that evolved over time. In earlier\nprototypes, `Config` was overloaded to maintain all of that state, but it\nrequired a lot more runtime support and type assertions to check that\nthe state was properly initialized for that phase.\n\n### Try to avoid undecipherable type errors\n\nThere's only so much that can be done to make readable error messages\nout of complex type expressions, so the API is designed to avoid them\nwhen possible. See also [microsoft/TypeScript#23689](https://github.com/microsoft/TypeScript/issues/23689).\n\n## Inspiration\n\n### [ESLint Flat Plugins](https://eslint.org/docs/latest/extend/plugin-migration-flat-config)\n\nThese have many of the same constraints as this project, although of course\nare used for a very different purpose\n\n### [MDXEditor](https://mdxeditor.dev/)\n\nThis editor is based on Lexical, but provides its own mechanism for plugins\nbased on a third party state management library. These plugins don't\nreally expose any type-safe configuration.\n\n### [@payloadcms/richtext-lexical](https://www.npmjs.com/package/@payloadcms/richtext-lexical)\n\nAnother Lexical based editor framework for use inside the\n[Payload](https://payloadcms.com/) CMS. It also doesn't really expose type-safe\nconfiguration, but it does seem to have quite a lot of features and seems\nwell-designed for laziness and SSR. Features are conceptually similar to Extensions.\n\n### [Effect](https://effect.website/)\n\nThis library is pretty much state of the art for type safety with a focus on\ncomposition and usability.\n\n### Tighter integration with LexicalEditor\n\nThis package was originally prototyped as an entirely optional feature,\nas adoption increases it's expected that more of Lexical will directly\ndepend on it.\n\n## Intentional trade-offs\n\n### No compile-time support for dependency resolution\n\nThe current theory is that it would require too much TypeScript\nin order to carry around the list of all dependency names that\nexist in the graph, and would likely add another type argument\nto `LexicalExtension`.\n\nThe features that this blocks are:\n\n- Compile-time support for detecting Extension conflicts. Detecting that two\n  extensions defined with the same name (but are not identical object\n  references, because a dependency can be shared!) would also be quite the\n  undertaking in TypeScript. This is automatically detected at runtime by\n  the builder.\n- Compile-time support for required configuration without defaults.\n  An extension can implement this at runtime in `init` or `register`.\n- Compile-time support for required peer dependencies. A use case for this\n  would be the requirement of a `ReactProviderExtension` provided by either\n  `LexicalExtensionComposer` or `ReactPluginHostExtension`. An extension can implement this\n  at runtime in `init` or `register`.\n\nGenerally speaking, all of these are already surfaced as runtime errors\nwhile building the editor with sufficient information to quickly track\ndown the root cause.\n\n## Known Missing Features\n\n### Direct support for devtools\n\nThis is a TODO, the infrastructure was designed with this in mind.\n\n### Documented patterns for RSC/SSR/Headless\n\nHaving a known peer dependency that is used to declare SSR may help\nguide future Extension development. In many cases there are decorator nodes\nthat have React dependencies that you do not want to (or can not) render\nin an SSR context. For example, RSC can not be supported anywhere that\n`React.Context` is used which is everywhere that React is currently used\nin Lexical.\n\nAnother option would be to build separate entrypoints for the SSR use case,\nwhich is more of an RSC specific strategy, but we could encourage people\nto use those import conditions even in a Vanilla JS or some other bespoke\nSSR use case. That strategy may make it hard to support including both\n\"headless\" and \"non-headless\" in the same module.\n","isInternal":false,"tokens":1049,"sizeBytes":4860},{"name":"included-extensions.md","path":"packages/lexical-website/docs/extensions/included-extensions.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-website/docs/extensions/included-extensions.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"# Included Extensions\n\n[@lexical/a11y](/docs/api/modules/lexical_a11y)\n\nFramework-agnostic accessibility extensions. See [Keyboard Accessibility](/docs/concepts/keyboard-accessibility) for the full contract and the matching `@lexical/react` adapters.\n\n- [AriaLiveRegionExtension](/docs/api/modules/lexical_a11y#arialiveregionextension) - Owns a visually hidden `aria-live` region and exposes a stable `announce` sink (WAI-ARIA status messages, WCAG 4.1.3)\n- [EditorModeAnnounceExtension](/docs/api/modules/lexical_a11y#editormodeannounceextension) - Announces editable / read-only transitions through the live region\n- [FocusManagerExtension](/docs/api/modules/lexical_a11y#focusmanagerextension) - Editor ↔ toolbar focus jump (Alt+F10 to the toolbar, Escape back to the editor), reference-counted per toolbar\n- [FocusTrapExtension](/docs/api/modules/lexical_a11y#focustrapextension) - Traps Tab / Shift+Tab focus inside one or more containers (modal dialogs), reference-counted per container\n- [HistoryAnnounceExtension](/docs/api/modules/lexical_a11y#historyannounceextension) - Announces undo / redo through the live region\n- [RovingTabIndexExtension](/docs/api/modules/lexical_a11y#rovingtabindexextension) - WAI-ARIA roving-tabindex pattern for toolbars / groups, reference-counted per container\n\n[@lexical/clipboard](/docs/api/modules/lexical_clipboard)\n\n- [ClipboardDOMImportExtension](/docs/api/modules/lexical_clipboard#clipboarddomimportextension) - Routes `text/html` pastes and drops through the `DOMImportExtension` pipeline (experimental)\n- [ClipboardImportExtension](/docs/api/modules/lexical_clipboard#clipboardimportextension) - Configurable per-MIME-type clipboard import for paste and drop (experimental)\n- [GetClipboardDataExtension](/docs/api/modules/lexical_clipboard#getclipboarddataextension) - Configurable serialization of the selection into clipboard MIME types for copy and drag\n\n[@lexical/code](/docs/api/modules/lexical_code)\n\n- [CodeExtension](/docs/api/modules/lexical_code#codeextension) - CodeNode (code blocks)\n- [CodeIndentExtension](/docs/api/modules/lexical_code#codeindentextension) - CodeNode tab key indentation (code blocks)\n\n[@lexical/code-prism](/docs/api/modules/lexical_code-prism)\n\n- [CodePrismExtension](/docs/api/modules/lexical_code-prism#codeprismextension) - Highlighting with Prism for CodeNode\n\n[@lexical/code-shiki](/docs/api/modules/lexical_code-shiki)\n\n- [CodeShikiExtension](/docs/api/modules/lexical_code-shiki#codeshikiextension) - Highlighting with Shiki for CodeNode\n\n[@lexical/dragon](/docs/api/modules/lexical_dragon)\n\n- [DragonExtension](/docs/api/modules/lexical_dragon#dragonextension) - Dragon (speech to text) support, included by default with `RichTextExtension` and `PlainTextExtension`\n\n[@lexical/extension](/docs/api/modules/lexical_extension)\n\n- [AutoFocusExtension](/docs/api/modules/lexical_extension#autofocusextension) - Focus the editor when it is created (e.g. on page load)\n- [ClearEditorExtension](/docs/api/modules/lexical_extension#cleareditorextension) - Implementation for the `CLEAR_EDITOR_COMMAND`\n- [ClickAfterLastBlockExtension](/docs/api/modules/lexical_extension#clickafterlastblockextension) - Inserts a paragraph when clicking below a last block that can not accommodate the click (e.g. a DecoratorNode or TableNode)\n- [DecoratorTextExtension](/docs/api/modules/lexical_extension#decoratortextextension) - DecoratorTextNode support, sets the format and CSS classes for the DOM container\n- [EditorStateExtension](/docs/api/modules/lexical_extension#editorstateextension) - Provide EditorState as a signal (alternative to `registerUpdateListener`)\n- [HMRExtension](/docs/api/modules/lexical_extension#hmrextension) - Preserves editor state, selection, editability and undo/redo history across Hot Module Replacement (HMR) cycles\n- [HorizontalRuleExtension](/docs/api/modules/lexical_extension#horizontalruleextension) - HorizontalRuleNode (`<hr>` tag)\n- [IMEExtension](/docs/api/modules/lexical_extension#imeextension) - Centralizes IME composition state as signals\n- [InitialStateExtension](/docs/api/modules/lexical_extension#initialstateextension) - Sets the initial state of the editor (always included)\n- [KeyboardShortcutsExtension](/docs/api/modules/lexical_extension#keyboardshortcutsextension) - Dispatches a table of keyboard shortcuts from a single compiled `KEY_DOWN_COMMAND` listener; the table is merged across the extension graph by name (experimental)\n- [NestedEditorExtension](/docs/api/modules/lexical_extension#nestededitorextension) - Configures an editor as a nested editor of a parent editor (theme inheritance, optionally editable state)\n- [NodeSelectionExtension](/docs/api/modules/lexical_extension#nodeselectionextension) - Tracks selection, typically for DecoratorNodes\n- [NodeSelectionDataSelectedExtension](/docs/api/modules/lexical_extension#nodeselectiondataselectedextension) - Mirrors a node's `NodeSelection` membership onto its host DOM as a `data-selected` attribute so CSS can outline selected `ElementNode` hosts; configured per node type (experimental)\n- [NormalizeInlineElementsExtension](/docs/api/modules/lexical_extension#normalizeinlineelementsextension) - Removes empty inline elements, included by default with `RichTextExtension` and `PlainTextExtension`\n- [NormalizeTripleClickSelectionExtension](/docs/api/modules/lexical_extension#normalizetripleclickselectionextension) - Corrects over-selection after triple click events, included by default with `RichTextExtension` and `PlainTextExtension`\n- [PreventSelectAllExtension](/docs/api/modules/lexical_extension#preventselectallextension) - Prevents select all (Ctrl/Cmd+A) inside input/textarea elements from selecting the editor content, included by default with `SelectBlockExtension`\n- [RootElementExtension](/docs/api/modules/lexical_extension#rootelementextension) - Exposes the editor's root element as a reactive `Signal<HTMLElement | null>`\n- [SelectBlockExtension](/docs/api/modules/lexical_extension#selectblockextension) - Select all (Ctrl/Cmd+A) selects the nearest block element first, pressing it again selects the whole document\n- [SelectionAlwaysOnDisplayExtension](/docs/api/modules/lexical_extension#selectionalwaysondisplayextension) - Highlights selected content even when the editor is not focused\n- [TabIndentationExtension](/docs/api/modules/lexical_extension#tabindentationextension) - Changes Tab key to insert tabs and indent instead of natively focusing the next field\n- [WatchEditableExtension](/docs/api/modules/lexical_extension#watcheditableextension) - Exposes the editor's editable state as a reactive `Signal<boolean>`\n\n[@lexical/hashtag](/docs/api/modules/lexical_hashtag)\n\n- [HashtagExtension](/docs/api/modules/lexical_hashtag#hashtagextension) - HashtagNode\n\n[@lexical/history](/docs/api/modules/lexical_history)\n\n- [HistoryExtension](/docs/api/modules/lexical_history#historyextension) - History support (undo/redo)\n- [SharedHistoryExtension](/docs/api/modules/lexical_history#sharedhistoryextension) - History sharing between a parent editor and its nested editors\n\n[@lexical/html](/docs/api/modules/lexical_html)\n\n- [CoreImportExtension](/docs/api/modules/lexical_html#coreimportextension) - DOM import rules for the core nodes, included implicitly by node-providing extensions (experimental)\n- [DOMImportExtension](/docs/api/modules/lexical_html#domimportextension) - Extension-based replacement for `importDOM`/`DOMConversion`, rules are compiled into a dispatcher at editor build time (experimental)\n- [DOMRenderExtension](/docs/api/modules/lexical_html#domrenderextension) - Overrides the DOM render and export behavior for an editor (experimental)\n\n[@lexical/link](/docs/api/modules/lexical_link)\n\n- [AutoLinkExtension](/docs/api/modules/lexical_link#autolinkextension) - AutoLinkNode, note that matchers have to be manually configured\n- [ClickableLinkExtension](/docs/api/modules/lexical_link#clickablelinkextension) - Toggle between lexical selection and native click to open behavior for links\n- [LinkExtension](/docs/api/modules/lexical_link#linkextension) - LinkNode (`<a>` tag)\n\n[@lexical/list](/docs/api/modules/lexical_list)\n\n- [ListExtension](/docs/api/modules/lexical_list#listextension) - ListNode, ListItemNode\n- [CheckListExtension](/docs/api/modules/lexical_list#checklistextension) - Checklist support for ListNode and ListItemNode\n\n[@lexical/mark](/docs/api/modules/lexical_mark)\n\n- [MarkExtension](/docs/api/modules/lexical_mark#markextension) - MarkNode\n\n[@lexical/mdast](/docs/api/modules/lexical_mdast)\n\nExperimental micromark/mdast-based Markdown support (every extension in this package is experimental). See [Markdown with @lexical/mdast](/docs/serialization/markdown-mdast) for the full story.\n\n- [MdastExtension](/docs/api/modules/lexical_mdast#mdastextension) - The configurable core registry, exposing both Markdown import and export\n- [MdastHeadingExtension](/docs/api/modules/lexical_mdast#mdastheadingextension) - ATX & setext headings (HeadingNode)\n- [MdastBlockquoteExtension](/docs/api/modules/lexical_mdast#mdastblockquoteextension) - Block quotes (QuoteNode)\n- [MdastRichTextExtension](/docs/api/modules/lexical_mdast#mdastrichtextextension) - Bundle of heading + blockquote\n- [MdastListExtension](/docs/api/modules/lexical_mdast#mdastlistextension) - Ordered/unordered lists (ListNode, ListItemNode)\n- [MdastTaskListExtension](/docs/api/modules/lexical_mdast#mdasttasklistextension) - GFM task lists (`- [x]`), layered on MdastListExtension\n- [MdastCodeExtension](/docs/api/modules/lexical_mdast#mdastcodeextension) - Fenced & indented code blocks (CodeNode)\n- [MdastLinkExtension](/docs/api/modules/lexical_mdast#mdastlinkextension) - Links, CommonMark autolinks, reference links (LinkNode)\n- [MdastAutolinkLiteralExtension](/docs/api/modules/lexical_mdast#mdastautolinkliteralextension) - GFM literal autolinks (bare URLs in prose)\n- [MdastHorizontalRuleExtension](/docs/api/modules/lexical_mdast#mdasthorizontalruleextension) - Thematic breaks (HorizontalRuleNode)\n- [MdastStrikethroughExtension](/docs/api/modules/lexical_mdast#mdaststrikethroughextension) - GFM strikethrough (text format)\n- [MdastTableExtension](/docs/api/modules/lexical_mdast#mdasttableextension) - GFM tables (TableNode, ...)\n- [MdastCommonMarkExtension](/docs/api/modules/lexical_mdast#mdastcommonmarkextension) - Bundle of the six CommonMark extensions\n- [MdastGfmExtension](/docs/api/modules/lexical_mdast#mdastgfmextension) - Bundle of the four GFM extensions\n- [MdastShadowRootQuoteExtension](/docs/api/modules/lexical_mdast#mdastshadowrootquoteextension) - Opt-in: blockquotes as block containers with full-fidelity nested content\n- [MdastHtmlExtension](/docs/api/modules/lexical_mdast#mdasthtmlextension) - Opt-in: raw HTML in Markdown routed through the `@lexical/html` DOM import rules (Markdown inside the tags included, GitHub-style), plus generic HTML-encoded export via `$exportViaDOM` / `rawHtmlBlock`\n- [MdastShortcutsExtension](/docs/api/modules/lexical_mdast#mdastshortcutsextension) - Streaming Markdown typing shortcuts, driven by the same grammar as import\n\n[@lexical/overflow](/docs/api/modules/lexical_overflow)\n\n- [OverflowExtension](/docs/api/modules/lexical_overflow#overflowextension) - OverflowNode\n\n[@lexical/plain-text](/docs/api/modules/lexical_plain-text)\n\n- [PlainTextExtension](/docs/api/modules/lexical_plain-text#plaintextextension) - Plain text editor, the return key creates a LineBreakNode by default (one ParagraphNode per document)\n\n`@lexical/react`\n\n- [ReactExtension](/docs/api/modules/lexical_react_ReactExtension#reactextension)\n- [ReactPluginHostExtension](/docs/api/modules/lexical_react_ReactPluginHostExtension#reactpluginhostextension)\n- [ReactProviderExtension](/docs/api/modules/lexical_react_ReactProviderExtension#reactproviderextension)\n- [TreeViewExtension](/docs/api/modules/lexical_react_TreeViewExtension#treeviewextension)\n\n[@lexical/rich-text](/docs/api/modules/lexical_rich-text)\n\n- [RichTextExtension](/docs/api/modules/lexical_rich-text#richtextextension) - Rich Text editor (QuoteNode, HeadingNode), the return key creates a ParagraphNode by default (multiple ParagraphNode per document). Includes configurable `escapeFormatTriggers` to escape text formatting (e.g. code) at text node boundaries, and `shouldHandlePasteAsFiles` to control whether files win over incidental text/HTML clipboard entries on paste\n\n[@lexical/table](/docs/api/modules/lexical_table)\n\n- [TableExtension](/docs/api/modules/lexical_table#tableextension) - TableNode, TableRowNode, TableCellNode\n\n[@lexical/tailwind](/docs/api/modules/lexical_tailwind)\n\n- [TailwindExtension](/docs/api/modules/lexical_tailwind#tailwindextension) - A theme configuration that uses Tailwind classes\n","isInternal":false,"tokens":2979,"sizeBytes":12770},{"name":"intro.md","path":"packages/lexical-website/docs/extensions/intro.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-website/docs/extensions/intro.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"# Lexical Extensions\n\nExtensions are a new convention to add configuration and behavior to a Lexical\neditor in a modular and framework-independent manner. It is easier to use and\nmore powerful than the React plug-in scheme that came before, and is framework\nagnostic.\n\nSee [Included Extensions](./included-extensions) for the list of extensions\nthat come with Lexical and its associated packages.\n\n## Use Case\n\nLexical Extensions allow complex features to be added to your editor\nin a single place. If you want to have an editor that supports check\nlists, you simply add the `CheckListExtension` to your dependencies.\nAll of its dependent configuration, registration, and dependencies on\nother extensions is encapsulated. Extensions also work the same way\nwith or without React, so you don't generally need separate\ndocumentation and exports to support both.\n\nAll of the complexity for how and when things need to happen is\nmoved to `buildEditorFromExtensions` and the definitions of the\nextensions themselves.\n\nIn contrast, using checklists without Lexical Extensions requires\n`ListNode` and `ListItemNode` to be configured, and both the `ListPlugin` and\n`CheckListPlugin` to be registered exactly one time each (or if not using\nReact: `registerList` and `registerCheckList`).\n\nCustomizing a Lexical editor without Lexical Extensions is done in two phases:\n\n* Configuration before the editor is constructed\n  (registered nodes, node replacement, theme, etc.)\n* Registration after the editor is constructed\n  (listeners, transforms, commands, react plug-ins, etc.)\n\nIn many cases, adding a feature to the editor requires *both* configuration\nand registration. These typically happen at very different places in the code,\npossibly even different files, and there are no transitive dependencies. It's\nup to you to make sure that every required snippet of configuration and\nregistration code is included exactly once in the right place. When some dependent\nconfiguration or registration is missing (or runs twice!), at best you get a runtime\nerror, but often it will partially work which is tricky to debug.\n\nAnother compelling use case for extensions is to provide functionality\nfrom one extension to another extension (e.g. collaboration with yjs),\nor have one extension enhance the capabilities of another (e.g. markdown\nimport/export, adding options to menus or toolbars, etc.).\nThis is very awkward and error-prone with legacy plug-ins since there is no\nestablished pattern for them to depend on each other, and providing this\nsort of functionality with React requires a strict hierarchy of context\nproviders. Any extension can provide output, which is equivalent to having\nextension-specific context scoped to an editor.\n\n## Core API Overview\n\nThe primary use case for extensions is to build an editor from a collection of\nextensions. Any configuration or behavior specific to your editor should be\nprovided as an extension with\n[defineExtension](/docs/api/modules/lexical#defineextension) or as an override\nof some dependency's configuration with\n[configExtension](/docs/api/modules/lexical#configextension). This is done\nwith either:\n\n* [buildEditorFromExtensions](/docs/api/modules/lexical_extension#buildeditorfromextensions) - when not using React\n* [LexicalExtensionComposer](/docs/api/modules/lexical_react_LexicalExtensionComposer) - when using React\n\nSee [Defining Extensions](./defining-extensions.md) for a detailed guide for\n`defineExtension`.\n\nThe following functions are included in the core\n[lexical](/docs/api/modules/lexical) module to ensure that defining\nextensions is lightweight and requires no dependency on the runtime\nfeatures of [@lexical/extension](/docs/api/modules/lexical_extension).\nThese functions are primarily for type inference with very minimal\nimplementations.\n\n* [defineExtension](/docs/api/modules/lexical#defineextension) - define an extension\n* [configExtension](/docs/api/modules/lexical#configextension) - provide configuration overrides for an extension\n* [declarePeerDependency](/docs/api/modules/lexical#declarepeerdependency) - declare an indirect dependency on another extension by name\n* [safeCast](/docs/api/modules/lexical#safecast) - a convenience for TypeScript casting, with more safety than `as`. Similar to using a variable with a declared type.\n* [shallowMergeConfig](/docs/api/modules/lexical#shallowmergeconfig) - the default configuration merge strategy\n\n## Advanced features\n\n- [Signals](./signals) - Can be used for extensions to change their configuration or behavior at runtime, such as dynamically enabling or disabling an extension. This is also used to facilitate some performance optimizations around subscriptions and listeners.\n- [Peer Dependencies](./peer-dependencies) - Used to declare indirect or optional dependencies between extensions, this can be used to enable some functionality or configuration only when some other extension is available. For example, an Emoji extension may declare a specific Markdown serialization for EmojiNode, but only if the Markdown extension is present.\n","isInternal":false,"tokens":1028,"sizeBytes":5063},{"name":"peer-dependencies.md","path":"packages/lexical-website/docs/extensions/peer-dependencies.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-website/docs/extensions/peer-dependencies.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"# Peer Dependencies\n\nPeer Dependencies are used to declare indirect or optional dependencies\nbetween extensions. This can be used to enable some functionality or\nconfiguration only when some other extension is available, or to\navoid a dependency loop. Extensions must form a directed acyclic graph,\nthere must not be a dependency path from one extension back to itself.\n\n## Use Cases\n\nGenerally the intended use case for `peerDependencies` is to provide\nfunctionality only when some other extension is present. For example,\nyou may have an extension that requires special code to interact with\nReact, but is also usable without React, so you could have\n`ReactProviderExtension` in the `peerDependencies` array.\n\nAnother example would be to provide Markdown serialization or import\nfor an extension that provides some custom node type, but only\nif the `MdastExtension` is present in that editor.\n","isInternal":false,"tokens":176,"sizeBytes":895},{"name":"react.md","path":"packages/lexical-website/docs/extensions/react.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-website/docs/extensions/react.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"# React and Lexical Extension\n\nLexical Extension vastly improved support for non-React usage, but\ndid not make any compromises when supporting React.\n\n## Minimal migration to LexicalExtensionComposer\n\nMigrating your application from `LexicalComposer` to\n`LexicalExtensionComposer` is straightforward, but has\ntwo things to consider.\n\n### Convert your `initialConfig` to an `extension`\n\nBefore:\n```tsx\nconst initialConfig = {\n  namespace: 'MyEditor',\n  nodes: [QuoteNode, HeadingNode],\n  theme,\n  editorState(editor) {\n    editor.update($initialEditorState, {tag: HISTORY_MERGE_TAG});\n  },\n  // This is the default when using Lexical Extension\n  onError(err) {\n    throw err;\n  },\n};\n```\n\nAfter:\n```tsx\n// Be careful to make sure that this is a stable reference, either by defining it\n// at the top-level of a module or by using something like `useRef` or `useState`\n// to create it. Any time this object changes, the editor will be recreated.\nconst appExtension = defineExtension({\n  name: 'MyEditor',\n  namespace: 'MyEditor',\n  nodes: () => [QuoteNode, HeadingNode],\n  theme,\n  $initialEditorState,\n});\n```\n\n### Switch from LexicalComposer to LexicalExtensionComposer\n\nBefore:\n```tsx\n<LexicalComposer initialConfig={initialConfig}>\n    <RichTextPlugin\n        contentEditable={<ContentEditable />}\n        ErrorBoundary={LexicalErrorBoundary}\n    />\n    {/* other legacy React plugins */}\n</LexicalComposer>\n```\n\nAfter:\n```tsx\n<LexicalExtensionComposer extension={appExtension} contentEditable={null}>\n    <RichTextPlugin\n        contentEditable={<ContentEditable />}\n        ErrorBoundary={LexicalErrorBoundary}\n    />\n    {/* other legacy React plugins */}\n</LexicalExtensionComposer>\n```\n\nThis is a minimal migration, in most cases you can migrate plug-in usage to\nextension dependencies. For example, by using\n[RichTextExtension](/docs/api/modules/lexical_rich-text#richtextextension) instead of\n`RichTextPlugin`:\n\n```tsx\nconst appExtension = defineExtension({\n  name: 'MyEditor',\n  namespace: 'MyEditor',\n  dependencies: [RichTextExtension],\n  theme,\n  $initialEditorState,\n});\n```\n\n```tsx\n<LexicalExtensionComposer extension={appExtension}>\n  {/* By default the contentEditable is the first child, defaulting to <ContentEditable /> */}\n  {/* other legacy React plugins */}\n</LexicalExtensionComposer>\n```\n\n## Managing the editor yourself with LexicalExtensionEditorComposer\n\n`LexicalExtensionComposer` builds its editor from `extension` and disposes it\nwhen the component unmounts. When you want to own that lifecycle instead,\nsuch as a nested editor that belongs to a node (like the caption in the\nplayground's `StickyComponent`), an editor shared with non-React code, or one\nthat should outlive the component, build the editor yourself and render it\nwith\n[`LexicalExtensionEditorComposer`](/docs/api/modules/lexical_react_LexicalExtensionEditorComposer).\n\nThe editor must include `ReactProviderExtension` and `ReactExtension`, which\n`LexicalExtensionComposer` otherwise adds for you:\n\n```tsx\nimport {buildEditorFromExtensions, defineExtension} from '@lexical/extension';\nimport {LexicalExtensionEditorComposer} from '@lexical/react/LexicalExtensionEditorComposer';\nimport {ReactExtension} from '@lexical/react/ReactExtension';\nimport {ReactProviderExtension} from '@lexical/react/ReactProviderExtension';\nimport {RichTextExtension} from '@lexical/rich-text';\n\n// Built once, at module scope, so it is not re-created on every render\nconst editor = buildEditorFromExtensions(\n  defineExtension({\n    dependencies: [ReactProviderExtension, ReactExtension, RichTextExtension],\n    name: '@my-app/editor',\n  }),\n);\n\nfunction App() {\n  return (\n    <LexicalExtensionEditorComposer initialEditor={editor}>\n      {/* The ContentEditable is rendered by ReactExtension, as with LexicalExtensionComposer */}\n      {/* other legacy React plugins */}\n    </LexicalExtensionEditorComposer>\n  );\n}\n```\n\n`LexicalExtensionEditorComposer` never disposes the editor, so call\n`editor.dispose()` yourself when you are done with it. Keep the\n`initialEditor` prop stable (module scope, `useMemo`, or state held by a\nnode), and render a given editor in only one place at a time, since an editor\nhas only one root element.\n\n## React Plug-ins (Legacy)\n\nThese just work as-is, by rendering them as children of your composer. You may\nwish to package them as an extension for easier re-use, or consider refactoring\nthem to work without React. See [Extension Decorators](#extension-decorators)\nand [Output Components](#output-components), and\n[React Independence](#react-independence) for strategies to repackage or\nrefactor your legacy plug-ins.\n\n## Extension Decorators\n\nA common use case for legacy react plug-ins is to render some React component,\nbut the position of that component in the document is not important. Some use\ncases for this are when the component is simply an effect that always returns\n`null`, or it is rendering something like a portal. There's no need for users\nto manually decide where these components are rendered, so Lexical's\nReactExtension maintains an array of these to render automatically.\n\n ```tsx\n // This is exactly how you would write a legacy React plug-in\nfunction LogEditorPlugin() {\n  const [editor] = useLexicalComposerContext()\n  useEffect(() => {\n    console.log(editor);\n  }, [editor]);\n  return null;\n}\n// This is how you would declare the plugin as an extension\n// that depends on React and renders this plug-in as a decorator\nexport const LogEditorExtension = defineExtension({\n  name: \"@example/LogEditor\",\n  dependencies: [\n    configExtension(ReactExtension, {\n      decorators: [<LogEditorPlugin />]\n    }),\n  ],\n});\n```\n\n## Output Components\n\nIn situations where it is important for the user to decide exactly where an\nextension's component is rendered in the React document, the convention is\nto provide an Output Component.\n\nHere we have the TreeViewExtension which defines an Output Component to render\nthe TreeView with the specified configuration.\n\n```tsx\nexport function TreeViewExtensionComponent(\n  props: Partial<TreeViewConfig>,\n): JSX.Element {\n  const [editor] = useLexicalComposerContext();\n  return (\n    <TreeView\n      editor={editor}\n      {...useExtensionDependency(TreeViewExtension).config}\n      {...props}\n    />\n  );\n}\n\nexport const TreeViewExtension = defineExtension({\n  build: () => ({Component: TreeViewExtensionComponent}),\n  config,\n  dependencies: [ReactExtension],\n  name: '@lexical/react/TreeView',\n});\n```\n\nThis extension component can be acquired with the\n[useExtensionComponent](/docs/api/modules/lexical_react_useExtensionComponent#useextensioncomponent)\nhook or the [ExtensionComponent](/docs/api/modules/lexical_react_ExtensionComponent#extensioncomponent) component.\n\n```tsx\nreturn (\n  <ExtensionComponent\n    lexical:extension={TreeViewExtension}\n    viewClassName=\"tree-view-output\" />\n);\n```\n\n```tsx\nconst TreeViewComponent = useExtensionComponent(TreeViewExtension);\nreturn (<TreeViewComponent viewClassName=\"tree-view-output\" />);\n```\n\n## React Independence\n\nMany legacy plug-ins are merely a `useEffect` wrapper, these tend to not\nneed React at all. For example:\n\nBefore:\n```tsx\nexport function CheckListPlugin(): null {\n  const [editor] = useLexicalComposerContext();\n  useEffect(() => {\n    return registerCheckList(editor);\n  }, [editor]);\n  return null;\n}\n```\n\nAfter:\n```tsx\nexport const CheckListExtension = defineExtension({\n  name: \"@lexical/list/CheckList\",\n  register: registerCheckList,\n});\n```\n\n## Using React extensions and plug-ins without JSX\n\n[ReactPluginHostExtension](/docs/api/modules/lexical_react_ReactPluginHostExtension#reactpluginhostextension)\nis an extension that allows you to mount the a React root at a\nspecific DOM element in your app so that applications that are\nnot natively React can still take advantage of existing legacy\nplug-ins and extensions that depend on React.\n\nWhile this does mean your application will include the React runtime, you\ndon't have to use JSX or have any other React infrastructure in your app.\n\nSee\n[extension-vanilla-react-plugin-host](https://github.com/facebook/lexical/blob/main/examples/extension-vanilla-react-plugin-host/src/main.ts)\n[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/facebook/lexical/tree/main/examples/?file=src%2Fmain.ts)\nfor usage of this extension and its API\n","isInternal":false,"tokens":1874,"sizeBytes":8388},{"name":"signals.md","path":"packages/lexical-website/docs/extensions/signals.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-website/docs/extensions/signals.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"# Signals\n\nLexical Extensions uses signals as the reactive interface that you can use to\ncommunicate with and between extensions.\n\n## Motivation\n\nAt the lowest level, communicating between parts of Lexical is done with\ncallbacks such as `registerCommand`, `registerUpdateListener`,\n`registerMutationListener`, and `addEventListener` with the DOM.\n\nHow these callbacks behave and in some cases whether they are registered\nat all depends on state local to the extension that may also be mutable,\nso would require a similar callback mechanism with a naïve implementation.\nWith legacy React plug-ins, this mechanism is `useEffect` which is very\ncoarse and doesn't really have a portable equivalent when using other\nor no framework.\n\nThe initial prototype was largely callback based as well, but this quickly\nbecomes very difficult to work with when you have to do work based on\nthe \"result\" of multiple callbacks or configuration values. Signals are\na more general solution to this problem. There's even a\n[TC39 Signals Proposal](https://github.com/tc39/proposal-signals) to add\ninfrastructure for this to the JavaScript standard.\n\nSignals also allow for optimizations by reducing redundant callbacks with\nprimitives such as `batch` or by allowing the current value to be inspected\nwith `peek` without adding any subscription. Basically, the same Signal can\nbe used either like React State when you want to run code as the value changes\nover time, or like a React Ref when you only need to know the current value\n(as is typical with event handlers).\n\nSignals also do not require you to manage a dependency array or use a\ncompiler, the subscriptions and dependencies are inferred at runtime\nas they are used. This means that the set of signals subscribed to by a\ngiven effect can change without violating any rules or generating any\nwarnings.\n\n## Implementation\n\nSince Signals are not yet standardized, and the\n[signal-polyfill](https://github.com/proposal-signals/signal-polyfill) is not\nready for production use, it was decided to leverage\n[@preact/signals-core](https://github.com/preactjs/signals). This\nimplementation is dependency-free, optimized, and well tested.\n\nFor future compatibility we *do not recommend* that you use\n`@preact/signals-core` directly, and instead use the re-exports from\n[@lexical/extension](https://lexical.dev/docs/api/modules/lexical_extension).\n\nAs of the initial release, these re-exports are:\n\n```ts\nexport {\n  batch,\n  computed,\n  effect,\n  type ReadonlySignal,\n  type Signal,\n  signal,\n  type SignalOptions,\n  untracked,\n} from '@preact/signals-core';\n```\n\n## Usage\n\nFor thorough examples on how the signal primitives work, you can read through the\n[Preact Signals](https://preactjs.com/guide/v10/signals/) documentation.\n\nLexical provides the following higher-level signal functions to make it easier\nto build extensions:\n\n### namedSignals\n\n[namedSignals](/docs/api/modules/lexical_extension#namedsignals)\nis a convenience function used to output independent signals from\neach property of a configuration object.\n\n```ts\nexport interface TabIndentationConfig {\n  disabled: boolean;\n  maxIndent: null | number;\n  /**\n   * By default, indents are set on all elements for which the\n   * {@link ElementNode.canIndent} returns true. This option allows you to set\n   * indents for specific nodes without overriding the method for others.\n   */\n  $canIndent: CanIndentPredicate;\n}\n\nexport const TabIndentationExtension = defineExtension({\n  build(editor, config, state) {\n    return namedSignals(config);\n  },\n  config: safeCast<TabIndentationConfig>({\n    $canIndent: $defaultCanIndent,\n    disabled: false,\n    maxIndent: null,\n  }),\n  name: '@lexical/extension/TabIndentation',\n  register(editor, config, state) {\n    const {disabled, maxIndent, $canIndent} = state.getOutput();\n    return effect(() => {\n      if (!disabled.value) {\n        // This register function's implementation uses peek on the maxIndent\n        // signal to get its current value when needed, no need to re-register\n        // every time this value changes.\n        return registerTabIndentation(editor, maxIndent, $canIndent);\n      }\n    });\n  },\n});\n```\n\n### watchedSignal\n\n[watchedSignal](/docs/api/modules/lexical_extension#watchedsignal)\nis equivalent to React's [useSyncExternalStore](https://react.dev/reference/react/useSyncExternalStore)\n\n```ts\nexport const EditorStateExtension = defineExtension({\n  build(editor) {\n    return watchedSignal(\n      () => editor.getEditorState(),\n      (editorStateSignal) =>\n        editor.registerUpdateListener((payload) => {\n          editorStateSignal.value = payload.editorState;\n        }),\n    );\n  },\n  name: '@lexical/extension/EditorState',\n});\n```\n","isInternal":false,"tokens":1032,"sizeBytes":4708},{"name":"faq.md","path":"packages/lexical-website/docs/faq.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-website/docs/faq.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"---\nsidebar_position: 7\n---\n\n# FAQ\n\n## Why does Lexical use the `$` prefix in the name of many of the functions?\n\nOriginally, Lexical didn't have `$` functions, instead these functions were provided to you through callback params:\n\n```js\n// Mid 2020 API\neditor.update((viewState) => {\n  const getRoot = viewState.getRoot();\n  ..\n});\neditor.addTextTransform((viewState) => {\n  const getRoot = viewState.getRoot();\n  ..\n});\n```\n\nInternally, this approach raised some negative feedback:\n\n- The `viewState` terminology was confusing. It wasn't really a `viewState`, more like a toolkit to manipulate the `EditorState`\n- For complex updates and transforms devs had to carry params around on many layers\n\nThis is when we decided to leverage the \"lexical\" scope instead to perform `EditorState` manipulation, and the `$` represents just that.\n\n```js\neditor.update(() => ...);\neditor.registerNodeTransform(FooNode, () => ...);\neditor.getEditorState().read(...);\n```\n\nIf you've used React Hooks before, you can think of `$` functions as being something that follows a similar pattern. These are functions that show their intent as to where they can or cannot be used. This makes it possible for a developer to create their own functions that give the same signal, by simply prefixing the function with the dollar.\n\nInternally, we've found this scales really well and developers get to grips with it in almost no time at all.\n\n## When does reconciliation happen?\n\nReconciliation is scheduled with\n[queueMicrotask](https://developer.mozilla.org/en-US/docs/Web/API/queueMicrotask),\nwhich means that it will happen very soon, but asynchronously. This is similar\nto something like `setTimeout(reconcile, 0)` with a bit more immediacy or\n`Promise.resolve().then(reconcile)` with less overhead. This is done so\nthat all of the updates that occur as a result of a single logical event will\nbe batched into one reconciliation.\n\nYou can force a reconciliation to take place synchronously with the discrete\noption to `editor.update` (demonstrated below).\n\n## Why do tests use `await editor.update(…)`\n\nYou may notice that many tests look like this:\n\n```js\nawait editor.update(updateA);\nawait editor.update(updateB);\n```\n\nAn astute observer would notice that this seems very strange, since\n`editor.update()` returns `void` and not `Promise<void>`. However,\nit does happen to work as you would want it to because\nthe implementation of Promise uses the same microtask queue.\n\nIt's not recommended to rely on this in browser code as it could depend on\nimplementation details of the compilers, bundlers, and VM. It's best to stick\nto using the `discrete` or the `onUpdate` callback options to be sure that\nthe reconciliation has taken place.\n\nIgnoring any other microtasks that were scheduled elsewhere,\nit is roughly equivalent to this synchronous code:\n\n```js\neditor.update(updateA, {discrete: true});\neditor.update(updateB, {discrete: true});\n```\n\nAt a high level, very roughly, the order of operations looks like this:\n\n1. `editor.update()` is called\n2. `updateA()` is called and updates the editor state\n3. `editor.update()` schedules a reconciliation microtask and returns\n4. `await` schedules a resume microtask and yields control to the task executor\n5. the reconciliation microtask runs, reconciling the editor state with the DOM\n6. the resume microtask runs\n\n## How do I listen for user text insertions?\n\nListening to text insertion events is problematic with content editables in general. It's a common source of bugs due to how\ndifferent browsers and third-party extensions interact with the DOM. Whilst it's possible to use DOM events like `input` and\n`beforeinput` to gauge some of the possible cases where a user has inserted text, these are hardly reliable and also don't\ntake into account edge-cases. Instead, Lexical prefers to consider any change as a possible user input, and as such doesn't\nmake a distinction between the cases. This is important for tools like spellcheck, browser extensions, IME, speech-to-text,\nscreen readers and other external tools that often don't reliably trigger a reliable event sequence (some don't even trigger\nany events at all!).\n\nFor those wanting to react to a text change and possibly block/alter the intent, the recommended approach is to use a node\ntransform. This also plays nicely with other sub-systems at play that might also be looking to do the same thing as you.\n\nFor those who just want to know of the changes, this can be achieved using a text content listener or an editor update listener.\n\n## How do I clear the contents of the editor?\n\nYou can go this by calling ```clear()``` on the RootNode in an update callback:\n\n```js\neditor.update(() => {\n  $getRoot().clear();\n})\n```\n\n## How do I listen to specific key down events?\n\nYou can leverage Lexical's command listening system. Lexical provides specific commands for many common keyboard operations, such as:\n\n- `KEY_ARROW_LEFT_COMMAND`\n- `KEY_ARROW_RIGHT_COMMAND`\n- `KEY_ARROW_UP_COMMAND`\n- `KEY_ARROW_DOWN_COMMAND`\n- `KEY_SPACE_COMMAND`\n- `KEY_ENTER_COMMAND`\n- `KEY_BACKSPACE_COMMAND`\n- `KEY_DELETE_COMMAND`\n- `KEY_TAB_COMMAND`\n- `KEY_ESCAPE_COMMAND`\n\n```js\nimport {KEY_ENTER_COMMAND, COMMAND_PRIORITY_LOW} from 'lexical';\n\neditor.registerCommand(KEY_ENTER_COMMAND, (event: KeyboardEvent) => {\n  // Handle enter key presses here\n  return false;\n}, COMMAND_PRIORITY_LOW);\n```\n\nYou can use the generic `KEY_DOWN_COMMAND` command to listen\nto all keydown events. Do note, that returning `true` in your listener will prevent any\nother key based commands from firing, so in most cases you'll want to return `false` from\nthe command listener.\n\n```js\nimport {KEY_DOWN_COMMAND, COMMAND_PRIORITY_LOW} from 'lexical';\n\neditor.registerCommand(KEY_DOWN_COMMAND, (event: KeyboardEvent) => {\n  // Handle event here\n  return false;\n}, COMMAND_PRIORITY_LOW)\n```\n\n:::tip\n\nIf you do return `true` from any listener with an event payload, you likely\nalso should call `event.preventDefault()` unless your command relies on the\nbrowser's native processing of that event.\n\n:::\n\n## Why do I get \"does not implement .getType()\" or \"does not match registered node\"?\n\n```\nLexicalNode: Node FooNode does not implement .getType().\nCreate node: Type heading in node HeadingNode does not match registered node HeadingNode with the same type\n```\n\nThese almost always mean your app resolved **two copies of `lexical`**, not that\nthe node is misdeclared — the `instanceof` and registration checks compare class\nreferences, and the two copies have different ones. Run `npm ls lexical` (or\n`pnpm why lexical`) from the app root: it must report exactly one version.\nSee [One Lexical per app](concepts/one-lexical-per-app.md) for the causes and\nthe fix, including why a library must declare `lexical` in `peerDependencies`.\n\n## Which module formats are published? (ESM, CommonJS, Node.js, React Native)\n\nEvery Lexical package is published as ES modules only. The packages declare\n`\"type\": \"module\"`, and their `exports` maps offer a `development` and a\n`production` build plus a `default` entry that picks one of the two at runtime\nfrom `process.env.NODE_ENV`.\n\n- **Bundlers** (webpack, Vite, Rollup, esbuild with the conditions set) resolve\n  the `development` or `production` condition and get exactly one build.\n- **Node.js** resolves `default`, which loads both builds and exports one. To\n  load a single build, pass the condition on the command line:\n  `node --conditions=production app.js` (or `--conditions=development`).\n- **CommonJS** code can `require()` the packages on Node.js 20.19 or later,\n  which loads ES modules from `require()` as long as nothing in the module\n  graph uses top-level `await` (Lexical's builds do not). Older Node.js\n  versions have to use `await import('lexical')` instead. Loading is all\n  that `require()` guarantees: Lexical's builds import their own\n  dependencies as ES modules, so a dependency that a CommonJS application\n  also loads through `require()` can end up in the application twice, once\n  per module system, with separate classes and module state. That breaks\n  any integration that hands such a dependency's objects to Lexical, for\n  example `yjs` documents given to `@lexical/yjs`, or `@preact/signals-core`\n  effects observing signals from `@lexical/extension`. Load those\n  dependencies as ES modules too (`await import('yjs')`), or move the\n  application to ES modules.\n- **React Native (Metro)** bundles the packages without extra configuration,\n  but Metro does not set the `development`/`production` conditions and does\n  not drop the build the `default` entry leaves unused, so both end up in the\n  bundle. To bundle only the one that matches the build mode, add the\n  condition for Lexical's packages in `metro.config.js`:\n\n  ```js\n  const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config');\n\n  module.exports = mergeConfig(getDefaultConfig(__dirname), {\n    resolver: {\n      resolveRequest: (context, moduleName, platform) =>\n        context.resolveRequest(\n          /^(lexical|@lexical\\/)/.test(moduleName)\n            ? {\n                ...context,\n                unstable_conditionNames: [\n                  ...context.unstable_conditionNames,\n                  context.dev ? 'development' : 'production',\n                ],\n              }\n            : context,\n          moduleName,\n          platform,\n        ),\n    },\n  });\n  ```\n\n","frontmatter":{"sidebar_position":7},"isInternal":false,"tokens":2121,"sizeBytes":9390},{"name":"_category_.json","path":"packages/lexical-website/docs/getting-started/_category_.json","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-website/docs/getting-started/_category_.json","title":"Getting-started Skill","category":"anthropic-skill","format":"json","content":"{\n  \"label\": \"Getting Started\",\n  \"position\": 2\n}\n","frontmatter":{"label":"Getting Started","position":2},"isInternal":false,"tokens":17,"sizeBytes":50},{"name":"devtools.md","path":"packages/lexical-website/docs/getting-started/devtools.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-website/docs/getting-started/devtools.md","title":"Getting-started Skill","category":"anthropic-skill","format":"markdown","content":"---\nsidebar_label: \"Developer Tools\"\n---\n\n# Lexical Developer Tools\n\n## Extension\n\nThe easiest way to debug websites built with Lexical is to install the Lexical Developer Tools browser extension. It is available for several popular browsers:\n\n\n[link-chrome]: https://chromewebstore.google.com/detail/lexical-developer-tools/kgljmdocanfjckcgfpcpdoklodllfdpc 'Version published on Chrome Web Store'\n[link-firefox]: https://addons.mozilla.org/en-US/firefox/addon/lexical-developer-tools/ 'Version published on Mozilla Add-ons'\n[link-safari]: https://apps.apple.com/us/app/lexical-developer-tools/id6502753400 'Version published on Mac App Store'\n\n[<img src=\"https://cdnjs.cloudflare.com/ajax/libs/browser-logos/74.1.0/chrome/chrome.svg\" width=\"48\" alt=\"Chrome\" valign=\"middle\"/>][link-chrome] [<img valign=\"middle\" src=\"https://img.shields.io/chrome-web-store/v/kgljmdocanfjckcgfpcpdoklodllfdpc?style=flat&label=%20\n\"/>][link-chrome]\n\n[<img src=\"https://cdnjs.cloudflare.com/ajax/libs/browser-logos/74.1.0/firefox/firefox.svg\" width=\"48\" alt=\"Firefox\" valign=\"middle\"/>][link-firefox] [<img valign=\"middle\" src=\"https://img.shields.io/amo/v/lexical-developer-tools.svg?label=%20\"/>][link-firefox]\n\n[<img src=\"https://cdnjs.cloudflare.com/ajax/libs/browser-logos/74.1.0/safari/safari.svg\" width=\"48\" alt=\"Safari logo\" valign=\"middle\"/>][link-safari] [<img valign=\"middle\" src=\"https://img.shields.io/itunes/v/6502753400?label=%20\"/>][link-safari]\n\nNow, if you visit a website built with Lexical (such as [https://playground.lexical.dev/](https://playground.lexical.dev/)), you will see the _Lexical_ panel in your browser Inspector window.\n\n<img src=\"/img/docs/devtools-demo.png\" width=\"460\" alt=\"Lexical Developer Tools Demo screenshot\"/>\n\n## Extension Support\n\nFeel free to reach out to us for support, or just to chat, at [EPAM-lexical-support@epam.com](mailto:EPAM-lexical-support@epam.com) or via our [community channels](/community).\n\nLegal information:\n\n- [Meta Open Source - Privacy Policy](https://opensource.facebook.com/legal/privacy/)\n- [Meta Open Source - Terms of Use](https://opensource.facebook.com/legal/terms/)\n","frontmatter":{"sidebar_label":"Developer Tools"},"isInternal":false,"tokens":564,"sizeBytes":2126},{"name":"supported-browsers.md","path":"packages/lexical-website/docs/getting-started/supported-browsers.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-website/docs/getting-started/supported-browsers.md","title":"Getting-started Skill","category":"anthropic-skill","format":"markdown","content":"---\nsidebar_position: 4\n---\n\n# Supported Browsers\n\n> Note: Lexical does not support Internet Explorer or legacy versions of Edge.\n\n- Firefox 115+\n- Chrome 86+ (and other browsers based on Chrome 86+ such as Android Chrome 86+, Edge 86+ and Opera 72+)\n- Safari 15+ (iOS 15+, iPad OS 15+)\n","frontmatter":{"sidebar_position":4},"isInternal":false,"tokens":82,"sizeBytes":287},{"name":"theming.md","path":"packages/lexical-website/docs/getting-started/theming.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-website/docs/getting-started/theming.md","title":"Getting-started Skill","category":"anthropic-skill","format":"markdown","content":"---\nsidebar_position: 3\n---\n\n# Theming\n\nLexical tries to make theming straight-forward, by providing a way of passing a customizable theming object that maps CSS class names to the editor on creation. Here's an example of a plain-text theme:\n\n```js\nconst exampleTheme = {\n  paragraph: 'editor-paragraph',\n};\n```\n\nIn your CSS, you can then add something like:\n\n```css\n.editor-placeholder {\n  color: #999;\n  overflow: hidden;\n  position: absolute;\n  top: 15px;\n  left: 15px;\n  user-select: none;\n  pointer-events: none;\n}\n\n.editor-paragraph {\n  margin: 0 0 15px 0;\n  position: relative;\n}\n```\n\nPut the theme on your root extension. The same theme configuration works with\nReact and vanilla JavaScript:\n\n```js\nimport {PlainTextExtension} from '@lexical/plain-text';\nimport {defineExtension} from 'lexical';\n\nexport const appExtension = defineExtension({\n  name: 'MyEditor',\n  namespace: 'MyEditor',\n  dependencies: [PlainTextExtension],\n  theme: exampleTheme,\n});\n```\n\nIn React, pass that stable extension to `LexicalExtensionComposer`. This example\nplaces its own `ContentEditable`, so it disables the composer's default one:\n\n```jsx\nimport {ContentEditable} from '@lexical/react/LexicalContentEditable';\nimport {LexicalExtensionComposer} from '@lexical/react/LexicalExtensionComposer';\nimport {appExtension} from './appExtension';\n\nexport default function Editor() {\n  return (\n    <LexicalExtensionComposer extension={appExtension} contentEditable={null}>\n      <div style={{position: 'relative'}}>\n        <ContentEditable\n          aria-label=\"Plain text editor\"\n          aria-placeholder=\"Enter some text...\"\n          placeholder={<div className=\"editor-placeholder\">Enter some text...</div>}\n        />\n      </div>\n    </LexicalExtensionComposer>\n  );\n}\n```\n\nIn vanilla JavaScript, build the editor from the same extension and attach it to\nyour editable element:\n\n```js\nimport {buildEditorFromExtensions} from '@lexical/extension';\nimport {appExtension} from './appExtension';\n\nconst editor = buildEditorFromExtensions(appExtension);\neditor.setRootElement(document.getElementById('editor'));\n// Call editor.dispose() when removing this editor permanently.\n```\n\nA theme supplies class names; it does not install features or include CSS. For\nexample, to use the heading and quote styles below, add `RichTextExtension` in\nplace of `PlainTextExtension`. Add the corresponding extensions for lists, links,\nand code highlighting when you need those features.\n\n## Merging extension themes\n\nEvery extension can supply a `theme`. Lexical recursively merges these themes in\ndependency order: dependencies contribute first, and extensions that depend on\nthem can override individual entries. Your root extension can therefore customize\nan extension's styles while keeping the rest of its theme, including nested\nentries such as `text` and `heading`.\n\n```js\nconst EmphasisExtension = defineExtension({\n  name: 'Emphasis',\n  theme: {\n    text: {\n      bold: 'emphasis-bold',\n      italic: 'emphasis-italic',\n    },\n  },\n});\n\nconst appExtension = defineExtension({\n  name: 'MyEditor',\n  dependencies: [PlainTextExtension, EmphasisExtension],\n  theme: {\n    paragraph: 'editor-paragraph',\n    text: {bold: 'editor-bold'},\n  },\n});\n\n// The merged theme is:\n// {\n//   paragraph: 'editor-paragraph',\n//   text: {bold: 'editor-bold', italic: 'emphasis-italic'},\n// }\n```\n\nWhen two themes set the same entry, the later value replaces the earlier one;\nclass-name strings are not concatenated. There is no need to spread a dependency's\ntheme into your own.\n\n## Available theme entries\n\nMany of the Lexical's core nodes also accept theming properties. Here's a more comprehensive theming object:\n\n```js\nconst exampleTheme = {\n  paragraph: 'editor-paragraph',\n  quote: 'editor-quote',\n  heading: {\n    h1: 'editor-heading-h1',\n    h2: 'editor-heading-h2',\n    h3: 'editor-heading-h3',\n    h4: 'editor-heading-h4',\n    h5: 'editor-heading-h5',\n    h6: 'editor-heading-h6',\n  },\n  list: {\n    nested: {\n      listitem: 'editor-nested-listitem',\n    },\n    ol: 'editor-list-ol',\n    ul: 'editor-list-ul',\n    listitem: 'editor-listItem',\n    listitemChecked: 'editor-listItemChecked',\n    listitemUnchecked: 'editor-listItemUnchecked',\n  },\n  hashtag: 'editor-hashtag',\n  image: 'editor-image',\n  link: 'editor-link',\n  text: {\n    bold: 'editor-textBold',\n    code: 'editor-textCode',\n    italic: 'editor-textItalic',\n    strikethrough: 'editor-textStrikethrough',\n    subscript: 'editor-textSubscript',\n    superscript: 'editor-textSuperscript',\n    underline: 'editor-textUnderline',\n    underlineStrikethrough: 'editor-textUnderlineStrikethrough',\n  },\n  code: 'editor-code',\n  codeHighlight: {\n    atrule: 'editor-tokenAttr',\n    attr: 'editor-tokenAttr',\n    boolean: 'editor-tokenProperty',\n    builtin: 'editor-tokenSelector',\n    cdata: 'editor-tokenComment',\n    char: 'editor-tokenSelector',\n    class: 'editor-tokenFunction',\n    'class-name': 'editor-tokenFunction',\n    comment: 'editor-tokenComment',\n    constant: 'editor-tokenProperty',\n    deleted: 'editor-tokenProperty',\n    doctype: 'editor-tokenComment',\n    entity: 'editor-tokenOperator',\n    function: 'editor-tokenFunction',\n    important: 'editor-tokenVariable',\n    inserted: 'editor-tokenSelector',\n    keyword: 'editor-tokenAttr',\n    namespace: 'editor-tokenVariable',\n    number: 'editor-tokenProperty',\n    operator: 'editor-tokenOperator',\n    prolog: 'editor-tokenComment',\n    property: 'editor-tokenProperty',\n    punctuation: 'editor-tokenPunctuation',\n    regex: 'editor-tokenVariable',\n    selector: 'editor-tokenSelector',\n    string: 'editor-tokenSelector',\n    symbol: 'editor-tokenProperty',\n    tag: 'editor-tokenProperty',\n    url: 'editor-tokenOperator',\n    variable: 'editor-tokenVariable',\n  },\n};\n```\n","frontmatter":{"sidebar_position":3},"isInternal":false,"tokens":1353,"sizeBytes":5782},{"name":"intro.md","path":"packages/lexical-website/docs/intro.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-website/docs/intro.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"---\nsidebar_position: 1\n---\n\n# Introduction\n\nLexical is an extensible text editor framework for the web, built for\nreliability, accessibility, and performance. It gives you a small,\ndependency-free core and a set of optional packages that you compose into the\neditor you need, from a plain-text input with mentions to a collaborative\nrich-text document editor.\n\nLexical attaches to a `contenteditable` element and keeps its own model of the\ndocument. You read and change that model with Lexical's APIs, and Lexical\ntakes care of keeping the DOM, the selection, and the browser's many\n`contenteditable` quirks in sync. Most code never touches the DOM directly;\nthe main exception is custom nodes, which define how they render.\n\n## What can you build? {#what-can-be-built-with-lexical}\n\n- Plain-text inputs that need more than a `<textarea>`: mentions, hashtags,\n  links, custom emoji.\n- Rich-text editors for comments, posts, and messages.\n- Full document editors with tables, lists, code blocks, and images, for a CMS\n  or a notes app.\n- Real-time collaborative editing with shared content and remote cursors,\n  using the [Yjs integration](./collaboration/react.md).\n\nLexical supplies the editing infrastructure. Your application supplies the\nlayout, toolbars, menus, styling, and storage.\n\nLexical is an open-source community project. It began at Meta, and its\ncontributors today include Meta engineers alongside many volunteers and\ndevelopers from other companies. It powers text editing in Meta's web products\nand at [Bloomberg](https://www.bloomberg.com), and is also the editor behind\n[Payload CMS](https://payloadcms.com),\n[Supabase](https://supabase.com),\n[Proton Docs](https://proton.me/drive/document-editor),\n[MDXEditor](https://mdxeditor.dev), [Sveltia CMS](https://sveltiacms.app),\n[Dify](https://dify.ai), [RAGFlow](https://ragflow.io),\n[DeepSeek Harness](https://deepseek.com/harness/),\n[Paperclip](https://paperclip.ing), and [Lexxy](https://lexxy.dev). To see\nwhat it can do, try the [playground](https://playground.lexical.dev).\n\n## How it fits together\n\nThe `lexical` package is the core: the editor, the editor state, the base\nnode types, selection, commands, and the DOM reconciler. Everything else is an\noptional package built on top of it, so an application only includes the\nfeatures it uses. The core is framework-agnostic, and `@lexical/react`\nprovides React bindings.\n\n```mermaid\nflowchart TB\n  accTitle: How Lexical's packages fit together\n  accDescr: Your application uses optional packages such as rich text, history, lists, tables, and React bindings. Both the application and those packages build on the lexical core, which contains the editor, the editor state, commands, and the DOM reconciler.\n  app[\"Your application\"]\n  subgraph features[\"Optional packages\"]\n    rich[\"@lexical/rich-text<br/>@lexical/plain-text\"] ~~~ more[\"@lexical/history<br/>@lexical/list<br/>@lexical/table<br/>…\"] ~~~ bindings[\"@lexical/react\"]\n  end\n  subgraph core[\"lexical (core)\"]\n    editor[\"Editor\"] ~~~ state[\"EditorState<br/>(node tree + selection)\"] ~~~ commands[\"Commands\"] ~~~ reconciler[\"DOM reconciler\"]\n  end\n  app --> features\n  app --> core\n  features --> core\n```\n\nFeatures are added to an editor as [extensions](./extensions/intro.md). An\nextension bundles everything a feature needs (its nodes, configuration,\ncommands, and listeners, plus any extensions it depends on) so it can be\nadded in one place:\n\n```ts\nimport {buildEditorFromExtensions} from '@lexical/extension';\nimport {HistoryExtension} from '@lexical/history';\nimport {RichTextExtension} from '@lexical/rich-text';\nimport {defineExtension} from 'lexical';\n\nconst editor = buildEditorFromExtensions(\n  defineExtension({\n    dependencies: [RichTextExtension, HistoryExtension],\n    name: '@my-app/editor',\n    namespace: 'my-app',\n  }),\n);\neditor.setRootElement(document.getElementById('editor'));\n```\n\nIn React, [`LexicalExtensionComposer`](./extensions/react.md) does the same\njob. The [Quick Start](/docs/getting-started/quick-start) and\n[React guide](/docs/getting-started/react) walk through a complete setup.\n\n## Core concepts {#lexicals-design}\n\nThese are the ideas the rest of the documentation builds on. Each one links to\na page with the details.\n\n### Editor {#editor-instances}\n\nThe editor wires everything together. It owns the current editor state,\nattaches to a root DOM element, and is where you register nodes, listeners,\ntransforms, and commands. You usually create it with\n`buildEditorFromExtensions` or through the React bindings rather than calling\n`createEditor()` yourself.\n\n### Editor state {#editor-states}\n\nAn [editor state](./concepts/editor-state.md) is an immutable snapshot of the\ndocument: a tree of [nodes](./concepts/nodes.mdx) under a single `RootNode`,\nplus a [selection](./concepts/selection.md). The editor state, not the DOM, is\nthe source of truth. Its `toJSON()` output is how you save and restore content.\n\n### A DOM-like document tree {#document-model}\n\nLexical's node tree is shaped like the HTML it renders. A paragraph contains\nits text, and a link is an element that contains the text it wraps. See\n[Document Model](./concepts/document-model.md), and\n[Compared with ProseMirror](./concepts/compared-with-prosemirror.md) for how\nthis differs from a mark-based editor.\n\n### Reading and updating editor state {#reading-and-updating-editor-state}\n\nAll reads and writes happen inside a synchronous callback, such as\n`editor.update(fn)` or `editor.read('force-commit', fn)`. Functions whose\nnames start with `$`, such as `$getRoot()`, only work inside those callbacks,\nin the same way React Hooks only work while a component renders. See\n[Editor State](./concepts/editor-state.md#updating-state).\n\n### Updates and the DOM reconciler {#dom-reconciler}\n\nUpdates change a pending copy of the editor state. Updates in the same tick\nare batched, and when the batch commits, the reconciler patches only the DOM\nthat belongs to changed nodes. Lexical also watches for DOM changes made\noutside it and keeps or reverts them. [Updates](./concepts/updates.mdx) walks\nthrough each phase, with an interactive example.\n\n### Commands, transforms, and listeners {#listeners-node-transforms-and-commands}\n\nMost editor behavior is built from three kinds of hooks.\n[Commands](./concepts/commands.md) carry user input and your own actions to\nhandlers in priority order. [Node transforms](./concepts/transforms.md) run\nduring an update to keep the document in shape.\n[Listeners](./concepts/listeners.md) react after an update is committed.\n\n### Serialization and running on a server\n\nContent converts to and from JSON, HTML, and Markdown; see\n[Serialization](./serialization/serialization.md). An editor with no root\nelement does no DOM work at all, so the same code can run on a server or in\ntests; see [Running Without a Browser](./concepts/headless.md).\n\n## Get started\n\n- [Quick Start](/docs/getting-started/quick-start) builds an editor without a\n  framework, and [Getting Started with React](/docs/getting-started/react) does\n  the same in React.\n- [Lexical Extensions](./extensions/intro.md) and\n  [Included Extensions](./extensions/included-extensions.md) cover how to add\n  features.\n- The [playground](https://playground.lexical.dev) shows many features working\n  together.\n","frontmatter":{"sidebar_position":1},"isInternal":false,"tokens":1717,"sizeBytes":7291},{"name":"maintainers-guide-link.md","path":"packages/lexical-website/docs/maintainers-guide-link.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-website/docs/maintainers-guide-link.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"---\nsidebar_position: 99\n---\n\n# Developing against a local Lexical checkout\n\nIf you're iterating on Lexical itself and want a downstream app to pick\nup your changes without publishing, the Lexical packages support two\nlinking workflows.\n\nBoth rely on the fact that each `packages/<name>/` directory is its own\npublishable npm package: after a build, `packages/lexical/package.json`\nplus the `dist/` it sits next to is a complete tarball. There is no\nintermediate `npm/` directory and no copy step — `pnpm pack` /\n`pnpm publish` run straight from the package root.\n\n## Option A: built artifacts (works for any consumer)\n\nBuild the artifacts, then point your downstream project at the package\ndirectory:\n\n```bash\n# in the Lexical checkout\npnpm install\npnpm run build-release   # produces dist/<Name>.{dev,prod}.js + .d.ts + .js.flow\n\n# in your downstream app\npnpm add link:/path/to/lexical/packages/lexical\npnpm add link:/path/to/lexical/packages/lexical-react   # etc.\n```\n\n`pnpm` follows the `link:` protocol by creating a real symlink into your\n`node_modules/`, so subsequent rebuilds of Lexical (e.g.\n`pnpm run build` in the Lexical checkout) become visible immediately.\nStandard `exports` resolution picks up the `development` /\n`production` conditions, so dev builds load the unminified variants and\nprod builds load the minified ones automatically. Only ESM is published;\na CommonJS consumer loads the same files through `require(esm)`\n(Node.js 20.19+).\n\nIf you prefer the dev-only loop (faster builds, no minification), use\n`pnpm run build` instead of `build-release`. The fork modules emitted\nby a dev-only build re-export the `.dev` variant unconditionally; you\nwon't be able to `pnpm publish` from such a checkout because the\npublish guard will report missing `.prod` files.\n\n`file:/path/to/...` works too if your package manager treats it as a\nsymlink (modern pnpm does); npm copies the directory into\n`node_modules` instead, which loses any live-reload benefit but is\notherwise equivalent for one-shot installs.\n\n## Option B: TypeScript source (no Lexical build needed)\n\nFor the tightest dev loop — edit a Lexical source file and have your\ndownstream bundler pick it up on the next request — opt into the\n`source` export condition. Each public package's `exports` map exposes\nits `./src/<entry>.tsx?` next to the compiled `development`/`production`/\n`default` conditions, so a bundler configured with\n`resolve.conditions: ['source', …]` will load TypeScript directly out\nof the linked package.\n\nThe only requirement is **bundler support**: your consumer's bundler has to\nbe configured to pick up the `source` condition AND to transform\n`.ts`/`.tsx` from inside `node_modules`. Vite, Rspack, and Parcel can all be\ntold to do this; webpack needs a loader override for the linked package.\n\nThere is **no aliasing and no extra `define` to set up**. Internal utilities\nthe source depends on are a real (internal) dependency, `@lexical/internal`,\nwhich resolves through normal package resolution; the React/test helpers are\npackage-internal; and the dev/prod branch is `process.env.NODE_ENV !==\n'production'`, which every mainstream bundler substitutes by default. So\n`source` mode works without any `resolve.alias` entries or build-time globals\n— including for the transitive `@lexical/*` packages.\n\nOne thing the compiled bundles have that the sources do not is the\n`/* @__PURE__ */` annotations on module-scope calls to `defineExtension`,\n`createCommand`, `createState`, and friends. Lexical's own build injects\nthem, and without them a bundler has to assume those calls have side effects\nand will keep every extension, command, and rule definition your app never\nuses. Add [`@lexical/compiler`](https://www.npmjs.com/package/@lexical/compiler)\nto your build to get the same annotations out of `source` mode — it annotates\nthe factory calls in your own code too.\n\n### Minimal Vite setup\n\n```ts\n// vite.config.ts\nimport {pureAnnotations} from '@lexical/compiler/PureAnnotations';\nimport {defineConfig} from 'vite';\n\nexport default defineConfig({\n  plugins: [pureAnnotations()],\n  resolve: {\n    conditions: ['source', 'development', 'module', 'browser', 'default'],\n  },\n});\n```\n\nBecause a `link:`ed checkout is the same version as the plugin, you can also\npass `pureAnnotations({inline: true})`, which replaces calls to the trivial\nfactories (`safeCast`, `defineExtension`, `configExtension`, ...) with the\nliteral they would have returned rather than annotating them.\n\n`@lexical/compiler/PureAnnotations` also exports the transform itself\n(`transformPureAnnotations`) for bundlers without a Rollup-style plugin API;\nsee its README for the options.\n\nThe integration fixture at\n`scripts/__tests__/integration/fixtures/lexical-link-source-mode/`\nexercises exactly this setup and is the canonical reference if you need\na fuller example.\n\n> [!NOTE]\n> `@lexical/internal` is published only so this resolution works (and for\n> direct internal consumers); the compiled `dist/` artifacts inline it, so\n> a normal `npm install` of `lexical` never executes a separate copy.\n","frontmatter":{"sidebar_position":99},"isInternal":false,"tokens":1195,"sizeBytes":5084},{"name":"maintainers-guide.md","path":"packages/lexical-website/docs/maintainers-guide.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-website/docs/maintainers-guide.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"# Maintainers' Guide\n\nThis is the grimoire of arcane knowledge covering the overall organization\nof the Lexical monorepo, including its conventions, quirks, and\nconfigurations.\n\n## Monorepo Organization\n\n### Workspaces\n\nThe monorepo is configured with\n[pnpm workspaces](https://pnpm.io/workspaces); the set of workspace\npackages is declared in the top-level `pnpm-workspace.yaml`. This mostly means that all packages share a\ntop-level `pnpm-lock.yaml` and `pnpm -C {package} run {command}` is often\nused to run a command from a nested package's package.json.\n\n### Private\n\nSome packages in the monorepo do not get published to npm, for example:\n\n* `packages/lexical-devtools` - browser extension for working with Lexical\n  sites\n* `packages/lexical-playground` - the\n  [playground.lexical.dev](https://playground.lexical.dev/) demo site\n* `packages/lexical-website` - the [lexical.dev](https://lexical.dev/)\n  docusaurus website that you may even be reading right now\nInternal runtime code shared by more than one package lives in\n`packages/lexical-internal` (`@lexical/internal`). Unlike the others above\nit **is** published, but only so its source resolves through normal package\nresolution (the `source` export condition used by linked-checkout\nconsumers); the compiled packages inline it, so it is never executed as a\nseparate runtime dependency. It is not a public API and has no semver\nguarantees — see [Developing against a local Lexical\ncheckout](./maintainers-guide-link.md).\n\nIt is required that private packages, and any other package that should not\nbe published to npm, have a `\"private\": true` property in their `package.json`.\nIf you have an in-progress package that will eventually be public, but is\nnot ready for consumption, it should probably still be set to\n`\"private\": true` otherwise the tooling will find it and publish it.\n\n## Package naming conventions\n\n### Overall\n\n| Usage | Convention |\n| -- | -- |\n| Directory name | `packages/lexical-package-name` |\n| Entrypoint | `packages/lexical-package-name/src/index.{ts,tsx}` |\n| Flow types | `packages/lexical-package/flow/LexicalPackageName.js.flow` |\n| package.json name | `@lexical/package-name` |\n| Documentation | `packages/lexical-package-name/README.md` |\n| Unit Tests | `packages/lexical-package-name/src/__tests__/unit/LexicalPackageName.test.{ts,tsx}` |\n| dist (gitignore'd build product) | `packages/lexical-package-name/dist` |\n| npm (gitignore'd prerelease product) | `packages/lexical-package-name/npm` |\n| www entrypoint | `packages/lexical-package-name/LexicalPackageName.js` |\n\n### Multiple module export (@lexical/react)\n\nInstead of having a single module, some packages may have many modules\n(currently only `@lexical/react`) that are each exported separately.\nIn that scenario, there should be no `index.ts` entrypoint file and every module\nat the top-level should be an entrypoint. All entrypoints should be a\nTypeScript file, not a subdirectory containing an index.ts file.\n\nThe [update-packages](#pnpm-run-update-packages) script will ensure that the\nexports match the files on disk.\n\n## Creating a new package\n\nThe first step in creating a new package is to create the workspace directory\nand package.json file. The example we will use is the steps that were used\nto create the `lexical-eslint-plugin`, which will be published to npm as\n`@lexical/eslint-plugin`.\n\n### Create the workspace\n\n```bash\nmkdir -p packages/lexical-eslint-plugin\n```\n\nCreate the initial `package.json` file (you can base it on an existing package\nor use the template below):\n\n<details><summary>\n\n`packages/lexical-eslint-plugin/package.json`\n</summary>\n\n```json\n{\n  \"name\": \"@lexical/eslint-plugin\",\n  \"description\": \"\",\n  \"keywords\": [\n    \"lexical\",\n    \"editor\"\n  ],\n  \"version\": \"0.14.3\",\n  \"license\": \"MIT\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/facebook/lexical.git\",\n    \"directory\": \"packages/lexical-eslint-plugin\"\n  },\n  \"main\": \"LexicalEslintPlugin.js\",\n  \"types\": \"index.d.ts\",\n  \"bugs\": {\n    \"url\": \"https://github.com/facebook/lexical/issues\"\n  },\n  \"homepage\": \"https://github.com/facebook/lexical#readme\"\n}\n```\n</details>\n\nSome next steps for this package.json before moving on:\n\n- Update the description\n- Add appropriate keywords\n\n### Create the initial source file\n\n```\nmkdir -p packages/lexical-eslint-plugin/src\ncode packages/lexical-eslint-plugin/src/index.ts\n```\n\nHere are some minimal examples of those files that you might start out with.\nI've elided the license header, the eslint header/header fixer will help you\nwith that!\n\n<details><summary>\n\n`packages/lexical-eslint-plugin/src/index.ts`\n</summary>\n\n```typescript\nimport {name, version} from '../package.json';\n\nconst plugin = {\n  meta: {name, version},\n  rules: {},\n};\n\nexport default plugin;\n```\n</details>\n\n### Run update-packages to generate boilerplate docs & config\n\n```\npnpm run update-packages\n```\n\nThis will set up the tsconfig, flow, etc. configuration to recognize your\nnew module. It will also create an initial README.md using only the\ndescription from the package.json.\n\n\n### Create an initial unit test\n\n```\nmkdir -p packages/lexical-eslint-plugin/src/__tests__/unit\ncode packages/lexical-eslint-plugin/src/__tests__/unit/LexicalEslintPlugin.test.ts\n```\n\n\n<details><summary>\n\n`packages/lexical-eslint-plugin/src/__tests__/unit/LexicalEslintPlugin.test.ts`\n</summary>\n\n```typescript\nimport plugin from '@lexical/eslint-plugin';\n\ndescribe('LexicalEslintPlugin', () => {\n  it('exports a plugin with meta and rules', () => {\n    expect(Object.keys(plugin).sort()).toMatchObject(['meta', 'rules']);\n  });\n});\n```\n</details>\n\n\n## Scripts for development\n\n### pnpm run update-packages\n\nThis script runs: update-version, update-tsconfig, update-flowconfig,\ncreate-docs, and create-www-stubs. This is safe to do at any time and will\nensure that package.json files are all at the correct versions, paths are\nset up correctly for module resolution of all public exports, and that\nvarious defaults are filled in.\n\nThese scripts can be run individually, but unless you're working on one\nof these scripts you might as well run them all.\n\n### pnpm run prepare-release\n\nThis runs `build-release` to produce all of the artifacts each public\npackage needs (the `dev`/`prod` ESM variants plus their fork modules,\n`.d.ts` declarations, and `.js.flow` stubs under\n`packages/<name>/dist/`), then runs the publish-time guard in\n`scripts/npm/prepare-release.mjs` to confirm every path the package's\n`exports`/`main`/`module`/`types` fields reference actually exists on\ndisk. The guard fails the build if e.g. you ran `pnpm run build` (dev\nonly) and then tried to publish — the `.prod.js` files would be\nmissing.\n\nOnly ESM is published to npm. Every public package is a\n`\"type\": \"module\"` package (`update-version` adds the field), so its\nbuild is plain `.js`. A CommonJS consumer gets the same files through\nNode's `require(esm)` (Node.js 20.19+), which is why the fork module that\nthe exports map's `default` condition resolves to has no top-level await.\nThe CommonJS variants are only built for www (`pnpm run build-www`).\n\nBecause a `.d.ts` in a `\"type\": \"module\"` package is an ES module\ndeclaration, the build gives the relative imports tsc emits an explicit\n`.js` extension (`from './LexicalEditor.js'`): TypeScript's `nodenext`\nresolution does not resolve extensionless relative imports in ESM, and a\nconsumer with `skipLibCheck` would silently get `any` for everything.\n`scripts/__tests__/integration/declaration-resolution.test.mjs` checks\nthat a `nodenext` consumer of every entry point still sees the types.\n\nEach package is its own publish root: `packages/<name>/` IS the\npublishable npm package after `build-release`. `pnpm publish` is run\ndirectly from that directory by `scripts/npm/release.mjs` so pnpm's\nautomatic `workspace:*` rewriting and the `files` whitelist do the\nright thing without an intermediate `npm/` copy step.\n\nThis will also update `scripts/error-codes/codes.json`, the mapping of\nproduction error codes to error messages. It's imperative to commit the\nresult of this before tagging a release.\n\n### pnpm run ci-check\n\nCheck flow, TypeScript, prettier and eslint for issues. A good command to run\nafter committing (which will auto-fix most prettier issues) and before pushing\na PR.\n\n### pnpm run flow\n\nCheck the Flow types\n\n### pnpm run tsc\n\nCheck the TypeScript types\n\n### pnpm run tsc-extension\n\nCheck the TypeScript types of the lexical-devtools extension\n\n### pnpm run test-unit\n\nRun the unit tests\n\n### pnpm run lint\n\nRun eslint\n\n### pnpm run generate-node-json\n\nRegenerate the specialized JSON serialization code for the built-in node\nclasses. A node's [serialization schema](/docs/serialization/#declarative-serialization-schemas-with-config)\nstates everything about a serialized property ahead of time — which accessor\nor field it uses, what its default is, what its domain admits — so the\ngeneric walk over that schema can be compiled into straight-line code. This\nscript does that compiling; the output is checked in.\n\nIt writes one module per package, beside the nodes it serializes:\n\n| Module | Classes |\n| --- | --- |\n| `packages/lexical/src/LexicalGeneratedJSON.ts` | ElementNode, TextNode, ParagraphNode, LineBreakNode, TabNode |\n| `packages/lexical-rich-text/src/LexicalRichTextGeneratedJSON.ts` | HeadingNode, QuoteNode |\n| `packages/lexical-link/src/LexicalLinkGeneratedJSON.ts` | LinkNode, AutoLinkNode |\n| `packages/lexical-mark/src/LexicalMarkGeneratedJSON.ts` | MarkNode |\n| `packages/lexical-list/src/LexicalListGeneratedJSON.ts` | ListNode, ListItemNode |\n| `packages/lexical-table/src/LexicalTableGeneratedJSON.ts` | TableNode, TableRowNode, TableCellNode |\n| `packages/lexical-code-core/src/LexicalCodeCoreGeneratedJSON.ts` | CodeNode, CodeHighlightNode |\n| `packages/lexical-react/src/shared/LexicalReactGeneratedJSON.ts` | DecoratorBlockNode |\n\nEach class receives its own generated code through its `$config`'s\n`generated` property, so nothing has to match code to class by type string at\nruntime. What the property holds is a factory: registration calls it with the\nclass's composed schema, and the generated code reads the lookup tables it\nneeds (`getterTable`, `setterTable`, `aliasedValue`'s) off that schema through\n`getterTableOf`, `setterTableOf` and `aliasTableOf`, so a generated module\ncarries no copy of a table and nothing about a table's contents is written at\nbuild time — only its type, so the field a value is assigned to is still\nchecked. A subclass inherits the code along with the schema when its compiled\naccessor tables are the ones the code was generated from — checked entry for\nentry at registration — and runs it over its own schema, while one that\noverrides an accessor a field stands in for, or declares a serialized property\nof its own, resolves differently and takes the schema-driven walk instead.\nGenerated exporters read `type` off the node for the same reason.\n\nThree things are worth knowing before touching it:\n\n- **The output is verified, not trusted.** The import direction is the\n  untrusted-JSON boundary, so every generated parser is run against the schema\n  it was compiled from over a corpus drawn from that schema plus a fixed set of\n  hostile values (`'__proto__'`, `'toString'`, `'1e999'`, …). A property whose\n  schema cannot be compiled faithfully takes its class out of the import half\n  rather than shipping a parser that disagrees with the walk — the script says\n  so on stdout when it happens.\n- **A stale checkout fails the tests.** `LexicalGeneratedJSON.test.ts`\n  regenerates into a temporary directory and compares every file byte for\n  byte, and separately asserts that each generated exporter agrees with the\n  schema-driven walk for both the legacy and compact forms. Change a schema\n  without rerunning this script and that test fails.\n- **Two lists, deliberately.** The script runs in two phases, because reading\n  the schemas means importing the packages and each package imports the file\n  the script writes for it. Phase one replaces every output with a valid\n  do-nothing stub from the static `MANIFEST` in\n  `scripts/shared/generateNodeJSONManifest.mjs` so the imports always succeed;\n  phase two re-enters under `tsx` and writes the real thing from `PACKAGES` in\n  `scripts/shared/generateNodeJSON.mjs`, which is also what the drift test\n  runs in-process. Adding a class means editing both lists, and the generation\n  fails loudly if they disagree.\n\nThe compact form compares each property against its default. A primitive\ndefault is a literal; a reference-typed default has no literal a value could\nbe `===`, so it gets the structural test the schema's own equality reduces to\nwhere that can be stated — MarkNode's `ids`, whose default is an empty array,\nbecomes a length test — and every emitted comparison is verified against that\nequality over a corpus, the way a parse is. A default the generator cannot\nstate that way (an object, a non-empty array, a non-finite number) takes the\nclass out of the compact half only: the script says so on stdout and the other\nforms are generated as usual. A class that carries flat NodeState is generated\nlike any other; the walk applies the state before handing the node to the\ngenerated parser, the mirror of how export appends it after the generated\nliteral.\n\n`afterCloneFrom` is generated too, and is the one direction whose fallback is\nnot the walk. A schema field is where a property is *stored*, so every class\nthat declares one gets an `afterCloneFrom` synthesized at registration —\ngenerated straight-line code when the class has some, and otherwise a loop over\nthe field names — copying the fields that class's own `$config` declared and\ndelegating the rest through `super`, which is why the emitted function covers\none class's own fields and nothing above it. A field an ancestor declares too\nis left to the ancestor, whose method has already assigned it: re-declaring an\ninherited property changes how it is serialized, not where it is stored, so a\nclass that only re-declares gets no method at all — `TabNode`, which restates\n`TextNode`'s `text`, `detail` and `mode`, is the in-tree case and simply\ninherits `TextNode`'s. Both accessor directions are read\nfor a field name, and the declared field is used rather than the one\n`resolveGetterAccessor` resolves to: an override changes how a property is\nserialized, not where it lives. A class that writes its own `afterCloneFrom` is\nleft alone and owns all of its properties, which is how `ElementNode` keeps\ncarrying `__first`/`__last`/`__size` and its slot bookkeeping; so is a property\ndeclared through accessor methods on both sides, which names no field for\nanything to copy. No in-tree node is in that second position — a property held\nin a field says so with `setter: {field, method}` and stays derived, as\n`MarkNode`'s `ids` does — so the boilerplate that remains is `ElementNode`'s\nand `CodeNode`'s, and each of those calls the generated `afterClone<Class>`\nfor its schema half and writes only the fields no schema describes.\n`ownSchemaFields` in `LexicalUtils.ts` is the single definition of that field\nlist, called by both the generator and the synthesized fallback.\n\nThe schema-to-JavaScript compiler itself lives in `@lexical/compiler`'s\n`SchemaJsonCodegen` entry point, so it is testable independently of the\ngenerator that drives it.\n\n## Scripts for release managers\n\n### pnpm run extract-codes\n\nThis will run a build that also extracts the generated error codes.json file.\n\nThis should be done, at minimum, before each release, but not in any PR as\nit would cause conflicts between serial numbers.\n\nIt's safe and probably advisable to do this more often, possibly any time a\nbranch is merged to main.\n\nThe codes.json file is also updated any time a release build is generated\nas a failsafe to ensure that these codes are up to date in a release.\nThis command runs a development build to extract the codes which is much\nfaster as it is not doing any optimization/minification steps.\n\n### pnpm run increment-version\n\nIncrement the monorepo version. The `-i` argument must be one of\n`minor` | `patch` | `prerelease`.\n\nThe postversion script will:\n- Create a local `${CHANNEL}__release` branch (the version-named\n  `${npm_package_version}__release` branch is only a remote push target,\n  and only when the channel is not `nightly`)\n- `pnpm run update-version` to update example and sub-package monorepo dependencies\n- `pnpm install` to update the pnpm-lock.yaml\n- `pnpm run update-packages` to update other generated config\n- `pnpm run extract-codes` to extract the error codes and\n  `pnpm run update-changelog` to update the changelog (both only when\n  `CHANNEL` is `latest`)\n- Create a version commit and tag from the branch\n\nThis is typically executed through the `version.yml` GitHub Workflow which\nwill also push the tag and branch.\n\n### pnpm run changelog\n\nUpdate the changelog from git history.\n\n### pnpm run release\n\n*Prerequisites:* all of the previous release manager scripts,\nplus creating a tag in git, and likely other steps.\n\nRuns prepare-release to do a full build and then uploads to npm.\n\n### pnpm run setup-trusted-publishing\n\nOne-time (idempotent) helper to register every public package with\n[npm trusted publishing](https://docs.npmjs.com/trusted-publishers).\nRe-run it whenever a new public package is added.\n\n#### Prerequisites\n\n- Node.js — whatever the repo's root `package.json#engines.node` says (currently `>=20.19.0`). Running with Node 24+ is recommended because that's what CI uses for publishes.\n- pnpm — pinned by `package.json#packageManager` (currently `pnpm@11.24.0`). Activate with [corepack](https://nodejs.org/api/corepack.html) or install directly.\n- npm CLI — **`npm ≥ 11.10`** (`npm i -g npm@latest`). The `npm trust` subcommand was added in npm 11; older versions will fail the preflight check.\n- An authenticated npm session (`npm login --registry https://registry.npmjs.org`) on a publisher account that has **account-level 2FA enabled** and write access to every `@lexical/*` package.\n\n#### Usage\n\nRun in check-only mode first:\n\n```bash\npnpm run setup-trusted-publishing\n```\n\nFor each public package in the monorepo, it queries\n`https://registry.npmjs.org` and reports whether the name is already\nclaimed. Packages that *don't* exist on the registry are listed; you\ncan re-run with `--bootstrap` to publish a deprecated\n`0.0.0-bootstrap.0` placeholder under the `bootstrap` dist-tag so the\nname can be claimed:\n\n```bash\nnpm login --registry https://registry.npmjs.org\npnpm run setup-trusted-publishing --bootstrap\n```\n\nOnce a package exists on the registry, you can configure trusted\npublishing for it programmatically by adding `--setup-trust`. This\nruns `npm trust github` under the hood (requires `npm` ≥ 11.10 and an\nauthenticated session with account-level 2FA on the publishing\naccount), and is idempotent — the script reads the existing trust\nconfiguration for each package via a read-only registry call (no OTP)\nand skips packages whose config already matches:\n\n```bash\nnpm login --registry https://registry.npmjs.org\npnpm run setup-trusted-publishing --setup-trust\n```\n\n`npm trust github` is a write operation, so each package that *does*\nneed configuring will trigger a one-time-password / web-auth prompt.\nOn the first prompt npm prints a URL; open it in a browser, sign in,\nand tick **\"Skip two-factor authentication for the next 5 minutes\"**.\nSubsequent packages in the same run will then go through without\nre-prompting. The script also inserts a small (~2 s) pause between\ncalls to stay under the registry's `E429` rate limit.\n\nFor full first-time setup of a brand-new monorepo, combine both flags:\n\n```bash\npnpm run setup-trusted-publishing --bootstrap --setup-trust\n```\n\nWhen adding a **single** new package to an existing monorepo — the common\ncase going forward — pass its name so the run only touches that package\ninstead of re-checking all 30+ already-configured ones (which just prints a\nwall of `CONFLICT` lines). The name can be the full npm name or the unscoped\nshort name, and `--package` / positional args are interchangeable and\nrepeatable:\n\n```bash\npnpm run setup-trusted-publishing --bootstrap --setup-trust @lexical/a11y\n# equivalently: --package a11y\n```\n\nUseful flags:\n\n- `--package <name>` (or a positional `<name>`, repeatable) — restrict the run to the given package(s), matched by full npm name (`@lexical/a11y`) or unscoped short name (`a11y`). Omit to process every public package.\n- `--dry-run` — print what would happen without touching the registry (works with both `--bootstrap` and `--setup-trust`)\n- `--workflow <filename>` — override the workflow filename (default `pre-release.yml`)\n- `--repo <owner/name>` — override the GitHub repo (default `facebook/lexical`)\n- `--stub-version <semver>` — override the placeholder version (default `0.0.0-bootstrap.0`)\n- `--registry <url>` — override the npm registry\n\nIn the default (check-only) mode the script also prints the npmjs.com\n`/access` URL for each existing package and the exact values to enter\nmanually, as a fallback for when `npm trust github` isn't an option.\n\n### Testing trusted publishing from a PR branch\n\nThe \"Publish to NPM\" workflow (`pre-release.yml`) exposes `ref`,\n`channel`, and `increment-version` inputs so it doubles as a test\nharness. Picking a branch in the \"Run workflow\" dropdown selects\nwhich version of the workflow files run, and the inputs determine\nwhat actually gets published. The workflow has no NPM_TOKEN secret to\nfall back on — publishes always go through OIDC trusted publishing —\nso a misconfigured trust setup fails loudly rather than silently\nfalling through to token auth.\n\nA safe end-to-end test looks like:\n\n| Input | Value |\n| -- | -- |\n| Branch (dropdown) | your PR branch |\n| `ref` | your PR branch (same value) |\n| `channel` | `dev` |\n| `increment-version` | checked |\n| `ignore-previously-published` | unchecked |\n\nWith `increment-version` on, the run bumps `package.json` to a fresh\nprerelease (e.g. `0.46.0-dev.0`), commits + tags it on a `dev__release`\nbranch on origin, and publishes the monorepo under the `dev` dist-tag\nvia OIDC. The `latest` tag is untouched, so default `npm install`\nusers are unaffected. After it succeeds:\n\n```bash\nnpm view lexical@dev version     # → the just-published prerelease\nnpm view lexical@latest version  # → unchanged\n```\n\nCleanup (the prerelease itself can't be reused, but the git refs\nshould go):\n\n```bash\ngit push --delete origin v0.46.0-dev.0 dev__release 0.46.0-dev.0__release\n```\n\nThe `increment-version=true + channel=latest` combination is refused\nby the workflow's guard job — real `latest` releases must go through\n`version.yml` first.\n\n## Continuous releases (pkg.pr.new)\n\nEvery commit to a pull request and every commit to `main` publishes all public\npackages to [pkg.pr.new](https://github.com/stackblitz-labs/pkg.pr.new) through\nthe `pkg-pr-new.yml` workflow. Nothing goes to npm: the nightly, next and latest\nchannels above are unchanged and still publish through `call-release.yml`.\n\n```sh\nnpm i https://pkg.pr.new/lexical@9372      # latest commit of PR #9372\nnpm i https://pkg.pr.new/lexical@4282a68   # a specific commit\nnpm i https://pkg.pr.new/lexical@main      # tip of main\n```\n\nThe bot comments install commands on each pull request. Previews are built\nwith `pnpm run prepare-release`, the same build as the npm channels, after\n`scripts/npm/preview-version.mjs` stamps a version in the nightly line:\n`0.52.1-pr.9372.g4282a68` for a PR and `0.52.1-main.g4282a68` for `main`, next\nto `0.52.1-nightly.20261019.0` on npm. That version is also what\n`LexicalEditor.version` reports. Dependencies between Lexical packages are\nrewritten to the same commit's pkg.pr.new URLs, so `@lexical/react` from a\npreview depends on `lexical` from that preview, not on an npm release.\n\nThe bot's comment lists npm commands, and npm dedupes those dependencies by\nname and version, so `@9372` or `@main` URLs are fine. pnpm and yarn key URL\ndependencies by URL instead: install with the long commit URLs that the\npackages use for each other, or you get more than one copy of `lexical`. pnpm\n11+ also refuses URL dependencies of URL dependencies unless\n`blockExoticSubdeps` is off. Each run's job summary lists these commands:\n\n```sh\npnpm add --config.block-exotic-subdeps=false \\\n  lexical@https://pkg.pr.new/facebook/lexical/lexical@4282a68 \\\n  @lexical/react@https://pkg.pr.new/facebook/lexical/@lexical/react@4282a68\nyarn add \\\n  lexical@https://pkg.pr.new/facebook/lexical/lexical@4282a68 \\\n  @lexical/react@https://pkg.pr.new/facebook/lexical/@lexical/react@4282a68\n```\n\nOn a cold cache, yarn 1 can fail with \"the file appears to be corrupt\" while\nit downloads the same URL for several packages at once. Run it again.\n\nThe workflow requires the [pkg.pr.new GitHub App](https://github.com/apps/pkg-pr-new)\nto be installed on the repository.\n\n## Release Procedure\n\nThis is the current release procedure for public releases, at least as of\nMay 2024 (~0.15.0).\n\nThe main branch should be \"frozen\" during this procedure (no other PRs should\nbe merged during this time). This avoids a mismatch between the contents of\nthe GitHub release (created from main in step 1) and the NPM release (created\nfrom main in step 4).\n\n1. Create a new version with the Github Actions \"Create New Release Branch\" workflow (`version.yml`)\n2. Raise a PR against version branch created by that action\n3. After PR is approved with passing tests, merge PR\n4. After PR is merged to main, publish to NPM with the Github Actions \"Publish to NPM\" workflow (`pre-release.yml`)\n5. Create a GitHub release from the tag created in step 1, manually editing the release notes\n6. Announce the release in #announcements on Discord\n\n## Release Protocol\n\n1. All PRs with breaking changes must have `[Breaking Change]` in the PR's title with documentation of what followup actions consumers of the lexical library need to be aware of.\n2. Monthly releases happen on the last week of the month, with a minor increment (eg. v0.20+1.0).\n3. Anything in between will be a patch increment (eg. 0.20.0+1), unless there is a breaking change. \n\n## Website Team Page\n\nThe [team page](https://lexical.dev/community) displays core team\nmembers, emeriti, and distinguished contributors. The `team.json` data is\ngenerated from GitHub contributor information and some predetermined decisions in\nthe script to acknowledge emeriti and historically important distinguished contributors.\n\nTo update the team page data:\n\n```bash\npnpm run update-team-data\n```\n\nThis fetches the latest contributor data from GitHub and categorizes team members\nbased on recent activity (last 12 months). See `packages/lexical-website/src/data/README.md`\nfor more details on configuration and team categorization logic.\n","isInternal":false,"tokens":6457,"sizeBytes":26855},{"name":"_category_.json","path":"packages/lexical-website/docs/react/_category_.json","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-website/docs/react/_category_.json","title":"React Skill","category":"anthropic-skill","format":"json","content":"{\n  \"label\": \"React\",\n  \"position\": 5\n}\n","frontmatter":{"label":"React","position":5},"isInternal":false,"tokens":16,"sizeBytes":40},{"name":"create_plugin.md","path":"packages/lexical-website/docs/react/create_plugin.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-website/docs/react/create_plugin.md","title":"React Skill","category":"anthropic-skill","format":"markdown","content":"# Creating a React Plugin\n\nIn addition to using the Lexical React plugins offered by the core library, you can make your own plugins to extend or alter Lexical's functionality to suit your own use cases.\n\nLexical's React plugin interface is simple - just create a React component and add it as a child of your LexicalComposer component:\n\n```jsx\n <LexicalComposer initialConfig={initialConfig}>\n    <MyLexicalPlugin />\n </LexicalComposer>\n```\n\nIf the Plugin introduces new nodes, they have to be registered in `initialConfig.nodes`:\n\n```js\nconst initialConfig = {\n  namespace: 'MyEditor',\n  nodes: [MyLexicalNode],\n};\n```\n\n```jsx\n <LexicalComposer initialConfig={initialConfig}>\n    <MyLexicalPlugin />\n </LexicalComposer>\n```\n\nLexicalComposer provides access to the underlying LexicalEditor instance via React Context:\n\n```jsx\n//MyLexicalPlugin.js\n\nexport function MyLexicalPlugin(props) {\n    const [editor] = useLexicalComposerContext();\n    ...\n}\n```\n\nWith access to the Editor, your plugin can extend Lexical via [Commands](../concepts/commands.md), [Transforms](../concepts/transforms.md), or other APIs. For example, the [TwitterPlugin](https://github.com/facebook/lexical/blob/0775ab929e65723433626fa8c25900941e7f232f/packages/lexical-playground/src/plugins/TwitterPlugin/index.ts#L18) embeds a tweet into the editor, fetching the data asynchronously from Twitter based on the provided Tweet ID:\n\n```jsx\nexport const INSERT_TWEET_COMMAND: LexicalCommand<string> = createCommand();\n\nexport default function TwitterPlugin(): JSX.Element | null {\n  const [editor] = useLexicalComposerContext();\n\n  useEffect(() => {\n    if (!editor.hasNodes([TweetNode])) {\n      throw new Error('TwitterPlugin: TweetNode not registered on editor (initialConfig.nodes)');\n    }\n\n    return editor.registerCommand<string>(\n      INSERT_TWEET_COMMAND,\n      (payload) => {\n        const tweetNode = $createTweetNode(payload);\n        $insertNodeToNearestRoot(tweetNode);\n\n        return true;\n      },\n      COMMAND_PRIORITY_EDITOR,\n    );\n  }, [editor]);\n\n  return null;\n}\n```\n\nTwitterPlugin is just a React component that accesses the Lexical editor via React Context (useLexicalComposerContext). Using the LexicalEditor instance, this plugin does two things:\n\n1. Verifies that there is a TweetNode registered on the editor (if you forget to register the node, you can't do #2)\n2. registers a \"command\", passing a callback that will run when that command is dispatched. The command callback creates and inserts a TweetNode in the editor.\n\nYou can see how [TwitterPlugin is used in the playground](https://github.com/facebook/lexical/blob/0775ab929e65723433626fa8c25900941e7f232f/packages/lexical-playground/src/Editor.tsx#L137). It's added as a child of a LexicalComposer component, which does the job of providing the Context necessary for access to the editor instance. To actually trigger this command callback and insert a [TweetNode](https://github.com/facebook/lexical/blob/b0fa38615c03f1c4fc7c8c5ea26412b723770e55/packages/lexical-playground/src/nodes/TweetNode.tsx#L212), we have a [button](https://github.com/facebook/lexical/blob/b0fa38615c03f1c4fc7c8c5ea26412b723770e55/packages/lexical-playground/src/plugins/ToolbarPlugin.tsx#L534) that \"dispatches\" the Tweet command we registered in the plugin.\n\nWhile the TwitterPlugin registers a command that inserts a custom node, this is only one example of what can be done with a plugin. To get a better idea of what's possible, take a look at the [plugins defined in the playground](https://github.com/facebook/lexical/tree/main/packages/lexical-playground/src/plugins).\n","isInternal":false,"tokens":861,"sizeBytes":3614},{"name":"faq.md","path":"packages/lexical-website/docs/react/faq.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-website/docs/react/faq.md","title":"React Skill","category":"anthropic-skill","format":"markdown","content":"# React FAQ\n\n## My app does not work in dev when using StrictMode, help!?\n\nWhen hooks are used correctly, there are no known issues with React StrictMode\nand Lexical. The first thing you should do is go through React's documentation\nto make sure that your usage of `useEffect` and other hooks follow React's\nconventions and guidelines. This is a great place to start:\n[My Effect runs twice when the component mounts](https://react.dev/reference/react/useEffect#my-effect-runs-twice-when-the-component-mounts)\n\nSome Lexical-specific concerns (which are consequences of React's\nconcurrent and StrictMode semantics, not due to anything unusual in Lexical):\n\n- In React 19, `useMemo` calls are cached across StrictMode re-renders, so\n  only one editor will be used for both renders. If you have a `useEffect`\n  call with side-effects (such as updating the document when a plug-in\n  initializes), then you should first check to make sure that this effect\n  has not already occurred (e.g. by checking the state of the document or\n  undoing the change as a cleanup function returned by the effect)\n- `LexicalComposer`'s initialConfig prop is only considered once during\n  the first render (`useMemo` is used to create the `LexicalComposerContext`\n  which includes the editor and theme)\n- If you are using an `editorState` argument in the config when creating the\n  editor, it will only be called once when the editor is created.\n- You should generally prefer to use hooks that return state such as\n  `useLexicalEditable` (`useLexicalSubscription` is a generalization of this\n  style) rather than manually registering the listeners and expecting a\n  particular sequence of triggers to be called, especially\n  when their source is an effect. Listeners are only called when state\n  changes, and in StrictMode the state may have changed during the initial\n  render. The listeners registered from your second render will not be called\n  if the change was triggered by the first render, and you will likely not\n  see the listeners triggered during the first render because those effects\n  were immediately cleaned up before the change effect occurred.\n\n## LexicalComposerContext.useLexicalComposerContext: cannot find a LexicalComposerContext\n\nThis error happens for one reason: the `useLexicalComposerContext()` hook\nwas called from a component that is not a child of a `LexicalComposer`,\n`LexicalNestedComposer`, or `LexicalComposerContext.Provider` from the same\nbuild of Lexical that the hook was imported from.\n\nThe most common root causes of this issue are:\n\n- You are trying to use `useLexicalComposerContext()` in a component that is\n  not a child of the `LexicalComposer`. If you need to do that, you need to\n  pass the context or editor up the tree with something like `EditorRefPlugin`.\n- You have multiple builds of Lexical in your project. This could be because\n  you have a dependency that has a direct dependency on some other version\n  of Lexical (these packages should have Lexical as `peerDependencies`, but\n  not all do), or because your bundler resolves Lexical to different files\n  from different places (for example the `development` build in one and the\n  `production` build in another, or a `source` condition in one and `dist`\n  in another). Resolving this generally requires overriding what\n  your package manager does in `package.json`, and/or what the bundler does in\n  some configuration file for your framework or bundler. There are a lot of\n  combinations of tools in the ecosystem (npm, pnpm, yarn, webpack, vite,\n  next.js, etc.), so the syntax of that workaround is quite dependent on\n  precisely which tools (and even versions of those tools) that your project\n  is using. See [One Lexical per app](../concepts/one-lexical-per-app.md) for\n  why one copy is required, how to confirm you have more than one, and why a\n  library should declare Lexical in `peerDependencies`.\n\n## Hot Module Replacement (HMR)\n\nDuring development, HMR re-executes modules on every code change. Because Lexical uses object identity for node class registration, command dispatch, and extension deduplication, a naive HMR cycle destroys the editor state and resets the document.\n\n### HMRExtension\n\n`@lexical/extension` exports an `HMRExtension` that preserves editor state, the selection, the editable flag, and undo/redo history across HMR cycles. It stashes a reference to the current `EditorState` in the bundler's HMR data store, and the module instance that replaces it serializes that state and rebuilds it against the new node classes. Serializing only at that point keeps the cost off of every editor update.\n\nThe editor state and the undo/redo entries are serialized together, as one family of related states, rather than one at a time. Serializing each state on its own (`toJSON()` / `parseEditorState()`) would lose two properties the editor depends on: consecutive versions of a document share nearly all of their node objects, because an update clones only what it touches, and reconciliation is a diff over `NodeKey`s. Independently parsed states share nothing and have disjoint key spaces, so the history would cost memory proportional to its depth and the first undo after a reload would rebuild the whole document instead of the part that changed. The family format stores one entry per distinct node *version* and rebuilds each one once, so what was shared comes back shared and every version of a node answers to one key — which is also what lets the selection be carried by key.\n\n```ts\nimport {buildEditorFromExtensions, configExtension, defineExtension, HMRExtension} from '@lexical/extension';\nimport {RichTextExtension} from '@lexical/rich-text';\nimport {HistoryExtension} from '@lexical/history';\n\nconst editor = buildEditorFromExtensions(\n  defineExtension({\n    name: '[root]',\n    namespace: 'my-editor',\n    dependencies: [\n      RichTextExtension,\n      HistoryExtension,\n      configExtension(HMRExtension, {hot: import.meta.hot ?? null}),\n    ],\n  }),\n);\n```\n\nThe `hot` config accepts any object with a mutable `data: Record<string, unknown>` property — Vite's `import.meta.hot` satisfies this directly. Webpack and Parcel expose `module.hot` instead, whose `data` is populated by dispose handlers and is not directly mutable, so `module.hot` cannot be passed here; those bundlers need a small adapter built on `module.hot.addDisposeHandler`. Pass `null` in production or when HMR is not available; the extension becomes a no-op.\n\nWhen `HistoryExtension` is present as a peer, undo/redo stacks are preserved automatically. The extension does not declare `HistoryExtension` as a dependency — it detects it at runtime via peer dependency lookup.\n\nEditors with distinct `namespace` values — set via `namespace` in `defineExtension` or `createEditor` — are isolated automatically, so no `id` is needed. An editor given no namespace at all is keyed without one, since the namespace `createEditor` generates in that case is a new random string on every reload; those editors all share a single key. When two editors share both the same `import.meta.hot` context and the same key, pass a stable `id` string to keep their states independent:\n\n```ts\ndefineExtension({ name: '[main]', namespace: 'shared', dependencies: [configExtension(HMRExtension, {hot: import.meta.hot ?? null, id: 'main'})] })\ndefineExtension({ name: '[sidebar]', namespace: 'shared', dependencies: [configExtension(HMRExtension, {hot: import.meta.hot ?? null, id: 'sidebar'})] })\n```\n\nA nested editor inherits its parent's namespace, so the two are not isolated from each other automatically the way independent editors are; give the nested editor a distinct `id`. In development, sharing a key with the parent is warned about.\n\nSaved state belongs to the namespace and `id` for as long as the page lives, not to the reload that produced it. An editor that is unmounted and later remounted under the same key during development restores what the previous one had rather than its own `$initialEditorState` — so two editors showing different documents need distinct namespaces even when they are never on screen at the same time.\n\n### Fast Refresh compatibility\n\nVite (and similar tools) apply React Fast Refresh — state-preserving HMR for React components — only when a module exports nothing but React components. Modules that also export hooks, classes, commands, or constants fall back to a full remount, which discards component state.\n\nSeveral `@lexical/react` plugin modules split their non-component exports into companion `*Utils` files. Consumers that import non-component values directly from the `*Utils` module get more granular HMR boundaries, since changes to the component file don't invalidate those imports. The original module re-exports these for backwards compatibility.\n\nA companion module only earns that boundary if it imports no component module itself, so shared values it needs have to live somewhere component-free too: `MenuOption` — the class a menu plugin's options extend — is exported from `@lexical/react/LexicalMenuOption` for that reason, and re-exported from the plugin modules that used to define it.\n\nValues shared between entry points also have to be defined in one, not in an internal module: an internal module is inlined into every entry point that imports it, so a command or class defined there is a different object in each of them, and commands and `instanceof` are matched by identity.\n\nIf you're building custom plugins, follow the same pattern: keep React components in one file and export hooks, constants, or classes from a separate file.\n\n### Fallback: `// @refresh reset`\n\nIf a module can't be split (e.g. it defines both a component and tightly-coupled non-component logic), you can mark it for a full refresh using your framework's directive. For example, [Next.js fast refresh](https://nextjs.org/docs/architecture/fast-refresh#tips) supports a `// @refresh reset` comment at the top of the file. This forces a full remount of all components in the file on every change.\n","isInternal":false,"tokens":2192,"sizeBytes":10037},{"name":"index.md","path":"packages/lexical-website/docs/react/index.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-website/docs/react/index.md","title":"React Skill","category":"anthropic-skill","format":"markdown","content":"---\nsidebar_label: 'Introduction'\n---\n\n# Lexical + React\n\nTo make it easier for React users to implement rich-text editors, Lexical exposes a set of abstractions that aim to cover a wide range of use cases out-of-the-box.\n\n<a\n    class=\"whitespace-nowrap rounded-md bg-blue-500 px-6 py-2 text-sm font-bold text-white transition-opacity hover:text-white hover:no-underline hover:opacity-90\"\n    href=\"/docs/getting-started/react\">\n{`Getting Started Guide`}\n</a>\n\n## Supported Versions\n\nReact 18+ (tested with React 18 and React 19)\n","frontmatter":{"sidebar_label":"Introduction"},"isInternal":false,"tokens":130,"sizeBytes":531},{"name":"plugins.md","path":"packages/lexical-website/docs/react/plugins.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-website/docs/react/plugins.md","title":"React Skill","category":"anthropic-skill","format":"markdown","content":"# Lexical Plugins\n\n:::tip\n\nMost of these plugins are available as modern\n[Lexical Extensions](../extensions/intro.md), which do not require separate\nconfiguration and can have more efficient implementations.\nYou should prefer using extensions over plugins where available, and\n[migrate](../extensions/migration.mdx) any project to extensions with\nLexicalExtensionComposer or LexicalExtensionEditorComposer if it is still\nusing LexicalComposer.\n\n:::\n\nReact-based plugins use a Lexical editor instance from `<LexicalExtensionComposer>` or `<LexicalComposer>` (legacy) context:\n\n```js\nimport {LexicalComposer} from '@lexical/react/LexicalComposer';\nimport {PlainTextPlugin} from '@lexical/react/LexicalPlainTextPlugin';\nimport {ContentEditable} from '@lexical/react/LexicalContentEditable';\nimport {LexicalErrorBoundary} from '@lexical/react/LexicalErrorBoundary';\nimport {HistoryPlugin} from '@lexical/react/LexicalHistoryPlugin';\nimport {OnChangePlugin} from '@lexical/react/LexicalOnChangePlugin';\n```\n\n```jsx\nconst initialConfig = {\n  namespace: 'MyEditor',\n  theme,\n  onError,\n};\n\n<LexicalComposer initialConfig={initialConfig}>\n  <PlainTextPlugin\n    contentEditable={\n      <ContentEditable placeholder={<div>Enter some text...</div>} />\n    }\n    ErrorBoundary={LexicalErrorBoundary}\n  />\n  <HistoryPlugin />\n  <OnChangePlugin onChange={onChange} />\n  ...\n</LexicalComposer>;\n```\n\n> Note: Many plugins might require you to register the one or many Lexical nodes in order for the plugin to work. You can do this by passing a reference to the node to the `nodes` array in your initial editor configuration.\n\n```jsx\nconst initialConfig = {\n  namespace: 'MyEditor',\n  theme,\n  nodes: [ListNode, ListItemNode], // Pass the references to the nodes here\n  onError,\n};\n```\n\n### `LexicalPlainTextPlugin`\n\nReact wrapper for `@lexical/plain-text` that adds major features for plain text editing, including typing, deletion and copy/pasting.\n\n```jsx\n<PlainTextPlugin\n  contentEditable={\n    <ContentEditable\n      aria-placeholder={'Enter some text...'}\n      placeholder={<div>Enter some text...</div>}\n    />\n  }\n  ErrorBoundary={LexicalErrorBoundary}\n/>\n```\n\n:::tip\n\nUse [PlainTextExtension](/docs/api/modules/lexical_plain-text#plaintextextension) when using extensions\n\n:::\n\n### Configuring an error boundary for React extensions\n\nThe `ErrorBoundary` prop on `PlainTextPlugin` and `RichTextPlugin` is configured\nthrough `ReactExtension` when using `PlainTextExtension` or\n`RichTextExtension`. Add a configured `ReactExtension` to the root extension's\ndependencies:\n\n```tsx\nimport {configExtension, defineExtension} from 'lexical';\nimport {PlainTextExtension} from '@lexical/plain-text';\nimport {ReactExtension} from '@lexical/react/ReactExtension';\nimport {LexicalErrorBoundary} from '@lexical/react/LexicalErrorBoundary';\nimport type {ReactElement} from 'react';\n\ntype ErrorBoundaryProps = {\n  children: ReactElement;\n  onError: (error: Error) => void;\n};\n\nfunction CustomErrorBoundary({children, onError}: ErrorBoundaryProps) {\n  return (\n    <LexicalErrorBoundary\n      fallback={<div>Something went wrong.</div>}\n      onError={onError}>\n      {children}\n    </LexicalErrorBoundary>\n  );\n}\n\nconst editorExtension = defineExtension({\n  name: 'MyEditor',\n  dependencies: [\n    PlainTextExtension,\n    configExtension(ReactExtension, {\n      ErrorBoundary: CustomErrorBoundary,\n    }),\n  ],\n});\n```\n\nIf you do not provide `ErrorBoundary`, `ReactExtension` uses its default\n`LexicalErrorBoundary`. Replace `CustomErrorBoundary` with\n`LexicalErrorBoundary` when the default fallback and error reporting are\nsufficient.\n\n### `LexicalRichTextPlugin`\n\nReact wrapper for `@lexical/rich-text` that adds major features for rich text editing, including typing, deletion, copy/pasting, indent/outdent and bold/italic/underline/strikethrough text formatting.\n\n```jsx\n<RichTextPlugin\n  contentEditable={\n    <ContentEditable\n      aria-placeholder={'Enter some text...'}\n      placeholder={<div>Enter some text...</div>}\n    />\n  }\n  ErrorBoundary={LexicalErrorBoundary}\n/>\n```\n\n:::tip\n\nUse [RichTextExtension](/docs/api/modules/lexical_rich-text#richtextextension) when using extensions\n\n:::\n\n### `LexicalAutoFocusPlugin`\n\nAutomatically focuses the editor when the component mounts and optionally places the cursor at the start or end of the root content.\n\n```jsx\n<AutoFocusPlugin defaultSelection=\"rootEnd\" />\n```\n\n:::tip\n\nUse [AutoFocusExtension](/docs/api/modules/lexical_extension#autofocusextension) when using extensions\n\n:::\n\n### `LexicalOnChangePlugin`\n\nPlugin that calls `onChange` whenever Lexical state is updated. Using `ignoreHistoryMergeTagChange` (`true` by default) and `ignoreSelectionChange` (`false` by default) can give more granular control over changes that are causing `onChange` call.\n\n```jsx\n<OnChangePlugin onChange={onChange} />\n```\n\n:::tip\n\nWhen using extensions, subscribe to [EditorStateExtension](/docs/api/modules/lexical_extension#editorstateextension) for a signal-based equivalent\n\n:::\n\n### `LexicalHistoryPlugin`\n\nReact wrapper for `@lexical/history` that adds support for history stack management and `undo` / `redo` commands.\n\n```jsx\n<HistoryPlugin />\n```\n\n:::tip\n\nUse [HistoryExtension](/docs/api/modules/lexical_history#historyextension) when using extensions\n\n:::\n\n### `LexicalLinkPlugin`\n\nReact wrapper for `@lexical/link` that adds support for links, including `$toggleLink` command support that toggles link for selected text.\n\n```jsx\n<LinkPlugin />\n```\n\n:::tip\n\nUse [LinkExtension](/docs/api/modules/lexical_link#linkextension) when using extensions\n\n:::\n\n### `LexicalClickableLinkPlugin`\n\nMakes `LinkNode` elements in the editor clickable, navigating to the link URL on click. Set `disabled` to `true` to turn off the behavior, or `newTab` to control whether links open in a new tab.\n\n> Note: Requires `LinkNode` from `@lexical/link` to be registered.\n\n```jsx\n<ClickableLinkPlugin newTab={true} disabled={false} />\n```\n\n:::tip\n\nUse [ClickableLinkExtension](/docs/api/modules/lexical_link#clickablelinkextension) when using extensions\n\n:::\n\n### `LexicalListPlugin`\n\nReact wrapper for `@lexical/list` that adds support for lists (ordered and unordered)\n\n```jsx\n<ListPlugin />\n```\n\n:::tip\n\nUse [ListExtension](/docs/api/modules/lexical_list#listextension) when using extensions\n\n:::\n\n### `LexicalCheckListPlugin`\n\nReact wrapper for `@lexical/list` that adds support for check lists. Note that it requires some css to render check/uncheck marks. See PlaygroundEditorTheme.css for details.\n\n```jsx\n<CheckListPlugin />\n```\n\n:::tip\n\nUse [CheckListExtension](/docs/api/modules/lexical_list#checklistextension) when using extensions\n\n:::\n\n### `LexicalTablePlugin`\n\n[![See API Documentation](/img/see-api-documentation.svg)](/docs/api/modules/lexical_react_LexicalTablePlugin)\n\nReact wrapper for `@lexical/table` that adds support for tables.\n\n```jsx\n<TablePlugin />\n```\n\n:::tip\n\nUse [TableExtension](/docs/api/modules/lexical_table#tableextension) when using extensions\n\n:::\n\n### `LexicalHorizontalRulePlugin`\n\nRegisters a command handler for `INSERT_HORIZONTAL_RULE_COMMAND`. When dispatched, it inserts a `HorizontalRuleNode` at the current selection.\n\n> Note: Requires `HorizontalRuleNode` to be registered. This plugin is deprecated in favor of `HorizontalRuleExtension` from `@lexical/extension`.\n\n```jsx\nimport {HorizontalRuleNode} from '@lexical/react/LexicalHorizontalRuleNode';\n\nconst config = {nodes: [HorizontalRuleNode]};\n\n<HorizontalRulePlugin />\n```\n\n:::tip\n\nUse [HorizontalRuleExtension](/docs/api/modules/lexical_extension#horizontalruleextension) when using extensions\n\n:::\n\n### `LexicalTabIndentationPlugin`\n\nPlugin that allows tab indentation in combination with `@lexical/rich-text`.\n\n```jsx\n<TabIndentationPlugin />\n```\n\n:::tip\n\nUse [TabIndentationExtension](/docs/api/modules/lexical_extension#tabindentationextension) when using extensions\n\n:::\n\n### `LexicalHashtagPlugin`\n\nAutomatically detects hashtag patterns (e.g., `#hello`) in text as the user types and converts them into `HashtagNode` instances with distinct styling.\n\n> Note: Requires `HashtagNode` from `@lexical/hashtag` to be registered.\n\n```jsx\nimport {HashtagNode} from '@lexical/hashtag';\n\nconst config = {nodes: [HashtagNode]};\n\n<HashtagPlugin />\n```\n\n:::tip\n\nUse [HashtagExtension](/docs/api/modules/lexical_hashtag#hashtagextension) when using extensions\n\n:::\n\n### `LexicalAutoLinkPlugin`\n\nPlugin will convert text into links based on passed matchers list. In example below whenever user types url-like string it will automatically convert it into a link node\n\n> Note: Requires `AutoLinkNode` from `@lexical/link` to be registered.\n\n```jsx\nconst URL_MATCHER =\n  /((https?:\\/\\/(www\\.)?)|(www\\.))[-a-zA-Z0-9@:%._+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/;\n\nconst MATCHERS = [\n  (text) => {\n    const match = URL_MATCHER.exec(text);\n    if (match === null) {\n      return null;\n    }\n    const fullMatch = match[0];\n    return {\n      index: match.index,\n      length: fullMatch.length,\n      text: fullMatch,\n      url: fullMatch.startsWith('http') ? fullMatch : `https://${fullMatch}`,\n      // attributes: { rel: 'noreferrer', target: '_blank' }, // Optional link attributes\n    };\n  },\n];\n\n...\n\n<AutoLinkPlugin matchers={MATCHERS} />\n```\n\n:::tip\n\nUse [AutoLinkExtension](/docs/api/modules/lexical_link#autolinkextension) when using extensions\n\n:::\n\n### `LexicalAutoEmbedPlugin`\n\nWatches for pasted links that match any of the provided embed configurations (e.g., YouTube, Twitter URLs). When a match is found, it shows a menu offering to replace the link with an embedded node.\n\n> Note: Requires `LinkNode` and `AutoLinkNode` from `@lexical/link` to be registered.\n\n```jsx\nimport {LexicalAutoEmbedPlugin, AutoEmbedOption} from '@lexical/react/LexicalAutoEmbedPlugin';\nimport type {EmbedConfig} from '@lexical/react/LexicalAutoEmbedPlugin';\n\nconst YouTubeEmbedConfig = {\n  type: 'youtube',\n  parseUrl: (url) => {\n    const match = url.match(/youtube\\.com\\/watch\\?v=([a-zA-Z0-9_-]+)/);\n    return match ? {url, id: match[1]} : null;\n  },\n  insertNode: (editor, result) => {\n    // Insert your custom embed node here\n  },\n};\n\n<LexicalAutoEmbedPlugin\n  embedConfigs={[YouTubeEmbedConfig]}\n  onOpenEmbedModalForConfig={(config) => {/* open modal */}}\n  getMenuOptions={(config, embedFn, dismissFn) => [\n    new AutoEmbedOption('Embed', {onSelect: embedFn}),\n    new AutoEmbedOption('Dismiss', {onSelect: dismissFn}),\n  ]}\n/>\n```\n\nThe optional `$disableMenuFor` predicate suppresses the menu for specific locations, e.g. inside nodes that only accept inline content. It receives the matched `AutoLinkNode`; when it returns true the menu is not shown and the pasted link is left as-is. The predicate can inspect the node's ancestors, e.g. with `$getNearestNodeOfType` from `@lexical/utils`.\n\n```jsx\n<LexicalAutoEmbedPlugin\n  embedConfigs={[YouTubeEmbedConfig]}\n  $disableMenuFor={(linkNode) =>\n    $getNearestNodeOfType(linkNode, InlineOnlyNode) !== null\n  }\n  // ...\n/>\n```\n\n### `LexicalCharacterLimitPlugin`\n\nTracks the number of characters in the editor and renders a count of remaining characters. Uses `OverflowNode` to visually indicate when the limit is exceeded.\n\n> Note: Requires `OverflowNode` from `@lexical/overflow` to be registered.\n\n```jsx\nimport {CharacterLimitPlugin} from '@lexical/react/LexicalCharacterLimitPlugin';\n\n<CharacterLimitPlugin charset=\"UTF-16\" maxLength={280} />\n```\n\n:::tip\n\nUse [OverflowExtension](/docs/api/modules/lexical_overflow#overflowextension) when using extensions\n\n:::\n\n### `LexicalClearEditorPlugin`\n\nAdds `clearEditor` command support to clear editor's content.\n\n```jsx\n<ClearEditorPlugin />\n```\n\n:::tip\n\nUse [ClearEditorExtension](/docs/api/modules/lexical_extension#cleareditorextension) when using extensions\n\n:::\n\n### `LexicalMarkdownShortcutPlugin`\n\nAdds markdown shortcut support: headings, lists, code blocks, quotes, links and inline styles (bold, italic, strikethrough).\n\n```jsx\n<MarkdownShortcutPlugin />\n```\n\n### `LexicalTableOfContentsPlugin`\n\nThis plugin allows you to render a table of contents for a page from the headings from the editor. It listens to any deletions or modifications to those headings and updates the table of contents. Additionally, it's able to track any newly added headings and inserts them in the table of contents once they are created. This plugin also supports lazy loading - so you can defer adding the plugin until when the user needs it.\n\nIn order to use `TableOfContentsPlugin`, you need to pass a callback function in its children. This callback function gives you access to the up-to-date data of the table of contents. You can access this data through a single parameter for the callback which comes in the form of an array of arrays `[[headingKey, headingTextContent, headingTag], [], [], ...]`\n\n`headingKey`: Unique key that identifies the heading.\n`headingTextContent`: A string of the exact text of the heading.\n`headingTag`: The heading's tag — `'h1'` through `'h6'`. Every heading in the document is listed; the plugin does not filter by level.\n\n```jsx\n<TableOfContentsPlugin>\n  {(tableOfContentsArray) => {\n    return (\n      <MyCustomTableOfContentsPlugin tableOfContents={tableOfContentsArray} />\n    );\n  }}\n</TableOfContentsPlugin>\n```\n\n### `LexicalEditorRefPlugin`\n\nAllows you to get a ref to the underlying editor instance outside of LexicalComposer, which is convenient when you want to interact with the editor\nfrom a separate part of your application.\n\n```jsx\nconst editorRef = useRef(null);\n<EditorRefPlugin editorRef={editorRef} />;\n```\n\n### `LexicalCollaborationPlugin`\n\nIntegrates Yjs-based real-time collaborative editing into Lexical. Creates a Yjs binding between the editor state and a shared Yjs document, renders remote user cursors, and provides collaborative undo/redo history.\n\n```jsx\nimport {LexicalCollaboration} from '@lexical/react/LexicalCollaborationContext';\nimport {CollaborationPlugin} from '@lexical/react/LexicalCollaborationPlugin';\nimport {WebsocketProvider} from 'y-websocket';\nimport {Doc} from 'yjs';\n\nfunction createProvider(id, yjsDocMap) {\n  const doc = new Doc();\n  yjsDocMap.set(id, doc);\n  return new WebsocketProvider('ws://localhost:1234', id, doc);\n}\n\n<LexicalCollaboration>\n  <LexicalComposer initialConfig={initialConfig}>\n    <CollaborationPlugin\n      id=\"my-document\"\n      providerFactory={createProvider}\n      shouldBootstrap={true}\n      username=\"Alice\"\n      cursorColor=\"#FF0000\"\n    />\n  </LexicalComposer>\n</LexicalCollaboration>\n```\n\n### `LexicalDraggableBlockPlugin`\n\nAdds a draggable block handle that appears next to top-level block nodes when hovered. Users can drag and drop blocks to reorder them. A target line indicates the drop position.\n\n> Note: This plugin is experimental.\n\n```jsx\nimport {DraggableBlockPlugin_EXPERIMENTAL} from '@lexical/react/LexicalDraggableBlockPlugin';\n\nconst menuRef = useRef(null);\nconst targetLineRef = useRef(null);\n\n<DraggableBlockPlugin_EXPERIMENTAL\n  anchorElem={document.body}\n  menuRef={menuRef}\n  targetLineRef={targetLineRef}\n  menuComponent={<div ref={menuRef} className=\"drag-handle\">::</div>}\n  targetLineComponent={<div ref={targetLineRef} className=\"target-line\" />}\n  isOnMenu={(el) => menuRef.current?.contains(el) ?? false}\n/>\n```\n\n### `LexicalNodeEventPlugin`\n\nAttaches a DOM event listener on the editor root element that fires only when the event target corresponds to a node of the specified type.\n\n```jsx\nimport {NodeEventPlugin} from '@lexical/react/LexicalNodeEventPlugin';\n\n<NodeEventPlugin\n  nodeType={ImageNode}\n  eventType=\"click\"\n  eventListener={(event, editor, nodeKey) => {\n    console.log('Clicked on node:', nodeKey);\n  }}\n/>\n```\n\n### `LexicalNodeMenuPlugin`\n\nRenders a menu anchored to a specific Lexical node identified by `nodeKey`. Provides keyboard navigation (arrow keys, enter, escape) through the menu options.\n\n```jsx\nimport {LexicalNodeMenuPlugin, MenuOption} from '@lexical/react/LexicalNodeMenuPlugin';\n\n<LexicalNodeMenuPlugin\n  nodeKey={selectedNodeKey}\n  options={menuOptions}\n  onSelectOption={(option, textNode, closeMenu) => {\n    option.action();\n    closeMenu();\n  }}\n  menuRenderFn={(anchorRef, {options, selectedIndex, selectOptionAndCleanUp}) =>\n    anchorRef.current\n      ? createPortal(\n          <ul>\n            {options.map((opt, i) => (\n              <li key={opt.key} onClick={() => selectOptionAndCleanUp(opt)}>\n                {opt.title}\n              </li>\n            ))}\n          </ul>,\n          anchorRef.current,\n        )\n      : null\n  }\n  onClose={() => setSelectedNodeKey(null)}\n/>\n```\n\n### `LexicalTypeaheadMenuPlugin`\n\nProvides a typeahead/autocomplete menu that appears when the user types a trigger character (e.g., `@` for mentions, `:` for emoji). It listens to text changes, runs a trigger function against text before the cursor, and when a match is found, positions a menu at the trigger location.\n\n```jsx\nimport {\n  LexicalTypeaheadMenuPlugin,\n  useBasicTypeaheadTriggerMatch,\n} from '@lexical/react/LexicalTypeaheadMenuPlugin';\n\nconst triggerMatch = useBasicTypeaheadTriggerMatch('@', {minLength: 1});\n\n<LexicalTypeaheadMenuPlugin\n  triggerFn={triggerMatch}\n  options={mentionOptions}\n  onQueryChange={setQuery}\n  onSelectOption={(option, textNode, closeMenu) => {\n    editor.update(() => {\n      const mentionNode = $createMentionNode(option.name);\n      if (textNode) {\n        textNode.replace(mentionNode);\n      }\n      closeMenu();\n    });\n  }}\n  menuRenderFn={(anchorRef, {options, selectedIndex, selectOptionAndCleanUp}) =>\n    anchorRef.current\n      ? createPortal(\n          <ul className=\"typeahead-menu\">\n            {options.map((opt, i) => (\n              <li\n                key={opt.key}\n                className={i === selectedIndex ? 'selected' : ''}\n                onClick={() => selectOptionAndCleanUp(opt)}>\n                {opt.name}\n              </li>\n            ))}\n          </ul>,\n          anchorRef.current,\n        )\n      : null\n  }\n/>\n```\n\n### `LexicalSelectionAlwaysOnDisplay`\n\nBy default, browser text selection becomes invisible when clicking away from the editor. This plugin ensures the selection remains visible.\n\n```jsx\n<SelectionAlwaysOnDisplay />\n```\n\n:::tip\n\nUse [SelectionAlwaysOnDisplayExtension](/docs/api/modules/lexical_extension#selectionalwaysondisplayextension) when using extensions\n\n:::\n","isInternal":false,"tokens":4368,"sizeBytes":18366},{"name":"_category_.json","path":"packages/lexical-website/docs/serialization/_category_.json","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-website/docs/serialization/_category_.json","title":"Serialization Skill","category":"anthropic-skill","format":"json","content":"{\n  \"label\": \"Serialization\",\n  \"position\": 4\n}\n","frontmatter":{"label":"Serialization","position":4},"isInternal":false,"tokens":16,"sizeBytes":48},{"name":"dom-import.md","path":"packages/lexical-website/docs/serialization/dom-import.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-website/docs/serialization/dom-import.md","title":"Serialization Skill","category":"anthropic-skill","format":"markdown","content":"# DOMImportExtension\n\n:::warning Experimental\n\n`DOMImportExtension` and everything described on this page are marked\n`@experimental` and may change between any two Lexical releases —\nincluding breaking renames, signature changes, or behavior changes —\nuntil the API stabilizes. We track issues and proposals in the\n[GitHub repo](https://github.com/facebook/lexical); breaking changes\nwill be called out in release notes. Apps that depend on this\npipeline should pin their Lexical version and treat upgrades as\nintentional.\n\nThe legacy static `importDOM` machinery and `$generateNodesFromDOM`\nentry are unchanged and remain the supported default for production\napps that don't want to track an experimental API.\n\nThe DOMImportExtension API was introduced in Lexical v0.45.0\n\n:::\n\nThe DOM import system in `@lexical/html` lets you convert any HTML or\nDOM tree into Lexical nodes. The legacy entry — the static\n`importDOM` declared on each node class — still works, but for new\ncode we offer an extension-based pipeline (`DOMImportExtension`) with\ntyped selectors, middleware-style rules, structural schemas,\nconfigurable whitespace handling, and a dedicated context system. It's\ndesigned for performance, ergonomics, and composability across\nextensions.\n\nThe new pipeline ships side-by-side with the legacy one in\n`@lexical/html`; the default `$generateNodesFromDOM` is unchanged. The\nnode-providing extensions (`RichTextExtension`, `ListExtension`,\n`LinkExtension`, `TableExtension`, `CodeExtension`, …) register their\nown import rules (plus the shared `CoreImportExtension` baseline), so\nan editor built from them already has a fully configured\n`DOMImportExtension` — to opt in, just read the imported nodes from\nthe extension's `$generateNodesFromDOM` output (or add\n`ClipboardDOMImportExtension` to route pastes through it).\n\n## Quick start\n\nThe smallest editor that imports HTML through the extension pipeline:\n\n```ts\nimport {\n  buildEditorFromExtensions,\n  getExtensionDependencyFromEditor,\n} from '@lexical/extension';\nimport {\n  CoreImportExtension,\n  DOMImportExtension,\n} from '@lexical/html';\nimport {$getRoot, defineExtension} from 'lexical';\n\nconst editor = buildEditorFromExtensions(\n  defineExtension({\n    name: 'app',\n    dependencies: [CoreImportExtension],\n  }),\n);\n\nconst dep = getExtensionDependencyFromEditor(editor, DOMImportExtension);\neditor.update(() => {\n  const dom = new DOMParser().parseFromString(\n    '<p>Hello <strong>world</strong></p>',\n    'text/html',\n  );\n  const nodes = dep.output.$generateNodesFromDOM(dom);\n  $getRoot().clear().splice(0, 0, nodes);\n});\n```\n\n`CoreImportExtension` ships the rules that cover `<p>`, `<span>`,\ninline format tags (`<b>`, `<strong>`, `<em>`, `<i>`, `<code>`,\n`<mark>`, `<s>`, `<sub>`, `<sup>`, `<u>`), `<br>`, and `#text` —\neverything the core lexical package's legacy `importDOM` machinery\nhandled — plus `<hr>`, gated on `HorizontalRuleNode` being registered\n(the node lives in `@lexical/extension`, which is upstream of\n`@lexical/html` and so cannot register import rules itself).\n\nThe per-tag rules ship with the extensions that provide the nodes, so\nlisting the node extensions you want is the entire import\nconfiguration:\n\n| Extension | Provides |\n| --- | --- |\n| `CoreImportExtension` (`@lexical/html`) | `<p>`, `<span>`, inline format tags, `<br>`, `#text`, `<hr>` (when `HorizontalRuleNode` is registered) |\n| `RichTextExtension` (`@lexical/rich-text`) | `<h1>`–`<h6>`, `<blockquote>`, Google Docs 26pt-title heuristic |\n| `ListExtension` (`@lexical/list`) | `<ol>`, `<ul>` (incl. checklist detection), `<li>`, GitHub task list, Joplin checkbox |\n| `LinkExtension` (`@lexical/link`) | `<a>` |\n| `TableExtension` (`@lexical/table`) | `<table>`, `<tr>`, `<td>`, `<th>` |\n| `CodeExtension` (`@lexical/code-core`) | `<pre>`, multi-line `<code>`, monospace `<div>`, GitHub raw-file-view tables |\n\nEach of these depends on `CoreImportExtension` itself, so the shared\nbaseline comes along automatically. The rules are inert unless the\neditor actually routes HTML through the pipeline (the legacy paste\npath is unchanged), and an extension's rules merge above its\ndependencies' rules, so app-registered rules still win dispatch.\n\n:::tip\n\nThe legacy entry `$generateNodesFromDOM(editor, dom)` and the new\nextension's `dep.output.$generateNodesFromDOM(dom)` both live in\n`@lexical/html`. Tell them apart by signature: the legacy takes the\neditor first, the new one takes only the DOM (plus optional\n`{context, preprocess}`).\n\n:::\n\n## Rules\n\nA rule is the unit of work in the extension. Each rule is built with\n`defineImportRule`:\n\n```ts\nimport {defineImportRule, sel} from '@lexical/html';\nimport {$createParagraphNode} from 'lexical';\n\nconst ParagraphRule = defineImportRule({\n  name: '@app/p',\n  match: sel.tag('p'),\n  $import: (ctx, el) => {\n    const p = $createParagraphNode();\n    p.splice(0, 0, ctx.$importChildren(el));\n    return [p];\n  },\n});\n```\n\nA rule has three parts:\n\n1. **`match`** — an opaque `CompiledSelector` produced by `sel.*`. The\n   dispatcher buckets rules by tag at compile time so the per-node\n   dispatch cost is bounded by the number of rules registered for\n   that tag (typically one or two).\n2. **`$import`** — middleware-style function `(ctx, node, $next) =>\n   readonly LexicalNode[]`. The element type of `node` is narrowed by\n   the selector (e.g. `sel.tag('a')` gives `HTMLAnchorElement`,\n   `sel.text()` gives `Text`). Return `[]` to drop the element, or\n   call `$next()` to defer to the next-lower matching rule.\n3. **`name`** (optional) — surfaces in dev-mode warnings and error\n   traces. Convention: `'@scope/package/rule-id'`.\n\n### Dispatch order\n\nThere is no numeric priority. `DOMImportConfig.rules` is a single flat,\nordered list, and dispatch walks it **front to back**: for a given DOM\nnode the dispatcher visits each rule whose `match` accepts that node, in\nlist order, and the first rule that returns without calling `$next()`\ndecides the outcome. \"Higher priority\" and \"earlier in the list\" are the\nsame thing.\n\nSo within one `configExtension(DOMImportExtension, {rules})` call, the\nrules run in the order you wrote them:\n\n```ts\nconfigExtension(DOMImportExtension, {\n  rules: [\n    // Tries this first\n    SpecificParagraphRule,\n    // Falls back to this via $next()\n    GenericParagraphRule,\n  ],\n})\n```\n\nCalling `$next()` from a rule walks the chain — the next matching rule\nin the list fires; if none is left, the framework's catch-all\n`DefaultHoistRule` descends into the element's children. Returning `[]`\nfrom a rule short-circuits: nothing else runs and the element is\ndropped.\n\n#### How the list is assembled\n\nMany extensions contribute to the same list. `DOMImportExtension`'s\n`mergeConfig` **prepends** each contribution to what has accumulated so\nfar, and the extension builder merges configs in dependency order — a\ndependency's contribution is merged before the contribution of the\nextension that depends on it. Two consequences:\n\n- **Each contribution stays contiguous and keeps its own order.** Your\n  `rules` array is inlined as one chunk, first entry first. Ordering\n  *within* your own rules is entirely up to you.\n- **The chunks run most-dependent first.** An extension's rules are\n  tried before the rules contributed by its dependencies. That is what\n  makes overriding work: `@lexical/rich-text` depends on\n  `CoreImportExtension`, so its `<p>` rule is reached before the core\n  one, and an app extension that depends on `@lexical/rich-text` gets to\n  go ahead of both.\n\nConcretely, for an app extension that depends on `RichTextExtension`\n(which in turn depends on `CoreImportExtension`), the compiled list is:\n\n```\n[ …rules passed to buildEditorFromExtensions… ]  ← merged last, highest priority\n[ …the app extension's rules…                 ]\n[ …@lexical/rich-text's rules…                ]\n[ …CoreImportExtension's rules…               ]\n[ DefaultHoistRule                            ]  ← the base config, always last\n```\n\nConfiguration handed straight to `buildEditorFromExtensions` is merged\nafter every extension's, so it outranks all of them. `DefaultHoistRule`\nis `DOMImportExtension`'s own default `config.rules` entry, so\neverything else lands in front of it and it is always the last rule\ntried.\n\n:::caution\n\nThe relative order of two extensions where neither transitively depends\non the other falls out of the topological sort and is not part of the\nAPI. If extension **A** must override a rule from extension **B**, say\nso structurally — make **B** a dependency of **A** — rather than relying\non the order they happen to be listed in.\n\n:::\n\n### `$next()` as a wrapper\n\nThe middleware shape is strictly more powerful than the legacy\nnumeric `priority`. A rule can call `$next()`, inspect the result,\nand transform it — perfect for cross-cutting decorators:\n\n```ts\nconst IdAttributeRule = defineImportRule({\n  name: '@app/id-decorator',\n  match: sel.any().attr('id', /\\S/),\n  $import: (ctx, el, $next) => {\n    const out = $next();\n    if (out.length === 1) {\n      $setState(out[0], idState, el.getAttribute('id')!);\n    }\n    return out;\n  },\n});\n```\n\nPlaced early in your `rules` array, this rule fires for every element\ncarrying an `id` BEFORE the tag-specific rule, calls `$next()` to get\nthe produced node, and tags it with state. No tag-specific machinery\nneeds to know about `id`.\n\n## Selectors\n\nBuild selectors with the combinator API:\n\n```ts\nsel.any()                                       // any HTMLElement\nsel.tag('a')                                    // <a> only (typed as HTMLAnchorElement)\nsel.tag('h1', 'h2', 'h3', 'h4', 'h5', 'h6')     // typed as HTMLHeadingElement\nsel.text()                                      // text nodes\nsel.comment()                                   // comment nodes\nsel.tag('li').classAll('task-list-item')        // <li class=\"task-list-item …\">\nsel.tag('span').classAny('hl', 'mark')          // <span class=\"hl|mark …\">\nsel.tag('a').attr('href', /^https:/)            // <a href> matching a regex\nsel.tag('a').attr('target', true)               // attribute present\nsel.tag('a').attr('href', '/wiki')              // exact value\nsel.tag('span').styleAny('font-size', /^(\\d+)pt/) // inline-style match\n```\n\nA CSS-subset parser is also available for terse selectors:\n\n```ts\nsel.css('p.google-docs-title')      // tag + class\nsel.css('h1, h2, h3, h4, h5, h6')   // tag list\nsel.css('img[src]')                 // attribute presence\nsel.css('a[href=\"/wiki\"]')          // attribute equality\n```\n\n`sel.css` covers the basics (tag, `.class`, `#id`, `[attr]`,\n`[attr=\"val\"]`, comma-separated lists, `*`). For anything outside that\nsubset (regex attribute, inline-style match, capturing), chain\ncombinator methods off the result:\n\n```ts\nsel.css('pre').attr('class', /(?:^|\\s)language-(\\S+)/, {capture: 'lang'})\n```\n\nThe grammar deliberately excludes descendant combinators (`>`, ` `)\nand `:not` — see [Context](#context) below for the idiomatic\nreplacements.\n\n### Captured matches\n\nPass `{capture: 'name'}` to `attr` or `styleAny` and the\n`RegExpMatchArray` for the successful match is surfaced on\n`ctx.captures.name`, fully typed:\n\n```ts\nconst CodeBlockRule = defineImportRule({\n  name: '@app/pre-code',\n  match: sel\n    .tag('pre')\n    .attr('class', /(?:^|\\s)language-(\\S+)/, {capture: 'lang'}),\n  $import: (ctx, el) => {\n    // ctx.captures.lang: RegExpMatchArray\n    const language = ctx.captures.lang[1];\n    const node = $createCodeNode(language);\n    node.splice(0, 0, ctx.$importChildren(el));\n    return [node];\n  },\n});\n```\n\nThe selector's element type and captures map both flow into `$import`'s\nsignature, so you never need to cast `el` or re-run the regex inside\nthe rule body.\n\n:::tip\n\nIf your selector becomes a runtime function (a `(node) => boolean`\nguard), the rule lands in the wildcard bucket and is consulted for\nevery element — expensive. Push as much as possible into the selector\nitself; do the remaining refinement inside the rule body, calling\n`$next()` to defer when the body decides not to handle the element.\n\n```ts\ndefineImportRule({\n  match: sel.tag('img'),  // tag bucket — cheap\n  $import: (ctx, el, $next) => {\n    if (!el.src || el.src.startsWith('data:')) {\n      return $next();      // body refinement\n    }\n    return [$createImageNode({src: el.src, alt: el.alt})];\n  },\n});\n```\n\n:::\n\n## Schemas\n\nA rule's `$import` body uses `ctx.$importChildren(parent, {schema})`\nto recursively import children. The schema enforces what node types\nare allowed in this position and how to package non-conforming runs:\n\n```ts\ninterface ChildSchema {\n  $accepts(child: LexicalNode, parent: LexicalNode | null): boolean;\n  $packageRun?(rejected: LexicalNode[], ...): LexicalNode[];\n  onReject?: 'hoist' | 'drop';\n  $finalize?(children: LexicalNode[], ...): LexicalNode[];\n}\n```\n\nThe `$`-prefixed members are invoked inside the import walk (which\nalready runs inside `editor.update`), so they can call `$is*`\npredicates and node methods on the `LexicalNode` arguments directly.\n\nBuilt-ins, all from `@lexical/html`:\n\n| Schema | Accepts | Rejected runs |\n| --- | --- | --- |\n| `BlockSchema` | Block ElementNodes and block DecoratorNodes | Wrapped in a fresh `ParagraphNode` (preserves `text-align`) |\n| `RootSchema` | Same as `BlockSchema` — aliased for clarity at the entry point | Same |\n| `InlineSchema` | Inline ElementNodes, TextNodes, inline DecoratorNodes | Dropped |\n| `NestedBlockSchema` | Block nodes | Inline runs pass through unchanged (no extra paragraph wrapping) |\n\nPer-package schemas:\n\n| Schema | Accepts | Rejected runs |\n| --- | --- | --- |\n| `ListSchema` (`@lexical/list`) | `ListItemNode`, nested `ListNode` | Wrapped in a synthetic `ListItemNode` (no intermediate paragraph) |\n| `TableSchema` (`@lexical/table`) | `TableRowNode` | If all cells, wrapped in a synthetic `TableRowNode`; else dropped |\n| `TableRowSchema` (`@lexical/table`) | `TableCellNode` | Dropped |\n\nA rule typically passes a schema when it knows its children's\nallowed type — the legacy `wrapContinuousInlines` and\n`ArtificialNode__DO_NOT_USE` cases are now `BlockSchema` and\n`NestedBlockSchema` respectively:\n\n```ts\nconst HeadingRule = defineImportRule({\n  match: sel.tag('h1', 'h2', 'h3', 'h4', 'h5', 'h6'),\n  $import: (ctx, el) => {\n    const node = $createHeadingNode(el.nodeName.toLowerCase() as HeadingTagType);\n    node.splice(0, 0, ctx.$importChildren(el, {schema: InlineSchema}));\n    return [node];\n  },\n});\n```\n\nCustom schemas implement `ChildSchema` directly. `$accepts`,\n`$packageRun`, and `$finalize` are invoked inside the editor context\nof the surrounding walk, so they receive `LexicalNode` arguments and\ncan call `$is*` predicates / node methods without setting up their\nown `editor.read`.\n\n### When `$packageRun` runs and `onReject` doesn't\n\nThe schema walks the assembled child array once, partitioning into\naccepted children (passed through individually) and maximal runs of\nnon-accepted children. For each rejected run, `$packageRun` runs\nfirst; if it's missing or returns `[]`, `onReject` decides:\n\n- `'drop'` (default) silently discards the run.\n- `'hoist'` emits the run unchanged at the same position. This\n  surfaces the mixed content to the calling rule's parent — only\n  useful when the rule's caller is prepared to receive mixed\n  children. It does NOT lift the run *out* of the calling rule's\n  parent.\n\n`$packageRun` is the right hook when the rejected content has a\nsensible re-shape that still belongs *inside* the rule's parent —\ne.g. `BlockSchema` wraps a run of inline siblings in a\n`ParagraphNode`, `TableSchema` wraps cells in a synthesized row.\n\n### Lifting blocks out of an inline parent\n\nSchemas cannot change the parent the rule already created. The\ncanonical example is an inline element with block children:\n\n```html\n<a href=\"link\"><h1>some text</h1><div>more text</div></a>\n```\n\nThe desired output is to lift each block out of the link and\nredistribute the link onto the block's inline contents:\n\n```\nHeadingNode\n└─ LinkNode(\"link\")\n   └─ TextNode(\"some text\")\nParagraphNode\n└─ LinkNode(\"link\")\n   └─ TextNode(\"more text\")\n```\n\n`InlineSchema` doesn't help here: with `onReject: 'drop'` the\nheadings are lost, with `'hoist'` they emerge unwrapped, and there's\nno `$packageRun` shape that produces \"a list of blocks each holding\nthe original inline wrapper\" because the schema sees only the\nchildren, not the parent.\n\nThis is a rule-level concern, but `@lexical/html` ships the\n`$distributeInlineWrapper(children, $makeWrapper)` helper so rules\nthat produce an inline parent (e.g. `LinkNode`, `MarkNode`) can\nopt in with one line:\n\n```ts\nimport {\n  $distributeInlineWrapper,\n  defineImportRule,\n  sel,\n} from '@lexical/html';\nimport {$createLinkNode} from '@lexical/link';\n\nconst AnchorRule = defineImportRule({\n  match: sel.tag('a'),\n  $import: (ctx, el) => {\n    const href = el.getAttribute('href') || '';\n    return $distributeInlineWrapper(ctx.$importChildren(el), () =>\n      $createLinkNode(href),\n    );\n  },\n});\n```\n\nThe helper walks the children: each block is descended into and its\ninline content is wrapped with a fresh instance from `$makeWrapper`;\neach contiguous inline run at the top level becomes its own wrapped\nnode. The enclosing `BlockSchema` then packages the inline-wrapped\nruns into paragraphs as usual.\n\n## Context\n\n`ImportContext` is the per-rule environment exposed as `ctx` to\n`$import`. It provides:\n\n- `ctx.captures` — typed regex captures from this rule's selector.\n- `ctx.get(cfg)` — read an `ImportStateConfig` value.\n- `ctx.$importChildren(parent, opts)` — recurse into children.\n- `ctx.$importOne(node, opts)` — recurse into one node.\n- `ctx.session` — per-import mutable store (see [Sessions](#sessions)).\n\n### State configs and `ctx.get`\n\n`createImportState` mints a typed key, similar to `createState` for\nnode state but scoped to the import pipeline:\n\n```ts\nimport {createImportState} from '@lexical/html';\n\nconst ImportMode = createImportState<'paste' | 'deserialize'>(\n  'app/importMode',\n  () => 'paste',\n);\n```\n\nRules read it with `ctx.get(ImportMode)`. The value is supplied\neither as a `contextDefaults` entry on the extension config (sticks\nfor every import call) or as a per-call `context` override on\n`$generateNodesFromDOM`:\n\n```ts\nimport {contextValue} from '@lexical/html';\n\ndep.output.$generateNodesFromDOM(dom, {\n  context: [contextValue(ImportMode, 'deserialize')],\n});\n```\n\nA nested `$importChildren` can also branch context for its descendants\nvia `opts.context`:\n\n```ts\nimport {contextValue, ImportTextFormat} from '@lexical/html';\nimport {IS_BOLD} from 'lexical';\n\nctx.$importChildren(el, {\n  context: [contextValue(ImportTextFormat, ctx.get(ImportTextFormat) | IS_BOLD)],\n});\n```\n\nBranched values are restored on the way back out — siblings outside\nthe branched subtree see the unchanged inherited value.\n\n### Built-in states\n\n`@lexical/html` ships several states out of the box:\n\n- **`ImportSource`** (`'paste' | 'unknown'`) — identifies how this\n  import started. Defaults to `'unknown'`; the clipboard handler sets\n  `'paste'`. Rules can branch on it to adapt behavior (e.g. preserve\n  more whitespace on `'paste'`). Apps that need a richer vocabulary\n  (e.g. `'drop'`, `'deserialize'`) can define their own\n  `ImportStateConfig` and pass it through the same `context` slot.\n- **`ImportSourceDataTransfer`** (`DataTransfer | null`) — the original\n  `DataTransfer` the paste/drop came from, surfaced so rules can peek\n  at companion MIME types or attached files. `null` outside clipboard\n  imports. See [below](#importsourcedatatransfer).\n- **`ImportTextFormat`** (`number`, a `TextFormatType` bitmask) —\n  used by the inline-format rules (`<b>`, `<strong>`, `<em>`, …) to\n  propagate format bits to descendant TextNodes. Replaces the legacy\n  `forChild` chain that the inline tags previously used. Build your\n  own format propagation by branching this state.\n- **`ImportTextStyle`** (`Readonly<Record<string, string>>`) — parsed\n  CSS-property record (e.g. `{color: 'red', 'font-family': 'serif'}`)\n  inherited by descendant TextNodes and materialized onto their\n  `style` string. Used by the core `#text` rule to pick up\n  inheritable cell/span styles like `color`. Properties already\n  covered by `ImportTextFormat` (`font-weight`, `font-style`,\n  `text-decoration`, `vertical-align`) are filtered out so they\n  remain owned by the format-bit path.\n- **`ImportOverlays`** — session slot (`readonly CompiledOverlayRules[]`)\n  holding overlays installed during the preprocess phase. The walker\n  primes its overlay stack from this list before starting, so a\n  preprocess can scope an overlay to the whole document based on a\n  signal it detected (e.g. a Word `Generator` meta tag). See\n  [Preprocessors](#preprocessors).\n- **`ImportWhitespaceConfig`** — controls text-node whitespace\n  handling (which DOM elements preserve whitespace, which count as\n  inline siblings). See below.\n\n### `ImportWhitespaceConfig`\n\nThe text-node whitespace handler walks DOM siblings to decide\nwhether trailing/leading spaces should be preserved (the neighbor is\ninline text) or collapsed (the neighbor is a block boundary). Two\npredicates govern this:\n\n```ts\ninterface WhitespaceImportConfig {\n  preservesWhitespace: (node: Node) => boolean;\n  isInline: (node: Node) => boolean;\n}\n```\n\nThe defaults match the legacy behavior: an element preserves\nwhitespace if its `nodeName` is `'PRE'` or its inline `style.whiteSpace`\nstarts with `'pre'`; an element is inline if its inline `style.display`\nstarts with `'inline'` or its tag name is in the standard inline-tag\nset used by lexical core's `isInlineDomNode` / `isBlockDomNode`\nhelpers (`a`, `abbr`, `acronym`, `b`, `cite`, `code`, `del`, `em`,\n`i`, `ins`, `kbd`, `label`, `mark`, `output`, `q`, `ruby`, `s`,\n`samp`, `span`, `strong`, `sub`, `sup`, `time`, `u`, `tt`, `var`).\nThe canonical list lives in `packages/lexical/src/LexicalUtils.ts` —\nsee the module-private `INLINE_TAG_RE` / `BLOCK_TAG_RE` constants if you\nwant to inspect the defaults.\n\nTo recognize custom tags (e.g. a custom `<tooltip>` that should be\ntreated as inline so the spaces around it survive), override the\npredicate:\n\n```ts\nimport {\n  contextValue,\n  defaultIsInline,\n  defaultPreservesWhitespace,\n  ImportWhitespaceConfig,\n} from '@lexical/html';\nimport {isHTMLElement} from 'lexical';\n\nconfigExtension(DOMImportExtension, {\n  contextDefaults: [\n    contextValue(ImportWhitespaceConfig, {\n      isInline: (node) =>\n        defaultIsInline(node) ||\n        (isHTMLElement(node) && node.nodeName === 'TOOLTIP'),\n      preservesWhitespace: defaultPreservesWhitespace,\n    }),\n  ],\n})\n```\n\nThis is the declarative replacement for setting `display: inline` on\nthe element from inside an extended TextNode importer.\n\n## Sessions\n\n`ImportSession` is the **root-layer context record** of the walk —\na mutable, document-order-shared store on `ctx`. Use it to make\ninformation from a node visited earlier in the document available\nto nodes visited later — e.g. a `<style>` / `<meta>` tag at the\ntop of the document influencing how later elements are interpreted.\n\nThe session and the scoped context share the same\n`ImportStateConfig` keys you create with `createImportState`:\n\n- `ctx.get(cfg)` reads through the current branch's prototype chain\n  (a `$importChildren({context})` branch layered above the session\n  record, falling through to the session record itself, falling\n  through to the editor's `contextDefaults`, then to the config's\n  default).\n- `ctx.session.get(cfg)` reads the same chain starting at the\n  session record (skips any active branch overlay).\n- `ctx.session.set(cfg, v)` / `update` mutate the session record's\n  own properties. Every unshadowed `ctx.get(cfg)` later in the walk\n  (including in not-yet-entered branches) picks up the new value.\n- A branch that writes `cfg` shadows the session value for the\n  duration of the branch only; on return, scoped reads see the\n  session value again.\n\n```ts\nimport {createImportState} from '@lexical/html';\n\nconst Stylesheets = createImportState<string[]>(\n  'app/stylesheets',\n  () => [],\n);\n\nconst CollectStyleSheetsRule = defineImportRule({\n  match: sel.tag('style'),\n  $import: (ctx, el) => {\n    ctx.session.update(Stylesheets, (prev) => [\n      ...prev,\n      el.textContent ?? '',\n    ]);\n    return [];\n  },\n});\n\nconst ConsumesStyleSheetsRule = defineImportRule({\n  match: sel.tag('article'),\n  $import: (ctx, el) => {\n    const sheets = ctx.session.get(Stylesheets);\n    // ... use the collected stylesheet text however the app needs\n  },\n});\n```\n\nA fresh session record is created for every `$generateNodesFromDOM`\ncall. It chains to the ambient import context when the call runs\nnested inside another import operation — a rule re-entering the walk\nfor sub-content, or raw HTML inside a `@lexical/mdast` Markdown import\n— so states layered by the outer operation stay readable; the\noutermost call chains to the editor's `contextDefaults`. Per-call\n`options.context` pairs are seeded into it before any preprocessors\nrun, and preprocess-time `ctx.session.set` writes mutate the same\nrecord (never the parent, so session writes don't leak outward).\n\n## Preprocessors\n\nBefore walking begins, `DOMImportExtension` runs a stack of\npreprocessors against the input DOM. They're middleware-shaped:\n\n```ts\ntype DOMPreprocessFn = (\n  dom: Document | ParentNode,\n  ctx: DOMPreprocessContext,\n  $next: () => void,\n) => void;\n```\n\nEach step can:\n\n- Mutate the DOM in place (e.g. inline stylesheet rules onto\n  matching elements, strip unsafe nodes, normalize attributes,\n  resolve relative URLs).\n- Write to `ctx.session` — the session IS the root layer of the\n  walk's context, so writes are visible to both `ctx.session.get`\n  and to unshadowed `ctx.get(cfg)` reads in any branch.\n- Call `$next()` to defer to the next-lower preprocessor (top of stack\n  runs first); skip the call to short-circuit.\n\nThe default config registers\n[`$inlineStylesFromStyleSheets`](#inlinestylesfromstylesheets) — the\nsame Excel-flavored CSS preprocess the legacy\n`$generateNodesFromDOM` uses. Apps append more preprocessors via\n`DOMImportConfig.preprocess` or per-call via\n`GenerateNodesFromDOMOptions.preprocess`.\n\n```ts\nconst $stripScripts: DOMPreprocessFn = (dom, _ctx, $next) => {\n  const root = 'body' in dom ? dom.body : (dom as ParentNode);\n  for (const el of Array.from(root.querySelectorAll('script'))) {\n    el.remove();\n  }\n  $next();\n};\n\nconfigExtension(DOMImportExtension, {\n  preprocess: [$stripScripts],\n})\n```\n\n### Preprocess order\n\n`preprocess` composes in the opposite array direction from\n[`rules`](#dispatch-order) but produces the same precedence. `mergeConfig`\n**appends** each contribution to the accumulated stack, and the runner\nstarts at the **last** entry and works backwards. So:\n\n- Within one contribution, the **last** entry runs first and wraps the\n  ones written before it.\n- Because configs are merged in dependency order, an extension's\n  preprocessors run before — and can wrap, via `$next()` — those of its\n  dependencies. `$inlineStylesFromStyleSheets`, sitting in the extension's\n  own base config, runs last of all, and only if every preprocessor above\n  it calls `$next()`.\n- Per-call preprocessors from `GenerateNodesFromDOMOptions.preprocess` are\n  appended on top of the configured stack, so they run **before** the\n  configured ones.\n\nSkipping `$next()` is therefore a real decision: it drops every\nlower-priority preprocessor, including the built-in stylesheet inlining.\n\n### Reading meta tags into context\n\nA common pattern: a preprocess step inspects a `<meta>` tag (or any\nDOM marker), then layers a typed context value the rest of the walk\ncan branch on.\n\n```ts\nimport {createImportState} from '@lexical/html';\n\nconst DocumentLanguage = createImportState<string>(\n  'app/documentLanguage',\n  () => 'unknown',\n);\n\nconst $readMetaLang: DOMPreprocessFn = (dom, ctx, $next) => {\n  const root = 'body' in dom ? dom.body : (dom as ParentNode);\n  const meta = root.querySelector('meta[name=\"content-language\"]');\n  if (meta && meta.getAttribute('content')) {\n    ctx.session.set(DocumentLanguage, meta.getAttribute('content')!);\n  }\n  $next();\n};\n```\n\n### `$inlineStylesFromStyleSheets`\n\nThe default preprocessor resolves CSS rules from `<style>` tags onto\nmatching elements as inline styles, so the rest of the pipeline can\nread those styles via plain `el.style.foo`. Apps like Excel paste\nHTML in this shape; the preprocess collapses the indirection so rule\nselectors and bodies don't need to chase stylesheets themselves.\nRe-exported as a `DOMPreprocessFn` for apps that want to compose\ntheir own preprocess pipeline:\n\n```ts\nimport {$inlineStylesFromStyleSheets} from '@lexical/html';\n\nconfigExtension(DOMImportExtension, {\n  preprocess: [\n    $appPreprocess,\n    // The stack runs from the END of the array, so listing the inliner\n    // last makes it run FIRST — useful when `$appPreprocess` expects the\n    // styles to already be inlined. (The copy in the extension's base\n    // config runs at the very bottom of the stack, after everything\n    // else, so re-listing it here is how you pull it forward.)\n    $inlineStylesFromStyleSheets,\n  ],\n});\n```\n\nSee [Preprocess order](#preprocess-order) for the full composition rules.\n\n## `$importChildren` rules overlay\n\n`ctx.$importChildren(parent, {rules: defineOverlayRules([...])})`\ninstalls an **overlay** dispatcher active only for the duration of\nthat children traversal (and nested `$importChildren` calls that\ndon't push their own overlay). Overlay rules are checked BEFORE the\nmain dispatcher; calling `$next()` from an overlay rule falls through\nto the next overlay-or-main rule.\n\nThis lets you scope cost-bearing rules to the subtrees where they\napply, rather than paying the predicate cost on every paste. The\nGitHub raw-file-view code-table rule in `@lexical/code-core` uses\nthe overlay to unwrap `<tr>` / `<td>` only while processing a code\ntable:\n\n```ts\n// Compile once at module scope. `defineOverlayRules` builds the same\n// tag-bucketed dispatch table the main config uses, so each\n// $importChildren call reuses it instead of recompiling.\nconst GitHubCodeTableOverlayRules = defineOverlayRules([\n  defineImportRule({\n    match: sel.tag('tr', 'td'),\n    $import: (ctx, el) => ctx.$importChildren(el),\n    name: '@lexical/code/github-code-table/unwrap',\n  }),\n]);\n\nconst GitHubCodeTableRule = defineImportRule({\n  match: sel.tag('table').classAll('js-file-line-container'),\n  $import: (ctx, el) => {\n    const node = $createCodeNode();\n    node.splice(\n      0,\n      0,\n      ctx.$importChildren(el, {rules: GitHubCodeTableOverlayRules}),\n    );\n    return [node];\n  },\n});\n```\n\nOutside the code table, the `tr` / `td` rule from `@lexical/table` is\nthe only one consulted; the overlay isn't installed unless we enter\na code-table subtree.\n\n`ImportChildrenOpts.rules` only accepts a `CompiledOverlayRules`\nproduced by `defineOverlayRules` — a raw rule array won't typecheck.\nThis is by design: a one-off overlay built inline on every walk would\nrecompile the dispatcher each time, defeating the point of the overlay\nbeing a fast-path.\n\n### Composing overlays\n\n`defineOverlayRules` accepts both raw `DOMImportRule`s and other\n`CompiledOverlayRules` in the same list (the union is\n`DOMImportRuleEntry`). Existing overlays are inlined at their position\nin priority order — earlier entries dispatch first — so any number of\noverlays compose with a single call, recompiled once.\n\n```ts\nconst allOverlays = defineOverlayRules([\n  GitHubCodeTableOverlayRules,\n  ExcelTableOverlayRules,\n  // ...plus inline rules if you want them at this priority position:\n  defineImportRule({match: sel.tag('foo'), $import: …}),\n]);\n\nctx.$importChildren(el, {rules: allOverlays});\n```\n\nThe same `DOMImportRuleEntry` union is accepted by\n`DOMImportConfig.rules` (the extension-level rule list), so a library\nthat ships a `CompiledOverlayRules` can be dropped straight into\n`configExtension(DOMImportExtension, {rules: […]})` without unpacking.\n\nThis is the definition-time complement to runtime composition (an\noverlay rule's `$import` calling\n`ctx.$importChildren(el, {rules: another})` to push a nested overlay\non top of the current stack): compose when you want a fixed merged\noverlay across an entire subtree, nest when the inner overlay should\nonly apply for a deeper region.\n\n### Walk-wide overlays installed by a preprocessor\n\nA preprocessor can install an overlay for the entire walk by writing to\nthe built-in `ImportOverlays` session slot. The runtime reads it once\nbefore walking and seeds the overlay stack from it, so rules don't have\nto bracket each subtree with `$importChildren({rules: …})`.\n\nThis is how you handle paste-source-conditional rule sets: detect the\nsource in preprocess, install only the relevant overlay, and pay\nnothing for other sources.\n\n```ts\nimport {\n  defineOverlayRules,\n  type DOMPreprocessFn,\n  ImportOverlays,\n} from '@lexical/html';\n\nconst WordPasteOverlay = defineOverlayRules([\n  WordOPRule,\n  WordListParagraphRule,\n  // …\n]);\n\nconst $installWordOverlay: DOMPreprocessFn = (dom, ctx, $next) => {\n  const meta = dom.querySelector('meta[name=\"Generator\"]');\n  if (meta && /Microsoft Word/i.test(meta.getAttribute('content') || '')) {\n    ctx.session.update(ImportOverlays, (prev) => [...prev, WordPasteOverlay]);\n  }\n  $next();\n};\n```\n\nThat is the pattern, not something you have to write for Word lists\nspecifically: `@lexical/list` ships this exact preprocess as\n`WordListImportExtension`. It is opt-in — `ListExtension` does not depend\non it, so an editor that never pastes from Word does not bundle it:\n\n```ts\nimport {WordListImportExtension} from '@lexical/list';\nimport {defineExtension} from 'lexical';\n\ndefineExtension({\n  dependencies: [WordListImportExtension],\n  name: 'my-editor',\n});\n```\n\nSee `packages/lexical-list/src/__tests__/unit/ListImportExtension.test.ts`\n(\"MS Word paste — preprocess-installed overlay\") for a worked unit\ntest, and [`dev-examples/dom-import`](https://github.com/facebook/lexical/tree/main/dev-examples/dom-import)\nfor a running editor that exercises the same overlay end-to-end (a\nreduced rich-text editor with lists, tables, code blocks, markdown\nshortcuts, and an \"Import HTML\" dialog so you can paste raw HTML\nwithout needing a real clipboard `text/html` slot). The dev-example\nalso ships a VS Code → Safari paste fixture and the matching\n`@lexical/code-core` preprocess that turns the flat sibling run of\nmonospace+pre divs into a single `CodeNode`.\n\n## ClipboardImportExtension\n\nThe clipboard's paste handler (`$insertDataTransferForRichText`)\ndelegates to `ClipboardImportExtension`. Its output owns the full\npaste-side iteration: it walks a per-MIME-type priority list, reads\nthe corresponding `DataTransfer` slot, and invokes that MIME type's\nhandler stack.\n\n`ClipboardImportConfig` mirrors `GetClipboardDataExtension`:\n\n```ts\nexport interface ClipboardImportConfig {\n  $importMimeType: ImportMimeTypeConfig;\n  priority: ImportMimeTypePriority;\n}\n```\n\n- `$importMimeType` — per-MIME-type stack of middleware functions\n  `(data, selection, $next, dataTransfer) => boolean`. Top of stack runs\n  first; `$next()` defers to the next-lower; return `true` to claim the\n  data. The `dataTransfer` is the original `DataTransfer` the paste/drop\n  came from, so a handler can peek at companion MIME types or attached\n  files (see [`ImportSourceDataTransfer`](#importsourcedatatransfer) for\n  how to surface it to rules).\n- `priority` — a per-MIME-type weight map (`Record<string, number>`,\n  lower runs first). Composable: each extension contributes weights\n  for its own MIME types without coordinating with others.\n\n`$next(data?, selection?)` can replace the data, selection, or both for the\nremaining handlers in the same MIME-type stack. Omitted arguments retain\nthe values received by the current handler, so `$next()`\nis equivalent to `$next(data, selection)`. For example, `$next(cleanedData)`\nchanges only the data, and `$next(data, otherSelection)` changes only\nthe selection. An empty string is forwarded as-is; it does not automatically\nfall back to another MIME type. These replacements do not modify the original\n`DataTransfer` or the arguments used when trying another MIME type.\n\n```ts\nconfigExtension(ClipboardImportExtension, {\n  $importMimeType: {\n    'application/vnd.myapp+json': [\n      (data, selection) => {\n        const nodes = parseMyAppFormat(data);\n        $insertGeneratedNodes($getEditor(), nodes, selection);\n        return true;\n      },\n    ],\n  },\n  // Slot between 'application/x-lexical-editor' (0) and 'text/html' (10).\n  priority: {'application/vnd.myapp+json': 5},\n})\n```\n\n### Routing pastes through `DOMImportExtension`\n\nThe easy on-switch is `ClipboardDOMImportExtension` from\n`@lexical/clipboard` — add it to your dependencies and `text/html`\npastes and drops route through the new pipeline (forwarding\n`ImportSource='paste'` and `ImportSourceDataTransfer` automatically).\nThe node extensions already registered their rules, so nothing else\nneeds to be configured:\n\n```ts\nimport {defineExtension} from 'lexical';\nimport {ClipboardDOMImportExtension} from '@lexical/clipboard';\nimport {RichTextExtension} from '@lexical/rich-text';\n\ndefineExtension({\n  name: 'app',\n  dependencies: [\n    RichTextExtension,\n    // …other node extensions (list, link, table, code, …)…\n    ClipboardDOMImportExtension,\n  ],\n});\n```\n\nApps that don't configure `ClipboardImportExtension` keep the legacy\nbehavior — `$insertDataTransferForRichText` falls back to the same\ndefaults the legacy code path uses, including the legacy\n`$generateNodesFromDOM` for HTML.\n\n### Transforming pasted HTML\n\n:::tip\n\nUse HTML transforms when integrating libraries that accept and return HTML\nstrings. For DOM-based normalization, prefer a [DOM import preprocessor](#preprocessors).\nIt works directly on the parsed DOM, avoiding an extra serialization and parse.\nSee [`WordListImportExtension`](#walk-wide-overlays-installed-by-a-preprocessor)\nand [`$inlineStylesFromStyleSheets`](#inlinestylesfromstylesheets) for examples.\n\n:::\n\nUse extension dependencies to enforce handler ordering. A transform's\nconfiguration is applied after its dependencies, so its handler runs first.\n\n**To use `ClipboardDOMImportExtension`**, declare it as a dependency of your\ntransform extension:\n\n```ts\nimport {\n  ClipboardDOMImportExtension,\n  ClipboardImportExtension,\n} from '@lexical/clipboard';\nimport {configExtension, defineExtension} from 'lexical';\nimport {normalizePastedHTML} from './normalizePastedHTML';\n\nconst TransformPastedHTMLExtension = defineExtension({\n  name: 'app/TransformPastedHTML',\n  dependencies: [\n    ClipboardDOMImportExtension,\n    configExtension(ClipboardImportExtension, {\n      $importMimeType: {\n        'text/html': [\n          (html, _selection, $next) => $next(normalizePastedHTML(html)),\n        ],\n      },\n    }),\n  ],\n});\n```\n\nAdd `TransformPastedHTMLExtension` to your root dependencies. This includes\n`ClipboardDOMImportExtension` and guarantees that the transform runs before\nits HTML importer, passing the transformed HTML to it through `$next`.\n\n**To support either importer**, declare `ClipboardDOMImportExtension` as an\noptional peer dependency:\n\n```ts\nimport {\n  type ClipboardDOMImportExtension,\n  ClipboardImportExtension,\n} from '@lexical/clipboard';\nimport {configExtension, declarePeerDependency, defineExtension} from 'lexical';\nimport {normalizePastedHTML} from './normalizePastedHTML';\n\nconst TransformPastedHTMLForEitherImporterExtension = defineExtension({\n  name: 'app/TransformPastedHTMLForEitherImporter',\n  dependencies: [\n    configExtension(ClipboardImportExtension, {\n      $importMimeType: {\n        'text/html': [\n          (html, _selection, $next) => $next(normalizePastedHTML(html)),\n        ],\n      },\n    }),\n  ],\n  peerDependencies: [\n    declarePeerDependency<typeof ClipboardDOMImportExtension>(\n      '@lexical/clipboard/DOMImport',\n    ),\n  ],\n});\n```\n\nAdd `TransformPastedHTMLForEitherImporterExtension` to your root dependencies.\nWhen the editor includes `ClipboardDOMImportExtension`, the peer dependency\nmakes this extension's clipboard configuration apply after the DOM importer's\nconfiguration, so the transform runs first. Otherwise, the transform runs\nbefore the legacy default HTML importer.\n\n### `ImportSourceDataTransfer`\n\nA builtin `ImportStateConfig<DataTransfer | null>` slot for surfacing\nthe original paste/drop `DataTransfer` to import rules and\npreprocessors. `ClipboardDOMImportExtension` forwards it via\n`context`; rules can then read it during the walk:\n\n```ts\nimport {ImportSourceDataTransfer} from '@lexical/html';\n\nconst ExcelAwareTableRule = defineImportRule({\n  match: sel.tag('table'),\n  $import: (ctx, el, $next) => {\n    const transfer = ctx.get(ImportSourceDataTransfer);\n    // Excel ships an HTML alternative alongside a real RTF payload; if\n    // we see both, we may want to defer to the RTF handler instead of\n    // parsing Excel's HTML quirks.\n    if (transfer && transfer.types.includes('application/rtf')) {\n      return $next();\n    }\n    // …handle the table…\n  },\n});\n```\n\nDefaults to `null` outside of clipboard imports (e.g. headless\n`$generateNodesFromDOM` calls). Use sparingly — the safer pattern is\nto decide *which* MIME-type payload to walk in the clipboard handler\nstack and hand a finalized DOM to the rules; only fall back to peeking\nat `ImportSourceDataTransfer` when the source-detection signal\ngenuinely lives in a companion slot.\n\n## Migrating from `importDOM`\n\nThe legacy `static importDOM(): DOMConversionMap` declaration on each\nnode class still works; the new pipeline is opt-in. When you're\nready to move a custom node, the translation is mechanical:\n\n| Legacy concept | New equivalent |\n| --- | --- |\n| `static importDOM(): DOMConversionMap` returning `{tag: () => ({conversion, priority})}` | One or more `defineImportRule({match, $import})` entries |\n| Numeric `priority` (0–4) | Position in the compiled `rules` list (earlier entry runs first; an extension's rules come ahead of its dependencies') plus `$next()` for deferring — see [Dispatch order](#dispatch-order) |\n| `forChild(node, parent)` | `ctx.$importChildren(el, {context: [...], $onChild})` |\n| `after(children)` | `ctx.$importChildren(el, {$after})` |\n| `wrapContinuousInlines` (block ancestor case) | `ctx.$importChildren(el, {schema: BlockSchema})` |\n| `ArtificialNode__DO_NOT_USE` (nested block) | `ctx.$importChildren(el, {schema: NestedBlockSchema})` |\n| Cross-tag setup via shared state | `createImportState` + `ctx.get` (scoped) or `ctx.session.get/set` (flat) |\n| Mutating the DOM before walking | `DOMPreprocessFn` in `DOMImportConfig.preprocess` |\n| `html: {import: ...}` field on a node-providing extension | `configExtension(DOMImportExtension, {rules: […]})` |\n\nA migrated rule typically replaces a forChild chain with explicit\ncontext propagation (clearer + survives across rule boundaries), and\nan `after` callback with either `$after` (when the post-processing\nruns on this rule's children) or a schema's `$packageRun` / `$finalize`\n(when the same logic belongs to anything appending to this kind of\nparent).\n\n### Importing children is explicit\n\nThe most consequential difference: the legacy `DOMConversion` pipeline\nwalked an element's children for you (via `wrapContinuousInlines` /\nthe post-conversion runner). In `DOMImportExtension`, a rule that\nwants its element's children must call `ctx.$importChildren(el)`\nitself.\n\nThis is deliberate. A rule decides:\n\n- Whether to recurse at all (skip children to drop them, or look at\n  `el` only)\n- Which schema the children must satisfy\n  (`schema: BlockSchema | InlineSchema | NestedBlockSchema | …`)\n- Whether to install a subtree-scoped overlay\n  (`rules: defineOverlayRules([...])`)\n- Where the produced children attach — usually\n  `parent.splice(0, 0, ctx.$importChildren(el))`, but a rule that\n  wants flat output can just return the array.\n\n### Concrete example: blockquote\n\nA typical legacy `importDOM` for a quote-like node:\n\n```ts\n// Legacy\nclass QuoteNode extends ElementNode {\n  static importDOM(): DOMConversionMap | null {\n    return {\n      blockquote: () => ({\n        conversion: (_el) => ({node: $createQuoteNode()}),\n        priority: 0,\n      }),\n    };\n  }\n}\n```\n\nThe migrated rule:\n\n```ts\nimport {\n  BlockSchema,\n  defineImportRule,\n  InlineSchema,\n  sel,\n} from '@lexical/html';\nimport {$createQuoteNode} from '@lexical/rich-text';\n\nconst QuoteRule = defineImportRule({\n  $import: (ctx, el) => {\n    const node = $createQuoteNode();\n    // The recursion is explicit. QuoteNode contains inline children\n    // only, so we constrain the schema; rejected blocks are dropped\n    // (InlineSchema declares no onReject, and 'drop' is the default).\n    node.splice(0, 0, ctx.$importChildren(el, {schema: InlineSchema}));\n    return [node];\n  },\n  match: sel.tag('blockquote'),\n  name: '@app/quote',\n});\n\nconfigExtension(DOMImportExtension, {rules: [QuoteRule]});\n```\n\nThe legacy pipeline would have inferred the recursion + wrapping\nbehavior from `QuoteNode`'s position in the tree and its acceptance\nrules. In the new pipeline both are stated at the call site: the rule\nasks for inline children, gets them packaged by `InlineSchema`, and\nattaches them via the normal `ElementNode.splice` primitive.\n\n### Concrete example: playground\n\nThe lexical-playground application was migrated in\n[#8590](https://github.com/facebook/lexical/pull/8590), which should\nbe a good example of what it would take to move an entire application\nfrom `importDOM` to `DOMImportExtension` (while adding test coverage).\n\n## Capabilities\n\nCurrent:\n\n- Typed selectors with element-narrowing through to `$import`'s\n  signature; no instanceof or cast boilerplate.\n- Middleware `$next()` chain replaces numeric priority.\n- Per-rule capture map (`ctx.captures`) avoids re-running regex\n  inside the rule body.\n- `ChildSchema` enforces structural invariants (block/inline/list/\n  table/code) without per-importer wrapping logic.\n- Per-call context (`ImportSource`), persistent context state\n  (`ImportTextFormat`, `ImportWhitespaceConfig`, app-defined).\n- Mutable session store for document-order-shared info.\n- DOM preprocess middleware (default: stylesheet inlining).\n- Subtree-scoped `rules` overlay for cost-bearing predicates.\n- Clipboard pipeline owns the whole `DataTransfer` iteration via\n  `ClipboardImportExtension`; per-MIME-type priority weights compose\n  across extensions.\n\nFuture:\n\n- The default pipeline (`$generateNodesFromDOM` and the static\n  `importDOM` methods on lexical-core node classes) still works\n  side-by-side. There is no plan in this iteration to flip the\n  default — both will coexist while ecosystem migrates.\n","isInternal":false,"tokens":11379,"sizeBytes":46892},{"name":"dom-render.md","path":"packages/lexical-website/docs/serialization/dom-render.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-website/docs/serialization/dom-render.md","title":"Serialization Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-website%2Fdocs%2Fserialization%2Fdom-render.md -->","isInternal":false,"tokens":4794,"sizeBytes":20587},{"name":"legacy.md","path":"packages/lexical-website/docs/serialization/legacy.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-website/docs/serialization/legacy.md","title":"Serialization Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-website%2Fdocs%2Fserialization%2Flegacy.md -->","isInternal":false,"tokens":2987,"sizeBytes":13409},{"name":"markdown-mdast.md","path":"packages/lexical-website/docs/serialization/markdown-mdast.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-website/docs/serialization/markdown-mdast.md","title":"Serialization Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-website%2Fdocs%2Fserialization%2Fmarkdown-mdast.md -->","isInternal":false,"tokens":2263,"sizeBytes":9967},{"name":"serialization.md","path":"packages/lexical-website/docs/serialization/serialization.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-website/docs/serialization/serialization.md","title":"Serialization Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-website%2Fdocs%2Fserialization%2Fserialization.md -->","isInternal":false,"tokens":10147,"sizeBytes":43655},{"name":"testing.md","path":"packages/lexical-website/docs/testing.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-website/docs/testing.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-website%2Fdocs%2Ftesting.md -->","frontmatter":{"sidebar_position":6},"isInternal":false,"tokens":2097,"sizeBytes":9068},{"name":"README.md","path":"packages/lexical-a11y/README.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-a11y/README.md","title":"lexical-a11y Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-a11y%2FREADME.md -->","isInternal":false,"tokens":96,"sizeBytes":408},{"name":"README.md","path":"packages/lexical-clipboard/README.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-clipboard/README.md","title":"lexical-clipboard Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-clipboard%2FREADME.md -->","isInternal":false,"tokens":51,"sizeBytes":237},{"name":"README.md","path":"packages/lexical-code-core/README.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-code-core/README.md","title":"lexical-code-core Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-code-core%2FREADME.md -->","isInternal":false,"tokens":412,"sizeBytes":1861},{"name":"README.md","path":"packages/lexical-code-prism/README.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-code-prism/README.md","title":"lexical-code-prism Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-code-prism%2FREADME.md -->","isInternal":false,"tokens":55,"sizeBytes":239},{"name":"README.md","path":"packages/lexical-code-shiki/README.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-code-shiki/README.md","title":"lexical-code-shiki Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-code-shiki%2FREADME.md -->","isInternal":false,"tokens":56,"sizeBytes":238},{"name":"README.md","path":"packages/lexical-code/README.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-code/README.md","title":"lexical-code Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-code%2FREADME.md -->","isInternal":false,"tokens":137,"sizeBytes":588},{"name":"README.md","path":"packages/lexical-compiler/README.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-compiler/README.md","title":"lexical-compiler Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-compiler%2FREADME.md -->","isInternal":false,"tokens":4254,"sizeBytes":18351},{"name":"README.md","path":"packages/lexical-devtools-core/README.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-devtools-core/README.md","title":"lexical-devtools-core Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-devtools-core%2FREADME.md -->","isInternal":false,"tokens":52,"sizeBytes":235},{"name":"README.md","path":"packages/lexical-devtools/README.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-devtools/README.md","title":"lexical-devtools Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-devtools%2FREADME.md -->","isInternal":false,"tokens":1362,"sizeBytes":5678},{"name":"README.md","path":"packages/lexical-dragon/README.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-dragon/README.md","title":"lexical-dragon Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-dragon%2FREADME.md -->","isInternal":false,"tokens":365,"sizeBytes":1727},{"name":"README.md","path":"packages/lexical-eslint-plugin/__tests__/integration/README.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-eslint-plugin/__tests__/integration/README.md","title":"integration Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-eslint-plugin%2F__tests__%2Fintegration%2FREADME.md -->","isInternal":false,"tokens":348,"sizeBytes":1516},{"name":"README.md","path":"packages/lexical-eslint-plugin/README.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-eslint-plugin/README.md","title":"lexical-eslint-plugin Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-eslint-plugin%2FREADME.md -->","isInternal":false,"tokens":1367,"sizeBytes":5923},{"name":"README.md","path":"packages/lexical-extension/README.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-extension/README.md","title":"lexical-extension Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-extension%2FREADME.md -->","isInternal":false,"tokens":47,"sizeBytes":222},{"name":"README.md","path":"packages/lexical-fast-check/README.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-fast-check/README.md","title":"lexical-fast-check Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-fast-check%2FREADME.md -->","isInternal":false,"tokens":1120,"sizeBytes":4870},{"name":"README.md","path":"packages/lexical-file/README.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-file/README.md","title":"lexical-file Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-file%2FREADME.md -->","isInternal":false,"tokens":51,"sizeBytes":236},{"name":"README.md","path":"packages/lexical-hashtag/README.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-hashtag/README.md","title":"lexical-hashtag Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-hashtag%2FREADME.md -->","isInternal":false,"tokens":49,"sizeBytes":217},{"name":"README.md","path":"packages/lexical-headless/README.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-headless/README.md","title":"lexical-headless Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-headless%2FREADME.md -->","isInternal":false,"tokens":624,"sizeBytes":2657},{"name":"README.md","path":"packages/lexical-history/README.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-history/README.md","title":"lexical-history Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-history%2FREADME.md -->","isInternal":false,"tokens":448,"sizeBytes":2010},{"name":"README.md","path":"packages/lexical-html/README.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-html/README.md","title":"lexical-html Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-html%2FREADME.md -->","isInternal":false,"tokens":377,"sizeBytes":1636},{"name":"README.md","path":"packages/lexical-internal/README.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-internal/README.md","title":"lexical-internal Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-internal%2FREADME.md -->","isInternal":false,"tokens":380,"sizeBytes":1779},{"name":"README.md","path":"packages/lexical-link/README.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-link/README.md","title":"lexical-link Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-link%2FREADME.md -->","isInternal":false,"tokens":46,"sizeBytes":208},{"name":"README.md","path":"packages/lexical-list/README.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-list/README.md","title":"lexical-list Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-list%2FREADME.md -->","isInternal":false,"tokens":711,"sizeBytes":3340},{"name":"README.md","path":"packages/lexical-mark/README.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-mark/README.md","title":"lexical-mark Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-mark%2FREADME.md -->","isInternal":false,"tokens":51,"sizeBytes":232},{"name":"README.md","path":"packages/lexical-markdown/README.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-markdown/README.md","title":"lexical-markdown Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-markdown%2FREADME.md -->","isInternal":false,"tokens":637,"sizeBytes":2838},{"name":"README.md","path":"packages/lexical-mdast/README.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-mdast/README.md","title":"lexical-mdast Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-mdast%2FREADME.md -->","isInternal":false,"tokens":3371,"sizeBytes":14423},{"name":"README.md","path":"packages/lexical-offset/README.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-offset/README.md","title":"lexical-offset Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-offset%2FREADME.md -->","isInternal":false,"tokens":47,"sizeBytes":213},{"name":"README.md","path":"packages/lexical-overflow/README.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-overflow/README.md","title":"lexical-overflow Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-overflow%2FREADME.md -->","isInternal":false,"tokens":49,"sizeBytes":229},{"name":"README.md","path":"packages/lexical-plain-text/README.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-plain-text/README.md","title":"lexical-plain-text Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-plain-text%2FREADME.md -->","isInternal":false,"tokens":122,"sizeBytes":615},{"name":"README.md","path":"packages/lexical-react/README.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-react/README.md","title":"lexical-react Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-react%2FREADME.md -->","isInternal":false,"tokens":591,"sizeBytes":2568},{"name":"README.md","path":"packages/lexical-rich-text/README.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-rich-text/README.md","title":"lexical-rich-text Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-rich-text%2FREADME.md -->","isInternal":false,"tokens":135,"sizeBytes":687},{"name":"README.md","path":"packages/lexical-selection/README.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-selection/README.md","title":"lexical-selection Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-selection%2FREADME.md -->","isInternal":false,"tokens":46,"sizeBytes":212},{"name":"README.md","path":"packages/lexical-table/README.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-table/README.md","title":"lexical-table Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-table%2FREADME.md -->","isInternal":false,"tokens":421,"sizeBytes":1968},{"name":"README.md","path":"packages/lexical-tailwind/README.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-tailwind/README.md","title":"lexical-tailwind Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-tailwind%2FREADME.md -->","isInternal":false,"tokens":59,"sizeBytes":247},{"name":"README.md","path":"packages/lexical-text/README.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-text/README.md","title":"lexical-text Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-text%2FREADME.md -->","isInternal":false,"tokens":48,"sizeBytes":220},{"name":"README.md","path":"packages/lexical-utils/README.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-utils/README.md","title":"lexical-utils Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-utils%2FREADME.md -->","isInternal":false,"tokens":45,"sizeBytes":201},{"name":"README.md","path":"packages/lexical-website/plugins/example-source/README.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-website/plugins/example-source/README.md","title":"example-source Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-website%2Fplugins%2Fexample-source%2FREADME.md -->","isInternal":false,"tokens":412,"sizeBytes":1907},{"name":"README.md","path":"packages/lexical-website/README.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-website/README.md","title":"lexical-website Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-website%2FREADME.md -->","isInternal":false,"tokens":176,"sizeBytes":770},{"name":"README.md","path":"packages/lexical-website/src/data/README.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-website/src/data/README.md","title":"data Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-website%2Fsrc%2Fdata%2FREADME.md -->","isInternal":false,"tokens":306,"sizeBytes":1481},{"name":"README.md","path":"packages/lexical-yjs/README.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical-yjs/README.md","title":"lexical-yjs Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical-yjs%2FREADME.md -->","isInternal":false,"tokens":57,"sizeBytes":247},{"name":"README.md","path":"packages/lexical/README.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical/README.md","title":"lexical Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical%2FREADME.md -->","isInternal":false,"tokens":1760,"sizeBytes":8380},{"name":"README.md","path":"packages/lexical/src/__bench__/README.md","rawUrl":"https://raw.githubusercontent.com/facebook/lexical/HEAD/packages/lexical/src/__bench__/README.md","title":"__bench__ Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/facebook/lexical?path=packages%2Flexical%2Fsrc%2F__bench__%2FREADME.md -->","isInternal":false,"tokens":1709,"sizeBytes":7362}],"systemPromptSnippet":"<yakaai_skills repo=\"facebook/lexical\">\n<!-- File: AGENTS.md (Tokens: ~6294 | Category: root-instruction) -->\n# Lexical Agent Guide\n\nThis file provides detailed guidance for AI agents and automated tools working with the Lexical codebase.\n\n## Build, Test, and Development Commands\n\n### Building\n- `pnpm run build` - Build all packages in development mode\n- `pnpm run build-prod` - Clean and build all packages in production mode\n- `pnpm run build-release` - Build production release with error codes\n- `pnpm run build-types` - Build TypeScript type definitions and validate them\n\n### Testing\n- `pnpm run test-unit` - Run all unit tests (Vitest, Node/jsdom)\n- `pnpm run test-unit-watch` - Run unit tests in watch mode\n- `pnpm run test-browser` - Run browser-mode unit tests (Vitest + Playwright, real browser)\n- `pnpm run test-browser-watch` - Run browser-mode tests in watch mode\n- `pnpm run test-e2e-chromium` - Run E2E tests in Chromium (requires dev server running)\n- `pnpm run test-e2e-firefox` - Run E2E tests in Firefox\n- `pnpm run test-e2e-webkit` - Run E2E tests in WebKit\n- `pnpm run debug-test-e2e-chromium` - Run E2E tests in debug mode (headed)\n- `pnpm run debug-test-unit` - Debug unit tests with inspector\n\nFor E2E testing workflow:\n1. Start the dev server: `pnpm run start` (or `pnpm run dev` if you don't need collab)\n2. In another terminal: `pnpm run test-e2e-chromium`\n\n### Development Servers\n- `pnpm run start` - Start playground dev server + collab server (http://localhost:3000)\n- `pnpm run dev` - Start only the playground dev server (no collab)\n- `pnpm run start:website` - Start Docusaurus website (http://localhost:3001)\n- `pnpm run collab` - Start collab server on localhost:1234\n\n### Code Quality\n- `pnpm run lint` - Run ESLint on all files\n- `pnpm run lint:fix` - Auto-fix lint issues\n- `pnpm run prettier` - Check code formatting\n- `pnpm run prettier:fix` - Auto-fix formatting issues\n- `pnpm run flow` - Run a full foreground Flow check (does not reuse a server)\n- `pnpm run tsc` - Run TypeScript compiler\n- `pnpm run ci-check` - Run all checks (TypeScript, Flow, Prettier, ESLint)\n\n**Never commit changes to `scripts/error-codes/codes.json`.**\nThat edit is not yours to make — revert it to the state of\n`main` before staging, and never `git add` the file.\n\n### Searching and refactoring\n\nPrefer **ast-grep** over line-oriented regex (`grep`/`sed`) for anything\nstructural — matching or rewriting imports, call sites, JSX, type\nannotations, etc. Regexes miss multi-line forms (a symbol on its own line\ninside a multi-line `import { ... }` block) and produce false positives\n(`IS_APPLE` matching inside `IS_APPLE_WEBKIT`); ast-grep matches the syntax\ntree, so it does neither.\n\nIt isn't a dependency, so run it via npx (the CLI binary is `ast-grep`, not\nthe shadow-utils `sg` that may be on `PATH`):\n\n```sh\n# Find every import of something from '@lexical/utils' (ts and tsx are\n# separate grammars, so pass -l for each; add --json=compact to post-process)\nnpx --package @ast-grep/cli ast-grep run \\\n  -p \"import { \\$\\$\\$NAMES } from '@lexical/utils'\" -l ts packages\n```\n\nMetavariables (`$NAME`, `$$$LIST`) capture nodes for reporting or rewriting\nwith `--rewrite`. Reach for it whenever a change spans many files and must be\nprecise — e.g. moving symbols that `@lexical/utils` merely re-exports back to\na direct `lexical` import.\n\n### Extension source imports\n\nImport from the `@lexical/extension` barrel across package boundaries. Within\n`packages/lexical-extension`, use relative imports and re-exports. Do not\nmanually rewrite these source imports to `@lexical/extension/*`: ESLint\nenforces the source convention, and `@lexical/compiler`'s `subpathImports`\npass rewrites them in all published package builds, including www. Public\nsibling entries stay external so shared modules such as signals retain one\ninstance. Helpers with only one owning entry live under `src/internal` and\nare bundled into that entry, without exposing additional public subpaths.\nDownstream users may import either the barrel or public subpaths.\n\n### Tree-shaking annotations\n\nModule-scope calls to the side-effect-free factories (`defineExtension`,\n`configExtension`, `safeCast`, `createCommand`, `createState`,\n`defineImportRule`, etc.) need a `/* @__PURE__ */` annotation so bundlers\ncan drop unused definitions from application bundles. **Do not write those\nannotations by hand** — they are injected at build time by\n`@lexical/compiler` (`packages/lexical-compiler`), which\nruns as a Rollup plugin in `scripts/build.mjs` for the published bundles\nand as a Vite plugin in `scripts/vite/lexicalMonorepoPlugin.ts` for\neverything the monorepo builds from source. The published package is also\nwhat consumers add to their own build when they compile Lexical from its\n`source` export condition rather than from `dist`.\n\nA hand-written annotation on one of these calls is a lint error\n(`@lexical/internal/no-pure-annotation`), and the rule is autofixable, so a\nbranch written before the transform existed migrates with\n`pnpm run lint:fix`. Annotations on anything else — a third-party factory, or\na call inside a function body, where the build never injects one — are left\nalone.\n\nWhen adding a new factory of this kind, annotate its definition with\n`@__NO_SIDE_EFFECTS__`. That alone is enough for calls in the same package to\nbe annotated; add its name to `PURE_FACTORY_FUNCTIONS` in\n`packages/lexical-compiler/src/LexicalCompiler.mjs` so that\ncalls in code that imports it by package name are too. An object whose\nmethods build values and touch nothing else (like `@lexical/html`'s `sel`) is\nmarked `@lexical-pure-namespace` instead, so that `sel.tag('p')` is annotated\nthe way a factory call is.\n\nThe build runs with `strict`, which fails on a call inside one of these\ndefinitions that nothing has established is side-effect free — such a call\npins the definition into every bundle that imports the module, however well\nannotated the definition itself is. If it stops you, either make the call\nlazy (a `nodes: () => [...]` callback is not evaluated at module scope), or\ndeclare the function side-effect free so its calls are annotated too. A\nthird-party factory that cannot be declared is annotated by hand at the call\nsite, which is what the remaining hand-written annotations in the tree are.\n\nFactory calls are not the only module-scope statements that pin a module.\nesbuild and webpack keep, as a side effect, any property read at module scope\n(`navigator.userAgent`, `Date.now`, `SomeExtension.name`), any call or `new`\nthey were not told is pure (`new RegExp(...)`, `Object.freeze(...)`,\n`[...].join(',')`), an array or object spread, an `in` test, a `try`\nstatement, a mutation (`table.push(...)`, `Klass.static = ...`), a class with\na computed member (`[SOME_SYMBOL]?: number` — write `declare` on it), and an\nIIFE — together with everything the statement references. One\n`LexicalEditor.version = ...` after the class kept nearly all of `lexical` in\na bundle that imported only `createCommand` (#9120). Move such work into a\nfunction declared `@__NO_SIDE_EFFECTS__` and call it at module scope, so the\nbuild annotates the call; annotate a call to a builtin or to a third-party\nfunction (`/* @__PURE__ */ forwardRef(...)`) by hand. Two tests enforce this\nby bundling a bare `import 'pkg'` of every entry with esbuild and requiring\nthat nothing is retained: `scripts/__tests__/unit/treeShakingSource.test.ts`\non the sources (runs with `pnpm run test-unit`) and\n`scripts/__tests__/integration/tree-shaking.test.mjs` on the published\nbuilds. A failure prints the retained code; its first statement is usually\nthe culprit.\n\nA factory whose body is a trivial expression over its own arguments (like\n`safeCast`, `defineExtension`, or `configExtension`) is additionally marked\n`@lexical-inline <form>` and listed in that file's `INLINE_FACTORIES`: the\nbuild replaces calls to it with the expression it would have returned, which\nneeds no annotation at all. **The body of a marked function is reproduced by\nthe build**, so keep it trivial — `packages/lexical-compiler`'s unit\ntests compare what the transform emits against what the real function returns\nand will fail if the two drift apart.\n\n## High-Level Architecture\n\n### Core Concepts\n\nLexical is built around several key architectural concepts that work together:\n\n**Editor Instance** - Created via `createEditor()`, wires everything together. Manages the EditorState, registers listeners/commands/transforms, and handles DOM reconciliation.\n\n**EditorState** - Immutable data model representing the editor content. Contains:\n- A node tree (hierarchical structure of LexicalNodes)\n- A selection object (current cursor/selection state)\n- Fully serializable to/from JSON\n\n**`$` Functions Convention** - Functions prefixed with `$` (e.g., `$getRoot()`, `$getSelection()`) can ONLY be called within:\n- `editor.update(() => {...})` - for mutations\n- `editor.read(() => {...})` - for read-only access\n- Node transforms and command handlers (which have implicit update context)\n\nThis is similar to React hooks' restrictions but enforces synchronous context instead of call order.\n\n**Double-Buffering Updates** - When `editor.update()` is called:\n1. Current EditorState is cloned as work-in-progress\n2. Mutations modify the work-in-progress state\n3. Multiple synchronous updates are batched\n4. DOM reconciler diffs and applies changes\n5. New immutable EditorState becomes current\n\n**Node Immutability & Keys** - All nodes are recursively frozen after reconciliation. Node methods automatically call `node.getWritable()` to create mutable clones. All versions of a logical node share the same runtime-only key, allowing node methods to always reference the latest version from the active EditorState.\n\n### Monorepo Structure\n\nThis is a monorepo with packages in `packages/`:\n\n**Core Packages:**\n- `lexical` - Core framework (Editor, EditorState, base nodes, selection, updates)\n- `@lexical/react` - React bindings (LexicalComposer, plugins as components)\n- `@lexical/headless` - Headless editor for server-side/testing\n\n**Feature Packages** (extend core with nodes/commands/utilities):\n- `@lexical/rich-text` - Rich text editing (headings, quotes, etc.)\n- `@lexical/plain-text` - Plain text editing\n- `@lexical/extension` - Extend editor functionality\n- `@lexical/list` - List nodes (ordered/unordered/checklist)\n- `@lexical/table` - Table support\n- `@lexical/code` - Code block with syntax highlighting\n- `@lexical/link` - Link nodes and utilities\n- `@lexical/markdown` - Markdown import/export\n- `@lexical/html` - HTML serialization\n- `@lexical/history` - Undo/redo\n- `@lexical/yjs` - Real-time collaboration via Yjs\n- And many more...\n\n**Development Packages:**\n- `lexical-playground` - Full-featured demo application\n- `lexical-website` - Docusaurus documentation site\n\n### Key Architectural Patterns\n\n**Extensions** - Extensions should be used to add features and configuration\nto an editor. The set of extensions in an editor must be determined when the\neditor is created with `buildEditorFromExtensions`. Extensions with\nfunctionality that can be toggled on or off typically have a `disabled`\nconfiguration property and output signal that defaults to `false`. See the\nlexical-extension package and the supporting code in lexical for\nmore examples and implementation details.\n\n```tsx\nexport interface MyConfig {\n  disabled: boolean;\n}\nexport const MyExtension = defineExtension({\n  build: (_editor, config, _state) => namedSignals(config),\n  config: safeCast<MyConfig>({ disabled: false }),\n  name: '@lexical/docs/My',\n  nodes: () => [MyNode],\n  register: (editor, _config, state) => {\n    const {disabled} = state.getOutput();\n    return effect(() => {\n      if (!disabled.value) {\n        return editor.registerUpdateListener(({editorState}) => {\n          // React to updates\n        });\n      }\n    })\n  },\n})\n```\n\n**Plugin System (React)** - Plugins are a legacy pattern for React components\nto hook into the editor lifecycle, extensions should be preferred for new code:\n```jsx\nfunction MyPlugin() {\n  const [editor] = useLexicalComposerContext();\n  useEffect(() => {\n    return editor.registerUpdateListener(({editorState}) => {\n      // React to updates\n    });\n  }, [editor]);\n  return null;\n}\n```\n\n**Command Pattern** - Commands are the primary communication mechanism:\n- Create with `createCommand()`\n- Dispatch with `editor.dispatchCommand(command, payload)`\n- Handle with `editor.registerCommand(command, handler, priority)`\n- Handlers propagate by priority until one stops propagation\n\n**Node Transforms** - Registered via `editor.registerNodeTransform(NodeClass, transform)`. Called automatically during updates when nodes of that type change. Have implicit update context.\n\n**Listeners** - All `editor.register*()` methods return cleanup functions for easy unsubscription.\n\n## Type System\n\nThis codebase uses **both TypeScript and Flow**:\n- Source files are primarily TypeScript (`.ts`, `.tsx`)\n- Flow type definitions are generated in `packages/*/flow/` directories\n- Run `pnpm run flow` to check Flow types\n- Run `pnpm run tsc` to check TypeScript types\n- Both are checked in CI via `pnpm run ci-check`\n\nWhen adding/modifying APIs, types must be maintained for both systems.\n\n## Backwards Compatibility\n\n**All changes MUST be backwards compatible.** Lexical is a widely-adopted OSS library, and breaking changes ripple out to every downstream consumer.\n\n- Do NOT remove or rename existing public APIs, exported functions, types, or `$` functions. Add new APIs alongside the old ones instead.\n- Do NOT change the signature, return type, or behavior of existing public APIs in ways that could break callers. Prefer additive, optional parameters.\n- Preserve the serialization format of `EditorState` and node JSON. Serialized content produced by older versions must continue to deserialize correctly.\n- If an API genuinely must change, deprecate the old one first (keep it working, document the replacement) rather than removing it outright.\n- When in doubt, assume external code depends on the current behavior and keep it intact.\n\n## Dependencies: Lexical is a singleton within one app\n\nAn application must resolve exactly one copy of `lexical`, and one copy of each\n`@lexical/*` package it uses. The active editor and editor state are\nmodule-scope variables in `LexicalUpdates.ts`, node registration and\n`instanceof LexicalNode` compare class references, and commands are object\nidentities from `createCommand()` — none of which survive a second copy of the\nmodule. Two copies also mean two versions, which is API drift inside one editor.\n\nThe boundary is the app, not the page: several self-contained apps, each with\nits own bundled Lexical, can coexist on one page (`isLexicalEditor` is an\n`instanceof` check precisely to support that). They just cannot interoperate —\nnodes, editors, `EditorState`s, selections and commands must never be passed\nacross such a boundary, only serialized data.\n\n- Inside this monorepo, packages depend on siblings with `\"lexical\":\n  \"workspace:*\"`; `pnpm publish` rewrites that to the exact published version,\n  so every `@lexical/*` release pins the matching `lexical`. Keep that pattern\n  when adding a package — do not hand-write a version range.\n- A **library** published on top of Lexical (anything outside this repo that\n  imports `lexical` or `@lexical/*`) declares those packages in\n  `peerDependencies`, plus `devDependencies` for its own build and tests, never\n  in `dependencies`. Apps bring their own Lexical and own the version.\n- Code under `examples/` and `packages/lexical-playground` are applications, so\n  they depend on Lexical directly — that is correct and should stay.\n\nThe user-facing version of this rule lives in\n`packages/lexical-website/docs/concepts/one-lexical-per-app.md`; keep the two in\nsync when either changes.\n\n## Important Development Notes\n\n### Reconciliation and Updates\n- `editor.read(...)`  or `editor.read('force-commit', ...)` flushes pending updates first, then provides consistent reconciled state\n- `editor.read('pending', ...)` reads the pending state (like `editor.update(...)`, but read-only)\n- `editor.read('latest', ...)` reads the latest consistent reconciled state\n- Inside `editor.update()`, you see pending state (transforms/reconciliation not yet run)\n- `editor.getEditorState().read()` always uses latest reconciled state, but prefer `editor.read('latest', ...)` in new code\n- Updates can be nested: `editor.update(() => editor.update(...))` is allowed but strongly discouraged\n- Do NOT nest updates in reads, or use a force-commit in an update\n\n### Node References\nAlways access node properties/methods within read/update context. Nodes automatically resolve to their latest version via their key. Don't store node references across update boundaries.\n\n### Testing Strategy\n- **Unit tests** - Vitest, located in `packages/**/__tests__/unit/**/*.test.{ts,tsx}`.\n  Add `// @vitest-environment node` after the copyright header for tests that\n  only exercise editor state, headless behavior, or pure utilities. DOM tests\n  use jsdom with `vmThreads` on Node 24.9+, which reuses the jsdom implementation\n  while keeping each file's window and module graph isolated. Older Node\n  versions use isolated forks because their VM contexts cannot load jsdom's\n  `require(esm)` dependencies. Platform-mocked tests depend on module isolation.\n  Compiler tests run with `scripts-unit` in\n  ordinary Node workers because native build tools are not VM-realm-safe.\n  Use `DOMParser` to parse HTML in DOM tests and iframes for separate-window\n  tests. Reserve direct JSDOM construction for Node fixtures or CSSOM behavior\n  that jsdom's DOMParser lacks.\n- **Browser tests** - Vitest browser mode driven by the Playwright runner, located in\n  `packages/**/__tests__/browser/**/*.test.{ts,tsx}`. Use these for behavior that depends on\n  a real layout/selection engine instead of stubbing the missing jsdom functionality from\n  `vitest.setup.mts` (e.g. `Range.getBoundingClientRect`, the Selection API). Run with\n  `pnpm run test-browser`; the browser set is controlled by the `VITEST_BROWSER` env var\n  (comma-separated, default `chromium`). Prefer building editors with the extension APIs\n  (`buildEditorFromExtensions`, or `LexicalExtensionComposer`/`LexicalExtensionEditorComposer`\n  in React).\n  - **Do not use `using`/`Disposable` in browser tests (or any browser-facing code).**\n    Explicit Resource Management (`using`, `Symbol.dispose`, `Disposable`) is not supported\n    in WebKit/Safari yet, so the syntax throws a `SyntaxError` there. `using` is fine in unit\n    tests (jsdom/Node), but browser tests should clean up with\n    `onTestFinished(() => editor.dispose())` instead — `editor.dispose()` is a plain method\n    available on the result of `buildEditorFromExtensions`.\n- **E2E tests** - Playwright, located in `packages/lexical-playground/__tests__/e2e/**/*.spec.{ts,mjs}`\n- E2E tests require the playground dev server running\n- Use `pnpm run debug-test-e2e-chromium` to debug E2E tests with browser UI\n\n### Custom Nodes\nWhen creating custom nodes:\n1. Extend a base node class (TextNode, ElementNode, DecoratorNode). The\n   constructor must work with no arguments.\n2. Implement `$config()` with `this.config('type', {extends: BaseNode, json:\n   schema})`, where `schema` is built with `nodeSchema<YourNode>()({...})`\n   as a module-scope `const` above the class. Written inline in `$config()`,\n   it makes TypeScript skip checking the class without a diagnostic.\n   Declare every property inherent to the node in it, and declare each one\n   stored verbatim in a field with\n   `withField(stringValue(), {field: '__name'})`. Initialize that field in\n   the class (`__name: string = '';`) so it is an own property of a fresh\n   node. Use `withAccessors` only where a property is not a plain field.\n3. Do not hand-write `clone`, `importJSON`, `exportJSON` or\n   `updateFromJSON`: `$config` derives them from the schema, and it carries\n   every property declared with `withField` across a clone. A field missing\n   from the schema is not carried, so it is lost the first time a later\n   update clones the node; declare it with `withField` rather than writing\n   `afterCloneFrom`. Ad-hoc data that is not inherent to the node type\n   belongs in NodeState (`createState`), which clones on its own. Write\n   `afterCloneFrom` only for storage neither covers, such as a property\n   declared through accessors on both sides whose value is not one field\n   (one that is can name it: `setter: {field: '__ids', method: 'setIDs'}`\n   in `MarkNode`). Call `super.afterCloneFrom(prevNode)` first, and copy\n   every field of the class, schema fields included: Lexical does not add\n   to a class's own `afterCloneFrom`.\n4. Implement `createDOM()` and `updateDOM()` (and `decorate()` for a\n   DecoratorNode). Keep `exportDOM()` on the class when the default export\n   is not enough.\n5. For HTML import, prefer `defineImportRule` rules registered by the\n   extension that provides the node: list `CoreImportExtension` and\n   `configExtension(DOMImportExtension, {rules: [...]})` in its\n   `dependencies` (see `ListExtension`, and `ListImportRules` for the rules,\n   in `@lexical/list`). Both APIs are `@experimental`, and the rules only\n   apply where the editor routes HTML through the pipeline; see\n   `packages/lexical-website/docs/serialization/dom-import.md`.\n   `importDOM()`, which that page calls legacy, is not deprecated yet, but\n   its deprecation is planned.\n6. Register with an extension (`nodes: () => [YourNode]`) or editor config.\n7. Export a `$createYourNode()` factory built on `$create(YourNode)` and the\n   node's setters, and a `$isYourNode()` guard.\n\nThe full schema reference is\n`packages/lexical-website/docs/serialization/serialization.md`; the cloning\nrules are in `packages/lexical-website/docs/concepts/node-cloning.md`.\n\n### Shadow DOM and iframe realm safety\n\nLexical supports editors whose root element lives inside a Shadow DOM or an\n`<iframe>` document. The editor resolves its `window` and `document` from\n`rootElement.ownerDocument.defaultView`, so code that reaches for the **global**\n`window` or `document` will silently use the wrong realm when the editor crosses\na frame boundary. Shadow DOM adds a second hazard: the browser **retargets**\nselection and focus reads to the shadow host, hiding the real nodes.\n\nUse the shadow/iframe-aware helpers exported from `lexical` instead of the raw\nbrowser globals:\n\n| Instead of | Use | Why |\n| --- | --- | --- |\n| `window` | `element.ownerDocument.defaultView` or `getDefaultView(element)` (@internal) | Returns the window that owns the element |\n| `window.getSelection()` | `getDOMSelection(rootElement.ownerDocument.defaultView)` | Reads selection from the correct window |\n| `selection.getRangeAt(0)` | `getDOMSelectionRange(selection, rootElement)` | Unwraps retargeted shadow-DOM selection |\n| `document` in `createDOM`/`updateDOM`/`exportDOM` | `$getDocument()` | Returns the document that owns the editor root (falls back to `globalThis.document`) |\n| `document` elsewhere | `getRootOwnerDocument(rootElement)` or `element.ownerDocument` | Returns the document that owns a specific element |\n| `document.activeElement` | `getActiveElement(element)` / `getActiveElementDeep(document)` | Walks through shadow roots to find the real focused element |\n| `event.target` | `getComposedEventTarget(event)` | Returns the un-retargeted target for composed events |\n| `element.parentElement` | `getParentElement(element)` | Crosses shadow boundaries correctly |\n| `selection.anchorNode` / `focusNode` | `getDOMSelectionPoints(selection, rootElement)` | Returns un-retargeted boundary points |\n\nTwo ESLint rules enforce the globals rows at lint time:\n- `no-restricted-syntax` (error) catches all `document.*` and `window.*` member access in library sources.\n- `@lexical/no-document-in-dom-methods` (error, with autofix) catches `document.*` specifically inside `createDOM`/`updateDOM`/`exportDOM` methods and autofixes to `$getDocument().*`.\n\nThe remaining rows involve local variable properties that lint cannot reliably detect, so they must be caught in code review.\n\nFor full details on the browser platform APIs involved, see\n[Shadow DOM and iframes](packages/lexical-website/docs/concepts/shadow-dom.md).\n\n### Commits and Pull Requests\nEvery commit message — not just PR bodies — must be written to the shape of\n`.github/pull_request_template.md`, so any commit can seed a PR directly\nwithout being rewritten. This applies to every commit you author, including\none-line fixes; do not wait to be asked. Read the template rather than working\nfrom memory, and fill in its sections:\n\n- **Subject line**: `[Affected Packages] PR Type: title`, where the packages are\n  the directory names under `packages/` that the diff touches and the type is\n  one of Breaking change / Refactor / Feature / Bug Fix / Documentation Update /\n  Chore. Test-only and tooling changes are `Chore`.\n- **`## Description`**: what the current behavior is and what this change makes\n  it do. Add `Closes #<issue>` only when there is a real issue number; drop the\n  line otherwise rather than leaving the template's placeholder behind.\n- **`## Test plan`** with `### Before` and `### After` subsections: the command\n  you ran, plus the actual failing output before and passing output after.\n  Paste real output — do not describe it. If a platform or browser in the\n  matrix could not be exercised, say so explicitly under `### After`.\n\nDrop any template section that does not apply to the diff instead of carrying\nan empty heading, and treat the template's HTML comments as instructions to\nfollow, not text to copy into the message.\n\n### Commit and PR Hygiene for Agents\nThis is an open source project: never include agent-session URLs or other\nprivate/team-internal links (e.g. `https://claude.ai/code/session_...`) in\ncommit messages, PR titles, or PR bodies. Those URLs are private to the\nperson or team that ran the session and are meaningless or misleading to\neveryone else. Co-authorship attribution (e.g. `Co-Authored-By:`) is fine.\nFor Claude Code this is enforced mechanically via `attribution.sessionUrl:\nfalse` in the checked-in `.claude/settings.json`; agents from other vendors\nshould follow this rule as written.\n\n### Build System\n- Uses Rollup for bundling\n- Build script: `scripts/build.mjs`\n- Supports multiple build modes: development, production, www (Meta internal)\n- TypeScript source → compiled to ESM for npm (`dist/*.js` in the\n  `\"type\": \"module\"` packages; CommonJS consumers load it through\n  `require(esm)`, so the fork modules must stay free of top-level await) and\n  to CommonJS for www only (`--www`)\n- Package manager logic in `scripts/shared/packagesManager.mjs`\n\n<!-- File: CLAUDE.md (Tokens: ~5 | Category: claude-rule) -->\n@AGENTS.md\n\n<!-- File: packages/lexical-website/docs/collaboration/_category_.json (Tokens: ~18 | Category: anthropic-skill) -->\n{\n  \"label\": \"Collaboration\",\n  \"position\": 6\n}\n\n<!-- File: packages/lexical-website/docs/collaboration/faq.md (Tokens: ~1279 | Category: anthropic-skill) -->\n---\nsidebar_position: 2\n---\n\n# Collaboration FAQ\n\n## Source of truth: Lexical State, Yjs and App's DB\n\nIt's recommended to treat the Yjs model as the source of truth. You can store the document to a database for indexing.\nBut, if possible, you should never forget the Yjs model, as this is the only way clients without internet access can reliably join and sync with the server.\n\nYou can also treat the database as the source of truth. This is how it could be achieved:\n\n- Clients receive a `sessionId` when they connect to the server\n- When a client connects without an existing `sessionId`, get the content from the database and create a `sessionId`\n- When all clients disconnect, forget the room content and `sessionId` on the server after some timeout (e.g. 1 hour)\n- When a client reconnects, use that content on the server. Furthermore, get the `sessionId` from the client\n- When two clients with different `sessionId` reconnect, one of the clients should forget the room content. _In this case the client will lose content_ - although it is very unlikely if you set the forget timeout (see point 2) very high.\n\nOr, there is an ever simpler approach:\n\n- When a client connects to the server, the server populates the room content if empty\n- When all clients disconnect, the server forgets the room content after some timeout (e.g. 1 hour)\n- When a client was not able to reconnect for 40 minutes, the client must forget its local updates and start fresh (this should be enforced by the server)\n\nWhen the database is the source of truth, and if you want to be able to forget the Yjs model, you will always run into cases where clients are not able to commit changes. That's not too bad in most projects. It somehow limits you, because you can't cache the document on the client using y-indexeddb. On the other hand, it is much easier to maintain, and do Yjs upgrades. Furthermore, most people would say that SQL is a bit more reliable than Yjs.\n\n_* Based on the advice of the Yjs author - [Kevin Jahns](https://github.com/yjs/yjs/issues/82#issuecomment-328365015)_\n\n\n## Initializing `EditorState` from Yjs Document\n\nIt's achievable by leveraging headless Lexical and no-op provider for Yjs:\n\n<details>\n  <summary>createHeadlessCollaborativeEditor.ts</summary>\n\n  ```typescript\n  import type {Binding, Provider} from '@lexical/yjs';\n  import type {\n    Klass,\n    LexicalEditor,\n    LexicalNode,\n    LexicalNodeReplacement,\n    SerializedEditorState,\n  } from 'lexical';\n\n  import {createHeadlessEditor} from '@lexical/headless';\n  import {\n    createBinding,\n    syncLexicalUpdateToYjs,\n    syncYjsChangesToLexical,\n  } from '@lexical/yjs';\n  import {type YEvent, applyUpdate, Doc, Transaction} from 'yjs';\n\n  export default function headlessConvertYDocStateToLexicalJSON(\n    nodes: ReadonlyArray<Klass<LexicalNode> | LexicalNodeReplacement>,\n    yDocState: Uint8Array,\n  ): SerializedEditorState {\n    return withHeadlessCollaborationEditor(nodes, (editor, binding) => {\n      applyUpdate(binding.doc, yDocState, {isUpdateRemote: true});\n      editor.update(() => {}, {discrete: true});\n\n      return editor.getEditorState().toJSON();\n    });\n  }\n\n  /**\n   * Creates headless collaboration editor with no-op provider (since it won't\n   * connect to message distribution infra) and binding. It also sets up\n   * bi-directional synchronization between yDoc and editor\n   */\n  export function withHeadlessCollaborationEditor<T>(\n    nodes: ReadonlyArray<Klass<LexicalNode> | LexicalNodeReplacement>,\n    callback: (editor: LexicalEditor, binding: Binding, provider: Provider) => T,\n  ): T {\n    const editor = createHeadlessEditor({\n      nodes,\n    });\n\n    const id = 'main';\n    const doc = new Doc();\n    const docMap = new Map([[id, doc]]);\n    const provider = createNoOpProvider();\n    const binding = createBinding(editor, provider, id, doc, docMap);\n\n    const unsubscribe = registerCollaborationListeners(editor, provider, binding);\n\n    const res = callback(editor, binding, provider);\n\n    unsubscribe();\n\n    return res;\n  }\n\n  function registerCollaborationListeners(\n    editor: LexicalEditor,\n    provider: Provider,\n    binding: Binding,\n  ): () => void {\n    const unsubscribeUpdateListener = editor.registerUpdateListener(\n      ({\n        dirtyElements,\n        dirtyLeaves,\n        editorState,\n        normalizedNodes,\n        prevEditorState,\n        tags,\n      }) => {\n        if (tags.has('skip-collab') === false) {\n          syncLexicalUpdateToYjs(\n            binding,\n            provider,\n            prevEditorState,\n            editorState,\n            dirtyElements,\n            dirtyLeaves,\n            normalizedNodes,\n            tags,\n          );\n        }\n      },\n    );\n\n    const observer = (events: Array<YEvent<any>>, transaction: Transaction) => {\n      if (transaction.origin !== binding) {\n        syncYjsChangesToLexical(binding, provider, events, false);\n      }\n    };\n\n    binding.root.getSharedType().observeDeep(observer);\n\n    return () => {\n      unsubscribeUpdateListener();\n      binding.root.getSharedType().unobserveDeep(observer);\n    };\n  }\n\n  function createNoOpProvider(): Provider {\n    const emptyFunction = () => {};\n\n    return {\n      awareness: {\n        getLocalState: () => null,\n        getStates: () => new Map(),\n        off: emptyFunction,\n        on: emptyFunction,\n        setLocalState: emptyFunction,\n        setLocalStateField: emptyFunction,\n      },\n      connect: emptyFunction,\n      disconnect: emptyFunction,\n      off: emptyFunction,\n      on: emptyFunction,\n    };\n  }\n  ```\n</details>\n\n<!-- File: packages/lexical-website/docs/collaboration/react.md (Tokens: ~2689 | Category: anthropic-skill) -->\n---\nsidebar_position: 1\n---\n\n# React\n\nLexical provides `LexicalCollaborationPlugin` and `useCollaborationContext` hook within `@lexical/react` to accelerate creation of the collaborative React backed editors.\nThis is on top of the Yjs bindings provided by `@lexical/yjs`.\n\n\n:::tip\n\nClone [Lexical GitHub repo](https://github.com/facebook/lexical), run `pnpm i && pnpm run start` and open [`http://localhost:3000/split/?isCollab=true`](http://localhost:3000/split/?isCollab=true) to launch playground in collaborative mode.\n\n:::\n\n## Getting started\n\nThis guide is based on [examples/react-rich](https://github.com/facebook/lexical/tree/main/examples/react-rich) example.\n\n**Install minimal set of the required dependencies:**\n```bash\n$ npm i -S @lexical/react @lexical/yjs lexical react react-dom y-websocket yjs\n```\n\n:::note\n\n`y-websocket` is the only officially supported Yjs connection provider at this point. Although other providers may work just fine.\n\n:::\n\n**Get WebSocket server running:**\n\nThis allows different browser windows and different browsers to find each other and sync Lexical state. On top of this `YPERSISTENCE` allows you to save Yjs documents in between server restarts so clients can simply reconnect and keep editing.\n\n```bash\n$ HOST=localhost PORT=1234 YPERSISTENCE=./yjs-wss-db npx y-websocket\n```\n\n**Get basic collaborative Lexical setup:**\n\n```tsx\nimport { useCallback } from 'react';\n\nimport {$getRoot, $createParagraphNode, $createTextNode} from 'lexical';\nimport {LexicalCollaboration} from '@lexical/react/LexicalCollaborationContext';\nimport {LexicalComposer} from '@lexical/react/LexicalComposer';\nimport {ContentEditable} from '@lexical/react/LexicalContentEditable';\nimport {LexicalErrorBoundary} from '@lexical/react/LexicalErrorBoundary';\nimport {RichTextPlugin} from '@lexical/react/LexicalRichTextPlugin';\nimport {CollaborationPlugin} from '@lexical/react/LexicalCollaborationPlugin';\nimport * as Y from 'yjs';\nimport {$initialEditorState} from './initialEditorState';\nimport {WebsocketProvider} from 'y-websocket';\n\nfunction Editor() {\n  const initialConfig = {\n    // NOTE: This is critical for collaboration plugin to set editor state to null. It\n    // would indicate that the editor should not try to set any default state\n    // (not even empty one), and let collaboration plugin do it instead\n    editorState: null,\n    namespace: 'Demo',\n    nodes: [],\n    onError: (error: Error) => {\n      throw error;\n    },\n    theme: {},\n  };\n\n  const getDocFromMap = (id: string, yjsDocMap: Map<string, Y.Doc>): Y.Doc => {\n    let doc = yjsDocMap.get(id);\n  \n    if (doc === undefined) {\n      doc = new Y.Doc();\n      yjsDocMap.set(id, doc);\n    } else {\n      doc.load();\n    }\n\n    return doc;\n  }\n\n  const providerFactory = useCallback(\n    (id: string, yjsDocMap: Map<string, Y.Doc>) => {\n      const doc = getDocFromMap(id, yjsDocMap);\n\n      return new WebsocketProvider('ws://localhost:1234', id, doc, {\n        connect: false,\n      });\n    }, [],\n  );\n\n  return (\n    <LexicalCollaboration>\n      <LexicalComposer initialConfig={initialConfig}>\n        <RichTextPlugin\n          contentEditable={<ContentEditable className=\"editor-input\" />}\n          placeholder={<div className=\"editor-placeholder\">Enter some rich text...</div>}\n          ErrorBoundary={LexicalErrorBoundary}\n        />\n        <CollaborationPlugin\n          id=\"lexical/react-rich-collab\"\n          providerFactory={providerFactory}\n          shouldBootstrap={true}\n        />\n      </LexicalComposer>\n    </LexicalCollaboration>\n  );\n}\n```\n\n**Initial editor content:**\n\nIn a production environment, you should bootstrap the editor's initial content on the server. If bootstrapping was left to the client and two clients connected at the same time, they could both try to initialize the content resulting in document corruption.\n\nUsing the `withHeadlessCollaborationEditor` function from the [FAQ](faq.md) page, you can create a bootstrapped `Y.Doc` with the following:\n\n```tsx\nimport type {CreateEditorArgs} from 'lexical';\n\nimport {$getRoot, $createParagraphNode} from 'lexical';\nimport {Doc} from 'yjs';\n\nimport {withHeadlessCollaborationEditor} from './createHeadlessCollaborativeEditor';\n\nfunction createBootstrappedYDoc(nodes: CreateEditorArgs['nodes']): Doc {\n  return withHeadlessCollaborationEditor(nodes, (editor, binding) => {\n    editor.update(() => {\n      $getRoot().append($createParagraphNode());\n    }, {discrete: true});\n    // binding.doc is the Doc the editor synced into\n    return binding.doc;\n  });\n}\n```\n\nIf you're simply following the above example to play around in a local dev environment, then you can add the following props to `CollaborationPlugin` to initialize the editor state client-side:\n\n```tsx\n// Dev-testing only, do not use in real-world cases.\ninitialEditorState={$initialEditorState}\nshouldBootstrap={true}\n```\n\n## See it in action\n\nSource code: [examples/react-rich-collab](https://github.com/facebook/lexical/tree/main/examples/react-rich-collab)\n\n<iframe width=\"100%\" height=\"600\" src=\"https://stackblitz.com/github/facebook/lexical/tree/main/examples/react-rich-collab?embed=1&file=src%2FApp.tsx&terminalHeight=0&ctl=1&showSidebar=0&devtoolsheight=0&view=preview\" sandbox=\"allow-forms allow-modals allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin allow-scripts\"></iframe>\n\n## Sharing one Yjs document between editors\n\nBy default a binding uses the `XmlText` shared type named `root` on the Yjs\n`Doc`, so each editor needs its own document. Two options let several editors\nlive in one document instead, which is what applications like collaborative\nsticky notes or comment threads usually want:\n\n- `rootName` picks a different top-level shared type, one per editor:\n\n```jsx\n<CollaborationPlugin\n  id={`note-${noteId}`}\n  providerFactory={providerFactory}\n  rootName={`note-${noteId}`}\n  shouldBootstrap={true}\n/>\n```\n\n- `getXmlText` resolves the root yourself, for roots that are not top-level\n  shared types (for example an `XmlText` stored in a `Y.Map`). It takes\n  precedence over `rootName`:\n\n```jsx\nconst getXmlText = useCallback(\n  (doc) => doc.getMap('notes').get(noteId),\n  [noteId],\n);\n\n<CollaborationPlugin\n  id={`note-${noteId}`}\n  providerFactory={providerFactory}\n  getXmlText={getXmlText}\n  shouldBootstrap={true}\n/>\n```\n\nGive each editor its own `id`, as above: the `id` keys the document map and the\nremote-cursor highlight registry, both of which are shared across the page, so\ntwo editors mounted under one `id` overwrite each other's entries.\n\nBoth options are also available on `createYjsBinding` for non-React usage.\n\n:::warning\n\nThe root is chosen when the binding is created, and a mounted editor cannot be\nrepointed at another root: changing `rootName` or `getXmlText` afterwards has no\neffect. To switch documents, remount the **editor** — put a `key` that changes\nwith the document on the component that owns the `LexicalComposer`. Remounting\nonly the plugin is not enough: the editor would keep the previous document's\ncontent and write it into the newly selected root.\n\n:::\n\n`getXmlText` runs while the binding is constructed, before the provider has\nsynced, so it has to resolve the root from whatever the local `Doc` holds at\nthat moment — reaching into a `Y.Array` that the server has not sent yet throws.\nEither mount the editor only once the note exists locally, or have the callback\ncreate the missing shared type. Write it to be idempotent: it can run more than\nonce for one editor (React StrictMode double-invokes render, and a remount\nbuilds a new binding), and a callback that unconditionally stores a new\n`XmlText` would throw away what the previous call stored.\n\nThe returned `XmlText` is checked to be an `XmlText` that is integrated into the\ndocument; that it is not already driving another live binding is yours to\nguarantee, since a second binding over one root takes over the collab nodes\ncached on its shared types.\n\nThe experimental `CollaborationPluginV2__EXPERIMENTAL` (and\n`createBindingV2__EXPERIMENTAL`) take the same two options, except that its\nroot is an `XmlElement`, so the callback is named `getXmlElement`. Create that\nelement as `new Y.XmlElement()` without a `nodeName`, which is how the root is\nrecognized.\n\n## Building collaborative plugins\n\n[Lexical Playground](https://playground.lexical.dev/) features set of the collaboration enabled plugins that integrate with primary document via `useCollaborationContext()` hook. Notable mentions:\n\n- [`CommentPlugin`](https://github.com/facebook/lexical/tree/main/packages/lexical-playground/src/plugins/CommentPlugin) - features use of the separate provider and Yjs room to sync comments.\n- [`ImageComponent`](https://github.com/facebook/lexical/blob/main/packages/lexical-playground/src/nodes/ImageComponent.tsx) - features use of the `LexicalExtensionEditorComposer` for its nested caption editor.\n- [`PollOptionComponent`](https://github.com/facebook/lexical/blob/main/packages/lexical-playground/src/nodes/PollComponent.tsx) - showcases poll implementation using `name` from the collaboration context.\n- [`StickyComponent`](https://github.com/facebook/lexical/blob/main/packages/lexical-playground/src/nodes/StickyComponent.tsx) - features use of the `LexicalExtensionEditorComposer` for its nested editor as well as sticky note position real-time sync.\n\n:::note\n\nWhile these \"playground\" plugins aren't production ready - they serve as a great example of collaborative Lexical capabilities as well as provide a good starting point.\n\n:::\n\n## Custom node property syncing\n\n`@lexical/yjs` syncs custom node properties by constructing a fresh node instance and inspecting its **own enumerable properties**. This means every property that should be synced across peers must be assigned in the constructor, even if the initial value is `undefined`.\n\n:::warning\n\nDeclaring a class property with TypeScript's optional syntax (`foo?: Type`) does **not** guarantee the property exists as an own enumerable property on the instance. Under TypeScript's default compilation settings (target < ES2022 without `useDefineForClassFields`), such declarations produce no initialization code.\n\n:::\n\n### What to do\n\nAlways initialize every node property in the constructor. Conditional initialization such as `if (someValue !== undefined) this.__someValue = someValue` is not enough, because the default construction path still needs to create the property.\n\nFor example:\n\n```ts\n// ✅ Correct — property is guaranteed to be an own property\nclass MyNode extends ElementNode {\n  __someValue: string | undefined;\n\n  constructor(someValue?: string, key?: NodeKey) {\n    super(key);\n    this.__someValue = someValue; // explicitly initialized\n  }\n}\n\n// ❌ Incorrect — property may not exist as an own property\nclass MyNode extends ElementNode {\n  __someValue?: string; // optional syntax without initialization\n\n  constructor(key?: NodeKey) {\n    super(key);\n    // __someValue is never assigned → won't be synced via yjs\n  }\n}\n```\n\nIf you use [`NodeState`](../concepts/node-state.md) for your custom properties, this concern does not apply. `NodeState` values are synced correctly without relying on constructor-created enumerable properties.\n\n## Yjs providers\n\nSetting up the communication between clients, managing awareness information, and storing shared data for offline usage is quite a hassle. Providers manage all that for you and are the perfect starting point for your collaborative app.\n\nSee [Yjs Website](https://docs.yjs.dev/ecosystem/connection-provider) for the list of the officially endorsed providers. Although it's not an exhaustive one.\n\n<!-- File: packages/lexical-website/docs/concepts/_category_.json (Tokens: ~17 | Category: anthropic-skill) -->\n{\n  \"label\": \"Concepts\",\n  \"position\": 3\n}\n\n<!-- File: packages/lexical-website/docs/concepts/commands.md (Tokens: ~1849 | Category: anthropic-skill) -->\n# Commands\n\nCommands are a very powerful feature of Lexical that lets you register listeners for events like `KEY_ENTER_COMMAND` or `KEY_TAB_COMMAND` and contextually react to them _wherever_ & _however_ you'd like.\n\nThis pattern is useful for building [`Toolbars`](https://github.com/facebook/lexical/blob/main/packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx) or complex extensions and nodes such as [tables](https://github.com/facebook/lexical/tree/main/packages/lexical-table) which require special handling for `selection`, `keyboard events`, and more.\n\nWhen registering a `command` you supply a `priority` and can return `true` to mark it as \"handled\", which stops other listeners from receiving the event. If a command isn't handled explicitly by you, it's likely handled by default in [`RichTextExtension`](https://github.com/facebook/lexical/blob/main/packages/lexical-rich-text/src/index.ts) or [`PlainTextExtension`](https://github.com/facebook/lexical/blob/main/packages/lexical-plain-text/src/index.ts).\n\n## `createCommand(...)`\n\nYou can view the core commands in the [`lexical` API reference](/docs/api/modules/lexical#commands). Other packages define their own, listed under Commands in each package's API reference, such as [`TOGGLE_LINK_COMMAND`](/docs/api/modules/lexical_link#toggle_link_command) in `@lexical/link`. If you need a custom command for your own use case, check out the typed `createCommand(...)` function.\n\n```js\nconst HELLO_WORLD_COMMAND: LexicalCommand<string> = createCommand('HELLO_WORLD');\n\neditor.registerCommand(\n  HELLO_WORLD_COMMAND,\n  (payload: string) => {\n    console.log(payload); // Hello World!\n    return false;\n  },\n  COMMAND_PRIORITY_EDITOR,\n);\n\neditor.dispatchCommand(HELLO_WORLD_COMMAND, 'Hello World!');\n```\n\n## `editor.dispatchCommand(...)`\n\nCommands can be dispatched from anywhere you have access to the `editor` such as a Toolbar Button, an event listener, or an extension, but most of the core commands are dispatched from [`LexicalEvents.ts`](https://github.com/facebook/lexical/blob/main/packages/lexical/src/LexicalEvents.ts).\n\nCalling `dispatchCommand` will implicitly call `editor.update` to trigger its command listeners if it was not called from inside `editor.update`.\n\nDon't dispatch a command from inside a read-only context such as\n`editor.read(fn)` or `editorState.read(fn)`. Command listeners usually change\nthe editor, so Lexical runs them in a separate writable update and\ndevelopment builds log a warning when they detect this. Dispatch after the\nread returns instead.\n\n```js\neditor.dispatchCommand(command, payload);\n```\n\nThe `payload`s are typed via the `createCommand(...)` API, but they're usually a DOM `event` for commands dispatched from an event listener.\n\nHere are some real examples from [`LexicalEvents.ts`](https://github.com/facebook/lexical/blob/main/packages/lexical/src/LexicalEvents.ts).\n\n```js\neditor.dispatchCommand(KEY_ARROW_LEFT_COMMAND, event);\n// ...\neditor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic');\n```\n\nAnd another example from the [`ToolbarPlugin`](https://github.com/facebook/lexical/blob/main/packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx) in our Playground.\n\n```js\nconst formatBulletList = () => {\n  editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND);\n};\n```\n\nWhich is later handled by [`ListExtension`](https://github.com/facebook/lexical/blob/main/packages/lexical-list/src/LexicalListExtension.ts) to insert the list into the editor.\n\n```js\neditor.registerCommand(\n  INSERT_UNORDERED_LIST_COMMAND,\n  () => {\n    $insertList('bullet');\n    return true;\n  },\n  COMMAND_PRIORITY_LOW,\n);\n```\n\n## `editor.registerCommand(...)`\n\nYou can register a command from anywhere you have access to the `editor` object, but it's important that you remember to clean up the listener with its remove listener callback when it's no longer needed.\n\nThe command listener will always be called from an `editor.update`, so you may use dollar functions. You should not use\n`editor.update` (and *never* call `editor.read(...)` or `editor.read('force-commit', ...)`) synchronously from within a command listener. It is safe to call\n`editor.read('latest', ...)` or `editor.getEditorState().read` if you need to read the previous state after updates have already been made.\n\n```js\nconst removeListener = editor.registerCommand(\n  COMMAND,\n  (payload) => boolean, // Return true to stop propagation.\n  priority,\n);\n// ...\nremoveListener(); // Cleans up the listener.\n```\n\nThe easiest way to get clean-up right is to register commands from an\n[extension](/docs/extensions/defining-extensions)'s `register` function and\nreturn the remove callback. The editor calls it when it is disposed. To\nregister several things, combine their callbacks with `mergeRegister` from\n`@lexical/utils`.\n\n```js\nimport {LinkExtension, TOGGLE_LINK_COMMAND} from '@lexical/link';\nimport {COMMAND_PRIORITY_BEFORE_EDITOR, defineExtension} from 'lexical';\n\nexport const LinkLoggerExtension = defineExtension({\n  // The command it observes is handled by LinkExtension\n  dependencies: [LinkExtension],\n  name: '@my-app/LinkLogger',\n  register: (editor) =>\n    editor.registerCommand(\n      TOGGLE_LINK_COMMAND,\n      (payload) => {\n        console.log('link', payload);\n        return false; // let the link extension handle it too\n      },\n      // LinkExtension handles this command at COMMAND_PRIORITY_EDITOR and\n      // returns true, so observe it at the BEFORE variant of that priority.\n      COMMAND_PRIORITY_BEFORE_EDITOR,\n    ),\n});\n```\n\nA listener that only observes a command, like this one, needs to run before\nthe listener that handles it. Depending on the extension that handles it and\nusing the `COMMAND_PRIORITY_BEFORE_*` variant of its priority does that\nwithout escalating to a higher priority. See\n[Priorities and ordering](#priorities-and-ordering) below.\n\nIn a legacy React plugin, return the `registerCommand` call from a\n`useEffect` instead.\n\nAnd as seen above and below, `registerCommand`'s callback can return `true` to signal to the other listeners that the command has been handled and propagation will be stopped.\n\nHere's a simplified example of handling a `KEY_TAB_COMMAND` from [`TabIndentationExtension`](https://github.com/facebook/lexical/blob/main/packages/lexical-extension/src/TabIndentationExtension.ts), which is used to dispatch a `OUTDENT_CONTENT_COMMAND` or `INDENT_CONTENT_COMMAND`.\n\n```js\neditor.registerCommand(\n  KEY_TAB_COMMAND,\n  (payload) => {\n    const event: KeyboardEvent = payload;\n    event.preventDefault();\n    return editor.dispatchCommand(\n      event.shiftKey ? OUTDENT_CONTENT_COMMAND : INDENT_CONTENT_COMMAND,\n    );\n  },\n  COMMAND_PRIORITY_EDITOR,\n);\n```\n\nNote that the same `KEY_TAB_COMMAND` command is registered by [`LexicalTableSelectionHelpers.ts`](https://github.com/facebook/lexical/blob/main/packages/lexical-table/src/LexicalTableSelectionHelpers.ts), which handles moving focus to the next or previous cell within a `TableNode`, but the priority is high (`COMMAND_PRIORITY_HIGH`) because this behavior is very important.\n\n### Priorities and ordering\n\nCommand listeners are called in the following order until a listener returns `true`:\n\n- From priority highest to lowest (critical, high, normal, low, editor)\n- All `COMMAND_PRIORITY_BEFORE_${priority}` listeners, most recently registered first\n- All `COMMAND_PRIORITY_${priority}` listeners, in registration order\n\n:::note\n\nThe `COMMAND_PRIORITY_BEFORE_*` priorities make it much easier to override\ndefault behavior without escalating the priority.\n\n:::\n\nIt is best practice to use the lowest priority possible, so most commands will\nbe registered with `COMMAND_PRIORITY_EDITOR` for the default behavior, then\ncommands to override that behavior can be registered with\n`COMMAND_PRIORITY_BEFORE_EDITOR`. The higher priorities mostly serve purposes\nsuch as being able to observe-but-not-handle events and to support legacy code\nthat predates the availability of `COMMAND_PRIORITY_BEFORE_*`.\n\nA modern lexical app will typically only need the\n`COMMAND_PRIORITY_BEFORE_EDITOR` priority since the\nlast-registered-called-first ordering is suitable for almost all use cases. The\nolder priorities without BEFORE can be considered legacy and are primarily\noffered for compatibility.\n\nHere is the full ordering of priorities, from lowest to highest:\n\n- `COMMAND_PRIORITY_EDITOR`\n- `COMMAND_PRIORITY_BEFORE_EDITOR`\n- `COMMAND_PRIORITY_LOW`\n- `COMMAND_PRIORITY_BEFORE_LOW`\n- `COMMAND_PRIORITY_NORMAL`\n- `COMMAND_PRIORITY_BEFORE_NORMAL`\n- `COMMAND_PRIORITY_HIGH`\n- `COMMAND_PRIORITY_BEFORE_HIGH`\n- `COMMAND_PRIORITY_CRITICAL`\n- `COMMAND_PRIORITY_BEFORE_CRITICAL`\n\n<!-- File: packages/lexical-website/docs/concepts/compared-with-prosemirror.md (Tokens: ~1091 | Category: anthropic-skill) -->\n# Compared with ProseMirror\n\nLexical and [ProseMirror](https://prosemirror.net) are both frameworks for\nbuilding rich text editors on top of `contentEditable`, and both keep an\nimmutable document model as the source of truth. This page collects the main\ndifferences in one place, for readers who know one and are learning the other.\n\n## Document model {#document-model}\n\nLexical's tree is shaped like the HTML it renders (see\n[Document Model](./document-model.md)). This is a deliberate difference from\nProseMirror. ProseMirror's document is also a tree,\nand blocks can contain other blocks, but the inline content of a textblock is\na flat sequence of nodes (text and inline nodes) with marks for formatting and\nlinks, and every position in the document is a single integer.\n\n| | Lexical | ProseMirror |\n| -- | -- | -- |\n| Document | Immutable tree of nodes, one `RootNode` | Immutable tree of nodes, one `doc` node |\n| Node identity and navigation | Each node has a runtime key and knows its parent and siblings | Nodes are plain values with no parent links; a resolved position supplies ancestor context |\n| Blocks | `ElementNode` subclasses (paragraph, heading, list, table, …), or a block `DecoratorNode` (image, embed, …) | Block nodes defined in the schema |\n| Inline formatting | Format flags on each `TextNode` (bold, italic, code, …) | Marks on text |\n| Links and other inline wrappers | Inline `ElementNode`s (`LinkNode`, `MarkNode`) that contain text nodes | Marks on text, like formatting |\n| Embedded content | `DecoratorNode`, inline or block, rendered by your framework (for example React) | Leaf or atom nodes, often with a custom `NodeView` |\n| Several editable regions in one node | [Named slots](./named-slots.md): regions addressed by name, like a card's `title`, each isolated so editing and selection never cross the boundary | Child nodes in the order the schema's content expression allows, optionally marked `isolating`, or a separate editor inside a `NodeView` |\n| Addressing a position | Node key plus offset (`{key, offset, type}`) | One integer counted across the whole document |\n| Allowed structure | Declared by node classes, enforced with node transforms and normalization | Declared by a schema of content expressions |\n| Applying edits | Call node and selection methods inside `editor.update()` | Build a transaction from steps that address positions or ranges |\n| Rendering | Each node's `createDOM()`/`updateDOM()`, applied by the reconciler, and optionally changed per node class by a `DOMRenderExtension` override | `toDOM` in the schema, or a `NodeView` |\n\n## Serialization {#serialization}\n\nBecause a Lexical link is an element that already contains its text, it\nexports as an `<a>` around those text nodes. A mark-based model has to find the\nadjacent text that shares a mark and group it into one element first.\n\n| Format | Lexical | ProseMirror |\n| -- | -- | -- |\n| JSON | `editorState.toJSON()`, restored with `editor.parseEditorState()` | `doc.toJSON()` and `Node.fromJSON(schema, json)` |\n| HTML export | `$generateHtmlFromNodes()`, using each node's `createDOM()` or `exportDOM()` | `DOMSerializer.fromSchema(schema)`, using `toDOM` from the schema's nodes and marks |\n| HTML import | `$generateNodesFromDOM()`, using `importDOM()` or `DOMImportExtension` rules | `DOMParser.fromSchema(schema)`, using the schema's `parseDOM` rules |\n| Markdown | `@lexical/markdown` transformers or `@lexical/mdast` | `prosemirror-markdown`: a markdown-it parser and a serializer with a function per node and mark |\n| Clipboard | Copy writes plain text, HTML, and Lexical JSON. Paste prefers the JSON, then HTML, then plain text. | Copy writes HTML and plain text. Paste parses the HTML with the schema's parse rules. |\n\nIn the JSON, a Lexical link is a `link` node with its text nodes as\n`children`, and bold is a `format` value on a `text` node, where\nProseMirror's JSON gives each text node a list of `marks`. In both editors the\ndefault HTML export reuses the definition that renders the editor\n(`createDOM()` in Lexical, `toDOM` in ProseMirror), so the two stay in step\nunless you override one. See [Serialization](../serialization/serialization.md)\nfor the details of each Lexical format.\n\n## History {#history}\n\nBoth editors group consecutive edits into one undo step. Lexical's\n[`HistoryExtension`](./history.md) merges edits made within `delay`\n(300ms by default) and keeps an unlimited stack unless you set `maxDepth`.\nProseMirror's history plugin uses `newGroupDelay: 500` and `depth: 100` by\ndefault, so `configExtension(HistoryExtension, {delay: 500, maxDepth: 100})`\nmatches its behavior.\n\n<!-- File: packages/lexical-website/docs/concepts/document-model.md (Tokens: ~949 | Category: anthropic-skill) -->\n# Document Model\n\nAn [editor state](./editor-state.md) holds a tree of nodes. This page explains\nhow that tree is shaped.\n\n## A DOM-like tree\n\nLexical's node tree is shaped like the HTML it renders. A paragraph is an\n`ElementNode` whose children are the nodes inside it, and inline structure\nnests the same way: a link is a `LinkNode` element that contains the text\nnodes it wraps, just as an `<a>` contains its text. Each node type decides how\nit renders with `createDOM()` and `updateDOM()`, so the model maps closely to\nthe DOM, and a position in the document is a node plus an offset within it.\n\nHere is a small document with a heading, a paragraph containing bold text and\na link, a block image, and a card that has a named `title` slot as well as\nordinary body children:\n\n```mermaid\nflowchart TB\n  accTitle: The shape of a Lexical document tree\n  accDescr: The root has four children in order: a heading, a paragraph, an image, and a card. The heading contains the text \"Welcome\". The paragraph contains the text \"Read the \", a bold text node \"docs\", and a link, which contains the text \"here\". The image is a block decorator node with no children. The card has a named slot called title, which holds a paragraph containing the text \"Card title\", and one ordinary child, a paragraph containing the text \"Card body\".\n  root[\"Root\"] --> heading[\"Heading (h1)\"]\n  root --> paragraph[\"Paragraph\"]\n  root --> image[\"Image<br/>(block DecoratorNode)\"]\n  root --> card[\"Card<br/>(ElementNode)\"]\n  heading --> welcome[\"Text: 'Welcome'\"]\n  paragraph --> read[\"Text: 'Read the '\"]\n  paragraph --> docs[\"Text: 'docs'<br/>{ format: bold }\"]\n  paragraph --> link[\"Link\"]\n  link --> here[\"Text: 'here'\"]\n  card -.->|\"slot: title\"| titlePara[\"Paragraph\"]\n  titlePara --> titleText[\"Text: 'Card title'\"]\n  card --> body[\"Paragraph\"]\n  body --> bodyText[\"Text: 'Card body'\"]\n```\n\nSolid arrows are ordinary children, which form an ordered list under their\nparent and are read with `getChildren()`. The dashed arrow is a\n[named slot](./named-slots.md): a separate channel on the host, keyed by name\ninstead of position and read with `$getSlot(card, 'title')`. A slot's value\nis the root of its own isolated region, so its `getParent()` is `null` and\n`$getSlotHost()` leads back to the card. Any `ElementNode` or `DecoratorNode`\ncan host slots, so a decorator such as the image could also expose an\neditable caption this way.\n\n## Working with the tree\n\nLexical code navigates the document the way DOM code does, with methods like\n`getParent()`, `getChildren()`, and `getNextSibling()`, and each node owns the\nDOM it renders. The resemblance is structural, not one\nnode per HTML element: bold or italic text, for example, is a format on a\n`TextNode`, not a separate `<strong>` or `<em>` node. See\n[Nodes](./nodes.mdx) for the built-in node types.\n\n## Customizing rendering\n\nYou can also change how nodes render without subclassing them.\n[`DOMRenderExtension`](../serialization/dom-render.md) from `@lexical/html`\ntakes overrides for `createDOM`, `updateDOM`, and `exportDOM`, for one node\nclass or for every node, such as adding a `data-` attribute or wrapping a\nnode's children in another element. Each override calls `$next()` to get the\ndefault result and adjusts it, so overrides from several extensions compose.\nExport overrides only affect HTML export, and `createDOM` overrides also carry\nthrough to export for nodes that use the default `exportDOM`. Changes made\nwhile updating the editor's DOM don't; see\n[Lexical -> HTML](../serialization/serialization.md#lexical---html) for which\nhooks reach export.\n\nFor how the tree is converted to and from JSON, HTML, and Markdown, see\n[Serialization](../serialization/serialization.md#formats-at-a-glance). For\nhow this model differs from a mark-based editor, see\n[Compared with ProseMirror](./compared-with-prosemirror.md).\n\n<!-- File: packages/lexical-website/docs/concepts/dom-events.md (Tokens: ~1266 | Category: anthropic-skill) -->\n# Working with DOM Events\n\nSometimes, when working with Lexical, it might be necessary or useful for you to attach a DOM Event Listener to the underlying DOM nodes that Lexical controls. For instance, you might want to show a popover when a user mouses over a specific node or open a modal when they click on a node. Either of these use cases (and many others) can be accomplished via native DOM Event Listeners. There are 3 main ways that you can listen for DOM Events on nodes controlled by Lexical:\n\n## 1. Event Delegation\n\nOne way to handle events inside the editor is to set a listener on the editor root element (the contentEditable Lexical attaches to). You can do this using a [Root Listener](listeners.md).\n\n```js\nfunction myListener(event) {\n    // You may want to filter on the event target here\n    // to only include clicks on certain types of DOM Nodes.\n    alert('Nice!');\n}\n\nconst ClickExtension = defineExtension({\n  name: '@my-app/Click',\n  register: (editor) =>\n    editor.registerRootListener((rootElement) => {\n      if (rootElement === null) {\n        return;\n      }\n      // add the listener to the current root element\n      rootElement.addEventListener('click', myListener);\n      // remove the listener when the root element changes - make sure the ref\n      // to myListener is stable so the removal works and you avoid a memory leak.\n      return () => rootElement.removeEventListener('click', myListener);\n    }),\n});\n```\n\nAn extension's `register` returns a cleanup function, and the editor calls it\nwhen it is disposed, so there is no teardown to write by hand. Add\n`ClickExtension` to your editor's `dependencies` to use it.\nThis can be a simple, efficient way to handle some use cases, since it's not necessary to attach a listener to each DOM node individually.\n\nThe `addEventListener`/`removeEventListener` pairing above is common enough that the core `lexical` package exports a `registerEventListener(target, type, listener, options?)` helper. It attaches the listener and returns a dispose function that removes it, so the example above becomes:\n\n```js\nimport {defineExtension, registerEventListener} from 'lexical';\n\nconst ClickExtension = defineExtension({\n  name: '@my-app/Click',\n  register: (editor) =>\n    editor.registerRootListener((rootElement) =>\n      // registerEventListener returns the matching removeEventListener cleanup,\n      // so there's no need to write the teardown by hand.\n      rootElement === null\n        ? undefined\n        : registerEventListener(rootElement, 'click', myListener),\n    ),\n});\n```\n\nIt mirrors the `addEventListener` overloads (so the event type is strongly typed) and composes well with `mergeRegister` when you need to register several listeners at once.\n\nTo attach several listeners to the same target, `registerEventListeners(target, listeners, options?)` takes a `{type: listener}` map and returns a single dispose function that removes all of them. Each listener's event argument is still strongly typed per event type, and the optional `options` argument is shared across every listener:\n\n```js\nimport {registerEventListeners} from 'lexical';\n\nconst removeListeners = registerEventListeners(\n  rootElement,\n  {\n    click: onClick,\n    keydown: onKeyDown, // receives a KeyboardEvent\n  },\n  {capture: true}, // shared by both listeners\n);\n```\n\nBecause `options` is shared, register a group that needs different options (such as a different `capture` flag) with a separate `registerEventListeners` call and combine the results with `mergeRegister`.\n\n## 2. Directly Attach Handlers\n\nIn some cases, it may be better to attach an event handler directly to the underlying DOM node of each specific node. With this approach, you generally don't need to filter the event target in the handler, which can make it a bit simpler. It will also guarantee that your handler isn't running for events that you don't care about. This approach is implemented via a [Mutation Listener](listeners.md).\n\n```js\nconst registeredElements: WeakSet<HTMLElement> = new WeakSet();\nconst removeMutationListener = editor.registerMutationListener(nodeType, (mutations) => {\n    editor.getEditorState().read(() => {\n        for (const [key, mutation] of mutations) {\n            const element: null | HTMLElement = editor.getElementByKey(key);\n            if (\n                // Updated might be a move, so that might mean a new DOM element\n                // is created. In this case, we need to add an event listener too.\n                (mutation === 'created' || mutation === 'updated') &&\n                element !== null &&\n                !registeredElements.has(element)\n            ) {\n                registeredElements.add(element);\n                element.addEventListener('click', (event: Event) => {\n                    alert('Nice!');\n                });\n            }\n        }\n    });\n});\n\n// teardown the listener - return it from an extension's `register` so the\n// editor removes it when it is disposed.\nremoveMutationListener();\n```\nNotice that here we don't worry about cleaning up, as Lexical will dereference the underlying DOM nodes and allow the JavaScript runtime garbage collector to clean up their listeners.\n\n## 3. Use NodeEventPlugin\n\nIf you're using React, we've wrapped approach #2 up into a simple React plugin that you can render inside your composer to achieve the same effect, without worrying about the details:\n\n```jsx\n<LexicalExtensionComposer extension={appExtension}>\n    <NodeEventPlugin\n        nodeType={LinkNode}\n        eventType={'click'}\n        eventListener={(e: Event) => {\n            alert('Nice!');\n        }}\n    />\n</LexicalExtensionComposer>\n```\n\nIf the editor lives inside a shadow root or an iframe, see\n[Shadow DOM and iframes](./shadow-dom.md) for the helper to read\n`event.target` through the shadow boundary — the browser retargets it to the\nshadow host on listeners attached above the boundary, so a plain\n`event.target` won't match a node inside the editor.\n\n<!-- File: packages/lexical-website/docs/concepts/editor-state.md (Tokens: ~3370 | Category: anthropic-skill) -->\n# Editor State\n\n## Why is it necessary?\n\nWith Lexical, the source of truth is not the DOM, but rather an underlying state model\nthat Lexical maintains and associates with an editor instance.\n\nWhile HTML is great for storing rich text content it's often \"way too flexible\" when it comes to text editing.\nFor example the following lines of content will produce equal outcome:\n\n```html\n<i><b>Lexical</b></i>\n<i><b>Lex<b><b>ical</b></i>\n<b><i>Lexical</i></b>\n```\n\n<details>\n  <summary>See rendered version!</summary>\n  <div>\n    <i><b>Lexical</b></i>\n    <i><b>Lex</b><b>ical</b></i>\n    <b><i>Lexical</i></b>\n  </div>\n</details>\n\nOf course, there are ways to normalize all these variants to a single canonical form, however this would require DOM manipulation and so re-rendering of the content. And to overcome this we can use Virtual DOM, or State.\n\nOn top of that it allows to decouple content structure from content formatting. Let's look at this example stored in HTML:\n\n```html\n<p>Why did the JavaScript developer go to the bar? <b>Because he couldn't handle his <i>Promise</i>s</b></p>\n```\n\n<figure class=\"text--center\">\n  <img src=\"/img/docs/state-formatting-html.drawio.svg\" alt=\"Nested structure of the HTML state\"/>\n  <figcaption>Nested structure of the HTML state because of the formatting</figcaption>\n</figure>\n\nIn contrast, Lexical decouples structure from formatting by offsetting this information to attributes. This allows us to have canonical document structure regardless of the order in which different styles were applied.\n\nHere is the same content as a Lexical node tree, printed the way the\n[tree view](/docs/getting-started/devtools) shows it (the numbers are node keys):\n\n```text\nroot\n  └ (2) paragraph\n    ├ (3) text \"Why did the JavaScript developer go to the bar? \"\n    ├ (4) text \"Because he couldn't handle his \" { format: bold }\n    ├ (5) text \"Promise\" { format: bold, italic }\n    └ (6) text \"s\" { format: bold }\n```\n\nThe paragraph's children are a flat list of text nodes, and the formatting is a\nproperty of each one, so there is only one way to represent this content.\n\n## Understanding the Editor State\n\nYou can get the latest editor state from an editor by calling `editor.getEditorState()`.\n\nEditor states have two phases:\n\n- During an update they can be thought of as \"mutable\". See \"Updating state\" below to\n  mutate an editor state.\n- After an update, the editor state is then locked and deemed immutable from there on. This\n  editor state can therefore be thought of as a \"snapshot\".\n\nEditor states contain two core things:\n\n- The editor node tree (starting from the root node). See\n  [Document Model](./document-model.md) for how the tree is shaped.\n- The editor [selection](./selection.md) (which can be null).\n\nFor example, \"Hello world\" with a link around \"world\" and the caret at the\nend looks like this:\n\n```mermaid\nflowchart TB\n  accTitle: A simple editor state\n  accDescr: The root contains a paragraph with a \"Hello \" text node and a link. The link contains the \"world\" text node. The selection's anchor and focus both point to offset 5 in \"world\".\n  subgraph state[\"EditorState\"]\n    direction TB\n    root[\"Root\"] --> paragraph[\"Paragraph\"]\n    paragraph --> hello[\"Text: 'Hello '\"]\n    paragraph --> link[\"Link\"]\n    link --> world[\"Text: 'world'\"]\n    selection[\"Selection\"] -.->|\"caret at offset 5\"| world\n  end\n```\n\n`editor.getEditorState()` returns the latest committed state. Its `toJSON()`\nmethod serializes the document tree only; the selection and the runtime node\nkeys are not included. To restore saved content, pass the JSON to\n`editor.parseEditorState()` and the result to `editor.setEditorState()`. The\neditor must have the same node types registered. See\n[Serialization](../serialization/serialization.md) for JSON, HTML, and\nMarkdown.\n\nHere's an example of how you can initialize an editor with saved content and\nthen persist it. The saved JSON is passed as the editor's `$initialEditorState`:\n\n```js\nimport {buildEditorFromExtensions} from '@lexical/extension';\nimport {RichTextExtension} from '@lexical/rich-text';\nimport {defineExtension} from 'lexical';\n\n// Get the saved content (e.g. loaded from a backend)\nconst initialEditorState = await loadContent();\n\nconst editor = buildEditorFromExtensions(\n  defineExtension({\n    $initialEditorState: initialEditorState,\n    dependencies: [RichTextExtension],\n    name: '@my-app/editor',\n  }),\n);\neditor.setRootElement(document.getElementById('editor'));\n\n// Store the content (e.g. when the user submits a form)\nasync function onSubmit() {\n  await saveContent(JSON.stringify(editor.getEditorState()));\n}\n```\n\nWith React, pass the extension to `LexicalExtensionComposer`. A component\nrendered inside it can reach the editor with `useLexicalComposerContext()`:\n\n```jsx\nimport {LexicalExtensionComposer} from '@lexical/react/LexicalExtensionComposer';\nimport {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';\nimport {RichTextExtension} from '@lexical/rich-text';\nimport {defineExtension} from 'lexical';\nimport {useMemo} from 'react';\n\nfunction SaveButton() {\n  const [editor] = useLexicalComposerContext();\n  return (\n    <button\n      onClick={() => saveContent(JSON.stringify(editor.getEditorState()))}>\n      Save\n    </button>\n  );\n}\n\nfunction Editor({initialEditorState}) {\n  // The editor is recreated whenever this extension changes,\n  // so keep it stable for the life of the editor.\n  const extension = useMemo(\n    () =>\n      defineExtension({\n        $initialEditorState: initialEditorState,\n        dependencies: [RichTextExtension],\n        name: '@my-app/editor',\n      }),\n    [initialEditorState],\n  );\n  return (\n    <LexicalExtensionComposer extension={extension}>\n      <SaveButton />\n    </LexicalExtensionComposer>\n  );\n}\n```\n\n`$initialEditorState` is applied once, when the editor is built; changing it\nlater has no effect on that editor. See \"Updating state\" below for the proper\nway to change editor state after initialization.\n\n`$initialEditorState` accepts:\n\n- a JSON string or a parsed `SerializedEditorState` object, passed to\n  `editor.parseEditorState()` (as in the example above);\n- an `EditorState` instance, applied directly with `editor.setEditorState()`;\n- a function `(editor) => void`, run inside `editor.update(...)`;\n- `null`, which skips default initialization entirely. Use this with\n  [collaboration](/docs/collaboration/react) so that the Yjs document, not\n  Lexical, owns the initial state.\n\nOmitting the field (or passing `undefined`) seeds the root with a default empty\n`ParagraphNode`. The two are not interchangeable: `null` leaves the root with no children,\nwhile `undefined` produces a single empty line. If your `loadContent` may yield `null` or\n`undefined` for new documents, coalesce to `undefined` (e.g. `(await loadContent()) ?? undefined`)\nso the editor still gets the default paragraph rather than the collab-style uninitialized\nstate.\n\nThe legacy `LexicalComposer` takes the same values as `initialConfig.editorState`,\nexcept that a function there only runs if the root is still empty.\n\n## Updating state\n\nAll reads and writes of the document happen inside a synchronous callback:\n\n```js\nimport {$createParagraphNode, $createTextNode, $getRoot} from 'lexical';\n\neditor.update(() => {\n  const paragraph = $createParagraphNode();\n  paragraph.append($createTextNode('Hello world'));\n  $getRoot().append(paragraph);\n});\n\nconst text = editor.read('force-commit', () => $getRoot().getTextContent());\n```\n\n### The `$` function convention {#dollar-functions}\n\nFunctions whose names start with `$`, such as `$getRoot()` and\n`$getSelection()`, only work inside one of these callbacks, because they act\non the active editor state. Calling them anywhere else throws an error. The\nconvention is similar to React Hooks:\n\n| | React Hooks | Lexical `$` functions |\n| -- | -- | -- |\n| Naming | `useFunction` | `$function` |\n| Can only be called | while rendering a component | inside an update or read |\n| Can call others of the same kind | ✅ | ✅ |\n| Must be synchronous | ✅ | ✅ |\n| Must be called unconditionally, in the same order | ✅ | ❌ No such rule |\n\nCommand handlers and node transforms already run inside an update, so they\ncan call `$` functions directly.\n\nThis works because, while a callback runs, Lexical keeps the active editor and\nthe active editor state in module-level variables, and `$` functions read\nthem: `$getRoot()` returns the root of the active state, and `$getEditor()`\nreturns the active editor. Because the callback is synchronous, nothing else\ncan run in between and change them. The same mechanism enforces the\ndifference between reads and updates. Inside a read, a method that would\nchange a node throws `Cannot use method in read-only mode.`\n\nGive your own functions a `$` prefix when they call `$` functions, so it is\nclear where they can be used. The\n[`@lexical/rules-of-lexical`](/docs/packages/lexical-eslint-plugin) ESLint rule\nchecks this convention.\n\nThe same rule applies to node objects. Call node methods only inside a read or\nupdate. Every node has a key that identifies it across versions of the editor\nstate, and node methods use that key to find the latest version of the node.\nThis is why a node reference taken earlier in an update stays usable after the\nnode changes. Keys exist only at runtime: they are not serialized, and you\nshould treat them as opaque.\n\nBecause the callbacks are synchronous, do any asynchronous work (fetching\ndata, awaiting a promise) first, and then enter an update with the result.\n\nAvoid nesting one `editor.update()` inside another: the inner update does not\nrun immediately but is queued to run after the outer one. Never start an\nupdate inside a read.\n\nEach update works on a pending copy of the editor state, and updates made in\nthe same tick are committed and reconciled to the DOM together. See\n[Updates](./updates.mdx#update-lifecycle) for the full lifecycle.\n\n### Replacing the state\n\nAnother way to set state is `setEditorState` method, which replaces current state with the one passed as an argument.\n\nHere's an example of how you can set editor state from a stringified JSON:\n\n```js\nconst editorState = editor.parseEditorState(editorStateJSONString);\neditor.setEditorState(editorState);\n```\n\n:::warning\n\n`setEditorState` rejects an `EditorState` that satisfies `editorState.isEmpty()` — i.e.\nthe root is the only node and there is no selection. In a development build it throws\n`\"setEditorState: the editor state is empty. Ensure the editor state's root node never\nbecomes empty.\"`; in production it warns with the same message and recovers by appending\nan empty `ParagraphNode`, so the editor stays usable. The `EditorState` produced by\ninitializing with `editorState: null` and never appending content (typical for a\ncollaboration document before peers connect) has exactly this shape, so persisting and\nreloading such a state hits this path. Note that `parseEditorState` itself succeeds — the\ncheck lands on the apply step. To avoid relying on the recovery, guard with\n`editorState.isEmpty()` before calling `setEditorState`, or seed the document with a\n`ParagraphNode` before serializing.\n\n:::\n\n## Reading state\n\nWhich state you see depends on how you read it:\n\n- Inside `editor.update()` you see the **pending** state, where your changes\n  are visible but transforms and reconciliation may not have run yet.\n  `editor.read('pending', fn)` gives you the same view without allowing\n  changes.\n- `editor.read('force-commit', fn)` first commits any pending updates, so it\n  always sees a consistent, reconciled state. Do not call it inside an update.\n  `editor.read(fn)` with no mode does the same thing, and is kept for\n  convenience and backwards compatibility. Inside a mutation or update\n  listener it does not commit an update that another listener started (that\n  update commits after the listeners have run, so they observe commits in\n  order); use `editor.read('latest', fn)` there.\n- `editor.read('latest', fn)` reads the most recently reconciled state without\n  committing anything, so pending changes are not visible.\n- `editorState.read(fn, {editor})` reads one particular snapshot, such as the\n  `editorState` an update listener receives. Passing the `editor` keeps\n  `$getEditor()` and extension lookups working inside the callback.\n\n## State update listener\n\nIf you want to know when the editor updates so you can react to the changes, you can add an update\nlistener to the editor, as shown below:\n\n```js\neditor.registerUpdateListener(({editorState}) => {\n  // The latest EditorState can be found as `editorState`.\n  // To read the contents of the EditorState, use the following API:\n\n  editorState.read(\n    () => {\n      // Just like editor.update(), .read() expects a closure where you can use\n      // the $ prefixed helper functions.\n    },\n    // Passing the editor keeps $getEditor() and extension lookups working.\n    {editor},\n  );\n});\n```\n\n## When are Listeners, Transforms, and Commands called?\n\nThere are several types of callbacks that can be registered with the editor that are related to\nupdates of the Editor State.\n\n| Callback Type | When It's Called |\n| -- | -- |\n| Update Listener | After reconciliation |\n| Mutation Listener | After reconciliation |\n| Node Transform | During `editor.update()`, after the callback finishes, if any instances of the node type they are registered for were updated |\n| Command | As soon as the command is dispatched to the editor (called from an implicit `editor.update()`) |\n\n## Synchronous reconciliation with discrete updates\n\nWhile commit scheduling and batching are normally what we want, they can sometimes get in the way.\n\nConsider this example: you're trying to manipulate an editor state in a server context and then persist it in a database.\n\n```js\neditor.update(() => {\n  // manipulate the state...\n});\n\nsaveToDatabase(editor.getEditorState().toJSON());\n```\n\nThis code will not work as expected, because the `saveToDatabase` call will happen before the state has been committed.\nThe state that will be saved will be the same one that existed before the update.\n\nFortunately, the `discrete` option for `LexicalEditor.update` forces an update to be immediately committed.\n\n```js\neditor.update(() => {\n  // manipulate the state...\n}, {discrete: true});\n\nsaveToDatabase(editor.getEditorState().toJSON());\n```\n\n### Cloning state\n\nLexical state can be cloned, optionally with custom selection. One of the scenarios where you'd want to do it\nis setting editor's state but not forcing any selection:\n\n```js\n// Passing `null` as a selection value to prevent focusing the editor\neditor.setEditorState(editorState.clone(null));\n```\n\n<!-- File: packages/lexical-website/docs/concepts/headless.md (Tokens: ~517 | Category: anthropic-skill) -->\n# Running Without a Browser\n\nThe DOM is only needed to show an editor on screen. Because every read and\nupdate goes through the editor state, Lexical also works where there is no\nbrowser DOM:\n\n- **With no DOM at all.** An editor that has no root element skips\n  reconciliation and DOM selection entirely, so you can build one with\n  `buildEditorFromExtensions()` and never call `setRootElement()`. Adding\n  `HeadlessExtension` from `@lexical/headless` makes that explicit: attaching a\n  root element then throws, and nested editors are headless too. Updates,\n  transforms, listeners, commands, and JSON serialization all still work,\n  which is enough to process documents on a server, apply changes from a\n  collaboration backend, or write tests.\n- **With a virtual DOM** such as happy-dom or jsdom, for the features that\n  create DOM nodes, like HTML import and export. `withDOM()` from\n  `@lexical/headless/dom` runs a callback with a temporary happy-dom window,\n  for example to convert HTML on a server.\n\n## Example: converting saved JSON to HTML on a server\n\n```ts\nimport {buildEditorFromExtensions} from '@lexical/extension';\nimport {HeadlessExtension} from '@lexical/headless';\nimport {withDOM} from '@lexical/headless/dom';\nimport {$generateHtmlFromNodes} from '@lexical/html';\nimport {RichTextExtension} from '@lexical/rich-text';\nimport {defineExtension} from 'lexical';\n\nconst editor = buildEditorFromExtensions(\n  defineExtension({\n    dependencies: [HeadlessExtension, RichTextExtension],\n    name: '@my-app/server-editor',\n  }),\n);\n\nexport function jsonToHtml(json: string): string {\n  editor.setEditorState(editor.parseEditorState(json));\n  return withDOM(() => editor.read(() => $generateHtmlFromNodes(editor)));\n}\n```\n\nThe editor must have the same node types registered as the editor that saved\nthe JSON, which is why it uses the same extensions. JSON and Markdown\nconversion do not need `withDOM()`.\n\n## `createHeadlessEditor()`\n\nBefore extensions, headless editors were created with `createHeadlessEditor()`\nfrom `@lexical/headless`. It still works, and it makes DOM-only methods such as\n`registerRootListener` and `registerMutationListener` throw. Because many\nextensions register those listeners, use `HeadlessExtension` with\n`buildEditorFromExtensions()` in new code.\n\n<!-- File: packages/lexical-website/docs/concepts/history.md (Tokens: ~1944 | Category: anthropic-skill) -->\n# History\n\nLexical adds support for Undo and Redo through the `@lexical/history` package —\n`registerHistory`, the `HistoryExtension` extension, or the `HistoryPlugin`\nReact component from `@lexical/react/LexicalHistoryPlugin`. Part of undo and redo requires supporting coalescing of certain dynamic operations such as continuous typing. The below provides background information regarding the continuous typing undo coalescing feature.\n\n**Undo Coalescing of Typed Text**\n\n**TL;DR**\nWhen working with text editors, users expect a well polished undo experience. A key part of this is a smooth, well thought out workflow when undoing continuous typing events. For this post, strict typing events may be limited to typing characters, backspacing and forward deleting. Users generally expect that continuous typing should fully undo with a single undo gesture and that they don’t require separate undo gestures for each character.\nThe algorithm that covers this is “undo coalescing of typed text” and there’s a certain degree of nuance involved. This post covers that nuance while attempting to avoid prescriptive implementation details.\n\n**What Kind of Edits Require Coalescing?**\nAs mentioned above, continuous typing should be coalesced into a single undo action. Continuous typing is a loaded phrase and we’ll get into some details below.\nThere are other types of edits that require undo coalescing such as rapidly resizing an image or rapidly changing the font size of selected text. These latter two examples fall under what might be termed “dynamic operations” and their implementation might vary quite a bit from coalescing of typing.\n\n**What Constitutes Continuous Typing?**\nFor the purposes of undo coalescing, continuous typing events are safely limited to these three actions:\n\n1. Forward typing characters (heads up, composed character typing discussed later).\n2. Deleting backwards, e.g. backspace gestures including backspace key presses.\n3. Forward deletion, e.g. forward delete gestures including Delete key presses.\n\nAny interruption to the above breaks the notion of continuous typing. Note that switching between either of these three actions may itself be considered an interruption of continuous typing or it may not. This ends up being a usability decision. In my opinion, when examining various word processors, switching between these three actions should maintain continuity, because users often make typos or corrections in the flow of typing and therefore would lean towards the whole sequence of typing, backspacing and deleting to be within a single undoable session.\nNote that Google Docs and Quip treat a switch between typing, backspacing and deleting as disruptions to the continuous typing sessions.\n\n**Start with Selected Text and Followup with Typing**\nLet’s start with some selected text. The user may have selected the text directly, as a result of pasting text or through some other sequence of steps. When followed by continuous typing, the initial selection should be included along with the following typing events as part of a single coalesced undo session.\n\n**Timeouts**\nText editing is generally a modeless user experience and so just about any event beyond the above typing actions will disrupt continuous typing. The first and foremost is a timeout.\nQuip seems to require continuous typing to occur within 1 second intervals, whereas Google Docs requires about 2 seconds. In my opinion, the timeout may stretch to as long as 3 to 5 seconds. The more time between typing events, the more chance the user has to intentionally input a block of text, a thought or concept.\nThe use of a timeout is not necessarily required, and the amount of time is certainly up for debate. Collaborative co-editing also makes use of timeouts when grouping text that is merged across clients. Longer timeouts tend to make collaboration a smoother, less disruptive experience, because receiving other client input while expressing one’s thoughts tends to be unhelpful.\nImplementers should consider the correct timeout based on their customer expectations and related collaborative features that might share this timeout.\n\n**Hard and Soft Returns**\nHard returns as generated by pressing the Return key tend to disrupt and thereby reset continuous typing.\nFor Google Docs and Quip, soft returns as generated by pressing shift+Return key do not terminate continuous typing. This makes sense in that the soft return ends text wrap but does not end the paragraph.\n\n**Changing Selections and Key Focus**\nAny change to the selection by the user should terminate coalescing. This includes:\n\n- Arrow gesture navigation.\n- Gestures to select a word, a sentence or a paragraph.\n- Any other selection change that does not move the blinking caret to the next or previous character by way of the above mentioned typing events.\n\nAdditionally, ending keyboard focus on the editing window, including moving focus to another editing window or a different view within the same window should also terminate coalescing. This also includes setting focus to modal popovers, such as, for example, a hyperlink property editor.\n\n**Composed Characters**\nCharacter composition occurs when typing diacritics, CJK and even emojis. The process usually involves at least these three phases:\n\n- Start composition.\n- Replace composed text with other text and remain composing. This step may occur many times within a single composition session.\n- Stop composition either through accepting the text or canceling the latest input.\n\nCoalescing should terminate at start composition. Replacing composed text should generally be coalesced. Stopping composition may or may not reset coalescing depending upon the implementation. Quip appears to try to coalesce multiple composed characters, but disrupts coalescing when switching between composed and non-composed characters. Google Docs appears to disrupt coalescing more frequently. Both coalesce the text replacements.\n\n**Auto Correct, Spelling Corrections and Text Substitutions**\nText substitution behaves much like composed characters. When the text is transformed, the coalescing should terminate and the transformed text becomes the first coalesced entry in the next undo session.\n\n**Copy & Pasting**\nAny actions like copy and paste typically trigger events that are not part of the 3 continuous typing actions described above. So, copy and paste also terminates coalescing.\n\n**In Conclusion**\nText undo coalescing greatly improves the usability of undo in that it allows the user to bypass all the intermediate events that went into forming or re-forming text within a paragraph. Given the modeless nature of text editing, limiting continuous typing to strictly three events allows for a robust and clean implementation. Implementers will need tight bottlenecks to trap selection changes, key focus changes as well as changes from collaborative clients. While the above is not complete by any means, I hope readers will find it helpful.\n\n## Tuning HistoryExtension for memory and long sessions\n\n`HistoryExtension` ships with config values chosen for the common case\nof a short-lived editor on a richly interactive surface. For\napplications that keep an editor mounted for a long time — chat\nthreads, long documents, anything where the user may produce thousands\nof distinct history events without reloading the page — two settings\nare worth tuning explicitly.\n\n### `maxDepth` — cap the undo stack\n\nBy default `maxDepth` is `null`, meaning the undo stack is\n**unbounded**. Every separate-history boundary (see \"What Constitutes\nContinuous Typing?\" above) pushes a `HistoryStateEntry` that retains a\nfull `EditorState` snapshot. Snapshots share a copy-on-write\n`NodeMap`, so the marginal cost of any one entry is small, but it is\nnot zero — and an unbounded stack accumulates a long tail over time.\n\nSet a finite `maxDepth` to keep the stack at a fixed length. The\nextension FIFO-evicts the oldest entries past the cap; older snapshots\nbecome unreachable and can be collected.\n\n```ts\nimport {configExtension, defineExtension} from 'lexical';\nimport {HistoryExtension} from '@lexical/history';\n\nconst editorExtension = defineExtension({\n  // ...\n  dependencies: [\n    // 100 events is a reasonable starting point that supports a deep\n    // enough undo stack for almost any interactive editing session.\n    configExtension(HistoryExtension, {maxDepth: 100}),\n  ],\n});\n```\n\nThe cap is also exposed as a writable signal on\n`HistoryExtension.output.maxDepth`, so it can be reconfigured at\nruntime (the new value applies to future pushes — the current stack\nis not retroactively trimmed when the cap is lowered).\n\n### `delay` — coalesce more aggressively\n\n`delay` is the merge window during which adjacent edits collapse into\nthe current history entry instead of pushing a new one. The default is\n`300`ms. A longer delay means fewer history events for the same\ntyping burst, which:\n\n- reduces the rate at which `undoStack` grows, multiplying the effect\n  of any `maxDepth` cap, and\n- gives the user undo gestures that match larger semantic chunks of\n  their writing.\n\nFor text-heavy editors a value in the `500`–`1000`ms range is common.\n\n```ts\nconfigExtension(HistoryExtension, {delay: 500, maxDepth: 100}),\n```\n\n### When NOT to cap history\n\nIf your editor is short-lived (mounted only for the duration of a\nmodal, page form, or single message draft) and undo is expected to\nreach back to the document's initial state, the default `maxDepth:\nnull` is fine — the stack will be discarded with the editor instance.\nThe cap matters when an editor is **long-lived** and can accumulate\nthousands of distinct history events without unmounting.\n\n<!-- File: packages/lexical-website/docs/concepts/key-management.md (Tokens: ~1792 | Category: anthropic-skill) -->\n# Key Management\n\nKeys are a fundamental concept in Lexical that enable efficient state management and node tracking. Understanding how keys work is crucial for building reliable editor implementations.\n\n## What are Keys?\n\nThe `__key` property is a unique identifier assigned to each node in the Lexical editor. These keys are:\n- Automatically generated by Lexical\n- Used to track nodes in the editor state\n- Essential for state management and updates\n- Immutable during a node's lifecycle\n\n## When to Use `__key`?\n\n### ✅ Correct Usage\n\nKeys should ONLY be used in two specific situations:\n\n1. **In Node Constructors**\n```typescript\nclass MyCustomNode extends ElementNode {\n  constructor(someData: string, key?: NodeKey) {\n    super(key); // Correctly passing key to parent constructor\n    this.__someData = someData;\n  }\n}\n```\n\n2. **In Static Clone Methods**\n```typescript\nclass MyCustomNode extends ElementNode {\n  static clone(node: MyCustomNode): MyCustomNode {\n    return new MyCustomNode(node.__someData, node.__key);\n  }\n}\n```\n\n### ❌ Incorrect Usage\n\nNever use keys in these situations:\n\n```typescript\n// ❌ Don't pass keys between different nodes\nconst newNode = new MyCustomNode(existingNode.__key);\n\n// ❌ Don't manipulate keys directly\nnode.__key = 'custom-key';\n```\n\n## How Lexical Uses Keys\n\n### Diagram\n\nThe dotted outlines show nodes that are re-used in a zero-copy fashion from one EditorState to the next\n\n```mermaid\ngraph TB\n  subgraph s0[\"Initial State\"]\n    direction TB\n    m0[\"NodeMap (v0)\"]\n    m0 -->|Key A| n0a[\"A (v0)\"]\n    m0 -->|Key B| n0b[\"B (v0)\"]\n  end\n  subgraph s1[\"Create Node C\"]\n    direction TB\n    style n1a stroke-dasharray: 5 5\n    style n1b stroke-dasharray: 5 5\n    m1[\"NodeMap (v1)\"]\n    m1 -->|Key A| n1a[\"A (v0)\"]\n    m1 -->|Key B| n1b[\"B (v0)\"]\n    m1 -->|Key C| n1c[\"C (v0)\"]\n  end\n  subgraph s2[\"Update Node A\"]\n    direction TB\n    style n2a stroke-dasharray: 5 0\n    style n2b stroke-dasharray: 5 5\n    style n2c stroke-dasharray: 5 5\n    m2[\"NodeMap (v1)\"]\n    m2 -->|Key A| n2a[\"A (v1)\"]\n    m2 -->|Key B| n2b[\"B (v0)\"]\n    m2 -->|Key C| n2c[\"C (v0)\"]\n  end\n  s0 -.-> s1 -.-> s2\n```\n\n### Node Map Structure\n\nThe EditorState maintains a `Map<NodeKey, LexicalNode>` that tracks all nodes. Nodes refer to each other using keys in their internal pointers:\n\n```typescript\n// Internal node structure (not for direct usage)\n{\n  __prev: null | NodeKey,\n  __next: null | NodeKey,\n  __parent: null | NodeKey,\n  // __first, __last and __size are only for ElementNode to track its children\n  __first: null | NodeKey,\n  __last: null | NodeKey,\n  __size: number\n}\n```\n\nThese internal pointers maintain the tree structure and should never be manipulated directly.\n\n### Key-Related APIs\n\n1. **Editor Methods**\n   ```typescript\n   // Get node by key\n   const node = $getNodeByKey(key);\n\n   // Get the DOM element a node key is reconciled to\n   const element = editor.getElementByKey(key);\n\n   // Get latest version of a node\n   const latest = node.getLatest();\n   \n   // Get mutable version for updates\n   const mutable = node.getWritable();\n   ```\n\n### Key Lifecycle\n\nNodeKeys are ephemeral and have several important characteristics:\n\n1. **Serialization**\n   - Keys are never serialized\n   - New keys are generated when deserializing (from JSON/HTML)\n   - Keys are only meaningful within their EditorState instance\n\n2. **Uniqueness**\n   - Keys are unique within an EditorState\n   - Current implementation uses serial numbers for debugging\n   - Should be treated as random and opaque values\n   - Never logically reused\n\nKeys are used internally by Lexical to:\n1. Track nodes in the editor state\n2. Manage node updates and versions\n3. Maintain referential integrity\n4. Enable efficient state updates\n\n## Common Pitfalls\n\n1. **Key Reuse**\n   ```typescript\n   // ❌ Never do this\n   function duplicateNode(node: LexicalNode) {\n     return new SameNodeType(data, node.__key);\n   }\n   ```\n\n2. **Manual Key Assignment**\n   ```typescript\n   // ❌ Never do this\n   node.__key = generateCustomKey();\n   ```\n\n3. **Incorrect Constructor/Clone Implementation**\n   ```typescript\n   // ❌ Never do this - missing key in constructor\n   class MyCustomNode extends ElementNode {\n     constructor(someData: string) {\n       super(); // Missing key parameter\n       this.__someData = someData;\n     }\n   }\n\n   // ✅ Correct implementation\n   class MyCustomNode extends ElementNode {\n     __someData: string;\n\n     constructor(someData: string, key?: NodeKey) {\n       super(key);\n       this.__someData = someData;\n     }\n     \n     static clone(node: MyCustomNode): MyCustomNode {\n       return new MyCustomNode(node.__someData, node.__key);\n     }\n\n     afterCloneFrom(prevNode: this): void {\n       super.afterCloneFrom(prevNode);\n       this.__someData = prevNode.__someData;\n     }\n   }\n   ```\n\n4. **Node Replacement**\n   ```typescript\n   // ❌ Never re-use the key when changing the node class\n   const editorConfig = {\n     nodes: [\n       CustomNodeType,\n       {\n         replace: OriginalNodeType,\n         with: (node: OriginalNodeType) => new CustomNodeType(node.__key),\n         withKlass: CustomNodeType\n       }\n     ]\n   };\n\n   // ✅ Correct: Use node replacement configuration\n   const editorConfig = {\n     nodes: [\n       CustomNodeType,\n       {\n         replace: OriginalNodeType,\n         with: (node: OriginalNodeType) => new CustomNodeType(),\n         withKlass: CustomNodeType\n       }\n     ]\n   };\n   ```\n\n   For proper node replacement, see the [Node Replacement guide](node-replacement.md).\n\n## Best Practices\n\n1. **Let Lexical Handle Keys**\n   ```typescript\n   import {$applyNodeReplacement} from 'lexical';\n\n   // Create node helper function\n   export function $createMyCustomNode(data: string): MyCustomNode {\n     return $applyNodeReplacement(new MyCustomNode(data));\n   }\n   ```\n\n## Testing Considerations\n\nWhen writing tests involving node keys:\n\n```typescript\ntest('node creation', async () => {\n  await editor.update(() => {\n    // ✅ Correct: Create nodes normally\n    const node = new MyCustomNode(\"test\");\n    \n    // ✅ Correct: Keys are automatically handled\n    expect(node.__key).toBeDefined();\n    expect(node.__key).not.toBe('');\n  });\n});\n```\n\n## Performance Impact\n\nUnderstanding key management is crucial for performance:\n\n1. Keys enable efficient node lookup (O(1))\n2. Proper key usage prevents unnecessary re-renders\n3. Lexical's key system optimizes state updates\n4. Improper key manipulation can cause performance issues\n\n## Common Questions\n\n**Q: How do I reference a node later?**\nA: Store a reference to the node. Conventionally, all node methods will use `getLatest()` or `getWritable()` which will look up the latest version of that node before reading or writing its properties, which is equivalent to using the key but is type-safe (but may cause errors if you try to use a reference to a node that no longer exists). In some situations it may be preferable to use the key directly, which is also fine.\n\n**Q: How do I ensure unique nodes?**\nA: Let Lexical handle key generation and management. Focus on node content and structure.\n\n<!-- File: packages/lexical-website/docs/concepts/keyboard-accessibility.md (Tokens: ~1980 | Category: anthropic-skill) -->\n# Keyboard Accessibility\n\nLexical follows the [WAI-ARIA Authoring Practices](https://www.w3.org/WAI/ARIA/apg/) keyboard model. This page documents the contracts that plugin authors and host applications rely on, so a Lexical editor can sit inside a larger keyboard-accessible flow without trapping or surprising the user.\n\n## Tab key\n\nBy default, `Tab` inside the editor follows the browser's normal focus order — it moves to the next focusable element on the page. There is no keyboard trap.\n\nThe optional `TabIndentationExtension` (`@lexical/extension`) takes over `Tab` and uses it for block indent / outdent. Hosts that mount this extension opt in to that behavior. WCAG 2.1.2 (No Keyboard Trap) still applies — pressing `Escape` blurs the editor (see below), at which point `Tab` leaves the editor again.\n\n## Escape key\n\nPressing `Escape` while the editor has focus calls `editor.blur()` via the rich-text Escape command handler (registered at `COMMAND_PRIORITY_EDITOR`). The handler only blurs when the current selection is a range selection; with a `NodeSelection` (or no selection) it returns `false` and falls through without blurring. After blur, the next `Tab` advances to the page's next focusable element — so the editor is not a hard keyboard trap. (`TabIndentationExtension` consumes `Tab` only while a range selection is active, and its own source notes that taking over `Tab` is generally discouraged for keyboard accessibility, so treat WCAG 2.1.2 conformance as something to verify in your integration rather than a guarantee.)\n\nPlugin authors who want to intercept `Escape` (modal close, dropdown close, etc.) should register their handler at a higher priority and return `true` to stop the command chain. Common patterns already established in the codebase:\n\n| Priority | Caller | Purpose |\n| --- | --- | --- |\n| `HIGH` | Floating link editor | Close the floating UI, keep editor focus |\n| `HIGH` | Equation component (while editing) | Close the equation editor input |\n| `NORMAL` | Comment plugin | Cancel inline comment editing |\n| `LOW` | Image component | Escape out of caption / resize editing: drop the inner selection and restore the `NodeSelection` on the image decorator |\n| `EDITOR` | Rich text (default) | `editor.blur()` (range selection only) |\n\nHandlers that return `false` let the chain continue, so Escape can still fall through to the default blur even after a higher-priority handler runs.\n\nSome host UI binds its close handler directly to a DOM `keydown` listener rather than going through `KEY_ESCAPE_COMMAND` — the playground's `Modal` via `window.addEventListener('keydown', ...)` and its `DropDown` via a React `onKeyDown` on the menu. Those paths run alongside the table above; they do not participate in the editor's command priority chain.\n\n## React hooks and `@lexical/a11y` extensions\n\nThe keyboard helpers below ship as `@lexical/a11y` _extensions_ — the extension is the API and does all of the work (event listeners, focus management, the live region). The `@lexical/react` hooks (`useLexicalFocusManagerRef`, `useLexicalRovingTabIndexRef`, `useLexicalFocusTrapRef`, `useLexicalAriaLiveRegion`) are thin React adapters that only _communicate_ with their extension: each returns a `RefCallback` (or an `announce` function) that drives the extension's signals. Using a hook therefore **requires** its matching extension to be registered in the editor's extension tree — it is not an optional integration path. Non-React hosts (Svelte, Vue, vanilla DOM) use the extensions directly and skip the hooks.\n\n## Toolbar focus jump\n\nThe playground uses `useLexicalFocusManagerRef` (`@lexical/react/useLexicalFocusManagerRef`) to provide a documented shortcut for moving focus from the editor to the toolbar without tabbing through the page. The hook returns a `RefCallback` that registers the attached element as a focus-managed toolbar. It listens for `Alt+F10` inside the editor and moves focus into the toolbar — preferring the item that currently carries `tabindex=\"0\"` (the active roving item, when paired with `useLexicalRovingTabIndexRef`), and otherwise the first focusable item. While focus is in the toolbar, pressing `Escape` restores both focus and the editor's prior selection via `editor.focus()`. Multiple toolbars can be registered simultaneously. Requires `FocusManagerExtension` from `@lexical/a11y` in the editor's extension tree.\n\nThis matches the WAI-ARIA editor menubar pattern and the convention used by Word and CKEditor. Hosts that don't mount `useLexicalFocusManagerRef` get the default browser flow (Tab in, Tab out).\n\n## Toolbar arrow-key navigation\n\nToolbars that opt into `useLexicalRovingTabIndexRef` (`@lexical/react/useLexicalRovingTabIndexRef`) collapse to a single tab stop: arrow keys move between items inside the toolbar; `Tab` moves past the toolbar as a whole. Items keep `tabindex=\"-1\"` except the active one (`tabindex=\"0\"`). The hook returns a `RefCallback` — multiple containers can be registered simultaneously. Tracks the WAI-ARIA toolbar pattern. Requires `RovingTabIndexExtension` from `@lexical/a11y` in the editor's extension tree.\n\n## Modal focus trap\n\n`useLexicalFocusTrapRef` (`@lexical/react/useLexicalFocusTrapRef`) returns a `RefCallback` that cycles `Tab` / `Shift+Tab` inside a modal container and restores focus to the previously-focused element on deactivation. While active, the trap also installs a document-level `focusin` listener that pulls any focus landing outside the container back to the first focusable descendant (or the container itself) — so it recovers from the browser routing `Tab` out to its own chrome. Although several containers can be _registered_, only one trap should be _active_ at a time: concurrent traps install competing `focusin` listeners and fight over focus. Escape is **not** intercepted by the hook — modal owners handle the close key themselves, so the editor's Escape contract above stays predictable. Requires `FocusTrapExtension` from `@lexical/a11y` in the editor's extension tree.\n\n## Screen reader announcements\n\n`useLexicalAriaLiveRegion` (`@lexical/react/useLexicalAriaLiveRegion`) returns a stable `announce(message)` function. The hook itself mounts no DOM — like the others above, it is a thin adapter that **requires `AriaLiveRegionExtension` from `@lexical/a11y` in the editor's extension tree**. That extension owns the lifecycle of the visually-hidden region (`role=\"status\"`, `aria-atomic`, `aria-live` defaulting to `polite` — configurable via its `politeness` config): the region is created in the extension's `register` step and disposed on the editor's teardown, not on the hook's unmount. `EditorModeAnnounceExtension` (mode transitions via `registerEditableListener`) and `HistoryAnnounceExtension` (undo / redo via `UNDO_COMMAND` / `REDO_COMMAND`) announce through the same region. Both extensions accept optional message overrides for i18n.\n\n## Component ARIA quick reference\n\nWhere Lexical already applies ARIA attributes today (set in each component's `createDOM` / `updateDOM`, declaratively in the surrounding React tree, or applied imperatively via `setAttribute` on a managed or portaled element — as the typeahead menu and the live region do):\n\n| Component | role | aria-\\* attributes |\n| --- | --- | --- |\n| `ToolbarPlugin` (playground) | `toolbar` | `aria-label` |\n| `Modal` (playground) | `dialog` | `aria-modal`, `aria-labelledby` |\n| `LexicalListItemNode` (check list leaf) | `checkbox` | `aria-checked` |\n| `LexicalMenu` typeahead popup | `listbox` | `aria-label` |\n| Editor root (while typeahead is open) | — | `aria-controls`, `aria-activedescendant` |\n| `EquationNode` (playground) | `math` | `aria-label` |\n| `useLexicalAriaLiveRegion` region | `status` | `aria-live`, `aria-atomic` |\n\nCustom nodes follow the same pattern: set the attributes in `createDOM` and re-apply in `updateDOM` when the state they reflect changes. There is no central ARIA contract on `LexicalNode`; treat each subclass's `createDOM` as the source of truth.\n\n## Browser notes\n\nLexical relies on the standard DOM `Escape` contract — verified on Chrome and Safari (macOS, normal window). Safari in fullscreen intercepts `Escape` at the browser level to exit fullscreen; that is the browser's contract, not Lexical's.\n\n## What this page does not cover\n\n- Touch / mobile screen reader gestures. The hooks above share the same DOM contracts with mobile (TalkBack, VoiceOver iOS) but mobile has not been exercised in this set of changes.\n- High contrast / forced-colors theming. See the playground's `index.css` `@media (forced-colors: active)` block for a baseline.\n\n<!-- File: packages/lexical-website/docs/concepts/listeners.md (Tokens: ~1320 | Category: anthropic-skill) -->\n# Listeners\n\nListeners are a mechanism that lets the Editor instance inform the user when a certain operation has occurred. All listeners follow a reactive pattern where you can do an operation upon something happening in the future. All listeners also return a function that easily allows for the\nlistener to be unregistered. Below are the different listeners that Lexical supports today:\n\n## `registerUpdateListener`\n\nGet notified when Lexical commits an update to the DOM.\n\n```js\nconst removeUpdateListener = editor.registerUpdateListener(({editorState}) => {\n  // The latest EditorState can be found as `editorState`.\n  // To read the contents of the EditorState, use the following API:\n\n  editorState.read(() => {\n    // Just like editor.update(), .read() expects a closure where you can use\n    // the $ prefixed helper functions.\n  });\n});\n\n// Do not forget to unregister the listener when no longer needed!\nremoveUpdateListener();\n```\n\nThe update listener callbacks receives a single argument containing the follow properties:\n\n- `editorState` the latest updated Editor State\n- `prevEditorState` the previous Editor State\n- `tags` a Set of all tags that were passed to the update\n- `dirtyElements` a Map of the NodeKeys of ElementNodes that were marked\n  dirty, to a boolean that is true when the node was intentionally mutated\n- `dirtyLeaves` a Set of the NodeKeys of all dirty nodes that are not\n  ElementNodes\n- `mutatedNodes` a Map of LexicalNode constructors to a\n  `Map<NodeKey, NodeMutation>`, or null when no DOM was mutated. Only\n  populated when at least one MutationListener is registered\n- `normalizedNodes` (advanced) the keys of TextNode descendants that were\n  merged with a sibling by normalization\n\nOne thing to be aware of is \"waterfall\" updates. This is where you might schedule an update inside an update\nlistener, as shown below:\n\n```js\neditor.registerUpdateListener(({editorState}) => {\n  // Read the editorState and maybe get some value.\n  editorState.read(() => {\n    // ...\n  });\n\n  // Then schedule another update.\n  editor.update(() => {\n    // ...\n  });\n});\n```\n\nThe problem with this pattern is that it means we end up doing two DOM updates, when we likely could have\ndone it in a single DOM update. This can have an impact on performance, which is important in a text editor.\n\nA listener that re-enqueues work without a stop condition is worse than slow.\nThe editor keeps a cascade budget: once a run of updates re-enqueues more\nthan 100 times without yielding to the event loop, it discards the entire\npending update queue and warns. Bounded activity never reaches the limit —\nthe budget is reset from a macrotask, which a genuine runaway starves — but a\ntrue loop silently loses whatever else was queued.\n\nTo avoid this, we recommend looking into [Node Transforms](transforms.md), which allow you to listen to node changes and\ntransform them as part of the same given update, meaning no waterfalls!\n\n## `registerTextContentListener`\n\nGet notified when Lexical commits an update to the DOM and the text content of the editor changes from\nthe previous state of the editor. If the text content is the same between updates, no notifications to\nthe listeners will happen.\n\n```js\nconst removeTextContentListener = editor.registerTextContentListener(\n  (textContent) => {\n    // The latest text content of the editor!\n    console.log(textContent);\n  },\n);\n\n// Do not forget to unregister the listener when no longer needed!\nremoveTextContentListener();\n```\n\n## `registerMutationListener`\n\nGet notified when a specific type of Lexical node has been mutated. There are three states of mutation:\n\n- `created`\n- `destroyed`\n- `updated`\n\nMutation listeners are great for tracking the lifecycle of specific types of node. They can be used to\nhandle external UI state and UI features relating to specific types of node.\n\nIf any existing nodes are in the DOM, and skipInitialization is not true, the listener\nwill be called immediately with an updateTag of 'registerMutationListener' where all\nnodes have the 'created' NodeMutation. This can be controlled with the skipInitialization option,\nwhich defaults to false.\n\n```js\nconst removeMutationListener = editor.registerMutationListener(\n  MyCustomNode,\n  (mutatedNodes, { updateTags, dirtyLeaves, prevEditorState }) => {\n    // mutatedNodes is a Map where each key is the NodeKey, and the value is the state of mutation.\n    for (let [nodeKey, mutation] of mutatedNodes) {\n      console.log(nodeKey, mutation)\n    }\n  },\n  {skipInitialization: false}\n);\n\n// Do not forget to unregister the listener when no longer needed!\nremoveMutationListener();\n```\n\n## `registerEditableListener`\n\nGet notified when the editor's mode has changed. The editor's mode can be changed\nvia `editor.setEditable(boolean)`.\n\n```js\nconst removeEditableListener = editor.registerEditableListener(\n  (editable) => {\n    // The editor's mode is passed in!\n    console.log(editable);\n  },\n);\n\n// Do not forget to unregister the listener when no longer needed!\nremoveEditableListener();\n```\n\n## `registerDecoratorListener`\n\nGet notified when the editor's decorator object changes. The decorator object contains\nall `DecoratorNode` keys -> their decorated value. This is primarily used with external\nUI frameworks.\n\n```js\nconst removeDecoratorListener = editor.registerDecoratorListener(\n  (decorators) => {\n    // The editor's decorators object is passed in!\n    console.log(decorators);\n  },\n);\n\n// Do not forget to unregister the listener when no longer needed!\nremoveDecoratorListener();\n```\n\n## `registerRootListener`\n\nGet notified when the editor's root DOM element (the content editable Lexical attaches to) changes. This is primarily used to\nattach event listeners to the root element. *The root listener function is executed directly upon registration and then on any subsequent update.*\n\n```js\nconst removeRootListener = editor.registerRootListener(\n  (rootElement, prevRootElement) => {\n   //add listeners to the new root element\n   //remove listeners from the old root element\n  },\n);\n\n// Do not forget to unregister the listener when no longer needed!\nremoveRootListener();\n```\n\n<!-- File: packages/lexical-website/docs/concepts/named-slots.md (Tokens: ~4854 | Category: anthropic-skill) -->\n# Named Slots\n\n:::warning\n\nNamed slots are experimental. The APIs described here are tagged\n`@experimental`, may change without a major version bump, and the\nserialization and collab formats should be treated as unstable until the\nfeature is stabilized.\n\n:::\n\nNamed slots let a single host node (an ElementNode or a DecoratorNode) own\nseveral isolated editable regions addressed by name — a Card's `title`, a\nPullQuote's `quote` and `attribution` — inside the host's own EditorState.\nEach region takes its own caret and formatting, never merges across the\nboundary, and `Cmd+A` doesn't spill into the rest of the document.\n\nBefore named slots, the usual answer was a nested editor per region: every\nregion lives in its own EditorState, so moving nodes between regions and\nkeeping history and collab in sync all require serialization and extra\n`editor.update` passes. The other node shapes don't fit either: plain\nElementNode children share one undivided linked list (Backspace at a region\nstart merges it into the previous region), and a DecoratorNode is atomic, so\nLexical can't own selection, collab, or serialization inside it. With named\nslots, editing a slot is just editing the one tree.\n\n:::note\n\nNamed slots are the model-level generalization of the rendering concept\nbehind [`$getDOMSlot`](/docs/api/modules/lexical#getdomslot) /\n`ElementDOMSlot` in the\n[DOM rendering](../serialization/dom-render.md) documentation. Every\nElementNode already has one _unnamed_ children channel, and `$getDOMSlot`\ncontrols where that channel's content attaches in the node's DOM. Named\nslots are additional, explicitly-named channels alongside it — symmetric in\nthat each renders into a controllable location in the host's DOM — but they\nalso carry the model-level semantics the unnamed channel doesn't: isolation\n(the virtual shadow root below), a separate NodeKey map, and their own\nserialization and collaboration.\n\n:::\n\n## The Model\n\nA host keeps a second child channel, a `Map` of slot name to NodeKey,\nseparate from its ordinary linked-list children. A slot value has its slot\nhost pointer set and `getParent() === null`, with exactly one of the two\nnon-null: `getParent()` stops at the slot boundary, and you climb out only\nthrough [`$getSlotHost()`](/docs/api/modules/lexical#getslothost).\n\nThe slot link itself acts as a **virtual invisible shadow root** between the\nhost and the value. Isolation is structural rather than a convention — an\naccidental boundary crossing surfaces as a thrown invariant, not silent\ncorruption.\n\nIn the DOM, each value renders synchronously into a keyless\n`<div data-lexical-slot=\"<name>\">` container parked slots-first in the host\nDOM as a **hidden placeholder** (`display: none`), in a canonical order\nderived from the host class (see [slot order](#slot-order)). Nothing is\nvisible until the host explicitly attaches the container somewhere (see\n[Rendering](#rendering)) — mirroring how\n[`getDOMSlot`](/docs/api/modules/lexical#getdomslot) gives an element\ncontrol over where its linked-list children render.\n\n## Hosts and Values\n\nAny ElementNode or DecoratorNode can host slots. An ElementNode host can mix\nslots with ordinary children (a Card with a `title` slot plus regular body\nparagraphs); a DecoratorNode host has no children channel, so slots are its\nonly editable content (a PullQuote that is otherwise atomic).\n\nA slot value is any non-inline ElementNode or DecoratorNode. The value's\nshape decides the editing model:\n\n- **A bare block** (for example a ParagraphNode) behaves as a single-line\n  field with no extra wrapper in the model, the JSON, or the DOM: its virtual\n  scope holds exactly one block, so Enter is a no-op (hosts may map it to\n  focus movement) and a multi-block paste flattens to inline content the way\n  an `<input>` sanitizes its value — line breaks stripped, block-only\n  decorators dropped.\n- **A shadow-root container** (an ElementNode whose\n  [`isShadowRoot()`](/docs/api/modules/lexical#isshadowroot) returns\n  `true`) behaves as a multi-block region with normal block editing inside.\n\n## Declaring and Setting Slots\n\nA host class declares its slot names in [`$config()`](nodes.mdx); values are\nattached with [`$setSlot`](/docs/api/modules/lexical#setslot):\n\n```ts\nimport {\n  $create,\n  $createParagraphNode,\n  $setSlot,\n  ElementNode,\n} from 'lexical';\n\nclass CardNode extends ElementNode {\n  $config() {\n    return this.config('card', {extends: ElementNode, slots: ['title']});\n  }\n  createDOM(): HTMLElement {\n    return document.createElement('div');\n  }\n  updateDOM(): boolean {\n    return false;\n  }\n}\n\nfunction $createCardNode(): CardNode {\n  const card = $create(CardNode);\n  // Single-line title: the bare paragraph IS the slot value. An empty\n  // paragraph is an empty field; to seed default text, append a non-empty\n  // TextNode (empty TextNodes are eliminated during reconciliation).\n  $setSlot(card, 'title', $createParagraphNode());\n  // Ordinary body child, edited like any other block.\n  return card.append($createParagraphNode());\n}\n```\n\nFor a multi-block region, use a shadow-root container as the value:\n\n```ts\nclass SlotContainerNode extends ElementNode {\n  $config() {\n    return this.config('slot-container', {extends: ElementNode});\n  }\n  createDOM(): HTMLElement {\n    return document.createElement('div');\n  }\n  updateDOM(): boolean {\n    return false;\n  }\n  isShadowRoot(): boolean {\n    return true;\n  }\n}\n\n$setSlot(\n  pullQuote,\n  'quote',\n  $create(SlotContainerNode).append(\n    $createParagraphNode().append($createTextNode('First block')),\n    $createParagraphNode().append($createTextNode('Second block')),\n  ),\n);\n```\n\nThe core API surface (all exported from `lexical`):\n\n- [`$setSlot(host, name, node)`](/docs/api/modules/lexical#setslot) — place a value into a named slot, replacing\n  any existing value under that name. Move semantics, mirroring `append`:\n  the value is detached from any current parent or slot first. Throws if the\n  value is inline or if the name is one of the reserved prototype keys\n  (`__proto__`, `constructor`, `prototype`). Slotting a node into its own\n  descendant would form a cycle; this is checked (and throws) in development\n  only — in production it behaves like appending an ancestor through the\n  children channel, which is likewise unguarded.\n- [`$getSlot(host, name)`](/docs/api/modules/lexical#getslot) — the value under a name, or `null`.\n- [`$getSlotNames(host)`](/docs/api/modules/lexical#getslotnames) — the host's slot names in canonical order.\n- [`$removeSlot(host, name)`](/docs/api/modules/lexical#removeslot) — detach the value under a name (the subtree is\n  garbage-collected unless you reattach it elsewhere).\n- [`$getSlotHost(node)`](/docs/api/modules/lexical#getslothost) — the host a value is slotted into, or `null`.\n- [`$getSlotNameWithinHost(node)`](/docs/api/modules/lexical#getslotnamewithinhost) — the name of the slot\n  a value occupies on its host (the reverse direction of `$getSlotHost`), or\n  `null` when the node is not a slot value.\n- [`$getSlotFrame(node)`](/docs/api/modules/lexical#getslotframe) — the innermost slot value containing a node (the\n  \"frame\" whose virtual shadow root scopes editing), or `null` when the node\n  is not inside any slot.\n- [`$getSelectionSlotFrame(selection)`](/docs/api/modules/lexical#getselectionslotframe) — the slot frame a\n  selection lives in, or `null` when it is outside any slot. Selection-driven\n  exporters walk this frame instead of the root's children, because a\n  selection inside a slot never contains its host and a root walk would\n  export nothing. Works for every selection type, not just `RangeSelection`.\n- [`$isSlotHost(node)`](/docs/api/modules/lexical#isslothost) /\n  [`$isSlotChild(node)`](/docs/api/modules/lexical#isslotchild) — type guards\n  for the [`SlotHostNode`](/docs/api/modules/lexical#slothostnode) /\n  [`SlotChildNode`](/docs/api/modules/lexical#slotchildnode) interfaces.\n\n## Slot Order\n\nSlot order is canonical and derived, never stored. Names declared in\n`$config()` (`slots: ['quote', 'attribution']`) sort first in declaration\norder; undeclared names sort after them, lexicographically by UTF-16 code\nunit (plain JavaScript string comparison, locale-independent). `$setSlot`\nre-canonicalizes on every write, so documents normalize on load and\nconcurrent collaborative additions converge to the same order on every\nclient. If presentation order matters, declare the names.\n\n## Rendering\n\nThe reconciler always renders every slot subtree synchronously, but into a\nhidden placeholder container — visibility is the host's explicit decision.\nThere are three ways to attach a slot, all sharing the same contract\n(attaching moves the container to the target, a no-op when it is already\nthere, and reveals it; the container renders as a normal block):\n\n1. **Synchronously in-lexical**: register a `$getSlotTargetElement`\n   [`DOMRenderMatch`](/docs/api/modules/lexical_html#domrendermatch)\n   override for the host's node class (a\n   [DOM render override](../serialization/dom-render.md), not a node\n   method — this is an advanced hook). The reconciler consults it whenever\n   it creates or reconciles the slot's container and attaches/reveals\n   within the same commit — no listener or framework hop. Returning\n   `hostDom` reveals the slot in its default slots-first position:\n\n   ```ts\n   import {domOverride, DOMRenderExtension} from '@lexical/html';\n   import {configExtension, defineExtension} from 'lexical';\n\n   export const CardExtension = defineExtension({\n     dependencies: [\n       configExtension(DOMRenderExtension, {\n         overrides: [\n           domOverride([CardNode], {\n             // Reveal the title in its default slots-first position within\n             // the same commit that renders it. Returning an element from\n             // deeper inside the host's DOM attaches it there instead;\n             // $next() defers to lower-priority overrides (default: null,\n             // a hidden placeholder).\n             $getSlotTargetElement: (node, slotName, hostDom, $next, editor) =>\n               hostDom,\n           }),\n         ],\n       }),\n     ],\n     name: 'card',\n     nodes: [CardNode],\n   });\n   ```\n\n2. **Imperatively**:\n   [`mountSlotContainer(editor, nodeKey, slotName, target)`](/docs/api/modules/lexical#mountslotcontainer)\n   and\n   [`unmountSlotContainer(editor, nodeKey, container)`](/docs/api/modules/lexical#unmountslotcontainer)\n   from `lexical` are the framework-independent primitives (e.g. from a\n   [mutation listener](/docs/api/modules/lexical#registermutationlistener),\n   which fires after the commit). `mountSlotContainer` resolves the container\n   against the committed editor state\n   ([`editor.getEditorState()`](/docs/api/modules/lexical#geteditorstate)), so\n   the model it reads matches the reconciled DOM it reveals;\n   `unmountSlotContainer` takes the container you already hold and only touches\n   the DOM:\n\n   ```ts\n   import {mountSlotContainer} from 'lexical';\n\n   // e.g. inside an extension's register(editor):\n   const unregister = editor.registerMutationListener(\n     CardNode,\n     (mutations) => {\n       for (const [nodeKey, mutation] of mutations) {\n         if (mutation === 'destroyed') {\n           continue;\n         }\n         const hostDom = editor.getElementByKey(nodeKey);\n         if (hostDom !== null) {\n           // Mounting in place: the placeholder is already parked in the\n           // host DOM, so this just reveals it in its slots-first position.\n           // Any element within the host's DOM works as a target.\n           mountSlotContainer(editor, nodeKey, 'title', hostDom);\n         }\n       }\n     },\n     {skipInitialization: false},\n   );\n   ```\n\n   `unmountSlotContainer(editor, nodeKey, container)` is the reverse: it\n   hides the container and parks it back in the host DOM, for when a mount\n   target goes away while the host remains.\n\n3. **From React chrome**: the\n   [`useLexicalSlotRef`](/docs/api/modules/lexical_react_useLexicalSlotRef#uselexicalslotref)\n   hook from `@lexical/react/useLexicalSlotRef` wraps the imperative pair and returns a\n   ref that mounts a slot's container into your component — the usual choice\n   for a DecoratorNode host's `decorate()` chrome (whose containers are\n   opted back into `contentEditable` automatically, since the decorator DOM\n   is non-editable):\n\n```tsx\nimport {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';\nimport {useLexicalSlotRef} from '@lexical/react/useLexicalSlotRef';\n\nfunction PullQuoteComponent({nodeKey}: {nodeKey: NodeKey}) {\n  const [editor] = useLexicalComposerContext();\n  const quoteRef = useLexicalSlotRef<HTMLDivElement>(editor, nodeKey, 'quote');\n  const attributionRef = useLexicalSlotRef<HTMLDivElement>(\n    editor,\n    nodeKey,\n    'attribution',\n  );\n  return (\n    <blockquote>\n      <div ref={quoteRef} />\n      <div ref={attributionRef} />\n    </blockquote>\n  );\n}\n```\n\nA `contentEditable=false` ElementNode shell can host React chrome the same\nway (slot containers opt into editing whenever the host DOM is\nnon-editable): the playground's Review demo portals chrome into the host DOM,\nattaches the author slot with `useLexicalSlotRef`, drives an interactive\nstar-rating widget that persists to NodeState, and applies the identical\nhidden-then-attach technique to its `getDOMSlot` children element. Such a shell should call\n[`setDOMUnmanaged(dom)`](/docs/api/modules/lexical#setdomunmanaged) in\n`createDOM` — the portal and the\nattach moves mutate the shell's children from outside the reconciler, and\nthe marker gives the shell the same mutation-observer exemption a\nDecoratorNode's DOM has.\n\n## Editable state\n\nA slot rendered inside a non-editable host (a DecoratorNode, or a\n`contentEditable=false` element shell) would not track the editor's editable\nstate on its own, so the reconciler gives its container an explicit\n`contentEditable` that follows `editor.isEditable()`, and re-renders the islands\nwhenever `setEditable` toggles — so a read-only editor's slots are not left\neditable. No extension is required. Slots always follow the editor; there is\ncurrently no way for a slot to override its own editable state.\n\nA host that attaches its own editable island which is *not* a slot container —\nfor example the Review demo's `getDOMSlot` children element inside its\n`contentEditable=false` shell — opts it into the same behavior with\n[`$markSlotEditable(element, editor)`](/docs/api/modules/lexical#marksloteditable),\nre-applying it from `updateDOM` so an editable toggle reaches the island.\n\n## Editing Behavior\n\n- **Selection never crosses a slot boundary.** Selections are clamped to the\n  anchor's slot frame at every entry point (DOM resolution,\n  [`$setSelection`](/docs/api/modules/lexical#setselection),\n  and point mutation), so a mouse drag and a `shift+arrow` across the\n  boundary land on the same clamped result. A focus in a slot of a host is\n  moved past the whole host, or to the edge of the host's own children when\n  the selection starts inside the host.\n- **Deletion stops at the boundary.** Backspace at the start of a slot and\n  forward-Delete at its end are no-ops instead of merging across the virtual\n  shadow root.\n- **`Cmd+A` scopes to the slot frame** when the caret is inside one; outside\n  slots the default handlers keep the legacy whole-document behavior.\n  Progressive expansion (block → enclosing slot frame → document on repeated\n  presses) is provided by the opt-in\n  [`SelectBlockExtension`](/docs/api/modules/lexical_extension#selectblockextension)\n  from `@lexical/extension`.\n- **Generic block conversion skips slot values.** A slot value has no\n  parent — its up-link is `$getSlotHost` — so selection-driven block\n  converters (`$setBlocksType`, `$wrapNodes`, `$insertList` / `$removeList`,\n  the markdown block shortcuts) treat it as ineligible and do nothing rather\n  than replace it: the slot assignment is managed by the node or extension\n  that owns the slot. Calling `LexicalNode.replace` directly on a slot value\n  throws; reassign the slot with `$setSlot` on its host instead.\n- **Slots stay bound to their host through `replace`.** Replacing a slot\n  *host* (`host.replace(other)`) does not transfer its slots to the\n  replacement — slots are not necessarily portable across node types, so the\n  slot map travels with the node, never with its position. If the replaced\n  host is reattached in the same update (the\n  [`$wrapNodeInElement`](/docs/api/modules/lexical_utils#wrapnodeinelement)\n  pattern) it keeps its slots; if it stays detached, its slot subtrees are\n  garbage-collected with it. The same applies to anything built on `replace`:\n  converting a slot host with `$setBlocksType` produces a slot-less\n  replacement block. Move a slot onto another host explicitly with\n  `$setSlot`.\n- **A NodeSelection of an element carries its children.** Copy and export\n  of a whole-host NodeSelection (e.g. a chrome click that selects \"the whole\n  Card\") include the host's body children even though they aren't in the\n  selection themselves — the old shell-only output made cut silently lossy.\n  This applies only to NodeSelection; a partial RangeSelection over the host\n  keeps per-child slicing.\n\n### Traversal is intentionally asymmetric\n\nContent reads include slot subtrees, slots-first:\n[`getTextContent()`](/docs/api/modules/lexical#gettextcontent),\n[`getAllTextNodes()`](/docs/api/modules/lexical#getalltextnodes),\nand the [`$dfsWithSlots`](/docs/api/modules/lexical_utils#dfswithslots)\nfamily in `@lexical/utils` count slot content for search, copy, and\naccessibility. Navigation excludes them:\n[`getChildren()`](/docs/api/modules/lexical#getchildren),\n[`getFirstDescendant()`](/docs/api/modules/lexical#getfirstdescendant)\nand friends stay linked-list-only, so caret movement never walks into a slot\nby accident. Choose [`$dfs`](/docs/api/modules/lexical_utils#dfs) or\n[`$dfsWithSlots`](/docs/api/modules/lexical_utils#dfswithslots) depending on whether \"this subtree\" should mean the\nnavigable tree or all content.\n\n## Serialization\n\nJSON serialization is automatic in both directions. A host's slots serialize\nunder the reserved `$slots` key on\n[`SerializedLexicalNode`](/docs/api/modules/lexical#serializedlexicalnode)\n(a sibling of\nNodeState's reserved `'$'` key), keyed by slot name:\n\n```json\n{\n  \"type\": \"card\",\n  \"version\": 1,\n  \"$slots\": {\n    \"title\": {\"type\": \"paragraph\", \"children\": [], \"version\": 1}\n  },\n  \"children\": []\n}\n```\n\nParsing re-attaches each subtree with `$setSlot` and throws if `$slots`\nappears on a node that cannot host slots. The `$` prefix keeps the\nframework-owned key from colliding with a subclass that already serializes a\n`slots` property of its own.\n\nHTML serialization is opt-in per host, like NodeState: the exporter never\ndescends into slots on its own. A host's `exportDOM` can emit each slot into\na wrapper using\n[`$appendNodeToHTML`](/docs/api/modules/lexical_html#appendnodetohtml) from\n`@lexical/html`, and a\n[DOM import rule](../serialization/dom-import.md) on the host's\ndistinguishing markup maps the wrappers back through `$setSlot`.\n\n## Collaboration\n\nSlots sync on both the V1 and V2 Yjs bindings through a per-slot-diffed\n`Y.Map` stored under the reserved `__slots` attribute key on the host's shared\ntype (the channel reuses the host's `__slots` field name, which is already\nexcluded from the property sync). Hosts with declared slots create that map\neagerly, so two clients\nconcurrently setting *different* slot names for the first time merge\nper-entry instead of racing. Malformed or hostile remote entries are\nvalidated and skipped.\n\n:::caution\n\nPre-slots clients receiving slot data from upgraded peers will error rather\nthan render it. Enabling slots in long-lived shared documents should be\ngated on all participants running a slots-aware version; new clients\ntolerate unknown slot data going forward.\n\n:::\n\n## Reserved Names\n\nAdding slots reserves a few identifiers that custom node subclasses should\nnot define for their own purposes:\n\n- the `__slots` and `__slotHost` fields on ElementNode / DecoratorNode — the\n  `__slots` name is also the collab attribute key for the slots channel, so it\n  is reserved on the Yjs shared type too;\n- the `$slots` serialized JSON key.\n\n## Current Limitations\n\n- The caret / NodeCaret APIs throw across a slot boundary (\"no common\n  ancestor\"); slot-aware caret traversal is a planned follow-up.\n- Nested slots (a slot whose host is itself slotted) are only handled one\n  level deep by the runtime selection comparator.\n- The mixed-version collab caveat above.\n\n<!-- File: packages/lexical-website/docs/concepts/node-cloning.md (Tokens: ~1768 | Category: anthropic-skill) -->\n# Node Cloning\n\nUnderstanding how to properly clone and modify nodes is crucial for working with Lexical. This guide explains the different cloning mechanisms and when to use each one.\n\n## Types of Node Cloning\n\nLexical provides several ways to clone nodes, each serving a different purpose:\n\n1. `static clone()` - Internal API for state management\n2. `$copyNode()` - Public API for creating new nodes\n3. `getWritable()` - Public API for node modifications\n\n## The `clone` Method\n\n### What is `clone`?\n\nThe `clone` method is a static method that creates the next version of a node. A node that declares [`$config()`](./nodes.mdx) has one synthesized for it and need not implement it — a hand-written `clone` is only required for a node that does not use `$config()`, or whose constructor has required arguments. Either way, it's important to understand that this is an **internal API** used by Lexical's state management system.\n\n```typescript\nclass MyCustomNode extends ElementNode {\n  static clone(node: MyCustomNode): MyCustomNode {\n    // ✅ Correct implementation\n    return new MyCustomNode(node.__someData, node.__key);\n  }\n}\n```\n\n### When is `clone` Used?\n\n1. **Internal State Management**\n   - Used by Lexical internally\n   - Part of the editor's state update system\n   - Called by `getWritable()`\n\n2. **Node Mutations**\n   ```typescript\n   // Example of a node method implementation\n   class MyCustomNode extends ElementNode {\n     setData(data: string): this {\n       const self = this.getWritable();\n       self.__data = data;\n       return self;\n     }\n   }\n\n   node.setData(\"new data\");\n   ```\n\n### When NOT to Use `clone`?\n\n```typescript\n// ❌ Never do this\nfunction $duplicateNode(node: MyCustomNode) {\n  return MyCustomNode.clone(node); // Don't call clone directly\n}\n```\n\n## The `afterCloneFrom` Method\n\n### What is `afterCloneFrom`?\n\n`afterCloneFrom(prevNode)` runs on every clone, right after it is constructed,\nand copies over whatever the constructor did not. The base implementation\ncarries the node's links to its parent and siblings and its\n[NodeState](./node-state.md), and `ElementNode` and `TextNode` carry their own\nbuilt-in properties such as format, style, and indent. A property that is\ncopied neither by the constructor nor by some `afterCloneFrom` silently\nreverts to its default on the node's next write.\n\n### When you don't need it\n\nWith the modern APIs you usually don't write one at all:\n\n- **Serialization schema.** A property declared as a field in a `$config`\n  [`json` schema](./nodes.mdx#creating-custom-nodes-with-a-serialization-schema)\n  (`withField(stringValue(), {field: '__label'})`) is carried across the\n  clone for you, because the schema says where it is stored. Available in\n  Lexical v0.51.0 and later.\n- **NodeState.** State created with `createState` and stored with `$setState`\n  is carried by the base `afterCloneFrom`, so a node whose extra data lives\n  only in NodeState needs nothing.\n\n```ts\n// At module scope, above the class, so TypeScript checks it against the class\nconst calloutSchema = nodeSchema<CalloutNode>()({\n  label: withField(stringValue(), {field: '__label'}),\n});\n\nclass CalloutNode extends ElementNode {\n  __label: string = '';\n\n  // No afterCloneFrom: `__label` is declared as a field in calloutSchema, so\n  // it is carried across every clone.\n  $config() {\n    return this.config('callout', {extends: ElementNode, json: calloutSchema});\n  }\n}\n```\n\n### When you still need it\n\nOverride `afterCloneFrom` for a property that neither of those covers: a\nfield that isn't in the node's schema, one declared only through accessor\nmethods (see\n[Carrying properties across a clone](../serialization/serialization.md#carrying-properties-across-a-clone)),\nor any property of a node written without a schema. Always call\n`super.afterCloneFrom(prevNode)` first so the inherited properties and\nNodeState are still carried:\n\n```ts\nclass MyCustomNode extends ElementNode {\n  __data: string = '';\n\n  afterCloneFrom(prevNode: this): void {\n    super.afterCloneFrom(prevNode);\n    this.__data = prevNode.__data;\n  }\n}\n```\n\nLike `clone`, `afterCloneFrom` is called by Lexical; don't call it yourself\nexcept through `super`.\n\n## Using `$copyNode`\n\n### What is `$copyNode`?\n\n`$copyNode` is the public API for creating a copy of a node with a new key. Use this when you need to create a duplicate node.\nBy default, all properties and `NodeState` will be copied to the new node, and then `resetOnCopyNodeFrom` will be called to\nallow the node to optionally reset certain properties (and NodeState configured with `resetOnCopyNode: true`) to defaults\n(such as the checked state of a `ListItemNode`).\n\n```typescript\n// ✅ Correct: Using $copyNode\nconst copy = $copyNode(existingNode);\n```\n\n### When to Use `$copyNode`?\n\n1. **Creating Duplicates**\n   ```typescript\n   // ✅ Correct: Duplicating a node\n   const duplicate = $copyNode(originalNode);\n   someParent.append(duplicate);\n   ```\n\n## Using `getWritable`\n\n### What is `getWritable`?\n\n`getWritable` is an internal API used within node method implementations to get a mutable version of a node. Node consumers should use the node's public methods instead.\n\n```typescript\n// ✅ Correct: Implementation of a node method\nclass MyCustomNode extends ElementNode {\n  setData(data: string): this {\n    const self = this.getWritable();\n    self.__data = data;\n    return self;\n  }\n}\n\n// ✅ Correct: Using the node\nconst node = new MyCustomNode();\nnode.setData(\"new value\");\n```\n\n## Common Patterns\n\n### Modifying Nodes\n\n```typescript\n// ✅ Correct: Modifying a node\nfunction $updateNodeData(node: MyCustomNode, newData: string): MyCustomNode {\n  return node.setData(newData);\n}\n\n// ❌ Incorrect: Don't clone manually\nfunction $updateNodeDataWrong(node: MyCustomNode, newData: string): MyCustomNode {\n  const clone = MyCustomNode.clone(node);\n  clone.setData(newData);\n  return clone;\n}\n```\n\n### Copying Nodes\n\n```typescript\n// ✅ Correct: Copying a node\nfunction $duplicateNode(node: MyCustomNode): MyCustomNode {\n  return $copyNode(node);\n}\n\n// ❌ Incorrect: Don't use clone\nfunction $duplicateNodeWrong(node: MyCustomNode): MyCustomNode {\n  return node.constructor.clone(node);\n}\n```\n\n## Performance Considerations\n\n1. **Referential Integrity**\n   - `getWritable()` ensures proper EditorState updates with new clones\n   - Prevents \"orphaned\" nodes that won't be rendered\n\n2. **State Management**\n   ```typescript\n   class MyCustomNode extends ElementNode {\n     setData(data: string): this {\n       const self = this.getWritable();\n       self.__data = data; // Properly tracked by editor\n       return self;\n     }\n   }\n   ```\n\n## Testing\n\n```typescript\ntest('node modification', async () => {\n  await editor.update(() => {\n    const node = new MyCustomNode(\"test\");\n    \n    node.setData(\"new data\");\n    \n    // ✅ Correct: Use $copyNode for duplication\n    const copy = $copyNode(node);\n  });\n});\n```\n\n## Related Concepts\n\n- [Editor State](editor-state.md) - How cloning affects editor state\n- [Nodes](nodes.mdx) - Core concepts about Lexical nodes\n\n## Common Questions\n\n**Q: How do I duplicate a node?**\nA: Use `$copyNode(node)` to create a new copy with a new key.\n\n**Q: When should I use clone?**\nA: Never directly. Use `$copyNode()` or `getWritable()` instead.\n\n<!-- File: packages/lexical-website/docs/concepts/node-replacement.md (Tokens: ~699 | Category: anthropic-skill) -->\n# Node Replacement\n\nNode Replacement allow you to replace all instances of a given node in your editor with instances of a subclass.\n\n## Use Case\n\n:::note\n\nIn earlier versions of this documentation, \"Node Replacement\" was called \"Node Overrides\".\nWe've changed the name to match the terms used in the implementation.\n\n:::\n\n:::tip\n\nIf your use case only requires adding ad-hoc data to existing nodes, you may be able to use the [NodeState](node-state.md) API instead of subclassing and node replacement.\n\n:::\n\nSome of the most commonly used Lexical Nodes are owned and maintained by the core library. For example, ParagraphNode, HeadingNode, QuoteNode, List(Item)Node etc - these are all provided by Lexical packages, which provides an easier out-of-the-box experience for some editor features, but makes it difficult to override their behavior. For instance, if you wanted to change the behavior of ListNode, you would typically extend the class and override the methods. However, how would you tell Lexical to use *your* ListNode subclass in `ListExtension` instead of using the core ListNode? That's where Node Replacement can help.\n\n\nNode Replacement allow you to replace all instances of a given node in your editor with instances of a different node class. This can be done through the `nodes` of an extension:\n\n```ts\nconst CustomParagraphExtension = defineExtension({\n  name: '@my-app/CustomParagraph',\n  nodes: () => [\n    // Don't forget to register your custom node separately!\n    CustomParagraphNode,\n    {\n      replace: ParagraphNode,\n      with: (node: ParagraphNode) => {\n        return $createCustomParagraphNode();\n      },\n      withKlass: CustomParagraphNode,\n    },\n  ],\n});\n```\nIn the snippet above,\n- `replace`: Specifies the core node type to be replaced. \n- `with`: Defines a transformation function to replace instances of the original node to the custom node.  \n- `withKlass`: This option ensures that behaviors associated with the original node type work seamlessly with the replacement. For instance, node transforms or mutation listeners targeting ParagraphNode will also apply to CustomParagraphNode when withKlass is specified. Without this option, the custom node might not fully integrate with the editor's built-in features, leading to unexpected behavior.\n\nOnce this is done, Lexical will replace all ParagraphNode instances with CustomParagraphNode instances. One important use case for this feature is overriding the serialization behavior of core nodes. Check out the full example below.\n\n## Node Replacement Example\n\nThis example demonstrates using Node Replacement to replace all `ParagraphNode` with a `CustomParagraphNode` that overrides `createDOM`.\n\n[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/facebook/lexical/tree/main/examples/node-replacement?file=src/main.tsx)\n\n<iframe width=\"100%\" height=\"600\" src=\"https://stackblitz.com/github/facebook/lexical/tree/main/examples/node-replacement?embed=1&file=src%2FApp.tsx&terminalHeight=0&ctl=1&showSidebar=0&devtoolsheight=0&view=preview\" sandbox=\"allow-forms allow-modals allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin allow-scripts\" title=\"Node Replacement Example\"></iframe>\n\n<!-- File: packages/lexical-website/docs/concepts/node-state.md (Tokens: ~2790 | Category: anthropic-skill) -->\n# NodeState\n\nThe NodeState API introduced in v0.26.0 allows arbitrary state to be added\nad-hoc to any node in a way that participates with reconciliation, history,\nand JSON serialization.\n\n## Use Case\n\nNodeState allows your application to define keys that can be stored on\nany node with automatic JSON support.\n\n:::tip\n\nYou can even add node state to the RootNode to store document-level metadata.\n\n:::\n\nWith a combination of NodeState and other APIs such as\n[Listeners](listeners.md) or\n[Transforms](transforms.md) you can\nlikely shape the editor to meet your needs without having to do much\n[Node Customization](node-replacement.md).\n\nEven when you are subclassing nodes, using NodeState instead of additional\nproperties to store the node's data can be [more efficient](#efficiency)\nand will save you from writing a lot of boilerplate in the constructor,\nupdateFromJSON, and exportJSON.\n\n## Usage\n\n### createState\n\n[createState](/docs/api/modules/lexical#createstate)\ncreates a\n[StateConfig](/docs/api/modules/lexical#stateconfig)\nwhich defines the key and configuration for your NodeState value.\n\nThe key must be locally unique, two distinct StateConfig must not have the\nsame string key if they are to be used on the same node.\n\nTypical usage will look something like this:\n\n```ts\nconst questionState = createState('question', {\n  parse: (v) => (typeof v === 'string' ? v : ''),\n});\n```\n\nThe required `parse` function serves two purposes:\n- It provides a type-safe and runtime-safe way to parse values that were\n  serialized to JSON\n- When called with `undefined` (or any invalid value) it should return some\n  default value (which may be `undefined` or `null` or any other value you\n  choose)\n\nIn this case, the question must be a string, and the default is an\nempty string.\n\nSee the\n[createState](/docs/api/modules/lexical#createstate)\nAPI documentation for more details, there are other optional settings\nthat you may want to define particularly if the value is not a primitive\nvalue such as boolean, string, number, null, or undefined.\n\n:::tip\n\nInstead of writing parse functions by hand, use the\n[serialization schema combinators](/docs/serialization/#declarative-serialization-schemas-with-config)\nfrom `lexical`, such as `stringValue`, `numberValue`, `enumValue`,\n`arrayValue`, `objectValue`, and `nullable`. They are the same ones a node's\n`$config` JSON schema uses. A schema is itself a parse function, so it can be\npassed directly as `parse`:\n\n```ts\nimport {arrayValue, createState, stringValue} from 'lexical';\n\nconst questionState = createState('question', {parse: stringValue()});\nconst tagsState = createState('tags', {parse: arrayValue(stringValue())});\n```\n\nBecause `parse` is just a function, the combinators also compose with schema\nlibraries such as [zod](https://zod.dev/), [ArkType](https://arktype.io/),\n[Effect](https://effect.website/), and [Valibot](https://valibot.dev/). Wrap a\nlibrary's validator in a parse function that returns a default for invalid\ninput, or use `transformValue` to refine what a combinator parsed.\n\n:::\n\n### $getState\n\n[$getState](/docs/api/modules/lexical#getstate) gets the\nNodeState value from the given node, or the default if that key was never\nset on the node.\n\n```ts\nconst question = $getState(pollNode, questionState);\n```\n\nSee also\n[$getStateChange](/docs/api/modules/lexical#getstatechange)\nif you need an efficient way to determine if the state has changed on two\nversions of the same node (typically used in updateDOM, but may be useful in\nan update listener or mutation listener).\n\n### $setState\n\n[$setState](/docs/api/modules/lexical#setstate) sets the\nNodeState value on the given node.\n\n```ts\nconst question = $setState(\n  pollNode,\n  questionState,\n  'Are you planning to use NodeState?',\n);\n```\n\n:::tip\n\nThe last argument is a ValueOrUpdater, just like with React's useState\nsetters. If you use an updater function and the value does not change,\nthe node and its NodeState *won't* be marked dirty.\n\n:::\n\n## Serialization\n\nThe NodeState for a node, if any values are set to non-default values, is\nserialized to a record under a single\n[NODE_STATE_KEY](/docs/api/modules/lexical#node_state_key)\nwhich is equal to `'$'`.\n\n```json\n{\n  \"type\": \"poll\",\n  \"$\": {\n    \"question\": \"Are you planning to use NodeState?\"\n  }\n}\n```\n\n:::tip\n\nBy default, it is assumed that your parsed values are JSON serializable,\nbut for advanced use cases you may use values such as Date, Map, or Set\nthat need to be transformed before JSON serialization. See the\n[StateValueConfig](/docs/api/modules/lexical#statevalueconfig)\nAPI documentation.\n\n:::\n\n### Flat serialization with `$config`\n\nNodes that declare a `StateConfig` in [`$config`](nodes.mdx#creating-custom-nodes-with-config-and-nodestate)\nwith `flat: true` lift that key to the top level of the serialized node\ninstead of nesting it under `'$'`. This makes the JSON shape identical\nto a legacy node that stored the value as a `__property` instance\nvariable, so existing payloads continue to round-trip after the node is\nmigrated to NodeState.\n\nFor example, a `ColoredNode` that extends `TextNode` and declares a flat\n`color` state (see [Extending TextNode with `$config`](nodes.mdx#extending-textnode-with-config)):\n\n```ts\n$config() {\n  return this.config('colored', {\n    extends: TextNode,\n    stateConfigs: [{flat: true, stateConfig: colorState}],\n  });\n}\n```\n\nserializes as:\n\n```json\n{\n  \"type\": \"colored\",\n  \"text\": \"hello\",\n  \"color\": \"red\"\n}\n```\n\nA non-flat (default) state on the same node would instead appear under\n`'$'`:\n\n```json\n{\n  \"type\": \"colored\",\n  \"text\": \"hello\",\n  \"$\": {\n    \"color\": \"red\"\n  }\n}\n```\n\nIn both cases, a key is only emitted when the current value is not equal\nto the default returned by its `parse` function — see\n[Efficiency](#efficiency).\n\n:::note\n\nDon't reuse a flat key that a superclass already serializes (e.g. `text`\non `TextNode`).\n\n:::\n\n### Upgrading a legacy JSON property to NodeState\n\nA node that stores data as a `__property` instance variable with custom\n`exportJSON`/`importJSON`/`updateFromJSON` can be migrated to NodeState\nwith `flat: true` without changing its serialized JSON shape.\n\nBefore — `ColoredNode` with a `__color` property and hand-written\nserialization (see the full legacy example in\n[Extending TextNode with `$config`](nodes.mdx#extending-textnode-with-config)):\n\n```ts\nexport type SerializedColoredNode = Spread<\n  {color?: string},\n  SerializedTextNode\n>;\n\nexport class ColoredNode extends TextNode {\n  __color: string;\n\n  constructor(text: string = '', color: string = DEFAULT_COLOR, key?: NodeKey) {\n    super(text, key);\n    this.__color = color;\n  }\n\n  static getType(): string {\n    return 'colored';\n  }\n\n  static clone(node: ColoredNode): ColoredNode {\n    return new ColoredNode(node.__text, node.__color, node.__key);\n  }\n\n  static importJSON(serializedNode: SerializedColoredNode) {\n    return new ColoredNode().updateFromJSON(serializedNode);\n  }\n\n  updateFromJSON(serializedNode: SerializedColoredNode) {\n    const self = super.updateFromJSON(serializedNode);\n    self.__color =\n      typeof serializedNode.color === 'string'\n        ? serializedNode.color\n        : DEFAULT_COLOR;\n    return self;\n  }\n\n  exportJSON(): SerializedColoredNode {\n    return {\n      ...super.exportJSON(),\n      color: this.__color === DEFAULT_COLOR ? undefined : this.__color,\n    };\n  }\n}\n```\n\nAfter — same on-the-wire JSON, no hand-written serialization:\n\n```ts\nconst colorState = createState('color', {\n  parse: (v) => (typeof v === 'string' ? v : DEFAULT_COLOR),\n});\n\nexport class ColoredNode extends TextNode {\n  $config() {\n    return this.config('colored', {\n      extends: TextNode,\n      stateConfigs: [{flat: true, stateConfig: colorState}],\n    });\n  }\n}\n```\n\nNo `exportJSON`, `importJSON`, `updateFromJSON`, `clone`, or\n`afterCloneFrom` override is needed: `$config` installs `clone` and\n`importJSON`, and the base `exportJSON`/`updateFromJSON`/`afterCloneFrom`\non `LexicalNode`/`TextNode` already round-trip NodeState. Read the color\nvia `$getState(node, colorState)` and write it via\n`$setState(node, colorState, value)` instead of `node.__color`.\n\n:::tip\n\nFor a seamless migration, keep the JSON key name the same — for a flat\nstate, that's the first argument to `createState`. Migrating from a\nnon-flat NodeState to a flat NodeState also works: state that appears\nunder the `$` (`NODE_STATE_KEY`) is still parsed even when the state is\nconfigured as flat, and the flat value takes precedence when both are\npresent.\n\n:::\n\n## Efficiency\n\nNodeState uses a copy-on-write scheme to manage each node's state. If\nnone of the state has changed, then the NodeState instance will be\nshared across multiple versions of that node.\n\n:::info\n\nIn a given reconciliation cycle, the first time a Lexical node is marked dirty\nvia `getWritable` will create a new instance of that node. All properties\nof the previous version are set on the new instance. NodeState is stored\nas a single property, and no copying of the internal state is done\nuntil the NodeState itself is marked writable.\n\n:::\n\nWhen serializing to JSON, each key will only be stored if the value\nis not equal to the default value. This can save quite a lot of space\nand bandwidth.\n\nParsing and serialization is only done at network boundaries, when\nintegrating with JSON or Yjs. When a value changes from an external\nsource, it is only parsed once the first time it is read.\nValues that do not come from external sources are not parsed, and\nvalues that are not used are never parsed.\n\n## Capabilities\n\nCurrent:\n\n- Allows you to define and add state to any node\n- Serializes that state automatically in the node's JSON, supporting\n  versioning and copy+paste\n- Works with the reconciler, TextNodes with differing state will not\n  be implicitly merged\n- @lexical/yjs support, NodeState will be automatically synchronized\n  like any other property\n- NodeState values that are not used will simply pass-through, making\n  it a bit easier for situations where multiple configurations are used\n  on the same data (e.g. older and newer versions of your editor,\n  a different set of plugins based on context, etc.).\n- Pre-registration system for nodes to declare expected state\n  and serialize them as top-level properties (`flat`) with `$config`\n  (see [#7260](https://github.com/facebook/lexical/issues/7260)).\n- Can be integrated with\n  [DOMRenderExtension](../serialization/dom-render.md) for editor\n  rendering and HTML export\n- Can be integrated with\n  [DOMImportExtension](../serialization/dom-import.md) for HTML import\n\nFuture:\n\n- Does not yet support direct integration with Yjs, e.g.\n  you can not store a Y.Map as a NodeState value\n  (see [#7293](https://github.com/facebook/lexical/issues/7293))\n\n## Node State Style Example\n\nThis example demonstrates an advanced use case of storing a style object on TextNode using NodeState.\n\n[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/facebook/lexical/tree/main/examples/node-state-style?file=src/main.tsx)\n\n<iframe width=\"100%\" height=\"600\" src=\"https://stackblitz.com/github/facebook/lexical/tree/main/examples/node-state-style?embed=1&file=src%2FApp.tsx&terminalHeight=0&ctl=1&showSidebar=0&devtoolsheight=0&view=preview\" sandbox=\"allow-forms allow-modals allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin allow-scripts\" title=\"Node State Style Example\"></iframe>\n\n<!-- File: packages/lexical-website/docs/concepts/one-lexical-per-app.md (Tokens: ~2113 | Category: anthropic-skill) -->\n---\nsidebar_label: One Lexical per app\n---\n\n# One Lexical per app\n\n`lexical` is effectively a singleton **within one app**: an app must resolve\nexactly **one** copy of `lexical`, and one copy of each `@lexical/*` package it\nuses. Two copies in the same bundle do not merely waste bytes — they break the\neditor in ways whose error messages point nowhere near the real cause.\n\nThe practical consequence, covered in [Publishing a library](#publishing-a-library)\nbelow: **a library built on Lexical declares `lexical` and the `@lexical/*`\npackages it imports as `peerDependencies`, never as `dependencies`. The\napplication brings its own Lexical.**\n\n:::note\n\nThis page is about npm `peerDependencies` in your `package.json`. Extensions\nhave their own, unrelated notion of\n[peer dependencies](../extensions/peer-dependencies.md) — optional links between\nextensions inside one editor.\n\n:::\n\n## The boundary is the app, not the page\n\nSeveral self-contained apps can share a page, each with its own bundled copy of\nLexical, and that is fine — a CMS page with two independently deployed widgets\non it, or a micro-frontend setup, does not have a bug just because two copies of\n`lexical` are loaded. Lexical anticipates this: `isLexicalEditor()` is an\n`instanceof` check specifically \"to prevent issues with multiple embedded\nLexical installations\", and an editor is tagged on its own root element rather\nthan in any page-global registry, so each app's editors resolve to that app's\nLexical and ignore the others.\n\nWhat those apps cannot do is interoperate. Two copies have no compatible API\nsurface between them, so treat each as a sealed island:\n\n- do not pass nodes, editors, `EditorState` objects, selections or commands\n  across the boundary — pass serialized JSON or HTML, which is data rather than\n  identity;\n- a plugin, node package or shared editor config must be bundled with the app it\n  serves, not shared as a live import between apps;\n- `@lexical/react` context does not cross the boundary either, since the context\n  object itself comes from one copy.\n\nThe rest of this page is about the failure case: two copies **inside one app**,\nwhere code from copy A is expected to work with an editor from copy B.\n\n## Why one copy per app\n\nLexical keeps state at module scope, and identifies things by reference rather\nthan by name. Both assumptions hold only within a single copy of the module.\n\n- **The active editor is module state.** `activeEditor`, `activeEditorState`\n  and `isReadOnlyMode` live at module scope in `LexicalUpdates.ts`, and every\n  `$` function reads them. `editor.update()` in copy A sets copy A's variables;\n  a `$` function imported from copy B reads copy B's, which are still `null`\n  ([Editor State](editor-state.md)).\n- **Node classes are compared by identity.** Node registration and the\n  `instanceof LexicalNode` checks compare class references. A node that extends\n  `LexicalNode` from copy A is not, as far as copy B is concerned, a\n  `LexicalNode` at all — even though both classes have the same name and come\n  from the same published version ([Nodes](nodes.mdx)).\n- **Commands are object identities.** `createCommand()` returns a fresh `{type}`\n  object, and `registerCommand`/`dispatchCommand` match on that object, not on\n  its `type` string. Two copies of `lexical` export two distinct\n  `FORMAT_TEXT_COMMAND` objects, so a dispatch from one never reaches a listener\n  registered with the other ([Commands](commands.md)).\n\nOn top of that, two copies are usually two *versions*, which is API drift inside\na single editor: half your plugins were compiled against a different API and\nserialization format than the editor running them, and the mismatch surfaces\nonly at runtime, on the paths that happen to differ.\n\n## Symptoms of a duplicated Lexical\n\nThe errors are unhelpfully literal — a class that plainly does implement\n`getType()` is reported as not implementing it, because the check is running\nagainst the other copy:\n\n```\nLexicalNode: Node FooNode does not implement .getType().\n\nCreate node: Type heading in node HeadingNode does not match registered node\nHeadingNode with the same type\n\ncreateEditor: nodes[0] FooNode (type foo) is not a constructor that subclasses\nLexicalNode from the lexical package used by this editor (0.50.0)\n\nUnable to find an active editor state. State helpers or node methods can only be\nused synchronously during the callback of editor.update(), editor.read(), or\neditorState.read(). Detected on the page: 0 compatible editor(s) with version\n0.50.0 and incompatible editors with versions 0.49.0\n```\n\nThat last sentence is Lexical telling you directly that it found editors built\nfrom another copy — a version different from its own, or the same version marked\n`(separately built, likely a bundler configuration issue)`. Note that this\ndiagnostic scans the whole page, so a genuinely separate app that ships its own\nLexical is counted there too; it is evidence of a bug only when those editors\nwere supposed to be part of *your* app.\n\nSilent failures look like: a command that dispatches but never fires its\nlistener, `$getSelection()` returning `null` inside an update, or a custom node\nrendering as plain text after a copy/paste. Real reports of this failure mode:\n[#4225](https://github.com/facebook/lexical/issues/4225) and\n[#7819](https://github.com/facebook/lexical/issues/7819).\n\n## Publishing a library\n\nIf you publish a package that imports from `lexical` or `@lexical/*` — a plugin,\na node package, a shared editor config, an in-house wrapper in a monorepo — put\nthose packages in `peerDependencies`, and in `devDependencies` so you can still\nbuild and test:\n\n```json\n{\n  \"name\": \"my-lexical-plugin\",\n  \"peerDependencies\": {\n    \"lexical\": \">=0.50.0\",\n    \"@lexical/react\": \">=0.50.0\",\n    \"@lexical/utils\": \">=0.50.0\"\n  },\n  \"devDependencies\": {\n    \"lexical\": \"0.50.0\",\n    \"@lexical/react\": \"0.50.0\",\n    \"@lexical/utils\": \"0.50.0\"\n  }\n}\n```\n\nNotes on the ranges:\n\n- Lexical is pre-1.0, so `^0.50.0` means `>=0.50.0 <0.51.0` — a caret peer range\n  locks your users out of the next release. Prefer an open `>=` range (or an\n  explicit `>=0.50.0 <1.0.0`) so an app can upgrade Lexical without waiting for\n  you.\n- Mark a peer optional with\n  [`peerDependenciesMeta`](https://docs.npmjs.com/cli/configuring-npm/package-json#peerdependenciesmeta)\n  when the import is behind an opt-in entry point, the way this repo's own\n  packages mark `typescript` optional.\n- Do not add `lexical` to `dependencies` \"just so it installs\". That is exactly\n  what pins a second copy under `node_modules/your-lib/node_modules/lexical` as\n  soon as your range and the app's range disagree.\n\n## Application setup\n\nThe application owns the version, and every Lexical package it installs moves\ntogether:\n\n- Install `lexical` and each `@lexical/*` package you use **at the same\n  version**. Published `@lexical/*` packages depend on the exact matching\n  `lexical` version (the monorepo's `workspace:*` is rewritten to an exact\n  version at publish time), so `@lexical/react@0.50.0` beside `lexical@0.49.0`\n  guarantees a nested second copy.\n- Verify there is only one **per app**, from the app root:\n\n  ```sh\n  npm ls lexical @lexical/react     # or: pnpm why lexical / yarn why lexical\n  ```\n\n  More than one resolved version, or a path with a nested `node_modules/lexical`\n  in it, is the bug.\n- In a monorepo or a linked-package setup (`npm link`, `pnpm link`, a Yalc\n  copy), the linked library resolves Lexical from *its own* `node_modules` and\n  duplicates are the default rather than the exception. Deduplicate explicitly:\n  hoist the dependency to the workspace root, or force resolution with\n  [pnpm `overrides`](https://pnpm.io/settings#overrides), Yarn `resolutions`, or\n  `npm dedupe`.\n- Bundlers can also be told to collapse duplicates — Vite\n  [`resolve.dedupe`](https://vite.dev/config/shared-options.html#resolve-dedupe),\n  webpack `resolve.alias` pointing each package at one path:\n\n  ```js\n  // vite.config.js\n  export default {\n    resolve: {dedupe: ['lexical', '@lexical/react', '@lexical/utils']},\n  };\n  ```\n\n  Treat that as a safety net, not a fix: it collapses copies that are already\n  version-compatible, and cannot rescue two genuinely different versions.\n- Duplication does not always come from the dependency tree: importing Lexical\n  through both `import` and `require` pulls in the ESM and the CJS build of the\n  *same* version, which are still two copies. The\n  [React FAQ](../react/faq.md#lexicalcomposercontextuselexicalcomposercontext-cannot-find-a-lexicalcomposercontext)\n  covers how this one shows up in `@lexical/react`.\n\n<!-- File: packages/lexical-website/docs/concepts/read-only.md (Tokens: ~424 | Category: anthropic-skill) -->\n# Read Mode / Edit Mode\n\nLexical supports two modes:\n\n- Read mode\n- Edit mode\n\nThe default behavior for Lexical is edit mode, or more accurately not read only mode. Under-the-hood, the main\nimplementation detail is that the `contentEditable` is being set to `\"false\"` or `\"true\"` depending on the mode.\nSpecific plugins can listen to the mode change too – allowing them to customize parts of the UI depending on the\nmode.\n\n## Setting the mode\n\nIn order to set the mode, this can be done on creation of the editor, with the\n`editable` property of your app's extension:\n\n```js\nimport {buildEditorFromExtensions} from '@lexical/extension';\nimport {RichTextExtension} from '@lexical/rich-text';\nimport {defineExtension} from 'lexical';\n\nconst editor = buildEditorFromExtensions(\n  defineExtension({\n    dependencies: [RichTextExtension],\n    editable: false,\n    name: '@my-app/editor',\n  }),\n);\n```\n\nWith React, pass the same extension to `<LexicalExtensionComposer>`:\n\n```jsx\n<LexicalExtensionComposer extension={appExtension}>\n  ...\n</LexicalExtensionComposer>\n```\n\nAfter an editor is created, the mode can be changed imperatively:\n\n```js\neditor.setEditable(true);\n```\n\n## Reading the mode\n\nIn order to find the current mode of the editor you can use:\n\n```js\nconst isEditable = editor.isEditable(); // Returns true or false\n```\n\nYou can also get notified when the editor's read only mode has changed:\n\n```js\nconst removeEditableListener = editor.registerEditableListener(\n  (isEditable) => {\n    // The editor's mode is passed in!\n    console.log(isEditable);\n  },\n);\n\n// Do not forget to unregister the listener when no longer needed!\nremoveEditableListener();\n```\n\nIn an extension, depend on\n[`WatchEditableExtension`](/docs/api/modules/lexical_extension#watcheditableextension),\nwhose output is a signal holding the current editable state, instead of\nregistering the listener yourself.\n\n<!-- File: packages/lexical-website/docs/concepts/selection.md (Tokens: ~2513 | Category: anthropic-skill) -->\n# Selection\n\n## Types of selection\n\nLexical's selection is part of the `EditorState`. This means that for every update, or change to the editor, the\nselection always remains consistent with that of the `EditorState`'s node tree.\n\nIn Lexical, there are four types of selection possible:\n\n- `RangeSelection`\n- `NodeSelection`\n- `TableSelection` (implemented in `@lexical/table`)\n- `null`\n\nIt is possible, but not generally recommended, to implement your own selection types that implement `BaseSelection`.\n\n### `RangeSelection`\n\nThis is the most common type of selection, and is a normalization of the browser's DOM Selection and Range APIs.\n`RangeSelection` consists of three main properties:\n\n- `anchor` representing a `RangeSelection` point\n- `focus` representing a `RangeSelection` point\n- `format` numeric bitwise flag, representing any active text formats\n\nBoth the `anchor` and `focus` points refer to an object that represents a specific part of the editor. The main properties of a `RangeSelection` point are:\n\n- `key` representing the `NodeKey` of the selected Lexical node\n- `offset` representing the position from within its selected Lexical node. For the `text` type this is the character, and for the `element` type this is the child index from within the `ElementNode`\n- `type` representing either `element` or `text`.\n\n### `NodeSelection`\n\nNodeSelection represents a selection of multiple arbitrary nodes. For example, three images selected at the same time.\n\n- `getNodes()` returns an array containing the selected LexicalNodes\n\n### `TableSelection`\n\nTableSelection represents a grid-like selection like tables. It stores the key of the parent node where the selection takes place and the start and end points.\n`TableSelection` consists of three main properties:\n\n- `tableKey` representing the parent node key where the selection takes place\n- `anchor` representing a `TableSelection` point\n- `focus` representing a `TableSelection` point\n\nFor example, a table where you select row = 1 col = 1 to row 2 col = 2 could be stored as follows:\n- `tableKey = 2` table key\n- `anchor = 4` table cell (key may vary)\n- `focus = 10` table cell (key may vary)\n\nNote that `anchor` and `focus` points work the same way as `RangeSelection`.\n\n### `null`\n\nThis is for when the editor doesn't have any active selection. This is common for when the editor has been blurred or when selection\nhas moved to another editor on the page. This can also happen when trying to select non-editable components within the editor space.\n\n## Working with selection\n\nSelection can be found using the `$getSelection()` helper, exported from the `lexical` package. This function can be used within\nan update, a read, or a command listener.\n\n:::tip\n\nTo walk the document from a selection point — or to write a traversal that\nhandles empty nodes and collapsed selections correctly — see\n[Node Traversals with NodeCaret](./traversals.md).\n`$caretRangeFromSelection(selection)` converts a `RangeSelection` into the\ncaret range that traversal API works with, and\n`$setSelectionFromCaretRange(range)` converts back.\n\n:::\n\n```js\nimport {$getSelection, SELECTION_CHANGE_COMMAND} from 'lexical';\n\neditor.update(() => {\n  const selection = $getSelection();\n});\n\neditorState.read(() => {\n  const selection = $getSelection();\n});\n\n// SELECTION_CHANGE_COMMAND runs before reconciliation in the pending update.\neditor.registerCommand(SELECTION_CHANGE_COMMAND, () => {\n  const selection = $getSelection();\n});\n```\n\nIn some cases you might want to create a new type of selection and set the editor selection to\nbe that. This can only be done in update or command listeners.\n\n```js\nimport {$setSelection, $createRangeSelection, $createNodeSelection} from 'lexical';\n\neditor.update(() => {\n  // Set a range selection\n  const rangeSelection = $createRangeSelection();\n  $setSelection(rangeSelection);\n\n  // You can also indirectly create a range selection, by calling some of the selection\n  // methods on Lexical nodes.\n  const someNode = $getNodeByKey(someKey);\n\n  // On element nodes, this will create a RangeSelection with type \"element\",\n  // referencing an offset relating to the child within the element.\n  // On text nodes, this will create a RangeSelection with type \"text\",\n  // referencing the text character offset.\n  someNode.select();\n  someNode.selectPrevious();\n  someNode.selectNext();\n\n  // You can use this on any node.\n  someNode.selectStart();\n  someNode.selectEnd();\n\n  // Set a node selection\n  const nodeSelection = $createNodeSelection();\n  // Add a node key to the selection.\n  nodeSelection.add(someKey);\n  $setSelection(nodeSelection);\n\n  // You can also clear selection by setting it to `null`.\n  $setSelection(null);\n});\n```\n\n## Focus\n\nYou may notice that when you issue an `editor.update` or\n`editor.dispatchCommand` then the editor can \"steal focus\" if there is\na selection and the editor is editable. This is because the Lexical\nselection is reconciled to the DOM selection during reconciliation,\nand the browser's focus follows its DOM selection.\n\nIf you want to make updates or dispatch commands to the editor without\nchanging the selection, can use the `SKIP_DOM_SELECTION_TAG` update tag\n(added in v0.22.0):\n\n```js\n// Call this from an editor.update or command listener\n$addUpdateTag(SKIP_DOM_SELECTION_TAG);\n```\n\nIf you want to add this tag during processing of a `dispatchCommand`,\nyou can wrap it in an `editor.update`:\n\n```js\n// NOTE: If you are already in a command listener or editor.update,\n//       do *not* nest a second editor.update! Nested updates have\n//       confusing semantics (dispatchCommand will re-use the\n//       current update without nesting)\neditor.update(() => {\n  $addUpdateTag(SKIP_DOM_SELECTION_TAG);\n  editor.dispatchCommand(/* … */);\n});\n```\n\n`SKIP_DOM_SELECTION_TAG` does not apply to the initial editor state setup\n(the `$initialEditorState` property of the root extension, or\n`initialConfig.editorState` with the legacy `LexicalComposer`). On first mount the editor still scrolls\nto and focuses the initial selection. To prevent that, call\n`$setSelection(null)` inside your initial state setup function:\n\n```js\nconst editor = buildEditorFromExtensions({\n  name: '[root]',\n  // ...\n  $initialEditorState: () => {\n    // ... build your initial nodes ...\n    $setSelection(null);\n  },\n});\n```\n\nIf you have to support older versions of Lexical, you can mark the editor\nas not editable during the update or dispatch.\n\n```js\n// NOTE: This code should be *outside* of your update or command listener, e.g.\n//       directly in the DOM event listener\nconst prevEditable = editor.isEditable();\neditor.setEditable(false);\neditor.update(\n  () => {\n    // run your update code or editor.dispatchCommand in here\n  }, {\n    onUpdate: () => {\n      editor.setEditable(prevEditable);\n    },\n  },\n);\n```\n\n## Reading selection across a shadow boundary\n\nIf your plugin reads the DOM selection directly — through\n`Selection.anchorNode`, `Selection.getRangeAt(0)`, or similar — and the\neditor's `contentEditable` lives inside a `ShadowRoot`, the browser retargets\nthose reads to the shadow host. See\n[Shadow DOM and iframes](./shadow-dom.md) for the shadow-aware helpers\n(`getDOMSelectionPoints`, `getDOMSelectionRange`, etc.) that return the\nun-retargeted boundary points; they fall through to the standard reads in the\nplain light DOM, so there's nothing to do until you actually mount the editor\nin a shadow tree.\n\n### Selection change timing\n\n`SELECTION_CHANGE_COMMAND` runs **before reconciliation**, in a writable update,\nfor native, programmatic, and non-range selection changes. `$getSelection()`\nreturns the pending selection; `$getPreviousSelection()` returns the selection\nfrom the last committed state. A listener can normalize the selection or edit\nnodes before that same update commits. Listeners that change the selection may\ncause another notification before commit; they must converge.\n\nThis covers programmatic changes such as `node.select()`, which notify without\nwaiting for a DOM `selectionchange` event, and non-range selections such as\n`NodeSelection` and `TableSelection`, as well as native range selections.\n\nSelection listeners use the update's normal error handling. A throwing listener\nreports through `onError` and can abort the entire pending update, including the\nedit that triggered the notification and any other edits batched into that\ncommit, just like an explicit command dispatch inside that update. Because a\nprogrammatic selection change also notifies, a listener error can abort content\nedits made in the same update. Listener edits do not have a separate rollback\nscope.\n\nThe DOM is not guaranteed to match the pending state, even for a `NodeSelection`.\nNew nodes may not have an element yet, and DOM ranges, layout, and focus may\nstill reflect the previous state. Schedule DOM-dependent work after the update\nand read the reconciled state there:\n\n```ts\neditor.registerCommand(\n  SELECTION_CHANGE_COMMAND,\n  () => {\n    $onUpdate(() => {\n      editor.read('latest', () => {\n        // Read $getSelection(), look up elements, or position floating UI here.\n      });\n    });\n    return false;\n  },\n  COMMAND_PRIORITY_LOW,\n);\n```\n\nImport `$onUpdate` and `COMMAND_PRIORITY_LOW` from `lexical`. Read selection and\nnodes inside the callback rather than capturing mutable pending objects.\n`registerUpdateListener` is another option for UI that must respond to every\ncommit, including content changes that do not change the selection. Updates\nusing `SKIP_DOM_SELECTION_TAG` still deliberately leave the browser selection\nunsynchronized.\n\n#### Update tags and local edits\n\nThe notification belongs to the pending update and inherits its tags. A listener\ncan inspect them with `$hasUpdateTag`. In particular, Yjs does not sync content\nedits back to peers when the update is tagged `COLLABORATION_TAG` or\n`HISTORIC_TAG`; history also treats `HISTORIC_TAG` as replaying an existing entry.\nThis applies to content edits made by selection listeners in those updates.\n\nListeners that only apply to local editing should skip those tagged updates. If\na remote change or undo must trigger an independent local content edit, schedule\nit after the tagged commit with `$onUpdate`, then start a fresh `editor.update`:\n\n```ts\nconst $applyLocalEdit = () => {\n  // Read the current selection and nodes, then apply a convergent local edit.\n};\n\neditor.registerCommand(\n  SELECTION_CHANGE_COMMAND,\n  () => {\n    if ($hasUpdateTag(COLLABORATION_TAG) || $hasUpdateTag(HISTORIC_TAG)) {\n      $onUpdate(() => editor.update($applyLocalEdit));\n    } else {\n      $applyLocalEdit();\n    }\n    return false;\n  },\n  COMMAND_PRIORITY_LOW,\n);\n```\n\nImport the tags and `$hasUpdateTag` from `lexical`. Re-read the state in the fresh\nupdate; it has its own collaboration and undo behavior. Calling `editor.update`\ndirectly inside the command listener queues a nested update that still belongs\nto the same tagged commit.\n\nAutomatic range and cleared-selection notifications require a connected editor\nroot. Changes committed while those notifications are skipped advance the\nselection baseline and are not replayed when the root reconnects. Non-range\nselections continue to notify in unmounted and headless editors,\nwith the same pre-reconciliation timing. Native dirty-selection notifications\nmay fire even when the selection compares equal to the previous selection.\n\n<!-- File: packages/lexical-website/docs/concepts/shadow-dom.md (Tokens: ~4273 | Category: anthropic-skill) -->\n---\nsidebar_label: Shadow DOM and iframes\n---\n\n# Shadow DOM and iframes\n\nA Lexical editor works when its `contentEditable` lives inside an **open**\n[DOM `ShadowRoot`](https://developer.mozilla.org/docs/Web/API/ShadowRoot) (for\nexample inside a [web component](https://developer.mozilla.org/docs/Web/API/Web_components))\nor inside an `<iframe>` document. Both are supported out of the box — you only\nhave to point the editor at the right root element.\n\n## Embedding in a shadow root\n\nAttach an open shadow root, render a `contentEditable` inside it, and pass that\nelement to `editor.setRootElement` (or, with `@lexical/react`, portal the\n`ContentEditable` into the shadow root — React context flows across the portal,\nso the editor is built exactly as it would be in the light DOM):\n\n```js\nconst host = document.querySelector('#editor-host');\nconst shadow = host.attachShadow({mode: 'open'});\nconst contentEditable = document.createElement('div');\ncontentEditable.contentEditable = 'true';\nshadow.appendChild(contentEditable);\n\neditor.setRootElement(contentEditable);\n```\n\nLexical detects the enclosing shadow root from the root element and resolves\nselection through it. There is nothing else to configure.\n\n### Why it needs platform support\n\nInside a shadow tree the browser **retargets** the document's selection to the\nshadow host: `Selection.anchorNode`/`focusNode` and `Selection.getRangeAt`\nreport the host element, not the node the caret is actually in, and\n`document.activeElement` reports the host rather than the focused element.\nLexical works around this using only standard platform APIs:\n\n- Selection is read with\n  [`Selection.getComposedRanges`](https://developer.mozilla.org/docs/Web/API/Selection/getComposedRanges)\n  and [`Selection.direction`](https://developer.mozilla.org/docs/Web/API/Selection/direction),\n  which return the real (un‑retargeted) boundary points.\n- Focus is read with\n  [`ShadowRoot.activeElement`](https://developer.mozilla.org/docs/Web/API/DocumentOrShadowRoot/activeElement)\n  via [`Node.getRootNode`](https://developer.mozilla.org/docs/Web/API/Node/getRootNode).\n- Caret movement and word/line deletion use the native `Selection.modify`, and\n  selection is written with `Selection.setBaseAndExtent`, both of which operate\n  on shadow‑tree nodes directly.\n- Drop targets and image-drag hit‑tests resolve through\n  [`Document.caretPositionFromPoint`](https://developer.mozilla.org/docs/Web/API/Document/caretPositionFromPoint)\n  with the `shadowRoots` option so the dropped point lands on the real node\n  rather than the retargeted host.\n\n### Browser support\n\nThe shadow-aware path requires the platform APIs listed above. Lexical falls\nback to the light‑DOM reads when those APIs are missing, so editors that don't\nlive in a shadow tree keep working on any engine the rest of Lexical supports.\n\n| Platform API | Used for | Chrome / Edge | Firefox | Safari |\n| --- | --- | --- | --- | --- |\n| `Selection.getComposedRanges` | Reading the un‑retargeted boundary points | 137+ | 142+ | 17.0+ |\n| `Selection.direction` | Mapping the composed range back onto anchor/focus | 137+ | 126+ | 17.0+ |\n| `ShadowRoot.activeElement` | Resolving the focused element through the host | All modern | All modern | All modern |\n| `Document.caretPositionFromPoint({shadowRoots})` | Shadow-aware drop / drag hit-tests | 128+ | 125+ | 18.1+ |\n\nLexical also supports the legacy variadic form of `getComposedRanges` shipped by\nSafari 17–18.1, automatically choosing the dictionary or variadic call shape\nat runtime.\n\n### Closed shadow roots\n\nA closed shadow root (`{mode: 'closed'}`) hides its contents from outside code:\n`Node.getRootNode({composed: false})` returns the host on every external probe,\nand `Selection.getComposedRanges({shadowRoots})` ignores the closed root. The\neditor inside a closed shadow root cannot read its own selection through the\nhost, so closed shadow roots are **not supported**.\n\nIf you need style/markup encapsulation without selection isolation, mount the\neditor in an open shadow root with `delegatesFocus: true` instead — outside\ncode can still observe focus and selection while CSS and markup remain\nencapsulated.\n\n### Styling\n\nShadow trees do not inherit the document's stylesheets, so your editor/theme\nCSS has to live inside the shadow root. A `<style>` element appended to the\nroot is the simplest option; a\n[constructable stylesheet](https://developer.mozilla.org/docs/Web/API/CSSStyleSheet#constructor)\nadopted via `shadowRoot.adoptedStyleSheets` scales better when one stylesheet\nserves several editor instances. The playground's `ShadowDomWrapper` clones\nthe page's existing `<link>` / `<style>` nodes into the shadow root so it can\nreuse Vite's HMR-managed stylesheets. This is a property of Shadow DOM, not\nof Lexical.\n\nCSS custom properties and inherited HTML attributes (`dir`, `lang`) cross\nthe shadow boundary on their own, so a page-level rule like `:root {\n--editor-bg: #1c1d22; }` or a host attribute like `<my-editor dir=\"rtl\"\nlang=\"ko\">` propagates into the editor without further setup. User\npreference media queries (`@media (prefers-color-scheme: dark)`,\n`(prefers-reduced-motion: reduce)`, `(forced-colors: active)`) evaluate\ninside the shadow root just like outside; place them in the shadow-mounted\nstylesheet rather than the page sheet.\n\n## Embedding in an iframe\n\nAn editor whose root element belongs to an `<iframe>` document is also\nsupported. Lexical reads the editor's `window`/`document` from the root\nelement (`rootElement.ownerDocument.defaultView`), so selection and focus are\nresolved against the iframe rather than the top‑level document:\n\n```js\nconst iframeDoc = iframe.contentDocument;\nconst contentEditable = iframeDoc.querySelector('#editor');\n\n// The editor can be built in the parent frame; once its root element is set it uses\n// the iframe's own window and document for selection and focus.\neditor.setRootElement(contentEditable);\n```\n\nSelection inside an iframe is **not** retargeted (an iframe is a separate\ndocument, not a shadow boundary), so `getComposedRanges` is not involved here —\nthe iframe's own selection already reports the correct nodes.\n\n## Shadow-aware helpers\n\nIf a plugin reads the DOM selection or the focused element directly, use the\nshadow/iframe‑aware helpers exported from `lexical` instead of\n`Selection.anchorNode` / `document.activeElement`, so it keeps working in these\ncontexts. In the plain light DOM each helper degrades to the standard read\nwithout any extra cost.\n\n| Instead of | Use |\n| --- | --- |\n| `Selection.anchorNode` / `anchorOffset` / `focusNode` / `focusOffset` | `getDOMSelectionPoints(selection, rootElement)` |\n| `selection.getRangeAt(0)` | `getDOMSelectionRange(selection, rootElement)` |\n| Both shapes from one read | `getDOMSelectionRangeAndPoints(selection, rootElement)` |\n| `selection.getComposedRanges` raw call | `getComposedStaticRange(selection, rootElement)` |\n| `document.activeElement === el` | `getActiveElement(el) === el` |\n| Deepest focused element through nested shadow trees | `getActiveElementDeep(document)` |\n| `event.target` on a `composed: true` event listening above the shadow boundary | `getComposedEventTarget(event)` |\n| `node.getRootNode() instanceof ShadowRoot` (cross‑realm‑safe) | `isDOMShadowRoot(node)` |\n| Walking up to discover enclosing shadow roots | `getDOMShadowRoots(node)` |\n\nAll nine helpers are marked `@experimental` while the surface stabilizes — the\nreturn shape may change, but the behavior in the light DOM is stable.\n\n### `getDOMSelectionPoints` read semantics\n\nIn the light DOM `getDOMSelectionPoints` returns the live `Selection` itself,\nso each property read is deferred and `$updateDOMSelection` only pays for the\nsynchronous style/layout recalculation that a `Selection.anchorNode` /\n`focusNode` read triggers when it actually needs the value. Inside a shadow\ntree the return is a snapshot taken at call time, including\n`Selection.direction` so callers can branch on it explicitly. Read the four\npoints immediately after the call rather than caching the returned reference,\nor use `points === domSelection` to detect the alias path.\n\nIf a future engine ships `getComposedRanges` without `Selection.direction`\n(no current configuration matches — every engine that ships the former\nalso ships the latter), the snapshot's `direction` is `undefined` and\nanchor/focus default to the StaticRange's tree order; callers needing strict\nbackward fidelity should check `direction !== undefined` before relying on\nthe swap.\n\n## Form association\n\nA Lexical editor inside a [form-associated custom\nelement](https://developer.mozilla.org/docs/Web/API/Web_components#form-associated_custom_elements)\nparticipates in a `<form>` like any built-in form control: submission carries\nthe editor's value, form reset clears it, and validation flows through. The\nsketch:\n\n```js\nclass LexicalEditorElement extends HTMLElement {\n  static formAssociated = true;\n\n  constructor() {\n    super();\n    this.internals = this.attachInternals();\n  }\n\n  get value() {\n    return this.editor\n      ? JSON.stringify(this.editor.getEditorState().toJSON())\n      : '';\n  }\n\n  set value(serialized) {\n    if (this.editor && serialized !== '') {\n      this.editor.setEditorState(this.editor.parseEditorState(serialized));\n    }\n  }\n\n  connectedCallback() {\n    // `delegatesFocus: true` routes `host.focus()` (and implicit focus\n    // from `<label for=\"...\">`) to the first focusable element inside\n    // the shadow root — for the editor that's the contentEditable.\n    const shadow =\n      this.shadowRoot ?? this.attachShadow({delegatesFocus: true, mode: 'open'});\n    const contentEditable = document.createElement('div');\n    contentEditable.contentEditable = 'true';\n    contentEditable.tabIndex = 0;\n    shadow.appendChild(contentEditable);\n\n    const editor = buildEditorFromExtensions(\n      defineExtension({name: 'lexical-editor-element'}),\n    );\n    editor.setRootElement(contentEditable);\n    this.editor = editor;\n\n    // Seed the form value so a submit before the user types still produces\n    // a non-empty serialized state, mirroring `<input value=\"...\">`.\n    this.internals.setFormValue(this.value);\n\n    editor.registerUpdateListener(({dirtyElements, dirtyLeaves}) => {\n      // Only fire on real content changes, not on pure selection updates —\n      // matches HTMLInputElement's `input` event contract.\n      if (dirtyElements.size === 0 && dirtyLeaves.size === 0) return;\n      this.internals.setFormValue(this.value);\n      this.dispatchEvent(new Event('input', {bubbles: true, composed: true}));\n    });\n  }\n}\n```\n\nWithout the dirty-leaf/element gate every caret move would wake the form's\n`oninput` listeners, so the `input` event matches `HTMLInputElement`'s\ncontract only after that guard. The initial `setFormValue` is what keeps a\nform submission before the user types from carrying an empty value, since\nthe update listener has not fired yet at that point. A full reference\nimplementation lives in\n[`examples/shadow-dom-web-component`](https://github.com/facebook/lexical/tree/main/examples/shadow-dom-web-component).\n\nDOM moves (re-parenting the host into a different `<form>` or list) trigger\n`disconnectedCallback` followed by `connectedCallback`, which rebuilds the\neditor against a fresh contentEditable. Round-trip the user's content the\nsame way `<input>` and `<textarea>` round-trip their `value` attribute:\ncache the serialized state in `disconnectedCallback` and replay it through\n`parseEditorState` on the next mount. The reference dev example uses a\n`pendingState` field for exactly this.\n\nThe same form-associated host can also implement the rest of the\n[form-associated callbacks](https://developer.mozilla.org/docs/Web/API/Web_components/Using_custom_elements#form-associated_callbacks):\n`formStateRestoreCallback(state, reason)` re-hydrates the editor on bfcache\nnavigation (`reason: 'restore'`) or form autocomplete restore\n(`reason: 'autocomplete'`); `formAssociatedCallback(form)` reacts to\nprogrammatic form moves; `formResetCallback()` clears the editor when the\nsurrounding form is reset; and `formDisabledCallback(isDisabled)` flips the\neditor's editable state when an ancestor `<fieldset disabled>` toggles. The\nserialized JSON `internals.setFormValue` published earlier is what comes\nback in the state-restore callback.\n\n## Common pitfalls\n\nMoving an existing editor into a shadow root exposes a handful of recurring\nmismatches between the DOM APIs you'd reach for and what they report.\n\n### Event retargeting\n\nListeners above the shadow boundary — `window`, `document`'s\n`selectionchange`, popups portaled to `document.body` — see `event.target`\nretargeted to the shadow host, not the actual clicked element. A\n`rootElement.contains(event.target)` gate therefore always rejects clicks\nthat came from the editor. Resolve through the composed path:\n\n```js\nconst target = getComposedEventTarget(event);\nif (target instanceof Node && rootElement.contains(target)) {\n  // click really did land inside the shadow-hosted editor\n}\n```\n\n### Focus probes\n\n`document.activeElement` reports the host when focus is inside an open\nshadow tree, so `document.activeElement === rootElement` is always false.\n`getActiveElement(rootElement)` reads `DocumentOrShadowRoot.activeElement`\nthrough `Node.getRootNode` instead, and `getActiveElementDeep` keeps\ndescending into nested shadow roots when an editor whose decorator embeds a\nweb component needs the innermost focused element.\n\n```js\nif (getActiveElement(rootElement) === rootElement) { ... }\n```\n\n### Outside-click handlers\n\nA typical dropdown registers `document.addEventListener('click', ...)` and\ncalls `setShowDropDown(false)` whenever `button.contains(event.target)` is\nfalse. From inside a shadow tree that check always fails (the target is the\nhost), so the dropdown closes on the very click that opened it. Compare\nagainst `getComposedEventTarget(event)` instead — the same fix the playground's\n`DropDown` and `@lexical/react`'s `DraggableBlockPlugin` use.\n\n### Drop hit-tests\n\n`document.caretRangeFromPoint` and the no-argument\n`document.caretPositionFromPoint` return the host when the pointer is over\nshadow content, so an image drop lands on the host rather than the textnode\nunder the cursor. `@lexical/clipboard`'s internal `caretFromPoint` helper switches to\n`caretPositionFromPoint(x, y, {shadowRoots})` when `rootElement` lives in a\nshadow tree, and verifies the returned offset node really did land inside\none of the requested shadow roots — engines that silently ignore the option\nfall through to the legacy paths.\n\n### Style mirror cleanup\n\nIf your shadow mount mirrors `<style>` / `<link>` nodes from `document.head`\nand watches for additions via `MutationObserver`, mirror removals too;\notherwise stylesheets removed by HMR or a runtime theme swap linger inside\nthe shadow. `ShadowDomWrapper` tracks an `original → clone` map so an\nupstream removal drops the corresponding clone, and on unmount it removes\nevery clone so React 18 StrictMode's double-mount can't accumulate\nduplicates through the persistent shadow root.\n\nDev-mode HMR has two paths that a `childList`-only observer misses.\nFirefox replaces a `<style>`'s text content in place, so the observer\nneeds `characterData + subtree` to see the mutation; Chrome and WebKit\nupdate CSS through the CSSOM (`style.sheet.replaceSync` / `insertRule`),\nwhich the DOM observer never sees at all. `ShadowDomWrapper` also\nsubscribes to Vite's `vite:afterUpdate` hook for an explicit resync of\nevery clone after each HMR pass — `import.meta.hot` is `undefined` in\nproduction builds, so the hook is a no-op outside dev.\n\n### Popover and dialog layout\n\nThe UA stylesheet defaults a closed\n[Popover](https://developer.mozilla.org/docs/Web/API/Popover_API) (or a\n`<dialog>` without `open`) to `display: none`. A base rule like\n`#my-popover { display: flex; ... }` overrides that and leaves the popover\nvisible after `hidePopover()`. Scope layout to the open state instead —\n`#my-popover:popover-open { display: flex; }` — so the closed state honors\nthe UA default. This is a popover-API gotcha, not a shadow-specific one,\nbut it surfaces here because floating UI anchored to a shadow-root\nselection (a format popover or link editor that reads coordinates through\n`getDOMSelectionRangeAndPoints`) is a common shadow integration pattern.\n\n### Observers across the shadow boundary\n\n`ResizeObserver`, `IntersectionObserver`, and `MutationObserver` observe\nnodes inside an open shadow root with no special configuration — pass the\ninner node (the contentEditable or the host) just like any other DOM\ntarget. A `MutationObserver` registered on the host does not see mutations\ninside the shadow tree, though; observe the contentEditable or a\nshadow-internal container instead. The host's `attributeChangedCallback`\ncovers the host's own attribute changes.\n\n## Migrating an existing light-DOM editor\n\nThe same code shape keeps working in a shadow root; migration is mostly\nabout removing pre-shadow workarounds. Direct selection reads\n(`Selection.anchorNode`, `Selection.getRangeAt(0)`,\n`document.querySelector('[contenteditable]')` used as an editor probe) route\nthrough `getDOMSelectionPoints` / `getDOMSelectionRange` /\n`getActiveElement`, and any `event.target` read above the shadow boundary\nbecomes `getComposedEventTarget(event)`. CSS variables inherit through the\nshadow boundary but class declarations don't, so the editor/theme CSS has\nto be adopted or cloned into the shadow root.\n\nThe focus and popup paths are the ones that surprise people in review:\noutside-click handlers, blur-then-refocus, and \"did the editor lose focus?\"\nprobes usually need `getActiveElement(rootElement)` rather than\n`document.activeElement`; floating UI that portals into `document.body` and\nresolves hover with `element.getRootNode().elementFromPoint(x, y)` keeps the\nsame call shape but has to guard the popup root narrowing against a\ndetached popup whose `getRootNode()` returns itself.\n\n## Examples\n\nRunnable examples live in the repository:\n\n- [`examples/shadow-dom`](https://github.com/facebook/lexical/tree/main/examples/shadow-dom)\n  — a React editor inside a shadow root with a light‑DOM toolbar.\n- [`examples/shadow-dom-web-component`](https://github.com/facebook/lexical/tree/main/examples/shadow-dom-web-component)\n  — a framework‑free `<lexical-editor>` custom element, form‑associated via\n  `ElementInternals`. The page also mounts one instance inside a wrapper\n  `<div>` that opens its own shadow root, exercising the multi‑level\n  walk through two nested shadow boundaries.\n- [`examples/vanilla-js-iframe`](https://github.com/facebook/lexical/tree/main/examples/vanilla-js-iframe)\n  — an editor rendered into an `<iframe>`.\n- The playground's **Render in Shadow DOM** setting toggles the same editor\n  between light and shadow mounts, useful for spot-checking your plugins\n  against both modes.\n\n<!-- File: packages/lexical-website/docs/concepts/transforms.md (Tokens: ~1851 | Category: anthropic-skill) -->\n# Node Transforms\n\nTransforms are the most efficient mechanism to respond to changes to the EditorState.\n\nFor example:\nUser types a character and you want to color the word blue if the word is now equal to \"congrats\".\nWe programmatically add an `@Mention` to the editor, the `@Mention` is immediately next to another `@Mention` (`@Mention@Mention`). Since we believe this makes mentions hard to read, we want to destroy/replace both mentions and render them as plain TextNode's instead.\n\n```js\nconst removeTransform = editor.registerNodeTransform(TextNode, (textNode) => {\n  if (textNode.getTextContent() === 'blue') {\n    textNode.setTextContent('green');\n  }\n});\n```\n\n## Syntax\n\n```typescript\neditor.registerNodeTransform<T extends LexicalNode>(klass: Klass<T>, listener: Transform<T>): () => void\n```\n\n## Lifecycle\n\nTransforms are executed sequentially before changes are propagated to the DOM and multiple transforms still lead to a single DOM reconciliation (the most expensive operation in Lexical's lifecycle).\n\n```mermaid\nflowchart LR\n  accTitle: Transforms lifecycle\n  accDescr: A keyboard event leads to an update function. Its changes mark nodes dirty, and the transforms registered for those nodes run one after another. If a transform changed more nodes, the transforms for those nodes run again. When nothing is left dirty, the DOM is reconciled once and the update listeners run.\n  event{{\"Keyboard event\"}} --> update[\"Update function\"]\n  update --> t1[\"transform1\"]\n  t1 --> t2[\"transform2\"]\n  t2 --> dirty{\"Nodes still dirty?\"}\n  dirty -->|\"yes\"| t1\n  dirty -->|\"no\"| reconcile[\"DOM reconciled once\"]\n  reconcile --> listeners[\"Update listeners\"]\n```\n\n:::caution Beware!\n\nWhile it is possible to achieve the same or very similar result through an [update listener](listeners.md#registerupdatelistener) followed by an update, this is highly discouraged as it triggers an additional render (the most expensive lifecycle operation).\n\nAdditionally, each cycle creates a brand new `EditorState` object which can interfere with features like undo/redo (`HistoryExtension`) if not handled correctly.\n\n```js\neditor.registerUpdateListener(() => {\n  editor.update(() => {\n    // Don't do this\n  });\n});\n```\n\n:::\n\n### Dirty Nodes\n\n*Dirty Leaves* (any LexicalNode that is not an ElementNode) and *Dirty Elements*\nare tracked separately to support the transform heuristic.\n\nInternally, there are two states for dirty nodes:\n* Intentionally Dirty nodes (leaves or elements) had `node.getWritable()` or\n  `node.markDirty()` (an alias) called on them. Maintaining the\n  tree-of-doubly-linked-lists structure of a lexical document requires that\n  this Intentionally Dirty state will propagate to immediate siblings and in\n  some cases the parent node.\n* Unintentionally Dirty Element nodes are an ancestor of a\n  dirty node that were not explicitly marked as Intentionally Dirty\n  Only elements can be Unintentionally Dirty, because\n  leaves by definition can not have children.\n\nThe reconciler works by starting at the RootNode (which is the ancestor\nof any attached node, and thus always dirty whenever any attached node\nis dirty). Intentionally Dirty nodes have their createDOM and/or updateDOM\ncalled. Dirty Elements reconcile all of their children. Thus, reconciliation\nstops at the highest node in a subtree that has no dirty nodes (unless it\nis running with a flag to do a full reconciliation which considers all\nnodes as Intentionally Dirty).\n\n:::info\n\nThe transform heuristic depends on these internal implementation details to\nfind a fixed point where no more transforms are required.\n\n:::\n\n### Transform heuristic\n\n1. We transform leaves first. If transforms generate additional dirty nodes we repeat `step 1`. The reasoning behind this is that marking a leaf as dirty marks all its parent elements as dirty too.\n2. We transform elements.\n    - If element transforms generate additional dirty nodes we repeat `step 1`.\n    - If element transforms only generate additional dirty elements we only repeat `step 2`.\n\nA node will be marked as dirty on any (or most) modifications done to it, its children, or siblings in certain cases.\n\n## Preconditions\n\nPreconditions are fundamental for transforms to prevent them from running multiple times and ultimately causing an infinite loop.\n\nTransforms are designed to run when nodes have been modified (aka Intentionally Dirty). For the most part, transforms only need to run once after the update but the sequential nature of transforms makes it possible to have order bias. Hence, transforms are run over and over until this particular type of Node is no longer marked as intentionally dirty by any of the transforms.\n\nHence, we have to make sure that the transforms do not mark the node dirty unnecessarily.\n\n```js\n// When a TextNode changes (marked as dirty) make it bold\neditor.registerNodeTransform(TextNode, textNode => {\n  // Important: Check current format state\n  if (!textNode.hasFormat('bold')) {\n    textNode.toggleFormat('bold');\n  }\n});\n```\n\nBut oftentimes, the order is not important. The below would always end up in the result of the two transforms:\n\n```js\n// Extension 1\neditor.registerNodeTransform(TextNode, textNode => {\n  // This transform runs twice but does nothing the first time because it doesn't meet the preconditions\n  if (textNode.getTextContent() === 'modified') {\n    textNode.setTextContent('re-modified');\n  }\n})\n// Extension 2\neditor.registerNodeTransform(TextNode, textNode => {\n  // This transform runs only once\n  if (textNode.getTextContent() === 'original') {\n    textNode.setTextContent('modified');\n  }\n})\n// App\neditor.registerUpdateListener(({editorState}) => {\n  const text = editorState.read(() => $getRoot().getTextContent());\n  // text === 're-modified'\n});\n```\n\n:::info\n\nThe transform heuristic considers `RootNode` to be Intentionally Dirty whenever\nany node is dirty, and it ensures that this transform is applied last after all\nother transforms. In this way, you can consider it a \"pre-update\" listener,\nwhich occurs before any DOM reconciliation has happened. As with any other\ntransform, it may get called multiple times in a given update (especially if\nyour RootNode transform makes any node dirty).\n\n:::\n\n## Transforms on parent nodes\n\nTransforms are very specific to a type of node. This applies to both the declaration (`registerNodeTransform(ImageNode)`) and the times it triggers during an update cycle.\n\n```js\n// Won't trigger\neditor.registerNodeTransform(ParagraphNode, () => {});\n// Will trigger as TextNode was marked dirty\neditor.registerNodeTransform(TextNode, () => {});\neditor.update(() => {\n  const textNode = $getNodeByKey('3');\n  textNode.setTextContent('foo');\n});\n```\n\nWhile the marked dirty rule is always true, there are some cases when it's not immediately obvious and/or we force nearby nodes to become dirty for the sake of easier transform logic:\nYou add a node to an ElementNode, the ElementNode and the newly added children are marked dirty, also its new immediate siblings\nYou remove a node, its parent is marked dirty, also the node's immediate siblings prior to being removed\nYou move a node via `replace`, rules 2 and 1 are applied.\n\n```js\neditor.registerNodeTransform(ParagraphNode, paragraph => {\n // Triggers\n});\neditor.update(() => {\n  const paragraph = $getRoot().getFirstChild();\n  paragraph.append($createTextNode('foo'));\n});\n```\n\n## registerLexicalTextEntity\n\nIt is common to have certain nodes that are created/destroyed based on their text content and siblings. For example, `#lexical` is a valid hashtag whereas `#!lexical` is not.\n\nThis is a perfectly valid case for transforms but we have gone ahead and already built a utility transform wrapper for you for this specific case:\n\n```typescript\nregisterLexicalTextEntity<T extends TextNode>(\n  editor: LexicalEditor,\n  getMatch: (text: string) => null | EntityMatch,\n  targetNode: Klass<T>,\n  createNode: (textNode: TextNode) => T,\n): Array<() => void>;\n```\n\n## Examples\n\n1. [Emojis](https://github.com/facebook/lexical/blob/main/packages/lexical-playground/src/plugins/EmojisExtension/index.ts)\n2. [AutoLink](https://github.com/facebook/lexical/blob/main/packages/lexical-playground/src/plugins/AutoLinkExtension/index.ts)\n3. [HashtagExtension](https://github.com/facebook/lexical/blob/main/packages/lexical-hashtag/src/LexicalHashtagExtension.ts)\n\n<!-- File: packages/lexical-website/docs/concepts/traversals.md (Tokens: ~4941 | Category: anthropic-skill) -->\n# Node Traversals with NodeCaret\n\nNodeCaret offers a unified and efficient way for traversing the document\ntree, making it much easier to correctly implement traversals and avoid\nedge cases around empty nodes and collapsed selections.\n\nThese new low-level functions were all designed to work together as a\nfully featured relatively lightweight API to use in the core to\nallow us to gradually address some edge cases and then simplify and shrink\nthe code. We expect higher-level utilities to be developed and shipped\nin @lexical/utils or another module at a later date. The current overhead\nshould be less than 3kB in a production environment.\n\nThe NodeCaret API was introduced in lexical v0.25.0.\n\n## Concepts\n\nThe core concept with `NodeCaret` is that you can represent any specific\npoint in the document by using an `origin` node, a `direction` that\npoints towards an adjacent node (`next` or `previous`), and a `type`\nto specify whether the arrow points towards a sibling (`sibling`) or\ntowards a child (`child`).\n\nAll of these types have a `D` type parameter that must be a `CaretDirection`, so you\ncan not accidentally mix up `next` and `previous` carets. Many of them\nalso have a `T` type parameter that encodes the type of the `origin` node.\n\n:::tip\n\nThe methods of a caret are designed to operate on nodes attached to the `origin`\nin the designated direction, not the `origin` itself. For example, this code is\na no-op because it will attach a node to the `origin`, and then remove the node\nthat was just attached.\n\n```ts\n// The origin is unaffected (other than being marked dirty)\ncaret.insert($createTextNode('no-op')).remove();\n```\n\n:::\n\n:::warning\n\nCarets are immutable, and designed for low-level usage. There is no attempt\nfor carets to automatically update based on changes to the document\n(this is a common source of bugs when working with `RangeSelection`).\nFunctions and methods that work with carets and are expected to change the\nstructure of the document will always return a possibly new caret.\n\nThe `origin` of a caret is the exact version of the object that it was\nconstructed with, all accessor methods on that origin will generally call\n`origin.getLatest()` so the operations will see the latest version.\n\n:::\n\n### NodeCaret\n\n`NodeCaret` is any `SiblingCaret` or any `ChildCaret`\n* Typically constructed with `$getChildCaretOrSelf($getSiblingCaret(origin, direction))`\n  which returns a `ChildCaret` when the origin is an `ElementNode`\n\n:::tip\n\nThis type does not include `TextPointCaret` or `TextPointCaretSlice`,\nso you will not have to consider those edge cases when you see this\nmore specific type.\n\n:::\n\n### SiblingCaret\n\n`SiblingCaret` is a caret that points towards a sibling of the origin\n\n* Constructed with `$getSiblingCaret(origin: LexicalNode, direction: CaretDirection)`\n* The `next` direction points towards the right\n* The `previous` direction points towards the left\n\n|                        | → direction: `'next'`      | ← direction: `'previous'`     |\n|------------------------|---------------------------|-------------------------------|\n| `getParentAtCaret()`   | `origin.getParent()`      | `origin.getParent()`          |\n| `getNodeAtCaret()`     | `origin.getNextSibling()` | `origin.getPreviousSibling()` |\n| `insert(node)`         | `origin.insertAfter(node)`| `origin.insertBefore(node)`   |\n\n### ChildCaret\n\n`ChildCaret` is a caret that points towards the first or last child of the origin\n\n* Constructed with `$getChildCaret(origin: ElementNode, direction: CaretDirection)`\n* The `next` direction points towards the first child\n* The `previous` direction points towards the last child\n\n|                        | ↘ direction: `'next'`      | ↙ direction: `'previous'`     |\n|------------------------|----------------------------|-------------------------------|\n| `getParentAtCaret()`   | `origin`                   | `origin`                      |\n| `getNodeAtCaret()`     | `origin.getFirstChild()`   | `origin.getLastChild()`       |\n| `insert(node)`         | `origin.splice(0, 0, node)`| `origin.append(node)`         |\n\n### PointCaret\n\n`PointCaret` is any `TextPointCaret`, `SiblingCaret` or `ChildCaret`. This\ntype can be used to represent any point in the document that `PointType` can represent.\n\n### TextPointCaret\n\n`TextPointCaret` is basically a `SiblingCaret` with a `TextNode` origin and an `offset` property\n* Constructed with `$getTextPointCaret(origin, direction, offset)`\n* The `offset` property is an absolute index into the string\n* The `next` direction implies all text content after `offset`\n* The `previous` direction implies all text content before `offset`\n* All methods that are also present on `SiblingCaret` behave in the same way\n\n### TextPointCaretSlice\n\n`TextPointCaretSlice` is a wrapper for `TextPointCaret` that provides a signed `distance`.\n\n* Constructed with `$getTextPointCaretSlice(caret, distance)`\n* `$splitTextPointCaretSlice(slice, selection?)` isolates the slice in a text node,\n  splitting at its boundaries when needed. It returns `null` for an empty slice\n  and the original node for a whole-node slice. Pass the range selection that\n  produced the slice to preserve its endpoints, including element points and\n  detached selections. Like `TextNode.splitText`, it does not treat token or\n  segmented nodes as atomic.\n* There are convenience methods like `removeTextSlice()` and `getTextContent()`,\n  so it's not generally necessary to know the implementation details here\n* `Math.min(caret.offset, caret.offset + distance)` refers to the start offset of the slice\n* `Math.max(caret.offset, caret.offset + distance)` refers to the end offset of the slice\n* The `direction` of the caret is generally ignored when working with a\n  `TextPointCaretSlice`, the slice is in absolute string coordinates\n\n:::info\n\nThe property name `distance` was chosen because `length` and `size` are\ncommonly used on other data structures in JavaScript and Lexical, and they\nare overwhelmingly non-negative. While most uses of `distance` are also\nnon-negative, in some contexts such as computer graphics it is not uncommon\nto use\n[Signed distance functions](https://en.wikipedia.org/wiki/Signed_distance_function)\nwhere the distance metric is signed.\n\nIn SDF terms, the subset of the space is `[offset, ∞)`. Any coordinate less\nthan the `offset` boundary is a negative distance; otherwise the distance is\nnon-negative.\n\n:::\n\n### CaretRange\n\n`CaretRange` contains a pair of `PointCaret` that are in the same direction. It\nis equivalent in purpose to a `RangeSelection`, and is what you would generally\nuse for depth first traversals.\n\n* Constructed with `$getCaretRange(anchor, focus)`, `$caretRangeFromSelection(selection)`,\n  or `$extendCaretToRange(anchor)`\n* The `anchor` is the start of the range, generally where the selection originated,\n  and it is \"anchored\" in place because when a selection grows or shrinks only the\n  `focus` will be moved\n* The `focus` is the end of the range, where the blinking cursor is, it's the current\n  focus of the user\n* Anchor and focus must point in the same direction. The `anchor` points towards the first\n  node *in the range* and the focus points towards the first node *not in the range*\n* The `getTextSlices()` method is essential to handle the literal edge cases where\n  the anchor and/or focus are a `TextPointCaret`. These edges are *not* included\n  in the default caret iteration of the `CaretRange`.\n\n:::warning\n\nIf you are iterating a `CaretRange` you must consider the `getTextSlices()`\nseparately, they are not included in the iteration. This is so you don't have\nto consider `TextPointCaretSlice` at every step. They are literal edge cases\nthat can only be at the start and/or end and typically have special\ntreatment (splitting instead of removing, for example).\n\n:::\n\n## Traversal Strategies\n\n<!-- when you update the example code below, please update the tests in packages/lexical/src/caret/__tests__/unit/docs-traversals.test.ts -->\n\n### Adjacent Caret Traversals\n\nThe lowest level building block for traversals with NodeCaret is the adjacent caret\ntraversal, which is supported directly by methods of NodeCaret.\n\n`getAdjacentCaret()` - Gets a `SiblingCaret` for the node attached to\n  `origin` in direction. If there is no attached node, it will return `null`\n\n`getParentCaret(rootMode)` - Gets a `SiblingCaret` for the parent node\n  of `origin` in the same direction. If there is no parent node, or the parent\n  is a root according to `rootMode`, then it will return `null`. `rootMode`\n  may be `'root'` to only return `null` for `RootNode` or `'shadowRoot'` to\n  return `null` for `RootNode` or any `ElementNode` parent where\n  `isShadowRoot()` returns true\n\n`getChildCaret()` - Gets a `ChildCaret` for this origin, or `null` if the\n  origin is not an `ElementNode`. Will return `this` if the caret is already\n  a `ChildCaret`\n\nFor example, iterating all siblings:\n\n```ts\n// Note that NodeCaret<D> already implements Iterable<NodeCaret<D>> in this\n// way, so this function is not very useful. You can just use startCaret as\n// the iterable.\nfunction *$iterSiblings<D extends CaretDirection>(\n  startCaret: NodeCaret<D>\n): Iterable<SiblingCaret<LexicalNode, D>> {\n  // Note that we start at the adjacent caret. The start caret\n  // points away from the origin node, so we do not want to\n  // trick ourselves into thinking that that origin is included.\n  for (\n    let caret = startCaret.getAdjacentCaret();\n    caret !== null;\n    caret = caret.getAdjacentCaret()\n  ) {\n    yield caret;\n  }\n}\n```\n\n### Examples\n\nGiven the following document tree, here are some examples of using the\nadjacent node traversal:\n\nRoot\n* Paragraph A\n  * Text A1\n  * Link A2\n    * Text A3\n  * Text A4\n* Paragraph B\n  * Text B1\n* Paragraph C\n\n```ts\n// The root does not have sibling nodes\nconst carets = [...$getSiblingCaret($getRoot(), 'next')];\nexpect(carets).toEqual([]);\n```\n\n```ts\n// The adjacent node to a ChildNode is its first or last child\n// and is always a SiblingNode. It does not traverse deeper.\nconst carets = [...$getChildCaret($getRoot(), 'next')];\n\n// next starts at the first child\nexpect(carets).toEqual([\n  $getSiblingCaret(paragraphA, 'next'),\n  $getSiblingCaret(paragraphB, 'next'),\n  $getSiblingCaret(paragraphC, 'next'),\n]);\n\n// previous starts at the last child\nconst prevCarets = [...$getChildCaret($getRoot(), 'previous')];\nexpect(prevCarets).toEqual([\n  $getSiblingCaret(paragraphC, 'previous'),\n  $getSiblingCaret(paragraphB, 'previous'),\n  $getSiblingCaret(paragraphA, 'previous'),\n]);\n```\n\n```ts\n// The iteration starts at the node where the head of the \"arrow\"\n// is pointing, which is away from the origin (the tail of the \"arrow\").\nconst carets = [...$getSiblingCaret(paragraphB, 'next')];\nexpect(carets).toEqual([\n  $getSiblingCaret(paragraphC, 'next'),\n]);\n\nconst prevCarets = [...$getSiblingCaret(paragraphB, 'previous')];\nexpect(prevCarets).toEqual([\n  $getSiblingCaret(paragraphA, 'previous'),\n]);\n```\n\n### Depth First Caret Traversals\n\nThe strategy to do a depth-first caret traversal is to use an adjacent caret\ntraversal and immediately use a `ChildCaret` any time that an `ElementNode`\norigin is encountered. This strategy yields all possible carets, but each\nElementNode in the traversal may be yielded once or twice (a `ChildCaret` on\nenter, and a `SiblingCaret` on leave). Allowing you to see whether an\n`ElementNode` is partially included in the range or not is one of the\nreasons that this abstraction exists.\n\n```ts\nfunction *$iterCaretsDepthFirst<D extends CaretDirection>(\n  startCaret: NodeCaret<D>\n): Iterable<NodeCaret<D>> {\n  function step(prevCaret: NodeCaret<D>): null | NodeCaret<D> {\n    // Get the adjacent SiblingCaret\n    const nextCaret = prevCaret.getAdjacentCaret();\n    return (\n      // If there is a sibling, try and get a ChildCaret from it\n      (nextCaret && nextCaret.getChildCaret()) ||\n      // Return the sibling if there is one\n      nextCaret ||\n      // Return a SiblingCaret of the parent, if there is one\n      prevCaret.getParentCaret('root')\n    );\n  }\n  // You may add an additional check here, usually some specific\n  // caret to terminate the iteration with (such as the parent caret\n  // of startCaret):\n  //\n  //  `caret !== null || caret.is(endCaret)`\n  //\n  for (\n    let caret = step(startCaret);\n    caret !== null;\n    caret = step(caret)\n  ) {\n    yield caret;\n  }\n}\n```\n\nNormally this type of iteration would be done from a `CaretRange`, where you\nwould specify a precise end caret (focus).\n\n```ts\nfunction $iterCaretsDepthFirst<D extends CaretDirection>(\n  startCaret: NodeCaret<D>,\n  endCaret?: NodeCaret<D>,\n): Iterable<NodeCaret<D>> {\n  return $getCaretRange(\n    startCaret,\n    // Use the root as the default end caret, but you might choose\n    // to use startCaret.getParentCaret('root') for example\n    endCaret || $getSiblingCaret($getRoot(), startCaret.direction)\n  );\n}\n```\n\nTo get all nodes that are entirely selected between two carets:\n\n```ts\nfunction *$iterNodesDepthFirst<D extends CaretDirection>(\n  startCaret: NodeCaret<D>,\n  endCaret: NodeCaret<D> = $getChildCaret($getRoot(), startCaret.direction),\n): Iterable<LexicalNode> {\n  const seen = new Set<NodeKey>();\n  for (const caret of $getCaretRange(startCaret, endCaret)) {\n    const {origin} = caret;\n    if ($isChildCaret(caret)) {\n      seen.add(origin.getKey());\n    } else if (!$isElementNode(origin) || seen.has(origin.getKey())) {\n      // If the origin is an element and we have not seen it as a ChildCaret\n      // then it was not entirely in the CaretRange\n      yield origin;\n    }\n  }\n}\n```\n\n### Examples\n\nGiven the following document tree, here are some examples of using the\ndepth-first node traversal (with a `CaretRange`):\n\nRoot\n* Paragraph A\n  * Text A1\n  * Link A2\n    * Text A3\n  * Text A4\n* Paragraph B\n  * Text B1\n* Paragraph C\n\n```ts\n// A full traversal of the document from root\nconst carets = [...$getCaretRange(\n  // Start with the arrow pointing towards the first child of root\n  $getChildCaret($getRoot(), 'next'),\n  // End when the arrow points away from root\n  $getSiblingCaret($getRoot(), 'next'),\n)];\nexpect(carets).toEqual([\n  $getChildCaret(paragraphA, 'next'),   // enter Paragraph A\n  $getSiblingCaret(textA1, 'next'),\n  $getChildCaret(linkA2, 'next'),       // enter Link A2\n  $getSiblingCaret(textA3, 'next'),\n  $getSiblingCaret(linkA2, 'next'),     // leave Link A2\n  $getSiblingCaret(textA4, 'next'),\n  $getSiblingCaret(paragraphA, 'next'), // leave Paragraph A\n  $getChildCaret(paragraphB, 'next'),   // enter Paragraph B\n  $getSiblingCaret(textB1, 'next'),\n  $getSiblingCaret(paragraphB, 'next'), // leave Paragraph B\n  $getChildCaret(paragraphC, 'next'),   // enter Paragraph C\n  $getSiblingCaret(paragraphC, 'next'), // leave Paragraph C\n]);\n```\n\n## Future Direction\n\nIt's expected that higher-level abstractions will be built on top of this\noutside of the core, either in @lexical/utils or a separate companion package.\nThis is just designed to be the lowest-level layer with a consistent and\ntype-safe interface. That sort of abstraction will probably look a little bit\nlike cheerio or jQuery, but for working with Lexical documents. It is not\nexpected that more abstractions will be added to the core.\n\nIn order to reduce code size and eliminate bugs, more of the core will be\nrefactored to use NodeCaret internally.\n\nOnce this happens, it's possible that the internal structure of PointType\nand/or RangeSelection may change to accommodate NodeCaret, as it is more\nresilient to document changes (only changes that directly affect the\norigin node will \"break\" the point). A simple version of this would be to\ncreate a caret any time that the point changes, and use that caret\nas a fallback if the selection would otherwise be lost.\n\nIt may be the case that NodeCaret will become the lowest level API, working\ndirectly with private LexicalNode/ElementNode internals. When/if that happens,\nthe methods on LexicalNode will remain for backwards compatibility,\nbut overriding them will not be supported. It isn't particularly safe to\noverride them as-is anyway, and these overrides are frequently the\nroot cause of bugs (e.g. parents that remove themselves after an operation\non a child, causing the point to be lost unless the caller was sophisticated\nenough to store the array of parents).\n\n## History\n\nBefore NodeCaret, Lexical's core API offered a relatively low-level DOM-like\ninterface for working with nodes and traversing them. It has accumulated\nmany functions over time for performing various kinds of traversals around\nthe tree (finding ancestors, children, depth, siblings, etc.), but most of\nthem are not implemented in a way that makes them easy to combine\nefficiently, and many of them have edge cases that are difficult to avoid\nand can't really be addressed without breaking compatibility.\n\nMany of these functions also have a lot of edge cases, particularly around\nassuming the reference nodes are inclusive. Many are also left-to-right\nbiased, don't offer an iterative version that can be aborted early or\nconsumed on the fly, etc.\n\nRefactoring many of these to use something like `PointType` would almost\nbe sufficient for many of these use cases, but the representation of\nthat type is inefficient and error-prone as any mutation to the tree\nrequires that each point be manually recomputed. `PointType` is also\ndirectionless, forcing a specific left-to-right bias into most APIs.\n`RangeSelection` can be used in many cases because a direction can\nbe inferred from any two different points, but that collapses with\na single point. It's also impractical to use `RangeSelection`\nconcurrently with mutations due to the problems with `PointType`.\n\nNodeCaret was born out of frustration with these APIs and a desire\nto unify it all in a coherent way to simplify and reduce errors in\nthe core.\n\n## Terminology\n\n### Caret\n\nThe term Caret was chosen because it is concise and specific term\nspecific to a point in a text document. Lexical is \"an extensible text editor\nframework\" so it makes sense that navigation in the document would use\nterms relevant to text. Most other terms such as Cursor or Point\nalready have meanings in Lexical and/or are less specific.\n\nSee also:\n- [Caret](https://developer.mozilla.org/en-US/docs/Glossary/Caret)\n- [Caret navigation](https://en.wikipedia.org/wiki/Caret_navigation)\n\n### Origin\n\nThe origin is the reference node for a NodeCaret. Absolute coordinates\nare determined by combining this origin node and an \"arrow\" that points\ntowards to where the adjacent node is (or could be). The \"arrow\" is\ndetermined by the `direction` and `type` of the caret.\n\nIn a way this \"arrow\" is considered to be something like a unit vector\nto indicate the direction, and adding it to an origin allows you to specify\nan absolute location relative to that origin. Unlike the other coordinate\nsystems available in Lexical, it does not need recomputing whenever\nsiblings or a parent changes, so long as the origin node is still attached.\n\nSee also:\n- [Origin](https://en.wikipedia.org/wiki/Origin_(mathematics))\n- [Unit Vector](https://en.wikipedia.org/wiki/Unit_vector)\n\n### ChildCaret / SiblingCaret\n\nThese were chosen because they match the existing methods on `ElementNode`\nand `LexicalNode` (`getFirstChild`, `getNextSibling`, etc.)\n\n### Direction\n\n`'next'` and `'previous'` were chosen for direction mostly to match the\nexisting methods such as `getNextSibling()` that exist in DOM and in Lexical.\nUsing other words such as `'left'` and `'right'` would be ambiguous since\ntext direction can be bidirectional and already uses the terms left-to-right\nand right-to-left.\n\n### Distance\n\nThe property name `distance` was chosen for `TextPointCaretSlice` because\n`length` and `size` are commonly used on other data structures in JavaScript\nand Lexical, and they are overwhelmingly non-negative. While most uses of\n`distance` are also non-negative, in some contexts such as computer graphics\nit is not uncommon to use\n[Signed distance functions](https://en.wikipedia.org/wiki/Signed_distance_function)\nwhere the distance metric is signed.\n\nIn SDF terms, the subset of the space is `[offset, ∞)`. Any coordinate less\nthan the `offset` boundary is a negative distance; otherwise the distance is\nnon-negative.\n\n<!-- File: packages/lexical-website/docs/design.md (Tokens: ~479 | Category: anthropic-skill) -->\n# Lexical's Design\n\n> Note: this document is still a work-in-progress and things are likely still in flux.\n\nLexical was built from the ground up to be a lean and extensible text-editing framework.\n\nYou can think of Lexical as more of a text editing engine, rather than a monolithic text\neditor that provides everything out-of-the-box. It might help to think of Lexical as\na framework like React – where React provides some hooks and a reconciler, and then you\nbuild the things you need on top, rather than have them included by default.\n\nThe main concept of Lexical is the editor state. Lexical uses a double-buffering technique\nto ensure consistency and reliability. There are never more than two editor states in play.\nThe \"current\" editor state represents what you can visibly see on screen and the \"pending\"\neditor state is what is currently being constructed to be shown in the future. Once the\n\"pending\" editor state is ready, it swaps and becomes the new \"current\" editor state.\n\nLexical's core concerns itself with four main concerns:\n\n- Updates: the act of making changes to editor state\n- Node Transforms: the process of acting on ongoing updates\n- Reconciliation: the process of patching the DOM with the latest editor state\n- Listening/Commands: the process of reacting to changes that occur internally\n\nAdditionally, Lexical uses DOM mutation observers to ensure that any outside changes to\nthe editor DOM element are either reverted back to Lexical's current editor state, or are\ncommunicated as intents that cause further updates to the editor state (text changes).\n\nThis separation was done to ensure developers have a way of applying custom logic with the\nimplementation and framework of their choice. This makes it possible to use Lexical with\nReact, or any other JavaScript web framework/library. This also makes it easier to tackle\nproblems around collaboration and undo/redo, where you might have an alternative model involved.\nInstead of hoping for the text editor to support what you want, you can work with the editor\nto make what you want.\n\nThis design can make getting started a bit more complex in certain cases, but as we've been\nable to demonstrate with our `@lexical/react` plugins and hooks – you can get started, in the\nform of an out-of-the-box experience.\n\n<!-- File: packages/lexical-website/docs/error.md (Tokens: ~31 | Category: anthropic-skill) -->\n---\nid: error\ntitle: Error Code\n---\nimport ErrorCodePage from '@site/src/components/ErrorCodePage';\n\n# Error Code\n<ErrorCodePage/>\n\n<!-- File: packages/lexical-website/docs/extensions/defining-extensions.md (Tokens: ~2269 | Category: anthropic-skill) -->\n# Defining Extensions\n\nAn extension is an plain JavaScript object that complies with the\n[LexicalExtension](/docs/api/modules/lexical#lexicalextension) interface.\nThe [defineExtension](/docs/api/modules/lexical#defineextension) function\nis merely an identity function to make TypeScript inference convenient.\nIt will be compiled down to the following, and likely optimized out by your\nbundler or minifier:\n\n```js\nfunction defineExtension(extension) {\n  return extension;\n}\n```\n\nExtensions *must* be stable references. It is most useful to define them at\nmodule scope, but if for some reason you have to define it inside of a\nReact component you'll need some way to make it stable such as with\n`useState`, `useMemo`, or `useRef` as appropriate.\n\n## Required Properties\n\n### name\n\nThe only required property for an extension is `name`. The name is a string\nthat should be unique to that extension your editor. It is best practice to\nuse a scope for your project or organization to ensure that reusable\nextensions don't conflict with each other.\n\nThe general practice in the lexical repository is to use the name of the\npackage as the extension name when it only makes sense to export a single\nextension, (e.g. `DragonExtension` has the name `@lexical/dragon`), or\nwith an additional path if multiple extensions are exported (e.g.\n`AutoFocusExtension` has the name `@lexical/extension/AutoFocus`).\n\n## InitialEditorConfig\n\nExtensions can specify configuration overrides for the editor, these are\ndefined by the\n[InitialEditorConfig](/docs/api/modules/lexical#initialeditorconfig)\ninterface. See the API docs for the full list of properties. Every property\nhere has some default.\n\n### Merged properties\n\nFor an extension designed to be used as a dependency, it can also be useful\nto specify properties that are merged such as\n`html` (override HTML import/export),\n`nodes` (register new nodes or node overrides with the editor),\nor `theme` (specify classes to be used for nodes provided by lexical).\n\n```ts\nexport const CodeExtension = defineExtension({\n  name: '@lexical/code',\n  nodes: () => [CodeNode, CodeHighlightNode],\n});\n```\n\n### Root properties\n\nThere are other properties that are more useful for the \"root\" extension\nthat you provide to\n[buildEditorFromExtensions](/docs/api/modules/lexical_extension#buildeditorfromextensions)\nor the `extension` prop of\n[LexicalExtensionComposer](/docs/api/modules/lexical_react_LexicalExtensionComposer#lexicalextensioncomposer)\nwhere you would specify properties that can only be meaningfully set\nonce such as `$initialEditorState`, `editable`, `onError`, `namespace` or `parentEditor`.\n\nThere is no requirement that these properties be set from any specific level\nof the extension hierarchy, or at all, but it is only meaningful to set them\nonce per editor.\n\n```ts\nconst editor = buildEditorFromExtensions(\n  defineExtension({\n    name: \"@example/basic-rich-text-editor\",\n    namespace: \"basic-rich-text-editor\",\n    dependencies: [RichTextExtension],\n    register: (editor: LexicalEditor) => {\n      console.log(\"Editor Created\");\n      return () => console.log(\"Editor Disposed\");\n    },\n  }),\n);\n```\n\n## Extension Dependencies\n\nLexical Extensions have two ways for extensions to depend on and specify\nconfiguration for each other:\n\n- [dependencies](#dependencies) are direct (by reference)\n- [peerDependencies](#peerdependencies) are indirect (by name) and optional\n\nThe exception to this rule is with `peerDependencies`. These are optional\ndependencies\n\n### dependencies\n\nThe `dependencies` property is an array of extensions by reference that your\nextension depends on. For example, if your extension uses React then it should\ndepend on the `ReactExtension`.\n\nDependencies form a directed acyclic graph. This means that there must not\nbe any circular dependencies. If extension A depends on extension B then\nthere must not be any dependency path from B to A\n(or from A to A).\n\nThis array can contain either direct references to extensions, or calls to\n[configExtension](/docs/api/modules/lexical#configextension).\n\n```ts\nexport const ExampleExtension = defineExtension({\n  name: \"@example/extension\",\n  dependencies: [\n    SomeExtension,\n    configExtension(ReactExtension, {\n      decorators: [<ExampleDecorator />]\n    }),\n  ],\n});\n```\n\n### peerDependencies\n\nThe `peerDependencies` property is an array of optional extensions by name.\nThey are not requirements, but specifying them here allows your extension\nto find them at runtime and override configuration for them if they are\nbuilt with the editor. You can use\n[declarePeerDependency](/docs/api/modules/lexical#declarepeerdependency) to\nbuild this array with type inference.\n\nSince these are optional and by name, an extension may declare a peer it does\nnot import. The graph must still be acyclic: peer edges take part in the same\ntopological sort as `dependencies`, so two extensions that peer-depend on each\nother and are both present will fail to build.\n\nThis is rarely needed in practice and is considered an advanced use case,\ntypically to avoid a direct import or dependency loop. See\n[Peer Dependencies](/docs/extensions/peer-dependencies) for more detail.\n\n### conflictsWith\n\nThe `conflictsWith` property is an array of extensions by name that are\nknown to conflict with this extension. For example, it does not make\nsense to have\n[RichTextExtension](/docs/api/modules/lexical_rich-text#richtextextension) and\n[PlainTextExtension](/docs/api/modules/lexical_plain-text#plaintextextension)\nin the same editor.\n\n```ts\nexport const PlainTextExtension = defineExtension({\n  conflictsWith: ['@lexical/rich-text'],\n  dependencies: [\n    DragonExtension,\n    NormalizeInlineElementsExtension,\n    NormalizeTripleClickSelectionExtension,\n  ],\n  name: '@lexical/plain-text',\n  register: registerPlainText,\n});\n```\n\nThis is rarely needed in practice and is considered an advanced use case,\nbut it can provide helpful early errors when it is needed.\n\n## Phases\n\nThe construction of an editor using extensions is done in sequential phases,\nand the following properties are useful ways to hook into that process at\nspecific points.\n\n### config\n\n`config` is an object that is the default configuration for your extension,\nproperties of this object can be overridden by other extensions using\n[configExtension](/docs/api/modules/lexical#configextension) or\n[declarePeerDependency](/docs/api/modules/lexical#declarepeerdependency).\n\nThis object is used in later phases to build the init and/or output.\n\nThe config phase happens before the editor is constructed.\n\n### mergeConfig\n\n`mergeConfig` is a function that can be used to merge configuration\nwith its overrides if you need something more fine-grained than a\nshallow object merge, such as concatenating arrays. For example:\n\n```ts\ninterface StringArrayConfig {\n  array: string[];\n}\nconst StringArrayExtension = defineExtension({\n  config: safeCast<StringArrayConfig>({array: []}),\n  name: \"@example/StringArray\",\n  mergeConfig(a, b) {\n    const config = shallowMergeConfig(a, b);\n    if (b.array) {\n      config.array =\n        b.array.length > 0\n          ? [...a.array, ...b.array]\n          : a.array;\n    }\n    return config;\n  },\n});\n```\n\nThe default implementation is\n[shallowMergeConfig](/docs/api/modules/lexical#shallowmergeconfig),\nmost extensions will not need to override this.\n\n### init\n\nThe init phase happens before the editor is constructed, but after\nthe merged editor configuration is available and all extensions are\nconfigured. It can be used to reference configuration\nfrom peers, compute data that should be available during\n[build](#build), and is generally a last resort for making mutations\nto any extension or editor configuration before the editor is\ncreated.\n\nThe result of this function is available in later phases.\n\nThis is rarely needed in practice and is considered an advanced use case.\n\n### build\n\nThe build phase happens after the editor is constructed — it receives the\n`LexicalEditor` — but before [register](#register), and after\n[config](#config) and [init](#init). The return value of the `build`\nphase is `output` and is available for later phases.\n\n`output` is generally how extensions provide functionality to each\nother and to the nodes in your application. A very common use case is\nto build signals from configuration with `namedSignals` so that the\nbehavior of your extension can be modified at runtime (e.g. `disabled`\nis a very common use case).\n\nOther use cases for build's `output` are to provide\nshared data structures, typed theme configuration, references to the\ncommands that the extension implements, etc.\n\n```ts\nexport interface TabIndentationConfig {\n  disabled: boolean;\n  maxIndent: null | number;\n  /**\n   * By default, indents are set on all elements for which the\n   * {@link ElementNode.canIndent} returns true. This option allows you to set\n   * indents for specific nodes without overriding the method for others.\n   */\n  $canIndent: CanIndentPredicate;\n}\nexport const TabIndentationExtension = defineExtension({\n  build(editor, config, state) {\n    return namedSignals(config);\n  },\n  config: safeCast<TabIndentationConfig>({\n    $canIndent: $defaultCanIndent,\n    disabled: false,\n    maxIndent: null,\n  }),\n  name: '@lexical/extension/TabIndentation',\n  register(editor, config, state) {\n    const {disabled, maxIndent, $canIndent} = state.getOutput();\n    return effect(() => {\n      if (!disabled.value) {\n        return registerTabIndentation(editor, maxIndent, $canIndent);\n      }\n    });\n  },\n});\n```\n\n### register\n\nThis happens after the editor has been constructed. This is where you will\nregister any commands, listeners, etc. that your extension needs. It can use\nthe result of `init` or `build` via `state.getInitResult()` and\n`state.getOutput()` respectively.\n\nThe return value is a dispose function, typically the result of `mergeRegister`.\n\n### afterRegistration\n\nThis happens after [register](#register) for every extension has been called,\nso all commands should be registered. This is the phase when the\n`$initialEditorState` is applied to the editor by `InitialStateExtension`,\nso `editor.setRootElement` should be called no sooner than this phase (it may\nof course be called after the editor is built, outside of extensions).\n\nThe return value is a dispose function, typically the result of\n`mergeRegister`.\n\n<!-- File: packages/lexical-website/docs/extensions/design.md (Tokens: ~1049 | Category: anthropic-skill) -->\n# Design Doc\n\nSee also the initial PR [#7706](https://github.com/facebook/lexical/pull/7706)\n\n## Guiding principles\n\n### Minimize boilerplate\n\nWherever possible, sensible defaults should be provided and it must be\npossible to override them. This is why a `Extension` has the `config` property\nwhich specifies the initial configuration.\n\n### Full backwards compatibility\n\nAlmost anything that you do with the editor or a legacy React plug-in should\njust work. Upgrading to use a `Extension` should be a simple change.\n\n### Minimize change required to add an Extension\n\nIdeally, adding an extension should be two or three lines of code:\n\n- Import it\n- Add it as a dependency (with config if necessary)\n- Add UI related code (only if needed, e.g. has to be rendered in a specific place in the DOM)\n\n### Avoid invalid states\n\nTypeScript goes a long way here, but it's only as good as the API design.\nFor example, splitting up the workflow for an Extension to have separate `Config`,\n`Init` and `Output` types was something that evolved over time. In earlier\nprototypes, `Config` was overloaded to maintain all of that state, but it\nrequired a lot more runtime support and type assertions to check that\nthe state was properly initialized for that phase.\n\n### Try to avoid undecipherable type errors\n\nThere's only so much that can be done to make readable error messages\nout of complex type expressions, so the API is designed to avoid them\nwhen possible. See also [microsoft/TypeScript#23689](https://github.com/microsoft/TypeScript/issues/23689).\n\n## Inspiration\n\n### [ESLint Flat Plugins](https://eslint.org/docs/latest/extend/plugin-migration-flat-config)\n\nThese have many of the same constraints as this project, although of course\nare used for a very different purpose\n\n### [MDXEditor](https://mdxeditor.dev/)\n\nThis editor is based on Lexical, but provides its own mechanism for plugins\nbased on a third party state management library. These plugins don't\nreally expose any type-safe configuration.\n\n### [@payloadcms/richtext-lexical](https://www.npmjs.com/package/@payloadcms/richtext-lexical)\n\nAnother Lexical based editor framework for use inside the\n[Payload](https://payloadcms.com/) CMS. It also doesn't really expose type-safe\nconfiguration, but it does seem to have quite a lot of features and seems\nwell-designed for laziness and SSR. Features are conceptually similar to Extensions.\n\n### [Effect](https://effect.website/)\n\nThis library is pretty much state of the art for type safety with a focus on\ncomposition and usability.\n\n### Tighter integration with LexicalEditor\n\nThis package was originally prototyped as an entirely optional feature,\nas adoption increases it's expected that more of Lexical will directly\ndepend on it.\n\n## Intentional trade-offs\n\n### No compile-time support for dependency resolution\n\nThe current theory is that it would require too much TypeScript\nin order to carry around the list of all dependency names that\nexist in the graph, and would likely add another type argument\nto `LexicalExtension`.\n\nThe features that this blocks are:\n\n- Compile-time support for detecting Extension conflicts. Detecting that two\n  extensions defined with the same name (but are not identical object\n  references, because a dependency can be shared!) would also be quite the\n  undertaking in TypeScript. This is automatically detected at runtime by\n  the builder.\n- Compile-time support for required configuration without defaults.\n  An extension can implement this at runtime in `init` or `register`.\n- Compile-time support for required peer dependencies. A use case for this\n  would be the requirement of a `ReactProviderExtension` provided by either\n  `LexicalExtensionComposer` or `ReactPluginHostExtension`. An extension can implement this\n  at runtime in `init` or `register`.\n\nGenerally speaking, all of these are already surfaced as runtime errors\nwhile building the editor with sufficient information to quickly track\ndown the root cause.\n\n## Known Missing Features\n\n### Direct support for devtools\n\nThis is a TODO, the infrastructure was designed with this in mind.\n\n### Documented patterns for RSC/SSR/Headless\n\nHaving a known peer dependency that is used to declare SSR may help\nguide future Extension development. In many cases there are decorator nodes\nthat have React dependencies that you do not want to (or can not) render\nin an SSR context. For example, RSC can not be supported anywhere that\n`React.Context` is used which is everywhere that React is currently used\nin Lexical.\n\nAnother option would be to build separate entrypoints for the SSR use case,\nwhich is more of an RSC specific strategy, but we could encourage people\nto use those import conditions even in a Vanilla JS or some other bespoke\nSSR use case. That strategy may make it hard to support including both\n\"headless\" and \"non-headless\" in the same module.\n\n<!-- ... +66 more modular skills available via /api/skills/facebook/lexical?path={path} -->\n</yakaai_skills>"}