1227 lines
		
	
	
		
			36 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
			
		
		
	
	
			1227 lines
		
	
	
		
			36 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
| /**
 | |
|  * Copyright (c) Meta Platforms, Inc. and affiliates.
 | |
|  *
 | |
|  * This source code is licensed under the MIT license found in the
 | |
|  * LICENSE file in the root directory of this source tree.
 | |
|  *
 | |
|  */
 | |
| 
 | |
| /* eslint-disable no-constant-condition */
 | |
| import type {EditorConfig, LexicalEditor} from './LexicalEditor';
 | |
| import type {BaseSelection, RangeSelection} from './LexicalSelection';
 | |
| import type {Klass, KlassConstructor} from 'lexical';
 | |
| 
 | |
| import invariant from 'lexical/shared/invariant';
 | |
| 
 | |
| import {
 | |
|   $createParagraphNode,
 | |
|   $isDecoratorNode,
 | |
|   $isElementNode,
 | |
|   $isRootNode,
 | |
|   $isTextNode,
 | |
|   type DecoratorNode,
 | |
|   ElementNode,
 | |
| } from '.';
 | |
| import {
 | |
|   $getSelection,
 | |
|   $isNodeSelection,
 | |
|   $isRangeSelection,
 | |
|   $moveSelectionPointToEnd,
 | |
|   $updateElementSelectionOnCreateDeleteNode,
 | |
|   moveSelectionPointToSibling,
 | |
| } from './LexicalSelection';
 | |
| import {
 | |
|   errorOnReadOnly,
 | |
|   getActiveEditor,
 | |
|   getActiveEditorState,
 | |
| } from './LexicalUpdates';
 | |
| import {
 | |
|   $cloneWithProperties,
 | |
|   $getCompositionKey,
 | |
|   $getNodeByKey,
 | |
|   $isRootOrShadowRoot,
 | |
|   $maybeMoveChildrenSelectionToParent,
 | |
|   $setCompositionKey,
 | |
|   $setNodeKey,
 | |
|   $setSelection,
 | |
|   errorOnInsertTextNodeOnRoot,
 | |
|   internalMarkNodeAsDirty,
 | |
|   removeFromParent,
 | |
| } from './LexicalUtils';
 | |
| 
 | |
| export type NodeMap = Map<NodeKey, LexicalNode>;
 | |
| 
 | |
| export type SerializedLexicalNode = {
 | |
|   type: string;
 | |
|   version: number;
 | |
| };
 | |
| 
 | |
| export function $removeNode(
 | |
|   nodeToRemove: LexicalNode,
 | |
|   restoreSelection: boolean,
 | |
|   preserveEmptyParent?: boolean,
 | |
| ): void {
 | |
|   errorOnReadOnly();
 | |
|   const key = nodeToRemove.__key;
 | |
|   const parent = nodeToRemove.getParent();
 | |
|   if (parent === null) {
 | |
|     return;
 | |
|   }
 | |
|   const selection = $maybeMoveChildrenSelectionToParent(nodeToRemove);
 | |
|   let selectionMoved = false;
 | |
|   if ($isRangeSelection(selection) && restoreSelection) {
 | |
|     const anchor = selection.anchor;
 | |
|     const focus = selection.focus;
 | |
|     if (anchor.key === key) {
 | |
|       moveSelectionPointToSibling(
 | |
|         anchor,
 | |
|         nodeToRemove,
 | |
|         parent,
 | |
|         nodeToRemove.getPreviousSibling(),
 | |
|         nodeToRemove.getNextSibling(),
 | |
|       );
 | |
|       selectionMoved = true;
 | |
|     }
 | |
|     if (focus.key === key) {
 | |
|       moveSelectionPointToSibling(
 | |
|         focus,
 | |
|         nodeToRemove,
 | |
|         parent,
 | |
|         nodeToRemove.getPreviousSibling(),
 | |
|         nodeToRemove.getNextSibling(),
 | |
|       );
 | |
|       selectionMoved = true;
 | |
|     }
 | |
|   } else if (
 | |
|     $isNodeSelection(selection) &&
 | |
|     restoreSelection &&
 | |
|     nodeToRemove.isSelected()
 | |
|   ) {
 | |
|     nodeToRemove.selectPrevious();
 | |
|   }
 | |
| 
 | |
|   if ($isRangeSelection(selection) && restoreSelection && !selectionMoved) {
 | |
|     // Doing this is O(n) so lets avoid it unless we need to do it
 | |
|     const index = nodeToRemove.getIndexWithinParent();
 | |
|     removeFromParent(nodeToRemove);
 | |
|     $updateElementSelectionOnCreateDeleteNode(selection, parent, index, -1);
 | |
|   } else {
 | |
|     removeFromParent(nodeToRemove);
 | |
|   }
 | |
| 
 | |
|   if (
 | |
|     !preserveEmptyParent &&
 | |
|     !$isRootOrShadowRoot(parent) &&
 | |
|     !parent.canBeEmpty() &&
 | |
|     parent.isEmpty()
 | |
|   ) {
 | |
|     $removeNode(parent, restoreSelection);
 | |
|   }
 | |
|   if (restoreSelection && $isRootNode(parent) && parent.isEmpty()) {
 | |
|     parent.selectEnd();
 | |
|   }
 | |
| }
 | |
| 
 | |
| export type DOMConversion<T extends HTMLElement = HTMLElement> = {
 | |
|   conversion: DOMConversionFn<T>;
 | |
|   priority?: 0 | 1 | 2 | 3 | 4;
 | |
| };
 | |
| 
 | |
| export type DOMConversionFn<T extends HTMLElement = HTMLElement> = (
 | |
|   element: T,
 | |
| ) => DOMConversionOutput | null;
 | |
| 
 | |
| export type DOMChildConversion = (
 | |
|   lexicalNode: LexicalNode,
 | |
|   parentLexicalNode: LexicalNode | null | undefined,
 | |
| ) => LexicalNode | null | undefined;
 | |
| 
 | |
| export type DOMConversionMap<T extends HTMLElement = HTMLElement> = Record<
 | |
|   NodeName,
 | |
|   (node: T) => DOMConversion<T> | null
 | |
| >;
 | |
