Skip to content

格式概览

带有具体示例的 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 语法,每行一个字段:

yaml
id: 123
name: Ada
active: true

缩进取代了 JSON 式的大括号。冒号后跟一个空格。

嵌套对象

嵌套对象增加一级缩进(默认:2 个空格):

yaml
user:
  id: 123
  name: Ada

当一个键以 : 结尾且同一行没有值时,它就表示接下来是一个嵌套对象。下一缩进层级的所有行都属于该对象。

空对象

根级别的空对象会产生一个空文档(没有任何行)。嵌套的空对象则表示为单独的 key:,没有子内容。

带键的表格化对象

当一个对象至少包含两个条目,且这些条目的值都是结构一致的对象(拥有相同键,值为基本类型或嵌套的一致对象)时,它会折叠为带键的表格形式:共享字段结构只在首部中出现一次,每个条目成为一行,并保留自身的键:

yaml
users[2:]{age,city}:
  alice: 30,Berlin
  bob: 25,Oslo

长度后紧跟的冒号([2:])标记这是带键首部,[N] 声明条目数量。每个条目行都是 entrykey: cell,cell,…:条目键后接该条目值按字段顺序展开后的叶子值。

当根对象本身符合条件时,会省略外层键:

text
[2:]{age,city}:
  alice: 30,Berlin
  bob: 25,Oslo

不满足条件的对象会保持嵌套形式不变:单条目对象、值的形状混杂或包含基本类型/数组/空对象的对象都不会折叠。实际使用中,这会让大多数配置风格的映射保持原样(规范 §9.5)。

数组

TOON 会检测数组结构,并选择最高效的表示方式。数组总是在方括号中声明其长度:[N]

基本类型数组(内联形式)

基本类型(字符串、数字、布尔值、null)的数组以内联形式呈现:

yaml
tags[3]: admin,ops,dev

值之间用分隔符(默认为逗号)隔开。包含当前生效分隔符的字符串必须加引号。

对象数组(表格化形式)

当数组中的所有对象共享同一组基本类型值的键时,TOON 会使用表格化形式:

yaml
items[2]{sku,qty,price}:
  A1,2,9.99
  B2,1,14.5
yaml
users[2]{id,name,role}:
  1,Ada Lovelace,admin
  2,"Smith, Bob",user

首部 items[2]{sku,qty,price}: 声明了:

  • 数组长度[2] 表示 2 行
  • 字段名{sku,qty,price} 定义了各列
  • 生效分隔符:逗号(默认)

每一行的值都按字段列表的顺序排列。值被编码为基本类型(字符串、数字、布尔值、null),并以分隔符隔开。

NOTE

表格化形式需要满足:所有对象的字段集合完全相同(键相同,但每个对象内的顺序可以不同);每个对象至少包含一个键;并且每一列要么是基本类型值,要么是结构一致的嵌套对象(见下文)。如果数组中包含空对象 {},或某一列的值类型混杂,则会回退到列表形式。

嵌套字段组

当某一列的值都是结构一致的子对象(每个元素中键相同,并且递归地只包含基本类型或结构一致的嵌套对象)时,该列会在首部中折叠为嵌套字段组,而行数据仍然保持扁平:

yaml
orders[2]{id,customer{name,country},total}:
  1,Ada,DK,99
  2,Bob,UK,149

首部中的 customer{name,country} 声明了一个嵌套对象列;每一行的单元格按字段列表的深度优先顺序排列,因此第一笔订单中的 Ada,DK 会填入 customer.namecustomer.country。嵌套深度不受限制(规范 §9.3)。

混合与非一致数组(列表形式)

不满足表格化条件的数组会使用带连字符标记的列表形式:

yaml
items[3]:
  - 1
  - a: 1
  - text

每个元素以 - 开头,缩进比父级数组首部深一级。

作为列表项的对象

当数组元素是对象时,它会以列表项的形式出现:

yaml
items[2]:
  - id: 1
    name: First
  - id: 2
    name: Second
    extra: true

当一个表格化数组是列表项对象的第一个字段时,表格化首部会出现在连字符所在的那一行;此时行数据缩进两级,其他字段缩进一级:

yaml
items[1]:
  - users[2]{id,name}:
      1,Ada
      2,Bob
    status: active

当对象只有一个表格化字段时,同样的模式也适用:

