XTL 语言
本简体中文版本仅供阅读辅助。规范的正典是英文原版,如有规范性解释差异以英文为准。
本文档定义 XTL 0.1 模板语言的表面。这里的写法对模板作者与实现而言均为规范性的。
{{ ... }} 模板块内容的形式语法见 grammar.ebnf —— 这是面向移植者与工具方的非规范性辅助材料。术语定义见 glossary.md。
模板块(Template Blocks)
模板表达式写在双花括号之间:
{{ expression }}
紧贴 {{ 与 }} 内侧的空白无关紧要 —— 解析器会在规范化之前去除首尾空白。下列写法等价:
{{ [name] }}
{{[name]}}
{{ [name] }}
{{
[name]
}}
运算符两侧的空白(例如 {{ [a] + [b] }} 与 {{ [a]+[b] }})同样无关紧要。字符串字面量内部的空白被保留("hello world" 保留其中的空格)。
内部内容为空({{ }} 或仅空白)的模板块属于解析错误,依 ADR-0021(xl3/parser/empty-block)。
模板块由 {{ 开启,并由单元格文本顺序中第一个后续的 }} 关闭。分隔符扫描器不感知字符串字面量:位于 "..." 字面量内部的 }} 仍会关闭模板块(ADR-0051)。如果作者需要在值中出现字面意义的 }},则把它放入 __config__[key],再通过 {{ __config__[key] }} 引用。当表达式正文的 " 个数为奇数(字面量不闭合,几乎总是由内嵌的分隔符造成)时,抛出 xl3/parser/unbalanced-literal。
数据块(Data Blocks)
工作表上的数据块是引擎在渲染源行时展开的矩形。它有两个维度:
- 行范围
[r_start..r_end]—— 连续行的最大区间,其中每一行都包含至少一个数据行单元格(其{{ ... }}正文在聚合函数之外引用至少一个[Column]的单元格)。两个数据行之间出现非数据行(没有[Column]引用)会关闭该块。 - 列范围
[c_start..c_end]—— 该块行范围内所有带任意{{ ... }}表达式的单元格的 bounding box,并向外扩展穿过相邻的非空单元格。紧邻 marker 单元格的原生 Excel 公式({ formula: "..." })或静态值在块内。被完全空列隔开的非空单元格在块外。
矩形内的单元格是 block cells。同一工作表上位于矩形外的单元格是 outside cells(ADR-0066)。
每张工作表一个块(0.x)。 在 XTL 0.x 中,一张工作表最多有一个数据块。如果解析器发现两个或更多断开的 [Column] 数据行单元格 cluster,则在解析时抛出 xl3/expression/bracket-outside-block,并标识第二个 cluster 的起始行。multi-block 支持(通过显式 @block directive 消除边界歧义)延后至未来 ADR。
块展开 语义(渲染侧;规范见 evaluation.md 的 "Render Phases"):
- block cells 会被克隆到展开后的行中 —— 每
(r_end - r_start + 1)个模板行对应一条记录。 - 行号
r < r_start的 outside cells 保持在原位置(块上方的 header / configuration 行)。 - 行号
r >= r_start的 outside cells 即使 splice 为块展开插入行,也保持在原始行r。它们不会下移(NOT)。其公式文本按字面保留。 - 位于内部列且行号
r > r_end的单元格(即 footer-row 场景:与数据相同的列中存在 "Total" 标签和 footer 公式)会下移(N - 1) × (r_end - r_start + 1)行,落在展开后的数据块下方。
这种不对称 —— 块下方的内部列单元格会移动,而同一行上的外部列单元格不会移动 —— 是有意设计的。它支持常见的“侧边汇总表”模式(位于主数据块右侧或左侧、独立于主块的并行报表区域),而不要求作者声明单独工作表。
如果作者希望一个标签/公式对一起移动(例如 A4='footer',B4=LOWER(A4)),则把两个单元格都放在块的列范围内。如果 marker 单元格刚好没有覆盖到 B(例如只有 A2 有 marker),则 B 在块外,不会跟随 A 移动;解决方法是在 B2 添加一个 marker(即便是 {{ "" }} 这样的 trivial literal expression 也可以),或者在未来版本可用时使用显式 @block A:B 形式。
源列(Source Columns)
源列通过 方括号语法引用:
{{ [Customer] }}
{{ [Customer Name] }}
{{ [Units Per Case] }}
[ 与 ] 之间的文本是去除首尾空白后的精确源列名。列名可以(MAY)包含空格、字母、数字以及除 ] 与换行符之外的标点。
像 {{ Customer }} 这样的裸名字在单元格中并不是源列引用。裸名字保留给工作表与文件分组键使用。
按 ADR-0054,模板块内部的裸标识符按以下顺序解析,未能解析则抛出 xl3/expression/unknown-name:
| 上下文 | 解析顺序 |
|---|---|
output_file_pattern | 文件分组键 → __inputs__[name] → __config__[name] |
| 工作表名模式 | 工作表分组键 → __inputs__[name] → __config__[name] |
| 数据单元格 | 所在文件分组键 → 所在工作表分组键 → __inputs__[name] → __config__[name](布尔字面量 TRUE/FALSE 仍会先以字面量形式解析,再进入此链) |
数据单元格中的裸标识符不会解析到源列;作者必须(MUST)使用显式的 [Column] 形式 进行列引用。在数据单元格中保留这条短链解析,是为了当 output_file_pattern 为 {{ [Region] }}.xlsx 的工作表内出现 {{ Region }} 时,能按预期读取当前活动分组的值。
字面量(Literals)
XTL 0.1 支持:
"text"
123
123.45
-123
字符串字面量(依 ADR-0028)
由一对匹配的 " 界定。没有转义序列 —— 反斜杠按字面透传;0.x 版本中没有规范性的方式在字符串字面量内嵌入 "。如果作者需要在某个值中出现 ",则把它放入 __config__ 的作者自定义键中(单元格内容可以是任意字符),然后通过 {{ __config__[key] }} 引 用。
引号不匹配或重复("a"b"、"a 等)的行为属于实现自定义;可移植的模板始终对每个字面量使用恰好一对匹配的引号。
数字字面量(依 ADR-0028)
十进制数字,可选带前导 - 表示取负。允许形状:5、-5、3.14、-3.14、0。Unicode 减号 U+2212 不被识别为符号位(依 ADR-0009 修订)。
XTL 0.x 不支持对非字面量表达式使用一元运算符。 以下全部抛出 xl3/eval/unsupported-syntax:
+5、+[col](一元加)--5、-(0 - 5)(双重取负)-[col]、-(expr)、-__config__[k](对非字面量的一元减)
列取负的替代写法:写作 (0 - [col]) 或 [col] * -1。
运算符(Operators)
算术 —— +、-、*、/
两侧操作数必须(MUST)能强制转换为有限数字。强制转换规则依 ADR-0023:
| 操作数类型 | 强制转换结果 |
|---|---|
| Number(有限) | 自身 |
| Boolean | 1(TRUE)/ 0(FALSE) |
| 空值(依 空值) | 0 |
| 可解析为有限数字的字符串 | 解析后的数字 |
| 不能解析为数字的字符串 | 错误 xl3/eval/operand-coercion |
| Date | 错误 |
| 其他任何类型 | 错误 |
字符串解析依 ADR-0009 与 ADR-0023:先去空白,再 Number(),不得产生 NaN。逗号视为千位分隔符("1,234" 解析为 1234);字面量中不允许科学计数法;不允许前导 +。Unicode 减号 U+2212 不是符号字符(依 ADR-0009 修订)。
依 ADR-0064,字符串→数字的强制转换(与字面量解析不同)接受以下形状:
| 形状 | 是否接受 | 示例 |
|---|---|---|
| 十进制整数 | 是 | "42"、"-42" |
| 十进制小数 | 是 | "3.14"、"-3.14" |
| 千位分隔 | 是 | "1,234"、"-1,234.56" |
| 科学计数法 | 是 | "1e5"、"-1.5e-3"、"1.5E10" |
十六进制前缀 0x/0X | 否 —— 错误 xl3/eval/operand-coercion | "0x10" |
二进制前缀 0b/0B | 否 | "0b101" |
八进制前缀 0o/0O | 否 | "0o17" |
前导 + | 否 | "+5" |
Unicode 减号 U+2212 前缀 | 否 | "−5" |
产生 ±Infinity | 否 | "Infinity"、IEEE 754 溢出 |
| 尾部带非数字字符 | 否 | "5px"、"5 abc" |
| 多行字符串 | 否 | 字符串内部含 LF |
字面量解析(严格)与字符串强制转换(宽松)的非对称性是有意设计的:字面量是作者书写的;而被强制转换的字符串来自数据(CSV 导出、财务系统),科学计数法或十六进制在那里自然出现。
{{ [price] * [quantity] }}
{{ [total] / 10 }}
{{ [a] + [b] }}
{{ [a] - [b] }}
示例:
| 表达式 | 结果 |
|---|---|
1 + 2 | 3 |
"10" + 5 | 15 |
"1,234" + 1 | 1235 |
TRUE + 1 | 2 |
[empty-cell] + 5 | 5 |
"abc" + 5 | 错误 |
除以零会产生 Excel 的 #DIV/0! 错误单元格(依 ADR-0025)。数值单表达式单元格渲染为值为 #DIV/0! 的真实 Excel 错误单元格;在文本格式单元格、混合文本单元格内,或在 & 拼接中,在对应位置代入字符串 "#DIV/0!"。如果该错误值在同一单元格表达式内继续流向其他算术运算符(例如 (1/0) + 5),它无法强制转换为有限数字,按上表抛出 xl3/eval/operand-coercion。
六个源端的 Excel 错误哨兵 —— #N/A、#VALUE!、#REF!、#NAME?、#NUM!、#NULL! —— 在读取源时按 ADR-0017 视为空值。依 ADR-0053,它们在混合文本与 & 拼接的位置上贡献 "",在数字/日期格式的单表达式单元格中则抛出 xl3/cell/numfmt-coercion。#DIV/0! 是引擎自身在 XTL 求值期间唯一会产出的哨兵;它遵循上述规则。如果作者想为源端哨兵显示可见的"缺失"标记,可用 IFEMPTY([col], "missing") 包裹列引用。
字符串拼接 —— &
每个操作数按规范字符串形式(见比较与字符串强制转换)转为字符串,然后依次拼接。永远成功,不会出现类型错误。
{{ [item] & " (" & [size] & ")" }}
比较 —— =、!=、>、<、>=、<=
用于 IF() 与 @filter。遵循比较与字符串强制转换中定义的算法。类型不一致时回退至规范字符串形式的码点序;不会产生强制转换错误。
=
!=
>
<
>=
<=