buildMathML.js (16233B)
1 /** 2 * This file converts a parse tree into a cooresponding MathML tree. The main 3 * entry point is the `buildMathML` function, which takes a parse tree from the 4 * parser. 5 */ 6 7 const buildCommon = require("./buildCommon"); 8 const fontMetrics = require("./fontMetrics"); 9 const mathMLTree = require("./mathMLTree"); 10 const ParseError = require("./ParseError"); 11 const symbols = require("./symbols"); 12 const utils = require("./utils"); 13 14 const makeSpan = buildCommon.makeSpan; 15 const fontMap = buildCommon.fontMap; 16 17 /** 18 * Takes a symbol and converts it into a MathML text node after performing 19 * optional replacement from symbols.js. 20 */ 21 const makeText = function(text, mode) { 22 if (symbols[mode][text] && symbols[mode][text].replace) { 23 text = symbols[mode][text].replace; 24 } 25 26 return new mathMLTree.TextNode(text); 27 }; 28 29 /** 30 * Returns the math variant as a string or null if none is required. 31 */ 32 const getVariant = function(group, options) { 33 const font = options.font; 34 if (!font) { 35 return null; 36 } 37 38 const mode = group.mode; 39 if (font === "mathit") { 40 return "italic"; 41 } 42 43 let value = group.value; 44 if (utils.contains(["\\imath", "\\jmath"], value)) { 45 return null; 46 } 47 48 if (symbols[mode][value] && symbols[mode][value].replace) { 49 value = symbols[mode][value].replace; 50 } 51 52 const fontName = fontMap[font].fontName; 53 if (fontMetrics.getCharacterMetrics(value, fontName)) { 54 return fontMap[options.font].variant; 55 } 56 57 return null; 58 }; 59 60 /** 61 * Functions for handling the different types of groups found in the parse 62 * tree. Each function should take a parse group and return a MathML node. 63 */ 64 const groupTypes = {}; 65 66 groupTypes.mathord = function(group, options) { 67 const node = new mathMLTree.MathNode( 68 "mi", 69 [makeText(group.value, group.mode)]); 70 71 const variant = getVariant(group, options); 72 if (variant) { 73 node.setAttribute("mathvariant", variant); 74 } 75 return node; 76 }; 77 78 groupTypes.textord = function(group, options) { 79 const text = makeText(group.value, group.mode); 80 81 const variant = getVariant(group, options) || "normal"; 82 83 let node; 84 if (/[0-9]/.test(group.value)) { 85 // TODO(kevinb) merge adjacent <mn> nodes 86 // do it as a post processing step 87 node = new mathMLTree.MathNode("mn", [text]); 88 if (options.font) { 89 node.setAttribute("mathvariant", variant); 90 } 91 } else { 92 node = new mathMLTree.MathNode("mi", [text]); 93 node.setAttribute("mathvariant", variant); 94 } 95 96 return node; 97 }; 98 99 groupTypes.bin = function(group) { 100 const node = new mathMLTree.MathNode( 101 "mo", [makeText(group.value, group.mode)]); 102 103 return node; 104 }; 105 106 groupTypes.rel = function(group) { 107 const node = new mathMLTree.MathNode( 108 "mo", [makeText(group.value, group.mode)]); 109 110 return node; 111 }; 112 113 groupTypes.open = function(group) { 114 const node = new mathMLTree.MathNode( 115 "mo", [makeText(group.value, group.mode)]); 116 117 return node; 118 }; 119 120 groupTypes.close = function(group) { 121 const node = new mathMLTree.MathNode( 122 "mo", [makeText(group.value, group.mode)]); 123 124 return node; 125 }; 126 127 groupTypes.inner = function(group) { 128 const node = new mathMLTree.MathNode( 129 "mo", [makeText(group.value, group.mode)]); 130 131 return node; 132 }; 133 134 groupTypes.punct = function(group) { 135 const node = new mathMLTree.MathNode( 136 "mo", [makeText(group.value, group.mode)]); 137 138 node.setAttribute("separator", "true"); 139 140 return node; 141 }; 142 143 groupTypes.ordgroup = function(group, options) { 144 const inner = buildExpression(group.value, options); 145 146 const node = new mathMLTree.MathNode("mrow", inner); 147 148 return node; 149 }; 150 151 groupTypes.text = function(group, options) { 152 const inner = buildExpression(group.value.body, options); 153 154 const node = new mathMLTree.MathNode("mtext", inner); 155 156 return node; 157 }; 158 159 groupTypes.color = function(group, options) { 160 const inner = buildExpression(group.value.value, options); 161 162 const node = new mathMLTree.MathNode("mstyle", inner); 163 164 node.setAttribute("mathcolor", group.value.color); 165 166 return node; 167 }; 168 169 groupTypes.supsub = function(group, options) { 170 const children = [buildGroup(group.value.base, options)]; 171 172 if (group.value.sub) { 173 children.push(buildGroup(group.value.sub, options)); 174 } 175 176 if (group.value.sup) { 177 children.push(buildGroup(group.value.sup, options)); 178 } 179 180 let nodeType; 181 if (!group.value.sub) { 182 nodeType = "msup"; 183 } else if (!group.value.sup) { 184 nodeType = "msub"; 185 } else { 186 nodeType = "msubsup"; 187 } 188 189 const node = new mathMLTree.MathNode(nodeType, children); 190 191 return node; 192 }; 193 194 groupTypes.genfrac = function(group, options) { 195 const node = new mathMLTree.MathNode( 196 "mfrac", 197 [ 198 buildGroup(group.value.numer, options), 199 buildGroup(group.value.denom, options), 200 ]); 201 202 if (!group.value.hasBarLine) { 203 node.setAttribute("linethickness", "0px"); 204 } 205 206 if (group.value.leftDelim != null || group.value.rightDelim != null) { 207 const withDelims = []; 208 209 if (group.value.leftDelim != null) { 210 const leftOp = new mathMLTree.MathNode( 211 "mo", [new mathMLTree.TextNode(group.value.leftDelim)]); 212 213 leftOp.setAttribute("fence", "true"); 214 215 withDelims.push(leftOp); 216 } 217 218 withDelims.push(node); 219 220 if (group.value.rightDelim != null) { 221 const rightOp = new mathMLTree.MathNode( 222 "mo", [new mathMLTree.TextNode(group.value.rightDelim)]); 223 224 rightOp.setAttribute("fence", "true"); 225 226 withDelims.push(rightOp); 227 } 228 229 const outerNode = new mathMLTree.MathNode("mrow", withDelims); 230 231 return outerNode; 232 } 233 234 return node; 235 }; 236 237 groupTypes.array = function(group, options) { 238 return new mathMLTree.MathNode( 239 "mtable", group.value.body.map(function(row) { 240 return new mathMLTree.MathNode( 241 "mtr", row.map(function(cell) { 242 return new mathMLTree.MathNode( 243 "mtd", [buildGroup(cell, options)]); 244 })); 245 })); 246 }; 247 248 groupTypes.sqrt = function(group, options) { 249 let node; 250 if (group.value.index) { 251 node = new mathMLTree.MathNode( 252 "mroot", [ 253 buildGroup(group.value.body, options), 254 buildGroup(group.value.index, options), 255 ]); 256 } else { 257 node = new mathMLTree.MathNode( 258 "msqrt", [buildGroup(group.value.body, options)]); 259 } 260 261 return node; 262 }; 263 264 groupTypes.leftright = function(group, options) { 265 const inner = buildExpression(group.value.body, options); 266 267 if (group.value.left !== ".") { 268 const leftNode = new mathMLTree.MathNode( 269 "mo", [makeText(group.value.left, group.mode)]); 270 271 leftNode.setAttribute("fence", "true"); 272 273 inner.unshift(leftNode); 274 } 275 276 if (group.value.right !== ".") { 277 const rightNode = new mathMLTree.MathNode( 278 "mo", [makeText(group.value.right, group.mode)]); 279 280 rightNode.setAttribute("fence", "true"); 281 282 inner.push(rightNode); 283 } 284 285 const outerNode = new mathMLTree.MathNode("mrow", inner); 286 287 return outerNode; 288 }; 289 290 groupTypes.middle = function(group, options) { 291 const middleNode = new mathMLTree.MathNode( 292 "mo", [makeText(group.value.middle, group.mode)]); 293 middleNode.setAttribute("fence", "true"); 294 return middleNode; 295 }; 296 297 groupTypes.accent = function(group, options) { 298 const accentNode = new mathMLTree.MathNode( 299 "mo", [makeText(group.value.accent, group.mode)]); 300 301 const node = new mathMLTree.MathNode( 302 "mover", 303 [buildGroup(group.value.base, options), accentNode]); 304 305 node.setAttribute("accent", "true"); 306 307 return node; 308 }; 309 310 groupTypes.spacing = function(group) { 311 let node; 312 313 if (group.value === "\\ " || group.value === "\\space" || 314 group.value === " " || group.value === "~") { 315 node = new mathMLTree.MathNode( 316 "mtext", [new mathMLTree.TextNode("\u00a0")]); 317 } else { 318 node = new mathMLTree.MathNode("mspace"); 319 320 node.setAttribute( 321 "width", buildCommon.spacingFunctions[group.value].size); 322 } 323 324 return node; 325 }; 326 327 groupTypes.op = function(group, options) { 328 let node; 329 330 // TODO(emily): handle big operators using the `largeop` attribute 331 332 if (group.value.symbol) { 333 // This is a symbol. Just add the symbol. 334 node = new mathMLTree.MathNode( 335 "mo", [makeText(group.value.body, group.mode)]); 336 } else if (group.value.value) { 337 // This is an operator with children. Add them. 338 node = new mathMLTree.MathNode( 339 "mo", buildExpression(group.value.value, options)); 340 } else { 341 // This is a text operator. Add all of the characters from the 342 // operator's name. 343 // TODO(emily): Add a space in the middle of some of these 344 // operators, like \limsup. 345 node = new mathMLTree.MathNode( 346 "mi", [new mathMLTree.TextNode(group.value.body.slice(1))]); 347 } 348 349 return node; 350 }; 351 352 groupTypes.mod = function(group, options) { 353 let inner = []; 354 355 if (group.value.modType === "pod" || group.value.modType === "pmod") { 356 inner.push(new mathMLTree.MathNode( 357 "mo", [makeText("(", group.mode)])); 358 } 359 if (group.value.modType !== "pod") { 360 inner.push(new mathMLTree.MathNode( 361 "mo", [makeText("mod", group.mode)])); 362 } 363 if (group.value.value) { 364 const space = new mathMLTree.MathNode("mspace"); 365 space.setAttribute("width", "0.333333em"); 366 inner.push(space); 367 inner = inner.concat(buildExpression(group.value.value, options)); 368 } 369 if (group.value.modType === "pod" || group.value.modType === "pmod") { 370 inner.push(new mathMLTree.MathNode( 371 "mo", [makeText(")", group.mode)])); 372 } 373 374 return new mathMLTree.MathNode("mo", inner); 375 }; 376 377 groupTypes.katex = function(group) { 378 const node = new mathMLTree.MathNode( 379 "mtext", [new mathMLTree.TextNode("KaTeX")]); 380 381 return node; 382 }; 383 384 groupTypes.font = function(group, options) { 385 const font = group.value.font; 386 return buildGroup(group.value.body, options.withFont(font)); 387 }; 388 389 groupTypes.delimsizing = function(group) { 390 const children = []; 391 392 if (group.value.value !== ".") { 393 children.push(makeText(group.value.value, group.mode)); 394 } 395 396 const node = new mathMLTree.MathNode("mo", children); 397 398 if (group.value.mclass === "mopen" || 399 group.value.mclass === "mclose") { 400 // Only some of the delimsizing functions act as fences, and they 401 // return "mopen" or "mclose" mclass. 402 node.setAttribute("fence", "true"); 403 } else { 404 // Explicitly disable fencing if it's not a fence, to override the 405 // defaults. 406 node.setAttribute("fence", "false"); 407 } 408 409 return node; 410 }; 411 412 groupTypes.styling = function(group, options) { 413 const inner = buildExpression(group.value.value, options); 414 415 const node = new mathMLTree.MathNode("mstyle", inner); 416 417 const styleAttributes = { 418 "display": ["0", "true"], 419 "text": ["0", "false"], 420 "script": ["1", "false"], 421 "scriptscript": ["2", "false"], 422 }; 423 424 const attr = styleAttributes[group.value.style]; 425 426 node.setAttribute("scriptlevel", attr[0]); 427 node.setAttribute("displaystyle", attr[1]); 428 429 return node; 430 }; 431 432 groupTypes.sizing = function(group, options) { 433 const inner = buildExpression(group.value.value, options); 434 435 const node = new mathMLTree.MathNode("mstyle", inner); 436 437 // TODO(emily): This doesn't produce the correct size for nested size 438 // changes, because we don't keep state of what style we're currently 439 // in, so we can't reset the size to normal before changing it. Now 440 // that we're passing an options parameter we should be able to fix 441 // this. 442 node.setAttribute( 443 "mathsize", buildCommon.sizingMultiplier[group.value.size] + "em"); 444 445 return node; 446 }; 447 448 groupTypes.overline = function(group, options) { 449 const operator = new mathMLTree.MathNode( 450 "mo", [new mathMLTree.TextNode("\u203e")]); 451 operator.setAttribute("stretchy", "true"); 452 453 const node = new mathMLTree.MathNode( 454 "mover", 455 [buildGroup(group.value.body, options), operator]); 456 node.setAttribute("accent", "true"); 457 458 return node; 459 }; 460 461 groupTypes.underline = function(group, options) { 462 const operator = new mathMLTree.MathNode( 463 "mo", [new mathMLTree.TextNode("\u203e")]); 464 operator.setAttribute("stretchy", "true"); 465 466 const node = new mathMLTree.MathNode( 467 "munder", 468 [buildGroup(group.value.body, options), operator]); 469 node.setAttribute("accentunder", "true"); 470 471 return node; 472 }; 473 474 groupTypes.rule = function(group) { 475 // TODO(emily): Figure out if there's an actual way to draw black boxes 476 // in MathML. 477 const node = new mathMLTree.MathNode("mrow"); 478 479 return node; 480 }; 481 482 groupTypes.kern = function(group) { 483 // TODO(kevin): Figure out if there's a way to add space in MathML 484 const node = new mathMLTree.MathNode("mrow"); 485 486 return node; 487 }; 488 489 groupTypes.llap = function(group, options) { 490 const node = new mathMLTree.MathNode( 491 "mpadded", [buildGroup(group.value.body, options)]); 492 493 node.setAttribute("lspace", "-1width"); 494 node.setAttribute("width", "0px"); 495 496 return node; 497 }; 498 499 groupTypes.rlap = function(group, options) { 500 const node = new mathMLTree.MathNode( 501 "mpadded", [buildGroup(group.value.body, options)]); 502 503 node.setAttribute("width", "0px"); 504 505 return node; 506 }; 507 508 groupTypes.phantom = function(group, options) { 509 const inner = buildExpression(group.value.value, options); 510 return new mathMLTree.MathNode("mphantom", inner); 511 }; 512 513 groupTypes.mclass = function(group, options) { 514 const inner = buildExpression(group.value.value, options); 515 return new mathMLTree.MathNode("mstyle", inner); 516 }; 517 518 /** 519 * Takes a list of nodes, builds them, and returns a list of the generated 520 * MathML nodes. A little simpler than the HTML version because we don't do any 521 * previous-node handling. 522 */ 523 const buildExpression = function(expression, options) { 524 const groups = []; 525 for (let i = 0; i < expression.length; i++) { 526 const group = expression[i]; 527 groups.push(buildGroup(group, options)); 528 } 529 return groups; 530 }; 531 532 /** 533 * Takes a group from the parser and calls the appropriate groupTypes function 534 * on it to produce a MathML node. 535 */ 536 const buildGroup = function(group, options) { 537 if (!group) { 538 return new mathMLTree.MathNode("mrow"); 539 } 540 541 if (groupTypes[group.type]) { 542 // Call the groupTypes function 543 return groupTypes[group.type](group, options); 544 } else { 545 throw new ParseError( 546 "Got group of unknown type: '" + group.type + "'"); 547 } 548 }; 549 550 /** 551 * Takes a full parse tree and settings and builds a MathML representation of 552 * it. In particular, we put the elements from building the parse tree into a 553 * <semantics> tag so we can also include that TeX source as an annotation. 554 * 555 * Note that we actually return a domTree element with a `<math>` inside it so 556 * we can do appropriate styling. 557 */ 558 const buildMathML = function(tree, texExpression, options) { 559 const expression = buildExpression(tree, options); 560 561 // Wrap up the expression in an mrow so it is presented in the semantics 562 // tag correctly. 563 const wrapper = new mathMLTree.MathNode("mrow", expression); 564 565 // Build a TeX annotation of the source 566 const annotation = new mathMLTree.MathNode( 567 "annotation", [new mathMLTree.TextNode(texExpression)]); 568 569 annotation.setAttribute("encoding", "application/x-tex"); 570 571 const semantics = new mathMLTree.MathNode( 572 "semantics", [wrapper, annotation]); 573 574 const math = new mathMLTree.MathNode("math", [semantics]); 575 576 // You can't style <math> nodes, so we wrap the node in a span. 577 return makeSpan(["katex-mathml"], [math]); 578 }; 579 580 module.exports = buildMathML;