Add DOCX and ODT export for the Full Book view

The main process now assembles the manuscript from the chapter files and
renders it to a Word document with the docx library, using the same
markdown-it plugin set as the on-screen renderer (highlight, callouts,
wikilinks). ODT is converted from that DOCX with LibreOffice running
headless, keeping the two formats consistent; a clear error is returned
when LibreOffice is missing. The Full Book header's single Export PDF
button becomes an Export menu (PDF/DOCX/ODT) wired to the File menu.

Tests: new run-office-export-test.mjs verifies DOCX/ODT structure,
chapter order and formatting; the e2e scenario exports through the real
IPC chain and checks magic bytes, extensions and the status notices.
This commit is contained in:
avi 2026-08-18 17:06:32 -05:00
commit 6679294231
12 changed files with 1403 additions and 15 deletions

View file

@ -408,16 +408,22 @@ const BOOK3_DIR = __BOOKDIR3_VALUE__;
out.fbWithEmpty = assembledWithEmpty;
out.fbEmptyOk = assembledWithEmpty.includes("# Empty") && assembledWithEmpty.includes("Beta Renamed");
// --- PDF export of the Full Book ---
// The e2e harness sets FOLIO_TEST_PDF, so the main process skips the
// native save dialog and writes to that path, returning the byte count
// and whether the buffer starts with the PDF magic number. The Full Book
// view is on screen here, so what gets printed is exactly the assembled
// manuscript (the print stylesheet hides all app chrome).
const pdfBtn = document.getElementById("exportPdfBtn");
out.pdfBtnPresent = !!pdfBtn && !pdfBtn.disabled;
// --- Export of the Full Book (PDF / DOCX / ODT) ---
// The e2e harness sets FOLIO_TEST_PDF/DOCX/ODT, so the main process skips
// the native save dialog and writes to those paths, returning the byte
// count and whether the buffer starts with the right magic number. The
// Full Book view is on screen here, so the PDF is the assembled manuscript
// (the print stylesheet hides all app chrome).
const exportMenuToggle = document.getElementById("exportMenuToggle");
const exportMenu = document.getElementById("exportMenu");
out.exportMenuPresent = !!exportMenuToggle && !!exportMenu;
out.pdfExportPdfVisible = !document.getElementById("fullBookView").classList.contains("hidden");
pdfBtn.click();
exportMenuToggle.click();
await new Promise((r) => setTimeout(r, 50));
out.exportMenuOpened = !exportMenu.classList.contains("hidden");
const pdfItem = exportMenu.querySelector('[data-export="pdf"]');
out.pdfItemPresent = !!pdfItem;
pdfItem.click();
await new Promise((r) => setTimeout(r, 2500));
out.pdfStatusShown = document
.getElementById("fullBookStatus")
@ -485,6 +491,39 @@ const BOOK3_DIR = __BOOKDIR3_VALUE__;
pdfLongRes.bytes > pdfRes.bytes * 2 &&
pdfLongRes.pdfOk === true;
// DOCX export: main process assembles the manuscript from the chapter
// files (all four chapters, including the very long one), writes a ZIP
// (PK magic) to FOLIO_TEST_DOCX, and reports its size. The deep content
// checks live in tests/run-office-export-test.mjs; here we verify the
// end-to-end IPC path, extension, magic number, and the status notice.
const docxRes = await window.folio.exportDocx();
out.docxExport =
docxRes && "filePath" in docxRes
? { filePath: docxRes.filePath, bytes: docxRes.bytes, docxOk: docxRes.docxOk }
: docxRes;
out.docxExportOk =
!!docxRes &&
!("error" in docxRes) &&
"filePath" in docxRes &&
docxRes.filePath.endsWith(".docx") &&
typeof docxRes.bytes === "number" &&
docxRes.bytes > 1000 &&
docxRes.docxOk === true;
const odtRes = await window.folio.exportOdt();
out.odtExport =
odtRes && "filePath" in odtRes
? { filePath: odtRes.filePath, bytes: odtRes.bytes, odtOk: odtRes.odtOk }
: odtRes;
out.odtExportOk =
!!odtRes &&
!("error" in odtRes) &&
"filePath" in odtRes &&
odtRes.filePath.endsWith(".odt") &&
typeof odtRes.bytes === "number" &&
odtRes.bytes > 1000 &&
odtRes.odtOk === true;
// Formatting must survive into the printed book: markdown-it is configured
// to pass through the toolbar's HTML (<u>, <span style=...>, <div>),
// ==highlight== becomes <mark>, callouts become styled divs, and
@ -810,7 +849,9 @@ const BOOK3_DIR = __BOOKDIR3_VALUE__;
"fbAfterDeleteOk",
"fbAfterRenameOk",
"fbEmptyOk",
"pdfBtnPresent",
"exportMenuPresent",
"exportMenuOpened",
"pdfItemPresent",
"pdfExportPdfVisible",
"pdfStatusShown",
"pdfExportOk",
@ -818,6 +859,8 @@ const BOOK3_DIR = __BOOKDIR3_VALUE__;
"printBookHasAllChapters",
"printBookLongFull",
"pdfLongExportOk",
"docxExportOk",
"odtExportOk",
"printFmtOk",
"navOptionsOk",
"navOpenAtOneOk",