diff --git a/blocksuite/affine/components/src/rich-text/markdown/markdown-input.ts b/blocksuite/affine/components/src/rich-text/markdown/markdown-input.ts index e08ec62d60..50a81b51c7 100644 --- a/blocksuite/affine/components/src/rich-text/markdown/markdown-input.ts +++ b/blocksuite/affine/components/src/rich-text/markdown/markdown-input.ts @@ -1,5 +1,9 @@ import { CodeBlockModel, ParagraphBlockModel } from '@blocksuite/affine-model'; -import { isMarkdownPrefix, matchModels } from '@blocksuite/affine-shared/utils'; +import { + isHorizontalRuleMarkdown, + isMarkdownPrefix, + matchModels, +} from '@blocksuite/affine-shared/utils'; import { type BlockStdScope, TextSelection } from '@blocksuite/block-std'; import { getInlineEditorByModel } from '../dom.js'; @@ -47,6 +51,10 @@ export function markdownInput( return toCode(std, model, prefixText, codeMatch[0].slice(3)); } + if (isHorizontalRuleMarkdown(prefixText.trim())) { + return toDivider(std, model, prefixText); + } + switch (prefixText.trim()) { case '[]': case '[ ]': @@ -60,9 +68,6 @@ export function markdownInput( case '-': case '*': return toList(std, model, 'bulleted', prefixText); - case '***': - case '---': - return toDivider(std, model, prefixText); case '#': return toParagraph(std, model, 'h1', prefixText); case '##': diff --git a/blocksuite/affine/shared/src/utils/string.ts b/blocksuite/affine/shared/src/utils/string.ts index c369594acc..3136e467f2 100644 --- a/blocksuite/affine/shared/src/utils/string.ts +++ b/blocksuite/affine/shared/src/utils/string.ts @@ -76,10 +76,32 @@ export function substringMatchScore(name: string, query: string) { /** * Checks if the prefix is a markdown prefix. - * Ex. 1. 2. 3. - * [] [ ] [x] # ## ### #### ##### ###### --- *** > ``` + * Ex. 1. 2. 3. - * [] [ ] [x] # ## ### #### ##### ###### --- *** ___ > ``` */ export function isMarkdownPrefix(prefix: string) { - return !!prefix.match( - /^(\d+\.|-|\*|\[ ?\]|\[x\]|(#){1,6}|(-){3}|(\*){3}|>|```([a-zA-Z0-9]*))$/ + return ( + !!prefix.match( + /^(\d+\.|-|\*|\[ ?\]|\[x\]|(#){1,6}|>|```([a-zA-Z0-9]*))$/ + ) || isHorizontalRuleMarkdown(prefix) ); } + +/** + * Checks if the prefix is a valid markdown horizontal rule - https://www.markdownguide.org/basic-syntax/#horizontal-rules + * @param prefix - The string to check for horizontal rule syntax + * @returns boolean - True if the string represents a valid horizontal rule + * + * Valid horizontal rules: + * - Three or more consecutive hyphens (e.g., "---", "----") + * - Three or more consecutive asterisks (e.g., "***", "****") + * - Three or more consecutive underscores (e.g., "___", "____") + * + * Invalid examples: + * - Mixed characters (e.g., "--*", "-*-") + * - Less than three characters (e.g., "--", "**") + */ +export function isHorizontalRuleMarkdown(prefix: string) { + const horizontalRulePattern = /^(-{3,}|\*{3,}|_{3,})$/; + + return !!prefix.match(horizontalRulePattern); +} diff --git a/blocksuite/tests-legacy/markdown.spec.ts b/blocksuite/tests-legacy/markdown.spec.ts index 19989a5f29..7163b5f17f 100644 --- a/blocksuite/tests-legacy/markdown.spec.ts +++ b/blocksuite/tests-legacy/markdown.spec.ts @@ -175,18 +175,48 @@ test('markdown shortcut', async ({ page }) => { await undoByClick(page); await assertRichTexts(page, ['']); + // testing various horizontal dividers await waitNextFrame(page); await type(page, '--- '); await undoByClick(page); await assertRichTexts(page, ['--- ']); await undoByClick(page); await assertRichTexts(page, ['']); + await waitNextFrame(page); await type(page, '*** '); await undoByClick(page); await assertRichTexts(page, ['*** ']); await undoByClick(page); await assertRichTexts(page, ['']); + + await waitNextFrame(page); + await type(page, '___ '); + await undoByClick(page); + await assertRichTexts(page, ['___ ']); + await undoByClick(page); + await assertRichTexts(page, ['']); + + await waitNextFrame(page); + await type(page, '------ '); + await undoByClick(page); + await assertRichTexts(page, ['------ ']); + await undoByClick(page); + await assertRichTexts(page, ['']); + + await waitNextFrame(page); + await type(page, '****** '); + await undoByClick(page); + await assertRichTexts(page, ['****** ']); + await undoByClick(page); + await assertRichTexts(page, ['']); + + await waitNextFrame(page); + await type(page, '______ '); + await undoByClick(page); + await assertRichTexts(page, ['______ ']); + await undoByClick(page); + await assertRichTexts(page, ['']); }); test.describe('markdown inline-text', () => {