yaml
items[1]:
  - users[2]{id,name}:
      1,Ada
      2,Bob

这是“第一个字段为表格化数组”的列表项对象的规范编码方式。

数组的数组(列表形式)

当数组的内部元素是基本类型数组时:

yaml
pairs[2]:
  - [2]: 1,2
  - [2]: 3,4

每个内部数组的首部都出现在它所在的列表项那一行。

当内部数组本身是对象数组或非一致数组时,同样的 - [N]: 首部会出现在连字符所在行,嵌套项紧随其后并再深一级缩进:

yaml
items[3]:
  - summary
  - id: 1
    name: Ada
  - [2]:
    - id: 2
    - status: draft

空数组

字段中的空数组渲染为 key: [],根级别的空数组则渲染为 []

yaml
items: []

出于向后兼容的考虑,旧式的 items[0]: 形式在解码时仍会被支持。

数组首部

首部语法

数组首部遵循以下模式:

key[N<delimiter?>]<{fields}>:

其中:

  • N 是非负整数长度
  • delimiter(可选)显式声明生效的分隔符:
    • 不填 → 英文逗号分隔
    • \t(制表符)→ 制表符分隔
    • | → 竖线分隔
  • fields(可选,用于表格化数组):{field1,field2,field3}

NOTE

数组长度 [N] 有助于大语言模型校验结构。如果你要求模型生成 TOON 输出,显式的长度声明能让你检测出内容是否被截断或出现格式错误。

分隔符选项

TOON 支持三种分隔符:逗号(默认)、制表符和竖线。分隔符的作用范围仅限于声明它的那个数组首部。

yaml
items[2]{sku,name,qty,price}:
  A1,Widget,2,9.99
  B2,Gadget,1,14.5
yaml
items[2	]{sku	name	qty	price}:
  A1	Widget	2	9.99
  B2	Gadget	1	14.5
yaml
items[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。

注释

解码器会在词法预处理阶段剥离所有第一个非空格字符为 # 的行,这一步发生在其他任何处理之前:

yaml
# Server configuration
host: example.com
port: 8080

注释只支持整行形式:一行中其他位置出现的 # 都是普通内容。注释也只在解码侧生效:编码器永远不会输出注释,并且以 # 开头的字符串值总会被加引号,因此编码器输出中不会出现可被解释为注释的行。表格行之间的注释不会终止该表格结构(规范 §5.1)。

引号与类型

字符串何时需要加引号

TOON 只在必要时才为字符串加引号,以最大化 token 效率。以下情况字符串必须加引号:

  • 空字符串("")
  • 开头或结尾是空格字符
  • 值等于 truefalsenull(区分大小写)
  • 看起来像数字(例如 "42""-3.14""1e-6""05""+1"
  • 包含特殊字符::"\[]{},或任何控制字符(U+0000–U+001F,包括换行符/制表符/回车符)
  • 包含相关的分隔符(数组作用范围内为生效分隔符,其他情况下为文档级分隔符)
  • 值等于 "-",或以 "-" 开头且后面还有其他字符
  • 值等于 "#",或以 "#" 开头(该行会被视为注释)

除上述情况,字符串都可以不加引号。Unicode、emoji 以及内部(非开头/结尾)带空格的字符串都可以安全地不加引号:

yaml
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 类型(NaNInfinityBigIntDateSetMapundefined 等)在编码前会被规范化——完整的映射关系请参阅 API 参考——类型规范化

解码器在输入时同时接受十进制和指数形式(例如 42-3.141e-6),并将带有非法前导零的 token(例如 "05")视为字符串而非数字。

使用 toJSON 进行自定义序列化

拥有 toJSON() 方法的对象在序列化时会先调用该方法,再对其结果进行归一化后编码,这与 JSON.stringify 的行为类似:

ts
const obj = {
  data: 'example',
  toJSON() {
    return { info: this.data }
  }
}

encode(obj)
// info: example

toJSON() 方法:

  • 优先于内置的归一化处理(Date、Array、Set、Map)
  • 其结果会被递归地归一化
  • 只要对象的原型链中存在 toJSON,就会被调用

关于引号、转义、类型转换以及严格模式解码的完整规则,请参阅 规范 §2–4(数据模型)、§7(字符串与键)以及 §14(严格模式)