feat(editor): eslint/tsconfig migration + type fixes

@robonen/editor: migrate to eslint flat config + composite tsconfig; fix
convergence test type annotations.
This commit is contained in:
2026-06-07 16:30:05 +07:00
parent 626fbc70d8
commit 09272dffeb
136 changed files with 7248 additions and 0 deletions
@@ -0,0 +1,104 @@
import { describe, expect, it } from 'vitest';
import { caret, createDoc, createNode, nodeInline, nodeText, textSelection } from '../../model';
import { applyInputRule, joinBackward, splitBlock, toggleMark } from '../../commands';
import { createDefaultRegistry } from '../../preset';
import { createEditor, createEditorState } from '../../state';
function editorWith(node: ReturnType<typeof createNode>, selection?: ReturnType<typeof caret>) {
const registry = createDefaultRegistry();
return createEditor({ state: createEditorState({ registry, doc: createDoc([node]), selection }) });
}
describe('default registry', () => {
it('registers the full block and mark set', () => {
const registry = createDefaultRegistry();
for (const type of ['paragraph', 'heading', 'blockquote', 'code-block', 'callout', 'bulleted-list', 'numbered-list', 'todo-list', 'divider', 'image'])
expect(registry.hasBlock(type)).toBe(true);
for (const mark of ['bold', 'italic', 'underline', 'strike', 'highlight', 'code', 'link'])
expect(registry.hasMark(mark)).toBe(true);
});
it('marks the list blocks as group "list" and gives to-do a checked attr', () => {
const registry = createDefaultRegistry();
expect(registry.getBlock('bulleted-list')?.spec.group).toBe('list');
expect(registry.getBlock('todo-list')?.spec.attrs?.['checked']).toBeDefined();
});
it('inline code mark excludes all others', () => {
expect(createDefaultRegistry().getMark('code')?.spec.excludes).toBe('_all');
});
});
describe('code block', () => {
it('Enter inserts a newline instead of splitting', () => {
const editor = editorWith(createNode('code-block', { id: 'c', content: [{ text: 'ab', marks: [] }] }), caret('c', 2));
expect(editor.command(splitBlock)).toBe(true);
expect(editor.state.doc.content.length).toBe(1);
expect(nodeText(editor.state.doc.content[0]!)).toBe('ab\n');
});
it('disallows inline marks', () => {
const editor = editorWith(
createNode('code-block', { id: 'c', content: [{ text: 'abc', marks: [] }] }),
textSelection({ blockId: 'c', offset: 0 }, { blockId: 'c', offset: 3 }),
);
expect(editor.command(toggleMark('bold'))).toBe(false);
});
it('does not absorb disallowed marks when another block merges into it', () => {
const registry = createDefaultRegistry();
const editor = createEditor({
state: createEditorState({
registry,
doc: createDoc([
createNode('code-block', { id: 'c', content: [{ text: 'x', marks: [] }] }),
createNode('paragraph', { id: 'p', content: [{ text: 'B', marks: [{ type: 'bold' }] }] }),
]),
selection: caret('p', 0),
}),
});
expect(editor.command(joinBackward)).toBe(true);
const merged = editor.state.doc.content[0]!;
expect(merged.type).toBe('code-block');
expect(nodeText(merged)).toBe('xB');
expect(nodeInline(merged).every(run => run.marks.length === 0)).toBe(true);
});
});
describe('input rules', () => {
it('"# " converts a paragraph to a level-1 heading and strips the marker', () => {
const editor = editorWith(createNode('paragraph', { id: 'p', content: [{ text: '# ', marks: [] }] }), caret('p', 2));
expect(editor.command(applyInputRule)).toBe(true);
const block = editor.state.doc.content[0]!;
expect(block.type).toBe('heading');
expect(block.attrs['level']).toBe(1);
expect(nodeText(block)).toBe('');
});
it('"- " converts a paragraph to a bulleted list', () => {
const editor = editorWith(createNode('paragraph', { id: 'p', content: [{ text: '- ', marks: [] }] }), caret('p', 2));
expect(editor.command(applyInputRule)).toBe(true);
expect(editor.state.doc.content[0]!.type).toBe('bulleted-list');
});
it('does not re-fire when the block is already the target type', () => {
const editor = editorWith(createNode('blockquote', { id: 'q', content: [{ text: '> ', marks: [] }] }), caret('q', 2));
expect(editor.command(applyInputRule)).toBe(false);
});
});
describe('to-do list', () => {
it('starts a new item unchecked when splitting a checked one', () => {
const editor = editorWith(
createNode('todo-list', { id: 't', attrs: { checked: true, indent: 0 }, content: [{ text: 'done', marks: [] }] }),
caret('t', 4),
);
expect(editor.command(splitBlock)).toBe(true);
expect(editor.state.doc.content.length).toBe(2);
const created = editor.state.doc.content[1]!;
expect(created.type).toBe('todo-list');
expect(created.attrs['checked']).toBe(false);
});
});