Wjybxx.Dson
1.0.0-rc2
The package has been merged into commons
See the version list below for details.
dotnet add package Wjybxx.Dson --version 1.0.0-rc2
NuGet\Install-Package Wjybxx.Dson -Version 1.0.0-rc2
<PackageReference Include="Wjybxx.Dson" Version="1.0.0-rc2" />
paket add Wjybxx.Dson --version 1.0.0-rc2
#r "nuget: Wjybxx.Dson, 1.0.0-rc2"
// Install Wjybxx.Dson as a Cake Addin #addin nuget:?package=Wjybxx.Dson&version=1.0.0-rc2&prerelease // Install Wjybxx.Dson as a Cake Tool #tool nuget:?package=Wjybxx.Dson&version=1.0.0-rc2&prerelease
Dson(DataScript Object Notation)
Dson是一个有点奇怪的配置文件格式,但这也许会成为流行的配置文件格式。
Dson同时设计了二进制和文本格式,二进制用于网络传输,文本格式用于配置文件;另外,还提供了数字表示字段的高压缩率二进制版本。
Dson核心包不提供Dson到对象的编解码实现,只包含Dson的二进制流和文本解析实现;Dson支持复杂的数据结构,同时还设计了对象头,因此实现自己的Codec是很容易的。
Dson最初是为序列化而创建的,但我们在这里只讨论文本格式,二进制格式详见
Dson二进制流,Commons。
另外,Dson的一些设计可能与你期望的不同,为避免频繁提问,我将一些设计理由进行了整理,见
Dson设计过程中的一些反思。
另外,我将一些特殊的测试用例或者说模板整理了一下,见
Dson测试用例。
Dson的目标
- 简单
- 精确
- 易于编写和阅读
- 易于维护(修改)
- 易于解析和输出
- 易于扩展
PS:Dson的文本格式从设计到最终实现大概2个月时间,尝试了多个版本的语法和行首,现版本我认为还是达到了目标,在缺乏编辑器的情况下,手写和阅读的体验都还不错。
Dson的特性
- 支持注释
- 支持指定值类型,精确解析
- 支持行合并,长内容行随意切割
- 支持纯文本输入,所见即所得,真正摆脱转义字符 - 也支持混合输入模式
- 引号不是必须的
- 支持对象头,可自定义对象头内容
- 支持类型传递,适用数组元素和Object
- 支持读取Json
文本示例(标准)
下面文本非手写,由程序读取手写文本格式化后输出。
(可运行测试用例 DsonTextReaderTest2.java
)
- {@{clsName: MyClassInfo, guid: 10001, flags: 0}
- name: wjybxx,
- age: 28,
- pos: {@Vector3
- x: 0,
- y: 0,
- z: 0
- },
- address: [
- beijing,
- chengdu
- ],
- intro: @ss 我是wjybxx,是一个游戏开发者,Dson是我设计的文档型数据表达法,你可以通过g
| ithub联系到我。
- thanks
~ , url: "https://www.github.com/hl845740757",
- time: {@dt date: 2023-06-17, time: 18:37:00, millis: 100,
- offset: +08:00}
- }
文本示例(宽松)
下面文本非手写,由上面的文本格式化输出得到 -- dson的java和csharp库都支持无行首输出。
{@{clsName: MyClassInfo, guid: 10001, flags: 0}
name: wjybxx,
age: 28,
pos: {@Vector3
x: 0,
y: 0,
z: 0
},
address: [
beijing,
chengdu
],
intro: " 我是wjybxx,是一个游戏开发者,Dson是我设计的文档型数据表达法,你可以通过githu
b联系到我。\n thanks",
url: "https://www.github.com/hl845740757",
time: {@dt date: 2023-06-17, time: 18:37:00, millis: 100,
offset: +08:00}
}
行首
Dson最特殊的地方在于行的构成,在Dson中,每一行由行首和内容构成,行首与内容之间通过一个空格分隔。
行首目前共5类,如下表:
标签 | 含义 | 格式或示例 | |
---|---|---|---|
# | comment | 注释行,当前行有效;不支持行尾注释 | # 这是一行注释 |
- | append line | 普通行,上一行换行符有效 | - 这是普通的一行 |
| | append | 合并行,上一行换行符无效 | | 这里续写上一行 |
~ | end of text | 表示上一个纯文本输入结束 | |
^ | switch mode | 模式切换,当前行有效;<br/>纯文本模式下插入转义行;<br/>双引号模式下插入纯文本行; | - "我想插入一段正则表达式,但不想处理转义 <br/> ^ ^[\u4e00-\u9fa5_a-zA-Z0-9]+$ <br/> | 接着书写" |
- 行首不是必须的,但只能全无或全有;当不使用行首时,所有内容被认为是连续的。
- 换行处理只在双引号字符串和纯文本模式下有效,其它输入没有换行逻辑。
- '^' 行首是append模式,即不会插入换行符。
- '^' 通常用于双引号模式中输入复杂的字符串,以避免转义。
- '|' 只有第一个空格是缩进,第二个字符开始是内容。
PS:挑选行首字符也蛮困难的,既要对阅读影响小,还要让人容易接受其语义暗示,还要敲着方便。。。
类型
Dson支持的值类型和内置结构体包括:
标签 | 类型 | 枚举 | 含义 | 内置结构体 | 格式或示例 |
---|---|---|---|---|---|
i | int32 | 1 | 32位整型 | @i 123 <br> @i 0xFF | |
L | int64 | 2 | 64位长整型,大写L | @L 123 <br> @L 0xFF | |
f | float | 3 | 32位浮点数 | @f 1.0 | |
d | double | 4 | 64位浮点数 | @d 1.5 <br> 1.5 | |
b | bool | 5 | bool值 | @b true <br> true <br/> @b 1 | |
s | string | 6 | 字符串 | "10" <br> abc | |
N | null | 7 | null,大写N | @N null <br> null | |
bin | binary | 8 | 二进制,带类型标签 | {<br> int32 type;<br> byte[] data; <br>} | 格式固定二元数组 [@bin type, data] <br> [@bin 1, FFFE] |
ei | extInt32 | 9 | 带类型标签的int32 | {<br> int32 type;<br> int32 value;<br> bool hasVal; <br>} | 格式固定二元数组 [@ei type, value] <br> [@ei 1, 10086] <br> [@ei 1, null] |
eL | extInt64 | 10 | 带类型标签的int64 | {<br> int32 type;<br> int64 value;<br> bool hasVal; <br>} | 格式固定二元数组 [@eL type, value] <br> [@eL 1, 10086] <br> [@eL 1, null] |
ed | extDouble | 11 | 带类型标签的double | {<br> int32 type;<br> double value;<br> bool hasVal; <br>} | 格式固定二元数组 [@ed type, value] <br> [@ed 1, 0.5] <br>[@ed 1, null] |
es | extString | 12 | <b>带类型标签的string</b> | {<br> int32 type;<br> string value; <br>} | 格式固定二元数组 [@es type, value] <br> - [@es 10, <br/>- @ss ^[\u4e00-\u9fa5_a-zA-Z0-9]+$ <br/> ~ ] |
ref | reference | 13 | 引用 | {<br> string namespace;<br> string localId;<br> int32 type; <br> int32 policy; <br>} | 格式为单值 '@ref localId' 格式或 object格式 <br/> @ref abcdefg <br> {@ref ns: wjybxx, localId: abcdefg, type: 0} |
dt | datetime | 14 | 日期时间 | { <br> int64 seconds; <br> int32 nanos;<br> int32 offset;<br> int32 enables; <br> } | 无需引号<br/> @dt 2023-06-17T18:37:00 <br/>{@dt date: 2023-06-17, time: 18:37:00, offset: +08:00, millis: 100} |
header | 29 | 对象头 | 对象形式: @{clsName: Vector3 } <br/> 简写形式: @Vector3 | ||
array | 30 | 数组 | [ 1, 2, 3, 4, 5 ] | ||
object | 31 | 对象/结构体 | { name: wjybxx, age: 28 } |
特殊标签
特殊标签不是真正的类型,而是对类型的修饰,是语法糖,用以简化书写。
标签 | 类型 | 含义 | 格式或示例 |
---|---|---|---|
ss | string string <br/> (text) | 纯文本 <br> 1. 不会对字符进行转义,所见即所得 <br/>2. 遇见'~'行首时结束 <br/>3. 既可修饰value,也可修饰key <br/>4. 相当于特殊的引号 | - {<br> - url: @ss https://github.com/hl845740757 <br> ~ , reg: @ss ^[\u4e00-\u9fa5_a-zA-Z0-9]+$ <br> ~ } |
在我自己的笔记中,示例其实比上面两张表复杂,但我实在不想写转义字符和标签,所以示例偷懒了...
@声明标签
我们可以通过'@label'声明一个元素的类型,一般是修饰下一个元素,元素可以是 key,也可以是value,取决于上下文。
普通value类型,可在value的前方通过 @ 声明类型;object和array则在{}和[]内声明其类型,且声明时和外层括号之间无空格。
ps: 当@作用于普通值类型和内置简单结构体时,我们称@声明的是其类型;当@作用于object和array时,我们称@声明的是对象的头信息。
全局规范:
- Dson是基于行解析的,换行符只支持 \n 和 \r\n,行首的空白将被忽略。
- Dson的每一行由 行首和内容 构成。
- # 表述注释行,# 号仅对当前行有效,不支持行尾注释。
- 行首与内容 之间至少键入一个空格(且必须是空格),以表达内容的开始。
- 类型标签的label为字符串类型,支持无引号和双引号模式;标签与被修饰的元素之间必须通过一个空格或换行分隔。
- 顶层对象必须是Object/Array/Header,你不可以直接声明一个基础值类型,顶层对象之间可以不使用逗号分隔。
- Dson大小写敏感。
- Dson支持无行首的文本输入,但必须所有行都无行首,所有行都被看做append行。
- Json是有效的Dson输入。
ps: 我去除了顶层不能是header的限制,因此可以用顶层的header来表达文件头,用作其它目的都是不好的。
各种值的规范
Number系列
- 简单数字格式以外的任何格式,都必须声明类型标签(含下划线时也是需要的)
- NaN、Infinity、-Infinity的支持取决于编程语言 —— 我现在不清楚是否有语言不支持
- int32和int64支持16进制和2进制,16进制以0x或0X开头,2进制以0b或0B开头;支持负号。
- 浮点数支持科学计数法。
- 整数和浮点数都支持下划线分割,不限制分割方式,但建议3个一组。
- 首尾不可以是下划线
- 不可以连续多个,下划线也不可以和小数点连续。
- {
- value1: 10001,
- value2: 1.05,
- value3: @i 0xFF,
- value4: @i 0b10010001,
- value5: @i 100_000_000,
- value6: @d 1.0E-6,
- value7: @d Infinity,
- value8: @d NaN,
- value9: @i -0xFF,
- value10: @i -0b10010001,
- }
bool值
- 未声明类型的情况下,只支持 true和false 两个值,且大小写严格;
- 声明类型的情况下,支持 0和1 输入; 1表示true,0表示false;
- 声明类型的情况下,拼写错误将引发错误 -- null同理
{value1: true, value2: false, value3: @b 1, value4: @b 0}
# 拼写错误将引发异常
{value1: @b ture, value2: @N nul}
字符串
- 可以使用@s或双引号强调值是一个字符串类型 —— 更推荐使用双引号强调。
- 普通无引号字符串只能由安全字符构成,安全字符较多,我们给出不安全字符集。
- 无引号字符串遇见不安全字符时将判定为结束。
- 使用双引号时,\ 是转义字符,解析时将对引号进行转义,因此内容中出现双引号时需要转义
- 在纯文本中,不需要加引号,\ 不是转义字符,所见即所得,遇见 '~' 行首时表示结束。
- 在纯文本中,@ss标签未立即换行的情况下,只有第一空格是分隔字符 —— 立即换行和输入一个空格再换行是等同的。
- 不论纯文本还是引号字符串,换行后只有行首后的第一个空格是缩进,无行首的情况下不可以缩进。
- 存在一些特殊含义的字符串,用户最好总是使用双引号强调是字符串,否则可能发生兼容性问题。
不安全字符
- 空白字符
- dson token字符集:花括号'{}' 、方括号'[]' 、逗号',' 、 冒号':' 、艾特'@' 、反斜杠 ''
- 保留字符集:圆括号 '()'
- 特殊含义字符串:"true", "false", "null", "undefine", "NaN", "Infinity", "-Infinity"
- 简单数字(整数和小数)
转义字符
转义字符与json基本一致,但 '/' 无需转义
引号 | 反斜杠 | 退格(BS) | 换页(FF) | 换行(LF) | 回车(CR) | 水平制表(HT) | unicode字符 |
---|---|---|---|---|---|---|---|
\" | \\ | \b | \f | \n | \r | \t | \u |
转义换行限制
在处理转义字符的过程中,必须保持输入在逻辑上连续。即:
- 双引号模式下,只可以切换到 '|' 行。
- 在纯文本单行转义模式下,只可以继续切换到 '^' 行。
二进制
- 配置格式限定二元组 [type, data]
- type限定为int32,且禁止负数
- data部分使用 16进制 编码,且不可以为null
ei、eL、ed、es
- 配置格式限定二元组 [type, value]
- type限定int32,且禁止负数
- value部分遵循各自的规范
- 允许value为null,使用null时必须显式输入。
{@ei type: 1, value: 1}
{@ei type: 1, value: null}
ref
- ref支持两种范式 @ref localId 和 {@ref localId: $localId, ns: $ns, type: $type, policy: $policy}
- @ref localId 简写方式适用大多数情况,结构体用于复杂情况。
- ns是namespace的缩写,localId和namespace限定字符串,无特殊符号时可省略引号。
- Dson默认不解析引用,只存储为ref结构,会提供根据localId解析的简单方法。
- 字段拼写错误将引发异常。
{@ref localId: 10001, ns : global, type: 1 }
{@ref localId: 10001, ns : global, type: 1, policy: 1}
PS:对于配置文件,引用的最大作用是复用和减少嵌套。
dt-日期时间
- dt支持两种范式 @dt datetime 和 {@dt date: yyyy-MM-dd, time: HH:mm:ss, offset: ±HH:mm:ss, millis: $millis, nanos: $nanos}
- @dt datetime 简写方式用于一般情况,datetime为 "yyyy-MM-ddTHH:mm:ss"格式,即ISO601格式
- date 部分为 "yyyy-MM-dd" 格式。
- time 部分为 "HH:mm:ss" 格式,不可省略秒部分。
- offset 部分为 "Z", "±H", "±HH", "±HH:mm" 或 "±HH:mm:ss" 格式,不可以省略正负号。
- millis 表示输入毫秒转纳秒。
- nanos 表示直接输入纳秒,millis 和 nanos通常只应该出现一个。
- date 和 time 至少输入一个,其它属性都是可选的,拼写错误将引发异常。
@dt 2023-06-17T18:37:00
{@dt date: 2023-06-17, time: 18:37:00}
{@dt date: 2023-06-17, time: 18:37:00, offset: +8}
{@dt date: 2023-06-17, time: 18:37:00, offset: +08:00, millis: 100}
{@dt date: 2023-06-17, time: 18:37:00, offset: +08:00, nanos: 100_000_000}
关于内置结构体的说明
- seconds 是纪元时间的秒时间(datetime)
- nanos 是时间戳的纳秒部分
- offset 是时区的秒数
- enables 记录了用户输入了哪些部分,由掩码构成,后4个比特位有效。
- date 的掩码是 0001
- time 的掩码是 0010
- offset 的掩码是 0100
- nanos 的掩码是 1000
object
- 键值对之间通过 ',' (英文逗号) 分隔。
- key和value之间通过 ':' (英文冒号)分隔,冒号两边空格分隔key和value不是必须的,但冒号和value之间建议输入空格。
- key为字符串类型,适用字符串规范,无特殊字符时可省略双引号,也可以使用 @ss 标签。
- 声明对象自己的类型时,@和object的 { 之间无空格,即: {@clsName ,否则将被解释为下一个元素的类型信息。
- 当声明value的类型时,可通过声明header结构体,并指定 'compClsName' 属性实现。
- 允许末尾出现逗号
array
- value之间通过 ',' (英文逗号) 分隔。
- 声明对象自己的类型时,@与array的 [ 之间无空格,即: [@clsName ,否则将被解释为下一个元素的类型信息。
- 当声明value的类型时,可通过声明header结构体,并指定 'compClsName' 属性实现。
- 允许末尾出现逗号
header
- header支持两种范式, "@clsName" 和 "@{ }"。
- "@clsName" 是 @{clsName: $clsName}的语法糖,用于一般情况下简化书写。
- clsName为字符串类型,适用字符串规范,无特殊字符时可不加引号。
- header结构体不可以再内嵌header
- header没有默认结构体,以允许用户自行扩展。
Header特殊属性依赖
虽然header没有默认结构体,但我们还是依赖了一些属性,以支持一些基础功能。因此,如果用户扩展header,使用了这三个属性名,请确保类型一致,否则可能引发兼容性问题。
属性名 | 类型 | 含义 | 备注 |
---|---|---|---|
clsName | string | className的缩写,表达当前对象的类型 | 可包括内置基础值和内置结构体 |
compClsName | string | componentClassName的缩写,表示数组成员或Object的value类型 | 可包括内置基础值和内置结构体 |
localId | string | 对象本地id | 用于支持默认的引用解析 |
以下是object声明header的示例
# 不声明header
{ x: 0, y: 0, z: 0 }
# 只声明ClassName
{@Vector3 x: 0, y: 0, z: 0 }
# 声明复杂的header
{@{clsName: Vector3, localId: 321123} x: 1, y: 1, z: 1}
无类型value解析规则
- 首先去掉value两端的空白字符
- 如果value首字符为 { ,则按照object解析
- 如果value首字符为 [ ,则按照array解析
- 如果value首字符为 " ,则按照字符串解析
- 如果value为 true 或 false ,则解析为bool类型
- 如果value为 null,则解析为null
- 如果value匹配整数或浮点数(特殊格式不匹配),则固定解析为double类型。
- 其它情况下默认解析为String
一些好的实践
- 行首总是写在行首位置,顶级对象之间通过空内容行分离
- key尽量遵守一般编程语言变量命名规范,仅使用数字字母和下划线,且非数字开头,以避免引号;同时key应当保持简短,避免换行。
- ClassName尽量不包含特殊字符,使得ClassName总是可以无引号表达。可考虑通过点号'.'或下划线'_'分隔;
- { 和 [ 与正式内容之间总是键入一个空格
- header的定义要保持简单和简短,避免增加额外的复杂度。
- 字符串尽量使用无引号模式 和 纯文本模式输入,手写转义是麻烦易出错的工作。
- 一旦你想以某种特殊形式输入一个值,都应该显式声明这个值的类型。
- 数字避免使用8进制,浮点数慎用16进制
PS:内置结构体的值类型都是确定的,因此可以不声明类型直接使用特殊方式输入。
与其他配置文件格式的比较
JSON和Bson
json存在的问题:
- 不支持注释。
- 不支持注释导致Json不适合做配置模板,虽然部分库支持注释,但并不是所有库都支持。
- 字符串不支持换行。
- 程序生成的json文件经常出现一行上千和上万个字符的情况。
- 手写json出现长文本时非常难受。
- 不支持文本模式,不支持所见即所得,要处理大量的转义字符。
- json字符串存在引号的时候,转义的问题更加明显。
- 总是要加引号,key要加,value要加,手写json的时候有点难受。
- 值的精确程度不够,json当初是为js脚本语言设计的。
bson存在的问题:
- bson最大的问题是兼容json,它包含了json的所有问题 —— 我觉得这是bson最大的失误。
- bson是为mongodb数据库设计的,其数据格式与类型与数据库强相关。
- bson的类型语法设计得有点复杂,因为它是基于标准的json格式设计出来。
YAML
我承认,初次看见YAML文件的时候感到很优美,比如unity生成的yaml文件。 但研究yaml的规则之后,我对于YAML的流行表示不解,我觉得yaml的流行是一种灾难。
虽然我讨厌写json,但我认为json设计的其实很优美,它非常简单,输入严格,你很容易学会使用它。
YAML看上去也很简单是不是?但YAML的简单只是表面的简单。
- 使用空格缩进来表示层级关系,这是YAML最大的优点和缺点。
- 使用不可见的空格缩进来表示层级关系,如果YAML是程序生成的,那很beautiful。
- 如果YAML是手工书写,并可能长期维护的,这就是灾难 —— 文件越大,嵌套越深,越痛苦。
- 个人认为缩进不应该影响正确性,这非常脆弱,多方面的脆弱。
- YAML换行处理治标不治本
- YAML提供了好多种符号来处理文本段换行问题,在我看来就是没有方案,因为各个都不彻底。
- YAML不易扩展,因为YAML的规则太复杂了。
- 写个无bug的Json解析工具可能一天,写个无bug的YAML解析工具呢?
YAML的设计哲学里忽略了一点,需要手工编写的文件,维护成本才是最重要的! 美观最多放第二。
其实json的可读性我觉得已经足够好了,yaml的排版虽然漂亮,但付出的代价太大 —— 我写个文件就是为了排版好看吗?
PS:
- 我经常怀疑YAML的设计者是不是程序员...
- 输入并不是越少越好,简化输入不能损害精确性
TOML
说实话,TOML我接触的很少,看了几个toml文件示例后,我觉得它太像properties文件的优化版了。 TOML的语法非常接近编程语言:
- 可通过点号 . 来获取属性和赋值;也支持了数据结构内联
- 字符串段落使用三个引号""" —— 现在编程语言流行这样,Java也支持;但我想彻底摆脱转义。
- 换行继续输入使用反斜杠+换行 —— 说起来我最开始也想过这个方案,使用shell的经验
不过,我的直觉是TOML并不适合编写复杂的配置文件,更不适合用作程序导出的文件格式,所以我不选择TOML。
PS:
- 支持数字使用下划线,来自TOML的提醒,我在写代码时经常用到。
- TOML语法也是比较独特的,Dson也是。
- 还有一个我比较陌生的配置文件格式 HOCON,但HOCON只适合程序员,因为太像脚本语言。
Dson呢?
Dson是一个看似复杂,实则简单的语言。
Dson主要解决的问题有三个:
- 单行过长问题 - 期望能换行继续输入
- 纯文本和转义问题 - 转义很影响可读性
- 序列化的精确性问题 - Dson最初是为序列化设计的
前两个问题,都通过行首得到了很好的解决;行首的设计,是Dson的特色, 行首带来的优势远大于编写时敲两个字符的负面影响,这使得我们可以编写所见即所得的文本,且可以自由拆分行内容。
序列化的精确性问题则通过对象头来解决;对象头是额外的数据区,不会污染正常的数据区; 我们通过在对象头中存储对象的类型信息,可以实现序列化的精确性,也可以实现一些其它的扩展功能。
多语言
作者本人是一个游戏开发者,熟悉JAVA、C#、LUA,后期会提供C#的Dson实现;其它语言可能只能依靠喜欢上Dson的小伙伴们提供。
- Java库使用指南。
# java maven坐标(最新可查看Mvn中央仓库) <groupId>cn.wjybxx.dson</groupId> <artifactId>dson-core</artifactId> <version>1.2.0</version>
- C#库使用指南。
nuget Wjybxx.Dson 1.0.0
Dson编辑器(缺失)
行首虽然很好地解决了内容拆分的问题,但对新手使用者有一定的干扰,如果有编辑器(和插件)支持的话,行首带来的影响就可以消除。 不过,作者我不是很擅长做这个,暂时无法提供。
特别鸣谢
这里感谢MongoDB的Bson团队,Bson源代码对于我实现Dson起到了很大的帮助作用。
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net6.0 is compatible. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. |
-
net6.0
- Wjybxx.Commons.Core (>= 1.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
Version | Downloads | Last updated | |
---|---|---|---|
2.2.0-rc3 | 139 | 7/31/2024 | |
2.2.0-rc2 | 155 | 7/21/2024 | |
2.2.0-rc1 | 150 | 7/14/2024 | |
2.1.2 | 149 | 5/20/2024 | |
2.1.1 | 150 | 5/16/2024 | |
2.1.0 | 160 | 5/15/2024 | |
2.0.2 | 169 | 5/4/2024 | |
2.0.1 | 135 | 4/9/2024 | |
2.0.0 | 156 | 3/30/2024 | |
1.4.1 | 178 | 1/7/2024 | |
1.4.0 | 157 | 1/6/2024 | |
1.4.0-rc2 | 152 | 1/2/2024 | |
1.4.0-rc1 | 145 | 12/31/2023 | |
1.3.0 | 176 | 12/30/2023 | |
1.2.0 | 165 | 12/27/2023 | |
1.0.0 | 162 | 12/26/2023 | |
1.0.0-rc2 | 157 | 12/25/2023 | |
1.0.0-rc1 | 155 | 12/23/2023 |