diff --git a/Lexer.js b/Lexer.js index 507c3abca..2e55ee484 100644 --- 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 index 3116e3ebd..028686021 100644 --- 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 index 7cd18a78c..5ab3b6ef8 100644 --- 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 index 822b9c3ca..47b0ceaea 100644 --- 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 index 335e6075c..de7d40439 100644 --- 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 new file mode 100644 index 000000000..8bc17bb6e Binary files /dev/null and b/test/huxley/Rule.hux/firefox-1.png differ diff --git a/test/huxley/Rule.hux/record.json b/test/huxley/Rule.hux/record.json new file mode 100644 index 000000000..3cae6ac65 --- /dev/null +++ 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 index 97a4faded..40b15c847 100644 --- 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); + }); +});