mirror of
https://github.com/voson-wang/toon.git
synced 2026-01-29 15:24:10 +08:00
feat: minor fixes for spec v1.4 compliance
This commit is contained in:
@@ -38,6 +38,6 @@
|
||||
"test": "vitest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@toon-format/spec": "^1.3.3"
|
||||
"@toon-format/spec": "^1.4.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,7 +219,9 @@ export function parsePrimitiveToken(token: string): JsonPrimitive {
|
||||
|
||||
// Numeric literal
|
||||
if (isNumericLiteral(trimmed)) {
|
||||
return Number.parseFloat(trimmed)
|
||||
const parsedNumber = Number.parseFloat(trimmed)
|
||||
// Normalize negative zero to positive zero
|
||||
return Object.is(parsedNumber, -0) ? 0 : parsedNumber
|
||||
}
|
||||
|
||||
// Unquoted string
|
||||
@@ -227,26 +229,26 @@ export function parsePrimitiveToken(token: string): JsonPrimitive {
|
||||
}
|
||||
|
||||
export function parseStringLiteral(token: string): string {
|
||||
const trimmed = token.trim()
|
||||
const trimmedToken = token.trim()
|
||||
|
||||
if (trimmed.startsWith(DOUBLE_QUOTE)) {
|
||||
if (trimmedToken.startsWith(DOUBLE_QUOTE)) {
|
||||
// Find the closing quote, accounting for escaped quotes
|
||||
const closingQuoteIndex = findClosingQuote(trimmed, 0)
|
||||
const closingQuoteIndex = findClosingQuote(trimmedToken, 0)
|
||||
|
||||
if (closingQuoteIndex === -1) {
|
||||
// No closing quote was found
|
||||
throw new SyntaxError('Unterminated string: missing closing quote')
|
||||
}
|
||||
|
||||
if (closingQuoteIndex !== trimmed.length - 1) {
|
||||
if (closingQuoteIndex !== trimmedToken.length - 1) {
|
||||
throw new SyntaxError('Unexpected characters after closing quote')
|
||||
}
|
||||
|
||||
const content = trimmed.slice(1, closingQuoteIndex)
|
||||
const content = trimmedToken.slice(1, closingQuoteIndex)
|
||||
return unescapeString(content)
|
||||
}
|
||||
|
||||
return trimmed
|
||||
return trimmedToken
|
||||
}
|
||||
|
||||
export function parseUnquotedKey(content: string, start: number): { key: string, end: number } {
|
||||
|
||||
@@ -30,7 +30,7 @@ export function decode(input: string, options?: DecodeOptions): JsonValue {
|
||||
const scanResult = toParsedLines(input, resolvedOptions.indent, resolvedOptions.strict)
|
||||
|
||||
if (scanResult.lines.length === 0) {
|
||||
throw new TypeError('Cannot decode empty input: input must be a non-empty string')
|
||||
return {}
|
||||
}
|
||||
|
||||
const cursor = new LineCursor(scanResult.lines, scanResult.blankLines)
|
||||
|
||||
@@ -23,6 +23,6 @@ export function isNumericLiteral(token: string): boolean {
|
||||
}
|
||||
|
||||
// Check if it's a valid number
|
||||
const num = Number(token)
|
||||
return !Number.isNaN(num) && Number.isFinite(num)
|
||||
const numericValue = Number(token)
|
||||
return !Number.isNaN(numericValue) && Number.isFinite(numericValue)
|
||||
}
|
||||
|
||||
@@ -5,19 +5,25 @@ import arraysTabular from '@toon-format/spec/tests/fixtures/decode/arrays-tabula
|
||||
import blankLines from '@toon-format/spec/tests/fixtures/decode/blank-lines.json'
|
||||
import delimiters from '@toon-format/spec/tests/fixtures/decode/delimiters.json'
|
||||
import indentationErrors from '@toon-format/spec/tests/fixtures/decode/indentation-errors.json'
|
||||
import numbers from '@toon-format/spec/tests/fixtures/decode/numbers.json'
|
||||
import objects from '@toon-format/spec/tests/fixtures/decode/objects.json'
|
||||
import primitives from '@toon-format/spec/tests/fixtures/decode/primitives.json'
|
||||
import rootForm from '@toon-format/spec/tests/fixtures/decode/root-form.json'
|
||||
import validationErrors from '@toon-format/spec/tests/fixtures/decode/validation-errors.json'
|
||||
import whitespace from '@toon-format/spec/tests/fixtures/decode/whitespace.json'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { decode } from '../src/index'
|
||||
|
||||
const fixtureFiles = [
|
||||
primitives,
|
||||
numbers,
|
||||
objects,
|
||||
arraysPrimitive,
|
||||
arraysTabular,
|
||||
arraysNested,
|
||||
delimiters,
|
||||
whitespace,
|
||||
rootForm,
|
||||
validationErrors,
|
||||
indentationErrors,
|
||||
blankLines,
|
||||
|
||||
@@ -5,13 +5,12 @@ import arraysObjects from '@toon-format/spec/tests/fixtures/encode/arrays-object
|
||||
import arraysPrimitive from '@toon-format/spec/tests/fixtures/encode/arrays-primitive.json'
|
||||
import arraysTabular from '@toon-format/spec/tests/fixtures/encode/arrays-tabular.json'
|
||||
import delimiters from '@toon-format/spec/tests/fixtures/encode/delimiters.json'
|
||||
import normalization from '@toon-format/spec/tests/fixtures/encode/normalization.json'
|
||||
import objects from '@toon-format/spec/tests/fixtures/encode/objects.json'
|
||||
import options from '@toon-format/spec/tests/fixtures/encode/options.json'
|
||||
import primitives from '@toon-format/spec/tests/fixtures/encode/primitives.json'
|
||||
import whitespace from '@toon-format/spec/tests/fixtures/encode/whitespace.json'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { decode, DEFAULT_DELIMITER, encode } from '../src/index'
|
||||
import { DEFAULT_DELIMITER, encode } from '../src/index'
|
||||
|
||||
const fixtureFiles = [
|
||||
primitives,
|
||||
@@ -21,22 +20,10 @@ const fixtureFiles = [
|
||||
arraysNested,
|
||||
arraysObjects,
|
||||
delimiters,
|
||||
normalization,
|
||||
whitespace,
|
||||
options,
|
||||
] as Fixtures[]
|
||||
|
||||
// Special test for round-trip fidelity (not in JSON fixtures)
|
||||
describe('round-trip fidelity', () => {
|
||||
it('preserves precision for repeating decimals', () => {
|
||||
const value = 1 / 3
|
||||
const encodedValue = encode({ value })
|
||||
const decodedValue = decode(encodedValue)
|
||||
expect((decodedValue as Record<string, unknown>)?.value).toBe(value) // Round-trip fidelity
|
||||
expect(encodedValue).toContain('0.3333333333333333') // Default JS precision
|
||||
})
|
||||
})
|
||||
|
||||
for (const fixtures of fixtureFiles) {
|
||||
describe(fixtures.description, () => {
|
||||
for (const test of fixtures.tests) {
|
||||
|
||||
115
packages/toon/test/normalization.test.ts
Normal file
115
packages/toon/test/normalization.test.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
/* eslint-disable test/prefer-lowercase-title */
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { decode, encode } from '../src/index'
|
||||
|
||||
describe('JavaScript-specific type normalization', () => {
|
||||
describe('BigInt normalization', () => {
|
||||
it('converts BigInt within safe integer range to number', () => {
|
||||
const result = encode(BigInt(123))
|
||||
expect(result).toBe('123')
|
||||
})
|
||||
|
||||
it('converts BigInt at MAX_SAFE_INTEGER boundary to number', () => {
|
||||
const result = encode(BigInt(Number.MAX_SAFE_INTEGER))
|
||||
expect(result).toBe('9007199254740991')
|
||||
})
|
||||
|
||||
it('converts BigInt beyond safe integer range to quoted string', () => {
|
||||
const result = encode(BigInt('9007199254740992'))
|
||||
expect(result).toBe('"9007199254740992"')
|
||||
})
|
||||
|
||||
it('converts large BigInt to quoted decimal string', () => {
|
||||
const result = encode(BigInt('12345678901234567890'))
|
||||
expect(result).toBe('"12345678901234567890"')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Date normalization', () => {
|
||||
it('converts Date to ISO 8601 quoted string', () => {
|
||||
const result = encode(new Date('2025-01-01T00:00:00.000Z'))
|
||||
expect(result).toBe('"2025-01-01T00:00:00.000Z"')
|
||||
})
|
||||
|
||||
it('converts Date with milliseconds to ISO quoted string', () => {
|
||||
const result = encode(new Date('2025-11-05T12:34:56.789Z'))
|
||||
expect(result).toBe('"2025-11-05T12:34:56.789Z"')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Set normalization', () => {
|
||||
it('converts Set to array', () => {
|
||||
const input = new Set(['a', 'b', 'c'])
|
||||
const encoded = encode(input)
|
||||
const decoded = decode(encoded)
|
||||
expect(decoded).toEqual(['a', 'b', 'c'])
|
||||
})
|
||||
|
||||
it('converts empty Set to empty array', () => {
|
||||
const result = encode(new Set())
|
||||
expect(result).toBe('[0]:')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Map normalization', () => {
|
||||
it('converts Map to object', () => {
|
||||
const input = new Map([['key1', 'value1'], ['key2', 'value2']])
|
||||
const encoded = encode(input)
|
||||
const decoded = decode(encoded)
|
||||
expect(decoded).toEqual({ key1: 'value1', key2: 'value2' })
|
||||
})
|
||||
|
||||
it('converts empty Map to empty object', () => {
|
||||
const input = new Map()
|
||||
const result = encode(input)
|
||||
expect(result).toBe('')
|
||||
})
|
||||
|
||||
it('converts Map with numeric keys to object with quoted string keys', () => {
|
||||
const input = new Map([[1, 'one'], [2, 'two']])
|
||||
const result = encode(input)
|
||||
expect(result).toBe('"1": one\n"2": two')
|
||||
})
|
||||
})
|
||||
|
||||
describe('undefined, function, and Symbol normalization', () => {
|
||||
it('converts undefined to null', () => {
|
||||
const result = encode(undefined)
|
||||
expect(result).toBe('null')
|
||||
})
|
||||
|
||||
it('converts function to null', () => {
|
||||
const result = encode(() => {})
|
||||
expect(result).toBe('null')
|
||||
})
|
||||
|
||||
it('converts Symbol to null', () => {
|
||||
const result = encode(Symbol('test'))
|
||||
expect(result).toBe('null')
|
||||
})
|
||||
})
|
||||
|
||||
describe('NaN and Infinity normalization', () => {
|
||||
it('converts NaN to null', () => {
|
||||
const result = encode(Number.NaN)
|
||||
expect(result).toBe('null')
|
||||
})
|
||||
|
||||
it('converts Infinity to null', () => {
|
||||
const result = encode(Number.POSITIVE_INFINITY)
|
||||
expect(result).toBe('null')
|
||||
})
|
||||
|
||||
it('converts negative Infinity to null', () => {
|
||||
const result = encode(Number.NEGATIVE_INFINITY)
|
||||
expect(result).toBe('null')
|
||||
})
|
||||
})
|
||||
|
||||
describe('negative zero normalization', () => {
|
||||
it('normalizes -0 to 0', () => {
|
||||
const result = encode(-0)
|
||||
expect(result).toBe('0')
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user