Add delimiter sizing

Summary:
Make delimiter sizing work. This involved
 - Adding the symbols for the remaining delimiters (like `\lfloor` and `\{`)
 - Adding metrics for the size1, size2, size3, and size4 fonts
 - Parsing delimiter sizing functions
 - Using the big fonts when possible, otherwise building large copies of the
   delimiters from scratch

Test Plan:
 - See that
   `\bigl\uparrow\Bigl\downarrow\biggl\updownarrow\Biggl\Uparrow
    \Biggr\Downarrow\biggr\Updownarrow\bigm/\Bigm\backslash\biggm|
    \Biggm|\big\lceil\Big\rceil\bigg\langle\Bigg\rangle\bigl(\Bigl)
    \biggl[\Biggl]\Biggr\{\biggr\}\Bigr\lfloor\bigr\rfloor`
   parses correctly (this contains all of the delimiters, and all of the sizing
   modes)
 - See that the huxley tests didn't change, and the new one looks good
 - See the normal tests work

Reviewers: alpert

Reviewed By: alpert

Differential Revision: http://phabricator.khanacademy.org/D7844
This commit is contained in:
Emily Eisenberg 2014-08-05 16:43:43 -07:00
parent 07e8d468de
commit 100798847b
10 changed files with 446 additions and 38 deletions

View File

