refactor(renderer): Step 7 plain state object; fix(export): ZIP emits Markdown for native chapters

This commit is contained in:
avi 2026-09-19 18:21:48 -05:00
commit f1ee73f2a1
4 changed files with 18 additions and 70 deletions

View file

@ -1,5 +1,4 @@
import { ZipArchive } from "archiver";
import fs from "fs";
import path from "path";
import { loadBook } from "./project.js";
import { listChaptersInOrder, loadChapterDoc } from "./chapters.js";
@ -52,13 +51,17 @@ export async function buildZip(bookPath: string): Promise<Buffer> {
archive.append(combineMarkdown(bookPath), { name: "book.md" });
for (const ch of chapters) {
// Per-chapter entries are readable Markdown generated from the native
// document model (same source as book.md). Raw storage files are never
// leaked: a native chapter's .json would otherwise land in the bundle
// as opaque JSON next to readable .md files.
let content = "";
try {
content = fs.readFileSync(path.join(bookPath, ch.file), "utf-8");
content = nativeToMarkdown(loadChapterDoc(bookPath, ch.id));
} catch {
content = "";
}
const entryName = path.posix.join("chapters", path.basename(ch.file));
const entryName = path.posix.join("chapters", `${ch.id}.md`);
archive.append(content, { name: entryName });
}
@ -71,7 +74,7 @@ export async function buildZip(bookPath: string): Promise<Buffer> {
chapters: chapters.map((c) => ({
id: c.id,
title: c.title,
file: path.basename(c.file),
file: `${c.id}.md`,
})),
};
archive.append(JSON.stringify(manifest, null, 2), { name: "manifest.json" });

View file

@ -1,10 +1,6 @@
// The chapter editing surface: main Tiptap editor instance, dirty-tracking +
// autosave engine, chapter selection (with stale-load guard), zoom, and the
// writing-statistics modal.
//
// Storage note: state.dirty / state.loadedDocJson are the shared contract
// fields; until Step 7 they are backed by the orchestrator's closure locals
// through the accessor bridge, so writes here are visible to every module.
import type { Editor } from "@tiptap/core";
import type { BookCtx } from "./types.js";

View file

@ -96,8 +96,7 @@ export function createExportsFeature(ctx: BookCtx): ExportsFeature {
// Explicitly migrate the whole book to the native document format. The main
// process performs a recovery backup and leaves the original .md files in
// place; after this, chapters are stored as native JSON but the (still
// Markdown-based) editor and exports keep working via the format bridge.
// place; after this, chapters are stored as native JSON.
async function migrateBookNative(): Promise<void> {
if (!state.currentBook) return;
const confirmed = await confirmMessage(

View file

@ -1,6 +1,5 @@
import type { BookMeta, ChapterComment } from "../../main/project.js";
import { alertMessage, confirmMessage, promptInput } from "../dialog.js";
import type { ChapterEntry, FolioAPI, Recent } from "./types.js";
import type { FolioAPI } from "./types.js";
import { alertMessage, confirmMessage } from "../dialog.js";
import type { BookCtx, BookHooks, SharedState } from "./types.js";
import { createEditorFeature } from "./editor.js";
import { createToolbarFeature } from "./toolbar.js";
@ -14,41 +13,21 @@ import { createSettingsFeature } from "./settings.js";
import {
$,
basename,
closeEl,
hideUndoToast,
openEl,
selectedText,
showUndoToast,
showToast,
} from "./ui.js";
export type { ChapterSnapshot, FolioAPI } from "./types.js";
export function initBook(api: FolioAPI) {
let currentBook: { path: string; title: string } | null = null;
let meta: BookMeta | null = null;
let ordered: ChapterEntry[] = [];
let selectedId: string | null = null;
let dirty = false;
let loadedDocJson = "";
// ---- P7 decomposition bridge -------------------------------------------
// Temporary: exposes these closure locals through the SharedState contract so
// extracted feature modules share one source of truth. Removed in Step 7 when
// the last feature leaves and state becomes a plain object literal.
// Shared mutable session state. Plain object literal: every feature module
// receives the same reference via ctx, so mutations stay visible everywhere.
const state: SharedState = {
get currentBook() { return currentBook; },
set currentBook(v) { currentBook = v; },
get meta() { return meta; },
set meta(v) { meta = v; },
get ordered() { return ordered; },
set ordered(v) { ordered = v; },
get selectedId() { return selectedId; },
set selectedId(v) { selectedId = v; },
get dirty() { return dirty; },
set dirty(v) { dirty = v; },
get loadedDocJson() { return loadedDocJson; },
set loadedDocJson(v) { loadedDocJson = v; },
currentBook: null,
meta: null,
ordered: [],
selectedId: null,
dirty: false,
loadedDocJson: "",
};
const hooks = {} as BookHooks;
const ctx: BookCtx = { api, state, hooks };
@ -110,35 +89,6 @@ export function initBook(api: FolioAPI) {
// Wire comment facade from the extracted module.
hooks.updateCommentsBadge = comments.updateCommentsBadge;
// ---- element consts (kept from the original file) ----
const editorPlaceholder = $("editorPlaceholder");
const chapterView = $("chapterView");
const chapterTitleEl = $("chapterTitle");
const saveStateEl = $("saveState");
const exportBtn = $("exportBtn") as HTMLButtonElement;
const exportZipBtn = $("exportZipBtn") as HTMLButtonElement;
const projectBar = $("projectBar");
const fullBookNav = $("fullBookNav");
const fullBookBtn = $("fullBookBtn") as HTMLButtonElement;
const exportMenuToggle = $("exportMenuToggle") as HTMLButtonElement;
const exportMenu = $("exportMenu");
const exportDropdown = $("exportDropdown");
const welcomeEl = $("welcome");
const recentList = $("recentList") as HTMLUListElement;
// ---- welcome / recents logic (moved from inline into createWelcomeFeature) ----
// The welcome module owns its own DOM chrome and wiring; here we only keep the
// top-level constructor call and the bridge assignments it already set above.
// ---- export workflow (whole book vs selected chapters) ----
// The exports module owns its own DOM chrome and wiring; here we only keep the
// top-level constructor call and the bridge assignments it already set above.
// ---- comments / annotations ----
// The comments module owns its own DOM chrome and wiring; here we only keep the
// top-level constructor call and the bridge assignments it already set above.
// ---- update flow ----
// "Check for Updates…" (File menu / welcome button): query the release
// server, then offer a one-click install. The app downloads the tarball,