UNPKG

katex

Version:

Fast math typesetting for the web.

511 lines (457 loc) 15.3 kB
// @flow import defineFunction, {normalizeArgument} from "../defineFunction"; import buildCommon from "../buildCommon"; import delimiter from "../delimiter"; import mathMLTree from "../mathMLTree"; import Style from "../Style"; import {assertNodeType} from "../parseNode"; import {assert} from "../utils"; import * as html from "../buildHTML"; import * as mml from "../buildMathML"; import {calculateSize, makeEm} from "../units"; const adjustStyle = (size, originalStyle) => { // Figure out what style this fraction should be in based on the // function used let style = originalStyle; if (size === "display") { // Get display style as a default. // If incoming style is sub/sup, use style.text() to get correct size. style = style.id >= Style.SCRIPT.id ? style.text() : Style.DISPLAY; } else if (size === "text" && style.size === Style.DISPLAY.size) { // We're in a \tfrac but incoming style is displaystyle, so: style = Style.TEXT; } else if (size === "script") { style = Style.SCRIPT; } else if (size === "scriptscript") { style = Style.SCRIPTSCRIPT; } return style; }; const htmlBuilder = (group, options) => { // Fractions are handled in the TeXbook on pages 444-445, rules 15(a-e). const style = adjustStyle(group.size, options.style); const nstyle = style.fracNum(); const dstyle = style.fracDen(); let newOptions; newOptions = options.havingStyle(nstyle); const numerm = html.buildGroup(group.numer, newOptions, options); if (group.continued) { // \cfrac inserts a \strut into the numerator. // Get \strut dimensions from TeXbook page 353. const hStrut = 8.5 / options.fontMetrics().ptPerEm; const dStrut = 3.5 / options.fontMetrics().ptPerEm; numerm.height = numerm.height < hStrut ? hStrut : numerm.height; numerm.depth = numerm.depth < dStrut ? dStrut : numerm.depth; } newOptions = options.havingStyle(dstyle); const denomm = html.buildGroup(group.denom, newOptions, options); let rule; let ruleWidth; let ruleSpacing; if (group.hasBarLine) { if (group.barSize) { ruleWidth = calculateSize(group.barSize, options); rule = buildCommon.makeLineSpan("frac-line", options, ruleWidth); } else { rule = buildCommon.makeLineSpan("frac-line", options); } ruleWidth = rule.height; ruleSpacing = rule.height; } else { rule = null; ruleWidth = 0; ruleSpacing = options.fontMetrics().defaultRuleThickness; } // Rule 15b let numShift; let clearance; let denomShift; if (style.size === Style.DISPLAY.size || group.size === "display") { numShift = options.fontMetrics().num1; if (ruleWidth > 0) { clearance = 3 * ruleSpacing; } else { clearance = 7 * ruleSpacing; } denomShift = options.fontMetrics().denom1; } else { if (ruleWidth > 0) { numShift = options.fontMetrics().num2; clearance = ruleSpacing; } else { numShift = options.fontMetrics().num3; clearance = 3 * ruleSpacing; } denomShift = options.fontMetrics().denom2; } let frac; if (!rule) { // Rule 15c const candidateClearance = (numShift - numerm.depth) - (denomm.height - denomShift); if (candidateClearance < clearance) { numShift += 0.5 * (clearance - candidateClearance); denomShift += 0.5 * (clearance - candidateClearance); } frac = buildCommon.makeVList({ positionType: "individualShift", children: [ {type: "elem", elem: denomm, shift: denomShift}, {type: "elem", elem: numerm, shift: -numShift}, ], }, options); } else { // Rule 15d const axisHeight = options.fontMetrics().axisHeight; if ((numShift - numerm.depth) - (axisHeight + 0.5 * ruleWidth) < clearance) { numShift += clearance - ((numShift - numerm.depth) - (axisHeight + 0.5 * ruleWidth)); } if ((axisHeight - 0.5 * ruleWidth) - (denomm.height - denomShift) < clearance) { denomShift += clearance - ((axisHeight - 0.5 * ruleWidth) - (denomm.height - denomShift)); } const midShift = -(axisHeight - 0.5 * ruleWidth); frac = buildCommon.makeVList({ positionType: "individualShift", children: [ {type: "elem", elem: denomm, shift: denomShift}, {type: "elem", elem: rule, shift: midShift}, {type: "elem", elem: numerm, shift: -numShift}, ], }, options); } // Since we manually change the style sometimes (with \dfrac or \tfrac), // account for the possible size change here. newOptions = options.havingStyle(style); frac.height *= newOptions.sizeMultiplier / options.sizeMultiplier; frac.depth *= newOptions.sizeMultiplier / options.sizeMultiplier; // Rule 15e let delimSize; if (style.size === Style.DISPLAY.size) { delimSize = options.fontMetrics().delim1; } else if (style.size === Style.SCRIPTSCRIPT.size) { delimSize = options.havingStyle(Style.SCRIPT).fontMetrics().delim2; } else { delimSize = options.fontMetrics().delim2; } let leftDelim; let rightDelim; if (group.leftDelim == null) { leftDelim = html.makeNullDelimiter(options, ["mopen"]); } else { leftDelim = delimiter.customSizedDelim( group.leftDelim, delimSize, true, options.havingStyle(style), group.mode, ["mopen"]); } if (group.continued) { rightDelim = buildCommon.makeSpan([]); // zero width for \cfrac } else if (group.rightDelim == null) { rightDelim = html.makeNullDelimiter(options, ["mclose"]); } else { rightDelim = delimiter.customSizedDelim( group.rightDelim, delimSize, true, options.havingStyle(style), group.mode, ["mclose"]); } return buildCommon.makeSpan( ["mord"].concat(newOptions.sizingClasses(options)), [leftDelim, buildCommon.makeSpan(["mfrac"], [frac]), rightDelim], options); }; const mathmlBuilder = (group, options) => { let node = new mathMLTree.MathNode( "mfrac", [ mml.buildGroup(group.numer, options), mml.buildGroup(group.denom, options), ]); if (!group.hasBarLine) { node.setAttribute("linethickness", "0px"); } else if (group.barSize) { const ruleWidth = calculateSize(group.barSize, options); node.setAttribute("linethickness", makeEm(ruleWidth)); } const style = adjustStyle(group.size, options.style); if (style.size !== options.style.size) { node = new mathMLTree.MathNode("mstyle", [node]); const isDisplay = (style.size === Style.DISPLAY.size) ? "true" : "false"; node.setAttribute("displaystyle", isDisplay); node.setAttribute("scriptlevel", "0"); } if (group.leftDelim != null || group.rightDelim != null) { const withDelims = []; if (group.leftDelim != null) { const leftOp = new mathMLTree.MathNode( "mo", [new mathMLTree.TextNode(group.leftDelim.replace("\\", ""))] ); leftOp.setAttribute("fence", "true"); withDelims.push(leftOp); } withDelims.push(node); if (group.rightDelim != null) { const rightOp = new mathMLTree.MathNode( "mo", [new mathMLTree.TextNode(group.rightDelim.replace("\\", ""))] ); rightOp.setAttribute("fence", "true"); withDelims.push(rightOp); } return mml.makeRow(withDelims); } return node; }; defineFunction({ type: "genfrac", names: [ "\\dfrac", "\\frac", "\\tfrac", "\\dbinom", "\\binom", "\\tbinom", "\\\\atopfrac", // can’t be entered directly "\\\\bracefrac", "\\\\brackfrac", // ditto ], props: { numArgs: 2, allowedInArgument: true, }, handler: ({parser, funcName}, args) => { const numer = args[0]; const denom = args[1]; let hasBarLine; let leftDelim = null; let rightDelim = null; let size = "auto"; switch (funcName) { case "\\dfrac": case "\\frac": case "\\tfrac": hasBarLine = true; break; case "\\\\atopfrac": hasBarLine = false; break; case "\\dbinom": case "\\binom": case "\\tbinom": hasBarLine = false; leftDelim = "("; rightDelim = ")"; break; case "\\\\bracefrac": hasBarLine = false; leftDelim = "\\{"; rightDelim = "\\}"; break; case "\\\\brackfrac": hasBarLine = false; leftDelim = "["; rightDelim = "]"; break; default: throw new Error("Unrecognized genfrac command"); } switch (funcName) { case "\\dfrac": case "\\dbinom": size = "display"; break; case "\\tfrac": case "\\tbinom": size = "text"; break; } return { type: "genfrac", mode: parser.mode, continued: false, numer, denom, hasBarLine, leftDelim, rightDelim, size, barSize: null, }; }, htmlBuilder, mathmlBuilder, }); defineFunction({ type: "genfrac", names: ["\\cfrac"], props: { numArgs: 2, }, handler: ({parser, funcName}, args) => { const numer = args[0]; const denom = args[1]; return { type: "genfrac", mode: parser.mode, continued: true, numer, denom, hasBarLine: true, leftDelim: null, rightDelim: null, size: "display", barSize: null, }; }, }); // Infix generalized fractions -- these are not rendered directly, but replaced // immediately by one of the variants above. defineFunction({ type: "infix", names: ["\\over", "\\choose", "\\atop", "\\brace", "\\brack"], props: { numArgs: 0, infix: true, }, handler({parser, funcName, token}) { let replaceWith; switch (funcName) { case "\\over": replaceWith = "\\frac"; break; case "\\choose": replaceWith = "\\binom"; break; case "\\atop": replaceWith = "\\\\atopfrac"; break; case "\\brace": replaceWith = "\\\\bracefrac"; break; case "\\brack": replaceWith = "\\\\brackfrac"; break; default: throw new Error("Unrecognized infix genfrac command"); } return { type: "infix", mode: parser.mode, replaceWith, token, }; }, }); const stylArray = ["display", "text", "script", "scriptscript"]; const delimFromValue = function(delimString: string): string | null { let delim = null; if (delimString.length > 0) { delim = delimString; delim = delim === "." ? null : delim; } return delim; }; defineFunction({ type: "genfrac", names: ["\\genfrac"], props: { numArgs: 6, allowedInArgument: true, argTypes: ["math", "math", "size", "text", "math", "math"], }, handler({parser}, args) { const numer = args[4]; const denom = args[5]; // Look into the parse nodes to get the desired delimiters. const leftNode = normalizeArgument(args[0]); const leftDelim = leftNode.type === "atom" && leftNode.family === "open" ? delimFromValue(leftNode.text) : null; const rightNode = normalizeArgument(args[1]); const rightDelim = rightNode.type === "atom" && rightNode.family === "close" ? delimFromValue(rightNode.text) : null; const barNode = assertNodeType(args[2], "size"); let hasBarLine; let barSize = null; if (barNode.isBlank) { // \genfrac acts differently than \above. // \genfrac treats an empty size group as a signal to use a // standard bar size. \above would see size = 0 and omit the bar. hasBarLine = true; } else { barSize = barNode.value; hasBarLine = barSize.number > 0; } // Find out if we want displaystyle, textstyle, etc. let size = "auto"; let styl = args[3]; if (styl.type === "ordgroup") { if (styl.body.length > 0) { const textOrd = assertNodeType(styl.body[0], "textord"); size = stylArray[Number(textOrd.text)]; } } else { styl = assertNodeType(styl, "textord"); size = stylArray[Number(styl.text)]; } return { type: "genfrac", mode: parser.mode, numer, denom, continued: false, hasBarLine, barSize, leftDelim, rightDelim, size, }; }, htmlBuilder, mathmlBuilder, }); // \above is an infix fraction that also defines a fraction bar size. defineFunction({ type: "infix", names: ["\\above"], props: { numArgs: 1, argTypes: ["size"], infix: true, }, handler({parser, funcName, token}, args) { return { type: "infix", mode: parser.mode, replaceWith: "\\\\abovefrac", size: assertNodeType(args[0], "size").value, token, }; }, }); defineFunction({ type: "genfrac", names: ["\\\\abovefrac"], props: { numArgs: 3, argTypes: ["math", "size", "math"], }, handler: ({parser, funcName}, args) => { const numer = args[0]; const barSize = assert(assertNodeType(args[1], "infix").size); const denom = args[2]; const hasBarLine = barSize.number > 0; return { type: "genfrac", mode: parser.mode, numer, denom, continued: false, hasBarLine, barSize, leftDelim: null, rightDelim: null, size: "auto", }; }, htmlBuilder, mathmlBuilder, });