diff --git a/src/Options.js b/src/Options.js
index e6e0996..e5de594 100644
--- a/src/Options.js
+++ b/src/Options.js
@@ -99,7 +99,7 @@ Options.prototype.withPhantom = function() {
*/
Options.prototype.withFont = function(font) {
return this.extend({
- font: font,
+ font: font || this.font,
});
};
diff --git a/src/buildCommon.js b/src/buildCommon.js
index c4ab8ba..bbd174d 100644
--- a/src/buildCommon.js
+++ b/src/buildCommon.js
@@ -36,6 +36,8 @@ var mainitLetters = [
* classes to be attached to the node.
*
* TODO: make argument order closer to makeSpan
+ * TODO: add a separate argument for math class (e.g. `mop`, `mbin`), which
+ * should if present come first in `classes`.
*/
var makeSymbol = function(value, fontFamily, mode, options, classes) {
// Replace the value with its replaced value from symbol.js
@@ -47,8 +49,12 @@ var makeSymbol = function(value, fontFamily, mode, options, classes) {
var symbolNode;
if (metrics) {
+ var italic = metrics.italic;
+ if (mode === "text") {
+ italic = 0;
+ }
symbolNode = new domTree.symbolNode(
- value, metrics.height, metrics.depth, metrics.italic, metrics.skew,
+ value, metrics.height, metrics.depth, italic, metrics.skew,
classes);
} else {
// TODO(emily): Figure out a good way to only print this in development
@@ -183,6 +189,8 @@ var sizeElementFromChildren = function(elem) {
*
* TODO: Ensure that `options` is always provided (currently some call sites
* don't pass it).
+ * TODO: add a separate argument for math class (e.g. `mop`, `mbin`), which
+ * should if present come first in `classes`.
*/
var makeSpan = function(classes, children, options) {
var span = new domTree.span(classes, children, options);
@@ -422,6 +430,10 @@ var fontMap = {
variant: "normal",
fontName: "Main-Regular",
},
+ "textit": {
+ variant: "italic",
+ fontName: "Main-Italic",
+ },
// "mathit" is missing because it requires the use of two fonts: Main-Italic
// and Math-Italic. This is handled by a special case in makeOrd which ends
diff --git a/src/buildHTML.js b/src/buildHTML.js
index 61ec1b7..dccf161 100644
--- a/src/buildHTML.js
+++ b/src/buildHTML.js
@@ -21,27 +21,52 @@ var isSpace = function(node) {
return node instanceof domTree.span && node.classes[0] === "mspace";
};
+// Binary atoms (first class `mbin`) change into ordinary atoms (`mord`)
+// depending on their surroundings. See TeXbook pg. 442-446, Rules 5 and 6,
+// and the text before Rule 19.
+
+var isBin = function(node) {
+ return node && node.classes[0] === "mbin";
+};
+
+var isBinLeftCanceller = function(node, isRealGroup) {
+ // TODO: This code assumes that a node's math class is the first element
+ // of its `classes` array. A later cleanup should ensure this, for
+ // instance by changing the signature of `makeSpan`.
+ if (node) {
+ return utils.contains(["mbin", "mopen", "mrel", "mop", "mpunct"],
+ node.classes[0]);
+ } else {
+ return isRealGroup;
+ }
+};
+
+var isBinRightCanceller = function(node, isRealGroup) {
+ if (node) {
+ return utils.contains(["mrel", "mclose", "mpunct"], node.classes[0]);
+ } else {
+ return isRealGroup;
+ }
+};
+
/**
* Take a list of nodes, build them in order, and return a list of the built
- * nodes. This function handles the `prev` node correctly, and passes the
- * previous element from the list as the prev of the next element, ignoring
- * spaces. documentFragments are flattened into their contents, so the
- * returned list contains no fragments.
+ * nodes. documentFragments are flattened into their contents, so the
+ * returned list contains no fragments. `isRealGroup` is true if `expression`
+ * is a real group (no atoms will be added on either side), as opposed to
+ * a partial group (e.g. one created by \color).
*/
-var buildExpression = function(expression, options, prev) {
+var buildExpression = function(expression, options, isRealGroup) {
// Parse expressions into `groups`.
var groups = [];
for (var i = 0; i < expression.length; i++) {
var group = expression[i];
- var output = buildGroup(group, options, prev);
+ var output = buildGroup(group, options);
if (output instanceof domTree.documentFragment) {
Array.prototype.push.apply(groups, output.children);
} else {
groups.push(output);
}
- if (!isSpace(output)) {
- prev = group;
- }
}
// At this point `groups` consists entirely of `symbolNode`s and `span`s.
@@ -68,69 +93,32 @@ var buildExpression = function(expression, options, prev) {
Array.prototype.push.apply(groups, spaces);
}
+ // Binary operators change to ordinary symbols in some contexts.
+ for (i = 0; i < groups.length; i++) {
+ if (isBin(groups[i])
+ && (isBinLeftCanceller(groups[i - 1], isRealGroup)
+ || isBinRightCanceller(groups[i + 1], isRealGroup))) {
+ groups[i].classes[0] = "mord";
+ }
+ }
+
return groups;
};
-// List of types used by getTypeOfGroup,
-// see https://github.com/Khan/KaTeX/wiki/Examining-TeX#group-types
-var groupToType = {
- mathord: "mord",
- textord: "mord",
- bin: "mbin",
- rel: "mrel",
- text: "mord",
- open: "mopen",
- close: "mclose",
- inner: "minner",
- genfrac: "mord",
- array: "mord",
- spacing: "mord",
- punct: "mpunct",
- ordgroup: "mord",
- op: "mop",
- katex: "mord",
- overline: "mord",
- underline: "mord",
- rule: "mord",
- leftright: "minner",
- sqrt: "mord",
- accent: "mord",
-};
-
-/**
- * Gets the final math type of an expression, given its group type. This type is
- * used to determine spacing between elements, and affects bin elements by
- * causing them to change depending on what types are around them. This type
- * must be attached to the outermost node of an element as a CSS class so that
- * spacing with its surrounding elements works correctly.
- *
- * Some elements can be mapped one-to-one from group type to math type, and
- * those are listed in the `groupToType` table.
- *
- * Others (usually elements that wrap around other elements) often have
- * recursive definitions, and thus call `getTypeOfGroup` on their inner
- * elements.
- */
-var getTypeOfGroup = function(group) {
- if (group == null) {
- // Like when typesetting $^3$
- return groupToType.mathord;
- } else if (group.type === "supsub") {
- return getTypeOfGroup(group.value.base);
- } else if (group.type === "llap" || group.type === "rlap") {
- return getTypeOfGroup(group.value);
- } else if (group.type === "color" || group.type === "sizing"
- || group.type === "styling") {
- // Return type of rightmost element of group.
- var atoms = group.value.value;
- return getTypeOfGroup(atoms[atoms.length - 1]);
- } else if (group.type === "font") {
- return getTypeOfGroup(group.value.body);
- } else if (group.type === "delimsizing") {
- return groupToType[group.value.delimType];
+// Return math atom class (mclass) of a domTree.
+var getTypeOfDomTree = function(node) {
+ if (node instanceof domTree.documentFragment) {
+ if (node.children.length) {
+ return getTypeOfDomTree(
+ node.children[node.children.length - 1]);
+ }
} else {
- return groupToType[group.type];
+ if (utils.contains(["mord", "mop", "mbin", "mrel", "mopen", "mclose",
+ "mpunct", "minner"], node.classes[0])) {
+ return node.classes[0];
+ }
}
+ return null;
};
/**
@@ -201,12 +189,11 @@ var isCharacterBox = function(group) {
baseElem.type === "punct";
};
-var makeNullDelimiter = function(options) {
- return makeSpan([
+var makeNullDelimiter = function(options, classes) {
+ return makeSpan(classes.concat([
"sizing", "reset-" + options.size, "size5",
options.style.reset(), Style.TEXT.cls(),
- "nulldelimiter",
- ]);
+ "nulldelimiter"]));
};
/**
@@ -215,81 +202,70 @@ var makeNullDelimiter = function(options) {
*/
var groupTypes = {};
-groupTypes.mathord = function(group, options, prev) {
+groupTypes.mathord = function(group, options) {
return buildCommon.makeOrd(group, options, "mathord");
};
-groupTypes.textord = function(group, options, prev) {
+groupTypes.textord = function(group, options) {
return buildCommon.makeOrd(group, options, "textord");
};
-groupTypes.bin = function(group, options, prev) {
- var className = "mbin";
- // Pull out the most recent element. Do some special handling to find
- // things at the end of a \color group. Note that we don't use the same
- // logic for ordgroups (which count as ords).
- var prevAtom = prev;
- while (prevAtom && prevAtom.type === "color") {
- var atoms = prevAtom.value.value;
- prevAtom = atoms[atoms.length - 1];
- }
- // See TeXbook pg. 442-446, Rules 5 and 6, and the text before Rule 19.
- // Here, we determine whether the bin should turn into an ord. We
- // currently only apply Rule 5.
- if (!prev || utils.contains(["mbin", "mopen", "mrel", "mop", "mpunct"],
- getTypeOfGroup(prevAtom))) {
- group.type = "textord";
- className = "mord";
- }
-
+groupTypes.bin = function(group, options) {
return buildCommon.mathsym(
- group.value, group.mode, options, [className]);
+ group.value, group.mode, options, ["mbin"]);
};
-groupTypes.rel = function(group, options, prev) {
+groupTypes.rel = function(group, options) {
return buildCommon.mathsym(
group.value, group.mode, options, ["mrel"]);
};
-groupTypes.open = function(group, options, prev) {
+groupTypes.open = function(group, options) {
return buildCommon.mathsym(
group.value, group.mode, options, ["mopen"]);
};
-groupTypes.close = function(group, options, prev) {
+groupTypes.close = function(group, options) {
return buildCommon.mathsym(
group.value, group.mode, options, ["mclose"]);
};
-groupTypes.inner = function(group, options, prev) {
+groupTypes.inner = function(group, options) {
return buildCommon.mathsym(
group.value, group.mode, options, ["minner"]);
};
-groupTypes.punct = function(group, options, prev) {
+groupTypes.punct = function(group, options) {
return buildCommon.mathsym(
group.value, group.mode, options, ["mpunct"]);
};
-groupTypes.ordgroup = function(group, options, prev) {
+groupTypes.ordgroup = function(group, options) {
return makeSpan(
["mord", options.style.cls()],
- buildExpression(group.value, options.reset()),
+ buildExpression(group.value, options.reset(), true),
options
);
};
-groupTypes.text = function(group, options, prev) {
- return makeSpan(["mord", "text", options.style.cls()],
- buildExpression(group.value.body, options.reset()),
- options);
+groupTypes.text = function(group, options) {
+ var newOptions = options.withFont(group.value.style);
+ var inner = buildExpression(group.value.body, newOptions, true);
+ for (var i = 0; i < inner.length - 1; i++) {
+ if (inner[i].tryCombine(inner[i + 1])) {
+ inner.splice(i + 1, 1);
+ i--;
+ }
+ }
+ return makeSpan(["mord", "text", newOptions.style.cls()],
+ inner, newOptions);
};
-groupTypes.color = function(group, options, prev) {
+groupTypes.color = function(group, options) {
var elements = buildExpression(
group.value.value,
options.withColor(group.value.color),
- prev
+ false
);
// \color isn't supposed to affect the type of the elements it contains.
@@ -299,14 +275,14 @@ groupTypes.color = function(group, options, prev) {
return new buildCommon.makeFragment(elements);
};
-groupTypes.supsub = function(group, options, prev) {
+groupTypes.supsub = function(group, options) {
// Superscript and subscripts are handled in the TeXbook on page
// 445-446, rules 18(a-f).
// Here is where we defer to the inner group if it should handle
// superscripts and subscripts itself.
if (shouldHandleSupSub(group.value.base, options)) {
- return groupTypes[group.value.base.type](group, options, prev);
+ return groupTypes[group.value.base.type](group, options);
}
var base = buildGroup(group.value.base, options.reset());
@@ -422,12 +398,13 @@ groupTypes.supsub = function(group, options, prev) {
}
// We ensure to wrap the supsub vlist in a span.msupsub to reset text-align
- return makeSpan([getTypeOfGroup(group.value.base)],
+ var mclass = getTypeOfDomTree(base) || "mord";
+ return makeSpan([mclass],
[base, makeSpan(["msupsub"], [supsub])],
options);
};
-groupTypes.genfrac = function(group, options, prev) {
+groupTypes.genfrac = function(group, options) {
// Fractions are handled in the TeXbook on pages 444-445, rules 15(a-e).
// Figure out what style this fraction should be in based on the
// function used
@@ -546,18 +523,18 @@ groupTypes.genfrac = function(group, options, prev) {
var leftDelim;
var rightDelim;
if (group.value.leftDelim == null) {
- leftDelim = makeNullDelimiter(options);
+ leftDelim = makeNullDelimiter(options, ["mopen"]);
} else {
leftDelim = delimiter.customSizedDelim(
group.value.leftDelim, delimSize, true,
- options.withStyle(style), group.mode);
+ options.withStyle(style), group.mode, ["mopen"]);
}
if (group.value.rightDelim == null) {
- rightDelim = makeNullDelimiter(options);
+ rightDelim = makeNullDelimiter(options, ["mclose"]);
} else {
rightDelim = delimiter.customSizedDelim(
group.value.rightDelim, delimSize, true,
- options.withStyle(style), group.mode);
+ options.withStyle(style), group.mode, ["mclose"]);
}
return makeSpan(
@@ -566,7 +543,7 @@ groupTypes.genfrac = function(group, options, prev) {
options);
};
-groupTypes.array = function(group, options, prev) {
+groupTypes.array = function(group, options) {
var r;
var c;
var nr = group.value.body.length;
@@ -730,47 +707,51 @@ groupTypes.array = function(group, options, prev) {
return makeSpan(["mord"], [body], options);
};
-groupTypes.spacing = function(group, options, prev) {
+groupTypes.spacing = function(group, options) {
if (group.value === "\\ " || group.value === "\\space" ||
group.value === " " || group.value === "~") {
// Spaces are generated by adding an actual space. Each of these
// things has an entry in the symbols table, so these will be turned
// into appropriate outputs.
- return makeSpan(
- ["mspace"],
- [buildCommon.mathsym(group.value, group.mode)]
- );
+ if (group.mode === "text") {
+ return buildCommon.makeOrd(group, options, "textord");
+ } else {
+ return makeSpan(["mspace"],
+ [buildCommon.mathsym(group.value, group.mode, options)],
+ options);
+ }
} else {
// Other kinds of spaces are of arbitrary width. We use CSS to
// generate these.
return makeSpan(
["mspace",
- buildCommon.spacingFunctions[group.value].className]);
+ buildCommon.spacingFunctions[group.value].className],
+ [], options);
}
};
-groupTypes.llap = function(group, options, prev) {
+groupTypes.llap = function(group, options) {
var inner = makeSpan(
["inner"], [buildGroup(group.value.body, options.reset())]);
var fix = makeSpan(["fix"], []);
return makeSpan(
- ["llap", options.style.cls()], [inner, fix], options);
+ ["mord", "llap", options.style.cls()], [inner, fix], options);
};
-groupTypes.rlap = function(group, options, prev) {
+groupTypes.rlap = function(group, options) {
var inner = makeSpan(
["inner"], [buildGroup(group.value.body, options.reset())]);
var fix = makeSpan(["fix"], []);
return makeSpan(
- ["rlap", options.style.cls()], [inner, fix], options);
+ ["mord", "rlap", options.style.cls()], [inner, fix], options);
};
-groupTypes.op = function(group, options, prev) {
+groupTypes.op = function(group, options) {
// Operators are handled in the TeXbook pg. 443-444, rule 13(a).
var supGroup;
var subGroup;
var hasLimits = false;
- if (group.type === "supsub" ) {
+ if (group.type === "supsub") {
// If we have limits, supsub will pass us its group to handle. Pull
// out the superscript and subscript and set the group to the op in
// its base.
@@ -816,6 +797,11 @@ groupTypes.op = function(group, options, prev) {
// The slant of the symbol is just its italic correction.
slant = base.italic;
+ } else if (group.value.value) {
+ // If this is a list, compose that list.
+ var inner = buildExpression(group.value.value, options, true);
+
+ base = makeSpan(["mop"], inner, options);
} else {
// Otherwise, this is a text operator. Build the text from the
// operator's name.
@@ -930,7 +916,7 @@ groupTypes.op = function(group, options, prev) {
}
};
-groupTypes.katex = function(group, options, prev) {
+groupTypes.katex = function(group, options) {
// The KaTeX logo. The offsets for the K and a were chosen to look
// good, but the offsets for the T, E, and X were taken from the
// definition of \TeX in TeX (see TeXbook pg. 356)
@@ -957,7 +943,7 @@ groupTypes.katex = function(group, options, prev) {
["mord", "katex-logo"], [k, a, t, e, x], options);
};
-groupTypes.overline = function(group, options, prev) {
+groupTypes.overline = function(group, options) {
// Overlines are handled in the TeXbook pg 443, Rule 9.
var style = options.style;
@@ -985,7 +971,7 @@ groupTypes.overline = function(group, options, prev) {
return makeSpan(["mord", "overline"], [vlist], options);
};
-groupTypes.underline = function(group, options, prev) {
+groupTypes.underline = function(group, options) {
// Underlines are handled in the TeXbook pg 443, Rule 10.
var style = options.style;
@@ -1011,7 +997,7 @@ groupTypes.underline = function(group, options, prev) {
return makeSpan(["mord", "underline"], [vlist], options);
};
-groupTypes.sqrt = function(group, options, prev) {
+groupTypes.sqrt = function(group, options) {
// Square roots are handled in the TeXbook pg. 443, Rule 11.
var style = options.style;
@@ -1110,12 +1096,12 @@ groupTypes.sqrt = function(group, options, prev) {
}
};
-groupTypes.sizing = function(group, options, prev) {
+groupTypes.sizing = function(group, options) {
// Handle sizing operators like \Huge. Real TeX doesn't actually allow
// these functions inside of math expressions, so we do some special
// handling.
var inner = buildExpression(group.value.value,
- options.withSize(group.value.size), prev);
+ options.withSize(group.value.size), false);
// Compute the correct maxFontSize.
var style = options.style;
@@ -1141,7 +1127,7 @@ groupTypes.sizing = function(group, options, prev) {
return buildCommon.makeFragment(inner);
};
-groupTypes.styling = function(group, options, prev) {
+groupTypes.styling = function(group, options) {
// Style changes are handled in the TeXbook on pg. 442, Rule 3.
// Figure out what style we're changing to.
@@ -1157,7 +1143,7 @@ groupTypes.styling = function(group, options, prev) {
// Build the inner expression in the new style.
var inner = buildExpression(
- group.value.value, newOptions, prev);
+ group.value.value, newOptions, false);
// Add style-resetting classes to the inner list. Handle nested changes.
for (var i = 0; i < inner.length; i++) {
@@ -1174,31 +1160,29 @@ groupTypes.styling = function(group, options, prev) {
return new buildCommon.makeFragment(inner);
};
-groupTypes.font = function(group, options, prev) {
+groupTypes.font = function(group, options) {
var font = group.value.font;
- return buildGroup(group.value.body, options.withFont(font), prev);
+ return buildGroup(group.value.body, options.withFont(font));
};
-groupTypes.delimsizing = function(group, options, prev) {
+groupTypes.delimsizing = function(group, options) {
var delim = group.value.value;
if (delim === ".") {
// Empty delimiters still count as elements, even though they don't
// show anything.
- return makeSpan([groupToType[group.value.delimType]]);
+ return makeSpan([group.value.mclass]);
}
// Use delimiter.sizedDelim to generate the delimiter.
- return makeSpan(
- [groupToType[group.value.delimType]],
- [delimiter.sizedDelim(
- delim, group.value.size, options, group.mode)],
- options);
+ return delimiter.sizedDelim(
+ delim, group.value.size, options, group.mode,
+ [group.value.mclass]);
};
-groupTypes.leftright = function(group, options, prev) {
+groupTypes.leftright = function(group, options) {
// Build the inner expression
- var inner = buildExpression(group.value.body, options.reset());
+ var inner = buildExpression(group.value.body, options.reset(), true);
var innerHeight = 0;
var innerDepth = 0;
@@ -1220,13 +1204,13 @@ groupTypes.leftright = function(group, options, prev) {
var leftDelim;
if (group.value.left === ".") {
// Empty delimiters in \left and \right make null delimiter spaces.
- leftDelim = makeNullDelimiter(options);
+ leftDelim = makeNullDelimiter(options, ["mopen"]);
} else {
// Otherwise, use leftRightDelim to generate the correct sized
// delimiter.
leftDelim = delimiter.leftRightDelim(
group.value.left, innerHeight, innerDepth, options,
- group.mode);
+ group.mode, ["mopen"]);
}
// Add it to the beginning of the expression
inner.unshift(leftDelim);
@@ -1234,11 +1218,11 @@ groupTypes.leftright = function(group, options, prev) {
var rightDelim;
// Same for the right delimiter
if (group.value.right === ".") {
- rightDelim = makeNullDelimiter(options);
+ rightDelim = makeNullDelimiter(options, ["mclose"]);
} else {
rightDelim = delimiter.leftRightDelim(
group.value.right, innerHeight, innerDepth, options,
- group.mode);
+ group.mode, ["mclose"]);
}
// Add it to the end of the expression.
inner.push(rightDelim);
@@ -1247,7 +1231,7 @@ groupTypes.leftright = function(group, options, prev) {
["minner", style.cls()], inner, options);
};
-groupTypes.rule = function(group, options, prev) {
+groupTypes.rule = function(group, options) {
// Make an empty span for the rule
var rule = makeSpan(["mord", "rule"], [], options);
var style = options.style;
@@ -1290,7 +1274,7 @@ groupTypes.rule = function(group, options, prev) {
return rule;
};
-groupTypes.kern = function(group, options, prev) {
+groupTypes.kern = function(group, options) {
// Make an empty span for the rule
var rule = makeSpan(["mord", "rule"], [], options);
var style = options.style;
@@ -1310,7 +1294,7 @@ groupTypes.kern = function(group, options, prev) {
return rule;
};
-groupTypes.accent = function(group, options, prev) {
+groupTypes.accent = function(group, options) {
// Accents are handled in the TeXbook pg. 443, rule 12.
var base = group.value.base;
var style = options.style;
@@ -1337,7 +1321,7 @@ groupTypes.accent = function(group, options, prev) {
// Rerender the supsub group with its new base, and store that
// result.
supsubGroup = buildGroup(
- supsub, options.reset(), prev);
+ supsub, options.reset());
}
// Build the base group
@@ -1419,11 +1403,11 @@ groupTypes.accent = function(group, options, prev) {
}
};
-groupTypes.phantom = function(group, options, prev) {
+groupTypes.phantom = function(group, options) {
var elements = buildExpression(
group.value.value,
options.withPhantom(),
- prev
+ false
);
// \phantom isn't supposed to affect the elements it contains.
@@ -1431,19 +1415,25 @@ groupTypes.phantom = function(group, options, prev) {
return new buildCommon.makeFragment(elements);
};
+groupTypes.mclass = function(group, options) {
+ var elements = buildExpression(group.value.value, options, true);
+
+ return makeSpan([group.value.mclass], elements, options);
+};
+
/**
* buildGroup is the function that takes a group and calls the correct groupType
* function for it. It also handles the interaction of size and style changes
* between parents and children.
*/
-var buildGroup = function(group, options, prev) {
+var buildGroup = function(group, options) {
if (!group) {
return makeSpan();
}
if (groupTypes[group.type]) {
// Call the groupTypes function
- var groupNode = groupTypes[group.type](group, options, prev);
+ var groupNode = groupTypes[group.type](group, options);
var multiplier;
// If the style changed between the parent and the current group,
@@ -1483,7 +1473,7 @@ var buildHTML = function(tree, options) {
tree = JSON.parse(JSON.stringify(tree));
// Build the expression contained in the tree
- var expression = buildExpression(tree, options);
+ var expression = buildExpression(tree, options, true);
var body = makeSpan(["base", options.style.cls()], expression, options);
// Add struts, which ensure that the top of the HTML element falls at the
diff --git a/src/buildMathML.js b/src/buildMathML.js
index 575ba75..11a088f 100644
--- a/src/buildMathML.js
+++ b/src/buildMathML.js
@@ -316,7 +316,7 @@ groupTypes.spacing = function(group) {
return node;
};
-groupTypes.op = function(group) {
+groupTypes.op = function(group, options) {
var node;
// TODO(emily): handle big operators using the `largeop` attribute
@@ -325,6 +325,10 @@ groupTypes.op = function(group) {
// This is a symbol. Just add the symbol.
node = new mathMLTree.MathNode(
"mo", [makeText(group.value.body, group.mode)]);
+ } else if (group.value.value) {
+ // This is an operator with children. Add them.
+ node = new mathMLTree.MathNode(
+ "mo", buildExpression(group.value.value, options));
} else {
// This is a text operator. Add all of the characters from the
// operator's name.
@@ -358,10 +362,10 @@ groupTypes.delimsizing = function(group) {
var node = new mathMLTree.MathNode("mo", children);
- if (group.value.delimType === "open" ||
- group.value.delimType === "close") {
+ if (group.value.mclass === "mopen" ||
+ group.value.mclass === "mclose") {
// Only some of the delimsizing functions act as fences, and they
- // return "open" or "close" delimTypes.
+ // return "mopen" or "mclose" mclass.
node.setAttribute("fence", "true");
} else {
// Explicitly disable fencing if it's not a fence, to override the
@@ -470,11 +474,16 @@ groupTypes.rlap = function(group, options) {
return node;
};
-groupTypes.phantom = function(group, options, prev) {
+groupTypes.phantom = function(group, options) {
var inner = buildExpression(group.value.value, options);
return new mathMLTree.MathNode("mphantom", inner);
};
+groupTypes.mclass = function(group, options) {
+ var inner = buildExpression(group.value.value, options);
+ return new mathMLTree.MathNode("mstyle", inner);
+};
+
/**
* 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
diff --git a/src/delimiter.js b/src/delimiter.js
index 3b62621..0dbc8c5 100644
--- a/src/delimiter.js
+++ b/src/delimiter.js
@@ -56,9 +56,11 @@ var mathrmSize = function(value, size, mode, options) {
* Puts a delimiter span in a given style, and adds appropriate height, depth,
* and maxFontSizes.
*/
-var styleWrap = function(delim, toStyle, options) {
+var styleWrap = function(delim, toStyle, options, classes) {
+ classes = classes || [];
var span = makeSpan(
- ["style-wrap", options.style.reset(), toStyle.cls()], [delim], options);
+ classes.concat(["style-wrap", options.style.reset(), toStyle.cls()]),
+ [delim], options);
var multiplier = toStyle.sizeMultiplier / options.style.sizeMultiplier;
@@ -74,10 +76,10 @@ var styleWrap = function(delim, toStyle, options) {
* font, but is restyled to either be in textstyle, scriptstyle, or
* scriptscriptstyle.
*/
-var makeSmallDelim = function(delim, style, center, options, mode) {
+var makeSmallDelim = function(delim, style, center, options, mode, classes) {
var text = buildCommon.makeSymbol(delim, "Main-Regular", mode, options);
- var span = styleWrap(text, style, options);
+ var span = styleWrap(text, style, options, classes);
if (center) {
var shift =
@@ -96,13 +98,12 @@ var makeSmallDelim = function(delim, style, center, options, mode) {
* Makes a large delimiter. This is a delimiter that comes in the Size1, Size2,
* Size3, or Size4 fonts. It is always rendered in textstyle.
*/
-var makeLargeDelim = function(delim, size, center, options, mode) {
+var makeLargeDelim = function(delim, size, center, options, mode, classes) {
var inner = mathrmSize(delim, size, mode, options);
var span = styleWrap(
- makeSpan(["delimsizing", "size" + size],
- [inner], options),
- Style.TEXT, options);
+ makeSpan(["delimsizing", "size" + size], [inner], options),
+ Style.TEXT, options, classes);
if (center) {
var shift = (1 - options.style.sizeMultiplier) *
@@ -142,7 +143,8 @@ var makeInner = function(symbol, font, mode) {
* Make a stacked delimiter out of a given delimiter, with the total height at
* least `heightTotal`. This routine is mentioned on page 442 of the TeXbook.
*/
-var makeStackedDelim = function(delim, heightTotal, center, options, mode) {
+var makeStackedDelim = function(delim, heightTotal, center, options, mode,
+ classes) {
// There are four parts, the top, an optional middle, a repeated part, and a
// bottom.
var top;
@@ -320,7 +322,7 @@ var makeStackedDelim = function(delim, heightTotal, center, options, mode) {
return styleWrap(
makeSpan(["delimsizing", "mult"], [inner], options),
- Style.TEXT, options);
+ Style.TEXT, options, classes);
};
// There are three kinds of delimiters, delimiters that stack when they become
@@ -354,7 +356,7 @@ var sizeToMaxHeight = [0, 1.2, 1.8, 2.4, 3.0];
/**
* Used to create a delimiter of a specific size, where `size` is 1, 2, 3, or 4.
*/
-var makeSizedDelim = function(delim, size, options, mode) {
+var makeSizedDelim = function(delim, size, options, mode, classes) {
// < and > turn into \langle and \rangle in delimiters
if (delim === "<" || delim === "\\lt") {
delim = "\\langle";
@@ -365,10 +367,10 @@ var makeSizedDelim = function(delim, size, options, mode) {
// Sized delimiters are never centered.
if (utils.contains(stackLargeDelimiters, delim) ||
utils.contains(stackNeverDelimiters, delim)) {
- return makeLargeDelim(delim, size, false, options, mode);
+ return makeLargeDelim(delim, size, false, options, mode, classes);
} else if (utils.contains(stackAlwaysDelimiters, delim)) {
return makeStackedDelim(
- delim, sizeToMaxHeight[size], false, options, mode);
+ delim, sizeToMaxHeight[size], false, options, mode, classes);
} else {
throw new ParseError("Illegal delimiter: '" + delim + "'");
}
@@ -471,7 +473,8 @@ var traverseSequence = function(delim, height, sequence, options) {
* Make a delimiter of a given height+depth, with optional centering. Here, we
* traverse the sequences, and create a delimiter that the sequence tells us to.
*/
-var makeCustomSizedDelim = function(delim, height, center, options, mode) {
+var makeCustomSizedDelim = function(delim, height, center, options, mode,
+ classes) {
if (delim === "<" || delim === "\\lt") {
delim = "\\langle";
} else if (delim === ">" || delim === "\\gt") {
@@ -494,11 +497,13 @@ var makeCustomSizedDelim = function(delim, height, center, options, mode) {
// Depending on the sequence element we decided on, call the appropriate
// function.
if (delimType.type === "small") {
- return makeSmallDelim(delim, delimType.style, center, options, mode);
+ return makeSmallDelim(delim, delimType.style, center, options, mode,
+ classes);
} else if (delimType.type === "large") {
- return makeLargeDelim(delim, delimType.size, center, options, mode);
+ return makeLargeDelim(delim, delimType.size, center, options, mode,
+ classes);
} else if (delimType.type === "stack") {
- return makeStackedDelim(delim, height, center, options, mode);
+ return makeStackedDelim(delim, height, center, options, mode, classes);
}
};
@@ -506,7 +511,8 @@ var makeCustomSizedDelim = function(delim, height, center, options, mode) {
* Make a delimiter for use with `\left` and `\right`, given a height and depth
* of an expression that the delimiters surround.
*/
-var makeLeftRightDelim = function(delim, height, depth, options, mode) {
+var makeLeftRightDelim = function(delim, height, depth, options, mode,
+ classes) {
// We always center \left/\right delimiters, so the axis is always shifted
var axisHeight =
options.style.metrics.axisHeight * options.style.sizeMultiplier;
@@ -533,7 +539,8 @@ var makeLeftRightDelim = function(delim, height, depth, options, mode) {
// Finally, we defer to `makeCustomSizedDelim` with our calculated total
// height
- return makeCustomSizedDelim(delim, totalHeight, true, options, mode);
+ return makeCustomSizedDelim(delim, totalHeight, true, options, mode,
+ classes);
};
module.exports = {
diff --git a/src/domTree.js b/src/domTree.js
index 0517b81..f09ff4e 100644
--- a/src/domTree.js
+++ b/src/domTree.js
@@ -57,6 +57,10 @@ span.prototype.setAttribute = function(attribute, value) {
this.attributes[attribute] = value;
};
+span.prototype.tryCombine = function(sibling) {
+ return false;
+};
+
/**
* Convert the span into an HTML node
*/
@@ -220,6 +224,34 @@ function symbolNode(value, height, depth, italic, skew, classes, style) {
}
}
+symbolNode.prototype.tryCombine = function(sibling) {
+ if (!sibling
+ || !(sibling instanceof symbolNode)
+ || this.italic > 0
+ || createClass(this.classes) !== createClass(sibling.classes)
+ || this.skew !== sibling.skew
+ || this.maxFontSize !== sibling.maxFontSize) {
+ return false;
+ }
+ for (var style in this.style) {
+ if (this.style.hasOwnProperty(style)
+ && this.style[style] !== sibling.style[style]) {
+ return false;
+ }
+ }
+ for (style in sibling.style) {
+ if (sibling.style.hasOwnProperty(style)
+ && this.style[style] !== sibling.style[style]) {
+ return false;
+ }
+ }
+ this.value += sibling.value;
+ this.height = Math.max(this.height, sibling.height);
+ this.depth = Math.max(this.depth, sibling.depth);
+ this.italic = sibling.italic;
+ return true;
+};
+
/**
* Creates a text node or span from a symbol node. Note that a span is only
* created if it is needed.
diff --git a/src/functions.js b/src/functions.js
index 7888242..6966ea7 100644
--- a/src/functions.js
+++ b/src/functions.js
@@ -1,5 +1,7 @@
var utils = require("./utils");
var ParseError = require("./ParseError");
+var parseData = require("./parseData");
+var ParseNode = parseData.ParseNode;
/* This file contains a list of functions that we parse, identified by
* the calls to defineFunction.
@@ -100,6 +102,16 @@ function defineFunction(names, props, handler) {
}
}
+// Since the corresponding buildHTML/buildMathML function expects a
+// list of elements, we normalize for different kinds of arguments
+var ordargument = function(arg) {
+ if (arg.type === "ordgroup") {
+ return arg.value;
+ } else {
+ return [arg];
+ }
+};
+
// A normal square root
defineFunction("\\sqrt", {
numArgs: 1,
@@ -114,26 +126,27 @@ defineFunction("\\sqrt", {
};
});
-// Some non-mathy text
-defineFunction("\\text", {
+// Non-mathy text, possibly in a font
+var textFunctionStyles = {
+ "\\text": undefined, "\\textrm": "mathrm", "\\textsf": "mathsf",
+ "\\texttt": "mathtt", "\\textnormal": "mathrm", "\\textbf": "mathbf",
+ "\\textit": "textit",
+};
+
+defineFunction([
+ "\\text", "\\textrm", "\\textsf", "\\texttt", "\\textnormal",
+ "\\textbf", "\\textit",
+], {
numArgs: 1,
argTypes: ["text"],
greediness: 2,
+ allowedInText: true,
}, function(context, args) {
var body = args[0];
- // 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") {
- inner = body.value;
- } else {
- inner = [body];
- }
-
return {
type: "text",
- body: inner,
+ body: ordargument(body),
+ style: textFunctionStyles[context.funcName],
};
});
@@ -146,18 +159,10 @@ defineFunction("\\color", {
}, function(context, args) {
var color = args[0];
var body = args[1];
- // Normalize the different kinds of bodies (see \text above)
- var inner;
- if (body.type === "ordgroup") {
- inner = body.value;
- } else {
- inner = [body];
- }
-
return {
type: "color",
color: color.value,
- value: inner,
+ value: ordargument(body),
};
});
@@ -223,37 +228,73 @@ defineFunction("\\phantom", {
numArgs: 1,
}, function(context, args) {
var body = args[0];
- var inner;
- if (body.type === "ordgroup") {
- inner = body.value;
- } else {
- inner = [body];
- }
-
return {
type: "phantom",
- value: inner,
+ value: ordargument(body),
+ };
+});
+
+// Math class commands except \mathop
+defineFunction([
+ "\\mathord", "\\mathbin", "\\mathrel", "\\mathopen",
+ "\\mathclose", "\\mathpunct", "\\mathinner",
+], {
+ numArgs: 1,
+}, function(context, args) {
+ var body = args[0];
+ return {
+ type: "mclass",
+ mclass: "m" + context.funcName.substr(5),
+ value: ordargument(body),
+ };
+});
+
+// Build a relation by placing one symbol on top of another
+defineFunction("\\stackrel", {
+ numArgs: 2,
+}, function(context, args) {
+ var top = args[0];
+ var bottom = args[1];
+
+ var bottomop = new ParseNode("op", {
+ type: "op",
+ limits: true,
+ alwaysHandleSupSub: true,
+ symbol: false,
+ value: ordargument(bottom),
+ }, bottom.mode);
+
+ var supsub = new ParseNode("supsub", {
+ base: bottomop,
+ sup: top,
+ sub: null,
+ }, top.mode);
+
+ return {
+ type: "mclass",
+ mclass: "mrel",
+ value: [supsub],
};
});
// Extra data needed for the delimiter handler down below
var delimiterSizes = {
- "\\bigl" : {type: "open", size: 1},
- "\\Bigl" : {type: "open", size: 2},
- "\\biggl": {type: "open", size: 3},
- "\\Biggl": {type: "open", size: 4},
- "\\bigr" : {type: "close", size: 1},
- "\\Bigr" : {type: "close", size: 2},
- "\\biggr": {type: "close", size: 3},
- "\\Biggr": {type: "close", size: 4},
- "\\bigm" : {type: "rel", size: 1},
- "\\Bigm" : {type: "rel", size: 2},
- "\\biggm": {type: "rel", size: 3},
- "\\Biggm": {type: "rel", size: 4},
- "\\big" : {type: "textord", size: 1},
- "\\Big" : {type: "textord", size: 2},
- "\\bigg" : {type: "textord", size: 3},
- "\\Bigg" : {type: "textord", size: 4},
+ "\\bigl" : {mclass: "mopen", size: 1},
+ "\\Bigl" : {mclass: "mopen", size: 2},
+ "\\biggl": {mclass: "mopen", size: 3},
+ "\\Biggl": {mclass: "mopen", size: 4},
+ "\\bigr" : {mclass: "mclose", size: 1},
+ "\\Bigr" : {mclass: "mclose", size: 2},
+ "\\biggr": {mclass: "mclose", size: 3},
+ "\\Biggr": {mclass: "mclose", size: 4},
+ "\\bigm" : {mclass: "mrel", size: 1},
+ "\\Bigm" : {mclass: "mrel", size: 2},
+ "\\biggm": {mclass: "mrel", size: 3},
+ "\\Biggm": {mclass: "mrel", size: 4},
+ "\\big" : {mclass: "mord", size: 1},
+ "\\Big" : {mclass: "mord", size: 2},
+ "\\bigg" : {mclass: "mord", size: 3},
+ "\\Bigg" : {mclass: "mord", size: 4},
};
var delimiters = [
@@ -298,17 +339,10 @@ defineFunction([
greediness: 3,
}, function(context, args) {
var body = args[0];
- var atoms;
- if (body.type === "ordgroup") {
- atoms = body.value;
- } else {
- atoms = [body];
- }
-
return {
type: "color",
color: "katex-" + context.funcName.slice(1),
- value: atoms,
+ value: ordargument(body),
};
});
@@ -378,10 +412,24 @@ defineFunction([
};
});
+// \mathop class command
+defineFunction("\\mathop", {
+ numArgs: 1,
+}, function(context, args) {
+ var body = args[0];
+ return {
+ type: "op",
+ limits: false,
+ symbol: false,
+ value: ordargument(body),
+ };
+});
+
// Fractions
defineFunction([
"\\dfrac", "\\frac", "\\tfrac",
"\\dbinom", "\\binom", "\\tbinom",
+ "\\\\atopfrac", // can’t be entered directly
], {
numArgs: 2,
greediness: 2,
@@ -399,6 +447,9 @@ defineFunction([
case "\\tfrac":
hasBarLine = true;
break;
+ case "\\\\atopfrac":
+ hasBarLine = false;
+ break;
case "\\dbinom":
case "\\binom":
case "\\tbinom":
@@ -472,7 +523,7 @@ defineFunction([
return {
type: "delimsizing",
size: delimiterSizes[context.funcName].size,
- delimType: delimiterSizes[context.funcName].type,
+ mclass: delimiterSizes[context.funcName].mclass,
value: delim.value,
};
}
@@ -535,7 +586,7 @@ defineFunction([
});
// Infix generalized fractions
-defineFunction(["\\over", "\\choose"], {
+defineFunction(["\\over", "\\choose", "\\atop"], {
numArgs: 0,
infix: true,
}, function(context) {
@@ -547,6 +598,9 @@ defineFunction(["\\over", "\\choose"], {
case "\\choose":
replaceWith = "\\binom";
break;
+ case "\\atop":
+ replaceWith = "\\\\atopfrac";
+ break;
default:
throw new Error("Unrecognized infix genfrac command");
}
diff --git a/src/symbols.js b/src/symbols.js
index 55301a4..92fbef0 100644
--- a/src/symbols.js
+++ b/src/symbols.js
@@ -87,7 +87,9 @@ defineSymbol(math, main, punct, "\u22c5", "\\cdotp");
// Misc Symbols
defineSymbol(math, main, textord, "\u0023", "\\#");
+defineSymbol(text, main, textord, "\u0023", "\\#");
defineSymbol(math, main, textord, "\u0026", "\\&");
+defineSymbol(text, main, textord, "\u0026", "\\&");
defineSymbol(math, main, textord, "\u2135", "\\aleph");
defineSymbol(math, main, textord, "\u2200", "\\forall");
defineSymbol(math, main, textord, "\u210f", "\\hbar");
@@ -394,8 +396,11 @@ defineSymbol(math, ams, rel, "\u21be", "\\restriction");
defineSymbol(math, main, textord, "\u2018", "`");
defineSymbol(math, main, textord, "$", "\\$");
+defineSymbol(text, main, textord, "$", "\\$");
defineSymbol(math, main, textord, "%", "\\%");
+defineSymbol(text, main, textord, "%", "\\%");
defineSymbol(math, main, textord, "_", "\\_");
+defineSymbol(text, main, textord, "_", "\\_");
defineSymbol(math, main, textord, "\u2220", "\\angle");
defineSymbol(math, main, textord, "\u221e", "\\infty");
defineSymbol(math, main, textord, "\u2032", "\\prime");
@@ -534,7 +539,9 @@ defineSymbol(math, main, bin, "\u22c6", "\\star");
defineSymbol(math, main, bin, "\u25c3", "\\triangleleft");
defineSymbol(math, main, bin, "\u25b9", "\\triangleright");
defineSymbol(math, main, open, "{", "\\{");
+defineSymbol(text, main, textord, "{", "\\{");
defineSymbol(math, main, close, "}", "\\}");
+defineSymbol(text, main, textord, "}", "\\}");
defineSymbol(math, main, open, "{", "\\lbrace");
defineSymbol(math, main, close, "}", "\\rbrace");
defineSymbol(math, main, open, "[", "\\lbrack");
@@ -652,3 +659,11 @@ for (i = 0x0410; i <= 0x044F; i++) {
ch = String.fromCharCode(i);
defineSymbol(text, main, textord, ch, ch);
}
+
+// Unicode versions of existing characters
+defineSymbol(text, main, textord, "\u2013", "–");
+defineSymbol(text, main, textord, "\u2014", "—");
+defineSymbol(text, main, textord, "\u2018", "‘");
+defineSymbol(text, main, textord, "\u2019", "’");
+defineSymbol(text, main, textord, "\u201c", "“");
+defineSymbol(text, main, textord, "\u201d", "”");
diff --git a/static/katex.less b/static/katex.less
index 7b8c293..764e446 100644
--- a/static/katex.less
+++ b/static/katex.less
@@ -49,6 +49,14 @@
display: inline-block;
}
+ .mathrm {
+ font-style: normal;
+ }
+
+ .textit {
+ font-style: italic;
+ }
+
.mathit {
font-family: KaTeX_Math;
font-style: italic;
diff --git a/test/katex-spec.js b/test/katex-spec.js
index 122c8db..2018b91 100644
--- a/test/katex-spec.js
+++ b/test/katex-spec.js
@@ -606,6 +606,15 @@ describe("A frac parser", function() {
expect(tfracParse.value.numer).toBeDefined();
expect(tfracParse.value.denom).toBeDefined();
});
+
+ it("should parse atop", function() {
+ var parse = getParsed("x \\atop y")[0];
+
+ expect(parse.type).toEqual("genfrac");
+ expect(parse.value.numer).toBeDefined();
+ expect(parse.value.denom).toBeDefined();
+ expect(parse.value.hasBarLine).toEqual(false);
+ });
});
describe("An over parser", function() {
@@ -883,8 +892,8 @@ describe("A delimiter sizing parser", function() {
var leftParse = getParsed(normalDelim)[0];
var rightParse = getParsed(bigDelim)[0];
- expect(leftParse.value.delimType).toEqual("open");
- expect(rightParse.value.delimType).toEqual("close");
+ expect(leftParse.value.mclass).toEqual("mopen");
+ expect(rightParse.value.mclass).toEqual("mclose");
});
it("should parse the correct size delimiter", function() {
@@ -1373,6 +1382,43 @@ describe("An HTML font tree-builder", function() {
expect(markup).toContain("R");
});
+ it("should render \\text{R} with the correct font", function() {
+ var markup = katex.renderToString("\\text{R}");
+ expect(markup).toContain("R");
+ });
+
+ it("should render \\textit{R} with the correct font", function() {
+ var markup = katex.renderToString("\\textit{R}");
+ expect(markup).toContain("R");
+ });
+
+ it("should render \\text{\\textit{R}} with the correct font", function() {
+ var markup = katex.renderToString("\\text{\\textit{R}}");
+ expect(markup).toContain("R");
+ });
+
+ it("should render \\text{R\\textit{S}T} with the correct fonts", function() {
+ var markup = katex.renderToString("\\text{R\\textit{S}T}");
+ expect(markup).toContain("R");
+ expect(markup).toContain("S");
+ expect(markup).toContain("T");
+ });
+
+ it("should render \\textbf{R} with the correct font", function() {
+ var markup = katex.renderToString("\\textbf{R}");
+ expect(markup).toContain("R");
+ });
+
+ it("should render \\textsf{R} with the correct font", function() {
+ var markup = katex.renderToString("\\textsf{R}");
+ expect(markup).toContain("R");
+ });
+
+ it("should render \\texttt{R} with the correct font", function() {
+ var markup = katex.renderToString("\\texttt{R}");
+ expect(markup).toContain("R");
+ });
+
it("should render a combination of font and color changes", function() {
var markup = katex.renderToString("\\color{blue}{\\mathbb R}");
var span = "R";
diff --git a/test/screenshotter/images/BinCancellation-chrome.png b/test/screenshotter/images/BinCancellation-chrome.png
new file mode 100644
index 0000000..749e2bd
Binary files /dev/null and b/test/screenshotter/images/BinCancellation-chrome.png differ
diff --git a/test/screenshotter/images/BinCancellation-firefox.png b/test/screenshotter/images/BinCancellation-firefox.png
new file mode 100644
index 0000000..021274e
Binary files /dev/null and b/test/screenshotter/images/BinCancellation-firefox.png differ
diff --git a/test/screenshotter/images/Cases-chrome.png b/test/screenshotter/images/Cases-chrome.png
index 775a3d9..d6b1382 100644
Binary files a/test/screenshotter/images/Cases-chrome.png and b/test/screenshotter/images/Cases-chrome.png differ
diff --git a/test/screenshotter/images/Cases-firefox.png b/test/screenshotter/images/Cases-firefox.png
index 4f77c85..901f2b6 100644
Binary files a/test/screenshotter/images/Cases-firefox.png and b/test/screenshotter/images/Cases-firefox.png differ
diff --git a/test/screenshotter/images/DashesAndQuotes-chrome.png b/test/screenshotter/images/DashesAndQuotes-chrome.png
index 14fdba8..ea21f69 100644
Binary files a/test/screenshotter/images/DashesAndQuotes-chrome.png and b/test/screenshotter/images/DashesAndQuotes-chrome.png differ
diff --git a/test/screenshotter/images/DashesAndQuotes-firefox.png b/test/screenshotter/images/DashesAndQuotes-firefox.png
index bf62c85..ca308ae 100644
Binary files a/test/screenshotter/images/DashesAndQuotes-firefox.png and b/test/screenshotter/images/DashesAndQuotes-firefox.png differ
diff --git a/test/screenshotter/images/FractionTest-chrome.png b/test/screenshotter/images/FractionTest-chrome.png
index b365f94..a54e5b3 100644
Binary files a/test/screenshotter/images/FractionTest-chrome.png and b/test/screenshotter/images/FractionTest-chrome.png differ
diff --git a/test/screenshotter/images/FractionTest-firefox.png b/test/screenshotter/images/FractionTest-firefox.png
index 7b4d559..298faed 100644
Binary files a/test/screenshotter/images/FractionTest-firefox.png and b/test/screenshotter/images/FractionTest-firefox.png differ
diff --git a/test/screenshotter/images/MathAtom-chrome.png b/test/screenshotter/images/MathAtom-chrome.png
new file mode 100644
index 0000000..175c72a
Binary files /dev/null and b/test/screenshotter/images/MathAtom-chrome.png differ
diff --git a/test/screenshotter/images/MathAtom-firefox.png b/test/screenshotter/images/MathAtom-firefox.png
new file mode 100644
index 0000000..d451d16
Binary files /dev/null and b/test/screenshotter/images/MathAtom-firefox.png differ
diff --git a/test/screenshotter/images/MathAtom2-chrome.png b/test/screenshotter/images/MathAtom2-chrome.png
new file mode 100644
index 0000000..cdfc0c7
Binary files /dev/null and b/test/screenshotter/images/MathAtom2-chrome.png differ
diff --git a/test/screenshotter/images/MathAtom2-firefox.png b/test/screenshotter/images/MathAtom2-firefox.png
new file mode 100644
index 0000000..cc0b696
Binary files /dev/null and b/test/screenshotter/images/MathAtom2-firefox.png differ
diff --git a/test/screenshotter/images/StackRel-chrome.png b/test/screenshotter/images/StackRel-chrome.png
new file mode 100644
index 0000000..f848e2f
Binary files /dev/null and b/test/screenshotter/images/StackRel-chrome.png differ
diff --git a/test/screenshotter/images/StackRel-firefox.png b/test/screenshotter/images/StackRel-firefox.png
new file mode 100644
index 0000000..deb5e2c
Binary files /dev/null and b/test/screenshotter/images/StackRel-firefox.png differ
diff --git a/test/screenshotter/images/Unicode-chrome.png b/test/screenshotter/images/Unicode-chrome.png
index 6d7472f..6c4f1ff 100644
Binary files a/test/screenshotter/images/Unicode-chrome.png and b/test/screenshotter/images/Unicode-chrome.png differ
diff --git a/test/screenshotter/images/Unicode-firefox.png b/test/screenshotter/images/Unicode-firefox.png
index bc0fc1b..18afc71 100644
Binary files a/test/screenshotter/images/Unicode-firefox.png and b/test/screenshotter/images/Unicode-firefox.png differ
diff --git a/test/screenshotter/images/UnsupportedCmds-chrome.png b/test/screenshotter/images/UnsupportedCmds-chrome.png
index 4d8a115..7eaba5f 100644
Binary files a/test/screenshotter/images/UnsupportedCmds-chrome.png and b/test/screenshotter/images/UnsupportedCmds-chrome.png differ
diff --git a/test/screenshotter/images/UnsupportedCmds-firefox.png b/test/screenshotter/images/UnsupportedCmds-firefox.png
index d2f19ca..797fbdd 100644
Binary files a/test/screenshotter/images/UnsupportedCmds-firefox.png and b/test/screenshotter/images/UnsupportedCmds-firefox.png differ
diff --git a/test/screenshotter/ss_data.yaml b/test/screenshotter/ss_data.yaml
index ca06906..a37a692 100644
--- a/test/screenshotter/ss_data.yaml
+++ b/test/screenshotter/ss_data.yaml
@@ -26,6 +26,11 @@ Arrays: |
ArrayType: 1\begin{array}{c}2\\3\end{array}4
Baseline: a+b-c\cdot d/e
BasicTest: a
+BinCancellation: |
+ \begin{array}{ccc}
+ +1 & 1+ & 1+1 & (,) \\
+ 1++1 & 3\times) & 1+, & \left(,\right)
+ \end{array}
BinomTest: \dbinom{a}{b}\tbinom{a}{b}^{\binom{a}{b}+17}
BoldSpacing: \mathbf{A}^2+\mathbf{B}_3*\mathscr{C}'
Cases: |
@@ -57,7 +62,7 @@ DisplayStyle: |
{\displaystyle\sqrt{x}}{\sqrt{x}}
{\displaystyle \frac12}{\frac12}{\displaystyle x^1_2}{x^1_2}
Exponents: a^{a^a_a}_{a^a_a}
-FractionTest: \dfrac{a}{b}\frac{a}{b}\tfrac{a}{b}\;-\dfrac12\;1\tfrac12
+FractionTest: \dfrac{a}{b}\frac{a}{b}\tfrac{a}{b}\;-\dfrac12\;1\tfrac12\;{1 \atop 2}
Functions: \sin\cos\tan\ln\log
GreekLetters: \alpha\beta\gamma\omega
KaTeX: \KaTeX
@@ -74,6 +79,8 @@ LeftRightStyleSizing: |
LimitControls: |
\displaystyle\int\limits_2^3 3x^2\,dx + \sum\nolimits^n_{i=1}i +
\textstyle\int\limits_x^y z
+MathAtom: a\mathrel{\mathop{=}\limits^{\blue ?}}b
+MathAtom2: \mathop{\overline\mathrm{lim}}\limits_{x\to\infty}f(x)
MathDefaultFonts: Ax2k\breve{a}\omega\Omega\imath+\KaTeX
MathBb: \mathbb{Ax2k\breve{a}\omega\Omega\imath+\KaTeX}
MathBf: \mathbf{Ax2k\breve{a}\omega\Omega\imath+\KaTeX}
@@ -109,6 +116,7 @@ Sqrt: |
^{\sqrt{\sqrt{\sqrt{x}}}}}
SqrtRoot: |
1+\sqrt[3]{2}+\sqrt[1923^234]{2^{2^{2^{2^{2^{2^{2^{2^{2^{2^{2^2}}}}}}}}}}}
+StackRel: a \stackrel{?}{=} b \stackrel{\text{def}}{=} c
StyleSwitching: a\cdot b\scriptstyle a\cdot ba\textstyle\cdot ba\scriptstyle\cdot b
SupSubCharacterBox: a_2f_2{f}_2{aa}_2{af}_2\mathbf{y}_Ay_A
SupSubHorizSpacing: |