auto-render.ts 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. /* eslint no-console:0 */
  2. import katex from "katex";
  3. import splitAtDelimiters from "./splitAtDelimiters";
  4. import type {DelimiterSpec} from "./splitAtDelimiters";
  5. interface RenderMathInElementOptions {
  6. delimiters?: DelimiterSpec[];
  7. preProcess?: (math: string) => string;
  8. ignoredTags?: string[];
  9. ignoredClasses?: string[];
  10. errorCallback?: (msg: string, err: Error) => void;
  11. displayMode?: boolean;
  12. macros?: Record<string, string>;
  13. }
  14. interface RenderMathInElementOptionsCopy {
  15. delimiters: DelimiterSpec[];
  16. preProcess?: (math: string) => string;
  17. ignoredTags: Set<string>;
  18. ignoredClasses: string[];
  19. errorCallback: (msg: string, err: Error) => void;
  20. displayMode?: boolean;
  21. macros?: Record<string, string>;
  22. }
  23. /* Note: optionsCopy is mutated by this method. If it is ever exposed in the
  24. * API, we should copy it before mutating.
  25. */
  26. const renderMathInText = function(
  27. text: string,
  28. optionsCopy: RenderMathInElementOptionsCopy
  29. ) {
  30. const data = splitAtDelimiters(text, optionsCopy.delimiters);
  31. if (data.length === 1 && data[0].type === 'text') {
  32. // There is no formula in the text.
  33. // Let's return null which means there is no need to replace
  34. // the current text node with a new one.
  35. return null;
  36. }
  37. const fragment = document.createDocumentFragment();
  38. for (let i = 0; i < data.length; i++) {
  39. if (data[i].type === "text") {
  40. fragment.appendChild(document.createTextNode(data[i].data));
  41. } else {
  42. const span = document.createElement("span");
  43. let math = data[i].data;
  44. // Override any display mode defined in the settings with that
  45. // defined by the text itself
  46. optionsCopy.displayMode = data[i].display;
  47. try {
  48. if (optionsCopy.preProcess) {
  49. math = optionsCopy.preProcess(math);
  50. }
  51. katex.render(math, span, optionsCopy);
  52. } catch (e) {
  53. if (!(e instanceof katex.ParseError)) {
  54. throw e;
  55. }
  56. optionsCopy.errorCallback(
  57. "KaTeX auto-render: Failed to parse `" + data[i].data +
  58. "` with ",
  59. e
  60. );
  61. fragment.appendChild(document.createTextNode(data[i].rawData!));
  62. continue;
  63. }
  64. fragment.appendChild(span);
  65. }
  66. }
  67. return fragment;
  68. };
  69. const renderElem = function(
  70. elem: HTMLElement,
  71. optionsCopy: RenderMathInElementOptionsCopy
  72. ) {
  73. for (let i = 0; i < elem.childNodes.length; i++) {
  74. const childNode = elem.childNodes[i];
  75. if (childNode.nodeType === 3) {
  76. // Text node
  77. // Concatenate all sibling text nodes.
  78. // Webkit browsers split very large text nodes into smaller ones,
  79. // so the delimiters may be split across different nodes.
  80. let textContentConcat = childNode.textContent ?? "";
  81. let sibling = childNode.nextSibling;
  82. let nSiblings = 0;
  83. while (sibling && (sibling.nodeType === Node.TEXT_NODE)) {
  84. textContentConcat += sibling.textContent ?? "";
  85. sibling = sibling.nextSibling;
  86. nSiblings++;
  87. }
  88. const frag = renderMathInText(textContentConcat, optionsCopy);
  89. if (frag) {
  90. // Remove extra text nodes
  91. for (let j = 0; j < nSiblings; j++) {
  92. childNode.nextSibling!.remove();
  93. }
  94. i += frag.childNodes.length - 1;
  95. elem.replaceChild(frag, childNode);
  96. } else {
  97. // If the concatenated text does not contain math
  98. // the siblings will not either
  99. i += nSiblings;
  100. }
  101. } else if (childNode.nodeType === 1) {
  102. // Element node
  103. const className = ' ' + (childNode as HTMLElement).className + ' ';
  104. const shouldRender = !optionsCopy.ignoredTags.has(
  105. childNode.nodeName.toLowerCase()) &&
  106. optionsCopy.ignoredClasses.every(
  107. (x: string) => !className.includes(' ' + x + ' '));
  108. if (shouldRender) {
  109. renderElem(childNode as HTMLElement, optionsCopy);
  110. }
  111. }
  112. // Otherwise, it's something else, and ignore it.
  113. }
  114. };
  115. const renderMathInElement = function(elem: HTMLElement, options?: RenderMathInElementOptions) {
  116. if (!elem) {
  117. throw new Error("No element provided to render");
  118. }
  119. const optionsCopy: Partial<RenderMathInElementOptionsCopy> = {};
  120. Object.assign(optionsCopy, options);
  121. // default options
  122. optionsCopy.delimiters = optionsCopy.delimiters || [
  123. {left: "$$", right: "$$", display: true},
  124. {left: "\\(", right: "\\)", display: false},
  125. // LaTeX uses $…$, but it ruins the display of normal `$` in text:
  126. // {left: "$", right: "$", display: false},
  127. // $ must come after $$
  128. // Render AMS environments even if outside $$…$$ delimiters.
  129. {left: "\\begin{equation}", right: "\\end{equation}", display: true},
  130. {left: "\\begin{align}", right: "\\end{align}", display: true},
  131. {left: "\\begin{alignat}", right: "\\end{alignat}", display: true},
  132. {left: "\\begin{gather}", right: "\\end{gather}", display: true},
  133. {left: "\\begin{CD}", right: "\\end{CD}", display: true},
  134. {left: "\\[", right: "\\]", display: true},
  135. ];
  136. optionsCopy.ignoredTags = new Set<string>(options?.ignoredTags || [
  137. "script", "noscript", "style", "textarea", "pre", "code", "option",
  138. ]);
  139. optionsCopy.ignoredClasses = optionsCopy.ignoredClasses || [];
  140. optionsCopy.errorCallback = optionsCopy.errorCallback || console.error;
  141. // Enable sharing of global macros defined via `\gdef` between different
  142. // math elements within a single call to `renderMathInElement`.
  143. optionsCopy.macros = optionsCopy.macros || {};
  144. renderElem(elem, optionsCopy as RenderMathInElementOptionsCopy);
  145. };
  146. export default renderMathInElement;