|
|
@@ -7,7 +7,7 @@ import { findProjectRoot, resolveBookId, log, logError } from "../utils.js";
|
|
|
export const exportCommand = new Command("export")
|
|
|
.description("Export book chapters to a single file")
|
|
|
.argument("[book-id]", "Book ID (auto-detected if only one book)")
|
|
|
- .option("--format <format>", "Output format (txt, md)", "txt")
|
|
|
+ .option("--format <format>", "Output format (txt, md, epub)", "txt")
|
|
|
.option("--output <path>", "Output file path")
|
|
|
.option("--approved-only", "Only export approved chapters")
|
|
|
.option("--json", "Output JSON metadata")
|
|
|
@@ -30,6 +30,11 @@ export const exportCommand = new Command("export")
|
|
|
throw new Error("No chapters to export.");
|
|
|
}
|
|
|
|
|
|
+ if (opts.format === "epub") {
|
|
|
+ await exportEpub(book, chapters, chaptersDir, bookId, root, opts);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
const parts: string[] = [];
|
|
|
|
|
|
if (opts.format === "md") {
|
|
|
@@ -77,3 +82,55 @@ export const exportCommand = new Command("export")
|
|
|
process.exit(1);
|
|
|
}
|
|
|
});
|
|
|
+
|
|
|
+async function exportEpub(
|
|
|
+ book: { readonly title: string },
|
|
|
+ chapters: ReadonlyArray<{ readonly number: number; readonly wordCount: number }>,
|
|
|
+ chaptersDir: string,
|
|
|
+ bookId: string,
|
|
|
+ root: string,
|
|
|
+ opts: { readonly output?: string; readonly json?: boolean },
|
|
|
+): Promise<void> {
|
|
|
+ const { marked } = await import("marked");
|
|
|
+ const { EPub } = await import("epub-gen-memory");
|
|
|
+
|
|
|
+ const epubChapters: Array<{ title: string; content: string }> = [];
|
|
|
+
|
|
|
+ for (const ch of chapters) {
|
|
|
+ const paddedNum = String(ch.number).padStart(4, "0");
|
|
|
+ const files = await readdir(chaptersDir);
|
|
|
+ const match = files.find((f) => f.startsWith(paddedNum));
|
|
|
+ if (!match) continue;
|
|
|
+
|
|
|
+ const markdown = await readFile(join(chaptersDir, match), "utf-8");
|
|
|
+ const html = await marked.parse(markdown);
|
|
|
+ // Extract title from first heading or fall back to filename
|
|
|
+ const titleMatch = markdown.match(/^#\s+(.+)/m);
|
|
|
+ const title = titleMatch?.[1] ?? match.replace(/\.md$/, "");
|
|
|
+
|
|
|
+ epubChapters.push({ title, content: html });
|
|
|
+ }
|
|
|
+
|
|
|
+ const epubInstance = new EPub(
|
|
|
+ { title: book.title, lang: "zh-CN" },
|
|
|
+ epubChapters,
|
|
|
+ );
|
|
|
+ const epubBuffer: Buffer = await epubInstance.genEpub();
|
|
|
+
|
|
|
+ const totalWords = chapters.reduce((sum, ch) => sum + ch.wordCount, 0);
|
|
|
+ const outputPath = opts.output ?? join(root, `${bookId}.epub`);
|
|
|
+ await writeFile(outputPath, epubBuffer);
|
|
|
+
|
|
|
+ if (opts.json) {
|
|
|
+ log(JSON.stringify({
|
|
|
+ bookId,
|
|
|
+ chaptersExported: chapters.length,
|
|
|
+ totalWords,
|
|
|
+ format: "epub",
|
|
|
+ outputPath,
|
|
|
+ }, null, 2));
|
|
|
+ } else {
|
|
|
+ log(`Exported ${chapters.length} chapters (${totalWords} words) to EPUB`);
|
|
|
+ log(`Output: ${outputPath}`);
|
|
|
+ }
|
|
|
+}
|