diff --git a/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocFormatter.java b/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocFormatter.java index b39e297d3..e2206a8d1 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocFormatter.java +++ b/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocFormatter.java @@ -25,6 +25,7 @@ import com.google.googlejavaformat.java.javadoc.JavadocLexer.LexException; import com.google.googlejavaformat.java.javadoc.Token.BeginJavadoc; import com.google.googlejavaformat.java.javadoc.Token.BlockQuoteCloseTag; +import com.google.googlejavaformat.java.javadoc.Token.BlockQuoteMarker; import com.google.googlejavaformat.java.javadoc.Token.BlockQuoteOpenTag; import com.google.googlejavaformat.java.javadoc.Token.BrTag; import com.google.googlejavaformat.java.javadoc.Token.CodeCloseTag; @@ -40,6 +41,8 @@ import com.google.googlejavaformat.java.javadoc.Token.ListItemOpenTag; import com.google.googlejavaformat.java.javadoc.Token.ListOpenTag; import com.google.googlejavaformat.java.javadoc.Token.Literal; +import com.google.googlejavaformat.java.javadoc.Token.MarkdownBlockQuoteClose; +import com.google.googlejavaformat.java.javadoc.Token.MarkdownBlockQuoteOpen; import com.google.googlejavaformat.java.javadoc.Token.MarkdownCodeSpanEnd; import com.google.googlejavaformat.java.javadoc.Token.MarkdownCodeSpanStart; import com.google.googlejavaformat.java.javadoc.Token.MarkdownFencedCodeBlock; @@ -121,6 +124,8 @@ private static String render(List input, int blockIndent, boolean classic case ParagraphOpenTag t -> output.writeParagraphOpen(standardizePToken(t)); case BlockQuoteOpenTag t -> output.writeBlockQuoteOpen(t); case BlockQuoteCloseTag t -> output.writeBlockQuoteClose(t); + case MarkdownBlockQuoteOpen t -> output.writeMarkdownBlockQuoteOpen(t); + case MarkdownBlockQuoteClose t -> output.writeMarkdownBlockQuoteClose(); case PreOpenTag t -> output.writePreOpen(t); case PreCloseTag t -> output.writePreClose(t); case CodeOpenTag t -> output.writeCodeOpen(t); @@ -142,6 +147,7 @@ private static String render(List input, int blockIndent, boolean classic case ParagraphCloseTag unused -> {} case MarkdownCodeSpanStart unused -> {} case MarkdownCodeSpanEnd unused -> {} + case BlockQuoteMarker unused -> {} } } throw new AssertionError(); diff --git a/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocLexer.java b/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocLexer.java index 9657348db..60a6054ee 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocLexer.java +++ b/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocLexer.java @@ -29,6 +29,7 @@ import com.google.common.collect.PeekingIterator; import com.google.googlejavaformat.java.javadoc.Token.BeginJavadoc; import com.google.googlejavaformat.java.javadoc.Token.BlockQuoteCloseTag; +import com.google.googlejavaformat.java.javadoc.Token.BlockQuoteMarker; import com.google.googlejavaformat.java.javadoc.Token.BlockQuoteOpenTag; import com.google.googlejavaformat.java.javadoc.Token.BrTag; import com.google.googlejavaformat.java.javadoc.Token.CodeCloseTag; @@ -44,6 +45,8 @@ import com.google.googlejavaformat.java.javadoc.Token.ListItemOpenTag; import com.google.googlejavaformat.java.javadoc.Token.ListOpenTag; import com.google.googlejavaformat.java.javadoc.Token.Literal; +import com.google.googlejavaformat.java.javadoc.Token.MarkdownBlockQuoteClose; +import com.google.googlejavaformat.java.javadoc.Token.MarkdownBlockQuoteOpen; import com.google.googlejavaformat.java.javadoc.Token.MarkdownCodeSpanEnd; import com.google.googlejavaformat.java.javadoc.Token.MarkdownCodeSpanStart; import com.google.googlejavaformat.java.javadoc.Token.MarkdownHardLineBreak; @@ -139,7 +142,10 @@ enum NestingContext { * An inline tag such as {@code {@link ...}} or {@code {@code ...}}, but not {@code {@snippet * ...}}. */ - INLINE_TAG_CONTEXT + INLINE_TAG_CONTEXT, + + /** Markdown {@code >}. */ + BLOCKQUOTE } private final CharStream input; @@ -171,21 +177,7 @@ private ImmutableList generateTokens() throws LexException { // assumed that there are no other tokens (markdown or otherwise) in a non-empty text span // covered by a markdown token. for (Token markdownToken : markdownPositions.tokensAt(input.position())) { - // For `...`, we switch to MARKDOWN_CODE_CONTEXT for the duration of the span, and we - // change the start or end token to a Literal so it will get joined to adjacent Literal - // tokens. That prevents line breaks adjacent to the backticks in "foo`bar`baz", but still - // allows them at the spaces in "foo `bar` baz" or "foo` bar `baz". - switch (markdownToken) { - case MarkdownCodeSpanStart unused -> { - contextStack.push(NestingContext.MARKDOWN_CODE_CONTEXT); - markdownToken = new Literal(markdownToken.value()); - } - case MarkdownCodeSpanEnd unused -> { - contextStack.popUntil(NestingContext.MARKDOWN_CODE_CONTEXT); - markdownToken = new Literal(markdownToken.value()); - } - default -> {} - } + markdownToken = processMarkdownToken(markdownToken); tokens.add(markdownToken); if (!markdownToken.value().isEmpty()) { boolean consumed = input.tryConsume(markdownToken.value()); @@ -202,6 +194,11 @@ private ImmutableList generateTokens() throws LexException { tokens.add(token); } + for (Token markdownToken : markdownPositions.tokensAt(input.position())) { + markdownToken = processMarkdownToken(markdownToken); + tokens.add(markdownToken); + } + checkMatchingTags(); token = new EndJavadoc(classicJavadoc ? "*/" : ""); @@ -217,6 +214,31 @@ private ImmutableList generateTokens() throws LexException { return result; } + private Token processMarkdownToken(Token markdownToken) { + // For `...`, we switch to MARKDOWN_CODE_CONTEXT for the duration of the span, and we change the + // start or end token to a Literal so it will get joined to adjacent Literal tokens. That + // prevents line breaks adjacent to the backticks in "foo`bar`baz", but still allows them at the + // spaces in "foo `bar` baz" or "foo` bar `baz". + switch (markdownToken) { + case MarkdownCodeSpanStart unused -> { + contextStack.push(NestingContext.MARKDOWN_CODE_CONTEXT); + return new Literal(markdownToken.value()); + } + case MarkdownCodeSpanEnd unused -> { + contextStack.popUntil(NestingContext.MARKDOWN_CODE_CONTEXT); + return new Literal(markdownToken.value()); + } + case MarkdownBlockQuoteOpen unused -> { + contextStack.push(NestingContext.BLOCKQUOTE); + } + case MarkdownBlockQuoteClose unused -> { + contextStack.popUntil(NestingContext.BLOCKQUOTE); + } + default -> {} + } + return markdownToken; + } + private Token readToken() throws LexException { Function tokenFactory = consumeToken(); String value = input.readAndResetRecorded(); @@ -230,7 +252,16 @@ private Function consumeToken() throws LexException { if (input.tryConsumeRegex(newlinePattern)) { somethingSinceNewline = false; return preserveExistingFormatting ? ForcedNewline::new : Whitespace::new; - } else if (input.tryConsume(" ") || input.tryConsume("\t")) { + } + + if (!classicJavadoc + && !somethingSinceNewline + && contextStack.contains(NestingContext.BLOCKQUOTE) + && input.tryConsumeRegex(BLOCKQUOTE_MARKER_PATTERN)) { + return BlockQuoteMarker::new; + } + + if (input.tryConsume(" ") || input.tryConsume("\t")) { // TODO(cpovirk): How about weird whitespace chars? Ideally we'd distinguish breaking vs. not. // Returning Literal here prevents us from breaking a
 line. For more info, see Literal.
       return preserveExistingFormatting ? Literal::new : Whitespace::new;
@@ -663,6 +694,7 @@ static boolean hasMultipleNewlines(String s) {
    * initial whitespace have been removed at the point where this pattern is applied.
    */
   private static final Pattern MARKDOWN_NEWLINE_PATTERN = compile("[ \t]*\n");
+  private static final Pattern BLOCKQUOTE_MARKER_PATTERN = compile("> ?");
 
   // We ensure elsewhere that we match this only at the beginning of a line.
   // Only match tags that start with a lowercase letter, to avoid false matches on unescaped
diff --git a/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocWriter.java b/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocWriter.java
index 1f04bde84..061fdcc44 100644
--- a/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocWriter.java
+++ b/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocWriter.java
@@ -36,6 +36,7 @@
 import com.google.googlejavaformat.java.javadoc.Token.ListItemOpenTag;
 import com.google.googlejavaformat.java.javadoc.Token.ListOpenTag;
 import com.google.googlejavaformat.java.javadoc.Token.Literal;
+import com.google.googlejavaformat.java.javadoc.Token.MarkdownBlockQuoteOpen;
 import com.google.googlejavaformat.java.javadoc.Token.MarkdownFencedCodeBlock;
 import com.google.googlejavaformat.java.javadoc.Token.MarkdownTable;
 import com.google.googlejavaformat.java.javadoc.Token.MoeBeginStripComment;
@@ -73,14 +74,13 @@ final class JavadocWriter {
   private boolean continuingListItemOfInnermostList;
 
   private boolean continuingFooterTag;
-  private final NestingStack.Int continuingListItemStack = new NestingStack.Int();
-  private final NestingStack.Int continuingListStack = new NestingStack.Int();
+  private final NestingStack indentStack = new NestingStack<>();
   private final NestingStack.Int postWriteModifiedContinuingListStack = new NestingStack.Int();
   private int remainingOnLine;
   private boolean atStartOfLine;
   private RequestedWhitespace requestedWhitespace = NONE;
   private Token requestedMoeBeginStripComment;
-  private int indentForMoeEndStripComment;
+  private String indentForMoeEndStripComment = "";
   private boolean wroteAnythingSignificant;
 
   JavadocWriter(int blockIndent, boolean classicJavadoc) {
@@ -155,8 +155,7 @@ void writeFooterJavadocTagStart(FooterJavadocTagStart token) {
      * currently know which of those tags are open.
      */
     continuingListItemOfInnermostList = false;
-    continuingListItemStack.reset();
-    continuingListStack.reset();
+    indentStack.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.
@@ -215,7 +214,7 @@ void writeListOpen(ListOpenTag token) {
     writeToken(token);
     continuingListItemOfInnermostList = false;
     int indent = token.value().isEmpty() ? 0 : 2; // No indent for Markdown since no explicit open
-    continuingListStack.push(indent);
+    indentStack.push(new ListIndent(indent));
     postWriteModifiedContinuingListStack.push();
 
     requestNewline();
@@ -226,8 +225,7 @@ void writeListClose(ListCloseTag token) {
       requestNewline();
     }
 
-    continuingListItemStack.popIfNotEmpty();
-    continuingListStack.popIfNotEmpty();
+    indentStack.popUntil(ListIndent.class);
     writeToken(token);
     postWriteModifiedContinuingListStack.popIfNotEmpty();
 
@@ -240,13 +238,14 @@ void writeListItemOpen(ListItemOpenTag token) {
     requestNewline();
 
     if (continuingListItemOfInnermostList) {
+      // TODO(cpovirk): consider whether we can handle this only with operations on the indent stack
       continuingListItemOfInnermostList = false;
-      continuingListItemStack.popIfNotEmpty();
+      indentStack.popUntil(ListItemIndent.class);
     }
     writeToken(token);
     continuingListItemOfInnermostList = true;
     int indent = token.value().length();
-    continuingListItemStack.push(indent);
+    indentStack.push(new ListItemIndent(indent));
   }
 
   void writeHeaderOpen(HeaderOpenTag token) {
@@ -277,20 +276,28 @@ void writeParagraphOpen(Token token) {
 
   void writeBlockQuoteOpen(BlockQuoteOpenTag token) {
     requestBlankLine();
-
     writeToken(token);
-
     requestNewline();
   }
 
   void writeBlockQuoteClose(BlockQuoteCloseTag token) {
     requestNewline();
-
     writeToken(token);
-
     requestBlankLine();
   }
 
+  void writeMarkdownBlockQuoteOpen(MarkdownBlockQuoteOpen token) {
+    if (!atStartOfLine) {
+      requestNewline();
+    }
+    writeToken(new MarkdownBlockQuoteOpen("> "));
+    indentStack.push(new BlockQuoteIndent());
+  }
+
+  void writeMarkdownBlockQuoteClose() {
+    indentStack.popUntil(BlockQuoteIndent.class);
+  }
+
   void writePreOpen(PreOpenTag token) {
     requestBlankLine();
 
@@ -325,7 +332,7 @@ void writeTableClose(TableCloseTag token) {
 
   void writeMoeEndStripComment(MoeEndStripComment token) {
     writeLineBreakNoAutoIndent();
-    appendSpaces(indentForMoeEndStripComment);
+    output.append(indentForMoeEndStripComment);
 
     // Or maybe just "output.append(token.getValue())?" I'm kind of surprised this is so easy.
     writeToken(token);
@@ -477,7 +484,7 @@ private void writeToken(Token token) {
     if (requestedMoeBeginStripComment != null) {
       output.append(requestedMoeBeginStripComment.value());
       requestedMoeBeginStripComment = null;
-      indentForMoeEndStripComment = innerIndent();
+      indentForMoeEndStripComment = innerIndentString();
       wroteAnythingSignificant = true;
       requestNewline();
       writeToken(token);
@@ -513,6 +520,14 @@ private void writeNewlineStart() {
 
   private void writeBlankLine() {
     writeNewlineStart();
+    String indent = innerIndentString();
+    if (!indent.isBlank()) {
+      // The motivation here is that we may have a blank line that is inside a Markdown block quote.
+      // Then we still want the `>` marker or markers at the start of the line, but we don't want
+      // any trailing whitespace.
+      output.append(" ");
+      output.append(indent.stripTrailing());
+    }
     writeNewline();
   }
 
@@ -525,8 +540,9 @@ private void writeNewline(AutoIndent autoIndent) {
     appendSpaces(1);
     remainingOnLine = JavadocFormatter.MAX_LINE_LENGTH - blockIndent - (classicJavadoc ? 3 : 4);
     if (autoIndent == AUTO_INDENT) {
-      appendSpaces(innerIndent());
-      remainingOnLine -= innerIndent();
+      String indent = innerIndentString();
+      output.append(indent);
+      remainingOnLine -= indent.length();
     }
     atStartOfLine = true;
   }
@@ -536,12 +552,40 @@ enum AutoIndent {
     NO_AUTO_INDENT
   }
 
-  private int innerIndent() {
-    int innerIndent = continuingListItemStack.total() + continuingListStack.total();
+  private String innerIndentString() {
+    StringBuilder sb = new StringBuilder();
     if (continuingFooterTag) {
-      innerIndent += classicJavadoc ? 4 : 2;
+      sb.repeat(' ', classicJavadoc ? 4 : 2);
+    }
+    for (Indent indent : indentStack.bottomToTop()) {
+      indent.render(sb);
+    }
+    return sb.toString();
+  }
+
+  private sealed interface Indent {
+    void render(StringBuilder sb);
+  }
+
+  private record ListIndent(int width) implements Indent {
+    @Override
+    public void render(StringBuilder sb) {
+      sb.repeat(' ', width);
+    }
+  }
+
+  private record ListItemIndent(int width) implements Indent {
+    @Override
+    public void render(StringBuilder sb) {
+      sb.repeat(' ', width);
+    }
+  }
+
+  private record BlockQuoteIndent() implements Indent {
+    @Override
+    public void render(StringBuilder sb) {
+      sb.append("> ");
     }
-    return innerIndent;
   }
 
   private void appendSpaces(int count) {
diff --git a/core/src/main/java/com/google/googlejavaformat/java/javadoc/MarkdownPositions.java b/core/src/main/java/com/google/googlejavaformat/java/javadoc/MarkdownPositions.java
index 27ebfc441..50d9b1f58 100644
--- a/core/src/main/java/com/google/googlejavaformat/java/javadoc/MarkdownPositions.java
+++ b/core/src/main/java/com/google/googlejavaformat/java/javadoc/MarkdownPositions.java
@@ -25,6 +25,8 @@
 import com.google.googlejavaformat.java.javadoc.Token.ListItemCloseTag;
 import com.google.googlejavaformat.java.javadoc.Token.ListItemOpenTag;
 import com.google.googlejavaformat.java.javadoc.Token.ListOpenTag;
+import com.google.googlejavaformat.java.javadoc.Token.MarkdownBlockQuoteClose;
+import com.google.googlejavaformat.java.javadoc.Token.MarkdownBlockQuoteOpen;
 import com.google.googlejavaformat.java.javadoc.Token.MarkdownCodeSpanEnd;
 import com.google.googlejavaformat.java.javadoc.Token.MarkdownCodeSpanStart;
 import com.google.googlejavaformat.java.javadoc.Token.MarkdownFencedCodeBlock;
@@ -32,6 +34,7 @@
 import com.google.googlejavaformat.java.javadoc.Token.ParagraphCloseTag;
 import com.google.googlejavaformat.java.javadoc.Token.ParagraphOpenTag;
 import java.util.Objects;
+import java.util.function.Function;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import org.commonmark.ext.gfm.tables.TableBlock;
@@ -105,8 +108,7 @@ void visit(Node node) {
           alreadyVisitedChildren = true;
         }
         case Code code -> visitCodeSpan(code);
-        case BlockQuote blockQuote ->
-            throw new UnsupportedOperationException("Block quotes not supported");
+        case BlockQuote blockQuote -> alreadyVisitedChildren = visitBlockQuote(blockQuote);
         case IndentedCodeBlock indentedCodeBlock ->
             throw new UnsupportedOperationException("Indented code blocks not supported");
         case ThematicBreak thematicBreak ->
@@ -123,17 +125,41 @@ void visit(Node node) {
 
     // Returns true if this method visited the children of the given ListItem.
     private boolean visitListItem(ListItem listItem) {
-      int startPosition =
-          listItem.getSourceSpans().getFirst().getInputIndex() + listItem.getMarkerIndent();
-      Matcher matcher =
-          LIST_ITEM_START_PATTERN.matcher(input).region(startPosition, input.length());
+      return visitListItemOrBlockQuote(
+          listItem,
+          startPosition(listItem) + listItem.getMarkerIndent(),
+          LIST_ITEM_START_PATTERN,
+          ListItemOpenTag::new,
+          LIST_ITEM_CLOSE_TOKEN);
+    }
+
+    // Returns true if this method visited the children of the given BlockQuote.
+    private boolean visitBlockQuote(BlockQuote blockQuote) {
+      // CommonMark can include preceding indentation in the BlockQuote node.
+      int start = input.indexOf('>', startPosition(blockQuote));
+      return visitListItemOrBlockQuote(
+          blockQuote,
+          start,
+          BLOCKQUOTE_START_PATTERN,
+          MarkdownBlockQuoteOpen::new,
+          BLOCKQUOTE_CLOSE_TOKEN);
+    }
+
+    private boolean visitListItemOrBlockQuote(
+        Node node,
+        int start,
+        Pattern startPattern,
+        Function openTokenFactory,
+        Token closeToken) {
+      Matcher matcher = startPattern.matcher(input).region(start, input.length());
       verify(matcher.lookingAt());
-      ListItemOpenTag openToken = new ListItemOpenTag(matcher.group(1));
-      addSpan(listItem, openToken, LIST_ITEM_CLOSE_TOKEN, startPosition);
-      if (listItem.getFirstChild() instanceof Paragraph paragraph) {
-        // A ListItem typically contains a Paragraph, but we don't want to visit that Paragraph
-        // because that would lead us to introduce a line break after the list introduction
-        // (the `-` or whatever). So we visit the children and siblings of the Paragraph instead.
+      Token openToken = openTokenFactory.apply(matcher.group(1));
+      addSpan(node, openToken, closeToken, matcher.start(1));
+      if (node.getFirstChild() instanceof Paragraph paragraph) {
+        // A ListItem or BlockQuote typically contains a Paragraph, but we don't want to visit that
+        // Paragraph because that would lead us to introduce a line break after the list or quote
+        // introduction (the `-` or whatever). So we visit the children and siblings of the
+        // Paragraph instead.
         visitNodeList(paragraph.getFirstChild());
         visitNodeList(paragraph.getNext());
         return true;
@@ -257,6 +283,9 @@ public String toString() {
   private static final ListOpenTag LIST_OPEN_TOKEN = new ListOpenTag("");
   private static final ListCloseTag LIST_CLOSE_TOKEN = new ListCloseTag("");
   private static final ListItemCloseTag LIST_ITEM_CLOSE_TOKEN = new ListItemCloseTag("");
+  private static final MarkdownBlockQuoteClose BLOCKQUOTE_CLOSE_TOKEN =
+      new MarkdownBlockQuoteClose("");
+  private static final Pattern BLOCKQUOTE_START_PATTERN = Pattern.compile("(> ?)");
 
   private static final Pattern LIST_ITEM_START_PATTERN =
       Pattern.compile("(([-+*]|[0-9]+[.)])(?:\\s|$))");
diff --git a/core/src/main/java/com/google/googlejavaformat/java/javadoc/NestingStack.java b/core/src/main/java/com/google/googlejavaformat/java/javadoc/NestingStack.java
index 29197b8b5..7e0489b93 100644
--- a/core/src/main/java/com/google/googlejavaformat/java/javadoc/NestingStack.java
+++ b/core/src/main/java/com/google/googlejavaformat/java/javadoc/NestingStack.java
@@ -26,11 +26,18 @@
  * lists, the entries represent indentation levels, and those depend on whether the list is an HTML
  * list or a Markdown list.
  *
+ * 

Iterating over the stack goes from the bottom to the top. So for an indentation stack, for + * example, it produces the elements of indentation in the correct order. + * * @param The type of the elements in the stack. */ final class NestingStack { private final Deque stack = new ArrayDeque<>(); + Iterable bottomToTop() { + return stack::descendingIterator; + } + void push(E value) { stack.push(value); } @@ -54,6 +61,12 @@ void popUntil(E value) { } } + void popUntil(Class valueClass) { + if (stack.stream().anyMatch(valueClass::isInstance)) { + while (!valueClass.isInstance(stack.pop())) {} + } + } + boolean contains(E value) { return stack.contains(value); } diff --git a/core/src/main/java/com/google/googlejavaformat/java/javadoc/Token.java b/core/src/main/java/com/google/googlejavaformat/java/javadoc/Token.java index ad7a9575e..f4d2cb670 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/javadoc/Token.java +++ b/core/src/main/java/com/google/googlejavaformat/java/javadoc/Token.java @@ -78,10 +78,14 @@ record ParagraphOpenTag(String value) implements Token, StartOfLineToken {} record ParagraphCloseTag(String value) implements Token {} - record BlockQuoteOpenTag(String value) implements Token {} + record BlockQuoteOpenTag(String value) implements Token, StartOfLineToken {} record BlockQuoteCloseTag(String value) implements Token {} + record MarkdownBlockQuoteOpen(String value) implements Token, StartOfLineToken {} + + record MarkdownBlockQuoteClose(String value) implements Token {} + record PreOpenTag(String value) implements Token {} record PreCloseTag(String value) implements Token {} @@ -181,6 +185,12 @@ record MarkdownHardLineBreak(String value) implements Token {} */ record OptionalLineBreak(String value) implements Token {} + /** + * Token representing a Markdown block quote marker ({@code >}). This token causes the + * corresponding text to be consumed, but does not itself lead to any output. + */ + record BlockQuoteMarker(String value) implements Token {} + /** * Anything else: {@code foo}, {@code }, {@code {@code foo}} etc. {@link JavadocLexer} * sometimes creates adjacent literal tokens, which it then merges into a single, larger literal diff --git a/core/src/test/java/com/google/googlejavaformat/java/JavadocFormattingTest.java b/core/src/test/java/com/google/googlejavaformat/java/JavadocFormattingTest.java index 7a60cb41d..2ab647c9f 100644 --- a/core/src/test/java/com/google/googlejavaformat/java/JavadocFormattingTest.java +++ b/core/src/test/java/com/google/googlejavaformat/java/JavadocFormattingTest.java @@ -2111,12 +2111,112 @@ public void markdownBlockQuotes() { /// > foo /// > bar /// + /// baz + class Test {} + """; + String expected = + """ + /// > foo bar /// + /// baz class Test {} """; - // TODO: block quotes are not supported. That means the input is unchanged. We can see this - // because the extra blank lines at the end are preserved. - String expected = input; + doFormatTest(input, expected); + } + + @Test + public void markdownBlockQuoteWithinListItem() { + assume().that(MARKDOWN_JAVADOC_SUPPORTED).isTrue(); + String input = + """ + /// 1. item one + /// 2. item two + /// - sublist + /// - sublist + /// - > foo + /// > bar + class Test {} + """; + String expected = + """ + /// 1. item one + /// 2. item two + /// - sublist + /// - sublist + /// - > foo bar + class Test {} + """; + doFormatTest(input, expected); + } + + @Test + public void markdownNestedBlockQuotes() { + assume().that(MARKDOWN_JAVADOC_SUPPORTED).isTrue(); + String input = + """ + /// > foo + /// > > bar + /// > > baz + class Test {} + """; + String expected = + """ + /// > foo + /// > > bar baz + class Test {} + """; + doFormatTest(input, expected); + } + + @Test + public void markdownBlockQuoteWithCodeBlockInside() { + assume().that(MARKDOWN_JAVADOC_SUPPORTED).isTrue(); + String input = + """ + /// > foo + /// > ``` + /// > > + /// > ``` + /// > bar + class Test {} + """; + String expected = + """ + /// > foo + /// > + /// > ``` + /// > > + /// > ``` + /// > + /// > bar + class Test {} + """; + doFormatTest(input, expected); + } + + @Test + public void markdownBlockQuoteInBlockTag() { + assume().that(MARKDOWN_JAVADOC_SUPPORTED).isTrue(); + String input = +""" +/// A test class. +/// @param foo a Foo +/// > In the reign of James the Second +/// > It was generally reckoned +/// > As a very serious crime +/// > To marry two wives at one time. +class Test {} +"""; + // TODO(emcmanus): the blank lines here should not be present. + String expected = +""" +/// A test class. +/// +/// @param foo a Foo +/// > In the reign of James the Second It was generally reckoned As a very serious crime To marry +/// > two wives at one time. +class Test {} +"""; doFormatTest(input, expected); }