S09: Teilgebiet 01 Iteration C Foto-Einbindung umgesetzt. Header als 2-Spalten-Grid-Table in cv.md mit Strich-Verhaeltnis 112:60 (= 65,1%/34,9% Spaltenbreite, ca. 10,15/5,43 cm bei 16 cm Textbreite). Foto rechts oben, 4,06x4,06 cm, beide Dimensionen explizit im Markdown um Pandocs Default-Wrapper keepaspectratio mit height=textheight zu vermeiden, der die Layout-Box auf 24cm Hoehe streckt und die Tabellen-Zeile zerschiesst. DOCX-Header-Spacing per neuer 4. Modifikation in build/post-process-docx.py (process_header_table): findet erste Tabelle, setzt Heading1-spacing-before=0 und Foto-Paragraph spacing-before=100 (=5pt) plus jc=right. PDF-Layout via neuem Pandoc-Lua-Filter build/header-image-wrap.lua: wrappt das Header-Foto im LaTeX-Output mit hfill+raisebox(-height)[0pt][0pt]{...}, hfill schiebt rechtsbuendig in raggedright-p-Spalte, raisebox setzt Bild-Top auf Cell-Top und reportet null Hoehe an die Tabellen-Zeile. Filter prueft FORMAT=latex und Image-Pfad enthaelt foto, DOCX bleibt unberuehrt. Lua-Filter-Erste-Version (Image durch RawInline ersetzt mit gebackenem Pfad) hat Pandocs Image-Resource-Resolution gebrochen und LuaLaTeX scheiterte mit File foto.jpg not found, Fix: Filter gibt Lua-Liste zurueck mit Original-img-Element zwischen RawInline-Wrappern. Template-Hotfixes fuer PDF: renewcommand-nolinkurl-zu-Plaintext (verhindert at-xverbatim-Bruch in longtable-Minipage durch URL-Display-Text-Verbatim-Mode), titlespacing-section-before=0pt fuer H1-Top-Alignment. cv.md: Pipe-Alignment in Grid Table programmatisch via Python ljust und Pipe-Position-Eindeutigkeitscheck (Pandoc 3.x ist beim Grid-Table-Pipe-Alignment streng, Sandbox-Pandoc 2.9 ist toleranter und damit irrefuehrend). build.ps1 erweitert um lua-filter-Argument in PDF und DOCX, plus Read-Host-Wait-on-Error entfernt (blockiert AI-Agents und CI), durch Start-Sleep 3s am Ende ersetzt. header-image-wrap.lua als Pflichtdatei in Test-Path-Check aufgenommen. Vier weitere Edit-Tool-Truncation-Vorfaelle in S09 (cv.md, template.tex zweimal, build.ps1), Lehre verschaerft Edit-Tool fuer jede nicht-triviale Modifikation auf NTFS-Mount-Dateien meiden. Sandbox-NTFS-Stale-Read auf DOCX-Output (DOCX-Datei als not a zip file, Workaround DOCX im Sandbox neu generieren). Sandbox-NTFS-Mount kann auch Datei-Schreiben mit open(w) verweigern obwohl os.path.exists True liefert, Workaround tmp-Datei plus os.rename. Build und visuelle Bestaetigung durch Thomas erfolgt fuer DOCX und PDF. teilgebiete/01-lebenslauf.md um Iteration-C-Block ergaenzt und Naechste-Schritte-Liste auf S10-Plan umgestellt (1 DOCX-Heading-Farben pruefen, 2 Doublecheck der generierten Texte mindestens elektrischer-Gehaeuse ist sinnverkehrt, 3 Buzzword-Kompetenzen brainstormen mindestens Umgang mit quantisierten LLMs fehlt, 4 PDF-Spacings H1/H2/Kontaktdaten und hellgraue Trennlinien korrigieren, 5 Hyphenation-Feintuning, 6 Teilgebiet abschliessen). agent-prompt.md Aktueller-Stand-Block fuer S10 fortgeschrieben.

This commit is contained in:
tlg
2026-04-27 18:51:18 +02:00
parent 93bf43301e
commit b26cfd0ab3
13 changed files with 3438 additions and 764 deletions

View File

