#!/usr/bin/env python3 """Render resume.yaml -> MikeEberlein_Resume.tex. The LaTeX preamble (styles, macros, page layout) lives inline as PREAMBLE below; if you want to tweak typography or colors, edit there. Content is read from resume.yaml. """ import os import sys import yaml HERE = os.path.dirname(os.path.abspath(__file__)) DATA = os.path.join(HERE, "resume.yaml") OUT = os.path.join(HERE, "MikeEberlein_Resume.tex") PREAMBLE = r"""% Mike Eberlein — single-page resume (generated from resume.yaml). % Build: pdflatex MikeEberlein_Resume.tex (run twice for stable layout) % Do not edit by hand — re-run `python3 render_tex.py` after changing resume.yaml. % % Fonts: Roboto via the roboto LaTeX package (texlive-fonts-extra). The official % Google Roboto font repo (with OFL license) is also bundled at ./fonts/ — % individual weights live in ./fonts/static/, variable fonts at ./fonts/. \documentclass[letterpaper,10pt]{extarticle} \usepackage[T1]{fontenc} \usepackage[utf8]{inputenc} \usepackage[sfdefault]{roboto} \usepackage{microtype} \DisableLigatures{encoding = *, family = *} % keep fi/fl as two letters so PDF text search / ATS works \usepackage[letterpaper,margin=0.5in,top=0.35in,bottom=0.3in]{geometry} \usepackage[dvipsnames]{xcolor} \usepackage{titlesec} \usepackage{enumitem} \usepackage{hyperref} \usepackage{ragged2e} \usepackage{parskip} \newcommand{\RobotoLight}{\fontseries{l}\selectfont} \newcommand{\RobotoMedium}{\fontseries{m}\selectfont} \definecolor{accent}{HTML}{1F3864} \definecolor{body}{HTML}{222222} \definecolor{muted}{HTML}{555555} \color{body} \hypersetup{colorlinks=true, urlcolor=accent, linkcolor=accent} \setlength{\parindent}{0pt} \setlength{\parskip}{0pt} \linespread{1.0} \pagestyle{empty} \titleformat{\section} {\color{accent}\RobotoMedium\bfseries\large} {}{0pt} {\MakeUppercase} \titlespacing*{\section}{0pt}{4pt}{0pt} \newcommand{\sectionrule}{\vspace{-4pt}{\color{accent}\rule{\linewidth}{0.5pt}}\par\vspace{1pt}} \newcommand{\job}[2]{% \vspace{3pt}% \noindent\textbf{#1}\hfill\textbf{#2}\par\vspace{0pt}% } \newcommand{\role}[2]{% \vspace{1pt}% \noindent\textit{\textcolor{muted}{#1}}\hfill\textit{\textcolor{muted}{#2}}\par% } \newcommand{\subrole}[2]{% \vspace{1pt}% \noindent\hspace{0.18in}#1\hfill\textit{\textcolor{muted}{#2}}\par% } \newlist{bullets}{itemize}{2} \setlist[bullets]{leftmargin=0.22in, itemsep=0pt, topsep=0pt, parsep=0pt, label={\textbullet}} \newlist{subbullets}{itemize}{2} \setlist[subbullets]{leftmargin=0.42in, itemsep=0pt, topsep=0pt, parsep=0pt, label={\textbullet}, before=\color{muted}} \newcommand{\name}[1]{% \begin{center}% {\color{accent}\fontseries{l}\fontsize{26}{30}\selectfont #1}% \end{center}% \vspace{10pt}% } \newcommand{\contact}[1]{% \begin{center}\textcolor{muted}{\small #1}\end{center}% \vspace{2pt}% } """ # LaTeX special characters that need escaping in content text. LATEX_ESCAPES = { "&": r"\&", "%": r"\%", "$": r"\$", "#": r"\#", "_": r"\_", "{": r"\{", "}": r"\}", "~": r"\textasciitilde{}", "^": r"\textasciicircum{}", "\\": r"\textbackslash{}", } def tex_escape(s: str) -> str: out = [] for ch in s: out.append(LATEX_ESCAPES.get(ch, ch)) return "".join(out) def emit_bullets(items, env="bullets"): lines = [f"\\begin{{{env}}}"] for b in items: lines.append(f" \\item {tex_escape(b)}") lines.append(f"\\end{{{env}}}") return "\n".join(lines) def render(data) -> str: out = [PREAMBLE, ""] out.append(r"\begin{document}") out.append("") h = data["header"] out.append(f"\\name{{{tex_escape(h['name'])}}}") contact = ( f"{tex_escape(h['tagline'])} \\textbullet{{}} " f"\\href{{mailto:{h['email']}}}{{{tex_escape(h['email'])}}}" ) out.append(f"\\contact{{{contact}}}") out.append("") out.append("{\\justifying\\noindent") out.append(tex_escape(data["summary"].strip()) + r"\par}") out.append("") out.append(r"\section{Experience}\sectionrule") out.append("") for company in data["experience"]: out.append(f"\\job{{{tex_escape(company['company'])}}}{{{tex_escape(company['dates'])}}}") for role in company["roles"]: out.append(f"\\role{{{tex_escape(role['title'])}}}{{{tex_escape(role['dates'])}}}") if "subroles" in role: out.append("") for sr in role["subroles"]: out.append(f"\\subrole{{{tex_escape(sr['title'])}}}{{{tex_escape(sr['dates'])}}}") out.append(emit_bullets(sr["bullets"], env="subbullets")) out.append("") elif "bullets" in role: out.append(emit_bullets(role["bullets"], env="bullets")) out.append("") out.append(r"\section{Education}\sectionrule") out.append("") for ed in data["education"]: out.append(f"\\job{{{tex_escape(ed['school'])}}}{{{tex_escape(ed['dates'])}}}") out.append(emit_bullets(ed["bullets"], env="bullets")) out.append("") out.append(r"\section{Certifications}\sectionrule") out.append("") out.append(" \\textbullet{} ".join(tex_escape(c) for c in data["certifications"])) out.append("") out.append(r"\end{document}") return "\n".join(out) + "\n" def main(): with open(DATA, "r", encoding="utf-8") as f: data = yaml.safe_load(f) tex = render(data) with open(OUT, "w", encoding="utf-8") as f: f.write(tex) print(f"wrote {OUT}") if __name__ == "__main__": main()