1 /* 2 * Copyright 2016 Google Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 * in compliance with the License. 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 distributed under the License 10 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 * or implied. See the License for the specific language governing permissions and limitations under 12 * the License. 13 */ 14 15 package com.google.googlejavaformat.java.javadoc; 16 17 import static com.google.common.base.Preconditions.checkNotNull; 18 import static com.google.common.collect.Comparators.max; 19 import static com.google.common.collect.Sets.immutableEnumSet; 20 import static com.google.googlejavaformat.java.javadoc.JavadocWriter.AutoIndent.AUTO_INDENT; 21 import static com.google.googlejavaformat.java.javadoc.JavadocWriter.AutoIndent.NO_AUTO_INDENT; 22 import static com.google.googlejavaformat.java.javadoc.JavadocWriter.RequestedWhitespace.BLANK_LINE; 23 import static com.google.googlejavaformat.java.javadoc.JavadocWriter.RequestedWhitespace.NEWLINE; 24 import static com.google.googlejavaformat.java.javadoc.JavadocWriter.RequestedWhitespace.NONE; 25 import static com.google.googlejavaformat.java.javadoc.JavadocWriter.RequestedWhitespace.WHITESPACE; 26 import static com.google.googlejavaformat.java.javadoc.Token.Type.HEADER_OPEN_TAG; 27 import static com.google.googlejavaformat.java.javadoc.Token.Type.LIST_ITEM_OPEN_TAG; 28 import static com.google.googlejavaformat.java.javadoc.Token.Type.PARAGRAPH_OPEN_TAG; 29 30 import com.google.common.collect.ImmutableSet; 31 import com.google.googlejavaformat.java.javadoc.Token.Type; 32 33 /** 34 * Stateful object that accepts "requests" and "writes," producing formatted Javadoc. 35 * 36 * <p>Our Javadoc formatter doesn't ever generate a parse tree, only a stream of tokens, so the 37 * writer must compute and store the answer to questions like "How many levels of nested HTML list 38 * are we inside?" 39 */ 40 final class JavadocWriter { 41 private final int blockIndent; 42 private final StringBuilder output = new StringBuilder(); 43 /** 44 * Whether we are inside an {@code <li>} element, excluding the case in which the {@code <li>} 45 * contains a {@code <ul>} or {@code <ol>} that we are also inside -- unless of course we're 46 * inside an {@code <li>} element in that inner list :) 47 */ 48 private boolean continuingListItemOfInnermostList; 49 50 private boolean continuingFooterTag; 51 private final NestingCounter continuingListItemCount = new NestingCounter(); 52 private final NestingCounter continuingListCount = new NestingCounter(); 53 private final NestingCounter postWriteModifiedContinuingListCount = new NestingCounter(); 54 private int remainingOnLine; 55 private boolean atStartOfLine; 56 private RequestedWhitespace requestedWhitespace = NONE; 57 private Token requestedMoeBeginStripComment; 58 private int indentForMoeEndStripComment; 59 private boolean wroteAnythingSignificant; 60 JavadocWriter(int blockIndent)61 JavadocWriter(int blockIndent) { 62 this.blockIndent = blockIndent; 63 } 64 65 /** 66 * Requests whitespace between the previously written token and the next written token. The 67 * request may be honored, or it may be overridden by a request for "more significant" whitespace, 68 * like a newline. 69 */ requestWhitespace()70 void requestWhitespace() { 71 requestWhitespace(WHITESPACE); 72 } 73 requestMoeBeginStripComment(Token token)74 void requestMoeBeginStripComment(Token token) { 75 // We queue this up so that we can put it after any requested whitespace. 76 requestedMoeBeginStripComment = checkNotNull(token); 77 } 78 writeBeginJavadoc()79 void writeBeginJavadoc() { 80 /* 81 * JavaCommentsHelper will make sure this is indented right. But it seems sensible enough that, 82 * if our input starts with ∕✱✱, so too does our output. 83 */ 84 output.append("/**"); 85 writeNewline(); 86 } 87 writeEndJavadoc()88 void writeEndJavadoc() { 89 output.append("\n"); 90 appendSpaces(blockIndent + 1); 91 output.append("*/"); 92 } 93 writeFooterJavadocTagStart(Token token)94 void writeFooterJavadocTagStart(Token token) { 95 // Close any unclosed lists (e.g., <li> without <ul>). 96 // TODO(cpovirk): Actually generate </ul>, etc.? 97 /* 98 * TODO(cpovirk): Also generate </pre> and </table> if appropriate. This is necessary for 99 * idempotency in broken Javadoc. (We don't necessarily need that, but full idempotency may be a 100 * nice goal, especially if it helps us use a fuzzer to test.) Unfortunately, the writer doesn't 101 * currently know which of those tags are open. 102 */ 103 continuingListItemOfInnermostList = false; 104 continuingListItemCount.reset(); 105 continuingListCount.reset(); 106 /* 107 * There's probably no need for this, since its only effect is to disable blank lines in some 108 * cases -- and we're doing that already in the footer. 109 */ 110 postWriteModifiedContinuingListCount.reset(); 111 112 if (!wroteAnythingSignificant) { 113 // Javadoc consists solely of tags. This is frowned upon in general but OK for @Overrides. 114 } else if (!continuingFooterTag) { 115 // First footer tag after a body tag. 116 requestBlankLine(); 117 } else { 118 // Subsequent footer tag. 119 continuingFooterTag = false; 120 requestNewline(); 121 } 122 writeToken(token); 123 continuingFooterTag = true; 124 } 125 writeListOpen(Token token)126 void writeListOpen(Token token) { 127 requestBlankLine(); 128 129 writeToken(token); 130 continuingListItemOfInnermostList = false; 131 continuingListCount.increment(); 132 postWriteModifiedContinuingListCount.increment(); 133 134 requestNewline(); 135 } 136 writeListClose(Token token)137 void writeListClose(Token token) { 138 requestNewline(); 139 140 continuingListItemCount.decrementIfPositive(); 141 continuingListCount.decrementIfPositive(); 142 writeToken(token); 143 postWriteModifiedContinuingListCount.decrementIfPositive(); 144 145 requestBlankLine(); 146 } 147 writeListItemOpen(Token token)148 void writeListItemOpen(Token token) { 149 requestNewline(); 150 151 if (continuingListItemOfInnermostList) { 152 continuingListItemOfInnermostList = false; 153 continuingListItemCount.decrementIfPositive(); 154 } 155 writeToken(token); 156 continuingListItemOfInnermostList = true; 157 continuingListItemCount.increment(); 158 } 159 writeHeaderOpen(Token token)160 void writeHeaderOpen(Token token) { 161 requestBlankLine(); 162 163 writeToken(token); 164 } 165 writeHeaderClose(Token token)166 void writeHeaderClose(Token token) { 167 writeToken(token); 168 169 requestBlankLine(); 170 } 171 writeParagraphOpen(Token token)172 void writeParagraphOpen(Token token) { 173 if (!wroteAnythingSignificant) { 174 /* 175 * The user included an initial <p> tag. Ignore it, and don't request a blank line before the 176 * next token. 177 */ 178 return; 179 } 180 181 requestBlankLine(); 182 183 writeToken(token); 184 } 185 writeBlockquoteOpenOrClose(Token token)186 void writeBlockquoteOpenOrClose(Token token) { 187 requestBlankLine(); 188 189 writeToken(token); 190 191 requestBlankLine(); 192 } 193 writePreOpen(Token token)194 void writePreOpen(Token token) { 195 requestBlankLine(); 196 197 writeToken(token); 198 } 199 writePreClose(Token token)200 void writePreClose(Token token) { 201 writeToken(token); 202 203 requestBlankLine(); 204 } 205 writeCodeOpen(Token token)206 void writeCodeOpen(Token token) { 207 writeToken(token); 208 } 209 writeCodeClose(Token token)210 void writeCodeClose(Token token) { 211 writeToken(token); 212 } 213 writeTableOpen(Token token)214 void writeTableOpen(Token token) { 215 requestBlankLine(); 216 217 writeToken(token); 218 } 219 writeTableClose(Token token)220 void writeTableClose(Token token) { 221 writeToken(token); 222 223 requestBlankLine(); 224 } 225 writeMoeEndStripComment(Token token)226 void writeMoeEndStripComment(Token token) { 227 writeLineBreakNoAutoIndent(); 228 appendSpaces(indentForMoeEndStripComment); 229 230 // Or maybe just "output.append(token.getValue())?" I'm kind of surprised this is so easy. 231 writeToken(token); 232 233 requestNewline(); 234 } 235 writeHtmlComment(Token token)236 void writeHtmlComment(Token token) { 237 requestNewline(); 238 239 writeToken(token); 240 241 requestNewline(); 242 } 243 writeBr(Token token)244 void writeBr(Token token) { 245 writeToken(token); 246 247 requestNewline(); 248 } 249 writeLineBreakNoAutoIndent()250 void writeLineBreakNoAutoIndent() { 251 writeNewline(NO_AUTO_INDENT); 252 } 253 writeLiteral(Token token)254 void writeLiteral(Token token) { 255 writeToken(token); 256 } 257 258 @Override toString()259 public String toString() { 260 return output.toString(); 261 } 262 requestBlankLine()263 private void requestBlankLine() { 264 requestWhitespace(BLANK_LINE); 265 } 266 requestNewline()267 private void requestNewline() { 268 requestWhitespace(NEWLINE); 269 } 270 requestWhitespace(RequestedWhitespace requestedWhitespace)271 private void requestWhitespace(RequestedWhitespace requestedWhitespace) { 272 this.requestedWhitespace = max(requestedWhitespace, this.requestedWhitespace); 273 } 274 275 /** 276 * The kind of whitespace that has been requested between the previous and next tokens. The order 277 * of the values is significant: It goes from lowest priority to highest. For example, if the 278 * previous token requests {@link #BLANK_LINE} after it but the next token requests only {@link 279 * #NEWLINE} before it, we insert {@link #BLANK_LINE}. 280 */ 281 enum RequestedWhitespace { 282 NONE, 283 WHITESPACE, 284 NEWLINE, 285 BLANK_LINE, 286 ; 287 } 288 writeToken(Token token)289 private void writeToken(Token token) { 290 if (requestedMoeBeginStripComment != null) { 291 requestNewline(); 292 } 293 294 if (requestedWhitespace == BLANK_LINE 295 && (postWriteModifiedContinuingListCount.isPositive() || continuingFooterTag)) { 296 /* 297 * We don't write blank lines inside lists or footer tags, even in cases where we otherwise 298 * would (e.g., before a <p> tag). Justification: We don't write blank lines _between_ list 299 * items or footer tags, so it would be strange to write blank lines _within_ one. Of course, 300 * an alternative approach would be to go ahead and write blank lines between items/tags, 301 * either always or only in the case that an item contains a blank line. 302 */ 303 requestedWhitespace = NEWLINE; 304 } 305 306 if (requestedWhitespace == BLANK_LINE) { 307 writeBlankLine(); 308 requestedWhitespace = NONE; 309 } else if (requestedWhitespace == NEWLINE) { 310 writeNewline(); 311 requestedWhitespace = NONE; 312 } 313 boolean needWhitespace = (requestedWhitespace == WHITESPACE); 314 315 /* 316 * Write a newline if necessary to respect the line limit. (But if we're at the beginning of the 317 * line, a newline won't help. Or it might help but only by separating "<p>veryverylongword," 318 * which goes against our style.) 319 */ 320 if (!atStartOfLine && token.length() + (needWhitespace ? 1 : 0) > remainingOnLine) { 321 writeNewline(); 322 } 323 if (!atStartOfLine && needWhitespace) { 324 output.append(" "); 325 remainingOnLine--; 326 } 327 328 if (requestedMoeBeginStripComment != null) { 329 output.append(requestedMoeBeginStripComment.getValue()); 330 requestedMoeBeginStripComment = null; 331 indentForMoeEndStripComment = innerIndent(); 332 requestNewline(); 333 writeToken(token); 334 return; 335 } 336 337 output.append(token.getValue()); 338 339 if (!START_OF_LINE_TOKENS.contains(token.getType())) { 340 atStartOfLine = false; 341 } 342 343 /* 344 * TODO(cpovirk): We really want the number of "characters," not chars. Figure out what the 345 * right way of measuring that is (grapheme count (with BreakIterator?)? sum of widths of all 346 * graphemes? I don't think that our style guide is specific about this.). Moreover, I am 347 * probably brushing other problems with surrogates, etc. under the table. Hopefully I mostly 348 * get away with it by joining all non-space, non-tab characters together. 349 * 350 * Possibly the "width" question has no right answer: 351 * http://denisbider.blogspot.com/2015/09/when-monospace-fonts-arent-unicode.html 352 */ 353 remainingOnLine -= token.length(); 354 requestedWhitespace = NONE; 355 wroteAnythingSignificant = true; 356 } 357 writeBlankLine()358 private void writeBlankLine() { 359 output.append("\n"); 360 appendSpaces(blockIndent + 1); 361 output.append("*"); 362 writeNewline(); 363 } 364 writeNewline()365 private void writeNewline() { 366 writeNewline(AUTO_INDENT); 367 } 368 writeNewline(AutoIndent autoIndent)369 private void writeNewline(AutoIndent autoIndent) { 370 output.append("\n"); 371 appendSpaces(blockIndent + 1); 372 output.append("*"); 373 appendSpaces(1); 374 remainingOnLine = JavadocFormatter.MAX_LINE_LENGTH - blockIndent - 3; 375 if (autoIndent == AUTO_INDENT) { 376 appendSpaces(innerIndent()); 377 remainingOnLine -= innerIndent(); 378 } 379 atStartOfLine = true; 380 } 381 382 enum AutoIndent { 383 AUTO_INDENT, 384 NO_AUTO_INDENT 385 } 386 innerIndent()387 private int innerIndent() { 388 int innerIndent = continuingListItemCount.value() * 4 + continuingListCount.value() * 2; 389 if (continuingFooterTag) { 390 innerIndent += 4; 391 } 392 return innerIndent; 393 } 394 395 // If this is a hotspot, keep a String of many spaces around, and call append(string, start, end). appendSpaces(int count)396 private void appendSpaces(int count) { 397 output.append(" ".repeat(count)); 398 } 399 400 /** 401 * Tokens that are always pinned to the following token. For example, {@code <p>} in {@code <p>Foo 402 * bar} (never {@code <p> Foo bar} or {@code <p>\nFoo bar}). 403 * 404 * <p>This is not the only kind of "pinning" that we do: See also the joining of LITERAL tokens 405 * done by the lexer. The special pinning here is necessary because these tokens are not of type 406 * LITERAL (because they require other special handling). 407 */ 408 private static final ImmutableSet<Type> START_OF_LINE_TOKENS = 409 immutableEnumSet(LIST_ITEM_OPEN_TAG, PARAGRAPH_OPEN_TAG, HEADER_OPEN_TAG); 410 } 411