feat(editor): support markdown adapter preprocessed with latex delimiters (#11431)

To close [BS-2870](https://linear.app/affine-design/issue/BS-2870/支持识别-和-[-包裹内容为公式)

## Add Markdown Preprocessor Extension and Enhanced LaTeX Support

### Markdown Preprocessor Extension
This PR introduces a new preprocessor extension for Markdown adapters that allows preprocessing of content before conversion:

Adds a new PreprocessorManager for handling text transformations
Introduces extensible preprocessor interface that supports different processing levels (block/slice/doc)

Integrates preprocessor extension into the existing Markdown adapter workflow

### LaTeX Support Enhancement
Extends LaTeX support to handle both traditional and alternative syntax:
Adds support for backslash LaTeX syntax:

Block math: ```\[...\] ``` alongside existing ```$$...$$```
Inline math: ```\(...\) ``` alongside existing ```$...$```

Implements LaTeX preprocessor to standardize syntax before conversion

Updates tests to cover both syntax variants
This commit is contained in:
donteatfriedrice
2025-04-07 02:18:04 +00:00
parent e376992ccf
commit 568a390b75
31 changed files with 450 additions and 260 deletions
@@ -3680,88 +3680,95 @@ bbb
expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot);
});
test('inline latex', async () => {
const markdown = 'inline $E=mc^2$ latex\n';
const blockSnapshot: BlockSnapshot = {
type: 'block',
id: 'matchesReplaceMap[0]',
flavour: 'affine:note',
props: {
xywh: '[0,0,800,95]',
background: DefaultTheme.noteBackgrounColor,
index: 'a0',
hidden: false,
displayMode: NoteDisplayMode.DocAndEdgeless,
},
children: [
{
type: 'block',
id: 'matchesReplaceMap[1]',
flavour: 'affine:paragraph',
props: {
type: 'text',
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: 'inline ',
},
{
insert: ' ',
attributes: {
latex: 'E=mc^2',
},
},
{
insert: ' latex',
},
],
},
},
children: [],
describe('inline latex', () => {
test.each([
['dollar sign syntax', 'inline $E=mc^2$ latex\n'],
['backslash syntax', 'inline \\(E=mc^2\\) latex\n'],
])('should convert %s correctly', async (_, markdown) => {
const blockSnapshot: BlockSnapshot = {
type: 'block',
id: 'matchesReplaceMap[0]',
flavour: 'affine:note',
props: {
xywh: '[0,0,800,95]',
background: DefaultTheme.noteBackgrounColor,
index: 'a0',
hidden: false,
displayMode: NoteDisplayMode.DocAndEdgeless,
},
],
};
children: [
{
type: 'block',
id: 'matchesReplaceMap[1]',
flavour: 'affine:paragraph',
props: {
type: 'text',
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: 'inline ',
},
{
insert: ' ',
attributes: {
latex: 'E=mc^2',
},
},
{
insert: ' latex',
},
],
},
},
children: [],
},
],
};
const mdAdapter = new MarkdownAdapter(createJob(), provider);
const rawBlockSnapshot = await mdAdapter.toBlockSnapshot({
file: markdown,
const mdAdapter = new MarkdownAdapter(createJob(), provider);
const rawBlockSnapshot = await mdAdapter.toBlockSnapshot({
file: markdown,
});
expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot);
});
expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot);
});
test('latex block', async () => {
const markdown = '$$\nE=mc^2\n$$\n';
const blockSnapshot: BlockSnapshot = {
type: 'block',
id: 'matchesReplaceMap[0]',
flavour: 'affine:note',
props: {
xywh: '[0,0,800,95]',
background: DefaultTheme.noteBackgrounColor,
index: 'a0',
hidden: false,
displayMode: NoteDisplayMode.DocAndEdgeless,
},
children: [
{
type: 'block',
id: 'matchesReplaceMap[1]',
flavour: 'affine:latex',
props: {
latex: 'E=mc^2',
},
children: [],
describe('latex block', () => {
test.each([
['dollar sign syntax', '$$\nE=mc^2\n$$\n'],
['backslash syntax', '\\[\nE=mc^2\n\\]\n'],
])('should convert %s correctly', async (_, markdown) => {
const blockSnapshot: BlockSnapshot = {
type: 'block',
id: 'matchesReplaceMap[0]',
flavour: 'affine:note',
props: {
xywh: '[0,0,800,95]',
background: DefaultTheme.noteBackgrounColor,
index: 'a0',
hidden: false,
displayMode: NoteDisplayMode.DocAndEdgeless,
},
],
};
children: [
{
type: 'block',
id: 'matchesReplaceMap[1]',
flavour: 'affine:latex',
props: {
latex: 'E=mc^2',
},
children: [],
},
],
};
const mdAdapter = new MarkdownAdapter(createJob(), provider);
const rawBlockSnapshot = await mdAdapter.toBlockSnapshot({
file: markdown,
const mdAdapter = new MarkdownAdapter(createJob(), provider);
const rawBlockSnapshot = await mdAdapter.toBlockSnapshot({
file: markdown,
});
expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot);
});
expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot);
});
test('reference', async () => {
@@ -20,6 +20,7 @@ import type { ExtensionType } from '@blocksuite/store';
import { defaultBlockHtmlAdapterMatchers } from './html/block-matcher';
import { defaultBlockMarkdownAdapterMatchers } from './markdown/block-matcher';
import { defaultMarkdownPreprocessors } from './markdown/preprocessor';
import { defaultBlockNotionHtmlAdapterMatchers } from './notion-html/block-matcher';
import { defaultBlockPlainTextAdapterMatchers } from './plain-text/block-matcher';
@@ -44,6 +45,7 @@ export const MarkdownAdapterExtension: ExtensionType[] = [
...MarkdownInlineToDeltaAdapterExtensions,
...defaultBlockMarkdownAdapterMatchers,
...InlineDeltaToMarkdownAdapterExtensions,
...defaultMarkdownPreprocessors,
];
export const NotionHtmlAdapterExtension: ExtensionType[] = [
@@ -1,5 +1,6 @@
export * from './extension.js';
export * from './html/block-matcher.js';
export * from './markdown/block-matcher.js';
export * from './markdown/preprocessor.js';
export * from './notion-html/block-matcher.js';
export * from './plain-text/block-matcher.js';
@@ -0,0 +1,7 @@
import { CodeMarkdownPreprocessorExtension } from '@blocksuite/affine-block-code';
import { LatexMarkdownPreprocessorExtension } from '@blocksuite/affine-block-latex';
export const defaultMarkdownPreprocessors = [
LatexMarkdownPreprocessorExtension,
CodeMarkdownPreprocessorExtension,
];
@@ -1,13 +1,13 @@
import type { ExtensionType } from '@blocksuite/store';
import { CodeBlockHtmlAdapterExtension } from './html.js';
import { CodeBlockMarkdownAdapterExtension } from './markdown.js';
import { CodeBlockMarkdownAdapterExtensions } from './markdown/index.js';
import { CodeBlockNotionHtmlAdapterExtension } from './notion-html.js';
import { CodeBlockPlainTextAdapterExtension } from './plain-text.js';
export const CodeBlockAdapterExtensions: ExtensionType[] = [
CodeBlockHtmlAdapterExtension,
CodeBlockMarkdownAdapterExtension,
CodeBlockMarkdownAdapterExtensions,
CodeBlockPlainTextAdapterExtension,
CodeBlockNotionHtmlAdapterExtension,
];
].flat();
@@ -1,4 +1,4 @@
export * from './html.js';
export * from './markdown.js';
export * from './markdown/index.js';
export * from './notion-html.js';
export * from './plain-text.js';
@@ -0,0 +1,12 @@
import type { ExtensionType } from '@blocksuite/store';
import { CodeBlockMarkdownAdapterExtension } from './markdown.js';
import { CodeMarkdownPreprocessorExtension } from './preprocessor.js';
export * from './markdown.js';
export * from './preprocessor.js';
export const CodeBlockMarkdownAdapterExtensions: ExtensionType[] = [
CodeMarkdownPreprocessorExtension,
CodeBlockMarkdownAdapterExtension,
];
@@ -0,0 +1,76 @@
import {
type MarkdownAdapterPreprocessor,
MarkdownPreprocessorExtension,
} from '@blocksuite/affine-shared/adapters';
const codePreprocessor: MarkdownAdapterPreprocessor = {
name: 'code',
levels: ['slice'],
preprocess: content => {
let codeFence = '';
const lines = content
.split('\n')
.map(line => {
if (line.trimStart().startsWith('-')) {
return line;
}
let trimmedLine = line.trimStart();
if (!codeFence && trimmedLine.startsWith('```')) {
codeFence = trimmedLine.substring(
0,
trimmedLine.lastIndexOf('```') + 3
);
if (codeFence.split('').every(c => c === '`')) {
return line;
}
codeFence = '';
}
if (!codeFence && trimmedLine.startsWith('~~~')) {
codeFence = trimmedLine.substring(
0,
trimmedLine.lastIndexOf('~~~') + 3
);
if (codeFence.split('').every(c => c === '~')) {
return line;
}
codeFence = '';
}
if (
!!codeFence &&
trimmedLine.startsWith(codeFence) &&
trimmedLine.lastIndexOf(codeFence) === 0
) {
codeFence = '';
}
if (codeFence) {
return line;
}
trimmedLine = trimmedLine.trimEnd();
if (!trimmedLine.startsWith('<') && !trimmedLine.endsWith('>')) {
// check if it is a url link and wrap it with the angle brackets
// sometimes the url includes emphasis `_` that will break URL parsing
//
// eg. /MuawcBMT1Mzvoar09-_66?mode=page&blockIds=rL2_GXbtLU2SsJVfCSmh_
// https://www.markdownguide.org/basic-syntax/#urls-and-email-addresses
try {
const valid =
URL.canParse?.(trimmedLine) ?? Boolean(new URL(trimmedLine));
if (valid) {
return `<${trimmedLine}>`;
}
} catch (err) {
console.log(err);
}
}
return line.replace(/^ /, '&#x20;');
})
.join('\n');
return lines;
},
};
export const CodeMarkdownPreprocessorExtension =
MarkdownPreprocessorExtension(codePreprocessor);
@@ -1,11 +1,11 @@
import type { ExtensionType } from '@blocksuite/store';
import { LatexBlockMarkdownAdapterExtension } from './markdown.js';
import { LatexMarkdownAdapterExtensions } from './markdown/index.js';
import { LatexBlockNotionHtmlAdapterExtension } from './notion-html.js';
import { LatexBlockPlainTextAdapterExtension } from './plain-text.js';
export const LatexBlockAdapterExtensions: ExtensionType[] = [
LatexBlockMarkdownAdapterExtension,
LatexMarkdownAdapterExtensions,
LatexBlockNotionHtmlAdapterExtension,
LatexBlockPlainTextAdapterExtension,
];
].flat();
@@ -1,3 +1,3 @@
export * from './markdown.js';
export * from './markdown/index.js';
export * from './notion-html.js';
export * from './plain-text.js';
@@ -0,0 +1,12 @@
import type { ExtensionType } from '@blocksuite/store';
import { LatexBlockMarkdownAdapterExtension } from './markdown.js';
import { LatexMarkdownPreprocessorExtension } from './preprocessor.js';
export * from './markdown.js';
export * from './preprocessor.js';
export const LatexMarkdownAdapterExtensions: ExtensionType[] = [
LatexMarkdownPreprocessorExtension,
LatexBlockMarkdownAdapterExtension,
];
@@ -0,0 +1,25 @@
import {
type MarkdownAdapterPreprocessor,
MarkdownPreprocessorExtension,
} from '@blocksuite/affine-shared/adapters';
const latexPreprocessor: MarkdownAdapterPreprocessor = {
name: 'latex',
levels: ['block', 'slice', 'doc'],
preprocess: content => {
// Replace block-level LaTeX delimiters \[ \] with $$ $$
const blockProcessedContent = content.replace(
/\\\[(.*?)\\\]/gs,
(_, equation) => `$$${equation}$$`
);
// Replace inline LaTeX delimiters \( \) with $ $
const inlineProcessedContent = blockProcessedContent.replace(
/\\\((.*?)\\\)/gs,
(_, equation) => `$${equation}$`
);
return inlineProcessedContent;
},
};
export const LatexMarkdownPreprocessorExtension =
MarkdownPreprocessorExtension(latexPreprocessor);
@@ -11,7 +11,7 @@ import {
DeltaASTConverter,
type DeltaASTConverterOptions,
type InlineDeltaMatcher,
} from '../types/adapter.js';
} from '../types/delta-converter.js';
import type { HtmlAST, InlineHtmlAST } from '../types/hast.js';
import { AdapterTextUtils } from '../utils/text.js';
@@ -30,11 +30,14 @@ export {
MarkdownAdapter,
MarkdownAdapterFactoryExtension,
MarkdownAdapterFactoryIdentifier,
type MarkdownAdapterPreprocessor,
type MarkdownAST,
MarkdownASTToDeltaExtension,
type MarkdownASTToDeltaMatcher,
MarkdownASTToDeltaMatcherIdentifier,
MarkdownDeltaConverter,
MarkdownPreprocessorExtension,
MarkdownPreprocessorManager,
} from './markdown';
export * from './middlewares';
export * from './mix-text';
@@ -10,7 +10,7 @@ import {
type ASTToDeltaMatcher,
DeltaASTConverter,
type InlineDeltaMatcher,
} from '../types/adapter.js';
} from '../types/delta-converter.js';
import type { MarkdownAST } from './type.js';
export type InlineDeltaToMarkdownAdapterMatcher =
@@ -1,4 +1,5 @@
export * from './block-adapter.js';
export * from './delta-converter.js';
export * from './markdown.js';
export * from './preprocessor.js';
export * from './type.js';
@@ -37,6 +37,7 @@ import {
MarkdownDeltaConverter,
} from './delta-converter';
import { remarkGfm } from './gfm';
import { MarkdownPreprocessorManager } from './preprocessor';
import type { Markdown, MarkdownAST } from './type';
type MarkdownToSliceSnapshotPayload = {
@@ -167,6 +168,7 @@ export class MarkdownAdapter extends BaseAdapter<Markdown> {
};
deltaConverter: MarkdownDeltaConverter;
preprocessorManager: MarkdownPreprocessorManager;
readonly blockMatchers: BlockMarkdownAdapterMatcher[];
@@ -187,6 +189,7 @@ export class MarkdownAdapter extends BaseAdapter<Markdown> {
inlineDeltaToMarkdownAdapterMatchers,
markdownInlineToDeltaMatchers
);
this.preprocessorManager = new MarkdownPreprocessorManager(provider);
}
private _astToMarkdown(ast: Root) {
@@ -273,7 +276,11 @@ export class MarkdownAdapter extends BaseAdapter<Markdown> {
async toBlockSnapshot(
payload: ToBlockSnapshotPayload<Markdown>
): Promise<BlockSnapshot> {
const markdownAst = this._markdownToAst(payload.file);
const markdownFile = this.preprocessorManager.process(
'block',
payload.file
);
const markdownAst = this._markdownToAst(markdownFile);
const blockSnapshotRoot = {
type: 'block',
id: nanoid(),
@@ -297,7 +304,8 @@ export class MarkdownAdapter extends BaseAdapter<Markdown> {
async toDocSnapshot(
payload: ToDocSnapshotPayload<Markdown>
): Promise<DocSnapshot> {
const markdownAst = this._markdownToAst(payload.file);
const markdownFile = this.preprocessorManager.process('doc', payload.file);
const markdownAst = this._markdownToAst(markdownFile);
const blockSnapshotRoot = {
type: 'block',
id: nanoid(),
@@ -356,67 +364,11 @@ export class MarkdownAdapter extends BaseAdapter<Markdown> {
async toSliceSnapshot(
payload: MarkdownToSliceSnapshotPayload
): Promise<SliceSnapshot | null> {
let codeFence = '';
payload.file = payload.file
.split('\n')
.map(line => {
if (line.trimStart().startsWith('-')) {
return line;
}
let trimmedLine = line.trimStart();
if (!codeFence && trimmedLine.startsWith('```')) {
codeFence = trimmedLine.substring(
0,
trimmedLine.lastIndexOf('```') + 3
);
if (codeFence.split('').every(c => c === '`')) {
return line;
}
codeFence = '';
}
if (!codeFence && trimmedLine.startsWith('~~~')) {
codeFence = trimmedLine.substring(
0,
trimmedLine.lastIndexOf('~~~') + 3
);
if (codeFence.split('').every(c => c === '~')) {
return line;
}
codeFence = '';
}
if (
!!codeFence &&
trimmedLine.startsWith(codeFence) &&
trimmedLine.lastIndexOf(codeFence) === 0
) {
codeFence = '';
}
if (codeFence) {
return line;
}
trimmedLine = trimmedLine.trimEnd();
if (!trimmedLine.startsWith('<') && !trimmedLine.endsWith('>')) {
// check if it is a url link and wrap it with the angle brackets
// sometimes the url includes emphasis `_` that will break URL parsing
//
// eg. /MuawcBMT1Mzvoar09-_66?mode=page&blockIds=rL2_GXbtLU2SsJVfCSmh_
// https://www.markdownguide.org/basic-syntax/#urls-and-email-addresses
try {
const valid =
URL.canParse?.(trimmedLine) ?? Boolean(new URL(trimmedLine));
if (valid) {
return `<${trimmedLine}>`;
}
} catch (err) {
console.log(err);
}
}
return line.replace(/^ /, '&#x20;');
})
.join('\n');
const markdownAst = this._markdownToAst(payload.file);
const markdownFile = this.preprocessorManager.process(
'slice',
payload.file
);
const markdownAst = this._markdownToAst(markdownFile);
const blockSnapshotRoot = {
type: 'block',
id: nanoid(),
@@ -0,0 +1,40 @@
import {
createIdentifier,
type ServiceIdentifier,
type ServiceProvider,
} from '@blocksuite/global/di';
import type { ExtensionType } from '@blocksuite/store';
import {
type AdapterPreprocessor,
PreprocessorManager,
} from '../types/preprocessor';
import type { Markdown } from './type';
export type MarkdownAdapterPreprocessor = AdapterPreprocessor<Markdown>;
const MarkdownPreprocessorIdentifier =
createIdentifier<MarkdownAdapterPreprocessor>('MarkdownPreprocessor');
export const MarkdownPreprocessorExtension = (
preprocessor: MarkdownAdapterPreprocessor
): ExtensionType & {
identifier: ServiceIdentifier<MarkdownAdapterPreprocessor>;
} => {
const identifier = MarkdownPreprocessorIdentifier(preprocessor.name);
return {
setup: di => {
di.addImpl(identifier, () => preprocessor);
},
identifier,
};
};
export class MarkdownPreprocessorManager extends PreprocessorManager<
Markdown,
MarkdownAdapterPreprocessor
> {
constructor(provider: ServiceProvider) {
super(provider, MarkdownPreprocessorIdentifier);
}
}
@@ -11,7 +11,7 @@ import {
DeltaASTConverter,
type DeltaASTConverterOptions,
type InlineDeltaMatcher,
} from '../types/adapter.js';
} from '../types/delta-converter.js';
import type { HtmlAST, InlineHtmlAST } from '../types/hast.js';
export type InlineDeltaToNotionHtmlAdapterMatcher =
@@ -5,12 +5,12 @@ import {
import type { DeltaInsert, ExtensionType } from '@blocksuite/store';
import type { AffineTextAttributes } from '../../types/index.js';
import type { TextBuffer } from '../types/adapter.js';
import {
type ASTToDeltaMatcher,
DeltaASTConverter,
type InlineDeltaMatcher,
type TextBuffer,
} from '../types/adapter.js';
} from '../types/delta-converter.js';
export type InlineDeltaToPlainTextAdapterMatcher =
InlineDeltaMatcher<TextBuffer>;
@@ -4,15 +4,13 @@ import {
type ASTWalker,
type ASTWalkerContext,
type BaseAdapter,
type BaseTextAttributes,
type BlockSnapshot,
BlockSnapshotSchema,
type DeltaInsert,
type NodeProps,
type Transformer,
} from '@blocksuite/store';
import type { AffineTextAttributes } from '../../types/index.js';
import type { DeltaASTConverter } from './delta-converter.js';
export const isBlockSnapshotNode = (node: unknown): node is BlockSnapshot =>
BlockSnapshotSchema.safeParse(node).success;
@@ -21,13 +19,6 @@ export type TextBuffer = {
content: string;
};
export type DeltaASTConverterOptions = {
trim?: boolean;
pre?: boolean;
pageMap?: Map<string, string>;
removeLastBr?: boolean;
};
export type AdapterContext<
ONode extends object,
TNode extends object = never,
@@ -124,56 +115,6 @@ export type BlockAdapterMatcher<
};
};
export abstract class DeltaASTConverter<
TextAttributes extends BaseTextAttributes = BaseTextAttributes,
AST = unknown,
> {
/**
* Convert AST format to delta format
*/
abstract astToDelta(
ast: AST,
options?: unknown
): DeltaInsert<TextAttributes>[];
/**
* Convert delta format to AST format
*/
abstract deltaToAST(
deltas: DeltaInsert<TextAttributes>[],
options?: unknown
): AST[];
}
export type InlineDeltaMatcher<TNode extends object = never> = {
name: keyof AffineTextAttributes | string;
match: (delta: DeltaInsert<AffineTextAttributes>) => boolean;
toAST: (
delta: DeltaInsert<AffineTextAttributes>,
context: {
configs: Map<string, string>;
current: TNode;
},
provider?: ServiceProvider
) => TNode;
};
export type ASTToDeltaMatcher<AST> = {
name: string;
match: (ast: AST) => boolean;
toDelta: (
ast: AST,
context: {
configs: Map<string, string>;
options: DeltaASTConverterOptions;
toDelta: (
ast: AST,
options?: DeltaASTConverterOptions
) => DeltaInsert<AffineTextAttributes>[];
}
) => DeltaInsert<AffineTextAttributes>[];
};
export type AdapterFactory = {
// TODO(@chen): Make it return the specific adapter type
get: (job: Transformer) => BaseAdapter;
@@ -0,0 +1,61 @@
import type { ServiceProvider } from '@blocksuite/global/di';
import type { BaseTextAttributes, DeltaInsert } from '@blocksuite/store';
import type { AffineTextAttributes } from '../../types';
export type DeltaASTConverterOptions = {
trim?: boolean;
pre?: boolean;
pageMap?: Map<string, string>;
removeLastBr?: boolean;
};
export abstract class DeltaASTConverter<
TextAttributes extends BaseTextAttributes = BaseTextAttributes,
AST = unknown,
> {
/**
* Convert AST format to delta format
*/
abstract astToDelta(
ast: AST,
options?: unknown
): DeltaInsert<TextAttributes>[];
/**
* Convert delta format to AST format
*/
abstract deltaToAST(
deltas: DeltaInsert<TextAttributes>[],
options?: unknown
): AST[];
}
export type InlineDeltaMatcher<TNode extends object = never> = {
name: keyof AffineTextAttributes | string;
match: (delta: DeltaInsert<AffineTextAttributes>) => boolean;
toAST: (
delta: DeltaInsert<AffineTextAttributes>,
context: {
configs: Map<string, string>;
current: TNode;
},
provider?: ServiceProvider
) => TNode;
};
export type ASTToDeltaMatcher<AST> = {
name: string;
match: (ast: AST) => boolean;
toDelta: (
ast: AST,
context: {
configs: Map<string, string>;
options: DeltaASTConverterOptions;
toDelta: (
ast: AST,
options?: DeltaASTConverterOptions
) => DeltaInsert<AffineTextAttributes>[];
}
) => DeltaInsert<AffineTextAttributes>[];
};
@@ -1,2 +1,4 @@
export * from './adapter.js';
export * from './delta-converter.js';
export * from './hast.js';
export * from './preprocessor.js';
@@ -0,0 +1,88 @@
import type { ServiceIdentifier, ServiceProvider } from '@blocksuite/global/di';
/**
* Level of preprocessing
* - doc: Process at to doc snapshot level
* - slice: Process at to slice snapshot level
* - block: Process at to block snapshot level
*/
export type PreprocessLevel = 'doc' | 'slice' | 'block';
/**
* Interface for adapter preprocessor
* @template T Type of content to process, defaults to string
*/
export type AdapterPreprocessor<T = string> = {
/**
* Unique name of the preprocessor
*/
name: string;
/**
* Levels this preprocessor supports
*/
levels: PreprocessLevel[];
/**
* Process the content
* @param content Content to process
* @returns Processed content
*/
preprocess: (content: T) => T;
};
/**
* Manager class for handling preprocessors
* @template T Type of content to process
* @template P Type of preprocessor
*/
export abstract class PreprocessorManager<T, P extends AdapterPreprocessor<T>> {
protected readonly preprocessors: Map<PreprocessLevel, Set<P>>;
constructor(
protected readonly provider: ServiceProvider,
protected readonly identifier: ServiceIdentifier<P>
) {
this.preprocessors = new Map();
// Initialize Sets for each level
this.preprocessors.set('doc', new Set());
this.preprocessors.set('slice', new Set());
this.preprocessors.set('block', new Set());
// Register all preprocessors from provider
this.initializePreprocessors();
}
/**
* Initialize preprocessors from provider
*/
private initializePreprocessors(): void {
const preprocessors = Array.from(
this.provider.getAll(this.identifier).values()
);
for (const preprocessor of preprocessors) {
for (const level of preprocessor.levels) {
const levelSet = this.preprocessors.get(level);
if (levelSet) {
levelSet.add(preprocessor);
}
}
}
}
/**
* Pre process content at specified level
* @param level Level to process at
* @param content Content to process
* @returns Processed content
*/
process(level: PreprocessLevel, content: T): T {
const processors = this.preprocessors.get(level) ?? new Set();
return Array.from(processors).reduce(
(result, preprocessor) => preprocessor.preprocess(result),
content
);
}
}
@@ -1,9 +1,5 @@
import { defaultBlockMarkdownAdapterMatchers } from '@blocksuite/affine/adapters';
import { MarkdownAdapterExtension } from '@blocksuite/affine/adapters';
import { Container } from '@blocksuite/affine/global/di';
import {
InlineDeltaToMarkdownAdapterExtensions,
MarkdownInlineToDeltaAdapterExtensions,
} from '@blocksuite/affine/inlines/preset';
import type {
AttachmentBlockModel,
BookmarkBlockModel,
@@ -123,11 +119,7 @@ function generateMarkdownPreviewBuilder(
};
const container = new Container();
[
...MarkdownInlineToDeltaAdapterExtensions,
...defaultBlockMarkdownAdapterMatchers,
...InlineDeltaToMarkdownAdapterExtensions,
].forEach(ext => {
[...MarkdownAdapterExtension].forEach(ext => {
ext.setup(container);
});
+2 -10
View File
@@ -26,12 +26,8 @@ import { configureBrowserWorkspaceFlavours } from '@affine/core/modules/workspac
import { getWorkerUrl } from '@affine/env/worker';
import { I18n } from '@affine/i18n';
import { StoreManagerClient } from '@affine/nbstore/worker/client';
import { defaultBlockMarkdownAdapterMatchers } from '@blocksuite/affine/adapters';
import { MarkdownAdapterExtension } from '@blocksuite/affine/adapters';
import { Container } from '@blocksuite/affine/global/di';
import {
InlineDeltaToMarkdownAdapterExtensions,
MarkdownInlineToDeltaAdapterExtensions,
} from '@blocksuite/affine/inlines/preset';
import {
docLinkBaseURLMiddleware,
MarkdownAdapter,
@@ -203,11 +199,7 @@ framework.impl(AIButtonProvider, {
const snapshot = transformer.docToSnapshot(blockSuiteDoc);
const container = new Container();
[
...MarkdownInlineToDeltaAdapterExtensions,
...defaultBlockMarkdownAdapterMatchers,
...InlineDeltaToMarkdownAdapterExtensions,
].forEach(ext => {
[...MarkdownAdapterExtension].forEach(ext => {
ext.setup(container);
});
const provider = container.provider();
+2 -10
View File
@@ -38,13 +38,9 @@ import { configureBrowserWorkspaceFlavours } from '@affine/core/modules/workspac
import { getWorkerUrl } from '@affine/env/worker';
import { I18n } from '@affine/i18n';
import { StoreManagerClient } from '@affine/nbstore/worker/client';
import { defaultBlockMarkdownAdapterMatchers } from '@blocksuite/affine/adapters';
import { MarkdownAdapterExtension } from '@blocksuite/affine/adapters';
import { MarkdownTransformer } from '@blocksuite/affine/blocks/root';
import { Container } from '@blocksuite/affine/global/di';
import {
InlineDeltaToMarkdownAdapterExtensions,
MarkdownInlineToDeltaAdapterExtensions,
} from '@blocksuite/affine/inlines/preset';
import {
docLinkBaseURLMiddleware,
MarkdownAdapter,
@@ -266,11 +262,7 @@ const frameworkProvider = framework.provider();
const snapshot = transformer.docToSnapshot(blockSuiteDoc);
const container = new Container();
[
...MarkdownInlineToDeltaAdapterExtensions,
...defaultBlockMarkdownAdapterMatchers,
...InlineDeltaToMarkdownAdapterExtensions,
].forEach(ext => {
[...MarkdownAdapterExtension].forEach(ext => {
ext.setup(container);
});
const provider = container.provider();
@@ -1,5 +1,5 @@
import { createReactComponentFromLit } from '@affine/component';
import { defaultBlockMarkdownAdapterMatchers } from '@blocksuite/affine/adapters';
import { MarkdownAdapterExtension } from '@blocksuite/affine/adapters';
import {
defaultImageProxyMiddleware,
ImageProxyService,
@@ -7,10 +7,6 @@ import {
import { PageEditorBlockSpecs } from '@blocksuite/affine/extensions';
import { Container, type ServiceProvider } from '@blocksuite/affine/global/di';
import { WithDisposable } from '@blocksuite/affine/global/lit';
import {
InlineDeltaToMarkdownAdapterExtensions,
MarkdownInlineToDeltaAdapterExtensions,
} from '@blocksuite/affine/inlines/preset';
import { codeBlockWrapMiddleware } from '@blocksuite/affine/shared/adapters';
import { LinkPreviewerService } from '@blocksuite/affine/shared/services';
import {
@@ -215,11 +211,7 @@ export class TextRenderer extends WithDisposable(ShadowlessElement) {
provider = this.host.std.provider;
} else {
const container = new Container();
[
...MarkdownInlineToDeltaAdapterExtensions,
...defaultBlockMarkdownAdapterMatchers,
...InlineDeltaToMarkdownAdapterExtensions,
].forEach(ext => {
[...MarkdownAdapterExtension].forEach(ext => {
ext.setup(container);
});
@@ -1,20 +1,12 @@
import { defaultBlockMarkdownAdapterMatchers } from '@blocksuite/affine/adapters';
import { MarkdownAdapterExtension } from '@blocksuite/affine/adapters';
import { Container } from '@blocksuite/affine/global/di';
import {
InlineDeltaToMarkdownAdapterExtensions,
MarkdownInlineToDeltaAdapterExtensions,
} from '@blocksuite/affine/inlines/preset';
import { TestWorkspace } from '@blocksuite/affine/store/test';
import { describe, expect, test } from 'vitest';
import { markdownToMindmap } from '../mindmap-preview.js';
const container = new Container();
[
...MarkdownInlineToDeltaAdapterExtensions,
...defaultBlockMarkdownAdapterMatchers,
...InlineDeltaToMarkdownAdapterExtensions,
].forEach(ext => {
[...MarkdownAdapterExtension].forEach(ext => {
ext.setup(container);
});
const provider = container.provider();