import { describe, it, expect } from 'vitest';
import { zipSync, strToU8 } from 'fflate';
import { importOdt } from '../../src/lib/import/odt';
// LibreOffice defines heading styles relative to the Heading style ("130%"), so a
// percentage must resolve against the inherited size — otherwise the file's own size is
// dropped and the editor default silently overrides it.
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:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0"';
function odtWithHeading(base: string, size: string): Uint8Array {
const styles = `
`;
const content = `
Kapitel
`;
return zipSync({ 'content.xml': strToU8(content), 'styles.xml': strToU8(styles) });
}
// The size a heading renders at: from the imported style registry, since a named
// style's formatting is no longer copied onto the block.
const styleSizeOf = (base: string, size: string) =>
importOdt(odtWithHeading(base, size)).styles.paragraph['Heading 1']?.text.fontSizePt;
const markSizeOf = (base: string, size: string) => {
const marks = (importOdt(odtWithHeading(base, size)).content.content?.[0] as any).content[0].marks ?? [];
return marks.find((m: any) => m.type === 'textStyle')?.attrs?.fontSize;
};
describe('percentage font sizes in the ODF style chain', () => {
it('resolves a percentage against the parent style', () => {
expect(styleSizeOf('12pt', '150%')).toBe(18);
expect(styleSizeOf('16pt', '125%')).toBe(20);
expect(styleSizeOf('14pt', '130%')).toBe(18.2);
});
it('still reads absolute sizes', () => {
expect(styleSizeOf('14pt', '24pt')).toBe(24);
});
it('leaves the block free of direct formatting either way', () => {
expect(markSizeOf('12pt', '150%')).toBeUndefined();
expect(markSizeOf('16pt', '125%')).toBeUndefined();
});
});
// A document written by LibreOffice must come in looking exactly as it does there:
// its heading styles equal the editor defaults, so nothing lands as direct formatting.
describe('LibreOffice heading styles import unchanged', () => {
const LO_SIZES = ['18pt', '16pt', '14pt', '13pt', '12pt'];
const headingStyles = LO_SIZES.map(
(size, i) => `
`,
).join('');
const styles = `
${headingStyles}`;
const content = `
${LO_SIZES.map((_, i) => `H${i + 1}`).join('')}
`;
const doc = importOdt(zipSync({ 'content.xml': strToU8(content), 'styles.xml': strToU8(styles) })).content;
it('keeps levels 1-5 without any direct formatting', () => {
const blocks = (doc.content ?? []) as any[];
expect(blocks.map((b) => b.type)).toEqual(Array(5).fill('heading'));
expect(blocks.map((b) => b.attrs.level)).toEqual([1, 2, 3, 4, 5]);
// No size mark (sizes match), no font mark (Liberation Sans is the heading default),
// no spacing attrs (margins match) — the file renders as the editor's own heading style.
expect(blocks.flatMap((b) => b.content[0].marks ?? [])).toEqual([]);
expect(blocks.flatMap((b) => [b.attrs.spaceBefore, b.attrs.spaceAfter].filter((v) => v != null))).toEqual([]);
});
});