diff options
author | Case Duckworth | 2023-08-15 23:33:17 -0500 |
---|---|---|
committer | Case Duckworth | 2023-08-15 23:33:17 -0500 |
commit | d4830cdd422258a7c91a5ed07af50f8c208a29ee (patch) | |
tree | bacdf4124ef9b9467ea64c6d098a5cd78426912a | |
parent | Etc (diff) | |
download | boudin-d4830cdd422258a7c91a5ed07af50f8c208a29ee.tar.gz boudin-d4830cdd422258a7c91a5ed07af50f8c208a29ee.zip |
A new start (again)
-rw-r--r-- | .dir-locals.el | 4 | ||||
-rw-r--r-- | .gitignore | 10 | ||||
-rw-r--r-- | .repl | 4 | ||||
-rw-r--r-- | Makefile | 15 | ||||
-rw-r--r-- | boudin.egg | 49 | ||||
-rw-r--r--[-rwxr-xr-x] | boudin.scm | 521 | ||||
-rw-r--r-- | boudin.sld | 7 | ||||
-rw-r--r-- | lib/config.sld | 45 | ||||
-rw-r--r-- | lib/schmaltz.sld | 17 | ||||
-rw-r--r-- | lib/schmaltz.sls | 103 | ||||
-rw-r--r-- | lib/types.sld | 24 | ||||
-rw-r--r-- | lib/types.sls | 72 | ||||
-rw-r--r-- | lib/util.sld | 50 | ||||
-rw-r--r-- | test/test.html | 19 |
14 files changed, 379 insertions, 561 deletions
diff --git a/.dir-locals.el b/.dir-locals.el new file mode 100644 index 0000000..9c78ce8 --- /dev/null +++ b/.dir-locals.el | |||
@@ -0,0 +1,4 @@ | |||
1 | ;;; Directory Local Variables -*- no-byte-compile: t -*- | ||
2 | ;;; For more information see (info "(emacs) Directory Variables") | ||
3 | |||
4 | ((scheme-mode . ((geiser-scheme-implementation . chicken)))) | ||
diff --git a/.gitignore b/.gitignore index 8e87b45..0b68fed 100644 --- a/.gitignore +++ b/.gitignore | |||
@@ -1,3 +1,9 @@ | |||
1 | boudin | 1 | boudin |
2 | *.sh | 2 | *.inline |
3 | out \ No newline at end of file | 3 | *.link |
4 | *.so | ||
5 | *.o | ||
6 | *.import.scm | ||
7 | *.types | ||
8 | *.build.sh | ||
9 | *.install.sh \ No newline at end of file | ||
diff --git a/.repl b/.repl new file mode 100644 index 0000000..229237b --- /dev/null +++ b/.repl | |||
@@ -0,0 +1,4 @@ | |||
1 | ;; -*- scheme -*- | ||
2 | (import (beaker system)) | ||
3 | |||
4 | (print "> (load-system \"boudin.egg\")") | ||
diff --git a/Makefile b/Makefile deleted file mode 100644 index 48999db..0000000 --- a/Makefile +++ /dev/null | |||
@@ -1,15 +0,0 @@ | |||
1 | # boudin : a small static site generator | ||
2 | |||
3 | DESTDIR = | ||
4 | PREFIX = $(DESTDIR)/usr | ||
5 | |||
6 | boudin: boudin.scm | ||
7 | chicken-install -n | ||
8 | |||
9 | .PHONY: install | ||
10 | install: | ||
11 | chicken-install | ||
12 | |||
13 | .PHONY: clean | ||
14 | clean: | ||
15 | rm -f *.build.sh *.install.sh | ||
diff --git a/boudin.egg b/boudin.egg index cc163ed..e9ca2dc 100644 --- a/boudin.egg +++ b/boudin.egg | |||
@@ -1,17 +1,42 @@ | |||
1 | ;; boudin -*- scheme | 1 | ;;; boudin -*- scheme -*- |
2 | 2 | ||
3 | ((synopsis "A small static site generator.") | 3 | ((synopsis "A small tasty ssg.") |
4 | (author "Case Duckworth") | 4 | (author "Case Duckworth") |
5 | (version "12044") | 5 | (version "0.0.0") |
6 | (license "God Willing License") | 6 | (license "God Willing License") |
7 | (category fluff) | 7 | |
8 | (dependencies chicanery | 8 | (dependencies chicanery r7rs utf8 |
9 | atom | ||
10 | html-parser | 9 | html-parser |
11 | scss | 10 | srfi-152) |
12 | srfi-37 | 11 | |
13 | srfi-152 | 12 | (component-options |
14 | sxml-serializer | 13 | (csc-options "-X" "r7rs" "-R" "r7rs" |
15 | sxpath) | 14 | "-X" "utf8" "-R" "utf8" |
15 | "-no-warnings")) | ||
16 | |||
16 | (components | 17 | (components |
17 | (program boudin))) | 18 | (program boudin |
19 | (component-dependencies boudin-lib)) | ||
20 | |||
21 | (extension boudin-lib | ||
22 | (source boudin.sld) | ||
23 | (modules boudin) | ||
24 | (install-name boudin) | ||
25 | (component-dependencies boudin.schmaltz)) | ||
26 | |||
27 | (extension boudin.config | ||
28 | (source lib/config.sld) | ||
29 | (component-dependencies boudin.schmaltz)) | ||
30 | |||
31 | (extension boudin.schmaltz | ||
32 | (source lib/schmaltz.sld) | ||
33 | (source-dependencies lib/schmaltz.sls)) | ||
34 | |||
35 | (extension boudin.types | ||
36 | (source lib/types.sld) | ||
37 | (source-dependencies lib/types.sls) | ||
38 | (component-dependencies boudin.config | ||
39 | boudin.util)) | ||
40 | |||
41 | (extension boudin.util | ||
42 | (source lib/util.sld)))) | ||
diff --git a/boudin.scm b/boudin.scm index 28727ff..7bd741d 100755..100644 --- a/boudin.scm +++ b/boudin.scm | |||
@@ -1,519 +1,14 @@ | |||
1 | #!/bin/sh | 1 | ;;; (boudin) --- A small tasty ssg |
2 | #| -*- scheme -*- | ||
3 | exec csi -R r7rs -s "$0" "$@" | ||
4 | boudin --- a little static site generator | ||
5 | |# | ||
6 | 2 | ||
7 | (import (chicken file) | 3 | (import (boudin)) |
8 | (chicken file posix) | ||
9 | (chicken irregex) | ||
10 | (chicken pathname) | ||
11 | (chicken port) | ||
12 | (chicken process-context) | ||
13 | (chicken random) | ||
14 | (chicken string) | ||
15 | (chicken time posix) | ||
16 | (chicanery) | ||
17 | (atom) | ||
18 | (html-parser) | ||
19 | (scss) | ||
20 | (srfi 37) | ||
21 | (srfi 152) | ||
22 | (sxml-serializer) | ||
23 | (sxpath)) | ||
24 | 4 | ||
25 | ;;; Utilities | 5 | (define foo (render-string "#,hello from Boudin!" |
6 | (interaction-environment))) | ||
26 | 7 | ||
27 | (define el sxml->html) | 8 | (define (main . args) |
28 | 9 | (print foo) | |
29 | ;;; Transformations | 10 | (for-each print args)) |
30 | ;; A static site generator can be thought of largely as two sets of | ||
31 | ;; transformations: one transforming given input content to output content, and | ||
32 | ;; another transforming source paths to destination paths. Since both, for my | ||
33 | ;; purposes, are strings, I have a generic function that can perform both | ||
34 | ;; transformations. | ||
35 | |||
36 | (define (transform str . procs) #| string (string ->string) ... -> string | ||
37 | Apply PROCS to STR, left-to-right, and return the result. | ||
38 | Each PROC will be called with its predecessor's output, and should take a | ||
39 | string as input and return a string. |# | ||
40 | (if (null? procs) | ||
41 | str | ||
42 | (apply transform ((car procs) str) (cdr procs)))) | ||
43 | |||
44 | ;;; Path transformations | ||
45 | |||
46 | (define (indexify path) #| path -> path | ||
47 | Replace the PATH's extension with "/index.html". | ||
48 | |# | ||
49 | (make-pathname (pathname-strip-extension path) | ||
50 | "index" | ||
51 | "html")) | ||
52 | |||
53 | (define (slugify path) | ||
54 | (transform path | ||
55 | normalize-pathname | ||
56 | ;; XXX: this should be much more robust | ||
57 | (lambda (p) (pathname-replace-directory p "/")) | ||
58 | pathname-strip-extension | ||
59 | (lambda (p) (string-append p "/")))) | ||
60 | |||
61 | (define (transform-path path outdir) #| path => path | ||
62 | Transform PATH according to boudin's needs. |# | ||
63 | (transform path | ||
64 | normalize-pathname | ||
65 | (lambda (p) (pathname-replace-directory p outdir)) | ||
66 | indexify)) | ||
67 | |||
68 | ;;; Content transformations | ||
69 | |||
70 | (define (split-paragraphs str) #| string -> (list string ...) | ||
71 | Split STR into paragraphs. | ||
72 | A paragraph is a contiguous series of text lines separated from other | ||
73 | paragraphs by at least 2 newline \n characters. This procedure collapses | ||
74 | inter-paragraph space. |# | ||
75 | (let loop ((lines (string-split str "\n")) | ||
76 | (par '()) | ||
77 | (acc '())) | ||
78 | (cond | ||
79 | ((and (null? lines) ; base case: no more lines | ||
80 | (null? par)) ; ... or pending paragraph | ||
81 | (reverse acc)) | ||
82 | ((null? lines) ; add the final paragraph | ||
83 | (loop '() '() (cons (apply string-append (reverse par)) acc))) | ||
84 | ((equal? (car lines) "") ; paragraph break | ||
85 | (loop (cdr lines) | ||
86 | '() | ||
87 | (cons (apply string-append (reverse par)) acc))) | ||
88 | (else ; line break | ||
89 | (loop (cdr lines) | ||
90 | (cons (string-append (car lines) "\n") par) | ||
91 | acc))))) | ||
92 | |||
93 | (define (wrap-paragraphs str) #| string -> string | ||
94 | Wrap naked paragraphs of STR in <p> tags. | ||
95 | A 'naked' paragraph is one that doesn't begin with '<' (after optional | ||
96 | beginning whitespace). |# | ||
97 | (let loop ((pars (map string-trim (split-paragraphs str))) | ||
98 | (acc '())) | ||
99 | (cond | ||
100 | ((null? pars) | ||
101 | (apply string-append (reverse acc))) | ||
102 | ((zero? (string-length (car pars))) | ||
103 | (loop (cdr pars) | ||
104 | acc)) | ||
105 | ((eq? #\< (string-ref (car pars) 0)) | ||
106 | (loop (cdr pars) | ||
107 | (cons (car pars) | ||
108 | acc))) | ||
109 | (else | ||
110 | (loop (cdr pars) | ||
111 | (cons (string-append "<p>" (car pars) "</p>\n") | ||
112 | acc)))))) | ||
113 | |||
114 | (define (preprocess-text str) #| string -> string | ||
115 | Preprocess STR before passing it to `expand-string'. | ||
116 | For example, replace all '#' with '##' unless followed by a '{' or '('. | ||
117 | |# | ||
118 | (set! str (irregex-replace/all | ||
119 | '(or (: #\# #\# (look-ahead (or #\{ #\())) | ||
120 | (: #\# (look-ahead (~ #\{ #\())) | ||
121 | (: #\# eos)) | ||
122 | str | ||
123 | "##")) | ||
124 | #; (set! str (irregex-replace/all ; XXX: missing end paren | ||
125 | '(: #\@ #\() | ||
126 | str | ||
127 | "#(sxml->html `(")) | ||
128 | ;; Return transformed string | ||
129 | str) | ||
130 | |||
131 | (define (expand-string str) #| string -> string | ||
132 | Expand STR by passing it in a port to CHICKEN's #<# string interpolation. | ||
133 | Yes, this is as cursed as it sounds. | ||
134 | |||
135 | To make it slightly less so, all # are duplicated to escape them, except for | ||
136 | those before ( and {. To escape /those/, double them. |# | ||
137 | (let* ((delim (let loop ((attempt (number->string (pseudo-random-real)))) | ||
138 | (if (irregex-search attempt str) | ||
139 | (loop (number->string (pseudo-random-real))) | ||
140 | attempt))) | ||
141 | (template (make-concatenated-port | ||
142 | (open-input-string (string-append "#<#" delim "\n")) | ||
143 | (open-input-string (preprocess-text str)) | ||
144 | (open-input-string (string-append "\n" delim "\n")))) | ||
145 | (expanded (with-output-to-string | ||
146 | (lambda () | ||
147 | (display (eval (read template) | ||
148 | (interaction-environment))))))) | ||
149 | (irregex-replace/all '(: "#<unspecified>" | ||
150 | (* whitespace)) | ||
151 | expanded | ||
152 | ""))) | ||
153 | |||
154 | (define (transform-content content) #| string -> string | ||
155 | Transform CONTENT according to boudin's needs. | ||
156 | This is the raw html, and will still need to be processed to extract metadata | ||
157 | and to be further wrapped in a template. |# | ||
158 | (transform content | ||
159 | expand-string | ||
160 | wrap-paragraphs)) | ||
161 | |||
162 | ;;; Pages | ||
163 | ;; A <page> is a record type that wraps the two transformations outlined above. | ||
164 | ;; It also includes the extracted metadata from the page for processing. | ||
165 | |||
166 | (define-record-type <page> | ||
167 | (make-page slug meta source dest source-path dest-path) | ||
168 | page? | ||
169 | (slug page-slug (setter page-slug)) | ||
170 | (meta page-meta (setter page-meta)) | ||
171 | (source page-source) | ||
172 | (dest page-dest (setter page-dest)) | ||
173 | (source-path page-source-path) | ||
174 | (dest-path page-dest-path (setter page-dest-path))) | ||
175 | |||
176 | (define (page-url page) | ||
177 | (normalize-pathname | ||
178 | (make-pathname (list (site-base-url) | ||
179 | (page-slug page)) | ||
180 | #f))) | ||
181 | |||
182 | (define (%read-port port) #| port -> string | ||
183 | Read PORT until it hits eof and return the results as a string. | ||
184 | |# | ||
185 | (let ((chunk-size 512)) | ||
186 | (let loop ((next (read-string chunk-size port)) | ||
187 | (blank? #f) | ||
188 | (acc '())) | ||
189 | (cond | ||
190 | ((or (eof-object? next) | ||
191 | (and blank? (equal? next ""))) | ||
192 | (close-input-port port) | ||
193 | (apply string-append (reverse acc))) | ||
194 | ((equal? next "") | ||
195 | (loop (read-string chunk-size port) | ||
196 | #t | ||
197 | (cons next acc))) | ||
198 | (else | ||
199 | (loop (read-string chunk-size port) | ||
200 | blank? | ||
201 | (cons next acc))))))) | ||
202 | |||
203 | (define read-port #| (optional port) -> string | ||
204 | Read PORT completely, returning the results as a string. | ||
205 | PORT defaults to `current-input-port'. | ||
206 | |# | ||
207 | (case-lambda | ||
208 | (() (%read-port (current-input-port))) | ||
209 | ((p) (%read-port p)))) | ||
210 | |||
211 | (define (file->page file) #| string -> <page> | ||
212 | Convert FILE to an sxml tree after transforming it. | ||
213 | This procedure returns both the sxml of the transformed content, but that | ||
214 | page's metadata, too. |# | ||
215 | (let* ((source (with-input-from-file file read-port)) | ||
216 | (dest (html->sxml (transform-content source)))) | ||
217 | (make-page (slugify file) | ||
218 | (extract-meta dest) | ||
219 | source | ||
220 | dest | ||
221 | file | ||
222 | (transform-path file (build-directory))))) | ||
223 | |||
224 | (define (extract-meta tree) #| sxml -> alist | ||
225 | Extract metadata from TREE's comments. | ||
226 | Returns an alist of (key . value) pairs where keys and values are strings. |# | ||
227 | (let loop ((tree tree) | ||
228 | (acc '())) | ||
229 | (cond | ||
230 | ((or (atom? tree) | ||
231 | (null? tree)) | ||
232 | (reverse acc)) | ||
233 | ((and (list? (car tree)) | ||
234 | (eq? (caar tree) '*COMMENT*)) | ||
235 | (loop (cdr tree) | ||
236 | (let* ((comment (string-trim-both (cadar tree))) | ||
237 | (lines (string-split comment "\n"))) | ||
238 | (map (lambda (l) | ||
239 | (let ((kv (string-split l ":"))) | ||
240 | (cons (string-trim-both (car kv)) | ||
241 | (string-trim | ||
242 | (string-intersperse (cdr kv) ":"))))) | ||
243 | lines)))) | ||
244 | ((list? (car tree)) | ||
245 | (loop (cdr tree) | ||
246 | (let ((subtree (loop (car tree) '()))) | ||
247 | (if (null? subtree) | ||
248 | acc | ||
249 | (cons subtree acc))))) | ||
250 | (else (loop (cdr tree) acc))))) | ||
251 | |||
252 | (define (meta-ref meta key default) #| alist string string -> <?> | ||
253 | Get KEY's value from META, or DEFAULT if it doesn't exist. | ||
254 | DEFAULT is required because I think it's a good idea to require it. |# | ||
255 | (let ((x (assoc key meta))) | ||
256 | (if x (cdr x) default))) | ||
257 | |||
258 | (define (page-meta-ref page key default) #| <page> string string -> <?> | ||
259 | Get KEY's value from PAGE's meta, or DEFAULT. | ||
260 | |# | ||
261 | (let ((meta (page-meta page))) | ||
262 | (meta-ref meta key default))) | ||
263 | |||
264 | ;;; Time | ||
265 | ;; Time really only matters in feeds ... but it really does matter. So I need a | ||
266 | ;; few helper functions. | ||
267 | |||
268 | (define publish-time ; this is a parameter so it's consistent across a run. | ||
269 | (make-parameter | ||
270 | (time->string (seconds->utc-time) "%FT%TZ"))) | ||
271 | |||
272 | (define (page-mtime page) #| <page> -> time-string | ||
273 | Grab the mtime field from PAGE's source file. |# | ||
274 | (let ((file (page-source-path page))) | ||
275 | (and file | ||
276 | (file-exists? file) | ||
277 | (time->string (seconds->utc-time | ||
278 | (file-modification-time | ||
279 | file)))))) | ||
280 | |||
281 | (define (page-guess-updated page) #| <page> -> time-string | ||
282 | Guess the "updated" property of PAGE. |# | ||
283 | (let ((meta-date (page-meta-ref page "date" #f))) | ||
284 | (if meta-date | ||
285 | ;; Attempt to parse the date metadata field. | ||
286 | (time->string (seconds->utc-time ; This double-conversion is /great/ | ||
287 | (local-time->seconds | ||
288 | (or (string->time meta-date "%Y-%m-%d") | ||
289 | (string->time meta-date "%Y-%m-%d%n%H:%M") | ||
290 | (string->time meta-date "%Y-%m-%d%n%I:%M%n%p") | ||
291 | ;; ... more ? | ||
292 | (or (page-mtime page) | ||
293 | (publish-time)))))) | ||
294 | (or (page-mtime page) | ||
295 | (publish-time))))) | ||
296 | |||
297 | ;;; Templating | ||
298 | ;; Templating uses sxml to define a layout for pages and indeces (index.html, | ||
299 | ;; feed.xml). Sxml's "stylesheets" can be used to extract metadata out of html | ||
300 | ;; comments and to further process the document. | ||
301 | |||
302 | ;; Each template has a default, but the user can override by defining templates | ||
303 | ;; in .config.scm (see below). All templates are function parameters that take | ||
304 | ;; a page's sxml tree (argument PAGE) and return a string. | ||
305 | |||
306 | (define page-template | ||
307 | (make-parameter | ||
308 | (lambda (page) | ||
309 | `(html (@ (lang "en")) | ||
310 | (head (title ,(page-meta-ref page "title" "[untitled]")) | ||
311 | (link (@ (href "../style.css") ; relative | ||
312 | (rel "stylesheet"))) | ||
313 | (meta (@ (name "viewport") | ||
314 | (content "initial-scale=1.0")))) | ||
315 | (body ,(let ((title (page-meta-ref page "title" #f))) | ||
316 | (if title `(h1 ,title) "")) | ||
317 | ,@(cdr (page-dest page))))))) | ||
318 | |||
319 | (define index-template | ||
320 | (make-parameter | ||
321 | (lambda pages | ||
322 | `(html (@ (lang "en")) | ||
323 | (head (title ,(site-name)) | ||
324 | (link (@ (href "./style.css") ; relative | ||
325 | (rel "stylesheet"))) | ||
326 | (meta (@ (name "viewport") | ||
327 | (content "initial-scale=1.0")))) | ||
328 | (body (h1 ,(site-name)) | ||
329 | (ul | ||
330 | ,@(map (lambda (pg) | ||
331 | `(li (a (@ (href ,(page-slug pg))) | ||
332 | ,(page-meta-ref pg | ||
333 | "title" | ||
334 | (pathname-file | ||
335 | (page-source-path pg)))))) | ||
336 | ;; TODO : sort pages | ||
337 | pages))))))) | ||
338 | |||
339 | (define feed-template | ||
340 | (make-parameter | ||
341 | (lambda pages | ||
342 | (make-atom-doc | ||
343 | (make-feed | ||
344 | title: (make-title (site-name)) | ||
345 | id: (site-base-url) | ||
346 | updated: (publish-time) ; I don't like these semantics .. | ||
347 | authors: (list (make-author name: (site-author) | ||
348 | uri: (site-base-url))) | ||
349 | links: (list (make-link type: 'html | ||
350 | uri-language: "en" | ||
351 | uri: (site-base-url)) | ||
352 | (make-link relation: "self" | ||
353 | type: "application/atom+xml" | ||
354 | uri: (make-pathname | ||
355 | (site-base-url) "feed" "xml"))) | ||
356 | rights: (make-rights (site-rights)) | ||
357 | generator: (make-generator "Boudin" | ||
358 | uri: "https://git.acdw.net/boudin" | ||
359 | version: "0.1.0") | ||
360 | entries: (map (lambda (pg) | ||
361 | (make-entry | ||
362 | title: (make-title | ||
363 | (page-meta-ref pg "title" "[untitled]")) | ||
364 | links: (list (make-link type: 'html | ||
365 | uri: (page-url pg))) | ||
366 | id: (page-url pg) | ||
367 | updated: (page-guess-updated pg) | ||
368 | ;;published: | ||
369 | content: (make-content | ||
370 | (sxml->html (page-dest pg)) | ||
371 | type: html) | ||
372 | #;`(atom:content "foo"))) | ||
373 | pages)))))) | ||
374 | |||
375 | ;;; Publishing | ||
376 | |||
377 | (define (write-style) | ||
378 | (print-log "writing style") | ||
379 | (with-output-to-file (make-pathname (build-directory) "style" "css") | ||
380 | (lambda () (write-css (site-style))))) | ||
381 | |||
382 | (define (write-page page) | ||
383 | (print-log "writing " (or (page-meta-ref page "title" #f) | ||
384 | (page-source-path page))) | ||
385 | (create-directory (pathname-directory (page-dest-path page)) 'parents) | ||
386 | (with-output-to-file (page-dest-path page) | ||
387 | (lambda () (sxml-display-as-html ((page-template) page))))) | ||
388 | |||
389 | (define (write-index pages) | ||
390 | (print-log "writing index") | ||
391 | (with-output-to-file (make-pathname (build-directory) "index" "html") | ||
392 | (lambda () (sxml-display-as-html (apply (index-template) pages))))) | ||
393 | |||
394 | (define (write-feed pages) | ||
395 | (print-log "writing feed") | ||
396 | (serialize-sxml (apply (feed-template) pages) | ||
397 | output: (make-pathname (build-directory) "feed" "xml") | ||
398 | cdata-section-elements: '(atom:content) | ||
399 | ns-prefixes: `((*default* . "http://www.w3.org/2005/Atom") | ||
400 | (*default* . "http://www.w3.org/1999/xhtml") | ||
401 | . ,(atom-ns-prefixes)) | ||
402 | allow-prefix-redeclarations: #t | ||
403 | )) | ||
404 | |||
405 | ;;; Configuration | ||
406 | |||
407 | ;; Build configuration | ||
408 | |||
409 | (define build-directory | ||
410 | (make-parameter "out/")) | ||
411 | |||
412 | (define build-config | ||
413 | (make-parameter "config.scm")) | ||
414 | |||
415 | ;; Site configuration | ||
416 | |||
417 | (define site-name | ||
418 | (make-parameter "[A boudin web site]")) | ||
419 | |||
420 | (define site-base-url | ||
421 | (make-parameter "https://example.com/")) | ||
422 | |||
423 | (define site-author | ||
424 | (make-parameter "nobody")) | ||
425 | |||
426 | (define site-rights | ||
427 | (make-parameter (string-append "(C) " (site-author)))) | ||
428 | |||
429 | (define site-style | ||
430 | (make-parameter | ||
431 | `(css+ (body (font "20px/1.4 sans-serif") | ||
432 | (max-width "48em") | ||
433 | (padding "1em") | ||
434 | (margin auto))))) | ||
435 | |||
436 | ;;; Options & Operands (SRFI 37) | ||
437 | |||
438 | (define (print-log . xs) | ||
439 | (with-output-to-port (current-error-port) | ||
440 | (lambda () (apply print xs)))) | ||
441 | |||
442 | (define (die error-code . xs) | ||
443 | (apply print-log xs) | ||
444 | (exit error-code)) | ||
445 | |||
446 | (define usage | ||
447 | (string-append | ||
448 | "Usage: boudin [OPTIONS] FILE ...\n" | ||
449 | "Options:\n" | ||
450 | "-h, --help show this help and exit\n" | ||
451 | "-o DIR, --output DIR build site to DIR (default: out/)\n" | ||
452 | "Operands:\n" | ||
453 | "FILE ... files to build\n")) | ||
454 | |||
455 | (define opt/help | ||
456 | (option '(#\h "help") ; Names | ||
457 | #f ; Required arg? | ||
458 | #f ; Optional arg? | ||
459 | (lambda _ ; Option proc (opt name arg seeds ...) | ||
460 | (die 0 usage)))) | ||
461 | |||
462 | (define opt/build-directory | ||
463 | (option '(#\o "output") #t #f | ||
464 | (lambda (opt name arg seeds) | ||
465 | (build-directory arg) | ||
466 | seeds))) | ||
467 | |||
468 | (define opt/build-config | ||
469 | (option '(#\c "config") #t #f | ||
470 | (lambda (opt name arg seeds) | ||
471 | (build-config arg) | ||
472 | seeds))) | ||
473 | |||
474 | (define (process-args args) | ||
475 | (let ((pages '())) | ||
476 | (args-fold args | ||
477 | ;; Options | ||
478 | (list opt/help | ||
479 | opt/build-directory | ||
480 | opt/build-config) | ||
481 | ;; Unrecognized option proc (option name arg seeds ...) | ||
482 | (lambda (_ name _ _) | ||
483 | (die 1 "Unrecognized option: -" name "\n" usage)) | ||
484 | ;; Operand proc (operand seeds ...) | ||
485 | (lambda (name seeds) | ||
486 | (if (file-exists? name) | ||
487 | (set! pages (cons name pages)) | ||
488 | (die 2 "File not found: " name)) | ||
489 | seeds) | ||
490 | ;; Seeds | ||
491 | '()) | ||
492 | pages)) | ||
493 | |||
494 | ;;; Main entry point | ||
495 | |||
496 | (define pages (make-parameter #f)) | ||
497 | |||
498 | (define (main args) | ||
499 | (parameterize ((pages (process-args args))) | ||
500 | (unless (pages) | ||
501 | (die 0 "No pages to process. Abort.")) | ||
502 | ;; Check for files, create directories | ||
503 | (if (file-exists? (build-config)) | ||
504 | (load (build-config)) | ||
505 | (print-log "No config.scm found; using default config")) | ||
506 | (create-directory (build-directory) 'parents) | ||
507 | ;; Convert pages to sxml. This needs to be done here because config.scm | ||
508 | ;; might define functions used by the pages. | ||
509 | (pages (map file->page (pages))) | ||
510 | ;; Build the site | ||
511 | (write-style) ; TODO: copy static assets (?) | ||
512 | (for-each write-page (pages)) | ||
513 | (write-index (pages)) | ||
514 | (write-feed (pages)))) | ||
515 | 11 | ||
516 | (cond-expand | 12 | (cond-expand |
517 | ((or chicken-script compiling) | 13 | (compiling (apply main (command-line))) |
518 | (main (command-line-arguments))) | ||
519 | (else)) | 14 | (else)) |
diff --git a/boudin.sld b/boudin.sld new file mode 100644 index 0000000..8129659 --- /dev/null +++ b/boudin.sld | |||
@@ -0,0 +1,7 @@ | |||
1 | (define-library (boudin) | ||
2 | (import (scheme base) | ||
3 | (scheme repl) | ||
4 | (boudin schmaltz)) | ||
5 | (export hello) | ||
6 | (begin | ||
7 | (define hello "Hi"))) | ||
diff --git a/lib/config.sld b/lib/config.sld new file mode 100644 index 0000000..bdd6ef5 --- /dev/null +++ b/lib/config.sld | |||
@@ -0,0 +1,45 @@ | |||
1 | ;;; (boudin config) --- default values for configuration options | ||
2 | ;; | ||
3 | ;; To change these, make a `config.scm' in your site's root directory and change | ||
4 | ;; these. They're all parameters so .. change em like that. | ||
5 | |||
6 | (define-library (boudin config) | ||
7 | (import (scheme base) | ||
8 | (boudin schmaltz) | ||
9 | ;; not portable | ||
10 | (chicken pathname) | ||
11 | (html-parser) | ||
12 | ) | ||
13 | |||
14 | (export site-url site-dest | ||
15 | page-path-transformers page-text-transformers | ||
16 | page-template index-template feed-template) | ||
17 | |||
18 | (begin | ||
19 | ;; Site information | ||
20 | (define site-url | ||
21 | (make-parameter "example.com")) | ||
22 | (define site-dest | ||
23 | (make-parameter "out/")) | ||
24 | ;; Transformers | ||
25 | (define page-path-transformers | ||
26 | (make-parameter | ||
27 | (list (lambda (path) (make-pathname (site-dest) path))))) | ||
28 | (define page-text-transformers | ||
29 | (make-parameter | ||
30 | (list wrap-paragraphs | ||
31 | render-string | ||
32 | html->sxml))) | ||
33 | ;; Templates --- note that we use quote but include unquote forms here. | ||
34 | ;; This is to simplify the configuration and to avoid a cyclical dependency | ||
35 | ;; with (boudin types). | ||
36 | (define page-template | ||
37 | (make-parameter | ||
38 | '(html (@ (lang "en-us")) | ||
39 | (head (title (or (page-ref pg "title") "[untitled]"))) | ||
40 | (body ,@(page-sxml pg))))) | ||
41 | (define index-template | ||
42 | (make-parameter 'todo)) | ||
43 | (define feed-template | ||
44 | (make-parameter 'todo)) | ||
45 | )) | ||
diff --git a/lib/schmaltz.sld b/lib/schmaltz.sld new file mode 100644 index 0000000..d54d53e --- /dev/null +++ b/lib/schmaltz.sld | |||
@@ -0,0 +1,17 @@ | |||
1 | (define-library (boudin schmaltz) | ||
2 | (export render | ||
3 | render-string | ||
4 | wrap-paragraphs) | ||
5 | |||
6 | (import (scheme base) | ||
7 | (scheme case-lambda) ; case-lambda | ||
8 | (scheme eval) ; eval | ||
9 | (scheme read) ; read | ||
10 | (scheme repl) ; interaction-environment | ||
11 | (scheme write) ; display | ||
12 | (only (html-parser) | ||
13 | sxml->html) | ||
14 | (only (srfi 152) | ||
15 | string-split string-trim)) | ||
16 | |||
17 | (include "lib/schmaltz.sls")) | ||
diff --git a/lib/schmaltz.sls b/lib/schmaltz.sls new file mode 100644 index 0000000..623191f --- /dev/null +++ b/lib/schmaltz.sls | |||
@@ -0,0 +1,103 @@ | |||
1 | ;;; (boudin schmaltz) --- transform almost-html plus scheme into html | ||
2 | |||
3 | ;;; Embedded scheme code | ||
4 | |||
5 | (define (->string x) | ||
6 | (call-with-port (open-output-string) | ||
7 | (lambda (port) | ||
8 | (display x port) | ||
9 | (get-output-string port)))) | ||
10 | |||
11 | (define render-string | ||
12 | (case-lambda | ||
13 | ((s) (render-string s (interaction-environment))) | ||
14 | ((s env) | ||
15 | (call-with-port (open-input-string s) | ||
16 | (lambda (port) | ||
17 | (render port env)))))) | ||
18 | |||
19 | (define (render port env) | ||
20 | ;; A few rough edges: | ||
21 | ;; #, x will try to render x | ||
22 | (define (burn-char) | ||
23 | ;; Throw a character away. I've defined this for clarity below. | ||
24 | (read-char port)) | ||
25 | |||
26 | (let loop ((ch (read-char port)) | ||
27 | (acc '())) | ||
28 | (define (proceed) (loop (read-char port) (cons ch acc))) | ||
29 | (cond | ||
30 | ((not ch) | ||
31 | (loop (read-char port) acc)) | ||
32 | ((eof-object? ch) | ||
33 | (list->string (reverse acc))) | ||
34 | ((eq? ch #\#) ; special processing to come | ||
35 | (case (peek-char port) | ||
36 | ((#\\) ; inhibit processing of the next char | ||
37 | (burn-char) | ||
38 | (loop (read-char port) (cons ch acc))) | ||
39 | ((#\,) ; scheme eval expansion | ||
40 | (burn-char) | ||
41 | (loop #f | ||
42 | (append (let ((s (->string | ||
43 | (eval (read port) | ||
44 | env)))) | ||
45 | (cond | ||
46 | ((equal? s "#<unspecified>") ; XXX NOT PORTABLE | ||
47 | '()) | ||
48 | ((equal? s "#!eof") ; XXX NOT PORTABLE | ||
49 | '(#\, #\#)) | ||
50 | (else (reverse (string->list s))))) | ||
51 | acc))) | ||
52 | ((#\@) ; embedded sxml | ||
53 | (burn-char) | ||
54 | (loop #f | ||
55 | (append (let ((h (eval `(sxml->html ,(list 'quasiquote | ||
56 | (read port))) | ||
57 | env))) | ||
58 | (cond | ||
59 | ((equal? h "#!eof") ; XXX NOT PORTABLE | ||
60 | '(#\@ #\#)) | ||
61 | (else (reverse (string->list h))))) | ||
62 | acc))) | ||
63 | (else (proceed)))) | ||
64 | (else (proceed))))) | ||
65 | |||
66 | ;;; Wrap paragraphs | ||
67 | |||
68 | (define (split-paragraphs str) | ||
69 | (let loop ((lines (string-split str "\n")) | ||
70 | (par '()) | ||
71 | (acc '())) | ||
72 | (cond | ||
73 | ((and (null? lines) ; base case: no more lines | ||
74 | (null? par)) ; ... or pending paragraph | ||
75 | (reverse acc)) | ||
76 | ((null? lines) ; add the final paragraph | ||
77 | (loop '() '() (cons (apply string-append (reverse par)) acc))) | ||
78 | ((equal? (car lines) "") ; paragraph break | ||
79 | (loop (cdr lines) | ||
80 | '() | ||
81 | (cons (apply string-append (reverse par)) acc))) | ||
82 | (else ; line break | ||
83 | (loop (cdr lines) | ||
84 | (cons (string-append (car lines) "\n") par) | ||
85 | acc))))) | ||
86 | |||
87 | (define (wrap-paragraphs str) | ||
88 | (let loop ((pars (split-paragraphs str)) | ||
89 | (acc '())) | ||
90 | (cond | ||
91 | ((null? pars) | ||
92 | (apply string-append (reverse acc))) | ||
93 | ((zero? (string-length (car pars))) | ||
94 | (loop (cdr pars) | ||
95 | acc)) | ||
96 | ((eq? #\< (string-ref (string-trim (car pars)) 0)) | ||
97 | (loop (cdr pars) | ||
98 | (cons (car pars) | ||
99 | acc))) | ||
100 | (else | ||
101 | (loop (cdr pars) | ||
102 | (cons (string-append "<p>" (car pars) "</p>\n") | ||
103 | acc)))))) | ||
diff --git a/lib/types.sld b/lib/types.sld new file mode 100644 index 0000000..791ff53 --- /dev/null +++ b/lib/types.sld | |||
@@ -0,0 +1,24 @@ | |||
1 | (define-library (boudin types) | ||
2 | (import (scheme base) | ||
3 | (scheme case-lambda) | ||
4 | (scheme file) | ||
5 | (boudin config) | ||
6 | (boudin util) | ||
7 | ;; non-portable bits | ||
8 | (chicken pathname) | ||
9 | (html-parser) | ||
10 | ) | ||
11 | |||
12 | (export | ||
13 | ;; pages | ||
14 | make-page page? | ||
15 | page-path page-dest page-text page-sxml page-meta | ||
16 | set-page-dest! set-page-text! set-page-sxml! set-page-meta! | ||
17 | extract-metadata page-ref page-set! | ||
18 | page-url page-slug | ||
19 | read-page write-page | ||
20 | ;; indeces | ||
21 | ;; static files | ||
22 | ) | ||
23 | |||
24 | (include "lib/types.sls")) | ||
diff --git a/lib/types.sls b/lib/types.sls new file mode 100644 index 0000000..2b4da5d --- /dev/null +++ b/lib/types.sls | |||
@@ -0,0 +1,72 @@ | |||
1 | ;;; (boudin types) --- pages, indeces, and static files | ||
2 | |||
3 | ;; All paths are relative to the site directory unless otherwise noted | ||
4 | |||
5 | (define-record-type page | ||
6 | (make-page path ; Input path | ||
7 | dest ; Output path (rel. to output directory) | ||
8 | text ; Input text | ||
9 | sxml ; Rendered sxml | ||
10 | meta ; Metadata (title, etc.) | ||
11 | ) | ||
12 | page? | ||
13 | (path page-path) | ||
14 | (dest page-dest set-page-dest!) | ||
15 | (text page-text set-page-text!) | ||
16 | (sxml page-sxml set-page-sxml!) | ||
17 | (meta page-meta set-page-meta!)) | ||
18 | |||
19 | (define (page-ref pg key) | ||
20 | (assoc-ref key (page-meta pg) (identity #f))) | ||
21 | |||
22 | (define (page-set! pg key val) | ||
23 | (set-page-meta! pg (cons (cons key val) | ||
24 | (page-meta pg)))) | ||
25 | |||
26 | (define (extract-metadata sxml) | ||
27 | #f) | ||
28 | |||
29 | (define (*urlify path) | ||
30 | (normalize-pathname | ||
31 | (make-pathname (list (site-url) | ||
32 | (pathname-strip-extension path)) | ||
33 | "index.html"))) | ||
34 | |||
35 | (define (page-url pg) ; foo.html => http://site.com/foo/index.html | ||
36 | (or (page-ref pg "url") ; memoization | ||
37 | (let ((url (*urlify (page-path pg)))) | ||
38 | (page-set! pg "url" url) | ||
39 | url))) | ||
40 | |||
41 | (define (*slugify url) ; I don't love how this is written.. | ||
42 | (let-values (((_ _ dirs) (decompose-directory url))) | ||
43 | (let loop ((this (car dirs)) | ||
44 | (rest (cdr dirs))) | ||
45 | (if (null? (cdr rest)) | ||
46 | (make-pathname (list "/" this) #f) | ||
47 | (loop (car rest) | ||
48 | (cdr rest)))))) | ||
49 | |||
50 | (define (page-slug pg) ; http://site.com/foo/index.html => /foo/ | ||
51 | (or (page-ref pg "slug") ; memoization | ||
52 | (let ((slug (*slugify (page-url pg)))) | ||
53 | (page-set! pg "slug" slug) | ||
54 | slug))) | ||
55 | |||
56 | (define (read-page path) | ||
57 | (let ((pg (make-page path #f #f #f #f))) | ||
58 | (set-page-dest! pg ((apply o (page-path-transformers)) path)) | ||
59 | (set-page-text! pg (with-input-from-file path slurp)) | ||
60 | (set-page-sxml! pg ((apply o (page-text-transformers)) (page-text pg))) | ||
61 | (set-page-meta! pg (extract-metadata (page-sxml pg))) | ||
62 | pg)) | ||
63 | |||
64 | (define write-page | ||
65 | (case-lambda | ||
66 | ((pg) (call-with-output-file (page-dest pg) | ||
67 | (lambda (port) (write-page pg port)))) | ||
68 | ((pg port) | ||
69 | (sxml-display-as-html ((eval/q (page-template)) pg) port)))) | ||
70 | |||
71 | |||
72 | |||
diff --git a/lib/util.sld b/lib/util.sld new file mode 100644 index 0000000..64c633e --- /dev/null +++ b/lib/util.sld | |||
@@ -0,0 +1,50 @@ | |||
1 | ;;; (boudin util) --- utility functions | ||
2 | |||
3 | (define-library (boudin util) | ||
4 | (import (scheme base) | ||
5 | (scheme case-lambda) | ||
6 | (scheme eval)) | ||
7 | |||
8 | (export identity | ||
9 | o | ||
10 | assoc-ref | ||
11 | slurp | ||
12 | eval/q) | ||
13 | |||
14 | (begin | ||
15 | (define (identity x) x) | ||
16 | |||
17 | (define (o . procs) ; stole from chicken core | ||
18 | (if (null? procs) | ||
19 | identity | ||
20 | (let loop ((procs procs)) | ||
21 | (let ((h (car procs)) | ||
22 | (t (cdr procs))) | ||
23 | (if (null? t) | ||
24 | h | ||
25 | (lambda (x) (h ((loop t) x)))))))) | ||
26 | |||
27 | (define assoc-ref | ||
28 | (case-lambda | ||
29 | ((key alist) | ||
30 | (assoc-ref alist | ||
31 | key | ||
32 | (lambda () (error "Unrecognized key." key)))) | ||
33 | ((key alist failure) | ||
34 | (cond ((assoc key alist) => cdr) | ||
35 | (else (failure)))))) | ||
36 | |||
37 | (define slurp | ||
38 | (case-lambda | ||
39 | (() (slurp (current-input-port))) | ||
40 | ((port) | ||
41 | (let loop ((ch (read-char)) | ||
42 | (acc '())) | ||
43 | (if (eof-object? ch) | ||
44 | (list->string (reverse acc)) | ||
45 | (loop (read-char) (cons ch acc))))))) | ||
46 | |||
47 | (define (eval/q form env) ; this is probably a bad idea | ||
48 | (eval (list 'quasiquote form) env)) | ||
49 | |||
50 | )) | ||
diff --git a/test/test.html b/test/test.html deleted file mode 100644 index c78e868..0000000 --- a/test/test.html +++ /dev/null | |||
@@ -1,19 +0,0 @@ | |||
1 | <!-- | ||
2 | title: foo bar | ||
3 | subtitle: uhhh: the thing | ||
4 | --> | ||
5 | |||
6 | <h1>some html</h1> | ||
7 | <h2>(without p tags)</h2> | ||
8 | |||
9 | Here is a test paragraph. <a href="#">example link</a>. | ||
10 | |||
11 | Here's another. I wonder if it'll just do the thing .. or whatever. Maybe I | ||
12 | should try to make it multiple lines, as well. | ||
13 | |||
14 | <ul class="again!"> | ||
15 | <li> | ||
16 | one plus two is #(+ 1 2). | ||
17 | </li> | ||
18 | <li>two</li> | ||
19 | </ul> | ||