You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
800 lines
20 KiB
800 lines
20 KiB
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 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; |
|
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 };
|
|
|