关于lisp:Elisp-06缓冲区变换

43次阅读

共计 6692 个字符,预计需要花费 17 分钟才能阅读完成。

上一章:文本匹配

在第一章「缓冲区和文件」和第二章「文本解析」里已初步介绍了缓冲区的基本知识。应用 Elisp 语言编写文本处理程序时,充分利用缓冲区,仿佛是着实是在施展 Elisp 的一项短处。因此本章要思考和解决的一个事实问题是,缓冲区能够用来做什么。

文本变换

将文本由一种模式变换为另一种模式,在「物理」上,可体现为一个字符串变换为另一个字符串,也可体现为一个文件变换为另一个文件。这是其余编程语言里常见的想法,而 Elisp 语言提供了一个新的思维,文本变换能够体现为缓冲区变换。

为什么要进行文本变换呢?因为人类总心愿用更少的语言去讲更多的话。

例如,假如有一份文件 foo.md,其内容为

# 明天要整顿厨房

我在一份 Elisp 教程里揭示本人,明天肯定要整顿厨房……

当初我想将上述内容变换为

<h1> 明天要整顿厨房 </h1>

<p> 我在一份 Elisp 教程里揭示本人,明天肯定要整顿厨房……</p>

实现这样的变换,后面几章所述的 Elisp 语法和函数曾经足够用了。

算法设计

要解决上一节所述的文本变换问题,首先须要设计一个有针对性的算法。这个算法天然是很简略的,简略到了任何一本以传授算法为主旨的教科书都不愿波及的水平。

假如 x 为 foo.md 文件里的任意一行文本,对于上一节提出的问题额演,它只可能属于以下三种状况之一:

  1. ^#+[[:blank:]]+.+$
  2. ^[[:blank:]]*$
  3. 不属于上述两种状况的状况。

还记得上一章所讲的正则表达式吗?上述第一种状况,就是以一个或多个 # 结尾且 # 之后能够有一个或多个空格的文本行。第二种状况是空行。

只需基于上述三种状况,对 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 缓冲区切换之前,我已应用变量 textmd-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)))

正文完
 0