feat: created CV using ConTeXt, Go and JSON
- ConTeXt is used for the CV template. - CV data is stored in a JSON document. - A small Go script is used to parse the JSON data and render the final cv.tex file. - ConTeXt is then used to render the CV in PDF (other formats to be supported soon).
This commit is contained in:
parent
044cba4d39
commit
a7b1d7d2cd
8 changed files with 444 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
__output/
|
||||||
|
tags
|
||||||
|
notes.txt
|
14
Makefile
Normal file
14
Makefile
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
OUTPUT_DIR = __output/
|
||||||
|
|
||||||
|
.PHONY: all tex pdf clean
|
||||||
|
|
||||||
|
all: pdf
|
||||||
|
|
||||||
|
tex:
|
||||||
|
@go run .
|
||||||
|
|
||||||
|
pdf: tex
|
||||||
|
@mtxrun --path=$(OUTPUT_DIR) --script context cv.tex
|
||||||
|
|
||||||
|
clean:
|
||||||
|
@rm -rf $(OUTPUT_DIR)
|
174
data/cv.json
Normal file
174
data/cv.json
Normal file
|
@ -0,0 +1,174 @@
|
||||||
|
{
|
||||||
|
"firstName": "Dan",
|
||||||
|
"lastName": "Anglin",
|
||||||
|
"jobTitle": "Software Engineer",
|
||||||
|
"contact": {
|
||||||
|
"email": "d.n.i.anglin@gmail.com",
|
||||||
|
"phone": "07XXX-XXX-XXX",
|
||||||
|
"location": "Hertfordshire, UK"
|
||||||
|
},
|
||||||
|
"links": {
|
||||||
|
"gitlab": "gitlab.com/dananglin",
|
||||||
|
"github": "github.com/dananglin"
|
||||||
|
},
|
||||||
|
"summary": "To be completed.",
|
||||||
|
"technologies": [
|
||||||
|
{
|
||||||
|
"category": "Programming Languages",
|
||||||
|
"values": [
|
||||||
|
"Go",
|
||||||
|
"Bash",
|
||||||
|
"Python"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Containerisation",
|
||||||
|
"values": [
|
||||||
|
"Docker",
|
||||||
|
"Kubernetes",
|
||||||
|
"LXD"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Continuous Integration",
|
||||||
|
"values": [
|
||||||
|
"Git",
|
||||||
|
"GitLab",
|
||||||
|
"Gitea",
|
||||||
|
"GitLab CI",
|
||||||
|
"Drone",
|
||||||
|
"Nexus",
|
||||||
|
"Jenkins"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Monitoring and Alerting",
|
||||||
|
"values": [
|
||||||
|
"Prometheus",
|
||||||
|
"Alertmanager",
|
||||||
|
"Grafana"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Other Tools, Services or Frameworks",
|
||||||
|
"values": [
|
||||||
|
"AWS",
|
||||||
|
"Openstack",
|
||||||
|
"UKCloud",
|
||||||
|
"GNU/Linux",
|
||||||
|
"Elasticsearch",
|
||||||
|
"Logstash",
|
||||||
|
"Fluentd",
|
||||||
|
"Kibana",
|
||||||
|
"Ansible",
|
||||||
|
"Puppet",
|
||||||
|
"OpenAPI v3",
|
||||||
|
"PostgreSQL",
|
||||||
|
"HAProxy",
|
||||||
|
"Kanban",
|
||||||
|
"Scrum",
|
||||||
|
"JIRA"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"employment": [
|
||||||
|
{
|
||||||
|
"company": "Ocado Technology",
|
||||||
|
"location": "Hatfield, Hertfordshire",
|
||||||
|
"jobTitle": "Platform Automation Engineer",
|
||||||
|
"duration": {
|
||||||
|
"start": "January, 2017",
|
||||||
|
"end": "Present"
|
||||||
|
},
|
||||||
|
"details": [
|
||||||
|
"To be completed..."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"company": "QA Consulting (Contracted to CACI)",
|
||||||
|
"location": "London",
|
||||||
|
"jobTitle": "DevOps Consultant",
|
||||||
|
"duration": {
|
||||||
|
"start": "March, 2015",
|
||||||
|
"end": "November, 2016"
|
||||||
|
},
|
||||||
|
"details": [
|
||||||
|
"I've built and supported Production infrastructure hosted in UKCloud (for public sector projects) and AWS (for commercial projects).",
|
||||||
|
"My general tasks included creating Ansible playbooks for network configuration, hardening of RedHat/CentOS VMs and software installation and configuration.",
|
||||||
|
"I've set up resilient and fault-tolerant infrastructure using tools such as HAProxy and Keepalive.",
|
||||||
|
"I've built a cross-domain protective monitoring solution using the Elastic stack with Redis.",
|
||||||
|
"I've installed and configured Nagios/Icinga for infrastructure monitoring.",
|
||||||
|
"I was involved in setting up the production Docker environment for a government website."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"company": "QA Consulting (Contracted to IBM)",
|
||||||
|
"location": "Home Office, London",
|
||||||
|
"jobTitle": "DevOps Consultant",
|
||||||
|
"duration": {
|
||||||
|
"start": "October, 2014",
|
||||||
|
"end": "March, 2015 "
|
||||||
|
},
|
||||||
|
"details": [
|
||||||
|
"I've built and supported new development, CIT and SIT environments for new projects at the Home Office.",
|
||||||
|
"These environments were built on UKCloud using Jenkins and UKCloud's REST API.",
|
||||||
|
"Linux (RedHat/CentOS) virtual machines were provisioned and hardenend using Puppet and Jenkins.",
|
||||||
|
"I learnt how to work in an Agile environment with SCRUM; using JIRA for keeping track of my Sprint tasks.",
|
||||||
|
"Confluence was used for internal documentation and I used it to create documentation for new starters in the DevOps space."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"company": "QA Consulting",
|
||||||
|
"location": "Worthing",
|
||||||
|
"jobTitle": "Trainee DevOps Consultant",
|
||||||
|
"duration": {
|
||||||
|
"start": "September, 2014",
|
||||||
|
"end": "October, 2014"
|
||||||
|
},
|
||||||
|
"details": [
|
||||||
|
"I've spent eight weeks in training to become a DevOps Consultant on behalf of QA Consulting.",
|
||||||
|
"I've completed a two week intensive training course on business analysis. Trainig course covered client engagement, businnes process modelling, use case modelling and process and evaluation improvements.",
|
||||||
|
"I then completed a six week hands-on training course on the fundamentals of DevOps and Continuous Integration.",
|
||||||
|
"The DevOps training course consisted of learning about various software such as Puppet, Jenkins, Nexus, Maven, Git, Tomcat and the Atlassian tools.",
|
||||||
|
"The training also included using Vagrant and Puppet to automate the provisioning of small CI and development environments for the continuous delivery of internal Java projects."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"company": "School of Systems Engineering, University of Reading",
|
||||||
|
"location": "Reading",
|
||||||
|
"jobTitle": "Undergraduate Researcher",
|
||||||
|
"duration": {
|
||||||
|
"start": "July, 2012",
|
||||||
|
"end": "September, 2012"
|
||||||
|
},
|
||||||
|
"details": [
|
||||||
|
"This was a summer internship where I helped developed an interactive Java application that processes Terahertz signals in order to reveal hidden details behind walls.",
|
||||||
|
"This was aimed at those who were not specialised in Terahertz signals to use an application to aid in their research in archaeology.",
|
||||||
|
"The hidden details were captured in images or videos that could be viewed in a 2D or a 3D virtual environment.",
|
||||||
|
"During this internship I learnt new techniques for processing Terahertz signals.",
|
||||||
|
"I was able to use sample Terahertz data to reveal and display hidden details within a 3D virtual environment."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"education": [
|
||||||
|
{
|
||||||
|
"school": "University of Reading",
|
||||||
|
"location": "Reading",
|
||||||
|
"qualification": "MEng (Hons) in Robotics, 2:1",
|
||||||
|
"duration": {
|
||||||
|
"start": "October, 2009",
|
||||||
|
"end": "July, 2013"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"school": "St Angela's & St Bonaventure's Sixth Form College",
|
||||||
|
"location": "London",
|
||||||
|
"qualification": "A-Levels in Maths, Physics and Product Design",
|
||||||
|
"duration": {
|
||||||
|
"start": "September, 2007",
|
||||||
|
"end": "July, 2009"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"interests": "To be completed..."
|
||||||
|
}
|
3
go.mod
Normal file
3
go.mod
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
module gitlab.com/dananglin/cv
|
||||||
|
|
||||||
|
go 1.12
|
67
main.go
Normal file
67
main.go
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"text/template"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
cvDataPath string = "data/cv.json"
|
||||||
|
cvTemplateDir string = "template/"
|
||||||
|
cvOutputDir string = "__output/"
|
||||||
|
cvOutputFileName string = "cv.tex"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
|
||||||
|
var cv Cv
|
||||||
|
cvOutputPath := cvOutputDir + cvOutputFileName
|
||||||
|
|
||||||
|
fmap := template.FuncMap{
|
||||||
|
"notLastElement": notLastElement,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the JSON data
|
||||||
|
log.Printf("INFO: Attempting to read data from %s...", cvDataPath)
|
||||||
|
data, err := ioutil.ReadFile(cvDataPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("ERROR: Unable to read data from file. %s", err.Error())
|
||||||
|
}
|
||||||
|
log.Print("INFO: Successfully read data.")
|
||||||
|
|
||||||
|
log.Printf("INFO: Attempting to unmarshal JSON data...")
|
||||||
|
if err = json.Unmarshal(data, &cv); err != nil {
|
||||||
|
log.Fatalf("ERROR: Unable to unmarshal JSON data. %s", err.Error())
|
||||||
|
}
|
||||||
|
log.Println("INFO: JSON unmarshalling was successful.")
|
||||||
|
|
||||||
|
// Create the Output tex file
|
||||||
|
log.Printf("INFO: Attempting to create output file %s...", cvOutputPath)
|
||||||
|
if err = os.MkdirAll(cvOutputDir, 0750); err != nil {
|
||||||
|
log.Fatalf("ERROR: Unable to create output directory %s. %s", cvOutputDir, err.Error())
|
||||||
|
}
|
||||||
|
output, err := os.Create(cvOutputPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("ERROR: Unable to create output file %s. %s.", cvOutputPath, err.Error())
|
||||||
|
}
|
||||||
|
defer output.Close()
|
||||||
|
log.Printf("INFO: Successfully created output file %s.", cvOutputPath)
|
||||||
|
|
||||||
|
// Execute template engine and produce the resulting TEX file
|
||||||
|
log.Println("INFO: Attempting template execution...")
|
||||||
|
t := template.Must(template.New("cv.tex.tmpl").Funcs(fmap).Delims("<<", ">>").ParseGlob(cvTemplateDir + "*.tex.tmpl"))
|
||||||
|
|
||||||
|
if err = t.Execute(output, cv); err != nil {
|
||||||
|
log.Fatalf("ERROR: Unable to execute the CV template. %s", err.Error())
|
||||||
|
}
|
||||||
|
log.Println("INFO: Template execution successful.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// notLastElement returns true if an element of an array
|
||||||
|
// or a slice is not the last.
|
||||||
|
func notLastElement(pos, length int) bool {
|
||||||
|
return pos < length-1
|
||||||
|
}
|
46
model.go
Normal file
46
model.go
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
type Cv struct {
|
||||||
|
FirstName string `json:"firstName"`
|
||||||
|
LastName string `json:"lastName"`
|
||||||
|
JobTitle string `json:"jobTitle"`
|
||||||
|
Contact Contact `json:"contact"`
|
||||||
|
Links Links `json:"links"`
|
||||||
|
Summary string `json:"summary"`
|
||||||
|
Technologies []Technologies `json:"technologies"`
|
||||||
|
Employment []Experience `json:"employment"`
|
||||||
|
Education []Experience `json:"education"`
|
||||||
|
Interests string `json:"interests"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Contact struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
Phone string `json:"phone"`
|
||||||
|
Location string `json:"location"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Links struct {
|
||||||
|
GitLab string `json:"gitlab"`
|
||||||
|
GitHub string `json:"github"`
|
||||||
|
Website string `json:"website,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Technologies struct {
|
||||||
|
Category string `json:"category"`
|
||||||
|
Values []string `json:"values"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Experience struct {
|
||||||
|
Company string `json:"company,omitempty"`
|
||||||
|
School string `json:"school,omitempty"`
|
||||||
|
Location string `json:"location"`
|
||||||
|
JobTitle string `json:"jobTitle,omitempty"`
|
||||||
|
Qualification string `json:"qualification,omitempty"`
|
||||||
|
Duration Duration `json:"duration"`
|
||||||
|
Details []string `json:"details"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Duration struct {
|
||||||
|
Start string `json:"start"`
|
||||||
|
End string `json:"end"`
|
||||||
|
}
|
53
template/cv.tex.tmpl
Normal file
53
template/cv.tex.tmpl
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
<<- /* Prepend the setup area */ ->>
|
||||||
|
<< template "cv_setup.tex.tmpl" .>>
|
||||||
|
|
||||||
|
\starttext
|
||||||
|
\starttitleAndContact
|
||||||
|
\cvtitle{<<.FirstName>> <<.LastName>>}{<<.JobTitle>>}
|
||||||
|
\titleAndContact
|
||||||
|
{\bf Email:} <<.Contact.Email>>\blank[none]
|
||||||
|
{\bf Phone:} <<.Contact.Phone>>\blank[none]
|
||||||
|
{\bf Location:} <<.Contact.Location>>\blank[medium]
|
||||||
|
|
||||||
|
{\bf GitLab:} \goto{<<.Links.GitLab>>}[url(https://<<.Links.GitLab>>)]\blank[none]
|
||||||
|
{\bf GitHub:} \goto{<<.Links.GitHub>>}[url(https://<<.Links.GitHub>>)]\blank[none]
|
||||||
|
\stoptitleAndContact
|
||||||
|
|
||||||
|
\section{SUMMARY}
|
||||||
|
<<.Summary>>
|
||||||
|
|
||||||
|
\section{SKILLS SUMMARY}
|
||||||
|
\starttabulate[|w(0.3\textwidth)lB|lp(0.7\textwidth)|]
|
||||||
|
<<$lenTech := len .Technologies>>
|
||||||
|
<<range $i, $tech := .Technologies>>
|
||||||
|
<<$lenValues := len $tech.Values>>
|
||||||
|
\NC <<$tech.Category>> \NC <<range $j, $val := $tech.Values>><<$val>><<if notLastElement $j $lenValues>>, <<end>><<end>>\NC\NR
|
||||||
|
<<if notLastElement $i $lenTech>>\TB[1mm]<<end>>
|
||||||
|
<<end>>
|
||||||
|
\stoptabulate
|
||||||
|
\section{EXPERIENCE}
|
||||||
|
<<- range .Employment>>
|
||||||
|
\jobsection{<<.Company>>}{<<.Location>>}{<<.JobTitle>>}{<<.Duration.Start>> - <<.Duration.End>>}
|
||||||
|
\startitemize
|
||||||
|
<<range .Details>>
|
||||||
|
\item <<.>>
|
||||||
|
<<end>>
|
||||||
|
\stopitemize
|
||||||
|
<<end>>
|
||||||
|
|
||||||
|
\section{EDUCATION}
|
||||||
|
<<range .Education ->>
|
||||||
|
\jobsection{<<.School>>}{<<.Location>>}{<<.Qualification>>}{<<.Duration.Start>> - <<.Duration.End>>}
|
||||||
|
\startitemize
|
||||||
|
<<range .Details>>
|
||||||
|
\item <<.>>
|
||||||
|
<<end>>
|
||||||
|
\stopitemize
|
||||||
|
<<end>>
|
||||||
|
|
||||||
|
\section{OTHER INTERESTS}
|
||||||
|
<<.Interests>>
|
||||||
|
|
||||||
|
\section{REFERENCES}
|
||||||
|
References are available upon request.
|
||||||
|
\stoptext
|
84
template/cv_setup.tex.tmpl
Normal file
84
template/cv_setup.tex.tmpl
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
\version [final]
|
||||||
|
|
||||||
|
% set main language to English
|
||||||
|
\mainlanguage [en]
|
||||||
|
|
||||||
|
% set paper size to A4
|
||||||
|
\setuppapersize [A4]
|
||||||
|
|
||||||
|
% define the page layout
|
||||||
|
\setuplayout
|
||||||
|
[backspace=20mm,
|
||||||
|
bottomspace=20mm,
|
||||||
|
cutspace=0mm,
|
||||||
|
footer=0mm,
|
||||||
|
header=0mm,
|
||||||
|
height=middle,
|
||||||
|
topspace=10mm,
|
||||||
|
width=middle]
|
||||||
|
% \showframe
|
||||||
|
|
||||||
|
% remove page numbers by setting an empty location
|
||||||
|
\setuppagenumbering[location=]
|
||||||
|
|
||||||
|
% Colour definitions for headings
|
||||||
|
\definecolor [bluetheme][h=1155cc]
|
||||||
|
\definecolor [greytheme][h=434343]
|
||||||
|
|
||||||
|
% Font setup
|
||||||
|
% https://wiki.contextgarden.net/Simplefonts
|
||||||
|
\definefontfamily [cvfontfamily][rm][LiberationSerif]
|
||||||
|
\definefontfamily [cvfontfamily][ss][Carlito]
|
||||||
|
\definefontfamily [cvfontfamily][tt][LiberationMono]
|
||||||
|
\definefontfamily [cvfontfamily][mm][LibertinusMath]
|
||||||
|
\setupbodyfont [cvfontfamily,ss,10pt]
|
||||||
|
|
||||||
|
% define the font scaling when using commands such as \tfa, \tfb, etc
|
||||||
|
\definebodyfontenvironment [10pt][a=11pt,b=20pt,c=32pt]
|
||||||
|
|
||||||
|
% cvtitle outputs the title of the CV
|
||||||
|
% arguments:
|
||||||
|
% #1 - name: name of the individual
|
||||||
|
% #2 - role: job role of the individual
|
||||||
|
\define[2]\cvtitle{
|
||||||
|
{\bfc#1}\blank[small]{\tfb#2}
|
||||||
|
}
|
||||||
|
|
||||||
|
% jobsection outputs the summary of a work experience
|
||||||
|
% arguments:
|
||||||
|
% #1 - company: the company where the work experience took place
|
||||||
|
% #2 - location: the location of the work experience
|
||||||
|
% #3 - role: the role of the work experience
|
||||||
|
% #4 - duration: the duration of the work experience (e.g. March, 2017 - April, 2018)
|
||||||
|
\define[4]\jobsection{
|
||||||
|
{\bfa#1,} {\tfa#2} — {\tfa\em#3}\blank[small]\color[greytheme]{#4}\blank[line]
|
||||||
|
}
|
||||||
|
\definehead [skillssection][subsection]
|
||||||
|
|
||||||
|
\setuphead [section][color=bluetheme]
|
||||||
|
\setuphead [skillssection][color=black]
|
||||||
|
\setuphead [section, skillssection][number=no, align=right, style=\bfa]
|
||||||
|
|
||||||
|
% setup the format of items (bullet points)
|
||||||
|
\setupitemize
|
||||||
|
[1]
|
||||||
|
[packed]
|
||||||
|
[align=right,
|
||||||
|
symbol=1,
|
||||||
|
margin=no,
|
||||||
|
distance=1mm]
|
||||||
|
|
||||||
|
% defineparagraphs and setupparagraphs allows us to create custom
|
||||||
|
% columns. Here we define and configure a paragraph environment for the CV title
|
||||||
|
% and contact columns.
|
||||||
|
% https://wiki.contextgarden.net/Command/defineparagraphs
|
||||||
|
% https://wiki.contextgarden.net/Command/setupparagraphs
|
||||||
|
\defineparagraphs[titleAndContact][n=2,after={\blank}]
|
||||||
|
\setupparagraphs [titleAndContact][1][width=0.6\textwidth]
|
||||||
|
\setupparagraphs [titleAndContact][2][width=0.4\textwidth]
|
||||||
|
|
||||||
|
% use Interaction to create hyperlinks to external profiles
|
||||||
|
\setupinteraction
|
||||||
|
[state=start,
|
||||||
|
color=bluetheme,
|
||||||
|
click=]
|
Loading…
Reference in a new issue