/************************************************************* * * Copyright (c) 2018-2025 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. */ /** * @file Implements the SvgMenclose wrapper for the MmlMenclose object * * @author dpvc@mathjax.org (Davide Cervone) */ import { SVG } from '../../svg.js'; import { SvgWrapper, SvgWrapperClass } from '../Wrapper.js'; import { SvgWrapperFactory } from '../WrapperFactory.js'; import { SvgCharOptions, SvgVariantData, SvgDelimiterData, SvgFontData, SvgFontDataClass, } from '../FontData.js'; import { CommonMenclose, CommonMencloseClass, CommonMencloseMixin, } from '../../common/Wrappers/menclose.js'; import { MmlNode } from '../../../core/MmlTree/MmlNode.js'; import { MmlMenclose } from '../../../core/MmlTree/MmlNodes/menclose.js'; import * as Notation from '../Notation.js'; import { SvgMsqrtNTD } from './msqrt.js'; import { OptionList } from '../../../util/Options.js'; /*****************************************************************/ /** * The SvgMenclose interface for the SVG menclose wrapper * * @template N The HTMLElement node class * @template T The Text node class * @template D The Document class */ export interface SvgMencloseNTD extends SvgWrapper, CommonMenclose< N, T, D, SVG, SvgWrapper, SvgWrapperFactory, SvgWrapperClass, SvgCharOptions, SvgVariantData, SvgDelimiterData, SvgFontData, SvgFontDataClass, SvgMsqrtNTD > { /** * Create a line element * * @param {number[]} pq The coordinates of the endpoints, [x1, y1, x2, y2] * @returns {N} The newly created line element */ line(pq: [number, number, number, number]): N; /** * Create a rectangle element * * @param {number} w The width of the rectangle * @param {number} h The height of the rectangle * @param {number} d The depth of the rectangle * @param {number=} r The corner radius for a rounded rectangle * @returns {N} The newly created line element */ box(w: number, h: number, d: number, r?: number): N; /** * Create an ellipse element * * @param {number} w The width of the ellipse * @param {number} h The height of the ellipse * @param {number} d The depth of the ellipse * @returns {N} The newly created ellipse node */ ellipse(w: number, h: number, d: number): N; /** * Create a path element from the commands that specify it * * @param {string} join The join style for the path * @param {(string|number)[]} P The list of commands and coordinates for the path * @returns {N} The newly created path */ path(join: string, ...P: (string | number)[]): N; /** * Create a filled path element from the commands the specify it * (same as path above, but no thickness adjustments) * * @param {(string|number)[]} P The list of commands and coordinates for the path * @returns {N} The newly created path */ fill(...P: (string | number)[]): N; } /** * The SvgMencloseClass interface for the SVG menclose wrapper * * @template N The HTMLElement node class * @template T The Text node class * @template D The Document class */ export interface SvgMencloseClass extends SvgWrapperClass, CommonMencloseClass< N, T, D, SVG, SvgWrapper, SvgWrapperFactory, SvgWrapperClass, SvgCharOptions, SvgVariantData, SvgDelimiterData, SvgFontData, SvgFontDataClass > { new ( factory: SvgWrapperFactory, node: MmlNode, parent?: SvgWrapper ): SvgMencloseNTD; } /*****************************************************************/ /** * The SvgMenclose wrapper for the MmlMenclose class */ export const SvgMenclose = (function (): SvgMencloseClass { const Base = CommonMencloseMixin< N, T, D, SVG, SvgWrapper, SvgWrapperFactory, SvgWrapperClass, SvgCharOptions, SvgVariantData, SvgDelimiterData, SvgFontData, SvgFontDataClass, SvgMsqrtNTD, SvgMencloseClass >(SvgWrapper); // @ts-expect-error Avoid message about base constructors not having the same // type (they should both be SvgWrapper, but are thought of as // different by typescript) return class SvgMenclose extends Base implements SvgMencloseNTD { /** * @override */ public static kind = MmlMenclose.prototype.kind; /** * The definitions of the various notations */ public static notations: Notation.DefList = new Map([ Notation.Border('top'), Notation.Border('right'), Notation.Border('bottom'), Notation.Border('left'), Notation.Border2('actuarial', 'top', 'right'), Notation.Border2('madruwb', 'bottom', 'right'), Notation.DiagonalStrike('up'), Notation.DiagonalStrike('down'), [ 'horizontalstrike', { renderer: Notation.RenderLine('horizontal', 'Y'), bbox: (node) => [0, node.padding, 0, node.padding], }, ], [ 'verticalstrike', { renderer: Notation.RenderLine('vertical', 'X'), bbox: (node) => [node.padding, 0, node.padding, 0], }, ], [ 'box', { renderer: (node, _child) => { const { w, h, d } = node.getBBox(); node.adaptor.append(node.dom[0], node.box(w, h, d)); }, bbox: Notation.fullBBox, border: Notation.fullBorder, remove: 'left right top bottom', }, ], [ 'roundedbox', { renderer: (node, _child) => { const { w, h, d } = node.getBBox(); const r = node.thickness + node.padding; node.adaptor.append(node.dom[0], node.box(w, h, d, r)); }, bbox: Notation.fullBBox, }, ], [ 'circle', { renderer: (node, _child) => { const { w, h, d } = node.getBBox(); node.adaptor.append(node.dom[0], node.ellipse(w, h, d)); }, bbox: Notation.fullBBox, }, ], [ 'phasorangle', { // // Use a bottom border and an upward strike properly angled // renderer: (node, _child) => { const { w, h, d } = node.getBBox(); const a = node.getArgMod(1.75 * node.padding, h + d)[0]; const t = node.thickness / 2; const HD = h + d; const cos = Math.cos(a); node.adaptor.append( node.dom[0], /* prettier-ignore */ node.path('mitre', 'M', w, t - d, 'L', t + cos * t, t - d, 'L', cos * HD + t, HD - d - t) ); }, bbox: (node) => { const p = node.padding / 2; const t = node.thickness; return [2 * p, p, p + t, 3 * p + t]; }, border: (node) => [0, 0, node.thickness, 0], remove: 'bottom', }, ], Notation.Arrow('up'), Notation.Arrow('down'), Notation.Arrow('left'), Notation.Arrow('right'), Notation.Arrow('updown'), Notation.Arrow('leftright'), Notation.DiagonalArrow('updiagonal'), // backward compatibility Notation.DiagonalArrow('northeast'), Notation.DiagonalArrow('southeast'), Notation.DiagonalArrow('northwest'), Notation.DiagonalArrow('southwest'), Notation.DiagonalArrow('northeastsouthwest'), Notation.DiagonalArrow('northwestsoutheast'), [ 'longdiv', { // // Use a line along the top followed by a half ellipse at the left // renderer: (node, _child) => { const { w, h, d } = node.getBBox(); const t = node.thickness / 2; const p = node.padding; node.adaptor.append( node.dom[0], /* prettier-ignore */ node.path('round', 'M', t, t - d, 'a', p - t / 2, (h + d) / 2 - 4 * t, 0, '0,1', 0, h + d - 2 * t, 'L', w - t, h - t) ); }, bbox: (node) => { const p = node.padding; const t = node.thickness; return [p + t, p, p, 2 * p + t / 2]; }, }, ], [ 'radical', { // // Use the msqrt rendering, but remove the extra space due to the radical // (it is added in at the end, so other notations overlap the root) // renderer: (node, child) => { node.msqrt.toSVG([child]); const left = node.sqrtTRBL()[3]; node.place(-left, 0, child); }, // // Create the needed msqrt wrapper // init: (node) => { node.msqrt = node.createMsqrt(node.childNodes[0]); }, // // Add back in the padding for the square root // bbox: (node) => node.sqrtTRBL(), // // This notation replaces the child // renderChild: true, }, ], ] as Notation.DefPair[]); /********************************************************/ /** * @override */ public line(pq: [number, number, number, number]): N { const [x1, y1, x2, y2] = pq; return this.svg('line', { x1: this.fixed(x1), y1: this.fixed(y1), x2: this.fixed(x2), y2: this.fixed(y2), 'stroke-width': this.fixed(this.thickness), }); } /** * @override */ public box(w: number, h: number, d: number, r: number = 0): N { const t = this.thickness; const def: OptionList = { x: this.fixed(t / 2), y: this.fixed(t / 2 - d), width: this.fixed(w - t), height: this.fixed(h + d - t), fill: 'none', 'stroke-width': this.fixed(t), }; if (r) { def.rx = this.fixed(r); } return this.svg('rect', def); } /** * @override */ public ellipse(w: number, h: number, d: number): N { const t = this.thickness; return this.svg('ellipse', { rx: this.fixed((w - t) / 2), ry: this.fixed((h + d - t) / 2), cx: this.fixed(w / 2), cy: this.fixed((h - d) / 2), fill: 'none', 'stroke-width': this.fixed(t), }); } /** * @override */ public path(join: string, ...P: (string | number)[]): N { return this.svg('path', { d: P.map((x) => (typeof x === 'string' ? x : this.fixed(x))).join(' '), style: { 'stroke-width': this.fixed(this.thickness) }, 'stroke-linecap': 'round', 'stroke-linejoin': join, fill: 'none', }); } /** * @override */ public fill(...P: (string | number)[]): N { return this.svg('path', { d: P.map((x) => (typeof x === 'string' ? x : this.fixed(x))).join(' '), }); } /********************************************************/ /** * @override */ /* prettier-ignore */ public arrow(W: number, a: number, double: boolean, offset: string = '', dist: number = 0): N { const {w, h, d} = this.getBBox(); const dw = (W - w) / 2; const m = (h - d) / 2; const t = this.thickness; const t2 = t / 2; const [x, y, dx] = [t * this.arrowhead.x, t * this.arrowhead.y, t * this.arrowhead.dx]; const arrow = (double ? this.fill( 'M', w + dw, m, // point of arrow 'l', -(x + dx), y, 'l', dx, t2 - y, // upper right head 'L', x - dw, m + t2, // upper side of shaft 'l', dx, y - t2, 'l', -(x + dx), - y, // left point 'l', x + dx, -y, 'l', -dx, y - t2, // lower left head 'L', w + dw - x, m - t2, // lower side of shaft 'l', -dx, t2 - y, 'Z' // lower head ) : this.fill( 'M', w + dw, m, // point of arrow 'l', -(x + dx), y, 'l', dx, t2 - y, // upper head 'L', -dw, m + t2, 'l', 0, -t, // upper side of shaft 'L', w + dw - x, m - t2, // lower side of shaft 'l', -dx, t2 - y, 'Z' // lower head )); const transform = []; if (dist) { transform.push(offset === 'X' ? `translate(${this.fixed(-dist)} 0)` : `translate(0 ${this.fixed(dist)})`); } if (a) { const A = this.jax.fixed(-a * 180 / Math.PI); transform.push(`rotate(${A} ${this.fixed(w / 2)} ${this.fixed(m)})`); } if (transform.length) { this.adaptor.setAttribute(arrow, 'transform', transform.join(' ')); } return arrow as N; } /********************************************************/ /** * @override */ public toSVG(parents: N[]) { const svg = this.standardSvgNodes(parents); // // Create a box at the correct position and add the children // const left = this.getBBoxExtenders()[3]; const def: OptionList = {}; if (left > 0) { def.transform = 'translate(' + this.fixed(left) + ', 0)'; } const block = this.adaptor.append(svg[0], this.svg('g', def)) as N; if (this.renderChild) { this.renderChild(this, block); } else { this.childNodes[0].toSVG([block]); this.childNodes[0].place(0, 0); } // // Render all the notations for this menclose element // for (const name of Object.keys(this.notations)) { const notation = this.notations[name]; if (!notation.renderChild) { notation.renderer(this, svg[0]); } } } }; })();