/************************************************************* * * Copyright (c) 2017 The MathJax Consortium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * @fileoverview Implements the CommonMo wrapper mixin for the MmlMo object * * @author dpvc@mathjax.org (Davide Cervone) */ import {AnyWrapper, WrapperConstructor, Constructor} from '../Wrapper.js'; import {MmlMo} from '../../../core/MmlTree/MmlNodes/mo.js'; import {BBox} from '../BBox.js'; import {DelimiterData} from '../FontData.js'; import {DIRECTION, NOSTRETCH} from '../FontData.js'; /*****************************************************************/ /** * Convert direction to letter */ export const DirectionVH: {[n: number]: string} = { [DIRECTION.Vertical]: 'v', [DIRECTION.Horizontal]: 'h' }; /*****************************************************************/ /** * The CommonMo interface */ export interface CommonMo extends AnyWrapper { /** * True if no italic correction should be used */ noIC: boolean; /** * The font size that a stretched operator uses. * If -1, then stretch arbitrarily, and bbox gives the actual height, depth, width */ size: number; /** * True if used as an accent in an munderover construct */ isAccent: boolean; /** * Determint variant for vertically/horizontally stretched character * * @param {number[]} WH size to stretch to, either [W] or [H, D] * @param {boolean} exact True if not allowed to use delimiter factor and shortfall */ getStretchedVariant(WH: number[], exact?: boolean): void; /** * @param {string} name The name of the attribute to get * @param {number} value The default value to use * @return {number} The size in em's of the attribute (or the default value) */ getSize(name: string, value: number): number; /** * @param {number[]} WH Either [W] for width, [H, D] for height and depth, or [] for min/max size * @return {number} Either the width or the total height of the character */ getWH(WH: number[]): number; /** * @param {number[]} WHD The [W] or [H, D] being requested from the parent mrow * @param {number} D The full dimension (including symmetry, etc) * @param {DelimiterData} C The delimiter data for the stretchy character */ getStretchBBox(WHD: number[], D: number, C: DelimiterData): void; /** * @param {number[]} WHD The [H, D] being requested from the parent mrow * @param {number} HD The full height (including symmetry, etc) * @param {DelimiterData} C The delimiter data for the stretchy character * @return {number[]} The height and depth for the vertically stretched delimiter */ getBaseline(WHD: number[], HD: number, C: DelimiterData): number[]; } /** * Shorthand for the CommonMo constructor */ export type MoConstructor = Constructor; /*****************************************************************/ /** * The CommomMo wrapper mixin for the MmlMo object * * @template T The Wrapper class constructor type */ export function CommonMoMixin(Base: T): MoConstructor & T { return class extends Base { /** * True if no italic correction should be used */ public noIC: boolean = false; /** * The font size that a stretched operator uses. * If -1, then stretch arbitrarily, and bbox gives the actual height, depth, width */ public size: number = null; /** * True if used as an accent in an munderover construct */ public isAccent: boolean; /** * @override */ constructor(...args: any[]) { super(...args); this.isAccent = (this.node as MmlMo).isAccent; } /** * @override */ public computeBBox(bbox: BBox, recompute: boolean = false) { const stretchy = (this.stretch.dir !== DIRECTION.None); if (stretchy && this.size === null) { this.getStretchedVariant([0]); } if (stretchy && this.size < 0) return; super.computeBBox(bbox); this.copySkewIC(bbox); if (this.noIC) { bbox.w -= bbox.ic; } if (this.node.attributes.get('symmetric') && this.stretch.dir !== DIRECTION.Horizontal) { const d = ((bbox.h + bbox.d) / 2 + this.font.params.axis_height) - bbox.h; bbox.h += d; bbox.d -= d; } } /** * @override */ public getVariant() { if (this.node.attributes.get('largeop')) { this.variant = (this.node.attributes.get('displaystyle') ? '-largeop' : '-smallop'); } else { super.getVariant(); } } /** * @override */ public canStretch(direction: DIRECTION) { if (this.stretch.dir !== DIRECTION.None) { return this.stretch.dir === direction; } const attributes = this.node.attributes; if (!attributes.get('stretchy')) return false; const c = this.getText(); if (c.length !== 1) return false; const delim = this.font.getDelimiter(c.charCodeAt(0)); this.stretch = (delim && delim.dir === direction ? delim : NOSTRETCH); return this.stretch.dir !== DIRECTION.None; } /** * Determint variant for vertically/horizontally stretched character * * @param {number[]} WH size to stretch to, either [W] or [H, D] * @param {boolean} exact True if not allowed to use delimiter factor and shortfall */ public getStretchedVariant(WH: number[], exact: boolean = false) { if (this.stretch.dir !== DIRECTION.None) { let D = this.getWH(WH); const min = this.getSize('minsize', 0); const max = this.getSize('maxsize', Infinity); // // Clamp the dimension to the max and min // then get the minimum size via TeX rules // D = Math.max(min, Math.min(max, D)); const m = (min || exact ? D : Math.max(D * this.font.params.delimiterfactor / 1000, D - this.font.params.delimitershortfall)); // // Look through the delimiter sizes for one that matches // const delim = this.stretch; const c = delim.c || this.getText().charCodeAt(0); let i = 0; if (delim.sizes) { for (const d of delim.sizes) { if (d >= m) { this.variant = this.font.getSizeVariant(c, i); this.size = i; return; } i++; } } // // No size matches, so if we can make multi-character delimiters, // record the data for that, otherwise, use the largest fixed size. // if (delim.stretch) { this.size = -1; this.invalidateBBox(); this.getStretchBBox(WH, D, delim); } else { this.variant = this.font.getSizeVariant(c, i - 1); this.size = i - 1; } } } /** * @param {string} name The name of the attribute to get * @param {number} value The default value to use * @return {number} The size in em's of the attribute (or the default value) */ public getSize(name: string, value: number) { let attributes = this.node.attributes; if (attributes.isSet(name)) { value = this.length2em(attributes.get(name), 1, 1); // FIXME: should use height of actual character } return value; } /** * @param {number[]} WH Either [W] for width, [H, D] for height and depth, or [] for min/max size * @return {number} Either the width or the total height of the character */ public getWH(WH: number[]) { if (WH.length === 0) return 0; if (WH.length === 1) return WH[0]; let [H, D] = WH; const a = this.font.params.axis_height; return (this.node.attributes.get('symmetric') ? 2 * Math.max(H - a, D + a) : H + D); } /** * @param {number[]} WHD The [W] or [H, D] being requested from the parent mrow * @param {number} D The full dimension (including symmetry, etc) * @param {DelimiterData} C The delimiter data for the stretchy character */ public getStretchBBox(WHD: number[], D: number, C: DelimiterData) { if (C.hasOwnProperty('min') && C.min > D) { D = C.min; } let [h, d, w] = C.HDW; if (this.stretch.dir === DIRECTION.Vertical) { [h, d] = this.getBaseline(WHD, D, C); } else { w = D; } this.bbox.h = h; this.bbox.d = d; this.bbox.w = w; } /** * @param {number[]} WHD The [H, D] being requested from the parent mrow * @param {number} HD The full height (including symmetry, etc) * @param {DelimiterData} C The delimiter data for the stretchy character * @return {number[]} The height and depth for the vertically stretched delimiter */ public getBaseline(WHD: number[], HD: number, C: DelimiterData) { const hasWHD = (WHD.length === 2 && WHD[0] + WHD[1] === HD); const symmetric = this.node.attributes.get('symmetric'); const [H, D] = (hasWHD ? WHD : [HD, 0]); let [h, d] = [H + D, 0]; if (symmetric) { // // Center on the math axis // const a = this.font.params.axis_height; if (hasWHD) { h = 2 * Math.max(H - a, D + a); } d = h / 2 - a; } else if (hasWHD) { // // Use the given depth (from mrow) // d = D; } else { // // Use depth proportional to the normal-size character // (when stretching for minsize or maxsize by itself) // let [ch, cd] = (C.HDW || [.75, .25]); d = cd * (h / (ch + cd)); } return [h - d, d]; } /** * @override */ public remapChars(chars: number[]) { if (chars.length == 1) { const parent = this.node.parent; const isAccent = this.isAccent && (parent === (this.node as MmlMo).coreParent() || parent.isEmbellished); const map = (isAccent ? 'accent' : 'mo'); const text = this.font.getRemappedChar(map, chars[0]); if (text) { chars = this.unicodeChars(text); } } return chars; } }; }