From 784dd5e06ff2d89db908175bfb2e0e2dd7f34d9f Mon Sep 17 00:00:00 2001 From: Steffen Becker Date: Wed, 2 Sep 2026 21:59:23 +0200 Subject: [PATCH] fix: a table aligned to the margins fills the text width MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ODF's table:align="margins" means the table spans between its own margins, and style:width is then the width the producing page had. Read as a left-aligned table, the undeclared right margin was derived from that width against ours and the table came out narrow — measured 12.4cm on a 14.7cm text width, its column rule 10.4mm left of where LibreOffice draws it. --- src/lib/import/odt.ts | 6 ++- tests/unit/table-align-margins.test.ts | 56 ++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 tests/unit/table-align-margins.test.ts diff --git a/src/lib/import/odt.ts b/src/lib/import/odt.ts index 1b53e5f..575b5bb 100644 --- a/src/lib/import/odt.ts +++ b/src/lib/import/odt.ts @@ -2881,8 +2881,12 @@ function tableMargins(el: Element, ctx: Ctx): { marginLeft?: number; marginRight let left = lengthToCm(props['fo:margin-left']); let right = lengthToCm(props['fo:margin-right']); + const align = props['table:align']; + // "margins" is ODF for a table filling the space between its own: the side it does + // not declare is 0, and style:width is the width the producer's page had, not one to + // measure a margin against. + if (align === 'margins') { left ??= 0; right ??= 0; } if (left == null && width != null) { - const align = props['table:align']; left = align === 'center' ? (content - width) / 2 : align === 'right' ? content - width : 0; } left = left ?? 0; // negative is real: a table may hang into the page margin diff --git a/tests/unit/table-align-margins.test.ts b/tests/unit/table-align-margins.test.ts new file mode 100644 index 0000000..5093166 --- /dev/null +++ b/tests/unit/table-align-margins.test.ts @@ -0,0 +1,56 @@ +import { describe, it, expect } from 'vitest'; +import { zipSync, strToU8 } from 'fflate'; +import { importOdt } from '../../src/lib/import/odt'; + +// table:align="margins" is a table stretched between the text margins. Its style:width +// is the width the producing page had, so deriving a margin from it against ours leaves +// the table narrow — measured 12.4cm on a 14.7cm text width. +const NS = + 'xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" ' + + 'xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" ' + + 'xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0" ' + + 'xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0" ' + + 'xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0"'; + +function odtWithTable(tableProps: string): Uint8Array { + const styles = ` + + + + + + `; + const content = ` + + + + + a + + `; + return zipSync({ 'content.xml': strToU8(content), 'styles.xml': strToU8(styles) }); +} + +const attrsOf = (tableProps: string) => + (importOdt(odtWithTable(tableProps)).content.content?.[0] as { attrs?: Record }).attrs ?? {}; + +describe('a table aligned to the margins', () => { + it('keeps the full text width whatever width the producer wrote', () => { + const attrs = attrsOf('style:width="14.7cm" table:align="margins"'); + expect(attrs.marginLeft).toBeUndefined(); + expect(attrs.marginRight).toBeUndefined(); + }); + + it('still honours the margins the file declares itself', () => { + const attrs = attrsOf('style:width="12cm" table:align="margins" fo:margin-left="1.5cm"'); + expect(attrs.marginLeft).toBe(1.5); + expect(attrs.marginRight).toBeUndefined(); + }); + + it('leaves a centred table centred', () => { + const attrs = attrsOf('style:width="10.7cm" table:align="center"'); + expect(attrs.marginLeft).toBe(2); + expect(attrs.marginRight).toBe(2); + }); +});