w:tcMar and an ODF cell style's own fo:padding were both dropped: the table's value (in ODF the first cell's) stood for every cell. cellPadding is now an attr on tableCell/tableHeader too, measured against the table's rather than the editor default, so only a real disagreement carries one. --cell-pad inherits, so the cell's own declaration wins for its own td and the CSS is unchanged. Checked against LibreOffice: a 1.2cm cell in a 0.3cm table puts its text at 332.2pt where the grid column starts at 297.6.
97 lines
4.7 KiB
TypeScript
97 lines
4.7 KiB
TypeScript
// 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';
|
||
import { importOdt } from '../../src/lib/import/odt';
|
||
import { buildDocx } from '../../src/lib/export/docx';
|
||
import { importDocx } from '../../src/lib/import/docx';
|
||
import { cellPaddingAttr } from '../../src/lib/editor/extensions/tableCellPadding';
|
||
|
||
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"/);
|
||
}
|
||
});
|
||
|
||
it('collapses a producer-rounded default, keeps a real value', () => {
|
||
// Word's 108 twips and LibreOffice's 0.191cm are both our 0.19cm default.
|
||
expect(cellPaddingAttr([0, 0.1905, 0, 0.191])).toBeNull();
|
||
expect(cellPaddingAttr([0.026, 0.026, 0.026, 0.026])).toEqual([0.026, 0.026, 0.026, 0.026]);
|
||
// A cell measures against its table's, so the editor default is an override there
|
||
// and its own table's value is not.
|
||
expect(cellPaddingAttr([0, 0.19, 0, 0.19], [0.3, 0.3, 0.3, 0.3])).toEqual([0, 0.19, 0, 0.19]);
|
||
expect(cellPaddingAttr([0.3, 0.3, 0.3, 0.3], [0.3, 0.3, 0.3, 0.3])).toBeNull();
|
||
// An undeclared side falls back to the table's, not the editor default.
|
||
expect(cellPaddingAttr([0.1, null, null, null], [0.3, 0.3, 0.3, 0.3])).toEqual([0.1, 0.3, 0.3, 0.3]);
|
||
});
|
||
|
||
it('round-trips one cell’s own margins against the table’s', async () => {
|
||
const mixed: N = structuredClone(doc);
|
||
mixed.content[1].attrs = { cellPadding: [0.3, 0.3, 0.3, 0.3] };
|
||
mixed.content[1].content[0].content[1].attrs.cellPadding = [0.02, 0.6, 0.02, 0.6];
|
||
const margins = { top: 2, bottom: 2, left: 2, right: 2 };
|
||
const cellOf = (d: N, row: number, col: number) =>
|
||
(d.content!.find((n: N) => n.type === 'table') as N).content[row].content[col];
|
||
|
||
const odt = await importOdt(await buildOdt(mixed, margins, 'portrait'));
|
||
expect(cellOf(odt.content, 0, 0).attrs?.cellPadding ?? null).toBeNull();
|
||
expect(cellOf(odt.content, 0, 1).attrs?.cellPadding).toEqual([0.02, 0.6, 0.02, 0.6]);
|
||
|
||
const docx = importDocx(await buildDocx(mixed, margins, 'portrait'));
|
||
expect(cellOf(docx.content, 0, 0).attrs?.cellPadding ?? null).toBeNull();
|
||
for (const [i, cm] of [0.02, 0.6, 0.02, 0.6].entries()) {
|
||
expect(cellOf(docx.content, 0, 1).attrs?.cellPadding[i]).toBeCloseTo(cm, 2);
|
||
}
|
||
});
|
||
|
||
it('round-trips a table’s own cell margins', async () => {
|
||
const tight: N = structuredClone(doc);
|
||
tight.content[1].attrs = { cellPadding: [0.03, 0.05, 0.03, 0.05] };
|
||
const bytes = await buildOdt(tight, { top: 2, bottom: 2, left: 2, right: 2 }, 'portrait');
|
||
const xml = strFromU8(unzipSync(bytes)['content.xml']);
|
||
expect(xml).toMatch(/fo:padding-left="0\.05cm"/);
|
||
expect(xml).toMatch(/fo:padding-top="0\.03cm"/);
|
||
|
||
const back = await importOdt(bytes);
|
||
const table = back.content.content!.find((n: N) => n.type === 'table') as N;
|
||
expect(table.attrs?.cellPadding).toEqual([0.03, 0.05, 0.03, 0.05]);
|
||
|
||
const docx = await buildDocx(tight, { top: 2, bottom: 2, left: 2, right: 2 }, 'portrait');
|
||
const backDocx = importDocx(docx);
|
||
const docxTable = backDocx.content.content!.find((n: N) => n.type === 'table') as N;
|
||
// Word measures in twips, so 0.05cm comes back a twentieth of a point off.
|
||
for (const [i, cm] of [0.03, 0.05, 0.03, 0.05].entries()) {
|
||
expect(docxTable.attrs?.cellPadding[i]).toBeCloseTo(cm, 2);
|
||
}
|
||
});
|
||
});
|