001    package org.bukkit.util;
002    
003    import org.bukkit.ChatColor;
004    
005    import java.util.LinkedList;
006    import java.util.List;
007    
008    /**
009     * The ChatPaginator takes a raw string of arbitrary length and breaks it down
010     * into an array of strings appropriate for displaying on the Minecraft player
011     * console.
012     */
013    public class ChatPaginator {
014        public static final int GUARANTEED_NO_WRAP_CHAT_PAGE_WIDTH = 55; // Will never wrap, even with the largest characters
015        public static final int AVERAGE_CHAT_PAGE_WIDTH = 65; // Will typically not wrap using an average character distribution
016        public static final int UNBOUNDED_PAGE_WIDTH = Integer.MAX_VALUE;
017        public static final int OPEN_CHAT_PAGE_HEIGHT = 20; // The height of an expanded chat window
018        public static final int CLOSED_CHAT_PAGE_HEIGHT = 10; // The height of the default chat window
019        public static final int UNBOUNDED_PAGE_HEIGHT = Integer.MAX_VALUE;
020    
021        /**
022         * Breaks a raw string up into pages using the default width and height.
023         *
024         * @param unpaginatedString The raw string to break.
025         * @param pageNumber The page number to fetch.
026         * @return A single chat page.
027         */
028        public static ChatPage paginate(String unpaginatedString, int pageNumber) {
029            return  paginate(unpaginatedString, pageNumber, GUARANTEED_NO_WRAP_CHAT_PAGE_WIDTH, CLOSED_CHAT_PAGE_HEIGHT);
030        }
031    
032        /**
033         * Breaks a raw string up into pages using a provided width and height.
034         *
035         * @param unpaginatedString The raw string to break.
036         * @param pageNumber The page number to fetch.
037         * @param lineLength The desired width of a chat line.
038         * @param pageHeight The desired number of lines in a page.
039         * @return A single chat page.
040         */
041        public static ChatPage paginate(String unpaginatedString, int pageNumber, int lineLength, int pageHeight) {
042            String[] lines = wordWrap(unpaginatedString, lineLength);
043    
044            int totalPages = lines.length / pageHeight + (lines.length % pageHeight == 0 ? 0 : 1);
045            int actualPageNumber = pageNumber <= totalPages ? pageNumber : totalPages;
046    
047            int from = (actualPageNumber - 1) * pageHeight;
048            int to = from + pageHeight <= lines.length  ? from + pageHeight : lines.length;
049            String[] selectedLines = Java15Compat.Arrays_copyOfRange(lines, from, to);
050    
051            return new ChatPage(selectedLines, actualPageNumber, totalPages);
052        }
053    
054        /**
055         * Breaks a raw string up into a series of lines. Words are wrapped using
056         * spaces as decimeters and the newline character is respected.
057         *
058         * @param rawString The raw string to break.
059         * @param lineLength The length of a line of text.
060         * @return An array of word-wrapped lines.
061         */
062        public static String[] wordWrap(String rawString, int lineLength) {
063            // A null string is a single line
064            if (rawString == null) {
065                return new String[] {""};
066            }
067    
068            // A string shorter than the lineWidth is a single line
069            if (rawString.length() <= lineLength && !rawString.contains("\n")) {
070                return new String[] {rawString};
071            }
072    
073            char[] rawChars = (rawString + ' ').toCharArray(); // add a trailing space to trigger pagination
074            StringBuilder word = new StringBuilder();
075            StringBuilder line = new StringBuilder();
076            List<String> lines = new LinkedList<String>();
077            int lineColorChars = 0;
078    
079            for (int i = 0; i < rawChars.length; i++) {
080                char c = rawChars[i];
081    
082                // skip chat color modifiers
083                if (c == ChatColor.COLOR_CHAR) {
084                    word.append(ChatColor.getByChar(rawChars[i + 1]));
085                    lineColorChars += 2;
086                    i++; // Eat the next character as we have already processed it
087                    continue;
088                }
089    
090                if (c == ' ' || c == '\n') {
091                    if (line.length() == 0 && word.length() > lineLength) { // special case: extremely long word begins a line
092                        for (String partialWord : word.toString().split("(?<=\\G.{" + lineLength + "})")) {
093                            lines.add(partialWord);
094                        }
095                    } else if (line.length() + word.length() - lineColorChars == lineLength) { // Line exactly the correct length...newline
096                        line.append(word);
097                        lines.add(line.toString());
098                        line = new StringBuilder();
099                        lineColorChars = 0;
100                    } else if (line.length() + 1 + word.length() - lineColorChars > lineLength) { // Line too long...break the line
101                        for (String partialWord : word.toString().split("(?<=\\G.{" + lineLength + "})")) {
102                            lines.add(line.toString());
103                            line = new StringBuilder(partialWord);
104                        }
105                        lineColorChars = 0;
106                    } else {
107                        if (line.length() > 0) {
108                            line.append(' ');
109                        }
110                        line.append(word);
111                    }
112                    word = new StringBuilder();
113    
114                    if (c == '\n') { // Newline forces the line to flush
115                        lines.add(line.toString());
116                        line = new StringBuilder();
117                    }
118                } else {
119                    word.append(c);
120                }
121            }
122    
123            if(line.length() > 0) { // Only add the last line if there is anything to add
124                lines.add(line.toString());
125            }
126    
127            // Iterate over the wrapped lines, applying the last color from one line to the beginning of the next
128            if (lines.get(0).length() == 0 || lines.get(0).charAt(0) != ChatColor.COLOR_CHAR) {
129                lines.set(0, ChatColor.WHITE + lines.get(0));
130            }
131            for (int i = 1; i < lines.size(); i++) {
132                final String pLine = lines.get(i-1);
133                final String subLine = lines.get(i);
134    
135                char color = pLine.charAt(pLine.lastIndexOf(ChatColor.COLOR_CHAR) + 1);
136                if (subLine.length() == 0 || subLine.charAt(0) != ChatColor.COLOR_CHAR) {
137                    lines.set(i, ChatColor.getByChar(color) + subLine);
138                }
139            }
140    
141            return lines.toArray(new String[lines.size()]);
142        }
143    
144        public static class ChatPage {
145    
146            private String[] lines;
147            private int pageNumber;
148            private int totalPages;
149    
150            public ChatPage(String[] lines, int pageNumber, int totalPages) {
151                this.lines = lines;
152                this.pageNumber = pageNumber;
153                this.totalPages = totalPages;
154            }
155    
156            public int getPageNumber() {
157                return pageNumber;
158            }
159    
160            public int getTotalPages() {
161                return totalPages;
162            }
163    
164            public String[] getLines() {
165    
166                return lines;
167            }
168        }
169    }