lexical
Version:
Lexical is an extensible text editor framework that provides excellent reliability, accessible and performance.
545 lines (543 loc) • 26.7 kB
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.
*
*/
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 {};