本章的目標(biāo)是完成一個(gè)簡(jiǎn)單的 HTML 生成器 —— 這個(gè)程序可以自動(dòng)生成一系列包含超文本鏈接的網(wǎng)頁。除了介紹特定 Lisp 技術(shù)之外,本章還是一個(gè)典型的自底向上編程(bottom-up programming)的例子。 我們以一些通用 HTML 實(shí)用函數(shù)作為開始,繼而將這些例程看作是一門編程語言,從而更好地編寫這個(gè)生成器。
HTML (HyperText Markup Language,超文本標(biāo)記語言)用于構(gòu)建網(wǎng)頁,是一種簡(jiǎn)單、易學(xué)的語言。本節(jié)就對(duì)這種語言作概括性介紹。
當(dāng)你使用網(wǎng)頁瀏覽器閱覽網(wǎng)頁時(shí),瀏覽器從遠(yuǎn)程服務(wù)器獲取 HTML 文件,并將它們顯示在你的屏幕上。每個(gè) HTML 文件都包含任意多個(gè)標(biāo)簽(tag),這些標(biāo)簽相當(dāng)于發(fā)送給瀏覽器的指令。
圖 16.2 一個(gè)網(wǎng)頁
注意在尖角括號(hào)之間的文本并沒有被顯示出來,這些用尖角括號(hào)包圍的文本就是標(biāo)簽。 HTML 的標(biāo)簽分為兩種,一種是成雙成對(duì)地出現(xiàn)的:
<tag>...</tag>
第一個(gè)標(biāo)簽標(biāo)志著某種情景(environment)的開始,而第二個(gè)標(biāo)簽標(biāo)志著這種情景的結(jié)束。 這種標(biāo)簽的一個(gè)例子是?<h2>
?:所有被<h2>
?和?</h2>
?包圍的文本,都會(huì)使用比平常字體尺寸稍大的字體來顯示。
另外一些成雙成對(duì)出現(xiàn)的標(biāo)簽包括:創(chuàng)建帶編號(hào)列表的?<ol>
?標(biāo)簽(ol 代表 ordered list,有序表),令文本居中的?<center>
?標(biāo)簽,以及創(chuàng)建鏈接的?<a>
?標(biāo)簽(a 代表 anchor,錨點(diǎn))。
被?<a>
?和?</a>
?包圍的文本就是超文本(hypertext)。 在大多數(shù)瀏覽器上,超文本都會(huì)以一種與眾不同的方式被凸顯出來 —— 它們通常會(huì)帶有下劃線 —— 并且點(diǎn)擊這些文本會(huì)讓瀏覽器跳轉(zhuǎn)到另一個(gè)頁面。 在標(biāo)簽?a
?之后的部分,指示了鏈接被點(diǎn)擊時(shí),瀏覽器應(yīng)該跳轉(zhuǎn)到的位置。
一個(gè)像
<a href="foo.html">
這樣的標(biāo)簽,就標(biāo)識(shí)了一個(gè)指向另一個(gè) HTML 文件的鏈接,其中這個(gè) HTML 文件和當(dāng)前網(wǎng)頁的文件夾相同。 當(dāng)點(diǎn)擊這個(gè)鏈接時(shí),瀏覽器就會(huì)獲取并顯示?foo.html
?這個(gè)文件。
當(dāng)然,鏈接并不一定都要指向相同文件夾下的 HTML 文件,實(shí)際上,一個(gè)鏈接可以指向互聯(lián)網(wǎng)的任何一個(gè)文件。
和成雙成對(duì)出現(xiàn)的標(biāo)簽相反,另一種標(biāo)簽沒有結(jié)束標(biāo)記。 在圖 16.1 里有一些這樣的標(biāo)簽,包括:創(chuàng)建一個(gè)新文本行的?<br>
?標(biāo)簽(br 代表 break ,斷行),以及在列表情景中,創(chuàng)建一個(gè)新列表項(xiàng)的?<li>
?標(biāo)簽(li 代表 list item ,列表項(xiàng))。
HTML 還有不少其他的標(biāo)簽,但是本章要用到的標(biāo)簽,基本都包含在圖 16.1 里了。
(defmacro as (tag content)
`(format t "<~(~A~)>~A</~(~A~)>"
',tag ,content ',tag))
(defmacro with (tag &rest body)
`(progn
(format t "~&<~(~A~)>~%" ',tag)
,@body
(format t "~&</~(~A~)>~%" ',tag)))
(defmacro brs (&optional (n 1))
(fresh-line)
(dotimes (i n)
(princ "<br>"))
(terpri))
圖 16.3 標(biāo)簽生成例程
本節(jié)會(huì)定義一些生成 HTML 的例程。 圖 16.3 包含了三個(gè)基本的、生成標(biāo)簽的例程。 所有例程都將它們的輸出發(fā)送到?*standard-output*
?;可以通過重新綁定這個(gè)變量,將輸出重定向到一個(gè)文件。
宏?as
?和?with
?都用于在標(biāo)簽之間生成表達(dá)式。其中?as
?接受一個(gè)字符串,并將它打印在兩個(gè)標(biāo)簽之間:
> (as center "The Missing Lambda")
<center>The Missing Lambda</center>
NIL
with
?則接受一個(gè)代碼體(body of code),并將它放置在兩個(gè)標(biāo)簽之間:
> (with center
(princ "The Unbalanced Parenthesis"))
<center>
The Unbalanced Parenthesis
</center>
NIL
兩個(gè)宏都使用了?~(...~)
?來進(jìn)行格式化,從而將標(biāo)簽轉(zhuǎn)化為小寫字母的標(biāo)簽。 HTML 并不介意標(biāo)簽是大寫還是小寫,但是在包含許許多多標(biāo)簽的 HTML 文件中,小寫字母的標(biāo)簽可讀性更好一些。
除此之外,?as
?傾向于將所有輸出都放在同一行,而?with
?則將標(biāo)簽和內(nèi)容都放在不同的行里。 (使用?~&
?來進(jìn)行格式化,以確保輸出從一個(gè)新行中開始。) 以上這些工作都只是為了讓 HTML 更具可讀性,實(shí)際上,標(biāo)簽之外的空白并不影響頁面的顯示方式。
圖 16.3 中的最后一個(gè)例程?brs
?用于創(chuàng)建多個(gè)文本行。 在很多瀏覽器中,這個(gè)例程都可以用于控制垂直間距。
(defun html-file (base)
(format nil "~(~A~).html" base))
(defmacro page (name title &rest body)
(let ((ti (gensym)))
`(with-open-file (*standard-output*
(html-file ,name)
:direction :output
:if-exists :supersede)
(let ((,ti ,title))
(as title ,ti)
(with center
(as h2 (string-upcase ,ti)))
(brs 3)
,@body))))
圖 16.4 HTML 文件生成例程
圖 16.4 包含用于生成 HTML 文件的例程。 第一個(gè)函數(shù)根據(jù)給定的符號(hào)(symbol)返回一個(gè)文件名。 在一個(gè)實(shí)際應(yīng)用中,這個(gè)函數(shù)可能會(huì)返回指向某個(gè)特定文件夾的路徑(path)。 目前來說,這個(gè)函數(shù)只是簡(jiǎn)單地將?.html
?后綴追加到給定符號(hào)名的后邊。
宏?page
?負(fù)責(zé)生成整個(gè)頁面,它的實(shí)現(xiàn)和?with-open-file
?很相似:?body
?中的表達(dá)式會(huì)被求值,求值的結(jié)果通過?*standard-output*
?所綁定的流,最終被寫入到相應(yīng)的 HTML 文件中。
6.7 小節(jié)展示了如何臨時(shí)性地綁定一個(gè)特殊變量。 在 113 頁的例子中,我們?cè)?let
?的體內(nèi)將?*print-base*
?綁定為?16
?。 這一次,通過將?*standard-output*
?和一個(gè)指向 HTML 文件的流綁定,只要我們?cè)?page
?的函數(shù)體內(nèi)調(diào)用?as
?或者?princ
?,輸出就會(huì)被傳送到 HTML 文件里。
page
?宏的輸出先在頂部打印?title
?,接著求值?body
?中的表達(dá)式,打印?body
?部分的輸出。
如果我們調(diào)用
(page 'paren "The Unbalanced Parenthesis"
(princ "Something in his expression told her..."))
這會(huì)產(chǎn)生一個(gè)名為?paren.html
?的文件(文件名由?html-file
?函數(shù)生成),文件中的內(nèi)容為:
<title>The Unbalanced Parenthesis</title>
<center>
<h2>THE UNBALANCED PARENTHESIS</h2>
</center>
<br><br><br>
Something in his expression told her...
除了?title
?標(biāo)簽以外,以上輸出的所有 HTML 標(biāo)簽在前面已經(jīng)見到過了。 被?<title>
?標(biāo)簽包圍的文本并不顯示在網(wǎng)頁之內(nèi),它們會(huì)顯示在瀏覽器窗口,用作頁面的標(biāo)題。
(defmacro with-link (dest &rest body)
`(progn
(format t "<a href=\"~A\">" (html-file ,dest))
,@body
(princ "</a>")))
(defun link-item (dest text)
(princ "<li>")
(with-link dest
(princ text)))
(defun button (dest text)
(princ "[ ")
(with-link dest
(princ text))
(format t " ]~%"))
圖 16.5 生成鏈接的例程
圖片 16.5 給出了用于生成鏈接的例程。?with-link
?和?with
?很相似:它根據(jù)給定的地址?dest
?,創(chuàng)建一個(gè)指向 HTML 文件的鏈接。 而鏈接內(nèi)部的文本,則通過求值?body
?參數(shù)中的代碼段得出:
> (with-link 'capture
(princ "The Captured Variable"))
<a href="capture.html">The Captured Variable</a>
"</a>"
with-link
?也被用在?link-item
?當(dāng)中,這個(gè)函數(shù)接受一個(gè)字符串,并創(chuàng)建一個(gè)帶鏈接的列表項(xiàng):
> (link-item 'bq "Backquote!")
<li><a href="bq.html">Backquote!</a>
"</a>"
最后,?button
?也使用了?with-link
?,從而創(chuàng)建一個(gè)被方括號(hào)包圍的鏈接:
> (button 'help "Help")
[ <a href="help.html">Help</a> ]
NIL
在這一節(jié),我們先暫停一下編寫 HTML 生成器的工作,轉(zhuǎn)到編寫迭代式例程的工作上來。
你可能會(huì)問,怎樣才能知道,什么時(shí)候應(yīng)該編寫主程序,什么時(shí)候又應(yīng)該編寫子例程?
實(shí)際上,這個(gè)問題,沒有答案。
通常情況下,你總是先開始寫一個(gè)程序,然后發(fā)現(xiàn)需要寫一個(gè)新的例程,于是你轉(zhuǎn)而去編寫新例程,完成它,接著再回過頭去編寫原來的程序。 時(shí)間關(guān)系,要在這里演示這個(gè)開始-完成-又再開始的過程是不太可能的,這里只展示這個(gè)迭代式例程的最終形態(tài),需要注意的是,這個(gè)程序的編寫并不如想象中的那么簡(jiǎn)單。 程序通常需要經(jīng)歷多次重寫,才會(huì)變得簡(jiǎn)單。
(defun map3 (fn lst)
(labels ((rec (curr prev next left)
(funcall fn curr prev next)
(when left
(rec (car left)
curr
(cadr left)
(cdr left)))))
(when lst
(rec (car lst) nil (cadr lst) (cdr lst)))))
圖 16.6 對(duì)樹進(jìn)行迭代
圖 16.6 里定義的新例程是?mapc
?的一個(gè)變種。它接受一個(gè)函數(shù)和一個(gè)列表作為參數(shù),對(duì)于傳入列表中的每個(gè)元素,它都會(huì)用三個(gè)參數(shù)來調(diào)用傳入函數(shù),分別是元素本身,前一個(gè)元素,以及后一個(gè)元素。(當(dāng)沒有前一個(gè)元素或者后一個(gè)元素時(shí),使用?nil
?代替。)
> (map3 #'(lambda (&rest args) (princ args))
'(a b c d))
(A NIL B) (B A C) (C B D) (D C NIL)
NIL
和?mapc
?一樣,?map3
?總是返回?nil
?作為函數(shù)的返回值。需要這類例程的情況非常多。在下一個(gè)小節(jié)就會(huì)看到,這個(gè)例程是如何讓每個(gè)頁面都實(shí)現(xiàn)“前進(jìn)一頁”和“后退一頁”功能的。
map3
?的一個(gè)常見功能是,在列表的兩個(gè)相鄰元素之間進(jìn)行某些處理:
> (map3 #'(lambda (c p n)
(princ c)
(if n (princ " | ")))
'(a b c d))
A | B | C | D
NIL
程序員經(jīng)常會(huì)遇到上面的這類問題,但只要花些功夫,定義一些例程來處理它們,就能為后續(xù)工作節(jié)省不少時(shí)間。
一本書可以有任意數(shù)量的大章,每個(gè)大章又有任意數(shù)量的小節(jié),而每個(gè)小節(jié)又有任意數(shù)量的分節(jié),整本書的結(jié)構(gòu)呈現(xiàn)出一棵樹的形狀。
盡管網(wǎng)頁使用的術(shù)語和書本不同,但多個(gè)網(wǎng)頁同樣可以被組織成樹狀。
本節(jié)要構(gòu)建的是這樣一個(gè)程序,它生成多個(gè)網(wǎng)頁,這些網(wǎng)頁帶有以下結(jié)構(gòu): 第一頁是一個(gè)目錄,目錄中的鏈接指向各個(gè)節(jié)點(diǎn)(section)頁面。 每個(gè)節(jié)點(diǎn)包含一些指向項(xiàng)(item)的鏈接。 而一個(gè)項(xiàng)就是一個(gè)包含純文本的頁面。
除了頁面本身的鏈接以外,根據(jù)頁面在樹狀結(jié)構(gòu)中的位置,每個(gè)頁面都會(huì)帶有前進(jìn)、后退和向上的鏈接。 其中,前進(jìn)和后退鏈接用于在同級(jí)(sibling)頁面中進(jìn)行導(dǎo)航。 舉個(gè)例子,點(diǎn)擊一個(gè)項(xiàng)頁面中的前進(jìn)鏈接時(shí),如果這個(gè)項(xiàng)的同一個(gè)節(jié)點(diǎn)下還有下一個(gè)項(xiàng),那么就跳到這個(gè)新項(xiàng)的頁面里。 另一方面,向上鏈接將頁面跳轉(zhuǎn)到樹形結(jié)構(gòu)的上一層 —— 如果當(dāng)前頁面是項(xiàng)頁面,那么返回到節(jié)點(diǎn)頁面;如果當(dāng)前頁面是節(jié)點(diǎn)頁面,那么返回到目錄頁面。 最后,還會(huì)有索引頁面:這個(gè)頁面包含一系列鏈接,按字母順序排列所有項(xiàng)。
更多建議: