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>
This commit is contained in:
commit
02449734a2
1 changed files with 7726 additions and 0 deletions
99
tests/test_math.py
Normal file
99
tests/test_math.py
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
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; 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 "]
|
||||
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"])
|
||||
Loading…
Reference in a new issue