mtable.ts 38 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177
  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 CommonMtable wrapper mixin for the MmlMtable object
  19. *
  20. * @author dpvc@mathjax.org (Davide Cervone)
  21. */
  22. import {AnyWrapper, WrapperConstructor, Constructor} from '../Wrapper.js';
  23. import {CommonMtr} from './mtr.js';
  24. import {CommonMo} from './mo.js';
  25. import {BBox} from '../../../util/BBox.js';
  26. import {DIRECTION} from '../FontData.js';
  27. import {split, isPercent} from '../../../util/string.js';
  28. import {sum, max} from '../../../util/numeric.js';
  29. /*****************************************************************/
  30. /**
  31. * The heights, depths, and widths of the rows and columns
  32. * Plus the natural height and depth (i.e., without the labels)
  33. * Plus the label column width
  34. */
  35. export type TableData = {
  36. H: number[];
  37. D: number[];
  38. W: number[];
  39. NH: number[];
  40. ND: number[];
  41. L: number;
  42. };
  43. /**
  44. * An array of table dimensions
  45. */
  46. export type ColumnWidths = (string | number | null)[];
  47. /*****************************************************************/
  48. /**
  49. * The CommonMtable interface
  50. *
  51. * @template C The class for table cells
  52. * @template R The class for table rows
  53. */
  54. export interface CommonMtable<C extends AnyWrapper, R extends CommonMtr<C>> extends AnyWrapper {
  55. /**
  56. * The number of columns and rows in the table
  57. */
  58. numCols: number;
  59. numRows: number;
  60. /**
  61. * True if there are labeled rows
  62. */
  63. hasLabels: boolean;
  64. /**
  65. * True if this mtable is the top element, or in a top-most mrow
  66. */
  67. isTop: boolean;
  68. /**
  69. * The parent node of this table (skipping non-parents and mrows)
  70. * and the position of the table as a child node
  71. */
  72. container: AnyWrapper;
  73. containerI: number;
  74. /**
  75. * The spacing and line data
  76. */
  77. frame: boolean;
  78. fLine: number;
  79. fSpace: number[];
  80. cSpace: number[];
  81. rSpace: number[];
  82. cLines: number[];
  83. rLines: number[];
  84. cWidths: (number | string)[];
  85. /**
  86. * The bounding box information for the table rows and columns
  87. */
  88. data: TableData;
  89. /**
  90. * The table cells that have percentage-width content
  91. */
  92. pwidthCells: [C, number][];
  93. /**
  94. * The full width of a percentage-width table
  95. */
  96. pWidth: number;
  97. /**
  98. * The rows of the table
  99. */
  100. readonly tableRows: R[];
  101. /**
  102. * @override
  103. */
  104. childNodes: R[];
  105. /**
  106. * Find the container and the child position of the table
  107. */
  108. findContainer(): void;
  109. /**
  110. * If the table has a precentage width or has labels, set the pwidth of the bounding box
  111. */
  112. getPercentageWidth(): void;
  113. /**
  114. * Stretch the rows to the equal height or natural height
  115. */
  116. stretchRows(): void;
  117. /**
  118. * Stretch the columns to their proper widths
  119. */
  120. stretchColumns(): void;
  121. /**
  122. * Handle horizontal stretching within the ith column
  123. *
  124. * @param {number} i The column number
  125. * @param {number} W The computed width of the column (or null of not computed)
  126. */
  127. stretchColumn(i: number, W: number): void;
  128. /**
  129. * Determine the row heights and depths, the column widths,
  130. * and the natural width and height of the table.
  131. *
  132. * @return {TableData} The dimensions of the rows and columns
  133. */
  134. getTableData(): TableData;
  135. /**
  136. * @param {C} cell The cell whose height, depth, and width are to be added into the H, D, W arrays
  137. * @param {number} i The column number for the cell
  138. * @param {number} j The row number for the cell
  139. * @param {string} align The row alignment
  140. * @param {number[]} H The maximum height for each of the rows
  141. * @param {number[]} D The maximum depth for each of the rows
  142. * @param {number[]} W The maximum width for each column
  143. * @param {number} M The current height for items aligned top and bottom
  144. * @return {number} The updated value for M
  145. */
  146. updateHDW(cell: C, i: number, j: number, align: string, H: number[], D: number[], W: number[], M: number): number;
  147. /**
  148. * Extend the H and D of a row to cover the maximum height needed by top/bottom aligned items
  149. *
  150. * @param {number} i The row whose hight and depth should be adjusted
  151. * @param {number[]} H The row heights
  152. * @param {number[]} D The row depths
  153. * @param {number} M The maximum height of top/bottom aligned items
  154. */
  155. extendHD(i: number, H: number[], D: number[], M: number): void;
  156. /**
  157. * Set cell widths for columns with percentage width children
  158. */
  159. setColumnPWidths(): void;
  160. /**
  161. * @param {number} height The total height of the table
  162. * @return {number[]} The [height, depth] for the aligned table
  163. */
  164. getBBoxHD(height: number): number[];
  165. /**
  166. * Get bbox left and right amounts to cover labels
  167. */
  168. getBBoxLR(): number[];
  169. /**
  170. * @param {string} side The side for the labels
  171. * @return {[number, string, number]} The padding, alignment, and shift amounts
  172. */
  173. getPadAlignShift(side: string): [number, string, number];
  174. /**
  175. * @return {number} The true width of the table (without labels)
  176. */
  177. getWidth(): number;
  178. /**
  179. * @return {number} The maximum height of a row
  180. */
  181. getEqualRowHeight(): number;
  182. /**
  183. * @return {number[]} The array of computed widths
  184. */
  185. getComputedWidths(): number[];
  186. /**
  187. * Determine the column widths that can be computed (and need to be set).
  188. * The resulting arrays will have numbers for fixed-size arrays,
  189. * strings for percentage sizes that can't be determined now,
  190. * and null for stretchy columns tht will expand to fill the extra space.
  191. * Depending on the width specified for the table, different column
  192. * values can be determined.
  193. *
  194. * @return {ColumnWidths} The array of widths
  195. */
  196. getColumnWidths(): ColumnWidths;
  197. /**
  198. * For tables with equal columns, get the proper amount per row.
  199. *
  200. * @return {ColumnWidths} The array of widths
  201. */
  202. getEqualColumns(width: string): ColumnWidths;
  203. /**
  204. * For tables with width="auto", auto and fit columns
  205. * will end up being natural width, so don't need to
  206. * set those explicitly.
  207. *
  208. * @return {ColumnWidths} The array of widths
  209. */
  210. getColumnWidthsAuto(swidths: string[]): ColumnWidths;
  211. /**
  212. * For tables with percentage widths, let 'fit' columns (or 'auto'
  213. * columns if there are not 'fit' ones) will stretch automatically,
  214. * but for 'auto' columns (when there are 'fit' ones), set the size
  215. * to the natural size of the column.
  216. *
  217. * @param {string[]} widths Strings giving the widths
  218. * @return {ColumnWidths} The array of widths
  219. */
  220. getColumnWidthsPercent(widths: string[]): ColumnWidths;
  221. /**
  222. * For fixed-width tables, compute the column widths of all columns.
  223. *
  224. * @return {ColumnWidths} The array of widths
  225. */
  226. getColumnWidthsFixed(swidths: string[], width: number): ColumnWidths;
  227. /**
  228. * @param {number} i The row number (starting at 0)
  229. * @param {string} align The alignment on that row
  230. * @return {number} The offest of the alignment position from the top of the table
  231. */
  232. getVerticalPosition(i: number, align: string): number;
  233. /**
  234. * @param {number} fspace The frame spacing to use
  235. * @param {number[]} space The array of spacing values to convert to strings
  236. * @param {number} scale A scaling factor to use for the sizes
  237. * @return {string[]} The half-spacing as stings with units of "em"
  238. * with frame spacing at the beginning and end
  239. */
  240. getEmHalfSpacing(fspace: number, space: number[], scale?: number): string[];
  241. /**
  242. * @return {number[]} The half-spacing for rows with frame spacing at the ends
  243. */
  244. getRowHalfSpacing(): number[];
  245. /**
  246. * @return {number[]} The half-spacing for columns with frame spacing at the ends
  247. */
  248. getColumnHalfSpacing(): number[];
  249. /**
  250. * @return {[string,number|null]} The alignment and row number (based at 0) or null
  251. */
  252. getAlignmentRow(): [string, number | null];
  253. /**
  254. * @param {string} name The name of the attribute to get as an array
  255. * @param {number=} i Return this many fewer than numCols entries
  256. * @return {string[]} The array of values in the given attribute, split at spaces,
  257. * padded to the number of table columns (minus 1) by repeating the last entry
  258. */
  259. getColumnAttributes(name: string, i?: number): string[];
  260. /**
  261. * @param {string} name The name of the attribute to get as an array
  262. * @param {number=} i Return this many fewer than numRows entries
  263. * @return {string[]} The array of values in the given attribute, split at spaces,
  264. * padded to the number of table rows (minus 1) by repeating the last entry
  265. */
  266. getRowAttributes(name: string, i?: number): string[];
  267. /**
  268. * @param {string} name The name of the attribute to get as an array
  269. * @return {string[]} The array of values in the given attribute, split at spaces
  270. * (after leading and trailing spaces are removed, and multiple
  271. * spaces have been collapsed to one).
  272. */
  273. getAttributeArray(name: string): string[];
  274. /**
  275. * Adds "em" to a list of dimensions, after dividing by n (defaults to 1).
  276. *
  277. * @param {string[]} list The array of dimensions (in em's)
  278. * @param {nunber=} n The number to divide each dimension by after converted
  279. * @return {string[]} The array of values with "em" added
  280. */
  281. addEm(list: number[], n?: number): string[];
  282. /**
  283. * Converts an array of dimensions (with arbitrary units) to an array of numbers
  284. * representing the dimensions in units of em's.
  285. *
  286. * @param {string[]} list The array of dimensions to be turned into em's
  287. * @return {number[]} The array of values converted to em's
  288. */
  289. convertLengths(list: string[]): number[];
  290. }
  291. /**
  292. * Shorthand for the CommonMtable constructor
  293. */
  294. export type MtableConstructor<C extends AnyWrapper, R extends CommonMtr<C>> = Constructor<CommonMtable<C, R>>;
  295. /*****************************************************************/
  296. /**
  297. * The CommonMtable wrapper mixin for the MmlMtable object
  298. *
  299. * @template C The table cell class
  300. * @temlpate R the table row class
  301. * @template T The Wrapper class constructor type
  302. */
  303. export function CommonMtableMixin<
  304. C extends AnyWrapper,
  305. R extends CommonMtr<C>,
  306. T extends WrapperConstructor
  307. >(Base: T): MtableConstructor<C, R> & T {
  308. return class extends Base {
  309. /**
  310. * The number of columns in the table
  311. */
  312. public numCols: number = 0;
  313. /**
  314. * The number of rows in the table
  315. */
  316. public numRows: number = 0;
  317. /**
  318. * True if there are labeled rows
  319. */
  320. public hasLabels: boolean;
  321. /**
  322. * True if this mtable is the top element, or in a top-most mrow
  323. */
  324. public isTop: boolean;
  325. /**
  326. * The parent node of this table (skipping non-parents and mrows)
  327. */
  328. public container: AnyWrapper;
  329. /**
  330. * The position of the table as a child node of its container
  331. */
  332. public containerI: number;
  333. /**
  334. * True if there is a frame
  335. */
  336. public frame: boolean;
  337. /**
  338. * The size of the frame line (or 0 if none)
  339. */
  340. public fLine: number;
  341. /**
  342. * frame spacing on the left and right
  343. */
  344. public fSpace: number[];
  345. /**
  346. * The spacing between columns
  347. */
  348. public cSpace: number[];
  349. /**
  350. * The spacing between rows
  351. */
  352. public rSpace: number[];
  353. /**
  354. * The width of columns lines (or 0 if no line for the column)
  355. */
  356. public cLines: number[];
  357. /**
  358. * The width of row lines (or 0 if no lone for that row)
  359. */
  360. public rLines: number[];
  361. /**
  362. * The column widths (or percentages, etc.)
  363. */
  364. public cWidths: (number | string)[];
  365. /**
  366. * The bounding box information for the table rows and columns
  367. */
  368. public data: TableData = null;
  369. /**
  370. * The table cells that have percentage-width content
  371. */
  372. public pwidthCells: [C, number][] = [];
  373. /**
  374. * The full width of a percentage-width table
  375. */
  376. public pWidth: number = 0;
  377. /**
  378. * @return {R[]} The rows of the table
  379. */
  380. get tableRows(): R[] {
  381. return this.childNodes;
  382. }
  383. /******************************************************************/
  384. /**
  385. * @override
  386. * @constructor
  387. */
  388. constructor(...args: any[]) {
  389. super(...args);
  390. //
  391. // Determine the number of columns and rows, and whether the table is stretchy
  392. //
  393. this.numCols = max(this.tableRows.map(row => row.numCells));
  394. this.numRows = this.childNodes.length;
  395. this.hasLabels = this.childNodes.reduce((value, row) => value || row.node.isKind('mlabeledtr'), false);
  396. this.findContainer();
  397. this.isTop = !this.container || (this.container.node.isKind('math') && !this.container.parent);
  398. if (this.isTop) {
  399. this.jax.table = this;
  400. }
  401. this.getPercentageWidth();
  402. //
  403. // Get the frame, row, and column parameters
  404. //
  405. const attributes = this.node.attributes;
  406. this.frame = attributes.get('frame') !== 'none';
  407. this.fLine = (this.frame && attributes.get('frame') ? .07 : 0);
  408. this.fSpace = (this.frame ? this.convertLengths(this.getAttributeArray('framespacing')) : [0, 0]);
  409. this.cSpace = this.convertLengths(this.getColumnAttributes('columnspacing'));
  410. this.rSpace = this.convertLengths(this.getRowAttributes('rowspacing'));
  411. this.cLines = this.getColumnAttributes('columnlines').map(x => (x === 'none' ? 0 : .07));
  412. this.rLines = this.getRowAttributes('rowlines').map(x => (x === 'none' ? 0 : .07));
  413. this.cWidths = this.getColumnWidths();
  414. //
  415. // Stretch the rows and columns
  416. //
  417. this.stretchRows();
  418. this.stretchColumns();
  419. }
  420. /**
  421. * Find the container and the child position of the table
  422. */
  423. public findContainer() {
  424. let node = this as AnyWrapper;
  425. let parent = node.parent as AnyWrapper;
  426. while (parent && (parent.node.notParent || parent.node.isKind('mrow'))) {
  427. node = parent;
  428. parent = parent.parent;
  429. }
  430. this.container = parent;
  431. this.containerI = node.node.childPosition();
  432. }
  433. /**
  434. * If the table has a precentage width or has labels, set the pwidth of the bounding box
  435. */
  436. public getPercentageWidth() {
  437. if (this.hasLabels) {
  438. this.bbox.pwidth = BBox.fullWidth;
  439. } else {
  440. const width = this.node.attributes.get('width') as string;
  441. if (isPercent(width)) {
  442. this.bbox.pwidth = width;
  443. }
  444. }
  445. }
  446. /**
  447. * Stretch the rows to the equal height or natural height
  448. */
  449. public stretchRows() {
  450. const equal = this.node.attributes.get('equalrows') as boolean;
  451. const HD = (equal ? this.getEqualRowHeight() : 0);
  452. const {H, D} = (equal ? this.getTableData() : {H: [0], D: [0]});
  453. const rows = this.tableRows;
  454. for (let i = 0; i < this.numRows; i++) {
  455. const hd = (equal ? [(HD + H[i] - D[i]) / 2, (HD - H[i] + D[i]) / 2] : null);
  456. rows[i].stretchChildren(hd);
  457. }
  458. }
  459. /**
  460. * Stretch the columns to their proper widths
  461. */
  462. public stretchColumns() {
  463. for (let i = 0; i < this.numCols; i++) {
  464. const width = (typeof this.cWidths[i] === 'number' ? this.cWidths[i] as number : null);
  465. this.stretchColumn(i, width);
  466. }
  467. }
  468. /**
  469. * Handle horizontal stretching within the ith column
  470. *
  471. * @param {number} i The column number
  472. * @param {number} W The computed width of the column (or null of not computed)
  473. */
  474. public stretchColumn(i: number, W: number) {
  475. let stretchy: AnyWrapper[] = [];
  476. //
  477. // Locate and count the stretchy children
  478. //
  479. for (const row of this.tableRows) {
  480. const cell = row.getChild(i);
  481. if (cell) {
  482. const child = cell.childNodes[0];
  483. if (child.stretch.dir === DIRECTION.None &&
  484. child.canStretch(DIRECTION.Horizontal)) {
  485. stretchy.push(child);
  486. }
  487. }
  488. }
  489. let count = stretchy.length;
  490. let nodeCount = this.childNodes.length;
  491. if (count && nodeCount > 1) {
  492. if (W === null) {
  493. W = 0;
  494. //
  495. // If all the children are stretchy, find the largest one,
  496. // otherwise, find the width of the non-stretchy children.
  497. //
  498. let all = (count > 1 && count === nodeCount);
  499. for (const row of this.tableRows) {
  500. const cell = row.getChild(i);
  501. if (cell) {
  502. const child = cell.childNodes[0];
  503. const noStretch = (child.stretch.dir === DIRECTION.None);
  504. if (all || noStretch) {
  505. const {w} = child.getBBox(noStretch);
  506. if (w > W) {
  507. W = w;
  508. }
  509. }
  510. }
  511. }
  512. }
  513. //
  514. // Stretch the stretchable children
  515. //
  516. for (const child of stretchy) {
  517. (child.coreMO() as CommonMo).getStretchedVariant([W]);
  518. }
  519. }
  520. }
  521. /******************************************************************/
  522. /**
  523. * Determine the row heights and depths, the column widths,
  524. * and the natural width and height of the table.
  525. *
  526. * @return {TableData} The dimensions of the rows and columns
  527. */
  528. public getTableData(): TableData {
  529. if (this.data) {
  530. return this.data;
  531. }
  532. const H = new Array(this.numRows).fill(0);
  533. const D = new Array(this.numRows).fill(0);
  534. const W = new Array(this.numCols).fill(0);
  535. const NH = new Array(this.numRows);
  536. const ND = new Array(this.numRows);
  537. const LW = [0];
  538. const rows = this.tableRows;
  539. for (let j = 0; j < rows.length; j++) {
  540. let M = 0;
  541. const row = rows[j];
  542. const align = row.node.attributes.get('rowalign') as string;
  543. for (let i = 0; i < row.numCells; i++) {
  544. const cell = row.getChild(i);
  545. M = this.updateHDW(cell, i, j, align, H, D, W, M);
  546. this.recordPWidthCell(cell, i);
  547. }
  548. NH[j] = H[j];
  549. ND[j] = D[j];
  550. if (row.labeled) {
  551. M = this.updateHDW(row.childNodes[0], 0, j, align, H, D, LW, M);
  552. }
  553. this.extendHD(j, H, D, M);
  554. this.extendHD(j, NH, ND, M);
  555. }
  556. const L = LW[0];
  557. this.data = {H, D, W, NH, ND, L};
  558. return this.data;
  559. }
  560. /**
  561. * @override
  562. */
  563. public updateHDW(
  564. cell: C, i: number, j: number, align: string, H: number[], D: number[], W: number[], M: number
  565. ): number {
  566. let {h, d, w} = cell.getBBox();
  567. const scale = cell.parent.bbox.rscale;
  568. if (cell.parent.bbox.rscale !== 1) {
  569. h *= scale;
  570. d *= scale;
  571. w *= scale;
  572. }
  573. if (this.node.getProperty('useHeight')) {
  574. if (h < .75) h = .75;
  575. if (d < .25) d = .25;
  576. }
  577. let m = 0;
  578. align = cell.node.attributes.get('rowalign') as string || align;
  579. if (align !== 'baseline' && align !== 'axis') {
  580. m = h + d;
  581. h = d = 0;
  582. }
  583. if (h > H[j]) H[j] = h;
  584. if (d > D[j]) D[j] = d;
  585. if (m > M) M = m;
  586. if (W && w > W[i]) W[i] = w;
  587. return M;
  588. }
  589. /**
  590. * @override
  591. */
  592. public extendHD(i: number, H: number[], D: number[], M: number) {
  593. const d = (M - (H[i] + D[i])) / 2;
  594. if (d < .00001) return;
  595. H[i] += d;
  596. D[i] += d;
  597. }
  598. /**
  599. * @param {C} cell The cell to check for percentage widths
  600. * @param {number} i The column index of the cell
  601. */
  602. public recordPWidthCell(cell: C, i: number) {
  603. if (cell.childNodes[0] && cell.childNodes[0].getBBox().pwidth) {
  604. this.pwidthCells.push([cell, i]);
  605. }
  606. }
  607. /**
  608. * @override
  609. */
  610. public computeBBox(bbox: BBox, _recompute: boolean = false) {
  611. const {H, D} = this.getTableData();
  612. let height, width;
  613. //
  614. // For equal rows, use the common height and depth for all rows
  615. // Otherwise, use the height and depths for each row separately.
  616. // Add in the spacing, line widths, and frame size.
  617. //
  618. if (this.node.attributes.get('equalrows') as boolean) {
  619. const HD = this.getEqualRowHeight();
  620. height = sum([].concat(this.rLines, this.rSpace)) + HD * this.numRows;
  621. } else {
  622. height = sum(H.concat(D, this.rLines, this.rSpace));
  623. }
  624. height += 2 * (this.fLine + this.fSpace[1]);
  625. //
  626. // Get the widths of all columns
  627. //
  628. const CW = this.getComputedWidths();
  629. //
  630. // Get the expected width of the table
  631. //
  632. width = sum(CW.concat(this.cLines, this.cSpace)) + 2 * (this.fLine + this.fSpace[0]);
  633. //
  634. // If the table width is not 'auto', determine the specified width
  635. // and pick the larger of the specified and computed widths.
  636. //
  637. const w = this.node.attributes.get('width') as string;
  638. if (w !== 'auto') {
  639. width = Math.max(this.length2em(w, 0) + 2 * this.fLine, width);
  640. }
  641. //
  642. // Return the bounding box information
  643. //
  644. let [h, d] = this.getBBoxHD(height);
  645. bbox.h = h;
  646. bbox.d = d;
  647. bbox.w = width;
  648. let [L, R] = this.getBBoxLR();
  649. bbox.L = L;
  650. bbox.R = R;
  651. //
  652. // Handle cell widths if width is not a percentage
  653. //
  654. if (!isPercent(w)) {
  655. this.setColumnPWidths();
  656. }
  657. }
  658. /**
  659. * @override
  660. */
  661. public setChildPWidths(_recompute: boolean, cwidth: number, _clear: boolean) {
  662. const width = this.node.attributes.get('width') as string;
  663. if (!isPercent(width)) return false;
  664. if (!this.hasLabels) {
  665. this.bbox.pwidth = '';
  666. this.container.bbox.pwidth = '';
  667. }
  668. const {w, L, R} = this.bbox;
  669. const labelInWidth = this.node.attributes.get('data-width-includes-label') as boolean;
  670. const W = Math.max(w, this.length2em(width, Math.max(cwidth, L + w + R))) - (labelInWidth ? L + R : 0);
  671. const cols = (this.node.attributes.get('equalcolumns') as boolean ?
  672. Array(this.numCols).fill(this.percent(1 / Math.max(1, this.numCols))) :
  673. this.getColumnAttributes('columnwidth', 0));
  674. this.cWidths = this.getColumnWidthsFixed(cols, W);
  675. const CW = this.getComputedWidths();
  676. this.pWidth = sum(CW.concat(this.cLines, this.cSpace)) + 2 * (this.fLine + this.fSpace[0]);
  677. if (this.isTop) {
  678. this.bbox.w = this.pWidth;
  679. }
  680. this.setColumnPWidths();
  681. if (this.pWidth !== w) {
  682. this.parent.invalidateBBox();
  683. }
  684. return this.pWidth !== w;
  685. }
  686. /**
  687. * Finalize any cells that have percentage-width content
  688. */
  689. public setColumnPWidths() {
  690. const W = this.cWidths as number[];
  691. for (const [cell, i] of this.pwidthCells) {
  692. if (cell.setChildPWidths(false, W[i])) {
  693. cell.invalidateBBox();
  694. cell.getBBox();
  695. }
  696. }
  697. }
  698. /**
  699. * @param {number} height The total height of the table
  700. * @return {[number, number]} The [height, depth] for the aligned table
  701. */
  702. public getBBoxHD(height: number): [number, number] {
  703. const [align, row] = this.getAlignmentRow();
  704. if (row === null) {
  705. const a = this.font.params.axis_height;
  706. const h2 = height / 2;
  707. const HD: {[key: string]: [number, number]} = {
  708. top: [0, height],
  709. center: [h2, h2],
  710. bottom: [height, 0],
  711. baseline: [h2, h2],
  712. axis: [h2 + a, h2 - a]
  713. };
  714. return HD[align] || [h2, h2];
  715. } else {
  716. const y = this.getVerticalPosition(row, align);
  717. return [y, height - y];
  718. }
  719. }
  720. /**
  721. * Get bbox left and right amounts to cover labels
  722. */
  723. public getBBoxLR() {
  724. if (this.hasLabels) {
  725. const attributes = this.node.attributes;
  726. const side = attributes.get('side') as string;
  727. let [pad, align] = this.getPadAlignShift(side);
  728. //
  729. // If labels are included in the width,
  730. // remove the frame spacing if there is no frame line (added by multline)
  731. // and use left or right justification rather than centering so that
  732. // there is no extra space reserved for the label on the opposite side,
  733. // (as there usually is to center the equation).
  734. //
  735. const labels = this.hasLabels && !!attributes.get('data-width-includes-label');
  736. if (labels && this.frame && this.fSpace[0]) {
  737. pad -= this.fSpace[0];
  738. }
  739. return (align === 'center' && !labels ? [pad, pad] :
  740. side === 'left' ? [pad, 0] : [0, pad]);
  741. }
  742. return [0, 0];
  743. }
  744. /**
  745. * @param {string} side The side for the labels
  746. * @return {[number, string, number]} The padding, alignment, and shift amounts
  747. */
  748. public getPadAlignShift(side: string): [number, string, number] {
  749. //
  750. // Make sure labels don't overlap table
  751. //
  752. const {L} = this.getTableData();
  753. const sep = this.length2em(this.node.attributes.get('minlabelspacing'));
  754. let pad = L + sep;
  755. const [lpad, rpad] = (this.styles == null ? ['', ''] :
  756. [this.styles.get('padding-left'), this.styles.get('padding-right')]);
  757. if (lpad || rpad) {
  758. pad = Math.max(pad, this.length2em(lpad || '0'), this.length2em(rpad || '0'));
  759. }
  760. //
  761. // Handle indentation
  762. //
  763. let [align, shift] = this.getAlignShift();
  764. if (align === side) {
  765. shift = (side === 'left' ? Math.max(pad, shift) - pad : Math.min(-pad, shift) + pad);
  766. }
  767. return [pad, align, shift] as [number, string, number];
  768. }
  769. /**
  770. * @override
  771. */
  772. public getAlignShift() {
  773. return (this.isTop ? super.getAlignShift() :
  774. [this.container.getChildAlign(this.containerI), 0] as [string, number]);
  775. }
  776. /**
  777. * @return {number} The true width of the table (without labels)
  778. */
  779. public getWidth(): number {
  780. return this.pWidth || this.getBBox().w;
  781. }
  782. /******************************************************************/
  783. /**
  784. * @return {number} The maximum height of a row
  785. */
  786. public getEqualRowHeight(): number {
  787. const {H, D} = this.getTableData();
  788. const HD = Array.from(H.keys()).map(i => H[i] + D[i]);
  789. return Math.max.apply(Math, HD);
  790. }
  791. /**
  792. * @return {number[]} The array of computed widths
  793. */
  794. public getComputedWidths(): number[] {
  795. const W = this.getTableData().W;
  796. let CW = Array.from(W.keys()).map(i => {
  797. return (typeof this.cWidths[i] === 'number' ? this.cWidths[i] as number : W[i]);
  798. });
  799. if (this.node.attributes.get('equalcolumns') as boolean) {
  800. CW = Array(CW.length).fill(max(CW));
  801. }
  802. return CW;
  803. }
  804. /**
  805. * Determine the column widths that can be computed (and need to be set).
  806. * The resulting arrays will have numbers for fixed-size arrays,
  807. * strings for percentage sizes that can't be determined now,
  808. * and null for stretchy columns that will expand to fill the extra space.
  809. * Depending on the width specified for the table, different column
  810. * values can be determined.
  811. *
  812. * @return {(string|number|null)[]} The array of widths
  813. */
  814. public getColumnWidths(): (string | number | null)[] {
  815. const width = this.node.attributes.get('width') as string;
  816. if (this.node.attributes.get('equalcolumns') as boolean) {
  817. return this.getEqualColumns(width);
  818. }
  819. const swidths = this.getColumnAttributes('columnwidth', 0);
  820. if (width === 'auto') {
  821. return this.getColumnWidthsAuto(swidths);
  822. }
  823. if (isPercent(width)) {
  824. return this.getColumnWidthsPercent(swidths);
  825. }
  826. return this.getColumnWidthsFixed(swidths, this.length2em(width));
  827. }
  828. /**
  829. * For tables with equal columns, get the proper amount per column.
  830. *
  831. * @param {string} width The width attribute of the table
  832. * @return {(string|number|null)[]} The array of widths
  833. */
  834. public getEqualColumns(width: string): (string | number | null)[] {
  835. const n = Math.max(1, this.numCols);
  836. let cwidth;
  837. if (width === 'auto') {
  838. const {W} = this.getTableData();
  839. cwidth = max(W);
  840. } else if (isPercent(width)) {
  841. cwidth = this.percent(1 / n);
  842. } else {
  843. const w = sum([].concat(this.cLines, this.cSpace)) + 2 * this.fSpace[0];
  844. cwidth = Math.max(0, this.length2em(width) - w) / n;
  845. }
  846. return Array(this.numCols).fill(cwidth);
  847. }
  848. /**
  849. * For tables with width="auto", auto and fit columns
  850. * will end up being natural width, so don't need to
  851. * set those explicitly.
  852. *
  853. * @param {string[]} swidths The split and padded columnwidths attribute
  854. * @return {ColumnWidths} The array of widths
  855. */
  856. public getColumnWidthsAuto(swidths: string[]): ColumnWidths {
  857. return swidths.map(x => {
  858. if (x === 'auto' || x === 'fit') return null;
  859. if (isPercent(x)) return x;
  860. return this.length2em(x);
  861. });
  862. }
  863. /**
  864. * For tables with percentage widths, the 'fit' columns (or 'auto'
  865. * columns if there are not 'fit' ones) will stretch automatically,
  866. * but for 'auto' columns (when there are 'fit' ones), set the size
  867. * to the natural size of the column.
  868. *
  869. * @param {string[]} swidths The split and padded columnwidths attribute
  870. * @return {ColumnWidths} The array of widths
  871. */
  872. public getColumnWidthsPercent(swidths: string[]): ColumnWidths {
  873. const hasFit = swidths.indexOf('fit') >= 0;
  874. const {W} = (hasFit ? this.getTableData() : {W: null});
  875. return Array.from(swidths.keys()).map(i => {
  876. const x = swidths[i];
  877. if (x === 'fit') return null;
  878. if (x === 'auto') return (hasFit ? W[i] : null);
  879. if (isPercent(x)) return x;
  880. return this.length2em(x);
  881. });
  882. }
  883. /**
  884. * For fixed-width tables, compute the column widths of all columns.
  885. *
  886. * @param {string[]} swidths The split and padded columnwidths attribute
  887. * @param {number} width The width of the table
  888. * @return {ColumnWidths} The array of widths
  889. */
  890. public getColumnWidthsFixed(swidths: string[], width: number): ColumnWidths {
  891. //
  892. // Get the indices of the fit and auto columns, and the number of fit or auto entries.
  893. // If there are fit or auto columns, get the column widths.
  894. //
  895. const indices = Array.from(swidths.keys());
  896. const fit = indices.filter(i => swidths[i] === 'fit');
  897. const auto = indices.filter(i => swidths[i] === 'auto');
  898. const n = fit.length || auto.length;
  899. const {W} = (n ? this.getTableData() : {W: null});
  900. //
  901. // Determine the space remaining from the fixed width after the
  902. // separation and lines have been removed (cwidth), and
  903. // after the width of the columns have been removed (dw).
  904. //
  905. const cwidth = width - sum([].concat(this.cLines, this.cSpace)) - 2 * this.fSpace[0];
  906. let dw = cwidth;
  907. indices.forEach(i => {
  908. const x = swidths[i];
  909. dw -= (x === 'fit' || x === 'auto' ? W[i] : this.length2em(x, cwidth));
  910. });
  911. //
  912. // Get the amount of extra space per column, or 0 (fw)
  913. //
  914. const fw = (n && dw > 0 ? dw / n : 0);
  915. //
  916. // Return the column widths (plus extra space for those that are stretching
  917. //
  918. return indices.map(i => {
  919. const x = swidths[i];
  920. if (x === 'fit') return W[i] + fw;
  921. if (x === 'auto') return W[i] + (fit.length === 0 ? fw : 0);
  922. return this.length2em(x, cwidth);
  923. });
  924. }
  925. /**
  926. * @param {number} i The row number (starting at 0)
  927. * @param {string} align The alignment on that row
  928. * @return {number} The offest of the alignment position from the top of the table
  929. */
  930. public getVerticalPosition(i: number, align: string): number {
  931. const equal = this.node.attributes.get('equalrows') as boolean;
  932. const {H, D} = this.getTableData();
  933. const HD = (equal ? this.getEqualRowHeight() : 0);
  934. const space = this.getRowHalfSpacing();
  935. //
  936. // Start with frame size and add in spacing, height and depth,
  937. // and line thickness for each row.
  938. //
  939. let y = this.fLine;
  940. for (let j = 0; j < i; j++) {
  941. y += space[j] + (equal ? HD : H[j] + D[j]) + space[j + 1] + this.rLines[j];
  942. }
  943. //
  944. // For equal rows, get updated height and depth
  945. //
  946. const [h, d] = (equal ? [(HD + H[i] - D[i]) / 2, (HD - H[i] + D[i]) / 2] : [H[i], D[i]]);
  947. //
  948. // Add the offset into the specified row
  949. //
  950. const offset: {[name: string]: number} = {
  951. top: 0,
  952. center: space[i] + (h + d) / 2,
  953. bottom: space[i] + h + d + space[i + 1],
  954. baseline: space[i] + h,
  955. axis: space[i] + h - .25
  956. };
  957. y += offset[align] || 0;
  958. //
  959. // Return the final result
  960. //
  961. return y;
  962. }
  963. /******************************************************************/
  964. /**
  965. * @param {number} fspace The frame spacing to use
  966. * @param {number[]} space The array of spacing values to convert to strings
  967. * @param {number} scale A scaling factor to use for the sizes
  968. * @return {string[]} The half-spacing as stings with units of "em"
  969. * with frame spacing at the beginning and end
  970. */
  971. public getEmHalfSpacing(fspace: number, space: number[], scale: number = 1): string[] {
  972. //
  973. // Get the column spacing values, and add the frame spacing values at the left and right
  974. //
  975. const fspaceEm = this.em(fspace * scale);
  976. const spaceEm = this.addEm(space, 2 / scale);
  977. spaceEm.unshift(fspaceEm);
  978. spaceEm.push(fspaceEm);
  979. return spaceEm;
  980. }
  981. /**
  982. * @return {number[]} The half-spacing for rows with frame spacing at the ends
  983. */
  984. public getRowHalfSpacing(): number[] {
  985. const space = this.rSpace.map(x => x / 2);
  986. space.unshift(this.fSpace[1]);
  987. space.push(this.fSpace[1]);
  988. return space;
  989. }
  990. /**
  991. * @return {number[]} The half-spacing for columns with frame spacing at the ends
  992. */
  993. public getColumnHalfSpacing(): number[] {
  994. const space = this.cSpace.map(x => x / 2);
  995. space.unshift(this.fSpace[0]);
  996. space.push(this.fSpace[0]);
  997. return space;
  998. }
  999. /**
  1000. * @return {[string,number|null]} The alignment and row number (based at 0) or null
  1001. */
  1002. public getAlignmentRow(): [string, number] {
  1003. const [align, row] = split(this.node.attributes.get('align') as string);
  1004. if (row == null) return [align, null];
  1005. let i = parseInt(row);
  1006. if (i < 0) i += this.numRows + 1;
  1007. return [align, i < 1 || i > this.numRows ? null : i - 1];
  1008. }
  1009. /**
  1010. * @param {string} name The name of the attribute to get as an array
  1011. * @param {number=} i Return this many fewer than numCols entries
  1012. * @return {string[]} The array of values in the given attribute, split at spaces,
  1013. * padded to the number of table columns (minus 1) by repeating the last entry
  1014. */
  1015. public getColumnAttributes(name: string, i: number = 1): string[] | null {
  1016. const n = this.numCols - i;
  1017. const columns = this.getAttributeArray(name);
  1018. if (columns.length === 0) return null;
  1019. while (columns.length < n) {
  1020. columns.push(columns[columns.length - 1]);
  1021. }
  1022. if (columns.length > n) {
  1023. columns.splice(n);
  1024. }
  1025. return columns;
  1026. }
  1027. /**
  1028. * @param {string} name The name of the attribute to get as an array
  1029. * @param {number=} i Return this many fewer than numRows entries
  1030. * @return {string[]} The array of values in the given attribute, split at spaces,
  1031. * padded to the number of table rows (minus 1) by repeating the last entry
  1032. */
  1033. public getRowAttributes(name: string, i: number = 1): string[] | null {
  1034. const n = this.numRows - i;
  1035. const rows = this.getAttributeArray(name);
  1036. if (rows.length === 0) return null;
  1037. while (rows.length < n) {
  1038. rows.push(rows[rows.length - 1]);
  1039. }
  1040. if (rows.length > n) {
  1041. rows.splice(n);
  1042. }
  1043. return rows;
  1044. }
  1045. /**
  1046. * @param {string} name The name of the attribute to get as an array
  1047. * @return {string[]} The array of values in the given attribute, split at spaces
  1048. * (after leading and trailing spaces are removed, and multiple
  1049. * spaces have been collapsed to one).
  1050. */
  1051. public getAttributeArray(name: string): string[] {
  1052. const value = this.node.attributes.get(name) as string;
  1053. if (!value) return [this.node.attributes.getDefault(name) as string];
  1054. return split(value);
  1055. }
  1056. /**
  1057. * Adds "em" to a list of dimensions, after dividing by n (defaults to 1).
  1058. *
  1059. * @param {string[]} list The array of dimensions (in em's)
  1060. * @param {nunber=} n The number to divide each dimension by after converted
  1061. * @return {string[]} The array of values with "em" added
  1062. */
  1063. public addEm(list: number[], n: number = 1): string[] | null {
  1064. if (!list) return null;
  1065. return list.map(x => this.em(x / n));
  1066. }
  1067. /**
  1068. * Converts an array of dimensions (with arbitrary units) to an array of numbers
  1069. * representing the dimensions in units of em's.
  1070. *
  1071. * @param {string[]} list The array of dimensions to be turned into em's
  1072. * @return {number[]} The array of values converted to em's
  1073. */
  1074. public convertLengths(list: string[]): number[] | null {
  1075. if (!list) return null;
  1076. return list.map(x => this.length2em(x));
  1077. }
  1078. };
  1079. }