#!/usr/bin/env python3 """ Build the neutral slohmaier Word document template. Output: templates-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=""): """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="", 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("") r1.font.size = Pt(9) r1.font.color.rgb = RGBColor(0x59, 0x59, 0x59) header_para.add_run("\t") r2 = header_para.add_run("") 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("") # Subtitle sub_p = doc.add_paragraph(style="Subtitle") sub_p.alignment = WD_ALIGN_PARAGRAPH.CENTER sub_p.add_run("") 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", ""), ("Document 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", ""), ("1.0", "YYYY-MM-DD", "First release", ""), ] 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( "" ) doc.add_heading("1.2 Scope", level=2) doc.add_paragraph("") doc.add_heading("1.3 Definitions", level=2) doc.add_paragraph( "" ) 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( "" ) # 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( "" ) # --------------------------------------------------------------------------- # 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 / "templates-word" / "slohmaier-doc-template.docx") build_template(out)