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)大小可以分成 elementsobjects 两种,以段落(paragraph)为基准,作用域比段落大(或者相同)的是 elements ,反之则是objects 。通俗地讲就是在日常写作中,能在段落里用的标记(如 *= 等记号)就是object ,必须要新起一行才能使用的(如代码块等)就是 element

elements

变量 org-element-all-elements 提供了完整的 elements 列表,其中的元素按作用范围可分为四个层级,从大到小分别是 :headingssectionsgreater elementsless-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

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

3

具体可以查看变量 org-element-all-objectsOrg 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)