diff options
Diffstat (limited to 'wikme.ss')
-rw-r--r-- | wikme.ss | 329 |
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))) | ||