;;; 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) ────────────────── ;; Work-experience project card — bullet summary + hover-reveal detail bullets (define (render-work-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)))) ;; Hobby/personal project card — sentence summary + hover-reveal links (define (render-project proj) (let* ((links (field proj 'detail-links)) (has-links (and (pair? links) (> (length links) 0)))) `(div (@ (class "project-card") ,@(if has-links '((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))) (p (@ (class "project-summary")) ,(field proj 'summary)) ,(if has-links `(div (@ (class "project-details")) (ul ,@(map (lambda (link) `(li (a (@ (href ,(cdr link)) (target "_blank") (rel "noopener")) ,(car link)))) links))) '(span)) ,(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)) (p (@ (class "title-blurb")) ,(string-join (field tc 'blurb) " ")))) ;; Emit a title card followed by its work-project cards as a flat list of siblings (define (render-title-flat tc) (append (list (render-title-card tc)) (map render-work-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))))))) ;; ─── Column distribution ─────────────────────────────────────────────────────── ;; Wrap all items in two independent divs, splitting at the midpoint. ;; Because each col is a separate block formatting context, expanding a card ;; pushes siblings down within that col only — no cross-column movement, ;; no hover loop. ;; ;; num-columns: change to 3 if your resume gets too long, and update the ;; CSS grid-template-columns to match. (define num-columns 3) (define (heading? item) (and (pair? item) (eq? (car item) 'h2))) (define (list-ref* lst i) (if (= i 0) (car lst) (list-ref* (cdr lst) (- i 1)))) (define (split-into-columns items n) (let* ((total (length items)) (chunk (inexact->exact (ceiling (/ total n))))) (let loop ((i 0) (start 0) (remaining n) (acc '())) (if (= remaining 1) (reverse (cons `(div (@ (class "col")) ,@(list-tail items start)) acc)) (let* ((raw-end (min (+ start chunk) total)) ;; Walk backwards from the cut point: if the last item ;; in this slice is a heading (orphaned at col bottom), ;; move it to the next column. (end (let nudge ((e raw-end)) (if (and (> e (+ start 1)) (heading? (list-ref* items (- e 1)))) (nudge (- e 1)) e)))) (loop (+ i 1) end (- remaining 1) (cons `(div (@ (class "col")) ,@(list-tail (list-head items end) start)) acc))))))) ;; ─── 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))) (cols (split-into-columns items num-columns))) `(*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")))) (body ,(render-front-matter fm) (main (@ (class "columns")) ,@cols)))))) (display "\n") (sxml->xml (render-resume resume)) (newline)