DuyTa's picture
Upload folder using huggingface_hub
bc20498 verified
raw
history blame
37.5 kB
// @flow
import buildCommon from "../buildCommon";
import Style from "../Style";
import defineEnvironment from "../defineEnvironment";
import {parseCD} from "./cd";
import defineFunction from "../defineFunction";
import defineMacro from "../defineMacro";
import mathMLTree 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 utils from "../utils";
import * as html from "../buildHTML";
import * as mml from "../buildMathML";
import type Parser from "../Parser";
import type {ParseNode, AnyParseNode} from "../parseNode";
import type {StyleStr} from "../types";
import type {HtmlBuilder, MathMLBuilder} from "../defineFunction";
// 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 => {
const settings = context.parser.settings;
if (!settings.displayMode) {
throw new ParseError(`{${context.envName}} can be used only in` +
` display mode.`);
}
};
// 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): ?boolean {
if (name.indexOf("ed") === -1) {
return name.indexOf("*") === -1;
}
// 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,
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 = [];
const body = [row];
const rowGaps = [];
const hLinesBeforeRow = [];
const tags = (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)
let cell = parser.parseExpression(false, singleRow ? "\\end" : "\\\\");
parser.gullet.endGroup();
parser.gullet.beginGroup();
cell = {
type: "ordgroup",
mode: parser.mode,
body: cell,
};
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[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): StyleStr {
if (envName.slice(0, 1) === "d") {
return "display";
} else {
return "text";
}
}
type Outrow = {
[idx: number]: *,
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;
let body = new Array(nr);
const hlines = [];
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): 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 to the
// \baselineskip via \openup.
if (group.addJot) {
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 = [];
let colSep;
let colDescrNum;
const tagSpans = [];
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 = buildCommon.makeSpan(["eqn-num"], [], options);
} else if (tag === false) {
// \nonumber/\notag or starred environment
tagSpan = buildCommon.makeSpan([], [], options);
} else { // manual \tag
tagSpan = buildCommon.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 = 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 = buildCommon.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 = buildCommon.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 = utils.deflt(colDescr.pregap, arraycolsep);
if (sepwidth !== 0) {
colSep = buildCommon.makeSpan(["arraycolsep"], []);
colSep.style.width = makeEm(sepwidth);
cols.push(colSep);
}
}
let col = [];
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;
col.push({type: "elem", elem: elem, shift: shift});
}
col = buildCommon.makeVList({
positionType: "individualShift",
children: col,
}, options);
col = buildCommon.makeSpan(
["col-align-" + (colDescr.align || "c")],
[col]);
cols.push(col);
if (c < nc - 1 || group.hskipBeforeAndAfter) {
sepwidth = utils.deflt(colDescr.postgap, arraycolsep);
if (sepwidth !== 0) {
colSep = buildCommon.makeSpan(["arraycolsep"], []);
colSep.style.width = makeEm(sepwidth);
cols.push(colSep);
}
}
}
body = buildCommon.makeSpan(["mtable"], cols);
// Add \hline(s), if any.
if (hlines.length > 0) {
const line = buildCommon.makeLineSpan("hline", options, ruleThickness);
const dashes = buildCommon.makeLineSpan("hdashline", options,
ruleThickness);
const vListElems = [{type: "elem", elem: body, shift: 0}];
while (hlines.length > 0) {
const hline = hlines.pop();
const lineShift = hline.pos - offset;
if (hline.isDashed) {
vListElems.push({type: "elem", elem: dashes, shift: lineShift});
} else {
vListElems.push({type: "elem", elem: line, shift: lineShift});
}
}
body = buildCommon.makeVList({
positionType: "individualShift",
children: vListElems,
}, options);
}
if (tagSpans.length === 0) {
return buildCommon.makeSpan(["mord"], [body], options);
} else {
let eqnNumCol = buildCommon.makeVList({
positionType: "individualShift",
children: tagSpans,
}, options);
eqnNumCol = buildCommon.makeSpan(["tag"], [eqnNumCol], options);
return buildCommon.makeFragment([body, eqnNumCol]);
}
};
const alignMap = {
c: "center ",
l: "left ",
r: "right ",
};
const mathmlBuilder: MathMLBuilder<"array"> = function(group, options) {
const tbl = [];
const glue = new mathMLTree.MathNode("mtd", [], ["mtr-glue"]);
const tag = new mathMLTree.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 mathMLTree.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 mathMLTree.MathNode("mtr", row));
}
let table = new mathMLTree.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++) {
if (cols[i].type === "align") {
align += alignMap[cols[i].align];
if (prevTypeWasAlign) {
columnLines += "none ";
}
prevTypeWasAlign = true;
} else if (cols[i].type === "separator") {
// MathML accepts only single lines between cells.
// So we read only the first of consecutive separators.
if (prevTypeWasAlign) {
columnLines += cols[i].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 mathMLTree.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 mathMLTree.MathNode("mstyle", [table]);
table.setAttribute("scriptlevel", "1");
}
return table;
};
// Convenience function for align, align*, aligned, alignat, alignat*, alignedat.
const alignedHandler = function(context, args) {
if (context.envName.indexOf("ed") === -1) {
validateAmsEnvironmentContext(context);
}
const cols = [];
const separationType = context.envName.indexOf("at") > -1 ? "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;
let numCols = 0;
const emptyGroup = {
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 = colalign.map(function(nde) {
const node = assertSymbolNodeType(nde);
const ca = node.text;
if ("lcr".indexOf(ca) !== -1) {
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 = {
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 = {
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".indexOf(colAlign) === -1) {
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 = {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 = colalign.map(function(nde) {
const node = assertSymbolNodeType(nde);
const ca = node.text;
// {subarray} only recognizes "l" & "c"
if ("lc".indexOf(ca) !== -1) {
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");
}
let res = {
cols,
hskipBeforeAndAfter: false,
arraystretch: 0.5,
};
res = parseArray(context.parser, res, "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 = {
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.indexOf("r") > -1 ? "." : "\\{",
right: context.envName.indexOf("r") > -1 ? "\\}" : ".",
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 (utils.contains(["gather", "gather*"], context.envName)) {
validateAmsEnvironmentContext(context);
}
const res = {
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 = {
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`);
},
});