diff --git a/src/lib/export/odt.ts b/src/lib/export/odt.ts index 73982e4..646b98e 100644 --- a/src/lib/export/odt.ts +++ b/src/lib/export/odt.ts @@ -875,7 +875,7 @@ function applyTableRowHeights(odtBytes: Uint8Array, heights: (string | null)[]): ``, ).join('\n'); - content = content.replace('', `${newStyles}\n`); + content = injectAutomaticStyles(content, `${newStyles}\n`); files['content.xml'] = strToU8(content); return rezipOdt(files); @@ -1012,7 +1012,7 @@ function applyListItemStyles(odtBytes: Uint8Array, styles: ParaStyle[]): Uint8Ar ``, ).join('\n'); - content = content.replace('', `${newStyles}\n`); + content = injectAutomaticStyles(content, `${newStyles}\n`); files['content.xml'] = strToU8(content); return rezipOdt(files); @@ -1095,7 +1095,7 @@ function applyPageBreaks(odtBytes: Uint8Array): Uint8Array { content = content.split(PGB).join(''); if (minted.length) { - content = content.replace('', `${minted.join('')}`); + content = injectAutomaticStyles(content, minted.join('')); } files['content.xml'] = strToU8(content); @@ -1179,7 +1179,7 @@ function applyEmptyLineFontSizes(odtBytes: Uint8Array): Uint8Array { content = content.replace(new RegExp(`${FSZ}[^${FSZ}]*${FSZ}`, 'g'), ''); if (minted.length) { - content = content.replace('', `${minted.join('')}`); + content = injectAutomaticStyles(content, minted.join('')); } files['content.xml'] = strToU8(content); @@ -1424,7 +1424,7 @@ function applyParagraphStyles(odtBytes: Uint8Array): Uint8Array { content = content.replace(new RegExp(`${STY}[^${STY}]*${STY}`, 'g'), ''); if (minted.length) { - content = content.replace('', `${minted.join('')}`); + content = injectAutomaticStyles(content, minted.join('')); } files['content.xml'] = strToU8(content); @@ -1480,7 +1480,7 @@ function applyParagraphBoxes(odtBytes: Uint8Array): Uint8Array { content = content.replace(new RegExp(`${PBX}[^${PBX}]*${PBX}`, 'g'), ''); if (minted.length) { - content = content.replace('', `${minted.join('')}`); + content = injectAutomaticStyles(content, minted.join('')); } files['content.xml'] = strToU8(content); @@ -1900,7 +1900,7 @@ function applyNestedListTypes(odtBytes: Uint8Array, fixes: (ListDef | null)[]): }); if (minted.length) { - content = content.replace('', `${minted.join('\n')}\n`); + content = injectAutomaticStyles(content, `${minted.join('\n')}\n`); } files['content.xml'] = strToU8(content); return rezipOdt(files); @@ -2093,7 +2093,7 @@ function applyCellBlocks(odtBytes: Uint8Array, cellBlocks: CellBlock[][]): Uint8 } if (additions.length) { - content = content.replace('', `${additions.join('\n')}\n`); + content = injectAutomaticStyles(content, `${additions.join('\n')}\n`); } files['content.xml'] = strToU8(content); @@ -2446,7 +2446,7 @@ function applyCharacterStyles(odtBytes: Uint8Array): Uint8Array { content = content.replace(new RegExp(`${CST}[^${CST}]*${CST}`, 'g'), ''); if (minted.length) { - content = content.replace('', `${minted.join('')}`); + content = injectAutomaticStyles(content, minted.join('')); } files['content.xml'] = strToU8(content); @@ -2510,9 +2510,7 @@ function applyTextEffects(odtBytes: Uint8Array): Uint8Array { const content = resolveTextEffects(before, (s) => minted.push(s), 'TE'); if (content === before) return odtBytes; - files['content.xml'] = strToU8( - minted.length ? content.replace('', `${minted.join('')}`) : content, - ); + files['content.xml'] = strToU8(injectAutomaticStyles(content, minted.join(''))); return rezipOdt(files); } @@ -2831,16 +2829,19 @@ function hfBackgroundStyleXml(index: number): string { ); } -// Inject automatic styles, tolerating an empty/self-closed or absent section. +// Inject automatic styles into content.xml or styles.xml, tolerating an empty/self-closed +// or absent section — a document odf-kit gave none writes ``, and +// a plain string replace on the closing tag would drop the styles there. function injectAutomaticStyles(content: string, styles: string): string { if (!styles) return content; if (content.includes('')) { return content.replace('', `${styles}`); } + const section = `${styles}`; if (content.includes('')) { - return content.replace('', `${styles}`); + return content.replace('', section); } - return content.replace('${styles} `${section}${m}`); } // Resolve image sentinels: swap each IMG{i}IMG for its , add the binary @@ -3587,8 +3588,7 @@ function applyHfPostProcess(odtBytes: Uint8Array, margins: PageMargins, headerPa } if (mintedStyles.length) { const defs = mintedStyles.join(''); - if (styles.includes('')) styles = styles.replace('', `${defs}`); - else styles = styles.replace('', `${defs}`); + styles = injectAutomaticStyles(styles, defs); } // Resolve HFIMG sentinels (default + first-page zones) to as-char s, then @@ -3601,8 +3601,7 @@ function applyHfPostProcess(odtBytes: Uint8Array, margins: PageMargins, headerPa }); const bgStyles = hfImages.map((img, i) => (img.wrap === 'inline' ? '' : hfBackgroundStyleXml(i))).join(''); if (bgStyles) { - if (styles.includes('')) styles = styles.replace('', `${bgStyles}`); - else styles = styles.replace('', `${bgStyles}`); + styles = injectAutomaticStyles(styles, bgStyles); } for (const img of hfImages) files[img.path] = img.bytes; const manifestBytes = files['META-INF/manifest.xml']; @@ -3719,9 +3718,7 @@ function applySectionMasterPages(odtBytes: Uint8Array, sets: HfSet[], pageCount: }, ); if (minted.length) { - content = content.includes('') - ? content.replace('', `${minted.join('')}`) - : content.replace('', `${minted.join('')}`); + content = injectAutomaticStyles(content, minted.join('')); } // Reuse the page layout odf-kit gave the Standard master page: the geometry is @@ -3737,9 +3734,7 @@ function applySectionMasterPages(odtBytes: Uint8Array, sets: HfSet[], pageCount: if (pages.length) styles = styles.replace('', `${pages.join('')}`); if (hfStyles.length) { const defs = hfStyles.join(''); - styles = styles.includes('') - ? styles.replace('', `${defs}`) - : styles.replace('', `${defs}`); + styles = injectAutomaticStyles(styles, defs); } files['content.xml'] = strToU8(content); diff --git a/tests/unit/odt-page-break-export.test.ts b/tests/unit/odt-page-break-export.test.ts new file mode 100644 index 0000000..060613c --- /dev/null +++ b/tests/unit/odt-page-break-export.test.ts @@ -0,0 +1,24 @@ +import { describe, it, expect } from 'vitest'; +import { unzipSync, strFromU8 } from 'fflate'; +import { buildOdt } from '../../src/lib/export/odt'; + +// A document odf-kit mints no automatic style for carries ``, +// self-closed — replacing the closing tag silently dropped every minted style there, +// so a manual page break exported as a plain paragraph. +const doc = { + type: 'doc', + content: [ + { type: 'paragraph', content: [{ type: 'text', text: 'one' }] }, + { type: 'paragraph', attrs: { breakBefore: 'page' }, content: [{ type: 'text', text: 'two' }] }, + ], +}; + +describe('ODF page break export', () => { + it('mints the break style even with no automatic styles section', async () => { + const bytes = await buildOdt(doc as never, undefined, 'portrait'); + const content = strFromU8(unzipSync(bytes)['content.xml']); + const name = /style:name="(PB\d+)"[^>]*>\s*two`); + }); +});