about summary refs log tree commit diff stats
path: root/wikme.ss
blob: cc7aac18193c0ca41c54693f9a656c03eb7572ef (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
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
;;; Wikme --- convert a directory of markdown files into a static wiki

;;; Records

(define-record-type <wiki>
  ;;; A wiki is a collection of pages and assorted metadata.
  (make-wiki base-url                   ; base URL for links
             origin-dir                 ; origin directory
             destination-dir            ; destination directory
             pages                      ; list of <page>s
             defaults                   ; alist of default properties for pages
             )
  wiki?
  (base-url wiki-base-url (setter wiki-base-url))
  (origin-dir wiki-origin-dir (setter wiki-origin-dir))
  (destination-dir wiki-destination-dir (setter wiki-destination-dir))
  (pages wiki-pages (setter wiki-pages))
  (defaults wiki-defaults (setter wiki-defaults)))

(define (wiki-default-ref wiki key)
  (alist-ref key (wiki-defaults wiki)))

(define (wiki-default-set! wiki key value)
  (set! (wiki-defaults wiki) (cons (cons key value)
                                   (wiki-defaults wiki))))

(define-record-type <page>
  ;;; A wiki page is a mapping between source and body content, and between the
  ;;; page's origin and its destination files, wrapped together with some
  ;;; metadata.
  (make-page source                     ; source markup
             body                       ; rendered page body
             origin                     ; source filename (relative to wiki)
             destination                ; destination file (relative to wiki)
             template                   ; this page's template
             source-transformers        ; list of source transformer functions
             path-transformers          ; list of path transformers
             wiki                       ; the <wiki> this page is a part of
             meta                       ; alist of metadata tags
             )
  page?
  (source page-source (setter page-source))
  (body page-body (setter page-body))
  (origin page-origin (setter page-origin))
  (destination page-destination (setter page-destination))
  (template page-template (setter page-template))
  (source-transformers page-source-transformers (setter page-source-transformers))
  (path-transformers page-path-transformers (setter page-path-transformers))
  (wiki page-wiki (setter page-wiki))
  (meta page-meta (setter page-meta)))

(define (page-meta-ref page key)
  ;;; Get metadata KEY from PAGE.
  (alist-ref key (page-meta page)))

(define (page-meta-set! page key value)
  ;;; Set KEY in PAGE's metadata to VALUE.
  (set! (page-meta page) (cons (cons key value)
                               (page-meta page))))

;;; Transformers

(define (transform page input transformers)
  (let loop ((ts transformers)
             (it input))
    (if (null? ts)
        it
        (loop (cdr ts)
              ;; This is ugly but I need it for some things (namely, `indexify').
              ;; Transformers can add an #!optional _ parameter to ignore it.
              ((car ts) it page)))))

(define (transform-source! page)
  ;;; Transform PAGE's source to html.
  ;; Passes page-source through each of page-transformers in order and sets
  ;; page-body to the result.
  (set! (page-body page)
    (transform page (page-source page) (page-source-transformers page))))

(define (transform-path! page)
  ;;; Transform PAGE's path from input directory to output directory.
  ;; This will take the page-origin  and transform it using
  ;; page-path-transformers.  It will then set page-destination to the result.
  (set! (page-destination page)
    (transform page (page-origin page) (page-path-transformers page))))

;; Cmark wrapper
(define (page-cmark->html input #!optional page)
  (cmark->html input (page-meta-ref page 'safe)))

;;; Templates

(define (render-template template env)
  ;;; Render TEMPLATE using ENV.
  ;; TEMPLATE is a string with {{placeholders}}; ENV is an alist of key-value
  ;; pairs to insert into the TEMPLATE's placeholders.
  ;; XXX: Depends on the deprecated library (regex) which is just glue around
  ;; irregex (apparently).
  (string-substitute* template (env->replacements env)))

(define (env->replacements env)
  ;;; Convert an ENV alist of the form `((X . Y) ...) to '(("{{X}}" . "Y") ...).
  ;; X's are template variables and Y's are the values of those variables.  In
  ;; the template, both "{{X}}" and "{{ X }}" will be replaced.
  ;; If Y is a thunk, call it.
  (let loop ((env env)
             (res '()))
    (if (null? env)
        res
        (let* ((this (car env))
               (rest (cdr env))
               (key (->string (car this)))
               (val (if (procedure? (cdr this))
                        ((cdr this))
                        (->string (cdr this)))))
          (loop (cdr env)
                (append (list (cons (sprintf "{{~a}}" key) val)
                              (cons (sprintf "{{ ~a }}" key) val))
                        res))))))

(define (render page)
  (render-template (page-template page)
                   (append `((source . ,(page-source page))
                             (body . ,(page-body page))
                             (origin . ,(page-origin page))
                             (destination . ,(page-destination page)))
                           (page-meta page))))

;;; Links

(define wiki-link-sre
  ;;; An SRE for [[wiki-style links|with optional titles]].
  ;; XXX
  '(: "[["
      (=> pname (*? (~ "|")))
      (? ($ "|" (=> ptitle
                    (*? (~ "]")))))
      "]]"))

(define (wikify-links text #!optional page)
  ;;; Convert [[Wiki-style links]] to <a> tags in TEXT.
  ;; The base url of PAGE's wiki is prepended to the generated links.
  (irregex-replace/all wiki-link-sre
                       text
                       (lambda (m)
                         (let* ((pname (irregex-match-substring m 'pname))
                                (ptitle (or (irregex-match-substring m 'ptitle)
                                            pname)))
                           (sprintf "<a href=\"~a\">~a</a>"
                                    (linkify pname
                                             (if page
                                                 (wiki-base-url
                                                  (page-wiki page))
                                                 ""))
                                    ptitle)))))

;;; TODO: merge linkify and indexify ... they're almost the same thing.
(define (linkify pagename base-url)
  ;;; Turn a page name into a link suitable for an <a> tag.
  (make-pathname (list base-url (slugify pagename))
                 "index"
                 "html"))

(define (slugify str)
  ;;;  Convert STR to a 'slug', that is, another string suitable for linking.
  ;; This function will return the input string, in sentence case, and with all
  ;; punctuation and spaces converted to a hypen.
  (string-capitalize
   (string-trim-both (irregex-replace/all '(+ (~ alnum)) str "-")
                     (lambda (c)
                       (char=? c #\-)))))

(define (string-capitalize str)
  ;;; Capitalize the first word in STR, and ensure the rest of it is lowercase.
  ;; Returns the new string.
  (let loop ((cs (string->list str))
             (it #f))
    (if (null? cs)
        (reverse-list->string it)
        (loop (cdr cs)
              (if it
                  (cons (char-downcase (car cs)) it)
                  (list (char-upcase (car cs))))))))

(define (unslugify str)
  ;;; Convert a SLUG back into a normal string as best as possible.
  ;; Because information is lost in slugification, it's impossible to be sure
  ;; that the result of this procedure is totally accurate.  That is,
  ;; slugification is not round-trippable.
  (irregex-replace/all "-" str " "))

(define (path-relativize path dir)
  ;;; Return PATH relative to DIR.
  ;; Currently, this doesn't do anything special if PATH begins with / or ~ ...
  ;; it probably should.
  (let ((path (normalize-pathname path))
        (dir (normalize-pathname dir)))
    (make-pathname
     dir
     (string-trim (string-drop path (string-prefix-length path dir))
                  (lambda (c) (char=? c #\/))))))

(define (wiki-page-origin-path page #!optional wiki)
  ;;; Return PAGE's origin path in WIKI.
  (path-relativize (page-origin page)
                   (wiki-origin-dir (or wiki
                                        (page-wiki page)))))

(define (wiki-page-destination-path page #!optional wiki)
  ;;; Return PAGE's destination path in WIKI.
  (path-relativize (page-destination page)
                   (wiki-destination-dir (or wiki
                                             (page-wiki page)))))

;;; Build a page

(define (file->page file wiki
                    #!key
                    (source
                     (with-input-from-file file read-string))
                    (template
                     (wiki-default-ref wiki 'template))
                    (source-transformers
                     (wiki-default-ref wiki 'source-transformers))
                    (path-transformers
                     (wiki-default-ref wiki 'path-transformers)))
  (let ((page (make-page source
                         #f
                         (path-relativize file (wiki-origin-dir wiki))
                         #f
                         (with-input-from-file template read-string)
                         source-transformers
                         path-transformers
                         wiki
                         '())))
    (transform-source! page)
    (page-meta-set! page 'title (guess-title page))
    (page-meta-set! page 'last-updated (guess-last-updated page))
    (transform-path! page)
    (set! (wiki-pages wiki) (cons page (wiki-pages wiki)))
    page))

(define (guess-title page)
  ;;; Guess the title from PAGE.
  ;; If the first line is a Markdown H1 ("# ..."), use that as the title.
  ;; Otherwise, unslugify the basename of the PAGE file to use as the title.
  (let* ((str (page-body page))
         (m (irregex-match '(: "#" (* whitespace) ($ (* nonl)) (* any))
                           str)))
    (if (irregex-match-data? m)
        (irregex-match-substring m 1)
        (unslugify (basename (page-origin page))))))

(define (basename file)
  (receive (_ base _) (decompose-pathname file)
    base))

(define (indexify _origin page)
  ;;; Transform a PAGE's filename from a markdown source to an html destination.
  (make-pathname (slugify (page-meta-ref page 'title))
                 "index"
                 "html"))

(define (guess-last-updated page)
  ;;; Guess when PAGE was last edited.
  ;; Tries to use git, but falls back to mtime.
  (let ((f (wiki-page-origin-path page)))
    (time->string
     (seconds->local-time
      (or #;
       (string->number
       (string-trim-both
       (with-input-from-pipe
       (sprintf "git log -1 --format=%ct --date=unix -C ~s ~s"
       (wiki-origin-dir (page-wiki page))
       f)
       read-string)))
       (file-modification-time f))))))

(define (page->file page)
  ;;; Write PAGE to its destination file.
  (let ((dest (make-pathname (wiki-destination-dir (page-wiki page))
                             (page-destination page))))
    (receive (dir _ _) (decompose-pathname dest)
      (create-directory dir 'parents))
    (with-output-to-file dest
      (lambda ()
        (write-string (render page))))))

(define (eprintf . args)
  (apply fprintf (current-error-port) args))

(define (build-wiki origin
                    #!key
                    (destination (make-pathname origin "out"))
                    (base-url "")
                    (base-template (make-pathname origin "template.html"))
                    (source-transformers (list page-cmark->html wikify-links))
                    (path-transformers (list indexify))
                    (source-extension "md"))
  (define w (make-wiki
             base-url
             origin
             destination
             '()
             `((template . ,base-template)
               (source-transformers . ,source-transformers)
               (path-transformers . ,path-transformers)
               (source-extension . ,source-extension))))

  (eprintf "\nBuilding pages...\n")
  (for-each (lambda (f)
              (let ((p (file->page f w)))
                (eprintf "~a -> ~a\n" f (page-meta-ref p 'title))))
            (glob (make-pathname origin
                                 "*"
                                 (wiki-default-ref w 'source-extension))))

  (let ((dd (wiki-destination-dir w)))
    (eprintf "\nCreating destination directory: ~a\n" dd)
    (create-directory dd 'parents))

  (eprintf "\nWriting pages...\n")
  (for-each (lambda (p)
              (eprintf "~a -> ~a\n"
                       (page-meta-ref p 'title)
                       (wiki-page-destination-path p))
              (page->file p))
            (wiki-pages w)))