#org-mode #parser #emacs #tree-structure #parse-tree #text-parser

starsector

以模块化和避免无关编辑为特色的 Org 模式结构解析/发射器。

4 个版本 (2 个稳定)

1.0.1 2022年10月2日
1.0.0 2022年9月18日
0.1.1 2021年12月13日
0.1.0 2021年12月4日

#6 in #org-mode

MIT 许可证

195KB
5K SLoC

Build Status Crates.io Document

简介

以模块化和避免无关编辑为特色的 Org 模式结构解析/发射器。

本库的目标是高效解析,并适度高效地编辑,以支持以下工作流程:解析大文件,进行少量修改,然后保存,而不在文件中产生虚假的差分。

特性

  • 快速的最小结构解析器,将文件分割成标题树。

  • 每个 UTF-8 字符串都是有效的输入,并且对于所有 UTF-8 字符串,emit(parse(text)) == text

  • 未经修改的标题将按输入的顺序输出,即使其他标题已更改。 (注意与换行符所在的节相关的边缘情况)。

  • 使用 headline-parser 标志添加标题(标签、关键字、优先级、计划等)的解析/生成器,它位于结构树之上。

  • 使用 orgize-integration 标志,使用 orgize 解析/生成属性抽屉。

  • 标题以文本形式在内存中表示,这使得解析和发射都非常快,并允许在文本偏移和每个标题之间进行双向映射,即使内存中的文档被修改,这些标题仍然有效。

  • Arena 分配器提供快速性能和精确的内存控制。

  • 使用 Ropey 的基于写时复制的文本存储。

  • 基于重新解析的编辑模型确保我们拥有的少量树不变性永远不会被破坏,内存中的格式不能表示无效状态,并且不允许可能会无意中更改树结构的编辑。例如,将节点文本更改为解析为多个节点的操作将被拒绝,但任何其他更改都是允许的。如果此类更改是希望得到的,则提供直接操作树结构的函数。

    这有助于将错误的影响限制在受影响的标题上,即使错误本身会导致添加可能被解析为标题的新文本。

限制

  • 仅解析 Org 模式的一小部分。

    我没有扩展此库的计划,除非可能添加对属性的本地解析而不是依赖 Orgize。我建议使用 orgize 解析节内容。

  • 由于节以文本形式存储,因此对标题的每次更改都需要重新解析整个标题。如果需要,提供构建器以批量处理此类更改。

  • 模糊测试产生了许多不引人注目的案例,其中Orgize和Starsector对相同的文本有不同的解析。虽然有一些逻辑来过滤已知差异,但模糊测试仍然相当嘈杂,需要手动检查来确定差异是否实际上是新的。尽管它仍然非常有用,但预计它不会产生任何违规。

入门指南

请参阅examples/edit.rs以获取解析和编辑的综合示例。

竞技场

文本使用rope存储,这允许与其他竞技场以及其他代码共享。这也允许高效地存储文档的多个版本。Section::clone_subtree在这里很有帮助。

API目前围绕IndexTree构建,以建模树结构。这意味着节点通过标识符而不是内容来引用其他节点,并且可以通过仅更改其中的节点来更改文档中的文本。给定竞技场可以存储多个文档,可以将其视为树的一种“构建器”。

因此,唯一可变的状态存储在Arena中。特定的节点使用类型Section引用(如果您熟悉IndexTree,这是一个围绕NodeId的包装器),它由树中的标识符组成。大多数函数都在Section上调用,并通过引用(或可变引用)接收其Arena

虽然我们可能内部重用IndexTree节点,但任何提供给客户端代码的Section都保证在竞技场存在期间保持有效。这意味着,例如,您可以从文档中删除一个Section,但它仍然有效,因此您可以在以后将其附加到另一个文档或同一文档的同一位置。

这意味着随着时间的推移,竞技场将积累节点。它们相当小,因此这不太可能成为问题,但对于经历许多编辑(基于inotify的重新解析等)的长期存在的竞技场,可能需要定期发出文本、创建新的竞技场并重新解析。如果这很麻烦,我们可以考虑添加一个便利函数——主要困难之处在于所有Sections都需要重新编号,因为保留现有编号将需要复制所有数据,从而抵消其目的。

分层解析

Org格式没有统一的规范。《草案规范》,org-element.el和Org模式命令在处理某些边缘情况时意见不一致。不同的Org模式命令甚至可能相互不一致。然而,在实践中,行为通常是一致的,在行为不一致的情况下,用户可能不会注意到或关心。请参阅Orgize问题跟踪器(打开和关闭)以获取示例。

