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.

Getting Started

First, install a Common Lisp implementation. SBCL is a good one; CLiki has a comprehensive list of Common Lisp implementations. Next, get the Quicklisp package manager.

This tutorial uses the following libraries:

CL-FAD
file utilities
CL-WHO
HTML generator
Hunchentoot
web server
Parenscript
JavaScript generator

Load them using Quicklisp:

(mapc #'ql:quickload '(:cl-fad :cl-who :hunchentoot :parenscript))

Next, define a package to hold the example code:

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

(in-package "PS-TUTORIAL")

CL-WHO leaves it up to you to escape HTML attributes. One way to make sure that quoted strings in inline JavaScript work inside HTML attributes is to use double quotes for HTML attributes and single quotes for JavaScript strings.

(setq cl-who:*attribute-quote-char* #\")

Now start the web server:

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

Examples

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

(define-easy-handler (example1 :uri "/example1") ()
  (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 (example2 :uri "/example2") ()
  (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:

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

Slideshow

Next let's try a more complicated example: an image slideshow viewer.

First we need a way to define slideshows. For this tutorial we will assume that we have several different folders containing image files, and we want to serve each of the folders as its own slideshow under its own URL. We will use a custom Hunchentoot handler to serve the slideshow under /slideshows/{slideshow-name}, and the built-in Hunchentoot folder dispatcher to serve the image files from /slideshow-images/{slideshow-name}/{image-file}.

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

(defun add-slideshow (slideshow-name image-folder)
  (setf (gethash slideshow-name *slideshows*) image-folder)
  (push (create-folder-dispatcher-and-handler
         (format nil "/slideshow-images/~a/" slideshow-name)
         image-folder)
        *dispatch-table*))

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. We can use JavaScript to view the slideshow without refreshing the whole page, and provide regular link navigation for client browsers that do not have JavaScript enabled. Either way, we want viewers of our slideshow to be able to bookmark their place in the slideshow viewing sequence.

We will need a way to generate URIs for slideshow images on both the server and browser. We can eliminate code duplication with the defmacro+ps macro, which shares macro definitions 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}.

JavaScript-enabled web browsers will get information about the slideshow in an inline script generated by ps*, a function used for translating code generated at run-time. Slideshow navigation will be done with onclick handlers, generated at compile-time by the ps macro.

Regular HTML slideshow navigation will be done using query parameters.

(defun slideshow-handler ()
  (cl-ppcre:register-groups-bind (slideshow-name)
      ("/slideshows/(.*)" (script-name*))
    (let* ((images (mapcar
                    (lambda (i) (url-encode (file-namestring i)))
                    (list-directory
                     (or (gethash slideshow-name *slideshows*)
                         (progn (setf (return-code*) 404)
                                (return-from slideshow-handler))))))
           (current-image-index
             (or (position (url-encode (or (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 are passing the symbol naming the handler instead of the function object, which lets us redefine the handler without touching the dispatcher.

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

Last, we need to define the /slideshow.js script.

(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*))))

    ;; use fragment identifiers to allow bookmarking
    (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)))))))))

Note the @ and chain property access convenience macros. (@ object slotA slotB) expands to (getprop (getprop object 'slotA) 'slotB). chain is similar and also provides nested method calls.

Author: Vladimir Sedach <vsedach@oneofus.la> Last modified: 2018-03-29