epub2md/tests/test_math.py
dschlueter 3b07fcf01a fix: Befunde aus der QA-Prüfung (Prompt_Kontrolle.md)
- F1: Nicht beschreibbares Ausgabeziel liefert eine verständliche
  Meldung und Exit 1 statt "Interner Fehler" (Exit 4).
- F2: Anker von <dt id> stehen vor dem fett gesetzten Glossarbegriff
  statt innerhalb der **-Marker.
- F3: Formeln als <img src="*.svg"> erscheinen im Bericht mit
  origin "svg" statt "image".

Regressionstests für alle drei Befunde ergänzt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 19:47:45 +02:00

106 lines
4.5 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
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