use std::error::Error;
use std::fmt;
use mathtex_editor_core::{
Command, Deco, Dir, FracStyle, InputContext, Mark, MatrixEnv, ScriptSlot, Symbol, UnderOverSpec, Variant,
};
#[cfg(test)]
mod fuzz;
#[cfg(test)]
mod tests;
#[derive(Debug, Clone)]
pub struct KeyInput {
pub key: String,
pub shift: bool,
pub ctrl: bool,
pub alt: bool,
pub meta: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InvalidWord(pub String);
impl fmt::Display for InvalidWord {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?} cannot be typed as a word, words are one or more ASCII letters", self.0)
}
}
impl Error for InvalidWord {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct KeymapEntry<'a> {
pub word: &'a str,
pub label: &'a str,
}
#[derive(Debug, Clone)]
struct Override {
word: String,
label: String,
commands: Vec<Command>,
}
#[derive(Debug, Clone)]
pub struct Keymap {
word: String,
last: Option<char>,
expect: u64,
overrides: Vec<Override>,
suffix_matching: bool,
autocorrect: bool,
}
impl Default for Keymap {
fn default() -> Self {
Self::new()
}
}
impl Keymap {
pub fn new() -> Self {
Self {
word: String::new(),
last: None,
expect: 0,
overrides: Vec::new(),
suffix_matching: true,
autocorrect: true,
}
}
pub fn reset(&mut self) {
self.word.clear();
self.last = None;
}
pub fn set_suffix_matching(&mut self, on: bool) {
self.suffix_matching = on;
}
pub fn set_autocorrect(&mut self, on: bool) {
self.autocorrect = on;
}
pub fn define_word(&mut self, word: &str, label: &str, commands: Vec<Command>) -> Result<(), InvalidWord> {
if word.is_empty() || !word.bytes().all(|b| b.is_ascii_alphabetic()) {
return Err(InvalidWord(word.to_string()));
}
let entry = Override { word: word.to_string(), label: label.to_string(), commands };
match self.overrides.iter_mut().find(|o| o.word == word) {
Some(o) => *o = entry,
None => self.overrides.push(entry),
}
Ok(())
}
pub fn undefine_word(&mut self, word: &str) {
self.overrides.retain(|o| o.word != word);
}
pub fn entries(&self) -> Vec<KeymapEntry<'_>> {
let mut out: Vec<KeymapEntry<'_>> =
self.overrides.iter().map(|o| KeymapEntry { word: &o.word, label: &o.label }).collect();
for s in CATALOG {
if !self.is_overridden(s.word) {
out.push(KeymapEntry { word: s.word, label: s.label });
}
}
out
}
pub fn commands_for_word(&self, word: &str) -> Option<Vec<Command>> {
if let Some(o) = self.overrides.iter().find(|o| o.word == word) {
return Some(o.commands.clone());
}
CATALOG.iter().find(|s| s.word == word).map(|s| s.insert.commands())
}
pub fn map_key(&mut self, input: &KeyInput, ctx: &InputContext) -> Vec<Command> {
self.sync(ctx);
let out = self.key(input, ctx);
self.expect = ctx.serial + out.len() as u64;
out
}
pub fn map_text(&mut self, text: &str, ctx: &InputContext) -> Vec<Command> {
self.sync(ctx);
let mut cx = Cx::new(ctx);
let mut out = Vec::new();
for ch in text.chars() {
let ch = match ch {
'\r' => continue,
'\n' | '\t' if cx.literal() => ' ',
'\n' | '\t' => {
self.reset();
continue;
}
c => c,
};
out.extend(self.type_char(ch, &mut cx));
}
self.expect = ctx.serial + out.len() as u64;
out
}
fn is_overridden(&self, word: &str) -> bool {
self.overrides.iter().any(|o| o.word == word)
}
fn sync(&mut self, ctx: &InputContext) {
if ctx.serial != self.expect {
self.reset();
}
}
fn key(&mut self, input: &KeyInput, ctx: &InputContext) -> Vec<Command> {
if let Some(cmd) = named_key(input) {
self.reset();
return vec![cmd];
}
let ch = single_char(&input.key).filter(|c| !c.is_control());
let altgr = input.ctrl && input.alt && !input.meta && ch.is_some();
if (input.ctrl || input.meta) && !altgr {
self.reset();
return if input.key.eq_ignore_ascii_case("a") { vec![Command::SelectAll] } else { vec![] };
}
match ch {
Some(c) => self.type_char(c, &mut Cx::new(ctx)),
None => {
if !MODIFIER_KEYS.contains(&input.key.as_str()) {
self.reset();
}
vec![]
}
}
}
fn type_char(&mut self, ch: char, cx: &mut Cx) -> Vec<Command> {
if cx.literal() {
self.reset();
return vec![Command::InsertText(ch.to_string())];
}
if ch == ' ' {
return self.convert(cx);
}
let prev = self.last.take();
if ch.is_ascii_alphabetic() {
self.word.push(ch);
} else {
self.word.clear();
}
let mut out = char_commands(ch, cx);
match prev.filter(|_| self.autocorrect).and_then(|p| autocorrect(p, ch, cx).map(|with| (p, with))) {
Some((p, with)) => out.push(Command::ReplaceTyped { typed: [p, ch].iter().collect(), with }),
None => self.last = Some(ch),
}
out
}
fn convert(&mut self, cx: &mut Cx) -> Vec<Command> {
self.last = None;
let word = std::mem::take(&mut self.word);
let starts = if self.suffix_matching { 0..word.len() } else { 0..word.len().min(1) };
for start in starts {
let typed = &word[start..];
if let Some(with) = self.commands_for_word(typed) {
cx.text |= with.contains(&Command::InsertStyled(Variant::Text));
return vec![Command::ReplaceTyped { typed: typed.to_string(), with }];
}
}
vec![]
}
}
struct Cx {
text: bool,
menu: bool,
closers: Vec<char>,
}
impl Cx {
fn new(ctx: &InputContext) -> Self {
Self { text: ctx.in_text_slot, menu: ctx.menu_open, closers: ctx.closing_delimiter.into_iter().collect() }
}
fn literal(&self) -> bool {
self.text || self.menu
}
fn open(&mut self, open: char, close: char) -> Vec<Command> {
self.closers.push(close);
vec![Command::InsertDelimiters { open, close }]
}
fn closes(&self, close: char) -> bool {
self.closers.last() == Some(&close)
}
fn close(&mut self, close: char) -> Command {
self.closers.pop();
Command::CloseDelimiter(close)
}
}
fn char_commands(ch: char, cx: &mut Cx) -> Vec<Command> {
match ch {
'/' => vec![Command::InsertFraction(FracStyle::Bar)],
'^' => vec![Command::InsertScript(ScriptSlot::Sup)],
'_' => vec![Command::InsertScript(ScriptSlot::Sub)],
'(' => cx.open('(', ')'),
'[' => cx.open('[', ']'),
'{' => cx.open('{', '}'),
'|' if cx.closes('|') => vec![cx.close('|')],
'|' => cx.open('|', '|'),
')' | ']' | '}' if cx.closes(ch) => vec![cx.close(ch)],
'\'' => vec![
Command::InsertScript(ScriptSlot::Sup),
Command::InsertAtom(Symbol::from_latex("\\prime")),
Command::Move(Dir::Right),
],
'*' => vec![Command::InsertAtom(Symbol::from_latex("\\cdot"))],
c => Symbol::from_char(c).map(Command::InsertAtom).into_iter().collect(),
}
}
fn autocorrect(a: char, b: char, cx: &mut Cx) -> Option<Vec<Command>> {
let latex = match (a, b) {
('<', '=') => "\\leq",
('>', '=') => "\\geq",
('~', '~') => "\\approx",
('=', '~') => "\\cong",
('-', '>') => "\\to",
('=', '>') => "\\implies",
('+', '-') => "\\pm",
('-', '+') => "\\mp",
('>', '>') if cx.closes('⟩') => return Some(vec![cx.close('⟩')]),
('<', '<') | ('>', '>') => return Some(cx.open('⟨', '⟩')),
_ => return None,
};
Some(vec![Command::InsertAtom(Symbol::from_latex(latex))])
}
fn named_key(input: &KeyInput) -> Option<Command> {
let dir = |d| if input.shift { Command::Extend(d) } else { Command::Move(d) };
Some(match input.key.as_str() {
"ArrowLeft" => dir(Dir::Left),
"ArrowRight" => dir(Dir::Right),
"ArrowUp" => dir(Dir::Up),
"ArrowDown" => dir(Dir::Down),
"Home" => Command::MoveLineStart,
"End" => Command::MoveLineEnd,
"Tab" if input.shift => Command::ShiftTab,
"Tab" => Command::Tab,
"Backspace" => Command::DeleteBackward,
"Delete" => Command::DeleteForward,
"Enter" => Command::Confirm,
"Escape" => Command::Collapse,
_ => return None,
})
}
const MODIFIER_KEYS: &[&str] = &[
"Shift", "Control", "Alt", "AltGraph", "Meta", "CapsLock", "NumLock", "ScrollLock", "Fn", "FnLock", "Hyper",
"Super", "Symbol", "SymbolLock", "OS", "Dead", "Compose", "Process", "Unidentified",
];
fn single_char(key: &str) -> Option<char> {
let mut it = key.chars();
match (it.next(), it.next()) {
(Some(c), None) => Some(c),
_ => None,
}
}
#[derive(Debug, Clone, Copy)]
enum Insertion {
Symbol(&'static str),
Fraction(FracStyle),
Root,
BigOperator(&'static str),
Matrix(MatrixEnv),
Accent(Mark),
Styled(Variant),
Brace { over: bool },
}
impl Insertion {
fn commands(self) -> Vec<Command> {
let cmd = match self {
Insertion::Symbol(latex) => Command::InsertAtom(Symbol::from_latex(latex)),
Insertion::Fraction(style) => Command::InsertFraction(style),
Insertion::Root => Command::InsertSqrt,
Insertion::BigOperator(latex) => Command::InsertBigOp(Symbol::from_latex(latex)),
Insertion::Matrix(env) => Command::InsertMatrix { env, rows: 2, cols: 2 },
Insertion::Accent(mark) => Command::InsertAccent(mark),
Insertion::Styled(variant) => Command::InsertStyled(variant),
Insertion::Brace { over } => Command::InsertUnderOver(UnderOverSpec {
over,
under: !over,
over_deco: if over { Deco::Brace } else { Deco::None },
under_deco: if over { Deco::None } else { Deco::Brace },
}),
};
vec![cmd]
}
}
#[derive(Debug, Clone, Copy)]
struct Shortcut {
word: &'static str,
label: &'static str,
insert: Insertion,
}
const fn sym(word: &'static str, latex: &'static str) -> Shortcut {
Shortcut { word, label: word, insert: Insertion::Symbol(latex) }
}
const fn bigop(word: &'static str, latex: &'static str) -> Shortcut {
Shortcut { word, label: word, insert: Insertion::BigOperator(latex) }
}
const fn shortcut(word: &'static str, label: &'static str, insert: Insertion) -> Shortcut {
Shortcut { word, label, insert }
}
const fn accent(word: &'static str, mark: Mark) -> Shortcut {
Shortcut { word, label: word, insert: Insertion::Accent(mark) }
}
const fn styled(word: &'static str, variant: Variant) -> Shortcut {
Shortcut { word, label: word, insert: Insertion::Styled(variant) }
}
const CATALOG: &[Shortcut] = &[
shortcut("frac", "fraction", Insertion::Fraction(FracStyle::Bar)),
shortcut("dfrac", "display fraction", Insertion::Fraction(FracStyle::Display)),
shortcut("tfrac", "text fraction", Insertion::Fraction(FracStyle::Text)),
shortcut("binom", "binomial", Insertion::Fraction(FracStyle::Binom)),
shortcut("sqrt", "square root", Insertion::Root),
shortcut("root", "root", Insertion::Root),
shortcut("overbrace", "brace above", Insertion::Brace { over: true }),
shortcut("underbrace", "brace below", Insertion::Brace { over: false }),
bigop("sum", "\\sum"),
bigop("prod", "\\prod"),
bigop("coprod", "\\coprod"),
bigop("int", "\\int"),
bigop("iint", "\\iint"),
bigop("iiint", "\\iiint"),
bigop("oint", "\\oint"),
bigop("bigcup", "\\bigcup"),
bigop("bigcap", "\\bigcap"),
bigop("bigsqcup", "\\bigsqcup"),
bigop("biguplus", "\\biguplus"),
bigop("bigoplus", "\\bigoplus"),
bigop("bigotimes", "\\bigotimes"),
bigop("bigodot", "\\bigodot"),
bigop("bigvee", "\\bigvee"),
bigop("bigwedge", "\\bigwedge"),
shortcut("pmatrix", "matrix in parentheses", Insertion::Matrix(MatrixEnv::Pmatrix)),
shortcut("bmatrix", "matrix in brackets", Insertion::Matrix(MatrixEnv::Bmatrix)),
shortcut("vmatrix", "determinant", Insertion::Matrix(MatrixEnv::Vmatrix)),
shortcut("matrix", "plain matrix", Insertion::Matrix(MatrixEnv::Matrix)),
shortcut("cases", "cases", Insertion::Matrix(MatrixEnv::Cases)),
shortcut("aligned", "aligned equations", Insertion::Matrix(MatrixEnv::Aligned)),
shortcut("align", "aligned equations, short word", Insertion::Matrix(MatrixEnv::Aligned)),
shortcut("array", "array", Insertion::Matrix(MatrixEnv::Array)),
accent("hat", Mark::Hat),
accent("vec", Mark::Vec),
accent("bar", Mark::Bar),
accent("tilde", Mark::Tilde),
accent("dot", Mark::Dot),
accent("ddot", Mark::Ddot),
accent("widehat", Mark::Widehat),
accent("widetilde", Mark::Widetilde),
accent("overline", Mark::Overline),
accent("underline", Mark::Underline),
accent("check", Mark::Check),
accent("breve", Mark::Breve),
styled("bold", Variant::Bold),
styled("bb", Variant::Blackboard),
styled("mathbb", Variant::Blackboard),
styled("mathcal", Variant::Calligraphic),
styled("mathfrak", Variant::Fraktur),
styled("mathrm", Variant::Roman),
styled("mathsf", Variant::SansSerif),
styled("mathtt", Variant::Typewriter),
styled("text", Variant::Text),
styled("op", Variant::OperatorName),
styled("operatorname", Variant::OperatorName),
sym("alpha", "\\alpha"),
sym("beta", "\\beta"),
sym("gamma", "\\gamma"),
sym("delta", "\\delta"),
sym("epsilon", "\\epsilon"),
sym("varepsilon", "\\varepsilon"),
sym("zeta", "\\zeta"),
sym("eta", "\\eta"),
sym("theta", "\\theta"),
sym("vartheta", "\\vartheta"),
sym("iota", "\\iota"),
sym("kappa", "\\kappa"),
sym("lambda", "\\lambda"),
sym("mu", "\\mu"),
sym("nu", "\\nu"),
sym("xi", "\\xi"),
sym("pi", "\\pi"),
sym("varpi", "\\varpi"),
sym("rho", "\\rho"),
sym("varrho", "\\varrho"),
sym("sigma", "\\sigma"),
sym("varsigma", "\\varsigma"),
sym("tau", "\\tau"),
sym("upsilon", "\\upsilon"),
sym("phi", "\\phi"),
sym("varphi", "\\varphi"),
sym("chi", "\\chi"),
sym("psi", "\\psi"),
sym("omega", "\\omega"),
sym("Gamma", "\\Gamma"),
sym("Delta", "\\Delta"),
sym("Theta", "\\Theta"),
sym("Lambda", "\\Lambda"),
sym("Xi", "\\Xi"),
sym("Pi", "\\Pi"),
sym("Sigma", "\\Sigma"),
sym("Upsilon", "\\Upsilon"),
sym("Phi", "\\Phi"),
sym("Psi", "\\Psi"),
sym("Omega", "\\Omega"),
sym("infty", "\\infty"),
sym("infinity", "\\infty"),
sym("partial", "\\partial"),
sym("nabla", "\\nabla"),
sym("ell", "\\ell"),
sym("hbar", "\\hbar"),
shortcut("dd", "differential", Insertion::Symbol("\\mathrm{d}")),
sym("pm", "\\pm"),
sym("mp", "\\mp"),
sym("times", "\\times"),
sym("div", "\\div"),
sym("cdot", "\\cdot"),
sym("ast", "\\ast"),
sym("star", "\\star"),
sym("circ", "\\circ"),
sym("oplus", "\\oplus"),
sym("otimes", "\\otimes"),
sym("setminus", "\\setminus"),
sym("cup", "\\cup"),
sym("cap", "\\cap"),
sym("wedge", "\\wedge"),
sym("vee", "\\vee"),
sym("land", "\\land"),
sym("lor", "\\lor"),
sym("leq", "\\leq"),
sym("le", "\\leq"),
sym("geq", "\\geq"),
sym("ge", "\\geq"),
sym("neq", "\\neq"),
sym("ne", "\\neq"),
sym("ll", "\\ll"),
sym("gg", "\\gg"),
sym("approx", "\\approx"),
sym("equiv", "\\equiv"),
sym("cong", "\\cong"),
sym("sim", "\\sim"),
sym("propto", "\\propto"),
sym("mid", "\\mid"),
sym("perp", "\\perp"),
sym("parallel", "\\parallel"),
sym("in", "\\in"),
sym("notin", "\\notin"),
sym("ni", "\\ni"),
sym("subset", "\\subset"),
sym("subseteq", "\\subseteq"),
sym("supset", "\\supset"),
sym("supseteq", "\\supseteq"),
sym("to", "\\to"),
sym("gets", "\\gets"),
sym("mapsto", "\\mapsto"),
sym("rightarrow", "\\rightarrow"),
sym("leftarrow", "\\leftarrow"),
sym("Rightarrow", "\\Rightarrow"),
sym("Leftarrow", "\\Leftarrow"),
sym("Leftrightarrow", "\\Leftrightarrow"),
sym("Longrightarrow", "\\Longrightarrow"),
sym("implies", "\\implies"),
sym("iff", "\\iff"),
sym("emptyset", "\\emptyset"),
sym("forall", "\\forall"),
sym("exists", "\\exists"),
sym("neg", "\\neg"),
sym("cdots", "\\cdots"),
sym("ldots", "\\ldots"),
sym("dots", "\\dots"),
sym("vdots", "\\vdots"),
sym("ddots", "\\ddots"),
sym("angle", "\\angle"),
sym("sin", "\\sin"),
sym("cos", "\\cos"),
sym("tan", "\\tan"),
sym("cot", "\\cot"),
sym("sec", "\\sec"),
sym("csc", "\\csc"),
sym("sinh", "\\sinh"),
sym("cosh", "\\cosh"),
sym("tanh", "\\tanh"),
sym("arcsin", "\\arcsin"),
sym("arccos", "\\arccos"),
sym("arctan", "\\arctan"),
sym("log", "\\log"),
sym("ln", "\\ln"),
sym("exp", "\\exp"),
sym("lim", "\\lim"),
sym("max", "\\max"),
sym("min", "\\min"),
sym("sup", "\\sup"),
sym("inf", "\\inf"),
sym("gcd", "\\gcd"),
sym("det", "\\det"),
sym("dim", "\\dim"),
sym("ker", "\\ker"),
sym("arg", "\\arg"),
sym("deg", "\\deg"),
sym("hom", "\\hom"),
];