Files
KaTeX/buildCommon.js
Emily Eisenberg 29b00ee6b7 Add vlist builder for more consistent stacking
Summary:
Add a way to automatically build vlists correctly. Previously, we built
vlists manually in ~4 different places, which made it difficult to manage
changes, and led to a large amount of duplication in the less. This also fixes
the vlist construction in safari, where the `display: inline-table` wasn't being
applied because of CSS specificity. This leads to the only significant change in
the huxley tests, with the vertical spacing.

Test Plan:
 - Make sure the tests still work
 - Make sure most of the huxley screenshots didn't change, and that the new
   changes are insignificant.
 - Make sure vlists now work in Safari
 - Make sure the change to the VerticalSpacing screenshot is caused by the
   fix-baseline span now correctly applying `display: inline-table` by creating
   the construct in master and adding `display: inline-table !important` to the
   `.fix-ie` css rule

Reviewers: alpert

Reviewed By: alpert

Differential Revision: http://phabricator.khanacademy.org/D13082
2014-09-12 14:41:31 -07:00

233 lines
8.2 KiB
JavaScript

var domTree = require("./domTree");
var fontMetrics = require("./fontMetrics");
var symbols = require("./symbols");
var makeText = function(value, style, mode) {
if (symbols[mode][value] && symbols[mode][value].replace) {
value = symbols[mode][value].replace;
}
var metrics = fontMetrics.getCharacterMetrics(value, style);
if (metrics) {
var textNode = new domTree.textNode(value, metrics.height,
metrics.depth);
if (metrics.italic > 0) {
var span = makeSpan([], [textNode]);
span.style.marginRight = metrics.italic + "em";
return span;
} else {
return textNode;
}
} else {
console && console.warn("No character metrics for '" + value +
"' in style '" + style + "'");
return new domTree.textNode(value, 0, 0);
}
};
var mathit = function(value, mode) {
return makeSpan(["mathit"], [makeText(value, "Math-Italic", mode)]);
};
var mathrm = function(value, mode) {
if (symbols[mode][value].font === "main") {
return makeText(value, "Main-Regular", mode);
} else {
return makeSpan(["amsrm"], [makeText(value, "AMS-Regular", mode)]);
}
};
var sizeElementFromChildren = function(elem) {
var height = 0;
var depth = 0;
var maxFontSize = 0;
if (elem.children) {
for (var i = 0; i < elem.children.length; i++) {
if (elem.children[i].height > height) {
height = elem.children[i].height;
}
if (elem.children[i].depth > depth) {
depth = elem.children[i].depth;
}
if (elem.children[i].maxFontSize > maxFontSize) {
maxFontSize = elem.children[i].maxFontSize;
}
}
}
elem.height = height;
elem.depth = depth;
elem.maxFontSize = maxFontSize;
};
var makeSpan = function(classes, children, color) {
var span = new domTree.span(classes, children);
sizeElementFromChildren(span);
if (color) {
span.style.color = color;
}
return span;
};
var makeFragment = function(children) {
var fragment = new domTree.documentFragment(children);
sizeElementFromChildren(fragment);
return fragment;
};
var makeFontSizer = function(options, fontSize) {
var fontSizeInner = makeSpan([], [new domTree.textNode("\u200b")]);
fontSizeInner.style.fontSize = (fontSize / options.style.sizeMultiplier) + "em";
var fontSizer = makeSpan(
["fontsize-ensurer", "reset-" + options.size, "size5"],
[fontSizeInner]);
return fontSizer;
};
/*
* Makes a vertical list by stacking elements and kerns on top of each other.
* Allows for many different ways of specifying the positioning method.
*
* Arguments:
* - children: A list of child or kern nodes to be stacked on top of each other
* (i.e. the first element will be at the bottom, and the last at
* the top). Element nodes are specified as
* {type: "elem", elem: node}
* while kern nodes are specified as
* {type: "kern", size: size}
* - positionType: The method by which the vlist should be positioned. Valid
* values are:
* - "individualShift": The children list only contains elem
* nodes, and each node contains an extra
* "shift" value of how much it should be
* shifted (note that shifting is always
* moving downwards). positionData is
* ignored.
* - "top": The positionData specifies the topmost point of
* the vlist (note this is expected to be a height,
* so positive values move up)
* - "bottom": The positionData specifies the bottommost point
* of the vlist (note this is expected to be a
* depth, so positive values move down
* - "shift": The vlist will be positioned such that its
* baseline is positionData away from the baseline
* of the first child. Positive values move
* downwards.
* - "firstBaseline": The vlist will be positioned such that
* its baseline is aligned with the
* baseline of the first child.
* positionData is ignored. (this is
* equivalent to "shift" with
* positionData=0)
* - positionData: Data used in different ways depending on positionType
* - options: An Options object
*
*/
var makeVList = function(children, positionType, positionData, options) {
var depth;
if (positionType === "individualShift") {
var oldChildren = children;
children = [oldChildren[0]];
// Add in kerns to the list of children to get each element to be
// shifted to the correct specified shift
depth = -oldChildren[0].shift - oldChildren[0].elem.depth;
var currPos = depth;
for (var i = 1; i < oldChildren.length; i++) {
var diff = -oldChildren[i].shift - currPos -
oldChildren[i].elem.depth;
var size = diff -
(oldChildren[i - 1].elem.height +
oldChildren[i - 1].elem.depth);
currPos = currPos + diff;
children.push({type: "kern", size: size});
children.push(oldChildren[i]);
}
} else if (positionType === "top") {
// We always start at the bottom, so calculate the bottom by adding up
// all the sizes
var bottom = positionData;
for (var i = 0; i < children.length; i++) {
if (children[i].type === "kern") {
bottom -= children[i].size;
} else {
bottom -= children[i].elem.height + children[i].elem.depth;
}
}
depth = bottom;
} else if (positionType === "bottom") {
depth = -positionData;
} else if (positionType === "shift") {
depth = -children[0].elem.depth - positionData;
} else if (positionType === "firstBaseline") {
depth = -children[0].elem.depth;
} else {
depth = 0;
}
// Make the fontSizer
var maxFontSize = 0;
for (var i = 0; i < children.length; i++) {
if (children[i].type === "elem") {
maxFontSize = Math.max(maxFontSize, children[i].elem.maxFontSize);
}
}
var fontSizer = makeFontSizer(options, maxFontSize);
// Create a new list of actual children at the correct offsets
var realChildren = [];
var currPos = depth;
for (var i = 0; i < children.length; i++) {
if (children[i].type === "kern") {
currPos += children[i].size;
} else {
var child = children[i].elem;
var shift = -child.depth - currPos;
currPos += child.height + child.depth;
var childWrap = makeSpan([], [fontSizer, child]);
childWrap.height -= shift;
childWrap.depth += shift;
childWrap.style.top = shift + "em";
realChildren.push(childWrap);
}
}
// Add in an element at the end with no offset to fix the calculation of
// baselines in some browsers (namely IE, sometimes safari)
var baselineFix = makeSpan(
["baseline-fix"], [fontSizer, new domTree.textNode("\u00a0")]);
realChildren.push(baselineFix);
var vlist = makeSpan(["vlist"], realChildren);
// Fix the final height and depth, in case there were kerns at the ends
// since the makeSpan calculation won't take that in to account.
vlist.height = Math.max(currPos, vlist.height);
vlist.depth = Math.max(-depth, vlist.depth);
return vlist;
};
module.exports = {
makeText: makeText,
mathit: mathit,
mathrm: mathrm,
makeSpan: makeSpan,
makeFragment: makeFragment,
makeFontSizer: makeFontSizer,
makeVList: makeVList
};