@blocknote/core
Version:
A "Notion-style" block-based extensible text editor built on top of Prosemirror and Tiptap.
1 lines • 17.4 kB
Source Map (JSON)
{"version":3,"file":"BlockNoteExtension-BJCLnwrE.cjs","names":[],"sources":["../src/schema/markGroups.ts","../src/util/Store.ts","../src/editor/managers/ExtensionManager/symbol.ts","../src/editor/BlockNoteExtension.ts"],"sourcesContent":["import { Editor, getExtensionField } from \"@tiptap/core\";\n\n/**\n * ProseMirror mark group for \"non-formatting\" marks: comments and the\n * suggestion/diff marks (the AI `insertion`/`deletion`/`modification` marks and\n * the Yjs `y-attributed-*` marks). These annotate content without representing\n * inline formatting and are ignored by BlockNote's content model\n * (`blocknoteIgnore`).\n *\n * They are the only marks allowed on `\"plain\"` blocks (e.g. code blocks), which\n * otherwise disallow all formatting marks. A block references this group rather\n * than the individual marks because each of them comes from an optional\n * extension (comments, AI suggestions, Yjs attribution) and so is only present\n * when that extension is configured.\n */\nexport const NON_FORMATTING_MARK_GROUP = \"annotation\";\n\n/**\n * The `marks` field value for `\"plain\"` blocks (e.g. code blocks), which hold\n * unstyled text and so disallow formatting marks but still allow the\n * non-formatting ones.\n *\n * Every mark in {@link NON_FORMATTING_MARK_GROUP} comes from an optional\n * extension, so a node spec that referenced the group unconditionally would make\n * ProseMirror throw \"Unknown mark type: 'annotation'\" while building the schema\n * of an editor that has none of them registered — an empty mark group is an\n * unknown reference just like a missing mark is. (This mirrors `suggestionMarks`\n * for the block-level suggestion group.)\n *\n * Returns the group name (which ProseMirror expands to every mark in it) when at\n * least one such mark is registered, or `\"\"` otherwise. Evaluated at\n * schema-build time, where the tiptap editor's fully-flattened extension list is\n * available on `this.editor`.\n */\nexport function nonFormattingMarks(editor: Editor | undefined): string {\n if (!editor) {\n return \"\";\n }\n const hasNonFormattingMark = editor.options.extensions.some((extension) => {\n if (extension.type !== \"mark\") {\n return false;\n }\n const group = getExtensionField(extension, \"group\") as string | undefined;\n return (\n typeof group === \"string\" &&\n group.split(\" \").includes(NON_FORMATTING_MARK_GROUP)\n );\n });\n return hasNonFormattingMark ? NON_FORMATTING_MARK_GROUP : \"\";\n}\n","// Vendored from https://github.com/TanStack/store/blob/main/packages/store/src/store.ts (MIT)\n//\n// BlockNote only ever used `Store` — never the `Derived`/`Effect` reactive graph that\n// makes up the rest of `@tanstack/store`, and that graph isn't tree-shakeable because\n// `setState` reaches into the scheduler. Since the `Store` type is part of BlockNote's\n// public API (every extension exposes one), owning these ~40 lines lets us keep that\n// surface stable instead of tracking a 0.x dependency's breaking changes.\n//\n// Behaviour matches `@tanstack/store@0.7.7`, minus the dependency graph and the\n// `updateFn`/`onSubscribe` options, which nothing used. `onUpdate` additionally receives\n// the new and previous state rather than requiring callers to close over the store.\n\n/**\n * The value a {@link Listener} is called with when a {@link Store} updates.\n */\nexport interface ListenerValue<T> {\n readonly prevVal: T;\n readonly currentVal: T;\n}\n\n/**\n * A callback invoked when a {@link Store}'s state changes.\n */\nexport type Listener<T> = (value: ListenerValue<T>) => void;\n\n/**\n * A new state, or a function deriving it from the previous state.\n */\nexport type Updater<T> = T | ((prev: T) => T);\n\nexport interface StoreOptions<TState> {\n /**\n * Called after the state has been updated, before listeners are notified.\n */\n onUpdate?: (state: TState, prevState: TState) => void;\n}\n\n/**\n * A minimal observable state container.\n *\n * Extensions expose one of these as their `store` so that both React (via\n * `useExtensionState`) and vanilla consumers can read and subscribe to their state.\n */\nexport class Store<TState> {\n listeners = new Set<Listener<TState>>();\n state: TState;\n prevState: TState;\n options?: StoreOptions<TState>;\n\n constructor(initialState: TState, options?: StoreOptions<TState>) {\n this.prevState = initialState;\n this.state = initialState;\n this.options = options;\n }\n\n subscribe = (listener: Listener<TState>) => {\n this.listeners.add(listener);\n\n return () => {\n this.listeners.delete(listener);\n };\n };\n\n /**\n * Update the store state, either with a new state or a function deriving it from the\n * previous one.\n */\n setState(updater: (prevState: TState) => TState): void;\n setState(updater: TState): void;\n setState(updater: Updater<TState>): void {\n this.prevState = this.state;\n this.state = isUpdaterFunction(updater) ? updater(this.prevState) : updater;\n\n flush(this);\n }\n\n /**\n * @internal Only to be called by {@link flush}.\n */\n _notify() {\n const value: ListenerValue<TState> = {\n prevVal: this.prevState,\n currentVal: this.state,\n };\n for (const listener of this.listeners) {\n listener(value);\n }\n }\n}\n\nfunction isUpdaterFunction<T>(updater: Updater<T>): updater is (prev: T) => T {\n return typeof updater === \"function\";\n}\n\n// Both `onUpdate` and listener notification can synchronously trigger further `setState`\n// calls — a callback that dispatches a ProseMirror transaction, for example. Rather than\n// recursing, re-entrant writes are queued and drained by the outermost flush, so a write\n// made during a callback still reaches every listener but the stack stays flat and\n// subscribers see the settled state once.\n//\n// `onUpdate` therefore runs *inside* the flush transaction: the store is queued and the\n// flush is marked in progress before the callback fires, so a nested write it makes is\n// coalesced into this drain rather than starting its own.\nlet isFlushing = false;\nconst pendingUpdates = new Set<Store<any>>();\n\nfunction flush(store: Store<any>) {\n pendingUpdates.add(store);\n\n const isOutermost = !isFlushing;\n isFlushing = true;\n\n try {\n store.options?.onUpdate?.(store.state, store.prevState);\n\n if (!isOutermost) {\n return;\n }\n\n while (pendingUpdates.size > 0) {\n const stores = Array.from(pendingUpdates);\n pendingUpdates.clear();\n for (const pendingStore of stores) {\n pendingStore._notify();\n }\n }\n } finally {\n if (isOutermost) {\n isFlushing = false;\n // A throwing callback would otherwise strand queued stores, letting them notify\n // during an unrelated store's next flush.\n pendingUpdates.clear();\n }\n }\n}\n","/**\n * Symbol used to track the original factory function for extensions.\n * This allows us to retrieve the original factory for comparison and other operations.\n */\nexport const originalFactorySymbol = Symbol(\"originalFactory\");\n","import { type AnyExtension } from \"@tiptap/core\";\nimport type { Plugin as ProsemirrorPlugin } from \"prosemirror-state\";\nimport type { PartialBlockNoDefaults } from \"../schema/index.js\";\nimport { Store, StoreOptions } from \"../util/Store.js\";\nimport type { BlockNoteEditor } from \"./BlockNoteEditor.js\";\nimport { originalFactorySymbol } from \"./managers/ExtensionManager/symbol.js\";\n\n/**\n * This function is called when the extension is destroyed.\n */\ntype OnDestroy = () => void;\n\n/**\n * Describes a BlockNote extension.\n */\nexport interface Extension<State = any, Key extends string = string> {\n /**\n * The unique identifier for the extension.\n */\n readonly key: Key;\n\n /**\n * Triggered when the extension is mounted to the editor.\n */\n readonly mount?: (ctx: {\n /**\n * The DOM element that the editor is mounted to.\n */\n dom: HTMLElement;\n /**\n * The root document of the {@link document} that the editor is mounted to.\n */\n root: Document | ShadowRoot;\n /**\n * An {@link AbortSignal} that will be aborted when the extension is destroyed.\n */\n signal: AbortSignal;\n }) => void | OnDestroy;\n\n /**\n * The store for the extension.\n */\n readonly store?: Store<State>;\n\n /**\n * Declares what {@link Extension}s that this extension depends on.\n */\n readonly runsBefore?: ReadonlyArray<string>;\n\n /**\n * Input rules for a block: An input rule is what is used to replace text in a block when a regular expression match is found.\n * As an example, typing `#` in a paragraph block will trigger an input rule to replace the text with a heading block.\n */\n readonly inputRules?: ReadonlyArray<InputRule>;\n\n /**\n * A mapping of a keyboard shortcut to a function that will be called when the shortcut is pressed\n *\n * The keys are in the format:\n * - Key names may be strings like `Shift-Ctrl-Enter`—a key identifier prefixed with zero or more modifiers\n * - Key identifiers are based on the strings that can appear in KeyEvent.key\n * - Use lowercase letters to refer to letter keys (or uppercase letters if you want shift to be held)\n * - You may use `Space` as an alias for the \" \" name\n * - Modifiers can be given in any order: `Shift-` (or `s-`), `Alt-` (or `a-`), `Ctrl-` (or `c-` or `Control-`) and `Cmd-` (or `m-` or `Meta-`)\n * - For characters that are created by holding shift, the Shift- prefix is implied, and should not be added explicitly\n * - You can use Mod- as a shorthand for Cmd- on Mac and Ctrl- on other platforms\n *\n * @example\n * ```typescript\n * keyboardShortcuts: {\n * \"Mod-Enter\": (ctx) => { return true; },\n * \"Shift-Ctrl-Space\": (ctx) => { return true; },\n * \"a\": (ctx) => { return true; },\n * \"Space\": (ctx) => { return true; }\n * }\n * ```\n */\n readonly keyboardShortcuts?: Record<\n string,\n (ctx: { editor: BlockNoteEditor<any, any, any> }) => boolean\n >;\n\n /**\n * Add additional prosemirror plugins to the editor.\n */\n readonly prosemirrorPlugins?: ReadonlyArray<ProsemirrorPlugin>;\n\n /**\n * Add additional tiptap extensions to the editor.\n */\n readonly tiptapExtensions?: ReadonlyArray<AnyExtension>;\n\n /**\n * Add additional BlockNote extensions to the editor.\n */\n readonly blockNoteExtensions?: ReadonlyArray<ExtensionFactoryInstance>;\n}\n\n/**\n * An input rule is what is used to replace text in a block when a regular expression match is found.\n * As an example, typing `#` in a paragraph block will trigger an input rule to replace the text with a heading block.\n */\ntype InputRule = {\n /**\n * The regex to match when to trigger the input rule\n */\n find: RegExp;\n /**\n * The function to call when the input rule is matched\n * @returns undefined if the input rule should not be triggered, or an object with the type and props to update the block\n */\n replace: (props: {\n /**\n * The result of the regex match\n */\n match: RegExpMatchArray;\n // TODO this will be a Point, when we have the Location API\n /**\n * The range of the text that was matched\n */\n range: { from: number; to: number };\n /**\n * The editor instance\n */\n editor: BlockNoteEditor<any, any, any>;\n }) => undefined | PartialBlockNoDefaults<any, any, any>;\n};\n\n/**\n * These are the arguments that are passed to an {@link ExtensionFactoryInstance}.\n */\nexport interface ExtensionOptions<\n Options extends Record<string, any> | undefined =\n | Record<string, any>\n | undefined,\n> {\n options: Options;\n editor: BlockNoteEditor<any, any, any>;\n}\n\n// a type that maps the extension key to the return type of the extension factory\nexport type ExtensionMap<T extends ReadonlyArray<ExtensionFactoryInstance>> = {\n [K in T[number] extends ExtensionFactoryInstance<infer Ext>\n ? Ext[\"key\"]\n : never]: T[number] extends ExtensionFactoryInstance<infer Ext>\n ? Ext\n : never;\n};\n\n/**\n * This is a type that represents the function which will actually create the extension.\n * It requires the editor instance to be passed in, but will already have the options applied automatically.\n *\n * @note Only the BlockNoteEditor should instantiate this function, not the user. Look at {@link createExtension} for user-facing functions.\n */\nexport type ExtensionFactoryInstance<\n Ext extends Extension<any, any> = Extension<any, any>,\n> = (ctx: Omit<ExtensionOptions<any>, \"options\">) => Ext;\n\n/**\n * This is the return type of the {@link createExtension} function.\n * It is a function that can be invoked with the extension's options to create a new extension factory.\n */\nexport type ExtensionFactory<\n State = any,\n Key extends string = string,\n Factory extends (ctx: any) => Extension<State, Key> = (\n ctx: ExtensionOptions<any>,\n ) => Extension<State, Key>,\n> =\n Parameters<Factory>[0] extends ExtensionOptions<infer Options>\n ? undefined extends Options\n ? (\n options?: Exclude<Options, undefined>,\n ) => ExtensionFactoryInstance<ReturnType<Factory>>\n : (options: Options) => ExtensionFactoryInstance<ReturnType<Factory>>\n : () => ExtensionFactoryInstance<ReturnType<Factory>>;\n\n/**\n * Constructs a BlockNote {@link ExtensionFactory} from a factory function or object\n */\n// This overload is for `createExtension({ key: \"test\", ... })`\nexport function createExtension<\n const State = any,\n const Key extends string = string,\n const Ext extends Extension<State, Key> = Extension<State, Key>,\n>(factory: Ext): ExtensionFactoryInstance<Ext>;\n// This overload is for `createExtension(({editor, options}) => ({ key: \"test\", ... }))`\nexport function createExtension<\n const State = any,\n const Options extends Record<string, any> | undefined = any,\n const Key extends string = string,\n const Factory extends (ctx: any) => Extension<State, Key> = (\n ctx: ExtensionOptions<Options>,\n ) => Extension<State, Key>,\n>(factory: Factory): ExtensionFactory<State, Key, Factory>;\n// This overload is for both of the above overloads as it is the implementation of the function\nexport function createExtension<\n const State = any,\n const Options extends Record<string, any> | undefined = any,\n const Key extends string = string,\n const Factory extends\n | Extension<State, Key>\n | ((ctx: any) => Extension<State, Key>) = (\n ctx: ExtensionOptions<Options>,\n ) => Extension<State, Key>,\n>(\n factory: Factory,\n): Factory extends Extension<State, Key>\n ? ExtensionFactoryInstance<Factory>\n : Factory extends (ctx: any) => Extension<State, Key>\n ? ExtensionFactory<State, Key, Factory>\n : never {\n if (typeof factory === \"object\" && \"key\" in factory) {\n return function factoryFn() {\n (factory as any)[originalFactorySymbol] = factoryFn;\n return factory;\n } as any;\n }\n\n if (typeof factory !== \"function\") {\n throw new Error(\"factory must be a function\");\n }\n\n return function factoryFn(options: Options) {\n return (ctx: { editor: BlockNoteEditor<any, any, any> }) => {\n const extension = factory({ editor: ctx.editor, options });\n // We stick a symbol onto the extension to allow us to retrieve the original factory for comparison later.\n // This enables us to do things like: `editor.getExtension(YSync).prosemirrorPlugins`\n (extension as any)[originalFactorySymbol] = factoryFn;\n return extension;\n };\n } as any;\n}\n\nexport function createStore<T = any>(\n initialState: T,\n options?: StoreOptions<T>,\n): Store<T> {\n return new Store(initialState, options);\n}\n"],"mappings":"8BAeA,IAAa,EAA4B,aAmBzC,SAAgB,EAAmB,EAAoC,CAcrE,OAbK,GAGwB,EAAO,QAAQ,WAAW,KAAM,GAAc,CACzE,GAAI,EAAU,OAAS,OACrB,MAAO,GAET,IAAM,GAAA,EAAQ,EAAA,kBAAA,CAAkB,EAAW,OAAO,EAClD,OACE,OAAO,GAAU,UACjB,EAAM,MAAM,GAAG,CAAC,CAAC,SAAA,YAAkC,CAEvD,CACO,EAAuB,EAA4B,EAC5D,CCNA,IAAa,EAAb,KAA2B,CACzB,UAAY,IAAI,IAChB,MACA,UACA,QAEA,YAAY,EAAsB,EAAgC,CAChE,KAAK,UAAY,EACjB,KAAK,MAAQ,EACb,KAAK,QAAU,CACjB,CAEA,UAAa,IACX,KAAK,UAAU,IAAI,CAAQ,MAEd,CACX,KAAK,UAAU,OAAO,CAAQ,CAChC,GASF,SAAS,EAAgC,CACvC,KAAK,UAAY,KAAK,MACtB,KAAK,MAAQ,EAAkB,CAAO,EAAI,EAAQ,KAAK,SAAS,EAAI,EAEpE,EAAM,IAAI,CACZ,CAKA,SAAU,CACR,IAAM,EAA+B,CACnC,QAAS,KAAK,UACd,WAAY,KAAK,KACnB,EACA,IAAK,IAAM,KAAY,KAAK,UAC1B,EAAS,CAAK,CAElB,CACF,EAEA,SAAS,EAAqB,EAAgD,CAC5E,OAAO,OAAO,GAAY,UAC5B,CAWA,IAAI,EAAa,GACX,EAAiB,IAAI,IAE3B,SAAS,EAAM,EAAmB,CAChC,EAAe,IAAI,CAAK,EAExB,IAAM,EAAc,CAAC,EACrB,EAAa,GAEb,GAAI,CAGF,GAFA,EAAM,SAAS,WAAW,EAAM,MAAO,EAAM,SAAS,EAElD,CAAC,EACH,OAGF,KAAO,EAAe,KAAO,GAAG,CAC9B,IAAM,EAAS,MAAM,KAAK,CAAc,EACxC,EAAe,MAAM,EACrB,IAAK,IAAM,KAAgB,EACzB,EAAa,QAAQ,CAEzB,CACF,QAAU,CACJ,IACF,EAAa,GAGb,EAAe,MAAM,EAEzB,CACF,CClIA,IAAa,EAAwB,OAAO,iBAAiB,ECiM7D,SAAgB,EAUd,EAKU,CACV,GAAI,OAAO,GAAY,UAAY,QAAS,EAC1C,OAAO,SAAS,GAAY,CAE1B,MADA,GAAiB,GAAyB,EACnC,CACT,EAGF,GAAI,OAAO,GAAY,WACrB,MAAU,MAAM,4BAA4B,EAG9C,OAAO,SAAS,EAAU,EAAkB,CAC1C,MAAQ,IAAoD,CAC1D,IAAM,EAAY,EAAQ,CAAE,OAAQ,EAAI,OAAQ,SAAQ,CAAC,EAIzD,MADA,GAAmB,GAAyB,EACrC,CACT,CACF,CACF,CAEA,SAAgB,EACd,EACA,EACU,CACV,OAAO,IAAI,EAAM,EAAc,CAAO,CACxC"}