GEML 块类型图解 · form 提案(GEP-0008,草案)与 geml-form/v1 profile · 评审图解第 5 版

GEP-0008 表单图解

这一版按你提的 form-* 家族重画:form-field、form-options、form-note、form-group,四个二级类型都只能出现在 form 内。它一次解决了 table 双逻辑、说明无绑定、围栏过深三个问题。字段用属性指向 form-options 和 form-note,方向只有一个。名字定为 form-note,form-group 不嵌套,看板 15 项全部已定。控件仍是 0008 要求的禁用预览。

看板

15 项决策,现在各在什么状态

已定 你拍板的,或提案原文已经写死的。15 项全部收口。

已定 15 · 全部收口,含 form-group 不嵌套
项结论 / 建议状态来源
1form-* 家族二级类型统一带 form- 前缀:form-field form-group form-options form-note,都只能出现在 form 内,id 全部在 form 作用域内,一条规则。form 因此自包含:geml get '#vendor' 拿到的就是完整表单。§8.5 补一句:扩展类型名不得以「已注册类型名 + 连字符」开头。已定你决定
2重复组form-group 天然可重复,就是 Ant Design 的 Form.List,不写 multiple;required 表示至少一条。开新作用域,地址 #vendor#contacts#email。非重复的复合分组不定义:视觉分组用标题。不嵌套:form-group 只放在 form 直接子层,里面只放 form-field。三个试验表单里嵌套重复组零实例;不嵌套则作用域固定两层、地址最多三段、围栏最多五杠,工具不用处理递归。已定你决定
3选项载体是 form-options,表格体,format / delim / src 同 table,只放在 form 直接子层。契约不变:第一列是值,label 列显示,其余列给 handler。options= 只能指向同一 form 里的 form-options,指向 table 是 error。跨 form 复用靠 src= 指向同一个文件。已定你决定
4type 集合text · textarea · number · date · boolean · select · file,共 7 个纯值形状。group 移出,成为 form-group。已定你确认;随第 2 项少一个
5未知 type 值降级为 text,加 warning unknown-field-type。已定你确认
6约束属性pattern min max step maxlength accept 进 geml-form/v1 profile,只声明不求值,handler 执行。已定你决定
7富文本label= description= placeholder= 三个属性同一条规则:明文,或以 # 开头指向同 form 内的 form-note。form-note 没有 for=,是和 form-options 一样的被动资源,可被多个字段共用。判别有先例:src= 的值以 # 开头就是引用。placeholder 指向 form-note 时渲染器压成纯文本。名字定为 form-note。已定你决定
8description=单行纯文本属性,控件下面那行灰字。长说明不再写成不绑定的散文,改由 form-note 承载。已定你决定
9form 体内标题允许。普通文档标题,id 文档级,不是字段。已定你决定
10placeholder=进 form-field 属性。已定你决定
11textarea format=不要。已定你决定
12value= 遇 multiple唯一预选项。已定你决定
13form-* 出现在 form 外error form-child-outside-form,四个子类型一视同仁。form-field 正文非空是 warning form-field-has-body。已定你决定,随家族推广
14条件逻辑 / 跨字段校验文档不承载,只在 handler。已定提案原文
15Steps / Tabs / 按钮 / 反馈应用层,归 0009 profile 和 geml-style。已定提案原文
分工

谁描述、谁渲染、谁校验

「校验放在哪」的答案。没有变。

描述 文档 .geml

form 和它的 form-* 子块。写的是:有哪些字段、什么类型、必填否、选项是什么,以及格式和范围的声明,这些键来自 geml-form/v1 profile。

不做:不求值、不校验、不提交。§9.1,§8.3(5)。

渲染 样式 / profile

决定长什么样:单页还是 Steps、分组要不要折叠、下拉还是单选组、要不要把 min/max 显示成提示。

不做:不判断对错。渲染出来的是禁用预览,它拿不到输入。

校验 · 提交 handler

宿主按名字注册(handler=onboarding)。唯一拿到用户输入的地方:执行声明的格式约束,再加文档不承载的业务规则。

