- --split teile|kapitel: Einzeldateien in einem Ordner mit index.md (Reihenfolge) und images/; Titelei und Inhaltsverzeichnis als Vorspann; YAML-Kopf je Datei (title, part, chapter, order, book, author) - --from-md DATEI|ORDNER: EPUB aus bearbeitetem Markdown bzw. Split-Ordner, Angaben aus dem YAML-Kopf; ersetzt den pandoc-Befehl aus Weg B - Markdown beginnt mit YAML-Kopf (title, author, lang, cover); Cover wird mit --images immer abgelegt - --jpeg-quality (Standard 85 statt fest 95): EPUB Band 1 3 MB statt 5 MB - --title/--author überschreiben PDF-Metadaten - Tests (tests/test_split.py), README, Bedienungsanleitung (Weg C), CLAUDE.md, TODO.md Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
146 lines
5.2 KiB
Python
146 lines
5.2 KiB
Python
"""--split, --from-md, YAML-Kopf und --jpeg-quality."""
|
||
|
||
import zipfile
|
||
|
||
import pytest
|
||
|
||
import pdf2md as p
|
||
from conftest import needs_pandoc
|
||
from test_e2e import run
|
||
|
||
BOOK = """# Das Buch
|
||
|
||
## Inhaltsverzeichnis
|
||
|
||
Teil I
|
||
|
||
1 Anfang
|
||
|
||
## Teil I: Der Anfang
|
||
|
||
Einleitung zum Teil.
|
||
|
||
### 1 Anfang
|
||
|
||
Text eins.
|
||
|
||
```
|
||
# kein Kapitel, nur ein Kommentar
|
||
```
|
||
|
||
### 2 Weiter
|
||
|
||
Text zwei.
|
||
|
||
## Impressum
|
||
|
||
Alle Rechte vorbehalten.
|
||
"""
|
||
|
||
|
||
def test_frontmatter_round_trip():
|
||
meta = {"title": "Ein \"Buch\": Teil 1", "author": "Max Muster", "chapter": 3, "leer": ""}
|
||
text = p.make_frontmatter(meta) + "# Text\n"
|
||
parsed, body = p.split_frontmatter(text)
|
||
assert parsed == {"title": "Ein \"Buch\": Teil 1", "author": "Max Muster", "chapter": 3}
|
||
assert body == "# Text\n"
|
||
|
||
|
||
def test_text_without_frontmatter_is_unchanged():
|
||
assert p.split_frontmatter("---\n\nText\n") == ({}, "---\n\nText\n")
|
||
|
||
|
||
def test_slugify():
|
||
assert p.slugify("2 Ein Brief – eine Liste – ein Vermächtnis") == "2-ein-brief-eine-liste-ein-vermaechtnis"
|
||
assert p.slugify("Größe & Maß") == "groesse-mass"
|
||
assert p.slugify("!!!") == "abschnitt"
|
||
|
||
|
||
def test_split_by_chapters():
|
||
sections = p.split_markdown(BOOK, "kapitel")
|
||
assert [s["title"] for s in sections] == [
|
||
"Vorspann", "Teil I: Der Anfang", "1 Anfang", "2 Weiter", "Impressum",
|
||
]
|
||
assert sections[0]["body"].startswith("# Das Buch") and "## Inhaltsverzeichnis" in sections[0]["body"]
|
||
assert sections[2]["part"] == "Teil I: Der Anfang" and sections[2]["chapter"] == 1
|
||
assert "# kein Kapitel" in sections[2]["body"] # Codeblock wird nicht geschnitten
|
||
assert "".join(s["body"] for s in sections).replace("\n", "") == BOOK.replace("\n", "")
|
||
|
||
|
||
def test_split_by_parts():
|
||
sections = p.split_markdown(BOOK, "teile")
|
||
assert [s["title"] for s in sections] == ["Vorspann", "Teil I: Der Anfang", "Impressum"]
|
||
assert "### 2 Weiter" in sections[1]["body"]
|
||
|
||
|
||
def test_write_split_and_read_back(tmp_path):
|
||
sections = p.split_markdown(BOOK, "kapitel")
|
||
files = p.write_split(tmp_path / "buch", sections, {"title": "Das Buch", "author": "Max"})
|
||
assert [f.name for f in files][:3] == ["00_vorspann.md", "01_teil-i-der-anfang.md", "02_1-anfang.md"]
|
||
meta, body, base = p.read_markdown_source(tmp_path / "buch")
|
||
assert meta["title"] == "Das Buch" and base == tmp_path / "buch"
|
||
assert body.replace("\n", "") == BOOK.replace("\n", "")
|
||
head, _ = p.split_frontmatter(files[2].read_text())
|
||
assert head == {"title": "1 Anfang", "part": "Teil I: Der Anfang", "chapter": 1,
|
||
"order": 2, "book": "Das Buch", "author": "Max"}
|
||
|
||
|
||
def test_missing_index_entry_is_reported(tmp_path):
|
||
folder = tmp_path / "buch"
|
||
p.write_split(folder, p.split_markdown(BOOK, "teile"), {"title": "X"})
|
||
(folder / "01_teil-i-der-anfang.md").unlink()
|
||
with pytest.raises(RuntimeError, match="nicht vorhanden"):
|
||
p.read_markdown_source(folder)
|
||
|
||
|
||
# --- Ende zu Ende ------------------------------------------------------------------
|
||
|
||
def test_cli_split_writes_folder(pdfs, tmp_path):
|
||
run(pdfs["digital"], tmp_path / "buch.md", "--split", "kapitel", "--images")
|
||
folder = tmp_path / "buch"
|
||
names = sorted(f.name for f in folder.glob("*.md"))
|
||
assert names[0] == "00_vorspann.md" and "index.md" in names
|
||
assert any(n.endswith("_1-der-notar.md") for n in names)
|
||
assert (folder / "images" / "cover.jpg").is_file()
|
||
text = "".join(f.read_text() for f in folder.glob("*.md"))
|
||
assert "](images/" in text and "_images/" not in text
|
||
|
||
|
||
@needs_pandoc
|
||
def test_from_md_folder_follows_index_order(pdfs, tmp_path):
|
||
run(pdfs["digital"], tmp_path / "buch.md", "--split", "kapitel", "--epub")
|
||
index = tmp_path / "buch" / "index.md"
|
||
lines = index.read_text().split("\n")
|
||
i = next(n for n, line in enumerate(lines) if "1-der-notar" in line)
|
||
j = next(n for n, line in enumerate(lines) if "2-ein-brief" in line)
|
||
lines[i], lines[j] = lines[j], lines[i]
|
||
index.write_text("\n".join(lines))
|
||
|
||
run("--from-md", tmp_path / "buch", "--epub-file", tmp_path / "neu.epub")
|
||
with zipfile.ZipFile(tmp_path / "neu.epub") as z:
|
||
nav = z.read("EPUB/nav.xhtml").decode()
|
||
opf = z.read("EPUB/content.opf").decode()
|
||
assert nav.index("2 Ein Brief") < nav.index("1 Der Notar")
|
||
assert "Erika Muster" in opf and "cover" in opf
|
||
|
||
|
||
@needs_pandoc
|
||
def test_from_md_file_uses_frontmatter_and_overrides(pdfs, tmp_path):
|
||
run(pdfs["digital"], tmp_path / "buch.md", "--images")
|
||
md = tmp_path / "buch.md"
|
||
md.write_text(md.read_text().replace("Der Regen", "Der starke Regen"))
|
||
run("--from-md", md, "--title", "Neuer Titel")
|
||
with zipfile.ZipFile(tmp_path / "buch.epub") as z:
|
||
opf = z.read("EPUB/content.opf").decode()
|
||
text = "".join(z.read(n).decode() for n in z.namelist() if n.endswith(".xhtml"))
|
||
assert ">Neuer Titel<" in opf and "Erika Muster" in opf and "cover" in opf
|
||
assert "Der starke Regen" in text and "title:" not in text
|
||
|
||
|
||
def test_jpeg_quality_changes_image_size(pdfs, tmp_path):
|
||
def size(quality):
|
||
out = tmp_path / f"q{quality}" / "buch.md"
|
||
run(pdfs["digital"], out, "--images", "--jpeg-quality", str(quality))
|
||
return sum(f.stat().st_size for f in (out.parent / "buch_images").glob("*.jpg"))
|
||
|
||
assert size(30) < size(95)
|