Skip to content

Commit 840626d

Browse files
feat: minor fixes for spec v1.4 compliance
1 parent 1b87cfe commit 840626d

File tree

10 files changed

+143
-33
lines changed

10 files changed

+143
-33
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
[![CI](https://github.com/toon-format/toon/actions/workflows/ci.yml/badge.svg)](https://github.com/toon-format/toon/actions)
66
[![npm version](https://img.shields.io/npm/v/@toon-format/toon.svg)](https://www.npmjs.com/package/@toon-format/toon)
7-
[![SPEC v1.3](https://img.shields.io/badge/spec-v1.3-lightgray)](https://github.com/toon-format/spec)
7+
[![SPEC v1.4](https://img.shields.io/badge/spec-v1.4-lightgray)](https://github.com/toon-format/spec)
88
[![npm downloads (total)](https://img.shields.io/npm/dt/@toon-format/toon.svg)](https://www.npmjs.com/package/@toon-format/toon)
99
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
1010

@@ -1022,7 +1022,7 @@ Task: Return only users with role "user" as TOON. Use the same header. Set [N] t
10221022
## Other Implementations
10231023

10241024
> [!NOTE]
1025-
> When implementing TOON in other languages, please follow the [specification](https://github.com/toon-format/spec/blob/main/SPEC.md) (currently v1.3) to ensure compatibility across implementations. The [conformance tests](https://github.com/toon-format/spec/tree/main/tests) provide language-agnostic test fixtures that validate implementations across any language.
1025+
> When implementing TOON in other languages, please follow the [specification](https://github.com/toon-format/spec/blob/main/SPEC.md) (currently v1.4) to ensure compatibility across implementations. The [conformance tests](https://github.com/toon-format/spec/tree/main/tests) provide language-agnostic test fixtures that validate implementations across any language.
10261026
10271027
### Official Implementations
10281028

SPEC.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ The TOON specification has moved to a dedicated repository: [github.com/toon-for
44

55
## Current Version
66

7-
**Version 1.3** (2025-10-31)
7+
**Version 1.4** (2025-11-05)
88

99
## Quick Links
1010

packages/toon/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,6 @@
3838
"test": "vitest"
3939
},
4040
"devDependencies": {
41-
"@toon-format/spec": "^1.3.3"
41+
"@toon-format/spec": "^1.4.0"
4242
}
4343
}

packages/toon/src/decode/parser.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -219,34 +219,36 @@ export function parsePrimitiveToken(token: string): JsonPrimitive {
219219

220220
// Numeric literal
221221
if (isNumericLiteral(trimmed)) {
222-
return Number.parseFloat(trimmed)
222+
const parsedNumber = Number.parseFloat(trimmed)
223+
// Normalize negative zero to positive zero
224+
return Object.is(parsedNumber, -0) ? 0 : parsedNumber
223225
}
224226

225227
// Unquoted string
226228
return trimmed
227229
}
228230

229231
export function parseStringLiteral(token: string): string {
230-
const trimmed = token.trim()
232+
const trimmedToken = token.trim()
231233

232-
if (trimmed.startsWith(DOUBLE_QUOTE)) {
234+
if (trimmedToken.startsWith(DOUBLE_QUOTE)) {
233235
// Find the closing quote, accounting for escaped quotes
234-
const closingQuoteIndex = findClosingQuote(trimmed, 0)
236+
const closingQuoteIndex = findClosingQuote(trimmedToken, 0)
235237

236238
if (closingQuoteIndex === -1) {
237239
// No closing quote was found
238240
throw new SyntaxError('Unterminated string: missing closing quote')
239241
}
240242

241-
if (closingQuoteIndex !== trimmed.length - 1) {
243+
if (closingQuoteIndex !== trimmedToken.length - 1) {
242244
throw new SyntaxError('Unexpected characters after closing quote')
243245
}
244246

245-
const content = trimmed.slice(1, closingQuoteIndex)
247+
const content = trimmedToken.slice(1, closingQuoteIndex)
246248
return unescapeString(content)
247249
}
248250

249-
return trimmed
251+
return trimmedToken
250252
}
251253

252254
export function parseUnquotedKey(content: string, start: number): { key: string, end: number } {

packages/toon/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export function decode(input: string, options?: DecodeOptions): JsonValue {
3030
const scanResult = toParsedLines(input, resolvedOptions.indent, resolvedOptions.strict)
3131

3232
if (scanResult.lines.length === 0) {
33-
throw new TypeError('Cannot decode empty input: input must be a non-empty string')
33+
return {}
3434
}
3535

3636
const cursor = new LineCursor(scanResult.lines, scanResult.blankLines)

packages/toon/src/shared/literal-utils.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,6 @@ export function isNumericLiteral(token: string): boolean {
2323
}
2424

2525
// Check if it's a valid number
26-
const num = Number(token)
27-
return !Number.isNaN(num) && Number.isFinite(num)
26+
const numericValue = Number(token)
27+
return !Number.isNaN(numericValue) && Number.isFinite(numericValue)
2828
}

packages/toon/test/decode.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,25 @@ import arraysTabular from '@toon-format/spec/tests/fixtures/decode/arrays-tabula
55
import blankLines from '@toon-format/spec/tests/fixtures/decode/blank-lines.json'
66
import delimiters from '@toon-format/spec/tests/fixtures/decode/delimiters.json'
77
import indentationErrors from '@toon-format/spec/tests/fixtures/decode/indentation-errors.json'
8+
import numbers from '@toon-format/spec/tests/fixtures/decode/numbers.json'
89
import objects from '@toon-format/spec/tests/fixtures/decode/objects.json'
910
import primitives from '@toon-format/spec/tests/fixtures/decode/primitives.json'
11+
import rootForm from '@toon-format/spec/tests/fixtures/decode/root-form.json'
1012
import validationErrors from '@toon-format/spec/tests/fixtures/decode/validation-errors.json'
13+
import whitespace from '@toon-format/spec/tests/fixtures/decode/whitespace.json'
1114
import { describe, expect, it } from 'vitest'
1215
import { decode } from '../src/index'
1316

1417
const fixtureFiles = [
1518
primitives,
19+
numbers,
1620
objects,
1721
arraysPrimitive,
1822
arraysTabular,
1923
arraysNested,
2024
delimiters,
25+
whitespace,
26+
rootForm,
2127
validationErrors,
2228
indentationErrors,
2329
blankLines,

packages/toon/test/encode.test.ts

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,12 @@ import arraysObjects from '@toon-format/spec/tests/fixtures/encode/arrays-object
55
import arraysPrimitive from '@toon-format/spec/tests/fixtures/encode/arrays-primitive.json'
66
import arraysTabular from '@toon-format/spec/tests/fixtures/encode/arrays-tabular.json'
77
import delimiters from '@toon-format/spec/tests/fixtures/encode/delimiters.json'
8-
import normalization from '@toon-format/spec/tests/fixtures/encode/normalization.json'
98
import objects from '@toon-format/spec/tests/fixtures/encode/objects.json'
109
import options from '@toon-format/spec/tests/fixtures/encode/options.json'
1110
import primitives from '@toon-format/spec/tests/fixtures/encode/primitives.json'
1211
import whitespace from '@toon-format/spec/tests/fixtures/encode/whitespace.json'
1312
import { describe, expect, it } from 'vitest'
14-
import { decode, DEFAULT_DELIMITER, encode } from '../src/index'
13+
import { DEFAULT_DELIMITER, encode } from '../src/index'
1514

1615
const fixtureFiles = [
1716
primitives,
@@ -21,22 +20,10 @@ const fixtureFiles = [
2120
arraysNested,
2221
arraysObjects,
2322
delimiters,
24-
normalization,
2523
whitespace,
2624
options,
2725
] as Fixtures[]
2826

29-
// Special test for round-trip fidelity (not in JSON fixtures)
30-
describe('round-trip fidelity', () => {
31-
it('preserves precision for repeating decimals', () => {
32-
const value = 1 / 3
33-
const encodedValue = encode({ value })
34-
const decodedValue = decode(encodedValue)
35-
expect((decodedValue as Record<string, unknown>)?.value).toBe(value) // Round-trip fidelity
36-
expect(encodedValue).toContain('0.3333333333333333') // Default JS precision
37-
})
38-
})
39-
4027
for (const fixtures of fixtureFiles) {
4128
describe(fixtures.description, () => {
4229
for (const test of fixtures.tests) {
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/* eslint-disable test/prefer-lowercase-title */
2+
import { describe, expect, it } from 'vitest'
3+
import { decode, encode } from '../src/index'
4+
5+
describe('JavaScript-specific type normalization', () => {
6+
describe('BigInt normalization', () => {
7+
it('converts BigInt within safe integer range to number', () => {
8+
const result = encode(BigInt(123))
9+
expect(result).toBe('123')
10+
})
11+
12+
it('converts BigInt at MAX_SAFE_INTEGER boundary to number', () => {
13+
const result = encode(BigInt(Number.MAX_SAFE_INTEGER))
14+
expect(result).toBe('9007199254740991')
15+
})
16+
17+
it('converts BigInt beyond safe integer range to quoted string', () => {
18+
const result = encode(BigInt('9007199254740992'))
19+
expect(result).toBe('"9007199254740992"')
20+
})
21+
22+
it('converts large BigInt to quoted decimal string', () => {
23+
const result = encode(BigInt('12345678901234567890'))
24+
expect(result).toBe('"12345678901234567890"')
25+
})
26+
})
27+
28+
describe('Date normalization', () => {
29+
it('converts Date to ISO 8601 quoted string', () => {
30+
const result = encode(new Date('2025-01-01T00:00:00.000Z'))
31+
expect(result).toBe('"2025-01-01T00:00:00.000Z"')
32+
})
33+
34+
it('converts Date with milliseconds to ISO quoted string', () => {
35+
const result = encode(new Date('2025-11-05T12:34:56.789Z'))
36+
expect(result).toBe('"2025-11-05T12:34:56.789Z"')
37+
})
38+
})
39+
40+
describe('Set normalization', () => {
41+
it('converts Set to array', () => {
42+
const input = new Set(['a', 'b', 'c'])
43+
const encoded = encode(input)
44+
const decoded = decode(encoded)
45+
expect(decoded).toEqual(['a', 'b', 'c'])
46+
})
47+
48+
it('converts empty Set to empty array', () => {
49+
const result = encode(new Set())
50+
expect(result).toBe('[0]:')
51+
})
52+
})
53+
54+
describe('Map normalization', () => {
55+
it('converts Map to object', () => {
56+
const input = new Map([['key1', 'value1'], ['key2', 'value2']])
57+
const encoded = encode(input)
58+
const decoded = decode(encoded)
59+
expect(decoded).toEqual({ key1: 'value1', key2: 'value2' })
60+
})
61+
62+
it('converts empty Map to empty object', () => {
63+
const input = new Map()
64+
const result = encode(input)
65+
expect(result).toBe('')
66+
})
67+
68+
it('converts Map with numeric keys to object with quoted string keys', () => {
69+
const input = new Map([[1, 'one'], [2, 'two']])
70+
const result = encode(input)
71+
expect(result).toBe('"1": one\n"2": two')
72+
})
73+
})
74+
75+
describe('undefined, function, and Symbol normalization', () => {
76+
it('converts undefined to null', () => {
77+
const result = encode(undefined)
78+
expect(result).toBe('null')
79+
})
80+
81+
it('converts function to null', () => {
82+
const result = encode(() => {})
83+
expect(result).toBe('null')
84+
})
85+
86+
it('converts Symbol to null', () => {
87+
const result = encode(Symbol('test'))
88+
expect(result).toBe('null')
89+
})
90+
})
91+
92+
describe('NaN and Infinity normalization', () => {
93+
it('converts NaN to null', () => {
94+
const result = encode(Number.NaN)
95+
expect(result).toBe('null')
96+
})
97+
98+
it('converts Infinity to null', () => {
99+
const result = encode(Number.POSITIVE_INFINITY)
100+
expect(result).toBe('null')
101+
})
102+
103+
it('converts negative Infinity to null', () => {
104+
const result = encode(Number.NEGATIVE_INFINITY)
105+
expect(result).toBe('null')
106+
})
107+
})
108+
109+
describe('negative zero normalization', () => {
110+
it('normalizes -0 to 0', () => {
111+
const result = encode(-0)
112+
expect(result).toBe('0')
113+
})
114+
})
115+
})

pnpm-lock.yaml

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)