#!/usr/bin/env python3 """Render resume.yaml -> a .tex file. Two output flavors are supported: --level=short (default) -> condensed single-page resume. --level=full -> detailed resume; concatenates each list with its `_full` sibling (e.g. bullets + bullets_full). The LaTeX preamble (styles, macros, page layout) lives inline as PREAMBLE below; tweak typography or colors there. Content is read from resume.yaml. """ import argparse import os import yaml HERE = os.path.dirname(os.path.abspath(__file__)) DATA = os.path.join(HERE, "resume.yaml") DEFAULT_OUT = { "short": os.path.join(HERE, "MikeEberlein_Resume.tex"), "full": os.path.join(HERE, "MikeEberlein_Resume_Detailed.tex"), } PREAMBLE = r"""% Mike Eberlein — single-page resume (generated from resume.yaml). % Build: lualatex 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, loaded directly from ./fonts/static/ via fontspec. Requires % lualatex (or xelatex). This makes the build self-contained — no dependency % on TeX Live's bundled `roboto` package. \documentclass[letterpaper,10pt]{extarticle} \usepackage{fontspec} \setmainfont{Roboto}[ Path = fonts/static/, Extension = .ttf, UprightFont = *-Regular, ItalicFont = *-Italic, BoldFont = *-Bold, BoldItalicFont = *-BoldItalic, Ligatures = NoCommon, % keep fi/fl as two letters so PDF text search / ATS works ] \newfontfamily\RobotoLight{Roboto}[ Path=fonts/static/, Extension=.ttf, UprightFont=*-Light, ItalicFont=*-LightItalic, Ligatures=NoCommon, ] \newfontfamily\RobotoMedium{Roboto}[ Path=fonts/static/, Extension=.ttf, UprightFont=*-Medium, BoldFont=*-Bold, Ligatures=NoCommon, ] \usepackage{microtype} \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} \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 merged(item, key, level): """Return item[key] for short, or item[key] + item[key + '_full'] for full.""" base = item.get(key) or [] if level == "full": base = list(base) + list(item.get(key + "_full") or []) return base 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, level="short") -> 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 merged(data, "experience", level): out.append(f"\\job{{{tex_escape(company['company'])}}}{{{tex_escape(company['dates'])}}}") for role in merged(company, "roles", level): out.append(f"\\role{{{tex_escape(role['title'])}}}{{{tex_escape(role['dates'])}}}") subroles = merged(role, "subroles", level) if subroles: out.append("") for sr in subroles: out.append(f"\\subrole{{{tex_escape(sr['title'])}}}{{{tex_escape(sr['dates'])}}}") out.append(emit_bullets(merged(sr, "bullets", level), env="subbullets")) out.append("") else: bullets = merged(role, "bullets", level) if bullets: out.append(emit_bullets(bullets, env="bullets")) out.append("") out.append(r"\section{Education}\sectionrule") out.append("") for ed in merged(data, "education", level): out.append(f"\\job{{{tex_escape(ed['school'])}}}{{{tex_escape(ed['dates'])}}}") out.append(emit_bullets(merged(ed, "bullets", level), env="bullets")) out.append("") out.append(r"\section{Certifications}\sectionrule") out.append("") out.append(" \\textbullet{} ".join(tex_escape(c) for c in merged(data, "certifications", level))) out.append("") out.append(r"\end{document}") return "\n".join(out) + "\n" def main(): ap = argparse.ArgumentParser() ap.add_argument("--level", choices=["short", "full"], default="short") ap.add_argument("--output", default=None, help="Output .tex path (default: MikeEberlein_Resume.tex / MikeEberlein_Resume_Detailed.tex)") args = ap.parse_args() out_path = args.output or DEFAULT_OUT[args.level] with open(DATA, "r", encoding="utf-8") as f: data = yaml.safe_load(f) tex = render(data, level=args.level) with open(out_path, "w", encoding="utf-8") as f: f.write(tex) print(f"wrote {out_path} (level={args.level})") if __name__ == "__main__": main()