summaryrefslogtreecommitdiff
path: root/haunt.scm
blob: 461d39b5f9bcd7760e1a858a19e721463c1822d5 (about) (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
;; -*- geiser-scheme-implementation: guile -*-

(use-modules (srfi srfi-1)  ; list stuff
             (srfi srfi-11) ; let-values
             (srfi srfi-19) ; date functions
             (srfi srfi-26) ; cut
             (ice-9 ftw)    ; file system
             (ice-9 match)  ; match-lambda
             (haunt reader)
             (haunt reader skribe)
             (haunt reader commonmark)
             (haunt site)
             (haunt asset)
             (haunt post) ;post-file-name
             (haunt page)
             (haunt html) ;sxml->html
             (haunt utils) ;absolute-file-name
             (haunt builder blog)
             (haunt builder atom))


(define skribe-reader
  (make-skribe-reader #:modules '((haunt skribe utils)
                                  (haunt utils)
                                  (skribe-utils))))


(define (photo-snippet post)
  ;; TODO: derive alt-text from "photo" metadata
  (let ((meta (post-ref post 'photo)))
    (if meta
        `(img (@ (class "stretch full")
                 (src ,(string-append "/images/posts/" meta))))
        '())))

;; TODO: use license
(define (license-snippet post)
  (let ((meta (post-ref post 'license)))
    (if meta
        `(div (@ (class "fineprint"))
              (div (@ (class "license"))
                   (a (@ (rel "license")
                         (href "http://creativecommons.org/licenses/by-sa/3.0/"))
                      (img (@ (alt "Creative Commons License")
                              (style "border-width:0")
                              (src "http://i.creativecommons.org/l/by-sa/3.0/80x15.png"))))
                   (span (@ (xmlns:dct "http://purl.org/dc/terms/")
                            (href "http://purl.org/dc/dcmitype/StillImage")
                            (property "dct:title")
                            (rel "dct:type"))
                         ,(post-ref post 'title))
                   " by "
                   (a (@ (xmlns:cc "http://creativecommons.org/ns#")
                         (href "http://elephly.net")
                         (property "cc:attributionName")
                         (rel "cc:attributionURL"))
                      "Ricardo Wurmus")
                   " is licensed under a "
                   (a (@ (rel "license")
                         (href "http://creativecommons.org/licenses/by-sa/3.0/"))
                      "Creative Commons Attribution-ShareAlike 3.0 Unported License")))
        '())))



(define (drop-extension file-name)
  (string-join
   (drop-right (string-split file-name #\.) 1) ""))


(define (post/file-base-name post)
  (drop-extension (basename (post-file-name post))))


(define* (read-page reader file-name layout target)
  "Read a page object from FILE-NAME using READER and wrap it in LAYOUT."
  (let-values (((metadata sxml) ((reader-proc reader) file-name)))
    (make-page target
               (layout #f (assoc-ref metadata 'title) sxml) ; site is #f
               sxml->html)))


(define* (wrap-pages directory dest layout readers)
  "Read all files in DIRECTORY, wrap them with the given LAYOUT and
place them in the directory DEST."
  (define enter? (const #t))

  ;; remove "directory" from the front of "file-name", prepend "dest"
  (define (leaf file-name stat memo)
    (let* ((reader      (find (cut reader-match? <> file-name) readers))
           (base-length (length (file-name-components directory)))
           (dest*       (file-name-components dest))
           (file-name*  (file-name-components file-name))
           (target      (join-file-name-components
                         (append dest* (drop file-name* base-length))))
           (target-name (string-append (drop-extension target) ".html")))
      (if reader
          (cons (read-page reader file-name default-layout target-name) memo)
          (error "no reader available for page: " file-name))))

  (define (noop file-name stat memo) memo)

  (define (err file-name stat errno memo)
    (error "layout processing failed with errno: " file-name errno))

  (file-system-fold enter? leaf noop noop noop err '() directory))


(define* (latest-blog-post #:key theme)
  "Return a builder procedure that copies the latest blog post to posts/latest.html."
  (lambda (site posts)
    (make-page "posts/latest.html"
               ((@@ (haunt builder blog) render-post)
                theme
                site
                (first (posts/reverse-chronological posts)))
               sxml->html)))

(define* (pin-blog-post file-name pinned-name #:key theme)
  "Return a builder procedure that copies FILE-NAME as PINNED-NAME."
  (lambda (site posts)
    (make-page pinned-name
               ((@@ (haunt builder blog) render-post) theme site
                (find (lambda (post)
                        (equal? (post-file-name post) file-name))
                      posts))
               sxml->html)))

(define* (tag-pages #:key
                    (theme elephly-theme)
                    (prefix "posts/")
                    (filter posts/reverse-chronological))
  "Return a builder procedure that renders a list page for every tag
used in a post.  All arguments are optional:

PREFIX: The directory in which to write the posts
FILTER: The procedure called to manipulate the posts list before rendering"
  (lambda (site posts)
    (define (tag-list tag posts all-posts)
      (define (render-list title posts prefix)
        (let ((body ((theme-collection-template theme)
                     site title posts prefix all-posts tag)))
          ((theme-layout theme) site title body)))
      (make-page (string-append "tags/" tag ".html")
                 (render-list (string-append "Posts tagged ‘" tag "’")
                              (filter posts)
                              prefix)
                 sxml->html))
    (let ((tag-groups (posts/group-by-tag posts)))
      (map (match-lambda
            ((tag . tagged-posts) (tag-list tag tagged-posts posts)))
           tag-groups))))


(define (tag-links posts)
  "Generate an alphabetically sorted list of links to tagged posts.
The link text consists of the tag name and the number of tagged posts
in parentheses."
  `(ul (@ (class "tags"))
       ,(map (match-lambda
              ((tag . posts)
               `(li (a (@ (href ,(string-append "/tags/" tag ".html")))
                       ,(string-append tag " (" (number->string (length posts)) ")")))))
             ;; sort by tag
             (sort (posts/group-by-tag posts)
                   (lambda (a b) (string<? (car a) (car b)))))))

(define (date->string* date)
  "Convert DATE to human readable string."
  (date->string date "~B ~e, ~Y"))



(define (default-layout site title body)
  `((doctype "html")
    (head
     (meta (@ (http-equiv "Content-Type") (content "text/html; charset=UTF-8")))
     (meta (@ (http-equiv "Content-Language") (content "en")))
     (meta (@ (name "author") (content "Ricardo Wurmus")))
     (meta (@ (name "viewport") (content "width=device-width")))
     (title ,(or title (if site (site-title site) "Rekado")))
     (link (@ (rel "openid2.provider")
              (href "https://openid.stackexchange.com/openid/provider")))
     (link (@ (rel "openid2.local_id")
              (href "https://openid.stackexchange.com/user/0f8f09d4-a2f3-429f-8752-b53a328c255c")))
     (link (@ (rel "stylesheet")
              (media "screen")
              (type "text/css")
              (href "/css/reset.css")))
     (link (@ (rel "stylesheet")
              (media "screen")
              (type "text/css")
              (href "/css/screen.css")))
     (link (@ (rel "shortcut icon")
              (href "https://elephly.net/favicon.ico")))
     (script (@ (type "text/javascript") (src "/js/deobfuscate.js")))
     (script (@ (type "text/javascript") (src "/js/hyphenator/Hyphenator.js")))
     (script (@ (type "text/javascript")) "Hyphenator.run();"))
    (body (@ (id "top"))
          (div (@ (id "index"))
               (a (@ (href "/") (title "show index"))
                  (img (@ (alt "logo")
                          (src "/images/logo.png")))))
          (div (@ (id "page") (class "hyphenate"))
               ,body))))


(define elephly-theme
  (theme #:name "Elephly"
         #:layout default-layout
         #:post-template  ; TODO: should also take "site" for "site-post-slug"
         (lambda (post)
           ;; TODO: similar version below for collection-template
           (define (post-uri post)
             (string-append "/posts/" (%make-slug post) ".html"))
           `((h1 (@ (class "donthyphenate")) ,(post-ref post 'title))
             (div (@ (class "time"))
                  (a (@ (href ,(post-uri post)))
                     ,(date->string* (post-date post))))
             ,(photo-snippet post)
             ,(post-sxml post)
             ,(license-snippet post)
             (p (@ (class "back"))
                (a (@ (href "/posts"))
                   "← other posts"))
             (div (@ (id "comments"))
              (p "Comments?  Then send me an email!  Interesting comments may be published here."))))
         #:collection-template
         (lambda* (site title posts prefix #:optional all-posts tag)
           (define (post-uri post)
             (string-append "/" (or prefix "") (site-post-slug site post) ".html"))
           `((h1 (@ (class "donthyphenate"))
                 ,title
                 ,(if tag
                      `(a (@ (href ,(string-append "/feeds/tags/" tag ".xml")))
                          (img (@ (class "feed-icon")
                                  (src "/images/feed.png")
                                  (alt "subscribe to atom feed"))))
                               '()))
             (ul (@ (class "archive"))
              ,@(map (lambda (post)
                       `(li
                         (a (@ (href ,(post-uri post)))
                            ,(post-ref post 'title))))
                     posts))
             (h2 "All tags")
             ;; TODO: I really want this to be computed only once for
             ;; all posts
             ,(tag-links (or all-posts posts))
             ,(if tag
                  '(a (@ (href "/posts"))
                      "← all posts")
                  '())))))


;; needed for post template, because the site is not passed to the
;; layout function
(define %make-slug post/file-base-name)

(site #:title "Rekado"
      #:domain "http://elephly.net"
      #:default-metadata
      '((author . "Ricardo Wurmus")
        (email  . "rekado+web@elephly.net"))
      #:make-slug %make-slug
      #:readers (list commonmark-reader skribe-reader html-reader)
      #:builders (list (lambda (args . rest)
                         (wrap-pages "non-posts" "." default-layout
                                     (list skribe-reader html-reader)))
                       (lambda _
                         (directory-assets "static" (const #t) "."))
                       (blog #:theme elephly-theme
                             #:prefix "posts/")
                       (tag-pages #:theme elephly-theme
                                  #:prefix "posts/")
                       (latest-blog-post #:theme elephly-theme)
                       (pin-blog-post "posts/2010-03-28-elephly.skr"
                                      "elephly.html"
                                      #:theme elephly-theme)
                       (pin-blog-post "posts/2010-03-23-fur-man.skr"
                                      "fur-man.html"
                                      #:theme elephly-theme)
                       (atom-feed #:blog-prefix "/posts")
                       (atom-feeds-by-tag #:blog-prefix "/posts")))