1use std::error::Error;
4use std::fmt;
5
6use mathtex_editor_core::{
7 Command, Deco, Dir, FracStyle, InputContext, Mark, MatrixEnv, ScriptSlot, Symbol, UnderOverSpec, Variant,
8};
9
10#[cfg(test)]
11mod fuzz;
12#[cfg(test)]
13mod tests;
14
15#[derive(Debug, Clone)]
17pub struct KeyInput {
18 pub key: String,
20 pub shift: bool,
22 pub ctrl: bool,
24 pub alt: bool,
26 pub meta: bool,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct InvalidWord(pub String);
33
34impl fmt::Display for InvalidWord {
35 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36 write!(f, "{:?} cannot be typed as a word, words are one or more ASCII letters", self.0)
37 }
38}
39
40impl Error for InvalidWord {}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub struct KeymapEntry<'a> {
45 pub word: &'a str,
47 pub label: &'a str,
49}
50
51#[derive(Debug, Clone)]
52struct Override {
53 word: String,
54 label: String,
55 commands: Vec<Command>,
56}
57
58#[derive(Debug, Clone)]
60pub struct Keymap {
61 word: String,
62 last: Option<char>,
63 expect: u64,
65 overrides: Vec<Override>,
66 suffix_matching: bool,
67 autocorrect: bool,
68}
69
70impl Default for Keymap {
71 fn default() -> Self {
72 Self::new()
73 }
74}
75
76impl Keymap {
77 pub fn new() -> Self {
79 Self {
80 word: String::new(),
81 last: None,
82 expect: 0,
83 overrides: Vec::new(),
84 suffix_matching: true,
85 autocorrect: true,
86 }
87 }
88
89 pub fn reset(&mut self) {
91 self.word.clear();
92 self.last = None;
93 }
94
95 pub fn set_suffix_matching(&mut self, on: bool) {
97 self.suffix_matching = on;
98 }
99
100 pub fn set_autocorrect(&mut self, on: bool) {
102 self.autocorrect = on;
103 }
104
105 pub fn define_word(&mut self, word: &str, label: &str, commands: Vec<Command>) -> Result<(), InvalidWord> {
107 if word.is_empty() || !word.bytes().all(|b| b.is_ascii_alphabetic()) {
108 return Err(InvalidWord(word.to_string()));
109 }
110 let entry = Override { word: word.to_string(), label: label.to_string(), commands };
111 match self.overrides.iter_mut().find(|o| o.word == word) {
112 Some(o) => *o = entry,
113 None => self.overrides.push(entry),
114 }
115 Ok(())
116 }
117
118 pub fn undefine_word(&mut self, word: &str) {
120 self.overrides.retain(|o| o.word != word);
121 }
122
123 pub fn entries(&self) -> Vec<KeymapEntry<'_>> {
125 let mut out: Vec<KeymapEntry<'_>> =
126 self.overrides.iter().map(|o| KeymapEntry { word: &o.word, label: &o.label }).collect();
127 for s in CATALOG {
128 if !self.is_overridden(s.word) {
129 out.push(KeymapEntry { word: s.word, label: s.label });
130 }
131 }
132 out
133 }
134
135 pub fn commands_for_word(&self, word: &str) -> Option<Vec<Command>> {
137 if let Some(o) = self.overrides.iter().find(|o| o.word == word) {
138 return Some(o.commands.clone());
139 }
140 CATALOG.iter().find(|s| s.word == word).map(|s| s.insert.commands())
141 }
142
143 pub fn map_key(&mut self, input: &KeyInput, ctx: &InputContext) -> Vec<Command> {
145 self.sync(ctx);
146 let out = self.key(input, ctx);
147 self.expect = ctx.serial + out.len() as u64;
148 out
149 }
150
151 pub fn map_text(&mut self, text: &str, ctx: &InputContext) -> Vec<Command> {
153 self.sync(ctx);
154 let mut cx = Cx::new(ctx);
155 let mut out = Vec::new();
156 for ch in text.chars() {
157 let ch = match ch {
158 '\r' => continue,
159 '\n' | '\t' if cx.literal() => ' ',
160 '\n' | '\t' => {
161 self.reset();
162 continue;
163 }
164 c => c,
165 };
166 out.extend(self.type_char(ch, &mut cx));
167 }
168 self.expect = ctx.serial + out.len() as u64;
169 out
170 }
171
172 fn is_overridden(&self, word: &str) -> bool {
173 self.overrides.iter().any(|o| o.word == word)
174 }
175
176 fn sync(&mut self, ctx: &InputContext) {
178 if ctx.serial != self.expect {
179 self.reset();
180 }
181 }
182
183 fn key(&mut self, input: &KeyInput, ctx: &InputContext) -> Vec<Command> {
184 if let Some(cmd) = named_key(input) {
185 self.reset();
186 return vec![cmd];
187 }
188 let ch = single_char(&input.key).filter(|c| !c.is_control());
189 let altgr = input.ctrl && input.alt && !input.meta && ch.is_some();
191 if (input.ctrl || input.meta) && !altgr {
192 self.reset();
193 return if input.key.eq_ignore_ascii_case("a") { vec![Command::SelectAll] } else { vec![] };
194 }
195 match ch {
196 Some(c) => self.type_char(c, &mut Cx::new(ctx)),
197 None => {
198 if !MODIFIER_KEYS.contains(&input.key.as_str()) {
199 self.reset();
200 }
201 vec![]
202 }
203 }
204 }
205
206 fn type_char(&mut self, ch: char, cx: &mut Cx) -> Vec<Command> {
207 if cx.literal() {
208 self.reset();
209 return vec![Command::InsertText(ch.to_string())];
210 }
211 if ch == ' ' {
212 return self.convert(cx);
213 }
214 let prev = self.last.take();
215 if ch.is_ascii_alphabetic() {
216 self.word.push(ch);
217 } else {
218 self.word.clear();
219 }
220 let mut out = char_commands(ch, cx);
221 match prev.filter(|_| self.autocorrect).and_then(|p| autocorrect(p, ch, cx).map(|with| (p, with))) {
222 Some((p, with)) => out.push(Command::ReplaceTyped { typed: [p, ch].iter().collect(), with }),
223 None => self.last = Some(ch),
224 }
225 out
226 }
227
228 fn convert(&mut self, cx: &mut Cx) -> Vec<Command> {
230 self.last = None;
231 let word = std::mem::take(&mut self.word);
232 let starts = if self.suffix_matching { 0..word.len() } else { 0..word.len().min(1) };
233 for start in starts {
234 let typed = &word[start..];
235 if let Some(with) = self.commands_for_word(typed) {
236 cx.text |= with.contains(&Command::InsertStyled(Variant::Text));
237 return vec![Command::ReplaceTyped { typed: typed.to_string(), with }];
238 }
239 }
240 vec![]
241 }
242}
243
244struct Cx {
246 text: bool,
247 menu: bool,
248 closers: Vec<char>,
249}
250
251impl Cx {
252 fn new(ctx: &InputContext) -> Self {
253 Self { text: ctx.in_text_slot, menu: ctx.menu_open, closers: ctx.closing_delimiter.into_iter().collect() }
254 }
255
256 fn literal(&self) -> bool {
258 self.text || self.menu
259 }
260
261 fn open(&mut self, open: char, close: char) -> Vec<Command> {
262 self.closers.push(close);
263 vec![Command::InsertDelimiters { open, close }]
264 }
265
266 fn closes(&self, close: char) -> bool {
267 self.closers.last() == Some(&close)
268 }
269
270 fn close(&mut self, close: char) -> Command {
271 self.closers.pop();
272 Command::CloseDelimiter(close)
273 }
274}
275
276fn char_commands(ch: char, cx: &mut Cx) -> Vec<Command> {
278 match ch {
279 '/' => vec![Command::InsertFraction(FracStyle::Bar)],
280 '^' => vec![Command::InsertScript(ScriptSlot::Sup)],
281 '_' => vec![Command::InsertScript(ScriptSlot::Sub)],
282 '(' => cx.open('(', ')'),
283 '[' => cx.open('[', ']'),
284 '{' => cx.open('{', '}'),
285 '|' if cx.closes('|') => vec![cx.close('|')],
286 '|' => cx.open('|', '|'),
287 ')' | ']' | '}' if cx.closes(ch) => vec![cx.close(ch)],
288 '\'' => vec![
289 Command::InsertScript(ScriptSlot::Sup),
290 Command::InsertAtom(Symbol::from_latex("\\prime")),
291 Command::Move(Dir::Right),
292 ],
293 '*' => vec![Command::InsertAtom(Symbol::from_latex("\\cdot"))],
294 c => Symbol::from_char(c).map(Command::InsertAtom).into_iter().collect(),
295 }
296}
297
298fn autocorrect(a: char, b: char, cx: &mut Cx) -> Option<Vec<Command>> {
300 let latex = match (a, b) {
301 ('<', '=') => "\\leq",
302 ('>', '=') => "\\geq",
303 ('~', '~') => "\\approx",
304 ('=', '~') => "\\cong",
305 ('-', '>') => "\\to",
306 ('=', '>') => "\\implies",
307 ('+', '-') => "\\pm",
308 ('-', '+') => "\\mp",
309 ('>', '>') if cx.closes('⟩') => return Some(vec![cx.close('⟩')]),
310 ('<', '<') | ('>', '>') => return Some(cx.open('⟨', '⟩')),
311 _ => return None,
312 };
313 Some(vec![Command::InsertAtom(Symbol::from_latex(latex))])
314}
315
316fn named_key(input: &KeyInput) -> Option<Command> {
317 let dir = |d| if input.shift { Command::Extend(d) } else { Command::Move(d) };
318 Some(match input.key.as_str() {
319 "ArrowLeft" => dir(Dir::Left),
320 "ArrowRight" => dir(Dir::Right),
321 "ArrowUp" => dir(Dir::Up),
322 "ArrowDown" => dir(Dir::Down),
323 "Home" => Command::MoveLineStart,
324 "End" => Command::MoveLineEnd,
325 "Tab" if input.shift => Command::ShiftTab,
326 "Tab" => Command::Tab,
327 "Backspace" => Command::DeleteBackward,
328 "Delete" => Command::DeleteForward,
329 "Enter" => Command::Confirm,
330 "Escape" => Command::Collapse,
331 _ => return None,
332 })
333}
334
335const MODIFIER_KEYS: &[&str] = &[
337 "Shift", "Control", "Alt", "AltGraph", "Meta", "CapsLock", "NumLock", "ScrollLock", "Fn", "FnLock", "Hyper",
338 "Super", "Symbol", "SymbolLock", "OS", "Dead", "Compose", "Process", "Unidentified",
339];
340
341fn single_char(key: &str) -> Option<char> {
342 let mut it = key.chars();
343 match (it.next(), it.next()) {
344 (Some(c), None) => Some(c),
345 _ => None,
346 }
347}
348
349#[derive(Debug, Clone, Copy)]
351enum Insertion {
352 Symbol(&'static str),
353 Fraction(FracStyle),
354 Root,
355 BigOperator(&'static str),
356 Matrix(MatrixEnv),
357 Accent(Mark),
358 Styled(Variant),
359 Brace { over: bool },
360}
361
362impl Insertion {
363 fn commands(self) -> Vec<Command> {
364 let cmd = match self {
365 Insertion::Symbol(latex) => Command::InsertAtom(Symbol::from_latex(latex)),
366 Insertion::Fraction(style) => Command::InsertFraction(style),
367 Insertion::Root => Command::InsertSqrt,
368 Insertion::BigOperator(latex) => Command::InsertBigOp(Symbol::from_latex(latex)),
369 Insertion::Matrix(env) => Command::InsertMatrix { env, rows: 2, cols: 2 },
370 Insertion::Accent(mark) => Command::InsertAccent(mark),
371 Insertion::Styled(variant) => Command::InsertStyled(variant),
372 Insertion::Brace { over } => Command::InsertUnderOver(UnderOverSpec {
373 over,
374 under: !over,
375 over_deco: if over { Deco::Brace } else { Deco::None },
376 under_deco: if over { Deco::None } else { Deco::Brace },
377 }),
378 };
379 vec![cmd]
380 }
381}
382
383#[derive(Debug, Clone, Copy)]
384struct Shortcut {
385 word: &'static str,
386 label: &'static str,
387 insert: Insertion,
388}
389
390const fn sym(word: &'static str, latex: &'static str) -> Shortcut {
391 Shortcut { word, label: word, insert: Insertion::Symbol(latex) }
392}
393
394const fn bigop(word: &'static str, latex: &'static str) -> Shortcut {
395 Shortcut { word, label: word, insert: Insertion::BigOperator(latex) }
396}
397
398const fn shortcut(word: &'static str, label: &'static str, insert: Insertion) -> Shortcut {
399 Shortcut { word, label, insert }
400}
401
402const fn accent(word: &'static str, mark: Mark) -> Shortcut {
403 Shortcut { word, label: word, insert: Insertion::Accent(mark) }
404}
405
406const fn styled(word: &'static str, variant: Variant) -> Shortcut {
407 Shortcut { word, label: word, insert: Insertion::Styled(variant) }
408}
409
410const CATALOG: &[Shortcut] = &[
412 shortcut("frac", "fraction", Insertion::Fraction(FracStyle::Bar)),
414 shortcut("dfrac", "display fraction", Insertion::Fraction(FracStyle::Display)),
415 shortcut("tfrac", "text fraction", Insertion::Fraction(FracStyle::Text)),
416 shortcut("binom", "binomial", Insertion::Fraction(FracStyle::Binom)),
417 shortcut("sqrt", "square root", Insertion::Root),
418 shortcut("root", "root", Insertion::Root),
419 shortcut("overbrace", "brace above", Insertion::Brace { over: true }),
420 shortcut("underbrace", "brace below", Insertion::Brace { over: false }),
421 bigop("sum", "\\sum"),
423 bigop("prod", "\\prod"),
424 bigop("coprod", "\\coprod"),
425 bigop("int", "\\int"),
426 bigop("iint", "\\iint"),
427 bigop("iiint", "\\iiint"),
428 bigop("oint", "\\oint"),
429 bigop("bigcup", "\\bigcup"),
430 bigop("bigcap", "\\bigcap"),
431 bigop("bigsqcup", "\\bigsqcup"),
432 bigop("biguplus", "\\biguplus"),
433 bigop("bigoplus", "\\bigoplus"),
434 bigop("bigotimes", "\\bigotimes"),
435 bigop("bigodot", "\\bigodot"),
436 bigop("bigvee", "\\bigvee"),
437 bigop("bigwedge", "\\bigwedge"),
438 shortcut("pmatrix", "matrix in parentheses", Insertion::Matrix(MatrixEnv::Pmatrix)),
440 shortcut("bmatrix", "matrix in brackets", Insertion::Matrix(MatrixEnv::Bmatrix)),
441 shortcut("vmatrix", "determinant", Insertion::Matrix(MatrixEnv::Vmatrix)),
442 shortcut("matrix", "plain matrix", Insertion::Matrix(MatrixEnv::Matrix)),
443 shortcut("cases", "cases", Insertion::Matrix(MatrixEnv::Cases)),
444 shortcut("aligned", "aligned equations", Insertion::Matrix(MatrixEnv::Aligned)),
445 shortcut("align", "aligned equations, short word", Insertion::Matrix(MatrixEnv::Aligned)),
446 shortcut("array", "array", Insertion::Matrix(MatrixEnv::Array)),
447 accent("hat", Mark::Hat),
449 accent("vec", Mark::Vec),
450 accent("bar", Mark::Bar),
451 accent("tilde", Mark::Tilde),
452 accent("dot", Mark::Dot),
453 accent("ddot", Mark::Ddot),
454 accent("widehat", Mark::Widehat),
455 accent("widetilde", Mark::Widetilde),
456 accent("overline", Mark::Overline),
457 accent("underline", Mark::Underline),
458 accent("check", Mark::Check),
459 accent("breve", Mark::Breve),
460 styled("bold", Variant::Bold),
462 styled("bb", Variant::Blackboard),
463 styled("mathbb", Variant::Blackboard),
464 styled("mathcal", Variant::Calligraphic),
465 styled("mathfrak", Variant::Fraktur),
466 styled("mathrm", Variant::Roman),
467 styled("mathsf", Variant::SansSerif),
468 styled("mathtt", Variant::Typewriter),
469 styled("text", Variant::Text),
470 styled("op", Variant::OperatorName),
471 styled("operatorname", Variant::OperatorName),
472 sym("alpha", "\\alpha"),
474 sym("beta", "\\beta"),
475 sym("gamma", "\\gamma"),
476 sym("delta", "\\delta"),
477 sym("epsilon", "\\epsilon"),
478 sym("varepsilon", "\\varepsilon"),
479 sym("zeta", "\\zeta"),
480 sym("eta", "\\eta"),
481 sym("theta", "\\theta"),
482 sym("vartheta", "\\vartheta"),
483 sym("iota", "\\iota"),
484 sym("kappa", "\\kappa"),
485 sym("lambda", "\\lambda"),
486 sym("mu", "\\mu"),
487 sym("nu", "\\nu"),
488 sym("xi", "\\xi"),
489 sym("pi", "\\pi"),
490 sym("varpi", "\\varpi"),
491 sym("rho", "\\rho"),
492 sym("varrho", "\\varrho"),
493 sym("sigma", "\\sigma"),
494 sym("varsigma", "\\varsigma"),
495 sym("tau", "\\tau"),
496 sym("upsilon", "\\upsilon"),
497 sym("phi", "\\phi"),
498 sym("varphi", "\\varphi"),
499 sym("chi", "\\chi"),
500 sym("psi", "\\psi"),
501 sym("omega", "\\omega"),
502 sym("Gamma", "\\Gamma"),
504 sym("Delta", "\\Delta"),
505 sym("Theta", "\\Theta"),
506 sym("Lambda", "\\Lambda"),
507 sym("Xi", "\\Xi"),
508 sym("Pi", "\\Pi"),
509 sym("Sigma", "\\Sigma"),
510 sym("Upsilon", "\\Upsilon"),
511 sym("Phi", "\\Phi"),
512 sym("Psi", "\\Psi"),
513 sym("Omega", "\\Omega"),
514 sym("infty", "\\infty"),
516 sym("infinity", "\\infty"),
517 sym("partial", "\\partial"),
518 sym("nabla", "\\nabla"),
519 sym("ell", "\\ell"),
520 sym("hbar", "\\hbar"),
521 shortcut("dd", "differential", Insertion::Symbol("\\mathrm{d}")),
522 sym("pm", "\\pm"),
524 sym("mp", "\\mp"),
525 sym("times", "\\times"),
526 sym("div", "\\div"),
527 sym("cdot", "\\cdot"),
528 sym("ast", "\\ast"),
529 sym("star", "\\star"),
530 sym("circ", "\\circ"),
531 sym("oplus", "\\oplus"),
532 sym("otimes", "\\otimes"),
533 sym("setminus", "\\setminus"),
534 sym("cup", "\\cup"),
535 sym("cap", "\\cap"),
536 sym("wedge", "\\wedge"),
537 sym("vee", "\\vee"),
538 sym("land", "\\land"),
539 sym("lor", "\\lor"),
540 sym("leq", "\\leq"),
542 sym("le", "\\leq"),
543 sym("geq", "\\geq"),
544 sym("ge", "\\geq"),
545 sym("neq", "\\neq"),
546 sym("ne", "\\neq"),
547 sym("ll", "\\ll"),
548 sym("gg", "\\gg"),
549 sym("approx", "\\approx"),
550 sym("equiv", "\\equiv"),
551 sym("cong", "\\cong"),
552 sym("sim", "\\sim"),
553 sym("propto", "\\propto"),
554 sym("mid", "\\mid"),
555 sym("perp", "\\perp"),
556 sym("parallel", "\\parallel"),
557 sym("in", "\\in"),
558 sym("notin", "\\notin"),
559 sym("ni", "\\ni"),
560 sym("subset", "\\subset"),
561 sym("subseteq", "\\subseteq"),
562 sym("supset", "\\supset"),
563 sym("supseteq", "\\supseteq"),
564 sym("to", "\\to"),
566 sym("gets", "\\gets"),
567 sym("mapsto", "\\mapsto"),
568 sym("rightarrow", "\\rightarrow"),
569 sym("leftarrow", "\\leftarrow"),
570 sym("Rightarrow", "\\Rightarrow"),
571 sym("Leftarrow", "\\Leftarrow"),
572 sym("Leftrightarrow", "\\Leftrightarrow"),
573 sym("Longrightarrow", "\\Longrightarrow"),
574 sym("implies", "\\implies"),
575 sym("iff", "\\iff"),
576 sym("emptyset", "\\emptyset"),
578 sym("forall", "\\forall"),
579 sym("exists", "\\exists"),
580 sym("neg", "\\neg"),
581 sym("cdots", "\\cdots"),
583 sym("ldots", "\\ldots"),
584 sym("dots", "\\dots"),
585 sym("vdots", "\\vdots"),
586 sym("ddots", "\\ddots"),
587 sym("angle", "\\angle"),
588 sym("sin", "\\sin"),
590 sym("cos", "\\cos"),
591 sym("tan", "\\tan"),
592 sym("cot", "\\cot"),
593 sym("sec", "\\sec"),
594 sym("csc", "\\csc"),
595 sym("sinh", "\\sinh"),
596 sym("cosh", "\\cosh"),
597 sym("tanh", "\\tanh"),
598 sym("arcsin", "\\arcsin"),
599 sym("arccos", "\\arccos"),
600 sym("arctan", "\\arctan"),
601 sym("log", "\\log"),
602 sym("ln", "\\ln"),
603 sym("exp", "\\exp"),
604 sym("lim", "\\lim"),
605 sym("max", "\\max"),
606 sym("min", "\\min"),
607 sym("sup", "\\sup"),
608 sym("inf", "\\inf"),
609 sym("gcd", "\\gcd"),
610 sym("det", "\\det"),
611 sym("dim", "\\dim"),
612 sym("ker", "\\ker"),
613 sym("arg", "\\arg"),
614 sym("deg", "\\deg"),
615 sym("hom", "\\hom"),
616];