summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorCase Duckworth2023-08-15 23:33:17 -0500
committerCase Duckworth2023-08-15 23:33:17 -0500
commitd4830cdd422258a7c91a5ed07af50f8c208a29ee (patch)
treebacdf4124ef9b9467ea64c6d098a5cd78426912a
parentEtc (diff)
downloadboudin-d4830cdd422258a7c91a5ed07af50f8c208a29ee.tar.gz
boudin-d4830cdd422258a7c91a5ed07af50f8c208a29ee.zip
A new start (again)
-rw-r--r--.dir-locals.el4
-rw-r--r--.gitignore10
-rw-r--r--.repl4
-rw-r--r--Makefile15
-rw-r--r--boudin.egg49
-rw-r--r--[-rwxr-xr-x]boudin.scm521
-rw-r--r--boudin.sld7
-rw-r--r--lib/config.sld45
-rw-r--r--lib/schmaltz.sld17
-rw-r--r--lib/schmaltz.sls103
-rw-r--r--lib/types.sld24
-rw-r--r--lib/types.sls72
-rw-r--r--lib/util.sld50
-rw-r--r--test/test.html19
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 @@
1boudin 1boudin
2*.sh 2*.inline
3out \ 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
3DESTDIR =
4PREFIX = $(DESTDIR)/usr
5
6boudin: boudin.scm
7 chicken-install -n
8
9.PHONY: install
10install:
11 chicken-install
12
13.PHONY: clean
14clean:
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 -*-
3exec csi -R r7rs -s "$0" "$@"
4boudin --- 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
9Here is a test paragraph. <a href="#">example link</a>.
10
11Here's another. I wonder if it'll just do the thing .. or whatever. Maybe I
12should 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>