fix: give docx export tables real column widths and cell padding (#2564)
- Add Word's default 108 twip left and right cell margins
This commit is contained in:
parent
59b6142ecd
commit
6a06d7fd7d
3 changed files with 93 additions and 8 deletions
|
|
@ -1 +1 @@
|
|||
Subproject commit 71625f8e246a7680991302fede87ba999c9db9da
|
||||
Subproject commit 662a0042eb6c56a01f8958447780fe2354467b14
|
||||
|
|
@ -26,10 +26,18 @@ import {
|
|||
ITableOptions,
|
||||
ITableRowOptions,
|
||||
IPropertiesOptions,
|
||||
WidthType,
|
||||
} from 'docx';
|
||||
import { imageDimensionsFromData } from 'image-dimensions';
|
||||
import { createNumbering, NumberingStyles } from './numbering';
|
||||
import { buildDoc, createShortId } from './utils';
|
||||
import {
|
||||
buildDoc,
|
||||
CELL_MARGIN,
|
||||
createShortId,
|
||||
getTableCellWidths,
|
||||
getTableColumnWidths,
|
||||
TEXT_WIDTH,
|
||||
} from './utils';
|
||||
import { IFootnotes, INumbering, Mutable, SectionConfig, SerializationState } from './types';
|
||||
|
||||
// This is duplicated from @curvenote/schema
|
||||
|
|
@ -337,7 +345,9 @@ export class DocxSerializerState {
|
|||
const { getCellOptions, getRowOptions, tableOptions } = opts;
|
||||
const actualChildren = this.children;
|
||||
const rows: TableRow[] = [];
|
||||
node.content.forEach((row) => {
|
||||
const columnWidths = getTableColumnWidths(node);
|
||||
const cellWidths = getTableCellWidths(node, columnWidths);
|
||||
node.content.forEach((row, _rowOffset, rowIndex) => {
|
||||
const cells: TableCell[] = [];
|
||||
// Check if all cells are headers in this row
|
||||
let tableHeader = true;
|
||||
|
|
@ -348,10 +358,13 @@ export class DocxSerializerState {
|
|||
});
|
||||
// This scales images inside of tables
|
||||
this.maxImageWidth = MAX_IMAGE_WIDTH / row.content.childCount;
|
||||
row.content.forEach((cell) => {
|
||||
row.content.forEach((cell, _cellOffset, cellIndex) => {
|
||||
this.children = [];
|
||||
this.renderContent(cell);
|
||||
const tableCellOpts: Mutable<ITableCellOptions> = { children: this.children };
|
||||
const tableCellOpts: Mutable<ITableCellOptions> = {
|
||||
children: this.children,
|
||||
width: { size: cellWidths[rowIndex][cellIndex], type: WidthType.DXA },
|
||||
};
|
||||
const colspan = cell.attrs.colspan ?? 1;
|
||||
const rowspan = cell.attrs.rowspan ?? 1;
|
||||
if (colspan > 1) tableCellOpts.columnSpan = colspan;
|
||||
|
|
@ -366,7 +379,13 @@ export class DocxSerializerState {
|
|||
rows.push(new TableRow({ ...(getRowOptions?.(row) || {}), children: cells, tableHeader }));
|
||||
});
|
||||
this.maxImageWidth = MAX_IMAGE_WIDTH;
|
||||
const table = new Table({ ...tableOptions, rows });
|
||||
const table = new Table({
|
||||
width: { size: TEXT_WIDTH, type: WidthType.DXA },
|
||||
columnWidths,
|
||||
margins: { left: CELL_MARGIN, right: CELL_MARGIN },
|
||||
...tableOptions,
|
||||
rows,
|
||||
});
|
||||
actualChildren.push(table);
|
||||
// If there are multiple tables, this seperates them
|
||||
actualChildren.push(new Paragraph(''));
|
||||
|
|
@ -763,6 +782,8 @@ export class DocxSerializerStateAsync {
|
|||
const { getCellOptions, getRowOptions, tableOptions } = opts;
|
||||
const actualChildren = this.children;
|
||||
const rows: TableRow[] = [];
|
||||
const columnWidths = getTableColumnWidths(node);
|
||||
const cellWidths = getTableCellWidths(node, columnWidths);
|
||||
|
||||
for (let rowIndex = 0; rowIndex < node.content.childCount; rowIndex += 1) {
|
||||
const row = node.content.child(rowIndex);
|
||||
|
|
@ -786,7 +807,10 @@ export class DocxSerializerStateAsync {
|
|||
this.children = [];
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await this.renderContent(cell); // Ensure order
|
||||
const tableCellOpts: Mutable<ITableCellOptions> = { children: this.children };
|
||||
const tableCellOpts: Mutable<ITableCellOptions> = {
|
||||
children: this.children,
|
||||
width: { size: cellWidths[rowIndex][cellIndex], type: WidthType.DXA },
|
||||
};
|
||||
const colspan = cell.attrs.colspan ?? 1;
|
||||
const rowspan = cell.attrs.rowspan ?? 1;
|
||||
if (colspan > 1) tableCellOpts.columnSpan = colspan;
|
||||
|
|
@ -803,7 +827,13 @@ export class DocxSerializerStateAsync {
|
|||
}
|
||||
|
||||
this.maxImageWidth = MAX_IMAGE_WIDTH;
|
||||
const table = new Table({ ...tableOptions, rows });
|
||||
const table = new Table({
|
||||
width: { size: TEXT_WIDTH, type: WidthType.DXA },
|
||||
columnWidths,
|
||||
margins: { left: CELL_MARGIN, right: CELL_MARGIN },
|
||||
...tableOptions,
|
||||
rows,
|
||||
});
|
||||
actualChildren.push(table);
|
||||
// If there are multiple tables, this separates them
|
||||
actualChildren.push(new Paragraph(''));
|
||||
|
|
|
|||
|
|
@ -5,14 +5,69 @@ import {
|
|||
ISectionOptions,
|
||||
Packer,
|
||||
SectionType,
|
||||
sectionMarginDefaults,
|
||||
sectionPageSizeDefaults,
|
||||
} from 'docx';
|
||||
import { Node as ProsemirrorNode } from 'prosemirror-model';
|
||||
import { TableMap } from '@tiptap/pm/tables';
|
||||
import { IFootnotes, SerializationState } from './types';
|
||||
|
||||
// twips between the margins of docx's default page
|
||||
export const TEXT_WIDTH =
|
||||
sectionPageSizeDefaults.WIDTH - sectionMarginDefaults.LEFT - sectionMarginDefaults.RIGHT;
|
||||
|
||||
// twips of left and right cell padding Word gives tables by default
|
||||
export const CELL_MARGIN = 108;
|
||||
|
||||
// colwidth is in editor px; these are the editor's default content width and cellMinWidth
|
||||
const EDITOR_WIDTH = 772;
|
||||
const CELL_MIN_WIDTH = 49;
|
||||
|
||||
export function createShortId() {
|
||||
return Math.random().toString(36).slice(2, 11);
|
||||
}
|
||||
|
||||
function sum(values: number[]) {
|
||||
return values.reduce((total, value) => total + value, 0);
|
||||
}
|
||||
|
||||
// Spreads TEXT_WIDTH over the columns in their editor proportions. Columns never resized
|
||||
// have no colwidth and share what the resized ones leave of the editor width.
|
||||
export function getTableColumnWidths(table: ProsemirrorNode): number[] {
|
||||
const colwidths: number[] = [];
|
||||
table.firstChild?.forEach((cell) => {
|
||||
for (let i = 0; i < cell.attrs.colspan; i += 1) {
|
||||
colwidths.push(cell.attrs.colwidth?.[i] ?? 0);
|
||||
}
|
||||
});
|
||||
|
||||
const columnCount = TableMap.get(table).width;
|
||||
const resized = colwidths.filter((width) => width > 0);
|
||||
const unresizedCount = columnCount - resized.length;
|
||||
const share = unresizedCount ? (EDITOR_WIDTH - sum(resized)) / unresizedCount : 0;
|
||||
const widths = Array.from({ length: columnCount }, (_, i) =>
|
||||
Math.max(colwidths[i] || share, CELL_MIN_WIDTH),
|
||||
);
|
||||
|
||||
const total = sum(widths);
|
||||
return widths.map((width) => Math.round((width / total) * TEXT_WIDTH));
|
||||
}
|
||||
|
||||
// Each cell, by row and cell index, is as wide as the columns it spans.
|
||||
export function getTableCellWidths(table: ProsemirrorNode, columnWidths: number[]): number[][] {
|
||||
const map = TableMap.get(table);
|
||||
const cellWidths: number[][] = [];
|
||||
table.forEach((row, rowOffset) => {
|
||||
const rowWidths: number[] = [];
|
||||
row.forEach((_cell, cellOffset) => {
|
||||
const { left, right } = map.findCell(rowOffset + 1 + cellOffset);
|
||||
rowWidths.push(sum(columnWidths.slice(left, right)));
|
||||
});
|
||||
cellWidths.push(rowWidths);
|
||||
});
|
||||
return cellWidths;
|
||||
}
|
||||
|
||||
export function buildDoc(state: SerializationState, opts?: IPropertiesOptions): Document {
|
||||
let sections = state?.sections?.length
|
||||
? state.sections.map((section) => ({
|
||||
|
|
|
|||
Loading…
Reference in a new issue