上下文
AsciiDoc语言中的内联语法从未有过正式的语法规范,最初也不是设计为要有一个。内联语法是指非逐字叶节点(例如,段落、标题、标题、块内容属性等)内容中的标记。我们在这里将这些内容称为常规文本。
AsciiDoc中的内联标记目前是通过对常规文本进行多次替换来处理的。这些替换实际上决定了该文本如何被解释。应用于常规文本的替换类型依次是:specialchars(特殊字符)、attributes(属性)、quotes(引用)、replacements(替代)、macros(宏)和post_replacements(后替代)。
尽管用于识别内联语法的替换操作中使用的正则表达式十分复杂,但这种方法不过是一系列搜索和替换操作的集合。这些操作在转换阶段逐步将文本从AsciiDoc格式转换为输出格式。这种方法与在解析阶段构建一个完整的结构化表示(即,解析树)形成了对比。
由于缺乏正式的语法规范,内联语法在很大程度上取决于实现定义——或者至少是实现倾向性的。此外,依赖替代来处理内联语法已经导致内联语法成为语言中最模糊、最脆弱和最依赖内容的方面。虽然我们最初计划在规范的第一个版本中坚持使用替代方法,但进一步分析揭示了这种方法留下了严重的问题未解决:
-
源代码在应用替换的同时发生了变化。这种方法本质上使得内联语法的定义变得模糊,因为每一次的替换都是在略有不同的源代码上操作的,这种源代码是输入和转换器产出的输出的混合体。换句话说,输出实际上影响了解析过程。
-
处理过程与转换器相耦合,而转换器目前尚未被指定。
-
解析阶段在转换阶段开始之前并未完成,这让实现生成一个用于验证的抽象语法树变得困难。
-
正则表达式的替换在技术规范中描述起来很复杂。此外,由于它们的复杂性,所需的正则表达式在不同语言运行时之间并不可移植,这不符合规范的可移植性目标。
如果此规范现在就以一系列替换的形式描述内联语法,我们担心这些问题将严重削弱规范,且将变得如此根深蒂固于语言中,以至于在未来版本中无法纠正。由于内联语法在整个语言中反复出现,这绝不是一个可以接受的情况。远离替换的转变比我们原先预期的更为迫切和重要。同样值得注意的是,关于规范化内联语法的请求可以追溯到十年前。
AsciiDoc中内联语法的当前处理方式使其成为语言中最容易引起歧义的方面之一。该规范提供了一个独特的机会来解决这个长期存在的问题。这种转变将使内联语法更易于理解、解析和教授。
被接受的决定
为了明确无误地指定AsciiDoc语言,我们决定从用替换描述内联语法转变为使用正式语法定义它——这是解析器生成器的输入。
决策摘要
必要性
使用正则表达式进行搜索和替换会留下太多的模糊性和特殊性。它也缺乏足够的上下文来考虑无数的排列组合(例如,限制性和嵌套的标记、反斜杠转义等)。因此,对内联标记的解释往往对写作者来说是出乎意料或者令人惊讶的。我们看到许多文件中,作者不得不诉诸于权宜之计或防御性标记来应对这些技巧性问题。一个强有力的规范意味着有一个稳定的内联语法定义。
我们需要的是:
-
为内联语法定义一种形式语法,可在规范中记录下来。
-
能够独立于输出格式产生一个完整的解析树,以验证实现的行为。
-
将解析和转换解耦;解析不应依赖于转换;输出不应影响解析
-
为了能够控制语法规则的应用范围,避免文本的意外解读
-
为了以可靠的方式支持受限和嵌套的语法
正式语法是在规范中描述AsciiDoc内联语法的唯一合理且可移植的方式。
福利
拥有一个包含内联语法的正式语法体系的重要性不容小觑。
-
因为解析会生成一个完整的解析树,进而产生一个包含内联节点的抽象语法树(ASG),所以验证实现的一致性变得可行。
-
它将语言的解析与输出格式分离,这对于保持在本规范的范围内至关重要。这种分离还避免了由于解析部分转换的输出或急切地转换某些元素而引入的特殊性。在pre-spec AsciiDoc中,由于在解析过程中转换元素,常常会有顺序或状态问题导致古怪的行为发生。
-
拥有一个完整的解析树可以使工具完全分析文档的结构并从中提取信息,一直到内联节点及其内容的层面。
-
一个完整的解析树能提高诊断消息的准确性,这些消息包括在出问题时精确的源位置。
某些先前非常复杂的语法方面变得简单多了。例如,AsciiDoc 可以增强以支持词法反斜杠转义(即,反斜杠转义保留字符和形式而不是上下文)。语法规则将自然嵌套,避免如何解释边界的歧义或迫使作者诉诸于技巧。 借助形式化语法,解析变得更加精确,能够自然地处理嵌套语法和非解释跨度等情形,解决了以前许多含糊不清的场景。
策略
我们决定使用PEG(解析表达式语法)来描述AsciiDoc的语法。使用这种形式主义进行内联解析并没有我们最初认为的那么困难。识别和隔离内联预处理器使其变得更简单,只是额外努力追踪偏移量的副作用。最重要的是,我们确定可以合理地维护兼容性。
AsciiDoc的内联语法很适合PEG语法,因为它天生就是递归的,这主要是由于输出格式(DocBook、HTML等)的限制。我们发现作家在写AsciiDoc时自然而然地遵循了这些限制。因此,我们可以自信地说,使用PEG完全可以准确描述AsciiDoc的内联语法。更重要的是,可以从该语法高效地解析内联语法(无论是否使用packrat解析)。
重新映射替代
转换到正式文法的挑战之一是弄清楚如何在文法中考虑每种替代类型。我们发现,并非所有替代类型都遵循内联结构。相反,他们在该结构之外工作,作为一个预处理器或后处理器。我们认识到需要引入一个内联预处理器,并依靠对解析结构的一些后处理。
这是替代类型如何映射到正式语法的方法:
-
specialchars - 一个对字符串节点进行的后处理操作,由转换器应用,因此不在抽象语法树中表示。
-
属性 - 由内联预处理器处理,它将每个属性引用替换为它所引用的属性的值
-
引号标记的跨度在语法中通过有序选择规则处理
-
替换 - 在语法中使用有序选择规则处理的排版提示
-
宏
-
内联直通 - 内联预处理器处理的受保护跨度,在解析期间将直通替换为占位符;这些占位符被语法中的规则匹配,该规则恢复原始文本而不进一步解析它。
-
普通宏和简写 - 在语法中使用有序选择规则处理的标记
-
-
post_replacements - 在语法中使用有序选择规则处理的标记替换
大多数替换可以清晰地映射到语法的有序选择上。显而易见的例外是特殊字符、属性和内联直通。
特殊字符
特殊字符(“special characters”)替换一直是AsciiDoc语法中的一个主要异常现象。第一个问题是它偏向于像HTML这样的SGML输出格式的需求。虽然它为该系列的输出格式编码正确的字符,但对于其他输出格式(如groff),它不必要地使用错误的语法编码这些字符。所以特殊字符的定义并不是普遍的。更重要的是,它改变了源代码,以至于替换必须考虑到语法中这些编码的字符。
在这个阶段没有必要进行特殊字符的替换。这些字符不是内联语法的一部分。转换器应该在转换过程中根据输出格式的要求对字符串节点中的特殊字符进行编码。
内联预处理器
将特殊字符排除在外后,下一组需要处理的语法是影响内联结构但不受其限制的语法。该语法包括属性引用(即属性)和内联直通。
如果我们考虑属性引用,解析后的值是解析器需要操作的文本。这个解析后的文本可能会根本改变解析器所看到的内容,从而可能改变结构。例如,一个URL宏可能以属性引用开始。解析器需要看到由属性引用添加的URL,而不是属性引用本身。这与行处理器处理的预处理指令情况非常相似。因此,我们必须将属性引用视为属于预处理步骤。其结果是,这实际上颠倒了引号和属性替换的顺序,这样引号(标记的跨度)将在插入文本时在属性的值中被找到和解析。
内联直通是相同的。但与替换文本不同,内联直通标记了内联解析应该跳过的文本区域(即,非解释区域)。这些受保护的区域也可以包含应当保持未解释的属性引用。由于内联预处理器在内联直通位置留下了占位符,这些占位符必须与内联语法相匹配,并且规则动作必须恢复受保护的文本。
因此我们可以得出结论,属性引用和内联直通均是内联预处理器的一部分,该预处理器操作在内联解析开始之前对常规文本进行。内联预处理器需要在文本发生变化时追踪源代码中的偏移量,以确保内联节点的位置保持准确。
相关话题
我们已经能够将内联语法的大多数方面建模为一种正式语法,但仍有一些话题需要进一步研究。
- 反斜杠转义
-
通过切换到正式的语法体系,我们希望能够使反斜杠转义变得稳定和可靠。目前的提案是使用词法反斜线转义,它会处理任何符号(包括反斜线)或宏前缀前的反斜线。然而,反斜杠用于转义块级语法的用法,仍然需要梳理和解决。
- 内联宏上的边界框属性列表
-
在块级别,装箱的 attrlist 总是在一行的末尾终止。在线内语法中,内联宏上的装箱 attrlist 可以在行中的任何位置终止。如何识别该闭合括号仍然是个问题。一种方法是贪婪地匹配它,然后处理箱内的 attrlist(或内容)。这将避免 attrlist 中的内联标记掩盖闭合括号。但是,这需要使用更多进阶的解析功能(如语义断言)来无条件地匹配闭合括号。另一种方法是将 attrlist 解析为宏规则的一部分。然而,这将允许偏离当前 AsciiDoc 工作方式的行为。这个决定仍然需要解决。
- 替补在传球宏上。
-
目前的 pass 宏允许指定自定义的替代文本。由于我们要去掉替代功能,我们需要找出如何将这种意图映射到结构化解析中。我们可能能够通过在行内预处理器中进行额外处理和/或在输入中插入转义字符来支持常见的替代组合。这里仍然需要更多的研究来解决这将如何工作。
向后兼容
这个决定带来了一个完全不同的心智模型,用以解读文本,但这并不意味着放弃兼容性。尽管当前内联语法并没有使用正式的语法来定义,但那种语法一直存在于作者对内联结构的认知中。我们的目标是提炼出这样的语法,以便常规文本的解读方式能够与这种认知相匹配,并且,因此,允许处理器尽可能地匹配当前的输出。在行为上有所不同的地方,它很可能以一种更接近作者期望的方式有所区别,因而是一个受欢迎的变化。在无法匹配当前行为的情况下,文本应该以一种不丢失任何信息的方式来解读。
转换到形式化语法确实需要摒弃现有的替换顺序和在转换阶段对内联标记的解释。这将影响到语言的两个方面。首先,属性引用将能够引入包含标记跨度的文本(来自引用替换),现在将会对其进行解释。引用替换包括格式化文本,例如加粗、强调和等宽,以及智能引号。以前,必须使用内联传递宏强制在属性定义的地方进行该替换。现在不再需要这样做。其次,内联传递宏将不能任意改变发生的解析。然而,我们很可能能够支持最常见的场景以确保兼容性(支持有限的排列组合)。当这不可能时,我们可能依靠不同的解析配置文件。
通过进行这次过渡,我们相信我们将能够在解决AsciiDoc语言内联语法长期存在的严重限制和特性的同时,与现有文档保持合理的兼容性。