Word's w:tblpPr takes a table out of the flow; the editor kept it in, so whatever followed — and every frame anchored to it — sat as far down as the table is tall. It now arrives as the box whose content a table may be, which is the shape LibreOffice keeps such a table in, and both exporters write it back as the file's own floating one. A drawing in one of its cells is ordinary table text, so the box-in-box guard lets it stand. Claude-Session: https://claude.ai/code/session_01Wzs2qG6Y5qm7mKmJHfu44G
61 lines
2.7 KiB
TypeScript
61 lines
2.7 KiB
TypeScript
// Word's floating table (w:tblpPr) is a frame holding a table — LibreOffice's own model
|
|
// for it — so what follows the table keeps its place instead of starting below it.
|
|
import { describe, it, expect } from 'vitest';
|
|
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 { unzipSync, strFromU8 } from 'fflate';
|
|
|
|
type N = any;
|
|
|
|
const cell = (text: string): N => ({ type: 'tableCell', attrs: { colspan: 1, rowspan: 1, colwidth: [180] },
|
|
content: [{ type: 'paragraph', content: [{ type: 'text', text }] }] });
|
|
|
|
const table: N = { type: 'table', content: [
|
|
{ type: 'tableRow', content: [cell('Report'), cell('2018')] },
|
|
{ type: 'tableRow', content: [cell('Company'), cell('Name')] },
|
|
] };
|
|
|
|
const doc: N = { type: 'doc', content: [
|
|
{ type: 'paragraph', content: [
|
|
{ type: 'textBox', attrs: { width: 372, wrap: 'left', wrapDist: 0.32 }, content: [table] },
|
|
{ type: 'text', text: 'beside the frame' },
|
|
] },
|
|
{ type: 'paragraph', content: [{ type: 'text', text: 'Body' }] },
|
|
] };
|
|
|
|
const find = (n: N, type: string): N => {
|
|
if (n.type === type) return n;
|
|
for (const c of n.content ?? []) { const hit = find(c, type); if (hit) return hit; }
|
|
return null;
|
|
};
|
|
const paraText = (p: N): string => (p.content ?? []).filter((n: N) => n.type === 'text').map((n: N) => n.text).join('');
|
|
const cellTexts = (t: N): string[] => (t.content ?? []).flatMap((r: N) =>
|
|
(r.content ?? []).map((c: N) => find(c, 'text')?.text ?? ''));
|
|
|
|
describe('a floating table', () => {
|
|
it('round-trips through ODF as a frame holding the table', async () => {
|
|
const bytes = await buildOdt(doc);
|
|
const xml = strFromU8(unzipSync(bytes)['content.xml']);
|
|
expect(xml).toMatch(/<draw:text-box[^>]*>\s*<table:table/);
|
|
const back = importOdt(bytes).content as N;
|
|
const box = find(back, 'textBox');
|
|
expect(box.attrs).toMatchObject({ wrap: 'left' });
|
|
expect(cellTexts(box.content[0])).toEqual(['Report', '2018', 'Company', 'Name']);
|
|
expect(paraText(back.content[0])).toBe('beside the frame');
|
|
});
|
|
|
|
it('round-trips through DOCX as w:tblpPr', async () => {
|
|
const bytes = await buildDocx(doc);
|
|
const xml = strFromU8(unzipSync(bytes)['word/document.xml']);
|
|
expect(xml).toMatch(/<w:tblpPr[^>]*w:horzAnchor="margin"/);
|
|
const back = importDocx(bytes).content as N;
|
|
const box = find(back, 'textBox');
|
|
expect(box.attrs).toMatchObject({ wrap: 'left' });
|
|
expect(cellTexts(box.content[0])).toEqual(['Report', '2018', 'Company', 'Name']);
|
|
// The table is out of the flow: the text it was anchored beside stays with it.
|
|
expect(paraText(back.content[0])).toBe('beside the frame');
|
|
});
|
|
});
|