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

@ -289,7 +289,8 @@ var delimiters = [
"|", "\\vert", "\\|", "\\Vert",
"\\uparrow", "\\Uparrow",
"\\downarrow", "\\Downarrow",
"\\updownarrow", "\\Updownarrow"
"\\updownarrow", "\\Updownarrow",
"."
];
// Parse a single delimiter
@ -429,6 +430,40 @@ Parser.prototype.parseNucleus = function(pos, mode) {
throw new ParseError(
"Expected delimiter after '" + nucleus.text + "'");
}
} else if (mode === "math" && nucleus.type === "\\left") {
// If we see a \left, first we parse the left delimiter
var leftDelim = this.parseDelimiter(nucleus.position, mode);
if (leftDelim) {
// Then, we parse an inner expression. Due to the handling of \right
// below, this should end just before the \right
var expression = this.parseExpression(leftDelim.position, mode);
// Make sure we see a \right
var right = this.lexer.lex(expression.position, mode);
this.expect(right, "\\right");
// Parse the right delimiter
var rightDelim = this.parseDelimiter(right.position, mode);
if (rightDelim) {
return new ParseResult(
new ParseNode("leftright", {
left: leftDelim.result.value,
right: rightDelim.result.value,
body: expression.result
}, mode),
rightDelim.position);
} else {
throw new ParseError(
"Expected delimiter after '" + right.text + "'");
}
} else {
throw new ParseError(
"Expected delimiter after '" + nucleus.text + "'");
}
} else if (mode === "math" && nucleus.type === "\\right") {
// If we see a right, we explicitly return null to break out of the
// parseExpression loop. The code for \left will handle the delimiter
return null;
} else if (nucleus.type === "\\llap" || nucleus.type === "\\rlap") {
// If this is an llap or rlap, parse its argument and return
var group = this.parseGroup(nucleus.position, mode);

View File

@ -77,5 +77,7 @@ var cramp = [Dc, Dc, Tc, Tc, Sc, Sc, SSc, SSc];
module.exports = {
DISPLAY: styles[D],
TEXT: styles[T]
TEXT: styles[T],
SCRIPT: styles[S],
SCRIPTSCRIPT: styles[SS]
};

104
buildCommon.js Normal file
View File

@ -0,0 +1,104 @@
var domTree = require("./domTree");
var fontMetrics = require("./fontMetrics");
var symbols = require("./symbols");
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 sizeElementFromChildren = function(elem) {
var height = 0;
var depth = 0;
var maxFontSize = 0;
if (elem.children) {
for (var i = 0; i < elem.children.length; i++) {
if (elem.children[i].height > height) {
height = elem.children[i].height;
}
if (elem.children[i].depth > depth) {
depth = elem.children[i].depth;
}
if (elem.children[i].maxFontSize > maxFontSize) {
maxFontSize = elem.children[i].maxFontSize;
}
}
}
elem.height = height;
elem.depth = depth;
elem.maxFontSize = maxFontSize;
};
var makeSpan = function(classes, children, color) {
var span = new domTree.span(classes, children);
sizeElementFromChildren(span);
if (color) {
span.style.color = color;
}
return span;
};
var makeFragment = function(children) {
var fragment = new domTree.documentFragment(children);
sizeElementFromChildren(fragment);
return fragment;
};
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;
};
module.exports = {
makeText: makeText,
mathit: mathit,
mathrm: mathrm,
makeSpan: makeSpan,
makeFragment: makeFragment,
makeFontSizer: makeFontSizer
};

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", "");

475
delimiter.js Normal file
View File

