chore: merge blocksuite source code (#9213)

This commit is contained in:
Mirone
2024-12-20 15:38:06 +08:00
committed by GitHub
parent 2c9ef916f4
commit 30200ff86d
2031 changed files with 238888 additions and 229 deletions
@@ -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';