feat: Inhaltsverzeichnis nach Cover, Titelseite und Impressum (pandoc/book.lua)

Pandocs --toc setzt Inhaltsverzeichnis und Titelkopf immer vor den
Buchtext, also vor Cover, Titelseite und Impressum.

- epub2md_merge.py: Kapiteltypen aus manifest.json; Platzhalter
  <div id="epub2md-toc"> hinter dem Vorspann und, ohne eigene Titelseite,
  <div id="epub2md-titlepage"> hinter dem Cover; --toc-Liste an derselben Stelle
- pandoc/book.lua (HTML/weasyprint, statt --toc): baut dort das
  Inhaltsverzeichnis, lässt Pandocs Titelkopf bei eigener Titelseite weg,
  Cover ohne Bildunterschrift, Kapitelanker → div.chapter-start;
  andere Formate: nur Platzhalter entfernen
- print.css: jedes Kapitel (auch Cover/Titelseite/Impressum) und das
  Verzeichnis auf neuer Seite, Verzeichnis mit Punktlinie und Seitenzahl
- Buch-README, Bedienungsanleitung, README: Befehle mit book.lua

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-10-06 06:27:27 +02:00
commit 99c3c609ca
9 changed files with 299 additions and 65 deletions

View file

@ -222,7 +222,7 @@ Blöcke übernommen:
```
GitHub und Obsidian zeigen das als farbigen Kasten mit Symbol; andere Programme als eingerückten Zitatblock.
Für Pandoc (HTML, EPUB, PDF) gibt es einen Filter, der echte Kästen zeichnet, siehe Kapitel 7.
Für Pandoc (HTML, EPUB, PDF) gibt es einen Filter, der echte Kästen zeichnet, siehe Kapitel 7 („Weiterverarbeiten“).
Andere Darstellungen wählst du mit `--boxes`:
- `quote`: Zitatblock ohne `[!TIP]`
@ -309,50 +309,52 @@ cd output/meinbuch
```
`meinbuch.md` steht im Folgenden für den Namen der Gesamtdatei. Er entspricht dem Namen des
Original-E-Books.
Original-E-Books. `$P` ist der Ordner mit den Pandoc-Hilfsdateien von epub2md; lege ihn einmal fest:
```bash
P=~/Python_Programs/epub2md/pandoc # Pfad zu deinem epub2md-Ordner anpassen
```
| Ziel | Befehl |
| --- | --- |
| Webseite (HTML) | `pandoc meinbuch.md -s --toc -o meinbuch.html` |
| Webseite mit Formeln | `pandoc meinbuch.md -s --toc --mathml -o meinbuch.html` |
| E-Book (EPUB) | `pandoc meinbuch.md --toc -o meinbuch.epub` |
| Word | `pandoc meinbuch.md -o meinbuch.docx` |
| LibreOffice | `pandoc meinbuch.md -o meinbuch.odt` |
| PDF mit LaTeX | `pandoc meinbuch.md -s -o meinbuch.html && pandoc meinbuch.html --toc --pdf-engine=xelatex -o meinbuch.pdf` |
| PDF ohne LaTeX | `pandoc meinbuch.md -s --toc --css ~/Python_Programs/epub2md/pandoc/print.css --pdf-engine=weasyprint -o meinbuch.pdf` |
| Kindle (AZW3) | `pandoc meinbuch.md --toc -o meinbuch.epub && ebook-convert meinbuch.epub meinbuch.azw3` |
| PDF (empfohlen) | `pandoc meinbuch.md -s --lua-filter $P/book.lua --lua-filter $P/callouts.lua --css $P/print.css --pdf-engine=weasyprint -o meinbuch.pdf` |
| Webseite (HTML) | `pandoc meinbuch.md -s --lua-filter $P/book.lua --lua-filter $P/callouts.lua -o meinbuch.html` |
| Webseite mit Formeln | wie HTML, zusätzlich `--mathml` |
| E-Book (EPUB) | `pandoc meinbuch.md --toc --lua-filter $P/callouts.lua -o meinbuch.epub` |
| Word | `pandoc meinbuch.md --toc --lua-filter $P/callouts.lua -o meinbuch.docx` |
| LibreOffice | `pandoc meinbuch.md --toc --lua-filter $P/callouts.lua -o meinbuch.odt` |
| PDF mit LaTeX | `pandoc meinbuch.md -s --lua-filter $P/callouts.lua -o meinbuch.html && pandoc meinbuch.html --toc --lua-filter $P/callouts.lua --pdf-engine=xelatex -o meinbuch.pdf` |
| Kindle (AZW3) | erst EPUB wie oben, dann `ebook-convert meinbuch.epub meinbuch.azw3` |
Die drei Hilfsdateien:
- **`book.lua`** ordnet den Buchanfang wie im gedruckten Buch: Cover, Titelseite, Impressum, dann das
Inhaltsverzeichnis, jeweils auf einer neuen Seite. Ohne den Filter setzt Pandoc mit `--toc` das
Inhaltsverzeichnis (und seinen eigenen Titelkopf) vor das Cover. Mit `book.lua` deshalb **kein** `--toc`
angeben. Hat das Buch keine eigene Titelseite, setzt der Filter Pandocs Titelkopf hinter das Cover. Er
wirkt nur bei HTML und beim PDF über weasyprint; EPUB, Word und LibreOffice bauen ihr Verzeichnis mit
`--toc` selbst.
- **`callouts.lua`** zeichnet die Infokästen (`> [!TIP]`). Pandoc in der Ubuntu-Version kennt diese
Schreibweise nicht und druckt „[!TIP]“ sonst einfach mit. In HTML, EPUB und im PDF über weasyprint werden
daraus gerahmte, farbige Kästen mit Titel („Tipp: …“), im PDF über LaTeX und in Word/LibreOffice ein
eingerückter Block mit fettem Titel.
- **`print.css`** liefert das Druck-Layout für weasyprint: A4, Seitenzahlen, jedes Kapitel auf einer neuen
Seite, Inhaltsverzeichnis mit Seitenzahlen, Silbentrennung und umbrechende lange Codezeilen. Ohne sie
übernimmt weasyprint das Bildschirmlayout, und lange Codezeilen werden am Seitenrand abgeschnitten.
Du musst die Befehle nicht abtippen: Die `README.md` im Ausgabeordner enthält sie fertig, mit allen Pfaden.
**Zum PDF:**
- **Mit weasyprint** (empfohlen) genügt ein Schritt. Tabellen, Farben, Infokästen und die Gestaltung aus
`--styles html` bleiben erhalten, der Buchanfang ist richtig geordnet. Für Bücher mit vielen Formeln ist
der LaTeX-Weg sicherer.
- **Mit LaTeX** immer den Zwischenschritt über HTML nehmen, wie in der Tabelle. Beim direkten Weg von
Markdown nach LaTeX lässt Pandoc komplexe Tabellen weg, und Querverweise im PDF funktionieren nicht. Bei
Büchern mit Formeln ist der LaTeX-Weg die beste Wahl.
- **Mit weasyprint** genügt ein Schritt; Tabellen, Farben, Infokästen und die Gestaltung aus `--styles html`
bleiben erhalten. Für Bücher mit vielen Formeln ist der LaTeX-Weg sicherer.
- **`print.css`** (im Ordner `pandoc/` von epub2md) sorgt beim weasyprint-Weg für ein Buchlayout: A4,
Seitenzahlen, jedes Kapitel auf einer neuen Seite, Silbentrennung und umbrechende lange Codezeilen. Ohne
sie übernimmt weasyprint das Bildschirmlayout, und lange Codezeilen werden am Seitenrand abgeschnitten.
Markdown nach LaTeX lässt Pandoc komplexe Tabellen weg, und Querverweise im PDF funktionieren nicht. Das
Inhaltsverzeichnis steht hier am Anfang, vor dem Cover.
Bei Büchern mit Formeln für HTML zusätzlich `--mathml` angeben, sonst bleiben Matrizen als TeX-Text stehen.
#### Infokästen richtig darstellen
Pandoc in der Ubuntu-Version kennt die Kasten-Schreibweise `> [!TIP]` nicht und druckt „[!TIP]“ einfach mit.
Dafür liegt im epub2md-Ordner der Filter `pandoc/callouts.lua`. Hänge ihn an jeden Pandoc-Befehl an:
```bash
F=~/Python_Programs/epub2md/pandoc/callouts.lua # Pfad zu deinem epub2md-Ordner
pandoc meinbuch.md -s --toc --lua-filter $F -o meinbuch.html
pandoc meinbuch.md --toc --lua-filter $F -o meinbuch.epub
pandoc meinbuch.md -s --lua-filter $F -o meinbuch.html && pandoc meinbuch.html --toc --lua-filter $F --pdf-engine=xelatex -o meinbuch.pdf
pandoc meinbuch.md -s --toc --lua-filter $F --css ~/Python_Programs/epub2md/pandoc/print.css --pdf-engine=weasyprint -o meinbuch.pdf
```
In HTML, EPUB und im PDF über weasyprint werden daraus gerahmte, farbige Kästen mit Titel („Tipp: …“). Im
PDF über LaTeX und in Word/LibreOffice wird ein eingerückter Block mit fettem Titel daraus. Du musst nicht
selbst nachsehen, ob dein Buch Kästen hat: Die `README.md` im Ausgabeordner enthält in diesem Fall die
Befehle bereits mit Filter.
#### Direkt aus den Kapiteldateien
Ohne Gesamtdatei geht es auch. Dann im Ordner `chapters/` arbeiten und die Buchdaten aus

