book-kit 0.0.0-stage → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE.md +72 -0
  3. package/README.md +251 -2
  4. package/bin/book-kit.js +115 -0
  5. package/commands/_emoji_io.py +32 -0
  6. package/commands/audit-chapter-lengths.js +244 -0
  7. package/commands/check-epub-size.js +68 -0
  8. package/commands/check-pdf-page-count.js +162 -0
  9. package/commands/composite-kindle-emoji.py +85 -0
  10. package/commands/composite-print-emoji.py +98 -0
  11. package/commands/conventions.js +76 -0
  12. package/commands/convert-pdfx.sh +194 -0
  13. package/commands/download-icc-profile.sh +105 -0
  14. package/commands/fonts.py +212 -0
  15. package/commands/generate-reference-docx.py +344 -0
  16. package/commands/kindle-fonts.js +173 -0
  17. package/commands/links-to-footnotes.js +366 -0
  18. package/commands/markdownlint.js +25 -0
  19. package/commands/minify-css.js +43 -0
  20. package/commands/optimize-images.js +73 -0
  21. package/commands/parity.sh +150 -0
  22. package/commands/postprocess-epub.js +541 -0
  23. package/commands/prep-print-src.js +77 -0
  24. package/commands/prose.js +38 -0
  25. package/commands/release-files.js +89 -0
  26. package/commands/reorder-print-frontmatter.js +79 -0
  27. package/commands/strip-page-rules.js +1168 -0
  28. package/commands/strip-property-rules.js +93 -0
  29. package/commands/update-key-takeaways.js +314 -0
  30. package/commands/validate.js +283 -0
  31. package/configs/markdownlint-cli2.jsonc +98 -0
  32. package/configs/textlintrc.json +22 -0
  33. package/css/theme/01-tokens.css +83 -0
  34. package/css/theme/02-fonts.css +79 -0
  35. package/css/theme/03-prose.css +53 -0
  36. package/css/theme/04-base.css +226 -0
  37. package/css/theme/05-components.css +546 -0
  38. package/css/theme/06-utilities.css +40 -0
  39. package/css/theme/07-ereader.css +245 -0
  40. package/css/theme/08-epub.css +865 -0
  41. package/css/theme/09-back-matter.css +47 -0
  42. package/css/theme/10-dark.css +302 -0
  43. package/css/theme/11-print.css +1229 -0
  44. package/css/theme.css +38 -0
  45. package/defaults/book.config.yml +318 -0
  46. package/defaults/pandoc-base.yml +19 -0
  47. package/defaults/pandoc-docx.yml +6 -0
  48. package/defaults/pandoc-epub-fonts.yml +6 -0
  49. package/defaults/pandoc-epub.yml +15 -0
  50. package/defaults/pandoc-kindle.yml +6 -0
  51. package/emoji/1f382.png +0 -0
  52. package/emoji/1f389.png +0 -0
  53. package/emoji/1f440.png +0 -0
  54. package/emoji/1f44d.png +0 -0
  55. package/emoji/1f44f.png +0 -0
  56. package/emoji/1f454.png +0 -0
  57. package/emoji/1f4a1.png +0 -0
  58. package/emoji/1f4bb.png +0 -0
  59. package/emoji/1f602.png +0 -0
  60. package/emoji/1f603.png +0 -0
  61. package/emoji/1f604.png +0 -0
  62. package/emoji/1f60a.png +0 -0
  63. package/emoji/1f60f.png +0 -0
  64. package/emoji/1f64c.png +0 -0
  65. package/emoji/1f64f.png +0 -0
  66. package/emoji/1f914.png +0 -0
  67. package/emoji/1f923.png +0 -0
  68. package/emoji/2714.png +0 -0
  69. package/emoji/274c.png +0 -0
  70. package/filters/add-div-titles.lua +196 -0
  71. package/filters/arc-notice.lua +36 -0
  72. package/filters/body-emoji-images.lua +48 -0
  73. package/filters/callout-classes.lua +40 -0
  74. package/filters/lib/book_kit.lua +110 -0
  75. package/filters/revision.lua +17 -0
  76. package/filters/strip-comments.lua +40 -0
  77. package/filters/strip-emoji-kindle.lua +134 -0
  78. package/fonts/LICENSE-source-code-pro.md +93 -0
  79. package/fonts/LICENSE-source-sans.md +93 -0
  80. package/fonts/LICENSE-source-serif.md +93 -0
  81. package/fonts/recipe.json +41 -0
  82. package/just/book-kit.just +229 -0
  83. package/lib/acronyms.js +47 -0
  84. package/lib/callouts.js +17 -0
  85. package/lib/config.js +211 -0
  86. package/lib/is-main.js +21 -0
  87. package/lib/lint.js +148 -0
  88. package/lib/pandoc.js +148 -0
  89. package/lib/paragraphs.js +173 -0
  90. package/lib/revision.js +35 -0
  91. package/lib/structure.js +39 -0
  92. package/lib/textlint-github-formatter.js +58 -0
  93. package/lib/validate.js +202 -0
  94. package/package.json +98 -4
  95. package/presets/business-nonfiction/CONVENTIONS.md +35 -0
  96. package/presets/business-nonfiction/preset.yml +128 -0
  97. package/profiles/pdfx-def.ps +42 -0
  98. package/requirements.txt +14 -0
  99. package/rules/no-ai-like-patterns.js +209 -0
  100. package/rules/no-assumptive.js +76 -0
  101. package/rules/no-bare-acronyms.js +129 -0
  102. package/rules/no-cliches.js +544 -0
  103. package/rules/no-cultural-idioms.js +422 -0
  104. package/rules/no-vague-intensifiers.js +94 -0
  105. package/templates/template-epub.html +79 -0
  106. package/templates/template.html +73 -0
  107. package/toolchain.json +42 -0
  108. package/validators/acronym-expansion.js +94 -0
  109. package/validators/bold-keywords.js +56 -0
  110. package/validators/callout-balance.js +74 -0
  111. package/validators/callout-divs.js +66 -0
  112. package/validators/callout-labels.js +102 -0
  113. package/validators/chapter-lengths.js +14 -0
  114. package/validators/chapter.js +57 -0
  115. package/validators/cross-references.js +45 -0
  116. package/validators/doubled-words.js +76 -0
  117. package/validators/duplicate-headings.js +52 -0
  118. package/validators/em-dashes.js +73 -0
  119. package/validators/escaped-smallcaps.js +46 -0
  120. package/validators/footnotes.js +78 -0
  121. package/validators/heading-hierarchy.js +58 -0
  122. package/validators/hypothetical-hooks.js +103 -0
  123. package/validators/index-integrity.js +28 -0
  124. package/validators/index.js +73 -0
  125. package/validators/list-punctuation.js +123 -0
  126. package/validators/min-opener-length.js +87 -0
  127. package/validators/no-bare-url-link-text.js +38 -0
  128. package/validators/no-todo-markers.js +49 -0
  129. package/validators/no-transition-todos.js +34 -0
  130. package/validators/number-range-dashes.js +72 -0
  131. package/validators/paragraph-length.js +62 -0
  132. package/validators/quote-punctuation.js +85 -0
  133. package/validators/section-intro-final-lead.js +90 -0
  134. package/validators/section-intro-has-lead.js +41 -0
  135. package/validators/section-intro-no-tldr.js +37 -0
  136. package/validators/section-intros.js +33 -0
  137. package/validators/sentence-starters.js +132 -0
  138. package/validators/smallcaps.js +67 -0
  139. package/validators/tldr-length.js +58 -0
