feat(editor): add callout block markdown adapter (#12070)
Closes: [BS-3358](https://linear.app/affine-design/issue/BS-3358/remark-callout-plugin) Closes: [BS-3247](https://linear.app/affine-design/issue/BS-3247/callout-markdown-adapter-适配) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added support for callout blocks in Markdown, enabling recognition and conversion of callout syntax (e.g., `[!emoji]`) to and from block structures. - **Bug Fixes** - Improved handling to distinguish callout blocks from regular blockquotes and paragraphs during Markdown processing. - **Tests** - Introduced comprehensive tests for callout block serialization, deserialization, and plugin behavior to ensure correct Markdown handling. - **Chores** - Added a new dependency for Markdown AST traversal. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
88
blocksuite/affine/blocks/callout/src/adapters/markdown.ts
Normal file
88
blocksuite/affine/blocks/callout/src/adapters/markdown.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { CalloutBlockSchema } from '@blocksuite/affine-model';
|
||||
import {
|
||||
BlockMarkdownAdapterExtension,
|
||||
type BlockMarkdownAdapterMatcher,
|
||||
getCalloutEmoji,
|
||||
isCalloutNode,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
import { nanoid } from '@blocksuite/store';
|
||||
|
||||
// Currently, the callout block children can only be paragraph block or list block
|
||||
// In mdast, the node types are `paragraph`, `list`, `heading`, `blockquote`
|
||||
const CALLOUT_BLOCK_CHILDREN_TYPES = new Set([
|
||||
'paragraph',
|
||||
'list',
|
||||
'heading',
|
||||
'blockquote',
|
||||
]);
|
||||
|
||||
export const calloutBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatcher = {
|
||||
flavour: CalloutBlockSchema.model.flavour,
|
||||
toMatch: o => isCalloutNode(o.node),
|
||||
fromMatch: o => o.node.flavour === CalloutBlockSchema.model.flavour,
|
||||
toBlockSnapshot: {
|
||||
enter: (o, context) => {
|
||||
if (!o.node.data || !isCalloutNode(o.node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Currently, the callout block children can only be a paragraph or a list
|
||||
// So we should filter out the other children
|
||||
o.node.children = o.node.children.filter(child =>
|
||||
CALLOUT_BLOCK_CHILDREN_TYPES.has(child.type)
|
||||
);
|
||||
|
||||
const { walkerContext } = context;
|
||||
const calloutEmoji = getCalloutEmoji(o.node);
|
||||
walkerContext.openNode(
|
||||
{
|
||||
type: 'block',
|
||||
id: nanoid(),
|
||||
flavour: CalloutBlockSchema.model.flavour,
|
||||
props: {
|
||||
emoji: calloutEmoji,
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
'children'
|
||||
);
|
||||
},
|
||||
leave: (o, context) => {
|
||||
const { walkerContext } = context;
|
||||
if (isCalloutNode(o.node)) {
|
||||
walkerContext.closeNode();
|
||||
}
|
||||
},
|
||||
},
|
||||
fromBlockSnapshot: {
|
||||
enter: (o, context) => {
|
||||
const emoji = o.node.props.emoji as string;
|
||||
const { walkerContext } = context;
|
||||
walkerContext
|
||||
.openNode(
|
||||
{
|
||||
type: 'blockquote',
|
||||
children: [],
|
||||
},
|
||||
'children'
|
||||
)
|
||||
.openNode({
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
value: `[!${emoji}]`,
|
||||
},
|
||||
],
|
||||
})
|
||||
.closeNode();
|
||||
},
|
||||
leave: (_, context) => {
|
||||
const { walkerContext } = context;
|
||||
walkerContext.closeNode();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const CalloutBlockMarkdownAdapterExtension =
|
||||
BlockMarkdownAdapterExtension(calloutBlockMarkdownAdapterMatcher);
|
||||
@@ -3,6 +3,7 @@ import { BlockViewExtension, FlavourExtension } from '@blocksuite/std';
|
||||
import type { ExtensionType } from '@blocksuite/store';
|
||||
import { literal } from 'lit/static-html.js';
|
||||
|
||||
import { CalloutBlockMarkdownAdapterExtension } from './adapters/markdown';
|
||||
import { CalloutKeymapExtension } from './callout-keymap';
|
||||
import { calloutSlashMenuConfig } from './configs/slash-menu';
|
||||
|
||||
@@ -11,4 +12,5 @@ export const CalloutBlockSpec: ExtensionType[] = [
|
||||
BlockViewExtension('affine:callout', literal`affine-callout`),
|
||||
CalloutKeymapExtension,
|
||||
SlashMenuConfigExtension('affine:callout', calloutSlashMenuConfig),
|
||||
CalloutBlockMarkdownAdapterExtension,
|
||||
];
|
||||
|
||||
@@ -4,11 +4,14 @@ import {
|
||||
} from '@blocksuite/affine-ext-loader';
|
||||
import { CalloutBlockSchemaExtension } from '@blocksuite/affine-model';
|
||||
|
||||
import { CalloutBlockMarkdownAdapterExtension } from './adapters/markdown';
|
||||
|
||||
export class CalloutStoreExtension extends StoreExtensionProvider {
|
||||
override name = 'affine-callout-block';
|
||||
|
||||
override setup(context: StoreExtensionContext) {
|
||||
super.setup(context);
|
||||
context.register(CalloutBlockSchemaExtension);
|
||||
context.register(CalloutBlockMarkdownAdapterExtension);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user