2026-10-05 20:24:47 +02:00
|
|
|
import re
|
|
|
|
|
|
|
|
|
|
import yaml
|
|
|
|
|
|
|
|
|
|
from conftest import PNG, Book, Doc
|
|
|
|
|
from epub2md.linkcheck import anchors_in, check_links
|
|
|
|
|
from epub2md.merge import main as merge_main
|
|
|
|
|
|
|
|
|
|
A = ('<h1 id="sec">Kapitel A</h1><p>Siehe <a href="b.xhtml#sec">B</a>, <a href="b.xhtml">Kapitel B</a>, '
|
|
|
|
|
'<a href="#sec">hier</a> und Anmerkung<a epub:type="noteref" href="b.xhtml#n1">1</a>.</p>'
|
|
|
|
|
'<p><img src="img/a.png" alt="Bild"/></p>'
|
|
|
|
|
'<p><a href="b.xhtml"><img src="img/a.png" alt="Bildlink"/></a></p>'
|
|
|
|
|
'<pre>[kein](link.md) id="sec" `code`</pre>')
|
|
|
|
|
B = ('<h1 id="sec">Kapitel B</h1><p>Zurück zu <a href="a.xhtml#sec">A</a>, nochmal Anmerkung'
|
|
|
|
|
'<a epub:type="noteref" href="#n1">1</a>.</p>'
|
|
|
|
|
'<aside epub:type="footnote" id="n1"><p>Gemeinsame Anmerkung.</p></aside>')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def book() -> Book:
|
|
|
|
|
return Book(title="Merge-Test", docs=[Doc("a.xhtml", A), Doc("b.xhtml", B)], media={"img/a.png": PNG})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_merge_complete_book(convert, tmp_path):
|
|
|
|
|
run = convert(book())
|
|
|
|
|
assert merge_main([str(run.out)]) == 0
|
|
|
|
|
target = run.out / f"{run.report['input']['name'][:-5]}.md"
|
|
|
|
|
text = target.read_text(encoding="utf-8")
|
|
|
|
|
# one book-level frontmatter, no chapter frontmatter
|
|
|
|
|
fm = yaml.safe_load(text.split("---\n")[1])
|
|
|
|
|
assert fm["title"] == "Merge-Test" and fm["author"] == ["Erika Musterfrau"] and fm["lang"] == "de"
|
|
|
|
|
assert text.count("chapter_number") == 0
|
|
|
|
|
# reading order
|
|
|
|
|
assert text.index("# Kapitel A") < text.index("# Kapitel B")
|
|
|
|
|
# links between chapters point into the merged file; colliding id "sec" renamed in chapter B
|
|
|
|
|
assert '<a id="sec"></a>' in text and '<a id="c002-sec"></a>' in text
|
|
|
|
|
assert "[B](#c002-sec)" in text and "[A](#sec)" in text and "[hier](#sec)" in text
|
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
|
|
|
assert "[Kapitel B](#kap-002-kapitel-b)" in text and '<a id="kap-002-kapitel-b"></a>' in text
|
2026-10-05 20:24:47 +02:00
|
|
|
# media paths relative to the merged file, also inside image links
|
|
|
|
|
assert "" in text
|
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
|
|
|
assert "[](#kap-002-kapitel-b)" in text
|
2026-10-05 20:24:47 +02:00
|
|
|
# code untouched
|
|
|
|
|
assert '[kein](link.md) id="sec" `code`' in text
|
|
|
|
|
# footnote defined in both chapters -> only once in the merged file
|
|
|
|
|
assert text.count("[^fn-001]:") == 1
|
|
|
|
|
assert check_links({target.as_posix(): text}, {p.as_posix() for p in run.out.rglob("*") if p.is_file()}) == []
|
|
|
|
|
prose = re.sub(r"^```.*?^```$", "", text, flags=re.M | re.S)
|
|
|
|
|
ids = re.findall(r'id="([^"]+)"', prose)
|
|
|
|
|
assert len(ids) == len(set(ids))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_merge_refuses_overwrite_without_force(convert):
|
|
|
|
|
run = convert(book())
|
|
|
|
|
assert merge_main([str(run.out)]) == 0
|
|
|
|
|
assert merge_main([str(run.out)]) == 1
|
|
|
|
|
assert merge_main([str(run.out), "--force"]) == 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_merge_to_other_location_rewrites_media_paths(convert, tmp_path):
|
|
|
|
|
run = convert(book())
|
|
|
|
|
(tmp_path / "ziel").mkdir()
|
|
|
|
|
target = tmp_path / "ziel" / "buch.md"
|
|
|
|
|
assert merge_main([str(run.out), "-o", str(target), "--toc", "--no-frontmatter"]) == 0
|
|
|
|
|
text = target.read_text(encoding="utf-8")
|
|
|
|
|
assert not text.startswith("---")
|
|
|
|
|
assert f"" in text
|
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
|
|
|
assert "1. [Kapitel A](#kap-001-kapitel-a)" in text
|
|
|
|
|
assert "kap-001-kapitel-a" in anchors_in(text)
|
2026-10-05 20:24:47 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_merge_invalid_directory(tmp_path):
|
|
|
|
|
assert merge_main([str(tmp_path / "gibt-es-nicht")]) == 1
|
|
|
|
|
(tmp_path / "leer").mkdir()
|
|
|
|
|
assert merge_main([str(tmp_path / "leer")]) == 1
|
2026-10-06 06:09:10 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_merge_shortens_iso_date(convert):
|
|
|
|
|
from epub2md.merge import display_date
|
|
|
|
|
assert display_date("2025-01-13T08:00:00+00:00", "de") == "13.01.2025"
|
|
|
|
|
assert display_date("2025-01-13", "de-DE") == "13.01.2025"
|
|
|
|
|
assert display_date("2025-01-13T08:00:00Z", "en") == "2025-01-13"
|
|
|
|
|
assert display_date("2025", "de") == "2025" # year only / other formats stay
|
|
|
|
|
assert display_date("Januar 2025", "de") == "Januar 2025"
|
|
|
|
|
run = convert(book())
|
|
|
|
|
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"
|
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
|
|
|
|
|
|
|
|
|
|
|
|
|
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")
|
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
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_merge_front_matter_region_with_gaps(convert):
|
|
|
|
|
"""Krooss/Blake-like: short untyped pages between title page and imprint; main matter starts at '1 …'."""
|
|
|
|
|
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("auflage.xhtml", "<h1>Vorspann</h1><p>4. Auflage 2025</p>"),
|
|
|
|
|
Doc("impressum.xhtml", "<p>© 2024 Testverlag</p><p>Das Werk ist urheberrechtlich geschützt.</p>"),
|
|
|
|
|
Doc("brief.xhtml", "<h1>Liebe Leserin, lieber Leser,</h1><p>Viel Spaß.</p>"),
|
|
|
|
|
Doc("k1.xhtml", "<h1>1 Der Einstieg</h1><p>a</p>"), Doc("k2.xhtml", "<h1>Zwischenspiel</h1><p>b</p>")]
|
|
|
|
|
book = Book(title="Vorspann", docs=docs, nav=[("1 Der Einstieg", "k1.xhtml"), ("Zwischenspiel", "k2.xhtml")],
|
|
|
|
|
media={"img/a.png": PNG})
|
|
|
|
|
run = convert(book)
|
|
|
|
|
assert merge_main([str(run.out)]) == 0
|
|
|
|
|
text = (run.out / f"{run.report['input']['name'][:-5]}.md").read_text(encoding="utf-8")
|
|
|
|
|
toc = text.index('<div id="epub2md-toc"></div>')
|
|
|
|
|
assert text.index("© 2024") < text.index("Viel Spaß.") < toc < text.index("# 1 Der Einstieg")
|
|
|
|
|
assert text.count("epub2md-toc") == 1 and "epub2md-titlepage" not in text
|