1// ============================================================================ 2// RESUME PREVIEW & OVERLAY 3// ============================================================================ 4 5export function buildResumePreview(resume) { 6 const card = document.createElement("div"); 7 card.className = "resume-preview"; 8 9 const header = document.createElement("div"); 10 header.className = "resume-preview-header"; 11 12 const icon = document.createElement("div"); 13 icon.className = "resume-preview-icon"; 14 icon.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>`; 15 16 const titleGroup = document.createElement("div"); 17 titleGroup.className = "resume-preview-title-group"; 18 19 const title = document.createElement("div"); 20 title.className = "resume-preview-title"; 21 title.textContent = "Resume"; 22 23 const subtitle = document.createElement("div"); 24 subtitle.className = "resume-preview-subtitle"; 25 const name = resume.basics?.name || ""; 26 const label = resume.basics?.label || ""; 27 subtitle.textContent = [name, label].filter(Boolean).join(" â ") || "JSON Resume"; 28 29 titleGroup.append(title, subtitle); 30 header.append(icon, titleGroup); 31 32 const stats = document.createElement("div"); 33 stats.className = "resume-preview-stats"; 34 35 const sections = [ 36 resume.work?.length && `${resume.work.length} role${resume.work.length > 1 ? "s" : ""}`, 37 resume.education?.length && `${resume.education.length} degree${resume.education.length > 1 ? "s" : ""}`, 38 resume.skills?.length && `${resume.skills.length} skill group${resume.skills.length > 1 ? "s" : ""}`, 39 ].filter(Boolean); 40 41 if (sections.length > 0) { 42 const statText = document.createElement("span"); 43 statText.className = "resume-preview-stat-text"; 44 statText.textContent = sections.join(" · "); 45 stats.appendChild(statText); 46 } 47 48 const viewBtn = document.createElement("button"); 49 viewBtn.className = "resume-preview-btn"; 50 viewBtn.textContent = "View Resume"; 51 viewBtn.addEventListener("click", () => { 52 buildResumeOverlay(resume); 53 }); 54 55 card.append(header, stats, viewBtn); 56 return card; 57} 58 59async function buildResumeOverlay(resume) { 60 document.body.style.overflow = "hidden"; // Lock background page scroll 61 62 const overlay = document.createElement("div"); 63 overlay.className = "resume-overlay"; 64 65 const backdrop = document.createElement("div"); 66 backdrop.className = "resume-overlay-backdrop"; 67 68 const container = document.createElement("div"); 69 container.className = "resume-overlay-container"; 70 71 const toolbar = document.createElement("div"); 72 toolbar.className = "resume-overlay-toolbar"; 73 74 const toolbarTitle = document.createElement("div"); 75 toolbarTitle.className = "resume-overlay-toolbar-title"; 76 toolbarTitle.textContent = "Resume"; 77 78 const toolbarActions = document.createElement("div"); 79 toolbarActions.className = "resume-overlay-toolbar-actions"; 80 81 const printBtn = document.createElement("button"); 82 printBtn.className = "resume-overlay-print"; 83 printBtn.textContent = "Print"; 84 85 const closeBtn = document.createElement("button"); 86 closeBtn.className = "resume-overlay-close"; 87 closeBtn.setAttribute("aria-label", "Close resume"); 88 closeBtn.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>`; 89 90 toolbarActions.append(printBtn, closeBtn); 91 toolbar.append(toolbarTitle, toolbarActions); 92 93 const status = document.createElement("div"); 94 status.className = "resume-overlay-status"; 95 const requestedTheme = resume.meta?.theme || "default"; 96 status.innerHTML = `<span class="spinner-inline"></span>Loading theme: ${requestedTheme}...`; 97 98 container.append(toolbar, status); 99 100 // Isolated iframe to prevent CSS bleed 101 const frame = document.createElement("iframe"); 102 frame.className = "resume-document-frame"; 103 frame.style.cssText = "width:100%; height:100%; border:none; background:#ffffff;"; 104 container.appendChild(frame); 105 106 overlay.append(backdrop, container); 107 108 function close() { 109 document.body.style.overflow = ""; 110 overlay.classList.add("closing"); 111 setTimeout(() => overlay.remove(), 250); 112 } 113 114 backdrop.addEventListener("click", close); 115 closeBtn.addEventListener("click", close); 116 117 document.addEventListener("keydown", function onEsc(e) { 118 if (e.key === "Escape") { 119 close(); 120 document.removeEventListener("keydown", onEsc); 121 } 122 }); 123 124 // Append overlay immediately so user sees it right away 125 document.body.appendChild(overlay); 126 requestAnimationFrame(() => overlay.classList.add("open")); 127 128 // Async: fetch/render theme content into the iframe 129 try { 130 const renderedContent = await executeThemeRender(requestedTheme, resume); 131 status.remove(); 132 133 const frameDoc = frame.contentWindow.document; 134 frameDoc.open(); 135 frameDoc.write(renderedContent); 136 frameDoc.close(); 137 138 printBtn.addEventListener("click", () => { 139 frame.contentWindow.focus(); 140 frame.contentWindow.print(); 141 }); 142 } catch (e) { 143 console.warn("[resume] Failed to render theme:", e); 144 status.textContent = `Failed to load theme "${requestedTheme}"`; 145 status.className = "resume-overlay-status error"; 146 } 147} 148 149/** 150 * Attempts to render theme via official registry service, falling back to local HTML/CSS builders 151 */ 152async function executeThemeRender(themeName, resume) { 153 const cleanTheme = themeName 154 .replace(/^jsonresume-theme-/, "") 155 .replace(/^@jsonresume\/jsonresume-theme-/, "") 156 .trim(); 157 158 // 1. Primary Attempt: Query JSON Resume official registry API 159 if (cleanTheme && cleanTheme !== "default") { 160 try { 161 const response = await fetch(`https://registry.jsonresume.org/theme/${cleanTheme}`, { 162 method: "POST", 163 headers: { "Content-Type": "application/json" }, 164 body: JSON.stringify(resume), 165 }); 166 167 if (response.ok) { 168 const html = await response.text(); 169 if (html && html.trim().length > 0) { 170 return html; 171 } 172 } 173 } catch (e) { 174 console.warn(`[Resume] Could not render remote theme "${cleanTheme}". Falling back to local default.`, e); 175 } 176 } 177 178 // 2. Fallback Attempt: Native Local Builder + resume-theme.css 179 return await renderLocalFallbackTheme(resume); 180} 181 182/** 183 * Generates standalone HTML document using local theme styles 184 */ 185async function renderLocalFallbackTheme(resume) { 186 const doc = document.createElement("div"); 187 doc.className = "resume-document"; 188 189 renderResumeBasics(doc, resume.basics); 190 renderResumeWork(doc, resume.work); 191 renderResumeEducation(doc, resume.education); 192 renderResumeSkills(doc, resume.skills); 193 renderResumeProjects(doc, resume.projects); 194 renderResumeAwards(doc, resume.awards); 195 renderResumeCertificates(doc, resume.certificates); 196 renderResumePublications(doc, resume.publications); 197 renderResumeVolunteer(doc, resume.volunteer); 198 renderResumeLanguages(doc, resume.languages); 199 renderResumeInterests(doc, resume.interests); 200 renderResumeReferences(doc, resume.references); 201 202 const cssUrl = new URL("../css/resume-theme.css", import.meta.url).href; 203 return ` 204 <!DOCTYPE html> 205 <html lang="en"> 206 <head> 207 <meta charset="utf-8"> 208 <title>${resume.basics?.name || "Resume"}</title> 209 <link rel="stylesheet" href="${cssUrl}"> 210 </head> 211 <body> 212 ${doc.outerHTML} 213 </body> 214 </html> 215 `; 216} 217 218// ============================================================================
219// LOCAL DOM BUILDER FUNCTIONS 220// ============================================================================ 221 222function renderResumeBasics(doc, basics) { 223 if (!basics) return; 224 const section = document.createElement("div"); 225 section.className = "resume-section resume-basics"; 226 227 if (basics.name || basics.label) { 228 const h1 = document.createElement("h1"); 229 h1.className = "resume-name"; 230 h1.textContent = basics.name || ""; 231 section.appendChild(h1); 232 } 233 234 if (basics.label) { 235 const label = document.createElement("div"); 236 label.className = "resume-label"; 237 label.textContent = basics.label; 238 section.appendChild(label); 239 } 240 241 const contactItems = []; 242 if (basics.email) contactItems.push({ label: "Email", value: basics.email, href: `mailto:${basics.email}` }); 243 if (basics.phone) contactItems.push({ label: "Phone", value: basics.phone, href: `tel:${basics.phone}` }); 244 if (basics.url) contactItems.push({ label: "Website", value: basics.url, href: basics.url }); 245 if (basics.location) { 246 const loc = [basics.location.city, basics.location.region, basics.location.countryCode].filter(Boolean).join(", "); 247 if (loc) contactItems.push({ label: "Location", value: loc }); 248 } 249 250 if (basics.summary) { 251 const summary = document.createElement("p"); 252 summary.className = "resume-summary"; 253 summary.textContent = basics.summary; 254 section.appendChild(summary); 255 } 256 257 if (contactItems.length > 0) { 258 const contactRow = document.createElement("div"); 259 contactRow.className = "resume-contact-row"; 260 contactItems.forEach((item) => { 261 const span = document.createElement("span"); 262 span.className = "resume-contact-item"; 263 if (item.href) { 264 const a = document.createElement("a"); 265 a.href = item.href; 266 a.textContent = item.value; 267 if (item.href.startsWith("http")) a.target = "_blank"; 268 span.appendChild(a); 269 } else { 270 span.textContent = item.value; 271 } 272 contactRow.appendChild(span); 273 }); 274 section.appendChild(contactRow); 275 } 276 277 if (basics.profiles?.length > 0) { 278 const profiles = document.createElement("div"); 279 profiles.className = "resume-contact-row"; 280 basics.profiles.forEach((p) => { 281 if (!p.url) return; 282 const span = document.createElement("span"); 283 span.className = "resume-contact-item"; 284 const a = document.createElement("a"); 285 a.href = p.url; 286 a.target = "_blank"; 287 a.textContent = p.network || p.url; 288 span.appendChild(a); 289 profiles.appendChild(span); 290 }); 291 if (profiles.children.length > 0) section.appendChild(profiles); 292 } 293 294 doc.appendChild(section); 295} 296 297function renderResumeSection(doc, title, items, renderer) { 298 if (!items || items.length === 0) return; 299 const section = document.createElement("div"); 300 section.className = "resume-section"; 301 302 const h2 = document.createElement("h2"); 303 h2.className = "resume-section-title"; 304 h2.textContent = title; 305 section.appendChild(h2); 306 307 items.forEach((item) => section.appendChild(renderer(item))); 308 309 doc.appendChild(section); 310} 311 312function renderResumeWork(doc, work) { 313 renderResumeSection(doc, "Work Experience", work, (w) => { 314 const entry = document.createElement("div"); 315 entry.className = "resume-entry"; 316 317 const header = document.createElement("div"); 318 header.className = "resume-entry-header"; 319 320 const left = document.createElement("div"); 321 const position = document.createElement("div"); 322 position.className = "resume-entry-title"; 323 position.textContent = w.position || ""; 324 const company = document.createElement("div"); 325 company.className = "resume-entry-subtitle"; 326 company.textContent = w.company || w.name || ""; 327 left.append(position, company); 328 329 const right = document.createElement("div"); 330 right.className = "resume-entry-dates"; 331 const start = w.startDate || ""; 332 const end = w.endDate || "Present"; 333 if (start) right.textContent = `${start} â ${end}`; 334 335 header.append(left, right); 336 337 if (w.summary) { 338 const summary = document.createElement("p"); 339 summary.className = "resume-entry-text"; 340 summary.textContent = w.summary; 341 entry.append(header, summary); 342 } else { 343 entry.appendChild(header); 344 } 345 346 if (w.highlights?.length > 0) { 347 const ul = document.createElement("ul"); 348 ul.className = "resume-entry-list";
349 w.highlights.forEach((h) => { 350 const li = document.createElement("li"); 351 li.textContent = h; 352 ul.appendChild(li); 353 }); 354 entry.appendChild(ul); 355 } 356 357 return entry; 358 }); 359} 360 361function renderResumeEducation(doc, education) { 362 renderResumeSection(doc, "Education", education, (e) => { 363 const entry = document.createElement("div"); 364 entry.className = "resume-entry"; 365 366 const header = document.createElement("div"); 367 header.className = "resume-entry-header"; 368 369 const left = document.createElement("div"); 370 const degree = document.createElement("div"); 371 degree.className = "resume-entry-title"; 372 degree.textContent = [e.studyType, e.area].filter(Boolean).join(" in ") || ""; 373 const institution = document.createElement("div"); 374 institution.className = "resume-entry-subtitle"; 375 institution.textContent = e.institution || ""; 376 left.append(degree, institution); 377 378 const right = document.createElement("div"); 379 right.className = "resume-entry-dates"; 380 const start = e.startDate || ""; 381 const end = e.endDate || ""; 382 if (start) right.textContent = end ? `${start} â ${end}` : start; 383 384 header.append(left, right); 385 entry.appendChild(header); 386 387 if (e.score) { 388 const score = document.createElement("div"); 389 score.className = "resume-entry-text"; 390 score.textContent = `GPA: ${e.score}`; 391 entry.appendChild(score); 392 } 393 394 if (e.courses?.length > 0) { 395 const courses = document.createElement("div"); 396 courses.className = "resume-entry-text"; 397 courses.textContent = `Courses: ${e.courses.join(", ")}`; 398 entry.appendChild(courses); 399 } 400 401 return entry; 402 }); 403} 404 405function renderResumeSkills(doc, skills) { 406 renderResumeSection(doc, "Skills", skills, (s) => { 407 const entry = document.createElement("div"); 408 entry.className = "resume-skill-group"; 409 410 const name = document.createElement("span"); 411 name.className = "resume-skill-name"; 412 name.textContent = s.name || ""; 413 414 if (s.level) { 415 const level = document.createElement("span"); 416 level.className = "resume-skill-level"; 417 level.textContent = s.level; 418 name.appendChild(level); 419 } 420 421 entry.appendChild(name); 422 423 if (s.keywords?.length > 0) { 424 const keywords = document.createElement("div"); 425 keywords.className = "resume-skill-keywords"; 426 s.keywords.forEach((k) => { 427 const tag = document.createElement("span"); 428 tag.className = "resume-skill-tag"; 429 tag.textContent = k; 430 keywords.appendChild(tag); 431 }); 432 entry.appendChild(keywords); 433 } 434 435 return entry; 436 }); 437} 438 439function renderResumeProjects(doc, projects) { 440 renderResumeSection(doc, "Projects", projects, (p) => { 441 const entry = document.createElement("div"); 442 entry.className = "resume-entry"; 443 444 const header = document.createElement("div"); 445 header.className = "resume-entry-header"; 446 447 const left = document.createElement("div"); 448 const name = document.createElement("div"); 449 name.className = "resume-entry-title"; 450 name.textContent = p.name || ""; 451 left.appendChild(name); 452 453 const right = document.createElement("div"); 454 right.className = "resume-entry-dates"; 455 const start = p.startDate || ""; 456 const end = p.endDate || ""; 457 if (start) right.textContent = end ? `${start} â ${end}` : start; 458 459 header.append(left, right); 460 entry.appendChild(header); 461 462 if (p.description) { 463 const desc = document.createElement("p"); 464 desc.className = "resume-entry-text"; 465 desc.textContent = p.description; 466 entry.appendChild(desc); 467 } 468 469 if (p.highlights?.length > 0) { 470 const ul = document.createElement("ul"); 471 ul.className = "resume-entry-list"; 472 p.highlights.forEach((h) => { 473 const li = document.createElement("li"); 474 li.textContent = h; 475 ul.appendChild(li); 476 }); 477 entry.appendChild(ul); 478 } 479 480 if (p.keywords?.length > 0) { 481 const tags = document.createElement("div"); 482 tags.className = "resume-skill-keywords"; 483 p.keywords.forEach((k) => { 484 const tag = document.createElement("span"); 485 tag.className = "resume-skill-tag"; 486 tag.textContent = k; 487 tags.appendChild(tag); 488 }); 489 entry.appendChild(tags); 490 } 491 492 return entry; 493 }); 494} 495 496function renderResumeAwards(doc, awards) { 497 renderResumeSection(doc, "Awards", awards, (a) => { 498 const entry = document.createElement("div"); 499 entry.className = "resume-entry"; 500 501 const title = document.createElement("div"); 502 title.className = "resume-entry-title"; 503 title.textContent = a.title || ""; 504 entry.appendChild(title); 505 506 const details = []; 507 if (a.date) details.push(a.date); 508 if (a.awarder) details.push(a.awarder); 509 if (details.length > 0) { 510 const sub = document.createElement("div"); 511 sub.className = "resume-entry-subtitle"; 512 sub.textContent = details.join(" · "); 513 entry.appendChild(sub); 514 } 515 516 if (a.summary) { 517 const p = document.createElement("p"); 518 p.className = "resume-entry-text"; 519 p.textContent = a.summary; 520 entry.appendChild(p); 521 } 522 523 return entry; 524 }); 525} 526 527function renderResumeCertificates(doc, certificates) { 528 renderResumeSection(doc, "Certificates", certificates, (c) => { 529 const entry = document.createElement("div"); 530 entry.className = "resume-entry"; 531 532 const title = document.createElement("div"); 533 title.className = "resume-entry-title"; 534 title.textContent = c.name || c.title || ""; 535 entry.appendChild(title); 536 537 const details = []; 538 if (c.date) details.push(c.date); 539 if (c.issuer) details.push(c.issuer); 540 if (details.length > 0) { 541 const sub = document.createElement("div"); 542 sub.className = "resume-entry-subtitle"; 543 sub.textContent = details.join(" · "); 544 entry.appendChild(sub); 545 } 546 547 if (c.url) { 548 const link = document.createElement("a"); 549 link.href = c.url; 550 link.target = "_blank"; 551 link.className = "resume-entry-link"; 552 link.textContent = "View Certificate"; 553 entry.appendChild(link); 554 } 555 556 return entry; 557 }); 558} 559 560function renderResumePublications(doc, publications) { 561 renderResumeSection(doc, "Publications", publications, (p) => { 562 const entry = document.createElement("div"); 563 entry.className = "resume-entry"; 564 565 const title = document.createElement("div"); 566 title.className = "resume-entry-title"; 567 title.textContent = p.name || ""; 568 entry.appendChild(title); 569 570 const details = []; 571 if (p.publisher) details.push(p.publisher); 572 if (p.releaseDate) details.push(p.releaseDate); 573 if (details.length > 0) { 574 const sub = document.createElement("div"); 575 sub.className = "resume-entry-subtitle"; 576 sub.textContent = details.join(" · "); 577 entry.appendChild(sub); 578 } 579 580 if (p.summary) { 581 const pEl = document.createElement("p"); 582 pEl.className = "resume-entry-text"; 583 pEl.textContent = p.summary; 584 entry.appendChild(pEl); 585 } 586 587 if (p.url) { 588 const a = document.createElement("a"); 589 a.className = "resume-entry-link"; 590 a.href = p.url; 591 a.target = "_blank"; 592 a.textContent = p.url; 593 entry.appendChild(a); 594 } 595 596 return entry; 597 }); 598} 599 600function renderResumeVolunteer(doc, volunteer) { 601 renderResumeSection(doc, "Volunteer", volunteer, (v) => { 602 const entry = document.createElement("div"); 603 entry.className = "resume-entry"; 604 605 const header = document.createElement("div"); 606 header.className = "resume-entry-header"; 607 608 const left = document.createElement("div"); 609 const position = document.createElement("div"); 610 position.className = "resume-entry-title"; 611 position.textContent = v.position || ""; 612 const org = document.createElement("div"); 613 org.className = "resume-entry-subtitle"; 614 org.textContent = v.organization || ""; 615 left.append(position, org); 616 617 const right = document.createElement("div"); 618 right.className = "resume-entry-dates"; 619 const start = v.startDate || ""; 620 const end = v.endDate || ""; 621 if (start) right.textContent = end ? `${start} â ${end}` : start; 622 623 header.append(left, right); 624 entry.appendChild(header); 625 626 if (v.summary) { 627 const p = document.createElement("p"); 628 p.className = "resume-entry-text"; 629 p.textContent = v.summary; 630 entry.appendChild(p); 631 } 632 633 if (v.highlights?.length > 0) { 634 const ul = document.createElement("ul"); 635 ul.className = "resume-entry-list";
636 v.highlights.forEach((h) => { 637 const li = document.createElement("li"); 638 li.textContent = h; 639 ul.appendChild(li); 640 }); 641 entry.appendChild(ul); 642 } 643 644 return entry; 645 }); 646} 647 648function renderResumeLanguages(doc, languages) { 649 renderResumeSection(doc, "Languages", languages, (l) => { 650 const entry = document.createElement("div"); 651 entry.className = "resume-entry resume-entry-inline"; 652 653 const name = document.createElement("span"); 654 name.className = "resume-entry-title"; 655 name.textContent = l.language || ""; 656 entry.appendChild(name); 657 658 if (l.fluency) { 659 const fluency = document.createElement("span"); 660 fluency.className = "resume-entry-subtitle"; 661 fluency.textContent = l.fluency; 662 entry.appendChild(fluency); 663 } 664 665 return entry; 666 }); 667} 668 669function renderResumeInterests(doc, interests) { 670 renderResumeSection(doc, "Interests", interests, (i) => { 671 const entry = document.createElement("div"); 672 entry.className = "resume-entry"; 673 674 const name = document.createElement("div"); 675 name.className = "resume-entry-title"; 676 name.textContent = i.name || ""; 677 entry.appendChild(name); 678 679 if (i.keywords?.length > 0) { 680 const keywords = document.createElement("div"); 681 keywords.className = "resume-skill-keywords"; 682 i.keywords.forEach((k) => { 683 const tag = document.createElement("span"); 684 tag.className = "resume-skill-tag"; 685 tag.textContent = k; 686 keywords.appendChild(tag); 687 }); 688 entry.appendChild(keywords); 689 } 690 691 return entry; 692 }); 693} 694 695function renderResumeReferences(doc, references) { 696 renderResumeSection(doc, "References", references, (r) => { 697 const entry = document.createElement("div"); 698 entry.className = "resume-entry"; 699 700 if (r.name) { 701 const name = document.createElement("div"); 702 name.className = "resume-entry-title"; 703 name.textContent = r.name; 704 entry.appendChild(name); 705 } 706 707 if (r.reference) { 708 const ref = document.createElement("blockquote"); 709 ref.className = "resume-entry-text"; 710 ref.textContent = r.reference; 711 entry.appendChild(ref); 712 } 713 714 return entry; 715 }); 716}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.