mtable.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418
  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 SVGmtable wrapper for the MmlMtable object
  19. *
  20. * @author dpvc@mathjax.org (Davide Cervone)
  21. */
  22. import {SVGWrapper, SVGConstructor} from '../Wrapper.js';
  23. import {SVGWrapperFactory} from '../WrapperFactory.js';
  24. import {CommonMtableMixin} from '../../common/Wrappers/mtable.js';
  25. import {SVGmtr} from './mtr.js';
  26. import {SVGmtd} from './mtd.js';
  27. import {MmlMtable} from '../../../core/MmlTree/MmlNodes/mtable.js';
  28. import {MmlNode} from '../../../core/MmlTree/MmlNode.js';
  29. import {OptionList} from '../../../util/Options.js';
  30. import {StyleList} from '../../../util/StyleList.js';
  31. const CLASSPREFIX = 'mjx-';
  32. /*****************************************************************/
  33. /**
  34. * The SVGmtable wrapper for the MmlMtable object
  35. *
  36. * @template N The HTMLElement node class
  37. * @template T The Text node class
  38. * @template D The Document class
  39. */
  40. export class SVGmtable<N, T, D> extends
  41. CommonMtableMixin<SVGmtd<any, any, any>, SVGmtr<any, any, any>, SVGConstructor<any, any, any>>(SVGWrapper) {
  42. /**
  43. * The mtable wrapper
  44. */
  45. public static kind = MmlMtable.prototype.kind;
  46. /**
  47. * @override
  48. */
  49. public static styles: StyleList = {
  50. 'g[data-mml-node="mtable"] > line[data-line], svg[data-table] > g > line[data-line]': {
  51. 'stroke-width': '70px',
  52. fill: 'none'
  53. },
  54. 'g[data-mml-node="mtable"] > rect[data-frame], svg[data-table] > g > rect[data-frame]': {
  55. 'stroke-width': '70px',
  56. fill: 'none'
  57. },
  58. 'g[data-mml-node="mtable"] > .mjx-dashed, svg[data-table] > g > .mjx-dashed': {
  59. 'stroke-dasharray': '140'
  60. },
  61. 'g[data-mml-node="mtable"] > .mjx-dotted, svg[data-table] > g > .mjx-dotted': {
  62. 'stroke-linecap': 'round',
  63. 'stroke-dasharray': '0,140'
  64. },
  65. 'g[data-mml-node="mtable"] > g > svg': {
  66. overflow: 'visible'
  67. }
  68. };
  69. /**
  70. * The column for labels
  71. */
  72. public labels: N;
  73. /******************************************************************/
  74. /**
  75. * @override
  76. */
  77. constructor(factory: SVGWrapperFactory<N, T, D>, node: MmlNode, parent: SVGWrapper<N, T, D> = null) {
  78. super(factory, node, parent);
  79. const def: OptionList = {'data-labels': true};
  80. if (this.isTop) {
  81. def.transform = 'matrix(1 0 0 -1 0 0)';
  82. }
  83. this.labels = this.svg('g', def);
  84. }
  85. /**
  86. * @override
  87. */
  88. public toSVG(parent: N) {
  89. const svg = this.standardSVGnode(parent);
  90. this.placeRows(svg);
  91. this.handleColumnLines(svg);
  92. this.handleRowLines(svg);
  93. this.handleFrame(svg);
  94. const dx = this.handlePWidth(svg);
  95. this.handleLabels(svg, parent, dx);
  96. }
  97. /**
  98. * @param {N} svg The container in which to place the rows
  99. */
  100. protected placeRows(svg: N) {
  101. const equal = this.node.attributes.get('equalrows') as boolean;
  102. const {H, D} = this.getTableData();
  103. const HD = this.getEqualRowHeight();
  104. const rSpace = this.getRowHalfSpacing();
  105. const rLines = [this.fLine, ...this.rLines, this.fLine];
  106. let y = this.getBBox().h - rLines[0];
  107. for (let i = 0; i < this.numRows; i++) {
  108. const row = this.childNodes[i];
  109. [row.H, row.D] = this.getRowHD(equal, HD, H[i], D[i]);
  110. [row.tSpace, row.bSpace] = [rSpace[i], rSpace[i + 1]];
  111. [row.tLine, row.bLine] = [rLines[i], rLines[i + 1]];
  112. row.toSVG(svg);
  113. row.place(0, y - rSpace[i] - row.H);
  114. y -= rSpace[i] + row.H + row.D + rSpace[i + 1] + rLines[i + 1];
  115. }
  116. }
  117. /**
  118. * @param {boolean} equal True for equal-height rows
  119. * @param {number} HD The height of equal-height rows
  120. * @param {number} H The natural height of the row
  121. * @param {number} D The natural depth of the row
  122. * @returns {number[]} The (possibly scaled) height and depth to use
  123. */
  124. protected getRowHD(equal: boolean, HD: number, H: number, D: number): [number, number] {
  125. return (equal ? [(HD + H - D) / 2, (HD - H + D) / 2] : [H, D]);
  126. }
  127. /******************************************************************/
  128. /**
  129. * @override
  130. */
  131. public handleColor() {
  132. super.handleColor();
  133. const rect = this.firstChild();
  134. if (rect) {
  135. this.adaptor.setAttribute(rect, 'width', this.fixed(this.getWidth()));
  136. }
  137. }
  138. /**
  139. * Add vertical lines between columns
  140. *
  141. * @param {N} svg The container for the table
  142. */
  143. protected handleColumnLines(svg: N) {
  144. if (this.node.attributes.get('columnlines') === 'none') return;
  145. const lines = this.getColumnAttributes('columnlines');
  146. if (!lines) return;
  147. const cSpace = this.getColumnHalfSpacing();
  148. const cLines = this.cLines;
  149. const cWidth = this.getComputedWidths();
  150. let x = this.fLine;
  151. for (let i = 0; i < lines.length; i++) {
  152. x += cSpace[i] + cWidth[i] + cSpace[i + 1];
  153. if (lines[i] !== 'none') {
  154. this.adaptor.append(svg, this.makeVLine(x, lines[i], cLines[i]));
  155. }
  156. x += cLines[i];
  157. }
  158. }
  159. /**
  160. * Add horizontal lines between rows
  161. *
  162. * @param {N} svg The container for the table
  163. */
  164. protected handleRowLines(svg: N) {
  165. if (this.node.attributes.get('rowlines') === 'none') return;
  166. const lines = this.getRowAttributes('rowlines');
  167. if (!lines) return;
  168. const equal = this.node.attributes.get('equalrows') as boolean;
  169. const {H, D} = this.getTableData();
  170. const HD = this.getEqualRowHeight();
  171. const rSpace = this.getRowHalfSpacing();
  172. const rLines = this.rLines;
  173. let y = this.getBBox().h - this.fLine;
  174. for (let i = 0; i < lines.length; i++) {
  175. const [rH, rD] = this.getRowHD(equal, HD, H[i], D[i]);
  176. y -= rSpace[i] + rH + rD + rSpace[i + 1];
  177. if (lines[i] !== 'none') {
  178. this.adaptor.append(svg, this.makeHLine(y, lines[i], rLines[i]));
  179. }
  180. y -= rLines[i];
  181. }
  182. }
  183. /**
  184. * Add a frame to the mtable, if needed
  185. *
  186. * @param {N} svg The container for the table
  187. */
  188. protected handleFrame(svg: N) {
  189. if (this.frame && this.fLine) {
  190. const {h, d, w} = this.getBBox();
  191. const style = this.node.attributes.get('frame') as string;
  192. this.adaptor.append(svg, this.makeFrame(w, h, d, style));
  193. }
  194. }
  195. /**
  196. * @returns {number} The x-adjustement needed to handle the true size of percentage-width tables
  197. */
  198. protected handlePWidth(svg: N): number {
  199. if (!this.pWidth) {
  200. return 0;
  201. }
  202. const {w, L, R} = this.getBBox();
  203. const W = L + this.pWidth + R;
  204. const align = this.getAlignShift()[0];
  205. const CW = Math.max(this.isTop ? W : 0, this.container.getWrapWidth(this.containerI)) - L - R;
  206. const dw = w - (this.pWidth > CW ? CW : this.pWidth);
  207. const dx = (align === 'left' ? 0 : align === 'right' ? dw : dw / 2);
  208. if (dx) {
  209. const table = this.svg('g', {}, this.adaptor.childNodes(svg));
  210. this.place(dx, 0, table);
  211. this.adaptor.append(svg, table);
  212. }
  213. return dx;
  214. }
  215. /******************************************************************/
  216. /**
  217. * @param {string} style The line style whose class is to be obtained
  218. * @returns {string} The class name for the style
  219. */
  220. protected lineClass(style: string): string {
  221. return CLASSPREFIX + style;
  222. }
  223. /**
  224. * @param {number} w The width of the frame
  225. * @param {number} h The height of the frame
  226. * @param {number} d The depth of the frame
  227. * @param {string} style The border style for the frame
  228. * @returns {N} The SVG element for the frame
  229. */
  230. protected makeFrame(w: number, h: number, d: number, style: string): N {
  231. const t = this.fLine;
  232. return this.svg('rect', this.setLineThickness(t, style, {
  233. 'data-frame': true, 'class': this.lineClass(style),
  234. width: this.fixed(w - t), height: this.fixed(h + d - t),
  235. x: this.fixed(t / 2), y: this.fixed(t / 2 - d)
  236. }));
  237. }
  238. /**
  239. * @param {number} x The x location of the line
  240. * @param {string} style The border style for the line
  241. * @param {number} t The line thickness
  242. * @returns {N} The SVG element for the line
  243. */
  244. protected makeVLine(x: number, style: string, t: number): N {
  245. const {h, d} = this.getBBox();
  246. const dt = (style === 'dotted' ? t / 2 : 0);
  247. const X = this.fixed(x + t / 2);
  248. return this.svg('line', this.setLineThickness(t, style, {
  249. 'data-line': 'v', 'class': this.lineClass(style),
  250. x1: X, y1: this.fixed(dt - d), x2: X, y2: this.fixed(h - dt)
  251. }));
  252. }
  253. /**
  254. * @param {number} y The y location of the line
  255. * @param {string} style The border style for the line
  256. * @param {number} t The line thickness
  257. * @returns {N} The SVG element for the line
  258. */
  259. protected makeHLine(y: number, style: string, t: number): N {
  260. const w = this.getBBox().w;
  261. const dt = (style === 'dotted' ? t / 2 : 0);
  262. const Y = this.fixed(y - t / 2);
  263. return this.svg('line', this.setLineThickness(t, style, {
  264. 'data-line': 'h', 'class': this.lineClass(style),
  265. x1: this.fixed(dt), y1: Y, x2: this.fixed(w - dt), y2: Y
  266. }));
  267. }
  268. /**
  269. * @param {number} t The thickness of the line
  270. * @param {string} style The border style for the line
  271. * @param {OptionList} properties The list of properties to modify
  272. * @param {OptionList} The modified properties
  273. */
  274. protected setLineThickness(t: number, style: string, properties: OptionList) {
  275. if (t !== .07) {
  276. properties['stroke-thickness'] = this.fixed(t);
  277. if (style !== 'solid') {
  278. properties['stroke-dasharray'] = (style === 'dotted' ? '0,' : '') + this.fixed(2 * t);
  279. }
  280. }
  281. return properties;
  282. }
  283. /******************************************************************/
  284. /**
  285. * Handle addition of labels to the table
  286. *
  287. * @param {N} svg The container for the table contents
  288. * @param {N} parent The parent containing the the table
  289. * @param {number} dx The adjustement for percentage width tables
  290. */
  291. protected handleLabels(svg: N, _parent: N, dx: number) {
  292. if (!this.hasLabels) return;
  293. const labels = this.labels;
  294. const attributes = this.node.attributes;
  295. //
  296. // Set the side for the labels
  297. //
  298. const side = attributes.get('side') as string;
  299. //
  300. // Add the labels to the table
  301. //
  302. this.spaceLabels();
  303. //
  304. // Handle top-level table to make it adapt to container size
  305. // but place subtables explicitly
  306. //
  307. this.isTop ? this.topTable(svg, labels, side) : this.subTable(svg, labels, side, dx);
  308. }
  309. /**
  310. * Add spacing elements between the label rows to align them with the rest of the table
  311. */
  312. protected spaceLabels() {
  313. const adaptor = this.adaptor;
  314. const h = this.getBBox().h;
  315. const L = this.getTableData().L;
  316. const space = this.getRowHalfSpacing();
  317. //
  318. // Start with frame size and add in spacing, height and depth,
  319. // and line thickness for each non-labeled row.
  320. //
  321. let y = h - this.fLine;
  322. let current = adaptor.firstChild(this.labels) as N;
  323. for (let i = 0; i < this.numRows; i++) {
  324. const row = this.childNodes[i] as SVGmtr<N, T, D>;
  325. if (row.node.isKind('mlabeledtr')) {
  326. const cell = row.childNodes[0];
  327. y -= space[i] + row.H;
  328. row.placeCell(cell, {x: 0, y: y, w: L, lSpace: 0, rSpace: 0, lLine: 0, rLine: 0});
  329. y -= row.D + space[i + 1] + this.rLines[i];
  330. current = adaptor.next(current) as N;
  331. } else {
  332. y -= space[i] + row.H + row.D + space[i + 1] + this.rLines[i];
  333. }
  334. }
  335. }
  336. /**
  337. * Handles tables with labels so that the label will move with the size of the container
  338. *
  339. * @param {N} svg The SVG container for the table
  340. * @param {N} labels The group of labels
  341. * @param {string} side The side alignment (left or right)
  342. */
  343. protected topTable(svg: N, labels: N, side: string) {
  344. const adaptor = this.adaptor;
  345. const {h, d, w, L, R} = this.getBBox();
  346. const W = L + (this.pWidth || w) + R;
  347. const LW = this.getTableData().L;
  348. const [ , align, shift] = this.getPadAlignShift(side);
  349. const dx = shift + (align === 'right' ? -W : align === 'center' ? -W / 2 : 0) + L;
  350. const matrix = 'matrix(1 0 0 -1 0 0)';
  351. const scale = `scale(${this.jax.fixed((this.font.params.x_height * 1000) / this.metrics.ex, 2)})`;
  352. const transform = `translate(0 ${this.fixed(h)}) ${matrix} ${scale}`;
  353. let table = this.svg('svg', {
  354. 'data-table': true,
  355. preserveAspectRatio: (align === 'left' ? 'xMinYMid' : align === 'right' ? 'xMaxYMid' : 'xMidYMid'),
  356. viewBox: [this.fixed(-dx), this.fixed(-h), 1, this.fixed(h + d)].join(' ')
  357. }, [
  358. this.svg('g', {transform: matrix}, adaptor.childNodes(svg))
  359. ]);
  360. labels = this.svg('svg', {
  361. 'data-labels': true,
  362. preserveAspectRatio: (side === 'left' ? 'xMinYMid' : 'xMaxYMid'),
  363. viewBox: [side === 'left' ? 0 : this.fixed(LW), this.fixed(-h), 1, this.fixed(h + d)].join(' ')
  364. }, [labels]);
  365. adaptor.append(svg, this.svg('g', {transform: transform}, [table, labels]));
  366. this.place(-L, 0, svg); // remove spacing for L, which is added by the parent during appending
  367. }
  368. /**
  369. * @param {N} svg The SVG container for the table
  370. * @param {N} labels The group of labels
  371. * @param {string} side The side alignment (left or right)
  372. * @param {number} dx The adjustement for percentage width tables
  373. */
  374. protected subTable(svg: N, labels: N, side: string, dx: number) {
  375. const adaptor = this.adaptor;
  376. const {w, L, R} = this.getBBox();
  377. const W = L + (this.pWidth || w) + R;
  378. const labelW = this.getTableData().L;
  379. const align = this.getAlignShift()[0];
  380. const CW = Math.max(W, this.container.getWrapWidth(this.containerI));
  381. this.place(side === 'left' ?
  382. (align === 'left' ? 0 : align === 'right' ? W - CW + dx : (W - CW) / 2 + dx) - L :
  383. (align === 'left' ? CW : align === 'right' ? W + dx : (CW + W) / 2 + dx) - L - labelW,
  384. 0, labels);
  385. adaptor.append(svg, labels);
  386. }
  387. }