org syntax 与 org-mode 字数统计

2024-03-15 2024-03-18History1390 字

本文简要介绍了 org 语法的组成和架构,并利用其 API 初步实现了一个比 count-words更精确的 org-mode 字数统计插件。

org syntax 简介

Org syntax , 顾名思义,即 Org 的语法。不像 Markdown 那样有各种各样的方言,Org 的语法只有一个,有着良好的一致性。

org syntax 的组成

org 组成部件按作用域(scope)大小可以分成 elements 和 objects 两种,以段落(paragraph)为基准,作用域比段落大(或者相同)的是 elements ,反之则是objects 。通俗地讲就是在日常写作中,能在段落里用的标记(如 *、= 等记号)就是object ,必须要新起一行才能使用的(如代码块等)就是 element 。

elements

变量 org-element-all-elements 提供了完整的 elements 列表,其中的元素按作用范围可分为四个层级,从大到小分别是 :headings、sections、greater elements 和 less-elements ,它们的关系如图 1 所示:

element-strafication.svg
Figure 1: element-strafication
headings

即 org 中的标题及其内容。

sections

sections 有如下两类:

  1. normal sections 一般来说,位于两个标题之间的内容都属于同一个 section
  2. the zeroth section 这是一个特殊的 section ,位于第一个标题前的所有元素都属于 the zeroth section 。
greater elements

greater elements 指那些能包含 less elements 或 greater elements 的元素,常见的 greater elements 如下:

Greater Blocks、Drawers and Property Drawers、Dynamic Blocks、Footnote Definitions、Inlinetasks、Items、Plain Lists、Property Drawers、Tables

lesser elements

lesser elements 是最小的元素,常见的 lesser elements 如下:

Blocks 、Clock、Diary Sexp、Planning、Comments、Fixed Width Areas、Horizontal Rules、Keywords、LaTeX Environments、Node Properties、Paragraphs、Table Rows

objects

大多数 objects 不能包含其他的 objects ,所以基本上可以认为是单独的一个元素。常见的 objects 如下:

Entities、LaTeX Fragments、Export Snippets、Footnote References、Citations、 Citation references、Inline Babel Calls、Inline Source Blocks、Line Breaks、 Links、Macros、Targets and Radio Targets、Statistics Cookies、Subscript and Superscript、Table Cells、Timestamps、Text Markup、Plain Text

org syntax API

org-element.el 用于解析 org 语法,提供了一系列有用的函数让我们处理 org syntax 对象。下面列举几个常用的函数:

org-element-parse-buffer
Recursively parse the buffer and return structure.
org-element-at-point
Determine closest element around point.
org-element-context
Return smallest element or object around point.
org-element-map
Map a function on selected elements or objects.
org-element-property
Extract the value from the PROPERTY of an ELEMENT.

字数统计

中文字数统计

Emacs 自带的 count-words 是不适用于 CJK 字符的,它不知道 CJK 中的一个 word 是如何定义的,所以会像英文那样,把一段连续的 CJK 字符统计为一个单词 ,导致最终得出的结果误差非常大。因此我们首先需要明确一下 word 的概念:在字数统计这方面,我们采用和 Word(或 WPS) 一样的方式:即一个 CJK 字符和一个英文单词一样都是一个 word 。

