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

View File

@@ -0,0 +1,274 @@
import { type Disposable, Slot } from '@blocksuite/global/utils';
import type { Signal } from '@preact/signals-core';
import { computed, signal } from '@preact/signals-core';
import type * as Y from 'yjs';
import { z } from 'zod';
import { Boxed } from '../reactive/boxed.js';
import { Text } from '../reactive/text.js';
import type { YBlock } from '../store/doc/block/index.js';
import type { Doc } from '../store/index.js';
import type { BaseBlockTransformer } from '../transformer/base.js';
const FlavourSchema = z.string();
const ParentSchema = z.array(z.string()).optional();
const ContentSchema = z.array(z.string()).optional();
const role = ['root', 'hub', 'content'] as const;
const RoleSchema = z.enum(role);
export type RoleType = (typeof role)[number];
export interface InternalPrimitives {
Text: (input?: Y.Text | string) => Text;
Boxed: <T>(input: T) => Boxed<T>;
}
export const internalPrimitives: InternalPrimitives = Object.freeze({
Text: (input: Y.Text | string = '') => new Text(input),
Boxed: <T>(input: T) => new Boxed(input),
});
export const BlockSchema = z.object({
version: z.number(),
model: z.object({
role: RoleSchema,
flavour: FlavourSchema,
parent: ParentSchema,
children: ContentSchema,
props: z
.function()
.args(z.custom<InternalPrimitives>())
.returns(z.record(z.any()))
.optional(),
toModel: z.function().args().returns(z.custom<BlockModel>()).optional(),
}),
transformer: z
.function()
.args()
.returns(z.custom<BaseBlockTransformer>())
.optional(),
});
export type BlockSchemaType = z.infer<typeof BlockSchema>;
export type PropsGetter<Props> = (
internalPrimitives: InternalPrimitives
) => Props;
export type SchemaToModel<
Schema extends {
model: {
props: PropsGetter<object>;
flavour: string;
};
},
> = BlockModel<ReturnType<Schema['model']['props']>> &
ReturnType<Schema['model']['props']> & {
flavour: Schema['model']['flavour'];
};
export function defineBlockSchema<
Flavour extends string,
Role extends RoleType,
Props extends object,
Metadata extends Readonly<{
version: number;
role: Role;
parent?: string[];
children?: string[];
}>,
Model extends BlockModel<Props>,
Transformer extends BaseBlockTransformer<Props>,
>(options: {
flavour: Flavour;
metadata: Metadata;
props?: (internalPrimitives: InternalPrimitives) => Props;
toModel?: () => Model;
transformer?: () => Transformer;
}): {
version: number;
model: {
props: PropsGetter<Props>;
flavour: Flavour;
} & Metadata;
transformer?: () => Transformer;
};
export function defineBlockSchema({
flavour,
props,
metadata,
toModel,
transformer,
}: {
flavour: string;
metadata: {
version: number;
role: RoleType;
parent?: string[];
children?: string[];
};
props?: (internalPrimitives: InternalPrimitives) => Record<string, unknown>;
toModel?: () => BlockModel;
transformer?: () => BaseBlockTransformer;
}): BlockSchemaType {
const schema = {
version: metadata.version,
model: {
role: metadata.role,
parent: metadata.parent,
children: metadata.children,
flavour,
props,
toModel,
},
transformer,
} satisfies z.infer<typeof BlockSchema>;
BlockSchema.parse(schema);
return schema;
}
type SignaledProps<Props> = Props & {
[P in keyof Props & string as `${P}$`]: Signal<Props[P]>;
};
/**
* The MagicProps function is used to append the props to the class.
* For example:
*
* ```ts
* class MyBlock extends MagicProps()<{ foo: string }> {}
* const myBlock = new MyBlock();
* // You'll get type checking for the foo prop
* myBlock.foo = 'bar';
* ```
*/
function MagicProps(): {
new <Props>(): Props;
} {
return class {} as never;
}
const modelLabel = Symbol('model_label');
// @ts-expect-error FIXME: ts error
export class BlockModel<
Props extends object = object,
PropsSignal extends object = SignaledProps<Props>,
> extends MagicProps()<PropsSignal> {
private _children = signal<string[]>([]);
/**
* @deprecated use doc instead
*/
page!: Doc;
private _childModels = computed(() => {
const value: BlockModel[] = [];
this._children.value.forEach(id => {
const block = this.page.getBlock$(id);
if (block) {
value.push(block.model);
}
});
return value;
});
private _onCreated: Disposable;
private _onDeleted: Disposable;
childMap = computed(() =>
this._children.value.reduce((map, id, index) => {
map.set(id, index);
return map;
}, new Map<string, number>())
);
created = new Slot();
deleted = new Slot();
flavour!: string;
id!: string;
isEmpty = computed(() => {
return this._children.value.length === 0;
});
keys!: string[];
// This is used to avoid https://stackoverflow.com/questions/55886792/infer-typescript-generic-class-type
[modelLabel]: Props = 'type_info_label' as never;
pop!: (prop: keyof Props & string) => void;
propsUpdated = new Slot<{ key: string }>();
role!: RoleType;
stash!: (prop: keyof Props & string) => void;
// text is optional
text?: Text;
version!: number;
yBlock!: YBlock;
get children() {
return this._childModels.value;
}
get doc() {
return this.page;
}
set doc(doc: Doc) {
this.page = doc;
}
get parent() {
return this.doc.getParent(this);
}
constructor() {
super();
this._onCreated = this.created.once(() => {
this._children.value = this.yBlock.get('sys:children').toArray();
this.yBlock.get('sys:children').observe(event => {
this._children.value = event.target.toArray();
});
this.yBlock.observe(event => {
if (event.keysChanged.has('sys:children')) {
this._children.value = this.yBlock.get('sys:children').toArray();
}
});
});
this._onDeleted = this.deleted.once(() => {
this._onCreated.dispose();
});
}
dispose() {
this.created.dispose();
this.deleted.dispose();
this.propsUpdated.dispose();
}
firstChild(): BlockModel | null {
return this.children[0] || null;
}
lastChild(): BlockModel | null {
if (!this.children.length) {
return this;
}
return this.children[this.children.length - 1].lastChild();
}
[Symbol.dispose]() {
this._onCreated.dispose();
this._onDeleted.dispose();
}
}

