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