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>
This commit is contained in:
Dieter Schlüter 2026-10-06 21:41:13 +02:00
commit 64d0cb0357
3 changed files with 157 additions and 18 deletions

View file

@ -2,7 +2,7 @@ import pytest
from lxml import etree
from conftest import PNG, SVG, Book, Doc
from epub2md.mathml import Unsupported, mathml_to_tex
from epub2md.mathml import Unsupported, mathml_to_tex, portable_tex
M = 'xmlns="http://www.w3.org/1998/Math/MathML"'
@ -111,6 +111,24 @@ def test_book_readme_suggests_mathml_for_pandoc_only_with_formulas(convert):
readme = (run.out / "README.md").read_text(encoding="utf-8")
assert "## Weiterverarbeitung" in readme and "epub2md_merge.py" in readme
assert "--mathml" in readme
pandoc_lines = [ln for ln in readme.splitlines() if ln.startswith("pandoc ")]
assert pandoc_lines and all("--mathml" in ln for ln in pandoc_lines) # EPUB too, else TeX stays text (QA3-04)
plain = convert(Book(docs=[Doc("a.xhtml", "<h1>A</h1><p>Text</p>")]))
plain_readme = (plain.out / "README.md").read_text(encoding="utf-8")
assert "## Weiterverarbeitung" in plain_readme and "--mathml" not in plain_readme
@pytest.mark.parametrize("tex,expected", [
(r'\mbox{gr\"un}, \text{F\"{u}r}', r"\mbox{grün}, \text{Für}"),
(r'\text{ f{\"u}r } j', r"\text{ f{ü}r } j"), # braces stay balanced
(r"{\mathscr L}(A,b)={{\O }}", r"{\mathscr L}(A,b)={{\emptyset }}"),
(r"M\cap N=\text{\O }", r"M\cap N=\emptyset"),
(r"\Omega\neq\O", r"\Omega\neq\emptyset"),
(r"\begin{array}{@{}l@{\qquad \quad }l@{}} a & b\end{array}", r"\begin{array}{ll} a & b\end{array}"),
(r"\arraycolsep8pt\begin{array}{c@{\,}r} x\end{array}", r"\begin{array}{cr} x\end{array}"),
(r"a & =&\displaystyle b, \\ & \vdots &\displaystyle \\ c", r"a & =&\displaystyle b, \\ & \vdots &\\ c"),
(r"\frac{a}{b}+\{x\}", r"\frac{a}{b}+\{x\}"),
])
def test_publisher_tex_is_made_portable(tex, expected):
"""Pandoc's texmath rejects \\", \\O, @{…} and \\arraycolsep (Springer alt texts, QA3-06)."""
assert portable_tex(tex) == expected

View file

@ -34,10 +34,10 @@ def test_merge_complete_book(convert, tmp_path):
# 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
assert "[Kapitel B](#002-kapitel-b)" in text and '<a id="002-kapitel-b"></a>' in text
assert "[Kapitel B](#kap-002-kapitel-b)" in text and '<a id="kap-002-kapitel-b"></a>' in text
# media paths relative to the merged file, also inside image links
assert "![Bild](media/images/0001-bild.png)" in text
assert "[![Bildlink](media/images/0001-bild.png)](#002-kapitel-b)" in text
assert "[![Bildlink](media/images/0001-bild.png)](#kap-002-kapitel-b)" in text
# code untouched
assert '[kein](link.md) id="sec" `code`' in text
# footnote defined in both chapters -> only once in the merged file
@ -63,8 +63,8 @@ def test_merge_to_other_location_rewrites_media_paths(convert, tmp_path):
text = target.read_text(encoding="utf-8")
assert not text.startswith("---")
assert f"![Bild](../{run.out.name}/media/images/0001-bild.png)" in text
assert "1. [Kapitel A](#001-kapitel-a)" in text
assert "001-kapitel-a" in anchors_in(text)
assert "1. [Kapitel A](#kap-001-kapitel-a)" in text
assert "kap-001-kapitel-a" in anchors_in(text)
def test_merge_invalid_directory(tmp_path):

60
tests/test_pandoc.py Normal file
View file

@ -0,0 +1,60 @@
"""Pandoc path: merged file → EPUB/HTML with the filters from pandoc/ (skipped without pandoc)."""
import re
import shutil
import subprocess
import zipfile
from pathlib import Path
import pytest
from conftest import Book, Doc
from epub2md.merge import main as merge_main
pytestmark = pytest.mark.skipif(shutil.which("pandoc") is None, reason="pandoc nicht installiert")
FILTERS = Path(__file__).resolve().parent.parent / "pandoc"
def merged(convert, book: Book) -> Path:
run = convert(book)
assert merge_main([str(run.out)]) == 0
return run.out / f"{run.report['input']['name'][:-5]}.md"
def pandoc(md: Path, *args: str) -> subprocess.CompletedProcess:
return subprocess.run(["pandoc", md.name, *args], cwd=md.parent, capture_output=True, text=True, check=True)
def links_and_ids(epub: Path) -> tuple[list[tuple[str, str]], dict[str, set[str]]]:
with zipfile.ZipFile(epub) as z:
docs = {n: z.read(n).decode("utf-8") for n in z.namelist() if n.endswith(".xhtml")}
ids = {n: set(re.findall(r'\bid="([^"]+)"', t)) for n, t in docs.items()}
links = [(n, h) for n, t in docs.items() for h in re.findall(r'href="([^"]+)"', t)
if not re.match(r"^[a-z]+:", h) and not h.endswith(".css")]
return links, ids
def test_epub_links_to_chapter_starts_resolve(convert):
"""Chapter start anchors must begin with a letter: Pandoc's EPUB writer breaks '#039-…' (QA3-05)."""
docs = [Doc("a.xhtml", '<h1>Kapitel A</h1><p>Siehe <a href="b.xhtml">Kapitel B</a> und <a href="c.xhtml">Bild</a>.</p>'),
Doc("b.xhtml", "<h1>Kapitel B</h1><p>Text B.</p>"),
Doc("c.xhtml", "<p>Nur eine Bildseite ohne Überschrift.</p>")]
md = merged(convert, Book(docs=docs))
pandoc(md, "--toc", "-o", "buch.epub")
links, ids = links_and_ids(md.parent / "buch.epub")
broken = []
for doc, href in links:
file, _, frag = href.partition("#")
target = str(Path(doc).parent / file).replace("\\", "/") if file else doc
target = re.sub(r"[^/]+/\.\./", "", target)
if target not in ids or (frag and frag not in ids[target]):
broken.append(f"{doc}: {href}")
assert not broken
def test_book_filter_turns_chapter_anchors_into_chapter_starts(convert):
docs = [Doc("a.xhtml", "<h1>Kapitel A</h1><p>a</p>"), Doc("b.xhtml", "<h1>Kapitel B</h1><p>b</p>")]
md = merged(convert, Book(docs=docs))
html = pandoc(md, "-s", "--lua-filter", str(FILTERS / "book.lua"), "-t", "html").stdout
assert html.count('class="chapter-start"') == 2
assert 'id="TOC"' in html and "Kapitel B" in html