|
|
|
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"; |
|
|
|
|
|
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}; |
|
|
|
|
|
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 === ".") { |
|
|
|
|
|
return buildCommon.makeSpan([group.mclass]); |
|
} |
|
|
|
|
|
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") { |
|
|
|
|
|
node.setAttribute("fence", "true"); |
|
} else { |
|
|
|
|
|
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) => { |
|
|
|
|
|
|
|
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, |
|
}; |
|
}, |
|
}); |
|
|
|
|
|
defineFunction({ |
|
type: "leftright", |
|
names: ["\\left"], |
|
props: { |
|
numArgs: 1, |
|
primitive: true, |
|
}, |
|
handler: (context, args) => { |
|
const delim = checkDelimiter(args[0], context); |
|
|
|
const parser = context.parser; |
|
|
|
++parser.leftrightDepth; |
|
|
|
const body = parser.parseExpression(false); |
|
--parser.leftrightDepth; |
|
|
|
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); |
|
|
|
const inner = html.buildExpression(group.body, options, true, |
|
["mopen", "mclose"]); |
|
|
|
let innerHeight = 0; |
|
let innerDepth = 0; |
|
let hadMiddle = false; |
|
|
|
|
|
for (let i = 0; i < inner.length; i++) { |
|
|
|
|
|
|
|
if (inner[i].isMiddle) { |
|
hadMiddle = true; |
|
} else { |
|
innerHeight = Math.max(inner[i].height, innerHeight); |
|
innerDepth = Math.max(inner[i].depth, innerDepth); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
innerHeight *= options.sizeMultiplier; |
|
innerDepth *= options.sizeMultiplier; |
|
|
|
let leftDelim; |
|
if (group.left === ".") { |
|
|
|
leftDelim = html.makeNullDelimiter(options, ["mopen"]); |
|
} else { |
|
|
|
|
|
leftDelim = delimiter.leftRightDelim( |
|
group.left, innerHeight, innerDepth, options, |
|
group.mode, ["mopen"]); |
|
} |
|
|
|
inner.unshift(leftDelim); |
|
|
|
|
|
if (hadMiddle) { |
|
for (let i = 1; i < inner.length; i++) { |
|
const middleDelim = inner[i]; |
|
|
|
|
|
|
|
const isMiddle: IsMiddle = middleDelim.isMiddle; |
|
if (isMiddle) { |
|
|
|
inner[i] = delimiter.leftRightDelim( |
|
isMiddle.delim, innerHeight, innerDepth, |
|
isMiddle.options, group.mode, []); |
|
} |
|
} |
|
} |
|
|
|
let rightDelim; |
|
|
|
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"]); |
|
} |
|
|
|
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}; |
|
|
|
|
|
|
|
|
|
|
|
middleDelim.isMiddle = isMiddle; |
|
} |
|
return middleDelim; |
|
}, |
|
mathmlBuilder: (group, options) => { |
|
|
|
|
|
|
|
|
|
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"); |
|
|
|
|
|
middleNode.setAttribute("lspace", "0.05em"); |
|
middleNode.setAttribute("rspace", "0.05em"); |
|
return middleNode; |
|
}, |
|
}); |
|
|