# bookforge

Turn a folder of Markdown chapters into a valid, good-looking **EPUB 3** for e-readers (Kindle accepts it via Send to Kindle) and a **print-ready HTML/PDF** book, without fighting Calibre or Pandoc templates. One `book.toml`, one command, one self-checking build.

## Features

- **Project file**: title, subtitle, authors, language, ISBN or generated stable UUID, publisher, date, description, subjects, cover, chapter globs, parts, dedication, copyright template, acknowledgements, about the author, appendices, theme.
- **Markdown** via markdown-it-py: headings, emphasis, lists, tables, footnotes, images, blockquotes and epigraphs, scene breaks, cross-references (`[@chapter-id]`), links between chapter files.
- **Typography**: curly quotes, em/en dashes, ellipses; language-aware quotes for `en`, `fr` (guillemets, no-break spaces) and `de`; small-caps first lines or drop caps.
- **Code** highlighted with Pygments using plain CSS classes (works offline in every reader).
- **EPUB 3 writer** written from scratch on `zipfile`: stored `mimetype` first, `container.xml`, OPF (with `dcterms:modified`, `cover-image`), nested `nav.xhtml` with landmarks, NCX for EPUB 2 readers, XHTML checked with lxml, three CSS themes, optional embedded fonts, reproducible with `SOURCE_DATE_EPOCH`.
- **EPUB 3 footnotes** (`epub:type="noteref"`/`footnote` with back-links) or per-chapter endnotes.
- **Images** copied once, shrunk to 1600 px, re-encoded as the smaller of PNG/JPEG (flat art as palette PNG).
- **Validator**: well-formedness, manifest/spine integrity, unique ids, link and fragment resolution, image sizes, missing alt text; runs `epubcheck` too when installed.
- **Print HTML**: single file with paged-media CSS (`@page 5.5x8.5in`, margins, running headers, page breaks, TOC with `target-counter`), ready for browser print or Paged.js. `bookforge pdf` drives headless Chrome and fills in real TOC page numbers with a two-pass render.
- **Extras**: `stats` (words and reading time per chapter), `watch` (rebuild on change), `new` (scaffold).

## Install

Requires Python 3.11+ (developed on 3.12).

```bash
cd bookforge
uv venv .venv && uv pip install -p .venv/bin/python -e '.[dev]'
source .venv/bin/activate        # or call .venv/bin/bookforge directly
```

`bookforge pdf` needs Chrome or Chromium (`google-chrome`, `chromium`, ...; or `--chrome PATH` / `$BOOKFORGE_CHROME`). `epubcheck` is optional.

## Quickstart

```bash
bookforge new mybook --title "My Book" --author "Me"
cd mybook
bookforge build          # build/my-book.epub + build/my-book-print.html, then validates
bookforge pdf            # build/my-book.pdf via headless Chrome
```

Or try the bundled novella (outputs already committed in `examples/sample-book/build/`):

```bash
bookforge build examples/sample-book
bookforge pdf examples/sample-book
```

## Usage

```
bookforge new DIRECTORY [--title T] [--author A]
bookforge build [PATH] [-o OUT] [-f epub|print|all]... [--strict] [--epubcheck/--no-epubcheck]
bookforge validate FILE.epub [--strict] [--epubcheck/--no-epubcheck]
bookforge pdf [PATH] [-o OUT] [--chrome EXE]
bookforge stats [PATH]
bookforge watch [PATH] [-o OUT] [--interval SECONDS]
```

`PATH` is the directory containing `book.toml` (default `.`), output goes to `PATH/build` unless `-o` is given. Errors in the book (missing files, unknown cross-reference, bad config) print `error: ...` and exit 1; validation errors exit 1, and warnings do too with `--strict`.

### build

```
$ bookforge build examples/sample-book
wrote examples/sample-book/build/the-salt-ledger.epub (89 KB)
epubcheck not installed; skipped (built-in checks only)
validation: 0 error(s), 0 warning(s)
wrote examples/sample-book/build/the-salt-ledger-print.html (148 KB)
```

### validate

Works on any EPUB. Output for a copy of the sample with a stored-as-deflated `mimetype`, an unclosed `</section>` and a duplicated id:

```
$ bookforge validate broken.epub
epubcheck not installed; skipped (built-in checks only)
ERROR   mimetype: must be stored uncompressed
ERROR   OEBPS/text/chapter-high-water.xhtml: not well-formed XML: Opening and ending tag mismatch: section line 10 and body, line 34, column 8 (<string>, line 34)
ERROR   OEBPS/text/chapter-salvage.xhtml: duplicate id 'dup'
validation: 3 error(s), 0 warning(s)
```

