这一版按你提的 form-* 家族重画:form-field、form-options、form-note、form-group,四个二级类型都只能出现在 form 内。它一次解决了 table 双逻辑、说明无绑定、围栏过深三个问题。字段用属性指向 form-options 和 form-note,方向只有一个。名字定为 form-note,form-group 不嵌套,看板 15 项全部已定。控件仍是 0008 要求的禁用预览。
已定 你拍板的,或提案原文已经写死的。15 项全部收口。
| 项 | 结论 / 建议 | 状态 | 来源 | |
|---|---|---|---|---|
| 1 | form-* 家族 | 二级类型统一带 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= 指向同一个文件。 | 已定 | 你决定 |
| 4 | type 集合 | 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。 | 已定 | 你决定 |
| 8 | description= | 单行纯文本属性,控件下面那行灰字。长说明不再写成不绑定的散文,改由 form-note 承载。 | 已定 | 你决定 |
| 9 | form 体内标题 | 允许。普通文档标题,id 文档级,不是字段。 | 已定 | 你决定 |
| 10 | placeholder= | 进 form-field 属性。 | 已定 | 你决定 |
| 11 | textarea format= | 不要。 | 已定 | 你决定 |
| 12 | value= 遇 multiple | 唯一预选项。 | 已定 | 你决定 |
| 13 | form-* 出现在 form 外 | error form-child-outside-form,四个子类型一视同仁。form-field 正文非空是 warning form-field-has-body。 | 已定 | 你决定,随家族推广 |
| 14 | 条件逻辑 / 跨字段校验 | 文档不承载,只在 handler。 | 已定 | 提案原文 |
| 15 | Steps / Tabs / 按钮 / 反馈 | 应用层,归 0009 profile 和 geml-style。 | 已定 | 提案原文 |
「校验放在哪」的答案。没有变。
.gemlform 和它的 form-* 子块。写的是:有哪些字段、什么类型、必填否、选项是什么,以及格式和范围的声明,这些键来自 geml-form/v1 profile。
不做:不求值、不校验、不提交。§9.1,§8.3(5)。
决定长什么样:单页还是 Steps、分组要不要折叠、下拉还是单选组、要不要把 min/max 显示成提示。
不做:不判断对错。渲染出来的是禁用预览,它拿不到输入。
宿主按名字注册(handler=onboarding)。唯一拿到用户输入的地方:执行声明的格式约束,再加文档不承载的业务规则。
不做:不决定字段长什么样,不改文档。
「约束」是格式和范围这类可以声明的东西,写在文档里。「校验」是动作,只发生在 handler。样式里没有校验,只有长相。
form-* 家族:一个 form,四种子块你的方案。下面是一个完整的 form,把四种子块和体内标题都用上;右边是它渲染出来的样子。
=== 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=
请先阅读《供应商行为准则》。勾选即表示接受其中的反商业贿赂条款。
| 类型 | 放在哪 | 体 | id | 作用 |
|---|---|---|---|---|
| form | 文档任意处 | 流式:form-* 子块、标题、散文 | 文档级 | 容器,handler= |
| form-field | form 或 form-group 内 | 空,非空 warning | 作用域内,#vendor#email | 一个字段,文字全在属性 |
| form-group | form 直接子层,不嵌套 | 只放 form-field | 作用域内,并开新作用域 | 可重复的一组字段;required 表示至少一条 |
| form-options | form 直接子层 | 表格体,format delim src 同 table | 作用域内,#vendor#entity-types | 选项列表,只被同 form 的 options= 消费,不单独渲染 |
| form-note | form 直接子层 | 流式 | 作用域内,#vendor#coc-note | 被字段的 label= / description= / placeholder= 指向的富文本,可被多个字段共用 |
options= 指向 table 是 error。文档里的 table 还是 table,chart 也不会绑到选项表上。之前:同一个 table 类型,在 form 外是数据表,在 field 内是选项表。description=#coc-note 指向 form-note,受检引用,渲染器能挂 aria-describedby。方向和 options= 一致。之前:只能写成字段上方不绑定的散文,geml get 取不到,读屏器读不到。geml list 一眼看出哪些块属于表单;form-field 正文永远为空。之前:field / table / text 与核心同名类型混在一起。form- 前缀,form-acme 这种名字要留给规范。现有 profile 的 style-rule 不受影响,style 不是注册类型。geml get '#vendor' 一次拿齐。跨 form 复用选项表改为 src= 指向同一个文件。options=#id 指 form-options,label= description= placeholder= 以 # 开头时指 form-note。这些 # 引用在所在 form 内解析,是 GEML 里唯一的相对引用,只出现在 form-field 的属性上。判别规则有先例:src= 的值以 # 开头就是引用,否则是路径。form-note,和核心 note 块平行,不会被当成 text。两个长得像、其实不同的东西。form-group 取代了上一版的 type=group,并且天然可重复。
multiple 落在标量字段上。Ant Design 是 Select mode="tags"。=== form-field {#tags label="标签" type=text multiple description="可填多个,逐个回车。"} ===
Form.List。分组是容器不是值形状,所以是 form-group 而不是 type=group;它天然可重复,不写 multiple。type= 回到 7 个纯值形状。==== 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 解析,组内任何字段都能用
form-group 就是重复组:零到多条,每条是它子字段值的一组;required 表示至少一条。只放在 form 直接子层,里面只放 form-field,开一个新作用域。multiple 只用在标量字段上表示多值,不是 form-group 的属性。非重复的复合分组不定义:纯视觉分组用体内标题。form-options契约不变,载体换成专门的类型。type=select 的值必须落在选项表的 value 列里。
=== 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 个带空格的选项也放得下。=== 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} ===
label 列是显示文本,其余列原样交给 handler。src= 可以从 CSV 文件引入长列表,两个 form 要同一份选项就指向同一个文件。form-options 只被同 form 的 options= 消费,不单独渲染;没人指向的给一条 warning。type 回到 7 个纯值形状。约束属性来自 profile,只声明不求值。
| type | 值的形状 | 渲染成 | 实例 | 备注 |
|---|---|---|---|---|
| text | 任意字符串,单行 | 输入框 | 11 | path、url、email、tel 全部并入。格式差异用 pattern= |
| textarea | 字符串,多段 | 多行输入框 | 7 | |
| number | 数 | 数字输入 | 1 | 范围用 min / max / step |
| date | 日期 | 日期选择 | 1 | time、datetime 有实例再加 |
| boolean | 真 / 假 | 勾选框或开关 | 1 | |
| select | form-options 的 value 列中的一个;multiple 时多个 | 下拉 / 单选组 / 多选组 | 4 | 含 next.js 的 44 选项下拉 |
| file | 文件 | 上传 | 0 | 唯一凭值形状留下的 |
geml-form/v1 profile,文档头部声明后才不报 unknown-attribute;处理器只存字符串,执行在 handler。=== 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 检查
| 约束 | 能否写进文档 | 谁执行 |
|---|---|---|
| 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,值照样保留。=== form-field {#brand-color label="品牌色" type=color} ===
短的在属性,长的在 form-note,form-field 正文永远为空。
=== 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 个链接的说明也放得下了。=== 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 的默认值逻辑。=== form-field {#plan label="结算方式" type=select multiple value=net30 options=#plans} ===
#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。
form-* 落在 form 外都是 error form-child-outside-form,消息里提示围栏可能写短了。=== form {#vendor handler=onboarding} 公司信息 === form-field {#legal-name label="公司名称" type=text required} ← 和 form 同长,把 form 关了 === ← 这行开了一个新的匿名块
==== form {#vendor handler=onboarding} 公司信息 === form-field {#legal-name label="公司名称" type=text required} === ====
geml check 退出码非零,文档不通过。| form-child-outside-form | error | form-* 落在 form 外 |
| form-field-has-body | warning | form-field 正文非空,不渲染 |
| unknown-field-type | warning | 7 个之外的 type,按 text |
| options-not-form-options | error | options= 指向的不是同 form 内的 form-options |
| note-not-form-note | error | label / description / placeholder 的 # 引用指向的不是同 form 内的 form-note |
| unused-form-block | warning | form-options 或 form-note 没有任何字段指向它 |
| duplicate-id | error | 同一作用域内字段 id 重复 |
GEML spec 1.0。带连字符的类型名能被当成块类型解析,profile 里的 style-rule 就是现成的例子。
| # | 输入 | 结果 | 说明了什么 |
|---|---|---|---|
| 1 | 按提案原文写的 form,末尾引用 [[#vendor#legal-name]] | warning unknown block type formerror unresolved reference | form 未注册只是 warning;字段引用是 error,#a#b 解析是实现的第一步。 |
| 2 | 同一文档,form / field 换成已注册的 note | 0 error 29 个字段、4 张选项表全部可寻址 | 嵌套在现有围栏规则下成立,缺的只是类型名和作用域规则。 |
| 3 | note 四层嵌套加体内标题加空体 | ok no diagnostics | group 嵌套、体内标题、空体机制上都已能解析。这一族最深只用到三层。 |