t-cool

Parenscript チュートリアル


本記事は、原著者の許諾のもと、翻訳・掲載しています。

Parenscript Tutorial / Vladimir Sedach


Parenscript チュートリアル

はじめに

このチュートリアルでは、LispからJavaScriptへのコンパイラであるParenscriptを用いて、簡単なWebアプリを開発します。Parenscriptの関数とマクロの詳細は、Parenscript Reference Manualを参照してください。

プロジェクトを始める

まず、Common Lispの処理系をインストールしましょう。SBCLは良い処理系です。

Common Lisp処理系の一覧は、CLikiCommon Lisp処理系一覧に記載されています。 次に、パッケージメネージャーのQuicklispをインストールしましょう。

このチュートリアルでは、以下のライブラリを使います:

CL-FAD   ファイル関連のユーティリティー CL-WHO   HTMLジェネレーター Hunchentoot   webサーバ Parenscript   JavaScriptジェネレーター

Quicklispで読み込みましょう:

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

次に、チュートリアルのコードを収納するためのパッケージを定義しましょう:

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

CL-WHOでは、HTML属性をエスケープするかどうかを、自分で決めることができます。JavaScriptのインラインコード内でクオートされた文字列が、HTML属性の中で正しく動作するには、HTML属性にはダブルクォートを使い、JavaScriptの文字列にはシングルクォートを使うことで対処できます。

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

では、サーバを起動しましょう:

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

psマクロは、S式でParenscriptのコードを受けとります。Parenscriptのコードは、Common Lispと同じように表現でき、マクロ展開時に、JavaScriptコードを含む文字列に可能な範囲で変換されます。

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

WebページにParenscriptのコードを含めるには、HTMLタグにコードを埋め込みます:

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

別の方法としては、生成されたJavaScriptのコードを、別のHTTPリソースとしてサーブする(serve)ことでも可能です。このリソースへのリクエストは、ブラウザ内でキャッシュできます:

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

スライドショー

次に、より複雑な例に挑戦しましょう。スライドショーで画像を閲覧するアプリです。

まず、スライドショーを定義する方法が必要です。このチュートリアルでは、画像ファイルを含むフォルダを用意して、フォルダーごとのURLに基づいて、それぞれのスライドショーを提供します。/slideshows/{スライドの名前}でスライドショーを提供するために、独自のHunchentootハンドラを使います。また、/slideshow-images/{スライドショーの名前}/{画像のファイル名}にある画像をサーブするために、Hunchentootのfolder dispatcher関数を使います。

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

マシン上で写真を探し出し、Hunchentootに写真をサーブさせましょう:

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

次に、スライドショーのWebページを作ります。JavaScriptを使うと、ページ全体を再読み込みせずに、スライドショーを見ることができます。また、JavaScpiptを有効にしていないブラウザでも通常のリンク移動を提供できます。どちらにせよ、スライドショーの視聴者に、スライドショーのシーケンス内での位置をブックマークしておきたいはずです。

サーバとブラウザ両方に、スライドショーの画像のための URIを生成する方法が必要です。defmacro+psマクロは、Common LispとParenscriptの間でのマクロ定義を共有します。defmacro+psマクロを使うと、コードの重複を省けます。

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

次に、スライドショーのページをサーブするための関数を定義しましょう。ページは、/slideshow-images/{スライドショーの名前}のURIで画像をサーブされます。全てのページは、{スライドショーの名前}でディスパッチされる単独の関数によって処理されます。

JavaScriptを有効にしているブラウザは、インラインスクリプト内のps*関数によって生成されたスライドショーに関する情報をえることができます。ps*関数は、ランタイム時に生成されたコードを変換するために使われます。スライドショーの移動は、onclickハンドラで実行されます。onclickハンドラは、コンパイル時に、psマクロによって生成されます。

通常のHTMLのスライドショー移動は、クエリパラメータを用いて行われます。

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

この関数は独自のハンドラなので、スライドショーのために新たにディスパッチャーを作る必要があります。関数オブジェクトの代わりに、ハンドラを命名するシンボルを渡す必要があります。そうすることで、ディスパッチャーに触れずに、ハンドラを再定義することができます。

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

最後に、/slideshow.jsを定義しましょう。

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

@chainプロパティーは、 便利なマクロにアクセスします。(@ object slotA slotB)は、(getprop (getprop object 'slotA) 'slotB)に展開されます。chainも似ていますが、ネストされたメソッド呼び出しを提供します。

作者: Vladimir Sedach <vsedach@oneofus.la> 最終更新日: 2018-03-28


(追記)

  • 訳者は、macOSでチュートリアルを追試しました。ホームディレクトリに"junk/lolcats/"というディレクトリを作成して、jpeg形式で猫の画像を入れました。
  • スライドショーにアクセスするには、http://localhost:8080/slideshows/lolcatからアクセスしました。