kdp-book-generator
Version:
Generate KDP-compliant PDFs and EPUBs from Markdown for Amazon book publishing
873 lines (759 loc) • 17.3 kB
JavaScript
"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