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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ben Balter
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/NOTICE.md ADDED
@@ -0,0 +1,72 @@
1
+ # Third-party notices
2
+
3
+ book-kit is licensed under the [MIT License](LICENSE). It bundles the
4
+ following third-party assets, whose own licenses apply to those files rather
5
+ than the MIT License.
6
+
7
+ ## Fonts
8
+
9
+ The theme uses Adobe's Source Serif 4, Source Sans 3, and Source Code Pro,
10
+ licensed under the [SIL Open Font License 1.1](https://openfontlicense.org)
11
+ with the Reserved Font Name "Source". The license and copyright notices are in:
12
+
13
+ - [`fonts/LICENSE-source-serif.md`](fonts/LICENSE-source-serif.md)
14
+ - [`fonts/LICENSE-source-sans.md`](fonts/LICENSE-source-sans.md)
15
+ - [`fonts/LICENSE-source-code-pro.md`](fonts/LICENSE-source-code-pro.md)
16
+
17
+ The kit doesn't redistribute the fonts. The OFL bars redistributing modified
18
+ versions under a Reserved Font Name, and the theme needs Latin subsets and
19
+ static Kindle instances. So `book-kit fonts` downloads the unmodified releases
20
+ pinned in `toolchain.json` (verified by SHA-256) and builds those files on your
21
+ machine from [`fonts/recipe.json`](fonts/recipe.json); embedding them in your
22
+ book is permitted.
23
+
24
+ ## Emoji (`emoji/`)
25
+
26
+ [Twemoji](https://github.com/jdecked/twemoji) graphics, © Twitter, Inc. and
27
+ other contributors, licensed under
28
+ [CC-BY 4.0](https://creativecommons.org/licenses/by/4.0/). Books built with
29
+ these images should credit Twemoji (for example, on the copyright page).
30
+
31
+ ## Pandoc templates (`templates/`)
32
+
33
+ `templates/template.html` and `templates/template-epub.html` are adapted from
34
+ [pandoc](https://pandoc.org)'s default HTML and EPUB templates. Copyright (c)
35
+ 2014--2024, John MacFarlane. Pandoc's templates are dual-licensed under the
36
+ GPL (v2 or higher) or, at your option, the BSD 3-clause license; book-kit uses
37
+ them under the BSD 3-clause license:
38
+
39
+ > Redistribution and use in source and binary forms, with or without
40
+ > modification, are permitted provided that the following conditions are met:
41
+ > (1) Redistributions of source code must retain the above copyright notice,
42
+ > this list of conditions and the following disclaimer. (2) Redistributions in
43
+ > binary form must reproduce the above copyright notice, this list of
44
+ > conditions and the following disclaimer in the documentation and/or other
45
+ > materials provided with the distribution. (3) Neither the name of the
46
+ > copyright holder nor the names of its contributors may be used to endorse or
47
+ > promote products derived from this software without specific prior written
48
+ > permission.
49
+ >
50
+ > THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
51
+ > AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
52
+ > IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
53
+ > DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
54
+ > FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
55
+ > DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
56
+ > SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
57
+ > CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
58
+ > OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
59
+ > OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
60
+
61
+ ## PDF/X output intent (`profiles/pdfx-def.ps`)
62
+
63
+ The pdfmark sequence that attaches the PDF/X output intent follows the
64
+ example `lib/PDFX_def.ps` shipped with [Ghostscript](https://ghostscript.com)
65
+ (Artifex Software, Inc.). Ghostscript itself isn't bundled; the kit runs it
66
+ as an external tool.
67
+
68
+ ## Not bundled
69
+
70
+ The USWebCoatedSWOP ICC profile used for PDF/X-1a conversion is downloaded
71
+ on demand and verified by SHA-256 (see `commands/download-icc-profile.sh`);
72
+ it isn't redistributed here.
package/README.md CHANGED
@@ -1,3 +1,252 @@
1
- # Temporary Holding Version
1
+ # book-kit
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Build and quality tooling for books written in Markdown. One source, every format a self-published book needs:
4
+
5
+ - **EPUB 3** for Apple Books, Kobo, Google Play, Draft2Digital, and IngramSpark, with EPUB accessibility metadata, DPUB-ARIA roles, and a clean EPUBCheck run against two rule sets
6
+ - **Kindle EPUB** with static fonts, e-ink-safe emoji, and links turned into footnotes
7
+ - **Print PDF** (6×9 trade paperback by default), rendered by WeasyPrint, with a lockable page count so a toolchain bump can't silently invalidate a cover spine
8
+ - **PDF/X-1a:2001** (CMYK, no transparency) for print-on-demand services
9
+ - **HTML** and **DOCX**
10
+
11
+ It's [pandoc](https://pandoc.org), [WeasyPrint](https://weasyprint.org), and [Tailwind CSS](https://tailwindcss.com) wired together with [just](https://just.systems), plus the fixes that make each retailer happy.
12
+
13
+ > [!NOTE]
14
+ > **Early alpha.** book-kit was extracted from the build for [*Open and Async*](https://open-and-async.com), and it still builds that book's published editions. The extraction isn't finished: some of the book's conventions are still hard-coded in the kit rather than set in config (tracked in [#12](https://github.com/benbalter/book-kit/issues/12)), and config keys and defaults may change between 0.x releases.
15
+
16
+ ## Quick start
17
+
18
+ A book is a directory with a `book.config.yml`, an `index.yml` listing chapters in order, and Markdown in `src/`. [`example/`](example/) is a complete one.
19
+
20
+ ```sh
21
+ npm install --save-dev book-kit
22
+ ```
23
+
24
+ `justfile`:
25
+
26
+ ```just
27
+ set allow-duplicate-recipes
28
+ import 'node_modules/book-kit/just/book-kit.just'
29
+ ```
30
+
31
+ `src/style.css`:
32
+
33
+ ```css
34
+ @import "tailwindcss";
35
+ @plugin "@tailwindcss/typography";
36
+ @import "book-kit/css/theme.css";
37
+ @source "../node_modules/book-kit/templates";
38
+ @source "../node_modules/book-kit/filters";
39
+ ```
40
+
41
+ `theme.css` imports the theme's sections from `css/theme/` (tokens, fonts, base, callouts, e-reader, dark mode, print, and so on). To leave one out, import the others yourself in the same order instead, for example everything but `09-back-matter.css`.
42
+
43
+ Then `just all` builds every format into `dist/`. `just --list` shows each recipe; the ones beyond the format builds:
44
+
45
+ | Recipe | What it does |
46
+ | --- | --- |
47
+ | `just arc` | Advance reader copy EPUBs (see [below](#advance-reader-copies)) |
48
+ | `just check-all` | Validators, Markdown lint, and prose lint: what a pull request should pass |
49
+ | `just check` | Checks on built output: the print page-count lock and EPUB size budgets |
50
+ | `just epubcheck` | EPUBCheck on the built EPUBs |
51
+ | `just reference-docx` | Regenerate `reference.docx`, the heading and body styles for the DOCX build |
52
+ | `just key-takeaways` | Regenerate each section's key takeaways chapter from its chapters' TL;DRs |
53
+ | `just optimize-images` | Losslessly recompress PNGs under `paths.img` |
54
+ | `just revision` | Print the build stamp (`<version> (<sha>)`) |
55
+ | `just release-files [tag]` | Copy the built formats to version-named files with checksums (see [below](#release-files)) |
56
+ | `just clean` | Remove build output |
57
+
58
+ ### Requirements
59
+
60
+ The exact versions live in [`toolchain.json`](toolchain.json). pandoc and WeasyPrint versions affect pagination, so they're pinned.
61
+
62
+ - [pandoc](https://pandoc.org/installing.html), [just](https://just.systems), Node.js
63
+ - Python packages from [`requirements.txt`](requirements.txt) (WeasyPrint, Pillow, pikepdf, python-docx, fontTools)
64
+ - Network access on the first build: `book-kit fonts` downloads the pinned Source font releases and builds the theme's subsets into `~/.cache/book-kit` (set `BOOK_KIT_CACHE` to move it)
65
+ - Ghostscript, for PDF/X
66
+ - Java and [EPUBCheck](https://www.w3.org/publishing/epubcheck/), for `just epubcheck`
67
+
68
+ Or use the Docker image, which has everything except Java and EPUBCheck: see [`docker/Dockerfile`](docker/Dockerfile). Run EPUBCheck outside it, or in CI with the [`epubcheck`](actions/epubcheck/action.yml) action.
69
+
70
+ ## Configuration
71
+
72
+ Everything is in `book.config.yml`. [`defaults/book.config.yml`](defaults/book.config.yml) documents every key and its default; a book only sets what differs.
73
+
74
+ ### Presets
75
+
76
+ A preset is a shared house style: a directory with a `preset.yml` whose keys sit between the kit defaults and the book's own. Name one or more in `extends`:
77
+
78
+ ```yaml
79
+ extends: business-nonfiction # or a list; later presets win
80
+ ```
81
+
82
+ An entry can be a kit preset (in [`presets/`](presets/)), a path relative to the file naming it (`./style`), or an npm package with a `preset.yml` at its root. Presets can extend other presets. The book's keys always win. Lists replace a preset's lists wholesale, as they do the defaults.
83
+
84
+ The kit ships [`business-nonfiction`](presets/business-nonfiction/preset.yml): six callouts (`tldr`, `pro-tip`, `for-managers`, `for-ics`, `common-objection`, `takeaways`), a TL;DR that opens every chapter, section takeaways chapters, and the house-style validators (section openers, smallcaps acronyms, dash and quote rules, prose habits).
85
+
86
+ **Upgrading from 0.3:** through 0.3, that preset *was* the kit default. In 0.4 the defaults are neutral, so a 0.3 book keeps its exact configuration by adding one line, `extends: business-nonfiction`. [`tests/presets.test.js`](tests/presets.test.js) checks that the two resolve identically.
87
+
88
+ `book-kit config --sources [key]` shows which layer (kit defaults, a preset, or `book.config.yml`) set each key.
89
+
90
+ ### Callouts
91
+
92
+ Fenced divs become labeled callouts in every format. The defaults have three, `note`, `tip`, and `warning`, styled by the theme (the business-nonfiction preset swaps in its six):
93
+
94
+ ```markdown
95
+ ::: {.tip custom-style="tip"}
96
+ Keep callouts short.
97
+ :::
98
+ ```
99
+
100
+ The build adds the label ("Tip:"), the ARIA role, and, for callouts with an `emoji`, per-format icons (emoji text in EPUB, Twemoji images on Kindle and in print). Change the set with the `callouts` list. A class you add gets the theme's note styling and its label in every format with no CSS; give it its own look in the book's stylesheet, or set `generic: false` on the entry to start from nothing.
101
+
102
+ ### Validation
103
+
104
+ `just check-all` runs three checks:
105
+ - `book-kit validate` checks structure: cross-references, `index.yml` against the files on disk, footnotes, heading levels, balanced callout divs, duplicate headings, and leftover TODOs. The business-nonfiction preset adds its house-style checks (required TL;DRs, section openers, smallcaps acronyms, dashes and quotes, paragraph length, and more).
106
+ - `just lint` runs markdownlint.
107
+ - `just prose` runs textlint with mechanical rules (misspellings, doubled spaces, unmatched pairs). The business-nonfiction preset adds the kit's rules for clichés, buzzwords, vague intensifiers, cultural idioms, assumptive phrasing, AI-sounding patterns, and bare acronyms, along with terminology, sentence-length, and comma limits. On GitHub Actions, each finding is reported as an annotation on its file and line.
108
+
109
+ Both lint configs layer like `book.config.yml`. The kit's base configs in [`configs/`](configs/) are merged with `lint.textlint` and `lint.markdownlint` from presets and the book. `lint.rules` switches the kit's own textlint rules on, off, or to an options object, such as `no-cliches: { allow: [Leverage] }`. A preset can ship its own textlint rules in a `rules/` directory. A book with its own `.textlintrc` or `.markdownlint-cli2.jsonc` uses that file as is.
110
+
111
+ Tune severity (`error`, `warn`, `on`, or `off`, per validator), thresholds (`validators.options.<name>`, e.g. `tldr-length: { max_words: 60 }`), or intentional exceptions under `validators` in `book.config.yml`; [`defaults/book.config.yml`](defaults/book.config.yml) lists the options.
112
+
113
+ Every validator is an ES module with the same shape, whether it's one of the kit's built-ins in [`validators/`](validators/), ships in a preset's `validators/` directory (every `*.js` there runs for books that extend the preset), or is a book's own plugin:
114
+
115
+ ```js
116
+ // checks/house-style.js, listed under validators.plugins
117
+ export default {
118
+ name: 'house-style',
119
+ tags: ['style'],
120
+ defaults: { max_exclamations: 3 },
121
+ run({ config, indexFiles, chapters, options, allow }) {
122
+ return { errors: [], warnings: [] };
123
+ },
124
+ };
125
+ ```
126
+
127
+ `options` is `defaults` merged with `validators.options.house-style`, and `allow` is the set from `validators.allow.house-style`. A `validators.plugins` entry is a path relative to the book or the name of an npm package installed in the book. Built-ins run first, then preset validators, then plugins.
128
+
129
+ `book-kit validate --list` prints the built-in validator names; `--list --verbose` shows every validator with its severity, tags, and where it comes from.
130
+
131
+ ### Overriding a step
132
+
133
+ Define a recipe with the same name in the book's `justfile` and it replaces the kit's.
134
+
135
+ Book-specific Lua filters go in `build.extra_filters`, which slots them into each format's chain. To change the chain itself, set `build.filters.<format>`. It's an ordered list of kit filter names, book-relative paths (`./filters/mine.lua`), and the literal `extra_filters`. `build.templates.<format>` picks the pandoc template the same way. A preset can ship `filters/` and `templates/` directories, and its files are found by bare name before the kit's.
136
+
137
+ Text the build adds to the book lives under `strings` (the ARC notice, the EPUB cover's screen-reader label, the table-of-contents entry for the copyright page), so a book in another language can translate it. Translated accessibility summaries live under `accessibility.summary_translations`.
138
+
139
+ For one-off pandoc options on the `epub`, `kindle`, and `html` builds, set `BOOK_KIT_PANDOC_ARGS` (e.g. `BOOK_KIT_PANDOC_ARGS='--toc-depth=3' just epub`); the recipes take no arguments, so `just epub kindle html` runs all three.
140
+
141
+ ## Continuous integration
142
+
143
+ ```yaml
144
+ jobs:
145
+ build:
146
+ uses: benbalter/book-kit/.github/workflows/build.yml@v0.6
147
+ permissions:
148
+ contents: read
149
+ ```
150
+
151
+ Builds every format, runs EPUBCheck 4.x and 5.x, validates the PDF/X, and uploads `dist/`.
152
+
153
+ ### Build parity
154
+
155
+ Some changes must not change the book at all: a book-kit upgrade, a pandoc or WeasyPrint bump, a build refactor. `parity.yml` builds the pull request's base and head on the same runner and fails on any difference in any format. Each side is built with the pandoc and WeasyPrint its own kit version pins in `toolchain.json`, so a toolchain bump that reflows the book shows up as a difference:
156
+
157
+ ```yaml
158
+ on:
159
+ pull_request:
160
+ paths: [package.json, package-lock.json, justfile, book.config.yml]
161
+ jobs:
162
+ parity:
163
+ uses: benbalter/book-kit/.github/workflows/parity.yml@v0.6
164
+ permissions:
165
+ contents: read
166
+ packages: read
167
+ ```
168
+
169
+ Underneath it's `book-kit parity BEFORE_DIST AFTER_DIST`. That compares EPUBs and DOCX member by member, the HTML byte for byte, and PDFs by page count, text, fonts, and every page's rendering. It ignores only build timestamps, PNGs re-encoded with identical pixels, and Ghostscript's random font-subset names. The head build must also leave the working tree clean.
170
+
171
+ Builds take their timestamps from the book's last commit (`SOURCE_DATE_EPOCH`, unless you set it yourself), so rebuilding a commit gives byte-identical EPUBs, DOCX, PDF, and HTML. The PDF/X is the exception: Ghostscript writes its own IDs and dates, which is why parity compares PDFs by rendering rather than bytes.
172
+
173
+ ### Release files
174
+
175
+ `just release-files v1.2.0` copies the built formats into `dist/release/` under names that carry the version, `<basename>-v1.2.0.epub`, `<basename>-kindle-v1.2.0.epub`, and so on, with a `sha256sum`-compatible `checksums.txt`. A downloaded file then says which release it came from, and `dist/` keeps its plain names for local builds. The version comes from `paths.version`, the same file the build stamps on the copyright page, and a tag that disagrees with it fails, so a file can't be named for one version and stamped with another. Attach `dist/release/*` to your release.
176
+
177
+ ## Advance reader copies
178
+
179
+ `just arc` builds `<basename>-arc.epub` and `<basename>-kindle-arc.epub`: the retail EPUBs with a "not for redistribution" notice on the copyright page.
180
+
181
+ ## Claude Code
182
+
183
+ The repo is also a [Claude Code plugin marketplace](https://code.claude.com/docs/en/plugins/create-marketplace.md). Its `book-kit` plugin ([`claude-plugin/`](claude-plugin/)) adds:
184
+
185
+ - **`book-kit:writing`**, an agent that reworks short passages the author wrote, such as a TL;DR, a section lead, or an opening or closing paragraph, for the author to review. It's built to assist an author, not write for them, so it declines to write chapters, sections, or other new prose
186
+ - **`book-kit:editor`**, an agent that copy edits, proofreads, and runs the validators
187
+ - **A chapter hook**: after Claude edits a chapter, it runs markdownlint and `book-kit validate` and sends errors in that chapter back to Claude to fix. textlint findings come back as advice, since whether they fail depends on the book's phase. It runs the book's installed kit, so checks match the version the book has locked.
188
+
189
+ The agents are deliberately voice-neutral, and they don't assume a house style.
190
+ - They read the book's `CLAUDE.md` for its voice, audience, style guide, and phase (drafting, revising, or content-locked), so put those there.
191
+ - They run `book-kit conventions` for its callouts, the structure its validators expect, the checks that run, and each preset's `CONVENTIONS.md`.
192
+
193
+ A preset that wants the agents to follow its house style ships a `CONVENTIONS.md`.
194
+
195
+ To enable it for everyone working on a book, add this to the book's `.claude/settings.json`:
196
+
197
+ ```json
198
+ {
199
+ "extraKnownMarketplaces": {
200
+ "book-kit": { "source": { "source": "github", "repo": "benbalter/book-kit" }, "autoUpdate": true }
201
+ },
202
+ "enabledPlugins": { "book-kit@book-kit": true }
203
+ }
204
+ ```
205
+
206
+ Or install it just for yourself:
207
+
208
+ ```text
209
+ /plugin install book-kit --marketplace benbalter/book-kit
210
+ ```
211
+
212
+ Check changes with `claude plugin validate ./claude-plugin` and `claude plugin validate .`.
213
+
214
+ ## Commands
215
+
216
+ `book-kit <command>` runs the kit's tools from anywhere inside a book. `book-kit help` lists them; most are build steps the recipes call. The ones worth running directly:
217
+
218
+ - `book-kit config <key>` prints a resolved setting, e.g. `book-kit config paths.dist`; `--sources` shows which layer set each key.
219
+ - `book-kit validate` runs the chapter validators; `--list` prints their names.
220
+ - `book-kit prose` and `book-kit markdownlint` run textlint and markdownlint with the book's resolved configs (what `just prose` and `just lint` call).
221
+ - `book-kit conventions` prints the book's writing conventions (callouts, structure, checks, preset house style).
222
+ - `book-kit audit-chapter-lengths` flags chapters much longer or shorter than the rest.
223
+ - `book-kit parity BEFORE_DIST AFTER_DIST` compares two builds (see [Build parity](#build-parity)).
224
+
225
+ ## Upgrading from 0.5
226
+
227
+ 0.6 moved from GitHub Packages (`@benbalter/book-kit`) to npm as `book-kit`. Run `npm uninstall @benbalter/book-kit && npm install --save-dev book-kit`, drop the `@benbalter:registry` lines from `.npmrc`, change `@benbalter/book-kit` to `book-kit` in your `justfile` import, `src/style.css`, and any scripts, and move your workflow refs to `@v0.6`. The reusable workflows no longer need `packages: read`, except `parity.yml` on the pull request that makes this move, since its base still installs the old package.
228
+
229
+ ## Versioning
230
+
231
+ book-kit follows [semver](https://semver.org). Before 1.0, a minor release (0.4 → 0.5) may break books, which is why books follow a floating `v0.MINOR` tag; patch releases don't. These are the public API:
232
+
233
+ - The `book.config.yml` keys documented in [`defaults/book.config.yml`](defaults/book.config.yml), and what their defaults do.
234
+ - The `book-kit` commands listed above and their options, and the `just` recipe names in [`just/book-kit.just`](just/book-kit.just).
235
+ - The interfaces for extending the kit: presets (`preset.yml`, `validators/`, `rules/`, `filters/`, `templates/`, `CONVENTIONS.md`), validator plugins (`{ name, tags, defaults, run(ctx) }`), and textlint rule options.
236
+ - The callout classes and the CSS custom properties the theme defines.
237
+ - The reusable workflows and composite actions.
238
+ - `loadConfig()` from `book-kit/lib/config.js`, for a book's own scripts that read `book.config.yml`.
239
+
240
+ Everything else can change in any release: other modules under `lib/`, the file layout under `commands/` and `filters/`, the exact CSS rules, and generated files under `dist/.book-kit/`. If a book depends on one of those, open an issue so it can become public API.
241
+
242
+ ## Contributing
243
+
244
+ See [CONTRIBUTING.md](CONTRIBUTING.md).
245
+
246
+ ## Third-party assets
247
+
248
+ See [NOTICE.md](NOTICE.md) for the fonts (built from upstream, not bundled) and the bundled emoji.
249
+
250
+ ## License
251
+
252
+ [MIT](LICENSE). Bundled third-party assets keep their own licenses; see [NOTICE.md](NOTICE.md).
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env node
2
+ // book-kit: build and quality tooling for Markdown books.
3
+ //
4
+ // book-kit config [key] print the resolved config (or one dotted key)
5
+ // book-kit config --sources [key] show which layer (kit, preset, book) set each key
6
+ // book-kit prepare <format> write dist/.book-kit/{config.json,pandoc-<format>.yml}
7
+ // book-kit revision print the build stamp, "<version> (<sha>)"
8
+ // book-kit kit-path [sub] print the kit's install directory (for justfiles)
9
+ // book-kit <command> [args] run a kit command from commands/ (see `book-kit help`)
10
+ //
11
+ // Commands run from the book root (the nearest book.config.yml) with BOOK_ROOT
12
+ // and BOOK_KIT_ROOT set, so they work from any subdirectory. Relative path
13
+ // arguments resolve against the book root, not the caller's directory.
14
+
15
+ import { spawnSync } from 'node:child_process';
16
+ import { existsSync, readdirSync } from 'node:fs';
17
+ import { extname } from 'node:path';
18
+ import { configSources, findBookRoot, get, kitPath, loadConfig, writeConfigJson } from '../lib/config.js';
19
+ import { revision } from '../lib/revision.js';
20
+ import { writePandocDefaults } from '../lib/pandoc.js';
21
+
22
+ const RUNNERS = { '.js': 'node', '.mjs': 'node', '.py': 'python3', '.sh': 'bash' };
23
+
24
+ function commands() {
25
+ return readdirSync(kitPath('commands'))
26
+ // Files starting with "_" are helpers the commands import, not commands.
27
+ .filter((f) => RUNNERS[extname(f)] && !f.startsWith('_'))
28
+ .map((f) => f.slice(0, -extname(f).length))
29
+ .sort();
30
+ }
31
+
32
+ function findCommand(name) {
33
+ if (name.startsWith('_')) return null;
34
+ for (const ext of Object.keys(RUNNERS)) {
35
+ const file = kitPath('commands', `${name}${ext}`);
36
+ if (existsSync(file)) return { file, runner: RUNNERS[ext] };
37
+ }
38
+ return null;
39
+ }
40
+
41
+ function print(value) {
42
+ console.log(typeof value === 'string' ? value : JSON.stringify(value, null, 2));
43
+ }
44
+
45
+ // A usage or config mistake, reported as one line rather than a stack trace.
46
+ class UsageError extends Error {}
47
+
48
+ const [name, ...args] = process.argv.slice(2);
49
+
50
+ function run() {
51
+ switch (name) {
52
+ case undefined:
53
+ case 'help':
54
+ case '--help':
55
+ console.log('Usage: book-kit <command> [args]\n\nBuilt-ins: config, prepare, revision, kit-path\nCommands: ' + commands().join(', '));
56
+ break;
57
+ case 'config': {
58
+ if (args[0] === '--sources') {
59
+ // Which layer (kit defaults, a preset, the book) set each key.
60
+ const prefix = args[1];
61
+ const entries = Object.entries(configSources()).filter(
62
+ ([key]) => !prefix || key === prefix || key.startsWith(`${prefix}.`),
63
+ );
64
+ if (prefix && entries.length === 0) throw new UsageError(`unknown config key "${prefix}"`);
65
+ const width = Math.max(...entries.map(([key]) => key.length));
66
+ for (const [key, source] of entries) console.log(`${key.padEnd(width)} ${source}`);
67
+ break;
68
+ }
69
+ const value = args[0] ? get(loadConfig(), args[0]) : loadConfig();
70
+ if (value === undefined) throw new UsageError(`unknown config key "${args[0]}"`);
71
+ print(value);
72
+ break;
73
+ }
74
+ case 'prepare': {
75
+ if (!args[0]) throw new UsageError('usage: book-kit prepare <html|pdf|epub|kindle|docx>');
76
+ const config = loadConfig();
77
+ writeConfigJson(config);
78
+ console.log(writePandocDefaults(args[0], config));
79
+ break;
80
+ }
81
+ case 'revision':
82
+ print(revision());
83
+ break;
84
+ case 'kit-path':
85
+ print(kitPath(...args));
86
+ break;
87
+ default: {
88
+ const cmd = findCommand(name);
89
+ if (!cmd) {
90
+ console.error(`book-kit: unknown command "${name}". Run \`book-kit help\` for the list.`);
91
+ process.exit(2);
92
+ }
93
+ // Commands run from the book root, so their relative defaults (dist/...) resolve.
94
+ const root = findBookRoot();
95
+ const env = { ...process.env, BOOK_ROOT: root, BOOK_KIT_ROOT: kitPath() };
96
+ const result = spawnSync(cmd.runner, [cmd.file, ...args], { stdio: 'inherit', env, cwd: root });
97
+ if (result.error?.code === 'ENOENT') throw new UsageError(`${cmd.runner} not found (needed by ${name})`);
98
+ if (result.error) throw result.error;
99
+ // Pass a signal on (Ctrl-C, a CI timeout) rather than reporting a bare exit 1.
100
+ if (result.signal) process.kill(process.pid, result.signal);
101
+ process.exit(result.status ?? 1);
102
+ }
103
+ }
104
+ }
105
+
106
+ try {
107
+ run();
108
+ } catch (error) {
109
+ // Expected failures (no book.config.yml, a bad key or argument, a bad
110
+ // `extends`, a missing runner) get one line; anything else keeps its stack
111
+ // for debugging.
112
+ const known = error instanceof UsageError || /book\.config\.yml|in extends/.test(error.message);
113
+ console.error(known ? `book-kit: ${error.message}` : error);
114
+ process.exit(2);
115
+ }
@@ -0,0 +1,32 @@
1
+ """Shared PNG output for the emoji composite scripts."""
2
+
3
+ import os
4
+
5
+ from PIL import Image, ImageChops
6
+
7
+
8
+ def save_if_changed(img, dst_path):
9
+ """Write img to dst_path unless the file there already has the same pixels.
10
+
11
+ Books often commit the composited PNGs and recompress them losslessly
12
+ (`just optimize-images`). Re-saving identical pixels with Pillow's encoder
13
+ would undo that recompression on every build and leave the working tree
14
+ dirty, so only real pixel changes are written. Returns True if it wrote.
15
+
16
+ getbbox(alpha_only=False): by default an RGBA bbox looks at alpha alone,
17
+ which would miss a color-only change.
18
+ """
19
+ if os.path.exists(dst_path):
20
+ try:
21
+ with Image.open(dst_path) as existing:
22
+ same = (
23
+ existing.mode == img.mode
24
+ and existing.size == img.size
25
+ and ImageChops.difference(existing.convert(img.mode), img).getbbox(alpha_only=False) is None
26
+ )
27
+ except OSError:
28
+ same = False # unreadable or corrupt: overwrite it
29
+ if same:
30
+ return False
31
+ img.save(dst_path, "PNG", optimize=True)
32
+ return True