scribble-math/buildTree.js
Emily Eisenberg fe6b67817c Add support for \overline
Summary:
Follow the instructions in the TeX book for drawing \overlines. This uses the
same code as fractions to produce the bars. Also added the ability to cramp
styles (i.e. T -> T' and T' -> T').

Test Plan:
 - Look at `\overline{x}`, `\overline{\dfrac{x}{y}+z}`, and
   `\blue{\overline{x}}` to make sure they look good.
 - Make sure the tests work
 - Make sure the huxley tests look good (Not here yet :( )

Reviewers: alpert

Reviewed By: alpert

Differential Revision: http://phabricator.khanacademy.org/D11604
2014-08-05 17:48:10 -07:00

728 lines
23 KiB
JavaScript

var Options = require("./Options");
var ParseError = require("./ParseError");
var Style = require("./Style");
var domTree = require("./domTree");
var fontMetrics = require("./fontMetrics");
var parseTree = require("./parseTree");
var utils = require("./utils");
var symbols = require("./symbols");
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 makeSpan = function(classes, children, color) {
var height = 0;
var depth = 0;
if (children) {
for (var i = 0; i < children.length; i++) {
if (children[i].height > height) {
height = children[i].height;
}
if (children[i].depth > depth) {
depth = children[i].depth;
}
}
}
var span = new domTree.span(classes, children, height, depth);
if (color) {
span.style.color = color;
}
return span;
};
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"
};
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 === "delimsizing") {
return group.value.type;
} 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"],
[mathit(group.value, group.mode)],
options.getColor()
);
},
textord: function(group, options, prev) {
return makeSpan(
["mord"],
[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(["bin", "open", "rel", "op", "punct"],
prevAtom.type)) {
group.type = "ord";
className = "mord";
}
return makeSpan(
[className],
[mathrm(group.value, group.mode)],
options.getColor()
);
},
rel: function(group, options, prev) {
return makeSpan(
["mrel"],
[mathrm(group.value, group.mode)],
options.getColor()
);
},
text: function(group, options, prev) {
return makeSpan(["text mord", options.style.cls()],
[buildGroup(group.value, options.deepen())]
);
},
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()).deepen());
var supmid = makeSpan(
[options.style.reset(), options.style.sup().cls()], [sup]);
var supwrap = makeSpan(["msup", options.style.reset()], [supmid]);
}
if (group.value.sub) {
var sub = buildGroup(group.value.sub,
options.withStyle(options.style.sub()).deepen());
var submid = makeSpan(
[options.style.reset(), options.style.sub().cls()], [sub]);
var subwrap = makeSpan(["msub"], [submid]);
}
if (isCharacterBox(group.value.base)) {
var u = 0;
var v = 0;
} else {
var u = base.height - fontMetrics.metrics.supDrop;
var 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 supsub;
var fixIE = makeSpan(["fix-ie"], [new domTree.textNode("\u00a0")]);
if (!group.value.sup) {
v = Math.max(v, fontMetrics.metrics.sub1,
sub.height - 0.8 * fontMetrics.metrics.xHeight);
subwrap.style.top = v + "em";
subwrap.depth = subwrap.depth + v;
subwrap.height = 0;
supsub = makeSpan(["msupsub"], [subwrap, fixIE]);
} else if (!group.value.sub) {
u = Math.max(u, p,
sup.depth + 0.25 * fontMetrics.metrics.xHeight);
supwrap.style.top = -u + "em";
supwrap.height = supwrap.height + u;
supwrap.depth = 0;
supsub = makeSpan(["msupsub"], [supwrap, fixIE]);
} 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;
}
}
supwrap.style.top = -u + "em";
subwrap.style.top = v + "em";
supwrap.height = supwrap.height + u;
supwrap.depth = 0;
subwrap.height = 0;
subwrap.depth = subwrap.depth + v;
supsub = makeSpan(["msupsub"], [supwrap, subwrap, fixIE]);
}
return makeSpan([getTypeOfGroup(group.value.base)], [base, supsub]);
},
open: function(group, options, prev) {
return makeSpan(
["mopen"],
[mathrm(group.value, group.mode)],
options.getColor()
);
},
close: function(group, options, prev) {
return makeSpan(
["mclose"],
[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).deepen());
var numernumer = makeSpan([fstyle.reset(), nstyle.cls()], [numer]);
var numerrow = makeSpan(["mfracnum"], [numernumer]);
var mid = makeSpan(["mfracmid"], [makeSpan()]);
var denom = buildGroup(group.value.denom, options.withStyle(dstyle).deepen());
var denomdenom = makeSpan([fstyle.reset(), dstyle.cls()], [denom])
var denomrow = makeSpan(["mfracden"], [denomdenom]);
var theta = fontMetrics.metrics.defaultRuleThickness;
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));
}
numerrow.style.top = -u + "em";
mid.style.top = -(a - 0.5 * theta) + "em";
denomrow.style.top = v + "em";
numerrow.height = numerrow.height + u;
numerrow.depth = 0;
denomrow.height = 0;
denomrow.depth = denomrow.depth + v;
var fixIE = makeSpan(["fix-ie"], [new domTree.textNode("\u00a0")]);
var frac = makeSpan([], [numerrow, mid, denomrow, fixIE]);
frac.height *= fstyle.sizeMultiplier / options.style.sizeMultiplier;
frac.depth *= fstyle.sizeMultiplier / options.style.sizeMultiplier;
var wrap = makeSpan([options.style.reset(), fstyle.cls()], [frac]);
return makeSpan(["minner"], [
makeSpan(["mfrac"], [wrap])
], options.getColor());
},
color: function(group, options, prev) {
var els = buildExpression(
group.value.value,
options.withColor(group.value.color),
prev
);
var height = 0;
var depth = 0;
for (var i = 0; i < els.length; i++) {
if (els[i].height > height) {
var height = els[i].height;
}
if (els[i].depth > depth) {
var depth = els[i].depth;
}
}
return new domTree.documentFragment(els, height, depth);
},
spacing: function(group, options, prev) {
if (group.value === "\\ " || group.value === "\\space" ||
group.value === " " || group.value === "~") {
return makeSpan(
["mord", "mspace"],
[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, options.deepen())]);
var fix = makeSpan(["fix"], []);
return makeSpan(["llap", options.style.cls()], [inner, fix]);
},
rlap: function(group, options, prev) {
var inner = makeSpan(
["inner"], [buildGroup(group.value, options.deepen())]);
var fix = makeSpan(["fix"], []);
return makeSpan(["rlap", options.style.cls()], [inner, fix]);
},
punct: function(group, options, prev) {
return makeSpan(
["mpunct"],
[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.length; i++) {
chars.push(mathrm(group.value[i], group.mode));
}
return makeSpan(["mop"], chars, options.getColor());
},
katex: function(group, options, prev) {
var k = makeSpan(["k"], [mathrm("K", group.mode)]);
var a = makeSpan(["a"], [mathrm("A", group.mode)]);
a.height = (a.height + 0.2) * 0.75;
a.depth = (a.height - 0.2) * 0.75;
var t = makeSpan(["t"], [mathrm("T", group.mode)]);
var e = makeSpan(["e"], [mathrm("E", group.mode)]);
e.height = (e.height - 0.2155);
e.depth = (e.depth + 0.2155);
var x = makeSpan(["x"], [mathrm("X", group.mode)]);
return makeSpan(["katex-logo"], [k, a, t, e, x], options.getColor());
},
overline: function(group, options, prev) {
var innerGroup = buildGroup(group.value.result,
options.withStyle(options.style.cramp()).deepen());
// The theta variable in the TeXbook
var lineWidth = fontMetrics.metrics.defaultRuleThickness;
var line = makeSpan(["overline-line"], [makeSpan([])]);
var inner = makeSpan(["overline-inner"], [innerGroup]);
var fixIE = makeSpan(["fix-ie"], []);
line.style.top = (-inner.height - 3 * lineWidth) + "em";
// The line is supposed to have 1 extra line width above it in height
// (TeXbook pg. 443, nr. 9)
line.height = inner.height + 5 * lineWidth;
return makeSpan(["overline", "mord"], [
line, inner, fixIE
], options.getColor());
},
sizing: function(group, options, prev) {
var inner = buildGroup(group.value.value,
options.withSize(group.value.size), prev);
return makeSpan(
["sizing", "reset-" + options.size, group.value.size,
getTypeOfGroup(group.value.value)],
[inner]);
},
delimsizing: function(group, options, prev) {
var normalDelimiters = [
"(", ")", "[", "\\lbrack", "]", "\\rbrack",
"\\{", "\\lbrace", "\\}", "\\rbrace",
"\\lfloor", "\\rfloor", "\\lceil", "\\rceil",
"<", ">", "\\langle", "\\rangle", "/", "\\backslash"
];
var stackDelimiters = [
"\\uparrow", "\\downarrow", "\\updownarrow",
"\\Uparrow", "\\Downarrow", "\\Updownarrow",
"|", "\\|", "\\vert", "\\Vert"
];
// Metrics of the different sizes. Found by looking at TeX's output of
// $\bigl| \Bigl| \biggl| \Biggl| \showlists$
var sizeToMetrics = {
1: {height: .85, depth: .35},
2: {height: 1.15, depth: .65},
3: {height: 1.45, depth: .95},
4: {height: 1.75, depth: 1.25}
};
// Make an inner span with the given offset and in the given font
var makeInner = function(symbol, offset, font) {
var sizeClass;
if (font === "size1-regular") {
sizeClass = "size1";
}
var inner = makeSpan(
["delimsizinginner", sizeClass],
[makeSpan([], [makeText(symbol, font, group.mode)])]);
inner.style.top = offset + "em";
inner.height -= offset;
inner.depth += offset;
return inner;
};
// Get the metrics for a given symbol and font, after transformation
var getMetrics = function(symbol, font) {
if (symbols["math"][symbol] && symbols["math"][symbol].replace) {
return fontMetrics.getCharacterMetrics(
symbols["math"][symbol].replace, font);
} else {
return fontMetrics.getCharacterMetrics(
symbol, font);
}
};
var original = group.value.value;
if (utils.contains(normalDelimiters, original)) {
// These delimiters can be created by simply using the size1-size4
// fonts, so they don't require special treatment
if (original === "<") {
original = "\\langle";
} else if (original === ">") {
original = "\\rangle";
}
var size = "size" + group.value.size;
var inner = mathrmSize(
original, group.value.size, group.mode);
return makeSpan(
["delimsizing", size, groupToType[group.value.type]],
[inner], options.getColor());
} else if (utils.contains(stackDelimiters, original)) {
// These delimiters can be created by stacking other delimiters on
// top of each other to create the correct size
// There are three parts, the top, a repeated middle, and a bottom.
var top = middle = bottom = original;
var font = "size1-regular";
var overlap = false;
// We set the parts and font based on the symbol. Note that we use
// '\u23d0' instead of '|' and '\u2016' instead of '\\|' for the
// middles of the arrows
if (original === "\\uparrow") {
middle = bottom = "\u23d0";
} else if (original === "\\Uparrow") {
middle = bottom = "\u2016";
} else if (original === "\\downarrow") {
top = middle = "\u23d0";
} else if (original === "\\Downarrow") {
top = middle = "\u2016";
} else if (original === "\\updownarrow") {
top = "\\uparrow";
middle = "\u23d0";
bottom = "\\downarrow";
} else if (original === "\\Updownarrow") {
top = "\\Uparrow";
middle = "\u2016";
bottom = "\\Downarrow";
} else if (original === "|" || original === "\\vert") {
overlap = true;
} else if (original === "\\|" || original === "\\Vert") {
overlap = true;
}
// Get the metrics of the final symbol
var metrics = sizeToMetrics[group.value.size];
var heightTotal = metrics.height + metrics.depth;
// Get the metrics of the three sections
var topMetrics = getMetrics(top, font);
var topHeightTotal = topMetrics.height + topMetrics.depth;
var middleMetrics = getMetrics(middle, font);
var middleHeightTotal = middleMetrics.height + middleMetrics.depth;
var bottomMetrics = getMetrics(bottom, font);
var bottomHeightTotal = bottomMetrics.height + bottomMetrics.depth;
var middleHeight = heightTotal - topHeightTotal - bottomHeightTotal;
var symbolCount = Math.ceil(middleHeight / middleHeightTotal);
if (overlap) {
// 2 * overlapAmount + middleHeight =
// (symbolCount - 1) * (middleHeightTotal - overlapAmount) +
// middleHeightTotal
var overlapAmount = (symbolCount * middleHeightTotal -
middleHeight) / (symbolCount + 1);
} else {
var overlapAmount = 0;
}
// Keep a list of the inner spans
var inners = [];
// Add the top symbol
inners.push(
makeInner(top, topMetrics.height - metrics.height, font));
// Add middle symbols until there's only space for the bottom symbol
var curr_height = metrics.height - topHeightTotal + overlapAmount;
for (var i = 0; i < symbolCount; i++) {
inners.push(
makeInner(middle, middleMetrics.height - curr_height, font));
curr_height -= middleHeightTotal - overlapAmount;
}
// Add the bottom symbol
inners.push(
makeInner(bottom, metrics.depth - bottomMetrics.depth, font));
var fixIE = makeSpan(["fix-ie"], [new domTree.textNode("\u00a0")]);
inners.push(fixIE);
return makeSpan(
["delimsizing", "mult", groupToType[group.value.type]],
inners, options.getColor());
} else {
throw new ParseError("Illegal delimiter: '" + original + "'");
}
}
};
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];
if (options.deep) {
throw new ParseError(
"Can't use sizing outside of the root node");
}
groupNode.height *= multiplier;
groupNode.depth *= multiplier;
}
return groupNode;
} else {
throw new ParseError(
"Got group of unknown type: '" + group.type + "'");
}
};
var makeText = function(value, style, mode) {
if (symbols[mode][value] && symbols[mode][value].replace) {
value = symbols[mode][value].replace;
}
var metrics = fontMetrics.getCharacterMetrics(value, style);
if (metrics) {
var textNode = new domTree.textNode(value, metrics.height,
metrics.depth);
if (metrics.italic > 0) {
var span = makeSpan([], [textNode]);
span.style.marginRight = metrics.italic + "em";
return span;
} else {
return textNode;
}
} else {
console && console.warn("No character metrics for '" + value +
"' in style '" + style + "'");
return new domTree.textNode(value, 0, 0);
}
};
var mathit = function(value, mode) {
return makeSpan(["mathit"], [makeText(value, "math-italic", mode)]);
};
var mathrm = function(value, mode) {
if (symbols[mode][value].font === "main") {
return makeText(value, "main-regular", mode);
} else {
return makeSpan(["amsrm"], [makeText(value, "ams-regular", mode)]);
}
};
var mathrmSize = function(value, size, mode) {
return makeText(value, "size" + size + "-regular", mode);
}
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.toDOM();
};
module.exports = buildTree;