mathlive
Version:
Render and edit beautifully typeset math
1,067 lines (958 loc) • 44.2 kB
JavaScript
/**
* @private
*/
import Definitions from '../core/definitions.js';
/**
* The index of this array is a keystroke combination as returned by the key
* field of a JavaScript keyboard event as documented here:
* https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values
* except for:
* - `Escape` → `Esc`
* - `LeftArrow`... → `Left/Right/Up/Down`
* - `Delete` → `Del`
* - `Escape` → `Esc`
* - ' ' → `Spacebar`
*
* The modifiers are specified before the main key, in the following order:
* 1. `Ctrl`
* 2. `Meta`: Command key on Mac OS. On Windows this is the Windows key,
* but the system intercepts those key combinations so they are never received
* 3. `Alt`: Option key on Mac OS
* 4. `Shift`
*
* The keys can be preceded by a context to restrict when the shortcut is
* applicable. For example, "math:Ctrl-KeyA" will restrict this shortcut
* to only apply in the "math" context (parseMode). Other valid context include
* "text" and "command".
*
* The value of the entries represent the command to perform.
* This can be either a single selector, or an array of a selector followed
* by its arguments.
* Selectors uses the following naming conventions:
* - a 'char' is a math atom (a letter, digit, symbol or compound atom)
* - a 'word' is a sequence of math atoms of the same type
* - a 'group' is a sequence of sibling atoms, for example a numerator or
* a superscript
* - the 'MathField' is the entire expression being edited
* - a 'placeholder' is either an actual placeholder atom or an empty child
* list, for example an empty numerator
* - 'move' changes the position of the insertion point (and collapses the
* selection range if necessary)
* - 'extend' keeps the anchor of the selection, but moves the focus (extends,
* or shrinks, the range of selected items)
*
* @type {Object<string,string>}
* @private
*/
const KEYBOARD_SHORTCUTS = {
'Left': 'moveToPreviousChar',
'Right': 'moveToNextChar',
'Up': 'moveUp',
'Down': 'moveDown',
'Shift-Left': 'extendToPreviousChar',
'Shift-Right': 'extendToNextChar',
'Shift-Up': 'extendUp',
'Shift-Down': 'extendDown',
'Backspace': 'deletePreviousChar',
'Alt-Del': 'deletePreviousChar',
'Del': 'deleteNextChar',
'Alt-Backspace': 'deleteNextChar',
'Alt-Left': 'moveToPreviousWord',
'Alt-Right': 'moveToNextWord',
'Alt-Shift-Left': 'extendToPreviousWord',
'Alt-Shift-Right': 'extendToNextWord',
'Ctrl-Left': 'moveToGroupStart',
'Ctrl-Right': 'moveToGroupEnd',
'Ctrl-Shift-Left': 'extendToGroupStart',
'Ctrl-Shift-Right': 'extendToGroupEnd',
'math:Spacebar': 'moveAfterParent',
'math:Shift-Spacebar': 'moveBeforeParent',
'Home': 'moveToMathFieldStart',
'mac:Meta-Left': 'moveToMathFieldStart',
'Shift-Home': 'extendToMathFieldStart',
'mac:Meta-Shift-Left': 'extendToMathFieldStart',
'End': 'moveToMathFieldEnd',
'mac:Meta-Right': 'moveToMathFieldEnd',
'Shift-End': 'extendToMathFieldEnd',
'mac:Meta-Shift-Right': 'extendToMathFieldEnd',
'PageUp': 'moveToGroupStart',
'PageDown': 'moveToGroupEnd',
'math:Tab': 'moveToNextPlaceholder',
'math:F8': 'moveToNextPlaceholder', // Visual Studio
'math:Shift-Tab': 'moveToPreviousPlaceholder',
'math:Shift-F8': 'moveToPreviousPlaceholder', // Visual Studio
'text:Tab': 'moveToNextPlaceholder',
'text:F8': 'moveToNextPlaceholder', // Visual Studio
'text:Shift-Tab': 'moveToPreviousPlaceholder',
'text:Shift-F8': 'moveToPreviousPlaceholder', // Visual Studio
'math:Esc': ['switch-mode', 'command'],
'math:Backslash': ['switch-mode', 'command'],
'math:IntlBackslash': ['switch-mode', 'command'],
'math:Alt-Equal': ['apply-style', {mode: 'text'}],
'text:Alt-Equal': ['apply-style', {mode: 'math'}],
'command:Esc': ['complete', {discard: true}], // discard the command, insert nothing
'command:Tab': ['complete', {acceptSuggestion: true}], // complete the suggestion
'command:Return': 'complete',
'command:Enter': 'complete',
'command:Shift-Esc': ['complete', {discard: true}], // Some keyboards can't generate
// this combination, for example in 60% keyboards it is mapped to ~
'command:Down': 'nextSuggestion',
'ios:command:Tab': 'nextSuggestion',
'command:Up': 'previousSuggestion',
'!mac:Ctrl-KeyA': 'selectAll',
'mac:Meta-KeyA': 'selectAll',
// Rare keys on some extended keyboards
'Cut': 'cut',
'Copy': 'copy',
'Paste': 'paste',
'Clear': 'delete',
'!mac:Ctrl-KeyZ': 'undo',
'mac:Meta-KeyZ': 'undo',
'Undo': 'undo',
'!mac:Ctrl-KeyY': 'redo', // ARIA recommendation
'mac:Meta-Shift-KeyY': 'redo',
'!mac:Ctrl-Shift-KeyZ': 'redo',
'mac:Meta-Shift-KeyZ': 'redo',
'Redo': 'redo',
'EraseEof': 'deleteToGroupEnd',
// EMACS/MACOS BINDINGS
'mac:Ctrl-KeyB': 'moveToPreviousChar',
'mac:Ctrl-KeyF': 'moveToNextChar',
'mac:Ctrl-KeyP': 'moveUp',
'mac:Ctrl-KeyN': 'moveDown',
'mac:Ctrl-KeyA': 'moveToMathFieldStart',
'mac:Ctrl-KeyE': 'moveToMathFieldEnd',
'mac:Ctrl-Shift-KeyB': 'extendToPreviousChar',
'mac:Ctrl-Shift-KeyF': 'extendToNextChar',
'mac:Ctrl-Shift-KeyP': 'extendUp',
'mac:Ctrl-Shift-KeyN': 'extendDown',
'mac:Ctrl-Shift-KeyA': 'extendToMathFieldStart',
'mac:Ctrl-Shift-KeyE': 'extendToMathFieldEnd',
'mac:Ctrl-Alt-KeyB': 'moveToPreviousWord',
'mac:Ctrl-Alt-KeyF': 'moveToNextWord',
'mac:Ctrl-Shift-Alt-KeyB': 'extendToPreviousWord',
'mac:Ctrl-Shift-Alt-KeyF': 'extendToNextWord',
'mac:Ctrl-KeyH': 'deletePreviousChar',
'mac:Ctrl-KeyD': 'deleteNextChar',
'mac:Ctrl-KeyL': 'scrollIntoView',
'mac:Ctrl-KeyT': 'transpose',
'math:Shift-Quote': ['switch-mode', 'text', '', '“'],
'text:Shift-Quote': ['switch-mode', 'math', '”', ''],
// WOLFRAM MATHEMATICA BINDINGS
'math:Ctrl-Digit2': ['insert', '\\sqrt{#0}'],
'math:Ctrl-Digit5': 'moveToOpposite',
'math:Ctrl-Digit6': 'moveToSuperscript',
'math:Ctrl-Minus': 'moveToSubscript',
'math:Alt-BracketLeft': ['insert', '\\left\\lbrack #0 \\right\\rbrack'],
'math:Alt-Shift-BracketLeft': ['insert', '\\left\\lbrace #0 \\right\\rbrace'],
'math:Return': 'addRowAfter',
'math:Enter': 'addRowAfter',
'math:Ctrl-Comma': 'addColumnAfter',
// Excel shortcuts:
// Shift-space: select entire row, ctrl+space: select an entire column
// ctrl+shift++ or ctrl+numpad+
// ctrl+- to delete a row or columns
// MATHLIVE BINDINGS
'math:Alt-KeyQ': ['insert', '\\theta'],
'math:Alt-KeyP': ['insert', '\\pi'],
'math:Alt-KeyV': ['insert', '\\sqrt{#0}'],
'math:Alt-KeyW': ['insert', '\\sum_{i=#?}^{#?}'],
'math:Alt-KeyB': ['insert', '\\int_{#?}^{#?}'],
'math:Alt-KeyU': ['insert', '\\cup'],
'math:Alt-KeyN': ['insert', '\\cap'],
'math:Alt-KeyO': ['insert', '\\emptyset'],
'math:Alt-KeyD': ['insert', '\\differentialD'],
'math:Alt-Shift-KeyO': ['insert', '\\varnothing'],
'math:Alt-Shift-KeyD': ['insert', '\\partial'],
'math:Alt-Shift-KeyP': ['insert', '\\prod_{i=#?}^{#?}'],
'math:Alt-Shift-KeyU': ['insert', '\\bigcup'],
'math:Alt-Shift-KeyN': ['insert', '\\bigcap'],
'math:Alt-Shift-KeyA': ['insert', '\\forall'],
'math:Alt-Shift-KeyE': ['insert', '\\exists'],
'math:Alt-Digit5': ['insert', '\\infty'], // "%" key
'math:Alt-Digit6': ['insert', '\\wedge'], // "^" key
'math:Alt-Shift-Digit6': ['insert', '\\vee'], // "^" key
'math:Alt-Digit9': ['insert', '('], // "(" key, override smartFence
'math:Alt-Digit0': ['insert', ')'], // ")" key, override smartFence
'math:Alt-Shift-Backslash': ['insert', '|'], // "|" key, override smartFence
'math:Alt-Backslash': ['insert', '\\backslash'], // "|" key, override command mode
'math:Slash': ['insert', '\\frac{#@}{#?}'],
'math:Alt-Slash': ['insert', '\\frac{#?}{#@}'],
'math:NumpadDivide': ['insert', '\\frac{#@}{#?}'],
'math:Alt-NumpadDivide': ['insert', '\\frac{#?}{#@}'],
'math:Shift-Backquote': ['insert', '\\~'],
'math:Alt-Shift-Slash': ['insert', '\\/'],
// Accessibility
'Alt-Shift-KeyK': 'toggleKeystrokeCaption',
'Alt-Space': 'toggleVirtualKeyboard',
// Note: On Mac OS (as of 10.12), there is a bug/behavior that causes
// a beep to be generated with certain command+control key combinations.
// The workaround is to create a default binding file to silence them.
// In ~/Library/KeyBindings/DefaultKeyBinding.dict add these entries:
/*
{
"^@\UF701" = "noop:";
"^@\UF702" = "noop:";
"^@\UF703" = "noop:";
}
*/
'mac:Ctrl-Meta-Up': ['speak', 'parent', {withHighlighting: false}],
'!mac:Ctrl-Alt-Up': ['speak', 'parent', {withHighlighting: false}],
'mac:Ctrl-Meta-Down': ['speak', 'all', {withHighlighting: false}],
'!mac:Ctrl-Alt-Down': ['speak', 'all', {withHighlighting: false}],
'mac:Ctrl-Meta-Left': ['speak', 'left', {withHighlighting: false}],
'!mac:Ctrl-Alt-Left': ['speak', 'left', {withHighlighting: false}],
'mac:Ctrl-Meta-Right': ['speak', 'right', {withHighlighting: false}],
'!mac:Ctrl-Alt-Right': ['speak', 'right', {withHighlighting: false}],
'!mac:Ctrl-Alt-Period': ['speak', 'selection', {withHighlighting: false}],
'mac:Ctrl-Meta-Period': ['speak', 'selection', {withHighlighting: false}],
'mac:Ctrl-Meta-Shift-Up': ['speak', 'parent', {withHighlighting: true}],
'!mac:Ctrl-Alt-Shift-Up': ['speak', 'parent', {withHighlighting: true}],
'mac:Ctrl-Meta-Shift-Down': ['speak', 'all', {withHighlighting: true}],
'!mac:Ctrl-Alt-Shift-Down': ['speak', 'all', {withHighlighting: true}],
'mac:Ctrl-Meta-Shift-Left': ['speak', 'left', {withHighlighting: true}],
'!mac:Ctrl-Alt-Shift-Left': ['speak', 'left', {withHighlighting: true}],
'mac:Ctrl-Meta-Shift-Right': ['speak', 'right', {withHighlighting: true}],
'!mac:Ctrl-Alt-Shift-Right': ['speak', 'right', {withHighlighting: true}],
'!mac:Ctrl-Alt-Shift-Period': ['speak', 'selection', {withHighlighting: true}],
'mac:Ctrl-Meta-Shift-Period': ['speak', 'selection', {withHighlighting: true}],
// '!mac:Ctrl-Alt-Shift-Home': ['speak', 'start', {withHighlighting: true}],
// 'mac:Ctrl-Alt-Shift-Home': ['speak', 'start', {withHighlighting: true}],
// '!mac:Ctrl-Alt-Shift-End': ['speak', 'end', {withHighlighting: true}],
// 'mac:Ctrl-Alt-Shift-End': ['speak', 'end', {withHighlighting: true}],
}
/**
* Most commands can be associated to their keyboard shortcuts from the
* KEYBOARD_SHORTCUTS table above, for example 'speakSelection' -> 'Ctrl-KeyR'
* However, those that contain complex commands are more ambiguous.
* For example, '\sqrt' -> 'math:Alt-KeyV'. This table provides the reverse
* mapping for those more complex commands. It is used when displaying
* keyboard shortcuts for specific commands in the popover.
* @type {Object<string,string>}
* @private
*/
const REVERSE_KEYBOARD_SHORTCUTS = {
'\\theta': 'Alt-KeyQ',
'\\sqrt': ['Alt-KeyV', 'Ctrl-Digit2'],
'\\pi': 'Alt-KeyP',
'\\prod': 'Alt-Shift-KeyP',
'\\sum': 'Alt-KeyW',
'\\int': 'Alt-KeyB',
'\\cup': 'Alt-KeyU',
'\\cap': 'Alt-KeyN',
'\\bigcup': 'Alt-Shift-KeyU',
'\\bigcap': 'Alt-Shift-KeyN',
'\\forall': 'Alt-Shift-KeyA',
'\\exists': 'Alt-Shift-KeyE',
'\\infty': 'Alt-Digit5',
'\\wedge': 'Alt-Digit5',
'\\vee': 'Alt-Shift-Digit6',
'\\differentialD': 'Alt-keyD',
'\\partial': 'Alt-Shift-KeyD',
'\\frac': 'Slash',
'\\emptyset': 'Alt-KeyO',
'\\varnothing': 'Alt-Shift-KeyO',
'\\~': '~'
}
/**
* These shortcut strings are replaced with the corresponding LaTeX expression
* without requiring an escape sequence or command.
*
* @type {Object.<string,string>}
* @private
*/
const INLINE_SHORTCUTS = {
// Primes
"''": { mode: 'math', value: '^{\\doubleprime}'},
// Greek letters
'alpha': '\\alpha',
'delta': '\\delta',
'Delta': '\\Delta',
'pi': { mode: 'math', value: '\\pi'},
'pi ': { mode: 'text', value: '\\pi '},
'Pi': { mode: 'math', value: '\\Pi'},
'theta': '\\theta',
'Theta': '\\Theta',
// Letter-like
'ii': { after: 'nothing+digit+function+frac+surd+binop+relop+punct+array+openfence+closefence+space+text',
value: '\\imaginaryI' },
'jj': { after: 'nothing+digit+function+frac+surd+binop+relop+punct+array+openfence+closefence+space+text',
value: '\\imaginaryJ' },
'ee': {
mode: 'math',
after: 'nothing+digit+function+frac+surd+binop+relop+punct+array+openfence+closefence+space+text',
value: '\\exponentialE',
},
'nabla': { mode: 'math', value: '\\nabla'},
'grad': { mode: 'math', value: '\\nabla'},
'del': { mode: 'math', value: '\\partial'},
'\u221e': '\\infty', // @TODO: doesn't work
// '∞': '\\infty',
// '∞': '\\infty',
'oo': {
mode: 'math',
after: 'nothing+digit+frac+surd+binop+relop+punct+array+openfence+closefence+space',
value: '\\infty',
},
// Big operators
'∑': { mode: 'math', value: '\\sum'},
'sum': { mode: 'math', value: '\\sum_{#?}^{#?}' },
'prod': { mode: 'math', value: '\\prod_{#?}^{#?}' },
'sqrt': { mode: 'math', value: '\\sqrt'},
// '∫': '\\int', // There's a alt-B command for this
'∆': { mode: 'math', value: '\\differentialD'}, // @TODO: is \\diffD most common?
'∂': { mode: 'math', value: '\\differentialD'},
// Functions
'sin': { mode: 'math', value: '\\sin'},
'cos': { mode: 'math', value: '\\cos'},
'tan': { mode: 'math', value: '\\tan'},
'tanh': { mode: 'math', value: '\\tanh'},
'log': { mode: 'math', value: '\\log'},
'ln': { mode: 'math', value: '\\ln'},
'exp': { mode: 'math', value: '\\exp'},
'lim': { mode: 'math', value: '\\lim_{#?}'},
// Differentials
// According to ISO31/XI (ISO 80000-2), differentials should be upright
'dx': '\\differentialD x',
'dy': '\\differentialD y',
'dt': '\\differentialD t',
// Logic
'AA': { mode: 'math', value: '\\forall'},
'EE': { mode: 'math', value: '\\exists'},
'!EE': { mode: 'math', value: '\\nexists'},
'&&': { mode: 'math', value: '\\land'},
// The shortcut for the greek letter "xi" is interfering with "x in"
'xin': {
mode: 'math',
after: 'nothing+text+relop+punct+openfence+space',
value: 'x \\in',
},
'in': {
mode: 'math',
after: 'nothing+letter+closefence',
value: '\\in',
},
'!in': { mode: 'math', value: '\\notin'},
// Sets
'NN': '\\N', // Natural numbers
'ZZ': '\\Z', // Integers
'QQ': '\\Q', // Rational numbers
'RR': '\\R', // Real numbers
'CC': '\\C', // Complex numbers
'PP': '\\P', // Prime numbers
// Operators
'xx': '\\times',
'+-': '\\pm',
// Relational operators
'!=': '\\ne',
'>=': '\\ge',
'<=': '\\le',
'<<': '\\ll',
'>>': '\\gg',
'~~': '\\approx',
// More operators
'≈': '\\approx',
'?=': '\\questeq',
'÷': '\\div',
'¬': '\\neg',
':=': '\\coloneq',
'::': '\\Colon',
// Fences
'(:': '\\langle',
':)': '\\rangle',
// More Greek letters
'beta': '\\beta',
'chi': '\\chi',
'epsilon': '\\epsilon',
'varepsilon': '\\varepsilon',
'eta': {
mode: 'math',
after: 'nothing+digit+function+frac+surd+binop+relop+punct+array+openfence+closefence+space+text',
value: '\\eta'
},
'eta ': {
mode: 'text',
after: 'nothing+digit+function+frac+surd+binop+relop+punct+array+openfence+closefence+space+text',
value: '\\eta '
},
'gamma': '\\gamma',
'Gamma': '\\Gamma',
'iota': '\\iota',
'kappa': '\\kappa',
'lambda': '\\lambda',
'Lambda': '\\Lambda',
'mu': {
mode: 'math',
after: 'nothing+digit+function+frac+surd+binop+relop+punct+array+openfence+closefence+space+text',
value: '\\mu'
},
'mu ': {
mode: 'text',
after: 'nothing+digit+function+frac+surd+binop+relop+punct+array+openfence+closefence+space+text',
value: '\\mu '
},
'nu': {
mode: 'math',
after: 'nothing+digit+function+frac+surd+binop+relop+punct+array+openfence+closefence+space+text',
value:'\\nu'
},
'nu ': {
mode: 'text',
after: 'nothing+digit+function+frac+surd+binop+relop+punct+array+openfence+closefence+space+text',
value:'\\nu '
},
'µ': '\\mu', // @TODO: or micro?
'phi': {
after: 'nothing+digit+function+frac+surd+binop+relop+punct+array+openfence+closefence+space+text',
value: '\\phi'
},
'Phi': {
after: 'nothing+digit+function+frac+surd+binop+relop+punct+array+openfence+closefence+space+text',
value:'\\Phi'
},
'varphi': '\\varphi',
'psi': {
after: 'nothing+digit+function+frac+surd+binop+relop+punct+array+openfence+closefence+space+text',
value: '\\psi'
},
'Psi': {
after: 'nothing+digit+function+frac+surd+binop+relop+punct+array+openfence+closefence+space+text',
value:'\\Psi'
},
'rho': {
after: 'nothing+digit+function+frac+surd+binop+relop+punct+array+openfence+closefence+space+text',
value: '\\rho'
},
'sigma': '\\sigma',
'Sigma': '\\Sigma',
'tau': {
after: 'nothing+digit+function+frac+surd+binop+relop+punct+array+openfence+closefence+space+text',
value: '\\tau'
},
'vartheta': '\\vartheta',
'upsilon': '\\upsilon',
'xi': {
after: 'nothing+digit+function+frac+surd+binop+relop+punct+array+openfence+closefence+space+text',
value:'\\xi'},
'Xi': {
after: 'nothing+digit+function+frac+surd+binop+relop+punct+array+openfence+closefence+space+text',
value:'\\Xi'},
'zeta': '\\zeta',
'omega': '\\omega',
'Omega': '\\Omega',
'Ω': '\\omega', // @TODO: or ohm?
// More Logic
'forall': '\\forall',
'exists': {
mode: 'math',
value:'\\exists'
},
'!exists': {
mode: 'math',
value: '\\nexists'
},
':.': {
mode: 'math',
value:'\\therefore'
},
// MORE FUNCTIONS
// 'arg': '\\arg',
'liminf': '\\operatorname*{lim~inf}_{#?}',
'limsup': '\\operatorname*{lim~sup}_{#?}',
'argmin': '\\operatorname*{arg~min}_{#?}',
'argmax': '\\operatorname*{arg~max}_{#?}',
'det': '\\det',
'mod': {
mode: 'math',
value:'\\mod'},
'max': {
mode: 'math',
value:'\\max'},
'min': {
mode: 'math',
value:'\\min'},
'erf': '\\operatorname{erf}',
'erfc': '\\operatorname{erfc}',
'bessel': {
mode: 'math',
value:'\\operatorname{bessel}'},
'mean': {
mode: 'math',
value: '\\operatorname{mean}'
},
'median': {
mode: 'math',
value: '\\operatorname{median}'},
'fft': {
mode: 'math',
value:'\\operatorname{fft}'},
'lcm': {
mode: 'math',
value:'\\operatorname{lcm}'},
'gcd': {
mode: 'math',
value:'\\operatorname{gcd}'},
'randomReal': '\\operatorname{randomReal}',
'randomInteger': '\\operatorname{randomInteger}',
'Re': {
mode: 'math',
value:'\\operatorname{Re}'},
'Im': {
mode: 'math',
value:'\\operatorname{Im}'},
// UNITS
'mm': { mode: 'math',
after: 'nothing+digit',
value: '\\operatorname{mm}', // millimeter
},
'cm': { mode: 'math',
after: 'nothing+digit',
value: '\\operatorname{cm}', // centimeter
},
'km': { mode: 'math',
after: 'nothing+digit',
value: '\\operatorname{km}', // kilometer
},
'kg': { mode: 'math',
after: 'nothing+digit',
value: '\\operatorname{kg}', // kilogram
},
// '||': '\\lor',
'...': '\\ldots', // In general, use \ldots
'+...': '+\\cdots', // ... but use \cdots after + ...
'-...': '-\\cdots', // ... - and ...
'->...': '\\to\\cdots', // ->
'->': '\\to',
'|->': '\\mapsto',
'-->': '\\longrightarrow',
// '<-': '\\leftarrow',
'<--': '\\longleftarrow',
'=>': '\\Rightarrow',
'==>': '\\Longrightarrow',
// '<=': '\\Leftarrow', // CONFLICTS WITH LESS THAN OR EQUAL
'<=>': '\\Leftrightarrow',
'<->': '\\leftrightarrow',
'(.)': '\\odot',
'(+)': '\\oplus',
'(/)': '\\oslash',
'(*)': '\\otimes',
'(-)': '\\ominus',
// '(-)': '\\circleddash',
'||': '\\Vert',
'{': '\\{',
'}': '\\}',
'*': '\\cdot',
/*
//
// ASCIIIMath
//
// Binary operation symbols
'**': '\\ast',
'***': '\\star',
'//': '\\slash',
'\\\\': '\\backslash',
'setminus': '\\backslash',
'|><': '\\ltimes',
'><|': '\\rtimes',
'|><|': '\\bowtie',
'-:': '\\div',
'divide': '\\div',
'@': '\\circ',
'o+': '\\oplus',
'ox': '\\otimes',
'o.': '\\odot',
'^^': '\\wedge',
'^^^': '\\bigwedge',
'vv': '\\vee',
'vvv': '\\bigvee',
'nn': '\\cap',
'nnn': '\\bigcap',
'uu': '\\cup',
'uuu': '\\bigcup',
// Binary relation symbols
'-=': '\\equiv',
'~=': '\\cong',
'lt': '<',
'lt=': '\\leq',
'gt': '>',
'gt=': '\\geq',
'-<': '\\prec',
'-lt': '\\prec',
'-<=': '\\preceq',
// '>-': '\\succ',
'>-=': '\\succeq',
'prop': '\\propto',
'diamond': '\\diamond',
'square': '\\square',
'iff': '\\iff',
'sub': '\\subset',
'sup': '\\supset',
'sube': '\\subseteq',
'supe': '\\supseteq',
'uarr': '\\uparrow',
'darr': '\\downarrow',
'rarr': '\\rightarrow',
'rArr': '\\Rightarrow',
'larr': '\\leftarrow',
'lArr': '\\Leftarrow',
'harr': '\\leftrightarrow',
'hArr': '\\Leftrightarrow',
'aleph': '\\aleph',
// Logic
'and': '\\land',
'or': '\\lor',
'not': '\\neg',
'_|_': '\\bot',
'TT': '\\top',
'|--': '\\vdash',
'|==': '\\models',
// Other functions
'|__': '\\lfloor',
'__|': '\\rfloor',
'|~': '\\lceil',
'~|': '\\rceil',
// Arrows
'>->': '\\rightarrowtail',
'->>': '\\twoheadrightarrow',
'>->>': '\\twoheadrightarrowtail'
*/
};
/**
* Return an array of potential shortcuts
* @param {string} s
* @param {object} config
* @return {string[]}
*/
function startsWithString(s, config) {
const result = [];
for (let i = 0; i <= s.length - 1; i++) {
const s2 = s.substring(i);
const skipDefaultShortcuts = config && config.overrideDefaultInlineShortcuts;
if (!skipDefaultShortcuts) {
Object.keys(INLINE_SHORTCUTS).forEach(key => {
if (key.startsWith(s2) && !result.includes(key)) {
result.push(key);
}
});
}
const customInlineShortcuts = config && config.inlineShortcuts ?
config.inlineShortcuts : null;
if (customInlineShortcuts) {
Object.keys(customInlineShortcuts).forEach(key => {
if (key.startsWith(s2)) {
result.push(key);
}
});
}
}
return result;
}
/**
*
* @param {string} mode
* @param {object[]} siblings atoms preceding this potential shortcut
* @param {string} shortcut
*/
function validateShortcut(mode, siblings, shortcut) {
if (!shortcut) return shortcut
// If it's a simple shortcut (no conditional), it's valid
if (typeof shortcut === 'string') return shortcut
if (typeof shortcut.mode === 'string' && shortcut.mode !== mode) return null;
// If we have no context, we assume all the shortcuts are valid
if (!siblings) return shortcut ? shortcut.value : undefined;
let nothing = false;
let letter = false;
let digit = false;
let isFunction = false;
let frac = false;
let surd = false;
let binop = false;
let relop = false;
let punct = false;
let array = false;
let openfence = false;
let closefence = false;
let text = false;
let space = false;
let sibling = siblings[siblings.length - 1];
let index = siblings.length - 1;
while (sibling && /msubsup|placeholder/.test(sibling.type)) {
index -= 1;
sibling = siblings[index];
}
nothing = !sibling || sibling.type === 'first'; // start of a group
if (sibling) {
text = sibling.mode === 'text';
letter = !text && sibling.type === 'mord' && Definitions.LETTER.test(sibling.body);
digit = !text && sibling.type === 'mord' && /[0-9]+$/.test(sibling.body);
isFunction = !text && sibling.isFunction;
frac = sibling.type === 'genfrac';
surd = sibling.type === 'surd';
binop = sibling.type === 'mbin';
relop = sibling.type === 'mrel';
punct = sibling.type === 'mpunct' || sibling.type === 'minner';
array = sibling.array;
openfence = sibling.type === 'mopen';
closefence = sibling.type === 'mclose' || sibling.type === 'leftright';
space = sibling.type === 'space';
}
if (typeof shortcut.after !== 'undefined') {
// If this is a conditional shortcut, consider the conditions now
if ( (/nothing/.test(shortcut.after) && nothing) ||
(/letter/.test(shortcut.after) && letter) ||
(/digit/.test(shortcut.after) && digit) ||
(/function/.test(shortcut.after) && isFunction) ||
(/frac/.test(shortcut.after) && frac) ||
(/surd/.test(shortcut.after) && surd) ||
(/binop/.test(shortcut.after) && binop) ||
(/relop/.test(shortcut.after) && relop) ||
(/punct/.test(shortcut.after) && punct) ||
(/array/.test(shortcut.after) && array) ||
(/openfence/.test(shortcut.after) && openfence) ||
(/closefence/.test(shortcut.after) && closefence) ||
(/text/.test(shortcut.after) && text) ||
(/space/.test(shortcut.after) && space)){
return shortcut.value;
}
return null;
}
return shortcut.value;
}
/**
* This function is used to resolve inline shortcuts.
*
* @param {string} mode
* @param {string} context - atoms preceding the candidate, potentially used
* to reduce which shortcuts are applicable. If 'null', no restrictions are
* applied.
* @param {string} s - candidate inline shortcuts (e.g. `'pi'`)
* @param {object} config
* @return {string} - A replacement string matching the shortcut (e.g. `'\pi'`)
* @memberof module:editor/shortcuts
* @private
*/
function forString(mode, context, s, config) {
let result = '';
const skipDefaultShortcuts = config && config.overrideDefaultInlineShortcuts;
if (!skipDefaultShortcuts) {
result = validateShortcut(mode, context, INLINE_SHORTCUTS[s]);
}
const customInlineShortcuts = config && config.inlineShortcuts ?
config.inlineShortcuts : null;
let customResult;
if (customInlineShortcuts) {
customResult = validateShortcut(mode, context, customInlineShortcuts[s]);
}
return customResult || result;
}
/**
* Return `p`, the platform name if `p` is the current platform, otherwise
* return `!p`. For example, when running on Windows, `platform('mac')` returns
* `'!mac'`.
* The valid values for `p` are:
* - `'mac'`
* - `'win'`
* - `'android`'
* - `'ios'`
* - `'chromeos'`
* - `'other'` (Linux, etc...)
* @param {string} p The platform to test against.
* @return {string} if we are running on the candidate platform, return it.
* Otherwise, return "!" + candidate.
* @memberof module:editor/shortcuts
* @private
*/
function platform(p) {
let result = 'other';
if (navigator && navigator.platform && navigator.userAgent) {
if (/^(mac)/i.test(navigator.platform)) {
result = 'mac';
} else if (/^(win)/i.test(navigator.platform)) {
result = 'win';
} else if (/(android)/i.test(navigator.userAgent)) {
result = 'android';
} else if (/(iphone)/i.test(navigator.userAgent) ||
/(ipod)/i.test(navigator.userAgent) ||
/(ipad)/i.test(navigator.userAgent)) {
result = 'ios';
} else if (/\bCrOS\b/i.test(navigator.userAgent)) {
result = 'chromeos';
}
}
return result === p ? p : '!' + p;
}
/**
* Return the selector matching the keystroke.
*
* @param {string} mode
* @param {string} keystroke
* @return {string}
* @memberof module:editor/shortcuts
* @private
*/
function selectorForKeystroke(mode, keystroke) {
for (const c of [
platform('mac') + ':' + mode + ':' + keystroke,
platform('win') + ':' + mode + ':' + keystroke,
platform('ios') + ':' + mode + ':' + keystroke,
platform('android') + ':' + mode + ':' + keystroke,
platform('chromeos') + ':' + mode + ':' + keystroke,
platform('other') + ':' + mode + ':' + keystroke,
platform('mac') + ':' + keystroke,
platform('win') + ':' + keystroke,
platform('ios') + ':' + keystroke,
platform('android') + ':' + keystroke,
platform('chromeos') + ':' + keystroke,
mode + ':' + keystroke,
keystroke,
]) {
if (KEYBOARD_SHORTCUTS[c]) {
return KEYBOARD_SHORTCUTS[c];
}
}
return '';
}
function commandToString(command) {
let result = command;
if (Array.isArray(result) && result.length > 0) {
result = result[0] + '(' + result.slice(1).join('') + ')';
}
return result;
}
function forCommand(command) {
let result = [];
if (typeof command === 'string') {
const candidate = REVERSE_KEYBOARD_SHORTCUTS[command];
if (Array.isArray(candidate)) {
result = candidate.slice();
} else if (candidate) {
result.push(candidate);
}
}
// A command can be either a simple selector, or a selector
// with arguments. Normalize it to a string
command = commandToString(command);
const regex = new RegExp('^' +
command.replace('\\','\\\\').
replace('|', '\\|').
replace('*', '\\*').
replace('$', '\\$').
replace('^', '\\^')
+ '([^*a-zA-Z]|$)');
Object.keys(KEYBOARD_SHORTCUTS).forEach(shortcut => {
if (regex.test(commandToString(KEYBOARD_SHORTCUTS[shortcut]))) {
const m = shortcut.match(/:([^:]*)$/);
if (m) result.push(m[1]);
}
}
);
return stringify(result);
}
/**
* Return a human readable representation of an array of shortcut strings
* @param {Object<string,string>} shortcuts
* @param {?string} join - optional, string in between each shortcut representation
* @memberof module:editor/shortcuts
* @private
*/
function stringify(shortcuts, join) {
let result = '';
if (!Array.isArray(shortcuts)) shortcuts = [shortcuts];
for (const shortcut of shortcuts) {
let keyboardShortcut;
const platMatch = shortcut.match(/(^[^:]*):/);
const plat = platMatch ? platMatch[1] : '';
if (plat === platform('mac') ||
plat === platform('win') ||
plat === platform('ios') ||
plat === platform('android') ||
plat === platform('chromeos') ||
plat === platform('other')) {
const m = shortcut.match(/:([^:]*)$/);
keyboardShortcut = m ? m[1] : shortcut;
} else if (!['mac', '!mac', 'win', '!win', 'ios', '!ios', 'android',
'!android', 'chromeos', '!chromeos', 'other', '!other'].includes(plat)) {
const m = shortcut.match(/:([^:]*)$/);
keyboardShortcut = m ? m[1] : shortcut;
}
if (keyboardShortcut) {
const useSymbol = platform('mac') === 'mac' ||
platform('ios') === 'ios';
const modifiers = keyboardShortcut.length > 1 ?
keyboardShortcut.split('-') : [keyboardShortcut];
let shortcutString = '';
for (const modifier of modifiers) {
if (!useSymbol && shortcutString.length > 0) {
shortcutString += '<span class="ML__shortcut-join">+</span>';
}
if (modifier.substr(0, 3) === 'Key') {
shortcutString += modifier.substr(3, 1);
} else if (modifier.substr(0, 5) === 'Digit') {
shortcutString += modifier.substr(5, 1);
} else {
shortcutString += {
'Meta': useSymbol ? '\u2318' : 'command',
'Shift': useSymbol ? '\u21e7' : 'shift',
'Alt': useSymbol ? '\u2325' : 'alt',
'Ctrl': useSymbol ? '\u2303' : 'control',
'\n': useSymbol ? '\u23ce' : 'return',
'Return': useSymbol ? '\u23ce' : 'return',
'Enter': useSymbol ? '\u2324' : 'enter',
'Tab': useSymbol ? '\u21e5' : 'tab',
// 'Esc': useSymbol ? '\u238b' : 'esc',
'Esc': 'esc',
'Backspace': useSymbol ? '\u232b' : 'backspace',
'Del': useSymbol ? '\u2326' : 'del',
'PageUp': useSymbol ? '\u21de' : 'page up',
'PageDown': useSymbol ? '\u21df' : 'page down',
'Home': useSymbol ? '\u2912' : 'home',
'End': useSymbol ? '\u2913' : 'end',
'Spacebar': 'space',
'Semicolon': ';',
'Period': '.',
'Comma': ',',
'Minus': '-',
'Equal': '=',
'Quote': '\'',
'BracketLeft': '[',
'BracketRight': ']',
'Backslash': '\\',
'IntlBackslash': '\\',
'Backquote': '`',
'Slash': '/',
'NumpadMultiply': '* 🔢',
'NumpadDivide': '/ 🔢', // Numeric keypad
'NumpadSubtract': '- 🔢',
'NumpadAdd': '+ 🔢',
'NumpadDecimal': '. 🔢',
'NumpadComma': ', 🔢',
'Help': 'help',
'Left': '\u21E0',
'Up': '\u21E1',
'Right': '\u21E2',
'Down': '\u21E3',
}[modifier] || modifier;
}
}
if (result.length > 0) {
result += join || ' or ';
}
// if (shortcutString.length === 1) {
// shortcutString = shortcutString + ' (U+' +
// ('0000' + shortcutString.codePointAt(0).toString(16)).substr(-4) +
// ')';
// }
result += shortcutString;
}
}
return result;
}
export default {
KEYBOARD_SHORTCUTS,
INLINE_SHORTCUTS,
stringify,
startsWithString,
forString,
selectorForKeystroke,
forCommand
}