epub2md/tests/test_math.py
dschlueter 64d0cb0357 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

134 lines
6.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import pytest
from lxml import etree
from conftest import PNG, SVG, Book, Doc
from epub2md.mathml import Unsupported, mathml_to_tex, portable_tex
M = 'xmlns="http://www.w3.org/1998/Math/MathML"'
def tex(xml: str) -> str:
return mathml_to_tex(etree.fromstring(f"<math {M}>{xml}</math>"))
@pytest.mark.parametrize(
("xml", "expected"),
[
("<mfrac><mi>a</mi><mi>b</mi></mfrac>", r"\frac{a}{b}"),
("<msup><mi>x</mi><mn>2</mn></msup>", "x^{2}"),
("<msub><mi>a</mi><mi>i</mi></msub>", "a_{i}"),
("<msubsup><mi>x</mi><mi>i</mi><mn>2</mn></msubsup>", "x_{i}^{2}"),
("<msqrt><mi>x</mi></msqrt>", r"\sqrt{x}"),
("<mroot><mi>x</mi><mn>3</mn></mroot>", r"\sqrt[3]{x}"),
("<munderover><mo>∑</mo><mrow><mi>i</mi><mo>=</mo><mn>1</mn></mrow><mi>n</mi></munderover><msub><mi>a</mi><mi>i</mi></msub>",
r"\sum_{i = 1}^{n} a_{i}"),
("<mi>α</mi><mo>≤</mo><mi>β</mi>", r"\alpha \leq \beta"),
("<mover><mi>x</mi><mo>^</mo></mover>", r"\hat{x}"),
("<mi mathvariant='double-struck'>R</mi>", r"\mathbb{R}"),
("<mtext>für alle</mtext>", r"\text{für alle}"),
("<mrow><mo>(</mo><mtable><mtr><mtd><mn>1</mn></mtd><mtd><mn>0</mn></mtd></mtr>"
"<mtr><mtd><mn>0</mn></mtd><mtd><mn>1</mn></mtd></mtr></mtable><mo>)</mo></mrow>",
r"\left( \begin{matrix} 1 & 0 \\ 0 & 1 \end{matrix} \right)"),
],
)
def test_mathml_to_tex(xml, expected):
assert tex(xml) == expected
def test_tex_annotation_is_preferred():
xml = ('<semantics><mi>x</mi><annotation encoding="application/x-tex">\\mathbf{x}</annotation></semantics>')
assert tex(xml) == r"\mathbf{x}"
@pytest.mark.parametrize("xml", ["<menclose><mi>x</mi></menclose>", "<mmultiscripts><mi>x</mi></mmultiscripts>",
"<mtable><mlabeledtr><mtd>1</mtd></mlabeledtr></mtable>"])
def test_unsupported_mathml_raises(xml):
with pytest.raises(Unsupported):
tex(xml)
BODY = (
"<h1>Formeln</h1>"
f'<p>Inline <math {M}><mfrac><mi>a</mi><mi>b</mi></mfrac></math> im Satz.</p>'
f'<div class="equation"><math {M} display="block"><msup><mi>x</mi><mn>2</mn></msup></math> <span>(3.7)</span></div>'
f'<p>Unbekannt: <math {M}><menclose notation="box"><mi>z</mi></menclose></math></p>'
'<p>SVG: <span class="math"><img src="f.svg" alt="Formel 1"/></span></p>'
'<p>Bild ohne TeX: <span class="equation"><img src="eq.png" alt="Summe"/></span></p>'
'<p>Bild mit TeX: <span class="InlineEquation"><img src="eq2.png" alt="$$a &amp;amp; b \\leq c$$"/></span></p>'
)
def math_book() -> Book:
return Book(docs=[Doc("m.xhtml", BODY)], media={"f.svg": SVG, "eq.png": PNG, "eq2.png": PNG})
def test_math_auto(convert):
run = convert(math_book())
text = run.chapter("001")
assert r"Inline $\frac{a}{b}$ im Satz." in text
assert "$$\nx^{2}\n$$" in text
assert "(3.7)" in text # equation number visible
assert "<math" in text and "menclose" in text # unsupported MathML kept, not lost
assert "![Formel 1](../media/images/" in text # SVG formula stays SVG
assert "![Summe](../media/images/" in text
assert r"$a & b \leq c$" in text # TeX from alt, double escaping repaired
codes = [w["code"] for w in run.report["warnings"]]
assert "mathml-kept" in codes and "formula-image" in codes
svgs = [m for m in run.report["media"]["files"] if m["file"].endswith(".svg")]
assert svgs and svgs[0]["media_type"] == "image/svg+xml"
def test_math_tex_mode_uses_backslash_delimiters(convert):
text = convert(math_book(), "--math", "tex").chapter("001")
assert r"\(\frac{a}{b}\)" in text
assert "\\[\nx^{2}\n\\]" in text
assert "$" not in text.replace("\\$", "")
def test_math_mathml_mode_keeps_mathml(convert):
text = convert(math_book(), "--math", "mathml").chapter("001")
assert '<math xmlns="http://www.w3.org/1998/Math/MathML"><mfrac><mi>a</mi><mi>b</mi></mfrac></math>' in text
assert "frac{" not in text
def test_math_image_mode(convert):
run = convert(math_book(), "--math", "image")
text = run.chapter("001")
assert text.count("![") == 3 # svg + both formula images
assert "<math" in text # pure MathML without image is not lost
assert any(w["code"] == "mathml-no-image" for w in run.report["warnings"])
def test_svg_file_formula_is_reported_as_svg(convert):
"""QA-F3: <img src="*.svg"> inside a formula container is an SVG formula in the report."""
report = convert(math_book()).report
origins = {f["origin"] for f in report["formulas"]["items"]}
assert "svg" in origins
def test_book_readme_suggests_mathml_for_pandoc_only_with_formulas(convert):
run = convert(math_book())
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