@@ -31,6 +31,17 @@ abbilden koennen:
(left - hanging) = Bullet-Position; "Sondereinzug Haengend" = hanging.
Daher rechnen wir: left = (gewuenschter Einzug + gewuenschter Hanging) in dxa.
4. Header-Tabelle (S10) — Foto-Position und H1-Spacing:
- Erste Tabelle des Dokuments ist unsere Header-Tabelle (Name + Kontaktdaten
in linker Zelle, Foto in rechter Zelle).
- Im Heading1-Absatz der linken Zelle wird `<w:spacing w:before="0"/>`
gesetzt, damit die H1-Oberkante an der Spalten-Oberkante steht
(Pandoc/Word-Default: 18 pt vor H1).
- Im Foto-Absatz der rechten Zelle wird `<w:spacing w:before="100"/>`
(= 5 pt) gesetzt und das `<w:jc>` von `left` auf `right` geaendert,
damit das Foto an die rechte Spaltenkante rueckt und die Oberkante
mit der H1-Oberkante in einer Linie liegt.
Voraussetzungen: nur Python-Stdlib.
"""
@@ -209,6 +220,88 @@ def process_numbering_xml(xml):
new_xml = XML_DECL + ET.tostring(root, encoding="unicode")
return new_xml, stats
def modify_ppr(p_xml, spacing_before=None, jc=None):
"""In einem <w:p>...</w:p>-Block die Eigenschaften setzen.
spacing_before: str (Wert in 1/20 pt, z.B. "0" oder "100"), oder None
jc: str ("left", "right", "center", "both"), oder None
"""
ppr_re = re.compile(r"<w:pPr>(.*?)</w:pPr>", re.DOTALL)
m = ppr_re.search(p_xml)
if m is None:
# kein pPr -> einen leeren pPr direkt nach <w:p> hinzufuegen
# und rekursiv aufrufen
new_p = p_xml.replace("<w:p>", "<w:p><w:pPr></w:pPr>", 1)
return modify_ppr(new_p, spacing_before=spacing_before, jc=jc)
inner = m.group(1)
new_inner = inner
if jc is not None:
jc_re = re.compile(r'<w:jc\s+w:val="[^"]*"\s*/>')
if jc_re.search(new_inner):
new_inner = jc_re.sub(f'<w:jc w:val="{jc}"/>', new_inner)
else:
new_inner = new_inner + f'<w:jc w:val="{jc}"/>'
if spacing_before is not None:
sp_re = re.compile(r'<w:spacing\b[^/]*/>')
sm = sp_re.search(new_inner)
if sm:
sp_xml = sm.group()
if 'w:before=' in sp_xml:
new_sp = re.sub(r'w:before="[^"]*"',
f'w:before="{spacing_before}"', sp_xml)
else:
new_sp = sp_xml.replace('<w:spacing',
f'<w:spacing w:before="{spacing_before}"', 1)
new_inner = new_inner.replace(sp_xml, new_sp, 1)
else:
new_inner = new_inner + f'<w:spacing w:before="{spacing_before}"/>'
if new_inner == inner:
return p_xml
return p_xml.replace(f"<w:pPr>{inner}</w:pPr>",
f"<w:pPr>{new_inner}</w:pPr>", 1)
HEADING1_RE = re.compile(r'<w:pStyle\s+w:val="Heading1"\s*/?>')
def process_header_table(xml):
"""Modifiziert die ERSTE <w:tbl>...</w:tbl> im Dokument.
- Heading1-Paragraph der linken Zelle: spacing-before = 0 (statt 18 pt)
- Foto-Paragraph der rechten Zelle (enthaelt <w:drawing>):
spacing-before = 100 (= 5 pt) und jc = right (statt left).
"""
stats = {"h1_modified": 0, "foto_modified": 0}
tbl_re = re.compile(r'<w:tbl>.*?</w:tbl>', re.DOTALL)
tbl_m = tbl_re.search(xml)
if tbl_m is None:
return xml, stats
tbl = tbl_m.group()
new_tbl = tbl
p_re = re.compile(r'<w:p\b[^>]*>.*?</w:p>', re.DOTALL)
for pm in list(p_re.finditer(tbl)):
p_xml = pm.group()
if HEADING1_RE.search(p_xml):
new_p = modify_ppr(p_xml, spacing_before="0")
if new_p != p_xml:
new_tbl = new_tbl.replace(p_xml, new_p, 1)
stats["h1_modified"] += 1
elif '<w:drawing>' in p_xml:
new_p = modify_ppr(p_xml, spacing_before="100", jc="right")
if new_p != p_xml:
new_tbl = new_tbl.replace(p_xml, new_p, 1)
stats["foto_modified"] += 1
if new_tbl == tbl:
return xml, stats
return xml.replace(tbl, new_tbl, 1), stats
def main():
if not DOCX_FILE.exists():
sys.stderr.write(f"FEHLER: {DOCX_FILE} existiert nicht. "
@@ -220,6 +313,7 @@ def main():
members = {name: z.read(name) for name in z.namelist()}
doc_xml = members["word/document.xml"].decode("utf-8")
doc_xml, header_stats = process_header_table(doc_xml)
new_doc_xml, doc_stats = process_document_xml(doc_xml)
members["word/document.xml"] = new_doc_xml.encode("utf-8")
@@ -245,6 +339,8 @@ def main():
log(f" H2-Trenn-Absaetze eingefuegt: {doc_stats['separators_added']}")
log(f" numbering.xml abstractNum-Eintraege: {num_stats['abstractNums']}")
log(f" numbering.xml lvls modifiziert: {num_stats['lvls_modified']}")
log(f" Header-Tabelle H1 modifiziert: {header_stats['h1_modified']}")
log(f" Header-Tabelle Foto modifiziert: {header_stats['foto_modified']}")
log("Fertig.")
return 0