@@ -0,0 +1,344 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Generate a styled reference.docx for pandoc docx output.
4
+
5
+ This creates a reference document with the book's typography:
6
+ - Body text: Source Serif 4
7
+ - Headings: Source Sans 3
8
+ - Code: Source Code Pro
9
+
10
+ Color palette matches the theme's tokens (css/theme/01-tokens.css):
11
+ --color-brand: #0056b3 (links, accents)
12
+ --color-emphasis: #495057 (body text, headings)
13
+ --color-muted: #6c757d (captions, footnotes)
14
+ --color-border: #dee2e6 (table borders, hr)
15
+ """
16
+
17
+ import sys
18
+ import subprocess
19
+ import tempfile
20
+ import os
21
+ from pathlib import Path
22
+ from docx import Document
23
+ from docx.oxml.ns import qn
24
+ from docx.oxml import OxmlElement
25
+
26
+ # The build looks for reference.docx in the book root (lib/pandoc.js), which
27
+ # `book-kit generate-reference-docx` passes as $BOOK_ROOT.
28
+ OUTPUT_PATH = Path(os.environ.get("BOOK_ROOT", os.getcwd())) / "reference.docx"
29
+
30
+ # ── Color palette ─────────────────────────────────────────────────────────────
31
+ COLOR_EMPHASIS = "495057" # body text, headings
32
+ COLOR_BRAND = "0056b3" # links
33
+ COLOR_MUTED = "6c757d" # captions, footnotes
34
+ COLOR_BORDER = "DEE2E6" # table borders, HR
35
+
36
+ # ── Fonts ─────────────────────────────────────────────────────────────────────
37
+ FONT_BODY = "Source Serif 4"
38
+ FONT_HEADING = "Source Sans 3"
39
+ FONT_MONO = "Source Code Pro"
40
+
41
+ # ── Helpers ───────────────────────────────────────────────────────────────────
42
+
43
+ W_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
44
+
45
+
46
+ def w(tag):
47
+ return f"{{{W_NS}}}{tag}"
48
+
49
+
50
+ def set_font(rPr, name):
51
+ """Set the font for a run-properties element, clearing theme inheritance."""
52
+ rFonts = rPr.find(w("rFonts"))
53
+ if rFonts is None:
54
+ rFonts = OxmlElement("w:rFonts")
55
+ rPr.insert(0, rFonts)
56
+ # Remove theme-based font attributes so our explicit fonts take precedence
57
+ for attr in ("w:asciiTheme", "w:hAnsiTheme", "w:eastAsiaTheme", "w:csTheme"):
58
+ if rFonts.get(qn(attr)):
59
+ del rFonts.attrib[qn(attr)]
60
+ rFonts.set(qn("w:ascii"), name)
61
+ rFonts.set(qn("w:hAnsi"), name)
62
+ rFonts.set(qn("w:cs"), name)
63
+
64
+
65
+ def set_color(rPr, hex_color):
66
+ """Set the run color, removing any theme color or shade overrides."""
67
+ color_el = rPr.find(w("color"))
68
+ if color_el is None:
69
+ color_el = OxmlElement("w:color")
70
+ rPr.append(color_el)
71
+ color_el.set(qn("w:val"), hex_color)
72
+ # Remove theme color/shade overrides so our hex value wins
73
+ for attr in ("w:themeColor", "w:themeShade", "w:themeTint"):
74
+ if color_el.get(qn(attr)):
75
+ del color_el.attrib[qn(attr)]
76
+
77
+
78
+ def set_size(rPr, half_pts):
79
+ """Set font size in half-points (e.g. 22 → 11 pt)."""
80
+ for tag in ("w:sz", "w:szCs"):
81
+ el = rPr.find(w(tag.split(":")[1]))
82
+ if el is None:
83
+ el = OxmlElement(tag)
84
+ rPr.append(el)
85
+ el.set(qn("w:val"), str(half_pts))
86
+
87
+
88
+ def set_spacing(pPr, before_twips=None, after_twips=None, line_twips=None, line_rule=None):
89
+ """Set paragraph spacing (1pt = 20 twips)."""
90
+ spacing = pPr.find(w("spacing"))
91
+ if spacing is None:
92
+ spacing = OxmlElement("w:spacing")
93
+ pPr.append(spacing)
94
+ if before_twips is not None:
95
+ spacing.set(qn("w:before"), str(before_twips))
96
+ if after_twips is not None:
97
+ spacing.set(qn("w:after"), str(after_twips))
98
+ if line_twips is not None:
99
+ spacing.set(qn("w:line"), str(line_twips))
100
+ if line_rule is not None:
101
+ spacing.set(qn("w:lineRule"), line_rule)
102
+
103
+
104
+ def get_or_create(parent, tag):
105
+ el = parent.find(w(tag))
106
+ if el is None:
107
+ el = OxmlElement(f"w:{tag}")
108
+ parent.append(el)
109
+ return el
110
+
111
+
112
+ def ensure_pPr(style_el):
113
+ return get_or_create(style_el, "pPr")
114
+
115
+
116
+ def ensure_rPr(style_el):
117
+ return get_or_create(style_el, "rPr")
118
+
119
+
120
+ def style_heading(style_el, size_half_pts, before_twips, after_twips=120):
121
+ """Apply book heading style (Source Sans 3, emphasis color)."""
122
+ pPr = ensure_pPr(style_el)
123
+ rPr = ensure_rPr(style_el)
124
+ set_spacing(pPr, before_twips=before_twips, after_twips=after_twips)
125
+ set_font(rPr, FONT_HEADING)
126
+ set_color(rPr, COLOR_EMPHASIS)
127
+ set_size(rPr, size_half_pts)
128
+ # Ensure bold is set
129
+ if rPr.find(w("b")) is None:
130
+ b = OxmlElement("w:b")
131
+ rPr.append(b)
132
+ if rPr.find(w("bCs")) is None:
133
+ bCs = OxmlElement("w:bCs")
134
+ rPr.append(bCs)
135
+
136
+
137
+ def apply_border(tcPr_or_tblPr, tag, color=COLOR_BORDER, sz="4", val="single"):
138
+ """Add a border child to a table cell or table properties element."""
139
+ borders = tcPr_or_tblPr.find(w(tag))
140
+ if borders is None:
141
+ borders = OxmlElement(f"w:{tag}")
142
+ tcPr_or_tblPr.append(borders)
143
+ for side in ("top", "left", "bottom", "right", "insideH", "insideV"):
144
+ el = borders.find(w(side))
145
+ if el is None:
146
+ el = OxmlElement(f"w:{side}")
147
+ borders.append(el)
148
+ el.set(qn("w:val"), val)
149
+ el.set(qn("w:sz"), sz)
150
+ el.set(qn("w:color"), color)
151
+
152
+
153
+ # ── Main ──────────────────────────────────────────────────────────────────────
154
+
155
+ def main():
156
+ # Start from pandoc's built-in reference.docx so all required styles exist
157
+ result = subprocess.run(
158
+ ["pandoc", "--print-default-data-file", "reference.docx"],
159
+ capture_output=True,
160
+ )
161
+ if result.returncode != 0:
162
+ print("ERROR: pandoc not found or failed", file=sys.stderr)
163
+ sys.exit(1)
164
+
165
+ with tempfile.NamedTemporaryFile(suffix=".docx", delete=False) as tmp:
166
+ tmp.write(result.stdout)
167
+ tmp_path = tmp.name
168
+
169
+ doc = Document(tmp_path)
170
+ os.unlink(tmp_path)
171
+
172
+ styles = {s.style_id: s for s in doc.styles}
173
+
174
+ # ── Normal (body text) ────────────────────────────────────────────────────
175
+ normal = styles["Normal"]
176
+ pPr = ensure_pPr(normal.element)
177
+ rPr = ensure_rPr(normal.element)
178
+ set_font(rPr, FONT_BODY)
179
+ set_color(rPr, COLOR_EMPHASIS)
180
+ set_size(rPr, 22) # 11 pt
181
+ set_spacing(pPr, after_twips=120, line_twips=336, line_rule="auto") # ~1.4 line spacing
182
+
183
+ # ── Body Text (used for most prose) ──────────────────────────────────────
184
+ if "BodyText" in styles:
185
+ bt = styles["BodyText"]
186
+ pPr = ensure_pPr(bt.element)
187
+ rPr = ensure_rPr(bt.element)
188
+ set_font(rPr, FONT_BODY)
189
+ set_color(rPr, COLOR_EMPHASIS)
190
+ set_size(rPr, 22)
191
+ set_spacing(pPr, after_twips=120, line_twips=336, line_rule="auto")
192
+
193
+ # ── Headings ──────────────────────────────────────────────────────────────
194
+ # H1: chapter / part titles — 20 pt, big space before
195
+ if "Heading1" in styles:
196
+ style_heading(styles["Heading1"].element, size_half_pts=40, before_twips=480, after_twips=200)
197
+
198
+ # H2: section titles — 16 pt
199
+ if "Heading2" in styles:
200
+ style_heading(styles["Heading2"].element, size_half_pts=32, before_twips=360, after_twips=160)
201
+
202
+ # H3: subsection — 13 pt
203
+ if "Heading3" in styles:
204
+ style_heading(styles["Heading3"].element, size_half_pts=26, before_twips=280, after_twips=120)
205
+
206
+ # H4: 12 pt
207
+ if "Heading4" in styles:
208
+ style_heading(styles["Heading4"].element, size_half_pts=24, before_twips=240, after_twips=80)
209
+
210
+ # H5: 11 pt
211
+ if "Heading5" in styles:
212
+ style_heading(styles["Heading5"].element, size_half_pts=22, before_twips=200, after_twips=80)
213
+
214
+ # H6: 11 pt, italic
215
+ if "Heading6" in styles:
216
+ style_heading(styles["Heading6"].element, size_half_pts=22, before_twips=200, after_twips=80)
217
+ h6_rPr = ensure_rPr(styles["Heading6"].element)
218
+ if h6_rPr.find(w("i")) is None:
219
+ i = OxmlElement("w:i")
220
+ h6_rPr.append(i)
221
+
222
+ # ── Title / Subtitle / Author (title page) ────────────────────────────────
223
+ if "Title" in styles:
224
+ title_style = styles["Title"]
225
+ pPr = ensure_pPr(title_style.element)
226
+ rPr = ensure_rPr(title_style.element)
227
+ set_font(rPr, FONT_HEADING)
228
+ set_color(rPr, COLOR_EMPHASIS)
229
+ set_size(rPr, 56) # 28 pt
230
+ set_spacing(pPr, before_twips=0, after_twips=240)
231
+ # Ensure bold is removed — title uses weight from font
232
+ for tag in ("w:b", "w:bCs"):
233
+ el = rPr.find(w(tag.split(":")[1]))
234
+ if el is not None:
235
+ rPr.remove(el)
236
+
237
+ if "Subtitle" in styles:
238
+ subtitle_style = styles["Subtitle"]
239
+ pPr = ensure_pPr(subtitle_style.element)
240
+ rPr = ensure_rPr(subtitle_style.element)
241
+ set_font(rPr, FONT_HEADING)
242
+ set_color(rPr, COLOR_MUTED)
243
+ set_size(rPr, 32) # 16 pt
244
+ set_spacing(pPr, before_twips=0, after_twips=480)
245
+
246
+ if "Author" in styles:
247
+ author_style = styles["Author"]
248
+ pPr = ensure_pPr(author_style.element)
249
+ rPr = ensure_rPr(author_style.element)
250
+ set_font(rPr, FONT_HEADING)
251
+ set_color(rPr, COLOR_MUTED)
252
+ set_size(rPr, 24) # 12 pt
253
+ set_spacing(pPr, before_twips=0, after_twips=120)
254
+
255
+ # ── Block Text (blockquote) ───────────────────────────────────────────────
256
+ if "BlockText" in styles:
257
+ bt = styles["BlockText"]
258
+ pPr = ensure_pPr(bt.element)
259
+ rPr = ensure_rPr(bt.element)
260
+ set_font(rPr, FONT_BODY)
261
+ set_color(rPr, COLOR_MUTED)
262
+ set_size(rPr, 22)
263
+ set_spacing(pPr, before_twips=120, after_twips=120, line_twips=336, line_rule="auto")
264
+ # Indent blockquotes
265
+ ind = pPr.find(w("ind"))
266
+ if ind is None:
267
+ ind = OxmlElement("w:ind")
268
+ pPr.append(ind)
269
+ ind.set(qn("w:left"), "720") # 0.5 inch
270
+ ind.set(qn("w:right"), "720")
271
+
272
+ # ── Verbatim (code blocks) ────────────────────────────────────────────────
273
+ if "VerbatimChar" in styles:
274
+ vc = styles["VerbatimChar"]
275
+ rPr = ensure_rPr(vc.element)
276
+ set_font(rPr, FONT_MONO)
277
+ set_color(rPr, COLOR_EMPHASIS)
278
+ set_size(rPr, 20) # 10 pt
279
+
280
+ # ── Footnote Text ─────────────────────────────────────────────────────────
281
+ if "FootnoteText" in styles:
282
+ fn = styles["FootnoteText"]
283
+ pPr = ensure_pPr(fn.element)
284
+ rPr = ensure_rPr(fn.element)
285
+ set_font(rPr, FONT_BODY)
286
+ set_color(rPr, COLOR_MUTED)
287
+ set_size(rPr, 18) # 9 pt
288
+ set_spacing(pPr, before_twips=0, after_twips=80)
289
+
290
+ # ── Hyperlink ────────────────────────────────────────────────────────────
291
+ if "Hyperlink" in styles:
292
+ hl = styles["Hyperlink"]
293
+ rPr = ensure_rPr(hl.element)
294
+ set_color(rPr, COLOR_BRAND)
295
+ # Ensure underline
296
+ u = rPr.find(w("u"))
297
+ if u is None:
298
+ u = OxmlElement("w:u")
299
+ rPr.append(u)
300
+ u.set(qn("w:val"), "single")
301
+
302
+ # ── Caption / Table Caption / Image Caption ───────────────────────────────
303
+ for style_id in ("Caption", "TableCaption", "ImageCaption"):
304
+ if style_id in styles:
305
+ cap = styles[style_id]
306
+ rPr = ensure_rPr(cap.element)
307
+ set_font(rPr, FONT_HEADING)
308
+ set_color(rPr, COLOR_MUTED)
309
+ set_size(rPr, 18) # 9 pt
310
+
311
+ # ── TOC Heading ───────────────────────────────────────────────────────────
312
+ if "TOCHeading" in styles:
313
+ toch = styles["TOCHeading"]
314
+ pPr = ensure_pPr(toch.element)
315
+ rPr = ensure_rPr(toch.element)
316
+ set_font(rPr, FONT_HEADING)
317
+ set_color(rPr, COLOR_EMPHASIS)
318
+ set_size(rPr, 32) # 16 pt
319
+
320
+ # ── Document-level defaults: page margins ─────────────────────────────────
321
+ # Set comfortable 1-inch margins
322
+ body = doc.element.body
323
+ sectPr = body.find(w("sectPr"))
324
+ if sectPr is None:
325
+ sectPr = OxmlElement("w:sectPr")
326
+ body.append(sectPr)
327
+ pgMar = sectPr.find(w("pgMar"))
328
+ if pgMar is None:
329
+ pgMar = OxmlElement("w:pgMar")
330
+ sectPr.append(pgMar)
331
+ # 1 inch = 1440 twips
332
+ pgMar.set(qn("w:top"), "1440")
333
+ pgMar.set(qn("w:right"), "1440")
334
+ pgMar.set(qn("w:bottom"), "1440")
335
+ pgMar.set(qn("w:left"), "1440")
336
+ pgMar.set(qn("w:header"), "720")
337
+ pgMar.set(qn("w:footer"), "720")
338
+
339
+ doc.save(str(OUTPUT_PATH))
340
+ print(f"Generated {OUTPUT_PATH}")
341
+
342
+
343
+ if __name__ == "__main__":
344
+ main()
@@ -0,0 +1,173 @@
1
+ #!/usr/bin/env node
2
+ // Generate Kindle-specific CSS with static font-face declarations.
3
+ // Kindle e-ink ignores variable fonts (font-weight: 200 900) and falls
4
+ // back to system fonts. This script replaces variable @font-face blocks
5
+ // with individual static declarations referencing fonts/kindle/*.woff2.
6
+
7
+ import { readFileSync, writeFileSync } from 'node:fs';
8
+ import { calloutSelectors, emojiSelectors } from '../lib/callouts.js';
9
+ import { join } from 'node:path';
10
+ import { pathFor } from '../lib/config.js';
11
+ import { isMain } from '../lib/is-main.js';
12
+
13
+ // Regex to strip all @font-face blocks (variable or static)
14
+ export const FONT_FACE_RE = /@font-face\s*\{[^}]*\}\s*/g;
15
+
16
+ // Static @font-face declarations for Kindle
17
+ export const staticFonts = `/* Static fonts for Kindle e-ink (variable fonts unsupported) */
18
+ @font-face {
19
+ font-family: "Source Serif 4";
20
+ font-style: normal;
21
+ font-weight: 400;
22
+ src: url("../fonts/SourceSerif4-Regular.woff2") format("woff2");
23
+ }
24
+ @font-face {
25
+ font-family: "Source Serif 4";
26
+ font-style: normal;
27
+ font-weight: 700;
28
+ src: url("../fonts/SourceSerif4-Bold.woff2") format("woff2");
29
+ }
30
+ @font-face {
31
+ font-family: "Source Serif 4";
32
+ font-style: italic;
33
+ font-weight: 400;
34
+ src: url("../fonts/SourceSerif4-Italic.woff2") format("woff2");
35
+ }
36
+ @font-face {
37
+ font-family: "Source Serif 4";
38
+ font-style: italic;
39
+ font-weight: 700;
40
+ src: url("../fonts/SourceSerif4-BoldItalic.woff2") format("woff2");
41
+ }
42
+ @font-face {
43
+ font-family: "Source Sans 3";
44
+ font-style: normal;
45
+ font-weight: 400;
46
+ src: url("../fonts/SourceSans3-Regular.woff2") format("woff2");
47
+ }
48
+ @font-face {
49
+ font-family: "Source Sans 3";
50
+ font-style: normal;
51
+ font-weight: 600;
52
+ src: url("../fonts/SourceSans3-SemiBold.woff2") format("woff2");
53
+ }
54
+ @font-face {
55
+ font-family: "Source Sans 3";
56
+ font-style: normal;
57
+ font-weight: 700;
58
+ src: url("../fonts/SourceSans3-Bold.woff2") format("woff2");
59
+ }
60
+ @font-face {
61
+ font-family: "Source Sans 3";
62
+ font-style: italic;
63
+ font-weight: 400;
64
+ src: url("../fonts/SourceSans3-Italic.woff2") format("woff2");
65
+ }
66
+ @font-face {
67
+ font-family: "Source Code Pro";
68
+ font-style: normal;
69
+ font-weight: 400;
70
+ src: url("../fonts/SourceCodePro-Regular.woff2") format("woff2");
71
+ }
72
+ @font-face {
73
+ font-family: "Source Code Pro";
74
+ font-style: normal;
75
+ font-weight: 700;
76
+ src: url("../fonts/SourceCodePro-Bold.woff2") format("woff2");
77
+ }
78
+ @font-face {
79
+ font-family: "Source Code Pro";
80
+ font-style: italic;
81
+ font-weight: 400;
82
+ src: url("../fonts/SourceCodePro-Italic.woff2") format("woff2");
83
+ }
84
+
85
+ `;
86
+
87
+ // Inline emoji image styling for Kindle (Twemoji PNGs)
88
+ export const emojiCss = `
89
+ /* Inline emoji images (Twemoji PNGs for Kindle e-ink) */
90
+ img.emoji {
91
+ height: 1em !important;
92
+ width: 1em !important;
93
+ vertical-align: middle !important;
94
+ margin: 0 !important;
95
+ padding: 0 !important;
96
+ display: inline !important;
97
+ line-height: inherit;
98
+ background-color: transparent !important;
99
+ }
100
+ /* Inline callout emoji with label text */
101
+ img.callout-emoji {
102
+ height: 1.2em !important;
103
+ width: 1.2em !important;
104
+ display: inline !important;
105
+ margin: 0 0.15em 0 0 !important;
106
+ vertical-align: middle !important;
107
+ }
108
+ /* Callouts have no fill on Kindle (see calloutFlowCss), so callout emoji
109
+ backgrounds stay transparent — the white-under-alpha PNG composite blends
110
+ into the (unfilled) callout. No per-callout color matching needed. */
111
+ ${emojiSelectors.flatMap((sel) => [`${sel} img.callout-emoji`, `${sel} img.emoji`]).join(',\n')} {
112
+ background-color: transparent !important;
113
+ background: transparent !important;
114
+ }
115
+ /* Dark mode: keep emoji backgrounds transparent (callouts are unfilled). */
116
+ @media (prefers-color-scheme: dark) {
117
+ img.emoji {
118
+ opacity: 1 !important;
119
+ background-color: transparent !important;
120
+ }
121
+ }
122
+ `;
123
+
124
+ // Kindle won't cleanly split a filled, fully-bordered box: it either jumps the
125
+ // whole callout to the next screen (leaving a blank gap) or — once allowed to
126
+ // split — emits an empty cloned-decoration frame. Drop the fill and the
127
+ // box-decoration cloning so callouts reflow across screens like a blockquote.
128
+ // The left border (set per-callout in the base CSS) stays as the visual cue,
129
+ // and the Lua-injected label still identifies each callout. Kindle-only: the
130
+ // shared EPUB (Apple Books / Kobo) keeps the filled boxes via style-epub.css.
131
+ //
132
+ // Two extra Kindle-only nudges so a callout backfills the bottom of a page
133
+ // instead of jumping whole and leaving a blank gap before it:
134
+ // - border-radius: 0 — a rounded box reads as atomic to Kindle's renderer,
135
+ // which then refuses to split it across a screen boundary (rounding is
136
+ // imperceptible on e-ink anyway, so nothing is lost visually).
137
+ // - orphans/widows: 2 — the base CSS sets 3, so any callout under ~6 lines
138
+ // can never satisfy "3 stay, 3 move" and is forced whole onto the next
139
+ // page. Dropping to 2 lets medium/long callouts split and backfill.
140
+ // This does NOT eliminate the gap for genuinely short (≤4-line) callouts —
141
+ // those can't split without leaving a single-line orphan, which looks worse
142
+ // than the gap. The remaining gap there is inherent to reflowable pagination.
143
+ export const calloutFlowCss = `
144
+ /* Kindle: callouts flow like blockquotes (no fill, no cloned frame). */
145
+ ${calloutSelectors.join(',\n')} {
146
+ background-color: transparent !important;
147
+ background: transparent !important;
148
+ border-radius: 0 !important;
149
+ orphans: 2 !important;
150
+ widows: 2 !important;
151
+ -webkit-box-decoration-break: slice !important;
152
+ box-decoration-break: slice !important;
153
+ page-break-before: auto !important;
154
+ break-before: auto !important;
155
+ page-break-inside: auto !important;
156
+ break-inside: auto !important;
157
+ }
158
+ `;
159
+
160
+ // Replace variable @font-face blocks with static Kindle declarations and append
161
+ // Kindle-specific rules (inline emoji images, flowing callouts).
162
+ export function generateKindleCss(epubCss) {
163
+ const stripped = epubCss.replace(FONT_FACE_RE, '');
164
+ return staticFonts + stripped + emojiCss + calloutFlowCss;
165
+ }
166
+
167
+ if (isMain(import.meta.url)) {
168
+ const distDir = pathFor('dist');
169
+ const input = join(distDir, 'style-epub.css');
170
+ const output = join(distDir, 'style-kindle.css');
171
+ writeFileSync(output, generateKindleCss(readFileSync(input, 'utf8')));
172
+ console.log(`Generated ${output} with static font-face declarations`);
173
+ }