google-java-format-master/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocWriter.java
/*
 * Copyright 2016 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */

package com.google.googlejavaformat.java.javadoc;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.Comparators.max;
import static com.google.common.collect.Sets.immutableEnumSet;
import static com.google.googlejavaformat.java.javadoc.JavadocWriter.AutoIndent.AUTO_INDENT;
import static com.google.googlejavaformat.java.javadoc.JavadocWriter.AutoIndent.NO_AUTO_INDENT;
import static com.google.googlejavaformat.java.javadoc.JavadocWriter.RequestedWhitespace.BLANK_LINE;
import static com.google.googlejavaformat.java.javadoc.JavadocWriter.RequestedWhitespace.NEWLINE;
import static com.google.googlejavaformat.java.javadoc.JavadocWriter.RequestedWhitespace.NONE;
import static com.google.googlejavaformat.java.javadoc.JavadocWriter.RequestedWhitespace.WHITESPACE;
import static com.google.googlejavaformat.java.javadoc.Token.Type.HEADER_OPEN_TAG;
import static com.google.googlejavaformat.java.javadoc.Token.Type.LIST_ITEM_OPEN_TAG;
import static com.google.googlejavaformat.java.javadoc.Token.Type.PARAGRAPH_OPEN_TAG;

import com.google.common.collect.ImmutableSet;
import com.google.googlejavaformat.java.javadoc.Token.Type;

/**
 * Stateful object that accepts "requests" and "writes," producing formatted Javadoc.
 *
 * <p>Our Javadoc formatter doesn't ever generate a parse tree, only a stream of tokens, so the
 * writer must compute and store the answer to questions like "How many levels of nested HTML list
 * are we inside?"
 */
final class JavadocWriter {
  private final int blockIndent;
  private final StringBuilder output = new StringBuilder();
  /**
   * Whether we are inside an {@code <li>} element, excluding the case in which the {@code <li>}
   * contains a {@code <ul>} or {@code <ol>} that we are also inside -- unless of course we're
   * inside an {@code <li>} element in that inner list :)
   */
  private boolean continuingListItemOfInnermostList;

  private boolean continuingFooterTag;
  private final NestingCounter continuingListItemCount = new NestingCounter();
  private final NestingCounter continuingListCount = new NestingCounter();
  private final NestingCounter postWriteModifiedContinuingListCount = new NestingCounter();
  private int remainingOnLine;
  private boolean atStartOfLine;
  private RequestedWhitespace requestedWhitespace = NONE;
  private Token requestedMoeBeginStripComment;
  private int indentForMoeEndStripComment;
  private boolean wroteAnythingSignificant;

  JavadocWriter(int blockIndent) {
    this.blockIndent = blockIndent;
  }

  /**
   * Requests whitespace between the previously written token and the next written token. The
   * request may be honored, or it may be overridden by a request for "more significant" whitespace,
   * like a newline.
   */
  void requestWhitespace() {
    requestWhitespace(WHITESPACE);
  }

  void requestMoeBeginStripComment(Token token) {
    // We queue this up so that we can put it after any requested whitespace.
    requestedMoeBeginStripComment = checkNotNull(token);
  }

  void writeBeginJavadoc() {
    /*
     * JavaCommentsHelper will make sure this is indented right. But it seems sensible enough that,
     * if our input starts with ∕✱✱, so too does our output.
     */
    output.append("/**");
    writeNewline();
  }

  void writeEndJavadoc() {
    output.append("\n");
    appendSpaces(blockIndent + 1);
    output.append("*/");
  }

  void writeFooterJavadocTagStart(Token token) {
    // Close any unclosed lists (e.g., <li> without <ul>).
    // TODO(cpovirk): Actually generate </ul>, etc.?
    /*
     * TODO(cpovirk): Also generate </pre> and </table> if appropriate. This is necessary for
     * idempotency in broken Javadoc. (We don't necessarily need that, but full idempotency may be a
     * nice goal, especially if it helps us use a fuzzer to test.) Unfortunately, the writer doesn't
     * currently know which of those tags are open.
     */
    continuingListItemOfInnermostList = false;
    continuingListItemCount.reset();
    continuingListCount.reset();
    /*
     * There's probably no need for this, since its only effect is to disable blank lines in some
     * cases -- and we're doing that already in the footer.
     */
    postWriteModifiedContinuingListCount.reset();

    if (!wroteAnythingSignificant) {
      // Javadoc consists solely of tags. This is frowned upon in general but OK for @Overrides.
    } else if (!continuingFooterTag) {
      // First footer tag after a body tag.
      requestBlankLine();
    } else {
      // Subsequent footer tag.
      continuingFooterTag = false;
      requestNewline();
    }
    writeToken(token);
    continuingFooterTag = true;
  }

