feat(import): add universal manuscript import pipeline (.docx, .odt, .epub, .md, .txt, .html, .rtf, .json)
CI / build (push) Waiting to run
CI / build (push) Waiting to run
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
exportProjectData,
|
||||
importManuscriptFile,
|
||||
type ExportProject,
|
||||
} from "./index.js";
|
||||
|
||||
const SAMPLE_PROJECT: ExportProject = {
|
||||
meta: {
|
||||
title: "The Harbor Ledger",
|
||||
author: "Austin Breinholt",
|
||||
language: "en",
|
||||
},
|
||||
books: [
|
||||
{
|
||||
id: "book-1",
|
||||
projectId: "proj-import-1",
|
||||
title: "Book One",
|
||||
order: 0,
|
||||
chapters: [
|
||||
{
|
||||
id: "ch-1",
|
||||
bookId: "book-1",
|
||||
projectId: "proj-import-1",
|
||||
title: "Chapter 1: Fog Over the Pier",
|
||||
order: 0,
|
||||
rev: 1,
|
||||
meta: { wordCount: 15, scenes: [], updatedAt: "2026-01-01T00:00:00.000Z" },
|
||||
doc: {
|
||||
type: "doc",
|
||||
content: [
|
||||
{
|
||||
type: "heading",
|
||||
attrs: { level: 1 },
|
||||
content: [{ type: "text", text: "Chapter 1: Fog Over the Pier" }],
|
||||
},
|
||||
{
|
||||
type: "paragraph",
|
||||
content: [
|
||||
{ type: "text", text: "The lantern swung in the " },
|
||||
{ type: "text", text: "harbor wind", marks: [{ type: "bold" }] },
|
||||
{ type: "text", text: " as Mara opened the ledger." },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "ch-2",
|
||||
bookId: "book-1",
|
||||
projectId: "proj-import-1",
|
||||
title: "Chapter 2: The Bell Tower",
|
||||
order: 1,
|
||||
rev: 1,
|
||||
meta: { wordCount: 6, scenes: [], updatedAt: "2026-01-01T00:00:00.000Z" },
|
||||
doc: {
|
||||
type: "doc",
|
||||
content: [
|
||||
{
|
||||
type: "heading",
|
||||
attrs: { level: 1 },
|
||||
content: [{ type: "text", text: "Chapter 2: The Bell Tower" }],
|
||||
},
|
||||
{
|
||||
type: "paragraph",
|
||||
content: [
|
||||
{ type: "text", text: "Twelve strokes echoed across the water." },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe("Universal Manuscript Importer (importer.ts)", () => {
|
||||
it("imports plain text (.txt) and splits chapters on Chapter headings", async () => {
|
||||
const txt = [
|
||||
"Chapter 1: The Arrival",
|
||||
"",
|
||||
"The train pulled into the station at dusk.",
|
||||
"",
|
||||
"* * *",
|
||||
"",
|
||||
"She stepped onto the wet platform.",
|
||||
"",
|
||||
"Chapter 2: The Departure",
|
||||
"",
|
||||
"By morning, the tracks were empty.",
|
||||
].join("\n");
|
||||
|
||||
const res = await importManuscriptFile("arrival-story.txt", txt);
|
||||
expect(res.format).toBe("txt");
|
||||
expect(res.chapters).toHaveLength(2);
|
||||
expect(res.chapters[0]?.title).toBe("Chapter 1: The Arrival");
|
||||
expect(res.chapters[1]?.title).toBe("Chapter 2: The Departure");
|
||||
expect(res.totalWords).toBeGreaterThan(10);
|
||||
});
|
||||
|
||||
it("imports Markdown (.md) with frontmatter, inline marks, and chapters", async () => {
|
||||
const md = [
|
||||
"---",
|
||||
"title: Glass Harbor",
|
||||
"author: Schwitzz",
|
||||
"---",
|
||||
"# Chapter One",
|
||||
"",
|
||||
"He read the **bold entry** and *italic margin note*.",
|
||||
"",
|
||||
"# Chapter Two",
|
||||
"",
|
||||
"The second chapter begins here.",
|
||||
].join("\n");
|
||||
|
||||
const res = await importManuscriptFile("glass-harbor.md", md);
|
||||
expect(res.format).toBe("md");
|
||||
expect(res.title).toBe("Glass Harbor");
|
||||
expect(res.author).toBe("Schwitzz");
|
||||
expect(res.chapters).toHaveLength(2);
|
||||
expect(res.chapters[0]?.title).toBe("Chapter One");
|
||||
expect(res.chapters[1]?.title).toBe("Chapter Two");
|
||||
});
|
||||
|
||||
it("round-trips DOCX (Word / OnlyOffice) into PubScript chapters", async () => {
|
||||
const docxBytes = (await exportProjectData(SAMPLE_PROJECT, "docx", {
|
||||
generatedAt: "2026-01-01T00:00:00.000Z",
|
||||
})) as Uint8Array;
|
||||
|
||||
const imported = await importManuscriptFile("the-harbor-ledger.docx", docxBytes);
|
||||
expect(imported.format).toBe("docx");
|
||||
expect(imported.chapters.length).toBeGreaterThanOrEqual(2);
|
||||
expect(imported.totalWords).toBeGreaterThan(10);
|
||||
});
|
||||
|
||||
it("round-trips ODT (OpenOffice / OnlyOffice / LibreOffice) into PubScript chapters", async () => {
|
||||
const odtBytes = (await exportProjectData(SAMPLE_PROJECT, "odt", {
|
||||
generatedAt: "2026-01-01T00:00:00.000Z",
|
||||
})) as Uint8Array;
|
||||
|
||||
const imported = await importManuscriptFile("the-harbor-ledger.odt", odtBytes);
|
||||
expect(imported.format).toBe("odt");
|
||||
expect(imported.title).toBe("The Harbor Ledger");
|
||||
expect(imported.chapters.length).toBeGreaterThanOrEqual(2);
|
||||
expect(imported.totalWords).toBeGreaterThan(10);
|
||||
});
|
||||
|
||||
it("round-trips EPUB into PubScript chapters", async () => {
|
||||
const epubBytes = (await exportProjectData(SAMPLE_PROJECT, "epub", {
|
||||
generatedAt: "2026-01-01T00:00:00.000Z",
|
||||
})) as Uint8Array;
|
||||
|
||||
const imported = await importManuscriptFile("the-harbor-ledger.epub", epubBytes);
|
||||
expect(imported.format).toBe("epub");
|
||||
expect(imported.title).toBe("The Harbor Ledger");
|
||||
expect(imported.chapters).toHaveLength(2);
|
||||
expect(imported.totalWords).toBeGreaterThan(10);
|
||||
});
|
||||
|
||||
it("imports RTF (.rtf) and HTML (.html) manuscripts", async () => {
|
||||
const rtf = "{\\rtf1\\ansi{\\fonttbl\\f0 Times;}\\pard Chapter 1: Dawn\\par The sun rose over the ridge.\\par}";
|
||||
const rtfRes = await importManuscriptFile("dawn.rtf", rtf);
|
||||
expect(rtfRes.format).toBe("rtf");
|
||||
expect(rtfRes.totalWords).toBeGreaterThan(4);
|
||||
|
||||
const html = "<html><head><title>Sea Wall</title></head><body><h1>Chapter 1</h1><p>Waves crashed against the <strong>stone</strong>.</p></body></html>";
|
||||
const htmlRes = await importManuscriptFile("seawall.html", html);
|
||||
expect(htmlRes.format).toBe("html");
|
||||
expect(htmlRes.title).toBe("Sea Wall");
|
||||
expect(htmlRes.chapters[0]?.title).toBe("Chapter 1");
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -132,3 +132,4 @@ export { exportEpub } from "./epub.js";
|
||||
export { exportDocx } from "./docx.js";
|
||||
export { exportOdt } from "./odt.js";
|
||||
export { exportPdf } from "./pdf.js";
|
||||
export * from "./importer.js";
|
||||
|
||||
Reference in New Issue
Block a user