UNPKG

lexical

Version:

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

1,090 lines (1,033 loc) 35 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 {PROTOTYPE_CONFIG_METHOD} from './LexicalConstants'; import type { AnyStaticNodeConfigValue, GetStaticNodeOwnConfig, GetStaticNodeType, } from './LexicalNode'; import invariant from '@lexical/internal/invariant'; import { $getEditor, type Klass, type LexicalNode, type LexicalNodeConfig, type LexicalUpdateJSON, NODE_STATE_KEY, type SerializedLexicalNode, type Spread, } from '.'; import {errorOnReadOnly} from './LexicalUpdates'; import { getRegisteredNodeOrThrow, iterStaticNodeConfigChain, } from './LexicalUtils'; const __DEV__ = process.env.NODE_ENV !== 'production'; /** * Read the state directly from the given object without `node.getLatest()`. * Safe to use outside of editor state context or to read a previous version, * equivalent to reading the property directly. */ export const NODE_STATE_DIRECT = 'direct'; /** * Use `node.getLatest()` before reading the state, per the lexical convention * of only working with the latest version of a node. */ export const NODE_STATE_LATEST = 'latest'; export type NodeStateVersion = | typeof NODE_STATE_DIRECT | typeof NODE_STATE_LATEST; /** * Get the value type (V) from a StateConfig */ export type StateConfigValue<S extends AnyStateConfig> = S extends StateConfig<infer _K, infer V> ? V : never; /** * Get the key type (K) from a StateConfig */ export type StateConfigKey<S extends AnyStateConfig> = S extends StateConfig<infer K, infer _V> ? K : never; /** * A value type, or an updater for that value type. For use with * {@link $setState} or any user-defined wrappers around it. */ export type ValueOrUpdater<V> = V | ((prevValue: V) => V); /** * A type alias to make it easier to define setter methods on your node class * * @example * ```ts * const fooState = createState("foo", { parse: ... }); * class MyClass extends TextNode { * // ... * setFoo(valueOrUpdater: StateValueOrUpdater<typeof fooState>): this { * return $setState(this, fooState, valueOrUpdater); * } * } * ``` */ export type StateValueOrUpdater<Cfg extends AnyStateConfig> = ValueOrUpdater< StateConfigValue<Cfg> >; export interface NodeStateConfig<S extends AnyStateConfig> { stateConfig: S; flat?: boolean; } export type RequiredNodeStateConfig = | NodeStateConfig<AnyStateConfig> | AnyStateConfig; export type StateConfigJSON<S> = S extends StateConfig<infer K, infer V> ? {[Key in K]?: V} : Record<never, never>; export type RequiredNodeStateConfigJSON< Config extends RequiredNodeStateConfig, Flat extends boolean, > = StateConfigJSON< Config extends NodeStateConfig<infer S> ? Spread<Config, {flat: false}> extends {flat: Flat} ? S : never : false extends Flat ? Config : never >; export type Prettify<T> = {[K in keyof T]: T[K]} & {}; /* eslint-disable @typescript-eslint/no-explicit-any */ export type UnionToIntersection<T> = ( T extends any ? (x: T) => any : never ) extends (x: infer R) => any ? R : never; /* eslint-enable @typescript-eslint/no-explicit-any */ export type CollectStateJSON< Tuple extends readonly RequiredNodeStateConfig[], Flat extends boolean, > = UnionToIntersection< {[K in keyof Tuple]: RequiredNodeStateConfigJSON<Tuple[K], Flat>}[number] >; // Read a node's own config out of its $config() record. Preferentially read it // from the STATIC_NODE_CONFIG accessor (see {@link GetStaticNodeOwnConfig}), // which resolves the most-derived own config directly — including for an // abstract base class keyed by a symbol, which has no string `type` to index by. // A record produced by the {@link BaseStaticNodeConfig} fallback (a node that // declares no `extends`, or a legacy node) sets no accessor; for those we fall // back to resolving the own `type` (see {@link GetStaticNodeType}) and indexing // by it. The own type is read through a mapped type (`{[P in Type]: ...}[Type]`) // so that the indexed access resolves against the concrete key literal rather // than the record's broad string index signature when `T` is still generic. type GetStaticNodeConfig<T extends LexicalNode> = [ GetStaticNodeOwnConfig<T>, ] extends [never] ? GetStaticNodeType<T> extends infer Type extends string ? string extends Type ? never : { [P in Type]: NonNullable< ReturnType<T[typeof PROTOTYPE_CONFIG_METHOD]>[P] >; }[Type] extends infer Config extends AnyStaticNodeConfigValue ? Config & {readonly type: Type} : never : never : GetStaticNodeOwnConfig<T> extends infer Config extends AnyStaticNodeConfigValue ? Config & {readonly type: GetStaticNodeType<T>} : never; type GetStaticNodeConfigs<T extends LexicalNode> = GetStaticNodeConfig<T> extends infer OwnConfig ? // `[X] extends [never]` checks for never without distributing (a naked // `never` would otherwise collapse the whole conditional to never). A node // with no $config — e.g. a legacy node keyed only by static getType() — // yields never here and contributes no state configs. [OwnConfig] extends [never] ? [] : OwnConfig extends {extends: Klass<infer Parent>} ? [GetStaticNodeConfig<Parent>] extends [never] ? [OwnConfig] : [OwnConfig, ...GetStaticNodeConfigs<Parent>] : [OwnConfig] : []; type CollectStateConfigs<Configs> = Configs extends [ infer OwnConfig, ...infer ParentConfigs, ] ? OwnConfig extends {stateConfigs: infer StateConfigs} ? StateConfigs extends readonly RequiredNodeStateConfig[] ? [...StateConfigs, ...CollectStateConfigs<ParentConfigs>] : CollectStateConfigs<ParentConfigs> : CollectStateConfigs<ParentConfigs> : []; export type GetNodeStateConfig<T extends LexicalNode> = CollectStateConfigs< GetStaticNodeConfigs<T> >; /** * The NodeState JSON produced by this LexicalNode */ export type NodeStateJSON<T extends LexicalNode> = Prettify< { [NODE_STATE_KEY]?: Prettify<CollectStateJSON<GetNodeStateConfig<T>, false>>; } & CollectStateJSON<GetNodeStateConfig<T>, true> >; /** * Configure a value to be used with StateConfig. * * The value type should be inferred from the definition of parse. * * If the value type is not JSON serializable, then unparse must also be provided. * * Values should be treated as immutable, much like React.useState. Mutating * stored values directly will cause unpredictable behavior, is not supported, * and may trigger errors in the future. * * @example * ```ts * const numberOrNullState = createState('numberOrNull', {parse: (v) => typeof v === 'number' ? v : null}); * // ^? State<'numberOrNull', StateValueConfig<number | null>> * const numberState = createState('number', {parse: (v) => typeof v === 'number' ? v : 0}); * // ^? State<'number', StateValueConfig<number>> * ``` * * Only the parse option is required, it is generally not useful to * override `unparse` or `isEqual`. However, if you are using * non-primitive types such as Array, Object, Date, or something * more exotic then you would want to override this. In these * cases you might want to reach for third party libraries. * * @example * ```ts * const isoDateState = createState('isoDate', { * parse: (v): null | Date => { * const date = typeof v === 'string' ? new Date(v) : null; * return date && !isNaN(date.valueOf()) ? date : null; * } * isEqual: (a, b) => a === b || (a && b && a.valueOf() === b.valueOf()), * unparse: (v) => v && v.toString() * }); * ``` * * You may find it easier to write a parse function using libraries like * zod, valibot, ajv, Effect, TypeBox, etc. perhaps with a wrapper function. */ export interface StateValueConfig<V> { /** * This function must return a default value when called with undefined, * otherwise it should parse the given JSON value to your type V. Note * that it is not required to copy or clone the given value, you can * pass it directly through if it matches the expected type. * * When you encounter an invalid value, it's up to you to decide * as to whether to ignore it and return the default value, * return some non-default error value, or throw an error. * * It is possible for V to include undefined, but if it does, then * it should also be considered the default value since undefined * can not be serialized to JSON so it is indistinguishable from the * default. * * Similarly, if your V is a function, then usage of {@link $setState} * must use an updater function because your type will be indistinguishable * from an updater function. */ parse: (jsonValue: unknown) => V; /** * This is optional and for advanced use cases only. * * You may specify a function that converts V back to JSON. * This is mandatory when V is not a JSON serializable type. */ unparse?: (parsed: V) => unknown; /** * This is optional and for advanced use cases only. * * Used to define the equality function so you can use an Array or Object * as V and still omit default values from the exported JSON. * * The default is `Object.is`, but something like `fast-deep-equal` might be * more appropriate for your use case. */ isEqual?: (a: V, b: V) => boolean; /** * When a node is copied with {@link $copyNode} (not cloned), reset this * value to the default. */ resetOnCopyNode?: boolean; } /** * The return value of {@link createState}, for use with * {@link $getState} and {@link $setState}. */ export class StateConfig<K extends string | symbol, V> { /** The string key used when serializing this state to JSON */ readonly key: K; /** The parse function from the StateValueConfig passed to createState */ readonly parse: (value?: unknown) => V; /** * The unparse function from the StateValueConfig passed to createState, * with a default that is simply a pass-through that assumes the value is * JSON serializable. */ readonly unparse: (value: V) => unknown; /** * An equality function from the StateValueConfig, with a default of * Object.is. */ readonly isEqual: (a: V, b: V) => boolean; /** * The result of `stateValueConfig.parse(undefined)`, which is computed only * once and used as the default value. When the current value `isEqual` to * the `defaultValue`, it will not be serialized to JSON. */ readonly defaultValue: V; readonly resetOnCopyNode: boolean; constructor(key: K, stateValueConfig: StateValueConfig<V>) { this.key = key; this.parse = stateValueConfig.parse.bind(stateValueConfig); this.unparse = (stateValueConfig.unparse || coerceToJSON).bind( stateValueConfig, ); this.isEqual = (stateValueConfig.isEqual || Object.is).bind( stateValueConfig, ); this.defaultValue = this.parse(undefined); this.resetOnCopyNode = stateValueConfig.resetOnCopyNode || false; } } /** * For advanced use cases, using this type is not recommended unless * it is required (due to TypeScript's lack of features like * higher-kinded types). * * A {@link StateConfig} type with any key and any value that can be * used in situations where the key and value type can not be known, * such as in a generic constraint when working with a collection of * StateConfig. * * {@link StateConfigKey} and {@link StateConfigValue} will be * useful when this is used as a generic constraint. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export type AnyStateConfig = StateConfig<any, any>; /** * Create a StateConfig for the given string key and StateValueConfig. * * The key must be locally unique. In dev you will get a key collision error * when you use two separate StateConfig on the same node with the same key. * * The returned StateConfig value should be used with {@link $getState} and * {@link $setState}. * * @param key The key to use * @param valueConfig Configuration for the value type * @returns a StateConfig * * @__NO_SIDE_EFFECTS__ */ export function createState<K extends symbol | string, V>( key: K, valueConfig: StateValueConfig<V>, ): StateConfig<K, V> { return new StateConfig(key, valueConfig); } /** * The accessor for working with node state. This will read the value for the * state on the given node, and will return `stateConfig.defaultValue` if the * state has never been set on this node. * * The `version` parameter is optional and should generally be {@link NODE_STATE_LATEST}, * consistent with the behavior of other node methods and functions, * but for certain use cases such as `updateDOM` you may have a need to * use {@link NODE_STATE_DIRECT} to read the state from a previous version of the node. * * For very advanced use cases, you can expect that {@link NODE_STATE_DIRECT} does not * require an editor state, just like directly accessing other properties * of a node without an accessor (e.g. `textNode.__text`). * * @param node Any LexicalNode * @param stateConfig The configuration of the state to read * @param version The default value {@link NODE_STATE_LATEST} will read the latest version of the node state, {@link NODE_STATE_DIRECT} will read the version that is stored on this LexicalNode which not reflect the version used in the current editor state * @returns The current value from the state, or the default value provided by the configuration. */ export function $getState<K extends string, V>( node: LexicalNode, stateConfig: StateConfig<K, V>, version: NodeStateVersion = NODE_STATE_LATEST, ): V { const latestOrDirectNode = version === NODE_STATE_LATEST ? node.getLatest() : node; const state = latestOrDirectNode.__state; if (state) { $checkCollision(node, stateConfig, state); return state.getValue(stateConfig); } return stateConfig.defaultValue; } /** * Given two versions of a node and a stateConfig, compare their state values * using `$getState(nodeVersion, stateConfig, NODE_STATE_DIRECT)`. * If the values are equal according to `stateConfig.isEqual`, return `null`, * otherwise return `[value, prevValue]`. * * This is useful for implementing updateDOM. Note that the `NODE_STATE_DIRECT` * version argument is used for both nodes. * * @param node Any LexicalNode * @param prevNode A previous version of node * @param stateConfig The configuration of the state to read * @returns `[value, prevValue]` if changed, otherwise `null` */ export function $getStateChange<T extends LexicalNode, K extends string, V>( node: T, prevNode: T, stateConfig: StateConfig<K, V>, ): null | [value: V, prevValue: V] { const value = $getState(node, stateConfig, NODE_STATE_DIRECT); const prevValue = $getState(prevNode, stateConfig, NODE_STATE_DIRECT); return stateConfig.isEqual(value, prevValue) ? null : [value, prevValue]; } /** * Set the state defined by stateConfig on node. Like with `React.useState` * you may directly specify the value or use an updater function that will * be called with the previous value of the state on that node (which will * be the `stateConfig.defaultValue` if not set). * * When an updater function is used, the node will only be marked dirty if * `stateConfig.isEqual(prevValue, value)` is false. * * @example * ```ts * const toggle = createState('toggle', {parse: Boolean}); * // set it direction * $setState(node, counterState, true); * // use an updater * $setState(node, counterState, (prev) => !prev); * ``` * * @param node The LexicalNode to set the state on * @param stateConfig The configuration for this state * @param valueOrUpdater The value or updater function * @returns node */ export function $setState<Node extends LexicalNode, K extends string, V>( node: Node, stateConfig: StateConfig<K, V>, valueOrUpdater: ValueOrUpdater<V>, ): Node { errorOnReadOnly(); let value: V; if (typeof valueOrUpdater === 'function') { const latest = node.getLatest(); const prevValue = $getState(latest, stateConfig); value = (valueOrUpdater as (v: V) => V)(prevValue); if (stateConfig.isEqual(prevValue, value)) { return latest; } } else { value = valueOrUpdater; } const writable = node.getWritable(); const state = $getWritableNodeState(writable); $checkCollision(node, stateConfig, state); state.updateFromKnown(stateConfig, value); return writable; } /** * @internal * * Register the config to this node's sharedConfigMap and throw an exception in * `__DEV__` when a collision is detected. */ function $checkCollision<Node extends LexicalNode, K extends string, V>( node: Node, stateConfig: StateConfig<K, V>, state: NodeState<Node>, ): void { if (__DEV__) { const collision = state.sharedNodeState.sharedConfigMap.get( stateConfig.key, ); if (collision !== undefined && collision !== stateConfig) { invariant( false, '$setState: State key collision %s detected in %s node with type %s and key %s. Only one StateConfig with a given key should be used on a node.', JSON.stringify(stateConfig.key), node.constructor.name, node.getType(), node.getKey(), ); } } } /** * @internal * * Opaque state to be stored on the editor's RegisterNode for use by NodeState */ export type SharedNodeState = { sharedConfigMap: SharedConfigMap; flatKeys: Set<string>; }; /** * @internal * * Create the state to store on RegisteredNode */ export function createSharedNodeState( nodeConfig: LexicalNodeConfig, ): SharedNodeState { const sharedConfigMap = new Map<string, AnyStateConfig>(); const flatKeys = new Set<string>(); for (const {ownNodeConfig} of iterStaticNodeConfigChain( typeof nodeConfig === 'function' ? nodeConfig : nodeConfig.replace, )) { if (ownNodeConfig && ownNodeConfig.stateConfigs) { for (const requiredStateConfig of ownNodeConfig.stateConfigs) { let stateConfig: AnyStateConfig; if ('stateConfig' in requiredStateConfig) { stateConfig = requiredStateConfig.stateConfig; if (requiredStateConfig.flat) { flatKeys.add(stateConfig.key); } } else { stateConfig = requiredStateConfig; } sharedConfigMap.set(stateConfig.key, stateConfig); } } } return {flatKeys, sharedConfigMap}; } type KnownStateMap = Map<AnyStateConfig, unknown>; type UnknownStateRecord = Record<string, unknown>; /** * Keys that must never be written into an {@link UnknownStateRecord} from * serialized (potentially untrusted) input. Writing a `__proto__` entry would * re-parent the record's prototype, and because {@link NodeState.getValue} * resolves keys with the `in` operator (which walks the prototype chain) an * attacker could otherwise inject arbitrary state values via a crafted * `__proto__`. These are never produced by {@link createState}. */ const UNSAFE_STATE_KEYS: ReadonlySet<string> = new Set([ '__proto__', 'constructor', 'prototype', ]); /** * @internal * * A Map of string keys to state configurations to be shared across nodes * and/or node versions. */ type SharedConfigMap = Map<string, AnyStateConfig>; /** * @internal */ export class NodeState<T extends LexicalNode> { /** * @internal * * Track the (versioned) node that this NodeState was created for, to * facilitate copy-on-write for NodeState. When a LexicalNode is cloned, * it will *reference* the NodeState from its prevNode. From the nextNode * you can continue to read state without copying, but the first $setState * will trigger a copy of the prevNode's NodeState with the node property * updated. */ readonly node: LexicalNode; /** * @internal * * State that has already been parsed in a get state, so it is safe. (can be returned with * just a cast since the proof was given before). * * Note that it uses StateConfig, so in addition to (1) the CURRENT VALUE, it has access to * (2) the State key (3) the DEFAULT VALUE and (4) the PARSE FUNCTION */ readonly knownState: KnownStateMap; /** * @internal * * A copy of serializedNode[NODE_STATE_KEY] that is made when JSON is * imported but has not been parsed yet. * * It stays here until a get state requires us to parse it, and since we * then know the value is safe we move it to knownState. * * Note that since only string keys are used here, we can only allow this * state to pass-through on export or on the next version since there is * no known value configuration. This pass-through is to support scenarios * where multiple versions of the editor code are working in parallel so * an old version of your code doesnt erase metadata that was * set by a newer version of your code. */ unknownState: undefined | UnknownStateRecord; /** * @internal * * This sharedNodeState is preserved across all instances of a given * node type in an editor and remains writable. It is how keys are resolved * to configuration. */ readonly sharedNodeState: SharedNodeState; /** * @internal * * The count of known or unknown keys in this state, ignoring the * intersection between the two sets. */ size: number; /** * @internal */ constructor( node: T, sharedNodeState: SharedNodeState, unknownState: undefined | UnknownStateRecord = undefined, knownState: KnownStateMap = new Map(), size: number | undefined = undefined, ) { this.node = node; this.sharedNodeState = sharedNodeState; this.unknownState = unknownState; this.knownState = knownState; const {sharedConfigMap} = this.sharedNodeState; const computedSize = size !== undefined ? size : computeSize(sharedConfigMap, unknownState, knownState); if (__DEV__) { invariant( size === undefined || computedSize === size, 'NodeState: size != computedSize (%s != %s)', String(size), String(computedSize), ); for (const stateConfig of knownState.keys()) { invariant( sharedConfigMap.has(stateConfig.key), 'NodeState: sharedConfigMap missing knownState key %s', stateConfig.key, ); } } this.size = computedSize; } /** * @internal * * Get the value from knownState, or parse it from unknownState * if it contains the given key. * * Updates the sharedConfigMap when no known state is found. * Updates unknownState and knownState when an unknownState is parsed. */ getValue<K extends string, V>(stateConfig: StateConfig<K, V>): V { const known = this.knownState.get(stateConfig) as V | undefined; if (known !== undefined) { return known; } this.sharedNodeState.sharedConfigMap.set(stateConfig.key, stateConfig); let parsed = stateConfig.defaultValue; if (this.unknownState && stateConfig.key in this.unknownState) { const jsonValue = this.unknownState[stateConfig.key]; if (jsonValue !== undefined) { parsed = stateConfig.parse(jsonValue); } // Only update if the key was unknown this.updateFromKnown(stateConfig, parsed); } return parsed; } /** * @internal * * Used only for advanced use cases, such as collab. The intent here is to * allow you to diff states with a more stable interface than the properties * of this class. */ getInternalState(): [ {readonly [k in string]: unknown} | undefined, ReadonlyMap<AnyStateConfig, unknown>, ] { return [this.unknownState, this.knownState]; } /** * Encode this NodeState to JSON in the format that its node expects. * This returns `{[NODE_STATE_KEY]?: UnknownStateRecord}` rather than * `UnknownStateRecord | undefined` so that we can support flattening * specific entries in the future when nodes can declare what * their required StateConfigs are. */ toJSON(): NodeStateJSON<T> { const state = {...this.unknownState}; const flatState: Record<string, unknown> = {}; for (const [stateConfig, v] of this.knownState) { if (stateConfig.isEqual(v, stateConfig.defaultValue)) { delete state[stateConfig.key]; } else { state[stateConfig.key] = stateConfig.unparse(v); } } for (const key of this.sharedNodeState.flatKeys) { if (key in state) { flatState[key] = state[key]; delete state[key]; } } if (undefinedIfEmpty(state)) { flatState[NODE_STATE_KEY] = state; } return flatState as NodeStateJSON<T>; } /** * @internal * * A NodeState is writable when the node to update matches * the node associated with the NodeState. This basically * mirrors how the EditorState NodeMap works, but in a * bottom-up organization rather than a top-down organization. * * This allows us to implement the same "copy on write" * pattern for state, without having the state version * update every time the node version changes (e.g. when * its parent or siblings change). * * @param node The node to associate with the state * @returns The next writable state */ getWritable(node: T): NodeState<T> { if (this.node === node) { return this; } const {sharedNodeState, unknownState} = this; const nextKnownState = new Map(this.knownState); return new NodeState( node, sharedNodeState, parseAndPruneNextUnknownState( sharedNodeState.sharedConfigMap, nextKnownState, unknownState, ), nextKnownState, this.size, ); } /** @internal */ resetOnCopyNode(): this { for (const stateConfig of this.knownState.keys()) { if (stateConfig.resetOnCopyNode) { this.knownState.set(stateConfig, stateConfig.defaultValue); } } return this; } /** @internal */ updateFromKnown<K extends string, V>( stateConfig: StateConfig<K, V>, value: V, ): void { const key = stateConfig.key; this.sharedNodeState.sharedConfigMap.set(key, stateConfig); const {knownState, unknownState} = this; if ( !(knownState.has(stateConfig) || (unknownState && key in unknownState)) ) { if (unknownState) { delete unknownState[key]; this.unknownState = undefinedIfEmpty(unknownState); } this.size++; } knownState.set(stateConfig, value); } /** * @internal * * This is intended for advanced use cases only, such * as collab or dev tools. * * Update a single key value pair from unknown state, * parsing it if the key is known to this node. This is * basically like updateFromJSON, but the effect is * isolated to a single entry. * * @param k The string key from an UnknownStateRecord * @param v The unknown value from an UnknownStateRecord */ updateFromUnknown(k: string, v: unknown): void { if (UNSAFE_STATE_KEYS.has(k)) { return; } const stateConfig = this.sharedNodeState.sharedConfigMap.get(k); if (stateConfig) { this.updateFromKnown(stateConfig, stateConfig.parse(v)); } else { this.unknownState = this.unknownState || {}; if (!(k in this.unknownState)) { this.size++; } this.unknownState[k] = v; } } /** * @internal * * Reset all existing state to default or empty values, * and perform any updates from the given unknownState. * * This is used when initializing a node's state from JSON, * or when resetting a node's state from JSON. * * @param unknownState The new state in serialized form */ updateFromJSON(unknownState: undefined | UnknownStateRecord): void { const {knownState} = this; // Reset all known state to defaults for (const stateConfig of knownState.keys()) { knownState.set(stateConfig, stateConfig.defaultValue); } // Since we are resetting all state to this new record, // the size starts at the number of known keys // and will be updated as we traverse the new state this.size = knownState.size; this.unknownState = undefined; if (unknownState) { for (const [k, v] of Object.entries(unknownState)) { this.updateFromUnknown(k, v); } } } } /** * @internal * * Only for direct use in very advanced integrations, such as lexical-yjs. * Typically you would only use {@link createState}, {@link $getState}, and * {@link $setState}. This is effectively the preamble for {@link $setState}. */ export function $getWritableNodeState<T extends LexicalNode>( node: T, ): NodeState<T> { const writable = node.getWritable(); const state = writable.__state ? writable.__state.getWritable(writable) : new NodeState(writable, $getSharedNodeState(writable)); writable.__state = state; return state; } /** * @internal * * Get the SharedNodeState for a node on this editor */ export function $getSharedNodeState<T extends LexicalNode>( node: T, ): SharedNodeState { return node.__state ? node.__state.sharedNodeState : getRegisteredNodeOrThrow($getEditor(), node.getType()).sharedNodeState; } /** * @internal * * This is used to implement LexicalNode.updateFromJSON and is * not intended to be exported from the package. * * @param node any LexicalNode * @param unknownState undefined or a serialized State * @returns A writable version of node, with the state set. */ export function $updateStateFromJSON<T extends LexicalNode>( node: T, serialized: LexicalUpdateJSON<SerializedLexicalNode>, ): T { const writable = node.getWritable(); const unknownState = serialized[NODE_STATE_KEY]; let parseState = unknownState; for (const k of $getSharedNodeState(writable).flatKeys) { if (k in serialized) { if (parseState === undefined || parseState === unknownState) { parseState = {...unknownState}; } parseState[k] = serialized[k as keyof typeof serialized]; } } if (writable.__state || parseState) { $getWritableNodeState(node).updateFromJSON(parseState); } return writable; } /** * @internal * * Return true if the two nodes have equivalent NodeState, to be used * to determine when TextNode are being merged, not a lot of use cases * otherwise. */ export function nodeStatesAreEquivalent<T extends LexicalNode>( a: undefined | NodeState<T>, b: undefined | NodeState<T>, ): boolean { if (a === b) { return true; } const keys = new Set<string>(); return !( (a && hasUnequalMapEntry(keys, a, b)) || (b && hasUnequalMapEntry(keys, b, a)) || (a && hasUnequalRecordEntry(keys, a, b)) || (b && hasUnequalRecordEntry(keys, b, a)) ); } /** * Compute the number of distinct keys that will be in a NodeState */ function computeSize( sharedConfigMap: SharedConfigMap, unknownState: UnknownStateRecord | undefined, knownState: KnownStateMap, ): number { let size = knownState.size; if (unknownState) { for (const k in unknownState) { const sharedConfig = sharedConfigMap.get(k); if (!sharedConfig || !knownState.has(sharedConfig)) { size++; } } } return size; } /** * @internal * * Return obj if it is an object with at least one property, otherwise * return undefined. */ function undefinedIfEmpty<T extends object>(obj: undefined | T): undefined | T { if (obj) { for (const key in obj) { return obj; } } return undefined; } /** * @internal * * Cast the given v to unknown */ function coerceToJSON(v: unknown): unknown { return v; } /** * @internal * * Parse all knowable values in an UnknownStateRecord into nextKnownState * and return the unparsed values in a new UnknownStateRecord. Returns * undefined if no unknown values remain. */ function parseAndPruneNextUnknownState( sharedConfigMap: SharedConfigMap, nextKnownState: KnownStateMap, unknownState: undefined | UnknownStateRecord, ): undefined | UnknownStateRecord { let nextUnknownState: undefined | UnknownStateRecord = undefined; if (unknownState) { for (const [k, v] of Object.entries(unknownState)) { if (UNSAFE_STATE_KEYS.has(k)) { continue; } const stateConfig = sharedConfigMap.get(k); if (stateConfig) { if (!nextKnownState.has(stateConfig)) { nextKnownState.set(stateConfig, stateConfig.parse(v)); } } else { nextUnknownState = nextUnknownState || {}; nextUnknownState[k] = v; } } } return nextUnknownState; } /** * @internal * * Compare each entry of sourceState.knownState that is not in keys to * otherState (or the default value if otherState is undefined. * Note that otherState will return the defaultValue as well if it * has never been set. Any checked entry's key will be added to keys. * * @returns true if any difference is found, false otherwise */ function hasUnequalMapEntry<T extends LexicalNode>( keys: Set<string>, sourceState: NodeState<T>, otherState?: NodeState<T>, ): boolean { for (const [stateConfig, value] of sourceState.knownState) { if (keys.has(stateConfig.key)) { continue; } keys.add(stateConfig.key); const otherValue = otherState ? otherState.getValue(stateConfig) : stateConfig.defaultValue; if (otherValue !== value && !stateConfig.isEqual(otherValue, value)) { return true; } } return false; } /** * @internal * * Compare each entry of sourceState.unknownState that is not in keys to * otherState.unknownState (or undefined if otherState is undefined). * Any checked entry's key will be added to keys. * * Notably since we have already checked hasUnequalMapEntry on both sides, * we do not do any parsing or checking of knownState. * * @returns true if any difference is found, false otherwise */ function hasUnequalRecordEntry<T extends LexicalNode>( keys: Set<string>, sourceState: NodeState<T>, otherState?: NodeState<T>, ): boolean { const {unknownState} = sourceState; const otherUnknownState = otherState ? otherState.unknownState : undefined; if (unknownState) { for (const [key, value] of Object.entries(unknownState)) { if (keys.has(key)) { continue; } keys.add(key); const otherValue = otherUnknownState ? otherUnknownState[key] : undefined; if (value !== otherValue) { return true; } } } return false; } /** * @internal * * Clones the NodeState for a given node. Handles aliasing if the state references the from node. */ export function $cloneNodeState<T extends LexicalNode>( from: T, to: T, ): undefined | NodeState<T> { const state = from.__state; return state && state.node === from ? state.getWritable(to) : state; }