Parenscript Tutorial

Introduction

This tutorial shows how to build a simple web application in Common Lisp, specifically demonstrating the Parenscript Lisp to JavaScript compiler.

The Parenscript reference manual contains a description of Parenscript functions and macros.

Setting up the Parenscript environment

This tutorial assumes you have a Common Lisp implementation already installed. SBCL is a good free software implementation if you don't have one. Besides Parenscript, the examples use Hunchentoot as a web server, CL-WHO for HTML generation, and CL-FAD for file utilities. These packages are available through Quicklisp. You will probably want to keep the Hunchentoot webpage open in another tab to refer to the documentation, since the Hunchentoot interface is not explained in this tutorial.

After installing the prerequisite libraries, let's load them.

(dolist (x '(:hunchentoot :cl-who :parenscript :cl-fad))
  (asdf:oos 'asdf:load-op x))
    

Next let's define a package to hold the example code

(defpackage "PS-TUTORIAL"
  (:use "COMMON-LISP" "HUNCHENTOOT" "CL-WHO" "PARENSCRIPT" "CL-FAD"))

(in-package "PS-TUTORIAL")
    

One thing we have to do is make sure that CL-WHO and Parenscript use different string delimiters so that literal strings will work as intended in JavaScript code inlined in HTML element properties.

(setf *js-string-delimiter* #\")
    

Now to start the server.

(start (make-instance 'acceptor :port 8080))
    

Examples

The ps macro takes Parenscript code in the form of lists (Parenscript code and Common Lisp code share the same representation), translates as much as it can into constant JavaScript strings at macro-expansion time, and expands into a form that will evaluate to a string containing JavaScript code.

(define-easy-handler (tutorial1 :uri "/tutorial1") ()
  (with-html-output-to-string (s)
    (:html
     (:head (:title "Parenscript tutorial: 1st example"))
     (:body (:h2 "Parenscript tutorial: 1st example")
            "Please click the link below." :br
            (:a :href "#" :onclick (ps (alert "Hello World"))
                "Hello World")))))
    

One way to include Parenscript code in web pages is to inline it in HTML script tags.

(define-easy-handler (tutorial2 :uri "/tutorial2") ()
  (with-html-output-to-string (s)
    (:html
     (:head
      (:title "Parenscript tutorial: 2nd example")
      (:script :type "text/javascript"
               (str (ps
                      (defun greeting-callback ()
                        (alert "Hello World"))))))
     (:body
      (:h2 "Parenscript tutorial: 2nd example")
      (:a :href "#" :onclick (ps (greeting-callback))
          "Hello World")))))
    

Another way to integrate Parenscript into a web application is to serve the generated JavaScript as a separate HTTP resource. Requests to this resource can then be cached by the browser and proxies.

(define-easy-handler (tutorial2-javascript :uri "/tutorial2.js") ()
  (setf (content-type*) "text/javascript")
  (ps
    (defun greeting-callback ()
      (alert "Hello World"))))
    

Slideshow

Next we're going to examine a more complicated example - a web-based slideshow viewer.

First we need a way to define slideshows. For this tutorial we'll assume that we have several different folders containing just images, and want to serve each of the folders as its own slideshow under its own URL. We'll use a hash table to keep track of what slideshows we are serving, which will also double as a cache of the image filenames, which will save us at least a system call and possibly disk access on every request. We'll use the folder dispatcher function provided by Hunchentoot to serve the image files under /slideshow-images/{slideshow-name}/.

(defvar *slideshows* (make-hash-table :test 'equalp))

(defun add-slideshow (slideshow-name image-folder)
  ;; we don't want to create duplicate dispatchers
  (unless (gethash slideshow-name *slideshows*)
    (push (create-folder-dispatcher-and-handler
           (format nil "/slideshow-images/~a/" slideshow-name)
           image-folder)
          *dispatch-table*))
  ;; but we do want to be able to refresh the cached list of image files
  (setf (gethash slideshow-name *slideshows*)
        (mapcar (lambda (pathname)
                  (url-encode (format nil "~a.~a"
                                      (pathname-name pathname)
                                      (pathname-type pathname))))
                (list-directory image-folder))))
    

Let's find some important pictures on our machine and get Hunchentoot to start serving them.

(add-slideshow "lolcat" "/home/junk/lolcats/")
(add-slideshow "lolrus" "/home/other-junk/lolruses/")
    

Next we need to create the slideshow web page. Ideally we'd like to use JavaScript and DHTML to move through the slideshow without refreshing the whole page, but with a fallback to regular link navigation in case the client browser has JavaScript turned off. Either way we want users of our slideshow to be able to bookmark their place in the slideshow viewing sequence. The techniques we'll use to achieve this can be similarly applied to arbitrary asynchronous data loading via AJAX.

We'll need a way to generate URIs for slideshow images on both the server and client sides. We can eliminate code duplication by using the defmacro/ps macro, which is used to share macros between Common Lisp and Parenscript.

(defmacro/ps slideshow-image-uri (slideshow-name image-file)
  `(concatenate 'string "/slideshow-images/" ,slideshow-name "/" ,image-file))
    

Next is the function to serve up the slideshow page. The pages will be served under /slideshows/{slideshow-name}, all of them handled by a single function that will dispatch on {slideshow-name}.

Regular navigation in the slideshow is done with the help of the image query parameter, which also allows bookmarking. For JavaScript-based image loading, we provide information about the slideshow in an inline script (generated using ps*, which is the function equivalent of the ps macro and is used for translating runtime-generated code) and give onclick handlers to the next/previous buttons.

(defun slideshow-handler ()
  (cl-ppcre:register-groups-bind (slideshow-name) ("/slideshows/(.*)" (script-name*))
    (let* ((images (gethash slideshow-name *slideshows*))
           (current-image-index (or (position (get-parameter "image") images :test #'equalp)
                                    0))
           (previous-image-index (max 0 (1- current-image-index)))
           (next-image-index (min (1- (length images)) (1+ current-image-index))))
      (with-html-output-to-string (s)
        (:html
         (:head
          (:title "Parenscript slideshow")
          (:script :type "text/javascript"
                   (str (ps* `(progn
                                (var *slideshow-name* ,slideshow-name)
                                (var *images* (array ,@images))
                                (var *current-image-index* ,current-image-index)))))
          (:script :type "text/javascript" :src "/slideshow.js"))
         (:body
          (:div :id "slideshow-container"
                :style "width:100%;text-align:center"
                (:img :id "slideshow-img-object"
                      :src (slideshow-image-uri slideshow-name
                                                (elt images current-image-index)))
                :br
                (:a :href (format nil "/slideshows/~a?image=~a"
                                  slideshow-name (elt images previous-image-index))
                    :onclick (ps (previous-image) (return false))
                    "Previous")
                " "
                (:a :href (format nil "/slideshows/~a?image=~a"
                                  slideshow-name (elt images next-image-index))
                    :onclick (ps (next-image) (return false))
                    "Next"))))))))
    

