refactor(renderer): Step 7 plain state object; fix(export): ZIP emits Markdown for native chapters
This commit is contained in:
parent
b80aa798b2
commit
f1ee73f2a1
4 changed files with 18 additions and 70 deletions
|
|
@ -1,5 +1,4 @@
|
||||||
import { ZipArchive } from "archiver";
|
import { ZipArchive } from "archiver";
|
||||||
import fs from "fs";
|
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { loadBook } from "./project.js";
|
import { loadBook } from "./project.js";
|
||||||
import { listChaptersInOrder, loadChapterDoc } from "./chapters.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" });
|
archive.append(combineMarkdown(bookPath), { name: "book.md" });
|
||||||
|
|
||||||
for (const ch of chapters) {
|
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 = "";
|
let content = "";
|
||||||
try {
|
try {
|
||||||
content = fs.readFileSync(path.join(bookPath, ch.file), "utf-8");
|
content = nativeToMarkdown(loadChapterDoc(bookPath, ch.id));
|
||||||
} catch {
|
} catch {
|
||||||
content = "";
|
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 });
|
archive.append(content, { name: entryName });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -71,7 +74,7 @@ export async function buildZip(bookPath: string): Promise<Buffer> {
|
||||||
chapters: chapters.map((c) => ({
|
chapters: chapters.map((c) => ({
|
||||||
id: c.id,
|
id: c.id,
|
||||||
title: c.title,
|
title: c.title,
|
||||||
file: path.basename(c.file),
|
file: `${c.id}.md`,
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
archive.append(JSON.stringify(manifest, null, 2), { name: "manifest.json" });
|
archive.append(JSON.stringify(manifest, null, 2), { name: "manifest.json" });
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,6 @@
|
||||||
// The chapter editing surface: main Tiptap editor instance, dirty-tracking +
|
// The chapter editing surface: main Tiptap editor instance, dirty-tracking +
|
||||||
// autosave engine, chapter selection (with stale-load guard), zoom, and the
|
// autosave engine, chapter selection (with stale-load guard), zoom, and the
|
||||||
// writing-statistics modal.
|
// 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 { Editor } from "@tiptap/core";
|
||||||
import type { BookCtx } from "./types.js";
|
import type { BookCtx } from "./types.js";
|
||||||
|
|
|
||||||
|
|
@ -96,8 +96,7 @@ export function createExportsFeature(ctx: BookCtx): ExportsFeature {
|
||||||
|
|
||||||
// Explicitly migrate the whole book to the native document format. The main
|
// Explicitly migrate the whole book to the native document format. The main
|
||||||
// process performs a recovery backup and leaves the original .md files in
|
// process performs a recovery backup and leaves the original .md files in
|
||||||
// place; after this, chapters are stored as native JSON but the (still
|
// place; after this, chapters are stored as native JSON.
|
||||||
// Markdown-based) editor and exports keep working via the format bridge.
|
|
||||||
async function migrateBookNative(): Promise<void> {
|
async function migrateBookNative(): Promise<void> {
|
||||||
if (!state.currentBook) return;
|
if (!state.currentBook) return;
|
||||||
const confirmed = await confirmMessage(
|
const confirmed = await confirmMessage(
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import type { BookMeta, ChapterComment } from "../../main/project.js";
|
import type { FolioAPI } from "./types.js";
|
||||||
import { alertMessage, confirmMessage, promptInput } from "../dialog.js";
|
import { alertMessage, confirmMessage } from "../dialog.js";
|
||||||
import type { ChapterEntry, FolioAPI, Recent } from "./types.js";
|
|
||||||
import type { BookCtx, BookHooks, SharedState } from "./types.js";
|
import type { BookCtx, BookHooks, SharedState } from "./types.js";
|
||||||
import { createEditorFeature } from "./editor.js";
|
import { createEditorFeature } from "./editor.js";
|
||||||
import { createToolbarFeature } from "./toolbar.js";
|
import { createToolbarFeature } from "./toolbar.js";
|
||||||
|
|
@ -14,41 +13,21 @@ import { createSettingsFeature } from "./settings.js";
|
||||||
import {
|
import {
|
||||||
$,
|
$,
|
||||||
basename,
|
basename,
|
||||||
closeEl,
|
|
||||||
hideUndoToast,
|
|
||||||
openEl,
|
|
||||||
selectedText,
|
|
||||||
showUndoToast,
|
|
||||||
showToast,
|
showToast,
|
||||||
} from "./ui.js";
|
} from "./ui.js";
|
||||||
|
|
||||||
export type { ChapterSnapshot, FolioAPI } from "./types.js";
|
export type { ChapterSnapshot, FolioAPI } from "./types.js";
|
||||||
|
|
||||||
export function initBook(api: FolioAPI) {
|
export function initBook(api: FolioAPI) {
|
||||||
let currentBook: { path: string; title: string } | null = null;
|
// Shared mutable session state. Plain object literal: every feature module
|
||||||
let meta: BookMeta | null = null;
|
// receives the same reference via ctx, so mutations stay visible everywhere.
|
||||||
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.
|
|
||||||
const state: SharedState = {
|
const state: SharedState = {
|
||||||
get currentBook() { return currentBook; },
|
currentBook: null,
|
||||||
set currentBook(v) { currentBook = v; },
|
meta: null,
|
||||||
get meta() { return meta; },
|
ordered: [],
|
||||||
set meta(v) { meta = v; },
|
selectedId: null,
|
||||||
get ordered() { return ordered; },
|
dirty: false,
|
||||||
set ordered(v) { ordered = v; },
|
loadedDocJson: "",
|
||||||
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; },
|
|
||||||
};
|
};
|
||||||
const hooks = {} as BookHooks;
|
const hooks = {} as BookHooks;
|
||||||
const ctx: BookCtx = { api, state, hooks };
|
const ctx: BookCtx = { api, state, hooks };
|
||||||
|
|
@ -110,35 +89,6 @@ export function initBook(api: FolioAPI) {
|
||||||
// Wire comment facade from the extracted module.
|
// Wire comment facade from the extracted module.
|
||||||
hooks.updateCommentsBadge = comments.updateCommentsBadge;
|
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 ----
|
// ---- update flow ----
|
||||||
// "Check for Updates…" (File menu / welcome button): query the release
|
// "Check for Updates…" (File menu / welcome button): query the release
|
||||||
// server, then offer a one-click install. The app downloads the tarball,
|
// server, then offer a one-click install. The app downloads the tarball,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue