Commit | Line | Data |
---|---|---|
8e198a08 MB |
1 | ;;;# ParenScript Tutorial |
2 | ||
3 | ;;; This chapter is a short introductory tutorial to ParenScript. It | |
4 | ;;; hopefully will give you an idea how ParenScript can be used in a | |
5 | ;;; Lisp web application. | |
6 | ||
7 | ;;;# Setting up the ParenScript environment | |
8 | ||
9 | ;;; In this tutorial, we will use the Portable Allegroserve webserver | |
10 | ;;; to serve the tutorial web application. We use the ASDF system to | |
11 | ;;; load both Allegroserve and ParenScript. I assume you have | |
12 | ;;; installed and downloaded Allegroserve and Parenscript, and know | |
13 | ;;; how to setup the central registry for ASDF. | |
14 | ||
15 | (asdf:oos 'asdf:load-op :aserve) | |
16 | ||
17 | ; ... lots of compiler output ... | |
18 | ||
19 | (asdf:oos 'asdf:load-op :parenscript) | |
20 | ||
21 | ; ... lots of compiler output ... | |
22 | ||
23 | ;;; The tutorial will be placed in its own package, which we first | |
24 | ;;; have to define. | |
25 | ||
26 | (defpackage :js-tutorial | |
d5ede101 | 27 | (:use :common-lisp :net.aserve :net.html.generator :parenscript)) |
8e198a08 MB |
28 | |
29 | (in-package :js-tutorial) | |
30 | ||
31 | ;;; The next command starts the webserver on the port 8000. | |
32 | ||
33 | (start :port 8000) | |
34 | ||
35 | ;;; We are now ready to generate the first JavaScript-enabled webpages | |
36 | ;;; using ParenScript. | |
37 | ||
38 | ;;;# A simple embedded example | |
39 | ||
40 | ;;; The first document we will generate is a simple HTML document, | |
41 | ;;; which features a single hyperlink. When clicking the hyperlink, a | |
42 | ;;; JavaScript handler opens a popup alert window with the string | |
43 | ;;; "Hello world". To facilitate the development, we will factor out | |
44 | ;;; the HTML generation to a separate function, and setup a handler | |
45 | ;;; for the url "/tutorial1", which will generate HTTP headers and | |
46 | ;;; call the function `TUTORIAL1'. At first, our function does nothing. | |
47 | ||
48 | (defun tutorial1 (req ent) | |
49 | (declare (ignore req ent)) | |
50 | nil) | |
51 | ||
52 | (publish :path "/tutorial1" | |
53 | :content-type "text/html; charset=ISO-8859-1" | |
54 | :function #'(lambda (req ent) | |
55 | (with-http-response (req ent) | |
56 | (with-http-body (req ent) | |
57 | (tutorial1 req ent))))) | |
58 | ||
59 | ;;; Browsing "http://localhost:8000/tutorial1" should return an empty | |
60 | ;;; HTML page. It's now time to fill this rather page with | |
61 | ;;; content. ParenScript features a macro that generates a string that | |
94a05cdf | 62 | ;;; can be used as an attribute value of HTML nodes. |
8e198a08 MB |
63 | |
64 | (defun tutorial1 (req ent) | |
65 | (declare (ignore req ent)) | |
66 | (html | |
67 | (:html | |
68 | (:head (:title "ParenScript tutorial: 1st example")) | |
69 | (:body (:h1 "ParenScript tutorial: 1st example") | |
70 | (:p "Please click the link below." :br | |
71 | ((:a :href "#" :onclick (js-inline | |
72 | (alert "Hello World"))) | |
73 | "Hello World")))))) | |
74 | ||
75 | ;;; Browsing "http://localhost:8000/tutorial1" should return the | |
76 | ;;; following HTML: | |
77 | ||
8e198a08 MB |
78 | <html><head><title>ParenScript tutorial: 1st example</title> |
79 | </head> | |
80 | <body><h1>ParenScript tutorial: 1st example</h1> | |
81 | <p>Please click the link below.<br/> | |
82 | <a href="#" | |
83 | onclick="javascript:alert("Hello World");">Hello World</a> | |
84 | </p> | |
85 | </body> | |
86 | </html> | |
87 | ||
88 | ;;;# Adding an inline ParenScript | |
89 | ||
90 | ;;; Suppose we now want to have a general greeting function. One way | |
91 | ;;; to do this is to add the javascript in a `SCRIPT' element at the | |
92 | ;;; top of the HTML page. This is done using the `JS-SCRIPT' macro | |
93 | ;;; which will generate the necessary XML and comment tricks to | |
94 | ;;; cleanly embed JavaScript. We will redefine our `TUTORIAL1' | |
95 | ;;; function and add a few links: | |
96 | ||
97 | (defun tutorial1 (req ent) | |
98 | (declare (ignore req ent)) | |
99 | (html | |
100 | (:html | |
101 | (:head | |
102 | (:title "ParenScript tutorial: 2nd example") | |
103 | (js-script | |
104 | (defun greeting-callback () | |
105 | (alert "Hello World")))) | |
106 | (:body | |
107 | (:h1 "ParenScript tutorial: 2nd example") | |
108 | (:p "Please click the link below." :br | |
109 | ((:a :href "#" :onclick (js-inline (greeting-callback))) | |
110 | "Hello World") | |
111 | :br "And maybe this link too." :br | |
112 | ((:a :href "#" :onclick (js-inline (greeting-callback))) | |
113 | "Knock knock") | |
114 | :br "And finally a third link." :br | |
115 | ((:a :href "#" :onclick (js-inline (greeting-callback))) | |
116 | "Hello there")))))) | |
117 | ||
118 | ;;; This will generate the following HTML page, with the embedded | |
119 | ;;; JavaScript nicely sitting on top. Take note how | |
120 | ;;; `GREETING-CALLBACK' was converted to camelcase, and how the lispy | |
121 | ;;; `DEFUN' was converted to a JavaScript function declaration. | |
122 | ||
8e198a08 MB |
123 | <html><head><title>ParenScript tutorial: 2nd example</title> |
124 | <script type="text/javascript"> | |
125 | // <![CDATA[ | |
126 | function greetingCallback() { | |
127 | alert("Hello World"); | |
128 | } | |
129 | // ]]> | |
130 | </script> | |
131 | </head> | |
132 | <body><h1>ParenScript tutorial: 2nd example</h1> | |
133 | <p>Please click the link below.<br/> | |
134 | <a href="#" | |
135 | onclick="javascript:greetingCallback();">Hello World</a> | |
136 | <br/> | |
137 | And maybe this link too.<br/> | |
138 | <a href="#" | |
139 | onclick="javascript:greetingCallback();">Knock knock</a> | |
140 | <br/> | |
141 | ||
142 | And finally a third link.<br/> | |
143 | <a href="#" | |
144 | onclick="javascript:greetingCallback();">Hello there</a> | |
145 | </p> | |
146 | </body> | |
147 | </html> | |
148 | ||
149 | ;;;# Generating a JavaScript file | |
150 | ||
151 | ;;; The best way to integrate ParenScript into a Lisp application is | |
152 | ;;; to generate a JavaScript file from ParenScript code. This file can | |
153 | ;;; be cached by intermediate proxies, and webbrowsers won't have to | |
154 | ;;; reload the javascript code on each pageview. A standalone | |
155 | ;;; JavaScript can be generated using the macro `JS-FILE'. We will | |
156 | ;;; publish the tutorial JavaScript under "/tutorial.js". | |
157 | ||
158 | (defun tutorial1-file (req ent) | |
159 | (declare (ignore req ent)) | |
160 | (js-file | |
161 | (defun greeting-callback () | |
162 | (alert "Hello World")))) | |
163 | ||
164 | (publish :path "/tutorial1.js" | |
165 | :content-type "text/javascript; charset=ISO-8859-1" | |
166 | :function #'(lambda (req ent) | |
167 | (with-http-response (req ent) | |
168 | (with-http-body (req ent) | |
169 | (tutorial1-file req ent))))) | |
170 | ||
171 | (defun tutorial1 (req ent) | |
172 | (declare (ignore req ent)) | |
173 | (html | |
174 | (:html | |
175 | (:head | |
176 | (:title "ParenScript tutorial: 3rd example") | |
177 | ((:script :language "JavaScript" :src "/tutorial1.js"))) | |
178 | (:body | |
179 | (:h1 "ParenScript tutorial: 3rd example") | |
180 | (:p "Please click the link below." :br | |
181 | ((:a :href "#" :onclick (js-inline (greeting-callback))) | |
182 | "Hello World") | |
183 | :br "And maybe this link too." :br | |
184 | ((:a :href "#" :onclick (js-inline (greeting-callback))) | |
185 | "Knock knock") | |
186 | :br "And finally a third link." :br | |
187 | ((:a :href "#" :onclick (js-inline (greeting-callback))) | |
188 | "Hello there")))))) | |
189 | ||
190 | ;;; This will generate the following JavaScript code under | |
191 | ;;; "/tutorial1.js": | |
192 | ||
193 | function greetingCallback() { | |
194 | alert("Hello World"); | |
195 | } | |
196 | ||
197 | ;;; and the following HTML code: | |
198 | ||
199 | <html><head><title>ParenScript tutorial: 3rd example</title> | |
200 | <script language="JavaScript" src="/tutorial1.js"></script> | |
201 | </head> | |
202 | <body><h1>ParenScript tutorial: 3rd example</h1> | |
203 | <p>Please click the link below.<br/> | |
204 | <a href="#" onclick="javascript:greetingCallback();">Hello World</a> | |
205 | <br/> | |
206 | And maybe this link too.<br/> | |
207 | <a href="#" onclick="javascript:greetingCallback();">Knock knock</a> | |
208 | <br/> | |
209 | ||
210 | And finally a third link.<br/> | |
211 | <a href="#" onclick="javascript:greetingCallback();">Hello there</a> | |
212 | </p> | |
213 | </body> | |
214 | </html> | |
215 | ||
216 | ;;;# A ParenScript slideshow | |
217 | ||
218 | ;;; While developing ParenScript, I used JavaScript programs from the | |
219 | ;;; web and rewrote them using ParenScript. This is a nice slideshow | |
220 | ;;; example from | |
221 | ||
222 | http://www.dynamicdrive.com/dynamicindex14/dhtmlslide.htm | |
223 | ||
224 | ;;; The slideshow will be accessible under "/slideshow", and will | |
225 | ;;; slide through the images "photo1.png", "photo2.png" and | |
226 | ;;; "photo3.png". The first ParenScript version will be very similar | |
227 | ;;; to the original JavaScript code. The second version will then show | |
228 | ;;; how to integrate data from the Lisp environment into the | |
229 | ;;; ParenScript code, allowing us to customize the slideshow | |
230 | ;;; application by supplying a list of image names. We first setup the | |
231 | ;;; slideshow path. | |
232 | ||
233 | (publish :path "/slideshow" | |
234 | :content-type "text/html" | |
235 | :function #'(lambda (req ent) | |
236 | (with-http-response (req ent) | |
237 | (with-http-body (req ent) | |
238 | (slideshow req ent))))) | |
239 | ||
240 | (publish :path "/slideshow.js" | |
241 | :content-type "text/html" | |
242 | :function #'(lambda (req ent) | |
243 | (with-http-response (req ent) | |
244 | (with-http-body (req ent) | |
245 | (js-slideshow req ent))))) | |
246 | ||
247 | ;;; The images are just random images I found on my harddrive. We will | |
248 | ;;; publish them by hand for now. | |
249 | ||
250 | (publish-file :path "/photo1.png" | |
251 | :file "/home/manuel/bknr-sputnik.png") | |
252 | (publish-file :path "/photo2.png" | |
253 | :file "/home/manuel/bknrlogo_red648.png") | |
254 | (publish-file :path "/photo3.png" | |
255 | :file "/home/manuel/bknr-sputnik.png") | |
256 | ||
257 | ;;; The function `SLIDESHOW' generates the HTML code for the main | |
258 | ;;; slideshow page. It also features little bits of ParenScript. These | |
259 | ;;; are the callbacks on the links for the slideshow application. In | |
260 | ;;; this special case, the javascript generates the links itself by | |
261 | ;;; using `document.write' in a "SCRIPT" element. Users that don't | |
262 | ;;; have JavaScript enabled won't see anything at all. | |
263 | ;;; | |
264 | ;;; `SLIDESHOW' also generates a static array called `PHOTOS' which | |
265 | ;;; holds the links to the photos of the slideshow. This array is | |
266 | ;;; handled by the ParenScript code in "slideshow.js". Note how the | |
267 | ;;; HTML code issued by the JavaScript is generated using the `HTML' | |
268 | ;;; construct. In fact, we have two different HTML generators in the | |
269 | ;;; example below, one is the standard Lisp HTML generator, and the | |
270 | ;;; other is the JavaScript HTML generator, which generates a | |
271 | ;;; JavaScript expression. | |
272 | ||
273 | (defun slideshow (req ent) | |
274 | (declare (ignore req ent)) | |
275 | (html | |
276 | (:html | |
277 | (:head (:title "ParenScript slideshow") | |
278 | ((:script :language "JavaScript" | |
279 | :src "/slideshow.js")) | |
280 | (js-script | |
281 | (defvar *linkornot* 0) | |
282 | (defvar photos (array "photo1.png" | |
283 | "photo2.png" | |
284 | "photo3.png")))) | |
285 | (:body (:h1 "ParenScript slideshow") | |
286 | (:body (:h2 "Hello") | |
287 | ((:table :border 0 | |
288 | :cellspacing 0 | |
289 | :cellpadding 0) | |
290 | (:tr ((:td :width "100%" :colspan 2 :height 22) | |
291 | (:center | |
292 | (js-script | |
293 | (let ((img | |
294 | (html | |
295 | ((:img :src (aref photos 0) | |
296 | :name "photoslider" | |
297 | :style ( + "filter:" | |
298 | (js (reveal-trans | |
299 | (setf duration 2) | |
300 | (setf transition 23)))) | |
301 | :border 0))))) | |
302 | (document.write | |
303 | (if (= *linkornot* 1) | |
304 | (html ((:a :href "#" | |
305 | :onclick (js-inline (transport))) | |
306 | img)) | |
307 | img))))))) | |
308 | (:tr ((:td :width "50%" :height "21") | |
309 | ((:p :align "left") | |
310 | ((:a :href "#" | |
311 | :onclick (js-inline (backward) | |
312 | (return false))) | |
313 | "Previous Slide"))) | |
314 | ((:td :width "50%" :height "21") | |
315 | ((:p :align "right") | |
316 | ((:a :href "#" | |
317 | :onclick (js-inline (forward) | |
318 | (return false))) | |
319 | "Next Slide")))))))))) | |
320 | ||
321 | ;;; `SLIDESHOW' generates the following HTML code (long lines have | |
322 | ;;; been broken down): | |
323 | ||
324 | <html><head><title>ParenScript slideshow</title> | |
325 | <script language="JavaScript" src="/slideshow.js"></script> | |
326 | <script type="text/javascript"> | |
327 | // <![CDATA[ | |
328 | var LINKORNOT = 0; | |
329 | var photos = [ "photo1.png", "photo2.png", "photo3.png" ]; | |
330 | // ]]> | |
331 | </script> | |
332 | </head> | |
333 | <body><h1>ParenScript slideshow</h1> | |
334 | <body><h2>Hello</h2> | |
335 | <table border="0" cellspacing="0" cellpadding="0"> | |
336 | <tr><td width="100%" colspan="2" height="22"> | |
337 | <center><script type="text/javascript"> | |
338 | // <![CDATA[ | |
339 | var img = | |
340 | "<img src=\"" + photos[0] | |
341 | + "\" name=\"photoslider\" | |
342 | style=\"filter:revealTrans(duration=2,transition=23)\" | |
343 | border=\"0\"></img>"; | |
344 | document.write(LINKORNOT == 1 ? | |
345 | "<a href=\"#\" | |
346 | onclick=\"javascript:transport()\">" | |
347 | + img + "</a>" | |
348 | : img); | |
349 | // ]]> | |
350 | </script> | |
351 | </center> | |
352 | </td> | |
353 | </tr> | |
354 | <tr><td width="50%" height="21"><p align="left"> | |
355 | <a href="#" | |
356 | onclick="javascript:backward(); return false;">Previous Slide</a> | |
357 | ||
358 | </p> | |
359 | </td> | |
360 | <td width="50%" height="21"><p align="right"> | |
361 | <a href="#" | |
362 | onclick="javascript:forward(); return false;">Next Slide</a> | |
363 | </p> | |
364 | </td> | |
365 | </tr> | |
366 | </table> | |
367 | </body> | |
368 | </body> | |
369 | </html> | |
370 | ||
8e198a08 MB |
371 | ;;; The actual slideshow application is generated by the function |
372 | ;;; `JS-SLIDESHOW', which generates a ParenScript file. The code is | |
373 | ;;; pretty straightforward for a lisp savy person. Symbols are | |
374 | ;;; converted to JavaScript variables, but the dot "." is left as | |
375 | ;;; is. This enables us to access object "slots" without using the | |
376 | ;;; `SLOT-VALUE' function all the time. However, when the object we | |
377 | ;;; are referring to is not a variable, but for example an element of | |
378 | ;;; an array, we have to revert to `SLOT-VALUE'. | |
379 | ||
380 | (defun js-slideshow (req ent) | |
381 | (declare (ignore req ent)) | |
382 | (js-file | |
383 | (defvar *preloaded-images* (make-array)) | |
384 | (defun preload-images (photos) | |
385 | (dotimes (i photos.length) | |
386 | (setf (aref *preloaded-images* i) (new *Image) | |
387 | (slot-value (aref *preloaded-images* i) 'src) | |
388 | (aref photos i)))) | |
551080b7 | 389 | |
8e198a08 MB |
390 | (defun apply-effect () |
391 | (when (and document.all photoslider.filters) | |
392 | (let ((trans photoslider.filters.reveal-trans)) | |
393 | (setf (slot-value trans '*Transition) | |
394 | (floor (* (random) 23))) | |
395 | (trans.stop) | |
396 | (trans.apply)))) | |
551080b7 | 397 | |
8e198a08 MB |
398 | (defun play-effect () |
399 | (when (and document.all photoslider.filters) | |
400 | (photoslider.filters.reveal-trans.play))) | |
401 | ||
402 | (defvar *which* 0) | |
551080b7 | 403 | |
8e198a08 MB |
404 | (defun keep-track () |
405 | (setf window.status | |
406 | (+ "Image " (1+ *which*) " of " photos.length))) | |
551080b7 | 407 | |
8e198a08 MB |
408 | (defun backward () |
409 | (when (> *which* 0) | |
410 | (decf *which*) | |
411 | (apply-effect) | |
412 | (setf document.images.photoslider.src | |
413 | (aref photos *which*)) | |
414 | (play-effect) | |
415 | (keep-track))) | |
551080b7 | 416 | |
8e198a08 MB |
417 | (defun forward () |
418 | (when (< *which* (1- photos.length)) | |
419 | (incf *which*) | |
420 | (apply-effect) | |
421 | (setf document.images.photoslider.src | |
422 | (aref photos *which*)) | |
423 | (play-effect) | |
424 | (keep-track))) | |
551080b7 | 425 | |
8e198a08 MB |
426 | (defun transport () |
427 | (setf window.location (aref photoslink *which*))))) | |
428 | ||
429 | ;;; `JS-SLIDESHOW' generates the following JavaScript code: | |
430 | ||
431 | var PRELOADEDIMAGES = new Array(); | |
432 | function preloadImages(photos) { | |
433 | for (var i = 0; i != photos.length; i = i++) { | |
434 | PRELOADEDIMAGES[i] = new Image; | |
435 | PRELOADEDIMAGES[i].src = photos[i]; | |
436 | } | |
437 | } | |
438 | function applyEffect() { | |
439 | if (document.all && photoslider.filters) { | |
440 | var trans = photoslider.filters.revealTrans; | |
441 | trans.Transition = Math.floor(Math.random() * 23); | |
442 | trans.stop(); | |
443 | trans.apply(); | |
444 | } | |
445 | } | |
446 | function playEffect() { | |
447 | if (document.all && photoslider.filters) { | |
448 | photoslider.filters.revealTrans.play(); | |
449 | } | |
450 | } | |
451 | var WHICH = 0; | |
452 | function keepTrack() { | |
453 | window.status = "Image " + (WHICH + 1) + " of " + | |
454 | photos.length; | |
455 | } | |
456 | function backward() { | |
457 | if (WHICH > 0) { | |
458 | --WHICH; | |
459 | applyEffect(); | |
460 | document.images.photoslider.src = photos[WHICH]; | |
461 | playEffect(); | |
462 | keepTrack(); | |
463 | } | |
464 | } | |
465 | function forward() { | |
466 | if (WHICH < photos.length - 1) { | |
467 | ++WHICH; | |
468 | applyEffect(); | |
469 | document.images.photoslider.src = photos[WHICH]; | |
470 | playEffect(); | |
471 | keepTrack(); | |
472 | } | |
473 | } | |
474 | function transport() { | |
475 | window.location = photoslink[WHICH]; | |
476 | } | |
477 | ||
478 | ;;;# Customizing the slideshow | |
479 | ||
480 | ;;; For now, the slideshow has the path to all the slideshow images | |
481 | ;;; hardcoded in the HTML code, as well as in the publish | |
482 | ;;; statements. We now want to customize this by publishing a | |
483 | ;;; slideshow under a certain path, and giving it a list of image urls | |
484 | ;;; and pathnames where those images can be found. For this, we will | |
485 | ;;; create a function `PUBLISH-SLIDESHOW' which takes a prefix as | |
486 | ;;; argument, as well as a list of image pathnames to be published. | |
487 | ||
488 | (defun publish-slideshow (prefix images) | |
489 | (let* ((js-url (format nil "~Aslideshow.js" prefix)) | |
490 | (html-url (format nil "~Aslideshow" prefix)) | |
491 | (image-urls | |
492 | (mapcar #'(lambda (image) | |
493 | (format nil "~A~A.~A" prefix | |
494 | (pathname-name image) | |
495 | (pathname-type image))) | |
496 | images))) | |
497 | (publish :path html-url | |
498 | :content-type "text/html" | |
499 | :function #'(lambda (req ent) | |
500 | (with-http-response (req ent) | |
501 | (with-http-body (req ent) | |
502 | (slideshow2 req ent image-urls))))) | |
503 | (publish :path js-url | |
504 | :content-type "text/html" | |
505 | :function #'(lambda (req ent) | |
506 | (with-http-response (req ent) | |
507 | (with-http-body (req ent) | |
508 | (js-slideshow req ent))))) | |
509 | (map nil #'(lambda (image url) | |
510 | (publish-file :path url | |
511 | :file image)) | |
512 | images image-urls))) | |
513 | ||
514 | (defun slideshow2 (req ent image-urls) | |
515 | (declare (ignore req ent)) | |
516 | (html | |
517 | (:html | |
518 | (:head (:title "ParenScript slideshow") | |
519 | ((:script :language "JavaScript" | |
520 | :src "/slideshow.js")) | |
521 | ((:script :type "text/javascript") | |
522 | (:princ (format nil "~%// <![CDATA[~%")) | |
523 | (:princ (js (defvar *linkornot* 0))) | |
524 | (:princ (js-to-string `(defvar photos | |
525 | (array ,@image-urls)))) | |
526 | (:princ (format nil "~%// ]]>~%")))) | |
527 | (:body (:h1 "ParenScript slideshow") | |
528 | (:body (:h2 "Hello") | |
529 | ((:table :border 0 | |
530 | :cellspacing 0 | |
531 | :cellpadding 0) | |
532 | (:tr ((:td :width "100%" :colspan 2 :height 22) | |
533 | (:center | |
534 | (js-script | |
535 | (let ((img | |
536 | (html | |
537 | ((:img :src (aref photos 0) | |
538 | :name "photoslider" | |
539 | :style ( + "filter:" | |
540 | (js (reveal-trans | |
541 | (setf duration 2) | |
542 | (setf transition 23)))) | |
543 | :border 0))))) | |
544 | (document.write | |
545 | (if (= *linkornot* 1) | |
546 | (html ((:a :href "#" | |
547 | :onclick (js-inline (transport))) | |
548 | img)) | |
549 | img))))))) | |
550 | (:tr ((:td :width "50%" :height "21") | |
551 | ((:p :align "left") | |
552 | ((:a :href "#" | |
553 | :onclick (js-inline (backward) | |
554 | (return false))) | |
555 | "Previous Slide"))) | |
556 | ((:td :width "50%" :height "21") | |
557 | ((:p :align "right") | |
558 | ((:a :href "#" | |
559 | :onclick (js-inline (forward) | |
560 | (return false))) | |
561 | "Next Slide")))))))))) | |
562 | ||
563 | ;;; We can now publish the same slideshow as before, under the | |
564 | ;;; "/bknr/" prefix: | |
565 | ||
94a05cdf | 566 | (publish-slideshow "/bknr/" |
8e198a08 MB |
567 | `("/home/manuel/bknr-sputnik.png" |
568 | "/home/manuel/bknrlogo_red648.png" | |
569 | "/home/manuel/screenshots/screenshot-14.03.2005-11.54.33.png")) | |
570 | ||
571 | ;;; That's it, we can now access our customized slideshow under | |
572 | ||
573 | http://localhost:8000/bknr/slideshow | |
574 |