747 lines
22 KiB
JavaScript
747 lines
22 KiB
JavaScript
|
// @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<T> = Array<T | NestedArray<T>>;
|
||
|
|
||
|
const buildString = (
|
||
|
str: string,
|
||
|
type: Atom | "normal",
|
||
|
a11yStrings: NestedArray<string>,
|
||
|
) => {
|
||
|
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<string>,
|
||
|
callback: (regionStrings: NestedArray<string>) => void,
|
||
|
) => {
|
||
|
const regionStrings: NestedArray<string> = [];
|
||
|
a11yStrings.push(regionStrings);
|
||
|
callback(regionStrings);
|
||
|
};
|
||
|
|
||
|
const handleObject = (
|
||
|
tree: AnyParseNode,
|
||
|
a11yStrings: NestedArray<string>,
|
||
|
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 "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": {
|
||
|
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 "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) => {
|
||
|
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;
|
||
|
} 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.
|
||
|
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 style we're using, e.g. display, 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: empty);
|
||
|
throw new Error("KaTeX a11y un-recognized type: " + tree.type);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
const buildA11yStrings = (
|
||
|
tree: AnyParseNode | AnyParseNode[],
|
||
|
a11yStrings: NestedArray<string> = [],
|
||
|
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,
|
||
|
): string {
|
||
|
|
||
|
const tree = katex.__parse(text, settings);
|
||
|
const a11yStrings = buildA11yStrings(tree, [], "normal");
|
||
|
return flatten(a11yStrings).join(", ");
|
||
|
};
|
||
|
|
||
|
export default renderA11yString;
|