;;; generate.scm - Walk resume data and produce HTML via SXML ;;; Run with: guile generate.scm > resume.html (use-modules (sxml simple)) (load "resume.scm") ;; ─── Utility ────────────────────────────────────────────────────────────────── (define (bullets items) `(ul ,@(map (lambda (s) `(li ,s)) items))) (define (date-range start end) `(span (@ (class "date-range")) ,start " – " ,end)) (define (tech-list techs) `(ul (@ (class "tech-list")) ,@(map (lambda (t) `(li (@ (class "tech-tag")) ,t)) techs))) ;; ─── Individual cards (all become flat children of .columns) ────────────────── ;; Project card — detail bullets revealed on hover (desktop) or click (mobile) (define (render-project proj) `(div (@ (class "project-card") (onclick "this.classList.toggle('open')")) (div (@ (class "project-header")) (span (@ (class "project-name")) ,(field proj 'name)) ,(date-range (field proj 'date-start) (field proj 'date-end))) (div (@ (class "project-summary")) ,(bullets (field proj 'summary-bullets))) (div (@ (class "project-details")) ,(bullets (field proj 'detail-bullets))) ,(tech-list (field proj 'technologies)))) ;; Title card: blurb only, no projects nested inside. ;; Projects are emitted as siblings after the title card. (define (render-title-card tc) `(div (@ (class "title-card")) (div (@ (class "title-header")) (span (@ (class "title-name")) ,(field tc 'title)) ,(date-range (field tc 'date-start) (field tc 'date-end))) (div (@ (class "title-meta")) ,(field tc 'company) " • " ,(field tc 'location)) (div (@ (class "title-blurb")) ,(bullets (field tc 'blurb))))) ;; Emit a title card followed by its project cards as a flat list of siblings (define (render-title-flat tc) (append (list (render-title-card tc)) (map render-project (field tc 'projects)))) (define (render-education edu) `(div (@ (class "education-card") (onclick "this.classList.toggle('open')")) (div (@ (class "edu-header")) (span (@ (class "edu-degree")) ,(field edu 'degree)) ,(date-range (field edu 'date-start) (field edu 'date-end))) (div (@ (class "edu-meta")) ,(field edu 'institution) " • " ,(field edu 'location)) (div (@ (class "edu-bullets")) ,(bullets (field edu 'bullets))) (div (@ (class "edu-extras")) ,(bullets (field edu 'extracurriculars))))) (define (render-interest item) `(div (@ (class "interest-card")) (span (@ (class "interest-title")) ,(field item 'title)) (span (@ (class "interest-description")) ,(field item 'description)))) (define (render-interests sec) `(div (@ (class "interests")) ,@(map render-interest (field sec 'items)))) (define (render-favorite-software sec) `(div (@ (class "favorite-software")) ,@(map (lambda (s) `(span (@ (class "sw-item")) ,s)) (field sec 'items)))) ;; ─── Section → flat list of column children ─────────────────────────────────── (define (section->items sec) (case (car sec) ((job) (apply append (map render-title-flat (field sec 'titles)))) ((education) (list (render-education sec))) ((hobby-projects) (map render-project (field sec 'projects))) ((interests) (list (render-interests sec))) ((favorite-software) (list (render-favorite-software sec))) (else '()))) (define (section-label sec) (case (car sec) ((job) "Work Experience") ((education) "Education") ((hobby-projects) "Projects") ((interests) "Interests") ((favorite-software) "Favorite Software") (else ""))) ;; Group consecutive sections of same type, emit one heading per group (define (sections->flat-items sections) (if (null? sections) '() (let loop ((rest (cdr sections)) (cur-type (caar sections)) (cur-label (section-label (car sections))) (cur-items (section->items (car sections))) (acc '())) (cond ((null? rest) ;; flush the last group — acc is already in forward order (append acc (list `(h2 (@ (class "sec-heading")) ,cur-label)) cur-items)) ((eq? (caar rest) cur-type) ;; same section type: merge items under one heading (loop (cdr rest) cur-type cur-label (append cur-items (section->items (car rest))) acc)) (else ;; new section type: flush current group into acc, start new (loop (cdr rest) (caar rest) (section-label (car rest)) (section->items (car rest)) (append acc (list `(h2 (@ (class "sec-heading")) ,cur-label)) cur-items))))))) ;; ─── Front matter ───────────────────────────────────────────────────────────── (define (render-front-matter fm) `(header (@ (class "front-matter")) (h1 (@ (class "fm-name")) ,(field fm 'name)) (div (@ (class "fm-contact")) (span ,(field fm 'location)) (span ,(field fm 'email))))) ;; ─── Document ───────────────────────────────────────────────────────────────── (define (render-resume res) (let* ((fm (field res 'front-matter)) (items (sections->flat-items (field res 'sections)))) `(*TOP* (html (@ (lang "en")) (head (meta (@ (charset "UTF-8"))) (meta (@ (name "viewport") (content "width=device-width, initial-scale=1"))) (title ,(field fm 'name)) (link (@ (rel "stylesheet") (href "style.css"))) ;; Measure the front-matter height after every resize/zoom and ;; set .columns height to exactly fill the remaining space. (script " function setColHeight() { var fm = document.querySelector('.front-matter'); var col = document.querySelector('.columns'); if (!fm || !col) return; var used = fm.getBoundingClientRect().height + parseInt(getComputedStyle(document.body).paddingTop) * 2; col.style.height = (window.innerHeight - used) + 'px'; } window.addEventListener('load', setColHeight); window.addEventListener('resize', setColHeight); // zoom via ctrl+/- fires resize in most browsers, but also poll as fallback var _lastW = window.innerWidth, _lastH = window.innerHeight; setInterval(function() { if (window.innerWidth !== _lastW || window.innerHeight !== _lastH) { _lastW = window.innerWidth; _lastH = window.innerHeight; setColHeight(); } }, 200); ")) (body ,(render-front-matter fm) (main (@ (class "columns")) ,@items)))))) (display "\n") (sxml->xml (render-resume resume)) (newline)