about summary refs log tree commit diff stats
path: root/wikme.ss
diff options
context:
space:
mode:
Diffstat (limited to 'wikme.ss')
-rw-r--r--wikme.ss329
1 files changed, 329 insertions, 0 deletions
diff --git a/wikme.ss b/wikme.ss new file mode 100644 index 0000000..cc7aac1 --- /dev/null +++ b/wikme.ss
@@ -0,0 +1,329 @@
1;;; Wikme --- convert a directory of markdown files into a static wiki
2
3;;; Records
4
5(define-record-type <wiki>
6 ;;; A wiki is a collection of pages and assorted metadata.
7 (make-wiki base-url ; base URL for links
8 origin-dir ; origin directory
9 destination-dir ; destination directory
10 pages ; list of <page>s
11 defaults ; alist of default properties for pages
12 )
13 wiki?
14 (base-url wiki-base-url (setter wiki-base-url))
15 (origin-dir wiki-origin-dir (setter wiki-origin-dir))
16 (destination-dir wiki-destination-dir (setter wiki-destination-dir))
17 (pages wiki-pages (setter wiki-pages))
18 (defaults wiki-defaults (setter wiki-defaults)))
19
20(define (wiki-default-ref wiki key)
21 (alist-ref key (wiki-defaults wiki)))
22
23(define (wiki-default-set! wiki key value)
24 (set! (wiki-defaults wiki) (cons (cons key value)
25 (wiki-defaults wiki))))
26
27(define-record-type <page>
28 ;;; A wiki page is a mapping between source and body content, and between the
29 ;;; page's origin and its destination files, wrapped together with some
30 ;;; metadata.
31 (make-page source ; source markup
32 body ; rendered page body
33 origin ; source filename (relative to wiki)
34 destination ; destination file (relative to wiki)
35 template ; this page's template
36 source-transformers ; list of source transformer functions
37 path-transformers ; list of path transformers
38 wiki ; the <wiki> this page is a part of
39 meta ; alist of metadata tags
40 )
41 page?
42 (source page-source (setter page-source))
43 (body page-body (setter page-body))
44 (origin page-origin (setter page-origin))
45 (destination page-destination (setter page-destination))
46 (template page-template (setter page-template))
47 (source-transformers page-source-transformers (setter page-source-transformers))
48 (path-transformers page-path-transformers (setter page-path-transformers))
49 (wiki page-wiki (setter page-wiki))
50 (meta page-meta (setter page-meta)))
51
52(define (page-meta-ref page key)
53 ;;; Get metadata KEY from PAGE.
54 (alist-ref key (page-meta page)))
55
56(define (page-meta-set! page key value)
57 ;;; Set KEY in PAGE's metadata to VALUE.
58 (set! (page-meta page) (cons (cons key value)
59 (page-meta page))))
60
61;;; Transformers
62
63(define (transform page input transformers)
64 (let loop ((ts transformers)
65 (it input))
66 (if (null? ts)
67 it
68 (loop (cdr ts)
69 ;; This is ugly but I need it for some things (namely, `indexify').
70 ;; Transformers can add an #!optional _ parameter to ignore it.
71 ((car ts) it page)))))
72
73(define (transform-source! page)
74 ;;; Transform PAGE's source to html.
75 ;; Passes page-source through each of page-transformers in order and sets
76 ;; page-body to the result.
77 (set! (page-body page)
78 (transform page (page-source page) (page-source-transformers page))))
79
80(define (transform-path! page)
81 ;;; Transform PAGE's path from input directory to output directory.
82 ;; This will take the page-origin and transform it using
83 ;; page-path-transformers. It will then set page-destination to the result.
84 (set! (page-destination page)
85 (transform page (page-origin page) (page-path-transformers page))))
86
87;; Cmark wrapper
88(define (page-cmark->html input #!optional page)
89 (cmark->html input (page-meta-ref page 'safe)))
90
91;;; Templates
92
93(define (render-template template env)
94 ;;; Render TEMPLATE using ENV.
95 ;; TEMPLATE is a string with {{placeholders}}; ENV is an alist of key-value
96 ;; pairs to insert into the TEMPLATE's placeholders.
97 ;; XXX: Depends on the deprecated library (regex) which is just glue around
98 ;; irregex (apparently).
99 (string-substitute* template (env->replacements env)))
100
101(define (env->replacements env)
102 ;;; Convert an ENV alist of the form `((X . Y) ...) to '(("{{X}}" . "Y") ...).
103 ;; X's are template variables and Y's are the values of those variables. In
104 ;; the template, both "{{X}}" and "{{ X }}" will be replaced.
105 ;; If Y is a thunk, call it.
106 (let loop ((env env)
107 (res '()))
108 (if (null? env)
109 res
110 (let* ((this (car env))
111 (rest (cdr env))
112 (key (->string (car this)))
113 (val (if (procedure? (cdr this))
114 ((cdr this))
115 (->string (cdr this)))))
116 (loop (cdr env)
117 (append (list (cons (sprintf "{{~a}}" key) val)
118 (cons (sprintf "{{ ~a }}" key) val))
119 res))))))
120
121(define (render page)
122 (render-template (page-template page)
123 (append `((source . ,(page-source page))
124 (body . ,(page-body page))
125 (origin . ,(page-origin page))
126 (destination . ,(page-destination page)))
127 (page-meta page))))
128
129;;; Links
130
131(define wiki-link-sre
132 ;;; An SRE for [[wiki-style links|with optional titles]].
133 ;; XXX
134 '(: "[["
135 (=> pname (*? (~ "|")))
136 (? ($ "|" (=> ptitle
137 (*? (~ "]")))))
138 "]]"))
139
140(define (wikify-links text #!optional page)
141 ;;; Convert [[Wiki-style links]] to <a> tags in TEXT.
142 ;; The base url of PAGE's wiki is prepended to the generated links.
143 (irregex-replace/all wiki-link-sre
144 text
145 (lambda (m)
146 (let* ((pname (irregex-match-substring m 'pname))
147 (ptitle (or (irregex-match-substring m 'ptitle)
148 pname)))
149 (sprintf "<a href=\"~a\">~a</a>"
150 (linkify pname
151 (if page
152 (wiki-base-url
153 (page-wiki page))
154 ""))
155 ptitle)))))
156
157;;; TODO: merge linkify and indexify ... they're almost the same thing.
158(define (linkify pagename base-url)
159 ;;; Turn a page name into a link suitable for an <a> tag.
160 (make-pathname (list base-url (slugify pagename))
161 "index"
162 "html"))
163
164(define (slugify str)
165 ;;; Convert STR to a 'slug', that is, another string suitable for linking.
166 ;; This function will return the input string, in sentence case, and with all
167 ;; punctuation and spaces converted to a hypen.
168 (string-capitalize
169 (string-trim-both (irregex-replace/all '(+ (~ alnum)) str "-")
170 (lambda (c)
171 (char=? c #\-)))))
172
173(define (string-capitalize str)
174 ;;; Capitalize the first word in STR, and ensure the rest of it is lowercase.
175 ;; Returns the new string.
176 (let loop ((cs (string->list str))
177 (it #f))
178 (if (null? cs)
179 (reverse-list->string it)
180 (loop (cdr cs)
181 (if it
182 (cons (char-downcase (car cs)) it)
183 (list (char-upcase (car cs))))))))
184
185(define (unslugify str)
186 ;;; Convert a SLUG back into a normal string as best as possible.
187 ;; Because information is lost in slugification, it's impossible to be sure
188 ;; that the result of this procedure is totally accurate. That is,
189 ;; slugification is not round-trippable.
190 (irregex-replace/all "-" str " "))
191
192(define (path-relativize path dir)
193 ;;; Return PATH relative to DIR.
194 ;; Currently, this doesn't do anything special if PATH begins with / or ~ ...
195 ;; it probably should.
196 (let ((path (normalize-pathname path))
197 (dir (normalize-pathname dir)))
198 (make-pathname
199 dir
200 (string-trim (string-drop path (string-prefix-length path dir))
201 (lambda (c) (char=? c #\/))))))
202
203(define (wiki-page-origin-path page #!optional wiki)
204 ;;; Return PAGE's origin path in WIKI.
205 (path-relativize (page-origin page)
206 (wiki-origin-dir (or wiki
207 (page-wiki page)))))
208
209(define (wiki-page-destination-path page #!optional wiki)
210 ;;; Return PAGE's destination path in WIKI.
211 (path-relativize (page-destination page)
212 (wiki-destination-dir (or wiki
213 (page-wiki page)))))
214
215;;; Build a page
216
217(define (file->page file wiki
218 #!key
219 (source
220 (with-input-from-file file read-string))
221 (template
222 (wiki-default-ref wiki 'template))
223 (source-transformers
224 (wiki-default-ref wiki 'source-transformers))
225 (path-transformers
226 (wiki-default-ref wiki 'path-transformers)))
227 (let ((page (make-page source
228 #f
229 (path-relativize file (wiki-origin-dir wiki))
230 #f
231 (with-input-from-file template read-string)
232 source-transformers
233 path-transformers
234 wiki
235 '())))
236 (transform-source! page)
237 (page-meta-set! page 'title (guess-title page))
238 (page-meta-set! page 'last-updated (guess-last-updated page))
239 (transform-path! page)
240 (set! (wiki-pages wiki) (cons page (wiki-pages wiki)))
241 page))
242
243(define (guess-title page)
244 ;;; Guess the title from PAGE.
245 ;; If the first line is a Markdown H1 ("# ..."), use that as the title.
246 ;; Otherwise, unslugify the basename of the PAGE file to use as the title.
247 (let* ((str (page-body page))
248 (m (irregex-match '(: "#" (* whitespace) ($ (* nonl)) (* any))
249 str)))
250 (if (irregex-match-data? m)
251 (irregex-match-substring m 1)
252 (unslugify (basename (page-origin page))))))
253
254(define (basename file)
255 (receive (_ base _) (decompose-pathname file)
256 base))
257
258(define (indexify _origin page)
259 ;;; Transform a PAGE's filename from a markdown source to an html destination.
260 (make-pathname (slugify (page-meta-ref page 'title))
261 "index"
262 "html"))
263
264(define (guess-last-updated page)
265 ;;; Guess when PAGE was last edited.
266 ;; Tries to use git, but falls back to mtime.
267 (let ((f (wiki-page-origin-path page)))
268 (time->string
269 (seconds->local-time
270 (or #;
271 (string->number
272 (string-trim-both
273 (with-input-from-pipe
274 (sprintf "git log -1 --format=%ct --date=unix -C ~s ~s"
275 (wiki-origin-dir (page-wiki page))
276 f)
277 read-string)))
278 (file-modification-time f))))))
279
280(define (page->file page)
281 ;;; Write PAGE to its destination file.
282 (let ((dest (make-pathname (wiki-destination-dir (page-wiki page))
283 (page-destination page))))
284 (receive (dir _ _) (decompose-pathname dest)
285 (create-directory dir 'parents))
286 (with-output-to-file dest
287 (lambda ()
288 (write-string (render page))))))
289
290(define (eprintf . args)
291 (apply fprintf (current-error-port) args))
292
293(define (build-wiki origin
294 #!key
295 (destination (make-pathname origin "out"))
296 (base-url "")
297 (base-template (make-pathname origin "template.html"))
298 (source-transformers (list page-cmark->html wikify-links))
299 (path-transformers (list indexify))
300 (source-extension "md"))
301 (define w (make-wiki
302 base-url
303 origin
304 destination
305 '()
306 `((template . ,base-template)
307 (source-transformers . ,source-transformers)
308 (path-transformers . ,path-transformers)
309 (source-extension . ,source-extension))))
310
311 (eprintf "\nBuilding pages...\n")
312 (for-each (lambda (f)
313 (let ((p (file->page f w)))
314 (eprintf "~a -> ~a\n" f (page-meta-ref p 'title))))
315 (glob (make-pathname origin
316 "*"
317 (wiki-default-ref w 'source-extension))))
318
319 (let ((dd (wiki-destination-dir w)))
320 (eprintf "\nCreating destination directory: ~a\n" dd)
321 (create-directory dd 'parents))
322
323 (eprintf "\nWriting pages...\n")
324 (for-each (lambda (p)
325 (eprintf "~a -> ~a\n"
326 (page-meta-ref p 'title)
327 (wiki-page-destination-path p))
328 (page->file p))
329 (wiki-pages w)))