相关问题

语境

块属性行中包含的属性列表需要专门的解析规则,这些规则有多种定义方式。该SDR文档记录了为定义这些规则及其边界条件而做出的决定,这些规则是在正式语法的背景下定义的。

块宏中的attrlist将使用相同的解析规则,但不包括第一个位置属性中的速记属性。

接受决定

规范将定义如何根据决策总结中指定的规则解析块属性行中的属性列表。这些规则将处理边界、属性引用、位置和命名属性、简写属性、累积属性、内容属性和内联解析、反斜杠转义、合并多个块属性行的属性,以及将数据和位置映射到ASG。它还将讨论向后兼容性,并记录被拒绝的替代方案。

决策摘要

属性列表指的是在块属性行开头的 [ 和结尾的 ] 字符之间的源文本。根据定义,块属性行是一个框起来的属性列表。属性列表被解析成与块节点相关联的属性映射。一个块可以有多个块属性行,这些行必须在块的开始前出现。以下部分将记录在这个上下文中如何解析属性列表。

界限与解析策略

属性列表不得以空格字符开始或结束。一旦找到块属性行,应该使用内部解析步骤解析属性列表。这个解析步骤由属性列表解析器执行,后者由一个独立的语法支持。这限制了属性列表只在块属性行内,因此防止了解析器超出其边界。

每个顺序块属性行中的attrlist应按文档顺序解析。

虽然将attrlist规则整合进匹配块属性行的语法中似乎可行,但由于需要预处理attrlist中的属性引用,这种方法并不现实。attrlist解析器必须是与块解析器截然不同的单独解析器。

属性引用

在对属性列表运行解析器之前,必须对属性列表进行预处理。这个预处理步骤必须解决属性列表中出现的任何文档属性引用,无论它们出现在属性列表中的哪个位置。在此模式下,内联预处理器不应考虑任何内联直通的边界。在此阶段,它应该只寻找属性引用并替换它们。请注意,相同的内联预处理器模式将用于解决属性条目值中的属性引用。一旦内联预处理器解决了属性引用,属性列表就准备好被解析了。

为了准确记录内联位置在内容属性值中的位置,应跟踪解析属性引用引入的偏移量。已解析文本的位置范围应该是属性引用的起始和结束位置。

属性列表条目

属性列表由一组具有名称和位置的属性条目组成。每个条目由逗号分隔,逗号前后可以有空格(例如,id=idname, role=rolename)。命名属性和位置属性可以交织使用,但推荐的风格是命名属性总是跟在位置属性后面。如果值被引号包围,可以使用一个或多个空格将该属性与后面的属性分开(不要求逗号)。

命名属性

命名属性由名称和值组成。在源代码中,名称通过等号与值分隔,等号周围可以有空格(例如,name=value)。命名属性的值使用它的名称分配给属性映射中的一个键。

任何属性名只要符合语法都是被允许的。每个节点都有一组保留的属性名共享。这些属性名如下:

  • 身份证明

  • 选项

  • 参考文本

  • 角色

命名属性有两个别名:1)`roles`是`role`的别名;2)`options`是`opts`的别名。

此外,所有的块节点都有以下保留的属性名:

  • 标题

  • 风格

  • 标题

随着规范的发展,特殊的块可能会定义额外的保留名称(例如,表格块的`cols`)。此SDR不尝试涵盖所有未来的可能性。它只是提供一个大纲。

位置属性

位置属性是一个没有命名的属性。换句话说,位置属性仅由一个值组成。位置属性根据其在attrlist中的基于1的位置进行索引。以这种方式索引位置属性的原因如下:

  • 对于作者来说,推理索引如何被分配要容易得多;只需计算在attrlist中的位置。

  • 它可以防止如果其前的位置属性被更改为命名属性时,索引发生偏移。

  • 无需特别条件即可识别承载速记属性的位置属性;根据定义,它永远是第一个位置属性。

然而,强烈鼓励作者撰写文档时始终让命名属性跟随在位置属性之后。

当位置属性被存储在属性映射中时,它被分配给一个由数字索引加上 $1 前缀构成的字符串键(例如,"$1": "sidebar")。如果位置属性的值为空,则不会存储在属性映射中。如果命名属性与命名属性交错使用,命名属性会影响位置索引。

可能在属性映射中,位置属性键之间存在间隙。例如:

在这种情况下,位置属性键是 $1、$4 和 $6。

引用值

