GEML block types, illustrated · the form proposal (GEP-0008, draft) and the geml-form/v1 profile · review page, 5th revision · 中文

GEML Blocks Illustrated · form

This page is the record of a design review, not a description of shipped behaviour: GEP-0008 is a draft, and none of the form-* types is registered yet. Fifteen decisions were taken over five revisions, each recorded on the board with who took it; below, every decision is drawn as it will look — GEML on the left, the control a host would render on the right. By the GEP's own conformance clause a renderer may only show a disabled preview, so every control here is greyed out and inert.

Board

15 decisions, all settled

Settled decided by the maintainer, or fixed in the proposal's text. Nothing remains open.

Settled 15 · including "form-group does not nest"
ItemDecisionStatusBy
1the form-* familyChild types carry the form- prefix: form-field form-group form-options form-note, all meaningful only inside a form, all ids in the form's scope — one rule. A form is therefore self-contained: geml get '#vendor' returns the whole form. §8.5 gains one sentence: an extension type name may not begin with a registered type name plus a hyphen.Settledmaintainer
2repeating groupsform-group repeats by nature — Ant Design's Form.List — with no multiple; required means at least one entry. It opens a scope; the address is #vendor#contacts#email. A non-repeating composite is not defined: visual grouping uses headings. Groups do not nest: a form-group sits directly in the form and holds only form-fields. Zero nested groups in the three trial forms; not nesting fixes the scope at two levels, the address at three segments, the fence at five =, and no tool walks a recursive scope.Settledmaintainer
3optionsCarried by form-options: a table body, format / delim / src as on table, placed directly in the form. Contract unchanged: first column is the value, a label column is the text, further columns go to the handler. options= may name only a form-options in the same form; naming a table is an error. Reuse across forms is one src= file.Settledmaintainer
4the type= settext · textarea · number · date · boolean · select · file — seven pure value shapes. group left the list and became form-group.Settledmaintainer
5unknown type valueDegrades to text with a warning, unknown-field-type.Settledmaintainer
6constraint attributespattern min max step maxlength accept go into the geml-form/v1 profile; declared, never evaluated; the handler enforces them.Settledmaintainer
7rich textlabel= description= placeholder= share one rule: plain text, or, beginning with #, a reference to a form-note in the same form. form-note carries no for=; like form-options it is a passive resource several fields may share. The test has precedent: a src= value beginning with # is a reference. A placeholder pointing at a form-note is flattened to text by the renderer. Named form-note.Settledmaintainer
8description=A one-line plain attribute, the grey line under the control. Longer guidance goes in a form-note, not in unbound prose.Settledmaintainer
9headings inside a formAllowed. Ordinary document headings, document-level ids, not fields.Settledmaintainer
10placeholder=A form-field attribute.Settledmaintainer
11textarea format=Not wanted.Settledmaintainer
12value= on multipleNames the single preselected option.Settledmaintainer
13form-* outside a formerror form-child-outside-form, for all four child types; a non-empty form-field body is a warning form-field-has-body.Settledmaintainer
14conditional logic / cross-field checksNot carried by the document; handler only.Settledproposal text
15Steps / tabs / buttons / feedbackApplication layer: GEP-0009 profiles and geml-style.Settledproposal text
Layers

Who describes, who renders, who validates

The answer to "where does validation live". It did not change through the revisions.

describes the document .geml

The form and its form-* children. What it says: which fields, of what type, required or not, which options, and declarations of format and range — keys admitted by the geml-form/v1 profile.

Does not: evaluate, validate or submit. §9.1, §8.3(5).

renders stylesheet / profile

Decides the look: one page or Steps, whether groups collapse, dropdown or radio group, whether min/max show as hints.

Does not: judge right or wrong. It renders a disabled preview and never sees input.

validates · submits the handler

Registered by the host under a name (handler=onboarding). The only party that ever holds a user's input: enforces the declared constraints, plus every business rule the document cannot carry.

Does not: decide how a field looks, or change the document.

A "constraint" is something declarable — a format or a range — written in the document. "Validation" is an act, and it happens only in the handler. A stylesheet holds no validation, only appearance.

Point 1

The form-* family: one form, four kinds of child

The maintainer's design. A complete form using all four child types and a heading inside the body; on the right, how it renders.

A complete form SettledFences go three deep at most: form at five =, form-group at four, everything else at three. A form-field body is always empty. Every form-* id lives in the form's scope.
GEML
=== meta
profile = "geml-form/v1"
===

