menclose.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489
  1. /*************************************************************
  2. *
  3. * Copyright (c) 2018-2022 The MathJax Consortium
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License");
  6. * you may not use this file except in compliance with the License.
  7. * You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. /**
  18. * @fileoverview Implements the CHTMLmenclose wrapper for the MmlMenclose object
  19. *
  20. * @author dpvc@mathjax.org (Davide Cervone)
  21. */
  22. import {CHTMLWrapper, CHTMLConstructor} from '../Wrapper.js';
  23. import {CommonMencloseMixin} from '../../common/Wrappers/menclose.js';
  24. import {CHTMLmsqrt} from './msqrt.js';
  25. import * as Notation from '../Notation.js';
  26. import {MmlMenclose} from '../../../core/MmlTree/MmlNodes/menclose.js';
  27. import {OptionList} from '../../../util/Options.js';
  28. import {StyleList} from '../../../util/StyleList.js';
  29. import {em} from '../../../util/lengths.js';
  30. /*****************************************************************/
  31. /**
  32. * The skew angle needed for the arrow head pieces
  33. */
  34. function Angle(x: number, y: number) {
  35. return Math.atan2(x, y).toFixed(3).replace(/\.?0+$/, '');
  36. }
  37. const ANGLE = Angle(Notation.ARROWDX, Notation.ARROWY);
  38. /*****************************************************************/
  39. /**
  40. * The CHTMLmenclose wrapper for the MmlMenclose object
  41. *
  42. * @template N The HTMLElement node class
  43. * @template T The Text node class
  44. * @template D The Document class
  45. */
  46. // @ts-ignore
  47. export class CHTMLmenclose<N, T, D> extends
  48. CommonMencloseMixin<
  49. CHTMLWrapper<any, any, any>,
  50. CHTMLmsqrt<any, any, any>,
  51. any,
  52. CHTMLConstructor<any, any, any>
  53. >(CHTMLWrapper) {
  54. /**
  55. * The menclose wrapper
  56. */
  57. public static kind = MmlMenclose.prototype.kind;
  58. /**
  59. * Styles needed for the various notations
  60. */
  61. public static styles: StyleList = {
  62. 'mjx-menclose': {
  63. position: 'relative'
  64. },
  65. 'mjx-menclose > mjx-dstrike': {
  66. display: 'inline-block',
  67. left: 0, top: 0,
  68. position: 'absolute',
  69. 'border-top': Notation.SOLID,
  70. 'transform-origin': 'top left'
  71. },
  72. 'mjx-menclose > mjx-ustrike': {
  73. display: 'inline-block',
  74. left: 0, bottom: 0,
  75. position: 'absolute',
  76. 'border-top': Notation.SOLID,
  77. 'transform-origin': 'bottom left'
  78. },
  79. 'mjx-menclose > mjx-hstrike': {
  80. 'border-top': Notation.SOLID,
  81. position: 'absolute',
  82. left: 0, right: 0, bottom: '50%',
  83. transform: 'translateY(' + em(Notation.THICKNESS / 2) + ')'
  84. },
  85. 'mjx-menclose > mjx-vstrike': {
  86. 'border-left': Notation.SOLID,
  87. position: 'absolute',
  88. top: 0, bottom: 0, right: '50%',
  89. transform: 'translateX(' + em(Notation.THICKNESS / 2) + ')'
  90. },
  91. 'mjx-menclose > mjx-rbox': {
  92. position: 'absolute',
  93. top: 0, bottom: 0, right: 0, left: 0,
  94. 'border': Notation.SOLID,
  95. 'border-radius': em(Notation.THICKNESS + Notation.PADDING)
  96. },
  97. 'mjx-menclose > mjx-cbox': {
  98. position: 'absolute',
  99. top: 0, bottom: 0, right: 0, left: 0,
  100. 'border': Notation.SOLID,
  101. 'border-radius': '50%'
  102. },
  103. 'mjx-menclose > mjx-arrow': {
  104. position: 'absolute',
  105. left: 0, bottom: '50%', height: 0, width: 0
  106. },
  107. 'mjx-menclose > mjx-arrow > *': {
  108. display: 'block',
  109. position: 'absolute',
  110. 'transform-origin': 'bottom',
  111. 'border-left': em(Notation.THICKNESS * Notation.ARROWX) + ' solid',
  112. 'border-right': 0,
  113. 'box-sizing': 'border-box'
  114. },
  115. 'mjx-menclose > mjx-arrow > mjx-aline': {
  116. left: 0, top: em(-Notation.THICKNESS / 2),
  117. right: em(Notation.THICKNESS * (Notation.ARROWX - 1)), height: 0,
  118. 'border-top': em(Notation.THICKNESS) + ' solid',
  119. 'border-left': 0
  120. },
  121. 'mjx-menclose > mjx-arrow[double] > mjx-aline': {
  122. left: em(Notation.THICKNESS * (Notation.ARROWX - 1)), height: 0,
  123. },
  124. 'mjx-menclose > mjx-arrow > mjx-rthead': {
  125. transform: 'skewX(' + ANGLE + 'rad)',
  126. right: 0, bottom: '-1px',
  127. 'border-bottom': '1px solid transparent',
  128. 'border-top': em(Notation.THICKNESS * Notation.ARROWY) + ' solid transparent'
  129. },
  130. 'mjx-menclose > mjx-arrow > mjx-rbhead': {
  131. transform: 'skewX(-' + ANGLE + 'rad)',
  132. 'transform-origin': 'top',
  133. right: 0, top: '-1px',
  134. 'border-top': '1px solid transparent',
  135. 'border-bottom': em(Notation.THICKNESS * Notation.ARROWY) + ' solid transparent'
  136. },
  137. 'mjx-menclose > mjx-arrow > mjx-lthead': {
  138. transform: 'skewX(-' + ANGLE + 'rad)',
  139. left: 0, bottom: '-1px',
  140. 'border-left': 0,
  141. 'border-right': em(Notation.THICKNESS * Notation.ARROWX) + ' solid',
  142. 'border-bottom': '1px solid transparent',
  143. 'border-top': em(Notation.THICKNESS * Notation.ARROWY) + ' solid transparent'
  144. },
  145. 'mjx-menclose > mjx-arrow > mjx-lbhead': {
  146. transform: 'skewX(' + ANGLE + 'rad)',
  147. 'transform-origin': 'top',
  148. left: 0, top: '-1px',
  149. 'border-left': 0,
  150. 'border-right': em(Notation.THICKNESS * Notation.ARROWX) + ' solid',
  151. 'border-top': '1px solid transparent',
  152. 'border-bottom': em(Notation.THICKNESS * Notation.ARROWY) + ' solid transparent'
  153. },
  154. 'mjx-menclose > dbox': {
  155. position: 'absolute',
  156. top: 0, bottom: 0, left: em(-1.5 * Notation.PADDING),
  157. width: em(3 * Notation.PADDING),
  158. border: em(Notation.THICKNESS) + ' solid',
  159. 'border-radius': '50%',
  160. 'clip-path': 'inset(0 0 0 ' + em(1.5 * Notation.PADDING) + ')',
  161. 'box-sizing': 'border-box'
  162. }
  163. };
  164. /**
  165. * The definitions of the various notations
  166. */
  167. public static notations: Notation.DefList<CHTMLmenclose<any, any, any>, any> = new Map([
  168. Notation.Border('top'),
  169. Notation.Border('right'),
  170. Notation.Border('bottom'),
  171. Notation.Border('left'),
  172. Notation.Border2('actuarial', 'top', 'right'),
  173. Notation.Border2('madruwb', 'bottom', 'right'),
  174. Notation.DiagonalStrike('up', 1),
  175. Notation.DiagonalStrike('down', -1),
  176. ['horizontalstrike', {
  177. renderer: Notation.RenderElement('hstrike', 'Y'),
  178. bbox: (node) => [0, node.padding, 0, node.padding]
  179. }],
  180. ['verticalstrike', {
  181. renderer: Notation.RenderElement('vstrike', 'X'),
  182. bbox: (node) => [node.padding, 0, node.padding, 0]
  183. }],
  184. ['box', {
  185. renderer: (node, child) => {
  186. node.adaptor.setStyle(child, 'border', node.em(node.thickness) + ' solid');
  187. },
  188. bbox: Notation.fullBBox,
  189. border: Notation.fullBorder,
  190. remove: 'left right top bottom'
  191. }],
  192. ['roundedbox', {
  193. renderer: Notation.RenderElement('rbox'),
  194. bbox: Notation.fullBBox
  195. }],
  196. ['circle', {
  197. renderer: Notation.RenderElement('cbox'),
  198. bbox: Notation.fullBBox
  199. }],
  200. ['phasorangle', {
  201. //
  202. // Use a bottom border and an upward strike properly angled
  203. //
  204. renderer: (node, child) => {
  205. const {h, d} = node.getBBox();
  206. const [a, W] = node.getArgMod(1.75 * node.padding, h + d);
  207. const t = node.thickness * Math.sin(a) * .9;
  208. node.adaptor.setStyle(child, 'border-bottom', node.em(node.thickness) + ' solid');
  209. const strike = node.adjustBorder(node.html('mjx-ustrike', {style: {
  210. width: node.em(W),
  211. transform: 'translateX(' + node.em(t) + ') rotate(' + node.fixed(-a) + 'rad)',
  212. }}));
  213. node.adaptor.append(node.chtml, strike);
  214. },
  215. bbox: (node) => {
  216. const p = node.padding / 2;
  217. const t = node.thickness;
  218. return [2 * p, p, p + t, 3 * p + t];
  219. },
  220. border: (node) => [0, 0, node.thickness, 0],
  221. remove: 'bottom'
  222. }],
  223. Notation.Arrow('up'),
  224. Notation.Arrow('down'),
  225. Notation.Arrow('left'),
  226. Notation.Arrow('right'),
  227. Notation.Arrow('updown'),
  228. Notation.Arrow('leftright'),
  229. Notation.DiagonalArrow('updiagonal'), // backward compatibility
  230. Notation.DiagonalArrow('northeast'),
  231. Notation.DiagonalArrow('southeast'),
  232. Notation.DiagonalArrow('northwest'),
  233. Notation.DiagonalArrow('southwest'),
  234. Notation.DiagonalArrow('northeastsouthwest'),
  235. Notation.DiagonalArrow('northwestsoutheast'),
  236. ['longdiv', {
  237. //
  238. // Use a line along the top followed by a half ellipse at the left
  239. //
  240. renderer: (node, child) => {
  241. const adaptor = node.adaptor;
  242. adaptor.setStyle(child, 'border-top', node.em(node.thickness) + ' solid');
  243. const arc = adaptor.append(node.chtml, node.html('dbox'));
  244. const t = node.thickness;
  245. const p = node.padding;
  246. if (t !== Notation.THICKNESS) {
  247. adaptor.setStyle(arc, 'border-width', node.em(t));
  248. }
  249. if (p !== Notation.PADDING) {
  250. adaptor.setStyle(arc, 'left', node.em(-1.5 * p));
  251. adaptor.setStyle(arc, 'width', node.em(3 * p));
  252. adaptor.setStyle(arc, 'clip-path', 'inset(0 0 0 ' + node.em(1.5 * p) + ')');
  253. }
  254. },
  255. bbox: (node) => {
  256. const p = node.padding;
  257. const t = node.thickness;
  258. return [p + t, p, p, 2 * p + t / 2];
  259. }
  260. }],
  261. ['radical', {
  262. //
  263. // Use the msqrt rendering, but remove the extra space due to the radical
  264. // (it is added in at the end, so other notations overlap the root)
  265. //
  266. renderer: (node, child) => {
  267. node.msqrt.toCHTML(child);
  268. const TRBL = node.sqrtTRBL();
  269. node.adaptor.setStyle(node.msqrt.chtml, 'margin', TRBL.map(x => node.em(-x)).join(' '));
  270. },
  271. //
  272. // Create the needed msqrt wrapper
  273. //
  274. init: (node) => {
  275. node.msqrt = node.createMsqrt(node.childNodes[0]);
  276. },
  277. //
  278. // Add back in the padding for the square root
  279. //
  280. bbox: (node) => node.sqrtTRBL(),
  281. //
  282. // This notation replaces the child
  283. //
  284. renderChild: true
  285. }]
  286. ] as Notation.DefPair<CHTMLmenclose<any, any, any>, any>[]);
  287. /********************************************************/
  288. /**
  289. * @override
  290. */
  291. public toCHTML(parent: N) {
  292. const adaptor = this.adaptor;
  293. const chtml = this.standardCHTMLnode(parent);
  294. //
  295. // Create a box for the child (that can have padding and borders added by the notations)
  296. // and add the child HTML into it
  297. //
  298. const block = adaptor.append(chtml, this.html('mjx-box')) as N;
  299. if (this.renderChild) {
  300. this.renderChild(this, block);
  301. } else {
  302. this.childNodes[0].toCHTML(block);
  303. }
  304. //
  305. // Render all the notations for this menclose element
  306. //
  307. for (const name of Object.keys(this.notations)) {
  308. const notation = this.notations[name];
  309. !notation.renderChild && notation.renderer(this, block);
  310. }
  311. //
  312. // Add the needed padding, if any
  313. //
  314. const pbox = this.getPadding();
  315. for (const name of Notation.sideNames) {
  316. const i = Notation.sideIndex[name];
  317. pbox[i] > 0 && adaptor.setStyle(block, 'padding-' + name, this.em(pbox[i]));
  318. }
  319. }
  320. /********************************************************/
  321. /**
  322. * Create an arrow using HTML elements
  323. *
  324. * @param {number} w The length of the arrow
  325. * @param {number} a The angle for the arrow
  326. * @param {boolean} double True if this is a double-headed arrow
  327. * @param {string} offset 'X' for vertical arrow, 'Y' for horizontal
  328. * @param {number} dist Distance to translate in the offset direction
  329. * @return {N} The newly created arrow
  330. */
  331. public arrow(w: number, a: number, double: boolean, offset: string = '', dist: number = 0): N {
  332. const W = this.getBBox().w;
  333. const style = {width: this.em(w)} as OptionList;
  334. if (W !== w) {
  335. style.left = this.em((W - w) / 2);
  336. }
  337. if (a) {
  338. style.transform = 'rotate(' + this.fixed(a) + 'rad)';
  339. }
  340. const arrow = this.html('mjx-arrow', {style: style}, [
  341. this.html('mjx-aline'), this.html('mjx-rthead'), this.html('mjx-rbhead')
  342. ]);
  343. if (double) {
  344. this.adaptor.append(arrow, this.html('mjx-lthead'));
  345. this.adaptor.append(arrow, this.html('mjx-lbhead'));
  346. this.adaptor.setAttribute(arrow, 'double', 'true');
  347. }
  348. this.adjustArrow(arrow, double);
  349. this.moveArrow(arrow, offset, dist);
  350. return arrow;
  351. }
  352. /**
  353. * @param {N} arrow The arrow whose thickness and arrow head is to be adjusted
  354. * @param {boolean} double True if the arrow is double-headed
  355. */
  356. protected adjustArrow(arrow: N, double: boolean) {
  357. const t = this.thickness;
  358. const head = this.arrowhead;
  359. if (head.x === Notation.ARROWX && head.y === Notation.ARROWY &&
  360. head.dx === Notation.ARROWDX && t === Notation.THICKNESS) return;
  361. const [x, y] = [t * head.x, t * head.y].map(x => this.em(x));
  362. const a = Angle(head.dx, head.y);
  363. const [line, rthead, rbhead, lthead, lbhead] = this.adaptor.childNodes(arrow);
  364. this.adjustHead(rthead, [y, '0', '1px', x], a);
  365. this.adjustHead(rbhead, ['1px', '0', y, x], '-' + a);
  366. this.adjustHead(lthead, [y, x, '1px', '0'], '-' + a);
  367. this.adjustHead(lbhead, ['1px', x, y, '0'], a);
  368. this.adjustLine(line, t, head.x, double);
  369. }
  370. /**
  371. * @param {N} head The piece of arrow head to be adjusted
  372. * @param {string[]} border The border sizes [T, R, B, L]
  373. * @param {string} a The skew angle for the piece
  374. */
  375. protected adjustHead(head: N, border: string[], a: string) {
  376. if (head) {
  377. this.adaptor.setStyle(head, 'border-width', border.join(' '));
  378. this.adaptor.setStyle(head, 'transform', 'skewX(' + a + 'rad)');
  379. }
  380. }
  381. /**
  382. * @param {N} line The arrow shaft to be adjusted
  383. * @param {number} t The arrow shaft thickness
  384. * @param {number} x The arrow head x size
  385. * @param {boolean} double True if the arrow is double-headed
  386. */
  387. protected adjustLine(line: N, t: number, x: number, double: boolean) {
  388. this.adaptor.setStyle(line, 'borderTop', this.em(t) + ' solid');
  389. this.adaptor.setStyle(line, 'top', this.em(-t / 2));
  390. this.adaptor.setStyle(line, 'right', this.em(t * (x - 1)));
  391. if (double) {
  392. this.adaptor.setStyle(line, 'left', this.em(t * (x - 1)));
  393. }
  394. }
  395. /**
  396. * @param {N} arrow The arrow whose position is to be adjusted
  397. * @param {string} offset The direction to move the arrow
  398. * @param {number} d The distance to translate in that direction
  399. */
  400. protected moveArrow(arrow: N, offset: string, d: number) {
  401. if (!d) return;
  402. const transform = this.adaptor.getStyle(arrow, 'transform');
  403. this.adaptor.setStyle(
  404. arrow, 'transform', `translate${offset}(${this.em(-d)})${(transform ? ' ' + transform : '')}`
  405. );
  406. }
  407. /********************************************************/
  408. /**
  409. * @param {N} node The HTML element whose border width must be
  410. * adjusted if the thickness isn't the default
  411. * @return {N} The adjusted element
  412. */
  413. public adjustBorder(node: N): N {
  414. if (this.thickness !== Notation.THICKNESS) {
  415. this.adaptor.setStyle(node, 'borderWidth', this.em(this.thickness));
  416. }
  417. return node;
  418. }
  419. /**
  420. * @param {N} shape The svg element whose stroke-thickness must be
  421. * adjusted if the thickness isn't the default
  422. * @return {N} The adjusted element
  423. */
  424. public adjustThickness(shape: N): N {
  425. if (this.thickness !== Notation.THICKNESS) {
  426. this.adaptor.setStyle(shape, 'strokeWidth', this.fixed(this.thickness));
  427. }
  428. return shape;
  429. }
  430. /********************************************************/
  431. /**
  432. * @param {number} m A number to be shown with a fixed number of digits
  433. * @param {number=} n The number of digits to use
  434. * @return {string} The formatted number
  435. */
  436. public fixed(m: number, n: number = 3): string {
  437. if (Math.abs(m) < .0006) {
  438. return '0';
  439. }
  440. return m.toFixed(n).replace(/\.?0+$/, '');
  441. }
  442. /**
  443. * @override
  444. * (make it public so it can be called by the notation functions)
  445. */
  446. public em(m: number) {
  447. return super.em(m);
  448. }
  449. }