refactor(editor): support dynamic text attribute key (#12947)

#### PR Dependency Tree


* **PR #12946**
  * **PR #12947** 👈
    * **PR #12948**

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)
This commit is contained in:
L-Sun
2025-07-02 16:09:01 +08:00
committed by GitHub
parent facf6ee28b
commit a66096cdf9
11 changed files with 101 additions and 49 deletions

View File

@@ -20,7 +20,9 @@ import { z } from 'zod';
export const CodeBlockUnitSpecExtension =
InlineSpecExtension<AffineTextAttributes>({
name: 'code-block-unit',
schema: z.undefined(),
schema: z.object({
'code-block-uint': z.undefined(),
}),
match: () => true,
renderer: ({ delta }) => {
return html`<affine-code-unit .delta=${delta}></affine-code-unit>`;

View File

@@ -3,6 +3,7 @@ import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
import { StdIdentifier } from '@blocksuite/std';
import { InlineSpecExtension } from '@blocksuite/std/inline';
import { html } from 'lit';
import z from 'zod';
import { FootNoteNodeConfigIdentifier } from './footnote-node/footnote-config';
@@ -13,7 +14,9 @@ export const FootNoteInlineSpecExtension =
provider.getOptional(FootNoteNodeConfigIdentifier) ?? undefined;
return {
name: 'footnote',
schema: FootNoteSchema.optional().nullable().catch(undefined),
schema: z.object({
footnote: FootNoteSchema.optional().nullable().catch(undefined),
}),
match: delta => {
return !!delta.attributes?.footnote;
},

View File

@@ -9,7 +9,9 @@ export const LatexInlineSpecExtension =
const std = provider.get(StdIdentifier);
return {
name: 'latex',
schema: z.string().optional().nullable().catch(undefined),
schema: z.object({
latex: z.string().optional().nullable().catch(undefined),
}),
match: delta => typeof delta.attributes?.latex === 'string',
renderer: ({ delta, selected, editor, startOffset, endOffset }) => {
return html`<affine-latex-node
@@ -28,7 +30,9 @@ export const LatexInlineSpecExtension =
export const LatexEditorUnitSpecExtension =
InlineSpecExtension<AffineTextAttributes>({
name: 'latex-editor-unit',
schema: z.undefined(),
schema: z.object({
'latex-editor-unit': z.undefined(),
}),
match: () => true,
renderer: ({ delta }) => {
return html`<latex-editor-unit .delta=${delta}></latex-editor-unit>`;

View File

@@ -9,7 +9,9 @@ export const LinkInlineSpecExtension =
const std = provider.get(StdIdentifier);
return {
name: 'link',
schema: z.string().optional().nullable().catch(undefined),
schema: z.object({
link: z.string().optional().nullable().catch(undefined),
}),
match: delta => {
return !!delta.attributes?.link;
},

View File

@@ -9,14 +9,16 @@ export const MentionInlineSpecExtension =
const std = provider.get(StdIdentifier);
return {
name: 'mention',
schema: z
.object({
member: z.string(),
notification: z.string().optional(),
})
.optional()
.nullable()
.catch(undefined),
schema: z.object({
mention: z
.object({
member: z.string(),
notification: z.string().optional(),
})
.optional()
.nullable()
.catch(undefined),
}),
match: delta => {
return !!delta.attributes?.mention?.member;
},

View File

@@ -12,7 +12,9 @@ export type AffineInlineRootElement = InlineRootElement<AffineTextAttributes>;
export const BoldInlineSpecExtension =
InlineSpecExtension<AffineTextAttributes>({
name: 'bold',
schema: z.literal(true).optional().nullable().catch(undefined),
schema: z.object({
bold: z.literal(true).optional().nullable().catch(undefined),
}),
match: delta => {
return !!delta.attributes?.bold;
},
@@ -24,7 +26,9 @@ export const BoldInlineSpecExtension =
export const ItalicInlineSpecExtension =
InlineSpecExtension<AffineTextAttributes>({
name: 'italic',
schema: z.literal(true).optional().nullable().catch(undefined),
schema: z.object({
italic: z.literal(true).optional().nullable().catch(undefined),
}),
match: delta => {
return !!delta.attributes?.italic;
},
@@ -36,7 +40,9 @@ export const ItalicInlineSpecExtension =
export const UnderlineInlineSpecExtension =
InlineSpecExtension<AffineTextAttributes>({
name: 'underline',
schema: z.literal(true).optional().nullable().catch(undefined),
schema: z.object({
underline: z.literal(true).optional().nullable().catch(undefined),
}),
match: delta => {
return !!delta.attributes?.underline;
},
@@ -48,7 +54,9 @@ export const UnderlineInlineSpecExtension =
export const StrikeInlineSpecExtension =
InlineSpecExtension<AffineTextAttributes>({
name: 'strike',
schema: z.literal(true).optional().nullable().catch(undefined),
schema: z.object({
strike: z.literal(true).optional().nullable().catch(undefined),
}),
match: delta => {
return !!delta.attributes?.strike;
},
@@ -60,7 +68,9 @@ export const StrikeInlineSpecExtension =
export const CodeInlineSpecExtension =
InlineSpecExtension<AffineTextAttributes>({
name: 'inline-code',
schema: z.literal(true).optional().nullable().catch(undefined),
schema: z.object({
code: z.literal(true).optional().nullable().catch(undefined),
}),
match: delta => {
return !!delta.attributes?.code;
},
@@ -72,7 +82,9 @@ export const CodeInlineSpecExtension =
export const BackgroundInlineSpecExtension =
InlineSpecExtension<AffineTextAttributes>({
name: 'background',
schema: z.string().optional().nullable().catch(undefined),
schema: z.object({
background: z.string().optional().nullable().catch(undefined),
}),
match: delta => {
return !!delta.attributes?.background;
},
@@ -84,7 +96,9 @@ export const BackgroundInlineSpecExtension =
export const ColorInlineSpecExtension =
InlineSpecExtension<AffineTextAttributes>({
name: 'color',
schema: z.string().optional().nullable().catch(undefined),
schema: z.object({
color: z.string().optional().nullable().catch(undefined),
}),
match: delta => {
return !!delta.attributes?.color;
},

View File

@@ -27,18 +27,20 @@ export const ReferenceInlineSpecExtension =
}
return {
name: 'reference',
schema: z
.object({
type: z.enum([
// @deprecated Subpage is deprecated, use LinkedPage instead
'Subpage',
'LinkedPage',
]),
})
.merge(ReferenceInfoSchema)
.optional()
.nullable()
.catch(undefined),
schema: z.object({
reference: z
.object({
type: z.enum([
// @deprecated Subpage is deprecated, use LinkedPage instead
'Subpage',
'LinkedPage',
]),
})
.merge(ReferenceInfoSchema)
.optional()
.nullable()
.catch(undefined),
}),
match: delta => {
return !!delta.attributes?.reference;
},

View File

@@ -8,7 +8,7 @@ import {
type DeltaInsert,
type ExtensionType,
} from '@blocksuite/store';
import { z, type ZodObject, type ZodTypeAny } from 'zod';
import { z } from 'zod';
import { StdIdentifier } from '../../identifier.js';
import type { BlockStdScope } from '../../scope/index.js';
@@ -42,20 +42,10 @@ export class InlineManager<TextAttributes extends BaseTextAttributes> {
return renderer;
};
getSchema = (): ZodObject<Record<keyof TextAttributes, ZodTypeAny>> => {
const defaultSchema = baseTextAttributes as unknown as ZodObject<
Record<keyof TextAttributes, ZodTypeAny>
>;
const schema: ZodObject<Record<keyof TextAttributes, ZodTypeAny>> =
this.specs.reduce((acc, cur) => {
const currentSchema = z.object({
[cur.name]: cur.schema,
}) as ZodObject<Record<keyof TextAttributes, ZodTypeAny>>;
return acc.merge(currentSchema) as ZodObject<
Record<keyof TextAttributes, ZodTypeAny>
>;
}, defaultSchema);
getSchema = (): z.ZodSchema => {
const schema = this.specs.reduce<z.ZodSchema>((acc, cur) => {
return z.intersection(acc, cur.schema);
}, baseTextAttributes);
return schema;
};

View File

@@ -5,13 +5,19 @@ import type {
} from '@blocksuite/std/inline';
import type { BaseTextAttributes, DeltaInsert } from '@blocksuite/store';
import type * as Y from 'yjs';
import type { ZodTypeAny } from 'zod';
import type { AnyZodObject, KeySchema, ZodEffects, ZodRecord } from 'zod';
export type InlineSpecs<
TextAttributes extends BaseTextAttributes = BaseTextAttributes,
> = {
name: keyof TextAttributes | string;
schema: ZodTypeAny;
schema:
| AnyZodObject
| ZodEffects<
ZodRecord<KeySchema>,
Partial<Record<string, unknown>>,
unknown
>;
match: (delta: DeltaInsert<TextAttributes>) => boolean;
renderer: AttributeRenderer<TextAttributes>;
embed?: boolean;

View File

@@ -0,0 +1,26 @@
import { z, type ZodTypeAny } from 'zod';
export function dynamicSchema<Key extends string, Value extends ZodTypeAny>(
keyValidator: (key: string) => key is Key,
valueType: Value
) {
return z.preprocess(
record => {
// check it is a record
if (typeof record !== 'object' || record === null) {
return {};
}
return Object.entries(record)
.filter((data): data is [Key, unknown] => keyValidator(data[0]))
.reduce(
(acc, [key, value]) => {
acc[key] = value;
return acc;
},
{} as Record<Key, unknown>
);
},
z.record(z.custom<Key>(keyValidator), valueType)
);
}

View File

@@ -1,5 +1,6 @@
export * from './attribute-renderer.js';
export * from './delta-convert.js';
export * from './dynamic-schema.js';
export * from './embed.js';
export * from './guard.js';
export * from './point-conversion.js';