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
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
* text=auto eol=lf
*.cmd text eol=crlf
8 changes: 8 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 4
64 changes: 41 additions & 23 deletions src/main/java/dev/toonformat/jtoon/decoder/ValueDecoder.java
Original file line number Diff line number Diff line change
Expand Up @@ -391,8 +391,21 @@ private List<Object> parseTabularArray(String header, int depth, String arrayDel
List<Object> result = new ArrayList<>();
currentLine++;

// Determine the expected row depth dynamically from the first non-blank line
int expectedRowDepth;
if (currentLine < lines.length) {
int nextNonBlankLine = findNextNonBlankLine(currentLine);
if (nextNonBlankLine < lines.length) {
expectedRowDepth = getDepth(lines[nextNonBlankLine]);
} else {
expectedRowDepth = depth + 1;
}
} else {
expectedRowDepth = depth + 1;
}

while (currentLine < lines.length) {
if (!processTabularArrayLine(depth, keys, arrayDelimiter, result)) {
if (!processTabularArrayLine(expectedRowDepth, keys, arrayDelimiter, result)) {
break;
}
}
Expand All @@ -403,42 +416,44 @@ private List<Object> parseTabularArray(String header, int depth, String arrayDel

/**
* Processes a single line in a tabular array.
* Returns true if parsing should continue, false if array should terminate.
* Returns true if parsing should continue, false if an array should terminate.
*/
private boolean processTabularArrayLine(int depth, List<String> keys, String arrayDelimiter,
private boolean processTabularArrayLine(int expectedRowDepth, List<String> keys, String arrayDelimiter,
List<Object> result) {
String line = lines[currentLine];

if (isBlankLine(line)) {
return !handleBlankLineInTabularArray(depth);
return !handleBlankLineInTabularArray(expectedRowDepth);
}

int lineDepth = getDepth(line);
if (shouldTerminateTabularArray(line, lineDepth, depth)) {
if (shouldTerminateTabularArray(line, lineDepth, expectedRowDepth)) {
return false;
}

if (processTabularRow(line, lineDepth, depth, keys, arrayDelimiter, result)) {
if (processTabularRow(line, lineDepth, expectedRowDepth, keys, arrayDelimiter, result)) {
currentLine++;
}
return true;
}

/**
* Handles blank line processing in tabular array.
* Returns true if array should terminate, false if line should be skipped.
* Handles blank line processing in a tabular array.
* Returns true if an array should terminate, false if a line should be skipped.
*/
private boolean handleBlankLineInTabularArray(int depth) {
private boolean handleBlankLineInTabularArray(int expectedRowDepth) {
int nextNonBlankLine = findNextNonBlankLine(currentLine + 1);

if (nextNonBlankLine < lines.length) {
int nextDepth = getDepth(lines[nextNonBlankLine]);
if (nextDepth <= depth) {
return true; // Blank line is outside array - terminate
// Header depth is one level above the expected row depth
int headerDepth = expectedRowDepth - 1;
if (nextDepth <= headerDepth) {
return true;
}
}

// Blank line is inside array
// Blank line is inside the array
if (options.strict()) {
throw new IllegalArgumentException(
"Blank line inside tabular array at line " + (currentLine + 1));
Expand All @@ -463,10 +478,13 @@ private int findNextNonBlankLine(int startIndex) {
* Determines if tabular array parsing should terminate based online depth.
* Returns true if array should terminate, false otherwise.
*/
private boolean shouldTerminateTabularArray(String line, int lineDepth, int depth) {
if (lineDepth < depth + 1) {
if (lineDepth == depth) {
String content = line.substring(depth * options.indent());
private boolean shouldTerminateTabularArray(String line, int lineDepth, int expectedRowDepth) {
// Header depth is one level above expected row depth
int headerDepth = expectedRowDepth - 1;

if (lineDepth < expectedRowDepth) {
if (lineDepth == headerDepth) {
String content = line.substring(headerDepth * options.indent());
int colonIdx = findUnquotedColon(content);
if (colonIdx > 0) {
return true; // Key-value pair at same depth - terminate array
Expand All @@ -476,8 +494,8 @@ private boolean shouldTerminateTabularArray(String line, int lineDepth, int dept
}

// Check for key-value pair at expected row depth
if (lineDepth == depth + 1) {
String rowContent = line.substring((depth + 1) * options.indent());
if (lineDepth == expectedRowDepth) {
String rowContent = line.substring(expectedRowDepth * options.indent());
int colonIdx = findUnquotedColon(rowContent);
return colonIdx > 0; // Key-value pair at same depth as rows - terminate array
}
Expand All @@ -490,14 +508,14 @@ private boolean shouldTerminateTabularArray(String line, int lineDepth, int dept
* Returns true if line was processed and currentLine should be incremented,
* false otherwise.
*/
private boolean processTabularRow(String line, int lineDepth, int depth, List<String> keys,
private boolean processTabularRow(String line, int lineDepth, int expectedRowDepth, List<String> keys,
String arrayDelimiter, List<Object> result) {
if (lineDepth == depth + 1) {
String rowContent = line.substring((depth + 1) * options.indent());
if (lineDepth == expectedRowDepth) {
String rowContent = line.substring(expectedRowDepth * options.indent());
Map<String, Object> row = parseTabularRow(rowContent, keys, arrayDelimiter);
result.add(row);
return true;
} else if (lineDepth > depth + 1) {
} else if (lineDepth > expectedRowDepth) {
// Line is deeper than expected - might be nested content, skip it
currentLine++;
return false;
Expand Down Expand Up @@ -636,7 +654,7 @@ private Object parseListItem(String content, int depth) {

// For nested arrays in list items, default to comma delimiter if not specified
String nestedArrayDelimiter = extractDelimiterFromHeader(arrayHeader);
var arrayValue = parseArrayWithDelimiter(arrayHeader, depth + 1, nestedArrayDelimiter);
List<Object> arrayValue = parseArrayWithDelimiter(arrayHeader, depth + 2, nestedArrayDelimiter);

Map<String, Object> item = new LinkedHashMap<>();
item.put(key, arrayValue);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ private ListItemEncoder() {

/**
* Encodes an object as a list item.
* First key-value appears on the "- " line, remaining fields are indented.
* The first key-value appears on the "- " line, remaining fields are indented.
*
* @param obj The object to encode
* @param writer LineWriter for output
Expand Down Expand Up @@ -101,13 +101,13 @@ private static void encodeFirstArrayAsObjects(String key, String encodedKey, Arr
options.delimiter().getValue(), options.lengthMarker());
writer.push(depth, LIST_ITEM_PREFIX + headerStr);
// Write just the rows, header was already written above
TabularArrayEncoder.writeTabularRows(arrayValue, header, writer, depth + 1, options);
TabularArrayEncoder.writeTabularRows(arrayValue, header, writer, depth + 2, options);
} else {
writer.push(depth,
LIST_ITEM_PREFIX + encodedKey + OPEN_BRACKET + arrayValue.size() + CLOSE_BRACKET + COLON);
for (JsonNode item : arrayValue) {
if (item.isObject()) {
encodeObjectAsListItem((ObjectNode) item, writer, depth + 1, options);
encodeObjectAsListItem((ObjectNode) item, writer, depth + 2, options);
}
}
}
Expand All @@ -119,14 +119,14 @@ private static void encodeFirstArrayAsComplex(String encodedKey, ArrayNode array

for (JsonNode item : arrayValue) {
if (item.isValueNode()) {
writer.push(depth + 1, LIST_ITEM_PREFIX
writer.push(depth + 2, LIST_ITEM_PREFIX
+ PrimitiveEncoder.encodePrimitive(item, options.delimiter().getValue()));
} else if (item.isArray() && ArrayEncoder.isArrayOfPrimitives(item)) {
String inline = ArrayEncoder.formatInlineArray((ArrayNode) item, options.delimiter().getValue(), null,
options.lengthMarker());
writer.push(depth + 1, LIST_ITEM_PREFIX + inline);
writer.push(depth + 2, LIST_ITEM_PREFIX + inline);
} else if (item.isObject()) {
encodeObjectAsListItem((ObjectNode) item, writer, depth + 1, options);
encodeObjectAsListItem((ObjectNode) item, writer, depth + 2, options);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ public static void encodeKeyValuePair(String key,
* @param remainingDepth the depth that remind to the limit
* @return EncodeOptions changes for Case 2
*/
private static EncodeOptions flatten(String key, Flatten.FoldResult foldResult, LineWriter writer, int depth, EncodeOptions options,Set<String> rootLiteralKeys, String pathPrefix, Set<String> blockedKeys, int remainingDepth) {
private static EncodeOptions flatten(String key, Flatten.FoldResult foldResult, LineWriter writer, int depth, EncodeOptions options, Set<String> rootLiteralKeys, String pathPrefix, Set<String> blockedKeys, int remainingDepth) {
String foldedKey = foldResult.foldedKey();

// prevent second folding pass
Expand Down
18 changes: 9 additions & 9 deletions src/test/java/dev/toonformat/jtoon/JToonTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -450,8 +450,8 @@ void usesListForArrayOfArrays() {
"""
items[1]:
- matrix[2]:
- [2]: 1,2
- [2]: 3,4
- [2]: 1,2
- [2]: 3,4
name: grid""",
encode(obj));
}
Expand All @@ -467,8 +467,8 @@ void usesTabularForNestedUniformArrays() {
"""
items[1]:
- users[2]{id,name}:
1,Ada
2,Bob
1,Ada
2,Bob
status: active""",
encode(obj));
}
Expand All @@ -483,9 +483,9 @@ void usesListForMismatchedKeys() {
"""
items[1]:
- users[2]:
- id: 1
name: Ada
- id: 2
- id: 1
name: Ada
- id: 2
status: active""",
encode(obj));
}
Expand Down Expand Up @@ -538,8 +538,8 @@ void placesTabularOnHyphenLine() {
"""
items[1]:
- users[2]{id}:
1
2
1
2
note: x""",
encode(obj));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dev.toonformat.jtoon.decoder;

import dev.toonformat.jtoon.DecodeOptions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -29,4 +30,30 @@ void throwsOnConstructor() throws NoSuchMethodException {
assertInstanceOf(UnsupportedOperationException.class, cause);
assertEquals("Utility class cannot be instantiated", cause.getMessage());
}

@Test
@DisplayName("parses list items whose first field is a tabular array")
void decodeTabularArray() {
// Given
String input = "items[1]:\n - users[2]{id,name}:\n 1,Ada\n 2,Bob\n status: active";

// When
String result = ValueDecoder.decodeToJson(input, DecodeOptions.DEFAULT);

// Then
assertEquals("{\"items\":[{\"users\":[{\"id\":1,\"name\":\"Ada\"},{\"id\":2,\"name\":\"Bob\"}],\"status\":\"active\"}]}", result);
}

@Test
@DisplayName("parses arrays of arrays within objects")
void decodeArraysOfArraysWithinObjects() {
// Given
String input = "items[1]:\n - matrix[2]:\n - [2]: 1,2\n - [2]: 3,4\n name: grid";

// When
String result = ValueDecoder.decodeToJson(input, DecodeOptions.DEFAULT);

// Then
assertEquals("{\"items\":[{\"matrix\":[[1,2],[3,4]],\"name\":\"grid\"}]}", result);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@
import dev.toonformat.jtoon.EncodeOptions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.node.ArrayNode;
import tools.jackson.databind.node.JsonNodeFactory;
import tools.jackson.databind.node.ObjectNode;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.HashSet;
import java.util.Set;

import static org.junit.jupiter.api.Assertions.*;

Expand Down Expand Up @@ -111,4 +114,56 @@ void givenMultipleFields_whenEncoded_thenRemainingFieldsAreDelegated() {
assertEquals("- a: 1\n" +
" b: 2", writer.toString());
}

@Test
void usesTabularFormatForNestedUniformObjectArrays() {
// Given
String json = "[\n" +
" { \"users\": [{ \"id\": 1, \"name\": \"Ada\" }, { \"id\": 2, \"name\": \"Bob\" }], \"status\": \"active\" }\n" +
" ]";
ArrayNode node = (ArrayNode) new ObjectMapper().readTree(json);

EncodeOptions options = EncodeOptions.DEFAULT;
LineWriter writer = new LineWriter(options.indent());
Set<String> rootKeys = new HashSet<>();

// When
ArrayEncoder.encodeArray("items",node, writer, 0, options);

// Then
String expected = String.join("\n",
"items[1]:",
" - users[2]{id,name}:",
" 1,Ada",
" 2,Bob",
" status: active");
assertEquals(expected, writer.toString());
}

@Test
void usesListFormatForNestedObjectArraysWithMismatchedKeys() {
// Given
String json = "[\n" +
" { \"users\": [{ \"id\": 1, \"name\": \"Ada\" }, { \"id\": 2 }], \"status\": \"active\" }\n" +
" ]";
ArrayNode node = (ArrayNode) new ObjectMapper().readTree(json);

EncodeOptions options = EncodeOptions.DEFAULT;
LineWriter writer = new LineWriter(options.indent());
Set<String> rootKeys = new HashSet<>();

// When
ArrayEncoder.encodeArray("items", node, writer, 0, options);


// Then
String expected = String.join("\n",
"items[1]:",
" - users[2]:",
" - id: 1",
" name: Ada",
" - id: 2",
" status: active");
assertEquals(expected, writer.toString());
}
}
Loading
Loading