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
This commit is contained in:
Emily Eisenberg 2014-03-27 12:34:45 -04:00
parent 7723d3dcaf
commit d729ba5281
8 changed files with 159 additions and 31 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

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

View File

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