jsonresume-theme-academic 1.3.1 → 1.3.3

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/README.md CHANGED
@@ -1,97 +1,105 @@
1
- # jsonresume-theme-academic
2
-
3
- > Academic serif theme for [JSON Resume](https://jsonresume.org) with EB Garamond typography, small caps headings, and gold accents. Designed for print and PDF.
4
-
5
- [![npm version](https://img.shields.io/npm/v/jsonresume-theme-academic.svg)](https://www.npmjs.com/package/jsonresume-theme-academic)
6
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
7
-
8
- ![Preview](examples/preview.png)
9
-
10
- ## Features
11
-
12
- - Academic serif typography (EB Garamond)
13
- - A4 print layout with precise margins
14
- - Built in HTML sanitization (XSS safe)
15
- - Customizable section headings via `meta.headings`
16
- - Zero runtime dependencies
17
- - ESM + CJS + UMD builds with TypeScript declarations
18
-
19
- ## Quick Start
20
-
21
- ### With resumed (recommended)
22
-
23
- ```bash
24
- npm install jsonresume-theme-academic
25
- npx resumed render resume.json --theme jsonresume-theme-academic
26
- # resume.html ready
27
- ```
28
-
29
- ### With resume-cli
30
-
31
- ```bash
32
- npm install -g resume-cli jsonresume-theme-academic
33
- resume export resume.pdf --theme academic
34
- ```
35
-
36
- ### Programmatic Usage
37
-
38
- ```typescript
39
- import { render, pdfRenderOptions } from 'jsonresume-theme-academic';
40
-
41
- const html = render(resumeJson);
42
- // html is a self-contained HTML document
43
-
44
- // For Puppeteer PDF rendering:
45
- await page.pdf(pdfRenderOptions);
46
- ```
47
-
48
- ## Customization
49
-
50
- ### Section Headings
51
-
52
- Override default headings via `meta.headings`:
53
-
54
- ```json
55
- {
56
- "meta": {
57
- "headings": {
58
- "experience": "Professional Experience",
59
- "skills": "Technical Skills",
60
- "education": "Academic Background"
61
- }
62
- }
63
- }
64
- ```
65
-
66
- Available keys: `skills`, `experience`, `projects`, `education`, `volunteer`, `certifications`, `additional`
67
-
68
- ### PDF Rendering Options
69
-
70
- The exported `pdfRenderOptions` provides optimized settings for Puppeteer/Gotenberg:
71
-
72
- ```typescript
73
- {
74
- mediaType: 'print',
75
- format: 'A4',
76
- margin: { top: '12mm', right: '14mm', bottom: '12mm', left: '14mm' }
77
- }
78
- ```
79
-
80
- ## Development
81
-
82
- ```bash
83
- git clone https://github.com/ebenezer-isaac/jsonresume-theme-academic.git
84
- cd jsonresume-theme-academic
85
- npm install
86
- npm run dev # Start dev server
87
- npm test # Run tests
88
- npm run build # Build ESM + CJS + UMD
89
- ```
90
-
91
- ## Contributing
92
-
93
- See [CONTRIBUTING.md](CONTRIBUTING.md)
94
-
95
- ## License
96
-
97
- MIT
1
+ # jsonresume-theme-academic
2
+
3
+ > Academic serif theme for [JSON Resume](https://jsonresume.org) with EB Garamond typography, small caps headings, and gold accents. Designed for print and PDF.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/jsonresume-theme-academic.svg)](https://www.npmjs.com/package/jsonresume-theme-academic)
6
+ [![npm downloads](https://img.shields.io/npm/dm/jsonresume-theme-academic.svg)](https://www.npmjs.com/package/jsonresume-theme-academic)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
8
+ [![TypeScript](https://img.shields.io/badge/TypeScript-strict-blue.svg)](https://www.typescriptlang.org/)
9
+
10
+ ![Preview](examples/preview.png)
11
+
12
+ ## Features
13
+
14
+ - Academic serif typography (EB Garamond)
15
+ - A4 print layout with precise margins
16
+ - Built in HTML sanitization (XSS safe)
17
+ - Customizable section headings via `meta.headings`
18
+ - Zero runtime dependencies
19
+ - ESM + CJS + UMD builds with TypeScript declarations
20
+
21
+ ## Quick Start
22
+
23
+ ### With resumed (recommended)
24
+
25
+ ```bash
26
+ npm install jsonresume-theme-academic
27
+ npx resumed render resume.json --theme jsonresume-theme-academic
28
+ # → resume.html ready
29
+ ```
30
+
31
+ ### With resume-cli
32
+
33
+ ```bash
34
+ npm install -g resume-cli jsonresume-theme-academic
35
+ resume export resume.pdf --theme academic
36
+ ```
37
+
38
+ ### Programmatic Usage
39
+
40
+ ```typescript
41
+ import { render, pdfRenderOptions } from 'jsonresume-theme-academic';
42
+
43
+ const html = render(resumeJson);
44
+ // html is a self-contained HTML document
45
+
46
+ // For Puppeteer PDF rendering:
47
+ await page.pdf(pdfRenderOptions);
48
+ ```
49
+
50
+ ## Customization
51
+
52
+ ### Section Headings
53
+
54
+ Override default headings via `meta.headings`:
55
+
56
+ ```json
57
+ {
58
+ "meta": {
59
+ "headings": {
60
+ "experience": "Professional Experience",
61
+ "skills": "Technical Skills",
62
+ "education": "Academic Background"
63
+ }
64
+ }
65
+ }
66
+ ```
67
+
68
+ Available keys: `skills`, `experience`, `projects`, `education`, `volunteer`, `certifications`, `additional`
69
+
70
+ ### PDF Rendering Options
71
+
72
+ The exported `pdfRenderOptions` provides optimized settings for Puppeteer/Gotenberg:
73
+
74
+ ```typescript
75
+ {
76
+ mediaType: 'print',
77
+ format: 'A4',
78
+ margin: { top: '12mm', right: '14mm', bottom: '12mm', left: '14mm' }
79
+ }
80
+ ```
81
+
82
+ ## Development
83
+
84
+ ```bash
85
+ git clone https://github.com/ebenezer-isaac/jsonresume-theme-academic.git
86
+ cd jsonresume-theme-academic
87
+ npm install
88
+ npm run dev # Start dev server
89
+ npm test # Run tests
90
+ npm run build # Build ESM + CJS + UMD
91
+ ```
92
+
93
+ ## Community
94
+
95
+ This theme is part of the open-source toolkit from [LLM Conveyors](https://llmconveyors.com), an AI-powered career platform that uses this theme for generating polished, print-ready resumes. We open-sourced it so anyone building with JSON Resume can have a production-quality academic theme out of the box.
96
+
97
+ If you're using this theme in your project, we'd love to hear about it — open an issue or start a discussion!
98
+
99
+ ## Contributing
100
+
101
+ See [CONTRIBUTING.md](CONTRIBUTING.md)
102
+
103
+ ## License
104
+
105
+ MIT
@@ -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(/&amp;/g, '&')\n .replace(/&lt;/g, '<')\n .replace(/&gt;/g, '>')\n .replace(/&quot;/g, '\"')\n .replace(/&#39;/g, \"'\")\n .replace(/&apos;/g, \"'\")\n .replace(/&nbsp;/g, ' ')\n .replace(/&mdash;/g, '\\u2014')\n .replace(/&ndash;/g, '\\u2013')\n .replace(/&hellip;/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, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#39;');\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, '&quot;')}\" target=\"_blank\" rel=\"noopener\">`;\n })\n .replace(/<mark\\s+[^>]*class=\"([^\"]*)\"[^>]*>/gi, (_, cls: string) =>\n `<mark class=\"${cls.replace(/\"/g, '&quot;')}\">`,\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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"}
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[] = [\r\n 'January',\r\n 'February',\r\n 'March',\r\n 'April',\r\n 'May',\r\n 'June',\r\n 'July',\r\n 'August',\r\n 'September',\r\n 'October',\r\n 'November',\r\n 'December',\r\n];\r\n\r\nexport const COUNTRY_NAMES: Readonly<Record<string, string>> = {\r\n GB: 'United Kingdom',\r\n US: 'United States',\r\n CA: 'Canada',\r\n AU: 'Australia',\r\n DE: 'Germany',\r\n FR: 'France',\r\n IN: 'India',\r\n JP: 'Japan',\r\n CN: 'China',\r\n BR: 'Brazil',\r\n NL: 'Netherlands',\r\n IT: 'Italy',\r\n ES: 'Spain',\r\n SE: 'Sweden',\r\n CH: 'Switzerland',\r\n SG: 'Singapore',\r\n NZ: 'New Zealand',\r\n IE: 'Ireland',\r\n AE: 'UAE',\r\n};\r\n\r\nexport const DEFAULT_HEADINGS: Readonly<Record<string, string>> = {\r\n summary: 'Summary',\r\n skills: 'Core Skills',\r\n experience: 'Experience',\r\n projects: 'Selected Projects',\r\n education: 'Education',\r\n volunteer: 'Leadership & Volunteering',\r\n certifications: 'Credentials',\r\n additional: 'Additional',\r\n};\r\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(/&amp;/g, '&')\n .replace(/&lt;/g, '<')\n .replace(/&gt;/g, '>')\n .replace(/&quot;/g, '\"')\n .replace(/&#39;/g, \"'\")\n .replace(/&apos;/g, \"'\")\n .replace(/&nbsp;/g, ' ')\n .replace(/&mdash;/g, '\\u2014')\n .replace(/&ndash;/g, '\\u2013')\n .replace(/&hellip;/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, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#39;');\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, '&quot;')}\" target=\"_blank\" rel=\"noopener\">`;\n })\n .replace(/<mark\\s+[^>]*class=\"([^\"]*)\"[^>]*>/gi, (_, cls: string) =>\n `<mark class=\"${cls.replace(/\"/g, '&quot;')}\">`,\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';\r\nimport { sectionTitle } from './shared.js';\r\n\r\nexport function renderSummary(summary: string | undefined, heading: string): string {\r\n if (!summary) return '';\r\n const html = richText(summary, { block: true });\r\n if (!html) return '';\r\n return `\r\n ${sectionTitle(heading)}\r\n <div class=\"summary\">${html}</div>`;\r\n}\r\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';\r\nimport { esc } from '../utils/escape.js';\r\nimport { dateRange } from '../utils/dates.js';\r\nimport { has, richText, stripHtml } from '../utils/text.js';\r\nimport { sectionTitle } from './shared.js';\r\n\r\n/** Check whether summary uses structured Tech-stack / Client format. */\r\nfunction isStructuredMeta(text: string): boolean {\r\n return /Tech-stack:\\s/i.test(text) || /Client:\\s/i.test(text);\r\n}\r\n\r\n/** Extract tech-stack, client, and any remaining narrative from a work summary. */\r\nfunction parseWorkMeta(summary: string | undefined): {\r\n techStack: string;\r\n client: string;\r\n narrative: string;\r\n} {\r\n if (!summary) return { techStack: '', client: '', narrative: '' };\r\n const text = stripHtml(summary).replace(/\\n/g, ' ').trim();\r\n if (!isStructuredMeta(text)) return { techStack: '', client: '', narrative: text };\r\n\r\n let remaining = text;\r\n\r\n const clientMatch = remaining.match(/Client:\\s*(.+)$/i);\r\n const client = clientMatch ? clientMatch[1].trim() : '';\r\n if (clientMatch) remaining = remaining.slice(0, clientMatch.index).trim();\r\n\r\n const techMatch = remaining.match(/Tech-stack:\\s*(.+)/i);\r\n const techStack = techMatch ? techMatch[1].trim() : '';\r\n if (techMatch) remaining = remaining.slice(0, techMatch.index).trim();\r\n\r\n return { techStack, client, narrative: remaining };\r\n}\r\n\r\nfunction renderWorkEntry(entry: ResumeWorkEntry): string {\r\n const { techStack, client, narrative } = parseWorkMeta(entry.summary);\r\n const duration = dateRange(entry.startDate, entry.endDate);\r\n\r\n return `\r\n <div class=\"work-entry\">\r\n <div class=\"work-header\">\r\n <div class=\"work-title\">${esc(entry.name)} - ${esc(entry.position)}</div>\r\n <div class=\"work-duration\">${duration ? `Duration: ${esc(duration)}` : ''}</div>\r\n </div>\r\n ${\r\n techStack || client\r\n ? `\r\n <div class=\"work-meta\">\r\n <div class=\"work-tech\">${techStack ? `Tech-stack: ${esc(techStack)}` : ''}</div>\r\n <div class=\"work-client\">${client ? `Client: ${esc(client)}` : ''}</div>\r\n </div>`\r\n : ''\r\n }\r\n ${narrative ? `<p class=\"work-summary\">${richText(narrative, { block: false })}</p>` : ''}\r\n ${\r\n has(entry.highlights)\r\n ? `\r\n <ul class=\"work-highlights\">\r\n ${entry.highlights.map((h) => `<li>${richText(h)}</li>`).join('\\n ')}\r\n </ul>`\r\n : ''\r\n }\r\n </div>`;\r\n}\r\n\r\nexport function renderWork(work: readonly ResumeWorkEntry[] | undefined, heading: string): string {\r\n if (!has(work)) return '';\r\n return `\r\n ${sectionTitle(heading)}\r\n <div class=\"section-body\">\r\n ${work.map(renderWorkEntry).join('')}\r\n </div>`;\r\n}\r\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';\r\nimport { esc } from '../utils/escape.js';\r\nimport { formatDate } from '../utils/dates.js';\r\nimport { has, richText } from '../utils/text.js';\r\nimport { sectionTitle } from './shared.js';\r\n\r\nfunction renderEducationEntry(entry: ResumeEducationEntry): string {\r\n const degree = [entry.studyType, entry.area].filter(Boolean).join(' ');\r\n\r\n let yearDisplay = '';\r\n if (entry.endDate) {\r\n yearDisplay = formatDate(entry.endDate);\r\n } else if (entry.startDate) {\r\n const year = parseInt(String(entry.startDate).split('-')[0], 10);\r\n const currentYear = new Date().getFullYear();\r\n yearDisplay = year >= currentYear ? `Expected ${formatDate(entry.startDate)}` : formatDate(entry.startDate);\r\n }\r\n\r\n const instParts = [entry.institution];\r\n if (entry.score) instParts.push(entry.score);\r\n const instLine = instParts.filter(Boolean).join(' | ');\r\n\r\n return `\r\n <div class=\"edu-entry\">\r\n <div class=\"edu-header\">\r\n <div class=\"edu-degree\">${esc(degree)}</div>\r\n <div class=\"edu-year\">${esc(yearDisplay)}</div>\r\n </div>\r\n ${entry.institution ? `<div class=\"edu-institution\">${esc(instLine)}</div>` : ''}\r\n ${\r\n has(entry.courses)\r\n ? `<div class=\"edu-courses\">Coursework: ${entry.courses.map((c) => richText(c)).join(', ')}</div>`\r\n : ''\r\n }\r\n </div>`;\r\n}\r\n\r\nexport function renderEducation(\r\n education: readonly ResumeEducationEntry[] | undefined,\r\n heading: string,\r\n): string {\r\n if (!has(education)) return '';\r\n return `\r\n ${sectionTitle(heading)}\r\n <div class=\"section-body\">\r\n ${education.map(renderEducationEntry).join('')}\r\n </div>`;\r\n}\r\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';\r\nimport { DEFAULT_HEADINGS } from './constants.js';\r\nimport { esc } from './utils/escape.js';\r\nimport { renderHeader } from './sections/header.js';\r\nimport { renderSummary } from './sections/summary.js';\r\nimport { renderSkills } from './sections/skills.js';\r\nimport { renderWork } from './sections/work.js';\r\nimport { renderProjects } from './sections/projects.js';\r\nimport { renderEducation } from './sections/education.js';\r\nimport { renderVolunteer } from './sections/volunteer.js';\r\nimport { renderCertificates } from './sections/certificates.js';\r\nimport { renderAdditional } from './sections/additional.js';\r\nimport css from './styles/academic.css?inline';\r\n\r\nexport function render(resume: ResumeSchema): string {\r\n const h = { ...DEFAULT_HEADINGS };\r\n\r\n const metaHeadings = resume?.meta?.headings;\r\n if (metaHeadings) {\r\n for (const [key, value] of Object.entries(metaHeadings)) {\r\n if (value && key in h) {\r\n h[key] = value;\r\n }\r\n }\r\n }\r\n\r\n const basics = resume?.basics || {};\r\n\r\n return `<!DOCTYPE html>\r\n<html lang=\"en\">\r\n<head>\r\n <meta charset=\"UTF-8\" />\r\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\r\n <title>${esc(basics.name || 'Resume')}</title>\r\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\" />\r\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin />\r\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\" />\r\n <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css\" />\r\n <style>${css}</style>\r\n</head>\r\n<body>\r\n <div class=\"resume\">\r\n ${renderHeader(basics)}\r\n ${renderSummary(basics.summary, h.summary)}\r\n ${renderSkills(resume.skills, h.skills)}\r\n ${renderWork(resume.work, h.experience)}\r\n ${renderProjects(resume.projects, h.projects)}\r\n ${renderEducation(resume.education, h.education)}\r\n ${renderVolunteer(resume.volunteer, h.volunteer)}\r\n ${renderCertificates(resume.certificates, h.certifications)}\r\n ${renderAdditional(resume, h.additional)}\r\n </div>\r\n</body>\r\n</html>`;\r\n}\r\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.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(/&amp;/g, '&')\n .replace(/&lt;/g, '<')\n .replace(/&gt;/g, '>')\n .replace(/&quot;/g, '\"')\n .replace(/&#39;/g, \"'\")\n .replace(/&apos;/g, \"'\")\n .replace(/&nbsp;/g, ' ')\n .replace(/&mdash;/g, '\\u2014')\n .replace(/&ndash;/g, '\\u2013')\n .replace(/&hellip;/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, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#39;');\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, '&quot;')}\" target=\"_blank\" rel=\"noopener\">`;\n })\n .replace(/<mark\\s+[^>]*class=\"([^\"]*)\"[^>]*>/gi, (_, cls: string) =>\n `<mark class=\"${cls.replace(/\"/g, '&quot;')}\">`,\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":"AAAO,MAAM,SAA4B;AAAA,EACvC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEO,MAAM,gBAAkD;AAAA,EAC7D,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAEO,MAAM,mBAAqD;AAAA,EAChE,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,UAAU;AAAA,EACV,WAAW;AAAA,EACX,WAAW;AAAA,EACX,gBAAgB;AAAA,EAChB,YAAY;AACd;AC7CO,SAAS,eAAe,MAAsB;AACnD,SAAO,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;AAClC;AAGO,SAAS,IAAI,MAAyC;AAC3D,MAAI,QAAQ,KAAM,QAAO;AACzB,SAAO,OAAO,IAAI,EACf,QAAQ,MAAM,OAAO,EACrB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,QAAQ,EACtB,QAAQ,MAAM,OAAO;AAC1B;ACxBO,SAAS,WAAW,SAA4C;AACrE,MAAI,CAAC,QAAS,QAAO;AACrB,MAAI,QAAQ,kBAAkB,UAAW,QAAO;AAChD,QAAM,QAAQ,OAAO,OAAO,EAAE,MAAM,GAAG;AACvC,MAAI,MAAM,WAAW,EAAG,QAAO,MAAM,CAAC;AACtC,QAAM,OAAO,MAAM,CAAC;AACpB,QAAM,QAAQ,OAAO,SAAS,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,KAAK;AACpD,SAAO,GAAG,KAAK,IAAI,IAAI;AACzB;AAEO,SAAS,UAAU,OAAkC,KAAwC;AAClG,QAAM,IAAI,WAAW,KAAK;AAC1B,QAAM,IAAI,WAAW,GAAG;AACxB,MAAI,CAAC,KAAK,MAAM,UAAW,QAAO;AAClC,MAAI,CAAC,EAAG,QAAO;AACf,SAAO,GAAG,CAAC,OAAO,CAAC;AACrB;AAEO,SAAS,WAAW,MAAyC;AAClE,MAAI,CAAC,KAAM,QAAO;AAClB,SAAO,cAAc,KAAK,YAAA,CAAa,KAAK;AAC9C;ACvBO,SAAS,YAAY,SAA4C;AACtE,MAAI,CAAC,QAAS,QAAO;AACrB,QAAM,IAAI,QAAQ,YAAA;AAClB,MAAI,MAAM,WAAY,QAAO;AAC7B,MAAI,MAAM,SAAU,QAAO;AAC3B,MAAI,MAAM,aAAa,MAAM,IAAK,QAAO;AACzC,MAAI,MAAM,gBAAiB,QAAO;AAClC,SAAO;AACT;ACPA,MAAM,iCAAiB,IAAI,CAAC,KAAK,MAAM,MAAM,MAAM,YAAY,CAAC;AAChE,MAAM,kCAAkB,IAAI;AAAA,EAC1B;AAAA,EAAM;AAAA,EAAK;AAAA,EAAU;AAAA,EAAK;AAAA,EAAM;AAAA,EAAK;AAAA,EAAK;AAAA,EAAQ;AAAA,EAAK;AAAA,EAAO;AAAA,EAAO;AACvE,CAAC;AACD,MAAM,mCAAmB,IAAI,CAAC,GAAG,YAAY,GAAG,WAAW,CAAC;AAG5D,MAAM,mBAAmB;AACzB,MAAM,eAAe;AAErB,SAAS,UAAU,MAAuB;AACxC,QAAM,UAAU,KAAK,KAAA;AACrB,MAAI,CAAC,QAAS,QAAO;AACrB,SAAO,iBAAiB,KAAK,OAAO,KAAK,aAAa,KAAK,OAAO;AACpE;AAWO,SAAS,aAAa,MAAiC,EAAE,SAAS,MAAA,IAA2B,CAAA,GAAY;AAC9G,MAAI,CAAC,KAAM,QAAO;AAClB,QAAM,UAAU,SAAS,cAAc;AAEvC,SAAO,OAAO,IAAI,EACf,QAAQ,oCAAoC,CAAC,GAAG,SAAiB;AAChE,QAAI,CAAC,UAAU,IAAI,EAAG,QAAO;AAC7B,WAAO,YAAY,KAAK,QAAQ,MAAM,QAAQ,CAAC;AAAA,EACjD,CAAC,EACA;AAAA,IAAQ;AAAA,IAAwC,CAAC,GAAG,QACnD,gBAAgB,IAAI,QAAQ,MAAM,QAAQ,CAAC;AAAA,EAAA,EAE5C,QAAQ,2CAA2C,CAAC,OAAO,OAAe,KAAa,cAAsB;AAC5G,UAAM,QAAQ,IAAI,YAAA;AAClB,QAAI,UAAU,OAAO,CAAC,SAAS,MAAM,SAAS,OAAO,EAAG,QAAO;AAC/D,QAAI,UAAU,UAAU,CAAC,SAAS,MAAM,SAAS,QAAQ,EAAG,QAAO;AACnE,QAAI,QAAQ,IAAI,KAAK,EAAG,QAAO,IAAI,KAAK,GAAG,KAAK,GAAG,SAAS;AAC5D,QAAI,UAAU,WAAW,IAAI,KAAK,EAAG,QAAO,QAAQ,MAAM;AAC1D,WAAO;AAAA,EACT,CAAC,EACA,QAAQ,iBAAiB,EAAE,EAC3B,QAAQ,WAAW,GAAG,EACtB,KAAA;AACL;AC7CO,SAAS,UAAU,MAAyC;AACjE,MAAI,CAAC,KAAM,QAAO;AAClB,SAAO;AAAA,IACL,OAAO,IAAI,EACR,QAAQ,gBAAgB,GAAG,EAC3B,QAAQ,0BAA0B,GAAG,EACrC,QAAQ,YAAY,EAAE;AAAA,EAAA,EAExB,QAAQ,WAAW,GAAG,EACtB,KAAA;AACL;AAEA,MAAM,SAAS,CAAC,SAA0B,kBAAkB,KAAK,IAAI;AACrE,MAAM,cAAc,CAAC,SAA0B,yCAAyC,KAAK,IAAI;AAO1F,SAAS,SAAS,MAAiC,EAAE,QAAQ,MAAA,IAA+B,CAAA,GAAY;AAC7G,MAAI,QAAQ,KAAM,QAAO;AACzB,QAAM,IAAI,OAAO,IAAI;AACrB,MAAI,OAAO,CAAC,EAAG,QAAO,aAAa,GAAG,EAAE,QAAQ,CAAC,OAAO;AACxD,MAAI,YAAY,CAAC,UAAU,IAAI,eAAe,CAAC,CAAC;AAChD,SAAO,IAAI,CAAC;AACd;AAGO,SAAS,IAAO,KAAgF;AACrG,SAAO,MAAM,QAAQ,GAAG,KAAK,IAAI,SAAS;AAC5C;ACxBO,SAAS,aAAa,QAA0C;AACrE,MAAI,CAAC,OAAQ,QAAO;AAEpB,QAAM,WAAW,OAAO,WACpB,CAAC,OAAO,SAAS,MAAM,WAAW,OAAO,SAAS,MAAM,CAAC,EAAE,OAAO,OAAO,EAAE,KAAK,IAAI,IACpF;AAEJ,QAAM,QAAuB,CAAA;AAC7B,MAAI;AACF,UAAM,KAAK,EAAE,MAAM,4CAA4C,MAAM,UAAU;AACjF,MAAI,OAAO;AACT,UAAM,KAAK,EAAE,MAAM,4CAA4C,MAAM,OAAO,OAAO;AACrF,MAAI,OAAO;AACT,UAAM,KAAK,EAAE,MAAM,wCAAwC,MAAM,OAAO,OAAO;AACjF,MAAI,OAAO,KAAK;AACd,UAAM,UAAU,OAAO,IAAI,QAAQ,gBAAgB,EAAE;AACrD,UAAM,KAAK,EAAE,MAAM,qCAAqC,MAAM,SAAS;AAAA,EACzE;AACA,MAAI,IAAI,OAAO,QAAQ,GAAG;AACxB,eAAW,KAAK,OAAO,UAAU;AAC/B,YAAM,KAAK,EAAE,MAAM,YAAY,EAAE,OAAO,GAAG,MAAM,EAAE,YAAY,EAAE,OAAO,IAAI;AAAA,IAC9E;AAAA,EACF;AAEA,SAAO;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;AAIzG;AC5CO,SAAS,aAAa,OAAuB;AAClD,SAAO,6BAA6B,IAAI,KAAK,CAAC;AAChD;ACDO,SAAS,cAAc,SAA6B,SAAyB;AAClF,MAAI,CAAC,QAAS,QAAO;AACrB,QAAM,OAAO,SAAS,SAAS,EAAE,OAAO,MAAM;AAC9C,MAAI,CAAC,KAAM,QAAO;AAClB,SAAO;AAAA,MACH,aAAa,OAAO,CAAC;AAAA,2BACA,IAAI;AAC/B;ACLO,SAAS,aAAa,QAA4C,SAAyB;AAChG,MAAI,CAAC,IAAI,MAAM,EAAG,QAAO;AACzB,SAAO;AAAA,MACH,aAAa,OAAO,CAAC;AAAA;AAAA,QAEnB,OACC;AAAA,IACC,CAAC,MAAM;AAAA;AAAA,mCAEkB,IAAI,EAAE,IAAI,CAAC,YAAY,KAAK,EAAE,YAAY,CAAA,GAAI,KAAK,IAAI,CAAC,CAAC;AAAA;AAAA,EAAA,EAGnF,KAAK,EAAE,CAAC;AAAA;AAEjB;ACZA,SAAS,iBAAiB,MAAuB;AAC/C,SAAO,iBAAiB,KAAK,IAAI,KAAK,aAAa,KAAK,IAAI;AAC9D;AAGA,SAAS,cAAc,SAIrB;AACA,MAAI,CAAC,QAAS,QAAO,EAAE,WAAW,IAAI,QAAQ,IAAI,WAAW,GAAA;AAC7D,QAAM,OAAO,UAAU,OAAO,EAAE,QAAQ,OAAO,GAAG,EAAE,KAAA;AACpD,MAAI,CAAC,iBAAiB,IAAI,EAAG,QAAO,EAAE,WAAW,IAAI,QAAQ,IAAI,WAAW,KAAA;AAE5E,MAAI,YAAY;AAEhB,QAAM,cAAc,UAAU,MAAM,kBAAkB;AACtD,QAAM,SAAS,cAAc,YAAY,CAAC,EAAE,SAAS;AACrD,MAAI,yBAAyB,UAAU,MAAM,GAAG,YAAY,KAAK,EAAE,KAAA;AAEnE,QAAM,YAAY,UAAU,MAAM,qBAAqB;AACvD,QAAM,YAAY,YAAY,UAAU,CAAC,EAAE,SAAS;AACpD,MAAI,uBAAuB,UAAU,MAAM,GAAG,UAAU,KAAK,EAAE,KAAA;AAE/D,SAAO,EAAE,WAAW,QAAQ,WAAW,UAAA;AACzC;AAEA,SAAS,gBAAgB,OAAgC;AACvD,QAAM,EAAE,WAAW,QAAQ,cAAc,cAAc,MAAM,OAAO;AACpE,QAAM,WAAW,UAAU,MAAM,WAAW,MAAM,OAAO;AAEzD,SAAO;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,YAAY,2BAA2B,SAAS,WAAW,EAAE,OAAO,MAAA,CAAO,CAAC,SAAS,EAAE;AAAA,QAEvF,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;AAEN;AAEO,SAAS,WAAW,MAA8C,SAAyB;AAChG,MAAI,CAAC,IAAI,IAAI,EAAG,QAAO;AACvB,SAAO;AAAA,MACH,aAAa,OAAO,CAAC;AAAA;AAAA,QAEnB,KAAK,IAAI,eAAe,EAAE,KAAK,EAAE,CAAC;AAAA;AAE1C;ACnEO,SAAS,eACd,UACA,SACQ;AACR,MAAI,CAAC,IAAI,QAAQ,EAAG,QAAO;AAC3B,SAAO;AAAA,MACH,aAAa,OAAO,CAAC;AAAA;AAAA,QAEnB,SACC,IAAI,CAAC,MAAM;AACV,UAAM,OAAO,SAAS,EAAE,WAAW;AACnC,WAAO;AAAA;AAAA,qCAEoB,IAAI,EAAE,IAAI,CAAC,YAAY,IAAI;AAAA;AAAA,EAExD,CAAC,EACA,KAAK,EAAE,CAAC;AAAA;AAEjB;ACjBA,SAAS,qBAAqB,OAAqC;AACjE,QAAM,SAAS,CAAC,MAAM,WAAW,MAAM,IAAI,EAAE,OAAO,OAAO,EAAE,KAAK,GAAG;AAErE,MAAI,cAAc;AAClB,MAAI,MAAM,SAAS;AACjB,kBAAc,WAAW,MAAM,OAAO;AAAA,EACxC,WAAW,MAAM,WAAW;AAC1B,UAAM,OAAO,SAAS,OAAO,MAAM,SAAS,EAAE,MAAM,GAAG,EAAE,CAAC,GAAG,EAAE;AAC/D,UAAM,eAAc,oBAAI,KAAA,GAAO,YAAA;AAC/B,kBAAc,QAAQ,cAAc,YAAY,WAAW,MAAM,SAAS,CAAC,KAAK,WAAW,MAAM,SAAS;AAAA,EAC5G;AAEA,QAAM,YAAY,CAAC,MAAM,WAAW;AACpC,MAAI,MAAM,MAAO,WAAU,KAAK,MAAM,KAAK;AAC3C,QAAM,WAAW,UAAU,OAAO,OAAO,EAAE,KAAK,KAAK;AAErD,SAAO;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,wCAAwC,MAAM,QAAQ,IAAI,CAAC,MAAM,SAAS,CAAC,CAAC,EAAE,KAAK,IAAI,CAAC,WACxF,EACN;AAAA;AAEN;AAEO,SAAS,gBACd,WACA,SACQ;AACR,MAAI,CAAC,IAAI,SAAS,EAAG,QAAO;AAC5B,SAAO;AAAA,MACH,aAAa,OAAO,CAAC;AAAA;AAAA,QAEnB,UAAU,IAAI,oBAAoB,EAAE,KAAK,EAAE,CAAC;AAAA;AAEpD;AC1CO,SAAS,gBACd,WACA,SACQ;AACR,MAAI,CAAC,IAAI,SAAS,EAAG,QAAO;AAC5B,SAAO;AAAA,MACH,aAAa,OAAO,CAAC;AAAA;AAAA,QAEnB,UACC,IAAI,CAAC,MAAM;AACV,UAAM,QAAQ,EAAE,UACZ,GAAG,EAAE,SAAS,IAAS,EAAE,OAAO,KAChC,EAAE,aAAa;AACnB,UAAM,QAAQ,EAAE,WACZ,GAAG,EAAE,QAAQ,MAAW,EAAE,YAAY,KACtC,EAAE;AACN,UAAM,UAAU,SAAS,EAAE,OAAO;AAElC,WAAO;AAAA;AAAA,kCAEiB,IAAI,KAAK,CAAC,GAAG,QAAQ,KAAK,IAAI,KAAK,CAAC,MAAM,EAAE,YAAY,OAAO;AAAA;AAAA,EAEzF,CAAC,EACA,KAAK,EAAE,CAAC;AAAA;AAEjB;AC1BO,SAAS,mBACd,cACA,SACQ;AACR,MAAI,CAAC,IAAI,YAAY,EAAG,QAAO;AAC/B,SAAO;AAAA,MACH,aAAa,OAAO,CAAC;AAAA;AAAA,QAEnB,aACC;AAAA,IACC,CAAC,MAAM;AAAA,iCACgB,SAAS,EAAE,IAAI,CAAC;AAAA,EAAA,EAExC,KAAK,EAAE,CAAC;AAAA;AAEjB;ACdO,SAAS,iBAAiB,QAAsB,SAAyB;AAC9E,QAAM,QAAkB,CAAA;AAExB,MAAI,IAAI,OAAO,SAAS,GAAG;AACzB,eAAW,YAAY,OAAO,WAAW;AACvC,YAAM,WAAW,IAAI,SAAS,QAAQ,IAAI,SAAS,SAAS,KAAK,IAAI,IAAI;AACzE,YAAM,QAAQ,SAAS,QAAQ;AAC/B,UAAI,UAAU;AACZ,cAAM;AAAA,UACJ,+DAA+D,IAAI,KAAK,CAAC,YAAY,IAAI,QAAQ,CAAC;AAAA,QAAA;AAAA,MAEtG;AAAA,IACF;AAAA,EACF;AAEA,MAAI,IAAI,OAAO,SAAS,GAAG;AACzB,UAAM,UAAU,OAAO,UACpB,IAAI,CAAC,MAAM,GAAG,EAAE,QAAQ,GAAG,EAAE,UAAU,KAAK,EAAE,OAAO,MAAM,EAAE,EAAE,EAC/D,KAAK,IAAI;AACZ,UAAM;AAAA,MACJ,iFAAiF,IAAI,OAAO,CAAC;AAAA,IAAA;AAAA,EAEjG;AAEA,MAAI,IAAI,OAAO,UAAU,GAAG;AAC1B,UAAM;AAAA,MACJ;AAAA,IAAA;AAAA,EAEJ;AAEA,MAAI,MAAM,WAAW,EAAG,QAAO;AAE/B,SAAO;AAAA,MACH,aAAa,OAAO,CAAC;AAAA;AAAA,QAEnB,MAAM,KAAK,UAAU,CAAC;AAAA;AAE9B;;AC5BO,SAAS,OAAO,QAA8B;AhBd9C;AgBeL,QAAM,IAAI,EAAE,GAAG,iBAAA;AAEf,QAAM,gBAAe,sCAAQ,SAAR,mBAAc;AACnC,MAAI,cAAc;AAChB,eAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,YAAY,GAAG;AACvD,UAAI,SAAS,OAAO,GAAG;AACrB,UAAE,GAAG,IAAI;AAAA,MACX;AAAA,IACF;AAAA,EACF;AAEA,QAAM,UAAS,iCAAQ,WAAU,CAAA;AAEjC,SAAO;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;AAI5C;AClDO,MAAM,mBAAmB;AAAA,EAC9B,WAAW;AAAA,EACX,QAAQ;AAAA,EACR,QAAQ;AAAA,IACN,KAAK;AAAA,IACL,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,MAAM;AAAA,EAAA;AAEV;"}
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[] = [\r\n 'January',\r\n 'February',\r\n 'March',\r\n 'April',\r\n 'May',\r\n 'June',\r\n 'July',\r\n 'August',\r\n 'September',\r\n 'October',\r\n 'November',\r\n 'December',\r\n];\r\n\r\nexport const COUNTRY_NAMES: Readonly<Record<string, string>> = {\r\n GB: 'United Kingdom',\r\n US: 'United States',\r\n CA: 'Canada',\r\n AU: 'Australia',\r\n DE: 'Germany',\r\n FR: 'France',\r\n IN: 'India',\r\n JP: 'Japan',\r\n CN: 'China',\r\n BR: 'Brazil',\r\n NL: 'Netherlands',\r\n IT: 'Italy',\r\n ES: 'Spain',\r\n SE: 'Sweden',\r\n CH: 'Switzerland',\r\n SG: 'Singapore',\r\n NZ: 'New Zealand',\r\n IE: 'Ireland',\r\n AE: 'UAE',\r\n};\r\n\r\nexport const DEFAULT_HEADINGS: Readonly<Record<string, string>> = {\r\n summary: 'Summary',\r\n skills: 'Core Skills',\r\n experience: 'Experience',\r\n projects: 'Selected Projects',\r\n education: 'Education',\r\n volunteer: 'Leadership & Volunteering',\r\n certifications: 'Credentials',\r\n additional: 'Additional',\r\n};\r\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(/&amp;/g, '&')\n .replace(/&lt;/g, '<')\n .replace(/&gt;/g, '>')\n .replace(/&quot;/g, '\"')\n .replace(/&#39;/g, \"'\")\n .replace(/&apos;/g, \"'\")\n .replace(/&nbsp;/g, ' ')\n .replace(/&mdash;/g, '\\u2014')\n .replace(/&ndash;/g, '\\u2013')\n .replace(/&hellip;/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, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#39;');\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, '&quot;')}\" target=\"_blank\" rel=\"noopener\">`;\n })\n .replace(/<mark\\s+[^>]*class=\"([^\"]*)\"[^>]*>/gi, (_, cls: string) =>\n `<mark class=\"${cls.replace(/\"/g, '&quot;')}\">`,\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';\r\nimport { sectionTitle } from './shared.js';\r\n\r\nexport function renderSummary(summary: string | undefined, heading: string): string {\r\n if (!summary) return '';\r\n const html = richText(summary, { block: true });\r\n if (!html) return '';\r\n return `\r\n ${sectionTitle(heading)}\r\n <div class=\"summary\">${html}</div>`;\r\n}\r\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';\r\nimport { esc } from '../utils/escape.js';\r\nimport { dateRange } from '../utils/dates.js';\r\nimport { has, richText, stripHtml } from '../utils/text.js';\r\nimport { sectionTitle } from './shared.js';\r\n\r\n/** Check whether summary uses structured Tech-stack / Client format. */\r\nfunction isStructuredMeta(text: string): boolean {\r\n return /Tech-stack:\\s/i.test(text) || /Client:\\s/i.test(text);\r\n}\r\n\r\n/** Extract tech-stack, client, and any remaining narrative from a work summary. */\r\nfunction parseWorkMeta(summary: string | undefined): {\r\n techStack: string;\r\n client: string;\r\n narrative: string;\r\n} {\r\n if (!summary) return { techStack: '', client: '', narrative: '' };\r\n const text = stripHtml(summary).replace(/\\n/g, ' ').trim();\r\n if (!isStructuredMeta(text)) return { techStack: '', client: '', narrative: text };\r\n\r\n let remaining = text;\r\n\r\n const clientMatch = remaining.match(/Client:\\s*(.+)$/i);\r\n const client = clientMatch ? clientMatch[1].trim() : '';\r\n if (clientMatch) remaining = remaining.slice(0, clientMatch.index).trim();\r\n\r\n const techMatch = remaining.match(/Tech-stack:\\s*(.+)/i);\r\n const techStack = techMatch ? techMatch[1].trim() : '';\r\n if (techMatch) remaining = remaining.slice(0, techMatch.index).trim();\r\n\r\n return { techStack, client, narrative: remaining };\r\n}\r\n\r\nfunction renderWorkEntry(entry: ResumeWorkEntry): string {\r\n const { techStack, client, narrative } = parseWorkMeta(entry.summary);\r\n const duration = dateRange(entry.startDate, entry.endDate);\r\n\r\n return `\r\n <div class=\"work-entry\">\r\n <div class=\"work-header\">\r\n <div class=\"work-title\">${esc(entry.name)} - ${esc(entry.position)}</div>\r\n <div class=\"work-duration\">${duration ? `Duration: ${esc(duration)}` : ''}</div>\r\n </div>\r\n ${\r\n techStack || client\r\n ? `\r\n <div class=\"work-meta\">\r\n <div class=\"work-tech\">${techStack ? `Tech-stack: ${esc(techStack)}` : ''}</div>\r\n <div class=\"work-client\">${client ? `Client: ${esc(client)}` : ''}</div>\r\n </div>`\r\n : ''\r\n }\r\n ${narrative ? `<p class=\"work-summary\">${richText(narrative, { block: false })}</p>` : ''}\r\n ${\r\n has(entry.highlights)\r\n ? `\r\n <ul class=\"work-highlights\">\r\n ${entry.highlights.map((h) => `<li>${richText(h)}</li>`).join('\\n ')}\r\n </ul>`\r\n : ''\r\n }\r\n </div>`;\r\n}\r\n\r\nexport function renderWork(work: readonly ResumeWorkEntry[] | undefined, heading: string): string {\r\n if (!has(work)) return '';\r\n return `\r\n ${sectionTitle(heading)}\r\n <div class=\"section-body\">\r\n ${work.map(renderWorkEntry).join('')}\r\n </div>`;\r\n}\r\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';\r\nimport { esc } from '../utils/escape.js';\r\nimport { formatDate } from '../utils/dates.js';\r\nimport { has, richText } from '../utils/text.js';\r\nimport { sectionTitle } from './shared.js';\r\n\r\nfunction renderEducationEntry(entry: ResumeEducationEntry): string {\r\n const degree = [entry.studyType, entry.area].filter(Boolean).join(' ');\r\n\r\n let yearDisplay = '';\r\n if (entry.endDate) {\r\n yearDisplay = formatDate(entry.endDate);\r\n } else if (entry.startDate) {\r\n const year = parseInt(String(entry.startDate).split('-')[0], 10);\r\n const currentYear = new Date().getFullYear();\r\n yearDisplay = year >= currentYear ? `Expected ${formatDate(entry.startDate)}` : formatDate(entry.startDate);\r\n }\r\n\r\n const instParts = [entry.institution];\r\n if (entry.score) instParts.push(entry.score);\r\n const instLine = instParts.filter(Boolean).join(' | ');\r\n\r\n return `\r\n <div class=\"edu-entry\">\r\n <div class=\"edu-header\">\r\n <div class=\"edu-degree\">${esc(degree)}</div>\r\n <div class=\"edu-year\">${esc(yearDisplay)}</div>\r\n </div>\r\n ${entry.institution ? `<div class=\"edu-institution\">${esc(instLine)}</div>` : ''}\r\n ${\r\n has(entry.courses)\r\n ? `<div class=\"edu-courses\">Coursework: ${entry.courses.map((c) => richText(c)).join(', ')}</div>`\r\n : ''\r\n }\r\n </div>`;\r\n}\r\n\r\nexport function renderEducation(\r\n education: readonly ResumeEducationEntry[] | undefined,\r\n heading: string,\r\n): string {\r\n if (!has(education)) return '';\r\n return `\r\n ${sectionTitle(heading)}\r\n <div class=\"section-body\">\r\n ${education.map(renderEducationEntry).join('')}\r\n </div>`;\r\n}\r\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';\r\nimport { DEFAULT_HEADINGS } from './constants.js';\r\nimport { esc } from './utils/escape.js';\r\nimport { renderHeader } from './sections/header.js';\r\nimport { renderSummary } from './sections/summary.js';\r\nimport { renderSkills } from './sections/skills.js';\r\nimport { renderWork } from './sections/work.js';\r\nimport { renderProjects } from './sections/projects.js';\r\nimport { renderEducation } from './sections/education.js';\r\nimport { renderVolunteer } from './sections/volunteer.js';\r\nimport { renderCertificates } from './sections/certificates.js';\r\nimport { renderAdditional } from './sections/additional.js';\r\nimport css from './styles/academic.css?inline';\r\n\r\nexport function render(resume: ResumeSchema): string {\r\n const h = { ...DEFAULT_HEADINGS };\r\n\r\n const metaHeadings = resume?.meta?.headings;\r\n if (metaHeadings) {\r\n for (const [key, value] of Object.entries(metaHeadings)) {\r\n if (value && key in h) {\r\n h[key] = value;\r\n }\r\n }\r\n }\r\n\r\n const basics = resume?.basics || {};\r\n\r\n return `<!DOCTYPE html>\r\n<html lang=\"en\">\r\n<head>\r\n <meta charset=\"UTF-8\" />\r\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\r\n <title>${esc(basics.name || 'Resume')}</title>\r\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\" />\r\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin />\r\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\" />\r\n <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css\" />\r\n <style>${css}</style>\r\n</head>\r\n<body>\r\n <div class=\"resume\">\r\n ${renderHeader(basics)}\r\n ${renderSummary(basics.summary, h.summary)}\r\n ${renderSkills(resume.skills, h.skills)}\r\n ${renderWork(resume.work, h.experience)}\r\n ${renderProjects(resume.projects, h.projects)}\r\n ${renderEducation(resume.education, h.education)}\r\n ${renderVolunteer(resume.volunteer, h.volunteer)}\r\n ${renderCertificates(resume.certificates, h.certifications)}\r\n ${renderAdditional(resume, h.additional)}\r\n </div>\r\n</body>\r\n</html>`;\r\n}\r\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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"}