diff --git a/.eslintrc b/.eslintrc index 750964f4..326ca4da 100644 --- a/.eslintrc +++ b/.eslintrc @@ -75,7 +75,8 @@ "env": { "es6": true, "node": true, - "browser": true + "browser": true, + "jest": true }, "settings": { "react": { diff --git a/.flowconfig b/.flowconfig index e9e155eb..64f143f9 100644 --- a/.flowconfig +++ b/.flowconfig @@ -1,3 +1,6 @@ +[version] +0.102.0 + [ignore] /dist @@ -8,3 +11,20 @@ [lints] [options] +# $FlowFixMe +# This suppression hould be used to suppress an issue that requires additional +# effort to be resolved. That effort should be recorded in our issue tracking +# system for follow-up. Usually, this suppression is added by tooling during +# flow upgrades and should not be used directly by developers. If it is +# necessary, make sure to raise the corresponding issue and add the issue +# number to a TODO comment associated with the suppression. +suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe + +# $FlowIgnore +# Every now and then, flow cannot understand our code the way we can. We just +# know better and we have to tell flow to trust us. Since this isn't something +# we expect to "fix", we can annotate them to just have flow ignore them. +# Using this suppression makes it clear that we know there's an error but it's +# ok to ignore it. Make sure the suppression has associated commentary to state +# why it's okay to ignore the flow error. +suppress_comment=\\(.\\|\n\\)*\\$FlowIgnore diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..4d9d6dd3 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "trailingComma": "all", + "tabWidth": 4, + "semi": true, + "singleQuote": false, + "bracketSpacing": false +} diff --git a/contrib/render-a11y-string/render-a11y-string.js b/contrib/render-a11y-string/render-a11y-string.js new file mode 100644 index 00000000..d1fc7314 --- /dev/null +++ b/contrib/render-a11y-string/render-a11y-string.js @@ -0,0 +1,706 @@ +// @flow +/** + * renderA11yString returns a readable string. + * + * In some cases the string will have the proper semantic math + * meaning,: + * renderA11yString("\\frac{1}{2}"") + * -> "start fraction, 1, divided by, 2, end fraction" + * + * However, other cases do not: + * renderA11yString("f(x) = x^2") + * -> "f, left parenthesis, x, right parenthesis, equals, x, squared" + * + * The commas in the string aim to increase ease of understanding + * when read by a screenreader. + */ + +// NOTE: since we're importing types here these files won't actually be +// included in the build. +import type {Atom} from "../../src/symbols"; +import type {AnyParseNode} from "../../src/parseNode"; +import type {SettingsOptions} from "../../src/Settings"; + +// $FlowIgnore: we import the types directly anyways +import katex from "katex"; + +const stringMap = { + "(": "left parenthesis", + ")": "right parenthesis", + "[": "open bracket", + "]": "close bracket", + "\\{": "left brace", + "\\}": "right brace", + "\\lvert": "open vertical bar", + "\\rvert": "close vertical bar", + "|": "vertical bar", + "\\uparrow": "up arrow", + "\\Uparrow": "up arrow", + "\\downarrow": "down arrow", + "\\Downarrow": "down arrow", + "\\updownarrow": "up down arrow", + "\\leftarrow": "left arrow", + "\\Leftarrow": "left arrow", + "\\rightarrow": "right arrow", + "\\Rightarrow": "right arrow", + "\\langle": "open angle", + "\\rangle": "close angle", + "\\lfloor": "open floor", + "\\rfloor": "close floor", + "\\int": "integral", + "\\intop": "integral", + "\\lim": "limit", + "\\ln": "natural log", + "\\log": "log", + "\\sin": "sine", + "\\cos": "cosine", + "\\tan": "tangent", + "\\cot": "cotangent", + "\\sum": "sum", + "/": "slash", + ",": "comma", + ".": "point", + "-": "negative", + "+": "plus", + "~": "tilde", + ":": "colon", + "?": "question mark", + "'": "apostrophe", + "\\%": "percent", + " ": "space", + "\\ ": "space", + "\\$": "dollar sign", + "\\angle": "angle", + "\\degree": "degree", + "\\circ": "circle", + "\\vec": "vector", + "\\triangle": "triangle", + "\\pi": "pi", + "\\prime": "prime", + "\\infty": "infinity", + "\\alpha": "alpha", + "\\beta": "beta", + "\\gamma": "gamma", + "\\omega": "omega", + "\\theta": "theta", + "\\sigma": "sigma", + "\\lambda": "lambda", + "\\tau": "tau", + "\\Delta": "delta", + "\\delta": "delta", + "\\mu": "mu", + "\\rho": "rho", + "\\nabla": "del", + "\\ell": "ell", + "\\ldots": "dots", + // TODO: add entries for all accents + "\\hat": "hat", + "\\acute": "acute", +}; + +const powerMap = { + "prime": "prime", + "degree": "degrees", + "circle": "degrees", + "2": "squared", + "3": "cubed", +}; + +const openMap = { + "|": "open vertical bar", + ".": "", +}; + +const closeMap = { + "|": "close vertical bar", + ".": "", +}; + +const binMap = { + "+": "plus", + "-": "minus", + "\\pm": "plus minus", + "\\cdot": "dot", + "*": "times", + "/": "divided by", + "\\times": "times", + "\\div": "divided by", + "\\circ": "circle", + "\\bullet": "bullet", +}; + +const relMap = { + "=": "equals", + "\\approx": "approximately equals", + "≠": "does not equal", + "\\geq": "is greater than or equal to", + "\\ge": "is greater than or equal to", + "\\leq": "is less than or equal to", + "\\le": "is less than or equal to", + ">": "is greater than", + "<": "is less than", + "\\leftarrow": "left arrow", + "\\Leftarrow": "left arrow", + "\\rightarrow": "right arrow", + "\\Rightarrow": "right arrow", + ":": "colon", +}; + +const accentUnderMap = { + "\\underleftarrow": "left arrow", + "\\underrightarrow": "right arrow", + "\\underleftrightarrow": "left-right arrow", + "\\undergroup": "group", + "\\underlinesegment": "line segment", + "\\utilde": "tilde", +}; + +type NestedArray = Array>; + +const buildString = ( + str: string, + type: Atom | "normal", + a11yStrings: NestedArray, +) => { + if (!str) { + return; + } + + let ret; + + if (type === "open") { + ret = str in openMap ? openMap[str] : stringMap[str] || str; + } else if (type === "close") { + ret = str in closeMap ? closeMap[str] : stringMap[str] || str; + } else if (type === "bin") { + ret = binMap[str] || str; + } else if (type === "rel") { + ret = relMap[str] || str; + } else { + ret = stringMap[str] || str; + } + + // If the text to add is a number and there is already a string + // in the list and the last string is a number then we should + // combine them into a single number + if ( + /^\d+$/.test(ret) && + a11yStrings.length > 0 && + // TODO(kevinb): check that the last item in a11yStrings is a string + // I think we might be able to drop the nested arrays, which would make + // this easier to type - $FlowFixMe + /^\d+$/.test(a11yStrings[a11yStrings.length - 1]) + ) { + a11yStrings[a11yStrings.length - 1] += ret; + } else if (ret) { + a11yStrings.push(ret); + } +}; + +const buildRegion = ( + a11yStrings: NestedArray, + callback: (regionStrings: NestedArray) => void, +) => { + const regionStrings: NestedArray = []; + a11yStrings.push(regionStrings); + callback(regionStrings); +}; + +const handleObject = ( + tree: AnyParseNode, + a11yStrings: NestedArray, + atomType: Atom | "normal", +) => { + // Everything else is assumed to be an object... + switch (tree.type) { + case "accent": { + buildRegion(a11yStrings, (a11yStrings) => { + buildA11yStrings(tree.base, a11yStrings, atomType); + a11yStrings.push("with"); + buildString(tree.label, "normal", a11yStrings); + a11yStrings.push("on top"); + }); + break; + } + + case "accentUnder": { + buildRegion(a11yStrings, (a11yStrings) => { + buildA11yStrings(tree.base, a11yStrings, atomType); + a11yStrings.push("with"); + buildString(accentUnderMap[tree.label], "normal", a11yStrings); + a11yStrings.push("underneath"); + }); + break; + } + + case "accent-token": { + // Used internally by accent symbols. + break; + } + + case "atom": { + const {text} = tree; + switch (tree.family) { + case "bin": { + buildString(text, "bin", a11yStrings); + break; + } + case "close": { + buildString(text, "close", a11yStrings); + break; + } + // TODO(kevinb): figure out what should be done for inner + case "inner": { + buildString(tree.text, "inner", a11yStrings); + break; + } + case "open": { + buildString(text, "open", a11yStrings); + break; + } + case "punct": { + buildString(text, "punct", a11yStrings); + break; + } + case "rel": { + buildString(text, "rel", a11yStrings); + break; + } + default: { + (tree.family: empty); + throw new Error(`"${tree.family}" is not a valid atom type`); + } + } + break; + } + + case "color": { + const color = tree.color.replace(/katex-/, ""); + + buildRegion(a11yStrings, (regionStrings) => { + regionStrings.push("start color " + color); + buildA11yStrings(tree.body, regionStrings, atomType); + regionStrings.push("end color " + color); + }); + break; + } + + case "color-token": { + // Used by \color, \colorbox, and \fcolorbox but not directly rendered. + // It's a leaf node and has no children so just break. + break; + } + + case "delimsizing": { + if (tree.delim && tree.delim !== ".") { + buildString(tree.delim, "normal", a11yStrings); + } + break; + } + + case "genfrac": { + buildRegion(a11yStrings, (regionStrings) => { + // genfrac can have unbalanced delimiters + const {leftDelim, rightDelim} = tree; + + // NOTE: Not sure if this is a safe assumption + // hasBarLine true -> fraction, false -> binomial + if (tree.hasBarLine) { + regionStrings.push("start fraction"); + leftDelim && buildString(leftDelim, "open", regionStrings); + buildA11yStrings(tree.numer, regionStrings, atomType); + regionStrings.push("divided by"); + buildA11yStrings(tree.denom, regionStrings, atomType); + rightDelim && buildString(rightDelim, "close", regionStrings); + regionStrings.push("end fraction"); + } else { + regionStrings.push("start binomial"); + leftDelim && buildString(leftDelim, "open", regionStrings); + buildA11yStrings(tree.numer, regionStrings, atomType); + regionStrings.push("over"); + buildA11yStrings(tree.denom, regionStrings, atomType); + rightDelim && buildString(rightDelim, "close", regionStrings); + regionStrings.push("end binomial"); + } + }); + break; + } + + case "kern": { + // No op: we don't attempt to present kerning information + // to the screen reader. + break; + } + + case "leftright": { + buildRegion(a11yStrings, (regionStrings) => { + buildString(tree.left, "open", regionStrings); + buildA11yStrings(tree.body, regionStrings, atomType); + buildString(tree.right, "close", regionStrings); + }); + break; + } + + case "leftright-right": { + // TODO: double check that this is a no-op + break; + } + + case "lap": { + buildA11yStrings(tree.body, a11yStrings, atomType); + break; + } + + case "mathord": { + buildString(tree.text, "normal", a11yStrings); + break; + } + + case "op": { + const {body, name} = tree; + if (body) { + buildA11yStrings(body, a11yStrings, atomType); + } else if (name) { + buildString(name, "normal", a11yStrings); + } + break; + } + + case "op-token": { + // Used internally by operator symbols. + buildString(tree.text, atomType, a11yStrings); + break; + } + + case "ordgroup": { + buildA11yStrings(tree.body, a11yStrings, atomType); + break; + } + + case "overline": { + buildRegion(a11yStrings, function(a11yStrings) { + a11yStrings.push("start overline"); + buildA11yStrings(tree.body, a11yStrings, atomType); + a11yStrings.push("end overline"); + }); + break; + } + + case "phantom": { + a11yStrings.push("empty space"); + break; + } + + case "raisebox": { + buildA11yStrings(tree.body, a11yStrings, atomType); + break; + } + + case "rule": { + a11yStrings.push("rectangle"); + break; + } + + case "sizing": { + buildA11yStrings(tree.body, a11yStrings, atomType); + break; + } + + case "spacing": { + a11yStrings.push("space"); + break; + } + + case "styling": { + // We ignore the styling and just pass through the contents + buildA11yStrings(tree.body, a11yStrings, atomType); + break; + } + + case "sqrt": { + buildRegion(a11yStrings, (regionStrings) => { + const {body, index} = tree; + if (index) { + const indexString = flatten( + buildA11yStrings(index, [], atomType)).join(","); + if (indexString === "3") { + regionStrings.push("cube root of"); + buildA11yStrings(body, regionStrings, atomType); + regionStrings.push("end cube root"); + return; + } + + regionStrings.push("root"); + regionStrings.push("start index"); + buildA11yStrings(index, regionStrings, atomType); + regionStrings.push("end index"); + return; + } + + regionStrings.push("square root of"); + buildA11yStrings(body, regionStrings, atomType); + regionStrings.push("end square root"); + }); + break; + } + + case "supsub": { + const {base, sub, sup} = tree; + let isLog = false; + + if (base) { + buildA11yStrings(base, a11yStrings, atomType); + isLog = base.type === "op" && base.name === "\\log"; + } + + if (sub) { + const regionName = isLog ? "base" : "subscript"; + buildRegion(a11yStrings, function(regionStrings) { + regionStrings.push(`start ${regionName}`); + buildA11yStrings(sub, regionStrings, atomType); + regionStrings.push(`end ${regionName}`); + }); + } + + if (sup) { + buildRegion(a11yStrings, function(regionStrings) { + const supString = flatten( + buildA11yStrings(sup, [], atomType)).join(","); + + if (supString in powerMap) { + regionStrings.push(powerMap[supString]); + return; + } + + regionStrings.push("start superscript"); + buildA11yStrings(sup, regionStrings, atomType); + regionStrings.push("end superscript"); + }); + } + break; + } + + case "text": { + // TODO: handle other fonts + if (tree.font === "\\textbf") { + buildRegion(a11yStrings, function(regionStrings) { + regionStrings.push("start bold text"); + buildA11yStrings(tree.body, regionStrings, atomType); + regionStrings.push("end bold text"); + }); + break; + } + buildRegion(a11yStrings, function(regionStrings) { + regionStrings.push("start text"); + buildA11yStrings(tree.body, regionStrings, atomType); + regionStrings.push("end text"); + }); + break; + } + + case "textord": { + buildString(tree.text, atomType, a11yStrings); + break; + } + + case "smash": { + buildA11yStrings(tree.body, a11yStrings, atomType); + break; + } + + case "enclose": { + // TODO: create a map for these. + // TODO: differentiate between a body with a single atom, e.g. + // "cancel a" instead of "start cancel, a, end cancel" + if (/cancel/.test(tree.label)) { + buildRegion(a11yStrings, function(regionStrings) { + regionStrings.push("start cancel"); + buildA11yStrings(tree.body, regionStrings, atomType); + regionStrings.push("end cancel"); + }); + break; + } else if (/box/.test(tree.label)) { + buildRegion(a11yStrings, function(regionStrings) { + regionStrings.push("start box"); + buildA11yStrings(tree.body, regionStrings, atomType); + regionStrings.push("end box"); + }); + break; + } else if (/sout/.test(tree.label)) { + buildRegion(a11yStrings, function(regionStrings) { + regionStrings.push("start strikeout"); + buildA11yStrings(tree.body, regionStrings, atomType); + regionStrings.push("end strikeout"); + }); + break; + } + throw new Error( + `KaTeX-a11y: enclose node with ${tree.label} not supported yet`); + } + + case "vphantom": { + throw new Error("KaTeX-a11y: vphantom not implemented yet"); + } + + case "hphantom": { + throw new Error("KaTeX-a11y: hphantom not implemented yet"); + } + + case "operatorname": { + buildA11yStrings(tree.body, a11yStrings, atomType); + break; + } + + case "array": { + throw new Error("KaTeX-a11y: array not implemented yet"); + } + + case "keyVals": { + throw new Error("KaTeX-a11y: keyVals not implemented yet"); + } + + case "raw": { + throw new Error("KaTeX-a11y: raw not implemented yet"); + } + + case "size": { + // Although there are nodes of type "size" in the parse tree, they have + // no semantic meaning and should be ignored. + break; + } + + case "url": { + throw new Error("KaTeX-a11y: url not implemented yet"); + } + + case "tag": { + throw new Error("KaTeX-a11y: tag not implemented yet"); + } + + case "verb": { + buildString(`start verbatim`, "normal", a11yStrings); + buildString(tree.body, "normal", a11yStrings); + buildString(`end verbatim`, "normal", a11yStrings); + break; + } + + case "environment": { + throw new Error("KaTeX-a11y: environment not implemented yet"); + } + + case "horizBrace": { + buildString(`start ${tree.label.slice(1)}`, "normal", a11yStrings); + buildA11yStrings(tree.base, a11yStrings, atomType); + buildString(`end ${tree.label.slice(1)}`, "normal", a11yStrings); + break; + } + + case "infix": { + // All infix nodes are replace with other nodes. + break; + } + + case "includegraphics": { + throw new Error("KaTeX-a11y: includegraphics not implemented yet"); + } + + case "font": { + // TODO: callout the start/end of specific fonts + // TODO: map \BBb{N} to "the naturals" or something like that + buildA11yStrings(tree.body, a11yStrings, atomType); + break; + } + + case "href": { + throw new Error("KaTeX-a11y: href not implemented yet"); + } + + case "cr": { + // This is used by environments. + throw new Error("KaTeX-a11y: cr not implemented yet"); + } + + case "underline": { + buildRegion(a11yStrings, function(a11yStrings) { + a11yStrings.push("start underline"); + buildA11yStrings(tree.body, a11yStrings, atomType); + a11yStrings.push("end underline"); + }); + break; + } + + case "xArrow": { + throw new Error("KaTeX-a11y: xArrow not implemented yet"); + } + + case "mclass": { + // \neq and \ne are macros so we let "htmlmathml" render the mathmal + // side of things and extract the text from that. + const atomType = tree.mclass.slice(1); + // $FlowFixMe: drop the leading "m" from the values in mclass + buildA11yStrings(tree.body, a11yStrings, atomType); + break; + } + + case "mathchoice": { + // TODO: track which which style we're using, e.g. dispaly, text, etc. + // default to text style if even that may not be the correct style + buildA11yStrings(tree.text, a11yStrings, atomType); + break; + } + + case "htmlmathml": { + buildA11yStrings(tree.mathml, a11yStrings, atomType); + break; + } + + case "middle": { + buildString(tree.delim, atomType, a11yStrings); + break; + } + + default: + (tree.type: empty); + throw new Error("KaTeX a11y un-recognized type: " + tree.type); + } +}; + +const buildA11yStrings = ( + tree: AnyParseNode | AnyParseNode[], + a11yStrings: NestedArray = [], + atomType: Atom | "normal", +) => { + if (tree instanceof Array) { + for (let i = 0; i < tree.length; i++) { + buildA11yStrings(tree[i], a11yStrings, atomType); + } + } else { + handleObject(tree, a11yStrings, atomType); + } + + return a11yStrings; +}; + + +const flatten = function(array) { + let result = []; + + array.forEach(function(item) { + if (item instanceof Array) { + result = result.concat(flatten(item)); + } else { + result.push(item); + } + }); + + return result; +}; + +const renderA11yString = function(text: string, settings?: SettingsOptions) { + + const tree = katex.__parse(text, settings); + const a11yStrings = buildA11yStrings(tree, [], "normal"); + return flatten(a11yStrings).join(", "); +}; + +export default renderA11yString; diff --git a/contrib/render-a11y-string/test/render-a11y-string-spec.js b/contrib/render-a11y-string/test/render-a11y-string-spec.js new file mode 100644 index 00000000..0929e6fc --- /dev/null +++ b/contrib/render-a11y-string/test/render-a11y-string-spec.js @@ -0,0 +1,526 @@ +/* eslint-disable max-len */ +// @flow +import renderA11yString from "../render-a11y-string"; + +describe("renderA11yString", () => { + describe("basic expressions", () => { + test("simple addition", () => { + const result = renderA11yString("1 + 2"); + expect(result).toMatchInlineSnapshot(`"1, plus, 2"`); + }); + }); + + describe("accent", () => { + test("\\vec", () => { + const result = renderA11yString("\\vec{a}"); + expect(result).toMatchInlineSnapshot(`"a, with, vector, on top"`); + }); + + test("\\acute{a}", () => { + const result = renderA11yString("\\acute{a}"); + expect(result).toMatchInlineSnapshot(`"a, with, acute, on top"`); + }); + + test("\\hat{a}", () => { + const result = renderA11yString("\\hat{a}"); + expect(result).toMatchInlineSnapshot(`"a, with, hat, on top"`); + }); + }); + + describe("accentUnder", () => { + test("\\underleftarrow", () => { + const result = renderA11yString("\\underleftarrow{1+2}"); + expect(result).toMatchInlineSnapshot( + `"1, plus, 2, with, left arrow, underneath"`, + ); + }); + + test("\\underlinesegment", () => { + const result = renderA11yString("\\underlinesegment{1+2}"); + expect(result).toMatchInlineSnapshot( + `"1, plus, 2, with, line segment, underneath"`, + ); + }); + }); + + describe("atom", () => { + test("punct", () => { + const result = renderA11yString("1, 2, 3"); + expect(result).toMatchInlineSnapshot(`"1, comma, 2, comma, 3"`); + }); + }); + + describe("color", () => { + test("\\color{red}", () => { + const result = renderA11yString("\\color{red}1+2"); + expect(result).toMatchInlineSnapshot( + `"start color red, 1, plus, 2, end color red"`, + ); + }); + + test("\\color{FF0000}", () => { + const result = renderA11yString("\\color{FF0000}1+2"); + expect(result).toMatchInlineSnapshot( + `"start color #FF0000, 1, plus, 2, end color #FF0000"`, + ); + }); + + // colorIsTextColor is an option added in KaTeX 0.9.0 for backward + // compatibility. It makes \color parse like \textcolor. We use it + // in the KA webapp, and need it here because the tests are written + // assuming it is set. + test("\\color{red} with {colorIsTextColor: true}", () => { + const result = renderA11yString("\\color{red}1+2", { + colorIsTextColor: true, + }); + expect(result).toMatchInlineSnapshot( + `"start color red, 1, end color red, plus, 2"`, + ); + }); + + test("\\textcolor{red}", () => { + const result = renderA11yString("\\textcolor{red}1+2"); + expect(result).toMatchInlineSnapshot( + `"start color red, 1, end color red, plus, 2"`, + ); + }); + }); + + describe("delimiters", () => { + test("simple parens", () => { + const result = renderA11yString("(1 + 3)"); + expect(result).toMatchInlineSnapshot( + `"left parenthesis, 1, plus, 3, right parenthesis"`, + ); + }); + + test("simple brackets", () => { + const result = renderA11yString("[1 + 3]"); + expect(result).toMatchInlineSnapshot( + `"open bracket, 1, plus, 3, close bracket"`, + ); + }); + + test("nested parens", () => { + const result = renderA11yString("(a + (b + c))"); + expect(result).toMatchInlineSnapshot( + `"left parenthesis, a, plus, left parenthesis, b, plus, c, right parenthesis, right parenthesis"`, + ); + }); + + test("stretchy parens around fractions", () => { + const result = renderA11yString("\\left(\\frac{1}{x}\\right)"); + expect(result).toMatchInlineSnapshot( + `"left parenthesis, start fraction, 1, divided by, x, end fraction, right parenthesis"`, + ); + }); + }); + + describe("delimsizing", () => { + test("\\bigl(1+2\\bigr)", () => { + const result = renderA11yString("\\bigl(1+2\\bigr)"); + expect(result).toMatchInlineSnapshot( + `"left parenthesis, 1, plus, 2, right parenthesis"`, + ); + }); + }); + + describe("enclose", () => { + test("\\cancel", () => { + const result = renderA11yString("\\cancel{a}"); + expect(result).toMatchInlineSnapshot( + `"start cancel, a, end cancel"`, + ); + }); + + test("\\fbox", () => { + const result = renderA11yString("\\fbox{a}"); + expect(result).toMatchInlineSnapshot(`"start box, a, end box"`); + }); + + test("\\boxed", () => { + const result = renderA11yString("\\boxed{a}"); + expect(result).toMatchInlineSnapshot(`"start box, a, end box"`); + }); + + test("\\sout", () => { + const result = renderA11yString("\\sout{a}"); + expect(result).toMatchInlineSnapshot( + `"start strikeout, a, end strikeout"`, + ); + }); + }); + + describe("exponents", () => { + test("simple exponent", () => { + const result = renderA11yString("e^x"); + expect(result).toMatchInlineSnapshot( + `"e, start superscript, x, end superscript"`, + ); + }); + + test("^{\\circ} => degrees", () => { + const result = renderA11yString("90^{\\circ}"); + expect(result).toMatchInlineSnapshot(`"90, degrees"`); + }); + + test("^{\\degree} => degrees", () => { + const result = renderA11yString("90^{\\degree}"); + expect(result).toMatchInlineSnapshot(`"90, degrees"`); + }); + + test("^{\\prime} => prime", () => { + const result = renderA11yString("f^{\\prime}"); + expect(result).toMatchInlineSnapshot(`"f, prime"`); + }); + + test("^2 => squared", () => { + const result = renderA11yString("x^2"); + expect(result).toMatchInlineSnapshot(`"x, squared"`); + }); + + test("^3 => cubed", () => { + const result = renderA11yString("x^3"); + expect(result).toMatchInlineSnapshot(`"x, cubed"`); + }); + + test("log_2", () => { + const result = renderA11yString("\\log_2{x+1}"); + expect(result).toMatchInlineSnapshot( + `"log, start base, 2, end base, x, plus, 1"`, + ); + }); + + test("a_{n+1}", () => { + const result = renderA11yString("a_{n+1}"); + expect(result).toMatchInlineSnapshot( + `"a, start subscript, n, plus, 1, end subscript"`, + ); + }); + }); + + describe("genfrac", () => { + test("simple fractions", () => { + const result = renderA11yString("\\frac{2}{3}"); + expect(result).toMatchInlineSnapshot( + `"start fraction, 2, divided by, 3, end fraction"`, + ); + }); + + test("nested fractions", () => { + const result = renderA11yString("\\frac{1}{1+\\frac{1}{x}}"); + // TODO: this result is ambiguous, we need to fix this + expect(result).toMatchInlineSnapshot( + `"start fraction, 1, divided by, 1, plus, start fraction, 1, divided by, x, end fraction, end fraction"`, + ); + }); + + test("binomials", () => { + const result = renderA11yString("\\binom{n}{k}"); + // TODO: drop the parenthesis as they're not normally read + expect(result).toMatchInlineSnapshot( + `"start binomial, left parenthesis, n, over, k, right parenthesis, end binomial"`, + ); + }); + }); + + describe("horizBrace", () => { + test("\\overbrace", () => { + const result = renderA11yString("\\overbrace{1+2}"); + expect(result).toMatchInlineSnapshot( + `"start overbrace, 1, plus, 2, end overbrace"`, + ); + }); + + test("\\underbrace", () => { + const result = renderA11yString("\\underbrace{1+2}"); + expect(result).toMatchInlineSnapshot( + `"start underbrace, 1, plus, 2, end underbrace"`, + ); + }); + }); + + describe("infix", () => { + test("\\over", () => { + const result = renderA11yString("a \\over b"); + expect(result).toMatchInlineSnapshot( + `"start fraction, a, divided by, b, end fraction"`, + ); + }); + + test("\\choose", () => { + const result = renderA11yString("a \\choose b"); + expect(result).toMatchInlineSnapshot( + `"start binomial, left parenthesis, a, over, b, right parenthesis, end binomial"`, + ); + }); + + test("\\above", () => { + const result = renderA11yString("a \\above{2pt} b"); + expect(result).toMatchInlineSnapshot( + `"start fraction, a, divided by, b, end fraction"`, + ); + }); + }); + + describe("inner", () => { + test("\\ldots", () => { + const result = renderA11yString("\\ldots"); + expect(result).toMatchInlineSnapshot(`"dots"`); + }); + }); + + describe("lap", () => { + test("\\llap", () => { + const result = renderA11yString("a\\llap{b}"); + expect(result).toMatchInlineSnapshot( + `"a, start text, b, end text"`, + ); + }); + + test("\\rlap", () => { + const result = renderA11yString("a\\rlap{b}"); + expect(result).toMatchInlineSnapshot( + `"a, start text, b, end text"`, + ); + }); + }); + + describe("middle", () => { + test("\\middle", () => { + const result = renderA11yString("\\left(a\\middle|b\\right)"); + expect(result).toMatchInlineSnapshot( + `"left parenthesis, a, vertical bar, b, right parenthesis"`, + ); + }); + }); + + describe("mod", () => { + test("\\mod", () => { + const result = renderA11yString("\\mod{23}"); + // TODO: drop the "space" + // TODO: collate m, o, d... we should fix this inside of KaTeX since + // this affects the HTML and MathML output as well + expect(result).toMatchInlineSnapshot(`"space, m, o, d, 23"`); + }); + }); + + describe("op", () => { + test("\\lim", () => { + const result = renderA11yString("\\lim{x+1}"); + // TODO: add begin/end to track argument of operators + expect(result).toMatchInlineSnapshot(`"limit, x, plus, 1"`); + }); + + test("\\sin 2\\pi", () => { + const result = renderA11yString("\\sin{2\\pi}"); + // TODO: add begin/end to track argument of operators + expect(result).toMatchInlineSnapshot(`"sine, 2, pi"`); + }); + + test("\\sum_{i=0}", () => { + const result = renderA11yString("\\sum_{i=0}"); + expect(result).toMatchInlineSnapshot( + `"sum, start subscript, i, equals, 0, end subscript"`, + ); + }); + + test("\u2211_{i=0}", () => { + const result = renderA11yString("\u2211_{i=0}"); + expect(result).toMatchInlineSnapshot( + `"sum, start subscript, i, equals, 0, end subscript"`, + ); + }); + }); + + describe("operatorname", () => { + test("\\limsup", () => { + const result = renderA11yString("\\limsup"); + // TODO: collate strings so that this is "lim, sup" + // NOTE: this also affect HTML and MathML output + expect(result).toMatchInlineSnapshot(`"l, i, m, s, u, p"`); + }); + + test("\\liminf", () => { + const result = renderA11yString("\\liminf"); + expect(result).toMatchInlineSnapshot(`"l, i, m, i, n, f"`); + }); + + test("\\argmin", () => { + const result = renderA11yString("\\argmin"); + expect(result).toMatchInlineSnapshot(`"a, r, g, m, i, n"`); + }); + }); + + describe("overline", () => { + test("\\overline", () => { + const result = renderA11yString("\\overline{1+2}"); + expect(result).toMatchInlineSnapshot( + `"start overline, 1, plus, 2, end overline"`, + ); + }); + }); + + describe("phantom", () => { + test("\\phantom", () => { + const result = renderA11yString("1+\\phantom{2}"); + expect(result).toMatchInlineSnapshot(`"1, plus, empty space"`); + }); + }); + + describe("raisebox", () => { + test("\\raisebox", () => { + const result = renderA11yString("x+\\raisebox{1em}{y}"); + expect(result).toMatchInlineSnapshot(`"x, plus, y"`); + }); + }); + + describe("relations", () => { + test("1 \\neq 2", () => { + const result = renderA11yString("1 \\neq 2"); + expect(result).toMatchInlineSnapshot(`"1, does not equal, 2"`); + }); + + test("1 \\ne 2", () => { + const result = renderA11yString("1 \\ne 2"); + expect(result).toMatchInlineSnapshot(`"1, does not equal, 2"`); + }); + + test("1 \\geq 2", () => { + const result = renderA11yString("1 \\geq 2"); + expect(result).toMatchInlineSnapshot( + `"1, is greater than or equal to, 2"`, + ); + }); + + test("1 \\ge 2", () => { + const result = renderA11yString("1 \\ge 2"); + expect(result).toMatchInlineSnapshot( + `"1, is greater than or equal to, 2"`, + ); + }); + + test("1 \\leq 2", () => { + const result = renderA11yString("1 \\leq 3"); + expect(result).toMatchInlineSnapshot( + `"1, is less than or equal to, 3"`, + ); + }); + + test("1 \\le 2", () => { + const result = renderA11yString("1 \\le 3"); + expect(result).toMatchInlineSnapshot( + `"1, is less than or equal to, 3"`, + ); + }); + }); + + describe("rule", () => { + test("\\rule", () => { + const result = renderA11yString("\\rule{1em}{1em}"); + expect(result).toMatchInlineSnapshot(`"rectangle"`); + }); + }); + + describe("smash", () => { + test("1 + \\smash{2}", () => { + const result = renderA11yString("1 + \\smash{2}"); + expect(result).toMatchInlineSnapshot(`"1, plus, 2"`); + }); + }); + + describe("sqrt", () => { + test("square root", () => { + const result = renderA11yString("\\sqrt{x + 1}"); + expect(result).toMatchInlineSnapshot( + `"square root of, x, plus, 1, end square root"`, + ); + }); + + test("nest square root", () => { + const result = renderA11yString("\\sqrt{x + \\sqrt{y}}"); + // TODO: this sounds ambiguous as well... we should probably say "start square root" + expect(result).toMatchInlineSnapshot( + `"square root of, x, plus, square root of, y, end square root, end square root"`, + ); + }); + + test("cube root", () => { + const result = renderA11yString("\\sqrt[3]{x + 1}"); + expect(result).toMatchInlineSnapshot( + `"cube root of, x, plus, 1, end cube root"`, + ); + }); + + test("nth root", () => { + const result = renderA11yString("\\sqrt[n]{x + 1}"); + expect(result).toMatchInlineSnapshot( + `"root, start index, n, end index"`, + ); + }); + }); + + describe("sizing", () => { + test("\\Huge is ignored", () => { + const result = renderA11yString("\\Huge{a+b}"); + expect(result).toMatchInlineSnapshot(`"a, plus, b"`); + }); + + test("\\small is ignored", () => { + const result = renderA11yString("\\small{a+b}"); + expect(result).toMatchInlineSnapshot(`"a, plus, b"`); + }); + + // We don't need to test all sizing commands since all style + // nodes are treated in the same way. + }); + + describe("styling", () => { + test("\\displaystyle is ignored", () => { + const result = renderA11yString("\\displaystyle{a+b}"); + expect(result).toMatchInlineSnapshot(`"a, plus, b"`); + }); + + test("\\textstyle is ignored", () => { + const result = renderA11yString("\\textstyle{a+b}"); + expect(result).toMatchInlineSnapshot(`"a, plus, b"`); + }); + + // We don't need to test all styling commands since all style + // nodes are treated in the same way. + }); + + describe("text", () => { + test("\\text", () => { + const result = renderA11yString("\\text{hello}"); + expect(result).toMatchInlineSnapshot( + `"start text, h, e, l, l, o, end text"`, + ); + }); + + test("\\textbf", () => { + const result = renderA11yString("\\textbf{hello}"); + expect(result).toMatchInlineSnapshot( + `"start bold text, h, e, l, l, o, end bold text"`, + ); + }); + }); + + describe("underline", () => { + test("\\underline", () => { + const result = renderA11yString("\\underline{1+2}"); + expect(result).toMatchInlineSnapshot( + `"start underline, 1, plus, 2, end underline"`, + ); + }); + }); + + describe("verb", () => { + test("\\verb", () => { + const result = renderA11yString("\\verb|hello|"); + expect(result).toMatchInlineSnapshot( + `"start verbatim, hello, end verbatim"`, + ); + }); + }); +}); diff --git a/flow-typed/npm/jest_v24.x.x.js b/flow-typed/npm/jest_v24.x.x.js new file mode 100644 index 00000000..7487ebd3 --- /dev/null +++ b/flow-typed/npm/jest_v24.x.x.js @@ -0,0 +1,1201 @@ +// flow-typed signature: 3e71a890e92658d48779f359ce8bf84b +// flow-typed version: cd4a902eba/jest_v24.x.x/flow_>=v0.39.x + +type JestMockFn, TReturn> = { + (...args: TArguments): TReturn, + /** + * An object for introspecting mock calls + */ + mock: { + /** + * An array that represents all calls that have been made into this mock + * function. Each call is represented by an array of arguments that were + * passed during the call. + */ + calls: Array, + /** + * An array that contains all the object instances that have been + * instantiated from this mock function. + */ + instances: Array, + /** + * An array that contains all the object results that have been + * returned by this mock function call + */ + results: Array<{ isThrow: boolean, value: TReturn }>, + }, + /** + * Resets all information stored in the mockFn.mock.calls and + * mockFn.mock.instances arrays. Often this is useful when you want to clean + * up a mock's usage data between two assertions. + */ + mockClear(): void, + /** + * Resets all information stored in the mock. This is useful when you want to + * completely restore a mock back to its initial state. + */ + mockReset(): void, + /** + * Removes the mock and restores the initial implementation. This is useful + * when you want to mock functions in certain test cases and restore the + * original implementation in others. Beware that mockFn.mockRestore only + * works when mock was created with jest.spyOn. Thus you have to take care of + * restoration yourself when manually assigning jest.fn(). + */ + mockRestore(): void, + /** + * Accepts a function that should be used as the implementation of the mock. + * The mock itself will still record all calls that go into and instances + * that come from itself -- the only difference is that the implementation + * will also be executed when the mock is called. + */ + mockImplementation( + fn: (...args: TArguments) => TReturn + ): JestMockFn, + /** + * Accepts a function that will be used as an implementation of the mock for + * one call to the mocked function. Can be chained so that multiple function + * calls produce different results. + */ + mockImplementationOnce( + fn: (...args: TArguments) => TReturn + ): JestMockFn, + /** + * Accepts a string to use in test result output in place of "jest.fn()" to + * indicate which mock function is being referenced. + */ + mockName(name: string): JestMockFn, + /** + * Just a simple sugar function for returning `this` + */ + mockReturnThis(): void, + /** + * Accepts a value that will be returned whenever the mock function is called. + */ + mockReturnValue(value: TReturn): JestMockFn, + /** + * Sugar for only returning a value once inside your mock + */ + mockReturnValueOnce(value: TReturn): JestMockFn, + /** + * Sugar for jest.fn().mockImplementation(() => Promise.resolve(value)) + */ + mockResolvedValue(value: TReturn): JestMockFn>, + /** + * Sugar for jest.fn().mockImplementationOnce(() => Promise.resolve(value)) + */ + mockResolvedValueOnce( + value: TReturn + ): JestMockFn>, + /** + * Sugar for jest.fn().mockImplementation(() => Promise.reject(value)) + */ + mockRejectedValue(value: TReturn): JestMockFn>, + /** + * Sugar for jest.fn().mockImplementationOnce(() => Promise.reject(value)) + */ + mockRejectedValueOnce(value: TReturn): JestMockFn>, +}; + +type JestAsymmetricEqualityType = { + /** + * A custom Jasmine equality tester + */ + asymmetricMatch(value: mixed): boolean, +}; + +type JestCallsType = { + allArgs(): mixed, + all(): mixed, + any(): boolean, + count(): number, + first(): mixed, + mostRecent(): mixed, + reset(): void, +}; + +type JestClockType = { + install(): void, + mockDate(date: Date): void, + tick(milliseconds?: number): void, + uninstall(): void, +}; + +type JestMatcherResult = { + message?: string | (() => string), + pass: boolean, +}; + +type JestMatcher = ( + received: any, + ...actual: Array +) => JestMatcherResult | Promise; + +type JestPromiseType = { + /** + * Use rejects to unwrap the reason of a rejected promise so any other + * matcher can be chained. If the promise is fulfilled the assertion fails. + */ + rejects: JestExpectType, + /** + * Use resolves to unwrap the value of a fulfilled promise so any other + * matcher can be chained. If the promise is rejected the assertion fails. + */ + resolves: JestExpectType, +}; + +/** + * Jest allows functions and classes to be used as test names in test() and + * describe() + */ +type JestTestName = string | Function; + +/** + * Plugin: jest-styled-components + */ + +type JestStyledComponentsMatcherValue = + | string + | JestAsymmetricEqualityType + | RegExp + | typeof undefined; + +type JestStyledComponentsMatcherOptions = { + media?: string, + modifier?: string, + supports?: string, +}; + +type JestStyledComponentsMatchersType = { + toHaveStyleRule( + property: string, + value: JestStyledComponentsMatcherValue, + options?: JestStyledComponentsMatcherOptions + ): void, +}; + +/** + * Plugin: jest-enzyme + */ +type EnzymeMatchersType = { + // 5.x + toBeEmpty(): void, + toBePresent(): void, + // 6.x + toBeChecked(): void, + toBeDisabled(): void, + toBeEmptyRender(): void, + toContainMatchingElement(selector: string): void, + toContainMatchingElements(n: number, selector: string): void, + toContainExactlyOneMatchingElement(selector: string): void, + toContainReact(element: React$Element): void, + toExist(): void, + toHaveClassName(className: string): void, + toHaveHTML(html: string): void, + toHaveProp: ((propKey: string, propValue?: any) => void) & + ((props: {}) => void), + toHaveRef(refName: string): void, + toHaveState: ((stateKey: string, stateValue?: any) => void) & + ((state: {}) => void), + toHaveStyle: ((styleKey: string, styleValue?: any) => void) & + ((style: {}) => void), + toHaveTagName(tagName: string): void, + toHaveText(text: string): void, + toHaveValue(value: any): void, + toIncludeText(text: string): void, + toMatchElement( + element: React$Element, + options?: {| ignoreProps?: boolean, verbose?: boolean |} + ): void, + toMatchSelector(selector: string): void, + // 7.x + toHaveDisplayName(name: string): void, +}; + +// DOM testing library extensions (jest-dom) +// https://github.com/testing-library/jest-dom +type DomTestingLibraryType = { + /** + * @deprecated + */ + toBeInTheDOM(container?: HTMLElement): void, + + toBeInTheDocument(): void, + toBeVisible(): void, + toBeEmpty(): void, + toBeDisabled(): void, + toBeEnabled(): void, + toBeInvalid(): void, + toBeRequired(): void, + toBeValid(): void, + toContainElement(element: HTMLElement | null): void, + toContainHTML(htmlText: string): void, + toHaveAttribute(attr: string, value?: any): void, + toHaveClass(...classNames: string[]): void, + toHaveFocus(): void, + toHaveFormValues(expectedValues: { [name: string]: any }): void, + toHaveStyle(css: string): void, + toHaveTextContent( + text: string | RegExp, + options?: { normalizeWhitespace: boolean } + ): void, + toHaveValue(value?: string | string[] | number): void, +}; + +// Jest JQuery Matchers: https://github.com/unindented/custom-jquery-matchers +type JestJQueryMatchersType = { + toExist(): void, + toHaveLength(len: number): void, + toHaveId(id: string): void, + toHaveClass(className: string): void, + toHaveTag(tag: string): void, + toHaveAttr(key: string, val?: any): void, + toHaveProp(key: string, val?: any): void, + toHaveText(text: string | RegExp): void, + toHaveData(key: string, val?: any): void, + toHaveValue(val: any): void, + toHaveCss(css: { [key: string]: any }): void, + toBeChecked(): void, + toBeDisabled(): void, + toBeEmpty(): void, + toBeHidden(): void, + toBeSelected(): void, + toBeVisible(): void, + toBeFocused(): void, + toBeInDom(): void, + toBeMatchedBy(sel: string): void, + toHaveDescendant(sel: string): void, + toHaveDescendantWithText(sel: string, text: string | RegExp): void, +}; + +// Jest Extended Matchers: https://github.com/jest-community/jest-extended +type JestExtendedMatchersType = { + /** + * Note: Currently unimplemented + * Passing assertion + * + * @param {String} message + */ + // pass(message: string): void; + + /** + * Note: Currently unimplemented + * Failing assertion + * + * @param {String} message + */ + // fail(message: string): void; + + /** + * Use .toBeEmpty when checking if a String '', Array [] or Object {} is empty. + */ + toBeEmpty(): void, + + /** + * Use .toBeOneOf when checking if a value is a member of a given Array. + * @param {Array.<*>} members + */ + toBeOneOf(members: any[]): void, + + /** + * Use `.toBeNil` when checking a value is `null` or `undefined`. + */ + toBeNil(): void, + + /** + * Use `.toSatisfy` when you want to use a custom matcher by supplying a predicate function that returns a `Boolean`. + * @param {Function} predicate + */ + toSatisfy(predicate: (n: any) => boolean): void, + + /** + * Use `.toBeArray` when checking if a value is an `Array`. + */ + toBeArray(): void, + + /** + * Use `.toBeArrayOfSize` when checking if a value is an `Array` of size x. + * @param {Number} x + */ + toBeArrayOfSize(x: number): void, + + /** + * Use `.toIncludeAllMembers` when checking if an `Array` contains all of the same members of a given set. + * @param {Array.<*>} members + */ + toIncludeAllMembers(members: any[]): void, + + /** + * Use `.toIncludeAnyMembers` when checking if an `Array` contains any of the members of a given set. + * @param {Array.<*>} members + */ + toIncludeAnyMembers(members: any[]): void, + + /** + * Use `.toSatisfyAll` when you want to use a custom matcher by supplying a predicate function that returns a `Boolean` for all values in an array. + * @param {Function} predicate + */ + toSatisfyAll(predicate: (n: any) => boolean): void, + + /** + * Use `.toBeBoolean` when checking if a value is a `Boolean`. + */ + toBeBoolean(): void, + + /** + * Use `.toBeTrue` when checking a value is equal (===) to `true`. + */ + toBeTrue(): void, + + /** + * Use `.toBeFalse` when checking a value is equal (===) to `false`. + */ + toBeFalse(): void, + + /** + * Use .toBeDate when checking if a value is a Date. + */ + toBeDate(): void, + + /** + * Use `.toBeFunction` when checking if a value is a `Function`. + */ + toBeFunction(): void, + + /** + * Use `.toHaveBeenCalledBefore` when checking if a `Mock` was called before another `Mock`. + * + * Note: Required Jest version >22 + * Note: Your mock functions will have to be asynchronous to cause the timestamps inside of Jest to occur in a differentJS event loop, otherwise the mock timestamps will all be the same + * + * @param {Mock} mock + */ + toHaveBeenCalledBefore(mock: JestMockFn): void, + + /** + * Use `.toBeNumber` when checking if a value is a `Number`. + */ + toBeNumber(): void, + + /** + * Use `.toBeNaN` when checking a value is `NaN`. + */ + toBeNaN(): void, + + /** + * Use `.toBeFinite` when checking if a value is a `Number`, not `NaN` or `Infinity`. + */ + toBeFinite(): void, + + /** + * Use `.toBePositive` when checking if a value is a positive `Number`. + */ + toBePositive(): void, + + /** + * Use `.toBeNegative` when checking if a value is a negative `Number`. + */ + toBeNegative(): void, + + /** + * Use `.toBeEven` when checking if a value is an even `Number`. + */ + toBeEven(): void, + + /** + * Use `.toBeOdd` when checking if a value is an odd `Number`. + */ + toBeOdd(): void, + + /** + * Use `.toBeWithin` when checking if a number is in between the given bounds of: start (inclusive) and end (exclusive). + * + * @param {Number} start + * @param {Number} end + */ + toBeWithin(start: number, end: number): void, + + /** + * Use `.toBeObject` when checking if a value is an `Object`. + */ + toBeObject(): void, + + /** + * Use `.toContainKey` when checking if an object contains the provided key. + * + * @param {String} key + */ + toContainKey(key: string): void, + + /** + * Use `.toContainKeys` when checking if an object has all of the provided keys. + * + * @param {Array.} keys + */ + toContainKeys(keys: string[]): void, + + /** + * Use `.toContainAllKeys` when checking if an object only contains all of the provided keys. + * + * @param {Array.} keys + */ + toContainAllKeys(keys: string[]): void, + + /** + * Use `.toContainAnyKeys` when checking if an object contains at least one of the provided keys. + * + * @param {Array.} keys + */ + toContainAnyKeys(keys: string[]): void, + + /** + * Use `.toContainValue` when checking if an object contains the provided value. + * + * @param {*} value + */ + toContainValue(value: any): void, + + /** + * Use `.toContainValues` when checking if an object contains all of the provided values. + * + * @param {Array.<*>} values + */ + toContainValues(values: any[]): void, + + /** + * Use `.toContainAllValues` when checking if an object only contains all of the provided values. + * + * @param {Array.<*>} values + */ + toContainAllValues(values: any[]): void, + + /** + * Use `.toContainAnyValues` when checking if an object contains at least one of the provided values. + * + * @param {Array.<*>} values + */ + toContainAnyValues(values: any[]): void, + + /** + * Use `.toContainEntry` when checking if an object contains the provided entry. + * + * @param {Array.} entry + */ + toContainEntry(entry: [string, string]): void, + + /** + * Use `.toContainEntries` when checking if an object contains all of the provided entries. + * + * @param {Array.>} entries + */ + toContainEntries(entries: [string, string][]): void, + + /** + * Use `.toContainAllEntries` when checking if an object only contains all of the provided entries. + * + * @param {Array.>} entries + */ + toContainAllEntries(entries: [string, string][]): void, + + /** + * Use `.toContainAnyEntries` when checking if an object contains at least one of the provided entries. + * + * @param {Array.>} entries + */ + toContainAnyEntries(entries: [string, string][]): void, + + /** + * Use `.toBeExtensible` when checking if an object is extensible. + */ + toBeExtensible(): void, + + /** + * Use `.toBeFrozen` when checking if an object is frozen. + */ + toBeFrozen(): void, + + /** + * Use `.toBeSealed` when checking if an object is sealed. + */ + toBeSealed(): void, + + /** + * Use `.toBeString` when checking if a value is a `String`. + */ + toBeString(): void, + + /** + * Use `.toEqualCaseInsensitive` when checking if a string is equal (===) to another ignoring the casing of both strings. + * + * @param {String} string + */ + toEqualCaseInsensitive(string: string): void, + + /** + * Use `.toStartWith` when checking if a `String` starts with a given `String` prefix. + * + * @param {String} prefix + */ + toStartWith(prefix: string): void, + + /** + * Use `.toEndWith` when checking if a `String` ends with a given `String` suffix. + * + * @param {String} suffix + */ + toEndWith(suffix: string): void, + + /** + * Use `.toInclude` when checking if a `String` includes the given `String` substring. + * + * @param {String} substring + */ + toInclude(substring: string): void, + + /** + * Use `.toIncludeRepeated` when checking if a `String` includes the given `String` substring the correct number of times. + * + * @param {String} substring + * @param {Number} times + */ + toIncludeRepeated(substring: string, times: number): void, + + /** + * Use `.toIncludeMultiple` when checking if a `String` includes all of the given substrings. + * + * @param {Array.} substring + */ + toIncludeMultiple(substring: string[]): void, +}; + +interface JestExpectType { + not: JestExpectType & + EnzymeMatchersType & + DomTestingLibraryType & + JestJQueryMatchersType & + JestStyledComponentsMatchersType & + JestExtendedMatchersType; + /** + * If you have a mock function, you can use .lastCalledWith to test what + * arguments it was last called with. + */ + lastCalledWith(...args: Array): void; + /** + * toBe just checks that a value is what you expect. It uses === to check + * strict equality. + */ + toBe(value: any): void; + /** + * Use .toBeCalledWith to ensure that a mock function was called with + * specific arguments. + */ + toBeCalledWith(...args: Array): void; + /** + * Using exact equality with floating point numbers is a bad idea. Rounding + * means that intuitive things fail. + */ + toBeCloseTo(num: number, delta: any): void; + /** + * Use .toBeDefined to check that a variable is not undefined. + */ + toBeDefined(): void; + /** + * Use .toBeFalsy when you don't care what a value is, you just want to + * ensure a value is false in a boolean context. + */ + toBeFalsy(): void; + /** + * To compare floating point numbers, you can use toBeGreaterThan. + */ + toBeGreaterThan(number: number): void; + /** + * To compare floating point numbers, you can use toBeGreaterThanOrEqual. + */ + toBeGreaterThanOrEqual(number: number): void; + /** + * To compare floating point numbers, you can use toBeLessThan. + */ + toBeLessThan(number: number): void; + /** + * To compare floating point numbers, you can use toBeLessThanOrEqual. + */ + toBeLessThanOrEqual(number: number): void; + /** + * Use .toBeInstanceOf(Class) to check that an object is an instance of a + * class. + */ + toBeInstanceOf(cls: Class<*>): void; + /** + * .toBeNull() is the same as .toBe(null) but the error messages are a bit + * nicer. + */ + toBeNull(): void; + /** + * Use .toBeTruthy when you don't care what a value is, you just want to + * ensure a value is true in a boolean context. + */ + toBeTruthy(): void; + /** + * Use .toBeUndefined to check that a variable is undefined. + */ + toBeUndefined(): void; + /** + * Use .toContain when you want to check that an item is in a list. For + * testing the items in the list, this uses ===, a strict equality check. + */ + toContain(item: any): void; + /** + * Use .toContainEqual when you want to check that an item is in a list. For + * testing the items in the list, this matcher recursively checks the + * equality of all fields, rather than checking for object identity. + */ + toContainEqual(item: any): void; + /** + * Use .toEqual when you want to check that two objects have the same value. + * This matcher recursively checks the equality of all fields, rather than + * checking for object identity. + */ + toEqual(value: any): void; + /** + * Use .toHaveBeenCalled to ensure that a mock function got called. + */ + toHaveBeenCalled(): void; + toBeCalled(): void; + /** + * Use .toHaveBeenCalledTimes to ensure that a mock function got called exact + * number of times. + */ + toHaveBeenCalledTimes(number: number): void; + toBeCalledTimes(number: number): void; + /** + * + */ + toHaveBeenNthCalledWith(nthCall: number, ...args: Array): void; + nthCalledWith(nthCall: number, ...args: Array): void; + /** + * + */ + toHaveReturned(): void; + toReturn(): void; + /** + * + */ + toHaveReturnedTimes(number: number): void; + toReturnTimes(number: number): void; + /** + * + */ + toHaveReturnedWith(value: any): void; + toReturnWith(value: any): void; + /** + * + */ + toHaveLastReturnedWith(value: any): void; + lastReturnedWith(value: any): void; + /** + * + */ + toHaveNthReturnedWith(nthCall: number, value: any): void; + nthReturnedWith(nthCall: number, value: any): void; + /** + * Use .toHaveBeenCalledWith to ensure that a mock function was called with + * specific arguments. + */ + toHaveBeenCalledWith(...args: Array): void; + toBeCalledWith(...args: Array): void; + /** + * Use .toHaveBeenLastCalledWith to ensure that a mock function was last called + * with specific arguments. + */ + toHaveBeenLastCalledWith(...args: Array): void; + lastCalledWith(...args: Array): void; + /** + * Check that an object has a .length property and it is set to a certain + * numeric value. + */ + toHaveLength(number: number): void; + /** + * + */ + toHaveProperty(propPath: string | $ReadOnlyArray, value?: any): void; + /** + * Use .toMatch to check that a string matches a regular expression or string. + */ + toMatch(regexpOrString: RegExp | string): void; + /** + * Use .toMatchObject to check that a javascript object matches a subset of the properties of an object. + */ + toMatchObject(object: Object | Array): void; + /** + * Use .toStrictEqual to check that a javascript object matches a subset of the properties of an object. + */ + toStrictEqual(value: any): void; + /** + * This ensures that an Object matches the most recent snapshot. + */ + toMatchSnapshot(propertyMatchers?: any, name?: string): void; + /** + * This ensures that an Object matches the most recent snapshot. + */ + toMatchSnapshot(name: string): void; + + toMatchInlineSnapshot(snapshot?: string): void; + toMatchInlineSnapshot(propertyMatchers?: any, snapshot?: string): void; + /** + * Use .toThrow to test that a function throws when it is called. + * If you want to test that a specific error gets thrown, you can provide an + * argument to toThrow. The argument can be a string for the error message, + * a class for the error, or a regex that should match the error. + * + * Alias: .toThrowError + */ + toThrow(message?: string | Error | Class | RegExp): void; + toThrowError(message?: string | Error | Class | RegExp): void; + /** + * Use .toThrowErrorMatchingSnapshot to test that a function throws a error + * matching the most recent snapshot when it is called. + */ + toThrowErrorMatchingSnapshot(): void; + toThrowErrorMatchingInlineSnapshot(snapshot?: string): void; +} + +type JestObjectType = { + /** + * Disables automatic mocking in the module loader. + * + * After this method is called, all `require()`s will return the real + * versions of each module (rather than a mocked version). + */ + disableAutomock(): JestObjectType, + /** + * An un-hoisted version of disableAutomock + */ + autoMockOff(): JestObjectType, + /** + * Enables automatic mocking in the module loader. + */ + enableAutomock(): JestObjectType, + /** + * An un-hoisted version of enableAutomock + */ + autoMockOn(): JestObjectType, + /** + * Clears the mock.calls and mock.instances properties of all mocks. + * Equivalent to calling .mockClear() on every mocked function. + */ + clearAllMocks(): JestObjectType, + /** + * Resets the state of all mocks. Equivalent to calling .mockReset() on every + * mocked function. + */ + resetAllMocks(): JestObjectType, + /** + * Restores all mocks back to their original value. + */ + restoreAllMocks(): JestObjectType, + /** + * Removes any pending timers from the timer system. + */ + clearAllTimers(): void, + /** + * Returns the number of fake timers still left to run. + */ + getTimerCount(): number, + /** + * The same as `mock` but not moved to the top of the expectation by + * babel-jest. + */ + doMock(moduleName: string, moduleFactory?: any): JestObjectType, + /** + * The same as `unmock` but not moved to the top of the expectation by + * babel-jest. + */ + dontMock(moduleName: string): JestObjectType, + /** + * Returns a new, unused mock function. Optionally takes a mock + * implementation. + */ + fn, TReturn>( + implementation?: (...args: TArguments) => TReturn + ): JestMockFn, + /** + * Determines if the given function is a mocked function. + */ + isMockFunction(fn: Function): boolean, + /** + * Given the name of a module, use the automatic mocking system to generate a + * mocked version of the module for you. + */ + genMockFromModule(moduleName: string): any, + /** + * Mocks a module with an auto-mocked version when it is being required. + * + * The second argument can be used to specify an explicit module factory that + * is being run instead of using Jest's automocking feature. + * + * The third argument can be used to create virtual mocks -- mocks of modules + * that don't exist anywhere in the system. + */ + mock( + moduleName: string, + moduleFactory?: any, + options?: Object + ): JestObjectType, + /** + * Returns the actual module instead of a mock, bypassing all checks on + * whether the module should receive a mock implementation or not. + */ + requireActual(moduleName: string): any, + /** + * Returns a mock module instead of the actual module, bypassing all checks + * on whether the module should be required normally or not. + */ + requireMock(moduleName: string): any, + /** + * Resets the module registry - the cache of all required modules. This is + * useful to isolate modules where local state might conflict between tests. + */ + resetModules(): JestObjectType, + + /** + * Creates a sandbox registry for the modules that are loaded inside the + * callback function. This is useful to isolate specific modules for every + * test so that local module state doesn't conflict between tests. + */ + isolateModules(fn: () => void): JestObjectType, + + /** + * Exhausts the micro-task queue (usually interfaced in node via + * process.nextTick). + */ + runAllTicks(): void, + /** + * Exhausts the macro-task queue (i.e., all tasks queued by setTimeout(), + * setInterval(), and setImmediate()). + */ + runAllTimers(): void, + /** + * Exhausts all tasks queued by setImmediate(). + */ + runAllImmediates(): void, + /** + * Executes only the macro task queue (i.e. all tasks queued by setTimeout() + * or setInterval() and setImmediate()). + */ + advanceTimersByTime(msToRun: number): void, + /** + * Executes only the macro task queue (i.e. all tasks queued by setTimeout() + * or setInterval() and setImmediate()). + * + * Renamed to `advanceTimersByTime`. + */ + runTimersToTime(msToRun: number): void, + /** + * Executes only the macro-tasks that are currently pending (i.e., only the + * tasks that have been queued by setTimeout() or setInterval() up to this + * point) + */ + runOnlyPendingTimers(): void, + /** + * Explicitly supplies the mock object that the module system should return + * for the specified module. Note: It is recommended to use jest.mock() + * instead. + */ + setMock(moduleName: string, moduleExports: any): JestObjectType, + /** + * Indicates that the module system should never return a mocked version of + * the specified module from require() (e.g. that it should always return the + * real module). + */ + unmock(moduleName: string): JestObjectType, + /** + * Instructs Jest to use fake versions of the standard timer functions + * (setTimeout, setInterval, clearTimeout, clearInterval, nextTick, + * setImmediate and clearImmediate). + */ + useFakeTimers(): JestObjectType, + /** + * Instructs Jest to use the real versions of the standard timer functions. + */ + useRealTimers(): JestObjectType, + /** + * Creates a mock function similar to jest.fn but also tracks calls to + * object[methodName]. + */ + spyOn( + object: Object, + methodName: string, + accessType?: 'get' | 'set' + ): JestMockFn, + /** + * Set the default timeout interval for tests and before/after hooks in milliseconds. + * Note: The default timeout interval is 5 seconds if this method is not called. + */ + setTimeout(timeout: number): JestObjectType, +}; + +type JestSpyType = { + calls: JestCallsType, +}; + +type JestDoneFn = { + (): void, + fail: (error: Error) => void, +}; + +/** Runs this function after every test inside this context */ +declare function afterEach( + fn: (done: JestDoneFn) => ?Promise, + timeout?: number +): void; +/** Runs this function before every test inside this context */ +declare function beforeEach( + fn: (done: JestDoneFn) => ?Promise, + timeout?: number +): void; +/** Runs this function after all tests have finished inside this context */ +declare function afterAll( + fn: (done: JestDoneFn) => ?Promise, + timeout?: number +): void; +/** Runs this function before any tests have started inside this context */ +declare function beforeAll( + fn: (done: JestDoneFn) => ?Promise, + timeout?: number +): void; + +/** A context for grouping tests together */ +declare var describe: { + /** + * Creates a block that groups together several related tests in one "test suite" + */ + (name: JestTestName, fn: () => void): void, + + /** + * Only run this describe block + */ + only(name: JestTestName, fn: () => void): void, + + /** + * Skip running this describe block + */ + skip(name: JestTestName, fn: () => void): void, + + /** + * each runs this test against array of argument arrays per each run + * + * @param {table} table of Test + */ + each( + ...table: Array | mixed> | [Array, string] + ): ( + name: JestTestName, + fn?: (...args: Array) => ?Promise, + timeout?: number + ) => void, +}; + +/** An individual test unit */ +declare var it: { + /** + * An individual test unit + * + * @param {JestTestName} Name of Test + * @param {Function} Test + * @param {number} Timeout for the test, in milliseconds. + */ + ( + name: JestTestName, + fn?: (done: JestDoneFn) => ?Promise, + timeout?: number + ): void, + + /** + * Only run this test + * + * @param {JestTestName} Name of Test + * @param {Function} Test + * @param {number} Timeout for the test, in milliseconds. + */ + only( + name: JestTestName, + fn?: (done: JestDoneFn) => ?Promise, + timeout?: number + ): { + each( + ...table: Array | mixed> | [Array, string] + ): ( + name: JestTestName, + fn?: (...args: Array) => ?Promise, + timeout?: number + ) => void, + }, + + /** + * Skip running this test + * + * @param {JestTestName} Name of Test + * @param {Function} Test + * @param {number} Timeout for the test, in milliseconds. + */ + skip( + name: JestTestName, + fn?: (done: JestDoneFn) => ?Promise, + timeout?: number + ): void, + + /** + * Highlight planned tests in the summary output + * + * @param {String} Name of Test to do + */ + todo(name: string): void, + + /** + * Run the test concurrently + * + * @param {JestTestName} Name of Test + * @param {Function} Test + * @param {number} Timeout for the test, in milliseconds. + */ + concurrent( + name: JestTestName, + fn?: (done: JestDoneFn) => ?Promise, + timeout?: number + ): void, + + /** + * each runs this test against array of argument arrays per each run + * + * @param {table} table of Test + */ + each( + ...table: Array | mixed> | [Array, string] + ): ( + name: JestTestName, + fn?: (...args: Array) => ?Promise, + timeout?: number + ) => void, +}; + +declare function fit( + name: JestTestName, + fn: (done: JestDoneFn) => ?Promise, + timeout?: number +): void; +/** An individual test unit */ +declare var test: typeof it; +/** A disabled group of tests */ +declare var xdescribe: typeof describe; +/** A focused group of tests */ +declare var fdescribe: typeof describe; +/** A disabled individual test */ +declare var xit: typeof it; +/** A disabled individual test */ +declare var xtest: typeof it; + +type JestPrettyFormatColors = { + comment: { close: string, open: string }, + content: { close: string, open: string }, + prop: { close: string, open: string }, + tag: { close: string, open: string }, + value: { close: string, open: string }, +}; + +type JestPrettyFormatIndent = string => string; +type JestPrettyFormatRefs = Array; +type JestPrettyFormatPrint = any => string; +type JestPrettyFormatStringOrNull = string | null; + +type JestPrettyFormatOptions = {| + callToJSON: boolean, + edgeSpacing: string, + escapeRegex: boolean, + highlight: boolean, + indent: number, + maxDepth: number, + min: boolean, + plugins: JestPrettyFormatPlugins, + printFunctionName: boolean, + spacing: string, + theme: {| + comment: string, + content: string, + prop: string, + tag: string, + value: string, + |}, +|}; + +type JestPrettyFormatPlugin = { + print: ( + val: any, + serialize: JestPrettyFormatPrint, + indent: JestPrettyFormatIndent, + opts: JestPrettyFormatOptions, + colors: JestPrettyFormatColors + ) => string, + test: any => boolean, +}; + +type JestPrettyFormatPlugins = Array; + +/** The expect function is used every time you want to test a value */ +declare var expect: { + /** The object that you want to make assertions against */ + ( + value: any + ): JestExpectType & + JestPromiseType & + EnzymeMatchersType & + DomTestingLibraryType & + JestJQueryMatchersType & + JestStyledComponentsMatchersType & + JestExtendedMatchersType, + + /** Add additional Jasmine matchers to Jest's roster */ + extend(matchers: { [name: string]: JestMatcher }): void, + /** Add a module that formats application-specific data structures. */ + addSnapshotSerializer(pluginModule: JestPrettyFormatPlugin): void, + assertions(expectedAssertions: number): void, + hasAssertions(): void, + any(value: mixed): JestAsymmetricEqualityType, + anything(): any, + arrayContaining(value: Array): Array, + objectContaining(value: Object): Object, + /** Matches any received string that contains the exact expected string. */ + stringContaining(value: string): string, + stringMatching(value: string | RegExp): string, + not: { + arrayContaining: (value: $ReadOnlyArray) => Array, + objectContaining: (value: {}) => Object, + stringContaining: (value: string) => string, + stringMatching: (value: string | RegExp) => string, + }, +}; + +// TODO handle return type +// http://jasmine.github.io/2.4/introduction.html#section-Spies +declare function spyOn(value: mixed, method: string): Object; + +/** Holds all functions related to manipulating test runner */ +declare var jest: JestObjectType; + +/** + * The global Jasmine object, this is generally not exposed as the public API, + * using features inside here could break in later versions of Jest. + */ +declare var jasmine: { + DEFAULT_TIMEOUT_INTERVAL: number, + any(value: mixed): JestAsymmetricEqualityType, + anything(): any, + arrayContaining(value: Array): Array, + clock(): JestClockType, + createSpy(name: string): JestSpyType, + createSpyObj( + baseName: string, + methodNames: Array + ): { [methodName: string]: JestSpyType }, + objectContaining(value: Object): Object, + stringMatching(value: string): string, +}; diff --git a/package.json b/package.json index f57011ab..d93727ce 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "mkdirp": "^0.5.1", "pako": "^1.0.8", "postcss-loader": "^3.0.0", + "prettier": "^1.18.2", "query-string": "^6.2.0", "rimraf": "^2.6.3", "rollup": "^1.2.2", diff --git a/src/functions/mclass.js b/src/functions/mclass.js index af18d7df..bb66c221 100644 --- a/src/functions/mclass.js +++ b/src/functions/mclass.js @@ -71,7 +71,7 @@ defineFunction({ return { type: "mclass", mode: parser.mode, - mclass: "m" + funcName.substr(5), + mclass: "m" + funcName.substr(5), // TODO(kevinb): don't prefix with 'm' body: ordargument(body), isCharacterBox: utils.isCharacterBox(body), }; diff --git a/src/unicodeMake.js b/src/unicodeMake.js index a361c815..bf80bb35 100644 --- a/src/unicodeMake.js +++ b/src/unicodeMake.js @@ -11,7 +11,7 @@ const target = path.join(__dirname, 'unicodeSymbols.js'); const targetMtime = fs.statSync(target).mtime; if (fs.statSync(__filename).mtime <= targetMtime && fs.statSync( path.join(__dirname, 'unicodeAccents.js')).mtime <= targetMtime) { - return; + process.exit(0); } require('@babel/register'); diff --git a/wallaby.js b/wallaby.js new file mode 100644 index 00000000..6b7c5d75 --- /dev/null +++ b/wallaby.js @@ -0,0 +1,39 @@ +const babelConfig = require("./babel.config.js"); + +module.exports = function(wallaby) { + const tests = [ + "test/*-spec.js", + "contrib/**/test/*-spec.js", + ]; + + return { + tests, + + // Wallaby needs to know about all files that may be loaded because + // of running a test. + files: [ + "src/**/*.js", + "test/**/*.js", + "contrib/**/*.js", + "submodules/**/*.js", + "katex.js", + + // These paths are excluded. + ...tests.map((test) => `!${test}`), + ], + + // Wallaby does its own compilation of .js files to support its + // advanced logging features. Wallaby can't parse the flow and + // JSX in our source files so need to provide a babel configuration. + compilers: { + "**/*.js": wallaby.compilers.babel(babelConfig), + }, + + env: { + type: "node", + runner: "node", + }, + + testFramework: "jest", + }; +}; diff --git a/webpack.common.js b/webpack.common.js index 4ee564a2..2decbabc 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -45,6 +45,10 @@ const targets /*: Array */ = [ name: 'contrib/mathtex-script-type', entry: './contrib/mathtex-script-type/mathtex-script-type.js', }, + { + name: 'contrib/render-a11y-string', + entry: './contrib/render-a11y-string/render-a11y-string.js', + }, ]; /** diff --git a/yarn.lock b/yarn.lock index f0576ee2..79a6841c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6673,6 +6673,11 @@ prelude-ls@~1.1.2: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= +prettier@^1.18.2: + version "1.18.2" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.18.2.tgz#6823e7c5900017b4bd3acf46fe9ac4b4d7bda9ea" + integrity sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw== + pretty-format@^24.0.0: version "24.0.0" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-24.0.0.tgz#cb6599fd73ac088e37ed682f61291e4678f48591"