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" | 不是构造——保持段落。 | ❌ |
| 段落 | 任意文本 | 相同 | ✅ |
| 类型块 | — | === 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 | 不是构造——字面文本 | ❌ |
| 图片 |  | 相同,并推广到音频/视频(按扩展名或 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] + === 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)。