GEML 图解 · profile · geml-style/v1(2026-08-30 落地,EXPERIMENTAL) · English

GEML 图解 · geml-style

样式表是一份普通的 .geml:声明 profile = "geml-style/v1",装三种全空体的块,用 CSS 味的选择器选进内容文档而不改它,把块绑到宿主注册的组件名上。没有脚本,歧义是构建错误,不靠源顺序。它的一致性面是 geml style check --json 吐出的视图模型。这一页用一份干净的样式表和一份故意写坏的样式表,把 13 条诊断逐条跑出来。

看板

19 条规则,各自的出处和状态

规范已定 profile 文档写死的。实测 文档不规定、工具今天这样做。实现偏差本页未发现。注意 §0.1:只有 codemap 显示旋钮用到的子集承诺稳定。

规范已定 16实测 3
规则出处状态
1样式表是普通 .geml,三种块全空体:信息都在属性对象里,因为未注册类型的正文是 raw、核心不解析,属性对象却对所有类型都解析。规则选进文档而不是模板包住文档,因为内容常是机器生成不可改的。无脚本:component / handler 只是名字,宿主给实现。§0 · §2规范已定
2profile 是空格分隔列表,多个 profile 取词汇并集;校验只问「名字允许吗」,不问含义。没声明时每条 style-rule 一个 unknown-block-type warning(实测 5 条),这正是训练人忽略 warning 的东西。§1规范已定
3style-rule:match= 必填;component= handler= show= filter= screen=(空格分隔)是 profile 自己消费的全部键;其余键原样透传给组件,所以 rule 上没有未知属性检查。§2.1规范已定
4style-state:match=(生产者选择器)和 on= 必填,on 是封闭词汇,目前只有 select;type= block-ref | scalar 不校验;value-from= 带方向;init-value=。未知键 warning。多个生产者写一个 state 是随时间赋值,不是冲突。§2.2规范已定
5style-screen:slots= 必填,逗号分隔、有序,每个是选择器或 $state;layout= 不校验(宿主的事);没有 route=,路由归宿主框架;未知键 warning。§2.3规范已定
6选择器 = §4 自己的词汇:type、.class、#id、[key]、[key=value],加唯一的后代组合子(空格)和逗号分支。拒绝的 CSS 被点名而不是静默不匹配:> + ~、伪类、*、子串匹配 → selector-unsupported error。扫描分区:伪类只在括号外找,子串运算符只在括号内找,因为属性值里合法地有 :。§3规范已定
7冲突按属性合并;两条规则给同一块设同一属性时,条件集严格超集者胜,否则 ambiguous-rule error。没有 specificity 算术、没有 !important、没有源顺序回退:靠顺序的样式表会被 geml set / add --before 悄悄改掉。只对语料里真实共现的块判。唯一的例外见规则 16:跨层按层号裁决,而层不是源顺序。§4 · §4.1规范已定
8绑定管线 interaction → state → view,单向三段,state 从不读 state,没有图所以没有环,目录里没有 binding-cycle。三个消费算子:show="$s"、filter="k=$s"、title="$s.caption";由运行时执行而不是组件;检查时只验每个 $name 有 state 声明。§5规范已定
9分隔符只有一条规则:名字列表用空格,选择器列表用逗号,因为空格是后代组合子。§6规范已定
10封闭词汇(运行时自己解释,如 on=)未知成员 → error;开放注册表(宿主注册,如 component= handler=)未知成员 → warning + inert;且 unknown-component / unknown-handler 只在调用者用 --components= / --handlers= 声明注册表时才检查。§7规范已定
11诊断目录 13 条,属于 profile 自己不进 Appendix A:7 个 error(selector-unsupported、ambiguous-rule、unknown-state、unknown-screen、unknown-value-source、unknown-interaction、style-missing-attribute)、6 个 warning(unmatched-rule、unmatched-producer、unknown-component、unknown-handler、style-unknown-attribute、style-embed-not-expanded)。§8规范已定
12geml style check <sheet> <corpus…> [--json] [--components=] [--handlers=];exit 0 干净或只有 warning,1 有 error,2 用法错。--json 是视图模型,一致性面:states、screens、bindings、diagnostics;binding 带 doc(id 只在文档内唯一);bindings 按 screen;slots 已解析成块列表,不给原始选择器。§9 · §10规范已定
13稳定范围:承诺稳定的只有 profile 声明、style-rule、match=、属性透传,加上样式入口的路径与 default-style——codemap 的显示旋钮用到的这些「逃出去了」:每次 build 往用户仓库播种 _index/style.geml 和指向它的 _index/index.geml,而渲染器只经那个入口找样式表。#sitemap 那一列表存在但真实 map 里还没人用,所以仍可动。style-state、style-screen、show / filter / handler / screen、on / value-from / init-value / type / layout 都是「已规定、已检查、未使用」,随第一个真实用例移动。v1 有意没有:脚本、URL、路由、设计令牌之外的主题、正文。§0.1 · §12规范已定
14实测:干净样式表 0 诊断;声明 --components=edge-list 后 method-card 报 unknown-component warning;写坏的样式表 7 error 3 warning,与目录逐条对上。geml style check实测
15样式入口:任何根只有 <root>/_index/index.geml 一个固定路径,宿主只探它;名字用来找,meta.profile 用来认,路径上放了别的东西就当「没有入口」。两个键说明加载哪几份:default-style(meta 键)和 #sitemap(表,document/template 两列,精确匹配、无 glob、无级联)。两者都是一次隐式 embed,所以环检测、深度上限、诊断全部沿用 embed 那一套;embed {src=index.geml} 因此等于「给我本根的默认,不管它叫什么」。§1.1规范已定
16层:入口把样式表排成 default-style(0)→ #sitemap 命中的那份(1)→ 入口自己的规则(2)。跨层按层号裁决、上层胜;层内规则 7 一字不改。层号是声明出来的顺序,不是从选择器算的分数——CSS @layer 的模型,不是 specificity。显式 embed 不开新层。排除源序的理由依然成立:同层内无序,#sitemap 精确匹配所以改行序不改结果,层数由那两个键固定。代价明写:「裁决与规则来自哪个文件无关」从此只对层内成立。§4.1规范已定
17实测:经入口的两层 0 诊断,#hero 被两条规则同时命中而 component 取上层的值、#facts 仍由默认层管;同两条规则挤进一层立刻 ambiguous-rule error;default-style 指不到时 style-embed-not-expanded warning、exit 0、本文件规则照常装载。geml style check实测
18第二个用例(2026-09-10,外壳归位):选择器可以以行内部件收尾(link image code-span strong emphasis,只能是最后一步、前面要有块步);内含词里有块上的 axis,以及看源码的 view / editable;when= 认五个内建条件——@hover @focus @invalid @disabled @checked;合并后没有 component= 接的参数报 style-unknown-attribute。判据仍是 §12.3「换个块还是不是这个意思」。设计 2026-09-10 §4规范已定
19实测:GitHub blob 复刻页的外壳全部改成 text 块里的列表 + 行内链接后,geml style check 0 error 0 warning;viewer 组件 7 → 2(tree、segments),宿主 CSS 页面段色值 22 → 0,样式表私有键 10 → 0,页面 CSS 的 59 处色值全部来自样式表。geml style check · render实测
干净

