143 lines
4.7 KiB
Python
143 lines
4.7 KiB
Python
|
|
"""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 = (
|
|||
|
|
"<p>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.</p>"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
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 + (
|
|||
|
|
"<p>Der Mond ist aufgegangen,<br>die goldnen Sternlein prangen<br>"
|
|||
|
|
"am Himmel hell und klar.</p>"
|
|||
|
|
)
|
|||
|
|
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)
|