"""Zeilengetreue Abschnitte: Register, Code, Verse – Fließtext bleibt Fließtext.""" import zipfile import pymupdf import pytest import pdf2md as p from conftest import needs_pandoc from test_e2e import run NBSP = " " PROSE = ( "

Dieser Absatz ist ganz normaler Fließtext im Flattersatz. Er wird vom " "Satzprogramm umbrochen, weil die Zeilen voll sind, und muss deshalb in der " "Ausgabe wieder zu einer einzigen Zeile zusammengefügt werden, ohne harte " "Umbrüche und ohne Leerzeilen dazwischen.

" ) def make_pdf(path, lines, html=PROSE): """Fließtext oben (legt die Spaltenbreite fest), darunter Zeilen an festen Positionen.""" doc = pymupdf.open() page = doc.new_page(width=420, height=595) page.insert_htmlbox(pymupdf.Rect(50, 40, 370, 200), html) y = 240 for x, text in lines: page.insert_text((x, y), text, fontname="tiro", fontsize=11) y += 14 doc.save(path) return path def convert(tmp_path, lines, html=PROSE) -> str: source = make_pdf(tmp_path / "probe.pdf", lines, html) out = tmp_path / "probe.md" run(source, out) return out.read_text(encoding="utf-8") def test_prose_stays_one_paragraph(tmp_path): md = convert(tmp_path, []) assert "Fließtext im Flattersatz. Er wird vom Satzprogramm umbrochen" in md assert "\\" not in md def test_register_keeps_lines_and_indentation(tmp_path): md = convert(tmp_path, [ (50, "Backslash"), (62, "Maskierung, 79"), (50, "BaseException, 227"), (50, "Basisklasse, 206"), (62, "abstrakte, 212"), ]) # "Backslash" hält das Layout-Modell hier für eine Überschrift. assert ( f"{NBSP * 4}Maskierung, 79\\\n" "BaseException, 227\\\n" "Basisklasse, 206\\\n" f"{NBSP * 4}abstrakte, 212" ) in md def test_code_in_proportional_font_becomes_code_block(tmp_path): md = convert(tmp_path, [ (50, "def quadrat(x):"), (62, "y = x * x"), (62, "return y"), (50, "print(quadrat(3))"), ]) assert "```\ndef quadrat(x):\n y = x * x\n return y\nprint(quadrat(3))\n```" in md def test_poem_keeps_line_breaks(tmp_path): html = PROSE + ( "

Der Mond ist aufgegangen,
die goldnen Sternlein prangen
" "am Himmel hell und klar.

" ) md = convert(tmp_path, [], html) assert "Der Mond ist aufgegangen,\\\ndie goldnen Sternlein prangen\\\nam Himmel hell und klar." in md # --- normalize_markdown mit Markierungen ------------------------------------------- H, P, B = p.HARD_BREAK, p.PARAGRAPH_END, p.BLOCK_END def test_hard_breaks_become_backslashes_except_last(): md = f"Erste{H}\n\nZweite{H}\n\nDritte{H}\n\nDanach Fließtext.\n" assert p.normalize_markdown(md) == "Erste\\\nZweite\\\nDritte\n\nDanach Fließtext.\n" def test_block_end_continues_when_word_did_not_fit(): md = f"Er sprach mit dem{B}3{B}\n\nGeheimdienstchef über alles.\n" assert p.normalize_markdown(md) == "Er sprach mit dem Geheimdienstchef über alles.\n" def test_block_end_is_hard_when_word_would_have_fit(): md = f"Moskau, im Mai{B}40{B}\n\nDer Zug fuhr ab.\n" assert p.normalize_markdown(md) == "Moskau, im Mai\n\nDer Zug fuhr ab.\n" def test_multiline_block_without_sentence_end_ends_paragraph(): md = f"Ein Absatz mit Fußnote.[8 ]{B}30P{B}\n\nDer nächste Absatz.\n" assert p.normalize_markdown(md) == "Ein Absatz mit Fußnote.[8 ]\n\nDer nächste Absatz.\n" def test_paragraph_end_closes_verbatim_run_without_blank(): md = f"Die Wahrheit erzählen,{H}\ndas ist der größte Witz.{P}\n\nWeiter.\n" assert p.normalize_markdown(md) == ( "Die Wahrheit erzählen,\\\ndas ist der größte Witz.\n\nWeiter.\n" ) def test_code_blocks_across_page_break_are_merged(): md = "```\nfn main() {\n```\n\n17\n\n```\n}\n```\n" assert p.normalize_markdown(md) == "```\nfn main() {\n}\n```\n" @pytest.mark.parametrize("text", ["fn main() {", "use std::io;", "def f(x):", "print(x)"]) def test_code_hints(text): assert p.CODE_HINT.search(text) @pytest.mark.parametrize("text", ["Maskierung, 79", "Der Mond ist aufgegangen,", "Kapitel (1)"]) def test_no_code_hints(text): assert not p.CODE_HINT.search(text) @needs_pandoc def test_epub_wraps_long_code_lines(pdfs, tmp_path): run(pdfs["digital"], tmp_path / "buch.md", "--epub") with zipfile.ZipFile(tmp_path / "buch.epub") as z: css = "".join(z.read(n).decode() for n in z.namelist() if n.endswith(".css")) assert "white-space: pre-wrap" in css def test_literal_fence_text_is_escaped_and_markers_never_leak(): md = f"```{B}66{B}\n\nText danach.\n\nRest{H}\n" out = p.normalize_markdown(md) assert out.startswith("\\```") assert not p.MARKERS.search(out) def test_contents_entries_are_uniform_paragraphs(): outline = {"vorwort": 2, "1 der notar": 3, "2 ein brief": 3} md = ( f"## Inhaltsverzeichnis\n\nTitel{H}\nVorwort{H}\n1 Der Notar{P}\n\n" f"2 Ein Brief{B}40{B}\n\n## Vorwort\n\nText.\n" ) assert p.normalize_markdown(md, outline) == ( "## Inhaltsverzeichnis\n\nTitel\n\nVorwort\n\n1 Der Notar\n\n2 Ein Brief\n\n" "## Vorwort\n\nText.\n" ) def test_outline_like_lines_outside_contents_become_paragraphs(): md = f"## Vorschau Band Zwei\n\n32 Oberst a.D.{H}\n33 Wolga-Attacke{P}\n\nTeil VI: Polen{H}\nOperation Morgenlicht{P}\n" assert p.normalize_markdown(md) == ( "## Vorschau Band Zwei\n\n32 Oberst a.D.\n\n33 Wolga-Attacke\n\n" "Teil VI: Polen\n\nOperation Morgenlicht\n" )