from pathlib import Path import html import re import sys from docx import Document from docx.enum.section import WD_SECTION from docx.enum.text import WD_ALIGN_PARAGRAPH from docx.oxml import OxmlElement from docx.oxml.ns import qn from docx.shared import Inches, Pt, RGBColor from lxml import html as lxml_html CONTACTS = { "Location": "Oak Grove, KY", "Phone (KY)": "(270) 889-8072", "Phone (TN)": "(865) 985-1641", "Email": "jasoneugenegarrison@icloud.com", "Website": "www.jasoneugenegarrison.com", } def clean(text): return re.sub(r"\s+", " ", html.unescape(text or "")).strip() def by_class(node, class_name, tag="*"): xpath = f".//{tag}[contains(concat(' ', normalize-space(@class), ' '), ' {class_name} ')]" return node.xpath(xpath) def add_bottom_border(paragraph, color="b91c1c", size="12"): p_pr = paragraph._p.get_or_add_pPr() p_bdr = OxmlElement("w:pBdr") bottom = OxmlElement("w:bottom") bottom.set(qn("w:val"), "single") bottom.set(qn("w:sz"), size) bottom.set(qn("w:space"), "4") bottom.set(qn("w:color"), color) p_bdr.append(bottom) p_pr.append(p_bdr) def set_run(run, size=None, bold=None, color=None, all_caps=False): if size: run.font.size = Pt(size) if bold is not None: run.bold = bold if color: run.font.color.rgb = RGBColor.from_string(color) if all_caps: run.text = run.text.upper() def add_heading(doc, text): p = doc.add_paragraph() p.paragraph_format.space_before = Pt(12) p.paragraph_format.space_after = Pt(6) run = p.add_run(text.upper()) run.bold = True run.font.name = "Arial" run.font.size = Pt(11) run.font.color.rgb = RGBColor(185, 28, 28) add_bottom_border(p, "e5e7eb", "6") return p def add_bullet(doc, text): p = doc.add_paragraph(style="List Bullet") p.paragraph_format.left_indent = Inches(0.22) p.paragraph_format.first_line_indent = Inches(-0.12) p.paragraph_format.space_after = Pt(3) run = p.add_run(text) run.font.name = "Arial" run.font.size = Pt(9.5) return p def build_docx(site_root, output_path): index_path = site_root / "index.html" tree = lxml_html.fromstring(index_path.read_text(encoding="utf-8")) name = clean(tree.xpath("string(//h1)")) role = clean(tree.xpath("string(//*[contains(@class, 'role')])")) availability = clean(tree.xpath("string(//*[contains(@class, 'availability')])")) strengths = [clean(el.text_content()) for el in tree.xpath(".//*[contains(concat(' ', normalize-space(@class), ' '), ' strength-cloud ')]//span")] sections = {} for section in tree.xpath(".//section[contains(concat(' ', normalize-space(@class), ' '), ' section ')]"): title = clean(section.xpath("string(.//*[contains(@class, 'section-title')])")) sections[title] = section doc = Document() section = doc.sections[0] section.top_margin = Inches(0.55) section.bottom_margin = Inches(0.55) section.left_margin = Inches(0.7) section.right_margin = Inches(0.7) styles = doc.styles styles["Normal"].font.name = "Arial" styles["Normal"].font.size = Pt(9.5) title = doc.add_paragraph() title.alignment = WD_ALIGN_PARAGRAPH.CENTER title.paragraph_format.space_after = Pt(2) r = title.add_run(name.upper()) set_run(r, size=18, bold=True, color="111827") subtitle = doc.add_paragraph() subtitle.alignment = WD_ALIGN_PARAGRAPH.CENTER subtitle.paragraph_format.space_after = Pt(4) r = subtitle.add_run(role) set_run(r, size=10.5, bold=True, color="b91c1c") avail = doc.add_paragraph() avail.alignment = WD_ALIGN_PARAGRAPH.CENTER avail.paragraph_format.space_after = Pt(6) r = avail.add_run(availability) set_run(r, size=8.8, color="4b5563") contact_line = " | ".join(f"{label}: {value}" for label, value in CONTACTS.items()) contact = doc.add_paragraph() contact.alignment = WD_ALIGN_PARAGRAPH.CENTER contact.paragraph_format.space_after = Pt(8) r = contact.add_run(contact_line) set_run(r, size=7.8, color="374151") add_bottom_border(contact, "b91c1c", "10") profile_section = sections.get("Professional Profile") if profile_section is not None: add_heading(doc, "Professional Profile") p = doc.add_paragraph() p.paragraph_format.space_after = Pt(5) r = p.add_run(clean(profile_section.xpath("string(.//*[contains(@class, 'profile-text')])"))) set_run(r, size=9.5, color="111827") if strengths: p = doc.add_paragraph() p.paragraph_format.space_after = Pt(2) r = p.add_run("Core strengths: ") set_run(r, size=9, bold=True, color="111827") r = p.add_run(", ".join(strengths)) set_run(r, size=9, color="374151") tools_section = sections.get("Core Tools & Work Types") if tools_section is not None: add_heading(doc, "Core Tools & Work Types") p = doc.add_paragraph() p.paragraph_format.space_after = Pt(3) r = p.add_run(clean(tools_section.xpath("string(.//*[contains(@class, 'profile-text')])"))) set_run(r, size=9.2, color="111827") experience_section = sections.get("Professional Experience") if experience_section is not None: add_heading(doc, "Professional Experience") for job in by_class(experience_section, "job-item"): head = doc.add_paragraph() head.paragraph_format.space_before = Pt(4) head.paragraph_format.space_after = Pt(1) title_text = clean(job.xpath("string(.//*[contains(@class, 'job-title')])")) date_text = clean(job.xpath("string(.//*[contains(@class, 'job-meta')])")) r = head.add_run(title_text) set_run(r, size=10.2, bold=True, color="111827") r = head.add_run(f" | {date_text}") set_run(r, size=9, bold=True, color="b91c1c") sub = doc.add_paragraph() sub.paragraph_format.space_after = Pt(2) r = sub.add_run(clean(job.xpath("string(.//*[contains(@class, 'job-sub')])"))) set_run(r, size=8.8, bold=True, color="6b7280") for li in by_class(job, "job-desc"): for item in li.xpath(".//li"): add_bullet(doc, clean(item.text_content())) education_section = sections.get("Education & Background") if education_section is not None: add_heading(doc, "Education & Background") p = doc.add_paragraph() p.paragraph_format.space_after = Pt(1) r = p.add_run(clean(education_section.xpath("string(.//*[contains(@class, 'edu-title')])"))) set_run(r, size=9.8, bold=True, color="111827") p = doc.add_paragraph() p.paragraph_format.space_after = Pt(4) r = p.add_run(clean(education_section.xpath("string(.//*[contains(@class, 'edu-sub')])"))) set_run(r, size=8.8, color="6b7280") for paragraph in by_class(education_section, "edu-text"): p = doc.add_paragraph() p.paragraph_format.space_after = Pt(3) r = p.add_run(clean(paragraph.text_content())) set_run(r, size=9.2, color="111827") target_section = sections.get("Career Target") if target_section is not None: add_heading(doc, "Career Target") p = doc.add_paragraph() p.paragraph_format.space_after = Pt(0) r = p.add_run(clean(target_section.xpath("string(.//*[contains(@class, 'profile-text')])"))) set_run(r, size=9.5, color="111827") footer = section.footer.paragraphs[0] footer.alignment = WD_ALIGN_PARAGRAPH.CENTER r = footer.add_run("Jason Eugene Garrison | Resume | Last updated June 15, 2026") set_run(r, size=7.5, color="6b7280") doc.save(output_path) if __name__ == "__main__": root = Path(sys.argv[1]).resolve() out = Path(sys.argv[2]).resolve() build_docx(root, out)