Coding my Resume


Somewhere around 2018, after getting the idea from a co-worker, I built my first Resume from Source. It was basically a reStructuredText file converted to HTML, styled with SCSS, and then converted to a PDF.

To update my resume, I would:

  1. Open up a text editor
  2. Make my changes
  3. Commit, tag, and push

After pushing, an automated CircleCI (remember the times before Github actions?) workflow would build, spellcheck, and release the HTML and PDF artifacts to my Resume repositories releases page.

While this resume and resume builder project has served me well, I wanted to address some things.

  1. Separate the content from the layout so I can independently experiment with layout changes and content changes.
  2. Build separate resumes with content targeted for each job description/listing
  3. Job listing specific links + link tracking
  4. Move from reStructuredText to Markdown
  5. Focus on Word (.docx) as a build artifact and generate PDFs from the Word doc instead of HTML

So, right after leaving my previous position, I dug in for a couple of weeks.

The Content

I first started with the content of my resume. I already had some decent content that could be reused from the old version but needed some updating. And I still needed to add highlights and everything for the last 5 years. I'm no writer, but resume copy is tough. I struggled to separate all I had done in the last five years in a few bullet points of ~25 words each.

I asked ChatGPT to help me, and it was a disaster, at least initially. I reached too far with my limited knowledge of LLMs and Prompt Engineering. I tried to get the system to play the role of a resume writing assistant when the amount of context (a prompt, a draft resume, and a job description) needed would always exceed the context window, but ChatGPT doesn't tell you that.

Imagine a world where there is no stderr and no exit code 0, only stdout, and no structured logging, so to ensure a program runs successfully, you have to look through all the logs and make sure they "look right" or write another program to check the logs and its turtles all the way down. That's trying to build a GPT assistant

The main issue I had was with ChatGPT lying straight-up, saying I had 10 years of experience with C++ or something similar. I spent a lot of time trying to minimize it, but the idea of a lie slipping through on a resume, even accidentally, was mortifying to me.

The second issue was laziness. It might do a great job on the first few sections of the resume-- clear language, factual statements (a big plus), following instructions, etc. But then each following section would get exponentially worse-- short, vague language, misconstrued statements, and baseless lies.

I spent hours over a couple of days chasing this dragon. I iterated on 20 or so versions of the prompt, ran them through incremental tests under different configurations, and couldn't consistently get what I wanted from the assistant. I was defeated; I only turned to Chat GPT because I needed help with about five bullet points.

I cut bait and went down the mostly manual route, curating any good content from Chat GPT and my old resume. Turning to ChatGPT again only for help summarizing a few sentences at a time in isolation. I'm happy with the content of my resume now; it just took a lot more time to get there than I anticipated. Sure, I "wasted" some time doing a programmer's favorite thing: spending 10 hours failing to automate something that could be done manually in 1. But hey, I'm not on anyone's dime, and I'd be doing this kind of stuff for fun anyway.

The Source

At this point, I have a JSON file loosely based on JSON Resume that I need (or want) to somehow transform into an HTML File, Word document, and PDF. My first instinct was to reach for React and use the old HTML generated from RST to serve as the base elements in my React components, use ReactDomServer to render a string, and go from there. I even found a Docx React reconciler, which was somewhat validating.

Ultimately, I chose not to take this route. Node.JS, TypeScript, React, and all the cruft that comes with a new JS project seemed like too many dependencies for what is ultimately just a tool to produce static files. I had been reading Thorsten Ball's Writing An Interpreter In Go and working on a toy language, but I put those on hold while I worked on my resume. So, this seemed like a time to eke out a side quest and get another new project to add to my resume.

So... I built a templating engine from scratch. Whoops!

Tmplrun is the shell of a Bring Your Own Language text templating system available as a Golang module and a minimal CLI. The elevator pitch is that it's basically like any other templating engine without the DSL for interacting with and manipulating data; use your favorite language.

It supports Javascript (via Goja) out of the box and needs a little more work to make bringing your own language possible. But Javascript as a templating language was good enough for this resume project; if I run into issues with it, I can make those fixes in Tmplrun, and I'm set. I'll give Tmplrun its own post at some point, but here's the TLDR for now.

The syntax is simple:

  • opening <% and closing tokens %>
  • An optional identifier for the language <%id id%>
  • Optionally pad with an additional % for disambiguation depending on the content <%% "just in case %> " %%>
  • Open and close must be "symmetrical" <%js "Good" js%> Bad: <%js "Bad" %>
  • Code in the middle <% code goes here %>
  • Code must be evaluated to a string (or something that the language driver can serialize into a JSON string)
The default is javascript: <% `%{user.lastName[0]} ,${user.firstName}` %>
But you can be specific <%js `%{user.lastName[0]} ,${user.firstName}` js%>
You can even get wild <% 
	(function() {
		const { lastName, firstName } = user;
		return `%{user.lastName[0]} ,${user.firstName}`
Other languages (future): <%rb "Hello ${user['firstName']}" rb%>

# Runtime features
Include plain text: <% include("path/to/some/file.txt") %>
Render another template with different props: <% template("path/to/template", { ...user, lastName: user.lastName[0] })  %>

And we're back

So now I have a templating language. Cool. I've also written my lexer+parser, which I'm actually going to use for something. Even cooler. I can produce a Markdown file from other source Markdown files, and my JSON file does not sound that impressive, nor is it much further than where I started with that old RST file. But remember, this whole Tmplrun thing was a side-quest, and I have a neat little project to add to my resume.

I'm playing it down a bit, but this markdown is really the IR (intermediate representation). With this, we should be able to compile it into HTML, Word, etc. I thought I could get away with a bunch of Pandoc magic and be done with it, but I couldn't achieve a unified look across all targets, even after a LaTeX rabbit hole I don't want to discuss. Plus, I knew I wanted to add a language extension to support TabStop. I wasn't going to let that go.

So I wrote the TabStop language extension, a Docx/XML renderer for the Goldmark Markdown parser (an outstanding project), a bunch of other glue tooling, and boom, Resume 3.0.0