geml

GEML — General Expressive Markup Language(通用表达型标记语言)

*English 中文*

规范(稳定版)

字段 取值
工作名 GEML(General Expressive Markup Language,通用表达型标记语言)
版本 1.0
状态 稳定(stable)
文件后缀 .geml

摘要

GEML 是一种用于结构化、富表达力文档的纯文本标记语言。.geml 文件无需渲染即可作为 纯文本完整阅读;代码、图形、表格、公式、提示框等各类结构化内容统一通过单一的类型块 原语表达;支持稳定标识符并在构建时校验引用;托管外部图形 DSL 而自身绝不内建图形 语言。本规范定义文档模型,块、属性、内联内容与引用的语法,以及合规处理器必须满足的 要求。

目录

  1. 预备
  2. 约束
  3. 文档模型
  4. 类型块原语
  5. 属性与标识符
  6. 内联内容与链接
  7. 表格
  8. 图形
  9. 一致性
  10. 安全与资源限制

附录 A:诊断目录

约定

本文档中的关键词 MUST(必须)MUST NOT(必须不)MAY(可)SHOULD(应) 用于表示要求等级:MUST 与 MUST NOT 表示绝对的要求或禁止,SHOULD 表示推荐,MAY 表示可选且被许可的行为。文中「§n」指代编号为该数字的章节。

标注为非规范性的文字仅作解释,不构成任何要求。除非出现在一致性测试集(§8.4)中, 示例均为非规范性。

英文版是本规范的规范性文本,本中文版为资料性译文:二者不一致时, 以英文版为准。


0. 预备

本节定义 GEML 处理器的字符级输入。§1–§9 的每条规则都建立在 §0.5 所定义的 归一化字符流之上。

0.1 字符编码

.geml 文件必须(MUST)使用 UTF-8 编码。处理器必须不(MUST NOT)尝试探测或接受 任何其他编码。

理由(非规范性): 与仅供渲染的格式不同,GEML 承载构建期身份——块 id、跨文档引用, 以及 .gemlhistory 边车中的 SHA-256 内容哈希。它们都定义在字节之上,因此一份文档若 经另一种编码往返,它就是另一份文档,其版本历史也不再可验证。

处理器必须(MUST)以 UTF-8 替换语义解码:非良构字节序列解码为 U+FFFD REPLACEMENT CHARACTER;必须不(MUST NOT)回退到以其他编码重新解释输入。

字符指一个 Unicode 码点。在直觉意义上不构成字符的码点(如组合记号)在本文档中同样 计为字符。

0.2 字节序标记(BOM)

若解码后的输入以 U+FEFF 开头,该单个字符必须(MUST)在解析前移除。只移除开头的一个 U+FEFF;第二个 U+FEFF,或出现在文档其他位置的 U+FEFF,均为普通内容。

0.3 行与行结束符

行结束符指换行符(U+000A)、其后不跟换行符的回车符(U+000D),或回车符加换行符。

指零个或多个非 U+000A、非 U+000D 的字符,其后跟一个行结束符或输入结束。

空行指不含任何字符,或仅含空格(U+0020)与制表符(U+0009)的行。

每个行结束符必须(MUST)在解析前归一为单个 U+000A。处理器必须不(MUST NOT)让行结束符 的选择改变文档模型:同一文档以 CRLF 与以 LF 书写必须产生完全相同的模型。

注(非规范性): .gemlhistory 边车单独记录文件的主导行结束符,因此恢复某个修订版本 能重现原始字节。归一化管辖的是解析,不是存储。

0.4 不安全字符

U+0000 必须(MUST)替换为 U+FFFD。

理由(非规范性): 对任何按 C 字符串处理的下游消费者,NUL 会截断文档。文档必须不能 (MUST NOT)让流水线中的某个工具看到比解析器更少的内容。

0.5 归一化输入

处理器必须(MUST)在解析前恰好按以下顺序执行:

  1. 按 UTF-8 解码,非良构序列变为 U+FFFD(§0.1);
  2. 移除开头的一个 U+FEFF(§0.2);
  3. 将每个行结束符替换为 U+000A(§0.3);
  4. 将 U+0000 替换为 U+FFFD(§0.4)。