@ -0,0 +1,475 @@
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 buildCommon = require("./buildCommon");
var makeSpan = require("./buildCommon").makeSpan;
// Get the metrics for a given symbol and font, after transformation (i.e.
// after following replacement from symbols.js)
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 mathrmSize = function(value, size, mode) {
return buildCommon.makeText(value, "Size" + size + "-Regular", mode);
};
var styleWrap = function(delim, toStyle, options) {
var span = makeSpan(["style-wrap", options.style.reset(), toStyle.cls()], [delim]);
var multiplier = toStyle.sizeMultiplier / options.style.sizeMultiplier;
span.height *= multiplier;
span.depth *= multiplier;
span.maxFontSize = toStyle.sizeMultiplier;
return span;
};
var makeSmallDelim = function(delim, style, center, options, mode) {
var text = buildCommon.makeText(delim, "Main-Regular", mode);
var span = styleWrap(text, style, options);
if (center) {
var shift =
(1 - options.style.sizeMultiplier / style.sizeMultiplier) *
fontMetrics.metrics.axisHeight;
span.style.top = shift + "em";
span.height -= shift;
span.depth += shift;
}
return span;
};
var makeLargeDelim = function(delim, size, center, options, mode) {
var inner = mathrmSize(delim, size, mode);
var span = styleWrap(
makeSpan(["delimsizing", "size" + size],
[inner], options.getColor()),
Style.TEXT, options);
if (center) {
var shift = (1 - options.style.sizeMultiplier) *
fontMetrics.metrics.axisHeight;
span.style.top = shift + "em";
span.height -= shift;
span.depth += shift;
}
return span;
};
// Make an inner span with the given offset and in the given font
var makeInner = function(symbol, offset, font, mode) {
var sizeClass;
if (font === "Size1-Regular") {
sizeClass = "size1";
} else if (font === "Size4-Regular") {
sizeClass = "size4";
}
var inner = makeSpan(
["delimsizinginner", sizeClass],
[makeSpan([], [buildCommon.makeText(symbol, font, mode)])]);
inner.style.top = offset + "em";
inner.height -= offset;
inner.depth += offset;
return inner;
};
var makeStackedDelim = function(delim, heightTotal, center, options, mode) {
// There are four parts, the top, a middle, a repeated part, and a bottom.
var top, middle, repeat, bottom;
top = repeat = bottom = delim;
middle = null;
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
// repeats of the arrows
if (delim === "\\uparrow") {
repeat = bottom = "\u23d0";
} else if (delim === "\\Uparrow") {
repeat = bottom = "\u2016";
} else if (delim === "\\downarrow") {
top = repeat = "\u23d0";
} else if (delim === "\\Downarrow") {
top = repeat = "\u2016";
} else if (delim === "\\updownarrow") {
top = "\\uparrow";
repeat = "\u23d0";
bottom = "\\downarrow";
} else if (delim === "\\Updownarrow") {
top = "\\Uparrow";
repeat = "\u2016";
bottom = "\\Downarrow";
// For some reason, the sizes of this one delimiter don't work out
// right, so we shrink it a bit to make it now add an extraneous
// repeating part
if (height + depth <= 1.21) {
height -= 0.01;
depth -= 0.01;
}
} else if (delim === "|" || delim === "\\vert") {
overlap = true;
} else if (delim === "\\|" || delim === "\\Vert") {
overlap = true;
} else if (delim === "[" || delim === "\\lbrack") {
top = "\u23a1";
repeat = "\u23a2";
bottom = "\u23a3";
font = "Size4-Regular";
overlap = true;
} else if (delim === "]" || delim === "\\rbrack") {
top = "\u23a4";
repeat = "\u23a5";
bottom = "\u23a6";
font = "Size4-Regular";
overlap = true;
} else if (delim === "\\lfloor") {
repeat = top = "\u23a2";
bottom = "\u23a3";
font = "Size4-Regular";
overlap = true;
} else if (delim === "\\lceil") {
top = "\u23a1";
repeat = bottom = "\u23a2";
font = "Size4-Regular";
overlap = true;
} else if (delim === "\\rfloor") {
repeat = top = "\u23a5";
bottom = "\u23a6";
font = "Size4-Regular";
overlap = true;
} else if (delim === "\\rceil") {
top = "\u23a4";
repeat = bottom = "\u23a5";
font = "Size4-Regular";
overlap = true;
} else if (delim === "(") {
top = "\u239b";
repeat = "\u239c";
bottom = "\u239d";
font = "Size4-Regular";
overlap = true;
} else if (delim === ")") {
top = "\u239e";
repeat = "\u239f";
bottom = "\u23a0";
font = "Size4-Regular";
overlap = true;
} else if (delim === "\\{" || delim === "\\lbrace") {
top = "\u23a7";
middle = "\u23a8";
bottom = "\u23a9";
repeat = "\u23aa";
font = "Size4-Regular";
} else if (delim === "\\}" || delim === "\\rbrace") {
top = "\u23ab";
middle = "\u23ac";
bottom = "\u23ad";
repeat = "\u23aa";
font = "Size4-Regular";
}
// Get the metrics of the three sections
var topMetrics = getMetrics(top, font);
var topHeightTotal = topMetrics.height + topMetrics.depth;
var repeatMetrics = getMetrics(repeat, font);
var repeatHeightTotal = repeatMetrics.height + repeatMetrics.depth;
var bottomMetrics = getMetrics(bottom, font);
var bottomHeightTotal = bottomMetrics.height + bottomMetrics.depth;
var middleMetrics, middleHeightTotal;
if (middle !== null) {
middleMetrics = getMetrics(middle, font);
middleHeightTotal = middleMetrics.height + middleMetrics.depth;
}
var realHeightTotal = topHeightTotal + bottomHeightTotal;
if (middle !== null) {
realHeightTotal += middleHeightTotal;
}
while (realHeightTotal < heightTotal) {
realHeightTotal += repeatHeightTotal;
if (middle !== null) {
realHeightTotal += repeatHeightTotal;
}
}
var axisHeight = fontMetrics.metrics.axisHeight;
if (center) {
axisHeight *= options.style.sizeMultiplier;
}
var height = realHeightTotal / 2 + axisHeight;
var depth = realHeightTotal / 2 - axisHeight;
// Keep a list of the inner spans
var inners = [];
// Add the top symbol
inners.push(
makeInner(top, topMetrics.height - height, font, mode));
if (middle === null) {
var repeatHeight = realHeightTotal - topHeightTotal - bottomHeightTotal;
var symbolCount = Math.ceil(repeatHeight / repeatHeightTotal);
var overlapAmount;
if (overlap) {
// 2 * overlapAmount + repeatHeight =
// (symbolCount - 1) * (repeatHeightTotal - overlapAmount) +
// repeatHeightTotal
overlapAmount = (symbolCount * repeatHeightTotal -
repeatHeight) / (symbolCount + 1);
} else {
overlapAmount = 0;
}
// Add repeat symbols until there's only space for the bottom symbol
var currHeight = height - topHeightTotal + overlapAmount;
for (var i = 0; i < symbolCount; i++) {
inners.push(
makeInner(repeat,
repeatMetrics.height - currHeight, font, mode));
currHeight -= repeatHeightTotal - overlapAmount;
}
} else {
// When there is a middle bit, we need the middle part and two repeated
// sections
// Calculate the number of symbols needed for the top and bottom
// repeated parts
var topRepeatHeight =
realHeightTotal / 2 - topHeightTotal - middleHeightTotal / 2;
var topSymbolCount = Math.ceil(topRepeatHeight / repeatHeightTotal);
var bottomRepeatHeight =
realHeightTotal / 2 - topHeightTotal - middleHeightTotal / 2;
var bottomSymbolCount =
Math.ceil(bottomRepeatHeight / repeatHeightTotal);
// Add the top repeated part
var currHeight = height - topHeightTotal;
for (var i = 0; i < topSymbolCount; i++) {
inners.push(
makeInner(repeat,
repeatMetrics.height - currHeight, font, mode));
currHeight -= repeatHeightTotal;
}
// Add the middle piece
var midPoint = realHeightTotal / 2 - depth;
inners.push(
makeInner(middle,
middleMetrics.height - midPoint - middleHeightTotal / 2,
font, mode));
// Add the bottom repeated part
currHeight = midPoint - middleHeightTotal / 2;
for (var i = 0; i < bottomSymbolCount; i++) {
inners.push(
makeInner(repeat,
repeatMetrics.height - currHeight, font, mode));
currHeight -= repeatHeightTotal;
}
}
// Add the bottom symbol
inners.push(
makeInner(bottom, depth - bottomMetrics.depth, font, mode));
var fixIE = makeSpan(["fix-ie"], [new domTree.textNode("\u00a0")]);
inners.push(fixIE);
return styleWrap(
makeSpan(["delimsizing", "mult"], inners, options.getColor()),
Style.TEXT, options);
};
var normalDelimiters = [
"(", ")", "[", "\\lbrack", "]", "\\rbrack",
"\\{", "\\lbrace", "\\}", "\\rbrace",
"\\lfloor", "\\rfloor", "\\lceil", "\\rceil",
"<", ">", "\\langle", "\\rangle", "/", "\\backslash"
];
var stackDelimiters = [
"\\uparrow", "\\downarrow", "\\updownarrow",
"\\Uparrow", "\\Downarrow", "\\Updownarrow",
"|", "\\|", "\\vert", "\\Vert"
];
var onlyNormalDelimiters = [
"<", ">", "\\langle", "\\rangle", "/", "\\backslash"
];
// Metrics of the different sizes. Found by looking at TeX's output of
// $\bigl| \Bigl| \biggl| \Biggl| \showlists$
var sizeToMaxHeight = [0, 1.2, 1.8, 2.4, 3.0];
var makeSizedDelim = function(delim, size, options, mode) {
if (delim === "<") {
delim = "\\langle";
} else if (delim === ">") {
delim = "\\rangle";
}
var retDelim;
if (utils.contains(normalDelimiters, delim)) {
return makeLargeDelim(delim, size, false, options, mode);
} else if (utils.contains(stackDelimiters, delim)) {
return makeStackedDelim(
delim, sizeToMaxHeight[size], false, options, mode);
} else {
throw new ParseError("Illegal delimiter: '" + delim + "'");
}
};
var normalDelimiterSequence = [
{type: "small", style: Style.SCRIPTSCRIPT},
{type: "small", style: Style.SCRIPT},
{type: "small", style: Style.TEXT},
{type: "large", size: 1},
{type: "large", size: 2},
{type: "large", size: 3},
{type: "large", size: 4}
];
var stackAlwaysDelimiterSequence = [
{type: "small", style: Style.SCRIPTSCRIPT},
{type: "small", style: Style.SCRIPT},
{type: "small", style: Style.TEXT},
{type: "stack"}
];
var stackLargeDelimiterSequence = [
{type: "small", style: Style.SCRIPTSCRIPT},
{type: "small", style: Style.SCRIPT},
{type: "small", style: Style.TEXT},
{type: "large", size: 1},
{type: "large", size: 2},
{type: "large", size: 3},
{type: "large", size: 4},
{type: "stack"}
];
var delimTypeToFont = function(type) {
if (type.type === "small") {
return "Main-Regular";
} else if (type.type === "large") {
return "Size" + type.size + "-Regular";
} else if (type.type === "stack") {
return "Size4-Regular";
}
};
var traverseSequence = function(delim, height, sequence, options) {
// Here, we choose the index we should start at in the sequences. In smaller
// sizes (which correspond to larger numbers in style.size) we start earlier
// in the sequence. Thus, scriptscript starts at index 3-3=0, script starts
// at index 3-2=1, text starts at 3-1=2, and display starts at min(2,3-0)=2
var start = Math.min(2, 3 - options.style.size);
for (var i = start; i < sequence.length; i++) {
if (sequence[i].type === "stack") {
// This is always the last delimiter, so we just break the loop now.
break;
}
var metrics = getMetrics(delim, delimTypeToFont(sequence[i]));
var heightDepth = metrics.height + metrics.depth;
if (sequence[i].type === "small") {
heightDepth *= sequence[i].style.sizeMultiplier;
}
if (heightDepth > height) {
return sequence[i];
}
}
return sequence[sequence.length - 1];
};
var makeCustomSizedDelim = function(delim, height, center, options, mode) {
if (delim === "<") {
delim = "\\langle";
} else if (delim === ">") {
delim = "\\rangle";
}
var sequence;
if (utils.contains(onlyNormalDelimiters, delim)) {
sequence = normalDelimiterSequence;
} else if (utils.contains(normalDelimiters, delim)) {
sequence = stackLargeDelimiterSequence;
} else {
sequence = stackAlwaysDelimiterSequence;
}
var delimType = traverseSequence(delim, height, sequence, options);
if (delimType.type === "small") {
return makeSmallDelim(delim, delimType.style, center, options, mode);
} else if (delimType.type === "large") {
return makeLargeDelim(delim, delimType.size, center, options, mode);
} else if (delimType.type === "stack") {
return makeStackedDelim(delim, height, center, options, mode);
}
};
var makeLeftRightDelim = function(delim, height, depth, options, mode) {
var axisHeight =
fontMetrics.metrics.axisHeight * options.style.sizeMultiplier;
// Taken from TeX source, tex.web, function make_left_right
var delimiterFactor = 901;
var delimiterExtend = 5.0 / fontMetrics.metrics.ptPerEm;
var maxDistFromAxis = Math.max(
height - axisHeight, depth + axisHeight);
var totalHeight = Math.max(
// In real TeX, calculations are done using integral values which are
// 65536 per pt, or 655360 per em. So, the division here truncates in
// TeX but doesn't here, producing different results. If we wanted to
// exactly match TeX's calculation, we could do
// Math.floor(655360 * maxDistFromAxis / 500) *
// delimiterFactor / 655360
// (To see the difference, compare
// x^{x^{\left(\rule{0.1em}{0.68em}\right)}}
// in TeX and KaTeX)
maxDistFromAxis / 500 * delimiterFactor,
2 * maxDistFromAxis - delimiterExtend);
return makeCustomSizedDelim(delim, totalHeight, true, options, mode);
};
module.exports = {
sizedDelim: makeSizedDelim,
customSizedDelim: makeCustomSizedDelim,
leftRightDelim: makeLeftRightDelim
};

