123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418 |
- /*************************************************************
- *
- * Copyright (c) 2018-2022 The MathJax Consortium
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * @fileoverview Implements the SVGmtable wrapper for the MmlMtable object
- *
- * @author dpvc@mathjax.org (Davide Cervone)
- */
- import {SVGWrapper, SVGConstructor} from '../Wrapper.js';
- import {SVGWrapperFactory} from '../WrapperFactory.js';
- import {CommonMtableMixin} from '../../common/Wrappers/mtable.js';
- import {SVGmtr} from './mtr.js';
- import {SVGmtd} from './mtd.js';
- import {MmlMtable} from '../../../core/MmlTree/MmlNodes/mtable.js';
- import {MmlNode} from '../../../core/MmlTree/MmlNode.js';
- import {OptionList} from '../../../util/Options.js';
- import {StyleList} from '../../../util/StyleList.js';
- const CLASSPREFIX = 'mjx-';
- /*****************************************************************/
- /**
- * The SVGmtable wrapper for the MmlMtable object
- *
- * @template N The HTMLElement node class
- * @template T The Text node class
- * @template D The Document class
- */
- export class SVGmtable<N, T, D> extends
- CommonMtableMixin<SVGmtd<any, any, any>, SVGmtr<any, any, any>, SVGConstructor<any, any, any>>(SVGWrapper) {
- /**
- * The mtable wrapper
- */
- public static kind = MmlMtable.prototype.kind;
- /**
- * @override
- */
- public static styles: StyleList = {
- 'g[data-mml-node="mtable"] > line[data-line], svg[data-table] > g > line[data-line]': {
- 'stroke-width': '70px',
- fill: 'none'
- },
- 'g[data-mml-node="mtable"] > rect[data-frame], svg[data-table] > g > rect[data-frame]': {
- 'stroke-width': '70px',
- fill: 'none'
- },
- 'g[data-mml-node="mtable"] > .mjx-dashed, svg[data-table] > g > .mjx-dashed': {
- 'stroke-dasharray': '140'
- },
- 'g[data-mml-node="mtable"] > .mjx-dotted, svg[data-table] > g > .mjx-dotted': {
- 'stroke-linecap': 'round',
- 'stroke-dasharray': '0,140'
- },
- 'g[data-mml-node="mtable"] > g > svg': {
- overflow: 'visible'
- }
- };
- /**
- * The column for labels
- */
- public labels: N;
- /******************************************************************/
- /**
- * @override
- */
- constructor(factory: SVGWrapperFactory<N, T, D>, node: MmlNode, parent: SVGWrapper<N, T, D> = null) {
- super(factory, node, parent);
- const def: OptionList = {'data-labels': true};
- if (this.isTop) {
- def.transform = 'matrix(1 0 0 -1 0 0)';
- }
- this.labels = this.svg('g', def);
- }
- /**
- * @override
- */
- public toSVG(parent: N) {
- const svg = this.standardSVGnode(parent);
- this.placeRows(svg);
- this.handleColumnLines(svg);
- this.handleRowLines(svg);
- this.handleFrame(svg);
- const dx = this.handlePWidth(svg);
- this.handleLabels(svg, parent, dx);
- }
- /**
- * @param {N} svg The container in which to place the rows
- */
- protected placeRows(svg: N) {
- const equal = this.node.attributes.get('equalrows') as boolean;
- const {H, D} = this.getTableData();
- const HD = this.getEqualRowHeight();
- const rSpace = this.getRowHalfSpacing();
- const rLines = [this.fLine, ...this.rLines, this.fLine];
- let y = this.getBBox().h - rLines[0];
- for (let i = 0; i < this.numRows; i++) {
- const row = this.childNodes[i];
- [row.H, row.D] = this.getRowHD(equal, HD, H[i], D[i]);
- [row.tSpace, row.bSpace] = [rSpace[i], rSpace[i + 1]];
- [row.tLine, row.bLine] = [rLines[i], rLines[i + 1]];
- row.toSVG(svg);
- row.place(0, y - rSpace[i] - row.H);
- y -= rSpace[i] + row.H + row.D + rSpace[i + 1] + rLines[i + 1];
- }
- }
- /**
- * @param {boolean} equal True for equal-height rows
- * @param {number} HD The height of equal-height rows
- * @param {number} H The natural height of the row
- * @param {number} D The natural depth of the row
- * @returns {number[]} The (possibly scaled) height and depth to use
- */
- protected getRowHD(equal: boolean, HD: number, H: number, D: number): [number, number] {
- return (equal ? [(HD + H - D) / 2, (HD - H + D) / 2] : [H, D]);
- }
- /******************************************************************/
- /**
- * @override
- */
- public handleColor() {
- super.handleColor();
- const rect = this.firstChild();
- if (rect) {
- this.adaptor.setAttribute(rect, 'width', this.fixed(this.getWidth()));
- }
- }
- /**
- * Add vertical lines between columns
- *
- * @param {N} svg The container for the table
- */
- protected handleColumnLines(svg: N) {
- if (this.node.attributes.get('columnlines') === 'none') return;
- const lines = this.getColumnAttributes('columnlines');
- if (!lines) return;
- const cSpace = this.getColumnHalfSpacing();
- const cLines = this.cLines;
- const cWidth = this.getComputedWidths();
- let x = this.fLine;
- for (let i = 0; i < lines.length; i++) {
- x += cSpace[i] + cWidth[i] + cSpace[i + 1];
- if (lines[i] !== 'none') {
- this.adaptor.append(svg, this.makeVLine(x, lines[i], cLines[i]));
- }
- x += cLines[i];
- }
- }
- /**
- * Add horizontal lines between rows
- *
- * @param {N} svg The container for the table
- */
- protected handleRowLines(svg: N) {
- if (this.node.attributes.get('rowlines') === 'none') return;
- const lines = this.getRowAttributes('rowlines');
- if (!lines) return;
- const equal = this.node.attributes.get('equalrows') as boolean;
- const {H, D} = this.getTableData();
- const HD = this.getEqualRowHeight();
- const rSpace = this.getRowHalfSpacing();
- const rLines = this.rLines;
- let y = this.getBBox().h - this.fLine;
- for (let i = 0; i < lines.length; i++) {
- const [rH, rD] = this.getRowHD(equal, HD, H[i], D[i]);
- y -= rSpace[i] + rH + rD + rSpace[i + 1];
- if (lines[i] !== 'none') {
- this.adaptor.append(svg, this.makeHLine(y, lines[i], rLines[i]));
- }
- y -= rLines[i];
- }
- }
- /**
- * Add a frame to the mtable, if needed
- *
- * @param {N} svg The container for the table
- */
- protected handleFrame(svg: N) {
- if (this.frame && this.fLine) {
- const {h, d, w} = this.getBBox();
- const style = this.node.attributes.get('frame') as string;
- this.adaptor.append(svg, this.makeFrame(w, h, d, style));
- }
- }
- /**
- * @returns {number} The x-adjustement needed to handle the true size of percentage-width tables
- */
- protected handlePWidth(svg: N): number {
- if (!this.pWidth) {
- return 0;
- }
- const {w, L, R} = this.getBBox();
- const W = L + this.pWidth + R;
- const align = this.getAlignShift()[0];
- const CW = Math.max(this.isTop ? W : 0, this.container.getWrapWidth(this.containerI)) - L - R;
- const dw = w - (this.pWidth > CW ? CW : this.pWidth);
- const dx = (align === 'left' ? 0 : align === 'right' ? dw : dw / 2);
- if (dx) {
- const table = this.svg('g', {}, this.adaptor.childNodes(svg));
- this.place(dx, 0, table);
- this.adaptor.append(svg, table);
- }
- return dx;
- }
- /******************************************************************/
- /**
- * @param {string} style The line style whose class is to be obtained
- * @returns {string} The class name for the style
- */
- protected lineClass(style: string): string {
- return CLASSPREFIX + style;
- }
- /**
- * @param {number} w The width of the frame
- * @param {number} h The height of the frame
- * @param {number} d The depth of the frame
- * @param {string} style The border style for the frame
- * @returns {N} The SVG element for the frame
- */
- protected makeFrame(w: number, h: number, d: number, style: string): N {
- const t = this.fLine;
- return this.svg('rect', this.setLineThickness(t, style, {
- 'data-frame': true, 'class': this.lineClass(style),
- width: this.fixed(w - t), height: this.fixed(h + d - t),
- x: this.fixed(t / 2), y: this.fixed(t / 2 - d)
- }));
- }
- /**
- * @param {number} x The x location of the line
- * @param {string} style The border style for the line
- * @param {number} t The line thickness
- * @returns {N} The SVG element for the line
- */
- protected makeVLine(x: number, style: string, t: number): N {
- const {h, d} = this.getBBox();
- const dt = (style === 'dotted' ? t / 2 : 0);
- const X = this.fixed(x + t / 2);
- return this.svg('line', this.setLineThickness(t, style, {
- 'data-line': 'v', 'class': this.lineClass(style),
- x1: X, y1: this.fixed(dt - d), x2: X, y2: this.fixed(h - dt)
- }));
- }
- /**
- * @param {number} y The y location of the line
- * @param {string} style The border style for the line
- * @param {number} t The line thickness
- * @returns {N} The SVG element for the line
- */
- protected makeHLine(y: number, style: string, t: number): N {
- const w = this.getBBox().w;
- const dt = (style === 'dotted' ? t / 2 : 0);
- const Y = this.fixed(y - t / 2);
- return this.svg('line', this.setLineThickness(t, style, {
- 'data-line': 'h', 'class': this.lineClass(style),
- x1: this.fixed(dt), y1: Y, x2: this.fixed(w - dt), y2: Y
- }));
- }
- /**
- * @param {number} t The thickness of the line
- * @param {string} style The border style for the line
- * @param {OptionList} properties The list of properties to modify
- * @param {OptionList} The modified properties
- */
- protected setLineThickness(t: number, style: string, properties: OptionList) {
- if (t !== .07) {
- properties['stroke-thickness'] = this.fixed(t);
- if (style !== 'solid') {
- properties['stroke-dasharray'] = (style === 'dotted' ? '0,' : '') + this.fixed(2 * t);
- }
- }
- return properties;
- }
- /******************************************************************/
- /**
- * Handle addition of labels to the table
- *
- * @param {N} svg The container for the table contents
- * @param {N} parent The parent containing the the table
- * @param {number} dx The adjustement for percentage width tables
- */
- protected handleLabels(svg: N, _parent: N, dx: number) {
- if (!this.hasLabels) return;
- const labels = this.labels;
- const attributes = this.node.attributes;
- //
- // Set the side for the labels
- //
- const side = attributes.get('side') as string;
- //
- // Add the labels to the table
- //
- this.spaceLabels();
- //
- // Handle top-level table to make it adapt to container size
- // but place subtables explicitly
- //
- this.isTop ? this.topTable(svg, labels, side) : this.subTable(svg, labels, side, dx);
- }
- /**
- * Add spacing elements between the label rows to align them with the rest of the table
- */
- protected spaceLabels() {
- const adaptor = this.adaptor;
- const h = this.getBBox().h;
- const L = this.getTableData().L;
- const space = this.getRowHalfSpacing();
- //
- // Start with frame size and add in spacing, height and depth,
- // and line thickness for each non-labeled row.
- //
- let y = h - this.fLine;
- let current = adaptor.firstChild(this.labels) as N;
- for (let i = 0; i < this.numRows; i++) {
- const row = this.childNodes[i] as SVGmtr<N, T, D>;
- if (row.node.isKind('mlabeledtr')) {
- const cell = row.childNodes[0];
- y -= space[i] + row.H;
- row.placeCell(cell, {x: 0, y: y, w: L, lSpace: 0, rSpace: 0, lLine: 0, rLine: 0});
- y -= row.D + space[i + 1] + this.rLines[i];
- current = adaptor.next(current) as N;
- } else {
- y -= space[i] + row.H + row.D + space[i + 1] + this.rLines[i];
- }
- }
- }
- /**
- * Handles tables with labels so that the label will move with the size of the container
- *
- * @param {N} svg The SVG container for the table
- * @param {N} labels The group of labels
- * @param {string} side The side alignment (left or right)
- */
- protected topTable(svg: N, labels: N, side: string) {
- const adaptor = this.adaptor;
- const {h, d, w, L, R} = this.getBBox();
- const W = L + (this.pWidth || w) + R;
- const LW = this.getTableData().L;
- const [ , align, shift] = this.getPadAlignShift(side);
- const dx = shift + (align === 'right' ? -W : align === 'center' ? -W / 2 : 0) + L;
- const matrix = 'matrix(1 0 0 -1 0 0)';
- const scale = `scale(${this.jax.fixed((this.font.params.x_height * 1000) / this.metrics.ex, 2)})`;
- const transform = `translate(0 ${this.fixed(h)}) ${matrix} ${scale}`;
- let table = this.svg('svg', {
- 'data-table': true,
- preserveAspectRatio: (align === 'left' ? 'xMinYMid' : align === 'right' ? 'xMaxYMid' : 'xMidYMid'),
- viewBox: [this.fixed(-dx), this.fixed(-h), 1, this.fixed(h + d)].join(' ')
- }, [
- this.svg('g', {transform: matrix}, adaptor.childNodes(svg))
- ]);
- labels = this.svg('svg', {
- 'data-labels': true,
- preserveAspectRatio: (side === 'left' ? 'xMinYMid' : 'xMaxYMid'),
- viewBox: [side === 'left' ? 0 : this.fixed(LW), this.fixed(-h), 1, this.fixed(h + d)].join(' ')
- }, [labels]);
- adaptor.append(svg, this.svg('g', {transform: transform}, [table, labels]));
- this.place(-L, 0, svg); // remove spacing for L, which is added by the parent during appending
- }
- /**
- * @param {N} svg The SVG container for the table
- * @param {N} labels The group of labels
- * @param {string} side The side alignment (left or right)
- * @param {number} dx The adjustement for percentage width tables
- */
- protected subTable(svg: N, labels: N, side: string, dx: number) {
- const adaptor = this.adaptor;
- const {w, L, R} = this.getBBox();
- const W = L + (this.pWidth || w) + R;
- const labelW = this.getTableData().L;
- const align = this.getAlignShift()[0];
- const CW = Math.max(W, this.container.getWrapWidth(this.containerI));
- this.place(side === 'left' ?
- (align === 'left' ? 0 : align === 'right' ? W - CW + dx : (W - CW) / 2 + dx) - L :
- (align === 'left' ? CW : align === 'right' ? W + dx : (CW + W) / 2 + dx) - L - labelW,
- 0, labels);
- adaptor.append(svg, labels);
- }
- }
|