kdp-book-generator
Version:
Generate KDP-compliant PDFs and EPUBs from Markdown for Amazon book publishing
233 lines • 8.63 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;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.MarkdownParser = void 0;
const markdown_it_1 = __importDefault(require("markdown-it"));
const markdown_it_anchor_1 = __importDefault(require("markdown-it-anchor"));
// eslint-disable-next-line @typescript-eslint/no-var-requires
const toc = require('markdown-it-table-of-contents');
const yaml = __importStar(require("yaml"));
/**
* Parser for converting Markdown files with YAML front matter into structured book content
*
* Features:
* - YAML front matter extraction for metadata
* - Automatic table of contents generation from H1 headings
* - Chapter extraction and organization
* - Support for page breaks using horizontal rules
* - Inline markdown processing (bold, italic, code)
*/
class MarkdownParser {
constructor() {
this.md = new markdown_it_1.default({
html: true,
linkify: true,
typographer: true,
breaks: false,
});
this.md.use(markdown_it_anchor_1.default, {
permalink: false, // Disable permalinks completely
level: [1, 2, 3, 4, 5, 6],
slugify: () => '', // Disable ID generation completely
});
this.md.use(toc, {
includeLevel: [1], // Only include H1 in TOC
containerClass: 'table-of-contents',
listType: 'ol',
});
this.addCustomRules();
}
addCustomRules() {
// Add custom rule for page breaks
this.md.renderer.rules.hr = (tokens, idx, _options, _env, _renderer) => {
const token = tokens[idx];
if (token.attrGet('class') === 'page-break') {
return '<div class="page-break"></div>\n';
}
return '<hr>\n';
};
// Custom rule for chapter headings
const originalHeadingOpen = this.md.renderer.rules.heading_open;
this.md.renderer.rules.heading_open = (tokens, idx, options, env, renderer) => {
const token = tokens[idx];
const level = parseInt(token.tag.slice(1));
if (level === 1) {
return `<${token.tag} class="chapter-heading">`;
}
return originalHeadingOpen
? originalHeadingOpen(tokens, idx, options, env, renderer)
: `<${token.tag}>`;
};
}
/**
* Parse a Markdown string into structured book content
*
* @param markdown - The raw Markdown content with optional YAML front matter
* @returns Parsed book structure including metadata, chapters, and rendered HTML
*
* @example
* ```typescript
* const parser = new MarkdownParser();
* const book = parser.parse(`---
* title: My Book
* author: Author Name
* ---
*
* # Chapter 1
*
* Content here...
* `);
* ```
*/
parse(markdown) {
const { frontMatter, content } = this.extractFrontMatter(markdown);
// Fix malformed markdown headers that start with ---#
const fixedContent = content.replace(/^---#\s+(.*)$/gm, '# $1');
const html = this.md.render(fixedContent);
const chapters = this.extractChapters(fixedContent);
const tableOfContents = this.extractTableOfContents(fixedContent);
return {
frontMatter,
tableOfContents,
chapters,
html,
};
}
extractFrontMatter(markdown) {
const frontMatterRegex = /^---\s*\n([\s\S]*?)\n---\s*\n/;
const match = markdown.match(frontMatterRegex);
if (match) {
const yamlContent = match[1];
const content = markdown.slice(match[0].length);
try {
const frontMatter = yaml.parse(yamlContent);
return { frontMatter, content };
}
catch (error) {
console.warn('Error parsing front matter:', error);
return { frontMatter: {}, content: markdown };
}
}
// Check if the file starts with # and extract title
const titleMatch = markdown.match(/^#\s+(.+)$/m);
if (titleMatch) {
const title = titleMatch[1].trim();
return {
frontMatter: { title },
content: markdown,
};
}
return { frontMatter: {}, content: markdown };
}
extractChapters(markdown) {
const chapters = [];
const lines = markdown.split('\n');
let currentChapter = null;
let contentLines = [];
for (const line of lines) {
const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
if (headingMatch) {
// Save previous chapter if exists
if (currentChapter) {
currentChapter.content = contentLines.join('\n');
chapters.push(currentChapter);
}
// Start new chapter
const level = headingMatch[1].length;
const title = headingMatch[2];
const anchor = this.generateAnchor(title);
currentChapter = {
title,
level,
anchor,
content: '',
};
contentLines = [];
}
else {
contentLines.push(line);
}
}
// Add the last chapter
if (currentChapter) {
currentChapter.content = contentLines.join('\n');
chapters.push(currentChapter);
}
return chapters;
}
extractTableOfContents(markdown) {
const toc = [];
const lines = markdown.split('\n');
for (const line of lines) {
const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
if (headingMatch) {
const level = headingMatch[1].length;
// Only include H1 in TOC
if (level !== 1)
continue;
let title = headingMatch[2];
// Process inline markdown in the title
title = this.processInlineMarkdown(title);
const anchor = this.generateAnchor(headingMatch[2]); // Use raw title for anchor
toc.push({
title,
level,
anchor,
});
}
}
return toc;
}
processInlineMarkdown(text) {
// Convert **text** to <strong>text</strong>
text = text.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
// Convert *text* to <em>text</em>
text = text.replace(/\*([^*]+)\*/g, '<em>$1</em>');
// Convert `text` to <code>text</code>
text = text.replace(/`([^`]+)`/g, '<code>$1</code>');
return text;
}
generateAnchor(title) {
return title
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
}
}
exports.MarkdownParser = MarkdownParser;
//# sourceMappingURL=markdown-parser.js.map