diff --git a/src/lib/import/styleResolver.ts b/src/lib/import/styleResolver.ts index eec8f49..a9cf836 100644 --- a/src/lib/import/styleResolver.ts +++ b/src/lib/import/styleResolver.ts @@ -587,12 +587,11 @@ export class StyleResolver { const height = lengthToCm(props.getAttributeNS(NS.svg, 'height')) ?? lengthToCm(props.getAttributeNS(NS.fo, 'min-height')) ?? 0; - // style:dynamic-spacing="true" makes the header↔body gap collapsible (the header - // grows into it), so the body isn't pushed down by it. Only fixed (non-dynamic) - // spacing is added to the body margin. - const dynamic = props.getAttributeNS(NS.style, 'dynamic-spacing') === 'true'; - const spacing = dynamic ? 0 : (lengthToCm(props.getAttributeNS(NS.fo, spacingAttr)) ?? 0); - return height + spacing; + // The zone's gap to the body is laid out *inside* that height, not added to it + // (probed: a footer's fo:margin-top moves its text down the band and leaves the + // last body line where it was; style:dynamic-spacing changes neither side). + const spacing = lengthToCm(props.getAttributeNS(NS.fo, spacingAttr)) ?? 0; + return Math.max(height, spacing); }; return { diff --git a/tests/roundtrip.test.ts b/tests/roundtrip.test.ts index c14a8e0..c54ea5d 100644 --- a/tests/roundtrip.test.ts +++ b/tests/roundtrip.test.ts @@ -1075,9 +1075,10 @@ describe('Leg 4: foreign header/footer → importOdt', () => { check('foreign hf: page-number field', hPara?.content?.some((n: N) => n.type === 'pageNumber'), hPara); check('foreign hf: page-count field', hPara?.content?.some((n: N) => n.type === 'pageCount'), hPara); check('foreign hf: footer text', fhf.footer?.content?.[0]?.content?.[0]?.text === 'Confidential', fhf.footer); - // Body top margin = page margin (1.5) + header height (0.8) + spacing (0.3) = 2.6. - check('foreign hf: body top margin reconstructed', Math.abs((fhf.margins?.top ?? 0) - 2.6) < 0.02, fhf.margins); - check('foreign hf: body bottom margin reconstructed', Math.abs((fhf.margins?.bottom ?? 0) - 2.4) < 0.02, fhf.margins); + // Body top margin = page margin (1.5) + the header band (0.8): its 0.3 spacing to + // the body is laid out inside that height, not added to it (styleResolver.ts). + check('foreign hf: body top margin reconstructed', Math.abs((fhf.margins?.top ?? 0) - 2.3) < 0.02, fhf.margins); + check('foreign hf: body bottom margin reconstructed', Math.abs((fhf.margins?.bottom ?? 0) - 2.1) < 0.02, fhf.margins); // header-first is a supported variant (Word "Different First Page" / ODF header-first). check('foreign hf: first-page header parsed', fhf.headerFirst?.content?.[0]?.content?.[0]?.text === 'Cover', fhf.headerFirst); check('foreign hf: different-first-page flag set', fhf.differentFirstPage === true, fhf.differentFirstPage);