scribble-math/delimiter.js
Emily Eisenberg 925c96dbe2 Add square roots (\sqrt)
Summary:
Follow the TeXbook instructions on how to construct square roots. Using
makeCustomSizedDelim, this becomes nearly trivial.

Test Plan:
 - Make sure normal tests work
 - Make sure the new huxley test looks good, and other huxley tests haven't changed.

Reviewers: alpert

Reviewed By: alpert

Differential Revision: http://phabricator.khanacademy.org/D12918
2014-09-06 15:08:23 -07:00

483 lines
16 KiB
JavaScript

var Options = require("./Options");
var ParseError = require("./ParseError");
var Style = require("./Style");
var domTree = require("./domTree");
var fontMetrics = require("./fontMetrics");
var parseTree = require("./parseTree");
var utils = require("./utils");
var symbols = require("./symbols");
var buildCommon = require("./buildCommon");
var makeSpan = require("./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);
}
};
var mathrmSize = function(value, size, mode) {
return buildCommon.makeText(value, "Size" + size + "-Regular", mode);
};
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;
};
var makeSmallDelim = function(delim, style, center, options, mode) {
var text = buildCommon.makeText(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;
};
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
var makeInner = function(symbol, offset, font, mode) {
var sizeClass;
if (font === "Size1-Regular") {
sizeClass = "size1";
} else if (font === "Size4-Regular") {
sizeClass = "size4";
}
var inner = makeSpan(
["delimsizinginner", sizeClass],
[makeSpan([], [buildCommon.makeText(symbol, font, mode)])]);
inner.style.top = offset + "em";
inner.height -= offset;
inner.depth += offset;
return inner;
};
var makeStackedDelim = function(delim, heightTotal, center, options, mode) {
// There are four parts, the top, a middle, a repeated part, and a bottom.
var top, middle, repeat, bottom;
top = repeat = bottom = delim;
middle = null;
var font = "Size1-Regular";
var overlap = false;
// 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";
// For some reason, the sizes of this one delimiter don't work out
// right, so we shrink it a bit to make it now add an extraneous
// repeating part
if (height + depth <= 1.21) {
height -= 0.01;
depth -= 0.01;
}
} else if (delim === "|" || delim === "\\vert") {
overlap = true;
} else if (delim === "\\|" || delim === "\\Vert") {
overlap = true;
} else if (delim === "[" || delim === "\\lbrack") {
top = "\u23a1";
repeat = "\u23a2";
bottom = "\u23a3";
font = "Size4-Regular";
overlap = true;
} else if (delim === "]" || delim === "\\rbrack") {
top = "\u23a4";
repeat = "\u23a5";
bottom = "\u23a6";
font = "Size4-Regular";
overlap = true;
} else if (delim === "\\lfloor") {
repeat = top = "\u23a2";
bottom = "\u23a3";
font = "Size4-Regular";
overlap = true;
} else if (delim === "\\lceil") {
top = "\u23a1";
repeat = bottom = "\u23a2";
font = "Size4-Regular";
overlap = true;
} else if (delim === "\\rfloor") {
repeat = top = "\u23a5";
bottom = "\u23a6";
font = "Size4-Regular";
overlap = true;
} else if (delim === "\\rceil") {
top = "\u23a4";
repeat = bottom = "\u23a5";
font = "Size4-Regular";
overlap = true;
} else if (delim === "(") {
top = "\u239b";
repeat = "\u239c";
bottom = "\u239d";
font = "Size4-Regular";
overlap = true;
} else if (delim === ")") {
top = "\u239e";
repeat = "\u239f";
bottom = "\u23a0";
font = "Size4-Regular";
overlap = true;
} 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";
overlap = true;
}
// Get the metrics of the three 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;
}
var realHeightTotal = topHeightTotal + bottomHeightTotal;
if (middle !== null) {
realHeightTotal += middleHeightTotal;
}
while (realHeightTotal < heightTotal) {
realHeightTotal += repeatHeightTotal;
if (middle !== null) {
realHeightTotal += repeatHeightTotal;
}
}
var axisHeight = fontMetrics.metrics.axisHeight;
if (center) {
axisHeight *= options.style.sizeMultiplier;
}
var height = realHeightTotal / 2 + axisHeight;
var depth = realHeightTotal / 2 - axisHeight;
// Keep a list of the inner spans
var inners = [];
// Add the top symbol
inners.push(
makeInner(top, topMetrics.height - height, font, mode));
if (middle === null) {
var repeatHeight = realHeightTotal - topHeightTotal - bottomHeightTotal;
var symbolCount = Math.ceil(repeatHeight / repeatHeightTotal);
var overlapAmount;
if (overlap) {
// 2 * overlapAmount + repeatHeight =
// (symbolCount - 1) * (repeatHeightTotal - overlapAmount) +
// repeatHeightTotal
overlapAmount = (symbolCount * repeatHeightTotal -
repeatHeight) / (symbolCount + 1);
} else {
overlapAmount = 0;
}
// Add repeat symbols until there's only space for the bottom symbol
var currHeight = height - topHeightTotal + overlapAmount;
for (var i = 0; i < symbolCount; i++) {
inners.push(
makeInner(repeat,
repeatMetrics.height - currHeight, font, mode));
currHeight -= repeatHeightTotal - overlapAmount;
}
} 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
var currHeight = height - topHeightTotal;
for (var i = 0; i < topSymbolCount; i++) {
inners.push(
makeInner(repeat,
repeatMetrics.height - currHeight, font, mode));
currHeight -= repeatHeightTotal;
}
// Add the middle piece
var midPoint = realHeightTotal / 2 - depth;
inners.push(
makeInner(middle,
middleMetrics.height - midPoint - middleHeightTotal / 2,
font, mode));
// Add the bottom repeated part
currHeight = midPoint - middleHeightTotal / 2;
for (var i = 0; i < bottomSymbolCount; i++) {
inners.push(
makeInner(repeat,
repeatMetrics.height - currHeight, font, mode));
currHeight -= repeatHeightTotal;
}
}
// Add the bottom symbol
inners.push(
makeInner(bottom, depth - bottomMetrics.depth, font, mode));
var fixIE = makeSpan(["fix-ie"], [new domTree.textNode("\u00a0")]);
inners.push(fixIE);
return styleWrap(
makeSpan(["delimsizing", "mult"], inners, options.getColor()),
Style.TEXT, options);
};
var normalDelimiters = [
"(", ")", "[", "\\lbrack", "]", "\\rbrack",
"\\{", "\\lbrace", "\\}", "\\rbrace",
"\\lfloor", "\\rfloor", "\\lceil", "\\rceil",
"<", ">", "\\langle", "\\rangle", "/", "\\backslash",
"\\surd"
];
var stackDelimiters = [
"\\uparrow", "\\downarrow", "\\updownarrow",
"\\Uparrow", "\\Downarrow", "\\Updownarrow",
"|", "\\|", "\\vert", "\\Vert"
];
var onlyNormalDelimiters = [
"<", ">", "\\langle", "\\rangle", "/", "\\backslash"
];
// Metrics of the different sizes. Found by looking at TeX's output of
// $\bigl| \Bigl| \biggl| \Biggl| \showlists$
var sizeToMaxHeight = [0, 1.2, 1.8, 2.4, 3.0];
var makeSizedDelim = function(delim, size, options, mode) {
if (delim === "<") {
delim = "\\langle";
} else if (delim === ">") {
delim = "\\rangle";
}
var retDelim;
if (utils.contains(normalDelimiters, delim)) {
return makeLargeDelim(delim, size, false, options, mode);
} else if (utils.contains(stackDelimiters, delim)) {
return makeStackedDelim(
delim, sizeToMaxHeight[size], false, options, mode);
} else {
throw new ParseError("Illegal delimiter: '" + delim + "'");
}
};
var normalDelimiterSequence = [
{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}
];
var stackAlwaysDelimiterSequence = [
{type: "small", style: Style.SCRIPTSCRIPT},
{type: "small", style: Style.SCRIPT},
{type: "small", style: Style.TEXT},
{type: "stack"}
];
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"}
];
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";
}
};
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;
if (sequence[i].type === "small") {
heightDepth *= sequence[i].style.sizeMultiplier;
}
if (heightDepth > height) {
return sequence[i];
}
}
return sequence[sequence.length - 1];
};
var makeCustomSizedDelim = function(delim, height, center, options, mode) {
if (delim === "<") {
delim = "\\langle";
} else if (delim === ">") {
delim = "\\rangle";
}
var sequence;
if (utils.contains(onlyNormalDelimiters, delim)) {
sequence = normalDelimiterSequence;
} else if (utils.contains(normalDelimiters, delim)) {
sequence = stackLargeDelimiterSequence;
} else {
sequence = stackAlwaysDelimiterSequence;
}
var delimType = traverseSequence(delim, height, sequence, options);
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);
}
};
var makeLeftRightDelim = function(delim, height, depth, options, mode) {
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);
return makeCustomSizedDelim(delim, totalHeight, true, options, mode);
};
module.exports = {
sizedDelim: makeSizedDelim,
customSizedDelim: makeCustomSizedDelim,
leftRightDelim: makeLeftRightDelim
};