对「Experimental public branch for inline special blocks」讨论的汇编与总结

关于(中文友好)的 Org Mode 行内标记语法的设计想法征集 这一帖子讨论了处理 Org 文档中的中文内容的行内标记在缺少空格的情况下无法被识别的几种方案。帖子内讨论的一大方案是使用 ZWS(零宽空格)是否妥当。

不过我的关注点可能不在那里。Org-mode 本身有个 special block 的块级元素来方便各后端用于额外用途的导出,比如在 HTML 后端中就可用于创建各种块级元素。但是,Org 的行内或者说内联文本标记只有下划线,删除线,斜体,粗体等等几种。就以导出为目的来说,允许支持可扩展的或者说可自定义的内联标记可以允许 Org 导出后端以更加简单的方式添加更丰富的标记支持(比如颜色)。一种做法是通过宏,不过老实说我不太喜欢。

#+MACRO: green @@html:<b style="color:green;">$1</b>@@

邮件讨论 中 Ihor Radchenko(@yantar92)提到了一种引入新的内联标记的方法: Experimental public branch for inline special blocks。这个帖子我介绍一下相关讨论的内容,同时也方便参与中文行内标记设计讨论的坛友了解一点细节。

(有 AI 了真方便)

4 个赞

后续呢

还在看 :rofl: ,zsbd

在 2024 年 3 月 1 日,Juan Manuel Macías(看起来有点像西班牙人名,以下简称 JMM)在 emacs-orgmode 邮件列表上发了一封名为 Experimental public branch for inline special blocks 的邮件,介绍了他实现的 inline-special-block(以下简称 ISB)。这一讨论随后分化出了多个子讨论,我将逐一介绍他们。JMM 也给出了 POC 实现:Juan Manuel Macías Chaín / org-mode · GitLab ,我会在邮件讨论涉及到时给出对应的 commit 链接,在完成对所有内容的介绍后我也许会简单看看他是如何实现的。

在首次讨论中,JMM 给出了 ISB 的语法描述,使用 & 符号和某一标记名开头,随后使用大括号 {} 添加内容:

&foo{lorem ipsum dolar}
&_{lorem ipsum dolar}

前者按作者的意思在 HTML 中会导出得到 <span class="foo">lorem ipsum dolar</span>,后者则导出到单纯的 <span> 标签。除此之外 ISB 还允许添加可选的属性列表,它支持一些普遍的属性(比如 :lang, :color 等)以及导出后端特定的属性,比如:

&foo[:color red :smallcaps t :lang it]{lorem ipsum dolor}
&foo[:prelatex [bar] :postlatex {baz} :lang it :latex-command blah]{lorem ipsum dolor}

(注:smallcaps 指将所有字母显示为大写字母。)

除此之外,ISB 还允许添加别名来组合多个属性,而且可以在表示属性的方括号中添加更多属性或覆盖属性,比如:

#+options: inline-special-block-aliases:(("latin" :lang "la" :latex-command "textit" :html-tag "em"))

Caesar's famous quote: &latin!{Alea iacta est}
Caesar's famous quote: &latin![:smallcaps t :color blue]{Alea iacta est}

分别会得到:

Caesar's famous quote: <em lang="la" class="latin">Alea iacta est</em>
Caesar's famous quote: <em style="color:blue;font-variant:small-caps;" lang="la" class="latin">Alea iacta est</em>

作为补充说明,这里说下 HTML 中的 Special block 的导出方式。对于类似 #+BEGIN_NAME 的 Special block,如果 NAME 是 HTML 标准标签之一,它会以 <NAME>...</NAME> 的方式导出,否则以 <div class="NAME">...</div> 的方式导出。可见在 ISB 的初版设计中 ISB 的名字对 HTML 导出来说仅仅作为标签的类(除非你用 :html-tag 来指定标签名)。

接下来,讨论分成了几个部分:对 inline-special-block 这个名字的讨论,对标记语法的讨论,以及对导出选项的讨论。首先我们从名字的讨论说起。

