1 | ;; |
---|
2 | ;; qwiki-sxml - SXML rules and tools for qwiki |
---|
3 | ;; |
---|
4 | ;; Copyright (c) 2009-2017 Peter Bex and Ivan Raikov |
---|
5 | ;; |
---|
6 | ;; Redistribution and use in source and binary forms, with or without |
---|
7 | ;; modification, are permitted provided that the following conditions |
---|
8 | ;; are met: |
---|
9 | ;; |
---|
10 | ;; - Redistributions of source code must retain the above copyright |
---|
11 | ;; notice, this list of conditions and the following disclaimer. |
---|
12 | ;; |
---|
13 | ;; - Redistributions in binary form must reproduce the above |
---|
14 | ;; copyright notice, this list of conditions and the following |
---|
15 | ;; disclaimer in the documentation and/or other materials provided |
---|
16 | ;; with the distribution. |
---|
17 | ;; |
---|
18 | ;; - Neither name of the copyright holders nor the names of its |
---|
19 | ;; contributors may be used to endorse or promote products derived |
---|
20 | ;; from this software without specific prior written permission. |
---|
21 | ;; |
---|
22 | ;; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND THE |
---|
23 | ;; CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, |
---|
24 | ;; INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF |
---|
25 | ;; MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
---|
26 | ;; DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR THE |
---|
27 | ;; CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
---|
28 | ;; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
---|
29 | ;; LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF |
---|
30 | ;; USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED |
---|
31 | ;; AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT |
---|
32 | ;; LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN |
---|
33 | ;; ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE |
---|
34 | ;; POSSIBILITY OF SUCH DAMAGE. |
---|
35 | |
---|
36 | (module qwiki-sxml |
---|
37 | |
---|
38 | (title-for-wiki-page |
---|
39 | qwiki-html-transformation-rules) |
---|
40 | |
---|
41 | (import chicken scheme) |
---|
42 | |
---|
43 | (use posix srfi-1 srfi-13 data-structures extras irregex) |
---|
44 | (use sxml-transforms doctype sxpath colorize html-parser) |
---|
45 | |
---|
46 | ;; Try to extract a meaningful title from the page contents |
---|
47 | ;; Unfortunately, title contents aren't always correct sxml but can |
---|
48 | ;; be a list of strings, so we need to do some massaging of the content. |
---|
49 | ;; This code doesn't work for links (eg, int-link doesn't have the target |
---|
50 | ;; as an attribute but as contents. d'oh!) |
---|
51 | (define (title-for-wiki-page page) |
---|
52 | (and-let* ((section ((if-car-sxpath '(// (section 1))) (cons 'root page))) |
---|
53 | (section-contents (caddr section))) |
---|
54 | (if (string? section-contents) |
---|
55 | section-contents |
---|
56 | (string-concatenate |
---|
57 | (append-map (lambda (x) |
---|
58 | (if (string? x) |
---|
59 | (list x) |
---|
60 | ((sxpath '(// *text*)) x))) |
---|
61 | section-contents))))) |
---|
62 | |
---|
63 | (define (lookup-def k lst . rest) |
---|
64 | (let-optionals rest ((default #f)) |
---|
65 | (alist-ref k lst eq? default))) |
---|
66 | |
---|
67 | (define (make-html-header head-params) |
---|
68 | `(head |
---|
69 | ,@(let ((title (lookup-def 'title head-params))) |
---|
70 | (if title `((title ,title)) '())) |
---|
71 | (meta (@ (http-equiv "Content-Style-Type") (content "text/css"))) |
---|
72 | (meta (@ (http-equiv "Content-Type") |
---|
73 | (content ,(lookup-def 'Content-Type head-params |
---|
74 | "text/html; charset=UTF-8")))) |
---|
75 | ,(let ((style (lookup-def 'style head-params)) |
---|
76 | (print-style (lookup-def 'print-style head-params)) |
---|
77 | (canonical (lookup-def 'canonical head-params))) |
---|
78 | (list (if style `(link (@ (rel "stylesheet") (type "text/css") (href ,style))) '()) |
---|
79 | (if print-style `(link (@ (rel "stylesheet") (type "text/css") |
---|
80 | (media "print") (href ,print-style))) '()) |
---|
81 | (if canonical `(link (@ (rel "canonical") (href ,canonical))) '()))) |
---|
82 | ;; Remove already processed head parameters, inserting only unprocessed ones |
---|
83 | ,@(remove (lambda (param) |
---|
84 | (member (car param) '(title style print-style canonical |
---|
85 | new-file existing-file |
---|
86 | read-only read-write))) |
---|
87 | head-params))) |
---|
88 | |
---|
89 | (define (internal-link r) |
---|
90 | (pre-post-order* |
---|
91 | r |
---|
92 | `((*default* . ,(lambda (tag . elems) elems)) |
---|
93 | (*text* . ,(lambda (trigger str) |
---|
94 | (let ((str (string-downcase str))) |
---|
95 | (fold (lambda (regex/subst str) |
---|
96 | (irregex-replace/all (car regex/subst) str (cdr regex/subst))) |
---|
97 | str |
---|
98 | '(("^[^a-z]+" . "") |
---|
99 | ("[^a-z0-9_ \t-]" . "") |
---|
100 | ("[ \t]+" . "-"))))))))) |
---|
101 | |
---|
102 | (define (qwiki-html-transformation-rules content) |
---|
103 | `(((wiki-page |
---|
104 | *macro* |
---|
105 | . ,(lambda (tag elems) |
---|
106 | `(html:begin . ,elems))) |
---|
107 | |
---|
108 | ;; Maybe this should be done in multiple steps to make it more "hookable" |
---|
109 | (history |
---|
110 | *macro* |
---|
111 | . ,(lambda (tag elements) |
---|
112 | (let* ((items-per-page (car elements)) |
---|
113 | (path (cadr elements)) |
---|
114 | (start-revisions (caddr elements)) |
---|
115 | (all-items (cdddr elements)) |
---|
116 | (items (if (> (length all-items) items-per-page) |
---|
117 | (take all-items items-per-page) |
---|
118 | all-items)) |
---|
119 | (first-item-on-next-page |
---|
120 | (and (> (length all-items) items-per-page) |
---|
121 | (list-ref all-items items-per-page)))) |
---|
122 | `(div |
---|
123 | (h3 "Edit history for page: " ,path) |
---|
124 | (form (@ (method "get") (action "")) |
---|
125 | (input (@ (type "hidden") (name "action") |
---|
126 | (value "diff"))) |
---|
127 | (table |
---|
128 | (tr (th "revision") |
---|
129 | (th "author") |
---|
130 | (th "date") |
---|
131 | (th "description") |
---|
132 | (th "r1") |
---|
133 | (th "r2")) |
---|
134 | ,@(map (lambda (item) |
---|
135 | ;; TODO: The nofollow should really apply |
---|
136 | ;; to the entire page instead of each |
---|
137 | ;; individual link. |
---|
138 | `(tr (td (a (@ (rel "nofollow") |
---|
139 | (href ,(string-append |
---|
140 | "?action=show&rev=" |
---|
141 | (number->string (car item))))) |
---|
142 | ,(car item))) |
---|
143 | (td ,(cadr item)) |
---|
144 | (td ,(time->string (caddr item))) |
---|
145 | (td ,(cadddr item)) |
---|
146 | (td (input (@ (type "radio") |
---|
147 | (name "rev1") |
---|
148 | (value ,(car item))))) |
---|
149 | (td (input (@ (type "radio") |
---|
150 | (name "rev2") |
---|
151 | (value ,(car item))))))) |
---|
152 | items)) |
---|
153 | (input (@ (type "submit") |
---|
154 | (value "show diff between selected revisions") |
---|
155 | (class "diff-selection")))) |
---|
156 | ;; Bleeeeergh |
---|
157 | (div (@ (class "pager")) |
---|
158 | ,@(if (not (null? start-revisions)) |
---|
159 | `((a (@ (href "?action=history" |
---|
160 | ,(string-join |
---|
161 | (map ->string |
---|
162 | (cdr start-revisions)) |
---|
163 | "&rev=" 'prefix)) |
---|
164 | (class "prev-page")) |
---|
165 | "prev page") " ") |
---|
166 | '()) |
---|
167 | ,@(if first-item-on-next-page |
---|
168 | (let ((revs (cons (car first-item-on-next-page) |
---|
169 | start-revisions))) |
---|
170 | `((a (@ (href "?action=history" |
---|
171 | ,(string-join |
---|
172 | (map ->string revs) |
---|
173 | "&rev=" 'prefix)) |
---|
174 | (class "next-page")) |
---|
175 | "next page"))) |
---|
176 | '())))))) |
---|
177 | |
---|
178 | (diff |
---|
179 | *macro* |
---|
180 | . ,(lambda (tag elems) |
---|
181 | ;; The diff-language class is a bit weird here, but |
---|
182 | ;; consistent with what we would emit in a highlight block. |
---|
183 | (let* ((classname "highlight diff-language diff-page") |
---|
184 | (diff (handle-exceptions exn elems |
---|
185 | (map (lambda (s) |
---|
186 | (cdr (html->sxml (html-colorize 'diff s)))) |
---|
187 | elems)))) |
---|
188 | `(pre (@ (class ,classname)) . ,diff)))) |
---|
189 | |
---|
190 | (wiki-content |
---|
191 | *macro* . |
---|
192 | ,(lambda (tag contents) |
---|
193 | `(div (@ (id "content")) . ,contents))) |
---|
194 | |
---|
195 | (tags |
---|
196 | *preorder* . |
---|
197 | ,(lambda (tag page-tags) |
---|
198 | `(ul (@ (class "tags")) |
---|
199 | . ,(map (lambda (tag) `(li ,tag)) |
---|
200 | (string-split (car page-tags)))))) |
---|
201 | |
---|
202 | (highlight |
---|
203 | *macro* |
---|
204 | . ,(lambda (tag elems) |
---|
205 | (let* ((lang (car elems)) |
---|
206 | (classname (conc "highlight " lang "-language")) |
---|
207 | (code (handle-exceptions exn |
---|
208 | (cdr elems) |
---|
209 | (map (lambda (s) |
---|
210 | (cdr (html->sxml (html-colorize lang s)))) |
---|
211 | (cdr elems))))) |
---|
212 | `(pre (@ (class ,classname)) . ,code)))) |
---|
213 | |
---|
214 | (examples |
---|
215 | ((example |
---|
216 | ((init |
---|
217 | *macro* |
---|
218 | . ,(lambda (tag elems) |
---|
219 | `(div (@ (class "init")) (highlight scheme . ,elems)))) |
---|
220 | (expr |
---|
221 | *macro* |
---|
222 | . ,(lambda (tag elems) |
---|
223 | `(div (@ (class "expression")) (highlight scheme . ,elems)))) |
---|
224 | (input |
---|
225 | *macro* |
---|
226 | . ,(lambda (tag elems) |
---|
227 | `(div (@ (class "io input")) (em "input: ") |
---|
228 | (highlight scheme . ,elems)))) |
---|
229 | (output |
---|
230 | *macro* |
---|
231 | . ,(lambda (tag elems) |
---|
232 | `(div (@ (class "io output")) (em "output: ") |
---|
233 | (highlight scheme . ,elems)))) |
---|
234 | (result |
---|
235 | *macro* |
---|
236 | . ,(lambda (tag elems) |
---|
237 | `(div (@ (class "result")) |
---|
238 | (span (@ (class "result-symbol")) " => ") |
---|
239 | (highlight scheme . ,elems))))) ;; Or use "basic lisp" here? |
---|
240 | . ,(lambda (tag elems) |
---|
241 | `(div (@ (class "example")) . ,elems)))) |
---|
242 | . ,(lambda (tag elems) |
---|
243 | `(div (@ (class "examples")) |
---|
244 | (span (@ (class "examples-heading")) "Examples:") . ,elems))) |
---|
245 | |
---|
246 | (page-specific-links |
---|
247 | *macro* |
---|
248 | . ,(lambda (tag elems) |
---|
249 | `(ul (@ (id "page-specific-links")) |
---|
250 | (li ,(if ((if-sxpath '(// new-file)) (cons tag elems)) |
---|
251 | `(span (@ (class "disabled") |
---|
252 | (title "This page doesn't exist yet")) |
---|
253 | "show") |
---|
254 | `(a (@ (href "?action=show")) "show"))) |
---|
255 | (li ,(if ((if-sxpath '(// read-only)) (cons tag elems)) |
---|
256 | `(span (@ (class "disabled") |
---|
257 | (title "This page has been frozen. " |
---|
258 | "Only someone with direct access " |
---|
259 | "to the repository can edit it.")) |
---|
260 | "edit") |
---|
261 | `(a (@ (href "?action=edit") (rel "nofollow")) "edit"))) |
---|
262 | (li ,(if ((if-sxpath '(// new-file)) (cons tag elems)) |
---|
263 | `(span (@ (class "disabled") |
---|
264 | (title "This page doesn't exist yet")) |
---|
265 | "history") |
---|
266 | `(a (@ (href "?action=history")) "history")))))) |
---|
267 | |
---|
268 | (@ *preorder* . ,(lambda (tag elements) (cons tag elements))) |
---|
269 | |
---|
270 | (Header |
---|
271 | *preorder* |
---|
272 | . ,(lambda (tag headers) |
---|
273 | (make-html-header headers))) |
---|
274 | |
---|
275 | (toc ;; Re-scan the content for "section" tags and generate |
---|
276 | *macro* |
---|
277 | . ,(lambda (tag rest) ;; the table of contents |
---|
278 | `(div (@ (id "toc")) |
---|
279 | ,rest |
---|
280 | (ol ,(let find-sections ((content content)) |
---|
281 | (cond |
---|
282 | ((not (pair? content)) '()) |
---|
283 | ((pair? (car content)) |
---|
284 | (append (find-sections (car content)) |
---|
285 | (find-sections (cdr content)))) |
---|
286 | ((eq? (car content) 'section) |
---|
287 | (let* ((level (cadr content)) |
---|
288 | (head-word (caddr content)) |
---|
289 | (href (list "#" (internal-link head-word))) |
---|
290 | (subsections (find-sections (cdddr content)))) |
---|
291 | (cond ((and (integer? level) head-word) |
---|
292 | `((li (a (@ (href (,href))) ,head-word) |
---|
293 | ,@(if (null? subsections) |
---|
294 | '() |
---|
295 | `((ol ,subsections)))))) |
---|
296 | (else |
---|
297 | (error 'html-transformation-rules |
---|
298 | "section elements must be of the form (section level head-word . contents)"))))) |
---|
299 | (else (find-sections (cdr content))))))))) |
---|
300 | |
---|
301 | (section |
---|
302 | *macro* |
---|
303 | . ,(lambda (tag elems) |
---|
304 | (let* ((level (car elems)) |
---|
305 | (head-word (cadr elems)) |
---|
306 | (link (internal-link head-word)) |
---|
307 | (contents (cddr elems))) |
---|
308 | (cond ((and (integer? level) head-word) |
---|
309 | `((a (@ (href ,@(list "#" link))) |
---|
310 | (,(string->symbol (string-append "h" (number->string level))) |
---|
311 | (@ (id ,link)) |
---|
312 | ,head-word)) . ,contents)) |
---|
313 | (else |
---|
314 | (error 'html-transformation-rules |
---|
315 | (conc "section elements must be of the form (section level head-word . contents), got " elems))))))) |
---|
316 | |
---|
317 | (section* |
---|
318 | *macro* |
---|
319 | . ,(lambda (tag elems) |
---|
320 | (let ((level (car elems)) |
---|
321 | (head-word (cadr elems)) |
---|
322 | (contents (cddr elems))) |
---|
323 | (cond ((and (integer? level) head-word) |
---|
324 | `((,(string->symbol (string-append "h" (number->string level))) |
---|
325 | ,head-word ) . ,contents)) |
---|
326 | (else |
---|
327 | (error 'html-transformation-rules |
---|
328 | (conc "section elements must be of the form (section level head-word . contents), got " elems))))))) |
---|
329 | |
---|
330 | (def |
---|
331 | ((sig . ,(lambda (tag types) |
---|
332 | (map (lambda (spec) |
---|
333 | `(span (@ (class ,(conc "definition " (car spec)))) |
---|
334 | (em "[" ,(symbol->string (car spec)) "]") |
---|
335 | " " (tt ,@(cdr spec)) (br))) |
---|
336 | types)))) |
---|
337 | . ,(lambda (tag elems) elems)) |
---|
338 | |
---|
339 | (pre |
---|
340 | . ,(lambda (tag elems) |
---|
341 | `(pre (tt . ,elems)))) |
---|
342 | |
---|
343 | (image-link |
---|
344 | *macro* |
---|
345 | . ,(lambda (tag elems) |
---|
346 | `(img (@ (src ,(car elems)) . ,(if (null? (cdr elems)) |
---|
347 | '() |
---|
348 | `((alt ,(cadr elems)) |
---|
349 | (title ,(cadr elems)))))))) |
---|
350 | |
---|
351 | (int-link |
---|
352 | *macro* |
---|
353 | . ,(lambda (tag elems) |
---|
354 | ;; Normalize links so people can refer to sections by their proper name |
---|
355 | (let* ((parts (string-split (car elems) "#" #t)) |
---|
356 | (nparts (intersperse |
---|
357 | (cons (car parts) (internal-link (cdr parts))) |
---|
358 | "#"))) |
---|
359 | `(a (@ (href ,@nparts) (class "internal")) |
---|
360 | ,(if (null? (cdr elems)) (car elems) (cadr elems)))))) |
---|
361 | |
---|
362 | (link |
---|
363 | *macro* |
---|
364 | . ,(lambda (tag elems) |
---|
365 | `(a (@ (href ,(car elems)) (class "external")) |
---|
366 | ,(if (null? (cdr elems)) (car elems) (cadr elems))))) |
---|
367 | |
---|
368 | ,@alist-conv-rules*) |
---|
369 | |
---|
370 | ((html:begin |
---|
371 | . ,(lambda (tag elems) |
---|
372 | (list xhtml-1.0-strict |
---|
373 | "<html xmlns=\"http://www.w3.org/1999/xhtml\">" |
---|
374 | elems |
---|
375 | "</html>"))) |
---|
376 | |
---|
377 | (verbatim |
---|
378 | *preorder* |
---|
379 | . ,(lambda (tag elems) |
---|
380 | elems)) |
---|
381 | |
---|
382 | ,@universal-conversion-rules*))) |
---|
383 | |
---|
384 | ) |
---|