epub2md/tests/test_math.py

134 lines
6.1 KiB
Python
Raw Normal View History

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