auto-render.mjs (6081B)
1 import katex from '../katex.mjs'; 2 3 /* eslint no-constant-condition:0 */ 4 var findEndOfMath = function findEndOfMath(delimiter, text, startIndex) { 5 // Adapted from 6 // https://github.com/Khan/perseus/blob/master/src/perseus-markdown.jsx 7 var index = startIndex; 8 var braceLevel = 0; 9 var delimLength = delimiter.length; 10 11 while (index < text.length) { 12 var character = text[index]; 13 14 if (braceLevel <= 0 && text.slice(index, index + delimLength) === delimiter) { 15 return index; 16 } else if (character === "\\") { 17 index++; 18 } else if (character === "{") { 19 braceLevel++; 20 } else if (character === "}") { 21 braceLevel--; 22 } 23 24 index++; 25 } 26 27 return -1; 28 }; 29 30 var escapeRegex = function escapeRegex(string) { 31 return string.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&"); 32 }; 33 34 var amsRegex = /^\\begin{/; 35 36 var splitAtDelimiters = function splitAtDelimiters(text, delimiters) { 37 var index; 38 var data = []; 39 var regexLeft = new RegExp("(" + delimiters.map(x => escapeRegex(x.left)).join("|") + ")"); 40 41 while (true) { 42 index = text.search(regexLeft); 43 44 if (index === -1) { 45 break; 46 } 47 48 if (index > 0) { 49 data.push({ 50 type: "text", 51 data: text.slice(0, index) 52 }); 53 text = text.slice(index); // now text starts with delimiter 54 } // ... so this always succeeds: 55 56 57 var i = delimiters.findIndex(delim => text.startsWith(delim.left)); 58 index = findEndOfMath(delimiters[i].right, text, delimiters[i].left.length); 59 60 if (index === -1) { 61 break; 62 } 63 64 var rawData = text.slice(0, index + delimiters[i].right.length); 65 var math = amsRegex.test(rawData) ? rawData : text.slice(delimiters[i].left.length, index); 66 data.push({ 67 type: "math", 68 data: math, 69 rawData, 70 display: delimiters[i].display 71 }); 72 text = text.slice(index + delimiters[i].right.length); 73 } 74 75 if (text !== "") { 76 data.push({ 77 type: "text", 78 data: text 79 }); 80 } 81 82 return data; 83 }; 84 85 /* eslint no-console:0 */ 86 /* Note: optionsCopy is mutated by this method. If it is ever exposed in the 87 * API, we should copy it before mutating. 88 */ 89 90 var renderMathInText = function renderMathInText(text, optionsCopy) { 91 var data = splitAtDelimiters(text, optionsCopy.delimiters); 92 93 if (data.length === 1 && data[0].type === 'text') { 94 // There is no formula in the text. 95 // Let's return null which means there is no need to replace 96 // the current text node with a new one. 97 return null; 98 } 99 100 var fragment = document.createDocumentFragment(); 101 102 for (var i = 0; i < data.length; i++) { 103 if (data[i].type === "text") { 104 fragment.appendChild(document.createTextNode(data[i].data)); 105 } else { 106 var span = document.createElement("span"); 107 var math = data[i].data; // Override any display mode defined in the settings with that 108 // defined by the text itself 109 110 optionsCopy.displayMode = data[i].display; 111 112 try { 113 if (optionsCopy.preProcess) { 114 math = optionsCopy.preProcess(math); 115 } 116 117 katex.render(math, span, optionsCopy); 118 } catch (e) { 119 if (!(e instanceof katex.ParseError)) { 120 throw e; 121 } 122 123 optionsCopy.errorCallback("KaTeX auto-render: Failed to parse `" + data[i].data + "` with ", e); 124 fragment.appendChild(document.createTextNode(data[i].rawData)); 125 continue; 126 } 127 128 fragment.appendChild(span); 129 } 130 } 131 132 return fragment; 133 }; 134 135 var renderElem = function renderElem(elem, optionsCopy) { 136 for (var i = 0; i < elem.childNodes.length; i++) { 137 var childNode = elem.childNodes[i]; 138 139 if (childNode.nodeType === 3) { 140 // Text node 141 var frag = renderMathInText(childNode.textContent, optionsCopy); 142 143 if (frag) { 144 i += frag.childNodes.length - 1; 145 elem.replaceChild(frag, childNode); 146 } 147 } else if (childNode.nodeType === 1) { 148 (function () { 149 // Element node 150 var className = ' ' + childNode.className + ' '; 151 var shouldRender = optionsCopy.ignoredTags.indexOf(childNode.nodeName.toLowerCase()) === -1 && optionsCopy.ignoredClasses.every(x => className.indexOf(' ' + x + ' ') === -1); 152 153 if (shouldRender) { 154 renderElem(childNode, optionsCopy); 155 } 156 })(); 157 } // Otherwise, it's something else, and ignore it. 158 159 } 160 }; 161 162 var renderMathInElement = function renderMathInElement(elem, options) { 163 if (!elem) { 164 throw new Error("No element provided to render"); 165 } 166 167 var optionsCopy = {}; // Object.assign(optionsCopy, option) 168 169 for (var option in options) { 170 if (options.hasOwnProperty(option)) { 171 optionsCopy[option] = options[option]; 172 } 173 } // default options 174 175 176 optionsCopy.delimiters = optionsCopy.delimiters || [{ 177 left: "$$", 178 right: "$$", 179 display: true 180 }, { 181 left: "\\(", 182 right: "\\)", 183 display: false 184 }, // LaTeX uses $…$, but it ruins the display of normal `$` in text: 185 // {left: "$", right: "$", display: false}, 186 // $ must come after $$ 187 // Render AMS environments even if outside $$…$$ delimiters. 188 { 189 left: "\\begin{equation}", 190 right: "\\end{equation}", 191 display: true 192 }, { 193 left: "\\begin{align}", 194 right: "\\end{align}", 195 display: true 196 }, { 197 left: "\\begin{alignat}", 198 right: "\\end{alignat}", 199 display: true 200 }, { 201 left: "\\begin{gather}", 202 right: "\\end{gather}", 203 display: true 204 }, { 205 left: "\\begin{CD}", 206 right: "\\end{CD}", 207 display: true 208 }, { 209 left: "\\[", 210 right: "\\]", 211 display: true 212 }]; 213 optionsCopy.ignoredTags = optionsCopy.ignoredTags || ["script", "noscript", "style", "textarea", "pre", "code", "option"]; 214 optionsCopy.ignoredClasses = optionsCopy.ignoredClasses || []; 215 optionsCopy.errorCallback = optionsCopy.errorCallback || console.error; // Enable sharing of global macros defined via `\gdef` between different 216 // math elements within a single call to `renderMathInElement`. 217 218 optionsCopy.macros = optionsCopy.macros || {}; 219 renderElem(elem, optionsCopy); 220 }; 221 222 export default renderMathInElement;