www

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

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;