上一章:文本匹配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)))