Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | 1x 1x 1x 5x 5x 5x 5x 6x 6x 5x 5x 15x 5x 17x 17x 16x 16x 1x 1x 15x 15x 15x 15x 5x 5x 5x 5x 5x 5x 1x 15x 15x 15x 5x 15x 1x | import {Editor, Range, Transforms} from 'slate';
import {Matcher} from '../Matcher';
import type {TranslitService} from '../TranslitService';
/**
* Slate plugin: while the service has an active scheme, re-route `insertText`
* through the matcher. Implementation notes:
*
* - The matcher state is recreated whenever the active scheme changes.
* - Range selections, paste, and IME composition all bypass the matcher.
* - The plugin does NOT gate by block type (e.g. `code-block`); the host
* (mutxt) wires that gate in via `shouldRun`. This keeps the binding
* reusable with any Slate editor.
*
* The plugin returns the patched editor.
*/
export interface WithTranslitOpts {
/**
* Optional gate. When provided and returns `false`, the matcher is bypassed
* for this `insertText` call (e.g. inside code blocks). Defaults to `true`.
*/
shouldRun?: (editor: Editor) => boolean;
}
export const withTranslit = <T extends Editor>(editor: T, service: TranslitService, opts: WithTranslitOpts = {}): T => {
const {insertText, deleteBackward, insertBreak} = editor;
let matcher: Matcher | null = null;
let composing = false;
const refresh = (): void => {
const compiled = service.activeScheme();
matcher = compiled ? new Matcher(compiled) : null;
};
refresh();
service.active.subscribe(refresh);
const shouldRun = opts.shouldRun ?? (() => true);
editor.insertText = (text) => {
const m = matcher;
if (!m || composing) return insertText(text);
const sel = editor.selection;
if (!sel || !Range.isCollapsed(sel)) {
m.reset();
return insertText(text);
}
Iif (!shouldRun(editor)) return insertText(text);
Iif (text.length !== 1) {
const flushed = m.flushAndPassthrough(text);
applyStep(editor, flushed.replaceTail, flushed.emit);
return;
}
const step = m.feed(text);
applyStep(editor, step.replaceTail, step.emit);
};
editor.deleteBackward = (unit) => {
const m = matcher;
if (m && m.buffer.length > 0) {
// Mid-buffer: drop a buffer char and let the user proceed; do not
// delete from the document. Keeps Backspace-during-digraph predictable.
m.buffer = m.buffer.slice(0, -1);
m.lastEmitLen = 0;
return;
}
if (m) m.reset();
deleteBackward(unit);
};
editor.insertBreak = () => {
const m = matcher;
if (m) {
const flushed = m.flushBuffer();
applyStep(editor, flushed.replaceTail, flushed.emit);
applyFinalForm(editor, m);
m.reset();
}
insertBreak();
};
// Caller wires composition events to these via the host's editable element.
(editor as any).translitOnCompositionStart = () => {
composing = true;
matcher?.reset();
};
(editor as any).translitOnCompositionEnd = () => {
composing = false;
};
(editor as any).translitMatcher = () => matcher;
return editor;
};
const applyStep = (editor: Editor, replaceTail: number, emit: string): void => {
Iif (!replaceTail && !emit) return;
Editor.withoutNormalizing(editor, () => {
if (replaceTail > 0) {
for (let i = 0; i < replaceTail; i++) Transforms.delete(editor, {distance: 1, reverse: true, unit: 'character'});
}
Eif (emit) Transforms.insertText(editor, emit);
});
};
const applyFinalForm = (editor: Editor, m: Matcher): void => {
// Look at the document at the caret; if the previous char's glyph has a
// final-form replacement, swap it in.
const sel = editor.selection;
if (!sel || !Range.isCollapsed(sel)) return;
try {
const before = Editor.before(editor, sel.anchor, {unit: 'character'});
if (!before) return;
const range = {anchor: before, focus: sel.anchor};
const text = Editor.string(editor, range);
const replacement = m.scheme.applyFinalForm(text);
if (!replacement) return;
Editor.withoutNormalizing(editor, () => {
Transforms.delete(editor, {at: range});
Transforms.insertText(editor, replacement.replacement);
});
} catch {}
};
|