読者です 読者をやめる 読者になる 読者になる

Caveman2でCL-Markupを使う準備

Lisp プログラミング Web

目的

何か作ってみようかとCommon LispのWebフレームワークCaveman2を使い始めてみました。デフォルトのテンプレートエンジンとしてDjulaがサポートされていますが、閉じタグ書くなんて面倒くさい…と思い、CLで記述できるCL-Markupの利用を考えています。これもCaveman2のREADMEで紹介されているのですが、デフォルトのDjulaのように使うには少し手間がいるようでした。「Djulaのように」といっても「("src"フォルダではなく)"templates"フォルダ配下に置いて認識させる(+ファイル追加を多少は楽にする)」ぐらいの目標です。

要はCLのパッケージシステムを知っている人には自明の話だと思いますが、知らなかったのでメモに残しておこうという感じです。とりあえず動かしたという感じなので、自己流の変なやり方をしている可能性が高いです。

caveman2:make-project後の差分一通りは以下です。

Merge branch 'develop' · eshamster/caveman-sample@016e0d2 · GitHub

asdファイルの設定

デフォルトではmoduleとして"src"のみが記述されていますが、これに"templates"モジュールを追加します。

templates側の準備

templates/utils.lispに以下のようなマクロwith-markup-to-stringを書きました。

(defmacro with-markup-to-string (&body body)
  (let ((str (gensym)))
    `(with-output-to-string (,str)
       (let ((cl-markup:*output-stream* ,str))
         ,@body))))

CL-Markup本家の以下のissueでも問い合わせがあったようですが、dolistのように値を返さないループ中にmarkdownマクロを置いてもレンダリングしてくれないようです。代わりに"mapcar"や"loop ... collect"を使うことが推奨されています。

code output not rendered. cannot embed lisp code. · Issue #7 · arielnetworks/cl-markup · GitHub

が、これを見る前に書いてしまったのが上記です。とりあえず標準出力には問題なく出力されるようでしたので、いったん文字列ストリームに書き出してそれを返せばいいだろう…としたところ無事に動きました。後は、templates/index.lispにあるように、以下で問題なくレンダリングされます*1

(with-markup-to-string
  (html5
   (:body
    (dotimes (i 20)
      (markup (:div (if (evenp i) t-str f-str)))))))

src側の準備

テンプレートを追加するたびにパッケージ名付きのフルネームで呼ぶかuseするかimportするか、というのも面倒なので次のようなマクロを書きました*2

(defmacro call-template (name &rest rest)
  `(,(intern (string-upcase
              (format nil "~A-html" name))
             (string-upcase
              (format nil "caveman-sample.templates.~A" name)))
     ,@rest))

パッケージ名がcaveman-sample.templates.Xで関数名がX-htmlという命名規則に従っていることを前提としたせこいマクロですが…、これを使って次のようにdefrouteすることができます。制約はありますが、Djula用のデフォルトのrender関数っぽく書けているかなと思います。

(defroute "/" ()
  (call-template index
   :t-str "true"
   :f-str "false"))

新しくtemplatesファイル(と新規route)を追加する場合の手順・注意点

ここまでの準備ができたら、後は以下のようにして新しいテンプレートファイルを追加できます。

  1. templatesファイル自体の作成。以下に注意。
    1. パッケージ名と関数の命名規則
    2. dolistなどを使いたい場合は、with-markup-to-stringマクロをインポート
  2. call-templateマクロを使って、web.lispに新しいrouteを作成
  3. asdファイルのtemplatesモジュールのコンポーネントに追加

Djulaに比べると3つめのasdファイルの追加が入るのが少し辛い感じです。パッケージの定義も少々面倒ですし。

興味先行なので、Djulaに比べて何か利点を見込んでいるわけではないですが、何か起きてもまあなんとかなるだろうというLispに対する漠然とした安心感があるぐらいでしょうか。欠点としては、上記のように少々手順が煩雑なのと、変更時に毎回コンパイルが必要(といってもSLIME上で開発している分にはC-cを二回押すだけですが)な辺りが思いつきます。

*1:おそらくCL-Markupのなんらかの利点を殺しています

*2:ついsrc/web.lispに書いてしまいましたが、今思うとsrc/view.lispが適切ですね。まあプロトタイプということで…