/************************************************************* * * 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 utilities for notations for menclose elements * * @author dpvc@mathjax.org (Davide Cervone) */ import { CommonWrapper } from './Wrapper.js'; import { CommonMenclose } from './Wrappers/menclose.js'; /*****************************************************************/ export const ARROWX = 4; export const ARROWDX = 1; export const ARROWY = 2; // default relative arrowhead values export const THICKNESS = 0.067; // default rule thickness export const PADDING = 0.2; // default padding export const SOLID = THICKNESS + 'em solid'; // a solid border /*****************************************************************/ /** * Shorthand for CommonMenclose */ /* prettier-ignore */ export type Menclose = CommonMenclose; /** * Shorthand for CommonWrapper */ /* prettier-ignore */ export type AnyWrapper = CommonWrapper; /** * Top, right, bottom, left padding data */ export type PaddingData = [number, number, number, number]; /** * The functions used for notation definitions * * @template N The DOM node class */ export type Renderer = (node: W, child: N) => void; export type BBoxExtender = (node: W) => PaddingData; export type BBoxBorder = (node: W) => PaddingData; export type Initializer = (node: W) => void; /** * The definition of a notation * * @template W The menclose wrapper class * @template N The DOM node class */ /* prettier-ignore */ export type NotationDef = { renderer: Renderer; // renders the DOM nodes for the notation bbox: BBoxExtender; // gives the offsets to the child bounding box: [top, right, bottom, left] border?: BBoxBorder; // gives the amount of the bbox offset that is due to borders on the child renderChild?: boolean; // true if the notation is used to render the child directly (e.g., radical) init?: Initializer; // function to be called during wrapper construction remove?: string; // list of notations that are suppressed by this one }; /** * For defining notation maps * * @template W The menclose wrapper class * @template N The DOM node class */ export type DefPair = [string, NotationDef]; export type DefList = Map>; export type DefPairF = (name: T) => DefPair; /** * The list of notations for an menclose element * * @template W The menclose wrapper class * @template N The DOM node class */ export type List = { [notation: string]: NotationDef; }; /*****************************************************************/ /** * The names and indices of sides for borders, padding, etc. */ export const sideIndex = { top: 0, right: 1, bottom: 2, left: 3 }; export type Side = keyof typeof sideIndex; export const sideNames = Object.keys(sideIndex) as Side[]; /** * Common BBox and Border functions * * @param {Menclose} node The enclose node * @returns {BBoxExtender} The bbox extender */ export const fullBBox = ((node) => new Array(4).fill(node.thickness + node.padding)) as BBoxExtender; export const fullPadding = ((node) => new Array(4).fill(node.padding)) as BBoxExtender; export const fullBorder = ((node) => new Array(4).fill(node.thickness)) as BBoxBorder; /*****************************************************************/ /** * The length of an arrowhead * * @param {Menclose} node The enclose node * @returns {number} The arrowhead length */ export const arrowHead = (node: Menclose): number => { return Math.max( node.padding, node.thickness * (node.arrowhead.x + node.arrowhead.dx + 1) ); }; /** * Adjust short bbox for tall arrow heads * * @param {Menclose} node The enclose node * @param {PaddingData} TRBL The arrow head data * @returns {PaddingData} The adjusted arrow head */ export const arrowBBoxHD = (node: Menclose, TRBL: PaddingData) => { if (node.childNodes[0]) { const { h, d } = node.childNodes[0].getBBox(); TRBL[0] = TRBL[2] = Math.max( 0, node.thickness * node.arrowhead.y - (h + d) / 2 ); } return TRBL; }; /** * Adjust thin bbox for wide arrow heads * * @param {Menclose} node The enclose node * @param {PaddingData} TRBL The arrow head data * @returns {PaddingData} The adjusted arrow head */ export const arrowBBoxW = (node: Menclose, TRBL: PaddingData) => { if (node.childNodes[0]) { const { w } = node.childNodes[0].getBBox(); TRBL[1] = TRBL[3] = Math.max(0, node.thickness * node.arrowhead.y - w / 2); } return TRBL; }; /** * The data for horizontal and vertical arrow notations * [angle, double, isVertical, remove] */ export const arrowDef = { up: [-Math.PI / 2, false, true, 'verticalstrike'], down: [Math.PI / 2, false, true, 'verticakstrike'], right: [0, false, false, 'horizontalstrike'], left: [Math.PI, false, false, 'horizontalstrike'], updown: [Math.PI / 2, true, true, 'verticalstrike uparrow downarrow'], leftright: [0, true, false, 'horizontalstrike leftarrow rightarrow'], } as { [name: string]: [number, boolean, boolean, string] }; /** * The data for diagonal arrow notations * [c, pi, double, remove] */ export const diagonalArrowDef = { updiagonal: [-1, 0, false, 'updiagonalstrike northeastarrow'], northeast: [-1, 0, false, 'updiagonalstrike updiagonalarrow'], southeast: [1, 0, false, 'downdiagonalstrike'], northwest: [1, Math.PI, false, 'downdiagonalstrike'], southwest: [-1, Math.PI, false, 'updiagonalstrike'], northeastsouthwest: [ -1, 0, true, 'updiagonalstrike northeastarrow updiagonalarrow southwestarrow', ], northwestsoutheast: [ 1, 0, true, 'downdiagonalstrike northwestarrow southeastarrow', ], } as { [name: string]: [number, number, boolean, string] }; /** * The BBox functions for horizontal and vertical arrows */ export const arrowBBox = { up: (node) => arrowBBoxW(node, [arrowHead(node), 0, node.padding, 0]), down: (node) => arrowBBoxW(node, [node.padding, 0, arrowHead(node), 0]), right: (node) => arrowBBoxHD(node, [0, arrowHead(node), 0, node.padding]), left: (node) => arrowBBoxHD(node, [0, node.padding, 0, arrowHead(node)]), updown: (node) => arrowBBoxW(node, [arrowHead(node), 0, arrowHead(node), 0]), leftright: (node) => arrowBBoxHD(node, [0, arrowHead(node), 0, arrowHead(node)]), } as { [name: string]: BBoxExtender }; /*****************************************************************/ /** * @param {Renderer} render The function for adding the border to the node * @returns {(s: string) => DefPair} The function returingn the notation * definition for the notation having a line on the given side */ export const CommonBorder = function ( render: Renderer ): DefPairF { /** * @param {string} side The side on which a border should appear * @returns {DefPair} The notation definition for the notation having a line on the given side */ return (side: Side) => { const i = sideIndex[side]; return [ side, { // // Add the border to the main child object // renderer: render, // // Indicate the extra space on the given side // bbox: (node) => { const bbox = [0, 0, 0, 0] as PaddingData; bbox[i] = node.thickness + node.padding; return bbox; }, // // Indicate the border on the given side // border: (node) => { const bbox = [0, 0, 0, 0] as PaddingData; bbox[i] = node.thickness; return bbox; }, }, ]; }; }; /** * @param {Renderer} render The function for adding the borders to the node * @returns {(n: string, s1: Side, s2: Side) => DefPair} The function returning * the notation definition for the notation having lines on two sides */ export const CommonBorder2 = function ( render: Renderer ): (name: string, side1: Side, side2: Side) => DefPair { /** * @param {string} name The name of the notation to define * @param {Side} side1 The first side to get a border * @param {Side} side2 The second side to get a border * @returns {DefPair} The notation definition for the notation having lines on two sides */ return (name: string, side1: Side, side2: Side) => { const i1 = sideIndex[side1]; const i2 = sideIndex[side2]; return [ name, { // // Add the border along the given sides // renderer: render, // // Mark the extra space along the two sides // bbox: (node) => { const t = node.thickness + node.padding; const bbox = [0, 0, 0, 0] as PaddingData; bbox[i1] = bbox[i2] = t; return bbox; }, // // Indicate the border on the two sides // border: (node) => { const bbox = [0, 0, 0, 0] as PaddingData; bbox[i1] = bbox[i2] = node.thickness; return bbox; }, // // Remove the single side notations, if present // remove: side1 + ' ' + side2, }, ]; }; }; /*****************************************************************/ /** * @param {(s: string) => Renderer} render The function for adding the strike to * the node * @returns {(s: string) => DefPair} The function returning the notation * definition for the diagonal strike */ export const CommonDiagonalStrike = function ( render: (sname: string) => Renderer ): DefPairF { /** * @param {string} name The name of the diagonal strike to define * @returns {DefPair} The notation definition for the diagonal strike */ return (name: string) => { const cname = 'mjx-' + name.charAt(0) + 'strike'; return [ name + 'diagonalstrike', { // // Find the angle and width from the bounding box size and create the diagonal line // renderer: render(cname), // // Add padding all around // bbox: fullBBox, }, ]; }; }; /*****************************************************************/ /** * @param {Renderer} render The function to add the arrow to the node * @returns {(s: string) => DefPair} The funciton returning the notation * definition for the diagonal arrow */ export const CommonDiagonalArrow = function ( render: Renderer ): DefPairF { /** * @param {string} name The name of the diagonal arrow to define * @returns {DefPair} The notation definition for the diagonal arrow */ return (name: string) => { const [c, pi, double, remove] = diagonalArrowDef[name]; return [ name + 'arrow', { // // Find the angle and width from the bounding box size and create // the arrow from them and the other arrow data // renderer: (node, _child) => { const [a, W] = node.arrowAW(); const arrow = node.arrow(W, c * (a - pi), double); render(node, arrow); }, // // Add space for the arrowhead all around // bbox: (node) => { const { a, x, y } = node.arrowData(); const [ax, ay, adx] = [ node.arrowhead.x, node.arrowhead.y, node.arrowhead.dx, ]; const [b, ar] = node.getArgMod(ax + adx, ay); const dy = y + (b > a ? node.thickness * ar * Math.sin(b - a) : 0); const dx = x + (b > Math.PI / 2 - a ? node.thickness * ar * Math.sin(b + a - Math.PI / 2) : 0); return [dy, dx, dy, dx]; }, // // Remove redundant notations // remove: remove, }, ]; }; }; /** * @param {Renderer} render The function to add the arrow to the node * @returns {(s: string) => DefPair} The function returning the notation * definition for the arrow */ export const CommonArrow = function ( render: Renderer ): DefPairF { /** * @param {string} name The name of the horizontal or vertical arrow to define * @returns {DefPair} The notation definition for the arrow */ return (name: string) => { const [angle, double, isVertical, remove] = arrowDef[name]; return [ name + 'arrow', { // // Get the arrow height and depth from the bounding box and the arrow direction // then create the arrow from that and the other data // renderer: (node, _child) => { const { w, h, d } = node.getBBox(); const [W, offset] = isVertical ? [h + d, 'X'] : [w, 'Y']; const dd = node.getOffset(offset); const arrow = node.arrow(W, angle, double, offset, dd); render(node, arrow); }, // // Add the padding to the proper sides // bbox: arrowBBox[name], // // Remove redundant notations // remove: remove, }, ]; }; };