#!/usr/bin/env python3 """ post-process-docx.py ==================== Wird auf das von Pandoc erzeugte DOCX angewendet, NACH `build.ps1`. Macht zwei XML-Modifikationen, die ein Stil oder die `reference.docx` nicht abbilden koennen: 1. 3-3-Regel fuer Listen-Bullets (B3.5): - 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. 2. H2-Trennlinie (S08): - Nach jedem H2-Absatz wird ein leerer Trenn-Absatz eingefuegt. - Trenn-Absatz: linksbuendige Bottom-Border, schwarz (000000), 1,25 pt (sz=10), 8,6 cm Linienlaenge (right-Indent 4196 dxa bei 9072 dxa Textbreite). - Run-Properties auf sz=2 (1 pt), damit der Absatz selbst minimale Hoehe hat. - Ein schmaler-als-Heading-Border ist ueber den Heading-Stil selbst nicht moeglich, weil Words right-Indent sowohl Text als auch Border begrenzt. Deshalb separater Trenn-Absatz. 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" # H2-Trenn-Absatz: linksbuendige Bottom-Border, schwarz, 8,6 cm lang, 1,25 pt dick. # Textbreite = PAGE_W - MARGIN_LEFT - MARGIN_RIGHT = 11906 - 1417 - 1417 = 9072 dxa # 8,6 cm = 8,6 * 567 dxa/cm = 4876 dxa # right-Indent = 9072 - 4876 = 4196 dxa # Border sz ist in 1/8 pt: 1,25 pt * 8 = 10 H2_SEP_XML = ( '' '' '' '' '' '' '' '' '' '' ) H2_STYLE_RE = re.compile(r'') def log(msg): print(f"[post-process-docx] {msg}", flush=True) def is_bullet_paragraph(p_xml): return " in das pPr-Element ein. Falls kein pPr existiert, wird es angelegt. Idempotent.""" 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) new_ppr = "" if ""): return p_xml.replace("", "" + new_ppr, 1) return p_xml.replace("", new_ppr + "", 1) P_RE = re.compile(r"]*>.*?", re.DOTALL) TBL_OPEN = "" TBL_CLOSE = "" def process_document_xml(xml): """Tokenisiert den Body, wendet 3-3-Regel auf Bullet-Listen an und fuegt nach jedem H2-Heading einen Trenn-Absatz ein.""" out = [] bullet_run = [] table_depth = 0 stats = {"lists": 0, "bullets_in_lists": 0, "bullets_keepnext": 0, "skipped_in_tables": 0, "h2_headings": 0, "separators_added": 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() 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): if m.start() > last_end: out.append(xml[last_end:m.start()]) last_end = m.end() if m.group("tblopen"): flush_run() table_depth += 1 out.append(m.group()) elif m.group("tblclose"): flush_run() table_depth -= 1 out.append(m.group()) else: p_xml = m.group("para") out.append(p_xml) if table_depth > 0: if is_bullet_paragraph(p_xml): stats["skipped_in_tables"] += 1 continue if is_bullet_paragraph(p_xml): bullet_run.append((len(out) - 1, p_xml)) continue flush_run() if is_h2_paragraph(p_xml): out.append(H2_SEP_XML) stats["h2_headings"] += 1 stats["separators_added"] += 1 if last_end < len(xml): out.append(xml[last_end:]) flush_run() return "".join(out), stats def main(): 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}") 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") members["word/document.xml"] = new_xml.encode("utf-8") with zipfile.ZipFile(DOCX_FILE, "w", zipfile.ZIP_DEFLATED) as z: 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(f" H2-Headings gefunden: {stats['h2_headings']}") log(f" H2-Trenn-Absaetze eingefuegt: {stats['separators_added']}") log("Fertig.") return 0 if __name__ == "__main__": sys.exit(main())