Add MathML rendering to improve accessibility
Summary: This adds support for rendering KaTeX to both HTML and MathML with the intent of improving accessibility. To accomplish this, both MathML and HTML are rendered, but with the MathML visually hidden and the HTML spans aria-hidden. Hopefully, this should produce much better accessibility for KaTeX. Should fix/improve #38 Closes #189 Test Plan: - Ensure all the tests, and the new tests, still pass. - Ensure that for each of the group types in `buildHTML.js`, there is a corresponding one in `buildMathML.js`. - Ensure that the huxley screenshots didn't change (except for BinomTest, which changed because I fixed a bug in `buildHTML` where `genfrac` didn't have a `groupToType` mapping). - Run ChromeVox on the test page, render some math. (for example, `\sqrt{x^2}`) - Ensure that a mathy-sounding expression is read. (I hear "group square root of x squared math"). - Ensure that nothing else is read (like no "x" or "2"). - Ensure that MathML markup is generated correctly and is interpreted by the browser correctly by running `document.getElementById("math").innerHTML = katex.renderToString("\\sqrt{x^2}");` and seeing that the same speech is read. Reviewers: john, alpert Reviewed By: john, alpert Subscribers: alpert, john Differential Revision: https://phabricator.khanacademy.org/D16373
This commit is contained in:
parent
2349a1ed85
commit
aaeab1200c
|
@ -44,7 +44,7 @@ file to get it to work.
|
|||
|
||||
If your function isn't similar to an existing function, you'll need to add a
|
||||
line to `functions.js` as well as adding an output function in
|
||||
[buildTree.js](src/buildTree.js).
|
||||
[buildHTML.js](src/buildHTML.js) and [buildMathML.js](src/buildMathML.js).
|
||||
|
||||
## Testing
|
||||
|
||||
|
|
12
katex.js
12
katex.js
|
@ -17,13 +17,13 @@ var utils = require("./src/utils");
|
|||
* Parse and build an expression, and place that expression in the DOM node
|
||||
* given.
|
||||
*/
|
||||
var render = function(toParse, baseNode, options) {
|
||||
var render = function(expression, baseNode, options) {
|
||||
utils.clearNode(baseNode);
|
||||
|
||||
var settings = new Settings(options);
|
||||
|
||||
var tree = parseTree(toParse, settings);
|
||||
var node = buildTree(tree, settings).toNode();
|
||||
var tree = parseTree(expression, settings);
|
||||
var node = buildTree(tree, expression, settings).toNode();
|
||||
|
||||
baseNode.appendChild(node);
|
||||
};
|
||||
|
@ -45,11 +45,11 @@ if (typeof document !== "undefined") {
|
|||
/**
|
||||
* Parse and build an expression, and return the markup for that.
|
||||
*/
|
||||
var renderToString = function(toParse, options) {
|
||||
var renderToString = function(expression, options) {
|
||||
var settings = new Settings(options);
|
||||
|
||||
var tree = parseTree(toParse, settings);
|
||||
return buildTree(tree, settings).toMarkup();
|
||||
var tree = parseTree(expression, settings);
|
||||
return buildTree(tree, expression, settings).toMarkup();
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
|
|
|
@ -261,11 +261,60 @@ var makeVList = function(children, positionType, positionData, options) {
|
|||
return vlist;
|
||||
};
|
||||
|
||||
// A table of size -> font size for the different sizing functions
|
||||
var sizingMultiplier = {
|
||||
size1: 0.5,
|
||||
size2: 0.7,
|
||||
size3: 0.8,
|
||||
size4: 0.9,
|
||||
size5: 1.0,
|
||||
size6: 1.2,
|
||||
size7: 1.44,
|
||||
size8: 1.73,
|
||||
size9: 2.07,
|
||||
size10: 2.49
|
||||
};
|
||||
|
||||
// A map of spacing functions to their attributes, like size and corresponding
|
||||
// CSS class
|
||||
var spacingFunctions = {
|
||||
"\\qquad": {
|
||||
size: "2em",
|
||||
className: "qquad"
|
||||
},
|
||||
"\\quad": {
|
||||
size: "1em",
|
||||
className: "quad"
|
||||
},
|
||||
"\\enspace": {
|
||||
size: "0.5em",
|
||||
className: "enspace"
|
||||
},
|
||||
"\\;": {
|
||||
size: "0.277778em",
|
||||
className: "thickspace"
|
||||
},
|
||||
"\\:": {
|
||||
size: "0.22222em",
|
||||
className: "mediumspace"
|
||||
},
|
||||
"\\,": {
|
||||
size: "0.16667em",
|
||||
className: "thinspace"
|
||||
},
|
||||
"\\!": {
|
||||
size: "-0.16667em",
|
||||
className: "negativethinspace"
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
makeSymbol: makeSymbol,
|
||||
mathit: mathit,
|
||||
mathrm: mathrm,
|
||||
makeSpan: makeSpan,
|
||||
makeFragment: makeFragment,
|
||||
makeVList: makeVList
|
||||
makeVList: makeVList,
|
||||
sizingMultiplier: sizingMultiplier,
|
||||
spacingFunctions: spacingFunctions
|
||||
};
|
||||
|
|
1151
src/buildHTML.js
Normal file
1151
src/buildHTML.js
Normal file
File diff suppressed because it is too large
Load Diff
442
src/buildMathML.js
Normal file
442
src/buildMathML.js
Normal file
|
@ -0,0 +1,442 @@
|
|||
/**
|
||||
* This file converts a parse tree into a cooresponding MathML tree. The main
|
||||
* entry point is the `buildMathML` function, which takes a parse tree from the
|
||||
* parser.
|
||||
*/
|
||||
|
||||
var buildCommon = require("./buildCommon");
|
||||
var mathMLTree = require("./mathMLTree");
|
||||
var ParseError = require("./ParseError");
|
||||
var symbols = require("./symbols");
|
||||
|
||||
var makeSpan = buildCommon.makeSpan;
|
||||
|
||||
/**
|
||||
* Takes a symbol and converts it into a MathML text node after performing
|
||||
* optional replacement from symbols.js.
|
||||
*/
|
||||
var makeText = function(text, mode) {
|
||||
if (symbols[mode][text] && symbols[mode][text].replace) {
|
||||
text = symbols[mode][text].replace;
|
||||
}
|
||||
|
||||
return new mathMLTree.TextNode(text);
|
||||
};
|
||||
|
||||
/**
|
||||
* Functions for handling the different types of groups found in the parse
|
||||
* tree. Each function should take a parse group and return a MathML node.
|
||||
*/
|
||||
var groupTypes = {
|
||||
mathord: function(group) {
|
||||
var node = new mathMLTree.MathNode(
|
||||
"mi",
|
||||
[makeText(group.value, group.mode)]);
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
textord: function(group) {
|
||||
var text = makeText(group.value, group.mode);
|
||||
|
||||
var node;
|
||||
if (/[0-9]/.test(group.value)) {
|
||||
node = new mathMLTree.MathNode("mn", [text]);
|
||||
} else {
|
||||
node = new mathMLTree.MathNode("mi", [text]);
|
||||
node.setAttribute("mathvariant", "normal");
|
||||
}
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
bin: function(group) {
|
||||
var node = new mathMLTree.MathNode(
|
||||
"mo", [makeText(group.value, group.mode)]);
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
rel: function(group) {
|
||||
var node = new mathMLTree.MathNode(
|
||||
"mo", [makeText(group.value, group.mode)]);
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
open: function(group) {
|
||||
var node = new mathMLTree.MathNode(
|
||||
"mo", [makeText(group.value, group.mode)]);
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
close: function(group) {
|
||||
var node = new mathMLTree.MathNode(
|
||||
"mo", [makeText(group.value, group.mode)]);
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
inner: function(group) {
|
||||
var node = new mathMLTree.MathNode(
|
||||
"mo", [makeText(group.value, group.mode)]);
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
punct: function(group) {
|
||||
var node = new mathMLTree.MathNode(
|
||||
"mo", [makeText(group.value, group.mode)]);
|
||||
|
||||
node.setAttribute("separator", "true");
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
ordgroup: function(group) {
|
||||
var inner = buildExpression(group.value);
|
||||
|
||||
var node = new mathMLTree.MathNode("mrow", inner);
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
text: function(group) {
|
||||
var inner = buildExpression(group.value.body);
|
||||
|
||||
var node = new mathMLTree.MathNode("mtext", inner);
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
color: function(group) {
|
||||
var inner = buildExpression(group.value.value);
|
||||
|
||||
var node = new mathMLTree.MathNode("mstyle", inner);
|
||||
|
||||
node.setAttribute("mathcolor", group.value.color);
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
supsub: function(group) {
|
||||
var children = [buildGroup(group.value.base)];
|
||||
|
||||
if (group.value.sub) {
|
||||
children.push(buildGroup(group.value.sub));
|
||||
}
|
||||
|
||||
if (group.value.sup) {
|
||||
children.push(buildGroup(group.value.sup));
|
||||
}
|
||||
|
||||
var nodeType;
|
||||
if (!group.value.sub) {
|
||||
nodeType = "msup";
|
||||
} else if (!group.value.sup) {
|
||||
nodeType = "msub";
|
||||
} else {
|
||||
nodeType = "msubsup";
|
||||
}
|
||||
|
||||
var node = new mathMLTree.MathNode(nodeType, children);
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
genfrac: function(group) {
|
||||
var node = new mathMLTree.MathNode(
|
||||
"mfrac",
|
||||
[buildGroup(group.value.numer),
|
||||
buildGroup(group.value.denom)]);
|
||||
|
||||
if (!group.value.hasBarLine) {
|
||||
node.setAttribute("linethickness", "0px");
|
||||
}
|
||||
|
||||
if (group.value.leftDelim != null || group.value.rightDelim != null) {
|
||||
var withDelims = [];
|
||||
|
||||
if (group.value.leftDelim != null) {
|
||||
var leftOp = new mathMLTree.MathNode(
|
||||
"mo", [new mathMLTree.TextNode(group.value.leftDelim)]);
|
||||
|
||||
leftOp.setAttribute("fence", "true");
|
||||
|
||||
withDelims.push(leftOp);
|
||||
}
|
||||
|
||||
withDelims.push(node);
|
||||
|
||||
if (group.value.rightDelim != null) {
|
||||
var rightOp = new mathMLTree.MathNode(
|
||||
"mo", [new mathMLTree.TextNode(group.value.rightDelim)]);
|
||||
|
||||
rightOp.setAttribute("fence", "true");
|
||||
|
||||
withDelims.push(rightOp);
|
||||
}
|
||||
|
||||
var outerNode = new mathMLTree.MathNode("mrow", withDelims);
|
||||
|
||||
return outerNode;
|
||||
}
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
sqrt: function(group) {
|
||||
var node = new mathMLTree.MathNode(
|
||||
"msqrt", [buildGroup(group.value.body)]);
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
leftright: function(group) {
|
||||
var inner = buildExpression(group.value.body);
|
||||
|
||||
if (group.value.left !== ".") {
|
||||
var leftNode = new mathMLTree.MathNode(
|
||||
"mo", [makeText(group.value.left, group.mode)]);
|
||||
|
||||
leftNode.setAttribute("fence", "true");
|
||||
|
||||
inner.unshift(leftNode);
|
||||
}
|
||||
|
||||
if (group.value.right !== ".") {
|
||||
var rightNode = new mathMLTree.MathNode(
|
||||
"mo", [makeText(group.value.right, group.mode)]);
|
||||
|
||||
rightNode.setAttribute("fence", "true");
|
||||
|
||||
inner.push(rightNode);
|
||||
}
|
||||
|
||||
var outerNode = new mathMLTree.MathNode("mrow", inner);
|
||||
|
||||
return outerNode;
|
||||
},
|
||||
|
||||
accent: function(group) {
|
||||
var accentNode = new mathMLTree.MathNode(
|
||||
"mo", [makeText(group.value.accent, group.mode)]);
|
||||
|
||||
var node = new mathMLTree.MathNode(
|
||||
"mover",
|
||||
[buildGroup(group.value.base),
|
||||
accentNode]);
|
||||
|
||||
node.setAttribute("accent", "true");
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
spacing: function(group) {
|
||||
var node;
|
||||
|
||||
if (group.value === "\\ " || group.value === "\\space" ||
|
||||
group.value === " " || group.value === "~") {
|
||||
node = new mathMLTree.MathNode(
|
||||
"mtext", [new mathMLTree.TextNode("\u00a0")]);
|
||||
} else {
|
||||
node = new mathMLTree.MathNode("mspace");
|
||||
|
||||
node.setAttribute(
|
||||
"width", buildCommon.spacingFunctions[group.value].size);
|
||||
}
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
op: function(group) {
|
||||
var node;
|
||||
|
||||
// TODO(emily): handle big operators using the `largeop` attribute
|
||||
|
||||
if (group.value.symbol) {
|
||||
// This is a symbol. Just add the symbol.
|
||||
node = new mathMLTree.MathNode(
|
||||
"mo", [makeText(group.value.body, group.mode)]);
|
||||
} else {
|
||||
// This is a text operator. Add all of the characters from the
|
||||
// operator's name.
|
||||
// TODO(emily): Add a space in the middle of some of these
|
||||
// operators, like \limsup.
|
||||
node = new mathMLTree.MathNode(
|
||||
"mo", [new mathMLTree.TextNode(group.value.body.slice(1))]);
|
||||
}
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
katex: function(group) {
|
||||
var node = new mathMLTree.MathNode(
|
||||
"mtext", [new mathMLTree.TextNode("KaTeX")]);
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
delimsizing: function(group) {
|
||||
var children = [];
|
||||
|
||||
if (group.value.value !== ".") {
|
||||
children.push(makeText(group.value.value, group.mode));
|
||||
}
|
||||
|
||||
var node = new mathMLTree.MathNode("mo", children);
|
||||
|
||||
if (group.value.delimType === "open" ||
|
||||
group.value.delimType === "close") {
|
||||
// Only some of the delimsizing functions act as fences, and they
|
||||
// return "open" or "close" delimTypes.
|
||||
node.setAttribute("fence", "true");
|
||||
} else {
|
||||
// Explicitly disable fencing if it's not a fence, to override the
|
||||
// defaults.
|
||||
node.setAttribute("fence", "false");
|
||||
}
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
styling: function(group) {
|
||||
var inner = buildExpression(group.value.value, inner);
|
||||
|
||||
var node = new mathMLTree.MathNode("mstyle", inner);
|
||||
|
||||
var styleAttributes = {
|
||||
"display": ["0", "true"],
|
||||
"text": ["0", "false"],
|
||||
"script": ["1", "false"],
|
||||
"scriptscript": ["2", "false"]
|
||||
};
|
||||
|
||||
var attr = styleAttributes[group.value.style];
|
||||
|
||||
node.setAttribute("scriptlevel", attr[0]);
|
||||
node.setAttribute("displaystyle", attr[1]);
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
sizing: function(group) {
|
||||
var inner = buildExpression(group.value.value);
|
||||
|
||||
var node = new mathMLTree.MathNode("mstyle", inner);
|
||||
|
||||
// TODO(emily): This doesn't produce the correct size for nested size
|
||||
// changes, because we don't keep state of what style we're currently
|
||||
// in, so we can't reset the size to normal before changing it.
|
||||
node.setAttribute(
|
||||
"mathsize", buildCommon.sizingMultiplier[group.value.size] + "em");
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
overline: function(group) {
|
||||
var operator = new mathMLTree.MathNode(
|
||||
"mo", [new mathMLTree.TextNode("\u203e")]);
|
||||
operator.setAttribute("stretchy", "true");
|
||||
|
||||
var node = new mathMLTree.MathNode(
|
||||
"mover",
|
||||
[buildGroup(group.value.body),
|
||||
operator]);
|
||||
node.setAttribute("accent", "true");
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
rule: function(group) {
|
||||
// TODO(emily): Figure out if there's an actual way to draw black boxes
|
||||
// in MathML.
|
||||
var node = new mathMLTree.MathNode("mrow");
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
llap: function(group) {
|
||||
var node = new mathMLTree.MathNode(
|
||||
"mpadded", [buildGroup(group.value.body)]);
|
||||
|
||||
node.setAttribute("lspace", "-1width");
|
||||
node.setAttribute("width", "0px");
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
rlap: function(group) {
|
||||
var node = new mathMLTree.MathNode(
|
||||
"mpadded", [buildGroup(group.value.body)]);
|
||||
|
||||
node.setAttribute("width", "0px");
|
||||
|
||||
return node;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Takes a list of nodes, builds them, and returns a list of the generated
|
||||
* MathML nodes. A little simpler than the HTML version because we don't do any
|
||||
* previous-node handling.
|
||||
*/
|
||||
var buildExpression = function(expression) {
|
||||
var groups = [];
|
||||
for (var i = 0; i < expression.length; i++) {
|
||||
var group = expression[i];
|
||||
groups.push(buildGroup(group));
|
||||
}
|
||||
return groups;
|
||||
};
|
||||
|
||||
/**
|
||||
* Takes a group from the parser and calls the appropriate groupTypes function
|
||||
* on it to produce a MathML node.
|
||||
*/
|
||||
var buildGroup = function(group) {
|
||||
if (!group) {
|
||||
return new mathMLTree.MathNode("mrow");
|
||||
}
|
||||
|
||||
if (groupTypes[group.type]) {
|
||||
// Call the groupTypes function
|
||||
return groupTypes[group.type](group);
|
||||
} else {
|
||||
throw new ParseError(
|
||||
"Got group of unknown type: '" + group.type + "'");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Takes a full parse tree and settings and builds a MathML representation of
|
||||
* it. In particular, we put the elements from building the parse tree into a
|
||||
* <semantics> tag so we can also include that TeX source as an annotation.
|
||||
*
|
||||
* Note that we actually return a domTree element with a `<math>` inside it so
|
||||
* we can do appropriate styling.
|
||||
*/
|
||||
var buildMathML = function(tree, texExpression, settings) {
|
||||
var expression = buildExpression(tree);
|
||||
|
||||
// Wrap up the expression in an mrow so it is presented in the semantics
|
||||
// tag correctly.
|
||||
var wrapper = new mathMLTree.MathNode("mrow", expression);
|
||||
|
||||
// Build a TeX annotation of the source
|
||||
var annotation = new mathMLTree.MathNode(
|
||||
"annotation", [new mathMLTree.TextNode(texExpression)]);
|
||||
|
||||
annotation.setAttribute("encoding", "application/x-tex");
|
||||
|
||||
var semantics = new mathMLTree.MathNode(
|
||||
"semantics", [wrapper, annotation]);
|
||||
|
||||
var math = new mathMLTree.MathNode("math", [semantics]);
|
||||
|
||||
// You can't style <math> nodes, so we wrap the node in a span.
|
||||
return makeSpan(["katex-mathml"], [math]);
|
||||
};
|
||||
|
||||
module.exports = buildMathML;
|
1167
src/buildTree.js
1167
src/buildTree.js
File diff suppressed because it is too large
Load Diff
|
@ -1,9 +1,11 @@
|
|||
/**
|
||||
* These objects store the data about the DOM nodes we create, as well as some
|
||||
* extra data. They can then be transformed into real DOM nodes with the toNode
|
||||
* function or HTML markup using toMarkup. They are useful for both storing
|
||||
* extra properties on the nodes, as well as providing a way to easily work
|
||||
* with the DOM.
|
||||
* extra data. They can then be transformed into real DOM nodes with the
|
||||
* `toNode` function or HTML markup using `toMarkup`. They are useful for both
|
||||
* storing extra properties on the nodes, as well as providing a way to easily
|
||||
* work with the DOM.
|
||||
*
|
||||
* Similar functions for working with MathML nodes exist in mathMLTree.js.
|
||||
*/
|
||||
|
||||
var utils = require("./utils");
|
||||
|
@ -35,8 +37,18 @@ function span(classes, children, height, depth, maxFontSize, style) {
|
|||
this.depth = depth || 0;
|
||||
this.maxFontSize = maxFontSize || 0;
|
||||
this.style = style || {};
|
||||
this.attributes = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an arbitrary attribute on the span. Warning: use this wisely. Not all
|
||||
* browsers support attributes the same, and having too many custom attributes
|
||||
* is probably bad.
|
||||
*/
|
||||
span.prototype.setAttribute = function(attribute, value) {
|
||||
this.attributes[attribute] = value;
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert the span into an HTML node
|
||||
*/
|
||||
|
@ -48,11 +60,18 @@ span.prototype.toNode = function() {
|
|||
|
||||
// Apply inline styles
|
||||
for (var style in this.style) {
|
||||
if (this.style.hasOwnProperty(style)) {
|
||||
if (Object.prototype.hasOwnProperty.call(this.style, style)) {
|
||||
span.style[style] = this.style[style];
|
||||
}
|
||||
}
|
||||
|
||||
// Apply attributes
|
||||
for (var attr in this.attributes) {
|
||||
if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) {
|
||||
span.setAttribute(attr, this.attributes[attr]);
|
||||
}
|
||||
}
|
||||
|
||||
// Append the children, also as HTML nodes
|
||||
for (var i = 0; i < this.children.length; i++) {
|
||||
span.appendChild(this.children[i].toNode());
|
||||
|
@ -87,6 +106,15 @@ span.prototype.toMarkup = function() {
|
|||
markup += " style=\"" + utils.escape(styles) + "\"";
|
||||
}
|
||||
|
||||
// Add the attributes
|
||||
for (var attr in this.attributes) {
|
||||
if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) {
|
||||
markup += " " + attr + "=\"";
|
||||
markup += utils.escape(this.attributes[attr]);
|
||||
markup += "\"";
|
||||
}
|
||||
}
|
||||
|
||||
markup += ">";
|
||||
|
||||
// Add the markup of the children, also as markup
|
||||
|
|
|
@ -60,10 +60,10 @@ var ParseError = require("./ParseError");
|
|||
* error messages
|
||||
* The function should return an object with the following keys:
|
||||
* - type: The type of element that this is. This is then used in
|
||||
* buildTree to determine which function should be called
|
||||
* to build this node into a DOM node
|
||||
* buildHTML/buildMathML to determine which function
|
||||
* should be called to build this node into a DOM node
|
||||
* Any other data can be added to the object, which will be passed
|
||||
* in to the function in buildTree as `group.value`.
|
||||
* in to the function in buildHTML/buildMathML as `group.value`.
|
||||
*/
|
||||
|
||||
var functions = {
|
||||
|
@ -91,8 +91,8 @@ var functions = {
|
|||
argTypes: ["text"],
|
||||
greediness: 2,
|
||||
handler: function(func, body) {
|
||||
// Since the corresponding buildTree function expects a list of
|
||||
// elements, we normalize for different kinds of arguments
|
||||
// Since the corresponding buildHTML/buildMathML function expects a
|
||||
// list of elements, we normalize for different kinds of arguments
|
||||
// TODO(emily): maybe this should be done somewhere else
|
||||
var inner;
|
||||
if (body.type === "ordgroup") {
|
||||
|
@ -407,8 +407,8 @@ var duplicatedFunctions = [
|
|||
this.lexer, positions[1]);
|
||||
}
|
||||
|
||||
// left and right are caught somewhere in Parser.js, which is
|
||||
// why this data doesn't match what is in buildTree
|
||||
// \left and \right are caught somewhere in Parser.js, which is
|
||||
// why this data doesn't match what is in buildHTML.
|
||||
if (func === "\\left" || func === "\\right") {
|
||||
return {
|
||||
type: "leftright",
|
||||
|
|
102
src/mathMLTree.js
Normal file
102
src/mathMLTree.js
Normal file
|
@ -0,0 +1,102 @@
|
|||
/**
|
||||
* These objects store data about MathML nodes. This is the MathML equivalent
|
||||
* of the types in domTree.js. Since MathML handles its own rendering, and
|
||||
* since we're mainly using MathML to improve accessibility, we don't manage
|
||||
* any of the styling state that the plain DOM nodes do.
|
||||
*
|
||||
* The `toNode` and `toMarkup` functions work simlarly to how they do in
|
||||
* domTree.js, creating namespaced DOM nodes and HTML text markup respectively.
|
||||
*/
|
||||
|
||||
var utils = require("./utils");
|
||||
|
||||
/**
|
||||
* This node represents a general purpose MathML node of any type. The
|
||||
* constructor requires the type of node to create (for example, `"mo"` or
|
||||
* `"mspace"`, corresponding to `<mo>` and `<mspace>` tags).
|
||||
*/
|
||||
function MathNode(type, children) {
|
||||
this.type = type;
|
||||
this.attributes = {};
|
||||
this.children = children || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an attribute on a MathML node. MathML depends on attributes to convey a
|
||||
* semantic content, so this is used heavily.
|
||||
*/
|
||||
MathNode.prototype.setAttribute = function(name, value) {
|
||||
this.attributes[name] = value;
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts the math node into a MathML-namespaced DOM element.
|
||||
*/
|
||||
MathNode.prototype.toNode = function() {
|
||||
var node = document.createElementNS(
|
||||
"http://www.w3.org/1998/Math/MathML", this.type);
|
||||
|
||||
for (var attr in this.attributes) {
|
||||
if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) {
|
||||
node.setAttribute(attr, this.attributes[attr]);
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < this.children.length; i++) {
|
||||
node.appendChild(this.children[i].toNode());
|
||||
}
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts the math node into an HTML markup string.
|
||||
*/
|
||||
MathNode.prototype.toMarkup = function() {
|
||||
var markup = "<" + this.type;
|
||||
|
||||
// Add the attributes
|
||||
for (var attr in this.attributes) {
|
||||
if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) {
|
||||
markup += " " + attr + "=\"";
|
||||
markup += utils.escape(this.attributes[attr]);
|
||||
markup += "\"";
|
||||
}
|
||||
}
|
||||
|
||||
markup += ">";
|
||||
|
||||
for (var i = 0; i < this.children.length; i++) {
|
||||
markup += this.children[i].toMarkup();
|
||||
}
|
||||
|
||||
markup += "</" + this.type + ">";
|
||||
|
||||
return markup;
|
||||
};
|
||||
|
||||
/**
|
||||
* This node represents a piece of text.
|
||||
*/
|
||||
function TextNode(text) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the text node into a DOM text node.
|
||||
*/
|
||||
TextNode.prototype.toNode = function() {
|
||||
return document.createTextNode(this.text);
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts the text node into HTML markup (which is just the text itself).
|
||||
*/
|
||||
TextNode.prototype.toMarkup = function() {
|
||||
return utils.escape(this.text);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
MathNode: MathNode,
|
||||
TextNode: TextNode
|
||||
};
|
|
@ -10,18 +10,35 @@
|
|||
}
|
||||
}
|
||||
|
||||
math.katex {
|
||||
font-size: 1.21em;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.katex {
|
||||
font: normal 1.21em KaTeX_Main;
|
||||
line-height: 1.2;
|
||||
white-space: nowrap;
|
||||
|
||||
.katex-inner {
|
||||
.katex-html {
|
||||
// Making .katex inline-block allows line breaks before and after,
|
||||
// which is undesireable ("to $x$,"). Instead, adjust the .katex-inner
|
||||
// which is undesireable ("to $x$,"). Instead, adjust the .katex-html
|
||||
// style and put nowrap on the inline .katex element.
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.katex-mathml {
|
||||
// Accessibility hack to only show to screen readers
|
||||
// Found at: http://a11yproject.com/posts/how-to-hide-content/
|
||||
position: absolute !important;
|
||||
clip: rect(1px, 1px, 1px, 1px);
|
||||
padding: 0 !important;
|
||||
border: 0 !important;
|
||||
height: 1px !important;
|
||||
width: 1px !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.base {
|
||||
display: inline-block;
|
||||
}
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
@ -1,4 +1,5 @@
|
|||
var buildTree = require("../src/buildTree");
|
||||
var buildHTML = require("../src/buildHTML");
|
||||
var buildMathML = require("../src/buildMathML");
|
||||
var katex = require("../katex");
|
||||
var ParseError = require("../src/ParseError");
|
||||
var parseTree = require("../src/parseTree");
|
||||
|
@ -9,10 +10,10 @@ var defaultSettings = new Settings({});
|
|||
var getBuilt = function(expr) {
|
||||
expect(expr).toBuild();
|
||||
|
||||
var built = buildTree(parseTree(expr), defaultSettings);
|
||||
var built = buildHTML(parseTree(expr), defaultSettings);
|
||||
|
||||
// Remove the outer .katex and .katex-inner layers
|
||||
return built.children[0].children[2].children;
|
||||
return built.children[2].children;
|
||||
};
|
||||
|
||||
var getParsed = function(expr) {
|
||||
|
@ -87,7 +88,7 @@ beforeEach(function() {
|
|||
expect(actual).toParse();
|
||||
|
||||
try {
|
||||
buildTree(parseTree(actual), defaultSettings);
|
||||
buildHTML(parseTree(actual), defaultSettings);
|
||||
} catch (e) {
|
||||
result.pass = false;
|
||||
if (e instanceof ParseError) {
|
||||
|
@ -1094,6 +1095,13 @@ describe("A markup generator", function() {
|
|||
expect(markup).toContain("margin-right");
|
||||
expect(markup).not.toContain("marginRight");
|
||||
});
|
||||
|
||||
it("generates both MathML and HTML", function() {
|
||||
var markup = katex.renderToString("a");
|
||||
|
||||
expect(markup).toContain("<span");
|
||||
expect(markup).toContain("<math");
|
||||
});
|
||||
});
|
||||
|
||||
describe("An accent parser", function() {
|
||||
|
@ -1174,3 +1182,37 @@ describe("An optional argument parser", function() {
|
|||
expect("\\sqrt[").toNotParse();
|
||||
});
|
||||
});
|
||||
|
||||
var getMathML = function(expr) {
|
||||
expect(expr).toParse();
|
||||
|
||||
var built = buildMathML(parseTree(expr));
|
||||
|
||||
// Strip off the surrounding <span>
|
||||
return built.children[0];
|
||||
};
|
||||
|
||||
describe("A MathML builder", function() {
|
||||
it("should generate math nodes", function() {
|
||||
var node = getMathML("x^2");
|
||||
|
||||
expect(node.type).toEqual("math");
|
||||
});
|
||||
|
||||
it("should generate appropriate MathML types", function() {
|
||||
var identifier = getMathML("x").children[0].children[0];
|
||||
expect(identifier.children[0].type).toEqual("mi");
|
||||
|
||||
var number = getMathML("1").children[0].children[0];
|
||||
expect(number.children[0].type).toEqual("mn");
|
||||
|
||||
var operator = getMathML("+").children[0].children[0];
|
||||
expect(operator.children[0].type).toEqual("mo");
|
||||
|
||||
var space = getMathML("\\;").children[0].children[0];
|
||||
expect(space.children[0].type).toEqual("mspace");
|
||||
|
||||
var text = getMathML("\\text{a}").children[0].children[0];
|
||||
expect(text.children[0].type).toEqual("mtext");
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue
Block a user