@ -259,6 +259,30 @@ Parser.prototype.parseTextGroup = function(pos, mode) {
}
};
var delimiters = [
"(", ")", "[", "\\lbrack", "]", "\\rbrack",
"\\{", "\\lbrace", "\\}", "\\rbrace",
"\\lfloor", "\\rfloor", "\\lceil", "\\rceil",
"<", ">", "\\langle", "\\rangle",
"/", "\\backslash",
"|", "\\vert", "\\|", "\\Vert",
"\\uparrow", "\\Uparrow",
"\\downarrow", "\\Downarrow",
"\\updownarrow", "\\Updownarrow"
];
// Parse a single delimiter
Parser.prototype.parseDelimiter = function(pos, mode) {
var delim = this.lexer.lex(pos, mode);
if (utils.contains(delimiters, delim.text)) {
return new ParseResult(
new ParseNode("delimiter", delim.text),
delim.position);
} else {
return null;
}
};
// A list of 1-argument color functions
var colorFuncs = [
"\\blue", "\\orange", "\\pink", "\\red", "\\green", "\\gray", "\\purple"
@ -278,6 +302,25 @@ var namedFns = [
"\\tan","\\tanh"
];
var delimiterSizes = {
"\\bigl" : {type: "open", size: 1},
"\\Bigl" : {type: "open", size: 2},
"\\biggl": {type: "open", size: 3},
"\\Biggl": {type: "open", size: 4},
"\\bigr" : {type: "close", size: 1},
"\\Bigr" : {type: "close", size: 2},
"\\biggr": {type: "close", size: 3},
"\\Biggr": {type: "close", size: 4},
"\\bigm" : {type: "rel", size: 1},
"\\Bigm" : {type: "rel", size: 2},
"\\biggm": {type: "rel", size: 3},
"\\Biggm": {type: "rel", size: 4},
"\\big" : {type: "textord", size: 1},
"\\Big" : {type: "textord", size: 2},
"\\bigg" : {type: "textord", size: 3},
"\\Bigg" : {type: "textord", size: 4}
};
// Parses a "nucleus", which is either a single token from the tokenizer or a
// function and its arguments
Parser.prototype.parseNucleus = function(pos, mode) {
@ -348,6 +391,23 @@ Parser.prototype.parseNucleus = function(pos, mode) {
return new ParseResult(
new ParseNode("namedfn", nucleus.text, mode),
nucleus.position);
} else if (mode === "math" && delimiterSizes[nucleus.type]) {
// If this is a delimiter size function, we parse a single delimiter
var delim = this.parseDelimiter(nucleus.position, mode);
if (delim) {
var type = delimiterSizes[nucleus.type].type;
return new ParseResult(
new ParseNode("delimsizing", {
size: delimiterSizes[nucleus.type].size,
type: delimiterSizes[nucleus.type].type,
value: delim.result.value
}, mode),
delim.position);
} else {
throw new ParseError(
"Expected delimiter after '" + nucleus.text + "'");
}
} 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

@ -70,6 +70,8 @@ var getTypeOfGroup = function(group) {
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];
}
@ -180,7 +182,7 @@ var groupTypes = {
}
var supsub;
var fixIE = makeSpan(["fix-ie"], []);
var fixIE = makeSpan(["fix-ie"], [new domTree.textNode("\u00a0")]);
if (!group.value.sup) {
v = Math.max(v, fontMetrics.metrics.sub1,
@ -303,7 +305,7 @@ var groupTypes = {
denomrow.height = 0;
denomrow.depth = denomrow.depth + v;
var fixIE = makeSpan(["fix-ie"], []);
var fixIE = makeSpan(["fix-ie"], [new domTree.textNode("\u00a0")]);
var frac = makeSpan([], [numerrow, mid, denomrow, fixIE]);
@ -425,6 +427,165 @@ var groupTypes = {
["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 + "'");
}
}
};
@ -478,7 +639,7 @@ var buildGroup = function(group, options, prev) {
};
var makeText = function(value, style, mode) {
if (symbols[mode][value].replace) {
if (symbols[mode][value] && symbols[mode][value].replace) {
value = symbols[mode][value].replace;
}
@ -514,6 +675,10 @@ var mathrm = function(value, 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", "");

File diff suppressed because one or more lines are too long

View File

@ -39,7 +39,8 @@ end
font_dir = File.join(File.dirname(__FILE__), 'static/fonts/')
metrics = {}
%w[main-regular math-italic ams-regular].each do |face|
%w[main-regular math-italic ams-regular
size1-regular size2-regular size3-regular size4-regular].each do |face|
metrics[face] = metrics_for_file(File.join(font_dir, 'katex_%s.ttf' % face))
end

View File

@ -308,38 +308,63 @@ big parens
}
}
.sizing { display: inline-block; }
.sizing {
display: inline-block;
@size-1: 0.5;
@size-2: 0.7;
@size-3: 0.8;
@size-4: 0.9;
@size-5: 1.0;
@size-6: 1.2;
@size-7: 1.44;
@size-8: 1.73;
@size-9: 2.07;
@size-10: 2.49;
@size-1: 0.5;
@size-2: 0.7;
@size-3: 0.8;
@size-4: 0.9;
@size-5: 1.0;
@size-6: 1.2;
@size-7: 1.44;
@size-8: 1.73;
@size-9: 2.07;
@size-10: 2.49;
.generate-size-change(@from, @to) {
.reset-size@{from}.size@{to} {
@sizeFromVariable: ~"size-@{from}";
@sizeToVariable: ~"size-@{to}";
font-size: (@@sizeToVariable / @@sizeFromVariable) * 1em;
.generate-size-change(@from, @to) {
&.reset-size@{from}.size@{to} {
@sizeFromVariable: ~"size-@{from}";
@sizeToVariable: ~"size-@{to}";
font-size: (@@sizeToVariable / @@sizeFromVariable) * 1em;
}
}
.generate-to-size-change(@from, @currTo) when (@currTo =< 10) {
.generate-size-change(@from, @currTo);
.generate-to-size-change(@from, (@currTo + 1));
}
.generate-from-size-change(@currFrom) when (@currFrom =< 10) {
.generate-to-size-change(@currFrom, 1);
.generate-from-size-change((@currFrom + 1));
}
.generate-from-size-change(1);
}
.delimsizing {
&.size1 { font-family: katex_size1; }
&.size2 { font-family: katex_size2; }
&.size3 { font-family: katex_size3; }
&.size4 { font-family: katex_size4; }
&.mult {
.baseline-align-hack-outer;
> .fix-ie,
> .delimsizinginner {
.baseline-align-hack-middle;
position: relative;
&.size1 {
> span {
font-family: katex_size1;
}
}
}
}
}
.generate-to-size-change(@from, @currTo) when (@currTo =< 10) {
.generate-size-change(@from, @currTo);
.generate-to-size-change(@from, (@currTo + 1));
}
.generate-from-size-change(@currFrom) when (@currFrom =< 10) {
.generate-to-size-change(@currFrom, 1);
.generate-from-size-change((@currFrom + 1));
}
.generate-from-size-change(1);
}

View File

@ -314,7 +314,7 @@ var symbols = {
"\\lvert": {
font: "main",
group: "open",
replace: "|"
replace: "\u2223"
},
")": {
font: "main",
@ -340,7 +340,7 @@ var symbols = {
"\\rvert": {
font: "main",
group: "close",
replace: "|"
replace: "\u2223"
},
"=": {
font: "main",
@ -563,6 +563,111 @@ var symbols = {
font: "main",
group: "textord",
replace: "\u25b9"
},
"\\{": {
font: "main",
group: "open",
replace: "{"
},
"\\}": {
font: "main",
group: "close",
replace: "}"
},
"\\lbrace": {
font: "main",
group: "open",
replace: "{"
},
"\\rbrace": {
font: "main",
group: "close",
replace: "}"
},
"\\lbrack": {
font: "main",
group: "open",
replace: "["
},
"\\rbrack": {
font: "main",
group: "close",
replace: "]"
},
"\\lfloor": {
font: "main",
group: "open",
replace: "\u230a"
},
"\\rfloor": {
font: "main",
group: "close",
replace: "\u230b"
},
"\\lceil": {
font: "main",
group: "open",
replace: "\u2308"
},
"\\rceil": {
font: "main",
group: "close",
replace: "\u2309"
},
"\\backslash": {
font: "main",
group: "textord",
replace: "\\"
},
"|": {
font: "main",
group: "textord",
replace: "\u2223"
},
"\\vert": {
font: "main",
group: "textord",
replace: "\u2223"
},
"\\|": {
font: "main",
group: "textord",
replace: "\u2225"
},
"\\Vert": {
font: "main",
group: "textord",
replace: "\u2225"
},
"\\uparrow": {
font: "main",
group: "textord",
replace: "\u2191"
},
"\\Uparrow": {
font: "main",
group: "textord",
replace: "\u21d1"
},
"\\downarrow": {
font: "main",
group: "textord",
replace: "\u2193"
},
"\\Downarrow": {
font: "main",
group: "textord",
replace: "\u21d3"
},
"\\updownarrow": {
font: "main",
group: "textord",
replace: "\u2195"
},
"\\Updownarrow": {
font: "main",
group: "textord",
replace: "\u21d5"
}
},
"text": {
@ -584,7 +689,7 @@ var symbols = {
}
};
var mathTextSymbols = "0123456789/|@.\"";
var mathTextSymbols = "0123456789/@.\"";
for (var i = 0; i < mathTextSymbols.length; i++) {
var ch = mathTextSymbols.charAt(i);
symbols["math"][ch] = {

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

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

View File

@ -93,5 +93,11 @@
"name": "Lap",
"screenSize": [1024, 768],
"url": "http://localhost:7936/test/huxley/test.html?m=ab\\llap{f}cd\\rlap{g}h"
},
{
"name": "DelimiterSizing",
"screenSize": [1024, 768],
"url": "http://localhost:7936/test/huxley/test.html?m=\\bigl\\uparrow\\Bigl\\downarrow\\biggl\\updownarrow\\Biggl\\Uparrow\\Biggr\\Downarrow\\biggr\\langle\\Bigr\\}\\bigr\\rfloor"
}
]

View File

@ -626,3 +626,44 @@ describe("A tie parser", function() {
expect(parse[2].type).toMatch("spacing");
});
});
describe("A delimiter sizing parser", function() {
var normalDelim = "\\bigl |";
var notDelim = "\\bigl x";
var bigDelim = "\\Biggr \\langle";
it("should parse normal delimiters", function() {
expect(function() {
parseTree(normalDelim);
parseTree(bigDelim);
}).not.toThrow();
});
it("should not parse not-delimiters", function() {
expect(function() {
parseTree(notDelim);
}).toThrow();
});
it("should produce a delimsizing", function() {
var parse = parseTree(normalDelim)[0];
expect(parse.type).toMatch("delimsizing");
});
it("should produce the correct direction delimiter", function() {
var leftParse = parseTree(normalDelim)[0];
var rightParse = parseTree(bigDelim)[0];
expect(leftParse.value.type).toMatch("open");
expect(rightParse.value.type).toMatch("close");
});
it("should parse the correct size delimiter", function() {
var smallParse = parseTree(normalDelim)[0];
var bigParse = parseTree(bigDelim)[0];
expect(smallParse.value.size).toEqual(1);
expect(bigParse.value.size).toEqual(4);
});
});