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