Copyright 2009, 2018 Vladimir Sedach.
Permission is granted to copy, distribute and/or modify this
document under the terms of the GNU Free Documentation License,
Version 1.3 or any later version published by the Free Software
Foundation; with no Invariant Sections, no Front-Cover Texts,
and no Back-Cover Texts. A copy of the license can be
found on the
GNU website.
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.
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:
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))
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"))))
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 <vas@oneofus.la> Last modified: 2018-03-29