From 77e659d0b035233f32b3c46ca979230647bb8a9a Mon Sep 17 00:00:00 2001 From: Saul-Mirone Date: Thu, 20 Mar 2025 10:40:11 +0000 Subject: [PATCH] feat(editor): std inline extensions (#11038) --- .../block-code/src/code-block-inline.ts | 57 +-- .../affine/rich-text/src/all-extensions.ts | 36 +- .../affine/rich-text/src/extension/index.ts | 4 - .../rich-text/src/extension/inline-manager.ts | 106 ------ .../rich-text/src/extension/inline-spec.ts | 47 --- .../src/extension/markdown-matcher.ts | 27 -- .../src/inline/presets/affine-inline-specs.ts | 221 ++++++------ .../rich-text/src/inline/presets/markdown.ts | 330 ++++++++++-------- .../nodes/latex-node/latex-editor-menu.ts | 14 +- blocksuite/affine/rich-text/src/rich-text.ts | 2 +- .../block-std/src/inline/extensions/index.ts | 4 + .../src/inline/extensions/inline-manager.ts | 117 +++++++ .../src/inline/extensions/inline-spec.ts | 49 +++ .../src/inline/extensions/markdown-matcher.ts | 30 ++ .../block-std/src/inline/extensions}/type.ts | 8 +- .../framework/block-std/src/inline/index.ts | 1 + 16 files changed, 552 insertions(+), 501 deletions(-) delete mode 100644 blocksuite/affine/rich-text/src/extension/inline-manager.ts delete mode 100644 blocksuite/affine/rich-text/src/extension/inline-spec.ts delete mode 100644 blocksuite/affine/rich-text/src/extension/markdown-matcher.ts create mode 100644 blocksuite/framework/block-std/src/inline/extensions/index.ts create mode 100644 blocksuite/framework/block-std/src/inline/extensions/inline-manager.ts create mode 100644 blocksuite/framework/block-std/src/inline/extensions/inline-spec.ts create mode 100644 blocksuite/framework/block-std/src/inline/extensions/markdown-matcher.ts rename blocksuite/{affine/rich-text/src/extension => framework/block-std/src/inline/extensions}/type.ts (79%) diff --git a/blocksuite/affine/blocks/block-code/src/code-block-inline.ts b/blocksuite/affine/blocks/block-code/src/code-block-inline.ts index 302972cfb2..63f585b852 100644 --- a/blocksuite/affine/blocks/block-code/src/code-block-inline.ts +++ b/blocksuite/affine/blocks/block-code/src/code-block-inline.ts @@ -3,39 +3,44 @@ import { BoldInlineSpecExtension, CodeInlineSpecExtension, ColorInlineSpecExtension, - InlineManagerExtension, - InlineSpecExtension, ItalicInlineSpecExtension, LatexInlineSpecExtension, LinkInlineSpecExtension, StrikeInlineSpecExtension, UnderlineInlineSpecExtension, } from '@blocksuite/affine-rich-text'; +import type { AffineTextAttributes } from '@blocksuite/affine-shared/types'; +import { + InlineManagerExtension, + InlineSpecExtension, +} from '@blocksuite/block-std/inline'; import { html } from 'lit'; import { z } from 'zod'; -export const CodeBlockUnitSpecExtension = InlineSpecExtension({ - name: 'code-block-unit', - schema: z.undefined(), - match: () => true, - renderer: ({ delta }) => { - return html``; - }, -}); +export const CodeBlockUnitSpecExtension = + InlineSpecExtension({ + name: 'code-block-unit', + schema: z.undefined(), + match: () => true, + renderer: ({ delta }) => { + return html``; + }, + }); -export const CodeBlockInlineManagerExtension = InlineManagerExtension({ - id: 'CodeBlockInlineManager', - enableMarkdown: false, - specs: [ - BoldInlineSpecExtension.identifier, - ItalicInlineSpecExtension.identifier, - UnderlineInlineSpecExtension.identifier, - StrikeInlineSpecExtension.identifier, - CodeInlineSpecExtension.identifier, - BackgroundInlineSpecExtension.identifier, - ColorInlineSpecExtension.identifier, - LatexInlineSpecExtension.identifier, - LinkInlineSpecExtension.identifier, - CodeBlockUnitSpecExtension.identifier, - ], -}); +export const CodeBlockInlineManagerExtension = + InlineManagerExtension({ + id: 'CodeBlockInlineManager', + enableMarkdown: false, + specs: [ + BoldInlineSpecExtension.identifier, + ItalicInlineSpecExtension.identifier, + UnderlineInlineSpecExtension.identifier, + StrikeInlineSpecExtension.identifier, + CodeInlineSpecExtension.identifier, + BackgroundInlineSpecExtension.identifier, + ColorInlineSpecExtension.identifier, + LatexInlineSpecExtension.identifier, + LinkInlineSpecExtension.identifier, + CodeBlockUnitSpecExtension.identifier, + ], + }); diff --git a/blocksuite/affine/rich-text/src/all-extensions.ts b/blocksuite/affine/rich-text/src/all-extensions.ts index a0213db486..7d909186b6 100644 --- a/blocksuite/affine/rich-text/src/all-extensions.ts +++ b/blocksuite/affine/rich-text/src/all-extensions.ts @@ -1,6 +1,7 @@ +import type { AffineTextAttributes } from '@blocksuite/affine-shared/types'; +import { InlineManagerExtension } from '@blocksuite/block-std/inline'; import type { ExtensionType } from '@blocksuite/store'; -import { InlineManagerExtension } from './extension/index.js'; import { BackgroundInlineSpecExtension, BoldInlineSpecExtension, @@ -19,22 +20,23 @@ import { } from './inline/index.js'; import { LatexEditorInlineManagerExtension } from './inline/presets/nodes/latex-node/latex-editor-menu.js'; -export const DefaultInlineManagerExtension = InlineManagerExtension({ - id: 'DefaultInlineManager', - specs: [ - BoldInlineSpecExtension.identifier, - ItalicInlineSpecExtension.identifier, - UnderlineInlineSpecExtension.identifier, - StrikeInlineSpecExtension.identifier, - CodeInlineSpecExtension.identifier, - BackgroundInlineSpecExtension.identifier, - ColorInlineSpecExtension.identifier, - LatexInlineSpecExtension.identifier, - ReferenceInlineSpecExtension.identifier, - LinkInlineSpecExtension.identifier, - FootNoteInlineSpecExtension.identifier, - ], -}); +export const DefaultInlineManagerExtension = + InlineManagerExtension({ + id: 'DefaultInlineManager', + specs: [ + BoldInlineSpecExtension.identifier, + ItalicInlineSpecExtension.identifier, + UnderlineInlineSpecExtension.identifier, + StrikeInlineSpecExtension.identifier, + CodeInlineSpecExtension.identifier, + BackgroundInlineSpecExtension.identifier, + ColorInlineSpecExtension.identifier, + LatexInlineSpecExtension.identifier, + ReferenceInlineSpecExtension.identifier, + LinkInlineSpecExtension.identifier, + FootNoteInlineSpecExtension.identifier, + ], + }); export const RichTextExtensions: ExtensionType[] = [ InlineSpecExtensions, diff --git a/blocksuite/affine/rich-text/src/extension/index.ts b/blocksuite/affine/rich-text/src/extension/index.ts index 5d8771e273..5e8d60898e 100644 --- a/blocksuite/affine/rich-text/src/extension/index.ts +++ b/blocksuite/affine/rich-text/src/extension/index.ts @@ -1,5 +1 @@ -export * from './inline-manager.js'; -export * from './inline-spec.js'; -export * from './markdown-matcher.js'; export * from './ref-node-slots.js'; -export * from './type.js'; diff --git a/blocksuite/affine/rich-text/src/extension/inline-manager.ts b/blocksuite/affine/rich-text/src/extension/inline-manager.ts deleted file mode 100644 index 77bfc49487..0000000000 --- a/blocksuite/affine/rich-text/src/extension/inline-manager.ts +++ /dev/null @@ -1,106 +0,0 @@ -import type { AffineTextAttributes } from '@blocksuite/affine-shared/types'; -import { type BlockStdScope, StdIdentifier } from '@blocksuite/block-std'; -import { - type AttributeRenderer, - getDefaultAttributeRenderer, -} from '@blocksuite/block-std/inline'; -import { - createIdentifier, - type ServiceIdentifier, -} from '@blocksuite/global/di'; -import { - baseTextAttributes, - type DeltaInsert, - type ExtensionType, -} from '@blocksuite/store'; -import { z, type ZodObject, type ZodTypeAny } from 'zod'; - -import { MarkdownMatcherIdentifier } from './markdown-matcher.js'; -import type { InlineMarkdownMatch, InlineSpecs } from './type.js'; - -export class InlineManager { - 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 = 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> => { - 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; - }; - - readonly specs: Array>; - - constructor( - readonly std: BlockStdScope, - readonly markdownMatches: InlineMarkdownMatch[], - ...specs: Array> - ) { - this.specs = specs; - } -} - -export const InlineManagerIdentifier = createIdentifier( - 'AffineInlineManager' -); - -export type InlineManagerExtensionConfig = { - id: string; - enableMarkdown?: boolean; - specs: ServiceIdentifier>[]; -}; - -export function InlineManagerExtension({ - id, - enableMarkdown = true, - specs, -}: InlineManagerExtensionConfig): ExtensionType & { - identifier: ServiceIdentifier; -} { - const identifier = InlineManagerIdentifier(id); - return { - setup: di => { - di.addImpl(identifier, provider => { - return new InlineManager( - provider.get(StdIdentifier), - enableMarkdown - ? Array.from(provider.getAll(MarkdownMatcherIdentifier).values()) - : [], - ...specs.map(spec => provider.get(spec)) - ); - }); - }, - identifier, - }; -} diff --git a/blocksuite/affine/rich-text/src/extension/inline-spec.ts b/blocksuite/affine/rich-text/src/extension/inline-spec.ts deleted file mode 100644 index cc291bd405..0000000000 --- a/blocksuite/affine/rich-text/src/extension/inline-spec.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { AffineTextAttributes } from '@blocksuite/affine-shared/types'; -import { - createIdentifier, - type ServiceIdentifier, - type ServiceProvider, -} from '@blocksuite/global/di'; -import type { ExtensionType } from '@blocksuite/store'; - -import type { InlineSpecs } from './type.js'; - -export const InlineSpecIdentifier = - createIdentifier>('AffineInlineSpec'); - -export function InlineSpecExtension( - name: string, - getSpec: (provider: ServiceProvider) => InlineSpecs -): ExtensionType & { - identifier: ServiceIdentifier>; -}; -export function InlineSpecExtension( - spec: InlineSpecs -): ExtensionType & { - identifier: ServiceIdentifier>; -}; -export function InlineSpecExtension( - nameOrSpec: string | InlineSpecs, - getSpec?: (provider: ServiceProvider) => InlineSpecs -): ExtensionType & { - identifier: ServiceIdentifier>; -} { - if (typeof nameOrSpec === 'string') { - const identifier = InlineSpecIdentifier(nameOrSpec); - return { - identifier, - setup: di => { - di.addImpl(identifier, provider => getSpec!(provider)); - }, - }; - } - const identifier = InlineSpecIdentifier(nameOrSpec.name); - return { - identifier, - setup: di => { - di.addImpl(identifier, nameOrSpec); - }, - }; -} diff --git a/blocksuite/affine/rich-text/src/extension/markdown-matcher.ts b/blocksuite/affine/rich-text/src/extension/markdown-matcher.ts deleted file mode 100644 index b63018537f..0000000000 --- a/blocksuite/affine/rich-text/src/extension/markdown-matcher.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { AffineTextAttributes } from '@blocksuite/affine-shared/types'; -import { - createIdentifier, - type ServiceIdentifier, -} from '@blocksuite/global/di'; -import type { ExtensionType } from '@blocksuite/store'; - -import type { InlineMarkdownMatch } from './type.js'; - -export const MarkdownMatcherIdentifier = createIdentifier< - InlineMarkdownMatch ->('AffineMarkdownMatcher'); - -export function InlineMarkdownExtension( - matcher: InlineMarkdownMatch -): ExtensionType & { - identifier: ServiceIdentifier>; -} { - const identifier = MarkdownMatcherIdentifier(matcher.name); - - return { - setup: di => { - di.addImpl(identifier, () => ({ ...matcher })); - }, - identifier, - }; -} diff --git a/blocksuite/affine/rich-text/src/inline/presets/affine-inline-specs.ts b/blocksuite/affine/rich-text/src/inline/presets/affine-inline-specs.ts index c6baf1e59f..417b92e2b3 100644 --- a/blocksuite/affine/rich-text/src/inline/presets/affine-inline-specs.ts +++ b/blocksuite/affine/rich-text/src/inline/presets/affine-inline-specs.ts @@ -2,14 +2,14 @@ import { FootNoteSchema, ReferenceInfoSchema } from '@blocksuite/affine-model'; import { ToolbarModuleExtension } from '@blocksuite/affine-shared/services'; import type { AffineTextAttributes } from '@blocksuite/affine-shared/types'; import { BlockFlavourIdentifier, StdIdentifier } from '@blocksuite/block-std'; -import type { - InlineEditor, - InlineRootElement, +import { + type InlineEditor, + type InlineRootElement, + InlineSpecExtension, } from '@blocksuite/block-std/inline'; import { html } from 'lit'; import { z } from 'zod'; -import { InlineSpecExtension } from '../../extension/index.js'; import { FootNoteNodeConfigIdentifier } from './nodes/footnote-node/footnote-config.js'; import { builtinInlineLinkToolbarConfig } from './nodes/link-node/configs/toolbar.js'; import { builtinInlineReferenceToolbarConfig } from './nodes/reference-node/configs/toolbar.js'; @@ -21,86 +21,92 @@ import { export type AffineInlineEditor = InlineEditor; export type AffineInlineRootElement = InlineRootElement; -export const BoldInlineSpecExtension = InlineSpecExtension({ - name: 'bold', - schema: z.literal(true).optional().nullable().catch(undefined), - match: delta => { - return !!delta.attributes?.bold; - }, - renderer: ({ delta }) => { - return html``; - }, -}); +export const BoldInlineSpecExtension = + InlineSpecExtension({ + name: 'bold', + schema: z.literal(true).optional().nullable().catch(undefined), + match: delta => { + return !!delta.attributes?.bold; + }, + renderer: ({ delta }) => { + return html``; + }, + }); -export const ItalicInlineSpecExtension = InlineSpecExtension({ - name: 'italic', - schema: z.literal(true).optional().nullable().catch(undefined), - match: delta => { - return !!delta.attributes?.italic; - }, - renderer: ({ delta }) => { - return html``; - }, -}); +export const ItalicInlineSpecExtension = + InlineSpecExtension({ + name: 'italic', + schema: z.literal(true).optional().nullable().catch(undefined), + match: delta => { + return !!delta.attributes?.italic; + }, + renderer: ({ delta }) => { + return html``; + }, + }); -export const UnderlineInlineSpecExtension = InlineSpecExtension({ - name: 'underline', - schema: z.literal(true).optional().nullable().catch(undefined), - match: delta => { - return !!delta.attributes?.underline; - }, - renderer: ({ delta }) => { - return html``; - }, -}); +export const UnderlineInlineSpecExtension = + InlineSpecExtension({ + name: 'underline', + schema: z.literal(true).optional().nullable().catch(undefined), + match: delta => { + return !!delta.attributes?.underline; + }, + renderer: ({ delta }) => { + return html``; + }, + }); -export const StrikeInlineSpecExtension = InlineSpecExtension({ - name: 'strike', - schema: z.literal(true).optional().nullable().catch(undefined), - match: delta => { - return !!delta.attributes?.strike; - }, - renderer: ({ delta }) => { - return html``; - }, -}); +export const StrikeInlineSpecExtension = + InlineSpecExtension({ + name: 'strike', + schema: z.literal(true).optional().nullable().catch(undefined), + match: delta => { + return !!delta.attributes?.strike; + }, + renderer: ({ delta }) => { + return html``; + }, + }); -export const CodeInlineSpecExtension = InlineSpecExtension({ - name: 'code', - schema: z.literal(true).optional().nullable().catch(undefined), - match: delta => { - return !!delta.attributes?.code; - }, - renderer: ({ delta }) => { - return html``; - }, -}); +export const CodeInlineSpecExtension = + InlineSpecExtension({ + name: 'code', + schema: z.literal(true).optional().nullable().catch(undefined), + match: delta => { + return !!delta.attributes?.code; + }, + renderer: ({ delta }) => { + return html``; + }, + }); -export const BackgroundInlineSpecExtension = InlineSpecExtension({ - name: 'background', - schema: z.string().optional().nullable().catch(undefined), - match: delta => { - return !!delta.attributes?.background; - }, - renderer: ({ delta }) => { - return html``; - }, -}); +export const BackgroundInlineSpecExtension = + InlineSpecExtension({ + name: 'background', + schema: z.string().optional().nullable().catch(undefined), + match: delta => { + return !!delta.attributes?.background; + }, + renderer: ({ delta }) => { + return html``; + }, + }); -export const ColorInlineSpecExtension = InlineSpecExtension({ - name: 'color', - schema: z.string().optional().nullable().catch(undefined), - match: delta => { - return !!delta.attributes?.color; - }, - renderer: ({ delta }) => { - return html``; - }, -}); +export const ColorInlineSpecExtension = + InlineSpecExtension({ + name: 'color', + schema: z.string().optional().nullable().catch(undefined), + match: delta => { + return !!delta.attributes?.color; + }, + renderer: ({ delta }) => { + return html``; + }, + }); -export const LatexInlineSpecExtension = InlineSpecExtension( - 'latex', - provider => { +export const LatexInlineSpecExtension = + InlineSpecExtension('latex', provider => { const std = provider.get(StdIdentifier); return { name: 'latex', @@ -118,12 +124,10 @@ export const LatexInlineSpecExtension = InlineSpecExtension( }, embed: true, }; - } -); + }); -export const ReferenceInlineSpecExtension = InlineSpecExtension( - 'reference', - provider => { +export const ReferenceInlineSpecExtension = + InlineSpecExtension('reference', provider => { const std = provider.get(StdIdentifier); const configProvider = new ReferenceNodeConfigProvider(std); const config = @@ -164,35 +168,35 @@ export const ReferenceInlineSpecExtension = InlineSpecExtension( }, embed: true, }; - } -); + }); -export const LinkInlineSpecExtension = InlineSpecExtension('link', provider => { - const std = provider.get(StdIdentifier); - return { - name: 'link', - schema: z.string().optional().nullable().catch(undefined), - match: delta => { - return !!delta.attributes?.link; - }, +export const LinkInlineSpecExtension = + InlineSpecExtension('link', provider => { + const std = provider.get(StdIdentifier); + return { + name: 'link', + schema: z.string().optional().nullable().catch(undefined), + match: delta => { + return !!delta.attributes?.link; + }, + renderer: ({ delta }) => { + return html``; + }, + }; + }); + +export const LatexEditorUnitSpecExtension = + InlineSpecExtension({ + name: 'latex-editor-unit', + schema: z.undefined(), + match: () => true, renderer: ({ delta }) => { - return html``; + return html``; }, - }; -}); + }); -export const LatexEditorUnitSpecExtension = InlineSpecExtension({ - name: 'latex-editor-unit', - schema: z.undefined(), - match: () => true, - renderer: ({ delta }) => { - return html``; - }, -}); - -export const FootNoteInlineSpecExtension = InlineSpecExtension( - 'footnote', - provider => { +export const FootNoteInlineSpecExtension = + InlineSpecExtension('footnote', provider => { const std = provider.get(StdIdentifier); const config = provider.getOptional(FootNoteNodeConfigIdentifier) ?? undefined; @@ -211,8 +215,7 @@ export const FootNoteInlineSpecExtension = InlineSpecExtension( }, embed: true, }; - } -); + }); export const InlineSpecExtensions = [ BoldInlineSpecExtension, diff --git a/blocksuite/affine/rich-text/src/inline/presets/markdown.ts b/blocksuite/affine/rich-text/src/inline/presets/markdown.ts index 4c1b2d41ce..689f3f49be 100644 --- a/blocksuite/affine/rich-text/src/inline/presets/markdown.ts +++ b/blocksuite/affine/rich-text/src/inline/presets/markdown.ts @@ -1,8 +1,8 @@ +import type { AffineTextAttributes } from '@blocksuite/affine-shared/types'; import type { BlockComponent } from '@blocksuite/block-std'; +import { InlineMarkdownExtension } from '@blocksuite/block-std/inline'; import type { ExtensionType } from '@blocksuite/store'; -import { InlineMarkdownExtension } from '../../extension/markdown-matcher.js'; - // inline markdown match rules: // covert: ***test*** + space // covert: ***t est*** + space @@ -10,63 +10,71 @@ import { InlineMarkdownExtension } from '../../extension/markdown-matcher.js'; // not convert: ***test *** + space // not convert: *** test *** + space -export const BoldItalicMarkdown = InlineMarkdownExtension({ - name: 'bolditalic', - pattern: /.*\*{3}([^\s*][^*]*[^\s*])\*{3}$|.*\*{3}([^\s*])\*{3}$/, - action: ({ inlineEditor, prefixText, inlineRange, pattern, undoManager }) => { - const match = prefixText.match(pattern); - if (!match) return; +export const BoldItalicMarkdown = InlineMarkdownExtension( + { + name: 'bolditalic', + pattern: /.*\*{3}([^\s*][^*]*[^\s*])\*{3}$|.*\*{3}([^\s*])\*{3}$/, + action: ({ + inlineEditor, + prefixText, + inlineRange, + pattern, + undoManager, + }) => { + const match = prefixText.match(pattern); + if (!match) return; - const targetText = match[1] ?? match[2]; - const annotatedText = match[0].slice(-targetText.length - 3 * 2); - const startIndex = inlineRange.index - annotatedText.length; + const targetText = match[1] ?? match[2]; + const annotatedText = match[0].slice(-targetText.length - 3 * 2); + const startIndex = inlineRange.index - annotatedText.length; - inlineEditor.insertText( - { - index: startIndex + annotatedText.length, + inlineEditor.insertText( + { + index: startIndex + annotatedText.length, + length: 0, + }, + ' ' + ); + inlineEditor.setInlineRange({ + index: startIndex + annotatedText.length + 1, length: 0, - }, - ' ' - ); - inlineEditor.setInlineRange({ - index: startIndex + annotatedText.length + 1, - length: 0, - }); + }); - undoManager.stopCapturing(); + undoManager.stopCapturing(); - inlineEditor.formatText( - { + inlineEditor.formatText( + { + index: startIndex, + length: annotatedText.length, + }, + { + bold: true, + italic: true, + } + ); + + inlineEditor.deleteText({ + index: startIndex + annotatedText.length, + length: 1, + }); + inlineEditor.deleteText({ + index: startIndex + annotatedText.length - 3, + length: 3, + }); + inlineEditor.deleteText({ index: startIndex, - length: annotatedText.length, - }, - { - bold: true, - italic: true, - } - ); + length: 3, + }); - inlineEditor.deleteText({ - index: startIndex + annotatedText.length, - length: 1, - }); - inlineEditor.deleteText({ - index: startIndex + annotatedText.length - 3, - length: 3, - }); - inlineEditor.deleteText({ - index: startIndex, - length: 3, - }); + inlineEditor.setInlineRange({ + index: startIndex + annotatedText.length - 6, + length: 0, + }); + }, + } +); - inlineEditor.setInlineRange({ - index: startIndex + annotatedText.length - 6, - length: 0, - }); - }, -}); - -export const BoldMarkdown = InlineMarkdownExtension({ +export const BoldMarkdown = InlineMarkdownExtension({ name: 'bold', pattern: /.*\*{2}([^\s][^*]*[^\s*])\*{2}$|.*\*{2}([^\s*])\*{2}$/, action: ({ inlineEditor, prefixText, inlineRange, pattern, undoManager }) => { @@ -121,7 +129,7 @@ export const BoldMarkdown = InlineMarkdownExtension({ }, }); -export const ItalicExtension = InlineMarkdownExtension({ +export const ItalicExtension = InlineMarkdownExtension({ name: 'italic', pattern: /.*\*{1}([^\s][^*]*[^\s*])\*{1}$|.*\*{1}([^\s*])\*{1}$/, action: ({ inlineEditor, prefixText, inlineRange, pattern, undoManager }) => { @@ -176,117 +184,131 @@ export const ItalicExtension = InlineMarkdownExtension({ }, }); -export const StrikethroughExtension = InlineMarkdownExtension({ - name: 'strikethrough', - pattern: /.*~{2}([^\s][^~]*[^\s])~{2}$|.*~{2}([^\s~])~{2}$/, - action: ({ inlineEditor, prefixText, inlineRange, pattern, undoManager }) => { - const match = prefixText.match(pattern); - if (!match) return; +export const StrikethroughExtension = + InlineMarkdownExtension({ + name: 'strikethrough', + pattern: /.*~{2}([^\s][^~]*[^\s])~{2}$|.*~{2}([^\s~])~{2}$/, + action: ({ + inlineEditor, + prefixText, + inlineRange, + pattern, + undoManager, + }) => { + const match = prefixText.match(pattern); + if (!match) return; - const targetText = match[1] ?? match[2]; - const annotatedText = match[0].slice(-targetText.length - 2 * 2); - const startIndex = inlineRange.index - annotatedText.length; + const targetText = match[1] ?? match[2]; + const annotatedText = match[0].slice(-targetText.length - 2 * 2); + const startIndex = inlineRange.index - annotatedText.length; - inlineEditor.insertText( - { - index: startIndex + annotatedText.length, + inlineEditor.insertText( + { + index: startIndex + annotatedText.length, + length: 0, + }, + ' ' + ); + inlineEditor.setInlineRange({ + index: startIndex + annotatedText.length + 1, length: 0, - }, - ' ' - ); - inlineEditor.setInlineRange({ - index: startIndex + annotatedText.length + 1, - length: 0, - }); + }); - undoManager.stopCapturing(); + undoManager.stopCapturing(); - inlineEditor.formatText( - { - index: startIndex, - length: annotatedText.length, - }, - { - strike: true, - } - ); + inlineEditor.formatText( + { + index: startIndex, + length: annotatedText.length, + }, + { + strike: true, + } + ); - inlineEditor.deleteText({ - index: startIndex + annotatedText.length, - length: 1, - }); - inlineEditor.deleteText({ - index: startIndex + annotatedText.length - 2, - length: 2, - }); - inlineEditor.deleteText({ - index: startIndex, - length: 2, - }); - - inlineEditor.setInlineRange({ - index: startIndex + annotatedText.length - 4, - length: 0, - }); - }, -}); - -export const UnderthroughExtension = InlineMarkdownExtension({ - name: 'underthrough', - pattern: /.*~{1}([^\s][^~]*[^\s~])~{1}$|.*~{1}([^\s~])~{1}$/, - action: ({ inlineEditor, prefixText, inlineRange, pattern, undoManager }) => { - const match = prefixText.match(pattern); - if (!match) return; - - const targetText = match[1] ?? match[2]; - const annotatedText = match[0].slice(-targetText.length - 1 * 2); - const startIndex = inlineRange.index - annotatedText.length; - - inlineEditor.insertText( - { + inlineEditor.deleteText({ index: startIndex + annotatedText.length, - length: 0, - }, - ' ' - ); - inlineEditor.setInlineRange({ - index: startIndex + annotatedText.length + 1, - length: 0, - }); - - undoManager.stopCapturing(); - - inlineEditor.formatText( - { + length: 1, + }); + inlineEditor.deleteText({ + index: startIndex + annotatedText.length - 2, + length: 2, + }); + inlineEditor.deleteText({ index: startIndex, - length: annotatedText.length, - }, - { - underline: true, - } - ); + length: 2, + }); - inlineEditor.deleteText({ - index: startIndex + annotatedText.length, - length: 1, - }); - inlineEditor.deleteText({ - index: inlineRange.index - 1, - length: 1, - }); - inlineEditor.deleteText({ - index: startIndex, - length: 1, - }); + inlineEditor.setInlineRange({ + index: startIndex + annotatedText.length - 4, + length: 0, + }); + }, + }); - inlineEditor.setInlineRange({ - index: startIndex + annotatedText.length - 2, - length: 0, - }); - }, -}); +export const UnderthroughExtension = + InlineMarkdownExtension({ + name: 'underthrough', + pattern: /.*~{1}([^\s][^~]*[^\s~])~{1}$|.*~{1}([^\s~])~{1}$/, + action: ({ + inlineEditor, + prefixText, + inlineRange, + pattern, + undoManager, + }) => { + const match = prefixText.match(pattern); + if (!match) return; -export const CodeExtension = InlineMarkdownExtension({ + const targetText = match[1] ?? match[2]; + const annotatedText = match[0].slice(-targetText.length - 1 * 2); + const startIndex = inlineRange.index - annotatedText.length; + + inlineEditor.insertText( + { + index: startIndex + annotatedText.length, + length: 0, + }, + ' ' + ); + inlineEditor.setInlineRange({ + index: startIndex + annotatedText.length + 1, + length: 0, + }); + + undoManager.stopCapturing(); + + inlineEditor.formatText( + { + index: startIndex, + length: annotatedText.length, + }, + { + underline: true, + } + ); + + inlineEditor.deleteText({ + index: startIndex + annotatedText.length, + length: 1, + }); + inlineEditor.deleteText({ + index: inlineRange.index - 1, + length: 1, + }); + inlineEditor.deleteText({ + index: startIndex, + length: 1, + }); + + inlineEditor.setInlineRange({ + index: startIndex + annotatedText.length - 2, + length: 0, + }); + }, + }); + +export const CodeExtension = InlineMarkdownExtension({ name: 'code', pattern: /.*`([^\s][^`]*[^\s])`$|.*`([^\s`])`$/, action: ({ inlineEditor, prefixText, inlineRange, pattern, undoManager }) => { @@ -341,7 +363,7 @@ export const CodeExtension = InlineMarkdownExtension({ }, }); -export const LinkExtension = InlineMarkdownExtension({ +export const LinkExtension = InlineMarkdownExtension({ name: 'link', pattern: /.*\[(.+?)\]\((.+?)\)$/, action: ({ inlineEditor, prefixText, inlineRange, pattern, undoManager }) => { @@ -401,7 +423,7 @@ export const LinkExtension = InlineMarkdownExtension({ }, }); -export const LatexExtension = InlineMarkdownExtension({ +export const LatexExtension = InlineMarkdownExtension({ name: 'latex', pattern: diff --git a/blocksuite/affine/rich-text/src/inline/presets/nodes/latex-node/latex-editor-menu.ts b/blocksuite/affine/rich-text/src/inline/presets/nodes/latex-node/latex-editor-menu.ts index 57b2b80367..2b73993e55 100644 --- a/blocksuite/affine/rich-text/src/inline/presets/nodes/latex-node/latex-editor-menu.ts +++ b/blocksuite/affine/rich-text/src/inline/presets/nodes/latex-node/latex-editor-menu.ts @@ -1,7 +1,9 @@ import { ColorScheme } from '@blocksuite/affine-model'; import { ThemeProvider } from '@blocksuite/affine-shared/services'; import { unsafeCSSVar } from '@blocksuite/affine-shared/theme'; +import type { AffineTextAttributes } from '@blocksuite/affine-shared/types'; import { type BlockStdScope, ShadowlessElement } from '@blocksuite/block-std'; +import { InlineManagerExtension } from '@blocksuite/block-std/inline'; import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit'; import { noop } from '@blocksuite/global/utils'; import { DoneIcon } from '@blocksuite/icons/lit'; @@ -11,14 +13,14 @@ import { property } from 'lit/decorators.js'; import { codeToTokensBase, type ThemedToken } from 'shiki'; import * as Y from 'yjs'; -import { InlineManagerExtension } from '../../../../extension/index.js'; import { LatexEditorUnitSpecExtension } from '../../affine-inline-specs.js'; -export const LatexEditorInlineManagerExtension = InlineManagerExtension({ - id: 'latex-inline-editor', - enableMarkdown: false, - specs: [LatexEditorUnitSpecExtension.identifier], -}); +export const LatexEditorInlineManagerExtension = + InlineManagerExtension({ + id: 'latex-inline-editor', + enableMarkdown: false, + specs: [LatexEditorUnitSpecExtension.identifier], + }); export class LatexEditorMenu extends SignalWatcher( WithDisposable(ShadowlessElement) diff --git a/blocksuite/affine/rich-text/src/rich-text.ts b/blocksuite/affine/rich-text/src/rich-text.ts index ac45221b72..8145d0dc43 100644 --- a/blocksuite/affine/rich-text/src/rich-text.ts +++ b/blocksuite/affine/rich-text/src/rich-text.ts @@ -3,6 +3,7 @@ import { ShadowlessElement } from '@blocksuite/block-std'; import { type AttributeRenderer, InlineEditor, + type InlineMarkdownMatch, type InlineRange, type InlineRangeProvider, type VLine, @@ -17,7 +18,6 @@ import { classMap } from 'lit/directives/class-map.js'; import * as Y from 'yjs'; import { z } from 'zod'; -import type { InlineMarkdownMatch } from './extension/type.js'; import { onVBeforeinput, onVCompositionEnd } from './hooks.js'; import type { AffineInlineEditor } from './inline/index.js'; diff --git a/blocksuite/framework/block-std/src/inline/extensions/index.ts b/blocksuite/framework/block-std/src/inline/extensions/index.ts new file mode 100644 index 0000000000..945e318216 --- /dev/null +++ b/blocksuite/framework/block-std/src/inline/extensions/index.ts @@ -0,0 +1,4 @@ +export * from './inline-manager'; +export * from './inline-spec'; +export * from './markdown-matcher'; +export * from './type'; diff --git a/blocksuite/framework/block-std/src/inline/extensions/inline-manager.ts b/blocksuite/framework/block-std/src/inline/extensions/inline-manager.ts new file mode 100644 index 0000000000..08a79aec98 --- /dev/null +++ b/blocksuite/framework/block-std/src/inline/extensions/inline-manager.ts @@ -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 { + 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 = 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> => { + 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; + }; + + get markdownMatches(): InlineMarkdownMatch[] { + if (!this.enableMarkdown) { + return []; + } + const matches = Array.from( + this.std.provider.getAll(MarkdownMatcherIdentifier).values() + ); + return matches as InlineMarkdownMatch[]; + } + + readonly specs: Array>; + + constructor( + readonly std: BlockStdScope, + readonly enableMarkdown: boolean, + ...specs: Array> + ) { + this.specs = specs; + } +} + +export type InlineManagerExtensionConfig< + TextAttributes extends BaseTextAttributes, +> = { + id: string; + enableMarkdown?: boolean; + specs: ServiceIdentifier>[]; +}; + +const InlineManagerIdentifier = createIdentifier( + 'AffineInlineManager' +); + +export function InlineManagerExtension< + TextAttributes extends BaseTextAttributes, +>({ + id, + enableMarkdown = true, + specs, +}: InlineManagerExtensionConfig): ExtensionType & { + identifier: ServiceIdentifier>; +} { + const identifier = InlineManagerIdentifier>(id); + return { + setup: di => { + di.addImpl(identifier, provider => { + return new InlineManager( + provider.get(StdIdentifier), + enableMarkdown, + ...specs.map(spec => provider.get(spec)) + ); + }); + }, + identifier, + }; +} diff --git a/blocksuite/framework/block-std/src/inline/extensions/inline-spec.ts b/blocksuite/framework/block-std/src/inline/extensions/inline-spec.ts new file mode 100644 index 0000000000..35cfc7caa4 --- /dev/null +++ b/blocksuite/framework/block-std/src/inline/extensions/inline-spec.ts @@ -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('AffineInlineSpec'); + +export function InlineSpecExtension( + name: string, + getSpec: (provider: ServiceProvider) => InlineSpecs +): ExtensionType & { + identifier: ServiceIdentifier>; +}; +export function InlineSpecExtension( + spec: InlineSpecs +): ExtensionType & { + identifier: ServiceIdentifier>; +}; +export function InlineSpecExtension( + nameOrSpec: string | InlineSpecs, + getSpec?: (provider: ServiceProvider) => InlineSpecs +): ExtensionType & { + identifier: ServiceIdentifier>; +} { + if (typeof nameOrSpec === 'string') { + const identifier = + InlineSpecIdentifier>(nameOrSpec); + return { + identifier, + setup: di => { + di.addImpl(identifier, provider => getSpec!(provider)); + }, + }; + } + const identifier = InlineSpecIdentifier>( + nameOrSpec.name as string + ); + return { + identifier, + setup: di => { + di.addImpl(identifier, nameOrSpec); + }, + }; +} diff --git a/blocksuite/framework/block-std/src/inline/extensions/markdown-matcher.ts b/blocksuite/framework/block-std/src/inline/extensions/markdown-matcher.ts new file mode 100644 index 0000000000..0319eb6cab --- /dev/null +++ b/blocksuite/framework/block-std/src/inline/extensions/markdown-matcher.ts @@ -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( + 'AffineMarkdownMatcher' +); + +export function InlineMarkdownExtension< + TextAttributes extends BaseTextAttributes, +>( + matcher: InlineMarkdownMatch +): ExtensionType & { + identifier: ServiceIdentifier>; +} { + const identifier = MarkdownMatcherIdentifier< + InlineMarkdownMatch + >(matcher.name); + + return { + setup: di => { + di.addImpl(identifier, () => ({ ...matcher })); + }, + identifier, + }; +} diff --git a/blocksuite/affine/rich-text/src/extension/type.ts b/blocksuite/framework/block-std/src/inline/extensions/type.ts similarity index 79% rename from blocksuite/affine/rich-text/src/extension/type.ts rename to blocksuite/framework/block-std/src/inline/extensions/type.ts index fb044a0075..ee2d364eea 100644 --- a/blocksuite/affine/rich-text/src/extension/type.ts +++ b/blocksuite/framework/block-std/src/inline/extensions/type.ts @@ -8,12 +8,12 @@ import type * as Y from 'yjs'; import type { ZodTypeAny } from 'zod'; export type InlineSpecs< - AffineTextAttributes extends BaseTextAttributes = BaseTextAttributes, + TextAttributes extends BaseTextAttributes = BaseTextAttributes, > = { - name: keyof AffineTextAttributes | string; + name: keyof TextAttributes | string; schema: ZodTypeAny; - match: (delta: DeltaInsert) => boolean; - renderer: AttributeRenderer; + match: (delta: DeltaInsert) => boolean; + renderer: AttributeRenderer; embed?: boolean; }; diff --git a/blocksuite/framework/block-std/src/inline/index.ts b/blocksuite/framework/block-std/src/inline/index.ts index f2a0f8cb5e..f332899ece 100644 --- a/blocksuite/framework/block-std/src/inline/index.ts +++ b/blocksuite/framework/block-std/src/inline/index.ts @@ -1,5 +1,6 @@ export * from './components'; export * from './consts'; +export * from './extensions'; export * from './inline-editor'; export * from './range'; export * from './services';