18c556bd54
- single-column, real-text PDFs: standard headings (Summary, Skills, Experience, Projects, Education), consistent dates, no letter-spacing - 'Systems Developer, eight years experience' (drops 'mid-level'); apprentice->developer progression shown; Projects section added; Norwegian written natively (errors fixed) - one page each, ~8 kB (was 132/172 kB); verified clean ATS text extraction + reading order - generator committed to tools/cv (content.js + pdfkit/docx builders) for reproducibility - editable .docx kept alongside; site file-size labels updated Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
108 lines
3.5 KiB
JavaScript
108 lines
3.5 KiB
JavaScript
const fs = require('fs');
|
|
const PDFDocument = require('pdfkit');
|
|
|
|
// Shared content (mirrors build-cv.js).
|
|
const { EN, NO } = require('./content.js');
|
|
|
|
const PAGE_W = 595.28;
|
|
const MARGIN = 54;
|
|
const CW = PAGE_W - MARGIN * 2; // content width
|
|
const INK = '#1a1a1a';
|
|
const GRAY = '#555555';
|
|
const RULE = '#b0b0b0';
|
|
|
|
function build(c, outfile) {
|
|
const doc = new PDFDocument({ size: 'A4', margins: { top: MARGIN, bottom: MARGIN, left: MARGIN, right: MARGIN } });
|
|
doc.pipe(fs.createWriteStream(outfile));
|
|
|
|
const rule = (gap = 4) => {
|
|
const y = doc.y + gap;
|
|
doc.moveTo(MARGIN, y).lineTo(MARGIN + CW, y).lineWidth(0.75).strokeColor(RULE).stroke();
|
|
doc.y = y + gap + 2;
|
|
};
|
|
|
|
const heading = (text) => {
|
|
doc.moveDown(0.5);
|
|
doc.fillColor(INK).font('Helvetica-Bold').fontSize(12).text(text, MARGIN, doc.y);
|
|
rule(2);
|
|
};
|
|
|
|
const bullet = (text, prefix) => {
|
|
const y = doc.y;
|
|
doc.fillColor(INK).font('Helvetica').fontSize(10).text('•', MARGIN + 2, y, { width: 10, lineBreak: false });
|
|
const tx = MARGIN + 16;
|
|
const tw = CW - 16;
|
|
if (prefix) {
|
|
doc.font('Helvetica-Bold').text(prefix, tx, y, { width: tw, continued: true });
|
|
doc.font('Helvetica').text(text, { width: tw });
|
|
} else {
|
|
doc.font('Helvetica').text(text, tx, y, { width: tw });
|
|
}
|
|
doc.moveDown(0.15);
|
|
};
|
|
|
|
const roleLine = (left, dates) => {
|
|
doc.moveDown(0.3);
|
|
const y = doc.y;
|
|
doc.font('Helvetica').fontSize(9.5).fillColor(GRAY).text(dates, MARGIN, y, { width: CW, align: 'right', lineBreak: false });
|
|
doc.font('Helvetica-Bold').fontSize(10.5).fillColor(INK).text(left, MARGIN, y, { width: CW - 90, lineBreak: false });
|
|
doc.y = y + 14;
|
|
};
|
|
|
|
// Header
|
|
doc.fillColor(INK).font('Helvetica-Bold').fontSize(20).text(c.name, MARGIN, MARGIN);
|
|
doc.font('Helvetica').fontSize(12).fillColor('#333').text(c.title, { paragraphGap: 2 });
|
|
doc.font('Helvetica').fontSize(9.5).fillColor(GRAY)
|
|
.text(`${c.location} · ${c.email} · ${c.phone}`);
|
|
doc.text('cesnimda.co.uk · cesnimda.co.uk/Linkedin');
|
|
rule(4);
|
|
|
|
// Summary
|
|
heading(c.t.summary);
|
|
doc.font('Helvetica').fontSize(10).fillColor(INK).text(c.summary, MARGIN, doc.y, { width: CW, align: 'justify' });
|
|
|
|
// Skills
|
|
heading(c.t.skills);
|
|
for (const s of c.skills) {
|
|
const y = doc.y;
|
|
doc.font('Helvetica-Bold').fontSize(10).fillColor(INK).text(`${s.group}: `, MARGIN, y, { width: CW, continued: true });
|
|
doc.font('Helvetica').text(s.items, { width: CW });
|
|
doc.moveDown(0.15);
|
|
}
|
|
|
|
// Experience
|
|
heading(c.t.experience);
|
|
for (const e of c.experience) {
|
|
roleLine(`${e.role} — ${e.org}`, e.dates);
|
|
if (e.note) {
|
|
doc.font('Helvetica-Oblique').fontSize(9.5).fillColor(GRAY).text(e.note, MARGIN, doc.y, { width: CW });
|
|
doc.moveDown(0.1);
|
|
}
|
|
for (const b of e.bullets) bullet(b);
|
|
}
|
|
|
|
// Earlier roles
|
|
doc.moveDown(0.4);
|
|
doc.font('Helvetica-Bold').fontSize(10).fillColor(INK).text(c.t.earlier, MARGIN, doc.y, { width: CW });
|
|
doc.moveDown(0.1);
|
|
for (const r of c.earlier) bullet(r);
|
|
|
|
// Projects
|
|
heading(c.t.projects);
|
|
for (const p of c.projects) bullet(p.desc, `${p.name} — `);
|
|
|
|
// Education
|
|
heading(c.t.education);
|
|
roleLine(c.education.qual, c.education.dates);
|
|
doc.font('Helvetica').fontSize(10).fillColor(INK).text(c.education.org, MARGIN, doc.y, { width: CW });
|
|
|
|
doc.end();
|
|
return new Promise((res) => doc.on('end', res));
|
|
}
|
|
|
|
(async () => {
|
|
await build(EN, 'connor-babbington-cv-en.pdf');
|
|
await build(NO, 'connor-babbington-cv-no.pdf');
|
|
console.log('wrote EN + NO pdf');
|
|
})();
|