Repository: Khan/KaTeX
Pull request #1116 (2018-01-30 03:09:16)

extract sizing and styling implicit functions
Getting closer to being able to remove `parseImplicitGroup`.


Patch:
diff --git a/src/Parser.js b/src/Parser.js
index 045c2bfe..dc7004f2 100644
--- a/src/Parser.js
+++ b/src/Parser.js
@@ -5,7 +5,6 @@ import functions from "./functions";
 import environments from "./environments";
 import MacroExpander from "./MacroExpander";
 import symbols from "./symbols";
-import utils from "./utils";
 import { validUnit } from "./units";
 import { supportedCodepoint } from "./unicodeScripts";
 import unicodeAccents from "./unicodeAccents";
@@ -16,7 +15,7 @@ import { combiningDiacriticalMarksEndRegex } from "./Lexer.js";
 import Settings from "./Settings";
 import { Token } from "./Token";
 
-import type { Mode, ArgType } from "./types";
+import type { Mode, ArgType, BreakToken } from "./types";
 import type { FunctionContext, FunctionSpec } from "./defineFunction" ;
 import type { EnvSpec } from "./defineEnvironment";
 
@@ -184,7 +183,7 @@ export default class Parser {
      */
     parseExpression(
         breakOnInfix: boolean,
-        breakOnTokenText?: "]" | "}" | "$",
+        breakOnTokenText?: BreakToken,
     ): ParseNode[] {
         const body = [];
         // Keep adding atoms to the body until we can't parse any more atoms (either
@@ -348,7 +347,7 @@ export default class Parser {
     /**
      * Parses a group with optional super/subscripts.
      */
-    parseAtom(breakOnTokenText?: "]" | "}" | "$"): ?ParseNode {
+    parseAtom(breakOnTokenText?: BreakToken): ?ParseNode {
         // The body of an atom is an implicit group, so that things like
         // \left(x\right)^2 work correctly.
         const base = this.parseImplicitGroup(breakOnTokenText);
@@ -435,17 +434,6 @@ export default class Parser {
         }
     }
 
-    // A list of the size-changing functions, for use in parseImplicitGroup
-    static sizeFuncs = [
-        "\\tiny", "\\sixptsize", "\\scriptsize", "\\footnotesize", "\\small",
-        "\\normalsize", "\\large", "\\Large", "\\LARGE", "\\huge", "\\Huge",
-    ];
-
-    // A list of the style-changing functions, for use in parseImplicitGroup
-    static styleFuncs = [
-        "\\displaystyle", "\\textstyle", "\\scriptstyle", "\\scriptscriptstyle",
-    ];
-
     // Old font functions
     static oldFontFuncs = {
         "\\rm": "mathrm",
@@ -465,7 +453,7 @@ export default class Parser {
      * implicit grouping after it until the end of the group. E.g.
      *   small text {\Large large text} small text again
      */
-    parseImplicitGroup(breakOnTokenText?: "]" | "}" | "$"): ?ParseNode {
+    parseImplicitGroup(breakOnTokenText?: BreakToken): ?ParseNode {
         const start = this.parseSymbol();
 
         if (start == null) {
@@ -527,25 +515,6 @@ export default class Parser {
                     endNameToken);
             }
             return result;
-        } else if (utils.contains(Parser.sizeFuncs, func)) {
-            // If we see a sizing function, parse out the implicit body
-            this.consumeSpaces();
-            const body = this.parseExpression(false, breakOnTokenText);
-            return new ParseNode("sizing", {
-                // Figure out what size to use based on the list of functions above
-                size: utils.indexOf(Parser.sizeFuncs, func) + 1,
-                value: body,
-            }, this.mode);
-        } else if (utils.contains(Parser.styleFuncs, func)) {
-            // If we see a styling function, parse out the implicit body
-            this.consumeSpaces();
-            const body = this.parseExpression(true, breakOnTokenText);
-            return new ParseNode("styling", {
-                // Figure out what style to use by pulling out the style from
-                // the function name
-                style: func.slice(1, func.length - 5),
-                value: body,
-            }, this.mode);
         } else if (func in Parser.oldFontFuncs) {
             const style = Parser.oldFontFuncs[func];
             // If we see an old font function, parse out the implicit body
@@ -576,7 +545,7 @@ export default class Parser {
             }, this.mode);
         } else {
             // Defer to parseGivenFunction if it's not a function we handle
-            return this.parseGivenFunction(start);
+            return this.parseGivenFunction(start, breakOnTokenText);
         }
     }
 
@@ -593,7 +562,10 @@ export default class Parser {
      * Same as parseFunction(), except that the base is provided, guaranteeing a
      * non-nullable result.
      */
-    parseGivenFunction(baseGroup: ParsedFuncOrArgOrDollar): ParseNode {
+    parseGivenFunction(
+        baseGroup: ParsedFuncOrArgOrDollar,
+        breakOnTokenText?: BreakToken,
+    ): ParseNode {
         baseGroup = assertFuncOrArg(baseGroup);
         if (baseGroup.type === "fn") {
             const func = baseGroup.result;
@@ -611,7 +583,8 @@ export default class Parser {
 
             const {args, optArgs} = this.parseArguments(func, funcData);
             const token = baseGroup.token;
-            const result = this.callFunction(func, args, optArgs, token);
+            const result =
+                this.callFunction(func, args, optArgs, token, breakOnTokenText);
             return new ParseNode(result.type, result, this.mode);
         } else {
             return baseGroup.result;
@@ -626,11 +599,13 @@ export default class Parser {
         args: ParseNode[],
         optArgs: (?ParseNode)[],
         token?: Token,
+        breakOnTokenText?: BreakToken,
     ): * {
         const context: FunctionContext = {
             funcName: name,
             parser: this,
             token,
+            breakOnTokenText,
         };
         const func = functions[name];
         if (func && func.handler) {
diff --git a/src/buildHTML.js b/src/buildHTML.js
index 9779436f..092b8c55 100644
--- a/src/buildHTML.js
+++ b/src/buildHTML.js
@@ -422,46 +422,6 @@ groupTypes.spacing = function(group, options) {
     }
 };
 
-function sizingGroup(value, options, baseOptions) {
-    const inner = buildExpression(value, options, false);
-    const multiplier = options.sizeMultiplier / baseOptions.sizeMultiplier;
-
-    // Add size-resetting classes to the inner list and set maxFontSize
-    // manually. Handle nested size changes.
-    for (let i = 0; i < inner.length; i++) {
-        const pos = utils.indexOf(inner[i].classes, "sizing");
-        if (pos < 0) {
-            Array.prototype.push.apply(inner[i].classes,
-                options.sizingClasses(baseOptions));
-        } else if (inner[i].classes[pos + 1] === "reset-size" + options.size) {
-            // This is a nested size change: e.g., inner[i] is the "b" in
-            // `\Huge a \small b`. Override the old size (the `reset-` class)
-            // but not the new size.
-            inner[i].classes[pos + 1] = "reset-size" + baseOptions.size;
-        }
-
-        inner[i].height *= multiplier;
-        inner[i].depth *= multiplier;
-    }
-
-    return buildCommon.makeFragment(inner);
-}
-
-groupTypes.sizing = function(group, options) {
-    // Handle sizing operators like \Huge. Real TeX doesn't actually allow
-    // these functions inside of math expressions, so we do some special
-    // handling.
-    const newOptions = options.havingSize(group.value.size);
-    return sizingGroup(group.value.value, newOptions, options);
-};
-
-groupTypes.styling = function(group, options) {
-    // Style changes are handled in the TeXbook on pg. 442, Rule 3.
-    const newStyle = styleMap[group.value.style];
-    const newOptions = options.havingStyle(newStyle);
-    return sizingGroup(group.value.value, newOptions, options);
-};
-
 groupTypes.font = function(group, options) {
     const font = group.value.font;
     return buildGroup(group.value.body, options.withFontFamily(font));
diff --git a/src/buildMathML.js b/src/buildMathML.js
index 4137a7c9..07b44cfd 100644
--- a/src/buildMathML.js
+++ b/src/buildMathML.js
@@ -247,55 +247,6 @@ groupTypes.font = function(group, options) {
     return buildGroup(group.value.body, options.withFontFamily(font));
 };
 
-groupTypes.styling = function(group, options) {
-    // Figure out what style we're changing to.
-    // TODO(kevinb): dedupe this with buildHTML.js
-    // This will be easier of handling of styling nodes is in the same file.
-    const styleMap = {
-        "display": Style.DISPLAY,
-        "text": Style.TEXT,
-        "script": Style.SCRIPT,
-        "scriptscript": Style.SCRIPTSCRIPT,
-    };
-
-    const newStyle = styleMap[group.value.style];
-    const newOptions = options.havingStyle(newStyle);
-
-    const inner = buildExpression(group.value.value, newOptions);
-
-    const node = new mathMLTree.MathNode("mstyle", inner);
-
-    const styleAttributes = {
-        "display": ["0", "true"],
-        "text": ["0", "false"],
-        "script": ["1", "false"],
-        "scriptscript": ["2", "false"],
-    };
-
-    const attr = styleAttributes[group.value.style];
-
-    node.setAttribute("scriptlevel", attr[0]);
-    node.setAttribute("displaystyle", attr[1]);
-
-    return node;
-};
-
-groupTypes.sizing = function(group, options) {
-    const newOptions = options.havingSize(group.value.size);
-    const inner = buildExpression(group.value.value, newOptions);
-
-    const node = new mathMLTree.MathNode("mstyle", inner);
-
-    // TODO(emily): This doesn't produce the correct size for nested size
-    // changes, because we don't keep state of what style we're currently
-    // in, so we can't reset the size to normal before changing it.  Now
-    // that we're passing an options parameter we should be able to fix
-    // this.
-    node.setAttribute("mathsize", newOptions.sizeMultiplier + "em");
-
-    return node;
-};
-
 groupTypes.horizBrace = function(group, options) {
     const accentNode = stretchy.mathMLnode(group.value.label);
     return new mathMLTree.MathNode(
diff --git a/src/defineFunction.js b/src/defineFunction.js
index ac5dc9b1..119ce804 100644
--- a/src/defineFunction.js
+++ b/src/defineFunction.js
@@ -5,7 +5,7 @@ import {groupTypes as mathmlGroupTypes} from "./buildMathML";
 import type Parser from "./Parser" ;
 import type ParseNode from "./ParseNode" ;
 import type Options from "./Options";
-import type {ArgType} from "./types" ;
+import type {ArgType, BreakToken} from "./types" ;
 import type {Token} from "./Token" ;
 
 /** Context provided to function handlers for error messages. */
@@ -13,6 +13,7 @@ export type FunctionContext = {|
     funcName: string,
     parser: Parser,
     token?: Token,
+    breakOnTokenText?: BreakToken,
 |};
 
 // TODO: Enumerate all allowed output types.
diff --git a/src/functions.js b/src/functions.js
index 9b56ede1..33023b38 100644
--- a/src/functions.js
+++ b/src/functions.js
@@ -179,18 +179,9 @@ import "./functions/smash";
 
 import "./functions/delimsizing";
 
-// Sizing functions (handled in Parser.js explicitly, hence no handler)
-defineFunction([
-    "\\tiny", "\\scriptsize", "\\footnotesize", "\\small",
-    "\\normalsize", "\\large", "\\Large", "\\LARGE", "\\huge", "\\Huge",
-], {numArgs: 0}, null);
+import "./functions/sizing";
 
-// Style changing functions (handled in Parser.js explicitly, hence no
-// handler)
-defineFunction([
-    "\\displaystyle", "\\textstyle", "\\scriptstyle",
-    "\\scriptscriptstyle",
-], {numArgs: 0}, null);
+import "./functions/styling";
 
 // Old font changing functions
 defineFunction([
diff --git a/src/functions/sizing.js b/src/functions/sizing.js
new file mode 100644
index 00000000..e1b5531e
--- /dev/null
+++ b/src/functions/sizing.js
@@ -0,0 +1,84 @@
+// @flow
+import buildCommon from "../buildCommon";
+import defineFunction from "../defineFunction";
+import mathMLTree from "../mathMLTree";
+import utils from "../utils";
+
+import * as html from "../buildHTML";
+import * as mml from "../buildMathML";
+
+import type Options from "../Options";
+
+export function sizingGroup(value: *, options: Options, baseOptions: Options) {
+    const inner = html.buildExpression(value, options, false);
+    const multiplier = options.sizeMultiplier / baseOptions.sizeMultiplier;
+
+    // Add size-resetting classes to the inner list and set maxFontSize
+    // manually. Handle nested size changes.
+    for (let i = 0; i < inner.length; i++) {
+        const pos = utils.indexOf(inner[i].classes, "sizing");
+        if (pos < 0) {
+            Array.prototype.push.apply(inner[i].classes,
+                options.sizingClasses(baseOptions));
+        } else if (inner[i].classes[pos + 1] === "reset-size" + options.size) {
+            // This is a nested size change: e.g., inner[i] is the "b" in
+            // `\Huge a \small b`. Override the old size (the `reset-` class)
+            // but not the new size.
+            inner[i].classes[pos + 1] = "reset-size" + baseOptions.size;
+        }
+
+        inner[i].height *= multiplier;
+        inner[i].depth *= multiplier;
+    }
+
+    return buildCommon.makeFragment(inner);
+}
+
+const sizeFuncs = [
+    "\\tiny", "\\sixptsize", "\\scriptsize", "\\footnotesize", "\\small",
+    "\\normalsize", "\\large", "\\Large", "\\LARGE", "\\huge", "\\Huge",
+];
+
+defineFunction({
+    type: "sizing",
+    names: sizeFuncs,
+    props: {
+        numArgs: 0,
+        allowedInText: true,
+    },
+    handler: (context, args) => {
+        const {breakOnTokenText, funcName, parser} = context;
+
+        parser.consumeSpaces();
+        const body = parser.parseExpression(false, breakOnTokenText);
+
+        return {
+            type: "sizing",
+            // Figure out what size to use based on the list of functions above
+            size: utils.indexOf(sizeFuncs, funcName) + 1,
+            value: body,
+        };
+    },
+    htmlBuilder: (group, options) => {
+        // Handle sizing operators like \Huge. Real TeX doesn't actually allow
+        // these functions inside of math expressions, so we do some special
+        // handling.
+        const newOptions = options.havingSize(group.value.size);
+        return sizingGroup(group.value.value, newOptions, options);
+    },
+    mathmlBuilder: (group, options) => {
+        const newOptions = options.havingSize(group.value.size);
+        const inner = mml.buildExpression(group.value.value, newOptions);
+
+        const node = new mathMLTree.MathNode("mstyle", inner);
+
+        // TODO(emily): This doesn't produce the correct size for nested size
+        // changes, because we don't keep state of what style we're currently
+        // in, so we can't reset the size to normal before changing it.  Now
+        // that we're passing an options parameter we should be able to fix
+        // this.
+        node.setAttribute("mathsize", newOptions.sizeMultiplier + "em");
+
+        return node;
+    },
+});
diff --git a/src/functions/styling.js b/src/functions/styling.js
new file mode 100644
index 00000000..69241406
--- /dev/null
+++ b/src/functions/styling.js
@@ -0,0 +1,79 @@
+// @flow
+import defineFunction from "../defineFunction";
+import mathMLTree from "../mathMLTree";
+import Style from "../Style";
+import {sizingGroup} from "./sizing";
+
+import * as mml from "../buildMathML";
+
+const styleMap = {
+    "display": Style.DISPLAY,
+    "text": Style.TEXT,
+    "script": Style.SCRIPT,
+    "scriptscript": Style.SCRIPTSCRIPT,
+};
+
+defineFunction({
+    type: "styling",
+    names: [
+        "\\displaystyle", "\\textstyle", "\\scriptstyle",
+        "\\scriptscriptstyle",
+    ],
+    props: {
+        numArgs: 0,
+        allowedInText: true,
+    },
+    handler: (context, args) => {
+        const {breakOnTokenText, funcName, parser} = context;
+
+        // parse out the implicit body
+        parser.consumeSpaces();
+        const body = parser.parseExpression(true, breakOnTokenText);
+
+        return {
+            type: "styling",
+            // Figure out what style to use by pulling out the style from
+            // the function name
+            style: funcName.slice(1, funcName.length - 5),
+            value: body,
+        };
+    },
+    htmlBuilder: (group, options) => {
+        // Style changes are handled in the TeXbook on pg. 442, Rule 3.
+        const newStyle = styleMap[group.value.style];
+        const newOptions = options.havingStyle(newStyle);
+        return sizingGroup(group.value.value, newOptions, options);
+    },
+    mathmlBuilder: (group, options) => {
+        // Figure out what style we're changing to.
+        // TODO(kevinb): dedupe this with buildHTML.js
+        // This will be easier of handling of styling nodes is in the same file.
+        const styleMap = {
+            "display": Style.DISPLAY,
+            "text": Style.TEXT,
+            "script": Style.SCRIPT,
+            "scriptscript": Style.SCRIPTSCRIPT,
+        };
+
+        const newStyle = styleMap[group.value.style];
+        const newOptions = options.havingStyle(newStyle);
+
+        const inner = mml.buildExpression(group.value.value, newOptions);
+
+        const node = new mathMLTree.MathNode("mstyle", inner);
+
+        const styleAttributes = {
+            "display": ["0", "true"],
+            "text": ["0", "false"],
+            "script": ["1", "false"],
+            "scriptscript": ["2", "false"],
+        };
+
+        const attr = styleAttributes[group.value.style];
+
+        node.setAttribute("scriptlevel", attr[0]);
+        node.setAttribute("displaystyle", attr[1]);
+
+        return node;
+    },
+});
diff --git a/src/types.js b/src/types.js
index 4173deeb..1b91de0a 100644
--- a/src/types.js
+++ b/src/types.js
@@ -22,3 +22,5 @@ export type ArgType = "color" | "size" | "url" | "original" | Mode;
 
 // LaTeX display style.
 export type StyleStr = "text" | "display";
+
+export type BreakToken = "]" | "}" | "$";


Test patch:
diff --git a/test/__snapshots__/katex-spec.js.snap b/test/__snapshots__/katex-spec.js.snap
index 2989348a..7d1324c1 100644
--- a/test/__snapshots__/katex-spec.js.snap
+++ b/test/__snapshots__/katex-spec.js.snap
@@ -26,6 +26,7 @@ exports[`An implicit group parser within optional groups should work style comma
             "type": "styling",
             "mode": "math",
             "value": {
+              "type": "styling",
               "style": "text",
               "value": [
                 {
@@ -113,6 +114,7 @@ exports[`An implicit group parser within optional groups should work with sizing
             "type": "sizing",
             "mode": "math",
             "value": {
+              "type": "sizing",
               "size": 5,
               "value": [
                 {