CL-WHO - Yet another Lisp markup language を訳してみた

HTMLジェネレータ、CL-WHOのドキュメントを訳してみました。

※どうも最後の方が途切れてるようです。原因不明。容量制限か? と思って、AAを消してみたり、単独記事を立てても途切れます。コードが多すぎるせいでしょうか? そのうち分割して記事を書きます。(2012/07/12)

注意

  • 原文はこちら:http://weitz.de/cl-who/
  • この訳は割とてきとーで、不正確な箇所、怪しい箇所、未訳の箇所があります
  • そのためツッコミ歓迎です
  • 転載改変お好きにどうぞ
  • 島は移動しません


Abstract

世の中にはたくさんの Lisp マークアップ言語がありますが、 -- どんなプログラマも1度くらいは書いたことがあるでしょう -- CL-WHO (WHO とは "with-html-output" の略です。他に適当な頭字語がなかったので) もまた、良くも悪くもそういうものです。 それらは、コードと交じり合ったS式を(X)HTMLやXMLや他のあれこれに変換するための便利な手段を提供するという点では、どれも程度の差はあれ似通ったものですが、シンタックス、実装、APIにおいて違いがあります。そのため、もしあなたが選択をしていないならば、CL-WHOにたまたまはじめに出くわしたという理由でこれを使い始める前に代替案をよく調べたほうがよいでしょう。(Was that repelling enough?)。もしあなたがちょっと違ったやり方を探しているなら、 HTML-TEMPLATE も見るといいでしょう。

私はこれを 2002 年に書きました。そのときは少なくとも、Tim Bradshaw の htout と、AllegroServe の HTML generation facilities が Franz Inc. の John Foderaro が使えました。実のところ、私はなぜ私のライブラリを書かなければならなかったのか覚えていません -- たぶんたいして時間もかからず、なおかつ楽しかったからでしょう。シンタックスhtout にインスパイアされたものですが、ちょっと違ってます。


CL-WHO は、なるべく固定長の文字列を作り出す点で効率的なコードを作ろうとしています。言い換えれば、 CL-WHO のマクロによって生成されるコードは、ユーザによりマクロに恣意的に挿入されたコードが、定数部分を出力するためのWRITE-STRINGフォームの羅列(sequence)の中に点在したものになります*1CL-WHO は2つの隣り合う WRITE-STRING フォームが存在しないようにします -- 下の例を見てください。CL-WHOの出力は XTML(デフォルト)あるいは 'plain' (SGML) HTML です -- それはあなたが HTML-MODE に何をセットするかによりますが。

CL-WHO はポータブルであり、すべての Common Lisp 実装で動作することを目的としています。もしあなたが問題にいきあたったら、お知らせください。

これは BSD-stype ライセンスと共に配布されているので、基本的に、あなたはこれを使って好きなことができます。

CL-WHO は、例えば clutuHeike Stephan によって使われています。

Download shortcut: http://weitz.de/files/cl-who.tar.gz.



Contents

Example usage

*HTTP-STREAM* があなたのウェプアプリケーションが書き込みをサポートしているストリームだと仮定しましょう。このわざとらしいコード片は、 CL-WHO によって生成されるLispコードとHTMLアウトプットを一緒にしたものです。

(略)


Download and installation

(略)



Support and mailing lists

質問、バグレポート、機能リクエスト、改善、あるいはパッチについては、the cl-who-devel mailing listを使ってください。もしあなたが未来のリリースについて通知を受けたいのなら、the cl-who-announce mailing listに申し込んでください。これらのメーリングリストcommon-lisp.netのおかげで使用可能になりました。

もしパッチを送りたいならば、これを最初に読んでください


Syntax and Semantics

CL-WHOは本質的に一つのマクロ WITH-HTML-OUTPUT である。これは、その包むところのコード(すなわちbody)を次なる変換ルールにしたがって別のものに変換する(私達はこれをtransformation rulesと呼ぶ)。