结果即归一化字符流。每一步都只在行内改写字符——没有任何一步会拆分或合并行—— 因此归一化字符流的行数与输入相同。据此处理器可(MAY)按行号索引原始字节,这正是块级 编辑(geml get/set)能对文件未改动部分保持字节保真的原因。

0.6 媒体类型、后缀与片段标识符

   
文件后缀 .geml(版本边车:.gemlhistory
媒体类型 text/geml
厂商树名称 text/vnd.geml
charset 参数 唯一允许的取值为 UTF-8,且因与 §0.1 重复而应(SHOULD)省略
片段标识符 id(§4)

text/geml 目前尚未在 IANA 注册;在必须使用已注册类型的场合,使用厂商树名称 text/vnd.geml.geml 资源上的片段标识符指代携带该 id 的块,与 §5.2 的引用语法 一致——other.geml#budget 无论写成 GEML 引用还是 URL,指的都是同一个块。


1. 约束

本节给出统领后续规范的设计约束。

  1. .geml 文件必须无需渲染即可作为纯文本完整阅读。
  2. 代码、图形、表格、公式、提示框必须共用唯一的类型块原语(§3);不为每种内容 单设语法。
  3. 每个块可携带稳定 id;引用必须在构建时解析并校验(§5)。
  4. 图形必须内嵌外部 DSL;格式只定义托管协议,绝不内建图形语言(§7)。
  5. 不存在原始 HTML 逃逸口;语义不绑定任何后端。
  6. 标题只用 ATX #。setext 标题与 ---/=== 分隔线、frontmatter 规则不属于 GEML。

2. 文档模型

一篇文档是的序列,块只有两种形态:

每个块可携带属性对象 {#id .class key=val}。内联内容只存在于无栅栏区块中。

2.1 列表

列表是一行或多行条目行的连续序列。一条条目行由前导缩进、一个标记、一个空格、 以及该条目的内联内容(§5)组成:

条目内容是单独一行。条目可以以任务标记开头——[ ][x][X] 后跟一个空格—— 该标记被剥离并记录为勾选/未勾选状态。

嵌套由缩进决定。 缩进按列计(制表符记为一列)。比当前条目标记缩进更深的条目,在该 条目下开启一个嵌套列表;缩进更浅的条目则收回到外层列表。两个同级条目之间的空行使 列表变为松散(loose)(否则为紧凑(tight));空行本身不会结束列表。列表在第一个 “既非空行、也不是缩进不浅于本列表的条目行”处结束。

多段落的列表条目不属于 GEML;丰富的条目内容应放进类型块(§3)。


3. 类型块原语

类型块的形态如下:

=== <类型> <属性>?
<正文>
===

3.1 文法

块结构是上下文无关的,如下所示。内联强调不是上下文无关构造,由 §5.3 的定界符游程算法 解析,而非本文法。

document       = { block } ;
block          = unfenced-block | typed-block ;

typed-block    = fence , SP , type , [ SP , attrs ] , NL , body , close-fence ;
fence          = "===" , { "=" } ;            (* open: N equals signs, N >= 3 *)
close-fence    = fence ;                      (* exactly equal to the opening length *)
type           = NAME ;
body           = { LINE } ;                    (* raw, flow or data per the registry *)

unfenced-block = heading | list | paragraph ;
heading        = "#" , { "#" } , SP , text , [ SP , attrs ] , NL ;
paragraph      = text-line , { text-line } ;

list           = item , { item | blank-line } ;
item           = indent , marker , SP , [ task ] , text , NL ;
marker         = "-" | "*" | DIGIT , { DIGIT } , "." ;
task           = "[" , ( " " | "x" | "X" ) , "]" , SP ;
indent         = { " " | TAB } ;              (* nesting depth, by column *)

attrs          = "{" , { attr-item , [ SP ] } , "}" ;
attr-item      = id-attr | class-attr | kv-attr ;
id-attr        = "#" , NAME ;
class-attr     = "." , NAME ;
kv-attr        = NAME , "=" , value ;
value          = bare-word | quoted-string ;

NAME           = ALPHA , { ALPHA | DIGIT | "-" | "_" } ;

4. 属性与标识符


5. 内联内容与链接

5.1 内联元素

内联元素只出现在无栅栏区块内部。

语法 含义
*强调* 强调(emphasis)
**加重** 加重(strong)
`代码` 代码片段(原样;内部不解析)
~~删除~~ 删除线
$…$ 内联数学(正文原样)
![alt](src){…} 就地媒体嵌入(图片/音频/视频)
行尾 \ 强制换行
\ + ASCII 标点 转义:该标点取字面值

5.2 链接与引用

内部与跨文档引用均在构建时校验。

形式 含义
[文字](https://…) 外部链接
[文字](#budget) 指向块 budget 的内部引用,文字自定义
[[#budget]] 自动引用:链接文字取自目标的 caption/标题
[文字](other.geml#budget) 跨文档引用
[^note] 脚注:把 id 为 note 的块渲染为脚注

5.3 识别顺序与强调

无栅栏区块的内联解析分两个阶段进行,并为每个输入指派恰好一个解析。

阶段一——atom(从左到右,按此优先级):

  1. 反斜杠转义(\ + ASCII 标点 → 该字面字符;行尾 \ → 硬换行)、代码片段、内联数学; 其内容不再进一步解析。
  2. 图片、链接、自动引用([[#id]])与脚注引用([^id]);链接或引用不得嵌套在另一个 链接或引用内部。

atom 之间的文本是字面文本。被转义的定界字符是一个字面 atom,因此不参与强调。

阶段二——强调在每一段位于阶段一 atom 之间的最大字面文本上运行;强调不跨越 atom,也 不跨越块边界。强调、加重、删除线由定界符游程 flanking 解析:

这是 CommonMark 强调算法在 GEML 定界符上的限定版:只有 *~~,没有 _ 强调。


6. 表格

块类型 table,两种可互换正文,解析为同一模型。

(a) 可视化形态

=== table {#budget caption="年成本"}
| 方案  | 人月 | 单价 |
|-------|-----:|-----:|
| 基础版 |    1 |   30 |
| 专业版 |    2 |   30 |
===

(b) 数据形态 —— 带计算列与汇总行:

=== table {#fy25 caption="FY2025 各部门营收($M)" format=csv header=1
           compute="FY [%.1f] = Q1 + Q2 + Q3 + Q4;
                    YoY [%.1f%%] = (FY - PriorFY) * 100 / PriorFY"
           summary="Segment = 'Total';
                    Q1 = sum(Q1); Q2 = sum(Q2); Q3 = sum(Q3); Q4 = sum(Q4);
                    PriorFY = sum(PriorFY); FY = sum(FY);
                    YoY [%.1f%%] = (sum(FY) - sum(PriorFY)) * 100 / sum(PriorFY)"}
Segment,   Q1,    Q2,    Q3,    Q4,    PriorFY
Cloud,     124.5, 131.2, 142.8, 158.3, 470.0
Hardware,  88.1,  84.6,  90.3,  95.7,  372.0
Services,  45.2,  47.8,  49.1,  52.6,  168.0
===

{…} 属性对象是一条物理行,上面的换行仅为便于阅读——按 §3.1,GEML 属性不跨行。 该例解析为:

Segment Q1 Q2 Q3 Q4 PriorFY FY YoY
Cloud 124.5 131.2 142.8 158.3 470.0 556.8 18.5%
Hardware 88.1 84.6 90.3 95.7 372.0 358.7 -3.6%
Services 45.2 47.8 49.1 52.6 168.0 194.7 15.9%
Total 257.8 263.6 282.2 306.6 1010 1110.2 9.9%

7. 图形

块类型 diagram 托管外部图形 DSL。

=== diagram {#flow format=mermaid caption="评审流程"}
graph LR
  A[草稿] --> B{评审}
  B -->|通过| C[发布]
  B -->|打回| A
===

7.1 绑定数据的图表

diagram 可用 data=#id 声明数据源。处理器必须解析该引用(悬空 id、或目标不是 table,都是构建错误),并把被引表的模型(含计算列)提供给渲染器。处理器仍 不解释 body

内置 geml-chart 渲染器把表画成图表。format 仍只选渲染器;图表完全用属性 描述,因此处理器能校验(body 留空——非空 body 给告警):

=== diagram {#rev format=geml-chart data=#fy25 type=bar x=Segment y=FY caption="FY 营收"}
===

8. 一致性

本规范定义三个合规等级。产品分别声明各自的合规:一个从不渲染的校验器可以是合规 解析器而不是合规渲染器,这并不使它不合规。

8.1 合规文档

合规 GEML 文档指这样一段归一化字符流(§0.5):合规解析器处理它时不产生任何严重级别 为 error 的诊断(附录 A)。

告警不使文档变得不合规:它们标记的是处理器无法完全解释、但必须(MUST)保留的构造—— 未知块类型、未知图格式、未经校验的跨文档引用。

尽管如此,任何输入都是可解析的:§2.1、§3、§5.3 与 §6 为任意字符流指派恰好一个文档 模型。不存在任何输入是合规解析器可以拒绝、拒绝建模或失败于其上的——不合规的文档同样 产出模型,只是同时带有描述它的错误。

8.2 合规解析器

合规解析器必须(MUST):

  1. 完全按 §0.5 归一化其输入。
  2. 解析类型块原语(§3)与属性对象(§4)。
  3. 构建文档模型,其中每个块 id 唯一且可解析。
  4. 解析内联强调(§5.3)与列表嵌套(§2.1),使每个输入恰好有一个解析。
  5. 对任何无法解析的内部或跨文档引用报错误(§5)。
  6. 把未知块 type 和未知图 format告警而非错误,原样保留正文。
  7. 以附录 A 指派的代码与严重级别报告每一条诊断。
  8. 遵守 §9.2 的资源限制,以诊断而非失败的方式降级。
  9. 不依赖任何特定编辑器,不依赖原始 HTML。

8.3 合规渲染器

渲染器是可选(OPTIONAL)的:合规解析器不必产出任何呈现格式的输出。若产出,则必须 (MUST):

  1. 呈现合规解析器所产出的文档模型,不重新解释 raw 块的正文(§3)。
  2. 不执行 code 块,不解释 diagram 正文(§7)——只能将其交给已注册的外部渲染器 (§9.1)。
  3. 对文档可控文本满足 §9.5 的落点要求。
  4. 输出中略去标记为 hidden 的块(§4),同时在模型中保留它们。

8.4 一致性测试集

规范配套一套一致性测试集:输入 .geml 与期望文档模型的归一化投影成对。对本文档以 算法方式陈述的规则——内联强调(§5.3)、列表嵌套(§2.1)、原子优先级、元数据插值 (§4)——该测试集是规范参照。第二个独立实现复现每个用例即为合规。在参考仓库中它位于 geml-parser/test/conformance/

8.5 版本

规范的版本独立于任何实现。本文档为 GEML 1.0;参考实现的包版本遵循其自身发布节奏, 不是规范版本。

实现以「符合 GEML 1.0」的形式声明合规。处理器遇到不认识的构造时必须(MUST)按 §8.2(6) 降级——这就是本格式的前向兼容机制,也是新增一种块类型或图格式不构成破坏性变更的原因。

类型注册表(§3)是开放的。非本规范定义、亦未注册的类型名应(SHOULD)包含连字符 (例如 acme-invoice),把不含连字符的名字留给本规范的未来版本。图的 format 名遵循 同一约定。


9. 安全与资源限制

GEML 文档常常由机器生成,也常常不可信:它可能来自模型、流水线或一个 pull request。本节 规定文档怀有恶意时处理器必须保证什么。它适用于 §8 的每个合规等级。

9.1 文档是数据,绝不是代码

处理器必须不(MUST NOT)执行或求值文档的任何部分:

9.2 资源限制

处理器必须(MUST)为自己在文档上的递归深度设定上界,且分别针对:类型块嵌套(§3)、 列表嵌套(§2.1)、内联嵌套(§5)。到达上界时必须(MUST)产出对应的 *-nesting-too-deep 错误(附录 A)并继续处理剩余输入。它必须不(MUST NOT)溢出调用栈、 中止,或无法产出模型。

上界由实现自定;处理器应(SHOULD)各自至少允许 64 层,这已远超任何为阅读而写的文档。 参考实现允许 256 层块嵌套与列表嵌套、100 层内联嵌套。

处理器必须不(MUST NOT)在未按目标文法转义的情况下,用文档可控文本构造正则表达式、 shell 命令或任何其他可执行形式。块 id、类名与属性值都是文档可控的;.geml 文件是不可信 输入,与 .zip 同理。

9.3 引用、环与终止性

引用解析必须(MUST)在任何输入上终止,包括为使其死循环而精心构造的输入:

9.4 跨文档解析与外部数据

解析跨文档引用(§5.2)会读取由文档指名的文件。处理器必须(MUST)把该解析限制在显式配置 的根目录内,必须(MUST)在判定目标是否位于根内之前解析每一个符号链接,并且必须(MUST) 拒绝逃逸出根目录的目标。解析必须(MUST)失败即关闭:无法确立限制根的处理器不解析 任何东西并报 unresolvable-document,而不是回退到不受限的查找。

表格的 src=(§6)与媒体 src(§5.1)在渲染时由渲染器抓取,解析器从不读取它们。 渲染器必须(MUST)把这类来源当作不可信输入。在文档可能来自不可信作者的场合,渲染器应 (SHOULD)把 src 限制在文档自身的源或目录内,并应(SHOULD)要求显式选择加入才执行 http(s) 抓取:被抓取的 URL 会把读者的地址、以及阅读这一事实与时间,泄露给控制该 URL 的人。

由于外部数据在渲染时抓取,其内容从不进入 .gemlhistory 哈希——只有 src 文本会进入 (§6)。

9.5 落点要求

链接或嵌入(§5.1、§5.2)中的目标地址,若其 URL 方案不属于 httphttpsmailtotel,则必须不(MUST NOT)被产出为可导航或可加载的目标。处理器必须(MUST)在构建模型 时施加此检查,而不是在渲染落点,这样模型的每个消费者都继承该保护。判定方案时该检查 必须(MUST)忽略位于 U+0000–U+0020 范围内的前导字符与内嵌字符,因为用户代理在对 URL 采取行动前会先剥除它们——java&#9;script: 就是 javascript:

产出标记语言的渲染器必须(MUST)按文档可控文本所处的位置——元素文本、属性值或 URL—— 对其转义,并且必须(MUST)把 .class 记号(§4)削减到目标格式的标识符字符集,而不是 仅仅转义它们。


附录 A:诊断目录

合规解析器产出的每一条诊断,除人类可读的消息外都携带一个代码。消息是散文:它可 (MAY)在版本之间被改写、翻译或补充上下文。代码与严重级别才是契约——它们是一致性 测试、编辑器集成或 CI 关卡所匹配的对象,处理器必须(MUST)以本附录指派的代码与严重级别 报告诊断。

对于本目录已覆盖的情况,处理器必须不(MUST NOT)另造目录之外的代码。处理器可(MAY)为 本规范未定义的情况产出额外诊断;这类代码应(SHOULD)带一个连字符分隔的厂商前缀 (acme-…),以免本目录的未来版本与之冲突。

诊断携带的行号从 1 开始,指归一化字符流(§0.5)中的行——按 §0.5,它同时也是原文件中的 行号。

完整的代码、严重级别与条件对照表见英文版附录 A: 该表是规范性的,并由参考实现的测试逐条机械校验,因此此处不作重复以免译文漂移。