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
package/css/theme.css ADDED
@@ -0,0 +1,38 @@
1
+ /* book-kit theme - Tailwind CSS v4 stylesheet
2
+ (imported by a book's src/style.css after @import "tailwindcss")
3
+ Custom styles for multi-format book output (HTML, EPUB, PDF)
4
+ Following Tailwind v4 CSS-first configuration best practices */
5
+
6
+ /*
7
+ CSS Pipeline:
8
+ src/style.css (this file)
9
+ → PostCSS (npm run build:css) → dist/style.css
10
+ ├── HTML: used directly with full CSS feature support
11
+ ├── PDF: WeasyPrint renders @media print rules (CSS Paged Media)
12
+ └── EPUB: commands/strip-page-rules.js → dist/style-epub.css
13
+ (strips @page, @layer, var(), calc(), oklch() etc.)
14
+ └── Kindle: commands/kindle-fonts.js replaces variable fonts
15
+ with static weight-specific font declarations
16
+
17
+ Format-specific notes:
18
+ • @media print — PDF/WeasyPrint: absolute pt sizes, float:footnote, @page rules
19
+ • @media not screen — EPUB overrides: em headings, inherit body typography, hyphens
20
+ • @media (prefers-color-scheme: dark) — Apple Books, Kobo dark mode adaptation
21
+ • WeasyPrint ignores variable font-weight ranges — explicit @font-face needed
22
+ • EPUB strips ::before labels — add-div-titles.lua injects text nodes instead
23
+ */
24
+
25
+ /* The theme is split by section into css/theme/, imported here in cascade
26
+ order. A book can import this file for all of it, or the parts it wants
27
+ (keeping this order), e.g. everything but 09-back-matter.css. */
28
+ @import "./theme/01-tokens.css";
29
+ @import "./theme/02-fonts.css";
30
+ @import "./theme/03-prose.css";
31
+ @import "./theme/04-base.css";
32
+ @import "./theme/05-components.css";
33
+ @import "./theme/06-utilities.css";
34
+ @import "./theme/07-ereader.css";
35
+ @import "./theme/08-epub.css";
36
+ @import "./theme/09-back-matter.css";
37
+ @import "./theme/10-dark.css";
38
+ @import "./theme/11-print.css";
@@ -0,0 +1,318 @@
1
+ # book-kit defaults. Every key here can be overridden in a book's
2
+ # `book.config.yml`. Maps are deep-merged; lists replace the default wholesale.
3
+ #
4
+ # This file doubles as the config reference: each key is documented where it's
5
+ # defined, so there's one place to look.
6
+
7
+ # Where the book's files live, relative to the directory holding book.config.yml.
8
+ paths:
9
+ src: src
10
+ dist: dist
11
+ index: index.yml # pandoc defaults file listing `input-files` in reading order
12
+ css: src/style.css # Tailwind entry point (imports the kit theme)
13
+ cover: img/cover.jpeg
14
+ img: img
15
+ fonts: fonts # if missing, the build symlinks the kit's fonts here (gitignore it)
16
+ version: VERSION # plain-text semver, stamped into the build as "<version> (<sha>)"
17
+ epub_metadata: epub.xml # Dublin Core supplement for the EPUB OPF; generated from metadata if absent
18
+
19
+ # Output files are dist/<basename>.epub, <basename>.pdf, <basename>-kindle.epub, etc.
20
+ output:
21
+ basename: book
22
+ # The PDF/X-1a file is dist/<basename><pdfx_suffix>.pdf. A book that already
23
+ # ships it under another name (e.g. -printer) can keep that name.
24
+ pdfx_suffix: -pdfx
25
+
26
+ # Bibliographic metadata. Passed to pandoc as metadata, so it lands in the
27
+ # EPUB OPF, the HTML <head>, and the PDF info dictionary.
28
+ metadata:
29
+ title: Untitled Book
30
+ subtitle: ""
31
+ description: ""
32
+ author: []
33
+ lang: en-US
34
+ rights: ""
35
+ publisher: ""
36
+ isbn_paperback: ""
37
+ isbn_ebook: ""
38
+ pub_date: "" # YYYY-MM-DD
39
+ copyright_year: ""
40
+ uuid: "" # stable EPUB identifier; mint once, never change
41
+ bisac: [] # BISAC codes, e.g. REF000000
42
+ keywords: [] # retailer keyword phrases
43
+ subjects: [] # <dc:subject> lines for the generated epub.xml
44
+
45
+ # Reserved: no kit command reads this yet. Public site for the book, if any,
46
+ # for books' own tooling. Leave empty if there isn't one.
47
+ site_url: ""
48
+
49
+ # EPUB accessibility metadata (schema.org properties in the OPF). The kit's
50
+ # markup is built for WCAG 2.1 AA (semantic structure, DPUB-ARIA roles, alt
51
+ # text, reflowable text), but a conformance claim is the publisher's to make:
52
+ # conforms_to, summary, and certified_by are empty until the book sets them,
53
+ # after checking its own content (e.g. with Ace by DAISY).
54
+ accessibility:
55
+ access_modes: [textual]
56
+ access_mode_sufficient: [textual]
57
+ features: [alternativeText, readingOrder, structuralNavigation, tableOfContents, displayTransformability, ARIA]
58
+ hazards: [none]
59
+ summary: "" # e.g. "This publication conforms to WCAG 2.1 Level AA. ..."
60
+ conforms_to: "" # e.g. "EPUB Accessibility 1.1 - WCAG 2.1 Level AA"
61
+ certified_by: "" # who makes the claim; defaults to metadata.publisher once conforms_to is set
62
+ conformance_url: "" # public accessibility statement, e.g. https://example.com/accessibility/
63
+ # The summary in other languages, keyed by primary language subtag, for
64
+ # translated editions (an EPUB's dc:language "pt-BR" uses pt). Used only
65
+ # when conforms_to is set, since these describe a WCAG 2.1 AA claim. The
66
+ # kit's are drafts: have a native speaker review one before shipping it.
67
+ summary_translations:
68
+ pt: Esta publicação está em conformidade com as WCAG 2.1 Nível AA. É bem estruturada, com ordem de leitura lógica e sumário navegável, e utiliza marcação semântica em todo o conteúdo. Todo o conteúdo é textual.
69
+ es: Esta publicación cumple con las WCAG 2.1 Nivel AA. Está bien estructurada, con un orden de lectura lógico y una tabla de contenidos navegable, y utiliza marcado semántico en todo el contenido. Todo el contenido es textual.
70
+ de: Diese Publikation erfüllt die WCAG 2.1 Stufe AA. Sie ist gut strukturiert, mit logischer Lesereihenfolge und navigierbarem Inhaltsverzeichnis, und verwendet durchgängig semantisches Markup. Alle Inhalte sind textbasiert.
71
+
72
+ # Text the build adds to the book, for books in other languages.
73
+ strings:
74
+ copyright_nav: Copyright # the copyright page's entry in the EPUB table of contents
75
+ cover_label: "Book cover:" # screen-reader label before the title on the EPUB cover page
76
+ # The notice the ARC builds (`just arc`) put at the top of the copyright page.
77
+ arc_notice:
78
+ heading: Advance Reader Copy — not for sale or redistribution.
79
+ body: This advance copy is licensed to you for review purposes only. Please don’t forward, upload, share, or resell this file. Brief quotations in a review are welcome and encouraged.
80
+
81
+ # Extra pandoc metadata merged into every format, for anything the keys above
82
+ # don't cover (e.g. belongs-to-collection).
83
+ pandoc_metadata: {}
84
+
85
+ # Print (PDF) settings.
86
+ print:
87
+ trim: [6, 9] # inches; must match the @page size in the CSS (`just check` verifies the PDF)
88
+ page_count: null # set to lock the PDF page count in CI (null = no lock)
89
+ pdfx_title: "" # PDF/X-1a info-dictionary title; defaults to metadata.title
90
+
91
+ # Format checks (`just check`).
92
+ checks:
93
+ # Size budget for each EPUB, retail and ARC, in MB. Retailers charge delivery
94
+ # fees by file size and some cap uploads, so a jump usually means an image
95
+ # went in uncompressed.
96
+ epub_max_mb: 2
97
+
98
+ # Callout registry, in priority order. Each entry is a fenced div class:
99
+ #
100
+ # ::: {.tip custom-style="tip"}
101
+ # ...
102
+ # :::
103
+ #
104
+ # class fenced-div class name
105
+ # label visible prefix the build injects ("Tip:"); omit for none
106
+ # aria aria-label for the div (role="doc-notice")
107
+ # emoji emoji shown before the label in EPUB
108
+ # emoji_cp Twemoji codepoint for the PNG used in Kindle/PDF (e-ink and
109
+ # WeasyPrint can't render color emoji text)
110
+ # emoji_alt alt text for that PNG
111
+ # pair_with another callout class a chapter using this one should also
112
+ # use; the callout-balance validator warns when it doesn't
113
+ # (list it on both entries for a two-way pair)
114
+ # generic false to style this class yourself instead of the theme's
115
+ # generic callout look (below)
116
+ #
117
+ # The labels are injected by the build; writers never type them. Validators
118
+ # flag a callout whose body starts with its own label. The kit theme styles
119
+ # these three and the business-nonfiction preset's six (tldr, pro-tip,
120
+ # for-managers, for-ics, common-objection, takeaways) each their own way. A
121
+ # class the book adds gets the .note look and its label in every format
122
+ # (filters/callout-classes.lua); restyle it in the book's CSS if you like.
123
+ callouts:
124
+ - class: note
125
+ label: "Note:"
126
+ aria: Note
127
+ - class: tip
128
+ label: "Tip:"
129
+ aria: Tip
130
+ - class: warning
131
+ label: "Warning:"
132
+ aria: Warning
133
+
134
+ # Emoji images. Color emoji don't render on Kindle e-ink and crash WeasyPrint
135
+ # in CSS content:, so those builds swap in Twemoji PNGs (CC-BY 4.0). The kit's
136
+ # composite scripts derive per-format variants from source_dir.
137
+ emoji:
138
+ source_dir: "" # empty = the kit's emoji/ directory
139
+ kindle_dir: dist/.book-kit/emoji-kindle/ # book-relative (EPUB embeds it)
140
+ pdf_callout_dir: .book-kit/emoji-print-callout/ # relative to dist/ (the PDF base URL)
141
+ pdf_body_dir: .book-kit/emoji-print-body/ # relative to dist/
142
+ # Emoji in body text that the Kindle and PDF builds swap for a PNG from
143
+ # source_dir: emoji (the exact character sequence) -> cp (the PNG's file
144
+ # name) and alt (its alt text). kindle: false keeps one as text on Kindle.
145
+ # A key can include a variation selector ("\u2744\uFE0F") so the selector
146
+ # is consumed with the emoji instead of rendering as a missing glyph.
147
+ # A book adds an emoji with an entry here plus its PNG in source_dir (copy
148
+ # the kit's emoji/ there first). On Kindle, emoji not listed are dropped.
149
+ images:
150
+ "\U0001F44D": { cp: 1f44d, alt: thumbs-up }
151
+ "\U0001F389": { cp: 1f389, alt: party }
152
+ "\U0001F602": { cp: 1f602, alt: laughing }
153
+ "\U0001F440": { cp: 1f440, alt: eyes }
154
+ "\U0001F604": { cp: 1f604, alt: grin }
155
+ "\U0001F60A": { cp: 1f60a, alt: smile }
156
+ "\U0001F603": { cp: 1f603, alt: smiley }
157
+ "\U0001F923": { cp: 1f923, alt: rofl }
158
+ "\U0001F44F": { cp: 1f44f, alt: clap }
159
+ "\U0001F64F": { cp: 1f64f, alt: folded-hands }
160
+ "\U0001F64C": { cp: 1f64c, alt: raised-hands }
161
+ "\U0001F914": { cp: 1f914, alt: thinking }
162
+ "\U0001F60F": { cp: 1f60f, alt: smirk }
163
+ "\U0001F382": { cp: 1f382, alt: cake }
164
+ "\U0001F4A1": { cp: 1f4a1, alt: light-bulb }
165
+ "\U0001F454": { cp: 1f454, alt: necktie }
166
+ "\U0001F4BB": { cp: 1f4bb, alt: laptop }
167
+ # Do/don't markers. Their text glyphs render poorly in both builds (tofu on
168
+ # Kindle, small and high in WeasyPrint). ✅ uses 2714, a heavy check with
169
+ # no box, not its own 2705 glyph, which is a check on a filled square.
170
+ "❌": { cp: 274c, alt: cross }
171
+ "✅": { cp: "2714", alt: check }
172
+
173
+ # Chapter structure conventions used by the validators, dashboard, and audits.
174
+ structure:
175
+ # Pages that aren't narrative chapters (matched by file slug; a trailing *
176
+ # matches a prefix). The prose validators and audits skip them, as they do
177
+ # takeaways collections and *-scratch.md drafts.
178
+ structural_slugs: [copyright, dedication, acknowledgements, about-the-author, back-cover, disclaimer]
179
+ # Drafts: files whose name ends with this suffix (before .md) aren't part of
180
+ # the book yet. The validators, audits, markdownlint, and the Claude chapter
181
+ # hook skip them.
182
+ draft_suffix: -scratch
183
+ # Chapters whose filename ends with this suffix collect the section's TL;DRs
184
+ # (generated by `just key-takeaways`); the validators skip them.
185
+ takeaways_suffix: -key-takeaways
186
+ # index.yml sections (comment names) that don't get a takeaways chapter.
187
+ takeaways_skip_sections: [Front matter, Appendix, Conclusion, Back cover]
188
+ # Every chapter must open with this callout class (null to disable;
189
+ # business-nonfiction sets tldr).
190
+ required_callout: null
191
+ # Heading id where Kindle's "Start Reading" lands. Empty = the first file in
192
+ # index.yml that isn't a structural page.
193
+ start_reading: ""
194
+
195
+ # Reserved: no kit command reads these yet (the kit ships no LLM review tools;
196
+ # the Claude Code agents read the book's CLAUDE.md instead). Kept so books
197
+ # that set them, or their own tooling, keep working. Neutral by default.
198
+ llm:
199
+ title: "" # defaults to metadata.title
200
+ voice: the author's voice
201
+ audience: ""
202
+
203
+ # Linting and validation.
204
+ # Paths in lint and validators are relative to the book root, as in index.yml
205
+ # (e.g. src/intro.md).
206
+ lint:
207
+ acronyms: [] # extra acronyms that must be written [ACRONYM]{.smallcaps}
208
+ universal_acronyms: [] # extra acronyms readers know without expansion
209
+ exclude: [] # files the per-chapter checks skip (index.yml, TODO, required callout)
210
+ generated: [] # machine-generated pages the lexical validators skip
211
+ dictionary: dictionary.txt # reserved: no kit command reads this yet
212
+ # `just prose` (book-kit prose) runs textlint with configs/textlintrc.json,
213
+ # whose rules are mechanical (misspellings, doubled spaces, unmatched
214
+ # pairs), deep-merged with this map. Add or tune any installed textlint
215
+ # rule here, e.g. {rules: {max-comma: {max: 4}}}.
216
+ textlint: {}
217
+ # The kit's own textlint rules (rules/), by name: true, false, or an options
218
+ # object (each takes `allow`, a list of findings to skip by message text).
219
+ # Off by default; business-nonfiction turns them on. Rules in a preset's
220
+ # rules/ directory are on unless set to false here.
221
+ rules:
222
+ no-ai-like-patterns: false
223
+ no-assumptive: false
224
+ no-bare-acronyms: false
225
+ no-cliches: false
226
+ no-cultural-idioms: false
227
+ no-vague-intensifiers: false
228
+ # `just lint` (book-kit markdownlint) runs markdownlint-cli2 with
229
+ # configs/markdownlint-cli2.jsonc, whose `config` this map is deep-merged
230
+ # into, e.g. {proper-names: {names: [GitHub]}}.
231
+ markdownlint: {}
232
+ # A book with its own .textlintrc or .markdownlint-cli2.jsonc uses that file
233
+ # instead, and these three keys don't apply to it.
234
+
235
+ validators:
236
+ # error | warn | on | off, keyed by validator name (`book-kit validate
237
+ # --list`). "on" runs a validator at its own severity, for turning one back
238
+ # on after a lower layer turned it off.
239
+ severity:
240
+ # A link to a missing anchor fails EPUBCheck (RSC-012); catch it here
241
+ # first, with the file and line.
242
+ cross-references: error
243
+ # House-style checks: off by default, on in the business-nonfiction
244
+ # preset. They encode one style (section-opener intros, TL;DRs, role
245
+ # callouts, smallcaps acronyms, dash and quote rules, prose habits)
246
+ # rather than correctness.
247
+ section-intros: "off"
248
+ section-intro-no-tldr: "off"
249
+ section-intro-has-lead: "off"
250
+ section-intro-final-lead: "off"
251
+ no-transition-todos: "off"
252
+ min-opener-length: "off"
253
+ chapter-lengths: "off"
254
+ smallcaps: "off"
255
+ escaped-smallcaps: "off"
256
+ callout-balance: "off"
257
+ tldr-length: "off"
258
+ acronym-expansion: "off"
259
+ em-dashes: "off"
260
+ number-range-dashes: "off"
261
+ quote-punctuation: "off"
262
+ sentence-starters: "off"
263
+ list-punctuation: "off"
264
+ bold-keywords: "off"
265
+ hypothetical-hooks: "off"
266
+ paragraph-length: "off"
267
+ # Intentional exceptions, keyed by validator name. Entries are "file:line"
268
+ # (sentence-starters, paragraph-length, hypothetical-hooks) or file paths
269
+ # (callout-balance).
270
+ allow: {}
271
+ # Tunable thresholds, keyed by validator name and merged over the
272
+ # validator's own defaults (maps merge; lists replace). `book-kit validate
273
+ # --list --verbose` shows every validator. The kit's options:
274
+ # min-opener-length min_words: 60
275
+ # tldr-length min_words: 8, max_words: 80
276
+ # chapter-lengths min_words: 500, max_words: 2500
277
+ # paragraph-length max_words: 120
278
+ # sentence-starters threshold: 3 (consecutive sentences, same first word)
279
+ # doubled-words allowlist: [had, that]
280
+ # hypothetical-hooks extra_patterns: [] (regex sources, case-insensitive)
281
+ # callout-balance names: {for-ics: For ICs} (class -> name in warnings)
282
+ # e.g. options: { tldr-length: { max_words: 60 } }
283
+ options: {}
284
+ # Book-specific validators, run after the kit's and any preset's: ES
285
+ # modules exporting { name, tags, defaults, run(ctx) }, like the built-ins in
286
+ # the kit's validators/. Each entry is a book-relative path (./checks/x.js)
287
+ # or an npm package name installed in the book.
288
+ plugins: []
289
+
290
+ # Hooks for book-specific build steps.
291
+ build:
292
+ # The Lua filter chain for each format, in order. An entry is a kit filter
293
+ # name (filters/<name>.lua; a preset's filters/ directory can add or shadow
294
+ # one), a book-relative path ("./filters/mine.lua"), or the literal
295
+ # extra_filters, which stands for that format's extra_filters below. Lists
296
+ # replace wholesale, so a book that sets one restates the whole chain.
297
+ # arc-notice is a no-op unless the ARC recipes pass --metadata arc=true.
298
+ filters:
299
+ epub: [callout-classes, revision, add-div-titles, extra_filters, arc-notice, strip-comments]
300
+ kindle: [callout-classes, revision, add-div-titles, strip-emoji-kindle, extra_filters, arc-notice, strip-comments]
301
+ html: [callout-classes, revision, extra_filters, strip-comments]
302
+ pdf: [callout-classes, revision, add-div-titles, body-emoji-images, extra_filters]
303
+ docx: [callout-classes, revision, add-div-titles, extra_filters]
304
+ # pandoc templates per format: a kit or preset template name (templates/),
305
+ # or a book-relative path. A format without one uses pandoc's default
306
+ # (the PDF build's HTML, and DOCX, which uses reference.docx instead).
307
+ templates:
308
+ epub: template-epub.html
309
+ kindle: template-epub.html
310
+ html: template.html
311
+ extra_filters: # extra Lua filters (book-relative paths), per format; slot in where filters lists extra_filters
312
+ epub: []
313
+ kindle: []
314
+ html: []
315
+ pdf: []
316
+ docx: []
317
+ print_exclude: [] # src-relative files dropped from PDF/Kindle builds
318
+ print_inject: {} # index.yml marker comment -> src-relative file, PDF only
@@ -0,0 +1,19 @@
1
+ # Shared pandoc options for the HTML and PDF builds. Identity metadata (title,
2
+ # author, ISBN, ...) is NOT here: the build generates it from book.config.yml
3
+ # into dist/.book-kit/pandoc-<format>.yml, passed after this file so it wins.
4
+ from: markdown+yaml_metadata_block+smart+auto_identifiers+footnotes+strikeout+implicit_figures+emoji+fenced_divs+inline_code_attributes+raw_attribute-citations
5
+ table-of-contents: true
6
+ toc-depth: 2
7
+ standalone: true
8
+ pdf-engine: weasyprint
9
+ metadata:
10
+ documentclass: book
11
+ css: style.css
12
+ document-css: false
13
+ top-level-division: chapter
14
+ embed-resources: true
15
+ pdf-engine-opts:
16
+ - --base-url=./dist/
17
+ - --pdf-version=1.4
18
+ - --dpi=300
19
+ - --optimize-images
@@ -0,0 +1,6 @@
1
+ # DOCX for editorial workflows (track changes with an editor).
2
+ from: markdown+yaml_metadata_block+smart+auto_identifiers+footnotes+strikeout+implicit_figures+emoji+fenced_divs+inline_code_attributes+raw_attribute-citations
3
+ table-of-contents: true
4
+ toc-depth: 2
5
+ standalone: true
6
+ top-level-division: chapter
@@ -0,0 +1,6 @@
1
+ # Font embedding for standard EPUB (Apple Books, Kobo, etc.): variable WOFF2
2
+ # with the full weight range and optical sizing. Split from pandoc-epub.yml
3
+ # because Kindle needs static fonts and pandoc merges (not replaces)
4
+ # epub-fonts across defaults files.
5
+ epub-fonts:
6
+ - "./fonts/*.woff2"
@@ -0,0 +1,15 @@
1
+ # EPUB 3 options (KDP, Apple Books, Kobo, D2D, IngramSpark). Identity metadata
2
+ # comes from the generated dist/.book-kit/pandoc-epub.yml.
3
+ from: markdown+yaml_metadata_block+smart+auto_identifiers+footnotes+strikeout+implicit_figures+emoji+fenced_divs+inline_code_attributes+raw_attribute-citations
4
+ table-of-contents: true
5
+ toc-depth: 2
6
+ standalone: true
7
+ epub-chapter-level: 1
8
+ metadata:
9
+ documentclass: book
10
+ css: style-epub.css
11
+ document-css: false
12
+ ibooks:
13
+ version: '1.0'
14
+ top-level-division: chapter
15
+ embed-resources: true
@@ -0,0 +1,6 @@
1
+ # Kindle overrides, passed after pandoc-epub.yml. Kindle ignores variable
2
+ # WOFF2, so it gets static fonts and the Kindle stylesheet.
3
+ metadata:
4
+ css: style-kindle.css
5
+ epub-fonts:
6
+ - "./fonts/kindle/*.woff2"
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/emoji/2714.png ADDED
Binary file
package/emoji/274c.png ADDED
Binary file
@@ -0,0 +1,196 @@
1
+ -- Lua filter that injects callout labels and ARIA roles.
2
+ --
3
+ -- Callout prefixes ("TL;DR:", "💡 Pro-Tip:", ...) are drawn by CSS ::before in
4
+ -- HTML, but EPUB readers and DOCX don't support pseudo-elements, so the build
5
+ -- adds them to the content here. For PDF, emoji text in CSS content: crashes
6
+ -- WeasyPrint (Pango ZeroDivisionError), so Kindle and PDF get Twemoji <img>s.
7
+ --
8
+ -- The callouts themselves come from the `callouts` registry in book.config.yml
9
+ -- (see the kit's defaults/book.config.yml), read from the JSON the build writes
10
+ -- to $BOOK_KIT_CONFIG.
11
+ --
12
+ -- Metadata flags:
13
+ -- --metadata kindle=true → Kindle Twemoji images (emoji.kindle_dir)
14
+ -- --metadata pdf=true → PDF Twemoji images (emoji.pdf_callout_dir)
15
+ -- Translated builds can override labels with `callout-labels` / `callout-aria`
16
+ -- metadata maps keyed by callout class.
17
+
18
+ package.path = pandoc.path.directory(PANDOC_SCRIPT_FILE) .. "/lib/?.lua;" .. package.path
19
+ local book_kit = require("book_kit")
20
+
21
+ local config = book_kit.config()
22
+ local callouts = config.callouts or {}
23
+ local emoji_dirs = config.emoji or {}
24
+ local output_mode = "epub" -- default
25
+
26
+ -- First pass: read metadata to detect output mode + pick up localized labels.
27
+ local function readMeta(meta)
28
+ if meta.kindle then
29
+ output_mode = "kindle"
30
+ elseif meta.pdf then
31
+ output_mode = "pdf"
32
+ end
33
+
34
+ local labels, aria = meta['callout-labels'], meta['callout-aria']
35
+ for _, c in ipairs(callouts) do
36
+ if labels and labels[c.class] then c.label = pandoc.utils.stringify(labels[c.class]) end
37
+ if aria and aria[c.class] then c.aria = pandoc.utils.stringify(aria[c.class]) end
38
+ end
39
+ end
40
+
41
+ -- Helper function to check if content starts with a specific text
42
+ local function startsWithText(content, text)
43
+ if #content == 0 then return false end
44
+
45
+ -- Stringify the first element to handle any inline type
46
+ -- (Str, Strong, SmallCaps, Emph, etc.)
47
+ local first_str = pandoc.utils.stringify(content[1])
48
+ -- Use literal prefix comparison (not Lua patterns) to avoid
49
+ -- metacharacter issues with labels like "Pro-Tip:"
50
+ return first_str:sub(1, #text) == text
51
+ end
52
+
53
+ -- The symbol for a callout in the current output mode: emoji text for EPUB,
54
+ -- a Twemoji image spec for Kindle/PDF, or nil when the callout has none.
55
+ local function symbolFor(c)
56
+ if not c.emoji then return nil end
57
+ if output_mode == "epub" then return c.emoji end
58
+ local dir = output_mode == "kindle" and emoji_dirs.kindle_dir or emoji_dirs.pdf_callout_dir
59
+ return {cp = c.emoji_cp, alt = c.emoji_alt, dir = dir}
60
+ end
61
+
62
+ -- Helper: prepend a symbol + label to a div's first paragraph
63
+ local function prependCalloutLabel(div, symbol_char, label_text)
64
+ -- Uses callout-label class instead of SmallCaps because e-readers
65
+ -- synthesize small caps poorly (crude all-caps), while PDF uses
66
+ -- real OpenType small caps via CSS ::before. The callout-label class
67
+ -- provides a clean sans-serif label that approximates the PDF look.
68
+ local label = pandoc.Strong({pandoc.Span({pandoc.Str(label_text)}, pandoc.Attr("", {"callout-label"}, {}))})
69
+ local space = pandoc.Space()
70
+
71
+ local prefix
72
+ if type(symbol_char) == "table" then
73
+ -- Kindle/PDF: inline Twemoji image + label on same line
74
+ local img_size = output_mode == "kindle" and "24" or "16"
75
+ local img = pandoc.Image(
76
+ {pandoc.Str(symbol_char.alt)},
77
+ symbol_char.dir .. symbol_char.cp .. ".png",
78
+ "",
79
+ pandoc.Attr("", {"emoji", "callout-emoji"}, {{"width", img_size}, {"height", img_size}})
80
+ )
81
+ prefix = {img, pandoc.Space(), label, space}
82
+ else
83
+ -- EPUB: inline emoji (if any) + label on same line
84
+ prefix = {}
85
+ if symbol_char then
86
+ table.insert(prefix, pandoc.Str(symbol_char))
87
+ table.insert(prefix, pandoc.Space())
88
+ end
89
+ table.insert(prefix, label)
90
+ table.insert(prefix, space)
91
+ end
92
+
93
+ if #div.content > 0 and div.content[1].t == "Para" then
94
+ for i = #prefix, 1, -1 do
95
+ table.insert(div.content[1].content, 1, prefix[i])
96
+ end
97
+ elseif #div.content > 0 then
98
+ table.insert(div.content, 1, pandoc.Para(prefix))
99
+ end
100
+ end
101
+
102
+ -- Second pass: process Div elements
103
+ local function processDiv(div)
104
+ -- Labels: every matching callout gets its prefix, unless the writer
105
+ -- already typed it (or its emoji).
106
+ for _, c in ipairs(callouts) do
107
+ if c.label and div.classes:includes(c.class) then
108
+ local already_has_prefix = false
109
+ if #div.content > 0 and div.content[1].t == "Para" then
110
+ local first = div.content[1].content
111
+ already_has_prefix = (c.emoji and startsWithText(first, c.emoji)) or
112
+ startsWithText(first, c.label)
113
+ end
114
+ if not already_has_prefix then
115
+ prependCalloutLabel(div, symbolFor(c), c.label)
116
+ end
117
+ end
118
+ end
119
+
120
+ -- ARIA: the first matching callout in registry order wins.
121
+ -- Use doc-notice (DPUB-ARIA) for all callout types; doc-tip is not a valid EPUB3 role
122
+ for _, c in ipairs(callouts) do
123
+ if div.classes:includes(c.class) then
124
+ div.attributes["role"] = "doc-notice"
125
+ if c.aria then div.attributes["aria-label"] = c.aria end
126
+ break
127
+ end
128
+ end
129
+
130
+ -- Add DPUB-ARIA roles for special structural sections
131
+ if div.classes:includes("dedication") then
132
+ div.attributes["role"] = "doc-dedication"
133
+ end
134
+ if div.identifier == "copyright-page" then
135
+ div.attributes["role"] = "doc-colophon"
136
+ end
137
+
138
+ return div
139
+ end
140
+
141
+ -- Strip role="doc-*" and epub:type from headings. Pandoc's EPUB writer
142
+ -- sometimes copies epub:type onto headings and synthesizes a matching
143
+ -- role attribute (e.g., <h1 epub:type="prologue" role="doc-prologue">),
144
+ -- but EPUBCheck's HTML role validation only allows "doc-subtitle",
145
+ -- "heading", "none", "presentation", or "tab" on heading elements, and
146
+ -- ACE requires any epub:type to have a matching role. Removing both from
147
+ -- the heading resolves the conflict: pandoc already emits identical
148
+ -- epub:type and role on the containing <section>, which is the correct
149
+ -- location per DPUB-ARIA and which both validators accept.
150
+ local function processHeader(h)
151
+ local role = h.attributes["role"]
152
+ if role and role:match("^doc%-") and role ~= "doc-subtitle" then
153
+ h.attributes["role"] = nil
154
+ h.attributes["epub:type"] = nil
155
+ end
156
+ return h
157
+ end
158
+
159
+ -- Insert zero-width spaces (U+200B) inside URL-like substrings so long URLs
160
+ -- in PDF footnotes can wrap mid-URL instead of being pushed onto their own
161
+ -- line. Triggered by links-to-footnotes.js rewriting `[text](url)` to
162
+ -- `text (url)` — the URL becomes one unbreakable Str token, and even with
163
+ -- `overflow-wrap: anywhere` WeasyPrint won't break it when there's a usable
164
+ -- space before the opening paren. ZWSPs after path separators give the line
165
+ -- breaker explicit opportunities to use only when they improve the line.
166
+ local ZWSP = "\226\128\139"
167
+ local function insertUrlBreakpoints(text)
168
+ return (text:gsub("https?://[^%)%s]+", function(url)
169
+ return (url:gsub("([/%?&=])", "%1" .. ZWSP))
170
+ end))
171
+ end
172
+
173
+ -- For PDF: convert pandoc Note elements (endnotes) to inline Span elements
174
+ -- with class "fn-inline" so CSS float:footnote renders them at page bottom.
175
+ -- This is a no-op for EPUB/Kindle/screen (only active when pdf=true).
176
+ local function convertNote(note)
177
+ if output_mode ~= "pdf" then return nil end
178
+ local inlines = pandoc.List:new()
179
+ for _, block in ipairs(note.content) do
180
+ if block.t == "Para" or block.t == "Plain" then
181
+ if #inlines > 0 then
182
+ inlines:insert(pandoc.Space())
183
+ end
184
+ inlines:extend(block.content)
185
+ end
186
+ end
187
+ inlines = inlines:map(function(inline)
188
+ if inline.t == "Str" then
189
+ return pandoc.Str(insertUrlBreakpoints(inline.text))
190
+ end
191
+ return inline
192
+ end)
193
+ return pandoc.Span(inlines, pandoc.Attr("", {"fn-inline"}, {}))
194
+ end
195
+
196
+ return {{Meta = readMeta}, {Div = processDiv, Header = processHeader, Note = convertNote}}