diff --git a/Lexer.js b/Lexer.js index 37ec056c7..b1d8d6aae 100644 --- a/Lexer.js +++ b/Lexer.js @@ -81,12 +81,35 @@ Lexer.prototype._innerLex = function(pos, normals, ignoreWhitespace) { "' at position " + pos); } +// A regex to match a CSS color (like #ffffff or BlueViolet) +var cssColor = /^(#[a-z0-9]+|[a-z]+)/i; + +Lexer.prototype._innerLexColor = function(pos) { + var input = this._input.slice(pos); + + // Ignore whitespace + var whitespace = input.match(/^\s*/)[0]; + pos += whitespace.length; + input = input.slice(whitespace.length); + + var match; + if ((match = input.match(cssColor))) { + // If we look like a color, return a color + return new LexResult("color", match[0], pos + match[0].length); + } + + // We didn't match a color, so throw an error. + throw new ParseError("Invalid color at position " + pos); +}; + // Lex a single token Lexer.prototype.lex = function(pos, mode) { if (mode === "math") { return this._innerLex(pos, mathNormals, true); } else if (mode === "text") { return this._innerLex(pos, textNormals, false); + } else if (mode === "color") { + return this._innerLexColor(pos); } }; diff --git a/Options.js b/Options.js index a65ca3ff2..5e741df0c 100644 --- a/Options.js +++ b/Options.js @@ -40,4 +40,18 @@ Options.prototype.reset = function() { this.style, this.size); }; +var colorMap = { + "katex-blue": "#6495ed", + "katex-orange": "#ffa500", + "katex-pink": "#ff00af", + "katex-red": "#df0030", + "katex-green": "#28ae7b", + "katex-gray": "gray", + "katex-purple": "#9d38bd" +}; + +Options.prototype.getColor = function() { + return colorMap[this.color] || this.color; +}; + module.exports = Options; diff --git a/Parser.js b/Parser.js index 0db2e1458..d2939272e 100644 --- a/Parser.js +++ b/Parser.js @@ -193,6 +193,26 @@ Parser.prototype.parseGroup = function(pos, mode) { } }; +// Parses a custom color group, which looks like "{#ffffff}" +Parser.prototype.parseColorGroup = function(pos, mode) { + var start = this.lexer.lex(pos, mode); + // Try to parse an open brace + if (start.type === "{") { + // Parse the color + var color = this.lexer.lex(start.position, "color"); + // Make sure we get a close brace + var closeBrace = this.lexer.lex(color.position, mode); + expect(closeBrace, "}"); + return new ParseResult( + new ParseNode("color", color.text), + closeBrace.position); + } else { + // It has to have an open brace, so if it doesn't we throw + throw new ParseError( + "Parse error: There must be braces around colors"); + } +}; + // A list of 1-argument color functions var colorFuncs = [ "\\blue", "\\orange", "\\pink", "\\red", "\\green", "\\gray", "\\purple" @@ -229,12 +249,39 @@ Parser.prototype.parseNucleus = function(pos, mode) { } return new ParseResult( new ParseNode("color", - {color: nucleus.type.slice(1), value: atoms}, mode), + {color: "katex-" + nucleus.type.slice(1), value: atoms}, + mode), group.position); } else { throw new ParseError( "Expected group after '" + nucleus.text + "'"); } + } else if (nucleus.type === "\\color") { + // If this is a custom color function, parse its first argument as a + // custom color and its second argument normally + var color = this.parseColorGroup(nucleus.position, mode); + if (color) { + var inner = this.parseGroup(color.position, mode); + if (inner) { + var atoms; + if (inner.result.type === "ordgroup") { + atoms = inner.result.value; + } else { + atoms = [inner.result]; + } + return new ParseResult( + new ParseNode("color", + {color: color.result.value, value: atoms}, + mode), + inner.position); + } else { + throw new ParseError( + "Expected second group after '" + nucleus.text + "'"); + } + } else { + throw new ParseError( + "Expected color after '" + nucleus.text + "'"); + } } else if (mode === "math" && utils.contains(sizeFuncs, nucleus.type)) { // If this is a size function, parse its argument and return var group = this.parseGroup(nucleus.position, mode); diff --git a/buildTree.js b/buildTree.js index 9c6e9ad3e..2e16993e5 100644 --- a/buildTree.js +++ b/buildTree.js @@ -18,7 +18,7 @@ var buildExpression = function(expression, options, prev) { return groups; }; -var makeSpan = function(classes, children) { +var makeSpan = function(classes, children, color) { var height = 0; var depth = 0; @@ -33,7 +33,13 @@ var makeSpan = function(classes, children) { } } - return new domTree.span(classes, children, height, depth); + var span = new domTree.span(classes, children, height, depth); + + if (color) { + span.style.color = color; + } + + return span; }; var groupToType = { @@ -71,15 +77,17 @@ var getTypeOfGroup = function(group) { var groupTypes = { mathord: function(group, options, prev) { return makeSpan( - ["mord", options.color], - [mathit(group.value, group.mode)] + ["mord"], + [mathit(group.value, group.mode)], + options.getColor() ); }, textord: function(group, options, prev) { return makeSpan( - ["mord", options.color], - [mathrm(group.value, group.mode)] + ["mord"], + [mathrm(group.value, group.mode)], + options.getColor() ); }, @@ -96,15 +104,17 @@ var groupTypes = { className = "mord"; } return makeSpan( - [className, options.color], - [mathrm(group.value, group.mode)] + [className], + [mathrm(group.value, group.mode)], + options.getColor() ); }, rel: function(group, options, prev) { return makeSpan( - ["mrel", options.color], - [mathrm(group.value, group.mode)] + ["mrel"], + [mathrm(group.value, group.mode)], + options.getColor() ); }, @@ -201,15 +211,17 @@ var groupTypes = { open: function(group, options, prev) { return makeSpan( - ["mopen", options.color], - [mathrm(group.value, group.mode)] + ["mopen"], + [mathrm(group.value, group.mode)], + options.getColor() ); }, close: function(group, options, prev) { return makeSpan( - ["mclose", options.color], - [mathrm(group.value, group.mode)] + ["mclose"], + [mathrm(group.value, group.mode)], + options.getColor() ); }, @@ -276,9 +288,9 @@ var groupTypes = { var wrap = makeSpan([options.style.reset(), fstyle.cls()], [frac]); - return makeSpan(["minner", options.color], [ + return makeSpan(["minner"], [ makeSpan(["mfrac"], [wrap]) - ]); + ], options.getColor()); }, color: function(group, options, prev) { @@ -339,13 +351,15 @@ var groupTypes = { punct: function(group, options, prev) { return makeSpan( - ["mpunct", options.color], - [mathrm(group.value, group.mode)] + ["mpunct"], + [mathrm(group.value, group.mode)], + options.getColor() ); }, ordgroup: function(group, options, prev) { - return makeSpan(["mord", options.style.cls()], + return makeSpan( + ["mord", options.style.cls()], buildExpression(group.value, options.reset()) ); }, @@ -356,7 +370,7 @@ var groupTypes = { chars.push(mathrm(group.value[i], group.mode)); } - return makeSpan(["mop", options.color], chars); + return makeSpan(["mop"], chars, options.getColor()); }, katex: function(group, options, prev) { @@ -374,7 +388,7 @@ var groupTypes = { var x = makeSpan(["x"], [mathrm("X", group.mode)]); - return makeSpan(["katex-logo", options.color], [k, a, t, e, x]); + return makeSpan(["katex-logo"], [k, a, t, e, x], options.getColor()); }, sizing: function(group, options, prev) { diff --git a/static/katex.less b/static/katex.less index 34fb63988..fca4e6412 100644 --- a/static/katex.less +++ b/static/katex.less @@ -288,14 +288,6 @@ big parens left: 0; } - .blue { color: #6495ed; } - .orange { color: #ffa500; } - .pink { color: #ff00af; } - .red { color: #df0030; } - .green { color: #28ae7b; } - .gray { color: gray; } - .purple { color: #9d38bd; } - .katex-logo { .a { font-size: 0.75em; diff --git a/test/huxley/Colors.huxley/screenshot0.png b/test/huxley/Colors.huxley/screenshot0.png index d9676c25b..4b2135a84 100644 Binary files a/test/huxley/Colors.huxley/screenshot0.png and b/test/huxley/Colors.huxley/screenshot0.png differ diff --git a/test/huxley/Huxleyfile b/test/huxley/Huxleyfile index 18116a139..dd6cc6af0 100644 --- a/test/huxley/Huxleyfile +++ b/test/huxley/Huxleyfile @@ -11,7 +11,7 @@ url=http://localhost:7936/test/huxley/test.html?m=\dfrac{\frac{a}{b}}{\frac{c}{d url=http://localhost:7936/test/huxley/test.html?m=a^{a^a_a}_{a^a_a} [Colors] -url=http://localhost:7936/test/huxley/test.html?m=\blue{a}\green{b}\red{c} +url=http://localhost:7936/test/huxley/test.html?m=\blue{a}\color{%%230f0}{b}\color{red}{c} [GreekLetters] url=http://localhost:7936/test/huxley/test.html?m=\alpha\beta\gamma\omega diff --git a/test/katex-tests.js b/test/katex-tests.js index 95b366e73..eed2e8333 100644 --- a/test/katex-tests.js +++ b/test/katex-tests.js @@ -496,3 +496,41 @@ describe("A text parser", function() { expect(group[3].type).toMatch("spacing"); }); }); + +describe("A color parser", function() { + var colorExpression = "\\blue{x}"; + var customColorExpression = "\\color{#fA6}{x}"; + var badCustomColorExpression = "\\color{bad-color}{x}"; + + it("should not fail", function() { + expect(function() { + parseTree(colorExpression); + }).not.toThrow(); + }); + + it("should build a color node", function() { + var parse = parseTree(colorExpression)[0]; + + expect(parse.type).toMatch("color"); + expect(parse.value.color).toBeDefined(); + expect(parse.value.value).toBeDefined(); + }); + + it("should parse a custom color", function() { + expect(function() { + parseTree(customColorExpression); + }).not.toThrow(); + }); + + it("should correctly extract the custom color", function() { + var parse = parseTree(customColorExpression)[0]; + + expect(parse.value.color).toMatch("#fA6"); + }); + + it("should not parse a bad custom color", function() { + expect(function() { + parseTree(badCustomColorExpression); + }).toThrow(); + }); +});