### pdf

```
$ bookforge pdf examples/sample-book
wrote examples/sample-book/build/the-salt-ledger-print.html
wrote examples/sample-book/build/the-salt-ledger.pdf (24 pages, 306 KB)
```

### stats

```
$ bookforge stats examples/sample-book
Title                        Kind               Words    Read Notes Imgs
------------------------------------------------------------------------
Copyright                    copyright             45   1 min     0    0
Dedication                   dedication            15   1 min     0    0
The Ninth Entry              chapter              444   2 min     1    0
R.W.                         chapter              342   2 min     1    0
The Shape of Water           chapter              353   2 min     0    1
Salvage                      chapter              350   2 min     0    0
What Pellam Knew             chapter              319   2 min     0    0
High Water                   chapter              389   2 min     1    0
Appendix: Predicting a Tide  appendix             118   1 min     0    0
Acknowledgements             acknowledgements      72   1 min     0    0
About the Author             about                 47   1 min     0    0
------------------------------------------------------------------------
Total                                           2,494  11 min     3    1

2,197 words in 6 chapters; reading time at 230 wpm: 11 min
```

### watch

```
$ bookforge watch mybook
[20:15:12] building...
wrote mybook/build/my-book.epub (6 KB)
validation: 0 error(s), 0 warning(s)
wrote mybook/build/my-book-print.html (13 KB)
[20:15:13] building...      <- after saving a chapter
```

## Writing a chapter

```markdown
# The Ninth Entry

> The sea keeps no accounts, which is why we keep them for it.
>
> --- Harbour Office motto

"Six feet, two inches," she read aloud.[^1]

***

See also [@salvage] for what happened next.

[^1]: Footnotes become pop-up notes in the EPUB.
```

See [docs/book-toml.md](docs/book-toml.md) for every setting.

## Configuration

Everything lives in `book.toml`; the complete reference is [docs/book-toml.md](docs/book-toml.md). The sample project's file is a good template: [examples/sample-book/book.toml](examples/sample-book/book.toml). Environment: `SOURCE_DATE_EPOCH` (reproducible timestamps), `BOOKFORGE_CHROME` (Chrome path).

Other docs: [theming guide](docs/theming.md), [EPUB internals, typography, validation and the print/PDF pipeline](docs/epub-internals.md).

## Architecture

```
src/bookforge/
  cli.py         click commands, error reporting
  config.py      book.toml -> Config (validation, glob expansion, identifiers)
  model.py       Project/Doc loading (front matter, ids, titles), render_project(target)
  render.py      markdown-it-py parser + rules: typography, notes, xrefs, images, epigraphs, code
  typography.py  language-aware smart quotes, dashes, ellipses
  images.py      resize/re-encode, content-hashed names
  styles.py      CSS assembly (themes + Pygments + fonts + user CSS)
  themes/        base.css, classic.css, modern.css, manuscript.css
  epub.py        EPUB 3 writer (OPF, nav, NCX, XHTML pages, zip)
  validate.py    built-in EPUB checks + optional epubcheck
  printhtml.py   single-file paged-media HTML
  pdf.py         headless Chrome driver with two-pass TOC numbering
  stats.py  watch.py  scaffold.py
```

## Tests

```bash
.venv/bin/pytest -q          # 131 tests, ~2 s
```

Covers Markdown conversions (footnotes, smart quotes per language, scene breaks), EPUB structure (zip order, stored mimetype, OPF/nav/NCX parsed with lxml, spine order), validator checks against planted errors, cross-references, print HTML structure, and CLI end-to-end including a real headless-Chrome PDF run when Chrome is installed. `ebooklib` is used in tests as an independent EPUB reader.

## Limitations

- Raw HTML in Markdown is escaped, not passed through.
- Only `en`, `fr` and `de` have language-specific quotes; other languages use English quotes. Nested quotes use the single-quote pair; leading-apostrophe words other than decades (`'twas`) may curl the wrong way.
- Chrome ignores `target-counter()` and `string-set`, so the PDF path uses the two-pass TOC and literal running headers described in the internals doc. Chrome applies `:first` only to a document's first page, so `running_header = "chapter"` repeats the chapter title on opening pages; the default `book` mode does not.
- PDF page numbers are physical (front matter counted, unnumbered); the print TOC lists parts and chapters, not sub-headings.
- Embedded fonts are not obfuscated; no font subsetting.
- SVG images are embedded unchanged and not size-checked.
- The built-in validator is not a substitute for `epubcheck` (it does not check the full XHTML schema or EPUB accessibility metadata); install `epubcheck` and bookforge will run it automatically.
