From f0e0562451fa32af849f0edfcd4b0e7292b163b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dieter=20Schl=C3=BCter?= Date: Fri, 2 Oct 2026 17:32:20 +0200 Subject: [PATCH] =?UTF-8?q?Inhaltsverzeichnis=20einheitlich:=20Eintr=C3=A4?= =?UTF-8?q?ge=20als=20Abs=C3=A4tze=20statt=20"\"-Zeilen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Im Inhaltsverzeichnis wird jeder Eintrag ein eigener Absatz, auch wenn er als zeilengetreu erkannt wurde (vorher gemischte Abstände im EPUB) - Zeilen, die wie Gliederungseinträge aussehen ("Teil V: …", "32 …"), werden auch ohne Überschrift "Inhalt" zu Absätzen (Vorschau Band Zwei) - Tests, README Co-Authored-By: Claude Opus 5.5 --- README.md | 6 ++++-- pdf2md.py | 18 ++++++++++++++++++ tests/test_verbatim.py | 20 ++++++++++++++++++++ 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5debf81..fbe7e08 100644 --- a/README.md +++ b/README.md @@ -66,8 +66,10 @@ python3 pdf2md.py --epub --ocr-language eng --ocr always scan.pdf scan.md # EP werden ebenfalls aufgelöst (`wach­|senden` → `wachsenden`). - **Überschriften:** Ebenen aus den PDF-Lesezeichen (Teile `##`, Kapitel `###`); Satzreste, die fälschlich als Überschrift erkannt wurden, kommen zurück in den Text. -- **Inhaltsverzeichnis:** zusammengeklebte Einträge werden getrennt; liegt das Verzeichnis - als Tabelle vor, bleibt die Tabelle erhalten. +- **Inhaltsverzeichnis:** zusammengeklebte Einträge werden getrennt; jeder Eintrag wird ein + eigener Absatz (auch bei Verzeichnissen ohne Überschrift „Inhalt“, z. B. „Vorschau Band + Zwei“), nie eine Zeile mit `\`. Liegt das Verzeichnis als Tabelle vor, bleibt die Tabelle + erhalten. - **OCR-Ebenen** in Courier werden nicht als Programmcode (`` `…` ``) gesetzt. - **Links:** URLs werden als `` geschrieben und sind damit in Markdown-Viewern und im EPUB anklickbar. Beim Zeilenumbruch zerbrochene URLs (`deutsche-|sehen`, diff --git a/pdf2md.py b/pdf2md.py index a9feea7..bdb3deb 100644 --- a/pdf2md.py +++ b/pdf2md.py @@ -131,6 +131,11 @@ PART_TITLE = re.compile( r"^(?:teil|buch|band|part|book)\s+(?:[ivxlcdm]+|\d+)\b", re.IGNORECASE, ) +# Zeile, die wie ein Gliederungseintrag aussieht ("Teil V: …", "32 Oberst …"). +OUTLINE_ENTRY = re.compile( + r"^(?:\d{1,3}\.?\s+\S|(?:teil|buch|band|part|book|kapitel|chapter)\s+(?:[ivxlcdm]+|\d+)\b)", + re.IGNORECASE, +) NUMBERED_TITLE = re.compile( r"^(?:\d+\.?|kapitel\s+\d+|chapter\s+\d+)\s", re.IGNORECASE, @@ -374,6 +379,19 @@ def normalize_markdown( # Mehrzeiliger Absatz: Absatzende; einzelne Zeile: harter Umbruch line += PARAGRAPH_END if end_mark.group(2) else HARD_BREAK + if in_contents and line.endswith((HARD_BREAK, PARAGRAPH_END)): + # Inhaltsverzeichnis: jeder Eintrag ein eigener Absatz (einheitlich, + # auch wenn das Verzeichnis in der PDF zeilengetreu erscheint). + line = line.rstrip(HARD_BREAK + PARAGRAPH_END) + elif line.endswith((HARD_BREAK, PARAGRAPH_END)) and OUTLINE_ENTRY.match( + clean_inline(line.rstrip(HARD_BREAK + PARAGRAPH_END)) + ): + # Verzeichnis ohne Überschrift "Inhalt" (z. B. "Vorschau Band Zwei"): + # Einträge als eigene Absätze statt als Zeilen mit "\". + flush_paragraph() + standalone(clean_inline(line.rstrip(HARD_BREAK + PARAGRAPH_END))) + continue + if line.endswith(PARAGRAPH_END): content = line.removesuffix(PARAGRAPH_END) if not paragraph and result and result[-1].endswith(HARD_BREAK): diff --git a/tests/test_verbatim.py b/tests/test_verbatim.py index bc39827..b8e7cd5 100644 --- a/tests/test_verbatim.py +++ b/tests/test_verbatim.py @@ -141,3 +141,23 @@ def test_literal_fence_text_is_escaped_and_markers_never_leak(): 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" + )