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