at dev 284 lines 8.2 kB view raw
1 2function makeLeaf(tag, content) { 3 return () => { 4 const element = $mathElement(tag); 5 element.textContent = content; 6 return element; 7 }; 8} 9 10function make(node) { 11 if (node.type === "group") { 12 return node.make(node.children); 13 } 14 else if (node.type === "leaf") { 15 return node.make(); 16 } 17 else { 18 throw new Error(`Node of type ${node.type} should not exist by the final make stage.`); 19 } 20} 21 22function makeGroup(tag) { 23 return (children) => { 24 const element = $mathElement(tag); 25 for (const child of children) { 26 element.appendChild(make(child)); 27 } 28 return element; 29 }; 30} 31 32export const greek = { 33 "alpha": "α", 34 "beta": "β", 35 "gamma": "γ", 36 "delta": "δ", 37 "epsilon": "ε", 38 "zeta": "ζ", 39 "eta": "η", 40 "theta": "θ", 41 "iota": "ι", 42 "kappa": "κ", 43 "lambda": "λ", 44 "mu": "μ", 45 "nu": "ν", 46 "xi": "ξ", 47 "omicron": "ο", 48 "pi": "π", 49 "rho": "ρ", 50 "sigma": "σ", 51 "tau": "τ", 52 "upsilon": "υ", 53 "phi": "ϕ", 54 "curlyphi": "φ", 55 "chi": "χ", 56 "psi": "ψ", 57 "omega": "ω" 58}; // TODO: capital letters 59 60const commonOps = { 61 "interpunct": "·" 62}; 63 64const whitespace = /\s/; 65const numeric = /[\d.,]/; 66const alphabet = /[a-zA-Z]/; 67 68function tokenize(expression, declarations) { 69 const tokens = []; 70 let current = ""; 71 let i = 0; 72 73 function token(source) { 74 let result; 75 if (numeric.test(source[0])) { 76 result = declarations["numeric"](source) 77 } else { 78 result = declarations[source]; 79 } 80 if (!result) throw new Error(`Undeclared token "${source}"`); 81 tokens.push(result); 82 } 83 84 while (i < expression.length) { 85 const char = expression[i]; 86 const begun = current !== ""; 87 88 if (/\s/.test(char)) { 89 if (begun) token(current); 90 current = ""; 91 } else if (alphabet.test(char)) { 92 if (begun && !alphabet.test(current[current.length - 1])) { 93 token(current); 94 current = char; 95 } else { 96 current += char; 97 } 98 } else if (numeric.test(char)) { // numerics 99 if (begun && !numeric.test(current[current.length - 1])) { 100 token(current); 101 current = char; 102 } else { 103 current += char; 104 } 105 } else { 106 if (begun) token(current); 107 token(char); 108 current = ""; 109 } 110 i++; 111 } 112 113 if (current !== "") token(current); 114 115 return tokens; 116} 117 118export async function main(target, expression) { 119 120 const lines = expression.trim().split("\n"); 121 122 const idents = {}; 123 const texts = {}; 124 const ops = {}; 125 const contentLines = []; 126 127 let contentStarted = false; 128 for (const line of lines) { 129 if (contentStarted) { 130 contentLines.push(line); 131 continue; 132 } 133 const trimmed = line.trim(); 134 if (trimmed === "in") { 135 contentStarted = true; 136 continue; 137 } 138 139 if (trimmed.startsWith("ident ")) { 140 const declarations = trimmed.split(' ').filter(_=>_).slice(1); 141 142 for (const declaration of declarations) { 143 const splitAt = declaration.indexOf(':'); 144 145 if (splitAt === -1) { 146 idents[declaration] = declaration; 147 } else { 148 const key = declaration.slice(0, splitAt); 149 const value = declaration.slice(splitAt + 1); 150 151 idents[key] = value === '~' ? greek[key] : value; 152 } 153 } 154 } 155 156 if (trimmed.startsWith("op ")) { 157 const declarations = trimmed.split(' ').filter(_=>_).slice(1); 158 159 for (const declaration of declarations) { 160 const splitAt = declaration.indexOf(':'); 161 162 if (splitAt === -1) { 163 ops[declaration] = declaration; 164 } else { 165 const key = declaration.slice(0, splitAt); 166 const value = declaration.slice(splitAt + 1); 167 168 ops[key] = value.startsWith("$") ? commonOps[value.slice(1)] : value; 169 } 170 } 171 } 172 173 // Claude's work, not fully reviewed 174 if (trimmed.startsWith("text ")) { 175 const declarations = trimmed.slice(5); 176 let i = 0; 177 178 while (i < declarations.length) { 179 const splitAt = declarations.indexOf(':', i); 180 if (splitAt === -1) break; 181 182 const key = declarations.slice(i, splitAt); 183 i = splitAt + 1; 184 185 if (i >= declarations.length) break; 186 187 let value; 188 if (declarations[i] === '~') { 189 value = key; 190 i += 1; 191 } else if (declarations[i] === '"') { 192 i += 1; 193 const closingQuote = declarations.indexOf('"', i); 194 if (closingQuote === -1) { 195 console.error(`Unclosed quote in text declarations: ${declarations}`); 196 break; 197 } 198 value = declarations.slice(i, closingQuote); 199 i = closingQuote + 1; 200 } else { 201 console.error(`Invalid declaration for text ${key}: ${declarations}`); 202 break; 203 } 204 205 texts[key] = value; 206 207 while (i < declarations.length && declarations[i] === " ") { 208 i += 1; 209 } 210 } 211 } 212 213 } 214 215 const declaredTokens = { 216 "^": { type: "infix", make: makeGroup("msup") }, 217 "_": { type: "infix", make: makeGroup("msub") }, 218 "{": { type: "row_begin" }, 219 "}": { type: "row_end" }, 220 "numeric": n => ({ type: "leaf", make: makeLeaf("mn", n) }) 221 }; 222 223 for (const ident in idents) { 224 declaredTokens[ident] = { type: "leaf", make: makeLeaf("mi", idents[ident]) }; 225 } 226 for (const op in ops) { 227 declaredTokens[op] = { type: "leaf", make: makeLeaf("mo", ops[op]) }; 228 } 229 for (const text in texts) { 230 declaredTokens[text] = { type: "leaf", make: makeLeaf("mtext", texts[text]) }; 231 } 232 233 for (const line of contentLines) { 234 const trimmed = line.trim(); 235 if (!trimmed) continue; 236 237 const tokens = tokenize(trimmed, declaredTokens); 238 239 const root = { type: "group", make: makeGroup("math"), children: [] }; 240 const groupStack = [root]; 241 for (const token of tokens) { 242 if (token.type === "row_begin") { 243 groupStack.push({ type: "group", make: makeGroup("mrow"), children: [] }); 244 } 245 else if (token.type === "row_end") { 246 const row = groupStack.pop(); 247 groupStack[groupStack.length - 1].children.push(row); 248 } 249 else { 250 groupStack[groupStack.length - 1].children.push(token); 251 } 252 } 253 254 while (groupStack.length > 0) { 255 const node = groupStack.pop(); 256 const modified = []; 257 258 let i = 0; 259 for (; i <= node.children.length - 3; i++) { 260 const [left, middle, right] = [node.children[i], node.children[i + 1], node.children[i + 2]]; 261 if (middle.type === "infix") { 262 const newGroup = { type: "group", make: middle.make, children: [left, right] }; 263 modified.push(newGroup); 264 groupStack.push(newGroup); 265 i += 2; 266 } 267 else { 268 modified.push(left); 269 if (left.type === "group") groupStack.push(left); 270 } 271 } 272 for (; i < node.children.length; i++) { 273 modified.push(node.children[i]); 274 if (node.children[i].type === "group") groupStack.push(node.children[i]); 275 } 276 277 node.children = modified; 278 279 } 280 281 target.appendChild(make(root)); 282 } 283} 284