about summary refs log tree commit diff stats
path: root/src/wikme.scm
blob: 14167d6f6b6e3aa3bb168a359452d8d26153d4a6 (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
;;;; wikme --- build a static wiki out of a folder of markdown files

(import (cmark)
        (srfi-152)
        (utf8)
        (chicken file)
        (chicken file posix)
        (chicken irregex)
        (chicken pathname)
        (chicken port)
        (chicken process)
        (chicken process-context)
        (chicken string)
        (chicken time posix))


;;; Wiki

(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
             page-template              ; template for pages
             file-transformers          ; list of filename transformers
             transformers               ; list of source transformer functions
             pages                      ; list of <page>s
             )
  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))
  (page-template wiki-page-template (setter wiki-page-template))
  (file-transformers wiki-file-transformers (setter wiki-file-transformers))
  (transformers wiki-transformers (setter wiki-transformers))
  (pages wiki-pages (setter wiki-pages)))

(define (directory->wiki directory
                         #!key
                         base-url
                         destination-directory
                         page-template
                         (extension "md")
                         (file-transformers (list indexify))
                         (transformers (list cmark->html wikify-links)))
  ;;; Build a <wiki> out of the markdown files in DIRECTORY.
  ;; The given keyword arguments will fill out the other fields in the result.

  (define wiki
    (make-wiki base-url
               directory
               destination-directory
               page-template
               file-transformers
               transformers
               '()))

  (set! (wiki-pages wiki)
    (map (lambda (f) (file->page f wiki))
         (glob (make-pathname directory "*.md"))))

  wiki)


;;; Pages

(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                     ; file containing the markup
             destination                ; destination file
             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-source))
  (origin page-origin (setter page-origin))
  (destination page-destination (setter page-destination))
  (wiki page-wiki (setter page-wiki))
  (meta page-meta (setter page-meta)))

(define (page-meta-ref page key)
  ;;; Get metadata KEY from PAGE.
  (cdr (assq 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))))

(define (basename file)
  (let-values (((_ base _) (decompose-pathname file)))
    base))

(define (indexify fname outdir)
  ;;; Transform a FILENAME of the form ..dir/name.md to outdir/name/index.html.
  (make-pathname (list outdir (basename fname)) "index" "html"))

(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) (submatch (nonl)) (* any))
                           str)))
    (if (irregex-match-data? m)
        (irregex-match-substring m 1)
        (unslugify (basename (page-origin page))))))

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

(define (file->page file wiki)
  ;;; Create a <page> from FILE in WIKI.
  ;; Wraps make-page for easier use.
  (define source
    (with-input-from-file file read-string))
  (define page
    (make-page source
               (apply transform source (wiki-transformers wiki))
               file
               (apply file-transform
                      file
                      (wiki-destination-dir wiki)
                      (wiki-file-transformers wiki))
               wiki
               '()))

  (page-meta-set! page 'title (guess-title page))
  (page-meta-set! page 'last-updated (guess-last-updated page))

  page)


;;; Wiki links

(define wiki-link-sre
  ;;; An SRE for [[wiki-style links|with optional titles]].
  '(: "[["
      (submatch-named page (+ (~ "|")))
      (? (submatch "|" (submatch-named title (*? nonl))))
      "]]"))

(define (wikify-links text)
  ;;; Convert [[Wiki-style links]] to HTML style in TEXT.
  (irregex-replace/all wiki-link-sre text
                       (lambda (m)
                         (let* ((page (irregex-match-substring m 'page))
                                (title (or (irregex-match-substring m 'title)
                                           page)))
                           (string-append
                            "<a href=\"" (linkify page) "\">" title "</a>")))))

(define (linkify pagename)
  ;;; Turn a page name into a link suitable for an <a> tag.
  (string-append (base-url) "/" (slugify pagename) "/index.html"))

(define (string-capitalize str)
  ;;; Capitalize the first word in STR, and ensure the rest of it is lowercase.
  ;; Stolen and adapted from MIT/GNU Scheme
  (let* ((end (string-length str))
         (str* (make-string end)))
    (do ((i 0 (+ i 1)))
        ((= i end))
      (string-set! str* i ((if (= i 0) char-upcase char-downcase)
                           (string-ref str i))))
    str*))

(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 (unslugify slug)
  ;;; 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 '("-") slug " "))


;;; Transform source

(define (transform source . transformers)
  ;;; Transform SOURCE to html by passing it through a series of TRANSFORMERS.
  ;; Each TRANSFORMER should be a one-argument procedure taking and returning a
  ;; string.
  (let loop ((transformers transformers)
             (output source))
    (if (null? transformers)
        output
        (loop (cdr transformers)
              ((car transformers) output)))))

(define (file-transform origin destination-directory . transformers)
  ;;; Transform ORIGIN to a DESTINATION filename using TRANSFORMERS.
  ;; Each TRANSFORMER will be called with two arguments: the ORIGIN filaname and
  ;; the DESTINATION-DIRECTORY.  It should return the transformed filename.
  (let loop ((transformers transformers)
             (destination origin))
    (if (null? transformers)
        destination
        (loop (cdr transformers)
              ((car transformers) origin destination-directory)))))


;;; Templates

(define (render 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.
  (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 (string-append "{{" key "}}") val)
                              (cons (string-append "{{ " key " }}") val))
                        env))))))

(define (render-page template page)
  ;;; Render PAGE with its metadata using TEMPLATE.
  (render template `((title . ,(page-meta-ref 'title page))
                     (body . ,(page-body page))
                     (last_updated ,(page-meta-ref 'last-updated page))
                     ;; TODO: backlinks and what-not
                     )))


;;; Wikify

(define (render-wiki wiki)
  ;;; Render the files in WIKI to disk.
  (create-directory (wiki-destination-dir wiki) #:parents)
  (for-each (lambda (page)
              (with-output-to-file (page-destination page)
                (lambda ()
                  (write-string
                   (render-page (wiki-page-template wiki) page)))))
            (wiki-pages wiki)))