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:
parent
65c90c67ca
commit
99c3c609ca
9 changed files with 299 additions and 65 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
32
README.md
32
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 (`<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)
|
||||
|
|
|
|||
|
|
@ -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()}
|
||||
|
|
|
|||
|
|
@ -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
140
pandoc/book.lua
Normal 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("&", "&"):gsub("<", "<"):gsub(">", ">"))
|
||||
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
|
||||
|
|
@ -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); }
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Reference in a new issue