mirror of
https://github.com/voson-wang/toon.git
synced 2026-01-29 15:24:10 +08:00
fix: implement nested array indentation
This commit is contained in:
27
README.md
27
README.md
@@ -335,6 +335,30 @@ items[2]{sku,qty,price}:
|
|||||||
B2,1,14.5
|
B2,1,14.5
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Tabular formatting applies recursively:** nested arrays of objects (whether as object properties or inside list items) also use tabular format if they meet the same requirements.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
encode({
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
users: [
|
||||||
|
{ id: 1, name: 'Ada' },
|
||||||
|
{ id: 2, name: 'Bob' }
|
||||||
|
],
|
||||||
|
status: 'active'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
items[1]:
|
||||||
|
- users[2]{id,name}:
|
||||||
|
1,Ada
|
||||||
|
2,Bob
|
||||||
|
status: active
|
||||||
|
```
|
||||||
|
|
||||||
#### Mixed and Non-Uniform Arrays
|
#### Mixed and Non-Uniform Arrays
|
||||||
|
|
||||||
Arrays that don't meet the tabular requirements use list format:
|
Arrays that don't meet the tabular requirements use list format:
|
||||||
@@ -357,6 +381,9 @@ items[2]:
|
|||||||
extra: true
|
extra: true
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> **Nested array indentation:** When the first field of a list item is an array (primitive, tabular, or nested), its contents are indented two spaces under the header line, and subsequent fields of the same object appear at that same indentation level. This remains unambiguous because list items begin with `"- "`, tabular arrays declare a fixed row count in their header, and object fields contain `":"`.
|
||||||
|
|
||||||
#### Arrays of Arrays
|
#### Arrays of Arrays
|
||||||
|
|
||||||
When you have arrays containing primitive inner arrays:
|
When you have arrays containing primitive inner arrays:
|
||||||
|
|||||||
@@ -191,11 +191,7 @@ export function encodeArrayOfObjectsAsTabular(
|
|||||||
const headerStr = formatHeader(rows.length, { key: prefix, fields: header })
|
const headerStr = formatHeader(rows.length, { key: prefix, fields: header })
|
||||||
writer.push(depth, `${headerStr}`)
|
writer.push(depth, `${headerStr}`)
|
||||||
|
|
||||||
for (const row of rows) {
|
writeTabularRows(rows, header, writer, depth + 1, options)
|
||||||
const values = header.map(key => row[key])
|
|
||||||
const joinedValue = joinEncodedValues(values as JsonPrimitive[], options.delimiter)
|
|
||||||
writer.push(depth + 1, joinedValue)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function detectTabularHeader(rows: readonly JsonObject[]): string[] | undefined {
|
export function detectTabularHeader(rows: readonly JsonObject[]): string[] | undefined {
|
||||||
@@ -238,6 +234,20 @@ export function isTabularArray(
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function writeTabularRows(
|
||||||
|
rows: readonly JsonObject[],
|
||||||
|
header: readonly string[],
|
||||||
|
writer: LineWriter,
|
||||||
|
depth: Depth,
|
||||||
|
options: ResolvedEncodeOptions,
|
||||||
|
): void {
|
||||||
|
for (const row of rows) {
|
||||||
|
const values = header.map(key => row[key])
|
||||||
|
const joinedValue = joinEncodedValues(values as JsonPrimitive[], options.delimiter)
|
||||||
|
writer.push(depth, joinedValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
// #region Array of objects (expanded format)
|
// #region Array of objects (expanded format)
|
||||||
@@ -287,9 +297,46 @@ export function encodeObjectAsListItem(obj: JsonObject, writer: LineWriter, dept
|
|||||||
writer.push(depth, `${LIST_ITEM_PREFIX}${encodedKey}: ${encodePrimitive(firstValue, options.delimiter)}`)
|
writer.push(depth, `${LIST_ITEM_PREFIX}${encodedKey}: ${encodePrimitive(firstValue, options.delimiter)}`)
|
||||||
}
|
}
|
||||||
else if (isJsonArray(firstValue)) {
|
else if (isJsonArray(firstValue)) {
|
||||||
// For arrays, we need to put them on separate lines
|
if (isArrayOfPrimitives(firstValue)) {
|
||||||
writer.push(depth, `${LIST_ITEM_PREFIX}${encodedKey}[${firstValue.length}]:`)
|
// Inline format for primitive arrays
|
||||||
// ... handle array encoding (simplified for now)
|
const formatted = formatInlineArray(firstValue, options.delimiter, firstKey)
|
||||||
|
writer.push(depth, `${LIST_ITEM_PREFIX}${formatted}`)
|
||||||
|
}
|
||||||
|
else if (isArrayOfObjects(firstValue)) {
|
||||||
|
// Check if array of objects can use tabular format
|
||||||
|
const header = detectTabularHeader(firstValue)
|
||||||
|
if (header) {
|
||||||
|
// Tabular format for uniform arrays of objects
|
||||||
|
const headerStr = formatHeader(firstValue.length, { key: firstKey, fields: header })
|
||||||
|
writer.push(depth, `${LIST_ITEM_PREFIX}${headerStr}`)
|
||||||
|
writeTabularRows(firstValue, header, writer, depth + 1, options)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Fall back to list format for non-uniform arrays of objects
|
||||||
|
writer.push(depth, `${LIST_ITEM_PREFIX}${encodedKey}[${firstValue.length}]:`)
|
||||||
|
for (const item of firstValue) {
|
||||||
|
encodeObjectAsListItem(item, writer, depth + 1, options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Complex arrays on separate lines (array of arrays, etc.)
|
||||||
|
writer.push(depth, `${LIST_ITEM_PREFIX}${encodedKey}[${firstValue.length}]:`)
|
||||||
|
|
||||||
|
// Encode array contents at depth + 1
|
||||||
|
for (const item of firstValue) {
|
||||||
|
if (isJsonPrimitive(item)) {
|
||||||
|
writer.push(depth + 1, `${LIST_ITEM_PREFIX}${encodePrimitive(item, options.delimiter)}`)
|
||||||
|
}
|
||||||
|
else if (isJsonArray(item) && isArrayOfPrimitives(item)) {
|
||||||
|
const inline = formatInlineArray(item, options.delimiter)
|
||||||
|
writer.push(depth + 1, `${LIST_ITEM_PREFIX}${inline}`)
|
||||||
|
}
|
||||||
|
else if (isJsonObject(item)) {
|
||||||
|
encodeObjectAsListItem(item, writer, depth + 1, options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (isJsonObject(firstValue)) {
|
else if (isJsonObject(firstValue)) {
|
||||||
const nestedKeys = Object.keys(firstValue)
|
const nestedKeys = Object.keys(firstValue)
|
||||||
|
|||||||
@@ -285,6 +285,122 @@ describe('arrays of objects (tabular and list items)', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('preserves field order in list items', () => {
|
||||||
|
const obj = { items: [{ nums: [1, 2, 3], name: 'test' }] }
|
||||||
|
expect(encode(obj)).toBe(
|
||||||
|
'items[1]:\n'
|
||||||
|
+ ' - nums[3]: 1,2,3\n'
|
||||||
|
+ ' name: test',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('preserves field order when primitive appears first', () => {
|
||||||
|
const obj = { items: [{ name: 'test', nums: [1, 2, 3] }] }
|
||||||
|
expect(encode(obj)).toBe(
|
||||||
|
'items[1]:\n'
|
||||||
|
+ ' - name: test\n'
|
||||||
|
+ ' nums[3]: 1,2,3',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uses list format for objects containing arrays of arrays', () => {
|
||||||
|
const obj = {
|
||||||
|
items: [
|
||||||
|
{ matrix: [[1, 2], [3, 4]], name: 'grid' },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
expect(encode(obj)).toBe(
|
||||||
|
'items[1]:\n'
|
||||||
|
+ ' - matrix[2]:\n'
|
||||||
|
+ ' - [2]: 1,2\n'
|
||||||
|
+ ' - [2]: 3,4\n'
|
||||||
|
+ ' name: grid',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uses tabular format for nested uniform object arrays', () => {
|
||||||
|
const obj = {
|
||||||
|
items: [
|
||||||
|
{ users: [{ id: 1, name: 'Ada' }, { id: 2, name: 'Bob' }], status: 'active' },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
expect(encode(obj)).toBe(
|
||||||
|
'items[1]:\n'
|
||||||
|
+ ' - users[2]{id,name}:\n'
|
||||||
|
+ ' 1,Ada\n'
|
||||||
|
+ ' 2,Bob\n'
|
||||||
|
+ ' status: active',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uses list format for nested object arrays with mismatched keys', () => {
|
||||||
|
const obj = {
|
||||||
|
items: [
|
||||||
|
{ users: [{ id: 1, name: 'Ada' }, { id: 2 }], status: 'active' },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
expect(encode(obj)).toBe(
|
||||||
|
'items[1]:\n'
|
||||||
|
+ ' - users[2]:\n'
|
||||||
|
+ ' - id: 1\n'
|
||||||
|
+ ' name: Ada\n'
|
||||||
|
+ ' - id: 2\n'
|
||||||
|
+ ' status: active',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uses list format for objects with multiple array fields', () => {
|
||||||
|
const obj = { items: [{ nums: [1, 2], tags: ['a', 'b'], name: 'test' }] }
|
||||||
|
expect(encode(obj)).toBe(
|
||||||
|
'items[1]:\n'
|
||||||
|
+ ' - nums[2]: 1,2\n'
|
||||||
|
+ ' tags[2]: a,b\n'
|
||||||
|
+ ' name: test',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uses list format for objects with only array fields', () => {
|
||||||
|
const obj = { items: [{ nums: [1, 2, 3], tags: ['a', 'b'] }] }
|
||||||
|
expect(encode(obj)).toBe(
|
||||||
|
'items[1]:\n'
|
||||||
|
+ ' - nums[3]: 1,2,3\n'
|
||||||
|
+ ' tags[2]: a,b',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handles objects with empty arrays in list format', () => {
|
||||||
|
const obj = {
|
||||||
|
items: [
|
||||||
|
{ name: 'test', data: [] },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
expect(encode(obj)).toBe(
|
||||||
|
'items[1]:\n'
|
||||||
|
+ ' - name: test\n'
|
||||||
|
+ ' data[0]:',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('places first field of nested tabular arrays on hyphen line', () => {
|
||||||
|
const obj = { items: [{ users: [{ id: 1 }, { id: 2 }], note: 'x' }] }
|
||||||
|
expect(encode(obj)).toBe(
|
||||||
|
'items[1]:\n'
|
||||||
|
+ ' - users[2]{id}:\n'
|
||||||
|
+ ' 1\n'
|
||||||
|
+ ' 2\n'
|
||||||
|
+ ' note: x',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('places empty arrays on hyphen line when first', () => {
|
||||||
|
const obj = { items: [{ data: [], name: 'x' }] }
|
||||||
|
expect(encode(obj)).toBe(
|
||||||
|
'items[1]:\n'
|
||||||
|
+ ' - data[0]:\n'
|
||||||
|
+ ' name: x',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
it('uses field order from first object for tabular headers', () => {
|
it('uses field order from first object for tabular headers', () => {
|
||||||
const obj = {
|
const obj = {
|
||||||
items: [
|
items: [
|
||||||
|
|||||||
Reference in New Issue
Block a user