# GEML — General Expressive Markup Language(通用表达型标记语言) *[English](GEML-spec.md) | 中文* ## 规范(稳定版) | 字段 | 取值 | |------|------| | 工作名 | GEML(General Expressive Markup Language,通用表达型标记语言) | | 版本 | 1.0 | | 状态 | 稳定(stable) | | 文件后缀 | `.geml` | --- ## 摘要 GEML 是一种用于结构化、富表达力文档的纯文本标记语言。`.geml` 文件无需渲染即可作为 纯文本完整阅读;代码、图形、表格、公式、提示框等各类结构化内容统一通过单一的类型块 原语表达;支持稳定标识符并在构建时校验引用;托管外部图形 DSL 而自身绝不内建图形 语言。本规范定义文档模型,块、属性、内联内容与引用的语法,以及合规处理器必须满足的 要求。 ## 目录 0. [预备](#0-预备) 1. [约束](#1-约束) 2. [文档模型](#2-文档模型) 3. [类型块原语](#3-类型块原语) 4. [属性与标识符](#4-属性与标识符) 5. [内联内容与链接](#5-内联内容与链接) 6. [表格](#6-表格) 7. [图形](#7-图形) 8. [一致性](#8-一致性) 9. [安全与资源限制](#9-安全与资源限制) [附录 A:诊断目录](#附录-a诊断目录) · [附录 B:语法清单](#附录-b语法清单非规范性) ## 约定 本文档中的关键词 **MUST(必须)**、**MUST NOT(必须不)**、**MAY(可)**、 **SHOULD(应)** 用于表示要求等级:MUST 与 MUST NOT 表示绝对的要求或禁止,SHOULD 表示推荐,MAY 表示可选且被许可的行为。文中「§*n*」指代编号为该数字的章节。 标注为*非规范性*的文字仅作解释,不构成任何要求。除非出现在一致性测试集(§8.4)中, 示例均为非规范性。 **[英文版](GEML-spec.md)是本规范的规范性文本**,本中文版为资料性译文:二者不一致时, 以英文版为准。 --- ## 0. 预备 本节定义 GEML 处理器的字符级输入。§1–§9 的每条规则都建立在 §0.5 所定义的 **归一化字符流**之上。 ### 0.1 字符编码 `.geml` 文件必须(MUST)使用 **UTF-8** 编码。处理器必须不(MUST NOT)尝试探测或接受 任何其他编码。 *理由(非规范性):* 与仅供渲染的格式不同,GEML 承载构建期身份——块 id、跨文档引用, 以及 `.gemlhistory` 边车中的 SHA-256 内容哈希。它们都定义在字节之上,因此一份文档若 经另一种编码往返,它就是另一份文档,其版本历史也不再可验证。 处理器必须(MUST)以 UTF-8 替换语义解码:非良构字节序列解码为 U+FFFD REPLACEMENT CHARACTER;必须不(MUST NOT)回退到以其他编码重新解释输入。 **字符**指一个 Unicode 码点。在直觉意义上不构成字符的码点(如组合记号)在本文档中同样 计为字符。 ### 0.2 字节序标记(BOM) 若解码后的输入以 U+FEFF 开头,该单个字符必须(MUST)在解析前移除。只移除开头的一个 U+FEFF;第二个 U+FEFF,或出现在文档其他位置的 U+FEFF,均为普通内容。 ### 0.3 行与行结束符 **行结束符**指换行符(U+000A)、其后不跟换行符的回车符(U+000D),或回车符加换行符。 **行**指零个或多个非 U+000A、非 U+000D 的字符,其后跟一个行结束符或输入结束。 **空行**指不含任何字符,或仅含空格(U+0020)与制表符(U+0009)的行。 每个行结束符必须(MUST)在解析前归一为单个 U+000A。处理器必须不(MUST NOT)让行结束符 的选择改变文档模型:同一文档以 CRLF 与以 LF 书写必须产生完全相同的模型。 *注(非规范性):* `.gemlhistory` 边车单独记录文件的主导行结束符,因此恢复某个修订版本 能重现原始字节。归一化管辖的是*解析*,不是存储。 ### 0.4 不安全字符 U+0000 必须(MUST)替换为 U+FFFD。 *理由(非规范性):* 对任何按 C 字符串处理的下游消费者,NUL 会截断文档。文档必须不能 (MUST NOT)让流水线中的某个工具看到比解析器更少的内容。 ### 0.5 归一化输入 处理器必须(MUST)在解析前恰好按以下顺序执行: 1. 按 UTF-8 解码,非良构序列变为 U+FFFD(§0.1); 2. 移除开头的一个 U+FEFF(§0.2); 3. 将每个行结束符替换为 U+000A(§0.3); 4. 将 U+0000 替换为 U+FFFD(§0.4)。 结果即**归一化字符流**。每一步都只在*行内*改写字符——没有任何一步会拆分或合并行—— 因此归一化字符流的行数与输入相同。据此处理器可(MAY)按行号索引原始字节,这正是块级 编辑(`geml get`/`set`)能对文件未改动部分保持字节保真的原因。 ### 0.6 媒体类型、后缀与片段标识符 | | | |---|---| | 文件后缀 | `.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,指的都是同一个块。 --- ## 1. 约束 本节给出统领后续规范的设计约束。 1. `.geml` 文件必须无需渲染即可作为纯文本完整阅读。 2. 代码、图形、表格、公式、提示框必须共用唯一的类型块原语(§3);不为每种内容 单设语法。 3. 每个可寻址的块——标题或类型块(§2)——可携带稳定 `id`;引用必须在构建时解析并 校验(§5)。 4. 图形必须内嵌外部 DSL;格式只定义托管协议,绝不内建图形语言(§7)。 5. 不存在原始 HTML 逃逸口;语义不绑定任何后端。 6. 标题只用 ATX `#`。setext 标题与 `---`/`===` 分隔线、frontmatter 规则不属于 GEML。 --- ## 2. 文档模型 一篇文档是**块**的序列,块只有两种形态: - **无栅栏区块(unfenced block)**——段落、标题、列表;正文按内联 GEML 解析。 - **类型块**——带围栏;正文如何处理由块*类型*决定(raw / flow / data——§3)。 标题与类型块可携带**属性对象** `{#id .class key=val}`(§4)。段落与列表不可携带: 段落末尾的 `{…}` 是字面文本,需要 id 的散文放进 `text` 块(§3)。内联内容只存在于无栅栏 区块中。 ### 2.1 段落 **段落**是一行或多行非空文本的序列。段落会被出现在行首的以下任何构造打断(结束): - 空行。 - 标题行。 - 列表项标记行。 - 类型块栅栏(`===`)。 - `%%` 注释行。 任何不匹配这些打断构造的行都是一条 `text-line`,并继续该段落。 ### 2.2 列表 **列表**是连续一行或多行**列表项**。一个列表项包含前导缩进、**标记**、一个空格、以及该项的内联内容(§5): - **无序**标记是 `-` 或 `*`; - **有序**标记是一个或多个数字后跟 `.`;首个条目的编号即列表的 `start`。 条目内容是单独一行。条目可以以**任务标记**开头——`[ ]`、`[x]` 或 `[X]` 后跟一个空格—— 该标记被剥离并记录为勾选/未勾选状态。 **嵌套由缩进决定。** 缩进按列计(制表符记为 4 列)。比当前条目标记缩进*更深*的条目,在该 条目下开启一个嵌套列表;缩进*更浅*的条目则收回到外层列表。两个同级条目之间的**空行**使 列表变为**松散(loose)**(否则为**紧凑(tight)**);空行本身不会结束列表。列表在第一个 "既非空行、也不是缩进不浅于本列表的条目行"处结束。`%%` 注释行(§4)也不是条目行, 同样会结束列表,之后在块级别被识别为注释。 多段落的列表条目不属于 GEML;丰富的条目内容应放进类型块(§3)。 --- ## 3. 类型块原语 类型块的形态如下: ``` === <类型> <属性>? <正文> === ``` - 围栏是连续的 `=`(≥3 个)。一个块由其后**最先出现**的下列两种行之一闭合:与开围栏 **等长**的 `=` 串,或——当块带有 `#id` 时——它的**带标签围栏** `=== #id`(长度 ≥3 的 `=` 串后跟该块的 id)。带 `#id` **不会**停用等长裸围栏的闭合:哪种闭合先出现, 块就在哪里结束。 - 嵌套的安全只来自围栏长度纪律:块体中任何一行都不得是与开围栏恰好等长的裸 `=` 串。 **推荐**的做法是**更长的外围栏**(`====` 包住 `===`),长于块体内所有围栏样式的行。 带标签的闭合不豁免这条规则——块体内一条与开围栏等长的裸 `=` 串会就地闭合该块: 后面的 `=== #id` 尚未到达,块体已被静默截断。 - 带标签的闭合消除的是*数长度*这一风险——闭合行点名它要闭合的块,而不是靠长度匹配—— **推荐**在长块中使用(数错 `=` 是长块的常见失误)。但它**不能**防止块体内出现与开围栏 等长的裸 `=` 串——那些串仍然会提前关闭块,带标签关闭对此无效。当块体可能含有围栏样式 的行时,它不能替代更长的开围栏。 - **类型注册表**声明每种类型的正文模式:`raw`(原样,如带 `lang=` 的 `code`、带 `format=` 的 `diagram`/`table`/`data`、`math`、带 `src=` 的 `embed`)、`flow`(解析,如 `note`、 `text`)或键值对(每行一个 `key=val`,如 `meta`;该模式在文档模型中序列化为 `"data"`——这个名字早于 `data` 块类型,为模型稳定性而保留)。 - 未知类型产生构建告警,其正文按 raw 保留。 - `text` 块是**可寻址的散文容器**:其 flow 正文存在的唯一目的,是给一段散文一个 `#id` 与属性,使其可被引用、可被按块编辑(`geml get`/`set`)、可被版本化。渲染 为中性块——不带标注样式(标注属于 `note`)。只包裹确实需要寻址的散文;普通段落 仍是默认写法。 - `embed` 块代表存放在别处的内容:`src=` 指明一个文档,可带片段 (`src=other.geml#budget`),该块就地渲染为那份内容。片段指向标题时选中标题的整节(即标题本身及其后所有的块,直到遇到下一个同级或更高级别的标题,或者到达文档末尾)。 `src=` 像任何引用一样受校验(§5),`embed` 块的 body 不使用。 `src=` 指向的那份文档,必须(MUST)**当作一份完整文档来解析**,目标再从解析结果中选出; 它绝不是一段拼进当前文档的文本。因此那份文档里的元数据、引用、相对路径的基准、以及外部 数据(`src=`),全部相对**它自己**解析,而不是相对写下 `embed` 的这份文档: ``` a.geml b.geml ───────────────────────────── ────────────────────────────── === embed {src=b.geml#tbl} === table {#tbl src=rows.csv} === === ``` `rows.csv` 相对 `b.geml` 所在目录解析(`b.geml` 自己被单独打开时也是这么解析的),而不是 相对 `a.geml`。写下 `embed` 的文档只决定结果显示在哪里,不改变目标那一侧的任何解析。 两种形式——`embed` 块与行内投射(§5.3)——都适用;链条的每一层也都适用:链上每份文档都是 它所指向的下一份的基准。 ### 3.1 文法 块结构是上下文无关的,如下所示。内联**强调**不是上下文无关构造,由 §5.3 的定界符游程算法 解析,而非本文法。 ```ebnf (* 本文法陈述在**逻辑行**之上。在它生效之前,以 `\` 结尾的围栏行或标题行会与其后的 行折叠——反斜杠与换行符合并为一个空格(§4 续行)——因此下文的 NL 指折叠后逻辑行 的结束,属性对象可以占据不止一条物理行。 *) 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 | comment-line ; heading = "#" , { "#" } , SP , text , [ SP , attrs ] , NL ; (* 1 to 6 #s *) paragraph = text-line , { text-line } ; text-line = LINE ; (* non-empty line not matching an interruption rule *) comment-line = indent , "%%" , [ SP , text ] , NL ; (* §4:保留在模型中,永不渲染 *) 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 | flag-attr ; id-attr = "#" , NAME ; class-attr = "." , NAME ; kv-attr = NAME , "=" , value ; flag-attr = NAME ; (* boolean true flag *) value = bare-word | quoted-string ; quoted-string = '"' , { quoted-char } , '"' ; quoted-char = escape-seq | ( CHAR - '"' - "\" ) ; escape-seq = "\" , ( '"' | "\" ) ; (* only " and \ can be escaped *) bare-word = NAME | number ; number = [ "-" ] , integer , [ frac ] ; integer = "0" | ( NONZERO , { DIGIT } ) ; frac = "." , DIGIT , { DIGIT } ; NONZERO = "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ; NAME = NAME-CHAR , { NAME-CHAR } ; NAME-CHAR = LETTER | DIGIT | "-" | "_" ; (* LETTER:任意 Unicode 字母 *) ``` NAME 不限于 ASCII,也不要求以字母开头:标题按自身文本派生出的 id(§4)可以以数字或 `-` 开头,非拉丁文字同样是普通的 NAME 字符。 ### 3.2 `data` 块 `data` 块承载**值树**——标量、序列与映射,恰为 JSON 的值域——作为*受校验的数据*; 而 `code` 承载的是处理器绝不解释的文本。body 在扫描期保持 `raw`(围栏界定逐字 文本),随后由**格式引擎**把它解析为块的**值**;引擎拒绝的 body 是构建**错误**, 并指名出错行。 - `format=` 在这一个模型**内部**选择表面语法。进入注册表要求语法**自描述**——仅凭 字节即可确定值,无方言参数。分隔文本不满足(分隔符、有无表头、引号规则都是参数, 且这些参数只有对着列模型才有意义),这正是 `csv`/`tsv` 属于 `table` 格式(§6)而 非 `data` 格式的原因。 - `json`——默认——body 必须(MUST)解析为一个 JSON 值。默认值遵循一条注册表通则: 当模型存在同构的规范语法时,即以其为默认(`table` → 管道形式)。JSON 是值树自身 的序列化,也是零依赖处理器始终能校验的那一种语法。 - `jsonl`——body 的每个非空行必须(MUST)解析为一个 JSON 值;空行忽略;块的值为各 行值的序列。这是记录流形式:由于文档是平铺的块序列,在文件末尾追加一个完整的 `data` 块即是任何文档的合法延续——jsonl 的盲追加工效,外加 id 与校验。 - `src=` 以外部文件命名块的内容,沿用表格的单一来源纪律(§6):`src=` 与行内 body 二选一——同时给出是**错误**。文件必须像数据(`.json`/`.jsonl`;显式 `format=` 仍优先于扩展名)。`http(s)` 源由**渲染器**获取,解析器绝不获取(§9.4)——该块及 其上的图表一并延迟;其他 URL scheme 一律拒绝。这正是日志的安排:记录仍是一个任何 现有工具都能追加、能 tail 的普通 `.jsonl` 文件,GEML 文档是它受校验、可寻址、可出 图的视图。 - `yaml` 与 `toml` 是**保留**格式名(那两种语法的值树读法)。没有相应引擎的处理器 必须(MUST)保留原文 body 并发出**警告**,且不得(MUST NOT)猜测——降级方式与未 知 `diagram` 格式(§7)完全相同。未知的 `format=` 值同样降级。 - `schema=` 为**保留**属性:指名存放 schema 的块(`#id`)或 GEML 文档(`doc.geml`, 可带 `#id`),并像任何引用一样接受引用检查(§5);其他形状是错误。按 schema 校验 值不在本版规范定义之内。 - 成功解析的 body 以块的 `value` 暴露在文档模型中。规范序列化把 `json` 重排为两空 格缩进、`jsonl` 重排为每行一个紧凑值;无引擎的 body 与任何 raw body 一样逐字节 保留。 - 值为**记录数组**的 `data` 块可作图表数据源——见 §7.1。 ### 3.3 源路由 `code` 块可(MAY)用 `src=` 指明它所展示的代码,而不在文档里存一份拷贝;`data` 块可(MAY)用同样的方式指明它的值(§3.2)。两者共用一套**路由**语法: ``` [#L[-]] ``` 路径指明一个文件;可选片段把它收窄到一个行范围,**1 起、闭区间**(`#L14-24`, 单行写 `#L14`)。不带片段时路由指整个文件。范围必须不(MUST NOT)为空或起于第 1 行之前。 - **解析基准。** 路由按文档相对解析。当命名了解析根(`--root`)时,文档相对不存在 的路由可(MAY)再按该根解析——生成式代码图正是这样从嵌在源码之下的文档里写路由的。 两种解析都仍受 §9.4 的约束边界限制。 - **远端。** `http(s)` 路由由**渲染器**获取,而非解析器(§9.4);该块及读取它的一切 一并延迟。其他 URL scheme 必须(MUST)拒绝。 - **无法解析的路由是告警,不是错误。** 无论那个文件此刻能否取到,`code` 块都仍然 指明「某位置的一段代码」,因此描述了缺失源码的文档——例如单独发布的生成式代码图, 或描述另一份 checkout 的图——依然有效,只是未经核对。`data` 路由相反:它的值正是 文档所承诺的东西,取不到就是错误(§3.2)。而被禁止的 URL scheme 两者都属于书写 错误,必须(MUST)拒绝。 - **过期范围是错误。** 若文件已不含所指的行,处理器必须(MUST)报 `bad-source-range`。这正是校验路由的意义:漂移的引用让构建失败,而不是渲染出一片 空白。 - **`src=` 旁的块体是错误。** `code` 块不得(MUST NOT)同时携带 `src=` 和内联块体;两者只能选其一(`code-src-and-body` 错误,附录 A)。 - **不设扩展名门。** 代码可以是任何语言,所以指向代码的路由不按后缀限制;安全边界 只由约束(§9.4)承担。`data` 路由保留它的 `.json`/`.jsonl` 门(§3.2),那道门的 用途是指明**格式**,不是界定文件系统范围。 对 `data`,范围只是把文件收窄为若干行,之后仍按该格式正常读取:`jsonl` 日志的窗口 是最自然的用法,而 `json` 切片当且仅当该切片自身就是一个值时才成立。 --- ## 4. 属性与标识符 - `{#budget}` 设定块 id 为 `budget`。文档内 id 必须唯一。 - `{.warning}` 添加语义类(不含样式)。 - `{caption="年成本"}` 等 `key=val` 是各类型自定义的参数。其中两个不属于任何单一类型、 在**每个**类型块上都合法:`caption`——渲染器随块显示的短标签,§5.2 也用它作自动引用的 文字——以及下文的 `hidden` 标志。类型未定义的键报 `unknown-attribute` 告警,绝非错误, 且原样保留。 - 标题的 id 未显式给出时按文本自动生成;显式给出时写在标题行末尾的属性对象里, 如 `## 标题 {#sec}`。该派生规则是**规范性**的:引用(`[[#id]]`、`other.geml#id`、 URL 片段)必须在所有实现中指向同一个块,因此标题产出什么 id 不能由实现自行决定。 从标题文本出发——取 `{{key}}` **插值之前**的原始文本,即字面包含的大括号和变量名,以保证 id 的稳定性,不受变量值改变的影响—— 处理器必须按此顺序: 1. 转为小写; 2. 删除每一处代码跨段(code span),连反引号带内容一并删除——这样 `` `foo()` `` 内部的标点不会渗进 id; 3. 删除既非 Unicode 字母、非数字、非空白、也非 `-`、也非 `_` 的每个字符; 4. 去掉首尾空白; 5. 把每一段连续空白替换为单个 `-`。 于是 `## Use \`foo()\` in 2024 设计` 派生出 `#use-in-2024-设计`。派生 id 与其他 id 一样会冲突:两个标题派生出**相同** id 是 `duplicate-id` **错误**(附录 A)——该 id 指向第一个,后面那个必须显式声明 `{#id}`。派生 id 保留下划线:`foo_bar` 派生 `#foo_bar`,`foobar` 派生 `#foobar`——两者不同。标题文本若既无字母也无数字,派生结果为空 id;空 id 同样是 一个派生 id,因此第二个这样的标题会与它冲突——请给其中之一显式写上 `{#id}`。 - 风格建议(非规范性):文档标题放在 `=== meta`(`title = "…"`)而不是顶级标题 ——这样每个标题都对应文档中一个真正的小节。 - 属性值类型:带引号 `"…"` 恒为字符串;`true`/`false` 为布尔;匹配整数/浮点 语法的裸词为数字;其余裸词为字符串。不支持数组、日期与嵌套表。 - 不带 `=` 的裸属性词是布尔标志,置为 `true`(如 `hidden`)。 - `=== meta` 块以每行一个 `key=val` 承载文档元数据,按上述属性值类型规则。若文档中存在多个 `=== meta` 块,它们的键将被合并;**先定义的键优先**——后来定义相同键时,触发 `duplicate-meta-key` **告警**,后定义被忽略。流式 正文中的 `{{key}}` 会被替换为对应的 meta 值;遇到未知键是构建 **error**。插值读取 流式正文的源文本,并遵循 §5.3 阶段一(1)的原样 atom:代码片段或行内公式里的 `{{key}}` 原样保留(因此 GEML 文档可以引用这一语法本身),原样(raw)块体从不 插值,反斜杠转义的 `\{{key}}` 渲染为字面文本 `{{key}}`。 插值是**对流式正文的单趟替换**,这两点都是规范性的。单趟:被代入的值绝不会被再次扫描, 因此值里若本身写着 `{{other}}`,产出的就是这六个字符本身——没有嵌套要解,也就没有环要 检测(`a = "{{b}}"` 与 `b = "{{a}}"` 会终止,各自产出字面文本)。流式正文:属性值里的 `{{key}}` **不**插值——`caption="{{title}}"` 就是那串字面字符串——这也正是标题派生 id (见上)稳定的原因。 - `hidden` 标志把一个块标记为属于文档、且**完全参与引用校验**,但 **不渲染**——例如只为图表供数的源表。`%%` 行是隐藏的、原样的、永不渲染的作者备注。两者的分工是:对于参与文档模型(作为数据源、可复用片段等)但应保持不可见的结构化内容,使用 `hidden`;对于不参与文档模型的废弃注释,使用 `%%`。注意,`%%` 仅在块位置(顶层,或 `flow` 块的正文内)被识别为注释。在 `raw` 块的正文内部,`%%` 行被精确原样保留,不视为注释。 - 属性顺序不影响语义;推荐顺序为 `#id`、`.class`、`key=val`。 - **续行(Line continuation):** 以反斜杠 `\` 结尾的块围栏(`===`)或标题(`#`)行, 会将其属性对象延续到下一行。反斜杠和换行符会被视为一个空格,从而允许将长属性对象 (例如表格的 schema)拆分换行以提高可读性。只要续上来的行同样以 `\` 结尾,折叠就继续, 直到第一条不以 `\` 结尾的行为止;折叠结果就是 §3.1 文法所解析的那条逻辑行。只有围栏行 与标题行会折叠:散文行尾的 `\` 是硬换行(§5.1),块正文内行尾的 `\` 是正文文本。 --- ## 5. 内联内容与链接 ### 5.1 内联元素 内联元素只出现在无栅栏区块内部。 | 语法 | 含义 | |------|------| | `*强调*` | 强调(emphasis) | | `**加重**` | 加重(strong) | | `` `代码` `` | 代码片段(原样;内部不解析) | | `~~删除~~` | 删除线 | | `$…$` | 内联数学(正文原样) | | `![alt](src){…}` | 就地媒体嵌入(图片/音频/视频) | | `![[#id]]` | 就地内容嵌入(内联投射) | | 行尾 `\` | 强制换行 | | `\` + ASCII 标点 | 转义:该标点取字面值 | - 强调/加重定界符必须贴住非空白字符,且不得跨块边界。 - **块级逃逸**:因为块级语法(如 `===` 围栏、`#` 标题、`-` 列表)必须在行首匹配,所以在行首添加反斜杠(如 `\===` 或 `\#`)会破坏块匹配,使其降级为普通正文。随后,内联解析器会将 `\`+标点 转义为字面字符,从而完美实现块语法的逃逸渲染。 - 块级数学使用 `=== math` 类型块(§3)。 - 嵌入 `![…]` 就地渲染/播放其源(绝不跳转),链接 `[…]` 则跳转。`as ∈ {image, audio, video}`,省略时按源扩展名推断。 - 列表项**可**以**任务标记**开头——`[ ]`(未完成)或 `[x]`/`[X]`(已完成)后跟一个 空格。该标记从项文本中剥离并记为勾选状态;其余文本按内联解析。 ### 5.2 链接与引用 内部与跨文档引用均在构建时校验。 | 形式 | 含义 | |------|------| | `[文字](https://…)` | 外部链接 | | `[文字](#budget)` | 指向块 `budget` 的内部引用,文字自定义 | | `[[#budget]]` | 自动引用:链接文字取自目标的 caption/标题(若皆无则后退使用字面串 `#id`) | | `![[#budget]]` | 内联投射:来自块 `budget` 的内容 | | `[[other.geml#budget]]` | 同上,但跨文档:指该文档中的那个块 | | `![[other.geml#budget]]` | 内联投射,跨文档 | | `[文字](other.geml#budget)` | 跨文档引用 | | `[^note]` | 脚注:把 id 为 `note` 的块渲染为脚注 | - 外链选项放进属性对象:`[文字](url){rel=nofollow target=_blank}`。 - 无法解析的 `#id`、`other.geml#id` 或 `[^id]` 是构建**错误**。 - **只有当目标是 `.geml` 文档时,`#` 之后的片段才被读作块 id。** 在 `page.html#sec` 或 `notes.md#sec` 里,片段属于那个格式——元素 id、站点生成的 标题 slug——GEML 构建既不解析它也不报告它。目标文档本身仍必须存在;只有 `#` 之后的部分交还给定义它的格式。 - 脚注引用会指向任意带有匹配 `#id` 的块(通常是一个 `note` 块),渲染器可据此将其表现为文档脚注。 - 内联投射 `![[#id]]` 必须(MUST)解析到一个正文恰好含有**一个非空段落**的 `text` 块—— 投射把该段落的内联内容插入投射位置。目标若为标题、非 `text` 类型块、或含多个段落的 `text` 块,则触发 `inline-transclusion-not-inline` 错误(附录 A)。块级内容请改用 `=== embed {src=#id}`。 - *注(非规范):* 反向链接与图谱是对已解析引用的倒排索引,由工具层提供;GEML 不为此新增语法。 ### 5.3 识别顺序与强调 无栅栏区块的内联解析分两个阶段进行,并为每个输入指派恰好一个解析。 **阶段一——atom(从左到右,按此优先级):** 1. 反斜杠转义(`\` + ASCII 标点 → 该字面字符;行尾 `\` → 硬换行)、代码片段、内联数学; 其内容不再进一步解析。 2. 元数据插值(`{{key}}`);替换为标量值。 3. 图片(`![alt](src)`)、链接、自动引用(`[[#id]]`)、内联投射(`![[#id]]`)与脚注引用(`[^id]`);链接或引用不得嵌套在另一个 链接或引用内部。 atom 之间的文本是字面文本。**被转义**的定界字符是一个字面 atom,因此不参与强调。 **阶段二——强调**在阶段一产出的**整个内联序列**上运行:字面文本段与 atom,按原顺序。 一对定界符**可以**包住 atom——`*见 [规范](s.geml)*` 是一段包含链接的强调——但每个 atom 都是**不透明**的:atom 内部的字符绝不是定界符(代码片段里或链接目标里的 `*` 保持原义),atom 内部也不再进一步解析。强调不跨越块边界。强调、加重、删除线由 **定界符游程 flanking** 解析: - 一个**定界符游程**是字面文本中 `*` 的最大连续串,或两个及以上 `~` 的最大连续串 (单个 `~` 是字面)。 - 取游程紧邻的前后源字符(整个内联序列的开头与结尾视为空白):若游程后面不是空白,且"要么后面不是 标点、要么前面是空白或标点",则它**左侧贴合(left-flanking)**;**右侧贴合**是镜像。 左侧贴合的游程**可开**,右侧贴合的游程**可闭**。 - 在 atom 边界上,"游程前/后的字符"是该 atom 的**边缘源字符**——它所消费的源文本 跨度的第一个或最后一个字符。`[链接](x.geml)` 之前的 `*` 看到 `[`,其后的看到 `)` (都是标点);硬换行之后的游程看到被换行消费的 `\n`(空白),因此不能在那里闭合。 被转义的定界符是一个 atom(§5.3(1)),其边缘字符是反斜杠与被转义的字符——绝不 并入相邻游程的长度。 - 此处的**标点**指任何属于 Unicode 标点类(`P*`)或符号类(`S*`)的字符,不限于 ASCII。 `“`、`)`、`,` 与 `"`、`)`、`,` 同样是标点,因此 `“*(foo)*”` 与 `"*(foo)*"` 的强调 判定完全一致。(§5.1 的 `\` 转义则相反:只作用于 ASCII 标点,因为非 ASCII 字符本就 不是 GEML 语法。) - 一次从左到右的扫描完成配对:每个可闭游程匹配最近的、同字符的、在它之前的可开游程。当一个 游程既可开又可闭时,若两个游程长度之和是 3 的倍数,则该配对被拒绝,除非两者长度都是 3 的 倍数(**rule of three**)。 - 匹配的 `*` 对是**强调**(每侧消耗一个)或**加重**(两者都 ≥ 2 时每侧消耗两个);匹配的 `~~` 对是**删除线**(每侧消耗两个)。任何未配对的定界符都是字面文本。 *这是 CommonMark 的**定界符游程**算法——flanking、三的规则、以及跨内联 atom 的配对——在 GEML 定界符上的限定版:只有 `*` 和 `~~`,没有 `_` 强调,也没有引用式链接。`*一段带 [链接](x.geml) 的文字*` 就是包住链接的强调,与 CommonMark 完全一致([GEP-0007](proposals/0007-emphasis-across-atoms.md))。* --- ## 6. 表格 块类型 `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 === ``` *`{…}` 属性对象用 §4 的 `\` 续行拆开——反斜杠与换行符合并为一个空格。这些反斜杠是必需的: 没有它们,开围栏的 `{…}` 就永远不闭合,整个块——连同围栏——都会变成一个段落。* 该例解析为: | 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%** | - **分隔符** —— 数据形态的正文按其格式的天然分隔符切分:`format=csv` 用 `,`, `format=tsv` 用制表符。`delim=` 把它改成任意单个字符,于是欧洲式 `;` 分隔的 CSV 或 `|` 分隔的导出文件无需先转换就能直接读。该值**必须**恰好是一个字符;否则是 `bad-table-delimiter` 错误,并改用天然分隔符,让表格其余部分照样读得出来。属性值不带 转义语法(§4),所以制表符分隔写作 `format=tsv`,绝不是 `delim="\t"`。数据正文只按分隔符 切分、别无他物:`delim="|"` 下,`| a | b |` 两端的竖线各自是一个单元格——剥掉它们是视觉 形态(a)的规则,不是这里的。`delim` 只细化数据形态,并不选择它:表格没有数据 `format` 时它被忽略,并报出一条 `ignored-table-delimiter` 警告。 - **数据来自别处** —— 表格可以不写内联正文,改为声明数据来自哪里。对于 `table` 块这个 属性是 `src=`(`diagram` 把同一件事写作 `data=`,见附录 B.3),它接受三种目标,按同一 规则解析:数据文件(`format=csv`/`tsv`,相对于文档的路径或 `http(s)` URL);`#id`,指 本文档中的某个 table 块;或 `doc.geml#id`,指另一个文档中的。相对路径与跨文档目标**必须**在构建期 解析并校验存在——解析不到是错误,目标存在但不是表格也是错误。进入 `.gemlhistory` 哈希的 只有 `src`/`data` 这串文本,**绝非**解析出的内容。表格**不得**同时给数据来源和内联正文 (错误)。内联仍是默认。 - **计算列** —— `compute` 列出一条或多条 `列名 = 表达式` 公式,以 `;` 分隔。每条 表达式对每个数据行求值一次,运算符限 `+ - * / ( )` 与一元 `-`(`*`/`/` 优先级高于 `+`/`-`,左结合)。当遇到空值或非数值单元格时,行级公式计算会将其作为 `0` 处理以保证公式完成,并必须(MUST)为每个被代入的单元格报出一条 `compute-non-numeric-cell` 警告:结果照常产出,但读者会被告知它建立在哪个单元格上,而不是拿到一个静默算错的数。而在执行聚合函数时,除 `count` 会统计所有非空单元格的数量外,其余函数(如 `sum`、`avg`)都会跳过非数值单元格(不计入总和或平均值分母)。列按表头名引用(名字含空格用引号,如 `'Unit Price'`),或按电子表格列字母引用(`A`、`B`…)。公式可引用更靠前的计算列 (上例 `YoY` 引用 `FY`);引用必须无环。计算列按公式顺序追加在数据列之后,正文中 不再书写。 - **单元格容纳不了的结果** —— 除以零得到 ±∞,`0 / 0` 得到 NaN,两者都不是表格值:该单元格 必须(MUST)不持有任何值、必须(MUST)显示为 `-`,处理器必须(MUST)报出一条指明该单元格的 `compute-not-a-number` 警告。既然该单元格不持有值,后续公式或聚合读到它时就按其他非数值 单元格处理(行级公式记为 `0`,`sum`/`avg` 跳过)。`summary` 表达式同理。分母为零是数据的 事实、不是文档的缺陷,所以它是警告、文档仍然合规——但绝不静默。 - **汇总行** —— `summary` 定义表尾的单独一行,由 `单元格 = 值` 条目组成、`;` 分隔, 左侧指明目标列。每个 `值` 要么是用作标签的字符串/数字字面量(`Segment = 'Total'`), 要么是把聚合函数 `sum、avg、min、max、count`(各作用于一列)用 `+ - * / ( )` 与 字面量组合而成的表达式(`(sum(FY) - sum(PriorFY)) * 100 / sum(PriorFY)`)。聚合把 一列在数据行上折叠,是唯一跨行的构造;汇总表达式中每个列引用都必须被聚合归约(裸 列名在汇总行没有值)。未指定的列留空。 - **显示格式** —— 计算列或汇总单元格可在左侧列名后附带 `[printf]` 格式:`FY [%.1f]`、 `YoY [%.1f%%]`(`%%` 为字面百分号)。格式仅作用于数值的显示,不改变存储值。不支持 日期/时间格式:单元格值只有字符串、数字、布尔(§4),日期按 ISO-8601 纯文本书写。 这个拆分只定义在公式的左侧:格式是左侧**最后**一个 `[…]` 组,它必须(MUST)位于左侧末尾 (允许尾随空白)、必须(MUST)不含 `]`、且必须(MUST)含有 `%`。它之前的部分去掉首尾空白 即为列名。正是这条 `%` 判据让列名本身可以带方括号:`[Data] = A + B` 中没有任何东西匹配 格式,列名就是 `[Data]`;`[Data] [%.1f] = A + B` 中格式是 `%.1f`,列名仍是 `[Data]`。 - **刻意排除**(让表格是文档特性而非电子表格引擎):单元格与区域寻址(`@3$4`、 `@2$1..@4$3`)、相对行引用(`@-1`)、条件式、跨表 `remote()` 引用、查表/VLOOKUP, 以及任何嵌入程序(无 Lisp、无 JS)。 --- ## 7. 图形 块类型 `diagram` 托管外部图形 DSL。 ``` === diagram {#flow format=mermaid caption="评审流程"} graph LR A[草稿] --> B{评审} B -->|通过| C[发布] B -->|打回| A === ``` - `format` 选择可插拔渲染器(`mermaid`、`graphviz`、`d2`、`plantuml`…)。 - 正文为 `raw`,原样交给该渲染器。 - 处理器必须暴露渲染器注册表,且不得自行解释正文。未知 `format` 产生告警,保留 正文。 - `#flow` 让该图可被引用:`见 [[#flow]]`。 ### 7.1 绑定数据的图表 `diagram` 可用 `data=` 声明数据源,接受的目标形式与 `table` 的 `src=` 相同(§6): 数据文件(`.csv`/`.tsv` 相对于文档的路径或 `http(s)` URL,视为匿名表;或本地 `.json`/`.jsonl` 文件,视为匿名记录源);`#id`,指本文档中的某个块;或 `doc.geml#id`,指另一个文档中的。处理器必须解析该引用,并向渲染器提供一个 **表模型**。`table` 块贡献其模型(含计算列);值为**记录数组**——非空的映射序列—— 的 `data` 块(§3.2)通过将记录键按首见顺序投影为列来贡献表模型。图表引用到的每一列 必须(MUST)在每条记录中存在且为标量值;图表未引用的列可放任何内容。悬空引用、 无法产出表模型的目标、或违反上述规则的记录,都是构建**错误**。处理器仍 **不解释 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 = 构建错误。(若表格数据是外部数据且按 §9.4 由渲染器抓取,列名校验推迟到渲染期)。 - 更复杂的图(标注、参考线、热力图…)改用托管 DSL: `=== diagram {format=vega-lite data=#fy25}`,spec 写进 body。body 为 raw、不校验列名。 --- ## 8. 一致性 本规范定义三个**合规等级**。产品分别声明各自的合规:一个从不渲染的校验器可以是合规 **解析器**而不是合规**渲染器**,这并不使它不合规。 ### 8.1 合规文档 **合规 GEML 文档**指这样一段归一化字符流(§0.5):合规解析器处理它时不产生任何严重级别 为 `error` 的诊断(附录 A)。 告警不使文档变得不合规:它们标记的是处理器无法完全解释、但必须(MUST)保留的构造—— 未知块类型、未知图格式、未经校验的跨文档引用。 尽管如此,任何输入都是*可解析的*:§2、§3、§5.3 与 §6 为任意字符流指派恰好一个文档 模型。不存在任何输入是合规解析器可以拒绝、拒绝建模或失败于其上的——不合规的文档同样 产出模型,只是同时带有描述它的错误。 ### 8.2 合规解析器 合规解析器必须(MUST): 1. 完全按 §0.5 归一化其输入。 2. 解析类型块原语(§3)与属性对象(§4)。 3. 构建文档模型,其中每个块 id 唯一且可解析。 4. 解析内联强调(§5.3)与列表嵌套(§2.2),使每个输入恰好有一个解析。 5. 对任何无法解析的内部或跨文档引用报**错误**(§5)。 6. 把未知块 `type` 和未知图 `format` 当**告警**而非错误,原样保留正文。 7. 以附录 A 指派的**代码与严重级别**报告每一条诊断。 8. 遵守 §9.2 的资源限制,以诊断而非失败的方式降级。 9. 不依赖任何特定编辑器,不依赖原始 HTML。 ### 8.3 合规渲染器 渲染器是可选(OPTIONAL)的:合规解析器不必产出任何呈现格式的输出。若产出,则必须 (MUST): 1. 呈现合规解析器所产出的文档模型,不重新解释 `raw` 块的正文(§3)。 2. 不执行 `code` 块,不解释 `diagram` 正文(§7)——只能将其交给已注册的外部渲染器 (§9.1)。 3. 对文档可控文本满足 §9.5 的落点要求。 4. 输出中略去标记为 `hidden` 的块(§4),同时在模型中保留它们。 ### 8.4 一致性测试集 规范配套一套**一致性测试集**:输入 `.geml` 与期望文档模型的归一化投影成对。对本文档以 算法方式陈述的规则——内联强调(§5.3)、列表嵌套(§2.2)、原子优先级、元数据插值 (§4)——该测试集是规范参照。第二个独立实现复现每个用例即为合规。在参考仓库中它位于 [`geml-parser/test/conformance/`](https://github.com/geml-spec/geml/tree/main/geml-parser/test/conformance)。 ### 8.5 版本 规范的版本独立于任何实现。本文档为 **GEML 1.0**;参考实现的包版本遵循其自身发布节奏, 不是规范版本。 实现以「符合 GEML 1.0」的形式声明合规。处理器遇到不认识的构造时必须(MUST)按 §8.2(6) 降级——这就是本格式的前向兼容机制,也是新增一种块类型或图格式不构成破坏性变更的原因。 **类型注册表**(§3)是开放的。非本规范定义、亦未注册的类型名应(SHOULD)包含连字符 (例如 `acme-invoice`),把不含连字符的名字留给本规范的未来版本。图的 `format` 名遵循 同一约定。 --- ## 9. 安全与资源限制 GEML 文档常常由机器生成,也常常不可信:它可能来自模型、流水线或一个 pull request。本节 规定文档怀有恶意时处理器必须保证什么。它适用于 §8 的每个合规等级。 ### 9.1 文档是数据,绝不是代码 处理器必须不(MUST NOT)执行或求值文档的任何部分: - `code` 块的正文是存储的文本;必须不(MUST NOT)被运行(§3); - `diagram` 正文必须(MUST)原样传给由 `format` 选定的外部渲染器,处理器必须不 (MUST NOT)解释它(§7); - 不存在原始 HTML 逃逸口(§1(5)),除 §6 的封闭算术外没有表达式语言——而后者在构造上 就没有条件分支、没有查找、没有跨表引用、没有内嵌程序。 ### 9.2 资源限制 处理器必须(MUST)为自己在文档上的递归深度设定上界,且分别针对:类型块嵌套(§3)、 列表嵌套(§2.2)、内联嵌套(§5)。到达上界时必须(MUST)产出对应的 `*-nesting-too-deep` 错误(附录 A)并继续处理剩余输入。它必须不(MUST NOT)溢出调用栈、 中止,或无法产出模型。 上界由实现自定;处理器应(SHOULD)各自至少允许 64 层,这已远超任何为阅读而写的文档。 *参考实现允许 256 层块嵌套与列表嵌套、100 层内联嵌套。* 处理器必须不(MUST NOT)在未按目标文法转义的情况下,用文档可控文本构造正则表达式、 shell 命令或任何其他可执行形式。*块 id、类名与属性值都是文档可控的;`.geml` 文件是不可信 输入,与 `.zip` 同理。* ### 9.3 引用、环与终止性 引用解析必须(MUST)在任何输入上终止,包括为使其死循环而精心构造的输入: - **内部引用**不可能成环:id 在文档内唯一(§4),因此解析 `#id` 是一次查找,而非遍历。 - **跨文档引用**在校验时恰好只解析一层。处理器收集目标文档的 id 时*不*解析该文档自身的引用, 因此两份互相引用的文档在校验引用时会终止。 - **内容投射**(`embed` 块或内联投射将其目标就地展开)是递归的。处理器必须(MUST)追踪已展开文档的链条,若发现某文档试图投射一个已经处于该展开链条中的文档的目标,则停止展开并报 `transclusion-cycle` 错误。 - **计算列**(§6)按声明顺序求值,公式只能看到数据列与*更早的*计算列。因此自引用或 前向引用不是环,而是未知列,报为 `compute-error`。GEML 表格不需要环检测器:求值顺序 在构造上就让依赖图无环。 ### 9.4 跨文档解析与外部数据 解析跨文档引用(§5.2)会读取由文档指名的文件。处理器必须(MUST)把该解析限制在显式配置 的根目录内,必须(MUST)在判定目标是否位于根内之前解析每一个符号链接,并且必须(MUST) 拒绝逃逸出根目录的目标。解析必须(MUST)**失败即关闭**:无法确立限制根的处理器不解析 任何东西并报 `unresolvable-document`,而不是回退到不受限的查找。 媒体 `src`(§5.1)以及指向 `http(s)` URL 的表格数据来源(§6)在**渲染时**由渲染器抓取,解析器从不读取它们。 渲染器必须(MUST)把这类来源当作不可信输入。在文档可能来自不可信作者的场合,渲染器应 (SHOULD)把 `src` 限制在文档自身的源或目录内,并应(SHOULD)要求显式选择加入才执行 `http(s)` 抓取:被抓取的 URL 会把读者的地址、以及阅读这一事实与时间,泄露给控制该 URL 的人。 由于外部数据在渲染时抓取,其内容从不进入 `.gemlhistory` 哈希——只有 `src` 文本会进入 (§6)。 ### 9.5 落点要求 链接或嵌入(§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)削减到目标格式的标识符字符集,而不是 仅仅转义它们。 ### 9.6 一致性套件能担保到哪一步 由于 §9.5 要求方案检查必须在**构建模型时**施加,其效果在模型里可见,因此是可被机器 校验的:一致性套件的 `safety.json` 用例既钉住哪些目标地址会消失,也钉住哪些必须存活; §8 的验收测试会把它们跑在第二实现上。 本节其余要求**同样是规范性的,且不在其覆盖范围内**。§9.2 的各项上限与 §9.3 的环检测以 诊断形式报出,机器校验它们就要钉死某一个具体界值;§9.5 的展开预算作用在渲染期;§9.4 的 限界是宿主环境的性质。声称一致的实现仍然必须(MUST)满足它们,并且应当(SHOULD)为每一 条自带测试——套件通过是关于**解析**的证据,不是一张安全证书。一致性套件自己的 README 列出了这些要求实际挡住过的失败。 --- ## 附录 A:诊断目录 合规解析器产出的每一条诊断,除人类可读的消息外都携带一个**代码**。消息是散文:它可 (MAY)在版本之间被改写、翻译或补充上下文。**代码与严重级别才是契约**——它们是一致性 测试、编辑器集成或 CI 关卡所匹配的对象,处理器必须(MUST)以本附录指派的代码与严重级别 报告诊断。 对于本目录已覆盖的情况,处理器必须不(MUST NOT)另造目录之外的代码。处理器可(MAY)为 本规范未定义的情况产出额外诊断;这类代码应(SHOULD)带一个连字符分隔的厂商前缀 (`acme-…`),以免本目录的未来版本与之冲突。 诊断携带的行号从 1 开始,指归一化字符流(§0.5)中的行——按 §0.5,它同时也是原文件中的 行号。 **完整的代码、严重级别与条件对照表见[英文版附录 A](GEML-spec.md#appendix-a-diagnostic-catalogue)**: 该表是规范性的,并由参考实现的测试逐条机械校验,因此此处不作重复以免译文漂移。 --- ## 附录 B:语法清单(非规范性) 本附录是语言全部语法构造的完整索引,按每个构造可占据的**位置**组织。它不定义任何 东西:每一行都引用真正给出定义的章节。任何新增、移除或移动构造的变更,都在同一变更 中更新本清单——清单里缺了条目是文档 bug,绝不是隐藏特性。 GEML 有三个语法位置: - **块位置**——文档层面:一篇文档是块的序列(§2)。 - **内联位置**——无栅栏区块以及 flow 类型块正文的流式内容内部(§2、§5)。 - **属性位置**——块的属性对象 `{…}` 内部(§4),其中若干键承载指向其他块、文档或 外部数据的引用。 ### B.1 块位置 | 构造 | 形态 | 正文 | 定义于 | |------|------|------|--------| | 段落 | 无栅栏 | 内联 | §2、§3.1 | | 标题 `#`…`######` | 无栅栏 | 内联 | §1(6)、§3.1、§4 | | 列表——`-`/`*`、`1.`;任务标记 `[ ]`/`[x]` | 无栅栏 | 条目为内联 | §2.2 | | `=== code` | 类型块 | raw | §3 | | `=== math` | 类型块 | raw | §3 | | `=== table` | 类型块 | raw:管道网格或 `format=` 数据 | §6 | | `=== data` | 类型块 | raw:`format=` 值树(默认 `json`、`jsonl`;`yaml`/`toml` 保留) | §3.2 | | `=== diagram` | 类型块 | raw:外部 DSL | §7 | | `=== embed` | 类型块 | raw(body 不使用);`src=` 指明内容所在 | §3、§6 | | `=== note` | 类型块 | flow | §3 | | `=== text` | 类型块 | flow | §3 | | `=== meta` | 类型块 | 键值对 | §3、§4 | | `%%` 注释行 | 行 | 原样,永不渲染 | §4 | **形态**取值:**无栅栏**(§2)、**类型块**(带围栏,§3)、**行**——块解析阶段识别的 单行构造。 ### B.2 内联位置 | 构造 | 家族 | 定义于 | |------|------|--------| | `*强调*` · `**加重**` · `~~删除~~` | 修饰 | §5.1、§5.3 | | `` `代码` `` | 原样 atom | §5.1 | | `$数学$` | 原样 atom | §5.1 | | `[文字](url)` · `[文字](#id)` · `[文字](doc.geml#id)` | 导航 | §5.2 | | `[[#id]]` · `[[doc.geml#id]]` | 导航,链接文字自动 | §5.2 | | `[^id]` | 脚注引用 | §5.2 | | `![alt](src)` | 投射:媒体 | §5.1 | | `{{key}}` | 投射:元数据标量 | §4 | | 行尾 `\` · `\` + 标点 | 强制换行 · 转义 | §5.1 | ### B.3 属性位置 七个属性键承载引用,且全部参与校验(附录 A): | 键 | 宿主块 | 目标 | 定义于 | |----|--------|------|--------| | `src=` | `table` | 数据来自哪里,三种形态:数据文件(`csv`/`tsv`,文档相对路径或 `http(s)` URL)、`#id`(本文档中的 table 块)、`doc.geml#id`(另一文档中的)。 | §6 | | `data=` | `diagram`(`geml-chart`) | 数据来自哪里,与表格 `src=` 相同的三种形态:数据文件(`csv`/`tsv` 代表它所描述的匿名表格;本地 `.json`/`.jsonl` 代表匿名记录源)、`#id`(本文档中的),或 `doc.geml#id`(另一文档中的)——指向一个 `table`,或值为记录数组的 `data` 块(§3.2)。 | §6、§7.1 | | `schema=` | `data` | 存放 schema 的块(`#id`)或 GEML 文档(`doc.geml[#id]`);仅做引用检查 | §3.2 | | `src=` | `data` | 块的外部内容:`.json`/`.jsonl` 文件,与代码源同一套路由语法(可用行范围收窄,这正是寻址 jsonl 日志窗口的方式);文档相对或 `http(s)`(渲染期获取) | §3.2 | | `src=` | `code` | 该块所展示的代码:一个源文件,可用行范围收窄——`[#L[-]]`,1 起、闭区间。按文档相对解析,也可相对解析根(`--root`);`http(s)` 路由在渲染期获取。文件已不含该范围时为错误。 | §3.3 | | `src=` | `embed` | 该块所代表的内容:一个文档,可带片段 | §3 | | `src=` | `diagram`(`geml-code-graph`) | 一篇 GEML 文档 | §7 | 其余属性机制——`#id`、`.class`、带类型的 `key=val` 值、`hidden` 标志——定义于 §4。 ### B.4 概念 × 位置矩阵 多数概念天然只占一个位置;三个概念同时具有内联与块两种形态,另有两对跨越两个位置的 定义↔使用配对。 | 概念 | 内联位置 | 块位置 | |------|----------|--------| | 散文 | 文本 run | 段落 | | 代码 | `` `代码` `` | `=== code` | | 数学 | `$…$` | `=== math` | | 投射——就地渲染目标 | `![alt](src)`(媒体)、`![[#id]]`(内容) | `=== embed {src=…}` | | 导航——供读者跳转的链接 | `[t](…)`、`[[#id]]`、`[^id]` | — | | 空间性内容 | — | 标题、列表、`table`、`data`、`diagram`、`note`、`text` | | 隐藏内容与注释 | — | `hidden` 标志、`%%` 行 | | 元数据 | `{{key}}`(使用) | `=== meta`(定义) | | 脚注 | `[^id]`(使用) | 带有目标 `#id` 的块 | *注(非规范性):* 按行读,这张矩阵分开了两个引用家族。**导航**渲染为供读者跳转的 链接(`[t](…)`、`[[#id]]`);**投射**则把被引目标本身渲染在原地。GEML 今天在三个 粒度上投射:标量(`{{key}}`,取自 `=== meta`)、媒体对象(`![alt](src)`)、表格的 数据模型(图表的 `data=#id`)。脚注引用是杂交体:一个导航标记,其目标同时被投射到 文档脚部。 `!` 通篇就是投射前缀:`![](src)` 投射媒体,`![[#id]]` 投射内容。两种内容投射的分界 是**值**与**内容**:`{{key}}` 代入元数据的**标量**——无标记、无上下文规则;而 `![[#id]]` 投射目标的**行内内容**,格式保留,并受 §3 全套上下文规则约束——引用点名 的那份文档按独立文档解析,再从解析结果中选出目标。再加上管块与小节的 `=== embed`, 三个粒度分别是标量、短语、块。