// @flow import buildCommon from "../buildCommon"; import defineFunction from "../defineFunction"; import mathMLTree from "../mathMLTree"; import * as html from "../buildHTML"; import * as mml from "../buildMathML"; import {assertSymbolNodeType} from "../parseNode"; import ParseError from "../ParseError"; import {makeEm} from "../units"; import type Parser from "../Parser"; import type {ParseNode, AnyParseNode} from "../parseNode"; const cdArrowFunctionName = { ">": "\\\\cdrightarrow", "<": "\\\\cdleftarrow", "=": "\\\\cdlongequal", "A": "\\uparrow", "V": "\\downarrow", "|": "\\Vert", ".": "no arrow", }; const newCell = () => { // Create an empty cell, to be filled below with parse nodes. // The parseTree from this module must be constructed like the // one created by parseArray(), so an empty CD cell must // be a ParseNode<"styling">. And CD is always displaystyle. // So these values are fixed and flow can do implicit typing. return {type: "styling", body: [], mode: "math", style: "display"}; }; const isStartOfArrow = (node: AnyParseNode) => { return (node.type === "textord" && node.text === "@"); }; const isLabelEnd = (node: AnyParseNode, endChar: string): boolean => { return ((node.type === "mathord" || node.type === "atom") && node.text === endChar); }; function cdArrow( arrowChar: string, labels: ParseNode<"ordgroup">[], parser: Parser ): AnyParseNode { // Return a parse tree of an arrow and its labels. // This acts in a way similar to a macro expansion. const funcName = cdArrowFunctionName[arrowChar]; switch (funcName) { case "\\\\cdrightarrow": case "\\\\cdleftarrow": return parser.callFunction( funcName, [labels[0]], [labels[1]] ); case "\\uparrow": case "\\downarrow": { const leftLabel = parser.callFunction( "\\\\cdleft", [labels[0]], [] ); const bareArrow = { type: "atom", text: funcName, mode: "math", family: "rel", }; const sizedArrow = parser.callFunction("\\Big", [bareArrow], []); const rightLabel = parser.callFunction( "\\\\cdright", [labels[1]], [] ); const arrowGroup = { type: "ordgroup", mode: "math", body: [leftLabel, sizedArrow, rightLabel], }; return parser.callFunction("\\\\cdparent", [arrowGroup], []); } case "\\\\cdlongequal": return parser.callFunction("\\\\cdlongequal", [], []); case "\\Vert": { const arrow = {type: "textord", text: "\\Vert", mode: "math"}; return parser.callFunction("\\Big", [arrow], []); } default: return {type: "textord", text: " ", mode: "math"}; } } export function parseCD(parser: Parser): ParseNode<"array"> { // Get the array's parse nodes with \\ temporarily mapped to \cr. const parsedRows: AnyParseNode[][] = []; parser.gullet.beginGroup(); parser.gullet.macros.set("\\cr", "\\\\\\relax"); parser.gullet.beginGroup(); while (true) { // eslint-disable-line no-constant-condition // Get the parse nodes for the next row. parsedRows.push(parser.parseExpression(false, "\\\\")); parser.gullet.endGroup(); parser.gullet.beginGroup(); const next = parser.fetch().text; if (next === "&" || next === "\\\\") { parser.consume(); } else if (next === "\\end") { if (parsedRows[parsedRows.length - 1].length === 0) { parsedRows.pop(); // final row ended in \\ } break; } else { throw new ParseError("Expected \\\\ or \\cr or \\end", parser.nextToken); } } let row = []; const body = [row]; // Loop thru the parse nodes. Collect them into cells and arrows. for (let i = 0; i < parsedRows.length; i++) { // Start a new row. const rowNodes = parsedRows[i]; // Create the first cell. let cell = newCell(); for (let j = 0; j < rowNodes.length; j++) { if (!isStartOfArrow(rowNodes[j])) { // If a parseNode is not an arrow, it goes into a cell. cell.body.push(rowNodes[j]); } else { // Parse node j is an "@", the start of an arrow. // Before starting on the arrow, push the cell into `row`. row.push(cell); // Now collect parseNodes into an arrow. // The character after "@" defines the arrow type. j += 1; const arrowChar = assertSymbolNodeType(rowNodes[j]).text; // Create two empty label nodes. We may or may not use them. const labels: ParseNode<"ordgroup">[] = new Array(2); labels[0] = {type: "ordgroup", mode: "math", body: []}; labels[1] = {type: "ordgroup", mode: "math", body: []}; // Process the arrow. if ("=|.".indexOf(arrowChar) > -1) { // Three "arrows", ``@=`, `@|`, and `@.`, do not take labels. // Do nothing here. } else if ("<>AV".indexOf(arrowChar) > -1) { // Four arrows, `@>>>`, `@<<<`, `@AAA`, and `@VVV`, each take // two optional labels. E.g. the right-point arrow syntax is // really: @>{optional label}>{optional label}> // Collect parseNodes into labels. for (let labelNum = 0; labelNum < 2; labelNum++) { let inLabel = true; for (let k = j + 1; k < rowNodes.length; k++) { if (isLabelEnd(rowNodes[k], arrowChar)) { inLabel = false; j = k; break; } if (isStartOfArrow(rowNodes[k])) { throw new ParseError("Missing a " + arrowChar + " character to complete a CD arrow.", rowNodes[k]); } labels[labelNum].body.push(rowNodes[k]); } if (inLabel) { // isLabelEnd never returned a true. throw new ParseError("Missing a " + arrowChar + " character to complete a CD arrow.", rowNodes[j]); } } } else { throw new ParseError(`Expected one of "<>AV=|." after @`, rowNodes[j]); } // Now join the arrow to its labels. const arrow: AnyParseNode = cdArrow(arrowChar, labels, parser); // Wrap the arrow in ParseNode<"styling">. // This is done to match parseArray() behavior. const wrappedArrow = { type: "styling", body: [arrow], mode: "math", style: "display", // CD is always displaystyle. }; row.push(wrappedArrow); // In CD's syntax, cells are implicit. That is, everything that // is not an arrow gets collected into a cell. So create an empty // cell now. It will collect upcoming parseNodes. cell = newCell(); } } if (i % 2 === 0) { // Even-numbered rows consist of: cell, arrow, cell, arrow, ... cell // The last cell is not yet pushed into `row`, so: row.push(cell); } else { // Odd-numbered rows consist of: vert arrow, empty cell, ... vert arrow // Remove the empty cell that was placed at the beginning of `row`. row.shift(); } row = []; body.push(row); } // End row group parser.gullet.endGroup(); // End array group defining \\ parser.gullet.endGroup(); // define column separation. const cols = new Array(body[0].length).fill({ type: "align", align: "c", pregap: 0.25, // CD package sets \enskip between columns. postgap: 0.25, // So pre and post each get half an \enskip, i.e. 0.25em. }); return { type: "array", mode: "math", body, arraystretch: 1, addJot: true, rowGaps: [null], cols, colSeparationType: "CD", hLinesBeforeRow: new Array(body.length + 1).fill([]), }; } // The functions below are not available for general use. // They are here only for internal use by the {CD} environment in placing labels // next to vertical arrows. // We don't need any such functions for horizontal arrows because we can reuse // the functionality that already exists for extensible arrows. defineFunction({ type: "cdlabel", names: ["\\\\cdleft", "\\\\cdright"], props: { numArgs: 1, }, handler({parser, funcName}, args) { return { type: "cdlabel", mode: parser.mode, side: funcName.slice(4), label: args[0], }; }, htmlBuilder(group, options) { const newOptions = options.havingStyle(options.style.sup()); const label = buildCommon.wrapFragment( html.buildGroup(group.label, newOptions, options), options); label.classes.push("cd-label-" + group.side); label.style.bottom = makeEm(0.8 - label.depth); // Zero out label height & depth, so vertical align of arrow is set // by the arrow height, not by the label. label.height = 0; label.depth = 0; return label; }, mathmlBuilder(group, options) { let label = new mathMLTree.MathNode("mrow", [mml.buildGroup(group.label, options)]); label = new mathMLTree.MathNode("mpadded", [label]); label.setAttribute("width", "0"); if (group.side === "left") { label.setAttribute("lspace", "-1width"); } // We have to guess at vertical alignment. We know the arrow is 1.8em tall, // But we don't know the height or depth of the label. label.setAttribute("voffset", "0.7em"); label = new mathMLTree.MathNode("mstyle", [label]); label.setAttribute("displaystyle", "false"); label.setAttribute("scriptlevel", "1"); return label; }, }); defineFunction({ type: "cdlabelparent", names: ["\\\\cdparent"], props: { numArgs: 1, }, handler({parser}, args) { return { type: "cdlabelparent", mode: parser.mode, fragment: args[0], }; }, htmlBuilder(group, options) { // Wrap the vertical arrow and its labels. // The parent gets position: relative. The child gets position: absolute. // So CSS can locate the label correctly. const parent = buildCommon.wrapFragment( html.buildGroup(group.fragment, options), options ); parent.classes.push("cd-vert-arrow"); return parent; }, mathmlBuilder(group, options) { return new mathMLTree.MathNode("mrow", [mml.buildGroup(group.fragment, options)]); }, });