From 24448659a446689dc09a2a08887a84c463e1b1db Mon Sep 17 00:00:00 2001 From: L-Sun Date: Wed, 11 Jun 2025 14:12:28 +0800 Subject: [PATCH] fix(editor): support markdown transform when using IME (#12778) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix #12284 Close [BS-3517](https://linear.app/affine-design/issue/BS-3517/微软新注音输入法无法使用markdown语法) This PR refactor the markdown transform during inputting, including: - Transfrom markdown syntax input in `inlineEditor.slots.inputting`, where we can detect the space character inputed by IME like Microsoft Bopomofo, but `keydown` event can't. - Remove `markdown-input.ts` which was used in `KeymapExtension` of paragraph, and refactor with `InlineMarkdownExtension` - Adjust existing `InlineMarkdownExtension` since the space is included in text. - Add two `InlineMarkdownExtension` for paragraph and list to impl Heading1-6, number, bullet, to-do list conversion. Other changes: - Improve type hint for parameter of `store.addBlock` ## Summary by CodeRabbit ## Summary by CodeRabbit - **New Features** - Added markdown shortcuts for creating code blocks and dividers in the rich text editor. - Introduced enhanced paragraph markdown support for headings and blockquotes with inline markdown patterns. - Integrated new list markdown extension supporting numbered, bulleted, and todo lists with checked states. - **Improvements** - Updated markdown formatting patterns to require trailing spaces for links, LaTeX, and inline styles, improving detection accuracy. - Markdown transformations now respond to input events instead of keydown for smoother editing experience. - Added focus management after markdown transformations to maintain seamless editing flow. - **Bug Fixes** - Removed unnecessary prevention of default behavior on space and shift-space key presses in list and paragraph editors. - **Refactor** - Enhanced event handling and typing for editor input events, improving reliability and maintainability. - Refined internal prefix text extraction logic for markdown processing. --- blocksuite/affine/blocks/code/src/markdown.ts | 61 +++++++ blocksuite/affine/blocks/code/src/view.ts | 2 + blocksuite/affine/blocks/divider/package.json | 1 + .../affine/blocks/divider/src/markdown.ts | 63 +++++++ blocksuite/affine/blocks/divider/src/view.ts | 2 + .../affine/blocks/divider/tsconfig.json | 1 + .../affine/blocks/list/src/list-keymap.ts | 15 -- blocksuite/affine/blocks/list/src/markdown.ts | 91 ++++++++++ blocksuite/affine/blocks/list/src/view.ts | 2 + .../affine/blocks/paragraph/src/markdown.ts | 74 ++++++++ .../blocks/paragraph/src/paragraph-keymap.ts | 19 -- .../affine/blocks/paragraph/src/view.ts | 10 +- .../affine/inlines/latex/src/markdown.ts | 50 +----- .../affine/inlines/link/src/markdown.ts | 19 +- .../affine/inlines/preset/src/inline-spec.ts | 2 +- .../affine/inlines/preset/src/markdown.ts | 162 ++++-------------- blocksuite/affine/rich-text/src/index.ts | 1 - .../affine/rich-text/src/markdown/divider.ts | 42 ----- .../affine/rich-text/src/markdown/index.ts | 1 - .../affine/rich-text/src/markdown/list.ts | 54 ------ .../rich-text/src/markdown/markdown-input.ts | 98 ----------- .../rich-text/src/markdown/paragraph.ts | 49 ------ .../affine/rich-text/src/markdown/to-code.ts | 42 ----- .../affine/rich-text/src/markdown/utils.ts | 39 ----- blocksuite/affine/rich-text/src/rich-text.ts | 75 +++++--- blocksuite/affine/rich-text/src/utils.ts | 14 ++ .../api/@blocksuite/store/classes/Store.md | 10 +- .../framework/std/src/inline/inline-editor.ts | 3 +- .../std/src/inline/services/event.ts | 22 ++- .../framework/store/src/model/store/store.ts | 4 +- tests/blocksuite/e2e/selection/native.spec.ts | 3 +- tools/utils/src/workspace.gen.ts | 1 + yarn.lock | 1 + 33 files changed, 440 insertions(+), 593 deletions(-) create mode 100644 blocksuite/affine/blocks/code/src/markdown.ts create mode 100644 blocksuite/affine/blocks/divider/src/markdown.ts create mode 100644 blocksuite/affine/blocks/list/src/markdown.ts create mode 100644 blocksuite/affine/blocks/paragraph/src/markdown.ts delete mode 100644 blocksuite/affine/rich-text/src/markdown/divider.ts delete mode 100644 blocksuite/affine/rich-text/src/markdown/index.ts delete mode 100644 blocksuite/affine/rich-text/src/markdown/list.ts delete mode 100644 blocksuite/affine/rich-text/src/markdown/markdown-input.ts delete mode 100644 blocksuite/affine/rich-text/src/markdown/paragraph.ts delete mode 100644 blocksuite/affine/rich-text/src/markdown/to-code.ts delete mode 100644 blocksuite/affine/rich-text/src/markdown/utils.ts diff --git a/blocksuite/affine/blocks/code/src/markdown.ts b/blocksuite/affine/blocks/code/src/markdown.ts new file mode 100644 index 0000000000..502ad037e4 --- /dev/null +++ b/blocksuite/affine/blocks/code/src/markdown.ts @@ -0,0 +1,61 @@ +import { + type CodeBlockModel, + CodeBlockSchema, + ParagraphBlockModel, +} from '@blocksuite/affine-model'; +import { focusTextModel } from '@blocksuite/affine-rich-text'; +import type { AffineTextAttributes } from '@blocksuite/affine-shared/types'; +import { matchModels } from '@blocksuite/affine-shared/utils'; +import type { BlockComponent } from '@blocksuite/std'; +import { InlineMarkdownExtension } from '@blocksuite/std/inline'; + +export const CodeBlockMarkdownExtension = + InlineMarkdownExtension({ + name: 'code-block', + pattern: /^```([a-zA-Z0-9]*)\s$/, + action: ({ inlineEditor, inlineRange, prefixText, pattern }) => { + if (inlineEditor.yTextString.slice(0, inlineRange.index).includes('\n')) { + return; + } + + const match = prefixText.match(pattern); + if (!match) return; + + const language = match[1]; + + if (!inlineEditor.rootElement) return; + const blockComponent = + inlineEditor.rootElement.closest('[data-block-id]'); + if (!blockComponent) return; + + const { model, std, store } = blockComponent; + + if ( + matchModels(model, [ParagraphBlockModel]) && + model.props.type === 'quote' + ) { + return; + } + + const parent = store.getParent(model); + if (!parent) return; + const index = parent.children.indexOf(model); + + store.captureSync(); + const codeId = store.addBlock( + CodeBlockSchema.model.flavour, + { language }, + parent, + index + ); + + if (model.text && model.text.length > prefixText.length) { + const text = model.text.clone(); + store.addBlock('affine:paragraph', { text }, parent, index + 1); + text.delete(0, prefixText.length); + } + store.deleteBlock(model, { bringChildrenTo: parent }); + + focusTextModel(std, codeId); + }, + }); diff --git a/blocksuite/affine/blocks/code/src/view.ts b/blocksuite/affine/blocks/code/src/view.ts index eab094fd3b..e88bcb7ae0 100644 --- a/blocksuite/affine/blocks/code/src/view.ts +++ b/blocksuite/affine/blocks/code/src/view.ts @@ -21,6 +21,7 @@ import { CodeKeymapExtension } from './code-keymap.js'; import { AFFINE_CODE_TOOLBAR_WIDGET } from './code-toolbar/index.js'; import { codeSlashMenuConfig } from './configs/slash-menu.js'; import { effects } from './effects.js'; +import { CodeBlockMarkdownExtension } from './markdown.js'; const codeToolbarWidget = WidgetViewExtension( 'affine:code', @@ -44,6 +45,7 @@ export class CodeBlockViewExtension extends ViewExtensionProvider { BlockViewExtension('affine:code', literal`affine-code`), SlashMenuConfigExtension('affine:code', codeSlashMenuConfig), CodeKeymapExtension, + CodeBlockMarkdownExtension, ...getCodeClipboardExtensions(), ]); context.register([ diff --git a/blocksuite/affine/blocks/divider/package.json b/blocksuite/affine/blocks/divider/package.json index 11b702eb24..9b3a89022d 100644 --- a/blocksuite/affine/blocks/divider/package.json +++ b/blocksuite/affine/blocks/divider/package.json @@ -13,6 +13,7 @@ "@blocksuite/affine-components": "workspace:*", "@blocksuite/affine-ext-loader": "workspace:*", "@blocksuite/affine-model": "workspace:*", + "@blocksuite/affine-rich-text": "workspace:*", "@blocksuite/affine-shared": "workspace:*", "@blocksuite/global": "workspace:*", "@blocksuite/std": "workspace:*", diff --git a/blocksuite/affine/blocks/divider/src/markdown.ts b/blocksuite/affine/blocks/divider/src/markdown.ts new file mode 100644 index 0000000000..8bd38a8ada --- /dev/null +++ b/blocksuite/affine/blocks/divider/src/markdown.ts @@ -0,0 +1,63 @@ +import { + type DividerBlockModel, + DividerBlockSchema, + ParagraphBlockModel, + ParagraphBlockSchema, +} from '@blocksuite/affine-model'; +import { focusTextModel } from '@blocksuite/affine-rich-text'; +import type { AffineTextAttributes } from '@blocksuite/affine-shared/types'; +import { matchModels } from '@blocksuite/affine-shared/utils'; +import type { BlockComponent } from '@blocksuite/std'; +import { InlineMarkdownExtension } from '@blocksuite/std/inline'; + +export const DividerMarkdownExtension = + InlineMarkdownExtension({ + name: 'divider', + pattern: /^(-{3,}|\*{3,}|_{3,})\s$/, + action: ({ inlineEditor, inlineRange }) => { + if (inlineEditor.yTextString.slice(0, inlineRange.index).includes('\n')) { + return; + } + + if (!inlineEditor.rootElement) return; + const blockComponent = + inlineEditor.rootElement.closest('[data-block-id]'); + if (!blockComponent) return; + + const { model, std, store } = blockComponent; + + if ( + matchModels(model, [ParagraphBlockModel]) && + model.props.type !== 'quote' + ) { + const parent = store.getParent(model); + if (!parent) return; + const index = parent.children.indexOf(model); + + store.captureSync(); + inlineEditor.deleteText({ + index: 0, + length: inlineRange.index, + }); + store.addBlock( + DividerBlockSchema.model.flavour, + { + children: model.children, + }, + parent, + index + ); + + const nextBlock = parent.children.at(index + 1); + let id = nextBlock?.id; + if (!id) { + id = store.addBlock( + ParagraphBlockSchema.model.flavour, + {}, + parent + ); + } + focusTextModel(std, id); + } + }, + }); diff --git a/blocksuite/affine/blocks/divider/src/view.ts b/blocksuite/affine/blocks/divider/src/view.ts index 917e8ead59..93372d7b2b 100644 --- a/blocksuite/affine/blocks/divider/src/view.ts +++ b/blocksuite/affine/blocks/divider/src/view.ts @@ -6,6 +6,7 @@ import { BlockViewExtension } from '@blocksuite/std'; import { literal } from 'lit/static-html.js'; import { effects } from './effects'; +import { DividerMarkdownExtension } from './markdown'; export class DividerViewExtension extends ViewExtensionProvider { override name = 'affine-divider-block'; @@ -19,6 +20,7 @@ export class DividerViewExtension extends ViewExtensionProvider { super.setup(context); context.register([ BlockViewExtension('affine:divider', literal`affine-divider`), + DividerMarkdownExtension, ]); } } diff --git a/blocksuite/affine/blocks/divider/tsconfig.json b/blocksuite/affine/blocks/divider/tsconfig.json index d60ba97d5e..45d028d7e6 100644 --- a/blocksuite/affine/blocks/divider/tsconfig.json +++ b/blocksuite/affine/blocks/divider/tsconfig.json @@ -10,6 +10,7 @@ { "path": "../../components" }, { "path": "../../ext-loader" }, { "path": "../../model" }, + { "path": "../../rich-text" }, { "path": "../../shared" }, { "path": "../../../framework/global" }, { "path": "../../../framework/std" }, diff --git a/blocksuite/affine/blocks/list/src/list-keymap.ts b/blocksuite/affine/blocks/list/src/list-keymap.ts index a4fddc2d7a..aff8909427 100644 --- a/blocksuite/affine/blocks/list/src/list-keymap.ts +++ b/blocksuite/affine/blocks/list/src/list-keymap.ts @@ -1,6 +1,5 @@ import { textKeymap } from '@blocksuite/affine-inline-preset'; import { ListBlockSchema } from '@blocksuite/affine-model'; -import { markdownInput } from '@blocksuite/affine-rich-text'; import { getSelectedModelsCommand } from '@blocksuite/affine-shared/commands'; import { IS_MAC } from '@blocksuite/global/env'; import { KeymapExtension, TextSelection } from '@blocksuite/std'; @@ -125,20 +124,6 @@ export const ListKeymapExtension = KeymapExtension( ctx.get('keyboardState').raw.preventDefault(); return true; }, - Space: ctx => { - if (!markdownInput(std)) { - return; - } - ctx.get('keyboardState').raw.preventDefault(); - return true; - }, - 'Shift-Space': ctx => { - if (!markdownInput(std)) { - return; - } - ctx.get('keyboardState').raw.preventDefault(); - return true; - }, }; }, { diff --git a/blocksuite/affine/blocks/list/src/markdown.ts b/blocksuite/affine/blocks/list/src/markdown.ts new file mode 100644 index 0000000000..01c1f7fd91 --- /dev/null +++ b/blocksuite/affine/blocks/list/src/markdown.ts @@ -0,0 +1,91 @@ +import { + type ListBlockModel, + ListBlockSchema, + type ListType, + ParagraphBlockModel, +} from '@blocksuite/affine-model'; +import { focusTextModel } from '@blocksuite/affine-rich-text'; +import type { AffineTextAttributes } from '@blocksuite/affine-shared/types'; +import { matchModels, toNumberedList } from '@blocksuite/affine-shared/utils'; +import type { BlockComponent } from '@blocksuite/std'; +import { InlineMarkdownExtension } from '@blocksuite/std/inline'; + +export const ListMarkdownExtension = + InlineMarkdownExtension({ + name: 'list', + // group 2: number + // group 3: bullet + // group 4: bullet + // group 5: todo + // group 6: todo checked + pattern: /^((\d+\.)|(-)|(\*)|(\[ ?\])|(\[x\]))\s$/, + action: ({ inlineEditor, pattern, inlineRange, prefixText }) => { + if (inlineEditor.yTextString.slice(0, inlineRange.index).includes('\n')) { + return; + } + + const match = prefixText.match(pattern); + if (!match) return; + + let type: ListType; + + if (match[2]) { + type = 'numbered'; + } else if (match[3] || match[4]) { + type = 'bulleted'; + } else if (match[5] || match[6]) { + type = 'todo'; + } else { + return; + } + + const checked = match[6] !== undefined; + + if (!inlineEditor.rootElement) return; + const blockComponent = + inlineEditor.rootElement.closest('[data-block-id]'); + if (!blockComponent) return; + + const { model, std, store } = blockComponent; + if (!matchModels(model, [ParagraphBlockModel])) return; + + if (type !== 'numbered') { + const parent = store.getParent(model); + if (!parent) return; + const index = parent.children.indexOf(model); + + store.captureSync(); + inlineEditor.deleteText({ + index: 0, + length: inlineRange.index, + }); + const id = store.addBlock( + ListBlockSchema.model.flavour, + { + type: type, + text: model.text?.clone(), + children: model.children, + ...(type === 'todo' ? { checked } : {}), + }, + parent, + index + ); + store.deleteBlock(model, { deleteChildren: false }); + focusTextModel(std, id); + } else { + let order = parseInt(match[2]); + if (!Number.isInteger(order)) order = 1; + + store.captureSync(); + inlineEditor.deleteText({ + index: 0, + length: inlineRange.index, + }); + + const id = toNumberedList(std, model, order); + if (!id) return; + + focusTextModel(std, id); + } + }, + }); diff --git a/blocksuite/affine/blocks/list/src/view.ts b/blocksuite/affine/blocks/list/src/view.ts index fac1b2288b..0b30ea1db0 100644 --- a/blocksuite/affine/blocks/list/src/view.ts +++ b/blocksuite/affine/blocks/list/src/view.ts @@ -7,6 +7,7 @@ import { literal } from 'lit/static-html.js'; import { effects } from './effects.js'; import { ListKeymapExtension, ListTextKeymapExtension } from './list-keymap.js'; +import { ListMarkdownExtension } from './markdown.js'; export class ListViewExtension extends ViewExtensionProvider { override name = 'affine-list-block'; @@ -23,6 +24,7 @@ export class ListViewExtension extends ViewExtensionProvider { BlockViewExtension('affine:list', literal`affine-list`), ListKeymapExtension, ListTextKeymapExtension, + ListMarkdownExtension, ]); } } diff --git a/blocksuite/affine/blocks/paragraph/src/markdown.ts b/blocksuite/affine/blocks/paragraph/src/markdown.ts new file mode 100644 index 0000000000..0d9143b654 --- /dev/null +++ b/blocksuite/affine/blocks/paragraph/src/markdown.ts @@ -0,0 +1,74 @@ +import { + ListBlockModel, + ParagraphBlockModel, + ParagraphBlockSchema, + type ParagraphType, +} from '@blocksuite/affine-model'; +import { focusTextModel } from '@blocksuite/affine-rich-text'; +import type { AffineTextAttributes } from '@blocksuite/affine-shared/types'; +import { matchModels } from '@blocksuite/affine-shared/utils'; +import type { BlockComponent } from '@blocksuite/std'; +import { InlineMarkdownExtension } from '@blocksuite/std/inline'; + +export const ParagraphMarkdownExtension = + InlineMarkdownExtension({ + name: 'heading', + pattern: /^((#{1,6})|(>))\s$/, + action: ({ inlineEditor, pattern, inlineRange, prefixText }) => { + if (inlineEditor.yTextString.slice(0, inlineRange.index).includes('\n')) { + return; + } + + const match = prefixText.match(pattern); + if (!match) return; + + const type = ( + match[2] ? `h${match[2].length}` : 'quote' + ) as ParagraphType; + + if (!inlineEditor.rootElement) return; + const blockComponent = + inlineEditor.rootElement.closest('[data-block-id]'); + if (!blockComponent) return; + + const { model, std, store } = blockComponent; + if ( + !matchModels(model, [ParagraphBlockModel]) && + matchModels(model, [ListBlockModel]) + ) { + const parent = store.getParent(model); + if (!parent) return; + const index = parent.children.indexOf(model); + + store.captureSync(); + inlineEditor.deleteText({ + index: 0, + length: inlineRange.index, + }); + store.deleteBlock(model, { deleteChildren: false }); + const id = store.addBlock( + ParagraphBlockSchema.model.flavour, + { + type: type, + text: model.text?.clone(), + children: model.children, + }, + parent, + index + ); + + focusTextModel(std, id); + } else if ( + matchModels(model, [ParagraphBlockModel]) && + model.props.type !== type + ) { + store.captureSync(); + inlineEditor.deleteText({ + index: 0, + length: inlineRange.index, + }); + store.updateBlock(model, { type }); + focusTextModel(std, model.id); + } + }, + }); diff --git a/blocksuite/affine/blocks/paragraph/src/paragraph-keymap.ts b/blocksuite/affine/blocks/paragraph/src/paragraph-keymap.ts index 021696075c..6ad5bbe387 100644 --- a/blocksuite/affine/blocks/paragraph/src/paragraph-keymap.ts +++ b/blocksuite/affine/blocks/paragraph/src/paragraph-keymap.ts @@ -7,7 +7,6 @@ import { import { focusTextModel, getInlineEditorByModel, - markdownInput, } from '@blocksuite/affine-rich-text'; import { calculateCollapsedSiblings, @@ -148,10 +147,6 @@ export const ParagraphKeymapExtension = KeymapExtension( raw.preventDefault(); - if (markdownInput(std, model.id)) { - return true; - } - if (model.props.type.startsWith('h') && model.props.collapsed) { const parent = store.getParent(model); if (!parent) return true; @@ -199,20 +194,6 @@ export const ParagraphKeymapExtension = KeymapExtension( event.preventDefault(); return true; }, - Space: ctx => { - if (!markdownInput(std)) { - return; - } - ctx.get('keyboardState').raw.preventDefault(); - return true; - }, - 'Shift-Space': ctx => { - if (!markdownInput(std)) { - return; - } - ctx.get('keyboardState').raw.preventDefault(); - return true; - }, Tab: ctx => { const [success] = std.command .chain() diff --git a/blocksuite/affine/blocks/paragraph/src/view.ts b/blocksuite/affine/blocks/paragraph/src/view.ts index 103625b7c5..345be8ddbf 100644 --- a/blocksuite/affine/blocks/paragraph/src/view.ts +++ b/blocksuite/affine/blocks/paragraph/src/view.ts @@ -2,9 +2,13 @@ import { type ViewExtensionContext, ViewExtensionProvider, } from '@blocksuite/affine-ext-loader'; +import { ParagraphBlockModel } from '@blocksuite/affine-model'; import { BlockViewExtension, FlavourExtension } from '@blocksuite/std'; import { literal } from 'lit/static-html.js'; +import { z } from 'zod'; +import { effects } from './effects'; +import { ParagraphMarkdownExtension } from './markdown.js'; import { ParagraphBlockConfigExtension } from './paragraph-block-config.js'; import { ParagraphKeymapExtension, @@ -22,11 +26,6 @@ const placeholders = { quote: '', }; -import { ParagraphBlockModel } from '@blocksuite/affine-model'; -import { z } from 'zod'; - -import { effects } from './effects'; - const optionsSchema = z.object({ getPlaceholder: z.optional( z.function().args(z.instanceof(ParagraphBlockModel)).returns(z.string()) @@ -61,6 +60,7 @@ export class ParagraphViewExtension extends ViewExtensionProvider< ParagraphBlockConfigExtension({ getPlaceholder, }), + ParagraphMarkdownExtension, ]); } } diff --git a/blocksuite/affine/inlines/latex/src/markdown.ts b/blocksuite/affine/inlines/latex/src/markdown.ts index 73875781b9..9fcac0c42b 100644 --- a/blocksuite/affine/inlines/latex/src/markdown.ts +++ b/blocksuite/affine/inlines/latex/src/markdown.ts @@ -10,7 +10,7 @@ export const LatexExtension = InlineMarkdownExtension({ name: 'latex', pattern: - /(?:\$\$)(?[^$]+)(?:\$\$)$|(?\$\$\$\$)|(?\$\$)$/g, + /(?:\$\$)(?[^$]+)(?:\$\$)\s$|(?\$\$\$\$)\s$|(?\$\$)\s$/g, action: ({ inlineEditor, prefixText, inlineRange, pattern, undoManager }) => { const match = pattern.exec(prefixText); if (!match || !match.groups) return; @@ -33,22 +33,10 @@ export const LatexExtension = InlineMarkdownExtension({ const ifEdgelessText = blockComponent.closest('affine-edgeless-text'); if (blockPrefix === '$$$$') { - inlineEditor.insertText( - { - index: inlineRange.index, - length: 0, - }, - ' ' - ); - inlineEditor.setInlineRange({ - index: inlineRange.index + 1, - length: 0, - }); - undoManager.stopCapturing(); inlineEditor.deleteText({ - index: inlineRange.index - 4, + index: inlineRange.index - 5, length: 5, }); @@ -88,34 +76,22 @@ export const LatexExtension = InlineMarkdownExtension({ } if (inlinePrefix === '$$') { - inlineEditor.insertText( - { - index: inlineRange.index, - length: 0, - }, - ' ' - ); - inlineEditor.setInlineRange({ - index: inlineRange.index + 1, - length: 0, - }); - undoManager.stopCapturing(); inlineEditor.deleteText({ - index: inlineRange.index - 2, + index: inlineRange.index - 3, length: 3, }); inlineEditor.insertText( { - index: inlineRange.index - 2, + index: inlineRange.index - 3, length: 0, }, ' ' ); inlineEditor.formatText( { - index: inlineRange.index - 2, + index: inlineRange.index - 3, length: 1, }, { @@ -129,7 +105,7 @@ export const LatexExtension = InlineMarkdownExtension({ await inlineEditor.waitForUpdate(); const textPoint = inlineEditor.getTextPoint( - inlineRange.index - 2 + 1 + inlineRange.index - 3 + 1 ); if (!textPoint) return; @@ -159,21 +135,9 @@ export const LatexExtension = InlineMarkdownExtension({ if (!content || content.length === 0) return; - inlineEditor.insertText( - { - index: inlineRange.index, - length: 0, - }, - ' ' - ); - inlineEditor.setInlineRange({ - index: inlineRange.index + 1, - length: 0, - }); - undoManager.stopCapturing(); - const startIndex = inlineRange.index - 2 - content.length - 2; + const startIndex = inlineRange.index - 1 - 2 - content.length - 2; inlineEditor.deleteText({ index: startIndex, length: 2 + content.length + 2 + 1, diff --git a/blocksuite/affine/inlines/link/src/markdown.ts b/blocksuite/affine/inlines/link/src/markdown.ts index bab9b76166..24cdbee03f 100644 --- a/blocksuite/affine/inlines/link/src/markdown.ts +++ b/blocksuite/affine/inlines/link/src/markdown.ts @@ -3,27 +3,18 @@ import { InlineMarkdownExtension } from '@blocksuite/std/inline'; export const LinkExtension = InlineMarkdownExtension({ name: 'link', - pattern: /.*\[(.+?)\]\((.+?)\)$/, + pattern: /.*\[(.+?)\]\((.+?)\)\s$/, action: ({ inlineEditor, prefixText, inlineRange, pattern, undoManager }) => { const match = prefixText.match(pattern); if (!match) return; const linkText = match[1]; const linkUrl = match[2]; - const annotatedText = match[0].slice(-linkText.length - linkUrl.length - 4); - const startIndex = inlineRange.index - annotatedText.length; - - inlineEditor.insertText( - { - index: inlineRange.index, - length: 0, - }, - ' ' + const annotatedText = match[0].slice( + -(linkText.length + linkUrl.length + 4 + 1), + -1 ); - inlineEditor.setInlineRange({ - index: inlineRange.index + 1, - length: 0, - }); + const startIndex = inlineRange.index - annotatedText.length - 1; undoManager.stopCapturing(); diff --git a/blocksuite/affine/inlines/preset/src/inline-spec.ts b/blocksuite/affine/inlines/preset/src/inline-spec.ts index d586d48110..2c6e17822c 100644 --- a/blocksuite/affine/inlines/preset/src/inline-spec.ts +++ b/blocksuite/affine/inlines/preset/src/inline-spec.ts @@ -59,7 +59,7 @@ export const StrikeInlineSpecExtension = export const CodeInlineSpecExtension = InlineSpecExtension({ - name: 'code', + name: 'inline-code', schema: z.literal(true).optional().nullable().catch(undefined), match: delta => { return !!delta.attributes?.code; diff --git a/blocksuite/affine/inlines/preset/src/markdown.ts b/blocksuite/affine/inlines/preset/src/markdown.ts index 56bdebb705..d34c3e3032 100644 --- a/blocksuite/affine/inlines/preset/src/markdown.ts +++ b/blocksuite/affine/inlines/preset/src/markdown.ts @@ -13,7 +13,7 @@ import type { ExtensionType } from '@blocksuite/store'; export const BoldItalicMarkdown = InlineMarkdownExtension( { name: 'bolditalic', - pattern: /.*\*{3}([^\s*][^*]*[^\s*])\*{3}$|.*\*{3}([^\s*])\*{3}$/, + pattern: /.*\*{3}([^\s*][^*]*[^\s*])\*{3}\s$|.*\*{3}([^\s*])\*{3}\s$/, action: ({ inlineEditor, prefixText, @@ -25,20 +25,11 @@ export const BoldItalicMarkdown = InlineMarkdownExtension( if (!match) return; 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, - length: 0, - }, - ' ' + const annotatedText = match[0].slice( + -(targetText.length + 3 * 2 + 1), + -1 ); - inlineEditor.setInlineRange({ - index: startIndex + annotatedText.length + 1, - length: 0, - }); + const startIndex = inlineRange.index - annotatedText.length - 1; undoManager.stopCapturing(); @@ -54,18 +45,13 @@ export const BoldItalicMarkdown = InlineMarkdownExtension( ); inlineEditor.deleteText({ - index: startIndex + annotatedText.length, - length: 1, - }); - inlineEditor.deleteText({ - index: startIndex + annotatedText.length - 3, - length: 3, + index: inlineRange.index - 4, + length: 4, }); inlineEditor.deleteText({ index: startIndex, length: 3, }); - inlineEditor.setInlineRange({ index: startIndex + annotatedText.length - 6, length: 0, @@ -76,26 +62,14 @@ export const BoldItalicMarkdown = InlineMarkdownExtension( export const BoldMarkdown = InlineMarkdownExtension({ name: 'bold', - pattern: /.*\*{2}([^\s][^*]*[^\s*])\*{2}$|.*\*{2}([^\s*])\*{2}$/, + pattern: /.*\*{2}([^\s][^*]*[^\s*])\*{2}\s$|.*\*{2}([^\s*])\*{2}\s$/, 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; - - inlineEditor.insertText( - { - index: startIndex + annotatedText.length, - length: 0, - }, - ' ' - ); - inlineEditor.setInlineRange({ - index: startIndex + annotatedText.length + 1, - length: 0, - }); + const annotatedText = match[0].slice(-(targetText.length + 2 * 2 + 1), -1); + const startIndex = inlineRange.index - annotatedText.length - 1; undoManager.stopCapturing(); @@ -110,18 +84,13 @@ export const BoldMarkdown = InlineMarkdownExtension({ ); inlineEditor.deleteText({ - index: startIndex + annotatedText.length, - length: 1, - }); - inlineEditor.deleteText({ - index: startIndex + annotatedText.length - 2, - length: 2, + index: inlineRange.index - 3, + length: 3, }); inlineEditor.deleteText({ index: startIndex, length: 2, }); - inlineEditor.setInlineRange({ index: startIndex + annotatedText.length - 4, length: 0, @@ -131,26 +100,14 @@ export const BoldMarkdown = InlineMarkdownExtension({ export const ItalicExtension = InlineMarkdownExtension({ name: 'italic', - pattern: /.*\*{1}([^\s][^*]*[^\s*])\*{1}$|.*\*{1}([^\s*])\*{1}$/, + pattern: /.*\*{1}([^\s][^*]*[^\s*])\*{1}\s$|.*\*{1}([^\s*])\*{1}\s$/, 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( - { - index: startIndex + annotatedText.length, - length: 0, - }, - ' ' - ); - inlineEditor.setInlineRange({ - index: startIndex + annotatedText.length + 1, - length: 0, - }); + const annotatedText = match[0].slice(-(targetText.length + 1 * 2 + 1), -1); + const startIndex = inlineRange.index - annotatedText.length - 1; undoManager.stopCapturing(); @@ -165,18 +122,13 @@ export const ItalicExtension = InlineMarkdownExtension({ ); inlineEditor.deleteText({ - index: startIndex + annotatedText.length, - length: 1, - }); - inlineEditor.deleteText({ - index: startIndex + annotatedText.length - 1, - length: 1, + index: inlineRange.index - 2, + length: 2, }); inlineEditor.deleteText({ index: startIndex, length: 1, }); - inlineEditor.setInlineRange({ index: startIndex + annotatedText.length - 2, length: 0, @@ -187,7 +139,7 @@ export const ItalicExtension = InlineMarkdownExtension({ export const StrikethroughExtension = InlineMarkdownExtension({ name: 'strikethrough', - pattern: /.*~{2}([^\s][^~]*[^\s])~{2}$|.*~{2}([^\s~])~{2}$/, + pattern: /.*~{2}([^\s][^~]*[^\s])~{2}\s$|.*~{2}([^\s~])~{2}\s$/, action: ({ inlineEditor, prefixText, @@ -199,20 +151,11 @@ export const StrikethroughExtension = if (!match) return; 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, - length: 0, - }, - ' ' + const annotatedText = match[0].slice( + -targetText.length - (2 * 2 + 1), + -1 ); - inlineEditor.setInlineRange({ - index: startIndex + annotatedText.length + 1, - length: 0, - }); + const startIndex = inlineRange.index - annotatedText.length - 1; undoManager.stopCapturing(); @@ -227,12 +170,8 @@ export const StrikethroughExtension = ); inlineEditor.deleteText({ - index: startIndex + annotatedText.length, - length: 1, - }); - inlineEditor.deleteText({ - index: startIndex + annotatedText.length - 2, - length: 2, + index: inlineRange.index - 3, + length: 3, }); inlineEditor.deleteText({ index: startIndex, @@ -249,7 +188,7 @@ export const StrikethroughExtension = export const UnderthroughExtension = InlineMarkdownExtension({ name: 'underthrough', - pattern: /.*~{1}([^\s][^~]*[^\s~])~{1}$|.*~{1}([^\s~])~{1}$/, + pattern: /.*~{1}([^\s][^~]*[^\s~])~{1}\s$|.*~{1}([^\s~])~{1}\s$/, action: ({ inlineEditor, prefixText, @@ -261,20 +200,11 @@ export const UnderthroughExtension = 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( - { - index: startIndex + annotatedText.length, - length: 0, - }, - ' ' + const annotatedText = match[0].slice( + -(targetText.length + 1 * 2 + 1), + -1 ); - inlineEditor.setInlineRange({ - index: startIndex + annotatedText.length + 1, - length: 0, - }); + const startIndex = inlineRange.index - annotatedText.length - 1; undoManager.stopCapturing(); @@ -289,12 +219,8 @@ export const UnderthroughExtension = ); inlineEditor.deleteText({ - index: startIndex + annotatedText.length, - length: 1, - }); - inlineEditor.deleteText({ - index: inlineRange.index - 1, - length: 1, + index: inlineRange.index - 2, + length: 2, }); inlineEditor.deleteText({ index: startIndex, @@ -310,26 +236,14 @@ export const UnderthroughExtension = export const CodeExtension = InlineMarkdownExtension({ name: 'code', - pattern: /.*`([^\s][^`]*[^\s])`$|.*`([^\s`])`$/, + pattern: /.*`([^\s][^`]*[^\s])`\s$|.*`([^\s`])`\s$/, 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( - { - index: startIndex + annotatedText.length, - length: 0, - }, - ' ' - ); - inlineEditor.setInlineRange({ - index: startIndex + annotatedText.length + 1, - length: 0, - }); + const annotatedText = match[0].slice(-(targetText.length + 1 * 2 + 1), -1); + const startIndex = inlineRange.index - annotatedText.length - 1; undoManager.stopCapturing(); @@ -344,12 +258,8 @@ export const CodeExtension = InlineMarkdownExtension({ ); inlineEditor.deleteText({ - index: startIndex + annotatedText.length, - length: 1, - }); - inlineEditor.deleteText({ - index: startIndex + annotatedText.length - 1, - length: 1, + index: inlineRange.index - 2, + length: 2, }); inlineEditor.deleteText({ index: startIndex, diff --git a/blocksuite/affine/rich-text/src/index.ts b/blocksuite/affine/rich-text/src/index.ts index 7074cd72c5..3de38d7825 100644 --- a/blocksuite/affine/rich-text/src/index.ts +++ b/blocksuite/affine/rich-text/src/index.ts @@ -10,6 +10,5 @@ export { onModelTextUpdated, selectTextModel, } from './dom'; -export { markdownInput } from './markdown'; export { RichText } from './rich-text'; export * from './utils'; diff --git a/blocksuite/affine/rich-text/src/markdown/divider.ts b/blocksuite/affine/rich-text/src/markdown/divider.ts deleted file mode 100644 index 1a831e1cb4..0000000000 --- a/blocksuite/affine/rich-text/src/markdown/divider.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { - DividerBlockModel, - ParagraphBlockModel, -} from '@blocksuite/affine-model'; -import { matchModels } from '@blocksuite/affine-shared/utils'; -import type { BlockStdScope } from '@blocksuite/std'; -import type { BlockModel } from '@blocksuite/store'; - -import { focusTextModel } from '../dom.js'; -import { beforeConvert } from './utils.js'; - -export function toDivider( - std: BlockStdScope, - model: BlockModel, - prefix: string -) { - const { store: doc } = std; - if ( - matchModels(model, [DividerBlockModel]) || - (matchModels(model, [ParagraphBlockModel]) && model.props.type === 'quote') - ) { - return; - } - - const parent = doc.getParent(model); - if (!parent) return; - - const index = parent.children.indexOf(model); - beforeConvert(std, model, prefix.length); - const blockProps = { - children: model.children, - }; - doc.addBlock('affine:divider', blockProps, parent, index); - - const nextBlock = parent.children[index + 1]; - let id = nextBlock?.id; - if (!id) { - id = doc.addBlock('affine:paragraph', {}, parent); - } - focusTextModel(std, id); - return id; -} diff --git a/blocksuite/affine/rich-text/src/markdown/index.ts b/blocksuite/affine/rich-text/src/markdown/index.ts deleted file mode 100644 index c2a452224a..0000000000 --- a/blocksuite/affine/rich-text/src/markdown/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { markdownInput } from './markdown-input.js'; diff --git a/blocksuite/affine/rich-text/src/markdown/list.ts b/blocksuite/affine/rich-text/src/markdown/list.ts deleted file mode 100644 index 57a325c631..0000000000 --- a/blocksuite/affine/rich-text/src/markdown/list.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { - type ListProps, - type ListType, - ParagraphBlockModel, -} from '@blocksuite/affine-model'; -import { matchModels, toNumberedList } from '@blocksuite/affine-shared/utils'; -import type { BlockStdScope } from '@blocksuite/std'; -import type { BlockModel } from '@blocksuite/store'; - -import { focusTextModel } from '../dom.js'; -import { beforeConvert } from './utils.js'; - -export function toList( - std: BlockStdScope, - model: BlockModel, - listType: ListType, - prefix: string, - otherProperties?: Partial -) { - if (!matchModels(model, [ParagraphBlockModel])) { - return; - } - const { store: doc } = std; - const parent = doc.getParent(model); - if (!parent) return; - - beforeConvert(std, model, prefix.length); - - if (listType !== 'numbered') { - const index = parent.children.indexOf(model); - const blockProps = { - type: listType, - text: model.text?.clone(), - children: model.children, - ...otherProperties, - }; - doc.deleteBlock(model, { - deleteChildren: false, - }); - - const id = doc.addBlock('affine:list', blockProps, parent, index); - focusTextModel(std, id); - return id; - } - - let order = parseInt(prefix.slice(0, -1)); - if (!Number.isInteger(order)) order = 1; - - const id = toNumberedList(std, model, order); - if (!id) return; - - focusTextModel(std, id); - return id; -} diff --git a/blocksuite/affine/rich-text/src/markdown/markdown-input.ts b/blocksuite/affine/rich-text/src/markdown/markdown-input.ts deleted file mode 100644 index 80c11cb9c0..0000000000 --- a/blocksuite/affine/rich-text/src/markdown/markdown-input.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { - CalloutBlockModel, - CodeBlockModel, - ParagraphBlockModel, -} from '@blocksuite/affine-model'; -import { - isHorizontalRuleMarkdown, - isMarkdownPrefix, - matchModels, -} from '@blocksuite/affine-shared/utils'; -import { type BlockStdScope, TextSelection } from '@blocksuite/std'; - -import { getInlineEditorByModel } from '../dom.js'; -import { toDivider } from './divider.js'; -import { toList } from './list.js'; -import { toParagraph } from './paragraph.js'; -import { toCode } from './to-code.js'; -import { getPrefixText } from './utils.js'; - -export function markdownInput( - std: BlockStdScope, - id?: string -): string | undefined { - if (!id) { - const selection = std.selection; - const text = selection.find(TextSelection); - id = text?.from.blockId; - } - if (!id) return; - const model = std.store.getBlock(id)?.model; - if (!model) return; - const inline = getInlineEditorByModel(std, model); - if (!inline) return; - const range = inline.getInlineRange(); - if (!range) return; - - const prefixText = getPrefixText(inline); - if (!isMarkdownPrefix(prefixText)) return; - - const isParagraph = matchModels(model, [ParagraphBlockModel]); - const isHeading = isParagraph && model.props.type.startsWith('h'); - const isParagraphQuoteBlock = isParagraph && model.props.type === 'quote'; - const isCodeBlock = matchModels(model, [CodeBlockModel]); - if ( - isHeading || - isParagraphQuoteBlock || - isCodeBlock || - matchModels(model.parent, [CalloutBlockModel]) - ) - return; - - const lineInfo = inline.getLine(range.index); - if (!lineInfo) return; - - const { lineIndex, rangeIndexRelatedToLine } = lineInfo; - if (lineIndex !== 0 || rangeIndexRelatedToLine > prefixText.length) return; - - // try to add code block - const codeMatch = prefixText.match(/^```([a-zA-Z0-9]*)$/g); - if (codeMatch) { - return toCode(std, model, prefixText, codeMatch[0].slice(3)); - } - - if (isHorizontalRuleMarkdown(prefixText.trim())) { - return toDivider(std, model, prefixText); - } - - switch (prefixText.trim()) { - case '[]': - case '[ ]': - return toList(std, model, 'todo', prefixText, { - checked: false, - }); - case '[x]': - return toList(std, model, 'todo', prefixText, { - checked: true, - }); - case '-': - case '*': - return toList(std, model, 'bulleted', prefixText); - case '#': - return toParagraph(std, model, 'h1', prefixText); - case '##': - return toParagraph(std, model, 'h2', prefixText); - case '###': - return toParagraph(std, model, 'h3', prefixText); - case '####': - return toParagraph(std, model, 'h4', prefixText); - case '#####': - return toParagraph(std, model, 'h5', prefixText); - case '######': - return toParagraph(std, model, 'h6', prefixText); - case '>': - return toParagraph(std, model, 'quote', prefixText); - default: - return toList(std, model, 'numbered', prefixText); - } -} diff --git a/blocksuite/affine/rich-text/src/markdown/paragraph.ts b/blocksuite/affine/rich-text/src/markdown/paragraph.ts deleted file mode 100644 index 3ce9101086..0000000000 --- a/blocksuite/affine/rich-text/src/markdown/paragraph.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { - ParagraphBlockModel, - type ParagraphType, -} from '@blocksuite/affine-model'; -import { matchModels } from '@blocksuite/affine-shared/utils'; -import type { BlockStdScope } from '@blocksuite/std'; -import type { BlockModel } from '@blocksuite/store'; - -import { focusTextModel } from '../dom.js'; -import { beforeConvert } from './utils.js'; - -export function toParagraph( - std: BlockStdScope, - model: BlockModel, - type: ParagraphType, - prefix: string -) { - const { store: doc } = std; - if (!matchModels(model, [ParagraphBlockModel])) { - const parent = doc.getParent(model); - if (!parent) return; - - const index = parent.children.indexOf(model); - - beforeConvert(std, model, prefix.length); - - const blockProps = { - type: type, - text: model.text?.clone(), - children: model.children, - }; - doc.deleteBlock(model, { deleteChildren: false }); - const id = doc.addBlock('affine:paragraph', blockProps, parent, index); - - focusTextModel(std, id); - return id; - } - - if (matchModels(model, [ParagraphBlockModel]) && model.props.type !== type) { - beforeConvert(std, model, prefix.length); - - doc.updateBlock(model, { type }); - - focusTextModel(std, model.id); - } - - // If the model is already a paragraph with the same type, do nothing - return model.id; -} diff --git a/blocksuite/affine/rich-text/src/markdown/to-code.ts b/blocksuite/affine/rich-text/src/markdown/to-code.ts deleted file mode 100644 index cff7248428..0000000000 --- a/blocksuite/affine/rich-text/src/markdown/to-code.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { ParagraphBlockModel } from '@blocksuite/affine-model'; -import { matchModels } from '@blocksuite/affine-shared/utils'; -import type { BlockStdScope } from '@blocksuite/std'; -import type { BlockModel } from '@blocksuite/store'; - -import { focusTextModel } from '../dom.js'; - -export function toCode( - std: BlockStdScope, - model: BlockModel, - prefixText: string, - language: string | null -) { - if ( - matchModels(model, [ParagraphBlockModel]) && - model.props.type === 'quote' - ) { - return; - } - - const doc = model.store; - const parent = doc.getParent(model); - if (!parent) { - return; - } - - doc.captureSync(); - const index = parent.children.indexOf(model); - - const codeId = doc.addBlock('affine:code', { language }, parent, index); - - if (model.text && model.text.length > prefixText.length) { - const text = model.text.clone(); - doc.addBlock('affine:paragraph', { text }, parent, index + 1); - text.delete(0, prefixText.length); - } - doc.deleteBlock(model, { bringChildrenTo: parent }); - - focusTextModel(std, codeId); - - return codeId; -} diff --git a/blocksuite/affine/rich-text/src/markdown/utils.ts b/blocksuite/affine/rich-text/src/markdown/utils.ts deleted file mode 100644 index 0f10d9e88a..0000000000 --- a/blocksuite/affine/rich-text/src/markdown/utils.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { BlockStdScope } from '@blocksuite/std'; -import type { InlineEditor } from '@blocksuite/std/inline'; -import type { BlockModel } from '@blocksuite/store'; - -import { focusTextModel } from '../dom.js'; - -export function getPrefixText(inlineEditor: InlineEditor) { - const inlineRange = inlineEditor.getInlineRange(); - if (!inlineRange) return ''; - const firstLineEnd = inlineEditor.yTextString.search(/\n/); - if (firstLineEnd !== -1 && inlineRange.index > firstLineEnd) { - return ''; - } - const textPoint = inlineEditor.getTextPoint(inlineRange.index); - if (!textPoint) return ''; - const [leafStart, offsetStart] = textPoint; - return leafStart.textContent - ? leafStart.textContent.slice(0, offsetStart) - : ''; -} - -export function beforeConvert( - std: BlockStdScope, - model: BlockModel, - index: number -) { - const { text } = model; - if (!text) return; - // Add a space after the text, then stop capturing - // So when the user undo, the prefix will be restored with a `space` - // Ex. (| is the cursor position) - // *| <- user input - // -> bullet list - // *| -> undo - text.insert(' ', index); - focusTextModel(std, model.id, index + 1); - std.store.captureSync(); - text.delete(0, index + 1); -} diff --git a/blocksuite/affine/rich-text/src/rich-text.ts b/blocksuite/affine/rich-text/src/rich-text.ts index 0923e6ba07..0e6e18bdb3 100644 --- a/blocksuite/affine/rich-text/src/rich-text.ts +++ b/blocksuite/affine/rich-text/src/rich-text.ts @@ -22,6 +22,7 @@ import * as Y from 'yjs'; import { z } from 'zod'; import { onVBeforeinput, onVCompositionEnd } from './hooks.js'; +import { getPrefixText } from './utils.js'; interface RichTextStackItem { meta: Map<'richtext-v-range', InlineRange | null>; @@ -186,38 +187,60 @@ export class RichText extends WithDisposable(ShadowlessElement) { const markdownMatches = this.markdownMatches; if (markdownMatches) { - inlineEditor.disposables.addFromEvent( - this.inlineEventSource ?? this.inlineEditorContainer, - 'keydown', - (e: KeyboardEvent) => { - if (e.key !== ' ' && e.key !== 'Enter') return; + const markdownTransform = (isEnter: boolean = false) => { + let inlineRange = inlineEditor.getInlineRange(); + if (!inlineRange) return false; - const inlineRange = inlineEditor.getInlineRange(); - if (!inlineRange || inlineRange.length > 0) return; + let prefixText = getPrefixText(inlineEditor); + if (isEnter) prefixText = `${prefixText} `; - const nearestLineBreakIndex = inlineEditor.yTextString - .slice(0, inlineRange.index) - .lastIndexOf('\n'); - const prefixText = inlineEditor.yTextString.slice( - nearestLineBreakIndex + 1, - inlineRange.index - ); - - for (const match of markdownMatches) { - const { pattern, action } = match; - if (prefixText.match(pattern)) { - action({ - inlineEditor, - prefixText, - inlineRange, - pattern, - undoManager: this.undoManager, + for (const match of markdownMatches) { + const { pattern, action } = match; + if (prefixText.match(pattern)) { + if (isEnter) { + inlineEditor.insertText( + { + index: inlineRange.index, + length: 0, + }, + ' ' + ); + inlineEditor.setInlineRange({ + index: inlineRange.index + 1, + length: 0, }); - e.preventDefault(); - break; + inlineRange = inlineEditor.getInlineRange(); + if (!inlineRange) return false; } + + action({ + inlineEditor, + prefixText, + inlineRange, + pattern, + undoManager: this.undoManager, + }); + return true; } } + return false; + }; + + inlineEditor.disposables.add( + inlineEditor.slots.inputting.subscribe(data => { + if (!inlineEditor.isComposing && data === ' ') { + markdownTransform(); + } + }) + ); + + inlineEditor.disposables.add( + inlineEditor.slots.keydown.subscribe(event => { + if (event.key === 'Enter' && markdownTransform(true)) { + event.stopPropagation(); + event.preventDefault(); + } + }) ); } diff --git a/blocksuite/affine/rich-text/src/utils.ts b/blocksuite/affine/rich-text/src/utils.ts index 8f57377e06..8317ba49a5 100644 --- a/blocksuite/affine/rich-text/src/utils.ts +++ b/blocksuite/affine/rich-text/src/utils.ts @@ -52,3 +52,17 @@ export function clearMarksOnDiscontinuousInput( } }); } + +export function getPrefixText(inlineEditor: InlineEditor) { + const inlineRange = inlineEditor.getInlineRange(); + if (!inlineRange || inlineRange.length > 0) return ''; + + const nearestLineBreakIndex = inlineEditor.yTextString + .slice(0, inlineRange.index) + .lastIndexOf('\n'); + const prefixText = inlineEditor.yTextString.slice( + nearestLineBreakIndex + 1, + inlineRange.index + ); + return prefixText; +} diff --git a/blocksuite/docs/api/@blocksuite/store/classes/Store.md b/blocksuite/docs/api/@blocksuite/store/classes/Store.md index 4f42a177f4..a21174d56f 100644 --- a/blocksuite/docs/api/@blocksuite/store/classes/Store.md +++ b/blocksuite/docs/api/@blocksuite/store/classes/Store.md @@ -118,10 +118,16 @@ Get the root block of the store. ### addBlock() -> **addBlock**(`flavour`, `blockProps`, `parent?`, `parentIndex?`): `string` +> **addBlock**\<`T`\>(`flavour`, `blockProps`, `parent?`, `parentIndex?`): `string` Creates and adds a new block to the store +#### Type Parameters + +##### T + +`T` *extends* `BlockModel`\<`object`\> = `BlockModel`\<`object`\> + #### Parameters ##### flavour @@ -132,7 +138,7 @@ The block's flavour (type) ##### blockProps -`Partial`\<`BlockSysProps` & `Record`\<`string`, `unknown`\> & `Omit`\<`BlockProps`, `"flavour"`\>\> = `{}` +`Partial`\<`BlockProps` \| `PropsOfModel`\<`T`\> & `BlockSysProps`\> = `{}` Optional properties for the new block diff --git a/blocksuite/framework/std/src/inline/inline-editor.ts b/blocksuite/framework/std/src/inline/inline-editor.ts index 652e3ab393..a67b5103b1 100644 --- a/blocksuite/framework/std/src/inline/inline-editor.ts +++ b/blocksuite/framework/std/src/inline/inline-editor.ts @@ -165,8 +165,9 @@ export class InlineEditor< inlineRangeSync: new Subject(), /** * Corresponding to the `compositionUpdate` and `beforeInput` events, and triggered only when the `inlineRange` is not null. + * The parameter is the `event.data`. */ - inputting: new Subject(), + inputting: new Subject(), /** * Triggered only when the `inlineRange` is not null. */ diff --git a/blocksuite/framework/std/src/inline/services/event.ts b/blocksuite/framework/std/src/inline/services/event.ts index 636a1113fd..b48b07134e 100644 --- a/blocksuite/framework/std/src/inline/services/event.ts +++ b/blocksuite/framework/std/src/inline/services/event.ts @@ -119,7 +119,7 @@ export class EventService { this.editor as never ); - this.editor.slots.inputting.next(); + this.editor.slots.inputting.next(event.data ?? ''); }; private readonly _onClick = (event: MouseEvent) => { @@ -181,10 +181,10 @@ export class EventService { }); } - this.editor.slots.inputting.next(); + this.editor.slots.inputting.next(event.data ?? ''); }; - private readonly _onCompositionStart = () => { + private readonly _onCompositionStart = (event: CompositionEvent) => { this._isComposing = true; if (!this.editor.rootElement) return; // embeds is not editable and it will break IME @@ -201,9 +201,11 @@ export class EventService { } else { this._compositionInlineRange = null; } + + this.editor.slots.inputting.next(event.data ?? ''); }; - private readonly _onCompositionUpdate = () => { + private readonly _onCompositionUpdate = (event: CompositionEvent) => { if (!this.editor.rootElement || !this.editor.rootElement.isConnected) { return; } @@ -216,7 +218,7 @@ export class EventService { ) return; - this.editor.slots.inputting.next(); + this.editor.slots.inputting.next(event.data ?? ''); }; private readonly _onKeyDown = (event: KeyboardEvent) => { @@ -359,13 +361,9 @@ export class EventService { 'compositionupdate', this._onCompositionUpdate ); - this.editor.disposables.addFromEvent( - eventSource, - 'compositionend', - (event: CompositionEvent) => { - this._onCompositionEnd(event).catch(console.error); - } - ); + this.editor.disposables.addFromEvent(eventSource, 'compositionend', e => { + this._onCompositionEnd(e).catch(console.error); + }); this.editor.disposables.addFromEvent( eventSource, 'keydown', diff --git a/blocksuite/framework/store/src/model/store/store.ts b/blocksuite/framework/store/src/model/store/store.ts index bcfccd0390..2ed37380d3 100644 --- a/blocksuite/framework/store/src/model/store/store.ts +++ b/blocksuite/framework/store/src/model/store/store.ts @@ -740,9 +740,9 @@ export class Store { * * @category Block CRUD */ - addBlock( + addBlock( flavour: string, - blockProps: Partial> = {}, + blockProps: Partial<(PropsOfModel & BlockSysProps) | BlockProps> = {}, parent?: BlockModel | string | null, parentIndex?: number ): string { diff --git a/tests/blocksuite/e2e/selection/native.spec.ts b/tests/blocksuite/e2e/selection/native.spec.ts index 3d3a94e1f7..7b26cc68e9 100644 --- a/tests/blocksuite/e2e/selection/native.spec.ts +++ b/tests/blocksuite/e2e/selection/native.spec.ts @@ -758,11 +758,12 @@ test('Delete the blank line between two dividers', async ({ page }) => { await initEmptyParagraphState(page); await focusRichText(page); await type(page, '--- '); + await waitNextFrame(page); await assertDivider(page, 1); - await waitNextFrame(page); await pressEnter(page); await type(page, '--- '); + await waitNextFrame(page); await assertDivider(page, 2); await assertRichTexts(page, ['', '']); diff --git a/tools/utils/src/workspace.gen.ts b/tools/utils/src/workspace.gen.ts index 2b6e77de0d..a53d27f902 100644 --- a/tools/utils/src/workspace.gen.ts +++ b/tools/utils/src/workspace.gen.ts @@ -184,6 +184,7 @@ export const PackageList = [ 'blocksuite/affine/components', 'blocksuite/affine/ext-loader', 'blocksuite/affine/model', + 'blocksuite/affine/rich-text', 'blocksuite/affine/shared', 'blocksuite/framework/global', 'blocksuite/framework/std', diff --git a/yarn.lock b/yarn.lock index d36ffb495f..0a422b350e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2593,6 +2593,7 @@ __metadata: "@blocksuite/affine-components": "workspace:*" "@blocksuite/affine-ext-loader": "workspace:*" "@blocksuite/affine-model": "workspace:*" + "@blocksuite/affine-rich-text": "workspace:*" "@blocksuite/affine-shared": "workspace:*" "@blocksuite/global": "workspace:*" "@blocksuite/std": "workspace:*"