geml-style/v1 · 逐维对照 · 2026-09-13 · English · 全部图解 · GEML 首页
十个维度,左边 CSS、右边 geml-style,例子都从 examples/style-demo
那份真跑起来的样式表里取。结论先说:八个维度上它是 CSS 的一个受限子集,
读 CSS 的人不会被绊住;剩下两个是 CSS 根本没有的东西(样式表决定内容摆在哪、
歧义直接报错),这两处一定会违反常识,而且该违反。
这次对照本身查出三处是缺口而不是立场(记号、简写、继承),三处都已写进 profile。
全部是当前分支上的实测,不是估计。样式表 = examples/style-demo/_index/github.style.geml,一份 1:1 复刻 GitHub blob 页的真实用例。
meta 里 13 个键喂 75 处 {{…}};裸色值只剩 8 处标签的含义:近似 = 换个写法而已,CSS 的直觉直接适用;有意不同 = 故意做成另一套,理由成立;反直觉 = 写 CSS 的人到这里会先愣一下。
/* 另一门语言,另一个文件,
另一套语法和解析器 */
.sidebar {
width: 296px;
}#「样式表」就是一份 GEML 文档, # 每条规则是一个可寻址的块 === style-rule {#w-side match="text#sidebar" width="296px"} ===
CSS 是一门独立语言;geml-style 没有自己的语法,它是 GEML 的一个 profile。
好处很具体:geml get sheet.geml '#w-side' 能单取一条规则,geml set 能只改这一条,
.gemlhistory 能回滚这一条 —— 这是给 agent 编辑用的,不是给人手写用的。
代价也很具体:写起来更啰嗦,一条规则一个块,104 条规则就是 104 个块。
#api table.kpi /* 后代 */ #api > table /* 子 */ h2 + p /* 相邻 */ li:nth-child(2n) /* 位置 */ a:hover /* 状态 */ [href^="https"] /* 前缀 */ * /* 通配 */
#api table.kpi ✓ 唯一的组合器 text#nav link ✓ 行内部件(末步) * ✓ 但只能整步 [key] [key=value] ✓ 存在 / 相等 > + ~ selector-unsupported :hover :nth-child() selector-unsupported ^= $= *= |= selector-unsupported
词汇量是 CSS 的一小半:类型 + .class + #id + 属性存在/相等,
加一个后代组合器。多出来的是行内部件步 —— link image code-span
strong emphasis —— CSS 里这些是元素,GEML 里块内行内没有独立地址,
所以给了五个名字。关键差别不在少了什么,在于少了的东西会报错。
写 #api > table 不是不匹配,是 selector-unsupported,check 直接失败。
「像 CSS 是个坡道,不是陷阱」——规范原话。
/* 四级仲裁,永远有赢家 */ !important > 特异性算术 (1,0,0) vs (0,2,0) > @layer 次序 > 源码顺序(最后的兜底) /* 两条同分?后写的赢。 静默,无提示。 */
# 一条关系:条件集严格超集 {type, .class, #id, [attr], screen:x, when:s=v} A ⊃ B → A 赢 否则 → ambiguous-rule(error) # 简写与单边同属一个裁决键: border vs border-left 同层、不同规则 → ambiguous-rule # 没有特异性算术 # 没有 !important # 没有源码顺序 # 层只来自 style-entry(0/1/2)
这是最大的一处分歧。CSS 保证「一定有一个赢家」,代价是赢家可能不是你以为的那个;
geml-style 保证「不确定就不猜」,代价是两条都合理的规则会让 check 失败,你必须自己拆开。
排掉源码顺序是有具体理由的,写在规范里:靠顺序定胜负的样式表,会被 geml add --before
这种块级 agent 编辑悄悄改掉渲染结果 —— 而这个格式存在的全部意义就是支持那种编辑。
层还在(默认层 / sitemap / 入口自己,0-1-2),但层号是声明的,选择器一点都不参与 —— 这是 CSS 的
@layer,不是特异性。
「没有源码顺序」是当真的,连简写都堵上了。裁决按属性名做,
而 border 和 border-left 是两个名字,本来永远不会相遇 ——
于是两条规则都活着,谁在文件里靠后谁赢,零诊断。这正是 geml add --before
会悄悄改掉渲染结果的场景。现在简写与四个单边被当作同一个裁决键:
同层、同 when 组、来自不同规则时直接报 ambiguous-rule。
选的是"不确定就不猜",不是"发 CSS 前替你排个序"—— 后者等于偷偷引入一条优先级规则。
/* 数百个属性,开放增长 */ display position z-index transform transition opacity box-shadow filter clip-path grid-template-areas aspect-ratio /* 认不出的属性 → 静默丢弃 */ colr: red; /* 什么都不发生 */
# 38 个内含词,封闭 width max-width min-width height max-height min-height padding margin gap font-size line-height font-family font-weight text-align color background underline border border-top/right/bottom/left border-radius axis item-align item-justify wrap anchor place visible grow sticky scroll hide-below view editable fade-out fade-in colr="red" → 透传给组件; 没有组件 → style-unknown-attribute
38 个词是「换个块还是不是这个意思」筛出来的:width 放段落、表格、图上意思一样,
所以是内含词;fold 只对树控件说得通,所以是组件参数。
缺席的东西值得单独说:没有 display、没有 position、没有 z-index、
没有 transform、没有 transition、没有 opacity、没有 box-shadow。
anchor=parent|viewport 连同 place,加上 sticky=top|right|bottom|left,
是 position 那一小块被封装后的样子;visible=no 是 display 那一小块。
这不是「还没做」,是「块布局需要的就这些」—— 但一旦要做阴影、过渡、任何装饰性效果,
今天只能回宿主 CSS。
:root { --border: #d1d9e0; }
.a { border-color: var(--border); }
.b { border-color: var(--border); }
width: calc(100% - 2rem);
color: color-mix(in srgb, ...);# 记号:样式表自己的 meta 就是表 === meta line = "#d1d9e0" === border="1px solid {{line}}" # 改主题只动 meta 这一个块 # 悬空记号 → unknown-token(error) # 值仍是不透明安全串,白名单字符: # [A-Za-z0-9 #%.,()+-/_] # calc() rgb() 能过(括号在白名单) # url( 和 /* 单独点名拒掉
开放值域的词(width color border)原样进 CSS 文本,
而样式表是不可信输入 —— 一个 width="0} body{display:none} .x{" 就能改写整页,
所以有字符白名单,拒掉的会报回去而不是静默。
变量这一块由「记号」补齐:样式表自己 meta 的每个键都能在任何属性里写成
{{key}},装载期展开,保留类型(数值词能被喂一个数),
展开发生在 safeCssValue 之前,所以"值是不透明串"这条没被破坏。
实测 13 个键喂 75 处引用,裸色值从散落各处收敛到只剩 8 处。
代价是它不是 CSS 自定义属性的等价物 —— 没有作用域、没有级联覆盖,
它是装载期的文本替换,一份样式表一张表。
/* CSS 从不决定内容出现在哪。
DOM 的顺序 = HTML 说了算。
order / grid-area 能重排,
但不能凭空把一个块搬进
另一个容器。 */=== style-frame {#header axis="row" slots="text#logo, text#nav link, #search-box"} === # 槽位决定哪些块进这个区域、 # 按什么顺序。内容文档里没有 # 任何一行说自己属于 header。
这一维 CSS 里根本不存在,所以谈不上「符不符合常识」—— 它更像 XSLT 或模板引擎,
不像样式表。实测的效果是:page.geml 里只有内容,一个字的 chrome 文本都没有,
27 个 frame 把它们组装成 GitHub 的那张页面。
这是整个设计里最值钱的一条,也是最容易被 CSS 出身的人误解的一条 ——
「样式表」这个名字在这里已经不准确了,它是一份版面单。
a:hover { color: blue }
input:checked ~ .panel { display: block }
/* 更复杂的状态 → :has()、
checkbox hack,或者交给 JS */=== style-state {#tab match="..." on="click" init-value="preview"} === === style-rule {match="#pane" when="$tab=code" visible="yes"} === === style-rule {match="text#nav link" when="@hover" underline="yes"} ===
@hover @focus @invalid @disabled @checked 是内建的,
直接翻成 :hover / :focus-visible / :invalid / :disabled /
:checked,CSS 的直觉照搬即可。后三个是控件自己的状态,而本 profile 不定义一个控件
什么时候处在其中——合不合法是 handler 的判断,样式表只说它长什么样。真正多出来的是具名状态:
$tab=code 是一个显式的状态格,有初值、有喂它的交互源,不是从 DOM 结构里凑出来的。
数据流被限死成 交互 → 状态 → 视图,状态永不读状态,所以没有环,
诊断目录里也就没有 binding-cycle。CSS 用 checkbox hack 做的事,这里是一等公民。
@media (max-width: 767px) { ... }
@supports (display: grid) { ... }
@layer base, theme;
@keyframes fade { ... }
@font-face { ... }
@container (min-width: 40em) { ... }hide-below="768" # 唯一的响应式 fade-out="1.2" # 唯一的动画 # 层 → 来自 style-entry,不是 at-规则 # 没有 @supports、@container、 # @font-face、@keyframes 的对等物。 # prefers-reduced-motion 的让步 # 写死在宿主静态表里。
响应式只剩一个断点方向(低于 N px 就隐藏),动画只剩一种(画出来自己淡掉)。
两个都是「按第一个真实用例圈死,多一个不加」的产物。
诚实的说法:这不是一个设计立场,这是还没走到。
一旦有人要做两栏在窄屏变一栏(而不是消失),hide-below 就不够了。
/* 每个属性显式声明是否继承。
color 继承,border 不继承。
inherit / initial / unset / revert
四个关键字随时可用。 */
.child { color: inherit }# §10:「继承由宿主决定, # 本 profile 不描述它。」 # box 是平的;继承是宿主那套 # 媒介的行为,不是 profile 的。 发 CSS 的宿主 color="{{fg}}" → 子节点跟着变 border="1px ..." → 不跟 画到 canvas / 排到 PDF 的宿主 → 由它自己说了算,允许不同
视图模型({states, screens, frames, bindings, diagnostics})里 box 是平的,
继承不在其中。换一个不发 CSS 的宿主(画到 canvas、排到 PDF),同一份样式表可以渲染出不同结果。
关键在于这一条现在是被写明的,不是被漏掉的 —— §10 明说"继承由宿主决定,
本 profile 不描述它"。这是把缺口写出来,不是补上它:
钉死继承等于为 38 个词重新裁一遍 CSS 的"继承/不继承",而目前没有消费者需要。
所以它仍然反直觉,但反直觉的理由从"没想清楚"变成了"想清楚了并且选择不管" ——
对一份规范来说,这两者的差别就是全部。
/* 容错是设计目标:
认不出的声明、选择器、at-规则
一律丢弃,继续解析。
页面永远渲染得出来。
代价:错字永远不告诉你。 */# 20 个具名诊断码(13 error / 7 warning) selector-unsupported error ambiguous-rule error unknown-state/screen/frame error frame-cycle / frame-too-deep error screen-nested error unknown-token error style-invalid-value error unmatched-rule warning unused-frame warning unknown-component warning style-unknown-attribute warning style-embed-not-expanded warning
严重性的分界线是明确的:结构性错误是 error(歧义、悬空引用、成环),
未知名字是 warning + 惰性回退。后者是为了保住前向兼容 —— 不认识的名字必须降级,
不能拒收整份文档。geml check 能把这整套跑出来,CSS 侧的对等物(stylelint)
是第三方工具且不是规范的一部分。这一维 geml-style 赢得干净。
写 CSS 的人第一次看到 ambiguous-rule 会觉得是工具在找茬。但这个格式的目标读者是
agent:geml set、geml add --before 天天在重排块,而任何靠源码顺序的仲裁
在那种编辑下都会静默改渲染结果。不猜是正确的。唯一要补的是诊断文案要好到让人知道怎么拆。
slots= 在 CSS 里没有对应物,这一步跨出了「样式」的定义。但实测收益是硬的:
page.geml 里零 chrome 文本,整张 GitHub 页面的 27 个区域全由样式表组装。
代价是名字骗人 —— 建议在 profile 开头就说清「这不是 CSS 的对等物,是版面单 + 样式表」,
别让人带着 CSS 的预期进来。
CSS 的开放属性集是它成为通用语言的原因,也是它没法被静态检查的原因。
38 个词加「换个块还是不是这个意思」这条判据,是个能自圆其说的立场。
但要防住一种滑坡:每来一个页面就加两个词,加到 80 个的时候,「封闭」的好处就没了,
坏处还在。view editable fade-out underline
就是这么来的 —— 各自为某一个页面的某一个需求而生。
后来加的八个是另一回事,判据也因此明确了。
height min-height max-height min-width
font-weight fade-in item-align item-justify
补的都是轴:宽度轴有三个词而高度轴一个没有、字号行距字族齐了独缺字重、
淡出有而淡入无、axis 开了轴 gap 给了间距却没有对齐。
一条轴只铺了一半是不对称,不是克制。所以闸门不是「等第二个用例」——那会拦住一切
前瞻性的补全;闸门是这个词补的是轴,还是补的是某一个情境。
上一轮对照查出的三处缺口(记号、简写、继承)都已写进 profile。 剩下的不是判断失误,是用例还没逼到。
hide-below="768" 是全部:窄于 N px 就隐藏。没有 @container,
没有断点区间,没有「两栏变一栏」——只有「两栏变一栏半」的那个栏消失。
这是「按第一个真实用例圈死,多一个不加」的产物,立场本身站得住。
但它撑不过下一个用例:任何一个要在窄屏重排而不是隐藏的版面,都会立刻要求
hide-below 之外的东西。到那时候要补的不是一个词,是一套断点模型 ——
而断点模型和「摆放」(§06 的 slots)是同一件事的两面,不能各做各的。
profile §0.1 的右列自己列着这批:filter= 从没对着真实噪音跑过
(codemap 那份语料的边全是 kind=call、confidence 全空,没有可过滤的东西),
handler= 没有真实宿主。
这条之所以算问题,不是因为它们可能有 bug,而是因为「封闭词表」这个立场的全部力量 来自每个词都被一个真实用例逼出来。一个没被用例逼出来的词,和 CSS 那种 「先定义、等人来用」的属性没有区别 —— 而 §03 提醒的那种滑坡,正是从这里开始的。 好在 profile 自己把这一栏单独列了出来,写清了「这买到的是什么」。
geml-style 不是「另一种 CSS」,是一份给 agent 编辑的版面单,顺带管样式。 它在选择器、属性、at-规则上是 CSS 的受限子集,读 CSS 的人不会迷路; 在层叠和摆放上是另一套东西,而这两处的偏离都有站得住的理由。 上一轮对照逼出的三处缺口 —— 记号、简写裁决、 继承写明为由宿主决定 —— 都已经进了 profile。 剩下的「不像 CSS」,不是问题,是还没被用例逼到的地方。