第十六章:示例:生成 HTML

2018-02-24 15:51 更新

本章的目標(biāo)是完成一個(gè)簡(jiǎn)單的 HTML 生成器 —— 這個(gè)程序可以自動(dòng)生成一系列包含超文本鏈接的網(wǎng)頁(yè)。除了介紹特定 Lisp 技術(shù)之外,本章還是一個(gè)典型的自底向上編程(bottom-up programming)的例子。 我們以一些通用 HTML 實(shí)用函數(shù)作為開(kāi)始,繼而將這些例程看作是一門(mén)編程語(yǔ)言,從而更好地編寫(xiě)這個(gè)生成器。

16.1 超文本標(biāo)記語(yǔ)言 (HTML)

HTML (HyperText Markup Language,超文本標(biāo)記語(yǔ)言)用于構(gòu)建網(wǎng)頁(yè),是一種簡(jiǎn)單、易學(xué)的語(yǔ)言。本節(jié)就對(duì)這種語(yǔ)言作概括性介紹。

當(dāng)你使用網(wǎng)頁(yè)瀏覽器閱覽網(wǎng)頁(yè)時(shí),瀏覽器從遠(yuǎn)程服務(wù)器獲取 HTML 文件,并將它們顯示在你的屏幕上。每個(gè) HTML 文件都包含任意多個(gè)標(biāo)簽(tag),這些標(biāo)簽相當(dāng)于發(fā)送給瀏覽器的指令。

圖 16.2 一個(gè)網(wǎng)頁(yè)

注意在尖角括號(hào)之間的文本并沒(méi)有被顯示出來(lái),這些用尖角括號(hào)包圍的文本就是標(biāo)簽。 HTML 的標(biāo)簽分為兩種,一種是成雙成對(duì)地出現(xiàn)的:

<tag>...</tag>

第一個(gè)標(biāo)簽標(biāo)志著某種情景(environment)的開(kāi)始,而第二個(gè)標(biāo)簽標(biāo)志著這種情景的結(jié)束。 這種標(biāo)簽的一個(gè)例子是?<h2>?:所有被<h2>?和?</h2>?包圍的文本,都會(huì)使用比平常字體尺寸稍大的字體來(lái)顯示。

另外一些成雙成對(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ì)以一種與眾不同的方式被凸顯出來(lái) —— 它們通常會(huì)帶有下劃線 —— 并且點(diǎn)擊這些文本會(huì)讓瀏覽器跳轉(zhuǎn)到另一個(gè)頁(yè)面。 在標(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)頁(yè)的文件夾相同。 當(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)簽沒(méi)有結(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 里了。

16.2 HTML 實(shí)用函數(shù) (HTML Utilities)

(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*?;可以通過(guò)重新綁定這個(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è)宏都使用了?~(...~)?來(lái)進(jìn)行格式化,從而將標(biāo)簽轉(zhuǎn)化為小寫(xiě)字母的標(biāo)簽。 HTML 并不介意標(biāo)簽是大寫(xiě)還是小寫(xiě),但是在包含許許多多標(biāo)簽的 HTML 文件中,小寫(xiě)字母的標(biāo)簽可讀性更好一些。

除此之外,?as?傾向于將所有輸出都放在同一行,而?with?則將標(biāo)簽和內(nèi)容都放在不同的行里。 (使用?~&?來(lái)進(jìn)行格式化,以確保輸出從一個(gè)新行中開(kāi)始。) 以上這些工作都只是為了讓 HTML 更具可讀性,實(shí)際上,標(biāo)簽之外的空白并不影響頁(yè)面的顯示方式。

圖 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)。 目前來(lái)說(shuō),這個(gè)函數(shù)只是簡(jiǎn)單地將?.html?后綴追加到給定符號(hào)名的后邊。

宏?page?負(fù)責(zé)生成整個(gè)頁(yè)面,它的實(shí)現(xiàn)和?with-open-file?很相似:?body?中的表達(dá)式會(huì)被求值,求值的結(jié)果通過(guò)?*standard-output*?所綁定的流,最終被寫(xiě)入到相應(yīng)的 HTML 文件中。

6.7 小節(jié)展示了如何臨時(shí)性地綁定一個(gè)特殊變量。 在 113 頁(yè)的例子中,我們?cè)?let?的體內(nèi)將?*print-base*?綁定為?16?。 這一次,通過(guò)將?*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)見(jiàn)到過(guò)了。 被?<title>?標(biāo)簽包圍的文本并不顯示在網(wǎng)頁(yè)之內(nèi),它們會(huì)顯示在瀏覽器窗口,用作頁(yè)面的標(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)部的文本,則通過(guò)求值?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

