UNPKG

mathlive

Version:

Render and edit beautifully typeset math

1,675 lines (1,402 loc) 112 kB
/** * This module contains the definitions of all the symbols and commands, for * example `\alpha`, `\sin`, `\mathrm`. * There are a few exceptions with some "built-in" commands that require * special parsing such as `\char`. * @module core/definitions * @private */ import FontMetrics from './fontMetrics.js'; /** * To organize the symbols when generating the documentation, we * keep track of a category that gets assigned to each symbol. * @private */ let category = ''; const MATH_SYMBOLS = {}; const FUNCTIONS = {}; const ENVIRONMENTS = {}; const MACROS = { 'iff': '\\;\u27fa\\;', //>2,000 Note: additional spaces around the arrows 'nicefrac': '^{#1}\\!\\!/\\!_{#2}', // From bracket.sty, Diract notation 'bra': '\\mathinner{\\langle{#1}|}', 'ket': '\\mathinner{|{#1}\\rangle}', 'braket': '\\mathinner{\\langle{#1}\\rangle}', 'set': '\\mathinner{\\lbrace #1 \\rbrace}', 'Bra': '\\left\\langle #1\\right|', 'Ket': '\\left|#1\\right\\rangle', 'Braket': '\\left\\langle{#1}\\right\\rangle', 'Set': '\\left\\lbrace #1 \\right\\rbrace', }; const RIGHT_DELIM = { '(': ')', '{': '}', '[': ']', '|': '|', '\\lbrace': '\\rbrace', '\\{': '\\}', '\\langle': '\\rangle', '\\lfloor': '\\rfloor', '\\lceil': '\\rceil', '\\vert': '\\vert', '\\lvert': '\\rvert', '\\Vert': '\\Vert', '\\lVert': '\\rVert', '\\lbrack': '\\rbrack', '\\ulcorner': '\\urcorner', '\\llcorner': '\\lrcorner', '\\lgroup': '\\rgroup', '\\lmoustache': '\\rmoustache' } // Frequency of a symbol. // String constants corresponding to frequency values, // which are the number of results returned by latexsearch.com // When the precise number is known, it is provided. Otherwise, // the following constants are used to denote an estimate. const CRYPTIC = 'CRYPTIC'; const ARCANE = 'ARCANE'; // const VERY_RARE = 'VERY_RARE'; const RARE = 'RARE'; const UNCOMMON = 'UNCOMMON'; const COMMON = 'COMMON'; const SUPERCOMMON = 'SUPERCOMMON'; /** * @type {Object.<string, number>} * @private */ const FREQUENCY_VALUE = { 'CRYPTIC': 0, 'ARCANE': 200, 'VERY_RARE': 600, 'RARE': 1200, 'UNCOMMON': 2000, 'COMMON': 3000, 'SUPERCOMMON': 4000 } /** * Set the frequency of the specified symbol. * Default frequency is UNCOMMON * The argument list is a frequency value, followed by one or more symbol strings * For example: * frequency(COMMON , '\\sin', '\\cos') * @param {string|number} value The frequency as a string constant, * or a numeric value [0...2000] * @param {...string} * @memberof module:definitions * @private */ function frequency(value, ...symbols) { const v = typeof value === 'string' ? FREQUENCY_VALUE[value] : value; for (const symbol of symbols) { if (MATH_SYMBOLS[symbol]) { MATH_SYMBOLS[symbol].frequency = v; } if (FUNCTIONS[symbol]) { // Make a copy of the entry, since it could be shared by multiple // symbols FUNCTIONS[symbol] = Object.assign({}, FUNCTIONS[symbol]); FUNCTIONS[symbol].frequency = v; } } } /** * * @param {string} latexName The common LaTeX command for this symbol * @param {(string|string[])} mode * @param {string} fontFamily * @param {string} type * @param {string} value * @param {(number|string)} [frequency] * @memberof module:definitions * @private */ function defineSymbol(latexName, fontFamily, type, value, frequency) { if (fontFamily && !/^(ams|cmr|bb|cal|frak|scr)$/.test(fontFamily)) { console.log(fontFamily, latexName); } // Convert a frequency constant to a numerical value if (typeof frequency === 'string') frequency = FREQUENCY_VALUE[frequency]; MATH_SYMBOLS[latexName] = { type: type === ORD ? MATHORD : type, baseFontFamily: fontFamily, value: value, category: category, // To group items when generating the documentation frequency: frequency }; } /** * Define a set of single-character symbols and all their attributes. * The value associated with the symbol is the symbol itself. * @param {string} string a string of single character symbols * @param {string} mode * @param {string} fontFamily * @param {string} type * @param {(string|number)} [frequency] * @memberof module:definitions * @private */ function defineSymbols(string) { for (let i = 0; i < string.length; i++) { const ch = string.charAt(i); defineSymbol(ch, '', MATHORD, ch); } } /** * Define a set of single-character symbols as a range of Unicode codepoints * @param {number} from First Unicode codepoint * @param {number} to Last Unicode codepoint * @memberof module:definitions * @private */ function defineSymbolRange(from, to) { for (let i = from; i <= to; i++) { const ch = String.fromCodePoint(i); defineSymbol(ch, '', 'mord', ch); } } const CODEPOINT_SHORTCUTS = { 8739: '|', 0x00b7: '\\cdot', 0x00bc: '\\frac{1}{4}', 0x00bd: '\\frac{1}{2}', 0x00be: '\\frac{3}{4}', 0x2070: '^{0}', 0x2071: '^{i}', 0x00b9: '^{1}', 0x00b2: '^{2}', 0x00b3: '^{3}', 0x2074: '^{4}', 0x2075: '^{5}', 0x2076: '^{6}', 0x2077: '^{7}', 0x2078: '^{8}', 0x2079: '^{9}', 0x207a: '^{+}', 0x207b: '^{-}', 0x207c: '^{=}', 0x207f: '^{n}', 0x2080: '_{0}', 0x2081: '_{1}', 0x2082: '_{2}', 0x2083: '_{3}', 0x2084: '_{4}', 0x2085: '_{5}', 0x2086: '_{6}', 0x2087: '_{7}', 0x2088: '_{8}', 0x2089: '_{9}', 0x208A: '_{+}', 0x208B: '_{-}', 0x208C: '_{=}', 0x2090: '_{a}', 0x2091: '_{e}', 0x2092: '_{o}', 0x2093: '_{x}', 0x2032: '\\prime', 0x2033: '\\doubleprime', 0x2220: '\\angle', 0x2102: '\\C', 0x2115: '\\N', 0x2119: '\\P', 0x211A: '\\Q', 0x211D: '\\R', 0x2124: '\\Z', }; /** * Given a character, return a LaTeX expression matching its Unicode codepoint. * If there is a matching symbol (e.g. \alpha) it is returned. * @param {string} parseMode * @param {number} cp * @return {string} * @memberof module:definitions * @private */ function matchCodepoint(parseMode, cp) { const s = String.fromCodePoint(cp); // Some symbols map to multiple codepoints. // Some symbols are 'pseudosuperscript'. Convert them to a super(or sub)script. // Map their alternative codepoints here. if (parseMode === 'math' && CODEPOINT_SHORTCUTS[s]) return CODEPOINT_SHORTCUTS[s]; // Don't map 'simple' code point. // For example "C" maps to \doubleStruckCapitalC, not the desired "C" if (cp > 32 && cp < 127) return s; let result = ''; if (parseMode === 'math') { for (const p in MATH_SYMBOLS) { if (Object.prototype.hasOwnProperty.call(MATH_SYMBOLS, p)) { if (MATH_SYMBOLS[p].value === s) { result = p; break; } } } } else { for (const p in TEXT_SYMBOLS) { if (Object.prototype.hasOwnProperty.call(TEXT_SYMBOLS, p)) { if (TEXT_SYMBOLS[p] === s) { result = p; break; } } } } return result || s; } /** * Given a Unicode character returns {char:, variant:, style} corresponding * to this codepoint. `variant` is optional. * This maps characters such as "blackboard uppercase C" to * {char: 'C', variant: 'double-struck', style:''} * @param {string} char */ /* Some symbols in the MATHEMATICAL ALPHANUMERICAL SYMBOLS block had been previously defined in other blocks. Remap them */ const MATH_LETTER_EXCEPTIONS = { 0x1d455: 0x0210e, 0x1D49D: 0x0212C, 0x1D4A0: 0x02130, 0x1D4A1: 0x02131, 0x1D4A3: 0x0210B, 0x1D4A4: 0x02110, 0x1D4A7: 0x02112, 0x1D4A8: 0x02133, 0x1D4AD: 0x0211B, 0x1D4BA: 0x0212F, 0x1D4BC: 0x0210A, 0x1D4C4: 0x02134, 0x1D506: 0x0212D, 0x1D50B: 0x0210C, 0x1D50C: 0x02111, 0x1D515: 0x0211C, 0x1D51D: 0x02128, 0x1D53A: 0x02102, 0x1D53F: 0x0210D, 0x1D545: 0x02115, 0x1D547: 0x02119, 0x1D548: 0x0211A, 0x1D549: 0x0211D, 0x1D551: 0x02124, } const MATH_UNICODE_BLOCKS = [ { start: 0x1D400, len: 26, offset: 65, style: 'bold' }, { start: 0x1D41A, len: 26, offset: 97, style: 'bold' }, { start: 0x1D434, len: 26, offset: 65, style: 'italic' }, { start: 0x1D44E, len: 26, offset: 97, style: 'italic' }, { start: 0x1D468, len: 26, offset: 65, style: 'bolditalic'}, { start: 0x1D482, len: 26, offset: 97, style: 'bolditalic'}, { start: 0x1D49c, len: 26, offset: 65, variant: 'script'}, { start: 0x1D4b6, len: 26, offset: 97, variant: 'script'}, { start: 0x1D4d0, len: 26, offset: 65, variant: 'script', style: 'bold'}, { start: 0x1D4ea, len: 26, offset: 97, variant: 'script', style: 'bold'}, { start: 0x1D504, len: 26, offset: 65, variant: 'fraktur'}, { start: 0x1D51e, len: 26, offset: 97, variant: 'fraktur'}, { start: 0x1D56c, len: 26, offset: 65, variant: 'fraktur', style: 'bold'}, { start: 0x1D586, len: 26, offset: 97, variant: 'fraktur', style: 'bold'}, { start: 0x1D538, len: 26, offset: 65, variant: 'double-struck'}, { start: 0x1D552, len: 26, offset: 97, variant: 'double-struck'}, { start: 0x1D5A0, len: 26, offset: 65, variant: 'sans-serif'}, { start: 0x1D5BA, len: 26, offset: 97, variant: 'sans-serif'}, { start: 0x1D5D4, len: 26, offset: 65, variant: 'sans-serif', style: 'bold'}, { start: 0x1D5EE, len: 26, offset: 97, variant: 'sans-serif', style: 'bold'}, { start: 0x1D608, len: 26, offset: 65, variant: 'sans-serif', style: 'italic'}, { start: 0x1D622, len: 26, offset: 97, variant: 'sans-serif', style: 'italic'}, { start: 0x1D63c, len: 26, offset: 65, variant: 'sans-serif', style: 'bolditalic'}, { start: 0x1D656, len: 26, offset: 97, variant: 'sans-serif', style: 'bolditalic'}, { start: 0x1D670, len: 26, offset: 65, variant: 'monospace'}, { start: 0x1D68A, len: 26, offset: 97, variant: 'monospace'}, { start: 0x1D6A8, len: 25, offset: 0x391, style: 'bold'}, { start: 0x1D6C2, len: 25, offset: 0x3B1, style: 'bold'}, { start: 0x1D6E2, len: 25, offset: 0x391, style: 'italic'}, { start: 0x1D6FC, len: 25, offset: 0x3B1, style: 'italic'}, { start: 0x1D71C, len: 25, offset: 0x391, style: 'bolditalic'}, { start: 0x1D736, len: 25, offset: 0x3B1, style: 'bolditalic'}, { start: 0x1D756, len: 25, offset: 0x391, variant: 'sans-serif', style: 'bold'}, { start: 0x1D770, len: 25, offset: 0x3B1, variant: 'sans-serif', style: 'bold'}, { start: 0x1D790, len: 25, offset: 0x391, variant: 'sans-serif', style: 'bolditalic'}, { start: 0x1D7AA, len: 25, offset: 0x3B1, variant: 'sans-serif', style: 'bolditalic'}, { start: 0x1D7CE, len: 10, offset: 48, variant: '', style: 'bold' }, { start: 0x1D7D8, len: 10, offset: 48, variant: 'double-struck' }, { start: 0x1D7E3, len: 10, offset: 48, variant: 'sans-serif' }, { start: 0x1D7Ec, len: 10, offset: 48, variant: 'sans-serif', style: 'bold' }, { start: 0x1D7F6, len: 10, offset: 48, variant: 'monospace'}, ] function unicodeToMathVariant(char) { let codepoint = char; if (typeof char === 'string') codepoint = char.codePointAt(0); if ((codepoint < 0x1d400 || codepoint > 0x1d7ff) && (codepoint < 0x2100 || codepoint > 0x214f)) { return {char:char}; } // Handle the 'gap' letters by converting them back into their logical range for (const c in MATH_LETTER_EXCEPTIONS) { if (Object.prototype.hasOwnProperty.call(MATH_LETTER_EXCEPTIONS, c)) { if (MATH_LETTER_EXCEPTIONS[c] === codepoint) { codepoint = c; break; } } } for (let i = 0; i < MATH_UNICODE_BLOCKS.length; i++) { if (codepoint >= MATH_UNICODE_BLOCKS[i].start && codepoint < MATH_UNICODE_BLOCKS[i].start + MATH_UNICODE_BLOCKS[i].len) { return { char: String.fromCodePoint(codepoint - MATH_UNICODE_BLOCKS[i].start + MATH_UNICODE_BLOCKS[i].offset), variant: MATH_UNICODE_BLOCKS[i].variant, style: MATH_UNICODE_BLOCKS[i].style, } } } return {char:char}; } /** * Given a character and variant ('bb', 'cal', etc...) * return the corresponding unicode character (a string) * @param {string} char * @param {string} variant * @memberof module:definitions * @private */ function mathVariantToUnicode(char, variant, style) { if (!/[A-Za-z0-9]/.test(char)) return char; if (!variant && !style) return char; const codepoint = char.codePointAt(0); for (let i = 0; i < MATH_UNICODE_BLOCKS.length; i++) { if (!variant || MATH_UNICODE_BLOCKS[i].variant === variant) { if (!style || MATH_UNICODE_BLOCKS[i].style === style) { if (codepoint >= MATH_UNICODE_BLOCKS[i].offset && codepoint < MATH_UNICODE_BLOCKS[i].offset + MATH_UNICODE_BLOCKS[i].len) { const result = MATH_UNICODE_BLOCKS[i].start + codepoint - MATH_UNICODE_BLOCKS[i].offset; return String.fromCodePoint(MATH_LETTER_EXCEPTIONS[result] || result); } } } } return char; } function codepointToLatex(parseMode, cp) { // Codepoint shortcuts have priority over variants // That is, "\N" vs "\mathbb{N}" if (parseMode === 'text') return String.fromCodePoint(cp); let result; if (CODEPOINT_SHORTCUTS[cp]) return CODEPOINT_SHORTCUTS[cp]; const v = unicodeToMathVariant(cp); if (!v.style && !v.variant) return matchCodepoint(parseMode, cp); result = v.char; if (v.variant) { result = '\\' + v.variant + '{' + result + '}'; } if (v.style === 'bold') { result = '\\mathbf{' + result + '}'; } else if (v.style === 'italic') { result = '\\mathit{' + result + '}'; } else if (v.style === 'bolditalic') { result = '\\mathbf{\\mathit{' + result + '}}'; } return '\\mathord{' + result + '}'; } function unicodeStringToLatex(parseMode, s) { let result = ''; for (const cp of s) { result += codepointToLatex(parseMode, cp.codePointAt(0)); } return result; } /** * * @param {string} mode * @param {string} command * @return {boolean} True if command is allowed in the mode * (note that command can also be a single character, e.g. "a") * @memberof module:definitions * @private */ function commandAllowed(mode, command) { if (FUNCTIONS[command] && (mode !== 'text' || FUNCTIONS[command].allowedInText)) { return true; } if ({'text': TEXT_SYMBOLS, 'math': MATH_SYMBOLS}[mode][command]) { return true; } return false; } function getValue(mode, symbol) { if (mode === 'math') { return MATH_SYMBOLS[symbol] && MATH_SYMBOLS[symbol].value ? MATH_SYMBOLS[symbol].value : symbol; } return TEXT_SYMBOLS[symbol] ? TEXT_SYMBOLS[symbol] : symbol; } function getEnvironmentInfo(name) { let result = ENVIRONMENTS[name]; if (!result) { result = { params: '', parser: null, mathstyle: 'displaystyle', tabular: true, colFormat: [], lFence: '.', rFence: '.', // arrayStretch: 1, }; } return result; } /** * @param {string} symbol A command (e.g. '\alpha') or a character (e.g. 'a') * @param {string} parseMode One of 'math' or 'text' * @param {object} macros A macros dictionary * @return {object} An info structure about the symbol, or null * @memberof module:definitions * @private */ function getInfo(symbol, parseMode, macros) { if (symbol.length === 0) return null; let info = null; if (symbol.charAt(0) === '\\') { // This could be a function or a symbol info = FUNCTIONS[symbol]; if (info) { // We've got a match if (parseMode === 'math' || info.allowedInText) return info; // That's a valid function, but it's not allowed in non-math mode, // and we're in non-math mode return null; } if (!info) { // It wasn't a function, maybe it's a symbol? if (parseMode === 'math') { info = MATH_SYMBOLS[symbol]; } else if (TEXT_SYMBOLS[symbol]) { info = { value: TEXT_SYMBOLS[symbol] }; } } if (!info) { // Maybe it's a macro const command = symbol.slice(1); if (macros && macros[command]) { let def = macros[command]; if (typeof def === 'object') { def = def.def; } let argCount = 0; // Let's see if there are arguments in the definition. if (/(^|[^\\])#1/.test(def)) argCount = 1; if (/(^|[^\\])#2/.test(def)) argCount = 2; if (/(^|[^\\])#3/.test(def)) argCount = 3; if (/(^|[^\\])#4/.test(def)) argCount = 4; if (/(^|[^\\])#5/.test(def)) argCount = 5; if (/(^|[^\\])#6/.test(def)) argCount = 6; if (/(^|[^\\])#7/.test(def)) argCount = 7; if (/(^|[^\\])#8/.test(def)) argCount = 8; if (/(^|[^\\])#9/.test(def)) argCount = 9; info = { type: 'group', allowedInText: false, params: [], infix: false } while (argCount >= 1) { info.params.push({ optional: false, type: 'math', defaultValue: null, placeholder: null }); argCount -= 1; } } } } else { if (parseMode === 'math') { info = MATH_SYMBOLS[symbol]; } else if (TEXT_SYMBOLS[symbol]) { info = { value: TEXT_SYMBOLS[symbol] }; } } // Special case `f`, `g` and `h` are recognized as functions. if (info && info.type === 'mord' && (info.value === 'f' || info.value === 'g' || info.value === 'h')) { info.isFunction = true; } return info; } /** * Return an array of suggestion for completing string 's'. * For example, for 'si', it could return ['sin', 'sinh', 'sim', 'simeq', 'sigma'] * Infix operators are excluded, since they are deprecated commands. * @param {string} s * @return {string[]} * @memberof module:definitions * @private */ function suggest(s) { if (s.length <= 1) return []; const result = []; // Iterate over items in the dictionary for (const p in FUNCTIONS) { if (Object.prototype.hasOwnProperty.call(FUNCTIONS, p)) { if (p.startsWith(s) && !FUNCTIONS[p].infix) { result.push({match:p, frequency:FUNCTIONS[p].frequency}); } } } for (const p in MATH_SYMBOLS) { if (Object.prototype.hasOwnProperty.call(MATH_SYMBOLS, p)) { if (p.startsWith(s)) { result.push({match:p, frequency:MATH_SYMBOLS[p].frequency}); } } } result.sort( (a, b) => { if (a.frequency === b.frequency) { return a.match.length - b.match.length; } return (b.frequency || 0) - (a.frequency || 0); }); return result; } // Fonts const MAIN = ''; // The "main" KaTeX font (in fact one of several // depending on the math variant, size, etc...) const AMS = 'ams'; // Some symbols are not in the "main" KaTeX font // or have a different glyph available in the "AMS" // font (`\hbar` and `\hslash` for example). // Type const ORD = 'mord'; const MATHORD = 'mord'; // Ordinary, e.g. '/' const BIN = 'mbin'; // e.g. '+' const REL = 'mrel'; // e.g. '=' const OPEN = 'mopen'; // e.g. '(' const CLOSE = 'mclose'; // e.g. ')' const PUNCT = 'mpunct'; // e.g. ',' const INNER = 'minner'; // for fractions and \left...\right. // const ACCENT = 'accent'; const SPACING = 'spacing'; /** * An argument template has the following syntax: * * <placeholder>:<type>=<default> * * where * - <placeholder> is a string whose value is displayed when the argument * is missing * - <type> is one of 'string', 'color', 'dimen', 'auto', 'text', 'math' * - <default> is the default value if none is provided for an optional * parameter * * @param {string} argTemplate * @param {boolean} isOptional * @memberof module:definitions * @private */ function parseParamTemplateArgument(argTemplate, isOptional) { let r = argTemplate.match(/=(.+)/); let defaultValue = '{}'; let type = 'auto'; let placeholder = null; if (r) { console.assert(isOptional, "Can't provide a default value for required parameters"); defaultValue = r[1]; } // Parse the type (:type) r = argTemplate.match(/:([^=]+)/); if (r) type = r[1].trim(); // Parse the placeholder r = argTemplate.match(/^([^:=]+)/); if (r) placeholder = r[1].trim(); return { optional: isOptional, type: type, defaultValue: defaultValue, placeholder: placeholder } } function parseParamTemplate(paramTemplate) { if (!paramTemplate || paramTemplate.length === 0) return []; let result = []; let params = paramTemplate.split(']'); if (params[0].charAt(0) === '[') { // We found at least one optional parameter. result.push(parseParamTemplateArgument(params[0].slice(1), true)); // Parse the rest for (let i = 1; i <= params.length; i++) { result = result.concat(parseParamTemplate(params[i])); } } else { params = paramTemplate.split('}'); if (params[0].charAt(0) === '{') { // We found a required parameter result.push(parseParamTemplateArgument(params[0].slice(1), false)); // Parse the rest for (let i = 1; i <= params.length; i++) { result = result.concat(parseParamTemplate(params[i])); } } } return result; } function parseArgAsString(arg) { return arg.map(x => x.body).join(''); } /** * Define one or more environments to be used with * \begin{<env-name>}...\end{<env-name>}. * * @param {string|string[]} names * @param {string} params The number and type of required and optional parameters. * @param {object} options * - * @param {function(*)} parser * @memberof module:definitions * @private */ function defineEnvironment(names, params, options, parser) { if (typeof names === 'string') names = [names]; if (!options) options = {}; const parsedParams = parseParamTemplate(params); // Set default values of functions const data = { // 'category' is a global variable keeping track of the // the current category being defined. This value is used // strictly to group items in generateDocumentation(). category: category, // Params: the parameters for this function, an array of // {optional, type, defaultValue, placeholder} params: parsedParams, // Callback to parse the arguments parser: parser, mathstyle: 'displaystyle', tabular: options.tabular || true, colFormat: options.colFormat || [], }; for (const name of names) { ENVIRONMENTS[name] = data; } } /** * Define one of more functions. * * @param {string|string[]} names * @param {string} params The number and type of required and optional parameters. * For example: '{}' defines a single mandatory parameter * '[index=2]{indicand:auto}' defines two params, one optional, one required * @param {object} options * - infix * - allowedInText * @param {function} parseFunction * @memberof module:definitions * @private */ function defineFunction(names, params, options, parseFunction) { if (typeof names === 'string') { names = [names]; } if (!options) options = {}; // Set default values of functions const data = { // 'category' is a global variable keeping track of the // the current category being defined. This value is used // strictly to group items in generateDocumentation(). category: category, // The base font family, if present, indicates that this font family // should always be used to render atom. For example, functions such // as "sin", etc... are always drawn in a roman font, // regardless of the font styling a user may specify. baseFontFamily: options.fontFamily, // The parameters for this function, an array of // {optional, type, defaultValue, placeholder} params: parseParamTemplate(params), allowedInText: !!options.allowedInText, infix: !!options.infix, parse: parseFunction }; for (const name of names) { FUNCTIONS[name] = data; } } category = 'Environments'; /* <columns> ::= <column>*<line> <column> ::= <line>('l'|'c'|'r') <line> ::= '|' | '||' | '' 'math', frequency 0 'displaymath', frequency 8 'equation' centered, numbered frequency 8 'subequations' with an 'equation' environment, appends a letter to eq no frequency 1 'array', {columns:text} cells are textstyle math no fence 'eqnarray' DEPRECATED see http://www.tug.org/pracjourn/2006-4/madsen/madsen.pdf {rcl} first and last cell in each row is displaystyle math each cell has a margin of \arraycolsep Each line has a eqno frequency 7 'theorem' text mode. Prepends in bold 'Theorem <counter>', then body in italics. 'multline' single column first row left aligned, last right aligned, others centered last line has an eqn. counter. multline* will omit the counter no output if inside an equation 'gather' at most two columns first column centered, second column right aligned frequency 1 'gathered' must be in equation environment single column, centered frequency: COMMON optional argument: [b], [t] to vertical align 'align' multiple columns, alternating rl there is some 'space' (additional column?) between each pair each line is numbered (except when inside an equation environment) there is an implicit {} at the beginning of left columns 'aligned' must be in equation environment frequency: COMMON @{}r@{}l@{\quad}@{}r@{}l@{} 'split' must be in an equation environment, two columns, additional columns are interpreted as line breaks first column is right aligned, second column is left aligned entire construct is numbered (as opposed to 'align' where each line is numbered) frequency: 0 'alignedat' From AMSMath: ---The alignedat environment was changed to take two arguments rather than one: a mandatory argument (as formerly) specifying the number of align structures, and a new optional one specifying the placement of the environment (parallel to the optional argument of aligned). However, aligned is simpler to use, allowing any number of aligned structures automatically, and therefore the use of alignedat is deprecated. 'alignat' {pairs:number} {rl} alternating as many times as indicated by <pairs> arg no space between column pairs (unlike align) there is an implicit {} at the beginning of left columns frequency: 0 'flalign' multiple columns alternate rl third column further away than align...? frequency: 0 'matrix' at most 10 columns cells centered no fence no colsep at beginning or end (mathtools package add an optional arg for the cell alignment) frequency: COMMON 'pmatrix' fence: () frequency: COMMON 'bmatrix' fence: [] frequency: COMMON 'Bmatrix' fence: {} frequency: 237 'vmatrix' fence: \vert frequency: 368 'Vmatrix' fence: \Vert frequency: 41 'smallmatrix' displaystyle: scriptstyle (?) frequency: 279 'cases' frequency: COMMON l@{2}l 'center' text mode only? frequency: ? */ // See https://en.wikibooks.org/wiki/LaTeX/Mathematics // and http://www.ele.uri.edu/faculty/vetter/Other-stuff/latex/Mathmode.pdf /* The star at the end of the name of a displayed math environment causes that the formula lines won't be numbered. Otherwise they would automatically get a number. \notag will also turn off the numbering. \shoveright and \shoveleft will force alignment of a line The only difference between align and equation is the spacing of the formulas. You should attempt to use equation when possible, and align when you have multi-line formulas. Equation will have space before/after < 1em if line before/after is short enough. Also: equation throws an error when you have an & inside the environment, so look out for that when converting between the two. Whereas align produces a structure whose width is the full line width, aligned gives a width that is the actual width of the contents, thus it can be used as a component in a containing expression, e.g. for putting the entire alignment in a parenthesis */ defineEnvironment('math', '', {frequency: 0}, function() { return { mathstyle: 'textstyle'}; }); defineEnvironment('displaymath', '', { frequency: 8, }, function() { return { mathstyle: 'displaystyle', }; }); // defineEnvironment('text', '', { // frequency: 0, // }, function(name, args) { // return { // mathstyle: 'text', // @todo: not quite right, not a style, a parsemode... // }; // }); defineEnvironment('array', '{columns:colspec}', { frequency: COMMON }, function(name, args) { return { colFormat: args[0], mathstyle: 'textstyle', }; }); defineEnvironment('eqnarray', '', {}, function() { return { }; }); defineEnvironment('equation', '', {}, function() { return { colFormat: [{ align: 'c'}] }; }); defineEnvironment('subequations', '', {}, function() { return { colFormat: [{ align: 'c'}] }; }); // Note spelling: MULTLINE, not multiline. defineEnvironment('multline', '', {}, function() { return { firstRowFormat: [{align: 'l'}], colFormat: [{align: 'c'}], lastRowFormat: [{align: 'r'}], }; }); // An AMS-Math environment // See amsmath.dtx:3565 // Note that some versions of AMS-Math have a gap on the left. // More recent version suppresses that gap, but have an option to turn it back on // for backward compatibility. defineEnvironment(['align', 'aligned'], '', {}, function(name, args, array) { let colCount = 0; for (const row of array) { colCount = Math.max(colCount, row.length); } const colFormat = [ { gap: 0, } , { align: 'r', } , { gap: 0, } , { align: 'l', } , ]; let i = 2; while ( i < colCount) { colFormat.push({gap:1}); colFormat.push({align:'r'}); colFormat.push({gap:0}); colFormat.push({align:'l'}); i += 2; } colFormat.push({gap: 0}); return { colFormat: colFormat, jot: 0.3, // Jot is an extra gap between lines of numbered equation. // It's 3pt by default in LaTeX (ltmath.dtx:181) }; }); // defineEnvironment('alignat', '', {}, function(name, args) { // return { // }; // }); // defineEnvironment('flalign', '', {}, function(name, args) { // return { // }; // }); defineEnvironment('split', '', {}, function() { return { }; }); defineEnvironment(['gather', 'gathered'], '', {}, function() { // An AMS-Math environment // % The \env{gathered} environment is for several lines that are // % centered independently. // From amstex.sty // \newenvironment{gathered}[1][c]{% // \relax\ifmmode\else\nonmatherr@{\begin{gathered}}\fi // \null\,% // \if #1t\vtop \else \if#1b\vbox \else \vcenter \fi\fi // \bgroup\Let@\restore@math@cr // \ifinany@\else\openup\jot\fi\ialign // \bgroup\hfil\strut@$\m@th\displaystyle##$\hfil\crcr return { colFormat: [{gap:.25}, { align: 'c', }, {gap:0}], jot: .3 }; }); // defineEnvironment('cardinality', '', {}, function() { // const result = {}; // result.mathstyle = 'textstyle'; // result.lFence = '|'; // result.rFence = '|'; // return result; // }); defineEnvironment(['matrix', 'pmatrix', 'bmatrix', 'Bmatrix', 'vmatrix', 'Vmatrix', 'smallmatrix', 'matrix*', 'pmatrix*', 'bmatrix*', 'Bmatrix*', 'vmatrix*', 'Vmatrix*', 'smallmatrix*'], '[columns:colspec]', {}, function(name, args) { // From amstex.sty: // \def\matrix{\hskip -\arraycolsep\array{*\c@MaxMatrixCols c}} // \def\endmatrix{\endarray \hskip -\arraycolsep} const result = {}; result.mathstyle = 'textstyle'; switch (name) { case 'pmatrix': case 'pmatrix*': result.lFence = '('; result.rFence = ')'; break; case 'bmatrix': case 'bmatrix*': result.lFence = '['; result.rFence = ']'; break; case 'Bmatrix': case 'Bmatrix*': result.lFence = '\\lbrace'; result.rFence = '\\rbrace'; break; case 'vmatrix': case 'vmatrix*': result.lFence = '\\vert'; result.rFence = '\\vert'; break; case 'Vmatrix': case 'Vmatrix*': result.lFence = '\\Vert'; result.rFence = '\\Vert'; break; case 'smallmatrix': case 'smallmatrix*': result.mathstyle = 'scriptstyle'; break; case 'matrix': case 'matrix*': // Specifying a fence, even a null fence, // will prevent the insertion of an initial and final gap result.lFence = '.'; result.rFence = '.'; break; default: } result.colFormat = args[0] || [{align:'c'}, {align:'c'}, {align:'c'}, {align:'c'}, {align:'c'}, {align:'c'}, {align:'c'}, {align:'c'}, {align:'c'}, {align:'c'}]; return result; }); defineEnvironment('cases', '', {}, function() { // From amstex.sty: // \def\cases{\left\{\def\arraystretch{1.2}\hskip-\arraycolsep // \array{l@{\quad}l}} // \def\endcases{\endarray\hskip-\arraycolsep\right.} // From amsmath.dtx // \def\env@cases{% // \let\@ifnextchar\new@ifnextchar // \left\lbrace // \def\arraystretch{1.2}% // \array{@{}l@{\quad}l@{}}% return { arraystretch: 1.2, lFence: '\\lbrace', rFence: '.', colFormat: [ { align: 'l', } , { gap: 1, } , { align: 'l', } ] } }); defineEnvironment('theorem', '', {}, function() { return { }; }); defineEnvironment('center', '', {}, function() { return {colFormat: [{align:'c'}]}; }); category = ''; // Simple characters allowed in math mode defineSymbols('0123456789/@.'); defineSymbolRange(0x0041, 0x005A); // a-z defineSymbolRange(0x0061, 0x007A); // A-Z category = 'Trigonometry'; defineFunction([ '\\arcsin', '\\arccos', '\\arctan', '\\arctg', '\\arcctg', '\\arg', '\\ch', '\\cos', '\\cosec', '\\cosh', '\\cot', '\\cotg', '\\coth', '\\csc', '\\ctg', '\\cth', '\\sec', '\\sin', '\\sinh', '\\sh', '\\tan', '\\tanh', '\\tg', '\\th',], '', null, function(name) { return { type: 'mop', limits: 'nolimits', symbol: false, isFunction: true, body: name.slice(1), baseFontFamily: 'cmr' }; }) frequency(SUPERCOMMON, '\\cos', '\\sin', '\\tan'); frequency(UNCOMMON, '\\arcsin', '\\arccos', '\\arctan', '\\arctg', '\\arcctg', '\\arcsec', '\\arccsc'); frequency(UNCOMMON, '\\arsinh', '\\arccosh', '\\arctanh', '\\arcsech', '\\arccsch'); frequency(UNCOMMON, '\\arg', '\\ch', '\\cosec', '\\cosh', '\\cot', '\\cotg', '\\coth', '\\csc', '\\ctg', '\\cth', '\\lg', '\\lb', '\\sec', '\\sinh', '\\sh', '\\tanh', '\\tg', '\\th'); category = 'Functions'; defineFunction([ '\\deg', '\\dim', '\\exp', '\\hom', '\\ker', '\\lb', '\\lg', '\\ln', '\\log'], '', null, function(name) { return { type: 'mop', limits: 'nolimits', symbol: false, isFunction: true, body: name.slice(1), baseFontFamily: 'cmr' }; }) frequency(SUPERCOMMON, '\\ln', '\\log', '\\exp'); frequency(292, '\\hom'); frequency(COMMON, '\\dim'); frequency(COMMON, '\\ker', '\\deg'); // >2,000 defineFunction(['\\lim', '\\mod'], '', null, function(name) { return { type: 'mop', limits: 'limits', symbol: false, body: name.slice(1), baseFontFamily: 'cmr' }; }) defineFunction(['\\det', '\\max', '\\min'], '', null, function(name) { return { type: 'mop', limits: 'limits', symbol: false, isFunction: true, body: name.slice(1), baseFontFamily: 'cmr' }; }) frequency(SUPERCOMMON, '\\lim'); frequency(COMMON, '\\det'); frequency(COMMON, '\\mod'); frequency(COMMON, '\\min'); frequency(COMMON, '\\max'); category = 'Decoration'; // A box of the width and height defineFunction('\\rule', '[raise:dimen]{width:dimen}{thickness:dimen}', null, function(name, args) { return { type: 'rule', shift: args[0], width: args[1], height: args[2], }; }); defineFunction('\\color', '{:color}', {allowedInText: true}, (_name, args) => { return { color: args[0] } }); // From the xcolor package. // As per xcolor, this command does not set the mode to text // (unlike what its name might suggest) defineFunction('\\textcolor', '{:color}{content:auto*}', {allowedInText: true}, (_name, args) => { return { color: args[0] }; }); frequency(3, '\\textcolor'); // An overline defineFunction('\\overline', '{:auto}', null, function(name, args) { return { type: 'line', position: 'overline', skipBoundary: true, body: args[0], }; }); frequency(COMMON, '\\overline'); // > 2,000 defineFunction('\\underline', '{:auto}', null, function(name, args) { return { type: 'line', position: 'underline', skipBoundary: true, body: args[0], }; }); frequency(COMMON, '\\underline'); // > 2,000 defineFunction('\\overset', '{annotation:auto}{symbol:auto}', null, function(name, args) { return { type: 'overunder', overscript: args[0], skipBoundary: true, body: args[1]}; }); frequency(COMMON, '\\overset'); // > 2,000 defineFunction('\\underset', '{annotation:auto}{symbol:auto}', null, function(name, args) { return { type: 'overunder', underscript: args[0], skipBoundary: true, body: args[1]}; }); frequency(COMMON, '\\underset'); // > 2,000 defineFunction(['\\stackrel', '\\stackbin'], '{annotation:auto}{symbol:auto}', null, function(name, args) { return { type: 'overunder', overscript: args[0], skipBoundary: true, body: args[1], mathtype: name === '\\stackrel' ? 'mrel' : 'mbin', }; }); frequency(COMMON, '\\stackrel'); // > 2,000 frequency(0, '\\stackbin'); defineFunction('\\rlap', '{:auto}', null, function(name, args) { return { type: 'overlap', align: 'right', skipBoundary: true, body: args[0], }; }); frequency(270, '\\rlap'); defineFunction('\\llap', '{:auto}', null, function(name, args) { return { type: 'overlap', align: 'left', skipBoundary: true, body: args[0], }; }); frequency(18, '\\llap'); defineFunction('\\mathrlap', '{:auto}', null, function(name, args) { return { type: 'overlap', mode: 'math', align: 'right', skipBoundary: true, body: args[0], }; }); frequency(CRYPTIC, '\\mathrlap'); defineFunction('\\mathllap', '{:auto}', null, function(name, args) { return { type: 'overlap', mode: 'math', align: 'left', skipBoundary: true, body: args[0], }; }); frequency(CRYPTIC, '\\mathllap'); // Can be preceded by e.g. '\fboxsep=4pt' (also \fboxrule) // Note: // - \boxed: sets content in displaystyle mode (@todo: should change type of argument) // equivalent to \fbox{$$<content>$$} // - \fbox: sets content in 'auto' mode (frequency 777) // - \framebox[<width>][<alignment>]{<content>} (<alignment> := 'c'|'t'|'b' (center, top, bottom) (frequency 28) // @todo defineFunction('\\boxed', '{content:math}', null, function(name, args) { return { type: 'box', framecolor: 'black', skipBoundary: true, body: args[0] } } ) frequency(1236, '\\boxed'); defineFunction('\\colorbox', '{background-color:color}{content:auto}', {allowedInText: true}, function(name, args) { return { type: 'box', backgroundcolor: args[0], skipBoundary: true, body: args[1] } } ) frequency(CRYPTIC, '\\colorbox'); defineFunction('\\fcolorbox', '{frame-color:color}{background-color:color}{content:auto}', {allowedInText: true}, function(name, args) { return { type: 'box', framecolor: args[0], backgroundcolor: args[1], skipBoundary: true, body: args[2] } } ) frequency(CRYPTIC, '\\fcolorbox'); // \bbox, MathJax extension // The first argument is a CSS border property shorthand, e.g. // \bbox[red], \bbox[5px,border:2px solid red] // The MathJax syntax is // arglist ::= <arg>[,<arg>[,<arg>]] // arg ::= [<background:color>|<padding:dimen>|<style>] // style ::= 'border:' <string> defineFunction('\\bbox', '[:bbox]{body:auto}', {allowedInText: true}, function(name, args) { if (args[0]) { return { type: 'box', padding: args[0].padding, border: args[0].border, backgroundcolor: args[0].backgroundcolor, skipBoundary: true, body: args[1] } } return { type: 'box', skipBoundary: true, body: args[1] } } ) frequency(CRYPTIC, '\\bbox'); // \enclose, a MathJax extension mapping to the MathML `menclose` tag. // The first argument is a comma delimited list of notations, as defined // here: https://developer.mozilla.org/en-US/docs/Web/MathML/Element/menclose // The second, optional, specifies the style to use for the notations. defineFunction('\\enclose', '{notation:string}[style:string]{body:auto}', null, function(name, args) { let notations = args[0] || []; const result = { type: 'enclose', strokeColor: 'currentColor', strokeWidth: 1, strokeStyle: 'solid', backgroundcolor: 'transparent', padding: 'auto', shadow: 'auto', captureSelection: true, // Do not let children be selected body: args[2] } // Extract info from style string if (args[1]) { // Split the string by comma delimited sub-strings, ignoring commas // that may be inside (). For example"x, rgb(a, b, c)" would return // ['x', 'rgb(a, b, c)'] const styles = args[1].split(/,(?![^(]*\)(?:(?:[^(]*\)){2})*[^"]*$)/); for (const s of styles) { const shorthand = s.match(/\s*(\S+)\s+(\S+)\s+(.*)/); if (shorthand) { result.strokeWidth = FontMetrics.toPx(shorthand[1], 'px'); if (!isFinite(result.strokeWidth)) { result.strokeWidth = 1; } result.strokeStyle = shorthand[2]; result.strokeColor = shorthand[3]; } else { const attribute = s.match(/\s*([a-z]*)\s*=\s*"(.*)"/); if (attribute) { if (attribute[1] === 'mathbackground') { result.backgroundcolor = attribute[2]; } else if (attribute[1] === 'mathcolor') { result.strokeColor = attribute[2]; } else if (attribute[1] === 'padding') { result.padding = FontMetrics.toPx(attribute[2], 'px'); } else if (attribute[1] === 'shadow') { result.shadow = attribute[2]; } } } } if (result.strokeStyle === 'dashed') { result.svgStrokeStyle = '5,5'; } else if (result.strokeStyle === 'dotted') { result.svgStrokeStyle = '1,5'; } } result.borderStyle = result.strokeWidth + 'px ' + result.strokeStyle + ' ' + result.strokeColor; // Normalize the list of notations. notations = notations.toString().split(/[, ]/). filter(v => v.length > 0).map(v => v.toLowerCase()); result.notation = {}; for (const notation of notations) { result.notation[notation] = true; } if (result.notation['updiagonalarrow']) result.notation['updiagonalstrike'] = false; if (result.notation['box']) { result.notation['left'] = false; result.notation['right'] = false; result.notation['bottom'] = false; result.notation['top'] = false; } return result; } ) frequency(CRYPTIC, '\\enclose'); defineFunction('\\cancel', '{body:auto}', null, function(name, args) { return { type: 'enclose', strokeColor: 'currentColor', strokeWidth: 1, strokeStyle: 'solid', borderStyle: '1px solid currentColor', backgroundcolor: 'transparent', padding: 'auto', shadow: 'auto', notation: {"updiagonalstrike": true}, body: args[0] } } ) defineFunction('\\bcancel', '{body:auto}', null, function(name, args) { return { type: 'enclose', strokeColor: 'currentColor', strokeWidth: 1, strokeStyle: 'solid', borderStyle: '1px solid currentColor', backgroundcolor: 'transparent', padding: 'auto', shadow: 'auto', notation: {"downdiagonalstrike": true}, body: args[0] } } ) defineFunction('\\xcancel', '{body:auto}', null, function(name, args) { return { type: 'enclose', strokeColor: 'currentColor', strokeWidth: 1, strokeStyle: 'solid', borderStyle: '1px solid currentColor', backgroundcolor: 'transparent', padding: 'auto', shadow: 'auto', notation: {"updiagonalstrike": true, "downdiagonalstrike": true}, body: args[0] } } ) frequency(CRYPTIC, '\\cancel', '\\bcancel', '\\xcancel'); category = 'Styling'; // Size defineFunction([ '\\tiny', '\\scriptsize', '\\footnotesize', '\\small', '\\normalsize', '\\large', '\\Large', '\\LARGE', '\\huge', '\\Huge' ], '', {allowedInText: true}, function(name, _args) { return { fontSize: { 'tiny': 'size1', 'scriptsize': 'size2', 'footnotesize': 'size3', 'small': 'size4', 'normalsize': 'size5', 'large': 'size6', 'Large': 'size7', 'LARGE': 'size8', 'huge': 'size9', 'Huge': 'size10' }[name.slice(1)] } } ) // SERIES: weight defineFunction('\\fontseries', '{:text}', {allowedInText: true}, (_name, args) => { return { fontSeries: parseArgAsString(args[0]) } }); defineFunction('\\bf', '', {allowedInText: true}, (_name, _args) => { return { fontSeries: 'b' } }); defineFunction('\\bm', '{:math*}', {allowedInText: true}, (_name, _args) => { return { fontSeries: 'b' } }); // Note: switches to math mode defineFunction('\\bold', '', {allowedInText: true}, (_name, _args) => { return { mode: 'math', fontSeries: 'b' } }); defineFunction(['\\mathbf', '\\boldsymbol'], '{:math*}', {allowedInText: true}, (_name, _args) => { return { mode: 'math', fontSeries: 'b', fontShape: 'n' } }); defineFunction('\\bfseries', '', {allowedInText: true}, (_name, _args) => { return {