GEML 块类型图解 · 第 3 页 / 5 · 规范 1.0

GEML 块图解 · table 与 view

table 是规范里规则最多的类型:两种体、一个模型、一门封闭的算术、一行汇总。GEP-0012 提出的 view 把「从另一张表选、算、聚合」搬出来,并打算把「借别的表的行」从 table 里拿走。这一页先把 table 今天的每条规则实测一遍,再把 view 的草案画出来,标清哪些是规范、哪些是草案、哪里两者冲突。

看板

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

规范已定 规范正文写死的。GEP 草案 提案定义,规范未收。实测 规范不规定、参考实现今天这样做。草案缺口 草案该说而没说的。实现偏差本页未发现。

规范已定 16 GEP 草案 1 实测 1
规则出处状态
table
1两种可互换的体、一个模型:可视管道表(默认),或 format=csv/tsv 数据体加 header=。模型是列模型,单元格值是字符串、数字或布尔,日期是 ISO-8601 文本。§6规范已定
2delim= 只对数据体有效,必须恰一个字符;tab 写 format=tsv。两字符是 bad-table-delimiter error 并回退自然分隔符;无 format 时是 ignored-table-delimiter warning。未知 format warning,按可视网格解析。§6 · A.3规范已定
3table 的 src= 只有一种目标:csv/tsv 数据文件(相对路径或 http(s))。解析不到 error;src 与正文同在 error;src= 指向块(#id 或 doc.geml#id)是 error 并指名 view(§6.1)——块的输出是别的块派生出来的东西。下面那组三种目标属于 view。只有 src 文本进 .gemlhistory 哈希,内容不进。§6 · A.3规范已定
4compute=(是 view 的属性,§6.1——写在 table 上只是 unknown-attribute warning,什么也不算):Name = expr 用 ; 分隔,算术 + - * / ( ) 和一元负;按表头名(含空格用单引号)或字母列引用;可引用前面算出的列,无环。空或非数单元格按 0 并 compute-non-numeric-cell warning;除零得无值、显示 - 并 compute-not-a-number warning。计算列追加在数据列后,不写进正文。§6 · A.3规范已定
5summary=(是 view 的属性,§6.1——写在 table 上同样只是 warning):一行汇总,Cell = value;值是字面量标签或聚合表达式,聚合只有 sum avg min max count,是唯一跨行的构造;列引用必须被聚合;未指定列空白;未知列 error。§6 · A.3规范已定
6[printf] 显示格式绑定在公式左侧名字上,只影响显示;判定是左侧最后一个含 % 的 […],所以列名本身可以带方括号。§6规范已定
7明确排除:单元格与区间寻址、相对行引用、条件、跨表 remote()、lookup、任何嵌入程序。表是文档特性,不是电子表格引擎。§6规范已定
8外部数据渲染时到达,所以 compute 和图表引用的列名在渲染时校验,不在构建时。§6 · §9.4规范已定
9渲染:figure.table-figure > table.geml-table,计算列 td.computed,数字单元格带 data-sort,汇总行在 tfoot,caption 在 figcaption。--to html实测
10坐标:#fy[2] 一行(1 起始,表头不算行)、#fy[2]["Q1"] 一格、#fy["Q1"] 一列、#fy[summary]["FY"] 汇总格。整数是行、引号是列、裸词是保留行名。list 不枚举坐标。GEP-0011GEP 草案
11借表:src= 指向块的 table 就是 error,并指名 view——table source `#fy` names another block's output; use `view`。块目标是别人的输出,借它的 table 就不再是「事实」。落地之前曾有过短命的 table {src=#fy compute=…} 写法;现在写它会同时得到那条 error 和 compute 的 unknown-attribute warning。§6 · §6.1规范已定
view(§6.1——已注册,所以不会有 unknown-block-type warning;GEP-0012 自己的 front matter 仍写 state: draft,但规范已经收录)
12view 无正文,必须 src=,目标同 §6 三种;正文与 src 同在 error。§6.1规范已定
13选择:where=(比较、and/or/not、数字或单引号字面量)、order=(asc/desc,稳定排序)、limit=、select=(只取名字,含 = 报错指向 compute)。§6.1规范已定
14派生与聚合:compute=/summary= 语法不变,搬到 view;by= 分组键,aggregate="Name = fn(col)";by= 无 aggregate 即 distinct。§6.1规范已定
15求值顺序是 SQL 的逻辑顺序:src → compute 行级 → where → compute 聚合 → by/aggregate → order → limit → select → summary。compute 两遍所以 where 能用刚算的列;select 晚跑所以 order 能用未显示的列;summary 最后,是对显示行的报告。§6.1规范已定
16一种表达式语言:单引号在列位置是列名、在值位置是字面量。where= 里未知列 error;全无数值的列比数字 error;单个非数单元格不匹配、无诊断。没有正则、like、函数。§6.1规范已定
17view 看到源的元组和计算列,看不到源的 summary 行;同名 compute= 遮蔽源列合法但 shadowed-source-column warning。过滤分组是再来一个 view,不是 HAVING。§6.1规范已定
18图表可以读 view:§7 已写明 data=「may name a view as readily as a table, and for a derived column it must」——画 FY 就是 data=#fy-report y=FY,指向基表则得到 chart: column `FY` not found in table。rows=all|summary 对 view 自己的 summary 行生效;view 上没有 summary= 时,rows=summary 是 error、rows=all 给 warning 并用数据行。(GEP-0012 正文未提图表时,这条曾记为草案缺口。)§7.1 · §6.1规范已定
两种体

可视表与数据体,算出同一个模型

左边一张管道表、一张 csv 表,以及在它之上做 compute 与 summary 的 view;右边是 --to html 实际吐出的结构。

管道表 · csv 体 · 计算列 · printf · 汇总行 规范已定属性对象用 \ 续行(§4);反斜杠是必需的,少一个整块就成了段落。
GEML · t1-table.geml
=== table {#budget caption="Annual cost"}
| Plan  | Months | Rate |
|-------|-------:|-----:|
| Basic |      1 |   30 |
| Pro   |      2 |   30 |
===

=== table {#fy caption="FY revenue ($M)" format=csv header=1 delim=;}
Segment ; Q1    ; Q2
Cloud   ; 124.5 ; 131.2
Hardware; 88.1  ; 84.6
Services; 45.2  ; 47.8
===
=== view {#fy-report src=#fy \
          compute="FY [%.1f] = Q1 + Q2; Share [%.1f%%] = FY * 100 / 500" \
          summary="Segment = 'Total'; Q1 = sum(Q1); Q2 = sum(Q2); FY = sum(FY)"}
===                                    %% 表装事实;view 负责派生与汇总
See [[#fy]] and [[#budget]].
--to htmlok: no diagnostics
figure.table-figure #budget › table.geml-table
PlanMonthsRate
Basic130
Pro230
figcaption · Annual cost。右对齐来自分隔行的 -----:;数字格带 data-sort。 figure.table-figure #fy-report › 这是 view 的输出,不是表的
SegmentQ1Q2FYShare
Cloud124.5131.2255.751.1%
Hardware88.184.6172.734.5%
Services45.247.893.018.6%
Total257.8263.6521.4 
蓝底是 td.computed:不在正文里,模型算出来的。汇总行在 tfoot,未指定的 Share 列空白。[[#fy]] 的链接文字是 caption「FY revenue ($M)」。
printf
FY [%.1f]:格式是左侧最后一个含 % 的方括号组,所以 93 显示成 93.0 而模型里 data-sort="93" 还是 93。%% 是字面百分号。没有日期格式,日期就是 ISO 文本。
算术
§6 的表达式只有 + - * / ( ) 和一元负,*// 优先,左结合。公式可引用前面算出的列(Share 用了 FY),必须无环。这是 §9.1「无表达式语言」允许的那点封闭算术。
汇总
聚合是唯一跨行的构造,且只有五个。汇总表达式里的列引用必须被聚合包住,裸列名在汇总行没有值。
坐标

行、格、列、汇总格各有地址

GEP-0011,本分支已实现,规范正文未收。三种记号不会混:整数是行,引号是列,裸词是保留行名。

对上面 #fy 的六次读取 GEP 草案
geml get
$ geml get t1-table.geml '#fy[2]'          第 2 个体行,表头不算
Hardware; 88.1; 84.6; 172.7; 34.5%
$ geml get t1-table.geml '#fy[2]["Q1"]'
88.1
$ geml get t1-table.geml '#fy["Q1"]'       一列,不含表头
124.5
88.1
45.2
$ geml get t1-table.geml '#fy[summary]["FY"]'
521.4
$ geml get t1-table.geml '#fy[summary]'
Total; 257.8; 263.6; 521.4;
$ geml get t1-table.geml '#borrowed[1]["Half"]'   借表自己算的列
127.85
规则
写法指什么
#fy[2]一个体行,1 起始。表头不是行,模型里 rows 本来就不含表头
#fy[2]["Q1"]一格,列按表头名
#fy["Q1"]一列,行序,不含表头
#fy[summary]["FY"]汇总行里的一格;[summary] 是唯一保留行名,插行不会移动它
A.geml#fy[summary]["FY"]跨文档同理;![[…]] 可以把这一个叶值投影进句子
无表头的表用字母列 A、B:和 compute= 读的是同一个列命名空间。geml list 不枚举坐标,四百行的表会把清单淹掉;坐标由作者构造。
读写
读先落地。写只在源文件里有对应字节时允许:计算列、src= 喂进来的表拒写;format=csv 格内含分隔符时也拒写,因为改一个字节会波及邻格。
为什么不混
[ 不能出现在合法 id 里(§4 的 NAME 只有字母、数字、-、_),所以坐标永远不会被读成 id,已有文档含义不变。
src=

数据从别处来,以及「借表」的去向

table 的 src= 收文件。借另一个块的关系是 view 的活——§6 把这个目标从 table 手里收走了,并指名了替代者。

文件、本文档的表、借来再算 规范已定 0012 要改
GEML · t1-table.geml 节选
=== table {#ext caption="From a file" format=csv src=rows.csv}
===
=== view {#borrowed caption="Borrowed rows" src=#fy-report compute="Half = FY / 2"}
===

%% rows.csv 在文档同目录:Segment,Q1,Q2 / Cloud,124.5,131.2 / Hardware,88.1,84.6
%% #borrowed 拿到 #fy-report 的元组和它算出的 FY,再算自己的 Half
同一件事写成 table1 error
=== table {#borrowed src=#fy-report compute="Half = FY / 2"}
===
%% error: table source `#fy-report` names another block's output; use `view`
%% 另有:warning: unknown attribute `compute` for block type `table`
今天的行为ok: no diagnostics
#ext · 文件内容装入,列名渲染时才校验
SegmentQ1Q2
Cloud124.5131.2
Hardware88.184.6
#borrowed · 源的计算列 FY 可见,自己再加 Half
SegmentQ1Q2FYHalf
Cloud124.5131.2255.7127.85
Hardware88.184.6172.786.35
Services45.247.893.046.50
源的 summary 行没有跟过来:借的是关系,不是报告。
谁收哪种目标
§6:table 的 src= 只收数据文件(csv/tsv,相对路径或 http(s));#id 与 doc.geml#id 这两种块目标归 view(§6.1),table 指它们是 error 并指名 view。解析不到 error,src 与正文同在 error。
哈希
只有 src 这串文字进 .gemlhistory 的哈希,取回的内容不进。外部数据在渲染时到达,所以 compute 和图表引用的列名推迟到渲染时校验。
谁来借
§6 按「src 指向什么」切分:指文件的留在 table,指块的归 view,指块的 table 是 error 并指名 view。理由写在规范里:块的输出是别的块派生出来的,而一块「事实」不该由别人派生。落地之前曾短暂存在过 table 自带 compute 的借表写法,现已没有——本页此前记的就是那个写法。
失败

十二种不通过的情形,一次跑完

八条 error 四条 warning,与 A.3 的诊断表逐条对上。边界在「文档说错了话」还是「数据不干净」。

t2-table-fail.geml 规范已定
GEML
=== table {#a format=csv src=rows.csv}
x,y                                   ← src 与正文同在
===
=== table {#b format=csv delim=;;}       ← 两个字符
a;b
===
=== table {#c delim=;}                  %% 没有 format,可视网格用不上 delim
| a | b |
===
=== table {#d format=weird}
| a | b |
===
%% compute=/summary= 是 view 的属性(§6.1),所以下面每个公式用例都是 view
=== view {#e src=#src compute="Z = Q9 + 1"}
===
=== table {#fsrc format=csv}
Q1,Q2
1,n/a                                %% 非数按 0,warning
===
=== view {#f src=#fsrc compute="S = Q1 + Q2"}
===
=== table {#gsrc format=csv}
Q1,Q2
1,0                                  %% 除零
===
=== view {#g src=#gsrc compute="R = Q1 / Q2"
          summary="Q1 = sum(Q1); Nope = 1; Q2 = Q2"}
===
=== table {#h src=#n}                   ← table 的 src= 不能指块
===
=== table {#i format=csv src=missing.csv}
===
=== view {#j src=#src compute="bad formula"}
===
geml check8 error 4 warning
error: table has both `src` and an inline body; provide one, not both (line 8)
error: `delim=";;"` must be exactly one character (for a tab use
  `format=tsv`); split on `,` instead (line 11)
warning: `delim` applies to a data body (`format=csv`/`tsv`); this body
  was parsed as a visual grid, so it is ignored (line 15)
warning: unknown table format `weird`; parsed as visual grid (line 20)
error: compute `Z`: unknown column `Q9` (line 25)
warning: column `Q2` row 1 is not a number (`n/a`); counted as 0 (line 29)
warning: compute `R` row 1: division by zero; the cell holds no value
  and shows `-` (line 33)
error: summary targets unknown column `Nope` (line 33)
error: summary `Q2`: column `Q2` must be reduced by an aggregate
  (e.g. sum(Q2)) (line 33)
error: bad compute formula `bad formula` (want `Name = expr`) (line 41)
error: cannot resolve table source `missing.csv` (line 39)
error: table source `#n` names another block's output; use `view`
  (line 37)
error
src 与正文同在、分隔符不是一个字符、未知列、汇总未知列、汇总裸列、公式不成形、源解析不到,以及 table 的 src= 指了块——最后这条现在由「指名 view」来回答,而不是「不是表」,所以指 note 和指真表都会触发。都是文档自己写错。
warning
delim 用错地方、未知 format 退回可视网格、非数单元格按 0、除零。前两个文档仍能读,后两个是数据的事实而不是文档的缺陷。§6 特别写明除零「是 warning,文档仍合规,但绝不静默」。
回退
坏 delim 回退到自然分隔符 ,,表照样读;坏 format 按管道表解析。降级永远保留内容。
view

view 草案:从另一个关系里选、算、聚合

已由 §6.1 规定、并在解析器里注册,所以 view 会被当作它自己解析,不会得到 unknown-block-type warning。(GEP-0012 自己的 front matter 仍写 state: draft;规范还是收了它。)下面左边是草案语法,右边是草案打算渲染成的样子和今天的实际输出。

两个 view:过滤排序取列,和分组聚合 GEP 草案
GEML · v1-view.geml
=== table {#tickets format=csv}
Area,Status,Days
Draft Mode,open,3
Turbopack,open,12
Draft Mode,closed,1
===
=== view {#open src=#tickets where="Status = 'open'" order="Days desc"
          limit=10 select="Area, Days"}
===
=== view {#by-area src=#tickets by="Area" aggregate="Open = count(Status)"}
===
今天的输出
warning: unknown block type `view`; body kept as raw (line 11)
warning: unknown block type `view`; body kept as raw (line 13)
0 error(s), 2 warning(s)   exit 0 —— 草案文档今天能过检查,只是什么都不算
草案打算渲染成未实现
#open · where → order → limit → select
AreaDays
Turbopack12
Draft Mode3
#by-area · by → aggregate
AreaOpen
Draft Mode2
Turbopack1
求值顺序,草案的承重句:
src→compute 行级→where→compute 聚合→by / aggregate→order→limit→select→summary
为什么是块
草案的判据:一个 view 的输出是另一个关系,可以再被下一个 view 消费(过滤分组就是「再来一个 view」,不是 HAVING)。放在 table 的属性上做不到这一点,也会让 table 不再是「事实」。
两遍 compute
只读本行的公式在 where 之前算,所以 where="FY > 40" 能用同块定义的 FY;读聚合的公式在 where 之后算,否则过滤依赖被过滤的结果就循环了。收益是 sum(FY) 在 compute 和 summary 里指同一批行。这是有意偏离 SQL 的地方。
表达式
不是第二种语言:在 §6 的算术上加比较、and/or/not、数字和单引号字面量。单引号在列位置是列名('Unit Price'),在值位置是字面量('open'),没有第三个位置,因为 §4 的属性值没有转义可以引入第二种引号。
看得见什么
源的元组加计算列;源的 summary 行不过来。要源的总数就寻址它:A.geml#fy[summary]["FY"]。同名 compute 遮蔽合法但 warning。
依据

探针文件与实测输出

全部用仓库内当前构建 node geml-parser/dist/geml.js 跑出。rows.csv 与探针同目录。

探针覆盖check 结果页面里对应
t1-table.geml · rows.csv管道表、csv 体、delim、compute、printf、summary、src 文件、借表、caption 自动文字、六个坐标0 诊断两种体、坐标、src=
t2-table-fail.gemlsrc+正文、坏 delim、delim 无 format、未知 format、未知列、非数、除零、汇总未知列、汇总裸列、坏公式、源解析不到、源非表8 error 4 warning十二种失败
v1-view.geml两个 view 块(草案语法)2 warning unknown-block-typeview 草案