Inhaltsverzeichnis einheitlich: Einträge als Absätze statt "\"-Zeilen
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
d322ee3523
commit
f0e0562451
3 changed files with 42 additions and 2 deletions
|
|
@ -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`).
|
werden ebenfalls aufgelöst (`wach|senden` → `wachsenden`).
|
||||||
- **Überschriften:** Ebenen aus den PDF-Lesezeichen (Teile `##`, Kapitel `###`);
|
- **Überschriften:** Ebenen aus den PDF-Lesezeichen (Teile `##`, Kapitel `###`);
|
||||||
Satzreste, die fälschlich als Überschrift erkannt wurden, kommen zurück in den Text.
|
Satzreste, die fälschlich als Überschrift erkannt wurden, kommen zurück in den Text.
|
||||||
- **Inhaltsverzeichnis:** zusammengeklebte Einträge werden getrennt; liegt das Verzeichnis
|
- **Inhaltsverzeichnis:** zusammengeklebte Einträge werden getrennt; jeder Eintrag wird ein
|
||||||
als Tabelle vor, bleibt die Tabelle erhalten.
|
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.
|
- **OCR-Ebenen** in Courier werden nicht als Programmcode (`` `…` ``) gesetzt.
|
||||||
- **Links:** URLs werden als `<https://…>` geschrieben und sind damit in Markdown-Viewern
|
- **Links:** URLs werden als `<https://…>` geschrieben und sind damit in Markdown-Viewern
|
||||||
und im EPUB anklickbar. Beim Zeilenumbruch zerbrochene URLs (`deutsche-|sehen`,
|
und im EPUB anklickbar. Beim Zeilenumbruch zerbrochene URLs (`deutsche-|sehen`,
|
||||||
|
|
|
||||||
18
pdf2md.py
18
pdf2md.py
|
|
@ -131,6 +131,11 @@ PART_TITLE = re.compile(
|
||||||
r"^(?:teil|buch|band|part|book)\s+(?:[ivxlcdm]+|\d+)\b",
|
r"^(?:teil|buch|band|part|book)\s+(?:[ivxlcdm]+|\d+)\b",
|
||||||
re.IGNORECASE,
|
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(
|
NUMBERED_TITLE = re.compile(
|
||||||
r"^(?:\d+\.?|kapitel\s+\d+|chapter\s+\d+)\s",
|
r"^(?:\d+\.?|kapitel\s+\d+|chapter\s+\d+)\s",
|
||||||
re.IGNORECASE,
|
re.IGNORECASE,
|
||||||
|
|
@ -374,6 +379,19 @@ def normalize_markdown(
|
||||||
# Mehrzeiliger Absatz: Absatzende; einzelne Zeile: harter Umbruch
|
# Mehrzeiliger Absatz: Absatzende; einzelne Zeile: harter Umbruch
|
||||||
line += PARAGRAPH_END if end_mark.group(2) else HARD_BREAK
|
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):
|
if line.endswith(PARAGRAPH_END):
|
||||||
content = line.removesuffix(PARAGRAPH_END)
|
content = line.removesuffix(PARAGRAPH_END)
|
||||||
if not paragraph and result and result[-1].endswith(HARD_BREAK):
|
if not paragraph and result and result[-1].endswith(HARD_BREAK):
|
||||||
|
|
|
||||||
|
|
@ -141,3 +141,23 @@ def test_literal_fence_text_is_escaped_and_markers_never_leak():
|
||||||
out = p.normalize_markdown(md)
|
out = p.normalize_markdown(md)
|
||||||
assert out.startswith("\\```")
|
assert out.startswith("\\```")
|
||||||
assert not p.MARKERS.search(out)
|
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"
|
||||||
|
)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue