/**
* This module 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`.
* @summary Handling of delimiters surrounds symbols.
* @module delimiters
* @private
*/
define(['mathlive/core/definitions', 'mathlive/core/span', 'mathlive/core/mathstyle', 'mathlive/core/fontMetrics'],
function(Definitions, Span, Mathstyle, FontMetrics) {
const makeSymbol = Span.makeSymbol;
const makeStyleWrap = Span.makeStyleWrap;
const makeSpan = Span.makeSpan;
const makeVlist = Span.makeVlist;
/**
* 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.
* @memberof module:delimiters
* @private
*/
function makeSmallDelim(type, delim, style, center, context, classes) {
const text = makeSymbol(Definitions.getFontName('math', delim),
Definitions.getValue('math', delim));
const span = makeStyleWrap(type, text, context.mathstyle, style, classes);
if (center) {
span.setTop((1 - context.mathstyle.sizeMultiplier / style.sizeMultiplier) *
context.mathstyle.metrics.axisHeight);
}
span.setStyle('color', context.color);
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.
* @memberof module:delimiters
* @private
*/
function makeLargeDelim(type, delim, size, center, context, classes) {
const inner = makeSymbol('Size' + size + '-Regular',
Definitions.getValue('math', delim));
const result = makeStyleWrap( type,
makeSpan(inner, 'delimsizing size' + size),
context.mathstyle, Mathstyle.TEXT, classes);
if (center) {
result.setTop((1 - context.mathstyle.sizeMultiplier) *
context.mathstyle.metrics.axisHeight);
}
result.setStyle('color', context.color);
return result;
}
/**
* 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.
* @memberof module:delimiters
* @private
*/
function makeInner(symbol, font) {
let 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';
}
// @todo: revisit if all this wrapping is needed or if the spans could
// be simplified
const inner = makeSpan(makeSymbol(font,
Definitions.getValue('math', symbol)), 'delimsizinginner' + sizeClass);
return 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.
* @memberof module:delimiters
* @private
*/
function makeStackedDelim(type, delim, heightTotal, center, context,
classes) {
// There are four parts, the top, an optional middle, a repeated part, and a
// bottom.
let top;
let middle;
let repeat;
let bottom;
top = repeat = bottom = delim;
middle = null;
// Also keep track of what font the delimiters are in
let 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 === '\\vert' || delim === '\\lvert' || delim === '\\rvert' || delim === '\\mvert' || delim === '\\mid') {
repeat = top = bottom = '\u2223';
} else if (delim === '\\Vert' || delim === '\\lVert' ||
delim === '\\rVert' || delim === '\\mVert' || delim === '\\|') {
repeat = top = bottom = '\u2225';
} else 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 = '\u2191';
repeat = '\u23d0';
bottom = '\u2193';
} else if (delim === '\\Updownarrow') {
top = '\u21d1';
repeat = '\u2016';
bottom = '\u21d3';
} 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 === '\\lgroup') {
top = '\u23a7';
bottom = '\u23a9';
repeat = '\u23aa';
font = 'Size4-Regular';
} else if (delim === '\\rgroup') {
top = '\u23ab';
bottom = '\u23ad';
repeat = '\u23aa';
font = 'Size4-Regular';
} else if (delim === '\\lmoustache') {
top = '\u23a7';
bottom = '\u23ad';
repeat = '\u23aa';
font = 'Size4-Regular';
} else if (delim === '\\rmoustache') {
top = '\u23ab';
bottom = '\u23a9';
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
const topMetrics = FontMetrics.getCharacterMetrics(
Definitions.getValue('math', top), font);
const topHeightTotal = topMetrics.height + topMetrics.depth;
const repeatMetrics = FontMetrics.getCharacterMetrics(
Definitions.getValue('math', repeat), font);
const repeatHeightTotal = repeatMetrics.height + repeatMetrics.depth;
const bottomMetrics = FontMetrics.getCharacterMetrics(
Definitions.getValue('math', bottom), font);
const bottomHeightTotal = bottomMetrics.height + bottomMetrics.depth;
let middleHeightTotal = 0;
let middleFactor = 1;
if (middle !== null) {
const middleMetrics = FontMetrics.getCharacterMetrics(
Definitions.getValue('math', middle), font);
middleHeightTotal = middleMetrics.height + middleMetrics.depth;
middleFactor = 2; // repeat symmetrically above and below middle
}
// Calculate the minimal height that the delimiter can have.
// It is at least the size of the top, bottom, and optional middle combined.
const minHeight = topHeightTotal + bottomHeightTotal + middleHeightTotal;
// Compute the number of copies of the repeat symbol we will need
const repeatCount = Math.ceil(
(heightTotal - minHeight) / (middleFactor * repeatHeightTotal));
// Compute the total height of the delimiter including all the symbols
const realHeightTotal =
minHeight + repeatCount * middleFactor * 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.
let axisHeight = context.mathstyle.metrics.axisHeight;
if (center) {
axisHeight *= context.mathstyle.sizeMultiplier;
}
// Calculate the depth
const depth = realHeightTotal / 2 - axisHeight;
// Now, we start building the pieces that will go into the vlist
// Keep a list of the inner pieces
const inners = [];
// Add the bottom symbol
inners.push(makeInner(bottom, font));
if (middle === null) {
// Add that many symbols
for (let i = 0; i < repeatCount; i++) {
inners.push(makeInner(repeat, font));
}
} else {
// When there is a middle bit, we need the middle part and two repeated
// sections
for (let i = 0; i < repeatCount; i++) {
inners.push(makeInner(repeat, font));
}
inners.push(makeInner(middle, font));
for (let i = 0; i < repeatCount; i++) {
inners.push(makeInner(repeat, font));
}
}
// Add the top symbol
inners.push(makeInner(top, font));
// Finally, build the vlist
const inner = makeVlist(context, inners, 'bottom', depth);
inner.setStyle('color', context.color);
return makeStyleWrap(type, makeSpan(inner, 'delimsizing mult'),
context.mathstyle, Mathstyle.TEXT, classes);
}
// There are three kinds of delimiters, delimiters that stack when they become
// too large
const stackLargeDelimiters = [
'(', ')', '[', '\\lbrack', ']', '\\rbrack',
'\\{', '\\lbrace', '\\}', '\\rbrace',
'\\lfloor', '\\rfloor', '\\lceil', '\\rceil',
'\\surd',
];
// delimiters that always stack
const stackAlwaysDelimiters = [
'\\uparrow', '\\downarrow', '\\updownarrow',
'\\Uparrow', '\\Downarrow', '\\Updownarrow',
'|', '\\|', '\\vert', '\\Vert',
'\\lvert', '\\rvert', '\\lVert', '\\rVert',
'\\mvert', '\\mid',
'\\lgroup', '\\rgroup', '\\lmoustache', '\\rmoustache',
];
// and delimiters that never stack
const stackNeverDelimiters = [
'<', '>', '\\langle', '\\rangle', '/', '\\backslash', '\\lt', '\\gt',
];
// 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.
const 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.
* @memberof module:delimiters
* @private
*/
function makeSizedDelim(type, delim, size, context, classes) {
if (delim === '.') {
// Empty delimiters still count as elements, even though they don't
// show anything.
return makeNullFence(type, context, classes);
// return makeSpan('', classes);
}
// < and > turn into \langle and \rangle in delimiters
if (delim === '<' || delim === '\\lt') {
delim = '\\langle';
} else if (delim === '>' || delim === '\\gt') {
delim = '\\rangle';
}
// Sized delimiters are never centered.
if (stackLargeDelimiters.includes(delim) ||
stackNeverDelimiters.includes(delim)) {
return makeLargeDelim(type, delim, size, false, context, classes);
} else if (stackAlwaysDelimiters.includes(delim)) {
return makeStackedDelim(
type, delim, sizeToMaxHeight[size], false, context, classes);
}
console.assert(false, 'Unknown delimiter \'' + delim + '\'');
return null;
}
/**
* 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
const stackNeverDelimiterSequence = [
{type: 'small', mathstyle: Mathstyle.SCRIPTSCRIPT},
{type: 'small', mathstyle: Mathstyle.SCRIPT},
{type: 'small', mathstyle: Mathstyle.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
const stackAlwaysDelimiterSequence = [
{type: 'small', mathstyle: Mathstyle.SCRIPTSCRIPT},
{type: 'small', mathstyle: Mathstyle.SCRIPT},
{type: 'small', mathstyle: Mathstyle.TEXT},
{type: 'stack'},
];
// Delimiters that stack when large try the small and then large delimiters, and
// stack afterwards
const stackLargeDelimiterSequence = [
{type: 'small', mathstyle: Mathstyle.SCRIPTSCRIPT},
{type: 'small', mathstyle: Mathstyle.SCRIPT},
{type: 'small', mathstyle: Mathstyle.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.
*/
function delimTypeToFont(type) {
if (type.type === 'small') {
return 'Main-Regular';
} else if (type.type === 'large') {
return 'Size' + type.size + '-Regular';
}
console.assert(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.
* @param {string} delim: a character value (not a command)
* @memberof module:delimiters
* @private
*/
function traverseSequence(delim, height, sequence, context) {
// 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
const start = Math.min(2, 3 - context.mathstyle.size);
for (let 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;
}
const metrics = FontMetrics.getCharacterMetrics(
delim,
delimTypeToFont(sequence[i]));
if (!metrics) {
// If we don't have metrics info for this character,
// assume we'll construct as a small delimiter
return {type: 'small', style: Mathstyle.SCRIPT};
}
let 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].mathstyle.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.
*
* @param {string} type 'mopen' or 'mclose'
* @param {string} delim
* @param {number} height
* @param {boolean} center
* @param {Context.Context} context
* @param {string[]} classes
* @memberof module:delimiters
* @private
*/
function makeCustomSizedDelim(type, delim, height, center, context, classes) {
if (!delim || delim.length === 0 || delim === '.') {
return makeNullFence(type, context, type);
}
if (delim === '<' || delim === '\\lt') {
delim = '\\langle';
} else if (delim === '>' || delim === '\\gt') {
delim = '\\rangle';
}
// Decide what sequence to use
let sequence;
if (stackNeverDelimiters.includes(delim)) {
sequence = stackNeverDelimiterSequence;
} else if (stackLargeDelimiters.includes(delim)) {
sequence = stackLargeDelimiterSequence;
} else {
sequence = stackAlwaysDelimiterSequence;
}
// Look through the sequence
const delimType = traverseSequence(Definitions.getValue('math', delim),
height, sequence, context);
// Depending on the sequence element we decided on, call the appropriate
// function.
if (delimType.type === 'small') {
return makeSmallDelim(type, delim, delimType.mathstyle, center, context,
classes);
} else if (delimType.type === 'large') {
return makeLargeDelim(type, delim, delimType.size, center, context,
classes);
}
console.assert(delimType.type === 'stack');
return makeStackedDelim(type, delim, height, center, context, classes);
}
/**
* Make a delimiter for use with `\left` and `\right`, given a height and depth
* of an expression that the delimiters surround.
* See tex.web:14994
* @memberof module:delimiters
* @private
*/
function makeLeftRightDelim(type, delim, height, depth, context, classes) {
// If this is the empty delimiter, return a null fence
if (delim === '.') {
return makeNullFence(type, context, classes);
}
// We always center \left/\right delimiters, so the axis is always shifted
const axisHeight =
context.mathstyle.metrics.axisHeight * context.mathstyle.sizeMultiplier;
// Taken from TeX source, tex.web, function make_left_right
const delimiterFactor = 901; // plain.tex:327
const delimiterShortfall = 5.0 / FontMetrics.metrics.ptPerEm; // plain.tex:345
let delta2 = depth + axisHeight;
let delta1 = height - axisHeight;
delta1 = Math.max(delta2, delta1);
let delta = (delta1 * delimiterFactor) / 500;
delta2 = 2 * delta1 - delimiterShortfall;
delta = Math.max(delta, delta2);
// const maxDistFromAxis = Math.max(height - axisHeight, depth + axisHeight);
// const 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 - delimiterShortfall);
// Finally, we defer to `makeCustomSizedDelim` with our calculated total
// height
return makeCustomSizedDelim(type, delim, delta, true, context, classes);
}
/**
*
* @param {*} context
* @param {string} [type] either 'mopen', 'mclose' or null
* @memberof module:delimiters
* @private
*/
function makeNullFence(type, context, classes) {
return Span.makeSpanOfType(type, '',
'sizing' + // @todo not useful, redundant with 'nulldelimiter'
// 'reset-' + context.size, 'size5', // @todo: that seems like a lot of resizing... do we need both?
context.mathstyle.adjustTo(Mathstyle.TEXT) +
' nulldelimiter ' // The null delimiter has a width, specified by class 'nulldelimiter'
+ (classes || '')
);
}
// Export the public interface for this module
return {
makeSizedDelim,
makeCustomSizedDelim,
makeLeftRightDelim
}
})