mathlive
Version:
Render and edit beautifully typeset math
1,675 lines (1,402 loc) • 112 kB
JavaScript
/**
* 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 {