Add automatic chapter page breaks to Full Book
This commit is contained in:
parent
06fa671f84
commit
ebbb424ee5
4 changed files with 144 additions and 7 deletions
|
|
@ -1,7 +1,8 @@
|
|||
import type { BookMeta } from "../main/project.js";
|
||||
import { alertMessage, confirmMessage, promptInput } from "./dialog.js";
|
||||
import { createMdEditor, editorText, setEditorText } from "./md-editor.js";
|
||||
import type { EditorView } from "@codemirror/view";
|
||||
import { Decoration, EditorView, type DecorationSet } from "@codemirror/view";
|
||||
import { StateEffect, StateField } from "@codemirror/state";
|
||||
import { redo, selectAll, undo, indentLess, indentMore } from "@codemirror/commands";
|
||||
import {
|
||||
applyFormatPainter,
|
||||
|
|
@ -117,9 +118,27 @@ export function initBook(api: FolioAPI) {
|
|||
const copyAllBtn = $("copyAllBtn") as HTMLButtonElement;
|
||||
const fullBookStatus = $("fullBookStatus");
|
||||
|
||||
// Marks the line where every chapter after the first begins. In print this
|
||||
// line carries the page-break; on screen it shows a subtle separator. The
|
||||
// marker lives only in the rendered DOM, never in the assembled Markdown.
|
||||
const chapterBreakLine = Decoration.line({ class: "chapter-break" });
|
||||
const setChapterBreaks = StateEffect.define<DecorationSet>();
|
||||
const chapterBreaksField = StateField.define<DecorationSet>({
|
||||
create: () => Decoration.none,
|
||||
update(decorations, tr) {
|
||||
decorations = decorations.map(tr.changes);
|
||||
for (const effect of tr.effects) {
|
||||
if (effect.is(setChapterBreaks)) decorations = effect.value;
|
||||
}
|
||||
return decorations;
|
||||
},
|
||||
provide: (field) => EditorView.decorations.from(field),
|
||||
});
|
||||
|
||||
const fullBookEditor: EditorView = createMdEditor(fullBookHost, {
|
||||
onChange: () => undefined,
|
||||
readOnly: true,
|
||||
extensions: [chapterBreaksField],
|
||||
});
|
||||
|
||||
const editorView: EditorView = createMdEditor(editorHost, {
|
||||
|
|
@ -504,11 +523,17 @@ export function initBook(api: FolioAPI) {
|
|||
// Assemble the whole book from the canonical book metadata (the same source
|
||||
// the chapter sidebar uses) so Full Book always reflects the current chapter
|
||||
// set and order, regardless of any in-memory renderer state.
|
||||
async function assembleFullBook(): Promise<string> {
|
||||
//
|
||||
// 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[] }> {
|
||||
const res = await api.getBookMeta();
|
||||
if (!res || typeof res !== "object" || "error" in res) return "";
|
||||
if (!res || typeof res !== "object" || "error" in res) return { text: "", chapterStarts: [] };
|
||||
const book = res as BookMeta;
|
||||
const parts: string[] = [];
|
||||
const chapterStarts: number[] = [];
|
||||
let offset = 0;
|
||||
for (const id of book.chapterOrder) {
|
||||
const ch = book.chapters[id];
|
||||
if (!ch) continue;
|
||||
|
|
@ -525,14 +550,23 @@ export function initBook(api: FolioAPI) {
|
|||
} else if (body) {
|
||||
block.push(body);
|
||||
}
|
||||
if (block.length) parts.push(block.join("\n"));
|
||||
if (block.length) {
|
||||
chapterStarts.push(offset);
|
||||
const joined = block.join("\n");
|
||||
parts.push(joined);
|
||||
offset += joined.length + 2;
|
||||
}
|
||||
}
|
||||
return parts.join("\n\n");
|
||||
return { text: parts.join("\n\n"), chapterStarts };
|
||||
}
|
||||
|
||||
async function renderFullBook() {
|
||||
const text = await assembleFullBook();
|
||||
const { text, chapterStarts } = 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}`;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -508,6 +508,61 @@ html, body {
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ---- full book chapter breaks ----
|
||||
The Full Book stays one continuous scroll on screen; every chapter after
|
||||
the first gets a subtle separator where a printed page will begin. The
|
||||
separator is a pure CSS border on the chapter's first line, so it never
|
||||
becomes part of the copied chapter text. */
|
||||
#fullBookHost .cm-line.chapter-break {
|
||||
border-top: 1px solid var(--color-border);
|
||||
margin-top: 2.5rem;
|
||||
padding-top: 1.5rem;
|
||||
}
|
||||
|
||||
@media print {
|
||||
html, body, #root {
|
||||
height: auto !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.sidebar, .full-book-nav, .chapter-header, .editor-placeholder {
|
||||
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;
|
||||
}
|
||||
|
||||
#fullBookHost .cm-scroller {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
#fullBookHost .cm-content {
|
||||
white-space: pre-wrap !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 {
|
||||
break-before: page;
|
||||
page-break-before: always;
|
||||
border-top: none;
|
||||
margin-top: 0;
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- editor toolbar ---- */
|
||||
.editor-toolbar {
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { EditorState } from "@codemirror/state";
|
||||
import { EditorState, type Extension } from "@codemirror/state";
|
||||
import {
|
||||
EditorView,
|
||||
drawSelection,
|
||||
|
|
@ -23,6 +23,7 @@ export interface MdEditorCallbacks {
|
|||
onChange: (text: string) => void;
|
||||
onSelection?: (view: EditorView) => void;
|
||||
readOnly?: boolean;
|
||||
extensions?: Extension[];
|
||||
}
|
||||
|
||||
const formatBindings: KeyBinding[] = [
|
||||
|
|
@ -119,6 +120,7 @@ export function createMdEditor(host: HTMLElement, callbacks: MdEditorCallbacks):
|
|||
search({ top: true }),
|
||||
syntaxHighlighting(mdHighlight),
|
||||
folioTheme,
|
||||
...(callbacks.extensions ?? []),
|
||||
...(readOnly ? [EditorState.readOnly.of(true)] : []),
|
||||
keymap.of(
|
||||
readOnly
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue