(load "utils.scm") (load "md-parser.scm") (import (chicken base) (chicken io) (chicken file posix) (chicken file) (chicken string) (chicken pathname) (chicken format) (chicken process-context) (chicken process) (chicken irregex) (chicken sort) (chicken port) (only srfi-13 string-join string-prefix?) srfi-18 matchable json utils md-parser ) ;; Utility: link adjacent archive posts (prev/newer, next/older) (define (add-prev-next-to-posts db) ;; Build a mapping from out-path -> navigation HTML, then patch each post's ;; work HTML (or, if already installed, the final out HTML). ;; ;; IMPORTANT: Only include real archive posts. ;; ;; The most recent "now" page exists as both: ;; - /archive/now-YYYY-season (real post) ;; - /archive/now (synthetic alias page) ;; ;; The alias must be excluded here, otherwise adjacency can point "previous" to ;; itself for the "now-*" post. ;; ;; Derive a stable "archive slug" from out-path, and de-duplicate based on it. ;; Relying on frontmatter `slug` is brittle because some pages don't have it. (define (archive-slug entry) (define out (lookup "out-path" entry)) (if (not (string? out)) #f (let* ((out2 (replace (format "^~A\\/" out-dir) "" out)) (out3 (replace "^\\/" "" out2)) ;; drop leading "archive/" and trailing ".html" (no-archive (replace (format "^~A\\/" archive-subdir) "" out3)) (no-ext (replace "\\.html$" "" no-archive))) (if (> (string-length no-ext) 0) no-ext #f)))) (define (only-archive-md-with-date entry) (and (equal? "md" (assocdr "file-type" entry)) (truthy? (lookup "is-archive?" entry)) (truthy? (lookup "date" entry)) (let ((out (lookup "out-path" entry))) (and (string? out) (irregex-search (irregex (format "^~A/~A/" out-dir archive-subdir)) out) (let ((s (archive-slug entry))) (and (string? s) (not (string=? s "now")))))))) (define posts (filter only-archive-md-with-date db)) (define (dedupe-by-archive-slug entries) (define (go xs seen acc) (if (null? xs) (reverse acc) (let* ((e (car xs)) (s (archive-slug e))) (if (and (string? s) (member s seen)) (go (cdr xs) seen acc) (go (cdr xs) (if (string? s) (cons s seen) seen) (cons e acc)))))) (go entries '() '())) (define posts-unique (dedupe-by-archive-slug posts)) ;; oldest first for clean adjacency indexing, then compute neighbors (define (less-oldest-first a b) (string (assocdr "date" a) (assocdr "date" b))) (define sorted (sort posts-unique less-oldest-first)) (define (strip-quotes s) (if (and (string? s) (>= (string-length s) 2) (string-prefix? "\"" s)) (substring s 1 (- (string-length s) 1)) s)) (define (nav-link class label-before label-after entry) (define slug (or (archive-slug entry) "")) (define title (strip-quotes (or (lookup "title" entry) ""))) (define href (format "/~A/~A" archive-subdir slug)) (string-append "" (if label-before (string-append label-before " ") "") title (if label-after (string-append " " label-after) "") "")) (define (nav-html prev next) ;; `prev` and `next` here refer to chronological neighbors: ;; - prev = older post ;; - next = newer post ;; Render with chevrons and align prev to the left, next to the right. (define prev-html (if prev (nav-link "post-nav-prev" "‹" #f prev) "")) (define next-html (if next (nav-link "post-nav-next" #f "›" next) "")) (string-append "")) (define (patch-file path nav) (if (and (string? path) (file-exists? path)) (let* ((html-or-eof (with-input-from-file path (λ () (read-string #f)))) (html (if (eof-object? html-or-eof) "" html-or-eof)) (marker "") (patched ;; If marker exists, replace it; otherwise, append nav before if present, else append at end. (let ((m (irregex-search (irregex (irregex-quote marker)) html))) (if m (irregex-replace/all (irregex (irregex-quote marker)) html nav) (let ((close-main (irregex-search (irregex "") html))) (if close-main (irregex-replace/all (irregex "") html (string-append nav "")) (string-append html "\n" nav))))))) (with-output-to-file path (λ () (print patched)))) #f)) ;; Iterate with indices to compute adjacent posts (define (loop xs prev) (if (null? xs) #f (let* ((curr (car xs)) (rest (cdr xs)) (next (if (null? rest) #f (car rest))) ;; Sorted oldest->newest, so: ;; - prev link should go to older => prev ;; - next link should go to newer => next ;; ;; Guard against self-links (can happen if duplicates slip through): ;; drop any neighbor whose derived archive slug equals the current one. (curr-slug (archive-slug curr)) (prev* (if (and prev curr-slug (archive-slug prev) (string=? (archive-slug prev) curr-slug)) #f prev)) (next* (if (and next curr-slug (archive-slug next) (string=? (archive-slug next) curr-slug)) #f next)) (nav (nav-html prev* next*)) (work-html (lookup "work-html-path" curr)) (out-html (lookup "out-path" curr))) ;; Prefer patching work file (before install), but if missing, patch final out file. (or (patch-file work-html nav) (patch-file out-html nav)) (loop rest curr)))) (loop sorted #f) db) ;; application (define src-dir (or (get-environment-variable "SRC_DIR") "pages")) (define out-dir (or (get-environment-variable "OUT_DIR") "out")) (define template-path (or (get-environment-variable "TEMPLATE_PATH") "template.html")) (define archive-subdir (or (get-environment-variable "ARCHIVE_SUBDIR") "archive")) (define static-dir (or (get-environment-variable "STATIC_DIR") "static")) (define linklog-json-path (or (get-environment-variable "LINKLOG_JSON_PATH") "linklog.json")) (define archive-index-md-path (create-temporary-file ".md")) (define feed-xml-path (create-temporary-file ".xml")) (define work-dir (create-temporary-directory)) (assert (> (string-length src-dir) 0)) (assert (> (string-length out-dir) 0)) (assert (> (string-length template-path) 0)) (define (preprocess-md md-path out-path) (define md-or-eof (with-input-from-file md-path (λ () (read-string #f)))) (define md (if (eof-object? md-or-eof) (begin (printf "[warn] \"~A\" is empty, using an empty md file...~%" md-path) "") ;; else md-or-eof)) (define cwd (decompose-pathname md-path)) (define out-path-dir (decompose-pathname out-path)) (create-directory out-path-dir #t) ;; Matches markdown links (define irx "\\[(.+?)\\]\\((.+?)\\)") (define (link-replace m) (define alt (submatch m 1)) (define link (submatch m 2)) ;; if link is absolute, external or in-page, leave as is. ;; otherwise, normalize it to the current directory context (define ret-link (if (or (substring=? "https://" link) (substring=? "/" link) (substring=? "#" link)) link ;; else (normalize-pathname (make-pathname cwd link)))) ;; replace src-dir prefix with webroot prefix (define rooted (replace (format "^~A\\/?" src-dir) "/" ret-link)) (format "[~A](~A)" alt rooted)) (define result (replace-all irx link-replace md)) (with-output-to-file out-path (λ () (print result)))) (define (preprocess-md-files db) (define (is-md? entry) (string=? "md" (assocdr "file-type" entry))) (define (act entry) (if (is-md? entry) (preprocess-md (assocdr "src-path" entry) (assocdr "work-md-path" entry)))) (for-each act db) db) (define (add-static-to-db db) (define paths (find-files static-dir)) (define new-entries '()) (define (act src-path) (define out-path (replace (format "^~A" static-dir) (make-pathname out-dir "static") src-path)) (set! new-entries (cons (list `("file-type" . "other") `("src-path" . ,src-path) `("out-path" . ,out-path)) new-entries))) (for-each act paths) (append new-entries db)) (define (add-now-page-to-db db) (define (inner return) (define (find-newest-now-page newest entries) (if (null? entries) newest (let* ((head (car entries)) (tail (cdr entries)) (head-file-type (lookup "file-type" head)) (head-date (lookup "date" head)) (head-kind (lookup "kind" head)) (newest-date (lookup "date" newest))) (if (and head-date (equal? head-file-type "md") (truthy? (lookup "is-archive?" head)) (equal? head-kind "now") (or (not newest) (string>? head-date newest-date))) (find-newest-now-page head tail) (find-newest-now-page newest tail))) )) (define newest (find-newest-now-page #f db)) (if (not newest) (return db)) (define out-path (format "~A/now.html" out-dir)) (define now-page (apply list `("out-path" . ,out-path) `("hide-body-title" . "defined") newest)) (cons now-page db)) (call/cc inner)) (define (is-md-path? path) (truthy? (irregex-match ".*?\\.md$" path))) (define (construct-db-from path) (match path [(? is-md-path?) (list (construct-db-md path))] [(? directory?) (construct-db-dir path)] [_ (list (construct-db-other path))])) (define (construct-db-md path) (define fm (or (read-md-frontmatter path) '())) (define-values (_ slug _) (decompose-pathname path)) (define work-md-file (pipe path (@ replace (format "^~A" src-dir) work-dir))) (define work-html-file (replace "\\.md$" ".html" work-md-file)) (define out-file (pipe path (@ replace (format "^~A" src-dir) out-dir) (@ replace "\\.md$" ".html"))) (define is-archive? (pipe path (@ irregex-match (format "^~A/~A.*" src-dir archive-subdir)) (λ (m) (not (eq? #f m))))) (apply list `("file-type" . "md") `("src-path" . ,path) `("work-md-path" . ,work-md-file) `("work-html-path" . ,work-html-file) `("out-path" . ,out-file) `("slug" . ,slug) `("is-archive?" . ,is-archive?) fm)) (define (construct-db-dir path) (define paths (pipe path (@ format "~A/*") (@ glob) (@ map (@ replace "^\\.\\/" "")))) (flatmap construct-db-from paths)) (define (construct-db-other path) (define out-path (replace (format "^~A" src-dir) out-dir path)) `(("file-type" . "other") ("src-path" . ,path) ("out-path" . ,out-path))) (define (apply-pandoc-to-md-files db) (define (is-md? entry) (equal? "md" (assocdr "file-type" entry))) (define posts (filter is-md? db)) (define (act post) (define from (assocdr "work-md-path" post)) (define to (assocdr "work-html-path" post)) (pandoc-md-to-html from to)) (for-each act posts) db) (define (generate-homepage db) ;; Replace the LATEST_POSTS placeholder inside the generated `index.html` ;; (created from the normal content pipeline) with HTML for the newest blog ;; posts from the in-memory db. (define (inner return) ;; We want posts that are: ;; - markdown-backed ;; - archive entries ;; - have a date for sorting ;; (any kind is allowed) (define (is-archive-md-post-with-date? entry) (and (equal? "md" (assocdr "file-type" entry)) (truthy? (lookup "is-archive?" entry)) (truthy? (lookup "date" entry)))) (define posts (filter is-archive-md-post-with-date? db)) ;; newest first (define (less? a b) (string>? (assocdr "date" a) (assocdr "date" b))) (define sorted (sort posts less?)) ;; take first n (non-destructive) (define (take n xs) (if (or (<= n 0) (null? xs)) '() (cons (car xs) (take (- n 1) (cdr xs))))) (define latest (take 3 sorted)) ;; Strip quotes from frontmatter fields like `"Title"` (define (strip-quotes s) (if (and (string? s) (>= (string-length s) 2) (string-prefix? "\"" s)) (substring s 1 (- (string-length s) 1)) s)) (define (post->html-li post) (define title (strip-quotes (or (lookup "title" post) ""))) (define date (or (lookup "date" post) "")) (define slug (or (lookup "slug" post) "")) (define link (format "/~A/~A" archive-subdir slug)) (format "