Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ dotnet test --collect:"XPlat Code Coverage"

### .NET Version Support

We target .NET 8.0 and .NET 9.0 for broad compatibility.
We target .NET Standard 2.0 for maximum compatibility across .NET Framework, .NET Core, and .NET 5+. On Windows machines, the `net481` development
framework will need to be installed to validate .NET Standard

### Type Safety

Expand Down
35 changes: 35 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<Project>
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net8.0;net9.0;net10.0</TargetFrameworks>
</PropertyGroup>

<!--
1. CI Builds only run tests on modern supported frameworks
2. Local builds run tests on .NET Framework 4.8.1 (Windows only) and modern supported frameworks
-->
<Choose>
<When Condition=" '$(CI)' == 'true'">
<PropertyGroup>
<TestTargetFrameworks>net8.0;net9.0;net10.0</TestTargetFrameworks>
</PropertyGroup>
</When>

<Otherwise>

<Choose>
<When Condition=" $([MSBuild]::IsOsPlatform('Windows')) ">
<PropertyGroup>
<TestTargetFrameworks>net481;net8.0;net9.0;net10.0</TestTargetFrameworks>
</PropertyGroup>
</When>

<Otherwise>
<PropertyGroup>
<TestTargetFrameworks>net8.0;net9.0;net10.0</TestTargetFrameworks>
</PropertyGroup>
</Otherwise>
</Choose>

</Otherwise>
</Choose>
</Project>
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
# TOON Format for .NET

