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.

794 lines
20 KiB

  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 "phantom":
  368. {
  369. a11yStrings.push("empty space");
  370. break;
  371. }
  372. case "raisebox":
  373. {
  374. buildA11yStrings(tree.body, a11yStrings, atomType);
  375. break;
  376. }
  377. case "rule":
  378. {
  379. a11yStrings.push("rectangle");
  380. break;
  381. }
  382. case "sizing":
  383. {
  384. buildA11yStrings(tree.body, a11yStrings, atomType);
  385. break;
  386. }
  387. case "spacing":
  388. {
  389. a11yStrings.push("space");
  390. break;
  391. }
  392. case "styling":
  393. {
  394. // We ignore the styling and just pass through the contents
  395. buildA11yStrings(tree.body, a11yStrings, atomType);
  396. break;
  397. }
  398. case "sqrt":
  399. {
  400. buildRegion(a11yStrings, regionStrings => {
  401. var {
  402. body,
  403. index
  404. } = tree;
  405. if (index) {
  406. var indexString = flatten(buildA11yStrings(index, [], atomType)).join(",");
  407. if (indexString === "3") {
  408. regionStrings.push("cube root of");
  409. buildA11yStrings(body, regionStrings, atomType);
  410. regionStrings.push("end cube root");
  411. return;
  412. }
  413. regionStrings.push("root");
  414. regionStrings.push("start index");
  415. buildA11yStrings(index, regionStrings, atomType);
  416. regionStrings.push("end index");
  417. return;
  418. }
  419. regionStrings.push("square root of");
  420. buildA11yStrings(body, regionStrings, atomType);
  421. regionStrings.push("end square root");
  422. });
  423. break;
  424. }
  425. case "supsub":
  426. {
  427. var {
  428. base,
  429. sub,
  430. sup
  431. } = tree;
  432. var isLog = false;
  433. if (base) {
  434. buildA11yStrings(base, a11yStrings, atomType);
  435. isLog = base.type === "op" && base.name === "\\log";
  436. }
  437. if (sub) {
  438. var regionName = isLog ? "base" : "subscript";
  439. buildRegion(a11yStrings, function (regionStrings) {
  440. regionStrings.push("start " + regionName);
  441. buildA11yStrings(sub, regionStrings, atomType);
  442. regionStrings.push("end " + regionName);
  443. });
  444. }
  445. if (sup) {
  446. buildRegion(a11yStrings, function (regionStrings) {
  447. var supString = flatten(buildA11yStrings(sup, [], atomType)).join(",");
  448. if (supString in powerMap) {
  449. regionStrings.push(powerMap[supString]);
  450. return;
  451. }
  452. regionStrings.push("start superscript");
  453. buildA11yStrings(sup, regionStrings, atomType);
  454. regionStrings.push("end superscript");
  455. });
  456. }
  457. break;
  458. }
  459. case "text":
  460. {
  461. // TODO: handle other fonts
  462. if (tree.font === "\\textbf") {
  463. buildRegion(a11yStrings, function (regionStrings) {
  464. regionStrings.push("start bold text");
  465. buildA11yStrings(tree.body, regionStrings, atomType);
  466. regionStrings.push("end bold text");
  467. });
  468. break;
  469. }
  470. buildRegion(a11yStrings, function (regionStrings) {
  471. regionStrings.push("start text");
  472. buildA11yStrings(tree.body, regionStrings, atomType);
  473. regionStrings.push("end text");
  474. });
  475. break;
  476. }
  477. case "textord":
  478. {
  479. buildString(tree.text, atomType, a11yStrings);
  480. break;
  481. }
  482. case "smash":
  483. {
  484. buildA11yStrings(tree.body, a11yStrings, atomType);
  485. break;
  486. }
  487. case "enclose":
  488. {
  489. // TODO: create a map for these.
  490. // TODO: differentiate between a body with a single atom, e.g.
  491. // "cancel a" instead of "start cancel, a, end cancel"
  492. if (/cancel/.test(tree.label)) {
  493. buildRegion(a11yStrings, function (regionStrings) {
  494. regionStrings.push("start cancel");
  495. buildA11yStrings(tree.body, regionStrings, atomType);
  496. regionStrings.push("end cancel");
  497. });
  498. break;
  499. } else if (/box/.test(tree.label)) {
  500. buildRegion(a11yStrings, function (regionStrings) {
  501. regionStrings.push("start box");
  502. buildA11yStrings(tree.body, regionStrings, atomType);
  503. regionStrings.push("end box");
  504. });
  505. break;
  506. } else if (/sout/.test(tree.label)) {
  507. buildRegion(a11yStrings, function (regionStrings) {
  508. regionStrings.push("start strikeout");
  509. buildA11yStrings(tree.body, regionStrings, atomType);
  510. regionStrings.push("end strikeout");
  511. });
  512. break;
  513. } else if (/phase/.test(tree.label)) {
  514. buildRegion(a11yStrings, function (regionStrings) {
  515. regionStrings.push("start phase angle");
  516. buildA11yStrings(tree.body, regionStrings, atomType);
  517. regionStrings.push("end phase angle");
  518. });
  519. break;
  520. }
  521. throw new Error("KaTeX-a11y: enclose node with " + tree.label + " not supported yet");
  522. }
  523. case "vcenter":
  524. {
  525. buildA11yStrings(tree.body, a11yStrings, atomType);
  526. break;
  527. }
  528. case "vphantom":
  529. {
  530. throw new Error("KaTeX-a11y: vphantom not implemented yet");
  531. }
  532. case "hphantom":
  533. {
  534. throw new Error("KaTeX-a11y: hphantom not implemented yet");
  535. }
  536. case "operatorname":
  537. {
  538. buildA11yStrings(tree.body, a11yStrings, atomType);
  539. break;
  540. }
  541. case "array":
  542. {
  543. throw new Error("KaTeX-a11y: array not implemented yet");
  544. }
  545. case "raw":
  546. {
  547. throw new Error("KaTeX-a11y: raw not implemented yet");
  548. }
  549. case "size":
  550. {
  551. // Although there are nodes of type "size" in the parse tree, they have
  552. // no semantic meaning and should be ignored.
  553. break;
  554. }
  555. case "url":
  556. {
  557. throw new Error("KaTeX-a11y: url not implemented yet");
  558. }
  559. case "tag":
  560. {
  561. throw new Error("KaTeX-a11y: tag not implemented yet");
  562. }
  563. case "verb":
  564. {
  565. buildString("start verbatim", "normal", a11yStrings);
  566. buildString(tree.body, "normal", a11yStrings);
  567. buildString("end verbatim", "normal", a11yStrings);
  568. break;
  569. }
  570. case "environment":
  571. {
  572. throw new Error("KaTeX-a11y: environment not implemented yet");
  573. }
  574. case "horizBrace":
  575. {
  576. buildString("start " + tree.label.slice(1), "normal", a11yStrings);
  577. buildA11yStrings(tree.base, a11yStrings, atomType);
  578. buildString("end " + tree.label.slice(1), "normal", a11yStrings);
  579. break;
  580. }
  581. case "infix":
  582. {
  583. // All infix nodes are replace with other nodes.
  584. break;
  585. }
  586. case "includegraphics":
  587. {
  588. throw new Error("KaTeX-a11y: includegraphics not implemented yet");
  589. }
  590. case "font":
  591. {
  592. // TODO: callout the start/end of specific fonts
  593. // TODO: map \BBb{N} to "the naturals" or something like that
  594. buildA11yStrings(tree.body, a11yStrings, atomType);
  595. break;
  596. }
  597. case "href":
  598. {
  599. throw new Error("KaTeX-a11y: href not implemented yet");
  600. }
  601. case "cr":
  602. {
  603. // This is used by environments.
  604. throw new Error("KaTeX-a11y: cr not implemented yet");
  605. }
  606. case "underline":
  607. {
  608. buildRegion(a11yStrings, function (a11yStrings) {
  609. a11yStrings.push("start underline");
  610. buildA11yStrings(tree.body, a11yStrings, atomType);
  611. a11yStrings.push("end underline");
  612. });
  613. break;
  614. }
  615. case "xArrow":
  616. {
  617. throw new Error("KaTeX-a11y: xArrow not implemented yet");
  618. }
  619. case "cdlabel":
  620. {
  621. throw new Error("KaTeX-a11y: cdlabel not implemented yet");
  622. }
  623. case "cdlabelparent":
  624. {
  625. throw new Error("KaTeX-a11y: cdlabelparent not implemented yet");
  626. }
  627. case "mclass":
  628. {
  629. // \neq and \ne are macros so we let "htmlmathml" render the mathmal
  630. // side of things and extract the text from that.
  631. var _atomType = tree.mclass.slice(1); // $FlowFixMe: drop the leading "m" from the values in mclass
  632. buildA11yStrings(tree.body, a11yStrings, _atomType);
  633. break;
  634. }
  635. case "mathchoice":
  636. {
  637. // TODO: track which which style we're using, e.g. dispaly, text, etc.
  638. // default to text style if even that may not be the correct style
  639. buildA11yStrings(tree.text, a11yStrings, atomType);
  640. break;
  641. }
  642. case "htmlmathml":
  643. {
  644. buildA11yStrings(tree.mathml, a11yStrings, atomType);
  645. break;
  646. }
  647. case "middle":
  648. {
  649. buildString(tree.delim, atomType, a11yStrings);
  650. break;
  651. }
  652. case "internal":
  653. {
  654. // internal nodes are never included in the parse tree
  655. break;
  656. }
  657. case "html":
  658. {
  659. buildA11yStrings(tree.body, a11yStrings, atomType);
  660. break;
  661. }
  662. default:
  663. tree.type;
  664. throw new Error("KaTeX a11y un-recognized type: " + tree.type);
  665. }
  666. };
  667. var buildA11yStrings = function buildA11yStrings(tree, a11yStrings, atomType) {
  668. if (a11yStrings === void 0) {
  669. a11yStrings = [];
  670. }
  671. if (tree instanceof Array) {
  672. for (var i = 0; i < tree.length; i++) {
  673. buildA11yStrings(tree[i], a11yStrings, atomType);
  674. }
  675. } else {
  676. handleObject(tree, a11yStrings, atomType);
  677. }
  678. return a11yStrings;
  679. };
  680. var flatten = function flatten(array) {
  681. var result = [];
  682. array.forEach(function (item) {
  683. if (item instanceof Array) {
  684. result = result.concat(flatten(item));
  685. } else {
  686. result.push(item);
  687. }
  688. });
  689. return result;
  690. };
  691. var renderA11yString = function renderA11yString(text, settings) {
  692. var tree = katex.__parse(text, settings);
  693. var a11yStrings = buildA11yStrings(tree, [], "normal");
  694. return flatten(a11yStrings).join(", ");
  695. };
  696. export { renderA11yString as default };