geml

GEML 与 CommonMark —— 逐构造对照

*English 中文*

CommonMark 0.31.2 是「Markdown 到底是什么意思」的 基准。本页按它的规范顺序走一遍每个构造,记录 GEML 对它的处理,最后列出 GEML 独有、 CommonMark 没有对应物的部分。

想看 Markdown / HTML / AsciiDoc / Org-mode 的横向能力矩阵,见 COMPARISON_CN.md。本页只对 CommonMark,但对得更深。

每一格「GEML」都是拿参考解析器实测出来的,不是照规范抄的——因为真正值得看的行, 恰恰是「同样的写法,出来的东西不一样」那些。GEML 不实现的构造不报错,它们退化成 普通文本,这正是需要知道它们的原因。

图例 —— ✅ 语法与语义相同 · ⚠️ 语法相同但结果不同 · 🔁 语法不同但能力对应 · ❌ 在 GEML 中不是构造(按普通文本解析)


1. 预备

主题 CommonMark GEML  
编码 刻意不规定 —— “This spec does not specify an encoding” 必须 UTF-8(§0.1) 🔁
为什么 只用于渲染 .gemlhistory 对文档字节做哈希,编码是承重结构  
BOM 未提及 移除开头的一个 U+FEFF(§0.2) 🔁
行结束符 LF / CR / CRLF 都认 同样三种,解析前归一为 LF(§0.3)
空行 无字符,或只有空格/制表符 相同(§0.3)
U+0000 替换为 U+FFFD 相同(§0.4)
制表符 复杂:4 列制表位、跨边界时部分展开(单独占一节) 一个制表符算一列,用于列表缩进(§2.1) ⚠️
反斜杠转义 任意 ASCII 标点 相同(§5.1)
实体引用 & © # " 全部解码 不解码 —— 按字面文本
非法输入 不存在这个概念:任何字节序列都是合法文档 文档可以带 error 诊断(附录 A);合规文档是没有 error 的那些(§8.1) 🔁

最后一行是最深的差异,并且决定了其余一切:CommonMark 没有失败态,所以它那 655 个示例 是用来钉「每个输入是什么意思」;GEML 是构建期校验的——悬空引用是带稳定代码的 error——所以它的规范既要钉意思,也要钉诊断。


2. 叶子块

CommonMark 构造 示例 GEML  
ATX 标题 # H1###### H6 相同,并且自动派生 #id## My Section#my-section);显式 id 写作 ## Sec {#s}
闭合序列 ## H2 ## 尾部的 ## 当作文本保留(”H2 ##”) ⚠️
setext 标题 Title / ===== 不是构造——保持段落。排除它是为了让 = 只属于围栏(§1.6)
分隔线 --- *** ___ 不是构造——保持段落(§1.6)
缩进代码块 4 个空格 不是构造——保持段落。用 === code
围栏代码块 ` js ` … ` ` 变成内联代码 span,不是块 —— 见 §6 ⚠️
同上(波浪号) ~~~js~~~ 保持段落
HTML 块 <div>…</div> 字面文本。按约束不存在原始 HTML 逃逸口(§1.5)
链接引用定义 [foo]: /url "t" 不是构造——保持段落。但脚注形式 [^f]: text 是支持的
段落 任意文本 相同
类型块 === type {attrs}=== —— 代码/公式/表格/图形/提示框/元数据共用的唯一原语(§3) 🔁

3. 容器块

CommonMark 构造 示例 GEML  
引用块 > quoted 不是构造——保持段落。提示框用 === note 🔁
嵌套引用块 > > deep 同上——字面文本
无序列表 - * + 只有 -*;+ 不是标记 ⚠️
有序列表 1. 1) 只有 1.;1) 不是标记 ⚠️
起始编号 5. 设定 start 相同(§2.1)
紧凑 / 松散 项之间有空行则松散 相同(§2.1)
嵌套 按缩进,有内容列规则 按缩进,列计数,制表符 = 1 列 ⚠️
多段落列表项 支持 不支持 —— 一项就是一行;富内容放进类型块(§2.1)
任务列表项 GFM 扩展,不属于 CommonMark 核心语法:- [ ] / - [x](§2.1) 🔁

4. 内联

CommonMark 构造 示例 GEML  
代码 span `code` 相同,反引号游程规则也相同
强调 *em*_em_ 只有 * —— _em_ 是字面文本(§5.3) ⚠️
加粗 **st**__st__ 只有 ** —— __st__ 是字面文本 ⚠️
定界符算法 flanking 游程 + 三的规则 完全相同的算法,只是限定在 *~~(§5.3)
删除线 GFM 扩展,不属于 CommonMark 核心语法:~~s~~(单个 ~ 是字面) 🔁
内联链接 [t](/url) 语法相同。但不带 scheme 的目标会被当成文档引用而非 href —— 见 §6 ⚠️
链接标题 [t](/url "ti") 那不是链接标题;选项写进属性对象:[t](url){rel=nofollow} ⚠️
引用式链接 [t][ref] + [ref]: /url 不是构造——字面文本
图片 ![alt](/img.png) 相同,并推广到音频/视频(按扩展名或 as=,§5.1)
自动链接 <https://e.com><a@b.com> 不是构造——字面文本。写 [text](url)
内联原始 HTML a <b>bold</b> c 字面文本(§1.5)
硬换行 行尾两个空格, \ 只有 \ —— 行尾空格不起作用 ⚠️
软换行 段落内的换行 相同
内联公式 $E=mc^2$,正文逐字(§5.1) 🔁

