import type { InlineEditor, KeyboardBindingContext } from '@blockeditor/inline'; import { type AttributeRenderer, type BaseTextAttributes, baseTextAttributes, type DeltaInsert, getDefaultAttributeRenderer, type InlineRange, KEYBOARD_ALLOW_DEFAULT, type KeyboardBindingHandler, } from '@blockeditor/inline'; import type { Y } from '@blockeditor/store'; import { z, type ZodObject, type ZodTypeAny } from 'zod'; export type InlineSpecs< TextAttributes extends BaseTextAttributes = BaseTextAttributes, > = { name: string; schema: ZodTypeAny; match: (delta: DeltaInsert) => boolean; renderer: AttributeRenderer; embed?: boolean; }; export type InlineMarkdownMatchAction< // @ts-expect-error We allow to covariance for TextAttributes in TextAttributes extends BaseTextAttributes = BaseTextAttributes, > = (props: { inlineEditor: InlineEditor; prefixText: string; inlineRange: InlineRange; pattern: RegExp; undoManager: Y.UndoManager; }) => ReturnType; export type InlineMarkdownMatch< TextAttributes extends BaseTextAttributes = BaseTextAttributes, > = { name: string; pattern: RegExp; action: InlineMarkdownMatchAction; }; export class InlineManager< in out TextAttributes extends BaseTextAttributes = BaseTextAttributes, > { private _specs: InlineSpecs[] = []; get specs() { return this._specs; } private _markdownMatches: InlineMarkdownMatch[] = []; get markdownMatches() { return this._markdownMatches; } registerSpecs(specs: InlineSpecs[]): void { this._specs = specs; } registerMarkdownMatches( markdownMatches: InlineMarkdownMatch[] ): void { this._markdownMatches = markdownMatches; } markdownShortcutHandler = ( context: KeyboardBindingContext, undoManager: Y.UndoManager ) => { const { inlineEditor, prefixText, inlineRange } = context; for (const match of this._markdownMatches) { const matchedText = prefixText.match(match.pattern); if (matchedText) { return match.action({ inlineEditor, prefixText, inlineRange, pattern: match.pattern, undoManager, }); } } return KEYBOARD_ALLOW_DEFAULT; }; embedChecker = (delta: DeltaInsert) => { for (const spec of this._specs) { if (spec.embed && spec.match(delta)) { return true; } } return false; }; getRenderer(): AttributeRenderer { const defaultRenderer = getDefaultAttributeRenderer(); const renderer: AttributeRenderer = (delta, selected) => { for (const spec of this._specs) { if (spec.match(delta)) { return spec.renderer(delta, selected); } } return defaultRenderer(delta, selected); }; return renderer; } getSchema(): ZodObject> { const defaultSchema = baseTextAttributes as unknown as ZodObject< Record >; const schema: ZodObject> = this._specs.reduce((acc, cur) => { const currentSchema = z.object({ [cur.name]: cur.schema, }) as ZodObject>; return acc.merge(currentSchema) as ZodObject< Record >; }, defaultSchema); return schema; } }