属性值可以用一对单引号或双引号括起来(例如,"value"`或者’value')。闭合引号后面必须跟着一个逗号、空格或者attrlist的结束。当值存储在属性映射中时,将移除其括起来的引号。

将值用引号包围可以让该值包含逗号,否则逗号会被当作是属性之间的分隔符。请注意,使用反斜线无法转义逗号,这也是为什么需要用引号括起来的值。一个属性值中的逗号只能通过用一对单引号或双引号来引用该属性值来转义。

引号内的所有空格都会被保留,包括紧挨着引号的空格。(相比之下,在没有引号的值中仅保留内部空格)。如果值中包含了用作引号的字符,那么这个字符必须进行转义。

如果一个属性值的开始或结束处是一个引号字符,并且在值的对侧没有找到匹配的引号字符(即引号不平衡),那么这个值不被视为已引用,并且引号字符被解释为值的一部分。

速记属性

在任何块属性行的第一个位置属性中允许使用简写属性。当满足这个条件时,应该识别并解析这些简写属性。这些简写是从它们所在的每一行解析和提取的,而不是在所有 attrlists 解析完成之后。

速记属性使用速记表示法在单个条目中定义多个命名属性。在第一个位置属性位置中,速记属性的完全表达的语法如下:

[idname,reference text]stylename#idname.rolename1.rolename2%optionname1%optionname2

所有简写属性都是可选的。名称分配如下:

  • idname ⇒ id

  • 参考文本 ⇒ reftext(解析为内联元素)

  • 样式名 ⇒ 样式

  • rolename1 rolename2 ⇒ 角色(累积)

  • optionname1、optionname2 ⇒ opts (累积的)

缩写文本不得用引号括起来。缩写的值不能是空的。除引用文本外,缩写文本中不得含有空格。如果不满足这些条件,缩写将不会被解析,值将按照输入的格式存储。

ID缩写有两种形式:锚点符号和哈希符号。锚点符号必须始终放在第一位。如果同一属性值中同时存在两种符号,哈希符号具有更高的优先权,这意味着最后出现的符号将会生效。在例子中由`stylename`表示的样式,必须始终遵循锚点符号,并在任何其他缩写之前。其余的缩写可以按任意顺序排列(例如,%optionname1.rolename1#idname)。

值得指出的是,第一个位置属性中增加锚点表示法可以统一Asciidoctor的块锚点行和块属性行。换句话说,`[[idname, reference text]]`只是使用了第一个位置属性中的锚点简写的块属性行。

参考文本的解析方式与单引号值类似,因此逗号被当作值的一部分处理。如果参考文本包含`]`字符,那么这个字符必须被转义。如果引用文本中包含逗号,也没有必要用引号将其括起来。如果值被引号括起来,引号则被视为值的一部分,这与引用属性值时不同。

累计属性

如果一个块属性被重新定义,该属性将在属性映射中被覆盖。因此,文档顺序中的最后一次出现始终是决定性的。例如,如果在两个连续的块属性行中第一个位置属性都是非空的,映射中的值将是第二行的值。在第一个位置属性中的速记属性被解析并积极存储,因此每个出现都将对映射中的属性有所贡献,而不仅仅是最后一次出现。

重写规则有两个例外,roleopts。这些属性的值会在每次使用时累积。例如,role=a,role=brole=a b 的解释是相同的。而 opts=option1,opts=options2opts="option1,option2" 的解释也是相同的。

当累积角色时,该值会被修剪并以一个或多个空格分开以提取角色名。当累积选项时,该值会被修剪并以一个或多个空格或一个可选的由空格包围的逗号分开以提取选项名。在这两种情况下,数组中的每个条目将不会有任何空格。重复项会被过滤掉。

内容属性和内联解析

大多数属性的值被保存为字符串。不过,有一个例外,那就是内容属性。内容属性是包含可显示内容的属性,但在源代码中是以块属性的形式定义的。

内容属性列表如下:

  • 标题

  • 参考文本

  • 标题

  • 引用标题

  • 归因

内容属性的值总是被转换为内联元素数组。如果值被单引号包围,那么将使用内联解析器将其解析成内联元素数组,从而解释任何内联标记,包括内联直通。如果该值没有被单引号包围,该值会被转换为文本节点并包裹在一个数组中。它会被当作文本已经被包含在内联直通中一样对待。未解析的值应该被保存起来,以便之后在属性映射中恢复。

如果使用内联解析器,那么内联预处理器应该只提取内联直通。这是在属性列表中唯一识别内联直通的时候,并且限制在引用值的边界内。属性列表解析器不应该解析属性引用,因为这会导致属性引用被解析两次。一旦内联解析完成,应该恢复内联直通。

如果实现跟踪位置信息,每个内联节点的位置应当被记录下来。对于非内容属性(即,字符串值)不必跟踪位置信息。

反斜杠转义

在属性列表中使用反斜杠来转义语法(即反斜杠转义)的处理方式与解析内联语法时不同。除了在内容属性的值中解析内联元素时外,属性列表中的所有语法都是从语法上进行转义的。这意味着反斜杠必须位于原子语法元素之前,例如属性引用,而不仅仅是任何符号。

这些是在属性列表中处理反斜杠的情况:

  • 在属性引用之前

  • 在引用属性值时,如果引号字符相同,则在引号字符之前。

  • 位于锚点参考文字部分右方括号前面

  • 根据内容属性的任何单引号值中的内联语法所定义的

例如,以下语法将在attrlist中转义一个属性引用:

{escaped}

预处理后的结果将是:

{escaped}

左花括号的位置应归因于反斜杠的位置,以解释其缺失。

在任何允许反斜杠转义的位置,必须有一种方法能够表现出一个文字意义上的反斜杠。因此,属性列表解析器必须处理所有连续反斜杠,直到一个可转义字符或结构。处理这些属性的规则如下:

  • 一个偶数数量的反斜杠将被解析为一半数量的反斜杠,并且不会转义其后的字符或形式。

  • 奇数个反斜杠将解析为少一个反斜杠的一半,最后一个反斜杠用来转义紧跟其后的字符或格式。

这种处理会影响当内联解析器运行在内容属性的值上时保留下来的反斜杠数量。因此,在某些边缘情况下可能需要使用额外的反斜杠。考虑这样一种情况,当你需要在内容属性中单引号包围的文本前放置一个字面意义上的反斜杠时。

标题=‘花括号中的文本’

内联解析器将看到的是:

text in curly quotes`’里的文本

