diff --git a/BEDIENUNGSANLEITUNG.md b/BEDIENUNGSANLEITUNG.md index 8fa4a26..72e83bf 100644 --- a/BEDIENUNGSANLEITUNG.md +++ b/BEDIENUNGSANLEITUNG.md @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 31115d1..41e40f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/README.md b/README.md index d6b1a89..c91bfab 100644 --- a/README.md +++ b/README.md @@ -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 (`
`, Kapiteltypen aus `manifest.json`) und, falls das Buch + keine eigene Titelseite hat, die Stelle hinter dem Cover (`
`). 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) diff --git a/epub2md/merge.py b/epub2md/merge.py index 80ef7b9..0eb2532 100644 --- a/epub2md/merge.py +++ b/epub2md/merge.py @@ -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 = '
' # pandoc/book.lua puts the table of contents here +TITLE_PLACEHOLDER = '
' # … 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'\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()} diff --git a/epub2md/writer.py b/epub2md/writer.py index 17cae9e..f00c8de 100644 --- a/epub2md/writer.py +++ b/epub2md/writer.py @@ -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") diff --git a/pandoc/book.lua b/pandoc/book.lua new file mode 100644 index 0000000..ef78a17 --- /dev/null +++ b/pandoc/book.lua @@ -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: + +
after the cover, only if the book has no title page of its own +
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 into
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("&", "&"):gsub("<", "<"):gsub(">", ">")) +end + +--
as Pandoc's HTML template writes it. +local function title_block(meta) + local html = { '
', '

' .. esc(pandoc.utils.stringify(meta.title)) .. "

" } + if meta.subtitle then + html[#html + 1] = '

' .. esc(pandoc.utils.stringify(meta.subtitle)) .. "

" + 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] = '

' .. esc(pandoc.utils.stringify(a)) .. "

" + end + if meta.date then + html[#html + 1] = '

' .. esc(pandoc.utils.stringify(meta.date)) .. "

" + end + html[#html + 1] = "
" + return pandoc.RawBlock("html", table.concat(html, "\n")) +end + +-- Paragraph that only holds chapter start anchors () → 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('^$') or inl.text:match('^$') + if found then + id = id or found + elseif inl.text ~= "" 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 diff --git a/pandoc/print.css b/pandoc/print.css index af35eb3..e68f52d 100644 --- a/pandoc/print.css +++ b/pandoc/print.css @@ -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

. No second break for the chapter's own

. */ +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); } diff --git a/tests/test_merge.py b/tests/test_merge.py index 3e02003..2aa702a 100644 --- a/tests/test_merge.py +++ b/tests/test_merge.py @@ -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", '
Cover
'), + Doc("titel.xhtml", "

Erika Musterfrau

Vorspann

Roman

"), + Doc("impressum.xhtml", "

© 2024 Testverlag

Das Werk ist urheberrechtlich geschützt.

"), + Doc("a.xhtml", "

Kapitel A

a

")] + 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('
'), 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", '
Cover
'), Doc("a.xhtml", "

Kapitel A

a

")] + 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('
') \ + < text.index('
') < 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") diff --git a/tests/test_styles.py b/tests/test_styles.py index 2964d5b..b3faa65 100644 --- a/tests/test_styles.py +++ b/tests/test_styles.py @@ -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):