#!/usr/bin/env python3 """ post-process-docx.py ==================== Wird auf das von Pandoc erzeugte DOCX angewendet, NACH `build.ps1`. Setzt Per-Bullet-keepNext-Markierungen, die ein Stil nicht abbilden kann: 3-3-Regel fuer Listen-Bullets: - Eine Liste ist eine Sequenz aufeinanderfolgender Absaetze mit -Eigenschaft im Body (nicht innerhalb von Tabellen-Zellen). - Bei einer Liste mit weniger als 6 Bullets: alle Bullets bekommen (Liste bleibt unteilbar — bei <6 ist die 3-3-Regel sowieso nur durch Zusammenhalten aller erfuellbar). - Bei einer Liste mit 6 oder mehr Bullets: die ersten 2 und die drittletzten und vorletzten Bullets bekommen . Damit gilt: nach Bullet 1 darf nicht getrennt werden (1+2+3 zusammen), und nach Bullet N-3 darf nicht getrennt werden (N-2+N-1+N zusammen). Trennen ist erlaubt zwischen den Bullets in der Mitte. Bullets in Tabellen-Zellen werden uebersprungen — Compact wird auch fuer Tabellen-Zellen-Inhalte verwendet, dort wollen wir kein keepNext. Voraussetzungen: nur Python-Stdlib. """ from __future__ import annotations import re import sys import zipfile from pathlib import Path SCRIPT_DIR = Path(__file__).resolve().parent BASE_DIR = SCRIPT_DIR.parent DOCX_FILE = BASE_DIR / "output" / "Lebenslauf_Dr-Ing_Thomas_Langer.docx" W_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main" def log(msg: str) -> None: print(f"[post-process-docx] {msg}", flush=True) def is_bullet_paragraph(p_xml: str) -> bool: """True wenn Absatz-XML eine numPr-Eigenschaft hat (= Listen-Bullet).""" return " bool: return " str: """Fuegt in das pPr-Element ein. Falls kein pPr existiert, wird es angelegt. Idempotent (wenn schon vorhanden, unveraendert).""" if has_keep_next(p_xml): return p_xml if "" in p_xml: return p_xml.replace("", "", 1) if "" in p_xml: return p_xml.replace("", "", 1) # kein pPr: vor oder vor new_ppr = "" if "", "" + new_ppr, 1) \ if p_xml.startswith("") else p_xml return p_xml.replace("", new_ppr + "", 1) # Regex: ein ..., optional gefolgt vom oeffnenden Marker fuer # Tabelle () oder schliessenden Body (). Wir splitten nicht, # sondern iterieren paragraphenweise und tracken Tabellen-Schachtelung. P_RE = re.compile(r"]*>.*?", re.DOTALL) TBL_OPEN = "" TBL_CLOSE = "" def process_document_xml(xml: str) -> tuple[str, dict]: """Findet Listen-Sequenzen ausserhalb von Tabellen, wendet 3-3-Regel an. Gibt das modifizierte XML und Statistiken zurueck.""" # Tokenize: ...-Bereiche markieren, damit wir sie ueberspringen. # Ansatz: wir gehen durch das XML und tracken aktuelle Tabellen-Tiefe. # Wenn Tiefe > 0: Bullets in Tabellen-Zellen ueberspringen. out = [] pos = 0 table_depth = 0 bullet_run: list[tuple[int, str]] = [] # (out_idx, p_xml) Indizes in out stats = {"lists": 0, "bullets_in_lists": 0, "bullets_keepnext": 0, "skipped_in_tables": 0} def flush_run(): if not bullet_run: return n = len(bullet_run) stats["lists"] += 1 stats["bullets_in_lists"] += n if n < 6: indices_keep = list(range(n)) else: indices_keep = [0, 1, n-3, n-2] for k in indices_keep: idx, p_xml = bullet_run[k] new_xml = add_keep_next(p_xml) if new_xml != p_xml: out[idx] = new_xml stats["bullets_keepnext"] += 1 bullet_run.clear() # Wir scannen das XML linear nach ..., , # und sammeln Bullet-Sequenzen ausserhalb von Tabellen. # Dafuer iterieren wir mit einem regex der ALLE drei Token findet. token_re = re.compile( r"(?P" + re.escape(TBL_OPEN) + r")" r"|(?P" + re.escape(TBL_CLOSE) + r")" r"|(?P]*>.*?)", re.DOTALL, ) last_end = 0 for m in token_re.finditer(xml): # nicht-tokenisierten Text dazwischen anhaengen if m.start() > last_end: out.append(xml[last_end:m.start()]) last_end = m.end() if m.group("tblopen"): flush_run() # Listen vor Tabelle abschliessen table_depth += 1 out.append(m.group()) elif m.group("tblclose"): flush_run() # innerhalb-Tabellen-Listen wir flushen, aber haben # sie eh nicht angesammelt table_depth -= 1 out.append(m.group()) else: p_xml = m.group("para") out.append(p_xml) if table_depth > 0: # Bullets in Tabellen-Zellen ignorieren if is_bullet_paragraph(p_xml): stats["skipped_in_tables"] += 1 # nicht-bullet-paragraph in tabelle: kein effekt continue if is_bullet_paragraph(p_xml): bullet_run.append((len(out) - 1, p_xml)) else: # Sequenz-Ende: 3-3-Regel anwenden flush_run() # Rest hinten dranhaengen if last_end < len(xml): out.append(xml[last_end:]) flush_run() # falls Liste am Body-Ende return "".join(out), stats def main() -> int: if not DOCX_FILE.exists(): sys.stderr.write(f"FEHLER: {DOCX_FILE} existiert nicht. " f"Erst build.ps1 laufen lassen.\n") return 1 log(f"Verarbeite: {DOCX_FILE}") # DOCX in memory einlesen with zipfile.ZipFile(DOCX_FILE, "r") as z: members = {name: z.read(name) for name in z.namelist()} doc_xml = members["word/document.xml"].decode("utf-8") new_xml, stats = process_document_xml(doc_xml) if new_xml == doc_xml: log(" keine Aenderung — keine bullet-Listen gefunden oder bereits gesetzt") members["word/document.xml"] = new_xml.encode("utf-8") # DOCX zurueckschreiben (mode='w' truncatet) with zipfile.ZipFile(DOCX_FILE, "w", zipfile.ZIP_DEFLATED) as z: # [Content_Types].xml zuerst order = sorted(members.keys(), key=lambda n: (0 if n == "[Content_Types].xml" else 1, n)) for name in order: z.writestr(name, members[name]) log(f" Listen gefunden: {stats['lists']}") log(f" Bullets in Listen: {stats['bullets_in_lists']}") log(f" keepNext gesetzt: {stats['bullets_keepnext']}") log(f" Bullets in Tabellen uebersprungen: {stats['skipped_in_tables']}") log("Fertig.") return 0 if __name__ == "__main__": sys.exit(main())