feat: a table keeps the cell margins its file gave it
The cell padding was a constant in three places that disagreed with each other, so a narrow column lost 0.38cm it did not have: a bibliography marker column of 0.71cm wrapped "[2] " onto three lines and every entry grew with it, costing a fixture a whole page. The table now carries its own margins (w:tblCellMar, direct or from the table style; fo:padding on the ODF cell style) and both exports write them back. A side within a twentieth of a millimetre of the default is the default, or Word's 108 twips would tag every table.
This commit is contained in:
1 parent
e4b679ad8b
commit
db0fe71cdb
11 files changed
+174
-16
No files matched your search
@@ -20,6 +20,7 @@ import { pageDimsCm, type PageFormat } from '../storage/pageFormat';
|
||||
import { HF_DISTANCE_CM, hfIsEmpty } from '../storage/headerFooter';
|
||||
import { HEADER_SHADE } from '../editor/extensions/tableHeaderRow';
|
||||
import { parseBorderAttr, type BorderSide } from '../editor/extensions/tableCellBorders';
|
||||
import { parseCellPadding, DEFAULT_CELL_PADDING } from '../editor/extensions/tableCellPadding';
|
||||
import { parseTabStops, type TabAlign } from '../editor/extensions/tabStops';
|
||||
import { charStyleProps, listMarkerFormat } from '../editor/extensions/listMarker';
|
||||
import { effectiveOrderedDefAt, formatOrdinal, childCycle, ROOT_ORDERED_CYCLE, type OrderedCycle } from '../utils/orderedListTypes';
|
||||
@@ -805,6 +806,7 @@ function tableToDocx(node: TiptapNode, contentWidthCm: number, num: Numbering):
|
||||
let ml = Math.max(0, Number(node.attrs?.marginLeft) || 0);
|
||||
let mr = Math.max(0, Number(node.attrs?.marginRight) || 0);
|
||||
if (ml + mr > contentWidthCm - 1) { ml = 0; mr = 0; }
|
||||
const pad = parseCellPadding(node.attrs?.cellPadding) ?? DEFAULT_CELL_PADDING;
|
||||
const colsCm = columnWidthsCm(node, contentWidthCm - ml - mr);
|
||||
const colsTwip = colsCm?.map(cmToTwip);
|
||||
const totalTwip = colsTwip ? colsTwip.reduce((a, b) => a + b, 0) : cmToTwip(contentWidthCm - ml - mr);
|
||||
@@ -856,7 +858,7 @@ function tableToDocx(node: TiptapNode, contentWidthCm: number, num: Numbering):
|
||||
width: { size: totalTwip, type: WidthType.DXA },
|
||||
indent: ml ? { size: cmToTwip(ml), type: WidthType.DXA } : undefined,
|
||||
layout: TableLayoutType.FIXED,
|
||||
margins: { marginUnitType: WidthType.DXA, top: cmToTwip(0.05), bottom: cmToTwip(0.05), left: cmToTwip(0.1), right: cmToTwip(0.1) },
|
||||
margins: { marginUnitType: WidthType.DXA, top: cmToTwip(pad[0]), right: cmToTwip(pad[1]), bottom: cmToTwip(pad[2]), left: cmToTwip(pad[3]) },
|
||||
borders: {
|
||||
top: cellBorder, bottom: cellBorder, left: cellBorder, right: cellBorder,
|
||||
insideHorizontal: cellBorder, insideVertical: cellBorder,
|
||||
|
||||
+14
-11
@@ -14,6 +14,7 @@ import {
|
||||
type TableStyleRef = { name: string; look: TableLook };
|
||||
import { HEADER_SHADE } from '../editor/extensions/tableHeaderRow';
|
||||
import { BORDER_SIDES, parseBorderAttr } from '../editor/extensions/tableCellBorders';
|
||||
import { parseCellPadding, DEFAULT_CELL_PADDING } from '../editor/extensions/tableCellPadding';
|
||||
import { TEXTBOX_PADDING_CM } from '../editor/extensions/textBox';
|
||||
import { parseTabStops } from '../editor/extensions/tabStops';
|
||||
import { charStyleProps, listMarkerFormat, type MarkerFormat } from '../editor/extensions/listMarker';
|
||||
@@ -40,10 +41,6 @@ const CUST_HF = '__cust_hf__';
|
||||
// Table export styling. Values mirror the editor's table CSS (src/styles/editor.css)
|
||||
// so the .odt matches the on-screen preview.
|
||||
const TABLE_BORDER = '0.5pt solid #000000';
|
||||
// Word's/LibreOffice's cell margins, asymmetric. Must match td/th padding in editor.css.
|
||||
const CELL_PADDING_X = '0.19cm';
|
||||
const CELL_PADDING_Y = '0cm';
|
||||
const CELL_PADDING = CELL_PADDING_X;
|
||||
|
||||
// odf-kit emits this as the document's default font (Standard style). The editor
|
||||
// renders the bundled, metric-identical Liberation Serif on screen.
|
||||
@@ -764,18 +761,21 @@ function applyTableRowHeights(odtBytes: Uint8Array, heights: (string | null)[]):
|
||||
}
|
||||
|
||||
// odf-kit only writes the fo:padding shorthand, which ODF defines as one length —
|
||||
// LibreOffice drops a two-value form. Cells need Word's asymmetric margins, so the
|
||||
// shorthand is expanded per side (scoped to cell properties; paragraphs use it too).
|
||||
// LibreOffice drops a multi-value form. The cells carry their table's margins as a
|
||||
// CSS-style TRBL shorthand, which this expands per side (scoped to cell properties).
|
||||
function expandCellPadding(odtBytes: Uint8Array): Uint8Array {
|
||||
const files = unzipSync(odtBytes);
|
||||
const contentBytes = files['content.xml'];
|
||||
if (!contentBytes) return odtBytes;
|
||||
|
||||
const perSide = `fo:padding-left="${CELL_PADDING_X}" fo:padding-right="${CELL_PADDING_X}"`
|
||||
+ ` fo:padding-top="${CELL_PADDING_Y}" fo:padding-bottom="${CELL_PADDING_Y}"`;
|
||||
const content = strFromU8(contentBytes).replace(
|
||||
/(<style:table-cell-properties\b[^>]*?)fo:padding="[^"]*"/g,
|
||||
(_m, head: string) => head + perSide,
|
||||
/(<style:table-cell-properties\b[^>]*?)fo:padding="([^"]*)"/g,
|
||||
(_m, head: string, value: string) => {
|
||||
const [top, right, bottom, left] = value.trim().split(/\s+/);
|
||||
if (!left) return head + `fo:padding="${value}"`;
|
||||
return head + `fo:padding-top="${top}" fo:padding-right="${right}"`
|
||||
+ ` fo:padding-bottom="${bottom}" fo:padding-left="${left}"`;
|
||||
},
|
||||
);
|
||||
|
||||
files['content.xml'] = strToU8(content);
|
||||
@@ -2489,6 +2489,9 @@ function exportTable(node: TiptapNode, doc: OdtDocument, contentWidthCm: number,
|
||||
const look = parseTableLook(node.attrs?.tableLook);
|
||||
tableStyleNames.push(tableStyle && styleName ? { name: styleName, look } : null);
|
||||
const columnWidths = tableColumnWidthsCm(node, contentWidthCm - (margins ? margins.ml + margins.mr : 0));
|
||||
// The table's cell margins ride on every cell as a TRBL shorthand; expandCellPadding
|
||||
// splits it, since odf-kit only writes fo:padding and ODF only allows one length.
|
||||
const cellPad = (parseCellPadding(node.attrs?.cellPadding) ?? DEFAULT_CELL_PADDING).map(n => `${n}cm`).join(' ');
|
||||
doc.addTable((t: TableBuilder) => {
|
||||
for (const row of rows) {
|
||||
t.addRow((r: RowBuilder) => {
|
||||
@@ -2497,7 +2500,7 @@ function exportTable(node: TiptapNode, doc: OdtDocument, contentWidthCm: number,
|
||||
// Emit the cell's runs (SEG-separated) and record its descriptor; addCell is
|
||||
// synchronous, so the push stays in document order, matching applyCellBlocks'
|
||||
// walk. Spans make odf-kit emit covered cells, which its cell regex never matches.
|
||||
const opts: CellOptions = { padding: CELL_PADDING };
|
||||
const opts: CellOptions = { padding: cellPad };
|
||||
const colspan = (cell.attrs?.colspan as number) ?? 1;
|
||||
const rowspan = (cell.attrs?.rowspan as number) ?? 1;
|
||||
if (colspan > 1) opts.colSpan = colspan;
|
||||
|
||||
Reference in new issue
Block a user