latex.ts 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. /*-----------------------------------------------------------------------------
  2. | Copyright (c) Jupyter Development Team.
  3. | Distributed under the terms of the Modified BSD License.
  4. |----------------------------------------------------------------------------*/
  5. // Some magic for deferring mathematical expressions to MathJax
  6. // by hiding them from the Markdown parser.
  7. // Some of the code here is adapted with permission from Davide Cervone
  8. // under the terms of the Apache2 license governing the MathJax project.
  9. // Other minor modifications are also due to StackExchange and are used with
  10. // permission.
  11. const inline = '$'; // the inline math delimiter
  12. // MATHSPLIT contains the pattern for math delimiters and special symbols
  13. // needed for searching for math in the text input.
  14. const MATHSPLIT = /(\$\$?|\\(?:begin|end)\{[a-z]*\*?\}|\\[{}$]|[{}]|(?:\n\s*)+|@@\d+@@|\\\\(?:\(|\)|\[|\]))/i;
  15. // A module-level initialization flag.
  16. let initialized = false;
  17. // Stub for window MathJax.
  18. declare var MathJax: any;
  19. /**
  20. * Break up the text into its component parts and search
  21. * through them for math delimiters, braces, linebreaks, etc.
  22. * Math delimiters must match and braces must balance.
  23. * Don't allow math to pass through a double linebreak
  24. * (which will be a paragraph).
  25. */
  26. export
  27. function removeMath(text: string): { text: string, math: string[] } {
  28. let math: string[] = []; // stores math strings for later
  29. let start: number;
  30. let end: string;
  31. let last: number;
  32. let braces: number;
  33. let deTilde: (text: string) => string;
  34. if (!initialized) {
  35. init();
  36. initialized = true;
  37. }
  38. // Except for extreme edge cases, this should catch precisely those pieces of the markdown
  39. // source that will later be turned into code spans. While MathJax will not TeXify code spans,
  40. // we still have to consider them at this point; the following issue has happened several times:
  41. //
  42. // `$foo` and `$bar` are variables. --> <code>$foo ` and `$bar</code> are variables.
  43. let hasCodeSpans = /`/.test(text);
  44. if (hasCodeSpans) {
  45. text = text.replace(/~/g, '~T').replace(/(^|[^\\])(`+)([^\n]*?[^`\n])\2(?!`)/gm, (wholematch) => wholematch.replace(/\$/g, '~D'));
  46. deTilde = (text: string) => {
  47. return text.replace(/~([TD])/g,
  48. (wholematch, character) => (character === 'T') ? '~' : inline);
  49. };
  50. } else {
  51. deTilde = (text: string) => { return text; };
  52. }
  53. let blocks = text.replace(/\r\n?/g, '\n').split(MATHSPLIT);
  54. for (let i = 1, m = blocks.length; i < m; i += 2) {
  55. let block = blocks[i];
  56. if (block.charAt(0) === '@') {
  57. //
  58. // Things that look like our math markers will get
  59. // stored and then retrieved along with the math.
  60. //
  61. blocks[i] = '@@' + math.length + '@@';
  62. math.push(block);
  63. } else if (start) {
  64. //
  65. // If we are in math, look for the end delimiter,
  66. // but don't go past double line breaks, and
  67. // and balance braces within the math.
  68. //
  69. if (block === end) {
  70. if (braces) {
  71. last = i;
  72. } else {
  73. blocks = processMath(start, i, deTilde, math, blocks);
  74. start = null;
  75. end = null;
  76. last = null;
  77. }
  78. } else if (block.match(/\n.*\n/)) {
  79. if (last) {
  80. i = last;
  81. blocks = processMath(start, i, deTilde, math, blocks);
  82. }
  83. start = null;
  84. end = null;
  85. last = null;
  86. braces = 0;
  87. } else if (block === '{') {
  88. braces++;
  89. } else if (block === '}' && braces) {
  90. braces--;
  91. }
  92. } else {
  93. //
  94. // Look for math start delimiters and when
  95. // found, set up the end delimiter.
  96. //
  97. if (block === inline || block === '$$') {
  98. start = i;
  99. end = block;
  100. braces = 0;
  101. } else if (block === '\\\\\(' || block === '\\\\\[') {
  102. start = i;
  103. end = block.slice(-1) === '(' ? '\\\\\)' : '\\\\\]';
  104. braces = 0;
  105. } else if (block.substr(1, 5) === 'begin') {
  106. start = i;
  107. end = '\\end' + block.substr(6);
  108. braces = 0;
  109. }
  110. }
  111. }
  112. if (last) {
  113. blocks = processMath(start, last, deTilde, math, blocks);
  114. start = null;
  115. end = null;
  116. last = null;
  117. }
  118. return { text: deTilde(blocks.join('')), math };
  119. };
  120. /**
  121. * Put back the math strings that were saved,
  122. * and clear the math array (no need to keep it around).
  123. */
  124. export
  125. function replaceMath(text: string, math: string[]): string {
  126. /**
  127. * Replace a math placeholder with its corresponding group.
  128. * The math delimiters "\\(", "\\[", "\\)" and "\\]" are replaced
  129. * removing one backslash in order to be interpreted correctly by MathJax.
  130. */
  131. let process = (match: string, n: number): string => {
  132. let group = math[n];
  133. if (group.substr(0, 3) === "\\\\\(" &&
  134. group.substr(group.length - 3) === "\\\\\)") {
  135. group = "\\\(" + group.substring(3, group.length - 3) + "\\\)";
  136. } else if (group.substr(0, 3) === "\\\\\[" &&
  137. group.substr(group.length - 3) === "\\\\\]") {
  138. group = "\\\[" + group.substring(3, group.length - 3) + "\\\]";
  139. }
  140. return group;
  141. };
  142. // Replace all the math group placeholders in the text
  143. // with the saved strings.
  144. return text.replace(/@@(\d+)@@/g, process);
  145. };
  146. /**
  147. * Typeset the math in a node.
  148. */
  149. export
  150. function typeset(node: HTMLElement): void {
  151. if (!initialized) {
  152. init();
  153. initialized = true;
  154. }
  155. if ((window as any).MathJax) {
  156. MathJax.Hub.Queue(['Typeset', MathJax.Hub, node]);
  157. }
  158. }
  159. /**
  160. * Initialize latex handling.
  161. */
  162. function init() {
  163. if (!(window as any).MathJax) {
  164. return;
  165. }
  166. MathJax.Hub.Config({
  167. tex2jax: {
  168. inlineMath: [ ['$', '$'], ['\\(', '\\)'] ],
  169. displayMath: [ ['$$', '$$'], ['\\[', '\\]'] ],
  170. processEscapes: true,
  171. processEnvironments: true
  172. },
  173. // Center justify equations in code and markdown cells. Elsewhere
  174. // we use CSS to left justify single line equations in code cells.
  175. displayAlign: 'center',
  176. CommonHTML: {
  177. linebreaks: { automatic: true }
  178. },
  179. 'HTML-CSS': {
  180. availableFonts: [],
  181. imageFont: null,
  182. preferredFont: null,
  183. webFont: 'STIX-Web',
  184. styles: {'.MathJax_Display': {'margin': 0}},
  185. linebreaks: { automatic: true }
  186. },
  187. });
  188. MathJax.Hub.Configured();
  189. }
  190. /**
  191. * Process math blocks.
  192. *
  193. * The math is in blocks i through j, so
  194. * collect it into one block and clear the others.
  195. * Replace &, <, and > by named entities.
  196. * For IE, put <br> at the ends of comments since IE removes \n.
  197. * Clear the current math positions and store the index of the
  198. * math, then push the math string onto the storage array.
  199. * The preProcess function is called on all blocks if it has been passed in
  200. */
  201. function processMath(i: number, j: number, preProcess: (input: string) => string, math: string[], blocks: string[]): string[] {
  202. let block = blocks.slice(i, j + 1).join('').replace(/&/g, '&amp;') // use HTML entity for &
  203. .replace(/</g, '&lt;') // use HTML entity for <
  204. .replace(/>/g, '&gt;') // use HTML entity for >
  205. ;
  206. if (navigator && navigator.appName === 'Microsoft Internet Explorer') {
  207. block = block.replace(/(%[^\n]*)\n/g, '$1<br/>\n');
  208. }
  209. while (j > i) {
  210. blocks[j] = '';
  211. j--;
  212. }
  213. blocks[i] = '@@' + math.length + '@@'; // replace the current block text with a unique tag to find later
  214. if (preProcess) {
  215. block = preProcess(block);
  216. }
  217. math.push(block);
  218. return blocks;
  219. };