Skip to content

Commit 540cb0d

Browse files
eamonnmcmanusgoogle-java-format Team
authored andcommitted
Initial support for Markdown Javadoc.
This is work in progress and *may mangle some Markdown Javadoc comments*. Later changes will address that. Currently supported Markdown constructs: newline-separated paragraphs; `# Headings`; and `- Bullet lists`. `*Emphasis*` and `**Strong emphasis**` should work too because they don't need any special treatment. Other constructs probably don't work, notably code blocks. PiperOrigin-RevId: 892607809
1 parent 742e4f3 commit 540cb0d

File tree

12 files changed

+547
-170
lines changed

12 files changed

+547
-170
lines changed

core/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@
3939
<groupId>com.google.guava</groupId>
4040
<artifactId>guava</artifactId>
4141
</dependency>
42+
<dependency>
43+
<groupId>org.commonmark</groupId>
44+
<artifactId>commonmark</artifactId>
45+
<version>0.28.0</version>
46+
</dependency>
4247

4348
<!-- Compile-time dependencies -->
4449
<dependency>

core/src/main/java/com/google/googlejavaformat/java/JavaCommentsHelper.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,13 @@ public String rewrite(Tok tok, int maxWidth, int column0) {
5050
}
5151
String text = tok.getOriginalText();
5252
if (tok.isJavadocComment() && options.formatJavadoc()) {
53-
text = JavadocFormatter.formatJavadoc(text, column0);
53+
if (text.startsWith("///")) {
54+
if (markdownJavadocPositions.contains(tok.getPosition())) {
55+
return JavadocFormatter.formatJavadoc(text, column0);
56+
}
57+
} else {
58+
text = JavadocFormatter.formatJavadoc(text, column0);
59+
}
5460
}
5561
List<String> lines = new ArrayList<>();
5662
Iterator<String> it = Newlines.lineIterator(text);

