UNPKG

kdp-book-generator

Version:

Generate KDP-compliant PDFs and EPUBs from Markdown for Amazon book publishing

233 lines 8.63 kB
"use strict"; 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