Since this function is a custom handler, we need to create a new dispatcher for it. Note that we're passing the symbol naming the handler instead of the function object (which we'd do with the #' reader macro), which lets us redefine the handler without touching the dispatcher.

(push (create-prefix-dispatcher "/slideshows/" 'slideshow-handler) 
      *dispatch-table*)
    

The slideshow page can accomodate regular HTML clients; now it is time to add dynamic image loading using JavaScript. We can accomplish bookmarkability by using the URI fragment identifier (however, this will make the browser forward and back buttons behave in a counterintuitive way; the fix for this is hacky and browser-specific and described here: http://ajaxpatterns.org/Unique_URLs).

Two things to note about the following code are the @ and chain property chaining convenience macros. (@ object slotA slotB) expands to (getprop (getprop object 'slotA) 'slotB). chain is similar, but also provides nested method calls.

(define-easy-handler (js-slideshow :uri "/slideshow.js") ()
  (setf (content-type*) "text/javascript")
  (ps
    (define-symbol-macro fragment-identifier (@ window location hash))
    
    (defun show-image-number (image-index)
      (let ((image-name (aref *images* (setf *current-image-index* image-index))))
        (setf (chain document (get-element-by-id "slideshow-img-object") src)
              (slideshow-image-uri *slideshow-name* image-name)
              fragment-identifier
              image-name)))
    
    (defun previous-image ()
      (when (> *current-image-index* 0)
        (show-image-number (1- *current-image-index*))))
      
    (defun next-image ()
      (when (< *current-image-index* (1- (getprop *images* 'length)))
        (show-image-number (1+ *current-image-index*))))

    ;; this gives bookmarkability using fragment identifiers
    (setf (getprop window 'onload)
          (lambda ()
            (when fragment-identifier
              (let ((image-name (chain fragment-identifier (slice 1))))
                (dotimes (i (length *images*))
                  (when (string= image-name (aref *images* i))
                    (show-image-number i)))))))))
    

Contact: vsedach@gmail.com. Last modified: 2010-10-17