  void writeListOpen(Token token) {
    requestBlankLine();

    writeToken(token);
    continuingListItemOfInnermostList = false;
    continuingListCount.increment();
    postWriteModifiedContinuingListCount.increment();

    requestNewline();
  }

  void writeListClose(Token token) {
    requestNewline();

    continuingListItemCount.decrementIfPositive();
    continuingListCount.decrementIfPositive();
    writeToken(token);
    postWriteModifiedContinuingListCount.decrementIfPositive();

    requestBlankLine();
  }

  void writeListItemOpen(Token token) {
    requestNewline();

    if (continuingListItemOfInnermostList) {
      continuingListItemOfInnermostList = false;
      continuingListItemCount.decrementIfPositive();
    }
    writeToken(token);
    continuingListItemOfInnermostList = true;
    continuingListItemCount.increment();
  }

  void writeHeaderOpen(Token token) {
    requestBlankLine();

    writeToken(token);
  }

  void writeHeaderClose(Token token) {
    writeToken(token);

    requestBlankLine();
  }

  void writeParagraphOpen(Token token) {
    if (!wroteAnythingSignificant) {
      /*
       * The user included an initial <p> tag. Ignore it, and don't request a blank line before the
       * next token.
       */
      return;
    }

    requestBlankLine();

    writeToken(token);
  }

  void writeBlockquoteOpenOrClose(Token token) {
    requestBlankLine();

    writeToken(token);

    requestBlankLine();
  }

  void writePreOpen(Token token) {
    requestBlankLine();

    writeToken(token);
  }

  void writePreClose(Token token) {
    writeToken(token);

    requestBlankLine();
  }

  void writeCodeOpen(Token token) {
    writeToken(token);
  }

  void writeCodeClose(Token token) {
    writeToken(token);
  }

  void writeTableOpen(Token token) {
    requestBlankLine();

    writeToken(token);
  }

  void writeTableClose(Token token) {
    writeToken(token);

    requestBlankLine();
  }

  void writeMoeEndStripComment(Token token) {
    writeLineBreakNoAutoIndent();
    appendSpaces(indentForMoeEndStripComment);

    // Or maybe just "output.append(token.getValue())?" I'm kind of surprised this is so easy.
    writeToken(token);

    requestNewline();
  }

  void writeHtmlComment(Token token) {
    requestNewline();

    writeToken(token);

    requestNewline();
  }

  void writeBr(Token token) {
    writeToken(token);

    requestNewline();
  }

  void writeLineBreakNoAutoIndent() {
    writeNewline(NO_AUTO_INDENT);
  }

  void writeLiteral(Token token) {
    writeToken(token);
  }

  @Override
  public String toString() {
    return output.toString();
  }

  private void requestBlankLine() {
    requestWhitespace(BLANK_LINE);
  }

  private void requestNewline() {
    requestWhitespace(NEWLINE);
  }

  private void requestWhitespace(RequestedWhitespace requestedWhitespace) {
    this.requestedWhitespace = max(requestedWhitespace, this.requestedWhitespace);
  }

  /**
   * The kind of whitespace that has been requested between the previous and next tokens. The order
   * of the values is significant: It goes from lowest priority to highest. For example, if the
   * previous token requests {@link #BLANK_LINE} after it but the next token requests only {@link
   * #NEWLINE} before it, we insert {@link #BLANK_LINE}.
   */
  enum RequestedWhitespace {
    NONE,
    WHITESPACE,
    NEWLINE,
    BLANK_LINE,
    ;
  }

