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
+22
View File
@@ -0,0 +1,22 @@
import type { Command } from '../state';
import type { Platform } from '../view/config';
import type { Keymap } from './types';
import { normalizeCombo } from './normalize';
/**
* Merge ordered keymaps into a single normalized lookup. Earlier keymaps win, so
* pass user overrides before the defaults: `compileKeymaps([user, defaults], …)`.
*/
export function compileKeymaps(keymaps: readonly Keymap[], platform: Platform): Map<string, Command> {
const compiled = new Map<string, Command>();
for (const keymap of keymaps) {
for (const combo in keymap) {
const normalized = normalizeCombo(combo, platform);
if (!compiled.has(normalized))
compiled.set(normalized, keymap[combo]!);
}
}
return compiled;
}
+55
View File
@@ -0,0 +1,55 @@
import {
chainCommands,
deleteSelection,
indentListItem,
insertHardBreak,
joinBackward,
joinForward,
moveBlockDown,
moveBlockUp,
outdentListItem,
selectAll,
setBlockType,
splitBlock,
toggleBlockType,
toggleMark,
} from '../commands';
import type { Command, Editor } from '../state';
import type { Keymap } from './types';
/**
* The standard editor keymap. Mark/heading shortcuts are no-ops when the mark or
* block type isn't registered. Enter/Backspace/Delete are no-ops except at block
* boundaries, so ordinary intra-block editing stays native. Arrow navigation and
* cross-block selection are fully native (one contenteditable spans the doc).
*/
export function defaultKeymap(editor: Editor): Keymap {
const undo: Command = () => editor.undo();
const redo: Command = () => editor.redo();
const keymap: Keymap = {
'Mod-b': toggleMark('bold'),
'Mod-i': toggleMark('italic'),
'Mod-u': toggleMark('underline'),
'Mod-Shift-s': toggleMark('strike'),
'Mod-e': toggleMark('code'),
'Mod-z': undo,
'Mod-Shift-z': redo,
'Mod-y': redo,
Enter: splitBlock,
'Shift-Enter': insertHardBreak,
Backspace: chainCommands(deleteSelection, joinBackward),
Delete: chainCommands(deleteSelection, joinForward),
'Mod-a': selectAll,
Tab: indentListItem,
'Shift-Tab': outdentListItem,
'Mod-Shift-ArrowUp': moveBlockUp,
'Mod-Shift-ArrowDown': moveBlockDown,
'Mod-Alt-0': setBlockType('paragraph'),
};
for (let level = 1; level <= 6; level++)
keymap[`Mod-Alt-${level}`] = toggleBlockType('heading', { level });
return keymap;
}
+17
View File
@@ -0,0 +1,17 @@
import type { Command, CommandView, Dispatch, EditorState } from '../state';
import { eventToCombo } from './normalize';
/**
* Look up and run the command bound to a keydown event. Returns `true` when a
* command handled it (the caller should then `preventDefault`).
*/
export function runKeydown(
event: KeyboardEvent,
compiled: Map<string, Command>,
state: EditorState,
dispatch: Dispatch,
view: CommandView,
): boolean {
const command = compiled.get(eventToCombo(event));
return command ? command(state, dispatch, view) : false;
}
+5
View File
@@ -0,0 +1,5 @@
export * from './types';
export * from './normalize';
export * from './compile';
export * from './defaults';
export * from './dispatcher';
+52
View File
@@ -0,0 +1,52 @@
import type { Platform } from '../view/config';
const MOD_ORDER = ['Ctrl', 'Alt', 'Shift', 'Meta'] as const;
function modAlias(token: string, platform: Platform): string | null {
switch (token.toLowerCase()) {
case 'mod': return platform === 'mac' ? 'Meta' : 'Ctrl';
case 'cmd':
case 'command':
case 'meta': return 'Meta';
case 'ctrl':
case 'control': return 'Ctrl';
case 'alt':
case 'option': return 'Alt';
case 'shift': return 'Shift';
default: return null;
}
}
/**
* Normalize a human combo (`'Mod-Shift-z'`) to a canonical, platform-resolved
* form (`'Shift-Meta-z'` on mac). Modifiers are ordered deterministically so a
* keydown event maps to the same string via {@link eventToCombo}.
*/
export function normalizeCombo(combo: string, platform: Platform): string {
const parts = combo.split(/[-+]/).map(part => part.trim()).filter(Boolean);
const key = parts.pop() ?? '';
const mods = new Set<string>();
for (const part of parts) {
const mod = modAlias(part, platform);
if (mod)
mods.add(mod);
}
const order = MOD_ORDER.filter(mod => mods.has(mod));
const normalizedKey = key.length === 1 ? key.toLowerCase() : key;
return [...order, normalizedKey].join('-');
}
/** Canonical combo string for a keydown event (matches {@link normalizeCombo}). */
export function eventToCombo(event: KeyboardEvent): string {
const mods: string[] = [];
if (event.ctrlKey) mods.push('Ctrl');
if (event.altKey) mods.push('Alt');
if (event.shiftKey) mods.push('Shift');
if (event.metaKey) mods.push('Meta');
const key = event.key.length === 1 ? event.key.toLowerCase() : event.key;
return [...mods, key].join('-');
}
+4
View File
@@ -0,0 +1,4 @@
import type { Command } from '../state';
/** A keymap: normalized (or human) key-combos mapped to commands. */
export type Keymap = Record<string, Command>;