独立性对照 · 两种标记语言 · 2026-10-06 实测 · English · 全部图解 · GEML 首页

GEML 与 Carve:一个为排版,一个为 agent

Carve .crvGEML .geml

两种语言都诞生于 2026 年,都想在 Markdown 之后再走一步,但方向不同:Carve 为排版和展现优化,追求更丰富的表现力;GEML 为 agent 优化,追求更强的元素操纵能力,它更像一个容器:用同一套地址和标准动作,容纳并操纵文档内外的各种文本形式。本页把它们的 82 个语法构造和每一项能力逐一摆在一起,标出各自的设计来源,再用两边的官方解析器跑同一批输入。两者写法相同的地方只有 13 条,全部来自更早的公共来源;其余部分各自成体系。

13写法相同的 13 条,都来自公共来源

ATX 标题、[t](url)、![](src)、`code`、[^id]、- [ ]、{#id .class k=v}、%% 等,分别来自 Markdown(2004)、PHP Markdown Extra、GFM、Pandoc、Obsidian/Mermaid、CommonMark。

29 · 11各自的独有写法互不重叠

Carve 独有 29 条:/斜体/ *粗体* _下划线_ =高亮=、|= 表头、^ 题注、::: 容器、</#id>、CriticMarkup 等。GEML 独有 11 条:类型块原语、数据形态表格、view、data、块内坐标、带标签闭合等。同一段文本两边的解析结果完全不同(实测 P7)。

两个方向一个为排版,一个为 agent

Carve 是 Djot 的继任者,力气花在内联表现、表格版式、题注编号和多种输出上。GEML 的力气花在地址、按块读写、写入护栏、块级历史和引用校验上,让 agent 能精确、安全地改动文档里的任何一个元素。

相反关键设计决定方向相反

Carve 否决了 {{name}} 变量,GEML 把 {{key}} 放进核心;Carve 让找不到目标的引用原样留给宿主,GEML 规定它是构建错误;Carve 默认放行 =html,GEML 没有 HTML 通道。

09-30两个项目何时知道对方

Carve 的仓库里没有任何关于 GEML 的记录。GEML 第一次记下 Carve 是 09-30 的一份同类项目调研,那时 GEML 的核心设计都已上线一到三个月。

229profile 与 view 都是一步步重构出来的

profile 从写进核心规范,到 08-31 抽成应用层机制,六个 profile 累计 229 次提交;view 从 table 的计算属性拆成独立的块。详见第五节。

一 · 设计方向

一个为排版,一个为 agent

Carve 想让文档呈现得更好,GEML 想让 agent 能精确、安全地改动文档里的每一个元素。左右两栏是两边各自投入最多的地方;下表是由此带来的设计取舍,每一行都引自两边各自的文档或首个提交。

Carve为排版与展现优化

投入集中在「一份文档能呈现得多好」。

  • 内联表现力下划线、高亮、上下标、粗斜体、语义 span(kbd、abbr、time)、语言标注、提及与标签。
  • 排版细节智能标点(破折号、省略号、箭头、©)、不换行空格、诗歌行块、续接标记。
  • 表格版式合并单元格、水平与垂直两轴对齐、续行、表头与表尾行数。
  • 题注与编号图、表、代码清单、公式的题注,Figure # 自动编号,复合图面板。
  • 编辑痕迹CriticMarkup 的插入、删除、替换与批注。
  • 多种输出HTML、Markdown、纯文本、ANSI 终端,并报告每种输出丢了什么。

GEML为 agent 操纵元素优化

投入集中在「一个程序能多精确、多安全地改动文档里的一个元素」。

  • 一切皆有地址显式 id、标题推导 id、两块之间散文的派生地址、内容地址 @<hex>、块内坐标 #t[2]["Q1"]、行号选择 L27-58。
  • 按地址读写get / set / add / delete / rename / revert 只动目标块;实测改一格,400 字节里 399 字节原样不动(P9)。
  • 写入有护栏每次写入前整份重查,结果不能解析就拒绝;#id@<hex> 拦住基于过期内容的写入;rename 同步改掉全部引用。
  • 改坏能退.gemlhistory 版本链,单块回滚,整链可重算校验。
  • 引用是契约八类引用在构建期校验,agent 改完一块立刻知道有没有弄断别处(P1、P3–P6)。
  • 数据可推导table、view、data 与绑定的图表:改一个数,汇总和图跟着变。
  • 给 agent 的接口MCP 的 11 个读写工具与 agent 技能;读一块只占一块的上下文(实测 133 B / 400 B)。

GEML 更像一个容器

同一套地址和标准动作,既作用于文档里的各种文本形式,也伸到文档外的文件、数据、代码库和其他文档。

文档内 · 容纳的文本形式
  • === code 任意编程语言,原样保存
  • === math TeX 公式
  • === diagram mermaid、vega-lite 等外部 DSL
  • === table 管道表、CSV、TSV
  • === data JSON、JSONL(YAML、TOML、EDN 已预留)
  • === meta 带类型的键值
  • === note、=== text 流式正文
  • 不认识的类型:告警,正文原样保留
GEML 文档

一种围栏 === type {attrs},由类型注册表决定正文按 raw、flow 还是键值来读。

同一套标准动作
listfindgetsetadddeleterenamerevertcheckhistoryto
同一套地址
#id## 标题=== type@<hex>L27-58#t[2]["Q1"]{lang=py}--within

命令行、MCP(工具名就是命令路径,07-29 起)和各 profile 的动词共用这一套。

文档外 · 伸出去的引用
  • code src=f.py#L10-20 源码文件的一段,行范围越界即报错
  • table src=data.csv 本地或 http(s) 的数据文件
  • data src=log.jsonl#L1-500 JSON、JSONL、YAML 文件的一段
  • data= 图表取数:CSV、JSON,或另一份文档里的表
  • embed src=doc.geml#id、[[doc.geml#id]] 其他文档里的块;get --view 可直接读穿嵌入
  • profile 伸得更远:codemap 把整个代码库写成文档,media 管音视频素材,translator 让另一种语言的文档成为投影

向下兼容 Markdown:内联写法沿用 Markdown;.md 文件不必转换就能 list、find、get、set,写进去的仍是 Markdown 语法(10-02);需要时一条 geml notes.md 就能转成 GEML。

Carve 的模型是「一份标记文档渲染成多种输出」。引入处理器只收 Carve 或纯文本,由宿主在解析器之外展开,核心解析器不读文件;命令行的动作作用于整篇文档(render、fmt、lint、diff、merge、migrate、flatten),按块的读取与替换 10-05 才出现在它的 MCP 服务器中。

维度CarveGEML
要解决的问题作为 Djot 的继任者,修掉 Markdown 二十年来的歧义,让写作者好学好记(docs/technical-rationale.md)让人和 AI agent 共写同一份文档:对人是纯文本,对机器可寻址、可校验、可版本化(初稿 §1「万物可寻址」)
自述的祖先Djot、Org-mode(/斜体/)、Creole(|=)、AsciiDoc(提示框)、CriticMarkup(Init 提交 README)Markdown/CommonMark、AsciiDoc、reStructuredText、Org-mode、MyST、Mermaid/Graphviz、TOML(06-17 初稿序言与提交说明),其中没有 Djot,也没有 Carve。属性写法另注明以 Pandoc 为祖先(06-18 COMPARISON.md);10-01 网站的「影响来源」一节列入 Djot
与 Markdown 的关系刻意改变含义:*x* 是粗体、_x_ 是下划线;.crv 不能交给 Markdown 解析器向下兼容 Markdown:内联沿用 Markdown;CLI 能直接对 .md 文件按块读写,不必转换,写入仍是 Markdown 语法
块的组织方式每类内容一套专用语法:``` 代码、$$` 数学、::: 容器、|= 表格、^ 题注、--- 前言一个类型块原语 === type {attrs} 承载代码、数学、表格、图、数据、注释、元数据,正文模式由类型注册表决定
外部内容引入处理器只收 Carve 或纯文本,由宿主在解析器之外展开(includes.md)src= 指向源码行范围、CSV、JSON/JSONL/YAML 和其他文档里的块,本地来源在构建期校验
动作的单位整篇文档:render、fmt、lint、diff、merge、migrate、flatten地址选中的元素:块、整节、散文、行范围、单元格;同一套动词覆盖命令行、MCP 与 profile
引用找不到目标时原样保留,交给宿主程序解决(extension-contract.md §12.4)构建错误,文档不合格(规范 §5.2、§8)
变量 / 插值列入「已否决语法」:把标记和模板混在一起(dismissed-syntax.md){{key}} 是核心语法,取值来自 === meta,拼错即报错
原始 HTML裸 HTML 是字面文本;```=html 透传在 JS/Rust 实现中默认开启完全没有 HTML 通道;文档是数据,从不是代码(规范 §9.1)
块的身份作者写的 {#id} 加上会话级节点 id;后者「在源码里没有写法」,会话结束即失效(ast-node-identity.md)写在源码里的 #id,加上由相邻块推导出的散文地址;它是读、写、校验、版本历史共用的键
元数据--- YAML 前言,原样保存=== meta 键值块,值有类型(字符串/数字/布尔),多个 meta 合并,#meta 可寻址
历史没有存储的历史;比较两个文件(carve diff)、三方合并(carve merge).gemlhistory 旁车文件:只追加的版本链、哈希校验、单块回滚
二 · 语法逐项

每一个语法构造,两边各怎么写

Carve 依据其规范仓库 HEAD bb09baea(2026-10-06)的 docs/cheatsheet.md 与 resources/grammar.ebnf;GEML 依据 spec/GEML-spec.md 附录 B 的语法清单及各节正文。点下面的按钮筛选。

同源继承两边写法相同,都来自更早的公共来源
同形异义符号或名字相同,含义不同
同题异解同一需求,写法与设计不同
仅 Carve
仅 GEML
构造CarveGEML判定说明 / 公共来源
行内
斜体/x/*x*同题异解Carve 取自 Org-mode;GEML 沿用 Markdown
粗体*x***x**同形异义*x* 在 Carve 是粗体,在 GEML 是斜体(实测 P7)
粗斜体/*x*/无专门写法仅 CarveCarve 自创
下划线_x_无;_x_ 原样输出仅 Carve实测 P7
删除线~x~~~x~~同题异解GEML 用 GFM 写法;Carve 的单波浪线在 GEML 里不是语法
高亮=x=无仅 CarveInit 时为 ==x==,06-11 改单字符
上标 / 下标{^x^} {,x,}无仅 Carve花括号强制形式来自 Djot
词内强制定界H{,2,}O 等无仅 CarveDjot 式
行内代码`x``x`同源继承Markdown(2004)
行内字面量!`x`无仅 CarveCarve 07-21 新增
行内数学$`x`$x$同题异解Carve 05-21 起改用 Djot 写法;GEML 用 LaTeX/Pandoc 的 $…$
原始行内输出`x`{=html}无,规范禁止仅 Carve来自 Djot / Pandoc RawInline
链接[t](url)[t](url)同源继承Markdown(2004)
引用式链接[t][ref] + [ref]: url无仅 CarveMarkdown 原有,GEML 未收
文内锚点链接[t](#id)[t](#id)同源继承写法同为 Markdown;但只有 GEML 校验目标存在(实测 P1)
自动取文字的交叉引用</#id>[[#id]]同题异解共同概念来自 LaTeX \ref、AsciiDoc <<id>>;GEML 初稿注明「这正是 AsciiDoc 有、HTML 从无的能力」
跨文档引用普通链接 [t](a.crv#id),不校验[t](a.geml#id) [[a.geml#id]],校验同题异解实测 P3:Carve 放过不存在的目标,GEML 报错
维基式链接[标题文字][];可选扩展 [[Heading]][[#id]],指向 id 而非标题文字同题异解[[ ]] 来自 MediaWiki / Obsidian / Logseq
图片 / 媒体![alt](src) 图片![alt](src) 图片、音频、视频,按扩展名推断同源继承Markdown;GEML 把它推广为就地媒体嵌入(06-17)
行内内容投影{{ path #id }} 引入处理器,非核心![[#id]] 核心语法,投影一个 text 块同题异解![[ ]] 是 Obsidian 的嵌入写法
脚注引用[^l],定义 [^l]: …;行内 ^[…][^id],指向任意带该 id 的块同源继承PHP Markdown Extra(2005);两边的定义方式不同
元数据插值已否决{{key}},转义 \{{仅 GEMLCarve 在 dismissed-syntax.md 中明确拒绝;GEML 06-18 加入,未定义的键是错误(实测 P5)
{{ … }} 这对符号文件引入 {{ path #id @lines:N-M }}元数据插值 {{key}}同形异义Carve 要求括号内有空格,{{path}} 是普通文本;GEML 的 {{key}} 与 Mustache、Hugo 一类模板同形
行内 span 与属性[t]{.c}、/x/{.c}仅链接与媒体可带属性仅 CarveDjot 的 span
语义 span[Tab]{kbd} {abbr=…} {time=…}无仅 CarveCarve 自创
语言标注{:fr} {:}无仅 CarveBCP 47
行内扩展:name[x]{a}无仅 Carve形似 remark-directive
符号:rocket:无仅 CarveDjot symbol
提及 / 标签@user #tag无仅 Carve社交平台惯例
编辑批注{+ +} {- -} {~a~>b~} {#注#}无仅 CarveCriticMarkup
智能排版-- ... --> (c)不做替换仅 CarveDjot / SmartyPants
转义\ + ASCII 标点\ + ASCII 标点同源继承CommonMark(2014)
硬换行行尾 \行尾 \同源继承CommonMark
不换行空格行中 \␠无仅 CarveDjot
块
标题#…#######…######同源继承Markdown ATX 标题;两边都不收 setext
标题显式 id写在上一行:{#id}⏎# 标题写在行尾:# 标题 {#id}同题异解Carve 06-04 起删除了行尾写法;GEML 06-17 当天定为行尾(Pandoc / kramdown 风格)
标题自动 id保留大小写:id="Intro"规范化算法:小写、NFD、去变音、空白转 -同题异解实测 P1 输出 <section id="Intro">;GEML 规范 §4 六步推导
散文地址无#前-between-后、#容器-before-后仅 GEML两块之间的散文由两端推导出唯一地址,插入新块后旧地址自动失效
段落 / 无序列表- *- *同源继承Markdown
有序列表1. 1) a. i. 及自动编号 .1.同题异解Carve 更丰富
任务列表[ ] [x] 外加 [-] [>] [?] [_][ ] [x]同源继承GitHub(2013);Carve 多出的状态取自 Org TODO
引用块>、::: >,^ 署名无仅 CarveGEML 语法清单里没有引用块
分隔线--- *** ___刻意不设仅 CarveGEML 06-17 选型说明:- 与分隔线、前言、setext、列表冲突,所以围栏改用 =
代码块```lang "标题" [标签]=== code {lang=… src=…}同题异解GEML 的反引号只作「护栏」,不开代码块
原始块```=html,默认放行无仅 Carve实测 P8:<script> 原样输出
容器 / 提示框::: note "标题" … :::=== note … ===同题异解::: 是 Pandoc / MyST / Djot 的围栏;GEML 初稿用过 7 分钟,07:17 改成 ===
围栏闭合与嵌套冒号数严格相等,内层多一个冒号等号数严格相等,外层更长同题异解「等长闭合」是围栏的通行做法(CommonMark 代码围栏)
未知类型::: word → <div class="word">=== word → 警告并保留正文同题异解GEML 初稿 §3 即定为前向兼容的告警
统一的类型块原语无=== type {attrs} + 类型注册表(raw / flow / 键值)仅 GEML代码、数学、表格、视图、数据、图、嵌入、注释、文本、元数据共用一种围栏
行块(诗歌)::: |无仅 CarvereST / Djot
定义列表:: 术语 / : 定义无仅 Carve
续接标记单独一行 +无仅 CarveCarve 自创
数学块$$`…`=== math同题异解
注释行%%,另有 %%% 块、行尾 %%、{% %}仅行首 %%同源继承Obsidian %%…%%、Mermaid %% 注释行(GEML 托管 Mermaid)。GEML 06-18 引入,用来替代被禁止的 <!-- -->
隐藏但参与校验的块无{hidden}仅 GEML只给图表供数的源表:在模型里、受引用校验、不渲染
元数据--- YAML 前言(---toml ---json)=== meta 键值,带类型、可合并、#meta 可寻址同题异解GEML 明确不支持 --- 前言
题注下一行 ^ 文字,Figure # 自动编号caption="…" 属性同题异解Carve 更强:编号、清单、公式题注
复合图::: figure,面板 Figure 2a无仅 Carve
属性对象内容{#id .class key=val}{#id .class key=val}同源继承PHP Markdown Extra、kramdown、Pandoc、MyST 早已通行;GEML 06-17 初稿 EBNF 即有
属性挂载位置块的上一行,可越过不可见块围栏行内 === t {…}、标题行尾同题异解GEML 支持 \ 续行把长属性拆行
裸属性词word → word=""word → 布尔 true;值分字符串/数字/布尔同题异解GEML 初稿待定项:借用 TOML 的类型纪律
缩写定义*[HTML]: …无仅 CarvePHP Markdown Extra
引入 / 嵌入{{ path #id @lines:N-M @shift:N }}=== embed {src=doc.geml#id};代码 src=f#L10-20同题异解Carve 参数更多;GEML 的越界行范围是错误(实测 P4)
放置指令::: toc ::: footnotes 等无仅 Carve
标签页 / 引文::: tab、[@key](可选)无仅 Carve
表格
表格外形裸管道表,|= 标表头=== table 内放 GFM 管道表同题异解|= 来自 Creole;管道表来自 PHP Markdown Extra / GFM
对齐粘连 < ~ >,垂直 ^ ~ v,继承 ?GFM 分隔行 :--:同题异解Carve 更强
合并单元格^ 向上、< 向左无仅 Carve
续行 / 表头行数+ 行、{header-rows=N}无仅 Carve
数据形态正文无format=csv|tsv header= delim=仅 GEML06-17 初稿「表格双形态」
外部数据源无src=data.csv 或 http(s)仅 GEML
派生视图无=== view {src=#t compute=… summary=…}仅 GEML计算列、汇总行、printf 显示格式(实测 P6)
数据块无=== data {format=json|jsonl schema=…}仅 GEML记录数组可直接喂给图表
图形
托管外部图形 DSL```mermaid 等,可选功能(06-21)=== diagram {format=mermaid}(06-17 初稿)同题异解GitLab、GitHub、Obsidian 早有 mermaid 代码块;GEML 初稿原则「只托管不发明 DSL」
图表```chart Chart.js JSON,数字写死在正文format=geml-chart data=#表 x= y=,按列校验同题异解只有 GEML 的图表绑定文档里的数据(实测 P6)
代码图无format=geml-code-graph仅 GEMLGEP-0002 / 0003(07-01)
寻址
带标签的闭合围栏无;只能数冒号=== #id仅 GEML06-29 加入:闭合行点名要关的块,不必数等号
块内坐标无#fy[2] #fy[2]["Q1"] #fy["Q1"] #fy[summary]仅 GEMLGEP-0011(09-03):引用表格的一行、一格、一列,跨文档同样可用,并且受校验
无 id 内容的地址AST 路径(JSON Pointer)、会话级节点 id内容地址 @<hex>(SHA-256 前 8 位)、散文派生地址同题异解GEML 的 @<hex> 08-04 加入,按内容取块;Carve 的路径随结构变动
按类型 / 属性筛选块carve_select_ast_nodes(MCP,09-09)'=== code {lang=py}' {.warn} --within(09-30)同题异解GEML 提交说明注明参照 mq 的查询语言
字符层与文件
输入规范化去一个 BOM、换行统一、U+0000→U+FFFD同上(§0,07-25)同源继承CommonMark §2「Insecure characters」;GEML 加入的直接原因是一个 BOM 让 # 标题 变成段落的 bug
扩展名 / 媒体类型.crv,剪贴板 text/x-carve.geml,text/vnd.geml(IANA 申请中)同题异解
三 · 同输入实测

同一份意图,两个官方解析器各跑一次

Carve 用 npm 上的 @markup-carve/carve@0.1.9(carve lint 与渲染);GEML 用 geml 1.12.3(geml check)。输出原样摘录,未改一字。

P1 · 链接指向不存在的块

两种链接都指向一个不存在的 #budget。

Carve

See [the budget](#budget) and </#budget>.
$ carve lint p1.crv
p1.crv:3:31 broken-crossref — Cross-reference </#budget>
has no matching heading id; it renders as the literal
text "</#budget>".
只报交叉引用;普通链接 […](#budget) 没被发现,渲染退出码 0

GEML

See [the budget](#budget) and [[#budget]].
$ geml check p1.geml
error: unresolved reference `#budget` (line 3)
error: unresolved reference `#budget` (line 3)
2 error(s)
两处都报,退出码 1

P2 · 重复 id

两个标题用了同一个 id。这一项两边都做到了。

Carve

p2.crv:5:1 duplicate-heading-id — Duplicate heading id "a"
lint 报出;但渲染仍输出两个 id="a"

GEML

error: duplicate id `#a` (first defined at line 1) (line 3)
报错,退出码 1

P3 · 跨文档引用,目标不存在

另一份文档存在,但其中没有 #nope。

Carve

See [x](other.crv#nope).
$ carve lint p3.crv      → 无输出,exit 0
$ carve p3.crv
<p>See <a href="other.crv#nope">x</a>.</p>
未发现

GEML

See [x](other.geml#nope).
error: unresolved reference `other.geml#nope` (line 3)
1 error(s)
报错

P4 · 引用代码文件的行范围,文件已经变短

code.txt 只有 10 行,文档引用第 50–60 行。

Carve

{{ code.txt @lines:50-60 }}
$ carve lint p4.crv      → 无输出,exit 0
$ carve p4.crv           → stderr 警告,exit 0
p4.crv:3:1 include-lines-out-of-range - Include line
range for "code.txt" starts past end of file.
<p>{{ code.txt <span class="mention"><strong>@lines
</strong></span>:50-60 }}</p>
仅警告;输出退化为字面文本,@lines 被当作 @提及

GEML

=== code {src=code.txt#L50-60}
===
error: code source `code.txt#L50-60`: the file has
10 line(s), so lines 50-60 no longer exist — the range
is stale (line 3)
报错并说明范围已过期

P5 · 元数据插值,有一个键拼错

product 写对了,prodct 拼错了。

Carve

---
product: Acme
---
Welcome to {{product}}, also {{prodct}}.
<p>Welcome to {{product}}, also {{prodct}}.</p>
没有这项功能(设计上否决),两处都原样输出

GEML

=== meta
product = "Acme"
===
Welcome to {{product}}, also {{prodct}}.
error: unknown metadata reference `{{prodct}}` (line 5)
拼错的键被拦下

P6 · 图表与表格数据

表格有 Q1、Q2 两列;一张图要画上半年合计,另一张图误写了不存在的 Q3。

Carve

|= Region |= Q1 |= Q2 |
| North | 10 | 12 |
| South |  7 |  9 |
^ Sales by region

``` chart
{"type":"bar","data":{"labels":["North","South"],
 "datasets":[{"label":"Q3","data":[99,99]}]}}
```
$ carve lint p6.crv      → 无输出,exit 0
图表数字手写在 JSON 里,与表格无关;写错列、写错数都无从发现

GEML

=== table {#sales}
| Region | Q1 | Q2 |
…
===
=== view {#sales-h1 src=#sales compute="H1 = Q1 + Q2"
  summary="Region = 'Total'; …; H1 = sum(H1)"}
===
=== diagram {format=geml-chart data=#sales-h1 x=Region y=H1}
===
=== diagram {format=geml-chart data=#sales x=Region y=Q3}
===
error: chart: column `Q3` not found in table (line 16)
#sales-h1 计算结果:
Region | Q1 | Q2 | H1
North  | 10 | 12 | 22
South  |  7 |  9 | 16
Total  | 17 | 21 | 38
图表绑定表格,列名校验;计算列与汇总行由视图推导

P7 · 同一段 Markdown 文本

把一行普通 Markdown 交给两边。这是 Carve 最有辨识度的部分。

Carve

Plain *emphasis*, **strong** and _under_ with ~~strike~~.
<p>Plain <strong>emphasis</strong>, **strong** and
<u>under</u> with ~~strike~~.</p>
*x* 成了粗体,_x_ 成了下划线,** 与 ~~ 不解析

GEML

Plain *emphasis*, **strong** and _under_ with ~~strike~~.
<p>Plain <em>emphasis</em>, <strong>strong</strong> and
_under_ with <del>strike</del>.</p>
与 Markdown 一致

P8 · 原始 HTML

行内的 HTML 两边都转义为文本;不同在于专门的透传通道。

Carve

```=html
<script>alert(1)</script>
```
<script>alert(1)</script>
默认透传(不可信输入需 --safe)

GEML

(语言里没有任何 HTML 透传构造)
规范 §9.1:文档是数据,从不是代码

P9 · 改一个单元格,记下版本,再单独撤回这一块

GEML 的核心用法。Carve 侧列出它最接近的三样工具。

Carve

$ carve diff before.crv after.crv
changed  text at line 5 (value: "10" -> "15")
1 structural change
能比较两个现成文件(08-01)、三方合并(08-11)
CLI 没有按 id 读或写一块的命令。
carve-mcp 的 carve_get_block / carve_replace_source
(按块读取与按字节替换)提交于 2026-10-05。
没有任何存储的文档历史。
无版本链、无单块回滚

GEML

$ geml get doc.geml '#sales'          → 只取这一块
$ geml history save doc.geml
saved 20261006T020617Z-d1701a38
$ … | geml set doc.geml '#sales' --body --in - -o doc.geml
wrote doc.geml — 1 B changed, 399 B of 400 B untouched
$ geml history save doc.geml
$ geml doc.geml --to json              → 视图自动重算
North | 15 | 12 | 27
$ geml revert doc.geml '#sales'
reverted #sales to 20261006T020617Z-d1701a38
$ diff before.geml doc.geml            → 逐字节相同
寻址读写自 07-01,版本旁车自 06-17
四 · 能力逐项

工具链与能力,谁有、谁先、谁强

日期为首次出现:仓库的服务器端创建时间,或首个包含该功能的提交。

能力CarveGEML判定
引用的构建期校验lint 查 </#id> 与重复 id;未解析的引用按设计留给宿主;不查跨文档规范硬性要求:链接、自动引用、跨文档、脚注、图表 data=、代码 src 行范围、{{key}}、schema= 全部校验(06-17 初稿 §8)GEML 更强
按 id 读取单块引入片段 #id(标题节 09-14 成为规范,任意块 10-05);carve_get_block(10-05)geml get #id(07-01);标题即整节;行号选择 L27-58;散文派生地址;直接读 .mdGEML 先、更广
按 id 写入单块字节偏移的源码补丁(09-07);carve_replace_source(10-05)set / add / delete / rename / revert(07-01 起);写入结果无法解析则拒绝;rename 同步改全部引用;内容地址 @<hex>(08-04),#id 加 @<hex> 作写入守卫(09-30)GEML 先、更广
文档版本历史没有存储的历史;carve diff(08-01)、三方 carve merge(08-11)、可逆 AST 补丁.gemlhistory(06-17):只追加版本链、SHA-256 校验、verify 重算整链、单块回滚;无三方合并各有所长
文档内数据层无数据形态表格、外部 CSV、view 计算列与汇总、data JSON/JSONL、图表按列绑定仅 GEML
「profile」一词内容限制预设:full / article / comment / minimal,用于不可信输入(06-16)应用层词汇表:form、media、style、translator、codemap、history,带各自的规范与一致性测试同名异物
翻译投影无(只有界面标签翻译)中文规范是 geml-translator/v1 投影,17 个 embed 指向英文块,随原文更新仅 GEML
代码依赖图无codemap:把调用图写成 GEML 文档,边是可校验引用(实测过 Flink 95,958 节点)仅 GEML
Agent 技能carve-skill(07-15)仓库内技能(06-18);geml skill installGEML 先
MCP 服务器carve-mcp(09-05):解析、lint、迁移、AST 补丁;按块读写 10-05geml mcp(07-25):11 个读写工具,写入前校验;代码图另加 4 个工具GEML 先
参考实现TypeScript、PHP(fork 自 djot-php)、Rust 三套逐字节一致(05-13);WASM(06-01);Go/Python/Ruby 绑定TypeScript(06-17);Rust + WASM 第二实现(10-03)Carve 更强
一致性测试集2,223 对 .crv/.html(首批 05-13)语言中立套件(06-28);06-17 初稿即写「仿 CommonMark / toml-test」通行做法
编辑器VS Code、tree-sitter、LSP、Zed(06-01),IntelliJ、Vim、Emacs、Helix、Sublime、Obsidian(08-21)等VS Code、Obsidian(06-30),Chrome 查看器,Logseq 桥Carve 更广
格式桥导入 Markdown/HTML/Djot/BBCode;输出 HTML/Markdown/纯文本/ANSI/AST;损失报告;pandoc-carveMarkdown → GEML;输出 HTML/Markdown/JSON/规范 GEMLCarve 更广
框架集成WordPress、Laravel、Symfony、Hugo、Jekyll、Astro、MkDocs、Docusaurus 等 60 余仓库网站 playgroundCarve 更广
安全模型URL 黑名单、嵌套上限、放大预算、引入沙箱;=html 默认开§9:无 HTML 通道、资源上限、跨文档单层且受限、URL 协议白名单取向不同
格式化 / lintcarve fmt(06-26)、约 45 条 lint 规则--to geml 规范重排(06-29)、附录 A 稳定诊断码通行做法
五 · 设计演进

GEML 自己的几次重构

GEML 的结构不是一次定型的。下面三次重构都由它自己的问题推动:第二个实现能否与参考实现对齐,以及一个不认识新语法的处理器会不会显示错误的内容。

Profile:从写进核心,到应用层词汇表

  1. .gemlhistory 作为「配套规范」与核心规范并列它的三种块类型在哪里都没有声明,7 个旁车文件累计报出 333 条去不掉的「未知块类型」警告。
  2. 代码图写进核心渲染器geml-code-graph 和规范自己的 geml-chart、mermaid 挂在同一条 if 链上;codemap、media、style 的命令各占一个 else-if;参考实现里还有一张规范没写的注册表在压掉诊断。
  3. GEP-0008 先把 form 当作核心块类型提出
  4. §8.6 profile 机制(GEP-0009)文档用 meta 里的 profile 声明词汇表;词汇表只许可名字,不得改变文档模型;禁止从扩展名推断;history 成为第一个 profile。一致性套件用一个谁都不认识的词汇表名来验证:不实现 profile 机制的第二个实现也能复现全部用例。
  5. form、translator、media 进入 profileform 改为 geml-form/v1 的 form-* 家族;09-07 translator,09-16 media。
  6. profile 治理命名、状态、检查、一致性测试。
  7. 核心不再按名字认识任何词汇表渲染、命令、meta 键三处改为查表,支持运行时注册。render.ts 的 2,894 行里有 1,683 行属于代码图,搬出后降到 1,125 行,没有图的页面小了 46%。

Carve 的 profile 从 Init 起就是另一回事:沿用 djot-php 的内容限制预设(full / article / comment / minimal),用于过滤不可信输入。

View:从 table 的属性,到独立的块

  1. 初稿把 compute= 写在 table 上「表格双形态 + 可移植计算」。
  2. 加上 summary 汇总行与 printf 显示格式
  3. GEP-0012 草案仍把 where / order / limit / select 放在 table 上
  4. 改为独立的 view 块(parser 1.10.0)理由经过逐项衡量:不认识 where= 或 select= 的处理器会显示作者想排除的行和列,这不是降级而是错误的渲染;没有正文的 view 被忽略时什么都不显示。compute=、summary= 和指向块的 src= 全部移到 view,table 只放事实,再带这些属性就报 unknown-attribute。规范自身的诊断从 6 个错误降到 4 个。

Output:从专用块,到通用嵌入

  1. === output 保存代码块的运行结果处理器从不执行,只保存,可用 of=#id 绑定代码块。
  2. 撤回 output,改为 === embed {src=doc.geml#id}任意块都能被嵌入,src= 与 data= 并成一条规则。

Carve 的表格没有计算能力,图表数据写在 JSON 里;它的引入处理器 {{ path #id }} 从 Init 起就是现在的写法,09-14 成为规范。

Profile做什么首次提交提交数当前规范与代码行数
geml-history/v1块级版本旁车:修订、关键帧、内容块06-17402,680
geml-codemap/v1把代码调用图写成可校验的 GEML 文档07-011479,705
geml-style/v1样式表按选择器作用于文档,从不改动内容08-30247,382
geml-form/v1块内可寻址的表单字段08-30141,592
geml-translator/v1译文作为原文的投影,随原文更新09-01172,035
geml-media/v1音视频素材、剪辑与生成谱系09-15366,685
profile 机制本身§8.6、注册与治理08-30271,062

按各 profile 的规范、设计文档与源码路径统计,去重后共 229 次提交,约占全部 940 次非合并提交的四分之一。核心解析器里为 profile 所做的改动没有计入,所以这些数字是下限。

六 · 各自所长

独创与更强之处

只列有实测或规范条文支撑的项。两边的长处并排看,就是两种不同的设计目标。

GEML独创或更强

  1. 一种围栏承载全部结构化内容=== type {attrs} 加类型注册表,正文按 raw / flow / 键值三种模式处理;06-17 初稿第一条设计目标。
  2. 引用是契约,不是建议八类引用全部在构建时校验,断了就是错误。实测 P1、P3、P4、P5、P6 中 Carve 均放行。
  3. 每一块都有地址,并且可写显式 id、标题推导、两块之间散文的推导地址;get / set / add / delete / rename / revert;写入前验证可解析。Carve 同类工具晚三个月,只有读取与替换。
  4. 块级版本历史.gemlhistory 哈希链,可校验、可单块回滚(实测 P9)。Carve 没有任何存储的历史。
  5. 文档里的数据关系层表格的数据形态与外部源、view 推导、计算列和汇总行、data 块、按列校验的图表。Carve 的图表只能内嵌写死的 JSON。
  6. 元数据是带类型、可校验的值=== meta 与 {{key}}:这正是 Carve 明确否决的方向。
  7. 应用层词汇表与投影form、media、style、translator、codemap 在核心之上扩展;中文规范本身就是英文规范的翻译投影。
  8. 向下兼容 Markdowngeml list / find / get / set 直接作用于 Markdown 文件,写入仍是 Markdown 语法;需要时一条命令转成 GEML。

Carve独创或更强

  1. 丰富的内联写法斜体、粗体、下划线、高亮、上下标、CriticMarkup、智能排版、提及与标签、语义 span。
  2. 表格排版合并单元格、两轴对齐、续行、表头与表尾行数。
  3. 题注与自动编号图、表、代码清单、公式的 ^ 题注,Figure # 编号,复合图面板。
  4. 三套逐字节一致的实现JS、PHP、Rust,外加 Go、Python、Ruby 绑定,2,223 对测试样例。
  5. 生态覆盖面十余种编辑器、数十种框架与静态站点集成、PDF 与 LaTeX 输出。
  6. 格式桥与损失报告从 HTML、Markdown、Djot、BBCode 导入;输出时报告丢失了什么。
  7. 不可信输入的内容限制profile 预设、链接策略、安全模式。
  8. 三方合并carve merge 合并两条独立的 AST 修改。
七 · 时间线(辅助)

关键功能各自何时出现

Carve 先公开(05-13),GEML 首个提交在 06-17。两条线上的功能各按自己的路线展开。

八 · 来源与复核

每条结论都能自己核对

可复核的来源

  • GEML 首个公开提交 geml-spec/geml@ace97761,2026-06-17 07:10 UTC,说明写明「调研 HTML/Markdown/TOML/AsciiDoc/Org/rST/MyST 后提炼」。仓库服务器端创建于 06-17 12:35 UTC。
  • GEML 更早的草稿 xiongjy2104/must-skills 分支 geml-spec-draft:df2fc31(UTM 初稿,序言列出所融合的格式)、619d33b(07:17,::: 改 === 并附选型说明)、93296fd(07:34,改名 GEML)。
  • Carve markup-carve/carve,HEAD bb09baea;首提交 341c1f98「Init.」;按块读写 markup-carve/carve-mcp@35ac412、@a7bca51(均为 2026-10-05)。
  • 服务器端时间:npm 记录 @geml/geml 1.0.0 发布于 2026-06-29 11:01 UTC,包内已有 .gemlhistory、geml-chart、compute、{{key}}、=== meta、hidden、[[#id]] 与跨文档断链报错。@markup-carve/carve 首次发布于 07-14。
  • 双向互查:Carve 全部分支历史与文件中没有 geml / General Expressive / gemlhistory。GEML 当前文件树中没有 carve;历史上它只出现在 09-30 的一份同类项目调研里(c78b28d0,10-01 移至网站),调研称 Carve 是功能上最接近的项目。两边公开的 issue 与 discussion 互无提及。
  • 实测版本:@markup-carve/carve@0.1.9(npm),geml 1.12.3。探针文件与命令即本页所示。

口径说明

  • Carve 公开早于 GEML:GitHub 仓库创建于 05-13,GEML 首个提交在 06-17。
  • Carve 03-30 的 Init 日期来自提交里的本地时间戳,GitHub 服务器端的创建日期是 05-13。GEML 的提交时间同理,服务器端佐证是仓库创建时间和 npm 发布时间。
  • Carve 的 npm 版本(0.1.9)可能落后于其规范仓库;10-05 才写进规范的「引入片段可选任意块」在该版本中还不生效。
  • 两边都有按块读写:GEML 自 07-01 起以作者写的 #id 和内容地址为键;Carve 自 10-05 起在 MCP 中以选择器、字节范围和全文 sha256 为键。目标相近,键的设计不同。

两个项目何时知道对方

  • Carve 全部分支的历史与文件中没有 GEML 的任何记录。
  • GEML 第一次记录 Carve 是 09-30 的同类项目调研,同一份调研还比较了 Djot、VersaDok、Pandoc、MyST、Markdoc、mq 等。此时类型块、引用校验、按块读写、版本历史、数据层、MCP 都已上线一到三个月。
  • 调研列出 Carve 有而 GEML 没有的两点:行内元素带 id、写入前核对预期哈希。行内 id 至今没有加入 GEML。
  • 同日晚些时候,GEML 让 #id 与 08-04 起就有的内容地址 @<hex> 组合成写入守卫(4ddb3eed),提交说明注明参照 mq 的查询语言;这类带预期值的写入与 HTTP If-Match 同理。