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
This commit is contained in:
Emily Eisenberg 2014-07-10 16:06:19 -07:00
parent e46dd418b3
commit 02935f7dde
5 changed files with 118 additions and 43 deletions

View File

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

View File

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

View File

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

View File

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

View File

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