S08: Teilgebiet 01 Iterationen B5 (Trainings als Tabelle) und B6 (Bullet-Einzuege verkleinert) abgeschlossen. B5: Trainings-Bullet-Liste in cv.md durch Pandoc-Multiline-Tabelle ersetzt analog Ausbildung. B6: build/post-process-docx.py um dritte Modifikation erweitert die direkt die numbering.xml manipuliert weil Pandoc die Werte aus reference.docx ignoriert. Bullet-Einzuege auf E1 0,25/0,35 cm und E2 0,80/0,40 cm gesetzt. Word-Konvention dokumentiert: Einzug-links zeigt (left - hanging). teilgebiete/01-lebenslauf.md und agent-prompt.md fuer S09 fortgeschrieben.
This commit is contained in:
Binary file not shown.
@@ -4,32 +4,32 @@ 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
|
||||
drei 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
|
||||
<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).
|
||||
<w:keepNext/>.
|
||||
- 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.
|
||||
|
||||
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.
|
||||
1,25 pt (sz=10), 8,6 cm Linienlaenge.
|
||||
|
||||
3. Bullet-Einzuege (S08):
|
||||
- Pandoc erzeugt fuer alle Bullet-Listen abstractNum-Eintraege mit
|
||||
festen Defaults (E1 left/hanging=480 dxa, E2 left=1200/hanging=480 dxa).
|
||||
Pandoc IGNORIERT die numbering.xml-Werte der reference.docx.
|
||||
- Im Post-Processing wird numbering.xml so modifiziert, dass alle
|
||||
abstractNum-Eintraege die kompakteren Wunschwerte bekommen.
|
||||
- Word-Konvention: "Einzug links" (im Absatz-Dialog) zeigt
|
||||
(left - hanging) = Bullet-Position; "Sondereinzug Haengend" = hanging.
|
||||
Daher rechnen wir: left = (gewuenschter Einzug + gewuenschter Hanging) in dxa.
|
||||
|
||||
Voraussetzungen: nur Python-Stdlib.
|
||||
"""
|
||||
@@ -47,11 +47,6 @@ 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 = (
|
||||
'<w:p>'
|
||||
'<w:pPr>'
|
||||
@@ -67,6 +62,23 @@ H2_SEP_XML = (
|
||||
|
||||
H2_STYLE_RE = re.compile(r'<w:pStyle\s+w:val="Heading2"\s*/?>')
|
||||
|
||||
# Bullet-Einzuege (1 cm = 567 dxa)
|
||||
# Word zeigt "Einzug links" = (left - hanging), "Sondereinzug Haengend" = hanging.
|
||||
# E1: Einzug 0,25 cm + Sondereinzug 0,35 cm -> hanging=198, left=142+198=340
|
||||
# E2: Einzug 0,80 cm + Sondereinzug 0,40 cm -> hanging=227, left=454+227=681
|
||||
# E3+: proportional zur E2 (jeweils +0,55 cm fuer Einzug), hanging analog E2
|
||||
BULLET_INDENTS = {
|
||||
0: {"left": 340, "hanging": 198},
|
||||
1: {"left": 681, "hanging": 227},
|
||||
2: {"left": 993, "hanging": 227},
|
||||
3: {"left": 1305, "hanging": 227},
|
||||
4: {"left": 1617, "hanging": 227},
|
||||
5: {"left": 1929, "hanging": 227},
|
||||
6: {"left": 2241, "hanging": 227},
|
||||
7: {"left": 2553, "hanging": 227},
|
||||
8: {"left": 2865, "hanging": 227},
|
||||
}
|
||||
|
||||
def log(msg):
|
||||
print(f"[post-process-docx] {msg}", flush=True)
|
||||
|
||||
@@ -80,8 +92,6 @@ def has_keep_next(p_xml):
|
||||
return "<w:keepNext" in p_xml
|
||||
|
||||
def add_keep_next(p_xml):
|
||||
"""Fuegt <w:keepNext/> in das pPr-Element ein. Falls kein pPr existiert,
|
||||
wird es angelegt. Idempotent."""
|
||||
if has_keep_next(p_xml):
|
||||
return p_xml
|
||||
if "<w:pPr>" in p_xml:
|
||||
@@ -98,8 +108,6 @@ TBL_OPEN = "<w:tbl>"
|
||||
TBL_CLOSE = "</w:tbl>"
|
||||
|
||||
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
|
||||
@@ -165,6 +173,42 @@ def process_document_xml(xml):
|
||||
flush_run()
|
||||
return "".join(out), stats
|
||||
|
||||
def process_numbering_xml(xml):
|
||||
"""In allen abstractNum-Eintraegen die Bullet-Einzuege ersetzen."""
|
||||
import xml.etree.ElementTree as ET
|
||||
W = "{%s}" % W_NS
|
||||
ET.register_namespace("w", W_NS)
|
||||
root = ET.fromstring(xml)
|
||||
|
||||
stats = {"abstractNums": 0, "lvls_modified": 0}
|
||||
|
||||
for absnum in root.findall(W+"abstractNum"):
|
||||
stats["abstractNums"] += 1
|
||||
for lvl in absnum.findall(W+"lvl"):
|
||||
ilvl_str = lvl.get(W+"ilvl")
|
||||
try:
|
||||
ilvl = int(ilvl_str)
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
target = BULLET_INDENTS.get(ilvl)
|
||||
if target is None:
|
||||
continue
|
||||
pPr = lvl.find(W+"pPr")
|
||||
if pPr is None:
|
||||
pPr = ET.SubElement(lvl, W+"pPr")
|
||||
ind = pPr.find(W+"ind")
|
||||
if ind is None:
|
||||
ind = ET.SubElement(pPr, W+"ind")
|
||||
ind.set(W+"left", str(target["left"]))
|
||||
ind.set(W+"hanging", str(target["hanging"]))
|
||||
if W+"firstLine" in ind.attrib:
|
||||
del ind.attrib[W+"firstLine"]
|
||||
stats["lvls_modified"] += 1
|
||||
|
||||
XML_DECL = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n'
|
||||
new_xml = XML_DECL + ET.tostring(root, encoding="unicode")
|
||||
return new_xml, stats
|
||||
|
||||
def main():
|
||||
if not DOCX_FILE.exists():
|
||||
sys.stderr.write(f"FEHLER: {DOCX_FILE} existiert nicht. "
|
||||
@@ -176,11 +220,16 @@ def main():
|
||||
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)
|
||||
new_doc_xml, doc_stats = process_document_xml(doc_xml)
|
||||
members["word/document.xml"] = new_doc_xml.encode("utf-8")
|
||||
|
||||
if new_xml == doc_xml:
|
||||
log(" keine Aenderung")
|
||||
members["word/document.xml"] = new_xml.encode("utf-8")
|
||||
num_stats = {"abstractNums": 0, "lvls_modified": 0}
|
||||
if "word/numbering.xml" in members:
|
||||
num_xml = members["word/numbering.xml"].decode("utf-8")
|
||||
new_num_xml, num_stats = process_numbering_xml(num_xml)
|
||||
members["word/numbering.xml"] = new_num_xml.encode("utf-8")
|
||||
else:
|
||||
log(" Hinweis: word/numbering.xml nicht im DOCX (keine Listen?)")
|
||||
|
||||
with zipfile.ZipFile(DOCX_FILE, "w", zipfile.ZIP_DEFLATED) as z:
|
||||
order = sorted(members.keys(),
|
||||
@@ -188,12 +237,14 @@ def main():
|
||||
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(f" Listen gefunden: {doc_stats['lists']}")
|
||||
log(f" Bullets in Listen: {doc_stats['bullets_in_lists']}")
|
||||
log(f" keepNext gesetzt: {doc_stats['bullets_keepnext']}")
|
||||
log(f" Bullets in Tabellen uebersprungen: {doc_stats['skipped_in_tables']}")
|
||||
log(f" H2-Headings gefunden: {doc_stats['h2_headings']}")
|
||||
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("Fertig.")
|
||||
return 0
|
||||
|
||||
|
||||
Binary file not shown.
@@ -169,16 +169,16 @@ endobj
|
||||
|
||||
endobj
|
||||
78 0 obj
|
||||
<< /S /GoTo /D (none.1) >>
|
||||
<< /S /GoTo /D (none.2) >>
|
||||
endobj
|
||||
80 0 obj
|
||||
81 0 obj
|
||||
(\376\377\000K\000e\000n\000n\000t\000n\000i\000s\000s\000e)
|
||||
|
||||
endobj
|
||||
81 0 obj
|
||||
<< /S /GoTo /D [ 82 0 R /Fit ] >>
|
||||
82 0 obj
|
||||
<< /S /GoTo /D [ 83 0 R /Fit ] >>
|
||||
endobj
|
||||
85 0 obj
|
||||
86 0 obj
|
||||
<< /Filter /FlateDecode /Length 3132 >>
|
||||
stream
|
||||
x<EFBFBD><EFBFBD>[͎#<23>
|
||||
@@ -195,31 +195,31 @@ r
|
||||
P<EFBFBD>2<EFBFBD>Eu<07><EFBFBD>=5٩yv<79><76><EFBFBD><EFBFBD>7U<37><55>ucȓ<63>}<7D>v<EFBFBD>ҙf<D299>E<EFBFBD>f<EFBFBD><66>8<EFBFBD>3<EFBFBD><33>$<24><>2<EFBFBD><32>Z<EFBFBD><5A><EFBFBD>Y<EFBFBD><59><EFBFBD><EFBFBD>4b<34>)<29><><EFBFBD>~<7E>*@<40>I:<0E>C<EFBFBD>i<EFBFBD>T<EFBFBD><54>]H<>f<EFBFBD>q<><71><EFBFBD><EFBFBD>W<EFBFBD><57>zE 8ab4<62>7<EFBFBD><37><EFBFBD>k<EFBFBD><6B><EFBFBD>_<EFBFBD><0C>B<EFBFBD>%L^m<><6D>nJU(<28><>:2<><32>UI<55>d<EFBFBD>)Z
|
||||
0w<>D,<2C><>2<EFBFBD><32>i<EFBFBD>l[<5B><>#<23><>4㖊<34>sx<73>K.<2E>q# | ||||