1119 lines
37 KiB
JavaScript
1119 lines
37 KiB
JavaScript
// @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`);
|
|
},
|
|
});
|