S07: Iteration B3 und B3.5 fuer Teilgebiet 01 abgeschlossen. B3 in build/build-reference-docx.py ergaenzt: DocDefault widowControl plus keepNext und keepLines auf Heading 1/2/3 und FirstParagraph (Pandoc-Stil fuer ersten Absatz nach einem Heading, deckt die fett formatierten Kenntnisse-Subsection-Labels KI Software-Design Methodik IT etc ab). Erster Versuch Compact-Stil mit keepNext hat Listen komplett unteilbar gemacht (Job-Stationen begannen jedes Mal auf einer neuen Seite, ungenutzte Seitenenden) und wurde verworfen. Auf Wunsch von Thomas auf 3-3-Regel umgestellt: bei Listen mit mindestens 6 Bullets duerfen Trennungen passieren, aber mindestens 3 Bullets bleiben jeweils zusammen vor und nach dem Umbruch. Bei kuerzeren Listen alles zusammen. Da das stilbasiert nicht abbildbar ist (alle Bullets haben pStyle Compact), neues Post-Processing-Skript build/post-process-docx.py: scannt das fertige DOCX, findet Sequenzen aufeinanderfolgender Bullets mit numPr-Eigenschaft ausserhalb von Tabellen-Zellen, setzt keepNext auf den ersten 2 und den N-3 N-2 Bullets jeder Liste mit n groesser gleich 6 (bei n kleiner 6 alle keepNext). build.ps1 erweitert auf 3 Schritte und ruft das Post-Processing-Skript automatisch nach erfolgreichem DOCX-Build auf, mit Console-Output und Log-Statistiken (Anzahl Listen Bullets keepNext-Markierungen). Sandbox-Verifikation 26 Listen 184 Bullets 93 keepNext, Pattern fuer 11-Bullet-Liste KK......KK.. Auf Thomas System visuell bestaetigt: Listen werden an guten Stellen getrennt, keine ungenutzten Seitenenden, keine einzelnen Bullets allein am Seitenrand. teilgebiete/01-lebenslauf.md um B3- und B3.5-Bloecke ergaenzt sowie Naechste-Schritte-Liste auf B4 C D umstrukturiert. agent-prompt.md Aktueller-Stand-Abschnitt fortgeschrieben mit B3 und B3.5, Hinweis auf 3-stufige DOCX-Pipeline und Edit-Tool-Truncation an build.ps1 ergaenzt. Naechste Session startet mit B4 (Heading-Farben oder Trennlinien analog PDF).
This commit is contained in:
185
artefakte/01-lebenslauf/build/post-process-docx.py
Normal file
185
artefakte/01-lebenslauf/build/post-process-docx.py
Normal file
@@ -0,0 +1,185 @@
|
||||
#!/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
|
||||
<w:numPr>-Eigenschaft im Body (nicht innerhalb von Tabellen-Zellen).
|
||||
- Bei einer Liste mit weniger als 6 Bullets: alle Bullets bekommen
|
||||
<w:keepNext/> (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 <w:keepNext/>.
|
||||
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 "<w:numPr" in p_xml
|
||||
|
||||
def has_keep_next(p_xml: str) -> bool:
|
||||
return "<w:keepNext" in p_xml
|
||||
|
||||
def add_keep_next(p_xml: str) -> str:
|
||||
"""Fuegt <w:keepNext/> 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 "<w:pPr>" in p_xml:
|
||||
return p_xml.replace("<w:pPr>", "<w:pPr><w:keepNext/>", 1)
|
||||
if "<w:pPr/>" in p_xml:
|
||||
return p_xml.replace("<w:pPr/>", "<w:pPr><w:keepNext/></w:pPr>", 1)
|
||||
# kein pPr: vor <w:r ...> oder vor </w:p>
|
||||
new_ppr = "<w:pPr><w:keepNext/></w:pPr>"
|
||||
if "<w:r" in p_xml:
|
||||
return p_xml.replace("<w:p>", "<w:p>" + new_ppr, 1) \
|
||||
if p_xml.startswith("<w:p>") else p_xml
|
||||
return p_xml.replace("</w:p>", new_ppr + "</w:p>", 1)
|
||||
|
||||
# Regex: ein <w:p ...>...</w:p>, optional gefolgt vom oeffnenden Marker fuer
|
||||
# Tabelle (<w:tbl>) oder schliessenden Body (</w:body>). Wir splitten nicht,
|
||||
# sondern iterieren paragraphenweise und tracken Tabellen-Schachtelung.
|
||||
|
||||
P_RE = re.compile(r"<w:p\b[^>]*>.*?</w:p>", re.DOTALL)
|
||||
TBL_OPEN = "<w:tbl>"
|
||||
TBL_CLOSE = "</w:tbl>"
|
||||
|
||||
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: <w:tbl>...</w:tbl>-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 <w:p ...>...</w:p>, <w:tbl>, </w:tbl>
|
||||
# und sammeln Bullet-Sequenzen ausserhalb von Tabellen.
|
||||
# Dafuer iterieren wir mit einem regex der ALLE drei Token findet.
|
||||
token_re = re.compile(
|
||||
r"(?P<tblopen>" + re.escape(TBL_OPEN) + r")"
|
||||
r"|(?P<tblclose>" + re.escape(TBL_CLOSE) + r")"
|
||||
r"|(?P<para><w:p\b[^>]*>.*?</w: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())
|
||||
Reference in New Issue
Block a user