feat(editor): rich text package (#10689)

This PR performs a significant architectural refactoring by extracting rich text functionality into a dedicated package. Here are the key changes:

1. **New Package Creation**
- Created a new package `@blocksuite/affine-rich-text` to house rich text related functionality
- Moved rich text components, utilities, and types from `@blocksuite/affine-components` to this new package

2. **Dependency Updates**
- Updated multiple block packages to include the new `@blocksuite/affine-rich-text` as a direct dependency:
  - block-callout
  - block-code
  - block-database
  - block-edgeless-text
  - block-embed
  - block-list
  - block-note
  - block-paragraph

3. **Import Path Updates**
- Refactored all imports that previously referenced rich text functionality from `@blocksuite/affine-components/rich-text` to now use `@blocksuite/affine-rich-text`
- Updated imports for components like:
  - DefaultInlineManagerExtension
  - RichText types and interfaces
  - Text manipulation utilities (focusTextModel, textKeymap, etc.)
  - Reference node components and providers

4. **Build Configuration Updates**
- Added references to the new rich text package in the `tsconfig.json` files of all affected packages
- Maintained workspace dependencies using the `workspace:*` version specifier

The primary motivation appears to be:
1. Better separation of concerns by isolating rich text functionality
2. Improved maintainability through more modular package structure
3. Clearer dependencies between packages
4. Potential for better tree-shaking and bundle optimization

This is primarily an architectural improvement that should make the codebase more maintainable and better organized.
This commit is contained in:
Saul-Mirone
2025-03-07 04:08:47 +00:00
parent 8da12025af
commit fe5f0f62ec
176 changed files with 503 additions and 180 deletions
@@ -0,0 +1,5 @@
export * from './inline-manager.js';
export * from './inline-spec.js';
export * from './markdown-matcher.js';
export * from './ref-node-slots.js';
export * from './type.js';
@@ -0,0 +1,104 @@
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
import { type BlockStdScope, StdIdentifier } from '@blocksuite/block-std';
import {
createIdentifier,
type ServiceIdentifier,
} from '@blocksuite/global/di';
import {
type AttributeRenderer,
baseTextAttributes,
type DeltaInsert,
getDefaultAttributeRenderer,
} from '@blocksuite/inline';
import type { ExtensionType } from '@blocksuite/store';
import { z, type ZodObject, type ZodTypeAny } from 'zod';
import { MarkdownMatcherIdentifier } from './markdown-matcher.js';
import type { InlineMarkdownMatch, InlineSpecs } from './type.js';
export class InlineManager {
embedChecker = (delta: DeltaInsert<AffineTextAttributes>) => {
for (const spec of this.specs) {
if (spec.embed && spec.match(delta)) {
return true;
}
}
return false;
};
getRenderer = (): AttributeRenderer<AffineTextAttributes> => {
const defaultRenderer = getDefaultAttributeRenderer<AffineTextAttributes>();
const renderer: AttributeRenderer<AffineTextAttributes> = props => {
// Priority increases from front to back
for (const spec of this.specs.toReversed()) {
if (spec.match(props.delta)) {
return spec.renderer(props);
}
}
return defaultRenderer(props);
};
return renderer;
};
getSchema = (): ZodObject<Record<keyof AffineTextAttributes, ZodTypeAny>> => {
const defaultSchema = baseTextAttributes as unknown as ZodObject<
Record<keyof AffineTextAttributes, ZodTypeAny>
>;
const schema: ZodObject<Record<keyof AffineTextAttributes, ZodTypeAny>> =
this.specs.reduce((acc, cur) => {
const currentSchema = z.object({
[cur.name]: cur.schema,
}) as ZodObject<Record<keyof AffineTextAttributes, ZodTypeAny>>;
return acc.merge(currentSchema) as ZodObject<
Record<keyof AffineTextAttributes, ZodTypeAny>
>;
}, defaultSchema);
return schema;
};
readonly specs: Array<InlineSpecs<AffineTextAttributes>>;
constructor(
readonly std: BlockStdScope,
readonly markdownMatches: InlineMarkdownMatch<AffineTextAttributes>[],
...specs: Array<InlineSpecs<AffineTextAttributes>>
) {
this.specs = specs;
}
}
export const InlineManagerIdentifier = createIdentifier<InlineManager>(
'AffineInlineManager'
);
export type InlineManagerExtensionConfig = {
id: string;
enableMarkdown?: boolean;
specs: ServiceIdentifier<InlineSpecs<AffineTextAttributes>>[];
};
export function InlineManagerExtension({
id,
enableMarkdown = true,
specs,
}: InlineManagerExtensionConfig): ExtensionType & {
identifier: ServiceIdentifier<InlineManager>;
} {
const identifier = InlineManagerIdentifier(id);
return {
setup: di => {
di.addImpl(identifier, provider => {
return new InlineManager(
provider.get(StdIdentifier),
enableMarkdown
? Array.from(provider.getAll(MarkdownMatcherIdentifier).values())
: [],
...specs.map(spec => provider.get(spec))
);
});
},
identifier,
};
}
@@ -0,0 +1,47 @@
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
import {
createIdentifier,
type ServiceIdentifier,
type ServiceProvider,
} from '@blocksuite/global/di';
import type { ExtensionType } from '@blocksuite/store';
import type { InlineSpecs } from './type.js';
export const InlineSpecIdentifier =
createIdentifier<InlineSpecs<AffineTextAttributes>>('AffineInlineSpec');
export function InlineSpecExtension(
name: string,
getSpec: (provider: ServiceProvider) => InlineSpecs<AffineTextAttributes>
): ExtensionType & {
identifier: ServiceIdentifier<InlineSpecs<AffineTextAttributes>>;
};
export function InlineSpecExtension(
spec: InlineSpecs<AffineTextAttributes>
): ExtensionType & {
identifier: ServiceIdentifier<InlineSpecs<AffineTextAttributes>>;
};
export function InlineSpecExtension(
nameOrSpec: string | InlineSpecs<AffineTextAttributes>,
getSpec?: (provider: ServiceProvider) => InlineSpecs<AffineTextAttributes>
): ExtensionType & {
identifier: ServiceIdentifier<InlineSpecs<AffineTextAttributes>>;
} {
if (typeof nameOrSpec === 'string') {
const identifier = InlineSpecIdentifier(nameOrSpec);
return {
identifier,
setup: di => {
di.addImpl(identifier, provider => getSpec!(provider));
},
};
}
const identifier = InlineSpecIdentifier(nameOrSpec.name);
return {
identifier,
setup: di => {
di.addImpl(identifier, nameOrSpec);
},
};
}
@@ -0,0 +1,27 @@
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
import {
createIdentifier,
type ServiceIdentifier,
} from '@blocksuite/global/di';
import type { ExtensionType } from '@blocksuite/store';
import type { InlineMarkdownMatch } from './type.js';
export const MarkdownMatcherIdentifier = createIdentifier<
InlineMarkdownMatch<AffineTextAttributes>
>('AffineMarkdownMatcher');
export function InlineMarkdownExtension(
matcher: InlineMarkdownMatch<AffineTextAttributes>
): ExtensionType & {
identifier: ServiceIdentifier<InlineMarkdownMatch<AffineTextAttributes>>;
} {
const identifier = MarkdownMatcherIdentifier(matcher.name);
return {
setup: di => {
di.addImpl(identifier, () => ({ ...matcher }));
},
identifier,
};
}
@@ -0,0 +1,18 @@
import { createIdentifier } from '@blocksuite/global/di';
import { Slot } from '@blocksuite/global/slot';
import type { ExtensionType } from '@blocksuite/store';
import type { RefNodeSlots } from '../inline/index.js';
export const RefNodeSlotsProvider =
createIdentifier<RefNodeSlots>('AffineRefNodeSlots');
const slots: RefNodeSlots = {
docLinkClicked: new Slot(),
};
export const RefNodeSlotsExtension: ExtensionType = {
setup: di => {
di.addImpl(RefNodeSlotsProvider, () => slots);
},
};
@@ -0,0 +1,38 @@
import type {
AttributeRenderer,
BaseTextAttributes,
DeltaInsert,
InlineEditor,
InlineRange,
} from '@blocksuite/inline';
import type * as Y from 'yjs';
import type { ZodTypeAny } from 'zod';
export type InlineSpecs<
AffineTextAttributes extends BaseTextAttributes = BaseTextAttributes,
> = {
name: keyof AffineTextAttributes | string;
schema: ZodTypeAny;
match: (delta: DeltaInsert<AffineTextAttributes>) => boolean;
renderer: AttributeRenderer<AffineTextAttributes>;
embed?: boolean;
};
export type InlineMarkdownMatchAction<
// @ts-expect-error We allow to covariance for AffineTextAttributes
in AffineTextAttributes extends BaseTextAttributes = BaseTextAttributes,
> = (props: {
inlineEditor: InlineEditor<AffineTextAttributes>;
prefixText: string;
inlineRange: InlineRange;
pattern: RegExp;
undoManager: Y.UndoManager;
}) => void;
export type InlineMarkdownMatch<
AffineTextAttributes extends BaseTextAttributes = BaseTextAttributes,
> = {
name: string;
pattern: RegExp;
action: InlineMarkdownMatchAction<AffineTextAttributes>;
};