Mercurial > repos > public > wdown
comparison katex/contrib/render-a11y-string.mjs @ 8:4a25b534c81c javascript-experiment
Add v8 engine and include katex
author | Jonatan Werpers <jonatan@werpers.com> |
---|---|
date | Wed, 17 Jun 2020 21:43:52 +0200 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
7:a5aa39557726 | 8:4a25b534c81c |
---|---|
1 import 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 */ | |
18 const 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 }; | |
91 const powerMap = { | |
92 "prime": "prime", | |
93 "degree": "degrees", | |
94 "circle": "degrees", | |
95 "2": "squared", | |
96 "3": "cubed" | |
97 }; | |
98 const openMap = { | |
99 "|": "open vertical bar", | |
100 ".": "" | |
101 }; | |
102 const closeMap = { | |
103 "|": "close vertical bar", | |
104 ".": "" | |
105 }; | |
106 const 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 }; | |
118 const 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 }; | |
134 const 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 | |
143 const buildString = (str, type, a11yStrings) => { | |
144 if (!str) { | |
145 return; | |
146 } | |
147 | |
148 let 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 - $FlowFixMe | |
168 /^\d+$/.test(a11yStrings[a11yStrings.length - 1])) { | |
169 a11yStrings[a11yStrings.length - 1] += ret; | |
170 } else if (ret) { | |
171 a11yStrings.push(ret); | |
172 } | |
173 }; | |
174 | |
175 const buildRegion = (a11yStrings, callback) => { | |
176 const regionStrings = []; | |
177 a11yStrings.push(regionStrings); | |
178 callback(regionStrings); | |
179 }; | |
180 | |
181 const handleObject = (tree, a11yStrings, atomType) => { | |
182 // Everything else is assumed to be an object... | |
183 switch (tree.type) { | |
184 case "accent": | |
185 { | |
186 buildRegion(a11yStrings, a11yStrings => { | |
187 buildA11yStrings(tree.base, a11yStrings, atomType); | |
188 a11yStrings.push("with"); | |
189 buildString(tree.label, "normal", a11yStrings); | |
190 a11yStrings.push("on top"); | |
191 }); | |
192 break; | |
193 } | |
194 | |
195 case "accentUnder": | |
196 { | |
197 buildRegion(a11yStrings, a11yStrings => { | |
198 buildA11yStrings(tree.base, a11yStrings, atomType); | |
199 a11yStrings.push("with"); | |
200 buildString(accentUnderMap[tree.label], "normal", a11yStrings); | |
201 a11yStrings.push("underneath"); | |
202 }); | |
203 break; | |
204 } | |
205 | |
206 case "accent-token": | |
207 { | |
208 // Used internally by accent symbols. | |
209 break; | |
210 } | |
211 | |
212 case "atom": | |
213 { | |
214 const text = tree.text; | |
215 | |
216 switch (tree.family) { | |
217 case "bin": | |
218 { | |
219 buildString(text, "bin", a11yStrings); | |
220 break; | |
221 } | |
222 | |
223 case "close": | |
224 { | |
225 buildString(text, "close", a11yStrings); | |
226 break; | |
227 } | |
228 // TODO(kevinb): figure out what should be done for inner | |
229 | |
230 case "inner": | |
231 { | |
232 buildString(tree.text, "inner", a11yStrings); | |
233 break; | |
234 } | |
235 | |
236 case "open": | |
237 { | |
238 buildString(text, "open", a11yStrings); | |
239 break; | |
240 } | |
241 | |
242 case "punct": | |
243 { | |
244 buildString(text, "punct", a11yStrings); | |
245 break; | |
246 } | |
247 | |
248 case "rel": | |
249 { | |
250 buildString(text, "rel", a11yStrings); | |
251 break; | |
252 } | |
253 | |
254 default: | |
255 { | |
256 tree.family; | |
257 throw new Error(`"${tree.family}" is not a valid atom type`); | |
258 } | |
259 } | |
260 | |
261 break; | |
262 } | |
263 | |
264 case "color": | |
265 { | |
266 const color = tree.color.replace(/katex-/, ""); | |
267 buildRegion(a11yStrings, regionStrings => { | |
268 regionStrings.push("start color " + color); | |
269 buildA11yStrings(tree.body, regionStrings, atomType); | |
270 regionStrings.push("end color " + color); | |
271 }); | |
272 break; | |
273 } | |
274 | |
275 case "color-token": | |
276 { | |
277 // Used by \color, \colorbox, and \fcolorbox but not directly rendered. | |
278 // It's a leaf node and has no children so just break. | |
279 break; | |
280 } | |
281 | |
282 case "delimsizing": | |
283 { | |
284 if (tree.delim && tree.delim !== ".") { | |
285 buildString(tree.delim, "normal", a11yStrings); | |
286 } | |
287 | |
288 break; | |
289 } | |
290 | |
291 case "genfrac": | |
292 { | |
293 buildRegion(a11yStrings, regionStrings => { | |
294 // genfrac can have unbalanced delimiters | |
295 const leftDelim = tree.leftDelim, | |
296 rightDelim = tree.rightDelim; // NOTE: Not sure if this is a safe assumption | |
297 // hasBarLine true -> fraction, false -> binomial | |
298 | |
299 if (tree.hasBarLine) { | |
300 regionStrings.push("start fraction"); | |
301 leftDelim && buildString(leftDelim, "open", regionStrings); | |
302 buildA11yStrings(tree.numer, regionStrings, atomType); | |
303 regionStrings.push("divided by"); | |
304 buildA11yStrings(tree.denom, regionStrings, atomType); | |
305 rightDelim && buildString(rightDelim, "close", regionStrings); | |
306 regionStrings.push("end fraction"); | |
307 } else { | |
308 regionStrings.push("start binomial"); | |
309 leftDelim && buildString(leftDelim, "open", regionStrings); | |
310 buildA11yStrings(tree.numer, regionStrings, atomType); | |
311 regionStrings.push("over"); | |
312 buildA11yStrings(tree.denom, regionStrings, atomType); | |
313 rightDelim && buildString(rightDelim, "close", regionStrings); | |
314 regionStrings.push("end binomial"); | |
315 } | |
316 }); | |
317 break; | |
318 } | |
319 | |
320 case "kern": | |
321 { | |
322 // No op: we don't attempt to present kerning information | |
323 // to the screen reader. | |
324 break; | |
325 } | |
326 | |
327 case "leftright": | |
328 { | |
329 buildRegion(a11yStrings, regionStrings => { | |
330 buildString(tree.left, "open", regionStrings); | |
331 buildA11yStrings(tree.body, regionStrings, atomType); | |
332 buildString(tree.right, "close", regionStrings); | |
333 }); | |
334 break; | |
335 } | |
336 | |
337 case "leftright-right": | |
338 { | |
339 // TODO: double check that this is a no-op | |
340 break; | |
341 } | |
342 | |
343 case "lap": | |
344 { | |
345 buildA11yStrings(tree.body, a11yStrings, atomType); | |
346 break; | |
347 } | |
348 | |
349 case "mathord": | |
350 { | |
351 buildString(tree.text, "normal", a11yStrings); | |
352 break; | |
353 } | |
354 | |
355 case "op": | |
356 { | |
357 const body = tree.body, | |
358 name = tree.name; | |
359 | |
360 if (body) { | |
361 buildA11yStrings(body, a11yStrings, atomType); | |
362 } else if (name) { | |
363 buildString(name, "normal", a11yStrings); | |
364 } | |
365 | |
366 break; | |
367 } | |
368 | |
369 case "op-token": | |
370 { | |
371 // Used internally by operator symbols. | |
372 buildString(tree.text, atomType, a11yStrings); | |
373 break; | |
374 } | |
375 | |
376 case "ordgroup": | |
377 { | |
378 buildA11yStrings(tree.body, a11yStrings, atomType); | |
379 break; | |
380 } | |
381 | |
382 case "overline": | |
383 { | |
384 buildRegion(a11yStrings, function (a11yStrings) { | |
385 a11yStrings.push("start overline"); | |
386 buildA11yStrings(tree.body, a11yStrings, atomType); | |
387 a11yStrings.push("end overline"); | |
388 }); | |
389 break; | |
390 } | |
391 | |
392 case "phantom": | |
393 { | |
394 a11yStrings.push("empty space"); | |
395 break; | |
396 } | |
397 | |
398 case "raisebox": | |
399 { | |
400 buildA11yStrings(tree.body, a11yStrings, atomType); | |
401 break; | |
402 } | |
403 | |
404 case "rule": | |
405 { | |
406 a11yStrings.push("rectangle"); | |
407 break; | |
408 } | |
409 | |
410 case "sizing": | |
411 { | |
412 buildA11yStrings(tree.body, a11yStrings, atomType); | |
413 break; | |
414 } | |
415 | |
416 case "spacing": | |
417 { | |
418 a11yStrings.push("space"); | |
419 break; | |
420 } | |
421 | |
422 case "styling": | |
423 { | |
424 // We ignore the styling and just pass through the contents | |
425 buildA11yStrings(tree.body, a11yStrings, atomType); | |
426 break; | |
427 } | |
428 | |
429 case "sqrt": | |
430 { | |
431 buildRegion(a11yStrings, regionStrings => { | |
432 const body = tree.body, | |
433 index = tree.index; | |
434 | |
435 if (index) { | |
436 const indexString = flatten(buildA11yStrings(index, [], atomType)).join(","); | |
437 | |
438 if (indexString === "3") { | |
439 regionStrings.push("cube root of"); | |
440 buildA11yStrings(body, regionStrings, atomType); | |
441 regionStrings.push("end cube root"); | |
442 return; | |
443 } | |
444 | |
445 regionStrings.push("root"); | |
446 regionStrings.push("start index"); | |
447 buildA11yStrings(index, regionStrings, atomType); | |
448 regionStrings.push("end index"); | |
449 return; | |
450 } | |
451 | |
452 regionStrings.push("square root of"); | |
453 buildA11yStrings(body, regionStrings, atomType); | |
454 regionStrings.push("end square root"); | |
455 }); | |
456 break; | |
457 } | |
458 | |
459 case "supsub": | |
460 { | |
461 const base = tree.base, | |
462 sub = tree.sub, | |
463 sup = tree.sup; | |
464 let isLog = false; | |
465 | |
466 if (base) { | |
467 buildA11yStrings(base, a11yStrings, atomType); | |
468 isLog = base.type === "op" && base.name === "\\log"; | |
469 } | |
470 | |
471 if (sub) { | |
472 const regionName = isLog ? "base" : "subscript"; | |
473 buildRegion(a11yStrings, function (regionStrings) { | |
474 regionStrings.push(`start ${regionName}`); | |
475 buildA11yStrings(sub, regionStrings, atomType); | |
476 regionStrings.push(`end ${regionName}`); | |
477 }); | |
478 } | |
479 | |
480 if (sup) { | |
481 buildRegion(a11yStrings, function (regionStrings) { | |
482 const supString = flatten(buildA11yStrings(sup, [], atomType)).join(","); | |
483 | |
484 if (supString in powerMap) { | |
485 regionStrings.push(powerMap[supString]); | |
486 return; | |
487 } | |
488 | |
489 regionStrings.push("start superscript"); | |
490 buildA11yStrings(sup, regionStrings, atomType); | |
491 regionStrings.push("end superscript"); | |
492 }); | |
493 } | |
494 | |
495 break; | |
496 } | |
497 | |
498 case "text": | |
499 { | |
500 // TODO: handle other fonts | |
501 if (tree.font === "\\textbf") { | |
502 buildRegion(a11yStrings, function (regionStrings) { | |
503 regionStrings.push("start bold text"); | |
504 buildA11yStrings(tree.body, regionStrings, atomType); | |
505 regionStrings.push("end bold text"); | |
506 }); | |
507 break; | |
508 } | |
509 | |
510 buildRegion(a11yStrings, function (regionStrings) { | |
511 regionStrings.push("start text"); | |
512 buildA11yStrings(tree.body, regionStrings, atomType); | |
513 regionStrings.push("end text"); | |
514 }); | |
515 break; | |
516 } | |
517 | |
518 case "textord": | |
519 { | |
520 buildString(tree.text, atomType, a11yStrings); | |
521 break; | |
522 } | |
523 | |
524 case "smash": | |
525 { | |
526 buildA11yStrings(tree.body, a11yStrings, atomType); | |
527 break; | |
528 } | |
529 | |
530 case "enclose": | |
531 { | |
532 // TODO: create a map for these. | |
533 // TODO: differentiate between a body with a single atom, e.g. | |
534 // "cancel a" instead of "start cancel, a, end cancel" | |
535 if (/cancel/.test(tree.label)) { | |
536 buildRegion(a11yStrings, function (regionStrings) { | |
537 regionStrings.push("start cancel"); | |
538 buildA11yStrings(tree.body, regionStrings, atomType); | |
539 regionStrings.push("end cancel"); | |
540 }); | |
541 break; | |
542 } else if (/box/.test(tree.label)) { | |
543 buildRegion(a11yStrings, function (regionStrings) { | |
544 regionStrings.push("start box"); | |
545 buildA11yStrings(tree.body, regionStrings, atomType); | |
546 regionStrings.push("end box"); | |
547 }); | |
548 break; | |
549 } else if (/sout/.test(tree.label)) { | |
550 buildRegion(a11yStrings, function (regionStrings) { | |
551 regionStrings.push("start strikeout"); | |
552 buildA11yStrings(tree.body, regionStrings, atomType); | |
553 regionStrings.push("end strikeout"); | |
554 }); | |
555 break; | |
556 } | |
557 | |
558 throw new Error(`KaTeX-a11y: enclose node with ${tree.label} not supported yet`); | |
559 } | |
560 | |
561 case "vphantom": | |
562 { | |
563 throw new Error("KaTeX-a11y: vphantom not implemented yet"); | |
564 } | |
565 | |
566 case "hphantom": | |
567 { | |
568 throw new Error("KaTeX-a11y: hphantom not implemented yet"); | |
569 } | |
570 | |
571 case "operatorname": | |
572 { | |
573 buildA11yStrings(tree.body, a11yStrings, atomType); | |
574 break; | |
575 } | |
576 | |
577 case "array": | |
578 { | |
579 throw new Error("KaTeX-a11y: array not implemented yet"); | |
580 } | |
581 | |
582 case "raw": | |
583 { | |
584 throw new Error("KaTeX-a11y: raw not implemented yet"); | |
585 } | |
586 | |
587 case "size": | |
588 { | |
589 // Although there are nodes of type "size" in the parse tree, they have | |
590 // no semantic meaning and should be ignored. | |
591 break; | |
592 } | |
593 | |
594 case "url": | |
595 { | |
596 throw new Error("KaTeX-a11y: url not implemented yet"); | |
597 } | |
598 | |
599 case "tag": | |
600 { | |
601 throw new Error("KaTeX-a11y: tag not implemented yet"); | |
602 } | |
603 | |
604 case "verb": | |
605 { | |
606 buildString(`start verbatim`, "normal", a11yStrings); | |
607 buildString(tree.body, "normal", a11yStrings); | |
608 buildString(`end verbatim`, "normal", a11yStrings); | |
609 break; | |
610 } | |
611 | |
612 case "environment": | |
613 { | |
614 throw new Error("KaTeX-a11y: environment not implemented yet"); | |
615 } | |
616 | |
617 case "horizBrace": | |
618 { | |
619 buildString(`start ${tree.label.slice(1)}`, "normal", a11yStrings); | |
620 buildA11yStrings(tree.base, a11yStrings, atomType); | |
621 buildString(`end ${tree.label.slice(1)}`, "normal", a11yStrings); | |
622 break; | |
623 } | |
624 | |
625 case "infix": | |
626 { | |
627 // All infix nodes are replace with other nodes. | |
628 break; | |
629 } | |
630 | |
631 case "includegraphics": | |
632 { | |
633 throw new Error("KaTeX-a11y: includegraphics not implemented yet"); | |
634 } | |
635 | |
636 case "font": | |
637 { | |
638 // TODO: callout the start/end of specific fonts | |
639 // TODO: map \BBb{N} to "the naturals" or something like that | |
640 buildA11yStrings(tree.body, a11yStrings, atomType); | |
641 break; | |
642 } | |
643 | |
644 case "href": | |
645 { | |
646 throw new Error("KaTeX-a11y: href not implemented yet"); | |
647 } | |
648 | |
649 case "cr": | |
650 { | |
651 // This is used by environments. | |
652 throw new Error("KaTeX-a11y: cr not implemented yet"); | |
653 } | |
654 | |
655 case "underline": | |
656 { | |
657 buildRegion(a11yStrings, function (a11yStrings) { | |
658 a11yStrings.push("start underline"); | |
659 buildA11yStrings(tree.body, a11yStrings, atomType); | |
660 a11yStrings.push("end underline"); | |
661 }); | |
662 break; | |
663 } | |
664 | |
665 case "xArrow": | |
666 { | |
667 throw new Error("KaTeX-a11y: xArrow not implemented yet"); | |
668 } | |
669 | |
670 case "mclass": | |
671 { | |
672 // \neq and \ne are macros so we let "htmlmathml" render the mathmal | |
673 // side of things and extract the text from that. | |
674 const atomType = tree.mclass.slice(1); // $FlowFixMe: drop the leading "m" from the values in mclass | |
675 | |
676 buildA11yStrings(tree.body, a11yStrings, atomType); | |
677 break; | |
678 } | |
679 | |
680 case "mathchoice": | |
681 { | |
682 // TODO: track which which style we're using, e.g. dispaly, text, etc. | |
683 // default to text style if even that may not be the correct style | |
684 buildA11yStrings(tree.text, a11yStrings, atomType); | |
685 break; | |
686 } | |
687 | |
688 case "htmlmathml": | |
689 { | |
690 buildA11yStrings(tree.mathml, a11yStrings, atomType); | |
691 break; | |
692 } | |
693 | |
694 case "middle": | |
695 { | |
696 buildString(tree.delim, atomType, a11yStrings); | |
697 break; | |
698 } | |
699 | |
700 default: | |
701 tree.type; | |
702 throw new Error("KaTeX a11y un-recognized type: " + tree.type); | |
703 } | |
704 }; | |
705 | |
706 const buildA11yStrings = function buildA11yStrings(tree, a11yStrings, atomType) { | |
707 if (a11yStrings === void 0) { | |
708 a11yStrings = []; | |
709 } | |
710 | |
711 if (tree instanceof Array) { | |
712 for (let i = 0; i < tree.length; i++) { | |
713 buildA11yStrings(tree[i], a11yStrings, atomType); | |
714 } | |
715 } else { | |
716 handleObject(tree, a11yStrings, atomType); | |
717 } | |
718 | |
719 return a11yStrings; | |
720 }; | |
721 | |
722 const flatten = function flatten(array) { | |
723 let result = []; | |
724 array.forEach(function (item) { | |
725 if (item instanceof Array) { | |
726 result = result.concat(flatten(item)); | |
727 } else { | |
728 result.push(item); | |
729 } | |
730 }); | |
731 return result; | |
732 }; | |
733 | |
734 const renderA11yString = function renderA11yString(text, settings) { | |
735 const tree = katex.__parse(text, settings); | |
736 | |
737 const a11yStrings = buildA11yStrings(tree, [], "normal"); | |
738 return flatten(a11yStrings).join(", "); | |
739 }; | |
740 | |
741 export default renderA11yString; |