
Summary: Add support for math-mode accents. This involves a couple changes. First, in order to correctly position the accents, we must know the kern between every character and the "skewchar" in that font. To do this, we improve our tfm parser to run the mini-kern-language and calculate kerns. We then export these into fontMetrics.js. Then, we add normal support for accents. In particular, we do some special handling for supsubs around accents. This involves building the supsub separately without the accent, and then replacing its base with the built accent. Finally, the character in the fonts for the \vec command is a combining unicode character, so it is shifted to the left, but none of the other characters do this. We add some special handling for \vec to account for this. Fixes #7 Test Plan: - Make sure tests pass - Make sure no huxley screenshots changed, and the new one looks good Reviewers: alpert Reviewed By: alpert Differential Revision: http://phabricator.khanacademy.org/D13157
886 lines
28 KiB
JavaScript
886 lines
28 KiB
JavaScript
var Options = require("./Options");
|
|
var ParseError = require("./ParseError");
|
|
var Style = require("./Style");
|
|
|
|
var buildCommon = require("./buildCommon");
|
|
var delimiter = require("./delimiter");
|
|
var domTree = require("./domTree");
|
|
var fontMetrics = require("./fontMetrics");
|
|
var parseTree = require("./parseTree");
|
|
var symbols = require("./symbols");
|
|
var utils = require("./utils");
|
|
|
|
var makeSpan = buildCommon.makeSpan;
|
|
|
|
var buildExpression = function(expression, options, prev) {
|
|
var groups = [];
|
|
for (var i = 0; i < expression.length; i++) {
|
|
var group = expression[i];
|
|
groups.push(buildGroup(group, options, prev));
|
|
prev = group;
|
|
}
|
|
return groups;
|
|
};
|
|
|
|
var groupToType = {
|
|
mathord: "mord",
|
|
textord: "mord",
|
|
bin: "mbin",
|
|
rel: "mrel",
|
|
text: "mord",
|
|
open: "mopen",
|
|
close: "mclose",
|
|
inner: "minner",
|
|
frac: "minner",
|
|
spacing: "mord",
|
|
punct: "mpunct",
|
|
ordgroup: "mord",
|
|
op: "mop",
|
|
katex: "mord",
|
|
overline: "mord",
|
|
rule: "mord",
|
|
leftright: "minner",
|
|
sqrt: "mord",
|
|
accent: "mord"
|
|
};
|
|
|
|
var getTypeOfGroup = function(group) {
|
|
if (group == null) {
|
|
// Like when typesetting $^3$
|
|
return groupToType.mathord;
|
|
} else if (group.type === "supsub") {
|
|
return getTypeOfGroup(group.value.base);
|
|
} else if (group.type === "llap" || group.type === "rlap") {
|
|
return getTypeOfGroup(group.value);
|
|
} else if (group.type === "color") {
|
|
return getTypeOfGroup(group.value.value);
|
|
} else if (group.type === "sizing") {
|
|
return getTypeOfGroup(group.value.value);
|
|
} else if (group.type === "styling") {
|
|
return getTypeOfGroup(group.value.value);
|
|
} else if (group.type === "delimsizing") {
|
|
return groupToType[group.value.delimType];
|
|
} else {
|
|
return groupToType[group.type];
|
|
}
|
|
};
|
|
|
|
var shouldHandleSupSub = function(group, options) {
|
|
if (group == null) {
|
|
return false;
|
|
} else if (group.type === "op") {
|
|
return group.value.limits && options.style.id === Style.DISPLAY.id;
|
|
} else if (group.type === "accent") {
|
|
return isCharacterBox(group.value.base);
|
|
} else {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
var getBaseElem = function(group) {
|
|
if (group == null) {
|
|
return false;
|
|
} else if (group.type === "ordgroup") {
|
|
if (group.value.length === 1) {
|
|
return getBaseElem(group.value[0]);
|
|
} else {
|
|
return group;
|
|
}
|
|
} else if (group.type === "color") {
|
|
if (group.value.value.length === 1) {
|
|
return getBaseElem(group.value.value[0]);
|
|
} else {
|
|
return group;
|
|
}
|
|
} else {
|
|
return group;
|
|
}
|
|
};
|
|
|
|
var isCharacterBox = function(group) {
|
|
var baseElem = getBaseElem(group);
|
|
|
|
return baseElem.type === "mathord" ||
|
|
baseElem.type === "textord" ||
|
|
baseElem.type === "bin" ||
|
|
baseElem.type === "rel" ||
|
|
baseElem.type === "open" ||
|
|
baseElem.type === "close" ||
|
|
baseElem.type === "punct";
|
|
};
|
|
|
|
var groupTypes = {
|
|
mathord: function(group, options, prev) {
|
|
return buildCommon.mathit(
|
|
group.value, group.mode, options.getColor(), ["mord"]);
|
|
},
|
|
|
|
textord: function(group, options, prev) {
|
|
return buildCommon.mathrm(
|
|
group.value, group.mode, options.getColor(), ["mord"]);
|
|
},
|
|
|
|
bin: function(group, options, prev) {
|
|
var className = "mbin";
|
|
var prevAtom = prev;
|
|
while (prevAtom && prevAtom.type == "color") {
|
|
var atoms = prevAtom.value.value;
|
|
prevAtom = atoms[atoms.length - 1];
|
|
}
|
|
if (!prev || utils.contains(["mbin", "mopen", "mrel", "mop", "mpunct"],
|
|
getTypeOfGroup(prevAtom))) {
|
|
group.type = "textord";
|
|
className = "mord";
|
|
}
|
|
|
|
return buildCommon.mathrm(
|
|
group.value, group.mode, options.getColor(), [className]);
|
|
},
|
|
|
|
rel: function(group, options, prev) {
|
|
return buildCommon.mathrm(
|
|
group.value, group.mode, options.getColor(), ["mrel"]);
|
|
},
|
|
|
|
text: function(group, options, prev) {
|
|
return makeSpan(["text", "mord", options.style.cls()],
|
|
buildExpression(group.value.body, options.reset()));
|
|
},
|
|
|
|
supsub: function(group, options, prev) {
|
|
var baseGroup = group.value.base;
|
|
|
|
if (shouldHandleSupSub(group.value.base, options)) {
|
|
return groupTypes[group.value.base.type](group, options, prev);
|
|
}
|
|
|
|
var base = buildGroup(group.value.base, options.reset());
|
|
|
|
if (group.value.sup) {
|
|
var sup = buildGroup(group.value.sup,
|
|
options.withStyle(options.style.sup()));
|
|
var supmid = makeSpan(
|
|
[options.style.reset(), options.style.sup().cls()], [sup]);
|
|
}
|
|
|
|
if (group.value.sub) {
|
|
var sub = buildGroup(group.value.sub,
|
|
options.withStyle(options.style.sub()));
|
|
var submid = makeSpan(
|
|
[options.style.reset(), options.style.sub().cls()], [sub]);
|
|
}
|
|
|
|
var u, v;
|
|
if (isCharacterBox(group.value.base)) {
|
|
u = 0;
|
|
v = 0;
|
|
} else {
|
|
u = base.height - fontMetrics.metrics.supDrop;
|
|
v = base.depth + fontMetrics.metrics.subDrop;
|
|
}
|
|
|
|
var p;
|
|
if (options.style === Style.DISPLAY) {
|
|
p = fontMetrics.metrics.sup1;
|
|
} else if (options.style.cramped) {
|
|
p = fontMetrics.metrics.sup3;
|
|
} else {
|
|
p = fontMetrics.metrics.sup2;
|
|
}
|
|
|
|
var multiplier = Style.TEXT.sizeMultiplier *
|
|
options.style.sizeMultiplier;
|
|
var scriptspace =
|
|
(0.5 / fontMetrics.metrics.ptPerEm) / multiplier + "em";
|
|
|
|
var supsub;
|
|
|
|
if (!group.value.sup) {
|
|
v = Math.max(v, fontMetrics.metrics.sub1,
|
|
sub.height - 0.8 * fontMetrics.metrics.xHeight);
|
|
|
|
supsub = buildCommon.makeVList([
|
|
{type: "elem", elem: submid}
|
|
], "shift", v, options);
|
|
|
|
supsub.children[0].style.marginRight = scriptspace;
|
|
|
|
if (base instanceof domTree.symbolNode) {
|
|
supsub.children[0].style.marginLeft = -base.italic + "em";
|
|
}
|
|
} else if (!group.value.sub) {
|
|
u = Math.max(u, p,
|
|
sup.depth + 0.25 * fontMetrics.metrics.xHeight);
|
|
|
|
supsub = buildCommon.makeVList([
|
|
{type: "elem", elem: supmid}
|
|
], "shift", -u, options);
|
|
|
|
supsub.children[0].style.marginRight = scriptspace;
|
|
} else {
|
|
u = Math.max(u, p,
|
|
sup.depth + 0.25 * fontMetrics.metrics.xHeight);
|
|
v = Math.max(v, fontMetrics.metrics.sub2);
|
|
|
|
var theta = fontMetrics.metrics.defaultRuleThickness;
|
|
|
|
if ((u - sup.depth) - (sub.height - v) < 4 * theta) {
|
|
v = 4 * theta - (u - sup.depth) + sub.height;
|
|
var psi = 0.8 * fontMetrics.metrics.xHeight - (u - sup.depth);
|
|
if (psi > 0) {
|
|
u += psi;
|
|
v -= psi;
|
|
}
|
|
}
|
|
|
|
supsub = buildCommon.makeVList([
|
|
{type: "elem", elem: submid, shift: v},
|
|
{type: "elem", elem: supmid, shift: -u}
|
|
], "individualShift", null, options);
|
|
|
|
if (base instanceof domTree.symbolNode) {
|
|
supsub.children[1].style.marginLeft = base.italic + "em";
|
|
base.italic = 0;
|
|
}
|
|
|
|
supsub.children[0].style.marginRight = scriptspace;
|
|
supsub.children[1].style.marginRight = scriptspace;
|
|
}
|
|
|
|
return makeSpan([getTypeOfGroup(group.value.base)],
|
|
[base, supsub]);
|
|
},
|
|
|
|
open: function(group, options, prev) {
|
|
return buildCommon.mathrm(
|
|
group.value, group.mode, options.getColor(), ["mopen"]);
|
|
},
|
|
|
|
close: function(group, options, prev) {
|
|
return buildCommon.mathrm(
|
|
group.value, group.mode, options.getColor(), ["mclose"]);
|
|
},
|
|
|
|
inner: function(group, options, prev) {
|
|
return buildCommon.mathrm(
|
|
group.value, group.mode, options.getColor(), ["minner"]);
|
|
},
|
|
|
|
frac: function(group, options, prev) {
|
|
var fstyle = options.style;
|
|
if (group.value.size === "dfrac") {
|
|
fstyle = Style.DISPLAY;
|
|
} else if (group.value.size === "tfrac") {
|
|
fstyle = Style.TEXT;
|
|
}
|
|
|
|
var nstyle = fstyle.fracNum();
|
|
var dstyle = fstyle.fracDen();
|
|
|
|
var numer = buildGroup(group.value.numer, options.withStyle(nstyle));
|
|
var numerreset = makeSpan([fstyle.reset(), nstyle.cls()], [numer]);
|
|
|
|
var denom = buildGroup(group.value.denom, options.withStyle(dstyle));
|
|
var denomreset = makeSpan([fstyle.reset(), dstyle.cls()], [denom])
|
|
|
|
var theta = fontMetrics.metrics.defaultRuleThickness / options.style.sizeMultiplier;
|
|
|
|
var mid = makeSpan([options.style.reset(), Style.TEXT.cls(), "frac-line"]);
|
|
mid.height = theta;
|
|
|
|
var u, v, phi;
|
|
if (fstyle.size === Style.DISPLAY.size) {
|
|
u = fontMetrics.metrics.num1;
|
|
v = fontMetrics.metrics.denom1;
|
|
phi = 3 * theta;
|
|
} else {
|
|
u = fontMetrics.metrics.num2;
|
|
v = fontMetrics.metrics.denom2;
|
|
phi = theta;
|
|
}
|
|
|
|
var a = fontMetrics.metrics.axisHeight;
|
|
|
|
if ((u - numer.depth) - (a + 0.5 * theta) < phi) {
|
|
u += phi - ((u - numer.depth) - (a + 0.5 * theta));
|
|
}
|
|
|
|
if ((a - 0.5 * theta) - (denom.height - v) < phi) {
|
|
v += phi - ((a - 0.5 * theta) - (denom.height - v));
|
|
}
|
|
|
|
var midShift = -(a - 0.5 * theta);
|
|
|
|
var frac = buildCommon.makeVList([
|
|
{type: "elem", elem: denomreset, shift: v},
|
|
{type: "elem", elem: mid, shift: midShift},
|
|
{type: "elem", elem: numerreset, shift: -u}
|
|
], "individualShift", null, options);
|
|
|
|
frac.height *= fstyle.sizeMultiplier / options.style.sizeMultiplier;
|
|
frac.depth *= fstyle.sizeMultiplier / options.style.sizeMultiplier;
|
|
|
|
return makeSpan(
|
|
["minner", "mfrac", options.style.reset(), fstyle.cls()],
|
|
[frac], options.getColor());
|
|
},
|
|
|
|
color: function(group, options, prev) {
|
|
var elements = buildExpression(
|
|
group.value.value,
|
|
options.withColor(group.value.color),
|
|
prev
|
|
);
|
|
|
|
return new buildCommon.makeFragment(elements);
|
|
},
|
|
|
|
spacing: function(group, options, prev) {
|
|
if (group.value === "\\ " || group.value === "\\space" ||
|
|
group.value === " " || group.value === "~") {
|
|
return makeSpan(
|
|
["mord", "mspace"],
|
|
[buildCommon.mathrm(group.value, group.mode)]
|
|
);
|
|
} else {
|
|
var spacingClassMap = {
|
|
"\\qquad": "qquad",
|
|
"\\quad": "quad",
|
|
"\\enspace": "enspace",
|
|
"\\;": "thickspace",
|
|
"\\:": "mediumspace",
|
|
"\\,": "thinspace",
|
|
"\\!": "negativethinspace"
|
|
};
|
|
|
|
return makeSpan(
|
|
["mord", "mspace", spacingClassMap[group.value]]);
|
|
}
|
|
},
|
|
|
|
llap: function(group, options, prev) {
|
|
var inner = makeSpan(
|
|
["inner"], [buildGroup(group.value.body, options.reset())]);
|
|
var fix = makeSpan(["fix"], []);
|
|
return makeSpan(
|
|
["llap", options.style.cls()], [inner, fix]);
|
|
},
|
|
|
|
rlap: function(group, options, prev) {
|
|
var inner = makeSpan(
|
|
["inner"], [buildGroup(group.value.body, options.reset())]);
|
|
var fix = makeSpan(["fix"], []);
|
|
return makeSpan(
|
|
["rlap", options.style.cls()], [inner, fix]);
|
|
},
|
|
|
|
punct: function(group, options, prev) {
|
|
return buildCommon.mathrm(
|
|
group.value, group.mode, options.getColor(), ["mpunct"]);
|
|
},
|
|
|
|
ordgroup: function(group, options, prev) {
|
|
return makeSpan(
|
|
["mord", options.style.cls()],
|
|
buildExpression(group.value, options.reset())
|
|
);
|
|
},
|
|
|
|
op: function(group, options, prev) {
|
|
var supGroup;
|
|
var subGroup;
|
|
var hasLimits = false;
|
|
if (group.type === "supsub" ) {
|
|
supGroup = group.value.sup;
|
|
subGroup = group.value.sub;
|
|
group = group.value.base;
|
|
hasLimits = true;
|
|
}
|
|
|
|
// Most operators have a large successor symbol, but these don't.
|
|
var noSuccessor = [
|
|
"\\smallint"
|
|
];
|
|
|
|
var large = false;
|
|
|
|
if (options.style.id === Style.DISPLAY.id &&
|
|
group.value.symbol &&
|
|
!utils.contains(noSuccessor, group.value.body)) {
|
|
|
|
// Make symbols larger in displaystyle, except for smallint
|
|
large = true;
|
|
}
|
|
|
|
var base;
|
|
var baseShift = 0;
|
|
var delta = 0;
|
|
if (group.value.symbol) {
|
|
var style = large ? "Size2-Regular" : "Size1-Regular";
|
|
base = buildCommon.makeSymbol(
|
|
group.value.body, style, "math", options.getColor(),
|
|
["op-symbol", large ? "large-op" : "small-op", "mop"]);
|
|
|
|
baseShift = (base.height - base.depth) / 2 -
|
|
fontMetrics.metrics.axisHeight *
|
|
options.style.sizeMultiplier;
|
|
delta = base.italic;
|
|
} else {
|
|
var output = [];
|
|
for (var i = 1; i < group.value.body.length; i++) {
|
|
output.push(buildCommon.mathrm(group.value.body[i], group.mode));
|
|
}
|
|
base = makeSpan(["mop"], output, options.getColor());
|
|
}
|
|
|
|
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.
|
|
var base = makeSpan([], [base]);
|
|
|
|
if (supGroup) {
|
|
var sup = buildGroup(supGroup,
|
|
options.withStyle(options.style.sup()));
|
|
var supmid = makeSpan(
|
|
[options.style.reset(), options.style.sup().cls()], [sup]);
|
|
|
|
var supKern = Math.max(
|
|
fontMetrics.metrics.bigOpSpacing1,
|
|
fontMetrics.metrics.bigOpSpacing3 - sup.depth);
|
|
}
|
|
|
|
if (subGroup) {
|
|
var sub = buildGroup(subGroup,
|
|
options.withStyle(options.style.sub()));
|
|
var submid = makeSpan(
|
|
[options.style.reset(), options.style.sub().cls()], [sub]);
|
|
|
|
var subKern = Math.max(
|
|
fontMetrics.metrics.bigOpSpacing2,
|
|
fontMetrics.metrics.bigOpSpacing4 - sub.height);
|
|
}
|
|
|
|
var finalGroup;
|
|
if (!supGroup) {
|
|
var top = base.height - baseShift;
|
|
|
|
finalGroup = buildCommon.makeVList([
|
|
{type: "kern", size: fontMetrics.metrics.bigOpSpacing5},
|
|
{type: "elem", elem: submid},
|
|
{type: "kern", size: subKern},
|
|
{type: "elem", elem: base}
|
|
], "top", top, options);
|
|
|
|
finalGroup.children[0].style.marginLeft = -delta + "em";
|
|
} else if (!subGroup) {
|
|
var bottom = base.depth + baseShift;
|
|
|
|
finalGroup = buildCommon.makeVList([
|
|
{type: "elem", elem: base},
|
|
{type: "kern", size: supKern},
|
|
{type: "elem", elem: supmid},
|
|
{type: "kern", size: fontMetrics.metrics.bigOpSpacing5}
|
|
], "bottom", bottom, options);
|
|
|
|
finalGroup.children[1].style.marginLeft = delta + "em";
|
|
} else if (!supGroup && !subGroup) {
|
|
return base;
|
|
} else {
|
|
var bottom = fontMetrics.metrics.bigOpSpacing5 +
|
|
submid.height + submid.depth +
|
|
subKern +
|
|
base.depth + baseShift;
|
|
|
|
finalGroup = buildCommon.makeVList([
|
|
{type: "kern", size: fontMetrics.metrics.bigOpSpacing5},
|
|
{type: "elem", elem: submid},
|
|
{type: "kern", size: subKern},
|
|
{type: "elem", elem: base},
|
|
{type: "kern", size: supKern},
|
|
{type: "elem", elem: supmid},
|
|
{type: "kern", size: fontMetrics.metrics.bigOpSpacing5}
|
|
], "bottom", bottom, options);
|
|
|
|
finalGroup.children[0].style.marginLeft = -delta + "em";
|
|
finalGroup.children[2].style.marginLeft = delta + "em";
|
|
}
|
|
|
|
return makeSpan(["mop", "op-limits"], [finalGroup]);
|
|
} else {
|
|
if (group.value.symbol) {
|
|
base.style.top = baseShift + "em";
|
|
}
|
|
|
|
return base;
|
|
}
|
|
},
|
|
|
|
katex: function(group, options, prev) {
|
|
var k = makeSpan(
|
|
["k"], [buildCommon.mathrm("K", group.mode)]);
|
|
var a = makeSpan(
|
|
["a"], [buildCommon.mathrm("A", group.mode)]);
|
|
|
|
a.height = (a.height + 0.2) * 0.75;
|
|
a.depth = (a.height - 0.2) * 0.75;
|
|
|
|
var t = makeSpan(
|
|
["t"], [buildCommon.mathrm("T", group.mode)]);
|
|
var e = makeSpan(
|
|
["e"], [buildCommon.mathrm("E", group.mode)]);
|
|
|
|
e.height = (e.height - 0.2155);
|
|
e.depth = (e.depth + 0.2155);
|
|
|
|
var x = makeSpan(
|
|
["x"], [buildCommon.mathrm("X", group.mode)]);
|
|
|
|
return makeSpan(
|
|
["katex-logo"], [k, a, t, e, x], options.getColor());
|
|
},
|
|
|
|
sqrt: function(group, options, prev) {
|
|
var inner = buildGroup(group.value.body,
|
|
options.withStyle(options.style.cramp()));
|
|
|
|
var theta = fontMetrics.metrics.defaultRuleThickness /
|
|
options.style.sizeMultiplier;
|
|
|
|
var line = makeSpan(
|
|
[options.style.reset(), Style.TEXT.cls(), "sqrt-line"], [],
|
|
options.getColor());
|
|
line.height = theta;
|
|
line.maxFontSize = 1.0;
|
|
|
|
var phi = theta;
|
|
if (options.style.id < Style.TEXT.id) {
|
|
phi = fontMetrics.metrics.xHeight;
|
|
}
|
|
|
|
var psi = theta + phi / 4;
|
|
|
|
var innerHeight =
|
|
(inner.height + inner.depth) * options.style.sizeMultiplier;
|
|
var minDelimiterHeight = innerHeight + psi + theta;
|
|
|
|
var delim = makeSpan(["sqrt-sign"], [
|
|
delimiter.customSizedDelim("\\surd", minDelimiterHeight,
|
|
false, options, group.mode)],
|
|
options.getColor());
|
|
|
|
var delimDepth = (delim.height + delim.depth) - theta;
|
|
|
|
if (delimDepth > inner.height + inner.depth + psi) {
|
|
psi = (psi + delimDepth - inner.height - inner.depth) / 2;
|
|
}
|
|
|
|
delimShift = -(inner.height + psi + theta) + delim.height;
|
|
delim.style.top = delimShift + "em";
|
|
delim.height -= delimShift;
|
|
delim.depth += delimShift;
|
|
|
|
// We add a special case here, because even when `inner` is empty, we
|
|
// still get a line. So, we use a simple heuristic to decide if we
|
|
// should omit the body entirely. (note this doesn't work for something
|
|
// like `\sqrt{\rlap{x}}`, but if someone is doing that they deserve for
|
|
// it not to work.
|
|
var body;
|
|
if (inner.height === 0 && inner.depth === 0) {
|
|
body = makeSpan();
|
|
} else {
|
|
body = buildCommon.makeVList([
|
|
{type: "elem", elem: inner},
|
|
{type: "kern", size: psi},
|
|
{type: "elem", elem: line},
|
|
{type: "kern", size: theta}
|
|
], "firstBaseline", null, options);
|
|
}
|
|
|
|
return makeSpan(["sqrt", "mord"], [delim, body]);
|
|
},
|
|
|
|
overline: function(group, options, prev) {
|
|
var innerGroup = buildGroup(group.value.body,
|
|
options.withStyle(options.style.cramp()));
|
|
|
|
var theta = fontMetrics.metrics.defaultRuleThickness /
|
|
options.style.sizeMultiplier;
|
|
|
|
var line = makeSpan(
|
|
[options.style.reset(), Style.TEXT.cls(), "overline-line"]);
|
|
line.height = theta;
|
|
line.maxFontSize = 1.0;
|
|
|
|
var vlist = buildCommon.makeVList([
|
|
{type: "elem", elem: innerGroup},
|
|
{type: "kern", size: 3 * theta},
|
|
{type: "elem", elem: line},
|
|
{type: "kern", size: theta}
|
|
], "firstBaseline", null, options);
|
|
|
|
return makeSpan(["overline", "mord"], [vlist], options.getColor());
|
|
},
|
|
|
|
sizing: function(group, options, prev) {
|
|
var inner = buildExpression(group.value.value,
|
|
options.withSize(group.value.size), prev);
|
|
|
|
var span = makeSpan(["mord"],
|
|
[makeSpan(["sizing", "reset-" + options.size, group.value.size,
|
|
options.style.cls()],
|
|
inner)]);
|
|
|
|
var sizeToFontSize = {
|
|
"size1": 0.5,
|
|
"size2": 0.7,
|
|
"size3": 0.8,
|
|
"size4": 0.9,
|
|
"size5": 1.0,
|
|
"size6": 1.2,
|
|
"size7": 1.44,
|
|
"size8": 1.73,
|
|
"size9": 2.07,
|
|
"size10": 2.49
|
|
};
|
|
|
|
var fontSize = sizeToFontSize[group.value.size];
|
|
span.maxFontSize = fontSize * options.style.sizeMultiplier;
|
|
|
|
return span;
|
|
},
|
|
|
|
styling: function(group, options, prev) {
|
|
var style = {
|
|
"display": Style.DISPLAY,
|
|
"text": Style.TEXT,
|
|
"script": Style.SCRIPT,
|
|
"scriptscript": Style.SCRIPTSCRIPT
|
|
};
|
|
|
|
var newStyle = style[group.value.style];
|
|
|
|
var inner = buildExpression(
|
|
group.value.value, options.withStyle(newStyle), prev);
|
|
|
|
return makeSpan([options.style.reset(), newStyle.cls()], inner);
|
|
},
|
|
|
|
delimsizing: function(group, options, prev) {
|
|
var delim = group.value.value;
|
|
|
|
if (delim === ".") {
|
|
return makeSpan([groupToType[group.value.delimType]]);
|
|
}
|
|
|
|
return makeSpan(
|
|
[groupToType[group.value.delimType]],
|
|
[delimiter.sizedDelim(
|
|
delim, group.value.size, options, group.mode)]);
|
|
},
|
|
|
|
leftright: function(group, options, prev) {
|
|
var inner = buildExpression(group.value.body, options.reset());
|
|
|
|
var innerHeight = 0;
|
|
var innerDepth = 0;
|
|
|
|
for (var i = 0; i < inner.length; i++) {
|
|
innerHeight = Math.max(inner[i].height, innerHeight);
|
|
innerDepth = Math.max(inner[i].depth, innerDepth);
|
|
}
|
|
|
|
innerHeight *= options.style.sizeMultiplier;
|
|
innerDepth *= options.style.sizeMultiplier;
|
|
|
|
var leftDelim;
|
|
if (group.value.left === ".") {
|
|
leftDelim = makeSpan(["nulldelimiter"]);
|
|
} else {
|
|
leftDelim = delimiter.leftRightDelim(
|
|
group.value.left, innerHeight, innerDepth, options,
|
|
group.mode);
|
|
}
|
|
inner.unshift(leftDelim);
|
|
|
|
var rightDelim;
|
|
if (group.value.right === ".") {
|
|
rightDelim = makeSpan(["nulldelimiter"]);
|
|
} else {
|
|
rightDelim = delimiter.leftRightDelim(
|
|
group.value.right, innerHeight, innerDepth, options,
|
|
group.mode);
|
|
}
|
|
inner.push(rightDelim);
|
|
|
|
return makeSpan(
|
|
["minner", options.style.cls()], inner, options.getColor());
|
|
},
|
|
|
|
rule: function(group, options, prev) {
|
|
// Make an empty span for the rule
|
|
var rule = makeSpan(["mord", "rule"], [], options.getColor());
|
|
|
|
var width = group.value.width.number;
|
|
if (group.value.width.unit === "ex") {
|
|
width *= fontMetrics.metrics.xHeight;
|
|
}
|
|
|
|
var height = group.value.height.number;
|
|
if (group.value.height.unit === "ex") {
|
|
height *= fontMetrics.metrics.xHeight;
|
|
}
|
|
|
|
width /= options.style.sizeMultiplier;
|
|
height /= options.style.sizeMultiplier;
|
|
|
|
// Style the rule to the right size
|
|
rule.style.borderRightWidth = width + "em";
|
|
rule.style.borderTopWidth = height + "em";
|
|
|
|
// Record the height and width
|
|
rule.width = width;
|
|
rule.height = height;
|
|
|
|
return rule;
|
|
},
|
|
|
|
accent: function(group, options, prev) {
|
|
var base = group.value.base;
|
|
|
|
var supsubGroup;
|
|
if (group.type === "supsub") {
|
|
var supsub = group;
|
|
group = group.value.base;
|
|
base = group.value.base;
|
|
supsub.value.base = base;
|
|
|
|
supsubGroup = buildGroup(
|
|
supsub, options.reset());
|
|
}
|
|
|
|
var body = buildGroup(
|
|
base, options.withStyle(options.style.cramp()));
|
|
|
|
var s;
|
|
if (isCharacterBox(group.value.base)) {
|
|
var baseChar = getBaseElem(group.value.base);
|
|
var baseGroup = buildGroup(
|
|
baseChar, options.withStyle(options.style.cramp()));
|
|
s = baseGroup.skew;
|
|
} else {
|
|
s = 0;
|
|
}
|
|
|
|
var delta = Math.min(body.height, fontMetrics.metrics.xHeight);
|
|
|
|
var accent = buildCommon.makeSymbol(
|
|
group.value.accent, "Main-Regular", "math", options.getColor());
|
|
accent.italic = 0;
|
|
|
|
// The \vec character that the fonts use is a combining character, and
|
|
// thus shows up much too far to the left. To account for this, we add a
|
|
// specific class which shifts the accent over to where we want it.
|
|
// TODO(emily): Fix this in a better way, like by changing the font
|
|
var vecClass = group.value.accent === "\\vec" ? "accent-vec" : null;
|
|
|
|
var accentBody = makeSpan(["accent-body", vecClass], [
|
|
makeSpan([], [accent])]);
|
|
|
|
var accentBody = buildCommon.makeVList([
|
|
{type: "elem", elem: body},
|
|
{type: "kern", size: -delta},
|
|
{type: "elem", elem: accentBody}
|
|
], "firstBaseline", null, options);
|
|
|
|
accentBody.children[1].style.marginLeft = 2 * s + "em";
|
|
|
|
var accentWrap = makeSpan(["mord", "accent"], [accentBody]);
|
|
|
|
if (supsubGroup) {
|
|
// Here, we replace the "base" child of the supsub with our newly
|
|
// generated accent.
|
|
supsubGroup.children[0] = accentWrap;
|
|
|
|
// Since we don't rerun the height calculation after replacing the
|
|
// accent, we manually recalculate height.
|
|
supsubGroup.height = Math.max(accentWrap.height, supsubGroup.height);
|
|
|
|
// Accents should always be ords, even when their innards are not.
|
|
supsubGroup.classes[0] = "mord";
|
|
|
|
return supsubGroup;
|
|
} else {
|
|
return accentWrap;
|
|
}
|
|
}
|
|
};
|
|
|
|
var sizingMultiplier = {
|
|
size1: 0.5,
|
|
size2: 0.7,
|
|
size3: 0.8,
|
|
size4: 0.9,
|
|
size5: 1.0,
|
|
size6: 1.2,
|
|
size7: 1.44,
|
|
size8: 1.73,
|
|
size9: 2.07,
|
|
size10: 2.49
|
|
};
|
|
|
|
var buildGroup = function(group, options, prev) {
|
|
if (!group) {
|
|
return makeSpan();
|
|
}
|
|
|
|
if (groupTypes[group.type]) {
|
|
var groupNode = groupTypes[group.type](group, options, prev);
|
|
|
|
if (options.style !== options.parentStyle) {
|
|
var multiplier = options.style.sizeMultiplier /
|
|
options.parentStyle.sizeMultiplier;
|
|
|
|
groupNode.height *= multiplier;
|
|
groupNode.depth *= multiplier;
|
|
}
|
|
|
|
if (options.size !== options.parentSize) {
|
|
var multiplier = sizingMultiplier[options.size] /
|
|
sizingMultiplier[options.parentSize];
|
|
|
|
groupNode.height *= multiplier;
|
|
groupNode.depth *= multiplier;
|
|
}
|
|
|
|
return groupNode;
|
|
} else {
|
|
throw new ParseError(
|
|
"Got group of unknown type: '" + group.type + "'");
|
|
}
|
|
};
|
|
|
|
var buildTree = function(tree) {
|
|
// Setup the default options
|
|
var options = new Options(Style.TEXT, "size5", "");
|
|
|
|
var expression = buildExpression(tree, options);
|
|
var span = makeSpan(["base", options.style.cls()], expression);
|
|
var topStrut = makeSpan(["strut"]);
|
|
var bottomStrut = makeSpan(["strut", "bottom"]);
|
|
|
|
topStrut.style.height = span.height + "em";
|
|
bottomStrut.style.height = (span.height + span.depth) + "em";
|
|
// We'd like to use `vertical-align: top` but in IE 9 this lowers the
|
|
// baseline of the box to the bottom of this strut (instead staying in the
|
|
// normal place) so we use an absolute value for vertical-align instead
|
|
bottomStrut.style.verticalAlign = -span.depth + "em";
|
|
|
|
var katexNode = makeSpan(["katex"], [
|
|
makeSpan(["katex-inner"], [topStrut, bottomStrut, span])
|
|
]);
|
|
|
|
return katexNode;
|
|
};
|
|
|
|
module.exports = buildTree;
|