arnaucube.com/blogo-input/js/katex/contrib/render-a11y-string.mjs
blogo-input/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 };