UNPKG

mathlive

Version:

Render and edit beautifully typeset math

1,199 lines (1,057 loc) 39.3 kB
/** * @module core/span * @private */ import FontMetrics from './fontMetrics.js'; /** * Return a string made up of the concatenated arguments. * Each arguments can be either a string, which is unchanged, * or a number, which is converted to a string with at most 5 fractional digits. * * @param {(Array.<any>|string|number)} arg * @return {string} * @memberof module:core/span * @private */ function toString(arg) { let result = ''; if (typeof arg === 'number') { result += Math.floor(1e2 * arg) / 1e2; } else if (typeof arg === 'string') { result += arg; } else if (Array.isArray(arg)) { for (const elem of arg) { result += toString(elem); } } else if (arg) { result += arg.toString(); } return result; } //---------------------------------------------------------------------------- // SPAN //---------------------------------------------------------------------------- /** * A span is the most elementary element that can be rendered. * It is composed of an optional body of text and an optional list * of children (other spans). Each span can be decorated with * CSS classes and style attributes. * * @param {string|Span|Span[]} content the items 'contained' by this node * @param {string} classes list of classes attributes associated with this node * @return {void} * @class * @memberof module:core/span * @property {string} type - For example, `'command'`, `'mrel'`, etc... * @property {string} classes - A string of space separated CSS classes * associated with this element * @property {string} cssID - A CSS ID assigned to this span (optional) * @property {Span[]} children - An array, potentially empty, of spans which * this span encloses * @property {string} body - Content of this span. Can be empty. * @property {Object.<string, any>} style - A set of key/value pairs specifying CSS properties * associated with this element. * @property {number} height - The measurement from baseline to top, in em. * @property {number} depth - The measurement from baseline to bottom, in em. * @private */ export class Span { constructor(content, classes) { // CLASSES this.classes = classes || ''; // CONTENT if (Array.isArray(content)) { // Check if isArray first, since an array is also an object // Flatten it (i.e. [[a1, a2], b1, b2] -> [a1, a2, b1, b2] this.children = [].concat.apply([], content); } else if (typeof content === 'string') { this.body = content; } else if (content && typeof content === 'object') { this.children = [content]; } // STYLE // CSS style, as an array of key value pairs. // Use this.setStyle() to modify it. this.style = null; // Calculate the dimensions of this span based on its children this.updateDimensions(); } /** * Update the dimensions of this node based on its children: * - height: distance from bottom to top * - depth: distance from bottom to baseline * - maxFontSize: a size multiplier (typically set with commands such as \huge) * @method module:core/span.Span#updateDimensions * @private */ updateDimensions() { let height = 0.0; let depth = 0.0; let maxFontSize = 1.0; if (this.children) { this.children.forEach(x => { if (x.height > height) height = x.height; if (x.depth > depth) depth = x.depth; if (x.maxFontSize > maxFontSize) maxFontSize = x.maxFontSize; }); } this.height = height; this.depth = depth; this.maxFontSize = maxFontSize; } selected(isSelected) { if (isSelected && !/ML__selected/.test(this.classes)) { if (this.classes.length > 0) this.classes += ' '; this.classes += 'ML__selected'; } if (!isSelected && /ML__selected/.test(this.classes)) { this.classes = this.classes.replace('ML__selected', ''); } if (this.children) { this.children.forEach(x => x.selected(isSelected)); } } /** * @param {object} style A style specification with the following * (all optionals) properties, which use the TeX terminology: * * - fontFamily: cmr, cmss, cmtt, cmsy (symbols), cmex (large symbols), * ptm (times), phv (helvetica), pcr (courier) * - fontSeries: m (medium), b (bold), bx (bold extended), sb (semi-bold), c (condensed) * - fontShape: italic, oblique, "roman": n (normal, upright), it, sl, sc * - fontSize: 'size1', 'size2'... * - color: * - background: * @private */ applyStyle(style) { if (!style) return; if (style.color) { if (style.color !== 'none') { this.setStyle('color', style.color); } else { this.setStyle('color', ''); } } if (style.backgroundColor) { if (style.backgroundColor !== 'none') { this.setStyle('background-color', style.backgroundColor); } else { this.setStyle('background-color', ''); } } // // 1. Add any custom style classes // if (style.cssClass) { this.classes += ' ' + style.cssClass; } // If the body is null (for example for a line), we're done. if (!this.body) return; // Determine the appropriate font (and font-related classes) // // 2. Determine the font family (i.e. 'amsrm', 'mathit', 'mathcal', etc...) // let fontFamily = style.fontFamily; if (fontFamily === 'math' && style.fontShape === 'n') { // 'math' is italic by default. If we need upright, switch to main. fontFamily = 'cmr'; } let fontName = 'Main-Regular'; // Default font if (fontFamily) { fontName = getFontName(this.body, fontFamily); } // // 3. Determine the classes necessary to represent the series and shape // if (style.fontShape) { this.classes += ' ' + ({ 'it': 'ML__it', 'sl': 'ML__shape_sl', // slanted 'sc': 'ML__shape_sc', // small caps 'ol': 'ML__shape_ol' // outline }[style.fontShape] || ''); } if (style.fontSeries) { const m = style.fontSeries.match(/(.?[lbm])?(.?[cx])?/); if (m) { this.classes += ' ' + ({ 'ul': 'ML__series_ul', 'el': 'ML__series_el', 'l': 'ML__series_l', 'sl': 'ML__series_sl', 'm': '', // medium (default) 'sb': 'ML__series_sb', 'b': 'ML__bold', 'eb': 'ML__series_eb', 'ub': 'ML__series_ub', }[m[1] || ''] || ''); this.classes += ' ' + ({ 'uc': 'ML__series_uc', 'ec': 'ML__series_ec', 'c': 'ML__series_c', 'sc': 'ML__series_sc', 'n': '', // normal (default) 'sx': 'ML__series_sx', 'x': 'ML__series_x', 'ex': 'ML__series_ex', 'ux': 'ML__series_ux', }[m[2] || ''] || ''); } } if (FONT_CLASS[fontFamily]) { this.classes += ' ' + FONT_CLASS[fontFamily]; } else if (fontFamily) { // Not a well-known family. Use a style. this.setStyle('font-family', fontFamily); } // // 3. Get the metrics information // if (this.body && this.body.length > 0 && fontName) { this.height = 0.0; this.depth = 0.0; this.maxFontSize = { 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, }[style.fontSize] || 1.0; this.skew = 0.0; this.italic = 0.0; for (let i = 0; i < this.body.length; i++) { const metrics = FontMetrics.getCharacterMetrics(this.body.charAt(i), fontName); // If we were able to get metrics info for this character, store it. if (metrics) { this.height = Math.max(this.height, metrics.height); this.depth = Math.max(this.depth, metrics.depth); this.skew = metrics.skew; this.italic = metrics.italic; } } } } /** * Set the value of a CSS property associated with this span. * For example, setStyle('border-right', 5.6, 'em'); * * @param {string} prop the CSS property to set * @param {...(string|number)} value a series of strings and numbers that will be concatenated. * @return {string} * @method module:core/span.Span#setStyle * @private */ setStyle(prop, ...value) { const v = toString(value); if (v.length > 0) { if (!this.style) this.style = {}; this.style[prop] = v; } } setTop(top) { if (top && top !== 0) { if (!this.style) this.style = {}; this.style['top'] = toString(top) + 'em'; this.height -= top; this.depth += top; } } /** * * @param {number} left * @private */ setLeft(left) { if (left && left !== 0) { if (!this.style) this.style = {}; this.style['margin-left'] = toString(left) + 'em'; } } /** * * @param {number} right * @private */ setRight(right) { if (right && right !== 0) { if (!this.style) this.style = {}; this.style['margin-right'] = toString(right) + 'em'; } } setWidth(width) { if (width && width !== 0) { if (!this.style) this.style = {}; this.style['width'] = toString(width) + 'em'; } } addMarginRight(margin) { if (margin && margin !== 0) { if (!this.style && !/qquad|quad|enspace|thickspace|mediumspace|thinspace|negativethinspace/.test(this.classes)) { // Attempt to use a class instead of an explicit margin const cls = { '2': 'qquad', '1': 'quad', '.5': 'enspace', '0.277778': 'thickspace', '0.222222': 'mediumspace', '0.166667': 'thinspace', '-0.166667': 'negativethinspace' }[margin.toString()]; if (cls) { this.classes += ' rspace ' + cls; return; } } if (!this.style) this.style = {}; const currentMargin = parseFloat(this.style['margin-right'] || '0'); this.style['margin-right'] = toString(currentMargin + margin) + 'em'; } } /** * Generate the HTML markup to represent this span. * * @param {number} [hskip=0] - Space (in mu, 1/18em) to leave on the left side * of the span. Implemented as a Unicode character if possible, a margin-left otherwise. * @param {number} [hscale=1.0] - If a value is provided, the margins are scaled by * this factor. * @return {string} HTML markup * @method module:core/span.Span#toMarkup * @private */ toMarkup(hskip, hscale) { hskip = hskip || 0; hscale = hscale || 1.0; let result = ''; let body = this.body || ''; if (this.children) { let previousType = 'none'; for (const child of this.children) { let spacing = 0; if (previousType) { let type = child.type; if (type) { if (type === 'textord') type = 'mord'; if (type === 'first') type = 'none'; if (child.isTight) { spacing = (INTER_ATOM_TIGHT_SPACING[previousType + '+' + type] || 0); } else { spacing = (INTER_ATOM_SPACING[previousType + '+' + type] || 0); } spacing = Math.floor(hscale * spacing); } } body += child.toMarkup(spacing, hscale); previousType = lastSpanType(child); } } // Collapse 'empty' spans if ((body === '\u200b' || !body) && (!this.classes || this.classes === 'ML__selected')) { result = ''; } else { // Note: We can't omit the tag, even if it has no class and no style, // as some layouts (vlist) depends on the presence of the tag to function result = '<span'; if (this.cssId) { result += ' id="' + this.cssId + '" ' } if (this.svgOverlay) { this.setStyle('position', 'relative'); this.setStyle('height', this.height + this.depth, 'em'); this.setStyle('vertical-align', -this.depth, 'em'); } if (this.attributes) { for (const attribute in this.attributes) { if (Object.prototype.hasOwnProperty.call(this.attributes, attribute)) { result += ' ' + attribute + '="' + this.attributes[attribute] + '"'; } } } const classes = this.classes.split(' '); // Add the type (mbin, mrel, etc...) if specified if (this.type) { if (/command|placeholder|error/.test(this.type)) { classes.push({ 'command': 'ML__command', 'placeholder': 'ML__placeholder', 'error': 'ML__error' }[this.type]); } if (this.caret && this.type === 'command') { classes.push('ML__command-caret'); } } // Remove duplicate and empty classes let classList = ''; if (classes.length > 1) { classList = classes.filter(function (x, e, a) { return x.length > 0 && a.indexOf(x) === e; }).join(' '); } else { classList = classes[0]; } if (classList.length > 0) { result += ' class="' + classList + '"'; } // If a `hskip` value was provided, add it to the margin-left if (hskip) { if (this.style && this.style['margin-left']) { // There was already a margin, add to it this.style['margin-left'] = toString((parseFloat(this.style['margin-left']) + hskip / 18)) + 'em'; } else { // No margin yet. Can we encode it as a Unicode space? if (hskip < 0 && NEGATIVE_SPACING_CHARACTER[-hskip]) { body = NEGATIVE_SPACING_CHARACTER[-hskip] + body; } else if (SPACING_CHARACTER[hskip]) { body = SPACING_CHARACTER[hskip] + body; } else { if (!this.style) this.style = {}; this.style['margin-left'] = toString(hskip / 18) + 'em'; } } } if (this.style) { let styleString = ''; const isSelected = /ML__selected/.test(this.classes); for (const style in this.style) { if (Object.prototype.hasOwnProperty.call(this.style, style)) { // Render the style property, except the background // of selected spans if (style !== 'background-color' || !isSelected) { styleString += style + ':' + this.style[style] + ';'; } } } if (styleString.length > 0) { result += ' style="' + styleString + '"'; } } result += '>'; // If there is some SVG markup associated with this span, // include it now if (this.svgOverlay) { result += '<span style="'; result += 'display: inline-block;'; result += 'height:' + (this.height + this.depth) + 'em;'; result += 'vertical-align:' + this.depth + 'em;'; result += '">'; result += body; result += '</span>'; result += '<svg '; // result += 'style="position:absolute;left:0;top:0;width:100%;height:100%;z-index:2;'; result += 'style="position:absolute;'; result += 'overflow:overlay;'; result += 'height:' + (this.height + this.depth) + 'em;'; result += 'transform:translateY(-' + Math.round(FontMetrics.toPx(this.depth, 'em') + FontMetrics.toPx(this.style.padding)) + 'px);'; if (this.style && this.style.padding) { result += 'top:' + this.style.padding + ';'; result += 'left:' + this.style.padding + ';'; result += 'width:calc(100% - 2 * ' + this.style.padding + ' );'; } else { result += 'top:0;'; result += 'left:0;'; result += 'width:100%;'; } result += 'z-index:2;'; result += '"'; if (this.svgStyle) { result += ' style="' + this.svgStyle + '"'; } result += '>'; result += this.svgOverlay; result += '</svg>'; } else { result += body; } result = result + '</span>'; } if (this.caret && this.type !== 'command') { if (this.caret === 'text') { result = result + '<span class="ML__text-caret"></span>'; } else { result = result + '<span class="ML__caret"></span>'; } } return result; } /** * Can this span be coalesced with 'span'? * This is used to 'coalesce' (i.e. group together) a series of spans that are * identical except for their value, and to avoid generating redundant spans. * That is: '12' -> * "<span class='mord mathrm'>12</span>" * rather than: * "<span class='mord mathrm'>1</span><span class='mord mathrm'>2</span>" * @param {Span} span * @return {boolean} * @method module:core/span.Span#tryCoalesceWith * @private */ tryCoalesceWith(span) { if (this.tag !== span.tag) return false; if (this.type !== span.type) return false; // Don't coalesce consecutive errors or placeholders if (this.type === 'error' || this.type === 'placeholder' || this.type === 'command') return false; // If this span or the candidate span have children, we can't // coalesce them, but we'll try to coalesce their children const hasChildren = this.children && this.children.length > 0; const spanHasChildren = span.children && span.children.length > 0; if (hasChildren || spanHasChildren) return false; // If they have a different number of styles, can't coalesce const thisStyleCount = this.style ? this.style.length : 0; const spanStyleCount = span.style ? span.style.length : 0; if (thisStyleCount !== spanStyleCount) return false; // For the purpose of our comparison, // any 'empty' classes (whitespace) const classes = this.classes.trim().replace(/\s+/g, ' ') .split(' '); const spanClasses = span.classes.trim().replace(/\s+/g, ' ') .split(' '); // If they have a different number of classes, can't coalesce if (classes.length !== spanClasses.length) return false; // OK, let's do the more expensive comparison now. // If they have different classes, can't coalesce classes.sort(); spanClasses.sort(); for (let i = 0; i < classes.length; i++) { // Don't coalesce vertical separators // (used in column formating with {l||r} for example if (classes[i] === 'vertical-separator') return false; if (classes[i] !== spanClasses[i]) return false; } // If the styles are different, can't coalesce if (this.style && span.style) { for (const style in this.style) { if (Object.prototype.hasOwnProperty.call(this.style, style) && Object.prototype.hasOwnProperty.call(span.style, style)) { if (this.style[style] !== span.style[style]) return false; } } } // OK, the attributes of those spans are compatible. // Merge span into this this.body += span.body; this.height = Math.max(this.height, span.height); this.depth = Math.max(this.depth, span.depth); this.maxFontSize = Math.max(this.maxFontSize, span.maxFontSize); // The italic correction for the coalesced spans is the // italic correction of the last span. this.italic = span.italic; return true; } } /** * Return HTML markup representing this span, its style, classes and * children. * * @param {number} [hskip] amount of whitespace to insert before this element * This is used to adjust the inter-spacing between spans of different types, * e.g. 'bin' and 'rel', according to the TeX rules. * @alias module:core/span.INTER_ATOM_SPACING * @private */ const INTER_ATOM_SPACING = { 'mord+mop': 3, 'mord+mbin': 4, 'mord+mrel': 5, 'mord+minner': 3, 'mop+mord': 3, 'mop+mop': 3, 'mop+mbin': 5, 'mop+minner': 3, 'mbin+mord': 4, 'mbin+mop': 4, 'mbin+mopen': 4, 'mbin+minner': 4, 'mrel+mord': 5, 'mrel+mop': 5, 'mrel+mopen': 5, 'mrel+minner': 5, 'mclose+mop': 3, 'mclose+mbin': 4, 'mclose+mrel': 5, 'mclose+minner': 3, 'mpunct+mord': 3, 'mpunct+mop': 3, 'mpunct+mbin': 4, 'mpunct+mrel': 5, 'mpunct+mopen': 3, 'mpunct+mpunct': 3, 'mpunct+minner': 3 } // See https://www.w3.org/TR/2000/WD-MathML2-20000328/chapter6.html // 6.1.4 Non-Marking Characters const SPACING_CHARACTER = [ '\u200b', // 0/18 ZERO-WIDTH SPACE '\u200a', // 1/18 HAIR SPACE '\u200a\u200a', // 2/18 '\u2009', // 3/18 THIN SPACE '\u205f', // 4/18 MEDIUM MATHEMATICAL SPACE '\u205f\u200a', // 5/18 MEDIUM MATHEMATICAL SPACE + HAIR SPACE '\u2004', // 6/18 THREE-PER-EM SPACE 1/3em '', '', '\u2002' // 9/18 EN SPACE 1/2em = 9/18 ]; const NEGATIVE_SPACING_CHARACTER = [ '', '\u200a\u2063', // -1/18 '', '\u2009\u2063', // -3/18 '\u205f\u2063', // -4/18 '\u2005\u2063' // -5/18 ]; /** * * @alias module:core/span.INTER_ATOM_TIGHT_SPACING * @private */ const INTER_ATOM_TIGHT_SPACING = { 'mord+mop': 3, 'mop+mord': 3, 'mop+mop': 3, 'mclose+mop': 3, 'minner+mop': 3 } function lastSpanType(span) { const result = span.type; if (result === 'first') return 'none'; if (result === 'textord') return 'mord'; return result; } /** * Attempts to coalesce (merge) spans, for example consecutive text spans. * Return a new tree with coalesced spans. * * @param {Span[]} spans * @return {Span[]} coalesced tree * @memberof module:core/span * @private */ function coalesce(spans) { if (!spans || spans.length === 0) return []; spans[0].children = coalesce(spans[0].children); const result = [spans[0]]; for (let i = 1; i < spans.length; i++) { if (!result[result.length - 1].tryCoalesceWith(spans[i])) { spans[i].children = coalesce(spans[i].children); result.push(spans[i]); } } return result; } //---------------------------------------------------------------------------- // UTILITY FUNCTIONS //---------------------------------------------------------------------------- function height(spans) { if (!spans) return 0; if (Array.isArray(spans)) { return spans.reduce((acc, x) => Math.max(acc, x.height), 0); } return spans.height; } function depth(spans) { if (!spans) return 0; if (Array.isArray(spans)) { return spans.reduce((acc, x) => Math.max(acc, x.depth), 0); } return spans.depth; } function skew(spans) { if (!spans) return 0; if (Array.isArray(spans)) { let result = 0; for (const span of spans) { result += span.skew || 0; } return result; } return spans.skew; } function italic(spans) { if (!spans) return 0; if (Array.isArray(spans)) { return spans[spans.length - 1].italic; } return spans.italic; } /** * Make an element made of a sequence of children with classes * @param {(string|Span|Span[])} content the items 'contained' by this node * @param {string} classes list of classes attributes associated with this node * @memberof module:core/span * @private */ export function makeSpan(content, classes) { if (Array.isArray(content)) { const c = []; for (const s of content) { if (s) c.push(s); } if (c.length === 1) { return makeSpan(c[0], classes); } } return new Span(content, classes); } /** * * @param {string} fontFamily * @param {string} symbol * @param {string} classes * @memberof module:core/span * @private */ function makeSymbol(fontFamily, symbol, classes) { const result = new Span(symbol, classes); const metrics = FontMetrics.getCharacterMetrics(symbol, fontFamily); result.height = metrics.height; result.depth = metrics.depth; result.skew = metrics.skew; result.italic = metrics.italic; result.setRight(result.italic); return result; } /** * Makes an element placed in each of the vlist elements to ensure that each * element has the same max font size. To do this, we create a zero-width space * with the correct font size. // * Note: without this, even when fontSize = 0, the fraction bar is no // * longer positioned correctly * @return {Span} * @memberof module:core/span * @private */ function makeFontSizer(context, fontSize) { const fontSizeAdjustment = fontSize ? fontSize / context.mathstyle.sizeMultiplier : 0; const fontSizeInner = new Span('\u200b'); // ZERO WIDTH SPACE if (fontSizeAdjustment !== 1) { fontSizeInner.setStyle('font-size', fontSizeAdjustment, (fontSizeAdjustment > 0) ? 'em' : ''); fontSizeInner.attributes = { "aria-hidden": true } } if (context.size !== 'size5') { return new Span(fontSizeInner, 'fontsize-ensurer reset-' + context.size + ' size5'); } return (fontSizeAdjustment !== 0) ? fontSizeInner : null; } /** * * @param {string} type One of 'mbin', 'mop', 'mord', 'mrel' 'mclose', * 'mpunct', 'minner' * @param {string|Span[]} content A string or an array of other Spans * @param {string} classes CSS classes decorating this span * See https://tex.stackexchange.com/questions/81752/ * for a thorough description of the TeXt atom type and their relevance to * proper kerning. * @memberof module:core/span * @private */ function makeSpanOfType(type, content, classes) { const result = makeSpan(content, classes); result.type = type; return result; } function makeOp(content, classes) { return makeSpanOfType('mop', content, classes); } export function makeOrd(content, classes) { return makeSpanOfType('mord', content, classes); } function makeRel(content, classes) { return makeSpanOfType('mrel', content, classes); } function makeClose(content, classes) { return makeSpanOfType('mclose', content, classes); } function makeOpen(content, classes) { return makeSpanOfType('mopen', content, classes); } export function makeInner(content, classes) { return makeSpanOfType('minner', content, classes); } function makePunct(content, classes) { return makeSpanOfType('mpunct', content, classes); } function makeStyleWrap(type, children, fromStyle, toStyle, classes) { classes = classes || ''; classes += ' style-wrap '; const result = makeHlist(children, classes + fromStyle.adjustTo(toStyle)); result.type = type; const multiplier = toStyle.sizeMultiplier / fromStyle.sizeMultiplier; result.height *= multiplier; result.depth *= multiplier; result.maxFontSize = toStyle.sizeMultiplier; return result; } /** * Add some SVG markup to be overlaid on top of the span * * @param {Span} body * @param {string} svgMarkup * @private */ function makeSVG(body, svgMarkup, svgStyle) { body.svgOverlay = svgMarkup; body.svgStyle = svgStyle; return body; } /** * * @param {Span|Span[]} spans * @param {string} classes * @memberof module:core/span * @private */ export function makeHlist(spans, classes) { if (!classes || classes.length === 0) { // No decorations... if (spans instanceof Span) { // A single span, use it as the output return spans; } else if (Array.isArray(spans) && spans.length === 1) { // An array, with a single span, use the single span as the output return spans[0] } } const result = new Span(spans, classes); let multiplier = 1.0; if (spans instanceof Span) { multiplier = spans.maxFontSize; } else { multiplier = spans.reduce((acc, x) => Math.max(acc, x.maxFontSize), 0); } result.height *= multiplier; result.depth *= multiplier; return result; } /** * Create a new span of type `vlist`, a set of vertically stacked items * @param {Context} context * @param {Array.<(number|Span)>} elements * An array of Span and integer. The integer can be either some kerning information * or the value of an individual shift of the preceding child if in 'individualShift' mode * @param {string} pos The method that will be used to position the elements in the vlist. * * One of: * - `"individualShift"`: each child must be followed by a number indicating how much to shift it (i.e. moved downwards) * - `"top"`: posData specifies the topmost point of the vlist (>0 move up) * - `"bottom"`: posData specifies the bottommost point of the vlist (>0 move down) * - `"shift"`: the baseline of the vlist will be positioned posData away from the baseline * of the first child. (>0 moves down) * @param {number} posData * @memberof module:core/span * @private */ export function makeVlist(context, elements, pos, posData) { let listDepth = 0; let currPos = 0; pos = pos || 'shift'; posData = posData || 0; // Normalize the elements so that they're all either a number or // a single span. If a child is an array of spans, // wrap it in a span for (let i = 0; i < elements.length; i++) { if (Array.isArray(elements[i])) { if (elements[i].length === 1) { // If that's an array made up of a single span, use that span elements[i] = elements[i][0]; } else { // Otherwise, wrap it in a span elements[i] = makeSpan(elements[i]); } } } if (pos === 'shift') { listDepth = -elements[0].depth - posData; } else if (pos === 'bottom') { listDepth = -posData; } else if (pos === 'top') { let bottom = posData; for (const element of elements) { if (element instanceof Span) { // It's a Span, use the dimension data bottom -= element.height + element.depth; } else { // It's a kern adjustment bottom -= element; } } listDepth = bottom; } else if (pos === 'individualShift') { // Individual adjustment to each elements. // The elements list is made up of a Span followed // by a shift adjustment as an integer const originalElements = elements; elements = [originalElements[0]]; // Add in kerns to the list of elements to get each element to be // shifted to the correct specified shift listDepth = -originalElements[1] - originalElements[0].depth; currPos = listDepth; for (let i = 2; i < originalElements.length; i += 2) { const diff = -originalElements[i + 1] - currPos - originalElements[i].depth; currPos = currPos + diff; const kern = diff - (originalElements[i - 2].height + originalElements[i - 2].depth); elements.push(kern); elements.push(originalElements[i]); } } else { console.assert(false, 'makeVList with unknown method: "' + pos + '"'); } // Make the fontSizer let maxFontSize = 1.0; for (const element of elements) { if (element instanceof Span) { maxFontSize = Math.max(maxFontSize, element.maxFontSize); } } const fontSizer = makeFontSizer(context, maxFontSize); const newElements = []; currPos = listDepth; for (const element of elements) { if (typeof element === 'number') { // It's a kern adjustment currPos += element; } else { const wrap = makeSpan([fontSizer, element]); wrap.setTop(-element.depth - currPos); newElements.push(wrap); currPos += element.height + element.depth; } } const result = makeSpan(newElements, 'vlist'); // Fix the final height and depth, in case there were kerns at the ends // since makeSpan won't take that into account. result.depth = Math.max(listDepth, depth(result) || 0); result.height = Math.max(-currPos, height(result) || 0); return result; } // /** // * // * @param {Span|Span[]} base // * @param {number} strutHeight // * @param {number} strutDepth // */ // function makeStrut(base, strutHeight, strutDepth) { // return [base]; // const topStrut = makeSpan(null, 'ML__strut'); // topStrut.height = strutHeight || 0; // topStrut.depth = 0; // topStrut.setStyle('height', strutHeight, 'em'); // const bottomStrut = makeSpan(null, 'ML__strut--bottom'); // bottomStrut.height = strutHeight || 0; // bottomStrut.depth = strutDepth || 0; // bottomStrut.setStyle('height', strutHeight + strutDepth, 'em'); // if (strutDepth) { // bottomStrut.setStyle('vertical-align', -strutDepth, 'em'); // } // if (Array.isArray(base)) { // base.unshift(topStrut); // base.unshift(bottomStrut); // return base; // } // return makeOrd([topStrut, bottomStrut, base]); // } //---------------------------------------------------------------------------- // FONTS //---------------------------------------------------------------------------- // Map an abstract 'fontFamily' to an actual font name const FONT_NAME = { 'ams': 'AMS-Regular', 'bb': 'AMS-Regular', 'cal': 'Caligraphic-Regular', 'frak': 'Fraktur-Regular', 'scr': 'Script-Regular', 'cmr': 'Main-Regular', 'cmss': 'SansSerif-Regular', 'cmtt': 'Typewriter-Regular', 'math': 'Math-Regular', 'mainit': 'Main-Italic', }; const FONT_CLASS = { 'ams': 'ML__ams', 'bb': 'ML__bb', 'cal': 'ML__cal', 'frak': 'ML__frak', 'scr': 'ML__script', 'cmr': 'ML__mathrm', 'cmss': 'ML__sans', 'cmtt': 'ML__tt', 'math': 'ML__mathit', 'mainit': 'ML__mainit', } /** * Given a font family ('frak', 'math'...) return a corresponding * font name. If the font does not support the specified symbol * return an alternate font or null if none could be determined. * @param {(string|Span[])} symbol the character for which we're seeking the font * @param {string} fontFamily such as 'mathbf', 'mathfrak', etc... * @return {string} a font name * @memberof module:span * @private */ function getFontName(symbol, fontFamily) { // If this is not a single char, just do a simple fontFamily -> fontName mapping if (typeof symbol !== 'string' || symbol.length > 1 || symbol === '\u200b') { return FONT_NAME[fontFamily]; } // This is a single character. Do some remapping as necessary. // If symbol is not in the repertoire of the font, // return null. if (fontFamily === 'bb' || fontFamily === 'scr') { // These fonts only support [A-Z ] if (!/^[A-Z ]$/.test(symbol)) return null; } else if (fontFamily === 'cal') { // Only supports uppercase latin and digits if (!/^[0-9A-Z ]$/.test(symbol)) return null; } else if (fontFamily === 'frak') { if (!/^[0-9A-Za-z ]$|^[!"#$%&'()*+,\-./:;=?[]^’‘]$/.test(symbol)) { return null; } } else if (fontFamily === 'cmtt' || fontFamily === 'cmss') { if (!/^[0-9A-Za-z ]$|^[!"&'()*+,\-./:;=?@[]^_~\u0131\u0237\u0393\u0394\u0398\u039b\u039e\u03A0\u03A3\u03A5\u03A8\u03a9’‘]$/.test(symbol)) { return null; } } return FONT_NAME[fontFamily]; } // Export the public interface for this module export default { coalesce, makeSpan, makeOp, makeOrd, makeRel, makeClose, makeOpen, makeInner, makePunct, makeSpanOfType, makeSymbol, makeVlist, makeHlist, makeStyleWrap, // makeStrut, makeSVG, height, depth, skew, italic }