mirror of
https://github.com/Smaug123/KaTeX
synced 2025-10-05 19:28:39 +00:00
Migrate genfrac, lap, smash, op, mod, and katex into their own files in src/functions (#871)
Fixes #869
This commit is contained in:
454
src/buildHTML.js
454
src/buildHTML.js
@@ -452,147 +452,6 @@ groupTypes.supsub = function(group, options) {
|
||||
options);
|
||||
};
|
||||
|
||||
groupTypes.genfrac = function(group, options) {
|
||||
// Fractions are handled in the TeXbook on pages 444-445, rules 15(a-e).
|
||||
// Figure out what style this fraction should be in based on the
|
||||
// function used
|
||||
let style = options.style;
|
||||
if (group.value.size === "display") {
|
||||
style = Style.DISPLAY;
|
||||
} else if (group.value.size === "text") {
|
||||
style = Style.TEXT;
|
||||
}
|
||||
|
||||
const nstyle = style.fracNum();
|
||||
const dstyle = style.fracDen();
|
||||
let newOptions;
|
||||
|
||||
newOptions = options.havingStyle(nstyle);
|
||||
const numerm = buildGroup(group.value.numer, newOptions, options);
|
||||
|
||||
newOptions = options.havingStyle(dstyle);
|
||||
const denomm = buildGroup(group.value.denom, newOptions, options);
|
||||
|
||||
let rule;
|
||||
let ruleWidth;
|
||||
let ruleSpacing;
|
||||
if (group.value.hasBarLine) {
|
||||
rule = 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) {
|
||||
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 (ruleWidth === 0) {
|
||||
// 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.value.leftDelim == null) {
|
||||
leftDelim = makeNullDelimiter(options, ["mopen"]);
|
||||
} else {
|
||||
leftDelim = delimiter.customSizedDelim(
|
||||
group.value.leftDelim, delimSize, true,
|
||||
options.havingStyle(style), group.mode, ["mopen"]);
|
||||
}
|
||||
if (group.value.rightDelim == null) {
|
||||
rightDelim = makeNullDelimiter(options, ["mclose"]);
|
||||
} else {
|
||||
rightDelim = delimiter.customSizedDelim(
|
||||
group.value.rightDelim, delimSize, true,
|
||||
options.havingStyle(style), group.mode, ["mclose"]);
|
||||
}
|
||||
|
||||
return makeSpan(
|
||||
["mord"].concat(newOptions.sizingClasses(options)),
|
||||
[leftDelim, makeSpan(["mfrac"], [frac]), rightDelim],
|
||||
options);
|
||||
};
|
||||
|
||||
groupTypes.spacing = function(group, options) {
|
||||
if (group.value === "\\ " || group.value === "\\space" ||
|
||||
group.value === " " || group.value === "~") {
|
||||
@@ -615,318 +474,7 @@ groupTypes.spacing = function(group, options) {
|
||||
}
|
||||
};
|
||||
|
||||
groupTypes.lap = function(group, options) {
|
||||
// mathllap, mathrlap, mathclap
|
||||
let inner;
|
||||
if (group.value.alignment === "clap") {
|
||||
// ref: https://www.math.lsu.edu/~aperlis/publications/mathclap/
|
||||
inner = makeSpan([], [buildGroup(group.value.body, options)]);
|
||||
// wrap, since CSS will center a .clap > .inner > span
|
||||
inner = makeSpan(["inner"], [inner], options);
|
||||
} else {
|
||||
inner = makeSpan(
|
||||
["inner"], [buildGroup(group.value.body, options)]);
|
||||
}
|
||||
const fix = makeSpan(["fix"], []);
|
||||
return makeSpan(
|
||||
["mord", group.value.alignment], [inner, fix], options);
|
||||
};
|
||||
|
||||
groupTypes.smash = function(group, options) {
|
||||
const node = makeSpan(["mord"], [buildGroup(group.value.body, options)]);
|
||||
|
||||
if (!group.value.smashHeight && !group.value.smashDepth) {
|
||||
return node;
|
||||
}
|
||||
|
||||
if (group.value.smashHeight) {
|
||||
node.height = 0;
|
||||
// In order to influence makeVList, we have to reset the children.
|
||||
if (node.children) {
|
||||
for (let i = 0; i < node.children.length; i++) {
|
||||
node.children[i].height = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (group.value.smashDepth) {
|
||||
node.depth = 0;
|
||||
if (node.children) {
|
||||
for (let i = 0; i < node.children.length; i++) {
|
||||
node.children[i].depth = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// At this point, we've reset the TeX-like height and depth values.
|
||||
// But the span still has an HTML line height.
|
||||
// makeVList applies "display: table-cell", which prevents the browser
|
||||
// from acting on that line height. So we'll call makeVList now.
|
||||
|
||||
return buildCommon.makeVList({
|
||||
positionType: "firstBaseline",
|
||||
children: [{type: "elem", elem: node}],
|
||||
}, options);
|
||||
};
|
||||
|
||||
groupTypes.op = function(group, options) {
|
||||
// Operators are handled in the TeXbook pg. 443-444, rule 13(a).
|
||||
let supGroup;
|
||||
let subGroup;
|
||||
let hasLimits = false;
|
||||
if (group.type === "supsub") {
|
||||
// If we have limits, supsub will pass us its group to handle. Pull
|
||||
// out the superscript and subscript and set the group to the op in
|
||||
// its base.
|
||||
supGroup = group.value.sup;
|
||||
subGroup = group.value.sub;
|
||||
group = group.value.base;
|
||||
hasLimits = true;
|
||||
}
|
||||
|
||||
const style = options.style;
|
||||
|
||||
// Most operators have a large successor symbol, but these don't.
|
||||
const noSuccessor = [
|
||||
"\\smallint",
|
||||
];
|
||||
|
||||
let large = false;
|
||||
if (style.size === Style.DISPLAY.size &&
|
||||
group.value.symbol &&
|
||||
!utils.contains(noSuccessor, group.value.body)) {
|
||||
|
||||
// Most symbol operators get larger in displaystyle (rule 13)
|
||||
large = true;
|
||||
}
|
||||
|
||||
let base;
|
||||
if (group.value.symbol) {
|
||||
// If this is a symbol, create the symbol.
|
||||
const fontName = large ? "Size2-Regular" : "Size1-Regular";
|
||||
base = buildCommon.makeSymbol(
|
||||
group.value.body, fontName, "math", options,
|
||||
["mop", "op-symbol", large ? "large-op" : "small-op"]);
|
||||
} else if (group.value.value) {
|
||||
// If this is a list, compose that list.
|
||||
const inner = buildExpression(group.value.value, options, true);
|
||||
if (inner.length === 1 && inner[0] instanceof domTree.symbolNode) {
|
||||
base = inner[0];
|
||||
base.classes[0] = "mop"; // replace old mclass
|
||||
} else {
|
||||
base = makeSpan(["mop"], inner, options);
|
||||
}
|
||||
} else {
|
||||
// Otherwise, this is a text operator. Build the text from the
|
||||
// operator's name.
|
||||
// TODO(emily): Add a space in the middle of some of these
|
||||
// operators, like \limsup
|
||||
const output = [];
|
||||
for (let i = 1; i < group.value.body.length; i++) {
|
||||
output.push(buildCommon.mathsym(group.value.body[i], group.mode));
|
||||
}
|
||||
base = makeSpan(["mop"], output, options);
|
||||
}
|
||||
|
||||
// If content of op is a single symbol, shift it vertically.
|
||||
let baseShift = 0;
|
||||
let slant = 0;
|
||||
if (base instanceof domTree.symbolNode) {
|
||||
// Shift the symbol so its center lies on the axis (rule 13). It
|
||||
// appears that our fonts have the centers of the symbols already
|
||||
// almost on the axis, so these numbers are very small. Note we
|
||||
// don't actually apply this here, but instead it is used either in
|
||||
// the vlist creation or separately when there are no limits.
|
||||
baseShift = (base.height - base.depth) / 2 -
|
||||
options.fontMetrics().axisHeight;
|
||||
|
||||
// The slant of the symbol is just its italic correction.
|
||||
slant = base.italic;
|
||||
}
|
||||
|
||||
if (hasLimits) {
|
||||
// IE 8 clips \int if it is in a display: inline-block. We wrap it
|
||||
// in a new span so it is an inline, and works.
|
||||
base = makeSpan([], [base]);
|
||||
|
||||
let supm;
|
||||
let supKern;
|
||||
let subm;
|
||||
let subKern;
|
||||
let newOptions;
|
||||
// We manually have to handle the superscripts and subscripts. This,
|
||||
// aside from the kern calculations, is copied from supsub.
|
||||
if (supGroup) {
|
||||
newOptions = options.havingStyle(style.sup());
|
||||
supm = buildGroup(supGroup, newOptions, options);
|
||||
|
||||
supKern = Math.max(
|
||||
options.fontMetrics().bigOpSpacing1,
|
||||
options.fontMetrics().bigOpSpacing3 - supm.depth);
|
||||
}
|
||||
|
||||
if (subGroup) {
|
||||
newOptions = options.havingStyle(style.sub());
|
||||
subm = buildGroup(subGroup, newOptions, options);
|
||||
|
||||
subKern = Math.max(
|
||||
options.fontMetrics().bigOpSpacing2,
|
||||
options.fontMetrics().bigOpSpacing4 - subm.height);
|
||||
}
|
||||
|
||||
// Build the final group as a vlist of the possible subscript, base,
|
||||
// and possible superscript.
|
||||
let finalGroup;
|
||||
let top;
|
||||
let bottom;
|
||||
if (!supGroup) {
|
||||
top = base.height - baseShift;
|
||||
|
||||
// Shift the limits by the slant of the symbol. Note
|
||||
// that we are supposed to shift the limits by 1/2 of the slant,
|
||||
// but since we are centering the limits adding a full slant of
|
||||
// margin will shift by 1/2 that.
|
||||
finalGroup = buildCommon.makeVList({
|
||||
positionType: "top",
|
||||
positionData: top,
|
||||
children: [
|
||||
{type: "kern", size: options.fontMetrics().bigOpSpacing5},
|
||||
{type: "elem", elem: subm, marginLeft: -slant + "em"},
|
||||
{type: "kern", size: subKern},
|
||||
{type: "elem", elem: base},
|
||||
],
|
||||
}, options);
|
||||
} else if (!subGroup) {
|
||||
bottom = base.depth + baseShift;
|
||||
|
||||
finalGroup = buildCommon.makeVList({
|
||||
positionType: "bottom",
|
||||
positionData: bottom,
|
||||
children: [
|
||||
{type: "elem", elem: base},
|
||||
{type: "kern", size: supKern},
|
||||
{type: "elem", elem: supm, marginLeft: slant + "em"},
|
||||
{type: "kern", size: options.fontMetrics().bigOpSpacing5},
|
||||
],
|
||||
}, options);
|
||||
} else if (!supGroup && !subGroup) {
|
||||
// This case probably shouldn't occur (this would mean the
|
||||
// supsub was sending us a group with no superscript or
|
||||
// subscript) but be safe.
|
||||
return base;
|
||||
} else {
|
||||
bottom = options.fontMetrics().bigOpSpacing5 +
|
||||
subm.height + subm.depth +
|
||||
subKern +
|
||||
base.depth + baseShift;
|
||||
|
||||
finalGroup = buildCommon.makeVList({
|
||||
positionType: "bottom",
|
||||
positionData: bottom,
|
||||
children: [
|
||||
{type: "kern", size: options.fontMetrics().bigOpSpacing5},
|
||||
{type: "elem", elem: subm, marginLeft: -slant + "em"},
|
||||
{type: "kern", size: subKern},
|
||||
{type: "elem", elem: base},
|
||||
{type: "kern", size: supKern},
|
||||
{type: "elem", elem: supm, marginLeft: slant + "em"},
|
||||
{type: "kern", size: options.fontMetrics().bigOpSpacing5},
|
||||
],
|
||||
}, options);
|
||||
}
|
||||
|
||||
return makeSpan(["mop", "op-limits"], [finalGroup], options);
|
||||
} else {
|
||||
if (baseShift) {
|
||||
base.style.position = "relative";
|
||||
base.style.top = baseShift + "em";
|
||||
}
|
||||
|
||||
return base;
|
||||
}
|
||||
};
|
||||
|
||||
groupTypes.mod = function(group, options) {
|
||||
const inner = [];
|
||||
|
||||
if (group.value.modType === "bmod") {
|
||||
// “\nonscript\mskip-\medmuskip\mkern5mu”
|
||||
if (!options.style.isTight()) {
|
||||
inner.push(makeSpan(
|
||||
["mspace", "negativemediumspace"], [], options));
|
||||
}
|
||||
inner.push(makeSpan(["mspace", "thickspace"], [], options));
|
||||
} else if (options.style.size === Style.DISPLAY.size) {
|
||||
inner.push(makeSpan(["mspace", "quad"], [], options));
|
||||
} else if (group.value.modType === "mod") {
|
||||
inner.push(makeSpan(["mspace", "twelvemuspace"], [], options));
|
||||
} else {
|
||||
inner.push(makeSpan(["mspace", "eightmuspace"], [], options));
|
||||
}
|
||||
|
||||
if (group.value.modType === "pod" || group.value.modType === "pmod") {
|
||||
inner.push(buildCommon.mathsym("(", group.mode));
|
||||
}
|
||||
|
||||
if (group.value.modType !== "pod") {
|
||||
const modInner = [
|
||||
buildCommon.mathsym("m", group.mode),
|
||||
buildCommon.mathsym("o", group.mode),
|
||||
buildCommon.mathsym("d", group.mode)];
|
||||
if (group.value.modType === "bmod") {
|
||||
inner.push(makeSpan(["mbin"], modInner, options));
|
||||
// “\mkern5mu\nonscript\mskip-\medmuskip”
|
||||
inner.push(makeSpan(["mspace", "thickspace"], [], options));
|
||||
if (!options.style.isTight()) {
|
||||
inner.push(makeSpan(
|
||||
["mspace", "negativemediumspace"], [], options));
|
||||
}
|
||||
} else {
|
||||
Array.prototype.push.apply(inner, modInner);
|
||||
inner.push(makeSpan(["mspace", "sixmuspace"], [], options));
|
||||
}
|
||||
}
|
||||
|
||||
if (group.value.value) {
|
||||
Array.prototype.push.apply(inner,
|
||||
buildExpression(group.value.value, options, false));
|
||||
}
|
||||
|
||||
if (group.value.modType === "pod" || group.value.modType === "pmod") {
|
||||
inner.push(buildCommon.mathsym(")", group.mode));
|
||||
}
|
||||
|
||||
return buildCommon.makeFragment(inner);
|
||||
};
|
||||
|
||||
groupTypes.katex = function(group, options) {
|
||||
// The KaTeX logo. The offsets for the K and a were chosen to look
|
||||
// good, but the offsets for the T, E, and X were taken from the
|
||||
// definition of \TeX in TeX (see TeXbook pg. 356)
|
||||
const k = makeSpan(
|
||||
["k"], [buildCommon.mathsym("K", group.mode)], options);
|
||||
const a = makeSpan(
|
||||
["a"], [buildCommon.mathsym("A", group.mode)], options);
|
||||
|
||||
a.height = (a.height + 0.2) * 0.75;
|
||||
a.depth = (a.height - 0.2) * 0.75;
|
||||
|
||||
const t = makeSpan(
|
||||
["t"], [buildCommon.mathsym("T", group.mode)], options);
|
||||
const e = makeSpan(
|
||||
["e"], [buildCommon.mathsym("E", group.mode)], options);
|
||||
|
||||
e.height = (e.height - 0.2155);
|
||||
e.depth = (e.depth + 0.2155);
|
||||
|
||||
const x = makeSpan(
|
||||
["x"], [buildCommon.mathsym("X", group.mode)], options);
|
||||
|
||||
return makeSpan(
|
||||
["mord", "katex-logo"], [k, a, t, e, x], options);
|
||||
};
|
||||
|
||||
const makeLineSpan = function(className, options, thickness) {
|
||||
export const makeLineSpan = function(className, options, thickness) {
|
||||
const line = makeSpan([className], [], options);
|
||||
line.height = thickness || options.fontMetrics().defaultRuleThickness;
|
||||
line.style.borderBottomWidth = line.height + "em";
|
||||
|
@@ -247,49 +247,6 @@ groupTypes.supsub = function(group, options) {
|
||||
return node;
|
||||
};
|
||||
|
||||
groupTypes.genfrac = function(group, options) {
|
||||
const node = new mathMLTree.MathNode(
|
||||
"mfrac",
|
||||
[
|
||||
buildGroup(group.value.numer, options),
|
||||
buildGroup(group.value.denom, options),
|
||||
]);
|
||||
|
||||
if (!group.value.hasBarLine) {
|
||||
node.setAttribute("linethickness", "0px");
|
||||
}
|
||||
|
||||
if (group.value.leftDelim != null || group.value.rightDelim != null) {
|
||||
const withDelims = [];
|
||||
|
||||
if (group.value.leftDelim != null) {
|
||||
const leftOp = new mathMLTree.MathNode(
|
||||
"mo", [new mathMLTree.TextNode(group.value.leftDelim)]);
|
||||
|
||||
leftOp.setAttribute("fence", "true");
|
||||
|
||||
withDelims.push(leftOp);
|
||||
}
|
||||
|
||||
withDelims.push(node);
|
||||
|
||||
if (group.value.rightDelim != null) {
|
||||
const rightOp = new mathMLTree.MathNode(
|
||||
"mo", [new mathMLTree.TextNode(group.value.rightDelim)]);
|
||||
|
||||
rightOp.setAttribute("fence", "true");
|
||||
|
||||
withDelims.push(rightOp);
|
||||
}
|
||||
|
||||
const outerNode = new mathMLTree.MathNode("mrow", withDelims);
|
||||
|
||||
return outerNode;
|
||||
}
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
groupTypes.sqrt = function(group, options) {
|
||||
let node;
|
||||
if (group.value.index) {
|
||||
@@ -341,66 +298,6 @@ groupTypes.spacing = function(group) {
|
||||
return node;
|
||||
};
|
||||
|
||||
groupTypes.op = function(group, options) {
|
||||
let node;
|
||||
|
||||
// TODO(emily): handle big operators using the `largeop` attribute
|
||||
|
||||
if (group.value.symbol) {
|
||||
// This is a symbol. Just add the symbol.
|
||||
node = new mathMLTree.MathNode(
|
||||
"mo", [makeText(group.value.body, group.mode)]);
|
||||
} else if (group.value.value) {
|
||||
// This is an operator with children. Add them.
|
||||
node = new mathMLTree.MathNode(
|
||||
"mo", buildExpression(group.value.value, options));
|
||||
} else {
|
||||
// This is a text operator. Add all of the characters from the
|
||||
// operator's name.
|
||||
// TODO(emily): Add a space in the middle of some of these
|
||||
// operators, like \limsup.
|
||||
node = new mathMLTree.MathNode(
|
||||
"mi", [new mathMLTree.TextNode(group.value.body.slice(1))]);
|
||||
|
||||
// TODO(ron): Append an <mo>⁡</mo> as in \operatorname
|
||||
// ref: https://www.w3.org/TR/REC-MathML/chap3_2.html#sec3.2.2
|
||||
}
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
groupTypes.mod = function(group, options) {
|
||||
let inner = [];
|
||||
|
||||
if (group.value.modType === "pod" || group.value.modType === "pmod") {
|
||||
inner.push(new mathMLTree.MathNode(
|
||||
"mo", [makeText("(", group.mode)]));
|
||||
}
|
||||
if (group.value.modType !== "pod") {
|
||||
inner.push(new mathMLTree.MathNode(
|
||||
"mo", [makeText("mod", group.mode)]));
|
||||
}
|
||||
if (group.value.value) {
|
||||
const space = new mathMLTree.MathNode("mspace");
|
||||
space.setAttribute("width", "0.333333em");
|
||||
inner.push(space);
|
||||
inner = inner.concat(buildExpression(group.value.value, options));
|
||||
}
|
||||
if (group.value.modType === "pod" || group.value.modType === "pmod") {
|
||||
inner.push(new mathMLTree.MathNode(
|
||||
"mo", [makeText(")", group.mode)]));
|
||||
}
|
||||
|
||||
return new mathMLTree.MathNode("mo", inner);
|
||||
};
|
||||
|
||||
groupTypes.katex = function(group) {
|
||||
const node = new mathMLTree.MathNode(
|
||||
"mtext", [new mathMLTree.TextNode("KaTeX")]);
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
groupTypes.font = function(group, options) {
|
||||
const font = group.value.font;
|
||||
return buildGroup(group.value.body, options.withFont(font));
|
||||
@@ -578,35 +475,6 @@ groupTypes.kern = function(group) {
|
||||
return node;
|
||||
};
|
||||
|
||||
groupTypes.lap = function(group, options) {
|
||||
// mathllap, mathrlap, mathclap
|
||||
const node = new mathMLTree.MathNode(
|
||||
"mpadded", [buildGroup(group.value.body, options)]);
|
||||
|
||||
if (group.value.alignment !== "rlap") {
|
||||
const offset = (group.value.alignment === "llap" ? "-1" : "-0.5");
|
||||
node.setAttribute("lspace", offset + "width");
|
||||
}
|
||||
node.setAttribute("width", "0px");
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
groupTypes.smash = function(group, options) {
|
||||
const node = new mathMLTree.MathNode(
|
||||
"mpadded", [buildGroup(group.value.body, options)]);
|
||||
|
||||
if (group.value.smashHeight) {
|
||||
node.setAttribute("height", "0px");
|
||||
}
|
||||
|
||||
if (group.value.smashDepth) {
|
||||
node.setAttribute("depth", "0px");
|
||||
}
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
groupTypes.mclass = function(group, options) {
|
||||
const inner = buildExpression(group.value.value, options);
|
||||
return new mathMLTree.MathNode("mstyle", inner);
|
||||
|
174
src/functions.js
174
src/functions.js
@@ -175,15 +175,7 @@ defineFunction(["\\kern", "\\mkern"], {
|
||||
};
|
||||
});
|
||||
|
||||
// A KaTeX logo
|
||||
defineFunction(["\\KaTeX"], {
|
||||
numArgs: 0,
|
||||
allowedInText: true,
|
||||
}, function(context) {
|
||||
return {
|
||||
type: "katex",
|
||||
};
|
||||
});
|
||||
import "./functions/katex";
|
||||
|
||||
import "./functions/phantom";
|
||||
|
||||
@@ -230,27 +222,7 @@ defineFunction(["\\stackrel"], {
|
||||
};
|
||||
});
|
||||
|
||||
// \mod-type functions
|
||||
defineFunction(["\\bmod"], {
|
||||
numArgs: 0,
|
||||
}, function(context, args) {
|
||||
return {
|
||||
type: "mod",
|
||||
modType: "bmod",
|
||||
value: null,
|
||||
};
|
||||
});
|
||||
|
||||
defineFunction(["\\pod", "\\pmod", "\\mod"], {
|
||||
numArgs: 1,
|
||||
}, function(context, args) {
|
||||
const body = args[0];
|
||||
return {
|
||||
type: "mod",
|
||||
modType: context.funcName.substr(1),
|
||||
value: ordargument(body),
|
||||
};
|
||||
});
|
||||
import "./functions/mod";
|
||||
|
||||
const fontAliases = {
|
||||
"\\Bbb": "\\mathbb",
|
||||
@@ -337,147 +309,15 @@ defineFunction([
|
||||
};
|
||||
});
|
||||
|
||||
// Limits, symbols
|
||||
defineFunction([
|
||||
"\\coprod", "\\bigvee", "\\bigwedge", "\\biguplus", "\\bigcap",
|
||||
"\\bigcup", "\\intop", "\\prod", "\\sum", "\\bigotimes",
|
||||
"\\bigoplus", "\\bigodot", "\\bigsqcup", "\\smallint",
|
||||
], {
|
||||
numArgs: 0,
|
||||
}, function(context) {
|
||||
return {
|
||||
type: "op",
|
||||
limits: true,
|
||||
symbol: true,
|
||||
body: context.funcName,
|
||||
};
|
||||
});
|
||||
import "./functions/op";
|
||||
|
||||
// \mathop class command
|
||||
defineFunction(["\\mathop"], {
|
||||
numArgs: 1,
|
||||
}, function(context, args) {
|
||||
const body = args[0];
|
||||
return {
|
||||
type: "op",
|
||||
limits: false,
|
||||
symbol: false,
|
||||
value: ordargument(body),
|
||||
};
|
||||
});
|
||||
import "./functions/operatorname";
|
||||
|
||||
import "./functions/operators";
|
||||
import "./functions/genfrac";
|
||||
|
||||
// Fractions
|
||||
defineFunction([
|
||||
"\\dfrac", "\\frac", "\\tfrac",
|
||||
"\\dbinom", "\\binom", "\\tbinom",
|
||||
"\\\\atopfrac", // can’t be entered directly
|
||||
], {
|
||||
numArgs: 2,
|
||||
greediness: 2,
|
||||
}, function(context, args) {
|
||||
const numer = args[0];
|
||||
const denom = args[1];
|
||||
let hasBarLine;
|
||||
let leftDelim = null;
|
||||
let rightDelim = null;
|
||||
let size = "auto";
|
||||
import "./functions/lap";
|
||||
|
||||
switch (context.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;
|
||||
default:
|
||||
throw new Error("Unrecognized genfrac command");
|
||||
}
|
||||
|
||||
switch (context.funcName) {
|
||||
case "\\dfrac":
|
||||
case "\\dbinom":
|
||||
size = "display";
|
||||
break;
|
||||
case "\\tfrac":
|
||||
case "\\tbinom":
|
||||
size = "text";
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
type: "genfrac",
|
||||
numer: numer,
|
||||
denom: denom,
|
||||
hasBarLine: hasBarLine,
|
||||
leftDelim: leftDelim,
|
||||
rightDelim: rightDelim,
|
||||
size: size,
|
||||
};
|
||||
});
|
||||
|
||||
// Horizontal overlap functions
|
||||
defineFunction(["\\mathllap", "\\mathrlap", "\\mathclap"], {
|
||||
numArgs: 1,
|
||||
allowedInText: true,
|
||||
}, function(context, args) {
|
||||
const body = args[0];
|
||||
return {
|
||||
type: "lap",
|
||||
alignment: context.funcName.slice(5),
|
||||
body: body,
|
||||
};
|
||||
});
|
||||
|
||||
// smash, with optional [tb], as in AMS
|
||||
defineFunction(["\\smash"], {
|
||||
numArgs: 1,
|
||||
numOptionalArgs: 1,
|
||||
allowedInText: true,
|
||||
}, function(context, args, optArgs) {
|
||||
let smashHeight = false;
|
||||
let smashDepth = false;
|
||||
const tbArg = optArgs[0];
|
||||
if (tbArg) {
|
||||
// Optional [tb] argument is engaged.
|
||||
// ref: amsmath: \renewcommand{\smash}[1][tb]{%
|
||||
// def\mb@t{\ht}\def\mb@b{\dp}\def\mb@tb{\ht\z@\z@\dp}%
|
||||
let letter = "";
|
||||
for (let i = 0; i < tbArg.value.length; ++i) {
|
||||
letter = tbArg.value[i].value;
|
||||
if (letter === "t") {
|
||||
smashHeight = true;
|
||||
} else if (letter === "b") {
|
||||
smashDepth = true;
|
||||
} else {
|
||||
smashHeight = false;
|
||||
smashDepth = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
smashHeight = true;
|
||||
smashDepth = true;
|
||||
}
|
||||
|
||||
const body = args[0];
|
||||
return {
|
||||
type: "smash",
|
||||
body: body,
|
||||
smashHeight: smashHeight,
|
||||
smashDepth: smashDepth,
|
||||
};
|
||||
});
|
||||
import "./functions/smash";
|
||||
|
||||
import "./functions/delimsizing";
|
||||
|
||||
|
253
src/functions/genfrac.js
Normal file
253
src/functions/genfrac.js
Normal file
@@ -0,0 +1,253 @@
|
||||
// @flow
|
||||
import defineFunction from "../defineFunction";
|
||||
import buildCommon from "../buildCommon";
|
||||
import delimiter from "../delimiter";
|
||||
import mathMLTree from "../mathMLTree";
|
||||
import Style from "../Style";
|
||||
|
||||
import * as html from "../buildHTML";
|
||||
import * as mml from "../buildMathML";
|
||||
|
||||
defineFunction({
|
||||
type: "genfrac",
|
||||
names: [
|
||||
"\\dfrac", "\\frac", "\\tfrac",
|
||||
"\\dbinom", "\\binom", "\\tbinom",
|
||||
"\\\\atopfrac", // can’t be entered directly
|
||||
],
|
||||
props: {
|
||||
numArgs: 2,
|
||||
greediness: 2,
|
||||
},
|
||||
handler: (context, args) => {
|
||||
const numer = args[0];
|
||||
const denom = args[1];
|
||||
let hasBarLine;
|
||||
let leftDelim = null;
|
||||
let rightDelim = null;
|
||||
let size = "auto";
|
||||
|
||||
switch (context.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;
|
||||
default:
|
||||
throw new Error("Unrecognized genfrac command");
|
||||
}
|
||||
|
||||
switch (context.funcName) {
|
||||
case "\\dfrac":
|
||||
case "\\dbinom":
|
||||
size = "display";
|
||||
break;
|
||||
case "\\tfrac":
|
||||
case "\\tbinom":
|
||||
size = "text";
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
type: "genfrac",
|
||||
numer: numer,
|
||||
denom: denom,
|
||||
hasBarLine: hasBarLine,
|
||||
leftDelim: leftDelim,
|
||||
rightDelim: rightDelim,
|
||||
size: size,
|
||||
};
|
||||
},
|
||||
htmlBuilder: (group, options) => {
|
||||
// Fractions are handled in the TeXbook on pages 444-445, rules 15(a-e).
|
||||
// Figure out what style this fraction should be in based on the
|
||||
// function used
|
||||
let style = options.style;
|
||||
if (group.value.size === "display") {
|
||||
style = Style.DISPLAY;
|
||||
} else if (group.value.size === "text") {
|
||||
style = Style.TEXT;
|
||||
}
|
||||
|
||||
const nstyle = style.fracNum();
|
||||
const dstyle = style.fracDen();
|
||||
let newOptions;
|
||||
|
||||
newOptions = options.havingStyle(nstyle);
|
||||
const numerm = html.buildGroup(group.value.numer, newOptions, options);
|
||||
|
||||
newOptions = options.havingStyle(dstyle);
|
||||
const denomm = html.buildGroup(group.value.denom, newOptions, options);
|
||||
|
||||
let rule;
|
||||
let ruleWidth;
|
||||
let ruleSpacing;
|
||||
if (group.value.hasBarLine) {
|
||||
rule = html.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) {
|
||||
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 (ruleWidth === 0) {
|
||||
// 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.value.leftDelim == null) {
|
||||
leftDelim = html.makeNullDelimiter(options, ["mopen"]);
|
||||
} else {
|
||||
leftDelim = delimiter.customSizedDelim(
|
||||
group.value.leftDelim, delimSize, true,
|
||||
options.havingStyle(style), group.mode, ["mopen"]);
|
||||
}
|
||||
if (group.value.rightDelim == null) {
|
||||
rightDelim = html.makeNullDelimiter(options, ["mclose"]);
|
||||
} else {
|
||||
rightDelim = delimiter.customSizedDelim(
|
||||
group.value.rightDelim, delimSize, true,
|
||||
options.havingStyle(style), group.mode, ["mclose"]);
|
||||
}
|
||||
|
||||
return buildCommon.makeSpan(
|
||||
["mord"].concat(newOptions.sizingClasses(options)),
|
||||
[leftDelim, buildCommon.makeSpan(["mfrac"], [frac]), rightDelim],
|
||||
options);
|
||||
},
|
||||
mathmlBuilder: (group, options) => {
|
||||
const node = new mathMLTree.MathNode(
|
||||
"mfrac",
|
||||
[
|
||||
mml.buildGroup(group.value.numer, options),
|
||||
mml.buildGroup(group.value.denom, options),
|
||||
]);
|
||||
|
||||
if (!group.value.hasBarLine) {
|
||||
node.setAttribute("linethickness", "0px");
|
||||
}
|
||||
|
||||
if (group.value.leftDelim != null || group.value.rightDelim != null) {
|
||||
const withDelims = [];
|
||||
|
||||
if (group.value.leftDelim != null) {
|
||||
const leftOp = new mathMLTree.MathNode(
|
||||
"mo", [new mathMLTree.TextNode(group.value.leftDelim)]);
|
||||
|
||||
leftOp.setAttribute("fence", "true");
|
||||
|
||||
withDelims.push(leftOp);
|
||||
}
|
||||
|
||||
withDelims.push(node);
|
||||
|
||||
if (group.value.rightDelim != null) {
|
||||
const rightOp = new mathMLTree.MathNode(
|
||||
"mo", [new mathMLTree.TextNode(group.value.rightDelim)]);
|
||||
|
||||
rightOp.setAttribute("fence", "true");
|
||||
|
||||
withDelims.push(rightOp);
|
||||
}
|
||||
|
||||
const outerNode = new mathMLTree.MathNode("mrow", withDelims);
|
||||
|
||||
return outerNode;
|
||||
}
|
||||
|
||||
return node;
|
||||
},
|
||||
});
|
51
src/functions/katex.js
Normal file
51
src/functions/katex.js
Normal file
@@ -0,0 +1,51 @@
|
||||
// @flow
|
||||
// A KaTeX logo
|
||||
import defineFunction from "../defineFunction";
|
||||
import buildCommon from "../buildCommon";
|
||||
import mathMLTree from "../mathMLTree";
|
||||
|
||||
defineFunction({
|
||||
type: "katex",
|
||||
names: ["\\KaTeX"],
|
||||
props: {
|
||||
numArgs: 0,
|
||||
allowedInText: true,
|
||||
},
|
||||
handler: (context, args) => {
|
||||
return {
|
||||
type: "katex",
|
||||
};
|
||||
},
|
||||
htmlBuilder: (group, options) => {
|
||||
// The KaTeX logo. The offsets for the K and a were chosen to look
|
||||
// good, but the offsets for the T, E, and X were taken from the
|
||||
// definition of \TeX in TeX (see TeXbook pg. 356)
|
||||
const k = buildCommon.makeSpan(
|
||||
["k"], [buildCommon.mathsym("K", group.mode)], options);
|
||||
const a = buildCommon.makeSpan(
|
||||
["a"], [buildCommon.mathsym("A", group.mode)], options);
|
||||
|
||||
a.height = (a.height + 0.2) * 0.75;
|
||||
a.depth = (a.height - 0.2) * 0.75;
|
||||
|
||||
const t = buildCommon.makeSpan(
|
||||
["t"], [buildCommon.mathsym("T", group.mode)], options);
|
||||
const e = buildCommon.makeSpan(
|
||||
["e"], [buildCommon.mathsym("E", group.mode)], options);
|
||||
|
||||
e.height = (e.height - 0.2155);
|
||||
e.depth = (e.depth + 0.2155);
|
||||
|
||||
const x = buildCommon.makeSpan(
|
||||
["x"], [buildCommon.mathsym("X", group.mode)], options);
|
||||
|
||||
return buildCommon.makeSpan(
|
||||
["mord", "katex-logo"], [k, a, t, e, x], options);
|
||||
},
|
||||
mathmlBuilder: (group, options) => {
|
||||
const node = new mathMLTree.MathNode(
|
||||
"mtext", [new mathMLTree.TextNode("KaTeX")]);
|
||||
|
||||
return node;
|
||||
},
|
||||
});
|
55
src/functions/lap.js
Normal file
55
src/functions/lap.js
Normal file
@@ -0,0 +1,55 @@
|
||||
// @flow
|
||||
// Horizontal overlap functions
|
||||
import defineFunction from "../defineFunction";
|
||||
import buildCommon from "../buildCommon";
|
||||
import mathMLTree from "../mathMLTree";
|
||||
|
||||
import * as html from "../buildHTML";
|
||||
import * as mml from "../buildMathML";
|
||||
|
||||
defineFunction({
|
||||
type: "lap",
|
||||
names: ["\\mathllap", "\\mathrlap", "\\mathclap"],
|
||||
props: {
|
||||
numArgs: 1,
|
||||
allowedInText: true,
|
||||
},
|
||||
handler: (context, args) => {
|
||||
const body = args[0];
|
||||
return {
|
||||
type: "lap",
|
||||
alignment: context.funcName.slice(5),
|
||||
body: body,
|
||||
};
|
||||
},
|
||||
htmlBuilder: (group, options) => {
|
||||
// mathllap, mathrlap, mathclap
|
||||
let inner;
|
||||
if (group.value.alignment === "clap") {
|
||||
// ref: https://www.math.lsu.edu/~aperlis/publications/mathclap/
|
||||
inner = buildCommon.makeSpan(
|
||||
[], [html.buildGroup(group.value.body, options)]);
|
||||
// wrap, since CSS will center a .clap > .inner > span
|
||||
inner = buildCommon.makeSpan(["inner"], [inner], options);
|
||||
} else {
|
||||
inner = buildCommon.makeSpan(
|
||||
["inner"], [html.buildGroup(group.value.body, options)]);
|
||||
}
|
||||
const fix = buildCommon.makeSpan(["fix"], []);
|
||||
return buildCommon.makeSpan(
|
||||
["mord", group.value.alignment], [inner, fix], options);
|
||||
},
|
||||
mathmlBuilder: (group, options) => {
|
||||
// mathllap, mathrlap, mathclap
|
||||
const node = new mathMLTree.MathNode(
|
||||
"mpadded", [mml.buildGroup(group.value.body, options)]);
|
||||
|
||||
if (group.value.alignment !== "rlap") {
|
||||
const offset = (group.value.alignment === "llap" ? "-1" : "-0.5");
|
||||
node.setAttribute("lspace", offset + "width");
|
||||
}
|
||||
node.setAttribute("width", "0px");
|
||||
|
||||
return node;
|
||||
},
|
||||
});
|
129
src/functions/mod.js
Normal file
129
src/functions/mod.js
Normal file
@@ -0,0 +1,129 @@
|
||||
// @flow
|
||||
// \mod-type functions
|
||||
import defineFunction, {ordargument} from "../defineFunction";
|
||||
import buildCommon from "../buildCommon";
|
||||
import mathMLTree from "../mathMLTree";
|
||||
import Style from "../Style";
|
||||
|
||||
import * as html from "../buildHTML";
|
||||
import * as mml from "../buildMathML";
|
||||
|
||||
const htmlModBuilder = (group, options) => {
|
||||
const inner = [];
|
||||
|
||||
if (group.value.modType === "bmod") {
|
||||
// “\nonscript\mskip-\medmuskip\mkern5mu”
|
||||
if (!options.style.isTight()) {
|
||||
inner.push(buildCommon.makeSpan(
|
||||
["mspace", "negativemediumspace"], [], options));
|
||||
}
|
||||
inner.push(
|
||||
buildCommon.makeSpan(["mspace", "thickspace"], [], options));
|
||||
} else if (options.style.size === Style.DISPLAY.size) {
|
||||
inner.push(buildCommon.makeSpan(["mspace", "quad"], [], options));
|
||||
} else if (group.value.modType === "mod") {
|
||||
inner.push(
|
||||
buildCommon.makeSpan(["mspace", "twelvemuspace"], [], options));
|
||||
} else {
|
||||
inner.push(
|
||||
buildCommon.makeSpan(["mspace", "eightmuspace"], [], options));
|
||||
}
|
||||
|
||||
if (group.value.modType === "pod" || group.value.modType === "pmod") {
|
||||
inner.push(buildCommon.mathsym("(", group.mode));
|
||||
}
|
||||
|
||||
if (group.value.modType !== "pod") {
|
||||
const modInner = [
|
||||
buildCommon.mathsym("m", group.mode),
|
||||
buildCommon.mathsym("o", group.mode),
|
||||
buildCommon.mathsym("d", group.mode)];
|
||||
if (group.value.modType === "bmod") {
|
||||
inner.push(buildCommon.makeSpan(["mbin"], modInner, options));
|
||||
// “\mkern5mu\nonscript\mskip-\medmuskip”
|
||||
inner.push(
|
||||
buildCommon.makeSpan(["mspace", "thickspace"], [], options));
|
||||
if (!options.style.isTight()) {
|
||||
inner.push(buildCommon.makeSpan(
|
||||
["mspace", "negativemediumspace"], [], options));
|
||||
}
|
||||
} else {
|
||||
Array.prototype.push.apply(inner, modInner);
|
||||
inner.push(
|
||||
buildCommon.makeSpan(["mspace", "sixmuspace"], [], options));
|
||||
}
|
||||
}
|
||||
|
||||
if (group.value.value) {
|
||||
Array.prototype.push.apply(inner,
|
||||
html.buildExpression(group.value.value, options, false));
|
||||
}
|
||||
|
||||
if (group.value.modType === "pod" || group.value.modType === "pmod") {
|
||||
inner.push(buildCommon.mathsym(")", group.mode));
|
||||
}
|
||||
|
||||
return buildCommon.makeFragment(inner);
|
||||
};
|
||||
|
||||
const mmlModBuilder = (group, options) => {
|
||||
let inner = [];
|
||||
|
||||
if (group.value.modType === "pod" || group.value.modType === "pmod") {
|
||||
inner.push(new mathMLTree.MathNode(
|
||||
"mo", [mml.makeText("(", group.mode)]));
|
||||
}
|
||||
if (group.value.modType !== "pod") {
|
||||
inner.push(new mathMLTree.MathNode(
|
||||
"mo", [mml.makeText("mod", group.mode)]));
|
||||
}
|
||||
if (group.value.value) {
|
||||
const space = new mathMLTree.MathNode("mspace");
|
||||
space.setAttribute("width", "0.333333em");
|
||||
inner.push(space);
|
||||
inner = inner.concat(mml.buildExpression(group.value.value, options));
|
||||
}
|
||||
if (group.value.modType === "pod" || group.value.modType === "pmod") {
|
||||
inner.push(new mathMLTree.MathNode(
|
||||
"mo", [mml.makeText(")", group.mode)]));
|
||||
}
|
||||
|
||||
return new mathMLTree.MathNode("mo", inner);
|
||||
};
|
||||
|
||||
defineFunction({
|
||||
type: "mod",
|
||||
names: ["\\bmod"],
|
||||
props: {
|
||||
numArgs: 0,
|
||||
},
|
||||
handler: (context, args) => {
|
||||
return {
|
||||
type: "mod",
|
||||
modType: "bmod",
|
||||
value: null,
|
||||
};
|
||||
},
|
||||
htmlBuilder: htmlModBuilder,
|
||||
mathmlBuilder: mmlModBuilder,
|
||||
});
|
||||
|
||||
// Note: calling defineFunction with a type that's already been defined only
|
||||
// works because the same htmlBuilder and mathmlBuilder are being used.
|
||||
defineFunction({
|
||||
type: "mod",
|
||||
names: ["\\pod", "\\pmod", "\\mod"],
|
||||
props: {
|
||||
numArgs: 1,
|
||||
},
|
||||
handler: (context, args) => {
|
||||
const body = args[0];
|
||||
return {
|
||||
type: "mod",
|
||||
modType: context.funcName.substr(1),
|
||||
value: ordargument(body),
|
||||
};
|
||||
},
|
||||
htmlBuilder: htmlModBuilder,
|
||||
mathmlBuilder: mmlModBuilder,
|
||||
});
|
260
src/functions/op.js
Normal file
260
src/functions/op.js
Normal file
@@ -0,0 +1,260 @@
|
||||
// @flow
|
||||
// Limits, symbols
|
||||
import defineFunction, {ordargument} from "../defineFunction";
|
||||
import buildCommon from "../buildCommon";
|
||||
import domTree from "../domTree";
|
||||
import mathMLTree from "../mathMLTree";
|
||||
import utils from "../utils";
|
||||
import Style from "../Style";
|
||||
|
||||
import * as html from "../buildHTML";
|
||||
import * as mml from "../buildMathML";
|
||||
|
||||
const htmlBuilder = (group, options) => {
|
||||
// Operators are handled in the TeXbook pg. 443-444, rule 13(a).
|
||||
let supGroup;
|
||||
let subGroup;
|
||||
let hasLimits = false;
|
||||
if (group.type === "supsub") {
|
||||
// If we have limits, supsub will pass us its group to handle. Pull
|
||||
// out the superscript and subscript and set the group to the op in
|
||||
// its base.
|
||||
supGroup = group.value.sup;
|
||||
subGroup = group.value.sub;
|
||||
group = group.value.base;
|
||||
hasLimits = true;
|
||||
}
|
||||
|
||||
const style = options.style;
|
||||
|
||||
// Most operators have a large successor symbol, but these don't.
|
||||
const noSuccessor = [
|
||||
"\\smallint",
|
||||
];
|
||||
|
||||
let large = false;
|
||||
if (style.size === Style.DISPLAY.size &&
|
||||
group.value.symbol &&
|
||||
!utils.contains(noSuccessor, group.value.body)) {
|
||||
|
||||
// Most symbol operators get larger in displaystyle (rule 13)
|
||||
large = true;
|
||||
}
|
||||
|
||||
let base;
|
||||
if (group.value.symbol) {
|
||||
// If this is a symbol, create the symbol.
|
||||
const fontName = large ? "Size2-Regular" : "Size1-Regular";
|
||||
base = buildCommon.makeSymbol(
|
||||
group.value.body, fontName, "math", options,
|
||||
["mop", "op-symbol", large ? "large-op" : "small-op"]);
|
||||
} else if (group.value.value) {
|
||||
// If this is a list, compose that list.
|
||||
const inner = html.buildExpression(group.value.value, options, true);
|
||||
if (inner.length === 1 && inner[0] instanceof domTree.symbolNode) {
|
||||
base = inner[0];
|
||||
base.classes[0] = "mop"; // replace old mclass
|
||||
} else {
|
||||
base = buildCommon.makeSpan(["mop"], inner, options);
|
||||
}
|
||||
} else {
|
||||
// Otherwise, this is a text operator. Build the text from the
|
||||
// operator's name.
|
||||
// TODO(emily): Add a space in the middle of some of these
|
||||
// operators, like \limsup
|
||||
const output = [];
|
||||
for (let i = 1; i < group.value.body.length; i++) {
|
||||
output.push(buildCommon.mathsym(group.value.body[i], group.mode));
|
||||
}
|
||||
base = buildCommon.makeSpan(["mop"], output, options);
|
||||
}
|
||||
|
||||
// If content of op is a single symbol, shift it vertically.
|
||||
let baseShift = 0;
|
||||
let slant = 0;
|
||||
if (base instanceof domTree.symbolNode) {
|
||||
// Shift the symbol so its center lies on the axis (rule 13). It
|
||||
// appears that our fonts have the centers of the symbols already
|
||||
// almost on the axis, so these numbers are very small. Note we
|
||||
// don't actually apply this here, but instead it is used either in
|
||||
// the vlist creation or separately when there are no limits.
|
||||
baseShift = (base.height - base.depth) / 2 -
|
||||
options.fontMetrics().axisHeight;
|
||||
|
||||
// The slant of the symbol is just its italic correction.
|
||||
slant = base.italic;
|
||||
}
|
||||
|
||||
if (hasLimits) {
|
||||
// IE 8 clips \int if it is in a display: inline-block. We wrap it
|
||||
// in a new span so it is an inline, and works.
|
||||
base = buildCommon.makeSpan([], [base]);
|
||||
|
||||
let supm;
|
||||
let supKern;
|
||||
let subm = {height: 0, depth: 0}; // Make flow happy
|
||||
let subKern;
|
||||
let newOptions;
|
||||
// We manually have to handle the superscripts and subscripts. This,
|
||||
// aside from the kern calculations, is copied from supsub.
|
||||
if (supGroup) {
|
||||
newOptions = options.havingStyle(style.sup());
|
||||
supm = html.buildGroup(supGroup, newOptions, options);
|
||||
|
||||
supKern = Math.max(
|
||||
options.fontMetrics().bigOpSpacing1,
|
||||
options.fontMetrics().bigOpSpacing3 - supm.depth);
|
||||
}
|
||||
|
||||
if (subGroup) {
|
||||
newOptions = options.havingStyle(style.sub());
|
||||
subm = html.buildGroup(subGroup, newOptions, options);
|
||||
|
||||
subKern = Math.max(
|
||||
options.fontMetrics().bigOpSpacing2,
|
||||
options.fontMetrics().bigOpSpacing4 - subm.height);
|
||||
}
|
||||
|
||||
// Build the final group as a vlist of the possible subscript, base,
|
||||
// and possible superscript.
|
||||
let finalGroup;
|
||||
let top;
|
||||
let bottom;
|
||||
if (!supGroup) {
|
||||
top = base.height - baseShift;
|
||||
|
||||
// Shift the limits by the slant of the symbol. Note
|
||||
// that we are supposed to shift the limits by 1/2 of the slant,
|
||||
// but since we are centering the limits adding a full slant of
|
||||
// margin will shift by 1/2 that.
|
||||
finalGroup = buildCommon.makeVList({
|
||||
positionType: "top",
|
||||
positionData: top,
|
||||
children: [
|
||||
{type: "kern", size: options.fontMetrics().bigOpSpacing5},
|
||||
{type: "elem", elem: subm, marginLeft: -slant + "em"},
|
||||
{type: "kern", size: subKern},
|
||||
{type: "elem", elem: base},
|
||||
],
|
||||
}, options);
|
||||
} else if (!subGroup) {
|
||||
bottom = base.depth + baseShift;
|
||||
|
||||
finalGroup = buildCommon.makeVList({
|
||||
positionType: "bottom",
|
||||
positionData: bottom,
|
||||
children: [
|
||||
{type: "elem", elem: base},
|
||||
{type: "kern", size: supKern},
|
||||
{type: "elem", elem: supm, marginLeft: slant + "em"},
|
||||
{type: "kern", size: options.fontMetrics().bigOpSpacing5},
|
||||
],
|
||||
}, options);
|
||||
} else if (!supGroup && !subGroup) {
|
||||
// This case probably shouldn't occur (this would mean the
|
||||
// supsub was sending us a group with no superscript or
|
||||
// subscript) but be safe.
|
||||
return base;
|
||||
} else {
|
||||
bottom = options.fontMetrics().bigOpSpacing5 +
|
||||
subm.height + subm.depth +
|
||||
subKern +
|
||||
base.depth + baseShift;
|
||||
|
||||
finalGroup = buildCommon.makeVList({
|
||||
positionType: "bottom",
|
||||
positionData: bottom,
|
||||
children: [
|
||||
{type: "kern", size: options.fontMetrics().bigOpSpacing5},
|
||||
{type: "elem", elem: subm, marginLeft: -slant + "em"},
|
||||
{type: "kern", size: subKern},
|
||||
{type: "elem", elem: base},
|
||||
{type: "kern", size: supKern},
|
||||
{type: "elem", elem: supm, marginLeft: slant + "em"},
|
||||
{type: "kern", size: options.fontMetrics().bigOpSpacing5},
|
||||
],
|
||||
}, options);
|
||||
}
|
||||
|
||||
return buildCommon.makeSpan(
|
||||
["mop", "op-limits"], [finalGroup], options);
|
||||
} else {
|
||||
if (baseShift) {
|
||||
base.style.position = "relative";
|
||||
base.style.top = baseShift + "em";
|
||||
}
|
||||
|
||||
return base;
|
||||
}
|
||||
};
|
||||
|
||||
const mathmlBuilder = (group, options) => {
|
||||
let node;
|
||||
|
||||
// TODO(emily): handle big operators using the `largeop` attribute
|
||||
|
||||
if (group.value.symbol) {
|
||||
// This is a symbol. Just add the symbol.
|
||||
node = new mathMLTree.MathNode(
|
||||
"mo", [mml.makeText(group.value.body, group.mode)]);
|
||||
} else if (group.value.value) {
|
||||
// This is an operator with children. Add them.
|
||||
node = new mathMLTree.MathNode(
|
||||
"mo", mml.buildExpression(group.value.value, options));
|
||||
} else {
|
||||
// This is a text operator. Add all of the characters from the
|
||||
// operator's name.
|
||||
// TODO(emily): Add a space in the middle of some of these
|
||||
// operators, like \limsup.
|
||||
node = new mathMLTree.MathNode(
|
||||
"mi", [new mathMLTree.TextNode(group.value.body.slice(1))]);
|
||||
|
||||
// TODO(ron): Append an <mo>⁡</mo> as in \operatorname
|
||||
// ref: https://www.w3.org/TR/REC-MathML/chap3_2.html#sec3.2.2
|
||||
}
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
defineFunction({
|
||||
type: "op",
|
||||
names: [
|
||||
"\\coprod", "\\bigvee", "\\bigwedge", "\\biguplus", "\\bigcap",
|
||||
"\\bigcup", "\\intop", "\\prod", "\\sum", "\\bigotimes",
|
||||
"\\bigoplus", "\\bigodot", "\\bigsqcup", "\\smallint",
|
||||
],
|
||||
props: {
|
||||
numArgs: 0,
|
||||
},
|
||||
handler: (context, args) => {
|
||||
return {
|
||||
type: "op",
|
||||
limits: true,
|
||||
symbol: true,
|
||||
body: context.funcName,
|
||||
};
|
||||
},
|
||||
htmlBuilder,
|
||||
mathmlBuilder,
|
||||
});
|
||||
|
||||
// Note: calling defineFunction with a type that's already been defined only
|
||||
// works because the same htmlBuilder and mathmlBuilder are being used.
|
||||
defineFunction({
|
||||
type: "op",
|
||||
names: ["\\mathop"],
|
||||
props: {
|
||||
numArgs: 1,
|
||||
},
|
||||
handler: (context, args) => {
|
||||
const body = args[0];
|
||||
return {
|
||||
type: "op",
|
||||
limits: false,
|
||||
symbol: false,
|
||||
value: ordargument(body),
|
||||
};
|
||||
},
|
||||
htmlBuilder,
|
||||
mathmlBuilder,
|
||||
});
|
103
src/functions/smash.js
Normal file
103
src/functions/smash.js
Normal file
@@ -0,0 +1,103 @@
|
||||
// @flow
|
||||
// smash, with optional [tb], as in AMS
|
||||
import defineFunction from "../defineFunction";
|
||||
import buildCommon from "../buildCommon";
|
||||
import mathMLTree from "../mathMLTree";
|
||||
|
||||
import * as html from "../buildHTML";
|
||||
import * as mml from "../buildMathML";
|
||||
|
||||
defineFunction({
|
||||
type: "smash",
|
||||
names: ["\\smash"],
|
||||
props: {
|
||||
numArgs: 1,
|
||||
numOptionalArgs: 1,
|
||||
allowedInText: true,
|
||||
},
|
||||
handler: (context, args, optArgs) => {
|
||||
let smashHeight = false;
|
||||
let smashDepth = false;
|
||||
const tbArg = optArgs[0];
|
||||
if (tbArg) {
|
||||
// Optional [tb] argument is engaged.
|
||||
// ref: amsmath: \renewcommand{\smash}[1][tb]{%
|
||||
// def\mb@t{\ht}\def\mb@b{\dp}\def\mb@tb{\ht\z@\z@\dp}%
|
||||
let letter = "";
|
||||
for (let i = 0; i < tbArg.value.length; ++i) {
|
||||
letter = tbArg.value[i].value;
|
||||
if (letter === "t") {
|
||||
smashHeight = true;
|
||||
} else if (letter === "b") {
|
||||
smashDepth = true;
|
||||
} else {
|
||||
smashHeight = false;
|
||||
smashDepth = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
smashHeight = true;
|
||||
smashDepth = true;
|
||||
}
|
||||
|
||||
const body = args[0];
|
||||
return {
|
||||
type: "smash",
|
||||
body: body,
|
||||
smashHeight: smashHeight,
|
||||
smashDepth: smashDepth,
|
||||
};
|
||||
},
|
||||
htmlBuilder: (group, options) => {
|
||||
const node = buildCommon.makeSpan(
|
||||
["mord"], [html.buildGroup(group.value.body, options)]);
|
||||
|
||||
if (!group.value.smashHeight && !group.value.smashDepth) {
|
||||
return node;
|
||||
}
|
||||
|
||||
if (group.value.smashHeight) {
|
||||
node.height = 0;
|
||||
// In order to influence makeVList, we have to reset the children.
|
||||
if (node.children) {
|
||||
for (let i = 0; i < node.children.length; i++) {
|
||||
node.children[i].height = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (group.value.smashDepth) {
|
||||
node.depth = 0;
|
||||
if (node.children) {
|
||||
for (let i = 0; i < node.children.length; i++) {
|
||||
node.children[i].depth = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// At this point, we've reset the TeX-like height and depth values.
|
||||
// But the span still has an HTML line height.
|
||||
// makeVList applies "display: table-cell", which prevents the browser
|
||||
// from acting on that line height. So we'll call makeVList now.
|
||||
|
||||
return buildCommon.makeVList({
|
||||
positionType: "firstBaseline",
|
||||
children: [{type: "elem", elem: node}],
|
||||
}, options);
|
||||
},
|
||||
mathmlBuilder: (group, options) => {
|
||||
const node = new mathMLTree.MathNode(
|
||||
"mpadded", [mml.buildGroup(group.value.body, options)]);
|
||||
|
||||
if (group.value.smashHeight) {
|
||||
node.setAttribute("height", "0px");
|
||||
}
|
||||
|
||||
if (group.value.smashDepth) {
|
||||
node.setAttribute("depth", "0px");
|
||||
}
|
||||
|
||||
return node;
|
||||
},
|
||||
});
|
Reference in New Issue
Block a user