mo.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413
  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 CommonMo wrapper mixin for the MmlMo object
  19. *
  20. * @author dpvc@mathjax.org (Davide Cervone)
  21. */
  22. import {AnyWrapper, WrapperConstructor, Constructor} from '../Wrapper.js';
  23. import {MmlMo} from '../../../core/MmlTree/MmlNodes/mo.js';
  24. import {BBox} from '../../../util/BBox.js';
  25. import {unicodeChars} from '../../../util/string.js';
  26. import {DelimiterData} from '../FontData.js';
  27. import {DIRECTION, NOSTRETCH} from '../FontData.js';
  28. /*****************************************************************/
  29. /**
  30. * Convert direction to letter
  31. */
  32. export const DirectionVH: {[n: number]: string} = {
  33. [DIRECTION.Vertical]: 'v',
  34. [DIRECTION.Horizontal]: 'h'
  35. };
  36. /*****************************************************************/
  37. /**
  38. * The CommonMo interface
  39. */
  40. export interface CommonMo extends AnyWrapper {
  41. /**
  42. * The font size that a stretched operator uses.
  43. * If -1, then stretch arbitrarily, and bbox gives the actual height, depth, width
  44. */
  45. size: number;
  46. /**
  47. * True if used as an accent in an munderover construct
  48. */
  49. isAccent: boolean;
  50. /**
  51. * Get the (unmodified) bbox of the contents (before centering or setting accents to width 0)
  52. *
  53. * @param {BBox} bbox The bbox to fill
  54. */
  55. protoBBox(bbox: BBox): void;
  56. /**
  57. * @return {number} Offset to the left by half the actual width of the accent
  58. */
  59. getAccentOffset(): number;
  60. /**
  61. * @param {BBox} bbox The bbox to center, or null to compute the bbox
  62. * @return {number} The offset to move the glyph to center it
  63. */
  64. getCenterOffset(bbox?: BBox): number;
  65. /**
  66. * Determint variant for vertically/horizontally stretched character
  67. *
  68. * @param {number[]} WH size to stretch to, either [W] or [H, D]
  69. * @param {boolean} exact True if not allowed to use delimiter factor and shortfall
  70. */
  71. getStretchedVariant(WH: number[], exact?: boolean): void;
  72. /**
  73. * @param {string} name The name of the attribute to get
  74. * @param {number} value The default value to use
  75. * @return {number} The size in em's of the attribute (or the default value)
  76. */
  77. getSize(name: string, value: number): number;
  78. /**
  79. * @param {number[]} WH Either [W] for width, [H, D] for height and depth, or [] for min/max size
  80. * @return {number} Either the width or the total height of the character
  81. */
  82. getWH(WH: number[]): number;
  83. /**
  84. * @param {number[]} WHD The [W] or [H, D] being requested from the parent mrow
  85. * @param {number} D The full dimension (including symmetry, etc)
  86. * @param {DelimiterData} C The delimiter data for the stretchy character
  87. */
  88. getStretchBBox(WHD: number[], D: number, C: DelimiterData): void;
  89. /**
  90. * @param {number[]} WHD The [H, D] being requested from the parent mrow
  91. * @param {number} HD The full height (including symmetry, etc)
  92. * @param {DelimiterData} C The delimiter data for the stretchy character
  93. * @return {number[]} The height and depth for the vertically stretched delimiter
  94. */
  95. getBaseline(WHD: number[], HD: number, C: DelimiterData): number[];
  96. /**
  97. * Determine the size of the delimiter based on whether full extenders should be used or not.
  98. *
  99. * @param {number} D The requested size of the delimiter
  100. * @param {DelimiterData} C The data for the delimiter
  101. * @return {number} The final size of the assembly
  102. */
  103. checkExtendedHeight(D: number, C: DelimiterData): number;
  104. }
  105. /**
  106. * Shorthand for the CommonMo constructor
  107. */
  108. export type MoConstructor = Constructor<CommonMo>;
  109. /*****************************************************************/
  110. /**
  111. * The CommomMo wrapper mixin for the MmlMo object
  112. *
  113. * @template T The Wrapper class constructor type
  114. */
  115. export function CommonMoMixin<T extends WrapperConstructor>(Base: T): MoConstructor & T {
  116. return class extends Base {
  117. /**
  118. * The font size that a stretched operator uses.
  119. * If -1, then stretch arbitrarily, and bbox gives the actual height, depth, width
  120. */
  121. public size: number = null;
  122. /**
  123. * True if used as an accent in an munderover construct
  124. */
  125. public isAccent: boolean;
  126. /**
  127. * @override
  128. */
  129. constructor(...args: any[]) {
  130. super(...args);
  131. this.isAccent = (this.node as MmlMo).isAccent;
  132. }
  133. /**
  134. * @override
  135. */
  136. public computeBBox(bbox: BBox, _recompute: boolean = false) {
  137. this.protoBBox(bbox);
  138. if (this.node.attributes.get('symmetric') &&
  139. this.stretch.dir !== DIRECTION.Horizontal) {
  140. const d = this.getCenterOffset(bbox);
  141. bbox.h += d;
  142. bbox.d -= d;
  143. }
  144. if (this.node.getProperty('mathaccent') &&
  145. (this.stretch.dir === DIRECTION.None || this.size >= 0)) {
  146. bbox.w = 0;
  147. }
  148. }
  149. /**
  150. * Get the (unmodified) bbox of the contents (before centering or setting accents to width 0)
  151. *
  152. * @param {BBox} bbox The bbox to fill
  153. */
  154. public protoBBox(bbox: BBox) {
  155. const stretchy = (this.stretch.dir !== DIRECTION.None);
  156. if (stretchy && this.size === null) {
  157. this.getStretchedVariant([0]);
  158. }
  159. if (stretchy && this.size < 0) return;
  160. super.computeBBox(bbox);
  161. this.copySkewIC(bbox);
  162. }
  163. /**
  164. * @return {number} Offset to the left by half the actual width of the accent
  165. */
  166. public getAccentOffset(): number {
  167. const bbox = BBox.empty();
  168. this.protoBBox(bbox);
  169. return -bbox.w / 2;
  170. }
  171. /**
  172. * @param {BBox} bbox The bbox to center, or null to compute the bbox
  173. * @return {number} The offset to move the glyph to center it
  174. */
  175. public getCenterOffset(bbox: BBox = null): number {
  176. if (!bbox) {
  177. bbox = BBox.empty();
  178. super.computeBBox(bbox);
  179. }
  180. return ((bbox.h + bbox.d) / 2 + this.font.params.axis_height) - bbox.h;
  181. }
  182. /**
  183. * @override
  184. */
  185. public getVariant() {
  186. if (this.node.attributes.get('largeop')) {
  187. this.variant = (this.node.attributes.get('displaystyle') ? '-largeop' : '-smallop');
  188. return;
  189. }
  190. if (!this.node.attributes.getExplicit('mathvariant') &&
  191. this.node.getProperty('pseudoscript') === false) {
  192. this.variant = '-tex-variant';
  193. return;
  194. }
  195. super.getVariant();
  196. }
  197. /**
  198. * @override
  199. */
  200. public canStretch(direction: DIRECTION) {
  201. if (this.stretch.dir !== DIRECTION.None) {
  202. return this.stretch.dir === direction;
  203. }
  204. const attributes = this.node.attributes;
  205. if (!attributes.get('stretchy')) return false;
  206. const c = this.getText();
  207. if (Array.from(c).length !== 1) return false;
  208. const delim = this.font.getDelimiter(c.codePointAt(0));
  209. this.stretch = (delim && delim.dir === direction ? delim : NOSTRETCH);
  210. return this.stretch.dir !== DIRECTION.None;
  211. }
  212. /**
  213. * Determint variant for vertically/horizontally stretched character
  214. *
  215. * @param {number[]} WH size to stretch to, either [W] or [H, D]
  216. * @param {boolean} exact True if not allowed to use delimiter factor and shortfall
  217. */
  218. public getStretchedVariant(WH: number[], exact: boolean = false) {
  219. if (this.stretch.dir !== DIRECTION.None) {
  220. let D = this.getWH(WH);
  221. const min = this.getSize('minsize', 0);
  222. const max = this.getSize('maxsize', Infinity);
  223. const mathaccent = this.node.getProperty('mathaccent');
  224. //
  225. // Clamp the dimension to the max and min
  226. // then get the target size via TeX rules
  227. //
  228. D = Math.max(min, Math.min(max, D));
  229. const df = this.font.params.delimiterfactor / 1000;
  230. const ds = this.font.params.delimitershortfall;
  231. const m = (min || exact ? D : mathaccent ? Math.min(D / df, D + ds) : Math.max(D * df, D - ds));
  232. //
  233. // Look through the delimiter sizes for one that matches
  234. //
  235. const delim = this.stretch;
  236. const c = delim.c || this.getText().codePointAt(0);
  237. let i = 0;
  238. if (delim.sizes) {
  239. for (const d of delim.sizes) {
  240. if (d >= m) {
  241. if (mathaccent && i) {
  242. i--;
  243. }
  244. this.variant = this.font.getSizeVariant(c, i);
  245. this.size = i;
  246. if (delim.schar && delim.schar[i]) {
  247. this.stretch = {...this.stretch, c: delim.schar[i]};
  248. }
  249. return;
  250. }
  251. i++;
  252. }
  253. }
  254. //
  255. // No size matches, so if we can make multi-character delimiters,
  256. // record the data for that, otherwise, use the largest fixed size.
  257. //
  258. if (delim.stretch) {
  259. this.size = -1;
  260. this.invalidateBBox();
  261. this.getStretchBBox(WH, this.checkExtendedHeight(D, delim), delim);
  262. } else {
  263. this.variant = this.font.getSizeVariant(c, i - 1);
  264. this.size = i - 1;
  265. }
  266. }
  267. }
  268. /**
  269. * @param {string} name The name of the attribute to get
  270. * @param {number} value The default value to use
  271. * @return {number} The size in em's of the attribute (or the default value)
  272. */
  273. public getSize(name: string, value: number): number {
  274. let attributes = this.node.attributes;
  275. if (attributes.isSet(name)) {
  276. value = this.length2em(attributes.get(name), 1, 1); // FIXME: should use height of actual character
  277. }
  278. return value;
  279. }
  280. /**
  281. * @param {number[]} WH Either [W] for width, [H, D] for height and depth, or [] for min/max size
  282. * @return {number} Either the width or the total height of the character
  283. */
  284. public getWH(WH: number[]): number {
  285. if (WH.length === 0) return 0;
  286. if (WH.length === 1) return WH[0];
  287. let [H, D] = WH;
  288. const a = this.font.params.axis_height;
  289. return (this.node.attributes.get('symmetric') ? 2 * Math.max(H - a, D + a) : H + D);
  290. }
  291. /**
  292. * @param {number[]} WHD The [W] or [H, D] being requested from the parent mrow
  293. * @param {number} D The full dimension (including symmetry, etc)
  294. * @param {DelimiterData} C The delimiter data for the stretchy character
  295. */
  296. public getStretchBBox(WHD: number[], D: number, C: DelimiterData) {
  297. if (C.hasOwnProperty('min') && C.min > D) {
  298. D = C.min;
  299. }
  300. let [h, d, w] = C.HDW;
  301. if (this.stretch.dir === DIRECTION.Vertical) {
  302. [h, d] = this.getBaseline(WHD, D, C);
  303. } else {
  304. w = D;
  305. }
  306. this.bbox.h = h;
  307. this.bbox.d = d;
  308. this.bbox.w = w;
  309. }
  310. /**
  311. * @param {number[]} WHD The [H, D] being requested from the parent mrow
  312. * @param {number} HD The full height (including symmetry, etc)
  313. * @param {DelimiterData} C The delimiter data for the stretchy character
  314. * @return {[number, number]} The height and depth for the vertically stretched delimiter
  315. */
  316. public getBaseline(WHD: number[], HD: number, C: DelimiterData): [number, number] {
  317. const hasWHD = (WHD.length === 2 && WHD[0] + WHD[1] === HD);
  318. const symmetric = this.node.attributes.get('symmetric');
  319. const [H, D] = (hasWHD ? WHD : [HD, 0]);
  320. let [h, d] = [H + D, 0];
  321. if (symmetric) {
  322. //
  323. // Center on the math axis
  324. //
  325. const a = this.font.params.axis_height;
  326. if (hasWHD) {
  327. h = 2 * Math.max(H - a, D + a);
  328. }
  329. d = h / 2 - a;
  330. } else if (hasWHD) {
  331. //
  332. // Use the given depth (from mrow)
  333. //
  334. d = D;
  335. } else {
  336. //
  337. // Use depth proportional to the normal-size character
  338. // (when stretching for minsize or maxsize by itself)
  339. //
  340. let [ch, cd] = (C.HDW || [.75, .25]);
  341. d = cd * (h / (ch + cd));
  342. }
  343. return [h - d, d];
  344. }
  345. /**
  346. * @override
  347. */
  348. public checkExtendedHeight(D: number, C: DelimiterData): number {
  349. if (C.fullExt) {
  350. const [extSize, endSize] = C.fullExt;
  351. const n = Math.ceil(Math.max(0, D - endSize) / extSize);
  352. D = endSize + n * extSize;
  353. }
  354. return D;
  355. }
  356. /**
  357. * @override
  358. */
  359. public remapChars(chars: number[]) {
  360. const primes = this.node.getProperty('primes') as string;
  361. if (primes) {
  362. return unicodeChars(primes);
  363. }
  364. if (chars.length === 1) {
  365. const parent = (this.node as MmlMo).coreParent().parent;
  366. const isAccent = this.isAccent && !parent.isKind('mrow');
  367. const map = (isAccent ? 'accent' : 'mo');
  368. const text = this.font.getRemappedChar(map, chars[0]);
  369. if (text) {
  370. chars = this.unicodeChars(text, this.variant);
  371. }
  372. }
  373. return chars;
  374. }
  375. };
  376. }