
Summary: Add comments everywhere! Also fix some small bugs like using Style.id instead of Style.size, and rename some variables to be more descriptive. Fixes #22 Test Plan: - Make sure the huxley screenshots didn't change - Make sure the tests still pass Reviewers: alpert Reviewed By: alpert Differential Revision: http://phabricator.khanacademy.org/D13158
549 lines
19 KiB
JavaScript
549 lines
19 KiB
JavaScript
/**
|
|
* This file deals with creating delimiters of various sizes. The TeXbook
|
|
* discusses these routines on page 441-442, in the "Another subroutine sets box
|
|
* x to a specified variable delimiter" paragraph.
|
|
*
|
|
* There are three main routines here. `makeSmallDelim` makes a delimiter in the
|
|
* normal font, but in either text, script, or scriptscript style.
|
|
* `makeLargeDelim` makes a delimiter in textstyle, but in one of the Size1,
|
|
* Size2, Size3, or Size4 fonts. `makeStackedDelim` makes a delimiter out of
|
|
* smaller pieces that are stacked on top of one another.
|
|
*
|
|
* The functions take a parameter `center`, which determines if the delimiter
|
|
* should be centered around the axis.
|
|
*
|
|
* Then, there are three exposed functions. `sizedDelim` makes a delimiter in
|
|
* one of the given sizes. This is used for things like `\bigl`.
|
|
* `customSizedDelim` makes a delimiter with a given total height+depth. It is
|
|
* called in places like `\sqrt`. `leftRightDelim` makes an appropriate
|
|
* delimiter which surrounds an expression of a given height an depth. It is
|
|
* used in `\left` and `\right`.
|
|
*/
|
|
|
|
var Options = require("./Options");
|
|
var ParseError = require("./ParseError");
|
|
var Style = require("./Style");
|
|
|
|
var buildCommon = require("./buildCommon");
|
|
var domTree = require("./domTree");
|
|
var fontMetrics = require("./fontMetrics");
|
|
var parseTree = require("./parseTree");
|
|
var symbols = require("./symbols");
|
|
var utils = require("./utils");
|
|
|
|
var makeSpan = buildCommon.makeSpan;
|
|
|
|
/**
|
|
* Get the metrics for a given symbol and font, after transformation (i.e.
|
|
* after following replacement from symbols.js)
|
|
*/
|
|
var getMetrics = function(symbol, font) {
|
|
if (symbols["math"][symbol] && symbols["math"][symbol].replace) {
|
|
return fontMetrics.getCharacterMetrics(
|
|
symbols["math"][symbol].replace, font);
|
|
} else {
|
|
return fontMetrics.getCharacterMetrics(
|
|
symbol, font);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Builds a symbol in the given font size (note size is an integer)
|
|
*/
|
|
var mathrmSize = function(value, size, mode) {
|
|
return buildCommon.makeSymbol(value, "Size" + size + "-Regular", mode);
|
|
};
|
|
|
|
/**
|
|
* Puts a delimiter span in a given style, and adds appropriate height, depth,
|
|
* and maxFontSizes.
|
|
*/
|
|
var styleWrap = function(delim, toStyle, options) {
|
|
var span = makeSpan(
|
|
["style-wrap", options.style.reset(), toStyle.cls()], [delim]);
|
|
|
|
var multiplier = toStyle.sizeMultiplier / options.style.sizeMultiplier;
|
|
|
|
span.height *= multiplier;
|
|
span.depth *= multiplier;
|
|
span.maxFontSize = toStyle.sizeMultiplier;
|
|
|
|
return span;
|
|
};
|
|
|
|
/**
|
|
* Makes a small delimiter. This is a delimiter that comes in the Main-Regular
|
|
* font, but is restyled to either be in textstyle, scriptstyle, or
|
|
* scriptscriptstyle.
|
|
*/
|
|
var makeSmallDelim = function(delim, style, center, options, mode) {
|
|
var text = buildCommon.makeSymbol(delim, "Main-Regular", mode);
|
|
|
|
var span = styleWrap(text, style, options);
|
|
|
|
if (center) {
|
|
var shift =
|
|
(1 - options.style.sizeMultiplier / style.sizeMultiplier) *
|
|
fontMetrics.metrics.axisHeight;
|
|
|
|
span.style.top = shift + "em";
|
|
span.height -= shift;
|
|
span.depth += shift;
|
|
}
|
|
|
|
return span;
|
|
};
|
|
|
|
/**
|
|
* 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 inner = mathrmSize(delim, size, mode);
|
|
|
|
var span = styleWrap(
|
|
makeSpan(["delimsizing", "size" + size],
|
|
[inner], options.getColor()),
|
|
Style.TEXT, options);
|
|
|
|
if (center) {
|
|
var shift = (1 - options.style.sizeMultiplier) *
|
|
fontMetrics.metrics.axisHeight;
|
|
|
|
span.style.top = shift + "em";
|
|
span.height -= shift;
|
|
span.depth += shift;
|
|
}
|
|
|
|
return span;
|
|
};
|
|
|
|
/**
|
|
* Make an inner span with the given offset and in the given font. This is used
|
|
* in `makeStackedDelim` to make the stacking pieces for the delimiter.
|
|
*/
|
|
var makeInner = function(symbol, font, mode) {
|
|
var sizeClass;
|
|
// Apply the correct CSS class to choose the right font.
|
|
if (font === "Size1-Regular") {
|
|
sizeClass = "delim-size1";
|
|
} else if (font === "Size4-Regular") {
|
|
sizeClass = "delim-size4";
|
|
}
|
|
|
|
var inner = makeSpan(
|
|
["delimsizinginner", sizeClass],
|
|
[makeSpan([], [buildCommon.makeSymbol(symbol, font, mode)])]);
|
|
|
|
// Since this will be passed into `makeVList` in the end, wrap the element
|
|
// in the appropriate tag that VList uses.
|
|
return {type: "elem", elem: inner};
|
|
};
|
|
|
|
/**
|
|
* 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) {
|
|
// There are four parts, the top, an optional middle, a repeated part, and a
|
|
// bottom.
|
|
var top, middle, repeat, bottom;
|
|
top = repeat = bottom = delim;
|
|
middle = null;
|
|
// Also keep track of what font the delimiters are in
|
|
var font = "Size1-Regular";
|
|
|
|
// We set the parts and font based on the symbol. Note that we use
|
|
// '\u23d0' instead of '|' and '\u2016' instead of '\\|' for the
|
|
// repeats of the arrows
|
|
if (delim === "\\uparrow") {
|
|
repeat = bottom = "\u23d0";
|
|
} else if (delim === "\\Uparrow") {
|
|
repeat = bottom = "\u2016";
|
|
} else if (delim === "\\downarrow") {
|
|
top = repeat = "\u23d0";
|
|
} else if (delim === "\\Downarrow") {
|
|
top = repeat = "\u2016";
|
|
} else if (delim === "\\updownarrow") {
|
|
top = "\\uparrow";
|
|
repeat = "\u23d0";
|
|
bottom = "\\downarrow";
|
|
} else if (delim === "\\Updownarrow") {
|
|
top = "\\Uparrow";
|
|
repeat = "\u2016";
|
|
bottom = "\\Downarrow";
|
|
} else if (delim === "|" || delim === "\\vert") {
|
|
} else if (delim === "\\|" || delim === "\\Vert") {
|
|
} else if (delim === "[" || delim === "\\lbrack") {
|
|
top = "\u23a1";
|
|
repeat = "\u23a2";
|
|
bottom = "\u23a3";
|
|
font = "Size4-Regular";
|
|
} else if (delim === "]" || delim === "\\rbrack") {
|
|
top = "\u23a4";
|
|
repeat = "\u23a5";
|
|
bottom = "\u23a6";
|
|
font = "Size4-Regular";
|
|
} else if (delim === "\\lfloor") {
|
|
repeat = top = "\u23a2";
|
|
bottom = "\u23a3";
|
|
font = "Size4-Regular";
|
|
} else if (delim === "\\lceil") {
|
|
top = "\u23a1";
|
|
repeat = bottom = "\u23a2";
|
|
font = "Size4-Regular";
|
|
} else if (delim === "\\rfloor") {
|
|
repeat = top = "\u23a5";
|
|
bottom = "\u23a6";
|
|
font = "Size4-Regular";
|
|
} else if (delim === "\\rceil") {
|
|
top = "\u23a4";
|
|
repeat = bottom = "\u23a5";
|
|
font = "Size4-Regular";
|
|
} else if (delim === "(") {
|
|
top = "\u239b";
|
|
repeat = "\u239c";
|
|
bottom = "\u239d";
|
|
font = "Size4-Regular";
|
|
} else if (delim === ")") {
|
|
top = "\u239e";
|
|
repeat = "\u239f";
|
|
bottom = "\u23a0";
|
|
font = "Size4-Regular";
|
|
} else if (delim === "\\{" || delim === "\\lbrace") {
|
|
top = "\u23a7";
|
|
middle = "\u23a8";
|
|
bottom = "\u23a9";
|
|
repeat = "\u23aa";
|
|
font = "Size4-Regular";
|
|
} else if (delim === "\\}" || delim === "\\rbrace") {
|
|
top = "\u23ab";
|
|
middle = "\u23ac";
|
|
bottom = "\u23ad";
|
|
repeat = "\u23aa";
|
|
font = "Size4-Regular";
|
|
} else if (delim === "\\surd") {
|
|
top = "\ue001";
|
|
bottom = "\u23b7";
|
|
repeat = "\ue000";
|
|
font = "Size4-Regular";
|
|
}
|
|
|
|
// Get the metrics of the four sections
|
|
var topMetrics = getMetrics(top, font);
|
|
var topHeightTotal = topMetrics.height + topMetrics.depth;
|
|
var repeatMetrics = getMetrics(repeat, font);
|
|
var repeatHeightTotal = repeatMetrics.height + repeatMetrics.depth;
|
|
var bottomMetrics = getMetrics(bottom, font);
|
|
var bottomHeightTotal = bottomMetrics.height + bottomMetrics.depth;
|
|
var middleMetrics, middleHeightTotal;
|
|
if (middle !== null) {
|
|
middleMetrics = getMetrics(middle, font);
|
|
middleHeightTotal = middleMetrics.height + middleMetrics.depth;
|
|
}
|
|
|
|
// Calcuate the real height that the delimiter will have. It is at least the
|
|
// size of the top, bottom, and optional middle combined.
|
|
var realHeightTotal = topHeightTotal + bottomHeightTotal;
|
|
if (middle !== null) {
|
|
realHeightTotal += middleHeightTotal;
|
|
}
|
|
|
|
// Then add repeated pieces until we reach the specified height.
|
|
while (realHeightTotal < heightTotal) {
|
|
realHeightTotal += repeatHeightTotal;
|
|
if (middle !== null) {
|
|
// If there is a middle section, we need an equal number of pieces
|
|
// on the top and bottom.
|
|
realHeightTotal += repeatHeightTotal;
|
|
}
|
|
}
|
|
|
|
// The center of the delimiter is placed at the center of the axis. Note
|
|
// that in this context, "center" means that the delimiter should be
|
|
// centered around the axis in the current style, while normally it is
|
|
// centered around the axis in textstyle.
|
|
var axisHeight = fontMetrics.metrics.axisHeight;
|
|
if (center) {
|
|
axisHeight *= options.style.sizeMultiplier;
|
|
}
|
|
// Calculate the height and depth
|
|
var height = realHeightTotal / 2 + axisHeight;
|
|
var depth = realHeightTotal / 2 - axisHeight;
|
|
|
|
// Now, we start building the pieces that will go into the vlist
|
|
|
|
// Keep a list of the inner pieces
|
|
var inners = [];
|
|
|
|
// Add the bottom symbol
|
|
inners.push(makeInner(bottom, font, mode));
|
|
|
|
if (middle === null) {
|
|
// Calculate the number of repeated symbols we need
|
|
var repeatHeight = realHeightTotal - topHeightTotal - bottomHeightTotal;
|
|
var symbolCount = Math.ceil(repeatHeight / repeatHeightTotal);
|
|
|
|
// Add that many symbols
|
|
for (var i = 0; i < symbolCount; i++) {
|
|
inners.push(makeInner(repeat, font, mode));
|
|
}
|
|
} else {
|
|
// When there is a middle bit, we need the middle part and two repeated
|
|
// sections
|
|
|
|
// Calculate the number of symbols needed for the top and bottom
|
|
// repeated parts
|
|
var topRepeatHeight =
|
|
realHeightTotal / 2 - topHeightTotal - middleHeightTotal / 2;
|
|
var topSymbolCount = Math.ceil(topRepeatHeight / repeatHeightTotal);
|
|
|
|
var bottomRepeatHeight =
|
|
realHeightTotal / 2 - topHeightTotal - middleHeightTotal / 2;
|
|
var bottomSymbolCount =
|
|
Math.ceil(bottomRepeatHeight / repeatHeightTotal);
|
|
|
|
// Add the top repeated part
|
|
for (var i = 0; i < topSymbolCount; i++) {
|
|
inners.push(makeInner(repeat, font, mode));
|
|
}
|
|
|
|
// Add the middle piece
|
|
inners.push(makeInner(middle, font, mode));
|
|
|
|
// Add the bottom repeated part
|
|
for (var i = 0; i < bottomSymbolCount; i++) {
|
|
inners.push(makeInner(repeat, font, mode));
|
|
}
|
|
}
|
|
|
|
// Add the top symbol
|
|
inners.push(makeInner(top, font, mode));
|
|
|
|
// Finally, build the vlist
|
|
var inner = buildCommon.makeVList(inners, "bottom", depth, options);
|
|
|
|
return styleWrap(
|
|
makeSpan(["delimsizing", "mult"], [inner], options.getColor()),
|
|
Style.TEXT, options);
|
|
};
|
|
|
|
// There are three kinds of delimiters, delimiters that stack when they become
|
|
// too large
|
|
var stackLargeDelimiters = [
|
|
"(", ")", "[", "\\lbrack", "]", "\\rbrack",
|
|
"\\{", "\\lbrace", "\\}", "\\rbrace",
|
|
"\\lfloor", "\\rfloor", "\\lceil", "\\rceil",
|
|
"\\surd"
|
|
];
|
|
|
|
// delimiters that always stack
|
|
var stackAlwaysDelimiters = [
|
|
"\\uparrow", "\\downarrow", "\\updownarrow",
|
|
"\\Uparrow", "\\Downarrow", "\\Updownarrow",
|
|
"|", "\\|", "\\vert", "\\Vert"
|
|
];
|
|
|
|
// and delimiters that never stack
|
|
var stackNeverDelimiters = [
|
|
"<", ">", "\\langle", "\\rangle", "/", "\\backslash"
|
|
];
|
|
|
|
// Metrics of the different sizes. Found by looking at TeX's output of
|
|
// $\bigl| // \Bigl| \biggl| \Biggl| \showlists$
|
|
// Used to create stacked delimiters of appropriate sizes in makeSizedDelim.
|
|
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) {
|
|
// < and > turn into \langle and \rangle in delimiters
|
|
if (delim === "<") {
|
|
delim = "\\langle";
|
|
} else if (delim === ">") {
|
|
delim = "\\rangle";
|
|
}
|
|
|
|
var retDelim;
|
|
|
|
// Sized delimiters are never centered.
|
|
if (utils.contains(stackLargeDelimiters, delim) ||
|
|
utils.contains(stackNeverDelimiters, delim)) {
|
|
return makeLargeDelim(delim, size, false, options, mode);
|
|
} else if (utils.contains(stackAlwaysDelimiters, delim)) {
|
|
return makeStackedDelim(
|
|
delim, sizeToMaxHeight[size], false, options, mode);
|
|
} else {
|
|
throw new ParseError("Illegal delimiter: '" + delim + "'");
|
|
}
|
|
};
|
|
|
|
/**
|
|
* There are three different sequences of delimiter sizes that the delimiters
|
|
* follow depending on the kind of delimiter. This is used when creating custom
|
|
* sized delimiters to decide whether to create a small, large, or stacked
|
|
* delimiter.
|
|
*
|
|
* In real TeX, these sequences aren't explicitly defined, but are instead
|
|
* defined inside the font metrics. Since there are only three sequences that
|
|
* are possible for the delimiters that TeX defines, it is easier to just encode
|
|
* them explicitly here.
|
|
*/
|
|
|
|
// Delimiters that never stack try small delimiters and large delimiters only
|
|
var stackNeverDelimiterSequence = [
|
|
{type: "small", style: Style.SCRIPTSCRIPT},
|
|
{type: "small", style: Style.SCRIPT},
|
|
{type: "small", style: Style.TEXT},
|
|
{type: "large", size: 1},
|
|
{type: "large", size: 2},
|
|
{type: "large", size: 3},
|
|
{type: "large", size: 4}
|
|
];
|
|
|
|
// Delimiters that always stack try the small delimiters first, then stack
|
|
var stackAlwaysDelimiterSequence = [
|
|
{type: "small", style: Style.SCRIPTSCRIPT},
|
|
{type: "small", style: Style.SCRIPT},
|
|
{type: "small", style: Style.TEXT},
|
|
{type: "stack"}
|
|
];
|
|
|
|
// Delimiters that stack when large try the small and then large delimiters, and
|
|
// stack afterwards
|
|
var stackLargeDelimiterSequence = [
|
|
{type: "small", style: Style.SCRIPTSCRIPT},
|
|
{type: "small", style: Style.SCRIPT},
|
|
{type: "small", style: Style.TEXT},
|
|
{type: "large", size: 1},
|
|
{type: "large", size: 2},
|
|
{type: "large", size: 3},
|
|
{type: "large", size: 4},
|
|
{type: "stack"}
|
|
];
|
|
|
|
/**
|
|
* Get the font used in a delimiter based on what kind of delimiter it is.
|
|
*/
|
|
var delimTypeToFont = function(type) {
|
|
if (type.type === "small") {
|
|
return "Main-Regular";
|
|
} else if (type.type === "large") {
|
|
return "Size" + type.size + "-Regular";
|
|
} else if (type.type === "stack") {
|
|
return "Size4-Regular";
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Traverse a sequence of types of delimiters to decide what kind of delimiter
|
|
* should be used to create a delimiter of the given height+depth.
|
|
*/
|
|
var traverseSequence = function(delim, height, sequence, options) {
|
|
// Here, we choose the index we should start at in the sequences. In smaller
|
|
// sizes (which correspond to larger numbers in style.size) we start earlier
|
|
// in the sequence. Thus, scriptscript starts at index 3-3=0, script starts
|
|
// at index 3-2=1, text starts at 3-1=2, and display starts at min(2,3-0)=2
|
|
var start = Math.min(2, 3 - options.style.size);
|
|
for (var i = start; i < sequence.length; i++) {
|
|
if (sequence[i].type === "stack") {
|
|
// This is always the last delimiter, so we just break the loop now.
|
|
break;
|
|
}
|
|
|
|
var metrics = getMetrics(delim, delimTypeToFont(sequence[i]));
|
|
var heightDepth = metrics.height + metrics.depth;
|
|
|
|
// Small delimiters are scaled down versions of the same font, so we
|
|
// account for the style change size.
|
|
|
|
if (sequence[i].type === "small") {
|
|
heightDepth *= sequence[i].style.sizeMultiplier;
|
|
}
|
|
|
|
// Check if the delimiter at this size works for the given height.
|
|
if (heightDepth > height) {
|
|
return sequence[i];
|
|
}
|
|
}
|
|
|
|
// If we reached the end of the sequence, return the last sequence element.
|
|
return sequence[sequence.length - 1];
|
|
};
|
|
|
|
/**
|
|
* 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) {
|
|
if (delim === "<") {
|
|
delim = "\\langle";
|
|
} else if (delim === ">") {
|
|
delim = "\\rangle";
|
|
}
|
|
|
|
// Decide what sequence to use
|
|
var sequence;
|
|
if (utils.contains(stackNeverDelimiters, delim)) {
|
|
sequence = stackNeverDelimiterSequence;
|
|
} else if (utils.contains(stackLargeDelimiters, delim)) {
|
|
sequence = stackLargeDelimiterSequence;
|
|
} else {
|
|
sequence = stackAlwaysDelimiterSequence;
|
|
}
|
|
|
|
// Look through the sequence
|
|
var delimType = traverseSequence(delim, height, sequence, options);
|
|
|
|
// Depending on the sequence element we decided on, call the appropriate
|
|
// function.
|
|
if (delimType.type === "small") {
|
|
return makeSmallDelim(delim, delimType.style, center, options, mode);
|
|
} else if (delimType.type === "large") {
|
|
return makeLargeDelim(delim, delimType.size, center, options, mode);
|
|
} else if (delimType.type === "stack") {
|
|
return makeStackedDelim(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) {
|
|
// We always center \left/\right delimiters, so the axis is always shifted
|
|
var axisHeight =
|
|
fontMetrics.metrics.axisHeight * options.style.sizeMultiplier;
|
|
|
|
// Taken from TeX source, tex.web, function make_left_right
|
|
var delimiterFactor = 901;
|
|
var delimiterExtend = 5.0 / fontMetrics.metrics.ptPerEm;
|
|
|
|
var maxDistFromAxis = Math.max(
|
|
height - axisHeight, depth + axisHeight);
|
|
|
|
var totalHeight = Math.max(
|
|
// In real TeX, calculations are done using integral values which are
|
|
// 65536 per pt, or 655360 per em. So, the division here truncates in
|
|
// TeX but doesn't here, producing different results. If we wanted to
|
|
// exactly match TeX's calculation, we could do
|
|
// Math.floor(655360 * maxDistFromAxis / 500) *
|
|
// delimiterFactor / 655360
|
|
// (To see the difference, compare
|
|
// x^{x^{\left(\rule{0.1em}{0.68em}\right)}}
|
|
// in TeX and KaTeX)
|
|
maxDistFromAxis / 500 * delimiterFactor,
|
|
2 * maxDistFromAxis - delimiterExtend);
|
|
|
|
// Finally, we defer to `makeCustomSizedDelim` with our calculated total
|
|
// height
|
|
return makeCustomSizedDelim(delim, totalHeight, true, options, mode);
|
|
};
|
|
|
|
module.exports = {
|
|
sizedDelim: makeSizedDelim,
|
|
customSizedDelim: makeCustomSizedDelim,
|
|
leftRightDelim: makeLeftRightDelim
|
|
};
|