123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800 |
- import katex from '../katex.mjs';
- /**
- * 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.
- */
- var 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"
- };
- var powerMap = {
- "prime": "prime",
- "degree": "degrees",
- "circle": "degrees",
- "2": "squared",
- "3": "cubed"
- };
- var openMap = {
- "|": "open vertical bar",
- ".": ""
- };
- var closeMap = {
- "|": "close vertical bar",
- ".": ""
- };
- var binMap = {
- "+": "plus",
- "-": "minus",
- "\\pm": "plus minus",
- "\\cdot": "dot",
- "*": "times",
- "/": "divided by",
- "\\times": "times",
- "\\div": "divided by",
- "\\circ": "circle",
- "\\bullet": "bullet"
- };
- var 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"
- };
- var accentUnderMap = {
- "\\underleftarrow": "left arrow",
- "\\underrightarrow": "right arrow",
- "\\underleftrightarrow": "left-right arrow",
- "\\undergroup": "group",
- "\\underlinesegment": "line segment",
- "\\utilde": "tilde"
- };
- var buildString = (str, type, a11yStrings) => {
- if (!str) {
- return;
- }
- var 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);
- }
- };
- var buildRegion = (a11yStrings, callback) => {
- var regionStrings = [];
- a11yStrings.push(regionStrings);
- callback(regionStrings);
- };
- var handleObject = (tree, a11yStrings, atomType) => {
- // 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":
- {
- var {
- 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;
- throw new Error("\"" + tree.family + "\" is not a valid atom type");
- }
- }
- break;
- }
- case "color":
- {
- var 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
- var {
- 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 "hbox":
- {
- buildA11yStrings(tree.body, a11yStrings, atomType);
- 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":
- {
- var {
- 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 "pmb":
- {
- a11yStrings.push("bold");
- 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 => {
- var {
- body,
- index
- } = tree;
- if (index) {
- var 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":
- {
- var {
- base,
- sub,
- sup
- } = tree;
- var isLog = false;
- if (base) {
- buildA11yStrings(base, a11yStrings, atomType);
- isLog = base.type === "op" && base.name === "\\log";
- }
- if (sub) {
- var 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) {
- var 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;
- } else if (/phase/.test(tree.label)) {
- buildRegion(a11yStrings, function (regionStrings) {
- regionStrings.push("start phase angle");
- buildA11yStrings(tree.body, regionStrings, atomType);
- regionStrings.push("end phase angle");
- });
- break;
- }
- throw new Error("KaTeX-a11y: enclose node with " + tree.label + " not supported yet");
- }
- case "vcenter":
- {
- buildA11yStrings(tree.body, a11yStrings, atomType);
- break;
- }
- 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 "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 "cdlabel":
- {
- throw new Error("KaTeX-a11y: cdlabel not implemented yet");
- }
- case "cdlabelparent":
- {
- throw new Error("KaTeX-a11y: cdlabelparent 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.
- var _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;
- }
- case "internal":
- {
- // internal nodes are never included in the parse tree
- break;
- }
- case "html":
- {
- buildA11yStrings(tree.body, a11yStrings, atomType);
- break;
- }
- default:
- tree.type;
- throw new Error("KaTeX a11y un-recognized type: " + tree.type);
- }
- };
- var buildA11yStrings = function buildA11yStrings(tree, a11yStrings, atomType) {
- if (a11yStrings === void 0) {
- a11yStrings = [];
- }
- if (tree instanceof Array) {
- for (var i = 0; i < tree.length; i++) {
- buildA11yStrings(tree[i], a11yStrings, atomType);
- }
- } else {
- handleObject(tree, a11yStrings, atomType);
- }
- return a11yStrings;
- };
- var flatten = function flatten(array) {
- var result = [];
- array.forEach(function (item) {
- if (item instanceof Array) {
- result = result.concat(flatten(item));
- } else {
- result.push(item);
- }
- });
- return result;
- };
- var renderA11yString = function renderA11yString(text, settings) {
- var tree = katex.__parse(text, settings);
- var a11yStrings = buildA11yStrings(tree, [], "normal");
- return flatten(a11yStrings).join(", ");
- };
- export { renderA11yString as default };
|