diff --git a/src/Options.js b/src/Options.js index 72ae999aa..323920312 100644 --- a/src/Options.js +++ b/src/Options.js @@ -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. diff --git a/src/buildCommon.js b/src/buildCommon.js index 62eceb3ae..36fe24a6c 100644 --- a/src/buildCommon.js +++ b/src/buildCommon.js @@ -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 }; diff --git a/src/buildHTML.js b/src/buildHTML.js index 7eb8619a0..b1b023243 100644 --- a/src/buildHTML.js +++ b/src/buildHTML.js @@ -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; diff --git a/src/buildMathML.js b/src/buildMathML.js index 736a61aec..28cd68054 100644 --- a/src/buildMathML.js +++ b/src/buildMathML.js @@ -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; diff --git a/src/functions.js b/src/functions.js index 6f6a1792b..1c2b2b2c0 100644 --- a/src/functions.js +++ b/src/functions.js @@ -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: [ diff --git a/src/symbols.js b/src/symbols.js index e5b1fd471..01e59dbaa 100644 --- a/src/symbols.js +++ b/src/symbols.js @@ -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": { diff --git a/test/katex-spec.js b/test/katex-spec.js index 52f34425d..271cf5e8c 100644 --- a/test/katex-spec.js +++ b/test/katex-spec.js @@ -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("R"); + }); + + it("should render \\mathrm{R} with the correct font", function () { + var markup = katex.renderToString("\\mathrm{R}"); + expect(markup).toContain("R"); + }); + + it("should render \\mathcal{R} with the correct font", function () { + var markup = katex.renderToString("\\mathcal{R}"); + expect(markup).toContain("R"); + }); + + it("should render \\mathfrak{R} with the correct font", function () { + var markup = katex.renderToString("\\mathfrak{R}"); + expect(markup).toContain("R"); + }); + + it("should render a combination of font and color changes", function () { + var markup = katex.renderToString("\\color{blue}{\\mathbb R}"); + var span = "R"; + expect(markup).toContain(span); + + markup = katex.renderToString("\\mathbb{\\color{blue}{R}}"); + span = "R"; + expect(markup).toContain(span); + }); +}); + describe("A bin builder", function() { it("should create mbins normally", function() { var built = getBuilt("x + y"); diff --git a/test/screenshotter/images/MathBb-firefox.png b/test/screenshotter/images/MathBb-firefox.png new file mode 100644 index 000000000..019562e10 Binary files /dev/null and b/test/screenshotter/images/MathBb-firefox.png differ diff --git a/test/screenshotter/images/MathBf-firefox.png b/test/screenshotter/images/MathBf-firefox.png new file mode 100644 index 000000000..57c7e5709 Binary files /dev/null and b/test/screenshotter/images/MathBf-firefox.png differ diff --git a/test/screenshotter/images/MathCal-firefox.png b/test/screenshotter/images/MathCal-firefox.png new file mode 100644 index 000000000..d35c399c8 Binary files /dev/null and b/test/screenshotter/images/MathCal-firefox.png differ diff --git a/test/screenshotter/images/MathDefaultFonts-firefox.png b/test/screenshotter/images/MathDefaultFonts-firefox.png new file mode 100644 index 000000000..f5267988a Binary files /dev/null and b/test/screenshotter/images/MathDefaultFonts-firefox.png differ diff --git a/test/screenshotter/images/MathFrak-firefox.png b/test/screenshotter/images/MathFrak-firefox.png new file mode 100644 index 000000000..152a7410f Binary files /dev/null and b/test/screenshotter/images/MathFrak-firefox.png differ diff --git a/test/screenshotter/images/MathIt-firefox.png b/test/screenshotter/images/MathIt-firefox.png new file mode 100644 index 000000000..9a1e7da31 Binary files /dev/null and b/test/screenshotter/images/MathIt-firefox.png differ diff --git a/test/screenshotter/images/MathRm-firefox.png b/test/screenshotter/images/MathRm-firefox.png new file mode 100644 index 000000000..48a7000a2 Binary files /dev/null and b/test/screenshotter/images/MathRm-firefox.png differ diff --git a/test/screenshotter/images/MathScr-firefox.png b/test/screenshotter/images/MathScr-firefox.png new file mode 100644 index 000000000..01b273c35 Binary files /dev/null and b/test/screenshotter/images/MathScr-firefox.png differ diff --git a/test/screenshotter/images/MathSf-firefox.png b/test/screenshotter/images/MathSf-firefox.png new file mode 100644 index 000000000..76fbff295 Binary files /dev/null and b/test/screenshotter/images/MathSf-firefox.png differ diff --git a/test/screenshotter/images/MathTt-firefox.png b/test/screenshotter/images/MathTt-firefox.png new file mode 100644 index 000000000..bbedf692f Binary files /dev/null and b/test/screenshotter/images/MathTt-firefox.png differ diff --git a/test/screenshotter/ss_data.yaml b/test/screenshotter/ss_data.yaml index 61775bb4c..48a1d6a5a 100644 --- a/test/screenshotter/ss_data.yaml +++ b/test/screenshotter/ss_data.yaml @@ -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}}