package rabbitescape.ui.swing; import java.util.ArrayList; import java.util.Collections; import java.util.regex.Matcher; import java.util.regex.Pattern; import rabbitescape.engine.util.Util; /** * @brief encapsulates what has been retrieved about a github issue */ public class GitHubIssue { private int number; /** < @brief github issue number. */ private boolean isLevel; private boolean isBug; private String body = ""; /** < @brief body text excluding world text. */ private String title; private ArrayList<String> wrappedWorlds; /** < @brief Worlds are []. These have \n */ private int worldIndex = 0; /** < @brief Some issues have multiple worlds: current selected index */ private static final String replaceWorldsWith = "\n-----\n"; private static final String commentSeparator = "\n*****\n"; /** * @brief Creates a GitHubIssue with no labels. */ public GitHubIssue( int gitHubIssueNumber, String gitHubIssueTitle, String openingCommentBody ) { this( gitHubIssueNumber, gitHubIssueTitle, openingCommentBody, new String[] {} ); } public GitHubIssue( int gitHubIssueNumber, String gitHubIssueTitle, String openingCommentBody, String[] labels ) { wrappedWorlds = new ArrayList<String>(); number = gitHubIssueNumber; addToBody( openingCommentBody ); title = stripEscape( gitHubIssueTitle ); for ( int i = 0; i < labels.length; i++ ) { if ( 0 == labels[i].compareTo( "bug" ) ) { isBug = true; } else if ( 0 == labels[i].compareTo( "level" ) ) { isLevel = true; } } } public int getNumber() { return number; } public boolean isLevel() { return isLevel; } public boolean isBug() { return isBug; } /** * @return true if it can be set (is not out of range) */ public boolean setCurrentWorldIndex( int index ) { if ( wrappedWorlds.size() > index ) { worldIndex = index; return true; } return false; } public int getCurrentWorldIndex() { return worldIndex; } public String getCurrentWorld() { if ( wrappedWorlds.size() > worldIndex ) { return wrappedWorlds.get( worldIndex ); } return null; } public void fetchComments( GitHubClient ghc ) { ghc.fetchComments( this ); } /** * @param i * Some issues contain multiple worlds. Ask for them by index. * @return World string with newline characters. */ public String getWorld( int i ) { if ( i >= wrappedWorlds.size() ) { return null; } return wrappedWorlds.get( i ); } public String getBody() { return body; } /** * @brief Keeps a copy of the body text and parses out the worlds. */ public void addToBody( String bodyIn ) { // Positions of worlds in the body text; ArrayList<Integer> startIndices = new ArrayList<Integer>(); ArrayList<Integer> endIndices = new ArrayList<Integer>(); String bodyAdd = "" + bodyIn; ArrayList<String> wrappedWorldsAdd = new ArrayList<String>(); findBacktickWorlds( bodyAdd, wrappedWorldsAdd, startIndices, endIndices ); findIndentWorlds( bodyAdd, wrappedWorldsAdd, startIndices, endIndices ); ArrayList<String> newWorldOrder = new ArrayList<String>( wrappedWorldsAdd.size() ); for ( int i = 0; i < startIndices.size(); i++ ) { newWorldOrder.add( "" ); } // re-order worlds and remove worlds from body text for ( int i = 0; i < startIndices.size(); i++ ) { int max = Collections.max( startIndices ); int maxIndex = -1; for ( int j = 0; j < startIndices.size(); j++ ) { if ( max == startIndices.get( j ) ) { maxIndex = j; break; } } newWorldOrder.set( startIndices.size() - 1 - i, wrappedWorldsAdd.get( maxIndex ) ); bodyAdd = bodyAdd.substring( 0, startIndices.get( maxIndex ) ) + replaceWorldsWith + bodyAdd.substring( endIndices.get( maxIndex ) ); startIndices.set( maxIndex, 0 ); // So it is not found again } wrappedWorlds.addAll( newWorldOrder ); bodyAdd = stripEscape( bodyAdd ); bodyAdd = realNewlines( bodyAdd ); body = body + commentSeparator + bodyAdd; } /** * @brief Add location of world in the body string to the list. Only add if * the new world is not inside another. Backtick worlds are parsed * first, so they win. */ private static void checkAddWorldIndices( ArrayList<Integer> startIndices, ArrayList<Integer> endIndices, int startIndex, int endIndex ) { for ( int i = 0; i < startIndices.size(); i++ ) { if ( startIndex >= startIndices.get( i ) && startIndex < endIndices.get( i ) ) { // It's in the range of another world, don't add. return; } } startIndices.add( startIndex ); endIndices.add( endIndex ); } /** * @brief parse out worlds from markdown contained in triple backticks */ private static void findBacktickWorlds( String bodyAdd, ArrayList<String> braveNewWrappedWorlds, ArrayList<Integer> startIndices, ArrayList<Integer> endIndices ) { Pattern worldPattern = Pattern.compile( "(\\\\n)?```(.*?)```" ); Matcher worldMatcher = worldPattern.matcher( bodyAdd ); while ( worldMatcher.find() ) { String worldWrapped = worldMatcher.group( 2 ); braveNewWrappedWorlds.add( fixWorld( worldWrapped ) ); checkAddWorldIndices( startIndices, endIndices, worldMatcher.start(), worldMatcher.end() ); } } /** * @brief parse out worlds from markdown contained in indent blocks Appends * to the ArrayList<Integer> describing the worlds location. */ private static void findIndentWorlds( String bodyAdd, ArrayList<String> braveNewWrappedWorlds, ArrayList<Integer> startIndices, ArrayList<Integer> endIndices ) { // at least 4 spaces or a tab // the json has \n in, not newline char // after java compiler \\\\ becomes \\, after regex compile \ Pattern firstLinePattern = Pattern.compile( "\\\\n(\\\\t| {4,}+)" ); Matcher firstLineMatcher = firstLinePattern.matcher( bodyAdd ); int worldStart, worldEnd; while ( firstLineMatcher.find() ) { worldStart = firstLineMatcher.start(); String blockPrefix = firstLineMatcher.group( 1 ); if ( 0 == blockPrefix.compareTo( "\\t" ) ) { blockPrefix = "\\\\t"; // Reform so regex is char pair, not tab // char. } Pattern subsequentLinePattern = Pattern.compile( "\\\\n" + blockPrefix + "(.+?)\\\\n" ); Matcher subsequentLineMatcher = subsequentLinePattern .matcher( bodyAdd ); // Do I need to store the result of region back in the Matcher? subsequentLineMatcher.region( firstLineMatcher.start(), bodyAdd.length() - 1 ); String worldWrapped = ""; int prevEndIndex = -1; while ( subsequentLineMatcher.find() ) { /* * Check for lines between matches, note this is to the start of * the whole match including the indent string. First time * through, let the -1 past. */ if ( -1 != prevEndIndex && subsequentLineMatcher.start() != prevEndIndex ) { break; } worldWrapped = worldWrapped + subsequentLineMatcher.group( 1 ) + "\n"; prevEndIndex = subsequentLineMatcher.end() - 2; // the end of the group is 2 chars before the end of the whole // match, so it // can find the \n again to start the next line. subsequentLineMatcher.region( subsequentLineMatcher.end( 1 ), bodyAdd.length() ); } braveNewWrappedWorlds.add( fixWorld( worldWrapped ) ); if ( -1 == prevEndIndex ) { // indent block runs to the end of the body worldEnd = bodyAdd.length(); checkAddWorldIndices( startIndices, endIndices, worldStart, worldEnd ); break; } else { worldEnd = prevEndIndex; firstLineMatcher = firstLineMatcher.region( prevEndIndex, bodyAdd.length() ); checkAddWorldIndices( startIndices, endIndices, worldStart, worldEnd ); } } } /** * @brief Perform some automatic fixing Can't be as strict as when loading * from files. */ private static String fixWorld( String world ) { String fixed = "" + world; fixed = stripEscape( fixed ); fixed = realNewlines( fixed ); // Remove blank lines fixed = fixed.replaceAll( "\n\n", "\n" ); fixed = fixed.replaceAll( "^\n", "" ); // Strip trailing spaces from meta lines fixed = Util.regexRemovePreserveGroup( fixed, "^(:.*?) *?$", Pattern.MULTILINE ); return fixed; } public String getTitle() { return title; } public void setTitle( String titleIn ) { this.title = stripEscape( titleIn ); } /** * @brief Tidy up escape characters. - Remove \ before " - Remove \r to * covert Win EOL to Unix EOL - Undouble \\ */ public static String stripEscape( String s1 ) { String s2; s2 = s1.replaceAll( "(\\\\r)", "" ); s2 = Util.regexRemovePreserveGroup( s2, "\\\\(\\\")" ); // Undouble slashes // I don't understand why this does not work without the capturing group // and back reference. s2 = Util.regexRemovePreserveGroup( s2, "(\\\\)\\\\" ); return s2; } /** * @brief Replace \n with actual newline chars. */ public static String realNewlines( String s ) { return s.replaceAll( "\\\\n", "\n" ); } }