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
This commit is contained in:
Emily Eisenberg 2014-08-29 14:45:27 -07:00
parent a75bf1afc2
commit f17bbf1b05
8 changed files with 178 additions and 4 deletions

View File

@ -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);
}
};

View File

@ -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

View File

@ -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;
}
};

View File

@ -305,6 +305,11 @@ big parens
}
}
.rule {
display: inline-block;
border-style: solid;
}
.overline {
.baseline-align-hack-outer;

View File

@ -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}"
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

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

View File

@ -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);
});
});