幸运的是,这些情况非常罕见。

当追踪位置时,带前导反斜杠的值的起始位置应该是该值的起始位置(第一个反斜杠),而带有尾随被转义反斜杠的值的结束位置应该是被转义反斜杠的位置(最后一个反斜杠)。换句话说,位置应该覆盖整个原始值的范围,在反斜杠被处理之前。

合并属性

从attrlist解析出来的属性应当被合并进任何相关同一块的前面的块属性行中解析出来的属性。如果定义了相同名称或位置的属性,最后的定义将胜出,但role和opts除外,这些是累积的。如果某一行中的位置属性条目为空,则不会替换掉该索引处已定义的位置属性。

请注意,合并操作并不会阻止被替换的内容属性进行解析,只是解析的结果会丢失。

命名位置属性

AsciiDoc支持一种功能,在这种功能中,如果块提供了这种映射,则位置属性会映射到命名属性。一个这样的例子是块宏上的alt文本、宽度和高度属性。在这些情况下,位置属性就好像有一个隐式的名称部分,以节省作者输入它的时间。这些被称为命名位置属性,或简称为posattrs。

具名位置属性的处理方法和时间将在另一个SDR中提出。

将数据和位置映射到ASG

在解析与一个块相关联的所有块属性行中的属性列表之后,结果是一个属性的映射。这个映射被赋值给ASG中节点的`metadata.attributes`属性。如果存在`role`属性,其值会被转换为数组并存储在`metadata.roles`属性上。如果存在`opts`属性,其值也会被转换为数组并存储在`metadata.options`属性上。如果存在`id`属性,其值将被存储在节点的`id`属性上。

所有内容属性都被提升为节点上的属性。在这一点上,未解析的值被重新保存在属性映射中。这确保了映射中每个属性的值都是一个字符串。

元数据上的位置属性,如果由实现设置,应该从第一个属性行的第一个字符开始,一直到开始块的那行之前的最后一个字符结束。换言之,它封装了所有的块属性行。节点本身的位置属性应该是块的第一行,不包括任何块属性行。通过这种方式设置,可以看到块的开始位置,无论是否包含块属性行。

向后兼容性

此SDR定义的解析规则源自Asciidoctor的行为,并且大多数情况下保持一致。然而,有些差异值得注意:

  • 内联解析器仅在内容属性的单引号值上运行;对任何单引号值启用这种行为很少需要,并且,通常会产生非理性行为,比如解析一个ID;这种限制不太可能影响现有文档。

  • 内联解析器仅在title属性的值被单引号包围时才会运行;这与AsciiDoc预规范版本不同,后者不论何时总是对title属性的值运行内联解析器。

  • 反斜杠转义现在是一致和可靠的;在Asciidoctor中,放在可转义语法前的反斜杠总是被视为转义字符,因此在那个位置无法表示一个字面意义上的反斜杠;由于这一改变,可能需要在某些情况下额外添加反斜杠。

  • 在attrlist中的属性引用只会被内联预处理器处理一次;在Asciidoctor中,如果attrlist中的属性引用的值包含了另一个属性引用,在一个单引号包裹的值中解析出的attrlist也会将其中的属性引用解析出来;这是一个漏洞;解析器不应该在已解析的attrlist中再解析属性引用。

