fix: use Word's asymmetric cell padding for tables
Cells padded 0.1cm on all sides; Word and LibreOffice use 0.19cm left and right and none top or bottom. Text sat 0.7mm too far left and every row was 2mm too tall, so the offset accumulated down a table. odf-kit only writes the fo:padding shorthand, which ODF defines as a single length — LibreOffice drops a two-value form — so the export expands it per side.
This commit is contained in:
1 parent
52cf5ac702
commit
275a77564e
3 files changed
+67
-4
No files matched your search
+24
-2
@@ -38,7 +38,10 @@ 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';
|
||||
const CELL_PADDING = '0.1cm'; // must match td/th padding in editor.css
|
||||
// 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.
|
||||
@@ -739,6 +742,25 @@ function applyTableRowHeights(odtBytes: Uint8Array, heights: (string | null)[]):
|
||||
return rezipOdt(files);
|
||||
}
|
||||
|
||||
// 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).
|
||||
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,
|
||||
);
|
||||
|
||||
files['content.xml'] = strToU8(content);
|
||||
return rezipOdt(files);
|
||||
}
|
||||
|
||||
// A dragged table edge → the table style's fo:margin-left/-right + style:width (with
|
||||
// odf-kit's table:align="margins" the table fills exactly what's left). odf-kit names
|
||||
// table styles Table1, Table2, … in document order, matching the descriptor list.
|
||||
@@ -2856,7 +2878,7 @@ export async function buildOdt(docJson: TiptapNode, margins: PageMargins = DEFAU
|
||||
|
||||
const rowHeights: (string | null)[] = [];
|
||||
collectTableRowHeights(raw, rowHeights);
|
||||
const styledRows = applyTableRowHeights(styledCells, rowHeights);
|
||||
const styledRows = applyTableRowHeights(expandCellPadding(styledCells), rowHeights);
|
||||
const withTableMargins = applyTableStyleNames(
|
||||
applyTableMargins(styledRows, tableMargins, contentWidthCm), tableStyleNames);
|
||||
|
||||
|
||||
@@ -460,7 +460,8 @@
|
||||
and the exported .odt look identical (see export/odt.ts CUST_TABLE handler):
|
||||
• border → export emits 0.5pt solid #000000; 1px solid renders crisply
|
||||
on screen and is the visual equivalent of a thin 0.5pt line.
|
||||
• padding → export emits fo:padding 0.1cm per cell (CELL_PADDING).
|
||||
• padding → Word's/LibreOffice's cell margins; export emits them per side
|
||||
(CELL_PADDING_X/_Y, expanded by expandCellPadding).
|
||||
Tables are wrapped in <div class="tableWrapper"> by TipTap's node view. */
|
||||
.paper .tiptap .tableWrapper {
|
||||
/* Default is flush with the text area; a dragged outer edge sets an inline
|
||||
@@ -482,7 +483,7 @@
|
||||
.paper .tiptap td,
|
||||
.paper .tiptap th {
|
||||
border: 1px solid #000;
|
||||
padding: 0.1cm;
|
||||
padding: 0 0.19cm;
|
||||
vertical-align: top;
|
||||
/* Positioning context for the column-resize handle widget (a child div the
|
||||
resize plugin places at the cell's right edge). */
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
// odf-kit writes the fo:padding shorthand, which LibreOffice drops when given the
|
||||
// two-value form Word's asymmetric cell margins need — so the export expands it per
|
||||
// side. jsdom (vitest `environment`) supplies the global DOMParser.
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { unzipSync, strFromU8 } from 'fflate';
|
||||
import { buildOdt } from '../../src/lib/export/odt';
|
||||
|
||||
type N = any;
|
||||
|
||||
const P = (text: string): N => ({ type: 'paragraph', content: [{ type: 'text', text }] });
|
||||
const cell = (text: string): N =>
|
||||
({ type: 'tableCell', attrs: { colspan: 1, rowspan: 1, colwidth: [120] }, content: [P(text)] });
|
||||
|
||||
const doc: N = {
|
||||
type: 'doc',
|
||||
content: [
|
||||
P('Before'),
|
||||
{ type: 'table', content: [
|
||||
{ type: 'tableRow', content: [cell('A1'), cell('B1')] },
|
||||
{ type: 'tableRow', content: [cell('A2'), cell('B2')] },
|
||||
] },
|
||||
],
|
||||
};
|
||||
|
||||
describe('table cell padding', () => {
|
||||
it('exports per-side padding, never the shorthand', async () => {
|
||||
const bytes = await buildOdt(doc, { top: 2, bottom: 2, left: 2, right: 2 }, 'portrait');
|
||||
const xml = strFromU8(unzipSync(bytes)['content.xml']);
|
||||
|
||||
const cellProps = xml.match(/<style:table-cell-properties\b[^>]*>/g) ?? [];
|
||||
expect(cellProps.length).toBeGreaterThan(0);
|
||||
for (const props of cellProps) {
|
||||
expect(props).not.toMatch(/fo:padding="/);
|
||||
expect(props).toMatch(/fo:padding-left="0\.19cm"/);
|
||||
expect(props).toMatch(/fo:padding-right="0\.19cm"/);
|
||||
expect(props).toMatch(/fo:padding-top="0cm"/);
|
||||
expect(props).toMatch(/fo:padding-bottom="0cm"/);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user