Removed mention of old package system from tutorial.lisp.
[clinton/parenscript.git] / docs / tutorial.lisp
CommitLineData
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(&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;;; 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[
126function 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/>
137And maybe this link too.<br/>
138<a href="#"
139 onclick="javascript:greetingCallback();">Knock knock</a>
140<br/>
141
142And 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
193function 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/>
206And maybe this link too.<br/>
207<a href="#" onclick="javascript:greetingCallback();">Knock knock</a>
208<br/>
209
210And 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[
328var LINKORNOT = 0;
329var 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[
339var img =
340 "<img src=\"" + photos[0]
341 + "\" name=\"photoslider\"
342 style=\"filter:revealTrans(duration=2,transition=23)\"
343 border=\"0\"></img>";
344document.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
431var PRELOADEDIMAGES = new Array();
432function 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}
438function 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}
446function playEffect() {
447 if (document.all && photoslider.filters) {
448 photoslider.filters.revealTrans.play();
449 }
450}
451var WHICH = 0;
452function keepTrack() {
453 window.status = "Image " + (WHICH + 1) + " of " +
454 photos.length;
455}
456function backward() {
457 if (WHICH > 0) {
458 --WHICH;
459 applyEffect();
460 document.images.photoslider.src = photos[WHICH];
461 playEffect();
462 keepTrack();
463 }
464}
465function forward() {
466 if (WHICH < photos.length - 1) {
467 ++WHICH;
468 applyEffect();
469 document.images.photoslider.src = photos[WHICH];
470 playEffect();
471 keepTrack();
472 }
473}
474function 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