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 ;;; The next command starts the webserver on the port 8080.
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:8080/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
(ps-inline
72 (alert "Hello World")))
75 ;;; Browsing "http://localhost:8080/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 ;;; (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:
97 (defmacro js-script
(&rest body
)
98 "Utility macro for including ParenScript into the HTML notation
99 of net.html.generator library that comes with AllegroServe."
100 `((:script
:type
"text/javascript")
101 (:princ
(format nil
"~%// <![CDATA[~%"))
103 (:princ
(format nil
"~%// ]]>~%"))))
105 (defun tutorial1 (req ent
)
106 (declare (ignore req ent
))
110 (:title
"ParenScript tutorial: 2nd example")
112 (defun greeting-callback ()
113 (alert "Hello World"))))
115 (:h1
"ParenScript tutorial: 2nd example")
116 (:p
"Please click the link below." :br
117 ((:a
:href
"#" :onclick
(ps-inline (greeting-callback)))
119 :br
"And maybe this link too." :br
120 ((:a
:href
"#" :onclick
(ps-inline (greeting-callback)))
122 :br
"And finally a third link." :br
123 ((:a
:href
"#" :onclick
(ps-inline (greeting-callback)))
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.
131 <html
><head
><title
>ParenScript tutorial
: 2nd example
</title
>
132 <script type
="text/javascript">
134 function greetingCallback
() {
135 alert
("Hello World");
140 <body
><h1
>ParenScript tutorial
: 2nd example
</h1
>
141 <p
>Please click the link below.
<br
/>
143 onclick
="javascript:greetingCallback();">Hello World
</a
>
145 And maybe this link too.
<br
/>
147 onclick
="javascript:greetingCallback();">Knock knock
</a
>
150 And finally a third link.
<br
/>
152 onclick
="javascript:greetingCallback();">Hello there
</a
>
157 ;;;# Generating a JavaScript file
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
162 ;;; reload the JavaScript code on each pageview. We will publish the
163 ;;; tutorial JavaScript under "/tutorial.js".
165 (defun tutorial1-file (req ent
)
166 (declare (ignore req ent
))
168 (ps (defun greeting-callback ()
169 (alert "Hello World"))))))
171 (publish :path
"/tutorial1.js"
172 :content-type
"text/javascript; charset=ISO-8859-1"
173 :function
(lambda (req ent
)
174 (with-http-response (req ent
)
175 (with-http-body (req ent
)
176 (tutorial1-file req ent
)))))
178 (defun tutorial1 (req ent
)
179 (declare (ignore req ent
))
183 (:title
"ParenScript tutorial: 3rd example")
184 ((:script
:language
"JavaScript" :src
"/tutorial1.js")))
186 (:h1
"ParenScript tutorial: 3rd example")
187 (:p
"Please click the link below." :br
188 ((:a
:href
"#" :onclick
(ps-inline (greeting-callback)))
190 :br
"And maybe this link too." :br
191 ((:a
:href
"#" :onclick
(ps-inline (greeting-callback)))
193 :br
"And finally a third link." :br
194 ((:a
:href
"#" :onclick
(ps-inline (greeting-callback)))
197 ;;; This will generate the following JavaScript code under
200 function greetingCallback
() {
201 alert
("Hello World");
204 ;;; and the following HTML code:
206 <html
><head
><title
>ParenScript tutorial
: 3rd example
</title
>
207 <script language
="JavaScript" src
="/tutorial1.js"></script
>
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
>
213 And maybe this link too.
<br
/>
214 <a href
="#" onclick
="javascript:greetingCallback();">Knock knock
</a
>
217 And finally a third link.
<br
/>
218 <a href
="#" onclick
="javascript:greetingCallback();">Hello there
</a
>
223 ;;;# A ParenScript slideshow
225 ;;; While developing ParenScript, I used JavaScript programs from the
226 ;;; web and rewrote them using ParenScript. This is a nice slideshow
229 http
://www.dynamicdrive.com
/dynamicindex14
/dhtmlslide.htm
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
240 (publish :path
"/slideshow"
241 :content-type
"text/html"
242 :function
(lambda (req ent
)
243 (with-http-response (req ent
)
244 (with-http-body (req ent
)
245 (slideshow req ent
)))))
247 (publish :path
"/slideshow.js"
248 :content-type
"text/html"
249 :function
(lambda (req ent
)
250 (with-http-response (req ent
)
251 (with-http-body (req ent
)
252 (js-slideshow req ent
)))))
254 ;;; The images are just random files I found on my harddrive. We will
255 ;;; publish them by hand for now.
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")
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.
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
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
281 (defun slideshow (req ent
)
282 (declare (ignore req ent
))
285 (:head
(:title
"ParenScript slideshow")
286 ((:script
:language
"JavaScript"
287 :src
"/slideshow.js"))
289 (defvar *linkornot
* 0)
290 (defvar photos
(array "photo1.jpg"
293 (:body
(:h1
"ParenScript slideshow")
298 (:tr
((:td
:width
"100%" :colspan
2 :height
22)
302 ((:img
:src
(aref photos
0)
305 (lisp (ps (reveal-trans
307 (setf transition
23)))))
310 (if (= *linkornot
* 1)
311 (ps-html ((:a
:href
"#"
312 :onclick
(lisp (ps-inline (transport))))
315 (:tr
((:td
:width
"50%" :height
"21")
318 :onclick
(ps-inline (backward)
321 ((:td
:width
"50%" :height
"21")
324 :onclick
(ps-inline (forward)
326 "Next Slide"))))))))))
328 ;;; `SLIDESHOW' generates the following HTML code (long lines have
331 <html
><head
><title
>ParenScript slideshow
</title
>
332 <script language
="JavaScript" src
="/slideshow.js"></script
>
333 <script type
="text/javascript">
336 var photos
= [ "photo1.jpg", "photo2.jpg", "photo3.jpg" ];
340 <body
><h1
>ParenScript slideshow
</h1
>
342 <table border
="0" cellspacing
="0" cellpadding
="0">
343 <tr
><td width
="100%" colspan
="2" height
="22">
344 <center
><script type
="text/javascript">
347 "<img src=\"" + photos
[0]
348 + "\" name=\"photoslider\"
349 style=\"filter:revealTrans(duration=2,transition=23)\"
350 border=\"0\"></img>";
351 document.write(LINKORNOT == 1 ?
353 onclick=\"javascript:transport()\">"
361 <tr><td width="50%" height="21"><p align="left">
363 onclick="javascript:backward(); return false;">Previous Slide</a>
367 <td width="50%" height="21"><p align="right">
369 onclick="javascript:forward(); return false;">Next Slide</a>
378 ;;; The actual slideshow application is generated by the function
379 ;;; `JS-SLIDESHOW', which generates a ParenScript file. Symbols are
380 ;;; converted to JavaScript variables, but the dot "." is left as
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'.
386 (defun js-slideshow (req ent)
387 (declare (ignore req ent))
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)
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)))
406 (defun play-effect ()
407 (when (and document.all photoslider.filters)
408 (photoslider.filters.reveal-trans.play)))
414 (+ "Image " (1+ *which*) " of " photos.length)))
420 (setf document.images.photoslider.src
421 (aref photos *which*))
426 (when (< *which* (1- photos.length))
429 (setf document.images.photoslider.src
430 (aref photos *which*))
435 (setf window.location (aref photoslink *which*)))))))
437 ;;; `JS-SLIDESHOW' generates the following JavaScript code:
439 var PRELOADEDIMAGES = new Array();
440 function preloadImages(photos) {
441 for (var i = 0; i != photos.length; i = i++) {
442 PRELOADEDIMAGES[i] = new Image;
443 PRELOADEDIMAGES[i].src = photos[i];
446 function applyEffect() {
447 if (document.all && photoslider.filters) {
448 var trans = photoslider.filters.revealTrans;
449 trans.Transition = Math.floor(Math.random() * 23);
454 function playEffect() {
455 if (document.all && photoslider.filters) {
456 photoslider.filters.revealTrans.play();
460 function keepTrack() {
461 window.status = "Image " + (WHICH + 1) + " of " +
464 function backward() {
468 document.images.photoslider.src = photos[WHICH];
474 if (WHICH < photos.length - 1) {
477 document.images.photoslider.src = photos[WHICH];
482 function transport() {
483 window.location = photoslink[WHICH];
486 ;;;# Customizing the slideshow
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.
496 (defun publish-slideshow (prefix images)
497 (let* ((js-url (format nil "~Aslideshow.js" prefix))
498 (html-url (format nil "~Aslideshow" prefix))
500 (mapcar (lambda (image)
501 (format nil "~A~A.~A" prefix
502 (pathname-name image)
503 (pathname-type image)))
505 (publish :path html-url
506 :content-type "text/html"
507 :function (lambda (req ent)
508 (with-http-response (req ent)
509 (with-http-body (req ent)
510 (slideshow2 req ent image-urls)))))
511 (publish :path js-url
512 :content-type "text/html"
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
522 (defun slideshow2 (req ent image-urls)
523 (declare (ignore req ent))
526 (:head (:title "ParenScript slideshow")
527 ((:script :language "JavaScript"
528 :src "/slideshow.js"))
529 ((:script :type "text/javascript")
530 (:princ (format nil "~%// <![CDATA[~%"))
531 (:princ (ps (defvar *linkornot* 0)))
532 (:princ (ps* `(defvar photos (array ,@image-urls))))
533 (:princ (format nil "~%// ]]>~%"))))
534 (:body (:h1 "ParenScript slideshow")
539 (:tr ((:td :width "100%" :colspan 2 :height 22)
543 ((:img :src (aref photos 0)
546 (lisp (ps (reveal-trans
548 (setf transition 23)))))
551 (if (= *linkornot* 1)
552 (ps-html ((:a :href "#"
553 :onclick (lisp (ps-inline (transport))))
556 (:tr ((:td :width "50%" :height "21")
559 :onclick (ps-inline (backward)
562 ((:td :width "50%" :height "21")
565 :onclick (ps-inline (forward)
567 "Next Slide"))))))))))
569 ;;; We can now publish the same slideshow as before, under the
572 (publish-slideshow "/bknr/"
573 `("/home/viper/photo1.jpg" "/home/viper/photo2.jpg" "/home/viper/photo3.jpg"))
575 ;;; That's it, we can now access our customized slideshow under
577 http://localhost:8080/bknr/slideshow