用 rx 写正则表达式,太舒服了!

最近在折腾博客,我使用的是 org-publish,我经常需要清空缓存后构建。

我目前会用 Git 跟踪构建后的 HTML,每次构建 org-publish 都会给 figure,details,pre 等元素添加一个随机的 id,于是总是有一大堆的 HTML 发生变化,我要找需要提交的内容会有点麻烦。

这些 id 对我来说是没用的,最好的情况是 org-publish 不要给我生成,但我目前找不到什么配置项。

所以我决定在文件转换成 HTML 的时候,用正则匹配,将这些不需要的 id 提前移除掉。

这是 org-publish 每次生成的 id,以及我移除之后的效果。

经过这么处理,Git 中就比较少出现 HTML 的变化了。

我对于 Emacs 中写正则表达式并不熟悉,尝试问 LLM,但给出来的执行不了。

在我之前看 info 时,我看到了 rx 这个包,它提供了一套可读性很好的语法用于构建 Emacs 中的正则表达式,于是我就自己尝试了一下。

这是我最终写出来的正则:

(rx
 (seq "<" ;; 匹配开头的 <
      (group (or "figure" "details" "pre")) ;; 匹配标签
      (group (zero-or-more (not ">"))) ;; 匹配 id 之前,标签之后的内容
      (group (seq whitespace "id=" (syntax string-quote) "org" (zero-or-more hex) (syntax string-quote))) ;; 匹配 id,形如 id="org123131"
      (group (zero-or-more (not ">"))) ;; 匹配 id 之后的内容
      ">")) ;; 匹配结尾的 >

