Add support for \left and \right

Summary:
Added stacked delimiter support for more delimiters. Split out delimiter
functions into its own file, and split out some tree building functions into a
common file. Supports the empty `.` delimiter with \left and \right, and doesn't
try to produce huge /, \backslash, <, or > delimiters. Depends on D7844

Test input:

\left( \left) \left[ \left\lbrack \left] \left\rbrack \left\{ \left\lbrace
\left\} \left\rbrace \left\lfloor \left\rfloor \left\lceil \left\rceil
\left\langle \left\rangle \left/ \left\backslash \left| \left\vert \left\|
\left\Vert \left\uparrow \left\Uparrow \left\downarrow \left\Downarrow
\left\updownarrow \left\Updownarrow {x^{x^{x^{x^{x^{x^{x^{x^{x^{x^x}}}}}}}}}}
\right.\right.\right.\right.\right.\right.\right.\right.\right.\right.
\right.\right.\right.\right.\right.\right.\right.\right.\right.\right.
\right.\right.\right.\right.\right.\right.\right.\right.

Test Plan:
 - Run the test input, see that it works
 - Run the tests, see that they work
 - Look at huxley screenshots (not here yet :( ) and make sure they look good

Reviewers: alpert

Reviewed By: alpert

Differential Revision: http://phabricator.khanacademy.org/D11602
This commit is contained in:
Emily Eisenberg
2014-09-04 21:58:43 -07:00
parent 513ae30fe1
commit c3f758c319
22 changed files with 837 additions and 317 deletions

View File

@@ -2,11 +2,15 @@ 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 utils = require("./utils");
var symbols = require("./symbols");
var utils = require("./utils");
var makeSpan = buildCommon.makeSpan;
var buildExpression = function(expression, options, prev) {
var groups = [];
@@ -18,46 +22,6 @@ var buildExpression = function(expression, options, prev) {
return groups;
};
var makeSpan = function(classes, children, color) {
var height = 0;
var depth = 0;
var maxFontSize = 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;
}
if (children[i].maxFontSize > maxFontSize) {
maxFontSize = children[i].maxFontSize;
}
}
}
var span = new domTree.span(
classes, children, height, depth, maxFontSize);
if (color) {
span.style.color = color;
}
return span;
};
var makeFontSizer = function(options, fontSize) {
var fontSizeInner = makeSpan([], [new domTree.textNode("\u200b")]);
fontSizeInner.style.fontSize = (fontSize / options.style.sizeMultiplier) + "em";
var fontSizer = makeSpan(
["fontsize-ensurer", "reset-" + options.size, "size5"],
[fontSizeInner]);
return fontSizer;
};
var groupToType = {
mathord: "mord",
textord: "mord",
@@ -73,7 +37,8 @@ var groupToType = {
namedfn: "mop",
katex: "mord",
overline: "mord",
rule: "mord"
rule: "mord",
leftright: "minner"
};
var getTypeOfGroup = function(group) {
@@ -89,7 +54,7 @@ var getTypeOfGroup = function(group) {
} else if (group.type === "sizing") {
return getTypeOfGroup(group.value.value);
} else if (group.type === "delimsizing") {
return group.value.type;
return groupToType[group.value.type];
} else {
return groupToType[group.type];
}
@@ -117,7 +82,7 @@ var groupTypes = {
mathord: function(group, options, prev) {
return makeSpan(
["mord"],
[mathit(group.value, group.mode)],
[buildCommon.mathit(group.value, group.mode)],
options.getColor()
);
},
@@ -125,7 +90,7 @@ var groupTypes = {
textord: function(group, options, prev) {
return makeSpan(
["mord"],
[mathrm(group.value, group.mode)],
[buildCommon.mathrm(group.value, group.mode)],
options.getColor()
);
},
@@ -137,14 +102,14 @@ var groupTypes = {
var atoms = prevAtom.value.value;
prevAtom = atoms[atoms.length - 1];
}
if (!prev || utils.contains(["bin", "open", "rel", "op", "punct"],
prevAtom.type)) {
if (!prev || utils.contains(["mbin", "mopen", "mrel", "mop", "mpunct"],
getTypeOfGroup(prevAtom))) {
group.type = "ord";
className = "mord";
}
return makeSpan(
[className],
[mathrm(group.value, group.mode)],
[buildCommon.mathrm(group.value, group.mode)],
options.getColor()
);
},
@@ -152,7 +117,7 @@ var groupTypes = {
rel: function(group, options, prev) {
return makeSpan(
["mrel"],
[mathrm(group.value, group.mode)],
[buildCommon.mathrm(group.value, group.mode)],
options.getColor()
);
},
@@ -199,13 +164,13 @@ var groupTypes = {
var multiplier = Style.TEXT.sizeMultiplier *
options.style.sizeMultiplier;
// \scriptspace is 0.5pt = 0.05em * 10pt/em
var scriptspace = 0.05 / multiplier + "em";
var scriptspace =
(0.5 / fontMetrics.metrics.ptPerEm) / multiplier + "em";
var supsub;
if (!group.value.sup) {
var fontSizer = makeFontSizer(options, submid.maxFontSize);
var fontSizer = buildCommon.makeFontSizer(options, submid.maxFontSize);
var subwrap = makeSpan(["msub"], [fontSizer, submid]);
v = Math.max(v, fontMetrics.metrics.sub1,
@@ -221,7 +186,7 @@ var groupTypes = {
supsub = makeSpan(["msupsub"], [subwrap, fixIE]);
} else if (!group.value.sub) {
var fontSizer = makeFontSizer(options, supmid.maxFontSize);
var fontSizer = buildCommon.makeFontSizer(options, supmid.maxFontSize);
var supwrap = makeSpan(["msup"], [fontSizer, supmid]);
u = Math.max(u, p,
@@ -237,7 +202,7 @@ var groupTypes = {
supsub = makeSpan(["msupsub"], [supwrap, fixIE]);
} else {
var fontSizer = makeFontSizer(options,
var fontSizer = buildCommon.makeFontSizer(options,
Math.max(submid.maxFontSize, supmid.maxFontSize));
var subwrap = makeSpan(["msub"], [fontSizer, submid]);
var supwrap = makeSpan(["msup"], [fontSizer, supmid]);
@@ -274,13 +239,14 @@ var groupTypes = {
supsub = makeSpan(["msupsub"], [supwrap, subwrap, fixIE]);
}
return makeSpan([getTypeOfGroup(group.value.base)], [base, supsub]);
return makeSpan([getTypeOfGroup(group.value.base)],
[base, supsub]);
},
open: function(group, options, prev) {
return makeSpan(
["mopen"],
[mathrm(group.value, group.mode)],
[buildCommon.mathrm(group.value, group.mode)],
options.getColor()
);
},
@@ -288,7 +254,7 @@ var groupTypes = {
close: function(group, options, prev) {
return makeSpan(
["mclose"],
[mathrm(group.value, group.mode)],
[buildCommon.mathrm(group.value, group.mode)],
options.getColor()
);
},
@@ -310,7 +276,7 @@ var groupTypes = {
var denom = buildGroup(group.value.denom, options.withStyle(dstyle));
var denomdenom = makeSpan([fstyle.reset(), dstyle.cls()], [denom])
var fontSizer = makeFontSizer(options,
var fontSizer = buildCommon.makeFontSizer(options,
Math.max(numer.maxFontSize, denom.maxFontSize));
var numerrow = makeSpan(["mfracnum"], [fontSizer, numernumer]);
@@ -358,7 +324,8 @@ var groupTypes = {
frac.height *= fstyle.sizeMultiplier / options.style.sizeMultiplier;
frac.depth *= fstyle.sizeMultiplier / options.style.sizeMultiplier;
var wrap = makeSpan([options.style.reset(), fstyle.cls()], [frac]);
var wrap = makeSpan(
[options.style.reset(), fstyle.cls()], [frac]);
return makeSpan(["minner"], [
makeSpan(["mfrac"], [wrap])
@@ -366,25 +333,13 @@ var groupTypes = {
},
color: function(group, options, prev) {
var els = buildExpression(
var elements = 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);
return new buildCommon.makeFragment(elements);
},
spacing: function(group, options, prev) {
@@ -392,7 +347,7 @@ var groupTypes = {
group.value === " " || group.value === "~") {
return makeSpan(
["mord", "mspace"],
[mathrm(group.value, group.mode)]
[buildCommon.mathrm(group.value, group.mode)]
);
} else {
var spacingClassMap = {
@@ -405,7 +360,8 @@ var groupTypes = {
"\\!": "negativethinspace"
};
return makeSpan(["mord", "mspace", spacingClassMap[group.value]]);
return makeSpan(
["mord", "mspace", spacingClassMap[group.value]]);
}
},
@@ -413,20 +369,22 @@ var groupTypes = {
var inner = makeSpan(
["inner"], [buildGroup(group.value, options.reset())]);
var fix = makeSpan(["fix"], []);
return makeSpan(["llap", options.style.cls()], [inner, fix]);
return makeSpan(
["llap", options.style.cls()], [inner, fix]);
},
rlap: function(group, options, prev) {
var inner = makeSpan(
["inner"], [buildGroup(group.value, options.reset())]);
var fix = makeSpan(["fix"], []);
return makeSpan(["rlap", options.style.cls()], [inner, fix]);
return makeSpan(
["rlap", options.style.cls()], [inner, fix]);
},
punct: function(group, options, prev) {
return makeSpan(
["mpunct"],
[mathrm(group.value, group.mode)],
[buildCommon.mathrm(group.value, group.mode)],
options.getColor()
);
},
@@ -441,35 +399,41 @@ var groupTypes = {
namedfn: function(group, options, prev) {
var chars = [];
for (var i = 1; i < group.value.length; i++) {
chars.push(mathrm(group.value[i], group.mode));
chars.push(buildCommon.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)]);
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"], [mathrm("T", group.mode)]);
var e = makeSpan(["e"], [mathrm("E", group.mode)]);
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"], [mathrm("X", group.mode)]);
var x = makeSpan(
["x"], [buildCommon.mathrm("X", group.mode)]);
return makeSpan(["katex-logo"], [k, a, t, e, x], options.getColor());
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()));
var fontSizer = makeFontSizer(options, innerGroup.maxFontSize);
var fontSizer = buildCommon.makeFontSizer(options, innerGroup.maxFontSize);
// The theta variable in the TeXbook
var lineWidth = fontMetrics.metrics.defaultRuleThickness;
@@ -518,185 +482,51 @@ var groupTypes = {
},
delimsizing: function(group, options, prev) {
var normalDelimiters = [
"(", ")", "[", "\\lbrack", "]", "\\rbrack",
"\\{", "\\lbrace", "\\}", "\\rbrace",
"\\lfloor", "\\rfloor", "\\lceil", "\\rceil",
"<", ">", "\\langle", "\\rangle", "/", "\\backslash"
];
var delim = group.value.value;
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);
var node = makeSpan(
[options.style.reset(), Style.TEXT.cls(),
groupToType[group.value.type]],
[makeSpan(
["delimsizing", size, groupToType[group.value.type]],
[inner], options.getColor())]);
var multiplier = Style.TEXT.sizeMultiplier /
options.style.sizeMultiplier;
node.height *= multiplier;
node.depth *= multiplier;
node.maxFontSize = 1.0;
return node;
} 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);
var node = makeSpan(
[options.style.reset(), Style.TEXT.cls(),
groupToType[group.value.type]],
[makeSpan(["delimsizing", "mult"],
inners, options.getColor())]);
var multiplier = Style.TEXT.sizeMultiplier /
options.style.sizeMultiplier;
node.height *= multiplier;
node.depth *= multiplier;
node.maxFontSize = 1.0;
return node;
} else {
throw new ParseError("Illegal delimiter: '" + original + "'");
if (delim === ".") {
return buildCommon.makeSpan([groupToType[group.value.type]]);
}
return 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) {
@@ -772,47 +602,6 @@ var buildGroup = function(group, options, prev) {
}
};
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", "");