|
| 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 | + |
| 12 | +const inline = '$'; // the inline math delimiter |
| 13 | + |
| 14 | +// MATHSPLIT contains the pattern for math delimiters and special symbols |
| 15 | +// needed for searching for math in the text input. |
| 16 | +const MATHSPLIT = |
| 17 | + /(\$\$?|\\(?:begin|end)\{[a-z]*\*?\}|\\[{}$]|[{}]|(?:\n\s*)+|@@\d+@@|\\\\(?:\(|\)|\[|\]))/i; |
| 18 | + |
| 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 function removeMath(text: string): { text: string; math: string[] } { |
| 27 | + const math: string[] = []; // stores math strings for later |
| 28 | + let start: number | null = null; |
| 29 | + let end: string | null = null; |
| 30 | + let last: number | null = null; |
| 31 | + let braces = 0; |
| 32 | + let deTilde: (text: string) => string; |
| 33 | + |
| 34 | + // Except for extreme edge cases, this should catch precisely those pieces of the markdown |
| 35 | + // source that will later be turned into code spans. While MathJax will not TeXify code spans, |
| 36 | + // we still have to consider them at this point; the following issue has happened several times: |
| 37 | + // |
| 38 | + // `$foo` and `$bar` are variables. --> <code>$foo ` and `$bar</code> are variables. |
| 39 | + const hasCodeSpans = /`/.test(text); |
| 40 | + if (hasCodeSpans) { |
| 41 | + text = text |
| 42 | + .replace(/~/g, '~T') |
| 43 | + .replace(/(^|[^\\])(`+)([^\n]*?[^`\n])\2(?!`)/gm, (wholematch) => |
| 44 | + wholematch.replace(/\$/g, '~D') |
| 45 | + ); |
| 46 | + deTilde = (text: string) => { |
| 47 | + return text.replace(/~([TD])/g, (wholematch, character) => |
| 48 | + character === 'T' ? '~' : inline |
| 49 | + ); |
| 50 | + }; |
| 51 | + } else { |
| 52 | + deTilde = (text: string) => { |
| 53 | + return text; |
| 54 | + }; |
| 55 | + } |
| 56 | + |
| 57 | + let blocks = text.replace(/\r\n?/g, '\n').split(MATHSPLIT); |
| 58 | + |
| 59 | + for (let i = 1, m = blocks.length; i < m; i += 2) { |
| 60 | + const block = blocks[i]; |
| 61 | + if (block.charAt(0) === '@') { |
| 62 | + // |
| 63 | + // Things that look like our math markers will get |
| 64 | + // stored and then retrieved along with the math. |
| 65 | + // |
| 66 | + blocks[i] = '@@' + math.length + '@@'; |
| 67 | + math.push(block); |
| 68 | + } else if (start !== null) { |
| 69 | + // |
| 70 | + // If we are in math, look for the end delimiter, |
| 71 | + // but don't go past double line breaks, and |
| 72 | + // and balance braces within the math. |
| 73 | + // |
| 74 | + if (block === end) { |
| 75 | + if (braces) { |
| 76 | + last = i; |
| 77 | + } else { |
| 78 | + blocks = processMath(start, i, deTilde, math, blocks); |
| 79 | + start = null; |
| 80 | + end = null; |
| 81 | + last = null; |
| 82 | + } |
| 83 | + } else if (block.match(/\n.*\n/)) { |
| 84 | + if (last !== null) { |
| 85 | + i = last; |
| 86 | + blocks = processMath(start, i, deTilde, math, blocks); |
| 87 | + } |
| 88 | + start = null; |
| 89 | + end = null; |
| 90 | + last = null; |
| 91 | + braces = 0; |
| 92 | + } else if (block === '{') { |
| 93 | + braces++; |
| 94 | + } else if (block === '}' && braces) { |
| 95 | + braces--; |
| 96 | + } |
| 97 | + } else { |
| 98 | + // |
| 99 | + // Look for math start delimiters and when |
| 100 | + // found, set up the end delimiter. |
| 101 | + // |
| 102 | + if (block === inline || block === '$$') { |
| 103 | + start = i; |
| 104 | + end = block; |
| 105 | + braces = 0; |
| 106 | + } else if (block === '\\\\(' || block === '\\\\[') { |
| 107 | + start = i; |
| 108 | + end = block.slice(-1) === '(' ? '\\\\)' : '\\\\]'; |
| 109 | + braces = 0; |
| 110 | + } else if (block.substr(1, 5) === 'begin') { |
| 111 | + start = i; |
| 112 | + end = '\\end' + block.substr(6); |
| 113 | + braces = 0; |
| 114 | + } |
| 115 | + } |
| 116 | + } |
| 117 | + if (start !== null && last !== null) { |
| 118 | + blocks = processMath(start, last, deTilde, math, blocks); |
| 119 | + start = null; |
| 120 | + end = null; |
| 121 | + last = null; |
| 122 | + } |
| 123 | + return { text: deTilde(blocks.join('')), math }; |
| 124 | +} |
| 125 | + |
| 126 | +/** |
| 127 | + * Put back the math strings that were saved, |
| 128 | + * and clear the math array (no need to keep it around). |
| 129 | + */ |
| 130 | +export function replaceMath(text: string, math: string[]): string { |
| 131 | + /** |
| 132 | + * Replace a math placeholder with its corresponding group. |
| 133 | + * The math delimiters "\\(", "\\[", "\\)" and "\\]" are replaced |
| 134 | + * removing one backslash in order to be interpreted correctly by MathJax. |
| 135 | + */ |
| 136 | + const process = (match: string, n: number): string => { |
| 137 | + let group = math[n]; |
| 138 | + if ( |
| 139 | + group.substr(0, 3) === '\\\\(' && |
| 140 | + group.substr(group.length - 3) === '\\\\)' |
| 141 | + ) { |
| 142 | + group = '\\(' + group.substring(3, group.length - 3) + '\\)'; |
| 143 | + } else if ( |
| 144 | + group.substr(0, 3) === '\\\\[' && |
| 145 | + group.substr(group.length - 3) === '\\\\]' |
| 146 | + ) { |
| 147 | + group = '\\[' + group.substring(3, group.length - 3) + '\\]'; |
| 148 | + } |
| 149 | + return group; |
| 150 | + }; |
| 151 | + // Replace all the math group placeholders in the text |
| 152 | + // with the saved strings. |
| 153 | + return text.replace(/@@(\d+)@@/g, process); |
| 154 | +} |
| 155 | + |
| 156 | +/** |
| 157 | + * Process math blocks. |
| 158 | + * |
| 159 | + * The math is in blocks i through j, so |
| 160 | + * collect it into one block and clear the others. |
| 161 | + * Replace &, <, and > by named entities. |
| 162 | + * For IE, put <br> at the ends of comments since IE removes \n. |
| 163 | + * Clear the current math positions and store the index of the |
| 164 | + * math, then push the math string onto the storage array. |
| 165 | + * The preProcess function is called on all blocks if it has been passed in |
| 166 | + */ |
| 167 | +function processMath( |
| 168 | + i: number, |
| 169 | + j: number, |
| 170 | + preProcess: (input: string) => string, |
| 171 | + math: string[], |
| 172 | + blocks: string[] |
| 173 | +): string[] { |
| 174 | + let block = blocks |
| 175 | + .slice(i, j + 1) |
| 176 | + .join('') |
| 177 | + .replace(/&/g, '&') // use HTML entity for & |
| 178 | + .replace(/</g, '<') // use HTML entity for < |
| 179 | + .replace(/>/g, '>'); // use HTML entity for > |
| 180 | + if (navigator && navigator.appName === 'Microsoft Internet Explorer') { |
| 181 | + block = block.replace(/(%[^\n]*)\n/g, '$1<br/>\n'); |
| 182 | + } |
| 183 | + while (j > i) { |
| 184 | + blocks[j] = ''; |
| 185 | + j--; |
| 186 | + } |
| 187 | + blocks[i] = '@@' + math.length + '@@'; // replace the current block text with a unique tag to find later |
| 188 | + if (preProcess) { |
| 189 | + block = preProcess(block); |
| 190 | + } |
| 191 | + math.push(block); |
| 192 | + return blocks; |
| 193 | +} |
0 commit comments