jsonresume-theme-academic 1.0.0
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/LICENSE +21 -0
- package/README.md +98 -0
- package/dist/index.cjs +390 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +135 -0
- package/dist/index.js +390 -0
- package/dist/index.js.map +1 -0
- package/dist/index.umd.cjs +394 -0
- package/dist/index.umd.cjs.map +1 -0
- package/package.json +84 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/** PDF rendering options for Puppeteer/Gotenberg. */
|
|
2
|
+
export declare const pdfRenderOptions: {
|
|
3
|
+
mediaType: "print";
|
|
4
|
+
format: "A4";
|
|
5
|
+
margin: {
|
|
6
|
+
top: string;
|
|
7
|
+
right: string;
|
|
8
|
+
bottom: string;
|
|
9
|
+
left: string;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export declare function render(resume: ResumeSchema): string;
|
|
14
|
+
|
|
15
|
+
declare interface ResumeBasics {
|
|
16
|
+
readonly name?: string;
|
|
17
|
+
readonly label?: string;
|
|
18
|
+
readonly image?: string;
|
|
19
|
+
readonly email?: string;
|
|
20
|
+
readonly phone?: string;
|
|
21
|
+
readonly url?: string;
|
|
22
|
+
readonly summary?: string;
|
|
23
|
+
readonly location?: ResumeLocation;
|
|
24
|
+
readonly profiles?: readonly ResumeProfile[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
declare interface ResumeCertificate {
|
|
28
|
+
readonly name?: string;
|
|
29
|
+
readonly date?: string;
|
|
30
|
+
readonly issuer?: string;
|
|
31
|
+
readonly url?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
declare interface ResumeEducationEntry {
|
|
35
|
+
readonly institution?: string;
|
|
36
|
+
readonly url?: string;
|
|
37
|
+
readonly area?: string;
|
|
38
|
+
readonly studyType?: string;
|
|
39
|
+
readonly startDate?: string;
|
|
40
|
+
readonly endDate?: string;
|
|
41
|
+
readonly score?: string;
|
|
42
|
+
readonly courses?: readonly string[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
declare interface ResumeInterest {
|
|
46
|
+
readonly name?: string;
|
|
47
|
+
readonly keywords?: readonly string[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
declare interface ResumeLanguage {
|
|
51
|
+
readonly language?: string;
|
|
52
|
+
readonly fluency?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** JSON Resume schema types with academic theme extensions. */
|
|
56
|
+
declare interface ResumeLocation {
|
|
57
|
+
readonly address?: string;
|
|
58
|
+
readonly postalCode?: string;
|
|
59
|
+
readonly city?: string;
|
|
60
|
+
readonly countryCode?: string;
|
|
61
|
+
readonly region?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
declare interface ResumeMeta {
|
|
65
|
+
readonly canonical?: string;
|
|
66
|
+
readonly version?: string;
|
|
67
|
+
readonly lastModified?: string;
|
|
68
|
+
readonly headings?: Readonly<Partial<Record<string, string>>>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
declare interface ResumeProfile {
|
|
72
|
+
readonly network?: string;
|
|
73
|
+
readonly username?: string;
|
|
74
|
+
readonly url?: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
declare interface ResumeProject {
|
|
78
|
+
readonly name?: string;
|
|
79
|
+
readonly description?: string;
|
|
80
|
+
readonly highlights?: readonly string[];
|
|
81
|
+
readonly keywords?: readonly string[];
|
|
82
|
+
readonly startDate?: string;
|
|
83
|
+
readonly endDate?: string;
|
|
84
|
+
readonly url?: string;
|
|
85
|
+
readonly roles?: readonly string[];
|
|
86
|
+
readonly entity?: string;
|
|
87
|
+
readonly type?: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
declare interface ResumeReference {
|
|
91
|
+
readonly name?: string;
|
|
92
|
+
readonly reference?: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export declare interface ResumeSchema {
|
|
96
|
+
readonly basics?: ResumeBasics;
|
|
97
|
+
readonly work?: readonly ResumeWorkEntry[];
|
|
98
|
+
readonly volunteer?: readonly ResumeVolunteerEntry[];
|
|
99
|
+
readonly education?: readonly ResumeEducationEntry[];
|
|
100
|
+
readonly skills?: readonly ResumeSkill[];
|
|
101
|
+
readonly projects?: readonly ResumeProject[];
|
|
102
|
+
readonly certificates?: readonly ResumeCertificate[];
|
|
103
|
+
readonly interests?: readonly ResumeInterest[];
|
|
104
|
+
readonly languages?: readonly ResumeLanguage[];
|
|
105
|
+
readonly references?: readonly ResumeReference[];
|
|
106
|
+
readonly meta?: ResumeMeta;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
declare interface ResumeSkill {
|
|
110
|
+
readonly name?: string;
|
|
111
|
+
readonly level?: string;
|
|
112
|
+
readonly keywords?: readonly string[];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
declare interface ResumeVolunteerEntry {
|
|
116
|
+
readonly organization?: string;
|
|
117
|
+
readonly position?: string;
|
|
118
|
+
readonly url?: string;
|
|
119
|
+
readonly startDate?: string;
|
|
120
|
+
readonly endDate?: string;
|
|
121
|
+
readonly summary?: string;
|
|
122
|
+
readonly highlights?: readonly string[];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
declare interface ResumeWorkEntry {
|
|
126
|
+
readonly name?: string;
|
|
127
|
+
readonly position?: string;
|
|
128
|
+
readonly url?: string;
|
|
129
|
+
readonly startDate?: string;
|
|
130
|
+
readonly endDate?: string;
|
|
131
|
+
readonly summary?: string;
|
|
132
|
+
readonly highlights?: readonly string[];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export { }
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
const MONTHS = [
|
|
2
|
+
"January",
|
|
3
|
+
"February",
|
|
4
|
+
"March",
|
|
5
|
+
"April",
|
|
6
|
+
"May",
|
|
7
|
+
"June",
|
|
8
|
+
"July",
|
|
9
|
+
"August",
|
|
10
|
+
"September",
|
|
11
|
+
"October",
|
|
12
|
+
"November",
|
|
13
|
+
"December"
|
|
14
|
+
];
|
|
15
|
+
const COUNTRY_NAMES = {
|
|
16
|
+
GB: "United Kingdom",
|
|
17
|
+
US: "United States",
|
|
18
|
+
CA: "Canada",
|
|
19
|
+
AU: "Australia",
|
|
20
|
+
DE: "Germany",
|
|
21
|
+
FR: "France",
|
|
22
|
+
IN: "India",
|
|
23
|
+
JP: "Japan",
|
|
24
|
+
CN: "China",
|
|
25
|
+
BR: "Brazil",
|
|
26
|
+
NL: "Netherlands",
|
|
27
|
+
IT: "Italy",
|
|
28
|
+
ES: "Spain",
|
|
29
|
+
SE: "Sweden",
|
|
30
|
+
CH: "Switzerland",
|
|
31
|
+
SG: "Singapore",
|
|
32
|
+
NZ: "New Zealand",
|
|
33
|
+
IE: "Ireland",
|
|
34
|
+
AE: "UAE"
|
|
35
|
+
};
|
|
36
|
+
const DEFAULT_HEADINGS = {
|
|
37
|
+
skills: "Core Skills",
|
|
38
|
+
experience: "Experience",
|
|
39
|
+
projects: "Selected Projects",
|
|
40
|
+
education: "Education",
|
|
41
|
+
volunteer: "Leadership & Volunteering",
|
|
42
|
+
certifications: "Credentials",
|
|
43
|
+
additional: "Additional"
|
|
44
|
+
};
|
|
45
|
+
function decodeEntities(text) {
|
|
46
|
+
return text.replace(/&#(\d+);/g, (_, c) => String.fromCharCode(Number(c))).replace(/&#x([0-9a-fA-F]+);/g, (_, h) => String.fromCharCode(parseInt(h, 16))).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/'/g, "'").replace(/ /g, " ").replace(/—/g, "—").replace(/–/g, "–").replace(/…/g, "…");
|
|
47
|
+
}
|
|
48
|
+
function esc(text) {
|
|
49
|
+
if (text == null) return "";
|
|
50
|
+
return String(text).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
51
|
+
}
|
|
52
|
+
function formatDate(dateStr) {
|
|
53
|
+
if (!dateStr) return "";
|
|
54
|
+
const parts = String(dateStr).split("-");
|
|
55
|
+
if (parts.length === 1) return parts[0];
|
|
56
|
+
const year = parts[0];
|
|
57
|
+
const month = MONTHS[parseInt(parts[1], 10) - 1] || "";
|
|
58
|
+
return `${month} ${year}`;
|
|
59
|
+
}
|
|
60
|
+
function dateRange(start, end) {
|
|
61
|
+
const s = formatDate(start);
|
|
62
|
+
const e = end ? formatDate(end) : "";
|
|
63
|
+
if (!s) return "";
|
|
64
|
+
if (!e) return s;
|
|
65
|
+
return `${s} to ${e}`;
|
|
66
|
+
}
|
|
67
|
+
function regionName(code) {
|
|
68
|
+
if (!code) return "";
|
|
69
|
+
return COUNTRY_NAMES[code.toUpperCase()] || code;
|
|
70
|
+
}
|
|
71
|
+
function profileIcon(network) {
|
|
72
|
+
if (!network) return '<i class="fa-solid fa-link"></i>';
|
|
73
|
+
const n = network.toLowerCase();
|
|
74
|
+
if (n === "linkedin") return '<i class="fa-brands fa-linkedin-in"></i>';
|
|
75
|
+
if (n === "github") return '<i class="fa-brands fa-github"></i>';
|
|
76
|
+
if (n === "twitter" || n === "x") return '<i class="fa-brands fa-x-twitter"></i>';
|
|
77
|
+
if (n === "stackoverflow") return '<i class="fa-brands fa-stack-overflow"></i>';
|
|
78
|
+
return '<i class="fa-solid fa-link"></i>';
|
|
79
|
+
}
|
|
80
|
+
const BLOCK_TAGS = /* @__PURE__ */ new Set(["p", "ul", "ol", "li", "blockquote"]);
|
|
81
|
+
const INLINE_TAGS = /* @__PURE__ */ new Set([
|
|
82
|
+
"br",
|
|
83
|
+
"b",
|
|
84
|
+
"strong",
|
|
85
|
+
"i",
|
|
86
|
+
"em",
|
|
87
|
+
"u",
|
|
88
|
+
"s",
|
|
89
|
+
"mark",
|
|
90
|
+
"a",
|
|
91
|
+
"sub",
|
|
92
|
+
"sup",
|
|
93
|
+
"code"
|
|
94
|
+
]);
|
|
95
|
+
const ALLOWED_TAGS = /* @__PURE__ */ new Set([...BLOCK_TAGS, ...INLINE_TAGS]);
|
|
96
|
+
const SAFE_URL_SCHEMES = /^(?:https?|mailto|tel):/i;
|
|
97
|
+
const RELATIVE_URL = /^[#/?.]/;
|
|
98
|
+
function isSafeUrl(href) {
|
|
99
|
+
const trimmed = href.trim();
|
|
100
|
+
if (!trimmed) return false;
|
|
101
|
+
return SAFE_URL_SCHEMES.test(trimmed) || RELATIVE_URL.test(trimmed);
|
|
102
|
+
}
|
|
103
|
+
function sanitizeHtml(html, { inline = false } = {}) {
|
|
104
|
+
if (!html) return "";
|
|
105
|
+
const allowed = inline ? INLINE_TAGS : ALLOWED_TAGS;
|
|
106
|
+
return String(html).replace(/<a\s+[^>]*href="([^"]*)"[^>]*>/gi, (_, href) => {
|
|
107
|
+
if (!isSafeUrl(href)) return "";
|
|
108
|
+
return `<a href="${href.replace(/"/g, """)}" target="_blank" rel="noopener">`;
|
|
109
|
+
}).replace(
|
|
110
|
+
/<mark\s+[^>]*class="([^"]*)"[^>]*>/gi,
|
|
111
|
+
(_, cls) => `<mark class="${cls.replace(/"/g, """)}">`
|
|
112
|
+
).replace(/<(\/?)([a-z][a-z0-9]*)\s*[^>]*?(\/?)>/gi, (match, slash, tag, selfClose) => {
|
|
113
|
+
const lower = tag.toLowerCase();
|
|
114
|
+
if (lower === "a" && !slash && match.includes("href=")) return match;
|
|
115
|
+
if (lower === "mark" && !slash && match.includes("class=")) return match;
|
|
116
|
+
if (allowed.has(lower)) return `<${slash}${lower}${selfClose}>`;
|
|
117
|
+
if (inline && BLOCK_TAGS.has(lower)) return slash ? " " : "";
|
|
118
|
+
return "";
|
|
119
|
+
}).replace(/<p>\s*<\/p>/gi, "").replace(/\s{2,}/g, " ").trim();
|
|
120
|
+
}
|
|
121
|
+
function stripHtml(text) {
|
|
122
|
+
if (!text) return "";
|
|
123
|
+
return decodeEntities(
|
|
124
|
+
String(text).replace(/<br\s*\/?>/gi, " ").replace(/<\/?(p|div|li)[^>]*>/gi, " ").replace(/<[^>]+>/g, "")
|
|
125
|
+
).replace(/\s{2,}/g, " ").trim();
|
|
126
|
+
}
|
|
127
|
+
const isHtml = (text) => /<[a-z][\s\S]*>/i.test(text);
|
|
128
|
+
const hasEntities = (text) => /&(?:amp|lt|gt|quot|#\d+|#x[0-9a-f]+);/i.test(text);
|
|
129
|
+
function richText(text, { block = false } = {}) {
|
|
130
|
+
if (text == null) return "";
|
|
131
|
+
const s = String(text);
|
|
132
|
+
if (isHtml(s)) return sanitizeHtml(s, { inline: !block });
|
|
133
|
+
if (hasEntities(s)) return esc(decodeEntities(s));
|
|
134
|
+
return esc(s);
|
|
135
|
+
}
|
|
136
|
+
function has(arr) {
|
|
137
|
+
return Array.isArray(arr) && arr.length > 0;
|
|
138
|
+
}
|
|
139
|
+
function renderHeader(basics) {
|
|
140
|
+
if (!basics) return "";
|
|
141
|
+
const location = basics.location ? [basics.location.city, regionName(basics.location.region)].filter(Boolean).join(", ") : "";
|
|
142
|
+
const lines = [];
|
|
143
|
+
if (location)
|
|
144
|
+
lines.push({ icon: '<i class="fa-solid fa-location-dot"></i>', text: location });
|
|
145
|
+
if (basics.phone)
|
|
146
|
+
lines.push({ icon: '<i class="fa-solid fa-square-phone"></i>', text: basics.phone });
|
|
147
|
+
if (basics.email)
|
|
148
|
+
lines.push({ icon: '<i class="fa-solid fa-envelope"></i>', text: basics.email });
|
|
149
|
+
if (basics.url) {
|
|
150
|
+
const display = basics.url.replace(/^https?:\/\//, "");
|
|
151
|
+
lines.push({ icon: '<i class="fa-solid fa-globe"></i>', text: display });
|
|
152
|
+
}
|
|
153
|
+
if (has(basics.profiles)) {
|
|
154
|
+
for (const p of basics.profiles) {
|
|
155
|
+
lines.push({ icon: profileIcon(p.network), text: p.username || p.url || "" });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return `
|
|
159
|
+
<header class="header">
|
|
160
|
+
<div class="header-left">
|
|
161
|
+
<h1 class="name">${esc(basics.name)}</h1>
|
|
162
|
+
<div class="label">${esc(basics.label)}</div>
|
|
163
|
+
</div>
|
|
164
|
+
<div class="contact-info">
|
|
165
|
+
${lines.map((l) => `<div class="contact-line">${esc(l.text)} ${l.icon}</div>`).join("\n ")}
|
|
166
|
+
</div>
|
|
167
|
+
</header>
|
|
168
|
+
<hr class="header-rule" />`;
|
|
169
|
+
}
|
|
170
|
+
function renderSummary(summary) {
|
|
171
|
+
if (!summary) return "";
|
|
172
|
+
const html = richText(summary, { block: true });
|
|
173
|
+
if (!html) return "";
|
|
174
|
+
return `<div class="summary">${html}</div>`;
|
|
175
|
+
}
|
|
176
|
+
function sectionTitle(title) {
|
|
177
|
+
return `<h2 class="section-title">${esc(title)}</h2>`;
|
|
178
|
+
}
|
|
179
|
+
function renderSkills(skills, heading) {
|
|
180
|
+
if (!has(skills)) return "";
|
|
181
|
+
return `
|
|
182
|
+
${sectionTitle(heading)}
|
|
183
|
+
<div class="section-body">
|
|
184
|
+
${skills.map(
|
|
185
|
+
(s) => `
|
|
186
|
+
<div class="bullet-item">
|
|
187
|
+
<span class="skill-name">${esc(s.name)}:</span> ${esc((s.keywords || []).join(", "))}
|
|
188
|
+
</div>`
|
|
189
|
+
).join("")}
|
|
190
|
+
</div>`;
|
|
191
|
+
}
|
|
192
|
+
function parseWorkMeta(summary) {
|
|
193
|
+
if (!summary) return { techStack: "", client: "" };
|
|
194
|
+
const text = stripHtml(summary).replace(/\n/g, " ").trim();
|
|
195
|
+
const clientMatch = text.match(/Client:\s*(.+)$/i);
|
|
196
|
+
const client = clientMatch ? clientMatch[1].trim() : "";
|
|
197
|
+
const techPart = clientMatch ? text.slice(0, clientMatch.index).trim() : text;
|
|
198
|
+
const techMatch = techPart.match(/Tech-stack:\s*(.+)/i);
|
|
199
|
+
const techStack = techMatch ? techMatch[1].trim() : "";
|
|
200
|
+
return { techStack, client };
|
|
201
|
+
}
|
|
202
|
+
function renderWorkEntry(entry) {
|
|
203
|
+
const { techStack, client } = parseWorkMeta(entry.summary);
|
|
204
|
+
const duration = dateRange(entry.startDate, entry.endDate);
|
|
205
|
+
return `
|
|
206
|
+
<div class="work-entry">
|
|
207
|
+
<div class="work-header">
|
|
208
|
+
<div class="work-title">${esc(entry.name)} - ${esc(entry.position)}</div>
|
|
209
|
+
<div class="work-duration">${duration ? `Duration: ${esc(duration)}` : ""}</div>
|
|
210
|
+
</div>
|
|
211
|
+
${techStack || client ? `
|
|
212
|
+
<div class="work-meta">
|
|
213
|
+
<div class="work-tech">${techStack ? `Tech-stack: ${esc(techStack)}` : ""}</div>
|
|
214
|
+
<div class="work-client">${client ? `Client: ${esc(client)}` : ""}</div>
|
|
215
|
+
</div>` : ""}
|
|
216
|
+
${has(entry.highlights) ? `
|
|
217
|
+
<ul class="work-highlights">
|
|
218
|
+
${entry.highlights.map((h) => `<li>${richText(h)}</li>`).join("\n ")}
|
|
219
|
+
</ul>` : ""}
|
|
220
|
+
</div>`;
|
|
221
|
+
}
|
|
222
|
+
function renderWork(work, heading) {
|
|
223
|
+
if (!has(work)) return "";
|
|
224
|
+
return `
|
|
225
|
+
${sectionTitle(heading)}
|
|
226
|
+
<div class="section-body">
|
|
227
|
+
${work.map(renderWorkEntry).join("")}
|
|
228
|
+
</div>`;
|
|
229
|
+
}
|
|
230
|
+
function renderProjects(projects, heading) {
|
|
231
|
+
if (!has(projects)) return "";
|
|
232
|
+
return `
|
|
233
|
+
${sectionTitle(heading)}
|
|
234
|
+
<div class="section-body">
|
|
235
|
+
${projects.map((p) => {
|
|
236
|
+
const desc = richText(p.description);
|
|
237
|
+
return `
|
|
238
|
+
<div class="bullet-item">
|
|
239
|
+
<span class="project-name">${esc(p.name)}:</span> ${desc}
|
|
240
|
+
</div>`;
|
|
241
|
+
}).join("")}
|
|
242
|
+
</div>`;
|
|
243
|
+
}
|
|
244
|
+
function renderEducationEntry(entry) {
|
|
245
|
+
const degree = [entry.studyType, entry.area].filter(Boolean).join(" ");
|
|
246
|
+
let yearDisplay = "";
|
|
247
|
+
if (entry.endDate) {
|
|
248
|
+
yearDisplay = formatDate(entry.endDate);
|
|
249
|
+
} else if (entry.startDate) {
|
|
250
|
+
const year = parseInt(String(entry.startDate).split("-")[0], 10);
|
|
251
|
+
const currentYear = (/* @__PURE__ */ new Date()).getFullYear();
|
|
252
|
+
yearDisplay = year >= currentYear ? `Expected ${formatDate(entry.startDate)}` : formatDate(entry.startDate);
|
|
253
|
+
}
|
|
254
|
+
const instParts = [entry.institution];
|
|
255
|
+
if (entry.score) instParts.push(entry.score);
|
|
256
|
+
const instLine = instParts.filter(Boolean).join(" | ");
|
|
257
|
+
return `
|
|
258
|
+
<div class="edu-entry">
|
|
259
|
+
<div class="edu-header">
|
|
260
|
+
<div class="edu-degree">${esc(degree)}</div>
|
|
261
|
+
<div class="edu-year">${esc(yearDisplay)}</div>
|
|
262
|
+
</div>
|
|
263
|
+
${entry.institution ? `<div class="edu-institution">${esc(instLine)}</div>` : ""}
|
|
264
|
+
${has(entry.courses) ? `
|
|
265
|
+
<div class="edu-courses">
|
|
266
|
+
${entry.courses.map((c) => `<div>${richText(c)}</div>`).join("")}
|
|
267
|
+
</div>` : ""}
|
|
268
|
+
</div>`;
|
|
269
|
+
}
|
|
270
|
+
function renderEducation(education, heading) {
|
|
271
|
+
if (!has(education)) return "";
|
|
272
|
+
return `
|
|
273
|
+
${sectionTitle(heading)}
|
|
274
|
+
<div class="section-body">
|
|
275
|
+
${education.map(renderEducationEntry).join("")}
|
|
276
|
+
</div>`;
|
|
277
|
+
}
|
|
278
|
+
function renderVolunteer(volunteer, heading) {
|
|
279
|
+
if (!has(volunteer)) return "";
|
|
280
|
+
return `
|
|
281
|
+
${sectionTitle(heading)}
|
|
282
|
+
<div class="section-body">
|
|
283
|
+
${volunteer.map((v) => {
|
|
284
|
+
const years = v.endDate ? `${v.startDate}–${v.endDate}` : v.startDate || "";
|
|
285
|
+
const title = v.position ? `${v.position} – ${v.organization}` : v.organization;
|
|
286
|
+
const summary = richText(v.summary);
|
|
287
|
+
return `
|
|
288
|
+
<div class="bullet-item volunteer-item">
|
|
289
|
+
<span class="vol-title">${esc(title)}${years ? ` (${esc(years)})` : ""}:</span> ${summary}
|
|
290
|
+
</div>`;
|
|
291
|
+
}).join("")}
|
|
292
|
+
</div>`;
|
|
293
|
+
}
|
|
294
|
+
function renderCertificates(certificates, heading) {
|
|
295
|
+
if (!has(certificates)) return "";
|
|
296
|
+
return `
|
|
297
|
+
${sectionTitle(heading)}
|
|
298
|
+
<div class="section-body">
|
|
299
|
+
${certificates.map(
|
|
300
|
+
(c) => `
|
|
301
|
+
<div class="bullet-item">${richText(c.name)}</div>`
|
|
302
|
+
).join("")}
|
|
303
|
+
</div>`;
|
|
304
|
+
}
|
|
305
|
+
function renderAdditional(resume, heading) {
|
|
306
|
+
const parts = [];
|
|
307
|
+
if (has(resume.interests)) {
|
|
308
|
+
for (const interest of resume.interests) {
|
|
309
|
+
const keywords = has(interest.keywords) ? interest.keywords.join(", ") : "";
|
|
310
|
+
const label = interest.name || "Interests";
|
|
311
|
+
if (keywords) {
|
|
312
|
+
parts.push(
|
|
313
|
+
`<div class="additional-line"><span class="additional-label">${esc(label)}:</span> ${esc(keywords)}</div>`
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (has(resume.languages)) {
|
|
319
|
+
const langStr = resume.languages.map((l) => `${l.language}${l.fluency ? ` (${l.fluency})` : ""}`).join(", ");
|
|
320
|
+
parts.push(
|
|
321
|
+
`<div class="additional-line"><span class="additional-label">Languages:</span> ${esc(langStr)}</div>`
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
if (has(resume.references)) {
|
|
325
|
+
parts.push(
|
|
326
|
+
`<div class="additional-line"><span class="additional-label">Referees:</span> Available on request</div>`
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
if (parts.length === 0) return "";
|
|
330
|
+
return `
|
|
331
|
+
${sectionTitle(heading)}
|
|
332
|
+
<div class="section-body">
|
|
333
|
+
${parts.join("\n ")}
|
|
334
|
+
</div>`;
|
|
335
|
+
}
|
|
336
|
+
const css = "@page {\n size: A4;\n margin: 12mm 14mm;\n}\n\n* {\n box-sizing: border-box;\n margin: 0;\n padding: 0;\n}\n\nhtml, body {\n font-family: 'EB Garamond', Georgia, 'Palatino Linotype', 'Times New Roman', serif;\n font-size: 10pt;\n line-height: 1.32;\n color: #000;\n -webkit-print-color-adjust: exact;\n print-color-adjust: exact;\n}\n\n/* Browser-only padding (overridden by @page margin in print/PDF) */\nbody {\n padding: 12mm 14mm;\n}\n@media print {\n body { padding: 0; }\n}\n\n/* ─── Header ─── */\n\n.header {\n display: flex;\n justify-content: space-between;\n align-items: flex-start;\n gap: 16px;\n}\n\n.header-left {\n flex: 1;\n min-width: 0;\n}\n\n.name {\n font-size: 32pt;\n font-weight: 400;\n line-height: 1.05;\n color: #000;\n letter-spacing: 0.3px;\n}\n\n.label {\n font-style: italic;\n font-size: 10.5pt;\n margin-top: 4px;\n color: #222;\n line-height: 1.3;\n}\n\n.contact-info {\n text-align: right;\n font-size: 8.5pt;\n line-height: 1.65;\n flex-shrink: 0;\n padding-top: 4px;\n}\n\n.contact-line {\n white-space: nowrap;\n}\n\n.contact-line i {\n display: inline-block;\n width: 11px;\n text-align: center;\n margin-left: 5px;\n font-size: 7.5pt;\n}\n\n.header-rule {\n border: none;\n border-top: 0.5px solid #666;\n margin: 6px 0 3px;\n}\n\n/* ─── Summary ─── */\n\n.summary {\n font-style: italic;\n font-size: 9pt;\n line-height: 1.38;\n margin-bottom: 2px;\n text-align: justify;\n}\n\n/* ─── Section Headings ─── */\n\n.section-title {\n font-variant: small-caps;\n font-size: 12.5pt;\n font-weight: 400;\n letter-spacing: 1.5px;\n border-bottom: 1.5px solid #C4962C;\n padding-bottom: 1px;\n margin-top: 8px;\n margin-bottom: 5px;\n color: #000;\n page-break-after: avoid;\n}\n\n/* ─── Bullet Items (shared) ─── */\n\n.bullet-item {\n position: relative;\n padding-left: 14px;\n margin-bottom: 2px;\n}\n\n.bullet-item::before {\n content: '\\25CB';\n position: absolute;\n left: 0;\n font-size: 7pt;\n top: 2.5px;\n}\n\n/* ─── Skills ─── */\n\n.skill-name {\n font-weight: 700;\n}\n\n/* ─── Work Experience ─── */\n\n.work-entry {\n margin-bottom: 7px;\n page-break-inside: avoid;\n}\n\n.work-header {\n display: flex;\n justify-content: space-between;\n align-items: baseline;\n gap: 8px;\n}\n\n.work-title {\n font-weight: 700;\n font-size: 10pt;\n}\n\n.work-duration {\n font-size: 9.5pt;\n white-space: nowrap;\n flex-shrink: 0;\n}\n\n.work-meta {\n display: flex;\n justify-content: space-between;\n align-items: baseline;\n gap: 8px;\n}\n\n.work-tech {\n font-style: italic;\n font-size: 9.5pt;\n}\n\n.work-client {\n font-style: italic;\n font-size: 9.5pt;\n white-space: nowrap;\n flex-shrink: 0;\n}\n\n.work-highlights {\n list-style: none;\n padding-left: 0;\n margin-top: 3px;\n}\n\n.work-highlights li {\n position: relative;\n padding-left: 14px;\n margin-bottom: 3px;\n text-align: justify;\n}\n\n.work-highlights li::before {\n content: '\\25CB';\n position: absolute;\n left: 0;\n font-size: 7pt;\n top: 2.5px;\n}\n\n/* ─── Education ─── */\n\n.edu-entry {\n margin-bottom: 5px;\n page-break-inside: avoid;\n}\n\n.edu-header {\n display: flex;\n justify-content: space-between;\n align-items: baseline;\n gap: 8px;\n}\n\n.edu-degree {\n font-weight: 700;\n font-size: 10pt;\n}\n\n.edu-year {\n font-size: 10pt;\n font-weight: 700;\n white-space: nowrap;\n flex-shrink: 0;\n}\n\n.edu-institution {\n font-size: 9.5pt;\n}\n\n.edu-courses {\n font-size: 9.5pt;\n}\n\n/* ─── Projects ─── */\n\n.project-name {\n font-weight: 700;\n}\n\n/* ─── Volunteer ─── */\n\n.volunteer-item {\n text-align: justify;\n margin-bottom: 4px;\n}\n\n.vol-title {\n font-weight: 700;\n}\n\n/* ─── Additional ─── */\n\n.additional-line {\n margin-bottom: 1px;\n}\n\n.additional-label {\n font-weight: 700;\n font-style: italic;\n}\n\n/* ─── Print ─── */\n\n@media print {\n .work-entry { page-break-inside: avoid; }\n .edu-entry { page-break-inside: avoid; }\n .section-title { page-break-after: avoid; }\n}\n";
|
|
337
|
+
function render(resume) {
|
|
338
|
+
var _a;
|
|
339
|
+
const h = { ...DEFAULT_HEADINGS };
|
|
340
|
+
const metaHeadings = (_a = resume == null ? void 0 : resume.meta) == null ? void 0 : _a.headings;
|
|
341
|
+
if (metaHeadings) {
|
|
342
|
+
for (const [key, value] of Object.entries(metaHeadings)) {
|
|
343
|
+
if (value && key in h) {
|
|
344
|
+
h[key] = value;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
const basics = (resume == null ? void 0 : resume.basics) || {};
|
|
349
|
+
return `<!DOCTYPE html>
|
|
350
|
+
<html lang="en">
|
|
351
|
+
<head>
|
|
352
|
+
<meta charset="UTF-8" />
|
|
353
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
354
|
+
<title>${esc(basics.name || "Resume")}</title>
|
|
355
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
356
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
357
|
+
<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" />
|
|
358
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" />
|
|
359
|
+
<style>${css}</style>
|
|
360
|
+
</head>
|
|
361
|
+
<body>
|
|
362
|
+
<div class="resume">
|
|
363
|
+
${renderHeader(basics)}
|
|
364
|
+
${renderSummary(basics.summary)}
|
|
365
|
+
${renderSkills(resume.skills, h.skills)}
|
|
366
|
+
${renderWork(resume.work, h.experience)}
|
|
367
|
+
${renderProjects(resume.projects, h.projects)}
|
|
368
|
+
${renderEducation(resume.education, h.education)}
|
|
369
|
+
${renderVolunteer(resume.volunteer, h.volunteer)}
|
|
370
|
+
${renderCertificates(resume.certificates, h.certifications)}
|
|
371
|
+
${renderAdditional(resume, h.additional)}
|
|
372
|
+
</div>
|
|
373
|
+
</body>
|
|
374
|
+
</html>`;
|
|
375
|
+
}
|
|
376
|
+
const pdfRenderOptions = {
|
|
377
|
+
mediaType: "print",
|
|
378
|
+
format: "A4",
|
|
379
|
+
margin: {
|
|
380
|
+
top: "12mm",
|
|
381
|
+
right: "14mm",
|
|
382
|
+
bottom: "12mm",
|
|
383
|
+
left: "14mm"
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
export {
|
|
387
|
+
pdfRenderOptions,
|
|
388
|
+
render
|
|
389
|
+
};
|
|
390
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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/summary.ts","../src/sections/shared.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 skills: 'Core Skills',\n experience: 'Experience',\n projects: 'Selected Projects',\n education: 'Education',\n volunteer: 'Leadership & Volunteering',\n certifications: 'Credentials',\n additional: 'Additional',\n};\n","/** Decode common HTML entities to literal characters. */\nexport function decodeEntities(text: string): string {\n return text\n .replace(/&#(\\d+);/g, (_, c) => String.fromCharCode(Number(c)))\n .replace(/&#x([0-9a-fA-F]+);/g, (_, h) => String.fromCharCode(parseInt(h, 16)))\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/"/g, '\"')\n .replace(/'/g, \"'\")\n .replace(/'/g, \"'\")\n .replace(/ /g, ' ')\n .replace(/—/g, '\\u2014')\n .replace(/–/g, '\\u2013')\n .replace(/…/g, '\\u2026');\n}\n\n/** HTML-escape for safe output (plain text fields like names, dates). */\nexport function esc(text: string | null | undefined): string {\n if (text == null) return '';\n return String(text)\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n","import { MONTHS, COUNTRY_NAMES } from '../constants.js';\n\nexport function formatDate(dateStr: string | null | undefined): string {\n if (!dateStr) return '';\n const parts = String(dateStr).split('-');\n if (parts.length === 1) return parts[0];\n const year = parts[0];\n const month = MONTHS[parseInt(parts[1], 10) - 1] || '';\n return `${month} ${year}`;\n}\n\nexport function dateRange(start: string | null | undefined, end: string | null | undefined): string {\n const s = formatDate(start);\n const e = end ? formatDate(end) : '';\n if (!s) return '';\n if (!e) return s;\n return `${s} to ${e}`;\n}\n\nexport function regionName(code: string | null | undefined): string {\n if (!code) return '';\n return COUNTRY_NAMES[code.toUpperCase()] || code;\n}\n","export function profileIcon(network: string | null | undefined): string {\n if (!network) return '<i class=\"fa-solid fa-link\"></i>';\n const n = network.toLowerCase();\n if (n === 'linkedin') return '<i class=\"fa-brands fa-linkedin-in\"></i>';\n if (n === 'github') return '<i class=\"fa-brands fa-github\"></i>';\n if (n === 'twitter' || n === 'x') return '<i class=\"fa-brands fa-x-twitter\"></i>';\n if (n === 'stackoverflow') return '<i class=\"fa-brands fa-stack-overflow\"></i>';\n return '<i class=\"fa-solid fa-link\"></i>';\n}\n","/** Tags allowed through sanitization — safe inline/block formatting. */\nconst BLOCK_TAGS = new Set(['p', 'ul', 'ol', 'li', 'blockquote']);\nconst INLINE_TAGS = new Set([\n 'br', 'b', 'strong', 'i', 'em', 'u', 's', 'mark', 'a', 'sub', 'sup', 'code',\n]);\nconst ALLOWED_TAGS = new Set([...BLOCK_TAGS, ...INLINE_TAGS]);\n\n/** URL schemes safe for <a href>. Everything else (javascript:, data:, vbscript:) is stripped. */\nconst SAFE_URL_SCHEMES = /^(?:https?|mailto|tel):/i;\nconst RELATIVE_URL = /^[#/?.]/;\n\nfunction isSafeUrl(href: string): boolean {\n const trimmed = href.trim();\n if (!trimmed) return false;\n return SAFE_URL_SCHEMES.test(trimmed) || RELATIVE_URL.test(trimmed);\n}\n\ninterface SanitizeOptions {\n readonly inline?: boolean;\n}\n\n/**\n * Sanitize HTML — keep allowed formatting tags, strip everything else.\n * Attributes are stripped from all tags except <a href> (safe schemes only) and <mark class>.\n * If inline=true, block tags (p, ul, ol, li, blockquote) are also stripped.\n */\nexport function sanitizeHtml(html: string | null | undefined, { inline = false }: SanitizeOptions = {}): string {\n if (!html) return '';\n const allowed = inline ? INLINE_TAGS : ALLOWED_TAGS;\n\n return String(html)\n .replace(/<a\\s+[^>]*href=\"([^\"]*)\"[^>]*>/gi, (_, href: string) => {\n if (!isSafeUrl(href)) return '';\n return `<a href=\"${href.replace(/\"/g, '"')}\" target=\"_blank\" rel=\"noopener\">`;\n })\n .replace(/<mark\\s+[^>]*class=\"([^\"]*)\"[^>]*>/gi, (_, cls: string) =>\n `<mark class=\"${cls.replace(/\"/g, '"')}\">`,\n )\n .replace(/<(\\/?)([a-z][a-z0-9]*)\\s*[^>]*?(\\/?)>/gi, (match, slash: string, tag: string, selfClose: string) => {\n const lower = tag.toLowerCase();\n if (lower === 'a' && !slash && match.includes('href=')) return match;\n if (lower === 'mark' && !slash && match.includes('class=')) return match;\n if (allowed.has(lower)) return `<${slash}${lower}${selfClose}>`;\n if (inline && BLOCK_TAGS.has(lower)) return slash ? ' ' : '';\n return '';\n })\n .replace(/<p>\\s*<\\/p>/gi, '')\n .replace(/\\s{2,}/g, ' ')\n .trim();\n}\n","import { decodeEntities, esc } from './escape.js';\nimport { sanitizeHtml } from './sanitize.js';\n\n/** Strip HTML tags → plain text (for fields parsed as data, not displayed). */\nexport function stripHtml(text: string | null | undefined): string {\n if (!text) return '';\n return decodeEntities(\n String(text)\n .replace(/<br\\s*\\/?>/gi, ' ')\n .replace(/<\\/?(p|div|li)[^>]*>/gi, ' ')\n .replace(/<[^>]+>/g, ''),\n )\n .replace(/\\s{2,}/g, ' ')\n .trim();\n}\n\nconst isHtml = (text: string): boolean => /<[a-z][\\s\\S]*>/i.test(text);\nconst hasEntities = (text: string): boolean => /&(?:amp|lt|gt|quot|#\\d+|#x[0-9a-f]+);/i.test(text);\n\n/**\n * Render user-provided rich text → safe HTML output.\n * block=true: preserve <p>, <ul>, <ol> etc.\n * block=false (default): inline mode — strip block tags, keep only inline formatting.\n */\nexport function richText(text: string | null | undefined, { block = false }: { block?: boolean } = {}): string {\n if (text == null) return '';\n const s = String(text);\n if (isHtml(s)) return sanitizeHtml(s, { inline: !block });\n if (hasEntities(s)) return esc(decodeEntities(s));\n return esc(s);\n}\n\n/** Check if an array is non-empty. */\nexport function has<T>(arr: readonly T[] | null | undefined): arr is readonly T[] & { length: number } {\n return Array.isArray(arr) && arr.length > 0;\n}\n","import type { ResumeBasics } from '../types/resume.js';\nimport { esc } from '../utils/escape.js';\nimport { regionName } from '../utils/dates.js';\nimport { profileIcon } from '../utils/icons.js';\nimport { has } from '../utils/text.js';\n\ninterface ContactLine {\n readonly icon: string;\n readonly text: string;\n}\n\nexport function renderHeader(basics: ResumeBasics | undefined): string {\n if (!basics) return '';\n\n const location = basics.location\n ? [basics.location.city, regionName(basics.location.region)].filter(Boolean).join(', ')\n : '';\n\n const lines: ContactLine[] = [];\n if (location)\n lines.push({ icon: '<i class=\"fa-solid fa-location-dot\"></i>', text: location });\n if (basics.phone)\n lines.push({ icon: '<i class=\"fa-solid fa-square-phone\"></i>', text: basics.phone });\n if (basics.email)\n lines.push({ icon: '<i class=\"fa-solid fa-envelope\"></i>', text: basics.email });\n if (basics.url) {\n const display = basics.url.replace(/^https?:\\/\\//, '');\n lines.push({ icon: '<i class=\"fa-solid fa-globe\"></i>', text: display });\n }\n if (has(basics.profiles)) {\n for (const p of basics.profiles) {\n lines.push({ icon: profileIcon(p.network), text: p.username || p.url || '' });\n }\n }\n\n return `\n <header class=\"header\">\n <div class=\"header-left\">\n <h1 class=\"name\">${esc(basics.name)}</h1>\n <div class=\"label\">${esc(basics.label)}</div>\n </div>\n <div class=\"contact-info\">\n ${lines.map((l) => `<div class=\"contact-line\">${esc(l.text)} ${l.icon}</div>`).join('\\n ')}\n </div>\n </header>\n <hr class=\"header-rule\" />`;\n}\n","import { richText } from '../utils/text.js';\n\nexport function renderSummary(summary: string | undefined): string {\n if (!summary) return '';\n const html = richText(summary, { block: true });\n if (!html) return '';\n return `<div class=\"summary\">${html}</div>`;\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 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/** Extract tech-stack and client from a work summary string. */\nfunction parseWorkMeta(summary: string | undefined): { techStack: string; client: string } {\n if (!summary) return { techStack: '', client: '' };\n const text = stripHtml(summary).replace(/\\n/g, ' ').trim();\n\n const clientMatch = text.match(/Client:\\s*(.+)$/i);\n const client = clientMatch ? clientMatch[1].trim() : '';\n\n const techPart = clientMatch ? text.slice(0, clientMatch.index).trim() : text;\n const techMatch = techPart.match(/Tech-stack:\\s*(.+)/i);\n const techStack = techMatch ? techMatch[1].trim() : '';\n\n return { techStack, client };\n}\n\nfunction renderWorkEntry(entry: ResumeWorkEntry): string {\n const { techStack, client } = parseWorkMeta(entry.summary);\n const duration = dateRange(entry.startDate, entry.endDate);\n\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 ${\n has(entry.highlights)\n ? `\n <ul class=\"work-highlights\">\n ${entry.highlights.map((h) => `<li>${richText(h)}</li>`).join('\\n ')}\n </ul>`\n : ''\n }\n </div>`;\n}\n\nexport function renderWork(work: readonly ResumeWorkEntry[] | undefined, heading: string): string {\n if (!has(work)) return '';\n return `\n ${sectionTitle(heading)}\n <div class=\"section-body\">\n ${work.map(renderWorkEntry).join('')}\n </div>`;\n}\n","import type { ResumeProject } from '../types/resume.js';\nimport { esc } from '../utils/escape.js';\nimport { has, richText } from '../utils/text.js';\nimport { sectionTitle } from './shared.js';\n\nexport function renderProjects(\n projects: readonly ResumeProject[] | undefined,\n heading: string,\n): string {\n if (!has(projects)) return '';\n return `\n ${sectionTitle(heading)}\n <div class=\"section-body\">\n ${projects\n .map((p) => {\n const desc = richText(p.description);\n return `\n <div class=\"bullet-item\">\n <span class=\"project-name\">${esc(p.name)}:</span> ${desc}\n </div>`;\n })\n .join('')}\n </div>`;\n}\n","import type { ResumeEducationEntry } from '../types/resume.js';\nimport { esc } from '../utils/escape.js';\nimport { formatDate } from '../utils/dates.js';\nimport { has, richText } from '../utils/text.js';\nimport { sectionTitle } from './shared.js';\n\nfunction renderEducationEntry(entry: ResumeEducationEntry): string {\n const degree = [entry.studyType, entry.area].filter(Boolean).join(' ');\n\n let yearDisplay = '';\n if (entry.endDate) {\n yearDisplay = formatDate(entry.endDate);\n } else if (entry.startDate) {\n const year = parseInt(String(entry.startDate).split('-')[0], 10);\n const currentYear = new Date().getFullYear();\n yearDisplay = year >= currentYear ? `Expected ${formatDate(entry.startDate)}` : formatDate(entry.startDate);\n }\n\n const instParts = [entry.institution];\n if (entry.score) instParts.push(entry.score);\n const instLine = instParts.filter(Boolean).join(' | ');\n\n return `\n <div class=\"edu-entry\">\n <div class=\"edu-header\">\n <div class=\"edu-degree\">${esc(degree)}</div>\n <div class=\"edu-year\">${esc(yearDisplay)}</div>\n </div>\n ${entry.institution ? `<div class=\"edu-institution\">${esc(instLine)}</div>` : ''}\n ${\n has(entry.courses)\n ? `\n <div class=\"edu-courses\">\n ${entry.courses.map((c) => `<div>${richText(c)}</div>`).join('')}\n </div>`\n : ''\n }\n </div>`;\n}\n\nexport function renderEducation(\n education: readonly ResumeEducationEntry[] | undefined,\n heading: string,\n): string {\n if (!has(education)) return '';\n return `\n ${sectionTitle(heading)}\n <div class=\"section-body\">\n ${education.map(renderEducationEntry).join('')}\n </div>`;\n}\n","import type { ResumeVolunteerEntry } from '../types/resume.js';\nimport { esc } from '../utils/escape.js';\nimport { has, richText } from '../utils/text.js';\nimport { sectionTitle } from './shared.js';\n\nexport function renderVolunteer(\n volunteer: readonly ResumeVolunteerEntry[] | undefined,\n heading: string,\n): string {\n if (!has(volunteer)) return '';\n return `\n ${sectionTitle(heading)}\n <div class=\"section-body\">\n ${volunteer\n .map((v) => {\n const years = v.endDate\n ? `${v.startDate}\\u2013${v.endDate}`\n : v.startDate || '';\n const title = v.position\n ? `${v.position} \\u2013 ${v.organization}`\n : v.organization;\n const summary = richText(v.summary);\n\n return `\n <div class=\"bullet-item volunteer-item\">\n <span class=\"vol-title\">${esc(title)}${years ? ` (${esc(years)})` : ''}:</span> ${summary}\n </div>`;\n })\n .join('')}\n </div>`;\n}\n","import type { ResumeCertificate } from '../types/resume.js';\nimport { has, richText } from '../utils/text.js';\nimport { sectionTitle } from './shared.js';\n\nexport function renderCertificates(\n certificates: readonly ResumeCertificate[] | undefined,\n heading: string,\n): string {\n if (!has(certificates)) return '';\n return `\n ${sectionTitle(heading)}\n <div class=\"section-body\">\n ${certificates\n .map(\n (c) => `\n <div class=\"bullet-item\">${richText(c.name)}</div>`,\n )\n .join('')}\n </div>`;\n}\n","import type { ResumeSchema } from '../types/resume.js';\nimport { esc } from '../utils/escape.js';\nimport { has } from '../utils/text.js';\nimport { sectionTitle } from './shared.js';\n\nexport function renderAdditional(resume: ResumeSchema, heading: string): string {\n const parts: string[] = [];\n\n if (has(resume.interests)) {\n for (const interest of resume.interests) {\n const keywords = has(interest.keywords) ? interest.keywords.join(', ') : '';\n const label = interest.name || 'Interests';\n if (keywords) {\n parts.push(\n `<div class=\"additional-line\"><span class=\"additional-label\">${esc(label)}:</span> ${esc(keywords)}</div>`,\n );\n }\n }\n }\n\n if (has(resume.languages)) {\n const langStr = resume.languages\n .map((l) => `${l.language}${l.fluency ? ` (${l.fluency})` : ''}`)\n .join(', ');\n parts.push(\n `<div class=\"additional-line\"><span class=\"additional-label\">Languages:</span> ${esc(langStr)}</div>`,\n );\n }\n\n if (has(resume.references)) {\n parts.push(\n `<div class=\"additional-line\"><span class=\"additional-label\">Referees:</span> Available on request</div>`,\n );\n }\n\n if (parts.length === 0) return '';\n\n return `\n ${sectionTitle(heading)}\n <div class=\"section-body\">\n ${parts.join('\\n ')}\n </div>`;\n}\n","import type { ResumeSchema } from './types/resume.js';\nimport { DEFAULT_HEADINGS } from './constants.js';\nimport { esc } from './utils/escape.js';\nimport { renderHeader } from './sections/header.js';\nimport { renderSummary } from './sections/summary.js';\nimport { renderSkills } from './sections/skills.js';\nimport { renderWork } from './sections/work.js';\nimport { renderProjects } from './sections/projects.js';\nimport { renderEducation } from './sections/education.js';\nimport { renderVolunteer } from './sections/volunteer.js';\nimport { renderCertificates } from './sections/certificates.js';\nimport { renderAdditional } from './sections/additional.js';\nimport css from './styles/academic.css?inline';\n\nexport function render(resume: ResumeSchema): string {\n const h = { ...DEFAULT_HEADINGS };\n\n const metaHeadings = resume?.meta?.headings;\n if (metaHeadings) {\n for (const [key, value] of Object.entries(metaHeadings)) {\n if (value && key in h) {\n h[key] = value;\n }\n }\n }\n\n const basics = resume?.basics || {};\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n <title>${esc(basics.name || 'Resume')}</title>\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\" />\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin />\n <link href=\"https://fonts.googleapis.com/css2?family=EB+Garamond:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500&display=swap\" rel=\"stylesheet\" />\n <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css\" />\n <style>${css}</style>\n</head>\n<body>\n <div class=\"resume\">\n ${renderHeader(basics)}\n ${renderSummary(basics.summary)}\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,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,UAAU;AAAA,EACV,WAAW;AAAA,EACX,WAAW;AAAA,EACX,gBAAgB;AAAA,EAChB,YAAY;AACd;AC5CO,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,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,MAAM,WAAW,GAAG,IAAI;AAClC,MAAI,CAAC,EAAG,QAAO;AACf,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;ACtBO,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,cAAc,SAAqC;AACjE,MAAI,CAAC,QAAS,QAAO;AACrB,QAAM,OAAO,SAAS,SAAS,EAAE,OAAO,MAAM;AAC9C,MAAI,CAAC,KAAM,QAAO;AAClB,SAAO,wBAAwB,IAAI;AACrC;ACLO,SAAS,aAAa,OAAuB;AAClD,SAAO,6BAA6B,IAAI,KAAK,CAAC;AAChD;ACCO,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,cAAc,SAAoE;AACzF,MAAI,CAAC,QAAS,QAAO,EAAE,WAAW,IAAI,QAAQ,GAAA;AAC9C,QAAM,OAAO,UAAU,OAAO,EAAE,QAAQ,OAAO,GAAG,EAAE,KAAA;AAEpD,QAAM,cAAc,KAAK,MAAM,kBAAkB;AACjD,QAAM,SAAS,cAAc,YAAY,CAAC,EAAE,SAAS;AAErD,QAAM,WAAW,cAAc,KAAK,MAAM,GAAG,YAAY,KAAK,EAAE,KAAA,IAAS;AACzE,QAAM,YAAY,SAAS,MAAM,qBAAqB;AACtD,QAAM,YAAY,YAAY,UAAU,CAAC,EAAE,SAAS;AAEpD,SAAO,EAAE,WAAW,OAAA;AACtB;AAEA,SAAS,gBAAgB,OAAgC;AACvD,QAAM,EAAE,WAAW,OAAA,IAAW,cAAc,MAAM,OAAO;AACzD,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,QAEE,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;ACrDO,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;AAAA;AAAA,UAEF,MAAM,QAAQ,IAAI,CAAC,MAAM,QAAQ,SAAS,CAAC,CAAC,QAAQ,EAAE,KAAK,EAAE,CAAC;AAAA,gBAE5D,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;AC7CO,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,OAAO,CAAC;AAAA,MAC7B,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;"}
|