diff --git a/src/renderer/book.ts b/src/renderer/book.ts index 6587c98..34c16fc 100644 --- a/src/renderer/book.ts +++ b/src/renderer/book.ts @@ -1,4 +1,5 @@ import type { BookMeta } from "../main/project.js"; +import MarkdownIt from "markdown-it"; import { alertMessage, confirmMessage, promptInput } from "./dialog.js"; import { createMdEditor, editorText, setEditorText } from "./md-editor.js"; import { Decoration, EditorView, type DecorationSet } from "@codemirror/view"; @@ -75,6 +76,12 @@ export type FolioAPI = { const $ = (id: string) => document.getElementById(id) as HTMLElement; +// Renders the assembled manuscript into the print-only #printBook container, +// which the print stylesheet substitutes for the whole app when exporting a +// PDF. Plain HTML paginates reliably regardless of the editor's virtualized +// viewport, so every chapter always reaches the printed book. +const md = new MarkdownIt(); + interface ChapterEntry { id: string; title: string; @@ -117,6 +124,7 @@ export function initBook(api: FolioAPI) { const fullBookView = $("fullBookView"); const fullBookHost = $("fullBookHost"); const fullBookStats = $("fullBookStats"); + const printBook = $("printBook"); const copyAllBtn = $("copyAllBtn") as HTMLButtonElement; const fullBookStatus = $("fullBookStatus"); const exportPdfBtn = $("exportPdfBtn") as HTMLButtonElement; @@ -530,9 +538,9 @@ export function initBook(api: FolioAPI) { // Each chapter is preserved as its own block (title + content) and we also // record where every chapter begins in the assembled text so the view can // mark chapter boundaries for page breaks without touching the chapter files. - async function assembleFullBook(): Promise<{ text: string; chapterStarts: number[] }> { + async function assembleFullBook(): Promise<{ text: string; chapterStarts: number[]; chapters: string[] }> { const res = await api.getBookMeta(); - if (!res || typeof res !== "object" || "error" in res) return { text: "", chapterStarts: [] }; + if (!res || typeof res !== "object" || "error" in res) return { text: "", chapterStarts: [], chapters: [] }; const book = res as BookMeta; const parts: string[] = []; const chapterStarts: number[] = []; @@ -560,17 +568,20 @@ export function initBook(api: FolioAPI) { offset += joined.length + 2; } } - return { text: parts.join("\n\n"), chapterStarts }; + return { text: parts.join("\n\n"), chapterStarts, chapters: parts }; } async function renderFullBook() { - const { text, chapterStarts } = await assembleFullBook(); + const { text, chapterStarts, chapters } = await assembleFullBook(); setEditorText(fullBookEditor, text); const ranges = chapterStarts.slice(1).map((from) => chapterBreakLine.range(from)); fullBookEditor.dispatch({ effects: setChapterBreaks.of(Decoration.set(ranges, true)), }); fullBookStats.textContent = `words: ${countWords(text)} · chars: ${text.length}`; + printBook.innerHTML = chapters + .map((c) => ``) + .join(""); } async function selectFullBook() { diff --git a/src/renderer/index.css b/src/renderer/index.css index 816d74d..e591f50 100644 --- a/src/renderer/index.css +++ b/src/renderer/index.css @@ -519,79 +519,107 @@ html, body { padding-top: 1.5rem; } +/* ---- PDF / print output ---- + PDF export prints the whole window via webContents.printToPDF, so the + print stylesheet replaces the entire app with a plain-HTML manuscript + (#printBook) that the renderer keeps populated from the assembled book. + Printing plain HTML (instead of the CodeMirror editor, which only keeps a + virtualized window of lines in the DOM) guarantees the PDF always contains + the complete book in full, no matter how long it is or where the editor is + scrolled. #printBook is hidden on screen and shown only when printing. */ +#printBook { + display: none; +} + @media print { - html, body, #root { + html, body { height: auto !important; overflow: visible !important; - /* Pin the page to light-on-white so the PDF is readable even when the - app is in dark mode (Chromium PDF export inherits the live theme). */ color: #000 !important; background: #fff !important; } - .sidebar, .full-book-nav, .chapter-header, .editor-placeholder { + /* Nothing from the app is printed — sidebar, toolbar, headers, buttons, + modals and the live editor are all replaced by the manuscript. */ + #root, .modal-backdrop { display: none !important; } - .editor, .full-book-view, .editor-panes { - height: auto !important; - min-height: 0 !important; - overflow: visible !important; - } - - #fullBookHost .cm-editor, - #fullBookHost .cm-scroller, - #fullBookHost .cm-content { - height: auto !important; - min-height: 0 !important; - max-height: none !important; - overflow: visible !important; - background: #fff !important; - color: #000 !important; - } - - #fullBookHost .cm-scroller { + #printBook { display: block !important; + font-family: var(--font-serif); + font-size: 12pt; + line-height: 1.6; + color: #000; + background: #fff; } - #fullBookHost .cm-content { - white-space: pre-wrap !important; - } - - /* Heading and inline styling stays dark enough to read on white. */ - #fullBookHost .cm-h1, - #fullBookHost .cm-h2, - #fullBookHost .cm-h3, - #fullBookHost .cm-h4, - #fullBookHost .cm-h5, - #fullBookHost .cm-h6, - #fullBookHost .cm-strong, - #fullBookHost .cm-em, - #fullBookHost .cm-code { - color: #000 !important; - } - - #fullBookHost .cm-link { color: #1a0dab !important; } - #fullBookHost .cm-url, - #fullBookHost .cm-quote, - #fullBookHost .cm-hr, - #fullBookHost .cm-comment { - color: #444 !important; - } - - #fullBookHost .cm-code { - background: #f2f2f2 !important; - } - - /* Every chapter after the first starts on a new printed page. The first - chapter keeps its natural position (no blank page before it). */ - #fullBookHost .cm-line.chapter-break { + /* Every chapter after the first starts on a new page. The first chapter + keeps its natural position (no blank page before it). */ + #printBook .print-chapter { break-before: page; page-break-before: always; - border-top: none; - margin-top: 0; - padding-top: 0; } + #printBook .print-chapter:first-child { + break-before: auto; + page-break-before: auto; + } + + #printBook h1, + #printBook h2, + #printBook h3, + #printBook h4, + #printBook h5, + #printBook h6 { + margin: 0 0 0.7em; + line-height: 1.3; + color: #000; + } + #printBook h1 { font-size: 1.7em; } + #printBook h2 { font-size: 1.45em; } + #printBook h3 { font-size: 1.25em; } + #printBook h4 { font-size: 1.1em; } + + #printBook p, + #printBook ul, + #printBook ol, + #printBook blockquote, + #printBook pre, + #printBook table { + margin: 0 0 0.8em; + } + #printBook ul, #printBook ol { padding-left: 1.5em; } + #printBook blockquote { + padding-left: 1em; + border-left: 3px solid #999; + color: #333; + } + #printBook pre { + background: #f2f2f2; + padding: 8px; + white-space: pre-wrap; + font-size: 0.9em; + } + #printBook code { + font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace; + background: #f2f2f2; + padding: 1px 4px; + border-radius: 3px; + font-size: 0.9em; + } + #printBook pre code { padding: 0; background: none; } + #printBook a { color: #1a0dab; } + #printBook hr { + border: none; + border-top: 1px solid #000; + margin: 1em 0; + } + #printBook table { border-collapse: collapse; width: 100%; } + #printBook th, #printBook td { + border: 1px solid #666; + padding: 4px 8px; + } + #printBook img { max-width: 100%; } } /* ---- editor toolbar ---- */ diff --git a/src/renderer/index.html b/src/renderer/index.html index e4eafc0..b19a630 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -185,6 +185,7 @@ +