/************************************************************* * * Copyright (c) 2018 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 CommonMenclose wrapper mixin for the MmlMenclose object * * @author dpvc@mathjax.org (Davide Cervone) */ import {AnyWrapper, WrapperConstructor, Constructor, AnyWrapperClass} from '../Wrapper.js'; import * as Notation from '../Notation.js'; import {CommonMsqrt} from './msqrt.js'; import {BBox} from '../BBox.js'; import {AbstractMmlNode, AttributeList} from '../../../core/MmlTree/MmlNode.js'; import {MmlMenclose} from '../../../core/MmlTree/MmlNodes/menclose.js'; import {OptionList} from '../../../util/Options.js'; import {StyleData} from '../../common/CssStyles.js'; import {split} from '../../../util/string.js'; /*****************************************************************/ /** * The CommonMenclose interface * * @template W The menclose wrapper type */ export interface CommonMenclose extends AnyWrapper { /** * The notations active on this menclose, and the one to use for the child, if any */ notations: Notation.List; renderChild: Notation.Renderer; /** * fake msqrt for radial notation (if used) */ msqrt: S; /** * The padding, thickness, and shape of the arrow head * (may be overriden using data-padding, data-thickness, and data-arrowhead attibutes) */ padding: number; thickness: number; arrowhead: {x: number, y: number, dx: number}; /** * Look up the data-* attributes and override the default values */ getParameters(): void; /** * Get the notations given in the notation attribute * and check if any are used to render the child nodes */ getNotations(): void; /** * Remove any redundant notations */ removeRedundantNotations(): void; /** * Run any initialization needed by notations in use */ initializeNotations(): void; /** * @return {number[]} Array of the maximum extra space from the notations along each side */ getBBoxExtenders(): number[]; /** * @return {number[]} Array of padding (i.e., BBox minus border) along each side */ getPadding(): number[]; /** * Each entry in X gets replaced by the corresponding one in Y if it is larger * * @param {number[]} X An array of numbers * @param {number[]} Y An array of numbers that replace smaller ones in X */ maximizeEntries(X: number[], Y: number[]): void; /** * @param {number} w The width of the box whose diagonal is needed * @param {number} h The height of the box whose diagonal is needed * @return {number[]} The angle and width of the diagonal of the box */ getArgMod(w: number, h: number): number[]; /** * Create an arrow using an svg element * * @param {number} w The length of the arrow * @param {number} a The angle for the arrow * @param {boolean=} double True if this is a double-headed arrow * @return {N} The newly created arrow */ arrow(w: number, a: number, double?: boolean): N; /** * Get the angle and width of a diagonal arrow, plus the x and y extension * past the content bounding box */ arrowData(): {a: number, W: number, x: number, y: number}; /** * Create an unattached msqrt wrapper to render the 'radical' notation. * We replace the inferred mrow of the msqrt with the one from the menclose * but without changing the parent pointer, so as not to detach it from * the menclose (which would desrtoy the original MathML tree). * * @param {W} child The inferred mrow that is the child of this menclose * @return {S} The newly created (but detached) msqrt wrapper */ createMsqrt(child: W): S; /** * @return {number[]} The differences between the msqrt bounding box * and its child bounding box (i.e., the extra space * created by the radical symbol). */ sqrtTRBL(): number[]; } /** * The CommonMenclose class interface * * @template W The menclose wrapper type * @templare N The DOM node class */ export interface CommonMencloseClass extends AnyWrapperClass { /** * The definitions of the various notations */ notations: Notation.DefList; } /** * Shorthand for the CommonMenclose constructor * * @template W The menclose wrapper type */ export type MencloseConstructor = Constructor>; /*****************************************************************/ /** * The CommonMenclose wrapper mixin for the MmlMenclose object * * @template W The menclose wrapper type * @templare N The DOM node class * @templare S The msqrt wrapper class * @template T The Wrapper class constructor type */ export function CommonMencloseMixin(Base: T): MencloseConstructor & T { return class extends Base { /** * The notations active on this menclose, and the one to use for the child, if any */ public notations: Notation.List = {}; public renderChild: Notation.Renderer = null; /** * fake msqrt for radial notation (if used) */ public msqrt: S = null; /** * The padding, thickness, and shape of the arrow head * (may be overriden using data-padding, data-thickness, and data-arrowhead attibutes) */ public padding: number = Notation.PADDING; public thickness: number = Notation.THICKNESS; public arrowhead = {x: Notation.ARROWX, y: Notation.ARROWY, dx: Notation.ARROWDX}; /** * @override * @constructor */ constructor(...args: any[]) { super(...args); this.getParameters(); this.getNotations(); this.removeRedundantNotations(); this.initializeNotations(); } /** * Look up the data-* attributes and override the default values */ public getParameters() { const attributes = this.node.attributes; const padding = attributes.get('data-padding'); if (padding !== undefined) { this.padding = this.length2em(padding, Notation.PADDING); } const thickness = attributes.get('data-thickness'); if (thickness !== undefined) { this.thickness = this.length2em(thickness, Notation.THICKNESS); } const arrowhead = attributes.get('data-arrowhead') as string; if (arrowhead !== undefined) { let [x, y, dx] = split(arrowhead); this.arrowhead = { x: (x ? parseFloat(x) : Notation.ARROWX), y: (y ? parseFloat(y) : Notation.ARROWY), dx: (dx ? parseFloat(dx) : Notation.ARROWDX) }; } } /** * Get the notations given in the notation attribute * and check if any are used to render the child nodes */ public getNotations() { const Notations = (this.constructor as CommonMencloseClass).notations; for (const name of split(this.node.attributes.get('notation') as string)) { const notation = Notations.get(name); if (notation) { this.notations[name] = notation; if (notation.renderChild) { this.renderChild = notation.renderer; } } } } /** * Remove any redundant notations */ public removeRedundantNotations() { for (const name of Object.keys(this.notations)) { if (this.notations[name]) { const remove = this.notations[name].remove || ''; for (const notation of remove.split(/ /)) { delete this.notations[notation]; } } } } /** * Run any initialization needed by notations in use */ public initializeNotations() { for (const name of Object.keys(this.notations)) { const init = this.notations[name].init; init && init(this as any); } } /********************************************************/ /** * @override */ public computeBBox(bbox: BBox, recompute: boolean = false) { // // Combine the BBox from the child and add the extenders // let [T, R, B, L] = this.getBBoxExtenders(); const child = this.childNodes[0].getBBox(); bbox.combine(child, L, 0); bbox.h += T; bbox.d += B; bbox.w += R; this.setChildPWidths(recompute); } /** * @return {number[]} Array of the maximum extra space from the notations along each side */ public getBBoxExtenders() { let TRBL = [0, 0, 0, 0]; for (const name of Object.keys(this.notations)) { this.maximizeEntries(TRBL, this.notations[name].bbox(this as any)); } return TRBL; } /** * @return {number[]} Array of padding (i.e., BBox minus border) along each side */ public getPadding() { let TRBL = [0, 0, 0, 0]; let BTRBL = [0, 0, 0, 0]; for (const name of Object.keys(this.notations)) { this.maximizeEntries(TRBL, this.notations[name].bbox(this as any)); const border = this.notations[name].border; if (border) { this.maximizeEntries(BTRBL, border(this as any)); } } return [0, 1, 2, 3].map(i => TRBL[i] - BTRBL[i]); } /** * Each entry in X gets replaced by the corresponding one in Y if it is larger * * @param {number[]} X An array of numbers * @param {number[]} Y An array of numbers that replace smaller ones in X */ public maximizeEntries(X: number[], Y: number[]) { for (let i = 0; i < X.length; i++) { if (X[i] < Y[i]) { X[i] = Y[i]; } } } /********************************************************/ /** * @param {number} w The width of the box whose diagonal is needed * @param {number} h The height of the box whose diagonal is needed * @return {number[]} The angle and width of the diagonal of the box */ public getArgMod(w: number, h: number) { return [Math.atan2(h, w), Math.sqrt(w * w + h * h)]; } /** * Create an arrow using an svg element * * @param {number} w The length of the arrow * @param {number} a The angle for the arrow * @param {boolean} double True if this is a double-headed arrow * @return {N} The newly created arrow */ public arrow(w: number, a: number, double: boolean = false) { return null as N; } /** * Get the angle and width of a diagonal arrow, plus the x and y extension * past the content bounding box * * @return {Object} The angle, width, and x and y extentions */ public arrowData() { const [p, t] = [this.padding, this.thickness]; const r = t * (this.arrowhead.x + Math.max(1, this.arrowhead.dx)); const {h, d, w} = this.childNodes[0].getBBox(); const H = h + d; const R = Math.sqrt(H * H + w * w); const x = Math.max(p, r * w / R); const y = Math.max(p, r * H / R); const [a, W] = this.getArgMod(w + 2 * x, H + 2 * y); return {a, W, x, y}; } /********************************************************/ /** * Create an unattached msqrt wrapper to render the 'radical' notation. * We replace the inferred mrow of the msqrt with the one from the menclose * but without changing the parent pointer, so as not to detach it from * the menclose (which would desrtoy the original MathML tree). * * @param {W} child The inferred mrow that is the child of this menclose * @return {S} The newly created (but detached) msqrt wrapper */ public createMsqrt(child: W) { const mmlFactory = (this.node as AbstractMmlNode).factory; const mml = mmlFactory.create('msqrt'); mml.inheritAttributesFrom(this.node); mml.childNodes[0] = child.node; const node = this.wrap(mml) as S; node.parent = this; return node; } /** * @return {number[]} The differences between the msqrt bounding box * and its child bounding box (i.e., the extra space * created by the radical symbol). */ public sqrtTRBL() { const bbox = this.msqrt.getBBox(); const cbox = this.msqrt.childNodes[0].getBBox(); return [bbox.h - cbox.h, 0, bbox.d - cbox.d, bbox.w - cbox.w]; } }; }