UNPKG

mathlive

Version:

Render and edit beautifully typeset math

1,166 lines (1,100 loc) 75.9 kB
/** * * See also the class {@linkcode MathAtom} * @module core/mathatom * @private */ import Mathstyle from './mathstyle.js'; import Context from './context.js'; import {METRICS as FONTMETRICS} from './fontMetrics.js'; import Span from './span.js'; import Delimiters from './delimiters.js'; const makeSpan = Span.makeSpan; const makeOrd = Span.makeOrd; const makeInner = Span.makeInner; const makeHlist = Span.makeHlist; const makeVlist = Span.makeVlist; export const GREEK_REGEX = /\u0393|\u0394|\u0398|\u039b|\u039E|\u03A0|\u03A3|\u03a5|\u03a6|\u03a8|\u03a9|[\u03b1-\u03c9]|\u03d1|\u03d5|\u03d6|\u03f1|\u03f5/; // TeX by default auto-italicize latin letters and lowercase greek letters const AUTO_ITALIC_REGEX = /^([A-Za-z]|[\u03b1-\u03c9]|\u03d1|\u03d5|\u03d6|\u03f1|\u03f5)$/; // A table of size -> font size for the different sizing functions const SIZING_MULTIPLIER = { size1: 0.5, size2: 0.7, size3: 0.8, size4: 0.9, size5: 1.0, size6: 1.2, size7: 1.44, size8: 1.73, size9: 2.07, size10: 2.49, }; /** * An atom is an object encapsulating an elementary mathematical unit, * independent of its graphical representation. * * It keeps track of the content, while the dimensions, position and style * are tracked by Span objects which are created by the `decompose()` functions. * * @param {string} mode * @param {string} type * @param {string|MathAtom[]} body * @param {Object.<string, any>} [style={}] A set of additional properties to append to * the atom * @return {MathAtom} * @property {string} mode `'display'`, `'command'`, etc... * @property {string} type - Type can be one of: * - `mord`: ordinary symbol, e.g. `x`, `\alpha` * - `textord`: ordinary characters * - `mop`: operators, including special functions, `\sin`, `\sum`, `\cap`. * - `mbin`: binary operator: `+`, `*`, etc... * - `mrel`: relational operator: `=`, `\ne`, etc... * - `mpunct`: punctuation: `,`, `:`, etc... * - `mopen`: opening fence: `(`, `\langle`, etc... * - `mclose`: closing fence: `)`, `\rangle`, etc... * - `minner`: special layout cases, overlap, `\left...\right` * * In addition to these basic types, which correspond to the TeX atom types, * some atoms represent more complex compounds, including: * - `space` and `spacing`: blank space between atoms * - `mathstyle`: to change the math style used: `display` or `text`. * The layout rules are different for each, the latter being more compact and * intended to be incorporated with surrounding non-math text. * - `root`: a group, which has no parent (only one per formula) * - `group`: a simple group of atoms, for example from a `{...}` * - `sizing`: set the size of the font used * - `rule`: draw a line, for the `\rule` command * - `line`: used by `\overline` and `\underline` commands * - `box`: a border drawn around an expression and change its background color * - `overlap`: display a symbol _over_ another * - `overunder`: displays an annotation above or below a symbol * - `array`: a group, which has children arranged in rows. Used * by environments such as `matrix`, `cases`, etc... * - `genfrac`: a generalized fraction: a numerator and denominator, separated * by an optional line, and surrounded by optional fences * - `surd`: a surd, aka root * - `leftright`: used by the `\left` and `\right` commands * - `delim`: some delimiter * - `sizeddelim`: a delimiter that can grow * * The following types are used by the editor: * - `command` indicate a command being entered. The text is displayed in * blue in the editor. * - `error`: indicate a command that is unknown, for example `\xyzy`. The text * is displayed with a wavy red underline in the editor. * - `placeholder`: indicate a temporary item. Placeholders are displayed * as a dashed square in the editor. * - `first`: a special, empty, atom put as the first atom in math lists in * order to be able to position the caret before the first element. Aside from * the caret, they display nothing. * * @property {string|MathAtom[]} body * @property {MathAtom[]} superscript * @property {MathAtom[]} subscript * @property {MathAtom[]} numer * @property {MathAtom[]} denom * * @property {boolean} captureSelection if true, this atom does not let its * children be selected. Used by the `\enclose` annotations, for example. * * @property {boolean} skipBoundary if true, when the caret reaches the * first position in this element's body, it automatically moves to the * outside of the element. Conversely, when the caret reaches the position * right after this element, it automatically moves to the last position * inside this element. * * @class * @private */ class MathAtom { /** * * @param {string} mode * @param {string} type * @param {string|Array} body * @param {object} style */ constructor(mode, type, body, style) { this.mode = mode; this.type = type; this.body = body; // Append all the properties in extras to this // This can override the mode, type and body this.applyStyle(style); } getStyle() { return { color: this.phantom ? 'transparent' : this.color, backgroundColor: this.phantom ? 'transparent' : this.backgroundColor, fontFamily: this.baseFontFamily || this.fontFamily || this.autoFontFamily, fontShape: this.fontShape, fontSeries: this.fontSeries, fontSize: this.fontSize, cssId: this.cssId, cssClass: this.cssClass } } applyStyle(style) { // Always apply the style, even if null. This will also set the // autoFontFamily, which account for auto-italic. This code path // is used by \char. Object.assign(this, style); if (this.fontFamily === 'none') { this.fontFamily = ''; } if (this.fontShape === 'auto') { this.fontShape = ''; } if (this.fontSeries === 'auto') { this.fontSeries = ''; } if (this.color === 'none') { this.color = ''; } if (this.backgroundColor === 'none') { this.backgroundColor = ''; } if (this.fontSize === 'auto') { this.fontSize = ''; } if (this.fontSize) { this.maxFontSize = SIZING_MULTIPLIER[this.fontSize] ; } if (this.mode === 'math') { const symbol = typeof this.body === 'string' ? this.body : ''; this.autoFontFamily = 'cmr'; if (AUTO_ITALIC_REGEX.test(symbol)) { // Auto italicize alphabetic and lowercase greek symbols // in math mode (European style: American style would not // italicize greek letters, but it's TeX's default behavior) this.autoFontFamily = 'math'; } else if (/\\imath|\\jmath|\\pounds/.test(symbol)) { // Some characters do not exist in the Math font, // use Main italic instead this.autoFontFamily = 'mainit'; } else if (!GREEK_REGEX.test(symbol) && this.baseFontFamily === 'math') { this.autoFontFamily = 'cmr'; } } else if (this.mode === 'text') { // A root can be in text mode (root created when creating a representation // of the selection, for copy/paste for example) if (this.type !== 'root') this.type = ''; delete this.baseFontFamily; delete this.autoFontFamily; } } getInitialBaseElement() { let result = this; if (Array.isArray(this.body) && this.body.length > 0) { if (this.body[0].type !== 'first') { result = this.body[0].getInitialBaseElement(); } else if (this.body[1]) { result = this.body[1].getInitialBaseElement(); } } return result; } getFinalBaseElement() { if (Array.isArray(this.body) && this.body.length > 0) { return this.body[this.body.length - 1].getFinalBaseElement(); } return this; } isCharacterBox() { const base = this.getInitialBaseElement(); return /minner|mbin|mrel|mpunct|mopen|mclose|textord/.test(base.type); } forEach(cb) { cb(this); if (Array.isArray(this.body)) { for (const atom of this.body) if (atom) atom.forEach(cb); } else if (this.body && typeof this.body === 'object') { // Note: body can be null, for example 'first' or 'rule' // (and null is an object) cb(this.body); } if (this.superscript) { for (const atom of this.superscript) if (atom) atom.forEach(cb); } if (this.subscript) { for (const atom of this.subscript) if (atom) atom.forEach(cb); } if (this.overscript) { for (const atom of this.overscript) if (atom) atom.forEach(cb); } if (this.underscript) { for (const atom of this.underscript) if (atom) atom.forEach(cb); } if (this.numer) { for (const atom of this.numer) if (atom) atom.forEach(cb); } if (this.denom) { for (const atom of this.denom) if (atom) atom.forEach(cb); } if (this.index) { for (const atom of this.index) if (atom) atom.forEach(cb); } if (this.array) { for (const row of this.array) { for (const cell of row) { for (const atom of cell) atom.forEach(cb); } } } } /** * Iterate over all the child atoms of this atom, this included, * and return an array of all the atoms for which the predicate callback * is true. * * @return {MathAtom[]} * @method MathAtom#filter * @private */ filter(cb) { let result = []; if (cb(this)) result.push(this); for (const relation of ['body', 'superscript', 'subscript', 'overscript', 'underscript', 'numer', 'denom', 'index']) { if (Array.isArray(this[relation])) { for (const atom of this[relation]) { if (atom) result = result.concat(atom.filter(cb)); } } } if (Array.isArray(this.array)) { for (const row of this.array) { for (const cell of row) { if (cell) result = result.concat(cell.filter(cb)); } } } return result; } decomposeGroup(context) { // The scope of the context is this group, so clone it // so that any changes to it will be discarded when finished // with this group. // Note that the mathstyle property is optional and could be undefined // If that's the case, clone() returns a clone of the // context with the same mathstyle. const localContext = context.clone({mathstyle: this.mathstyle}); const span = makeOrd(decompose(localContext, this.body)); if (this.cssId) span.cssId = this.cssId; span.applyStyle({ backgroundColor: this.backgroundColor, cssClass: this.cssClass }); return span; } decomposeArray(context) { // See http://tug.ctan.org/macros/latex/base/ltfsstrc.dtx // and http://tug.ctan.org/macros/latex/base/lttab.dtx let colFormat = this.colFormat; if (colFormat && colFormat.length === 0) { colFormat = [{ align: 'l' }]; } if (!colFormat) { colFormat = [{ align: 'l' }, { align: 'l' }, { align: 'l' }, { align: 'l' }, { align: 'l' }, { align: 'l' }, { align: 'l' }, { align: 'l' }, { align: 'l' }, { align: 'l' }]; } // Fold the array so that there are no more columns of content than // there are columns prescribed by the column format. const array = []; let colMax = 0; // Maximum number of columns of content for (const colSpec of colFormat) { if (colSpec.align) colMax++; } for (const row of this.array) { let colIndex = 0; while (colIndex < row.length) { const newRow = []; const lastCol = Math.min(row.length, colIndex + colMax); while (colIndex < lastCol) { newRow.push(row[colIndex++]); } array.push(newRow); } } // If the last row is empty, ignore it. if (array[array.length - 1].length === 1 && array[array.length - 1][0].length === 0) { array.pop(); } const mathstyle = Mathstyle.toMathstyle(this.mathstyle) || context.mathstyle; // Row spacing // Default \arraystretch from lttab.dtx const arraystretch = this.arraystretch || 1; const arrayskip = arraystretch * FONTMETRICS.baselineskip; const arstrutHeight = 0.7 * arrayskip; const arstrutDepth = 0.3 * arrayskip; // \@arstrutbox in lttab.dtx let totalHeight = 0; let nc = 0; const body = []; const nr = array.length; for (let r = 0; r < nr; ++r) { const inrow = array[r]; nc = Math.max(nc, inrow.length); let height = arstrutHeight; // \@array adds an \@arstrut let depth = arstrutDepth; // to each row (via the template) const outrow = []; for (let c = 0; c < inrow.length; ++c) { const localContext = context.clone({mathstyle: this.mathstyle}); const cell = decompose(localContext, inrow[c]) || []; const elt = [makeOrd(null)].concat(cell); depth = Math.max(depth, Span.depth(elt)); height = Math.max(height, Span.height(elt)); outrow.push(elt); } let jot = r === nr - 1 ? 0 : (this.jot || 0); if (this.rowGaps && this.rowGaps[r]) { jot = this.rowGaps[r]; if (jot > 0) { // \@argarraycr jot += arstrutDepth; if (depth < jot) { depth = jot; // \@xargarraycr } jot = 0; } } outrow.height = height; outrow.depth = depth; totalHeight += height; outrow.pos = totalHeight; totalHeight += depth + jot; // \@yargarraycr body.push(outrow); } const offset = totalHeight / 2 + mathstyle.metrics.axisHeight; const contentCols = []; for (let colIndex = 0; colIndex < nc; colIndex++) { const col = []; for (const row of body) { const elem = row[colIndex]; if (!elem) { continue; } elem.depth = row.depth; elem.height = row.height; col.push(elem); col.push(row.pos - offset); } if (col.length > 0) { contentCols.push(makeVlist(context, col, 'individualShift')); } } // Iterate over each column description. // Each `colDesc` will indicate whether to insert a gap, a rule or // a column from 'contentCols' const cols = []; let prevColContent = false; let prevColRule = false; let currentContentCol = 0; let firstColumn = !this.lFence; for (const colDesc of colFormat) { if (colDesc.align && currentContentCol >= contentCols.length) { break; } else if (colDesc.align && currentContentCol < contentCols.length) { // If an alignment is specified, insert a column of content if (prevColContent) { // If no gap was provided, insert a default gap between // consecutive columns of content cols.push(makeColGap(2 * FONTMETRICS.arraycolsep)); } else if (prevColRule || firstColumn) { // If the previous column was a rule or this is the first column // add a smaller gap cols.push(makeColGap(FONTMETRICS.arraycolsep)); } cols.push(makeSpan(contentCols[currentContentCol], 'col-align-' + colDesc.align)); currentContentCol++; prevColContent = true; prevColRule = false; firstColumn = false; } else if (typeof colDesc.gap !== 'undefined') { // Something to insert in between columns of content if (typeof colDesc.gap === 'number') { // It's a number, indicating how much space, in em, // to leave in between columns cols.push(makeColGap(colDesc.gap)); } else { // It's a mathlist // Create a column made up of the mathlist // as many times as there are rows. cols.push(makeColOfRepeatingElements(context, body, offset, colDesc.gap)); } prevColContent = false; prevColRule = false; firstColumn = false; } else if (colDesc.rule) { // It's a rule. const separator = makeSpan(null, 'vertical-separator'); separator.setStyle('height', totalHeight, 'em'); // result.setTop((1 - context.mathstyle.sizeMultiplier) * // context.mathstyle.metrics.axisHeight); separator.setStyle('margin-top', 3 * context.mathstyle.metrics.axisHeight - offset, 'em'); separator.setStyle('vertical-align', 'top'); // separator.setStyle('display', 'inline-block'); let gap = 0; if (prevColRule) { gap = FONTMETRICS.doubleRuleSep - FONTMETRICS.arrayrulewidth; } else if (prevColContent) { gap = FONTMETRICS.arraycolsep - FONTMETRICS.arrayrulewidth; } separator.setLeft(gap, 'em'); cols.push(separator); prevColContent = false; prevColRule = true; firstColumn = false; } } if (prevColContent && !this.rFence) { // If the last column was content, add a small gap cols.push(makeColGap(FONTMETRICS.arraycolsep)); } if ((!this.lFence || this.lFence === '.') && (!this.rFence || this.rFence === '.')) { // There are no delimiters around the array, just return what // we've built so far. return makeOrd(cols, 'mtable'); } // There is at least one delimiter. Wrap the core of the array with // appropriate left and right delimiters // const inner = makeSpan(makeSpan(cols, 'mtable'), 'mord'); const inner = makeSpan(cols, 'mtable'); const innerHeight = Span.height(inner); const innerDepth = Span.depth(inner); return makeOrd([ this.bind(context, Delimiters.makeLeftRightDelim('mopen', this.lFence, innerHeight, innerDepth, context)), inner, this.bind(context, Delimiters.makeLeftRightDelim('mclose', this.rFence, innerHeight, innerDepth, context)) ]); } /** * Gengrac -- Generalized fraction * * Decompose fractions, binomials, and in general anything made * of two expressions on top of each other, optionally separated by a bar, * and optionally surrounded by fences (parentheses, brackets, etc...) * * Depending on the type of fraction the mathstyle is either * display math or inline math (which is indicated by 'textstyle'). This value can * also be set to 'auto', which indicates it should use the current mathstyle * * @method MathAtom#decomposeGenfrac * @private */ decomposeGenfrac(context) { const mathstyle = this.mathstyle === 'auto' ? context.mathstyle : Mathstyle.toMathstyle(this.mathstyle); const newContext = context.clone({mathstyle: mathstyle}); let numer = []; if (this.numerPrefix) { numer.push(makeOrd(this.numerPrefix)); } const numeratorStyle = this.continuousFraction ? mathstyle : mathstyle.fracNum(); numer = numer.concat(decompose(newContext.clone({mathstyle: numeratorStyle}), this.numer)); const numerReset = makeHlist(numer, context.mathstyle.adjustTo(numeratorStyle)); let denom = []; if (this.denomPrefix) { denom.push(makeOrd(this.denomPrefix)); } const denominatorStyle = this.continuousFraction ? mathstyle : mathstyle.fracDen(); denom = denom.concat(decompose(newContext.clone({mathstyle: denominatorStyle}), this.denom)); const denomReset = makeHlist(denom, context.mathstyle.adjustTo(denominatorStyle)); const ruleWidth = !this.hasBarLine ? 0 : FONTMETRICS.defaultRuleThickness / mathstyle.sizeMultiplier; // Rule 15b from Appendix G let numShift; let clearance; let denomShift; if (mathstyle.size === Mathstyle.DISPLAY.size) { numShift = mathstyle.metrics.num1; if (ruleWidth > 0) { clearance = 3 * ruleWidth; } else { clearance = 7 * FONTMETRICS.defaultRuleThickness; } denomShift = mathstyle.metrics.denom1; } else { if (ruleWidth > 0) { numShift = mathstyle.metrics.num2; clearance = ruleWidth; } else { numShift = mathstyle.metrics.num3; clearance = 3 * FONTMETRICS.defaultRuleThickness; } denomShift = mathstyle.metrics.denom2; } const numerDepth = numerReset ? numerReset.depth : 0; const denomHeight = denomReset ? denomReset.height : 0; let frac; if (ruleWidth === 0) { // Rule 15c from Appendix G // No bar line between numerator and denominator const candidateClearance = (numShift - numerDepth) - (denomHeight - denomShift); if (candidateClearance < clearance) { numShift += 0.5 * (clearance - candidateClearance); denomShift += 0.5 * (clearance - candidateClearance); } frac = makeVlist(newContext, [numerReset, -numShift, denomReset, denomShift], 'individualShift'); } else { // Rule 15d from Appendix G // There is a bar line between the numerator and the denominator const axisHeight = mathstyle.metrics.axisHeight; if ((numShift - numerDepth) - (axisHeight + 0.5 * ruleWidth) < clearance) { numShift += clearance - ((numShift - numerDepth) - (axisHeight + 0.5 * ruleWidth)); } if ((axisHeight - 0.5 * ruleWidth) - (denomHeight - denomShift) < clearance) { denomShift += clearance - ((axisHeight - 0.5 * ruleWidth) - (denomHeight - denomShift)); } const mid = makeSpan(null, /* newContext.mathstyle.adjustTo(Mathstyle.TEXT) + */ ' frac-line'); mid.applyStyle(this.getStyle()); // @todo: do we really need to reset the size? // Manually set the height of the line because its height is // created in CSS mid.height = ruleWidth; const elements = []; if (numerReset) { elements.push(numerReset); elements.push(-numShift); } elements.push(mid); elements.push(ruleWidth / 2 - axisHeight); if (denomReset) { elements.push(denomReset); elements.push(denomShift); } frac = makeVlist(newContext, elements, 'individualShift'); } // Add a 'mfrac' class to provide proper context for // other css selectors (such as 'frac-line') frac.classes += ' mfrac'; // Since we manually change the style sometimes (with \dfrac or \tfrac), // account for the possible size change here. frac.height *= mathstyle.sizeMultiplier / context.mathstyle.sizeMultiplier; frac.depth *= mathstyle.sizeMultiplier / context.mathstyle.sizeMultiplier; // if (!this.leftDelim && !this.rightDelim) { // return makeOrd(frac, // context.parentMathstyle.adjustTo(mathstyle) + // ((context.parentSize !== context.size) ? // (' sizing reset-' + context.parentSize + ' ' + context.size) : '')); // } // Rule 15e of Appendix G const delimSize = mathstyle.size === Mathstyle.DISPLAY.size ? mathstyle.metrics.delim1 : mathstyle.metrics.delim2; // Optional delimiters const leftDelim = Delimiters.makeCustomSizedDelim('mopen', this.leftDelim, delimSize, true, context.clone({mathstyle: mathstyle}) ); const rightDelim = Delimiters.makeCustomSizedDelim('mclose', this.rightDelim, delimSize, true, context.clone({mathstyle: mathstyle}) ); leftDelim.applyStyle(this.getStyle()); rightDelim.applyStyle(this.getStyle()); const result = makeOrd([leftDelim, frac, rightDelim], ((context.parentSize !== context.size) ? ('sizing reset-' + context.parentSize + ' ' + context.size) : '')); return this.bind(context, result); } /** * \left....\right * * Note that we can encounter malformed \left...\right, for example * a \left without a matching \right or vice versa. In that case, the * leftDelim (resp. rightDelim) will be undefined. We still need to handle * those cases. * * @method MathAtom#decomposeLeftright * @private */ decomposeLeftright(context) { if (!this.body) { // No body, only a delimiter if (this.leftDelim) { return new MathAtom('math', 'mopen', this.leftDelim).decompose(context); } if (this.rightDelim) { return new MathAtom('math', 'mclose', this.rightDelim).decompose(context); } return null; } // The scope of the context is this group, so make a copy of it // so that any changes to it will be discarded when finished // with this group. const localContext = context.clone(); const inner = decompose(localContext, this.body); const mathstyle = localContext.mathstyle; let innerHeight = 0; let innerDepth = 0; let result = []; // Calculate its height and depth // The size of delimiters is the same, regardless of what mathstyle we are // in. Thus, to correctly calculate the size of delimiter we need around // a group, we scale down the inner size based on the size. innerHeight = Span.height(inner) * mathstyle.sizeMultiplier; innerDepth = Span.depth(inner) * mathstyle.sizeMultiplier; // Add the left delimiter to the beginning of the expression if (this.leftDelim) { result.push(Delimiters.makeLeftRightDelim( 'mopen', this.leftDelim, innerHeight, innerDepth, localContext )); result[result.length - 1].applyStyle(this.getStyle()); } if (inner) { // Replace the delim (\middle) spans with proper ones now that we know // the height/depth for (let i = 0; i < inner.length; i++) { if (inner[i].delim) { const savedCaret = inner[i].caret; const savedSelected = /ML__selected/.test(inner[i].classes); inner[i] = Delimiters.makeLeftRightDelim('minner', inner[i].delim, innerHeight, innerDepth, localContext); inner[i].caret = savedCaret; inner[i].selected(savedSelected); } } result = result.concat(inner); } // Add the right delimiter to the end of the expression. if (this.rightDelim) { let delim = this.rightDelim; let classes; if (delim === '?') { // Use a placeholder delimiter matching the open delimiter delim = { '(': ')', '\\{': '\\}', '\\[': '\\]', '\\lbrace': '\\rbrace', '\\langle': '\\rangle', '\\lfloor': '\\rfloor', '\\lceil': '\\rceil', '\\vert': '\\vert', '\\lvert': '\\rvert', '\\Vert': '\\Vert', '\\lVert': '\\rVert', '\\lbrack': '\\rbrack', '\\ulcorner': '\\urcorner', '\\llcorner': '\\lrcorner', '\\lgroup': '\\rgroup', '\\lmoustache': '\\rmoustache' }[this.leftDelim]; delim = delim || this.leftDelim; classes = 'ML__smart-fence__close'; } result.push(Delimiters.makeLeftRightDelim('mclose', delim, innerHeight, innerDepth, localContext, classes )); result[result.length - 1].applyStyle(this.getStyle()); } // If the `inner` flag is set, return the `inner` element (that's the // behavior for the regular `\left...\right` if (this.inner) return makeInner(result, mathstyle.cls()); // Otherwise, include a `\mathopen{}...\mathclose{}`. That's the // behavior for `\mleft...\mright`, which allows for tighter spacing // for example in `\sin\mleft(x\mright)` return result; } decomposeSurd(context) { // See the TeXbook pg. 443, Rule 11. // http://www.ctex.org/documents/shredder/src/texbook.pdf const mathstyle = context.mathstyle; // First, we do the same steps as in overline to build the inner group // and line const inner = decompose(context.cramp(), this.body); const ruleWidth = FONTMETRICS.defaultRuleThickness / mathstyle.sizeMultiplier; let phi = ruleWidth; if (mathstyle.id < Mathstyle.TEXT.id) { phi = mathstyle.metrics.xHeight; } // Calculate the clearance between the body and line let lineClearance = ruleWidth + phi / 4; const innerTotalHeight = Math.max(2 * phi, (Span.height(inner) + Span.depth(inner)) * mathstyle.sizeMultiplier); const minDelimiterHeight = innerTotalHeight + (lineClearance + ruleWidth); // Create a \surd delimiter of the required minimum size const delim = makeSpan(Delimiters.makeCustomSizedDelim('', '\\surd', minDelimiterHeight, false, context), 'sqrt-sign'); delim.applyStyle(this.getStyle()); const delimDepth = (delim.height + delim.depth) - ruleWidth; // Adjust the clearance based on the delimiter size if (delimDepth > Span.height(inner) + Span.depth(inner) + lineClearance) { lineClearance = (lineClearance + delimDepth - (Span.height(inner) + Span.depth(inner))) / 2; } // Shift the delimiter so that its top lines up with the top of the line delim.setTop((delim.height - Span.height(inner)) - (lineClearance + ruleWidth)); const line = makeSpan(null, context.mathstyle.adjustTo(Mathstyle.TEXT) + ' sqrt-line'); line.applyStyle(this.getStyle()); line.height = ruleWidth; const body = makeVlist(context, [inner, lineClearance, line, ruleWidth]); if (!this.index) { return this.bind(context, makeOrd([delim, body], 'sqrt')); } // Handle the optional root index // The index is always in scriptscript style const newcontext = context.clone({mathstyle: Mathstyle.SCRIPTSCRIPT}); const root = makeSpan(decompose(newcontext, this.index), mathstyle.adjustTo(Mathstyle.SCRIPTSCRIPT)); // Figure out the height and depth of the inner part const innerRootHeight = Math.max(delim.height, body.height); const innerRootDepth = Math.max(delim.depth, body.depth); // The amount the index is shifted by. This is taken from the TeX // source, in the definition of `\r@@t`. const toShift = 0.6 * (innerRootHeight - innerRootDepth); // Build a VList with the superscript shifted up correctly const rootVlist = makeVlist(context, [root], 'shift', -toShift); // Add a class surrounding it so we can add on the appropriate // kerning return this.bind(context, makeOrd([makeSpan(rootVlist, 'root'), delim, body], 'sqrt')); } decomposeAccent(context) { // Accents are handled in the TeXbook pg. 443, rule 12. const mathstyle = context.mathstyle; // Build the base atom let base = decompose(context.cramp(), this.body); if (this.superscript || this.subscript) { // If there is a supsub attached to the accent // apply it to the base. // Note this does not give the same result as TeX when there // are stacked accents, e.g. \vec{\breve{\hat{\acute{...}}}}^2 base = this.attachSupsub(context, makeOrd(base), 'mord'); } // Calculate the skew of the accent. This is based on the line "If the // nucleus is not a single character, let s = 0; otherwise set s to the // kern amount for the nucleus followed by the \skewchar of its font." // Note that our skew metrics are just the kern between each character // and the skewchar. let skew = 0; if (Array.isArray(this.body) && this.body.length === 1 && this.body[0].isCharacterBox()) { skew = Span.skew(base); } // calculate the amount of space between the body and the accent const clearance = Math.min(Span.height(base), mathstyle.metrics.xHeight); // Build the accent const accent = Span.makeSymbol('Main-Regular', this.accent, 'math'); // Remove the italic correction of the accent, because it only serves to // shift the accent over to a place we don't want. accent.italic = 0; // The \vec character that the fonts use is a combining character, and // thus shows up much too far to the left. To account for this, we add a // specific class which shifts the accent over to where we want it. const vecClass = this.accent === '\u20d7' ? ' accent-vec' : ''; let accentBody = makeSpan(makeSpan(accent), 'accent-body' + vecClass); accentBody = makeVlist(context, [base, -clearance, accentBody]); // Shift the accent over by the skew. Note we shift by twice the skew // because we are centering the accent, so by adding 2*skew to the left, // we shift it to the right by 1*skew. accentBody.children[1].setLeft(2 * skew); return makeOrd(accentBody, 'accent'); } /** * \overline and \underline * * @method MathAtom#decomposeLine * @private */ decomposeLine(context) { const mathstyle = context.mathstyle; // TeXBook:443. Rule 9 and 10 const inner = decompose(context.cramp(), this.body); const ruleWidth = FONTMETRICS.defaultRuleThickness / mathstyle.sizeMultiplier; const line = makeSpan(null, context.mathstyle.adjustTo(Mathstyle.TEXT) + ' ' + this.position + '-line'); line.height = ruleWidth; line.maxFontSize = 1.0; let vlist; if (this.position === 'overline') { vlist = makeVlist(context, [inner, 3 * ruleWidth, line, ruleWidth]); } else { const innerSpan = makeSpan(inner); vlist = makeVlist(context, [ruleWidth, line, 3 * ruleWidth, innerSpan], 'top', Span.height(innerSpan)); } return makeOrd(vlist, this.position); } decomposeOverunder(context) { const base = decompose(context, this.body); const annotationStyle = context.clone({mathstyle: 'scriptstyle'}); const above = this.overscript ? makeSpan(decompose(annotationStyle, this.overscript), context.mathstyle.adjustTo(annotationStyle.mathstyle)) : null; const below = this.underscript ? makeSpan(decompose(annotationStyle, this.underscript), context.mathstyle.adjustTo(annotationStyle.mathstyle)) : null; return makeStack(context, base, 0, 0, above, below, this.mathtype || 'mrel'); } decomposeOverlap(context) { const inner = makeSpan(decompose(context, this.body), 'inner'); return makeOrd([inner, makeSpan(null, 'fix')], (this.align === 'left' ? 'llap' : 'rlap')); } /** * \rule * @memberof MathAtom * @instance * @private */ decomposeRule(context) { const mathstyle = context.mathstyle; const result = makeOrd('', 'rule'); let shift = this.shift && !isNaN(this.shift) ? this.shift : 0; shift = shift / mathstyle.sizeMultiplier; const width = (this.width) / mathstyle.sizeMultiplier; const height = (this.height) / mathstyle.sizeMultiplier; result.setStyle('border-right-width', width, 'em'); result.setStyle('border-top-width', height, 'em'); result.setStyle('margin-top', -(height - shift), 'em'); result.setStyle('border-color', context.color); result.width = width; result.height = height + shift; result.depth = -shift; return result; } decomposeOp(context) { // Operators are handled in the TeXbook pg. 443-444, rule 13(a). const mathstyle = context.mathstyle; let large = false; if (mathstyle.size === Mathstyle.DISPLAY.size && typeof this.body === 'string' && this.body !== '\\smallint') { // Most symbol operators get larger in displaystyle (rule 13) large = true; } let base; let baseShift = 0; let slant = 0; if (this.symbol) { // If this is a symbol, create the symbol. const fontName = large ? 'Size2-Regular' : 'Size1-Regular'; base = Span.makeSymbol(fontName, this.body, 'op-symbol ' + (large ? 'large-op' : 'small-op')); base.type = 'mop'; // Shift the symbol so its center lies on the axis (rule 13). It // appears that our fonts have the centers of the symbols already // almost on the axis, so these numbers are very small. Note we // don't actually apply this here, but instead it is used either in // the vlist creation or separately when there are no limits. baseShift = (base.height - base.depth) / 2 - mathstyle.metrics.axisHeight * mathstyle.sizeMultiplier; // The slant of the symbol is just its italic correction. slant = base.italic; // Bind the generated span and this atom so the atom can be retrieved // from the span later. this.bind(context, base); } else if (Array.isArray(this.body)) { // If this is a list, decompose that list. base = Span.makeOp(decompose(context, this.body)); // Bind the generated span and this atom so the atom can be retrieved // from the span later. this.bind(context, base); } else { // Otherwise, this is a text operator. Build the text from the // operator's name. console.assert(this.type === 'mop'); base = this.makeSpan(context, this.body); } if (this.superscript || this.subscript) { const limits = this.limits || 'auto'; if (this.alwaysHandleSupSub || limits === 'limits' || (limits === 'auto' && mathstyle.size === Mathstyle.DISPLAY.size)) { return this.attachLimits(context, base, baseShift, slant); } return this.attachSupsub(context, base, 'mop'); } if (this.symbol) base.setTop(baseShift); return base; } decomposeBox(context) { // Base is the main content "inside" the box const base = makeOrd(decompose(context, this.body)); // This span will represent the box (background and border) // It's positioned to overlap the base const box = makeSpan(); box.setStyle('position', 'absolute'); // The padding extends outside of the base const padding = typeof this.padding === 'number' ? this.padding : FONTMETRICS.fboxsep; box.setStyle('height', base.height + base.depth + 2 * padding, 'em'); if (padding !== 0) { box.setStyle('width', 'calc(100% + ' + (2 * padding) + 'em)'); } else { box.setStyle('width', '100%'); } box.setStyle('top', -padding, 'em'); box.setStyle('left', -padding, 'em'); box.setStyle('z-index', '-1'); // Ensure the box is *behind* the base if (this.backgroundcolor) box.setStyle('background-color', this.backgroundcolor); if (this.framecolor) box.setStyle('border', FONTMETRICS.fboxrule + 'em solid ' + this.framecolor); if (this.border) box.setStyle('border', this.border); base.setStyle('display', 'inline-block'); base.setStyle('height', base.height + base.depth, 'em'); base.setStyle('vertical-align', -base.depth + padding, 'em'); // The result is a span that encloses the box and the base const result = makeSpan([box, base]); // Set its position as relative so that the box can be absolute positioned // over the base result.setStyle('position', 'relative'); result.setStyle('vertical-align', -padding + base.depth, 'em'); // The padding adds to the width and height of the pod result.height = base.height + padding; result.depth = base.depth + padding; result.setLeft(padding); result.setRight(padding); return result; } decomposeEnclose(context) { const base = makeOrd(decompose(context, this.body)); const result = base; // Account for the padding const padding = this.padding === 'auto' ? .2 : this.padding; // em result.setStyle('padding', padding, 'em'); result.setStyle('display', 'inline-block'); result.setStyle('height', result.height + result.depth, 'em'); result.setStyle('left', -padding, 'em'); if (this.backgroundcolor && this.backgroundcolor !== 'transparent') { result.setStyle('background-color', this.backgroundcolor); } let svg = ''; if (this.notation.box) result.setStyle('border', this.borderStyle); if (this.notation.actuarial) { result.setStyle('border-top', this.borderStyle); result.setStyle('border-right', this.borderStyle); } if (this.notation.madruwb) { result.setStyle('border-bottom', this.borderStyle); result.setStyle('border-right', this.borderStyle); } if (this.notation.roundedbox) { result.setStyle('border-radius', (Span.height(result) + Span.depth(result)) / 2, 'em'); result.setStyle('border', this.borderStyle); } if (this.notation.circle) { result.setStyle('border-radius', '50%'); result.setStyle('border', this.borderStyle); } if (this.notation.top) result.setStyle('border-top', this.borderStyle); if (this.notation.left) result.setStyle('border-left', this.borderStyle); if (this.notation.right) result.setStyle('border-right', this.borderStyle); if (this.notation.bottom) result.setStyle('border-bottom', this.borderStyle); if (this.notation.horizontalstrike) { svg += '<line x1="3%" y1="50%" x2="97%" y2="50%"'; svg += ` stroke-width="${this.strokeWidth}" stroke="${this.strokeColor}"`; svg += ' stroke-linecap="round"'; if (this.svgStrokeStyle) { svg += ` stroke-dasharray="${this.svgStrokeStyle}"`; } svg += '/>'; } if (this.notation.verticalstrike) { svg += '<line x1="50%" y1="3%" x2="50%" y2="97%"'; svg += ` stroke-width="${this.strokeWidth}" stroke="${this.strokeColor}"`; svg += ' stroke-linecap="round"'; if (this.svgStrokeStyle) { svg += ` stroke-dasharray="${this.svgStrokeStyle}"`; } svg += '/>'; } if (this.notation.updiagonalstrike) { svg += '<line x1="3%" y1="97%" x2="97%" y2="3%"'; svg += ` stroke-width="${this.strokeWidth}" stroke="${this.strokeColor}"`; svg += ' stroke-linecap="round"'; if (this.svgStrokeStyle) { svg += ` stroke-dasharray="${this.svgStrokeStyle}"`; } svg += '/>'; } if (this.notation.downdiagonalstrike) { svg += '<line x1="3%" y1="3%" x2="97%" y2="97%"'; svg += ` stroke-width="${this.strokeWidth}" stroke="${this.strokeColor}"`; svg += ' stroke-linecap="round"'; if (this.svgStrokeStyle) { svg += ` stroke-dasharray="${this.svgStrokeStyle}"`; } svg += '/>'; } // if (this.notation.updiagonalarrow) { // const t = 1; // const length = Math.sqrt(w * w + h * h); // const f = 1 / length / 0.075 * t; // const wf = w * f; // const hf = h * f; // const x = w - t / 2; // let y = t / 2; // if (y + hf - .4 * wf < 0 ) y = 0.4 * wf - hf; // svg += '<line '; // svg += `x1="1" y1="${h - 1}px" x2="${x - .7 * wf}px" y2="${y + .7 * hf}px"`; // svg += ` stroke-width="${this.strokeWidth}" stroke="${this.strokeColor}"`; // svg += ' stroke-linecap="round"'; // if (this.svgStrokeStyle) { // svg += ` stroke-dasharray="${this.svgStrokeStyle}"`; // } // svg += '/>'; // svg += '<polygon points="'; // svg += `${x},${y} ${x - wf - .4 * hf},${y + hf - .4 * wf} `; // svg += `${x - .7 * wf},${y + .7 * hf} ${x - wf + .4 * hf},${y + hf + .4 * wf} `; // svg += `${x},${y}`; // svg += `" stroke='none' fill="${this.strokeColor}"`; // svg += '/>'; // } // if (this.notation.phasorangle) { // svg += '<path d="'; // svg += `M ${h / 2},1 L1,${h} L${w},${h} "`; // svg += ` stroke-width="${this.strokeWidth}" stroke="${this.strokeColor}" fill="none"`; // if (this.svgStrokeStyle) { // svg += ' stroke-linecap="round"'; // svg += ` stroke-dasharray="${this.svgStrokeStyle}"`; // } // svg += '/>'; // } // if (this.notation.radical) { // svg += '<path d="'; // svg += `M 0,${.6 * h} L1,${h} L${emToPx(padding) * 2},1 "`; // svg += ` stroke-width="${this.strokeWidth}" stroke="${this.strokeColor}" fill="none"`; // if (this.svgStrokeStyle) { // svg += ' stroke-linecap="round"'; // svg += ` stroke-dasharray="${this.svgStrokeStyle}"`; // } // svg += '/>'; // } // if (this.notation.longdiv) { // svg += '<path d="'; // svg += `M ${w} 1 L1 1 a${emToPx(padding)} ${h / 2}, 0, 0, 1, 1 ${h} "`; // svg += ` stroke-width="${this.strokeWidth}" stroke="${this.strokeColor}" fill="none"`; // if (this.svgStrokeStyle) { // svg += ' stroke-linecap="round"'; // svg += ` stroke-dasharray="${this.svgStrokeStyle}"`; // } // svg += '/>'; // } if (svg) { let svgStyle; if (this.shadow !== 'none') { if (this.shadow === 'auto') { svgStyle = 'filter: drop-shadow(0 0 .5px rgba(255, 255, 255, .7)) drop-shadow(1px 1px 2px #333)'; } else { svgStyle = 'filter: drop-shadow(' + this.shadow + ')'; } } return Span.makeSVG(result, svg, svgStyle); } return result; } /** * Return a representation of this, but decomposed in an array of Spans * * @param {Context} context Font variant, size, color, etc... * @param {Span[]} [phantomBase=null] If not null, the spans to use to * calculate the placement of the supsub * @return {Span[]} * @method MathAtom#decompose * @private */ decompose(context, phantomBase) { console.assert(context instanceof Context.Context); let result = null; if (!this.type || /mord|minner|mbin|mrel|mpunct|mopen|mclose|textord/.test