上一章:文本匹配html
在第一章「緩衝區和文件」和第二章「文本解析」裏已初步介紹了緩衝區的基本知識。使用 Elisp 語言編寫文本處理程序時,充分利用緩衝區,彷佛是着實是在發揮 Elisp 的一項長處。於是本章要思考和解決的一個現實問題是,緩衝區能夠用來作什麼。正則表達式
將文本由一種形式變換爲另外一種形式,在「物理」上,可體現爲一個字符串變換爲另外一個字符串,也可體現爲一個文件變換爲另外一個文件。這是其餘編程語言裏常見的想法,而 Elisp 語言提供了一個新的思想,文本變換能夠體現爲緩衝區變換。算法
爲何要進行文本變換呢?由於人類總但願用更少的語言去講更多的話。編程
例如,假設有一份文件 foo.md,其內容爲segmentfault
# 今天要整理廚房 我在一份 Elisp 教程裏提醒本身,今天必定要整理廚房……
如今我想將上述內容變換爲安全
<h1>今天要整理廚房</h1> <p>我在一份 Elisp 教程裏提醒本身,今天必定要整理廚房……</p>
完成這樣的變換,前面幾章所述的 Elisp 語法和函數已經足夠用了。markdown
要解決上一節所述的文本變換問題,首先須要設計一個有針對性的算法。這個算法天然是很簡單的,簡單到了任何一本以教授算法爲宗旨的教科書都不肯涉及的程度。編程語言
假設 x 爲 foo.md 文件裏的任意一行文本,對於上一節提出的問題額演,它只可能屬於如下三種狀況之一:函數
^#+[[:blank:]]+.+$
;^[[:blank:]]*$
;還記得上一章所講的正則表達式嗎?上述第一種狀況,就是以一個或多個 #
開頭且 #
以後能夠有一個或多個空格的文本行。第二種狀況是空行。編碼
只需基於上述三種狀況,對 x 進行變換。第一種狀況,將 x 變爲 <hn>...</hn>
的形式,n
爲 #
的個數。第二種狀況,將 x 變換爲空字符串。第三種狀況,則在 x 的開頭和結尾增長 <p>
和 </p>
。如此,問題便得以解決。下文將逐步實現這個算法。
爲一個具體的問題設計一個具體的算法,我認爲,這至關因而要站在一個高處對問題的俯瞰。算法設計出來以後,在着手實現算法時,我建議這個過程應當自下而上進行。由於底層的邏輯是最簡單的。
在實現「### foo
」到「<h3>foo</h3>
」的變換時,爲了追求簡單,我甚至能夠假設已經將前者拆分爲「###
」和「foo
」兩個部分了,而後只須要根據前者包含的了多少個 #
,即可以肯定 <hn>...</hn>
裏的 n
是多少了。因而,上一節裏第一種狀況的變換,其實現以下:
(defun section-n (level name) (let ((n (length level))) (format "<h%d>%s</h%d>" n name n)))
其中,Elisp 函數 length
函數在第二章裏已經用過,它能夠算出字符串包含多少個字符,也能夠算出列表包含多少個元素。Elisp 函數 format
是第一次使用,該函數能夠構造一個字符串模板,而後特定類型的變量或數據對象的求值結果填充到模板裏,從而生成一個字符串。若是學過 C 語言,對這種構造字符串的方法必定不陌生,由於 C 語言裏經常使用的 printf
即是相似的函數。
section-n
的用法示例以下:
(section-n "###" "#foo")
求值結果爲字符串 "<h3>foo</h3>"
。假若不放心,就使用以前章節裏定義的 princ\'
函數,將結果在終端裏顯現出來:
(princ\' (section-n "###" "#foo"))
這是最後一次如此羅嗦。
上一節的三種狀況裏,後兩種狀況對應的文本變換更爲簡單,下面直接給出,再也不講解了。
(defun empty-line (text) "") (defun paragraph (text) (format "<p>%s</p>" text))
如今,能夠打開 foo.md 文件,將其內容讀取到緩衝區了。所需代碼,在第二章便已給出,亦即
(find-file "foo.md")
find-file
過程結束後,當前緩衝區的名字是 foo.md
,其中存放的是 foo.md 文件的所有內容,且插入點位於緩衝區首部,亦即座標爲 1。
逐行讀取緩衝區內容的過程,一開始在第二章我是使用遞歸函數實現的,後來在第四章裏,將遞歸函數改寫成了迭代形式:
(defun current-line () (buffer-substring (line-beginning-position) (line-end-position))) (defun every-line () (while (< (point) (point-max)) (princ\' (current-line)) (forward-line 1)))
要實現對當前緩衝區內容的變換,可將文本匹配和變換過程嵌入上述的 every-line
函數的定義裏,可是我想作的更優雅一些。
首先,將文本匹配和變換過程定義爲一個函數
(defun translate (text) (if (string-match "^\\(#+\\)[[:blank:]]+\\(.+\\)$" text) (section-n (match-string 1 text) (match-string 2 text)) (if (string-match "^$" text) (empty-line text) (paragraph text))))
Elisp 並未提供相似其餘編程語言裏 if ... else if ... else
這種條件表達式,所以上述代碼是基於嵌套的 if ... else ...
表達式實現了三種狀況的文本匹配及變換。
不過,Elisp 提供了 cond
表達式,它的邏輯與 if ... else if ... else
等價,可用於消除 if ... else ...
表達式嵌套。cond
表達式的結構以下:
(cond (邏輯表達式 1 程序分支 1) (邏輯表達式 2 程序分支 2) (... ...))
基於 cond
表達式,可將 translate
函數從新定義爲:
(defun translate (text) (cond ((string-match "^\\(#+\\)[[:blank:]]+\\(.+\\)$" text) (section-n (match-string 1 text) (match-string 2 text))) ((string-match "^$" text) (empty-line text)) (t (paragraph text))))
而後在 every-line
函數裏調用 translate
即可對緩衝區內容逐行予以變換,即
(defun every-line () (while (< (point) (point-max)) (translate (current-line)) (forward-line 1)))
假若在 every-line
函數的定義裏,使用 princ\'
將文本變換結果逐行輸出到終端,能夠查看變換過程是否正確。例如
(defun every-line () (while (< (point) (point-max)) (princ\' (translate (current-line))) (forward-line 1)))
可是,若是我想將變換後的文本保存到另外一個緩衝區裏,該如何實現呢?
首先,確定是建立一個新的緩衝區,它能夠叫 html
,且可與符號 html-buffer
綁定,成爲一個變量的值。我將這件事放在 foo.md 文件被打開以後進行,亦即
(find-file "foo.md") (setq html-buffer (generate-new-buffer "html"))
而後在 every-line
函數裏,將當前緩衝區切換爲 other
緩衝區,插入變換後的文本,再將當前緩衝區切回,繼續進行下一行文本的變換和保存。因而,every-line
函數定義裏的迭代過程可描述爲
(while (< (point) (point-max)) (setq text (translate (current-line))) (setq md-buffer (current-buffer)) (set-buffer html-buffer) (insert (concat text "\n")) (set-buffer md-buffer) (forward-line 1))
上述代碼使用了 Elisp 函數 concat
,它能夠將多個字符串鏈接成一個字符串。
在上述代碼裏,當前緩衝區每次向 html-buffer
緩衝區切換以前,我已使用變量 text
和 md-buffer
已分別將變換後的文本以及當前緩衝區記了下來,故而在 html-buffer
爲當前緩衝區時,可以插入 text
的值,且能經過 (set-buffer md-buffer)
將當前緩衝區切回。因爲這樣的緩衝區切換操做較爲繁瑣,所以 Elisp 提供了一個更方便的函數 with-current-buffer
,可在維持當前緩衝區不變的狀況下,將數據寫入另外一個給定的緩衝區。該函數的用法以下:
(with-current-buffer 緩衝區或緩衝區的名字 一組表達式)
基於這個函數,上述迭代過程可改寫爲
(while (< (point) (point-max)) (setq text (translate (current-line))) (with-current-buffer html-buffer (insert (concat text "\n"))) (forward-line 1))
不過,上述代碼裏定義了一個全局變量 text
,不夠安全,可以使用 let
表達式將其變爲局部變量:
(let (text) (while (< (point) (point-max)) (setq text (translate (current-line))) (with-current-buffer html-buffer (insert text) (insert "\n")) (forward-line 1))))
可是,不幸的是,上述代碼裏還有一個全局變量 html-buffer
,它憑空就出現了,就像神蹟同樣。
真的有神蹟嗎?從函數的角度來看,這個神蹟徹底能夠轉化爲一個參數,因而,就有了一個可將當前緩衝區內容逐行變換到另外一個緩衝區的函數了,即
(defun every-line-in-current-buffer-to-other-buffer (target) (let (text) (while (< (point) (point-max)) (setq text (translate (current-line))) (with-current-buffer target (insert (concat text "\n")) (forward-line 1))))
上一節末尾定義的那個函數,它的名字太長了。任何很長的名字,均可以經過修辭將其變得簡短。修辭的基礎是在宏觀的角度上理解待修辭的對象。站在宏觀的角度來看這個函數,不管它是怎樣運做的,它的工做無非是將一個緩衝區裏的東西變換到另外一個緩衝區,那麼可將這個過程修辭爲緩衝區變換,用英文來寫,可表示爲 translate-buffer
,不管它是將當前緩衝區內容變換到另外一個緩衝區,仍是將任意一個給定的緩衝區內容變換到另外一個緩衝區,這樣的過程皆可定義爲
(defun translate-buffer (source target) (with-current-buffer source (let (text) (while (< (point) (point-max)) (setq text (translate (current-line))) (with-current-buffer target (insert (concat text "\n")) (forward-line 1)))))
基於 translate-buffer
,將緩衝區 foo.md 中的內容變換另外一個緩衝區的完整示例可寫爲:
(find-file "foo.md") (setq html-buffer (generate-new-buffer "html")) (translate-buffer ((current-buffer) html-buffer))
基於 let
表達式,能夠消除掉全局變量 html-buffer
而且可將程序進一步簡化,例如
(let ((html-buffer (generate-new-buffer "html"))) (translate-buffer (find-file "foo.md") html-buffer))
沒錯,(find-file "foo.md")
的求值結果是緩衝區,所以它能夠做爲 translate-buffer
的參數值。
假若在完成緩衝區變換後,想查看緩衝區 html-buffer 的內容,能夠再使用一次 with-current-buffer
表達式,即
(let ((html-buffer (generate-new-buffer "html"))) (translate-buffer (find-file "foo.md") html-buffer) (with-current-buffer html-buffer (princ\' (buffer-string))))
也可將 html-buffer 的內容保存爲文件 foo.html,還記得第二章提到的 write-file
函數嗎?可是,不推薦使用它,由於它是面向 Emacs 圖形界面的,工做比較多,致使運行起來有些慢吞吞的。比它更快且更爲底層的函數是 write-region
,它能夠經過第一個參數和第二個參數,將當前緩衝區的一個局部區域寫入指定文件。假若 write-region
的第一個參數爲 nil
,那麼不管第二個參數值是什麼,它會將當前緩衝區的所有內容寫入指定文件。
如下代碼實現了緩衝區變換和文件保存過程:
(let ((html-buffer (generate-new-buffer "html"))) (translate-buffer (find-file "foo.md") html-buffer) (with-current-buffer html-buffer (write-region nil nil "foo.html")))
緩衝區也許是 Elisp 語言裏也許是最爲重要的數據類型了。雖然 Elisp 沒有 Scheme 語言的 call/cc,可是它有 with-current-buffer。我甚至隱約以爲,用 Elisp 語言編程,基於緩衝區類型,能夠開闢一個其餘編程語言所沒有的範式,面向緩衝區編程。
在本章示例裏,要編譯的 Markdown 文件以及做爲編譯結果的 HTML 文件,它們都是硬編碼到程序裏的。下一章,我要讓程序可以經過命令行參數傳遞文件的名字。
下一章:命令行界面
可將 foo.md 變換爲 foo.html 的完整代碼以下:
(defun section-n (level name) (let ((n (length level))) (format "<h%d>%s</h%d>" n name n))) (defun empty-line (text) "") (defun paragraph (text) (format "<p>%s</p>" text)) (defun translate (text) (cond ((string-match "^\\(#+\\)[[:blank:]]+\\(.+\\)$" text) (section-n (match-string 1 text) (match-string 2 text))) ((string-match "^$" text) (empty-line text)) (t (paragraph text)))) (defun current-line () (buffer-substring (line-beginning-position) (line-end-position))) (defun translate-buffer (source target) (with-current-buffer source (let (text) (while (< (point) (point-max)) (setq text (translate (current-line))) (with-current-buffer target (insert (concat text "\n")) (forward-line 1))))) (let ((html-buffer (generate-new-buffer "html"))) (translate-buffer (find-file input) html-buffer) (with-current-buffer html-buffer (write-region nil nil output)))