katex
Version:
Fast math typesetting for the web.
1,131 lines (1,046 loc) • 38.4 kB
text/typescript
import {makeFragment, makeLineSpan, makeSpan, makeVList} from "../buildCommon";
import Style from "../Style";
import defineEnvironment from "../defineEnvironment";
import {parseCD} from "./cd";
import defineFunction from "../defineFunction";
import defineMacro from "../defineMacro";
import {MathNode} from "../mathMLTree";
import ParseError from "../ParseError";
import {assertNodeType, assertSymbolNodeType} from "../parseNode";
import {checkSymbolNodeType} from "../parseNode";
import {Token} from "../Token";
import {calculateSize, makeEm} from "../units";
import * as html from "../buildHTML";
import * as mml from "../buildMathML";
import type Parser from "../Parser";
import type {ParseNode, AnyParseNode} from "../parseNode";
import type {StyleStr, Mode} from "../types";
import type {HtmlBuilder, MathMLBuilder} from "../defineFunction";
import type {HtmlDomNode} from "../domTree";
type EnvContextLike = {
parser: Parser;
envName: string;
mode: Mode;
};
// Data stored in the ParseNode associated with the environment.
export type AlignSpec = {type: "separator", separator: string} | {
type: "align";
align: string;
pregap?: number;
postgap?: number;
};
// Type to indicate column separation in MathML
export type ColSeparationType = "align" | "alignat" | "gather" | "small" | "CD";
// Helper functions
function getHLines(parser: Parser): boolean[] {
// Return an array. The array length = number of hlines.
// Each element in the array tells if the line is dashed.
const hlineInfo = [];
parser.consumeSpaces();
let nxt = parser.fetch().text;
if (nxt === "\\relax") { // \relax is an artifact of the \cr macro below
parser.consume();
parser.consumeSpaces();
nxt = parser.fetch().text;
}
while (nxt === "\\hline" || nxt === "\\hdashline") {
parser.consume();
hlineInfo.push(nxt === "\\hdashline");
parser.consumeSpaces();
nxt = parser.fetch().text;
}
return hlineInfo;
}
const validateAmsEnvironmentContext = (context: EnvContextLike) => {
const settings = context.parser.settings;
if (!settings.displayMode) {
throw new ParseError(`{${context.envName}} can be used only in` +
` display mode.`);
}
};
const gatherEnvironments = new Set(["gather", "gather*"]);
// autoTag (an argument to parseArray) can be one of three values:
// * undefined: Regular (not-top-level) array; no tags on each row
// * true: Automatic equation numbering, overridable by \tag
// * false: Tags allowed on each row, but no automatic numbering
// This function *doesn't* work with the "split" environment name.
function getAutoTag(name: string): boolean | null | undefined {
if (!name.includes("ed")) {
return !name.includes("*");
}
// return undefined;
}
/**
* Parse the body of the environment, with rows delimited by \\ and
* columns delimited by &, and create a nested list in row-major order
* with one group per cell. If given an optional argument style
* ("text", "display", etc.), then each cell is cast into that style.
*/
function parseArray(
parser: Parser,
{
hskipBeforeAndAfter,
addJot,
cols,
arraystretch,
colSeparationType,
autoTag,
singleRow,
emptySingleRow,
maxNumCols,
leqno,
}: {
hskipBeforeAndAfter?: boolean;
addJot?: boolean;
cols?: AlignSpec[];
arraystretch?: number;
colSeparationType?: ColSeparationType;
autoTag?: boolean | null | undefined;
singleRow?: boolean;
emptySingleRow?: boolean;
maxNumCols?: number;
leqno?: boolean;
},
style: StyleStr,
): ParseNode<"array"> {
parser.gullet.beginGroup();
if (!singleRow) {
// \cr is equivalent to \\ without the optional size argument (see below)
// TODO: provide helpful error when \cr is used outside array environment
parser.gullet.macros.set("\\cr", "\\\\\\relax");
}
// Get current arraystretch if it's not set by the environment
if (!arraystretch) {
const stretch = parser.gullet.expandMacroAsText("\\arraystretch");
if (stretch == null) {
// Default \arraystretch from lttab.dtx
arraystretch = 1;
} else {
arraystretch = parseFloat(stretch);
if (!arraystretch || arraystretch < 0) {
throw new ParseError(`Invalid \\arraystretch: ${stretch}`);
}
}
}
// Start group for first cell
parser.gullet.beginGroup();
let row: AnyParseNode[] = [];
const body: AnyParseNode[][] = [row];
const rowGaps = [];
const hLinesBeforeRow = [];
const tags: Array<AnyParseNode[] | boolean> | undefined =
(autoTag != null ? [] : undefined);
// amsmath uses \global\@eqnswtrue and \global\@eqnswfalse to represent
// whether this row should have an equation number. Simulate this with
// a \@eqnsw macro set to 1 or 0.
function beginRow() {
if (autoTag) {
parser.gullet.macros.set("\\@eqnsw", "1", true);
}
}
function endRow() {
if (tags) {
if (parser.gullet.macros.get("\\df@tag")) {
tags.push(parser.subparse([new Token("\\df@tag")]));
parser.gullet.macros.set("\\df@tag", undefined, true);
} else {
tags.push(Boolean(autoTag) &&
parser.gullet.macros.get("\\@eqnsw") === "1");
}
}
}
beginRow();
// Test for \hline at the top of the array.
hLinesBeforeRow.push(getHLines(parser));
while (true) { // eslint-disable-line no-constant-condition
// Parse each cell in its own group (namespace)
const cellBody = parser.parseExpression(false, singleRow ? "\\end" : "\\\\");
parser.gullet.endGroup();
parser.gullet.beginGroup();
let cell: AnyParseNode = {
type: "ordgroup",
mode: parser.mode,
body: cellBody,
};
if (style) {
cell = {
type: "styling",
mode: parser.mode,
style,
body: [cell],
};
}
row.push(cell);
const next = parser.fetch().text;
if (next === "&") {
if (maxNumCols && row.length === maxNumCols) {
if (singleRow || colSeparationType) {
// {equation} or {split}
throw new ParseError("Too many tab characters: &",
parser.nextToken);
} else {
// {array} environment
parser.settings.reportNonstrict("textEnv", "Too few columns " +
"specified in the {array} column argument.");
}
}
parser.consume();
} else if (next === "\\end") {
endRow();
// Arrays terminate newlines with `\crcr` which consumes a `\cr` if
// the last line is empty. However, AMS environments keep the
// empty row if it's the only one.
// NOTE: Currently, `cell` is the last item added into `row`.
if (row.length === 1 && cell.type === "styling" &&
cell.body.length === 1 && cell.body[0].type === "ordgroup" &&
cell.body[0].body.length === 0 &&
(body.length > 1 || !emptySingleRow)) {
body.pop();
}
if (hLinesBeforeRow.length < body.length + 1) {
hLinesBeforeRow.push([]);
}
break;
} else if (next === "\\\\") {
parser.consume();
let size;
// \def\Let@{\let\\\math@cr}
// \def\math@cr{...\math@cr@}
// \def\math@cr@{\new@ifnextchar[\math@cr@@{\math@cr@@[\z@]}}
// \def\math@cr@@[#1]{...\math@cr@@@...}
// \def\math@cr@@@{\cr}
if (parser.gullet.future().text !== " ") {
size = parser.parseSizeGroup(true);
}
rowGaps.push(size ? size.value : null);
endRow();
// check for \hline(s) following the row separator
hLinesBeforeRow.push(getHLines(parser));
row = [];
body.push(row);
beginRow();
} else {
throw new ParseError("Expected & or \\\\ or \\cr or \\end",
parser.nextToken);
}
}
// End cell group
parser.gullet.endGroup();
// End array group defining \cr
parser.gullet.endGroup();
return {
type: "array",
mode: parser.mode,
addJot,
arraystretch,
body,
cols,
rowGaps,
hskipBeforeAndAfter,
hLinesBeforeRow,
colSeparationType,
tags,
leqno,
};
}
// Decides on a style for cells in an array according to whether the given
// environment name starts with the letter 'd'.
function dCellStyle(envName: string): StyleStr {
if (envName.slice(0, 1) === "d") {
return "display";
} else {
return "text";
}
}
type Outrow = {
[idx: number]: any;
height: number;
depth: number;
pos: number;
};
const htmlBuilder: HtmlBuilder<"array"> = function(group, options) {
let r;
let c;
const nr = group.body.length;
const hLinesBeforeRow = group.hLinesBeforeRow;
let nc = 0;
const body = new Array(nr);
const hlines: Array<{pos: number; isDashed: boolean}> = [];
const ruleThickness = Math.max(
// From LaTeX \showthe\arrayrulewidth. Equals 0.04 em.
options.fontMetrics().arrayRuleWidth,
options.minRuleThickness, // User override.
);
// Horizontal spacing
const pt = 1 / options.fontMetrics().ptPerEm;
let arraycolsep = 5 * pt; // default value, i.e. \arraycolsep in article.cls
if (group.colSeparationType && group.colSeparationType === "small") {
// We're in a {smallmatrix}. Default column space is \thickspace,
// i.e. 5/18em = 0.2778em, per amsmath.dtx for {smallmatrix}.
// But that needs adjustment because LaTeX applies \scriptstyle to the
// entire array, including the colspace, but this function applies
// \scriptstyle only inside each element.
const localMultiplier = options.havingStyle(Style.SCRIPT).sizeMultiplier;
arraycolsep = 0.2778 * (localMultiplier / options.sizeMultiplier);
}
// Vertical spacing
const baselineskip = group.colSeparationType === "CD"
? calculateSize({number: 3, unit: "ex"}, options)
: 12 * pt; // see size10.clo
// Default \jot from ltmath.dtx
// TODO(edemaine): allow overriding \jot via \setlength (#687)
const jot = 3 * pt;
const arrayskip = group.arraystretch * baselineskip;
const arstrutHeight = 0.7 * arrayskip; // \strutbox in ltfsstrc.dtx and
const arstrutDepth = 0.3 * arrayskip; // \@arstrutbox in lttab.dtx
let totalHeight = 0;
// Set a position for \hline(s) at the top of the array, if any.
function setHLinePos(hlinesInGap: boolean[]) {
for (let i = 0; i < hlinesInGap.length; ++i) {
if (i > 0) {
totalHeight += 0.25;
}
hlines.push({pos: totalHeight, isDashed: hlinesInGap[i]});
}
}
setHLinePos(hLinesBeforeRow[0]);
for (r = 0; r < group.body.length; ++r) {
const inrow = group.body[r];
let height = arstrutHeight; // \@array adds an \@arstrut
let depth = arstrutDepth; // to each tow (via the template)
if (nc < inrow.length) {
nc = inrow.length;
}
const outrow: Outrow = (new Array(inrow.length) as any);
for (c = 0; c < inrow.length; ++c) {
const elt = html.buildGroup(inrow[c], options);
if (depth < elt.depth) {
depth = elt.depth;
}
if (height < elt.height) {
height = elt.height;
}
outrow[c] = elt;
}
const rowGap = group.rowGaps[r];
let gap = 0;
if (rowGap) {
gap = calculateSize(rowGap, options);
if (gap > 0) { // \@argarraycr
gap += arstrutDepth;
if (depth < gap) {
depth = gap; // \@xargarraycr
}
gap = 0;
}
}
// In AMS multiline environments such as aligned and gathered, rows
// correspond to lines that have additional \jot added between lines
// via \openup.
// We simulate this by adding \jot depth to each row except the last.
if (group.addJot && r < group.body.length - 1) {
depth += jot;
}
outrow.height = height;
outrow.depth = depth;
totalHeight += height;
outrow.pos = totalHeight;
totalHeight += depth + gap; // \@yargarraycr
body[r] = outrow;
// Set a position for \hline(s), if any.
setHLinePos(hLinesBeforeRow[r + 1]);
}
const offset = totalHeight / 2 + options.fontMetrics().axisHeight;
const colDescriptions = group.cols || [];
const cols: HtmlDomNode[] = [];
let colSep;
let colDescrNum;
const tagSpans: Array<{
type: "elem";
elem: HtmlDomNode;
shift: number;
}> = [];
if (group.tags && group.tags.some(tag => tag)) {
// An environment with manual tags and/or automatic equation numbers.
// Create node(s), the latter of which trigger CSS counter increment.
for (r = 0; r < nr; ++r) {
const rw = body[r];
const shift = rw.pos - offset;
const tag = group.tags[r];
let tagSpan;
if (tag === true) { // automatic numbering
tagSpan = makeSpan(["eqn-num"], [], options);
} else if (tag === false) {
// \nonumber/\notag or starred environment
tagSpan = makeSpan([], [], options);
} else { // manual \tag
tagSpan = makeSpan([],
html.buildExpression(tag, options, true), options);
}
tagSpan.depth = rw.depth;
tagSpan.height = rw.height;
tagSpans.push({type: "elem", elem: tagSpan, shift});
}
}
for (c = 0, colDescrNum = 0;
// Continue while either there are more columns or more column
// descriptions, so trailing separators don't get lost.
c < nc || colDescrNum < colDescriptions.length;
++c, ++colDescrNum) {
let colDescr: AlignSpec | undefined = colDescriptions[colDescrNum];
let firstSeparator = true;
while (colDescr?.type === "separator") {
// If there is more than one separator in a row, add a space
// between them.
if (!firstSeparator) {
colSep = makeSpan(["arraycolsep"], []);
colSep.style.width =
makeEm(options.fontMetrics().doubleRuleSep);
cols.push(colSep);
}
if (colDescr.separator === "|" || colDescr.separator === ":") {
const lineType = colDescr.separator === "|" ? "solid" : "dashed";
const separator = makeSpan(["vertical-separator"], [], options);
separator.style.height = makeEm(totalHeight);
separator.style.borderRightWidth = makeEm(ruleThickness);
separator.style.borderRightStyle = lineType;
separator.style.margin = `0 ${makeEm(-ruleThickness / 2)}`;
const shift = totalHeight - offset;
if (shift) {
separator.style.verticalAlign = makeEm(-shift);
}
cols.push(separator);
} else {
throw new ParseError(
"Invalid separator type: " + colDescr.separator);
}
colDescrNum++;
colDescr = colDescriptions[colDescrNum];
firstSeparator = false;
}
if (c >= nc) {
continue;
}
let sepwidth;
if (c > 0 || group.hskipBeforeAndAfter) {
sepwidth = colDescr?.pregap ?? arraycolsep;
if (sepwidth !== 0) {
colSep = makeSpan(["arraycolsep"], []);
colSep.style.width = makeEm(sepwidth);
cols.push(colSep);
}
}
const colElems: Array<{
type: "elem";
elem: HtmlDomNode;
shift: number;
}> = [];
for (r = 0; r < nr; ++r) {
const row = body[r];
const elem = row[c];
if (!elem) {
continue;
}
const shift = row.pos - offset;
elem.depth = row.depth;
elem.height = row.height;
colElems.push({type: "elem", elem: elem, shift: shift});
}
const colVList = makeVList({
positionType: "individualShift",
children: colElems,
}, options);
const colSpan = makeSpan(
["col-align-" + (colDescr?.align || "c")],
[colVList],
);
cols.push(colSpan);
if (c < nc - 1 || group.hskipBeforeAndAfter) {
sepwidth = colDescr?.postgap ?? arraycolsep;
if (sepwidth !== 0) {
colSep = makeSpan(["arraycolsep"], []);
colSep.style.width = makeEm(sepwidth);
cols.push(colSep);
}
}
}
let tableBody: HtmlDomNode = makeSpan(["mtable"], cols);
// Add \hline(s), if any.
if (hlines.length > 0) {
const line = makeLineSpan("hline", options, ruleThickness);
const dashes = makeLineSpan("hdashline", options, ruleThickness);
const vListElems = [{type: "elem" as const, elem: tableBody, shift: 0}];
while (hlines.length > 0) {
const hline = hlines.pop()!;
const lineShift = hline.pos - offset;
if (hline.isDashed) {
vListElems.push({type: "elem" as const, elem: dashes, shift: lineShift});
} else {
vListElems.push({type: "elem" as const, elem: line, shift: lineShift});
}
}
tableBody = makeVList({
positionType: "individualShift",
children: vListElems,
}, options);
}
if (tagSpans.length === 0) {
return makeSpan(["mord"], [tableBody], options);
} else {
const eqnNumCol = makeVList({
positionType: "individualShift",
children: tagSpans,
}, options);
const tagCol = makeSpan(["tag"], [eqnNumCol], options);
return makeFragment([tableBody, tagCol]);
}
};
const alignMap: Record<string, string> = {
c: "center ",
l: "left ",
r: "right ",
};
const mathmlBuilder: MathMLBuilder<"array"> = function(group, options) {
const tbl = [];
const glue = new MathNode("mtd", [], ["mtr-glue"]);
const tag = new MathNode("mtd", [], ["mml-eqn-num"]);
for (let i = 0; i < group.body.length; i++) {
const rw = group.body[i];
const row = [];
for (let j = 0; j < rw.length; j++) {
row.push(new MathNode("mtd",
[mml.buildGroup(rw[j], options)]));
}
if (group.tags && group.tags[i]) {
row.unshift(glue);
row.push(glue);
if (group.leqno) {
row.unshift(tag);
} else {
row.push(tag);
}
}
tbl.push(new MathNode("mtr", row));
}
let table = new MathNode("mtable", tbl);
// Set column alignment, row spacing, column spacing, and
// array lines by setting attributes on the table element.
// Set the row spacing. In MathML, we specify a gap distance.
// We do not use rowGap[] because MathML automatically increases
// cell height with the height/depth of the element content.
// LaTeX \arraystretch multiplies the row baseline-to-baseline distance.
// We simulate this by adding (arraystretch - 1)em to the gap. This
// does a reasonable job of adjusting arrays containing 1 em tall content.
// The 0.16 and 0.09 values are found empirically. They produce an array
// similar to LaTeX and in which content does not interfere with \hlines.
const gap = (group.arraystretch === 0.5)
? 0.1 // {smallmatrix}, {subarray}
: 0.16 + group.arraystretch - 1 + (group.addJot ? 0.09 : 0);
table.setAttribute("rowspacing", makeEm(gap));
// MathML table lines go only between cells.
// To place a line on an edge we'll use <menclose>, if necessary.
let menclose = "";
let align = "";
if (group.cols && group.cols.length > 0) {
// Find column alignment, column spacing, and vertical lines.
const cols = group.cols;
let columnLines = "";
let prevTypeWasAlign = false;
let iStart = 0;
let iEnd = cols.length;
if (cols[0].type === "separator") {
menclose += "top ";
iStart = 1;
}
if (cols[cols.length - 1].type === "separator") {
menclose += "bottom ";
iEnd -= 1;
}
for (let i = iStart; i < iEnd; i++) {
const col = cols[i];
if (col.type === "align") {
align += alignMap[col.align];
if (prevTypeWasAlign) {
columnLines += "none ";
}
prevTypeWasAlign = true;
} else if (col.type === "separator") {
// MathML accepts only single lines between cells.
// So we read only the first of consecutive separators.
if (prevTypeWasAlign) {
columnLines += col.separator === "|" ? "solid " : "dashed ";
prevTypeWasAlign = false;
}
}
}
table.setAttribute("columnalign", align.trim());
if (/[sd]/.test(columnLines)) {
table.setAttribute("columnlines", columnLines.trim());
}
}
// Set column spacing.
if (group.colSeparationType === "align") {
const cols = group.cols || [];
let spacing = "";
for (let i = 1; i < cols.length; i++) {
spacing += i % 2 ? "0em " : "1em ";
}
table.setAttribute("columnspacing", spacing.trim());
} else if (group.colSeparationType === "alignat" ||
group.colSeparationType === "gather") {
table.setAttribute("columnspacing", "0em");
} else if (group.colSeparationType === "small") {
table.setAttribute("columnspacing", "0.2778em");
} else if (group.colSeparationType === "CD") {
table.setAttribute("columnspacing", "0.5em");
} else {
table.setAttribute("columnspacing", "1em");
}
// Address \hline and \hdashline
let rowLines = "";
const hlines = group.hLinesBeforeRow;
menclose += hlines[0].length > 0 ? "left " : "";
menclose += hlines[hlines.length - 1].length > 0 ? "right " : "";
for (let i = 1; i < hlines.length - 1; i++) {
rowLines += (hlines[i].length === 0)
? "none "
// MathML accepts only a single line between rows. Read one element.
: hlines[i][0] ? "dashed " : "solid ";
}
if (/[sd]/.test(rowLines)) {
table.setAttribute("rowlines", rowLines.trim());
}
if (menclose !== "") {
table = new MathNode("menclose", [table]);
table.setAttribute("notation", menclose.trim());
}
if (group.arraystretch && group.arraystretch < 1) {
// A small array. Wrap in scriptstyle so row gap is not too large.
table = new MathNode("mstyle", [table]);
table.setAttribute("scriptlevel", "1");
}
return table;
};
// Convenience function for align, align*, aligned, alignat, alignat*, alignedat.
const alignedHandler = function(context: EnvContextLike, args: AnyParseNode[]) {
if (!context.envName.includes("ed")) {
validateAmsEnvironmentContext(context);
}
const cols: AlignSpec[] = [];
const separationType: ColSeparationType = context.envName.includes("at") ? "alignat" : "align";
const isSplit = context.envName === "split";
const res = parseArray(context.parser,
{
cols,
addJot: true,
autoTag: isSplit ? undefined : getAutoTag(context.envName),
emptySingleRow: true,
colSeparationType: separationType,
maxNumCols: isSplit ? 2 : undefined,
leqno: context.parser.settings.leqno,
},
"display"
);
// Determining number of columns.
// 1. If the first argument is given, we use it as a number of columns,
// and makes sure that each row doesn't exceed that number.
// 2. Otherwise, just count number of columns = maximum number
// of cells in each row ("aligned" mode -- isAligned will be true).
//
// At the same time, prepend empty group {} at beginning of every second
// cell in each row (starting with second cell) so that operators become
// binary. This behavior is implemented in amsmath's \start@aligned.
let numMaths = 0;
let numCols = 0;
const emptyGroup: ParseNode<"ordgroup"> = {
type: "ordgroup",
mode: context.mode,
body: [],
};
if (args[0] && args[0].type === "ordgroup") {
let arg0 = "";
for (let i = 0; i < args[0].body.length; i++) {
const textord = assertNodeType(args[0].body[i], "textord");
arg0 += textord.text;
}
numMaths = Number(arg0);
numCols = numMaths * 2;
}
const isAligned = !numCols;
res.body.forEach(function(row) {
for (let i = 1; i < row.length; i += 2) {
// Modify ordgroup node within styling node
const styling = assertNodeType(row[i], "styling");
const ordgroup = assertNodeType(styling.body[0], "ordgroup");
ordgroup.body.unshift(emptyGroup);
}
if (!isAligned) { // Case 1
const curMaths = row.length / 2;
if (numMaths < curMaths) {
throw new ParseError(
"Too many math in a row: " +
`expected ${numMaths}, but got ${curMaths}`,
row[0]);
}
} else if (numCols < row.length) { // Case 2
numCols = row.length;
}
});
// Adjusting alignment.
// In aligned mode, we add one \qquad between columns;
// otherwise we add nothing.
for (let i = 0; i < numCols; ++i) {
let align = "r";
let pregap = 0;
if (i % 2 === 1) {
align = "l";
} else if (i > 0 && isAligned) { // "aligned" mode.
pregap = 1; // add one \quad
}
cols[i] = {
type: "align",
align: align,
pregap: pregap,
postgap: 0,
};
}
res.colSeparationType = isAligned ? "align" : "alignat";
return res;
};
// Arrays are part of LaTeX, defined in lttab.dtx so its documentation
// is part of the source2e.pdf file of LaTeX2e source documentation.
// {darray} is an {array} environment where cells are set in \displaystyle,
// as defined in nccmath.sty.
defineEnvironment({
type: "array",
names: ["array", "darray"],
props: {
numArgs: 1,
},
handler(context, args) {
// Since no types are specified above, the two possibilities are
// - The argument is wrapped in {} or [], in which case Parser's
// parseGroup() returns an "ordgroup" wrapping some symbol node.
// - The argument is a bare symbol node.
const symNode = checkSymbolNodeType(args[0]);
const colalign: AnyParseNode[] =
symNode ? [args[0]] : assertNodeType(args[0], "ordgroup").body;
const cols: AlignSpec[] = colalign.map(function(nde) {
const node = assertSymbolNodeType(nde);
const ca = node.text;
if ("lcr".includes(ca)) {
return {
type: "align",
align: ca,
};
} else if (ca === "|") {
return {
type: "separator",
separator: "|",
};
} else if (ca === ":") {
return {
type: "separator",
separator: ":",
};
}
throw new ParseError("Unknown column alignment: " + ca, nde);
});
const res: Parameters<typeof parseArray>[1] = {
cols,
hskipBeforeAndAfter: true, // \@preamble in lttab.dtx
maxNumCols: cols.length,
};
return parseArray(context.parser, res, dCellStyle(context.envName));
},
htmlBuilder,
mathmlBuilder,
});
// The matrix environments of amsmath builds on the array environment
// of LaTeX, which is discussed above.
// The mathtools package adds starred versions of the same environments.
// These have an optional argument to choose left|center|right justification.
defineEnvironment({
type: "array",
names: [
"matrix",
"pmatrix",
"bmatrix",
"Bmatrix",
"vmatrix",
"Vmatrix",
"matrix*",
"pmatrix*",
"bmatrix*",
"Bmatrix*",
"vmatrix*",
"Vmatrix*",
],
props: {
numArgs: 0,
},
handler(context) {
const delimiters = {
"matrix": null,
"pmatrix": ["(", ")"],
"bmatrix": ["[", "]"],
"Bmatrix": ["\\{", "\\}"],
"vmatrix": ["|", "|"],
"Vmatrix": ["\\Vert", "\\Vert"],
}[context.envName.replace("*", "")];
// \hskip -\arraycolsep in amsmath
let colAlign = "c";
const payload: Parameters<typeof parseArray>[1] = {
hskipBeforeAndAfter: false,
cols: [{type: "align", align: colAlign}],
};
if (context.envName.charAt(context.envName.length - 1) === "*") {
// It's one of the mathtools starred functions.
// Parse the optional alignment argument.
const parser = context.parser;
parser.consumeSpaces();
if (parser.fetch().text === "[") {
parser.consume();
parser.consumeSpaces();
colAlign = parser.fetch().text;
if (!"lcr".includes(colAlign)) {
throw new ParseError("Expected l or c or r", parser.nextToken);
}
parser.consume();
parser.consumeSpaces();
parser.expect("]");
parser.consume();
payload.cols = [{type: "align", align: colAlign}];
}
}
const res: ParseNode<"array"> =
parseArray(context.parser, payload, dCellStyle(context.envName));
// Populate cols with the correct number of column alignment specs.
const numCols = Math.max(0, ...res.body.map(row => row.length));
res.cols = new Array(numCols).fill(
{type: "align", align: colAlign}
);
return delimiters ? {
type: "leftright",
mode: context.mode,
body: [res],
left: delimiters[0],
right: delimiters[1],
rightColor: undefined, // \right uninfluenced by \color in array
} : res;
},
htmlBuilder,
mathmlBuilder,
});
defineEnvironment({
type: "array",
names: ["smallmatrix"],
props: {
numArgs: 0,
},
handler(context) {
const payload: Parameters<typeof parseArray>[1] = {arraystretch: 0.5};
const res = parseArray(context.parser, payload, "script");
res.colSeparationType = "small";
return res;
},
htmlBuilder,
mathmlBuilder,
});
defineEnvironment({
type: "array",
names: ["subarray"],
props: {
numArgs: 1,
},
handler(context, args) {
// Parsing of {subarray} is similar to {array}
const symNode = checkSymbolNodeType(args[0]);
const colalign: AnyParseNode[] =
symNode ? [args[0]] : assertNodeType(args[0], "ordgroup").body;
const cols: AlignSpec[] = colalign.map(function(nde) {
const node = assertSymbolNodeType(nde);
const ca = node.text;
// {subarray} only recognizes "l" & "c"
if ("lc".includes(ca)) {
return {
type: "align",
align: ca,
};
}
throw new ParseError("Unknown column alignment: " + ca, nde);
});
if (cols.length > 1) {
throw new ParseError("{subarray} can contain only one column");
}
const payload: Parameters<typeof parseArray>[1] = {
cols,
hskipBeforeAndAfter: false,
arraystretch: 0.5,
};
const res = parseArray(context.parser, payload, "script");
if (res.body.length > 0 && res.body[0].length > 1) {
throw new ParseError("{subarray} can contain only one column");
}
return res;
},
htmlBuilder,
mathmlBuilder,
});
// A cases environment (in amsmath.sty) is almost equivalent to
// \def\arraystretch{1.2}%
// \left\{\begin{array}{@{}l@{\quad}l@{}} … \end{array}\right.
// {dcases} is a {cases} environment where cells are set in \displaystyle,
// as defined in mathtools.sty.
// {rcases} is another mathtools environment. It's brace is on the right side.
defineEnvironment({
type: "array",
names: [
"cases",
"dcases",
"rcases",
"drcases",
],
props: {
numArgs: 0,
},
handler(context) {
const payload: Parameters<typeof parseArray>[1] = {
arraystretch: 1.2,
cols: [{
type: "align",
align: "l",
pregap: 0,
// TODO(kevinb) get the current style.
// For now we use the metrics for TEXT style which is what we were
// doing before. Before attempting to get the current style we
// should look at TeX's behavior especially for \over and matrices.
postgap: 1.0, /* 1em quad */
}, {
type: "align",
align: "l",
pregap: 0,
postgap: 0,
}],
};
const res: ParseNode<"array"> =
parseArray(context.parser, payload, dCellStyle(context.envName));
return {
type: "leftright",
mode: context.mode,
body: [res],
left: context.envName.includes("r") ? "." : "\\{",
right: context.envName.includes("r") ? "\\}" : ".",
rightColor: undefined,
};
},
htmlBuilder,
mathmlBuilder,
});
// In the align environment, one uses ampersands, &, to specify number of
// columns in each row, and to locate spacing between each column.
// align gets automatic numbering. align* and aligned do not.
// The alignedat environment can be used in math mode.
// Note that we assume \nomallineskiplimit to be zero,
// so that \strut@ is the same as \strut.
defineEnvironment({
type: "array",
names: ["align", "align*", "aligned", "split"],
props: {
numArgs: 0,
},
handler: alignedHandler,
htmlBuilder,
mathmlBuilder,
});
// A gathered environment is like an array environment with one centered
// column, but where rows are considered lines so get \jot line spacing
// and contents are set in \displaystyle.
defineEnvironment({
type: "array",
names: ["gathered", "gather", "gather*"],
props: {
numArgs: 0,
},
handler(context) {
if (gatherEnvironments.has(context.envName)) {
validateAmsEnvironmentContext(context);
}
const res: Parameters<typeof parseArray>[1] = {
cols: [{
type: "align",
align: "c",
}],
addJot: true,
colSeparationType: "gather",
autoTag: getAutoTag(context.envName),
emptySingleRow: true,
leqno: context.parser.settings.leqno,
};
return parseArray(context.parser, res, "display");
},
htmlBuilder,
mathmlBuilder,
});
// alignat environment is like an align environment, but one must explicitly
// specify maximum number of columns in each row, and can adjust spacing between
// each columns.
defineEnvironment({
type: "array",
names: ["alignat", "alignat*", "alignedat"],
props: {
numArgs: 1,
},
handler: alignedHandler,
htmlBuilder,
mathmlBuilder,
});
defineEnvironment({
type: "array",
names: ["equation", "equation*"],
props: {
numArgs: 0,
},
handler(context) {
validateAmsEnvironmentContext(context);
const res: Parameters<typeof parseArray>[1] = {
autoTag: getAutoTag(context.envName),
emptySingleRow: true,
singleRow: true,
maxNumCols: 1,
leqno: context.parser.settings.leqno,
};
return parseArray(context.parser, res, "display");
},
htmlBuilder,
mathmlBuilder,
});
defineEnvironment({
type: "array",
names: ["CD"],
props: {
numArgs: 0,
},
handler(context) {
validateAmsEnvironmentContext(context);
return parseCD(context.parser);
},
htmlBuilder,
mathmlBuilder,
});
defineMacro("\\nonumber", "\\gdef\\@eqnsw{0}");
defineMacro("\\notag", "\\nonumber");
// Catch \hline outside array environment
defineFunction({
type: "text", // Doesn't matter what this is.
names: ["\\hline", "\\hdashline"],
props: {
numArgs: 0,
allowedInText: true,
allowedInMath: true,
},
handler(context, args) {
throw new ParseError(
`${context.funcName} valid only within array environment`);
},
});