ルール1
文字列はそのまま

文字列は文字通りプリントされる。より正確に言えば、その文字列をユーザが与えたストリームに書き出すフォームに変換される。

"foo" => (write-string "foo" s)

(ここでいう、またこのドキュメントの残りにおいて、赤い矢印*2は「左辺を右辺に相当するコードに変換する」ことを意味する。相当する、とは、すべてのアウトプットが「正しい」ストリームに送られるという意味である)

ルール2
キーワードはタグになる

キーワードで始まるリストは、次のルールに従い、同じ(通常、ダウンケースされた)名前のタグに変換される。

ルール2.1
空のタグ

もしリストがキーワードの他に何も含んでいなければ、結果のタグは空になる。

(:br) => (write-string "<br />" s)

HTML-MODE:SGMLにセットされている場合、空要素は次のように書かれる。

(:br) => (write-string "<br>" s)
ルール2.2
タグと属性

キーワードの後には別のキーワードを続けることができる。続けられたキーワードは属性名として解釈される。さらにその次のフォームは属性の値とされる(もし次のフォームがなければ、それはNILであったかのように振る舞う)。属性値を示しているフォームは次のように解釈される。(注意! 属性に関する振る舞いはバージョン0.3.0以前とは互換性がない!)

  • もしそれが文字列ならば、文字通り印字される。
(:td :bgcolor "red") => (write-string "<td bgcolor='red' />" s)
  • もしそれが T であり、HTML-MODE:XMLならば、属性値は属性名になる。(XHTMLの慣習で、HTMLにおける属性値のない属性はこう書く)*3
(:td :nowrap t) => (write-string "<td nowrap='nowrap' />" s)

HTML-MODE:SGML ならば、次のようになる。

(:td :nowrap t) => (write-string "<td nowrap>" s)
  • もし NIL ならば、属性は完全に除去される
(:td :nowrap nil) => (write-string "<td />" s)
  • もし定数フォームなら、評価結果がフォーマット文字列"~A"によってプリントされるような文字列として、マクロ展開時に結果文字列に挿入される。
(:table :border 3) => (write-string "<table border='3' />" s)
  • それ以外のフォームの場合そのまま残され、実行時にprincを使って表示される。ただし、TNILは上の通り処理する。(正しい表示コントロール変数を用意するのはアプリケーション開発者の仕事である)。
;; simplified example, see function CHECKBOX below
;; note that this form is not necessarily CONSTANTP in all Lisps

(:table :border (+ 1 2)) => (write-string "<table border='" s)
                              (princ (+ 1 2) s)
                              (write-string "' />" s)
ルール2.3
複数の属性

ひとたび属性/値ペアが完成すれば、別のペアがそれに続けて書ける。
言い換えれば、属性値の次のフォームがキーワードならばそれはまた属性名として扱われる。

(:table :border 0 :cellpadding 5 :cellspacing 5)
      => (write-string "<table border='0' cellpadding='5' cellspacing='5' />" s)
ルール2.4
タグの要素

タグ名あるいはキーワードでない属性値に続くはじめのフォームは、タグの要素の開始を定める。そのフォームと、続くすべてのフォームは今記述しているtransformation rulesにしたがう。

(:p "Paragraph") => (write-string "<p>Paragraph</p>" s)
(:p :class "foo" "Paragraph") => (write-string "<p class='foo'>Paragraph</p>" s)
(:p :class "foo" "One" " " "long" " " "sentence")
    => (write-string "<p class='foo'>One long sentence</p>" s)
(:p :class "foo" "Visit " (:a :href "http://www.cliki.net/" "CLiki"))
    => (write-string "<p class='foo'>Visit <a href='http://www.cliki.net/'>CLiki</a></p>" s)
ルール2.5
属性の別の書き方

バージョン0.4.0から、あなたはLHTMLのように、タグとすべての属性/値のペアを、追加のリストに括ったシンタックスを使える。