有了这个共识后我们来看一下 count-words 的代码:

 1: (defun count-words (start end &optional totals)
 2:   (interactive (list nil nil current-prefix-arg))
 3:   ;; When called from Lisp, return the data.
 4:   (if (not (called-interactively-p 'any))
 5:       (let ((words 0)
 6:             ;; Count across field boundaries. (Bug#41761)
 7:             (inhibit-field-text-motion t))
 8:         (save-excursion
 9:           (save-restriction
10:             (narrow-to-region start end)
11:             (goto-char (point-min))
12:             (while (forward-word-strictly 1)
13:               (setq words (1+ words)))))
14:         words)
15:     ;; When called interactively, message the data.
16:     (let ((totals (if (and totals
17:                            (or (use-region-p)
18:                                (buffer-narrowed-p)))
19:                       (save-restriction
20:                         (widen)
21:                         (count-words--format "; buffer in total"
22:                                              (point-min) (point-max)))
23:                     "")))
24:       (if (use-region-p)
25:           (message "%s%s" (count-words--format
26:                            "Region" (region-beginning) (region-end))
27:                    totals)
28:         (message "%s%s" (count-words--buffer-format) totals)))))

很明显,这段代码的核心就是第 12 行,它是利用了 forward-word-strictly 来遍历整个 buffer ,此函数默认采用英文的分词方式。而 forward-word-strictly 又不受 find-word-boundary-function-table 的影响,所以我们不能像 subword-mode 那样通过修改 find-word-boundary-function-table 来改变 forward-word 的行为。那么如何统计中文字数呢?最简单的方法就是直接把 forward-word-strictly 替换为我们自己的遍历函数:

 1: (defun eli/count-words (start end &optional totals)
 2:   (interactive (list nil nil current-prefix-arg))
 3:   ;; When called from Lisp, return the data.
 4:   (if (not (called-interactively-p 'any))
 5:       (let ((words 0)
 6:             ;; Count across field boundaries. (Bug#41761)
 7:             (inhibit-field-text-motion t))
 8:         (save-excursion
 9:           (save-restriction
10:             (narrow-to-region start end)
11:             (goto-char (point-min))
12:             (while (re-search-forward
13:                     "\\cc\\|[A-Za-z0-9][A-Za-z0-9[:punct:]]*" end t)
14:               (setq words (1+ words)))))
15:         words)
16:     ;; When called interactively, message the data.
17:     (let ((totals (if (and totals
18:                            (or (use-region-p)
19:                                (buffer-narrowed-p)))
20:                       (save-restriction
21:                         (widen)
22:                         (count-words--format "; buffer in total"
23:                                              (point-min) (point-max)))
24:                     "")))
25:       (if (use-region-p)
26:           (message "%s%s" (count-words--format
27:                            "Region" (region-beginning) (region-end))
28:                    totals)
29:         (message "%s%s" (count-words--buffer-format) totals)))))
第 12 行
这里简单替换了一下正则,现在的 word 计量单位是一个 CJK 字符或一个英文单词。

这样就可以正确统计中文字数了。

org-mode 中的字数统计

在 org-mode 中,情况又复杂很多。由第一节我们可以知道,一个 org 文件包含了各种各样的元素,其中有很多是我们不想在字数统计中算进去的,如各种 drawer 、代码块和一些 org 特有的语法。因此在 org-mode 中精确统计字数是一件非常困难的事(更不用说每个人对此的标准还不一样)。但话又说回来,我们在绝大多数情况下也不需要精确到个位数的字数统计,精确到百位就足够了。

那么如何在 org-mode 中只统计我们需要的部分呢?这就需要用到上文提到的 org syntax API 了。下面说一下大致的实现思路:

  1. 利用 org-element-parse-buffer 获取整个 buffer 的语法结构。
  2. 利用 org-element-map 来遍历步骤 1 中返回的结果,可以用 types参数来指定需要遍历的 elements 或 objects 。
  3. 对于不同的 element 或 object ,我们采用不同的计量方式。如处理 Regular links 时,只统计其description部分。

这样我们就可以比 count-words 更精确地统计字数了, 具体实现可以看 Elilif/org-count-words: Count words in org-mode.

Footnotes:

1

本节内容只是对 org syntax 术语和约定的简要概括,详细内容可以移步:Org Syntax 。

2

headings 和 greater elements 都可以包含自己类型或更低类型的元素。

3

具体可以查看变量 org-element-all-objects 和 Org Syntax 的 Objects 一节。

4

此变量用于查找 word 边界。

5

forward-word-strictly 的代码显示可以通过修改 word-move-empty-char-table 来影响其行为,但是我这里偷了一下懒,脑测觉得实现起来可能更复杂,就没有尝试。

6

至少对于我而言。

7

各函数的具体用法可以查阅相应文档。


Author: Eli Qian Email: eli.q.qian@gmail.com Create Date: 2024-03-15 Last modified: 2024-03-18 Creator: Emacs 29.2 (Org mode 9.6.15)