UNPKG

lexical

Version:

Lexical is an extensible text editor framework that provides excellent reliability, accessible and performance.

545 lines (543 loc) 26.7 kB
/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * */ import type { LexicalNode } from '../LexicalNode'; import { ElementNode } from '../nodes/LexicalElementNode'; import { TextNode } from '../nodes/LexicalTextNode'; /** * The direction of a caret, 'next' points towards the end of the document * and 'previous' points towards the beginning */ export type CaretDirection = 'next' | 'previous'; /** * A type utility to flip next and previous */ export type FlipDirection<D extends CaretDirection> = (typeof FLIP_DIRECTION)[D]; /** * A sibling caret type points from a LexicalNode origin to its next or previous sibling, * and a child caret type points from an ElementNode origin to its first or last child. */ export type CaretType = 'sibling' | 'child'; /** * The RootMode is specified in all caret traversals where the traversal can go up * towards the root. 'root' means that it will stop at the document root, * and 'shadowRoot' will stop at the document root or any shadow root * (per {@link $isRootOrShadowRoot}). */ export type RootMode = 'root' | 'shadowRoot'; declare const FLIP_DIRECTION: { readonly next: "previous"; readonly previous: "next"; }; /** @noInheritDoc */ export interface BaseCaret<T extends LexicalNode, D extends CaretDirection, Type> extends Iterable<SiblingCaret<LexicalNode, D>> { /** The origin node of this caret, typically this is what you will use in traversals */ readonly origin: T; /** sibling for a SiblingCaret (pointing at the next or previous sibling) or child for a ChildCaret (pointing at the first or last child) */ readonly type: Type; /** next if pointing at the next sibling or first child, previous if pointing at the previous sibling or last child */ readonly direction: D; /** Get the ElementNode that is the logical parent (`origin` for `ChildCaret`, `origin.getParent()` for `SiblingCaret`) */ getParentAtCaret: () => null | ElementNode; /** Get the node connected to the origin in the caret's direction, or null if there is no node */ getNodeAtCaret: () => null | LexicalNode; /** Get a new SiblingCaret from getNodeAtCaret() in the same direction. */ getAdjacentCaret: () => null | SiblingCaret<LexicalNode, D>; /** * Get a new SiblingCaret with this same node */ getSiblingCaret: () => SiblingCaret<T, D>; /** Remove the getNodeAtCaret() node that this caret is pointing towards, if it exists */ remove: () => this; /** * Insert a node connected to origin in this direction (before the node that this caret is pointing towards, if any existed). * For a `SiblingCaret` this is `origin.insertAfter(node)` for next, or `origin.insertBefore(node)` for previous. * For a `ChildCaret` this is `origin.splice(0, 0, [node])` for next or `origin.append(node)` for previous. */ insert: (node: LexicalNode) => this; /** If getNodeAtCaret() is not null then replace it with node, otherwise insert node */ replaceOrInsert: (node: LexicalNode, includeChildren?: boolean) => this; /** * Splice an iterable (typically an Array) of nodes into this location. * * @param deleteCount The number of existing nodes to replace or delete * @param nodes An iterable of nodes that will be inserted in this location, using replace instead of insert for the first deleteCount nodes * @param nodesDirection The direction of the nodes iterable, defaults to 'next' */ splice: (deleteCount: number, nodes: Iterable<LexicalNode>, nodesDirection?: CaretDirection) => this; } /** * A RangeSelection expressed as a pair of Carets */ export interface CaretRange<D extends CaretDirection = CaretDirection> extends Iterable<NodeCaret<D>> { readonly type: 'node-caret-range'; readonly direction: D; anchor: PointCaret<D>; focus: PointCaret<D>; /** Return true if anchor and focus are the same caret */ isCollapsed: () => boolean; /** * Iterate the carets between anchor and focus in a pre-order fashion, note * that this does not include any text slices represented by the anchor and/or * focus. Those are accessed separately from getTextSlices. * * An ElementNode origin will be yielded as a ChildCaret on enter, * and a SiblingCaret on leave. */ iterNodeCarets: (rootMode?: RootMode) => IterableIterator<NodeCaret<D>>; /** * There are between zero and two non-null TextSliceCarets for a CaretRange. * Note that when anchor and focus share an origin node the second element * will be null becaues the slice is entirely represented by the first element. * * `[slice, slice]`: anchor and focus are TextPointCaret with distinct origin nodes * `[slice, null]`: anchor is a TextPointCaret * `[null, slice]`: focus is a TextPointCaret * `[null, null]`: Neither anchor nor focus are TextPointCarets */ getTextSlices: () => TextPointCaretSliceTuple<D>; } export interface StepwiseIteratorConfig<State, Stop, Value> { readonly initial: State | Stop; readonly hasNext: (value: State | Stop) => value is State; readonly step: (value: State) => State | Stop; readonly map: (value: State) => Value; } /** * A NodeCaret is the combination of an origin node and a direction * that points towards where a connected node will be fetched, inserted, * or replaced. A SiblingCaret points from a node to its next or previous * sibling, and a ChildCaret points to its first or last child * (using next or previous as direction, for symmetry with SiblingCaret). * * The differences between NodeCaret and PointType are: * - NodeCaret can only be used to refer to an entire node (PointCaret is used when a full analog is needed). A PointType of text type can be used to refer to a specific location inside of a TextNode. * - NodeCaret stores an origin node, type (sibling or child), and direction (next or previous). A PointType stores a type (text or element), the key of a node, and a text or child offset within that node. * - NodeCaret is directional and always refers to a very specific node, eliminating all ambiguity. PointType can refer to the location before or at a node depending on context. * - NodeCaret is more robust to nearby mutations, as it relies only on a node's direct connections. An element Any change to the count of previous siblings in an element PointType will invalidate it. * - NodeCaret is designed to work more directly with the internal representation of the document tree, making it suitable for use in traversals without performing any redundant work. * * The caret does *not* update in response to any mutations, you should * not persist it across editor updates, and using a caret after its origin * node has been removed or replaced may result in runtime errors. */ export type NodeCaret<D extends CaretDirection = CaretDirection> = SiblingCaret<LexicalNode, D> | ChildCaret<ElementNode, D>; /** * A PointCaret is a NodeCaret that also includes a * TextPointCaret type which refers to a specific offset of a TextNode. * This type is separate because it is not relevant to general node traversal * so it doesn't make sense to have it show up except when defining * a CaretRange and in those cases there will be at most two of them only * at the boundaries. * * The addition of TextPointCaret allows this type to represent any location * that is representable by PointType, as the TextPointCaret refers to a * specific offset within a TextNode. */ export type PointCaret<D extends CaretDirection = CaretDirection> = TextPointCaret<TextNode, D> | SiblingCaret<LexicalNode, D> | ChildCaret<ElementNode, D>; /** * A SiblingCaret points from an origin LexicalNode towards its next or previous sibling. */ export interface SiblingCaret<T extends LexicalNode = LexicalNode, D extends CaretDirection = CaretDirection> extends BaseCaret<T, D, 'sibling'> { /** Get a new caret with the latest origin pointer */ getLatest: () => SiblingCaret<T, D>; /** * If the origin of this node is an ElementNode, return the ChildCaret of this origin in the same direction. * If the origin is not an ElementNode, this will return null. */ getChildCaret: () => null | ChildCaret<T & ElementNode, D>; /** * Get the caret in the same direction from the parent of this origin. * * @param mode 'root' to return null at the root, 'shadowRoot' to return null at the root or any shadow root * @returns A SiblingCaret with the parent of this origin, or null if the parent is a root according to mode. */ getParentCaret: (mode?: RootMode) => null | SiblingCaret<ElementNode, D>; /** * Retun true if other is a SiblingCaret or TextPointCaret with the same * origin (by node key comparion) and direction. */ isSameNodeCaret: (other: null | undefined | PointCaret) => other is SiblingCaret<T, D> | T extends TextNode ? TextPointCaret<T & TextNode, D> : never; /** * Retun true if other is a SiblingCaret with the same * origin (by node key comparion) and direction. */ isSamePointCaret: (other: null | undefined | PointCaret) => other is SiblingCaret<T, D>; /** * Get a new NodeCaret with the head and tail of its directional arrow flipped, such that flipping twice is the identity. * For example, given a non-empty parent with a firstChild and lastChild, and a second emptyParent node with no children: * * @example * ``` * caret.getFlipped().getFlipped().is(caret) === true; * $getChildCaret(parent, 'next').getFlipped().is($getSiblingCaret(firstChild, 'previous')) === true; * $getSiblingCaret(lastChild, 'next').getFlipped().is($getChildCaret(parent, 'previous')) === true; * $getSiblingCaret(firstChild, 'next).getFlipped().is($getSiblingCaret(lastChild, 'previous')) === true; * $getChildCaret(emptyParent, 'next').getFlipped().is($getChildCaret(emptyParent, 'previous')) === true; * ``` */ getFlipped: () => NodeCaret<FlipDirection<D>>; } /** * A ChildCaret points from an origin ElementNode towards its first or last child. */ export interface ChildCaret<T extends ElementNode = ElementNode, D extends CaretDirection = CaretDirection> extends BaseCaret<T, D, 'child'> { /** Get a new caret with the latest origin pointer */ getLatest: () => ChildCaret<T, D>; getParentCaret: (mode?: RootMode) => null | SiblingCaret<T, D>; getParentAtCaret: () => T; /** Return this, the ChildCaret is already a child caret of its origin */ getChildCaret: () => this; /** * Retun true if other is a ChildCaret with the same * origin (by node key comparion) and direction. */ isSameNodeCaret: (other: null | undefined | PointCaret) => other is ChildCaret<T, D>; /** * Retun true if other is a ChildCaret with the same * origin (by node key comparion) and direction. */ isSamePointCaret: (other: null | undefined | PointCaret) => other is ChildCaret<T, D>; /** * Get a new NodeCaret with the head and tail of its directional arrow flipped, such that flipping twice is the identity. * For example, given a non-empty parent with a firstChild and lastChild, and a second emptyParent node with no children: * * @example * ``` * caret.getFlipped().getFlipped().is(caret) === true; * $getChildCaret(parent, 'next').getFlipped().is($getSiblingCaret(firstChild, 'previous')) === true; * $getSiblingCaret(lastChild, 'next').getFlipped().is($getChildCaret(parent, 'previous')) === true; * $getSiblingCaret(firstChild, 'next).getFlipped().is($getSiblingCaret(lastChild, 'previous')) === true; * $getChildCaret(emptyParent, 'next').getFlipped().is($getChildCaret(emptyParent, 'previous')) === true; * ``` */ getFlipped: () => NodeCaret<FlipDirection<D>>; } /** * A TextPointCaret is a special case of a SiblingCaret that also carries * an offset used for representing partially selected TextNode at the edges * of a CaretRange. * * The direction determines which part of the text is adjacent to the caret, * if next it's all of the text after offset. If previous, it's all of the * text before offset. * * While this can be used in place of any SiblingCaret of a TextNode, * the offset into the text will be ignored except in contexts that * specifically use the TextPointCaret or PointCaret types. */ export interface TextPointCaret<T extends TextNode = TextNode, D extends CaretDirection = CaretDirection> extends BaseCaret<T, D, 'text'> { /** The offset into the string */ readonly offset: number; /** Get a new caret with the latest origin pointer */ getLatest: () => TextPointCaret<T, D>; /** * A TextPointCaret can not have a ChildCaret. */ getChildCaret: () => null; /** * Get the caret in the same direction from the parent of this origin. * * @param mode 'root' to return null at the root, 'shadowRoot' to return null at the root or any shadow root * @returns A SiblingCaret with the parent of this origin, or null if the parent is a root according to mode. */ getParentCaret: (mode?: RootMode) => null | SiblingCaret<ElementNode, D>; /** * Retun true if other is a TextPointCaret or SiblingCaret with the same * origin (by node key comparion) and direction. */ isSameNodeCaret: (other: null | undefined | PointCaret) => other is TextPointCaret<T, D> | SiblingCaret<T, D>; /** * Retun true if other is a ChildCaret with the same * origin (by node key comparion) and direction. */ isSamePointCaret: (other: null | undefined | PointCaret) => other is TextPointCaret<T, D>; /** * Get a new TextPointCaret with the head and tail of its directional arrow flipped, such that flipping twice is the identity. * For a TextPointCaret this merely flips the direction because the arrow is internal to the node. * * @example * ``` * caret.getFlipped().getFlipped().is(caret) === true; * ``` */ getFlipped: () => TextPointCaret<T, FlipDirection<D>>; } /** * A TextPointCaretSlice is a wrapper for a TextPointCaret that carries a signed * distance representing the direction and amount of text selected from the given * caret. A negative distance means that text before offset is selected, a * positive distance means that text after offset is selected. The offset+distance * pair is not affected in any way by the direction of the caret. */ export interface TextPointCaretSlice<T extends TextNode = TextNode, D extends CaretDirection = CaretDirection> { readonly type: 'slice'; readonly caret: TextPointCaret<T, D>; readonly distance: number; /** * @returns absolute coordinates into the text (for use with `text.slice(...)`) */ getSliceIndices: () => [startIndex: number, endIndex: number]; /** * @returns The text represented by the slice */ getTextContent: () => string; /** * @returns The size of the text represented by the slice */ getTextContentSize: () => number; /** * Remove the slice of text from the contained caret, returning a new * TextPointCaret without the wrapper (since the size would be zero). * * Note that this is a lower-level utility that does not have any specific * behavior for 'segmented' or 'token' modes and it will not remove * an empty TextNode. * * @returns The inner TextPointCaret with the same offset and direction * and the latest TextNode origin after mutation */ removeTextSlice(): TextPointCaret<T, D>; } /** * A utility type to specify that a CaretRange may have zero, * one, or two associated TextPointCaretSlice. If the anchor * and focus are on the same node, the anchorSlice will contain * the slice and focusSlie will be null. */ export type TextPointCaretSliceTuple<D extends CaretDirection> = readonly [ anchorSlice: null | TextPointCaretSlice<TextNode, D>, focusSlice: null | TextPointCaretSlice<TextNode, D> ]; /** * Flip a direction ('next' -> 'previous'; 'previous' -> 'next'). * * Note that TypeScript can't prove that FlipDirection is its own * inverse (but if you have a concrete 'next' or 'previous' it will * simplify accordingly). * * @param direction A direction * @returns The opposite direction */ export declare function flipDirection<D extends CaretDirection>(direction: D): FlipDirection<D>; /** * Guard to check if the given caret is specifically a TextPointCaret * * @param caret Any caret * @returns true if it is a TextPointCaret */ export declare function $isTextPointCaret<D extends CaretDirection>(caret: null | undefined | PointCaret<D>): caret is TextPointCaret<TextNode, D>; /** * Guard to check if the given argument is any type of caret * * @param caret * @returns true if caret is any type of caret */ export declare function $isNodeCaret<D extends CaretDirection>(caret: null | undefined | PointCaret<D>): caret is PointCaret<D>; /** * Guard to check if the given argument is specifically a SiblingCaret (or TextPointCaret) * * @param caret * @returns true if caret is a SiblingCaret */ export declare function $isSiblingCaret<D extends CaretDirection>(caret: null | undefined | PointCaret<D>): caret is SiblingCaret<LexicalNode, D>; /** * Guard to check if the given argument is specifically a ChildCaret * @param caret * @returns true if caret is a ChildCaret */ export declare function $isChildCaret<D extends CaretDirection>(caret: null | undefined | PointCaret<D>): caret is ChildCaret<ElementNode, D>; /** * Get a caret that points at the next or previous sibling of the given origin node. * * @param origin The origin node * @param direction 'next' or 'previous' * @returns null if origin is null, otherwise a SiblingCaret for this origin and direction */ export declare function $getSiblingCaret<T extends LexicalNode, D extends CaretDirection>(origin: T, direction: D): SiblingCaret<T, D>; export declare function $getSiblingCaret<T extends LexicalNode, D extends CaretDirection>(origin: null | T, direction: D): null | SiblingCaret<T, D>; /** * Construct a TextPointCaret * * @param origin The TextNode * @param direction The direction (next points to the end of the text, previous points to the beginning) * @param offset The offset into the text in absolute positive string coordinates (0 is the start) * @returns a TextPointCaret */ export declare function $getTextPointCaret<T extends TextNode, D extends CaretDirection>(origin: T, direction: D, offset: number | CaretDirection): TextPointCaret<T, D>; export declare function $getTextPointCaret<T extends TextNode, D extends CaretDirection>(origin: null | T, direction: D, offset: number | CaretDirection): null | TextPointCaret<T, D>; /** * Get a normalized offset into a TextNode given a numeric offset or a * direction for which end of the string to use. Throws if the offset * is not in the bounds of the text content size. * * @param origin a TextNode * @param offset An absolute offset into the TextNode string, or a direction for which end to use as the offset * @returns An absolute offset into the TextNode string */ export declare function $getTextNodeOffset(origin: TextNode, offset: number | CaretDirection): number; /** * Construct a TextPointCaretSlice given a TextPointCaret and a signed distance. The * distance should be negative to slice text before the caret's offset, and positive * to slice text after the offset. The direction of the caret itself is not * relevant to the string coordinates when working with a TextPointCaretSlice * but mutation operations will preserve the direction. * * @param caret * @param distance * @returns TextPointCaretSlice */ export declare function $getTextPointCaretSlice<T extends TextNode, D extends CaretDirection>(caret: TextPointCaret<T, D>, distance: number): TextPointCaretSlice<T, D>; /** * Get a caret that points at the first or last child of the given origin node, * which must be an ElementNode. * * @param origin The origin ElementNode * @param direction 'next' for first child or 'previous' for last child * @returns null if origin is null or not an ElementNode, otherwise a ChildCaret for this origin and direction */ export declare function $getChildCaret<T extends ElementNode, D extends CaretDirection>(origin: T, direction: D): ChildCaret<T, D>; /** * Gets the ChildCaret if one is possible at this caret origin, otherwise return the caret */ export declare function $getChildCaretOrSelf<Caret extends PointCaret | null>(caret: Caret): Caret | ChildCaret<ElementNode, NonNullable<Caret>['direction']>; /** * Gets the adjacent caret, if not-null and if the origin of the adjacent caret is an ElementNode, then return * the ChildCaret. This can be used along with the getParentAdjacentCaret method to perform a full DFS * style traversal of the tree. * * @param caret The caret to start at */ export declare function $getAdjacentChildCaret<D extends CaretDirection>(caret: null | NodeCaret<D>): null | NodeCaret<D>; /** * Guard to check for a TextPointCaretSlice * * @param caretOrSlice A caret or slice * @returns true if caretOrSlice is a TextPointCaretSlice */ export declare function $isTextPointCaretSlice<D extends CaretDirection>(caretOrSlice: null | undefined | PointCaret<D> | TextPointCaretSlice<TextNode, D>): caretOrSlice is TextPointCaretSlice<TextNode, D>; /** * Construct a CaretRange that starts at anchor and goes to the end of the * document in the anchor caret's direction. */ export declare function $extendCaretToRange<D extends CaretDirection>(anchor: PointCaret<D>): CaretRange<D>; /** * Construct a collapsed CaretRange that starts and ends at anchor. */ export declare function $getCollapsedCaretRange<D extends CaretDirection>(anchor: PointCaret<D>): CaretRange<D>; /** * Construct a CaretRange from anchor and focus carets pointing in the * same direction. In order to get the expected behavior, * the anchor must point towards the focus or be the same point. * * In the 'next' direction the anchor should be at or before the * focus in the document. In the 'previous' direction the anchor * should be at or after the focus in the document * (similar to a backwards RangeSelection). * * @param anchor * @param focus * @returns a CaretRange */ export declare function $getCaretRange<D extends CaretDirection>(anchor: PointCaret<D>, focus: PointCaret<D>): CaretRange<D>; /** * A generalized utility for creating a stepwise iterator * based on: * * - an initial state * - a stop guard that returns true if the iteration is over, this * is typically used to detect a sentinel value such as null or * undefined from the state but may return true for other conditions * as well * - a step function that advances the state (this will be called * after map each time next() is called to prepare the next state) * - a map function that will be called that may transform the state * before returning it. It will only be called once for each next() * call when stop(state) === false * * @param config * @returns An IterableIterator */ export declare function makeStepwiseIterator<State, Stop, Value>(config: StepwiseIteratorConfig<State, Stop, Value>): IterableIterator<Value>; /** * A total ordering for `PointCaret<'next'>`, based on * the same order that a {@link CaretRange} would iterate * them. * * For a given origin node: * - ChildCaret comes before SiblingCaret * - TextPointCaret comes before SiblingCaret * * An exception is thrown when a and b do not have any * common ancestor. * * This ordering is a sort of mix of pre-order and post-order * because each ElementNode will show up as a ChildCaret * on 'enter' (pre-order) and a SiblingCaret on 'leave' (post-order). * * @param a * @param b * @returns -1 if a comes before b, 0 if a and b are the same, or 1 if a comes after b */ export declare function $comparePointCaretNext(a: PointCaret<'next'>, b: PointCaret<'next'>): -1 | 0 | 1; /** * Return the ordering of siblings in a CommonAncestorResultBranch * @param branch Returns -1 if a precedes b, 1 otherwise */ export declare function $getCommonAncestorResultBranchOrder<A extends LexicalNode, B extends LexicalNode>(compare: CommonAncestorResultBranch<A, B>): -1 | 1; /** * The two compared nodes are the same */ export interface CommonAncestorResultSame<A extends LexicalNode> { readonly type: 'same'; readonly commonAncestor: A; } /** * Node a was a descendant of node b, and not the same node */ export interface CommonAncestorResultDescendant<B extends ElementNode> { readonly type: 'descendant'; readonly commonAncestor: B; } /** * Node a is an ancestor of node b, and not the same node */ export interface CommonAncestorResultAncestor<A extends ElementNode> { readonly type: 'ancestor'; readonly commonAncestor: A; } /** * Node a and node b have a common ancestor but are on different branches, * the `a` and `b` properties of this result are the ancestors of a and b * that are children of the commonAncestor. Since they are siblings, their * positions are comparable to determine order in the document. */ export interface CommonAncestorResultBranch<A extends LexicalNode, B extends LexicalNode> { readonly type: 'branch'; readonly commonAncestor: ElementNode; /** The ancestor of `a` that is a child of `commonAncestor` */ readonly a: A | ElementNode; /** The ancestor of `b` that is a child of `commonAncestor` */ readonly b: B | ElementNode; } /** * The result of comparing two nodes that share some common ancestor */ export type CommonAncestorResult<A extends LexicalNode, B extends LexicalNode> = CommonAncestorResultSame<A> | CommonAncestorResultAncestor<A & ElementNode> | CommonAncestorResultDescendant<B & ElementNode> | CommonAncestorResultBranch<A, B>; /** * Find a common ancestor of a and b and return a detailed result object, * or null if there is no common ancestor between the two nodes. * * The result object will have a commonAncestor property, and the other * properties can be used to quickly compare these positions in the tree. * * @param a A LexicalNode * @param b A LexicalNode * @returns A comparison result between the two nodes or null if they have no common ancestor */ export declare function $getCommonAncestor<A extends LexicalNode, B extends LexicalNode>(a: A, b: B): null | CommonAncestorResult<A, B>; export {};