Skip to content

Commit 20aced1

Browse files
authored
refactor decode (#62)
* refactoring and writing some tests * add javadoc
1 parent 8a5f480 commit 20aced1

17 files changed

+2004
-1377
lines changed
Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
package dev.toonformat.jtoon.decoder;
2+
3+
import java.util.ArrayList;
4+
import java.util.Collections;
5+
import java.util.List;
6+
import java.util.regex.Matcher;
7+
8+
import static dev.toonformat.jtoon.util.Headers.ARRAY_HEADER_PATTERN;
9+
import static dev.toonformat.jtoon.util.Headers.TABULAR_HEADER_PATTERN;
10+
11+
/**
12+
* Handles decoding of TOON arrays to JSON format.
13+
*/
14+
public class ArrayDecoder {
15+
16+
private ArrayDecoder() {
17+
throw new UnsupportedOperationException("Utility class cannot be instantiated");
18+
}
19+
20+
/**
21+
* Parses array from header string and following lines.
22+
* Detects array type (tabular, list, or primitive) and routes accordingly.
23+
* @param header the header string to parse
24+
* @param depth the depth of array
25+
* @param context decode object in order to deal with lines, delimiter and options
26+
* @return parsed array with delimiter
27+
*/
28+
protected static List<Object> parseArray(String header, int depth, DecodeContext context) {
29+
String arrayDelimiter = extractDelimiterFromHeader(header, context);
30+
31+
return parseArrayWithDelimiter(header, depth, arrayDelimiter, context);
32+
}
33+
34+
/**
35+
* Extracts delimiter from array header.
36+
* Returns tab, pipe, or comma (default) based on header pattern.
37+
* @param header the header string to parse
38+
* @param context decode object in order to deal with lines, delimiter and options
39+
* @return extracted delimiter from header
40+
*/
41+
protected static String extractDelimiterFromHeader(String header, DecodeContext context) {
42+
Matcher matcher = ARRAY_HEADER_PATTERN.matcher(header);
43+
if (matcher.find() && matcher.groupCount() == 3) {
44+
String delimChar = matcher.group(3);
45+
if (delimChar != null) {
46+
if ("\t".equals(delimChar)) {
47+
return "\t";
48+
} else if ("|".equals(delimChar)) {
49+
return "|";
50+
}
51+
}
52+
}
53+
// Default to comma
54+
return context.delimiter;
55+
}
56+
57+
/**
58+
* Parses array from header string and following lines with a specific
59+
* delimiter.
60+
* Detects array type (tabular, list, or primitive) and routes accordingly.
61+
* @param header the header string to parse
62+
* @param depth depth of array
63+
* @param arrayDelimiter array delimiter
64+
* @param context decode object in order to deal with lines, delimiter and options
65+
* @return parsed array
66+
*/
67+
protected static List<Object> parseArrayWithDelimiter(String header, int depth, String arrayDelimiter, DecodeContext context) {
68+
Matcher tabularMatcher = TABULAR_HEADER_PATTERN.matcher(header);
69+
Matcher arrayMatcher = ARRAY_HEADER_PATTERN.matcher(header);
70+
71+
if (tabularMatcher.find()) {
72+
return TabularArrayDecoder.parseTabularArray(header, depth, arrayDelimiter, context);
73+
}
74+
75+
if (arrayMatcher.find()) {
76+
int headerEndIdx = arrayMatcher.end();
77+
String afterHeader = header.substring(headerEndIdx).trim();
78+
79+
if (afterHeader.startsWith(":")) {
80+
String inlineContent = afterHeader.substring(1).trim();
81+
82+
if (!inlineContent.isEmpty()) {
83+
List<Object> result = parseArrayValues(inlineContent, arrayDelimiter);
84+
validateArrayLength(header, result.size());
85+
context.currentLine++;
86+
return result;
87+
}
88+
}
89+
90+
context.currentLine++;
91+
if (context.currentLine < context.lines.length) {
92+
String nextLine = context.lines[context.currentLine];
93+
int nextDepth = DecodeHelper.getDepth(nextLine, context);
94+
String nextContent = nextLine.substring(nextDepth * context.options.indent());
95+
96+
if (nextDepth <= depth) {
97+
// The next line is not a child of this array,
98+
// the array is empty
99+
validateArrayLength(header, 0);
100+
return Collections.emptyList();
101+
}
102+
103+
if (nextContent.startsWith("- ")) {
104+
context.currentLine--;
105+
return parseListArray(depth, header, context);
106+
} else {
107+
context.currentLine++;
108+
List<Object> result = parseArrayValues(nextContent, arrayDelimiter);
109+
validateArrayLength(header, result.size());
110+
return result;
111+
}
112+
}
113+
List<Object> empty = new ArrayList<>();
114+
validateArrayLength(header, 0);
115+
return empty;
116+
}
117+
118+
if (context.options.strict()) {
119+
throw new IllegalArgumentException("Invalid array header: " + header);
120+
}
121+
return Collections.emptyList();
122+
}
123+
124+
/**
125+
* Validates array length if declared in header.
126+
* @param header header
127+
* @param actualLength actual length
128+
*/
129+
protected static void validateArrayLength(String header, int actualLength) {
130+
Integer declaredLength = extractLengthFromHeader(header);
131+
if (declaredLength != null && declaredLength != actualLength) {
132+
throw new IllegalArgumentException(
133+
String.format("Array length mismatch: declared %d, found %d", declaredLength, actualLength));
134+
}
135+
}
136+
137+
/**
138+
* Extracts declared length from array header.
139+
* Returns the number specified in [n] or null if not found.
140+
*/
141+
private static Integer extractLengthFromHeader(String header) {
142+
Matcher matcher = ARRAY_HEADER_PATTERN.matcher(header);
143+
if (matcher.find()) {
144+
try {
145+
return Integer.parseInt(matcher.group(2));
146+
} catch (NumberFormatException e) {
147+
return null;
148+
}
149+
}
150+
return null;
151+
}
152+
153+
/**
154+
* Parses array values from a delimiter-separated string.
155+
* @param values the values string to parse
156+
* @param arrayDelimiter array delimiter
157+
* @return parsed array values
158+
*/
159+
protected static List<Object> parseArrayValues(String values, String arrayDelimiter) {
160+
List<Object> result = new ArrayList<>();
161+
List<String> rawValues = parseDelimitedValues(values, arrayDelimiter);
162+
for (String value : rawValues) {
163+
result.add(PrimitiveDecoder.parse(value));
164+
}
165+
return result;
166+
}
167+
168+
/**
169+
* Splits a string by delimiter, respecting quoted sections.
170+
* Whitespace around delimiters is tolerated and trimmed.
171+
* @param input the input string to parse
172+
* @param arrayDelimiter array delimiter
173+
* @return parsed delimited values
174+
*/
175+
protected static List<String> parseDelimitedValues(String input, String arrayDelimiter) {
176+
List<String> result = new ArrayList<>();
177+
StringBuilder current = new StringBuilder();
178+
boolean inQuotes = false;
179+
boolean escaped = false;
180+
char delimChar = arrayDelimiter.charAt(0);
181+
182+
int i = 0;
183+
while (i < input.length()) {
184+
char c = input.charAt(i);
185+
186+
if (escaped) {
187+
current.append(c);
188+
escaped = false;
189+
i++;
190+
} else if (c == '\\') {
191+
current.append(c);
192+
escaped = true;
193+
i++;
194+
} else if (c == '"') {
195+
current.append(c);
196+
inQuotes = !inQuotes;
197+
i++;
198+
} else if (c == delimChar && !inQuotes) {
199+
// Found delimiter - add current value (trimmed) and reset
200+
String value = current.toString().trim();
201+
result.add(value);
202+
current = new StringBuilder();
203+
// Skip whitespace after delimiter
204+
do {
205+
i++;
206+
} while (i < input.length() && Character.isWhitespace(input.charAt(i)));
207+
} else {
208+
current.append(c);
209+
i++;
210+
}
211+
}
212+
213+
// Add final value
214+
if (!current.isEmpty() || input.endsWith(arrayDelimiter)) {
215+
result.add(current.toString().trim());
216+
}
217+
218+
return result;
219+
}
220+
221+
/**
222+
* Parses list array format where items are prefixed with "- ".
223+
* Example: items[2]:\n - item1\n - item2
224+
*/
225+
private static List<Object> parseListArray(int depth, String header, DecodeContext context) {
226+
List<Object> result = new ArrayList<>();
227+
context.currentLine++;
228+
229+
boolean shouldContinue = true;
230+
while (shouldContinue && context.currentLine < context.lines.length) {
231+
String line = context.lines[context.currentLine];
232+
233+
if (DecodeHelper.isBlankLine(line)) {
234+
if (handleBlankLineInListArray(depth, context)) {
235+
shouldContinue = false;
236+
}
237+
} else {
238+
int lineDepth = DecodeHelper.getDepth(line, context);
239+
if (shouldTerminateListArray(lineDepth, depth, line, context)) {
240+
shouldContinue = false;
241+
} else {
242+
ListItemDecoder.processListArrayItem(line, lineDepth, depth, result, context);
243+
}
244+
}
245+
}
246+
247+
if (header != null) {
248+
ArrayDecoder.validateArrayLength(header, result.size());
249+
}
250+
return result;
251+
}
252+
253+
/**
254+
* Handles blank line processing in list array.
255+
* Returns true if array should terminate, false if line should be skipped.
256+
*/
257+
private static boolean handleBlankLineInListArray(int depth, DecodeContext context) {
258+
int nextNonBlankLine = DecodeHelper.findNextNonBlankLine(context.currentLine + 1, context);
259+
260+
if (nextNonBlankLine >= context.lines.length) {
261+
return true; // End of file - terminate array
262+
}
263+
264+
int nextDepth = DecodeHelper.getDepth(context.lines[nextNonBlankLine], context);
265+
if (nextDepth <= depth) {
266+
return true; // Blank line is outside array - terminate
267+
}
268+
269+
// Blank line is inside array
270+
if (context.options.strict()) {
271+
throw new IllegalArgumentException("Blank line inside list array at line " + (context.currentLine + 1));
272+
}
273+
// In non-strict mode, skip blank lines
274+
context.currentLine++;
275+
return false;
276+
}
277+
278+
/**
279+
* Determines if list array parsing should terminate based online depth.
280+
* Returns true if array should terminate, false otherwise.
281+
*/
282+
private static boolean shouldTerminateListArray(int lineDepth, int depth, String line, DecodeContext context) {
283+
if (lineDepth < depth + 1) {
284+
return true; // Line depth is less than expected - terminate
285+
}
286+
// Also terminate if line is at expected depth but doesn't start with "-"
287+
if (lineDepth == depth + 1) {
288+
String content = line.substring((depth + 1) * context.options.indent());
289+
return !content.startsWith("-"); // Not an array item - terminate
290+
}
291+
return false;
292+
}
293+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package dev.toonformat.jtoon.decoder;
2+
3+
import dev.toonformat.jtoon.DecodeOptions;
4+
5+
/**
6+
* Deals with the main attributes used to decode TOON to JSON format
7+
*/
8+
public class DecodeContext {
9+
10+
protected String[] lines;
11+
protected DecodeOptions options;
12+
protected String delimiter;
13+
protected int currentLine = 0;
14+
15+
public DecodeContext () {}
16+
}

0 commit comments

Comments
 (0)