格式概览
带有具体示例的 TOON 语法参考。入门介绍请参阅 快速开始。
四种形式
形式是某个值的一种呈现方式。具体采用哪种形式由数据形状及其所在位置决定,你不需要手动选择。下面所有内容都是这四种形式的变体:
| 形式 | 适用于 | 看起来像 |
|---|---|---|
| 内联 | 基本类型数组 | tags[3]: admin,ops,dev |
| 列表 | 不适合内联或表格化形式的数组 | items[2]: 后每个元素一行 - |
| 表格化 | 结构一致的对象数组 | items[2]{sku,qty}: 后每个元素一行 |
| 带键表格化 | 值为结构一致对象的对象 | users[2:]{age,city}: 后每个条目一行 |
“形式”这个词是有意使用的:这些是 TOON 内部的形状,而不是与 JSON 或 YAML 并列的另一种格式。
数据模型
TOON 与 JSON 使用相同的方式对数据建模:
- 基本类型(Primitives):字符串、数字、布尔值和
null - 对象(Objects):字符串键到值的映射
- 数组(Arrays):值的有序序列
根形式
一个 TOON 文档可以表示不同的根形式:
- 根对象(最常见):字段位于顶层(深度为 0),没有父键
- 根数组:以顶层(深度为 0)的
[N]:或[N]{fields}:开头 - 根基本类型:单个基本类型值(字符串、数字、布尔值或 null)
本指南中的大多数示例使用根对象,但该格式同等支持这三种形式(规范第 5 节)。
对象
简单对象
带有基本类型值的对象使用 key: value 语法,每行一个字段:
id: 123
name: Ada
active: true缩进取代了 JSON 式的大括号。冒号后跟一个空格。
嵌套对象
嵌套对象增加一级缩进(默认:2 个空格):
user:
id: 123
name: Ada当一个键以 : 结尾且同一行没有值时,它就表示接下来是一个嵌套对象。下一缩进层级的所有行都属于该对象。
空对象
根级别的空对象会产生一个空文档(没有任何行)。嵌套的空对象则表示为单独的 key:,没有子内容。
带键的表格化对象
当一个对象至少包含两个条目,且这些条目的值都是结构一致的对象(拥有相同键,值为基本类型或嵌套的一致对象)时,它会折叠为带键的表格形式:共享字段结构只在首部中出现一次,每个条目成为一行,并保留自身的键:
users[2:]{age,city}:
alice: 30,Berlin
bob: 25,Oslo长度后紧跟的冒号([2:])标记这是带键首部,[N] 声明条目数量。每个条目行都是 entrykey: cell,cell,…:条目键后接该条目值按字段顺序展开后的叶子值。
当根对象本身符合条件时,会省略外层键:
[2:]{age,city}:
alice: 30,Berlin
bob: 25,Oslo不满足条件的对象会保持嵌套形式不变:单条目对象、值的形状混杂或包含基本类型/数组/空对象的对象都不会折叠。实际使用中,这会让大多数配置风格的映射保持原样(规范 §9.5)。
数组
TOON 会检测数组结构,并选择最高效的表示方式。数组总是在方括号中声明其长度:[N]。
基本类型数组(内联形式)
基本类型(字符串、数字、布尔值、null)的数组以内联形式呈现:
tags[3]: admin,ops,dev值之间用分隔符(默认为逗号)隔开。包含当前生效分隔符的字符串必须加引号。
对象数组(表格化形式)
当数组中的所有对象共享同一组基本类型值的键时,TOON 会使用表格化形式:
items[2]{sku,qty,price}:
A1,2,9.99
B2,1,14.5users[2]{id,name,role}:
1,Ada Lovelace,admin
2,"Smith, Bob",user首部 items[2]{sku,qty,price}: 声明了:
- 数组长度:
[2]表示 2 行 - 字段名:
{sku,qty,price}定义了各列 - 生效分隔符:逗号(默认)
每一行的值都按字段列表的顺序排列。值被编码为基本类型(字符串、数字、布尔值、null),并以分隔符隔开。
NOTE
表格化形式需要满足:所有对象的字段集合完全相同(键相同,但每个对象内的顺序可以不同);每个对象至少包含一个键;并且每一列要么是基本类型值,要么是结构一致的嵌套对象(见下文)。如果数组中包含空对象 {},或某一列的值类型混杂,则会回退到列表形式。
嵌套字段组
当某一列的值都是结构一致的子对象(每个元素中键相同,并且递归地只包含基本类型或结构一致的嵌套对象)时,该列会在首部中折叠为嵌套字段组,而行数据仍然保持扁平:
orders[2]{id,customer{name,country},total}:
1,Ada,DK,99
2,Bob,UK,149首部中的 customer{name,country} 声明了一个嵌套对象列;每一行的单元格按字段列表的深度优先顺序排列,因此第一笔订单中的 Ada,DK 会填入 customer.name 和 customer.country。嵌套深度不受限制(规范 §9.3)。
混合与非一致数组(列表形式)
不满足表格化条件的数组会使用带连字符标记的列表形式:
items[3]:
- 1
- a: 1
- text每个元素以 - 开头,缩进比父级数组首部深一级。
作为列表项的对象
当数组元素是对象时,它会以列表项的形式出现:
items[2]:
- id: 1
name: First
- id: 2
name: Second
extra: true当一个表格化数组是列表项对象的第一个字段时,表格化首部会出现在连字符所在的那一行;此时行数据缩进两级,其他字段缩进一级:
items[1]:
- users[2]{id,name}:
1,Ada
2,Bob
status: active当对象只有一个表格化字段时,同样的模式也适用:
items[1]:
- users[2]{id,name}:
1,Ada
2,Bob这是“第一个字段为表格化数组”的列表项对象的规范编码方式。
数组的数组(列表形式)
当数组的内部元素是基本类型数组时:
pairs[2]:
- [2]: 1,2
- [2]: 3,4每个内部数组的首部都出现在它所在的列表项那一行。
当内部数组本身是对象数组或非一致数组时,同样的 - [N]: 首部会出现在连字符所在行,嵌套项紧随其后并再深一级缩进:
items[3]:
- summary
- id: 1
name: Ada
- [2]:
- id: 2
- status: draft空数组
字段中的空数组渲染为 key: [],根级别的空数组则渲染为 []:
items: []出于向后兼容的考虑,旧式的 items[0]: 形式在解码时仍会被支持。
数组首部
首部语法
数组首部遵循以下模式:
key[N<delimiter?>]<{fields}>:其中:
- N 是非负整数长度
- delimiter(可选)显式声明生效的分隔符:
- 不填 → 英文逗号分隔
\t(制表符)→ 制表符分隔|→ 竖线分隔
- fields(可选,用于表格化数组):
{field1,field2,field3}
NOTE
数组长度 [N] 有助于大语言模型校验结构。如果你要求模型生成 TOON 输出,显式的长度声明能让你检测出内容是否被截断或出现格式错误。
分隔符选项
TOON 支持三种分隔符:逗号(默认)、制表符和竖线。分隔符的作用范围仅限于声明它的那个数组首部。
items[2]{sku,name,qty,price}:
A1,Widget,2,9.99
B2,Gadget,1,14.5items[2 ]{sku name qty price}:
A1 Widget 2 9.99
B2 Gadget 1 14.5items[2|]{sku|name|qty|price}:
A1|Widget|2|9.99
B2|Gadget|1|14.5制表符和竖线分隔符会显式编码在首部的方括号和字段大括号中。在某个数组的作用范围内,只有生效的分隔符会触发引号处理——其他分隔符字符则作为字面数据处理。对象字段的值(key: value)遵循文档级分隔符(§11.1),而不受周围任何数组生效分隔符的影响。
TIP
制表符分隔通常比逗号分隔更高效地进行 token 化,尤其是在引号字符串较少的数据中。使用 encode(data, { delimiter: '\t' }) 可以进一步节省 token。
注释
解码器会在词法预处理阶段剥离所有第一个非空格字符为 # 的行,这一步发生在其他任何处理之前:
# Server configuration
host: example.com
port: 8080注释只支持整行形式:一行中其他位置出现的 # 都是普通内容。注释也只在解码侧生效:编码器永远不会输出注释,并且以 # 开头的字符串值总会被加引号,因此编码器输出中不会出现可被解释为注释的行。表格行之间的注释不会终止该表格结构(规范 §5.1)。
引号与类型
字符串何时需要加引号
TOON 只在必要时才为字符串加引号,以最大化 token 效率。以下情况字符串必须加引号:
- 空字符串(
"") - 开头或结尾是空格字符
- 值等于
true、false或null(区分大小写) - 看起来像数字(例如
"42"、"-3.14"、"1e-6"、"05"、"+1") - 包含特殊字符:
:、"、\、[、]、{、},或任何控制字符(U+0000–U+001F,包括换行符/制表符/回车符) - 包含相关的分隔符(数组作用范围内为生效分隔符,其他情况下为文档级分隔符)
- 值等于
"-",或以"-"开头且后面还有其他字符 - 值等于
"#",或以"#"开头(该行会被视为注释)
除上述情况,字符串都可以不加引号。Unicode、emoji 以及内部(非开头/结尾)带空格的字符串都可以安全地不加引号:
message: Hello 世界 👋
note: This has inner spaces转义序列
带引号的字符串中允许使用六种转义字符:
| 字符 | 转义 |
|---|---|
反斜杠(\) | \\ |
双引号(") | \" |
| 换行符(U+000A) | \n |
| 回车符(U+000D) | \r |
| 制表符(U+0009) | \t |
| 其他任意 U+0000–U+001F 控制字符 | \uXXXX |
其他转义方式(例如 \x、\0、\b)一律会被拒绝,未配对的 UTF-16 代理码位 \uXXXX(U+D800–U+DFFF)同样会被拒绝。
类型转换
§2 特例范围内的数值以规范的十进制形式输出;超出该范围时可使用指数记法。非 JSON 类型(NaN、Infinity、BigInt、Date、Set、Map、undefined 等)在编码前会被规范化——完整的映射关系请参阅 API 参考——类型规范化。
解码器在输入时同时接受十进制和指数形式(例如 42、-3.14、1e-6),并将带有非法前导零的 token(例如 "05")视为字符串而非数字。
使用 toJSON 进行自定义序列化
拥有 toJSON() 方法的对象在序列化时会先调用该方法,再对其结果进行归一化后编码,这与 JSON.stringify 的行为类似:
const obj = {
data: 'example',
toJSON() {
return { info: this.data }
}
}
encode(obj)
// info: exampletoJSON() 方法:
- 优先于内置的归一化处理(Date、Array、Set、Map)
- 其结果会被递归地归一化
- 只要对象的原型链中存在
toJSON,就会被调用
关于引号、转义、类型转换以及严格模式解码的完整规则,请参阅 规范 §2–4(数据模型)、§7(字符串与键)以及 §14(严格模式)。