package nl.andrewlalis.blockbookbinder.model.build; import nl.andrewlalis.blockbookbinder.model.Book; import nl.andrewlalis.blockbookbinder.model.BookPage; import nl.andrewlalis.blockbookbinder.util.CharWidthMapper; import nl.andrewlalis.blockbookbinder.util.ApplicationProperties; import java.util.ArrayList; import java.util.List; public class BookBuilder { /** * Builds a full book of pages from the given source text. * @param source The source text to convert. * @return A book containing the source text formatted for a minecraft book. */ public Book build(String source) { final int maxLines = ApplicationProperties.getIntProp("book.page_max_lines"); List lines = this.convertSourceToLines(this.cleanSource(source)); Book book = new Book(); BookPage page = new BookPage(); int currentPageLineCount = 0; for (String line : lines) { page.addLine(line); currentPageLineCount++; if (currentPageLineCount == maxLines) { book.addPage(page); page = new BookPage(); currentPageLineCount = 0; } } if (page.hasContent()) { book.addPage(page); } return book; } /** * Cleans a given source text, by removing superfluous newlines that won't * look too nice in minecraft text, and by erasing manually-imposed single * newlines that may be in the source text to format it in a normal editor. * @param source The source text. * @return The cleaned string. */ public String cleanSource(String source) { return source.trim() .replaceAll("(?>\\v)+(\\v)", "\n\n") // Replace large chunks of newline with just two. .replaceAll("\\S\n\\S", " ") // Unwrap previously-imposed single-line wrapping. .replaceAll("\t", " ") // Replace tabs with single-spaces, due to space constraints. .replaceAll(" [ ]+", " "); // Remove any superfluous spaces. } /** * Converts the given source string into a formatted list of lines that can * be copied to a minecraft book. * @param source The source string. * @return A list of lines. */ private List convertSourceToLines(String source) { List lines = new ArrayList<>(); final char[] sourceChars = source.toCharArray(); final int maxLinePixelWidth = ApplicationProperties.getIntProp("book.page_max_width"); int sourceIndex = 0; StringBuilder lineBuilder = new StringBuilder(64); int linePixelWidth = 0; StringBuilder symbolBuilder = new StringBuilder(64); while (sourceIndex < sourceChars.length) { final char c = sourceChars[sourceIndex]; sourceIndex++; symbolBuilder.setLength(0); symbolBuilder.append(c); int symbolWidth = CharWidthMapper.getInstance().getWidth(c); // Since there's a 1-pixel gap between characters, add it to the width if this isn't the first char. if (lineBuilder.length() > 0) { symbolWidth++; } // If we encounter a non-newline whitespace at the beginning of the line, skip it. if (c == ' ' && lineBuilder.length() == 0) { continue; } // If we encounter a newline, immediately skip to a new line. if (c == '\n') { lines.add(lineBuilder.toString()); lineBuilder.setLength(0); linePixelWidth = 0; continue; } // If we encounter a word, keep accepting characters until we reach the end. if (Character.isLetterOrDigit(c)) { while ( sourceIndex < sourceChars.length && Character.isLetterOrDigit(sourceChars[sourceIndex]) ) { char nextChar = sourceChars[sourceIndex]; symbolBuilder.append(nextChar); symbolWidth += 1 + CharWidthMapper.getInstance().getWidth(nextChar); sourceIndex++; } } final String symbol = symbolBuilder.toString(); // Check if we need to go to the next line to fit the symbol. if (linePixelWidth + symbolWidth > maxLinePixelWidth) { lines.add(lineBuilder.toString()); lineBuilder.setLength(0); linePixelWidth = 0; } // Finally, append the symbol. lineBuilder.append(symbol); linePixelWidth += symbolWidth; } // Append any remaining text. if (lineBuilder.length() > 0) { lines.add(lineBuilder.toString()); } return lines; } }