feat(editor): support footnote adapter (#9844)

[BS-2373](https://linear.app/affine-design/issue/BS-2373/适配-footnote-adapter)
This commit is contained in:
donteatfriedrice
2025-01-22 06:42:35 +00:00
parent a5025cf470
commit bf797c7a0c
15 changed files with 385 additions and 11 deletions
@@ -3,15 +3,19 @@ import type { ExtensionType } from '@blocksuite/store';
import {
DocNoteBlockHtmlAdapterExtension,
EdgelessNoteBlockHtmlAdapterExtension,
} from './html.js';
} from './html';
import {
DocNoteBlockMarkdownAdapterExtension,
EdgelessNoteBlockMarkdownAdapterExtension,
} from './markdown.js';
} from './markdown';
import {
DocNoteBlockPlainTextAdapterExtension,
EdgelessNoteBlockPlainTextAdapterExtension,
} from './plain-text.js';
} from './plain-text';
export * from './html';
export * from './markdown';
export * from './plain-text';
export const DocNoteBlockAdapterExtensions: ExtensionType[] = [
DocNoteBlockMarkdownAdapterExtension,
@@ -2,7 +2,35 @@ import { NoteBlockSchema, NoteDisplayMode } from '@blocksuite/affine-model';
import {
BlockMarkdownAdapterExtension,
type BlockMarkdownAdapterMatcher,
FOOTNOTE_DEFINITION_PREFIX,
type MarkdownAST,
} from '@blocksuite/affine-shared/adapters';
import type { FootnoteDefinition, Root } from 'mdast';
const isRootNode = (node: MarkdownAST): node is Root => node.type === 'root';
const isFootnoteDefinitionNode = (
node: MarkdownAST
): node is FootnoteDefinition => node.type === 'footnoteDefinition';
const createFootnoteDefinition = (
identifier: string,
content: string
): MarkdownAST => ({
type: 'footnoteDefinition',
label: identifier,
identifier,
children: [
{
type: 'paragraph',
children: [
{
type: 'text',
value: content,
},
],
},
],
});
/**
* Create a markdown adapter matcher for note block.
@@ -15,9 +43,36 @@ const createNoteBlockMarkdownAdapterMatcher = (
displayModeToSkip: NoteDisplayMode
): BlockMarkdownAdapterMatcher => ({
flavour: NoteBlockSchema.model.flavour,
toMatch: () => false,
toMatch: o => isRootNode(o.node),
fromMatch: o => o.node.flavour === NoteBlockSchema.model.flavour,
toBlockSnapshot: {},
toBlockSnapshot: {
enter: (o, context) => {
if (!isRootNode(o.node)) {
return;
}
const noteAst = o.node;
// Find all the footnoteDefinition in the noteAst
const { configs } = context;
noteAst.children.forEach(child => {
if (isFootnoteDefinitionNode(child)) {
const identifier = child.identifier;
const definitionKey = `${FOOTNOTE_DEFINITION_PREFIX}${identifier}`;
// Get the text content of the footnoteDefinition
const textContent = child.children
.find(child => child.type === 'paragraph')
?.children.find(child => child.type === 'text')?.value;
if (textContent) {
configs.set(definitionKey, textContent);
}
}
});
// Remove the footnoteDefinition node from the noteAst
noteAst.children = noteAst.children.filter(
child => !isFootnoteDefinitionNode(child)
);
},
},
fromBlockSnapshot: {
enter: (o, context) => {
const node = o.node;
@@ -25,6 +80,33 @@ const createNoteBlockMarkdownAdapterMatcher = (
context.walkerContext.skipAllChildren();
}
},
leave: (_, context) => {
const { walkerContext, configs } = context;
// Get all the footnote definitions config starts with FOOTNOTE_DEFINITION_PREFIX
// And create footnoteDefinition AST node for each of them
Array.from(configs.keys())
.filter(key => key.startsWith(FOOTNOTE_DEFINITION_PREFIX))
.forEach(key => {
const hasFootnoteDefinition = !!walkerContext.getGlobalContext(key);
// If the footnoteDefinition node is already in md ast, skip it
// In markdown file, we only need to create footnoteDefinition once
if (hasFootnoteDefinition) {
return;
}
const definition = configs.get(key);
const identifier = key.slice(FOOTNOTE_DEFINITION_PREFIX.length);
if (definition && identifier) {
walkerContext
.openNode(
createFootnoteDefinition(identifier, definition),
'children'
)
.closeNode();
// Set the footnoteDefinition node as global context to avoid duplicate creation
walkerContext.setGlobalContext(key, true);
}
});
},
},
});