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
:js
))
29 (in-package :js-tutorial
)
31 ;;; The next command starts the webserver on the port 8000.
35 ;;; We are now ready to generate the first JavaScript-enabled webpages
36 ;;; using ParenScript.
38 ;;;# A simple embedded example
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.
48 (defun tutorial1 (req ent
)
49 (declare (ignore req ent
))
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
)))))
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.
64 (defun tutorial1 (req ent
)
65 (declare (ignore req ent
))
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")))
75 ;;; Browsing "http://localhost:8000/tutorial1" should return the
78 <html
><head
><title
>ParenScript tutorial
: 1st example
</title
>
80 <body
><h1
>ParenScript tutorial
: 1st example
</h1
>
81 <p
>Please click the link below.
<br
/>
83 onclick
="javascript:alert("Hello World");">Hello World
</a
>
88 ;;;# Adding an inline ParenScript
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:
97 (defun tutorial1 (req ent
)
98 (declare (ignore req ent
))
102 (:title
"ParenScript tutorial: 2nd example")
104 (defun greeting-callback ()
105 (alert "Hello World"))))
107 (:h1
"ParenScript tutorial: 2nd example")
108 (:p
"Please click the link below." :br
109 ((:a
:href
"#" :onclick
(js-inline (greeting-callback)))
111 :br
"And maybe this link too." :br
112 ((:a
:href
"#" :onclick
(js-inline (greeting-callback)))
114 :br
"And finally a third link." :br
115 ((:a
:href
"#" :onclick
(js-inline (greeting-callback)))
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.
123 <html
><head
><title
>ParenScript tutorial
: 2nd example
</title
>
124 <script type
="text/javascript">
126 function greetingCallback
() {
127 alert
("Hello World");
132 <body
><h1
>ParenScript tutorial
: 2nd example
</h1
>
133 <p
>Please click the link below.
<br
/>
135 onclick
="javascript:greetingCallback();">Hello World
</a
>
137 And maybe this link too.
<br
/>
139 onclick
="javascript:greetingCallback();">Knock knock
</a
>
142 And finally a third link.
<br
/>
144 onclick
="javascript:greetingCallback();">Hello there
</a
>
149 ;;;# Generating a JavaScript file
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".
158 (defun tutorial1-file (req ent
)
159 (declare (ignore req ent
))
161 (defun greeting-callback ()
162 (alert "Hello World"))))
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
)))))
171 (defun tutorial1 (req ent
)
172 (declare (ignore req ent
))
176 (:title
"ParenScript tutorial: 3rd example")
177 ((:script
:language
"JavaScript" :src
"/tutorial1.js")))
179 (:h1
"ParenScript tutorial: 3rd example")
180 (:p
"Please click the link below." :br
181 ((:a
:href
"#" :onclick
(js-inline (greeting-callback)))
183 :br
"And maybe this link too." :br
184 ((:a
:href
"#" :onclick
(js-inline (greeting-callback)))
186 :br
"And finally a third link." :br
187 ((:a
:href
"#" :onclick
(js-inline (greeting-callback)))
190 ;;; This will generate the following JavaScript code under
193 function greetingCallback
() {
194 alert
("Hello World");
197 ;;; and the following HTML code:
199 <html
><head
><title
>ParenScript tutorial
: 3rd example
</title
>
200 <script language
="JavaScript" src
="/tutorial1.js"></script
>
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
>
206 And maybe this link too.
<br
/>
207 <a href
="#" onclick
="javascript:greetingCallback();">Knock knock
</a
>
210 And finally a third link.
<br
/>
211 <a href
="#" onclick
="javascript:greetingCallback();">Hello there
</a
>
216 ;;;# A ParenScript slideshow
218 ;;; While developing ParenScript, I used JavaScript programs from the
219 ;;; web and rewrote them using ParenScript. This is a nice slideshow
222 http
://www.dynamicdrive.com
/dynamicindex14
/dhtmlslide.htm
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
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
)))))
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
)))))
247 ;;; The images are just random images I found on my harddrive. We will
248 ;;; publish them by hand for now.
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")
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.
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.
273 (defun slideshow (req ent
)
274 (declare (ignore req ent
))
277 (:head
(:title
"ParenScript slideshow")
278 ((:script
:language
"JavaScript"
279 :src
"/slideshow.js"))
281 (defvar *linkornot
* 0)
282 (defvar photos
(array "photo1.png"
285 (:body
(:h1
"ParenScript slideshow")
290 (:tr
((:td
:width
"100%" :colspan
2 :height
22)
295 ((:img
:src
(aref photos
0)
300 (setf transition
23))))
303 (if (= *linkornot
* 1)
305 :onclick
(js-inline (transport)))
308 (:tr
((:td
:width
"50%" :height
"21")
311 :onclick
(js-inline (backward)
314 ((:td
:width
"50%" :height
"21")
317 :onclick
(js-inline (forward)
319 "Next Slide"))))))))))
321 ;;; `SLIDESHOW' generates the following HTML code (long lines have
322 ;;; been broken down):
324 <html
><head
><title
>ParenScript slideshow
</title
>
325 <script language
="JavaScript" src
="/slideshow.js"></script
>
326 <script type
="text/javascript">
329 var photos
= [ "photo1.png", "photo2.png", "photo3.png" ];
333 <body
><h1
>ParenScript slideshow
</h1
>
335 <table border
="0" cellspacing
="0" cellpadding
="0">
336 <tr
><td width
="100%" colspan
="2" height
="22">
337 <center
><script type
="text/javascript">
340 "<img src=\"" + photos
[0]
341 + "\" name=\"photoslider\"
342 style=\"filter:revealTrans(duration=2,transition=23)\"
343 border=\"0\"></img>";
344 document.write(LINKORNOT == 1 ?
346 onclick=\"javascript:transport()\">"
354 <tr><td width="50%" height="21"><p align="left">
356 onclick="javascript:backward(); return false;">Previous Slide</a>
360 <td width="50%" height="21"><p align="right">
362 onclick="javascript:forward(); return false;">Next Slide</a>
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'.
380 (defun js-slideshow (req ent)
381 (declare (ignore req ent))
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)
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)))
398 (defun play-effect ()
399 (when (and document.all photoslider.filters)
400 (photoslider.filters.reveal-trans.play)))
406 (+ "Image " (1+ *which*) " of " photos.length)))
412 (setf document.images.photoslider.src
413 (aref photos *which*))
418 (when (< *which* (1- photos.length))
421 (setf document.images.photoslider.src
422 (aref photos *which*))
427 (setf window.location (aref photoslink *which*)))))
429 ;;; `JS-SLIDESHOW' generates the following JavaScript code:
431 var PRELOADEDIMAGES = new Array();
432 function preloadImages(photos) {
433 for (var i = 0; i != photos.length; i = i++) {
434 PRELOADEDIMAGES[i] = new Image;
435 PRELOADEDIMAGES[i].src = photos[i];
438 function applyEffect() {
439 if (document.all && photoslider.filters) {
440 var trans = photoslider.filters.revealTrans;
441 trans.Transition = Math.floor(Math.random() * 23);
446 function playEffect() {
447 if (document.all && photoslider.filters) {
448 photoslider.filters.revealTrans.play();
452 function keepTrack() {
453 window.status = "Image " + (WHICH + 1) + " of " +
456 function backward() {
460 document.images.photoslider.src = photos[WHICH];
466 if (WHICH < photos.length - 1) {
469 document.images.photoslider.src = photos[WHICH];
474 function transport() {
475 window.location = photoslink[WHICH];
478 ;;;# Customizing the slideshow
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.
488 (defun publish-slideshow (prefix images)
489 (let* ((js-url (format nil "~Aslideshow.js" prefix))
490 (html-url (format nil "~Aslideshow" prefix))
492 (mapcar #'(lambda (image)
493 (format nil "~A~A.~A" prefix
494 (pathname-name image)
495 (pathname-type image)))
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
514 (defun slideshow2 (req ent image-urls)
515 (declare (ignore req ent))
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")
532 (:tr ((:td :width "100%" :colspan 2 :height 22)
537 ((:img :src (aref photos 0)
542 (setf transition 23))))
545 (if (= *linkornot* 1)
547 :onclick (js-inline (transport)))
550 (:tr ((:td :width "50%" :height "21")
553 :onclick (js-inline (backward)
556 ((:td :width "50%" :height "21")
559 :onclick (js-inline (forward)
561 "Next Slide"))))))))))
563 ;;; We can now publish the same slideshow as before, under the
566 (publish-slideshow "/bknr/"
567 `("/home/manuel/bknr-sputnik.png"
568 "/home/manuel/bknrlogo_red648.png"
569 "/home/manuel/screenshots/screenshot-14.03.2005-11.54.33.png"))
571 ;;; That's it, we can now access our customized slideshow under
573 http://localhost:8000/bknr/slideshow