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.
This commit is contained in:
@@ -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'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\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'<w:hdr xmlns:w="' + NS["w"].encode() + b'">\n'
|
||||
b' <w:p>\n'
|
||||
@@ -259,14 +316,14 @@ def header_default_xml() -> bytes:
|
||||
b'</w:hdr>\n'
|
||||
)
|
||||
|
||||
def header_first_blank_xml() -> bytes:
|
||||
def header_first_blank_xml():
|
||||
return (
|
||||
b'<w:hdr xmlns:w="' + NS["w"].encode() + b'">\n'
|
||||
b' <w:p/>\n'
|
||||
b'</w:hdr>\n'
|
||||
)
|
||||
|
||||
def footer_default_xml() -> bytes:
|
||||
def footer_default_xml():
|
||||
return (
|
||||
b'<w:ftr xmlns:w="' + NS["w"].encode() + b'">\n'
|
||||
b' <w:p>\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'<Relationship Type="{rtype}" Id="{rid}" Target="{target}" />'
|
||||
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("</Relationships>", new_rel + "</Relationships>")
|
||||
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'<Override PartName="{part_name}" ContentType="{ct}"/>'
|
||||
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("</Types>", new_override + "</Types>")
|
||||
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'<w:sectPr>'
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user