Max Nikulin 首先对 inline-special-block element 这一名称表达了疑惑,首先在 Org 中 element 指大于段落的元素,object 指小于段落的元素,比如链接。ISB 显然属于段落内部的内容,更接近于 Object。此外 Max 提到 CSS 中的 inline-block 是一种显示属性,display: inline-block 使得元素既可以像内联元素一样与其他内容保持在同一行,又可以像块级元素一样设置高度和宽度,但 ISB 从设计上就位于正常的文本流中,它更像是一个纯粹的内联对象而不是独立的块。

在 Max 发言后,Ihor 建议使用「Custom inline markup」。JMM 解释了他最初选择 ISB 名称的原因:为了对标现有的 Special block 和与现有的 inline code block 相对应。他也提出了「Custom span」这个新的名字。

Max 承认 JMM 的选择有点道理,但他还是坚持认为 Special block 本质上是块级元素,如果能找到一个更准确的术语那就可以避免掉 inline 这个限定词。他认为 ISB 这一功能与宏和 LaTex 的 fragment 有些相似,所以建议使用「Custom fragment」。Ihor 不太确定自己是否喜欢这个名字,他感觉 fragment 一般意味着不存在嵌套,但 ISB 本身就允许嵌套。

对于 Custom 这一名字组成,JMM 表示认同,他认为这个新的内联对象与 Org-mode 中已有的标记存在区别,这些标记本身对读者有意义,但 &foo{bar} 这样的标记不向 Org 文本的读者直接提供信息,而是告诉导出后端如何处理这段文本。鉴于此,他建议使用 Custom Export Fragment/Span/Whatever,并表示个人倾向于 Span。

Suhail Singh 同意 Ihor 的看法,fragment 通常暗示着无嵌套。他认为从 Org-mode 用户的角度来看 ISB 这个名称是清晰且容易理解的,code block 对应于 inline code block 就像是 special block 对应于 inline special block 一样。

关于「Custom inline markup」还有 Ihor 和 Max 的后续讨论。Max 觉得这太抽象了,建议使用「inline decorators」,但 Ihor 不太喜欢,因为 decorator 并不是 Org-mode 使用过的术语。Max 在回信中详细说明了他认为 markup 不好的理由:markup 过于抽象和笼统,由于 Org 中几乎所有元素都可以被称为标记,在不添加更多限定词的情况下容易造成混淆。不过他也同意 decorator 因为从未被使用过而不适合引入。

从结果上来看,对名字的讨论似乎没有改变 inline special block 这一最初的名字。所以这一帖可以不用看了

You missed most of the messages in the discussion. Please use the full message list as in https://list.orgmode.org/orgmode/875xwqj4tl.fsf@localhost/t/#u

是的,目前我在上面的帖子中提到的仅仅是关于这一可能的新的对象的名字的讨论。距离完成整个帖子的内容还有一段距离。


Yes, what I mentioned in the post above so far is merely a discussion about the name of this possible new object. I still have a ways to go before completing the entire post’s content.

在 2024 年 3 月 7 日,Max 指出了当前的实现存在一些问题,某些不完整的结构也会被解析为 ISB:

