Skip to content

Commit a307315

Browse files
committed
Add support for equation labels
1 parent b35db44 commit a307315

File tree

3 files changed

+215
-51
lines changed

3 files changed

+215
-51
lines changed

mathjax3-ts/output/chtml/Wrappers/mtable.ts

Lines changed: 175 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,16 @@ import {DIRECTION} from '../FontData.js';
3333

3434
/*
3535
* The heights, depths, and widths of the rows and columns
36+
* Plus the natural height and depth (i.e., without the labels)
37+
* Plus the label column width
3638
*/
3739
export type TableData = {
3840
H: number[];
3941
D: number[];
4042
W: number[];
43+
NH: number[];
44+
ND: number[];
45+
L: number;
4146
};
4247

4348
/*
@@ -82,15 +87,18 @@ export class CHTMLmtable<N, T, D> extends CHTMLWrapper<N, T, D> {
8287
public static styles: StyleList = {
8388
'mjx-mtable': {
8489
'vertical-align': '.25em',
85-
'text-align': 'center'
90+
'text-align': 'center',
91+
'position': 'relative'
8692
},
8793
'mjx-mtable > mjx-itable': {
8894
'vertical-align': 'middle',
8995
'text-align': 'left',
9096
'box-sizing': 'border-box'
9197
},
92-
'mjx-mtable[width] > mjx-itable': {
93-
width: '100%'
98+
'mjx-labels': {
99+
display: 'inline-table',
100+
position: 'absolute',
101+
top: 0
94102
},
95103
'mjx-mtable[align]': {
96104
'vertical-align': 'baseline'
@@ -109,6 +117,11 @@ export class CHTMLmtable<N, T, D> extends CHTMLWrapper<N, T, D> {
109117
}
110118
};
111119

120+
/*
121+
* The column for labels
122+
*/
123+
public labels: N;
124+
112125
/*
113126
* The number of columns and rows in the table
114127
*/
@@ -139,6 +152,7 @@ export class CHTMLmtable<N, T, D> extends CHTMLWrapper<N, T, D> {
139152
*/
140153
constructor(factory: CHTMLWrapperFactory<N, T, D>, node: MmlNode, parent: CHTMLWrapper<N, T, D> = null) {
141154
super(factory, node, parent);
155+
this.labels = this.html('mjx-labels', {align: node.attributes.get('side')});
142156
//
143157
// Determine the number of columns and rows
144158
//
@@ -264,6 +278,7 @@ export class CHTMLmtable<N, T, D> extends CHTMLWrapper<N, T, D> {
264278
this.handleEqualRows();
265279
this.handleFrame();
266280
this.handleWidth();
281+
this.handleLabels();
267282
this.handleAlign();
268283
}
269284

@@ -280,22 +295,45 @@ export class CHTMLmtable<N, T, D> extends CHTMLWrapper<N, T, D> {
280295
const H = new Array(this.numRows).fill(0);
281296
const D = new Array(this.numRows).fill(0);
282297
const W = new Array(this.numCols).fill(0);
298+
const NH = new Array(this.numRows);
299+
const ND = new Array(this.numRows);
300+
const LW = [0];
283301
for (let j = 0; j < this.numRows; j++) {
284302
const row = this.childNodes[j] as CHTMLmtr<N, T, D>;
285-
for (let i = 0; i < row.childNodes.length; i++) {
286-
const cbox = row.childNodes[i].getBBox();
287-
const h = Math.max(cbox.h, .75);
288-
const d = Math.max(cbox.d, .25);
289-
if (h > H[j]) H[j] = h;
290-
if (d > D[j]) D[j] = d;
291-
if (cbox.w > W[i]) W[i] = cbox.w;
303+
const cellCount = row.numCells;
304+
const firstCell = row.firstCell;
305+
for (let i = 0; i < cellCount; i++) {
306+
this.updateHDW(row.childNodes[i + firstCell], i, j, H, D, W);
307+
}
308+
NH[j] = H[j];
309+
ND[j] = D[j];
310+
if (firstCell > 0) {
311+
this.updateHDW(row.childNodes[0], 0, j, H, D, LW);
292312
}
293313
}
294314
const w = this.node.attributes.get('width') as string;
295-
this.data = {H, D, W};
315+
const L = LW[0];
316+
this.data = {H, D, W, NH, ND, L};
296317
return this.data;
297318
}
298319

320+
/*
321+
* @param{CHTMLWrapper} cell The cell whose height, depth, and width are to be added into the H, D, W arrays
322+
* @param{number} i The column number for the cell
323+
* @param{number} j The row number for the cell
324+
* @param{number[]} H The maximum height for each of the rows
325+
* @param{number[]} D The maximum depth for each of the rows
326+
* @param{number[]} W The maximum width for each column
327+
*/
328+
protected updateHDW(cell: CHTMLWrapper<N, T, D>, i: number, j: number, H: number[], D: number[], W: number[] = null) {
329+
let {h, d, w} = cell.getBBox();
330+
if (h < .75) h = .75;
331+
if (d < .25) d = .25;
332+
if (h > H[j]) H[j] = h;
333+
if (d > D[j]) D[j] = d;
334+
if (W && w > W[i]) W[i] = w;
335+
}
336+
299337
/*
300338
* @override
301339
*/
@@ -396,12 +434,13 @@ export class CHTMLmtable<N, T, D> extends CHTMLWrapper<N, T, D> {
396434
//
397435
// For each row...
398436
//
399-
for (const row of this.childNodes) {
437+
for (const row of (this.childNodes as CHTMLmtr<N, T, D>[])) {
400438
let i = 0;
401439
//
402440
// For each cell in the row...
403441
//
404-
for (const cell of row.childNodes) {
442+
const children = (row.firstCell ? row.childNodes.slice(row.firstCell) : row.childNodes);
443+
for (const cell of children) {
405444
//
406445
// Get the left and right-hand spacing
407446
//
@@ -412,10 +451,10 @@ export class CHTMLmtable<N, T, D> extends CHTMLWrapper<N, T, D> {
412451
// default already set in the mtd styles
413452
//
414453
const styleNode = (cell ? cell.chtml : this.adaptor.childNodes(row.chtml)[i] as N);
415-
if ((i > 1 || frame) && lspace !== '.5em') {
454+
if ((i > 1 && lspace !== '0.4em') || (frame && i === 1)) {
416455
this.adaptor.setStyle(styleNode, 'paddingLeft', lspace);
417456
}
418-
if ((i < this.numCols || frame) && rspace !== '.5em') {
457+
if ((i < this.numCols && rspace !== '0.4em') || (frame && i === this.numCols)) {
419458
this.adaptor.setStyle(styleNode, 'paddingRight', rspace);
420459
}
421460
}
@@ -491,10 +530,10 @@ export class CHTMLmtable<N, T, D> extends CHTMLWrapper<N, T, D> {
491530
// Set the style for the spacing, if it is needed, and isn't the
492531
// default already set in the mtd styles
493532
//
494-
if ((i > 1 || frame) && tspace !== '.125em') {
533+
if ((i > 1 && tspace !== '0.215em') || (frame && i === 1)) {
495534
this.adaptor.setStyle(cell.chtml, 'paddingTop', tspace);
496535
}
497-
if ((i < this.numRows || frame) && bspace !== '.125em') {
536+
if ((i < this.numRows && bspace !== '0.215em') || (frame && i === this.numRows)) {
498537
this.adaptor.setStyle(cell.chtml, 'paddingBottom', bspace);
499538
}
500539
}
@@ -533,25 +572,39 @@ export class CHTMLmtable<N, T, D> extends CHTMLWrapper<N, T, D> {
533572
//
534573
// Loop through the rows and set their heights
535574
//
536-
for (const i of Array.from(this.childNodes.keys())) {
575+
for (let i = 0; i < this.numRows; i++) {
537576
const row = this.childNodes[i];
538577
if (HD !== H[i] + D[i]) {
539-
this.adaptor.setStyle(row.chtml, 'height', this.em(space[i] + HD + space[i + 1]));
540-
const ralign = row.node.attributes.get('rowalign');
541-
//
542-
// Loop through the cells and set the strut height and depth to spread
543-
// the extra height equally above and below the baseline. The strut
544-
// is the last element in the cell.
545-
//
546-
for (const cell of row.childNodes) {
547-
const calign = cell.node.attributes.get('rowalign');
548-
if (calign === 'baseline' || calign === 'axis') {
549-
const child = this.adaptor.lastChild(cell.chtml) as N;
550-
this.adaptor.setStyle(child, 'height', HDem);
551-
this.adaptor.setStyle(child, 'verticalAlign', this.em(-((HD - H[i] + D[i]) / 2)));
552-
if (ralign === 'baseline' || ralign === 'axis') break;
553-
}
554-
}
578+
this.setRowHeight(row, HD, (HD - H[i] + D[i]) / 2, space[i] + space[i + 1]);
579+
}
580+
}
581+
}
582+
583+
/*
584+
* Set the height of the row, and make sure that the baseline is in the right position for cells
585+
* that are row aligned to baseline ot axis
586+
*
587+
* @param{CHTMLWrapper} row The row to be set
588+
* @param{number} HD The total height+depth for the row
589+
* @param{number] D The new depth for the row
590+
* @param{number} space The total spacing above and below the row
591+
*/
592+
protected setRowHeight(row: CHTMLWrapper<N, T, D>, HD: number, D: number, space: number) {
593+
const adaptor = this.adaptor;
594+
adaptor.setStyle(row.chtml, 'height', this.em(HD + space));
595+
const ralign = row.node.attributes.get('rowalign');
596+
//
597+
// Loop through the cells and set the strut height and depth.
598+
// The strut is the last element in the cell.
599+
//
600+
for (const cell of row.childNodes) {
601+
const calign = cell.node.attributes.get('rowalign');
602+
if (calign === 'baseline' || calign === 'axis') {
603+
const child = adaptor.lastChild(cell.chtml) as N;
604+
adaptor.setStyle(child, 'height', this.em(HD));
605+
adaptor.setStyle(child, 'verticalAlign', this.em(-D));
606+
if ((row.kind !== 'mlabeledtr' || cell !== row.childNodes[0]) &&
607+
(ralign === 'baseline' || ralign === 'axis')) break;
555608
}
556609
}
557610
}
@@ -571,14 +624,16 @@ export class CHTMLmtable<N, T, D> extends CHTMLWrapper<N, T, D> {
571624
*/
572625
protected handleWidth() {
573626
let w = this.node.attributes.get('width') as string;
574-
if (w === 'auto') return;
575-
if (isPercent(w)) {
576-
this.bbox.pwidth = w;
627+
const hasLabels = (this.adaptor.childNodes(this.labels).length > 0);
628+
if (isPercent(w) || hasLabels) {
629+
this.bbox.pwidth = (hasLabels ? '100%' : w);
630+
this.adaptor.setStyle(this.chtml, 'width', '100%');
577631
} else {
632+
if (w === 'auto') return;
578633
w = this.em(this.length2em(w) + (this.frame ? .14 : 0));
579634
}
580-
this.adaptor.setStyle(this.chtml, 'minWidth', w);
581-
this.adaptor.setAttribute(this.chtml, 'width', w);
635+
const table = this.adaptor.firstChild(this.chtml) as N;
636+
this.adaptor.setStyle(table, 'minWidth', w);
582637
}
583638

584639
/*
@@ -599,6 +654,87 @@ export class CHTMLmtable<N, T, D> extends CHTMLWrapper<N, T, D> {
599654

600655
/******************************************************************/
601656

657+
/*
658+
* Handle addition of labels to the table
659+
*/
660+
protected handleLabels() {
661+
const labels = this.labels;
662+
const adaptor = this.adaptor;
663+
if (adaptor.childNodes(labels).length === 0) return;
664+
//
665+
// Set the side for the labels
666+
//
667+
const side = this.node.attributes.get('side') as string;
668+
adaptor.setAttribute(labels, 'side', side);
669+
adaptor.setStyle(labels, side, '0');
670+
//
671+
// Make sure labels don't overlap table
672+
//
673+
const {L} = this.getTableData();
674+
const sep = this.length2em(this.node.attributes.get('minlabelspacing'));
675+
const table = adaptor.firstChild(this.chtml) as N;
676+
adaptor.setStyle(table, 'margin', '0 ' + this.em(L + sep)); // FIXME, handle indentalign values
677+
//
678+
// Add the labels to the table
679+
//
680+
this.updateRowHeights();
681+
this.addLabelSpacing();
682+
adaptor.append(this.chtml, labels);
683+
}
684+
685+
/*
686+
* Update any rows that are not naturally tall enough for the labels
687+
*/
688+
protected updateRowHeights() {
689+
if (this.node.attributes.get('equalrows') as boolean) return;
690+
let {H, D, NH, ND} = this.getTableData();
691+
const space = this.rSpace.map(x => x / 2);
692+
space.unshift(this.fSpace[1]);
693+
space.push(this.fSpace[1]);
694+
for (let i = 0; i < this.numRows; i++) {
695+
if (H[i] !== NH[i] || D[i] !== ND[i]) {
696+
this.setRowHeight(this.childNodes[i], H[i] + D[i], D[i], space[i] + space[i + 1]);
697+
}
698+
}
699+
}
700+
701+
/*
702+
* Add spacing elements between the label rows so align them with the rest of the table
703+
*/
704+
protected addLabelSpacing() {
705+
const adaptor = this.adaptor;
706+
const equal = this.node.attributes.get('equalrows') as boolean;
707+
const {H, D} = this.getTableData();
708+
const HD = (equal ? this.getEqualRowHeight() : 0);
709+
//
710+
// Use half spaces in each row, with frame spacing at top and bottom
711+
//
712+
const space = this.rSpace.map(x => x / 2);
713+
space.unshift(this.fSpace[1]);
714+
space.push(this.fSpace[1]);
715+
//
716+
// Start with frame size and add in spacing, height and depth,
717+
// and line thickness for each non-labeled row.
718+
//
719+
let h = (this.frame ? .07 : 0);
720+
let current = adaptor.firstChild(this.labels) as N;
721+
for (let i = 0; i < this.numRows; i++) {
722+
const row = this.childNodes[i];
723+
if (row.kind === 'mlabeledtr') {
724+
if (h) {
725+
adaptor.insert(this.html('mjx-mtr', {style: {height: this.em(h)}}), current);
726+
adaptor.setStyle(current, 'height', this.em((equal ? HD : H[i] + D[i]) + space[i] + space[i+1]));
727+
current = adaptor.next(current) as N;
728+
h = 0;
729+
}
730+
} else {
731+
h += space[i] + (equal ? HD : H[i] + D[i]) + space[i + 1] + this.rLines[i];
732+
}
733+
}
734+
}
735+
736+
/******************************************************************/
737+
602738
/*
603739
* Get the maximum height of a row
604740
*/

mathjax3-ts/output/chtml/Wrappers/mtd.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export class CHTMLmtd<N, T, D> extends CHTMLWrapper<N, T, D> {
4242
'mjx-mtd': {
4343
display: 'table-cell',
4444
'text-align': 'center',
45-
'padding': '.25em .5em'
45+
'padding': '.215em .4em'
4646
},
4747
'mjx-mtd:first-child': {
4848
'padding-left': 0
@@ -61,6 +61,12 @@ export class CHTMLmtd<N, T, D> extends CHTMLWrapper<N, T, D> {
6161
height: '1em',
6262
'vertical-align': '-.25em'
6363
},
64+
'mjx-labels[align="left"] > mjx-mtr > mjx-mtd': {
65+
'text-align': 'left'
66+
},
67+
'mjx-labels[align="right"] > mjx-mtr > mjx-mtd': {
68+
'text-align': 'right'
69+
},
6470
'mjx-mtr mjx-mtd[rowalign="top"], mjx-mlabeledtr mjx-mtd[rowalign="top"]': {
6571
'vertical-align': 'top'
6672
},
@@ -89,7 +95,9 @@ export class CHTMLmtd<N, T, D> extends CHTMLWrapper<N, T, D> {
8995
if (ralign !== palign) {
9096
this.adaptor.setAttribute(this.chtml, 'rowalign', ralign);
9197
}
92-
if (calign !== 'center') {
98+
if (calign !== 'center' &&
99+
(this.parent.kind !== 'mlabeledtr' || this !== this.parent.childNodes[0] ||
100+
calign !== this.parent.parent.node.attributes.get('side'))) {
93101
this.adaptor.setStyle(this.chtml, 'textAlign', calign);
94102
}
95103
//

0 commit comments

Comments
 (0)