1 ;;;# ParenScript Tutorial
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.
7 ;;;# Setting up the ParenScript environment
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.
15 (asdf:oos
'asdf
:load-op
:aserve
)
17 ; ... lots of compiler output ...
19 (asdf:oos
'asdf
:load-op
:parenscript
)
21 ; ... lots of compiler output ...
23 ;;; The tutorial will be placed in its own package, which we first
26 (defpackage :js-tutorial
27 (:use
:common-lisp
:net.aserve
:net.html.generator
:parenscript
))
29 (in-package :js-tutorial
)
31 ;;; Since we will not be using ParenScript's package system for this
32 ;;; tutorial, disable it for now.
34 (setf *enable-package-system
* nil
)
36 ;;; The next command starts the webserver on the port 8000.
40 ;;; We are now ready to generate the first JavaScript-enabled webpages
41 ;;; using ParenScript.
43 ;;;# A simple embedded example
45 ;;; The first document we will generate is a simple HTML document,
46 ;;; which features a single hyperlink. When clicking the hyperlink, a
47 ;;; JavaScript handler opens a popup alert window with the string
48 ;;; "Hello world". To facilitate the development, we will factor out
49 ;;; the HTML generation to a separate function, and setup a handler
50 ;;; for the url "/tutorial1", which will generate HTTP headers and
51 ;;; call the function `TUTORIAL1'. At first, our function does nothing.
53 (defun tutorial1 (req ent
)
54 (declare (ignore req ent
))
57 (publish :path
"/tutorial1"
58 :content-type
"text/html; charset=ISO-8859-1"
59 :function
#'(lambda (req ent
)
60 (with-http-response (req ent
)
61 (with-http-body (req ent
)
62 (tutorial1 req ent
)))))
64 ;;; Browsing "http://localhost:8000/tutorial1" should return an empty
65 ;;; HTML page. It's now time to fill this rather page with
66 ;;; content. ParenScript features a macro that generates a string that
67 ;;; can be used as an attribute value of HTML nodes.
69 (defun tutorial1 (req ent
)
70 (declare (ignore req ent
))
73 (:head
(:title
"ParenScript tutorial: 1st example"))
74 (:body
(:h1
"ParenScript tutorial: 1st example")
75 (:p
"Please click the link below." :br
76 ((:a
:href
"#" :onclick
(js-inline
77 (alert "Hello World")))
80 ;;; Browsing "http://localhost:8000/tutorial1" should return the
83 <html
><head
><title
>ParenScript tutorial
: 1st example
</title
>
85 <body
><h1
>ParenScript tutorial
: 1st example
</h1
>
86 <p
>Please click the link below.
<br
/>
88 onclick
="javascript:alert("Hello World");">Hello World
</a
>
93 ;;;# Adding an inline ParenScript
95 ;;; Suppose we now want to have a general greeting function. One way
96 ;;; to do this is to add the javascript in a `SCRIPT' element at the
97 ;;; top of the HTML page. This is done using the `JS-SCRIPT' macro
98 ;;; which will generate the necessary XML and comment tricks to
99 ;;; cleanly embed JavaScript. We will redefine our `TUTORIAL1'
100 ;;; function and add a few links:
102 (defun tutorial1 (req ent
)
103 (declare (ignore req ent
))
107 (:title
"ParenScript tutorial: 2nd example")
109 (defun greeting-callback ()
110 (alert "Hello World"))))
112 (:h1
"ParenScript tutorial: 2nd example")
113 (:p
"Please click the link below." :br
114 ((:a
:href
"#" :onclick
(js-inline (greeting-callback)))
116 :br
"And maybe this link too." :br
117 ((:a
:href
"#" :onclick
(js-inline (greeting-callback)))
119 :br
"And finally a third link." :br
120 ((:a
:href
"#" :onclick
(js-inline (greeting-callback)))
123 ;;; This will generate the following HTML page, with the embedded
124 ;;; JavaScript nicely sitting on top. Take note how
125 ;;; `GREETING-CALLBACK' was converted to camelcase, and how the lispy
126 ;;; `DEFUN' was converted to a JavaScript function declaration.
128 <html
><head
><title
>ParenScript tutorial
: 2nd example
</title
>
129 <script type
="text/javascript">
131 function greetingCallback
() {
132 alert
("Hello World");
137 <body
><h1
>ParenScript tutorial
: 2nd example
</h1
>
138 <p
>Please click the link below.
<br
/>
140 onclick
="javascript:greetingCallback();">Hello World
</a
>
142 And maybe this link too.
<br
/>
144 onclick
="javascript:greetingCallback();">Knock knock
</a
>
147 And finally a third link.
<br
/>
149 onclick
="javascript:greetingCallback();">Hello there
</a
>
154 ;;;# Generating a JavaScript file
156 ;;; The best way to integrate ParenScript into a Lisp application is
157 ;;; to generate a JavaScript file from ParenScript code. This file can
158 ;;; be cached by intermediate proxies, and webbrowsers won't have to
159 ;;; reload the javascript code on each pageview. A standalone
160 ;;; JavaScript can be generated using the macro `JS-FILE'. We will
161 ;;; publish the tutorial JavaScript under "/tutorial.js".
163 (defun tutorial1-file (req ent
)
164 (declare (ignore req ent
))
166 (defun greeting-callback ()
167 (alert "Hello World"))))
169 (publish :path
"/tutorial1.js"
170 :content-type
"text/javascript; charset=ISO-8859-1"
171 :function
#'(lambda (req ent
)
172 (with-http-response (req ent
)
173 (with-http-body (req ent
)
174 (tutorial1-file req ent
)))))
176 (defun tutorial1 (req ent
)
177 (declare (ignore req ent
))
181 (:title
"ParenScript tutorial: 3rd example")
182 ((:script
:language
"JavaScript" :src
"/tutorial1.js")))
184 (:h1
"ParenScript tutorial: 3rd example")
185 (:p
"Please click the link below." :br
186 ((:a
:href
"#" :onclick
(js-inline (greeting-callback)))
188 :br
"And maybe this link too." :br
189 ((:a
:href
"#" :onclick
(js-inline (greeting-callback)))
191 :br
"And finally a third link." :br
192 ((:a
:href
"#" :onclick
(js-inline (greeting-callback)))
195 ;;; This will generate the following JavaScript code under
198 function greetingCallback
() {
199 alert
("Hello World");
202 ;;; and the following HTML code:
204 <html
><head
><title
>ParenScript tutorial
: 3rd example
</title
>
205 <script language
="JavaScript" src
="/tutorial1.js"></script
>
207 <body
><h1
>ParenScript tutorial
: 3rd example
</h1
>
208 <p
>Please click the link below.
<br
/>
209 <a href
="#" onclick
="javascript:greetingCallback();">Hello World
</a
>
211 And maybe this link too.
<br
/>
212 <a href
="#" onclick
="javascript:greetingCallback();">Knock knock
</a
>
215 And finally a third link.
<br
/>
216 <a href
="#" onclick
="javascript:greetingCallback();">Hello there
</a
>
221 ;;;# A ParenScript slideshow
223 ;;; While developing ParenScript, I used JavaScript programs from the
224 ;;; web and rewrote them using ParenScript. This is a nice slideshow
227 http
://www.dynamicdrive.com
/dynamicindex14
/dhtmlslide.htm
229 ;;; The slideshow will be accessible under "/slideshow", and will
230 ;;; slide through the images "photo1.png", "photo2.png" and
231 ;;; "photo3.png". The first ParenScript version will be very similar
232 ;;; to the original JavaScript code. The second version will then show
233 ;;; how to integrate data from the Lisp environment into the
234 ;;; ParenScript code, allowing us to customize the slideshow
235 ;;; application by supplying a list of image names. We first setup the
238 (publish :path
"/slideshow"
239 :content-type
"text/html"
240 :function
#'(lambda (req ent
)
241 (with-http-response (req ent
)
242 (with-http-body (req ent
)
243 (slideshow req ent
)))))
245 (publish :path
"/slideshow.js"
246 :content-type
"text/html"
247 :function
#'(lambda (req ent
)
248 (with-http-response (req ent
)
249 (with-http-body (req ent
)
250 (js-slideshow req ent
)))))
252 ;;; The images are just random images I found on my harddrive. We will
253 ;;; publish them by hand for now.
255 (publish-file :path
"/photo1.png"
256 :file
"/home/manuel/bknr-sputnik.png")
257 (publish-file :path
"/photo2.png"
258 :file
"/home/manuel/bknrlogo_red648.png")
259 (publish-file :path
"/photo3.png"
260 :file
"/home/manuel/bknr-sputnik.png")
262 ;;; The function `SLIDESHOW' generates the HTML code for the main
263 ;;; slideshow page. It also features little bits of ParenScript. These
264 ;;; are the callbacks on the links for the slideshow application. In
265 ;;; this special case, the javascript generates the links itself by
266 ;;; using `document.write' in a "SCRIPT" element. Users that don't
267 ;;; have JavaScript enabled won't see anything at all.
269 ;;; `SLIDESHOW' also generates a static array called `PHOTOS' which
270 ;;; holds the links to the photos of the slideshow. This array is
271 ;;; handled by the ParenScript code in "slideshow.js". Note how the
272 ;;; HTML code issued by the JavaScript is generated using the `HTML'
273 ;;; construct. In fact, we have two different HTML generators in the
274 ;;; example below, one is the standard Lisp HTML generator, and the
275 ;;; other is the JavaScript HTML generator, which generates a
276 ;;; JavaScript expression.
278 (defun slideshow (req ent
)
279 (declare (ignore req ent
))
282 (:head
(:title
"ParenScript slideshow")
283 ((:script
:language
"JavaScript"
284 :src
"/slideshow.js"))
286 (defvar *linkornot
* 0)
287 (defvar photos
(array "photo1.png"
290 (:body
(:h1
"ParenScript slideshow")
295 (:tr
((:td
:width
"100%" :colspan
2 :height
22)
300 ((:img
:src
(aref photos
0)
305 (setf transition
23))))
308 (if (= *linkornot
* 1)
310 :onclick
(js-inline (transport)))
313 (:tr
((:td
:width
"50%" :height
"21")
316 :onclick
(js-inline (backward)
319 ((:td
:width
"50%" :height
"21")
322 :onclick
(js-inline (forward)
324 "Next Slide"))))))))))
326 ;;; `SLIDESHOW' generates the following HTML code (long lines have
327 ;;; been broken down):
329 <html
><head
><title
>ParenScript slideshow
</title
>
330 <script language
="JavaScript" src
="/slideshow.js"></script
>
331 <script type
="text/javascript">
334 var photos
= [ "photo1.png", "photo2.png", "photo3.png" ];
338 <body
><h1
>ParenScript slideshow
</h1
>
340 <table border
="0" cellspacing
="0" cellpadding
="0">
341 <tr
><td width
="100%" colspan
="2" height
="22">
342 <center
><script type
="text/javascript">
345 "<img src=\"" + photos
[0]
346 + "\" name=\"photoslider\"
347 style=\"filter:revealTrans(duration=2,transition=23)\"
348 border=\"0\"></img>";
349 document.write(LINKORNOT == 1 ?
351 onclick=\"javascript:transport()\">"
359 <tr><td width="50%" height="21"><p align="left">
361 onclick="javascript:backward(); return false;">Previous Slide</a>
365 <td width="50%" height="21"><p align="right">
367 onclick="javascript:forward(); return false;">Next Slide</a>
376 ;;; The actual slideshow application is generated by the function
377 ;;; `JS-SLIDESHOW', which generates a ParenScript file. The code is
378 ;;; pretty straightforward for a lisp savy person. Symbols are
379 ;;; converted to JavaScript variables, but the dot "." is left as
380 ;;; is. This enables us to access object "slots" without using the
381 ;;; `SLOT-VALUE' function all the time. However, when the object we
382 ;;; are referring to is not a variable, but for example an element of
383 ;;; an array, we have to revert to `SLOT-VALUE'.
385 (defun js-slideshow (req ent)
386 (declare (ignore req ent))
388 (defvar *preloaded-images* (make-array))
389 (defun preload-images (photos)
390 (dotimes (i photos.length)
391 (setf (aref *preloaded-images* i) (new *Image)
392 (slot-value (aref *preloaded-images* i) 'src)
395 (defun apply-effect ()
396 (when (and document.all photoslider.filters)
397 (let ((trans photoslider.filters.reveal-trans))
398 (setf (slot-value trans '*Transition)
399 (floor (* (random) 23)))
403 (defun play-effect ()
404 (when (and document.all photoslider.filters)
405 (photoslider.filters.reveal-trans.play)))
411 (+ "Image " (1+ *which*) " of " photos.length)))
417 (setf document.images.photoslider.src
418 (aref photos *which*))
423 (when (< *which* (1- photos.length))
426 (setf document.images.photoslider.src
427 (aref photos *which*))
432 (setf window.location (aref photoslink *which*)))))
434 ;;; `JS-SLIDESHOW' generates the following JavaScript code:
436 var PRELOADEDIMAGES = new Array();
437 function preloadImages(photos) {
438 for (var i = 0; i != photos.length; i = i++) {
439 PRELOADEDIMAGES[i] = new Image;
440 PRELOADEDIMAGES[i].src = photos[i];
443 function applyEffect() {
444 if (document.all && photoslider.filters) {
445 var trans = photoslider.filters.revealTrans;
446 trans.Transition = Math.floor(Math.random() * 23);
451 function playEffect() {
452 if (document.all && photoslider.filters) {
453 photoslider.filters.revealTrans.play();
457 function keepTrack() {
458 window.status = "Image " + (WHICH + 1) + " of " +
461 function backward() {
465 document.images.photoslider.src = photos[WHICH];
471 if (WHICH < photos.length - 1) {
474 document.images.photoslider.src = photos[WHICH];
479 function transport() {
480 window.location = photoslink[WHICH];
483 ;;;# Customizing the slideshow
485 ;;; For now, the slideshow has the path to all the slideshow images
486 ;;; hardcoded in the HTML code, as well as in the publish
487 ;;; statements. We now want to customize this by publishing a
488 ;;; slideshow under a certain path, and giving it a list of image urls
489 ;;; and pathnames where those images can be found. For this, we will
490 ;;; create a function `PUBLISH-SLIDESHOW' which takes a prefix as
491 ;;; argument, as well as a list of image pathnames to be published.
493 (defun publish-slideshow (prefix images)
494 (let* ((js-url (format nil "~Aslideshow.js" prefix))
495 (html-url (format nil "~Aslideshow" prefix))
497 (mapcar #'(lambda (image)
498 (format nil "~A~A.~A" prefix
499 (pathname-name image)
500 (pathname-type image)))
502 (publish :path html-url
503 :content-type "text/html"
504 :function #'(lambda (req ent)
505 (with-http-response (req ent)
506 (with-http-body (req ent)
507 (slideshow2 req ent image-urls)))))
508 (publish :path js-url
509 :content-type "text/html"
510 :function #'(lambda (req ent)
511 (with-http-response (req ent)
512 (with-http-body (req ent)
513 (js-slideshow req ent)))))
514 (map nil #'(lambda (image url)
515 (publish-file :path url
519 (defun slideshow2 (req ent image-urls)
520 (declare (ignore req ent))
523 (:head (:title "ParenScript slideshow")
524 ((:script :language "JavaScript"
525 :src "/slideshow.js"))
526 ((:script :type "text/javascript")
527 (:princ (format nil "~%// <![CDATA[~%"))
528 (:princ (js (defvar *linkornot* 0)))
529 (:princ (js-to-string `(defvar photos
530 (array ,@image-urls))))
531 (:princ (format nil "~%// ]]>~%"))))
532 (:body (:h1 "ParenScript slideshow")
537 (:tr ((:td :width "100%" :colspan 2 :height 22)
542 ((:img :src (aref photos 0)
547 (setf transition 23))))
550 (if (= *linkornot* 1)
552 :onclick (js-inline (transport)))
555 (:tr ((:td :width "50%" :height "21")
558 :onclick (js-inline (backward)
561 ((:td :width "50%" :height "21")
564 :onclick (js-inline (forward)
566 "Next Slide"))))))))))
568 ;;; We can now publish the same slideshow as before, under the
571 (publish-slideshow "/bknr/"
572 `("/home/manuel/bknr-sputnik.png"
573 "/home/manuel/bknrlogo_red648.png"
574 "/home/manuel/screenshots/screenshot-14.03.2005-11.54.33.png"))
576 ;;; That's it, we can now access our customized slideshow under
578 http://localhost:8000/bknr/slideshow