Seitenzahlen bei --split, Teilen nach Länge, Links mit eigenem Text
- add_source_pages: source_pages [von, bis] im Frontmatter; Startseite aus
der PDF-Gliederung, sonst Textsuche ohne Verzeichnisseiten; Ende davor,
wenn der nächste Abschnitt oben auf der Seite beginnt
- split_by_length: ohne verwertbare Gliederung an Absatzgrenzen nach Länge
(ca. 30 000 Zeichen), Codeblöcke bleiben ganz
- load_link_anchors/link_anchors: Links mit eigenem Text ("hier") über
Linktext und Wörter davor wiederfinden und als [Text](Ziel) setzen
(Zensur-Report: 75 von 76)
- Tests, README, Bedienungsanleitung, TODO
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
b8ad0310d0
commit
3d418e3987
5 changed files with 273 additions and 17 deletions
|
|
@ -200,10 +200,13 @@ band1/
|
|||
part: "Teil I: Das Vermächtnis"
|
||||
chapter: 3
|
||||
order: 5
|
||||
source_pages: [40, 43] # Seiten der PDF-Datei (nicht die gedruckten Seitenzahlen)
|
||||
book: "Operation Ljutsch"
|
||||
author: "Reinhard Otto Kranz"
|
||||
---
|
||||
```
|
||||
- Erkennt das Programm keine Gliederung, teilt es nach Länge (ca. 30 000 Zeichen, an
|
||||
Absatzgrenzen) in `Abschnitt 1`, `Abschnitt 2` … und meldet das.
|
||||
- **Umsortieren:** in `index.md` die Zeilen verschieben. **Weglassen:** Zeile löschen.
|
||||
Dateien dürfen umbenannt werden, wenn der Link in `index.md` mitgeändert wird.
|
||||
- **EPUB neu bauen:** `python3 pdf2md.py --from-md Output/band1/` → `Output/band1.epub`
|
||||
|
|
|
|||
12
README.md
12
README.md
|
|
@ -86,11 +86,14 @@ python3 pdf2md.py --epub --ocr-language eng --ocr always scan.pdf scan.md # EP
|
|||
und im EPUB anklickbar. Beim Zeilenumbruch zerbrochene URLs (`deutsche-|sehen`,
|
||||
`…/ youtube-…`) werden mit den Linkzielen der PDF abgeglichen und repariert;
|
||||
Tracking-Parameter (`utm_…`) entfallen. Ohne Linkziel wird die URL aus dem gedruckten
|
||||
Text zusammengesetzt.
|
||||
Text zusammengesetzt. Links mit eigenem Text („hier“, „Bericht“) werden über den Linktext
|
||||
und die Wörter davor im Markdown wiedergefunden und als `[hier](https://…)` gesetzt.
|
||||
- **Seitenzahlen, Kolumnentitel, Text in Fotos** werden entfernt.
|
||||
- **YAML-Kopf:** Jede Markdown-Datei beginnt mit `title`, `author`, `lang`, `cover`
|
||||
(Einzeldateien von `--split` mit `title`, `part`, `chapter`, `order`, `book`, `author`).
|
||||
`--from-md` liest daraus die EPUB-Angaben.
|
||||
(Einzeldateien von `--split` mit `title`, `part`, `chapter`, `order`, `source_pages`,
|
||||
`book`, `author`; `source_pages` = Seiten der PDF-Datei, nicht die gedruckten).
|
||||
`--from-md` liest daraus die EPUB-Angaben. Ohne verwertbare Gliederung teilt `--split`
|
||||
nach Länge (ca. 30 000 Zeichen, an Absatzgrenzen) in „Abschnitt 1“, „Abschnitt 2“ …
|
||||
|
||||
## Tests
|
||||
|
||||
|
|
@ -116,7 +119,8 @@ synthetisch erzeugt.
|
|||
- **Was am PDF liegt, bleibt:** fehlende Ligaturen ohne Textzuordnung („Offzielle“),
|
||||
gesperrt gesetzte Überschriften („M e i n k l e i nes …“), flache PDF-Gliederungen
|
||||
(alle Kapitel auf einer Ebene).
|
||||
- Links mit anderem Text als der URL (z. B. „hier“) werden nicht übernommen.
|
||||
- Links mit eigenem Text: Findet sich der Linktext nicht wieder (z. B. Trennung mitten in
|
||||
einem langen Titel), bleibt er ohne Link (Zensur-Report: 75 von 76 gesetzt).
|
||||
- Zeilengetreue Erkennung: Verszeilen mit Satzzeichen am Ende werden zu eigenen Absätzen
|
||||
statt zu Zeilen; einzelne Konfigurations- oder Ausgabezeilen neben Code (`[dependencies]`)
|
||||
stehen als Zeilen statt im Codeblock.
|
||||
|
|
|
|||
15
TODO.md
15
TODO.md
|
|
@ -1,13 +1,6 @@
|
|||
# TODO
|
||||
|
||||
## 1. Ideen
|
||||
|
||||
- `--split`: Seitenzahlen der PDF ins Frontmatter (`source_pages`); pymupdf4llm liefert sie
|
||||
mit `page_chunks=True`, müsste aber durch die ganze Nachbearbeitung getragen werden.
|
||||
- `--split`: Bücher ohne erkennbare Gliederung werden nicht geteilt (nur `00_vorspann.md`);
|
||||
denkbar wäre ein Schnitt nach Seitenzahl oder Länge.
|
||||
|
||||
## 2. Bekannte Schwächen (aus Probeläufen mit fremden PDFs)
|
||||
## Bekannte Schwächen (aus Probeläufen mit fremden PDFs)
|
||||
|
||||
- Reine Zahlen als Überschrift: pymupdf4llm macht Fußnotenzeichen zu Überschriften
|
||||
(„## 90“ im Zensur-Report). Nicht pauschal entfernen – im DDR-Buch sind „## 1“, „## 2“
|
||||
|
|
@ -22,6 +15,6 @@
|
|||
(`[dependencies]`, Programmausgaben) landen als `\`-Zeilen neben dem Codeblock statt darin.
|
||||
- Markdown-Sonderzeichen werden nicht maskiert: `__add__` im Register wird fett gesetzt.
|
||||
- Links: Setzt sich eine URL in einem neuen Block fort (z. B. als Listenpunkt „- juuuport-…“),
|
||||
bleibt sie abgeschnitten. Links mit eigenem Text („hier“) gehen verloren; Abhilfe wäre,
|
||||
die Link-Rechtecke der PDF dem Text zuzuordnen (`[hier](…)`). Die Heuristik ohne Linkziel
|
||||
hängt Wörter mit „/“ an („https://a.de/ und/oder“ → eine URL).
|
||||
bleibt sie abgeschnitten. Die Heuristik ohne Linkziel hängt Wörter mit „/“ an
|
||||
(„https://a.de/ und/oder“ → eine URL). Linktexte mit Trennung mitten in langen Titeln
|
||||
werden nicht wiedergefunden.
|
||||
|
|
|
|||
172
pdf2md.py
172
pdf2md.py
|
|
@ -744,6 +744,94 @@ def link_urls(markdown: str, uris: list[str] | None = None) -> str:
|
|||
return "\n".join(lines)
|
||||
|
||||
|
||||
# Links mit eigenem Text ("hier", "Bericht"): Linktext und die Wörter davor aus
|
||||
# der PDF lesen und die Stelle im Markdown als [Text](Ziel) markieren.
|
||||
|
||||
ANCHOR_CONTEXT_WORDS = 3
|
||||
URL_LIKE_WORD = re.compile(r"://|^www\.|/|\w\.(?:[a-z]{2,4})\.?$", re.IGNORECASE)
|
||||
|
||||
|
||||
def load_link_anchors(doc: pymupdf.Document) -> list[tuple[str, list[str], list[str]]]:
|
||||
"""(Ziel, Wörter davor, Linkwörter) für Links, deren Text keine URL ist."""
|
||||
anchors: list[tuple[str, list[str], list[str]]] = []
|
||||
for page in doc:
|
||||
words = page.get_text("words", sort=True)
|
||||
links = [link for link in page.get_links() if (link.get("uri") or "").startswith("http")]
|
||||
merged: list[tuple[str, list[int]]] = [] # (Ziel, Indizes der Wörter)
|
||||
for link in links:
|
||||
rect = link["from"]
|
||||
hits = [
|
||||
i for i, w in enumerate(words)
|
||||
if rect.contains(pymupdf.Point((w[0] + w[2]) / 2, (w[1] + w[3]) / 2))
|
||||
]
|
||||
if not hits:
|
||||
continue
|
||||
if merged and merged[-1][0] == link["uri"] and hits[0] <= merged[-1][1][-1] + 1:
|
||||
merged[-1][1].extend(i for i in hits if i not in merged[-1][1])
|
||||
else:
|
||||
merged.append((link["uri"], hits))
|
||||
for uri, hits in merged:
|
||||
text = [words[i][4] for i in sorted(hits)]
|
||||
if any(URL_LIKE_WORD.search(word) for word in text):
|
||||
continue # URL als Linktext: erledigt link_urls
|
||||
first = min(hits)
|
||||
before = [w[4] for w in words[max(0, first - ANCHOR_CONTEXT_WORDS):first]]
|
||||
anchors.append((clean_uri(uri), before, text))
|
||||
return anchors
|
||||
|
||||
|
||||
def _words_pattern(words: list[str]) -> str:
|
||||
"""Wörter mit beliebigem Leerraum, Hervorhebung oder Trennung dazwischen."""
|
||||
parts = []
|
||||
for word in words:
|
||||
core = word.rstrip("-\u00ad") # Silbentrennung am Zeilenende
|
||||
parts.append(re.escape(core) + (r"-?" if core != word else ""))
|
||||
return r"[\s*_]*".join(parts)
|
||||
|
||||
|
||||
# Zwischen Kontextwörtern: Satzzeichen, Fußnotenziffern, Linkziele "](…)".
|
||||
CONTEXT_GAP = r"(?:[\W_\d]|\]\([^)]*\))*"
|
||||
|
||||
|
||||
def _context_pattern(words: list[str]) -> str:
|
||||
cores = [core for word in words for core in re.findall(r"[^\W\d_]+", word)]
|
||||
return CONTEXT_GAP.join(re.escape(core) for core in cores)
|
||||
|
||||
|
||||
def link_anchors(markdown: str, anchors: list[tuple[str, list[str], list[str]]]) -> str:
|
||||
"""Linktexte im Markdown als [Text](Ziel) setzen, in Lesereihenfolge."""
|
||||
pos = 0
|
||||
for uri, before, text in anchors:
|
||||
anchor = r"(?P<anchor>" + _words_pattern(text) + r")"
|
||||
patterns = []
|
||||
if context := _context_pattern(before):
|
||||
patterns.append(re.compile(context + CONTEXT_GAP + anchor))
|
||||
if len(" ".join(text)) >= 6:
|
||||
patterns.append(re.compile(r"(?<![\w\[])" + anchor)) # ohne Kontext
|
||||
for pattern in patterns:
|
||||
match = next(
|
||||
(m for m in pattern.finditer(markdown, pos)
|
||||
if not _inside_link(markdown, m.start("anchor"))),
|
||||
None,
|
||||
)
|
||||
if match:
|
||||
start, end = match.span("anchor")
|
||||
label = markdown[start:end].replace("[", "\\[").replace("]", "\\]")
|
||||
markdown = f"{markdown[:start]}[{label}]({uri}){markdown[end:]}"
|
||||
pos = start
|
||||
break
|
||||
return markdown
|
||||
|
||||
|
||||
def _inside_link(markdown: str, index: int) -> bool:
|
||||
"""Liegt die Stelle schon in einem Link (<…>, [..](…))?"""
|
||||
line_start = markdown.rfind("\n", 0, index) + 1
|
||||
before = markdown[line_start:index]
|
||||
return before.count("<") > before.count(">") or before.count("[") > before.count("]") or (
|
||||
before.rfind("](") > before.rfind(")")
|
||||
)
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# Korrektur für pymupdf4llm: Trennstriche am Zeilenende, OCR-Ebenen
|
||||
# ===========================================================================
|
||||
|
|
@ -1580,6 +1668,34 @@ def iter_headings(lines: list[str]):
|
|||
yield index, len(match.group(1)), match.group(2).strip()
|
||||
|
||||
|
||||
SPLIT_CHARS = 30_000 # ohne Gliederung: Abschnitte von etwa dieser Länge
|
||||
|
||||
|
||||
def split_by_length(markdown: str, size: int = SPLIT_CHARS) -> list[dict]:
|
||||
"""Ohne verwertbare Überschriften: an Absatzgrenzen in etwa gleich lange Stücke."""
|
||||
blocks, fence = [], False
|
||||
for part in re.split(r"(\n\n+)", markdown):
|
||||
if blocks and fence:
|
||||
blocks[-1] += part # Codeblöcke nicht zerteilen
|
||||
elif part.strip() or not blocks:
|
||||
blocks.append(part)
|
||||
else:
|
||||
blocks[-1] += part
|
||||
fence ^= sum(1 for line in part.split("\n") if FENCE.match(line)) % 2 == 1
|
||||
sections, current = [], ""
|
||||
for block in blocks:
|
||||
if current and len(current) + len(block) > size:
|
||||
sections.append(current)
|
||||
current = ""
|
||||
current += block
|
||||
if current.strip():
|
||||
sections.append(current)
|
||||
return [
|
||||
{"title": f"Abschnitt {n}", "body": body.strip() + "\n"}
|
||||
for n, body in enumerate(sections, start=1)
|
||||
]
|
||||
|
||||
|
||||
def split_markdown(markdown: str, mode: str) -> list[dict]:
|
||||
"""Buch in Abschnitte teilen: "teile" (oberste Ebene) oder "kapitel".
|
||||
|
||||
|
|
@ -1590,7 +1706,7 @@ def split_markdown(markdown: str, mode: str) -> list[dict]:
|
|||
counts = Counter(level for _, level, _ in headings)
|
||||
levels = sorted(level for level, n in counts.items() if n >= 2 and level <= 3)
|
||||
if not levels:
|
||||
return [{"title": "Vorspann", "body": markdown}]
|
||||
return split_by_length(markdown)
|
||||
part_level, chapter_level = levels[0], levels[-1]
|
||||
cut_levels = {part_level} if mode == "teile" else {part_level, chapter_level}
|
||||
|
||||
|
|
@ -1623,6 +1739,50 @@ def split_markdown(markdown: str, mode: str) -> list[dict]:
|
|||
return sections
|
||||
|
||||
|
||||
def _page_key(text: str) -> str:
|
||||
return re.sub(r"[\W_]+", "", clean_inline(text).lower())
|
||||
|
||||
|
||||
def add_source_pages(doc: pymupdf.Document, sections: list[dict]) -> None:
|
||||
"""Seiten der PDF (1-basiert) je Abschnitt.
|
||||
|
||||
Startseite aus der PDF-Gliederung, sonst die erste Seite (nach der vorigen),
|
||||
auf der die Überschrift steht – Verzeichnisseiten ausgenommen.
|
||||
"""
|
||||
outline = {norm_title(title): page - 1 for _, title, page in doc.get_toc() if page > 0}
|
||||
pages = [_page_key(page.get_text()) for page in doc]
|
||||
keys = [_page_key(section["title"])[:40] for section in sections]
|
||||
listing = { # Verzeichnisse: viele Abschnittstitel auf einer Seite
|
||||
i for i, text in enumerate(pages) if sum(1 for k in keys if k and k in text) >= 3
|
||||
}
|
||||
starts, page = [], 0
|
||||
for section, key in zip(sections, keys):
|
||||
found = outline.get(norm_title(section["title"]))
|
||||
if found is None and section["title"] != "Vorspann" and key:
|
||||
found = next(
|
||||
(i for i in range(page, len(pages)) if i not in listing and key in pages[i]),
|
||||
None,
|
||||
)
|
||||
page = max(page, found) if found is not None else page
|
||||
starts.append(page)
|
||||
for n, section in enumerate(sections):
|
||||
end = starts[n + 1] if n + 1 < len(sections) else len(pages) - 1
|
||||
# Beginnt der nächste Abschnitt oben auf einer Seite, endet dieser davor.
|
||||
if n + 1 < len(sections) and end > starts[n] and _starts_page(doc[end], keys[n + 1]):
|
||||
end -= 1
|
||||
section["source_pages"] = [starts[n] + 1, max(starts[n], end) + 1]
|
||||
|
||||
|
||||
def _starts_page(page: pymupdf.Page, key: str) -> bool:
|
||||
"""Steht über der Überschrift nur Kolumnentitel/Seitenzahl (≤ 3 Wörter je Block)?"""
|
||||
blocks = [b for b in page.get_text("blocks") if b[6] == 0 and b[4].strip()]
|
||||
heading = next((b for b in blocks if key and key in _page_key(b[4])), None)
|
||||
if heading is None or not _page_key(heading[4]).startswith(key):
|
||||
return False # nicht gefunden oder Text davor im selben Block
|
||||
above = [b for b in blocks if b[3] <= heading[1] + 1 and b is not heading]
|
||||
return all(len(b[4].split()) <= 3 for b in above)
|
||||
|
||||
|
||||
def write_split(folder: Path, sections: list[dict], meta: dict) -> list[Path]:
|
||||
"""Abschnitte als nummerierte Dateien plus index.md (Reihenfolge) schreiben."""
|
||||
folder.mkdir(parents=True, exist_ok=True)
|
||||
|
|
@ -1635,6 +1795,7 @@ def write_split(folder: Path, sections: list[dict], meta: dict) -> list[Path]:
|
|||
"part": section.get("part"),
|
||||
"chapter": section.get("chapter"),
|
||||
"order": order,
|
||||
"source_pages": section.get("source_pages"),
|
||||
"book": meta.get("title"),
|
||||
"author": meta.get("author"),
|
||||
}
|
||||
|
|
@ -1847,6 +2008,7 @@ def main() -> int:
|
|||
kinds, plan = plan_pages(doc, args.ocr, args.ocr_language, args.ocr_dpi)
|
||||
outline = load_outline(doc)
|
||||
uris = load_link_uris(doc)
|
||||
anchors = load_link_anchors(doc)
|
||||
page_count = doc.page_count
|
||||
file_title, file_author = title_author_from_filename(source.stem)
|
||||
title = (
|
||||
|
|
@ -1904,6 +2066,7 @@ def main() -> int:
|
|||
running_head_min=max(3, page_count // 10),
|
||||
)
|
||||
markdown = link_urls(markdown, uris)
|
||||
markdown = link_anchors(markdown, anchors)
|
||||
|
||||
base = folder or target.parent
|
||||
meta = {
|
||||
|
|
@ -1913,7 +2076,12 @@ def main() -> int:
|
|||
"cover": cover.relative_to(base).as_posix() if cover else None,
|
||||
}
|
||||
if folder:
|
||||
files = write_split(folder, split_markdown(markdown, args.split), meta)
|
||||
sections = split_markdown(markdown, args.split)
|
||||
if sections and sections[0]["title"] == "Abschnitt 1":
|
||||
print("Hinweis: keine Gliederung erkannt – nach Länge geteilt.")
|
||||
with pymupdf.open(source) as doc:
|
||||
add_source_pages(doc, sections)
|
||||
files = write_split(folder, sections, meta)
|
||||
else:
|
||||
target.write_text(make_frontmatter(meta) + markdown, encoding="utf-8")
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
import zipfile
|
||||
|
||||
import pymupdf
|
||||
|
||||
import pytest
|
||||
|
||||
import pdf2md as p
|
||||
|
|
@ -144,3 +146,89 @@ def test_jpeg_quality_changes_image_size(pdfs, tmp_path):
|
|||
return sum(f.stat().st_size for f in (out.parent / "buch_images").glob("*.jpg"))
|
||||
|
||||
assert size(30) < size(95)
|
||||
|
||||
|
||||
# --- Seitenzahlen, Teilen nach Länge -----------------------------------------------
|
||||
|
||||
def _book_pdf(outline: bool):
|
||||
doc = pymupdf.open()
|
||||
texts = [
|
||||
"Inhalt\n1 Anfang\n2 Weiter\n3 Schluss",
|
||||
"1 Anfang\nText eins.",
|
||||
"Noch mehr Text\nzum Anfang,\nüber mehrere\nZeilen.\n2 Weiter\nText zwei.",
|
||||
"3 Schluss\nText drei.",
|
||||
]
|
||||
for text in texts:
|
||||
doc.new_page().insert_text((72, 72), text, fontsize=11)
|
||||
if outline:
|
||||
doc.set_toc([[1, "1 Anfang", 2], [1, "2 Weiter", 3], [1, "3 Schluss", 4]])
|
||||
return doc
|
||||
|
||||
|
||||
@pytest.mark.parametrize("outline", [True, False])
|
||||
def test_source_pages(outline):
|
||||
sections = [{"title": t} for t in ("Vorspann", "1 Anfang", "2 Weiter", "3 Schluss")]
|
||||
p.add_source_pages(_book_pdf(outline), sections)
|
||||
assert [s["source_pages"] for s in sections] == [[1, 1], [2, 3], [3, 3], [4, 4]]
|
||||
|
||||
|
||||
def test_split_without_headings_by_length():
|
||||
paragraphs = [f"Absatz {n} " + " ".join(["Text"] * 40) for n in range(30)]
|
||||
code = "```\n" + "\n\n".join(f"zeile_{n} = {n}" for n in range(20)) + "\n```"
|
||||
markdown = "\n\n".join(paragraphs[:15] + [code] + paragraphs[15:]) + "\n"
|
||||
assert p.split_markdown(markdown, "kapitel")[0]["title"] == "Abschnitt 1"
|
||||
sections = p.split_by_length(markdown, size=1500)
|
||||
assert len(sections) > 3
|
||||
assert [s["title"] for s in sections[:2]] == ["Abschnitt 1", "Abschnitt 2"]
|
||||
assert all(s["body"].count("```") in (0, 2) for s in sections) # Code bleibt ganz
|
||||
assert "".join(s["body"] for s in sections).replace("\n", "") == markdown.replace("\n", "")
|
||||
|
||||
|
||||
def test_cli_split_writes_source_pages(pdfs, tmp_path):
|
||||
run(pdfs["digital"], tmp_path / "buch.md", "--split", "kapitel")
|
||||
files = {f.name: p.split_frontmatter(f.read_text())[0] for f in (tmp_path / "buch").glob("0*.md")}
|
||||
notar = next(meta for name, meta in files.items() if name.endswith("_1-der-notar.md"))
|
||||
assert notar["source_pages"] == [2, 3]
|
||||
|
||||
|
||||
# --- Links mit eigenem Text --------------------------------------------------------
|
||||
|
||||
def _link_pdf(path):
|
||||
doc = pymupdf.open()
|
||||
page = doc.new_page()
|
||||
page.insert_text((72, 72), "Den ganzen Bericht finden Sie hier im Netz.", fontsize=11)
|
||||
page.insert_text((72, 90), "Siehe auch www.example.org/info zum Thema.", fontsize=11)
|
||||
for word, uri in (("hier", "https://beispiel.de/bericht?utm_source=x"),
|
||||
("www.example.org/info", "https://www.example.org/info")):
|
||||
rect = page.search_for(word)[0]
|
||||
page.insert_link({"kind": pymupdf.LINK_URI, "from": rect, "uri": uri})
|
||||
doc.save(path)
|
||||
return path
|
||||
|
||||
|
||||
def test_load_link_anchors_skips_url_texts(tmp_path):
|
||||
doc = pymupdf.open(_link_pdf(tmp_path / "links.pdf"))
|
||||
assert p.load_link_anchors(doc) == [
|
||||
("https://beispiel.de/bericht", ["Bericht", "finden", "Sie"], ["hier"])
|
||||
]
|
||||
|
||||
|
||||
def test_link_anchors_uses_context_and_skips_existing_links():
|
||||
anchors = [("https://a.de/x", ["finden", "Sie"], ["hier"])]
|
||||
md = "Hier steht hier. Den Bericht finden Sie hier im Netz.\n"
|
||||
assert p.link_anchors(md, anchors) == (
|
||||
"Hier steht hier. Den Bericht finden Sie [hier](https://a.de/x) im Netz.\n"
|
||||
)
|
||||
# Fußnotenziffer und Trennung zwischen Kontext und Linktext stören nicht
|
||||
anchors = [("https://b.de/", ["überprüfen.81"], ["Landes-", "medienanstalten"])]
|
||||
md = "Die Landesmedienanstalten.\n\nzu überprüfen.[81 ] Die Landesmedienanstalten prüfen.\n"
|
||||
anchors = [("https://b.de/", ["überprüfen.81", "Die"], ["Landes-", "medienanstalten"])]
|
||||
assert "Die [Landesmedienanstalten](https://b.de/) prüfen" in p.link_anchors(md, anchors)
|
||||
|
||||
|
||||
def test_cli_sets_anchor_links(tmp_path):
|
||||
out = tmp_path / "links.md"
|
||||
run(_link_pdf(tmp_path / "links.pdf"), out)
|
||||
md = out.read_text()
|
||||
assert "[hier](https://beispiel.de/bericht)" in md
|
||||
assert "<https://www.example.org/info>" in md or "[www.example.org/info]" in md
|
||||
|
|
|
|||
Loading…
Reference in a new issue