===== form {#vendor handler=onboarding}
## Company {#company}

=== form-field {#legal-name label="Legal name" type=text required
                description="Exactly as on the business licence."}
===

=== form-options {#entity-types format=csv delim=;}
value ; label
llc   ; Limited liability company
jsc   ; Joint-stock company
sole  ; Sole proprietorship
===
=== form-field {#entity-type label="Entity type" type=select required
                options=#entity-types}
===

## Compliance {#compliance}

=== form-note {#coc-note}
Read the [code of conduct](https://example.invalid/coc) first. Ticking the
box accepts its **anti-bribery** clauses.
===
=== form-field {#agree label="I have read and accept the code of conduct" type=boolean required
                description=#coc-note}
===

## Contacts {#contacts-heading}

==== form-group {#contacts label="Contacts" required description="At least one."}
=== form-field {#name label="Name" type=text required}
===
=== form-field {#email label="Email" type=text pattern="^[^@]+@[^@]+$" required}
===
====
=====

%% addresses: #vendor#legal-name, #vendor#contacts#email, #vendor#entity-types, #vendor#coc-note
%% every form-* id is unique within the form's scope; the heading #company is not form-*, so document-level
%% a form-field attribute value beginning with # names a form-* block of the enclosing form: options=, label=, description=, placeholder=
Rendereddisabled preview
Company
Legal name
 
Exactly as on the business licence.
Entity type
Limited liability company ▼
Compliance
I have read and accept the code of conduct

Read the code of conduct first. Ticking the box accepts its anti-bribery clauses.

Contacts
Contacts At least one.
Name
Wang Fang
Email
wang.fang@example.invalid
Name
Li Qiang
Email
li.qiang@example.invalid
+ Add a contact
TypeWhereBodyidRole
formanywhere in a documentflow: form-* children, headings, prosedocument-levelthe container; handler=
form-fieldin a form or a form-groupempty; non-empty warnsin scope, #vendor#emailone field; all its text in attributes
form-groupdirectly in the form; does not nestform-fields onlyin scope, and opens a scopea repeating set of fields; required = at least one
form-optionsdirectly in the forma table body: format delim src as on tablein scope, #vendor#entity-typesan option list, consumed only by options= in the same form; never rendered alone
form-notedirectly in the formflowin scope, #vendor#coc-noterich text pointed at by a field's label= / description= / placeholder=; shareable
A table is no longer two thingsform-options is its own type; options= pointing at a table is an error. A table in the document stays a table, and a chart cannot bind to an option list.Before: one table type meant a data table outside a form and an option list inside a field.
Rich text is bound to its fieldThe field points at a form-note with description=#coc-note, a checked reference; the renderer can hang it as aria-describedby. The same direction as options=.Before: only unbound prose above the field, invisible to geml get and to screen readers.
One fence level fewerOption lists are no longer nested inside fields; the deepest is form › group › field.Before: form › group › field › table, four deep, the form opening at six =.
Self-describing types, one body rulegeml list shows at a glance which blocks belong to a form; a form-field body is always empty.Before: field / table / text blurred into the core types of the same name.
§8.5
The maintainer read it right: it only asks extensions to hyphenate; it does not forbid the specification a hyphen. What needs adding is the converse: an extension type name may not begin with a registered type name plus -, since form- is now the specification's. Existing profile names such as style-rule are unaffected — style is not a registered type.
Self-contained
With every id in scope, one form block is everything a renderer or a handler needs; geml get '#vendor' fetches it whole. The price: a form-field's # attributes resolve relative to the enclosing form, and reusing an option list across forms means one shared src= file.
One direction
Every binding is a field pointing at a helper block: options=#id at a form-options, and label= description= placeholder=, when they begin with #, at a form-note. These are GEML's only relative references, confined to form-field's attributes. The test has precedent: a src= value beginning with # is a reference, anything else a path.
Name
form-note, parallel to the core note block, so it cannot be mistaken for text.
Point 2

Multi-valued fields and repeating groups

Two things that look alike and are not. form-group replaced an earlier type=group, and it repeats by nature.

One field, several values SettledThe proposal's multiple on a scalar field. Ant Design: Select mode="tags".
GEML
=== form-field {#tags label="Tags" type=text multiple
                description="Several allowed; press Enter after each."}
===
Rendereddisabled preview
Tags
Yangtze Delta ✕ISO 9001 ✕contract ✕type, then Enter
Several allowed; press Enter after each.
A set of fields, repeated N times SettledAnt Design's Form.List. A group is a shape of structure, not of value, hence form-group rather than type=group; it repeats by nature, so no multiple. type= is back to seven pure value shapes.
GEML
==== form-group {#contacts label="Contacts" required
                description="At least one; add as many as needed."}
=== form-field {#name label="Name" type=text required}
===
=== form-field {#email label="Email" type=text pattern="^[^@]+@[^@]+$" required}
===
=== form-field {#role label="Role" type=select options=#roles}
===
====

=== form-options {#roles format=csv delim=;}
value ; label
biz   ; Business
tech  ; Technical
fin   ; Finance
===

%% addresses: #vendor#contacts#email, #vendor#roles
%% form-options sit directly in the form; options=#roles resolves in the enclosing form, so any group's field may use it
Rendereddisabled preview
Contacts At least one; add as many as needed
Name
Wang Fang
Email
wang.fang@example.invalid
Role
Business ▼
Name
Li Qiang
Email
li.qiang@example.invalid
Role
Technical ▼
+ Add a contact
Rule
form-group is the repeating group: zero or more entries, each the set of its children's values; required means at least one. It sits directly in the form, holds only form-fields, and opens one scope. multiple is for scalar fields only. A non-repeating composite is not defined: visual grouping is a heading's job.
No nesting
A form-group may not contain a form-group; doing so is an error. Zero instances in the three trial forms; not nesting fixes the id scope at two levels (form, group), the address at three segments, the fence at five =, and spares the parser and every downstream tool a recursive scope. The cost recorded under the proposal's Drawbacks 1 — a non-flat id space — is thereby bounded. It waits for a first instance.
Point 3

Options: form-options

The contract is unchanged; the carrier became a type of its own. A type=select value must fall in the option list's value column.

One document, rendered as a dropdown or as a radio group by the stylesheet Settled
GEML
=== form-options {#states format=csv delim=;}
value    ; label
draft    ; Draft
accepted ; Accepted
final    ; Final
===
=== form-field {#state label="State" type=select required options=#states}
===

%% what is submitted is the value `accepted`, not the label "Accepted"
%% options= pointing at a table rather than a form-options: error
Rendered · two skinsdisabled preview
State
Accepted ▼
dropdown
State
Draft
Accepted
Final
radio group, the stylesheet's choice
With multiple it is a multi-select, and option labels may carry links SettledA form-options body parses its cells inline like a table, so a label column may hold a link. The 44 space-carrying options of the next.js form fit too.
GEML
=== form-options {#categories-list format=csv delim=;}
value        ; label
raw          ; [Raw materials](https://example.invalid/cat/raw) — metals, polymers, textiles
parts        ; [Components](https://example.invalid/cat/parts) — machined, moulded, PCB
contract-mfg ; Contract manufacturing
logistics    ; Logistics and warehousing
===
=== form-field {#categories label="Supply categories" type=select multiple required
                options=#categories-list}
===
Rendereddisabled preview
Supply categories
Raw materials ✕Contract manufacturing ✕▼
Raw materials — metals, polymers, textiles
Components — machined, moulded, PCB
Contract manufacturing
Logistics and warehousing
Contract
The first column is the value submitted, a label column the text shown, further columns pass to the handler unread. src= brings a long list in from a CSV file; two forms wanting the same list point at the same file. A form-options is consumed only by options= in its own form and never rendered alone; one nothing points at earns a warning.
Point 4

type and constraints

type is back to seven pure value shapes. Constraint keys come from the profile: declared, never evaluated.

Seven types: value shape, control, measured count SettledCounts come from the 23 fields of the two checked-in trial forms plus the 9 elements of the next.js form. GitHub's form schema has only four field types.
typevalue shaperenders ascountnote
textany string, one lineinput11path, url, email and tel all fold in; format differences via pattern=
textareastring, several paragraphsmulti-line input7
numbera numbernumeric input1range via min / max / step
datea datedate picker1time and datetime wait for an instance
booleantrue / falsecheckbox or switch1
selectone value of a form-options' value column; several with multipledropdown / radio group / checkbox group4includes the 44-option next.js dropdown
filea fileupload0the only one kept on value shape rather than measurement
Three fields, three homes Settled · in the profileConstraint keys come from the geml-form/v1 profile; without the declaration they are unknown-attribute warnings. The processor stores the string; the handler enforces it.
GEML
=== meta
profile = "geml-form/v1"
===

=== form-field {#revenue label="Annual revenue (CNY, millions)" type=number
                min=0 max=99999 step=1 description="Whole millions; blank if unaudited."}
===
=== form-field {#phone label="Mobile" type=text pattern="^1\d{10}$" required
                description="11 digits"}
===
=== form-field {#contract-end label="Contract end" type=date required
                description="Must be after the start date."}
===
%% "after the start date" is a cross-field rule: the document can only say it to a reader; only the handler checks it
Rendereddisabled preview
Annual revenue (CNY, millions)
12
Whole millions; blank if unaudited. declared in the document handler enforces
Mobile
138 0000 0000
11 digits declared in the document handler enforces
Contract start
2026-10-01 ▦
Contract end
2027-09-30 ▦
Must be after the start date. handler only
ConstraintCan the document carry it?Who enforces
pattern · min · max · step · maxlength · acceptYes, as profile attributes. The document stores strings and never evaluates them.handler; a stylesheet may show them as hints
type=number · date · select · fileYes. The type itself is a shape constraint.handler
end after start · A excludes B · Y required only when X is tickedNo. §9.1: no expressions, no conditionals. Write it in the description for the reader.handler only
An unknown type= value SettledDegrades to text with a warning, unknown-field-type; the value is kept.
GEML
=== form-field {#brand-color label="Brand colour" type=color}
===
Rendereddisabled preview
Brand colour
#1F6F8B
warning · unknown-field-type rendered as text.
Point 5

label · description · form-note: where each kind of text goes

Short text in attributes, long text in a form-note, the form-field body always empty.

An ordinary field: two attributes, an empty body Settled"Exactly as on the business licence." is the grey line under the control. Its position is the stylesheet's.
GEML
=== form-field {#legal-name label="Legal name" description="Exactly as on the business licence."
                type=text required}
===

%% a non-empty body: warning form-field-has-body, not rendered
Rendereddisabled preview
Legal name
 
Exactly as on the business licence.
Guidance with links or several paragraphs: an attribute pointing at a form-note SettledThe form-note sits in the form, outside the field, with no for=; the field's description= beginning with # points at it, and the renderer hangs it as aria-describedby. The next.js form's five-paragraph, fifteen-link descriptions fit.
GEML · inside the form
=== form-note {#coc-note}
Read the [code of conduct](https://example.invalid/coc) first.

Ticking the box accepts its **anti-bribery** and **data protection** clauses.
===
=== form-field {#agree label="I have read and accept the code of conduct" type=boolean required
                description=#coc-note}
===

%% #coc-note resolves in the enclosing form, i.e. #vendor#coc-note; several fields may point at one form-note
%% pointing at something other than a form-note: error; a form-note nothing points at: warning
Rendereddisabled preview
I have read and accept the code of conduct

Read the code of conduct first.

Ticking the box accepts its anti-bribery and data protection clauses.

Rule
label= description= placeholder= share one rule: the value is plain text, or, beginning with #, points at a form-note in the same form. Plain is one line of text; a form-note is flow content with links, bold, several paragraphs. A label pointing at a form-note renders inline; a placeholder pointing at one is flattened to text, since an input's placeholder cannot hold markup.
Name
Settled as form-note.
Edge cases

Three settled corners

value= on a multi-select names the single preselected option SettledAttribute values have no arrays. Preselecting several is the handler's default-value logic.
GEML
=== form-field {#plan label="Settlement plan" type=select multiple value=net30 options=#plans}
===
Rendereddisabled preview
Settlement plan
net30 ✕▼
Only one preselected.
A heading inside a form: allowed SettledThe heading's id enters the document space; field ids enter the form's scope. A probe confirmed the parser already reads it this way.
actual geml list output
#vendor            form            L7-30
#contacts-heading  heading  h2     L8-29   ← document-level id, not #vendor#contacts-heading
#contact-name      …                     ← per the proposal, #vendor#contact-name
Rule

A heading inside a form is an ordinary document heading: its id is document-level, untouched by field scoping, and it is not a field. Its section ends at the enclosing form or form-group fence, or at the next heading of the same or higher level. Visual grouping uses it; form-group is not for that.

A fence one = too short Settled · errorAny form-* block outside a form is an error, form-child-outside-form, whose message points at the likely short fence.
GEML · wrongthe form closes early
=== form {#vendor handler=onboarding}
Company
=== form-field {#legal-name label="Legal name" type=text required}   ← same length as the form: closes it
===                                         ← opens a new anonymous block
GEML · right
==== form {#vendor handler=onboarding}
Company
=== form-field {#legal-name label="Legal name" type=text required}
===
====
result of the wrong version
error · form-child-outside-form
form-field {#legal-name …} sits outside a form. The enclosing form's fence may be too short.
geml check exits non-zero; the document does not pass.
the family's diagnostics
form-child-outside-formerrora form-* block outside a form
form-field-has-bodywarninga non-empty form-field body, not rendered
unknown-field-typewarninga type outside the seven; rendered as text
options-not-form-optionserroroptions= names something other than a form-options in this form
note-not-form-noteerrora # in label / description / placeholder names something other than a form-note in this form
unused-form-blockwarninga form-options or form-note no field points at
duplicate-iderrortwo ids the same within one scope
Evidence

Three probes

GEML spec 1.0. Hyphenated type names already parse as block types — the profile's style-rule is an existing case.

#InputResultWhat it showed
1A form written as the first draft spelled it, ending with [[#vendor#legal-name]]warning unknown block type form
error unresolved reference
An unregistered form is only a warning; a field reference is an error, so #a#b resolution is the first thing to implement.
2The same document with form / field replaced by the registered note0 errors 29 fields and 4 option tables all addressableNesting holds under the existing fence rules; what is missing is the type names and the scoping rule.
3note nested four deep, with a heading in the body and an empty bodyok no diagnosticsGroup nesting, headings in a body and empty bodies all parse already; the family uses at most three levels.