1import katex from '../katex.mjs'; 2 3/* eslint no-constant-condition:0 */ 4const findEndOfMath = function findEndOfMath(delimiter, text, startIndex) { 5 // Adapted from 6 // https://github.com/Khan/perseus/blob/master/src/perseus-markdown.jsx 7 let index = startIndex; 8 let braceLevel = 0; 9 const delimLength = delimiter.length; 10 11 while (index < text.length) { 12 const 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 30const splitAtDelimiters = function splitAtDelimiters(startData, leftDelim, rightDelim, display) { 31 const finalData = []; 32 33 for (let i = 0; i < startData.length; i++) { 34 if (startData[i].type === "text") { 35 const text = startData[i].data; 36 let lookingForLeft = true; 37 let currIndex = 0; 38 let nextIndex; 39 nextIndex = text.indexOf(leftDelim); 40 41 if (nextIndex !== -1) { 42 currIndex = nextIndex; 43 finalData.push({ 44 type: "text", 45 data: text.slice(0, currIndex) 46 }); 47 lookingForLeft = false; 48 } 49 50 while (true) { 51 if (lookingForLeft) { 52 nextIndex = text.indexOf(leftDelim, currIndex); 53 54 if (nextIndex === -1) { 55 break; 56 } 57 58 finalData.push({ 59 type: "text", 60 data: text.slice(currIndex, nextIndex) 61 }); 62 currIndex = nextIndex; 63 } else { 64 nextIndex = findEndOfMath(rightDelim, text, currIndex + leftDelim.length); 65 66 if (nextIndex === -1) { 67 break; 68 } 69 70 finalData.push({ 71 type: "math", 72 data: text.slice(currIndex + leftDelim.length, nextIndex), 73 rawData: text.slice(currIndex, nextIndex + rightDelim.length), 74 display: display 75 }); 76 currIndex = nextIndex + rightDelim.length; 77 } 78 79 lookingForLeft = !lookingForLeft; 80 } 81 82 finalData.push({ 83 type: "text", 84 data: text.slice(currIndex) 85 }); 86 } else { 87 finalData.push(startData[i]); 88 } 89 } 90 91 return finalData; 92}; 93 94/* eslint no-console:0 */ 95 96const splitWithDelimiters = function splitWithDelimiters(text, delimiters) { 97 let data = [{ 98 type: "text", 99 data: text 100 }]; 101 102 for (let i = 0; i < delimiters.length; i++) { 103 const delimiter = delimiters[i]; 104 data = splitAtDelimiters(data, delimiter.left, delimiter.right, delimiter.display || false); 105 } 106 107 return data; 108}; 109/* Note: optionsCopy is mutated by this method. If it is ever exposed in the 110 * API, we should copy it before mutating. 111 */ 112 113 114const renderMathInText = function renderMathInText(text, optionsCopy) { 115 const data = splitWithDelimiters(text, optionsCopy.delimiters); 116 const fragment = document.createDocumentFragment(); 117 118 for (let i = 0; i < data.length; i++) { 119 if (data[i].type === "text") { 120 fragment.appendChild(document.createTextNode(data[i].data)); 121 } else { 122 const span = document.createElement("span"); 123 let math = data[i].data; // Override any display mode defined in the settings with that 124 // defined by the text itself 125 126 optionsCopy.displayMode = data[i].display; 127 128 try { 129 if (optionsCopy.preProcess) { 130 math = optionsCopy.preProcess(math); 131 } 132 133 katex.render(math, span, optionsCopy); 134 } catch (e) { 135 if (!(e instanceof katex.ParseError)) { 136 throw e; 137 } 138 139 optionsCopy.errorCallback("KaTeX auto-render: Failed to parse `" + data[i].data + "` with ", e); 140 fragment.appendChild(document.createTextNode(data[i].rawData)); 141 continue; 142 } 143 144 fragment.appendChild(span); 145 } 146 } 147 148 return fragment; 149}; 150 151const renderElem = function renderElem(elem, optionsCopy) { 152 for (let i = 0; i < elem.childNodes.length; i++) { 153 const childNode = elem.childNodes[i]; 154 155 if (childNode.nodeType === 3) { 156 // Text node 157 const frag = renderMathInText(childNode.textContent, optionsCopy); 158 i += frag.childNodes.length - 1; 159 elem.replaceChild(frag, childNode); 160 } else if (childNode.nodeType === 1) { 161 // Element node 162 const className = ' ' + childNode.className + ' '; 163 const shouldRender = optionsCopy.ignoredTags.indexOf(childNode.nodeName.toLowerCase()) === -1 && optionsCopy.ignoredClasses.every(x => className.indexOf(' ' + x + ' ') === -1); 164 165 if (shouldRender) { 166 renderElem(childNode, optionsCopy); 167 } 168 } // Otherwise, it's something else, and ignore it. 169 170 } 171}; 172 173const renderMathInElement = function renderMathInElement(elem, options) { 174 if (!elem) { 175 throw new Error("No element provided to render"); 176 } 177 178 const optionsCopy = {}; // Object.assign(optionsCopy, option) 179 180 for (const option in options) { 181 if (options.hasOwnProperty(option)) { 182 optionsCopy[option] = options[option]; 183 } 184 } // default options 185 186 187 optionsCopy.delimiters = optionsCopy.delimiters || [{ 188 left: "$$", 189 right: "$$", 190 display: true 191 }, { 192 left: "\\(", 193 right: "\\)", 194 display: false 195 }, // LaTeX uses $…$, but it ruins the display of normal `$` in text: 196 // {left: "$", right: "$", display: false}, 197 // \[…\] must come last in this array. Otherwise, renderMathInElement 198 // will search for \[ before it searches for $$ or \( 199 // That makes it susceptible to finding a \\[0.3em] row delimiter and 200 // treating it as if it were the start of a KaTeX math zone. 201 { 202 left: "\\[", 203 right: "\\]", 204 display: true 205 }]; 206 optionsCopy.ignoredTags = optionsCopy.ignoredTags || ["script", "noscript", "style", "textarea", "pre", "code"]; 207 optionsCopy.ignoredClasses = optionsCopy.ignoredClasses || []; 208 optionsCopy.errorCallback = optionsCopy.errorCallback || console.error; // Enable sharing of global macros defined via `\gdef` between different 209 // math elements within a single call to `renderMathInElement`. 210 211 optionsCopy.macros = optionsCopy.macros || {}; 212 renderElem(elem, optionsCopy); 213}; 214 215export default renderMathInElement; 216