org syntax 与 org-mode 字数统计
本文简要介绍了 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 所示:
headings
即 org 中的标题及其内容。
sections
sections 有如下两类:
- normal sections 一般来说,位于两个标题之间的内容都属于同一个 section
- 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 了。下面说一下大致的实现思路:
- 利用
org-element-parse-buffer
获取整个 buffer 的语法结构。 - 利用
org-element-map
来遍历步骤 1 中返回的结果,可以用types
参数来指定需要遍历的 elements 或 objects 。 - 对于不同的 element 或 object ,我们采用不同的计量方式。如处理
Regular links
时,只统计其description
部分。
这样我们就可以比 count-words
更精确地统计字数了, 具体实现可以看
Elilif/org-count-words: Count words in org-mode.
Footnotes:
本节内容只是对 org syntax 术语和约定的简要概括,详细内容可以移步:Org Syntax 。
headings
和 greater elements
都可以包含自己类型或更低类型的元素。
具体可以查看变量 org-element-all-objects
和 Org Syntax 的 Objects 一节。
此变量用于查找 word
边界。
forward-word-strictly
的代码显示可以通过修改 word-move-empty-char-table
来影响其行为,但是我这里偷了一下懒,脑测觉得实现起来可能更复杂,就没有尝试。
至少对于我而言。
各函数的具体用法可以查阅相应文档。