(org-export-string-as "Alpha&Beta{"
                    'html
                    '(:export-options (body-only)))
"<p>\nAlpha<span class=\"Beta\"></span>{</p>\n"

对此,JMM 提出了另一种语法来尝试消除模糊:&:type[attrs]{text}&:_[attrs]{text}。Ihor 则建议采用更简洁的 @type[...]{...}@_[...]{...} 形式,理由是它类似于 Texinfo 格式的构造。JMM 表示赞同并在 b2b4d01797a26a30 中提交了这一变化,而且将匿名形式从 @_ 改为 @@

紧接着的是 Max 的测试和 JMM 的修复。后续还有一些小问题,不过最后还是修完了,解释器不会抛出错误而是将不完整的内容作为普通文本处理。

因为这里还涉及不到对具体实现的分析(这部分感觉也不是很有必要来写),所以只记录了语法变化的部分。因为稍微有点少,补充一下关于 smallcaps 和 ZWS(可能在下一楼)的相关讨论,这些讨论也许可以看作独立的子讨论。


在 JMM 发布这一话题后的 3 月 5 日,Max 认为:smallcaps 硬编码为一种「通用属性」的实现方式过于僵硬,对于 HTML 的导出直接使用 style="font-variant:small-caps"; 而不是通过 CSS 类 class="..." 不利于用户进行全局样式控制,并建议通过全局设置来配置将 :smallcaps 导出到 style="..." 还是 class="..."

对此,JMM 的解释是全局属性是为了给用户提供一种快捷且直接的格式化方式,类似于 LaTeX 的 \textcolor\textsc。如果用户需要在不同后端下使用不同的外观,可以考虑使用不同的属性,比如 &_[:html style="color:red;" :prelatex \scshape{}]{text}。对于需要精细控制的用户,可以使用别名或者专门的后端属性。

当然,对于这一回复 Max 在随后进一步明确强调了他希望通过全局配置来控制 :smallcaps 这样的属性到底导出为内联样式还是 CSS 类,JMM 确认了当前实现中如果同时设置了 :smallcaps 属性和别名,它会同时生成内联 style 和类名,他认为可以添加某些全局属性来禁用一些全局属性。

With the current implementation this:

#+options: inline-special-block-aliases:(("definition" :smallcaps t))

&definition{Example}

produces:

<span style="font-variant:small-caps;" class="definition">Example</span>

在同年 4 月 11 日,JMM 表示他倾向于移除 :smallcaps:color 等全局属性,因为它们会使每个后端的实现变得不必要地复杂。

关于使用零宽空格来让用于中文的标记生效这回事并不仅仅只有中文有这个问题,*this*is 也无法正确识别为粗体,需要在 *is 之间加上 ZWS 才行。正如 Max 提到的,规避使用 ZWS 来解决这个问题的讨论在历史上有一些:

  • [RFC] Creole-style / Support for **emphasis**within**a word** Jambunathan K 建议使用双字符标记来允许词内强调,比如 **bold**and**bold**//italic// __underline__==and==++strike-through++ plain text,但在讨论中存在破坏向后兼容性的顾虑。最初的讨论是在 2014 年,在 2022 年 Vincent Belaïche 重新提起了这个问题。

  • On zero width spaces and Org syntax 。在 2021 年 12 月,JMM 指出 ZWS 被用于解决词内强调不被识别的方法存在问题,应该使用一个可见的定界符号来解决这个问题。

  • Org-syntax: Intra-word markup 同样也发生在 2021 年 12 月,某种意义上来说是对过往方案的一些总结。

Max 认为 JMM 提出的 ISB 有望成为解决 ZWS 确实的理想方案,为了让匿名 ISB 成为 ZWS 的替代品,Max 要求当匿名 ISB 没有指定任何属性时,在导出结果中不应该添加任何包裹元素。如果 ISB 的唯一作用是用来界定 Org 标记的范围,那么最终的 HTML 或 LaTeX 输出中不应出现任何多余的标签或命令,但目前的实现存在这个问题(注意多余的 span):

(org-export-string-as "Example of &_{*intra*}&_{/word/} markup"
                      'html
                      '(:export-options (body-only)))
"<p>
Example of <span><b>intra</b></span><span><i>word</i></span> markup</p>
"

JMM 采纳了这一建议,

最后一部分是关于导出属性的讨论。这是整个讨论中比较复杂的部分,围绕如何使用 :export 属性来精细控制 ISB 在不同导出后端中的行为。

最初的方案中,JMM 设想通过 :export 属性指定需要导出到的后端,name+ 表示完整导出(包含标记),name* 表示仅导出内容,noexport 表示不导出任何内容。@foo[:export latex+ odt* html*]{...} 的意思是完整导出到 LaTeX,仅导出内容到 ODT 和 HTML。

Max 提出了更高的要求:处理那些未指定后端的内容。现有的导出片段机制要求为每个目标后端显式编写代码,比如 @@latex:...@@@@html:...@@ 。如果想要让内容以特定方式导出到 LaTeX 和 HTML,而对所有其他未指定的后端采用一个简单的默认格式则难以实现。另一个问题是,Org-mode 的导出后端之间可能存在继承关系,Max 希望能明确指定为某一后端(比如 latex)设定的规则是仅适用于它本身还是用于所有派生出来的后端。

对此,JMM 认为通过定义别名可以部分避免使用多个导出片段的情况,比如:

@@latex:\textbf{\LaTeX() formatting}@@@@html:<strong>HTML formatting</strong>@@

可以写成

#+options: inline-special-block-aliases:(("strong" :latex-command textbf :html-tag strong :export "latex+ html+ odt*" ))

@strong{formatting}

or even:

@strong{@@latex:\latex{}: @@@@html:HTML: @@ formatting}

另外,为了避免列出所有的后端,JMM 表示可以考虑添加一个 rest 选项,就像 :export "latex+ html+ rest*" 这样。对于导出设定是否应该应用于某个后端的派生后端,JMM 没有明确的态度。考虑到 rest 实际上可以作为后端名,Stefan Nobis 建议 使用 :others 来表示没有显式提到的所有导出后端,另外可以考虑将 + 作为默认行为,这样可以省区一个符号。JMM 实现+ 改进并给出了一些例子:

Examples:

@foo[:export noexport]{lorem ipsum dolor}

==> does not export anything

@foo[:export contents]{lorem ipsum dolor}

==> only contents

@foo[:export latex]{lorem ipsum dolor}

==> exports only to LaTeX

@foo[:export latex odt* html*]{lorem ipsum dolor}

==> exports everything to LaTeX, but to odt and HTML only the content

@foo[:export latex* rest]{lorem ipsum dolor}

==> content to LaTeX but full export to the rest of the backends

@foo[:export latex rest*]{lorem ipsum dolor}

==> the opposite of the above.

Max 指出 JMM 的规则无法解决一个复杂的实际用例:如何排除特定的后端(例如,不向 HTML 导出 YouTube 链接,因为 HTML 已经使用嵌入式播放器,此处有详细解释)而将内容导出到所有其他后端(EPUB, LaTeX, ODT, text 等)。为了实现这种“排除”逻辑和更精细的控制,Max 建议放弃使用 noexport 和 rest 等可能与后端名称混淆的字符串,转而引入特定的操作符,例如使用 -*+ 等字符来明确表达“不导出到此后端及其派生后端”或“仅导出内容”等语义。

在 JMM 给出的新方案后,他与 Max 经过一些讨论后得到了以下导出规则:

  • * 仅导出内容
  • - 不导出
  • = 对未提及的后端进行完整导出
  • *= 对未提及的后端进行仅内容导出
  • =- 不导出到未提及的后端

(老实说,看到这里我已经感觉到了这玩意在越变越复杂)

对于这一方案,Max 指出 - 是一个有效的后端名称或后端名称的有效结尾字符。如果单独使用 - 或将它作为后缀表达不导出的语义,这可能与实际的后端名称产生冲突。另一个问题是如果为多个后端·指定了 = 规则,它的效果并不明确。此外,规则仍然不足以表达对派生后端的控制。在 JMM 回复之前有个语法讨论的小插曲,Ihor 提议直接使用 sexp 来表达导出规则,不过 Max 表示重点是在约束派生后端上,以及可能没有直接可用的函数来解释可能的 sexp 描述。

在 JMM 的回复中,他承认了没有考虑到 - 可能与有效的后端名称冲突的问题。如果用户只想导出到一个后端,必须输入冗长的 :export backend=-,理想状态是只输入 :export backend,但他目前无法实现这一要求。在考虑了派生后端的相关问题后,JMM 整理并提出了下一版简化语法的设想:* 表示导出内容,/ 表示跳过,. 表示不导出派生后端,= 表示完整导出此后端和派生后端。在这一规则下:

  • :export latex* 表示对 latex 仅导出内容,其余后端导出全部
  • :export latex/ 表示不导出到 latex
  • :export latex. 表示不导出到 latex 的派生后端
  • :export latex .(注意空格)表示对 latex 进行全部导出且不导出到所有其他后端的派生后端

(越来越复杂了)

在这一子话题下的后续内容是对实现的一些讨论,此处略过。

在 24 年 4 月,在详细阅读相关讨论后,Ihor 极其详尽地提出了一些疑问和提案。对此 JMM 表示最近有点忙可能要点时间:

https://lists.gnu.org/archive/html/emacs-orgmode/2024-04/msg00178.html

Hi, Ihor. Thanks for all your comments. I will try to answer each of the
questions you have raised. Let's see if I can find a space next week...

The latest added to the branch are Maxim's proposals (and code) for
backend control. See this thread:

Max 此处给出了一些讨论上下文: Re: Experimental public branch for inline special blocks

这一话题的最后一次讨论日期是在 24 年的 7 月,JMM 表示其他人可以接手他的工作。

https://lists.gnu.org/archive/html/emacs-orgmode/2024-07/msg00255.html

Juan Manuel Macías <[email protected]> writes:

> ... until
> September I will not be able to participate actively or continuously on
> the list.

No problem.
I just wanted to make sure that this is not forgotten.

> Of course, if someone wants to actively collaborate in the development
> of this branch, I have no problem. If necessary, I also have no
> objection to moving the repository to another more ‘open’ place (in Gitlab
> it is necessary to have an account to collaborate).

+1

This is an important feature that should be added sooner or later to
address numerous edge cases in the Org markup.

On my side, it is not on the very top of the priorities, so I am happy
that Juan is working on it. If anyone else wants to help, it will be
very welcome as well.

以上就是讨论的内容,其中的一些细节部分讲的可能不是很清楚。需要改正的地方请及时通知我。

我个人(和 Gemini)的总结评价如下:

好的,我们将您观点和前面讨论的技术总结进行综合,形成一段关于 Inline Special Block (ISB) 设计哲学和未来方向的总结性陈述。


总结:ISB 的设计哲学与权力下放的必要性

Inline Special Block (ISB) 的引入,本质上是为了解决 Org-mode 现有标记系统的两大核心限制,但这两种目标对设计复杂度的要求截然不同 。

目标一:低成本解决 ZWS 问题 (ZWS Replacement)

ISB 最直接且低成本的应用是作为零宽度空格(ZWS)的结构化替代品,用于实现词内强调(intra-word emphasis)。ZWS 因其不可见性、难以管理以及在导出时可能破坏格式(如 LaTeX 断字)而备受诟病 。引入 ISB 的匿名语法(如 @@{...} )作为可见的定界符,只需确保其在导出时是无痕的 (即在不带属性时,不输出任何包裹标签或命令),即可完美解决这一长期存在的痛点,同时避免了修改 Org 核心标记解析器带来的向后兼容性风险 。

目标二:高成本实现复杂导出控制 (Advanced Export Control)

ISB 的第二个目标是作为 Special Block 的内联等效物,实现对导出行为的细粒度、跨后端控制 。然而,为实现复杂的 Fallback 机制和派生后端继承逻辑,开发者社区引入了极其复杂的基于操作符的语法(如 latex- , * , =- )和规则匹配系统 。

您的观点:权力下放是关键

从简洁性角度出发,应倾向于认为 ISB 的引入应以替代 ZWS 为主要目的。对于复杂的导出控制,试图用一套统一的、在源文件层面操作的规则来管理所有后端(HTML、LaTeX、ODT)的导出差异,是不可取的 。Special Block 的导出结果本质上是特定于后端的 。因此,更合理的设计是 Org-mode 仅提供 ISB 这一灵活的容器 ,将具体的标记管理和导出实现权 下放给各个后端(通过后端过滤器、宏或别名定义)。这既能保持 Org 模式语法的简洁和可维护性,又能满足用户对高度自定义输出的需求。


彻底搞清楚整个 ZWS 标记和怎么设计一个比较好的 inline-special-block 可能还要看看历史,有时间看看 JMM 的思路搓着玩玩去。