UNPKG

react-components

Version:

React components used by Khan Academy

491 lines (420 loc) 14.4 kB
/** * KaTeX A11y * A library for converting KaTeX math into readable strings. */ /* global 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", }; const powerMap = { "\\prime": "prime", "\\degree": "degree", "\\circ": "degree", }; 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", "\\neq": "does not equal", "\\ne": "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 buildString = function(str, type, a11yStrings) { 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 nothing was found and it's not a plain string or number if (ret === str && !/^\w+$/.test(str)) { // This is likely a case that we'll need to handle throw new Error("KaTeX a11y " + type + " string not found: " + 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 && /^\d+$/.test(a11yStrings[a11yStrings.length - 1])) { a11yStrings[a11yStrings.length - 1] += ret; } else if (ret) { a11yStrings.push(ret); } }; const buildRegion = function(a11yStrings, callback) { const region = []; a11yStrings.push(region); callback(region); }; const typeHandlers = { accent: function(tree, a11yStrings) { buildRegion(a11yStrings, function(a11yStrings) { buildA11yStrings(tree.value.base, a11yStrings); a11yStrings.push("with"); buildA11yStrings(tree.value.accent, a11yStrings); a11yStrings.push("on top"); }); }, bin: function(tree, a11yStrings) { buildString(tree.value, "bin", a11yStrings); }, close: function(tree, a11yStrings) { buildString(tree.value, "close", a11yStrings); }, color: function(tree, a11yStrings) { const color = tree.value.color.replace(/katex-/, ""); buildRegion(a11yStrings, function(a11yStrings) { a11yStrings.push("start color " + color); buildA11yStrings(tree.value.value, a11yStrings); a11yStrings.push("end color " + color); }); }, delimsizing: function(tree, a11yStrings) { if (tree.value.value && tree.value.value !== ".") { buildString(tree.value.value, "normal", a11yStrings); } }, genfrac: function(tree, a11yStrings) { buildRegion(a11yStrings, function(a11yStrings) { // NOTE: Not sure if this is a safe assumption // hasBarLine true -> fraction, false -> binomial if (tree.value.hasBarLine) { a11yStrings.push("start fraction"); buildString(tree.value.leftDelim, "open", a11yStrings); buildA11yStrings(tree.value.numer, a11yStrings); a11yStrings.push("divided by"); buildA11yStrings(tree.value.denom, a11yStrings); buildString(tree.value.rightDelim, "close", a11yStrings); a11yStrings.push("end fraction"); } else { a11yStrings.push("start binomial"); buildString(tree.value.leftDelim, "open", a11yStrings); buildA11yStrings(tree.value.numer, a11yStrings); a11yStrings.push("over"); buildA11yStrings(tree.value.denom, a11yStrings); buildString(tree.value.rightDelim, "close", a11yStrings); a11yStrings.push("end binomial"); } }); }, // inner katex: function(tree, a11yStrings) { a11yStrings.push("KaTeX"); }, leftright: function(tree, a11yStrings) { buildRegion(a11yStrings, function(a11yStrings) { buildString(tree.value.left, "open", a11yStrings); buildA11yStrings(tree.value.body, a11yStrings); buildString(tree.value.right, "close", a11yStrings); }); }, llap: function(tree, a11yStrings) { buildA11yStrings(tree.value.body, a11yStrings); }, mathord: function(tree, a11yStrings) { buildA11yStrings(tree.value, a11yStrings); }, op: function(tree, a11yStrings) { buildString(tree.value.body, "normal", a11yStrings); }, open: function(tree, a11yStrings) { buildString(tree.value, "open", a11yStrings); }, ordgroup: function(tree, a11yStrings) { buildA11yStrings(tree.value, a11yStrings); }, overline: function(tree, a11yStrings) { buildRegion(a11yStrings, function(a11yStrings) { a11yStrings.push("start overline"); buildA11yStrings(tree.value.body, a11yStrings); a11yStrings.push("end overline"); }); }, phantom: function(tree, a11yStrings) { a11yStrings.push("empty space"); }, punct: function(tree, a11yStrings) { buildString(tree.value, "punct", a11yStrings); }, rel: function(tree, a11yStrings) { buildString(tree.value, "rel", a11yStrings); }, rlap: function(tree, a11yStrings) { buildA11yStrings(tree.value.body, a11yStrings); }, rule: function(tree, a11yStrings) { // NOTE: Is there something more useful that we can put here? a11yStrings.push("rule"); }, sizing: function(tree, a11yStrings) { buildA11yStrings(tree.value.value, a11yStrings); }, spacing: function(tree, a11yStrings) { a11yStrings.push("space"); }, styling: function(tree, a11yStrings) { // We ignore the styling and just pass through the contents buildA11yStrings(tree.value.value, a11yStrings); }, sqrt: function(tree, a11yStrings) { buildRegion(a11yStrings, function(a11yStrings) { if (tree.value.index) { a11yStrings.push("root"); a11yStrings.push("start index"); buildA11yStrings(tree.value.index, a11yStrings); a11yStrings.push("end index"); } a11yStrings.push("square root of"); buildA11yStrings(tree.value.body, a11yStrings); a11yStrings.push("end square root"); }); }, supsub: function(tree, a11yStrings) { if (tree.value.base) { buildA11yStrings(tree.value.base, a11yStrings); } if (tree.value.sub) { buildRegion(a11yStrings, function(a11yStrings) { a11yStrings.push("start subscript"); buildA11yStrings(tree.value.sub, a11yStrings); a11yStrings.push("end subscript"); }); } const sup = tree.value.sup; if (sup) { // There are some cases that just read better if we don't have // the extra start/end baggage, so we skip the extra text let newPower = powerMap[sup]; const supValue = sup.value; // The value stored inside the sup property is not always // consistent. It could be a string (handled above), an object // with a string property in value, or an array of objects that // have a value property. if (!newPower && supValue) { // If supValue is an object and it has a length of 1 we assume // it's an array that has only a single item in it. This is the // case that we care about and we only check that one value. if (typeof supValue === "object" && supValue.length === 1) { newPower = powerMap[supValue[0].value]; // This is the case where it's a string in the value property } else { newPower = powerMap[supValue]; } } buildRegion(a11yStrings, function(a11yStrings) { if (newPower) { a11yStrings.push(newPower); return; } a11yStrings.push("start superscript"); buildA11yStrings(tree.value.sup, a11yStrings); a11yStrings.push("end superscript"); }); } }, text: function(tree, a11yStrings) { if (typeof tree.value !== "string") { buildA11yStrings(tree.value.body, a11yStrings); } else { buildString(tree, "normal", a11yStrings); } }, textord: function(tree, a11yStrings) { buildA11yStrings(tree.value, a11yStrings); }, }; const buildA11yStrings = function(tree, a11yStrings) { a11yStrings = a11yStrings || []; // Handle strings if (typeof tree === "string") { buildString(tree, "normal", a11yStrings); // Handle arrays } else if (tree.constructor === Array) { for (let i = 0; i < tree.length; i++) { buildA11yStrings(tree[i], a11yStrings); } // Everything else is assumed to be an object... } else { if (!tree.type || !(tree.type in typeHandlers)) { throw new Error("KaTeX a11y un-recognized type: " + tree.type); } else { typeHandlers[tree.type](tree, a11yStrings); } } return a11yStrings; }; const renderStrings = function(a11yStrings, a11yNode) { const doc = a11yNode.ownerDocument; for (let i = 0; i < a11yStrings.length; i++) { const a11yString = a11yStrings[i]; if (i > 0) { // Note: We insert commas in (not just spaces) to provide // screen readers with some "breathing room". When inserting the // commas the screen reader knows to pause slightly and it provides // an overall better listening experience. a11yNode.appendChild(doc.createTextNode(", ")); } if (typeof a11yString === "string") { a11yNode.appendChild(doc.createTextNode(a11yString)); } else { const newBaseNode = doc.createElement("span"); // NOTE(jeresig): We may want to add in a tabIndex property // to the node here, in order to support keyboard navigation. a11yNode.appendChild(newBaseNode); renderStrings(a11yString, newBaseNode); } } }; const flattenStrings = function(a11yStrings, results) { if (!results) { results = []; } for (let i = 0; i < a11yStrings.length; i++) { const a11yString = a11yStrings[i]; if (typeof a11yString === "string") { results.push(a11yString); } else { flattenStrings(a11yString, results); } } return results; }; const parseMath = function(text) { // NOTE: `katex` is a global, should be included using require return katex.__parse(text); }; const render = function(text, a11yNode) { const tree = parseMath(text); const a11yStrings = buildA11yStrings(tree); renderStrings(a11yStrings, a11yNode); }; const flatten = function(array) { let result = []; for (const item of array) { if (Array.isArray(item)) { result = result.concat(flatten(item)); } else { result.push(item); } } return result; }; const renderString = function(text) { const tree = parseMath(text); const a11yStrings = buildA11yStrings(tree); return flatten(a11yStrings).join(", "); }; if (typeof module !== "undefined") { module.exports = { render: render, renderString: renderString, parseMath: parseMath, }; } else { this.katexA11yRender = render; }