是的,json.el 函数是递归的,但是在 Org-Element 上调用的递归函数导致堆栈溢出不是因为 org-element-parse-buffer 返回一个巨大的 AST,而是因为它返回一个 circular list。循环列表上的树递归函数就像笼子里的松鼠。
我想,在 AST 返回中使用自引用背后的想法是,如果您遍历它,您可以随时通过在关键字 :parent 上运行 plist-get 来返回父级。我想象这种用于上下遍历 AST 的用法:
(let ((xs '#1=(:text "foo" :child (:text "bar" :parent #1#))))
(plist-get
(plist-get
xs
:child) ; (:text "bar" :parent (:text "foo" :child #0))
:parent)) ; (:text "foo" :child (:text "bar" :parent #0))
但 JSON 不支持循环列表,因此您需要在尝试转换为任何数据序列化格式之前从 AST 中删除这些自引用。我还没有找到优雅地删除 AST 中的循环引用的方法,所以我求助于一个肮脏的 hack:
- 将 AST 转换为字符串
- 删除带有regular expressions 的引用
- 将字符串转换回 Elisp 数据结构
假设我有一个名为test.org 的组织文件,其内容如下:
* Heading
** Subheading
Text
然后变量tree 包含来自此缓冲区的已解析组织数据:(setq tree (with-current-buffer "test.org" (org-element-parse-buffer)))。然后为 JSON 导出准备这些数据,我只需运行:
(car (read-from-string (replace-regexp-in-string ":parent #[0-9]+?" "" (prin1-to-string tree)))))
即使删除了所有提到的:parent,新的AST仍然有效,所以如果新的AST在变量tree2中,那么以下3个表达式是等价的:
(org-element-interpret-data tree2)
(with-current-buffer "test.org" (buffer-substring-no-properties 1 (buffer-end 1)))
"* Heading\n** Subheading\nText\n"
请注意,由于某种原因,org-element-interpret-data 会删除前面的空格,因此当您的 Org 文件中有 text 之类的行时,上述内容在技术上并不正确。
现在您需要做的就是将新的非循环 AST 编码为 JSON 并将其写入文件:
(f-write (json-encode tree2) 'utf-8 "test.json")
注意事项
Elisp 的 cons cells 是一对 2 个插槽:car 和 cdr。如果每个单元格的cdr 包含指向另一个 cons 单元格的链接,我们将得到一个链表。如果car 和cdr 都指向 2 个值,我们会得到一个点对。因此(1 . (2 . (3 . nil))) 等价于(1 2 3)。但是cdr(或car)可能指向任何其他缺点单元格,包括已经在列表中较早的那个单元格,从而产生circular linked list。
练习: 创建一个复杂的树数据结构,其中包含对不同子树的多个自引用。然后尝试遍历这棵树并通过自引用跳跃来获得这个想法。
-
使用来自dash 列表操作库的->> 线程宏,表达式相当于:
(->> tree prin1-to-string (replace-regexp-in-string ":parent #[0-9]+?" "") read-from-string car)
(buffer-substring-no-properties 1 (buffer-end 1)) 类似于(buffer-string),但没有附加恼人的text properties。
f-write 是一个将文本从f 第三方文件操作库写入文件的函数。