feat(import): add universal manuscript import pipeline (.docx, .odt, .epub, .md, .txt, .html, .rtf, .json)
CI / build (push) Waiting to run

This commit is contained in:
Schwitzz
2026-10-30 16:51:33 -05:00
parent aee9c428fc
commit 020563ed85
8 changed files with 1788 additions and 5 deletions
+173
View File
@@ -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
+1
View File
@@ -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";