;;;# ParenScript Tutorial
;;; This chapter is a short introductory tutorial to ParenScript. It
;;; hopefully will give you an idea how ParenScript can be used in a
;;; Lisp web application.
;;;# Setting up the ParenScript environment
;;; In this tutorial, we will use the Portable Allegroserve webserver
;;; to serve the tutorial web application. We use the ASDF system to
;;; load both Allegroserve and ParenScript. I assume you have
;;; installed and downloaded Allegroserve and Parenscript, and know
;;; how to setup the central registry for ASDF.
(asdf:oos 'asdf:load-op :aserve)
; ... lots of compiler output ...
(asdf:oos 'asdf:load-op :parenscript)
; ... lots of compiler output ...
;;; The tutorial will be placed in its own package, which we first
;;; have to define.
(defpackage :js-tutorial
(:use :common-lisp :net.aserve :net.html.generator :parenscript))
(in-package :js-tutorial)
;;; The next command starts the webserver on the port 8080.
(start :port 8080)
;;; We are now ready to generate the first JavaScript-enabled webpages
;;; using ParenScript.
;;;# A simple embedded example
;;; The first document we will generate is a simple HTML document,
;;; which features a single hyperlink. When clicking the hyperlink, a
;;; JavaScript handler opens a popup alert window with the string
;;; "Hello world". To facilitate the development, we will factor out
;;; the HTML generation to a separate function, and setup a handler
;;; for the url "/tutorial1", which will generate HTTP headers and
;;; call the function `TUTORIAL1'. At first, our function does nothing.
(defun tutorial1 (req ent)
(declare (ignore req ent))
nil)
(publish :path "/tutorial1"
:content-type "text/html; charset=ISO-8859-1"
:function (lambda (req ent)
(with-http-response (req ent)
(with-http-body (req ent)
(tutorial1 req ent)))))
;;; Browsing "http://localhost:8080/tutorial1" should return an empty
;;; HTML page. It's now time to fill this rather page with
;;; content. ParenScript features a macro that generates a string that
;;; can be used as an attribute value of HTML nodes.
(defun tutorial1 (req ent)
(declare (ignore req ent))
(html
(:html
(:head (:title "ParenScript tutorial: 1st example"))
(:body (:h1 "ParenScript tutorial: 1st example")
(:p "Please click the link below." :br
((:a :href "#" :onclick (ps-inline
(alert "Hello World")))
"Hello World"))))))
;;; Browsing "http://localhost:8080/tutorial1" should return the
;;; following HTML:
ParenScript tutorial: 1st example
ParenScript tutorial: 1st example
Please click the link below.
Hello World
;;;# Adding an inline ParenScript
;;; Suppose we now want to have a general greeting function. One way
;;; to do this is to add the javascript in a `SCRIPT' element at the
;;; top of the HTML page. This is done using the `JS-SCRIPT' macro
;;; (defined below) which will generate the necessary XML and comment
;;; tricks to cleanly embed JavaScript. We will redefine our
;;; `TUTORIAL1' function and add a few links:
(defmacro js-script (&rest body)
"Utility macro for including ParenScript into the HTML notation
of net.html.generator library that comes with AllegroServe."
`((:script :type "text/javascript")
(:princ (format nil "~%// ~%"))))
(defun tutorial1 (req ent)
(declare (ignore req ent))
(html
(:html
(:head
(:title "ParenScript tutorial: 2nd example")
(js-script
(defun greeting-callback ()
(alert "Hello World"))))
(:body
(:h1 "ParenScript tutorial: 2nd example")
(:p "Please click the link below." :br
((:a :href "#" :onclick (ps-inline (greeting-callback)))
"Hello World")
:br "And maybe this link too." :br
((:a :href "#" :onclick (ps-inline (greeting-callback)))
"Knock knock")
:br "And finally a third link." :br
((:a :href "#" :onclick (ps-inline (greeting-callback)))
"Hello there"))))))
;;; This will generate the following HTML page, with the embedded
;;; JavaScript nicely sitting on top. Take note how
;;; `GREETING-CALLBACK' was converted to camelcase, and how the lispy
;;; `DEFUN' was converted to a JavaScript function declaration.
ParenScript tutorial: 2nd example
ParenScript tutorial: 2nd example
Please click the link below.
Hello World
And maybe this link too.
Knock knock
And finally a third link.
Hello there
;;;# Generating a JavaScript file
;;; The best way to integrate ParenScript into a Lisp application is
;;; to generate a JavaScript file from ParenScript code. This file can
;;; be cached by intermediate proxies, and webbrowsers won't have to
;;; reload the JavaScript code on each pageview. We will publish the
;;; tutorial JavaScript under "/tutorial.js".
(defun tutorial1-file (req ent)
(declare (ignore req ent))
(html (:princ
(ps (defun greeting-callback ()
(alert "Hello World"))))))
(publish :path "/tutorial1.js"
:content-type "text/javascript; charset=ISO-8859-1"
:function (lambda (req ent)
(with-http-response (req ent)
(with-http-body (req ent)
(tutorial1-file req ent)))))
(defun tutorial1 (req ent)
(declare (ignore req ent))
(html
(:html
(:head
(:title "ParenScript tutorial: 3rd example")
((:script :language "JavaScript" :src "/tutorial1.js")))
(:body
(:h1 "ParenScript tutorial: 3rd example")
(:p "Please click the link below." :br
((:a :href "#" :onclick (ps-inline (greeting-callback)))
"Hello World")
:br "And maybe this link too." :br
((:a :href "#" :onclick (ps-inline (greeting-callback)))
"Knock knock")
:br "And finally a third link." :br
((:a :href "#" :onclick (ps-inline (greeting-callback)))
"Hello there"))))))
;;; This will generate the following JavaScript code under
;;; "/tutorial1.js":
function greetingCallback() {
alert("Hello World");
}
;;; and the following HTML code:
ParenScript tutorial: 3rd example
ParenScript tutorial: 3rd example
Please click the link below.
Hello World
And maybe this link too.
Knock knock
And finally a third link.
Hello there
;;;# A ParenScript slideshow
;;; While developing ParenScript, I used JavaScript programs from the
;;; web and rewrote them using ParenScript. This is a nice slideshow
;;; example from
http://www.dynamicdrive.com/dynamicindex14/dhtmlslide.htm
;;; The slideshow will be accessible under "/slideshow", and will
;;; slide through the images "photo1.png", "photo2.png" and
;;; "photo3.png". The first ParenScript version will be very similar
;;; to the original JavaScript code. The second version will then show
;;; how to integrate data from the Lisp environment into the
;;; ParenScript code, allowing us to customize the slideshow
;;; application by supplying a list of image names. We first setup the
;;; slideshow path.
(publish :path "/slideshow"
:content-type "text/html"
:function (lambda (req ent)
(with-http-response (req ent)
(with-http-body (req ent)
(slideshow req ent)))))
(publish :path "/slideshow.js"
:content-type "text/html"
:function (lambda (req ent)
(with-http-response (req ent)
(with-http-body (req ent)
(js-slideshow req ent)))))
;;; The images are just random files I found on my harddrive. We will
;;; publish them by hand for now.
(publish-file :path "/photo1.jpg"
:file "/home/viper/photo1.jpg")
(publish-file :path "/photo2.jpg"
:file "/home/viper/photo2.jpg")
(publish-file :path "/photo3.jpg"
:file "/home/viper/photo3.jpg")
;;; The function `SLIDESHOW' generates the HTML code for the main
;;; slideshow page. It also features little bits of ParenScript. These
;;; are the callbacks on the links for the slideshow application. In
;;; this special case, the javascript generates the links itself by
;;; using `document.write' in a "SCRIPT" element. Users that don't
;;; have JavaScript enabled won't see anything at all.
;;;
;;; `SLIDESHOW' also generates a static array called `PHOTOS' which
;;; holds the links to the photos of the slideshow. This array is
;;; handled by the ParenScript code in "slideshow.js". Note how the
;;; HTML code issued by ParenScrip is generated using the `PS-HTML'
;;; construct. In fact, there are two different HTML generators in the
;;; example below, one is the AllegroServe HTML generator, and the
;;; other is the ParenScript standard library HTML generator, which
;;; produces a JavaScript expression which evaluates to an HTML
;;; string.
(defun slideshow (req ent)
(declare (ignore req ent))
(html
(:html
(:head (:title "ParenScript slideshow")
((:script :language "JavaScript"
:src "/slideshow.js"))
(js-script
(defvar *linkornot* 0)
(defvar photos (array "photo1.jpg"
"photo2.jpg"
"photo3.jpg"))))
(:body (:h1 "ParenScript slideshow")
(:body (:h2 "Hello")
((:table :border 0
:cellspacing 0
:cellpadding 0)
(:tr ((:td :width "100%" :colspan 2 :height 22)
(:center
(js-script
(let ((img (ps-html
((:img :src (aref photos 0)
:name "photoslider"
:style (+ "filter:"
(lisp (ps (reveal-trans
(setf duration 2)
(setf transition 23)))))
:border 0)))))
(document.write
(if (= *linkornot* 1)
(ps-html ((:a :href "#"
:onclick (lisp (ps-inline (transport))))
img))
img)))))))
(:tr ((:td :width "50%" :height "21")
((:p :align "left")
((:a :href "#"
:onclick (ps-inline (backward)
(return false)))
"Previous Slide")))
((:td :width "50%" :height "21")
((:p :align "right")
((:a :href "#"
:onclick (ps-inline (forward)
(return false)))
"Next Slide"))))))))))
;;; `SLIDESHOW' generates the following HTML code (long lines have
;;; been broken):
ParenScript slideshow
ParenScript slideshow
Hello
;;; The actual slideshow application is generated by the function
;;; `JS-SLIDESHOW', which generates a ParenScript file. Symbols are
;;; converted to JavaScript variables, but the dot "." is left as
;;; is. This enables convenient access to object slots without using
;;; the `SLOT-VALUE' function all the time. However, when the object
;;; we are referring to is not a variable, but for example an element
;;; of an array, we have to revert to `SLOT-VALUE'.
(defun js-slideshow (req ent)
(declare (ignore req ent))
(html
(:princ
(ps
(defvar *preloaded-images* (make-array))
(defun preload-images (photos)
(dotimes (i photos.length)
(setf (aref *preloaded-images* i) (new *Image)
(slot-value (aref *preloaded-images* i) 'src)
(aref photos i))))
(defun apply-effect ()
(when (and document.all photoslider.filters)
(let ((trans photoslider.filters.reveal-trans))
(setf (slot-value trans '*Transition)
(floor (* (random) 23)))
(trans.stop)
(trans.apply))))
(defun play-effect ()
(when (and document.all photoslider.filters)
(photoslider.filters.reveal-trans.play)))
(defvar *which* 0)
(defun keep-track ()
(setf window.status
(+ "Image " (1+ *which*) " of " photos.length)))
(defun backward ()
(when (> *which* 0)
(decf *which*)
(apply-effect)
(setf document.images.photoslider.src
(aref photos *which*))
(play-effect)
(keep-track)))
(defun forward ()
(when (< *which* (1- photos.length))
(incf *which*)
(apply-effect)
(setf document.images.photoslider.src
(aref photos *which*))
(play-effect)
(keep-track)))
(defun transport ()
(setf window.location (aref photoslink *which*)))))))
;;; `JS-SLIDESHOW' generates the following JavaScript code:
var PRELOADEDIMAGES = new Array();
function preloadImages(photos) {
for (var i = 0; i != photos.length; i = i++) {
PRELOADEDIMAGES[i] = new Image;
PRELOADEDIMAGES[i].src = photos[i];
}
}
function applyEffect() {
if (document.all && photoslider.filters) {
var trans = photoslider.filters.revealTrans;
trans.Transition = Math.floor(Math.random() * 23);
trans.stop();
trans.apply();
}
}
function playEffect() {
if (document.all && photoslider.filters) {
photoslider.filters.revealTrans.play();
}
}
var WHICH = 0;
function keepTrack() {
window.status = "Image " + (WHICH + 1) + " of " +
photos.length;
}
function backward() {
if (WHICH > 0) {
--WHICH;
applyEffect();
document.images.photoslider.src = photos[WHICH];
playEffect();
keepTrack();
}
}
function forward() {
if (WHICH < photos.length - 1) {
++WHICH;
applyEffect();
document.images.photoslider.src = photos[WHICH];
playEffect();
keepTrack();
}
}
function transport() {
window.location = photoslink[WHICH];
}
;;;# Customizing the slideshow
;;; For now, the slideshow has the path to all the slideshow images
;;; hardcoded in the HTML code, as well as in the publish
;;; statements. We now want to customize this by publishing a
;;; slideshow under a certain path, and giving it a list of image urls
;;; and pathnames where those images can be found. For this, we will
;;; create a function `PUBLISH-SLIDESHOW' which takes a prefix as
;;; argument, as well as a list of image pathnames to be published.
(defun publish-slideshow (prefix images)
(let* ((js-url (format nil "~Aslideshow.js" prefix))
(html-url (format nil "~Aslideshow" prefix))
(image-urls
(mapcar (lambda (image)
(format nil "~A~A.~A" prefix
(pathname-name image)
(pathname-type image)))
images)))
(publish :path html-url
:content-type "text/html"
:function (lambda (req ent)
(with-http-response (req ent)
(with-http-body (req ent)
(slideshow2 req ent image-urls)))))
(publish :path js-url
:content-type "text/html"
:function (lambda (req ent)
(with-http-response (req ent)
(with-http-body (req ent)
(js-slideshow req ent)))))
(map nil (lambda (image url)
(publish-file :path url
:file image))
images image-urls)))
(defun slideshow2 (req ent image-urls)
(declare (ignore req ent))
(html
(:html
(:head (:title "ParenScript slideshow")
((:script :language "JavaScript"
:src "/slideshow.js"))
((:script :type "text/javascript")
(:princ (format nil "~%// ~%"))))
(:body (:h1 "ParenScript slideshow")
(:body (:h2 "Hello")
((:table :border 0
:cellspacing 0
:cellpadding 0)
(:tr ((:td :width "100%" :colspan 2 :height 22)
(:center
(js-script
(let ((img (ps-html
((:img :src (aref photos 0)
:name "photoslider"
:style (+ "filter:"
(lisp (ps (reveal-trans
(setf duration 2)
(setf transition 23)))))
:border 0)))))
(document.write
(if (= *linkornot* 1)
(ps-html ((:a :href "#"
:onclick (lisp (ps-inline (transport))))
img))
img)))))))
(:tr ((:td :width "50%" :height "21")
((:p :align "left")
((:a :href "#"
:onclick (ps-inline (backward)
(return false)))
"Previous Slide")))
((:td :width "50%" :height "21")
((:p :align "right")
((:a :href "#"
:onclick (ps-inline (forward)
(return false)))
"Next Slide"))))))))))
;;; We can now publish the same slideshow as before, under the
;;; "/bknr/" prefix:
(publish-slideshow "/bknr/"
`("/home/viper/photo1.jpg" "/home/viper/photo2.jpg" "/home/viper/photo3.jpg"))
;;; That's it, we can now access our customized slideshow under
http://localhost:8080/bknr/slideshow