From 02935f7dde07efcf44179e4349304e06e5607378 Mon Sep 17 00:00:00 2001 From: Emily Eisenberg Date: Thu, 10 Jul 2014 16:06:19 -0700 Subject: [PATCH] Add an 'implicit group' parser, use with sizing Summary: Implicit groups are objects that act like groups but don't have brackets around them. This is used for things like sizing functions or font-change functions that can occur in the middle of the group, but act like they apply to a group after them which stops when the current group stops. E.g. `Hello {world \Large hello} world` produces normal, normal, Large, normal text. (Note, I just came up with the name implicit group, I don't think this is actually how it is parsed in LaTeX but it fits nicely with the current parsing scheme and seems to work well). For now, apply this to the sizing functions (we don't have any other functions that act like this). Also note that this doesn't really do much practically because we limit sizing functions to only be on the top level of the expression, but it does let people do `\Large x` and get a large `x`, without having to add braces. Test Plan: - Run the tests, see they work - Make sure `abc \Large abc` looks correct Reviewers: alpert Reviewed By: alpert Differential Revision: http://phabricator.khanacademy.org/D10876 --- Options.js | 27 ++++++++++++++++----------- Parser.js | 35 +++++++++++++++++++++-------------- buildTree.js | 16 ++++++++-------- static/katex.less | 42 ++++++++++++++++++++++++++++++++---------- test/katex-tests.js | 41 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 118 insertions(+), 43 deletions(-) diff --git a/Options.js b/Options.js index 5e741df0c..8d2a8d032 100644 --- a/Options.js +++ b/Options.js @@ -1,42 +1,47 @@ -function Options(style, size, color, depth, parentStyle, parentSize) { +function Options(style, size, color, deep, parentStyle, parentSize) { this.style = style; this.color = color; this.size = size; - // TODO(emily): Get rid of depth when we can actually use sizing everywhere - if (!depth) { - depth = 0; + // TODO(emily): Get rid of deep when we can actually use sizing everywhere + if (deep === undefined) { + deep = false; } - this.depth = depth; + this.deep = deep; - if (!parentStyle) { + if (parentStyle === undefined) { parentStyle = style; } this.parentStyle = parentStyle; - if (!parentSize) { + if (parentSize === undefined) { parentSize = size; } this.parentSize = parentSize; } Options.prototype.withStyle = function(style) { - return new Options(style, this.size, this.color, this.depth + 1, + return new Options(style, this.size, this.color, this.deep, this.style, this.size); }; Options.prototype.withSize = function(size) { - return new Options(this.style, size, this.color, this.depth + 1, + return new Options(this.style, size, this.color, this.deep, this.style, this.size); }; Options.prototype.withColor = function(color) { - return new Options(this.style, this.size, color, this.depth + 1, + return new Options(this.style, this.size, color, this.deep, this.style, this.size); }; +Options.prototype.deepen = function() { + return new Options(this.style, this.size, this.color, true, + this.parentStyle, this.parentSize); +}; + Options.prototype.reset = function() { - return new Options(this.style, this.size, this.color, this.depth + 1, + return new Options(this.style, this.size, this.color, this.deep, this.style, this.size); }; diff --git a/Parser.js b/Parser.js index 3329ccae5..740ed4f2f 100644 --- a/Parser.js +++ b/Parser.js @@ -201,6 +201,20 @@ Parser.prototype.parseGroup = function(pos, mode) { } }; +// Parses an implicit group, which is a group that starts where you want it, and +// ends right before a higher explicit group ends, or at EOL. It is used for +// functions that appear to affect the current style, like \Large or \textrm, +// where instead of keeping a style we just pretend that there is an implicit +// grouping after it until the end of the group. +Parser.prototype.parseImplicitGroup = function(pos, mode) { + // Since parseExpression already ends where we want it to, we just need to + // call that and it does what we want. + var expression = this.parseExpression(pos, mode); + return new ParseResult( + new ParseNode("ordgroup", expression.result, mode), + expression.position); +}; + // Parses a custom color group, which looks like "{#ffffff}" Parser.prototype.parseColorGroup = function(pos, mode) { var start = this.lexer.lex(pos, mode); @@ -322,20 +336,13 @@ Parser.prototype.parseNucleus = function(pos, mode) { } } 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); - if (group) { - return new ParseResult( - new ParseNode("sizing", { - size: "size" + (utils.indexOf(sizeFuncs, nucleus.type) + 1), - value: group.result - }, mode), - group.position); - } else { - throw new ParseError( - "Expected group after '" + nucleus.text + "'", - this.lexer, nucleus.position - ); - } + var group = this.parseImplicitGroup(nucleus.position, mode); + return new ParseResult( + new ParseNode("sizing", { + size: "size" + (utils.indexOf(sizeFuncs, nucleus.type) + 1), + value: group.result + }, mode), + group.position); } else if (mode === "math" && utils.contains(namedFns, nucleus.type)) { // If this is a named function, just return it plain return new ParseResult( diff --git a/buildTree.js b/buildTree.js index d98623f81..f8fe16238 100644 --- a/buildTree.js +++ b/buildTree.js @@ -139,7 +139,7 @@ var groupTypes = { text: function(group, options, prev) { return makeSpan(["text mord", options.style.cls()], - [buildGroup(group.value, options.reset())] + [buildGroup(group.value, options.deepen())] ); }, @@ -148,7 +148,7 @@ var groupTypes = { if (group.value.sup) { var sup = buildGroup(group.value.sup, - options.withStyle(options.style.sup())); + options.withStyle(options.style.sup()).deepen()); var supmid = makeSpan( [options.style.reset(), options.style.sup().cls()], [sup]); var supwrap = makeSpan(["msup", options.style.reset()], [supmid]); @@ -156,7 +156,7 @@ var groupTypes = { if (group.value.sub) { var sub = buildGroup(group.value.sub, - options.withStyle(options.style.sub())); + options.withStyle(options.style.sub()).deepen()); var submid = makeSpan( [options.style.reset(), options.style.sub().cls()], [sub]); var subwrap = makeSpan(["msub"], [submid]); @@ -260,13 +260,13 @@ var groupTypes = { var nstyle = fstyle.fracNum(); var dstyle = fstyle.fracDen(); - var numer = buildGroup(group.value.numer, options.withStyle(nstyle)); + var numer = buildGroup(group.value.numer, options.withStyle(nstyle).deepen()); var numernumer = makeSpan([fstyle.reset(), nstyle.cls()], [numer]); var numerrow = makeSpan(["mfracnum"], [numernumer]); var mid = makeSpan(["mfracmid"], [makeSpan()]); - var denom = buildGroup(group.value.denom, options.withStyle(dstyle)); + var denom = buildGroup(group.value.denom, options.withStyle(dstyle).deepen()); var denomdenom = makeSpan([fstyle.reset(), dstyle.cls()], [denom]) var denomrow = makeSpan(["mfracden"], [denomdenom]); @@ -363,14 +363,14 @@ var groupTypes = { llap: function(group, options, prev) { var inner = makeSpan( - ["inner"], [buildGroup(group.value, options.reset())]); + ["inner"], [buildGroup(group.value, options.deepen())]); var fix = makeSpan(["fix"], []); return makeSpan(["llap", options.style.cls()], [inner, fix]); }, rlap: function(group, options, prev) { var inner = makeSpan( - ["inner"], [buildGroup(group.value, options.reset())]); + ["inner"], [buildGroup(group.value, options.deepen())]); var fix = makeSpan(["fix"], []); return makeSpan(["rlap", options.style.cls()], [inner, fix]); }, @@ -461,7 +461,7 @@ var buildGroup = function(group, options, prev) { var multiplier = sizingMultiplier[options.size] / sizingMultiplier[options.parentSize]; - if (options.depth > 1) { + if (options.deep) { throw new ParseError( "Can't use sizing outside of the root node"); } diff --git a/static/katex.less b/static/katex.less index a1144c290..c274eb5cc 100644 --- a/static/katex.less +++ b/static/katex.less @@ -310,14 +310,36 @@ big parens .sizing { display: inline-block; } - .reset-size5.size1 { font-size: 0.5em; } - .reset-size5.size2 { font-size: 0.7em; } - .reset-size5.size3 { font-size: 0.8em; } - .reset-size5.size4 { font-size: 0.9em; } - .reset-size5.size5 { font-size: 1.0em; } - .reset-size5.size6 { font-size: 1.2em; } - .reset-size5.size7 { font-size: 1.44em; } - .reset-size5.size8 { font-size: 1.73em; } - .reset-size5.size9 { font-size: 2.07em; } - .reset-size5.size10 { font-size: 2.49em; } + @size-1: 0.5; + @size-2: 0.7; + @size-3: 0.8; + @size-4: 0.9; + @size-5: 1.0; + @size-6: 1.2; + @size-7: 1.44; + @size-8: 1.73; + @size-9: 2.07; + @size-10: 2.49; + + .generate-size-change(@from, @to) { + .reset-size@{from}.size@{to} { + @sizeFromVariable: ~"size-@{from}"; + @sizeToVariable: ~"size-@{to}"; + font-size: (@@sizeToVariable / @@sizeFromVariable) * 1em; + } + } + + .generate-to-size-change(@from, @currTo) when (@currTo =< 10) { + .generate-size-change(@from, @currTo); + + .generate-to-size-change(@from, (@currTo + 1)); + } + + .generate-from-size-change(@currFrom) when (@currFrom =< 10) { + .generate-to-size-change(@currFrom, 1); + + .generate-from-size-change((@currFrom + 1)); + } + + .generate-from-size-change(1); } diff --git a/test/katex-tests.js b/test/katex-tests.js index 6c2759c39..c9e2540f4 100644 --- a/test/katex-tests.js +++ b/test/katex-tests.js @@ -326,6 +326,47 @@ describe("A group parser", function() { }); }); +describe("An implicit group parser", function() { + it("should not fail", function() { + expect(function() { + parseTree("\\Large x"); + parseTree("abc {abc \Large xyz} abc"); + }).not.toThrow(); + }); + + it("should produce a single object", function() { + var parse = parseTree("\\Large abc"); + + expect(parse.length).toBe(1); + + var sizing = parse[0]; + + expect(sizing.type).toMatch("sizing"); + expect(sizing.value).toBeTruthy(); + }); + + it("should apply only after the function", function() { + var parse = parseTree("a \\Large abc"); + + expect(parse.length).toBe(2); + + var sizing = parse[1]; + + expect(sizing.type).toMatch("sizing"); + expect(sizing.value.value.value.length).toBe(3); + }); + + it("should stop at the ends of groups", function() { + var parse = parseTree("a { b \\Large c } d"); + + var group = parse[1]; + var sizing = group.value[1]; + + expect(sizing.type).toMatch("sizing"); + expect(sizing.value.value.value.length).toBe(1); + }); +}); + describe("A function parser", function() { it("should parse no argument functions", function() { expect(function() {