  private void writeToken(Token token) {
    if (requestedMoeBeginStripComment != null) {
      requestNewline();
    }

    if (requestedWhitespace == BLANK_LINE
        && (postWriteModifiedContinuingListCount.isPositive() || continuingFooterTag)) {
      /*
       * We don't write blank lines inside lists or footer tags, even in cases where we otherwise
       * would (e.g., before a <p> tag). Justification: We don't write blank lines _between_ list
       * items or footer tags, so it would be strange to write blank lines _within_ one. Of course,
       * an alternative approach would be to go ahead and write blank lines between items/tags,
       * either always or only in the case that an item contains a blank line.
       */
      requestedWhitespace = NEWLINE;
    }

    if (requestedWhitespace == BLANK_LINE) {
      writeBlankLine();
      requestedWhitespace = NONE;
    } else if (requestedWhitespace == NEWLINE) {
      writeNewline();
      requestedWhitespace = NONE;
    }
    boolean needWhitespace = (requestedWhitespace == WHITESPACE);

    /*
     * Write a newline if necessary to respect the line limit. (But if we're at the beginning of the
     * line, a newline won't help. Or it might help but only by separating "<p>veryverylongword,"
     * which goes against our style.)
     */
    if (!atStartOfLine && token.length() + (needWhitespace ? 1 : 0) > remainingOnLine) {
      writeNewline();
    }
    if (!atStartOfLine && needWhitespace) {
      output.append(" ");
      remainingOnLine--;
    }

    if (requestedMoeBeginStripComment != null) {
      output.append(requestedMoeBeginStripComment.getValue());
      requestedMoeBeginStripComment = null;
      indentForMoeEndStripComment = innerIndent();
      requestNewline();
      writeToken(token);
      return;
    }

    output.append(token.getValue());

    if (!START_OF_LINE_TOKENS.contains(token.getType())) {
      atStartOfLine = false;
    }

    /*
     * TODO(cpovirk): We really want the number of "characters," not chars. Figure out what the
     * right way of measuring that is (grapheme count (with BreakIterator?)? sum of widths of all
     * graphemes? I don't think that our style guide is specific about this.). Moreover, I am
     * probably brushing other problems with surrogates, etc. under the table. Hopefully I mostly
     * get away with it by joining all non-space, non-tab characters together.
     *
     * Possibly the "width" question has no right answer:
     * http://denisbider.blogspot.com/2015/09/when-monospace-fonts-arent-unicode.html
     */
    remainingOnLine -= token.length();
    requestedWhitespace = NONE;
    wroteAnythingSignificant = true;
  }

  private void writeBlankLine() {
    output.append("\n");
    appendSpaces(blockIndent + 1);
    output.append("*");
    writeNewline();
  }

  private void writeNewline() {
    writeNewline(AUTO_INDENT);
  }

  private void writeNewline(AutoIndent autoIndent) {
    output.append("\n");
    appendSpaces(blockIndent + 1);
    output.append("*");
    appendSpaces(1);
    remainingOnLine = JavadocFormatter.MAX_LINE_LENGTH - blockIndent - 3;
    if (autoIndent == AUTO_INDENT) {
      appendSpaces(innerIndent());
      remainingOnLine -= innerIndent();
    }
    atStartOfLine = true;
  }

  enum AutoIndent {
    AUTO_INDENT,
    NO_AUTO_INDENT
  }

  private int innerIndent() {
    int innerIndent = continuingListItemCount.value() * 4 + continuingListCount.value() * 2;
    if (continuingFooterTag) {
      innerIndent += 4;
    }
    return innerIndent;
  }

  // If this is a hotspot, keep a String of many spaces around, and call append(string, start, end).
  private void appendSpaces(int count) {
    output.append(" ".repeat(count));
  }

  /**
   * Tokens that are always pinned to the following token. For example, {@code <p>} in {@code <p>Foo
   * bar} (never {@code <p> Foo bar} or {@code <p>\nFoo bar}).
   *
   * <p>This is not the only kind of "pinning" that we do: See also the joining of LITERAL tokens
   * done by the lexer. The special pinning here is necessary because these tokens are not of type
   * LITERAL (because they require other special handling).
   */
  private static final ImmutableSet<Type> START_OF_LINE_TOKENS =
      immutableEnumSet(LIST_ITEM_OPEN_TAG, PARAGRAPH_OPEN_TAG, HEADER_OPEN_TAG);
}
