Fix ambiguities in tutorial slideshow and add 404 handling
[parenscript.git] / docs / tutorial.html
blobc47920f3cc2a488c8a21d798e4032a80aba32191
1 <!DOCTYPE html>
3 <html lang="en">
4 <head>
5 <meta charset="utf-8">
6 <meta name="viewport"
7 content="width=device-width, initial-scale=1"
10 <title>Parenscript Tutorial</title>
11 <style type="text/css">
12 body {
13 max-width: 70ex;
14 margin: auto;
16 pre {
17 border: 1px solid #d5d5d5;
18 background: #f9f9f9;
19 padding: 1ex;
21 </style>
22 </head>
24 <body>
25 <h1 style="text-align:center;">Parenscript Tutorial</h1>
27 <h2>Introduction</h2>
29 <p>
30 This tutorial shows how to build a simple web application in
31 Common Lisp, specifically demonstrating
32 the <a href="http://common-lisp.net/project/parenscript/">Parenscript</a>
33 Lisp to JavaScript compiler.
34 </p>
36 <p>
37 The <a href="reference.html">Parenscript
38 reference manual</a> contains a description of Parenscript
39 functions and macros.
40 </p>
42 <h2>Getting Started</h2>
44 <p>
45 First, install a Common Lisp
46 implementation. <a href="http://sbcl.org/">SBCL</a> is a good
47 one; <a href="https://www.cliki.net/">CLiki</a> has
48 a <a href="https://www.cliki.net/Common+Lisp+implementation">comprehensive
49 list of Common Lisp implementations</a>. Next, get
50 the <a href="https://www.quicklisp.org/">Quicklisp</a> package
51 manager.
52 </p>
54 <p>
55 This tutorial uses the following libraries:
56 </p>
58 <dl>
59 <dt><a href="https://edicl.github.io/cl-fad/">CL-FAD</a></dt>
60 <dd>file utilities</dd>
62 <dt><a href="https://edicl.github.io/cl-who/">CL-WHO</a></dt>
63 <dd>HTML generator</dd>
65 <dt><a href="https://edicl.github.io/hunchentoot/">Hunchentoot</a></dt>
66 <dd>web server</dd>
68 <dt><a href="https://common-lisp.net/project/parenscript/">Parenscript</a></dt>
69 <dd>JavaScript generator</dd>
70 </dl>
72 <p>
73 Load them using Quicklisp:
74 </p>
76 <pre><code>(mapc #'ql:quickload '(:cl-fad :cl-who :hunchentoot :parenscript))</code></pre>
78 <p>
79 Next, define a package to hold the example code:
80 </p>
82 <pre><code>(defpackage &quot;PS-TUTORIAL&quot;
83 (:use &quot;COMMON-LISP&quot; &quot;HUNCHENTOOT&quot; &quot;CL-WHO&quot; &quot;PARENSCRIPT&quot; &quot;CL-FAD&quot;))
85 (in-package &quot;PS-TUTORIAL&quot;)</code></pre>
87 <p>
88 CL-WHO leaves it up to you to escape HTML attributes. One way to
89 make sure that quoted strings in inline JavaScript work inside
90 HTML attributes is to use double quotes for HTML attributes and
91 single quotes for JavaScript strings.
92 </p>
94 <pre><code>(setq cl-who:*attribute-quote-char* #\&quot;)</code></pre>
96 <p>
97 Now start the web server:
98 </p>
100 <pre><code>(start (make-instance 'easy-acceptor :port 8080))</code></pre>
102 <h2>Examples</h2>
105 The <code>ps</code> macro takes Parenscript code in the form of
106 s-expressions (Parenscript code and Common Lisp code share the
107 same representation), translates as much as it can into constant
108 strings at macro-expansion time, and expands into a form that
109 will evaluate to a string containing JavaScript code.
110 </p>
112 <pre><code>(define-easy-handler (example1 :uri "/example1") ()
113 (with-html-output-to-string (s)
114 (:html
115 (:head (:title "Parenscript tutorial: 1st example"))
116 (:body (:h2 "Parenscript tutorial: 1st example")
117 "Please click the link below." :br
118 (:a :href "#" :onclick (ps (alert "Hello World"))
119 "Hello World")))))</code></pre>
122 One way to include Parenscript code in web pages is to inline it
123 in HTML <code>script</code> tags:
124 </p>
126 <pre><code>(define-easy-handler (example2 :uri "/example2") ()
127 (with-html-output-to-string (s)
128 (:html
129 (:head
130 (:title "Parenscript tutorial: 2nd example")
131 (:script :type "text/javascript"
132 (str (ps
133 (defun greeting-callback ()
134 (alert "Hello World"))))))
135 (:body
136 (:h2 "Parenscript tutorial: 2nd example")
137 (:a :href "#" :onclick (ps (greeting-callback))
138 "Hello World")))))</code></pre>
141 Another way to integrate Parenscript into a web application is
142 to serve the generated JavaScript as a separate HTTP resource.
143 Requests to this resource can then be cached by the browser:
144 </p>
146 <pre><code>(define-easy-handler (example3 :uri "/example3.js") ()
147 (setf (content-type*) "text/javascript")
149 (defun greeting-callback ()
150 (alert "Hello World"))))</code></pre>
152 <h2>Slideshow</h2>
155 Next let's try a more complicated example: an image slideshow
156 viewer.
157 </p>
160 First we need a way to define slideshows. For this tutorial we
161 will assume that we have several different folders containing
162 image files, and we want to serve each of the folders as its own
163 slideshow under its own URL. We will use a custom Hunchentoot
164 handler to serve the slideshow
165 under <samp>/slideshows/{slideshow-name}</samp>, and the
166 built-in Hunchentoot
167 <a href="https://edicl.github.io/hunchentoot/#create-folder-dispatcher-and-handler">folder
168 dispatcher</a> to serve the image files
169 from <samp>/slideshow-images/{slideshow-name}/{image-file}</samp>.
170 </p>
172 <pre><code>(defvar *slideshows* (make-hash-table :test 'equalp))
174 (defun add-slideshow (slideshow-name image-folder)
175 (setf (gethash slideshow-name *slideshows*) image-folder)
176 (push (create-folder-dispatcher-and-handler
177 (format nil "/slideshow-images/~a/" slideshow-name)
178 image-folder)
179 *dispatch-table*))</code></pre>
182 Let's find some important pictures on our machine and get
183 Hunchentoot to start serving them:
184 </p>
186 <pre><code>(add-slideshow "lolcat" "/home/junk/lolcats/")
187 (add-slideshow "lolrus" "/home/other-junk/lolruses/")</code></pre>
190 Next we need to create the slideshow web page. We can use
191 JavaScript to view the slideshow without refreshing the whole
192 page, and provide regular link navigation for client browsers
193 that do not have JavaScript enabled. Either way, we want viewers
194 of our slideshow to be able to bookmark their place in the
195 slideshow viewing sequence.
196 </p>
199 We will need a way to generate URIs for slideshow images on both
200 the server and browser. We can eliminate code duplication with
201 the <code>defmacro+ps</code> macro, which shares macro
202 definitions between Common Lisp and Parenscript.
203 </p>
205 <pre><code>(defmacro+ps slideshow-image-uri (slideshow-name image-file)
206 `(concatenate 'string "/slideshow-images/" ,slideshow-name "/" ,image-file))</code></pre>
209 Next is the function to serve up the slideshow page. The pages
210 will be served under <samp>/slideshows/{slideshow-name}</samp>,
211 all of them handled by a single function that will dispatch on
212 <samp>{slideshow-name}</samp>.
213 </p>
216 JavaScript-enabled web browsers will get information about the
217 slideshow in an inline script generated
218 by <a href="reference.html#ps*"><code>ps*</code></a>,
219 a function used for translating code generated at run-time.
220 Slideshow navigation will be done with <code>onclick</code>
221 handlers, generated at compile-time by
222 the <a href="reference.html#ps"><code>ps</code></a>
223 macro.
224 </p>
227 Regular HTML slideshow navigation will be done using query
228 parameters.
229 </p>
231 <pre><code>(defun slideshow-handler ()
232 (cl-ppcre:register-groups-bind (slideshow-name)
233 ("/slideshows/(.*)" (script-name*))
234 (let* ((images (mapcar
235 (lambda (i) (url-encode (file-namestring i)))
236 (list-directory
237 (or (gethash slideshow-name *slideshows*)
238 (progn (setf (return-code*) 404)
239 (return-from slideshow-handler))))))
240 (current-image-index
241 (or (position (url-encode (or (get-parameter "image") ""))
242 images
243 :test #'equalp)
245 (previous-image-index (max 0
246 (1- current-image-index)))
247 (next-image-index (min (1- (length images))
248 (1+ current-image-index))))
249 (with-html-output-to-string (s)
250 (:html
251 (:head
252 (:title "Parenscript slideshow")
253 (:script
254 :type "text/javascript"
255 (str
256 (ps*
257 `(progn
258 (var *slideshow-name* ,slideshow-name)
259 (var *images* (array ,@images))
260 (var *current-image-index* ,current-image-index)))))
261 (:script :type "text/javascript" :src "/slideshow.js"))
262 (:body
263 (:div :id "slideshow-container"
264 :style "width:100%;text-align:center"
265 (:img :id "slideshow-img-object"
266 :src (slideshow-image-uri
267 slideshow-name
268 (elt images current-image-index)))
270 (:a :href (format nil "/slideshows/~a?image=~a"
271 slideshow-name
272 (elt images previous-image-index))
273 :onclick (ps (previous-image) (return false))
274 "Previous")
276 (:a :href (format nil "/slideshows/~a?image=~a"
277 slideshow-name
278 (elt images next-image-index))
279 :onclick (ps (next-image) (return false))
280 "Next"))))))))</code></pre>
283 Since this function is a custom handler, we need to create a new
284 dispatcher for it. Note that we are passing the symbol naming
285 the handler instead of the function object, which lets us
286 redefine the handler without touching the dispatcher.
287 </p>
289 <pre><code>(push (create-prefix-dispatcher "/slideshows/" 'slideshow-handler)
290 *dispatch-table*)</code></pre>
293 Last, we need to define the <samp>/slideshow.js</samp> script.
294 </p>
296 <pre><code>(define-easy-handler (js-slideshow :uri "/slideshow.js") ()
297 (setf (content-type*) "text/javascript")
299 (define-symbol-macro fragment-identifier (@ window location hash))
301 (defun show-image-number (image-index)
302 (let ((image-name (aref *images* (setf *current-image-index* image-index))))
303 (setf (chain document (get-element-by-id "slideshow-img-object") src)
304 (slideshow-image-uri *slideshow-name* image-name)
305 fragment-identifier
306 image-name)))
308 (defun previous-image ()
309 (when (> *current-image-index* 0)
310 (show-image-number (1- *current-image-index*))))
312 (defun next-image ()
313 (when (< *current-image-index* (1- (getprop *images* 'length)))
314 (show-image-number (1+ *current-image-index*))))
316 ;; use fragment identifiers to allow bookmarking
317 (setf (getprop window 'onload)
318 (lambda ()
319 (when fragment-identifier
320 (let ((image-name (chain fragment-identifier (slice 1))))
321 (dotimes (i (length *images*))
322 (when (string= image-name (aref *images* i))
323 (show-image-number i)))))))))</code></pre>
326 Note
327 the <a href="reference.html#@"><code>@</code></a>
328 and <a href="reference.html#chain"><code>chain</code></a>
329 property access convenience macros. <code>(@ object slotA
330 slotB)</code> expands to
331 <code>(getprop (getprop object 'slotA)
332 'slotB)</code>. <code>chain</code> is similar and also provides
333 nested method calls.
334 </p>
336 <p style="font-size:xx-small; float:right;">Author: Vladimir Sedach &lt;<a href="mailto:vsedach@oneofus.la">vsedach@oneofus.la</a>&gt; Last modified: 2018-03-29</p>
337 </body>
338 </html>