scribble-math/buildTree.js
Emily Eisenberg ad97dab19c Update fonts from MathJax
Summary:
Also, rename all of our uses of fonts to use the uppercased versions. We want to
use the uppercase versions because it makes updating and modifying the fonts
much easier (since the font names inside the actual font files are uppercased).

Test Plan:
  - Make sure the huxley screenshots look good (You can compare a diff of them
    on github at
    f90d093361
    By my eye, it seems like some things have moved up ~1/2 pixel, and some of
    the fonts have maybe slightly changed shape, like the large `b` in
    SizingBaseline)

Reviewers: alpert

Reviewed By: alpert

Differential Revision: http://phabricator.khanacademy.org/D11979
2014-08-06 17:52:26 -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;