feat(editor): std inline extensions (#11038)

This commit is contained in:
Saul-Mirone
2025-03-20 10:40:11 +00:00
parent b24376a9f7
commit 77e659d0b0
16 changed files with 552 additions and 501 deletions

View File

@@ -0,0 +1,4 @@
export * from './inline-manager';
export * from './inline-spec';
export * from './markdown-matcher';
export * from './type';

View File

@@ -0,0 +1,117 @@
import {
createIdentifier,
type ServiceIdentifier,
} from '@blocksuite/global/di';
import {
type BaseTextAttributes,
baseTextAttributes,
type DeltaInsert,
type ExtensionType,
} from '@blocksuite/store';
import { z, type ZodObject, type ZodTypeAny } from 'zod';
import { StdIdentifier } from '../../identifier.js';
import type { BlockStdScope } from '../../scope/index.js';
import type { AttributeRenderer } from '../types.js';
import { getDefaultAttributeRenderer } from '../utils/attribute-renderer.js';
import { MarkdownMatcherIdentifier } from './markdown-matcher.js';
import type { InlineMarkdownMatch, InlineSpecs } from './type.js';
export class InlineManager<TextAttributes extends BaseTextAttributes> {
embedChecker = (delta: DeltaInsert<TextAttributes>) => {
for (const spec of this.specs) {
if (spec.embed && spec.match(delta)) {
return true;
}
}
return false;
};
getRenderer = (): AttributeRenderer<TextAttributes> => {
const defaultRenderer = getDefaultAttributeRenderer<TextAttributes>();
const renderer: AttributeRenderer<TextAttributes> = props => {
// Priority increases from front to back
for (const spec of this.specs.toReversed()) {
if (spec.match(props.delta)) {
return spec.renderer(props);
}
}
return defaultRenderer(props);
};
return renderer;
};
getSchema = (): ZodObject<Record<keyof TextAttributes, ZodTypeAny>> => {
const defaultSchema = baseTextAttributes as unknown as ZodObject<
Record<keyof TextAttributes, ZodTypeAny>
>;
const schema: ZodObject<Record<keyof TextAttributes, ZodTypeAny>> =
this.specs.reduce((acc, cur) => {
const currentSchema = z.object({
[cur.name]: cur.schema,
}) as ZodObject<Record<keyof TextAttributes, ZodTypeAny>>;
return acc.merge(currentSchema) as ZodObject<
Record<keyof TextAttributes, ZodTypeAny>
>;
}, defaultSchema);
return schema;
};
get markdownMatches(): InlineMarkdownMatch<TextAttributes>[] {
if (!this.enableMarkdown) {
return [];
}
const matches = Array.from(
this.std.provider.getAll(MarkdownMatcherIdentifier).values()
);
return matches as InlineMarkdownMatch<TextAttributes>[];
}
readonly specs: Array<InlineSpecs<TextAttributes>>;
constructor(
readonly std: BlockStdScope,
readonly enableMarkdown: boolean,
...specs: Array<InlineSpecs<TextAttributes>>
) {
this.specs = specs;
}
}
export type InlineManagerExtensionConfig<
TextAttributes extends BaseTextAttributes,
> = {
id: string;
enableMarkdown?: boolean;
specs: ServiceIdentifier<InlineSpecs<TextAttributes>>[];
};
const InlineManagerIdentifier = createIdentifier<unknown>(
'AffineInlineManager'
);
export function InlineManagerExtension<
TextAttributes extends BaseTextAttributes,
>({
id,
enableMarkdown = true,
specs,
}: InlineManagerExtensionConfig<TextAttributes>): ExtensionType & {
identifier: ServiceIdentifier<InlineManager<TextAttributes>>;
} {
const identifier = InlineManagerIdentifier<InlineManager<TextAttributes>>(id);
return {
setup: di => {
di.addImpl(identifier, provider => {
return new InlineManager(
provider.get(StdIdentifier),
enableMarkdown,
...specs.map(spec => provider.get(spec))
);
});
},
identifier,
};
}

View File

@@ -0,0 +1,49 @@
import {
createIdentifier,
type ServiceIdentifier,
type ServiceProvider,
} from '@blocksuite/global/di';
import type { BaseTextAttributes, ExtensionType } from '@blocksuite/store';
import type { InlineSpecs } from './type.js';
export const InlineSpecIdentifier =
createIdentifier<unknown>('AffineInlineSpec');
export function InlineSpecExtension<TextAttributes extends BaseTextAttributes>(
name: string,
getSpec: (provider: ServiceProvider) => InlineSpecs<TextAttributes>
): ExtensionType & {
identifier: ServiceIdentifier<InlineSpecs<TextAttributes>>;
};
export function InlineSpecExtension<TextAttributes extends BaseTextAttributes>(
spec: InlineSpecs<TextAttributes>
): ExtensionType & {
identifier: ServiceIdentifier<InlineSpecs<TextAttributes>>;
};
export function InlineSpecExtension<TextAttributes extends BaseTextAttributes>(
nameOrSpec: string | InlineSpecs<TextAttributes>,
getSpec?: (provider: ServiceProvider) => InlineSpecs<TextAttributes>
): ExtensionType & {
identifier: ServiceIdentifier<InlineSpecs<TextAttributes>>;
} {
if (typeof nameOrSpec === 'string') {
const identifier =
InlineSpecIdentifier<InlineSpecs<TextAttributes>>(nameOrSpec);
return {
identifier,
setup: di => {
di.addImpl(identifier, provider => getSpec!(provider));
},
};
}
const identifier = InlineSpecIdentifier<InlineSpecs<TextAttributes>>(
nameOrSpec.name as string
);
return {
identifier,
setup: di => {
di.addImpl(identifier, nameOrSpec);
},
};
}

View File

@@ -0,0 +1,30 @@
import {
createIdentifier,
type ServiceIdentifier,
} from '@blocksuite/global/di';
import type { BaseTextAttributes, ExtensionType } from '@blocksuite/store';
import type { InlineMarkdownMatch } from './type.js';
export const MarkdownMatcherIdentifier = createIdentifier<unknown>(
'AffineMarkdownMatcher'
);
export function InlineMarkdownExtension<
TextAttributes extends BaseTextAttributes,
>(
matcher: InlineMarkdownMatch<TextAttributes>
): ExtensionType & {
identifier: ServiceIdentifier<InlineMarkdownMatch<TextAttributes>>;
} {
const identifier = MarkdownMatcherIdentifier<
InlineMarkdownMatch<TextAttributes>
>(matcher.name);
return {
setup: di => {
di.addImpl(identifier, () => ({ ...matcher }));
},
identifier,
};
}

View File

@@ -0,0 +1,37 @@
import type {
AttributeRenderer,
InlineEditor,
InlineRange,
} from '@blocksuite/block-std/inline';
import type { BaseTextAttributes, DeltaInsert } from '@blocksuite/store';
import type * as Y from 'yjs';
import type { ZodTypeAny } from 'zod';
export type InlineSpecs<
TextAttributes extends BaseTextAttributes = BaseTextAttributes,
> = {
name: keyof TextAttributes | string;
schema: ZodTypeAny;
match: (delta: DeltaInsert<TextAttributes>) => boolean;
renderer: AttributeRenderer<TextAttributes>;
embed?: boolean;
};
export type InlineMarkdownMatchAction<
// @ts-expect-error We allow to covariance for AffineTextAttributes
in AffineTextAttributes extends BaseTextAttributes = BaseTextAttributes,
> = (props: {
inlineEditor: InlineEditor<AffineTextAttributes>;
prefixText: string;
inlineRange: InlineRange;
pattern: RegExp;
undoManager: Y.UndoManager;
}) => void;
export type InlineMarkdownMatch<
AffineTextAttributes extends BaseTextAttributes = BaseTextAttributes,
> = {
name: string;
pattern: RegExp;
action: InlineMarkdownMatchAction<AffineTextAttributes>;
};

View File

@@ -1,5 +1,6 @@
export * from './components';
export * from './consts';
export * from './extensions';
export * from './inline-editor';
export * from './range';
export * from './services';