jsonresume-theme-academic 1.2.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +15 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +15 -17
- package/dist/index.js.map +1 -1
- package/dist/index.umd.cjs +15 -17
- package/dist/index.umd.cjs.map +1 -1
- package/examples/example.pdf +0 -0
- package/examples/preview.html +17 -12
- package/examples/resume.json +279 -31
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -53,7 +53,8 @@ function esc(text) {
|
|
|
53
53
|
return String(text).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
54
54
|
}
|
|
55
55
|
function formatDate(dateStr) {
|
|
56
|
-
if (!dateStr) return "";
|
|
56
|
+
if (!dateStr) return "Present";
|
|
57
|
+
if (dateStr.toLowerCase() === "present") return "Present";
|
|
57
58
|
const parts = String(dateStr).split("-");
|
|
58
59
|
if (parts.length === 1) return parts[0];
|
|
59
60
|
const year = parts[0];
|
|
@@ -62,8 +63,8 @@ function formatDate(dateStr) {
|
|
|
62
63
|
}
|
|
63
64
|
function dateRange(start, end) {
|
|
64
65
|
const s = formatDate(start);
|
|
65
|
-
const e =
|
|
66
|
-
if (!s) return "";
|
|
66
|
+
const e = formatDate(end);
|
|
67
|
+
if (!s || s === "Present") return "";
|
|
67
68
|
if (!e) return s;
|
|
68
69
|
return `${s} to ${e}`;
|
|
69
70
|
}
|
|
@@ -198,21 +199,21 @@ function isStructuredMeta(text) {
|
|
|
198
199
|
return /Tech-stack:\s/i.test(text) || /Client:\s/i.test(text);
|
|
199
200
|
}
|
|
200
201
|
function parseWorkMeta(summary) {
|
|
201
|
-
if (!summary) return { techStack: "", client: "" };
|
|
202
|
+
if (!summary) return { techStack: "", client: "", narrative: "" };
|
|
202
203
|
const text = stripHtml(summary).replace(/\n/g, " ").trim();
|
|
203
|
-
if (!isStructuredMeta(text)) return { techStack: "", client: "" };
|
|
204
|
-
|
|
204
|
+
if (!isStructuredMeta(text)) return { techStack: "", client: "", narrative: text };
|
|
205
|
+
let remaining = text;
|
|
206
|
+
const clientMatch = remaining.match(/Client:\s*(.+)$/i);
|
|
205
207
|
const client = clientMatch ? clientMatch[1].trim() : "";
|
|
206
|
-
|
|
207
|
-
const techMatch =
|
|
208
|
+
if (clientMatch) remaining = remaining.slice(0, clientMatch.index).trim();
|
|
209
|
+
const techMatch = remaining.match(/Tech-stack:\s*(.+)/i);
|
|
208
210
|
const techStack = techMatch ? techMatch[1].trim() : "";
|
|
209
|
-
|
|
211
|
+
if (techMatch) remaining = remaining.slice(0, techMatch.index).trim();
|
|
212
|
+
return { techStack, client, narrative: remaining };
|
|
210
213
|
}
|
|
211
214
|
function renderWorkEntry(entry) {
|
|
212
|
-
const { techStack, client } = parseWorkMeta(entry.summary);
|
|
215
|
+
const { techStack, client, narrative } = parseWorkMeta(entry.summary);
|
|
213
216
|
const duration = dateRange(entry.startDate, entry.endDate);
|
|
214
|
-
const plainSummary = entry.summary ? stripHtml(entry.summary).replace(/\n/g, " ").trim() : "";
|
|
215
|
-
const isNarrative = plainSummary && !isStructuredMeta(plainSummary);
|
|
216
217
|
return `
|
|
217
218
|
<div class="work-entry">
|
|
218
219
|
<div class="work-header">
|
|
@@ -224,7 +225,7 @@ function renderWorkEntry(entry) {
|
|
|
224
225
|
<div class="work-tech">${techStack ? `Tech-stack: ${esc(techStack)}` : ""}</div>
|
|
225
226
|
<div class="work-client">${client ? `Client: ${esc(client)}` : ""}</div>
|
|
226
227
|
</div>` : ""}
|
|
227
|
-
${
|
|
228
|
+
${narrative ? `<p class="work-summary">${richText(narrative, { block: false })}</p>` : ""}
|
|
228
229
|
${has(entry.highlights) ? `
|
|
229
230
|
<ul class="work-highlights">
|
|
230
231
|
${entry.highlights.map((h) => `<li>${richText(h)}</li>`).join("\n ")}
|
|
@@ -273,10 +274,7 @@ function renderEducationEntry(entry) {
|
|
|
273
274
|
<div class="edu-year">${esc(yearDisplay)}</div>
|
|
274
275
|
</div>
|
|
275
276
|
${entry.institution ? `<div class="edu-institution">${esc(instLine)}</div>` : ""}
|
|
276
|
-
${has(entry.courses) ?
|
|
277
|
-
<div class="edu-courses">
|
|
278
|
-
${entry.courses.map((c) => `<div>${richText(c)}</div>`).join("")}
|
|
279
|
-
</div>` : ""}
|
|
277
|
+
${has(entry.courses) ? `<div class="edu-courses">Coursework: ${entry.courses.map((c) => richText(c)).join(", ")}</div>` : ""}
|
|
280
278
|
</div>`;
|
|
281
279
|
}
|
|
282
280
|
function renderEducation(education, heading) {
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","sources":["../src/constants.ts","../src/utils/escape.ts","../src/utils/dates.ts","../src/utils/icons.ts","../src/utils/sanitize.ts","../src/utils/text.ts","../src/sections/header.ts","../src/sections/shared.ts","../src/sections/summary.ts","../src/sections/skills.ts","../src/sections/work.ts","../src/sections/projects.ts","../src/sections/education.ts","../src/sections/volunteer.ts","../src/sections/certificates.ts","../src/sections/additional.ts","../src/render.ts","../src/index.ts"],"sourcesContent":["export const MONTHS: readonly string[] = [\n 'January',\n 'February',\n 'March',\n 'April',\n 'May',\n 'June',\n 'July',\n 'August',\n 'September',\n 'October',\n 'November',\n 'December',\n];\n\nexport const COUNTRY_NAMES: Readonly<Record<string, string>> = {\n GB: 'United Kingdom',\n US: 'United States',\n CA: 'Canada',\n AU: 'Australia',\n DE: 'Germany',\n FR: 'France',\n IN: 'India',\n JP: 'Japan',\n CN: 'China',\n BR: 'Brazil',\n NL: 'Netherlands',\n IT: 'Italy',\n ES: 'Spain',\n SE: 'Sweden',\n CH: 'Switzerland',\n SG: 'Singapore',\n NZ: 'New Zealand',\n IE: 'Ireland',\n AE: 'UAE',\n};\n\nexport const DEFAULT_HEADINGS: Readonly<Record<string, string>> = {\n summary: 'Summary',\n skills: 'Core Skills',\n experience: 'Experience',\n projects: 'Selected Projects',\n education: 'Education',\n volunteer: 'Leadership & Volunteering',\n certifications: 'Credentials',\n additional: 'Additional',\n};\n","/** Decode common HTML entities to literal characters. */\nexport function decodeEntities(text: string): string {\n return text\n .replace(/&#(\\d+);/g, (_, c) => String.fromCharCode(Number(c)))\n .replace(/&#x([0-9a-fA-F]+);/g, (_, h) => String.fromCharCode(parseInt(h, 16)))\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/"/g, '\"')\n .replace(/'/g, \"'\")\n .replace(/'/g, \"'\")\n .replace(/ /g, ' ')\n .replace(/—/g, '\\u2014')\n .replace(/–/g, '\\u2013')\n .replace(/…/g, '\\u2026');\n}\n\n/** HTML-escape for safe output (plain text fields like names, dates). */\nexport function esc(text: string | null | undefined): string {\n if (text == null) return '';\n return String(text)\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n","import { MONTHS, COUNTRY_NAMES } from '../constants.js';\n\nexport function formatDate(dateStr: string | null | undefined): string {\n if (!dateStr) return '';\n const parts = String(dateStr).split('-');\n if (parts.length === 1) return parts[0];\n const year = parts[0];\n const month = MONTHS[parseInt(parts[1], 10) - 1] || '';\n return `${month} ${year}`;\n}\n\nexport function dateRange(start: string | null | undefined, end: string | null | undefined): string {\n const s = formatDate(start);\n const e = end ? formatDate(end) : '';\n if (!s) return '';\n if (!e) return s;\n return `${s} to ${e}`;\n}\n\nexport function regionName(code: string | null | undefined): string {\n if (!code) return '';\n return COUNTRY_NAMES[code.toUpperCase()] || code;\n}\n","export function profileIcon(network: string | null | undefined): string {\n if (!network) return '<i class=\"fa-solid fa-link\"></i>';\n const n = network.toLowerCase();\n if (n === 'linkedin') return '<i class=\"fa-brands fa-linkedin-in\"></i>';\n if (n === 'github') return '<i class=\"fa-brands fa-github\"></i>';\n if (n === 'twitter' || n === 'x') return '<i class=\"fa-brands fa-x-twitter\"></i>';\n if (n === 'stackoverflow') return '<i class=\"fa-brands fa-stack-overflow\"></i>';\n return '<i class=\"fa-solid fa-link\"></i>';\n}\n","/** Tags allowed through sanitization — safe inline/block formatting. */\nconst BLOCK_TAGS = new Set(['p', 'ul', 'ol', 'li', 'blockquote']);\nconst INLINE_TAGS = new Set([\n 'br', 'b', 'strong', 'i', 'em', 'u', 's', 'mark', 'a', 'sub', 'sup', 'code',\n]);\nconst ALLOWED_TAGS = new Set([...BLOCK_TAGS, ...INLINE_TAGS]);\n\n/** URL schemes safe for <a href>. Everything else (javascript:, data:, vbscript:) is stripped. */\nconst SAFE_URL_SCHEMES = /^(?:https?|mailto|tel):/i;\nconst RELATIVE_URL = /^[#/?.]/;\n\nfunction isSafeUrl(href: string): boolean {\n const trimmed = href.trim();\n if (!trimmed) return false;\n return SAFE_URL_SCHEMES.test(trimmed) || RELATIVE_URL.test(trimmed);\n}\n\ninterface SanitizeOptions {\n readonly inline?: boolean;\n}\n\n/**\n * Sanitize HTML — keep allowed formatting tags, strip everything else.\n * Attributes are stripped from all tags except <a href> (safe schemes only) and <mark class>.\n * If inline=true, block tags (p, ul, ol, li, blockquote) are also stripped.\n */\nexport function sanitizeHtml(html: string | null | undefined, { inline = false }: SanitizeOptions = {}): string {\n if (!html) return '';\n const allowed = inline ? INLINE_TAGS : ALLOWED_TAGS;\n\n return String(html)\n .replace(/<a\\s+[^>]*href=\"([^\"]*)\"[^>]*>/gi, (_, href: string) => {\n if (!isSafeUrl(href)) return '';\n return `<a href=\"${href.replace(/\"/g, '"')}\" target=\"_blank\" rel=\"noopener\">`;\n })\n .replace(/<mark\\s+[^>]*class=\"([^\"]*)\"[^>]*>/gi, (_, cls: string) =>\n `<mark class=\"${cls.replace(/\"/g, '"')}\">`,\n )\n .replace(/<(\\/?)([a-z][a-z0-9]*)\\s*[^>]*?(\\/?)>/gi, (match, slash: string, tag: string, selfClose: string) => {\n const lower = tag.toLowerCase();\n if (lower === 'a' && !slash && match.includes('href=')) return match;\n if (lower === 'mark' && !slash && match.includes('class=')) return match;\n if (allowed.has(lower)) return `<${slash}${lower}${selfClose}>`;\n if (inline && BLOCK_TAGS.has(lower)) return slash ? ' ' : '';\n return '';\n })\n .replace(/<p>\\s*<\\/p>/gi, '')\n .replace(/\\s{2,}/g, ' ')\n .trim();\n}\n","import { decodeEntities, esc } from './escape.js';\nimport { sanitizeHtml } from './sanitize.js';\n\n/** Strip HTML tags → plain text (for fields parsed as data, not displayed). */\nexport function stripHtml(text: string | null | undefined): string {\n if (!text) return '';\n return decodeEntities(\n String(text)\n .replace(/<br\\s*\\/?>/gi, ' ')\n .replace(/<\\/?(p|div|li)[^>]*>/gi, ' ')\n .replace(/<[^>]+>/g, ''),\n )\n .replace(/\\s{2,}/g, ' ')\n .trim();\n}\n\nconst isHtml = (text: string): boolean => /<[a-z][\\s\\S]*>/i.test(text);\nconst hasEntities = (text: string): boolean => /&(?:amp|lt|gt|quot|#\\d+|#x[0-9a-f]+);/i.test(text);\n\n/**\n * Render user-provided rich text → safe HTML output.\n * block=true: preserve <p>, <ul>, <ol> etc.\n * block=false (default): inline mode — strip block tags, keep only inline formatting.\n */\nexport function richText(text: string | null | undefined, { block = false }: { block?: boolean } = {}): string {\n if (text == null) return '';\n const s = String(text);\n if (isHtml(s)) return sanitizeHtml(s, { inline: !block });\n if (hasEntities(s)) return esc(decodeEntities(s));\n return esc(s);\n}\n\n/** Check if an array is non-empty. */\nexport function has<T>(arr: readonly T[] | null | undefined): arr is readonly T[] & { length: number } {\n return Array.isArray(arr) && arr.length > 0;\n}\n","import type { ResumeBasics } from '../types/resume.js';\nimport { esc } from '../utils/escape.js';\nimport { regionName } from '../utils/dates.js';\nimport { profileIcon } from '../utils/icons.js';\nimport { has } from '../utils/text.js';\n\ninterface ContactLine {\n readonly icon: string;\n readonly text: string;\n}\n\nexport function renderHeader(basics: ResumeBasics | undefined): string {\n if (!basics) return '';\n\n const location = basics.location\n ? [basics.location.city, regionName(basics.location.region)].filter(Boolean).join(', ')\n : '';\n\n const lines: ContactLine[] = [];\n if (location)\n lines.push({ icon: '<i class=\"fa-solid fa-location-dot\"></i>', text: location });\n if (basics.phone)\n lines.push({ icon: '<i class=\"fa-solid fa-square-phone\"></i>', text: basics.phone });\n if (basics.email)\n lines.push({ icon: '<i class=\"fa-solid fa-envelope\"></i>', text: basics.email });\n if (basics.url) {\n const display = basics.url.replace(/^https?:\\/\\//, '');\n lines.push({ icon: '<i class=\"fa-solid fa-globe\"></i>', text: display });\n }\n if (has(basics.profiles)) {\n for (const p of basics.profiles) {\n lines.push({ icon: profileIcon(p.network), text: p.username || p.url || '' });\n }\n }\n\n return `\n <header class=\"header\">\n <div class=\"header-left\">\n <h1 class=\"name\">${esc(basics.name)}</h1>\n <div class=\"label\">${esc(basics.label)}</div>\n </div>\n <div class=\"contact-info\">\n ${lines.map((l) => `<div class=\"contact-line\">${esc(l.text)} ${l.icon}</div>`).join('\\n ')}\n </div>\n </header>\n <hr class=\"header-rule\" />`;\n}\n","import { esc } from '../utils/escape.js';\n\nexport function sectionTitle(title: string): string {\n return `<h2 class=\"section-title\">${esc(title)}</h2>`;\n}\n","import { richText } from '../utils/text.js';\nimport { sectionTitle } from './shared.js';\n\nexport function renderSummary(summary: string | undefined, heading: string): string {\n if (!summary) return '';\n const html = richText(summary, { block: true });\n if (!html) return '';\n return `\n ${sectionTitle(heading)}\n <div class=\"summary\">${html}</div>`;\n}\n","import type { ResumeSkill } from '../types/resume.js';\nimport { esc } from '../utils/escape.js';\nimport { has } from '../utils/text.js';\nimport { sectionTitle } from './shared.js';\n\nexport function renderSkills(skills: readonly ResumeSkill[] | undefined, heading: string): string {\n if (!has(skills)) return '';\n return `\n ${sectionTitle(heading)}\n <div class=\"section-body\">\n ${skills\n .map(\n (s) => `\n <div class=\"bullet-item\">\n <span class=\"skill-name\">${esc(s.name)}:</span> ${esc((s.keywords || []).join(', '))}\n </div>`,\n )\n .join('')}\n </div>`;\n}\n","import type { ResumeWorkEntry } from '../types/resume.js';\nimport { esc } from '../utils/escape.js';\nimport { dateRange } from '../utils/dates.js';\nimport { has, richText, stripHtml } from '../utils/text.js';\nimport { sectionTitle } from './shared.js';\n\n/** Check whether summary uses structured Tech-stack / Client format. */\nfunction isStructuredMeta(text: string): boolean {\n return /Tech-stack:\\s/i.test(text) || /Client:\\s/i.test(text);\n}\n\n/** Extract tech-stack and client from a structured work summary string. */\nfunction parseWorkMeta(summary: string | undefined): { techStack: string; client: string } {\n if (!summary) return { techStack: '', client: '' };\n const text = stripHtml(summary).replace(/\\n/g, ' ').trim();\n if (!isStructuredMeta(text)) return { techStack: '', client: '' };\n\n const clientMatch = text.match(/Client:\\s*(.+)$/i);\n const client = clientMatch ? clientMatch[1].trim() : '';\n\n const techPart = clientMatch ? text.slice(0, clientMatch.index).trim() : text;\n const techMatch = techPart.match(/Tech-stack:\\s*(.+)/i);\n const techStack = techMatch ? techMatch[1].trim() : '';\n\n return { techStack, client };\n}\n\nfunction renderWorkEntry(entry: ResumeWorkEntry): string {\n const { techStack, client } = parseWorkMeta(entry.summary);\n const duration = dateRange(entry.startDate, entry.endDate);\n const plainSummary = entry.summary ? stripHtml(entry.summary).replace(/\\n/g, ' ').trim() : '';\n const isNarrative = plainSummary && !isStructuredMeta(plainSummary);\n\n return `\n <div class=\"work-entry\">\n <div class=\"work-header\">\n <div class=\"work-title\">${esc(entry.name)} - ${esc(entry.position)}</div>\n <div class=\"work-duration\">${duration ? `Duration: ${esc(duration)}` : ''}</div>\n </div>\n ${\n techStack || client\n ? `\n <div class=\"work-meta\">\n <div class=\"work-tech\">${techStack ? `Tech-stack: ${esc(techStack)}` : ''}</div>\n <div class=\"work-client\">${client ? `Client: ${esc(client)}` : ''}</div>\n </div>`\n : ''\n }\n ${isNarrative ? `<p class=\"work-summary\">${richText(entry.summary, { block: false })}</p>` : ''}\n ${\n has(entry.highlights)\n ? `\n <ul class=\"work-highlights\">\n ${entry.highlights.map((h) => `<li>${richText(h)}</li>`).join('\\n ')}\n </ul>`\n : ''\n }\n </div>`;\n}\n\nexport function renderWork(work: readonly ResumeWorkEntry[] | undefined, heading: string): string {\n if (!has(work)) return '';\n return `\n ${sectionTitle(heading)}\n <div class=\"section-body\">\n ${work.map(renderWorkEntry).join('')}\n </div>`;\n}\n","import type { ResumeProject } from '../types/resume.js';\nimport { esc } from '../utils/escape.js';\nimport { has, richText } from '../utils/text.js';\nimport { sectionTitle } from './shared.js';\n\nexport function renderProjects(\n projects: readonly ResumeProject[] | undefined,\n heading: string,\n): string {\n if (!has(projects)) return '';\n return `\n ${sectionTitle(heading)}\n <div class=\"section-body\">\n ${projects\n .map((p) => {\n const desc = richText(p.description);\n return `\n <div class=\"bullet-item\">\n <span class=\"project-name\">${esc(p.name)}:</span> ${desc}\n </div>`;\n })\n .join('')}\n </div>`;\n}\n","import type { ResumeEducationEntry } from '../types/resume.js';\nimport { esc } from '../utils/escape.js';\nimport { formatDate } from '../utils/dates.js';\nimport { has, richText } from '../utils/text.js';\nimport { sectionTitle } from './shared.js';\n\nfunction renderEducationEntry(entry: ResumeEducationEntry): string {\n const degree = [entry.studyType, entry.area].filter(Boolean).join(' ');\n\n let yearDisplay = '';\n if (entry.endDate) {\n yearDisplay = formatDate(entry.endDate);\n } else if (entry.startDate) {\n const year = parseInt(String(entry.startDate).split('-')[0], 10);\n const currentYear = new Date().getFullYear();\n yearDisplay = year >= currentYear ? `Expected ${formatDate(entry.startDate)}` : formatDate(entry.startDate);\n }\n\n const instParts = [entry.institution];\n if (entry.score) instParts.push(entry.score);\n const instLine = instParts.filter(Boolean).join(' | ');\n\n return `\n <div class=\"edu-entry\">\n <div class=\"edu-header\">\n <div class=\"edu-degree\">${esc(degree)}</div>\n <div class=\"edu-year\">${esc(yearDisplay)}</div>\n </div>\n ${entry.institution ? `<div class=\"edu-institution\">${esc(instLine)}</div>` : ''}\n ${\n has(entry.courses)\n ? `\n <div class=\"edu-courses\">\n ${entry.courses.map((c) => `<div>${richText(c)}</div>`).join('')}\n </div>`\n : ''\n }\n </div>`;\n}\n\nexport function renderEducation(\n education: readonly ResumeEducationEntry[] | undefined,\n heading: string,\n): string {\n if (!has(education)) return '';\n return `\n ${sectionTitle(heading)}\n <div class=\"section-body\">\n ${education.map(renderEducationEntry).join('')}\n </div>`;\n}\n","import type { ResumeVolunteerEntry } from '../types/resume.js';\nimport { esc } from '../utils/escape.js';\nimport { has, richText } from '../utils/text.js';\nimport { sectionTitle } from './shared.js';\n\nexport function renderVolunteer(\n volunteer: readonly ResumeVolunteerEntry[] | undefined,\n heading: string,\n): string {\n if (!has(volunteer)) return '';\n return `\n ${sectionTitle(heading)}\n <div class=\"section-body\">\n ${volunteer\n .map((v) => {\n const years = v.endDate\n ? `${v.startDate}\\u2013${v.endDate}`\n : v.startDate || '';\n const title = v.position\n ? `${v.position} \\u2013 ${v.organization}`\n : v.organization;\n const summary = richText(v.summary);\n\n return `\n <div class=\"bullet-item volunteer-item\">\n <span class=\"vol-title\">${esc(title)}${years ? ` (${esc(years)})` : ''}:</span> ${summary}\n </div>`;\n })\n .join('')}\n </div>`;\n}\n","import type { ResumeCertificate } from '../types/resume.js';\nimport { has, richText } from '../utils/text.js';\nimport { sectionTitle } from './shared.js';\n\nexport function renderCertificates(\n certificates: readonly ResumeCertificate[] | undefined,\n heading: string,\n): string {\n if (!has(certificates)) return '';\n return `\n ${sectionTitle(heading)}\n <div class=\"section-body\">\n ${certificates\n .map(\n (c) => `\n <div class=\"bullet-item\">${richText(c.name)}</div>`,\n )\n .join('')}\n </div>`;\n}\n","import type { ResumeSchema } from '../types/resume.js';\nimport { esc } from '../utils/escape.js';\nimport { has } from '../utils/text.js';\nimport { sectionTitle } from './shared.js';\n\nexport function renderAdditional(resume: ResumeSchema, heading: string): string {\n const parts: string[] = [];\n\n if (has(resume.interests)) {\n for (const interest of resume.interests) {\n const keywords = has(interest.keywords) ? interest.keywords.join(', ') : '';\n const label = interest.name || 'Interests';\n if (keywords) {\n parts.push(\n `<div class=\"additional-line\"><span class=\"additional-label\">${esc(label)}:</span> ${esc(keywords)}</div>`,\n );\n }\n }\n }\n\n if (has(resume.languages)) {\n const langStr = resume.languages\n .map((l) => `${l.language}${l.fluency ? ` (${l.fluency})` : ''}`)\n .join(', ');\n parts.push(\n `<div class=\"additional-line\"><span class=\"additional-label\">Languages:</span> ${esc(langStr)}</div>`,\n );\n }\n\n if (has(resume.references)) {\n parts.push(\n `<div class=\"additional-line\"><span class=\"additional-label\">Referees:</span> Available on request</div>`,\n );\n }\n\n if (parts.length === 0) return '';\n\n return `\n ${sectionTitle(heading)}\n <div class=\"section-body\">\n ${parts.join('\\n ')}\n </div>`;\n}\n","import type { ResumeSchema } from './types/resume.js';\nimport { DEFAULT_HEADINGS } from './constants.js';\nimport { esc } from './utils/escape.js';\nimport { renderHeader } from './sections/header.js';\nimport { renderSummary } from './sections/summary.js';\nimport { renderSkills } from './sections/skills.js';\nimport { renderWork } from './sections/work.js';\nimport { renderProjects } from './sections/projects.js';\nimport { renderEducation } from './sections/education.js';\nimport { renderVolunteer } from './sections/volunteer.js';\nimport { renderCertificates } from './sections/certificates.js';\nimport { renderAdditional } from './sections/additional.js';\nimport css from './styles/academic.css?inline';\n\nexport function render(resume: ResumeSchema): string {\n const h = { ...DEFAULT_HEADINGS };\n\n const metaHeadings = resume?.meta?.headings;\n if (metaHeadings) {\n for (const [key, value] of Object.entries(metaHeadings)) {\n if (value && key in h) {\n h[key] = value;\n }\n }\n }\n\n const basics = resume?.basics || {};\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n <title>${esc(basics.name || 'Resume')}</title>\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\" />\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin />\n <link href=\"https://fonts.googleapis.com/css2?family=EB+Garamond:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500&display=swap\" rel=\"stylesheet\" />\n <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css\" />\n <style>${css}</style>\n</head>\n<body>\n <div class=\"resume\">\n ${renderHeader(basics)}\n ${renderSummary(basics.summary, h.summary)}\n ${renderSkills(resume.skills, h.skills)}\n ${renderWork(resume.work, h.experience)}\n ${renderProjects(resume.projects, h.projects)}\n ${renderEducation(resume.education, h.education)}\n ${renderVolunteer(resume.volunteer, h.volunteer)}\n ${renderCertificates(resume.certificates, h.certifications)}\n ${renderAdditional(resume, h.additional)}\n </div>\n</body>\n</html>`;\n}\n","export { render } from './render.js';\nexport type { ResumeSchema } from './types/resume.js';\n\n/** PDF rendering options for Puppeteer/Gotenberg. */\nexport const pdfRenderOptions = {\n mediaType: 'print' as const,\n format: 'A4' as const,\n margin: {\n top: '12mm',\n right: '14mm',\n bottom: '12mm',\n left: '14mm',\n },\n};\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":["../src/constants.ts","../src/utils/escape.ts","../src/utils/dates.ts","../src/utils/icons.ts","../src/utils/sanitize.ts","../src/utils/text.ts","../src/sections/header.ts","../src/sections/shared.ts","../src/sections/summary.ts","../src/sections/skills.ts","../src/sections/work.ts","../src/sections/projects.ts","../src/sections/education.ts","../src/sections/volunteer.ts","../src/sections/certificates.ts","../src/sections/additional.ts","../src/render.ts","../src/index.ts"],"sourcesContent":["export const MONTHS: readonly string[] = [\n 'January',\n 'February',\n 'March',\n 'April',\n 'May',\n 'June',\n 'July',\n 'August',\n 'September',\n 'October',\n 'November',\n 'December',\n];\n\nexport const COUNTRY_NAMES: Readonly<Record<string, string>> = {\n GB: 'United Kingdom',\n US: 'United States',\n CA: 'Canada',\n AU: 'Australia',\n DE: 'Germany',\n FR: 'France',\n IN: 'India',\n JP: 'Japan',\n CN: 'China',\n BR: 'Brazil',\n NL: 'Netherlands',\n IT: 'Italy',\n ES: 'Spain',\n SE: 'Sweden',\n CH: 'Switzerland',\n SG: 'Singapore',\n NZ: 'New Zealand',\n IE: 'Ireland',\n AE: 'UAE',\n};\n\nexport const DEFAULT_HEADINGS: Readonly<Record<string, string>> = {\n summary: 'Summary',\n skills: 'Core Skills',\n experience: 'Experience',\n projects: 'Selected Projects',\n education: 'Education',\n volunteer: 'Leadership & Volunteering',\n certifications: 'Credentials',\n additional: 'Additional',\n};\n","/** Decode common HTML entities to literal characters. */\nexport function decodeEntities(text: string): string {\n return text\n .replace(/&#(\\d+);/g, (_, c) => String.fromCharCode(Number(c)))\n .replace(/&#x([0-9a-fA-F]+);/g, (_, h) => String.fromCharCode(parseInt(h, 16)))\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/"/g, '\"')\n .replace(/'/g, \"'\")\n .replace(/'/g, \"'\")\n .replace(/ /g, ' ')\n .replace(/—/g, '\\u2014')\n .replace(/–/g, '\\u2013')\n .replace(/…/g, '\\u2026');\n}\n\n/** HTML-escape for safe output (plain text fields like names, dates). */\nexport function esc(text: string | null | undefined): string {\n if (text == null) return '';\n return String(text)\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n","import { MONTHS, COUNTRY_NAMES } from '../constants.js';\n\nexport function formatDate(dateStr: string | null | undefined): string {\n if (!dateStr) return 'Present';\n if (dateStr.toLowerCase() === 'present') return 'Present';\n const parts = String(dateStr).split('-');\n if (parts.length === 1) return parts[0];\n const year = parts[0];\n const month = MONTHS[parseInt(parts[1], 10) - 1] || '';\n return `${month} ${year}`;\n}\n\nexport function dateRange(start: string | null | undefined, end: string | null | undefined): string {\n const s = formatDate(start);\n const e = formatDate(end);\n if (!s || s === 'Present') return ''; // start date should not be empty or present\n if (!e) return s;\n return `${s} to ${e}`;\n}\n\nexport function regionName(code: string | null | undefined): string {\n if (!code) return '';\n return COUNTRY_NAMES[code.toUpperCase()] || code;\n}\n","export function profileIcon(network: string | null | undefined): string {\n if (!network) return '<i class=\"fa-solid fa-link\"></i>';\n const n = network.toLowerCase();\n if (n === 'linkedin') return '<i class=\"fa-brands fa-linkedin-in\"></i>';\n if (n === 'github') return '<i class=\"fa-brands fa-github\"></i>';\n if (n === 'twitter' || n === 'x') return '<i class=\"fa-brands fa-x-twitter\"></i>';\n if (n === 'stackoverflow') return '<i class=\"fa-brands fa-stack-overflow\"></i>';\n return '<i class=\"fa-solid fa-link\"></i>';\n}\n","/** Tags allowed through sanitization — safe inline/block formatting. */\nconst BLOCK_TAGS = new Set(['p', 'ul', 'ol', 'li', 'blockquote']);\nconst INLINE_TAGS = new Set([\n 'br', 'b', 'strong', 'i', 'em', 'u', 's', 'mark', 'a', 'sub', 'sup', 'code',\n]);\nconst ALLOWED_TAGS = new Set([...BLOCK_TAGS, ...INLINE_TAGS]);\n\n/** URL schemes safe for <a href>. Everything else (javascript:, data:, vbscript:) is stripped. */\nconst SAFE_URL_SCHEMES = /^(?:https?|mailto|tel):/i;\nconst RELATIVE_URL = /^[#/?.]/;\n\nfunction isSafeUrl(href: string): boolean {\n const trimmed = href.trim();\n if (!trimmed) return false;\n return SAFE_URL_SCHEMES.test(trimmed) || RELATIVE_URL.test(trimmed);\n}\n\ninterface SanitizeOptions {\n readonly inline?: boolean;\n}\n\n/**\n * Sanitize HTML — keep allowed formatting tags, strip everything else.\n * Attributes are stripped from all tags except <a href> (safe schemes only) and <mark class>.\n * If inline=true, block tags (p, ul, ol, li, blockquote) are also stripped.\n */\nexport function sanitizeHtml(html: string | null | undefined, { inline = false }: SanitizeOptions = {}): string {\n if (!html) return '';\n const allowed = inline ? INLINE_TAGS : ALLOWED_TAGS;\n\n return String(html)\n .replace(/<a\\s+[^>]*href=\"([^\"]*)\"[^>]*>/gi, (_, href: string) => {\n if (!isSafeUrl(href)) return '';\n return `<a href=\"${href.replace(/\"/g, '"')}\" target=\"_blank\" rel=\"noopener\">`;\n })\n .replace(/<mark\\s+[^>]*class=\"([^\"]*)\"[^>]*>/gi, (_, cls: string) =>\n `<mark class=\"${cls.replace(/\"/g, '"')}\">`,\n )\n .replace(/<(\\/?)([a-z][a-z0-9]*)\\s*[^>]*?(\\/?)>/gi, (match, slash: string, tag: string, selfClose: string) => {\n const lower = tag.toLowerCase();\n if (lower === 'a' && !slash && match.includes('href=')) return match;\n if (lower === 'mark' && !slash && match.includes('class=')) return match;\n if (allowed.has(lower)) return `<${slash}${lower}${selfClose}>`;\n if (inline && BLOCK_TAGS.has(lower)) return slash ? ' ' : '';\n return '';\n })\n .replace(/<p>\\s*<\\/p>/gi, '')\n .replace(/\\s{2,}/g, ' ')\n .trim();\n}\n","import { decodeEntities, esc } from './escape.js';\nimport { sanitizeHtml } from './sanitize.js';\n\n/** Strip HTML tags → plain text (for fields parsed as data, not displayed). */\nexport function stripHtml(text: string | null | undefined): string {\n if (!text) return '';\n return decodeEntities(\n String(text)\n .replace(/<br\\s*\\/?>/gi, ' ')\n .replace(/<\\/?(p|div|li)[^>]*>/gi, ' ')\n .replace(/<[^>]+>/g, ''),\n )\n .replace(/\\s{2,}/g, ' ')\n .trim();\n}\n\nconst isHtml = (text: string): boolean => /<[a-z][\\s\\S]*>/i.test(text);\nconst hasEntities = (text: string): boolean => /&(?:amp|lt|gt|quot|#\\d+|#x[0-9a-f]+);/i.test(text);\n\n/**\n * Render user-provided rich text → safe HTML output.\n * block=true: preserve <p>, <ul>, <ol> etc.\n * block=false (default): inline mode — strip block tags, keep only inline formatting.\n */\nexport function richText(text: string | null | undefined, { block = false }: { block?: boolean } = {}): string {\n if (text == null) return '';\n const s = String(text);\n if (isHtml(s)) return sanitizeHtml(s, { inline: !block });\n if (hasEntities(s)) return esc(decodeEntities(s));\n return esc(s);\n}\n\n/** Check if an array is non-empty. */\nexport function has<T>(arr: readonly T[] | null | undefined): arr is readonly T[] & { length: number } {\n return Array.isArray(arr) && arr.length > 0;\n}\n","import type { ResumeBasics } from '../types/resume.js';\nimport { esc } from '../utils/escape.js';\nimport { regionName } from '../utils/dates.js';\nimport { profileIcon } from '../utils/icons.js';\nimport { has } from '../utils/text.js';\n\ninterface ContactLine {\n readonly icon: string;\n readonly text: string;\n}\n\nexport function renderHeader(basics: ResumeBasics | undefined): string {\n if (!basics) return '';\n\n const location = basics.location\n ? [basics.location.city, regionName(basics.location.region)].filter(Boolean).join(', ')\n : '';\n\n const lines: ContactLine[] = [];\n if (location)\n lines.push({ icon: '<i class=\"fa-solid fa-location-dot\"></i>', text: location });\n if (basics.phone)\n lines.push({ icon: '<i class=\"fa-solid fa-square-phone\"></i>', text: basics.phone });\n if (basics.email)\n lines.push({ icon: '<i class=\"fa-solid fa-envelope\"></i>', text: basics.email });\n if (basics.url) {\n const display = basics.url.replace(/^https?:\\/\\//, '');\n lines.push({ icon: '<i class=\"fa-solid fa-globe\"></i>', text: display });\n }\n if (has(basics.profiles)) {\n for (const p of basics.profiles) {\n lines.push({ icon: profileIcon(p.network), text: p.username || p.url || '' });\n }\n }\n\n return `\n <header class=\"header\">\n <div class=\"header-left\">\n <h1 class=\"name\">${esc(basics.name)}</h1>\n <div class=\"label\">${esc(basics.label)}</div>\n </div>\n <div class=\"contact-info\">\n ${lines.map((l) => `<div class=\"contact-line\">${esc(l.text)} ${l.icon}</div>`).join('\\n ')}\n </div>\n </header>\n <hr class=\"header-rule\" />`;\n}\n","import { esc } from '../utils/escape.js';\n\nexport function sectionTitle(title: string): string {\n return `<h2 class=\"section-title\">${esc(title)}</h2>`;\n}\n","import { richText } from '../utils/text.js';\nimport { sectionTitle } from './shared.js';\n\nexport function renderSummary(summary: string | undefined, heading: string): string {\n if (!summary) return '';\n const html = richText(summary, { block: true });\n if (!html) return '';\n return `\n ${sectionTitle(heading)}\n <div class=\"summary\">${html}</div>`;\n}\n","import type { ResumeSkill } from '../types/resume.js';\nimport { esc } from '../utils/escape.js';\nimport { has } from '../utils/text.js';\nimport { sectionTitle } from './shared.js';\n\nexport function renderSkills(skills: readonly ResumeSkill[] | undefined, heading: string): string {\n if (!has(skills)) return '';\n return `\n ${sectionTitle(heading)}\n <div class=\"section-body\">\n ${skills\n .map(\n (s) => `\n <div class=\"bullet-item\">\n <span class=\"skill-name\">${esc(s.name)}:</span> ${esc((s.keywords || []).join(', '))}\n </div>`,\n )\n .join('')}\n </div>`;\n}\n","import type { ResumeWorkEntry } from '../types/resume.js';\nimport { esc } from '../utils/escape.js';\nimport { dateRange } from '../utils/dates.js';\nimport { has, richText, stripHtml } from '../utils/text.js';\nimport { sectionTitle } from './shared.js';\n\n/** Check whether summary uses structured Tech-stack / Client format. */\nfunction isStructuredMeta(text: string): boolean {\n return /Tech-stack:\\s/i.test(text) || /Client:\\s/i.test(text);\n}\n\n/** Extract tech-stack, client, and any remaining narrative from a work summary. */\nfunction parseWorkMeta(summary: string | undefined): {\n techStack: string;\n client: string;\n narrative: string;\n} {\n if (!summary) return { techStack: '', client: '', narrative: '' };\n const text = stripHtml(summary).replace(/\\n/g, ' ').trim();\n if (!isStructuredMeta(text)) return { techStack: '', client: '', narrative: text };\n\n let remaining = text;\n\n const clientMatch = remaining.match(/Client:\\s*(.+)$/i);\n const client = clientMatch ? clientMatch[1].trim() : '';\n if (clientMatch) remaining = remaining.slice(0, clientMatch.index).trim();\n\n const techMatch = remaining.match(/Tech-stack:\\s*(.+)/i);\n const techStack = techMatch ? techMatch[1].trim() : '';\n if (techMatch) remaining = remaining.slice(0, techMatch.index).trim();\n\n return { techStack, client, narrative: remaining };\n}\n\nfunction renderWorkEntry(entry: ResumeWorkEntry): string {\n const { techStack, client, narrative } = parseWorkMeta(entry.summary);\n const duration = dateRange(entry.startDate, entry.endDate);\n\n return `\n <div class=\"work-entry\">\n <div class=\"work-header\">\n <div class=\"work-title\">${esc(entry.name)} - ${esc(entry.position)}</div>\n <div class=\"work-duration\">${duration ? `Duration: ${esc(duration)}` : ''}</div>\n </div>\n ${\n techStack || client\n ? `\n <div class=\"work-meta\">\n <div class=\"work-tech\">${techStack ? `Tech-stack: ${esc(techStack)}` : ''}</div>\n <div class=\"work-client\">${client ? `Client: ${esc(client)}` : ''}</div>\n </div>`\n : ''\n }\n ${narrative ? `<p class=\"work-summary\">${richText(narrative, { block: false })}</p>` : ''}\n ${\n has(entry.highlights)\n ? `\n <ul class=\"work-highlights\">\n ${entry.highlights.map((h) => `<li>${richText(h)}</li>`).join('\\n ')}\n </ul>`\n : ''\n }\n </div>`;\n}\n\nexport function renderWork(work: readonly ResumeWorkEntry[] | undefined, heading: string): string {\n if (!has(work)) return '';\n return `\n ${sectionTitle(heading)}\n <div class=\"section-body\">\n ${work.map(renderWorkEntry).join('')}\n </div>`;\n}\n","import type { ResumeProject } from '../types/resume.js';\nimport { esc } from '../utils/escape.js';\nimport { has, richText } from '../utils/text.js';\nimport { sectionTitle } from './shared.js';\n\nexport function renderProjects(\n projects: readonly ResumeProject[] | undefined,\n heading: string,\n): string {\n if (!has(projects)) return '';\n return `\n ${sectionTitle(heading)}\n <div class=\"section-body\">\n ${projects\n .map((p) => {\n const desc = richText(p.description);\n return `\n <div class=\"bullet-item\">\n <span class=\"project-name\">${esc(p.name)}:</span> ${desc}\n </div>`;\n })\n .join('')}\n </div>`;\n}\n","import type { ResumeEducationEntry } from '../types/resume.js';\nimport { esc } from '../utils/escape.js';\nimport { formatDate } from '../utils/dates.js';\nimport { has, richText } from '../utils/text.js';\nimport { sectionTitle } from './shared.js';\n\nfunction renderEducationEntry(entry: ResumeEducationEntry): string {\n const degree = [entry.studyType, entry.area].filter(Boolean).join(' ');\n\n let yearDisplay = '';\n if (entry.endDate) {\n yearDisplay = formatDate(entry.endDate);\n } else if (entry.startDate) {\n const year = parseInt(String(entry.startDate).split('-')[0], 10);\n const currentYear = new Date().getFullYear();\n yearDisplay = year >= currentYear ? `Expected ${formatDate(entry.startDate)}` : formatDate(entry.startDate);\n }\n\n const instParts = [entry.institution];\n if (entry.score) instParts.push(entry.score);\n const instLine = instParts.filter(Boolean).join(' | ');\n\n return `\n <div class=\"edu-entry\">\n <div class=\"edu-header\">\n <div class=\"edu-degree\">${esc(degree)}</div>\n <div class=\"edu-year\">${esc(yearDisplay)}</div>\n </div>\n ${entry.institution ? `<div class=\"edu-institution\">${esc(instLine)}</div>` : ''}\n ${\n has(entry.courses)\n ? `<div class=\"edu-courses\">Coursework: ${entry.courses.map((c) => richText(c)).join(', ')}</div>`\n : ''\n }\n </div>`;\n}\n\nexport function renderEducation(\n education: readonly ResumeEducationEntry[] | undefined,\n heading: string,\n): string {\n if (!has(education)) return '';\n return `\n ${sectionTitle(heading)}\n <div class=\"section-body\">\n ${education.map(renderEducationEntry).join('')}\n </div>`;\n}\n","import type { ResumeVolunteerEntry } from '../types/resume.js';\nimport { esc } from '../utils/escape.js';\nimport { has, richText } from '../utils/text.js';\nimport { sectionTitle } from './shared.js';\n\nexport function renderVolunteer(\n volunteer: readonly ResumeVolunteerEntry[] | undefined,\n heading: string,\n): string {\n if (!has(volunteer)) return '';\n return `\n ${sectionTitle(heading)}\n <div class=\"section-body\">\n ${volunteer\n .map((v) => {\n const years = v.endDate\n ? `${v.startDate}\\u2013${v.endDate}`\n : v.startDate || '';\n const title = v.position\n ? `${v.position} \\u2013 ${v.organization}`\n : v.organization;\n const summary = richText(v.summary);\n\n return `\n <div class=\"bullet-item volunteer-item\">\n <span class=\"vol-title\">${esc(title)}${years ? ` (${esc(years)})` : ''}:</span> ${summary}\n </div>`;\n })\n .join('')}\n </div>`;\n}\n","import type { ResumeCertificate } from '../types/resume.js';\nimport { has, richText } from '../utils/text.js';\nimport { sectionTitle } from './shared.js';\n\nexport function renderCertificates(\n certificates: readonly ResumeCertificate[] | undefined,\n heading: string,\n): string {\n if (!has(certificates)) return '';\n return `\n ${sectionTitle(heading)}\n <div class=\"section-body\">\n ${certificates\n .map(\n (c) => `\n <div class=\"bullet-item\">${richText(c.name)}</div>`,\n )\n .join('')}\n </div>`;\n}\n","import type { ResumeSchema } from '../types/resume.js';\nimport { esc } from '../utils/escape.js';\nimport { has } from '../utils/text.js';\nimport { sectionTitle } from './shared.js';\n\nexport function renderAdditional(resume: ResumeSchema, heading: string): string {\n const parts: string[] = [];\n\n if (has(resume.interests)) {\n for (const interest of resume.interests) {\n const keywords = has(interest.keywords) ? interest.keywords.join(', ') : '';\n const label = interest.name || 'Interests';\n if (keywords) {\n parts.push(\n `<div class=\"additional-line\"><span class=\"additional-label\">${esc(label)}:</span> ${esc(keywords)}</div>`,\n );\n }\n }\n }\n\n if (has(resume.languages)) {\n const langStr = resume.languages\n .map((l) => `${l.language}${l.fluency ? ` (${l.fluency})` : ''}`)\n .join(', ');\n parts.push(\n `<div class=\"additional-line\"><span class=\"additional-label\">Languages:</span> ${esc(langStr)}</div>`,\n );\n }\n\n if (has(resume.references)) {\n parts.push(\n `<div class=\"additional-line\"><span class=\"additional-label\">Referees:</span> Available on request</div>`,\n );\n }\n\n if (parts.length === 0) return '';\n\n return `\n ${sectionTitle(heading)}\n <div class=\"section-body\">\n ${parts.join('\\n ')}\n </div>`;\n}\n","import type { ResumeSchema } from './types/resume.js';\nimport { DEFAULT_HEADINGS } from './constants.js';\nimport { esc } from './utils/escape.js';\nimport { renderHeader } from './sections/header.js';\nimport { renderSummary } from './sections/summary.js';\nimport { renderSkills } from './sections/skills.js';\nimport { renderWork } from './sections/work.js';\nimport { renderProjects } from './sections/projects.js';\nimport { renderEducation } from './sections/education.js';\nimport { renderVolunteer } from './sections/volunteer.js';\nimport { renderCertificates } from './sections/certificates.js';\nimport { renderAdditional } from './sections/additional.js';\nimport css from './styles/academic.css?inline';\n\nexport function render(resume: ResumeSchema): string {\n const h = { ...DEFAULT_HEADINGS };\n\n const metaHeadings = resume?.meta?.headings;\n if (metaHeadings) {\n for (const [key, value] of Object.entries(metaHeadings)) {\n if (value && key in h) {\n h[key] = value;\n }\n }\n }\n\n const basics = resume?.basics || {};\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n <title>${esc(basics.name || 'Resume')}</title>\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\" />\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin />\n <link href=\"https://fonts.googleapis.com/css2?family=EB+Garamond:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500&display=swap\" rel=\"stylesheet\" />\n <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css\" />\n <style>${css}</style>\n</head>\n<body>\n <div class=\"resume\">\n ${renderHeader(basics)}\n ${renderSummary(basics.summary, h.summary)}\n ${renderSkills(resume.skills, h.skills)}\n ${renderWork(resume.work, h.experience)}\n ${renderProjects(resume.projects, h.projects)}\n ${renderEducation(resume.education, h.education)}\n ${renderVolunteer(resume.volunteer, h.volunteer)}\n ${renderCertificates(resume.certificates, h.certifications)}\n ${renderAdditional(resume, h.additional)}\n </div>\n</body>\n</html>`;\n}\n","export { render } from './render.js';\nexport type { ResumeSchema } from './types/resume.js';\n\n/** PDF rendering options for Puppeteer/Gotenberg. */\nexport const pdfRenderOptions = {\n mediaType: 'print' as const,\n format: 'A4' as const,\n margin: {\n top: '12mm',\n right: '14mm',\n bottom: '12mm',\n left: '14mm',\n },\n};\n"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -51,7 +51,8 @@ function esc(text) {
|
|
|
51
51
|
return String(text).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
52
52
|
}
|
|
53
53
|
function formatDate(dateStr) {
|
|
54
|
-
if (!dateStr) return "";
|
|
54
|
+
if (!dateStr) return "Present";
|
|
55
|
+
if (dateStr.toLowerCase() === "present") return "Present";
|
|
55
56
|
const parts = String(dateStr).split("-");
|
|
56
57
|
if (parts.length === 1) return parts[0];
|
|
57
58
|
const year = parts[0];
|
|
@@ -60,8 +61,8 @@ function formatDate(dateStr) {
|
|
|
60
61
|
}
|
|
61
62
|
function dateRange(start, end) {
|
|
62
63
|
const s = formatDate(start);
|
|
63
|
-
const e =
|
|
64
|
-
if (!s) return "";
|
|
64
|
+
const e = formatDate(end);
|
|
65
|
+
if (!s || s === "Present") return "";
|
|
65
66
|
if (!e) return s;
|
|
66
67
|
return `${s} to ${e}`;
|
|
67
68
|
}
|
|
@@ -196,21 +197,21 @@ function isStructuredMeta(text) {
|
|
|
196
197
|
return /Tech-stack:\s/i.test(text) || /Client:\s/i.test(text);
|
|
197
198
|
}
|
|
198
199
|
function parseWorkMeta(summary) {
|
|
199
|
-
if (!summary) return { techStack: "", client: "" };
|
|
200
|
+
if (!summary) return { techStack: "", client: "", narrative: "" };
|
|
200
201
|
const text = stripHtml(summary).replace(/\n/g, " ").trim();
|
|
201
|
-
if (!isStructuredMeta(text)) return { techStack: "", client: "" };
|
|
202
|
-
|
|
202
|
+
if (!isStructuredMeta(text)) return { techStack: "", client: "", narrative: text };
|
|
203
|
+
let remaining = text;
|
|
204
|
+
const clientMatch = remaining.match(/Client:\s*(.+)$/i);
|
|
203
205
|
const client = clientMatch ? clientMatch[1].trim() : "";
|
|
204
|
-
|
|
205
|
-
const techMatch =
|
|
206
|
+
if (clientMatch) remaining = remaining.slice(0, clientMatch.index).trim();
|
|
207
|
+
const techMatch = remaining.match(/Tech-stack:\s*(.+)/i);
|
|
206
208
|
const techStack = techMatch ? techMatch[1].trim() : "";
|
|
207
|
-
|
|
209
|
+
if (techMatch) remaining = remaining.slice(0, techMatch.index).trim();
|
|
210
|
+
return { techStack, client, narrative: remaining };
|
|
208
211
|
}
|
|
209
212
|
function renderWorkEntry(entry) {
|
|
210
|
-
const { techStack, client } = parseWorkMeta(entry.summary);
|
|
213
|
+
const { techStack, client, narrative } = parseWorkMeta(entry.summary);
|
|
211
214
|
const duration = dateRange(entry.startDate, entry.endDate);
|
|
212
|
-
const plainSummary = entry.summary ? stripHtml(entry.summary).replace(/\n/g, " ").trim() : "";
|
|
213
|
-
const isNarrative = plainSummary && !isStructuredMeta(plainSummary);
|
|
214
215
|
return `
|
|
215
216
|
<div class="work-entry">
|
|
216
217
|
<div class="work-header">
|
|
@@ -222,7 +223,7 @@ function renderWorkEntry(entry) {
|
|
|
222
223
|
<div class="work-tech">${techStack ? `Tech-stack: ${esc(techStack)}` : ""}</div>
|
|
223
224
|
<div class="work-client">${client ? `Client: ${esc(client)}` : ""}</div>
|
|
224
225
|
</div>` : ""}
|
|
225
|
-
${
|
|
226
|
+
${narrative ? `<p class="work-summary">${richText(narrative, { block: false })}</p>` : ""}
|
|
226
227
|
${has(entry.highlights) ? `
|
|
227
228
|
<ul class="work-highlights">
|
|
228
229
|
${entry.highlights.map((h) => `<li>${richText(h)}</li>`).join("\n ")}
|
|
@@ -271,10 +272,7 @@ function renderEducationEntry(entry) {
|
|
|
271
272
|
<div class="edu-year">${esc(yearDisplay)}</div>
|
|
272
273
|
</div>
|
|
273
274
|
${entry.institution ? `<div class="edu-institution">${esc(instLine)}</div>` : ""}
|
|
274
|
-
${has(entry.courses) ?
|
|
275
|
-
<div class="edu-courses">
|
|
276
|
-
${entry.courses.map((c) => `<div>${richText(c)}</div>`).join("")}
|
|
277
|
-
</div>` : ""}
|
|
275
|
+
${has(entry.courses) ? `<div class="edu-courses">Coursework: ${entry.courses.map((c) => richText(c)).join(", ")}</div>` : ""}
|
|
278
276
|
</div>`;
|
|
279
277
|
}
|
|
280
278
|
function renderEducation(education, heading) {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../src/constants.ts","../src/utils/escape.ts","../src/utils/dates.ts","../src/utils/icons.ts","../src/utils/sanitize.ts","../src/utils/text.ts","../src/sections/header.ts","../src/sections/shared.ts","../src/sections/summary.ts","../src/sections/skills.ts","../src/sections/work.ts","../src/sections/projects.ts","../src/sections/education.ts","../src/sections/volunteer.ts","../src/sections/certificates.ts","../src/sections/additional.ts","../src/render.ts","../src/index.ts"],"sourcesContent":["export const MONTHS: readonly string[] = [\n 'January',\n 'February',\n 'March',\n 'April',\n 'May',\n 'June',\n 'July',\n 'August',\n 'September',\n 'October',\n 'November',\n 'December',\n];\n\nexport const COUNTRY_NAMES: Readonly<Record<string, string>> = {\n GB: 'United Kingdom',\n US: 'United States',\n CA: 'Canada',\n AU: 'Australia',\n DE: 'Germany',\n FR: 'France',\n IN: 'India',\n JP: 'Japan',\n CN: 'China',\n BR: 'Brazil',\n NL: 'Netherlands',\n IT: 'Italy',\n ES: 'Spain',\n SE: 'Sweden',\n CH: 'Switzerland',\n SG: 'Singapore',\n NZ: 'New Zealand',\n IE: 'Ireland',\n AE: 'UAE',\n};\n\nexport const DEFAULT_HEADINGS: Readonly<Record<string, string>> = {\n summary: 'Summary',\n skills: 'Core Skills',\n experience: 'Experience',\n projects: 'Selected Projects',\n education: 'Education',\n volunteer: 'Leadership & Volunteering',\n certifications: 'Credentials',\n additional: 'Additional',\n};\n","/** Decode common HTML entities to literal characters. */\nexport function decodeEntities(text: string): string {\n return text\n .replace(/&#(\\d+);/g, (_, c) => String.fromCharCode(Number(c)))\n .replace(/&#x([0-9a-fA-F]+);/g, (_, h) => String.fromCharCode(parseInt(h, 16)))\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/"/g, '\"')\n .replace(/'/g, \"'\")\n .replace(/'/g, \"'\")\n .replace(/ /g, ' ')\n .replace(/—/g, '\\u2014')\n .replace(/–/g, '\\u2013')\n .replace(/…/g, '\\u2026');\n}\n\n/** HTML-escape for safe output (plain text fields like names, dates). */\nexport function esc(text: string | null | undefined): string {\n if (text == null) return '';\n return String(text)\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n","import { MONTHS, COUNTRY_NAMES } from '../constants.js';\n\nexport function formatDate(dateStr: string | null | undefined): string {\n if (!dateStr) return '';\n const parts = String(dateStr).split('-');\n if (parts.length === 1) return parts[0];\n const year = parts[0];\n const month = MONTHS[parseInt(parts[1], 10) - 1] || '';\n return `${month} ${year}`;\n}\n\nexport function dateRange(start: string | null | undefined, end: string | null | undefined): string {\n const s = formatDate(start);\n const e = end ? formatDate(end) : '';\n if (!s) return '';\n if (!e) return s;\n return `${s} to ${e}`;\n}\n\nexport function regionName(code: string | null | undefined): string {\n if (!code) return '';\n return COUNTRY_NAMES[code.toUpperCase()] || code;\n}\n","export function profileIcon(network: string | null | undefined): string {\n if (!network) return '<i class=\"fa-solid fa-link\"></i>';\n const n = network.toLowerCase();\n if (n === 'linkedin') return '<i class=\"fa-brands fa-linkedin-in\"></i>';\n if (n === 'github') return '<i class=\"fa-brands fa-github\"></i>';\n if (n === 'twitter' || n === 'x') return '<i class=\"fa-brands fa-x-twitter\"></i>';\n if (n === 'stackoverflow') return '<i class=\"fa-brands fa-stack-overflow\"></i>';\n return '<i class=\"fa-solid fa-link\"></i>';\n}\n","/** Tags allowed through sanitization — safe inline/block formatting. */\nconst BLOCK_TAGS = new Set(['p', 'ul', 'ol', 'li', 'blockquote']);\nconst INLINE_TAGS = new Set([\n 'br', 'b', 'strong', 'i', 'em', 'u', 's', 'mark', 'a', 'sub', 'sup', 'code',\n]);\nconst ALLOWED_TAGS = new Set([...BLOCK_TAGS, ...INLINE_TAGS]);\n\n/** URL schemes safe for <a href>. Everything else (javascript:, data:, vbscript:) is stripped. */\nconst SAFE_URL_SCHEMES = /^(?:https?|mailto|tel):/i;\nconst RELATIVE_URL = /^[#/?.]/;\n\nfunction isSafeUrl(href: string): boolean {\n const trimmed = href.trim();\n if (!trimmed) return false;\n return SAFE_URL_SCHEMES.test(trimmed) || RELATIVE_URL.test(trimmed);\n}\n\ninterface SanitizeOptions {\n readonly inline?: boolean;\n}\n\n/**\n * Sanitize HTML — keep allowed formatting tags, strip everything else.\n * Attributes are stripped from all tags except <a href> (safe schemes only) and <mark class>.\n * If inline=true, block tags (p, ul, ol, li, blockquote) are also stripped.\n */\nexport function sanitizeHtml(html: string | null | undefined, { inline = false }: SanitizeOptions = {}): string {\n if (!html) return '';\n const allowed = inline ? INLINE_TAGS : ALLOWED_TAGS;\n\n return String(html)\n .replace(/<a\\s+[^>]*href=\"([^\"]*)\"[^>]*>/gi, (_, href: string) => {\n if (!isSafeUrl(href)) return '';\n return `<a href=\"${href.replace(/\"/g, '"')}\" target=\"_blank\" rel=\"noopener\">`;\n })\n .replace(/<mark\\s+[^>]*class=\"([^\"]*)\"[^>]*>/gi, (_, cls: string) =>\n `<mark class=\"${cls.replace(/\"/g, '"')}\">`,\n )\n .replace(/<(\\/?)([a-z][a-z0-9]*)\\s*[^>]*?(\\/?)>/gi, (match, slash: string, tag: string, selfClose: string) => {\n const lower = tag.toLowerCase();\n if (lower === 'a' && !slash && match.includes('href=')) return match;\n if (lower === 'mark' && !slash && match.includes('class=')) return match;\n if (allowed.has(lower)) return `<${slash}${lower}${selfClose}>`;\n if (inline && BLOCK_TAGS.has(lower)) return slash ? ' ' : '';\n return '';\n })\n .replace(/<p>\\s*<\\/p>/gi, '')\n .replace(/\\s{2,}/g, ' ')\n .trim();\n}\n","import { decodeEntities, esc } from './escape.js';\nimport { sanitizeHtml } from './sanitize.js';\n\n/** Strip HTML tags → plain text (for fields parsed as data, not displayed). */\nexport function stripHtml(text: string | null | undefined): string {\n if (!text) return '';\n return decodeEntities(\n String(text)\n .replace(/<br\\s*\\/?>/gi, ' ')\n .replace(/<\\/?(p|div|li)[^>]*>/gi, ' ')\n .replace(/<[^>]+>/g, ''),\n )\n .replace(/\\s{2,}/g, ' ')\n .trim();\n}\n\nconst isHtml = (text: string): boolean => /<[a-z][\\s\\S]*>/i.test(text);\nconst hasEntities = (text: string): boolean => /&(?:amp|lt|gt|quot|#\\d+|#x[0-9a-f]+);/i.test(text);\n\n/**\n * Render user-provided rich text → safe HTML output.\n * block=true: preserve <p>, <ul>, <ol> etc.\n * block=false (default): inline mode — strip block tags, keep only inline formatting.\n */\nexport function richText(text: string | null | undefined, { block = false }: { block?: boolean } = {}): string {\n if (text == null) return '';\n const s = String(text);\n if (isHtml(s)) return sanitizeHtml(s, { inline: !block });\n if (hasEntities(s)) return esc(decodeEntities(s));\n return esc(s);\n}\n\n/** Check if an array is non-empty. */\nexport function has<T>(arr: readonly T[] | null | undefined): arr is readonly T[] & { length: number } {\n return Array.isArray(arr) && arr.length > 0;\n}\n","import type { ResumeBasics } from '../types/resume.js';\nimport { esc } from '../utils/escape.js';\nimport { regionName } from '../utils/dates.js';\nimport { profileIcon } from '../utils/icons.js';\nimport { has } from '../utils/text.js';\n\ninterface ContactLine {\n readonly icon: string;\n readonly text: string;\n}\n\nexport function renderHeader(basics: ResumeBasics | undefined): string {\n if (!basics) return '';\n\n const location = basics.location\n ? [basics.location.city, regionName(basics.location.region)].filter(Boolean).join(', ')\n : '';\n\n const lines: ContactLine[] = [];\n if (location)\n lines.push({ icon: '<i class=\"fa-solid fa-location-dot\"></i>', text: location });\n if (basics.phone)\n lines.push({ icon: '<i class=\"fa-solid fa-square-phone\"></i>', text: basics.phone });\n if (basics.email)\n lines.push({ icon: '<i class=\"fa-solid fa-envelope\"></i>', text: basics.email });\n if (basics.url) {\n const display = basics.url.replace(/^https?:\\/\\//, '');\n lines.push({ icon: '<i class=\"fa-solid fa-globe\"></i>', text: display });\n }\n if (has(basics.profiles)) {\n for (const p of basics.profiles) {\n lines.push({ icon: profileIcon(p.network), text: p.username || p.url || '' });\n }\n }\n\n return `\n <header class=\"header\">\n <div class=\"header-left\">\n <h1 class=\"name\">${esc(basics.name)}</h1>\n <div class=\"label\">${esc(basics.label)}</div>\n </div>\n <div class=\"contact-info\">\n ${lines.map((l) => `<div class=\"contact-line\">${esc(l.text)} ${l.icon}</div>`).join('\\n ')}\n </div>\n </header>\n <hr class=\"header-rule\" />`;\n}\n","import { esc } from '../utils/escape.js';\n\nexport function sectionTitle(title: string): string {\n return `<h2 class=\"section-title\">${esc(title)}</h2>`;\n}\n","import { richText } from '../utils/text.js';\nimport { sectionTitle } from './shared.js';\n\nexport function renderSummary(summary: string | undefined, heading: string): string {\n if (!summary) return '';\n const html = richText(summary, { block: true });\n if (!html) return '';\n return `\n ${sectionTitle(heading)}\n <div class=\"summary\">${html}</div>`;\n}\n","import type { ResumeSkill } from '../types/resume.js';\nimport { esc } from '../utils/escape.js';\nimport { has } from '../utils/text.js';\nimport { sectionTitle } from './shared.js';\n\nexport function renderSkills(skills: readonly ResumeSkill[] | undefined, heading: string): string {\n if (!has(skills)) return '';\n return `\n ${sectionTitle(heading)}\n <div class=\"section-body\">\n ${skills\n .map(\n (s) => `\n <div class=\"bullet-item\">\n <span class=\"skill-name\">${esc(s.name)}:</span> ${esc((s.keywords || []).join(', '))}\n </div>`,\n )\n .join('')}\n </div>`;\n}\n","import type { ResumeWorkEntry } from '../types/resume.js';\nimport { esc } from '../utils/escape.js';\nimport { dateRange } from '../utils/dates.js';\nimport { has, richText, stripHtml } from '../utils/text.js';\nimport { sectionTitle } from './shared.js';\n\n/** Check whether summary uses structured Tech-stack / Client format. */\nfunction isStructuredMeta(text: string): boolean {\n return /Tech-stack:\\s/i.test(text) || /Client:\\s/i.test(text);\n}\n\n/** Extract tech-stack and client from a structured work summary string. */\nfunction parseWorkMeta(summary: string | undefined): { techStack: string; client: string } {\n if (!summary) return { techStack: '', client: '' };\n const text = stripHtml(summary).replace(/\\n/g, ' ').trim();\n if (!isStructuredMeta(text)) return { techStack: '', client: '' };\n\n const clientMatch = text.match(/Client:\\s*(.+)$/i);\n const client = clientMatch ? clientMatch[1].trim() : '';\n\n const techPart = clientMatch ? text.slice(0, clientMatch.index).trim() : text;\n const techMatch = techPart.match(/Tech-stack:\\s*(.+)/i);\n const techStack = techMatch ? techMatch[1].trim() : '';\n\n return { techStack, client };\n}\n\nfunction renderWorkEntry(entry: ResumeWorkEntry): string {\n const { techStack, client } = parseWorkMeta(entry.summary);\n const duration = dateRange(entry.startDate, entry.endDate);\n const plainSummary = entry.summary ? stripHtml(entry.summary).replace(/\\n/g, ' ').trim() : '';\n const isNarrative = plainSummary && !isStructuredMeta(plainSummary);\n\n return `\n <div class=\"work-entry\">\n <div class=\"work-header\">\n <div class=\"work-title\">${esc(entry.name)} - ${esc(entry.position)}</div>\n <div class=\"work-duration\">${duration ? `Duration: ${esc(duration)}` : ''}</div>\n </div>\n ${\n techStack || client\n ? `\n <div class=\"work-meta\">\n <div class=\"work-tech\">${techStack ? `Tech-stack: ${esc(techStack)}` : ''}</div>\n <div class=\"work-client\">${client ? `Client: ${esc(client)}` : ''}</div>\n </div>`\n : ''\n }\n ${isNarrative ? `<p class=\"work-summary\">${richText(entry.summary, { block: false })}</p>` : ''}\n ${\n has(entry.highlights)\n ? `\n <ul class=\"work-highlights\">\n ${entry.highlights.map((h) => `<li>${richText(h)}</li>`).join('\\n ')}\n </ul>`\n : ''\n }\n </div>`;\n}\n\nexport function renderWork(work: readonly ResumeWorkEntry[] | undefined, heading: string): string {\n if (!has(work)) return '';\n return `\n ${sectionTitle(heading)}\n <div class=\"section-body\">\n ${work.map(renderWorkEntry).join('')}\n </div>`;\n}\n","import type { ResumeProject } from '../types/resume.js';\nimport { esc } from '../utils/escape.js';\nimport { has, richText } from '../utils/text.js';\nimport { sectionTitle } from './shared.js';\n\nexport function renderProjects(\n projects: readonly ResumeProject[] | undefined,\n heading: string,\n): string {\n if (!has(projects)) return '';\n return `\n ${sectionTitle(heading)}\n <div class=\"section-body\">\n ${projects\n .map((p) => {\n const desc = richText(p.description);\n return `\n <div class=\"bullet-item\">\n <span class=\"project-name\">${esc(p.name)}:</span> ${desc}\n </div>`;\n })\n .join('')}\n </div>`;\n}\n","import type { ResumeEducationEntry } from '../types/resume.js';\nimport { esc } from '../utils/escape.js';\nimport { formatDate } from '../utils/dates.js';\nimport { has, richText } from '../utils/text.js';\nimport { sectionTitle } from './shared.js';\n\nfunction renderEducationEntry(entry: ResumeEducationEntry): string {\n const degree = [entry.studyType, entry.area].filter(Boolean).join(' ');\n\n let yearDisplay = '';\n if (entry.endDate) {\n yearDisplay = formatDate(entry.endDate);\n } else if (entry.startDate) {\n const year = parseInt(String(entry.startDate).split('-')[0], 10);\n const currentYear = new Date().getFullYear();\n yearDisplay = year >= currentYear ? `Expected ${formatDate(entry.startDate)}` : formatDate(entry.startDate);\n }\n\n const instParts = [entry.institution];\n if (entry.score) instParts.push(entry.score);\n const instLine = instParts.filter(Boolean).join(' | ');\n\n return `\n <div class=\"edu-entry\">\n <div class=\"edu-header\">\n <div class=\"edu-degree\">${esc(degree)}</div>\n <div class=\"edu-year\">${esc(yearDisplay)}</div>\n </div>\n ${entry.institution ? `<div class=\"edu-institution\">${esc(instLine)}</div>` : ''}\n ${\n has(entry.courses)\n ? `\n <div class=\"edu-courses\">\n ${entry.courses.map((c) => `<div>${richText(c)}</div>`).join('')}\n </div>`\n : ''\n }\n </div>`;\n}\n\nexport function renderEducation(\n education: readonly ResumeEducationEntry[] | undefined,\n heading: string,\n): string {\n if (!has(education)) return '';\n return `\n ${sectionTitle(heading)}\n <div class=\"section-body\">\n ${education.map(renderEducationEntry).join('')}\n </div>`;\n}\n","import type { ResumeVolunteerEntry } from '../types/resume.js';\nimport { esc } from '../utils/escape.js';\nimport { has, richText } from '../utils/text.js';\nimport { sectionTitle } from './shared.js';\n\nexport function renderVolunteer(\n volunteer: readonly ResumeVolunteerEntry[] | undefined,\n heading: string,\n): string {\n if (!has(volunteer)) return '';\n return `\n ${sectionTitle(heading)}\n <div class=\"section-body\">\n ${volunteer\n .map((v) => {\n const years = v.endDate\n ? `${v.startDate}\\u2013${v.endDate}`\n : v.startDate || '';\n const title = v.position\n ? `${v.position} \\u2013 ${v.organization}`\n : v.organization;\n const summary = richText(v.summary);\n\n return `\n <div class=\"bullet-item volunteer-item\">\n <span class=\"vol-title\">${esc(title)}${years ? ` (${esc(years)})` : ''}:</span> ${summary}\n </div>`;\n })\n .join('')}\n </div>`;\n}\n","import type { ResumeCertificate } from '../types/resume.js';\nimport { has, richText } from '../utils/text.js';\nimport { sectionTitle } from './shared.js';\n\nexport function renderCertificates(\n certificates: readonly ResumeCertificate[] | undefined,\n heading: string,\n): string {\n if (!has(certificates)) return '';\n return `\n ${sectionTitle(heading)}\n <div class=\"section-body\">\n ${certificates\n .map(\n (c) => `\n <div class=\"bullet-item\">${richText(c.name)}</div>`,\n )\n .join('')}\n </div>`;\n}\n","import type { ResumeSchema } from '../types/resume.js';\nimport { esc } from '../utils/escape.js';\nimport { has } from '../utils/text.js';\nimport { sectionTitle } from './shared.js';\n\nexport function renderAdditional(resume: ResumeSchema, heading: string): string {\n const parts: string[] = [];\n\n if (has(resume.interests)) {\n for (const interest of resume.interests) {\n const keywords = has(interest.keywords) ? interest.keywords.join(', ') : '';\n const label = interest.name || 'Interests';\n if (keywords) {\n parts.push(\n `<div class=\"additional-line\"><span class=\"additional-label\">${esc(label)}:</span> ${esc(keywords)}</div>`,\n );\n }\n }\n }\n\n if (has(resume.languages)) {\n const langStr = resume.languages\n .map((l) => `${l.language}${l.fluency ? ` (${l.fluency})` : ''}`)\n .join(', ');\n parts.push(\n `<div class=\"additional-line\"><span class=\"additional-label\">Languages:</span> ${esc(langStr)}</div>`,\n );\n }\n\n if (has(resume.references)) {\n parts.push(\n `<div class=\"additional-line\"><span class=\"additional-label\">Referees:</span> Available on request</div>`,\n );\n }\n\n if (parts.length === 0) return '';\n\n return `\n ${sectionTitle(heading)}\n <div class=\"section-body\">\n ${parts.join('\\n ')}\n </div>`;\n}\n","import type { ResumeSchema } from './types/resume.js';\nimport { DEFAULT_HEADINGS } from './constants.js';\nimport { esc } from './utils/escape.js';\nimport { renderHeader } from './sections/header.js';\nimport { renderSummary } from './sections/summary.js';\nimport { renderSkills } from './sections/skills.js';\nimport { renderWork } from './sections/work.js';\nimport { renderProjects } from './sections/projects.js';\nimport { renderEducation } from './sections/education.js';\nimport { renderVolunteer } from './sections/volunteer.js';\nimport { renderCertificates } from './sections/certificates.js';\nimport { renderAdditional } from './sections/additional.js';\nimport css from './styles/academic.css?inline';\n\nexport function render(resume: ResumeSchema): string {\n const h = { ...DEFAULT_HEADINGS };\n\n const metaHeadings = resume?.meta?.headings;\n if (metaHeadings) {\n for (const [key, value] of Object.entries(metaHeadings)) {\n if (value && key in h) {\n h[key] = value;\n }\n }\n }\n\n const basics = resume?.basics || {};\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n <title>${esc(basics.name || 'Resume')}</title>\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\" />\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin />\n <link href=\"https://fonts.googleapis.com/css2?family=EB+Garamond:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500&display=swap\" rel=\"stylesheet\" />\n <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css\" />\n <style>${css}</style>\n</head>\n<body>\n <div class=\"resume\">\n ${renderHeader(basics)}\n ${renderSummary(basics.summary, h.summary)}\n ${renderSkills(resume.skills, h.skills)}\n ${renderWork(resume.work, h.experience)}\n ${renderProjects(resume.projects, h.projects)}\n ${renderEducation(resume.education, h.education)}\n ${renderVolunteer(resume.volunteer, h.volunteer)}\n ${renderCertificates(resume.certificates, h.certifications)}\n ${renderAdditional(resume, h.additional)}\n </div>\n</body>\n</html>`;\n}\n","export { render } from './render.js';\nexport type { ResumeSchema } from './types/resume.js';\n\n/** PDF rendering options for Puppeteer/Gotenberg. */\nexport const pdfRenderOptions = {\n mediaType: 'print' as const,\n format: 'A4' as const,\n margin: {\n top: '12mm',\n right: '14mm',\n bottom: '12mm',\n left: '14mm',\n },\n};\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/constants.ts","../src/utils/escape.ts","../src/utils/dates.ts","../src/utils/icons.ts","../src/utils/sanitize.ts","../src/utils/text.ts","../src/sections/header.ts","../src/sections/shared.ts","../src/sections/summary.ts","../src/sections/skills.ts","../src/sections/work.ts","../src/sections/projects.ts","../src/sections/education.ts","../src/sections/volunteer.ts","../src/sections/certificates.ts","../src/sections/additional.ts","../src/render.ts","../src/index.ts"],"sourcesContent":["export const MONTHS: readonly string[] = [\n 'January',\n 'February',\n 'March',\n 'April',\n 'May',\n 'June',\n 'July',\n 'August',\n 'September',\n 'October',\n 'November',\n 'December',\n];\n\nexport const COUNTRY_NAMES: Readonly<Record<string, string>> = {\n GB: 'United Kingdom',\n US: 'United States',\n CA: 'Canada',\n AU: 'Australia',\n DE: 'Germany',\n FR: 'France',\n IN: 'India',\n JP: 'Japan',\n CN: 'China',\n BR: 'Brazil',\n NL: 'Netherlands',\n IT: 'Italy',\n ES: 'Spain',\n SE: 'Sweden',\n CH: 'Switzerland',\n SG: 'Singapore',\n NZ: 'New Zealand',\n IE: 'Ireland',\n AE: 'UAE',\n};\n\nexport const DEFAULT_HEADINGS: Readonly<Record<string, string>> = {\n summary: 'Summary',\n skills: 'Core Skills',\n experience: 'Experience',\n projects: 'Selected Projects',\n education: 'Education',\n volunteer: 'Leadership & Volunteering',\n certifications: 'Credentials',\n additional: 'Additional',\n};\n","/** Decode common HTML entities to literal characters. */\nexport function decodeEntities(text: string): string {\n return text\n .replace(/&#(\\d+);/g, (_, c) => String.fromCharCode(Number(c)))\n .replace(/&#x([0-9a-fA-F]+);/g, (_, h) => String.fromCharCode(parseInt(h, 16)))\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/"/g, '\"')\n .replace(/'/g, \"'\")\n .replace(/'/g, \"'\")\n .replace(/ /g, ' ')\n .replace(/—/g, '\\u2014')\n .replace(/–/g, '\\u2013')\n .replace(/…/g, '\\u2026');\n}\n\n/** HTML-escape for safe output (plain text fields like names, dates). */\nexport function esc(text: string | null | undefined): string {\n if (text == null) return '';\n return String(text)\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n","import { MONTHS, COUNTRY_NAMES } from '../constants.js';\n\nexport function formatDate(dateStr: string | null | undefined): string {\n if (!dateStr) return 'Present';\n if (dateStr.toLowerCase() === 'present') return 'Present';\n const parts = String(dateStr).split('-');\n if (parts.length === 1) return parts[0];\n const year = parts[0];\n const month = MONTHS[parseInt(parts[1], 10) - 1] || '';\n return `${month} ${year}`;\n}\n\nexport function dateRange(start: string | null | undefined, end: string | null | undefined): string {\n const s = formatDate(start);\n const e = formatDate(end);\n if (!s || s === 'Present') return ''; // start date should not be empty or present\n if (!e) return s;\n return `${s} to ${e}`;\n}\n\nexport function regionName(code: string | null | undefined): string {\n if (!code) return '';\n return COUNTRY_NAMES[code.toUpperCase()] || code;\n}\n","export function profileIcon(network: string | null | undefined): string {\n if (!network) return '<i class=\"fa-solid fa-link\"></i>';\n const n = network.toLowerCase();\n if (n === 'linkedin') return '<i class=\"fa-brands fa-linkedin-in\"></i>';\n if (n === 'github') return '<i class=\"fa-brands fa-github\"></i>';\n if (n === 'twitter' || n === 'x') return '<i class=\"fa-brands fa-x-twitter\"></i>';\n if (n === 'stackoverflow') return '<i class=\"fa-brands fa-stack-overflow\"></i>';\n return '<i class=\"fa-solid fa-link\"></i>';\n}\n","/** Tags allowed through sanitization — safe inline/block formatting. */\nconst BLOCK_TAGS = new Set(['p', 'ul', 'ol', 'li', 'blockquote']);\nconst INLINE_TAGS = new Set([\n 'br', 'b', 'strong', 'i', 'em', 'u', 's', 'mark', 'a', 'sub', 'sup', 'code',\n]);\nconst ALLOWED_TAGS = new Set([...BLOCK_TAGS, ...INLINE_TAGS]);\n\n/** URL schemes safe for <a href>. Everything else (javascript:, data:, vbscript:) is stripped. */\nconst SAFE_URL_SCHEMES = /^(?:https?|mailto|tel):/i;\nconst RELATIVE_URL = /^[#/?.]/;\n\nfunction isSafeUrl(href: string): boolean {\n const trimmed = href.trim();\n if (!trimmed) return false;\n return SAFE_URL_SCHEMES.test(trimmed) || RELATIVE_URL.test(trimmed);\n}\n\ninterface SanitizeOptions {\n readonly inline?: boolean;\n}\n\n/**\n * Sanitize HTML — keep allowed formatting tags, strip everything else.\n * Attributes are stripped from all tags except <a href> (safe schemes only) and <mark class>.\n * If inline=true, block tags (p, ul, ol, li, blockquote) are also stripped.\n */\nexport function sanitizeHtml(html: string | null | undefined, { inline = false }: SanitizeOptions = {}): string {\n if (!html) return '';\n const allowed = inline ? INLINE_TAGS : ALLOWED_TAGS;\n\n return String(html)\n .replace(/<a\\s+[^>]*href=\"([^\"]*)\"[^>]*>/gi, (_, href: string) => {\n if (!isSafeUrl(href)) return '';\n return `<a href=\"${href.replace(/\"/g, '"')}\" target=\"_blank\" rel=\"noopener\">`;\n })\n .replace(/<mark\\s+[^>]*class=\"([^\"]*)\"[^>]*>/gi, (_, cls: string) =>\n `<mark class=\"${cls.replace(/\"/g, '"')}\">`,\n )\n .replace(/<(\\/?)([a-z][a-z0-9]*)\\s*[^>]*?(\\/?)>/gi, (match, slash: string, tag: string, selfClose: string) => {\n const lower = tag.toLowerCase();\n if (lower === 'a' && !slash && match.includes('href=')) return match;\n if (lower === 'mark' && !slash && match.includes('class=')) return match;\n if (allowed.has(lower)) return `<${slash}${lower}${selfClose}>`;\n if (inline && BLOCK_TAGS.has(lower)) return slash ? ' ' : '';\n return '';\n })\n .replace(/<p>\\s*<\\/p>/gi, '')\n .replace(/\\s{2,}/g, ' ')\n .trim();\n}\n","import { decodeEntities, esc } from './escape.js';\nimport { sanitizeHtml } from './sanitize.js';\n\n/** Strip HTML tags → plain text (for fields parsed as data, not displayed). */\nexport function stripHtml(text: string | null | undefined): string {\n if (!text) return '';\n return decodeEntities(\n String(text)\n .replace(/<br\\s*\\/?>/gi, ' ')\n .replace(/<\\/?(p|div|li)[^>]*>/gi, ' ')\n .replace(/<[^>]+>/g, ''),\n )\n .replace(/\\s{2,}/g, ' ')\n .trim();\n}\n\nconst isHtml = (text: string): boolean => /<[a-z][\\s\\S]*>/i.test(text);\nconst hasEntities = (text: string): boolean => /&(?:amp|lt|gt|quot|#\\d+|#x[0-9a-f]+);/i.test(text);\n\n/**\n * Render user-provided rich text → safe HTML output.\n * block=true: preserve <p>, <ul>, <ol> etc.\n * block=false (default): inline mode — strip block tags, keep only inline formatting.\n */\nexport function richText(text: string | null | undefined, { block = false }: { block?: boolean } = {}): string {\n if (text == null) return '';\n const s = String(text);\n if (isHtml(s)) return sanitizeHtml(s, { inline: !block });\n if (hasEntities(s)) return esc(decodeEntities(s));\n return esc(s);\n}\n\n/** Check if an array is non-empty. */\nexport function has<T>(arr: readonly T[] | null | undefined): arr is readonly T[] & { length: number } {\n return Array.isArray(arr) && arr.length > 0;\n}\n","import type { ResumeBasics } from '../types/resume.js';\nimport { esc } from '../utils/escape.js';\nimport { regionName } from '../utils/dates.js';\nimport { profileIcon } from '../utils/icons.js';\nimport { has } from '../utils/text.js';\n\ninterface ContactLine {\n readonly icon: string;\n readonly text: string;\n}\n\nexport function renderHeader(basics: ResumeBasics | undefined): string {\n if (!basics) return '';\n\n const location = basics.location\n ? [basics.location.city, regionName(basics.location.region)].filter(Boolean).join(', ')\n : '';\n\n const lines: ContactLine[] = [];\n if (location)\n lines.push({ icon: '<i class=\"fa-solid fa-location-dot\"></i>', text: location });\n if (basics.phone)\n lines.push({ icon: '<i class=\"fa-solid fa-square-phone\"></i>', text: basics.phone });\n if (basics.email)\n lines.push({ icon: '<i class=\"fa-solid fa-envelope\"></i>', text: basics.email });\n if (basics.url) {\n const display = basics.url.replace(/^https?:\\/\\//, '');\n lines.push({ icon: '<i class=\"fa-solid fa-globe\"></i>', text: display });\n }\n if (has(basics.profiles)) {\n for (const p of basics.profiles) {\n lines.push({ icon: profileIcon(p.network), text: p.username || p.url || '' });\n }\n }\n\n return `\n <header class=\"header\">\n <div class=\"header-left\">\n <h1 class=\"name\">${esc(basics.name)}</h1>\n <div class=\"label\">${esc(basics.label)}</div>\n </div>\n <div class=\"contact-info\">\n ${lines.map((l) => `<div class=\"contact-line\">${esc(l.text)} ${l.icon}</div>`).join('\\n ')}\n </div>\n </header>\n <hr class=\"header-rule\" />`;\n}\n","import { esc } from '../utils/escape.js';\n\nexport function sectionTitle(title: string): string {\n return `<h2 class=\"section-title\">${esc(title)}</h2>`;\n}\n","import { richText } from '../utils/text.js';\nimport { sectionTitle } from './shared.js';\n\nexport function renderSummary(summary: string | undefined, heading: string): string {\n if (!summary) return '';\n const html = richText(summary, { block: true });\n if (!html) return '';\n return `\n ${sectionTitle(heading)}\n <div class=\"summary\">${html}</div>`;\n}\n","import type { ResumeSkill } from '../types/resume.js';\nimport { esc } from '../utils/escape.js';\nimport { has } from '../utils/text.js';\nimport { sectionTitle } from './shared.js';\n\nexport function renderSkills(skills: readonly ResumeSkill[] | undefined, heading: string): string {\n if (!has(skills)) return '';\n return `\n ${sectionTitle(heading)}\n <div class=\"section-body\">\n ${skills\n .map(\n (s) => `\n <div class=\"bullet-item\">\n <span class=\"skill-name\">${esc(s.name)}:</span> ${esc((s.keywords || []).join(', '))}\n </div>`,\n )\n .join('')}\n </div>`;\n}\n","import type { ResumeWorkEntry } from '../types/resume.js';\nimport { esc } from '../utils/escape.js';\nimport { dateRange } from '../utils/dates.js';\nimport { has, richText, stripHtml } from '../utils/text.js';\nimport { sectionTitle } from './shared.js';\n\n/** Check whether summary uses structured Tech-stack / Client format. */\nfunction isStructuredMeta(text: string): boolean {\n return /Tech-stack:\\s/i.test(text) || /Client:\\s/i.test(text);\n}\n\n/** Extract tech-stack, client, and any remaining narrative from a work summary. */\nfunction parseWorkMeta(summary: string | undefined): {\n techStack: string;\n client: string;\n narrative: string;\n} {\n if (!summary) return { techStack: '', client: '', narrative: '' };\n const text = stripHtml(summary).replace(/\\n/g, ' ').trim();\n if (!isStructuredMeta(text)) return { techStack: '', client: '', narrative: text };\n\n let remaining = text;\n\n const clientMatch = remaining.match(/Client:\\s*(.+)$/i);\n const client = clientMatch ? clientMatch[1].trim() : '';\n if (clientMatch) remaining = remaining.slice(0, clientMatch.index).trim();\n\n const techMatch = remaining.match(/Tech-stack:\\s*(.+)/i);\n const techStack = techMatch ? techMatch[1].trim() : '';\n if (techMatch) remaining = remaining.slice(0, techMatch.index).trim();\n\n return { techStack, client, narrative: remaining };\n}\n\nfunction renderWorkEntry(entry: ResumeWorkEntry): string {\n const { techStack, client, narrative } = parseWorkMeta(entry.summary);\n const duration = dateRange(entry.startDate, entry.endDate);\n\n return `\n <div class=\"work-entry\">\n <div class=\"work-header\">\n <div class=\"work-title\">${esc(entry.name)} - ${esc(entry.position)}</div>\n <div class=\"work-duration\">${duration ? `Duration: ${esc(duration)}` : ''}</div>\n </div>\n ${\n techStack || client\n ? `\n <div class=\"work-meta\">\n <div class=\"work-tech\">${techStack ? `Tech-stack: ${esc(techStack)}` : ''}</div>\n <div class=\"work-client\">${client ? `Client: ${esc(client)}` : ''}</div>\n </div>`\n : ''\n }\n ${narrative ? `<p class=\"work-summary\">${richText(narrative, { block: false })}</p>` : ''}\n ${\n has(entry.highlights)\n ? `\n <ul class=\"work-highlights\">\n ${entry.highlights.map((h) => `<li>${richText(h)}</li>`).join('\\n ')}\n </ul>`\n : ''\n }\n </div>`;\n}\n\nexport function renderWork(work: readonly ResumeWorkEntry[] | undefined, heading: string): string {\n if (!has(work)) return '';\n return `\n ${sectionTitle(heading)}\n <div class=\"section-body\">\n ${work.map(renderWorkEntry).join('')}\n </div>`;\n}\n","import type { ResumeProject } from '../types/resume.js';\nimport { esc } from '../utils/escape.js';\nimport { has, richText } from '../utils/text.js';\nimport { sectionTitle } from './shared.js';\n\nexport function renderProjects(\n projects: readonly ResumeProject[] | undefined,\n heading: string,\n): string {\n if (!has(projects)) return '';\n return `\n ${sectionTitle(heading)}\n <div class=\"section-body\">\n ${projects\n .map((p) => {\n const desc = richText(p.description);\n return `\n <div class=\"bullet-item\">\n <span class=\"project-name\">${esc(p.name)}:</span> ${desc}\n </div>`;\n })\n .join('')}\n </div>`;\n}\n","import type { ResumeEducationEntry } from '../types/resume.js';\nimport { esc } from '../utils/escape.js';\nimport { formatDate } from '../utils/dates.js';\nimport { has, richText } from '../utils/text.js';\nimport { sectionTitle } from './shared.js';\n\nfunction renderEducationEntry(entry: ResumeEducationEntry): string {\n const degree = [entry.studyType, entry.area].filter(Boolean).join(' ');\n\n let yearDisplay = '';\n if (entry.endDate) {\n yearDisplay = formatDate(entry.endDate);\n } else if (entry.startDate) {\n const year = parseInt(String(entry.startDate).split('-')[0], 10);\n const currentYear = new Date().getFullYear();\n yearDisplay = year >= currentYear ? `Expected ${formatDate(entry.startDate)}` : formatDate(entry.startDate);\n }\n\n const instParts = [entry.institution];\n if (entry.score) instParts.push(entry.score);\n const instLine = instParts.filter(Boolean).join(' | ');\n\n return `\n <div class=\"edu-entry\">\n <div class=\"edu-header\">\n <div class=\"edu-degree\">${esc(degree)}</div>\n <div class=\"edu-year\">${esc(yearDisplay)}</div>\n </div>\n ${entry.institution ? `<div class=\"edu-institution\">${esc(instLine)}</div>` : ''}\n ${\n has(entry.courses)\n ? `<div class=\"edu-courses\">Coursework: ${entry.courses.map((c) => richText(c)).join(', ')}</div>`\n : ''\n }\n </div>`;\n}\n\nexport function renderEducation(\n education: readonly ResumeEducationEntry[] | undefined,\n heading: string,\n): string {\n if (!has(education)) return '';\n return `\n ${sectionTitle(heading)}\n <div class=\"section-body\">\n ${education.map(renderEducationEntry).join('')}\n </div>`;\n}\n","import type { ResumeVolunteerEntry } from '../types/resume.js';\nimport { esc } from '../utils/escape.js';\nimport { has, richText } from '../utils/text.js';\nimport { sectionTitle } from './shared.js';\n\nexport function renderVolunteer(\n volunteer: readonly ResumeVolunteerEntry[] | undefined,\n heading: string,\n): string {\n if (!has(volunteer)) return '';\n return `\n ${sectionTitle(heading)}\n <div class=\"section-body\">\n ${volunteer\n .map((v) => {\n const years = v.endDate\n ? `${v.startDate}\\u2013${v.endDate}`\n : v.startDate || '';\n const title = v.position\n ? `${v.position} \\u2013 ${v.organization}`\n : v.organization;\n const summary = richText(v.summary);\n\n return `\n <div class=\"bullet-item volunteer-item\">\n <span class=\"vol-title\">${esc(title)}${years ? ` (${esc(years)})` : ''}:</span> ${summary}\n </div>`;\n })\n .join('')}\n </div>`;\n}\n","import type { ResumeCertificate } from '../types/resume.js';\nimport { has, richText } from '../utils/text.js';\nimport { sectionTitle } from './shared.js';\n\nexport function renderCertificates(\n certificates: readonly ResumeCertificate[] | undefined,\n heading: string,\n): string {\n if (!has(certificates)) return '';\n return `\n ${sectionTitle(heading)}\n <div class=\"section-body\">\n ${certificates\n .map(\n (c) => `\n <div class=\"bullet-item\">${richText(c.name)}</div>`,\n )\n .join('')}\n </div>`;\n}\n","import type { ResumeSchema } from '../types/resume.js';\nimport { esc } from '../utils/escape.js';\nimport { has } from '../utils/text.js';\nimport { sectionTitle } from './shared.js';\n\nexport function renderAdditional(resume: ResumeSchema, heading: string): string {\n const parts: string[] = [];\n\n if (has(resume.interests)) {\n for (const interest of resume.interests) {\n const keywords = has(interest.keywords) ? interest.keywords.join(', ') : '';\n const label = interest.name || 'Interests';\n if (keywords) {\n parts.push(\n `<div class=\"additional-line\"><span class=\"additional-label\">${esc(label)}:</span> ${esc(keywords)}</div>`,\n );\n }\n }\n }\n\n if (has(resume.languages)) {\n const langStr = resume.languages\n .map((l) => `${l.language}${l.fluency ? ` (${l.fluency})` : ''}`)\n .join(', ');\n parts.push(\n `<div class=\"additional-line\"><span class=\"additional-label\">Languages:</span> ${esc(langStr)}</div>`,\n );\n }\n\n if (has(resume.references)) {\n parts.push(\n `<div class=\"additional-line\"><span class=\"additional-label\">Referees:</span> Available on request</div>`,\n );\n }\n\n if (parts.length === 0) return '';\n\n return `\n ${sectionTitle(heading)}\n <div class=\"section-body\">\n ${parts.join('\\n ')}\n </div>`;\n}\n","import type { ResumeSchema } from './types/resume.js';\nimport { DEFAULT_HEADINGS } from './constants.js';\nimport { esc } from './utils/escape.js';\nimport { renderHeader } from './sections/header.js';\nimport { renderSummary } from './sections/summary.js';\nimport { renderSkills } from './sections/skills.js';\nimport { renderWork } from './sections/work.js';\nimport { renderProjects } from './sections/projects.js';\nimport { renderEducation } from './sections/education.js';\nimport { renderVolunteer } from './sections/volunteer.js';\nimport { renderCertificates } from './sections/certificates.js';\nimport { renderAdditional } from './sections/additional.js';\nimport css from './styles/academic.css?inline';\n\nexport function render(resume: ResumeSchema): string {\n const h = { ...DEFAULT_HEADINGS };\n\n const metaHeadings = resume?.meta?.headings;\n if (metaHeadings) {\n for (const [key, value] of Object.entries(metaHeadings)) {\n if (value && key in h) {\n h[key] = value;\n }\n }\n }\n\n const basics = resume?.basics || {};\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n <title>${esc(basics.name || 'Resume')}</title>\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\" />\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin />\n <link href=\"https://fonts.googleapis.com/css2?family=EB+Garamond:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500&display=swap\" rel=\"stylesheet\" />\n <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css\" />\n <style>${css}</style>\n</head>\n<body>\n <div class=\"resume\">\n ${renderHeader(basics)}\n ${renderSummary(basics.summary, h.summary)}\n ${renderSkills(resume.skills, h.skills)}\n ${renderWork(resume.work, h.experience)}\n ${renderProjects(resume.projects, h.projects)}\n ${renderEducation(resume.education, h.education)}\n ${renderVolunteer(resume.volunteer, h.volunteer)}\n ${renderCertificates(resume.certificates, h.certifications)}\n ${renderAdditional(resume, h.additional)}\n </div>\n</body>\n</html>`;\n}\n","export { render } from './render.js';\nexport type { ResumeSchema } from './types/resume.js';\n\n/** PDF rendering options for Puppeteer/Gotenberg. */\nexport const pdfRenderOptions = {\n mediaType: 'print' as const,\n format: 'A4' as const,\n margin: {\n top: '12mm',\n right: '14mm',\n bottom: '12mm',\n left: '14mm',\n },\n};\n"],"names":[],"mappings":"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"}
|
package/dist/index.umd.cjs
CHANGED
|
@@ -55,7 +55,8 @@
|
|
|
55
55
|
return String(text).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
56
56
|
}
|
|
57
57
|
function formatDate(dateStr) {
|
|
58
|
-
if (!dateStr) return "";
|
|
58
|
+
if (!dateStr) return "Present";
|
|
59
|
+
if (dateStr.toLowerCase() === "present") return "Present";
|
|
59
60
|
const parts = String(dateStr).split("-");
|
|
60
61
|
if (parts.length === 1) return parts[0];
|
|
61
62
|
const year = parts[0];
|
|
@@ -64,8 +65,8 @@
|
|
|
64
65
|
}
|
|
65
66
|
function dateRange(start, end) {
|
|
66
67
|
const s = formatDate(start);
|
|
67
|
-
const e =
|
|
68
|
-
if (!s) return "";
|
|
68
|
+
const e = formatDate(end);
|
|
69
|
+
if (!s || s === "Present") return "";
|
|
69
70
|
if (!e) return s;
|
|
70
71
|
return `${s} to ${e}`;
|
|
71
72
|
}
|
|
@@ -200,21 +201,21 @@
|
|
|
200
201
|
return /Tech-stack:\s/i.test(text) || /Client:\s/i.test(text);
|
|
201
202
|
}
|
|
202
203
|
function parseWorkMeta(summary) {
|
|
203
|
-
if (!summary) return { techStack: "", client: "" };
|
|
204
|
+
if (!summary) return { techStack: "", client: "", narrative: "" };
|
|
204
205
|
const text = stripHtml(summary).replace(/\n/g, " ").trim();
|
|
205
|
-
if (!isStructuredMeta(text)) return { techStack: "", client: "" };
|
|
206
|
-
|
|
206
|
+
if (!isStructuredMeta(text)) return { techStack: "", client: "", narrative: text };
|
|
207
|
+
let remaining = text;
|
|
208
|
+
const clientMatch = remaining.match(/Client:\s*(.+)$/i);
|
|
207
209
|
const client = clientMatch ? clientMatch[1].trim() : "";
|
|
208
|
-
|
|
209
|
-
const techMatch =
|
|
210
|
+
if (clientMatch) remaining = remaining.slice(0, clientMatch.index).trim();
|
|
211
|
+
const techMatch = remaining.match(/Tech-stack:\s*(.+)/i);
|
|
210
212
|
const techStack = techMatch ? techMatch[1].trim() : "";
|
|
211
|
-
|
|
213
|
+
if (techMatch) remaining = remaining.slice(0, techMatch.index).trim();
|
|
214
|
+
return { techStack, client, narrative: remaining };
|
|
212
215
|
}
|
|
213
216
|
function renderWorkEntry(entry) {
|
|
214
|
-
const { techStack, client } = parseWorkMeta(entry.summary);
|
|
217
|
+
const { techStack, client, narrative } = parseWorkMeta(entry.summary);
|
|
215
218
|
const duration = dateRange(entry.startDate, entry.endDate);
|
|
216
|
-
const plainSummary = entry.summary ? stripHtml(entry.summary).replace(/\n/g, " ").trim() : "";
|
|
217
|
-
const isNarrative = plainSummary && !isStructuredMeta(plainSummary);
|
|
218
219
|
return `
|
|
219
220
|
<div class="work-entry">
|
|
220
221
|
<div class="work-header">
|
|
@@ -226,7 +227,7 @@
|
|
|
226
227
|
<div class="work-tech">${techStack ? `Tech-stack: ${esc(techStack)}` : ""}</div>
|
|
227
228
|
<div class="work-client">${client ? `Client: ${esc(client)}` : ""}</div>
|
|
228
229
|
</div>` : ""}
|
|
229
|
-
${
|
|
230
|
+
${narrative ? `<p class="work-summary">${richText(narrative, { block: false })}</p>` : ""}
|
|
230
231
|
${has(entry.highlights) ? `
|
|
231
232
|
<ul class="work-highlights">
|
|
232
233
|
${entry.highlights.map((h) => `<li>${richText(h)}</li>`).join("\n ")}
|
|
@@ -275,10 +276,7 @@
|
|
|
275
276
|
<div class="edu-year">${esc(yearDisplay)}</div>
|
|
276
277
|
</div>
|
|
277
278
|
${entry.institution ? `<div class="edu-institution">${esc(instLine)}</div>` : ""}
|
|
278
|
-
${has(entry.courses) ?
|
|
279
|
-
<div class="edu-courses">
|
|
280
|
-
${entry.courses.map((c) => `<div>${richText(c)}</div>`).join("")}
|
|
281
|
-
</div>` : ""}
|
|
279
|
+
${has(entry.courses) ? `<div class="edu-courses">Coursework: ${entry.courses.map((c) => richText(c)).join(", ")}</div>` : ""}
|
|
282
280
|
</div>`;
|
|
283
281
|
}
|
|
284
282
|
function renderEducation(education, heading) {
|
package/dist/index.umd.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.umd.cjs","sources":["../src/constants.ts","../src/utils/escape.ts","../src/utils/dates.ts","../src/utils/icons.ts","../src/utils/sanitize.ts","../src/utils/text.ts","../src/sections/header.ts","../src/sections/shared.ts","../src/sections/summary.ts","../src/sections/skills.ts","../src/sections/work.ts","../src/sections/projects.ts","../src/sections/education.ts","../src/sections/volunteer.ts","../src/sections/certificates.ts","../src/sections/additional.ts","../src/render.ts","../src/index.ts"],"sourcesContent":["export const MONTHS: readonly string[] = [\n 'January',\n 'February',\n 'March',\n 'April',\n 'May',\n 'June',\n 'July',\n 'August',\n 'September',\n 'October',\n 'November',\n 'December',\n];\n\nexport const COUNTRY_NAMES: Readonly<Record<string, string>> = {\n GB: 'United Kingdom',\n US: 'United States',\n CA: 'Canada',\n AU: 'Australia',\n DE: 'Germany',\n FR: 'France',\n IN: 'India',\n JP: 'Japan',\n CN: 'China',\n BR: 'Brazil',\n NL: 'Netherlands',\n IT: 'Italy',\n ES: 'Spain',\n SE: 'Sweden',\n CH: 'Switzerland',\n SG: 'Singapore',\n NZ: 'New Zealand',\n IE: 'Ireland',\n AE: 'UAE',\n};\n\nexport const DEFAULT_HEADINGS: Readonly<Record<string, string>> = {\n summary: 'Summary',\n skills: 'Core Skills',\n experience: 'Experience',\n projects: 'Selected Projects',\n education: 'Education',\n volunteer: 'Leadership & Volunteering',\n certifications: 'Credentials',\n additional: 'Additional',\n};\n","/** Decode common HTML entities to literal characters. */\nexport function decodeEntities(text: string): string {\n return text\n .replace(/&#(\\d+);/g, (_, c) => String.fromCharCode(Number(c)))\n .replace(/&#x([0-9a-fA-F]+);/g, (_, h) => String.fromCharCode(parseInt(h, 16)))\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/"/g, '\"')\n .replace(/'/g, \"'\")\n .replace(/'/g, \"'\")\n .replace(/ /g, ' ')\n .replace(/—/g, '\\u2014')\n .replace(/–/g, '\\u2013')\n .replace(/…/g, '\\u2026');\n}\n\n/** HTML-escape for safe output (plain text fields like names, dates). */\nexport function esc(text: string | null | undefined): string {\n if (text == null) return '';\n return String(text)\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n","import { MONTHS, COUNTRY_NAMES } from '../constants.js';\n\nexport function formatDate(dateStr: string | null | undefined): string {\n if (!dateStr) return '';\n const parts = String(dateStr).split('-');\n if (parts.length === 1) return parts[0];\n const year = parts[0];\n const month = MONTHS[parseInt(parts[1], 10) - 1] || '';\n return `${month} ${year}`;\n}\n\nexport function dateRange(start: string | null | undefined, end: string | null | undefined): string {\n const s = formatDate(start);\n const e = end ? formatDate(end) : '';\n if (!s) return '';\n if (!e) return s;\n return `${s} to ${e}`;\n}\n\nexport function regionName(code: string | null | undefined): string {\n if (!code) return '';\n return COUNTRY_NAMES[code.toUpperCase()] || code;\n}\n","export function profileIcon(network: string | null | undefined): string {\n if (!network) return '<i class=\"fa-solid fa-link\"></i>';\n const n = network.toLowerCase();\n if (n === 'linkedin') return '<i class=\"fa-brands fa-linkedin-in\"></i>';\n if (n === 'github') return '<i class=\"fa-brands fa-github\"></i>';\n if (n === 'twitter' || n === 'x') return '<i class=\"fa-brands fa-x-twitter\"></i>';\n if (n === 'stackoverflow') return '<i class=\"fa-brands fa-stack-overflow\"></i>';\n return '<i class=\"fa-solid fa-link\"></i>';\n}\n","/** Tags allowed through sanitization — safe inline/block formatting. */\nconst BLOCK_TAGS = new Set(['p', 'ul', 'ol', 'li', 'blockquote']);\nconst INLINE_TAGS = new Set([\n 'br', 'b', 'strong', 'i', 'em', 'u', 's', 'mark', 'a', 'sub', 'sup', 'code',\n]);\nconst ALLOWED_TAGS = new Set([...BLOCK_TAGS, ...INLINE_TAGS]);\n\n/** URL schemes safe for <a href>. Everything else (javascript:, data:, vbscript:) is stripped. */\nconst SAFE_URL_SCHEMES = /^(?:https?|mailto|tel):/i;\nconst RELATIVE_URL = /^[#/?.]/;\n\nfunction isSafeUrl(href: string): boolean {\n const trimmed = href.trim();\n if (!trimmed) return false;\n return SAFE_URL_SCHEMES.test(trimmed) || RELATIVE_URL.test(trimmed);\n}\n\ninterface SanitizeOptions {\n readonly inline?: boolean;\n}\n\n/**\n * Sanitize HTML — keep allowed formatting tags, strip everything else.\n * Attributes are stripped from all tags except <a href> (safe schemes only) and <mark class>.\n * If inline=true, block tags (p, ul, ol, li, blockquote) are also stripped.\n */\nexport function sanitizeHtml(html: string | null | undefined, { inline = false }: SanitizeOptions = {}): string {\n if (!html) return '';\n const allowed = inline ? INLINE_TAGS : ALLOWED_TAGS;\n\n return String(html)\n .replace(/<a\\s+[^>]*href=\"([^\"]*)\"[^>]*>/gi, (_, href: string) => {\n if (!isSafeUrl(href)) return '';\n return `<a href=\"${href.replace(/\"/g, '"')}\" target=\"_blank\" rel=\"noopener\">`;\n })\n .replace(/<mark\\s+[^>]*class=\"([^\"]*)\"[^>]*>/gi, (_, cls: string) =>\n `<mark class=\"${cls.replace(/\"/g, '"')}\">`,\n )\n .replace(/<(\\/?)([a-z][a-z0-9]*)\\s*[^>]*?(\\/?)>/gi, (match, slash: string, tag: string, selfClose: string) => {\n const lower = tag.toLowerCase();\n if (lower === 'a' && !slash && match.includes('href=')) return match;\n if (lower === 'mark' && !slash && match.includes('class=')) return match;\n if (allowed.has(lower)) return `<${slash}${lower}${selfClose}>`;\n if (inline && BLOCK_TAGS.has(lower)) return slash ? ' ' : '';\n return '';\n })\n .replace(/<p>\\s*<\\/p>/gi, '')\n .replace(/\\s{2,}/g, ' ')\n .trim();\n}\n","import { decodeEntities, esc } from './escape.js';\nimport { sanitizeHtml } from './sanitize.js';\n\n/** Strip HTML tags → plain text (for fields parsed as data, not displayed). */\nexport function stripHtml(text: string | null | undefined): string {\n if (!text) return '';\n return decodeEntities(\n String(text)\n .replace(/<br\\s*\\/?>/gi, ' ')\n .replace(/<\\/?(p|div|li)[^>]*>/gi, ' ')\n .replace(/<[^>]+>/g, ''),\n )\n .replace(/\\s{2,}/g, ' ')\n .trim();\n}\n\nconst isHtml = (text: string): boolean => /<[a-z][\\s\\S]*>/i.test(text);\nconst hasEntities = (text: string): boolean => /&(?:amp|lt|gt|quot|#\\d+|#x[0-9a-f]+);/i.test(text);\n\n/**\n * Render user-provided rich text → safe HTML output.\n * block=true: preserve <p>, <ul>, <ol> etc.\n * block=false (default): inline mode — strip block tags, keep only inline formatting.\n */\nexport function richText(text: string | null | undefined, { block = false }: { block?: boolean } = {}): string {\n if (text == null) return '';\n const s = String(text);\n if (isHtml(s)) return sanitizeHtml(s, { inline: !block });\n if (hasEntities(s)) return esc(decodeEntities(s));\n return esc(s);\n}\n\n/** Check if an array is non-empty. */\nexport function has<T>(arr: readonly T[] | null | undefined): arr is readonly T[] & { length: number } {\n return Array.isArray(arr) && arr.length > 0;\n}\n","import type { ResumeBasics } from '../types/resume.js';\nimport { esc } from '../utils/escape.js';\nimport { regionName } from '../utils/dates.js';\nimport { profileIcon } from '../utils/icons.js';\nimport { has } from '../utils/text.js';\n\ninterface ContactLine {\n readonly icon: string;\n readonly text: string;\n}\n\nexport function renderHeader(basics: ResumeBasics | undefined): string {\n if (!basics) return '';\n\n const location = basics.location\n ? [basics.location.city, regionName(basics.location.region)].filter(Boolean).join(', ')\n : '';\n\n const lines: ContactLine[] = [];\n if (location)\n lines.push({ icon: '<i class=\"fa-solid fa-location-dot\"></i>', text: location });\n if (basics.phone)\n lines.push({ icon: '<i class=\"fa-solid fa-square-phone\"></i>', text: basics.phone });\n if (basics.email)\n lines.push({ icon: '<i class=\"fa-solid fa-envelope\"></i>', text: basics.email });\n if (basics.url) {\n const display = basics.url.replace(/^https?:\\/\\//, '');\n lines.push({ icon: '<i class=\"fa-solid fa-globe\"></i>', text: display });\n }\n if (has(basics.profiles)) {\n for (const p of basics.profiles) {\n lines.push({ icon: profileIcon(p.network), text: p.username || p.url || '' });\n }\n }\n\n return `\n <header class=\"header\">\n <div class=\"header-left\">\n <h1 class=\"name\">${esc(basics.name)}</h1>\n <div class=\"label\">${esc(basics.label)}</div>\n </div>\n <div class=\"contact-info\">\n ${lines.map((l) => `<div class=\"contact-line\">${esc(l.text)} ${l.icon}</div>`).join('\\n ')}\n </div>\n </header>\n <hr class=\"header-rule\" />`;\n}\n","import { esc } from '../utils/escape.js';\n\nexport function sectionTitle(title: string): string {\n return `<h2 class=\"section-title\">${esc(title)}</h2>`;\n}\n","import { richText } from '../utils/text.js';\nimport { sectionTitle } from './shared.js';\n\nexport function renderSummary(summary: string | undefined, heading: string): string {\n if (!summary) return '';\n const html = richText(summary, { block: true });\n if (!html) return '';\n return `\n ${sectionTitle(heading)}\n <div class=\"summary\">${html}</div>`;\n}\n","import type { ResumeSkill } from '../types/resume.js';\nimport { esc } from '../utils/escape.js';\nimport { has } from '../utils/text.js';\nimport { sectionTitle } from './shared.js';\n\nexport function renderSkills(skills: readonly ResumeSkill[] | undefined, heading: string): string {\n if (!has(skills)) return '';\n return `\n ${sectionTitle(heading)}\n <div class=\"section-body\">\n ${skills\n .map(\n (s) => `\n <div class=\"bullet-item\">\n <span class=\"skill-name\">${esc(s.name)}:</span> ${esc((s.keywords || []).join(', '))}\n </div>`,\n )\n .join('')}\n </div>`;\n}\n","import type { ResumeWorkEntry } from '../types/resume.js';\nimport { esc } from '../utils/escape.js';\nimport { dateRange } from '../utils/dates.js';\nimport { has, richText, stripHtml } from '../utils/text.js';\nimport { sectionTitle } from './shared.js';\n\n/** Check whether summary uses structured Tech-stack / Client format. */\nfunction isStructuredMeta(text: string): boolean {\n return /Tech-stack:\\s/i.test(text) || /Client:\\s/i.test(text);\n}\n\n/** Extract tech-stack and client from a structured work summary string. */\nfunction parseWorkMeta(summary: string | undefined): { techStack: string; client: string } {\n if (!summary) return { techStack: '', client: '' };\n const text = stripHtml(summary).replace(/\\n/g, ' ').trim();\n if (!isStructuredMeta(text)) return { techStack: '', client: '' };\n\n const clientMatch = text.match(/Client:\\s*(.+)$/i);\n const client = clientMatch ? clientMatch[1].trim() : '';\n\n const techPart = clientMatch ? text.slice(0, clientMatch.index).trim() : text;\n const techMatch = techPart.match(/Tech-stack:\\s*(.+)/i);\n const techStack = techMatch ? techMatch[1].trim() : '';\n\n return { techStack, client };\n}\n\nfunction renderWorkEntry(entry: ResumeWorkEntry): string {\n const { techStack, client } = parseWorkMeta(entry.summary);\n const duration = dateRange(entry.startDate, entry.endDate);\n const plainSummary = entry.summary ? stripHtml(entry.summary).replace(/\\n/g, ' ').trim() : '';\n const isNarrative = plainSummary && !isStructuredMeta(plainSummary);\n\n return `\n <div class=\"work-entry\">\n <div class=\"work-header\">\n <div class=\"work-title\">${esc(entry.name)} - ${esc(entry.position)}</div>\n <div class=\"work-duration\">${duration ? `Duration: ${esc(duration)}` : ''}</div>\n </div>\n ${\n techStack || client\n ? `\n <div class=\"work-meta\">\n <div class=\"work-tech\">${techStack ? `Tech-stack: ${esc(techStack)}` : ''}</div>\n <div class=\"work-client\">${client ? `Client: ${esc(client)}` : ''}</div>\n </div>`\n : ''\n }\n ${isNarrative ? `<p class=\"work-summary\">${richText(entry.summary, { block: false })}</p>` : ''}\n ${\n has(entry.highlights)\n ? `\n <ul class=\"work-highlights\">\n ${entry.highlights.map((h) => `<li>${richText(h)}</li>`).join('\\n ')}\n </ul>`\n : ''\n }\n </div>`;\n}\n\nexport function renderWork(work: readonly ResumeWorkEntry[] | undefined, heading: string): string {\n if (!has(work)) return '';\n return `\n ${sectionTitle(heading)}\n <div class=\"section-body\">\n ${work.map(renderWorkEntry).join('')}\n </div>`;\n}\n","import type { ResumeProject } from '../types/resume.js';\nimport { esc } from '../utils/escape.js';\nimport { has, richText } from '../utils/text.js';\nimport { sectionTitle } from './shared.js';\n\nexport function renderProjects(\n projects: readonly ResumeProject[] | undefined,\n heading: string,\n): string {\n if (!has(projects)) return '';\n return `\n ${sectionTitle(heading)}\n <div class=\"section-body\">\n ${projects\n .map((p) => {\n const desc = richText(p.description);\n return `\n <div class=\"bullet-item\">\n <span class=\"project-name\">${esc(p.name)}:</span> ${desc}\n </div>`;\n })\n .join('')}\n </div>`;\n}\n","import type { ResumeEducationEntry } from '../types/resume.js';\nimport { esc } from '../utils/escape.js';\nimport { formatDate } from '../utils/dates.js';\nimport { has, richText } from '../utils/text.js';\nimport { sectionTitle } from './shared.js';\n\nfunction renderEducationEntry(entry: ResumeEducationEntry): string {\n const degree = [entry.studyType, entry.area].filter(Boolean).join(' ');\n\n let yearDisplay = '';\n if (entry.endDate) {\n yearDisplay = formatDate(entry.endDate);\n } else if (entry.startDate) {\n const year = parseInt(String(entry.startDate).split('-')[0], 10);\n const currentYear = new Date().getFullYear();\n yearDisplay = year >= currentYear ? `Expected ${formatDate(entry.startDate)}` : formatDate(entry.startDate);\n }\n\n const instParts = [entry.institution];\n if (entry.score) instParts.push(entry.score);\n const instLine = instParts.filter(Boolean).join(' | ');\n\n return `\n <div class=\"edu-entry\">\n <div class=\"edu-header\">\n <div class=\"edu-degree\">${esc(degree)}</div>\n <div class=\"edu-year\">${esc(yearDisplay)}</div>\n </div>\n ${entry.institution ? `<div class=\"edu-institution\">${esc(instLine)}</div>` : ''}\n ${\n has(entry.courses)\n ? `\n <div class=\"edu-courses\">\n ${entry.courses.map((c) => `<div>${richText(c)}</div>`).join('')}\n </div>`\n : ''\n }\n </div>`;\n}\n\nexport function renderEducation(\n education: readonly ResumeEducationEntry[] | undefined,\n heading: string,\n): string {\n if (!has(education)) return '';\n return `\n ${sectionTitle(heading)}\n <div class=\"section-body\">\n ${education.map(renderEducationEntry).join('')}\n </div>`;\n}\n","import type { ResumeVolunteerEntry } from '../types/resume.js';\nimport { esc } from '../utils/escape.js';\nimport { has, richText } from '../utils/text.js';\nimport { sectionTitle } from './shared.js';\n\nexport function renderVolunteer(\n volunteer: readonly ResumeVolunteerEntry[] | undefined,\n heading: string,\n): string {\n if (!has(volunteer)) return '';\n return `\n ${sectionTitle(heading)}\n <div class=\"section-body\">\n ${volunteer\n .map((v) => {\n const years = v.endDate\n ? `${v.startDate}\\u2013${v.endDate}`\n : v.startDate || '';\n const title = v.position\n ? `${v.position} \\u2013 ${v.organization}`\n : v.organization;\n const summary = richText(v.summary);\n\n return `\n <div class=\"bullet-item volunteer-item\">\n <span class=\"vol-title\">${esc(title)}${years ? ` (${esc(years)})` : ''}:</span> ${summary}\n </div>`;\n })\n .join('')}\n </div>`;\n}\n","import type { ResumeCertificate } from '../types/resume.js';\nimport { has, richText } from '../utils/text.js';\nimport { sectionTitle } from './shared.js';\n\nexport function renderCertificates(\n certificates: readonly ResumeCertificate[] | undefined,\n heading: string,\n): string {\n if (!has(certificates)) return '';\n return `\n ${sectionTitle(heading)}\n <div class=\"section-body\">\n ${certificates\n .map(\n (c) => `\n <div class=\"bullet-item\">${richText(c.name)}</div>`,\n )\n .join('')}\n </div>`;\n}\n","import type { ResumeSchema } from '../types/resume.js';\nimport { esc } from '../utils/escape.js';\nimport { has } from '../utils/text.js';\nimport { sectionTitle } from './shared.js';\n\nexport function renderAdditional(resume: ResumeSchema, heading: string): string {\n const parts: string[] = [];\n\n if (has(resume.interests)) {\n for (const interest of resume.interests) {\n const keywords = has(interest.keywords) ? interest.keywords.join(', ') : '';\n const label = interest.name || 'Interests';\n if (keywords) {\n parts.push(\n `<div class=\"additional-line\"><span class=\"additional-label\">${esc(label)}:</span> ${esc(keywords)}</div>`,\n );\n }\n }\n }\n\n if (has(resume.languages)) {\n const langStr = resume.languages\n .map((l) => `${l.language}${l.fluency ? ` (${l.fluency})` : ''}`)\n .join(', ');\n parts.push(\n `<div class=\"additional-line\"><span class=\"additional-label\">Languages:</span> ${esc(langStr)}</div>`,\n );\n }\n\n if (has(resume.references)) {\n parts.push(\n `<div class=\"additional-line\"><span class=\"additional-label\">Referees:</span> Available on request</div>`,\n );\n }\n\n if (parts.length === 0) return '';\n\n return `\n ${sectionTitle(heading)}\n <div class=\"section-body\">\n ${parts.join('\\n ')}\n </div>`;\n}\n","import type { ResumeSchema } from './types/resume.js';\nimport { DEFAULT_HEADINGS } from './constants.js';\nimport { esc } from './utils/escape.js';\nimport { renderHeader } from './sections/header.js';\nimport { renderSummary } from './sections/summary.js';\nimport { renderSkills } from './sections/skills.js';\nimport { renderWork } from './sections/work.js';\nimport { renderProjects } from './sections/projects.js';\nimport { renderEducation } from './sections/education.js';\nimport { renderVolunteer } from './sections/volunteer.js';\nimport { renderCertificates } from './sections/certificates.js';\nimport { renderAdditional } from './sections/additional.js';\nimport css from './styles/academic.css?inline';\n\nexport function render(resume: ResumeSchema): string {\n const h = { ...DEFAULT_HEADINGS };\n\n const metaHeadings = resume?.meta?.headings;\n if (metaHeadings) {\n for (const [key, value] of Object.entries(metaHeadings)) {\n if (value && key in h) {\n h[key] = value;\n }\n }\n }\n\n const basics = resume?.basics || {};\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n <title>${esc(basics.name || 'Resume')}</title>\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\" />\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin />\n <link href=\"https://fonts.googleapis.com/css2?family=EB+Garamond:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500&display=swap\" rel=\"stylesheet\" />\n <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css\" />\n <style>${css}</style>\n</head>\n<body>\n <div class=\"resume\">\n ${renderHeader(basics)}\n ${renderSummary(basics.summary, h.summary)}\n ${renderSkills(resume.skills, h.skills)}\n ${renderWork(resume.work, h.experience)}\n ${renderProjects(resume.projects, h.projects)}\n ${renderEducation(resume.education, h.education)}\n ${renderVolunteer(resume.volunteer, h.volunteer)}\n ${renderCertificates(resume.certificates, h.certifications)}\n ${renderAdditional(resume, h.additional)}\n </div>\n</body>\n</html>`;\n}\n","export { render } from './render.js';\nexport type { ResumeSchema } from './types/resume.js';\n\n/** PDF rendering options for Puppeteer/Gotenberg. */\nexport const pdfRenderOptions = {\n mediaType: 'print' as const,\n format: 'A4' as const,\n margin: {\n top: '12mm',\n right: '14mm',\n bottom: '12mm',\n left: '14mm',\n },\n};\n"],"names":[],"mappings":";;;;AAAO,QAAM,SAA4B;AAAA,IACvC;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEO,QAAM,gBAAkD;AAAA,IAC7D,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,EACN;AAEO,QAAM,mBAAqD;AAAA,IAChE,SAAS;AAAA,IACT,QAAQ;AAAA,IACR,YAAY;AAAA,IACZ,UAAU;AAAA,IACV,WAAW;AAAA,IACX,WAAW;AAAA,IACX,gBAAgB;AAAA,IAChB,YAAY;AAAA,EACd;AC7CO,WAAS,eAAe,MAAsB;AACnD,WAAO,KACJ,QAAQ,aAAa,CAAC,GAAG,MAAM,OAAO,aAAa,OAAO,CAAC,CAAC,CAAC,EAC7D,QAAQ,uBAAuB,CAAC,GAAG,MAAM,OAAO,aAAa,SAAS,GAAG,EAAE,CAAC,CAAC,EAC7E,QAAQ,UAAU,GAAG,EACrB,QAAQ,SAAS,GAAG,EACpB,QAAQ,SAAS,GAAG,EACpB,QAAQ,WAAW,GAAG,EACtB,QAAQ,UAAU,GAAG,EACrB,QAAQ,WAAW,GAAG,EACtB,QAAQ,WAAW,GAAG,EACtB,QAAQ,YAAY,GAAQ,EAC5B,QAAQ,YAAY,GAAQ,EAC5B,QAAQ,aAAa,GAAQ;AAAA,EAClC;AAGO,WAAS,IAAI,MAAyC;AAC3D,QAAI,QAAQ,KAAM,QAAO;AACzB,WAAO,OAAO,IAAI,EACf,QAAQ,MAAM,OAAO,EACrB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,QAAQ,EACtB,QAAQ,MAAM,OAAO;AAAA,EAC1B;ACxBO,WAAS,WAAW,SAA4C;AACrE,QAAI,CAAC,QAAS,QAAO;AACrB,UAAM,QAAQ,OAAO,OAAO,EAAE,MAAM,GAAG;AACvC,QAAI,MAAM,WAAW,EAAG,QAAO,MAAM,CAAC;AACtC,UAAM,OAAO,MAAM,CAAC;AACpB,UAAM,QAAQ,OAAO,SAAS,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,KAAK;AACpD,WAAO,GAAG,KAAK,IAAI,IAAI;AAAA,EACzB;AAEO,WAAS,UAAU,OAAkC,KAAwC;AAClG,UAAM,IAAI,WAAW,KAAK;AAC1B,UAAM,IAAI,MAAM,WAAW,GAAG,IAAI;AAClC,QAAI,CAAC,EAAG,QAAO;AACf,QAAI,CAAC,EAAG,QAAO;AACf,WAAO,GAAG,CAAC,OAAO,CAAC;AAAA,EACrB;AAEO,WAAS,WAAW,MAAyC;AAClE,QAAI,CAAC,KAAM,QAAO;AAClB,WAAO,cAAc,KAAK,YAAA,CAAa,KAAK;AAAA,EAC9C;ACtBO,WAAS,YAAY,SAA4C;AACtE,QAAI,CAAC,QAAS,QAAO;AACrB,UAAM,IAAI,QAAQ,YAAA;AAClB,QAAI,MAAM,WAAY,QAAO;AAC7B,QAAI,MAAM,SAAU,QAAO;AAC3B,QAAI,MAAM,aAAa,MAAM,IAAK,QAAO;AACzC,QAAI,MAAM,gBAAiB,QAAO;AAClC,WAAO;AAAA,EACT;ACPA,QAAM,iCAAiB,IAAI,CAAC,KAAK,MAAM,MAAM,MAAM,YAAY,CAAC;AAChE,QAAM,kCAAkB,IAAI;AAAA,IAC1B;AAAA,IAAM;AAAA,IAAK;AAAA,IAAU;AAAA,IAAK;AAAA,IAAM;AAAA,IAAK;AAAA,IAAK;AAAA,IAAQ;AAAA,IAAK;AAAA,IAAO;AAAA,IAAO;AAAA,EACvE,CAAC;AACD,QAAM,mCAAmB,IAAI,CAAC,GAAG,YAAY,GAAG,WAAW,CAAC;AAG5D,QAAM,mBAAmB;AACzB,QAAM,eAAe;AAErB,WAAS,UAAU,MAAuB;AACxC,UAAM,UAAU,KAAK,KAAA;AACrB,QAAI,CAAC,QAAS,QAAO;AACrB,WAAO,iBAAiB,KAAK,OAAO,KAAK,aAAa,KAAK,OAAO;AAAA,EACpE;AAWO,WAAS,aAAa,MAAiC,EAAE,SAAS,MAAA,IAA2B,CAAA,GAAY;AAC9G,QAAI,CAAC,KAAM,QAAO;AAClB,UAAM,UAAU,SAAS,cAAc;AAEvC,WAAO,OAAO,IAAI,EACf,QAAQ,oCAAoC,CAAC,GAAG,SAAiB;AAChE,UAAI,CAAC,UAAU,IAAI,EAAG,QAAO;AAC7B,aAAO,YAAY,KAAK,QAAQ,MAAM,QAAQ,CAAC;AAAA,IACjD,CAAC,EACA;AAAA,MAAQ;AAAA,MAAwC,CAAC,GAAG,QACnD,gBAAgB,IAAI,QAAQ,MAAM,QAAQ,CAAC;AAAA,IAAA,EAE5C,QAAQ,2CAA2C,CAAC,OAAO,OAAe,KAAa,cAAsB;AAC5G,YAAM,QAAQ,IAAI,YAAA;AAClB,UAAI,UAAU,OAAO,CAAC,SAAS,MAAM,SAAS,OAAO,EAAG,QAAO;AAC/D,UAAI,UAAU,UAAU,CAAC,SAAS,MAAM,SAAS,QAAQ,EAAG,QAAO;AACnE,UAAI,QAAQ,IAAI,KAAK,EAAG,QAAO,IAAI,KAAK,GAAG,KAAK,GAAG,SAAS;AAC5D,UAAI,UAAU,WAAW,IAAI,KAAK,EAAG,QAAO,QAAQ,MAAM;AAC1D,aAAO;AAAA,IACT,CAAC,EACA,QAAQ,iBAAiB,EAAE,EAC3B,QAAQ,WAAW,GAAG,EACtB,KAAA;AAAA,EACL;AC7CO,WAAS,UAAU,MAAyC;AACjE,QAAI,CAAC,KAAM,QAAO;AAClB,WAAO;AAAA,MACL,OAAO,IAAI,EACR,QAAQ,gBAAgB,GAAG,EAC3B,QAAQ,0BAA0B,GAAG,EACrC,QAAQ,YAAY,EAAE;AAAA,IAAA,EAExB,QAAQ,WAAW,GAAG,EACtB,KAAA;AAAA,EACL;AAEA,QAAM,SAAS,CAAC,SAA0B,kBAAkB,KAAK,IAAI;AACrE,QAAM,cAAc,CAAC,SAA0B,yCAAyC,KAAK,IAAI;AAO1F,WAAS,SAAS,MAAiC,EAAE,QAAQ,MAAA,IAA+B,CAAA,GAAY;AAC7G,QAAI,QAAQ,KAAM,QAAO;AACzB,UAAM,IAAI,OAAO,IAAI;AACrB,QAAI,OAAO,CAAC,EAAG,QAAO,aAAa,GAAG,EAAE,QAAQ,CAAC,OAAO;AACxD,QAAI,YAAY,CAAC,UAAU,IAAI,eAAe,CAAC,CAAC;AAChD,WAAO,IAAI,CAAC;AAAA,EACd;AAGO,WAAS,IAAO,KAAgF;AACrG,WAAO,MAAM,QAAQ,GAAG,KAAK,IAAI,SAAS;AAAA,EAC5C;ACxBO,WAAS,aAAa,QAA0C;AACrE,QAAI,CAAC,OAAQ,QAAO;AAEpB,UAAM,WAAW,OAAO,WACpB,CAAC,OAAO,SAAS,MAAM,WAAW,OAAO,SAAS,MAAM,CAAC,EAAE,OAAO,OAAO,EAAE,KAAK,IAAI,IACpF;AAEJ,UAAM,QAAuB,CAAA;AAC7B,QAAI;AACF,YAAM,KAAK,EAAE,MAAM,4CAA4C,MAAM,UAAU;AACjF,QAAI,OAAO;AACT,YAAM,KAAK,EAAE,MAAM,4CAA4C,MAAM,OAAO,OAAO;AACrF,QAAI,OAAO;AACT,YAAM,KAAK,EAAE,MAAM,wCAAwC,MAAM,OAAO,OAAO;AACjF,QAAI,OAAO,KAAK;AACd,YAAM,UAAU,OAAO,IAAI,QAAQ,gBAAgB,EAAE;AACrD,YAAM,KAAK,EAAE,MAAM,qCAAqC,MAAM,SAAS;AAAA,IACzE;AACA,QAAI,IAAI,OAAO,QAAQ,GAAG;AACxB,iBAAW,KAAK,OAAO,UAAU;AAC/B,cAAM,KAAK,EAAE,MAAM,YAAY,EAAE,OAAO,GAAG,MAAM,EAAE,YAAY,EAAE,OAAO,IAAI;AAAA,MAC9E;AAAA,IACF;AAEA,WAAO;AAAA;AAAA;AAAA,2BAGkB,IAAI,OAAO,IAAI,CAAC;AAAA,6BACd,IAAI,OAAO,KAAK,CAAC;AAAA;AAAA;AAAA,UAGpC,MAAM,IAAI,CAAC,MAAM,6BAA6B,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,QAAQ,EAAE,KAAK,YAAY,CAAC;AAAA;AAAA;AAAA;AAAA,EAIzG;AC5CO,WAAS,aAAa,OAAuB;AAClD,WAAO,6BAA6B,IAAI,KAAK,CAAC;AAAA,EAChD;ACDO,WAAS,cAAc,SAA6B,SAAyB;AAClF,QAAI,CAAC,QAAS,QAAO;AACrB,UAAM,OAAO,SAAS,SAAS,EAAE,OAAO,MAAM;AAC9C,QAAI,CAAC,KAAM,QAAO;AAClB,WAAO;AAAA,MACH,aAAa,OAAO,CAAC;AAAA,2BACA,IAAI;AAAA,EAC/B;ACLO,WAAS,aAAa,QAA4C,SAAyB;AAChG,QAAI,CAAC,IAAI,MAAM,EAAG,QAAO;AACzB,WAAO;AAAA,MACH,aAAa,OAAO,CAAC;AAAA;AAAA,QAEnB,OACC;AAAA,MACC,CAAC,MAAM;AAAA;AAAA,mCAEkB,IAAI,EAAE,IAAI,CAAC,YAAY,KAAK,EAAE,YAAY,CAAA,GAAI,KAAK,IAAI,CAAC,CAAC;AAAA;AAAA,IAAA,EAGnF,KAAK,EAAE,CAAC;AAAA;AAAA,EAEjB;ACZA,WAAS,iBAAiB,MAAuB;AAC/C,WAAO,iBAAiB,KAAK,IAAI,KAAK,aAAa,KAAK,IAAI;AAAA,EAC9D;AAGA,WAAS,cAAc,SAAoE;AACzF,QAAI,CAAC,QAAS,QAAO,EAAE,WAAW,IAAI,QAAQ,GAAA;AAC9C,UAAM,OAAO,UAAU,OAAO,EAAE,QAAQ,OAAO,GAAG,EAAE,KAAA;AACpD,QAAI,CAAC,iBAAiB,IAAI,UAAU,EAAE,WAAW,IAAI,QAAQ,GAAA;AAE7D,UAAM,cAAc,KAAK,MAAM,kBAAkB;AACjD,UAAM,SAAS,cAAc,YAAY,CAAC,EAAE,SAAS;AAErD,UAAM,WAAW,cAAc,KAAK,MAAM,GAAG,YAAY,KAAK,EAAE,KAAA,IAAS;AACzE,UAAM,YAAY,SAAS,MAAM,qBAAqB;AACtD,UAAM,YAAY,YAAY,UAAU,CAAC,EAAE,SAAS;AAEpD,WAAO,EAAE,WAAW,OAAA;AAAA,EACtB;AAEA,WAAS,gBAAgB,OAAgC;AACvD,UAAM,EAAE,WAAW,OAAA,IAAW,cAAc,MAAM,OAAO;AACzD,UAAM,WAAW,UAAU,MAAM,WAAW,MAAM,OAAO;AACzD,UAAM,eAAe,MAAM,UAAU,UAAU,MAAM,OAAO,EAAE,QAAQ,OAAO,GAAG,EAAE,KAAA,IAAS;AAC3F,UAAM,cAAc,gBAAgB,CAAC,iBAAiB,YAAY;AAElE,WAAO;AAAA;AAAA;AAAA,kCAGyB,IAAI,MAAM,IAAI,CAAC,MAAM,IAAI,MAAM,QAAQ,CAAC;AAAA,qCACrC,WAAW,aAAa,IAAI,QAAQ,CAAC,KAAK,EAAE;AAAA;AAAA,QAGzE,aAAa,SACT;AAAA;AAAA,iCAEqB,YAAY,eAAe,IAAI,SAAS,CAAC,KAAK,EAAE;AAAA,mCAC9C,SAAS,WAAW,IAAI,MAAM,CAAC,KAAK,EAAE;AAAA,gBAE7D,EACN;AAAA,QACE,cAAc,2BAA2B,SAAS,MAAM,SAAS,EAAE,OAAO,MAAA,CAAO,CAAC,SAAS,EAAE;AAAA,QAE7F,IAAI,MAAM,UAAU,IAChB;AAAA;AAAA,UAEF,MAAM,WAAW,IAAI,CAAC,MAAM,OAAO,SAAS,CAAC,CAAC,OAAO,EAAE,KAAK,YAAY,CAAC;AAAA,eAEvE,EACN;AAAA;AAAA,EAEN;AAEO,WAAS,WAAW,MAA8C,SAAyB;AAChG,QAAI,CAAC,IAAI,IAAI,EAAG,QAAO;AACvB,WAAO;AAAA,MACH,aAAa,OAAO,CAAC;AAAA;AAAA,QAEnB,KAAK,IAAI,eAAe,EAAE,KAAK,EAAE,CAAC;AAAA;AAAA,EAE1C;AC9DO,WAAS,eACd,UACA,SACQ;AACR,QAAI,CAAC,IAAI,QAAQ,EAAG,QAAO;AAC3B,WAAO;AAAA,MACH,aAAa,OAAO,CAAC;AAAA;AAAA,QAEnB,SACC,IAAI,CAAC,MAAM;AACV,YAAM,OAAO,SAAS,EAAE,WAAW;AACnC,aAAO;AAAA;AAAA,qCAEoB,IAAI,EAAE,IAAI,CAAC,YAAY,IAAI;AAAA;AAAA,IAExD,CAAC,EACA,KAAK,EAAE,CAAC;AAAA;AAAA,EAEjB;ACjBA,WAAS,qBAAqB,OAAqC;AACjE,UAAM,SAAS,CAAC,MAAM,WAAW,MAAM,IAAI,EAAE,OAAO,OAAO,EAAE,KAAK,GAAG;AAErE,QAAI,cAAc;AAClB,QAAI,MAAM,SAAS;AACjB,oBAAc,WAAW,MAAM,OAAO;AAAA,IACxC,WAAW,MAAM,WAAW;AAC1B,YAAM,OAAO,SAAS,OAAO,MAAM,SAAS,EAAE,MAAM,GAAG,EAAE,CAAC,GAAG,EAAE;AAC/D,YAAM,eAAc,oBAAI,KAAA,GAAO,YAAA;AAC/B,oBAAc,QAAQ,cAAc,YAAY,WAAW,MAAM,SAAS,CAAC,KAAK,WAAW,MAAM,SAAS;AAAA,IAC5G;AAEA,UAAM,YAAY,CAAC,MAAM,WAAW;AACpC,QAAI,MAAM,MAAO,WAAU,KAAK,MAAM,KAAK;AAC3C,UAAM,WAAW,UAAU,OAAO,OAAO,EAAE,KAAK,KAAK;AAErD,WAAO;AAAA;AAAA;AAAA,kCAGyB,IAAI,MAAM,CAAC;AAAA,gCACb,IAAI,WAAW,CAAC;AAAA;AAAA,QAExC,MAAM,cAAc,gCAAgC,IAAI,QAAQ,CAAC,WAAW,EAAE;AAAA,QAE9E,IAAI,MAAM,OAAO,IACb;AAAA;AAAA,UAEF,MAAM,QAAQ,IAAI,CAAC,MAAM,QAAQ,SAAS,CAAC,CAAC,QAAQ,EAAE,KAAK,EAAE,CAAC;AAAA,gBAE5D,EACN;AAAA;AAAA,EAEN;AAEO,WAAS,gBACd,WACA,SACQ;AACR,QAAI,CAAC,IAAI,SAAS,EAAG,QAAO;AAC5B,WAAO;AAAA,MACH,aAAa,OAAO,CAAC;AAAA;AAAA,QAEnB,UAAU,IAAI,oBAAoB,EAAE,KAAK,EAAE,CAAC;AAAA;AAAA,EAEpD;AC7CO,WAAS,gBACd,WACA,SACQ;AACR,QAAI,CAAC,IAAI,SAAS,EAAG,QAAO;AAC5B,WAAO;AAAA,MACH,aAAa,OAAO,CAAC;AAAA;AAAA,QAEnB,UACC,IAAI,CAAC,MAAM;AACV,YAAM,QAAQ,EAAE,UACZ,GAAG,EAAE,SAAS,IAAS,EAAE,OAAO,KAChC,EAAE,aAAa;AACnB,YAAM,QAAQ,EAAE,WACZ,GAAG,EAAE,QAAQ,MAAW,EAAE,YAAY,KACtC,EAAE;AACN,YAAM,UAAU,SAAS,EAAE,OAAO;AAElC,aAAO;AAAA;AAAA,kCAEiB,IAAI,KAAK,CAAC,GAAG,QAAQ,KAAK,IAAI,KAAK,CAAC,MAAM,EAAE,YAAY,OAAO;AAAA;AAAA,IAEzF,CAAC,EACA,KAAK,EAAE,CAAC;AAAA;AAAA,EAEjB;AC1BO,WAAS,mBACd,cACA,SACQ;AACR,QAAI,CAAC,IAAI,YAAY,EAAG,QAAO;AAC/B,WAAO;AAAA,MACH,aAAa,OAAO,CAAC;AAAA;AAAA,QAEnB,aACC;AAAA,MACC,CAAC,MAAM;AAAA,iCACgB,SAAS,EAAE,IAAI,CAAC;AAAA,IAAA,EAExC,KAAK,EAAE,CAAC;AAAA;AAAA,EAEjB;ACdO,WAAS,iBAAiB,QAAsB,SAAyB;AAC9E,UAAM,QAAkB,CAAA;AAExB,QAAI,IAAI,OAAO,SAAS,GAAG;AACzB,iBAAW,YAAY,OAAO,WAAW;AACvC,cAAM,WAAW,IAAI,SAAS,QAAQ,IAAI,SAAS,SAAS,KAAK,IAAI,IAAI;AACzE,cAAM,QAAQ,SAAS,QAAQ;AAC/B,YAAI,UAAU;AACZ,gBAAM;AAAA,YACJ,+DAA+D,IAAI,KAAK,CAAC,YAAY,IAAI,QAAQ,CAAC;AAAA,UAAA;AAAA,QAEtG;AAAA,MACF;AAAA,IACF;AAEA,QAAI,IAAI,OAAO,SAAS,GAAG;AACzB,YAAM,UAAU,OAAO,UACpB,IAAI,CAAC,MAAM,GAAG,EAAE,QAAQ,GAAG,EAAE,UAAU,KAAK,EAAE,OAAO,MAAM,EAAE,EAAE,EAC/D,KAAK,IAAI;AACZ,YAAM;AAAA,QACJ,iFAAiF,IAAI,OAAO,CAAC;AAAA,MAAA;AAAA,IAEjG;AAEA,QAAI,IAAI,OAAO,UAAU,GAAG;AAC1B,YAAM;AAAA,QACJ;AAAA,MAAA;AAAA,IAEJ;AAEA,QAAI,MAAM,WAAW,EAAG,QAAO;AAE/B,WAAO;AAAA,MACH,aAAa,OAAO,CAAC;AAAA;AAAA,QAEnB,MAAM,KAAK,UAAU,CAAC;AAAA;AAAA,EAE9B;;AC5BO,WAAS,OAAO,QAA8B;;AACnD,UAAM,IAAI,EAAE,GAAG,iBAAA;AAEf,UAAM,gBAAe,sCAAQ,SAAR,mBAAc;AACnC,QAAI,cAAc;AAChB,iBAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,YAAY,GAAG;AACvD,YAAI,SAAS,OAAO,GAAG;AACrB,YAAE,GAAG,IAAI;AAAA,QACX;AAAA,MACF;AAAA,IACF;AAEA,UAAM,UAAS,iCAAQ,WAAU,CAAA;AAEjC,WAAO;AAAA;AAAA;AAAA;AAAA;AAAA,WAKE,IAAI,OAAO,QAAQ,QAAQ,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA,WAK5B,GAAG;AAAA;AAAA;AAAA;AAAA,MAIR,aAAa,MAAM,CAAC;AAAA,MACpB,cAAc,OAAO,SAAS,EAAE,OAAO,CAAC;AAAA,MACxC,aAAa,OAAO,QAAQ,EAAE,MAAM,CAAC;AAAA,MACrC,WAAW,OAAO,MAAM,EAAE,UAAU,CAAC;AAAA,MACrC,eAAe,OAAO,UAAU,EAAE,QAAQ,CAAC;AAAA,MAC3C,gBAAgB,OAAO,WAAW,EAAE,SAAS,CAAC;AAAA,MAC9C,gBAAgB,OAAO,WAAW,EAAE,SAAS,CAAC;AAAA,MAC9C,mBAAmB,OAAO,cAAc,EAAE,cAAc,CAAC;AAAA,MACzD,iBAAiB,QAAQ,EAAE,UAAU,CAAC;AAAA;AAAA;AAAA;AAAA,EAI5C;AClDO,QAAM,mBAAmB;AAAA,IAC9B,WAAW;AAAA,IACX,QAAQ;AAAA,IACR,QAAQ;AAAA,MACN,KAAK;AAAA,MACL,OAAO;AAAA,MACP,QAAQ;AAAA,MACR,MAAM;AAAA,IAAA;AAAA,EAEV;;;;;"}
|
|
1
|
+
{"version":3,"file":"index.umd.cjs","sources":["../src/constants.ts","../src/utils/escape.ts","../src/utils/dates.ts","../src/utils/icons.ts","../src/utils/sanitize.ts","../src/utils/text.ts","../src/sections/header.ts","../src/sections/shared.ts","../src/sections/summary.ts","../src/sections/skills.ts","../src/sections/work.ts","../src/sections/projects.ts","../src/sections/education.ts","../src/sections/volunteer.ts","../src/sections/certificates.ts","../src/sections/additional.ts","../src/render.ts","../src/index.ts"],"sourcesContent":["export const MONTHS: readonly string[] = [\n 'January',\n 'February',\n 'March',\n 'April',\n 'May',\n 'June',\n 'July',\n 'August',\n 'September',\n 'October',\n 'November',\n 'December',\n];\n\nexport const COUNTRY_NAMES: Readonly<Record<string, string>> = {\n GB: 'United Kingdom',\n US: 'United States',\n CA: 'Canada',\n AU: 'Australia',\n DE: 'Germany',\n FR: 'France',\n IN: 'India',\n JP: 'Japan',\n CN: 'China',\n BR: 'Brazil',\n NL: 'Netherlands',\n IT: 'Italy',\n ES: 'Spain',\n SE: 'Sweden',\n CH: 'Switzerland',\n SG: 'Singapore',\n NZ: 'New Zealand',\n IE: 'Ireland',\n AE: 'UAE',\n};\n\nexport const DEFAULT_HEADINGS: Readonly<Record<string, string>> = {\n summary: 'Summary',\n skills: 'Core Skills',\n experience: 'Experience',\n projects: 'Selected Projects',\n education: 'Education',\n volunteer: 'Leadership & Volunteering',\n certifications: 'Credentials',\n additional: 'Additional',\n};\n","/** Decode common HTML entities to literal characters. */\nexport function decodeEntities(text: string): string {\n return text\n .replace(/&#(\\d+);/g, (_, c) => String.fromCharCode(Number(c)))\n .replace(/&#x([0-9a-fA-F]+);/g, (_, h) => String.fromCharCode(parseInt(h, 16)))\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/"/g, '\"')\n .replace(/'/g, \"'\")\n .replace(/'/g, \"'\")\n .replace(/ /g, ' ')\n .replace(/—/g, '\\u2014')\n .replace(/–/g, '\\u2013')\n .replace(/…/g, '\\u2026');\n}\n\n/** HTML-escape for safe output (plain text fields like names, dates). */\nexport function esc(text: string | null | undefined): string {\n if (text == null) return '';\n return String(text)\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n","import { MONTHS, COUNTRY_NAMES } from '../constants.js';\n\nexport function formatDate(dateStr: string | null | undefined): string {\n if (!dateStr) return 'Present';\n if (dateStr.toLowerCase() === 'present') return 'Present';\n const parts = String(dateStr).split('-');\n if (parts.length === 1) return parts[0];\n const year = parts[0];\n const month = MONTHS[parseInt(parts[1], 10) - 1] || '';\n return `${month} ${year}`;\n}\n\nexport function dateRange(start: string | null | undefined, end: string | null | undefined): string {\n const s = formatDate(start);\n const e = formatDate(end);\n if (!s || s === 'Present') return ''; // start date should not be empty or present\n if (!e) return s;\n return `${s} to ${e}`;\n}\n\nexport function regionName(code: string | null | undefined): string {\n if (!code) return '';\n return COUNTRY_NAMES[code.toUpperCase()] || code;\n}\n","export function profileIcon(network: string | null | undefined): string {\n if (!network) return '<i class=\"fa-solid fa-link\"></i>';\n const n = network.toLowerCase();\n if (n === 'linkedin') return '<i class=\"fa-brands fa-linkedin-in\"></i>';\n if (n === 'github') return '<i class=\"fa-brands fa-github\"></i>';\n if (n === 'twitter' || n === 'x') return '<i class=\"fa-brands fa-x-twitter\"></i>';\n if (n === 'stackoverflow') return '<i class=\"fa-brands fa-stack-overflow\"></i>';\n return '<i class=\"fa-solid fa-link\"></i>';\n}\n","/** Tags allowed through sanitization — safe inline/block formatting. */\nconst BLOCK_TAGS = new Set(['p', 'ul', 'ol', 'li', 'blockquote']);\nconst INLINE_TAGS = new Set([\n 'br', 'b', 'strong', 'i', 'em', 'u', 's', 'mark', 'a', 'sub', 'sup', 'code',\n]);\nconst ALLOWED_TAGS = new Set([...BLOCK_TAGS, ...INLINE_TAGS]);\n\n/** URL schemes safe for <a href>. Everything else (javascript:, data:, vbscript:) is stripped. */\nconst SAFE_URL_SCHEMES = /^(?:https?|mailto|tel):/i;\nconst RELATIVE_URL = /^[#/?.]/;\n\nfunction isSafeUrl(href: string): boolean {\n const trimmed = href.trim();\n if (!trimmed) return false;\n return SAFE_URL_SCHEMES.test(trimmed) || RELATIVE_URL.test(trimmed);\n}\n\ninterface SanitizeOptions {\n readonly inline?: boolean;\n}\n\n/**\n * Sanitize HTML — keep allowed formatting tags, strip everything else.\n * Attributes are stripped from all tags except <a href> (safe schemes only) and <mark class>.\n * If inline=true, block tags (p, ul, ol, li, blockquote) are also stripped.\n */\nexport function sanitizeHtml(html: string | null | undefined, { inline = false }: SanitizeOptions = {}): string {\n if (!html) return '';\n const allowed = inline ? INLINE_TAGS : ALLOWED_TAGS;\n\n return String(html)\n .replace(/<a\\s+[^>]*href=\"([^\"]*)\"[^>]*>/gi, (_, href: string) => {\n if (!isSafeUrl(href)) return '';\n return `<a href=\"${href.replace(/\"/g, '"')}\" target=\"_blank\" rel=\"noopener\">`;\n })\n .replace(/<mark\\s+[^>]*class=\"([^\"]*)\"[^>]*>/gi, (_, cls: string) =>\n `<mark class=\"${cls.replace(/\"/g, '"')}\">`,\n )\n .replace(/<(\\/?)([a-z][a-z0-9]*)\\s*[^>]*?(\\/?)>/gi, (match, slash: string, tag: string, selfClose: string) => {\n const lower = tag.toLowerCase();\n if (lower === 'a' && !slash && match.includes('href=')) return match;\n if (lower === 'mark' && !slash && match.includes('class=')) return match;\n if (allowed.has(lower)) return `<${slash}${lower}${selfClose}>`;\n if (inline && BLOCK_TAGS.has(lower)) return slash ? ' ' : '';\n return '';\n })\n .replace(/<p>\\s*<\\/p>/gi, '')\n .replace(/\\s{2,}/g, ' ')\n .trim();\n}\n","import { decodeEntities, esc } from './escape.js';\nimport { sanitizeHtml } from './sanitize.js';\n\n/** Strip HTML tags → plain text (for fields parsed as data, not displayed). */\nexport function stripHtml(text: string | null | undefined): string {\n if (!text) return '';\n return decodeEntities(\n String(text)\n .replace(/<br\\s*\\/?>/gi, ' ')\n .replace(/<\\/?(p|div|li)[^>]*>/gi, ' ')\n .replace(/<[^>]+>/g, ''),\n )\n .replace(/\\s{2,}/g, ' ')\n .trim();\n}\n\nconst isHtml = (text: string): boolean => /<[a-z][\\s\\S]*>/i.test(text);\nconst hasEntities = (text: string): boolean => /&(?:amp|lt|gt|quot|#\\d+|#x[0-9a-f]+);/i.test(text);\n\n/**\n * Render user-provided rich text → safe HTML output.\n * block=true: preserve <p>, <ul>, <ol> etc.\n * block=false (default): inline mode — strip block tags, keep only inline formatting.\n */\nexport function richText(text: string | null | undefined, { block = false }: { block?: boolean } = {}): string {\n if (text == null) return '';\n const s = String(text);\n if (isHtml(s)) return sanitizeHtml(s, { inline: !block });\n if (hasEntities(s)) return esc(decodeEntities(s));\n return esc(s);\n}\n\n/** Check if an array is non-empty. */\nexport function has<T>(arr: readonly T[] | null | undefined): arr is readonly T[] & { length: number } {\n return Array.isArray(arr) && arr.length > 0;\n}\n","import type { ResumeBasics } from '../types/resume.js';\nimport { esc } from '../utils/escape.js';\nimport { regionName } from '../utils/dates.js';\nimport { profileIcon } from '../utils/icons.js';\nimport { has } from '../utils/text.js';\n\ninterface ContactLine {\n readonly icon: string;\n readonly text: string;\n}\n\nexport function renderHeader(basics: ResumeBasics | undefined): string {\n if (!basics) return '';\n\n const location = basics.location\n ? [basics.location.city, regionName(basics.location.region)].filter(Boolean).join(', ')\n : '';\n\n const lines: ContactLine[] = [];\n if (location)\n lines.push({ icon: '<i class=\"fa-solid fa-location-dot\"></i>', text: location });\n if (basics.phone)\n lines.push({ icon: '<i class=\"fa-solid fa-square-phone\"></i>', text: basics.phone });\n if (basics.email)\n lines.push({ icon: '<i class=\"fa-solid fa-envelope\"></i>', text: basics.email });\n if (basics.url) {\n const display = basics.url.replace(/^https?:\\/\\//, '');\n lines.push({ icon: '<i class=\"fa-solid fa-globe\"></i>', text: display });\n }\n if (has(basics.profiles)) {\n for (const p of basics.profiles) {\n lines.push({ icon: profileIcon(p.network), text: p.username || p.url || '' });\n }\n }\n\n return `\n <header class=\"header\">\n <div class=\"header-left\">\n <h1 class=\"name\">${esc(basics.name)}</h1>\n <div class=\"label\">${esc(basics.label)}</div>\n </div>\n <div class=\"contact-info\">\n ${lines.map((l) => `<div class=\"contact-line\">${esc(l.text)} ${l.icon}</div>`).join('\\n ')}\n </div>\n </header>\n <hr class=\"header-rule\" />`;\n}\n","import { esc } from '../utils/escape.js';\n\nexport function sectionTitle(title: string): string {\n return `<h2 class=\"section-title\">${esc(title)}</h2>`;\n}\n","import { richText } from '../utils/text.js';\nimport { sectionTitle } from './shared.js';\n\nexport function renderSummary(summary: string | undefined, heading: string): string {\n if (!summary) return '';\n const html = richText(summary, { block: true });\n if (!html) return '';\n return `\n ${sectionTitle(heading)}\n <div class=\"summary\">${html}</div>`;\n}\n","import type { ResumeSkill } from '../types/resume.js';\nimport { esc } from '../utils/escape.js';\nimport { has } from '../utils/text.js';\nimport { sectionTitle } from './shared.js';\n\nexport function renderSkills(skills: readonly ResumeSkill[] | undefined, heading: string): string {\n if (!has(skills)) return '';\n return `\n ${sectionTitle(heading)}\n <div class=\"section-body\">\n ${skills\n .map(\n (s) => `\n <div class=\"bullet-item\">\n <span class=\"skill-name\">${esc(s.name)}:</span> ${esc((s.keywords || []).join(', '))}\n </div>`,\n )\n .join('')}\n </div>`;\n}\n","import type { ResumeWorkEntry } from '../types/resume.js';\nimport { esc } from '../utils/escape.js';\nimport { dateRange } from '../utils/dates.js';\nimport { has, richText, stripHtml } from '../utils/text.js';\nimport { sectionTitle } from './shared.js';\n\n/** Check whether summary uses structured Tech-stack / Client format. */\nfunction isStructuredMeta(text: string): boolean {\n return /Tech-stack:\\s/i.test(text) || /Client:\\s/i.test(text);\n}\n\n/** Extract tech-stack, client, and any remaining narrative from a work summary. */\nfunction parseWorkMeta(summary: string | undefined): {\n techStack: string;\n client: string;\n narrative: string;\n} {\n if (!summary) return { techStack: '', client: '', narrative: '' };\n const text = stripHtml(summary).replace(/\\n/g, ' ').trim();\n if (!isStructuredMeta(text)) return { techStack: '', client: '', narrative: text };\n\n let remaining = text;\n\n const clientMatch = remaining.match(/Client:\\s*(.+)$/i);\n const client = clientMatch ? clientMatch[1].trim() : '';\n if (clientMatch) remaining = remaining.slice(0, clientMatch.index).trim();\n\n const techMatch = remaining.match(/Tech-stack:\\s*(.+)/i);\n const techStack = techMatch ? techMatch[1].trim() : '';\n if (techMatch) remaining = remaining.slice(0, techMatch.index).trim();\n\n return { techStack, client, narrative: remaining };\n}\n\nfunction renderWorkEntry(entry: ResumeWorkEntry): string {\n const { techStack, client, narrative } = parseWorkMeta(entry.summary);\n const duration = dateRange(entry.startDate, entry.endDate);\n\n return `\n <div class=\"work-entry\">\n <div class=\"work-header\">\n <div class=\"work-title\">${esc(entry.name)} - ${esc(entry.position)}</div>\n <div class=\"work-duration\">${duration ? `Duration: ${esc(duration)}` : ''}</div>\n </div>\n ${\n techStack || client\n ? `\n <div class=\"work-meta\">\n <div class=\"work-tech\">${techStack ? `Tech-stack: ${esc(techStack)}` : ''}</div>\n <div class=\"work-client\">${client ? `Client: ${esc(client)}` : ''}</div>\n </div>`\n : ''\n }\n ${narrative ? `<p class=\"work-summary\">${richText(narrative, { block: false })}</p>` : ''}\n ${\n has(entry.highlights)\n ? `\n <ul class=\"work-highlights\">\n ${entry.highlights.map((h) => `<li>${richText(h)}</li>`).join('\\n ')}\n </ul>`\n : ''\n }\n </div>`;\n}\n\nexport function renderWork(work: readonly ResumeWorkEntry[] | undefined, heading: string): string {\n if (!has(work)) return '';\n return `\n ${sectionTitle(heading)}\n <div class=\"section-body\">\n ${work.map(renderWorkEntry).join('')}\n </div>`;\n}\n","import type { ResumeProject } from '../types/resume.js';\nimport { esc } from '../utils/escape.js';\nimport { has, richText } from '../utils/text.js';\nimport { sectionTitle } from './shared.js';\n\nexport function renderProjects(\n projects: readonly ResumeProject[] | undefined,\n heading: string,\n): string {\n if (!has(projects)) return '';\n return `\n ${sectionTitle(heading)}\n <div class=\"section-body\">\n ${projects\n .map((p) => {\n const desc = richText(p.description);\n return `\n <div class=\"bullet-item\">\n <span class=\"project-name\">${esc(p.name)}:</span> ${desc}\n </div>`;\n })\n .join('')}\n </div>`;\n}\n","import type { ResumeEducationEntry } from '../types/resume.js';\nimport { esc } from '../utils/escape.js';\nimport { formatDate } from '../utils/dates.js';\nimport { has, richText } from '../utils/text.js';\nimport { sectionTitle } from './shared.js';\n\nfunction renderEducationEntry(entry: ResumeEducationEntry): string {\n const degree = [entry.studyType, entry.area].filter(Boolean).join(' ');\n\n let yearDisplay = '';\n if (entry.endDate) {\n yearDisplay = formatDate(entry.endDate);\n } else if (entry.startDate) {\n const year = parseInt(String(entry.startDate).split('-')[0], 10);\n const currentYear = new Date().getFullYear();\n yearDisplay = year >= currentYear ? `Expected ${formatDate(entry.startDate)}` : formatDate(entry.startDate);\n }\n\n const instParts = [entry.institution];\n if (entry.score) instParts.push(entry.score);\n const instLine = instParts.filter(Boolean).join(' | ');\n\n return `\n <div class=\"edu-entry\">\n <div class=\"edu-header\">\n <div class=\"edu-degree\">${esc(degree)}</div>\n <div class=\"edu-year\">${esc(yearDisplay)}</div>\n </div>\n ${entry.institution ? `<div class=\"edu-institution\">${esc(instLine)}</div>` : ''}\n ${\n has(entry.courses)\n ? `<div class=\"edu-courses\">Coursework: ${entry.courses.map((c) => richText(c)).join(', ')}</div>`\n : ''\n }\n </div>`;\n}\n\nexport function renderEducation(\n education: readonly ResumeEducationEntry[] | undefined,\n heading: string,\n): string {\n if (!has(education)) return '';\n return `\n ${sectionTitle(heading)}\n <div class=\"section-body\">\n ${education.map(renderEducationEntry).join('')}\n </div>`;\n}\n","import type { ResumeVolunteerEntry } from '../types/resume.js';\nimport { esc } from '../utils/escape.js';\nimport { has, richText } from '../utils/text.js';\nimport { sectionTitle } from './shared.js';\n\nexport function renderVolunteer(\n volunteer: readonly ResumeVolunteerEntry[] | undefined,\n heading: string,\n): string {\n if (!has(volunteer)) return '';\n return `\n ${sectionTitle(heading)}\n <div class=\"section-body\">\n ${volunteer\n .map((v) => {\n const years = v.endDate\n ? `${v.startDate}\\u2013${v.endDate}`\n : v.startDate || '';\n const title = v.position\n ? `${v.position} \\u2013 ${v.organization}`\n : v.organization;\n const summary = richText(v.summary);\n\n return `\n <div class=\"bullet-item volunteer-item\">\n <span class=\"vol-title\">${esc(title)}${years ? ` (${esc(years)})` : ''}:</span> ${summary}\n </div>`;\n })\n .join('')}\n </div>`;\n}\n","import type { ResumeCertificate } from '../types/resume.js';\nimport { has, richText } from '../utils/text.js';\nimport { sectionTitle } from './shared.js';\n\nexport function renderCertificates(\n certificates: readonly ResumeCertificate[] | undefined,\n heading: string,\n): string {\n if (!has(certificates)) return '';\n return `\n ${sectionTitle(heading)}\n <div class=\"section-body\">\n ${certificates\n .map(\n (c) => `\n <div class=\"bullet-item\">${richText(c.name)}</div>`,\n )\n .join('')}\n </div>`;\n}\n","import type { ResumeSchema } from '../types/resume.js';\nimport { esc } from '../utils/escape.js';\nimport { has } from '../utils/text.js';\nimport { sectionTitle } from './shared.js';\n\nexport function renderAdditional(resume: ResumeSchema, heading: string): string {\n const parts: string[] = [];\n\n if (has(resume.interests)) {\n for (const interest of resume.interests) {\n const keywords = has(interest.keywords) ? interest.keywords.join(', ') : '';\n const label = interest.name || 'Interests';\n if (keywords) {\n parts.push(\n `<div class=\"additional-line\"><span class=\"additional-label\">${esc(label)}:</span> ${esc(keywords)}</div>`,\n );\n }\n }\n }\n\n if (has(resume.languages)) {\n const langStr = resume.languages\n .map((l) => `${l.language}${l.fluency ? ` (${l.fluency})` : ''}`)\n .join(', ');\n parts.push(\n `<div class=\"additional-line\"><span class=\"additional-label\">Languages:</span> ${esc(langStr)}</div>`,\n );\n }\n\n if (has(resume.references)) {\n parts.push(\n `<div class=\"additional-line\"><span class=\"additional-label\">Referees:</span> Available on request</div>`,\n );\n }\n\n if (parts.length === 0) return '';\n\n return `\n ${sectionTitle(heading)}\n <div class=\"section-body\">\n ${parts.join('\\n ')}\n </div>`;\n}\n","import type { ResumeSchema } from './types/resume.js';\nimport { DEFAULT_HEADINGS } from './constants.js';\nimport { esc } from './utils/escape.js';\nimport { renderHeader } from './sections/header.js';\nimport { renderSummary } from './sections/summary.js';\nimport { renderSkills } from './sections/skills.js';\nimport { renderWork } from './sections/work.js';\nimport { renderProjects } from './sections/projects.js';\nimport { renderEducation } from './sections/education.js';\nimport { renderVolunteer } from './sections/volunteer.js';\nimport { renderCertificates } from './sections/certificates.js';\nimport { renderAdditional } from './sections/additional.js';\nimport css from './styles/academic.css?inline';\n\nexport function render(resume: ResumeSchema): string {\n const h = { ...DEFAULT_HEADINGS };\n\n const metaHeadings = resume?.meta?.headings;\n if (metaHeadings) {\n for (const [key, value] of Object.entries(metaHeadings)) {\n if (value && key in h) {\n h[key] = value;\n }\n }\n }\n\n const basics = resume?.basics || {};\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n <title>${esc(basics.name || 'Resume')}</title>\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\" />\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin />\n <link href=\"https://fonts.googleapis.com/css2?family=EB+Garamond:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500&display=swap\" rel=\"stylesheet\" />\n <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css\" />\n <style>${css}</style>\n</head>\n<body>\n <div class=\"resume\">\n ${renderHeader(basics)}\n ${renderSummary(basics.summary, h.summary)}\n ${renderSkills(resume.skills, h.skills)}\n ${renderWork(resume.work, h.experience)}\n ${renderProjects(resume.projects, h.projects)}\n ${renderEducation(resume.education, h.education)}\n ${renderVolunteer(resume.volunteer, h.volunteer)}\n ${renderCertificates(resume.certificates, h.certifications)}\n ${renderAdditional(resume, h.additional)}\n </div>\n</body>\n</html>`;\n}\n","export { render } from './render.js';\nexport type { ResumeSchema } from './types/resume.js';\n\n/** PDF rendering options for Puppeteer/Gotenberg. */\nexport const pdfRenderOptions = {\n mediaType: 'print' as const,\n format: 'A4' as const,\n margin: {\n top: '12mm',\n right: '14mm',\n bottom: '12mm',\n left: '14mm',\n },\n};\n"],"names":[],"mappings":"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"}
|
|
Binary file
|
package/examples/preview.html
CHANGED
|
@@ -183,6 +183,13 @@ body {
|
|
|
183
183
|
flex-shrink: 0;
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
+
.work-summary {
|
|
187
|
+
margin: 2px 0 0;
|
|
188
|
+
font-style: italic;
|
|
189
|
+
font-size: 9.5pt;
|
|
190
|
+
color: #333;
|
|
191
|
+
}
|
|
192
|
+
|
|
186
193
|
.work-highlights {
|
|
187
194
|
list-style: none;
|
|
188
195
|
padding-left: 0;
|
|
@@ -293,6 +300,8 @@ body {
|
|
|
293
300
|
</div>
|
|
294
301
|
</header>
|
|
295
302
|
<hr class="header-rule" />
|
|
303
|
+
|
|
304
|
+
<h2 class="section-title">Summary</h2>
|
|
296
305
|
<div class="summary">Web/Application Developer with 3 years’ experience in both enterprise SAP and full stack web development, now pursuing a M.Sc. in Systems Engineering for IoT at UCL. Shipped RTL enablement and 240+ Carbon Design System v2 migrations for IBM.com, automated support workflows, and delivered production-grade university portals. Passionate about building automation tools to improve developer productivity and system efficiency.</div>
|
|
297
306
|
|
|
298
307
|
<h2 class="section-title">Core Skills</h2>
|
|
@@ -326,6 +335,7 @@ body {
|
|
|
326
335
|
<div class="work-client">Client: IBM.com</div>
|
|
327
336
|
</div>
|
|
328
337
|
|
|
338
|
+
|
|
329
339
|
<ul class="work-highlights">
|
|
330
340
|
<li>Led roughly 50% of all RTL enablement code changes for IBM.com in a 3-person team, and proposed alternative component implementations to de-risk project timelines.</li>
|
|
331
341
|
<li>Built a Node.js tool to map dependencies across 240+ AEM components, cutting issue-triage time by ~70%; complemented with Python automation that reduced planning by ~1 week.</li>
|
|
@@ -345,6 +355,7 @@ body {
|
|
|
345
355
|
<div class="work-client">Client: Volkswagen Financial Services</div>
|
|
346
356
|
</div>
|
|
347
357
|
|
|
358
|
+
|
|
348
359
|
<ul class="work-highlights">
|
|
349
360
|
<li>Cut average incident first-response time by ~80% by introducing Python script automation.</li>
|
|
350
361
|
<li>Identified a pattern of recurring issues and proactively escalated it, creating an 8-hour mitigation window that prevented a major P1 incident.</li>
|
|
@@ -362,6 +373,7 @@ body {
|
|
|
362
373
|
<div class="work-client">Client: Center for Academic Courses, Anna University</div>
|
|
363
374
|
</div>
|
|
364
375
|
|
|
376
|
+
|
|
365
377
|
<ul class="work-highlights">
|
|
366
378
|
<li>Digitized affiliated college fee records and enabled self-service status checks, with options to export data to Excel/PDF.</li>
|
|
367
379
|
<li>Developed a PDF Generator tool to template official letters, reducing manual work and errors by ~90%, and transferred know-how to staff for adoption.</li>
|
|
@@ -378,6 +390,7 @@ body {
|
|
|
378
390
|
<div class="work-client"></div>
|
|
379
391
|
</div>
|
|
380
392
|
|
|
393
|
+
|
|
381
394
|
<ul class="work-highlights">
|
|
382
395
|
<li>Completed SAP S/4 HANA 2.0 certification and gained exposure to a full project lifecycle.</li>
|
|
383
396
|
</ul>
|
|
@@ -393,6 +406,7 @@ body {
|
|
|
393
406
|
<div class="work-client"></div>
|
|
394
407
|
</div>
|
|
395
408
|
|
|
409
|
+
|
|
396
410
|
<ul class="work-highlights">
|
|
397
411
|
<li>Built a Time & Expense Tracker application and prototyped a machine-learning model on Identification of Intoxicated Faces.</li>
|
|
398
412
|
</ul>
|
|
@@ -428,10 +442,7 @@ body {
|
|
|
428
442
|
<div class="edu-year">2026</div>
|
|
429
443
|
</div>
|
|
430
444
|
<div class="edu-institution">University College London</div>
|
|
431
|
-
|
|
432
|
-
<div class="edu-courses">
|
|
433
|
-
<div>Designing Sensor Systems</div><div>Real-World Multi-agent Systems</div>
|
|
434
|
-
</div>
|
|
445
|
+
<div class="edu-courses">Coursework: Designing Sensor Systems, Real-World Multi-agent Systems</div>
|
|
435
446
|
</div>
|
|
436
447
|
<div class="edu-entry">
|
|
437
448
|
<div class="edu-header">
|
|
@@ -439,10 +450,7 @@ body {
|
|
|
439
450
|
<div class="edu-year">2022</div>
|
|
440
451
|
</div>
|
|
441
452
|
<div class="edu-institution">Anna University, College of Engineering – Guindy | 8.5 CGPA</div>
|
|
442
|
-
|
|
443
|
-
<div class="edu-courses">
|
|
444
|
-
<div>Cyber Security and Internet of Things</div><div>Advanced Java</div><div>Machine Learning</div>
|
|
445
|
-
</div>
|
|
453
|
+
<div class="edu-courses">Coursework: Cyber Security and Internet of Things, Advanced Java, Machine Learning</div>
|
|
446
454
|
</div>
|
|
447
455
|
<div class="edu-entry">
|
|
448
456
|
<div class="edu-header">
|
|
@@ -450,10 +458,7 @@ body {
|
|
|
450
458
|
<div class="edu-year">2020</div>
|
|
451
459
|
</div>
|
|
452
460
|
<div class="edu-institution">Maharaja Sayajirao University of Baroda | 7.5 CGPA</div>
|
|
453
|
-
|
|
454
|
-
<div class="edu-courses">
|
|
455
|
-
<div>Advanced Java</div><div>Machine Learning</div><div>Android Development</div><div>Networking</div>
|
|
456
|
-
</div>
|
|
461
|
+
<div class="edu-courses">Coursework: Advanced Java, Machine Learning, Android Development, Networking</div>
|
|
457
462
|
</div>
|
|
458
463
|
</div>
|
|
459
464
|
|
package/examples/resume.json
CHANGED
|
@@ -1,56 +1,304 @@
|
|
|
1
1
|
{
|
|
2
2
|
"basics": {
|
|
3
|
-
"name": "
|
|
4
|
-
"label": "
|
|
5
|
-
"email": "
|
|
6
|
-
"phone": "+
|
|
7
|
-
"url": "https://
|
|
8
|
-
"summary": "
|
|
3
|
+
"name": "Ebenezer Isaac",
|
|
4
|
+
"label": "Software Developer · Web/Application Developer · Full-stack (Java/JavaScript)",
|
|
5
|
+
"email": "ebnezr.isaac@gmail.com",
|
|
6
|
+
"phone": "+44 75010 53232",
|
|
7
|
+
"url": "https://ebenezer-isaac.com",
|
|
8
|
+
"summary": "Web/Application Developer with 3 years’ experience in both enterprise SAP and full stack web development, now pursuing a M.Sc. in Systems Engineering for IoT at UCL. Shipped RTL enablement and 240+ Carbon Design System v2 migrations for IBM.com, automated support workflows, and delivered production-grade university portals. Passionate about building automation tools to improve developer productivity and system efficiency.",
|
|
9
9
|
"location": {
|
|
10
|
-
"city": "
|
|
11
|
-
"
|
|
12
|
-
"
|
|
10
|
+
"city": "London",
|
|
11
|
+
"countryCode": "GB",
|
|
12
|
+
"region": "UK"
|
|
13
13
|
},
|
|
14
14
|
"profiles": [
|
|
15
15
|
{
|
|
16
16
|
"network": "LinkedIn",
|
|
17
|
-
"username": "
|
|
17
|
+
"username": "ebnezr-isaac",
|
|
18
|
+
"url": "https://linkedin.com/in/ebnezr-isaac"
|
|
18
19
|
},
|
|
19
20
|
{
|
|
20
21
|
"network": "GitHub",
|
|
21
|
-
"username": "
|
|
22
|
+
"username": "ebenezer-isaac",
|
|
23
|
+
"url": "https://github.com/ebenezer-isaac"
|
|
22
24
|
}
|
|
23
25
|
]
|
|
24
26
|
},
|
|
25
|
-
"
|
|
27
|
+
"work": [
|
|
26
28
|
{
|
|
27
|
-
"name": "
|
|
28
|
-
"
|
|
29
|
+
"name": "IBM India Private Limited",
|
|
30
|
+
"position": "AEM Sites Application Developer",
|
|
31
|
+
"startDate": "2023-11",
|
|
32
|
+
"endDate": "2025-07",
|
|
33
|
+
"summary": "Tech-stack: AEM Cloud, Java, Node.Js, HTML, CSS, JavaScript, GitHub, Python Client: IBM.com",
|
|
34
|
+
"highlights": [
|
|
35
|
+
"Led roughly 50% of all RTL enablement code changes for IBM.com in a 3-person team, and proposed alternative component implementations to de-risk project timelines.",
|
|
36
|
+
"Built a Node.js tool to map dependencies across 240+ AEM components, cutting issue-triage time by ~70%; complemented with Python automation that reduced planning by ~1 week.",
|
|
37
|
+
"Led Carbon Design System v2 UI modernization for an 8-person team; mentored developers in CSS/Sass and unblocked three complex UI components.",
|
|
38
|
+
"Published an AEM URL Switcher Chrome extension to compare the same page across multiple environments/versions, accelerating multi-environment testing.",
|
|
39
|
+
"Customized three AEM Commons DAM asset reports with dynamic parameters to support Adobe Cloud content operations."
|
|
40
|
+
]
|
|
29
41
|
},
|
|
30
42
|
{
|
|
31
|
-
"name": "
|
|
32
|
-
"
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
43
|
+
"name": "IBM India Private Limited",
|
|
44
|
+
"position": "SAP BI AMS BW Consultant",
|
|
45
|
+
"startDate": "2022-09",
|
|
46
|
+
"endDate": "2023-10",
|
|
47
|
+
"summary": "Tech-stack: SAP BW, Python Client: Volkswagen Financial Services",
|
|
48
|
+
"highlights": [
|
|
49
|
+
"Cut average incident first-response time by ~80% by introducing Python script automation.",
|
|
50
|
+
"Identified a pattern of recurring issues and proactively escalated it, creating an 8-hour mitigation window that prevented a major P1 incident.",
|
|
51
|
+
"Reduced average ticket closure time by more than 24 hours."
|
|
52
|
+
]
|
|
53
|
+
},
|
|
36
54
|
{
|
|
37
|
-
"name": "
|
|
38
|
-
"position": "
|
|
39
|
-
"startDate": "
|
|
40
|
-
"
|
|
55
|
+
"name": "College of Engineering - Guindy",
|
|
56
|
+
"position": "Full Stack Developer Intern",
|
|
57
|
+
"startDate": "2022-07",
|
|
58
|
+
"endDate": "2022-08",
|
|
59
|
+
"summary": "Tech-stack: PHP, MySQL, HTML, CSS, JavaScript Client: Center for Academic Courses, Anna University",
|
|
41
60
|
"highlights": [
|
|
42
|
-
"
|
|
43
|
-
"
|
|
61
|
+
"Digitized affiliated college fee records and enabled self-service status checks, with options to export data to Excel/PDF.",
|
|
62
|
+
"Developed a PDF Generator tool to template official letters, reducing manual work and errors by ~90%, and transferred know-how to staff for adoption."
|
|
44
63
|
]
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"name": "IBM India Private Limited",
|
|
67
|
+
"position": "Associate Application Developer Intern",
|
|
68
|
+
"startDate": "2022-01",
|
|
69
|
+
"endDate": "2022-06",
|
|
70
|
+
"summary": "Tech-stack: SAP S/4 HANA 2.0",
|
|
71
|
+
"highlights": [
|
|
72
|
+
"Completed SAP S/4 HANA 2.0 certification and gained exposure to a full project lifecycle."
|
|
73
|
+
]
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"name": "Rishabh Software Private Limited, Vadodara",
|
|
77
|
+
"position": "Machine Learning Intern",
|
|
78
|
+
"startDate": "2020-01",
|
|
79
|
+
"endDate": "2020-06",
|
|
80
|
+
"summary": "Tech-stack: PHP, MySQL, HTML, CSS, JavaScript, Keras, Tensorflow",
|
|
81
|
+
"highlights": [
|
|
82
|
+
"Built a Time & Expense Tracker application and prototyped a machine-learning model on Identification of Intoxicated Faces."
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
],
|
|
86
|
+
"volunteer": [
|
|
87
|
+
{
|
|
88
|
+
"organization": "Crossroads",
|
|
89
|
+
"position": "Volunteer Youth Mentor",
|
|
90
|
+
"startDate": "2024",
|
|
91
|
+
"endDate": "2024",
|
|
92
|
+
"summary": "Co-led weekly teen sessions, designing activities that build communication and conflict-resolution skills while maintaining a safe, respectful environment. Followed safeguarding procedures, kept matters confidential, and sign-posted students to appropriate support."
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"organization": "Anna University",
|
|
96
|
+
"position": "MCA Class Representative",
|
|
97
|
+
"startDate": "2020",
|
|
98
|
+
"endDate": "2022",
|
|
99
|
+
"summary": "Elected cohort liaison; ran quick pulse-checks to surface concerns, presented consolidated feedback and solutions to faculty, mediated timetable/assessment conflicts, and ensured updates and actions were communicated and closed."
|
|
45
100
|
}
|
|
46
101
|
],
|
|
47
102
|
"education": [
|
|
48
103
|
{
|
|
49
|
-
"institution": "
|
|
50
|
-
"area": "
|
|
51
|
-
"studyType": "M.
|
|
52
|
-
"
|
|
53
|
-
"
|
|
104
|
+
"institution": "University College London",
|
|
105
|
+
"area": "Systems Engineering for the Internet of Things",
|
|
106
|
+
"studyType": "M.Sc",
|
|
107
|
+
"startDate": "2025",
|
|
108
|
+
"endDate": "2026",
|
|
109
|
+
"courses": [
|
|
110
|
+
"Designing Sensor Systems",
|
|
111
|
+
"Real-World Multi-agent Systems"
|
|
112
|
+
]
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
"institution": "Anna University, College of Engineering – Guindy",
|
|
116
|
+
"area": "Computer Application",
|
|
117
|
+
"studyType": "Master",
|
|
118
|
+
"startDate": "2020",
|
|
119
|
+
"endDate": "2022",
|
|
120
|
+
"score": "8.5 CGPA",
|
|
121
|
+
"courses": [
|
|
122
|
+
"Cyber Security and Internet of Things",
|
|
123
|
+
"Advanced Java",
|
|
124
|
+
"Machine Learning"
|
|
125
|
+
]
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
"institution": "Maharaja Sayajirao University of Baroda",
|
|
129
|
+
"area": "Computer Application",
|
|
130
|
+
"studyType": "Bachelor",
|
|
131
|
+
"startDate": "2017",
|
|
132
|
+
"endDate": "2020",
|
|
133
|
+
"score": "7.5 CGPA",
|
|
134
|
+
"courses": [
|
|
135
|
+
"Advanced Java",
|
|
136
|
+
"Machine Learning",
|
|
137
|
+
"Android Development",
|
|
138
|
+
"Networking"
|
|
139
|
+
]
|
|
140
|
+
}
|
|
141
|
+
],
|
|
142
|
+
"certificates": [
|
|
143
|
+
{
|
|
144
|
+
"name": "AWS Certified Cloud Practitioner"
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
"name": "Adobe Certified Expert - Adobe Experience Manager Sites Developer"
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
"name": "Adobe Certified Professional - Adobe Experience Manager Forms Backend Developer"
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"name": "SAP Certified Development Specialist - ABAP for SAP HANA 2.0"
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
"name": "15+ additional Adobe, IBM, and Credly-verified certifications"
|
|
157
|
+
}
|
|
158
|
+
],
|
|
159
|
+
"skills": [
|
|
160
|
+
{
|
|
161
|
+
"name": "Languages/Frameworks",
|
|
162
|
+
"keywords": [
|
|
163
|
+
"JavaScript",
|
|
164
|
+
"Java",
|
|
165
|
+
"PHP",
|
|
166
|
+
"Node.Js",
|
|
167
|
+
"Flutter",
|
|
168
|
+
"Python",
|
|
169
|
+
"HTML",
|
|
170
|
+
"CSS",
|
|
171
|
+
"JSON",
|
|
172
|
+
"XML",
|
|
173
|
+
"jQuery"
|
|
174
|
+
]
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
"name": "Platforms",
|
|
178
|
+
"keywords": [
|
|
179
|
+
"AEM Sites",
|
|
180
|
+
"Sling",
|
|
181
|
+
"OSGi",
|
|
182
|
+
"AWS",
|
|
183
|
+
"GCP KMS"
|
|
184
|
+
]
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
"name": "Data",
|
|
188
|
+
"keywords": [
|
|
189
|
+
"Firebase",
|
|
190
|
+
"MySQL",
|
|
191
|
+
"MongoDB"
|
|
192
|
+
]
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
"name": "Tools/Libraries/Devices",
|
|
196
|
+
"keywords": [
|
|
197
|
+
"REST APIs",
|
|
198
|
+
"React",
|
|
199
|
+
"GitHub",
|
|
200
|
+
"Google Tink",
|
|
201
|
+
"Arduino",
|
|
202
|
+
"Raspberry Pi"
|
|
203
|
+
]
|
|
204
|
+
}
|
|
205
|
+
],
|
|
206
|
+
"languages": [
|
|
207
|
+
{
|
|
208
|
+
"language": "English",
|
|
209
|
+
"fluency": "IELTS 8.0"
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
"language": "Tamil",
|
|
213
|
+
"fluency": "Native"
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
"language": "Hindi",
|
|
217
|
+
"fluency": "Fluent"
|
|
218
|
+
}
|
|
219
|
+
],
|
|
220
|
+
"interests": [
|
|
221
|
+
{
|
|
222
|
+
"name": "Weight Training"
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
"name": "Guitar"
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
"name": "Trekking"
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
"name": "Swimming"
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
"name": "Cycling"
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
"name": "Marathon Running"
|
|
238
|
+
}
|
|
239
|
+
],
|
|
240
|
+
"references": [
|
|
241
|
+
{
|
|
242
|
+
"name": "Available on request"
|
|
243
|
+
}
|
|
244
|
+
],
|
|
245
|
+
"projects": [
|
|
246
|
+
{
|
|
247
|
+
"name": "Low-Cost Automation of Existing Electric Devices (MycroLinks)",
|
|
248
|
+
"description": "Mobile app + NodeMCU (ESP8266) with real-time control and state sync.",
|
|
249
|
+
"keywords": [
|
|
250
|
+
"Flutter",
|
|
251
|
+
"Dart",
|
|
252
|
+
"Firebase",
|
|
253
|
+
"Arduino"
|
|
254
|
+
],
|
|
255
|
+
"url": "https://github.com/ebenezer-isaac/mycrolinks"
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
"name": "Fingerprint Attendance Management System",
|
|
259
|
+
"description": "Distributed biometric devices with central admin portal; CSV/Excel exports and audit logs.",
|
|
260
|
+
"keywords": [
|
|
261
|
+
"Java Web",
|
|
262
|
+
"Python",
|
|
263
|
+
"Raspberry Pi",
|
|
264
|
+
"Arduino",
|
|
265
|
+
"MySQL",
|
|
266
|
+
"JS/CSS"
|
|
267
|
+
],
|
|
268
|
+
"url": "https://github.com/ebenezer-isaac/cerberus-web"
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
"name": "Event Enrolment & Online Payments (Deepwoods MCC)",
|
|
272
|
+
"description": "Enrolment, ticketing, and payment confirmations with dashboard and exports.",
|
|
273
|
+
"keywords": [
|
|
274
|
+
"PHP",
|
|
275
|
+
"MySQL",
|
|
276
|
+
"InstaMojo",
|
|
277
|
+
"jQuery",
|
|
278
|
+
"SEO",
|
|
279
|
+
"Bootstrap"
|
|
280
|
+
],
|
|
281
|
+
"url": "https://github.com/ebenezer-isaac/deepwoods-mcc.com"
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
"name": "Android Password Manager",
|
|
285
|
+
"description": "On-device encryption with cloud-managed keys; Secure credential sync.",
|
|
286
|
+
"keywords": [
|
|
287
|
+
"Android",
|
|
288
|
+
"Google Tink",
|
|
289
|
+
"GCP KMS",
|
|
290
|
+
"Node REST API",
|
|
291
|
+
"Firebase"
|
|
292
|
+
],
|
|
293
|
+
"url": "https://github.com/ebenezer-isaac/android-music-player"
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
"name": "Classroom Assignment Evaluator",
|
|
297
|
+
"description": "Gemini-based LLM for automated assignment evaluation and feedback using teacher-defined criteria.",
|
|
298
|
+
"keywords": [
|
|
299
|
+
"Flutter",
|
|
300
|
+
"Firebase"
|
|
301
|
+
]
|
|
54
302
|
}
|
|
55
303
|
]
|
|
56
|
-
}
|
|
304
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jsonresume-theme-academic",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "Academic serif theme for JSON Resume with EB Garamond typography, small caps headings, and gold accents. Designed for print and PDF.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs",
|