commit f17bbf1b0533d93ad282f44ad98b06ea8a38c95a
parent a75bf1afc2b13540dbfa051b66642bf5db53d241
Author: Emily Eisenberg <emily@khanacademy.org>
Date: Fri, 29 Aug 2014 14:45:27 -0700
Add the '\rule' command for drawing boxes
Summary:
Supports the 'ex' and 'em' units for sizes. Doesn't support the optional depth
argument.
Test Plan:
- See that the huxley test looks good, and nothing else changed
- See that the tests pass
Reviewers: alpert
Reviewed By: alpert
Differential Revision: http://phabricator.khanacademy.org/D12777
Diffstat:
8 files changed, 178 insertions(+), 4 deletions(-)
diff --git a/Lexer.js b/Lexer.js
@@ -36,6 +36,9 @@ var textNormals = [
[/^~/, "spacing"]
];
+var whitespaceRegex = /^\s*/;
+var whitespaceConcatRegex = /^( +|\\ +)/;
+
// Build a regex to easily parse the functions
var anyFunc = /^\\(?:[a-zA-Z]+|.)/;
@@ -44,12 +47,12 @@ Lexer.prototype._innerLex = function(pos, normals, ignoreWhitespace) {
// Get rid of whitespace
if (ignoreWhitespace) {
- var whitespace = input.match(/^\s*/)[0];
+ var whitespace = input.match(whitespaceRegex)[0];
pos += whitespace.length;
input = input.slice(whitespace.length);
} else {
// Do the funky concatenation of whitespace
- var whitespace = input.match(/^( +|\\ +)/);
+ var whitespace = input.match(whitespaceConcatRegex);
if (whitespace !== null) {
return new LexResult(" ", " ", pos + whitespace[0].length);
}
@@ -90,7 +93,7 @@ Lexer.prototype._innerLexColor = function(pos) {
var input = this._input.slice(pos);
// Ignore whitespace
- var whitespace = input.match(/^\s*/)[0];
+ var whitespace = input.match(whitespaceRegex)[0];
pos += whitespace.length;
input = input.slice(whitespace.length);
@@ -104,6 +107,31 @@ Lexer.prototype._innerLexColor = function(pos) {
throw new ParseError("Invalid color", this, pos);
};
+var sizeRegex = /^(\d+(?:\.\d*)?|\.\d+)\s*([a-z]{2})/;
+
+Lexer.prototype._innerLexSize = function(pos) {
+ var input = this._input.slice(pos);
+
+ // Ignore whitespace
+ var whitespace = input.match(whitespaceRegex)[0];
+ pos += whitespace.length;
+ input = input.slice(whitespace.length);
+
+ var match;
+ if ((match = input.match(sizeRegex))) {
+ var unit = match[2];
+ if (unit !== "em" && unit !== "ex") {
+ throw new ParseError("Invalid unit: '" + unit + "'", this, pos);
+ }
+ return new LexResult("size", {
+ number: +match[1],
+ unit: unit
+ }, pos + match[0].length);
+ }
+
+ throw new ParseError("Invalid size", this, pos);
+};
+
// Lex a single token
Lexer.prototype.lex = function(pos, mode) {
if (mode === "math") {
@@ -112,6 +140,8 @@ Lexer.prototype.lex = function(pos, mode) {
return this._innerLex(pos, textNormals, false);
} else if (mode === "color") {
return this._innerLexColor(pos);
+ } else if (mode === "size") {
+ return this._innerLexSize(pos);
}
};
diff --git a/Parser.js b/Parser.js
@@ -259,6 +259,27 @@ Parser.prototype.parseTextGroup = function(pos, mode) {
}
};
+Parser.prototype.parseSizeGroup = function(pos, mode) {
+ var start = this.lexer.lex(pos, mode);
+ // Try to parse an open brace
+ if (start.type === "{") {
+ // Parse the size
+ var size = this.lexer.lex(start.position, "size");
+ // Make sure we get a close brace
+ var closeBrace = this.lexer.lex(size.position, mode);
+ this.expect(closeBrace, "}");
+ return new ParseResult(
+ new ParseNode("size", size.text),
+ closeBrace.position);
+ } else {
+ // It has to have an open brace, so if it doesn't we throw
+ throw new ParseError(
+ "There must be braces around sizes",
+ this.lexer, pos
+ );
+ }
+};
+
var delimiters = [
"(", ")", "[", "\\lbrack", "]", "\\rbrack",
"\\{", "\\lbrace", "\\}", "\\rbrace",
@@ -479,6 +500,31 @@ Parser.prototype.parseNucleus = function(pos, mode) {
this.lexer, nucleus.position
);
}
+ } else if (mode === "math" && nucleus.type === "\\rule") {
+ // Parse the width of the rule
+ var widthGroup = this.parseSizeGroup(nucleus.position, mode);
+ if (widthGroup) {
+ // Parse the height of the rule
+ var heightGroup = this.parseSizeGroup(widthGroup.position, mode);
+ if (heightGroup) {
+ return new ParseResult(
+ new ParseNode("rule", {
+ width: widthGroup.result.value,
+ height: heightGroup.result.value
+ }, mode),
+ heightGroup.position);
+ } else {
+ throw new ParseError("Expected second size group after '" +
+ nucleus.type + "'",
+ this.lexer, nucleus.position
+ );
+ }
+ } else {
+ throw new ParseError("Expected size group after '" +
+ nucleus.type + "'",
+ this.lexer, nucleus.position
+ );
+ }
} else if (symbols[mode][nucleus.text]) {
// Otherwise if this is a no-argument function, find the type it
// corresponds to in the symbols map
diff --git a/buildTree.js b/buildTree.js
@@ -72,7 +72,8 @@ var groupToType = {
ordgroup: "mord",
namedfn: "mop",
katex: "mord",
- overline: "mord"
+ overline: "mord",
+ rule: "mord"
};
var getTypeOfGroup = function(group) {
@@ -696,6 +697,31 @@ var groupTypes = {
} else {
throw new ParseError("Illegal delimiter: '" + original + "'");
}
+ },
+
+ rule: function(group, options, prev) {
+ // Make an empty span for the rule
+ var rule = makeSpan(["mord", "rule"], []);
+
+ 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;
+ }
+
+ // 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;
}
};
diff --git a/static/katex.less b/static/katex.less
@@ -305,6 +305,11 @@ big parens
}
}
+ .rule {
+ display: inline-block;
+ border-style: solid;
+ }
+
.overline {
.baseline-align-hack-outer;
diff --git a/test/huxley/Huxleyfile.json b/test/huxley/Huxleyfile.json
@@ -123,5 +123,11 @@
"name": "SupSubHorizSpacing",
"screenSize": [1024, 768],
"url": "http://localhost:7936/test/huxley/test.html?m=x^{x^{x}}\\Big|x_{x_{x_{x_{x}}}}\\bigg|x^{x^{x_{x_{x_{x_{x}}}}}}\\bigg|"
+ },
+
+ {
+ "name": "Rule",
+ "screenSize": [1024, 768],
+ "url": "http://localhost:7936/test/huxley/test.html?m=\\rule{1em}{0.5em}\\rule{1ex}{2ex}\\rule{1em}{1ex}\\rule{1em}{0.431ex}"
}
]
diff --git a/test/huxley/Rule.hux/firefox-1.png b/test/huxley/Rule.hux/firefox-1.png
Binary files differ.
diff --git a/test/huxley/Rule.hux/record.json b/test/huxley/Rule.hux/record.json
@@ -0,0 +1,5 @@
+[
+ {
+ "action": "screenshot"
+ }
+]
diff --git a/test/katex-tests.js b/test/katex-tests.js
@@ -683,3 +683,59 @@ describe("An overline parser", function() {
expect(parse.type).toMatch("overline");
});
});
+
+describe("A rule parser", function() {
+ var emRule = "\\rule{1em}{2em}";
+ var exRule = "\\rule{1ex}{2em}";
+ var badUnitRule = "\\rule{1px}{2em}";
+ var noNumberRule = "\\rule{1em}{em}";
+ var incompleteRule = "\\rule{1em}";
+ var hardNumberRule = "\\rule{ 01.24ex}{2.450 em }";
+
+ it("should not fail", function() {
+ expect(function() {
+ parseTree(emRule);
+ parseTree(exRule);
+ }).not.toThrow();
+ });
+
+ it("should not parse invalid units", function() {
+ expect(function() {
+ parseTree(badUnitRule);
+ }).toThrow();
+
+ expect(function() {
+ parseTree(noNumberRule);
+ }).toThrow();
+ });
+
+ it("should not parse incomplete rules", function() {
+ expect(function() {
+ parseTree(incompleteRule);
+ }).toThrow();
+ });
+
+ it("should produce a rule", function() {
+ var parse = parseTree(emRule)[0];
+
+ expect(parse.type).toMatch("rule");
+ });
+
+ it("should list the correct units", function() {
+ var emParse = parseTree(emRule)[0];
+ var exParse = parseTree(exRule)[0];
+
+ expect(emParse.value.width.unit).toMatch("em");
+ expect(emParse.value.height.unit).toMatch("em");
+
+ expect(exParse.value.width.unit).toMatch("ex");
+ expect(exParse.value.height.unit).toMatch("em");
+ });
+
+ it("should parse the number correctly", function() {
+ var hardNumberParse = parseTree(hardNumberRule)[0];
+
+ expect(hardNumberParse.value.width.number).toBeCloseTo(1.24);
+ expect(hardNumberParse.value.height.number).toBeCloseTo(2.45);
+ });
+});