Files
KaTeX/src/functions/genfrac.js
ylemkimon 5a90558116 feat(function): add allowedInArgument instead of greediness property (#2134)
* Remove `greediness` property

Use boolean `grouped` property instead, which is more consistent with 
LaTeX.

BREAKING CHANGE: \cfrac, \color, \textcolor, \colorbox, \fcolorbox are 
no longer grouped.

* Rename grouped to allowedInArgument

* Reenable tests

* Update documentation

* Fix typo

Co-authored-by: Kevin Barabash <kevinb@khanacademy.org>
2020-09-07 13:38:17 +09:00

509 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// @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} 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 {
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", ruleWidth + "em");
}
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", // cant 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,
});