Files
AFFiNE-Mirror/blocksuite/affine/inlines/latex/src/markdown.ts
T
L-Sun 24448659a4 fix(editor): support markdown transform when using IME (#12778)
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`

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## 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.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-11 14:12:28 +08:00

181 lines
4.8 KiB
TypeScript

import {
DocModeProvider,
TelemetryProvider,
} from '@blocksuite/affine-shared/services';
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
import type { BlockComponent } from '@blocksuite/std';
import { InlineMarkdownExtension } from '@blocksuite/std/inline';
export const LatexExtension = InlineMarkdownExtension<AffineTextAttributes>({
name: 'latex',
pattern:
/(?:\$\$)(?<content>[^$]+)(?:\$\$)\s$|(?<blockPrefix>\$\$\$\$)\s$|(?<inlinePrefix>\$\$)\s$/g,
action: ({ inlineEditor, prefixText, inlineRange, pattern, undoManager }) => {
const match = pattern.exec(prefixText);
if (!match || !match.groups) return;
const content = match.groups['content'];
const inlinePrefix = match.groups['inlinePrefix'];
const blockPrefix = match.groups['blockPrefix'];
if (!inlineEditor.rootElement) return;
const blockComponent =
inlineEditor.rootElement.closest<BlockComponent>('[data-block-id]');
if (!blockComponent) return;
const doc = blockComponent.store;
const std = blockComponent.std;
const parentComponent = blockComponent.parentComponent;
if (!parentComponent) return;
const index = parentComponent.model.children.indexOf(blockComponent.model);
if (index === -1) return;
const mode = std.get(DocModeProvider).getEditorMode() ?? 'page';
const ifEdgelessText = blockComponent.closest('affine-edgeless-text');
if (blockPrefix === '$$$$') {
undoManager.stopCapturing();
inlineEditor.deleteText({
index: inlineRange.index - 5,
length: 5,
});
const id = doc.addBlock(
'affine:latex',
{
latex: '',
},
parentComponent.model,
index + 1
);
blockComponent.host.updateComplete
.then(() => {
const latexBlock = blockComponent.std.view.getBlock(id);
if (!latexBlock || latexBlock.flavour !== 'affine:latex') return;
//FIXME(@Flrande): wait for refactor
// @ts-expect-error BS-2241
latexBlock.toggleEditor();
})
.catch(console.error);
std.getOptional(TelemetryProvider)?.track('Latex', {
from:
mode === 'page'
? 'doc'
: ifEdgelessText
? 'edgeless text'
: 'edgeless note',
page: mode === 'page' ? 'doc' : 'edgeless',
segment: mode === 'page' ? 'doc' : 'whiteboard',
module: 'equation',
control: 'create equation',
});
return;
}
if (inlinePrefix === '$$') {
undoManager.stopCapturing();
inlineEditor.deleteText({
index: inlineRange.index - 3,
length: 3,
});
inlineEditor.insertText(
{
index: inlineRange.index - 3,
length: 0,
},
' '
);
inlineEditor.formatText(
{
index: inlineRange.index - 3,
length: 1,
},
{
latex: '',
}
);
inlineEditor
.waitForUpdate()
.then(async () => {
await inlineEditor.waitForUpdate();
const textPoint = inlineEditor.getTextPoint(
inlineRange.index - 3 + 1
);
if (!textPoint) return;
const [text] = textPoint;
const latexNode = text.parentElement?.closest('affine-latex-node');
if (!latexNode) return;
latexNode.toggleEditor();
})
.catch(console.error);
std.getOptional(TelemetryProvider)?.track('Latex', {
from:
mode === 'page'
? 'doc'
: ifEdgelessText
? 'edgeless text'
: 'edgeless note',
page: mode === 'page' ? 'doc' : 'edgeless',
segment: mode === 'page' ? 'doc' : 'whiteboard',
module: 'inline equation',
control: 'create inline equation',
});
return;
}
if (!content || content.length === 0) return;
undoManager.stopCapturing();
const startIndex = inlineRange.index - 1 - 2 - content.length - 2;
inlineEditor.deleteText({
index: startIndex,
length: 2 + content.length + 2 + 1,
});
inlineEditor.insertText(
{
index: startIndex,
length: 0,
},
' '
);
inlineEditor.formatText(
{
index: startIndex,
length: 1,
},
{
latex: String.raw`${content}`,
}
);
inlineEditor.setInlineRange({
index: startIndex + 1,
length: 0,
});
std.getOptional(TelemetryProvider)?.track('Latex', {
from:
mode === 'page'
? 'doc'
: ifEdgelessText
? 'edgeless text'
: 'edgeless note',
page: mode === 'page' ? 'doc' : 'edgeless',
segment: mode === 'page' ? 'doc' : 'whiteboard',
module: 'inline equation',
control: 'create inline equation',
});
},
});