Skip to content

Commit 7be1bc2

Browse files
committed
GH-163: Add LaTeX resume export functionality
this close #163 Introduce support for generating ATS-compatible resumes in LaTeX format. Updated Dockerfile, build scripts, and tests to support this feature, along with adjustments to file handling and output structure. Signed-off-by: David Ng <[email protected]>
1 parent 5267d5e commit 7be1bc2

File tree

7 files changed

+156
-53
lines changed

7 files changed

+156
-53
lines changed

docker-compose.build.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ services:
1717
- bind:
1818
create_host_path: true
1919
source: ./dist
20-
target: /app/public
20+
target: /app/dist
2121
type: bind
2222
web:
2323
command: ./scripts/docker/build.sh

systems/resume/Dockerfile

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ FROM mcr.microsoft.com/playwright:v1.50.1 AS base
33

44
WORKDIR /app
55

6-
COPY ./package.json ./package-lock.json ./index.html ./vite.config.js ./resume-to-pdf.js ./
6+
COPY ./package.json ./package-lock.json ./index.html ./vite.config.js ./resume-to-pdf.js ./resume-to-latex.js ./workspace.js ./
77
COPY ./scripts/docker/ ./scripts/docker/
8-
COPY ./public ./public/
8+
COPY ./public/ ./public/
99

1010
RUN sh ./scripts/docker/setup.sh

systems/resume/pdf.test.js

+19
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,22 @@ it('Test PDF content', async () => {
3333
).join('\n');
3434
expect(fullText.length).toBeGreaterThan(0);
3535
});
36+
37+
it('Test ATS Resume PDF content', async () => {
38+
const { getDocument } = await import('pdfjs-dist/legacy/build/pdf.mjs');
39+
const document = await getDocument(`${PUBLIC_FOLDER}/ats-resume.pdf`).promise;
40+
const fullText = (
41+
await withFromAsync(Array).fromAsync(
42+
Array.from({ length: document.numPages }).map(async (_, i) => {
43+
return document
44+
.getPage(i + 1)
45+
.then(page => page.getTextContent())
46+
.then(textContent =>
47+
textContent.items.map(item => item.str).join('\n'),
48+
);
49+
}),
50+
)
51+
).join('\n\n');
52+
console.log(fullText);
53+
expect(fullText.length).toBeGreaterThan(0);
54+
});

systems/resume/public/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
*.json
22
*.pdf
3+
*.tex

systems/resume/public/resume.tex

-48
This file was deleted.

systems/resume/resume-to-latex.js

+128
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
export function resumeToLatex(resume) {
2+
function escapes(source) {
3+
return source.replaceAll('&', '\\&');
4+
}
5+
const { basics, education, skills, work: works } = resume,
6+
flattenSkills = skills
7+
.map(skill => [skill.name, skill.keywords])
8+
.reduce((acc, item) => {
9+
if (acc[item[0]]) {
10+
acc[item[0]].push(...item[1]);
11+
} else {
12+
acc[item[0]] = item[1];
13+
}
14+
return acc;
15+
}, {}),
16+
githubProfileUrl = basics.profiles.find(
17+
profile => profile.network === 'Github',
18+
).url,
19+
linkedInProfileUrl = basics.profiles.find(
20+
profile => profile.network === 'Linkedin',
21+
).url;
22+
return `\\documentclass{res}
23+
24+
\\name{${basics.name}}
25+
\\address{${basics.location.region}}
26+
\\address{${basics.email}}
27+
28+
\\begin{document}
29+
Name: ${basics.name}
30+
\\\\
31+
Position Applied For: ${basics.label}
32+
\\\\
33+
Email: ${basics.email}
34+
\\\\
35+
Address: ${basics.location.region}
36+
\\\\
37+
My website: https://neviaumi.github.io/portfolio/resume
38+
\\\\
39+
Github: ${githubProfileUrl}
40+
\\\\
41+
LinkedIn: ${linkedInProfileUrl}
42+
\\\\
43+
\\section{ABOUT THIS DOCUMENT}
44+
This document has been optimized for Applicant Tracking Systems (ATS) to ensure accurate parsing and alignment with job-specific requirements.
45+
For a comprehensive view of my portfolio and additional details, please visit: https://neviaumi.github.io/portfolio/resume
46+
\\
47+
\\
48+
49+
\\hyphenpenalty=10000
50+
\\exhyphenpenalty=10000
51+
\\section{Summary}
52+
${basics.summary}
53+
\\
54+
\\
55+
\\section{EXPERIENCE}
56+
\\begin{itemize}
57+
${works
58+
.map(work => {
59+
return `\\item[-] ${work.position} | ${work.company}, ${work.location} | ${work.startDate} - ${work.endDate}
60+
${work.summary ? `\\\\ \\textit{Role Summary:} ${work.summary.trim()}` : ''}
61+
${
62+
work.highlights
63+
? `\\\\ \\textbf{Highlighted:}
64+
\\begin{itemize}
65+
${work.highlights
66+
?.map(highlight => {
67+
return `\\item[-] ${highlight}`;
68+
})
69+
.join('\n')}
70+
\\end{itemize}`
71+
: ''
72+
}
73+
74+
`;
75+
})
76+
.join('\n')}
77+
\\end{itemize}
78+
\\
79+
\\
80+
81+
\\section{TECHNICAL SKILLS}
82+
\\begin{itemize}
83+
${Object.entries(flattenSkills)
84+
.map(
85+
([skill, keywords]) =>
86+
` \\raggedright
87+
\\item[-] ${skill}: ${escapes(keywords.join(', '))}`,
88+
)
89+
.join('\n')}
90+
\\end{itemize}
91+
\\
92+
\\
93+
\\section{EDUCATION}
94+
\\begin{itemize}
95+
${education.map(edu => ` \\item[-] ${edu.studyType} in ${edu.area}, ${edu.institution} \\hfill ${edu.startDate} - ${edu.endDate}`).join('\n')}
96+
\\end{itemize}
97+
98+
99+
100+
\\end{document}`;
101+
}
102+
103+
(async () => {
104+
const os = await import('node:os');
105+
const fs = await import('node:fs/promises');
106+
const path = await import('node:path');
107+
const useTailoredResume = process.env['VITE_IS_TAILORED_RESUME']
108+
? true
109+
: false;
110+
const resumeSource =
111+
process.env['VITE_RESUME_SOURCE'] ??
112+
'https://neviaumi.github.io/portfolio/resume.json';
113+
if (useTailoredResume) {
114+
return fs
115+
.readFile(path.join(os.tmpdir(), resumeSource), {
116+
encoding: 'utf8',
117+
})
118+
.then(JSON.parse);
119+
}
120+
return fetch(resumeSource).then(res => res.json());
121+
})()
122+
.then(resumeToLatex)
123+
.then(async latexText => {
124+
const workspace = await import('./workspace.js');
125+
const fs = await import('node:fs/promises');
126+
await fs.writeFile(`${workspace.PUBLIC_FOLDER}/resume.tex`, latexText);
127+
return latexText;
128+
});

systems/resume/scripts/docker/build.sh

+5-2
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,8 @@
33
set -ex
44

55
node ./resume-to-pdf.js
6-
7-
(cd public && pdflatex -jobname=ast-resume resume.tex)
6+
node ./resume-to-latex.js
7+
(cd public && pdflatex -jobname=ats-resume resume.tex)
8+
cp public/resume.pdf ./dist
9+
cp public/resume.tex ./dist
10+
cp public/ats-resume.pdf ./dist

0 commit comments

Comments
 (0)