-
Notifications
You must be signed in to change notification settings - Fork 17
refactor decode #62
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
refactor decode #62
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
293 changes: 293 additions & 0 deletions
293
src/main/java/dev/toonformat/jtoon/decoder/ArrayDecoder.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,293 @@ | ||
| package dev.toonformat.jtoon.decoder; | ||
|
|
||
| import java.util.ArrayList; | ||
| import java.util.Collections; | ||
| import java.util.List; | ||
| import java.util.regex.Matcher; | ||
|
|
||
| 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 class ArrayDecoder { | ||
|
|
||
| private ArrayDecoder() { | ||
| throw new UnsupportedOperationException("Utility class cannot be instantiated"); | ||
| } | ||
|
|
||
| /** | ||
| * Parses array from header string and following lines. | ||
| * Detects array type (tabular, list, or primitive) and routes accordingly. | ||
| * @param header the header string to parse | ||
| * @param depth the depth of array | ||
| * @param context decode object in order to deal with lines, delimiter and options | ||
| * @return parsed array with delimiter | ||
| */ | ||
| protected static List<Object> parseArray(String header, int depth, DecodeContext context) { | ||
| String arrayDelimiter = extractDelimiterFromHeader(header, context); | ||
|
|
||
| return parseArrayWithDelimiter(header, depth, arrayDelimiter, context); | ||
| } | ||
|
|
||
| /** | ||
| * Extracts delimiter from array header. | ||
| * Returns tab, pipe, or comma (default) based on header pattern. | ||
| * @param header the header string to parse | ||
| * @param context decode object in order to deal with lines, delimiter and options | ||
| * @return extracted delimiter from header | ||
| */ | ||
jenspapenhagen marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| protected static String extractDelimiterFromHeader(String header, DecodeContext context) { | ||
| Matcher matcher = ARRAY_HEADER_PATTERN.matcher(header); | ||
| if (matcher.find() && matcher.groupCount() == 3) { | ||
| String delimChar = matcher.group(3); | ||
| if (delimChar != null) { | ||
| if ("\t".equals(delimChar)) { | ||
| return "\t"; | ||
| } else if ("|".equals(delimChar)) { | ||
| return "|"; | ||
| } | ||
| } | ||
| } | ||
| // Default to comma | ||
| return context.delimiter; | ||
| } | ||
|
|
||
| /** | ||
| * Parses array from 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 array | ||
| * @param arrayDelimiter array delimiter | ||
| * @param context decode object in order to deal with lines, delimiter and options | ||
| * @return parsed array | ||
| */ | ||
| protected static List<Object> parseArrayWithDelimiter(String header, int depth, String 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(":")) { | ||
| 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("- ")) { | ||
| 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; | ||
jenspapenhagen marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| if (context.options.strict()) { | ||
| throw new IllegalArgumentException("Invalid array header: " + header); | ||
| } | ||
| return Collections.emptyList(); | ||
| } | ||
|
|
||
| /** | ||
| * Validates array length if declared in header. | ||
| * @param header header | ||
| * @param actualLength actual length | ||
| */ | ||
| protected 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 array header. | ||
| * Returns the number specified in [n] or null if not found. | ||
| */ | ||
| private static Integer extractLengthFromHeader(String header) { | ||
| Matcher matcher = ARRAY_HEADER_PATTERN.matcher(header); | ||
| if (matcher.find()) { | ||
| try { | ||
| return Integer.parseInt(matcher.group(2)); | ||
| } catch (NumberFormatException e) { | ||
| return null; | ||
| } | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| /** | ||
| * Parses array values from a delimiter-separated string. | ||
| * @param values the values string to parse | ||
| * @param arrayDelimiter array delimiter | ||
| * @return parsed array values | ||
| */ | ||
| protected static List<Object> parseArrayValues(String values, String 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 | ||
| */ | ||
| protected static List<String> parseDelimitedValues(String input, String arrayDelimiter) { | ||
| List<String> result = new ArrayList<>(); | ||
| StringBuilder current = new StringBuilder(); | ||
| boolean inQuotes = false; | ||
| boolean escaped = false; | ||
| char delimChar = arrayDelimiter.charAt(0); | ||
|
|
||
| int i = 0; | ||
| while (i < input.length()) { | ||
| char c = input.charAt(i); | ||
|
|
||
| if (escaped) { | ||
| current.append(c); | ||
| escaped = false; | ||
| i++; | ||
| } else if (c == '\\') { | ||
| current.append(c); | ||
| escaped = true; | ||
| i++; | ||
| } else if (c == '"') { | ||
| current.append(c); | ||
| inQuotes = !inQuotes; | ||
| i++; | ||
| } else if (c == delimChar && !inQuotes) { | ||
| // Found delimiter - add current value (trimmed) and reset | ||
| String value = current.toString().trim(); | ||
| result.add(value); | ||
| current = new StringBuilder(); | ||
| // Skip whitespace after delimiter | ||
| do { | ||
| i++; | ||
| } while (i < input.length() && Character.isWhitespace(input.charAt(i))); | ||
| } else { | ||
| current.append(c); | ||
| i++; | ||
| } | ||
| } | ||
|
|
||
| // Add final value | ||
| if (!current.isEmpty() || input.endsWith(arrayDelimiter)) { | ||
| result.add(current.toString().trim()); | ||
| } | ||
|
|
||
| return result; | ||
| } | ||
|
|
||
| /** | ||
| * Parses list 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) { | ||
| ArrayDecoder.validateArrayLength(header, result.size()); | ||
| } | ||
| return result; | ||
| } | ||
|
|
||
| /** | ||
| * Handles blank line processing in list array. | ||
| * Returns true if array should terminate, false if 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; // End of file - 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 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 online depth. | ||
| * Returns true if 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; | ||
| } | ||
| } | ||
16 changes: 16 additions & 0 deletions
16
src/main/java/dev/toonformat/jtoon/decoder/DecodeContext.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| package dev.toonformat.jtoon.decoder; | ||
|
|
||
| import dev.toonformat.jtoon.DecodeOptions; | ||
|
|
||
| /** | ||
| * Deals with the main attributes used to decode TOON to JSON format | ||
| */ | ||
| public class DecodeContext { | ||
jenspapenhagen marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| protected String[] lines; | ||
| protected DecodeOptions options; | ||
| protected String delimiter; | ||
| protected int currentLine = 0; | ||
|
|
||
| public DecodeContext () {} | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.