执行正则后得到的正则时这样的: <\\(\\(?:details\\|\\(?:figu\\|p\\)re\\)\\)\\([^>]*\\)\\([[:space:]]id=\\s\"org[[:xdigit:]]*\\s\"\\)\\([^>]*\\)>

它的作用就是匹配形如 <details xxxx id="orgxxxx" xxxx> 的文字。

用 rx 写正则非常容易写,它提供了很多便利的方法,然后自己一点点慢慢构造就好了。

测试的话,可以利用 replace-regexp-in-string:

;; Happy hacking, spike - Emacs ♥ you!

(replace-regexp-in-string (rx
                           (seq "<"
                                (group (or "figure" "details" "pre"))
                                (group (zero-or-more (not ">")))
                                (group (seq whitespace "id=" (syntax string-quote) "org" (zero-or-more hex) (syntax string-quote)))
                                (group (zero-or-more (not ">")))
                                ">"))
                          (lambda (match)
                            (format "<%s%s%s%s>"
                                    (match-string 1 match) ;; tag
                                    (match-string 2 match) ;; keep other attrs
                                    "" ;; remove id
                                    (match-string 4 match) ;; keep other attrs
                                    ))
                         "测试文本")

就这样,如果你需要写正则的话,试试 rx 吧!

;; Happy hacking, Emacs :heart: you!

11 个赞

我也有这个需求,导致我都不怎么使用 org 内置的导出,而转而使用 pandoc 了。感谢这个 rx 的分享,看起来 org 内置的导出又可以使用了。

哈哈,可以翻一下 org export 的 info,里面有一节是讲 org export 的整个流程的,从里面找一个合适的钩子处理一下试试。

如果你需要一些参考代码的话,可以看看我目前的实现:

spike-leung/remove-unnessary-id-from-html
(defun spike-leung/remove-unnessary-id-from-html (text backend info)
  "Remove unnecessarily id attibute.
These elements's ID will be remove: figure,details,pre ..."
  (when (org-export-derived-backend-p backend 'html)
    (replace-regexp-in-string (rx (seq "<"
                                       (group (or "figure" "details" "pre"))
                                       (group (zero-or-more (not ">")))
                                       (group (seq whitespace "id=" (syntax string-quote) "org" (zero-or-more hex) (syntax string-quote)))
                                       (group (zero-or-more (not ">")))
                                       ">"))
                              (lambda (match)
                                (format "<%s%s%s%s>"
                                        (match-string 1 match) ;; tag
                                        (match-string 2 match) ;; keep other attrs
                                        "" ;; remove id
                                        (match-string 4 match) ;; keep other attrs
                                        ))
                              text)))

(with-eval-after-load 'ox
  (add-to-list 'org-export-filter-final-output-functions
               'spike-leung/remove-unnessary-id-from-html))

好奇问一下,您这个需求用 org-html-prefer-user-labels 如何,这样就可以固定使用非生成的 ID。

我之前套了个 ox-html-stable-ids.el 把随机 ID 全清掉了导致我都没发现除了标题之外的其它元素导出也有同样问题。目前我的解决办法主要是:

  1. 用 ox-html-stable-ids.el,此时会变成只有标题有纯英文 ID
  2. 用 Quail 反向提取中文拼音,此时标题 ID 就会可以用拼音来标识中文标题(比如:#html-biao-ti-id-sheng-cheng,链接里也有代码)
  3. 给 ox-html-stable-ids.el 加上 advice 以兼容其它元素的 ID(因为这个包似乎跳过了 html-prefer-user-labels 的处理,所以要用 #+name: 来引用就要 advice 来提取 :name代码

这样至少目前用起来是没有多余的 ID,并且所有 ID 都是人类可读且固定的。

2 个赞

org-html-prefer-user-labels 的文档描述时这样的:

When non-nil use user-defined names and ID over internal ones.

By default, Org generates its own internal ID values during HTML export. This process ensures that these values are unique and valid, but the keys are not available in advance of the export process, and not so readable.

When this variable is non-nil, Org will use NAME keyword, or the real name of the target to create the ID attribute.

Independently of this variable, however, CUSTOM_ID are always used as a reference.

我读下来的理解是,如果有 CUSTOM_ID 会优先使用(heading 我就是都设置了 CUSTOM_ID 避免变化),如果用户自己定义了 NAME,或者对应的元素有名字(“ or the real name of the target”) 就会用这个作为 ID。

这就意味着,我启用了这个配置,应该还需要给 figure、details 等设置一个 name?如果没有设置,还是会用默认的 org id 生成逻辑。

但是这些 figure 的 ID 我本来就不在意,我还需要额外想一个唯一的 name 设置上去,感觉有些麻烦(我也尝试过,但不知道是不是设置的语法不对,也没生效)

所以我还是倾向于直接移除 id :stuck_out_tongue:

1 个赞

Cool!

看了一下 ox-html-stable-ids.el,如果 heading 是中文的话,转换的 id 应该也是中文?所以你需要用 Quail 解析中文,获取拼音用作 id?

em…我目前不太在意 heading 这个 id 的可读性,只要是一个不会发生变化的唯一的 id 就好了。

我的做法是每次保存 org 文件,都给 heading 生成 ID 和 CUSTOM_ID 的 properties,这样在我导出的时候,就会用 CUSTOM_ID 作为 id 了。

我是通过 org-mode-hook 中的 before-save-hook 实现的:

相关代码
(defun spike-leung/org-add-custom-id-to-headings-in-files ()
  "Add a CUSTOM_ID property to all headings in the current buffer, if it does not already exist."
  (interactive)
  (org-map-entries
   (lambda ()
       (unless (org-entry-get nil "CUSTOM_ID")
         (let ((custom-id (org-id-new)))
           (org-set-property "CUSTOM_ID" custom-id))))))

(add-hook 'org-mode-hook
          (lambda ()
            (add-hook 'before-save-hook 'spike-leung/org-add-custom-id-to-headings-in-files nil 'local)))
2 个赞

正则扔给ai帮你写就好了,各种语法都可以,然后自己稍微验证下,改改,很省事

AI 很可能直接给我一个 <\\(\\(?:details\\|\\(?:figu\\|p\\)re\\)\\)\\([^>]*\\)\\([[:space:]]id=\\s\"org[[:xdigit:]]*\\s\"\\)\\([^>]*\\)>,也许能用,但实在不直观,后面我就不知道怎么维护了。

当然我知道 rx 的话,也可以让 AI 帮我用 rx 写,我最开始也让它写了一个,但或许是我用法不对,没有给它足够的文档,也可能是模型没选对,我只给了一段 prompt,它给到我的结果并不好。

另外,正则不复杂的时候,自己组装一下也蛮有趣的。 :winking_face_with_tongue:

ox-html-stable-ids.el 其实会把非 ASCII 范围的字符都给清掉,所以全中文标题的 ID 就会变成 -

(defun org-html-stable-ids--to-kebab-case (string)
  "Convert STRING to kebab-case."
  (string-trim
   (replace-regexp-in-string
    "[^a-z0-9]+" "-"
    (downcase string))
   "-" "-"))

另外我看了一下 ox-html-stable-ids.el 的代码,它是通过 advice org-export-get-reference 来控制 ID 的;似乎也可以用同样的方法来移除不需要的 ID?比如:

(defvar no-id-types '(special-block example-block))
(define-advice org-export-get-reference (:around (fun datum info))
  (unless (memq (org-element-type datum) no-id-types)
    (funcall fun datum info)))

感觉可行,以后试试,也是种不错的办法。目前用正则替换的方式能跑通先不折腾了。

(同一个问题,解决的方法有好多呀!)

1 个赞

之前魔改过,支持中文heading

1 个赞

不错,和上面 @Kana 分享的方法是类似的,覆盖了 org-export-get-reference 的实现,然后通过 advice 应用在 org-publish 上。

(你博客的 favicon 和 twitter 的新名字好像 :rofl:)