In my previous post we created the resume using orgmode. Although it was very
versatile and friendly with version control, I noticed that I had trouble with
blocks of text having tables like below where I had to mix in the latex and html
settings along with the content which wasn’t very pretty with git diff
.
* FirstName LastName
#+attr_html: :class mytable meta :rules all :border nil :cellspacing nil :cellpadding nil :frame nil
#+attr_latex: :align c|c|c|c
| [[mailto:email@gmail.com][email@gmail.com]] | [[https://linkedin.com/in/username][linkedin.com/in/username]] | +91-9876543210 | City, Country |
** Experience
*** Company 1
#+attr_html: :class mytable exp :rules nil :border nil :cellspacing nil :cellpadding nil :frame nil
#+attr_latex: :align L{0.27\textwidth}C{0.40\textwidth}R{0.25\textwidth}
| *Software Engineer* | *Company Inc.* | *Feb 2015 -- Present* |
| Software Team | City | |
In addition to that, to get anonymized resume reviews, I had to manually copy paste parts of the content into another org file and anonymize the relevant info before publishing it. I don’t find this to be that efficient use of my time. I ended up doing what the programmer stereotype does - automate this task of generating my resume which plays well with version control.
Yaml seemed to be the popular format which is (al)most human readable and easy to parse.
The gist of this automation is as follows.
- Write resume in yaml
- Parse yaml with python. Python code will have string snippets for org mode. Write this to an org file
- Run emacs from the command line with a custom init set up and export to both html and pdf
- Have a Makefile to automate all of this.
Seems straightforward enough.
Yaml structure
resume.yaml
name: Kishore Vancheeshwaran
contact:
email: personal.email@gmail.com
linkedin: linkedin.com/in/kishvanchee
phone: +91-0000000000
location: Bangalore, IN
experience:
- company: Company 2
position: Software Engineer
team: Awesome
start_date: Jan 2015
location: Bangalore
summary:
- I did something awesome
- I automated some work
- company: Company 1
position: Software Engineer
team: Awesome
start_date: Jan 2005
to_date: Dec 2014
location: Bangalore
summary:
- I did something awesome
- I automated some work
technical_skills:
languages: Python
frameworks: Django
databases: PostgreSQL
dev_tools: Git
education:
- degree: Fancy degree
university: University of freedom
from: Jul 2000
to: Jun 2004
The yaml file is pretty self explanatory. You can have multiple experience/companies, multiple education entries, etc.
Here’s a a not so pretty but works python script which takes care of parsing the yaml and writing it to org file.
#!/usr/bin/env python3
import yaml
import sys
import argparse
with open("resume.yaml", "r") as fin:
resume = yaml.safe_load(fin)
parser = argparse.ArgumentParser()
parser.add_argument("-t", "--type", required=False, help="type of resume")
args = parser.parse_args()
outfile = "resume_kishore.org"
if args.type == "anon":
outfile = "resume_anon.org"
resume["name"] = "First LastName"
resume["contact"]["email"] = "first.lastname@gmail.com"
resume["contact"]["linkedin"] = "linkedin.com/in/anon"
resume["contact"]["phone"] = "+91-9876543210"
resume["contact"]["location"] = "Earth, SS"
for i, _ in enumerate(resume["education"]):
resume["education"][i]["university"] = "University Name"
for i, _ in enumerate(resume["experience"]):
resume["experience"][i]["company"] = f"Company Name"
elif args.type == "recent":
resume["experience"][0]["summary"] = ["I currently work here"]
foreword = r"""
#+TITLE: Resume
#+author: {name}
#+options: toc:nil num:nil title:nil author:nil timestamp:nil html-style:nil prop:nil html-postamble:nil
#+latex_compiler: xelatex
#+latex_class: article
#+latex_class_options: [letterpaper,10pt]
#+latex_header: \include{{latexTemplate.tex}}
#+begin_src python :exports (if (org-export-derived-backend-p org-export-current-backend 'html) "results" "none") :results raw :eval yes
comment = '''
#+ATTR_HTML: :class linktopdf :title Kishore's Resume
[[https://kishvanchee.com/resume_kishore.pdf][Click here for pdf]]'''
return comment
#+end_src
"""
foreword = foreword.format(name=resume["name"])
header = """
* {name}
#+attr_html: :class mytable meta :rules all :border nil :cellspacing nil :cellpadding nil :frame nil
#+attr_latex: :align c|c|c|c
| [[mailto:{email}][_{email}_]] | [[https://{linkedin}][_{linkedin}_]] | {phone} | {location} |
"""
header = header.format(
name=resume["name"],
email=resume["contact"]["email"],
linkedin=resume["contact"]["linkedin"],
phone=resume["contact"]["phone"],
location=resume["contact"]["location"],
)
tbl_posn_format = r"""
#+attr_html: :class mytable exp :rules nil :border nil :cellspacing nil :cellpadding nil :frame nil
#+attr_latex: :align L{0.27\textwidth}C{0.40\textwidth}R{0.25\textwidth}
"""
experience_header = """
** Experience
"""
exp = []
for e in resume["experience"]:
company = "*** {company}".format(company=e["company"])
posn_header = (
f"| *{e['position']}* | *{e['company']}* | *{e['start_date']} -- {e['to_date'] if e.get('to_date') else 'Present'}* |"
+ "\n"
+ f"| {e['team']} | {e['location']} | |"
)
summary = "\n".join(["- " + s for s in e["summary"]])
components = company + tbl_posn_format + posn_header + "\n" + summary
# components = "".join([company, tbl_posn_format, posn_header, summary]) + "\n"
exp.append(components)
experience = experience_header + "\n".join(exp)
tech_skills = """
** Technical Skills
- *Languages* -- {languages}
- *Frameworks* -- {frameworks}
- *Databases* -- {databases}
- *Dev tools* -- {dev_tools}
"""
tech_skills = tech_skills.format(
languages=resume["technical_skills"]["languages"],
frameworks=resume["technical_skills"]["frameworks"],
dev_tools=resume["technical_skills"]["dev_tools"],
databases=resume["technical_skills"]["databases"],
)
edu = [
f"| *{ed['degree']}* | {ed['university']} | {ed['from']} -- {ed['to']} |"
for ed in resume["education"]
]
final_edu = "\n".join(edu)
education = r"""
** Education
#+attr_html: :class mytable education :rules nil :border nil :cellspacing nil :cellpadding nil :frame nil
#+attr_latex: :align L{0.27\textwidth}C{0.40\textwidth}R{0.25\textwidth}
"""
education = education + final_edu
final_resume = "".join([foreword, header, experience, tech_skills, education])
with open(outfile, "w") as fout:
fout.write(final_resume)
Now we have the Makefile
which automates the entire process. The recent
option is so that you can have your most recent work experience listed in your
yaml but not the actual pdf/html. It will be prepopulated with I currently work
here as the entry. This way you don’t have to worry about having two separate
entries with one having a noexport
in the org file or even having two separate
org files for the same. I wanted this feature to avoid disclosing what I am
currently working on in the current company unless it was necessary.
.PHONY: clean pdf anon recent html
all: clean pdf anon html
pdf:
python generate_resume.py
emacs resume_kishore.org --batch -f org-latex-export-to-pdf
html:
python generate_resume.py
emacs resume_kishore.org --batch -l custominit.el -f org-html-export-to-html
anon:
python generate_resume.py -t anon
emacs resume_anon.org --batch -f org-latex-export-to-pdf
recent:
python generate_resume.py -t recent
emacs resume_kishore.org --batch -f org-latex-export-to-pdf
emacs resume_kishore.org --batch -l custominit.el -f org-html-export-to-html
clean:
find . -maxdepth 1 ! -iname 'generate_resume.py' ! -iname 'resume.yaml' ! -iname 'Makefile' ! -iname 'latexTemplate.tex' ! -iname 'style.css' ! -iname 'custominit.el' -and -type f -exec rm "{}" \;
The custominit file is below. The file is necessary because -batch
mode runs emacs with -q
option which means without the default init file. Since we require only parts of the init file, we can have a custom file with the bare necessity to run the command.
;; This function is to run the eval without confirmation, at the top of the org
;; file to generate a link for the pdf file from the html file.
(defun org-confirm-babel-evaluate (lang body)
(not (or (string= lang "python") )))
(setq org-confirm-babel-evaluate 'org-confirm-babel-evaluate)
(org-babel-do-load-languages 'org-babel-load-languages '((python . t)))
(defun my-org-inline-css-hook (exporter)
"Insert custom inline css"
(when (eq exporter 'html)
(let* ((dir (ignore-errors (file-name-directory (buffer-file-name))))
(path (concat dir "style.css"))
(homestyle (or (null dir) (null (file-exists-p path))))
(final (if homestyle "~/.emacs.d/org-style.css" path)))
(setq org-html-head-include-default-style nil)
(setq org-html-head (concat
"<style type=\"text/css\">\n"
"<!--/*--><![CDATA[/*><!--*/\n"
(with-temp-buffer
(insert-file-contents final)
(buffer-string))
"/*]]>*/-->\n"
"</style>\n")))))
(add-hook 'org-export-before-processing-hook 'my-org-inline-css-hook)
The style.css
and latexTemplate.tex
remain the same as in the previous post.
Phew, now we can run make
and it automates the entire build. And we have
perfect diffs
.