core/src/main/java/com/google/googlejavaformat/java/JavaInput.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,9 +160,13 @@ public boolean isSlashStarComment() {
160160

161161
@Override
162162
public boolean isJavadocComment() {
163-
// comments like `/***` are also javadoc, but their formatting probably won't be improved
164-
// by the javadoc formatter
165-
return text.startsWith("/**") && text.charAt("/**".length()) != '*' && text.length() > 4;
163+
// comments like `/***` or `////` are also javadoc, but their formatting probably won't be
164+
// improved by the javadoc formatter
165+
return ((text.startsWith("/**") && !text.startsWith("/***"))
166+
|| (Runtime.version().feature() >= 23
167+
&& text.startsWith("///")
168+
&& !text.startsWith("////")))
169+
&& text.length() > 4;
166170
}
167171

168172
@Override

core/src/main/java/com/google/googlejavaformat/java/javadoc/CharStream.java

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,18 @@
2727
*/
2828
final class CharStream {
2929
private final String input;
30-
private int start;
30+
private int position;
3131
private int tokenEnd = -1; // Negative value means no token, and will cause an exception if used.
3232

3333
CharStream(String input) {
3434
this.input = checkNotNull(input);
3535
}
3636

3737
boolean tryConsume(String expected) {
38-
if (!input.startsWith(expected, start)) {
38+
if (!input.startsWith(expected, position)) {
3939
return false;
4040
}
41-
tokenEnd = start + expected.length();
41+
tokenEnd = position + expected.length();
4242
return true;
4343
}
4444

@@ -48,7 +48,7 @@ boolean tryConsume(String expected) {
4848
* @param pattern the pattern to search for, which must be anchored to match only at position 0
4949
*/
5050
boolean tryConsumeRegex(Pattern pattern) {
51-
Matcher matcher = pattern.matcher(input).region(start, input.length());
51+
Matcher matcher = pattern.matcher(input).region(position, input.length());
5252
if (!matcher.lookingAt()) {
5353
return false;
5454
}
@@ -57,13 +57,17 @@ boolean tryConsumeRegex(Pattern pattern) {
5757
}
5858

5959
String readAndResetRecorded() {
60-
String result = input.substring(start, tokenEnd);
61-
start = tokenEnd;
60+
String result = input.substring(position, tokenEnd);
61+
position = tokenEnd;
6262
tokenEnd = -1;
6363
return result;
6464
}
6565

6666
boolean isExhausted() {
67-
return start == input.length();
67+
return position == input.length();
68+
}
69+
70+
int position() {
71+
return position;
6872
}
6973
}

core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocFormatter.java

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,15 @@
1414

1515
package com.google.googlejavaformat.java.javadoc;
1616

17+
import static com.google.common.base.Preconditions.checkState;
1718
import static com.google.googlejavaformat.java.javadoc.JavadocLexer.lex;
1819
import static com.google.googlejavaformat.java.javadoc.Token.Type.BR_TAG;
1920
import static com.google.googlejavaformat.java.javadoc.Token.Type.PARAGRAPH_OPEN_TAG;
2021
import static java.util.regex.Pattern.CASE_INSENSITIVE;
2122
import static java.util.regex.Pattern.compile;
23+
import static java.util.stream.Collectors.joining;
2224

25+
import com.google.common.base.CharMatcher;
2326
import com.google.common.collect.ImmutableList;
2427
import com.google.googlejavaformat.java.javadoc.JavadocLexer.LexException;
2528
import java.util.List;
@@ -39,22 +42,37 @@ public final class JavadocFormatter {
3942
static final int MAX_LINE_LENGTH = 100;
4043

4144
/**
42-
* Formats the given Javadoc comment, which must start with ∕✱✱ and end with ✱∕. The output will
43-
* start and end with the same characters.
45+
* Formats the given Javadoc comment. A classic Javadoc comment must start with ∕✱✱ and end with
46+
* ✱∕, and the output will start and end with the same characters. A Markdown Javadoc comment
47+
* consists of lines each of which starts with ///, and the output will also consist of such
48+
* lines.
4449
*/
4550
public static String formatJavadoc(String input, int blockIndent) {
51+
boolean classicJavadoc =
52+
switch (input) {
53+
case String s when s.startsWith("/**") -> true;
54+
case String s when s.startsWith("///") -> false;
55+
default ->
56+
throw new IllegalArgumentException("Input does not start with /** or ///: " + input);
57+
};
58+
if (!classicJavadoc) {
59+
input = "/// " + markdownCommentText(input);
60+
}
4661
ImmutableList<Token> tokens;
4762
try {
48-
tokens = lex(input);
63+
tokens = lex(input, classicJavadoc);
4964
} catch (LexException e) {
5065
return input;
5166
}
52-
String result = render(tokens, blockIndent);
53-
return makeSingleLineIfPossible(blockIndent, result);
67+
String result = render(tokens, blockIndent, classicJavadoc);
68+
if (classicJavadoc) {
69+
result = makeSingleLineIfPossible(blockIndent, result);
70+
}
71+
return result;
5472
}
5573

56-
private static String render(List<Token> input, int blockIndent) {
57-
JavadocWriter output = new JavadocWriter(blockIndent);
74+
private static String render(List<Token> input, int blockIndent, boolean classicJavadoc) {
75+
JavadocWriter output = new JavadocWriter(blockIndent, classicJavadoc);
5876
for (Token token : input) {
5977
switch (token.type()) {
6078
case BEGIN_JAVADOC -> output.writeBeginJavadoc();
@@ -144,5 +162,29 @@ private static boolean oneLineJavadoc(String line, int blockIndent) {
144162
return true;
145163
}
146164

165+
private static final CharMatcher NOT_SPACE_OR_TAB = CharMatcher.noneOf(" \t");
166+
167+
/**
168+
* Returns the given string with the leading /// and any common leading whitespace removed from
169+
* each line. The resultant string can then be fed to a standard Markdown parser.
170+
*/
171+
private static String markdownCommentText(String input) {
172+
List<String> lines =
173+
input
174+
.lines()
175+
.peek(line -> checkState(line.contains("///"), "Line does not contain ///: %s", line))
176+
.map(line -> line.substring(line.indexOf("///") + 3))
177+
.toList();
178+
int leadingSpace =
179+
lines.stream()
180+
.filter(line -> NOT_SPACE_OR_TAB.matchesAnyOf(line))
181+
.mapToInt(NOT_SPACE_OR_TAB::indexIn)
182+
.min()
183+
.orElse(0);
184+
return lines.stream()
185+
.map(line -> line.length() < leadingSpace ? "" : line.substring(leadingSpace))
186+
.collect(joining("\n"));
187+
}
188+
147189
private JavadocFormatter() {}
148190
}

0 commit comments

Comments
 (0)