table 是规范里规则最多的类型:两种体、一个模型、一门封闭的算术、一行汇总。GEP-0012 提出的 view 把「从另一张表选、算、聚合」搬出来,并打算把「借别的表的行」从 table 里拿走。这一页先把 table 今天的每条规则实测一遍,再把 view 的草案画出来,标清哪些是规范、哪些是草案、哪里两者冲突。
规范已定 规范正文写死的。GEP 草案 提案定义,规范未收。实测 规范不规定、参考实现今天这样做。草案缺口 草案该说而没说的。实现偏差本页未发现。
| 规则 | 出处 | 状态 | |
|---|---|---|---|
| table | |||
| 1 | 两种可互换的体、一个模型:可视管道表(默认),或 format=csv/tsv 数据体加 header=。模型是列模型,单元格值是字符串、数字或布尔,日期是 ISO-8601 文本。 | §6 | 规范已定 |
| 2 | delim= 只对数据体有效,必须恰一个字符;tab 写 format=tsv。两字符是 bad-table-delimiter error 并回退自然分隔符;无 format 时是 ignored-table-delimiter warning。未知 format warning,按可视网格解析。 | §6 · A.3 | 规范已定 |
| 3 | table 的 src= 只有一种目标:csv/tsv 数据文件(相对路径或 http(s))。解析不到 error;src 与正文同在 error;src= 指向块(#id 或 doc.geml#id)是 error 并指名 view(§6.1)——块的输出是别的块派生出来的东西。下面那组三种目标属于 view。只有 src 文本进 .gemlhistory 哈希,内容不进。 | §6 · A.3 | 规范已定 |
| 4 | compute=(是 view 的属性,§6.1——写在 table 上只是 unknown-attribute warning,什么也不算):Name = expr 用 ; 分隔,算术 + - * / ( ) 和一元负;按表头名(含空格用单引号)或字母列引用;可引用前面算出的列,无环。空或非数单元格按 0 并 compute-non-numeric-cell warning;除零得无值、显示 - 并 compute-not-a-number warning。计算列追加在数据列后,不写进正文。 | §6 · A.3 | 规范已定 |
| 5 | summary=(是 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-0011 | GEP 草案 |
| 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,但规范已经收录) | |||
| 12 | view 无正文,必须 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 | 规范已定 |
| 17 | view 看到源的元组和计算列,看不到源的 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 实际吐出的结构。
\ 续行(§4);反斜杠是必需的,少一个整块就成了段落。=== 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]].
| Plan | Months | Rate |
|---|---|---|
| Basic | 1 | 30 |
| Pro | 2 | 30 |
-----:;数字格带 data-sort。
figure.table-figure #fy-report › 这是 view 的输出,不是表的
| Segment | Q1 | Q2 | FY | Share |
|---|---|---|---|---|
| Cloud | 124.5 | 131.2 | 255.7 | 51.1% |
| Hardware | 88.1 | 84.6 | 172.7 | 34.5% |
| Services | 45.2 | 47.8 | 93.0 | 18.6% |
| Total | 257.8 | 263.6 | 521.4 |
td.computed:不在正文里,模型算出来的。汇总行在 tfoot,未指定的 Share 列空白。[[#fy]] 的链接文字是 caption「FY revenue ($M)」。
FY [%.1f]:格式是左侧最后一个含 % 的方括号组,所以 93 显示成 93.0 而模型里 data-sort="93" 还是 93。%% 是字面百分号。没有日期格式,日期就是 ISO 文本。+ - * / ( ) 和一元负,*// 优先,左结合。公式可引用前面算出的列(Share 用了 FY),必须无环。这是 §9.1「无表达式语言」允许的那点封闭算术。GEP-0011,本分支已实现,规范正文未收。三种记号不会混:整数是行,引号是列,裸词是保留行名。
#fy 的六次读取 GEP 草案$ 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,已有文档含义不变。table 的 src= 收文件。借另一个块的关系是 view 的活——§6 把这个目标从 table 手里收走了,并指名了替代者。
=== 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
=== 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`
| Segment | Q1 | Q2 |
|---|---|---|
| Cloud | 124.5 | 131.2 |
| Hardware | 88.1 | 84.6 |
| Segment | Q1 | Q2 | FY | Half |
|---|---|---|---|---|
| Cloud | 124.5 | 131.2 | 255.7 | 127.85 |
| Hardware | 88.1 | 84.6 | 172.7 | 86.35 |
| Services | 45.2 | 47.8 | 93.0 | 46.50 |
table 的 src= 只收数据文件(csv/tsv,相对路径或 http(s));#id 与 doc.geml#id 这两种块目标归 view(§6.1),table 指它们是 error 并指名 view。解析不到 error,src 与正文同在 error。src 这串文字进 .gemlhistory 的哈希,取回的内容不进。外部数据在渲染时到达,所以 compute 和图表引用的列名推迟到渲染时校验。table,指块的归 view,指块的 table 是 error 并指名 view。理由写在规范里:块的输出是别的块派生出来的,而一块「事实」不该由别人派生。落地之前曾短暂存在过 table 自带 compute 的借表写法,现已没有——本页此前记的就是那个写法。八条 error 四条 warning,与 A.3 的诊断表逐条对上。边界在「文档说错了话」还是「数据不干净」。
=== 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"} ===
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)
table 的 src= 指了块——最后这条现在由「指名 view」来回答,而不是「不是表」,所以指 note 和指真表都会触发。都是文档自己写错。,,表照样读;坏 format 按管道表解析。降级永远保留内容。view 草案:从另一个关系里选、算、聚合已由 §6.1 规定、并在解析器里注册,所以 view 会被当作它自己解析,不会得到 unknown-block-type warning。(GEP-0012 自己的 front matter 仍写 state: draft;规范还是收了它。)下面左边是草案语法,右边是草案打算渲染成的样子和今天的实际输出。
=== 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 —— 草案文档今天能过检查,只是什么都不算
| Area | Days |
|---|---|
| Turbopack | 12 |
| Draft Mode | 3 |
| Area | Open |
|---|---|
| Draft Mode | 2 |
| Turbopack | 1 |
where="FY > 40" 能用同块定义的 FY;读聚合的公式在 where 之后算,否则过滤依赖被过滤的结果就循环了。收益是 sum(FY) 在 compute 和 summary 里指同一批行。这是有意偏离 SQL 的地方。and/or/not、数字和单引号字面量。单引号在列位置是列名('Unit Price'),在值位置是字面量('open'),没有第三个位置,因为 §4 的属性值没有转义可以引入第二种引号。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.geml | src+正文、坏 delim、delim 无 format、未知 format、未知列、非数、除零、汇总未知列、汇总裸列、坏公式、源解析不到、源非表 | 8 error 4 warning | 十二种失败 |
| v1-view.geml | 两个 view 块(草案语法) | 2 warning unknown-block-type | view 草案 |