/** * 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 {LexicalBuilder} from '../LexicalBuilder'; import invariant from '@lexical/internal/invariant'; import { type AnyLexicalExtension, type ExtensionBuildState, type ExtensionInitState, type ExtensionRegisterState, type InitialEditorConfig, type LexicalEditor, type LexicalExtensionConfig, type LexicalExtensionDependency, type LexicalExtensionInit, type LexicalExtensionOutput, shallowMergeConfig, } from 'lexical'; export const ExtensionRepStateIds = { /* eslint-disable sort-keys-fix/sort-keys-fix */ unmarked: 0, temporary: 1, permanent: 2, configured: 3, initialized: 4, built: 5, registered: 6, afterRegistration: 7, /* eslint-enable sort-keys-fix/sort-keys-fix */ } as const; interface UnmarkedState { id: (typeof ExtensionRepStateIds)['unmarked']; } interface TemporaryState { id: (typeof ExtensionRepStateIds)['temporary']; } interface PermanentState { id: (typeof ExtensionRepStateIds)['permanent']; } interface ConfiguredState { id: (typeof ExtensionRepStateIds)['configured']; config: LexicalExtensionConfig; registerState: ExtensionInitState; } interface InitializedState extends Omit< ConfiguredState, 'id' | 'registerState' > { id: (typeof ExtensionRepStateIds)['initialized']; initResult: LexicalExtensionInit; registerState: ExtensionBuildState>; } interface BuiltState extends Omit< ConfiguredState, 'id' | 'registerState' > { id: (typeof ExtensionRepStateIds)['built']; initResult: LexicalExtensionInit; output: LexicalExtensionOutput; registerState: ExtensionRegisterState< LexicalExtensionInit, LexicalExtensionOutput >; } interface RegisteredState extends Omit< BuiltState, 'id' > { id: (typeof ExtensionRepStateIds)['registered']; } interface AfterRegistrationState< Extension extends AnyLexicalExtension, > extends Omit, 'id'> { id: (typeof ExtensionRepStateIds)['afterRegistration']; } export type ExtensionRepState = | UnmarkedState | TemporaryState | PermanentState | ConfiguredState | InitializedState | BuiltState | RegisteredState | AfterRegistrationState; export function isExactlyUnmarkedExtensionRepState< Extension extends AnyLexicalExtension, >(state: ExtensionRepState): state is UnmarkedState { return state.id === ExtensionRepStateIds.unmarked; } function isExactlyTemporaryExtensionRepState< Extension extends AnyLexicalExtension, >(state: ExtensionRepState): state is TemporaryState { return state.id === ExtensionRepStateIds.temporary; } export function isExactlyPermanentExtensionRepState< Extension extends AnyLexicalExtension, >(state: ExtensionRepState): state is PermanentState { return state.id === ExtensionRepStateIds.permanent; } function isConfiguredExtensionRepState( state: ExtensionRepState, ): state is | ConfiguredState | InitializedState | BuiltState | RegisteredState | AfterRegistrationState { return state.id >= ExtensionRepStateIds.configured; } function isInitializedExtensionRepState( state: ExtensionRepState, ): state is | InitializedState | BuiltState | RegisteredState | AfterRegistrationState { return state.id >= ExtensionRepStateIds.initialized; } function isBuiltExtensionRepState( state: ExtensionRepState, ): state is | BuiltState | RegisteredState | AfterRegistrationState { return state.id >= ExtensionRepStateIds.built; } function isAfterRegistrationState( state: ExtensionRepState, ): state is AfterRegistrationState { return state.id >= ExtensionRepStateIds.afterRegistration; } export function applyTemporaryMark( state: ExtensionRepState, ): TemporaryState { invariant( isExactlyUnmarkedExtensionRepState(state), 'LexicalBuilder: Can not apply a temporary mark from state id %s (expected %s unmarked)', String(state.id), String(ExtensionRepStateIds.unmarked), ); return Object.assign(state, {id: ExtensionRepStateIds.temporary}); } export function applyPermanentMark( state: ExtensionRepState, ): PermanentState { invariant( isExactlyTemporaryExtensionRepState(state), 'LexicalBuilder: Can not apply a permanent mark from state id %s (expected %s temporary)', String(state.id), String(ExtensionRepStateIds.temporary), ); return Object.assign(state, {id: ExtensionRepStateIds.permanent}); } export function applyConfiguredState( state: PermanentState, config: LexicalExtensionConfig, registerState: ExtensionInitState, ): ConfiguredState { return Object.assign(state, { config, id: ExtensionRepStateIds.configured, registerState, }); } export function applyInitializedState( state: ConfiguredState, initResult: LexicalExtensionInit, registerState: ExtensionBuildState>, ): InitializedState { return Object.assign(state, { id: ExtensionRepStateIds.initialized, initResult, registerState, }); } export function applyBuiltState( state: InitializedState, output: LexicalExtensionOutput, registerState: ExtensionRegisterState< LexicalExtensionInit, LexicalExtensionOutput >, ): BuiltState { return Object.assign(state, { id: ExtensionRepStateIds.built, output, registerState, }); } export function applyRegisteredState( state: BuiltState, ) { return Object.assign(state, { id: ExtensionRepStateIds.registered, }); } export function applyAfterRegistrationState< Extension extends AnyLexicalExtension, >(state: RegisteredState): AfterRegistrationState { return Object.assign(state, {id: ExtensionRepStateIds.afterRegistration}); } export function rollbackToBuiltState( state: AfterRegistrationState, ): BuiltState { return Object.assign(state, {id: ExtensionRepStateIds.built}); } const emptySet: ReadonlySet = new Set(); /** * @internal */ export class ExtensionRep { builder: LexicalBuilder; configs: Set>>; _dependency?: LexicalExtensionDependency; _peerNameSet?: Set; extension: Extension; state: ExtensionRepState; _signal?: AbortSignal; constructor(builder: LexicalBuilder, extension: Extension) { this.builder = builder; this.extension = extension; this.configs = new Set(); this.state = {id: ExtensionRepStateIds.unmarked}; } mergeConfigs(): LexicalExtensionConfig { let config: LexicalExtensionConfig = this.extension.config || {}; const mergeConfig = this.extension.mergeConfig ? this.extension.mergeConfig.bind(this.extension) : shallowMergeConfig; for (const cfg of this.configs) { config = mergeConfig(config, cfg); } return config; } init(editorConfig: InitialEditorConfig) { const initialState = this.state; invariant( isExactlyPermanentExtensionRepState(initialState), 'ExtensionRep: Can not configure from state id %s', String(initialState.id), ); const initState: ExtensionInitState = { getDependency: this.getInitDependency.bind(this), getDirectDependentNames: this.getDirectDependentNames.bind(this), getPeer: this.getInitPeer.bind(this), getPeerNameSet: this.getPeerNameSet.bind(this), }; const buildState: ExtensionBuildState> = { ...initState, getDependency: this.getDependency.bind(this), getInitResult: this.getInitResult.bind(this), getPeer: this.getPeer.bind(this), }; const state = applyConfiguredState( initialState, this.mergeConfigs(), initState, ); this.state = state; let initResult: LexicalExtensionInit | undefined; if (this.extension.init) { initResult = this.extension.init( editorConfig, state.config, initState, ) as LexicalExtensionInit; } this.state = applyInitializedState(state, initResult!, buildState); } build(editor: LexicalEditor) { const state = this.state; invariant( state.id === ExtensionRepStateIds.initialized, 'ExtensionRep: register called in state id %s (expected %s initialized)', String(state.id), String(ExtensionRepStateIds.built), ); let output: undefined | LexicalExtensionOutput; if (this.extension.build) { output = this.extension.build(editor, state.config, state.registerState); } const registerState: ExtensionRegisterState< LexicalExtensionInit, LexicalExtensionOutput > = { ...state.registerState, getOutput: () => output!, getSignal: this.getSignal.bind(this), }; this.state = applyBuiltState(state, output!, registerState); } register( editor: LexicalEditor, signal: AbortSignal, ): undefined | (() => void) { this._signal = signal; const state = this.state; invariant( state.id === ExtensionRepStateIds.built, 'ExtensionRep: register called in state id %s (expected %s built)', String(state.id), String(ExtensionRepStateIds.built), ); const cleanup = this.extension.register && this.extension.register(editor, state.config, state.registerState); this.state = applyRegisteredState(state); return () => { const afterRegistrationState = this.state; invariant( afterRegistrationState.id === ExtensionRepStateIds.afterRegistration, 'ExtensionRep: rollbackToBuiltState called in state id %s (expected %s afterRegistration)', String(state.id), String(ExtensionRepStateIds.afterRegistration), ); this.state = rollbackToBuiltState(afterRegistrationState); if (cleanup) { cleanup(); } }; } afterRegistration(editor: LexicalEditor): undefined | (() => void) { const state = this.state; invariant( state.id === ExtensionRepStateIds.registered, 'ExtensionRep: afterRegistration called in state id %s (expected %s registered)', String(state.id), String(ExtensionRepStateIds.registered), ); let rval: undefined | (() => void); if (this.extension.afterRegistration) { rval = this.extension.afterRegistration( editor, state.config, state.registerState, ); } this.state = applyAfterRegistrationState(state); return rval; } getSignal(): AbortSignal { invariant( this._signal !== undefined, 'ExtensionRep.getSignal() called before register', ); return this._signal; } getInitResult(): LexicalExtensionInit { invariant( this.extension.init !== undefined, 'ExtensionRep: getInitResult() called for Extension %s that does not define init', this.extension.name, ); const state = this.state; invariant( isInitializedExtensionRepState(state), 'ExtensionRep: getInitResult() called for ExtensionRep in state id %s < %s (initialized)', String(state.id), String(ExtensionRepStateIds.initialized), ); return state.initResult; } getInitPeer( name: PeerExtension['name'], ): | undefined | Omit, 'output' | 'init'> { const rep = this.builder.extensionNameMap.get(name); return rep ? rep.getExtensionInitDependency() : undefined; } getExtensionInitDependency(): Omit< LexicalExtensionDependency, 'output' | 'init' > { const state = this.state; invariant( isConfiguredExtensionRepState(state), 'ExtensionRep: getExtensionInitDependency called in state id %s (expected >= %s configured)', String(state.id), String(ExtensionRepStateIds.configured), ); return {config: state.config}; } getPeer( name: PeerExtension['name'], ): undefined | LexicalExtensionDependency { const rep = this.builder.extensionNameMap.get(name); return rep ? (rep.getExtensionDependency() as LexicalExtensionDependency) : undefined; } getInitDependency( dep: Dependency, ): Omit, 'output' | 'init'> { const rep = this.builder.getExtensionRep(dep); invariant( rep !== undefined, 'LexicalExtensionBuilder: Extension %s missing dependency extension %s to be in registry', this.extension.name, dep.name, ); return rep.getExtensionInitDependency(); } getDependency( dep: Dependency, ): LexicalExtensionDependency { const rep = this.builder.getExtensionRep(dep); invariant( rep !== undefined, 'LexicalExtensionBuilder: Extension %s missing dependency extension %s to be in registry', this.extension.name, dep.name, ); return rep.getExtensionDependency(); } getState(): AfterRegistrationState { const state = this.state; invariant( isAfterRegistrationState(state), 'ExtensionRep getState called in state id %s (expected %s afterRegistration)', String(state.id), String(ExtensionRepStateIds.afterRegistration), ); return state; } getDirectDependentNames(): ReadonlySet { return this.builder.incomingEdges.get(this.extension.name) || emptySet; } getPeerNameSet(): ReadonlySet { let s = this._peerNameSet; if (!s) { s = new Set( (this.extension.peerDependencies || []).map(([name]) => name), ); this._peerNameSet = s; } return s; } getExtensionDependency(): LexicalExtensionDependency { if (!this._dependency) { const state = this.state; invariant( isBuiltExtensionRepState(state), 'Extension %s used as a dependency before build', this.extension.name, ); this._dependency = { config: state.config, init: state.initResult, output: state.output, }; } return this._dependency; } }