Line data Source code
1 : // Copyright (C) 2016 The Android Open Source Project 2 : // 3 : // Licensed under the Apache License, Version 2.0 (the "License"); 4 : // you may not use this file except in compliance with the License. 5 : // You may obtain a copy of the License at 6 : // 7 : // http://www.apache.org/licenses/LICENSE-2.0 8 : // 9 : // Unless required by applicable law or agreed to in writing, software 10 : // distributed under the License is distributed on an "AS IS" BASIS, 11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 : // See the License for the specific language governing permissions and 13 : // limitations under the License. 14 : 15 : package com.google.gerrit.server.mail.send; 16 : 17 : import static com.google.common.base.Strings.isNullOrEmpty; 18 : 19 : import com.google.common.base.Splitter; 20 : import com.google.common.collect.ImmutableList; 21 : import com.google.gerrit.common.Nullable; 22 : import java.util.ArrayList; 23 : import java.util.List; 24 : 25 0 : public class CommentFormatter { 26 66 : public enum BlockType { 27 66 : LIST, 28 66 : PARAGRAPH, 29 66 : PRE_FORMATTED, 30 66 : QUOTE 31 : } 32 : 33 66 : public static class Block { 34 : public BlockType type; 35 : public String text; 36 : public List<String> items; // For the items of list blocks. 37 : public List<Block> quotedBlocks; // For the contents of quote blocks. 38 : } 39 : 40 : /** 41 : * Take a string of comment text that was written using the wiki-Like format and emit a list of 42 : * blocks that can be rendered to block-level HTML. This method does not escape HTML. 43 : * 44 : * <p>Adapted from the {@code wikify} method found in: 45 : * com.google.gwtexpui.safehtml.client.SafeHtml 46 : * 47 : * @param source The raw, unescaped comment in the Gerrit wiki-like format. 48 : * @return List of block objects, each with unescaped comment content. 49 : */ 50 : public static ImmutableList<Block> parse(@Nullable String source) { 51 66 : if (isNullOrEmpty(source)) { 52 62 : return ImmutableList.of(); 53 : } 54 : 55 66 : ImmutableList.Builder<Block> result = ImmutableList.builder(); 56 66 : for (String p : Splitter.on("\n\n").split(source)) { 57 66 : if (isQuote(p)) { 58 1 : result.add(makeQuote(p)); 59 66 : } else if (isPreFormat(p)) { 60 1 : result.add(makePre(p)); 61 66 : } else if (isList(p)) { 62 1 : makeList(p, result); 63 66 : } else if (!p.isEmpty()) { 64 66 : result.add(makeParagraph(p)); 65 : } 66 66 : } 67 66 : return result.build(); 68 : } 69 : 70 : /** 71 : * Take a block of comment text that contains a list and potentially paragraphs (but does not 72 : * contain blank lines), generate appropriate block elements and append them to the output list. 73 : * 74 : * <p>In simple cases, this will generate a single list block. For example, on the following 75 : * input. 76 : * 77 : * <p>* Item one. * Item two. * item three. 78 : * 79 : * <p>However, if the list is adjacent to a paragraph, it will need to also generate that 80 : * paragraph. Consider the following input. 81 : * 82 : * <p>A bit of text describing the context of the list: * List item one. * List item two. * Et 83 : * cetera. 84 : * 85 : * <p>In this case, {@code makeList} generates a paragraph block object containing the 86 : * non-bullet-prefixed text, followed by a list block. 87 : * 88 : * <p>Adapted from the {@code wikifyList} method found in: 89 : * com.google.gwtexpui.safehtml.client.SafeHtml 90 : * 91 : * @param p The block containing the list (as well as potential paragraphs). 92 : * @param out The list of blocks to append to. 93 : */ 94 : private static void makeList(String p, ImmutableList.Builder<Block> out) { 95 1 : Block block = null; 96 1 : StringBuilder textBuilder = null; 97 1 : boolean inList = false; 98 1 : boolean inParagraph = false; 99 : 100 1 : for (String line : Splitter.on('\n').split(p)) { 101 1 : if (line.startsWith("-") || line.startsWith("*")) { 102 : // The next line looks like a list item. If not building a list already, 103 : // then create one. Remove the list item marker (* or -) from the line. 104 1 : if (!inList) { 105 1 : if (inParagraph) { 106 : // Add the finished paragraph block to the result. 107 1 : inParagraph = false; 108 1 : block.text = textBuilder.toString(); 109 1 : out.add(block); 110 : } 111 : 112 1 : inList = true; 113 1 : block = new Block(); 114 1 : block.type = BlockType.LIST; 115 1 : block.items = new ArrayList<>(); 116 : } 117 1 : line = line.substring(1).trim(); 118 : 119 1 : } else if (!inList) { 120 : // Otherwise, if a list has not yet been started, but the next line does 121 : // not look like a list item, then add the line to a paragraph block. If 122 : // a paragraph block has not yet been started, then create one. 123 1 : if (!inParagraph) { 124 1 : inParagraph = true; 125 1 : block = new Block(); 126 1 : block.type = BlockType.PARAGRAPH; 127 1 : textBuilder = new StringBuilder(); 128 : } else { 129 1 : textBuilder.append(" "); 130 : } 131 1 : textBuilder.append(line); 132 1 : continue; 133 : } 134 : 135 1 : block.items.add(line); 136 1 : } 137 : 138 1 : if (block != null) { 139 1 : out.add(block); 140 : } 141 1 : } 142 : 143 : private static Block makeQuote(String p) { 144 1 : String quote = p.replaceAll("\n\\s?>\\s?", "\n"); 145 1 : if (quote.startsWith("> ")) { 146 1 : quote = quote.substring(2); 147 1 : } else if (quote.startsWith(" > ")) { 148 1 : quote = quote.substring(3); 149 : } 150 : 151 1 : Block block = new Block(); 152 1 : block.type = BlockType.QUOTE; 153 1 : block.quotedBlocks = CommentFormatter.parse(quote); 154 1 : return block; 155 : } 156 : 157 : private static Block makePre(String p) { 158 1 : Block block = new Block(); 159 1 : block.type = BlockType.PRE_FORMATTED; 160 1 : block.text = p; 161 1 : return block; 162 : } 163 : 164 : private static Block makeParagraph(String p) { 165 66 : Block block = new Block(); 166 66 : block.type = BlockType.PARAGRAPH; 167 66 : block.text = p; 168 66 : return block; 169 : } 170 : 171 : private static boolean isQuote(String p) { 172 66 : return p.startsWith("> ") || p.startsWith(" > "); 173 : } 174 : 175 : private static boolean isPreFormat(String p) { 176 66 : return p.startsWith(" ") || p.startsWith("\t") || p.contains("\n ") || p.contains("\n\t"); 177 : } 178 : 179 : private static boolean isList(String p) { 180 66 : return p.startsWith("- ") || p.startsWith("* ") || p.contains("\n- ") || p.contains("\n* "); 181 : } 182 : }