scribble-math/buildTree.js
Emily Eisenberg 29b00ee6b7 Add vlist builder for more consistent stacking
Summary:
Add a way to automatically build vlists correctly. Previously, we built
vlists manually in ~4 different places, which made it difficult to manage
changes, and led to a large amount of duplication in the less. This also fixes
the vlist construction in safari, where the `display: inline-table` wasn't being
applied because of CSS specificity. This leads to the only significant change in
the huxley tests, with the vertical spacing.

Test Plan:
 - Make sure the tests still work
 - Make sure most of the huxley screenshots didn't change, and that the new
   changes are insignificant.
 - Make sure vlists now work in Safari
 - Make sure the change to the VerticalSpacing screenshot is caused by the
   fix-baseline span now correctly applying `display: inline-table` by creating
   the construct in master and adding `display: inline-table !important` to the
   `.fix-ie` css rule

Reviewers: alpert

Reviewed By: alpert

Differential Revision: http://phabricator.khanacademy.org/D13082
2014-09-12 14:41:31 -07:00

667 lines
20 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",
frac: "minner",
spacing: "mord",
punct: "mpunct",
ordgroup: "mord",
namedfn: "mop",
katex: "mord",
overline: "mord",
rule: "mord",
leftright: "minner",
sqrt: "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 isCharacterBox = function(group) {
if (group == null) {
return false;
} else if (group.type === "mathord" ||
group.type === "textord" ||
group.type === "bin" ||
group.type === "rel" ||
group.type === "open" ||
group.type === "close" ||
group.type === "punct") {
return true;
} else if (group.type === "ordgroup") {
return group.value.length === 1 && isCharacterBox(group.value[0]);
} else {
return false;
}
};
var groupTypes = {
mathord: function(group, options, prev) {
return makeSpan(
["mord"],
[buildCommon.mathit(group.value, group.mode)],
options.getColor()
);
},
textord: function(group, options, prev) {
return makeSpan(
["mord"],
[buildCommon.mathrm(group.value, group.mode)],
options.getColor()
);
},
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 = "ord";
className = "mord";
}
return makeSpan(
[className],
[buildCommon.mathrm(group.value, group.mode)],
options.getColor()
);
},
rel: function(group, options, prev) {
return makeSpan(
["mrel"],
[buildCommon.mathrm(group.value, group.mode)],
options.getColor()
);
},
text: function(group, options, prev) {
return makeSpan(["text mord", options.style.cls()],
buildExpression(group.value.body, options.reset()));
},
supsub: function(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;
} 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);
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 makeSpan(
["mopen"],
[buildCommon.mathrm(group.value, group.mode)],
options.getColor()
);
},
close: function(group, options, prev) {
return makeSpan(
["mclose"],
[buildCommon.mathrm(group.value, group.mode)],
options.getColor()
);
},
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 makeSpan(
["mpunct"],
[buildCommon.mathrm(group.value, group.mode)],
options.getColor()
);
},
ordgroup: function(group, options, prev) {
return makeSpan(
["mord", options.style.cls()],
buildExpression(group.value, options.reset())
);
},
namedfn: function(group, options, prev) {
var chars = [];
for (var i = 1; i < group.value.body.length; i++) {
chars.push(buildCommon.mathrm(group.value.body[i], group.mode));
}
return makeSpan(["mop"], chars, options.getColor());
},
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"], 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;
}
};
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;