Adds math commands, HTML rendering, and screenshotter tests.

This is part 2 of 3.  Part 1 added new fonts metrics.  Part 2 will add MathML support and unit tests.
This commit is contained in:
Kevin Barabash 2015-07-04 15:28:18 -06:00 committed by Kevin Barabash
parent f32d615813
commit fd2d58fd80
18 changed files with 338 additions and 34 deletions

View File

@ -6,9 +6,9 @@
*/
/**
* This is the main options class. It contains the style, size, and color of the
* current parse level. It also contains the style and size of the parent parse
* level, so size changes can be handled efficiently.
* This is the main options class. It contains the style, size, color and font
* of the current parse level. It also contains the style and size of the parent
* parse level, so size changes can be handled efficiently.
*
* Each of the `.with*` and `.reset` functions passes its current style and size
* as the parentStyle and parentSize of the new options class, so parent
@ -19,6 +19,7 @@ function Options(data) {
this.color = data.color;
this.size = data.size;
this.phantom = data.phantom;
this.font = data.font;
if (data.parentStyle === undefined) {
this.parentStyle = data.style;
@ -44,7 +45,8 @@ Options.prototype.extend = function(extension) {
color: this.color,
parentStyle: this.style,
parentSize: this.size,
phantom: this.phantom
phantom: this.phantom,
font: this.font
};
for (var key in extension) {
@ -92,6 +94,15 @@ Options.prototype.withPhantom = function() {
});
};
/**
* Create a new options objects with the give font.
*/
Options.prototype.withFont = function(font) {
return this.extend({
font: font
});
};
/**
* Create a new options object with the same style, size, and color. This is
* used so that parent style and size changes are handled correctly.

View File

@ -6,6 +6,26 @@
var domTree = require("./domTree");
var fontMetrics = require("./fontMetrics");
var symbols = require("./symbols");
var utils = require("./utils");
var greekCapitals = [
"\\Gamma",
"\\Delta",
"\\Theta",
"\\Lambda",
"\\Xi",
"\\Pi",
"\\Sigma",
"\\Upsilon",
"\\Phi",
"\\Psi",
"\\Omega"
];
var dotlessLetters = [
"\u0131", // dotless i, \imath
"\u0237" // dotless j, \jmath
];
/**
* Makes a symbolNode after translation via the list of symbols in symbols.js.
@ -41,17 +61,10 @@ var makeSymbol = function(value, style, mode, color, classes) {
};
/**
* Makes a symbol in the italic math font.
* Makes a symbol in Main-Regular or AMS-Regular.
* Used for rel, bin, open, close, inner, and punct.
*/
var mathit = function(value, mode, color, classes) {
return makeSymbol(
value, "Math-Italic", mode, color, classes.concat(["mathit"]));
};
/**
* Makes a symbol in the upright roman font.
*/
var mathrm = function(value, mode, color, classes) {
var mathsym = function(value, mode, color, classes) {
// Decide what font to render the symbol in by its entry in the symbols
// table.
// Have a special case for when the value = \ because the \ is used as a
@ -66,6 +79,67 @@ var mathrm = function(value, mode, color, classes) {
}
};
/**
* Makes a symbol in the default font for mathords and textords.
*/
var mathDefault = function(value, mode, color, classes, type) {
if (type === "mathord") {
return mathit(value, mode, color, classes);
} else if (type === "textord") {
return makeSymbol(
value, "Main-Regular", mode, color, classes.concat(["mathrm"]));
} else {
throw new Error("unexpected type: " + type + " in mathDefault");
}
};
/**
* Makes a symbol in the italic math font.
*/
var mathit = function(value, mode, color, classes) {
if (/[0-9]/.test(value.charAt(0)) ||
// glyphs for \imath and \jmath do not exist in Math-Italic so we
// need to use Main-Italic instead
utils.contains(dotlessLetters, value) ||
utils.contains(greekCapitals, value)) {
return makeSymbol(
value, "Main-Italic", mode, color, classes.concat(["mainit"]));
} else {
return makeSymbol(
value, "Math-Italic", mode, color, classes.concat(["mathit"]));
}
};
/**
* Makes either a mathord or textord in the correct font and color.
*/
var makeOrd = function(group, options, type) {
var mode = group.mode;
var value = group.value;
if (symbols[mode][value] && symbols[mode][value].replace) {
value = symbols[mode][value].replace;
}
var classes = ["mord"];
var color = options.getColor();
var font = options.font;
if (font) {
if (font === "mathit" || utils.contains(dotlessLetters, value)) {
return mathit(value, mode, color, classes);
} else {
var fontName = fontMap[font].fontName;
if (fontMetrics.getCharacterMetrics(value, fontName)) {
return makeSymbol(value, fontName, mode, color, classes.concat([font]));
} else {
return mathDefault(value, mode, color, classes, type);
}
}
} else {
return mathDefault(value, mode, color, classes, type);
}
};
/**
* Calculate the height, depth, and maxFontSize of an element based on its
* children.
@ -312,13 +386,61 @@ var spacingFunctions = {
}
};
/**
* Maps TeX font commands to objects containing:
* - variant: string used for "mathvariant" attribute in buildMathML.js
* - fontName: the "style" parameter to fontMetrics.getCharacterMetrics
*/
// A map between tex font commands an MathML mathvariant attribute values
var fontMap = {
// styles
"mathbf": {
variant: "bold",
fontName: "Main-Bold"
},
"mathrm": {
variant: "normal",
fontName: "Main-Regular"
},
// "mathit" is missing because it requires the use of two fonts: Main-Italic
// and Math-Italic. This is handled by a special case in makeOrd which ends
// up calling mathit.
// families
"mathbb": {
variant: "double-struck",
fontName: "AMS-Regular"
},
"mathcal": {
variant: "script",
fontName: "Caligraphic-Regular"
},
"mathfrak": {
variant: "fraktur",
fontName: "Fraktur-Regular"
},
"mathscr": {
variant: "script",
fontName: "Script-Regular"
},
"mathsf": {
variant: "sans-serif",
fontName: "SansSerif-Regular"
},
"mathtt": {
variant: "monospace",
fontName: "Typewriter-Regular"
}
};
module.exports = {
makeSymbol: makeSymbol,
mathit: mathit,
mathrm: mathrm,
mathsym: mathsym,
makeSpan: makeSpan,
makeFragment: makeFragment,
makeVList: makeVList,
makeOrd: makeOrd,
sizingMultiplier: sizingMultiplier,
spacingFunctions: spacingFunctions
};

View File

@ -171,13 +171,11 @@ var makeNullDelimiter = function(options) {
*/
var groupTypes = {
mathord: function(group, options, prev) {
return buildCommon.mathit(
group.value, group.mode, options.getColor(), ["mord"]);
return buildCommon.makeOrd(group, options, "mathord");
},
textord: function(group, options, prev) {
return buildCommon.mathrm(
group.value, group.mode, options.getColor(), ["mord"]);
return buildCommon.makeOrd(group, options, "textord");
},
bin: function(group, options, prev) {
@ -199,32 +197,32 @@ var groupTypes = {
className = "mord";
}
return buildCommon.mathrm(
return buildCommon.mathsym(
group.value, group.mode, options.getColor(), [className]);
},
rel: function(group, options, prev) {
return buildCommon.mathrm(
return buildCommon.mathsym(
group.value, group.mode, options.getColor(), ["mrel"]);
},
open: function(group, options, prev) {
return buildCommon.mathrm(
return buildCommon.mathsym(
group.value, group.mode, options.getColor(), ["mopen"]);
},
close: function(group, options, prev) {
return buildCommon.mathrm(
return buildCommon.mathsym(
group.value, group.mode, options.getColor(), ["mclose"]);
},
inner: function(group, options, prev) {
return buildCommon.mathrm(
return buildCommon.mathsym(
group.value, group.mode, options.getColor(), ["minner"]);
},
punct: function(group, options, prev) {
return buildCommon.mathrm(
return buildCommon.mathsym(
group.value, group.mode, options.getColor(), ["mpunct"]);
},
@ -628,7 +626,7 @@ var groupTypes = {
// into appropriate outputs.
return makeSpan(
["mord", "mspace"],
[buildCommon.mathrm(group.value, group.mode)]
[buildCommon.mathsym(group.value, group.mode)]
);
} else {
// Other kinds of spaces are of arbitrary width. We use CSS to
@ -712,7 +710,7 @@ var groupTypes = {
// operators, like \limsup
var output = [];
for (var i = 1; i < group.value.body.length; i++) {
output.push(buildCommon.mathrm(group.value.body[i], group.mode));
output.push(buildCommon.mathsym(group.value.body[i], group.mode));
}
base = makeSpan(["mop"], output, options.getColor());
}
@ -819,26 +817,26 @@ var groupTypes = {
// good, but the offsets for the T, E, and X were taken from the
// definition of \TeX in TeX (see TeXbook pg. 356)
var k = makeSpan(
["k"], [buildCommon.mathrm("K", group.mode)]);
["k"], [buildCommon.mathsym("K", group.mode)]);
var a = makeSpan(
["a"], [buildCommon.mathrm("A", group.mode)]);
["a"], [buildCommon.mathsym("A", group.mode)]);
a.height = (a.height + 0.2) * 0.75;
a.depth = (a.height - 0.2) * 0.75;
var t = makeSpan(
["t"], [buildCommon.mathrm("T", group.mode)]);
["t"], [buildCommon.mathsym("T", group.mode)]);
var e = makeSpan(
["e"], [buildCommon.mathrm("E", group.mode)]);
["e"], [buildCommon.mathsym("E", group.mode)]);
e.height = (e.height - 0.2155);
e.depth = (e.depth + 0.2155);
var x = makeSpan(
["x"], [buildCommon.mathrm("X", group.mode)]);
["x"], [buildCommon.mathsym("X", group.mode)]);
return makeSpan(
["katex-logo"], [k, a, t, e, x], options.getColor());
["katex-logo", "mord"], [k, a, t, e, x], options.getColor());
},
overline: function(group, options, prev) {
@ -1006,6 +1004,11 @@ var groupTypes = {
return makeSpan([options.style.reset(), newStyle.cls()], inner);
},
font: function(group, options, prev) {
var font = group.value.font;
return buildGroup(group.value.body, options.withFont(font), prev);
},
delimsizing: function(group, options, prev) {
var delim = group.value.value;

View File

@ -252,6 +252,11 @@ var groupTypes = {
return node;
},
font: function(group) {
// pass through so we can render something without throwing
return buildGroup(group.value.body);
},
spacing: function(group) {
var node;

View File

@ -215,6 +215,12 @@ var delimiters = [
"."
];
var fontAliases = {
"\\Bbb": "\\mathbb",
"\\bold": "\\mathbf",
"\\frak": "\\mathfrak"
};
/*
* This is a list of functions which each have the same function but have
* different names so that we don't have to duplicate the data a bunch of times.
@ -476,6 +482,33 @@ var duplicatedFunctions = [
}
},
{
funcs: [
// styles
"\\mathrm", "\\mathit", "\\mathbf",
// families
"\\mathbb", "\\mathcal", "\\mathfrak", "\\mathscr", "\\mathsf",
"\\mathtt",
// aliases
"\\Bbb", "\\bold", "\\frak"
],
data: {
numArgs: 1,
handler: function (func, body) {
if (func in fontAliases) {
func = fontAliases[func];
}
return {
type: "font",
font: func.slice(1),
body: body
};
}
}
},
// Accents
{
funcs: [

View File

@ -2514,6 +2514,17 @@ var symbols = {
font: "main",
group: "accent",
replace: "\u02d9"
},
"\\imath": {
font: "main",
group: "mathord",
replace: "\u0131"
},
"\\jmath": {
font: "main",
group: "mathord",
replace: "\u0237"
}
},
"text": {

View File

@ -1160,6 +1160,115 @@ describe("A style change parser", function() {
});
});
describe("A font parser", function () {
it("should parse \\mathrm, \\mathbb, and \\mathit", function () {
expect("\\mathrm x").toParse();
expect("\\mathbb x").toParse();
expect("\\mathit x").toParse();
expect("\\mathrm {x + 1}").toParse();
expect("\\mathbb {x + 1}").toParse();
expect("\\mathit {x + 1}").toParse();
});
it("should parse \\mathcal and \\mathfrak", function () {
expect("\\mathcal{ABC123}").toParse();
expect("\\mathfrak{abcABC123}").toParse();
});
it("should produce the correct fonts", function () {
var mathbbParse = getParsed("\\mathbb x")[0];
expect(mathbbParse.value.font).toMatch("mathbb");
expect(mathbbParse.value.type).toMatch("font");
var mathrmParse = getParsed("\\mathrm x")[0];
expect(mathrmParse.value.font).toMatch("mathrm");
expect(mathrmParse.value.type).toMatch("font");
var mathitParse = getParsed("\\mathit x")[0];
expect(mathitParse.value.font).toMatch("mathit");
expect(mathitParse.value.type).toMatch("font");
var mathcalParse = getParsed("\\mathcal C")[0];
expect(mathcalParse.value.font).toMatch("mathcal");
expect(mathcalParse.value.type).toMatch("font");
var mathfrakParse = getParsed("\\mathfrak C")[0];
expect(mathfrakParse.value.font).toMatch("mathfrak");
expect(mathfrakParse.value.type).toMatch("font");
});
it("should parse nested font commands", function () {
var nestedParse = getParsed("\\mathbb{R \\neq \\mathrm{R}}")[0];
expect(nestedParse.value.font).toMatch("mathbb");
expect(nestedParse.value.type).toMatch("font");
expect(nestedParse.value.body.value.length).toMatch(3);
var bbBody = nestedParse.value.body.value;
expect(bbBody[0].type).toMatch("mathord");
expect(bbBody[1].type).toMatch("rel");
expect(bbBody[2].type).toMatch("font");
expect(bbBody[2].value.font).toMatch("mathrm");
expect(bbBody[2].value.type).toMatch("font");
});
it("should work with \\color", function () {
var colorMathbbParse = getParsed("\\color{blue}{\\mathbb R}")[0];
expect(colorMathbbParse.value.type).toMatch("color");
expect(colorMathbbParse.value.color).toMatch("blue");
var body = colorMathbbParse.value.value;
expect(body.length).toMatch(1);
expect(body[0].value.type).toMatch("font");
expect(body[0].value.font).toMatch("mathbb");
});
it("should not parse a series of font commands", function () {
expect("\\mathbb \\mathrm R").toNotParse();
});
it("should nest fonts correctly", function () {
var bf = getParsed("\\mathbf{a\\mathrm{b}c}")[0];
expect(bf.value.type).toMatch("font");
expect(bf.value.font).toMatch("mathbf");
expect(bf.value.body.value.length).toMatch(3);
expect(bf.value.body.value[0].value).toMatch("a");
expect(bf.value.body.value[1].value.type).toMatch("font");
expect(bf.value.body.value[1].value.font).toMatch("mathrm");
expect(bf.value.body.value[2].value).toMatch("c");
});
});
describe("An HTML font tree-builder", function () {
it("should render \\mathbb{R} with the correct font", function () {
var markup = katex.renderToString("\\mathbb{R}");
expect(markup).toContain("<span class=\"mord mathbb\">R</span>");
});
it("should render \\mathrm{R} with the correct font", function () {
var markup = katex.renderToString("\\mathrm{R}");
expect(markup).toContain("<span class=\"mord mathrm\">R</span>");
});
it("should render \\mathcal{R} with the correct font", function () {
var markup = katex.renderToString("\\mathcal{R}");
expect(markup).toContain("<span class=\"mord mathcal\">R</span>");
});
it("should render \\mathfrak{R} with the correct font", function () {
var markup = katex.renderToString("\\mathfrak{R}");
expect(markup).toContain("<span class=\"mord mathfrak\">R</span>");
});
it("should render a combination of font and color changes", function () {
var markup = katex.renderToString("\\color{blue}{\\mathbb R}");
var span = "<span class=\"mord mathbb\" style=\"color:blue;\">R</span>";
expect(markup).toContain(span);
markup = katex.renderToString("\\mathbb{\\color{blue}{R}}");
span = "<span class=\"mord mathbb\" style=\"color:blue;\">R</span>";
expect(markup).toContain(span);
});
});
describe("A bin builder", function() {
it("should create mbins normally", function() {
var built = getBuilt("x + y");

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -65,6 +65,16 @@ LeftRightStyleSizing: |
LimitControls: |
\displaystyle\int\limits_2^3 3x^2\,dx + \sum\nolimits^n_{i=1}i +
\textstyle\int\limits_x^y z
MathDefaultFonts: Ax2k\breve{a}\omega\Omega\imath+\KaTeX
MathBb: \mathbb{Ax2k\breve{a}\omega\Omega\imath+\KaTeX}
MathBf: \mathbf{Ax2k\breve{a}\omega\Omega\imath+\KaTeX}
MathCal: \mathcal{Ax2k\breve{a}\omega\Omega\imath+\KaTeX}
MathFrak: \mathfrak{Ax2k\breve{a}\omega\Omega\imath+\KaTeX}
MathIt: \mathit{Ax2k\breve{a}\omega\Omega\imath+\KaTeX}
MathRm: \mathrm{Ax2k\breve{a}\omega\Omega\imath+\KaTeX}
MathSf: \mathsf{Ax2k\breve{a}\omega\Omega\imath+\KaTeX}
MathScr: \mathscr{Ax2k\breve{a}\omega\Omega\imath+\KaTeX}
MathTt: \mathtt{Ax2k\breve{a}\omega\Omega\imath+\KaTeX}
NestedFractions: |
\dfrac{\frac{a}{b}}{\frac{c}{d}}\dfrac{\dfrac{a}{b}}
{\dfrac{c}{d}}\frac{\frac{a}{b}}{\frac{c}{d}}