""" Canonical renderer — takes a `Resume` model and produces a single canonical visual format DOCX (and via pdf_writer, the matching PDF). The visual is locked. Every tailored resume looks identical: - Header: name (20pt bold, centered) + contact (10pt, centered) - Section headers: 11pt bold indigo, ALL CAPS, thin underline rule - Role: title (11pt bold) + company·location (10pt italic gray) + dates (10pt italic gray) - Bullets: 10.5pt, hanging indent, single line spacing - Achievements: 3-5 quantified bullets - Education: degree (bold) + institution·dates (italic gray) NO Skills/Competencies section anywhere. NO sub-sections. """ from __future__ import annotations import os import re from docx import Document from docx.shared import Pt, RGBColor, Inches from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_LINE_SPACING from docx.oxml.ns import qn from docx.oxml import OxmlElement from .resume_model import Resume, Role, Education # Colors used throughout — single source of truth _NAME_COLOR = RGBColor(0x1A, 0x1A, 0x2E) # near-black navy _BODY_COLOR = RGBColor(0x1F, 0x29, 0x37) # body text _META_COLOR = RGBColor(0x55, 0x5B, 0x68) # italic gray _SECTION_COLOR = RGBColor(0x16, 0x48, 0x9E) # indigo blue _HR_LINE_COLOR = "16489E" def render_resume_docx(resume: Resume, filepath: str) -> str: """ Render the Resume model to a DOCX at filepath. Returns the filepath. Visual spec is fixed — every resume looks identical. """ doc = Document() _set_page_margins(doc) _set_default_paragraph_style(doc) _write_header(doc, resume) _write_summary(doc, resume) _write_experience(doc, resume) _write_achievements(doc, resume) _write_education(doc, resume) os.makedirs(os.path.dirname(filepath), exist_ok=True) doc.save(filepath) return filepath # ───────────────────────────────────────────────────────────────────────── # Document setup # ───────────────────────────────────────────────────────────────────────── def _set_page_margins(doc: Document) -> None: for section in doc.sections: section.top_margin = Inches(0.6) section.bottom_margin = Inches(0.6) section.left_margin = Inches(0.7) section.right_margin = Inches(0.7) def _set_default_paragraph_style(doc: Document) -> None: """Set the base style: Calibri 10.5pt, single-line spacing.""" style = doc.styles["Normal"] style.font.name = "Calibri" style.font.size = Pt(10.5) style.paragraph_format.space_after = Pt(0) style.paragraph_format.line_spacing = 1.15 # ───────────────────────────────────────────────────────────────────────── # Header (name + contact) # ───────────────────────────────────────────────────────────────────────── def _write_header(doc: Document, resume: Resume) -> None: name_para = doc.add_paragraph() name_para.alignment = WD_ALIGN_PARAGRAPH.CENTER name_para.paragraph_format.space_after = Pt(2) run = name_para.add_run(resume.name.upper()) run.bold = True run.font.name = "Calibri" run.font.size = Pt(20) run.font.color.rgb = _NAME_COLOR contact_line = resume.contact.render_line() if contact_line: cp = doc.add_paragraph(contact_line) cp.alignment = WD_ALIGN_PARAGRAPH.CENTER cp.paragraph_format.space_after = Pt(6) for r in cp.runs: r.font.name = "Calibri" r.font.size = Pt(10) r.font.color.rgb = _META_COLOR # Thin horizontal rule under the header rule = doc.add_paragraph() rule.paragraph_format.space_after = Pt(6) _add_bottom_border(rule, size="6", color=_HR_LINE_COLOR) # ───────────────────────────────────────────────────────────────────────── # Sections # ───────────────────────────────────────────────────────────────────────── def _write_summary(doc: Document, resume: Resume) -> None: if not resume.summary: return _add_section_header(doc, "PROFESSIONAL SUMMARY") p = doc.add_paragraph(resume.summary) p.paragraph_format.space_after = Pt(6) p.paragraph_format.line_spacing = 1.25 for r in p.runs: r.font.name = "Calibri" r.font.size = Pt(10.5) r.font.color.rgb = _BODY_COLOR def _write_experience(doc: Document, resume: Resume) -> None: if not resume.roles: return _add_section_header(doc, "PROFESSIONAL EXPERIENCE") for role in resume.roles: _write_role(doc, role) def _write_role(doc: Document, role: Role) -> None: # Role title — bold, 11pt title_para = doc.add_paragraph() title_para.paragraph_format.space_before = Pt(8) title_para.paragraph_format.space_after = Pt(0) run = title_para.add_run(role.title) run.bold = True run.font.name = "Calibri" run.font.size = Pt(11) run.font.color.rgb = _NAME_COLOR # Company · Location · Dates — italic gray, 10pt meta_parts = [] if role.company: meta_parts.append(role.company) if role.location: meta_parts.append(role.location) if role.dates: meta_parts.append(role.dates) meta_text = " · ".join(meta_parts) if meta_text: meta_para = doc.add_paragraph() meta_para.paragraph_format.space_after = Pt(3) run = meta_para.add_run(meta_text) run.italic = True run.font.name = "Calibri" run.font.size = Pt(10) run.font.color.rgb = _META_COLOR # Bullets for bullet in role.bullets: p = doc.add_paragraph(style="List Bullet") p.paragraph_format.space_after = Pt(2) p.paragraph_format.left_indent = Inches(0.2) run = p.add_run(_clean_bullet_text(bullet)) run.font.name = "Calibri" run.font.size = Pt(10.5) run.font.color.rgb = _BODY_COLOR def _write_achievements(doc: Document, resume: Resume) -> None: if not resume.achievements: return _add_section_header(doc, "KEY ACHIEVEMENTS") for ach in resume.achievements: p = doc.add_paragraph(style="List Bullet") p.paragraph_format.space_after = Pt(2) p.paragraph_format.left_indent = Inches(0.2) run = p.add_run(_clean_bullet_text(ach)) run.font.name = "Calibri" run.font.size = Pt(10.5) run.font.color.rgb = _BODY_COLOR def _write_education(doc: Document, resume: Resume) -> None: if not resume.education: return _add_section_header(doc, "EDUCATION") for edu in resume.education: # Degree — bold 10.5pt deg_para = doc.add_paragraph() deg_para.paragraph_format.space_before = Pt(4) deg_para.paragraph_format.space_after = Pt(0) run = deg_para.add_run(edu.degree) run.bold = True run.font.name = "Calibri" run.font.size = Pt(10.5) run.font.color.rgb = _NAME_COLOR # Institution · Dates — italic gray meta_parts = [] if edu.institution: meta_parts.append(edu.institution) if edu.dates: meta_parts.append(edu.dates) meta_text = " · ".join(meta_parts) if meta_text: ip = doc.add_paragraph() ip.paragraph_format.space_after = Pt(2) run = ip.add_run(meta_text) run.italic = True run.font.name = "Calibri" run.font.size = Pt(10) run.font.color.rgb = _META_COLOR # ───────────────────────────────────────────────────────────────────────── # Section header helper # ───────────────────────────────────────────────────────────────────────── def _add_section_header(doc: Document, title: str) -> None: """ALL-CAPS section header with thin indigo underline.""" p = doc.add_paragraph() p.paragraph_format.space_before = Pt(10) p.paragraph_format.space_after = Pt(4) run = p.add_run(title.upper()) run.bold = True run.font.name = "Calibri" run.font.size = Pt(11) run.font.color.rgb = _SECTION_COLOR _add_bottom_border(p) def _add_bottom_border(paragraph, size: str = "6", color: str = _HR_LINE_COLOR) -> None: pPr = paragraph._p.get_or_add_pPr() pBdr = OxmlElement("w:pBdr") bottom = OxmlElement("w:bottom") bottom.set(qn("w:val"), "single") bottom.set(qn("w:sz"), size) bottom.set(qn("w:space"), "1") bottom.set(qn("w:color"), color) pBdr.append(bottom) pPr.append(pBdr) # ───────────────────────────────────────────────────────────────────────── # Helpers # ───────────────────────────────────────────────────────────────────────── def _clean_bullet_text(text: str) -> str: """Strip leading bullet chars and collapse whitespace.""" s = re.sub(r"^[•\-–—*▪●\s]+", "", text) return re.sub(r"\s+", " ", s).strip()