kdp-book-generator
Version:
Generate KDP-compliant PDFs and EPUBs from Markdown for Amazon book publishing
479 lines • 19.7 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.MultiFileAssembler = void 0;
const fs_1 = require("fs");
const path_1 = require("path");
const yaml = __importStar(require("yaml"));
const markdown_parser_1 = require("../parser/markdown-parser");
const book_config_1 = require("../config/book-config");
class MultiFileAssembler {
constructor(configPath, baseDir) {
this.translations = {
Published: {
en: 'Published',
es: 'Publicado',
fr: 'Publié',
de: 'Veröffentlicht',
it: 'Pubblicato',
pt: 'Publicado',
nl: 'Gepubliceerd',
pl: 'Opublikowany',
ru: 'Опубликовано',
ja: '出版日',
zh: '出版日期',
ko: '출판일',
},
};
this.config = this.loadConfig(configPath);
this.baseDir = baseDir || (0, path_1.dirname)(configPath);
this.parser = new markdown_parser_1.MarkdownParser();
}
getTranslation(key, language) {
const lang = language || this.config.book_info.language || 'en';
const langCode = lang.toLowerCase().substring(0, 2);
return this.translations[key]?.[langCode] || this.translations[key]?.['en'] || key;
}
getConfig() {
return this.config;
}
loadConfig(configPath) {
if (!(0, fs_1.existsSync)(configPath)) {
throw new Error(`Configuration file not found: ${configPath}`);
}
const configContent = (0, fs_1.readFileSync)(configPath, 'utf-8');
try {
return yaml.parse(configContent);
}
catch (error) {
throw new Error(`Error parsing configuration YAML: ${error}`);
}
}
async assemble(options) {
console.log('📚 Starting multi-file assembly...');
console.log(`📖 Book: ${this.config.book_info.title}`);
console.log(`📁 Base directory: ${this.baseDir}`);
if (options.validateFiles) {
await this.validateFiles();
}
const assembledContent = await this.assembleContent();
const frontMatter = this.generateFrontMatter();
// Parse the assembled content
const parsedBook = this.parser.parse(assembledContent);
// Override front matter with assembly config
parsedBook.frontMatter = { ...parsedBook.frontMatter, ...frontMatter };
console.log('✅ Assembly completed successfully');
console.log(`📊 Total sections: ${this.config.assembly_order.length}`);
console.log(`📊 Total files: ${this.getTotalFileCount()}`);
console.log(`📊 Total chapters: ${parsedBook.chapters.length}`);
return parsedBook;
}
async validateFiles() {
console.log('🔍 Validating files...');
const allFiles = this.getAllFiles();
const missingFiles = [];
const invalidFiles = [];
for (const file of allFiles) {
const filePath = (0, path_1.join)(this.baseDir, file.filename);
if (!(0, fs_1.existsSync)(filePath)) {
missingFiles.push(file.filename);
continue;
}
// Validate file format if specified
if (this.config.format_validation) {
const content = (0, fs_1.readFileSync)(filePath, 'utf-8');
if (!this.validateFileFormat(content, file)) {
invalidFiles.push(file.filename);
}
}
}
if (missingFiles.length > 0) {
throw new Error(`Missing files: ${missingFiles.join(', ')}`);
}
if (invalidFiles.length > 0) {
console.warn(`⚠️ Files with format issues: ${invalidFiles.join(', ')}`);
}
console.log(`✅ Validated ${allFiles.length} files`);
}
validateFileFormat(content, _file) {
const formatValidation = this.config.format_validation;
if (!formatValidation)
return true;
// Check header format
if (formatValidation.header_format) {
const lines = content.split('\n');
const firstLine = lines[0];
const secondLine = lines[1];
// Check for dual language header format
if (!firstLine.startsWith('# **') || !firstLine.endsWith('**')) {
return false;
}
if (secondLine && !secondLine.startsWith('*') && !secondLine.endsWith('*')) {
return false;
}
}
// Check required sections
if (formatValidation.file_structure?.required_sections) {
for (const section of formatValidation.file_structure.required_sections) {
if (!content.includes(section)) {
return false;
}
}
}
return true;
}
async assembleContent() {
console.log('🔧 Assembling content...');
const parts = [];
// Add book header
parts.push(this.generateBookHeader());
// Add table of contents if enabled
if (this.config.assembly_settings.include_table_of_contents) {
parts.push(this.generateTableOfContents());
}
// Process each section
for (const section of this.config.assembly_order) {
console.log(`📝 Processing section: ${section.title}`);
// Add section header
parts.push(this.generateSectionHeader(section));
// Process files in the section
for (const file of section.files) {
if (!file.required)
continue;
console.log(` 📄 Adding file: ${file.filename}`);
const filePath = (0, path_1.join)(this.baseDir, file.filename);
if (!(0, fs_1.existsSync)(filePath)) {
console.warn(`⚠️ File not found: ${file.filename}`);
continue;
}
const content = (0, fs_1.readFileSync)(filePath, 'utf-8');
// Add page break if specified
if (file.page_break) {
parts.push(this.config.assembly_settings.formatting.page_break_marker);
}
// Process and add file content
const processedContent = this.processFileContent(content, file);
parts.push(processedContent);
}
// Add section separator
parts.push(this.config.assembly_settings.formatting.section_separator);
}
return parts.join('\n');
}
generateBookHeader() {
const bookInfo = this.config.book_info;
const parts = [];
// Main title
parts.push(`# **${bookInfo.title}**`);
if (bookInfo.subtitle) {
parts.push(`## ${bookInfo.subtitle}`);
}
if (bookInfo.description) {
parts.push(`**${bookInfo.description}**`);
}
if (bookInfo.spanish_title) {
parts.push(`*${bookInfo.spanish_title}*`);
}
// Metadata
if (this.config.assembly_settings.metadata.include_version_info && bookInfo.version) {
parts.push(`**Version:** ${bookInfo.version}`);
}
if (this.config.assembly_settings.metadata.include_generation_date) {
const publishedLabel = this.getTranslation('Published');
parts.push(`**${publishedLabel}:** ${new Date().toISOString().split('T')[0]}`);
}
parts.push('---');
return parts.join('\n\n');
}
generateTableOfContents() {
const tocConfig = this.config.table_of_contents;
if (!tocConfig?.generate_automatically)
return '';
const parts = [];
parts.push('# Table of Contents');
parts.push('');
let chapterNumber = 1;
for (const section of this.config.assembly_order) {
// Add section header
const sectionConfig = tocConfig.sections.find((s) => s.name.includes(section.title.replace(/^[📚📗📙📕🚀\s]/u, '')));
const emoji = sectionConfig?.emoji || '';
parts.push(`## ${emoji} ${section.title}`);
// Add files in section
for (const file of section.files) {
if (!file.required)
continue;
const indent = '';
let numbering = '';
if (file.type === 'chapter' && tocConfig.include_chapter_numbers) {
numbering = `${chapterNumber}. `;
chapterNumber++;
}
else if (file.type === 'appendix') {
numbering = `Appendix ${file.appendix_letter}: `;
}
const title = file.title;
parts.push(`${indent}- ${numbering}${title}`);
if (file.spanish_title) {
parts.push(`${indent} *${file.spanish_title}*`);
}
}
parts.push('');
}
parts.push('---');
return parts.join('\n');
}
generateSectionHeader(section) {
const parts = [];
parts.push(`# ${section.title}`);
if (section.spanish_title) {
parts.push(`*${section.spanish_title}*`);
}
return parts.join('\n\n');
}
processFileContent(content, file) {
let processed = content;
// Remove file-specific headers if they duplicate the assembly title
// This prevents duplicate titles when files are assembled
// NOTE: Only remove titles for non-chapter files to preserve chapter headings
if (file.type !== 'chapter' && file.type !== 'appendix') {
const lines = processed.split('\n');
if (lines[0].startsWith('# **') && lines[0].includes(file.title)) {
// Remove the title line and any following subtitle/description
let linesToRemove = 1;
if (lines[1] && lines[1].startsWith('*') && lines[1].endsWith('*')) {
linesToRemove = 2;
}
processed = lines.slice(linesToRemove).join('\n');
}
}
// Add cross-references if enabled
if (this.config.cross_references?.enable_internal_links) {
processed = this.addCrossReferences(processed);
}
return processed;
}
addCrossReferences(content) {
const crossRefs = this.config.cross_references;
if (!crossRefs)
return content;
const patterns = crossRefs.reference_patterns;
if (!patterns)
return content;
let processed = content;
// Split content into lines to handle headings separately
const lines = processed.split('\n');
const processedLines = lines.map((line) => {
// Skip processing if this is a heading line
if (line.match(/^#{1,6}\s+/)) {
return line;
}
let processedLine = line;
// Replace chapter references (only in non-heading lines)
if (crossRefs.enable_chapter_references) {
processedLine = processedLine.replace(/Chapter (\d+)/g, (match, num) => {
const chapterFile = this.findChapterFile(parseInt(num));
if (chapterFile) {
return patterns.chapter_reference
.replace('{number}', num)
.replace('{title}', chapterFile.title.replace(/^Chapter \d+: /, ''));
}
return match;
});
}
return processedLine;
});
processed = processedLines.join('\n');
// Replace appendix references (continue with the line-by-line processing)
if (crossRefs.enable_appendix_references) {
const finalLines = processed.split('\n');
const finalProcessedLines = finalLines.map((line) => {
// Skip processing if this is a heading line
if (line.match(/^#{1,6}\s+/)) {
return line;
}
return line.replace(/Appendix ([A-Z])/g, (match, letter) => {
const appendixFile = this.findAppendixFile(letter);
if (appendixFile) {
return patterns.appendix_reference
.replace('{letter}', letter)
.replace('{title}', appendixFile.title.replace(/^Appendix [A-Z]: /, ''));
}
return match;
});
});
processed = finalProcessedLines.join('\n');
}
return processed;
}
findChapterFile(chapterNumber) {
for (const section of this.config.assembly_order) {
const file = section.files.find((f) => f.chapter_number === chapterNumber);
if (file)
return file;
}
return null;
}
findAppendixFile(letter) {
for (const section of this.config.assembly_order) {
const file = section.files.find((f) => f.appendix_letter === letter);
if (file)
return file;
}
return null;
}
generateFrontMatter() {
const bookInfo = this.config.book_info;
const metadata = this.config.metadata;
return {
title: bookInfo.title,
author: bookInfo.author || metadata?.author || 'Unknown Author',
description: bookInfo.description,
language: bookInfo.language || 'en',
version: bookInfo.version,
level: bookInfo.level,
spanish_title: bookInfo.spanish_title,
total_chapters: bookInfo.total_chapters,
total_sections: bookInfo.total_sections,
total_files: bookInfo.total_files,
generation_date: new Date().toISOString().split('T')[0],
target_audience: metadata?.target_audience,
complementary_resource: metadata?.complementary_resource,
};
}
getAllFiles() {
const files = [];
for (const section of this.config.assembly_order) {
files.push(...section.files);
}
return files;
}
getTotalFileCount() {
return this.getAllFiles().length;
}
getTypographyConfig() {
const defaultTypography = book_config_1.BookConfigManager.getDefaultTypography();
const override = this.config.typography_override;
if (!override) {
console.log('📝 No typography overrides found, using defaults');
return defaultTypography;
}
// Apply overrides
const typography = { ...defaultTypography };
if (override.body_font !== undefined) {
typography.body.size = override.body_font;
}
if (override.body_line_height !== undefined) {
typography.body.lineHeight = override.body_line_height;
}
if (override.h1_font !== undefined) {
typography.heading1.size = override.h1_font;
}
if (override.h1_margin_top !== undefined) {
typography.heading1.marginTop = override.h1_margin_top;
}
if (override.h1_margin_bottom !== undefined) {
typography.heading1.marginBottom = override.h1_margin_bottom;
}
if (override.h2_font !== undefined) {
typography.heading2.size = override.h2_font;
}
if (override.h2_margin_top !== undefined) {
typography.heading2.marginTop = override.h2_margin_top;
}
if (override.h2_margin_bottom !== undefined) {
typography.heading2.marginBottom = override.h2_margin_bottom;
}
if (override.h3_font !== undefined) {
typography.heading3.size = override.h3_font;
}
if (override.h3_margin_top !== undefined) {
typography.heading3.marginTop = override.h3_margin_top;
}
if (override.h3_margin_bottom !== undefined) {
typography.heading3.marginBottom = override.h3_margin_bottom;
}
if (override.h4_font !== undefined) {
typography.heading4.size = override.h4_font;
}
if (override.h5_font !== undefined) {
typography.heading5.size = override.h5_font;
}
if (override.h6_font !== undefined) {
typography.heading6.size = override.h6_font;
}
return typography;
}
// Static utility methods
static async validateConfig(configPath) {
const errors = [];
try {
if (!(0, fs_1.existsSync)(configPath)) {
errors.push(`Configuration file not found: ${configPath}`);
return { valid: false, errors };
}
const configContent = (0, fs_1.readFileSync)(configPath, 'utf-8');
const config = yaml.parse(configContent);
// Validate required fields
if (!config.book_info) {
errors.push('Missing required field: book_info');
}
if (!config.assembly_order || config.assembly_order.length === 0) {
errors.push('Missing or empty assembly_order');
}
if (!config.assembly_settings) {
errors.push('Missing required field: assembly_settings');
}
// Validate file references
const baseDir = (0, path_1.dirname)(configPath);
for (const section of config.assembly_order || []) {
for (const file of section.files || []) {
const filePath = (0, path_1.join)(baseDir, file.filename);
if (file.required && !(0, fs_1.existsSync)(filePath)) {
errors.push(`Required file not found: ${file.filename}`);
}
}
}
}
catch (error) {
errors.push(`Error parsing configuration: ${error}`);
}
return {
valid: errors.length === 0,
errors,
};
}
}
exports.MultiFileAssembler = MultiFileAssembler;
//# sourceMappingURL=multi-file-assembler.js.map