mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-09 13:15:51 +08:00
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:
@@ -0,0 +1,42 @@
|
||||
import {
|
||||
DividerBlockModel,
|
||||
ParagraphBlockModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { matchModels } from '@blocksuite/affine-shared/utils';
|
||||
import type { BlockStdScope } from '@blocksuite/block-std';
|
||||
import type { BlockModel } from '@blocksuite/store';
|
||||
|
||||
import { focusTextModel } from '../dom.js';
|
||||
import { beforeConvert } from './utils.js';
|
||||
|
||||
export function toDivider(
|
||||
std: BlockStdScope,
|
||||
model: BlockModel,
|
||||
prefix: string
|
||||
) {
|
||||
const { store: doc } = std;
|
||||
if (
|
||||
matchModels(model, [DividerBlockModel]) ||
|
||||
(matchModels(model, [ParagraphBlockModel]) && model.type === 'quote')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parent = doc.getParent(model);
|
||||
if (!parent) return;
|
||||
|
||||
const index = parent.children.indexOf(model);
|
||||
beforeConvert(std, model, prefix.length);
|
||||
const blockProps = {
|
||||
children: model.children,
|
||||
};
|
||||
doc.addBlock('affine:divider', blockProps, parent, index);
|
||||
|
||||
const nextBlock = parent.children[index + 1];
|
||||
let id = nextBlock?.id;
|
||||
if (!id) {
|
||||
id = doc.addBlock('affine:paragraph', {}, parent);
|
||||
}
|
||||
focusTextModel(std, id);
|
||||
return id;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { markdownInput } from './markdown-input.js';
|
||||
@@ -0,0 +1,54 @@
|
||||
import {
|
||||
type ListProps,
|
||||
type ListType,
|
||||
ParagraphBlockModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { matchModels, toNumberedList } from '@blocksuite/affine-shared/utils';
|
||||
import type { BlockStdScope } from '@blocksuite/block-std';
|
||||
import type { BlockModel } from '@blocksuite/store';
|
||||
|
||||
import { focusTextModel } from '../dom.js';
|
||||
import { beforeConvert } from './utils.js';
|
||||
|
||||
export function toList(
|
||||
std: BlockStdScope,
|
||||
model: BlockModel,
|
||||
listType: ListType,
|
||||
prefix: string,
|
||||
otherProperties?: Partial<ListProps>
|
||||
) {
|
||||
if (!matchModels(model, [ParagraphBlockModel])) {
|
||||
return;
|
||||
}
|
||||
const { store: doc } = std;
|
||||
const parent = doc.getParent(model);
|
||||
if (!parent) return;
|
||||
|
||||
beforeConvert(std, model, prefix.length);
|
||||
|
||||
if (listType !== 'numbered') {
|
||||
const index = parent.children.indexOf(model);
|
||||
const blockProps = {
|
||||
type: listType,
|
||||
text: model.text?.clone(),
|
||||
children: model.children,
|
||||
...otherProperties,
|
||||
};
|
||||
doc.deleteBlock(model, {
|
||||
deleteChildren: false,
|
||||
});
|
||||
|
||||
const id = doc.addBlock('affine:list', blockProps, parent, index);
|
||||
focusTextModel(std, id);
|
||||
return id;
|
||||
}
|
||||
|
||||
let order = parseInt(prefix.slice(0, -1));
|
||||
if (!Number.isInteger(order)) order = 1;
|
||||
|
||||
const id = toNumberedList(std, model, order);
|
||||
if (!id) return;
|
||||
|
||||
focusTextModel(std, id);
|
||||
return id;
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import {
|
||||
CalloutBlockModel,
|
||||
CodeBlockModel,
|
||||
ParagraphBlockModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
import {
|
||||
isHorizontalRuleMarkdown,
|
||||
isMarkdownPrefix,
|
||||
matchModels,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import { type BlockStdScope, TextSelection } from '@blocksuite/block-std';
|
||||
|
||||
import { getInlineEditorByModel } from '../dom.js';
|
||||
import { toDivider } from './divider.js';
|
||||
import { toList } from './list.js';
|
||||
import { toParagraph } from './paragraph.js';
|
||||
import { toCode } from './to-code.js';
|
||||
import { getPrefixText } from './utils.js';
|
||||
|
||||
export function markdownInput(
|
||||
std: BlockStdScope,
|
||||
id?: string
|
||||
): string | undefined {
|
||||
if (!id) {
|
||||
const selection = std.selection;
|
||||
const text = selection.find(TextSelection);
|
||||
id = text?.from.blockId;
|
||||
}
|
||||
if (!id) return;
|
||||
const model = std.store.getBlock(id)?.model;
|
||||
if (!model) return;
|
||||
const inline = getInlineEditorByModel(std.host, model);
|
||||
if (!inline) return;
|
||||
const range = inline.getInlineRange();
|
||||
if (!range) return;
|
||||
|
||||
const prefixText = getPrefixText(inline);
|
||||
if (!isMarkdownPrefix(prefixText)) return;
|
||||
|
||||
const isParagraph = matchModels(model, [ParagraphBlockModel]);
|
||||
const isHeading = isParagraph && model.type.startsWith('h');
|
||||
const isParagraphQuoteBlock = isParagraph && model.type === 'quote';
|
||||
const isCodeBlock = matchModels(model, [CodeBlockModel]);
|
||||
if (
|
||||
isHeading ||
|
||||
isParagraphQuoteBlock ||
|
||||
isCodeBlock ||
|
||||
matchModels(model.parent, [CalloutBlockModel])
|
||||
)
|
||||
return;
|
||||
|
||||
const lineInfo = inline.getLine(range.index);
|
||||
if (!lineInfo) return;
|
||||
|
||||
const { lineIndex, rangeIndexRelatedToLine } = lineInfo;
|
||||
if (lineIndex !== 0 || rangeIndexRelatedToLine > prefixText.length) return;
|
||||
|
||||
// try to add code block
|
||||
const codeMatch = prefixText.match(/^```([a-zA-Z0-9]*)$/g);
|
||||
if (codeMatch) {
|
||||
return toCode(std, model, prefixText, codeMatch[0].slice(3));
|
||||
}
|
||||
|
||||
if (isHorizontalRuleMarkdown(prefixText.trim())) {
|
||||
return toDivider(std, model, prefixText);
|
||||
}
|
||||
|
||||
switch (prefixText.trim()) {
|
||||
case '[]':
|
||||
case '[ ]':
|
||||
return toList(std, model, 'todo', prefixText, {
|
||||
checked: false,
|
||||
});
|
||||
case '[x]':
|
||||
return toList(std, model, 'todo', prefixText, {
|
||||
checked: true,
|
||||
});
|
||||
case '-':
|
||||
case '*':
|
||||
return toList(std, model, 'bulleted', prefixText);
|
||||
case '#':
|
||||
return toParagraph(std, model, 'h1', prefixText);
|
||||
case '##':
|
||||
return toParagraph(std, model, 'h2', prefixText);
|
||||
case '###':
|
||||
return toParagraph(std, model, 'h3', prefixText);
|
||||
case '####':
|
||||
return toParagraph(std, model, 'h4', prefixText);
|
||||
case '#####':
|
||||
return toParagraph(std, model, 'h5', prefixText);
|
||||
case '######':
|
||||
return toParagraph(std, model, 'h6', prefixText);
|
||||
case '>':
|
||||
return toParagraph(std, model, 'quote', prefixText);
|
||||
default:
|
||||
return toList(std, model, 'numbered', prefixText);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import {
|
||||
ParagraphBlockModel,
|
||||
type ParagraphType,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { matchModels } from '@blocksuite/affine-shared/utils';
|
||||
import type { BlockStdScope } from '@blocksuite/block-std';
|
||||
import type { BlockModel } from '@blocksuite/store';
|
||||
|
||||
import { focusTextModel } from '../dom.js';
|
||||
import { beforeConvert } from './utils.js';
|
||||
|
||||
export function toParagraph(
|
||||
std: BlockStdScope,
|
||||
model: BlockModel,
|
||||
type: ParagraphType,
|
||||
prefix: string
|
||||
) {
|
||||
const { store: doc } = std;
|
||||
if (!matchModels(model, [ParagraphBlockModel])) {
|
||||
const parent = doc.getParent(model);
|
||||
if (!parent) return;
|
||||
|
||||
const index = parent.children.indexOf(model);
|
||||
|
||||
beforeConvert(std, model, prefix.length);
|
||||
|
||||
const blockProps = {
|
||||
type: type,
|
||||
text: model.text?.clone(),
|
||||
children: model.children,
|
||||
};
|
||||
doc.deleteBlock(model, { deleteChildren: false });
|
||||
const id = doc.addBlock('affine:paragraph', blockProps, parent, index);
|
||||
|
||||
focusTextModel(std, id);
|
||||
return id;
|
||||
}
|
||||
|
||||
if (matchModels(model, [ParagraphBlockModel]) && model.type !== type) {
|
||||
beforeConvert(std, model, prefix.length);
|
||||
|
||||
doc.updateBlock(model, { type });
|
||||
|
||||
focusTextModel(std, model.id);
|
||||
}
|
||||
|
||||
// If the model is already a paragraph with the same type, do nothing
|
||||
return model.id;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { ParagraphBlockModel } from '@blocksuite/affine-model';
|
||||
import { matchModels } from '@blocksuite/affine-shared/utils';
|
||||
import type { BlockStdScope } from '@blocksuite/block-std';
|
||||
import type { BlockModel } from '@blocksuite/store';
|
||||
|
||||
import { focusTextModel } from '../dom.js';
|
||||
|
||||
export function toCode(
|
||||
std: BlockStdScope,
|
||||
model: BlockModel,
|
||||
prefixText: string,
|
||||
language: string | null
|
||||
) {
|
||||
if (matchModels(model, [ParagraphBlockModel]) && model.type === 'quote') {
|
||||
return;
|
||||
}
|
||||
|
||||
const doc = model.doc;
|
||||
const parent = doc.getParent(model);
|
||||
if (!parent) {
|
||||
return;
|
||||
}
|
||||
|
||||
doc.captureSync();
|
||||
const index = parent.children.indexOf(model);
|
||||
|
||||
const codeId = doc.addBlock('affine:code', { language }, parent, index);
|
||||
|
||||
if (model.text && model.text.length > prefixText.length) {
|
||||
const text = model.text.clone();
|
||||
doc.addBlock('affine:paragraph', { text }, parent, index + 1);
|
||||
text.delete(0, prefixText.length);
|
||||
}
|
||||
doc.deleteBlock(model, { bringChildrenTo: parent });
|
||||
|
||||
focusTextModel(std, codeId);
|
||||
|
||||
return codeId;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { BlockStdScope } from '@blocksuite/block-std';
|
||||
import type { InlineEditor } from '@blocksuite/inline';
|
||||
import type { BlockModel } from '@blocksuite/store';
|
||||
|
||||
import { focusTextModel } from '../dom.js';
|
||||
|
||||
export function getPrefixText(inlineEditor: InlineEditor) {
|
||||
const inlineRange = inlineEditor.getInlineRange();
|
||||
if (!inlineRange) return '';
|
||||
const firstLineEnd = inlineEditor.yTextString.search(/\n/);
|
||||
if (firstLineEnd !== -1 && inlineRange.index > firstLineEnd) {
|
||||
return '';
|
||||
}
|
||||
const textPoint = inlineEditor.getTextPoint(inlineRange.index);
|
||||
if (!textPoint) return '';
|
||||
const [leafStart, offsetStart] = textPoint;
|
||||
return leafStart.textContent
|
||||
? leafStart.textContent.slice(0, offsetStart)
|
||||
: '';
|
||||
}
|
||||
|
||||
export function beforeConvert(
|
||||
std: BlockStdScope,
|
||||
model: BlockModel,
|
||||
index: number
|
||||
) {
|
||||
const { text } = model;
|
||||
if (!text) return;
|
||||
// Add a space after the text, then stop capturing
|
||||
// So when the user undo, the prefix will be restored with a `space`
|
||||
// Ex. (| is the cursor position)
|
||||
// *| <- user input
|
||||
// <space> -> bullet list
|
||||
// *<space>| -> undo
|
||||
text.insert(' ', index);
|
||||
focusTextModel(std, model.id, index + 1);
|
||||
std.store.captureSync();
|
||||
text.delete(0, index + 1);
|
||||
}
|
||||
Reference in New Issue
Block a user