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

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

注意

原文はこちら：http://weitz.de/cl-who/

この訳は割とてきとーで、不正確な箇所、怪しい箇所、未訳の箇所があります

そのためツッコミ歓迎です

転載改変お好きにどうぞ

島は移動しません





Abstract

CL-WHO

世の中にはたくさんの Lisp マークアップ言語 がありますが、 -- どんな プログラマ も1度くらいは書いたことがあるでしょう -- CL-WHO (WHO とは "with-html-output" の略です。他に適当な頭字語がなかったので) もまた、良くも悪くもそういうものです。 それらは、コードと交じり合ったS式を(X)HTMLやや他のあれこれに変換するための便利な手段を提供するという点では、どれも程度の差はあれ似通ったものですが、 シンタックス 、実装、 API において違いがあります。そのため、もしあなたが選択をしていないならば、にたまたまはじめに出くわしたという理由でこれを使い始める前に代替案をよく調べたほうがよいでしょう。(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）の中に点在したものになります*1。 CL-WHO は2つの隣り合う WRITE-STRING フォームが存在しないようにします -- 下の例を見てください。 CL-WHO の出力は XTML（デフォルト）あるいは 'plain' (SGML) HTML です -- それはあなたが HTML-MODE に何をセットするかによりますが。

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

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

CL-WHO は、例えば clutu や Heike 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 を使って表示される。ただし、 T と NIL は上の通り処理する。（正しい表示コントロール変数を用意するのはアプリケーション開発者の仕事である）。

;; 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 は、 HTML 、 XHTML について何も知らないことに注意すべし。換言すれば、タグや属性名を許されない名前にミスタイプしてもチェックされない。 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 の主要なマクロである。これはその body を Syntax and Semantics で記述された transformation rules にしたがって、生成した結果を var および stream で示されるストリームに出力する形式に変換する。 stream が NIL ならば、 var がすでにストリームに束縛されていると仮定する。そうでなければ、 var は実行時に評価された stream フォームの結果に束縛される。 prologue は文字列でなければならず（もしくは、デフォルトでそうなっているが、空文字列を表す NIL でなければならず）、それはマクロのボディからストリームに送られる最初のものであることが保証されている。もし prologue が T なら、 prologue 文字列の値は *PROLOGUE* の値となる。 CL-WHO は通常、帯域幅をセーブするため一切の不要な空白を入れないようにする。しかしながら、 indent が真ならば、改行が挿入され、ネストされたタグは適切にインデントする。もし indent の値が整数ならば、初めのインデントの深さとして捉えられる。もし整数でなければ、0を意味する。（しかし、インデントは生成される HTML のセマンティクスを変えてしまう可能性があることに注意されたい。 例えば PRE や TEXTAREA のケース、そして、場合によっては追加の空白がテーブルのレイアウトを買えてしまうこともある）。 result* は forms によって返される値です。

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

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

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

[Special variable]

この 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 string を HTML 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 Semantics の ESC ショートカットについても見よ。





* (escape-string "<Hühner> 'naïve'") "<Hühner> 'naïve'" * (with-html-output-to-string (s) (:b (esc "<Hühner> 'naïve'"))) "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\"<b><Hühner> 'naïve'</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