scribble-math/buildTree.js
Emily Eisenberg 387c159a8e Add sizing functions (like \small)
Summary:
Right now, when the size gets bigger, this still doesn't work, so there's a
check to prevent that. However, functions that go smaller (like `\small`,
`\tiny`, etc) do work. Also, we can't seem to use the sizing functions inside
of fractions (so something like `\dfrac{\small\frac{x}{y}}{z}` doesn't work).
However, the most prominent use case is `\small` as the outer-most object, so
this is still helpful. This commit has the parsing and stuff to handle all of
it, but it'll throw an error if you try to do something that doesn't work. (For
the record, "doesn't work" means "looks bad", not "throws an unexpected
error").

Test Plan:
Make sure things like `\small x` work, and things like `\Huge x` and
`\frac{\small x}{y}` don't.

Reviewers: alpert

Reviewed By: alpert

Differential Revision: http://phabricator.khanacademy.org/D3619
2013-08-21 20:22:24 -07:00

541 lines
15 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 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) {
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;
}
}
}
return new domTree.span(classes, children, height, depth);
};
var groupToType = {
mathord: "mord",
textord: "mord",
bin: "mbin",
rel: "mrel",
amsrel: "mrel",
open: "mopen",
close: "mclose",
frac: "minner",
spacing: "mord",
punct: "mpunct",
ordgroup: "mord",
namedfn: "mop",
katex: "mord",
};
var getTypeOfGroup = function(group) {
if (group == null) {
// Like when typesetting $^3$
return groupToType.ord;
} 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 {
return groupToType[group.type];
}
};
var groupTypes = {
mathord: function(group, options, prev) {
return makeSpan(["mord", options.color], [mathit(group.value)]);
},
textord: function(group, options, prev) {
return makeSpan(["mord", options.color], [mathrm(group.value)]);
},
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, options.color], [mathrm(group.value)]);
},
rel: function(group, options, prev) {
return makeSpan(["mrel", options.color], [mathrm(group.value)]);
},
amsrel: function(group, options, prev) {
return makeSpan(["mrel", options.color], [amsrm(group.value)]);
},
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]);
var supwrap = makeSpan(["msup", options.style.reset()], [supmid]);
}
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 subwrap = makeSpan(["msub"], [submid]);
}
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;
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]);
} 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]);
} 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]);
}
return makeSpan([getTypeOfGroup(group.value.base)], [base, supsub]);
},
open: function(group, options, prev) {
return makeSpan(["mopen", options.color], [mathrm(group.value)]);
},
close: function(group, options, prev) {
return makeSpan(["mclose", options.color], [mathrm(group.value)]);
},
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 numerrow = makeSpan(["mfracnum"], [numernumer]);
var mid = makeSpan(["mfracmid"], [makeSpan()]);
var denom = buildGroup(group.value.denom, options.withStyle(dstyle));
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 frac = makeSpan([], [numerrow, mid, denomrow]);
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", options.color], [
makeSpan(["mfrac"], [wrap])
]);
},
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") {
return makeSpan(["mord", "mspace"], [mathrm(group.value)]);
} else {
var spacingClassMap = {
"\\qquad": "qquad",
"\\quad": "quad",
"\\;": "thickspace",
"\\:": "mediumspace",
"\\,": "thinspace"
};
return makeSpan(["mord", "mspace", spacingClassMap[group.value]]);
}
},
llap: function(group, options, prev) {
var inner = makeSpan([], [buildGroup(group.value, options.reset())]);
return makeSpan(["llap", options.style.cls()], [inner]);
},
rlap: function(group, options, prev) {
var inner = makeSpan([], [buildGroup(group.value, options.reset())]);
return makeSpan(["rlap", options.style.cls()], [inner]);
},
punct: function(group, options, prev) {
return makeSpan(["mpunct", options.color], [mathrm(group.value)]);
},
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]));
}
return makeSpan(["mop", options.color], chars);
},
katex: function(group, options, prev) {
var k = makeSpan(["k"], [mathrm("K")]);
var a = makeSpan(["a"], [mathrm("A")]);
a.height = (a.height + 0.2) * 0.75;
a.depth = (a.height - 0.2) * 0.75;
var t = makeSpan(["t"], [mathrm("T")]);
var e = makeSpan(["e"], [mathrm("E")]);
e.height = (e.height - 0.2155);
e.depth = (e.depth + 0.2155);
var x = makeSpan(["x"], [mathrm("X")]);
return makeSpan(["katex-logo", options.color], [k, a, t, e, x]);
},
sizing: function(group, options, prev) {
var inner = buildGroup(group.value.value,
options.withSize(group.value.size), prev);
return makeSpan(
["reset-" + options.size, group.value.size,
getTypeOfGroup(group.value.value)],
[inner]);
}
};
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;
if (multiplier > 1) {
throw new ParseError(
"Error: Can't go from small to large style");
}
groupNode.height *= multiplier;
groupNode.depth *= multiplier;
}
if (options.size !== options.parentSize) {
var multiplier = sizingMultiplier[options.size] /
sizingMultiplier[options.parentSize];
if (multiplier > 1) {
throw new ParseError(
"Error: Can't go from small to large size");
}
if (options.depth > 1) {
throw new ParseError(
"Error: Can't use sizing outside of the root node");
}
groupNode.height *= multiplier;
groupNode.depth *= multiplier;
}
return groupNode;
} else {
throw new ParseError(
"Lex error: Got group of unknown type: '" + group.type + "'");
}
};
var charLookup = {
"*": "\u2217",
"-": "\u2212",
"`": "\u2018",
"\\ ": "\u00a0",
"\\$": "$",
"\\angle": "\u2220",
"\\cdot": "\u22c5",
"\\circ": "\u2218",
"\\colon": ":",
"\\div": "\u00f7",
"\\geq": "\u2265",
"\\gets": "\u2190",
"\\infty": "\u221e",
"\\leftarrow": "\u2190",
"\\leq": "\u2264",
"\\lvert": "|",
"\\neq": "\u2260",
"\\ngeq": "\u2271",
"\\nleq": "\u2270",
"\\pm": "\u00b1",
"\\prime": "\u2032",
"\\rightarrow": "\u2192",
"\\rvert": "|",
"\\space": "\u00a0",
"\\times": "\u00d7",
"\\to": "\u2192",
"\\alpha": "\u03b1",
"\\beta": "\u03b2",
"\\gamma": "\u03b3",
"\\delta": "\u03b4",
"\\epsilon": "\u03f5",
"\\zeta": "\u03b6",
"\\eta": "\u03b7",
"\\theta": "\u03b8",
"\\iota": "\u03b9",
"\\kappa": "\u03ba",
"\\lambda": "\u03bb",
"\\mu": "\u03bc",
"\\nu": "\u03bd",
"\\xi": "\u03be",
"\\omicron": "o",
"\\pi": "\u03c0",
"\\rho": "\u03c1",
"\\sigma": "\u03c3",
"\\tau": "\u03c4",
"\\upsilon": "\u03c5",
"\\phi": "\u03d5",
"\\chi": "\u03c7",
"\\psi": "\u03c8",
"\\omega": "\u03c9",
"\\varepsilon": "\u03b5",
"\\vartheta": "\u03d1",
"\\varpi": "\u03d6",
"\\varrho": "\u03f1",
"\\varsigma": "\u03c2",
"\\varphi": "\u03c6",
"\\Gamma": "\u0393",
"\\Delta": "\u0394",
"\\Theta": "\u0398",
"\\Lambda": "\u039b",
"\\Xi": "\u039e",
"\\Pi": "\u03a0",
"\\Sigma": "\u03a3",
"\\Upsilon": "\u03a5",
"\\Phi": "\u03a6",
"\\Psi": "\u03a8",
"\\Omega": "\u03a9"
};
var makeText = function(value, style) {
if (value in charLookup) {
value = charLookup[value];
}
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["margin-right"] = 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) {
return makeSpan(["mathit"], [makeText(value, "math-italic")]);
};
var mathrm = function(value) {
return makeText(value, "main-regular");
};
var amsrm = function(value) {
return makeSpan(["amsrm"], [makeText(value, "ams-regular")]);
};
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";
var katexNode = makeSpan(["katex"], [topStrut, bottomStrut, span]);
return katexNode.toDOM();
};
module.exports = buildTree;