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
:net.html.generator
))
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 ;;;f "images/tutorial1-1.png" 0.6 Embedded ParenScript example
80 <html
><head
><title
>ParenScript tutorial
: 1st example
</title
>
82 <body
><h1
>ParenScript tutorial
: 1st example
</h1
>
83 <p
>Please click the link below.
<br
/>
85 onclick
="javascript:alert("Hello World");">Hello World
</a
>
90 ;;;# Adding an inline ParenScript
92 ;;; Suppose we now want to have a general greeting function. One way
93 ;;; to do this is to add the javascript in a `SCRIPT' element at the
94 ;;; top of the HTML page. This is done using the `JS-SCRIPT' macro
95 ;;; which will generate the necessary XML and comment tricks to
96 ;;; cleanly embed JavaScript. We will redefine our `TUTORIAL1'
97 ;;; function and add a few links:
99 (defun tutorial1 (req ent
)
100 (declare (ignore req ent
))
104 (:title
"ParenScript tutorial: 2nd example")
106 (defun greeting-callback ()
107 (alert "Hello World"))))
109 (:h1
"ParenScript tutorial: 2nd example")
110 (:p
"Please click the link below." :br
111 ((:a
:href
"#" :onclick
(js-inline (greeting-callback)))
113 :br
"And maybe this link too." :br
114 ((:a
:href
"#" :onclick
(js-inline (greeting-callback)))
116 :br
"And finally a third link." :br
117 ((:a
:href
"#" :onclick
(js-inline (greeting-callback)))
120 ;;; This will generate the following HTML page, with the embedded
121 ;;; JavaScript nicely sitting on top. Take note how
122 ;;; `GREETING-CALLBACK' was converted to camelcase, and how the lispy
123 ;;; `DEFUN' was converted to a JavaScript function declaration.
125 ;;;f "images/tutorial1-2.png" 0.6 Inline ParenScript example
127 <html
><head
><title
>ParenScript tutorial
: 2nd example
</title
>
128 <script type
="text/javascript">
130 function greetingCallback
() {
131 alert
("Hello World");
136 <body
><h1
>ParenScript tutorial
: 2nd example
</h1
>
137 <p
>Please click the link below.
<br
/>
139 onclick
="javascript:greetingCallback();">Hello World
</a
>
141 And maybe this link too.
<br
/>
143 onclick
="javascript:greetingCallback();">Knock knock
</a
>
146 And finally a third link.
<br
/>
148 onclick
="javascript:greetingCallback();">Hello there
</a
>
153 ;;;# Generating a JavaScript file
155 ;;; The best way to integrate ParenScript into a Lisp application is
156 ;;; to generate a JavaScript file from ParenScript code. This file can
157 ;;; be cached by intermediate proxies, and webbrowsers won't have to
158 ;;; reload the javascript code on each pageview. A standalone
159 ;;; JavaScript can be generated using the macro `JS-FILE'. We will
160 ;;; publish the tutorial JavaScript under "/tutorial.js".
162 (defun tutorial1-file (req ent
)
163 (declare (ignore req ent
))
165 (defun greeting-callback ()
166 (alert "Hello World"))))
168 (publish :path
"/tutorial1.js"
169 :content-type
"text/javascript; charset=ISO-8859-1"
170 :function
#'(lambda (req ent
)
171 (with-http-response (req ent
)
172 (with-http-body (req ent
)
173 (tutorial1-file req ent
)))))
175 (defun tutorial1 (req ent
)
176 (declare (ignore req ent
))
180 (:title
"ParenScript tutorial: 3rd example")
181 ((:script
:language
"JavaScript" :src
"/tutorial1.js")))
183 (:h1
"ParenScript tutorial: 3rd example")
184 (:p
"Please click the link below." :br
185 ((:a
:href
"#" :onclick
(js-inline (greeting-callback)))
187 :br
"And maybe this link too." :br
188 ((:a
:href
"#" :onclick
(js-inline (greeting-callback)))
190 :br
"And finally a third link." :br
191 ((:a
:href
"#" :onclick
(js-inline (greeting-callback)))
194 ;;; This will generate the following JavaScript code under
197 function greetingCallback
() {
198 alert
("Hello World");
201 ;;; and the following HTML code:
203 <html
><head
><title
>ParenScript tutorial
: 3rd example
</title
>
204 <script language
="JavaScript" src
="/tutorial1.js"></script
>
206 <body
><h1
>ParenScript tutorial
: 3rd example
</h1
>
207 <p
>Please click the link below.
<br
/>
208 <a href
="#" onclick
="javascript:greetingCallback();">Hello World
</a
>
210 And maybe this link too.
<br
/>
211 <a href
="#" onclick
="javascript:greetingCallback();">Knock knock
</a
>
214 And finally a third link.
<br
/>
215 <a href
="#" onclick
="javascript:greetingCallback();">Hello there
</a
>
220 ;;;# A ParenScript slideshow
222 ;;; While developing ParenScript, I used JavaScript programs from the
223 ;;; web and rewrote them using ParenScript. This is a nice slideshow
226 http
://www.dynamicdrive.com
/dynamicindex14
/dhtmlslide.htm
228 ;;; The slideshow will be accessible under "/slideshow", and will
229 ;;; slide through the images "photo1.png", "photo2.png" and
230 ;;; "photo3.png". The first ParenScript version will be very similar
231 ;;; to the original JavaScript code. The second version will then show
232 ;;; how to integrate data from the Lisp environment into the
233 ;;; ParenScript code, allowing us to customize the slideshow
234 ;;; application by supplying a list of image names. We first setup the
237 (publish :path
"/slideshow"
238 :content-type
"text/html"
239 :function
#'(lambda (req ent
)
240 (with-http-response (req ent
)
241 (with-http-body (req ent
)
242 (slideshow req ent
)))))
244 (publish :path
"/slideshow.js"
245 :content-type
"text/html"
246 :function
#'(lambda (req ent
)
247 (with-http-response (req ent
)
248 (with-http-body (req ent
)
249 (js-slideshow req ent
)))))
251 ;;; The images are just random images I found on my harddrive. We will
252 ;;; publish them by hand for now.
254 (publish-file :path
"/photo1.png"
255 :file
"/home/manuel/bknr-sputnik.png")
256 (publish-file :path
"/photo2.png"
257 :file
"/home/manuel/bknrlogo_red648.png")
258 (publish-file :path
"/photo3.png"
259 :file
"/home/manuel/bknr-sputnik.png")
261 ;;; The function `SLIDESHOW' generates the HTML code for the main
262 ;;; slideshow page. It also features little bits of ParenScript. These
263 ;;; are the callbacks on the links for the slideshow application. In
264 ;;; this special case, the javascript generates the links itself by
265 ;;; using `document.write' in a "SCRIPT" element. Users that don't
266 ;;; have JavaScript enabled won't see anything at all.
268 ;;; `SLIDESHOW' also generates a static array called `PHOTOS' which
269 ;;; holds the links to the photos of the slideshow. This array is
270 ;;; handled by the ParenScript code in "slideshow.js". Note how the
271 ;;; HTML code issued by the JavaScript is generated using the `HTML'
272 ;;; construct. In fact, we have two different HTML generators in the
273 ;;; example below, one is the standard Lisp HTML generator, and the
274 ;;; other is the JavaScript HTML generator, which generates a
275 ;;; JavaScript expression.
277 (defun slideshow (req ent
)
278 (declare (ignore req ent
))
281 (:head
(:title
"ParenScript slideshow")
282 ((:script
:language
"JavaScript"
283 :src
"/slideshow.js"))
285 (defvar *linkornot
* 0)
286 (defvar photos
(array "photo1.png"
289 (:body
(:h1
"ParenScript slideshow")
294 (:tr
((:td
:width
"100%" :colspan
2 :height
22)
299 ((:img
:src
(aref photos
0)
304 (setf transition
23))))
307 (if (= *linkornot
* 1)
309 :onclick
(js-inline (transport)))
312 (:tr
((:td
:width
"50%" :height
"21")
315 :onclick
(js-inline (backward)
318 ((:td
:width
"50%" :height
"21")
321 :onclick
(js-inline (forward)
323 "Next Slide"))))))))))
325 ;;; `SLIDESHOW' generates the following HTML code (long lines have
326 ;;; been broken down):
328 <html
><head
><title
>ParenScript slideshow
</title
>
329 <script language
="JavaScript" src
="/slideshow.js"></script
>
330 <script type
="text/javascript">
333 var photos
= [ "photo1.png", "photo2.png", "photo3.png" ];
337 <body
><h1
>ParenScript slideshow
</h1
>
339 <table border
="0" cellspacing
="0" cellpadding
="0">
340 <tr
><td width
="100%" colspan
="2" height
="22">
341 <center
><script type
="text/javascript">
344 "<img src=\"" + photos
[0]
345 + "\" name=\"photoslider\"
346 style=\"filter:revealTrans(duration=2,transition=23)\"
347 border=\"0\"></img>";
348 document.write(LINKORNOT == 1 ?
350 onclick=\"javascript:transport()\">"
358 <tr><td width="50%" height="21"><p align="left">
360 onclick="javascript:backward(); return false;">Previous Slide</a>
364 <td width="50%" height="21"><p align="right">
366 onclick="javascript:forward(); return false;">Next Slide</a>
375 ;;;f "images/slideshow.png" 0.45 ParenScript Slideshow
377 ;;; The actual slideshow application is generated by the function
378 ;;; `JS-SLIDESHOW', which generates a ParenScript file. The code is
379 ;;; pretty straightforward for a lisp savy person. Symbols are
380 ;;; converted to JavaScript variables, but the dot "." is left as
381 ;;; is. This enables us to access object "slots" without using the
382 ;;; `SLOT-VALUE' function all the time. However, when the object we
383 ;;; are referring to is not a variable, but for example an element of
384 ;;; an array, we have to revert to `SLOT-VALUE'.
386 (defun js-slideshow (req ent)
387 (declare (ignore req ent))
389 (defvar *preloaded-images* (make-array))
390 (defun preload-images (photos)
391 (dotimes (i photos.length)
392 (setf (aref *preloaded-images* i) (new *Image)
393 (slot-value (aref *preloaded-images* i) 'src)
396 (defun apply-effect ()
397 (when (and document.all photoslider.filters)
398 (let ((trans photoslider.filters.reveal-trans))
399 (setf (slot-value trans '*Transition)
400 (floor (* (random) 23)))
404 (defun play-effect ()
405 (when (and document.all photoslider.filters)
406 (photoslider.filters.reveal-trans.play)))
412 (+ "Image " (1+ *which*) " of " photos.length)))
418 (setf document.images.photoslider.src
419 (aref photos *which*))
424 (when (< *which* (1- photos.length))
427 (setf document.images.photoslider.src
428 (aref photos *which*))
433 (setf window.location (aref photoslink *which*)))))
435 ;;; `JS-SLIDESHOW' generates the following JavaScript code:
437 var PRELOADEDIMAGES = new Array();
438 function preloadImages(photos) {
439 for (var i = 0; i != photos.length; i = i++) {
440 PRELOADEDIMAGES[i] = new Image;
441 PRELOADEDIMAGES[i].src = photos[i];
444 function applyEffect() {
445 if (document.all && photoslider.filters) {
446 var trans = photoslider.filters.revealTrans;
447 trans.Transition = Math.floor(Math.random() * 23);
452 function playEffect() {
453 if (document.all && photoslider.filters) {
454 photoslider.filters.revealTrans.play();
458 function keepTrack() {
459 window.status = "Image " + (WHICH + 1) + " of " +
462 function backward() {
466 document.images.photoslider.src = photos[WHICH];
472 if (WHICH < photos.length - 1) {
475 document.images.photoslider.src = photos[WHICH];
480 function transport() {
481 window.location = photoslink[WHICH];
484 ;;;# Customizing the slideshow
486 ;;; For now, the slideshow has the path to all the slideshow images
487 ;;; hardcoded in the HTML code, as well as in the publish
488 ;;; statements. We now want to customize this by publishing a
489 ;;; slideshow under a certain path, and giving it a list of image urls
490 ;;; and pathnames where those images can be found. For this, we will
491 ;;; create a function `PUBLISH-SLIDESHOW' which takes a prefix as
492 ;;; argument, as well as a list of image pathnames to be published.
494 (defun publish-slideshow (prefix images)
495 (let* ((js-url (format nil "~Aslideshow.js" prefix))
496 (html-url (format nil "~Aslideshow" prefix))
498 (mapcar #'(lambda (image)
499 (format nil "~A~A.~A" prefix
500 (pathname-name image)
501 (pathname-type image)))
503 (publish :path html-url
504 :content-type "text/html"
505 :function #'(lambda (req ent)
506 (with-http-response (req ent)
507 (with-http-body (req ent)
508 (slideshow2 req ent image-urls)))))
509 (publish :path js-url
510 :content-type "text/html"
511 :function #'(lambda (req ent)
512 (with-http-response (req ent)
513 (with-http-body (req ent)
514 (js-slideshow req ent)))))
515 (map nil #'(lambda (image url)
516 (publish-file :path url
520 (defun slideshow2 (req ent image-urls)
521 (declare (ignore req ent))
524 (:head (:title "ParenScript slideshow")
525 ((:script :language "JavaScript"
526 :src "/slideshow.js"))
527 ((:script :type "text/javascript")
528 (:princ (format nil "~%// <![CDATA[~%"))
529 (:princ (js (defvar *linkornot* 0)))
530 (:princ (js-to-string `(defvar photos
531 (array ,@image-urls))))
532 (:princ (format nil "~%// ]]>~%"))))
533 (:body (:h1 "ParenScript slideshow")
538 (:tr ((:td :width "100%" :colspan 2 :height 22)
543 ((:img :src (aref photos 0)
548 (setf transition 23))))
551 (if (= *linkornot* 1)
553 :onclick (js-inline (transport)))
556 (:tr ((:td :width "50%" :height "21")
559 :onclick (js-inline (backward)
562 ((:td :width "50%" :height "21")
565 :onclick (js-inline (forward)
567 "Next Slide"))))))))))
569 ;;; We can now publish the same slideshow as before, under the
572 (publish-slideshow "/bknr/"
573 `("/home/manuel/bknr-sputnik.png"
574 "/home/manuel/bknrlogo_red648.png"
575 "/home/manuel/screenshots/screenshot-14.03.2005-11.54.33.png"))
577 ;;; That's it, we can now access our customized slideshow under
579 http://localhost:8000/bknr/slideshow