Defined ps-inline as a PS macro in addition to a CL macro.
[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
6291b505 31;;; The next command starts the webserver on the port 8080.
8e198a08 32
6291b505 33(start :port 8080)
8e198a08
MB
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"
6291b505
VS
54 :function (lambda (req ent)
55 (with-http-response (req ent)
56 (with-http-body (req ent)
57 (tutorial1 req ent)))))
8e198a08 58
6291b505 59;;; Browsing "http://localhost:8080/tutorial1" should return an empty
8e198a08
MB
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
6291b505
VS
71 ((:a :href "#" :onclick (ps-inline
72 (alert "Hello World")))
8e198a08
MB
73 "Hello World"))))))
74
6291b505 75;;; Browsing "http://localhost:8080/tutorial1" should return the
8e198a08
MB
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
6291b505
VS
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
99of 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 "~%// ]]>~%"))))
8e198a08
MB
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
6291b505 117 ((:a :href "#" :onclick (ps-inline (greeting-callback)))
8e198a08
MB
118 "Hello World")
119 :br "And maybe this link too." :br
6291b505 120 ((:a :href "#" :onclick (ps-inline (greeting-callback)))
8e198a08
MB
121 "Knock knock")
122 :br "And finally a third link." :br
6291b505 123 ((:a :href "#" :onclick (ps-inline (greeting-callback)))
8e198a08
MB
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
8e198a08
MB
131<html><head><title>ParenScript tutorial: 2nd example</title>
132<script type="text/javascript">
133// <![CDATA[
134function 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/>
145And maybe this link too.<br/>
146<a href="#"
147 onclick="javascript:greetingCallback();">Knock knock</a>
148<br/>
149
150And 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
6291b505
VS
162;;; reload the JavaScript code on each pageview. We will publish the
163;;; tutorial JavaScript under "/tutorial.js".
8e198a08
MB
164
165(defun tutorial1-file (req ent)
166 (declare (ignore req ent))
6291b505
VS
167 (html (:princ
168 (ps (defun greeting-callback ()
169 (alert "Hello World"))))))
8e198a08
MB
170
171(publish :path "/tutorial1.js"
172 :content-type "text/javascript; charset=ISO-8859-1"
6291b505
VS
173 :function (lambda (req ent)
174 (with-http-response (req ent)
175 (with-http-body (req ent)
176 (tutorial1-file req ent)))))
8e198a08
MB
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
6291b505 188 ((:a :href "#" :onclick (ps-inline (greeting-callback)))
8e198a08
MB
189 "Hello World")
190 :br "And maybe this link too." :br
6291b505 191 ((:a :href "#" :onclick (ps-inline (greeting-callback)))
8e198a08
MB
192 "Knock knock")
193 :br "And finally a third link." :br
6291b505 194 ((:a :href "#" :onclick (ps-inline (greeting-callback)))
8e198a08
MB
195 "Hello there"))))))
196
197;;; This will generate the following JavaScript code under
198;;; "/tutorial1.js":
199
200function 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/>
213And maybe this link too.<br/>
214<a href="#" onclick="javascript:greetingCallback();">Knock knock</a>
215<br/>
216
217And 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"
6291b505
VS
242 :function (lambda (req ent)
243 (with-http-response (req ent)
244 (with-http-body (req ent)
245 (slideshow req ent)))))
8e198a08
MB
246
247(publish :path "/slideshow.js"
248 :content-type "text/html"
6291b505
VS
249 :function (lambda (req ent)
250 (with-http-response (req ent)
251 (with-http-body (req ent)
252 (js-slideshow req ent)))))
8e198a08 253
6291b505 254;;; The images are just random files I found on my harddrive. We will
8e198a08
MB
255;;; publish them by hand for now.
256
6291b505
VS
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")
8e198a08
MB
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
a961cbd8
VS
274;;; HTML code issued by ParenScrip is generated using the `PS-HTML'
275;;; construct. In fact, there are two different HTML generators in the
276;;; example below, one is the AllegroServe HTML generator, and the
277;;; other is the ParenScript standard library HTML generator, which
278;;; produces a JavaScript expression which evaluates to an HTML
279;;; string.
8e198a08
MB
280
281(defun slideshow (req ent)
282 (declare (ignore req ent))
283 (html
284 (:html
285 (:head (:title "ParenScript slideshow")
286 ((:script :language "JavaScript"
287 :src "/slideshow.js"))
288 (js-script
289 (defvar *linkornot* 0)
6291b505
VS
290 (defvar photos (array "photo1.jpg"
291 "photo2.jpg"
292 "photo3.jpg"))))
8e198a08
MB
293 (:body (:h1 "ParenScript slideshow")
294 (:body (:h2 "Hello")
295 ((:table :border 0
296 :cellspacing 0
297 :cellpadding 0)
298 (:tr ((:td :width "100%" :colspan 2 :height 22)
299 (:center
300 (js-script
6291b505
VS
301 (let ((img (ps-html
302 ((:img :src (aref photos 0)
303 :name "photoslider"
304 :style (+ "filter:"
305 (lisp (ps (reveal-trans
306 (setf duration 2)
307 (setf transition 23)))))
308 :border 0)))))
8e198a08
MB
309 (document.write
310 (if (= *linkornot* 1)
6291b505
VS
311 (ps-html ((:a :href "#"
312 :onclick (lisp (ps-inline (transport))))
313 img))
8e198a08
MB
314 img)))))))
315 (:tr ((:td :width "50%" :height "21")
316 ((:p :align "left")
317 ((:a :href "#"
6291b505 318 :onclick (ps-inline (backward)
8e198a08
MB
319 (return false)))
320 "Previous Slide")))
321 ((:td :width "50%" :height "21")
322 ((:p :align "right")
323 ((:a :href "#"
6291b505 324 :onclick (ps-inline (forward)
8e198a08
MB
325 (return false)))
326 "Next Slide"))))))))))
327
328;;; `SLIDESHOW' generates the following HTML code (long lines have
a961cbd8 329;;; been broken):
8e198a08
MB
330
331<html><head><title>ParenScript slideshow</title>
332<script language="JavaScript" src="/slideshow.js"></script>
333<script type="text/javascript">
334// <![CDATA[
335var LINKORNOT = 0;
6291b505 336var photos = [ "photo1.jpg", "photo2.jpg", "photo3.jpg" ];
8e198a08
MB
337// ]]>
338</script>
339</head>
340<body><h1>ParenScript slideshow</h1>
341<body><h2>Hello</h2>
342<table border="0" cellspacing="0" cellpadding="0">
343<tr><td width="100%" colspan="2" height="22">
344<center><script type="text/javascript">
345// <![CDATA[
346var img =
347 "<img src=\"" + photos[0]
348 + "\" name=\"photoslider\"
349 style=\"filter:revealTrans(duration=2,transition=23)\"
350 border=\"0\"></img>";
351document.write(LINKORNOT == 1 ?
352 "<a href=\"#\"
353 onclick=\"javascript:transport()\">"
354 + img + "</a>"
355 : img);
356// ]]>
357</script>
358</center>
359</td>
360</tr>
361<tr><td width="50%" height="21"><p align="left">
362<a href="#"
363 onclick="javascript:backward(); return false;">Previous Slide</a>
364
365</p>
366</td>
367<td width="50%" height="21"><p align="right">
368<a href="#"
369 onclick="javascript:forward(); return false;">Next Slide</a>
370</p>
371</td>
372</tr>
373</table>
374</body>
375</body>
376</html>
377
8e198a08 378;;; The actual slideshow application is generated by the function
6291b505 379;;; `JS-SLIDESHOW', which generates a ParenScript file. Symbols are
8e198a08 380;;; converted to JavaScript variables, but the dot "." is left as
6291b505
VS
381;;; is. This enables convenient access to object slots without using
382;;; the `SLOT-VALUE' function all the time. However, when the object
383;;; we are referring to is not a variable, but for example an element
384;;; of an array, we have to revert to `SLOT-VALUE'.
8e198a08
MB
385
386(defun js-slideshow (req ent)
387 (declare (ignore req ent))
6291b505
VS
388 (html
389 (:princ
390 (ps
391 (defvar *preloaded-images* (make-array))
392 (defun preload-images (photos)
393 (dotimes (i photos.length)
394 (setf (aref *preloaded-images* i) (new *Image)
395 (slot-value (aref *preloaded-images* i) 'src)
396 (aref photos i))))
397
398 (defun apply-effect ()
399 (when (and document.all photoslider.filters)
400 (let ((trans photoslider.filters.reveal-trans))
401 (setf (slot-value trans '*Transition)
402 (floor (* (random) 23)))
403 (trans.stop)
404 (trans.apply))))
405
406 (defun play-effect ()
407 (when (and document.all photoslider.filters)
408 (photoslider.filters.reveal-trans.play)))
409
410 (defvar *which* 0)
411
412 (defun keep-track ()
413 (setf window.status
414 (+ "Image " (1+ *which*) " of " photos.length)))
415
416 (defun backward ()
417 (when (> *which* 0)
418 (decf *which*)
419 (apply-effect)
420 (setf document.images.photoslider.src
421 (aref photos *which*))
422 (play-effect)
423 (keep-track)))
424
425 (defun forward ()
426 (when (< *which* (1- photos.length))
427 (incf *which*)
428 (apply-effect)
429 (setf document.images.photoslider.src
430 (aref photos *which*))
431 (play-effect)
432 (keep-track)))
433
434 (defun transport ()
435 (setf window.location (aref photoslink *which*)))))))
8e198a08
MB
436
437;;; `JS-SLIDESHOW' generates the following JavaScript code:
438
439var PRELOADEDIMAGES = new Array();
440function preloadImages(photos) {
441 for (var i = 0; i != photos.length; i = i++) {
442 PRELOADEDIMAGES[i] = new Image;
443 PRELOADEDIMAGES[i].src = photos[i];
444 }
445}
446function applyEffect() {
447 if (document.all && photoslider.filters) {
448 var trans = photoslider.filters.revealTrans;
449 trans.Transition = Math.floor(Math.random() * 23);
450 trans.stop();
451 trans.apply();
452 }
453}
454function playEffect() {
455 if (document.all && photoslider.filters) {
456 photoslider.filters.revealTrans.play();
457 }
458}
459var WHICH = 0;
460function keepTrack() {
461 window.status = "Image " + (WHICH + 1) + " of " +
462 photos.length;
463}
464function backward() {
465 if (WHICH > 0) {
466 --WHICH;
467 applyEffect();
468 document.images.photoslider.src = photos[WHICH];
469 playEffect();
470 keepTrack();
471 }
472}
473function forward() {
474 if (WHICH < photos.length - 1) {
475 ++WHICH;
476 applyEffect();
477 document.images.photoslider.src = photos[WHICH];
478 playEffect();
479 keepTrack();
480 }
481}
482function transport() {
483 window.location = photoslink[WHICH];
484}
485
486;;;# Customizing the slideshow
487
488;;; For now, the slideshow has the path to all the slideshow images
489;;; hardcoded in the HTML code, as well as in the publish
490;;; statements. We now want to customize this by publishing a
491;;; slideshow under a certain path, and giving it a list of image urls
492;;; and pathnames where those images can be found. For this, we will
493;;; create a function `PUBLISH-SLIDESHOW' which takes a prefix as
494;;; argument, as well as a list of image pathnames to be published.
495
496(defun publish-slideshow (prefix images)
497 (let* ((js-url (format nil "~Aslideshow.js" prefix))
498 (html-url (format nil "~Aslideshow" prefix))
499 (image-urls
6291b505
VS
500 (mapcar (lambda (image)
501 (format nil "~A~A.~A" prefix
502 (pathname-name image)
503 (pathname-type image)))
8e198a08
MB
504 images)))
505 (publish :path html-url
506 :content-type "text/html"
6291b505
VS
507 :function (lambda (req ent)
508 (with-http-response (req ent)
509 (with-http-body (req ent)
510 (slideshow2 req ent image-urls)))))
8e198a08
MB
511 (publish :path js-url
512 :content-type "text/html"
6291b505
VS
513 :function (lambda (req ent)
514 (with-http-response (req ent)
515 (with-http-body (req ent)
516 (js-slideshow req ent)))))
517 (map nil (lambda (image url)
518 (publish-file :path url
519 :file image))
8e198a08
MB
520 images image-urls)))
521
522(defun slideshow2 (req ent image-urls)
523 (declare (ignore req ent))
524 (html
525 (:html
526 (:head (:title "ParenScript slideshow")
527 ((:script :language "JavaScript"
528 :src "/slideshow.js"))
529 ((:script :type "text/javascript")
530 (:princ (format nil "~%// <![CDATA[~%"))
6291b505
VS
531 (:princ (ps (defvar *linkornot* 0)))
532 (:princ (ps* `(defvar photos (array ,@image-urls))))
8e198a08
MB
533 (:princ (format nil "~%// ]]>~%"))))
534 (:body (:h1 "ParenScript slideshow")
535 (:body (:h2 "Hello")
536 ((:table :border 0
537 :cellspacing 0
538 :cellpadding 0)
539 (:tr ((:td :width "100%" :colspan 2 :height 22)
540 (:center
541 (js-script
6291b505
VS
542 (let ((img (ps-html
543 ((:img :src (aref photos 0)
544 :name "photoslider"
545 :style (+ "filter:"
546 (lisp (ps (reveal-trans
547 (setf duration 2)
548 (setf transition 23)))))
549 :border 0)))))
8e198a08
MB
550 (document.write
551 (if (= *linkornot* 1)
6291b505
VS
552 (ps-html ((:a :href "#"
553 :onclick (lisp (ps-inline (transport))))
554 img))
8e198a08
MB
555 img)))))))
556 (:tr ((:td :width "50%" :height "21")
557 ((:p :align "left")
558 ((:a :href "#"
6291b505 559 :onclick (ps-inline (backward)
8e198a08
MB
560 (return false)))
561 "Previous Slide")))
562 ((:td :width "50%" :height "21")
563 ((:p :align "right")
564 ((:a :href "#"
6291b505 565 :onclick (ps-inline (forward)
8e198a08
MB
566 (return false)))
567 "Next Slide"))))))))))
568
569;;; We can now publish the same slideshow as before, under the
570;;; "/bknr/" prefix:
571
94a05cdf 572(publish-slideshow "/bknr/"
6291b505 573 `("/home/viper/photo1.jpg" "/home/viper/photo2.jpg" "/home/viper/photo3.jpg"))
8e198a08
MB
574
575;;; That's it, we can now access our customized slideshow under
576
6291b505 577 http://localhost:8080/bknr/slideshow
8e198a08 578