第十六章:示例:生成 HTML

2018-02-24 15:51 更新

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

16.1 超文本標記語言 (HTML)

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

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

圖 16.2 一個網(wǎng)頁

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

<tag>...</tag>

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

另外一些成雙成對出現(xiàn)的標簽包括:創(chuàng)建帶編號列表的?<ol>?標簽(ol 代表 ordered list,有序表),令文本居中的?<center>?標簽,以及創(chuàng)建鏈接的?<a>?標簽(a 代表 anchor,錨點)。

被?<a>?和?</a>?包圍的文本就是超文本(hypertext)。 在大多數(shù)瀏覽器上,超文本都會以一種與眾不同的方式被凸顯出來 —— 它們通常會帶有下劃線 —— 并且點擊這些文本會讓瀏覽器跳轉(zhuǎn)到另一個頁面。 在標簽?a?之后的部分,指示了鏈接被點擊時,瀏覽器應(yīng)該跳轉(zhuǎn)到的位置。

一個像

<a href="foo.html">

這樣的標簽,就標識了一個指向另一個 HTML 文件的鏈接,其中這個 HTML 文件和當前網(wǎng)頁的文件夾相同。 當點擊這個鏈接時,瀏覽器就會獲取并顯示?foo.html?這個文件。

當然,鏈接并不一定都要指向相同文件夾下的 HTML 文件,實際上,一個鏈接可以指向互聯(lián)網(wǎng)的任何一個文件。

和成雙成對出現(xiàn)的標簽相反,另一種標簽沒有結(jié)束標記。 在圖 16.1 里有一些這樣的標簽,包括:創(chuàng)建一個新文本行的?<br>?標簽(br 代表 break ,斷行),以及在列表情景中,創(chuàng)建一個新列表項的?<li>?標簽(li 代表 list item ,列表項)。

HTML 還有不少其他的標簽,但是本章要用到的標簽,基本都包含在圖 16.1 里了。

16.2 HTML 實用函數(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 標簽生成例程

本節(jié)會定義一些生成 HTML 的例程。 圖 16.3 包含了三個基本的、生成標簽的例程。 所有例程都將它們的輸出發(fā)送到?*standard-output*?;可以通過重新綁定這個變量,將輸出重定向到一個文件。

宏?as?和?with?都用于在標簽之間生成表達式。其中?as?接受一個字符串,并將它打印在兩個標簽之間:

> (as center "The Missing Lambda")
<center>The Missing Lambda</center>
NIL

with?則接受一個代碼體(body of code),并將它放置在兩個標簽之間:

> (with center
    (princ "The Unbalanced Parenthesis"))
<center>
The Unbalanced Parenthesis
</center>
NIL

兩個宏都使用了?~(...~)?來進行格式化,從而將標簽轉(zhuǎn)化為小寫字母的標簽。 HTML 并不介意標簽是大寫還是小寫,但是在包含許許多多標簽的 HTML 文件中,小寫字母的標簽可讀性更好一些。

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

圖 16.3 中的最后一個例程?brs?用于創(chuàng)建多個文本行。 在很多瀏覽器中,這個例程都可以用于控制垂直間距。

(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 文件的例程。 第一個函數(shù)根據(jù)給定的符號(symbol)返回一個文件名。 在一個實際應(yīng)用中,這個函數(shù)可能會返回指向某個特定文件夾的路徑(path)。 目前來說,這個函數(shù)只是簡單地將?.html?后綴追加到給定符號名的后邊。

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

6.7 小節(jié)展示了如何臨時性地綁定一個特殊變量。 在 113 頁的例子中,我們在?let?的體內(nèi)將?*print-base*?綁定為?16?。 這一次,通過將?*standard-output*?和一個指向 HTML 文件的流綁定,只要我們在?page?的函數(shù)體內(nèi)調(diào)用?as?或者?princ?,輸出就會被傳送到 HTML 文件里。

page?宏的輸出先在頂部打印?title?,接著求值?body?中的表達式,打印?body?部分的輸出。

如果我們調(diào)用

(page 'paren "The Unbalanced Parenthesis"
  (princ "Something in his expression told her..."))

這會產(chǎn)生一個名為?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?標簽以外,以上輸出的所有 HTML 標簽在前面已經(jīng)見到過了。 被?<title>?標簽包圍的文本并不顯示在網(wǎng)頁之內(nèi),它們會顯示在瀏覽器窗口,用作頁面的標題。

(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)建一個指向 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?當中,這個函數(shù)接受一個字符串,并創(chuàng)建一個帶鏈接的列表項:

> (link-item 'bq "Backquote!")
<li><a href="bq.html">Backquote!</a>
"</a>"

最后,?button?也使用了?with-link?,從而創(chuàng)建一個被方括號包圍的鏈接:

> (button 'help "Help")
[ <a href="help.html">Help</a> ]
NIL

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

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

你可能會問,怎樣才能知道,什么時候應(yīng)該編寫主程序,什么時候又應(yīng)該編寫子例程?

實際上,這個問題,沒有答案。

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

(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 對樹進行迭代

圖 16.6 里定義的新例程是?mapc?的一個變種。它接受一個函數(shù)和一個列表作為參數(shù),對于傳入列表中的每個元素,它都會用三個參數(shù)來調(diào)用傳入函數(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ù)的返回值。需要這類例程的情況非常多。在下一個小節(jié)就會看到,這個例程是如何讓每個頁面都實現(xiàn)“前進一頁”和“后退一頁”功能的。

map3?的一個常見功能是,在列表的兩個相鄰元素之間進行某些處理:

> (map3 #'(lambda (c p n)
            (princ c)
            (if n (princ " | ")))
        '(a b c d))
A | B | C | D
NIL

程序員經(jīng)常會遇到上面的這類問題,但只要花些功夫,定義一些例程來處理它們,就能為后續(xù)工作節(jié)省不少時間。

16.4 生成頁面 (Generating Pages)

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

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

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

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

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

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號