[![NuGet version](https://img.shields.io/nuget/v/Toon.Format.svg)](https://www.nuget.org/packages/Toon.Format/)
[![.NET version](https://img.shields.io/badge/.NET-8.0%20%7C%209.0-512BD4)](https://dotnet.microsoft.com/)
[![.NET version](https://img.shields.io/badge/.NET-Standard%202.0-512BD4)](https://dotnet.microsoft.com/)
[![.NET version](https://img.shields.io/badge/.NET-8.0%20%7C%209.0%20%7C10.0-512BD4)](https://dotnet.microsoft.com/)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)

**Token-Oriented Object Notation** is a compact, human-readable encoding of the JSON data model that minimizes tokens and makes structure easy for models to follow. Combines YAML-like indentation with CSV-like tabular arrays. Fully compatible with the [official TOON specification v3.0](https://github.com/toon-format/spec).

**Key Features:** Minimal syntax • TOON Encoding and Decoding • Tabular arrays for uniform data • Path expansion • Strict mode validation • .NET 8.0, 9.0 and 10.0 • 520+ tests with 99.7% spec coverage.
**Key Features:** Minimal syntax • TOON Encoding and Decoding • Tabular arrays for uniform data • Path expansion • Strict mode validation • .NET Standard, .NET 8.0, 9.0 and 10.0 • 520+ tests with 99.7% spec coverage.

## Quick Start

Expand Down Expand Up @@ -253,7 +254,7 @@ This implementation:
- Supports all TOON v3.0 features
- Handles all edge cases and strict mode validations
- Fully documented with XML comments
- Production-ready for .NET 8.0, .NET 9.0 and .NET 10.0
- Production-ready for .NET Standard 2.0, .NET 8.0, .NET 9.0 and .NET 10.0

See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed guidelines.

Expand Down
4 changes: 4 additions & 0 deletions src/ToonFormat/Internal/Decode/Parser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,11 @@ internal static class Parser
int bracketStart = -1;

// For quoted keys, find bracket after closing quote (not inside the quoted string)
#if NETSTANDARD2_0
if (trimmed.StartsWith(Constants.DOUBLE_QUOTE.ToString()))
#else
if (trimmed.StartsWith(Constants.DOUBLE_QUOTE))
#endif
{
var closingQuoteIndex = StringUtils.FindClosingQuote(trimmed, 0);
if (closingQuoteIndex == -1)
Expand Down
4 changes: 2 additions & 2 deletions src/ToonFormat/Internal/Decode/Scanner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -188,9 +188,9 @@ public static ScanResult ToParsedLines(string source, int indentSize, bool stric
}
parsed.Add(new ParsedLine
{
Raw = new string(lineSpan),
Raw = lineSpan.ToString(),
Indent = indent,
Content = new string(contentSpan),
Content = contentSpan.ToString(),
Depth = lineDepth,
LineNumber = lineNumber
});
Expand Down
4 changes: 2 additions & 2 deletions src/ToonFormat/Internal/Encode/Encoders.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public static string EncodeValue(JsonNode? value, ResolvedEncodeOptions options)
/// <summary>
/// Encodes a JsonObject as key-value pairs.
/// </summary>
public static void EncodeObject(JsonObject value, LineWriter writer, int depth, ResolvedEncodeOptions options, IReadOnlySet<string>? rootLiteralKeys = null,
public static void EncodeObject(JsonObject value, LineWriter writer, int depth, ResolvedEncodeOptions options, IReadOnlyCollection<string>? rootLiteralKeys = null,
string? pathPrefix = null, int? remainingDepth = null)
{
var keys = (value as IDictionary<string, JsonNode>).Keys!;
Expand Down Expand Up @@ -96,7 +96,7 @@ public static void EncodeKeyValuePair(
int depth,
ResolvedEncodeOptions options,
IReadOnlyCollection<string>? siblings = null,
IReadOnlySet<string>? rootLiteralKeys = null,
IReadOnlyCollection<string>? rootLiteralKeys = null,
string? pathPrefix = null,
int? flattenDepth = null)
{
Expand Down
6 changes: 5 additions & 1 deletion src/ToonFormat/Internal/Encode/Folding.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ internal class KeyChain

internal static class Folding
{
public static FoldResult? TryFoldKeyChain(string key, JsonNode? value, IReadOnlyCollection<string> siblings, ResolvedEncodeOptions options, IReadOnlySet<string>? rootLiteralKeys = null,
public static FoldResult? TryFoldKeyChain(string key, JsonNode? value, IReadOnlyCollection<string> siblings, ResolvedEncodeOptions options, IReadOnlyCollection<string>? rootLiteralKeys = null,
string? pathPrefix = null, int? flattenDepth = null)
{
// Only fold when safe mode is enabled
Expand Down Expand Up @@ -149,7 +149,11 @@ private static KeyChain CollectSingleKeyChain(string startKey, JsonNode? startVa

private static string BuildFoldedKey(IReadOnlyCollection<string> segments)
{
#if NETSTANDARD2_0
return string.Join(Constants.DOT.ToString(), segments);
#else
return string.Join(Constants.DOT, segments);
#endif
}
}
}
14 changes: 10 additions & 4 deletions src/ToonFormat/Internal/Encode/Normalize.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ internal static class Normalize
/// Normalizes an arbitrary .NET value to a JsonNode representation.
/// Handles primitives, collections, dates, and custom objects.
/// </summary>
/// <param name="value">The value to be normalized.</param>
public static JsonNode? NormalizeValue(object? value)
{
// null
Expand All @@ -40,7 +41,7 @@ internal static class Normalize
{
// Canonicalize signed zero via FloatUtils
var dn = FloatUtils.NormalizeSignedZero(d);
if (!double.IsFinite(dn))
if (!NumericUtils.IsFinite(dn))
return null;
return JsonValue.Create(dn);
}
Expand All @@ -49,7 +50,7 @@ internal static class Normalize
{
// Canonicalize signed zero via FloatUtils
var fn = FloatUtils.NormalizeSignedZero(f);
if (!float.IsFinite(fn))
if (!NumericUtils.IsFinite(fn))
return null;
return JsonValue.Create(fn);
}
Expand Down Expand Up @@ -138,11 +139,16 @@ internal static class Normalize
return JsonValue.Create(l);
case double d:
if (BitConverter.DoubleToInt64Bits(d) == BitConverter.DoubleToInt64Bits(-0.0)) return JsonValue.Create(0.0);
if (!double.IsFinite(d)) return null;
if (!NumericUtils.IsFinite(d)) return null;
return JsonValue.Create(d);
case float f:
#if NETSTANDARD2_0
// netstandard does not have BitConverter.SingleToInt32Bits
if (FloatUtils.NormalizeSignedZero(f).Equals(0.0f)) return JsonValue.Create(0.0f);
#else
if (BitConverter.SingleToInt32Bits(f) == BitConverter.SingleToInt32Bits(-0.0f)) return JsonValue.Create(0.0f);
if (!float.IsFinite(f)) return null;
#endif
if (!NumericUtils.IsFinite(f)) return null;
return JsonValue.Create(f);
case decimal dec:
return JsonValue.Create(dec);
Expand Down
6 changes: 5 additions & 1 deletion src/ToonFormat/Internal/Encode/Primitives.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,11 @@
if (result.Contains('.'))
{
result = result.TrimEnd('0');
if (result.EndsWith('.'))
#if NETSTANDARD2_0
if (result.EndsWith(Constants.DOT.ToString()))
#else
if (result.EndsWith(Constants.DOT))
#endif
result = result.TrimEnd('.');
}

Expand Down Expand Up @@ -166,7 +170,7 @@
// Add key if present
if (!string.IsNullOrEmpty(key))
{
header += EncodeKey(key);

Check warning on line 173 in src/ToonFormat/Internal/Encode/Primitives.cs

View workflow job for this annotation

GitHub Actions / .NET 10.0 on ubuntu-latest

Possible null reference argument for parameter 'key' in 'string Primitives.EncodeKey(string key)'.

Check warning on line 173 in src/ToonFormat/Internal/Encode/Primitives.cs

View workflow job for this annotation

GitHub Actions / .NET 10.0 on ubuntu-latest

Possible null reference argument for parameter 'key' in 'string Primitives.EncodeKey(string key)'.

Check warning on line 173 in src/ToonFormat/Internal/Encode/Primitives.cs

View workflow job for this annotation

GitHub Actions / .NET 8.0 on ubuntu-latest

Possible null reference argument for parameter 'key' in 'string Primitives.EncodeKey(string key)'.

Check warning on line 173 in src/ToonFormat/Internal/Encode/Primitives.cs

View workflow job for this annotation

GitHub Actions / .NET 8.0 on ubuntu-latest

Possible null reference argument for parameter 'key' in 'string Primitives.EncodeKey(string key)'.

Check warning on line 173 in src/ToonFormat/Internal/Encode/Primitives.cs

View workflow job for this annotation

GitHub Actions / .NET 9.0 on ubuntu-latest

Possible null reference argument for parameter 'key' in 'string Primitives.EncodeKey(string key)'.

Check warning on line 173 in src/ToonFormat/Internal/Encode/Primitives.cs

View workflow job for this annotation

GitHub Actions / .NET 9.0 on ubuntu-latest

Possible null reference argument for parameter 'key' in 'string Primitives.EncodeKey(string key)'.
}

// Add array length with optional marker and delimiter
Expand Down
22 changes: 17 additions & 5 deletions src/ToonFormat/Internal/Shared/FloatUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ internal static class FloatUtils
/// <returns></returns>
public static bool NearlyEqual(double a, double b, double absEps = 1e-12, double relEps = 1e-9)
{
if (double.IsNaN(a) && double.IsNaN(b)) return true; // ҵ���ϳ���Ҫ�� NaN == NaN
if (double.IsNaN(a) && double.IsNaN(b)) return true;
if (double.IsInfinity(a) || double.IsInfinity(b)) return a.Equals(b);
if (a == b) return true; // ���� 0.0 == -0.0����ȫ���
if (a == b) return true;

var diff = Math.Abs(a - b);
var scale = Math.Max(Math.Abs(a), Math.Abs(b));
if (scale == 0) return diff <= absEps; // ���߶��ӽ� 0
if (scale == 0) return diff <= absEps;
return diff <= Math.Max(absEps, relEps * scale);
}

Expand All @@ -35,7 +35,19 @@ public static double NormalizeSignedZero(double v) =>
/// <summary>
/// Explicitly change -0.0f to +0.0f for float values.
/// </summary>
public static float NormalizeSignedZero(float v) =>
BitConverter.SingleToInt32Bits(v) == BitConverter.SingleToInt32Bits(-0.0f) ? 0.0f : v;
public static float NormalizeSignedZero(float v)
{
#if NETSTANDARD2_0
unsafe
{
int* vBits = (int*)&v;
float negZero = -0.0f;
int* zeroBits = (int*)&negZero;
return *vBits == *zeroBits ? 0.0f : v;
}
#else
return BitConverter.SingleToInt32Bits(v) == BitConverter.SingleToInt32Bits(-0.0f) ? 0.0f : v;
#endif
}
}
}
19 changes: 19 additions & 0 deletions src/ToonFormat/Internal/Shared/NumericUtils.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;

namespace Toon.Format.Internal.Shared
Expand Down Expand Up @@ -44,5 +45,23 @@ public static decimal EmitCanonicalDecimalForm(double value)

return decimalValue;
}

public static bool IsFinite(double value)
{
#if NETSTANDARD2_0
return !(double.IsNaN(value) || double.IsInfinity(value));
#else
return double.IsFinite(value);
#endif
}

public static bool IsFinite(float value)
{
#if NETSTANDARD2_0
return !(float.IsNaN(value) || float.IsInfinity(value));
#else
return float.IsFinite(value);
#endif
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#if NETSTANDARD2_0

namespace System.Diagnostics.CodeAnalysis
{
/// <summary>
/// Compiler attribute for setting required members
/// </summary>
[AttributeUsage(AttributeTargets.Constructor, AllowMultiple = false, Inherited = true)]
public sealed class SetsRequiredMembersAttribute : Attribute { }
}

#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#if NETSTANDARD2_0
using System;

namespace System.Runtime.CompilerServices
{
/// <summary>
/// Indicates that a compiler feature is required.
/// </summary>
[AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = false)]
public sealed class CompilerFeatureRequiredAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="CompilerFeatureRequiredAttribute"/> class.
/// </summary>
/// <param name="featureName">The name of the feature.</param>
public CompilerFeatureRequiredAttribute(string featureName)
{
FeatureName = featureName;
}

/// <summary>
/// Gets the name of the feature.
/// </summary>
public string FeatureName { get; }
}
}

#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#if NETSTANDARD2_0

using System;

namespace System.Runtime.CompilerServices
{
/// <summary>
/// Attribute for decorating members as required.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false, Inherited = true)]
public sealed class RequiredMemberAttribute : Attribute { }
}

#endif
27 changes: 25 additions & 2 deletions src/ToonFormat/ToonDecoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -184,8 +184,14 @@ public static class ToonDecoder
{
if (stream == null)
throw new ArgumentNullException(nameof(stream));

#if NETSTANDARD2_0
using var reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, bufferSize: 1024, leaveOpen: true);
#else
using var reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, leaveOpen: true);
#endif
var text = reader.ReadToEnd();

return Decode(text, options ?? new ToonDecodeOptions());
}

Expand All @@ -209,8 +215,15 @@ public static class ToonDecoder
{
if (stream == null)
throw new ArgumentNullException(nameof(stream));

#if NETSTANDARD2_0
using var reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, bufferSize: 1024, leaveOpen: true);
#else
using var reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, leaveOpen: true);
#endif

var text = reader.ReadToEnd();

return Decode<T>(text, options ?? new ToonDecodeOptions());
}

Expand Down Expand Up @@ -296,9 +309,14 @@ public static class ToonDecoder
if (stream == null)
throw new ArgumentNullException(nameof(stream));

#if NETSTANDARD2_0
using var reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, bufferSize: 1024, leaveOpen: true);
var text = await reader.ReadToEndAsync().ConfigureAwait(false);
#else
using var reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, leaveOpen: true);
var text = await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
return Decode(text, options ?? new ToonDecodeOptions());
#endif
return await DecodeAsync(text, options ?? new ToonDecodeOptions(), cancellationToken: cancellationToken);
}

/// <summary>
Expand Down Expand Up @@ -327,9 +345,14 @@ public static class ToonDecoder
if (stream == null)
throw new ArgumentNullException(nameof(stream));

#if NETSTANDARD2_0
using var reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, bufferSize: 1024, leaveOpen: true);
var text = await reader.ReadToEndAsync().ConfigureAwait(false);
#else
using var reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, leaveOpen: true);
var text = await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
return Decode<T>(text, options ?? new ToonDecodeOptions());
#endif
return await DecodeAsync<T>(text, options ?? new ToonDecodeOptions(), cancellationToken: cancellationToken);
}

#endregion
Expand Down
Loading