754 lines
		
	
	
		
			19 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
		
		
			
		
	
	
			754 lines
		
	
	
		
			19 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
|   | 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. | ||
|  |  */ | ||
|  | 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" | ||
|  | }; | ||
|  | 
 | ||
|  | const buildString = (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 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, callback) => { | ||
|  |   const regionStrings = []; | ||
|  |   a11yStrings.push(regionStrings); | ||
|  |   callback(regionStrings); | ||
|  | }; | ||
|  | 
 | ||
|  | const 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": | ||
|  |       { | ||
|  |         const text = tree.text; | ||
|  | 
 | ||
|  |         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": | ||
|  |       { | ||
|  |         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 = tree.leftDelim, | ||
|  |                 rightDelim = tree.rightDelim; // 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 = tree.body, | ||
|  |               name = tree.name; | ||
|  | 
 | ||
|  |         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 = tree.body, | ||
|  |                 index = tree.index; | ||
|  | 
 | ||
|  |           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 = tree.base, | ||
|  |               sub = tree.sub, | ||
|  |               sup = tree.sup; | ||
|  |         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 "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; | ||
|  |       } | ||
|  | 
 | ||
|  |     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); | ||
|  |   } | ||
|  | }; | ||
|  | 
 | ||
|  | const buildA11yStrings = function buildA11yStrings(tree, a11yStrings, atomType) { | ||
|  |   if (a11yStrings === void 0) { | ||
|  |     a11yStrings = []; | ||
|  |   } | ||
|  | 
 | ||
|  |   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 flatten(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 renderA11yString(text, settings) { | ||
|  |   const tree = katex.__parse(text, settings); | ||
|  | 
 | ||
|  |   const a11yStrings = buildA11yStrings(tree, [], "normal"); | ||
|  |   return flatten(a11yStrings).join(", "); | ||
|  | }; | ||
|  | 
 | ||
|  | export default renderA11yString; |