From d729ba528171ca1a3d4633cbe0edd018ec62a734 Mon Sep 17 00:00:00 2001 From: Emily Eisenberg Date: Thu, 27 Mar 2014 12:34:45 -0400 Subject: [PATCH] Add a \color command for custom colors Summary: Keep track of the color inside the style now, and use that when we are rendering things. Added a custom lexing mode to handle lexing colors correctly. Prefixed the old katex colors (internally) with "katex-" to distinguish them from CSS colors. Test Plan: - Run the normal tests, see they work - Run the huxley tests, see that they didn't change except for the color one which looks right Reviewers: alpert Reviewed By: alpert Differential Revision: http://phabricator.khanacademy.org/D7763 --- Lexer.js | 23 +++++++++ Options.js | 14 ++++++ Parser.js | 49 ++++++++++++++++++- buildTree.js | 56 ++++++++++++++-------- static/katex.less | 8 ---- test/huxley/Colors.huxley/screenshot0.png | Bin 8087 -> 7578 bytes test/huxley/Huxleyfile | 2 +- test/katex-tests.js | 38 +++++++++++++++ 8 files changed, 159 insertions(+), 31 deletions(-) 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 d9676c25b3aebd10b643ac7b0180d0eefe79fc99..4b2135a84f8c3ea896b2dad7a69dde63ace65a30 100644 GIT binary patch literal 7578 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