Spaces:
Sleeping
Sleeping
| 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`); | |
| }, | |
| }); | |