89 lines
3.2 KiB
Python
89 lines
3.2 KiB
Python
|
|
"""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"<INS>\1</INS>", text, flags=re.DOTALL)
|
||
|
|
text = re.sub(r"§DEL§(.*?)§DEL§", r"<DEL>\1</DEL>", text, flags=re.DOTALL)
|
||
|
|
# Markdown-Konvention: **...** → ins, ~~...~~ → del
|
||
|
|
text = re.sub(r"\*\*(.*?)\*\*", r"<INS>\1</INS>", text, flags=re.DOTALL)
|
||
|
|
text = re.sub(r"~~(.*?)~~", r"<DEL>\1</DEL>", text, flags=re.DOTALL)
|
||
|
|
|
||
|
|
# Splitten an Tags, Typen zuordnen
|
||
|
|
segments: list[dict] = []
|
||
|
|
parts = re.split(r"(<INS>.*?</INS>|<DEL>.*?</DEL>)", text, flags=re.DOTALL)
|
||
|
|
for part in parts:
|
||
|
|
if not part:
|
||
|
|
continue
|
||
|
|
ins_m = re.fullmatch(r"<INS>(.*)</INS>", part, re.DOTALL)
|
||
|
|
del_m = re.fullmatch(r"<DEL>(.*)</DEL>", 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])
|
||
|
|
return f"/api/wahlprogramm-cite?pid={pid}&seite={seite}&q={quote_plus(q)}"
|