View File

@@ -0,0 +1,20 @@
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
export class MigrationError extends BlockSuiteError {
constructor(description: string) {
super(
ErrorCode.MigrationError,
`Migration failed. Please report to https://github.com/toeverything/blocksuite/issues
${description}`
);
}
}
export class SchemaValidateError extends BlockSuiteError {
constructor(flavour: string, message: string) {
super(
ErrorCode.SchemaValidateError,
`Invalid schema for ${flavour}: ${message}`
);
}
}

View File

@@ -0,0 +1,2 @@
export * from './base.js';
export { Schema } from './schema.js';

View File

@@ -0,0 +1,182 @@
import { minimatch } from 'minimatch';
import { SCHEMA_NOT_FOUND_MESSAGE } from '../consts.js';
import type { BlockSchemaType } from './base.js';
import { BlockSchema } from './base.js';
import { SchemaValidateError } from './error.js';
export class Schema {
readonly flavourSchemaMap = new Map<string, BlockSchemaType>();
validate = (
flavour: string,
parentFlavour?: string,
childFlavours?: string[]
): void => {
const schema = this.flavourSchemaMap.get(flavour);
if (!schema) {
throw new SchemaValidateError(flavour, SCHEMA_NOT_FOUND_MESSAGE);
}
const validateChildren = () => {
childFlavours?.forEach(childFlavour => {
const childSchema = this.flavourSchemaMap.get(childFlavour);
if (!childSchema) {
throw new SchemaValidateError(childFlavour, SCHEMA_NOT_FOUND_MESSAGE);
}
this.validateSchema(childSchema, schema);
});
};
if (schema.model.role === 'root') {
if (parentFlavour) {
throw new SchemaValidateError(
schema.model.flavour,
'Root block cannot have parent.'
);
}
validateChildren();
return;
}
if (!parentFlavour) {
throw new SchemaValidateError(
schema.model.flavour,
'Hub/Content must have parent.'
);
}
const parentSchema = this.flavourSchemaMap.get(parentFlavour);
if (!parentSchema) {
throw new SchemaValidateError(parentFlavour, SCHEMA_NOT_FOUND_MESSAGE);
}
this.validateSchema(schema, parentSchema);
validateChildren();
};
get versions() {
return Object.fromEntries(
Array.from(this.flavourSchemaMap.values()).map(
(schema): [string, number] => [schema.model.flavour, schema.version]
)
);
}
private _matchFlavour(childFlavour: string, parentFlavour: string) {
return (
minimatch(childFlavour, parentFlavour) ||
minimatch(parentFlavour, childFlavour)
);
}
private _validateParent(
child: BlockSchemaType,
parent: BlockSchemaType
): boolean {
const _childFlavour = child.model.flavour;
const _parentFlavour = parent.model.flavour;
const childValidFlavours = child.model.parent || ['*'];
const parentValidFlavours = parent.model.children || ['*'];
return parentValidFlavours.some(parentValidFlavour => {
return childValidFlavours.some(childValidFlavour => {
if (parentValidFlavour === '*' && childValidFlavour === '*') {
return true;
}
if (parentValidFlavour === '*') {
return this._matchFlavour(childValidFlavour, _parentFlavour);
}
if (childValidFlavour === '*') {
return this._matchFlavour(_childFlavour, parentValidFlavour);
}
return (
this._matchFlavour(_childFlavour, parentValidFlavour) &&
this._matchFlavour(childValidFlavour, _parentFlavour)
);
});
});
}
private _validateRole(child: BlockSchemaType, parent: BlockSchemaType) {
const childRole = child.model.role;
const parentRole = parent.model.role;
const childFlavour = child.model.flavour;
const parentFlavour = parent.model.flavour;
if (childRole === 'root') {
throw new SchemaValidateError(
childFlavour,
`Root block cannot have parent: ${parentFlavour}.`
);
}
if (childRole === 'hub' && parentRole === 'content') {
throw new SchemaValidateError(
childFlavour,
`Hub block cannot be child of content block: ${parentFlavour}.`
);
}
if (childRole === 'content' && parentRole === 'root') {
throw new SchemaValidateError(
childFlavour,
`Content block can only be child of hub block or itself. But get: ${parentFlavour}.`
);
}
}
isValid(child: string, parent: string) {
const childSchema = this.flavourSchemaMap.get(child);
const parentSchema = this.flavourSchemaMap.get(parent);
if (!childSchema || !parentSchema) {
return false;
}
try {
this.validateSchema(childSchema, parentSchema);
return true;
} catch {
return false;
}
}
register(blockSchema: BlockSchemaType[]) {
blockSchema.forEach(schema => {
BlockSchema.parse(schema);
this.flavourSchemaMap.set(schema.model.flavour, schema);
});
return this;
}
toJSON() {
return Object.fromEntries(
Array.from(this.flavourSchemaMap.values()).map(
(schema): [string, Record<string, unknown>] => [
schema.model.flavour,
{
role: schema.model.role,
parent: schema.model.parent,
children: schema.model.children,
},
]
)
);
}
validateSchema(child: BlockSchemaType, parent: BlockSchemaType) {
this._validateRole(child, parent);
const relationCheckSuccess = this._validateParent(child, parent);
if (!relationCheckSuccess) {
throw new SchemaValidateError(
child.model.flavour,
`Block cannot have parent: ${parent.model.flavour}.`
);
}
}
}