refactor(editor): merge inline to std (#11025)

This commit is contained in:
Saul-Mirone
2025-03-20 05:46:56 +00:00
parent 5aa36efab0
commit 92d76ba571
176 changed files with 142 additions and 534 deletions
@@ -0,0 +1,145 @@
import { expect, test } from 'vitest';
import {
deltaInsertsToChunks,
transformDelta,
} from '../../inline/utils/delta-convert.js';
test('transformDelta', () => {
expect(
transformDelta({
insert: 'aaa',
attributes: {
bold: true,
},
})
).toEqual([
{
insert: 'aaa',
attributes: {
bold: true,
},
},
]);
expect(
transformDelta({
insert: '\n\naaa\n\nbbb\n\n',
attributes: {
bold: true,
},
})
).toEqual([
'\n',
'\n',
{
insert: 'aaa',
attributes: {
bold: true,
},
},
'\n',
'\n',
{
insert: 'bbb',
attributes: {
bold: true,
},
},
'\n',
'\n',
]);
});
test('deltaInsertsToChunks', () => {
expect(
deltaInsertsToChunks([
{
insert: 'aaa',
attributes: {
bold: true,
},
},
])
).toEqual([
[
{
insert: 'aaa',
attributes: {
bold: true,
},
},
],
]);
expect(
deltaInsertsToChunks([
{
insert: '\n\naaa\nbbb\n\n',
attributes: {
bold: true,
},
},
])
).toEqual([
[],
[],
[
{
insert: 'aaa',
attributes: {
bold: true,
},
},
],
[
{
insert: 'bbb',
attributes: {
bold: true,
},
},
],
[],
[],
]);
expect(
deltaInsertsToChunks([
{
insert: '\n\naaa\n',
attributes: {
bold: true,
},
},
{
insert: '\nbbb\n\n',
attributes: {
italic: true,
},
},
])
).toEqual([
[],
[],
[
{
insert: 'aaa',
attributes: {
bold: true,
},
},
],
[],
[
{
insert: 'bbb',
attributes: {
italic: true,
},
},
],
[],
[],
]);
});
@@ -0,0 +1,526 @@
import { expect, test } from 'vitest';
import * as Y from 'yjs';
import { InlineEditor } from '../../inline/index.js';
test('getDeltaByRangeIndex', () => {
const yDoc = new Y.Doc();
const yText = yDoc.getText('text');
yText.applyDelta([
{
insert: 'aaa',
attributes: {
bold: true,
},
},
{
insert: 'bbb',
attributes: {
italic: true,
},
},
]);
const inlineEditor = new InlineEditor(yText);
expect(inlineEditor.getDeltaByRangeIndex(0)).toEqual({
insert: 'aaa',
attributes: {
bold: true,
},
});
expect(inlineEditor.getDeltaByRangeIndex(1)).toEqual({
insert: 'aaa',
attributes: {
bold: true,
},
});
expect(inlineEditor.getDeltaByRangeIndex(3)).toEqual({
insert: 'aaa',
attributes: {
bold: true,
},
});
expect(inlineEditor.getDeltaByRangeIndex(4)).toEqual({
insert: 'bbb',
attributes: {
italic: true,
},
});
});
test('getDeltasByInlineRange', () => {
const yDoc = new Y.Doc();
const yText = yDoc.getText('text');
yText.applyDelta([
{
insert: 'aaa',
attributes: {
bold: true,
},
},
{
insert: 'bbb',
attributes: {
italic: true,
},
},
{
insert: 'ccc',
attributes: {
underline: true,
},
},
]);
const inlineEditor = new InlineEditor(yText);
expect(
inlineEditor.getDeltasByInlineRange({
index: 0,
length: 0,
})
).toEqual([
[
{
insert: 'aaa',
attributes: {
bold: true,
},
},
{
index: 0,
length: 3,
},
],
]);
expect(
inlineEditor.getDeltasByInlineRange({
index: 0,
length: 1,
})
).toEqual([
[
{
insert: 'aaa',
attributes: {
bold: true,
},
},
{
index: 0,
length: 3,
},
],
]);
expect(
inlineEditor.getDeltasByInlineRange({
index: 0,
length: 3,
})
).toEqual([
[
{
insert: 'aaa',
attributes: {
bold: true,
},
},
{
index: 0,
length: 3,
},
],
]);
expect(
inlineEditor.getDeltasByInlineRange({
index: 0,
length: 4,
})
).toEqual([
[
{
insert: 'aaa',
attributes: {
bold: true,
},
},
{
index: 0,
length: 3,
},
],
[
{
insert: 'bbb',
attributes: {
italic: true,
},
},
{
index: 3,
length: 3,
},
],
]);
expect(
inlineEditor.getDeltasByInlineRange({
index: 3,
length: 1,
})
).toEqual([
[
{
insert: 'aaa',
attributes: {
bold: true,
},
},
{
index: 0,
length: 3,
},
],
[
{
insert: 'bbb',
attributes: {
italic: true,
},
},
{
index: 3,
length: 3,
},
],
]);
expect(
inlineEditor.getDeltasByInlineRange({
index: 3,
length: 3,
})
).toEqual([
[
{
insert: 'aaa',
attributes: {
bold: true,
},
},
{
index: 0,
length: 3,
},
],
[
{
insert: 'bbb',
attributes: {
italic: true,
},
},
{
index: 3,
length: 3,
},
],
]);
expect(
inlineEditor.getDeltasByInlineRange({
index: 3,
length: 4,
})
).toEqual([
[
{
insert: 'aaa',
attributes: {
bold: true,
},
},
{
index: 0,
length: 3,
},
],
[
{
insert: 'bbb',
attributes: {
italic: true,
},
},
{
index: 3,
length: 3,
},
],
[
{
insert: 'ccc',
attributes: {
underline: true,
},
},
{
index: 6,
length: 3,
},
],
]);
expect(
inlineEditor.getDeltasByInlineRange({
index: 4,
length: 0,
})
).toEqual([
[
{
insert: 'bbb',
attributes: {
italic: true,
},
},
{
index: 3,
length: 3,
},
],
]);
expect(
inlineEditor.getDeltasByInlineRange({
index: 4,
length: 1,
})
).toEqual([
[
{
insert: 'bbb',
attributes: {
italic: true,
},
},
{
index: 3,
length: 3,
},
],
]);
expect(
inlineEditor.getDeltasByInlineRange({
index: 4,
length: 2,
})
).toEqual([
[
{
insert: 'bbb',
attributes: {
italic: true,
},
},
{
index: 3,
length: 3,
},
],
]);
expect(
inlineEditor.getDeltasByInlineRange({
index: 4,
length: 4,
})
).toEqual([
[
{
insert: 'bbb',
attributes: {
italic: true,
},
},
{
index: 3,
length: 3,
},
],
[
{
insert: 'ccc',
attributes: {
underline: true,
},
},
{
index: 6,
length: 3,
},
],
]);
});
test('cursor with format', () => {
const yDoc = new Y.Doc();
const yText = yDoc.getText('text');
const inlineEditor = new InlineEditor(yText);
inlineEditor.insertText(
{
index: 0,
length: 0,
},
'aaa',
{
bold: true,
}
);
inlineEditor.setMarks({
italic: true,
});
inlineEditor.insertText(
{
index: 3,
length: 0,
},
'bbb'
);
expect(inlineEditor.yText.toDelta()).toEqual([
{
insert: 'aaa',
attributes: {
bold: true,
},
},
{
insert: 'bbb',
attributes: {
italic: true,
},
},
]);
});
test('getFormat', () => {
const yDoc = new Y.Doc();
const yText = yDoc.getText('text');
const inlineEditor = new InlineEditor(yText);
inlineEditor.insertText(
{
index: 0,
length: 0,
},
'aaa',
{
bold: true,
}
);
inlineEditor.insertText(
{
index: 3,
length: 0,
},
'bbb',
{
italic: true,
}
);
expect(inlineEditor.getFormat({ index: 0, length: 0 })).toEqual({});
expect(inlineEditor.getFormat({ index: 0, length: 1 })).toEqual({
bold: true,
});
expect(inlineEditor.getFormat({ index: 0, length: 3 })).toEqual({
bold: true,
});
expect(inlineEditor.getFormat({ index: 3, length: 0 })).toEqual({
bold: true,
});
expect(inlineEditor.getFormat({ index: 3, length: 1 })).toEqual({
italic: true,
});
expect(inlineEditor.getFormat({ index: 3, length: 3 })).toEqual({
italic: true,
});
expect(inlineEditor.getFormat({ index: 6, length: 0 })).toEqual({
italic: true,
});
});
test('incorrect format value `false`', () => {
const yDoc = new Y.Doc();
const yText = yDoc.getText('text');
const inlineEditor = new InlineEditor(yText);
inlineEditor.insertText(
{
index: 0,
length: 0,
},
'aaa',
{
// @ts-expect-error insert incorrect value
bold: false,
italic: true,
}
);
inlineEditor.insertText(
{
index: 3,
length: 0,
},
'bbb',
{
underline: true,
}
);
expect(inlineEditor.yText.toDelta()).toEqual([
{
insert: 'aaa',
attributes: {
italic: true,
},
},
{
insert: 'bbb',
attributes: {
underline: true,
},
},
]);
});
test('yText should not contain \r', () => {
const yDoc = new Y.Doc();
const yText = yDoc.getText('text');
yText.insert(0, 'aaa\r');
expect(yText.toString()).toEqual('aaa\r');
expect(() => {
new InlineEditor(yText);
}).toThrow(
'yText must not contain "\\r" because it will break the range synchronization'
);
});
@@ -0,0 +1,347 @@
import { expect, test } from 'vitest';
import {
intersectInlineRange,
isInlineRangeAfter,
isInlineRangeBefore,
isInlineRangeContain,
isInlineRangeEdge,
isInlineRangeEdgeAfter,
isInlineRangeEdgeBefore,
isInlineRangeEqual,
isInlineRangeIntersect,
isPoint,
mergeInlineRange,
} from '../../inline/utils/inline-range.js';
test('isInlineRangeContain', () => {
expect(
isInlineRangeContain({ index: 0, length: 0 }, { index: 0, length: 0 })
).toEqual(true);
expect(
isInlineRangeContain({ index: 0, length: 0 }, { index: 0, length: 2 })
).toEqual(false);
expect(
isInlineRangeContain({ index: 0, length: 2 }, { index: 0, length: 0 })
).toEqual(true);
expect(
isInlineRangeContain({ index: 0, length: 2 }, { index: 0, length: 1 })
).toEqual(true);
expect(
isInlineRangeContain({ index: 0, length: 2 }, { index: 0, length: 2 })
).toEqual(true);
expect(
isInlineRangeContain({ index: 1, length: 3 }, { index: 0, length: 0 })
).toEqual(false);
expect(
isInlineRangeContain({ index: 1, length: 3 }, { index: 0, length: 1 })
).toEqual(false);
expect(
isInlineRangeContain({ index: 1, length: 3 }, { index: 0, length: 2 })
).toEqual(false);
expect(
isInlineRangeContain({ index: 1, length: 4 }, { index: 2, length: 0 })
).toEqual(true);
expect(
isInlineRangeContain({ index: 1, length: 4 }, { index: 2, length: 3 })
).toEqual(true);
expect(
isInlineRangeContain({ index: 1, length: 4 }, { index: 2, length: 4 })
).toEqual(false);
});
test('isInlineRangeEqual', () => {
expect(
isInlineRangeEqual({ index: 0, length: 0 }, { index: 0, length: 0 })
).toEqual(true);
expect(
isInlineRangeEqual({ index: 0, length: 2 }, { index: 0, length: 1 })
).toEqual(false);
expect(
isInlineRangeEqual({ index: 1, length: 3 }, { index: 1, length: 3 })
).toEqual(true);
expect(
isInlineRangeEqual({ index: 0, length: 0 }, { index: 1, length: 0 })
).toEqual(false);
expect(
isInlineRangeEqual({ index: 2, length: 0 }, { index: 2, length: 0 })
).toEqual(true);
});
test('isInlineRangeIntersect', () => {
expect(
isInlineRangeIntersect({ index: 0, length: 2 }, { index: 0, length: 0 })
).toEqual(true);
expect(
isInlineRangeIntersect({ index: 0, length: 2 }, { index: 2, length: 0 })
).toEqual(true);
expect(
isInlineRangeIntersect({ index: 0, length: 0 }, { index: 1, length: 0 })
).toEqual(false);
expect(
isInlineRangeIntersect({ index: 1, length: 0 }, { index: 1, length: 0 })
).toEqual(true);
expect(
isInlineRangeIntersect({ index: 1, length: 0 }, { index: 0, length: 1 })
).toEqual(true);
expect(
isInlineRangeIntersect({ index: 1, length: 0 }, { index: 0, length: 0 })
).toEqual(false);
expect(
isInlineRangeIntersect({ index: 1, length: 0 }, { index: 2, length: 0 })
).toEqual(false);
expect(
isInlineRangeIntersect({ index: 1, length: 0 }, { index: 0, length: 2 })
).toEqual(true);
});
test('isInlineRangeBefore', () => {
expect(
isInlineRangeBefore({ index: 0, length: 1 }, { index: 2, length: 0 })
).toEqual(true);
expect(
isInlineRangeBefore({ index: 2, length: 0 }, { index: 0, length: 1 })
).toEqual(false);
expect(
isInlineRangeBefore({ index: 0, length: 0 }, { index: 1, length: 0 })
).toEqual(true);
expect(
isInlineRangeBefore({ index: 1, length: 0 }, { index: 0, length: 0 })
).toEqual(false);
expect(
isInlineRangeBefore({ index: 0, length: 0 }, { index: 0, length: 0 })
).toEqual(true);
expect(
isInlineRangeBefore({ index: 0, length: 0 }, { index: 0, length: 1 })
).toEqual(true);
expect(
isInlineRangeBefore({ index: 0, length: 1 }, { index: 0, length: 0 })
).toEqual(false);
});
test('isInlineRangeAfter', () => {
expect(
isInlineRangeAfter({ index: 2, length: 0 }, { index: 0, length: 1 })
).toEqual(true);
expect(
isInlineRangeAfter({ index: 0, length: 1 }, { index: 2, length: 0 })
).toEqual(false);
expect(
isInlineRangeAfter({ index: 1, length: 0 }, { index: 0, length: 0 })
).toEqual(true);
expect(
isInlineRangeAfter({ index: 0, length: 0 }, { index: 1, length: 0 })
).toEqual(false);
expect(
isInlineRangeAfter({ index: 0, length: 0 }, { index: 0, length: 0 })
).toEqual(true);
expect(
isInlineRangeAfter({ index: 0, length: 0 }, { index: 0, length: 1 })
).toEqual(false);
expect(
isInlineRangeAfter({ index: 0, length: 1 }, { index: 0, length: 0 })
).toEqual(true);
});
test('isInlineRangeEdge', () => {
expect(isInlineRangeEdge(1, { index: 1, length: 0 })).toEqual(true);
expect(isInlineRangeEdge(1, { index: 0, length: 1 })).toEqual(true);
expect(isInlineRangeEdge(0, { index: 0, length: 0 })).toEqual(true);
expect(isInlineRangeEdge(1, { index: 0, length: 0 })).toEqual(false);
expect(isInlineRangeEdge(0, { index: 1, length: 0 })).toEqual(false);
expect(isInlineRangeEdge(0, { index: 0, length: 1 })).toEqual(true);
});
test('isInlineRangeEdgeBefore', () => {
expect(isInlineRangeEdgeBefore(1, { index: 1, length: 0 })).toEqual(true);
expect(isInlineRangeEdgeBefore(1, { index: 0, length: 1 })).toEqual(false);
expect(isInlineRangeEdgeBefore(0, { index: 0, length: 0 })).toEqual(true);
expect(isInlineRangeEdgeBefore(1, { index: 0, length: 0 })).toEqual(false);
expect(isInlineRangeEdgeBefore(0, { index: 1, length: 0 })).toEqual(false);
expect(isInlineRangeEdgeBefore(0, { index: 0, length: 1 })).toEqual(true);
});
test('isInlineRangeEdgeAfter', () => {
expect(isInlineRangeEdgeAfter(1, { index: 0, length: 1 })).toEqual(true);
expect(isInlineRangeEdgeAfter(1, { index: 1, length: 0 })).toEqual(true);
expect(isInlineRangeEdgeAfter(0, { index: 0, length: 0 })).toEqual(true);
expect(isInlineRangeEdgeAfter(0, { index: 1, length: 0 })).toEqual(false);
expect(isInlineRangeEdgeAfter(1, { index: 0, length: 0 })).toEqual(false);
expect(isInlineRangeEdgeAfter(0, { index: 0, length: 1 })).toEqual(false);
expect(isInlineRangeEdgeAfter(0, { index: 0, length: 0 })).toEqual(true);
});
test('isPoint', () => {
expect(isPoint({ index: 1, length: 0 })).toEqual(true);
expect(isPoint({ index: 0, length: 2 })).toEqual(false);
expect(isPoint({ index: 0, length: 0 })).toEqual(true);
expect(isPoint({ index: 2, length: 0 })).toEqual(true);
expect(isPoint({ index: 2, length: 2 })).toEqual(false);
});
test('mergeInlineRange', () => {
expect(
mergeInlineRange({ index: 0, length: 0 }, { index: 1, length: 0 })
).toEqual({
index: 0,
length: 1,
});
expect(
mergeInlineRange({ index: 0, length: 0 }, { index: 0, length: 0 })
).toEqual({
index: 0,
length: 0,
});
expect(
mergeInlineRange({ index: 1, length: 0 }, { index: 2, length: 0 })
).toEqual({
index: 1,
length: 1,
});
expect(
mergeInlineRange({ index: 2, length: 0 }, { index: 1, length: 0 })
).toEqual({
index: 1,
length: 1,
});
expect(
mergeInlineRange({ index: 1, length: 3 }, { index: 2, length: 2 })
).toEqual({
index: 1,
length: 3,
});
expect(
mergeInlineRange({ index: 2, length: 2 }, { index: 1, length: 1 })
).toEqual({
index: 1,
length: 3,
});
expect(
mergeInlineRange({ index: 3, length: 2 }, { index: 2, length: 1 })
).toEqual({
index: 2,
length: 3,
});
expect(
mergeInlineRange({ index: 0, length: 4 }, { index: 1, length: 1 })
).toEqual({
index: 0,
length: 4,
});
expect(
mergeInlineRange({ index: 1, length: 1 }, { index: 0, length: 4 })
).toEqual({
index: 0,
length: 4,
});
expect(
mergeInlineRange({ index: 0, length: 2 }, { index: 1, length: 3 })
).toEqual({
index: 0,
length: 4,
});
});
test('intersectInlineRange', () => {
expect(
intersectInlineRange({ index: 0, length: 0 }, { index: 1, length: 0 })
).toEqual(null);
expect(
intersectInlineRange({ index: 0, length: 2 }, { index: 1, length: 1 })
).toEqual({ index: 1, length: 1 });
expect(
intersectInlineRange({ index: 0, length: 2 }, { index: 2, length: 0 })
).toEqual({ index: 2, length: 0 });
expect(
intersectInlineRange({ index: 1, length: 0 }, { index: 1, length: 0 })
).toEqual({ index: 1, length: 0 });
expect(
intersectInlineRange({ index: 1, length: 3 }, { index: 2, length: 2 })
).toEqual({ index: 2, length: 2 });
expect(
intersectInlineRange({ index: 1, length: 2 }, { index: 0, length: 3 })
).toEqual({ index: 1, length: 2 });
expect(
intersectInlineRange({ index: 1, length: 1 }, { index: 2, length: 2 })
).toEqual({ index: 2, length: 0 });
expect(
intersectInlineRange({ index: 2, length: 2 }, { index: 1, length: 3 })
).toEqual({ index: 2, length: 2 });
expect(
intersectInlineRange({ index: 2, length: 1 }, { index: 1, length: 1 })
).toEqual({ index: 2, length: 0 });
expect(
intersectInlineRange({ index: 0, length: 4 }, { index: 1, length: 2 })
).toEqual({ index: 1, length: 2 });
});
@@ -0,0 +1,173 @@
import type { DeltaInsert } from '@blocksuite/store';
import { expect, type Page } from '@playwright/test';
import type { InlineEditor, InlineRange } from '../../inline/index.js';
const defaultPlaygroundURL = new URL(
`http://localhost:${process.env.CI ? 4173 : 5173}/`
);
export async function type(page: Page, content: string) {
await page.keyboard.type(content, { delay: 50 });
}
export async function press(page: Page, content: string) {
await page.keyboard.press(content, { delay: 50 });
await page.waitForTimeout(50);
}
export async function enterInlineEditorPlayground(page: Page) {
const url = new URL('examples/inline/index.html', defaultPlaygroundURL);
await page.goto(url.toString());
}
export async function focusInlineRichText(
page: Page,
index = 0
): Promise<void> {
await page.evaluate(index => {
const richTexts = document
.querySelector('test-page')
?.querySelectorAll('test-rich-text');
if (!richTexts) {
throw new Error('Cannot find test-rich-text');
}
(richTexts[index] as any).inlineEditor.focusEnd();
}, index);
}
export async function getDeltaFromInlineRichText(
page: Page,
index = 0
): Promise<DeltaInsert> {
await page.waitForTimeout(100);
return page.evaluate(index => {
const richTexts = document
.querySelector('test-page')
?.querySelectorAll('test-rich-text');
if (!richTexts) {
throw new Error('Cannot find test-rich-text');
}
const editor = (richTexts[index] as any).inlineEditor as InlineEditor;
return editor.yText.toDelta();
}, index);
}
export async function getInlineRangeFromInlineRichText(
page: Page,
index = 0
): Promise<InlineRange | null> {
await page.waitForTimeout(100);
return page.evaluate(index => {
const richTexts = document
.querySelector('test-page')
?.querySelectorAll('test-rich-text');
if (!richTexts) {
throw new Error('Cannot find test-rich-text');
}
const editor = (richTexts[index] as any).inlineEditor as InlineEditor;
return editor.getInlineRange();
}, index);
}
export async function setInlineRichTextRange(
page: Page,
inlineRange: InlineRange,
index = 0
): Promise<void> {
await page.evaluate(
([inlineRange, index]) => {
const richTexts = document
.querySelector('test-page')
?.querySelectorAll('test-rich-text');
if (!richTexts) {
throw new Error('Cannot find test-rich-text');
}
const editor = (richTexts[index as number] as any)
.inlineEditor as InlineEditor;
editor.setInlineRange(inlineRange as InlineRange);
},
[inlineRange, index]
);
}
export async function getInlineRichTextLine(
page: Page,
index: number,
i = 0
): Promise<readonly [string, number]> {
return page.evaluate(
([index, i]) => {
const richTexts = document.querySelectorAll('test-rich-text');
if (!richTexts) {
throw new Error('Cannot find test-rich-text');
}
const editor = (richTexts[i] as any).inlineEditor as InlineEditor;
const result = editor.getLine(index);
if (!result) {
throw new Error('Cannot find line');
}
const { line, rangeIndexRelatedToLine } = result;
return [line.vTextContent, rangeIndexRelatedToLine] as const;
},
[index, i]
);
}
export async function getInlineRangeIndexRect(
page: Page,
[richTextIndex, inlineIndex]: [number, number],
coordOffSet: { x: number; y: number } = { x: 0, y: 0 }
) {
const rect = await page.evaluate(
({ richTextIndex, inlineIndex: vIndex, coordOffSet }) => {
const richText = document.querySelectorAll('test-rich-text')[
richTextIndex
] as any;
const domRange = richText.inlineEditor.toDomRange({
index: vIndex,
length: 0,
});
const pointBound = domRange.getBoundingClientRect();
return {
x: pointBound.left + coordOffSet.x,
y: pointBound.top + pointBound.height / 2 + coordOffSet.y,
};
},
{
richTextIndex,
inlineIndex,
coordOffSet,
}
);
return rect;
}
export async function assertSelection(
page: Page,
richTextIndex: number,
rangeIndex: number,
rangeLength = 0
) {
const actual = await page.evaluate(
([richTextIndex]) => {
const richText =
document?.querySelectorAll('test-rich-text')[richTextIndex];
// @ts-expect-error getInlineRange
const inlineEditor = richText.inlineEditor;
return inlineEditor?.getInlineRange();
},
[richTextIndex]
);
expect(actual).toEqual({ index: rangeIndex, length: rangeLength });
}