被拒绝的替代方案

在预处理器中解析属性引用

考虑的一种替代方案是让行预处理器解析 attrlist 中的属性引用。这种方法的优点是可以直接将 attrlist 的解析规则整合到块语法中。然而,存在一个重大的含义使得这种替代方案无法成立。在属性值包含多行的情况下,它会导致预处理器产生一个结果,这个结果将不再被块解析器识别为块属性行,可能会改变文档的解析。Attrlist 不能突破一行的界限。因此,这个策略被拒绝了。

按出现次序索引位置属性

位置属性索引的一种替代方法是按照出现次序给它们编号。在这种情况下,命名属性会被跳过,只有当出现位置属性时,索引才会递增。

起初,这似乎是合乎逻辑的,因为命名属性不是位置属性。然而,这样索引位置属性引入了其他复杂性。

首先,如果被一个命名属性先行,它允许第一个位置属性从最左侧位置浮动。如果发生这种情况,则必须解释只有在最左侧位置的第一个位置属性中才能识别速记属性,解析器必须考虑到这一点。其次,确定位置属性的位置变得更加困难,因为它不是列表中的位置,而是在排除命名属性后的有效位置。最后,如果前面的位置属性更改为命名属性,则位置属性的索引可能会发生变化。

最终,这种选择被拒绝了,因为它使得行为和排列组合变得更难以解释和理解。

在任何单引号属性值上运行内联解析器

Asciidoctor 对块属性列表中的任何单引号属性值应用正常替换。在 AsciiDoc 语言规范中,这等同于在值上运行内联解析器。允许用户为任何属性启用此功能会给实现带来重大问题。

允许任何值被解析为内联,这意味着解析器必须为任何属性可能存储一个内联数组。在客户端代码需要未解析值的情况下,也必须存储该值。实现/客户端代码接下来需要检查属性值是否具有这种数据结构,并选择想要的值。解析器也会做更多不必要的工作来存储支持此功能的信息。这项工作包括不必要地运行内联解析器和跟踪位置。

大多数情况下,这个功能被激活是不必要的或无意的(即,解析role、opts或id属性的值是无意义的)。因此,我们决定只允许这个功能用于已知的内容属性,从而特别对待它们和其他属性。解析后的值将提升到节点上,未解析的值存储在属性映射中以供参考。

如果有必要,我们将来可以扩展内容属性列表。

避免在解析属性引用时使用内联直通。

我们本可以考虑先对属性列表(attrlist)运行完整的内联预处理器,然后再将其解析为属性。然而,由于几个原因,在检查之后这个想法很快就被否决了。首先,这将意味着内联直通(passthroughs)可以在属性列表的任何位置生效,而现在它们并不可以。这也将给实现带来巨大的负担,因为必须回过头去替换直通占位符,由于它们可以出现在属性映射的任何地方,包括在键中。相反,如果内联直通被避免,但不被替换,这将意味着内联直通标记会遗留在不通过内联解析器运行的属性列表的部分。换句话说,这将使事情变得一团糟。最重要的是,这将与AsciiDoc规范之前的属性列表解析方式完全不同。

处理attrlist中属性引用的最一致方法是定义一种特殊模式,该模式在不考虑任何内联直通的情况下预先解析它们,因此这是我们决定采用的策略。

基本反斜杠转义

规范的目标之一是使反斜杠除号的转义变得一致和可靠。然而,我们考虑对attrlist作一个例外,以摆脱那个目标。

一种想法是在这里使用原始的反斜杠转义机制,它仅考虑是否有反斜杠紧邻一个可转义字符或结构,并将其视为转义字符。这个策略最接近于Asciidoctor的实现方式,而且也可以避免在一系列的反斜杠转义中所可能出现的连锁复合现象。最终,我们决定如果我们要在其他地方正确处理反斜杠转义,那么这里也应该这么做。

另一个想法是在词汇级别而不是在句法级别上应用反斜杠转义到属性列表。这当然是一个诱人的选择,因为它将更接近与段落文本中的工作方式相匹配。然而,这将使属性列表中的语法规则与在规范之前的AsciiDoc中大不相同,因为反斜杠现在将在更多地方被识别。这也会使实现更难以跟踪位置追踪所留下的偏移量,并引发新的问题,比如如何处理逗号、等号和引号前的反斜杠。最终,我们认为这将是一个过于巨大的变化,并且认为在使用上和兼容性方面,句法级别的反斜杠转义提供了一个合理的折中方案。