mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-29 16:19:43 +08:00
chore: merge blocksuite source code (#9213)
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
import type { ExtensionType } from '@blocksuite/block-std';
|
||||
import {
|
||||
createIdentifier,
|
||||
type ServiceIdentifier,
|
||||
} from '@blocksuite/global/di';
|
||||
|
||||
import type { BlockAdapterMatcher } from '../types/adapter.js';
|
||||
import type { HtmlAST } from '../types/hast.js';
|
||||
import type { HtmlDeltaConverter } from './delta-converter.js';
|
||||
|
||||
export type BlockHtmlAdapterMatcher = BlockAdapterMatcher<
|
||||
HtmlAST,
|
||||
HtmlDeltaConverter
|
||||
>;
|
||||
|
||||
export const BlockHtmlAdapterMatcherIdentifier =
|
||||
createIdentifier<BlockHtmlAdapterMatcher>('BlockHtmlAdapterMatcher');
|
||||
|
||||
export function BlockHtmlAdapterExtension(
|
||||
matcher: BlockHtmlAdapterMatcher
|
||||
): ExtensionType & {
|
||||
identifier: ServiceIdentifier<BlockHtmlAdapterMatcher>;
|
||||
} {
|
||||
const identifier = BlockHtmlAdapterMatcherIdentifier(matcher.flavour);
|
||||
return {
|
||||
setup: di => {
|
||||
di.addImpl(identifier, () => matcher);
|
||||
},
|
||||
identifier,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
import type { ExtensionType } from '@blocksuite/block-std';
|
||||
import {
|
||||
createIdentifier,
|
||||
type ServiceIdentifier,
|
||||
} from '@blocksuite/global/di';
|
||||
import type { DeltaInsert } from '@blocksuite/inline';
|
||||
|
||||
import type { AffineTextAttributes } from '../../types/index.js';
|
||||
import {
|
||||
type ASTToDeltaMatcher,
|
||||
DeltaASTConverter,
|
||||
type DeltaASTConverterOptions,
|
||||
type InlineDeltaMatcher,
|
||||
} from '../types/adapter.js';
|
||||
import type { HtmlAST, InlineHtmlAST } from '../types/hast.js';
|
||||
import { TextUtils } from '../utils/text.js';
|
||||
|
||||
export type InlineDeltaToHtmlAdapterMatcher = InlineDeltaMatcher<InlineHtmlAST>;
|
||||
|
||||
export const InlineDeltaToHtmlAdapterMatcherIdentifier =
|
||||
createIdentifier<InlineDeltaToHtmlAdapterMatcher>(
|
||||
'InlineDeltaToHtmlAdapterMatcher'
|
||||
);
|
||||
|
||||
export function InlineDeltaToHtmlAdapterExtension(
|
||||
matcher: InlineDeltaToHtmlAdapterMatcher
|
||||
): ExtensionType & {
|
||||
identifier: ServiceIdentifier<InlineDeltaToHtmlAdapterMatcher>;
|
||||
} {
|
||||
const identifier = InlineDeltaToHtmlAdapterMatcherIdentifier(matcher.name);
|
||||
return {
|
||||
setup: di => {
|
||||
di.addImpl(identifier, () => matcher);
|
||||
},
|
||||
identifier,
|
||||
};
|
||||
}
|
||||
|
||||
export type HtmlASTToDeltaMatcher = ASTToDeltaMatcher<HtmlAST>;
|
||||
|
||||
export const HtmlASTToDeltaMatcherIdentifier =
|
||||
createIdentifier<HtmlASTToDeltaMatcher>('HtmlASTToDeltaMatcher');
|
||||
|
||||
export function HtmlASTToDeltaExtension(
|
||||
matcher: HtmlASTToDeltaMatcher
|
||||
): ExtensionType & {
|
||||
identifier: ServiceIdentifier<HtmlASTToDeltaMatcher>;
|
||||
} {
|
||||
const identifier = HtmlASTToDeltaMatcherIdentifier(matcher.name);
|
||||
return {
|
||||
setup: di => {
|
||||
di.addImpl(identifier, () => matcher);
|
||||
},
|
||||
identifier,
|
||||
};
|
||||
}
|
||||
|
||||
export class HtmlDeltaConverter extends DeltaASTConverter<
|
||||
AffineTextAttributes,
|
||||
HtmlAST
|
||||
> {
|
||||
constructor(
|
||||
readonly configs: Map<string, string>,
|
||||
readonly inlineDeltaMatchers: InlineDeltaToHtmlAdapterMatcher[],
|
||||
readonly htmlASTToDeltaMatchers: HtmlASTToDeltaMatcher[]
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
private _applyTextFormatting(
|
||||
delta: DeltaInsert<AffineTextAttributes>
|
||||
): InlineHtmlAST {
|
||||
let hast: InlineHtmlAST = {
|
||||
type: 'text',
|
||||
value: delta.insert,
|
||||
};
|
||||
|
||||
const context: {
|
||||
configs: Map<string, string>;
|
||||
current: InlineHtmlAST;
|
||||
} = {
|
||||
configs: this.configs,
|
||||
current: hast,
|
||||
};
|
||||
for (const matcher of this.inlineDeltaMatchers) {
|
||||
if (matcher.match(delta)) {
|
||||
hast = matcher.toAST(delta, context);
|
||||
context.current = hast;
|
||||
}
|
||||
}
|
||||
|
||||
return hast;
|
||||
}
|
||||
|
||||
private _spreadAstToDelta(
|
||||
ast: HtmlAST,
|
||||
options: DeltaASTConverterOptions = Object.create(null)
|
||||
): DeltaInsert<AffineTextAttributes>[] {
|
||||
const context = {
|
||||
configs: this.configs,
|
||||
options,
|
||||
toDelta: (ast: HtmlAST, options?: DeltaASTConverterOptions) =>
|
||||
this._spreadAstToDelta(ast, options),
|
||||
};
|
||||
for (const matcher of this.htmlASTToDeltaMatchers) {
|
||||
if (matcher.match(ast)) {
|
||||
return matcher.toDelta(ast, context);
|
||||
}
|
||||
}
|
||||
return 'children' in ast
|
||||
? ast.children.flatMap(child => this._spreadAstToDelta(child, options))
|
||||
: [];
|
||||
}
|
||||
|
||||
astToDelta(
|
||||
ast: HtmlAST,
|
||||
options: DeltaASTConverterOptions = Object.create(null)
|
||||
): DeltaInsert<AffineTextAttributes>[] {
|
||||
return this._spreadAstToDelta(ast, options).reduce((acc, cur) => {
|
||||
return TextUtils.mergeDeltas(acc, cur);
|
||||
}, [] as DeltaInsert<AffineTextAttributes>[]);
|
||||
}
|
||||
|
||||
deltaToAST(
|
||||
deltas: DeltaInsert<AffineTextAttributes>[],
|
||||
depth = 0
|
||||
): InlineHtmlAST[] {
|
||||
if (depth > 0) {
|
||||
deltas.unshift({ insert: ' '.repeat(4).repeat(depth) });
|
||||
}
|
||||
|
||||
return deltas.map(delta => this._applyTextFormatting(delta));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './block-adapter.js';
|
||||
export * from './delta-converter.js';
|
||||
export * from './type.js';
|
||||
@@ -0,0 +1 @@
|
||||
export type Html = string;
|
||||
@@ -0,0 +1,53 @@
|
||||
export {
|
||||
BlockHtmlAdapterExtension,
|
||||
type BlockHtmlAdapterMatcher,
|
||||
BlockHtmlAdapterMatcherIdentifier,
|
||||
type Html,
|
||||
type HtmlASTToDeltaMatcher,
|
||||
HtmlASTToDeltaMatcherIdentifier,
|
||||
HtmlDeltaConverter,
|
||||
type InlineDeltaToHtmlAdapterMatcher,
|
||||
InlineDeltaToHtmlAdapterMatcherIdentifier,
|
||||
} from './html-adapter/index.js';
|
||||
export {
|
||||
BlockMarkdownAdapterExtension,
|
||||
type BlockMarkdownAdapterMatcher,
|
||||
BlockMarkdownAdapterMatcherIdentifier,
|
||||
type InlineDeltaToMarkdownAdapterMatcher,
|
||||
InlineDeltaToMarkdownAdapterMatcherIdentifier,
|
||||
isMarkdownAST,
|
||||
type Markdown,
|
||||
type MarkdownAST,
|
||||
type MarkdownASTToDeltaMatcher,
|
||||
MarkdownASTToDeltaMatcherIdentifier,
|
||||
MarkdownDeltaConverter,
|
||||
} from './markdown/index.js';
|
||||
export {
|
||||
BlockNotionHtmlAdapterExtension,
|
||||
type BlockNotionHtmlAdapterMatcher,
|
||||
BlockNotionHtmlAdapterMatcherIdentifier,
|
||||
type InlineDeltaToNotionHtmlAdapterMatcher,
|
||||
type NotionHtml,
|
||||
type NotionHtmlASTToDeltaMatcher,
|
||||
NotionHtmlASTToDeltaMatcherIdentifier,
|
||||
NotionHtmlDeltaConverter,
|
||||
} from './notion-html/index.js';
|
||||
export {
|
||||
BlockPlainTextAdapterExtension,
|
||||
type BlockPlainTextAdapterMatcher,
|
||||
BlockPlainTextAdapterMatcherIdentifier,
|
||||
type InlineDeltaToPlainTextAdapterMatcher,
|
||||
InlineDeltaToPlainTextAdapterMatcherIdentifier,
|
||||
type PlainText,
|
||||
PlainTextDeltaConverter,
|
||||
} from './plain-text/index.js';
|
||||
export {
|
||||
type AdapterContext,
|
||||
type BlockAdapterMatcher,
|
||||
DeltaASTConverter,
|
||||
type HtmlAST,
|
||||
type InlineHtmlAST,
|
||||
isBlockSnapshotNode,
|
||||
type TextBuffer,
|
||||
} from './types/index.js';
|
||||
export * from './utils/index.js';
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { ExtensionType } from '@blocksuite/block-std';
|
||||
import {
|
||||
createIdentifier,
|
||||
type ServiceIdentifier,
|
||||
} from '@blocksuite/global/di';
|
||||
|
||||
import type { BlockAdapterMatcher } from '../types/adapter.js';
|
||||
import type { MarkdownDeltaConverter } from './delta-converter.js';
|
||||
import type { MarkdownAST } from './type.js';
|
||||
|
||||
export type BlockMarkdownAdapterMatcher = BlockAdapterMatcher<
|
||||
MarkdownAST,
|
||||
MarkdownDeltaConverter
|
||||
>;
|
||||
|
||||
export const BlockMarkdownAdapterMatcherIdentifier =
|
||||
createIdentifier<BlockMarkdownAdapterMatcher>('BlockMarkdownAdapterMatcher');
|
||||
|
||||
export function BlockMarkdownAdapterExtension(
|
||||
matcher: BlockMarkdownAdapterMatcher
|
||||
): ExtensionType & {
|
||||
identifier: ServiceIdentifier<BlockMarkdownAdapterMatcher>;
|
||||
} {
|
||||
const identifier = BlockMarkdownAdapterMatcherIdentifier(matcher.flavour);
|
||||
return {
|
||||
setup: di => {
|
||||
di.addImpl(identifier, () => matcher);
|
||||
},
|
||||
identifier,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import type { ExtensionType } from '@blocksuite/block-std';
|
||||
import {
|
||||
createIdentifier,
|
||||
type ServiceIdentifier,
|
||||
} from '@blocksuite/global/di';
|
||||
import type { DeltaInsert } from '@blocksuite/inline/types';
|
||||
import type { PhrasingContent } from 'mdast';
|
||||
|
||||
import type { AffineTextAttributes } from '../../types/index.js';
|
||||
import {
|
||||
type ASTToDeltaMatcher,
|
||||
DeltaASTConverter,
|
||||
type InlineDeltaMatcher,
|
||||
} from '../types/adapter.js';
|
||||
import type { MarkdownAST } from './type.js';
|
||||
|
||||
export type InlineDeltaToMarkdownAdapterMatcher =
|
||||
InlineDeltaMatcher<PhrasingContent>;
|
||||
|
||||
export const InlineDeltaToMarkdownAdapterMatcherIdentifier =
|
||||
createIdentifier<InlineDeltaToMarkdownAdapterMatcher>(
|
||||
'InlineDeltaToMarkdownAdapterMatcher'
|
||||
);
|
||||
|
||||
export function InlineDeltaToMarkdownAdapterExtension(
|
||||
matcher: InlineDeltaToMarkdownAdapterMatcher
|
||||
): ExtensionType & {
|
||||
identifier: ServiceIdentifier<InlineDeltaToMarkdownAdapterMatcher>;
|
||||
} {
|
||||
const identifier = InlineDeltaToMarkdownAdapterMatcherIdentifier(
|
||||
matcher.name
|
||||
);
|
||||
return {
|
||||
setup: di => {
|
||||
di.addImpl(identifier, () => matcher);
|
||||
},
|
||||
identifier,
|
||||
};
|
||||
}
|
||||
|
||||
export type MarkdownASTToDeltaMatcher = ASTToDeltaMatcher<MarkdownAST>;
|
||||
|
||||
export const MarkdownASTToDeltaMatcherIdentifier =
|
||||
createIdentifier<MarkdownASTToDeltaMatcher>('MarkdownASTToDeltaMatcher');
|
||||
|
||||
export function MarkdownASTToDeltaExtension(
|
||||
matcher: MarkdownASTToDeltaMatcher
|
||||
): ExtensionType & {
|
||||
identifier: ServiceIdentifier<MarkdownASTToDeltaMatcher>;
|
||||
} {
|
||||
const identifier = MarkdownASTToDeltaMatcherIdentifier(matcher.name);
|
||||
return {
|
||||
setup: di => {
|
||||
di.addImpl(identifier, () => matcher);
|
||||
},
|
||||
identifier,
|
||||
};
|
||||
}
|
||||
|
||||
export class MarkdownDeltaConverter extends DeltaASTConverter<
|
||||
AffineTextAttributes,
|
||||
MarkdownAST
|
||||
> {
|
||||
constructor(
|
||||
readonly configs: Map<string, string>,
|
||||
readonly inlineDeltaMatchers: InlineDeltaToMarkdownAdapterMatcher[],
|
||||
readonly markdownASTToDeltaMatchers: MarkdownASTToDeltaMatcher[]
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
applyTextFormatting(
|
||||
delta: DeltaInsert<AffineTextAttributes>
|
||||
): PhrasingContent {
|
||||
let mdast: PhrasingContent = {
|
||||
type: 'text',
|
||||
value: delta.attributes?.underline
|
||||
? `<u>${delta.insert}</u>`
|
||||
: delta.insert,
|
||||
};
|
||||
|
||||
const context: {
|
||||
configs: Map<string, string>;
|
||||
current: PhrasingContent;
|
||||
} = {
|
||||
configs: this.configs,
|
||||
current: mdast,
|
||||
};
|
||||
for (const matcher of this.inlineDeltaMatchers) {
|
||||
if (matcher.match(delta)) {
|
||||
mdast = matcher.toAST(delta, context);
|
||||
context.current = mdast;
|
||||
}
|
||||
}
|
||||
|
||||
return mdast;
|
||||
}
|
||||
|
||||
astToDelta(ast: MarkdownAST): DeltaInsert<AffineTextAttributes>[] {
|
||||
const context = {
|
||||
configs: this.configs,
|
||||
options: Object.create(null),
|
||||
toDelta: (ast: MarkdownAST) => this.astToDelta(ast),
|
||||
};
|
||||
for (const matcher of this.markdownASTToDeltaMatchers) {
|
||||
if (matcher.match(ast)) {
|
||||
return matcher.toDelta(ast, context);
|
||||
}
|
||||
}
|
||||
return 'children' in ast
|
||||
? ast.children.flatMap(child => this.astToDelta(child))
|
||||
: [];
|
||||
}
|
||||
|
||||
deltaToAST(
|
||||
deltas: DeltaInsert<AffineTextAttributes>[],
|
||||
depth = 0
|
||||
): PhrasingContent[] {
|
||||
if (depth > 0) {
|
||||
deltas.unshift({ insert: ' '.repeat(4).repeat(depth) });
|
||||
}
|
||||
|
||||
return deltas.map(delta => this.applyTextFormatting(delta));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './block-adapter.js';
|
||||
export * from './delta-converter.js';
|
||||
export * from './type.js';
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { Root, RootContentMap } from 'mdast';
|
||||
|
||||
export type Markdown = string;
|
||||
|
||||
type MdastUnionType<
|
||||
K extends keyof RootContentMap,
|
||||
V extends RootContentMap[K],
|
||||
> = V;
|
||||
|
||||
export type MarkdownAST =
|
||||
| MdastUnionType<keyof RootContentMap, RootContentMap[keyof RootContentMap]>
|
||||
| Root;
|
||||
|
||||
export const isMarkdownAST = (node: unknown): node is MarkdownAST =>
|
||||
!Array.isArray(node) &&
|
||||
'type' in (node as object) &&
|
||||
(node as MarkdownAST).type !== undefined;
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { ExtensionType } from '@blocksuite/block-std';
|
||||
import {
|
||||
createIdentifier,
|
||||
type ServiceIdentifier,
|
||||
} from '@blocksuite/global/di';
|
||||
|
||||
import type { BlockAdapterMatcher } from '../types/adapter.js';
|
||||
import type { HtmlAST } from '../types/hast.js';
|
||||
import type { NotionHtmlDeltaConverter } from './delta-converter.js';
|
||||
|
||||
export type BlockNotionHtmlAdapterMatcher = BlockAdapterMatcher<
|
||||
HtmlAST,
|
||||
NotionHtmlDeltaConverter
|
||||
>;
|
||||
|
||||
export const BlockNotionHtmlAdapterMatcherIdentifier =
|
||||
createIdentifier<BlockNotionHtmlAdapterMatcher>(
|
||||
'BlockNotionHtmlAdapterMatcher'
|
||||
);
|
||||
|
||||
export function BlockNotionHtmlAdapterExtension(
|
||||
matcher: BlockNotionHtmlAdapterMatcher
|
||||
): ExtensionType & {
|
||||
identifier: ServiceIdentifier<BlockNotionHtmlAdapterMatcher>;
|
||||
} {
|
||||
const identifier = BlockNotionHtmlAdapterMatcherIdentifier(matcher.flavour);
|
||||
return {
|
||||
setup: di => {
|
||||
di.addImpl(identifier, () => matcher);
|
||||
},
|
||||
identifier,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import type { ExtensionType } from '@blocksuite/block-std';
|
||||
import {
|
||||
createIdentifier,
|
||||
type ServiceIdentifier,
|
||||
} from '@blocksuite/global/di';
|
||||
import { isEqual } from '@blocksuite/global/utils';
|
||||
import type { DeltaInsert } from '@blocksuite/inline';
|
||||
|
||||
import type { AffineTextAttributes } from '../../types/index.js';
|
||||
import {
|
||||
type ASTToDeltaMatcher,
|
||||
DeltaASTConverter,
|
||||
type DeltaASTConverterOptions,
|
||||
type InlineDeltaMatcher,
|
||||
} from '../types/adapter.js';
|
||||
import type { HtmlAST, InlineHtmlAST } from '../types/hast.js';
|
||||
|
||||
export type InlineDeltaToNotionHtmlAdapterMatcher =
|
||||
InlineDeltaMatcher<InlineHtmlAST>;
|
||||
|
||||
export type NotionHtmlASTToDeltaMatcher = ASTToDeltaMatcher<HtmlAST>;
|
||||
|
||||
export const NotionHtmlASTToDeltaMatcherIdentifier =
|
||||
createIdentifier<NotionHtmlASTToDeltaMatcher>('NotionHtmlASTToDeltaMatcher');
|
||||
|
||||
export function NotionHtmlASTToDeltaExtension(
|
||||
matcher: NotionHtmlASTToDeltaMatcher
|
||||
): ExtensionType & {
|
||||
identifier: ServiceIdentifier<NotionHtmlASTToDeltaMatcher>;
|
||||
} {
|
||||
const identifier = NotionHtmlASTToDeltaMatcherIdentifier(matcher.name);
|
||||
return {
|
||||
setup: di => {
|
||||
di.addImpl(identifier, () => matcher);
|
||||
},
|
||||
identifier,
|
||||
};
|
||||
}
|
||||
|
||||
export class NotionHtmlDeltaConverter extends DeltaASTConverter<
|
||||
AffineTextAttributes,
|
||||
HtmlAST
|
||||
> {
|
||||
constructor(
|
||||
readonly configs: Map<string, string>,
|
||||
readonly inlineDeltaMatchers: InlineDeltaToNotionHtmlAdapterMatcher[],
|
||||
readonly htmlASTToDeltaMatchers: NotionHtmlASTToDeltaMatcher[]
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
private _spreadAstToDelta(
|
||||
ast: HtmlAST,
|
||||
options: DeltaASTConverterOptions = Object.create(null)
|
||||
): DeltaInsert<AffineTextAttributes>[] {
|
||||
const context = {
|
||||
configs: this.configs,
|
||||
options,
|
||||
toDelta: (ast: HtmlAST, options?: DeltaASTConverterOptions) =>
|
||||
this._spreadAstToDelta(ast, options),
|
||||
};
|
||||
for (const matcher of this.htmlASTToDeltaMatchers) {
|
||||
if (matcher.match(ast)) {
|
||||
return matcher.toDelta(ast, context);
|
||||
}
|
||||
}
|
||||
|
||||
const result =
|
||||
'children' in ast
|
||||
? ast.children.flatMap(child => this._spreadAstToDelta(child, options))
|
||||
: [];
|
||||
|
||||
if (options.removeLastBr && result.length > 0) {
|
||||
const lastItem = result[result.length - 1];
|
||||
if (lastItem.insert === '\n') {
|
||||
result.pop();
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
astToDelta(
|
||||
ast: HtmlAST,
|
||||
options: DeltaASTConverterOptions = Object.create(null)
|
||||
): DeltaInsert<AffineTextAttributes>[] {
|
||||
return this._spreadAstToDelta(ast, options).reduce((acc, cur) => {
|
||||
if (acc.length === 0) {
|
||||
return [cur];
|
||||
}
|
||||
const last = acc[acc.length - 1];
|
||||
if (
|
||||
typeof last.insert === 'string' &&
|
||||
typeof cur.insert === 'string' &&
|
||||
isEqual(last.attributes, cur.attributes)
|
||||
) {
|
||||
last.insert += cur.insert;
|
||||
return acc;
|
||||
}
|
||||
return [...acc, cur];
|
||||
}, [] as DeltaInsert<AffineTextAttributes>[]);
|
||||
}
|
||||
|
||||
deltaToAST(_: DeltaInsert<AffineTextAttributes>[]): InlineHtmlAST[] {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './block-adapter.js';
|
||||
export * from './delta-converter.js';
|
||||
export * from './type.js';
|
||||
@@ -0,0 +1 @@
|
||||
export type NotionHtml = string;
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { ExtensionType } from '@blocksuite/block-std';
|
||||
import {
|
||||
createIdentifier,
|
||||
type ServiceIdentifier,
|
||||
} from '@blocksuite/global/di';
|
||||
|
||||
import type { BlockAdapterMatcher, TextBuffer } from '../types/adapter.js';
|
||||
|
||||
export type BlockPlainTextAdapterMatcher = BlockAdapterMatcher<TextBuffer>;
|
||||
|
||||
export const BlockPlainTextAdapterMatcherIdentifier =
|
||||
createIdentifier<BlockPlainTextAdapterMatcher>(
|
||||
'BlockPlainTextAdapterMatcher'
|
||||
);
|
||||
|
||||
export function BlockPlainTextAdapterExtension(
|
||||
matcher: BlockPlainTextAdapterMatcher
|
||||
): ExtensionType & {
|
||||
identifier: ServiceIdentifier<BlockPlainTextAdapterMatcher>;
|
||||
} {
|
||||
const identifier = BlockPlainTextAdapterMatcherIdentifier(matcher.flavour);
|
||||
return {
|
||||
setup: di => {
|
||||
di.addImpl(identifier, () => matcher);
|
||||
},
|
||||
identifier,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { createIdentifier } from '@blocksuite/global/di';
|
||||
import type { DeltaInsert } from '@blocksuite/inline';
|
||||
|
||||
import type { AffineTextAttributes } from '../../types/index.js';
|
||||
import {
|
||||
type ASTToDeltaMatcher,
|
||||
DeltaASTConverter,
|
||||
type InlineDeltaMatcher,
|
||||
type TextBuffer,
|
||||
} from '../types/adapter.js';
|
||||
|
||||
export type InlineDeltaToPlainTextAdapterMatcher =
|
||||
InlineDeltaMatcher<TextBuffer>;
|
||||
|
||||
export const InlineDeltaToPlainTextAdapterMatcherIdentifier =
|
||||
createIdentifier<InlineDeltaToPlainTextAdapterMatcher>(
|
||||
'InlineDeltaToPlainTextAdapterMatcher'
|
||||
);
|
||||
|
||||
export type PlainTextASTToDeltaMatcher = ASTToDeltaMatcher<string>;
|
||||
|
||||
export class PlainTextDeltaConverter extends DeltaASTConverter<
|
||||
AffineTextAttributes,
|
||||
string
|
||||
> {
|
||||
constructor(
|
||||
readonly configs: Map<string, string>,
|
||||
readonly inlineDeltaMatchers: InlineDeltaToPlainTextAdapterMatcher[],
|
||||
readonly plainTextASTToDeltaMatchers: PlainTextASTToDeltaMatcher[]
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
astToDelta(ast: string) {
|
||||
const context = {
|
||||
configs: this.configs,
|
||||
options: Object.create(null),
|
||||
toDelta: (ast: string) => this.astToDelta(ast),
|
||||
};
|
||||
for (const matcher of this.plainTextASTToDeltaMatchers) {
|
||||
if (matcher.match(ast)) {
|
||||
return matcher.toDelta(ast, context);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
deltaToAST(deltas: DeltaInsert<AffineTextAttributes>[]): string[] {
|
||||
return deltas.map(delta => {
|
||||
const context = {
|
||||
configs: this.configs,
|
||||
current: { content: delta.insert },
|
||||
};
|
||||
for (const matcher of this.inlineDeltaMatchers) {
|
||||
if (matcher.match(delta)) {
|
||||
context.current = matcher.toAST(delta, context);
|
||||
}
|
||||
}
|
||||
return context.current.content;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './block-adapter.js';
|
||||
export * from './delta-converter.js';
|
||||
export * from './type.js';
|
||||
@@ -0,0 +1 @@
|
||||
export type PlainText = string;
|
||||
@@ -0,0 +1,170 @@
|
||||
import type { BaseTextAttributes, DeltaInsert } from '@blocksuite/inline';
|
||||
import {
|
||||
type AssetsManager,
|
||||
type ASTWalker,
|
||||
type ASTWalkerContext,
|
||||
type BlockSnapshot,
|
||||
BlockSnapshotSchema,
|
||||
type Job,
|
||||
type NodeProps,
|
||||
} from '@blocksuite/store';
|
||||
|
||||
import type { AffineTextAttributes } from '../../types/index.js';
|
||||
|
||||
export const isBlockSnapshotNode = (node: unknown): node is BlockSnapshot =>
|
||||
BlockSnapshotSchema.safeParse(node).success;
|
||||
|
||||
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,
|
||||
TConverter extends DeltaASTConverter = DeltaASTConverter,
|
||||
> = {
|
||||
walker: ASTWalker<ONode, TNode>;
|
||||
walkerContext: ASTWalkerContext<TNode>;
|
||||
configs: Map<string, string>;
|
||||
job: Job;
|
||||
deltaConverter: TConverter;
|
||||
textBuffer: TextBuffer;
|
||||
assets?: AssetsManager;
|
||||
pageMap?: Map<string, string>;
|
||||
updateAssetIds?: (assetsId: string) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Defines the interface for adapting between different blocks and target formats.
|
||||
* Used to convert blocks between a source format (TNode) and BlockSnapshot format.
|
||||
*
|
||||
* @template TNode - The source/target node type to convert from/to
|
||||
* @template TConverter - The converter used for handling delta format conversions
|
||||
*/
|
||||
export type BlockAdapterMatcher<
|
||||
TNode extends object = never,
|
||||
TConverter extends DeltaASTConverter = DeltaASTConverter,
|
||||
> = {
|
||||
/** The block flavour identifier */
|
||||
flavour: string;
|
||||
|
||||
/**
|
||||
* Function to check if a target node matches this adapter
|
||||
* @param o - The target node properties to check
|
||||
* @returns true if this adapter can handle the node
|
||||
*/
|
||||
toMatch: (o: NodeProps<TNode>) => boolean;
|
||||
|
||||
/**
|
||||
* Function to check if a BlockSnapshot matches this adapter
|
||||
* @param o - The BlockSnapshot properties to check
|
||||
* @returns true if this adapter can handle the snapshot
|
||||
*/
|
||||
fromMatch: (o: NodeProps<BlockSnapshot>) => boolean;
|
||||
|
||||
/**
|
||||
* Handlers for converting from target format to BlockSnapshot
|
||||
*/
|
||||
toBlockSnapshot: {
|
||||
/**
|
||||
* Called when entering a target walker node during traversal
|
||||
* @param o - The target node properties
|
||||
* @param context - The adapter context
|
||||
*/
|
||||
enter?: (
|
||||
o: NodeProps<TNode>,
|
||||
context: AdapterContext<TNode, BlockSnapshot, TConverter>
|
||||
) => void | Promise<void>;
|
||||
|
||||
/**
|
||||
* Called when leaving a target walker node during traversal
|
||||
* @param o - The target node properties
|
||||
* @param context - The adapter context
|
||||
*/
|
||||
leave?: (
|
||||
o: NodeProps<TNode>,
|
||||
context: AdapterContext<TNode, BlockSnapshot, TConverter>
|
||||
) => void | Promise<void>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Handlers for converting from BlockSnapshot to target format
|
||||
*/
|
||||
fromBlockSnapshot: {
|
||||
/**
|
||||
* Called when entering a BlockSnapshot walker node during traversal
|
||||
* @param o - The BlockSnapshot properties
|
||||
* @param context - The adapter context
|
||||
*/
|
||||
enter?: (
|
||||
o: NodeProps<BlockSnapshot>,
|
||||
context: AdapterContext<BlockSnapshot, TNode, TConverter>
|
||||
) => void | Promise<void>;
|
||||
|
||||
/**
|
||||
* Called when leaving a BlockSnapshot walker node during traversal
|
||||
* @param o - The BlockSnapshot properties
|
||||
* @param context - The adapter context
|
||||
*/
|
||||
leave?: (
|
||||
o: NodeProps<BlockSnapshot>,
|
||||
context: AdapterContext<BlockSnapshot, TNode, TConverter>
|
||||
) => void | Promise<void>;
|
||||
};
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
) => 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>[];
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { Element, Root, RootContentMap, Text } from 'hast';
|
||||
|
||||
export type HastUnionType<
|
||||
K extends keyof RootContentMap,
|
||||
V extends RootContentMap[K],
|
||||
> = V;
|
||||
|
||||
export type HtmlAST =
|
||||
| HastUnionType<keyof RootContentMap, RootContentMap[keyof RootContentMap]>
|
||||
| Root;
|
||||
|
||||
export type InlineHtmlAST = Element | Text;
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './adapter.js';
|
||||
export * from './hast.js';
|
||||
@@ -0,0 +1,37 @@
|
||||
const fetchImage = async (url: string, init?: RequestInit, proxy?: string) => {
|
||||
try {
|
||||
if (!proxy) {
|
||||
return await fetch(url, init);
|
||||
}
|
||||
if (url.startsWith('blob:')) {
|
||||
return await fetch(url, init);
|
||||
}
|
||||
if (url.startsWith('data:')) {
|
||||
return await fetch(url, init);
|
||||
}
|
||||
if (url.startsWith(window.location.origin)) {
|
||||
return await fetch(url, init);
|
||||
}
|
||||
return await fetch(proxy + '?url=' + encodeURIComponent(url), init)
|
||||
.then(res => {
|
||||
if (!res.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
return res;
|
||||
})
|
||||
.catch(() => fetch(url, init));
|
||||
} catch (error) {
|
||||
console.warn('Error fetching image:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchable = (url: string) =>
|
||||
url.startsWith('http:') ||
|
||||
url.startsWith('https:') ||
|
||||
url.startsWith('data:');
|
||||
|
||||
export const FetchUtils = {
|
||||
fetchImage,
|
||||
fetchable,
|
||||
};
|
||||
@@ -0,0 +1,266 @@
|
||||
import type { Element, ElementContent, Text } from 'hast';
|
||||
|
||||
import type { HtmlAST } from '../types/hast.js';
|
||||
|
||||
const isElement = (ast: HtmlAST): ast is Element => {
|
||||
return ast.type === 'element';
|
||||
};
|
||||
|
||||
const getTextContent = (ast: HtmlAST | undefined, defaultStr = ''): string => {
|
||||
if (!ast) {
|
||||
return defaultStr;
|
||||
}
|
||||
switch (ast.type) {
|
||||
case 'text': {
|
||||
return ast.value.replace(/\s+/g, ' ');
|
||||
}
|
||||
case 'element': {
|
||||
switch (ast.tagName) {
|
||||
case 'br': {
|
||||
return '\n';
|
||||
}
|
||||
}
|
||||
return ast.children.map(child => getTextContent(child)).join('');
|
||||
}
|
||||
}
|
||||
return defaultStr;
|
||||
};
|
||||
|
||||
const getElementChildren = (ast: HtmlAST | undefined): Element[] => {
|
||||
if (!ast) {
|
||||
return [];
|
||||
}
|
||||
if (ast.type === 'element') {
|
||||
return ast.children.filter(child => child.type === 'element') as Element[];
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const getTextChildren = (ast: HtmlAST | undefined): Text[] => {
|
||||
if (!ast) {
|
||||
return [];
|
||||
}
|
||||
if (ast.type === 'element') {
|
||||
return ast.children.filter(child => child.type === 'text') as Text[];
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const getTextChildrenOnlyAst = (ast: Element): Element => {
|
||||
return {
|
||||
...ast,
|
||||
children: getTextChildren(ast),
|
||||
};
|
||||
};
|
||||
|
||||
const isTagInline = (tagName: string): boolean => {
|
||||
// Phrasing content
|
||||
const inlineElements = [
|
||||
'a',
|
||||
'abbr',
|
||||
'audio',
|
||||
'b',
|
||||
'bdi',
|
||||
'bdo',
|
||||
'br',
|
||||
'button',
|
||||
'canvas',
|
||||
'cite',
|
||||
'code',
|
||||
'data',
|
||||
'datalist',
|
||||
'del',
|
||||
'dfn',
|
||||
'em',
|
||||
'embed',
|
||||
'i',
|
||||
// 'iframe' is not included because it needs special handling
|
||||
// 'img' is not included because it needs special handling
|
||||
'input',
|
||||
'ins',
|
||||
'kbd',
|
||||
'label',
|
||||
'link',
|
||||
'map',
|
||||
'mark',
|
||||
'math',
|
||||
'meta',
|
||||
'meter',
|
||||
'noscript',
|
||||
'object',
|
||||
'output',
|
||||
'picture',
|
||||
'progress',
|
||||
'q',
|
||||
'ruby',
|
||||
's',
|
||||
'samp',
|
||||
'script',
|
||||
'select',
|
||||
'slot',
|
||||
'small',
|
||||
'span',
|
||||
'strong',
|
||||
'sub',
|
||||
'sup',
|
||||
'svg',
|
||||
'template',
|
||||
'textarea',
|
||||
'time',
|
||||
'u',
|
||||
'var',
|
||||
'video',
|
||||
'wbr',
|
||||
];
|
||||
return inlineElements.includes(tagName);
|
||||
};
|
||||
|
||||
const isElementInline = (element: Element): boolean => {
|
||||
return (
|
||||
isTagInline(element.tagName) ||
|
||||
// Inline elements
|
||||
!!(
|
||||
typeof element.properties?.style === 'string' &&
|
||||
element.properties.style.match(/display:\s*inline/)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const getInlineElementsAndText = (ast: Element): (Element | Text)[] => {
|
||||
if (!ast || !ast.children) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return ast.children.filter((child): child is Element | Text => {
|
||||
if (child.type === 'text') {
|
||||
return true;
|
||||
}
|
||||
if (child.type === 'element' && child.tagName && isElementInline(child)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
const getInlineOnlyElementAST = (ast: Element): Element => {
|
||||
return {
|
||||
...ast,
|
||||
children: getInlineElementsAndText(ast),
|
||||
};
|
||||
};
|
||||
|
||||
const querySelectorTag = (
|
||||
ast: HtmlAST,
|
||||
tagName: string
|
||||
): Element | undefined => {
|
||||
if (ast.type === 'element') {
|
||||
if (ast.tagName === tagName) {
|
||||
return ast;
|
||||
}
|
||||
for (const child of ast.children) {
|
||||
const result = querySelectorTag(child, tagName);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const querySelectorClass = (
|
||||
ast: HtmlAST,
|
||||
className: string
|
||||
): Element | undefined => {
|
||||
if (ast.type === 'element') {
|
||||
if (
|
||||
Array.isArray(ast.properties?.className) &&
|
||||
ast.properties.className.includes(className)
|
||||
) {
|
||||
return ast;
|
||||
}
|
||||
for (const child of ast.children) {
|
||||
const result = querySelectorClass(child, className);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const querySelectorId = (ast: HtmlAST, id: string): Element | undefined => {
|
||||
if (ast.type === 'element') {
|
||||
if (ast.properties.id === id) {
|
||||
return ast;
|
||||
}
|
||||
for (const child of ast.children) {
|
||||
const result = querySelectorId(child, id);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const querySelector = (ast: HtmlAST, selector: string): Element | undefined => {
|
||||
if (ast.type === 'root') {
|
||||
for (const child of ast.children) {
|
||||
const result = querySelector(child, selector);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
} else if (ast.type === 'element') {
|
||||
if (selector.startsWith('.')) {
|
||||
return querySelectorClass(ast, selector.slice(1));
|
||||
} else if (selector.startsWith('#')) {
|
||||
return querySelectorId(ast, selector.slice(1));
|
||||
} else {
|
||||
return querySelectorTag(ast, selector);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const flatNodes = (
|
||||
ast: HtmlAST,
|
||||
expression: (tagName: string) => boolean
|
||||
): HtmlAST => {
|
||||
if (ast.type === 'element') {
|
||||
const children = ast.children.map(child => flatNodes(child, expression));
|
||||
return {
|
||||
...ast,
|
||||
children: children.flatMap(child => {
|
||||
if (child.type === 'element' && expression(child.tagName)) {
|
||||
return child.children;
|
||||
}
|
||||
return child;
|
||||
}) as ElementContent[],
|
||||
};
|
||||
}
|
||||
return ast;
|
||||
};
|
||||
|
||||
// Check if it is a paragraph like element
|
||||
// https://html.spec.whatwg.org/#paragraph
|
||||
const isParagraphLike = (node: Element): boolean => {
|
||||
// Flex container
|
||||
return (
|
||||
(typeof node.properties?.style === 'string' &&
|
||||
node.properties.style.match(/display:\s*flex/) !== null) ||
|
||||
getElementChildren(node).every(child => isElementInline(child))
|
||||
);
|
||||
};
|
||||
|
||||
export const HastUtils = {
|
||||
isElement,
|
||||
getTextContent,
|
||||
getElementChildren,
|
||||
getTextChildren,
|
||||
getTextChildrenOnlyAst,
|
||||
getInlineOnlyElementAST,
|
||||
querySelector,
|
||||
flatNodes,
|
||||
isParagraphLike,
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './fetch.js';
|
||||
export * from './hast.js';
|
||||
export * from './text.js';
|
||||
@@ -0,0 +1,83 @@
|
||||
import { isEqual } from '@blocksuite/global/utils';
|
||||
import type { DeltaInsert } from '@blocksuite/inline';
|
||||
|
||||
const mergeDeltas = (
|
||||
acc: DeltaInsert[],
|
||||
cur: DeltaInsert,
|
||||
options: { force?: boolean } = { force: false }
|
||||
) => {
|
||||
if (acc.length === 0) {
|
||||
return [cur];
|
||||
}
|
||||
const last = acc[acc.length - 1];
|
||||
if (options?.force) {
|
||||
last.insert = last.insert + cur.insert;
|
||||
last.attributes = Object.create(null);
|
||||
return acc;
|
||||
} else if (
|
||||
typeof last.insert === 'string' &&
|
||||
typeof cur.insert === 'string' &&
|
||||
(isEqual(last.attributes, cur.attributes) ||
|
||||
(last.attributes === undefined && cur.attributes === undefined))
|
||||
) {
|
||||
last.insert += cur.insert;
|
||||
return acc;
|
||||
}
|
||||
return [...acc, cur];
|
||||
};
|
||||
|
||||
const isNullish = (value: unknown) => value === null || value === undefined;
|
||||
|
||||
const createText = (s: string) => {
|
||||
return {
|
||||
'$blocksuite:internal:text$': true,
|
||||
delta: s.length === 0 ? [] : [{ insert: s }],
|
||||
};
|
||||
};
|
||||
|
||||
const isText = (o: unknown) => {
|
||||
if (
|
||||
typeof o === 'object' &&
|
||||
o !== null &&
|
||||
'$blocksuite:internal:text$' in o
|
||||
) {
|
||||
return o['$blocksuite:internal:text$'] === true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
function toURLSearchParams(
|
||||
params?: Partial<Record<string, string | string[]>>
|
||||
) {
|
||||
if (!params) return;
|
||||
|
||||
const items = Object.entries(params)
|
||||
.filter(([_, v]) => !isNullish(v))
|
||||
.filter(([_, v]) => {
|
||||
if (typeof v === 'string') {
|
||||
return v.length > 0;
|
||||
}
|
||||
if (Array.isArray(v)) {
|
||||
return v.length > 0;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.map(([k, v]) => [k, Array.isArray(v) ? v.filter(v => v.length) : v]) as [
|
||||
string,
|
||||
string | string[],
|
||||
][];
|
||||
|
||||
return new URLSearchParams(
|
||||
items
|
||||
.filter(([_, v]) => v.length)
|
||||
.map(([k, v]) => [k, Array.isArray(v) ? v.join(',') : v])
|
||||
);
|
||||
}
|
||||
|
||||
export const TextUtils = {
|
||||
mergeDeltas,
|
||||
isNullish,
|
||||
createText,
|
||||
isText,
|
||||
toURLSearchParams,
|
||||
};
|
||||
Reference in New Issue
Block a user