diff --git a/src/Parser.js b/src/Parser.js index 97bbbf3f..0b87bc8e 100644 --- a/src/Parser.js +++ b/src/Parser.js @@ -52,6 +52,8 @@ function Parser(input, settings) { this.gullet = new MacroExpander(input, settings.macros); // Store the settings for use in parsing this.settings = settings; + // Count leftright depth (for \middle errors) + this.leftrightDepth = 0; } var ParseNode = parseData.ParseNode; @@ -411,7 +413,9 @@ Parser.prototype.parseImplicitGroup = function() { // Parse the entire left function (including the delimiter) var left = this.parseFunction(start); // Parse out the implicit body + ++this.leftrightDepth; body = this.parseExpression(false); + --this.leftrightDepth; // Check the next token this.expect("\\right", false); var right = this.parseFunction(); diff --git a/src/buildHTML.js b/src/buildHTML.js index dccf161a..dcf71107 100644 --- a/src/buildHTML.js +++ b/src/buildHTML.js @@ -1186,11 +1186,16 @@ groupTypes.leftright = function(group, options) { var innerHeight = 0; var innerDepth = 0; + var hadMiddle = false; // Calculate its height and depth for (var i = 0; i < inner.length; i++) { - innerHeight = Math.max(inner[i].height, innerHeight); - innerDepth = Math.max(inner[i].depth, innerDepth); + if (inner[i].isMiddle) { + hadMiddle = true; + } else { + innerHeight = Math.max(inner[i].height, innerHeight); + innerDepth = Math.max(inner[i].depth, innerDepth); + } } var style = options.style; @@ -1215,6 +1220,18 @@ groupTypes.leftright = function(group, options) { // Add it to the beginning of the expression inner.unshift(leftDelim); + // Handle middle delimiters + if (hadMiddle) { + for (i = 1; i < inner.length; i++) { + if (inner[i].isMiddle) { + // Apply the options that were active when \middle was called + inner[i] = delimiter.leftRightDelim( + inner[i].isMiddle.value, innerHeight, innerDepth, + inner[i].isMiddle.options, group.mode, []); + } + } + } + var rightDelim; // Same for the right delimiter if (group.value.right === ".") { @@ -1231,6 +1248,19 @@ groupTypes.leftright = function(group, options) { ["minner", style.cls()], inner, options); }; +groupTypes.middle = function(group, options) { + var middleDelim; + if (group.value.value === ".") { + middleDelim = makeNullDelimiter(options, []); + } else { + middleDelim = delimiter.sizedDelim( + group.value.value, 1, options, + group.mode, []); + middleDelim.isMiddle = {value: group.value.value, options: options}; + } + return middleDelim; +}; + groupTypes.rule = function(group, options) { // Make an empty span for the rule var rule = makeSpan(["mord", "rule"], [], options); diff --git a/src/buildMathML.js b/src/buildMathML.js index 11a088f9..795f402c 100644 --- a/src/buildMathML.js +++ b/src/buildMathML.js @@ -285,6 +285,13 @@ groupTypes.leftright = function(group, options) { return outerNode; }; +groupTypes.middle = function(group, options) { + var middleNode = new mathMLTree.MathNode( + "mo", [makeText(group.value.middle, group.mode)]); + middleNode.setAttribute("fence", "true"); + return middleNode; +}; + groupTypes.accent = function(group, options) { var accentNode = new mathMLTree.MathNode( "mo", [makeText(group.value.accent, group.mode)]); diff --git a/src/functions.js b/src/functions.js index 6966ea7f..121bc995 100644 --- a/src/functions.js +++ b/src/functions.js @@ -496,37 +496,61 @@ defineFunction(["\\llap", "\\rlap"], { }); // Delimiter functions +var checkDelimiter = function(delim, context) { + if (utils.contains(delimiters, delim.value)) { + return delim; + } else { + throw new ParseError( + "Invalid delimiter: '" + delim.value + "' after '" + + context.funcName + "'", delim); + } +}; + defineFunction([ "\\bigl", "\\Bigl", "\\biggl", "\\Biggl", "\\bigr", "\\Bigr", "\\biggr", "\\Biggr", "\\bigm", "\\Bigm", "\\biggm", "\\Biggm", "\\big", "\\Big", "\\bigg", "\\Bigg", +], { + numArgs: 1, +}, function(context, args) { + var delim = checkDelimiter(args[0], context); + + return { + type: "delimsizing", + size: delimiterSizes[context.funcName].size, + mclass: delimiterSizes[context.funcName].mclass, + value: delim.value, + }; +}); + +defineFunction([ "\\left", "\\right", ], { numArgs: 1, }, function(context, args) { - var delim = args[0]; - if (!utils.contains(delimiters, delim.value)) { - throw new ParseError( - "Invalid delimiter: '" + delim.value + "' after '" + - context.funcName + "'", delim); - } + var delim = checkDelimiter(args[0], context); // \left and \right are caught somewhere in Parser.js, which is // why this data doesn't match what is in buildHTML. - if (context.funcName === "\\left" || context.funcName === "\\right") { - return { - type: "leftright", - value: delim.value, - }; - } else { - return { - type: "delimsizing", - size: delimiterSizes[context.funcName].size, - mclass: delimiterSizes[context.funcName].mclass, - value: delim.value, - }; + return { + type: "leftright", + value: delim.value, + }; +}); + +defineFunction("\\middle", { + numArgs: 1, +}, function(context, args) { + var delim = checkDelimiter(args[0], context); + if (!context.parser.leftrightDepth) { + throw new ParseError("\\middle without preceding \\left", delim); } + + return { + type: "middle", + value: delim.value, + }; }); // Sizing functions (handled in Parser.js explicitly, hence no handler) diff --git a/test/katex-spec.js b/test/katex-spec.js index 2018b916..21617cc6 100644 --- a/test/katex-spec.js +++ b/test/katex-spec.js @@ -1042,6 +1042,26 @@ describe("A left/right parser", function() { var normalEmpty = "\\Bigl ."; expect(normalEmpty).toParse(); }); + + it("should handle \\middle", function() { + var normalMiddle = "\\left( \\dfrac{x}{y} \\middle| \\dfrac{y}{z} \\right)"; + expect(normalMiddle).toParse(); + }); + + it("should handle multiple \\middles", function() { + var multiMiddle = "\\left( \\dfrac{x}{y} \\middle| \\dfrac{y}{z} \\middle/ \\dfrac{z}{q} \\right)"; + expect(multiMiddle).toParse(); + }); + + it("should handle nested \\middles", function() { + var nestedMiddle = "\\left( a^2 \\middle| \\left( b \\middle/ c \\right) \\right)"; + expect(nestedMiddle).toParse(); + }); + + it("should error when \\middle is not in \\left...\\right", function() { + var unmatchedMiddle = "(\\middle|\\dfrac{x}{y})"; + expect(unmatchedMiddle).toNotParse(); + }); }); describe("A begin/end parser", function() { diff --git a/test/screenshotter/images/LeftRightMiddle-chrome.png b/test/screenshotter/images/LeftRightMiddle-chrome.png new file mode 100644 index 00000000..f731f146 Binary files /dev/null and b/test/screenshotter/images/LeftRightMiddle-chrome.png differ diff --git a/test/screenshotter/images/LeftRightMiddle-firefox.png b/test/screenshotter/images/LeftRightMiddle-firefox.png new file mode 100644 index 00000000..4fe9d8c6 Binary files /dev/null and b/test/screenshotter/images/LeftRightMiddle-firefox.png differ diff --git a/test/screenshotter/ss_data.yaml b/test/screenshotter/ss_data.yaml index a37a6920..4d0991ec 100644 --- a/test/screenshotter/ss_data.yaml +++ b/test/screenshotter/ss_data.yaml @@ -72,6 +72,7 @@ Kern: Lap: ab\llap{f}cd\rlap{g}h LeftRight: \left( x^2 \right) \left\{ x^{x^{x^{x^x}}} \right. LeftRightListStyling: a+\left(x+y\right)-x +LeftRightMiddle: \left( x^2 \middle/ \right) \left\{ x^{x^{x^{x^x}}} \middle/ y \right. LeftRightStyleSizing: | +\left\{\rule{0.1em}{1em}\right. x^{+\left\{\rule{0.1em}{1em}\right.