# epub2md Kommandozeilenwerkzeug, das **DRM-freie EPUB-Dateien** (EPUB 2 und EPUB 3) lokal und offline in eine strukturierte Sammlung **kapitelweiser Markdown-Dateien** umwandelt – mit extrahierten Medien, YAML-Metadaten, Inhaltsverzeichnis, maschinenlesbarem Manifest und einem Konvertierungsbericht. Gedacht für die Weiterverarbeitung mit Markdown-Renderern, Pandoc, Obsidian/Zettelkasten, Git, RAG-/Embedding-Pipelines, Volltextsuche und lokale LLMs. Eine Schritt-für-Schritt-Anleitung für Anwender steht in der [BEDIENUNGSANLEITUNG.md](BEDIENUNGSANLEITUNG.md). Das Projekt enthält zwei Programme: | Programm | Zweck | | --- | --- | | `epub2md.py` | EPUB → Verzeichnis mit Kapitel-Markdown, Medien, Metadaten und Bericht | | `epub2md_merge.py` | Ausgabeverzeichnis → **eine** vollständige Markdown-Datei des Buches | ## Schnellstart ```bash pip install -r requirements.txt python3 epub2md.py Beispiele/buch.epub output/buch # Kapitel-Dateien erzeugen python3 epub2md_merge.py output/buch # → output/buch/buch.md (Gesamtdatei) ``` ## Grenzen - **Kein DRM:** Verschlüsselte EPUBs (`META-INF/encryption.xml` mit verschlüsselten Inhalten) werden mit Exit-Code 2 abgelehnt. Es wird kein Kopierschutz umgangen. Reine Font-Obfuscation ist erlaubt. - **Keine OCR:** Gescannte Bücher (Seiten als Bilder) liefern nur Bildreferenzen, keinen Text. - **Formeln als Bild/SVG** werden nie in TeX „geraten“. Sie bleiben als Bild/SVG erhalten und werden im Bericht gemeldet. TeX entsteht nur aus echtem MathML, TeX-Annotationen oder TeX im `alt`-Text. - **Layout-Heuristiken** (z. B. Überschriften, die als Tabellen gesetzt sind) sind konservativ und erfassen nicht jedes Verlagslayout. ## Installation (Ubuntu 24.04) ```bash sudo apt install python3 python3-venv python3 -m venv .venv source .venv/bin/activate pip install -r requirements.txt # Laufzeit: nur lxml pip install -r requirements-dev.txt # Tests: pytest, PyYAML ``` Laufzeitabhängigkeit ist ausschließlich **lxml** (sicheres XML/HTML-Parsing). Alles andere ist Standardbibliothek. Pandoc, Calibre, Java oder Node.js werden **nicht** benötigt. Pandoc ist nur für die optionale Weiterverarbeitung der Ausgabe nützlich. ## Aufruf ```bash python3 epub2md.py INPUT.epub OUTPUT_VERZEICHNIS [Optionen] # alternativ python3 -m epub2md INPUT.epub OUTPUT_VERZEICHNIS [Optionen] ``` Beispiele: ```bash # Programmierbuch mit Standardoptionen python3 epub2md.py \ Beispiele/programmierbuch.epub \ output/programmierbuch \ --format gfm \ --math auto \ --footnotes end-of-chapter \ --extract-media \ --frontmatter \ --toc \ --verbose # Roman mit Teilen (h1) und Kapiteln (h2): ein File pro Kapitel python3 epub2md.py roman.epub output/roman --chapter-level 2 # Nur analysieren, nichts schreiben python3 epub2md.py buch.epub output/buch --dry-run # Erneut konvertieren und frühere Ausgabe ersetzen python3 epub2md.py buch.epub output/buch --force --report berichte/buch.json ``` ## Optionen | Option | Standard | Wirkung | | --- | --- | --- | | `--format {gfm,commonmark}` | `gfm` | `gfm`: Pipe-Tabellen, `~~durchgestrichen~~`, `[^fn]`-Fußnoten. `commonmark`: Tabellen als HTML, ``, Fußnoten als Anker-Links | | `--extract-media` / `--no-extract-media` | an | Medien nach `media/` extrahieren. Ohne Extraktion stehen Bildreferenzen als Text `[Bild: alt – Pfad]` da (keine kaputten Links) | | `--copy-original` / `--no-copy-original` | aus | Original-EPUB nach `source/` kopieren | | `--chapter-level N` | `1` | Überschriftenebene (1–6), ab der eine neue Kapiteldatei beginnt | | `--toc` / `--no-toc` | an | `README.md` mit Inhaltsverzeichnis, Statistik und Reproduktionsbefehl | | `--frontmatter` / `--no-frontmatter` | an | YAML-Frontmatter in jeder Kapiteldatei | | `--footnotes {keep-links,end-of-chapter,end-of-book,inline}` | `end-of-chapter` | siehe [Fußnoten](#fußnoten-modi) | | `--math {auto,tex,mathml,image}` | `auto` | siehe [Mathematik](#math-modi) | | `--images {relative,flat,original-path}` | `relative` | `relative`: `media/images/0001-name.ext`; `flat`: `media/name.ext`; `original-path`: `media/` | | `--slug-language {de,en,none}` | `de` | Dateinamen: `de` ä→ae, ß→ss; `en` ä→a, ß→ss; `none` Unicode bleibt | | `--code-language SPRACHE` | – | Sprachlabel für Codeblöcke ohne erkannte Sprache (z. B. `python`, `c`; Aliase wie `py` erlaubt). Erkannte Labels haben Vorrang; im Bericht `detected_by: "option"` | | `--keep-wrap-markers` | aus | Druck-Umbruchzeichen `↪`/`➥` in Codeblöcken stehen lassen. Standard: Fortsetzungszeilen werden wieder zur ursprünglichen Codezeile zusammengefügt (Anzahl im Bericht: `wrap_lines_joined`) | | `--keep-html` | aus | Unbekannte/nicht sicher konvertierbare Elemente als bereinigtes HTML erhalten (sonst: Text übernommen + Eintrag in `diagnostics/unsupported-elements.md`) | | `--strict` | aus | Abbruch bei kritischen Fehlern (fehlende/unlesbare Spine-Dokumente, unsichere ZIP-Pfade, ungültige Spine-Verweise) | | `--force` | aus | Bestehendes Ausgabeverzeichnis ersetzen – siehe [Sicherheit](#sicherheit-und-fehlerbehandlung) | | `--verbose`, `-v` | aus | Verarbeitungsschritte und Warnungen auf stderr | | `--dry-run` | aus | EPUB analysieren, geplante Kapiteldateien anzeigen, **nichts** schreiben (auch kein `--report`) | | `--report DATEI.json` | – | Bericht zusätzlich an diesen Ort schreiben. `OUTPUT/conversion-report.json` entsteht immer | | `--version` | – | Programmversion | ### Rückgabecodes | Code | Bedeutung | | --- | --- | | 0 | Erfolgreich, ggf. mit nicht-kritischen Warnungen | | 1 | Ungültige Parameter, Eingabedatei fehlt/nicht lesbar, Ausgabeverzeichnis belegt oder nicht beschreibbar (Schreibfehler) | | 2 | ZIP-/Container-Fehler, DRM, unsichere ZIP-Pfade (mit `--strict`) | | 3 | OPF-/Spine-/XHTML-Fehler (z. B. leere Spine; mit `--strict` auch einzelne defekte Dokumente) | | 4 | Unerwarteter interner Fehler (Traceback mit `--verbose`) | ## Ausgabestruktur ```text output/mein-buch/ ├── README.md # Titel, Metadaten, Inhaltsverzeichnis, Statistik, Reproduktion ├── metadata.yaml # Buchmetadaten (nur vorhandene Felder, nichts erfunden) ├── manifest.json # Kapitel mit Anker-Listen, Wortzahl, Code/Bild/Formel-Zählern – für RAG ├── conversion-report.json # vollständiger Bericht (Optionen, SHA-256, Warnungen, Formeln, Links …) ├── source/mein-buch.epub # nur mit --copy-original ├── media/ │ ├── images/0001-cover.jpg # stabile, kollisionsfreie Namen; jede Quelle genau einmal │ ├── audio/ video/ other/ # Audio, Video, Schriften, CSS … ├── chapters/ │ ├── 001-cover.md │ ├── 002-vorwort.md │ ├── 003-1-einleitung.md │ ├── … │ └── 999-anmerkungen.md # nur bei --footnotes end-of-book ├── diagnostics/ │ ├── warnings.md # Warnungen/Hinweise gruppiert, für Menschen │ └── unsupported-elements.md └── mein-buch.md # Gesamtdatei, erst nach epub2md_merge.py ``` - Dateinamen: fortlaufend dreistellig nummeriert (`001-…`) plus Slug aus dem Kapiteltitel. Gleiche Titel bekommen eindeutige Namen, weil die Nummer immer Teil des Namens ist. - Kapitel-Frontmatter (YAML, `---`-Begrenzer): `book_title`, `book_author`, `language`, `chapter_number`, `chapter_title`, `source_epub`, `source_href` (+ `source_hrefs`, wenn ein Kapitel aus mehreren XHTML-Dateien besteht), `epub_type`, `conversion_status` (`converted` oder `converted-with-warnings`). - Spezialabschnitte (Glossar, Literatur, Sach-/Personenregister, Anhang, Endnoten) erhalten eigene Dateien und werden in `README.md` unter „Besondere Abschnitte“ verlinkt. - Nicht-lineare Spine-Dokumente (`linear="no"`, z. B. Bild-Zoomseiten) bleiben in Spine-Reihenfolge erhalten und sind im Inhaltsverzeichnis als *Zusatzseite* markiert. ## Wie Kapitel gebildet werden 1. Die **OPF-Spine** bestimmt die Lesereihenfolge (nicht Dateinamen, nicht das Inhaltsverzeichnis). 2. Überschriften `h1`…`hN` (N = `--chapter-level`) und semantische Abschnitte (`epub:type`/`role`: chapter, part, appendix, glossary, bibliography, index, preface, …) beginnen neue Kapiteldateien, auch mitten in einer XHTML-Datei. 3. Eine XHTML-Datei, die nur mit einer tieferen Überschrift beginnt (z. B. eine Datei pro Abschnitt `1.2`), wird an das laufende Kapitel angehängt. 4. Dateien ohne Überschrift (Cover, Titelei, Widmung) werden eigene Kapitel. Ihr Titel kommt aus NAV/NCX, `` oder `epub:type`. Fehlt all das, entscheidet der Inhalt: - nur ein Bild am Buchanfang → „Cover“ - Buchtitel und Autorenname → „Titelseite“ - ©/ISBN/„urheberrechtlich“ → „Impressum“ - sonst die ersten acht Wörter - erst als letzte Notlösung der Dateiname 5. Ein HTML-Inhaltsverzeichnis wird übersprungen, wenn NAV/NCX vorhanden ist (keine doppelte Ausgabe). Links darauf zeigen auf `README.md`. 6. Überschriftenebenen werden je Datei verschoben: Der Kapiteltitel ist `#`, alle weiteren beginnen bei `##`. Verlagsspezifische Heuristiken: Layout-Tabellen mit Klassen wie `Head1`/`Title_document` werden zu Überschriften. `<pre>` mit `<br/>`-Zeilen und `div.*programlisting* > span.pre` werden zu Codeblöcken. Literaturlisten mit eigenen Labels (`[BryL06]`) werden nicht doppelt nummeriert. **Tipp:** Hat ein Buch Teile als `h1` und Kapitel als `h2` (typisch bei Romanen), erzeugt `--chapter-level 2` eine Datei pro Kapitel. Mit dem Standard `1` entsteht eine Datei pro Teil. Ist eine Kapiteldatei größer als 30 000 Wörter und hat sie mindestens zwei `##`-Unterabschnitte, meldet epub2md eine Warnung `chapter-large` mit der passenden `--chapter-level`-Empfehlung. ## Math-Modi | Modus | MathML | TeX in `alt`/Annotation | SVG-/Bildformel ohne TeX | | --- | --- | --- | --- | | `auto` | konservativ → TeX `$…$` / `$$…$$`; bei Unklarheit MathML behalten | TeX `$…$` / `$$…$$` | Bild + Warnung | | `tex` | wie auto, aber Begrenzer `\(…\)` / `\[…\]` (nie `$`) | TeX `\(…\)` / `\[…\]` | Bild + Warnung | | `mathml` | MathML als eingebettetes XML (Namespace normalisiert) | Bild (TeX bleibt im Alt-Text) | Bild | | `image` | `altimg`-Bild falls vorhanden, sonst MathML + Warnung | Bild | Bild | - Die MathML→TeX-Umsetzung unterstützt `mi, mn, mo, mtext, mspace, mrow, mstyle, mfrac, msqrt, mroot, msup, msub, msubsup, mover, munder, munderover, mfenced, mtable/mtr/mtd, semantics, maction`. Bei allem anderen (z. B. `menclose`, `mmultiscripts`, `mlabeledtr`) bleibt das MathML erhalten. Lieber MathML als falsches TeX. - TeX-Annotationen (`<annotation encoding="application/x-tex">`) haben Vorrang. - Im `alt`-Text doppelt escapte Zeichen (`&amp;` → `&`) werden repariert. - Gleichungsnummern wie `(3.7)` bleiben als Text sichtbar, Gleichungsanker als `<a id>` erhalten. - Formelbilder, die durch TeX ersetzt wurden, werden nicht extrahiert. Die Anzahl steht im Bericht (`formula_images_replaced_by_tex`). - **Qualität hängt vom EPUB ab:** Echtes MathML/TeX liefert semantische Formeln. Reine SVG-/Bitmap-Formeln bleiben Bilder. ## Fußnoten-Modi Erkannt werden `epub:type="noteref|footnote|endnote|rearnote"`, `role="doc-noteref|doc-footnote|doc-endnote"`, Klassen wie `footnote`/`endnote` und Links auf solche Ziele. Rücklinks innerhalb der Note werden entfernt. Die Note erscheint nicht zusätzlich im Fließtext. | Modus | Ergebnis | | --- | --- | | `end-of-chapter` | `Text[^fn-001]` und `[^fn-001]: …` am Kapitelende. Mehrere Absätze, Code, Formeln und Links bleiben erhalten. Mehrfachverweise nutzen dieselbe Definition | | `end-of-book` | `chapters/999-anmerkungen.md` mit Ankern. Verweise `<sup>[1](999-anmerkungen.md#fn-001)</sup>`, Rücklinks `↩` zu jeder Verweisstelle | | `inline` | `Text (Anmerkung: …)`. Noten mit Blockinhalt (Code, Listen, Tabellen) fallen auf `end-of-chapter` zurück (Hinweis im Bericht) | | `keep-links` | Originalstruktur: Note bleibt an ihrer Stelle, Verweise sind normale Links auf Anker | Mit `--format commonmark` werden Fußnoten als Anker-Links mit Abschnitt „Anmerkungen“ ausgegeben, weil CommonMark keine `[^…]`-Syntax kennt. ## Qualitätshinweise - **Formelbasierte EPUBs:** Am besten sind EPUBs mit MathML oder TeX im `alt`-Text (z. B. Springer). - **SVG-Formeln** bleiben SVG (nie gerastert) und werden im Bericht als Formel markiert. - **Layouttabellen / komplexe Tabellen** (`colspan`, `rowspan`, Blockinhalte in Zellen, verschachtelte Tabellen) bleiben als bereinigtes HTML im Markdown erhalten, mit Warnung. Einfache Tabellen werden GFM-Pipe-Tabellen. Einzellige Layouttabellen werden als normaler Text ausgegeben. - **Syntax-Highlighting:** Farb-Spans, `<strong>`/`<em>` in Listings und Zeilennummern werden entfernt. Einrückung, Tabs, `<`, `>`, `&`, `$`, `|`, Backslashes und Backticks bleiben exakt erhalten. Die Sprache kommt aus Klassen/Attributen (`language-python`, `data-lang`, …) oder starken Inhaltssignalen (`#include`, `def …:`, Shebang, …). Ist sie unsicher, bleibt das Label leer. Bei einsprachigen Büchern kann `--code-language python` (o. Ä.) alle übrigen Blöcke kennzeichnen. Achtung: Das betrifft auch Programmausgaben und Shell-Befehle im Buch. - **Gescannte Texte:** nur Bilder, kein Text (keine OCR). - **DRM:** wird erkannt und abgelehnt. - **Register:** Buchstabengruppen werden zu `##`-Überschriften, Untereinträge erhalten `– `. Interne Registerlinks werden auf Kapiteldatei + Anker umgeschrieben. Seitenzahlen bleiben als Text sichtbar. ## Gesamtdatei erzeugen: `epub2md_merge.py` Fügt die Kapiteldateien einer epub2md-Ausgabe in Lesereihenfolge (aus `manifest.json`) zu **einer** Markdown-Datei zusammen. Die Datei heißt wie das Original-EPUB, aber mit der Endung `.md`. ```bash python3 epub2md_merge.py output/programmierhandbuch # → output/programmierhandbuch/<Original-Name>.md python3 epub2md_merge.py output/programmierhandbuch -o buecher/handbuch.md --toc --force # alternativ: python3 -m epub2md.merge … ``` Eingabe ist ein von `epub2md.py` erzeugtes Verzeichnis. Die Reihenfolge stammt aus `manifest.json`, der Dateiname aus `conversion-report.json`. Fehlen diese Dateien, werden `chapters/*.md` alphabetisch verwendet und das Verzeichnis gibt den Namen vor. | Option | Wirkung | | --- | --- | | `-o`, `--output DATEI.md` | anderer Zielort; Medienpfade werden relativ zum Ziel umgeschrieben | | `--frontmatter` / `--no-frontmatter` | ein YAML-Kopf mit Buchmetadaten (`title`, `author`, `lang`, …) für Pandoc (Standard: an) | | `--toc` | Inhaltsverzeichnis mit Sprunglinks auf die Kapitel einfügen | | `--force` | bestehende Zieldatei überschreiben (sonst Abbruch mit Exit 1) | | `--version` | Programmversion | Beim Zusammenfügen passiert Folgendes: - Die Frontmatter der einzelnen Kapitel wird entfernt. - Jedes Kapitel erhält einen Startanker (`<a id="003-kapitelname"></a>`). - Links zwischen Kapiteln (`003-x.md#anker`) werden zu Sprunglinks in der Gesamtdatei. - Anker, die in mehreren Kapiteln vorkommen (z. B. `Sec1` in jedem Kapitel eines Springer-Buchs), werden ab dem zweiten Vorkommen in `c012-Sec1` umbenannt, samt aller Links darauf. - Medienpfade (`../media/…`) werden auf den Ort der Gesamtdatei umgeschrieben. - Fußnotendefinitionen, die in mehreren Kapiteln stehen, erscheinen nur einmal. - Codeblöcke und Inline-Code werden nie verändert. - Kapitelüberschriften bleiben `#`, jedes Kapitel beginnt also mit einer H1. Pandoc behandelt sie als Kapitel. - Der Buch-Kopf enthält nur Felder, die im EPUB vorhanden sind (`title`, `subtitle`, `author`, `lang`, `publisher`, `date`, `identifier`, `rights`, `description`). Am Ende werden alle Links der Gesamtdatei geprüft. Ein Link auf einen nicht vorhandenen Anker zeigt auf den Kapitelanfang und erzeugt eine Warnung auf stderr. Die Zusammenfassung nennt Kapitelanzahl, umbenannte Anker, entfernte doppelte Fußnoten und ungültige Links. Rückgabecodes: `0` Erfolg, `1` Verzeichnis ungültig/leer, Kapiteldatei fehlt oder Zieldatei existiert, `4` unerwarteter interner Fehler. ## Weiterverarbeitung mit Pandoc Am einfachsten geht das über die Gesamtdatei aus `epub2md_merge.py`. Sie hat einen Buch-Kopf und konsistente Anker: ```bash cd output/programmierbuch # Medienpfade sind relativ zur Gesamtdatei pandoc programmierbuch.md --toc -s -o programmierbuch.html pandoc programmierbuch.md --toc -o programmierbuch.epub cd output/mathematikbuch pandoc mathematikbuch.md --mathml -s -o mathematikbuch.html pandoc mathematikbuch.md --pdf-engine=xelatex -o mathematikbuch.pdf ``` Alternativ direkt aus den Kapiteldateien: ```bash pandoc output/programmierbuch/chapters/*.md \ --metadata-file=output/programmierbuch/metadata.yaml \ -o programmierbuch.html ``` - Für HTML mit Formeln `--mathml`, `--mathjax` oder `--katex` verwenden. Pandocs Standard-HTML kann Matrizen nicht in Unicode umsetzen, meldet „Could not convert TeX math“ und belässt das TeX. - Ausgaben mit `--math tex` (`\(…\)`) brauchen `-f markdown+tex_math_single_backslash`. - Das YAML-Frontmatter jeder Kapiteldatei wird von Pandoc als Metadaten gelesen. Wer die Kapiteldateien direkt zusammen verarbeitet, sollte mit `--no-frontmatter` konvertieren oder die Gesamtdatei verwenden. - Einzelne TeX-Befehle aus EPUBs (z. B. `\O` oder `@`-Konstrukte) kennt Pandocs Formelparser nicht. Er belässt sie als TeX, mit der Warnung „Could not convert TeX math“. ## Bericht und Diagnose `conversion-report.json` enthält u. a. Programmversion, Zeitstempel, Eingabedatei mit SHA-256, EPUB-Version, Optionen, Metadaten, Kapitel mit Statistik, alle Medien, Codeblöcke (Sprache + Erkennungsart), Formeln (Herkunft + Darstellung), Fußnoten (Modus, erkannt/referenziert/ausgegeben), Spezialabschnitte, Warnungen, nicht-fatale Fehler, nicht aufgelöste interne Links, nicht unterstützte Elemente, übersprungene Dokumente und das Ergebnis eines **Linkchecks über die erzeugte Ausgabe**. Dabei wird jeder relative Link und jedes Bild auf Zieldatei und Anker geprüft. `status` ist `success`, `success-with-warnings` oder `success-with-errors`. Bei Warnungen wird nie Verlustfreiheit behauptet (`lossless: false`). ## Sicherheit und Fehlerbehandlung - Das EPUB wird **nie** pauschal entpackt. Einträge werden einzeln gelesen. Absolute Pfade und `..` werden ignoriert und gemeldet (mit `--strict`: Abbruch). Alle Ausgabedateien werden vor dem Schreiben gegen das Zielverzeichnis geprüft. - Schutz gegen ZIP-Bomben: Größenlimit je Eintrag und Prüfung des Kompressionsverhältnisses. - XML wird mit lxml **ohne DTD-Laden, ohne Entity-Auflösung und ohne Netzwerk** geparst (kein XXE). HTML-Entities (` ` …) werden vorab in Zeichenreferenzen umgesetzt. - Ungültiges UTF-8 wird mit Ersatzzeichen gelesen und als Warnung gemeldet. - Keine Netzwerkzugriffe, keine Shell-Aufrufe, keine Telemetrie. - Die Ausgabe entsteht in einem temporären Verzeichnis neben dem Ziel und wird erst am Ende an ihren Platz verschoben. Bei Fehlern wird das temporäre Verzeichnis gelöscht, und es bleibt keine halbe Ausgabe zurück. - **Bestehendes Ausgabeverzeichnis:** Ist es nicht leer, bricht das Programm ab (Exit 1). `--force` ersetzt es nur, wenn es eine frühere epub2md-Ausgabe ist (`conversion-report.json`/`manifest.json` vorhanden), nicht das Arbeits- oder Home-Verzeichnis ist und die Eingabedatei nicht darin liegt. ## Bekannte Einschränkungen - Titel überschriftsloser Seiten werden aus dem Inhalt abgeleitet (Cover, Titelseite, Impressum, erste Wörter). Das ist eine Heuristik und trifft nicht jede Seite. - Aneinanderstoßende Feld-Spans ohne Leerraum (z. B. Autor | Titel | Reihe in Springer-Kapitelköpfen) erhalten ein trennendes Leerzeichen. Das gilt nur bei unterschiedlichen Klassen und wenn rechts ein Großbuchstabe oder eine Ziffer folgt. Andere direkt aneinanderstoßende Inline-Elemente bleiben unverändert, damit Initialen und Wortteile nicht zerrissen werden. - Fehlerhafte Einrückungen der Vorlage werden zeichengetreu übernommen. Druck-Fortsetzungszeichen (`↪`/`➥`) am Zeilenanfang werden dagegen standardmäßig aufgelöst (siehe `--keep-wrap-markers`). - Formelbilder ohne TeX-Alternativtext bleiben Bilder. - Audio/Video sind implementiert, aber nur mit synthetischen Daten geprüft. Bildbände sind mit einem realen Buch geprüft (434 Bilder, 246 Figuren, ausführliche Alternativtexte). - Rein per CSS erzeugte Inhalte (`::before`/`content:`, z. B. automatische Schrittnummern) werden nicht übernommen, weil CSS nicht ausgewertet wird. ## Architektur ```text epub2md.py Einstiegspunkt epub2md_merge.py Einstiegspunkt: Gesamtdatei aus einer Ausgabe erzeugen (epub2md/merge.py) epub2md/ ├── cli.py argparse, Exit-Codes ├── pipeline.py Ablaufsteuerung, Schreiben (atomar), Bericht/Manifest ├── epub_reader.py ZIP-Sicherheit, DRM-Check, container.xml, OPF, NAV, NCX, Guide ├── normalize.py Laden der Spine-Dokumente, Layout-Heuristiken, Seitenumbrüche, HTML-TOC ├── footnotes.py Erkennung/Auslagerung von Fuß- und Endnoten ├── chapters.py Kapitelbildung (Spine + Überschriften + epub:type), Klassifikation ├── references.py Ziel-IDs → Kapiteldatei + Anker, Link-Auflösung ├── html_converter.py rekursiver XHTML→Markdown-Renderer (Blöcke, Inline, Tabellen, Listen, Bilder) ├── codeblocks.py Codeerkennung, exakte Textextraktion, Spracherkennung, Fences ├── mathml.py konservative MathML→TeX-Konvertierung, TeX-Erkennung ├── media.py Medienregistry mit stabilen Namen ├── linkcheck.py Validierung aller relativen Links/Anker der Ausgabe ├── merge.py Gesamtdatei: Reihenfolge, Anker-Umbenennung, Pfade, Fußnoten-Deduplizierung ├── writer.py README, metadata.yaml, Frontmatter, Diagnose-Markdown ├── diagnostics.py zentrale Sammlung von Warnungen/Fehlern ├── models.py Dataclasses (Kapitel, Medien, Noten, Metadaten …) └── utils.py Slugs, YAML-Emitter, Pfadhelfer ``` Statt BeautifulSoup wird direkt lxml verwendet: Es ist für die XHTML-Bäume ausreichend, schneller und ermöglicht die sichere Parser-Konfiguration (XXE). Die Konvertierung ist deterministisch. Gleiche Eingabe und Optionen ergeben identische Kapiteldateien. ## Tests ```bash pip install -r requirements-dev.txt python3 -m pytest -q ruff check epub2md tests && mypy epub2md --ignore-missing-imports # optional, falls installiert ``` Die Tests erzeugen synthetische Minimal-EPUBs (EPUB 2/3, NAV/NCX, Code, Tabellen, MathML, SVG, Fußnoten, Glossar/Literatur/Register, unsichere ZIP-Pfade, DRM, XXE, defekte Container, Kodierung) und prüfen die tatsächliche CLI-Ausgabe. `tests/test_merge.py` prüft die Gesamtdatei: Reihenfolge, Anker-Umbenennung, Pfade, Fußnoten, unveränderter Code und Überschreibschutz. Liegen EPUBs unter `Beispiele/`, werden sie zusätzlich auf Struktur-Invarianten geprüft: keine kaputten Links, balancierte Code-Fences, gültiges YAML, alle Medien vorhanden. Dabei werden keine Buchtexte verglichen. `Beispiele/` ist per `.gitignore` vom Repository ausgeschlossen.