UNPKG

kdp-book-generator

Version:

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

873 lines (759 loc) 17.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.StyleGenerator = void 0; class StyleGenerator { generateCSS(config) { const format = config.format; const margins = config.margins; const typography = config.typography; // Check if this is Kindle format const isKindle = format.name === 'Kindle'; if (isKindle) { return ` ${this.generateKindlePageSetup(format, margins)} ${this.generateKindleTypography(typography)} ${this.generateKindleLayout()} ${this.generateTableOfContents()} `.trim(); } return ` ${this.generatePageSetup(format, margins, config.bleed)} ${this.generateTypography(typography)} ${this.generateLayout()} ${this.generatePageNumbers(config.pageNumbers)} ${this.generateTableOfContents()} ${this.generatePrintStyles()} `.trim(); } generatePageSetup(format, margins, bleed) { const width = this.convertToPixels(format.width, format.unit); const height = this.convertToPixels(format.height, format.unit); const bleedSize = bleed ? this.convertToPixels(0.125, 'in') : 0; const topMargin = this.convertToPixels(margins.top, margins.unit); const bottomMargin = this.convertToPixels(margins.bottom, margins.unit); const insideMargin = this.convertToPixels(margins.inside, margins.unit); const outsideMargin = this.convertToPixels(margins.outside, margins.unit); return ` @page { size: ${width + bleedSize * 2}px ${height + bleedSize * 2}px; margin: ${topMargin}px ${outsideMargin}px ${bottomMargin}px ${insideMargin}px; } @page :left { margin-left: ${outsideMargin}px; margin-right: ${insideMargin}px; } @page :right { margin-left: ${insideMargin}px; margin-right: ${outsideMargin}px; } @page :first { } html, body { width: ${width}px; height: ${height}px; margin: 0; padding: 0; color: #000; background: white; -webkit-print-color-adjust: exact; print-color-adjust: exact; } .page-content { width: 100%; height: 100%; box-sizing: border-box; }`; } generateTypography(typography) { return ` body { ${this.fontConfigToCss(typography.body)} font-feature-settings: "liga" 1, "clig" 1, "kern" 1; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } * { box-sizing: border-box; } /* Enable ligatures and kerning for all text */ p, li, td, th { font-feature-settings: "liga" 1, "clig" 1, "kern" 1; letter-spacing: 0.01em; } section { margin-bottom: 8pt; } h1, .chapter-heading { ${this.fontConfigToCss(typography.heading1)} text-transform: uppercase; page-break-after: avoid; page-break-inside: avoid; display: block; visibility: visible; text-align: center; } /* Style for subtitle that follows H1 */ h1 + p em, .chapter-heading + p em { display: block; text-align: center; font-style: italic; margin-bottom: 24pt; font-size: 14pt; line-height: 1.4; } h2 { ${this.fontConfigToCss(typography.heading2)} page-break-after: avoid; page-break-inside: avoid; display: block; visibility: visible; } /* Ensure h2 has enough content after it */ h2:not(:last-child) { break-after: avoid-page; } h3 { ${this.fontConfigToCss(typography.heading3)} page-break-after: avoid; page-break-inside: avoid; display: block; visibility: visible; } h4 { ${this.fontConfigToCss(typography.heading4)} page-break-after: avoid; display: block; visibility: visible; } h5 { ${this.fontConfigToCss(typography.heading5)} page-break-after: avoid; display: block; visibility: visible; } h6 { ${this.fontConfigToCss(typography.heading6)} page-break-after: avoid; display: block; visibility: visible; } p { margin-top: 0; margin-bottom: 10pt; text-align: justify; text-justify: inter-word; hyphens: auto; orphans: 3; widows: 3; text-indent: 0; } /* No indentation for any paragraphs */ p { text-indent: 0; } /* No drop caps for educational content */ blockquote { margin: 16pt 24pt; padding: 8pt 16pt; border-left: 3pt solid #e0e0e0; font-style: italic; background: #fafafa; border-radius: 2pt; } blockquote p { text-indent: 0 !important; } code { font-family: 'Courier New', monospace; font-size: 10pt; background: #f5f5f5; padding: 2pt 4pt; border-radius: 2pt; } pre { font-family: 'Courier New', monospace; font-size: 10pt; background: #f5f5f5; padding: 12pt; border-radius: 4pt; overflow: hidden; page-break-inside: avoid; }`; } generateLayout() { return ` .page-break { page-break-before: always; page-break-after: avoid; height: 0; margin: 0; padding: 0; clear: both; } .section-separator { height: 12pt; margin: 12pt 0; border: none; clear: both; } .chapter-heading { text-align: center; } .no-break { page-break-inside: avoid; } .keep-together { page-break-inside: avoid; break-inside: avoid; display: block; } /* Force page break before heading + table combos when near bottom */ .heading-before-table { /* If this appears in bottom third of page, move to next page */ page-break-before: auto; break-before: auto; } /* Keep heading + intro + table together as a unit */ .keep-together > h2, .keep-together > h3 { page-break-after: avoid; break-after: avoid; page-break-before: auto; break-before: auto; } .keep-together > table { page-break-before: avoid; break-before: avoid; } img { max-width: 100%; height: auto; display: block; margin: 12pt auto; page-break-inside: avoid; } table { border-collapse: collapse; width: 100%; max-width: 100%; margin: 12pt 0; page-break-inside: avoid; table-layout: fixed; overflow-wrap: break-word; word-wrap: break-word; } th, td { border: 1pt solid #ddd; padding: 3pt 6pt; text-align: left; line-height: 1.2; overflow-wrap: break-word; word-wrap: break-word; max-width: 0; } th { background-color: #f2f2f2; font-weight: bold; padding: 4pt 6pt; } ul, ol { margin: 8pt 0 8pt 0; padding-left: 20pt; } li { margin-bottom: 4pt; line-height: 1.4; } /* Remove bullets from list items that start with check marks or other symbols */ li { position: relative; } /* Hide default bullets for items starting with symbols like */ li:first-child:not(:empty) { /* This will be handled by the HTML processor */ } /* Style for check mark lists */ .checklist { list-style: none; padding-left: 0; } .checklist li { text-indent: 0; margin-left: 0; list-style: none; } ul ul, ol ol, ul ol, ol ul { margin-top: 4pt; margin-bottom: 4pt; } .header-anchor, .header-anchor-hidden, a.header-anchor, a.header-anchor-hidden { display: none !important; visibility: hidden !important; }`; } generatePageNumbers(pageNumbers) { if (!pageNumbers.enabled) { return ` @page { @top-center { content: none; } @bottom-center { content: none; } }`; } const position = pageNumbers.position === 'header' ? 'top' : 'bottom'; const alignment = pageNumbers.alignment || 'center'; const format = pageNumbers.format || 'decimal'; let counterStyle = 'decimal'; if (format === 'roman') counterStyle = 'lower-roman'; if (format === 'alpha') counterStyle = 'lower-alpha'; return ` @page { @${position}-${alignment} { content: counter(page, ${counterStyle}); font-size: 8pt; font-family: 'Nunito', sans-serif; color: #666666; } } @page :first { @top-center { content: none; } @bottom-center { content: none; } } @page .table-of-contents { @top-center { content: none; } @bottom-center { content: none; } }`; } generateTableOfContents() { return ` .table-of-contents { page-break-after: always; font-family: 'Nunito', sans-serif; font-size: 10pt; } .table-of-contents h1 { font-family: 'Lexend', sans-serif; font-size: 20pt; text-align: center; margin-bottom: 30pt; font-weight: normal; } .table-of-contents ol { list-style: none; padding-left: 0; margin: 0; } .table-of-contents li { margin-bottom: 4pt; line-height: 1.4; } .table-of-contents .toc-title { display: block; } .table-of-contents .toc-level-1 { font-weight: bold; margin-top: 8pt; margin-bottom: 6pt; font-size: 10pt; } .table-of-contents .toc-level-2 { font-weight: normal; margin-bottom: 3pt; } .table-of-contents .toc-level-3 { font-size: 8pt; margin-bottom: 2pt; color: #333; } .table-of-contents .toc-level-4 { font-size: 8pt; font-style: italic; margin-bottom: 2pt; color: #666; } .table-of-contents strong { font-weight: bold; } .table-of-contents em { font-style: italic; } .table-of-contents code { font-family: 'Courier New', monospace; font-size: 8pt; background: #f5f5f5; padding: 1pt 2pt; border-radius: 2pt; }`; } generatePrintStyles() { return ` @media print { * { -webkit-print-color-adjust: exact !important; print-color-adjust: exact !important; } body { font-size: inherit !important; } h1, h2, h3, h4, h5, h6 { page-break-after: avoid !important; margin-top: auto !important; } p, blockquote, pre { page-break-inside: avoid !important; } img, table, figure { page-break-inside: avoid !important; } li { page-break-inside: avoid !important; } /* Ensure consistent emoji rendering */ span.emoji { font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif; font-size: 1.1em; vertical-align: middle; } } /* Better handling of nested content */ .chapter-content > * { margin-bottom: 12pt; } .chapter-content > *:last-child { margin-bottom: 0; } /* Reduce spacing between consecutive paragraphs */ p + p { margin-top: 4pt; } /* Tighter spacing for lists after paragraphs */ p + ul, p + ol { margin-top: 6pt; } /* Fix for dialogue examples and conversations */ p > strong:first-child { display: inline-block; margin-right: 2pt; } /* Prevent large gaps in content */ .chapter-content p:empty { display: none; } /* Better spacing for example sections */ p em { font-style: italic; line-height: inherit; } /* Dialogue formatting */ p strong { font-weight: 600; } /* Prevent text overlap */ .page-content { width: 100%; min-height: 100%; } /* Emoji and icon support */ .emoji, .icon { display: inline-block; vertical-align: text-bottom; margin-right: 4pt; } /* Professional typography enhancements */ /* Smart quotes */ q:before { content: '"'; } q:after { content: '"'; } q q:before { content: '''; } q q:after { content: '''; } /* Small caps for acronyms */ .smallcaps, abbr { font-variant: small-caps; letter-spacing: 0.05em; } /* Better list spacing */ ul, ol { margin-left: 0; padding-left: 2em; } li::marker { color: #666; } /* Professional table styling */ table { border-collapse: separate; border-spacing: 0; box-shadow: 0 1pt 3pt rgba(0,0,0,0.1); } th { background-color: #f8f8f8; text-transform: uppercase; font-size: 9pt; letter-spacing: 0.05em; } /* Hanging punctuation */ p { hanging-punctuation: first allow-end last; } /* Heading widow/orphan control */ h1, h2, h3 { /* Ensure heading stays with at least 3 lines of following content */ break-after: avoid; break-inside: avoid; } /* Keep heading with following paragraph */ h1 + p, h2 + p, h3 + p, h1 + ul, h2 + ul, h3 + ul, h1 + ol, h2 + ol, h3 + ol { break-before: avoid; margin-top: 12pt; } /* Prevent orphaned headings at bottom of page */ h1:last-child, h2:last-child, h3:last-child { break-after: page; } /* Force headings to start on new page if they would appear in bottom 25% of page */ h1, h2, h3 { /* Ensure heading has enough space (at least 150pt) for content after it */ min-height: 0; } /* Tables that follow headings should stay together */ h1 + table, h2 + table, h3 + table, h1 + p + table, h2 + p + table, h3 + p + table { break-before: avoid; } /* Keep headings with their immediate content */ h1, h2, h3 { page-break-after: avoid; break-after: avoid; } /* Ensure headings near bottom of page move to next page */ @media print { h1, h2, h3 { /* If a heading would appear with less than 25% of page remaining, move to next page */ break-inside: avoid; orphans: 4; /* Require at least 4 lines of content after heading */ } } /* Special handling for headings followed by large content blocks */ h2:has(+ table), h2:has(+ * + table), h3:has(+ table), h3:has(+ * + table) { /* Force page break before if not enough space */ break-before: auto; } /* Fallback for browsers without :has() support */ .heading-before-table { break-before: auto; page-break-before: auto; }`; } fontConfigToCss(font) { const fallback = font.family === 'Lexend' || font.family === 'Nunito' ? '-apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif' : 'serif'; return ` font-family: '${font.family}', ${fallback}; font-size: ${font.size}pt !important; line-height: ${font.lineHeight}; font-weight: ${font.weight}; font-style: ${font.style}; color: ${font.color}; margin-top: ${font.marginTop}pt; margin-bottom: ${font.marginBottom}pt;`.trim(); } convertToPixels(value, unit) { switch (unit) { case 'in': return value * 96; // 96 DPI case 'mm': return value * 3.78; // 96 DPI case 'cm': return value * 37.8; // 96 DPI default: return value; } } generateKindlePageSetup(format, margins) { const width = this.convertToPixels(format.width, format.unit); const height = this.convertToPixels(format.height, format.unit); const topMargin = this.convertToPixels(margins.top, margins.unit); const bottomMargin = this.convertToPixels(margins.bottom, margins.unit); const leftMargin = this.convertToPixels(margins.inside, margins.unit); const rightMargin = this.convertToPixels(margins.outside, margins.unit); return ` /* Kindle-optimized page setup */ @page { size: ${width}px ${height}px; margin: ${topMargin}px ${rightMargin}px ${bottomMargin}px ${leftMargin}px; } html, body { width: 100%; margin: 0; padding: 0; color: #000; background: white; } .page-content { width: 100%; box-sizing: border-box; padding: 0; }`; } generateKindleTypography(typography) { return ` /* Kindle-optimized typography */ body { ${this.fontConfigToCss(typography.body)} text-rendering: optimizeLegibility; } /* Headings optimized for e-readers */ h1, .chapter-heading { ${this.fontConfigToCss(typography.heading1)} page-break-after: avoid; text-align: left; text-transform: none; } h2 { ${this.fontConfigToCss(typography.heading2)} page-break-after: avoid; } h3 { ${this.fontConfigToCss(typography.heading3)} page-break-after: avoid; } h4 { ${this.fontConfigToCss(typography.heading4)} } h5 { ${this.fontConfigToCss(typography.heading5)} } h6 { ${this.fontConfigToCss(typography.heading6)} } /* Paragraphs optimized for screen reading */ p { margin-top: 0; margin-bottom: 1em; text-align: left; /* No justification for better e-reader rendering */ text-indent: 0; orphans: 2; widows: 2; } /* Better spacing between elements */ h1 + p, h2 + p, h3 + p { margin-top: 0.5em; } /* Blockquotes for Kindle */ blockquote { margin: 1em 2em; padding: 0; border-left: 3px solid #ccc; padding-left: 1em; font-style: italic; } /* Code blocks for Kindle */ code { font-family: monospace; font-size: 0.9em; background: #f5f5f5; padding: 0.1em 0.3em; } pre { font-family: monospace; font-size: 0.9em; background: #f5f5f5; padding: 1em; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word; } /* Links for Kindle */ a { color: #0066cc; text-decoration: underline; }`; } generateKindleLayout() { return ` /* Kindle-optimized layout */ .page-break { page-break-before: always; height: 0; margin: 0; padding: 0; } .section-separator { height: 2em; margin: 2em 0; border: none; text-align: center; } .section-separator::before { content: "* * *"; color: #999; } /* Images for Kindle */ img { max-width: 100%; height: auto; display: block; margin: 1em auto; page-break-inside: avoid; } /* Tables for Kindle */ table { border-collapse: collapse; width: 100%; margin: 1em 0; font-size: 0.9em; } th, td { border: 1px solid #ddd; padding: 0.25em 0.5em; text-align: left; line-height: 1.2; } th { background-color: #f5f5f5; font-weight: bold; padding: 0.3em 0.5em; } /* Lists for Kindle */ ul, ol { margin: 1em 0; padding-left: 2em; } li { margin-bottom: 0.3em; } /* Hide elements not suitable for Kindle */ .page-number, .header, .footer { display: none; } /* Title page for Kindle */ .title-page { text-align: center; page-break-after: always; padding: 20% 0; } .title-page h1 { font-size: 2em; margin-bottom: 1em; } .title-page h2 { font-size: 1.5em; font-weight: normal; }`; } } exports.StyleGenerator = StyleGenerator; //# sourceMappingURL=style-generator.js.map