| *English | 中文* |
| 字段 | 取值 |
|---|---|
| 工作名 | GEML(General Expressive Markup Language,通用表达型标记语言) |
| 版本 | 1.0 |
| 状态 | 稳定(stable) |
| 文件后缀 | .geml |
GEML 是一种用于结构化、富表达力文档的纯文本标记语言。.geml 文件无需渲染即可作为
纯文本完整阅读;代码、图形、表格、公式、提示框等各类结构化内容统一通过单一的类型块
原语表达;支持稳定标识符并在构建时校验引用;托管外部图形 DSL 而自身绝不内建图形
语言。本规范定义文档模型,块、属性、内联内容与引用的语法,以及合规处理器必须满足的
要求。
本文档中的关键词 MUST(必须)、MUST NOT(必须不)、MAY(可)、 SHOULD(应) 用于表示要求等级:MUST 与 MUST NOT 表示绝对的要求或禁止,SHOULD 表示推荐,MAY 表示可选且被许可的行为。文中「§n」指代编号为该数字的章节。
标注为非规范性的文字仅作解释,不构成任何要求。除非出现在一致性测试集(§8.4)中, 示例均为非规范性。
英文版是本规范的规范性文本,本中文版为资料性译文:二者不一致时, 以英文版为准。
本节定义 GEML 处理器的字符级输入。§1–§9 的每条规则都建立在 §0.5 所定义的 归一化字符流之上。
.geml 文件必须(MUST)使用 UTF-8 编码。处理器必须不(MUST NOT)尝试探测或接受
任何其他编码。
理由(非规范性): 与仅供渲染的格式不同,GEML 承载构建期身份——块 id、跨文档引用,
以及 .gemlhistory 边车中的 SHA-256 内容哈希。它们都定义在字节之上,因此一份文档若
经另一种编码往返,它就是另一份文档,其版本历史也不再可验证。
处理器必须(MUST)以 UTF-8 替换语义解码:非良构字节序列解码为 U+FFFD REPLACEMENT CHARACTER;必须不(MUST NOT)回退到以其他编码重新解释输入。
字符指一个 Unicode 码点。在直觉意义上不构成字符的码点(如组合记号)在本文档中同样 计为字符。
若解码后的输入以 U+FEFF 开头,该单个字符必须(MUST)在解析前移除。只移除开头的一个 U+FEFF;第二个 U+FEFF,或出现在文档其他位置的 U+FEFF,均为普通内容。
行结束符指换行符(U+000A)、其后不跟换行符的回车符(U+000D),或回车符加换行符。
行指零个或多个非 U+000A、非 U+000D 的字符,其后跟一个行结束符或输入结束。
空行指不含任何字符,或仅含空格(U+0020)与制表符(U+0009)的行。
每个行结束符必须(MUST)在解析前归一为单个 U+000A。处理器必须不(MUST NOT)让行结束符 的选择改变文档模型:同一文档以 CRLF 与以 LF 书写必须产生完全相同的模型。
注(非规范性): .gemlhistory 边车单独记录文件的主导行结束符,因此恢复某个修订版本
能重现原始字节。归一化管辖的是解析,不是存储。
U+0000 必须(MUST)替换为 U+FFFD。
理由(非规范性): 对任何按 C 字符串处理的下游消费者,NUL 会截断文档。文档必须不能 (MUST NOT)让流水线中的某个工具看到比解析器更少的内容。
处理器必须(MUST)在解析前恰好按以下顺序执行:
结果即归一化字符流。每一步都只在行内改写字符——没有任何一步会拆分或合并行——
因此归一化字符流的行数与输入相同。据此处理器可(MAY)按行号索引原始字节,这正是块级
编辑(geml get/set)能对文件未改动部分保持字节保真的原因。
| 文件后缀 | .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,指的都是同一个块。
本节给出统领后续规范的设计约束。
.geml 文件必须无需渲染即可作为纯文本完整阅读。id;引用必须在构建时解析并校验(§5)。#。setext 标题与 ---/=== 分隔线、frontmatter 规则不属于
GEML。一篇文档是块的序列,块只有两种形态:
每个块可携带属性对象 {#id .class key=val}。内联内容只存在于无栅栏区块中。
列表是一行或多行条目行的连续序列。一条条目行由前导缩进、一个标记、一个空格、 以及该条目的内联内容(§5)组成:
- 或 *;.;首个条目的编号即列表的 start。条目内容是单独一行。条目可以以任务标记开头——[ ]、[x] 或 [X] 后跟一个空格——
该标记被剥离并记录为勾选/未勾选状态。
嵌套由缩进决定。 缩进按列计(制表符记为一列)。比当前条目标记缩进更深的条目,在该 条目下开启一个嵌套列表;缩进更浅的条目则收回到外层列表。两个同级条目之间的空行使 列表变为松散(loose)(否则为紧凑(tight));空行本身不会结束列表。列表在第一个 “既非空行、也不是缩进不浅于本列表的条目行”处结束。
多段落的列表条目不属于 GEML;丰富的条目内容应放进类型块(§3)。
类型块的形态如下:
=== <类型> <属性>?
<正文>
===
=(≥3 个)。一个块由与开围栏等长的 = 串闭合,或——当块带有
#id 时——由带标签的围栏 === #id(长度 ≥3 的 = 串后跟该块的 id)闭合。==== 包住 ===),或更稳妥地,给每个块一个
#id 并用 === #id 闭合。带标签的闭合是局部的——不依赖数 =——在块体本身含有
围栏样式的行时推荐使用。raw(原样,如带 lang= 的 code、带
format= 的 diagram/table、math、output)、flow(解析,如 note、
text)或 data(每行一个 key=val,如 meta)。text 块是可寻址的散文容器:其 flow 正文存在的唯一目的,是给一段散文一个
#id 与属性,使其可被引用、可被按块编辑(geml get/set)、可被版本化。渲染
为中性块——不带标注样式(标注属于 note)。只包裹确实需要寻址的散文;普通段落
仍是默认写法。output 块保存某 code 块被捕获的结果(文本/数据),由工具记录——处理器绝不执行。
可选 of=#id 把它绑定到该 code 块,并受引用校验(§5)。块结构是上下文无关的,如下所示。内联强调不是上下文无关构造,由 §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 | "-" | "_" } ;
{#budget} 设定块 id 为 budget。文档内 id 必须唯一。{.warning} 添加语义类(不含样式)。{caption="年成本"} 等 key=val 是各类型自定义的参数。## 标题 {#sec}。=== meta(title = "…")而不是顶级标题
——这样每个标题都对应文档中一个真正的小节。"…" 恒为字符串;true/false 为布尔;匹配整数/浮点
语法的裸词为数字;其余裸词为字符串。不支持数组、日期与嵌套表。= 的裸属性词是布尔标志,置为 true(如 hidden)。=== meta 块以每行一个 key=val 承载文档元数据,沿用上述属性值类型规则。流式
正文中的 会被替换为对应 `meta` 值;未定义的键是构建**错误**。插值读取
流式正文的源文本,并遵循 §5.3 阶段一(1)的原样 atom:代码片段或行内公式里的
原样保留(因此 GEML 文档可以引用这一语法本身),原样(raw)块体从不
插值,反斜杠转义的 \ 渲染为字面文本 ``。hidden 标志把一个块(或 %% 行)标记为属于文档、且完全参与引用校验,但
不渲染——例如只为图表供数的源表。%% 行是隐藏的、原样的、永不渲染的备注。#id、.class、key=val。内联元素只出现在无栅栏区块内部。
| 语法 | 含义 |
|---|---|
*强调* |
强调(emphasis) |
**加重** |
加重(strong) |
`代码` |
代码片段(原样;内部不解析) |
~~删除~~ |
删除线 |
$…$ |
内联数学(正文原样) |
{…} |
就地媒体嵌入(图片/音频/视频) |
行尾 \ |
强制换行 |
\ + ASCII 标点 |
转义:该标点取字面值 |
=== math 类型块(§3)。![…] 就地渲染/播放其源(绝不跳转),链接 […] 则跳转。as ∈ {image,
audio, video},省略时按源扩展名推断。[ ](未完成)或 [x]/[X](已完成)后跟一个
空格。该标记从项文本中剥离并记为勾选状态;其余文本按内联解析。内部与跨文档引用均在构建时校验。
| 形式 | 含义 |
|---|---|
[文字](https://…) |
外部链接 |
[文字](#budget) |
指向块 budget 的内部引用,文字自定义 |
[[#budget]] |
自动引用:链接文字取自目标的 caption/标题 |
[文字](other.geml#budget) |
跨文档引用 |
[^note] |
脚注:把 id 为 note 的块渲染为脚注 |
[文字](url){rel=nofollow target=_blank}。#id、other.geml#id 或 [^id] 是构建错误。[^id]: 文本(Markdown 风格):它记录一个
带该 id 的 note 块,使匹配的 [^id] 引用得以解析。无栅栏区块的内联解析分两个阶段进行,并为每个输入指派恰好一个解析。
阶段一——atom(从左到右,按此优先级):
\ + ASCII 标点 → 该字面字符;行尾 \ → 硬换行)、代码片段、内联数学;
其内容不再进一步解析。[[#id]])与脚注引用([^id]);链接或引用不得嵌套在另一个
链接或引用内部。atom 之间的文本是字面文本。被转义的定界字符是一个字面 atom,因此不参与强调。
阶段二——强调在每一段位于阶段一 atom 之间的最大字面文本上运行;强调不跨越 atom,也 不跨越块边界。强调、加重、删除线由定界符游程 flanking 解析:
* 的最大连续串,或两个及以上 ~ 的最大连续串(单个 ~ 是字面)。* 对是强调(每侧消耗一个)或加重(两者都 ≥ 2 时每侧消耗两个);匹配的
~~ 对是删除线(每侧消耗两个)。任何未配对的定界符都是字面文本。这是 CommonMark 强调算法在 GEML 定界符上的限定版:只有 * 和 ~~,没有 _ 强调。
块类型 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% |
src="data.csv"(相对文档的路径,或
http(s) URL)配 format=csv/tsv 加载数据。与媒体 src(§5)一样,它在渲染期
获取、不在构建期校验存在;进入 .gemlhistory 哈希的只有 src 这串文本,绝非
文件内容。表格不得同时给 src 和内联正文(错误)。因为数据在渲染期才到,compute
与引用它的 geml-chart 所用的列名也在那时校验,而非构建期。内联仍是默认;src 是显式
选择。span 声明、不画线:span="r2c1:2x1"。compute 列出一条或多条 列名 = 表达式 公式,以 ; 分隔。每条
表达式对每个数据行求值一次,运算符限 + - * / ( ) 与一元 -(*// 优先级高于
+/-,左结合),仅作用于数值单元格。列按表头名引用(名字含空格用引号,如
'Unit Price'),或按电子表格列字母引用(A、B…)。公式可引用更靠前的计算列
(上例 YoY 引用 FY);引用必须无环。计算列按公式顺序追加在数据列之后,正文中
不再书写。summary 定义表尾的单独一行,由 单元格 = 值 条目组成、; 分隔,
左侧指明目标列。每个 值 要么是用作标签的字符串/数字字面量(Segment = 'Total'),
要么是把聚合函数 sum、avg、min、max、count(各作用于一列)用 + - * / ( ) 与
字面量组合而成的表达式((sum(FY) - sum(PriorFY)) * 100 / sum(PriorFY))。聚合把
一列在数据行上折叠,是唯一跨行的构造;汇总表达式中每个列引用都必须被聚合归约(裸
列名在汇总行没有值)。未指定的列留空。[printf] 格式:FY [%.1f]、
YoY [%.1f%%](%% 为字面百分号)。格式仅作用于数值的显示,不改变存储值。不支持
日期/时间格式:单元格值只有字符串、数字、布尔(§4),日期按 ISO-8601 纯文本书写。@3$4、
@2$1..@4$3)、相对行引用(@-1)、条件式、跨表 remote() 引用、查表/VLOOKUP,
以及任何嵌入程序(无 Lisp、无 JS)。块类型 diagram 托管外部图形 DSL。
=== diagram {#flow format=mermaid caption="评审流程"}
graph LR
A[草稿] --> B{评审}
B -->|通过| C[发布]
B -->|打回| A
===
format 选择可插拔渲染器(mermaid、graphviz、d2、plantuml…)。raw,原样交给该渲染器。format 产生告警,保留
正文。#flow 让该图可被引用:见 [[#flow]]。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 营收"}
===
type —— bar | line | area | pie | scatter,只改画法,绝不新增属性。x(类目)、y(数值;逗号列表即多系列)、series(按列
分组)、size(散点气泡)。必填 x、y。类型用不到的通道给告警。rows —— data(默认,排除汇总行)、all(数据行 + 汇总行作为额外一点)、
summary(只画汇总行)。data id、rows 都对照表校验:写错列名或悬空 id = 构建错误。=== diagram {format=vega-lite data=#fy25},spec 写进 body。body 为 raw、不校验列名。本规范定义三个合规等级。产品分别声明各自的合规:一个从不渲染的校验器可以是合规 解析器而不是合规渲染器,这并不使它不合规。
合规 GEML 文档指这样一段归一化字符流(§0.5):合规解析器处理它时不产生任何严重级别
为 error 的诊断(附录 A)。
告警不使文档变得不合规:它们标记的是处理器无法完全解释、但必须(MUST)保留的构造—— 未知块类型、未知图格式、未经校验的跨文档引用。
尽管如此,任何输入都是可解析的:§2.1、§3、§5.3 与 §6 为任意字符流指派恰好一个文档 模型。不存在任何输入是合规解析器可以拒绝、拒绝建模或失败于其上的——不合规的文档同样 产出模型,只是同时带有描述它的错误。
合规解析器必须(MUST):
type 和未知图 format 当告警而非错误,原样保留正文。渲染器是可选(OPTIONAL)的:合规解析器不必产出任何呈现格式的输出。若产出,则必须 (MUST):
raw 块的正文(§3)。code 块,不解释 diagram 正文(§7)——只能将其交给已注册的外部渲染器
(§9.1)。hidden 的块(§4),同时在模型中保留它们。规范配套一套一致性测试集:输入 .geml 与期望文档模型的归一化投影成对。对本文档以
算法方式陈述的规则——内联强调(§5.3)、列表嵌套(§2.1)、原子优先级、元数据插值
(§4)——该测试集是规范参照。第二个独立实现复现每个用例即为合规。在参考仓库中它位于
geml-parser/test/conformance/。
规范的版本独立于任何实现。本文档为 GEML 1.0;参考实现的包版本遵循其自身发布节奏, 不是规范版本。
实现以「符合 GEML 1.0」的形式声明合规。处理器遇到不认识的构造时必须(MUST)按 §8.2(6) 降级——这就是本格式的前向兼容机制,也是新增一种块类型或图格式不构成破坏性变更的原因。
类型注册表(§3)是开放的。非本规范定义、亦未注册的类型名应(SHOULD)包含连字符
(例如 acme-invoice),把不含连字符的名字留给本规范的未来版本。图的 format 名遵循
同一约定。
GEML 文档常常由机器生成,也常常不可信:它可能来自模型、流水线或一个 pull request。本节 规定文档怀有恶意时处理器必须保证什么。它适用于 §8 的每个合规等级。
处理器必须不(MUST NOT)执行或求值文档的任何部分:
code 块的正文是存储的文本;必须不(MUST NOT)被运行(§3);output 块是已记录的结果;处理器必须不(MUST NOT)通过执行任何东西来产生它(§3);diagram 正文必须(MUST)原样传给由 format 选定的外部渲染器,处理器必须不
(MUST NOT)解释它(§7);处理器必须(MUST)为自己在文档上的递归深度设定上界,且分别针对:类型块嵌套(§3)、
列表嵌套(§2.1)、内联嵌套(§5)。到达上界时必须(MUST)产出对应的
*-nesting-too-deep 错误(附录 A)并继续处理剩余输入。它必须不(MUST NOT)溢出调用栈、
中止,或无法产出模型。
上界由实现自定;处理器应(SHOULD)各自至少允许 64 层,这已远超任何为阅读而写的文档。 参考实现允许 256 层块嵌套与列表嵌套、100 层内联嵌套。
处理器必须不(MUST NOT)在未按目标文法转义的情况下,用文档可控文本构造正则表达式、
shell 命令或任何其他可执行形式。块 id、类名与属性值都是文档可控的;.geml 文件是不可信
输入,与 .zip 同理。
引用解析必须(MUST)在任何输入上终止,包括为使其死循环而精心构造的输入:
#id 是一次查找,而非遍历。compute-error。GEML 表格不需要环检测器:求值顺序
在构造上就让依赖图无环。解析跨文档引用(§5.2)会读取由文档指名的文件。处理器必须(MUST)把该解析限制在显式配置
的根目录内,必须(MUST)在判定目标是否位于根内之前解析每一个符号链接,并且必须(MUST)
拒绝逃逸出根目录的目标。解析必须(MUST)失败即关闭:无法确立限制根的处理器不解析
任何东西并报 unresolvable-document,而不是回退到不受限的查找。
表格的 src=(§6)与媒体 src(§5.1)在渲染时由渲染器抓取,解析器从不读取它们。
渲染器必须(MUST)把这类来源当作不可信输入。在文档可能来自不可信作者的场合,渲染器应
(SHOULD)把 src 限制在文档自身的源或目录内,并应(SHOULD)要求显式选择加入才执行
http(s) 抓取:被抓取的 URL 会把读者的地址、以及阅读这一事实与时间,泄露给控制该 URL
的人。
由于外部数据在渲染时抓取,其内容从不进入 .gemlhistory 哈希——只有 src 文本会进入
(§6)。
链接或嵌入(§5.1、§5.2)中的目标地址,若其 URL 方案不属于 http、https、mailto、
tel,则必须不(MUST NOT)被产出为可导航或可加载的目标。处理器必须(MUST)在构建模型
时施加此检查,而不是在渲染落点,这样模型的每个消费者都继承该保护。判定方案时该检查
必须(MUST)忽略位于 U+0000–U+0020 范围内的前导字符与内嵌字符,因为用户代理在对 URL
采取行动前会先剥除它们——java	script: 就是 javascript:。
产出标记语言的渲染器必须(MUST)按文档可控文本所处的位置——元素文本、属性值或 URL——
对其转义,并且必须(MUST)把 .class 记号(§4)削减到目标格式的标识符字符集,而不是
仅仅转义它们。
合规解析器产出的每一条诊断,除人类可读的消息外都携带一个代码。消息是散文:它可 (MAY)在版本之间被改写、翻译或补充上下文。代码与严重级别才是契约——它们是一致性 测试、编辑器集成或 CI 关卡所匹配的对象,处理器必须(MUST)以本附录指派的代码与严重级别 报告诊断。
对于本目录已覆盖的情况,处理器必须不(MUST NOT)另造目录之外的代码。处理器可(MAY)为
本规范未定义的情况产出额外诊断;这类代码应(SHOULD)带一个连字符分隔的厂商前缀
(acme-…),以免本目录的未来版本与之冲突。
诊断携带的行号从 1 开始,指归一化字符流(§0.5)中的行——按 §0.5,它同时也是原文件中的 行号。
完整的代码、严重级别与条件对照表见英文版附录 A: 该表是规范性的,并由参考实现的测试逐条机械校验,因此此处不作重复以免译文漂移。