scribble-math/buildTree.js
Emily Eisenberg a76f6eb985 Make all the lines the correct sizes
Fix the \frac and \overline lines so they actually render at the correct sizes
(i.e. don't get smaller when in script and scriptscript sizes). This also seems
to fix #33.

Test Plan:
 - Make sure new huxley screenshot look good.
 - Render
     \frac{1}{\Bigl(\sqrt{\phi \sqrt{5}}-\phi\Bigr) e^{\frac25 \pi}} =
     1+\frac{e^{-2\pi}} {1+\frac{e^{-4\pi}} {1+\frac{e^{-6\pi}}
     {1+\frac{e^{-8\pi}} {1+...} } } }
   at small font sizes like 30pt and smaller, see that it looks much better

Auditors: alpert
2014-09-10 16:30:38 -07:00

697 lines
22 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 === "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]);
}
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 multiplier = Style.TEXT.sizeMultiplier *
options.style.sizeMultiplier;
var scriptspace =
(0.5 / fontMetrics.metrics.ptPerEm) / multiplier + "em";
var supsub;
if (!group.value.sup) {
var fontSizer = buildCommon.makeFontSizer(options, submid.maxFontSize);
var subwrap = makeSpan(["msub"], [fontSizer, submid]);
v = Math.max(v, fontMetrics.metrics.sub1,
sub.height - 0.8 * fontMetrics.metrics.xHeight);
subwrap.style.top = v + "em";
subwrap.style.marginRight = scriptspace;
subwrap.depth = subwrap.depth + v;
subwrap.height = 0;
var fixIE = makeSpan(["fix-ie"], [fontSizer, new domTree.textNode("\u00a0")]);
supsub = makeSpan(["msupsub"], [subwrap, fixIE]);
} else if (!group.value.sub) {
var fontSizer = buildCommon.makeFontSizer(options, supmid.maxFontSize);
var supwrap = makeSpan(["msup"], [fontSizer, supmid]);
u = Math.max(u, p,
sup.depth + 0.25 * fontMetrics.metrics.xHeight);
supwrap.style.top = -u + "em";
supwrap.style.marginRight = scriptspace;
supwrap.height = supwrap.height + u;
supwrap.depth = 0;
var fixIE = makeSpan(["fix-ie"], [fontSizer, new domTree.textNode("\u00a0")]);
supsub = makeSpan(["msupsub"], [supwrap, fixIE]);
} else {
var fontSizer = buildCommon.makeFontSizer(options,
Math.max(submid.maxFontSize, supmid.maxFontSize));
var subwrap = makeSpan(["msub"], [fontSizer, submid]);
var supwrap = makeSpan(["msup"], [fontSizer, supmid]);
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.style.marginRight = scriptspace;
subwrap.style.marginRight = scriptspace;
supwrap.height = supwrap.height + u;
supwrap.depth = 0;
subwrap.height = 0;
subwrap.depth = subwrap.depth + v;
var fixIE = makeSpan(["fix-ie"], [fontSizer, new domTree.textNode("\u00a0")]);
supsub = makeSpan(["msupsub"], [supwrap, subwrap, fixIE]);
}
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 numernumer = makeSpan([fstyle.reset(), nstyle.cls()], [numer]);
var denom = buildGroup(group.value.denom, options.withStyle(dstyle));
var denomdenom = makeSpan([fstyle.reset(), dstyle.cls()], [denom])
var fontSizer = buildCommon.makeFontSizer(options,
Math.max(numer.maxFontSize, denom.maxFontSize));
var line = makeSpan([options.style.reset(), Style.TEXT.cls(), "line"]);
var numerrow = makeSpan(["mfracnum"], [fontSizer, numernumer]);
var mid = makeSpan(["mfracmid"], [fontSizer, line]);
var denomrow = makeSpan(["mfracden"], [fontSizer, denomdenom]);
var theta = fontMetrics.metrics.defaultRuleThickness / options.style.sizeMultiplier;
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"], [
fontSizer, 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 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 innerGroup = buildGroup(group.value.body,
options.withStyle(options.style.cramp()));
var fontSizer = buildCommon.makeFontSizer(
options, Math.max(innerGroup.maxFontSize, 1.0));
// The theta variable in the TeXbook
var lineWidth = fontMetrics.metrics.defaultRuleThickness;
var lineInner =
makeSpan([options.style.reset(), Style.TEXT.cls(), "line"]);
lineInner.maxFontSize = 1.0;
var line = makeSpan(["sqrt-line"], [fontSizer, lineInner]);
var inner = makeSpan(["sqrt-inner"], [fontSizer, innerGroup]);
var fixIE = makeSpan(
["fix-ie"], [fontSizer, new domTree.textNode("\u00a0")]);
var theta = fontMetrics.metrics.defaultRuleThickness /
options.style.sizeMultiplier;
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)]);
var delimDepth = delim.height + delim.depth;
if (delimDepth > inner.height + inner.depth + psi) {
psi = (psi + delimDepth - inner.height - inner.depth) / 2;
}
delim.style.top = (-inner.height - psi + delim.height - theta) + "em";
line.style.top = (-inner.height - psi) + "em";
line.height = inner.height + psi + 2 * theta;
// 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 = makeSpan(["sqrt-body"], [line, inner, fixIE]);
}
return makeSpan(["sqrt", "mord"], [delim, body]);
},
overline: function(group, options, prev) {
var innerGroup = buildGroup(group.value.body,
options.withStyle(options.style.cramp()));
var fontSizer = buildCommon.makeFontSizer(options, innerGroup.maxFontSize);
// The theta variable in the TeXbook
var lineWidth = fontMetrics.metrics.defaultRuleThickness /
options.style.sizeMultiplier;
var line = makeSpan(
["overline-line"], [fontSizer, makeSpan([options.style.reset(), Style.TEXT.cls(), "line"])]);
var inner = makeSpan(["overline-inner"], [fontSizer, innerGroup]);
var fixIE = makeSpan(
["fix-ie"], [fontSizer, new domTree.textNode("\u00a0")]);
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 = 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;
},
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.toDOM();
};
module.exports = buildTree;