diff options
-rw-r--r-- | .dir-locals.el | 4 | ||||
-rw-r--r-- | .gitignore | 3 | ||||
-rw-r--r-- | Makefile | 15 | ||||
-rw-r--r-- | README.txt | 5 | ||||
-rw-r--r-- | main.scm | 8 | ||||
-rw-r--r-- | test/birds.md | 5 | ||||
-rw-r--r-- | test/template.html | 6 | ||||
-rw-r--r-- | wikme-impl.scm | 290 | ||||
-rw-r--r-- | wikme-module.scm | 275 | ||||
-rw-r--r-- | wikme.scm | 56 |
10 files changed, 335 insertions, 332 deletions
diff --git a/.dir-locals.el b/.dir-locals.el new file mode 100644 index 0000000..9c78ce8 --- /dev/null +++ b/.dir-locals.el | |||
@@ -0,0 +1,4 @@ | |||
1 | ;;; Directory Local Variables -*- no-byte-compile: t -*- | ||
2 | ;;; For more information see (info "(emacs) Directory Variables") | ||
3 | |||
4 | ((scheme-mode . ((geiser-scheme-implementation . chicken)))) | ||
diff --git a/.gitignore b/.gitignore index 8270092..637f6da 100644 --- a/.gitignore +++ b/.gitignore | |||
@@ -1,2 +1,3 @@ | |||
1 | wikme | 1 | wikme |
2 | ref/ | 2 | *.so |
3 | *.o | ||
diff --git a/Makefile b/Makefile index 7b5b337..506d366 100644 --- a/Makefile +++ b/Makefile | |||
@@ -1,11 +1,20 @@ | |||
1 | # wikme | 1 | # wikme |
2 | 2 | ||
3 | PREFIX = /usr/bin | 3 | PREFIX = /usr/bin |
4 | CSC = csc -I$(PWD) | ||
4 | 5 | ||
5 | wikme: wikme.scm wikme-module.scm | 6 | OBJ = wikme.scm.o |
6 | csc -o $@ wikme.scm | ||
7 | 7 | ||
8 | .PHONY: install | 8 | wikme: main.scm $(OBJ) |
9 | $(CSC) -o $@ $(OBJ) -uses wikme main.scm | ||
10 | |||
11 | $(OBJ): wikme.scm wikme-impl.scm | ||
12 | $(CSC) -c -J wikme.scm -unit wikme -o $@ | ||
13 | |||
14 | .PHONY: install clean | ||
9 | 15 | ||
10 | install: wikme | 16 | install: wikme |
11 | install -Dt $(DESTDIR)$(PREFIX)/$@ $< | 17 | install -Dt $(DESTDIR)$(PREFIX)/$@ $< |
18 | |||
19 | clean: | ||
20 | rm -f *.import.scm *.so *.o | ||
diff --git a/README.txt b/README.txt index 8fd6f44..ed0c990 100644 --- a/README.txt +++ b/README.txt | |||
@@ -7,13 +7,16 @@ requirements: | |||
7 | - cmark | 7 | - cmark |
8 | - chicken scheme with the following eggs: | 8 | - chicken scheme with the following eggs: |
9 | - cmark | 9 | - cmark |
10 | - regex | ||
11 | - srfi-13 | ||
10 | - srfi-152 | 12 | - srfi-152 |
11 | - utf8 | 13 | - utf8 |
12 | 14 | ||
13 | build: | 15 | build: |
14 | 16 | ||
17 | - ./bootstrap | ||
15 | - make | 18 | - make |
16 | 19 | ||
17 | install: | 20 | install: |
18 | 21 | ||
19 | - make install | 22 | - make install |
diff --git a/main.scm b/main.scm new file mode 100644 index 0000000..c3e4554 --- /dev/null +++ b/main.scm | |||
@@ -0,0 +1,8 @@ | |||
1 | (import wikme | ||
2 | (chicken process-context)) | ||
3 | |||
4 | (define (main args) | ||
5 | (display (wikify-links "Hi from [[wikme]]!")) | ||
6 | (newline)) | ||
7 | |||
8 | (main (command-line-arguments)) | ||
diff --git a/test/birds.md b/test/birds.md new file mode 100644 index 0000000..0a02800 --- /dev/null +++ b/test/birds.md | |||
@@ -0,0 +1,5 @@ | |||
1 | # Birds! | ||
2 | |||
3 | Birds are pretty cool, you know. It's true. | ||
4 | |||
5 | Let's test a link to [[lions]] now. | ||
diff --git a/test/template.html b/test/template.html new file mode 100644 index 0000000..3de5bf2 --- /dev/null +++ b/test/template.html | |||
@@ -0,0 +1,6 @@ | |||
1 | <!DOCTYPE html> | ||
2 | <title>{{ title }}</title> | ||
3 | |||
4 | <body> | ||
5 | {{body}} | ||
6 | </body> | ||
diff --git a/wikme-impl.scm b/wikme-impl.scm new file mode 100644 index 0000000..2097af7 --- /dev/null +++ b/wikme-impl.scm | |||
@@ -0,0 +1,290 @@ | |||
1 | ;;; Wikme --- convert a directory of markdown files into a static wiki | ||
2 | |||
3 | (import (chicken file) | ||
4 | (chicken file posix) | ||
5 | (chicken format) | ||
6 | (chicken io) | ||
7 | (chicken irregex) | ||
8 | (chicken pathname) | ||
9 | (chicken port) | ||
10 | (chicken process) | ||
11 | (chicken process-context) | ||
12 | (chicken string) | ||
13 | (chicken time posix) | ||
14 | (cmark) | ||
15 | (regex) ; XXX: deprecated upstream | ||
16 | (srfi-13)) | ||
17 | |||
18 | ;;; Records | ||
19 | |||
20 | (define-record-type <wiki> | ||
21 | ;;; A wiki is a collection of pages and assorted metadata. | ||
22 | (make-wiki base-url ; base URL for links | ||
23 | origin-dir ; origin directory | ||
24 | destination-dir ; destination directory | ||
25 | pages ; list of <page>s | ||
26 | defaults ; alist of default properties for pages | ||
27 | ) | ||
28 | wiki? | ||
29 | (base-url wiki-base-url (setter wiki-base-url)) | ||
30 | (origin-dir wiki-origin-dir (setter wiki-origin-dir)) | ||
31 | (destination-dir wiki-destination-dir (setter wiki-destination-dir)) | ||
32 | (pages wiki-pages (setter wiki-pages)) | ||
33 | (defaults wiki-defaults (setter wiki-defaults))) | ||
34 | |||
35 | (define (wiki-default-ref wiki key) | ||
36 | (alist-ref key (wiki-defaults wiki))) | ||
37 | |||
38 | (define (wiki-default-set! wiki key value) | ||
39 | (set! (wiki-defaults wiki) (cons (cons key value) | ||
40 | (wiki-defaults wiki)))) | ||
41 | |||
42 | (define-record-type <page> | ||
43 | ;;; A wiki page is a mapping between source and body content, and between the | ||
44 | ;;; page's origin and its destination files, wrapped together with some | ||
45 | ;;; metadata. | ||
46 | (make-page source ; source markup | ||
47 | body ; rendered page body | ||
48 | origin ; source filename (relative to wiki) | ||
49 | destination ; destination file (relative to wiki) | ||
50 | template ; this page's template | ||
51 | source-transformers ; list of source transformer functions | ||
52 | path-transformers ; list of path transformers | ||
53 | wiki ; the <wiki> this page is a part of | ||
54 | meta ; alist of metadata tags | ||
55 | ) | ||
56 | page? | ||
57 | (source page-source (setter page-source)) | ||
58 | (body page-body (setter page-body)) | ||
59 | (origin page-origin (setter page-origin)) | ||
60 | (destination page-destination (setter page-destination)) | ||
61 | (template page-template (setter page-template)) | ||
62 | (source-transformers page-source-transformers (setter page-source-transformers)) | ||
63 | (path-transformers page-path-transformers (setter page-path-transformers)) | ||
64 | (wiki page-wiki (setter page-wiki)) | ||
65 | (meta page-meta (setter page-meta))) | ||
66 | |||
67 | (define (page-meta-ref page key) | ||
68 | ;;; Get metadata KEY from PAGE. | ||
69 | (alist-ref key (page-meta page))) | ||
70 | |||
71 | (define (page-meta-set! page key value) | ||
72 | ;;; Set KEY in PAGE's metadata to VALUE. | ||
73 | (set! (page-meta page) (cons (cons key value) | ||
74 | (page-meta page)))) | ||
75 | |||
76 | ;;; Transformers | ||
77 | |||
78 | (define (transform page input transformers) | ||
79 | (let loop ((ts transformers) | ||
80 | (it input)) | ||
81 | (if (null? ts) | ||
82 | it | ||
83 | (loop (cdr ts) | ||
84 | ;; This is ugly but I need it for some things (namely, `indexify'). | ||
85 | ;; Transformers can add an #!optional _ parameter to ignore it. | ||
86 | ((car ts) it page))))) | ||
87 | |||
88 | (define (transform-source! page) | ||
89 | ;;; Transform PAGE's source to html. | ||
90 | ;; Passes page-source through each of page-transformers in order and sets | ||
91 | ;; page-body to the result. | ||
92 | (set! (page-body page) | ||
93 | (transform page (page-source page) (page-source-transformers page)))) | ||
94 | |||
95 | (define (transform-path! page) | ||
96 | ;;; Transform PAGE's path from input directory to output directory. | ||
97 | ;; This will take the page-origin and transform it using | ||
98 | ;; page-path-transformers. It will then set page-destination to the result. | ||
99 | (set! (page-destination page) | ||
100 | (transform page (page-origin page) (page-path-transformers page)))) | ||
101 | |||
102 | ;; Cmark wrapper | ||
103 | (define (page-cmark->html input #!optional page) | ||
104 | (cmark->html input (page-meta-ref page 'safe))) | ||
105 | |||
106 | ;;; Templates | ||
107 | |||
108 | (define (render-template template env) | ||
109 | ;;; Render TEMPLATE using ENV. | ||
110 | ;; TEMPLATE is a string with {{placeholders}}; ENV is an alist of key-value | ||
111 | ;; pairs to insert into the TEMPLATE's placeholders. | ||
112 | ;; XXX: Depends on the deprecated library (regex) which is just glue around | ||
113 | ;; irregex (apparently). | ||
114 | (string-substitute* template (env->replacements env))) | ||
115 | |||
116 | (define (env->replacements env) | ||
117 | ;;; Convert an ENV alist of the form `((X . Y) ...) to '(("{{X}}" . "Y") ...). | ||
118 | ;; X's are template variables and Y's are the values of those variables. In | ||
119 | ;; the template, both "{{X}}" and "{{ X }}" will be replaced. | ||
120 | ;; If Y is a thunk, call it. | ||
121 | (let loop ((env env) | ||
122 | (res '())) | ||
123 | (if (null? env) | ||
124 | res | ||
125 | (let* ((this (car env)) | ||
126 | (rest (cdr env)) | ||
127 | (key (->string (car this))) | ||
128 | (val (if (procedure? (cdr this)) | ||
129 | ((cdr this)) | ||
130 | (->string (cdr this))))) | ||
131 | (loop (cdr env) | ||
132 | (append (list (cons (sprintf "{{~a}}" key) val) | ||
133 | (cons (sprintf "{{ ~a }}" key) val)) | ||
134 | res)))))) | ||
135 | |||
136 | (define (render page) | ||
137 | (render-template (page-template page) | ||
138 | (append `((source . ,(page-source page)) | ||
139 | (body . ,(page-body page)) | ||
140 | (origin . ,(page-origin page)) | ||
141 | (destination . ,(page-destination page))) | ||
142 | (page-meta page)))) | ||
143 | |||
144 | ;;; Links | ||
145 | |||
146 | (define wiki-link-sre | ||
147 | ;;; An SRE for [[wiki-style links|with optional titles]]. | ||
148 | '(: "[[" | ||
149 | (submatch-named pname (+ (~ "|"))) | ||
150 | (? (submatch "|" (submatch-named ptitle (*? nonl)))) | ||
151 | "]]")) | ||
152 | |||
153 | (define (wikify-links text #!optional page) | ||
154 | ;;; Convert [[Wiki-style links]] to <a> tags in TEXT. | ||
155 | ;; The base url of PAGE's wiki is prepended to the generated links. | ||
156 | (irregex-replace/all wiki-link-sre | ||
157 | text | ||
158 | (lambda (m) | ||
159 | (let* ((pname (irregex-match-substring m 'pname)) | ||
160 | (ptitle (or (irregex-match-substring m 'ptitle) | ||
161 | pname))) | ||
162 | (sprintf "<a href=\"~a\">~a</a>" | ||
163 | (linkify pname | ||
164 | (if page | ||
165 | (wiki-base-url | ||
166 | (page-wiki page)) | ||
167 | "")) | ||
168 | ptitle))))) | ||
169 | |||
170 | (define (linkify pagename base-url) | ||
171 | ;;; Turn a page name into a link suitable for an <a> tag. | ||
172 | (make-pathname (list base-url (slugify pagename)) | ||
173 | "index" | ||
174 | "html")) | ||
175 | |||
176 | (define (slugify str) | ||
177 | ;;; Convert STR to a 'slug', that is, another string suitable for linking. | ||
178 | ;; This function will return the input string, in sentence case, and with all | ||
179 | ;; punctuation and spaces converted to a hypen. | ||
180 | (string-capitalize | ||
181 | (string-trim-both (irregex-replace/all '(+ (~ alnum)) str "-") | ||
182 | (lambda (c) | ||
183 | (char=? c #\-))))) | ||
184 | |||
185 | (define (string-capitalize str) | ||
186 | ;;; Capitalize the first word in STR, and ensure the rest of it is lowercase. | ||
187 | ;; Returns the new string. | ||
188 | (let loop ((cs (string->list str)) | ||
189 | (it #f)) | ||
190 | (if (null? cs) | ||
191 | (reverse-list->string it) | ||
192 | (loop (cdr cs) | ||
193 | (if it | ||
194 | (cons (char-downcase (car cs)) it) | ||
195 | (list (char-upcase (car cs)))))))) | ||
196 | |||
197 | (define (unslugify str) | ||
198 | ;;; Convert a SLUG back into a normal string as best as possible. | ||
199 | ;; Because information is lost in slugification, it's impossible to be sure | ||
200 | ;; that the result of this procedure is totally accurate. That is, | ||
201 | ;; slugification is not round-trippable. | ||
202 | (irregex-replace/all "-" str " ")) | ||
203 | |||
204 | (define (path-relativize path dir) | ||
205 | ;;; Return PATH relative to DIR. | ||
206 | ;; Currently, this doesn't do anything special if PATH begins with / or ~ ... | ||
207 | ;; it probably should. | ||
208 | (let ((path (normalize-pathname path)) | ||
209 | (dir (normalize-pathname dir))) | ||
210 | (make-pathname | ||
211 | dir | ||
212 | (string-trim (string-drop path (string-prefix-length path dir)) | ||
213 | (lambda (c) (char=? c #\/)))))) | ||
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 (path-relativize (page-origin page) | ||
268 | (wiki-origin-dir (page-wiki page))))) | ||
269 | (time->string | ||
270 | (seconds->local-time | ||
271 | (or #; | ||
272 | (string->number | ||
273 | (string-trim-both | ||
274 | (with-input-from-pipe | ||
275 | (sprintf "git log -1 --format=%ct --date=unix -C ~s ~s" | ||
276 | (wiki-origin-dir (page-wiki page)) | ||
277 | f) | ||
278 | read-string))) | ||
279 | (file-modification-time f)))))) | ||
280 | |||
281 | (define (page->file page) | ||
282 | ;;; Write PAGE to its destination file. | ||
283 | (let ((dest (make-pathname (wiki-destination-dir (page-wiki page)) | ||
284 | (page-destination page)))) | ||
285 | (receive (dir _ _) (decompose-pathname dest) | ||
286 | (create-directory dir 'parents)) | ||
287 | (with-output-to-file dest | ||
288 | (lambda () | ||
289 | (write-string (render page)))))) | ||
290 | |||
diff --git a/wikme-module.scm b/wikme-module.scm deleted file mode 100644 index 14167d6..0000000 --- a/wikme-module.scm +++ /dev/null | |||
@@ -1,275 +0,0 @@ | |||
1 | ;;;; wikme --- build a static wiki out of a folder of markdown files | ||
2 | |||
3 | (import (cmark) | ||
4 | (srfi-152) | ||
5 | (utf8) | ||
6 | (chicken file) | ||
7 | (chicken file posix) | ||
8 | (chicken irregex) | ||
9 | (chicken pathname) | ||
10 | (chicken port) | ||
11 | (chicken process) | ||
12 | (chicken process-context) | ||
13 | (chicken string) | ||
14 | (chicken time posix)) | ||
15 | |||
16 | |||
17 | ;;; Wiki | ||
18 | |||
19 | (define-record-type <wiki> | ||
20 | ;;; A wiki is a collection of pages and assorted metadata. | ||
21 | (make-wiki base-url ; base URL for links | ||
22 | origin-dir ; origin directory | ||
23 | destination-dir ; destination directory | ||
24 | page-template ; template for pages | ||
25 | file-transformers ; list of filename transformers | ||
26 | transformers ; list of source transformer functions | ||
27 | pages ; list of <page>s | ||
28 | ) | ||
29 | wiki? | ||
30 | (base-url wiki-base-url (setter wiki-base-url)) | ||
31 | (origin-dir wiki-origin-dir (setter wiki-origin-dir)) | ||
32 | (destination-dir wiki-destination-dir (setter wiki-destination-dir)) | ||
33 | (page-template wiki-page-template (setter wiki-page-template)) | ||
34 | (file-transformers wiki-file-transformers (setter wiki-file-transformers)) | ||
35 | (transformers wiki-transformers (setter wiki-transformers)) | ||
36 | (pages wiki-pages (setter wiki-pages))) | ||
37 | |||
38 | (define (directory->wiki directory | ||
39 | #!key | ||
40 | base-url | ||
41 | destination-directory | ||
42 | page-template | ||
43 | (extension "md") | ||
44 | (file-transformers (list indexify)) | ||
45 | (transformers (list cmark->html wikify-links))) | ||
46 | ;;; Build a <wiki> out of the markdown files in DIRECTORY. | ||
47 | ;; The given keyword arguments will fill out the other fields in the result. | ||
48 | |||
49 | (define wiki | ||
50 | (make-wiki base-url | ||
51 | directory | ||
52 | destination-directory | ||
53 | page-template | ||
54 | file-transformers | ||
55 | transformers | ||
56 | '())) | ||
57 | |||
58 | (set! (wiki-pages wiki) | ||
59 | (map (lambda (f) (file->page f wiki)) | ||
60 | (glob (make-pathname directory "*.md")))) | ||
61 | |||
62 | wiki) | ||
63 | |||
64 | |||
65 | ;;; Pages | ||
66 | |||
67 | (define-record-type <page> | ||
68 | ;;; A wiki page is a mapping between source and body content, and between the | ||
69 | ;;; page's origin and its destination files, wrapped together with some | ||
70 | ;;; metadata. | ||
71 | (make-page source ; source markup | ||
72 | body ; rendered page body | ||
73 | origin ; file containing the markup | ||
74 | destination ; destination file | ||
75 | wiki ; the <wiki> this page is a part of | ||
76 | meta ; alist of metadata tags | ||
77 | ) | ||
78 | page? | ||
79 | (source page-source (setter page-source)) | ||
80 | (body page-body (setter page-source)) | ||
81 | (origin page-origin (setter page-origin)) | ||
82 | (destination page-destination (setter page-destination)) | ||
83 | (wiki page-wiki (setter page-wiki)) | ||
84 | (meta page-meta (setter page-meta))) | ||
85 | |||
86 | (define (page-meta-ref page key) | ||
87 | ;;; Get metadata KEY from PAGE. | ||
88 | (cdr (assq key (page-meta page)))) | ||
89 | |||
90 | (define (page-meta-set! page key value) | ||
91 | ;;; Set KEY in PAGE's metadata to VALUE. | ||
92 | (set! (page-meta page) (cons (cons key value) | ||
93 | (page-meta page)))) | ||
94 | |||
95 | (define (basename file) | ||
96 | (let-values (((_ base _) (decompose-pathname file))) | ||
97 | base)) | ||
98 | |||
99 | (define (indexify fname outdir) | ||
100 | ;;; Transform a FILENAME of the form ..dir/name.md to outdir/name/index.html. | ||
101 | (make-pathname (list outdir (basename fname)) "index" "html")) | ||
102 | |||
103 | (define (guess-title page) | ||
104 | ;;; Guess the title from PAGE. | ||
105 | ;; If the first line is a Markdown H1 ("# ..."), use that as the title. | ||
106 | ;; Otherwise, unslugify the basename of the PAGE file to use as the title. | ||
107 | (let* ((str (page-body page)) | ||
108 | (m (irregex-match '(: "#" (* whitespace) (submatch (nonl)) (* any)) | ||
109 | str))) | ||
110 | (if (irregex-match-data? m) | ||
111 | (irregex-match-substring m 1) | ||
112 | (unslugify (basename (page-origin page)))))) | ||
113 | |||
114 | (define (guess-last-updated page) | ||
115 | ;;; Guess when PAGE was last edited. | ||
116 | ;; Tries to use git, but falls back to mtime. | ||
117 | (let ((f (page-origin page))) | ||
118 | (time->string | ||
119 | (seconds->local-time | ||
120 | (or (string->number | ||
121 | (string-trim-both | ||
122 | (with-input-from-pipe | ||
123 | (string-join '("git" "log" "-1" "--format=%ct" "--date=unix" | ||
124 | "-C" (wiki-origin-dir (page-wiki page)) | ||
125 | f) | ||
126 | " ") | ||
127 | read-string))) | ||
128 | (file-modification-time f)))))) | ||
129 | |||
130 | (define (file->page file wiki) | ||
131 | ;;; Create a <page> from FILE in WIKI. | ||
132 | ;; Wraps make-page for easier use. | ||
133 | (define source | ||
134 | (with-input-from-file file read-string)) | ||
135 | (define page | ||
136 | (make-page source | ||
137 | (apply transform source (wiki-transformers wiki)) | ||
138 | file | ||
139 | (apply file-transform | ||
140 | file | ||
141 | (wiki-destination-dir wiki) | ||
142 | (wiki-file-transformers wiki)) | ||
143 | wiki | ||
144 | '())) | ||
145 | |||
146 | (page-meta-set! page 'title (guess-title page)) | ||
147 | (page-meta-set! page 'last-updated (guess-last-updated page)) | ||
148 | |||
149 | page) | ||
150 | |||
151 | |||
152 | ;;; Wiki links | ||
153 | |||
154 | (define wiki-link-sre | ||
155 | ;;; An SRE for [[wiki-style links|with optional titles]]. | ||
156 | '(: "[[" | ||
157 | (submatch-named page (+ (~ "|"))) | ||
158 | (? (submatch "|" (submatch-named title (*? nonl)))) | ||
159 | "]]")) | ||
160 | |||
161 | (define (wikify-links text) | ||
162 | ;;; Convert [[Wiki-style links]] to HTML style in TEXT. | ||
163 | (irregex-replace/all wiki-link-sre text | ||
164 | (lambda (m) | ||
165 | (let* ((page (irregex-match-substring m 'page)) | ||
166 | (title (or (irregex-match-substring m 'title) | ||
167 | page))) | ||
168 | (string-append | ||
169 | "<a href=\"" (linkify page) "\">" title "</a>"))))) | ||
170 | |||
171 | (define (linkify pagename) | ||
172 | ;;; Turn a page name into a link suitable for an <a> tag. | ||
173 | (string-append (base-url) "/" (slugify pagename) "/index.html")) | ||
174 | |||
175 | (define (string-capitalize str) | ||
176 | ;;; Capitalize the first word in STR, and ensure the rest of it is lowercase. | ||
177 | ;; Stolen and adapted from MIT/GNU Scheme | ||
178 | (let* ((end (string-length str)) | ||
179 | (str* (make-string end))) | ||
180 | (do ((i 0 (+ i 1))) | ||
181 | ((= i end)) | ||
182 | (string-set! str* i ((if (= i 0) char-upcase char-downcase) | ||
183 | (string-ref str i)))) | ||
184 | str*)) | ||
185 | |||
186 | (define (slugify str) | ||
187 | ;;; Convert STR to a 'slug', that is, another string suitable for linking. | ||
188 | ;; This function will return the input string, in sentence case, and with all | ||
189 | ;; punctuation and spaces converted to a hypen. | ||
190 | (string-capitalize | ||
191 | (string-trim-both (irregex-replace/all '(+ (~ alnum)) str "-") | ||
192 | (lambda (c) | ||
193 | (char=? c #\-))))) | ||
194 | |||
195 | (define (unslugify slug) | ||
196 | ;;; Convert a SLUG back into a normal string as best as possible. | ||
197 | ;; Because information is lost in slugification, it's impossible to be sure | ||
198 | ;; that the result of this procedure is totally accurate. That is, | ||
199 | ;; slugification is not round-trippable. | ||
200 | (irregex-replace/all '("-") slug " ")) | ||
201 | |||
202 | |||
203 | ;;; Transform source | ||
204 | |||
205 | (define (transform source . transformers) | ||
206 | ;;; Transform SOURCE to html by passing it through a series of TRANSFORMERS. | ||
207 | ;; Each TRANSFORMER should be a one-argument procedure taking and returning a | ||
208 | ;; string. | ||
209 | (let loop ((transformers transformers) | ||
210 | (output source)) | ||
211 | (if (null? transformers) | ||
212 | output | ||
213 | (loop (cdr transformers) | ||
214 | ((car transformers) output))))) | ||
215 | |||
216 | (define (file-transform origin destination-directory . transformers) | ||
217 | ;;; Transform ORIGIN to a DESTINATION filename using TRANSFORMERS. | ||
218 | ;; Each TRANSFORMER will be called with two arguments: the ORIGIN filaname and | ||
219 | ;; the DESTINATION-DIRECTORY. It should return the transformed filename. | ||
220 | (let loop ((transformers transformers) | ||
221 | (destination origin)) | ||
222 | (if (null? transformers) | ||
223 | destination | ||
224 | (loop (cdr transformers) | ||
225 | ((car transformers) origin destination-directory))))) | ||
226 | |||
227 | |||
228 | ;;; Templates | ||
229 | |||
230 | (define (render template env) | ||
231 | ;;; Render TEMPLATE using ENV. | ||
232 | ;; TEMPLATE is a string with {{placeholders}}; ENV is an alist of key-value | ||
233 | ;; pairs to insert into the TEMPLATE's placeholders. | ||
234 | (string-substitute* template (env->replacements env))) | ||
235 | |||
236 | (define (env->replacements env) | ||
237 | ;;; Convert an ENV alist of the form `((X . Y) ...) to '(("{{X}}" . "Y") ...). | ||
238 | ;; X's are template variables and Y's are the values of those variables. In | ||
239 | ;; the template, both "{{X}}" and "{{ X }}" will be replaced. | ||
240 | ;; If Y is a thunk, call it. | ||
241 | (let loop ((env env) | ||
242 | (res '())) | ||
243 | (if (null? env) | ||
244 | res | ||
245 | (let* ((this (car env)) | ||
246 | (rest (cdr env)) | ||
247 | (key (->string (car this))) | ||
248 | (val (if (procedure? (cdr this)) | ||
249 | ((cdr this)) | ||
250 | (->string (cdr this))))) | ||
251 | (loop (cdr env) | ||
252 | (append (list (cons (string-append "{{" key "}}") val) | ||
253 | (cons (string-append "{{ " key " }}") val)) | ||
254 | env)))))) | ||
255 | |||
256 | (define (render-page template page) | ||
257 | ;;; Render PAGE with its metadata using TEMPLATE. | ||
258 | (render template `((title . ,(page-meta-ref 'title page)) | ||
259 | (body . ,(page-body page)) | ||
260 | (last_updated ,(page-meta-ref 'last-updated page)) | ||
261 | ;; TODO: backlinks and what-not | ||
262 | ))) | ||
263 | |||
264 | |||
265 | ;;; Wikify | ||
266 | |||
267 | (define (render-wiki wiki) | ||
268 | ;;; Render the files in WIKI to disk. | ||
269 | (create-directory (wiki-destination-dir wiki) #:parents) | ||
270 | (for-each (lambda (page) | ||
271 | (with-output-to-file (page-destination page) | ||
272 | (lambda () | ||
273 | (write-string | ||
274 | (render-page (wiki-page-template wiki) page))))) | ||
275 | (wiki-pages wiki))) | ||
diff --git a/wikme.scm b/wikme.scm index b0a5177..10b1a1a 100644 --- a/wikme.scm +++ b/wikme.scm | |||
@@ -1,52 +1,4 @@ | |||
1 | ;;; wikme.scm --- build a wiki from a folder of markdown --- executable | 1 | (module wikme * |
2 | 2 | (import scheme | |
3 | (import (args) | 3 | (chicken base)) |
4 | (chicken pathname) | 4 | (include "wikme-impl.scm")) |
5 | (chicken process-context) | ||
6 | (chicken port)) | ||
7 | |||
8 | (include "wikme-module") | ||
9 | |||
10 | |||
11 | |||
12 | (define +opts+ | ||
13 | (list (args:make-option | ||
14 | (u base-url) (optional: "URL") | ||
15 | "Base URL for the generated Wiki.") | ||
16 | (args:make-option | ||
17 | (s source) (optional: "DIRECTORY") | ||
18 | "Directory containing source files (default: PWD).") | ||
19 | (args:make-option | ||
20 | (o out) (optional: "DIRECTORY") | ||
21 | "Directory in which to place rendered files (default: PWD/out).") | ||
22 | (args:make-option | ||
23 | (t template) (optional: "FILE") | ||
24 | "Template file for wiki pages (default: PWD/template.html)."))) | ||
25 | |||
26 | |||
27 | |||
28 | (define (usage) | ||
29 | (with-output-to-port (current-error-port) | ||
30 | (lambda () | ||
31 | (print "Usage: " (car (argv)) " [options...]") | ||
32 | (newline) | ||
33 | (print (args:usage +opts+)))) | ||
34 | (exit 1)) | ||
35 | |||
36 | (define (main args) | ||
37 | (receive (options operands) | ||
38 | (args:parse args +opts+) | ||
39 | (render-wiki | ||
40 | (directory->wiki | ||
41 | (or (alist-ref 'source options) | ||
42 | (current-directory)) | ||
43 | #:base-url (or (alist-ref 'base-url options) | ||
44 | "https://www.example.com") | ||
45 | #:destination-directory (or (alist-ref 'out options) | ||
46 | (make-pathname | ||
47 | (current-directory) "out")) | ||
48 | #:page-template (or (alist-ref 'template options) | ||
49 | (make-pathname | ||
50 | (current-directory) "template.html")))))) | ||
51 | |||
52 | (main (command-line-arguments)) | ||