resume/generate.scm
2026-08-27 12:55:55 -04:00

220 lines
9.3 KiB
Scheme
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

;;; 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 "<!DOCTYPE html>\n")
(sxml->xml (render-resume resume))
(newline)