From d4830cdd422258a7c91a5ed07af50f8c208a29ee Mon Sep 17 00:00:00 2001
From: Case Duckworth
Date: Tue, 15 Aug 2023 23:33:17 -0500
Subject: A new start (again)
---
boudin.scm | 521 +------------------------------------------------------------
1 file changed, 8 insertions(+), 513 deletions(-)
mode change 100755 => 100644 boudin.scm
(limited to 'boudin.scm')
diff --git a/boudin.scm b/boudin.scm
old mode 100755
new mode 100644
index 28727ff..7bd741d
--- a/boudin.scm
+++ b/boudin.scm
@@ -1,519 +1,14 @@
-#!/bin/sh
-#| -*- scheme -*-
-exec csi -R r7rs -s "$0" "$@"
-boudin --- a little static site generator
-|#
+;;; (boudin) --- A small tasty ssg
-(import (chicken file)
- (chicken file posix)
- (chicken irregex)
- (chicken pathname)
- (chicken port)
- (chicken process-context)
- (chicken random)
- (chicken string)
- (chicken time posix)
- (chicanery)
- (atom)
- (html-parser)
- (scss)
- (srfi 37)
- (srfi 152)
- (sxml-serializer)
- (sxpath))
+(import (boudin))
-;;; Utilities
+(define foo (render-string "#,hello from Boudin!"
+ (interaction-environment)))
-(define el sxml->html)
-
-;;; Transformations
-;; A static site generator can be thought of largely as two sets of
-;; transformations: one transforming given input content to output content, and
-;; another transforming source paths to destination paths. Since both, for my
-;; purposes, are strings, I have a generic function that can perform both
-;; transformations.
-
-(define (transform str . procs) #| string (string ->string) ... -> string
- Apply PROCS to STR, left-to-right, and return the result.
- Each PROC will be called with its predecessor's output, and should take a
- string as input and return a string. |#
- (if (null? procs)
- str
- (apply transform ((car procs) str) (cdr procs))))
-
-;;; Path transformations
-
-(define (indexify path) #| path -> path
- Replace the PATH's extension with "/index.html".
- |#
- (make-pathname (pathname-strip-extension path)
- "index"
- "html"))
-
-(define (slugify path)
- (transform path
- normalize-pathname
- ;; XXX: this should be much more robust
- (lambda (p) (pathname-replace-directory p "/"))
- pathname-strip-extension
- (lambda (p) (string-append p "/"))))
-
-(define (transform-path path outdir) #| path => path
- Transform PATH according to boudin's needs. |#
- (transform path
- normalize-pathname
- (lambda (p) (pathname-replace-directory p outdir))
- indexify))
-
-;;; Content transformations
-
-(define (split-paragraphs str) #| string -> (list string ...)
- Split STR into paragraphs.
- A paragraph is a contiguous series of text lines separated from other
- paragraphs by at least 2 newline \n characters. This procedure collapses
- inter-paragraph space. |#
- (let loop ((lines (string-split str "\n"))
- (par '())
- (acc '()))
- (cond
- ((and (null? lines) ; base case: no more lines
- (null? par)) ; ... or pending paragraph
- (reverse acc))
- ((null? lines) ; add the final paragraph
- (loop '() '() (cons (apply string-append (reverse par)) acc)))
- ((equal? (car lines) "") ; paragraph break
- (loop (cdr lines)
- '()
- (cons (apply string-append (reverse par)) acc)))
- (else ; line break
- (loop (cdr lines)
- (cons (string-append (car lines) "\n") par)
- acc)))))
-
-(define (wrap-paragraphs str) #| string -> string
- Wrap naked paragraphs of STR in
tags.
- A 'naked' paragraph is one that doesn't begin with '<' (after optional
- beginning whitespace). |#
- (let loop ((pars (map string-trim (split-paragraphs str)))
- (acc '()))
- (cond
- ((null? pars)
- (apply string-append (reverse acc)))
- ((zero? (string-length (car pars)))
- (loop (cdr pars)
- acc))
- ((eq? #\< (string-ref (car pars) 0))
- (loop (cdr pars)
- (cons (car pars)
- acc)))
- (else
- (loop (cdr pars)
- (cons (string-append "
" (car pars) "
\n")
- acc))))))
-
-(define (preprocess-text str) #| string -> string
- Preprocess STR before passing it to `expand-string'.
- For example, replace all '#' with '##' unless followed by a '{' or '('.
- |#
- (set! str (irregex-replace/all
- '(or (: #\# #\# (look-ahead (or #\{ #\()))
- (: #\# (look-ahead (~ #\{ #\()))
- (: #\# eos))
- str
- "##"))
- #; (set! str (irregex-replace/all ; XXX: missing end paren
- '(: #\@ #\()
- str
- "#(sxml->html `("))
- ;; Return transformed string
- str)
-
-(define (expand-string str) #| string -> string
- Expand STR by passing it in a port to CHICKEN's #<# string interpolation.
- Yes, this is as cursed as it sounds.
-
- To make it slightly less so, all # are duplicated to escape them, except for
- those before ( and {. To escape /those/, double them. |#
- (let* ((delim (let loop ((attempt (number->string (pseudo-random-real))))
- (if (irregex-search attempt str)
- (loop (number->string (pseudo-random-real)))
- attempt)))
- (template (make-concatenated-port
- (open-input-string (string-append "#<#" delim "\n"))
- (open-input-string (preprocess-text str))
- (open-input-string (string-append "\n" delim "\n"))))
- (expanded (with-output-to-string
- (lambda ()
- (display (eval (read template)
- (interaction-environment)))))))
- (irregex-replace/all '(: "#"
- (* whitespace))
- expanded
- "")))
-
-(define (transform-content content) #| string -> string
- Transform CONTENT according to boudin's needs.
- This is the raw html, and will still need to be processed to extract metadata
- and to be further wrapped in a template. |#
- (transform content
- expand-string
- wrap-paragraphs))
-
-;;; Pages
-;; A is a record type that wraps the two transformations outlined above.
-;; It also includes the extracted metadata from the page for processing.
-
-(define-record-type
- (make-page slug meta source dest source-path dest-path)
- page?
- (slug page-slug (setter page-slug))
- (meta page-meta (setter page-meta))
- (source page-source)
- (dest page-dest (setter page-dest))
- (source-path page-source-path)
- (dest-path page-dest-path (setter page-dest-path)))
-
-(define (page-url page)
- (normalize-pathname
- (make-pathname (list (site-base-url)
- (page-slug page))
- #f)))
-
-(define (%read-port port) #| port -> string
- Read PORT until it hits eof and return the results as a string.
- |#
- (let ((chunk-size 512))
- (let loop ((next (read-string chunk-size port))
- (blank? #f)
- (acc '()))
- (cond
- ((or (eof-object? next)
- (and blank? (equal? next "")))
- (close-input-port port)
- (apply string-append (reverse acc)))
- ((equal? next "")
- (loop (read-string chunk-size port)
- #t
- (cons next acc)))
- (else
- (loop (read-string chunk-size port)
- blank?
- (cons next acc)))))))
-
-(define read-port #| (optional port) -> string
- Read PORT completely, returning the results as a string.
- PORT defaults to `current-input-port'.
- |#
- (case-lambda
- (() (%read-port (current-input-port)))
- ((p) (%read-port p))))
-
-(define (file->page file) #| string ->
- Convert FILE to an sxml tree after transforming it.
- This procedure returns both the sxml of the transformed content, but that
- page's metadata, too. |#
- (let* ((source (with-input-from-file file read-port))
- (dest (html->sxml (transform-content source))))
- (make-page (slugify file)
- (extract-meta dest)
- source
- dest
- file
- (transform-path file (build-directory)))))
-
-(define (extract-meta tree) #| sxml -> alist
- Extract metadata from TREE's comments.
- Returns an alist of (key . value) pairs where keys and values are strings. |#
- (let loop ((tree tree)
- (acc '()))
- (cond
- ((or (atom? tree)
- (null? tree))
- (reverse acc))
- ((and (list? (car tree))
- (eq? (caar tree) '*COMMENT*))
- (loop (cdr tree)
- (let* ((comment (string-trim-both (cadar tree)))
- (lines (string-split comment "\n")))
- (map (lambda (l)
- (let ((kv (string-split l ":")))
- (cons (string-trim-both (car kv))
- (string-trim
- (string-intersperse (cdr kv) ":")))))
- lines))))
- ((list? (car tree))
- (loop (cdr tree)
- (let ((subtree (loop (car tree) '())))
- (if (null? subtree)
- acc
- (cons subtree acc)))))
- (else (loop (cdr tree) acc)))))
-
-(define (meta-ref meta key default) #| alist string string -> >
- Get KEY's value from META, or DEFAULT if it doesn't exist.
- DEFAULT is required because I think it's a good idea to require it. |#
- (let ((x (assoc key meta)))
- (if x (cdr x) default)))
-
-(define (page-meta-ref page key default) #| string string -> >
- Get KEY's value from PAGE's meta, or DEFAULT.
- |#
- (let ((meta (page-meta page)))
- (meta-ref meta key default)))
-
-;;; Time
-;; Time really only matters in feeds ... but it really does matter. So I need a
-;; few helper functions.
-
-(define publish-time ; this is a parameter so it's consistent across a run.
- (make-parameter
- (time->string (seconds->utc-time) "%FT%TZ")))
-
-(define (page-mtime page) #| -> time-string
- Grab the mtime field from PAGE's source file. |#
- (let ((file (page-source-path page)))
- (and file
- (file-exists? file)
- (time->string (seconds->utc-time
- (file-modification-time
- file))))))
-
-(define (page-guess-updated page) #| -> time-string
- Guess the "updated" property of PAGE. |#
- (let ((meta-date (page-meta-ref page "date" #f)))
- (if meta-date
- ;; Attempt to parse the date metadata field.
- (time->string (seconds->utc-time ; This double-conversion is /great/
- (local-time->seconds
- (or (string->time meta-date "%Y-%m-%d")
- (string->time meta-date "%Y-%m-%d%n%H:%M")
- (string->time meta-date "%Y-%m-%d%n%I:%M%n%p")
- ;; ... more ?
- (or (page-mtime page)
- (publish-time))))))
- (or (page-mtime page)
- (publish-time)))))
-
-;;; Templating
-;; Templating uses sxml to define a layout for pages and indeces (index.html,
-;; feed.xml). Sxml's "stylesheets" can be used to extract metadata out of html
-;; comments and to further process the document.
-
-;; Each template has a default, but the user can override by defining templates
-;; in .config.scm (see below). All templates are function parameters that take
-;; a page's sxml tree (argument PAGE) and return a string.
-
-(define page-template
- (make-parameter
- (lambda (page)
- `(html (@ (lang "en"))
- (head (title ,(page-meta-ref page "title" "[untitled]"))
- (link (@ (href "../style.css") ; relative
- (rel "stylesheet")))
- (meta (@ (name "viewport")
- (content "initial-scale=1.0"))))
- (body ,(let ((title (page-meta-ref page "title" #f)))
- (if title `(h1 ,title) ""))
- ,@(cdr (page-dest page)))))))
-
-(define index-template
- (make-parameter
- (lambda pages
- `(html (@ (lang "en"))
- (head (title ,(site-name))
- (link (@ (href "./style.css") ; relative
- (rel "stylesheet")))
- (meta (@ (name "viewport")
- (content "initial-scale=1.0"))))
- (body (h1 ,(site-name))
- (ul
- ,@(map (lambda (pg)
- `(li (a (@ (href ,(page-slug pg)))
- ,(page-meta-ref pg
- "title"
- (pathname-file
- (page-source-path pg))))))
- ;; TODO : sort pages
- pages)))))))
-
-(define feed-template
- (make-parameter
- (lambda pages
- (make-atom-doc
- (make-feed
- title: (make-title (site-name))
- id: (site-base-url)
- updated: (publish-time) ; I don't like these semantics ..
- authors: (list (make-author name: (site-author)
- uri: (site-base-url)))
- links: (list (make-link type: 'html
- uri-language: "en"
- uri: (site-base-url))
- (make-link relation: "self"
- type: "application/atom+xml"
- uri: (make-pathname
- (site-base-url) "feed" "xml")))
- rights: (make-rights (site-rights))
- generator: (make-generator "Boudin"
- uri: "https://git.acdw.net/boudin"
- version: "0.1.0")
- entries: (map (lambda (pg)
- (make-entry
- title: (make-title
- (page-meta-ref pg "title" "[untitled]"))
- links: (list (make-link type: 'html
- uri: (page-url pg)))
- id: (page-url pg)
- updated: (page-guess-updated pg)
- ;;published:
- content: (make-content
- (sxml->html (page-dest pg))
- type: html)
- #;`(atom:content "foo")))
- pages))))))
-
-;;; Publishing
-
-(define (write-style)
- (print-log "writing style")
- (with-output-to-file (make-pathname (build-directory) "style" "css")
- (lambda () (write-css (site-style)))))
-
-(define (write-page page)
- (print-log "writing " (or (page-meta-ref page "title" #f)
- (page-source-path page)))
- (create-directory (pathname-directory (page-dest-path page)) 'parents)
- (with-output-to-file (page-dest-path page)
- (lambda () (sxml-display-as-html ((page-template) page)))))
-
-(define (write-index pages)
- (print-log "writing index")
- (with-output-to-file (make-pathname (build-directory) "index" "html")
- (lambda () (sxml-display-as-html (apply (index-template) pages)))))
-
-(define (write-feed pages)
- (print-log "writing feed")
- (serialize-sxml (apply (feed-template) pages)
- output: (make-pathname (build-directory) "feed" "xml")
- cdata-section-elements: '(atom:content)
- ns-prefixes: `((*default* . "http://www.w3.org/2005/Atom")
- (*default* . "http://www.w3.org/1999/xhtml")
- . ,(atom-ns-prefixes))
- allow-prefix-redeclarations: #t
- ))
-
-;;; Configuration
-
-;; Build configuration
-
-(define build-directory
- (make-parameter "out/"))
-
-(define build-config
- (make-parameter "config.scm"))
-
-;; Site configuration
-
-(define site-name
- (make-parameter "[A boudin web site]"))
-
-(define site-base-url
- (make-parameter "https://example.com/"))
-
-(define site-author
- (make-parameter "nobody"))
-
-(define site-rights
- (make-parameter (string-append "(C) " (site-author))))
-
-(define site-style
- (make-parameter
- `(css+ (body (font "20px/1.4 sans-serif")
- (max-width "48em")
- (padding "1em")
- (margin auto)))))
-
-;;; Options & Operands (SRFI 37)
-
-(define (print-log . xs)
- (with-output-to-port (current-error-port)
- (lambda () (apply print xs))))
-
-(define (die error-code . xs)
- (apply print-log xs)
- (exit error-code))
-
-(define usage
- (string-append
- "Usage: boudin [OPTIONS] FILE ...\n"
- "Options:\n"
- "-h, --help show this help and exit\n"
- "-o DIR, --output DIR build site to DIR (default: out/)\n"
- "Operands:\n"
- "FILE ... files to build\n"))
-
-(define opt/help
- (option '(#\h "help") ; Names
- #f ; Required arg?
- #f ; Optional arg?
- (lambda _ ; Option proc (opt name arg seeds ...)
- (die 0 usage))))
-
-(define opt/build-directory
- (option '(#\o "output") #t #f
- (lambda (opt name arg seeds)
- (build-directory arg)
- seeds)))
-
-(define opt/build-config
- (option '(#\c "config") #t #f
- (lambda (opt name arg seeds)
- (build-config arg)
- seeds)))
-
-(define (process-args args)
- (let ((pages '()))
- (args-fold args
- ;; Options
- (list opt/help
- opt/build-directory
- opt/build-config)
- ;; Unrecognized option proc (option name arg seeds ...)
- (lambda (_ name _ _)
- (die 1 "Unrecognized option: -" name "\n" usage))
- ;; Operand proc (operand seeds ...)
- (lambda (name seeds)
- (if (file-exists? name)
- (set! pages (cons name pages))
- (die 2 "File not found: " name))
- seeds)
- ;; Seeds
- '())
- pages))
-
-;;; Main entry point
-
-(define pages (make-parameter #f))
-
-(define (main args)
- (parameterize ((pages (process-args args)))
- (unless (pages)
- (die 0 "No pages to process. Abort."))
- ;; Check for files, create directories
- (if (file-exists? (build-config))
- (load (build-config))
- (print-log "No config.scm found; using default config"))
- (create-directory (build-directory) 'parents)
- ;; Convert pages to sxml. This needs to be done here because config.scm
- ;; might define functions used by the pages.
- (pages (map file->page (pages)))
- ;; Build the site
- (write-style) ; TODO: copy static assets (?)
- (for-each write-page (pages))
- (write-index (pages))
- (write-feed (pages))))
+(define (main . args)
+ (print foo)
+ (for-each print args))
(cond-expand
- ((or chicken-script compiling)
- (main (command-line-arguments)))
+ (compiling (apply main (command-line)))
(else))
--
cgit 1.4.1-21-gabe81