"""Redline-Parser-Hilfsfunktionen — keine FastAPI-Abhängigkeiten. Wird von app.main._row_to_detail() und von Tests direkt importiert. """ from __future__ import annotations import re from urllib.parse import quote_plus def parse_redline_segments(vorschlag: str | None) -> list[dict]: """Parst §INS§text§INS§/§DEL§text§DEL§-Marker sowie **text**- und ~~text~~-Markdown in eine Liste von {type, text}-Segmenten (ctx/ins/del). Toleriert beide Formate gleichzeitig. Unausgewogene Marker bleiben als ctx. Leerer oder None-Input liefert []. Beispiel: >>> parse_redline_segments("§ 3 §DEL§alt§DEL§ §INS§neu§INS§ Ende") [{'type': 'ctx', 'text': '§ 3 '}, {'type': 'del', 'text': 'alt'}, {'type': 'ctx', 'text': ' '}, {'type': 'ins', 'text': 'neu'}, {'type': 'ctx', 'text': ' Ende'}] """ if not vorschlag: return [] # Normalisierung: §INS§...§INS§ und §DEL§...§DEL§ → interne Tags text = vorschlag text = re.sub(r"§INS§(.*?)§INS§", r"\1", text, flags=re.DOTALL) text = re.sub(r"§DEL§(.*?)§DEL§", r"\1", text, flags=re.DOTALL) # Markdown-Konvention: **...** → ins, ~~...~~ → del text = re.sub(r"\*\*(.*?)\*\*", r"\1", text, flags=re.DOTALL) text = re.sub(r"~~(.*?)~~", r"\1", text, flags=re.DOTALL) # Splitten an Tags, Typen zuordnen segments: list[dict] = [] parts = re.split(r"(.*?|.*?)", text, flags=re.DOTALL) for part in parts: if not part: continue ins_m = re.fullmatch(r"(.*)", part, re.DOTALL) del_m = re.fullmatch(r"(.*)", part, re.DOTALL) if ins_m: segments.append({"type": "ins", "text": ins_m.group(1)}) elif del_m: segments.append({"type": "del", "text": del_m.group(1)}) else: segments.append({"type": "ctx", "text": part}) return segments def build_pdf_href(zitat: dict, bundesland: str = "") -> str: """Gibt den pdf_href für ein Zitat zurück. Bevorzugt das bereits gepflegte url-Feld. Falls leer, rekonstruiert die URL aus dem quelle-Feld (Format: 'Titel · S. N' oder 'Titel, S. N') über die WAHLPROGRAMME-Registry. """ url = zitat.get("url", "") if url: return url quelle = zitat.get("quelle", "") seite_m = re.search(r"[·,]?\s*S\.\s*(\d+)", quelle) if not seite_m: return "" seite = seite_m.group(1) # pid aus WAHLPROGRAMME-Registry ermitteln: Dateiname ohne .pdf from .wahlprogramme import WAHLPROGRAMME pid = "" for bl_data in WAHLPROGRAMME.values(): for partei_data in bl_data.values(): titel = partei_data.get("titel", "") partei_name = partei_data.get("partei", "") file_name = partei_data.get("file", "") if titel and (titel in quelle or partei_name in quelle): pid = file_name.replace(".pdf", "") break if pid: break if not pid: return "" text = zitat.get("text", "") q = " ".join(text.split()[:5]) # #page=N als URL-Hash, damit der Browser-PDF-Viewer direkt zur Seite # springt — OpenAction im PDF wird von Chrome/Firefox ignoriert. return f"/api/wahlprogramm-cite?pid={pid}&seite={seite}&q={quote_plus(q)}#page={seite}"