render-a11y-string.mjs 20 KB

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