Files
dev-process/tools/build_word_template.py
T
Stefan Lohmaier cc1a2b8129 feat(i18n): tool scripts in English
- tools/build_word_template.py: default field placeholder
- tools/generate_word_vorlagen.sh: header comments
2026-05-12 06:14:24 -07:00

584 lines
19 KiB
Python

#!/usr/bin/env python3
"""
Build the neutral slohmaier Word document template.
Output: vorlagen-word/slohmaier-doc-template.docx
The template is ISO 9001 / ASPICE-friendly:
- Cover page with project, doc-ID, version, classification
- Document Control (Approvals, Revision History, Distribution)
- Inhaltsverzeichnis / Abbildungsverzeichnis / Tabellenverzeichnis (auto-fields)
- Custom styles: Heading 1-4, Body, Caption, Code, Note, Warning, Requirement
- Header/Footer with project + doc-ID + classification + page
Run: python3 tools/build_word_template.py
"""
from __future__ import annotations
import sys
from pathlib import Path
from docx import Document
from docx.enum.style import WD_STYLE_TYPE
from docx.enum.table import WD_ALIGN_VERTICAL, WD_TABLE_ALIGNMENT
from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_BREAK
from docx.oxml import OxmlElement
from docx.oxml.ns import qn
from docx.shared import Cm, Pt, RGBColor
# ---------------------------------------------------------------------------
# OXML helpers
# ---------------------------------------------------------------------------
def _qn_set(elem, attr, value):
elem.set(qn(attr), value)
def add_field(paragraph, instr_text, default_text="<Update field with F9>"):
"""Insert a Word field (e.g. TOC, PAGE) into a paragraph."""
run = paragraph.add_run()
r_elem = run._r
fld_begin = OxmlElement("w:fldChar")
_qn_set(fld_begin, "w:fldCharType", "begin")
r_elem.append(fld_begin)
instr = OxmlElement("w:instrText")
_qn_set(instr, "xml:space", "preserve")
instr.text = instr_text
r_elem.append(instr)
fld_sep = OxmlElement("w:fldChar")
_qn_set(fld_sep, "w:fldCharType", "separate")
r_elem.append(fld_sep)
default_t = OxmlElement("w:t")
default_t.text = default_text
r_elem.append(default_t)
fld_end = OxmlElement("w:fldChar")
_qn_set(fld_end, "w:fldCharType", "end")
r_elem.append(fld_end)
def set_cell_shading(cell, fill_hex):
tc_pr = cell._tc.get_or_add_tcPr()
shd = OxmlElement("w:shd")
_qn_set(shd, "w:val", "clear")
_qn_set(shd, "w:color", "auto")
_qn_set(shd, "w:fill", fill_hex)
tc_pr.append(shd)
def set_cell_borders(cell, color="808080", size=4):
tc_pr = cell._tc.get_or_add_tcPr()
tc_borders = OxmlElement("w:tcBorders")
for edge in ("top", "left", "bottom", "right"):
b = OxmlElement(f"w:{edge}")
_qn_set(b, "w:val", "single")
_qn_set(b, "w:sz", str(size))
_qn_set(b, "w:space", "0")
_qn_set(b, "w:color", color)
tc_borders.append(b)
tc_pr.append(tc_borders)
def set_paragraph_border(paragraph, color="808080", size=12, side="left"):
p_pr = paragraph._p.get_or_add_pPr()
p_bdr = OxmlElement("w:pBdr")
b = OxmlElement(f"w:{side}")
_qn_set(b, "w:val", "single")
_qn_set(b, "w:sz", str(size))
_qn_set(b, "w:space", "8")
_qn_set(b, "w:color", color)
p_bdr.append(b)
p_pr.append(p_bdr)
def set_paragraph_shading(paragraph, fill_hex):
p_pr = paragraph._p.get_or_add_pPr()
shd = OxmlElement("w:shd")
_qn_set(shd, "w:val", "clear")
_qn_set(shd, "w:color", "auto")
_qn_set(shd, "w:fill", fill_hex)
p_pr.append(shd)
# ---------------------------------------------------------------------------
# Styles
# ---------------------------------------------------------------------------
NEUTRAL_GREY = "595959"
SOFT_GREY = "808080"
LIGHT_GREY = "F2F2F2"
ACCENT_DARK = "1F3864"
NOTE_BG = "E7F0FA"
NOTE_BORDER = "2E74B5"
WARN_BG = "FFF4E5"
WARN_BORDER = "C55A11"
REQ_BG = "EAEAEA"
REQ_BORDER = "404040"
def configure_styles(doc):
styles = doc.styles
# --- Normal / Body Text ---
normal = styles["Normal"]
normal.font.name = "Calibri"
normal.font.size = Pt(11)
normal.font.color.rgb = RGBColor(0x20, 0x20, 0x20)
normal.paragraph_format.space_after = Pt(6)
normal.paragraph_format.line_spacing = 1.15
# --- Headings ---
heading_specs = [
("Heading 1", 18, True, RGBColor(0x1F, 0x38, 0x64), 18, 6),
("Heading 2", 14, True, RGBColor(0x1F, 0x38, 0x64), 12, 6),
("Heading 3", 12, True, RGBColor(0x40, 0x40, 0x40), 8, 4),
("Heading 4", 11, True, RGBColor(0x40, 0x40, 0x40), 6, 4),
]
for name, size, bold, color, before, after in heading_specs:
s = styles[name]
s.font.name = "Calibri"
s.font.size = Pt(size)
s.font.bold = bold
s.font.color.rgb = color
s.paragraph_format.space_before = Pt(before)
s.paragraph_format.space_after = Pt(after)
s.paragraph_format.keep_with_next = True
# --- Title ---
title = styles["Title"]
title.font.name = "Calibri"
title.font.size = Pt(36)
title.font.bold = True
title.font.color.rgb = RGBColor(0x1F, 0x38, 0x64)
# --- Subtitle ---
subtitle = styles["Subtitle"]
subtitle.font.name = "Calibri"
subtitle.font.size = Pt(16)
subtitle.font.color.rgb = RGBColor(0x59, 0x59, 0x59)
# --- Caption (figure/table caption) ---
caption = styles["Caption"]
caption.font.name = "Calibri"
caption.font.size = Pt(10)
caption.font.italic = True
caption.font.color.rgb = RGBColor(0x59, 0x59, 0x59)
# --- Code (custom paragraph style) ---
if "Code" not in [s.name for s in styles]:
code = styles.add_style("Code", WD_STYLE_TYPE.PARAGRAPH)
code.base_style = styles["Normal"]
code.font.name = "Consolas"
code.font.size = Pt(10)
code.paragraph_format.left_indent = Cm(0.5)
code.paragraph_format.space_before = Pt(4)
code.paragraph_format.space_after = Pt(4)
# --- Note (custom paragraph style) ---
if "Note" not in [s.name for s in styles]:
note = styles.add_style("Note", WD_STYLE_TYPE.PARAGRAPH)
note.base_style = styles["Normal"]
note.font.name = "Calibri"
note.font.size = Pt(10)
note.paragraph_format.left_indent = Cm(0.4)
note.paragraph_format.space_before = Pt(6)
note.paragraph_format.space_after = Pt(6)
# --- Warning ---
if "Warning" not in [s.name for s in styles]:
warn = styles.add_style("Warning", WD_STYLE_TYPE.PARAGRAPH)
warn.base_style = styles["Normal"]
warn.font.name = "Calibri"
warn.font.size = Pt(10)
warn.font.bold = True
warn.paragraph_format.left_indent = Cm(0.4)
# --- Requirement Box ---
if "Requirement" not in [s.name for s in styles]:
req = styles.add_style("Requirement", WD_STYLE_TYPE.PARAGRAPH)
req.base_style = styles["Normal"]
req.font.name = "Calibri"
req.font.size = Pt(11)
req.paragraph_format.left_indent = Cm(0.4)
req.paragraph_format.space_before = Pt(6)
req.paragraph_format.space_after = Pt(6)
# ---------------------------------------------------------------------------
# Page setup, header/footer
# ---------------------------------------------------------------------------
def setup_page(doc):
for section in doc.sections:
section.top_margin = Cm(2.5)
section.bottom_margin = Cm(2.5)
section.left_margin = Cm(2.5)
section.right_margin = Cm(2.5)
section.header_distance = Cm(1.25)
section.footer_distance = Cm(1.25)
def build_header_footer(doc, doc_id_placeholder="<DOC-ID>", classification="CONFIDENTIAL"):
section = doc.sections[0]
section.different_first_page_header_footer = True
# --- Default header (skipped on cover page) ---
header = section.header
header_para = header.paragraphs[0]
header_para.alignment = WD_ALIGN_PARAGRAPH.LEFT
tabs = header_para.paragraph_format.tab_stops
tabs.add_tab_stop(Cm(8), WD_ALIGN_PARAGRAPH.CENTER)
tabs.add_tab_stop(Cm(16), WD_ALIGN_PARAGRAPH.RIGHT)
r1 = header_para.add_run("<PROJECT>")
r1.font.size = Pt(9)
r1.font.color.rgb = RGBColor(0x59, 0x59, 0x59)
header_para.add_run("\t")
r2 = header_para.add_run("<DOCUMENT-TITLE>")
r2.font.size = Pt(9)
r2.font.color.rgb = RGBColor(0x59, 0x59, 0x59)
r2.bold = True
header_para.add_run("\t")
r3 = header_para.add_run(doc_id_placeholder)
r3.font.size = Pt(9)
r3.font.color.rgb = RGBColor(0x59, 0x59, 0x59)
# --- Default footer (skipped on cover page) ---
footer = section.footer
footer_para = footer.paragraphs[0]
footer_para.alignment = WD_ALIGN_PARAGRAPH.LEFT
f_tabs = footer_para.paragraph_format.tab_stops
f_tabs.add_tab_stop(Cm(8), WD_ALIGN_PARAGRAPH.CENTER)
f_tabs.add_tab_stop(Cm(16), WD_ALIGN_PARAGRAPH.RIGHT)
fr1 = footer_para.add_run("© slohmaier.com")
fr1.font.size = Pt(9)
fr1.font.color.rgb = RGBColor(0x59, 0x59, 0x59)
footer_para.add_run("\t")
fr2 = footer_para.add_run(classification)
fr2.font.size = Pt(9)
fr2.font.color.rgb = RGBColor(0x59, 0x59, 0x59)
fr2.bold = True
footer_para.add_run("\t")
fr3 = footer_para.add_run("Page ")
fr3.font.size = Pt(9)
fr3.font.color.rgb = RGBColor(0x59, 0x59, 0x59)
add_field(footer_para, "PAGE", "1")
fr4 = footer_para.add_run(" / ")
fr4.font.size = Pt(9)
fr4.font.color.rgb = RGBColor(0x59, 0x59, 0x59)
add_field(footer_para, "NUMPAGES", "1")
# --- First-page header/footer (cover): empty ---
fp_header = section.first_page_header
fp_header.paragraphs[0].text = ""
fp_footer = section.first_page_footer
fp_footer.paragraphs[0].text = ""
# ---------------------------------------------------------------------------
# Content
# ---------------------------------------------------------------------------
def add_cover_page(doc):
# Top logo placeholder
p = doc.add_paragraph()
p.alignment = WD_ALIGN_PARAGRAPH.RIGHT
r = p.add_run("[LOGO]")
r.font.size = Pt(10)
r.font.color.rgb = RGBColor(0x80, 0x80, 0x80)
# Vertical space
for _ in range(8):
doc.add_paragraph()
# Classification banner
cls_p = doc.add_paragraph()
cls_p.alignment = WD_ALIGN_PARAGRAPH.CENTER
cls_r = cls_p.add_run("CONFIDENTIAL")
cls_r.font.size = Pt(11)
cls_r.font.bold = True
cls_r.font.color.rgb = RGBColor(0xC5, 0x5A, 0x11)
# Title
title_p = doc.add_paragraph(style="Title")
title_p.alignment = WD_ALIGN_PARAGRAPH.CENTER
title_p.add_run("<Document title>")
# Subtitle
sub_p = doc.add_paragraph(style="Subtitle")
sub_p.alignment = WD_ALIGN_PARAGRAPH.CENTER
sub_p.add_run("<Subtitle or short description>")
for _ in range(4):
doc.add_paragraph()
# Metadata block
meta_tbl = doc.add_table(rows=6, cols=2)
meta_tbl.alignment = WD_TABLE_ALIGNMENT.CENTER
meta_tbl.autofit = False
meta_tbl.columns[0].width = Cm(4.5)
meta_tbl.columns[1].width = Cm(8.5)
meta_data = [
("Project", "<Project name>"),
("Document ID", "<DOC-ID>"),
("Version", "1.0"),
("Date", "YYYY-MM-DD"),
("Status", "Draft"),
("Classification", "Confidential"),
]
for i, (k, v) in enumerate(meta_data):
c1 = meta_tbl.cell(i, 0)
c1.width = Cm(4.5)
c1.text = ""
c1_p = c1.paragraphs[0]
c1_r = c1_p.add_run(k)
c1_r.bold = True
c1_r.font.size = Pt(11)
set_cell_shading(c1, LIGHT_GREY)
set_cell_borders(c1, SOFT_GREY)
c2 = meta_tbl.cell(i, 1)
c2.width = Cm(8.5)
c2.text = ""
c2_p = c2.paragraphs[0]
c2_r = c2_p.add_run(v)
c2_r.font.size = Pt(11)
set_cell_borders(c2, SOFT_GREY)
# Page break
doc.add_page_break()
def add_document_control(doc):
doc.add_heading("Document Control", level=1)
doc.add_paragraph(
"This page documents the formal control of this document per ISO 9001. "
"Changes only take effect after the release process."
)
doc.add_heading("Approvals", level=2)
appr_tbl = doc.add_table(rows=4, cols=4)
appr_tbl.style = "Light Grid Accent 1"
hdr = appr_tbl.rows[0].cells
for i, h in enumerate(["Role", "Name", "Signature / Date", "Notes"]):
hdr[i].text = ""
r = hdr[i].paragraphs[0].add_run(h)
r.bold = True
set_cell_shading(hdr[i], LIGHT_GREY)
roles = ["Author", "Reviewer", "Approver"]
for i, role in enumerate(roles, start=1):
appr_tbl.rows[i].cells[0].text = role
for j in (1, 2, 3):
appr_tbl.rows[i].cells[j].text = ""
doc.add_heading("Revision History", level=2)
rev_tbl = doc.add_table(rows=3, cols=4)
rev_tbl.style = "Light Grid Accent 1"
rev_hdr = rev_tbl.rows[0].cells
for i, h in enumerate(["Version", "Date", "Change", "Author"]):
rev_hdr[i].text = ""
r = rev_hdr[i].paragraphs[0].add_run(h)
r.bold = True
set_cell_shading(rev_hdr[i], LIGHT_GREY)
rev_data = [
("0.1", "YYYY-MM-DD", "Initial draft", "<Author>"),
("1.0", "YYYY-MM-DD", "First release", "<Author>"),
]
for i, row in enumerate(rev_data, start=1):
for j, v in enumerate(row):
rev_tbl.rows[i].cells[j].text = v
doc.add_heading("Distribution List", level=2)
dist_tbl = doc.add_table(rows=3, cols=3)
dist_tbl.style = "Light Grid Accent 1"
dist_hdr = dist_tbl.rows[0].cells
for i, h in enumerate(["Recipient", "Role", "Organisation"]):
dist_hdr[i].text = ""
r = dist_hdr[i].paragraphs[0].add_run(h)
r.bold = True
set_cell_shading(dist_hdr[i], LIGHT_GREY)
for i in range(1, 3):
for j in range(3):
dist_tbl.rows[i].cells[j].text = ""
doc.add_page_break()
def add_toc_pages(doc):
doc.add_heading("Table of Contents", level=1)
p = doc.add_paragraph()
add_field(
p,
'TOC \\o "1-3" \\h \\z \\u',
"Update TOC: F9 (right-click on TOC > Update Field)",
)
doc.add_page_break()
doc.add_heading("List of Figures", level=1)
p = doc.add_paragraph()
add_field(
p,
'TOC \\h \\z \\c "Figure"',
"Update list of figures: F9",
)
doc.add_page_break()
doc.add_heading("List of Tables", level=1)
p = doc.add_paragraph()
add_field(
p,
'TOC \\h \\z \\c "Table"',
"Update list of tables: F9",
)
doc.add_page_break()
def add_abbreviations(doc):
doc.add_heading("Abbreviations", level=1)
tbl = doc.add_table(rows=4, cols=2)
tbl.style = "Light Grid Accent 1"
hdr = tbl.rows[0].cells
for i, h in enumerate(["Abbreviation", "Meaning"]):
hdr[i].text = ""
r = hdr[i].paragraphs[0].add_run(h)
r.bold = True
set_cell_shading(hdr[i], LIGHT_GREY)
examples = [
("ASIL", "Automotive Safety Integrity Level"),
("ECU", "Electronic Control Unit"),
("MISRA", "Motor Industry Software Reliability Association"),
]
for i, (k, v) in enumerate(examples, start=1):
tbl.rows[i].cells[0].text = k
tbl.rows[i].cells[1].text = v
doc.add_page_break()
def add_main_content(doc):
# Section 1
doc.add_heading("1. Introduction", level=1)
doc.add_heading("1.1 Purpose", level=2)
doc.add_paragraph(
"<Short paragraph: why does this document exist? What question does it answer?>"
)
doc.add_heading("1.2 Scope", level=2)
doc.add_paragraph("<Which product, project, phase?>")
doc.add_heading("1.3 Definitions", level=2)
doc.add_paragraph(
"<Domain-specific terms used in this document. Generic abbreviations are listed "
"in the Abbreviations section.>"
)
doc.add_heading("1.4 References", level=2)
ref_tbl = doc.add_table(rows=3, cols=3)
ref_tbl.style = "Light Grid Accent 1"
ref_hdr = ref_tbl.rows[0].cells
for i, h in enumerate(["ID", "Title", "Version / Location"]):
ref_hdr[i].text = ""
r = ref_hdr[i].paragraphs[0].add_run(h)
r.bold = True
set_cell_shading(ref_hdr[i], LIGHT_GREY)
for i in range(1, 3):
for j in range(3):
ref_tbl.rows[i].cells[j].text = ""
# Section 2
doc.add_heading("2. Main Content", level=1)
doc.add_paragraph(
"<This is where the document-type-specific content begins. This template "
"provides structure and styles; concrete sections come from each document "
"type (PID, QA Plan, SWE Plan, ...).>"
)
# Demonstrate styles
doc.add_heading("2.1 Example: Styles", level=2)
doc.add_paragraph(
"Body text in the template. Calibri 11 with 1.15 line spacing."
)
code_p = doc.add_paragraph(style="Code")
code_p.add_run(
"// Example code in code style (Consolas 10)\n"
"Status epb_apply(uint8_t force_percent);"
)
set_paragraph_shading(code_p, LIGHT_GREY)
note_p = doc.add_paragraph(style="Note")
note_p.add_run("NOTE: ").bold = True
note_p.add_run("Note style for supplementary information.")
set_paragraph_border(note_p, NOTE_BORDER, size=18, side="left")
set_paragraph_shading(note_p, NOTE_BG)
warn_p = doc.add_paragraph(style="Warning")
warn_p.add_run("WARNING: ").bold = True
warn_p.add_run("Warning style for safety-relevant information.")
set_paragraph_border(warn_p, WARN_BORDER, size=18, side="left")
set_paragraph_shading(warn_p, WARN_BG)
req_p = doc.add_paragraph(style="Requirement")
req_p.add_run("REQ-001: ").bold = True
req_p.add_run(
"Requirement style for inline requirements "
"(usually in Markdown via Doorstop, but in Word for formal reports)."
)
set_paragraph_border(req_p, REQ_BORDER, size=18, side="left")
set_paragraph_shading(req_p, REQ_BG)
doc.add_heading("2.2 Example Table", level=2)
tbl = doc.add_table(rows=4, cols=3)
tbl.style = "Light Grid Accent 1"
hdr = tbl.rows[0].cells
for i, h in enumerate(["ID", "Description", "ASIL"]):
hdr[i].text = ""
r = hdr[i].paragraphs[0].add_run(h)
r.bold = True
set_cell_shading(hdr[i], LIGHT_GREY)
rows = [
("F-01", "Apply on driver request", "D"),
("F-05", "Release on driver request", "B"),
("F-10", "HMI: LED control", "QM"),
]
for i, row in enumerate(rows, start=1):
for j, v in enumerate(row):
tbl.rows[i].cells[j].text = v
# Caption demo
cap_p = doc.add_paragraph(style="Caption")
cap_p.add_run("Table 1: Example requirements with ASIL classification")
# Section 3 — Appendix
doc.add_heading("3. Appendix", level=1)
doc.add_paragraph(
"<Appendices: detail calculations, scripts, extra tables, glossary.>"
)
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def build_template(out_path: Path):
doc = Document()
setup_page(doc)
configure_styles(doc)
build_header_footer(doc)
add_cover_page(doc)
add_document_control(doc)
add_toc_pages(doc)
add_abbreviations(doc)
add_main_content(doc)
out_path.parent.mkdir(parents=True, exist_ok=True)
doc.save(out_path)
print(f"Wrote: {out_path}")
if __name__ == "__main__":
out = Path(sys.argv[1] if len(sys.argv) > 1 else
Path(__file__).resolve().parent.parent / "vorlagen-word" / "slohmaier-doc-template.docx")
build_template(out)