((:p) "Paragraph") => (write-string "<p>Paragraph</p>" s)
((:p :class "foo") "Paragraph") => (write-string "<p class='foo'>Paragraph</p>" s)
((:p :class "foo" :name "humpty-dumpty") "One" " " "long" " " "sentence")
    => (write-string "<p class='foo' name='humpty-dumpty'>One long sentence</p>" s)
((:p :class "foo") "Visit " ((:a :href "http://www.cliki.net/") "CLiki"))
    => (write-string "<p class='foo'>Visit <a href='http://www.cliki.net/'>CLiki</a></p>" s)

ちょっと複雑な例。

* (defun checkbox (stream name checked &optional value)
    (with-html-output (stream)
      (:input :type "checkbox" :name name :checked checked :value value)))

CHECKBOX
* (with-output-to-string (s) (checkbox s "foo" t))

 "<input type='checkbox' name='foo' checked='checked' />"
* (with-output-to-string (s) (checkbox s "foo" nil))

"<input type='checkbox' name='foo' />"
* (with-output-to-string (s) (checkbox s "foo" nil "bar"))

"<input type='checkbox' name='foo' value='bar' />"
* (with-output-to-string (s) (checkbox s "foo" t "bar"))

"<input type='checkbox' name='foo' checked='checked' value='bar' />"
ルール3
キーワードのみもタグに

キーワードのみがあれば、それはキーワードのみを含むリストと同じに取り扱われる。

:hr => (write-string "<hr />" s)
ルール4
特別扱いされるシンボル

文字列でもキーワードでも、キーワードではじまるリストでもないフォームは、次の変換を除いてそのまま残される。

str
文字列化

(str form1 form*)

のようなフォームは、

(let ((result form1)) (when result (princ result s)))