| type NodeName = string;
 | |
| 
 | |
| /**
 | |
|  * Output for a DOM conversion.
 | |
|  * Node can be set to 'ignore' to ignore the conversion and handling of the DOMNode
 | |
|  * including all its children.
 | |
|  */
 | |
| export type DOMConversionOutput = {
 | |
|   after?: (childLexicalNodes: Array<LexicalNode>) => Array<LexicalNode>;
 | |
|   forChild?: DOMChildConversion;
 | |
|   node: null | LexicalNode | Array<LexicalNode> | 'ignore';
 | |
| };
 | |
| 
 | |
| export type DOMExportOutputMap = Map<
 | |
|   Klass<LexicalNode>,
 | |
|   (editor: LexicalEditor, target: LexicalNode) => DOMExportOutput
 | |
| >;
 | |
| 
 | |
| export type DOMExportOutput = {
 | |
|   after?: (
 | |
|     generatedElement: HTMLElement | Text | null | undefined,
 | |
|   ) => HTMLElement | Text | null | undefined;
 | |
|   element: HTMLElement | Text | null;
 | |
| };
 | |
| 
 | |
| export type NodeKey = string;
 | |
| 
 | |
| export class LexicalNode {
 | |
|   // Allow us to look up the type including static props
 | |
|   ['constructor']!: KlassConstructor<typeof LexicalNode>;
 | |
|   /** @internal */
 | |
|   __type: string;
 | |
|   /** @internal */
 | |
|   //@ts-ignore We set the key in the constructor.
 | |
|   __key: string;
 | |
|   /** @internal */
 | |
|   __parent: null | NodeKey;
 | |
|   /** @internal */
 | |
|   __prev: null | NodeKey;
 | |
|   /** @internal */
 | |
|   __next: null | NodeKey;
 | |
| 
 | |
|   // Flow doesn't support abstract classes unfortunately, so we can't _force_
 | |
|   // subclasses of Node to implement statics. All subclasses of Node should have
 | |
|   // a static getType and clone method though. We define getType and clone here so we can call it
 | |
|   // on any  Node, and we throw this error by default since the subclass should provide
 | |
|   // their own implementation.
 | |
|   /**
 | |
|    * Returns the string type of this node. Every node must
 | |
|    * implement this and it MUST BE UNIQUE amongst nodes registered
 | |
|    * on the editor.
 | |
|    *
 | |
|    */
 | |
|   static getType(): string {
 | |
|     invariant(
 | |
|       false,
 | |
|       'LexicalNode: Node %s does not implement .getType().',
 | |
|       this.name,
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Clones this node, creating a new node with a different key
 | |
|    * and adding it to the EditorState (but not attaching it anywhere!). All nodes must
 | |
|    * implement this method.
 | |
|    *
 | |
|    */
 | |
|   static clone(_data: unknown): LexicalNode {
 | |
|     invariant(
 | |
|       false,
 | |
|       'LexicalNode: Node %s does not implement .clone().',
 | |
|       this.name,
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Perform any state updates on the clone of prevNode that are not already
 | |
|    * handled by the constructor call in the static clone method. If you have
 | |
|    * state to update in your clone that is not handled directly by the
 | |
|    * constructor, it is advisable to override this method but it is required
 | |
|    * to include a call to `super.afterCloneFrom(prevNode)` in your
 | |
|    * implementation. This is only intended to be called by
 | |
|    * {@link $cloneWithProperties} function or via a super call.
 | |
|    *
 | |
|    * @example
 | |
|    * ```ts
 | |
|    * class ClassesTextNode extends TextNode {
 | |
|    *   // Not shown: static getType, static importJSON, exportJSON, createDOM, updateDOM
 | |
|    *   __classes = new Set<string>();
 | |
|    *   static clone(node: ClassesTextNode): ClassesTextNode {
 | |
|    *     // The inherited TextNode constructor is used here, so
 | |
|    *     // classes is not set by this method.
 | |
|    *     return new ClassesTextNode(node.__text, node.__key);
 | |
|    *   }
 | |
|    *   afterCloneFrom(node: this): void {
 | |
|    *     // This calls TextNode.afterCloneFrom and LexicalNode.afterCloneFrom
 | |
|    *     // for necessary state updates
 | |
|    *     super.afterCloneFrom(node);
 | |
|    *     this.__addClasses(node.__classes);
 | |
|    *   }
 | |
|    *   // This method is a private implementation detail, it is not
 | |
|    *   // suitable for the public API because it does not call getWritable
 | |
|    *   __addClasses(classNames: Iterable<string>): this {
 | |
|    *     for (const className of classNames) {
 | |
|    *       this.__classes.add(className);
 | |
|    *     }
 | |
|    *     return this;
 | |
|    *   }
 | |
|    *   addClass(...classNames: string[]): this {
 | |
|    *     return this.getWritable().__addClasses(classNames);
 | |
|    *   }
 | |
|    *   removeClass(...classNames: string[]): this {
 | |
|    *     const node = this.getWritable();
 | |
|    *     for (const className of classNames) {
 | |
|    *       this.__classes.delete(className);
 | |
|    *     }
 | |
|    *     return this;
 | |
|    *   }
 | |
|    *   getClasses(): Set<string> {
 | |
|    *     return this.getLatest().__classes;
 | |
|    *   }
 | |
|    * }
 | |
|    * ```
 | |
|    *
 | |
|    */
 | |
|   afterCloneFrom(prevNode: this) {
 | |
|     this.__parent = prevNode.__parent;
 | |
|     this.__next = prevNode.__next;
 | |
|     this.__prev = prevNode.__prev;
 | |
|   }
 | |
| 
 | |
|   // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | |
|   static importDOM?: () => DOMConversionMap<any> | null;
 | |
| 
 | |
|   constructor(key?: NodeKey) {
 | |
|     this.__type = this.constructor.getType();
 | |
|     this.__parent = null;
 | |
|     this.__prev = null;
 | |
|     this.__next = null;
 | |
|     $setNodeKey(this, key);
 | |
| 
 | |
|     if (__DEV__) {
 | |
|       if (this.__type !== 'root') {
 | |
|         errorOnReadOnly();
 | |
|         errorOnTypeKlassMismatch(this.__type, this.constructor);
 | |
|       }
 | |
|     }
 | |
|   }
 | |
|   // Getters and Traversers
 | |
| 
 | |
|   /**
 | |
|    * Returns the string type of this node.
 | |
|    */
 | |
|   getType(): string {
 | |
|     return this.__type;
 | |
|   }
 | |
| 
 | |
|   isInline(): boolean {
 | |
|     invariant(
 | |
|       false,
 | |
|       'LexicalNode: Node %s does not implement .isInline().',
 | |
|       this.constructor.name,
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Returns true if there is a path between this node and the RootNode, false otherwise.
 | |
|    * This is a way of determining if the node is "attached" EditorState. Unattached nodes
 | |
|    * won't be reconciled and will ultimatelt be cleaned up by the Lexical GC.
 | |
|    */
 | |
|   isAttached(): boolean {
 | |
|     let nodeKey: string | null = this.__key;
 | |
|     while (nodeKey !== null) {
 | |
|       if (nodeKey === 'root') {
 | |
|         return true;
 | |
|       }
 | |
| 
 | |
|       const node: LexicalNode | null = $getNodeByKey(nodeKey);
 | |
| 
 | |
|       if (node === null) {
 | |
|         break;
 | |
|       }
 | |
|       nodeKey = node.__parent;
 | |
|     }
 | |
|     return false;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Returns true if this node is contained within the provided Selection., false otherwise.
 | |
|    * Relies on the algorithms implemented in {@link BaseSelection.getNodes} to determine
 | |
|    * what's included.
 | |
|    *
 | |
|    * @param selection - The selection that we want to determine if the node is in.
 | |
|    */
 | |
|   isSelected(selection?: null | BaseSelection): boolean {
 | |
|     const targetSelection = selection || $getSelection();
 | |
|     if (targetSelection == null) {
 | |
|       return false;
 | |
|     }
 | |
| 
 | |
|     const isSelected = targetSelection
 | |
|       .getNodes()
 | |
|       .some((n) => n.__key === this.__key);
 | |
| 
 | |
|     if ($isTextNode(this)) {
 | |
|       return isSelected;
 | |
|     }
 | |
|     // For inline images inside of element nodes.
 | |
|     // Without this change the image will be selected if the cursor is before or after it.
 | |
|     const isElementRangeSelection =
 | |
|       $isRangeSelection(targetSelection) &&
 | |
|       targetSelection.anchor.type === 'element' &&
 | |
|       targetSelection.focus.type === 'element';
 | |
| 
 | |
|     if (isElementRangeSelection) {
 | |
|       if (targetSelection.isCollapsed()) {
 | |
|         return false;
 | |
|       }
 | |
| 
 | |
|       const parentNode = this.getParent();
 | |
|       if ($isDecoratorNode(this) && this.isInline() && parentNode) {
 | |
|         const firstPoint = targetSelection.isBackward()
 | |
|           ? targetSelection.focus
 | |
|           : targetSelection.anchor;
 | |
|         const firstElement = firstPoint.getNode() as ElementNode;
 | |
|         if (
 | |
|           firstPoint.offset === firstElement.getChildrenSize() &&
 | |
|           firstElement.is(parentNode) &&
 | |
|           firstElement.getLastChildOrThrow().is(this)
 | |
|         ) {
 | |
|           return false;
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|     return isSelected;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Returns this nodes key.
 | |
|    */
 | |
|   getKey(): NodeKey {
 | |
|     // Key is stable between copies
 | |
|     return this.__key;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Returns the zero-based index of this node within the parent.
 | |
|    */
 | |
|   getIndexWithinParent(): number {
 | |
|     const parent = this.getParent();
 | |
|     if (parent === null) {
 | |
|       return -1;
 | |
|     }
 | |
|     let node = parent.getFirstChild();
 | |
|     let index = 0;
 | |
|     while (node !== null) {
 | |
|       if (this.is(node)) {
 | |
|         return index;
 | |
|       }
 | |
|       index++;
 | |
|       node = node.getNextSibling();
 | |
|     }
 | |
|     return -1;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Returns the parent of this node, or null if none is found.
 | |
|    */
 | |
|   getParent<T extends ElementNode>(): T | null {
 | |
|     const parent = this.getLatest().__parent;
 | |
|     if (parent === null) {
 | |
|       return null;
 | |
|     }
 | |
|     return $getNodeByKey<T>(parent);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Returns the parent of this node, or throws if none is found.
 | |
|    */
 | |
|   getParentOrThrow<T extends ElementNode>(): T {
 | |
|     const parent = this.getParent<T>();
 | |
|     if (parent === null) {
 | |
|       invariant(false, 'Expected node %s to have a parent.', this.__key);
 | |
|     }
 | |
|     return parent;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Returns the highest (in the EditorState tree)
 | |
|    * non-root ancestor of this node, or null if none is found. See {@link lexical!$isRootOrShadowRoot}
 | |
|    * for more information on which Elements comprise "roots".
 | |
|    */
 | |
|   getTopLevelElement(): ElementNode | DecoratorNode<unknown> | null {
 | |
|     let node: ElementNode | this | null = this;
 | |
|     while (node !== null) {
 | |
|       const parent: ElementNode | null = node.getParent();
 | |
|       if ($isRootOrShadowRoot(parent)) {
 | |
|         invariant(
 | |
|           $isElementNode(node) || (node === this && $isDecoratorNode(node)),
 | |
|           'Children of root nodes must be elements or decorators',
 | |
|         );
 | |
|         return node;
 | |
|       }
 | |
|       node = parent;
 | |
|     }
 | |
|     return null;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Returns the highest (in the EditorState tree)
 | |
|    * non-root ancestor of this node, or throws if none is found. See {@link lexical!$isRootOrShadowRoot}
 | |
|    * for more information on which Elements comprise "roots".
 | |
|    */
 | |
|   getTopLevelElementOrThrow(): ElementNode | DecoratorNode<unknown> {
 | |
|     const parent = this.getTopLevelElement();
 | |
|     if (parent === null) {
 | |
|       invariant(
 | |
|         false,
 | |
|         'Expected node %s to have a top parent element.',
 | |
|         this.__key,
 | |
|       );
 | |
|     }
 | |
|     return parent;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Returns a list of the every ancestor of this node,
 | |
|    * all the way up to the RootNode.
 | |
|    *
 | |
|    */
 | |
|   getParents(): Array<ElementNode> {
 | |
|     const parents: Array<ElementNode> = [];
 | |
|     let node = this.getParent();
 | |
|     while (node !== null) {
 | |
|       parents.push(node);
 | |
|       node = node.getParent();
 | |
|     }
 | |
|     return parents;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Returns a list of the keys of every ancestor of this node,
 | |
|    * all the way up to the RootNode.
 | |
|    *
 | |
|    */
 | |
|   getParentKeys(): Array<NodeKey> {
 | |
|     const parents = [];
 | |
|     let node = this.getParent();
 | |
|     while (node !== null) {
 | |
|       parents.push(node.__key);
 | |
|       node = node.getParent();
 | |
|     }
 | |
|     return parents;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Returns the "previous" siblings - that is, the node that comes
 | |
|    * before this one in the same parent.
 | |
|    *
 | |
|    */
 | |
|   getPreviousSibling<T extends LexicalNode>(): T | null {
 | |
|     const self = this.getLatest();
 | |
|     const prevKey = self.__prev;
 | |
|     return prevKey === null ? null : $getNodeByKey<T>(prevKey);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Returns the "previous" siblings - that is, the nodes that come between
 | |
|    * this one and the first child of it's parent, inclusive.
 | |
|    *
 | |
|    */
 | |
|   getPreviousSiblings<T extends LexicalNode>(): Array<T> {
 | |
|     const siblings: Array<T> = [];
 | |
|     const parent = this.getParent();
 | |
|     if (parent === null) {
 | |
|       return siblings;
 | |
|     }
 | |
|     let node: null | T = parent.getFirstChild();
 | |
|     while (node !== null) {
 | |
|       if (node.is(this)) {
 | |
|         break;
 | |
|       }
 | |
|       siblings.push(node);
 | |
|       node = node.getNextSibling();
 | |
|     }
 | |
|     return siblings;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Returns the "next" siblings - that is, the node that comes
 | |
|    * after this one in the same parent
 | |
|    *
 | |
|    */
 | |
|   getNextSibling<T extends LexicalNode>(): T | null {
 | |
|     const self = this.getLatest();
 | |
|     const nextKey = self.__next;
 | |
|     return nextKey === null ? null : $getNodeByKey<T>(nextKey);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Returns all "next" siblings - that is, the nodes that come between this
 | |
|    * one and the last child of it's parent, inclusive.
 | |
|    *
 | |
|    */
 | |
|   getNextSiblings<T extends LexicalNode>(): Array<T> {
 | |
|     const siblings: Array<T> = [];
 | |
|     let node: null | T = this.getNextSibling();
 | |
|     while (node !== null) {
 | |
|       siblings.push(node);
 | |
|       node = node.getNextSibling();
 | |
|     }
 | |
|     return siblings;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Returns the closest common ancestor of this node and the provided one or null
 | |
|    * if one cannot be found.
 | |
|    *
 | |
|    * @param node - the other node to find the common ancestor of.
 | |
|    */
 | |
|   getCommonAncestor<T extends ElementNode = ElementNode>(
 | |
|     node: LexicalNode,
 | |
|   ): T | null {
 | |
|     const a = this.getParents();
 | |
|     const b = node.getParents();
 | |
|     if ($isElementNode(this)) {
 | |
|       a.unshift(this);
 | |
|     }
 | |
|     if ($isElementNode(node)) {
 | |
|       b.unshift(node);
 | |
|     }
 | |
|     const aLength = a.length;
 | |
|     const bLength = b.length;
 | |
|     if (aLength === 0 || bLength === 0 || a[aLength - 1] !== b[bLength - 1]) {
 | |
|       return null;
 | |
|     }
 | |
|     const bSet = new Set(b);
 | |
|     for (let i = 0; i < aLength; i++) {
 | |
|       const ancestor = a[i] as T;
 | |
|       if (bSet.has(ancestor)) {
 | |
|         return ancestor;
 | |
|       }
 | |
|     }
 | |
|     return null;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Returns true if the provided node is the exact same one as this node, from Lexical's perspective.
 | |
|    * Always use this instead of referential equality.
 | |
|    *
 | |
|    * @param object - the node to perform the equality comparison on.
 | |
|    */
 | |
|   is(object: LexicalNode | null | undefined): boolean {
 | |
|     if (object == null) {
 | |
|       return false;
 | |
|     }
 | |
|     return this.__key === object.__key;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Returns true if this node logical precedes the target node in the editor state.
 | |
|    *
 | |
|    * @param targetNode - the node we're testing to see if it's after this one.
 | |
|    */
 | |
|   isBefore(targetNode: LexicalNode): boolean {
 | |
|     if (this === targetNode) {
 | |
|       return false;
 | |
|     }
 | |
|     if (targetNode.isParentOf(this)) {
 | |
|       return true;
 | |
|     }
 | |
|     if (this.isParentOf(targetNode)) {
 | |
|       return false;
 | |
|     }
 | |
|     const commonAncestor = this.getCommonAncestor(targetNode);
 | |
|     let indexA = 0;
 | |
|     let indexB = 0;
 | |
|     let node: this | ElementNode | LexicalNode = this;
 | |
|     while (true) {
 | |
|       const parent: ElementNode = node.getParentOrThrow();
 | |
|       if (parent === commonAncestor) {
 | |
|         indexA = node.getIndexWithinParent();
 | |
|         break;
 | |
|       }
 | |
|       node = parent;
 | |
|     }
 | |
|     node = targetNode;
 | |
|     while (true) {
 | |
|       const parent: ElementNode = node.getParentOrThrow();
 | |
|       if (parent === commonAncestor) {
 | |
|         indexB = node.getIndexWithinParent();
 | |
|         break;
 | |
|       }
 | |
|       node = parent;
 | |
|     }
 | |
|     return indexA < indexB;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Returns true if this node is the parent of the target node, false otherwise.
 | |
|    *
 | |
|    * @param targetNode - the would-be child node.
 | |
|    */
 | |
|   isParentOf(targetNode: LexicalNode): boolean {
 | |
|     const key = this.__key;
 | |
|     if (key === targetNode.__key) {
 | |
|       return false;
 | |
|     }
 | |
|     let node: ElementNode | LexicalNode | null = targetNode;
 | |
|     while (node !== null) {
 | |
|       if (node.__key === key) {
 | |
|         return true;
 | |
|       }
 | |
|       node = node.getParent();
 | |
|     }
 | |
|     return false;
 | |
|   }
 | |
| 
 | |
|   // TO-DO: this function can be simplified a lot
 | |
|   /**
 | |
|    * Returns a list of nodes that are between this node and
 | |
|    * the target node in the EditorState.
 | |
|    *
 | |
|    * @param targetNode - the node that marks the other end of the range of nodes to be returned.
 | |
|    */
 | |
|   getNodesBetween(targetNode: LexicalNode): Array<LexicalNode> {
 | |
|     const isBefore = this.isBefore(targetNode);
 | |
|     const nodes = [];
 | |
|     const visited = new Set();
 | |
|     let node: LexicalNode | this | null = this;
 | |
|     while (true) {
 | |
|       if (node === null) {
 | |
|         break;
 | |
|       }
 | |
|       const key = node.__key;
 | |
|       if (!visited.has(key)) {
 | |
|         visited.add(key);
 | |
|         nodes.push(node);
 | |
|       }
 | |
|       if (node === targetNode) {
 | |
|         break;
 | |
|       }
 | |
|       const child: LexicalNode | null = $isElementNode(node)
 | |
|         ? isBefore
 | |
|           ? node.getFirstChild()
 | |
|           : node.getLastChild()
 | |
|         : null;
 | |
|       if (child !== null) {
 | |
|         node = child;
 | |
|         continue;
 | |
|       }
 | |
|       const nextSibling: LexicalNode | null = isBefore
 | |
|         ? node.getNextSibling()
 | |
|         : node.getPreviousSibling();
 | |
|       if (nextSibling !== null) {
 | |
|         node = nextSibling;
 | |
|         continue;
 | |
|       }
 | |
|       const parent: LexicalNode | null = node.getParentOrThrow();
 | |
|       if (!visited.has(parent.__key)) {
 | |
|         nodes.push(parent);
 | |
|       }
 | |
|       if (parent === targetNode) {
 | |
|         break;
 | |
|       }
 | |
|       let parentSibling = null;
 | |
|       let ancestor: LexicalNode | null = parent;
 | |
|       do {
 | |
|         if (ancestor === null) {
 | |
|           invariant(false, 'getNodesBetween: ancestor is null');
 | |
|         }
 | |
|         parentSibling = isBefore
 | |
|           ? ancestor.getNextSibling()
 | |
|           : ancestor.getPreviousSibling();
 | |
|         ancestor = ancestor.getParent();
 | |
|         if (ancestor !== null) {
 | |
|           if (parentSibling === null && !visited.has(ancestor.__key)) {
 | |
|             nodes.push(ancestor);
 | |
|           }
 | |
|         } else {
 | |
|           break;
 | |
|         }
 | |
|       } while (parentSibling === null);
 | |
|       node = parentSibling;
 | |
|     }
 | |
|     if (!isBefore) {
 | |
|       nodes.reverse();
 | |
|     }
 | |
|     return nodes;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Returns true if this node has been marked dirty during this update cycle.
 | |
|    *
 | |
|    */
 | |
|   isDirty(): boolean {
 | |
|     const editor = getActiveEditor();
 | |
|     const dirtyLeaves = editor._dirtyLeaves;
 | |
|     return dirtyLeaves !== null && dirtyLeaves.has(this.__key);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Returns the latest version of the node from the active EditorState.
 | |
|    * This is used to avoid getting values from stale node references.
 | |
|    *
 | |
|    */
 | |
|   getLatest(): this {
 | |
|     const latest = $getNodeByKey<this>(this.__key);
 | |
|     if (latest === null) {
 | |
|       invariant(
 | |
|         false,
 | |
|         'Lexical node does not exist in active editor state. Avoid using the same node references between nested closures from editorState.read/editor.update.',
 | |
|       );
 | |
|     }
 | |
|     return latest;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Returns a mutable version of the node using {@link $cloneWithProperties}
 | |
|    * if necessary. Will throw an error if called outside of a Lexical Editor
 | |
|    * {@link LexicalEditor.update} callback.
 | |
|    *
 | |
|    */
 | |
|   getWritable(): this {
 | |
|     errorOnReadOnly();
 | |
|     const editorState = getActiveEditorState();
 | |
|     const editor = getActiveEditor();
 | |
|     const nodeMap = editorState._nodeMap;
 | |
|     const key = this.__key;
 | |
|     // Ensure we get the latest node from pending state
 | |
|     const latestNode = this.getLatest();
 | |
|     const cloneNotNeeded = editor._cloneNotNeeded;
 | |
|     const selection = $getSelection();
 | |
|     if (selection !== null) {
 | |
|       selection.setCachedNodes(null);
 | |
|     }
 | |
|     if (cloneNotNeeded.has(key)) {
 | |
|       // Transforms clear the dirty node set on each iteration to keep track on newly dirty nodes
 | |
|       internalMarkNodeAsDirty(latestNode);
 | |
|       return latestNode;
 | |
|     }
 | |
|     const mutableNode = $cloneWithProperties(latestNode);
 | |
|     cloneNotNeeded.add(key);
 | |
|     internalMarkNodeAsDirty(mutableNode);
 | |
|     // Update reference in node map
 | |
|     nodeMap.set(key, mutableNode);
 | |
| 
 | |
|     return mutableNode;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Returns the text content of the node. Override this for
 | |
|    * custom nodes that should have a representation in plain text
 | |
|    * format (for copy + paste, for example)
 | |
|    *
 | |
|    */
 | |
|   getTextContent(): string {
 | |
|     return '';
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Returns the length of the string produced by calling getTextContent on this node.
 | |
|    *
 | |
|    */
 | |
|   getTextContentSize(): number {
 | |
|     return this.getTextContent().length;
 | |
|   }
 | |
| 
 | |
|   // View
 | |
| 
 | |
|   /**
 | |
|    * Called during the reconciliation process to determine which nodes
 | |
|    * to insert into the DOM for this Lexical Node.
 | |
|    *
 | |
|    * This method must return exactly one HTMLElement. Nested elements are not supported.
 | |
|    *
 | |
|    * Do not attempt to update the Lexical EditorState during this phase of the update lifecyle.
 | |
|    *
 | |
|    * @param _config - allows access to things like the EditorTheme (to apply classes) during reconciliation.
 | |
|    * @param _editor - allows access to the editor for context during reconciliation.
 | |
|    *
 | |
|    * */
 | |
|   createDOM(_config: EditorConfig, _editor: LexicalEditor): HTMLElement {
 | |
|     invariant(false, 'createDOM: base method not extended');
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Called when a node changes and should update the DOM
 | |
|    * in whatever way is necessary to make it align with any changes that might
 | |
|    * have happened during the update.
 | |
|    *
 | |
|    * Returning "true" here will cause lexical to unmount and recreate the DOM node
 | |
|    * (by calling createDOM). You would need to do this if the element tag changes,
 | |
|    * for instance.
 | |
|    *
 | |
|    * */
 | |
|   updateDOM(
 | |
|     _prevNode: unknown,
 | |
|     _dom: HTMLElement,
 | |
|     _config: EditorConfig,
 | |
|   ): boolean {
 | |
|     invariant(false, 'updateDOM: base method not extended');
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Controls how the this node is serialized to HTML. This is important for
 | |
|    * copy and paste between Lexical and non-Lexical editors, or Lexical editors with different namespaces,
 | |
|    * in which case the primary transfer format is HTML. It's also important if you're serializing
 | |
|    * to HTML for any other reason via {@link @lexical/html!$generateHtmlFromNodes}. You could
 | |
|    * also use this method to build your own HTML renderer.
 | |
|    *
 | |
|    * */
 | |
|   exportDOM(editor: LexicalEditor): DOMExportOutput {
 | |
|     const element = this.createDOM(editor._config, editor);
 | |
|     return {element};
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Controls how the this node is serialized to JSON. This is important for
 | |
|    * copy and paste between Lexical editors sharing the same namespace. It's also important
 | |
|    * if you're serializing to JSON for persistent storage somewhere.
 | |
|    * See [Serialization & Deserialization](https://lexical.dev/docs/concepts/serialization#lexical---html).
 | |
|    *
 | |
|    * */
 | |
|   exportJSON(): SerializedLexicalNode {
 | |
|     invariant(false, 'exportJSON: base method not extended');
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Controls how the this node is deserialized from JSON. This is usually boilerplate,
 | |
|    * but provides an abstraction between the node implementation and serialized interface that can
 | |
|    * be important if you ever make breaking changes to a node schema (by adding or removing properties).
 | |
|    * See [Serialization & Deserialization](https://lexical.dev/docs/concepts/serialization#lexical---html).
 | |
|    *
 | |
|    * */
 | |
|   static importJSON(_serializedNode: SerializedLexicalNode): LexicalNode {
 | |
|     invariant(
 | |
|       false,
 | |
|       'LexicalNode: Node %s does not implement .importJSON().',
 | |
|       this.name,
 | |
|     );
 | |
|   }
 | |
|   /**
 | |
|    * @experimental
 | |
|    *
 | |
|    * Registers the returned function as a transform on the node during
 | |
|    * Editor initialization. Most such use cases should be addressed via
 | |
|    * the {@link LexicalEditor.registerNodeTransform} API.
 | |
|    *
 | |
|    * Experimental - use at your own risk.
 | |
|    */
 | |
|   static transform(): ((node: LexicalNode) => void) | null {
 | |
|     return null;
 | |
|   }
 | |
| 
 | |
|   // Setters and mutators
 | |
| 
 | |
|   /**
 | |
|    * Removes this LexicalNode from the EditorState. If the node isn't re-inserted
 | |
|    * somewhere, the Lexical garbage collector will eventually clean it up.
 | |
|    *
 | |
|    * @param preserveEmptyParent - If falsy, the node's parent will be removed if
 | |
|    * it's empty after the removal operation. This is the default behavior, subject to
 | |
|    * other node heuristics such as {@link ElementNode#canBeEmpty}
 | |
|    * */
 | |
|   remove(preserveEmptyParent?: boolean): void {
 | |
|     $removeNode(this, true, preserveEmptyParent);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Replaces this LexicalNode with the provided node, optionally transferring the children
 | |
|    * of the replaced node to the replacing node.
 | |
|    *
 | |
|    * @param replaceWith - The node to replace this one with.
 | |
|    * @param includeChildren - Whether or not to transfer the children of this node to the replacing node.
 | |
|    * */
 | |
|   replace<N extends LexicalNode>(replaceWith: N, includeChildren?: boolean): N {
 | |
|     errorOnReadOnly();
 | |
|     let selection = $getSelection();
 | |
|     if (selection !== null) {
 | |
|       selection = selection.clone();
 | |
|     }
 | |
|     errorOnInsertTextNodeOnRoot(this, replaceWith);
 | |
|     const self = this.getLatest();
 | |
|     const toReplaceKey = this.__key;
 | |
|     const key = replaceWith.__key;
 | |
|     const writableReplaceWith = replaceWith.getWritable();
 | |
|     const writableParent = this.getParentOrThrow().getWritable();
 | |
|     const size = writableParent.__size;
 | |
|     removeFromParent(writableReplaceWith);
 | |
|     const prevSibling = self.getPreviousSibling();
 | |
|     const nextSibling = self.getNextSibling();
 | |
|     const prevKey = self.__prev;
 | |
|     const nextKey = self.__next;
 | |
|     const parentKey = self.__parent;
 | |
|     $removeNode(self, false, true);
 | |
| 
 | |
|     if (prevSibling === null) {
 | |
|       writableParent.__first = key;
 | |
|     } else {
 | |
|       const writablePrevSibling = prevSibling.getWritable();
 | |
|       writablePrevSibling.__next = key;
 | |
|     }
 | |
|     writableReplaceWith.__prev = prevKey;
 | |
|     if (nextSibling === null) {
 | |
|       writableParent.__last = key;
 | |
|     } else {
 | |
|       const writableNextSibling = nextSibling.getWritable();
 | |
|       writableNextSibling.__prev = key;
 | |
|     }
 | |
|     writableReplaceWith.__next = nextKey;
 | |
|     writableReplaceWith.__parent = parentKey;
 | |
|     writableParent.__size = size;
 | |
|     if (includeChildren) {
 | |
|       invariant(
 | |
|         $isElementNode(this) && $isElementNode(writableReplaceWith),
 | |
|         'includeChildren should only be true for ElementNodes',
 | |
|       );
 | |
|       this.getChildren().forEach((child: LexicalNode) => {
 | |
|         writableReplaceWith.append(child);
 | |
|       });
 | |
|     }
 | |
|     if ($isRangeSelection(selection)) {
 | |
|       $setSelection(selection);
 | |
|       const anchor = selection.anchor;
 | |
|       const focus = selection.focus;
 | |
|       if (anchor.key === toReplaceKey) {
 | |
|         $moveSelectionPointToEnd(anchor, writableReplaceWith);
 | |
|       }
 | |
|       if (focus.key === toReplaceKey) {
 | |
|         $moveSelectionPointToEnd(focus, writableReplaceWith);
 | |
|       }
 | |
|     }
 | |
|     if ($getCompositionKey() === toReplaceKey) {
 | |
|       $setCompositionKey(key);
 | |
|     }
 | |
|     return writableReplaceWith;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Inserts a node after this LexicalNode (as the next sibling).
 | |
|    *
 | |
|    * @param nodeToInsert - The node to insert after this one.
 | |
|    * @param restoreSelection - Whether or not to attempt to resolve the
 | |
|    * selection to the appropriate place after the operation is complete.
 | |
|    * */
 | |
|   insertAfter(nodeToInsert: LexicalNode, restoreSelection = true): LexicalNode {
 | |
|     errorOnReadOnly();
 | |
|     errorOnInsertTextNodeOnRoot(this, nodeToInsert);
 | |
|     const writableSelf = this.getWritable();
 | |
|     const writableNodeToInsert = nodeToInsert.getWritable();
 | |
|     const oldParent = writableNodeToInsert.getParent();
 | |
|     const selection = $getSelection();
 | |
|     let elementAnchorSelectionOnNode = false;
 | |
|     let elementFocusSelectionOnNode = false;
 | |
|     if (oldParent !== null) {
 | |
|       // TODO: this is O(n), can we improve?
 | |
|       const oldIndex = nodeToInsert.getIndexWithinParent();
 | |
|       removeFromParent(writableNodeToInsert);
 | |
|       if ($isRangeSelection(selection)) {
 | |
|         const oldParentKey = oldParent.__key;
 | |
|         const anchor = selection.anchor;
 | |
|         const focus = selection.focus;
 | |
|         elementAnchorSelectionOnNode =
 | |
|           anchor.type === 'element' &&
 | |
|           anchor.key === oldParentKey &&
 | |
|           anchor.offset === oldIndex + 1;
 | |
|         elementFocusSelectionOnNode =
 | |
|           focus.type === 'element' &&
 | |
|           focus.key === oldParentKey &&
 | |
|           focus.offset === oldIndex + 1;
 | |
|       }
 | |
|     }
 | |
|     const nextSibling = this.getNextSibling();
 | |
|     const writableParent = this.getParentOrThrow().getWritable();
 | |
|     const insertKey = writableNodeToInsert.__key;
 | |
|     const nextKey = writableSelf.__next;
 | |
|     if (nextSibling === null) {
 | |
|       writableParent.__last = insertKey;
 | |
|     } else {
 | |
|       const writableNextSibling = nextSibling.getWritable();
 | |
|       writableNextSibling.__prev = insertKey;
 | |
|     }
 | |
|     writableParent.__size++;
 | |
|     writableSelf.__next = insertKey;
 | |
|     writableNodeToInsert.__next = nextKey;
 | |
|     writableNodeToInsert.__prev = writableSelf.__key;
 | |
|     writableNodeToInsert.__parent = writableSelf.__parent;
 | |
|     if (restoreSelection && $isRangeSelection(selection)) {
 | |
|       const index = this.getIndexWithinParent();
 | |
|       $updateElementSelectionOnCreateDeleteNode(
 | |
|         selection,
 | |
|         writableParent,
 | |
|         index + 1,
 | |
|       );
 | |
|       const writableParentKey = writableParent.__key;
 | |
|       if (elementAnchorSelectionOnNode) {
 | |
|         selection.anchor.set(writableParentKey, index + 2, 'element');
 | |
|       }
 | |
|       if (elementFocusSelectionOnNode) {
 | |
|         selection.focus.set(writableParentKey, index + 2, 'element');
 | |
|       }
 | |
|     }
 | |
|     return nodeToInsert;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Inserts a node before this LexicalNode (as the previous sibling).
 | |
|    *
 | |
|    * @param nodeToInsert - The node to insert before this one.
 | |
|    * @param restoreSelection - Whether or not to attempt to resolve the
 | |
|    * selection to the appropriate place after the operation is complete.
 | |
|    * */
 | |
|   insertBefore(
 | |
|     nodeToInsert: LexicalNode,
 | |
|     restoreSelection = true,
 | |
|   ): LexicalNode {
 | |
|     errorOnReadOnly();
 | |
|     errorOnInsertTextNodeOnRoot(this, nodeToInsert);
 | |
|     const writableSelf = this.getWritable();
 | |
|     const writableNodeToInsert = nodeToInsert.getWritable();
 | |
|     const insertKey = writableNodeToInsert.__key;
 | |
|     removeFromParent(writableNodeToInsert);
 | |
|     const prevSibling = this.getPreviousSibling();
 | |
|     const writableParent = this.getParentOrThrow().getWritable();
 | |
|     const prevKey = writableSelf.__prev;
 | |
|     // TODO: this is O(n), can we improve?
 | |
|     const index = this.getIndexWithinParent();
 | |
|     if (prevSibling === null) {
 | |
|       writableParent.__first = insertKey;
 | |
|     } else {
 | |
|       const writablePrevSibling = prevSibling.getWritable();
 | |
|       writablePrevSibling.__next = insertKey;
 | |
|     }
 | |
|     writableParent.__size++;
 | |
|     writableSelf.__prev = insertKey;
 | |
|     writableNodeToInsert.__prev = prevKey;
 | |
|     writableNodeToInsert.__next = writableSelf.__key;
 | |
|     writableNodeToInsert.__parent = writableSelf.__parent;
 | |
|     const selection = $getSelection();
 | |
|     if (restoreSelection && $isRangeSelection(selection)) {
 | |
|       const parent = this.getParentOrThrow();
 | |
|       $updateElementSelectionOnCreateDeleteNode(selection, parent, index);
 | |
|     }
 | |
|     return nodeToInsert;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Whether or not this node has a required parent. Used during copy + paste operations
 | |
|    * to normalize nodes that would otherwise be orphaned. For example, ListItemNodes without
 | |
|    * a ListNode parent or TextNodes with a ParagraphNode parent.
 | |
|    *
 | |
|    * */
 | |
|   isParentRequired(): boolean {
 | |
|     return false;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * The creation logic for any required parent. Should be implemented if {@link isParentRequired} returns true.
 | |
|    *
 | |
|    * */
 | |
|   createParentElementNode(): ElementNode {
 | |
|     return $createParagraphNode();
 | |
|   }
 | |
| 
 | |
|   selectStart(): RangeSelection {
 | |
|     return this.selectPrevious();
 | |
|   }
 | |
| 
 | |
|   selectEnd(): RangeSelection {
 | |
|     return this.selectNext(0, 0);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Moves selection to the previous sibling of this node, at the specified offsets.
 | |
|    *
 | |
|    * @param anchorOffset - The anchor offset for selection.
 | |
|    * @param focusOffset -  The focus offset for selection
 | |
|    * */
 | |
|   selectPrevious(anchorOffset?: number, focusOffset?: number): RangeSelection {
 | |
|     errorOnReadOnly();
 | |
|     const prevSibling = this.getPreviousSibling();
 | |
|     const parent = this.getParentOrThrow();
 | |
|     if (prevSibling === null) {
 | |
|       return parent.select(0, 0);
 | |
|     }
 | |
|     if ($isElementNode(prevSibling)) {
 | |
|       return prevSibling.select();
 | |
|     } else if (!$isTextNode(prevSibling)) {
 | |
|       const index = prevSibling.getIndexWithinParent() + 1;
 | |
|       return parent.select(index, index);
 | |
|     }
 | |
|     return prevSibling.select(anchorOffset, focusOffset);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Moves selection to the next sibling of this node, at the specified offsets.
 | |
|    *
 | |
|    * @param anchorOffset - The anchor offset for selection.
 | |
|    * @param focusOffset -  The focus offset for selection
 | |
|    * */
 | |
|   selectNext(anchorOffset?: number, focusOffset?: number): RangeSelection {
 | |
|     errorOnReadOnly();
 | |
|     const nextSibling = this.getNextSibling();
 | |
|     const parent = this.getParentOrThrow();
 | |
|     if (nextSibling === null) {
 | |
|       return parent.select();
 | |
|     }
 | |
|     if ($isElementNode(nextSibling)) {
 | |
|       return nextSibling.select(0, 0);
 | |
|     } else if (!$isTextNode(nextSibling)) {
 | |
|       const index = nextSibling.getIndexWithinParent();
 | |
|       return parent.select(index, index);
 | |
|     }
 | |
|     return nextSibling.select(anchorOffset, focusOffset);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Marks a node dirty, triggering transforms and
 | |
|    * forcing it to be reconciled during the update cycle.
 | |
|    *
 | |
|    * */
 | |
|   markDirty(): void {
 | |
|     this.getWritable();
 | |
|   }
 | |
| }
 | |
| 
 | |
| function errorOnTypeKlassMismatch(
 | |
|   type: string,
 | |
|   klass: Klass<LexicalNode>,
 | |
| ): void {
 | |
|   const registeredNode = getActiveEditor()._nodes.get(type);
 | |
|   // Common error - split in its own invariant
 | |
|   if (registeredNode === undefined) {
 | |
|     invariant(
 | |
|       false,
 | |
|       'Create node: Attempted to create node %s that was not configured to be used on the editor.',
 | |
|       klass.name,
 | |
|     );
 | |
|   }
 | |
|   const editorKlass = registeredNode.klass;
 | |
|   if (editorKlass !== klass) {
 | |
|     invariant(
 | |
|       false,
 | |
|       'Create node: Type %s in node %s does not match registered node %s with the same type',
 | |
|       type,
 | |
|       klass.name,
 | |
|       editorKlass.name,
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Insert a series of nodes after this LexicalNode (as next siblings)
 | |
|  *
 | |
|  * @param firstToInsert - The first node to insert after this one.
 | |
|  * @param lastToInsert - The last node to insert after this one. Must be a
 | |
|  * later sibling of FirstNode. If not provided, it will be its last sibling.
 | |
|  */
 | |
| export function insertRangeAfter(
 | |
|   node: LexicalNode,
 | |
|   firstToInsert: LexicalNode,
 | |
|   lastToInsert?: LexicalNode,
 | |
| ) {
 | |
|   const lastToInsert2 =
 | |
|     lastToInsert || firstToInsert.getParentOrThrow().getLastChild()!;
 | |
|   let current = firstToInsert;
 | |
|   const nodesToInsert = [firstToInsert];
 | |
|   while (current !== lastToInsert2) {
 | |
|     if (!current.getNextSibling()) {
 | |
|       invariant(
 | |
|         false,
 | |
|         'insertRangeAfter: lastToInsert must be a later sibling of firstToInsert',
 | |
|       );
 | |
|     }
 | |
|     current = current.getNextSibling()!;
 | |
|     nodesToInsert.push(current);
 | |
|   }
 | |
| 
 | |
|   let currentNode: LexicalNode = node;
 | |
|   for (const nodeToInsert of nodesToInsert) {
 | |
|     currentNode = currentNode.insertAfter(nodeToInsert);
 | |
|   }
 | |
| }
 |