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.
- package/LICENSE +21 -0
- package/NOTICE.md +72 -0
- package/README.md +251 -2
- package/bin/book-kit.js +115 -0
- package/commands/_emoji_io.py +32 -0
- package/commands/audit-chapter-lengths.js +244 -0
- package/commands/check-epub-size.js +68 -0
- package/commands/check-pdf-page-count.js +162 -0
- package/commands/composite-kindle-emoji.py +85 -0
- package/commands/composite-print-emoji.py +98 -0
- package/commands/conventions.js +76 -0
- package/commands/convert-pdfx.sh +194 -0
- package/commands/download-icc-profile.sh +105 -0
- package/commands/fonts.py +212 -0
- package/commands/generate-reference-docx.py +344 -0
- package/commands/kindle-fonts.js +173 -0
- package/commands/links-to-footnotes.js +366 -0
- package/commands/markdownlint.js +25 -0
- package/commands/minify-css.js +43 -0
- package/commands/optimize-images.js +73 -0
- package/commands/parity.sh +150 -0
- package/commands/postprocess-epub.js +541 -0
- package/commands/prep-print-src.js +77 -0
- package/commands/prose.js +38 -0
- package/commands/release-files.js +89 -0
- package/commands/reorder-print-frontmatter.js +79 -0
- package/commands/strip-page-rules.js +1168 -0
- package/commands/strip-property-rules.js +93 -0
- package/commands/update-key-takeaways.js +314 -0
- package/commands/validate.js +283 -0
- package/configs/markdownlint-cli2.jsonc +98 -0
- package/configs/textlintrc.json +22 -0
- package/css/theme/01-tokens.css +83 -0
- package/css/theme/02-fonts.css +79 -0
- package/css/theme/03-prose.css +53 -0
- package/css/theme/04-base.css +226 -0
- package/css/theme/05-components.css +546 -0
- package/css/theme/06-utilities.css +40 -0
- package/css/theme/07-ereader.css +245 -0
- package/css/theme/08-epub.css +865 -0
- package/css/theme/09-back-matter.css +47 -0
- package/css/theme/10-dark.css +302 -0
- package/css/theme/11-print.css +1229 -0
- package/css/theme.css +38 -0
- package/defaults/book.config.yml +318 -0
- package/defaults/pandoc-base.yml +19 -0
- package/defaults/pandoc-docx.yml +6 -0
- package/defaults/pandoc-epub-fonts.yml +6 -0
- package/defaults/pandoc-epub.yml +15 -0
- package/defaults/pandoc-kindle.yml +6 -0
- package/emoji/1f382.png +0 -0
- package/emoji/1f389.png +0 -0
- package/emoji/1f440.png +0 -0
- package/emoji/1f44d.png +0 -0
- package/emoji/1f44f.png +0 -0
- package/emoji/1f454.png +0 -0
- package/emoji/1f4a1.png +0 -0
- package/emoji/1f4bb.png +0 -0
- package/emoji/1f602.png +0 -0
- package/emoji/1f603.png +0 -0
- package/emoji/1f604.png +0 -0
- package/emoji/1f60a.png +0 -0
- package/emoji/1f60f.png +0 -0
- package/emoji/1f64c.png +0 -0
- package/emoji/1f64f.png +0 -0
- package/emoji/1f914.png +0 -0
- package/emoji/1f923.png +0 -0
- package/emoji/2714.png +0 -0
- package/emoji/274c.png +0 -0
- package/filters/add-div-titles.lua +196 -0
- package/filters/arc-notice.lua +36 -0
- package/filters/body-emoji-images.lua +48 -0
- package/filters/callout-classes.lua +40 -0
- package/filters/lib/book_kit.lua +110 -0
- package/filters/revision.lua +17 -0
- package/filters/strip-comments.lua +40 -0
- package/filters/strip-emoji-kindle.lua +134 -0
- package/fonts/LICENSE-source-code-pro.md +93 -0
- package/fonts/LICENSE-source-sans.md +93 -0
- package/fonts/LICENSE-source-serif.md +93 -0
- package/fonts/recipe.json +41 -0
- package/just/book-kit.just +229 -0
- package/lib/acronyms.js +47 -0
- package/lib/callouts.js +17 -0
- package/lib/config.js +211 -0
- package/lib/is-main.js +21 -0
- package/lib/lint.js +148 -0
- package/lib/pandoc.js +148 -0
- package/lib/paragraphs.js +173 -0
- package/lib/revision.js +35 -0
- package/lib/structure.js +39 -0
- package/lib/textlint-github-formatter.js +58 -0
- package/lib/validate.js +202 -0
- package/package.json +98 -4
- package/presets/business-nonfiction/CONVENTIONS.md +35 -0
- package/presets/business-nonfiction/preset.yml +128 -0
- package/profiles/pdfx-def.ps +42 -0
- package/requirements.txt +14 -0
- package/rules/no-ai-like-patterns.js +209 -0
- package/rules/no-assumptive.js +76 -0
- package/rules/no-bare-acronyms.js +129 -0
- package/rules/no-cliches.js +544 -0
- package/rules/no-cultural-idioms.js +422 -0
- package/rules/no-vague-intensifiers.js +94 -0
- package/templates/template-epub.html +79 -0
- package/templates/template.html +73 -0
- package/toolchain.json +42 -0
- package/validators/acronym-expansion.js +94 -0
- package/validators/bold-keywords.js +56 -0
- package/validators/callout-balance.js +74 -0
- package/validators/callout-divs.js +66 -0
- package/validators/callout-labels.js +102 -0
- package/validators/chapter-lengths.js +14 -0
- package/validators/chapter.js +57 -0
- package/validators/cross-references.js +45 -0
- package/validators/doubled-words.js +76 -0
- package/validators/duplicate-headings.js +52 -0
- package/validators/em-dashes.js +73 -0
- package/validators/escaped-smallcaps.js +46 -0
- package/validators/footnotes.js +78 -0
- package/validators/heading-hierarchy.js +58 -0
- package/validators/hypothetical-hooks.js +103 -0
- package/validators/index-integrity.js +28 -0
- package/validators/index.js +73 -0
- package/validators/list-punctuation.js +123 -0
- package/validators/min-opener-length.js +87 -0
- package/validators/no-bare-url-link-text.js +38 -0
- package/validators/no-todo-markers.js +49 -0
- package/validators/no-transition-todos.js +34 -0
- package/validators/number-range-dashes.js +72 -0
- package/validators/paragraph-length.js +62 -0
- package/validators/quote-punctuation.js +85 -0
- package/validators/section-intro-final-lead.js +90 -0
- package/validators/section-intro-has-lead.js +41 -0
- package/validators/section-intro-no-tldr.js +37 -0
- package/validators/section-intros.js +33 -0
- package/validators/sentence-starters.js +132 -0
- package/validators/smallcaps.js +67 -0
- 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
|
+
}
|