arnaucube
arnaucube/arnaucube.comPublic Self-hosted repository
arnaucube.com/blog/js/katex/contrib/render-a11y-string.mjs
blog/js/katex/contrib/render-a11y-string.mjsRaw
  1import katex from '../katex.mjs';
  2
  3/**
  4 * renderA11yString returns a readable string.
  5 *
  6 * In some cases the string will have the proper semantic math
  7 * meaning,:
  8 *   renderA11yString("\\frac{1}{2}"")
  9 *   -> "start fraction, 1, divided by, 2, end fraction"
 10 *
 11 * However, other cases do not:
 12 *   renderA11yString("f(x) = x^2")
 13 *   -> "f, left parenthesis, x, right parenthesis, equals, x, squared"
 14 *
 15 * The commas in the string aim to increase ease of understanding
 16 * when read by a screenreader.
 17 */
 18var stringMap = {
 19  "(": "left parenthesis",
 20  ")": "right parenthesis",
 21  "[": "open bracket",
 22  "]": "close bracket",
 23  "\\{": "left brace",
 24  "\\}": "right brace",
 25  "\\lvert": "open vertical bar",
 26  "\\rvert": "close vertical bar",
 27  "|": "vertical bar",
 28  "\\uparrow": "up arrow",
 29  "\\Uparrow": "up arrow",
 30  "\\downarrow": "down arrow",
 31  "\\Downarrow": "down arrow",
 32  "\\updownarrow": "up down arrow",
 33  "\\leftarrow": "left arrow",
 34  "\\Leftarrow": "left arrow",
 35  "\\rightarrow": "right arrow",
 36  "\\Rightarrow": "right arrow",
 37  "\\langle": "open angle",
 38  "\\rangle": "close angle",
 39  "\\lfloor": "open floor",
 40  "\\rfloor": "close floor",
 41  "\\int": "integral",
 42  "\\intop": "integral",
 43  "\\lim": "limit",
 44  "\\ln": "natural log",
 45  "\\log": "log",
 46  "\\sin": "sine",
 47  "\\cos": "cosine",
 48  "\\tan": "tangent",
 49  "\\cot": "cotangent",
 50  "\\sum": "sum",
 51  "/": "slash",
 52  ",": "comma",
 53  ".": "point",
 54  "-": "negative",
 55  "+": "plus",
 56  "~": "tilde",
 57  ":": "colon",
 58  "?": "question mark",
 59  "'": "apostrophe",
 60  "\\%": "percent",
 61  " ": "space",
 62  "\\ ": "space",
 63  "\\$": "dollar sign",
 64  "\\angle": "angle",
 65  "\\degree": "degree",
 66  "\\circ": "circle",
 67  "\\vec": "vector",
 68  "\\triangle": "triangle",
 69  "\\pi": "pi",
 70  "\\prime": "prime",
 71  "\\infty": "infinity",
 72  "\\alpha": "alpha",
 73  "\\beta": "beta",
 74  "\\gamma": "gamma",
 75  "\\omega": "omega",
 76  "\\theta": "theta",
 77  "\\sigma": "sigma",
 78  "\\lambda": "lambda",
 79  "\\tau": "tau",
 80  "\\Delta": "delta",
 81  "\\delta": "delta",
 82  "\\mu": "mu",
 83  "\\rho": "rho",
 84  "\\nabla": "del",
 85  "\\ell": "ell",
 86  "\\ldots": "dots",
 87  // TODO: add entries for all accents
 88  "\\hat": "hat",
 89  "\\acute": "acute"
 90};
 91var powerMap = {
 92  "prime": "prime",
 93  "degree": "degrees",
 94  "circle": "degrees",
 95  "2": "squared",
 96  "3": "cubed"
 97};
 98var openMap = {
 99  "|": "open vertical bar",
100  ".": ""
101};
102var closeMap = {
103  "|": "close vertical bar",
104  ".": ""
105};
106var binMap = {
107  "+": "plus",
108  "-": "minus",
109  "\\pm": "plus minus",
110  "\\cdot": "dot",
111  "*": "times",
112  "/": "divided by",
113  "\\times": "times",
114  "\\div": "divided by",
115  "\\circ": "circle",
116  "\\bullet": "bullet"
117};
118var relMap = {
119  "=": "equals",
120  "\\approx": "approximately equals",
121  "≠": "does not equal",
122  "\\geq": "is greater than or equal to",
123  "\\ge": "is greater than or equal to",
124  "\\leq": "is less than or equal to",
125  "\\le": "is less than or equal to",
126  ">": "is greater than",
127  "<": "is less than",
128  "\\leftarrow": "left arrow",
129  "\\Leftarrow": "left arrow",
130  "\\rightarrow": "right arrow",
131  "\\Rightarrow": "right arrow",
132  ":": "colon"
133};
134var accentUnderMap = {
135  "\\underleftarrow": "left arrow",
136  "\\underrightarrow": "right arrow",
137  "\\underleftrightarrow": "left-right arrow",
138  "\\undergroup": "group",
139  "\\underlinesegment": "line segment",
140  "\\utilde": "tilde"
141};
142
143var buildString = (str, type, a11yStrings) => {
144  if (!str) {
145    return;
146  }
147
148  var ret;
149
150  if (type === "open") {
151    ret = str in openMap ? openMap[str] : stringMap[str] || str;
152  } else if (type === "close") {
153    ret = str in closeMap ? closeMap[str] : stringMap[str] || str;
154  } else if (type === "bin") {
155    ret = binMap[str] || str;
156  } else if (type === "rel") {
157    ret = relMap[str] || str;
158  } else {
159    ret = stringMap[str] || str;
160  } // If the text to add is a number and there is already a string
161  // in the list and the last string is a number then we should
162  // combine them into a single number
163
164
165  if (/^\d+$/.test(ret) && a11yStrings.length > 0 && // TODO(kevinb): check that the last item in a11yStrings is a string
166  // I think we might be able to drop the nested arrays, which would make
167  // this easier to type
168  // $FlowFixMe
169  /^\d+$/.test(a11yStrings[a11yStrings.length - 1])) {
170    a11yStrings[a11yStrings.length - 1] += ret;
171  } else if (ret) {
172    a11yStrings.push(ret);
173  }
174};
175
176var buildRegion = (a11yStrings, callback) => {
177  var regionStrings = [];
178  a11yStrings.push(regionStrings);
179  callback(regionStrings);
180};
181
182var handleObject = (tree, a11yStrings, atomType) => {
183  // Everything else is assumed to be an object...
184  switch (tree.type) {
185    case "accent":
186      {
187        buildRegion(a11yStrings, a11yStrings => {
188          buildA11yStrings(tree.base, a11yStrings, atomType);
189          a11yStrings.push("with");
190          buildString(tree.label, "normal", a11yStrings);
191          a11yStrings.push("on top");
192        });
193        break;
194      }
195
196    case "accentUnder":
197      {
198        buildRegion(a11yStrings, a11yStrings => {
199          buildA11yStrings(tree.base, a11yStrings, atomType);
200          a11yStrings.push("with");
201          buildString(accentUnderMap[tree.label], "normal", a11yStrings);
202          a11yStrings.push("underneath");
203        });
204        break;
205      }
206
207    case "accent-token":
208      {
209        // Used internally by accent symbols.
210        break;
211      }
212
213    case "atom":
214      {
215        var {
216          text
217        } = tree;
218
219        switch (tree.family) {
220          case "bin":
221            {
222              buildString(text, "bin", a11yStrings);
223              break;
224            }
225
226          case "close":
227            {
228              buildString(text, "close", a11yStrings);
229              break;
230            }
231          // TODO(kevinb): figure out what should be done for inner
232
233          case "inner":
234            {
235              buildString(tree.text, "inner", a11yStrings);
236              break;
237            }
238
239          case "open":
240            {
241              buildString(text, "open", a11yStrings);
242              break;
243            }
244
245          case "punct":
246            {
247              buildString(text, "punct", a11yStrings);
248              break;
249            }
250
251          case "rel":
252            {
253              buildString(text, "rel", a11yStrings);
254              break;
255            }
256
257          default:
258            {
259              tree.family;
260              throw new Error("\"" + tree.family + "\" is not a valid atom type");
261            }
262        }
263
264        break;
265      }
266
267    case "color":
268      {
269        var color = tree.color.replace(/katex-/, "");
270        buildRegion(a11yStrings, regionStrings => {
271          regionStrings.push("start color " + color);
272          buildA11yStrings(tree.body, regionStrings, atomType);
273          regionStrings.push("end color " + color);
274        });
275        break;
276      }
277
278    case "color-token":
279      {
280        // Used by \color, \colorbox, and \fcolorbox but not directly rendered.
281        // It's a leaf node and has no children so just break.
282        break;
283      }
284
285    case "delimsizing":
286      {
287        if (tree.delim && tree.delim !== ".") {
288          buildString(tree.delim, "normal", a11yStrings);
289        }
290
291        break;
292      }
293
294    case "genfrac":
295      {
296        buildRegion(a11yStrings, regionStrings => {
297          // genfrac can have unbalanced delimiters
298          var {
299            leftDelim,
300            rightDelim
301          } = tree; // NOTE: Not sure if this is a safe assumption
302          // hasBarLine true -> fraction, false -> binomial
303
304          if (tree.hasBarLine) {
305            regionStrings.push("start fraction");
306            leftDelim && buildString(leftDelim, "open", regionStrings);
307            buildA11yStrings(tree.numer, regionStrings, atomType);
308            regionStrings.push("divided by");
309            buildA11yStrings(tree.denom, regionStrings, atomType);
310            rightDelim && buildString(rightDelim, "close", regionStrings);
311            regionStrings.push("end fraction");
312          } else {
313            regionStrings.push("start binomial");
314            leftDelim && buildString(leftDelim, "open", regionStrings);
315            buildA11yStrings(tree.numer, regionStrings, atomType);
316            regionStrings.push("over");
317            buildA11yStrings(tree.denom, regionStrings, atomType);
318            rightDelim && buildString(rightDelim, "close", regionStrings);
319            regionStrings.push("end binomial");
320          }
321        });
322        break;
323      }
324
325    case "hbox":
326      {
327        buildA11yStrings(tree.body, a11yStrings, atomType);
328        break;
329      }
330
331    case "kern":
332      {
333        // No op: we don't attempt to present kerning information
334        // to the screen reader.
335        break;
336      }
337
338    case "leftright":
339      {
340        buildRegion(a11yStrings, regionStrings => {
341          buildString(tree.left, "open", regionStrings);
342          buildA11yStrings(tree.body, regionStrings, atomType);
343          buildString(tree.right, "close", regionStrings);
344        });
345        break;
346      }
347
348    case "leftright-right":
349      {
350        // TODO: double check that this is a no-op
351        break;
352      }
353
354    case "lap":
355      {
356        buildA11yStrings(tree.body, a11yStrings, atomType);
357        break;
358      }
359
360    case "mathord":
361      {
362        buildString(tree.text, "normal", a11yStrings);
363        break;
364      }
365
366    case "op":
367      {
368        var {
369          body,
370          name
371        } = tree;
372
373        if (body) {
374          buildA11yStrings(body, a11yStrings, atomType);
375        } else if (name) {
376          buildString(name, "normal", a11yStrings);
377        }
378
379        break;
380      }
381
382    case "op-token":
383      {
384        // Used internally by operator symbols.
385        buildString(tree.text, atomType, a11yStrings);
386        break;
387      }
388
389    case "ordgroup":
390      {
391        buildA11yStrings(tree.body, a11yStrings, atomType);
392        break;
393      }
394
395    case "overline":
396      {
397        buildRegion(a11yStrings, function (a11yStrings) {
398          a11yStrings.push("start overline");
399          buildA11yStrings(tree.body, a11yStrings, atomType);
400          a11yStrings.push("end overline");
401        });
402        break;
403      }
404
405    case "phantom":
406      {
407        a11yStrings.push("empty space");
408        break;
409      }
410
411    case "raisebox":
412      {
413        buildA11yStrings(tree.body, a11yStrings, atomType);
414        break;
415      }
416
417    case "rule":
418      {
419        a11yStrings.push("rectangle");
420        break;
421      }
422
423    case "sizing":
424      {
425        buildA11yStrings(tree.body, a11yStrings, atomType);
426        break;
427      }
428
429    case "spacing":
430      {
431        a11yStrings.push("space");
432        break;
433      }
434
435    case "styling":
436      {
437        // We ignore the styling and just pass through the contents
438        buildA11yStrings(tree.body, a11yStrings, atomType);
439        break;
440      }
441
442    case "sqrt":
443      {
444        buildRegion(a11yStrings, regionStrings => {
445          var {
446            body,
447            index
448          } = tree;
449
450          if (index) {
451            var indexString = flatten(buildA11yStrings(index, [], atomType)).join(",");
452
453            if (indexString === "3") {
454              regionStrings.push("cube root of");
455              buildA11yStrings(body, regionStrings, atomType);
456              regionStrings.push("end cube root");
457              return;
458            }
459
460            regionStrings.push("root");
461            regionStrings.push("start index");
462            buildA11yStrings(index, regionStrings, atomType);
463            regionStrings.push("end index");
464            return;
465          }
466
467          regionStrings.push("square root of");
468          buildA11yStrings(body, regionStrings, atomType);
469          regionStrings.push("end square root");
470        });
471        break;
472      }
473
474    case "supsub":
475      {
476        var {
477          base,
478          sub,
479          sup
480        } = tree;
481        var isLog = false;
482
483        if (base) {
484          buildA11yStrings(base, a11yStrings, atomType);
485          isLog = base.type === "op" && base.name === "\\log";
486        }
487
488        if (sub) {
489          var regionName = isLog ? "base" : "subscript";
490          buildRegion(a11yStrings, function (regionStrings) {
491            regionStrings.push("start " + regionName);
492            buildA11yStrings(sub, regionStrings, atomType);
493            regionStrings.push("end " + regionName);
494          });
495        }
496
497        if (sup) {
498          buildRegion(a11yStrings, function (regionStrings) {
499            var supString = flatten(buildA11yStrings(sup, [], atomType)).join(",");
500
501            if (supString in powerMap) {
502              regionStrings.push(powerMap[supString]);
503              return;
504            }
505
506            regionStrings.push("start superscript");
507            buildA11yStrings(sup, regionStrings, atomType);
508            regionStrings.push("end superscript");
509          });
510        }
511
512        break;
513      }
514
515    case "text":
516      {
517        // TODO: handle other fonts
518        if (tree.font === "\\textbf") {
519          buildRegion(a11yStrings, function (regionStrings) {
520            regionStrings.push("start bold text");
521            buildA11yStrings(tree.body, regionStrings, atomType);
522            regionStrings.push("end bold text");
523          });
524          break;
525        }
526
527        buildRegion(a11yStrings, function (regionStrings) {
528          regionStrings.push("start text");
529          buildA11yStrings(tree.body, regionStrings, atomType);
530          regionStrings.push("end text");
531        });
532        break;
533      }
534
535    case "textord":
536      {
537        buildString(tree.text, atomType, a11yStrings);
538        break;
539      }
540
541    case "smash":
542      {
543        buildA11yStrings(tree.body, a11yStrings, atomType);
544        break;
545      }
546
547    case "enclose":
548      {
549        // TODO: create a map for these.
550        // TODO: differentiate between a body with a single atom, e.g.
551        // "cancel a" instead of "start cancel, a, end cancel"
552        if (/cancel/.test(tree.label)) {
553          buildRegion(a11yStrings, function (regionStrings) {
554            regionStrings.push("start cancel");
555            buildA11yStrings(tree.body, regionStrings, atomType);
556            regionStrings.push("end cancel");
557          });
558          break;
559        } else if (/box/.test(tree.label)) {
560          buildRegion(a11yStrings, function (regionStrings) {
561            regionStrings.push("start box");
562            buildA11yStrings(tree.body, regionStrings, atomType);
563            regionStrings.push("end box");
564          });
565          break;
566        } else if (/sout/.test(tree.label)) {
567          buildRegion(a11yStrings, function (regionStrings) {
568            regionStrings.push("start strikeout");
569            buildA11yStrings(tree.body, regionStrings, atomType);
570            regionStrings.push("end strikeout");
571          });
572          break;
573        } else if (/phase/.test(tree.label)) {
574          buildRegion(a11yStrings, function (regionStrings) {
575            regionStrings.push("start phase angle");
576            buildA11yStrings(tree.body, regionStrings, atomType);
577            regionStrings.push("end phase angle");
578          });
579          break;
580        }
581
582        throw new Error("KaTeX-a11y: enclose node with " + tree.label + " not supported yet");
583      }
584
585    case "vcenter":
586      {
587        buildA11yStrings(tree.body, a11yStrings, atomType);
588        break;
589      }
590
591    case "vphantom":
592      {
593        throw new Error("KaTeX-a11y: vphantom not implemented yet");
594      }
595
596    case "hphantom":
597      {
598        throw new Error("KaTeX-a11y: hphantom not implemented yet");
599      }
600
601    case "operatorname":
602      {
603        buildA11yStrings(tree.body, a11yStrings, atomType);
604        break;
605      }
606
607    case "array":
608      {
609        throw new Error("KaTeX-a11y: array not implemented yet");
610      }
611
612    case "raw":
613      {
614        throw new Error("KaTeX-a11y: raw not implemented yet");
615      }
616
617    case "size":
618      {
619        // Although there are nodes of type "size" in the parse tree, they have
620        // no semantic meaning and should be ignored.
621        break;
622      }
623
624    case "url":
625      {
626        throw new Error("KaTeX-a11y: url not implemented yet");
627      }
628
629    case "tag":
630      {
631        throw new Error("KaTeX-a11y: tag not implemented yet");
632      }
633
634    case "verb":
635      {
636        buildString("start verbatim", "normal", a11yStrings);
637        buildString(tree.body, "normal", a11yStrings);
638        buildString("end verbatim", "normal", a11yStrings);
639        break;
640      }
641
642    case "environment":
643      {
644        throw new Error("KaTeX-a11y: environment not implemented yet");
645      }
646
647    case "horizBrace":
648      {
649        buildString("start " + tree.label.slice(1), "normal", a11yStrings);
650        buildA11yStrings(tree.base, a11yStrings, atomType);
651        buildString("end " + tree.label.slice(1), "normal", a11yStrings);
652        break;
653      }
654
655    case "infix":
656      {
657        // All infix nodes are replace with other nodes.
658        break;
659      }
660
661    case "includegraphics":
662      {
663        throw new Error("KaTeX-a11y: includegraphics not implemented yet");
664      }
665
666    case "font":
667      {
668        // TODO: callout the start/end of specific fonts
669        // TODO: map \BBb{N} to "the naturals" or something like that
670        buildA11yStrings(tree.body, a11yStrings, atomType);
671        break;
672      }
673
674    case "href":
675      {
676        throw new Error("KaTeX-a11y: href not implemented yet");
677      }
678
679    case "cr":
680      {
681        // This is used by environments.
682        throw new Error("KaTeX-a11y: cr not implemented yet");
683      }
684
685    case "underline":
686      {
687        buildRegion(a11yStrings, function (a11yStrings) {
688          a11yStrings.push("start underline");
689          buildA11yStrings(tree.body, a11yStrings, atomType);
690          a11yStrings.push("end underline");
691        });
692        break;
693      }
694
695    case "xArrow":
696      {
697        throw new Error("KaTeX-a11y: xArrow not implemented yet");
698      }
699
700    case "cdlabel":
701      {
702        throw new Error("KaTeX-a11y: cdlabel not implemented yet");
703      }
704
705    case "cdlabelparent":
706      {
707        throw new Error("KaTeX-a11y: cdlabelparent not implemented yet");
708      }
709
710    case "mclass":
711      {
712        // \neq and \ne are macros so we let "htmlmathml" render the mathmal
713        // side of things and extract the text from that.
714        var _atomType = tree.mclass.slice(1); // $FlowFixMe: drop the leading "m" from the values in mclass
715
716
717        buildA11yStrings(tree.body, a11yStrings, _atomType);
718        break;
719      }
720
721    case "mathchoice":
722      {
723        // TODO: track which which style we're using, e.g. dispaly, text, etc.
724        // default to text style if even that may not be the correct style
725        buildA11yStrings(tree.text, a11yStrings, atomType);
726        break;
727      }
728
729    case "htmlmathml":
730      {
731        buildA11yStrings(tree.mathml, a11yStrings, atomType);
732        break;
733      }
734
735    case "middle":
736      {
737        buildString(tree.delim, atomType, a11yStrings);
738        break;
739      }
740
741    case "internal":
742      {
743        // internal nodes are never included in the parse tree
744        break;
745      }
746
747    case "html":
748      {
749        buildA11yStrings(tree.body, a11yStrings, atomType);
750        break;
751      }
752
753    default:
754      tree.type;
755      throw new Error("KaTeX a11y un-recognized type: " + tree.type);
756  }
757};
758
759var buildA11yStrings = function buildA11yStrings(tree, a11yStrings, atomType) {
760  if (a11yStrings === void 0) {
761    a11yStrings = [];
762  }
763
764  if (tree instanceof Array) {
765    for (var i = 0; i < tree.length; i++) {
766      buildA11yStrings(tree[i], a11yStrings, atomType);
767    }
768  } else {
769    handleObject(tree, a11yStrings, atomType);
770  }
771
772  return a11yStrings;
773};
774
775var flatten = function flatten(array) {
776  var result = [];
777  array.forEach(function (item) {
778    if (item instanceof Array) {
779      result = result.concat(flatten(item));
780    } else {
781      result.push(item);
782    }
783  });
784  return result;
785};
786
787var renderA11yString = function renderA11yString(text, settings) {
788  var tree = katex.__parse(text, settings);
789
790  var a11yStrings = buildA11yStrings(tree, [], "normal");
791  return flatten(a11yStrings).join(", ");
792};
793
794export { renderA11yString as default };