feat: epub2md – EPUB nach kapitelweisem Markdown
Kommandozeilenwerkzeug, das DRM-freie EPUB-2/3-Dateien offline in
kapitelweise Markdown-Dateien umwandelt. Erzeugt werden außerdem Medien,
metadata.yaml, manifest.json, conversion-report.json und Diagnose-Dateien.
- Spine-basierte Kapitelbildung mit --chapter-level, epub:type-Abschnitten,
Fortsetzungsdateien und Erkennung von Layout-Überschriften
- Code mit exakter Einrückung, sicheren Fences und konservativer
Spracherkennung
- MathML->TeX (konservativ), TeX aus alt-Texten, SVG-/Bildformeln
bleiben Bilder
- Fußnoten-Modi end-of-chapter, end-of-book, inline und keep-links
- Interne Links/Anker werden umgeschrieben, Linkcheck über die Ausgabe
- Sicher: kein Entpacken, Schutz vor Path Traversal und XXE,
DRM-Erkennung, atomares Schreiben, --force nur für frühere Ausgaben
- 82 pytest-Tests mit synthetischen EPUBs und Invariantentests für Beispiele/
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 19:33:42 +02:00
|
|
|
|
"""Builders for README, metadata.yaml, manifest.json, report and diagnostics files."""
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
import json
|
|
|
|
|
|
import posixpath
|
|
|
|
|
|
import shlex
|
|
|
|
|
|
from collections import Counter
|
2026-10-06 05:32:27 +02:00
|
|
|
|
from pathlib import Path
|
feat: epub2md – EPUB nach kapitelweisem Markdown
Kommandozeilenwerkzeug, das DRM-freie EPUB-2/3-Dateien offline in
kapitelweise Markdown-Dateien umwandelt. Erzeugt werden außerdem Medien,
metadata.yaml, manifest.json, conversion-report.json und Diagnose-Dateien.
- Spine-basierte Kapitelbildung mit --chapter-level, epub:type-Abschnitten,
Fortsetzungsdateien und Erkennung von Layout-Überschriften
- Code mit exakter Einrückung, sicheren Fences und konservativer
Spracherkennung
- MathML->TeX (konservativ), TeX aus alt-Texten, SVG-/Bildformeln
bleiben Bilder
- Fußnoten-Modi end-of-chapter, end-of-book, inline und keep-links
- Interne Links/Anker werden umgeschrieben, Linkcheck über die Ausgabe
- Sicher: kein Entpacken, Schutz vor Path Traversal und XXE,
DRM-Erkennung, atomares Schreiben, --force nur für frühere Ausgaben
- 82 pytest-Tests mit synthetischen EPUBs und Invariantentests für Beispiele/
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 19:33:42 +02:00
|
|
|
|
from typing import Any
|
|
|
|
|
|
|
|
|
|
|
|
from . import TOOL_NAME, __version__
|
|
|
|
|
|
from .diagnostics import Diagnostics
|
|
|
|
|
|
from .html_converter import md_escape
|
|
|
|
|
|
from .models import BookMetadata, Chapter, NavEntry
|
|
|
|
|
|
from .references import LinkResolver
|
|
|
|
|
|
from .utils import rel_link, to_yaml, url_path
|
|
|
|
|
|
|
2026-10-06 05:32:27 +02:00
|
|
|
|
CALLOUT_FILTER = Path(__file__).resolve().parent.parent / "pandoc" / "callouts.lua"
|
2026-10-06 06:03:55 +02:00
|
|
|
|
PRINT_CSS = Path(__file__).resolve().parent.parent / "pandoc" / "print.css"
|
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>
2026-10-06 06:27:27 +02:00
|
|
|
|
BOOK_FILTER = Path(__file__).resolve().parent.parent / "pandoc" / "book.lua"
|
2026-10-06 05:32:27 +02:00
|
|
|
|
|
feat: epub2md – EPUB nach kapitelweisem Markdown
Kommandozeilenwerkzeug, das DRM-freie EPUB-2/3-Dateien offline in
kapitelweise Markdown-Dateien umwandelt. Erzeugt werden außerdem Medien,
metadata.yaml, manifest.json, conversion-report.json und Diagnose-Dateien.
- Spine-basierte Kapitelbildung mit --chapter-level, epub:type-Abschnitten,
Fortsetzungsdateien und Erkennung von Layout-Überschriften
- Code mit exakter Einrückung, sicheren Fences und konservativer
Spracherkennung
- MathML->TeX (konservativ), TeX aus alt-Texten, SVG-/Bildformeln
bleiben Bilder
- Fußnoten-Modi end-of-chapter, end-of-book, inline und keep-links
- Interne Links/Anker werden umgeschrieben, Linkcheck über die Ausgabe
- Sicher: kein Entpacken, Schutz vor Path Traversal und XXE,
DRM-Erkennung, atomares Schreiben, --force nur für frühere Ausgaben
- 82 pytest-Tests mit synthetischen EPUBs und Invariantentests für Beispiele/
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 19:33:42 +02:00
|
|
|
|
SPECIAL_LABELS = {
|
|
|
|
|
|
"glossary": "Glossar",
|
|
|
|
|
|
"bibliography": "Literatur / Quellen",
|
|
|
|
|
|
"index": "Sachregister",
|
|
|
|
|
|
"index-persons": "Personenregister",
|
|
|
|
|
|
"endnotes": "Anmerkungen",
|
|
|
|
|
|
"appendix": "Anhang",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def chapter_frontmatter(ch: Chapter, md: BookMetadata, source_epub: str, opf_dir: str, warnings: int) -> str:
|
|
|
|
|
|
def rel(p: str) -> str:
|
|
|
|
|
|
return p[len(opf_dir) + 1 :] if opf_dir and p.startswith(opf_dir + "/") else p
|
|
|
|
|
|
|
|
|
|
|
|
data: dict[str, Any] = {
|
|
|
|
|
|
"book_title": md.title,
|
|
|
|
|
|
"book_author": md.authors,
|
|
|
|
|
|
"language": md.language or None,
|
|
|
|
|
|
"chapter_number": ch.number,
|
|
|
|
|
|
"chapter_title": ch.title,
|
|
|
|
|
|
"source_epub": source_epub,
|
|
|
|
|
|
"source_href": rel(ch.docs[0]) if ch.docs else None,
|
|
|
|
|
|
}
|
|
|
|
|
|
if len(ch.docs) > 1:
|
|
|
|
|
|
data["source_hrefs"] = [rel(d) for d in ch.docs]
|
|
|
|
|
|
data["epub_type"] = ch.types
|
|
|
|
|
|
data["conversion_status"] = "converted-with-warnings" if warnings else "converted"
|
|
|
|
|
|
return "---\n" + to_yaml(data) + "---\n\n"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def metadata_yaml(md: BookMetadata, source_name: str, sha256: str, epub_version: str, cover: str,
|
|
|
|
|
|
options: dict[str, Any], generated_at: str) -> str:
|
|
|
|
|
|
data: dict[str, Any] = {}
|
|
|
|
|
|
|
|
|
|
|
|
def put(key: str, value: Any) -> None:
|
|
|
|
|
|
if value not in ("", None, []):
|
|
|
|
|
|
data[key] = value
|
|
|
|
|
|
|
|
|
|
|
|
put("title", md.title)
|
|
|
|
|
|
put("subtitle", md.subtitle)
|
|
|
|
|
|
put("authors", md.authors)
|
|
|
|
|
|
put("contributors", md.contributors)
|
|
|
|
|
|
put("language", md.language)
|
|
|
|
|
|
put("publisher", md.publisher)
|
|
|
|
|
|
put("published", md.published)
|
|
|
|
|
|
put("identifier", md.identifier)
|
|
|
|
|
|
put("isbn", md.isbn)
|
|
|
|
|
|
data["source_file"] = source_name
|
|
|
|
|
|
data["source_sha256"] = sha256
|
|
|
|
|
|
put("epub_version", epub_version)
|
|
|
|
|
|
put("description", md.description)
|
|
|
|
|
|
put("rights", md.rights)
|
|
|
|
|
|
put("subjects", md.subjects)
|
|
|
|
|
|
put("cover_image", cover)
|
|
|
|
|
|
data["conversion"] = {"tool": TOOL_NAME, "version": __version__, "generated_at": generated_at, "options": options}
|
|
|
|
|
|
return to_yaml(data)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _nav_lines(entries: list[NavEntry], resolver: LinkResolver, depth: int = 0) -> list[str]:
|
|
|
|
|
|
lines = []
|
|
|
|
|
|
for e in entries:
|
|
|
|
|
|
label = md_escape(e.label or "(ohne Titel)")
|
|
|
|
|
|
url = None
|
|
|
|
|
|
if e.path:
|
|
|
|
|
|
url, _ = resolver.resolve("README.md", e.path, ("#" + e.fragment) if e.fragment else "")
|
|
|
|
|
|
lines.append(" " * depth + (f"- [{label}]({url})" if url else f"- {label}"))
|
|
|
|
|
|
lines.extend(_nav_lines(e.children, resolver, depth + 1))
|
|
|
|
|
|
return lines
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def readme(md: BookMetadata, chapters: list[Chapter], endnotes: Chapter | None, nav: list[NavEntry],
|
|
|
|
|
|
resolver: LinkResolver, stats: dict[str, int], cover: str, epub_version: str, source_name: str,
|
2026-10-06 05:32:27 +02:00
|
|
|
|
command: list[str], output_dir: str = "OUTPUT_VERZEICHNIS", callouts: int = 0) -> str:
|
feat: epub2md – EPUB nach kapitelweisem Markdown
Kommandozeilenwerkzeug, das DRM-freie EPUB-2/3-Dateien offline in
kapitelweise Markdown-Dateien umwandelt. Erzeugt werden außerdem Medien,
metadata.yaml, manifest.json, conversion-report.json und Diagnose-Dateien.
- Spine-basierte Kapitelbildung mit --chapter-level, epub:type-Abschnitten,
Fortsetzungsdateien und Erkennung von Layout-Überschriften
- Code mit exakter Einrückung, sicheren Fences und konservativer
Spracherkennung
- MathML->TeX (konservativ), TeX aus alt-Texten, SVG-/Bildformeln
bleiben Bilder
- Fußnoten-Modi end-of-chapter, end-of-book, inline und keep-links
- Interne Links/Anker werden umgeschrieben, Linkcheck über die Ausgabe
- Sicher: kein Entpacken, Schutz vor Path Traversal und XXE,
DRM-Erkennung, atomares Schreiben, --force nur für frühere Ausgaben
- 82 pytest-Tests mit synthetischen EPUBs und Invariantentests für Beispiele/
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 19:33:42 +02:00
|
|
|
|
out = [f"# {md_escape(md.title or source_name)}"]
|
|
|
|
|
|
if md.subtitle:
|
|
|
|
|
|
out.append(f"*{md_escape(md.subtitle)}*")
|
|
|
|
|
|
if md.authors:
|
|
|
|
|
|
out.append("**" + md_escape(", ".join(md.authors)) + "**")
|
|
|
|
|
|
if cover:
|
|
|
|
|
|
out.append(f"})")
|
|
|
|
|
|
rows = [("Sprache", md.language), ("Verlag", md.publisher), ("Veröffentlicht", md.published),
|
|
|
|
|
|
("Identifier", md.identifier), ("ISBN", md.isbn), ("EPUB-Version", epub_version),
|
|
|
|
|
|
("Quelldatei", source_name)]
|
|
|
|
|
|
table = ["| Feld | Wert |", "| --- | --- |"] + [
|
|
|
|
|
|
f"| {k} | {md_escape(v, table=True)} |" for k, v in rows if v
|
|
|
|
|
|
]
|
|
|
|
|
|
out.append("\n".join(table))
|
|
|
|
|
|
out.append("## Inhaltsverzeichnis")
|
|
|
|
|
|
lines = [f"{c.number}. [{md_escape(c.title)}]({url_path(c.filename)})"
|
|
|
|
|
|
+ (" *(Zusatzseite, nicht in linearer Lesereihenfolge)*" if "auxiliary" in c.types else "")
|
|
|
|
|
|
for c in chapters]
|
|
|
|
|
|
if endnotes is not None:
|
|
|
|
|
|
lines.append(f"{len(chapters) + 1}. [{md_escape(endnotes.title)}]({url_path(endnotes.filename)})")
|
|
|
|
|
|
out.append("\n".join(lines) if lines else "*(keine Kapitel)*")
|
|
|
|
|
|
specials = [(c, t) for c in chapters for t in c.types if t in SPECIAL_LABELS]
|
|
|
|
|
|
if specials or endnotes is not None:
|
|
|
|
|
|
out.append("## Besondere Abschnitte")
|
|
|
|
|
|
sl = [f"- {SPECIAL_LABELS[t]}: [{md_escape(c.title)}]({url_path(c.filename)})" for c, t in specials]
|
|
|
|
|
|
if endnotes is not None:
|
|
|
|
|
|
sl.append(f"- Anmerkungen (gesammelt): [{md_escape(endnotes.title)}]({url_path(endnotes.filename)})")
|
|
|
|
|
|
out.append("\n".join(sl))
|
|
|
|
|
|
if nav:
|
|
|
|
|
|
out.append("## Original-Inhaltsverzeichnis (NAV/NCX)")
|
|
|
|
|
|
out.append("\n".join(_nav_lines(nav, resolver)))
|
|
|
|
|
|
out.append("## Statistik")
|
|
|
|
|
|
labels = [("chapters", "Kapitel"), ("images", "Bilder"), ("svgs", "SVG-Grafiken"),
|
|
|
|
|
|
("media_files", "Extrahierte Mediendateien"), ("code_blocks", "Codeblöcke"),
|
|
|
|
|
|
("formulas", "Erkannte Formeln"), ("formulas_as_image", "Formeln als Bild/SVG belassen"),
|
|
|
|
|
|
("formulas_as_mathml", "Formeln als MathML belassen"), ("footnotes", "Fußnoten/Anmerkungen"),
|
|
|
|
|
|
("warnings", "Warnungen")]
|
|
|
|
|
|
out.append("\n".join(["| Kennzahl | Anzahl |", "| --- | ---: |"] + [f"| {lbl} | {stats.get(k, 0)} |" for k, lbl in labels]))
|
|
|
|
|
|
out.append("Details: [conversion-report.json](conversion-report.json), "
|
|
|
|
|
|
"[manifest.json](manifest.json), [Warnungen](diagnostics/warnings.md)")
|
2026-10-05 20:48:41 +02:00
|
|
|
|
stem = source_name[:-5] if source_name.lower().endswith(".epub") else source_name
|
|
|
|
|
|
math = " --mathml" if stats.get("formulas") else ""
|
2026-10-06 05:32:27 +02:00
|
|
|
|
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))}"
|
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>
2026-10-06 06:27:27 +02:00
|
|
|
|
# book.lua: table of contents after cover/title page/imprint instead of Pandoc's --toc at the very top
|
feat: Buchanfang Cover → Titel → Impressum → Inhaltsverzeichnis für alle Bücher
- Kapitelerkennung: erstes Kapitel nur aus einem Bild = Cover ("[Titelinfo]",
"Deckblatt"), Titel "Titel" = Titelseite
- epub2md_merge: Vorspann reicht bis vor den Hauptteil (Teil, Vorwort,
Einleitung, nummerierter Titel, > 1000 Wörter), kurze Zwischenseiten
(Auflage, Verlagsbrief, "Über dieses Buch") bleiben davor
- book.lua: Cover am Inhalt erkannt; LaTeX-Weg (Filter in beiden Schritten):
\tableofcontents hinter dem Vorspann, \clearpage je Kapitel, kein \maketitle
vor dem Cover, A4, seitengroßes Cover, DejaVu-Schriften und Mathe-Symbole
für fehlende Glyphen (Mathebuch: >10.000 fehlende Zeichen → 0)
- Buch-README: bei Formeln LaTeX-Weg empfohlen, sonst weasyprint
- Alle sechs Beispielbücher geprüft: Seite 1 Cover, Verzeichnis nach dem Vorspann
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 06:48:18 +02:00
|
|
|
|
book = f" --lua-filter {shlex.quote(str(BOOK_FILTER))}" if BOOK_FILTER.is_file() else ""
|
|
|
|
|
|
toc = book or " --toc"
|
|
|
|
|
|
md_, html_, pdf_ = (shlex.quote(stem + ext) for ext in (".md", ".html", ".pdf"))
|
|
|
|
|
|
latex = [
|
|
|
|
|
|
"# PDF mit LaTeX über HTML: Pandoc übernimmt dann auch HTML-Tabellen und interne Sprungziele",
|
|
|
|
|
|
f"pandoc {md_} -s{math}{book}{lua} -o {html_} && pandoc {html_}{toc}{lua} --pdf-engine=xelatex -o {pdf_}",
|
|
|
|
|
|
]
|
|
|
|
|
|
weasy = [
|
|
|
|
|
|
"# PDF mit weasyprint: Farben, Kästen und HTML-Stile bleiben erhalten" + ("" if math else " (empfohlen)"),
|
|
|
|
|
|
f"pandoc {md_} -s{math}{toc}{lua} --css {shlex.quote(str(PRINT_CSS))} --pdf-engine=weasyprint -o {pdf_}",
|
|
|
|
|
|
] if PRINT_CSS.is_file() else []
|
|
|
|
|
|
if math:
|
|
|
|
|
|
latex[0] += " (empfohlen: weasyprint kann keine Formeln setzen)"
|
2026-10-05 20:48:41 +02:00
|
|
|
|
commands = [
|
|
|
|
|
|
"# Gesamtdatei erzeugen (im selben Arbeitsverzeichnis wie die Konvertierung)",
|
|
|
|
|
|
f"python3 epub2md_merge.py {shlex.quote(output_dir)}",
|
|
|
|
|
|
"# danach in diesem Ordner:",
|
feat: Buchanfang Cover → Titel → Impressum → Inhaltsverzeichnis für alle Bücher
- Kapitelerkennung: erstes Kapitel nur aus einem Bild = Cover ("[Titelinfo]",
"Deckblatt"), Titel "Titel" = Titelseite
- epub2md_merge: Vorspann reicht bis vor den Hauptteil (Teil, Vorwort,
Einleitung, nummerierter Titel, > 1000 Wörter), kurze Zwischenseiten
(Auflage, Verlagsbrief, "Über dieses Buch") bleiben davor
- book.lua: Cover am Inhalt erkannt; LaTeX-Weg (Filter in beiden Schritten):
\tableofcontents hinter dem Vorspann, \clearpage je Kapitel, kein \maketitle
vor dem Cover, A4, seitengroßes Cover, DejaVu-Schriften und Mathe-Symbole
für fehlende Glyphen (Mathebuch: >10.000 fehlende Zeichen → 0)
- Buch-README: bei Formeln LaTeX-Weg empfohlen, sonst weasyprint
- Alle sechs Beispielbücher geprüft: Seite 1 Cover, Verzeichnis nach dem Vorspann
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 06:48:18 +02:00
|
|
|
|
f"pandoc {md_} -s{math}{toc}{lua} -o {html_}",
|
fix: Pandoc-Weg – EPUB-Links, Formeln im EPUB, portables TeX (QA3-04 bis QA3-06)
Prüfung aller 5 Beispielbücher über die Befehle der Buch-README (HTML,
EPUB, PDF mit weasyprint und xelatex):
- QA3-04: Der EPUB-Befehl der Buch-README hatte kein --mathml; im
Mathebuch blieben 1072 Formeln als TeX-Text. Jetzt 4904 als MathML.
- QA3-05: Kapitel-Startanker von epub2md_merge.py begannen mit einer
Ziffer (039-…). Pandocs EPUB-Writer löst Links darauf nicht auf (C-Buch
32, Mathebuch 20 tote Links). Jetzt kap-039-…; book.lua erkennt beide.
- QA3-06: portable_tex() passt Verlags-TeX für texmath an (\"u, \O,
@{…} und \arraycolsep in array, leeres \displaystyle vor Zellenende).
Mathebuch: 1 statt 9 nicht umsetzbare Formeln.
- Cover ohne Seitenzahl (print.css, book.lua).
Neue Tests: tests/test_pandoc.py (nur mit installiertem pandoc),
Parametertests für portable_tex. Andere Bücher byte-identisch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 21:41:13 +02:00
|
|
|
|
f"pandoc {md_} --toc{math}{lua} -o {shlex.quote(stem + '.epub')}",
|
feat: Buchanfang Cover → Titel → Impressum → Inhaltsverzeichnis für alle Bücher
- Kapitelerkennung: erstes Kapitel nur aus einem Bild = Cover ("[Titelinfo]",
"Deckblatt"), Titel "Titel" = Titelseite
- epub2md_merge: Vorspann reicht bis vor den Hauptteil (Teil, Vorwort,
Einleitung, nummerierter Titel, > 1000 Wörter), kurze Zwischenseiten
(Auflage, Verlagsbrief, "Über dieses Buch") bleiben davor
- book.lua: Cover am Inhalt erkannt; LaTeX-Weg (Filter in beiden Schritten):
\tableofcontents hinter dem Vorspann, \clearpage je Kapitel, kein \maketitle
vor dem Cover, A4, seitengroßes Cover, DejaVu-Schriften und Mathe-Symbole
für fehlende Glyphen (Mathebuch: >10.000 fehlende Zeichen → 0)
- Buch-README: bei Formeln LaTeX-Weg empfohlen, sonst weasyprint
- Alle sechs Beispielbücher geprüft: Seite 1 Cover, Verzeichnis nach dem Vorspann
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 06:48:18 +02:00
|
|
|
|
] + (latex + weasy if math else weasy + latex)
|
2026-10-05 20:48:41 +02:00
|
|
|
|
out.append("## Weiterverarbeitung")
|
|
|
|
|
|
out.append("```bash\n" + "\n".join(commands) + "\n```")
|
2026-10-06 05:32:27 +02:00
|
|
|
|
if callouts:
|
|
|
|
|
|
noun = "Infokasten" if callouts == 1 else "Infokästen"
|
|
|
|
|
|
out.append(f"Das Buch enthält {callouts} {noun} (`> [!TIP]` …). Der Filter `callouts.lua` macht daraus "
|
|
|
|
|
|
"in HTML, EPUB und PDF gerahmte Kästen mit Titel; ohne ihn zeigt Pandoc „[!TIP]“ als Text.")
|
|
|
|
|
|
if stats.get("formulas"):
|
fix: Pandoc-Weg – EPUB-Links, Formeln im EPUB, portables TeX (QA3-04 bis QA3-06)
Prüfung aller 5 Beispielbücher über die Befehle der Buch-README (HTML,
EPUB, PDF mit weasyprint und xelatex):
- QA3-04: Der EPUB-Befehl der Buch-README hatte kein --mathml; im
Mathebuch blieben 1072 Formeln als TeX-Text. Jetzt 4904 als MathML.
- QA3-05: Kapitel-Startanker von epub2md_merge.py begannen mit einer
Ziffer (039-…). Pandocs EPUB-Writer löst Links darauf nicht auf (C-Buch
32, Mathebuch 20 tote Links). Jetzt kap-039-…; book.lua erkennt beide.
- QA3-06: portable_tex() passt Verlags-TeX für texmath an (\"u, \O,
@{…} und \arraycolsep in array, leeres \displaystyle vor Zellenende).
Mathebuch: 1 statt 9 nicht umsetzbare Formeln.
- Cover ohne Seitenzahl (print.css, book.lua).
Neue Tests: tests/test_pandoc.py (nur mit installiertem pandoc),
Parametertests für portable_tex. Andere Bücher byte-identisch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 21:41:13 +02:00
|
|
|
|
out.append("Das Buch enthält Formeln: Für HTML und EPUB `--mathml` (bei HTML auch `--mathjax`) verwenden, sonst belässt "
|
2026-10-05 20:48:41 +02:00
|
|
|
|
"Pandoc Matrizen und ähnliche Konstrukte als TeX-Text.")
|
feat: epub2md – EPUB nach kapitelweisem Markdown
Kommandozeilenwerkzeug, das DRM-freie EPUB-2/3-Dateien offline in
kapitelweise Markdown-Dateien umwandelt. Erzeugt werden außerdem Medien,
metadata.yaml, manifest.json, conversion-report.json und Diagnose-Dateien.
- Spine-basierte Kapitelbildung mit --chapter-level, epub:type-Abschnitten,
Fortsetzungsdateien und Erkennung von Layout-Überschriften
- Code mit exakter Einrückung, sicheren Fences und konservativer
Spracherkennung
- MathML->TeX (konservativ), TeX aus alt-Texten, SVG-/Bildformeln
bleiben Bilder
- Fußnoten-Modi end-of-chapter, end-of-book, inline und keep-links
- Interne Links/Anker werden umgeschrieben, Linkcheck über die Ausgabe
- Sicher: kein Entpacken, Schutz vor Path Traversal und XXE,
DRM-Erkennung, atomares Schreiben, --force nur für frühere Ausgaben
- 82 pytest-Tests mit synthetischen EPUBs und Invariantentests für Beispiele/
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 19:33:42 +02:00
|
|
|
|
out.append("## Reproduktion")
|
|
|
|
|
|
out.append("```bash\n" + " ".join(shlex.quote(c) for c in command) + "\n```")
|
|
|
|
|
|
return "\n\n".join(out) + "\n"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def warnings_md(diag: Diagnostics) -> str:
|
|
|
|
|
|
out = ["# Warnungen und Hinweise", ""]
|
|
|
|
|
|
levels = Counter(d.level for d in diag.items)
|
|
|
|
|
|
out.append(f"Fehler: {levels.get('error', 0)} · Warnungen: {levels.get('warning', 0)} · Hinweise: {levels.get('info', 0)}")
|
|
|
|
|
|
if not diag.items:
|
|
|
|
|
|
out += ["", "Keine Auffälligkeiten."]
|
|
|
|
|
|
return "\n".join(out) + "\n"
|
|
|
|
|
|
by_code: dict[str, list[Any]] = {}
|
|
|
|
|
|
for d in diag.items:
|
|
|
|
|
|
by_code.setdefault(f"{d.level}: {d.code}", []).append(d)
|
|
|
|
|
|
order = {"error": 0, "warning": 1, "info": 2}
|
|
|
|
|
|
for key in sorted(by_code, key=lambda k: (order.get(k.split(":")[0], 3), k)):
|
|
|
|
|
|
items = by_code[key]
|
|
|
|
|
|
out += ["", f"## {key} ({len(items)})", ""]
|
|
|
|
|
|
for d in items:
|
|
|
|
|
|
where = " · ".join(x for x in (d.source, d.chapter) if x)
|
2026-10-06 00:24:35 +02:00
|
|
|
|
times = f" (×{d.count})" if d.count > 1 else ""
|
|
|
|
|
|
out.append(f"- {md_escape(d.message)}{times}" + (f" (`{where}`)" if where else ""))
|
feat: epub2md – EPUB nach kapitelweisem Markdown
Kommandozeilenwerkzeug, das DRM-freie EPUB-2/3-Dateien offline in
kapitelweise Markdown-Dateien umwandelt. Erzeugt werden außerdem Medien,
metadata.yaml, manifest.json, conversion-report.json und Diagnose-Dateien.
- Spine-basierte Kapitelbildung mit --chapter-level, epub:type-Abschnitten,
Fortsetzungsdateien und Erkennung von Layout-Überschriften
- Code mit exakter Einrückung, sicheren Fences und konservativer
Spracherkennung
- MathML->TeX (konservativ), TeX aus alt-Texten, SVG-/Bildformeln
bleiben Bilder
- Fußnoten-Modi end-of-chapter, end-of-book, inline und keep-links
- Interne Links/Anker werden umgeschrieben, Linkcheck über die Ausgabe
- Sicher: kein Entpacken, Schutz vor Path Traversal und XXE,
DRM-Erkennung, atomares Schreiben, --force nur für frühere Ausgaben
- 82 pytest-Tests mit synthetischen EPUBs und Invariantentests für Beispiele/
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 19:33:42 +02:00
|
|
|
|
return "\n".join(out) + "\n"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def unsupported_md(diag: Diagnostics) -> str:
|
|
|
|
|
|
out = ["# Nicht direkt unterstützte Elemente", ""]
|
|
|
|
|
|
if not diag.unsupported:
|
|
|
|
|
|
out.append("Keine.")
|
|
|
|
|
|
else:
|
|
|
|
|
|
out.append("Inhalte dieser Elemente wurden als Text übernommen (mit `--keep-html` als HTML).")
|
|
|
|
|
|
out += ["", "| Element | Anzahl | Beispiel |", "| --- | ---: | --- |"]
|
|
|
|
|
|
for tag, n in diag.unsupported.most_common():
|
|
|
|
|
|
out.append(f"| `{tag}` | {n} | `{diag.unsupported_examples.get(tag, '')}` |")
|
|
|
|
|
|
return "\n".join(out) + "\n"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def to_json(data: Any) -> str:
|
|
|
|
|
|
return json.dumps(data, ensure_ascii=False, indent=2) + "\n"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def rel(from_file: str, to: str) -> str:
|
|
|
|
|
|
return rel_link(from_file, to) or posixpath.basename(to)
|