geml-style/v1 · 逐维对照 · 2026-09-13 · English · 全部图解 · GEML 首页

geml-style对照CSS

十个维度,左边 CSS、右边 geml-style,例子都从 examples/style-demo 那份真跑起来的样式表里取。结论先说:八个维度上它是 CSS 的一个受限子集, 读 CSS 的人不会被绊住;剩下两个是 CSS 根本没有的东西(样式表决定内容摆在哪、 歧义直接报错),这两处一定会违反常识,而且该违反。 这次对照本身查出三处是缺口而不是立场(记号、简写、继承),三处都已写进 profile。

读数

全部是当前分支上的实测,不是估计。样式表 = examples/style-demo/_index/github.style.geml,一份 1:1 复刻 GitHub blob 页的真实用例。

样式表块数
1041 screen · 27 frame · 6 state · 70 rule
内含词
38封闭清单,CSS 属性数百个
诊断码
2013 个 error · 7 个 warning
选择器步数
40 / 47一步 / 两步,没有第三步
记号
13meta 里 13 个键喂 75 处 {{…}};裸色值只剩 8 处
组合器
1只有后代(空格)

十个维度

标签的含义:近似 = 换个写法而已,CSS 的直觉直接适用;有意不同 = 故意做成另一套,理由成立;反直觉 = 写 CSS 的人到这里会先愣一下。

01

样式表是什么

有意不同
CSS
/* 另一门语言,另一个文件,
   另一套语法和解析器 */
.sidebar {
  width: 296px;
}
geml-style
#「样式表」就是一份 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 个块。

02

选择器

反直觉
CSS
#api table.kpi        /* 后代 */
#api > table          /* 子 */
h2 + p                /* 相邻 */
li:nth-child(2n)      /* 位置 */
a:hover               /* 状态 */
[href^="https"]       /* 前缀 */
*                     /* 通配 */
geml-style
#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 是个坡道,不是陷阱」——规范原话。

03

层叠

反直觉
CSS
/* 四级仲裁,永远有赢家 */
!important
> 特异性算术 (1,0,0) vs (0,2,0)
> @layer 次序
> 源码顺序(最后的兜底)

/* 两条同分?后写的赢。
   静默,无提示。 */
geml-style
# 一条关系:条件集严格超集
{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 前替你排个序"—— 后者等于偷偷引入一条优先级规则。

04

属性

有意不同
CSS
/* 数百个属性,开放增长 */
display  position  z-index
transform  transition  opacity
box-shadow  filter  clip-path
grid-template-areas  aspect-ratio
/* 认不出的属性 → 静默丢弃 */
colr: red;   /* 什么都不发生 */
geml-style
# 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。

05

取值

反直觉
CSS
:root { --border: #d1d9e0; }
.a { border-color: var(--border); }
.b { border-color: var(--border); }

width: calc(100% - 2rem);
color: color-mix(in srgb, ...);
geml-style
# 记号:样式表自己的 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 自定义属性的等价物 —— 没有作用域、没有级联覆盖, 它是装载期的文本替换,一份样式表一张表。

06

摆放(CSS 没有的那一维)

反直觉
CSS
/* CSS 从不决定内容出现在哪。
   DOM 的顺序 = HTML 说了算。
   order / grid-area 能重排,
   但不能凭空把一个块搬进
   另一个容器。 */
geml-style
=== style-frame {#header
     axis="row"
     slots="text#logo,
            text#nav link,
            #search-box"}
===
# 槽位决定哪些块进这个区域、
# 按什么顺序。内容文档里没有
# 任何一行说自己属于 header。

这一维 CSS 里根本不存在,所以谈不上「符不符合常识」—— 它更像 XSLT 或模板引擎, 不像样式表。实测的效果是:page.geml 里只有内容,一个字的 chrome 文本都没有, 27 个 frame 把它们组装成 GitHub 的那张页面。 这是整个设计里最值钱的一条,也是最容易被 CSS 出身的人误解的一条 —— 「样式表」这个名字在这里已经不准确了,它是一份版面单。

07

状态

近似
CSS
a:hover { color: blue }
input:checked ~ .panel { display: block }
/* 更复杂的状态 → :has()、
   checkbox hack,或者交给 JS */
geml-style
=== 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 做的事,这里是一等公民。

08

at-规则

有意不同
CSS
@media (max-width: 767px) { ... }
@supports (display: grid) { ... }
@layer base, theme;
@keyframes fade { ... }
@font-face { ... }
@container (min-width: 40em) { ... }
geml-style
hide-below="768"   # 唯一的响应式
fade-out="1.2"     # 唯一的动画
# 层 → 来自 style-entry,不是 at-规则

# 没有 @supports、@container、
# @font-face、@keyframes 的对等物。
# prefers-reduced-motion 的让步
# 写死在宿主静态表里。

响应式只剩一个断点方向(低于 N px 就隐藏),动画只剩一种(画出来自己淡掉)。 两个都是「按第一个真实用例圈死,多一个不加」的产物。 诚实的说法:这不是一个设计立场,这是还没走到。 一旦有人要做两栏在窄屏变一栏(而不是消失),hide-below 就不够了。

09

继承

反直觉
CSS
/* 每个属性显式声明是否继承。
   color 继承,border 不继承。
   inherit / initial / unset / revert
   四个关键字随时可用。 */
.child { color: inherit }
geml-style
# §10:「继承由宿主决定,
#       本 profile 不描述它。」
# box 是平的;继承是宿主那套
# 媒介的行为,不是 profile 的。
发 CSS 的宿主
color="{{fg}}"    → 子节点跟着变
border="1px ..."  → 不跟

画到 canvas / 排到 PDF 的宿主
→ 由它自己说了算,允许不同

视图模型({states, screens, frames, bindings, diagnostics})里 box 是平的, 继承不在其中。换一个不发 CSS 的宿主(画到 canvas、排到 PDF),同一份样式表可以渲染出不同结果。 关键在于这一条现在是被写明的,不是被漏掉的 —— §10 明说"继承由宿主决定, 本 profile 不描述它"。这是把缺口写出来,不是补上它: 钉死继承等于为 38 个词重新裁一遍 CSS 的"继承/不继承",而目前没有消费者需要。 所以它仍然反直觉,但反直觉的理由从"没想清楚"变成了"想清楚了并且选择不管" —— 对一份规范来说,这两者的差别就是全部。

10

出错哲学

有意不同
CSS
/* 容错是设计目标:
   认不出的声明、选择器、at-规则
   一律丢弃,继续解析。
   页面永远渲染得出来。
   代价:错字永远不告诉你。 */
geml-style
# 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 的预期进来。

③ 属性表是封闭的 38 个词

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」,不是问题,是还没被用例逼到的地方。