Skip to content

GEML 与 CommonMark —— 逐构造对照 ​

English | 中文

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

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

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

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


1. 预备 ​

主题CommonMarkGEML
编码刻意不规定 —— "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"不是构造——保持段落。❌
段落任意文本相同✅
类型块—=== 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 游程 + 三的规则flanking 规则与三的规则相同,限定在 * 和 ~~(§5.3)🔁
强调跨越内联 atom*a [链接](x) b* 是包住链接的强调相同 —— phase 2 在整个内联序列上运行,atom 是不透明单元,一对定界符可以包住链接、代码 span、公式或图片(GEP-0007,已接受)✅
删除线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)❌
内联原始 HTMLa <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] + === note {#f}核心语法,不是扩展
文档元数据=== meta,每行 key = value取代 frontmatter
插值{{key}}从 meta 取值;未知 key 是 error;代码 span 与公式内不替换;{{key}} 转义
派生关系表格之上的 view:compute="FY = Q1+Q2+Q3+Q4"、summary="…sum(FY)…"、where/order/limit/select/by构建期算出计算列与汇总行;公式按求值顺序天然无环(§6)
外部表数据src=data.csv、src=#fy25、src=other.geml#fy25一个属性、三种目标;本地路径与跨文档目标在构建期做存在性校验,而 src 与内联正文并存是错误——数据永远只有一个来源(§6)
自定义分隔符delim=";"覆盖该 format 的天然分隔符,于是欧洲式 ; 分隔的 CSV 或 | 分隔的导出文件可以原样读入;值超过一个字符是错误(§6)
受校验的 data 块=== data {#limits format=json} … ===JSON 的值域——标量、序列、映射——作为构建会解析的数据;正文格式不对就是错误,并指出出错的那一行。format=jsonl 是记录流形态:在文件末尾追加一个完整的 data 块,对任何文档都是合法的续写(§3.2)
数据取自普通文件=== data {#events format=jsonl src=events.jsonl}记录仍留在一个现有工具都能 append、能 tail 的 .jsonl 文件里;文档则成为它受校验、可寻址、可制图的视图。schema= 为保留属性(§3.2)
源路由=== code {lang=ts src=src/parser.ts#L14-24}这个块就是真实文件的那几行(1 起、闭区间)。文件里若已没有这些行,构建报错(bad-source-range);与 src= 并存的正文是快照,一旦与源漂移就告警(§3.3)
托管图形=== diagram {format=mermaid}正文原样交给外部渲染器;GEML 自身不定义图形语言(§7)
绑定数据的图表=== diagram {format=geml-chart data=#fy25 type=bar x=Segment y=FY}图表完全由属性描述,所以列名能在构建期对着目标校验。data= 可指向 table,也可指向值为记录数组的 data 块(§7.1)
可寻址散文=== text {#intro}给一段散文一个 id,使其可被引用、可块级编辑
块级嵌入=== embed {src=other.geml#id}就地渲染另一个文档的块。src= 受引用校验,嵌入成环是错误;被指名的文档是作为一篇独立文档解析的,因此它的元数据与相对路径都相对它自己解析——引用链的每一层都如此(§3)
行内投影![[#intro]]、![[other.geml#intro]]同一件事,但写在句子中间。只有行内内容才够格——指向块级内容是错误,那种场合该用 embed 块(§5.1)
隐藏内容{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。 而且只有 http、https、mailto、tel 会被输出为可导航目标(§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)。

Code MIT · Specification CC BY 4.0