5. GEML 独有

CommonMark 没有对应物——这些正是这个格式存在的理由。

能力 语法 说明
类型块 === code {lang=js}=== 代码/公式/表格/图形/提示框/元数据共用一个原语。围栏是 ≥3 个 =;更长的围栏可嵌套;正文自身含围栏时用 === #id 按标签闭合(§3)
块身份 {#budget} 文档内唯一——块的主键(§4)
类 / 参数 {.warning caption="Annual cost"} 语义类与类型自定义参数(§4)
可校验引用 [t](#budget)[[#budget]]other.geml#budget 构建期解析;悬空引用是 error,而不是事后才发现的坏链接(§5.2)
自动引用 [[#budget]] 链接文字取自目标的 caption / 标题
脚注 [^f] + [^f]: text 核心语法,不是扩展
文档元数据 === meta,每行 key = value 取代 frontmatter
插值 `` meta 取值;未知 key 是 error;代码 span 与公式内不替换;\ 转义
计算表格 compute="FY = Q1+Q2+Q3+Q4"summary="…sum(FY)…" 构建期算出计算列与汇总行;公式按求值顺序天然无环(§6)
外部表数据 src="data.csv" format=csv 渲染时抓取
合并单元格 span="r2c1:2x1" 声明式,不是画出来的
托管图形 === diagram {format=mermaid} 正文原样交给外部渲染器;GEML 自身不定义图形语言(§7)
绑定数据的图表 === diagram {format=geml-chart data=#fy25 type=bar x=Segment y=FY} 图表完全由属性描述,所以列名能在构建期对着表校验(§7.1)
可寻址散文 === text {#intro} 给一段散文一个 id,使其可被引用、可块级编辑
捕获的输出 === output {of=#code-id} 代码块的结果,由工具记录,永不执行
隐藏内容 {hidden}%% 行 在模型中且受引用校验,但不渲染
诊断 稳定代码 + 固定严重级别(附录 A)
版本 .gemlhistory 边车 块级撤销(geml revert file #id

6. 会让 Markdown 作者意外的四件事

以下是「同样的按键、出来不同文档」的几行,四条都是实测的,不是推断的。

1. ` ``` ` 围栏是代码 span,不是代码块。

```js
x = 1
```

解析结果是一个段落,里面一个内联代码 span,值为 "js\nx = 1\n" —— 反引号游程开启了 代码 span,换行落在它内部,闭合游程结束它。不报任何错,渲染出来也大致像那么回事, 这正是它成为陷阱的原因。要用类型块:

=== code {lang=js}
x = 1
===

2. 不带 scheme 的链接目标是文档引用。

[t](/url) 产生的链接其 doc/url —— GEML 会试图把它当跨文档引用解析,并 告警 unchecked-cross-document-reference[t](https://example.com) 才是普通 href。 而且只有 httphttpsmailtotel 会被输出为可导航目标(§9.5)。

3. _下划线___双下划线__ 是字面文本。

GEML 跑的是 CommonMark 一模一样的定界符游程算法,但只作用于 *~~。这是故意的: 标识符里的 _my_var_name)是 Markdown 最常见的误强调 bug,而去掉它没有任何代价—— * 覆盖同样的场景。

4. 行尾两个空格不是换行。

只有行尾的 \ 是。一个会被去尾空格工具静默删掉的不可见语法,不该出现在一个以 diff 和 机器编辑为目标的格式里。


7. 迁移

geml notes.md 把 Markdown 转成 GEML:

Markdown 变成
YAML frontmatter === meta
` ```lang ` 围栏代码 === code {lang=…}
` ```mermaid ` / graphviz / … === diagram {format=…}
$$ … $$ === math
> 引用块 === note
GFM 管道表格 === table
[^f]: text 脚注定义 === note {#f}
<https://e.com> 自动链接 [https://e.com](https://e.com)
setext 标题 ATX 标题
分隔线 丢弃(会作为 note 报出)

转换出的块会自动获得 id#note-1#code-1#table-1 …),所以结果立刻就是 可寻址、可块级编辑的。内联语法原样通过,因为 GEML 的内联文法是 Markdown 作者实际会用 的那个子集的超集。

geml <file.geml> --to md 是反向转换,天然有损——Markdown 没有类型块原语——所以每个 无法映射的构造(geml-chart{hidden}、块 id)都会被报出,而不是静默丢弃。


8. 这些差异从何而来

CommonMark 的任务是说清「人们已经在写的 Markdown」是什么意思,且没有失败态。它的约束是 兼容十年积累的既有文档,所以它必须带着 setext 标题、四种强调写法、两族列表标记,以及 一个 HTML 逃逸口。

GEML 没有这些包袱。它的约束(§1)是:文件无需渲染即可当纯文本读、所有结构化内容共用 一个原语、引用在构建期校验、语义不绑定任何渲染后端。上面每一处「删减」都在买其中 一条:去掉 setext 与分隔线,=--- 就腾给围栏且不再歧义;去掉原始 HTML,语义就与 后端无关;去掉 _ 强调和行尾空格换行,就消掉两类静默误解析。

结果是一个更小的语法面,对接受什么更严格——而且第二个实现能照着规范复现出来,这正是 GEML 给自己定的验收标准(§8.4)。