View file

@ -7,6 +7,11 @@ Versionsnummern nach [Semantic Versioning](https://semver.org/lang/de/).
### Neu
- `pandoc/book.lua`: Inhaltsverzeichnis wie im gedruckten Buch nach Cover, Titelseite und Impressum statt
davor (Pandocs `--toc` setzt es immer an den Anfang). `epub2md_merge.py` markiert dafür das Ende des
Vorspanns; ohne eigene Titelseite kommt Pandocs Titelkopf hinter das Cover. Jedes Kapitel, auch Cover,
Titelseite und Impressum, beginnt mit `print.css` auf einer neuen Seite; das Inhaltsverzeichnis hat
Seitenzahlen. Mit `epub2md_merge.py --toc` steht dessen Verzeichnis an derselben Stelle.
- `pandoc/print.css`: Druck-Layout für PDFs mit weasyprint (A4, Seitenzahlen, Kapitel auf neuer Seite,
Silbentrennung außer in Code, umbrechende Codezeilen). Die Buch-README enthält den passenden Befehl.

View file

@ -331,9 +331,11 @@ konsistente Anker:
```bash
cd output/programmierbuch # Medienpfade sind relativ zur Gesamtdatei
F=~/Python_Programs/epub2md/pandoc/callouts.lua # Pfad zum epub2md-Ordner anpassen
pandoc programmierbuch.md --toc -s --lua-filter $F -o programmierbuch.html
pandoc programmierbuch.md --toc --lua-filter $F -o programmierbuch.epub
P=~/Python_Programs/epub2md/pandoc # Pfad zum epub2md-Ordner anpassen
pandoc programmierbuch.md -s --lua-filter $P/book.lua --lua-filter $P/callouts.lua \
--css $P/print.css --pdf-engine=weasyprint -o programmierbuch.pdf # PDF (empfohlen)
pandoc programmierbuch.md -s --lua-filter $P/book.lua --lua-filter $P/callouts.lua -o programmierbuch.html
pandoc programmierbuch.md --toc --lua-filter $P/callouts.lua -o programmierbuch.epub
cd output/mathematikbuch
pandoc mathematikbuch.md --mathml -s -o mathematikbuch.html
@ -341,16 +343,21 @@ pandoc mathematikbuch.md --mathml -s -o mathematikbuch.html
pandoc mathematikbuch.html --toc --pdf-engine=xelatex -o mathematikbuch.pdf
```
**PDF mit weasyprint** (ohne LaTeX; Farben, Kästen und `--styles html` bleiben erhalten):
Hilfsdateien in `pandoc/`:
```bash
pandoc buch.md -s --toc --lua-filter $F --css ~/Python_Programs/epub2md/pandoc/print.css \
--pdf-engine=weasyprint -o buch.pdf
```
`pandoc/print.css` liefert das Buchlayout: A4, Seitenzahlen, Kapitel auf neuer Seite, Silbentrennung (nicht in
Code) und umbrechende Codezeilen. Mit `--css` lässt Pandoc sein eigenes Stylesheet weg, deshalb bringt
`print.css` auch Grundschrift, Zeilenabstand und Titelkopf mit. Getestet mit dem Python-Buch (342 Seiten).
- **`book.lua`** (HTML, PDF über weasyprint; statt `--toc`): Pandocs `--toc` setzt Inhaltsverzeichnis und
Titelkopf immer an den Anfang, vor Cover, Titelseite und Impressum. `epub2md_merge.py` markiert deshalb das
Ende des Vorspanns (`<div id="epub2md-toc"></div>`, Kapiteltypen aus `manifest.json`) und, falls das Buch
keine eigene Titelseite hat, die Stelle hinter dem Cover (`<div id="epub2md-titlepage"></div>`). Der
Filter baut dort das Inhaltsverzeichnis (`--toc-depth` gilt), lässt Pandocs Titelkopf bei vorhandener
Titelseite weg, zeigt das Cover ohne Bildunterschrift und macht aus den Kapitelankern
`div.chapter-start` für Seitenumbrüche. Bei anderen Formaten entfernt er nur die Platzhalter. Wer selbst
`--toc` angibt, bekommt Pandocs Verzeichnis wie gewohnt.
- **`print.css`** (weasyprint): A4, Seitenzahlen, jedes Kapitel (auch Cover, Titelseite, Impressum) und das
Inhaltsverzeichnis auf neuer Seite, Inhaltsverzeichnis mit Punktlinie und Seitenzahl, Silbentrennung
(nicht in Code), umbrechende Codezeilen. Mit `--css` lässt Pandoc sein eigenes Stylesheet weg, deshalb
bringt `print.css` auch Grundschrift, Zeilenabstand und Titelkopf mit. Getestet mit dem Python-Buch
(362 Seiten).
**Infokästen:** Pandoc kennt die Schreibweise `> [!TIP]` erst ab neueren Versionen (Erweiterung `alerts`);
3.1.3 (Ubuntu 24.04) zeigt „[!TIP]“ als Text. Der Filter `pandoc/callouts.lua` behebt das: In HTML und
@ -450,6 +457,7 @@ epub2md/
├── models.py Dataclasses (Kapitel, Medien, Noten, Metadaten …)
└── utils.py Slugs, YAML-Emitter, Pfadhelfer
pandoc/
├── book.lua Pandoc-Filter: Inhaltsverzeichnis nach Cover/Titelseite/Impressum, Kapitel-Seitenumbrüche
├── callouts.lua Pandoc-Filter: '> [!TIP]'-Kästen → gerahmte Kästen (HTML/EPUB) bzw. Zitatblock mit Titel
├── callouts.css Gestaltung der Kästen (vom Filter eingebunden; muss neben callouts.lua liegen)
└── print.css Druck-Layout für PDFs mit weasyprint (A4, Seitenzahlen, Kapitel auf neuer Seite, Codeumbruch)

View file

@ -46,6 +46,7 @@ class ChapterFile:
body: str = ""
start_anchor: str = ""
anchor_map: dict[str, str] = field(default_factory=dict)
types: list[str] = field(default_factory=list)
@dataclass
@ -74,12 +75,12 @@ def load_book(out_dir: Path) -> tuple[dict, list[ChapterFile]]:
report = json.loads(report_path.read_text(encoding="utf-8")) if report_path.is_file() else {}
if manifest_path.is_file():
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
entries = [(c["number"], c["title"], c["file"]) for c in manifest.get("chapters", [])]
entries = [(c["number"], c["title"], c["file"], c.get("types") or []) for c in manifest.get("chapters", [])]
else:
files = sorted((out_dir / "chapters").glob("*.md"))
entries = [(i + 1, p.stem, f"chapters/{p.name}") for i, p in enumerate(files)]
chapters = [ChapterFile(n, t, rel) for n, t, rel in entries if (out_dir / rel).is_file()]
missing = [rel for _, _, rel in entries if not (out_dir / rel).is_file()]
entries = [(i + 1, p.stem, f"chapters/{p.name}", []) for i, p in enumerate(files)]
chapters = [ChapterFile(n, t, rel, types=list(ty)) for n, t, rel, ty in entries if (out_dir / rel).is_file()]
missing = [rel for _, _, rel, _ in entries if not (out_dir / rel).is_file()]
if missing:
raise MergeError(f"Kapiteldateien fehlen: {', '.join(missing[:5])}")
if not chapters:
@ -89,6 +90,21 @@ def load_book(out_dir: Path) -> tuple[dict, list[ChapterFile]]:
return report, chapters
# Front matter that precedes the table of contents in a printed book (cover, title page, imprint …).
FRONT_TYPES = {"cover", "titlepage", "halftitlepage", "colophon", "copyright-page", "imprint", "dedication",
"epigraph", "frontispiece", "seriespage", "imprimatur"}
TOC_PLACEHOLDER = '<div id="epub2md-toc"></div>' # pandoc/book.lua puts the table of contents here
TITLE_PLACEHOLDER = '<div id="epub2md-titlepage"></div>' # … and Pandoc's title block here (no own title page)
def front_matter_count(chapters: list[ChapterFile]) -> int:
"""Number of leading chapters that are front matter (cover, title page, imprint …)."""
n = 0
while n < len(chapters) and FRONT_TYPES & set(chapters[n].types):
n += 1
return n
ISO_DATE = re.compile(r"^(\d{4})-(\d{2})-(\d{2})(?:[T ][\d:.]+(?:Z|[+-]\d{2}:?\d{2})?)?$")
@ -240,15 +256,28 @@ def merge(out_dir: Path, target: Path | None = None, *, frontmatter: bool = True
fm[key] = display_date(md[src], md.get("language", "")) if key == "date" else md[src]
if fm:
parts.append("---\n" + to_yaml(fm) + "---")
front = front_matter_count(chapters)
has_title_page = any("titlepage" in ch.types for ch in chapters[:front])
cover_first = bool(chapters) and "cover" in chapters[0].types
if toc:
lines = ["## Inhalt" if (report.get("metadata") or {}).get("language", "de").startswith("de") else "## Contents", ""]
lines += [f"{ch.number}. [{_escape_link_text(ch.title)}](#{ch.start_anchor})" for ch in chapters]
parts.append("\n".join(lines))
for ch in chapters:
toc_block = "\n".join(lines)
else:
toc_block = TOC_PLACEHOLDER
if not has_title_page and not cover_first:
parts.append(TITLE_PLACEHOLDER)
if front == 0:
parts.append(toc_block)
for i, ch in enumerate(chapters):
segments = [(is_code, seg if is_code else rewrite_segment(seg, ch)) for is_code, seg in _non_code_segments(ch.body)]
body, n = _drop_duplicate_definitions(_join(segments), seen_defs)
dropped += n
parts.append(f'<a id="{ch.start_anchor}"></a>\n\n{body.strip()}')
if i == 0 and cover_first and not has_title_page:
parts.append(TITLE_PLACEHOLDER)
if i + 1 == front:
parts.append(toc_block)
text = "\n\n".join(parts).rstrip() + "\n"
existing = {p.resolve().as_posix() for p in out_dir.rglob("*") if p.is_file()}

View file

@ -18,6 +18,7 @@ from .utils import rel_link, to_yaml, url_path
CALLOUT_FILTER = Path(__file__).resolve().parent.parent / "pandoc" / "callouts.lua"
PRINT_CSS = Path(__file__).resolve().parent.parent / "pandoc" / "print.css"
BOOK_FILTER = Path(__file__).resolve().parent.parent / "pandoc" / "book.lua"
SPECIAL_LABELS = {
"glossary": "Glossar",
@ -137,11 +138,13 @@ def readme(md: BookMetadata, chapters: list[Chapter], endnotes: Chapter | None,
lua = ""
if callouts and CALLOUT_FILTER.is_file(): # '> [!TIP]' boxes: Pandoc needs the filter to draw real boxes
lua = f" --lua-filter {shlex.quote(str(CALLOUT_FILTER))}"
# book.lua: table of contents after cover/title page/imprint instead of Pandoc's --toc at the very top
toc = f" --lua-filter {shlex.quote(str(BOOK_FILTER))}" if BOOK_FILTER.is_file() else " --toc"
commands = [
"# Gesamtdatei erzeugen (im selben Arbeitsverzeichnis wie die Konvertierung)",
f"python3 epub2md_merge.py {shlex.quote(output_dir)}",
"# danach in diesem Ordner:",
f"pandoc {shlex.quote(stem + '.md')} --toc -s{math}{lua} -o {shlex.quote(stem + '.html')}",
f"pandoc {shlex.quote(stem + '.md')} -s{math}{toc}{lua} -o {shlex.quote(stem + '.html')}",
f"pandoc {shlex.quote(stem + '.md')} --toc{lua} -o {shlex.quote(stem + '.epub')}",
"# PDF über HTML: Pandoc übernimmt dann auch HTML-Tabellen und interne Sprungziele",
f"pandoc {shlex.quote(stem + '.md')} -s{math}{lua} -o {shlex.quote(stem + '.html')} && "
@ -150,7 +153,7 @@ def readme(md: BookMetadata, chapters: list[Chapter], endnotes: Chapter | None,
if PRINT_CSS.is_file():
commands += [
"# PDF ohne LaTeX mit weasyprint: Farben, Kästen und HTML-Stile bleiben erhalten",
f"pandoc {shlex.quote(stem + '.md')} -s --toc{math}{lua} --css {shlex.quote(str(PRINT_CSS))} "
f"pandoc {shlex.quote(stem + '.md')} -s{math}{toc}{lua} --css {shlex.quote(str(PRINT_CSS))} "
f"--pdf-engine=weasyprint -o {shlex.quote(stem + '.pdf')}",
]
out.append("## Weiterverarbeitung")

140
pandoc/book.lua Normal file
View file

@ -0,0 +1,140 @@
--[[
book.lua – Pandoc filter for book files from epub2md_merge.py (HTML output, e.g. PDF via weasyprint).
Pandoc's --toc always puts the table of contents (and its own title block) before the whole text,
i.e. before the book's cover, title page and imprint. epub2md_merge.py marks the right places:
<div id="epub2md-titlepage"></div> after the cover, only if the book has no title page of its own
<div id="epub2md-toc"></div> after the front matter (cover, title page, imprint …)
This filter, for HTML output:
* builds the table of contents at the placeholder (use it INSTEAD of --toc; --toc-depth is honoured),
* drops Pandoc's title block when the book has its own title page, or moves it to the placeholder,
* shows the cover picture (chapter "NNN-cover") without the caption "Cover",
* turns the chapter anchors <a id="001-…"></a> into <div class="chapter-start"> so that print.css can
start every chapter – including cover, title page and imprint – on a new page.
For other formats it only removes the placeholders (EPUB, DOCX, ODT: use --toc there).
Usage:
pandoc buch.md -s --lua-filter /pfad/zu/epub2md/pandoc/book.lua \
--lua-filter /pfad/zu/epub2md/pandoc/callouts.lua --css /pfad/zu/epub2md/pandoc/print.css \
--pdf-engine=weasyprint -o buch.pdf
]]
local TOC_ID, TITLE_ID = "epub2md-toc", "epub2md-titlepage"
local TOC_TITLE = { de = "Inhaltsverzeichnis", en = "Contents" }
local function is_placeholder(block, id)
return block.t == "Div" and block.identifier == id and #block.content == 0
end
local function esc(s)
return (s:gsub("&", "&amp;"):gsub("<", "&lt;"):gsub(">", "&gt;"))
end
-- <header id="title-block-header"> as Pandoc's HTML template writes it.
local function title_block(meta)
local html = { '<header id="title-block-header">', '<h1 class="title">' .. esc(pandoc.utils.stringify(meta.title)) .. "</h1>" }
if meta.subtitle then
html[#html + 1] = '<p class="subtitle">' .. esc(pandoc.utils.stringify(meta.subtitle)) .. "</p>"
end
local authors = meta.author
if authors and pandoc.utils.type(authors) ~= "List" then
authors = { authors }
end
for _, a in ipairs(authors or {}) do
html[#html + 1] = '<p class="author">' .. esc(pandoc.utils.stringify(a)) .. "</p>"
end
if meta.date then
html[#html + 1] = '<p class="date">' .. esc(pandoc.utils.stringify(meta.date)) .. "</p>"
end
html[#html + 1] = "</header>"
return pandoc.RawBlock("html", table.concat(html, "\n"))
end
-- Paragraph that only holds chapter start anchors (<a id="001-cover"></a>) → id of the first one.
local function chapter_anchor(block)
if block.t ~= "Para" and block.t ~= "Plain" then
return nil
end
local id = nil
for _, inl in ipairs(block.content) do
if inl.t ~= "RawInline" then
return nil
end
local found = inl.text:match('^<a id="(%d%d%d%-[^"]*)">$') or inl.text:match('^<a id="(%d%d%d%-[^"]*)"></a>$')
if found then
id = id or found
elseif inl.text ~= "</a>" then
return nil
end
end
return id
end
function Pandoc(doc)
local html = FORMAT:match("html") and not FORMAT:match("epub")
local toc_at, title_at = nil, nil
for i, block in ipairs(doc.blocks) do
if is_placeholder(block, TOC_ID) then
toc_at = i
elseif is_placeholder(block, TITLE_ID) then
title_at = i
end
end
if not toc_at and not title_at then
return nil -- not a file from epub2md_merge.py
end
if not html then -- placeholders are top-level blocks: just drop them
local kept = pandoc.Blocks({})
for _, block in ipairs(doc.blocks) do
if not is_placeholder(block, TOC_ID) and not is_placeholder(block, TITLE_ID) then
kept:insert(block)
end
end
doc.blocks = kept
return doc
end
local blocks = pandoc.Blocks({})
for i, block in ipairs(doc.blocks) do
local anchor = chapter_anchor(block)
if i == toc_at then
if PANDOC_WRITER_OPTIONS.table_of_contents then
-- --toc given as well: Pandoc's own table of contents stays at the top, no second one
else
local lang = doc.meta.lang and pandoc.utils.stringify(doc.meta.lang):sub(1, 2):lower() or "de"
local toc = pandoc.structure.table_of_contents(doc.blocks, PANDOC_WRITER_OPTIONS)
blocks:insert(pandoc.Div({
pandoc.Header(1, TOC_TITLE[lang] or TOC_TITLE.de, pandoc.Attr("toc-title", { "unnumbered", "unlisted" })),
toc,
}, pandoc.Attr("TOC", { "toc" })))
end
elseif i == title_at then
if doc.meta.title then
blocks:insert(title_block(doc.meta))
end
elseif anchor then
blocks:insert(pandoc.Div({}, pandoc.Attr(anchor, { "chapter-start" })))
local nxt = doc.blocks[i + 1]
if anchor:match("^%d%d%d%-cover") and nxt and nxt.t == "Figure" then
-- cover chapter: the picture without the caption "Cover", centred on its own page
local imgs = {}
nxt:walk({ Image = function(img) imgs[#imgs + 1] = img end })
if #imgs == 1 then
doc.blocks[i + 1] = pandoc.Div({ pandoc.Plain({ imgs[1] }) }, pandoc.Attr("", { "cover" }))
end
end
else
blocks:insert(block)
end
end
doc.blocks = blocks
-- The book's own title page (or the moved block) replaces Pandoc's title block at the very top.
if doc.meta.title then
doc.meta.pagetitle = doc.meta.pagetitle or pandoc.utils.stringify(doc.meta.title)
doc.meta.title = nil
end
return doc
end

View file

@ -1,5 +1,7 @@
/* Print layout for PDFs from epub2md output via Pandoc + weasyprint:
pandoc buch.md -s --toc --lua-filter pandoc/callouts.lua --css pandoc/print.css --pdf-engine=weasyprint -o buch.pdf */
pandoc buch.md -s --lua-filter pandoc/book.lua --lua-filter pandoc/callouts.lua --css pandoc/print.css \
--pdf-engine=weasyprint -o buch.pdf
book.lua places the table of contents after cover, title page and imprint and marks chapter starts. */
@page { size: A4; margin: 2cm 1.8cm; @bottom-center { content: counter(page); font-size: 9pt; } }
body { max-width: none; font-size: 10.5pt; line-height: 1.4; hyphens: auto; text-align: justify;
font-family: "DejaVu Serif", "Liberation Serif", Georgia, serif; }
@ -7,13 +9,23 @@ body { max-width: none; font-size: 10.5pt; line-height: 1.4; hyphens: auto; text
h1, h2, h3, h4 { text-align: left; hyphens: manual; page-break-after: avoid; }
pre, code { font-family: "DejaVu Sans Mono", "Liberation Mono", monospace; hyphens: none; }
pre { background: #f6f8fa; padding: 0.5em 0.8em; border-radius: 0.3em; page-break-inside: auto; }
blockquote { margin: 1em 0 1em 1.5em; padding-left: 0.8em; border-left: 2px solid #ccc; color: #444; }
nav#TOC ul { list-style: none; padding-left: 1.2em; }
a { color: inherit; text-decoration: none; }
pre, pre code { white-space: pre-wrap; overflow-wrap: anywhere; font-size: 8.5pt; } /* long code lines wrap */
pre { overflow: visible; }
img { max-width: 100%; height: auto; }
h1 { page-break-before: always; } /* every chapter on a new page */
#title-block-header h1, #TOC h1 { page-break-before: auto; }
nav#TOC { page-break-after: always; }
blockquote { margin: 1em 0 1em 1.5em; padding-left: 0.8em; border-left: 2px solid #ccc; color: #444; }
a { color: inherit; text-decoration: none; }
img { max-width: 100%; max-height: 24cm; height: auto; }
table { font-size: 9pt; }
.cover { text-align: center; }
.cover img { max-height: 25cm; }
/* New pages: every chapter (book.lua: div.chapter-start, also cover, title page, imprint), the title
block, the table of contents; without book.lua every <h1>. No second break for the chapter's own <h1>. */
h1, .chapter-start, #title-block-header, #TOC { page-break-before: always; }
.chapter-start + h1, #title-block-header h1, #TOC h1 { page-break-before: auto; }
#TOC { page-break-after: always; }
/* Table of contents with dotted leaders and page numbers. */
#TOC ul { list-style: none; padding-left: 1.2em; margin: 0; }
#TOC > ul { padding-left: 0; }
#TOC li { text-align: left; }
#TOC a::after { content: leader(".") target-counter(attr(href), page); }

View file

@ -84,3 +84,37 @@ def test_merge_shortens_iso_date(convert):
assert merge_main([str(run.out)]) == 0
text = (run.out / f"{run.report['input']['name'][:-5]}.md").read_text(encoding="utf-8")
assert yaml.safe_load(text.split("---\n")[1])["date"] == "01.01.2024"
def front_book() -> Book:
docs = [Doc("cover.xhtml", '<div><img src="img/a.png" alt="Cover"/></div>'),
Doc("titel.xhtml", "<p>Erika Musterfrau</p><p>Vorspann</p><p>Roman</p>"),
Doc("impressum.xhtml", "<p>© 2024 Testverlag</p><p>Das Werk ist urheberrechtlich geschützt.</p>"),
Doc("a.xhtml", "<h1>Kapitel A</h1><p>a</p>")]
return Book(title="Vorspann", docs=docs, nav=[("Kapitel A", "a.xhtml")], media={"img/a.png": PNG})
def test_merge_places_toc_placeholder_after_front_matter(convert):
run = convert(front_book())
assert merge_main([str(run.out)]) == 0
text = (run.out / f"{run.report['input']['name'][:-5]}.md").read_text(encoding="utf-8")
cover, title, imprint = text.index("![Cover]"), text.index("\nRoman"), text.index("© 2024")
toc, first = text.index('<div id="epub2md-toc"></div>'), text.index("# Kapitel A")
assert cover < title < imprint < toc < first
assert "epub2md-titlepage" not in text # the book has its own title page
def test_merge_title_placeholder_after_cover_without_own_title_page(convert):
docs = [Doc("cover.xhtml", '<div><img src="img/a.png" alt="Cover"/></div>'), Doc("a.xhtml", "<h1>Kapitel A</h1><p>a</p>")]
run = convert(Book(title="Ohne Titelei", docs=docs, nav=[("Kapitel A", "a.xhtml")], media={"img/a.png": PNG}))
assert merge_main([str(run.out)]) == 0
text = (run.out / f"{run.report['input']['name'][:-5]}.md").read_text(encoding="utf-8")
assert text.index("![Cover]") < text.index('<div id="epub2md-titlepage"></div>') \
< text.index('<div id="epub2md-toc"></div>') < text.index("# Kapitel A")
def test_merge_own_toc_replaces_placeholder(convert):
run = convert(front_book())
assert merge_main([str(run.out), "--toc"]) == 0
text = (run.out / f"{run.report['input']['name'][:-5]}.md").read_text(encoding="utf-8")
assert "epub2md-toc" not in text and text.index("© 2024") < text.index("## Inhalt") < text.index("# Kapitel A")

View file

@ -191,8 +191,9 @@ def test_book_readme_suggests_callout_filter(convert):
readme = (convert(Book(docs=[Doc("c.xhtml", body)])).out / "README.md").read_text(encoding="utf-8")
assert "--lua-filter" in readme and "pandoc/callouts.lua" in readme and "1 Infokasten " in readme
assert "pandoc/print.css --pdf-engine=weasyprint" in readme
assert "pandoc/book.lua --lua-filter" in readme # table of contents after the front matter
plain = convert(Book(docs=[Doc("c.xhtml", body)]), "--boxes", "plain").out / "README.md"
assert "--lua-filter" not in plain.read_text(encoding="utf-8")
assert "callouts.lua" not in plain.read_text(encoding="utf-8")
def test_hidden_span_gets_no_field_separator_and_class_type_wins(convert):