View File

@ -37,10 +37,11 @@ span.prototype.toDOM = function() {
return span;
};
function documentFragment(children, height, depth) {
function documentFragment(children, height, depth, maxFontSize) {
this.children = children || [];
this.height = height || 0;
this.depth = depth || 0;
this.maxFontSize = maxFontSize || 0;
}
documentFragment.prototype.toDOM = function() {

File diff suppressed because one or more lines are too long

View File

@ -53,9 +53,9 @@ def main():
tfm_char = font_name_to_tfm[font].get_char_metrics(tex_char_num)
height = round(tfm_char.height + yshift / 1000.0, 3)
depth = round(tfm_char.depth - yshift / 1000.0, 3)
italic = round(tfm_char.italic_correction, 3)
height = round(tfm_char.height + yshift / 1000.0, 5)
depth = round(tfm_char.depth - yshift / 1000.0, 5)
italic = round(tfm_char.italic_correction, 5)
families[family][char_num] = {
'height': height,

View File

@ -8,7 +8,9 @@
<link href="main.css" rel="stylesheet" type="text/css">
</head>
<body>
<input type="text" value="\blue\dfrac{\frac{\phi^2}{3}-G_a^{x^3}}{2\times3+4}+\orange\dfrac{(x^2+y^2)^\frac{1}{2}}{\tan\psi^\tau+2/3}" id="input" />
<input type="text"
value="(\left( x \right) \left( x^2 \right) \left( \frac{a}{b} \right) \left( \frac{a^2}{b} \right) \left( \dfrac{a}{b} \right) \left( \dfrac{a^2}{b} \right)"
id="input" />
<div id="math"></div>
<input id="permalink" type="button" value="permalink">
<script src="main.js" type="text/javascript"></script>

View File

@ -39,6 +39,11 @@ big parens
table-layout: fixed;
}
// This value is also used in fontMetrics.js, if you change it make sure the
// values match.
@ptperem: 10.0;
@nulldelimiterspace: 1.2em / @ptperem;
@thinspace: 0.16667em;
@mediumspace: 0.22222em;
@thickspace: 0.27778em;
@ -162,6 +167,10 @@ big parens
.reset-scriptscriptstyle.scriptstyle { font-size: 1.4em; }
.reset-scriptscriptstyle.scriptscriptstyle { font-size: 1em; }
.style-wrap {
position: relative;
}
.baseline-align-hack-outer() {
display: inline-block;
}
@ -392,12 +401,18 @@ big parens
.baseline-align-hack-middle;
position: relative;
&.size1 {
> span {
font-family: KaTeX_Size1;
}
&.size1 > span {
font-family: Katex_Size1;
}
&.size4 > span {
font-family: Katex_Size4;
}
}
}
}
.nulldelimiter {
display: inline-block;
width: @nulldelimiterspace;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -131,9 +131,27 @@
"url": "http://localhost:7936/test/huxley/test.html?m=\\rule{1em}{0.5em}\\rule{1ex}{2ex}\\rule{1em}{1ex}\\rule{1em}{0.431ex}"
},
{
"name": "LeftRight",
"screenSize": [1024, 768],
"url": "http://localhost:7936/test/huxley/test.html?m=\\left( x^2 \\right) \\left\\{ x^{x^{x^{x^x}}} \\right."
},
{
"name": "LeftRightStyleSizing",
"screenSize": [1024, 768],
"url": "http://localhost:7936/test/huxley/test.html?m=+\\left\\{\\rule{0.1em}{1em}\\right.x^{+\\left\\{\\rule{0.1em}{1em}\\right.x^{+\\left\\{\\rule{0.1em}{1em}\\right.}}"
},
{
"name": "PrimeSpacing",
"screenSize": [1024, 768],
"url": "http://localhost:7936/test/huxley/test.html?m=f'+f_2'+f^{f'}"
},
{
"name": "NullDelimiterInteraction",
"screenSize": [1024, 768],
"url": "http://localhost:7936/test/huxley/test.html?m=a \\bigl. + 2 \\quad \\left. + a \\right)"
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -0,0 +1,5 @@
[
{
"action": "screenshot"
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,5 @@
[
{
"action": "screenshot"
}
]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,5 @@
[
{
"action": "screenshot"
}
]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -739,3 +739,62 @@ describe("A rule parser", function() {
expect(hardNumberParse.value.height.number).toBeCloseTo(2.45);
});
});
describe("A left/right parser", function() {
var normalLeftRight = "\\left( \\dfrac{x}{y} \\right)";
var emptyRight = "\\left( \\dfrac{x}{y} \\right.";
it("should not fail", function() {
expect(function() {
parseTree(normalLeftRight);
}).not.toThrow();
});
it("should produce a leftright", function() {
var parse = parseTree(normalLeftRight)[0];
expect(parse.type).toMatch("leftright");
expect(parse.value.left).toMatch("\\(");
expect(parse.value.right).toMatch("\\)");
});
it("should error when it is mismatched", function() {
var unmatchedLeft = "\\left( \\dfrac{x}{y}";
var unmatchedRight = "\\dfrac{x}{y} \\right)";
expect(function() {
parseTree(unmatchedLeft);
}).toThrow();
expect(function() {
parseTree(unmatchedRight);
}).toThrow();
});
it("should error when braces are mismatched", function() {
var unmatched = "{ \\left( \\dfrac{x}{y} } \\right)";
expect(function() {
parseTree(unmatched);
}).toThrow();
});
it("should error when non-delimiters are provided", function() {
var nonDelimiter = "\\left$ \\dfrac{x}{y} \\right)";
expect(function() {
parseTree(nonDelimiter);
}).toThrow();
});
it("should parse the empty '.' delimiter", function() {
expect(function() {
parseTree(emptyRight);
}).not.toThrow();
});
it("should parse the '.' delimiter with normal sizes", function() {
var normalEmpty = "\\Bigl .";
expect(function() {
parseTree(normalEmpty);
}).not.toThrow();
});
});