一份干净的样式表,和它的视图模型

内容文档是仓库测试夹具里的小 codemap:两个方法、一张 #calls 表。

state · rule ×3 · screen → --json 规范已定
good.style.geml
=== meta
profile = "geml-style/v1"
===
=== style-state {#sel type=block-ref match="table#calls" on=select value-from=to}
===
=== style-rule {#edges match="table#calls" component=edge-list selectable}
===                                  %% selectable 不是 profile 的键:原样透传给组件
=== style-rule {#methods match="code[anchor]" component=method-card}
===
=== style-rule {#leaves match="code.leaf[anchor]" collapsed badge="leaf"}
===                                  %% 条件集是 #methods 的严格超集:叶子上两条都生效,不冲突
=== style-screen {#overview layout=split slots="table#calls, $sel"}
===                                  %% 逗号分隔;$sel 槽渲染 state 指向的块
content.geml · 夹具
=== meta
profile = "geml-codemap/v1"
module = "geml-parser/core"
===
=== code {#renderHtml anchor="ts:render-html.ts#renderHtml(Document,RenderOptions)"}
===
=== code {#esc .leaf anchor="ts:render.ts#esc(string)"}
===
=== table {#calls format=csv}
from,to,kind,confidence
renderHtml,esc,call,high
===
geml style check good.style.geml content.geml --json0 error 0 warning
{
 "states": [ { "id": "sel", "type": "block-ref", "on": "select", "valueFrom": "to" } ],
 "screens": [ {
   "id": "overview", "layout": "split",
   "slots": [
     { "kind": "blocks", "selector": "table#calls",
       "blocks": [ { "doc": "content.geml", "block": "#calls" } ] },   ← 已解析
     { "kind": "state", "state": "sel" }
   ] } ],
 "bindings": [
   { "doc": "content.geml", "block": "#renderHtml", "rules": ["methods"],
     "params": { "component": "method-card" } },
   { "doc": "content.geml", "block": "#esc", "rules": ["methods", "leaves"],
     "params": { "component": "method-card", "collapsed": true, "badge": "leaf" } },
   { "doc": "content.geml", "block": "#calls", "rules": ["edges"],
     "params": { "selectable": true, "component": "edge-list" } }
 ],
 "diagnostics": []
}

$ geml style check good.style.geml content.geml --components=edge-list
warning: unknown-component: component `method-card` is not registered — renders inert (#methods)
合并
§4:#esc 被 #methods(code[anchor])和 #leaves(code.leaf[anchor])同时选中,但两条设的属性不同、且后者条件是前者的严格超集,所以 params 是两者之和。同属性、条件不可比才是 ambiguous-rule。
视图模型
§10:这份 JSON 是第二个实现必须一致的东西。binding 带 doc,因为 §4 只保证 id 在文档内唯一,一份样式表管一个目录时两份文档都可以有 #budget。slots 已经解析成块列表,宿主不必在运行时重做选择器匹配。
注册表
§7:不给 --components= 时 unknown-component 根本不检查,因为永远不会响的诊断比没有更糟;给了 edge-list 之后 method-card 就报出来,inert 回退。
写坏

一份故意写坏的样式表,十条诊断

七个 error 三个 warning,对着 §8 的目录逐条数。

bad.style.geml 对同一份 content.geml 规范已定
GEML
=== style-rule {#a match="code[anchor]" component=card-a}
===
=== style-rule {#b match="code.leaf" component=card-b}
===                                  %% 同设 component,条件集不可比:#esc 上撞车
=== style-rule {#c match="table > code" component=x}
===                                  %% 子组合子:块模型只有包含没有相邻
=== style-rule {#d match="table#nowhere" component=y show="$ghost"}
===                                  %% 没人声明的 state;选择器也匹配不到任何块
=== style-state {#s match="table#calls" on=hover value-from=nope}
===                                  %% on 封闭词汇只有 select;nope 不是 #calls 的列
=== style-screen {#scr slots="table#calls" route="/x"}
===                                  %% 没有 route=:路由归宿主
=== style-rule {#e match="code" screen="missing-screen"}
===
geml style check7 error 3 warning
error: selector-unsupported: `>` is not supported (supported: type, .class,
  #id, [attr], [attr=val], descendant) (#c)
error: unknown-interaction: `on=hover` is not an interaction this profile
  defines (known: select) (#s)
warning: style-unknown-attribute: unknown attribute `route` for `style-screen` (#scr)
error: unknown-screen: rule `#e`: `screen=missing-screen` names no
  `style-screen` block (#e)
error: ambiguous-rule: `#a` and `#b` both set `component` on
  `content.geml#esc` — neither is more specific; write a rule matching
  the union of both selectors (#b)
error: ambiguous-rule: … in screen `#scr` … (#b)
warning: unmatched-rule: rule `#d` matched no block in the corpus (#d)
warning: unmatched-rule: rule `#e` matched no block in the corpus (#e)
error: unknown-state: `$ghost` is not declared by any `style-state` block (#d)
error: unknown-value-source: state `#s`: `value-from=nope` is not a column
  of `content.geml#calls` (has: from, to, kind, confidence) (#s)
7 error(s), 3 warning(s)   exit 1
error
结构性的错:不支持的 CSS 被点名;封闭词汇外的交互;引用不存在的 screen、state、列;同一属性两条不可比的规则。ambiguous-rule 报了两次,一次全局一次在 #scr 屏幕里,因为 bindings 按 screen 各算一份。
warning
漂移和多余:规则没匹配到任何块(样式表内部一致、但和语料脱节,是样式层的 bad-source-range);screen 上的未知键。
为什么点名
§3:CSS 相似度是坡道不是陷阱。> 若被静默忽略,作者会以为它生效了。
入口

一个目录怎么说明自己怎么渲染:样式入口与层

单份样式表可以直接交给工具。一整个目录要说明「我这堆文档怎么渲染」,就需要一个约定位置:<root>/_index/index.geml。它用两个键排出层——§4.1,profile 里唯一按来源裁决的地方。

default-style 命中与否都加载;#sitemap 命中的那份叠在上面 规范已定
_index/index.geml · 样式入口
=== meta
profile = "geml-style/v1"
default-style = "base.geml"
===                                  %% 层 0:命中与否都加载
=== table {#sitemap}
| document  | template  |
|---|---|
| page.geml | hero.geml |
===                                  %% 层 1:精确匹配,没 glob 没级联
_index/base.geml · 层 0
=== style-rule {#d-notes match="note" component=section}
===
=== style-rule {#d-tables match="table" component=card-grid}
===                                  %% 按类型定的默认层
_index/hero.geml · 层 1
=== style-rule {#hero match="#hero" component=hero}
===                                  %% 按具体块定的覆盖层
page.geml · 内容
=== note {#hero}
GEML is an Agent-Native base document format.
===
=== table {#facts}
| k | v |
|---|---|
| spec | v1 |
===
geml style check _index/index.geml page.geml --json0 error 0 warning
{
 "bindings": [
  {
   "doc": "page.geml",
   "block": "#hero",
   "rules": [
    "d-notes",
    "hero"
   ],
   "params": {
    "component": "hero"
   }
  },
  {
   "doc": "page.geml",
   "block": "#facts",
   "rules": [
    "d-tables"
   ],
   "params": {
    "component": "card-grid"
   }
  }
 ],
 "diagnostics": []
}

入口就是那份模板 —— 两个键都是一次隐式 embed,所以这条命令不必先人肉查表再传另一个文件。宿主也因此一行解析逻辑都不用写。

叠加,不是替换
#facts 只被层 0 管到,照样渲染成 card-grid。「命中就换一份」的实现会让它一条绑定都没有。
两条规则都在 rules 里
#hero 被 d-notes(层 0,match="note")和 hero(层 1,match="#hero")同时命中,两个 id 都记在绑定里;决定 component 值的是层号。绑定保留全部命中规则,是为了让人看得出这个属性为什么是这个值。
层号不是 specificity
#hero 赢不是因为 id 选择器「更值钱」——CSS 的那套算术 §4 明确拒绝过。它赢是因为 #sitemap 把它放进了更上面那一层。这是 CSS @layer 的模型:层号来自入口那两个键,选择器一个字都不参与。
源序依然不参与
层不是文件里的行序:同层内无序,#sitemap 精确匹配所以改行序不改结果,层数由那两个键固定。agent 的按块编辑(geml set、geml add --before)因此仍然不会静默改变渲染——那正是 §4 排除源序要守的东西。
把同两条规则挤进一层:立刻撞 ambiguous-rule 实测
flat.geml · 同一份文件,同一层
=== style-rule {#d-notes match="note" component=section}
===
=== style-rule {#hero match="#hero" component=hero}
===                                  %% {type=note} 与 {id=hero} 互不包含
geml style check flat.geml page.geml1 error
error: ambiguous-rule: `#d-notes` and `#hero` both set `component` on
  `page.geml#hero` — neither is more specific; write a rule matching the
  union of both selectors (#hero)
1 error(s), 0 warning(s)   exit 1

这就是层存在的理由,不是它的边角情形:「默认层按类型 + 覆盖层按具体块」是这个 profile 最常见的写法,而这两种选择器的条件集永远互不包含。没有层的时候,playground 那五份首页文档全部报错。

层内 §4 一字不改
上面这条错误在有层之后依然是错误——层只解决跨层。补救办法也照旧:把覆盖规则写成严格超集(match="note#hero"),或者把它挪到更上面那一层。
代价说清楚
§4 开头「裁决与规则来自哪个文件无关」从此只对层内成立。这是 §4.1 明写的取舍,不是实现漏洞。
第 13 条诊断:default-style 指不到,说出来而不是静默少一层 规范已定
_index/broken.geml
=== meta
profile = "geml-style/v1"
default-style = "gone.geml"
===
=== style-rule {#only match="table" component=card-grid}
===                                  %% 本文件的规则照常装载
geml style check _index/broken.geml page.geml1 warning
warning: style-embed-not-expanded: `embed` of `gone.geml` contributed no
  rules: cannot resolve `gone.geml` (#default-style)
0 error(s), 1 warning(s)   exit 0

诊断的 id 是 #default-style —— 那是合成的 embed 块的 id。隐式 embed 和写出来的 embed 在下游无从区分,所以报错路径也是同一条。

为什么是 warning
和 style-unknown-attribute 同一性质:我们忽略了作者写下的东西,该说出来。消息带原因——读不到、锚点不存在、成环、或者调用方没给文档解析钩子。
为什么不能沉默
一份看起来组合好了的样式表,实际只有本文件那几条规则生效,页面少一大块而没有人吭声。这是这条诊断存在之前真实发生过的:同一份文档该出 26 个绑定,只出了 3 个,零诊断。
旋钮

第一份真实样式表:codemap 播种的 style.geml

§11 的例子。它只用了稳定子集:一条 style-rule、match=、透传。

每个旋钮都是组件参数,不是 profile 词汇 规范已定
<codemap>/_index/style.geml
=== meta
profile = "geml-style/v1"
title = "codemap graph style"
===

=== style-rule {#graph match="diagram[format=geml-code-graph]" \
                fold=1 depth=6 hide-accessors=true \
                palette="#e3f2fd #e8f5e9 …"}
===                                  %% palette 是名字列表:空格分隔(§6)
它体现的规则
profile 声明
+
一条 style-rule
+
match= 选中 code-graph 图
+
fold / depth / hide-accessors / palette 透传
§0.1:这四样是 v1 唯一承诺稳定的子集,因为它们已经随每次 codemap build 种进了用户仓库
为什么这样
§11:以前显示的那一半写死在渲染器里,「想调一下长相」就得改一个服务所有人的渲染器。现在数字从样式表来,渲染器没换,默认值逐个等于原行为,缺文件就回退到内置默认。
与 foldings.geml
一对:foldings.geml 调构建期的模块命名,style.geml 调显示。首次构建播种,之后永不重写。见第 9 页。
其余
§12:filter= 从未对真实噪声跑过,handler= 没有真实宿主,on= 封闭词汇只有一个成员因为只接了一种交互。它们「已规定、已检查、未经实战」,所以 geml style check 在 --help 里标 EXPERIMENTAL:今天正确,明年拼写不保证。
依据

探针

在短路径临时目录里跑,内容文档取自 geml-parser/test/fixtures/style/codemap-content.geml。用仓库内当前构建,1.10.1。

探针结果对应
good.style.geml + content.geml0 诊断,--json 视图模型如上干净、看板 12
同上 --components=edge-list1 warning unknown-component method-card看板 10
bad.style.geml + content.geml7 error 3 warning写坏、看板 11
_index/index.geml(两层)+ page.geml0 诊断,#hero→hero、#facts→card-grid入口
flat.geml(同两条规则挤一层)+ page.geml1 error ambiguous-rule入口
_index/broken.geml(default-style 指不到)1 warning style-embed-not-expanded入口
删掉 profile 声明后 geml check5 warning unknown-block-type看板 2