Wrapper.ts 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845
  1. /*************************************************************
  2. *
  3. * Copyright (c) 2017-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 CommonWrapper class
  19. *
  20. * @author dpvc@mathjax.org (Davide Cervone)
  21. */
  22. import {AbstractWrapper, WrapperClass} from '../../core/Tree/Wrapper.js';
  23. import {PropertyList} from '../../core/Tree/Node.js';
  24. import {MmlNode, TextNode, AbstractMmlNode, indentAttributes} from '../../core/MmlTree/MmlNode.js';
  25. import {MmlMo} from '../../core/MmlTree/MmlNodes/mo.js';
  26. import {Property} from '../../core/Tree/Node.js';
  27. import {unicodeChars} from '../../util/string.js';
  28. import * as LENGTHS from '../../util/lengths.js';
  29. import {Styles} from '../../util/Styles.js';
  30. import {StyleList} from '../../util/StyleList.js';
  31. import {CommonOutputJax} from './OutputJax.js';
  32. import {CommonWrapperFactory} from './WrapperFactory.js';
  33. import {BBox} from '../../util/BBox.js';
  34. import {FontData, DelimiterData, CharData, CharOptions, DIRECTION, NOSTRETCH} from './FontData.js';
  35. /*****************************************************************/
  36. /**
  37. * Shorthand for a dictionary object (an object of key:value pairs)
  38. */
  39. export type StringMap = {[key: string]: string};
  40. /**
  41. * MathML spacing rules
  42. */
  43. /* tslint:disable-next-line:whitespace */
  44. const SMALLSIZE = 2/18;
  45. /**
  46. * @param {boolean} script The scriptlevel
  47. * @param {number} size The space size
  48. * @return {number} The size clamped to SMALLSIZE when scriptlevel > 0
  49. */
  50. function MathMLSpace(script: boolean, size: number): number {
  51. return (script ? size < SMALLSIZE ? 0 : SMALLSIZE : size);
  52. }
  53. export type Constructor<T> = new(...args: any[]) => T;
  54. /**
  55. * Shorthands for wrappers and their constructors
  56. */
  57. export type AnyWrapper = CommonWrapper<any, any, any, any, any, any>;
  58. export type AnyWrapperClass = CommonWrapperClass<any, any, any, any, any, any>;
  59. export type WrapperConstructor = Constructor<AnyWrapper>;
  60. /*********************************************************/
  61. /**
  62. * The CommonWrapper class interface
  63. *
  64. * @template J The OutputJax type
  65. * @template W The Wrapper type
  66. * @template C The WrapperClass type
  67. * @template CC The CharOptions type
  68. * @template FD The FontData type
  69. */
  70. export interface CommonWrapperClass<
  71. J extends CommonOutputJax<any, any, any, W, CommonWrapperFactory<J, W, C, CC, DD, FD>, FD, any>,
  72. W extends CommonWrapper<J, W, C, CC, DD, FD>,
  73. C extends CommonWrapperClass<J, W, C, CC, DD, FD>,
  74. CC extends CharOptions,
  75. DD extends DelimiterData,
  76. FD extends FontData<CC, any, DD>
  77. > extends WrapperClass<MmlNode, CommonWrapper<J, W, C, CC, DD, FD>> {
  78. /**
  79. * @override
  80. */
  81. new(factory: CommonWrapperFactory<J, W, C, CC, DD, FD>, node: MmlNode, ...args: any[]): W;
  82. }
  83. /*****************************************************************/
  84. /**
  85. * The base CommonWrapper class
  86. *
  87. * @template J The OutputJax type
  88. * @template W The Wrapper type
  89. * @template C The WrapperClass type
  90. * @template CC The CharOptions type
  91. * @template FD The FontData type
  92. */
  93. export class CommonWrapper<
  94. J extends CommonOutputJax<any, any, any, W, CommonWrapperFactory<J, W, C, CC, DD, FD>, FD, any>,
  95. W extends CommonWrapper<J, W, C, CC, DD, FD>,
  96. C extends CommonWrapperClass<J, W, C, CC, DD, FD>,
  97. CC extends CharOptions,
  98. DD extends DelimiterData,
  99. FD extends FontData<CC, any, DD>
  100. > extends AbstractWrapper<MmlNode, CommonWrapper<J, W, C, CC, DD, FD>> {
  101. /**
  102. * The wrapper kind
  103. */
  104. public static kind: string = 'unknown';
  105. /**
  106. * Any styles needed for the class
  107. */
  108. public static styles: StyleList = {};
  109. /**
  110. * Styles that should not be passed on from style attribute
  111. */
  112. public static removeStyles: string[] = [
  113. 'fontSize', 'fontFamily', 'fontWeight',
  114. 'fontStyle', 'fontVariant', 'font'
  115. ];
  116. /**
  117. * Non-MathML attributes on MathML elements NOT to be copied to the
  118. * corresponding DOM elements. If set to false, then the attribute
  119. * WILL be copied. Most of these (like the font attributes) are handled
  120. * in other ways.
  121. */
  122. public static skipAttributes: {[name: string]: boolean} = {
  123. fontfamily: true, fontsize: true, fontweight: true, fontstyle: true,
  124. color: true, background: true,
  125. 'class': true, href: true, style: true,
  126. xmlns: true
  127. };
  128. /**
  129. * The translation of mathvariant to bold styles, or to remove
  130. * bold from a mathvariant.
  131. */
  132. public static BOLDVARIANTS: {[name: string]: StringMap} = {
  133. bold: {
  134. normal: 'bold',
  135. italic: 'bold-italic',
  136. fraktur: 'bold-fraktur',
  137. script: 'bold-script',
  138. 'sans-serif': 'bold-sans-serif',
  139. 'sans-serif-italic': 'sans-serif-bold-italic'
  140. },
  141. normal: {
  142. bold: 'normal',
  143. 'bold-italic': 'italic',
  144. 'bold-fraktur': 'fraktur',
  145. 'bold-script': 'script',
  146. 'bold-sans-serif': 'sans-serif',
  147. 'sans-serif-bold-italic': 'sans-serif-italic'
  148. }
  149. };
  150. /**
  151. * The translation of mathvariant to italic styles, or to remove
  152. * italic from a mathvariant.
  153. */
  154. public static ITALICVARIANTS: {[name: string]: StringMap} = {
  155. italic: {
  156. normal: 'italic',
  157. bold: 'bold-italic',
  158. 'sans-serif': 'sans-serif-italic',
  159. 'bold-sans-serif': 'sans-serif-bold-italic'
  160. },
  161. normal: {
  162. italic: 'normal',
  163. 'bold-italic': 'bold',
  164. 'sans-serif-italic': 'sans-serif',
  165. 'sans-serif-bold-italic': 'bold-sans-serif'
  166. }
  167. };
  168. /**
  169. * The factory used to create more wrappers
  170. */
  171. protected factory: CommonWrapperFactory<J, W, C, CC, DD, FD>;
  172. /**
  173. * The parent of this node
  174. */
  175. public parent: W = null;
  176. /**
  177. * The children of this node
  178. */
  179. public childNodes: W[];
  180. /**
  181. * Styles that must be handled directly by the wrappers (mostly having to do with fonts)
  182. */
  183. protected removedStyles: StringMap = null;
  184. /**
  185. * The explicit styles set by the node
  186. */
  187. protected styles: Styles = null;
  188. /**
  189. * The mathvariant for this node
  190. */
  191. public variant: string = '';
  192. /**
  193. * The bounding box for this node
  194. */
  195. public bbox: BBox;
  196. /**
  197. * Whether the bounding box has been computed yet
  198. */
  199. protected bboxComputed: boolean = false;
  200. /**
  201. * Delimiter data for stretching this node (NOSTRETCH means not yet determined)
  202. */
  203. public stretch: DD = NOSTRETCH as DD;
  204. /**
  205. * Easy access to the font parameters
  206. */
  207. public font: FD = null;
  208. /**
  209. * Easy access to the output jax for this node
  210. */
  211. get jax() {
  212. return this.factory.jax;
  213. }
  214. /**
  215. * Easy access to the DOMAdaptor object
  216. */
  217. get adaptor() {
  218. return this.factory.jax.adaptor;
  219. }
  220. /**
  221. * Easy access to the metric data for this node
  222. */
  223. get metrics() {
  224. return this.factory.jax.math.metrics;
  225. }
  226. /**
  227. * True if children with percentage widths should be resolved by this container
  228. */
  229. get fixesPWidth() {
  230. return !this.node.notParent && !this.node.isToken;
  231. }
  232. /*******************************************************************/
  233. /**
  234. * @override
  235. */
  236. constructor(factory: CommonWrapperFactory<J, W, C, CC, DD, FD>, node: MmlNode, parent: W = null) {
  237. super(factory, node);
  238. this.parent = parent;
  239. this.font = factory.jax.font;
  240. this.bbox = BBox.zero();
  241. this.getStyles();
  242. this.getVariant();
  243. this.getScale();
  244. this.getSpace();
  245. this.childNodes = node.childNodes.map((child: MmlNode) => {
  246. const wrapped = this.wrap(child);
  247. if (wrapped.bbox.pwidth && (node.notParent || node.isKind('math'))) {
  248. this.bbox.pwidth = BBox.fullWidth;
  249. }
  250. return wrapped;
  251. });
  252. }
  253. /**
  254. * @param {MmlNode} node The node to the wrapped
  255. * @param {W} parent The wrapped parent node
  256. * @return {W} The newly wrapped node
  257. */
  258. public wrap(node: MmlNode, parent: W = null): W {
  259. const wrapped = this.factory.wrap(node, parent || this);
  260. if (parent) {
  261. parent.childNodes.push(wrapped);
  262. }
  263. this.jax.nodeMap.set(node, wrapped);
  264. return wrapped;
  265. }
  266. /*******************************************************************/
  267. /**
  268. * Return the wrapped node's bounding box, either the cached one, if it exists,
  269. * or computed directly if not.
  270. *
  271. * @param {boolean} save Whether to cache the bbox or not (used for stretchy elements)
  272. * @return {BBox} The computed bounding box
  273. */
  274. public getBBox(save: boolean = true): BBox {
  275. if (this.bboxComputed) {
  276. return this.bbox;
  277. }
  278. const bbox = (save ? this.bbox : BBox.zero());
  279. this.computeBBox(bbox);
  280. this.bboxComputed = save;
  281. return bbox;
  282. }
  283. /**
  284. * Return the wrapped node's bounding box that includes borders and padding
  285. *
  286. * @param {boolean} save Whether to cache the bbox or not (used for stretchy elements)
  287. * @return {BBox} The computed bounding box
  288. */
  289. public getOuterBBox(save: boolean = true): BBox {
  290. const bbox = this.getBBox(save);
  291. if (!this.styles) return bbox;
  292. const obox = new BBox();
  293. Object.assign(obox, bbox);
  294. for (const [name, side] of BBox.StyleAdjust) {
  295. const x = this.styles.get(name);
  296. if (x) {
  297. (obox as any)[side] += this.length2em(x, 1, obox.rscale);
  298. }
  299. }
  300. return obox;
  301. }
  302. /**
  303. * @param {BBox} bbox The bounding box to modify (either this.bbox, or an empty one)
  304. * @param {boolean} recompute True if we are recomputing due to changes in children that have percentage widths
  305. */
  306. protected computeBBox(bbox: BBox, recompute: boolean = false) {
  307. bbox.empty();
  308. for (const child of this.childNodes) {
  309. bbox.append(child.getOuterBBox());
  310. }
  311. bbox.clean();
  312. if (this.fixesPWidth && this.setChildPWidths(recompute)) {
  313. this.computeBBox(bbox, true);
  314. }
  315. }
  316. /**
  317. * Recursively resolve any percentage widths in the child nodes using the given
  318. * container width (or the child width, if none was passed).
  319. * Overriden for mtables in order to compute the width.
  320. *
  321. * @param {boolean} recompute True if we are recomputing due to changes in children
  322. * @param {(number|null)=} w The width of the container (from which percentages are computed)
  323. * @param {boolean=} clear True if pwidth marker is to be cleared
  324. * @return {boolean} True if a percentage width was found
  325. */
  326. public setChildPWidths(recompute: boolean, w: (number | null) = null, clear: boolean = true): boolean {
  327. if (recompute) {
  328. return false;
  329. }
  330. if (clear) {
  331. this.bbox.pwidth = '';
  332. }
  333. let changed = false;
  334. for (const child of this.childNodes) {
  335. const cbox = child.getOuterBBox();
  336. if (cbox.pwidth && child.setChildPWidths(recompute, w === null ? cbox.w : w, clear)) {
  337. changed = true;
  338. }
  339. }
  340. return changed;
  341. }
  342. /**
  343. * Mark BBox to be computed again (e.g., when an mo has stretched)
  344. */
  345. public invalidateBBox() {
  346. if (this.bboxComputed) {
  347. this.bboxComputed = false;
  348. if (this.parent) {
  349. this.parent.invalidateBBox();
  350. }
  351. }
  352. }
  353. /**
  354. * Copy child skew and italic correction
  355. *
  356. * @param {BBox} bbox The bounding box to modify
  357. */
  358. protected copySkewIC(bbox: BBox) {
  359. const first = this.childNodes[0];
  360. if (first?.bbox.sk) {
  361. bbox.sk = first.bbox.sk;
  362. }
  363. if (first?.bbox.dx) {
  364. bbox.dx = first.bbox.dx;
  365. }
  366. const last = this.childNodes[this.childNodes.length - 1];
  367. if (last?.bbox.ic) {
  368. bbox.ic = last.bbox.ic;
  369. bbox.w += bbox.ic;
  370. }
  371. }
  372. /*******************************************************************/
  373. /**
  374. * Add the style attribute, but remove any font-related styles
  375. * (since these are handled separately by the variant)
  376. */
  377. protected getStyles() {
  378. const styleString = this.node.attributes.getExplicit('style') as string;
  379. if (!styleString) return;
  380. const style = this.styles = new Styles(styleString);
  381. for (let i = 0, m = CommonWrapper.removeStyles.length; i < m; i++) {
  382. const id = CommonWrapper.removeStyles[i];
  383. if (style.get(id)) {
  384. if (!this.removedStyles) this.removedStyles = {};
  385. this.removedStyles[id] = style.get(id);
  386. style.set(id, '');
  387. }
  388. }
  389. }
  390. /**
  391. * Get the mathvariant (or construct one, if needed).
  392. */
  393. protected getVariant() {
  394. if (!this.node.isToken) return;
  395. const attributes = this.node.attributes;
  396. let variant = attributes.get('mathvariant') as string;
  397. if (!attributes.getExplicit('mathvariant')) {
  398. const values = attributes.getList('fontfamily', 'fontweight', 'fontstyle') as StringMap;
  399. if (this.removedStyles) {
  400. const style = this.removedStyles;
  401. if (style.fontFamily) values.family = style.fontFamily;
  402. if (style.fontWeight) values.weight = style.fontWeight;
  403. if (style.fontStyle) values.style = style.fontStyle;
  404. }
  405. if (values.fontfamily) values.family = values.fontfamily;
  406. if (values.fontweight) values.weight = values.fontweight;
  407. if (values.fontstyle) values.style = values.fontstyle;
  408. if (values.weight && values.weight.match(/^\d+$/)) {
  409. values.weight = (parseInt(values.weight) > 600 ? 'bold' : 'normal');
  410. }
  411. if (values.family) {
  412. variant = this.explicitVariant(values.family, values.weight, values.style);
  413. } else {
  414. if (this.node.getProperty('variantForm')) variant = '-tex-variant';
  415. variant = (CommonWrapper.BOLDVARIANTS[values.weight] || {})[variant] || variant;
  416. variant = (CommonWrapper.ITALICVARIANTS[values.style] || {})[variant] || variant;
  417. }
  418. }
  419. this.variant = variant;
  420. }
  421. /**
  422. * Set the CSS for a token element having an explicit font (rather than regular mathvariant).
  423. *
  424. * @param {string} fontFamily The font family to use
  425. * @param {string} fontWeight The font weight to use
  426. * @param {string} fontStyle The font style to use
  427. */
  428. protected explicitVariant(fontFamily: string, fontWeight: string, fontStyle: string) {
  429. let style = this.styles;
  430. if (!style) style = this.styles = new Styles();
  431. style.set('fontFamily', fontFamily);
  432. if (fontWeight) style.set('fontWeight', fontWeight);
  433. if (fontStyle) style.set('fontStyle', fontStyle);
  434. return '-explicitFont';
  435. }
  436. /**
  437. * Determine the scaling factor to use for this wrapped node, and set the styles for it.
  438. */
  439. protected getScale() {
  440. let scale = 1, parent = this.parent;
  441. let pscale = (parent ? parent.bbox.scale : 1);
  442. let attributes = this.node.attributes;
  443. let scriptlevel = Math.min(attributes.get('scriptlevel') as number, 2);
  444. let fontsize = attributes.get('fontsize');
  445. let mathsize = (this.node.isToken || this.node.isKind('mstyle') ?
  446. attributes.get('mathsize') : attributes.getInherited('mathsize'));
  447. //
  448. // If scriptsize is non-zero, set scale based on scriptsizemultiplier
  449. //
  450. if (scriptlevel !== 0) {
  451. scale = Math.pow(attributes.get('scriptsizemultiplier') as number, scriptlevel);
  452. let scriptminsize = this.length2em(attributes.get('scriptminsize'), .8, 1);
  453. if (scale < scriptminsize) scale = scriptminsize;
  454. }
  455. //
  456. // If there is style="font-size:...", and not fontsize attribute, use that as fontsize
  457. //
  458. if (this.removedStyles && this.removedStyles.fontSize && !fontsize) {
  459. fontsize = this.removedStyles.fontSize;
  460. }
  461. //
  462. // If there is a fontsize and no mathsize attribute, is that
  463. //
  464. if (fontsize && !attributes.getExplicit('mathsize')) {
  465. mathsize = fontsize;
  466. }
  467. //
  468. // Incorporate the mathsize, if any
  469. //
  470. if (mathsize !== '1') {
  471. scale *= this.length2em(mathsize, 1, 1);
  472. }
  473. //
  474. // Record the scaling factors and set the element's CSS
  475. //
  476. this.bbox.scale = scale;
  477. this.bbox.rscale = scale / pscale;
  478. }
  479. /**
  480. * Sets the spacing based on TeX or MathML algorithm
  481. */
  482. protected getSpace() {
  483. const isTop = this.isTopEmbellished();
  484. const hasSpacing = this.node.hasSpacingAttributes();
  485. if (this.jax.options.mathmlSpacing || hasSpacing) {
  486. isTop && this.getMathMLSpacing();
  487. } else {
  488. this.getTeXSpacing(isTop, hasSpacing);
  489. }
  490. }
  491. /**
  492. * Get the spacing using MathML rules based on the core MO
  493. */
  494. protected getMathMLSpacing() {
  495. const node = this.node.coreMO() as MmlMo;
  496. //
  497. // If the mo is not within a multi-node mrow, don't add space
  498. //
  499. const child = node.coreParent();
  500. const parent = child.parent;
  501. if (!parent || !parent.isKind('mrow') || parent.childNodes.length === 1) return;
  502. //
  503. // Get the lspace and rspace
  504. //
  505. const attributes = node.attributes;
  506. const isScript = (attributes.get('scriptlevel') > 0);
  507. this.bbox.L = (attributes.isSet('lspace') ?
  508. Math.max(0, this.length2em(attributes.get('lspace'))) :
  509. MathMLSpace(isScript, node.lspace));
  510. this.bbox.R = (attributes.isSet('rspace') ?
  511. Math.max(0, this.length2em(attributes.get('rspace'))) :
  512. MathMLSpace(isScript, node.rspace));
  513. //
  514. // If there are two adjacent <mo>, use enough left space to make it
  515. // the maximum of the rspace of the first and lspace of the second
  516. //
  517. const n = parent.childIndex(child);
  518. if (n === 0) return;
  519. const prev = parent.childNodes[n - 1] as AbstractMmlNode;
  520. if (!prev.isEmbellished) return;
  521. const bbox = this.jax.nodeMap.get(prev).getBBox();
  522. if (bbox.R) {
  523. this.bbox.L = Math.max(0, this.bbox.L - bbox.R);
  524. }
  525. }
  526. /**
  527. * Get the spacing using the TeX rules
  528. *
  529. * @parm {boolean} isTop True when this is a top-level embellished operator
  530. * @parm {boolean} hasSpacing True when there is an explicit or inherited 'form' attribute
  531. */
  532. protected getTeXSpacing(isTop: boolean, hasSpacing: boolean) {
  533. if (!hasSpacing) {
  534. const space = this.node.texSpacing();
  535. if (space) {
  536. this.bbox.L = this.length2em(space);
  537. }
  538. }
  539. if (isTop || hasSpacing) {
  540. const attributes = this.node.coreMO().attributes;
  541. if (attributes.isSet('lspace')) {
  542. this.bbox.L = Math.max(0, this.length2em(attributes.get('lspace')));
  543. }
  544. if (attributes.isSet('rspace')) {
  545. this.bbox.R = Math.max(0, this.length2em(attributes.get('rspace')));
  546. }
  547. }
  548. }
  549. /**
  550. * @return {boolean} True if this is the top-most container of an embellished operator that is
  551. * itself an embellished operator (the maximal embellished operator for its core)
  552. */
  553. protected isTopEmbellished(): boolean {
  554. return (this.node.isEmbellished &&
  555. !(this.node.parent && this.node.parent.isEmbellished));
  556. }
  557. /*******************************************************************/
  558. /**
  559. * @return {CommonWrapper} The wrapper for this node's core node
  560. */
  561. public core(): CommonWrapper<J, W, C, CC, DD, FD> {
  562. return this.jax.nodeMap.get(this.node.core());
  563. }
  564. /**
  565. * @return {CommonWrapper} The wrapper for this node's core <mo> node
  566. */
  567. public coreMO(): CommonWrapper<J, W, C, CC, DD, FD> {
  568. return this.jax.nodeMap.get(this.node.coreMO());
  569. }
  570. /**
  571. * @return {string} For a token node, the combined text content of the node's children
  572. */
  573. public getText(): string {
  574. let text = '';
  575. if (this.node.isToken) {
  576. for (const child of this.node.childNodes) {
  577. if (child instanceof TextNode) {
  578. text += child.getText();
  579. }
  580. }
  581. }
  582. return text;
  583. }
  584. /**
  585. * @param {DIRECTION} direction The direction to stretch this node
  586. * @return {boolean} Whether the node can stretch in that direction
  587. */
  588. public canStretch(direction: DIRECTION): boolean {
  589. this.stretch = NOSTRETCH as DD;
  590. if (this.node.isEmbellished) {
  591. let core = this.core();
  592. if (core && core.node !== this.node) {
  593. if (core.canStretch(direction)) {
  594. this.stretch = core.stretch;
  595. }
  596. }
  597. }
  598. return this.stretch.dir !== DIRECTION.None;
  599. }
  600. /**
  601. * @return {[string, number]} The alignment and indentation shift for the expression
  602. */
  603. protected getAlignShift(): [string, number] {
  604. let {indentalign, indentshift, indentalignfirst, indentshiftfirst} =
  605. this.node.attributes.getList(...indentAttributes) as StringMap;
  606. if (indentalignfirst !== 'indentalign') {
  607. indentalign = indentalignfirst;
  608. }
  609. if (indentalign === 'auto') {
  610. indentalign = this.jax.options.displayAlign;
  611. }
  612. if (indentshiftfirst !== 'indentshift') {
  613. indentshift = indentshiftfirst;
  614. }
  615. if (indentshift === 'auto') {
  616. indentshift = this.jax.options.displayIndent;
  617. if (indentalign === 'right' && !indentshift.match(/^\s*0[a-z]*\s*$/)) {
  618. indentshift = ('-' + indentshift.trim()).replace(/^--/, '');
  619. }
  620. }
  621. const shift = this.length2em(indentshift, this.metrics.containerWidth);
  622. return [indentalign, shift] as [string, number];
  623. }
  624. /**
  625. * @param {number} W The total width
  626. * @param {BBox} bbox The bbox to be aligned
  627. * @param {string} align How to align (left, center, right)
  628. * @return {number} The x position of the aligned width
  629. */
  630. protected getAlignX(W: number, bbox: BBox, align: string): number {
  631. return (align === 'right' ? W - (bbox.w + bbox.R) * bbox.rscale :
  632. align === 'left' ? bbox.L * bbox.rscale :
  633. (W - bbox.w * bbox.rscale) / 2);
  634. }
  635. /**
  636. * @param {number} H The total height
  637. * @param {number} D The total depth
  638. * @param {number} h The height to be aligned
  639. * @param {number} d The depth to be aligned
  640. * @param {string} align How to align (top, bottom, center, axis, baseline)
  641. * @return {number} The y position of the aligned baseline
  642. */
  643. protected getAlignY(H: number, D: number, h: number, d: number, align: string): number {
  644. return (align === 'top' ? H - h :
  645. align === 'bottom' ? d - D :
  646. align === 'center' ? ((H - h) - (D - d)) / 2 :
  647. 0); // baseline and axis
  648. }
  649. /**
  650. * @param {number} i The index of the child element whose container is needed
  651. * @return {number} The inner width as a container (for percentage widths)
  652. */
  653. public getWrapWidth(i: number): number {
  654. return this.childNodes[i].getBBox().w;
  655. }
  656. /**
  657. * @param {number} i The index of the child element whose container is needed
  658. * @return {string} The alignment child element
  659. */
  660. public getChildAlign(_i: number): string {
  661. return 'left';
  662. }
  663. /*******************************************************************/
  664. /*
  665. * Easy access to some utility routines
  666. */
  667. /**
  668. * @param {number} m A number to be shown as a percent
  669. * @return {string} The number m as a percent
  670. */
  671. protected percent(m: number): string {
  672. return LENGTHS.percent(m);
  673. }
  674. /**
  675. * @param {number} m A number to be shown in ems
  676. * @return {string} The number with units of ems
  677. */
  678. protected em(m: number): string {
  679. return LENGTHS.em(m);
  680. }
  681. /**
  682. * @param {number} m A number of em's to be shown as pixels
  683. * @param {number} M The minimum number of pixels to allow
  684. * @return {string} The number with units of px
  685. */
  686. protected px(m: number, M: number = -LENGTHS.BIGDIMEN): string {
  687. return LENGTHS.px(m, M, this.metrics.em);
  688. }
  689. /**
  690. * @param {Property} length A dimension (giving number and units) or number to be converted to ems
  691. * @param {number} size The default size of the dimension (for percentage values)
  692. * @param {number} scale The current scaling factor (to handle absolute units)
  693. * @return {number} The dimension converted to ems
  694. */
  695. protected length2em(length: Property, size: number = 1, scale: number = null): number {
  696. if (scale === null) {
  697. scale = this.bbox.scale;
  698. }
  699. return LENGTHS.length2em(length as string, size, scale, this.jax.pxPerEm);
  700. }
  701. /**
  702. * @param {string} text The text to turn into unicode locations
  703. * @param {string} name The name of the variant for the characters
  704. * @return {number[]} Array of numbers represeting the string's unicode character positions
  705. */
  706. protected unicodeChars(text: string, name: string = this.variant): number[] {
  707. let chars = unicodeChars(text);
  708. //
  709. // Remap to Math Alphanumerics block
  710. //
  711. const variant = this.font.getVariant(name);
  712. if (variant && variant.chars) {
  713. const map = variant.chars;
  714. //
  715. // Is map[n] doesn't exist, (map[n] || []) still gives an CharData array.
  716. // If the array doesn't have a CharOptions element use {} instead.
  717. // Then check if the options has an smp property, which gives
  718. // the Math Alphabet mapping for this character.
  719. // Otherwise use the original code point, n.
  720. //
  721. chars = chars.map((n) => ((map[n] || [])[3] || {}).smp || n);
  722. }
  723. return chars;
  724. }
  725. /**
  726. * @param {number[]} chars The array of unicode character numbers to remap
  727. * @return {number[]} The converted array
  728. */
  729. public remapChars(chars: number[]): number[] {
  730. return chars;
  731. }
  732. /**
  733. * @param {string} text The text from which to create a TextNode object
  734. * @return {TextNode} The TextNode with the given text
  735. */
  736. public mmlText(text: string): TextNode {
  737. return ((this.node as AbstractMmlNode).factory.create('text') as TextNode).setText(text);
  738. }
  739. /**
  740. * @param {string} kind The kind of MmlNode to create
  741. * @param {ProperyList} properties The properties to set initially
  742. * @param {MmlNode[]} children The child nodes to add to the created node
  743. * @return {MmlNode} The newly created MmlNode
  744. */
  745. public mmlNode(kind: string, properties: PropertyList = {}, children: MmlNode[] = []): MmlNode {
  746. return (this.node as AbstractMmlNode).factory.create(kind, properties, children);
  747. }
  748. /**
  749. * Create an mo wrapper with the given text,
  750. * link it in, and give it the right defaults.
  751. *
  752. * @param {string} text The text for the wrapped element
  753. * @return {CommonWrapper} The wrapped MmlMo node
  754. */
  755. protected createMo(text: string): CommonWrapper<J, W, C, CC, DD, FD> {
  756. const mmlFactory = (this.node as AbstractMmlNode).factory;
  757. const textNode = (mmlFactory.create('text') as TextNode).setText(text);
  758. const mml = mmlFactory.create('mo', {stretchy: true}, [textNode]);
  759. mml.inheritAttributesFrom(this.node);
  760. const node = this.wrap(mml);
  761. node.parent = this as any as W;
  762. return node;
  763. }
  764. /**
  765. * @param {string} variant The variant in which to look for the character
  766. * @param {number} n The number of the character to look up
  767. * @return {CharData} The full CharData object, with CharOptions guaranteed to be defined
  768. */
  769. protected getVariantChar(variant: string, n: number): CharData<CC> {
  770. const char = this.font.getChar(variant, n) || [0, 0, 0, {unknown: true} as CC];
  771. if (char.length === 3) {
  772. (char as any)[3] = {};
  773. }
  774. return char as [number, number, number, CC];
  775. }
  776. }