diff --git a/src/ParseNode.js b/src/ParseNode.js index 192bbe5d..8d71cf23 100644 --- a/src/ParseNode.js +++ b/src/ParseNode.js @@ -145,6 +145,7 @@ export type ParseNodeTypes = { "size": {| type: "size", value: Measurement, + isBlank: boolean, |}, "styling": {| type: "styling", @@ -244,6 +245,7 @@ export type ParseNodeTypes = { leftDelim: ?string, rightDelim: ?string, size: StyleStr | "auto", + barSize: Measurement | null, |}, "horizBrace": {| type: "horizBrace", @@ -259,6 +261,7 @@ export type ParseNodeTypes = { "infix": {| type: "infix", replaceWith: string, + sizeNode?: ParseNode<"size">, token: ?Token, |}, "kern": {| diff --git a/src/Parser.js b/src/Parser.js index 5760ab25..6905ffd9 100644 --- a/src/Parser.js +++ b/src/Parser.js @@ -248,7 +248,13 @@ export default class Parser { denomNode = new ParseNode("ordgroup", denomBody, this.mode); } - const node = this.callFunction(funcName, [numerNode, denomNode], []); + let node; + if (funcName === "\\\\abovefrac") { + node = this.callFunction(funcName, + [numerNode, body[overIndex], denomNode], []); + } else { + node = this.callFunction(funcName, [numerNode, denomNode], []); + } return [node]; } else { return body; @@ -821,6 +827,7 @@ export default class Parser { */ parseSizeGroup(optional: boolean): ?ParsedArg { let res; + let isBlank = false; if (!optional && this.nextToken.text !== "{") { res = this.parseRegexGroup( /^[-+]? *(?:$|\d+|\d+\.\d*|\.\d*) *[a-z]{0,2} *$/, "size"); @@ -830,6 +837,13 @@ export default class Parser { if (!res) { return null; } + if (!optional && res.text.length === 0) { + // Because we've tested for what is !optional, this block won't + // affect \kern, \hspace, etc. It will capture the mandatory arguments + // to \genfrac and \above. + res.text = "0pt"; // Enable \above{} + isBlank = true; // This is here specifically for \genfrac + } const match = (/([-+]?) *(\d+(?:\.\d*)?|\.\d+) *([a-z]{2})/).exec(res.text); if (!match) { throw new ParseError("Invalid size: '" + res.text + "'", res); @@ -844,6 +858,7 @@ export default class Parser { return newArgument(new ParseNode("size", { type: "size", value: data, + isBlank: isBlank, }, this.mode), res); } diff --git a/src/functions/genfrac.js b/src/functions/genfrac.js index e757bea7..d58f4e3b 100644 --- a/src/functions/genfrac.js +++ b/src/functions/genfrac.js @@ -4,10 +4,11 @@ import buildCommon from "../buildCommon"; import delimiter from "../delimiter"; import mathMLTree from "../mathMLTree"; import Style from "../Style"; -import ParseNode from "../ParseNode"; +import ParseNode, {assertNodeType, checkNodeType} from "../ParseNode"; import * as html from "../buildHTML"; import * as mml from "../buildMathML"; +import {calculateSize} from "../units"; const htmlBuilder = (group, options) => { // Fractions are handled in the TeXbook on pages 444-445, rules 15(a-e). @@ -20,6 +21,10 @@ const htmlBuilder = (group, options) => { style.size === Style.DISPLAY.size) { // We're in a \tfrac but incoming style is displaystyle, so: style = Style.TEXT; + } else if (group.value.size === "script") { + style = Style.SCRIPT; + } else if (group.value.size === "scriptscript") { + style = Style.SCRIPTSCRIPT; } const nstyle = style.fracNum(); @@ -45,7 +50,12 @@ const htmlBuilder = (group, options) => { let ruleWidth; let ruleSpacing; if (group.value.hasBarLine) { - rule = buildCommon.makeLineSpan("frac-line", options); + if (group.value.barSize) { + ruleWidth = calculateSize(group.value.barSize, options); + rule = buildCommon.makeLineSpan("frac-line", options, ruleWidth); + } else { + rule = buildCommon.makeLineSpan("frac-line", options); + } ruleWidth = rule.height; ruleSpacing = rule.height; } else { @@ -174,6 +184,9 @@ const mathmlBuilder = (group, options) => { if (!group.value.hasBarLine) { node.setAttribute("linethickness", "0px"); + } else if (group.value.barSize) { + const ruleWidth = calculateSize(group.value.barSize, options); + node.setAttribute("linethickness", ruleWidth + "em"); } if (group.value.leftDelim != null || group.value.rightDelim != null) { @@ -277,6 +290,7 @@ defineFunction({ leftDelim: leftDelim, rightDelim: rightDelim, size: size, + barSize: null, }, parser.mode); }, @@ -321,3 +335,137 @@ defineFunction({ }, parser.mode); }, }); + +const stylArray = ["display", "text", "script", "scriptscript"]; + +const delimFromValue = function(delimString: string): string | null { + let delim = null; + if (delimString.length > 0) { + delim = delimString; + delim = delim === "." ? null : delim; + } + return delim; +}; + +defineFunction({ + type: "genfrac", + names: ["\\genfrac"], + props: { + numArgs: 6, + greediness: 6, + argTypes: ["math", "math", "size", "text", "math", "math"], + }, + handler: ({parser}, args) => { + const numer = args[4]; + const denom = args[5]; + + // Look into the parse nodes to get the desired delimiters. + let leftNode = checkNodeType(args[0], "ordgroup"); + if (leftNode) { + leftNode = assertNodeType(leftNode.value[0], "open"); + } else { + leftNode = assertNodeType(args[0], "open"); + } + const leftDelim = delimFromValue(leftNode.value); + + let rightNode = checkNodeType(args[1], "ordgroup"); + if (rightNode) { + rightNode = assertNodeType(rightNode.value[0], "close"); + } else { + rightNode = assertNodeType(args[1], "close"); + } + const rightDelim = delimFromValue(rightNode.value); + + const barNode = assertNodeType(args[2], "size"); + let hasBarLine; + let barSize = null; + if (barNode.value.isBlank) { + // \genfrac acts differently than \above. + // \genfrac treats an empty size group as a signal to use a + // standard bar size. \above would see size = 0 and omit the bar. + hasBarLine = true; + } else { + barSize = barNode.value.value; + hasBarLine = barSize.number > 0; + } + + // Find out if we want displaystyle, textstyle, etc. + let size = "auto"; + let styl = checkNodeType(args[3], "ordgroup"); + if (styl) { + if (styl.value.length > 0) { + size = stylArray[Number(styl.value[0].value)]; + } + } else { + styl = assertNodeType(args[3], "textord"); + size = stylArray[Number(styl.value)]; + } + + return new ParseNode("genfrac", { + type: "genfrac", + numer: numer, + denom: denom, + continued: false, + hasBarLine: hasBarLine, + barSize: barSize, + leftDelim: leftDelim, + rightDelim: rightDelim, + size: size, + }, parser.mode); + }, + + htmlBuilder, + mathmlBuilder, +}); + +// \above is an infix fraction that also defines a fraction bar size. +defineFunction({ + type: "infix", + names: ["\\above"], + props: { + numArgs: 1, + argTypes: ["size"], + infix: true, + }, + handler({parser, funcName, token}, args) { + const sizeNode = assertNodeType(args[0], "size"); + return new ParseNode("infix", { + type: "infix", + replaceWith: "\\\\abovefrac", + sizeNode: sizeNode, + token: token, + }, parser.mode); + }, +}); + +defineFunction({ + type: "genfrac", + names: ["\\\\abovefrac"], + props: { + numArgs: 3, + argTypes: ["math", "size", "math"], + }, + handler: ({parser, funcName}, args) => { + const numer = args[0]; + const infixNode = assertNodeType(args[1], "infix"); + const sizeNode = assertNodeType(infixNode.value.sizeNode, "size"); + const denom = args[2]; + + const barSize = sizeNode.value.value; + const hasBarLine = barSize.number > 0; + return new ParseNode("genfrac", { + type: "genfrac", + numer: numer, + denom: denom, + continued: false, + hasBarLine: hasBarLine, + barSize: barSize, + leftDelim: null, + rightDelim: null, + size: "auto", + }, parser.mode); + }, + + htmlBuilder, + mathmlBuilder, +}); diff --git a/test/katex-spec.js b/test/katex-spec.js index 0db175bb..ff2f287f 100644 --- a/test/katex-spec.js +++ b/test/katex-spec.js @@ -428,6 +428,8 @@ describe("A frac parser", function() { const dfracExpression = "\\dfrac{x}{y}"; const tfracExpression = "\\tfrac{x}{y}"; const cfracExpression = "\\cfrac{x}{y}"; + const genfrac1 = "\\genfrac ( ] {0.06em}{0}{a}{b+c}"; + const genfrac2 = "\\genfrac ( ] {0.8pt}{}{a}{b+c}"; it("should not fail", function() { expect(expression).toParse(); @@ -441,13 +443,15 @@ describe("A frac parser", function() { expect(parse.value.denom).toBeDefined(); }); - it("should also parse dfrac and tfrac", function() { + it("should also parse cfrac, dfrac, tfrac, and genfrac", function() { + expect(cfracExpression).toParse(); expect(dfracExpression).toParse(); - expect(tfracExpression).toParse(); + expect(genfrac1).toParse(); + expect(genfrac2).toParse(); }); - it("should parse dfrac and tfrac as fracs", function() { + it("should parse cfrac, dfrac, tfrac, and genfrac as fracs", function() { const dfracParse = getParsed(dfracExpression)[0]; expect(dfracParse.type).toEqual("genfrac"); @@ -465,6 +469,24 @@ describe("A frac parser", function() { expect(cfracParse.type).toEqual("genfrac"); expect(cfracParse.value.numer).toBeDefined(); expect(cfracParse.value.denom).toBeDefined(); + + const genfracParse = getParsed(genfrac1)[0]; + + expect(genfracParse.type).toEqual("genfrac"); + expect(genfracParse.value.numer).toBeDefined(); + expect(genfracParse.value.denom).toBeDefined(); + expect(genfracParse.value.leftDelim).toBeDefined(); + expect(genfracParse.value.rightDelim).toBeDefined(); + }); + + it("should fail, given math as a line thickness to genfrac", function() { + const badGenFrac = "\\genfrac ( ] {b+c}{0}{a}{b+c}"; + expect(badGenFrac).toNotParse(); + }); + + it("should fail if genfrac is given less than 6 arguments", function() { + const badGenFrac = "\\genfrac ( ] {0.06em}{0}{a}"; + expect(badGenFrac).toNotParse(); }); it("should parse atop", function() { @@ -587,6 +609,17 @@ describe("An over/brace/brack parser", function() { }); }); +describe("A genfrac builder", function() { + it("should not fail", function() { + expect("\\frac{x}{y}").toBuild(); + expect("\\dfrac{x}{y}").toBuild(); + expect("\\tfrac{x}{y}").toBuild(); + expect("\\cfrac{x}{y}").toBuild(); + expect("\\genfrac ( ] {0.06em}{0}{a}{b+c}").toBuild(); + expect("\\genfrac ( ] {0.8pt}{}{a}{b+c}").toBuild(); + }); +}); + describe("A infix builder", function() { it("should not fail", function() { expect("a \\over b").toBuild(); diff --git a/test/screenshotter/images/FractionTest-chrome.png b/test/screenshotter/images/FractionTest-chrome.png index 4c7a5e28..3b6578bb 100644 Binary files a/test/screenshotter/images/FractionTest-chrome.png and b/test/screenshotter/images/FractionTest-chrome.png differ diff --git a/test/screenshotter/images/FractionTest-firefox.png b/test/screenshotter/images/FractionTest-firefox.png index 9f4c0c22..b91b92c9 100644 Binary files a/test/screenshotter/images/FractionTest-firefox.png and b/test/screenshotter/images/FractionTest-firefox.png differ diff --git a/test/screenshotter/ss_data.yaml b/test/screenshotter/ss_data.yaml index 3e6ce889..c5ebb093 100644 --- a/test/screenshotter/ss_data.yaml +++ b/test/screenshotter/ss_data.yaml @@ -116,7 +116,8 @@ ExtensibleArrows: | FractionTest: | \begin{array}{l} \dfrac{a}{b}\frac{a}{b}\tfrac{a}{b}\;-\dfrac12\;1\tfrac12\;{1 \atop 2}\; \cfrac{1}{1+\cfrac{1}{x}} \\[2.5em] - {a \brace b} \; {a \brack b} + {a \brace b} \; {a \brack b} \; \genfrac \{ ]{0.8pt}{0}{a}{b} + \; {a \above1.0pt b} \end{array} Functions: \sin\cos\tan\ln\log Gathered: |