へと置換される。(form1より後のフォームはすべて無視される)

        (loop for i below 10 do (str i)) => 
        (loop for i below 10 do
           (let ((#:result i))
             (when <a href='#:result (princ '>:result (princ </a>:result *standard-output*))))

fmt
フォーマット

(fmt form*)
<||
というフォームは、
>||
(format s form*)
<||
というフォームに置換される。
>||
(loop for i below 10 do (fmt "~R" i)) => (loop for i below 10 do (format s "~R" i))

esc
エスケープ
(esc form1 form*)
というフォームは、
(let ((result form1)) (when result (write-string (escape-string result s))))
というフォームに置換される。
(htm form*)
というフォームは、それぞれのフォームが今記述しているtransformation rulesに従う

(loop for i below 100 do (htm (:b "foo") :br))
    => (loop for i below 100 do (progn (write-string "<b>foo</b><br />" s)))
ルール5
あとはそのまま

これで全てである。CL-WHOは、HTMLXHTMLについて何も知らないことに注意すべし。換言すれば、タグや属性名を許されない名前にミスタイプしてもチェックされない。CL-WHOはあなたが:hrの代わりに:foobarを使っても何も気にしない。


The CL-WHO dictionary

CL-WHOは次のシンボルをエクスポートしている。

[Macro] with-html-output (var &optional stream &key prologue indent) declaration* form* => result*

CL-WHOの主要なマクロである。これはそのbodySyntax and Semanticsで記述されたtransformation rulesにしたがって、生成した結果をvarおよびstreamで示されるストリームに出力する形式に変換する。streamNILならば、varがすでにストリームに束縛されていると仮定する。そうでなければ、varは実行時に評価されたstreamフォームの結果に束縛される。prologueは文字列でなければならず(もしくは、デフォルトでそうなっているが、空文字列を表すNILでなければならず)、それはマクロのボディからストリームに送られる最初のものであることが保証されている。もし prologueT なら、 prologue 文字列の値は*PROLOGUE* の値となる。CL-WHO は通常、帯域幅をセーブするため一切の不要な空白を入れないようにする。しかしながら、 indent が真ならば、改行が挿入され、ネストされたタグは適切にインデントする。もし indent の値が整数ならば、初めのインデントの深さとして捉えられる。もし整数でなければ、0を意味する。(しかし、インデントは生成されるHTMLのセマンティクスを変えてしまう可能性があることに注意されたい。 例えばPRETEXTAREAのケース、そして、場合によっては追加の空白がテーブルのレイアウトを買えてしまうこともある)。result*forms によって返される値です。

キーワード引数 prologueindent はマクロ展開時に使われることに注意されたい。

    * (with-html-output (*standard-output* nil :prologue t)
        (:html (:body "Not much there"))
        (values))
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"><html><body>Not much there</body></html>
    * (with-html-output (*standard-output*)
        (:html (:body :bgcolor "white"
             "Not much there"))
        (values))
    <html><body bgcolor='white'>Not much there</body></html>
    * (with-html-output (*standard-output* nil :prologue t :indent t)
        (:html (:body :bgcolor "white"
                 "Not much there"))
        (values))
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
    <html>
      <body bgcolor='white'>
        Not much there
      </body>
    </html>

[Macro] with-html-output-to-string (var &optional string-form &key element-type prologue indent) declaration* form* => result*


WITH-HTML-OUTPUT まわりの薄いラッパーである.実際、あんまり薄いので、説明するのに一番の方法は、その定義を見せることだろう。

(defmacro with-html-output-to-string ((var &optional string-form
                                           &key (element-type 'character)
                                                prologue
                                                indent)
                                      &body body)
  "Transform the enclosed BODY consisting of HTML as s-expressions
into Lisp code which creates the corresponding HTML as a string."
  `(with-output-to-string (,var ,string-form :elementy-type ,element-type)
    (with-html-output (,var nil :prologue ,prologue :indent ,indent)
      ,@body)))


このマクロの結果は WITH-OUTPUT-TO-STRING の振る舞いによることに注意されたい。


[Macro] show-html-expansion (var &optional stream &key prologue indent) declaration* form* =>

このマクロはデバッグのためのものである。このマクロは、*STANDARD-OUTPUT*に同じ引数で起動された WITH-HTML-OUTPUT によって生成されるコードを印字する。

* (show-html-expansion (s)
    (:html
     (:body :bgcolor "white"
      (:table
       (:tr
        (dotimes (i 5)
          (htm (:td :align "left"
                (str i)))))))))
(LET ((S S))
  (PROGN
    (WRITE-STRING
      "<html><body bgcolor='white'><table><tr>" S)
    (DOTIMES (I 5)
      (PROGN
        (WRITE-STRING "<td align='left'>" S)
        (PRINC I S)
        (WRITE-STRING "</td>" S)))
    (WRITE-STRING "</tr></table></body></html>" S)))

[Special variable] *attribute-quote-char*

この文字は属性を構築するときに使うクォート文字として使われる。シングルクォート文字#\'がデフォルトになる。他に唯一意味をなす文字はダブルクォート文字#\"である。


[Special variable] *prologue*

これは WITH-HTML-OUTPUT に与えられたprologueキーワード引数がTであったときに印字されるプロローグ文字列である。HTML-MODEをセットする時には変更されたい*4。初期値は次のとおり。

    "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">"


[Special variable] *html-empty-tag-aware-p*

strictXMLジェネレータとしてCL-WHOを使いたい時、これをNILにセットされたい。そうでなければ、CL-WHO*HTML-EMPTY-TAGS* に列挙された空タグは、 (XHTML mode) あるいは(SGML mode) と記述される。その他のすべてのタグは、と出力される。この変数の初期値はTである。


[Special variable] *html-empty-tags*

このHTMLのタグのリストは空タグとして出力されるべきタグのリストである。 *HTML-EMPTY-TAG-AWARE-P* を見よ。この変数の初期値は次のリストである。

    (:area :atop :audioscope :base :basefont :br :choose :col :frame
     :hr :img :input :isindex :keygen :left :limittext :link :meta
     :nextid :of :over :param :range :right :spacer :spot :tab :wbr)

[Special variable] *downcase-tokens-p*

もしこの変数の値が NIL ならば、タグもしくは属性名を表しているキーワードシンボルは自動的にダウンケースされない。
これは大文字・小文字の区別に敏感なXMLを出力する時に便利である。デフォルトは T である。


[Symbol] fmt
[Symbol] htm
[Symbol] str


これらはいかなる束縛も関連付けられていないただのシンボルである。これらがエクスポートされている唯一の理由は、 Syntax and Semantics で記述された変換中で特別な意味を持っているからである。



[Accessor] html-mode => mode
[Accessor] (setf (html-mode) mode)


関数HTML-MODEは、現在のHTML生成モードを返す。そのデフォルトはXHTMLのために:XML である。あなたはこのモードを

(SETF (HTML-MODE) :SGML)

を使って XML以前のHTMLモードに変更できる。

もしモードをSGML HTMLにセットしたならば、 *prologue*doctype stringHTML 4.01 transitionalにセットせよ。


    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">


SGML HTMLにおけるコード生成は、 XHTML とは少し違う -- 空要素を /> で終わる必要がない他、空属性が許される。


[Function] escape-string string &key test => escaped-string

この関数は文字列stringを受け付け、testが真を返す文字を文字符号で置き換える。HTML-MODE:SGMLのとき、古いクライアントの機能を考慮して、数値実体参照は16進数ではなく10進数で行われる。testは1引数の関数でなければならず、それは文字を受け取りgeneralized boolean を返さねばならない。そのデフォルト値は *ESCAPE-CHAR-P* である。Syntax and SemanticsESC ショートカットについても見よ。

    * (escape-string "<H&#252;hner> 'na&#239;ve'")
    "&lt;H&#xFC;hner&gt; &#x27;na&#xEF;ve&#x27;"
    * (with-html-output-to-string (s)
        (:b (esc "<H&#252;hner> 'na&#239;ve'")))
    "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\"<b>&lt;H&#xFC;hner&gt; &#x27;na&#xEF;ve&#x27;</b>"

[Function] escape-char character &key test => escaped-string

この関数は、文字列ではなく文字を受け取る他は、ESCAPED-STRINGと同じように振る舞う。


[Special variable] *escape-char-p*

これは ESCAPE-CHAR へのtest キーワード引数のデフォルト値である。この変数のデフォルト値は以下のとおり。

    #'(lambda (char)
        (or (find char "<>&'\"")
            (> (char-code char) 127)))


[Function] escape-string-minimal string => escaped-string
[Function] escape-string-minimal-plus-quotes string => escaped-string
[Function] escape-string-iso-8859-1 string => escaped-string
[Function] escape-string-iso-8859 string => escaped-string
[Function] escape-string-all string => escaped-string
[Function] escape-char-minimal character => escaped-string
[Function] escape-char-minimal-plus-quotes character => escaped-string
[Function] escape-char-iso-8859-1 character => escaped-string
[Function] escape-char-all character => escaped-string

これらは ESCAPE-STRING および ESCAPE-CHAR の上に作られた便利関数である。文字列関数は次のものと同じように定義されている。

    (defun escape-string-minimal (string)
      "Escape only #\<, #\>, and #\& in STRING."
      (escape-string string :test #'(lambda (char) (find char "<>&"))))

    (defun escape-string-minimal-plus-quotes (string)
      "Like ESCAPE-STRING-MINIMAL but also escapes quotes."
      (escape-string string :test #'(lambda (char) (find char "<>&'\""))))

    (defun escape-string-iso-8859-1 (string)
      "Escapes all characters in STRING which aren't defined in ISO-8859-1."
      (escape-string string :test #'(lambda (char)
                                      (or (find char "<>&'\"")
                                          (> (char-code char) 255)))))

    (defun escape-string-iso-8859 (string)
      "Identical to ESCAPE-STRING-ISO-8859-1.  Kept for backward compatibility."
      (escape-string-iso-8859-1 string))

    (defun escape-string-all (string)
      "Escapes all characters in STRING which aren't in the 7-bit ASCII
    character set."
      (escape-string string :test #'(lambda (char)
                                      (or (find char "<>&'\"")
                                          (> (char-code char) 127)))))


文字関数も同じように定義されている。


[Function] conc &rest string-list => string


すべての引数(文字列でなければならない)を一つの文字列に連結するユーティリティ関数。主に属性値とともに使われることを意図している。

    * (conc "This" " " "is" " " "a" " " "sentence")
    "This is a sentence"
    * (with-html-output-to-string (s)
        (:div :style (conc "padding:"
                           (format nil "~A" (+ 3 2)))
         "Foobar"))
    "<div style='padding:5'>Foobar</div>"


[Generic Function] convert-tag-to-string-list tag attr-list body body-fn => strings-or-forms


この関数はCL-WHOのいくつかの内部シンボルを、ユーザがその振る舞いを変更できるように晒す。この関数はタグが処理される時にはいつでも呼び出され、関連する文字列あるいはLISPフォームのリストを返さねばならない。いくつかののタグをユーザ自身で処理するためにこの総称関数を特殊化することができる。

tagは外部タグに名前をつけるためのキーワードシンボルであり、attr-listはその連想リストを表す連想リスト(そのCAR部は属性名でありキーワード、CDR部はその値)、bodyはタグの要素、そしてbody-fnはその要素をさらに処理するためにに適用されるべき関数である。もちろん、もしあなたが独自のメソッドを定義したならば、body-fnを無視したければ無視できる。

これは単純なサンプルである。

    * (defmethod convert-tag-to-string-list ((tag (eql :red)) attr-list body body-fn)
        (declare (ignore attr-list))
        (nconc (cons "<font color='red'>" (funcall body-fn body)) (list "</font>"))) 
    ; Compiling LAMBDA (PCL::.PV-CELL. PCL::.NEXT-METHOD-CALL. TAG ATTR-LIST BODY BODY-FN): 
    ; Compiling Top-Level Form: 

    #<STANDARD-METHOD CONVERT-TAG-TO-STRING-LIST ((EQL :RED) T T T) {582B268D}>
    * (with-html-output (*standard-output*)
        (:red (:b "Bold and red")) 
        (values))
    <font color='red'><b>Bold and red</b></font>
    * (show-html-expansion (s)
        (:red :style "spiffy" (if (foo) (htm "Attributes are ignored")))) 

    (LET ((S S))
      (PROGN
       NIL
       (WRITE-STRING "<font color='red'>" S)
       (IF (FOO) (PROGN (WRITE-STRING "Attributes are ignored" S)))
       (WRITE-STRING "</font>" S)))
    * (defmethod convert-tag-to-string-list ((tag (eql :table)) attr-list body body-fn)
        (cond ((cdr (assoc :simple attr-list))
               (nconc (cons "<table"
                            (convert-attributes (remove :simple attr-list :key #'car)))
                      (list ">")
                      (loop for row in body
                            collect "<tr>"
                            nconc (loop for col in row
                                        collect "<td>"
                                        when (constantp col)
                                          collect (format nil "~A" col)
                                        else 
                                          collect col
                                        collect "</td>")
                            collect "</tr>")
                      (list "</table>")))
              (t 

*1:訳があやしい。原文は、"In other words, the code generated by the CL-WHO macros will usually be a sequence of WRITE-STRING forms for constant parts of the output interspersed with arbitrary code inserted by the user of the macro." @goodmadさんに指摘を受け多少修正。2012/07/15

*2:赤くないが、=>のこと

*3:翻訳が怪しい。原文は、"If it is T and HTML-MODE is :XML (default) the attribute's value will be the attribute's name (following XHTML convention to denote attributes which don't have a value in HTML). "

*4:変更される、かもしれない。原文は"Gets changed when you set HTML-MODE.