16.3 迭代式實(shí)用函數(shù) (An Iteration Utility)

在這一節(jié),我們先暫停一下編寫(xiě) HTML 生成器的工作,轉(zhuǎn)到編寫(xiě)迭代式例程的工作上來(lái)。

你可能會(huì)問(wèn),怎樣才能知道,什么時(shí)候應(yīng)該編寫(xiě)主程序,什么時(shí)候又應(yīng)該編寫(xiě)子例程?

實(shí)際上,這個(gè)問(wèn)題,沒(méi)有答案。

通常情況下,你總是先開(kāi)始寫(xiě)一個(gè)程序,然后發(fā)現(xiàn)需要寫(xiě)一個(gè)新的例程,于是你轉(zhuǎn)而去編寫(xiě)新例程,完成它,接著再回過(guò)頭去編寫(xiě)原來(lái)的程序。 時(shí)間關(guān)系,要在這里演示這個(gè)開(kāi)始-完成-又再開(kāi)始的過(guò)程是不太可能的,這里只展示這個(gè)迭代式例程的最終形態(tài),需要注意的是,這個(gè)程序的編寫(xiě)并不如想象中的那么簡(jiǎn)單。 程序通常需要經(jīng)歷多次重寫(xiě),才會(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ì)樹(shù)進(jìn)行迭代

圖 16.6 里定義的新例程是?mapc?的一個(gè)變種。它接受一個(gè)函數(shù)和一個(gè)列表作為參數(shù),對(duì)于傳入列表中的每個(gè)元素,它都會(huì)用三個(gè)參數(shù)來(lái)調(diào)用傳入函數(shù),分別是元素本身,前一個(gè)元素,以及后一個(gè)元素。(當(dāng)沒(méi)有前一個(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è)頁(yè)面都實(shí)現(xiàn)“前進(jìn)一頁(yè)”和“后退一頁(yè)”功能的。

map3?的一個(gè)常見(jiàn)功能是,在列表的兩個(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ì)遇到上面的這類問(wèn)題,但只要花些功夫,定義一些例程來(lái)處理它們,就能為后續(xù)工作節(jié)省不少時(shí)間。

16.4 生成頁(yè)面 (Generating Pages)

一本書(shū)可以有任意數(shù)量的大章,每個(gè)大章又有任意數(shù)量的小節(jié),而每個(gè)小節(jié)又有任意數(shù)量的分節(jié),整本書(shū)的結(jié)構(gòu)呈現(xiàn)出一棵樹(shù)的形狀。

盡管網(wǎng)頁(yè)使用的術(shù)語(yǔ)和書(shū)本不同,但多個(gè)網(wǎng)頁(yè)同樣可以被組織成樹(shù)狀。

本節(jié)要構(gòu)建的是這樣一個(gè)程序,它生成多個(gè)網(wǎng)頁(yè),這些網(wǎng)頁(yè)帶有以下結(jié)構(gòu): 第一頁(yè)是一個(gè)目錄,目錄中的鏈接指向各個(gè)節(jié)點(diǎn)(section)頁(yè)面。 每個(gè)節(jié)點(diǎn)包含一些指向項(xiàng)(item)的鏈接。 而一個(gè)項(xiàng)就是一個(gè)包含純文本的頁(yè)面。

除了頁(yè)面本身的鏈接以外,根據(jù)頁(yè)面在樹(shù)狀結(jié)構(gòu)中的位置,每個(gè)頁(yè)面都會(huì)帶有前進(jìn)、后退和向上的鏈接。 其中,前進(jìn)和后退鏈接用于在同級(jí)(sibling)頁(yè)面中進(jìn)行導(dǎo)航。 舉個(gè)例子,點(diǎn)擊一個(gè)項(xiàng)頁(yè)面中的前進(jìn)鏈接時(shí),如果這個(gè)項(xiàng)的同一個(gè)節(jié)點(diǎn)下還有下一個(gè)項(xiàng),那么就跳到這個(gè)新項(xiàng)的頁(yè)面里。 另一方面,向上鏈接將頁(yè)面跳轉(zhuǎn)到樹(shù)形結(jié)構(gòu)的上一層 —— 如果當(dāng)前頁(yè)面是項(xiàng)頁(yè)面,那么返回到節(jié)點(diǎn)頁(yè)面;如果當(dāng)前頁(yè)面是節(jié)點(diǎn)頁(yè)面,那么返回到目錄頁(yè)面。 最后,還會(huì)有索引頁(yè)面:這個(gè)頁(yè)面包含一系列鏈接,按字母順序排列所有項(xiàng)。

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)