// @flow import buildCommon from "../buildCommon"; import defineFunction from "../defineFunction"; import delimiter from "../delimiter"; import mathMLTree from "../mathMLTree"; import ParseError from "../ParseError"; import utils from "../utils"; import {assertNodeType, checkSymbolNodeType} from "../parseNode"; import {makeEm} from "../units"; import * as html from "../buildHTML"; import * as mml from "../buildMathML"; import type Options from "../Options"; import type {AnyParseNode, ParseNode, SymbolParseNode} from "../parseNode"; import type {FunctionContext} from "../defineFunction"; // Extra data needed for the delimiter handler down below const delimiterSizes = { "\\bigl" : {mclass: "mopen", size: 1}, "\\Bigl" : {mclass: "mopen", size: 2}, "\\biggl": {mclass: "mopen", size: 3}, "\\Biggl": {mclass: "mopen", size: 4}, "\\bigr" : {mclass: "mclose", size: 1}, "\\Bigr" : {mclass: "mclose", size: 2}, "\\biggr": {mclass: "mclose", size: 3}, "\\Biggr": {mclass: "mclose", size: 4}, "\\bigm" : {mclass: "mrel", size: 1}, "\\Bigm" : {mclass: "mrel", size: 2}, "\\biggm": {mclass: "mrel", size: 3}, "\\Biggm": {mclass: "mrel", size: 4}, "\\big" : {mclass: "mord", size: 1}, "\\Big" : {mclass: "mord", size: 2}, "\\bigg" : {mclass: "mord", size: 3}, "\\Bigg" : {mclass: "mord", size: 4}, }; const delimiters = [ "(", "\\lparen", ")", "\\rparen", "[", "\\lbrack", "]", "\\rbrack", "\\{", "\\lbrace", "\\}", "\\rbrace", "\\lfloor", "\\rfloor", "\u230a", "\u230b", "\\lceil", "\\rceil", "\u2308", "\u2309", "<", ">", "\\langle", "\u27e8", "\\rangle", "\u27e9", "\\lt", "\\gt", "\\lvert", "\\rvert", "\\lVert", "\\rVert", "\\lgroup", "\\rgroup", "\u27ee", "\u27ef", "\\lmoustache", "\\rmoustache", "\u23b0", "\u23b1", "/", "\\backslash", "|", "\\vert", "\\|", "\\Vert", "\\uparrow", "\\Uparrow", "\\downarrow", "\\Downarrow", "\\updownarrow", "\\Updownarrow", ".", ]; type IsMiddle = {delim: string, options: Options}; // Delimiter functions function checkDelimiter( delim: AnyParseNode, context: FunctionContext, ): SymbolParseNode { const symDelim = checkSymbolNodeType(delim); if (symDelim && utils.contains(delimiters, symDelim.text)) { return symDelim; } else if (symDelim) { throw new ParseError( `Invalid delimiter '${symDelim.text}' after '${context.funcName}'`, delim); } else { throw new ParseError(`Invalid delimiter type '${delim.type}'`, delim); } } defineFunction({ type: "delimsizing", names: [ "\\bigl", "\\Bigl", "\\biggl", "\\Biggl", "\\bigr", "\\Bigr", "\\biggr", "\\Biggr", "\\bigm", "\\Bigm", "\\biggm", "\\Biggm", "\\big", "\\Big", "\\bigg", "\\Bigg", ], props: { numArgs: 1, argTypes: ["primitive"], }, handler: (context, args) => { const delim = checkDelimiter(args[0], context); return { type: "delimsizing", mode: context.parser.mode, size: delimiterSizes[context.funcName].size, mclass: delimiterSizes[context.funcName].mclass, delim: delim.text, }; }, htmlBuilder: (group, options) => { if (group.delim === ".") { // Empty delimiters still count as elements, even though they don't // show anything. return buildCommon.makeSpan([group.mclass]); } // Use delimiter.sizedDelim to generate the delimiter. return delimiter.sizedDelim( group.delim, group.size, options, group.mode, [group.mclass]); }, mathmlBuilder: (group) => { const children = []; if (group.delim !== ".") { children.push(mml.makeText(group.delim, group.mode)); } const node = new mathMLTree.MathNode("mo", children); if (group.mclass === "mopen" || group.mclass === "mclose") { // Only some of the delimsizing functions act as fences, and they // return "mopen" or "mclose" mclass. node.setAttribute("fence", "true"); } else { // Explicitly disable fencing if it's not a fence, to override the // defaults. node.setAttribute("fence", "false"); } node.setAttribute("stretchy", "true"); const size = makeEm(delimiter.sizeToMaxHeight[group.size]); node.setAttribute("minsize", size); node.setAttribute("maxsize", size); return node; }, }); function assertParsed(group: ParseNode<"leftright">) { if (!group.body) { throw new Error("Bug: The leftright ParseNode wasn't fully parsed."); } } defineFunction({ type: "leftright-right", names: ["\\right"], props: { numArgs: 1, primitive: true, }, handler: (context, args) => { // \left case below triggers parsing of \right in // `const right = parser.parseFunction();` // uses this return value. const color = context.parser.gullet.macros.get("\\current@color"); if (color && typeof color !== "string") { throw new ParseError( "\\current@color set to non-string in \\right"); } return { type: "leftright-right", mode: context.parser.mode, delim: checkDelimiter(args[0], context).text, color, // undefined if not set via \color }; }, }); defineFunction({ type: "leftright", names: ["\\left"], props: { numArgs: 1, primitive: true, }, handler: (context, args) => { const delim = checkDelimiter(args[0], context); const parser = context.parser; // Parse out the implicit body ++parser.leftrightDepth; // parseExpression stops before '\\right' const body = parser.parseExpression(false); --parser.leftrightDepth; // Check the next token parser.expect("\\right", false); const right = assertNodeType(parser.parseFunction(), "leftright-right"); return { type: "leftright", mode: parser.mode, body, left: delim.text, right: right.delim, rightColor: right.color, }; }, htmlBuilder: (group, options) => { assertParsed(group); // Build the inner expression const inner = html.buildExpression(group.body, options, true, ["mopen", "mclose"]); let innerHeight = 0; let innerDepth = 0; let hadMiddle = false; // Calculate its height and depth for (let i = 0; i < inner.length; i++) { // Property `isMiddle` not defined on `span`. See comment in // "middle"'s htmlBuilder. // $FlowFixMe if (inner[i].isMiddle) { hadMiddle = true; } else { innerHeight = Math.max(inner[i].height, innerHeight); innerDepth = Math.max(inner[i].depth, innerDepth); } } // The size of delimiters is the same, regardless of what style we are // in. Thus, to correctly calculate the size of delimiter we need around // a group, we scale down the inner size based on the size. innerHeight *= options.sizeMultiplier; innerDepth *= options.sizeMultiplier; let leftDelim; if (group.left === ".") { // Empty delimiters in \left and \right make null delimiter spaces. leftDelim = html.makeNullDelimiter(options, ["mopen"]); } else { // Otherwise, use leftRightDelim to generate the correct sized // delimiter. leftDelim = delimiter.leftRightDelim( group.left, innerHeight, innerDepth, options, group.mode, ["mopen"]); } // Add it to the beginning of the expression inner.unshift(leftDelim); // Handle middle delimiters if (hadMiddle) { for (let i = 1; i < inner.length; i++) { const middleDelim = inner[i]; // Property `isMiddle` not defined on `span`. See comment in // "middle"'s htmlBuilder. // $FlowFixMe const isMiddle: IsMiddle = middleDelim.isMiddle; if (isMiddle) { // Apply the options that were active when \middle was called inner[i] = delimiter.leftRightDelim( isMiddle.delim, innerHeight, innerDepth, isMiddle.options, group.mode, []); } } } let rightDelim; // Same for the right delimiter, but using color specified by \color if (group.right === ".") { rightDelim = html.makeNullDelimiter(options, ["mclose"]); } else { const colorOptions = group.rightColor ? options.withColor(group.rightColor) : options; rightDelim = delimiter.leftRightDelim( group.right, innerHeight, innerDepth, colorOptions, group.mode, ["mclose"]); } // Add it to the end of the expression. inner.push(rightDelim); return buildCommon.makeSpan(["minner"], inner, options); }, mathmlBuilder: (group, options) => { assertParsed(group); const inner = mml.buildExpression(group.body, options); if (group.left !== ".") { const leftNode = new mathMLTree.MathNode( "mo", [mml.makeText(group.left, group.mode)]); leftNode.setAttribute("fence", "true"); inner.unshift(leftNode); } if (group.right !== ".") { const rightNode = new mathMLTree.MathNode( "mo", [mml.makeText(group.right, group.mode)]); rightNode.setAttribute("fence", "true"); if (group.rightColor) { rightNode.setAttribute("mathcolor", group.rightColor); } inner.push(rightNode); } return mml.makeRow(inner); }, }); defineFunction({ type: "middle", names: ["\\middle"], props: { numArgs: 1, primitive: true, }, handler: (context, args) => { const delim = checkDelimiter(args[0], context); if (!context.parser.leftrightDepth) { throw new ParseError("\\middle without preceding \\left", delim); } return { type: "middle", mode: context.parser.mode, delim: delim.text, }; }, htmlBuilder: (group, options) => { let middleDelim; if (group.delim === ".") { middleDelim = html.makeNullDelimiter(options, []); } else { middleDelim = delimiter.sizedDelim( group.delim, 1, options, group.mode, []); const isMiddle: IsMiddle = {delim: group.delim, options}; // Property `isMiddle` not defined on `span`. It is only used in // this file above. // TODO: Fix this violation of the `span` type and possibly rename // things since `isMiddle` sounds like a boolean, but is a struct. // $FlowFixMe middleDelim.isMiddle = isMiddle; } return middleDelim; }, mathmlBuilder: (group, options) => { // A Firefox \middle will stretch a character vertically only if it // is in the fence part of the operator dictionary at: // https://www.w3.org/TR/MathML3/appendixc.html. // So we need to avoid U+2223 and use plain "|" instead. const textNode = (group.delim === "\\vert" || group.delim === "|") ? mml.makeText("|", "text") : mml.makeText(group.delim, group.mode); const middleNode = new mathMLTree.MathNode("mo", [textNode]); middleNode.setAttribute("fence", "true"); // MathML gives 5/18em spacing to each element. // \middle should get delimiter spacing instead. middleNode.setAttribute("lspace", "0.05em"); middleNode.setAttribute("rspace", "0.05em"); return middleNode; }, });