URLs als anklickbare Links, zerbrochene URLs reparieren
- load_link_uris: Linkziele aus den Link-Annotationen der PDF lesen - link_urls: URLs als Autolink <https://…> schreiben; beim Zeilenumbruch zerbrochene URLs mit den Linkzielen abgleichen (tolerant gegenüber Bindestrichen, Leerraum, "/" am Ende, Abfrageteil, .html) und utm-Parameter entfernen; ohne Linkziel aus dem gedruckten Text zusammensetzen - Bindestrich am Zeilenende innerhalb einer URL bleibt erhalten - pandoc liest mit +autolink_bare_uris - Tests, README, Bedienungsanleitung und TODO ergänzt Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
f9dfa3a52e
commit
619421ef10
5 changed files with 221 additions and 3 deletions
|
|
@ -97,6 +97,7 @@ Wenn du kein Cover und kein Probe-EPUB brauchst, reicht `--images` statt `--epub
|
||||||
| Zeilenumbruch innerhalb eines Absatzes | einfacher Umbruch | wird zu einem Leerzeichen |
|
| Zeilenumbruch innerhalb eines Absatzes | einfacher Umbruch | wird zu einem Leerzeichen |
|
||||||
| Bild | `` auf eigener Zeile | Bild an dieser Stelle |
|
| Bild | `` auf eigener Zeile | Bild an dieser Stelle |
|
||||||
| kursiv / fett | `_Wort_` / `**Wort**` | kursiv / fett |
|
| kursiv / fett | `_Wort_` / `**Wort**` | kursiv / fett |
|
||||||
|
| Link | `<https://beispiel.de/seite>` oder `[Text](https://beispiel.de/seite)` | anklickbarer Link |
|
||||||
|
|
||||||
Wichtig:
|
Wichtig:
|
||||||
|
|
||||||
|
|
@ -134,7 +135,7 @@ Im Ordner der Markdown-Datei starten (wichtig für die Bilder):
|
||||||
```bash
|
```bash
|
||||||
cd ~/Downloads/ljutsch/Output
|
cd ~/Downloads/ljutsch/Output
|
||||||
pandoc band1.md \
|
pandoc band1.md \
|
||||||
--from markdown-yaml_metadata_block --to epub3 \
|
--from markdown-yaml_metadata_block+autolink_bare_uris --to epub3 \
|
||||||
--output band1.epub \
|
--output band1.epub \
|
||||||
--resource-path . \
|
--resource-path . \
|
||||||
--split-level=3 --toc-depth=3 \
|
--split-level=3 --toc-depth=3 \
|
||||||
|
|
@ -239,6 +240,7 @@ Kolumnentitel und Seitenzahlen gedruckter Bücher werden automatisch entfernt, g
|
||||||
| Überschrift besteht nur aus einer Zahl (`## 90`, `## XX`) | meist Fußnotenzeichen oder Seitenzahl → in Weg B löschen |
|
| Überschrift besteht nur aus einer Zahl (`## 90`, `## XX`) | meist Fußnotenzeichen oder Seitenzahl → in Weg B löschen |
|
||||||
| Buchstaben fehlen („Offzielle“, „treff ch“) | Ligatur (fi, fl) ohne Textzuordnung in der PDF; von Hand korrigieren |
|
| Buchstaben fehlen („Offzielle“, „treff ch“) | Ligatur (fi, fl) ohne Textzuordnung in der PDF; von Hand korrigieren |
|
||||||
| Überschrift gesperrt („M e i n k l e i nes Stück“) | Buchstaben einzeln gesetzt in der PDF; von Hand korrigieren |
|
| Überschrift gesperrt („M e i n k l e i nes Stück“) | Buchstaben einzeln gesetzt in der PDF; von Hand korrigieren |
|
||||||
|
| Link führt ins Leere oder ist abgeschnitten | URL in der PDF nachsehen und in `<…>` korrigieren; manche PDFs enthalten selbst abgeschnittene Linkziele |
|
||||||
| Formeln erscheinen als Bilder | bei Mathebüchern normal; dafür ist das Programm nicht gebaut |
|
| Formeln erscheinen als Bilder | bei Mathebüchern normal; dafür ist das Programm nicht gebaut |
|
||||||
| Alter Scan: falsche Überschriften, viele Lesefehler | `--ocr always` (vorhandene OCR-Ebene ignorieren) |
|
| Alter Scan: falsche Überschriften, viele Lesefehler | `--ocr always` (vorhandene OCR-Ebene ignorieren) |
|
||||||
| Auf Scans fehlen Leerzeichen („deutetin“) | OCR-Fehler; höhere Auflösung versuchen (`--ocr-dpi 400`) oder von Hand korrigieren |
|
| Auf Scans fehlen Leerzeichen („deutetin“) | OCR-Fehler; höhere Auflösung versuchen (`--ocr-dpi 400`) oder von Hand korrigieren |
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,11 @@ python3 pdf2md.py --epub --lang en --ocr-language eng --ocr always scan.pdf scan
|
||||||
- **Inhaltsverzeichnis:** zusammengeklebte Einträge werden getrennt; liegt das Verzeichnis
|
- **Inhaltsverzeichnis:** zusammengeklebte Einträge werden getrennt; liegt das Verzeichnis
|
||||||
als Tabelle vor, bleibt die Tabelle erhalten.
|
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
|
||||||
|
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.
|
||||||
- **Seitenzahlen, Kolumnentitel, Text in Fotos** werden entfernt.
|
- **Seitenzahlen, Kolumnentitel, Text in Fotos** werden entfernt.
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
@ -88,6 +93,7 @@ synthetisch erzeugt.
|
||||||
- **Was am PDF liegt, bleibt:** fehlende Ligaturen ohne Textzuordnung („Offzielle“),
|
- **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
|
gesperrt gesetzte Überschriften („M e i n k l e i nes …“), flache PDF-Gliederungen
|
||||||
(alle Kapitel auf einer Ebene).
|
(alle Kapitel auf einer Ebene).
|
||||||
|
- Links mit anderem Text als der URL (z. B. „hier“) werden nicht übernommen.
|
||||||
- Reine Zahlen können als Überschrift erscheinen (Fußnotenzeichen, römische Seitenzahlen).
|
- Reine Zahlen können als Überschrift erscheinen (Fußnotenzeichen, römische Seitenzahlen).
|
||||||
Weitere bekannte Schwächen: [TODO.md](TODO.md).
|
Weitere bekannte Schwächen: [TODO.md](TODO.md).
|
||||||
- Bilder werden als JPEG (Qualität 95) gespeichert; EPUB Band 1 ca. 5 MB.
|
- Bilder werden als JPEG (Qualität 95) gespeichert; EPUB Band 1 ca. 5 MB.
|
||||||
|
|
|
||||||
4
TODO.md
4
TODO.md
|
|
@ -72,3 +72,7 @@ wieder zusammengesetzt = Gesamt-Markdown.
|
||||||
wegen echter Kapitel „I.“, „II.“.
|
wegen echter Kapitel „I.“, „II.“.
|
||||||
- Gesperrte Überschriften („M e i n k l e i nes Stück D D R“) bleiben zerrissen.
|
- Gesperrte Überschriften („M e i n k l e i nes Stück D D R“) bleiben zerrissen.
|
||||||
- Alte Scans mit eigener OCR-Ebene: `--ocr always` liefert meist besseren Text.
|
- Alte Scans mit eigener OCR-Ebene: `--ocr always` liefert meist besseren Text.
|
||||||
|
- 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).
|
||||||
|
|
|
||||||
135
pdf2md.py
135
pdf2md.py
|
|
@ -21,6 +21,7 @@ import re
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import urllib.parse
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
from concurrent.futures import ProcessPoolExecutor
|
from concurrent.futures import ProcessPoolExecutor
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
@ -487,6 +488,131 @@ def load_outline(doc: pymupdf.Document) -> dict[str, int]:
|
||||||
return levels
|
return levels
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# Links
|
||||||
|
# ===========================================================================
|
||||||
|
# pymupdf4llm übernimmt keine Link-Annotationen, und gedruckte URLs zerbrechen
|
||||||
|
# beim Zeilenumbruch ("deutsche-|sehen" -> "deutschesehen", ".../ youtube-...").
|
||||||
|
# Wir gleichen URLs im Text mit den Linkzielen der PDF ab, ersetzen sie durch
|
||||||
|
# das korrekte Ziel und schreiben sie als Autolink <https://…>, den pandoc im
|
||||||
|
# EPUB als echten Link ausgibt.
|
||||||
|
|
||||||
|
URL_START = re.compile(
|
||||||
|
r"(?<![<\]/A-Za-z0-9])(?<!\]\()(?:https?://|www\.)", re.IGNORECASE
|
||||||
|
) # nicht in <…>-Links und [Text](…)-Zielen
|
||||||
|
URL_TRAILING = ".,;:!?)]}»«“”„\"'*_"
|
||||||
|
MAX_URL_PARTS = 4 # eine URL kann über so viele Zeilen gebrochen sein
|
||||||
|
# Ohne Linkziel: Folgewort gehört zur URL, wenn diese an einer typischen
|
||||||
|
# Umbruchstelle endet und das Wort wie ein URL-Teil aussieht.
|
||||||
|
URL_BREAK_AFTER = "/_-.=&?#"
|
||||||
|
URL_CONTINUATION = re.compile(r"^(?:\S*/\S*|(?:html?|php|pdf|aspx?)\W*)$", re.IGNORECASE)
|
||||||
|
|
||||||
|
|
||||||
|
def url_key(text: str) -> str:
|
||||||
|
"""Vergleichsschlüssel: Bindestriche, Leerraum und "/" am Ende zählen nicht."""
|
||||||
|
return re.sub(r"[-\s]", "", text).rstrip("/").lower()
|
||||||
|
|
||||||
|
|
||||||
|
def uri_keys(uri: str) -> set[str]:
|
||||||
|
"""Schlüssel, unter denen ein Linkziel im gedruckten Text auftauchen kann.
|
||||||
|
|
||||||
|
Gedruckt fehlen oft Abfrageteil ("?utm_source=…") oder Endung (".html").
|
||||||
|
"""
|
||||||
|
base = uri.split("#")[0]
|
||||||
|
keys = set()
|
||||||
|
for variant in (uri, base, base.split("?")[0]):
|
||||||
|
keys.add(url_key(variant))
|
||||||
|
keys.add(url_key(re.sub(r"\.(?:html?|php|aspx?)$", "", variant.rstrip("/"))))
|
||||||
|
return keys
|
||||||
|
|
||||||
|
|
||||||
|
def clean_uri(uri: str) -> str:
|
||||||
|
"""Tracking-Parameter (utm_…) entfernen."""
|
||||||
|
parts = urllib.parse.urlsplit(uri)
|
||||||
|
if "utm_" not in parts.query.lower():
|
||||||
|
return uri
|
||||||
|
query = [
|
||||||
|
(k, v) for k, v in urllib.parse.parse_qsl(parts.query, keep_blank_values=True)
|
||||||
|
if not k.lower().startswith("utm_")
|
||||||
|
]
|
||||||
|
return urllib.parse.urlunsplit(parts._replace(query=urllib.parse.urlencode(query)))
|
||||||
|
|
||||||
|
|
||||||
|
def load_link_uris(doc: pymupdf.Document) -> list[str]:
|
||||||
|
"""Web-Linkziele der PDF (Link-Annotationen), ohne Dubletten."""
|
||||||
|
uris: dict[str, None] = {}
|
||||||
|
for page in doc:
|
||||||
|
for link in page.get_links():
|
||||||
|
uri = (link.get("uri") or "").strip()
|
||||||
|
if re.match(r"https?://", uri, re.IGNORECASE):
|
||||||
|
uris.setdefault(uri.replace(" ", "%20"))
|
||||||
|
return list(uris)
|
||||||
|
|
||||||
|
|
||||||
|
def _autolink(url: str) -> str:
|
||||||
|
if url.lower().startswith("www."):
|
||||||
|
return f"[{url}](https://{url})"
|
||||||
|
return f"<{url}>"
|
||||||
|
|
||||||
|
|
||||||
|
def _match_known(tokens: list[str], known: dict[str, str]) -> tuple[str, int] | None:
|
||||||
|
"""Längste Verbindung von Wörtern, die zu einem Linkziel der PDF passt."""
|
||||||
|
for count in range(min(MAX_URL_PARTS, len(tokens)), 0, -1):
|
||||||
|
parts = [t.rstrip() for t in tokens[:count]]
|
||||||
|
joined = "".join(parts)
|
||||||
|
consumed = sum(map(len, tokens[: count - 1])) + len(parts[-1])
|
||||||
|
for strip in range(len(joined)):
|
||||||
|
candidate = joined[: len(joined) - strip]
|
||||||
|
if uri := known.get(url_key(candidate)):
|
||||||
|
return uri, consumed - strip
|
||||||
|
if candidate[-1] not in URL_TRAILING:
|
||||||
|
break
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _match_printed(tokens: list[str]) -> tuple[str, int]:
|
||||||
|
"""URL ohne (passendes) Linkziel: aus dem gedruckten Text zusammensetzen."""
|
||||||
|
parts = [tokens[0].rstrip()]
|
||||||
|
for token in tokens[1:MAX_URL_PARTS]:
|
||||||
|
if parts[-1][-1] in URL_BREAK_AFTER and URL_CONTINUATION.match(token.rstrip()):
|
||||||
|
parts.append(token.rstrip())
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
joined = "".join(parts)
|
||||||
|
url = joined.rstrip(URL_TRAILING)
|
||||||
|
consumed = sum(map(len, tokens[: len(parts) - 1])) + len(parts[-1])
|
||||||
|
return url, consumed - (len(joined) - len(url))
|
||||||
|
|
||||||
|
|
||||||
|
def _link_line(line: str, known: dict[str, str]) -> str:
|
||||||
|
out = []
|
||||||
|
pos = 0
|
||||||
|
while match := URL_START.search(line, pos):
|
||||||
|
out.append(line[pos:match.start()])
|
||||||
|
tokens = re.findall(r"\S+\s*", line[match.start():])
|
||||||
|
url, consumed = _match_known(tokens, known) or _match_printed(tokens)
|
||||||
|
out.append(_autolink(url) if "." in url else line[match.start():][:consumed])
|
||||||
|
pos = match.start() + consumed
|
||||||
|
out.append(line[pos:])
|
||||||
|
return "".join(out)
|
||||||
|
|
||||||
|
|
||||||
|
def link_urls(markdown: str, uris: list[str] | None = None) -> str:
|
||||||
|
"""URLs im Fließtext anklickbar machen und mit PDF-Linkzielen reparieren."""
|
||||||
|
known: dict[str, str] = {}
|
||||||
|
for uri in uris or []:
|
||||||
|
for key in uri_keys(uri):
|
||||||
|
known.setdefault(key, clean_uri(uri))
|
||||||
|
lines = markdown.split("\n")
|
||||||
|
in_fence = False
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if FENCE.match(line):
|
||||||
|
in_fence = not in_fence
|
||||||
|
elif not in_fence and not IMAGE.match(line):
|
||||||
|
lines[i] = _link_line(line, known)
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
# Korrektur für pymupdf4llm: Trennstriche am Zeilenende, OCR-Ebenen
|
# Korrektur für pymupdf4llm: Trennstriche am Zeilenende, OCR-Ebenen
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
|
|
@ -522,7 +648,10 @@ def _mark_line_end_hyphens(spans: list[dict]) -> list[dict]:
|
||||||
span = {**span, "text": text[:-1] + "-"}
|
span = {**span, "text": text[:-1] + "-"}
|
||||||
elif line_end and text.endswith("-"):
|
elif line_end and text.endswith("-"):
|
||||||
word = FIRST_WORD.match(nxt["text"].lstrip())
|
word = FIRST_WORD.match(nxt["text"].lstrip())
|
||||||
if word and word.group(0).lower() in CONJUNCTIONS:
|
last = text.split()[-1]
|
||||||
|
if URL_START.search(last) or "/" in last:
|
||||||
|
span = {**span, "text": text + KEEP_HYPHEN} # URLs trennt niemand
|
||||||
|
elif word and word.group(0).lower() in CONJUNCTIONS:
|
||||||
span = {**span, "text": text + KEEP_HYPHEN_SPACE}
|
span = {**span, "text": text + KEEP_HYPHEN_SPACE}
|
||||||
elif word and word.group(0)[0].isupper():
|
elif word and word.group(0)[0].isupper():
|
||||||
span = {**span, "text": text + KEEP_HYPHEN}
|
span = {**span, "text": text + KEEP_HYPHEN}
|
||||||
|
|
@ -945,7 +1074,7 @@ def write_epub(
|
||||||
cmd = [
|
cmd = [
|
||||||
pandoc,
|
pandoc,
|
||||||
str(markdown_path),
|
str(markdown_path),
|
||||||
"--from", "markdown-yaml_metadata_block",
|
"--from", "markdown-yaml_metadata_block+autolink_bare_uris",
|
||||||
"--to", "epub3",
|
"--to", "epub3",
|
||||||
"--output", str(epub_path),
|
"--output", str(epub_path),
|
||||||
"--resource-path", str(markdown_path.parent),
|
"--resource-path", str(markdown_path.parent),
|
||||||
|
|
@ -1075,6 +1204,7 @@ def main() -> int:
|
||||||
with pymupdf.open(source) as doc:
|
with pymupdf.open(source) as doc:
|
||||||
kinds, plan = plan_pages(doc, args.ocr, args.ocr_language, args.ocr_dpi)
|
kinds, plan = plan_pages(doc, args.ocr, args.ocr_language, args.ocr_dpi)
|
||||||
outline = load_outline(doc)
|
outline = load_outline(doc)
|
||||||
|
uris = load_link_uris(doc)
|
||||||
page_count = doc.page_count
|
page_count = doc.page_count
|
||||||
title = (doc.metadata or {}).get("title") or source.stem
|
title = (doc.metadata or {}).get("title") or source.stem
|
||||||
author = (doc.metadata or {}).get("author") or ""
|
author = (doc.metadata or {}).get("author") or ""
|
||||||
|
|
@ -1124,6 +1254,7 @@ def main() -> int:
|
||||||
drop_running_heads=not args.keep_headers,
|
drop_running_heads=not args.keep_headers,
|
||||||
running_head_min=max(3, page_count // 10),
|
running_head_min=max(3, page_count // 10),
|
||||||
)
|
)
|
||||||
|
markdown = link_urls(markdown, uris)
|
||||||
target.write_text(markdown, encoding="utf-8")
|
target.write_text(markdown, encoding="utf-8")
|
||||||
|
|
||||||
if epub_path:
|
if epub_path:
|
||||||
|
|
|
||||||
|
|
@ -326,3 +326,78 @@ def test_make_image_links_relative(tmp_path, monkeypatch):
|
||||||
"\n\n\n\n"
|
"\n\n\n\n"
|
||||||
"\n"
|
"\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Links -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def test_link_urls_repairs_broken_urls_with_pdf_targets():
|
||||||
|
uris = [
|
||||||
|
"https://www.theeuropean.de/insa-umfrage-deutsche-sehen/",
|
||||||
|
"https://www.fsm.de/en/fsm/netzdg/",
|
||||||
|
]
|
||||||
|
md = (
|
||||||
|
"Quelle: https://www.theeuropean.de/insa-umfrage-deutschesehen/.\n"
|
||||||
|
"Siehe https://www.fsm.de/en/fsm/ netzdg/. Danach Text.\n"
|
||||||
|
)
|
||||||
|
assert p.link_urls(md, uris) == (
|
||||||
|
"Quelle: <https://www.theeuropean.de/insa-umfrage-deutsche-sehen/>.\n"
|
||||||
|
"Siehe <https://www.fsm.de/en/fsm/netzdg/>. Danach Text.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_link_urls_without_pdf_targets():
|
||||||
|
md = "Mehr unter https://example.org/a_b, www.example.org und (https://x.de/y).\n"
|
||||||
|
assert p.link_urls(md) == (
|
||||||
|
"Mehr unter <https://example.org/a_b>, [www.example.org](https://www.example.org)"
|
||||||
|
" und (<https://x.de/y>).\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_link_urls_leaves_images_and_existing_links_alone():
|
||||||
|
md = "\n\n<https://a.de/> und [b](https://b.de/)\n"
|
||||||
|
assert p.link_urls(md) == md
|
||||||
|
|
||||||
|
|
||||||
|
def test_pymupdf4llm_patch_keeps_hyphen_inside_url():
|
||||||
|
spans = [
|
||||||
|
{"text": "Siehe https://a.de/umfrage-deutsche-", "flags": 0, "char_flags": 16,
|
||||||
|
"font": "x", "block": 0, "line": 0},
|
||||||
|
{"text": "sehen/ und mehr.", "flags": 0, "char_flags": 16, "font": "x",
|
||||||
|
"block": 0, "line": 1},
|
||||||
|
]
|
||||||
|
output, _ = p.document_layout.get_styled_text(spans)
|
||||||
|
assert output.strip() == "Siehe https://a.de/umfrage-deutsche-sehen/ und mehr."
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_link_uris():
|
||||||
|
doc = pymupdf.open()
|
||||||
|
page = doc.new_page()
|
||||||
|
for uri in ("https://a.de/x", "mailto:a@b.de", "https://a.de/x"):
|
||||||
|
page.insert_link({"kind": pymupdf.LINK_URI, "from": pymupdf.Rect(0, 0, 10, 10),
|
||||||
|
"uri": uri})
|
||||||
|
doc = pymupdf.open("pdf", doc.tobytes())
|
||||||
|
assert p.load_link_uris(doc) == ["https://a.de/x"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_link_urls_tolerates_printed_variants():
|
||||||
|
uris = [
|
||||||
|
"https://a.de/artikel?utm_source=chatgpt.com",
|
||||||
|
"https://b.de/news/Gesetz-Kritik-3718401.html",
|
||||||
|
"https://c.de/seite",
|
||||||
|
]
|
||||||
|
md = "Siehe https://a.de/artikel/, https://b.de/news/ Gesetz-Kritik-3718401 und https://c.de/seite/.\n"
|
||||||
|
assert p.link_urls(md, uris) == (
|
||||||
|
"Siehe <https://a.de/artikel>, <https://b.de/news/Gesetz-Kritik-3718401.html> und "
|
||||||
|
"<https://c.de/seite>.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_link_urls_joins_printed_continuation_without_pdf_target():
|
||||||
|
md = (
|
||||||
|
"Siehe https://a.org/digital_ dispatches/towards-x/. und https://b.de/ "
|
||||||
|
"Docs/artikel. html. Oder https://c.de/ und mehr.\n"
|
||||||
|
)
|
||||||
|
assert p.link_urls(md) == (
|
||||||
|
"Siehe <https://a.org/digital_dispatches/towards-x/>. und "
|
||||||
|
"<https://b.de/Docs/artikel.html>. Oder <https://c.de/> und mehr.\n"
|
||||||
|
)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue