mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-21 03:56:23 +08:00
chore: merge blocksuite source code (#9213)
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
|
||||
import { sha } from '@blocksuite/global/utils';
|
||||
|
||||
/**
|
||||
* @internal just for test
|
||||
*/
|
||||
export class MemoryBlobCRUD {
|
||||
private readonly _map = new Map<string, Blob>();
|
||||
|
||||
delete(key: string) {
|
||||
this._map.delete(key);
|
||||
}
|
||||
|
||||
get(key: string) {
|
||||
return this._map.get(key) ?? null;
|
||||
}
|
||||
|
||||
list() {
|
||||
return Array.from(this._map.keys());
|
||||
}
|
||||
|
||||
async set(value: Blob): Promise<string>;
|
||||
|
||||
async set(key: string, value: Blob): Promise<string>;
|
||||
|
||||
async set(valueOrKey: string | Blob, _value?: Blob) {
|
||||
const key =
|
||||
typeof valueOrKey === 'string'
|
||||
? valueOrKey
|
||||
: await sha(await valueOrKey.arrayBuffer());
|
||||
const value = typeof valueOrKey === 'string' ? _value : valueOrKey;
|
||||
|
||||
if (!value) {
|
||||
throw new BlockSuiteError(
|
||||
ErrorCode.TransformerError,
|
||||
'value is required'
|
||||
);
|
||||
}
|
||||
|
||||
this._map.set(key, value);
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
export const mimeExtMap = new Map([
|
||||
['application/epub+zip', 'epub'],
|
||||
['application/gzip', 'gz'],
|
||||
['application/java-archive', 'jar'],
|
||||
['application/json', 'json'],
|
||||
['application/ld+json', 'jsonld'],
|
||||
['application/msword', 'doc'],
|
||||
['application/octet-stream', 'bin'],
|
||||
['application/ogg', 'ogx'],
|
||||
['application/pdf', 'pdf'],
|
||||
['application/rtf', 'rtf'],
|
||||
['application/vnd.amazon.ebook', 'azw'],
|
||||
['application/vnd.apple.installer+xml', 'mpkg'],
|
||||
['application/vnd.mozilla.xul+xml', 'xul'],
|
||||
['application/vnd.ms-excel', 'xls'],
|
||||
['application/vnd.ms-fontobject', 'eot'],
|
||||
['application/vnd.ms-powerpoint', 'ppt'],
|
||||
['application/vnd.oasis.opendocument.presentation', 'odp'],
|
||||
['application/vnd.oasis.opendocument.spreadsheet', 'ods'],
|
||||
['application/vnd.oasis.opendocument.text', 'odt'],
|
||||
[
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
'pptx',
|
||||
],
|
||||
['application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'xlsx'],
|
||||
[
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'docx',
|
||||
],
|
||||
['application/vnd.rar', 'rar'],
|
||||
['application/vnd.visio', 'vsd'],
|
||||
['application/x-7z-compressed', '7z'],
|
||||
['application/x-abiword', 'abw'],
|
||||
['application/x-bzip', 'bz'],
|
||||
['application/x-bzip2', 'bz2'],
|
||||
['application/x-cdf', 'cda'],
|
||||
['application/x-csh', 'csh'],
|
||||
['application/x-freearc', 'arc'],
|
||||
['application/x-httpd-php', 'php'],
|
||||
['application/x-sh', 'sh'],
|
||||
['application/x-tar', 'tar'],
|
||||
['application/xhtml+xml', 'xhtml'],
|
||||
['application/xml', 'xml'],
|
||||
['application/zip', 'zip'],
|
||||
['application/zstd', 'zst'],
|
||||
['audio/3gpp', '3gp'],
|
||||
['audio/3gpp2', '3g2'],
|
||||
['audio/aac', 'aac'],
|
||||
['audio/midi', 'mid'],
|
||||
['audio/mpeg', 'mp3'],
|
||||
['audio/ogg', 'oga'],
|
||||
['audio/opus', 'opus'],
|
||||
['audio/wav', 'wav'],
|
||||
['audio/webm', 'weba'],
|
||||
['audio/x-midi', 'midi'],
|
||||
['font/otf', 'otf'],
|
||||
['font/ttf', 'ttf'],
|
||||
['font/woff', 'woff'],
|
||||
['font/woff2', 'woff2'],
|
||||
['image/apng', 'apng'],
|
||||
['image/avif', 'avif'],
|
||||
['image/bmp', 'bmp'],
|
||||
['image/gif', 'gif'],
|
||||
['image/jpeg', 'jpeg'],
|
||||
['image/png', 'png'],
|
||||
['image/svg+xml', 'svg'],
|
||||
['image/tiff', 'tiff'],
|
||||
['image/vnd.microsoft.icon', 'ico'],
|
||||
['image/webp', 'webp'],
|
||||
['text/calendar', 'ics'],
|
||||
['text/css', 'css'],
|
||||
['text/csv', 'csv'],
|
||||
['text/html', 'html'],
|
||||
['text/javascript', 'js'],
|
||||
['text/plain', 'txt'],
|
||||
['text/xml', 'xml'],
|
||||
['video/3gpp', '3gp'],
|
||||
['video/3gpp2', '3g2'],
|
||||
['video/mp2t', 'ts'],
|
||||
['video/mp4', 'mp4'],
|
||||
['video/mpeg', 'mpeg'],
|
||||
['video/ogg', 'ogv'],
|
||||
['video/webm', 'webm'],
|
||||
['video/x-msvideo', 'avi'],
|
||||
]);
|
||||
|
||||
export const extMimeMap = new Map(
|
||||
Array.from(mimeExtMap.entries()).map(([mime, ext]) => [ext, mime])
|
||||
);
|
||||
|
||||
const getExt = (type: string) => {
|
||||
if (type === '') return 'blob';
|
||||
const ext = mimeExtMap.get(type);
|
||||
if (ext) return ext;
|
||||
const guessExt = type.split('/');
|
||||
return guessExt.at(-1) ?? 'blob';
|
||||
};
|
||||
|
||||
export function getAssetName(assets: Map<string, Blob>, blobId: string) {
|
||||
const blob = assets.get(blobId);
|
||||
if (!blob) {
|
||||
throw new BlockSuiteError(
|
||||
ErrorCode.TransformerError,
|
||||
`blob not found for blobId: ${blobId}`
|
||||
);
|
||||
}
|
||||
|
||||
const name =
|
||||
'name' in blob && typeof blob.name === 'string' ? blob.name : undefined;
|
||||
if (name) {
|
||||
if (name.includes('.')) return name;
|
||||
return `${name}.${getExt(blob.type)}`;
|
||||
}
|
||||
return `${blobId}.${getExt(blob.type)}`;
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
import { BlockSuiteError } from '@blocksuite/global/exceptions';
|
||||
|
||||
import type { Doc } from '../store/index.js';
|
||||
import type { AssetsManager } from '../transformer/assets.js';
|
||||
import type { DraftModel, Job, Slice } from '../transformer/index.js';
|
||||
import type {
|
||||
BlockSnapshot,
|
||||
DocSnapshot,
|
||||
SliceSnapshot,
|
||||
} from '../transformer/type.js';
|
||||
import { ASTWalkerContext } from './context.js';
|
||||
|
||||
export type FromDocSnapshotPayload = {
|
||||
snapshot: DocSnapshot;
|
||||
assets?: AssetsManager;
|
||||
};
|
||||
export type FromBlockSnapshotPayload = {
|
||||
snapshot: BlockSnapshot;
|
||||
assets?: AssetsManager;
|
||||
};
|
||||
export type FromSliceSnapshotPayload = {
|
||||
snapshot: SliceSnapshot;
|
||||
assets?: AssetsManager;
|
||||
};
|
||||
export type FromDocSnapshotResult<Target> = {
|
||||
file: Target;
|
||||
assetsIds: string[];
|
||||
};
|
||||
export type FromBlockSnapshotResult<Target> = {
|
||||
file: Target;
|
||||
assetsIds: string[];
|
||||
};
|
||||
export type FromSliceSnapshotResult<Target> = {
|
||||
file: Target;
|
||||
assetsIds: string[];
|
||||
};
|
||||
export type ToDocSnapshotPayload<Target> = {
|
||||
file: Target;
|
||||
assets?: AssetsManager;
|
||||
};
|
||||
export type ToBlockSnapshotPayload<Target> = {
|
||||
file: Target;
|
||||
assets?: AssetsManager;
|
||||
};
|
||||
export type ToSliceSnapshotPayload<Target> = {
|
||||
file: Target;
|
||||
assets?: AssetsManager;
|
||||
};
|
||||
|
||||
export function wrapFakeNote(snapshot: SliceSnapshot) {
|
||||
if (snapshot.content[0]?.flavour !== 'affine:note') {
|
||||
snapshot.content = [
|
||||
{
|
||||
type: 'block',
|
||||
id: '',
|
||||
flavour: 'affine:note',
|
||||
props: {},
|
||||
children: snapshot.content,
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class BaseAdapter<AdapterTarget = unknown> {
|
||||
job: Job;
|
||||
|
||||
get configs() {
|
||||
return this.job.adapterConfigs;
|
||||
}
|
||||
|
||||
constructor(job: Job) {
|
||||
this.job = job;
|
||||
}
|
||||
|
||||
async fromBlock(model: DraftModel) {
|
||||
try {
|
||||
const blockSnapshot = this.job.blockToSnapshot(model);
|
||||
if (!blockSnapshot) return;
|
||||
return await this.fromBlockSnapshot({
|
||||
snapshot: blockSnapshot,
|
||||
assets: this.job.assetsManager,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Cannot convert block to snapshot');
|
||||
console.error(error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
abstract fromBlockSnapshot(
|
||||
payload: FromBlockSnapshotPayload
|
||||
):
|
||||
| Promise<FromBlockSnapshotResult<AdapterTarget>>
|
||||
| FromBlockSnapshotResult<AdapterTarget>;
|
||||
|
||||
async fromDoc(doc: Doc) {
|
||||
try {
|
||||
const docSnapshot = this.job.docToSnapshot(doc);
|
||||
if (!docSnapshot) return;
|
||||
return await this.fromDocSnapshot({
|
||||
snapshot: docSnapshot,
|
||||
assets: this.job.assetsManager,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Cannot convert doc to snapshot');
|
||||
console.error(error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
abstract fromDocSnapshot(
|
||||
payload: FromDocSnapshotPayload
|
||||
):
|
||||
| Promise<FromDocSnapshotResult<AdapterTarget>>
|
||||
| FromDocSnapshotResult<AdapterTarget>;
|
||||
|
||||
async fromSlice(slice: Slice) {
|
||||
try {
|
||||
const sliceSnapshot = this.job.sliceToSnapshot(slice);
|
||||
if (!sliceSnapshot) return;
|
||||
wrapFakeNote(sliceSnapshot);
|
||||
return await this.fromSliceSnapshot({
|
||||
snapshot: sliceSnapshot,
|
||||
assets: this.job.assetsManager,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Cannot convert slice to snapshot');
|
||||
console.error(error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
abstract fromSliceSnapshot(
|
||||
payload: FromSliceSnapshotPayload
|
||||
):
|
||||
| Promise<FromSliceSnapshotResult<AdapterTarget>>
|
||||
| FromSliceSnapshotResult<AdapterTarget>;
|
||||
|
||||
async toBlock(
|
||||
payload: ToBlockSnapshotPayload<AdapterTarget>,
|
||||
doc: Doc,
|
||||
parent?: string,
|
||||
index?: number
|
||||
) {
|
||||
try {
|
||||
const snapshot = await this.toBlockSnapshot(payload);
|
||||
if (!snapshot) return;
|
||||
return await this.job.snapshotToBlock(snapshot, doc, parent, index);
|
||||
} catch (error) {
|
||||
console.error('Cannot convert block snapshot to block');
|
||||
console.error(error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
abstract toBlockSnapshot(
|
||||
payload: ToBlockSnapshotPayload<AdapterTarget>
|
||||
): Promise<BlockSnapshot> | BlockSnapshot;
|
||||
|
||||
async toDoc(payload: ToDocSnapshotPayload<AdapterTarget>) {
|
||||
try {
|
||||
const snapshot = await this.toDocSnapshot(payload);
|
||||
if (!snapshot) return;
|
||||
return await this.job.snapshotToDoc(snapshot);
|
||||
} catch (error) {
|
||||
console.error('Cannot convert doc snapshot to doc');
|
||||
console.error(error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
abstract toDocSnapshot(
|
||||
payload: ToDocSnapshotPayload<AdapterTarget>
|
||||
): Promise<DocSnapshot> | DocSnapshot;
|
||||
|
||||
async toSlice(
|
||||
payload: ToSliceSnapshotPayload<AdapterTarget>,
|
||||
doc: Doc,
|
||||
parent?: string,
|
||||
index?: number
|
||||
) {
|
||||
try {
|
||||
const snapshot = await this.toSliceSnapshot(payload);
|
||||
if (!snapshot) return;
|
||||
return await this.job.snapshotToSlice(snapshot, doc, parent, index);
|
||||
} catch (error) {
|
||||
console.error('Cannot convert slice snapshot to slice');
|
||||
console.error(error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
abstract toSliceSnapshot(
|
||||
payload: ToSliceSnapshotPayload<AdapterTarget>
|
||||
): Promise<SliceSnapshot | null> | SliceSnapshot | null;
|
||||
}
|
||||
|
||||
type Keyof<T> = T extends unknown ? keyof T : never;
|
||||
|
||||
type WalkerFn<ONode extends object, TNode extends object> = (
|
||||
o: NodeProps<ONode>,
|
||||
context: ASTWalkerContext<TNode>
|
||||
) => Promise<void> | void;
|
||||
|
||||
export type NodeProps<Node extends object> = {
|
||||
node: Node;
|
||||
next?: Node | null;
|
||||
parent: NodeProps<Node> | null;
|
||||
prop: Keyof<Node> | null;
|
||||
index: number | null;
|
||||
};
|
||||
|
||||
// Ported from https://github.com/Rich-Harris/estree-walker MIT License
|
||||
export class ASTWalker<ONode extends object, TNode extends object | never> {
|
||||
private _enter: WalkerFn<ONode, TNode> | undefined;
|
||||
|
||||
private _isONode!: (node: unknown) => node is ONode;
|
||||
|
||||
private _leave: WalkerFn<ONode, TNode> | undefined;
|
||||
|
||||
private _visit = async (o: NodeProps<ONode>) => {
|
||||
if (!o.node) return;
|
||||
this.context._skipChildrenNum = 0;
|
||||
this.context._skip = false;
|
||||
|
||||
if (this._enter) {
|
||||
await this._enter(o, this.context);
|
||||
}
|
||||
|
||||
if (this.context._skip) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const key in o.node) {
|
||||
const value = o.node[key];
|
||||
|
||||
if (value && typeof value === 'object') {
|
||||
if (Array.isArray(value)) {
|
||||
for (
|
||||
let i = this.context._skipChildrenNum;
|
||||
i < value.length;
|
||||
i += 1
|
||||
) {
|
||||
const item = value[i];
|
||||
if (
|
||||
item !== null &&
|
||||
typeof item === 'object' &&
|
||||
this._isONode(item)
|
||||
) {
|
||||
const nextItem = value[i + 1] ?? null;
|
||||
await this._visit({
|
||||
node: item,
|
||||
next: nextItem,
|
||||
parent: o,
|
||||
prop: key as unknown as Keyof<ONode>,
|
||||
index: i,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
this.context._skipChildrenNum === 0 &&
|
||||
this._isONode(value)
|
||||
) {
|
||||
await this._visit({
|
||||
node: value,
|
||||
next: null,
|
||||
parent: o,
|
||||
prop: key as unknown as Keyof<ONode>,
|
||||
index: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this._leave) {
|
||||
await this._leave(o, this.context);
|
||||
}
|
||||
};
|
||||
|
||||
private context: ASTWalkerContext<TNode>;
|
||||
|
||||
setEnter = (fn: WalkerFn<ONode, TNode>) => {
|
||||
this._enter = fn;
|
||||
};
|
||||
|
||||
setLeave = (fn: WalkerFn<ONode, TNode>) => {
|
||||
this._leave = fn;
|
||||
};
|
||||
|
||||
setONodeTypeGuard = (fn: (node: unknown) => node is ONode) => {
|
||||
this._isONode = fn;
|
||||
};
|
||||
|
||||
walk = async (oNode: ONode, tNode: TNode) => {
|
||||
this.context.openNode(tNode);
|
||||
await this._visit({ node: oNode, parent: null, prop: null, index: null });
|
||||
if (this.context.stack.length !== 1) {
|
||||
throw new BlockSuiteError(1, 'There are unclosed nodes');
|
||||
}
|
||||
return this.context.currentNode();
|
||||
};
|
||||
|
||||
walkONode = async (oNode: ONode) => {
|
||||
await this._visit({ node: oNode, parent: null, prop: null, index: null });
|
||||
};
|
||||
|
||||
constructor() {
|
||||
this.context = new ASTWalkerContext<TNode>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
type Keyof<T> = T extends unknown ? keyof T : never;
|
||||
|
||||
export class ASTWalkerContext<TNode extends object> {
|
||||
private _defaultProp: Keyof<TNode> = 'children' as unknown as Keyof<TNode>;
|
||||
|
||||
private _globalContext: Record<string, unknown> = Object.create(null);
|
||||
|
||||
private _stack: {
|
||||
node: TNode;
|
||||
prop: Keyof<TNode>;
|
||||
context: Record<string, unknown>;
|
||||
}[] = [];
|
||||
|
||||
_skip = false;
|
||||
|
||||
_skipChildrenNum = 0;
|
||||
|
||||
setDefaultProp = (parentProp: Keyof<TNode>) => {
|
||||
this._defaultProp = parentProp;
|
||||
};
|
||||
|
||||
get stack() {
|
||||
return this._stack;
|
||||
}
|
||||
|
||||
private current() {
|
||||
return this._stack[this._stack.length - 1];
|
||||
}
|
||||
|
||||
cleanGlobalContextStack(key: string) {
|
||||
if (this._globalContext[key] instanceof Array) {
|
||||
this._globalContext[key] = [];
|
||||
}
|
||||
}
|
||||
|
||||
closeNode() {
|
||||
const ele = this._stack.pop();
|
||||
if (!ele) return this;
|
||||
const parent = this._stack.pop();
|
||||
if (!parent) {
|
||||
this._stack.push(ele);
|
||||
return this;
|
||||
}
|
||||
if (parent.node[ele.prop] instanceof Array) {
|
||||
(parent.node[ele.prop] as Array<object>).push(ele.node);
|
||||
}
|
||||
this._stack.push(parent);
|
||||
return this;
|
||||
}
|
||||
|
||||
currentNode() {
|
||||
return this.current()?.node;
|
||||
}
|
||||
|
||||
getGlobalContext(key: string) {
|
||||
return this._globalContext[key];
|
||||
}
|
||||
|
||||
getGlobalContextStack<StackElement>(key: string) {
|
||||
const stack = this._globalContext[key];
|
||||
if (stack instanceof Array) {
|
||||
return stack as StackElement[];
|
||||
} else {
|
||||
return [] as StackElement[];
|
||||
}
|
||||
}
|
||||
|
||||
getNodeContext(key: string) {
|
||||
return this.current().context[key];
|
||||
}
|
||||
|
||||
getPreviousNodeContext(key: string) {
|
||||
return this._stack[this._stack.length - 2]?.context[key];
|
||||
}
|
||||
|
||||
openNode(node: TNode, parentProp?: Keyof<TNode>) {
|
||||
this._stack.push({
|
||||
node,
|
||||
prop: parentProp ?? this._defaultProp,
|
||||
context: Object.create(null),
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
previousNode() {
|
||||
return this._stack[this._stack.length - 2]?.node;
|
||||
}
|
||||
|
||||
pushGlobalContextStack<StackElement>(key: string, value: StackElement) {
|
||||
const stack = this._globalContext[key];
|
||||
if (stack instanceof Array) {
|
||||
stack.push(value);
|
||||
} else {
|
||||
this._globalContext[key] = [value];
|
||||
}
|
||||
}
|
||||
|
||||
setGlobalContext(key: string, value: unknown) {
|
||||
this._globalContext[key] = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
setGlobalContextStack<StackElement>(key: string, value: StackElement[]) {
|
||||
this._globalContext[key] = value;
|
||||
}
|
||||
|
||||
setNodeContext(key: string, value: unknown) {
|
||||
this._stack[this._stack.length - 1].context[key] = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
skipAllChildren() {
|
||||
this._skip = true;
|
||||
}
|
||||
|
||||
skipChildren(num = 1) {
|
||||
this._skipChildrenNum = num;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './assets.js';
|
||||
export * from './base.js';
|
||||
export * from './context.js';
|
||||
Reference in New Issue
Block a user