diff --git a/.checkpoint-pending.txt b/.checkpoint-pending.txt
index 306fc51..e6ee86d 100644
--- a/.checkpoint-pending.txt
+++ b/.checkpoint-pending.txt
@@ -1,2 +1,2 @@
-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).
+S08
+Teilgebiet 01 Iteration B4 fuer DOCX umgesetzt. Heading 1/2/3 in destengsblue (build/build-reference-docx.py Funktion set_heading_colors mit explizitem color val=0B5394, themeColor accent1 entfernt). Heading-Bottom-Borders direkt am Stil verworfen, weil Word die Border bei hanging-Indent linksbuendig statt zentriert rendert und der right-Indent sowohl Text als auch Border begrenzt. 21 Markdown-HRs aus cv.md entfernt - Quelle der wahrgenommenen Doppellinien war Pandocs DOCX-Konvertierung von --- Zeilen zu VML-rect mit o:hr=t (Embossed-Look). Tabellen-Strich-Zeilen blieben unangetastet. Zwischenfall: NTFS-Mount-Stale-Read der cv.md (20043 statt 20201 Bytes) haette fast die Live-Datei truncated, sofortige Wiederherstellung aus git show HEAD und HR-Removal erneut mit git-Version als Input. H2-Trennlinien via Post-Processing eingefuehrt (build/post-process-docx.py um Logik erweitert): nach jedem H2 wird ein leerer Trenn-Absatz mit linksbuendiger Bottom-Border eingefuegt, schwarz (000000), 8,6 cm Linienlaenge (right-Indent 4196 dxa), 1,25 pt Dicke (sz=10). Sandbox-Verifikation 7 H2 zu 7 Trenner. Visuelle Bestaetigung durch Thomas. teilgebiete/01-lebenslauf.md um Iteration-B4-Block ergaenzt (B4.1 Farben, B4.2 Heading-Border-Sackgasse, B4.3 HR-Removal inkl. Zwischenfall, B4.4 H2-Trennlinien) und Naechste-Schritte-Liste auf C/D verkuerzt.
diff --git a/artefakte/01-lebenslauf/build/__pycache__/build-reference-docx.cpython-310.pyc b/artefakte/01-lebenslauf/build/__pycache__/build-reference-docx.cpython-310.pyc
new file mode 100644
index 0000000..5a5c7c6
Binary files /dev/null and b/artefakte/01-lebenslauf/build/__pycache__/build-reference-docx.cpython-310.pyc differ
diff --git a/artefakte/01-lebenslauf/build/__pycache__/post-process-docx.cpython-310.pyc b/artefakte/01-lebenslauf/build/__pycache__/post-process-docx.cpython-310.pyc
new file mode 100644
index 0000000..26f7bf2
Binary files /dev/null and b/artefakte/01-lebenslauf/build/__pycache__/post-process-docx.cpython-310.pyc differ
diff --git a/artefakte/01-lebenslauf/build/build-reference-docx.py b/artefakte/01-lebenslauf/build/build-reference-docx.py
index df7494d..99babe2 100644
--- a/artefakte/01-lebenslauf/build/build-reference-docx.py
+++ b/artefakte/01-lebenslauf/build/build-reference-docx.py
@@ -6,7 +6,7 @@ build-reference-docx.py
Baut die templates/reference.docx fuer die Pandoc-DOCX-Pipeline aus der
Pandoc-Default-Reference, mit gezielten Anpassungen.
-Iteration B1 + B1.5 + B2 + B3 (aktuell):
+Iteration B1 + B1.5 + B2 + B3 + B4 (aktuell):
B1 - Theme-Schriften (majorFont und minorFont) beide auf Calibri.
B1 - Direkte Schriftnamen-Referenzen in styles.xml auf Calibri
(Code-Schriften wie Consolas bleiben).
@@ -18,14 +18,20 @@ Iteration B1 + B1.5 + B2 + B3 (aktuell):
analog PDF (top/bottom 2.2 cm, left/right 2.5 cm).
B3 - DocDefault widowControl. Heading 1/2/3 mit keepNext + keepLines.
Zusaetzlich 'FirstParagraph' (Pandoc-Stil fuer den ersten Absatz
- nach einem Heading) — deckt die fett formatierten Kenntnisse-
+ nach einem Heading) - deckt die fett formatierten Kenntnisse-
Subsection-Labels ab. Hinweis: Listen-Bullet-Schutz (3-3-Regel)
passiert nicht hier, sondern im Post-Processing
(build/post-process-docx.py), das auf das fertige DOCX angewendet
- wird — ein Stil kann keine Per-Bullet-Logik abbilden.
+ wird - ein Stil kann keine Per-Bullet-Logik abbilden.
+ B4 - Heading 1/2/3 in destengsblue (0B5394) gefaerbt (themeColor
+ entfernt, damit die Farbe nicht aus dem Word-Theme kommt).
+ Hinweis (S08): die zwischenzeitlich eingebauten Heading-
+ Trennlinien (Bottom-Border + Indent-Trick) wurden zurueck-
+ gerollt, weil sie in Word linksbuendig statt zentriert
+ gerendert wurden (Word-Border folgt bei hanging-Indent der
+ visuellen Absatz-Position, nicht den Indent-Werten).
Geplant in Folge-Iterationen:
- B4 - optional Heading-Farben auf DesTEngS-Blau analog PDF
C - Foto-Einbindung
D - Hyphenation-Feintuning fuer PDF
"""
@@ -70,10 +76,15 @@ SIZE_HEADING3 = 24
HEADING_SIZES = {"Heading1": SIZE_HEADING1,
"Heading2": SIZE_HEADING2,
"Heading3": SIZE_HEADING3}
-# Compact NICHT mehr in dieser Liste — Listen-Bullet-Schutz uebernimmt das
+# Compact NICHT mehr in dieser Liste - Listen-Bullet-Schutz uebernimmt das
# Post-Processing-Skript pro-Bullet.
KEEP_STYLES = ("Heading1", "Heading2", "Heading3", "FirstParagraph")
+# B4 - Heading-Farben (Trennlinien wurden in S08 zurueckgerollt, siehe
+# Modul-Docstring). Bleibt: Heading 1/2/3 in destengsblue, themeColor entfernt.
+HEADING_COLOR = "0B5394" # destengsblue (analog template.tex)
+HEADING_COLOR_STYLES = ("Heading1", "Heading2", "Heading3")
+
PAGE_W = 11906
PAGE_H = 16838
MARGIN_TOP = 1247
@@ -87,19 +98,19 @@ HEADER_RIGHT_TAB = PAGE_W - MARGIN_LEFT - MARGIN_RIGHT
HEADER_LEFT = "Dr.-Ing. Thomas Langer"
HEADER_RIGHT = "Lebenslauf"
-def log(msg: str) -> None:
+def log(msg):
print(f"[build-reference-docx] {msg}", flush=True)
XML_DECL = b'\n'
-def write_xml(tree: ET.ElementTree, dest: Path) -> None:
+def write_xml(tree, dest):
body = ET.tostring(tree.getroot(), encoding="utf-8")
dest.write_bytes(XML_DECL + body)
-def write_xml_bytes(content: bytes, dest: Path) -> None:
+def write_xml_bytes(content, dest):
dest.write_bytes(XML_DECL + content)
-def fetch_pandoc_default(dest: Path) -> None:
+def fetch_pandoc_default(dest):
log("Pandoc-Default-Reference extrahieren ...")
result = subprocess.run(
["pandoc", "--print-default-data-file", "reference.docx"],
@@ -111,11 +122,11 @@ def fetch_pandoc_default(dest: Path) -> None:
dest.write_bytes(result.stdout)
log(f" -> {dest} ({dest.stat().st_size} Bytes)")
-def unpack_docx(src: Path, dest_dir: Path) -> None:
+def unpack_docx(src, dest_dir):
with zipfile.ZipFile(src, "r") as z:
z.extractall(dest_dir)
-def repack_docx(src_dir: Path, dest: Path) -> None:
+def repack_docx(src_dir, dest):
files = []
for path in src_dir.rglob("*"):
if path.is_file():
@@ -126,12 +137,12 @@ def repack_docx(src_dir: Path, dest: Path) -> None:
for path, arcname in files:
z.write(path, arcname)
-def is_code_font(name: str) -> bool:
+def is_code_font(name):
return (name or "").strip().lower() in CODE_FONTS
# --- B1: Schriften ---------------------------------------------------------
-def set_theme_fonts_to_calibri(theme_xml: Path) -> None:
+def set_theme_fonts_to_calibri(theme_xml):
tree = ET.parse(theme_xml)
root = tree.getroot()
for kind in ("majorFont", "minorFont"):
@@ -146,7 +157,7 @@ def set_theme_fonts_to_calibri(theme_xml: Path) -> None:
log(f" Theme {kind}/latin: {old!r} -> {TARGET_FONT!r}")
write_xml(tree, theme_xml)
-def replace_direct_fonts_in_styles(styles_xml: Path) -> None:
+def replace_direct_fonts_in_styles(styles_xml):
tree = ET.parse(styles_xml)
root = tree.getroot()
changed = 0
@@ -166,14 +177,16 @@ def replace_direct_fonts_in_styles(styles_xml: Path) -> None:
f" gesetzt (Code-Fonts unangetastet: {skipped})")
write_xml(tree, styles_xml)
-def set_table_borders_none(styles_xml: Path) -> None:
+def set_table_borders_none(styles_xml):
tree = ET.parse(styles_xml)
root = tree.getroot()
style = next((s for s in root.findall(f"{W}style")
if s.get(f"{W}styleId") == "Table"), None)
if style is None:
raise RuntimeError("Style 'Table' nicht in styles.xml")
- tbl_pr = style.find(f"{W}tblPr") or ET.SubElement(style, f"{W}tblPr")
+ tbl_pr = style.find(f"{W}tblPr")
+ if tbl_pr is None:
+ tbl_pr = ET.SubElement(style, f"{W}tblPr")
existing = tbl_pr.find(f"{W}tblBorders")
if existing is not None:
tbl_pr.remove(existing)
@@ -187,19 +200,27 @@ def set_table_borders_none(styles_xml: Path) -> None:
log(" Style 'Table': tblBorders=none auf allen Sides")
write_xml(tree, styles_xml)
-def set_default_body_size(styles_xml: Path) -> None:
+def set_default_body_size(styles_xml):
tree = ET.parse(styles_xml)
root = tree.getroot()
- docDefaults = root.find(f"{W}docDefaults") or ET.SubElement(root, f"{W}docDefaults")
- rPrDefault = docDefaults.find(f"{W}rPrDefault") or ET.SubElement(docDefaults, f"{W}rPrDefault")
- rPr = rPrDefault.find(f"{W}rPr") or ET.SubElement(rPrDefault, f"{W}rPr")
+ docDefaults = root.find(f"{W}docDefaults")
+ if docDefaults is None:
+ docDefaults = ET.SubElement(root, f"{W}docDefaults")
+ rPrDefault = docDefaults.find(f"{W}rPrDefault")
+ if rPrDefault is None:
+ rPrDefault = ET.SubElement(docDefaults, f"{W}rPrDefault")
+ rPr = rPrDefault.find(f"{W}rPr")
+ if rPr is None:
+ rPr = ET.SubElement(rPrDefault, f"{W}rPr")
for tag in (f"{W}sz", f"{W}szCs"):
- elem = rPr.find(tag) or ET.SubElement(rPr, tag)
+ elem = rPr.find(tag)
+ if elem is None:
+ elem = ET.SubElement(rPr, tag)
elem.set(f"{W}val", str(SIZE_BODY))
log(f" DocDefault Body-Schriftgroesse: {SIZE_BODY/2} pt")
write_xml(tree, styles_xml)
-def set_heading_sizes(styles_xml: Path) -> None:
+def set_heading_sizes(styles_xml):
tree = ET.parse(styles_xml)
root = tree.getroot()
for style in root.findall(f"{W}style"):
@@ -207,25 +228,35 @@ def set_heading_sizes(styles_xml: Path) -> None:
if sid not in HEADING_SIZES:
continue
target = HEADING_SIZES[sid]
- rPr = style.find(f"{W}rPr") or ET.SubElement(style, f"{W}rPr")
+ rPr = style.find(f"{W}rPr")
+ if rPr is None:
+ rPr = ET.SubElement(style, f"{W}rPr")
for tag in (f"{W}sz", f"{W}szCs"):
- elem = rPr.find(tag) or ET.SubElement(rPr, tag)
+ elem = rPr.find(tag)
+ if elem is None:
+ elem = ET.SubElement(rPr, tag)
elem.set(f"{W}val", str(target))
log(f" Stil {sid!r}: Schriftgroesse {target/2} pt")
write_xml(tree, styles_xml)
-def set_widow_control_default(styles_xml: Path) -> None:
+def set_widow_control_default(styles_xml):
tree = ET.parse(styles_xml)
root = tree.getroot()
- docDefaults = root.find(f"{W}docDefaults") or ET.SubElement(root, f"{W}docDefaults")
- pPrDefault = docDefaults.find(f"{W}pPrDefault") or ET.SubElement(docDefaults, f"{W}pPrDefault")
- pPr = pPrDefault.find(f"{W}pPr") or ET.SubElement(pPrDefault, f"{W}pPr")
+ docDefaults = root.find(f"{W}docDefaults")
+ if docDefaults is None:
+ docDefaults = ET.SubElement(root, f"{W}docDefaults")
+ pPrDefault = docDefaults.find(f"{W}pPrDefault")
+ if pPrDefault is None:
+ pPrDefault = ET.SubElement(docDefaults, f"{W}pPrDefault")
+ pPr = pPrDefault.find(f"{W}pPr")
+ if pPr is None:
+ pPr = ET.SubElement(pPrDefault, f"{W}pPr")
if pPr.find(f"{W}widowControl") is None:
ET.SubElement(pPr, f"{W}widowControl")
log(" pPrDefault: widowControl aktiviert")
write_xml(tree, styles_xml)
-def set_keep_next_styles(styles_xml: Path) -> None:
+def set_keep_next_styles(styles_xml):
tree = ET.parse(styles_xml)
root = tree.getroot()
seen = set()
@@ -233,7 +264,9 @@ def set_keep_next_styles(styles_xml: Path) -> None:
sid = style.get(f"{W}styleId")
if sid not in KEEP_STYLES:
continue
- pPr = style.find(f"{W}pPr") or ET.SubElement(style, f"{W}pPr")
+ pPr = style.find(f"{W}pPr")
+ if pPr is None:
+ pPr = ET.SubElement(style, f"{W}pPr")
for tag in (f"{W}keepNext", f"{W}keepLines"):
if pPr.find(tag) is None:
ET.SubElement(pPr, tag)
@@ -244,7 +277,31 @@ def set_keep_next_styles(styles_xml: Path) -> None:
log(f" Hinweis: Stil(e) {sorted(missing)!r} nicht gefunden, uebersprungen")
write_xml(tree, styles_xml)
-def header_default_xml() -> bytes:
+# --- B4: Heading-Farben ----------------------------------------------------
+
+def set_heading_colors(styles_xml):
+ tree = ET.parse(styles_xml)
+ root = tree.getroot()
+ for style in root.findall(f"{W}style"):
+ sid = style.get(f"{W}styleId")
+ if sid not in HEADING_COLOR_STYLES:
+ continue
+ rPr = style.find(f"{W}rPr")
+ if rPr is None:
+ rPr = ET.SubElement(style, f"{W}rPr")
+ color = rPr.find(f"{W}color")
+ if color is None:
+ color = ET.SubElement(rPr, f"{W}color")
+ # Theme-Color-Attribute entfernen, damit die Farbe nicht aus dem
+ # Word-Theme abgeleitet wird (Pandoc-Default: themeColor accent1).
+ for attr in (f"{W}themeColor", f"{W}themeTint", f"{W}themeShade"):
+ if attr in color.attrib:
+ del color.attrib[attr]
+ color.set(f"{W}val", HEADING_COLOR)
+ log(f" Stil {sid!r}: color={HEADING_COLOR} (themeColor entfernt)")
+ write_xml(tree, styles_xml)
+
+def header_default_xml():
return (
b'\n'
b' \n'
@@ -259,14 +316,14 @@ def header_default_xml() -> bytes:
b'\n'
)
-def header_first_blank_xml() -> bytes:
+def header_first_blank_xml():
return (
b'\n'
b' \n'
b'\n'
)
-def footer_default_xml() -> bytes:
+def footer_default_xml():
return (
b'\n'
b' \n'
@@ -292,12 +349,12 @@ REL_FOOTER = "http://schemas.openxmlformats.org/officeDocument/2006/relationship
CT_HEADER = "application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml"
CT_FOOTER = "application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml"
-def next_free_rel_id(rels_xml: Path) -> int:
+def next_free_rel_id(rels_xml):
text = rels_xml.read_text(encoding="utf-8")
ids = [int(m.group(1)) for m in re.finditer(r'Id="rId(\d+)"', text)]
return (max(ids) + 1) if ids else 1
-def add_relationship(rels_xml: Path, rid: str, rtype: str, target: str) -> None:
+def add_relationship(rels_xml, rid, rtype, target):
text = rels_xml.read_text(encoding="utf-8")
new_rel = f''
if new_rel in text:
@@ -305,7 +362,7 @@ def add_relationship(rels_xml: Path, rid: str, rtype: str, target: str) -> None:
text = text.replace("", new_rel + "")
rels_xml.write_text(text, encoding="utf-8")
-def add_content_type_override(ct_xml: Path, part_name: str, ct: str) -> None:
+def add_content_type_override(ct_xml, part_name, ct):
text = ct_xml.read_text(encoding="utf-8")
new_override = f''
if part_name in text:
@@ -313,10 +370,7 @@ def add_content_type_override(ct_xml: Path, part_name: str, ct: str) -> None:
text = text.replace("", new_override + "")
ct_xml.write_text(text, encoding="utf-8")
-def update_sectpr_with_headers(document_xml: Path,
- header_default_rid: str,
- header_first_rid: str,
- footer_default_rid: str) -> None:
+def update_sectpr_with_headers(document_xml, header_default_rid, header_first_rid, footer_default_rid):
text = document_xml.read_text(encoding="utf-8")
new_sectpr = (
f''
@@ -341,7 +395,7 @@ def update_sectpr_with_headers(document_xml: Path,
log(f" document.xml sectPr: pgSz/pgMar (A4, 2.2/2.5cm Raender), Header"
f" default+first, Footer default+first auf gleicher rId, titlePg")
-def add_header_footer(unpacked: Path) -> None:
+def add_header_footer(unpacked):
word_dir = unpacked / "word"
rels_xml = word_dir / "_rels" / "document.xml.rels"
ct_xml = unpacked / "[Content_Types].xml"
@@ -368,7 +422,7 @@ def add_header_footer(unpacked: Path) -> None:
update_sectpr_with_headers(doc_xml, rid_h_def, rid_h_first, rid_f_def)
-def main() -> int:
+def main():
log(f"Ziel: {OUTPUT_FILE}")
TEMPLATES_DIR.mkdir(parents=True, exist_ok=True)
@@ -398,6 +452,8 @@ def main() -> int:
set_widow_control_default(styles_xml)
log("Anpassung: keepNext + keepLines auf Heading 1/2/3 + FirstParagraph (B3)")
set_keep_next_styles(styles_xml)
+ log("Anpassung: Heading 1/2/3 in destengsblue (B4)")
+ set_heading_colors(styles_xml)
log("Anpassung: Header und Footer einbauen (B2)")
add_header_footer(unpacked)
diff --git a/artefakte/01-lebenslauf/build/post-process-docx.py b/artefakte/01-lebenslauf/build/post-process-docx.py
index eb4b848..b3e6193 100644
--- a/artefakte/01-lebenslauf/build/post-process-docx.py
+++ b/artefakte/01-lebenslauf/build/post-process-docx.py
@@ -3,23 +3,33 @@
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:
+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:
-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.
+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.
-Bullets in Tabellen-Zellen werden uebersprungen — Compact wird auch fuer
-Tabellen-Zellen-Inhalte verwendet, dort wollen wir kein keepNext.
+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.
"""
@@ -37,52 +47,64 @@ 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:
+# 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: str) -> bool:
- """True wenn Absatz-XML eine numPr-Eigenschaft hat (= Listen-Bullet)."""
+def is_bullet_paragraph(p_xml):
return " bool:
+def is_h2_paragraph(p_xml):
+ return bool(H2_STYLE_RE.search(p_xml))
+
+def has_keep_next(p_xml):
return " str:
+def add_keep_next(p_xml):
"""Fuegt in das pPr-Element ein. Falls kein pPr existiert,
- wird es angelegt. Idempotent (wenn schon vorhanden, unveraendert)."""
+ 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)
- # kein pPr: vor oder vor
new_ppr = ""
- if "", "" + new_ppr, 1) \
- if p_xml.startswith("") else p_xml
+ if ""):
+ return p_xml.replace("", "" + new_ppr, 1)
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)
+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.
+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 = []
- pos = 0
+ bullet_run = []
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}
+ "skipped_in_tables": 0, "h2_headings": 0, "separators_added": 0}
def flush_run():
if not bullet_run:
@@ -102,9 +124,6 @@ def process_document_xml(xml: str) -> tuple[str, dict]:
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")"
@@ -113,49 +132,46 @@ def process_document_xml(xml: str) -> tuple[str, dict]:
)
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
+ flush_run()
table_depth += 1
out.append(m.group())
elif m.group("tblclose"):
- flush_run() # innerhalb-Tabellen-Listen wir flushen, aber haben
- # sie eh nicht angesammelt
+ flush_run()
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()
+ continue
+ flush_run()
+ if is_h2_paragraph(p_xml):
+ out.append(H2_SEP_XML)
+ stats["h2_headings"] += 1
+ stats["separators_added"] += 1
- # Rest hinten dranhaengen
if last_end < len(xml):
out.append(xml[last_end:])
- flush_run() # falls Liste am Body-Ende
+ flush_run()
return "".join(out), stats
-def main() -> int:
+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}")
- # DOCX in memory einlesen
with zipfile.ZipFile(DOCX_FILE, "r") as z:
members = {name: z.read(name) for name in z.namelist()}
@@ -163,12 +179,10 @@ def main() -> int:
new_xml, stats = process_document_xml(doc_xml)
if new_xml == doc_xml:
- log(" keine Aenderung — keine bullet-Listen gefunden oder bereits gesetzt")
+ log(" keine Aenderung")
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:
@@ -178,6 +192,8 @@ def main() -> int:
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
diff --git a/artefakte/01-lebenslauf/output/Lebenslauf_Dr-Ing_Thomas_Langer.docx b/artefakte/01-lebenslauf/output/Lebenslauf_Dr-Ing_Thomas_Langer.docx
index d4cf75f..30769ce 100644
Binary files a/artefakte/01-lebenslauf/output/Lebenslauf_Dr-Ing_Thomas_Langer.docx and b/artefakte/01-lebenslauf/output/Lebenslauf_Dr-Ing_Thomas_Langer.docx differ
diff --git a/artefakte/01-lebenslauf/output/Lebenslauf_Dr-Ing_Thomas_Langer.pdf b/artefakte/01-lebenslauf/output/Lebenslauf_Dr-Ing_Thomas_Langer.pdf
index 6cf210f..3eacf59 100644
--- a/artefakte/01-lebenslauf/output/Lebenslauf_Dr-Ing_Thomas_Langer.pdf
+++ b/artefakte/01-lebenslauf/output/Lebenslauf_Dr-Ing_Thomas_Langer.pdf
@@ -179,18 +179,20 @@ endobj
<< /S /GoTo /D [ 82 0 R /Fit ] >>
endobj
85 0 obj
-<< /Filter /FlateDecode /Length 2849 >>
+<< /Filter /FlateDecode /Length 3132 >>
stream
-x[͎#
S"Ram[-A9$~$Q(U]=ӝ]j%şu?=v]
-?i:%8w-^oߞ~591[~;kt_?(YhMLW$OI_n:,84+m.TBAb/︺PM$?)O5dͽ.wl4ĭ^Ir]5MH[M'+Iz4n6b\}>a {IVr42\))@ڴNPflȗ!̖,?"G^#)sL}Z&]?An1
lxZ*$$jSuTTIO!>>jWƛ-*ug8^;Oj7NK!ey}ejF
$