PDFTranslator / test /test_render_markup.py
hoang.nguyen6
deploy
f66643d unverified
Raw
History Blame Contribute Delete
8.42 kB
"""Unit tests for pdf2zh.render.markup — HTML/LaTeX → Typst markup conversion."""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
from pdf2zh.render.markup import parse_toc_line, to_typst_markup, to_typst_native
class TestMathConversion:
def test_display_math(self):
out = to_typst_markup('<math display="block">\\frac{a}{b}</math>')
assert "$$\\frac{a}{b}$$" in out
def test_inline_math(self):
out = to_typst_markup("<math>x^2</math>")
assert "$x^2$" in out
def test_multiple_math_blocks(self):
out = to_typst_markup("See <math>a+b</math> and <math>c-d</math>")
assert "$a+b$" in out
assert "$c-d$" in out
def test_math_preserved_in_equation_mode(self):
out = to_typst_markup("$a=b$", is_equation=True)
# Already has $...$, should stay
assert "$a=b$" in out
def test_bare_latex_wrapped_in_equation_mode(self):
out = to_typst_markup("We get \\frac{a}{b}", is_equation=True)
assert "$" in out
assert "\\frac{a}{b}" in out
def test_bare_latex_not_wrapped_outside_equation(self):
# In non-equation mode, bare LaTeX is not wrapped
out = to_typst_markup("Some text \\frac{a}{b}", is_equation=False)
# No wrapping — just passes through (stripped as unknown tag or kept)
assert "\\frac{a}{b}" in out
class TestHtmlFormatting:
def test_bold(self):
assert "**hello**" in to_typst_markup("<b>hello</b>")
def test_strong(self):
assert "**world**" in to_typst_markup("<strong>world</strong>")
def test_italic(self):
assert "_hi_" in to_typst_markup("<i>hi</i>")
def test_em(self):
assert "_em_" in to_typst_markup("<em>em</em>")
def test_superscript_outside_math(self):
out = to_typst_markup("x<sup>2</sup>")
assert "^2^" in out
def test_subscript_outside_math(self):
out = to_typst_markup("H<sub>2</sub>O")
assert "~2~" in out
def test_bold_with_italic_inside(self):
out = to_typst_markup("<b>bold <i>and italic</i></b>")
assert "**" in out
assert "_" in out
class TestLiteralCharacters:
def test_less_than_escaped(self):
out = to_typst_markup("If a < b then")
assert "\\<" in out
def test_italic_variable_with_comparison(self):
out = to_typst_markup("If <i>a</i> < <i>b</i>")
assert "_a_" in out
assert "_b_" in out
assert "\\<" in out
def test_plain_text_hash_escaped(self):
out = to_typst_markup("Price: #100")
assert "\\#100" in out
def test_plain_text_at_escaped(self):
out = to_typst_markup("Email @user")
assert "\\@user" in out
def test_no_false_tag_match(self):
# A tag-like that isn't a known formatting tag
out = to_typst_markup("<unknown>text</unknown>")
# Unknown tags stripped
assert "<unknown>" not in out
assert "text" in out
class TestMathAndProseEquation:
def test_mixed_prose_and_math(self):
out = to_typst_markup(
"Nếu ax<sup>2</sup> + bx + c = 0, thì x = \\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a}.",
is_equation=True,
)
# Vietnamese prose preserved
assert "Nếu" in out
assert "thì" in out
# Math wrapped
assert "$" in out
def test_equation_with_html_math_tags(self):
out = to_typst_markup(
'a(b + c) = ab + ac <math display="block">\\frac{a+c}{b}</math>',
is_equation=True,
)
assert "$$\\frac{a+c}{b}$$" in out
assert "a(b + c) = ab + ac" in out
class TestTypstNativeMath:
def test_preserves_multilevel_symbol_modifiers(self):
out = to_typst_native("<math>arrow.r.double dot.triple</math>")
assert "$arrow.r.double dot.triple$" in out
def test_breaks_unknown_symbol_modifiers(self):
out = to_typst_native("<math>plus.unknown</math>")
assert "plus.unknown" not in out
assert "$plus u n k n o w n$" in out
def test_quotes_long_underscore_identifiers(self):
out = to_typst_native("<math>page_index = k</math>")
assert '$upright("page_index") = k$' in out
def test_preserves_simple_subscripts(self):
out = to_typst_native("<math>x_i + a_1</math>")
assert "$x_i + a_1$" in out
class TestMathSanitizer:
"""Regression tests: LLM math output must never break the Typst compile."""
def test_no_double_wrap_of_underscore_identifiers(self):
# Was: $upright("upright("page_index")")$ — nested unescaped quotes.
out = to_typst_native("<math>page_index = k</math>")
assert out == '$upright("page_index") = k$'
def test_existing_upright_quotes_untouched(self):
out = to_typst_native('<math>upright("page_index") = k</math>')
assert out == '$upright("page_index") = k$'
def test_quoted_strings_not_letter_split(self):
# Was: "pairs with" → "p a i r s w i t h"
out = to_typst_native('<math>x = "pairs with" y</math>')
assert '"pairs with"' in out
def test_bare_hash_escaped_in_math(self):
# Bare # in Typst math starts a code expression → compile error.
out = to_typst_native('<math>TP(t) = #{"pairs" IoU >= t}</math>')
assert "\\#" in out
assert '"pairs"' in out
def test_unknown_function_name_quoted(self):
# Bare TP(t) is 'unknown variable: TP' at compile time.
out = to_typst_native("<math>TP(t) = 1</math>")
assert 'upright("TP")(t)' in out
def test_known_function_call_kept(self):
out = to_typst_native("<math>frac(a, b)</math>")
assert "$frac(a, b)$" in out
def test_known_identifier_subscript_kept(self):
out = to_typst_native("<math>sigma_x^2</math>")
assert "$sigma_x^2$" in out
def test_leading_attach_gets_empty_base(self):
# $_(x)$ is 'unexpected underscore' — needs an empty base.
out = to_typst_native("<math>_(x)</math>")
assert '$""_(x)$' in out
def test_trailing_attach_gets_empty_script(self):
out = to_typst_native("<math>x_</math>")
assert '$x_""$' in out
def test_unmatched_quote_escaped(self):
# A lone quote opens a string that swallows the rest of the source.
out = to_typst_native('<math>x = "unclosed</math>')
assert '\\"' in out
assert out.count('"') % 2 == 0 or '\\"' in out
def test_typst_block_math_idempotent(self):
out = to_typst_native("<typst>page $page_index$ = k</typst>")
assert 'page $upright("page_index")$ = k' == out
class TestTocLineParsing:
def test_simple_entry(self):
result = parse_toc_line("Introduction 1")
assert result == ("Introduction", "1")
def test_entry_with_dots(self):
result = parse_toc_line("Chapter 1: Overview ....... 15")
assert result is not None
assert result[1] == "15"
assert "Chapter 1" in result[0]
def test_entry_with_bold_markup(self):
result = parse_toc_line("<b>Derivatives</b> 174")
assert result is not None
assert result[1] == "174"
def test_entry_no_page_number(self):
result = parse_toc_line("Just a heading")
assert result is None
def test_empty_line(self):
assert parse_toc_line("") is None
def test_multipart_number(self):
result = parse_toc_line("3.1 Derivatives of Polynomials 174")
assert result is not None
assert result[1] == "174"
class TestNewlineHandling:
def test_lone_newline_becomes_hard_break(self):
# A single '\n' must become a CommonMark hard break (backslash + newline)
# so cmarker keeps the line break instead of collapsing it to a space.
out = to_typst_markup("Ho Chi Minh City\nStudent group")
assert "\\\n" in out
assert out == "Ho Chi Minh City\\\nStudent group"
def test_paragraph_break_preserved(self):
# A blank line (double newline) stays a paragraph break, not a hard break.
out = to_typst_markup("Para one.\n\nPara two.")
assert "\\\n" not in out
assert "\n\n" in out
def test_newline_inside_math_untouched(self):
out = to_typst_markup("text <math>a\nb</math> more")
assert "$a\nb$" in out # no backslash injected inside math