不做:不决定字段长什么样,不改文档。

「约束」是格式和范围这类可以声明的东西,写在文档里。「校验」是动作,只发生在 handler。样式里没有校验,只有长相。

评审点 1

form-* 家族:一个 form,四种子块

你的方案。下面是一个完整的 form,把四种子块和体内标题都用上;右边是它渲染出来的样子。

一个完整的 form 已定围栏只到三层:form 五杠、form-group 四杠、其余三杠。form-field 正文永远为空。所有 form-* 的 id 都在 form 作用域内。
GEML
=== meta
profile = "geml-form/v1"
===

===== form {#vendor handler=onboarding}
## 公司 {#company}

=== form-field {#legal-name label="公司名称" type=text required
                description="按营业执照填写。"}
===

=== form-options {#entity-types format=csv delim=;}
value ; label
llc   ; 有限责任公司
jsc   ; 股份有限公司
sole  ; 个体工商户
===
=== form-field {#entity-type label="企业类型" type=select required
                options=#entity-types}
===

## 合规 {#compliance}

=== form-note {#coc-note}
请先阅读[《供应商行为准则》](https://example.invalid/coc)。勾选即表示
接受其中的**反商业贿赂**条款。
===
=== form-field {#agree label="我已阅读并同意《供应商行为准则》" type=boolean required
                description=#coc-note}
===

## 联系人 {#contacts-heading}

==== form-group {#contacts label="联系人" required description="至少一位。"}
=== form-field {#name label="姓名" type=text required}
===
=== form-field {#email label="邮箱" type=text pattern="^[^@]+@[^@]+$" required}
===
====
=====

%% 地址:#vendor#legal-name、#vendor#contacts#email、#vendor#entity-types、#vendor#coc-note
%% 所有 form-* 的 id 都在 form 作用域内且唯一;标题 #company 不是 form-*,是文档级
%% form-field 上以 # 开头的属性值都指向同 form 内的 form-* 块:options=、label=、description=、placeholder=
渲染禁用预览
公司
公司名称
 
按营业执照填写。
企业类型
有限责任公司 ▼
合规
我已阅读并同意《供应商行为准则》

请先阅读《供应商行为准则》。勾选即表示接受其中的反商业贿赂条款。

联系人
联系人 至少一位。
姓名
王芳
邮箱
wang.fang@example.invalid
姓名
李强
邮箱
li.qiang@example.invalid
+ 添加联系人
类型放在哪体id作用
form文档任意处流式:form-* 子块、标题、散文文档级容器,handler=
form-fieldform 或 form-group 内空,非空 warning作用域内,#vendor#email一个字段,文字全在属性
form-groupform 直接子层,不嵌套只放 form-field作用域内,并开新作用域可重复的一组字段;required 表示至少一条
form-optionsform 直接子层表格体,format delim src 同 table作用域内,#vendor#entity-types选项列表,只被同 form 的 options= 消费,不单独渲染
form-noteform 直接子层流式作用域内,#vendor#coc-note被字段的 label= / description= / placeholder= 指向的富文本,可被多个字段共用
table 不再两种逻辑form-options 是自己的类型;options= 指向 table 是 error。文档里的 table 还是 table,chart 也不会绑到选项表上。之前:同一个 table 类型,在 form 外是数据表,在 field 内是选项表。
富文本说明绑定到字段字段用 description=#coc-note 指向 form-note,受检引用,渲染器能挂 aria-describedby。方向和 options= 一致。之前:只能写成字段上方不绑定的散文,geml get 取不到,读屏器读不到。
围栏少一层选项表不再嵌在字段里,最深 form › group › field 三层。之前:form › group › field › table 四层,form 要开到六杠。
类型名自解释,正文一条规则geml list 一眼看出哪些块属于表单;form-field 正文永远为空。之前:field / table / text 与核心同名类型混在一起。
§8.5
你读得对:它只要求扩展名带连字符,没禁止规范类型带连字符。要补的是反方向一句:扩展类型名不得以「已注册类型名 + 连字符」开头,因为规范现在拥有 form- 前缀,form-acme 这种名字要留给规范。现有 profile 的 style-rule 不受影响,style 不是注册类型。
自包含
id 全在作用域内之后,一个 form 块就是渲染器和 handler 需要的全部,geml get '#vendor' 一次拿齐。跨 form 复用选项表改为 src= 指向同一个文件。
一个方向
所有绑定都是字段指向辅助块:options=#id 指 form-options,label= description= placeholder= 以 # 开头时指 form-note。这些 # 引用在所在 form 内解析,是 GEML 里唯一的相对引用,只出现在 form-field 的属性上。判别规则有先例:src= 的值以 # 开头就是引用,否则是路径。
名字
form-note,和核心 note 块平行,不会被当成 text。
评审点 2

多值字段和重复组

两个长得像、其实不同的东西。form-group 取代了上一版的 type=group,并且天然可重复。

一个字段,多个值 已定提案里的 multiple 落在标量字段上。Ant Design 是 Select mode="tags"。
GEML
=== form-field {#tags label="标签" type=text multiple
                description="可填多个,逐个回车。"}
===
渲染禁用预览
标签
长三角 ✕ ISO 9001 ✕ 代工 ✕ 输入后回车
可填多个,逐个回车。
一组字段,整体重复 N 次 已定Ant Design 叫 Form.List。分组是容器不是值形状,所以是 form-group 而不是 type=group;它天然可重复,不写 multiple。type= 回到 7 个纯值形状。
GEML
==== form-group {#contacts label="联系人" required
                description="至少一位,可添加多位。"}
=== form-field {#name label="姓名" type=text required}
===
=== form-field {#email label="邮箱" type=text pattern="^[^@]+@[^@]+$" required}
===
=== form-field {#role label="角色" type=select options=#roles}
===
====

=== form-options {#roles format=csv delim=;}
value ; label
biz   ; 商务
tech  ; 技术
fin   ; 财务
===

%% 地址:#vendor#contacts#email、#vendor#roles
%% form-options 只放 form 直接子层,options=#roles 相对所在 form 解析,组内任何字段都能用
渲染禁用预览
联系人 至少一位,可添加多位
姓名
王芳
邮箱
wang.fang@example.invalid
角色
商务 ▼
姓名
李强
邮箱
li.qiang@example.invalid
角色
技术 ▼
+ 添加联系人
规则
form-group 就是重复组:零到多条,每条是它子字段值的一组;required 表示至少一条。只放在 form 直接子层,里面只放 form-field,开一个新作用域。multiple 只用在标量字段上表示多值,不是 form-group 的属性。非重复的复合分组不定义:纯视觉分组用体内标题。
不嵌套
form-group 不能再放 form-group,放了是 error。三个试验表单里零实例;不嵌套则 id 作用域固定为 form、group 两层,地址最多三段,围栏最多五杠,解析器和所有下游工具都不用处理递归作用域。Drawbacks 第 1 条「非扁平 id 空间」的代价因此有界。等第一个嵌套实例出现再开。
代价
解析器的 id 作用域从「一层」改成「递归」,实现后最难改。围栏因为选项表外移反而少了一层。
评审点 3

选项:form-options

契约不变,载体换成专门的类型。type=select 的值必须落在选项表的 value 列里。

同一份文档,样式表可以渲染成下拉,也可以渲染成单选组 已定
GEML
=== form-options {#states format=csv delim=;}
value    ; label
draft    ; 草稿
accepted ; 已接受
final    ; 定稿
===
=== form-field {#state label="状态" type=select required options=#states}
===

%% 提交的是 accepted 这个 value,不是「已接受」这个显示文本
%% options= 指向一个 table 而不是 form-options:error
渲染 · 两种皮肤禁用预览
状态
已接受 ▼
下拉框
状态
草稿
已接受
定稿
单选组,由样式表选
加 multiple 后是多选,选项文字可以带链接 已定form-options 的体和 table 一样按单元格行内解析,所以 label 列可以有链接。next.js 那 44 个带空格的选项也放得下。
GEML
=== form-options {#categories-list format=csv delim=;}
value        ; label
raw          ; [原材料](https://example.invalid/cat/raw):金属、聚合物、纺织
parts        ; [零部件](https://example.invalid/cat/parts):机加工、注塑、PCB
contract-mfg ; 代工制造
logistics    ; 物流与仓储
===
=== form-field {#categories label="供应类别" type=select multiple required
                options=#categories-list}
===
渲染禁用预览
供应类别
原材料 ✕ 代工制造 ✕ ▼
原材料:金属、聚合物、纺织
零部件:机加工、注塑、PCB
代工制造
物流与仓储
契约
第一列是提交的值,label 列是显示文本,其余列原样交给 handler。src= 可以从 CSV 文件引入长列表,两个 form 要同一份选项就指向同一个文件。form-options 只被同 form 的 options= 消费,不单独渲染;没人指向的给一条 warning。
评审点 4

type 与约束

type 回到 7 个纯值形状。约束属性来自 profile,只声明不求值。

7 个 type,每个对应什么值、什么控件、测到几次 已定实例数来自两个入库试验表单的 23 个字段加 next.js 表单的 9 个元素。GitHub 的表单 schema 只有 4 种字段类型。
type值的形状渲染成实例备注
text任意字符串,单行输入框11path、url、email、tel 全部并入。格式差异用 pattern=
textarea字符串,多段多行输入框7
number数数字输入1范围用 min / max / step
date日期日期选择1time、datetime 有实例再加
boolean真 / 假勾选框或开关1
selectform-options 的 value 列中的一个;multiple 时多个下拉 / 单选组 / 多选组4含 next.js 的 44 选项下拉
file文件上传0唯一凭值形状留下的
三个字段,三种归属 已定 · 进 profile约束键来自 geml-form/v1 profile,文档头部声明后才不报 unknown-attribute;处理器只存字符串,执行在 handler。
GEML
=== meta
profile = "geml-form/v1"
===

=== form-field {#revenue label="年营收(百万元)" type=number
                min=0 max=99999 step=1 description="整数;未审计可留空。"}
===
=== form-field {#phone label="手机" type=text pattern="^1\d{10}$" required
                description="11 位手机号"}
===
=== form-field {#contract-end label="合同结束" type=date required
                description="须晚于开始日期。"}
===
%% 「晚于开始日期」是跨字段规则:文档只能写给人看,只有 handler 检查
渲染禁用预览
年营收(百万元)
12
整数;未审计可留空。 声明在文档 handler 执行
手机
138 0000 0000
11 位手机号 声明在文档 handler 执行
合同开始
2026-10-01 ▦
合同结束
2027-09-30 ▦
须晚于开始日期。 只在 handler
约束能否写进文档谁执行
pattern · min · max · step · maxlength · accept能,profile 属性声明。文档只存字符串,不求值。handler 样式可选择显示成提示
type=number · date · select · file能。类型本身就是一条形状约束。handler
结束晚于开始 · A 与 B 互斥 · 勾了 X 才必填 Y不能。§9.1 无表达式、无条件。写在 description 里给人看即可。只在 handler
未知 type= 值 已定降级为 text,加 warning unknown-field-type,值照样保留。
GEML
=== form-field {#brand-color label="品牌色" type=color}
===
渲染禁用预览
品牌色
#1F6F8B
warning · unknown-field-type 按 text 渲染。
评审点 5

label · description · form-note:三种文字各归哪里

短的在属性,长的在 form-note,form-field 正文永远为空。

常规字段:两个属性,正文为空 已定「按营业执照填写。」是控件下面那行灰字。位置由样式表定。
GEML
=== form-field {#legal-name label="公司名称" description="按营业执照填写。"
                type=text required}
===

%% 正文非空:warning form-field-has-body,不渲染
渲染禁用预览
公司名称
 
按营业执照填写。
带链接、多段的说明:属性指向 form-note 已定你定的方向。form-note 放在 form 内、field 外,没有 for=;字段的 description= 以 # 开头指向它,渲染器把它挂成 aria-describedby。next.js 那种 5 段 15 个链接的说明也放得下了。
GEML · form 体内
=== form-note {#coc-note}
请先阅读[《供应商行为准则》](https://example.invalid/coc)。

勾选即表示接受其中的**反商业贿赂**与**数据保护**条款。
===
=== form-field {#agree label="我已阅读并同意《供应商行为准则》" type=boolean required
                description=#coc-note}
===

%% #coc-note 在所在 form 内解析,即 #vendor#coc-note;同一个 form-note 可被多个字段指向
%% 指向的不是 form-note:error;没人指向的 form-note:warning
渲染禁用预览
我已阅读并同意《供应商行为准则》

请先阅读《供应商行为准则》。

勾选即表示接受其中的反商业贿赂与数据保护条款。

规则
label= description= placeholder= 三个属性同一条:值是明文,或以 # 开头指向同 form 内的 form-note。明文是一行纯文本;form-note 是流式内容,带链接、粗体、多段。label 指向 form-note 时按行内渲染;placeholder 指向 form-note 时渲染器压成纯文本,因为输入框的占位符放不下标记。
名字
定为 form-note。
小口子

三个已定的边角

value= 在多选字段上是唯一预选项 已定属性值没有数组。要预选多个,交给 handler 的默认值逻辑。
GEML
=== form-field {#plan label="结算方式" type=select multiple value=net30 options=#plans}
===
渲染禁用预览
结算方式
net30 ✕▼
只能预选一项。
form 体内出现标题:允许 已定标题 id 进文档全局空间,字段 id 进 form 作用域。探针证实现在就是这样解析的。
geml list 的实际输出
#vendor            form            L7-30
#contacts-heading  heading  h2     L8-29   ← 文档级 id,不是 #vendor#contacts-heading
#contact-name      …                     ← 提案里应为 #vendor#contact-name
规则

form 体内的标题是普通文档标题:id 在文档全局空间,不受字段作用域影响,不是字段。它的节止于所在 form 或 form-group 结束,或下一个同级、更高级标题。纯视觉分组用它,不用 form-group。

围栏写短了 已定 · error任何 form-* 落在 form 外都是 error form-child-outside-form,消息里提示围栏可能写短了。
GEML · 错误写法form 提前关闭
=== form {#vendor handler=onboarding}
公司信息
=== form-field {#legal-name label="公司名称" type=text required}   ← 和 form 同长,把 form 关了
===                                         ← 这行开了一个新的匿名块
GEML · 正确写法
==== form {#vendor handler=onboarding}
公司信息
=== form-field {#legal-name label="公司名称" type=text required}
===
====
错误写法的结果
error · form-child-outside-form
form-field {#legal-name …} 出现在 form 外面。上一层 form 的围栏可能写短了。
geml check 退出码非零,文档不通过。
这一族的诊断一览
form-child-outside-formerrorform-* 落在 form 外
form-field-has-bodywarningform-field 正文非空,不渲染
unknown-field-typewarning7 个之外的 type,按 text
options-not-form-optionserroroptions= 指向的不是同 form 内的 form-options
note-not-form-noteerrorlabel / description / placeholder 的 # 引用指向的不是同 form 内的 form-note
unused-form-blockwarningform-options 或 form-note 没有任何字段指向它
duplicate-iderror同一作用域内字段 id 重复
依据

三组探针

GEML spec 1.0。带连字符的类型名能被当成块类型解析,profile 里的 style-rule 就是现成的例子。

#输入结果说明了什么
1按提案原文写的 form,末尾引用 [[#vendor#legal-name]]warning unknown block type form
error unresolved reference
form 未注册只是 warning;字段引用是 error,#a#b 解析是实现的第一步。
2同一文档,form / field 换成已注册的 note0 error 29 个字段、4 张选项表全部可寻址嵌套在现有围栏规则下成立,缺的只是类型名和作用域规则。
3note 四层嵌套加体内标题加空体ok no diagnosticsgroup 嵌套、体内标题、空体机制上都已能解析。这一族最深只用到三层。