diff --git a/core/pom.xml b/core/pom.xml index d3235cb18..b6f070c5c 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -39,11 +39,6 @@ com.google.guava guava - - org.commonmark - commonmark - 0.28.0 - @@ -66,6 +61,17 @@ auto-service-annotations true + + org.commonmark + commonmark + 0.28.0 + + + org.commonmark + commonmark-ext-gfm-tables + 0.28.0 + compile + 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 dbc2be841..f801a4d75 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 @@ -44,6 +44,7 @@ import com.google.googlejavaformat.java.javadoc.Token.MarkdownCodeSpanStart; import com.google.googlejavaformat.java.javadoc.Token.MarkdownFencedCodeBlock; import com.google.googlejavaformat.java.javadoc.Token.MarkdownHardLineBreak; +import com.google.googlejavaformat.java.javadoc.Token.MarkdownTable; import com.google.googlejavaformat.java.javadoc.Token.MoeBeginStripComment; import com.google.googlejavaformat.java.javadoc.Token.MoeEndStripComment; import com.google.googlejavaformat.java.javadoc.Token.OptionalLineBreak; @@ -137,6 +138,7 @@ private static String render(List input, int blockIndent, boolean classic case MarkdownHardLineBreak unused -> output.writeMarkdownHardLineBreak(); case Literal t -> output.writeLiteral(t); case MarkdownFencedCodeBlock t -> output.writeMarkdownFencedCodeBlock(t); + case MarkdownTable t -> output.writeMarkdownTable(t); case ListItemCloseTag unused -> {} case OptionalLineBreak unused -> {} case ParagraphCloseTag unused -> {} 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 492bb0150..ae3226301 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 @@ -35,6 +35,7 @@ import com.google.googlejavaformat.java.javadoc.Token.ListOpenTag; import com.google.googlejavaformat.java.javadoc.Token.Literal; import com.google.googlejavaformat.java.javadoc.Token.MarkdownFencedCodeBlock; +import com.google.googlejavaformat.java.javadoc.Token.MarkdownTable; import com.google.googlejavaformat.java.javadoc.Token.MoeBeginStripComment; import com.google.googlejavaformat.java.javadoc.Token.MoeEndStripComment; import com.google.googlejavaformat.java.javadoc.Token.PreCloseTag; @@ -44,6 +45,7 @@ import com.google.googlejavaformat.java.javadoc.Token.StartOfLineToken; import com.google.googlejavaformat.java.javadoc.Token.TableCloseTag; import com.google.googlejavaformat.java.javadoc.Token.TableOpenTag; +import java.util.List; /** * Stateful object that accepts "requests" and "writes," producing formatted Javadoc. @@ -335,6 +337,9 @@ void writeLiteral(Literal token) { } void writeMarkdownFencedCodeBlock(MarkdownFencedCodeBlock token) { + if (!token.precededByNonWhitespace() && wroteAnythingSignificant) { + requestBlankLine(); + } flushWhitespace(); output.append(token.start()); token @@ -350,6 +355,20 @@ void writeMarkdownFencedCodeBlock(MarkdownFencedCodeBlock token) { requestBlankLine(); } + void writeMarkdownTable(MarkdownTable token) { + if (!token.precededByNonWhitespace() && wroteAnythingSignificant) { + requestBlankLine(); + } + flushWhitespace(); + List lines = token.value().lines().toList(); + output.append(lines.get(0)); + for (String line : lines.subList(1, lines.size())) { + writeNewline(AutoIndent.NO_AUTO_INDENT); + output.append(line); + } + requestBlankLine(); + } + @Override public String toString() { return output.toString(); 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 7f66b32e8..11cddea68 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 @@ -28,10 +28,14 @@ import com.google.googlejavaformat.java.javadoc.Token.MarkdownCodeSpanEnd; import com.google.googlejavaformat.java.javadoc.Token.MarkdownCodeSpanStart; import com.google.googlejavaformat.java.javadoc.Token.MarkdownFencedCodeBlock; +import com.google.googlejavaformat.java.javadoc.Token.MarkdownTable; import com.google.googlejavaformat.java.javadoc.Token.ParagraphCloseTag; import com.google.googlejavaformat.java.javadoc.Token.ParagraphOpenTag; +import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.commonmark.ext.gfm.tables.TableBlock; +import org.commonmark.ext.gfm.tables.TablesExtension; import org.commonmark.node.BulletList; import org.commonmark.node.Code; import org.commonmark.node.FencedCodeBlock; @@ -92,6 +96,10 @@ void visit(Node node) { case OrderedList orderedList -> addSpan(orderedList, LIST_OPEN_TOKEN, LIST_CLOSE_TOKEN); case ListItem listItem -> alreadyVisitedChildren = visitListItem(listItem); case FencedCodeBlock fencedCodeBlock -> visitFencedCodeBlock(fencedCodeBlock); + case TableBlock tableBlock -> { + visitTableBlock(tableBlock); + alreadyVisitedChildren = true; + } case Code code -> visitCodeSpan(code); // TODO: others default -> {} @@ -128,16 +136,30 @@ private void visitFencedCodeBlock(FencedCodeBlock fencedCodeBlock) { // indentation gets subtracted from FencedCodeBlock.getLiteral(), which is the actual text // represented by the code block. int start = startPosition(fencedCodeBlock) + fencedCodeBlock.getFenceIndent(); + boolean precededByNonWhitespace = precededByNonWhitespace(start); + int closingLength = + Objects.requireNonNullElse( + fencedCodeBlock.getClosingFenceLength(), fencedCodeBlock.getOpeningFenceLength()); MarkdownFencedCodeBlock token = new MarkdownFencedCodeBlock( input.substring(start, endPosition(fencedCodeBlock)), fencedCodeBlock.getFenceCharacter().repeat(fencedCodeBlock.getOpeningFenceLength()) + fencedCodeBlock.getInfo(), - fencedCodeBlock.getFenceCharacter().repeat(fencedCodeBlock.getClosingFenceLength()), - fencedCodeBlock.getLiteral()); + fencedCodeBlock.getFenceCharacter().repeat(closingLength), + fencedCodeBlock.getLiteral(), + precededByNonWhitespace); positionToToken.get(start).addLast(token); } + private void visitTableBlock(TableBlock tableBlock) { + int start = startPosition(tableBlock); + boolean precededByNonWhitespace = precededByNonWhitespace(start); + int end = endPosition(tableBlock); + positionToToken + .get(start) + .addLast(new MarkdownTable(input.substring(start, end), precededByNonWhitespace)); + } + private void visitCodeSpan(Code code) { int start = startPosition(code); int end = endPosition(code); @@ -164,6 +186,15 @@ private void visitNodeList(Node node) { } } + private boolean precededByNonWhitespace(int position) { + for (int i = position - 1; i >= 0 && input.charAt(i) != '\n'; i--) { + if (!Character.isWhitespace(input.charAt(i))) { + return true; + } + } + return false; + } + /** * Adds tokens for the given node, {@code startToken} at the point where the node starts in the * input, and {@code endToken} at the point where it ends. The {@code startToken} goes after any @@ -195,7 +226,10 @@ public String toString() { } private static final Parser PARSER = - Parser.builder().includeSourceSpans(IncludeSourceSpans.BLOCKS_AND_INLINES).build(); + Parser.builder() + .includeSourceSpans(IncludeSourceSpans.BLOCKS_AND_INLINES) + .extensions(ImmutableList.of(TablesExtension.create())) + .build(); private static final HeaderOpenTag HEADER_OPEN_TOKEN = new HeaderOpenTag(""); private static final HeaderCloseTag HEADER_CLOSE_TOKEN = new HeaderCloseTag(""); 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 134fbc8f6..e66a5c4cc 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 @@ -135,10 +135,29 @@ record MarkdownCodeSpanEnd(String value) implements Token {} * @param end the end fence. * @param literal the text that the code block represents. This does not include the start and end * fences, nor any indentation that precedes these fences and every intervening line. + * @param precededByNonWhitespace whether the start of the code block is preceded by at least one + * non-whitespace character on the same line, for example {@code - ```}. */ - record MarkdownFencedCodeBlock(String value, String start, String end, String literal) + record MarkdownFencedCodeBlock( + String value, String start, String end, String literal, boolean precededByNonWhitespace) implements Token {} + /** + * A Markdown table, like: + * + * {@snippet : + * | foo | bar | + * | --- | --- | + * | baz | qux | + * } + * + * @param value the full text of the table as it appeared in the input, including the delimiters + * and the literal content. + * @param precededByNonWhitespace whether the start of the table is preceded by at least one + * non-whitespace character on the same line, for example {@code - |foo|bar|}. + */ + record MarkdownTable(String value, boolean precededByNonWhitespace) implements Token {} + /** * Whitespace that is not in a {@code
} or {@code } section. Whitespace includes
    * leading newlines, asterisks, and tabs and spaces. In the output, it is translated to newlines
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 bf7ac1c2d..983d66387 100644
--- a/core/src/test/java/com/google/googlejavaformat/java/JavadocFormattingTest.java
+++ b/core/src/test/java/com/google/googlejavaformat/java/JavadocFormattingTest.java
@@ -1738,6 +1738,12 @@ public void markdownFencedCodeBlocks() {
 ///    in a list
 ///    ```
 ///
+/// - flibbertigibbet
+///
+///   ```
+///   code block in a list after text
+///   ```
+///
 /// ~~~java
 /// code block
 /// with tildes and an info string ("java")
@@ -1761,6 +1767,12 @@ class Test {}
 ///   in a list
 ///   ```
 ///
+/// - flibbertigibbet
+///
+///   ```
+///   code block in a list after text
+///   ```
+///
 /// ~~~java
 /// code block
 /// with tildes and an info string ("java")
@@ -1993,6 +2005,8 @@ public void markdownTables() {
     assume().that(MARKDOWN_JAVADOC_SUPPORTED).isTrue();
     String input =
 """
+/// Table McTableface
+///
 /// | foo | bar |
 /// | --- | --- |
 /// | baz | qux |
@@ -2000,15 +2014,16 @@ public void markdownTables() {
 /// - |foo|bar|
 ///   |--:|:--|
 ///   |baz|qux|
+///
+/// - Another list.
+///
+///   | which | contains |
+///   | ----- | -------- |
+///   | a | table |
 class Test {}
 """;
-    // TODO: unmangle the tables
-    String expected =
-"""
-/// | foo | bar | | --- | --- | | baz | qux |
-/// - |foo|bar| |--:|:--| |baz|qux|
-class Test {}
-""";
+    // We don't currently try to align the column markers in the rows of the last table.
+    String expected = input;
     doFormatTest(input, expected);
   }
 
diff --git a/core/src/test/java/com/google/googlejavaformat/java/javadoc/MarkdownPositionsTest.java b/core/src/test/java/com/google/googlejavaformat/java/javadoc/MarkdownPositionsTest.java
index e1b5fcd12..f3104e119 100644
--- a/core/src/test/java/com/google/googlejavaformat/java/javadoc/MarkdownPositionsTest.java
+++ b/core/src/test/java/com/google/googlejavaformat/java/javadoc/MarkdownPositionsTest.java
@@ -129,6 +129,7 @@ public void codeBlock() {
     int secondCodeEnd = text.indexOf("~~~", secondCodeStart + 3) + 3;
     int thirdCodeStart = text.indexOf("````", secondCodeEnd);
     int thirdCodeEnd = text.indexOf("````", thirdCodeStart + 4) + 4;
+    boolean precededByNonWhitespace = true;
     ImmutableListMultimap expected =
         ImmutableListMultimap.builder()
             .put(bullet, new ListOpenTag(""))
@@ -136,7 +137,11 @@ public void codeBlock() {
             .put(
                 firstCodeStart,
                 new MarkdownFencedCodeBlock(
-                    text.substring(firstCodeStart, firstCodeEnd), "```", "```", "foo\nbar\n"))
+                    text.substring(firstCodeStart, firstCodeEnd),
+                    "```",
+                    "```",
+                    "foo\nbar\n",
+                    /* precededByNonWhitespace= */ true))
             .put(firstCodeEnd, new ListItemCloseTag(""))
             .put(firstCodeEnd, new ListCloseTag(""))
             .put(
@@ -145,14 +150,16 @@ public void codeBlock() {
                     text.substring(secondCodeStart, secondCodeEnd),
                     "~~~java",
                     "~~~",
-                    "code\nwith tildes\n"))
+                    "code\nwith tildes\n",
+                    /* precededByNonWhitespace= */ false))
             .put(
                 thirdCodeStart,
                 new MarkdownFencedCodeBlock(
                     text.substring(thirdCodeStart, thirdCodeEnd),
                     "````",
                     "````",
-                    "indented code\nwith more than three backticks\n"))
+                    "indented code\nwith more than three backticks\n",
+                    /* precededByNonWhitespace= */ false))
             .build();
     assertThat(map).isEqualTo(expected);
   }