Resume/render_tex.py

171 lines
5.5 KiB
Python

#!/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()