epub2md/tests/test_math.py
dschlueter 02449734a2 feat: epub2md – EPUB nach kapitelweisem Markdown
Kommandozeilenwerkzeug, das DRM-freie EPUB-2/3-Dateien offline in
kapitelweise Markdown-Dateien umwandelt. Erzeugt werden außerdem Medien,
metadata.yaml, manifest.json, conversion-report.json und Diagnose-Dateien.

- Spine-basierte Kapitelbildung mit --chapter-level, epub:type-Abschnitten,
  Fortsetzungsdateien und Erkennung von Layout-Überschriften
- Code mit exakter Einrückung, sicheren Fences und konservativer
  Spracherkennung
- MathML->TeX (konservativ), TeX aus alt-Texten, SVG-/Bildformeln
  bleiben Bilder
- Fußnoten-Modi end-of-chapter, end-of-book, inline und keep-links
- Interne Links/Anker werden umgeschrieben, Linkcheck über die Ausgabe
- Sicher: kein Entpacken, Schutz vor Path Traversal und XXE,
  DRM-Erkennung, atomares Schreiben, --force nur für frühere Ausgaben
- 82 pytest-Tests mit synthetischen EPUBs und Invariantentests für Beispiele/

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

99 lines
4.2 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"])