refactor(editor): getFirstContentBlock -> getFirstBlock & getLastContentBlock -> getLastBlock (#10809)

This commit is contained in:
yoyoyohamapi
2025-03-14 02:35:22 +00:00
parent d3aae962bc
commit e086fd2a43
10 changed files with 530 additions and 325 deletions
@@ -0,0 +1,230 @@
/**
* @vitest-environment happy-dom
*/
import { describe, expect, it } from 'vitest';
import { getFirstBlockCommand } from '../../../commands/block-crud/get-first-content-block';
import { affine } from '../../helpers/affine-template';
describe('commands/block-crud', () => {
describe('getFirstBlockCommand', () => {
it('should return null when root is not exists', () => {
const host = affine`<affine-page></affine-page>`;
const [_, { firstBlock }] = host.command.exec(getFirstBlockCommand, {
role: 'content',
root: undefined,
});
expect(firstBlock).toBeNull();
});
it('should return first block with content role when found', () => {
const host = affine`
<affine-page>
<affine-note id="note-1">
<affine-paragraph id="paragraph-1-1">First Paragraph</affine-paragraph>
<affine-paragraph id="paragraph-1-2">Second Paragraph</affine-paragraph>
</affine-note>
<affine-note id="note-2">
<affine-paragraph id="paragraph-2-1">First Paragraph</affine-paragraph>
<affine-paragraph id="paragraph-2-2">Second Paragraph</affine-paragraph>
</affine-note>
</affine-page>
`;
const [_, { firstBlock }] = host.command.exec(getFirstBlockCommand, {
role: 'hub',
root: undefined,
});
expect(firstBlock?.id).toBe('note-1');
});
it('should return first block with any role in the array when found', () => {
const host = affine`
<affine-page>
<affine-note id="note-1">
<affine-paragraph id="paragraph-1-1">First Paragraph</affine-paragraph>
<affine-paragraph id="paragraph-1-2">Second Paragraph</affine-paragraph>
</affine-note>
<affine-note id="note-2">
<affine-paragraph id="paragraph-2-1">First Paragraph</affine-paragraph>
<affine-paragraph id="paragraph-2-2">Second Paragraph</affine-paragraph>
</affine-note>
</affine-page>
`;
const [_, { firstBlock }] = host.command.exec(getFirstBlockCommand, {
role: ['hub', 'content'],
root: undefined,
});
expect(firstBlock?.id).toBe('note-1');
});
it('should return first block with specified flavour when found', () => {
const host = affine`
<affine-page>
<affine-note id="note-1">
<affine-paragraph id="paragraph-1">Paragraph</affine-paragraph>
<affine-list id="list-1">List Item</affine-list>
</affine-note>
</affine-page>
`;
const note = host.doc.getBlock('note-1')?.model;
const [_, { firstBlock }] = host.command.exec(getFirstBlockCommand, {
flavour: 'affine:list',
root: note,
});
expect(firstBlock?.id).toBe('list-1');
});
it('should return first block with any flavour in the array when found', () => {
const host = affine`
<affine-page>
<affine-note id="note-1">
<affine-paragraph id="paragraph-1">Paragraph</affine-paragraph>
<affine-list id="list-1">List Item</affine-list>
</affine-note>
</affine-page>
`;
const note = host.doc.getBlock('note-1')?.model;
const [_, { firstBlock }] = host.command.exec(getFirstBlockCommand, {
flavour: ['affine:list', 'affine:code'],
root: note,
});
expect(firstBlock?.id).toBe('list-1');
});
it('should return first block matching both role and flavour when both specified', () => {
const host = affine`
<affine-page>
<affine-note id="note-1">
<affine-paragraph id="paragraph-1">Content Paragraph</affine-paragraph>
<affine-list id="list-1">Content List</affine-list>
<affine-paragraph id="paragraph-2">hub Paragraph</affine-paragraph>
</affine-note>
</affine-page>
`;
const note = host.doc.getBlock('note-1')?.model;
const [_, { firstBlock }] = host.command.exec(getFirstBlockCommand, {
role: 'content',
flavour: 'affine:list',
root: note,
});
expect(firstBlock?.id).toBe('list-1');
});
it('should return first block with default roles when role not specified', () => {
const host = affine`
<affine-page>
<affine-note id="note-1">
<affine-paragraph id="paragraph-1">hub Paragraph</affine-paragraph>
<affine-paragraph id="paragraph-2">Content Paragraph</affine-paragraph>
<affine-paragraph id="paragraph-3">Hub Paragraph</affine-paragraph>
</affine-note>
</affine-page>
`;
const [_, { firstBlock }] = host.command.exec(getFirstBlockCommand, {
root: undefined,
});
expect(firstBlock?.id).toBe('note-1');
});
it('should return first block with specified role when found', () => {
const host = affine`
<affine-page>
<affine-note id="note-1">
<affine-paragraph id="paragraph-1">Content Paragraph</affine-paragraph>
<affine-paragraph id="paragraph-2">hub Paragraph</affine-paragraph>
<affine-database id="database-1">Database</affine-database>
</affine-note>
</affine-page>
`;
const note = host.doc.getBlock('note-1')?.model;
const [_, { firstBlock }] = host.command.exec(getFirstBlockCommand, {
role: 'hub',
root: note,
});
expect(firstBlock?.id).toBe('database-1');
});
it('should return null when no blocks with specified role are found in children', () => {
const host = affine`
<affine-page>
<affine-note id="note-1">
<affine-paragraph id="paragraph-1">Content Paragraph</affine-paragraph>
<affine-paragraph id="paragraph-2">Another Content Paragraph</affine-paragraph>
</affine-note>
</affine-page>
`;
const note = host.doc.getBlock('note-1')?.model;
const [_, { firstBlock }] = host.command.exec(getFirstBlockCommand, {
role: 'hub',
root: note,
});
expect(firstBlock).toBeNull();
});
it('should return null when no blocks with specified flavour are found in children', () => {
const host = affine`
<affine-page>
<affine-note id="note-1">
<affine-paragraph id="paragraph-1">Paragraph</affine-paragraph>
<affine-paragraph id="paragraph-2">Another Paragraph</affine-paragraph>
</affine-note>
</affine-page>
`;
const note = host.doc.getBlock('note-1')?.model;
const [_, { firstBlock }] = host.command.exec(getFirstBlockCommand, {
flavour: 'affine:list',
root: note,
});
expect(firstBlock).toBeNull();
});
it('should return first block with specified role within specified root subtree', () => {
const host = affine`
<affine-page>
<affine-note id="note-1">
<affine-paragraph id="paragraph-1-1">1-1 Content</affine-paragraph>
<affine-paragraph id="paragraph-1-2">1-2 hub</affine-paragraph>
</affine-note>
<affine-note id="note-2">
<affine-paragraph id="paragraph-2-1">2-1 hub</affine-paragraph>
<affine-paragraph id="paragraph-2-2">2-2 Content</affine-paragraph>
</affine-note>
</affine-page>
`;
const note = host.doc.getBlock('note-2')?.model;
const [_, { firstBlock }] = host.command.exec(getFirstBlockCommand, {
role: 'content',
root: note,
});
expect(firstBlock?.id).toBe('paragraph-2-1');
});
});
});
@@ -1,89 +0,0 @@
/**
* @vitest-environment happy-dom
*/
import { describe, expect, it } from 'vitest';
import { getFirstContentBlockCommand } from '../../../commands/block-crud/get-first-content-block';
import { affine } from '../../helpers/affine-template';
describe('commands/block-crud', () => {
describe('getFirstContentBlockCommand', () => {
it('should return null when root is not provided and no note block exists', () => {
const host = affine`<affine-page></affine-page>`;
const [_, { firstBlock }] = host.command.exec(
getFirstContentBlockCommand,
{
root: undefined,
std: {
host,
} as any,
}
);
expect(firstBlock).toBeNull();
});
it('should return first content block when found', () => {
const host = affine`
<affine-page>
<affine-note id="note-1">
<affine-paragraph id="paragraph-1">First Paragraph</affine-paragraph>
<affine-paragraph id="paragraph-2">Second Paragraph</affine-paragraph>
</affine-note>
</affine-page>
`;
const [_, { firstBlock }] = host.command.exec(
getFirstContentBlockCommand,
{
root: undefined,
}
);
expect(firstBlock?.id).toBe('paragraph-1');
});
it('should return null when no content blocks are found in children', () => {
const host = affine`
<affine-page>
<affine-note id="note-1">
</affine-note>
</affine-page>
`;
const [_, { firstBlock }] = host.command.exec(
getFirstContentBlockCommand,
{}
);
expect(firstBlock).toBeNull();
});
it('should return first content block within specified root subtree', () => {
const host = affine`
<affine-page>
<affine-note id="note-1">
<affine-paragraph id="paragraph-1-1">1-1 Paragraph</affine-paragraph>
<affine-paragraph id="paragraph-1-2">1-2 Paragraph</affine-paragraph>
</affine-note>
<affine-note id="note-2">
<affine-paragraph id="paragraph-2-1">2-1 Paragraph</affine-paragraph>
<affine-paragraph id="paragraph-2-2">2-2 Paragraph</affine-paragraph>
</affine-note>
</affine-page>
`;
const noteBlock = host.doc.getBlock('note-2')?.model;
const [_, { firstBlock }] = host.command.exec(
getFirstContentBlockCommand,
{
root: noteBlock,
}
);
expect(firstBlock?.id).toBe('paragraph-2-1');
});
});
});
@@ -0,0 +1,230 @@
/**
* @vitest-environment happy-dom
*/
import { describe, expect, it } from 'vitest';
import { getLastBlockCommand } from '../../../commands/block-crud/get-last-content-block';
import { affine } from '../../helpers/affine-template';
describe('commands/block-crud', () => {
describe('getLastBlockCommand', () => {
it('should return null when root is not exists', () => {
const host = affine`<affine-page></affine-page>`;
const [_, { lastBlock }] = host.command.exec(getLastBlockCommand, {
role: 'content',
root: undefined,
});
expect(lastBlock).toBeNull();
});
it('should return last block with content role when found', () => {
const host = affine`
<affine-page>
<affine-note id="note-1">
<affine-paragraph id="paragraph-1-1">First Paragraph</affine-paragraph>
<affine-paragraph id="paragraph-1-2">Second Paragraph</affine-paragraph>
</affine-note>
<affine-note id="note-2">
<affine-paragraph id="paragraph-2-1">First Paragraph</affine-paragraph>
<affine-paragraph id="paragraph-2-2">Second Paragraph</affine-paragraph>
</affine-note>
</affine-page>
`;
const [_, { lastBlock }] = host.command.exec(getLastBlockCommand, {
role: 'hub',
root: undefined,
});
expect(lastBlock?.id).toBe('note-2');
});
it('should return last block with any role in the array when found', () => {
const host = affine`
<affine-page>
<affine-note id="note-1">
<affine-paragraph id="paragraph-1-1">First Paragraph</affine-paragraph>
<affine-paragraph id="paragraph-1-2">Second Paragraph</affine-paragraph>
</affine-note>
<affine-note id="note-2">
<affine-paragraph id="paragraph-2-1">First Paragraph</affine-paragraph>
<affine-paragraph id="paragraph-2-2">Second Paragraph</affine-paragraph>
</affine-note>
</affine-page>
`;
const [_, { lastBlock }] = host.command.exec(getLastBlockCommand, {
role: ['hub', 'content'],
root: undefined,
});
expect(lastBlock?.id).toBe('note-2');
});
it('should return last block with specified flavour when found', () => {
const host = affine`
<affine-page>
<affine-note id="note-1">
<affine-paragraph id="paragraph-1">Paragraph</affine-paragraph>
<affine-list id="list-1">List Item</affine-list>
</affine-note>
</affine-page>
`;
const note = host.doc.getBlock('note-1')?.model;
const [_, { lastBlock }] = host.command.exec(getLastBlockCommand, {
flavour: 'affine:list',
root: note,
});
expect(lastBlock?.id).toBe('list-1');
});
it('should return last block with any flavour in the array when found', () => {
const host = affine`
<affine-page>
<affine-note id="note-1">
<affine-paragraph id="paragraph-1">Paragraph</affine-paragraph>
<affine-list id="list-1">List Item</affine-list>
</affine-note>
</affine-page>
`;
const note = host.doc.getBlock('note-1')?.model;
const [_, { lastBlock }] = host.command.exec(getLastBlockCommand, {
flavour: ['affine:list', 'affine:code'],
root: note,
});
expect(lastBlock?.id).toBe('list-1');
});
it('should return last block matching both role and flavour when both specified', () => {
const host = affine`
<affine-page>
<affine-note id="note-1">
<affine-paragraph id="paragraph-1">Content Paragraph</affine-paragraph>
<affine-list id="list-1">Content List</affine-list>
<affine-paragraph id="paragraph-2">hub Paragraph</affine-paragraph>
</affine-note>
</affine-page>
`;
const note = host.doc.getBlock('note-1')?.model;
const [_, { lastBlock }] = host.command.exec(getLastBlockCommand, {
role: 'content',
flavour: 'affine:list',
root: note,
});
expect(lastBlock?.id).toBe('list-1');
});
it('should return last block with default roles when role not specified', () => {
const host = affine`
<affine-page>
<affine-note id="note-1">
<affine-paragraph id="paragraph-1">hub Paragraph</affine-paragraph>
<affine-paragraph id="paragraph-2">Content Paragraph</affine-paragraph>
<affine-paragraph id="paragraph-3">Hub Paragraph</affine-paragraph>
</affine-note>
</affine-page>
`;
const [_, { lastBlock }] = host.command.exec(getLastBlockCommand, {
root: undefined,
});
expect(lastBlock?.id).toBe('note-1');
});
it('should return last block with specified role when found', () => {
const host = affine`
<affine-page>
<affine-note id="note-1">
<affine-paragraph id="paragraph-1">Content Paragraph</affine-paragraph>
<affine-paragraph id="paragraph-2">hub Paragraph</affine-paragraph>
<affine-database id="database-1">Database</affine-database>
</affine-note>
</affine-page>
`;
const note = host.doc.getBlock('note-1')?.model;
const [_, { lastBlock }] = host.command.exec(getLastBlockCommand, {
role: 'hub',
root: note,
});
expect(lastBlock?.id).toBe('database-1');
});
it('should return null when no blocks with specified role are found in children', () => {
const host = affine`
<affine-page>
<affine-note id="note-1">
<affine-paragraph id="paragraph-1">Content Paragraph</affine-paragraph>
<affine-paragraph id="paragraph-2">Another Content Paragraph</affine-paragraph>
</affine-note>
</affine-page>
`;
const note = host.doc.getBlock('note-1')?.model;
const [_, { lastBlock }] = host.command.exec(getLastBlockCommand, {
role: 'hub',
root: note,
});
expect(lastBlock).toBeNull();
});
it('should return null when no blocks with specified flavour are found in children', () => {
const host = affine`
<affine-page>
<affine-note id="note-1">
<affine-paragraph id="paragraph-1">Paragraph</affine-paragraph>
<affine-paragraph id="paragraph-2">Another Paragraph</affine-paragraph>
</affine-note>
</affine-page>
`;
const note = host.doc.getBlock('note-1')?.model;
const [_, { lastBlock }] = host.command.exec(getLastBlockCommand, {
flavour: 'affine:list',
root: note,
});
expect(lastBlock).toBeNull();
});
it('should return last block with specified role within specified root subtree', () => {
const host = affine`
<affine-page>
<affine-note id="note-1">
<affine-paragraph id="paragraph-1-1">1-1 Content</affine-paragraph>
<affine-paragraph id="paragraph-1-2">1-2 hub</affine-paragraph>
</affine-note>
<affine-note id="note-2">
<affine-paragraph id="paragraph-2-1">2-1 hub</affine-paragraph>
<affine-paragraph id="paragraph-2-2">2-2 Content</affine-paragraph>
</affine-note>
</affine-page>
`;
const note = host.doc.getBlock('note-2')?.model;
const [_, { lastBlock }] = host.command.exec(getLastBlockCommand, {
role: 'content',
root: note,
});
expect(lastBlock?.id).toBe('paragraph-2-2');
});
});
});
@@ -1,80 +0,0 @@
/**
* @vitest-environment happy-dom
*/
import { describe, expect, it } from 'vitest';
import { getLastContentBlockCommand } from '../../../commands/block-crud/get-last-content-block';
import { affine } from '../../helpers/affine-template';
describe('commands/block-crud', () => {
describe('getLastContentBlockCommand', () => {
it('should return null when root is not provided and no note block exists', () => {
const host = affine`<affine-page></affine-page>`;
const [_, { lastBlock }] = host.command.exec(getLastContentBlockCommand, {
root: undefined,
std: {
host,
} as any,
});
expect(lastBlock).toBeNull();
});
it('should return last content block when found', () => {
const host = affine`
<affine-page>
<affine-note id="note-1">
<affine-paragraph id="paragraph-1">First Paragraph</affine-paragraph>
<affine-paragraph id="paragraph-2">Second Paragraph</affine-paragraph>
</affine-note>
</affine-page>
`;
const [_, { lastBlock }] = host.command.exec(getLastContentBlockCommand, {
root: undefined,
});
expect(lastBlock?.id).toBe('paragraph-2');
});
it('should return null when no content blocks are found in children', () => {
const host = affine`
<affine-page>
<affine-note id="note-1">
</affine-note>
</affine-page>
`;
const [_, { lastBlock }] = host.command.exec(
getLastContentBlockCommand,
{}
);
expect(lastBlock).toBeNull();
});
it('should return last content block within specified root subtree', () => {
const host = affine`
<affine-page>
<affine-note id="note-1">
<affine-paragraph id="paragraph-1-1">1-1 Paragraph</affine-paragraph>
<affine-paragraph id="paragraph-1-2">1-2 Paragraph</affine-paragraph>
</affine-note>
<affine-note id="note-2">
<affine-paragraph id="paragraph-2-1">2-1 Paragraph</affine-paragraph>
<affine-paragraph id="paragraph-2-2">2-2 Paragraph</affine-paragraph>
</affine-note>
</affine-page>
`;
const noteBlock = host.doc.getBlock('note-2')?.model;
const [_, { lastBlock }] = host.command.exec(getLastContentBlockCommand, {
root: noteBlock,
});
expect(lastBlock?.id).toBe('paragraph-2-2');
});
});
});
@@ -1,4 +1,5 @@
import {
DatabaseBlockSchemaExtension,
ImageBlockSchemaExtension,
ListBlockSchemaExtension,
NoteBlockSchemaExtension,
@@ -18,6 +19,7 @@ const extensions = [
ParagraphBlockSchemaExtension,
ListBlockSchemaExtension,
ImageBlockSchemaExtension,
DatabaseBlockSchemaExtension,
];
// Mapping from tag names to flavours
@@ -27,6 +29,7 @@ const tagToFlavour: Record<string, string> = {
'affine-paragraph': 'affine:paragraph',
'affine-list': 'affine:list',
'affine-image': 'affine:image',
'affine-database': 'affine:database',
};
/**
@@ -1,135 +0,0 @@
import {
BlockSchemaExtension,
defineBlockSchema,
type Store,
Text,
} from '@blocksuite/store';
import { TestWorkspace } from '@blocksuite/store/test';
import { type Element as HappyDOMElement, Window } from 'happy-dom';
// Define schema
const PageBlockSchema = defineBlockSchema({
flavour: 'affine:page',
props: () => ({}),
metadata: {
version: 1,
role: 'root',
children: ['affine:note'],
},
});
const NoteBlockSchema = defineBlockSchema({
flavour: 'affine:note',
props: () => ({}),
metadata: {
version: 1,
role: 'hub',
parent: ['affine:page'],
children: ['affine:paragraph'],
},
});
const ParagraphBlockSchema = defineBlockSchema({
flavour: 'affine:paragraph',
props: internal => ({
text: internal.Text(),
}),
metadata: {
version: 1,
role: 'content',
parent: ['affine:note'],
},
});
// Create schema extensions
const PageBlockSchemaExtension = BlockSchemaExtension(PageBlockSchema);
const NoteBlockSchemaExtension = BlockSchemaExtension(NoteBlockSchema);
const ParagraphBlockSchemaExtension =
BlockSchemaExtension(ParagraphBlockSchema);
// Extensions array
const extensions = [
PageBlockSchemaExtension,
NoteBlockSchemaExtension,
ParagraphBlockSchemaExtension,
];
/**
* Parse HTML string and create document block structure
* @param node Current DOM node
* @param doc Document object
* @param parentId Parent block ID
* @returns Created block ID
*/
function processNode(
node: HappyDOMElement,
doc: Store,
parentId?: string
): string | undefined {
// Skip text nodes and comments
if (node.nodeType !== 1) {
return undefined;
}
const tagName = node.tagName.toLowerCase();
let blockId: string | undefined = undefined;
// Create appropriate block based on tag name
if (tagName === 'affine-page') {
blockId = doc.addBlock('affine:page', {}, parentId);
} else if (tagName === 'affine-note') {
blockId = doc.addBlock('affine:note', {}, parentId);
} else if (tagName === 'affine-paragraph') {
// Get paragraph text content
const textContent = node.textContent || '';
// Get attributes
const props: Record<string, any> = { text: new Text(textContent) };
// Process custom attributes
for (const attr of Array.from(node.attributes)) {
if (attr.name === 'type') {
props.type = attr.value;
} else if (attr.name === 'checked' && attr.value === 'true') {
props.checked = true;
}
}
blockId = doc.addBlock('affine:paragraph', props, parentId);
} else {
console.warn(`Unknown tag name: ${tagName}`);
return undefined;
}
// Process child nodes
for (const childNode of Array.from(node.children) as HappyDOMElement[]) {
processNode(childNode, doc, blockId);
}
return blockId;
}
/**
* Create document from HTML string
* @param template HTML template string
* @returns Created document object
*/
export function createDocFromHTML(template: string) {
const workspace = new TestWorkspace({});
workspace.meta.initialize();
const doc = workspace.createDoc({ id: 'test-doc', extensions });
doc.load(() => {
const window = new Window();
const document = window.document;
const container = document.createElement('div');
container.innerHTML = template;
// Process each child node of the root
for (const childNode of Array.from(container.children)) {
processNode(childNode, doc);
}
});
return doc;
}