而不是试图产生一个在所有边缘情况下都达成一致的解析树,该项目采用分层方法,包括整个文件的解析器、标题解析器和当前使用Orgize的属性解析器。Orgize也可以用来完全解析标题的内容。

Org模式本身不操作解析树。命令是针对原始文本编写的,这使得不同的命令可以以不同的方式解释语法。虽然这对解析器来说很令人沮丧,但这种方法确实提供了强大的抽象。Org模式是从文本编辑器发展而来的,从许多方面来看,其命令可以被视为高度专业化的编辑命令,这些命令由用户调用,可以考虑到上下文。这并非是我设计的方式,但我必须承认,它确实有一种优雅。

因此,我们采取类似的策略:将结构解析为文本块的树,并让客户端代码决定如何处理它。

结构解析器

结构解析器使用最少的语法将文件分割成标题的树。我们将每个标题称为包含以下行的“部分”:带有星号的行本身以及该行下面的所有文本,直到下一个标题(或文件末尾)。部分组织成树状结构,子标题作为其父部分的子元素。在文档的根处还有一个特殊的部分,它不对应于标题,级别为0。级别指的是标题中的星号数量。

语义被选择以尽可能接近Org模式。特别是,换行符仅指'\n',并且必须跟随星号的ASCII空格' '。尽管如此,应该完全支持Unicode,尽管对重要空白符有特定的解释。

任何部分的根可以由调用to_rope生成为Org文件。由于部分以文本形式存储,这仅按顺序遍历树并连接每个部分与换行符。这将产生与输入相同的文本,除了三个换行符边缘情况。

文档本身也可以生成为Org文件,但它通过存储来自原始解析的额外状态来处理这三个边缘情况,使得emit(parse(text)) == text对所有UTF-8输入。如果文档被修改,它将具有相同的三个边缘情况。

这种设计允许我们模拟文档中的所有边缘情况,这意味着标题可以自由添加、移动、删除和编辑,同时保持“仅连接文本块”的不变性。

部分以纯文本形式存储。可以使用set_levelset_raw直接修改文本,前提是这种修改不会破坏树的不变性。例如,从标题中删除星号仅在被新部分严格多于上级的部分时才允许。同样,通过编辑原始文本将文本更改成多个部分是不允许的,必须使用操作树结构的结构化编辑命令(如appendprepend等)。

这种限制是一个特性,因为它意味着操作部分的客户端代码不能导致其他部分发生变化,也不能在意外引入以星号开头的行时破坏树结构。这使得编写能够安全地频繁读取和写入大型或复杂Org模式文件的程序变得更容易,通过隔离它们的更改。

标题解析器

在大多数情况下,对原始文本的操作将不方便。我们经常只想在标题下的文本上操作,或者只想在标题本身上更改优先级、关键字、标签等。

当启用(默认)headline-parser功能标志时,标题编辑命令将可用。这些命令基于结构解析器构建,并逐个解析标题。每次调用访问器时,我们都会解析标题并返回它。要修改标题,我们解析标题,进行更改,将新的标题作为文本输出,然后替换该节文本。

我们选择这种方法,以便标题解析器不需要满足我们为整个文件提供的恒等不变性。此外,标题解析会带来许多边缘情况,这些情况在不同的实现之间各不相同,即使它们是一致的,正确处理也会很费劲。这种设计意味着客户端代码更改的标题将以标准化的方式解释和重新格式化,但只有修改后的标题才会受到影响。

作为便利,可以通过在节上调用set_keywordset_priority等来执行个别更改。还可以通过调用parse_headline来获取Headline。这是一个值类型,它提供了对标题属性(包括正文文本)的访问。调用to_builder提供了一个HeadlineBuilder,可以用于同时更改多个属性,然后在调用headline以构建新的标题之前使用它。然后可以在节上调用set_headline

与更改节原始文本一样,破坏树的不变性的编辑将失败。

属性解析器

使用orgize-integration功能标志(默认启用),在SectionHeadlineBuilder上可用获取和属性(在属性抽屉中)的功能。这些工作方式与标题解析器类似,但它们依赖于Orgize来解析标题。

我想像为标题和计划一样实现自己的解析器,因为我有潜力重新格式化整个标题。

未来计划

我想实现自己的属性抽屉解析,以便更好地集成。我没有计划复制任何其他Orgize功能。

对于编辑树的写时复制API很感兴趣,但我对之前的原型不满意。然而,Ropey似乎做得很好,所以这可以完成。

结构解析器的测试覆盖率相当高,标题解析器的覆盖率足够,但构建在其上的API可能需要更多的覆盖率(可能作为文档/示例的双重用途)。

依赖项

~1.8–3.5MB
~65K SLoC