Skip to content

Commit d1860cf

Browse files
authored
Properly format javadocs (#1168)
1 parent f7b34fb commit d1860cf

9 files changed

Lines changed: 791 additions & 220 deletions

File tree

codegen/codegen-core/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ dependencies {
1313
implementation(project(":core"))
1414
implementation(project(":logging"))
1515
implementation(libs.jspecify)
16+
implementation(libs.commonmark)
17+
implementation(libs.jsoup)
1618
}
1719

1820
// Internal source set for the types-only SmithyBuildPlugin.

codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/integrations/javadoc/DocumentationTraitInterceptor.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
package software.amazon.smithy.java.codegen.integrations.javadoc;
77

8+
import software.amazon.smithy.java.codegen.sections.ClassSection;
89
import software.amazon.smithy.java.codegen.sections.JavadocSection;
910
import software.amazon.smithy.java.codegen.writer.JavaWriter;
1011
import software.amazon.smithy.model.traits.DocumentationTrait;
@@ -13,13 +14,20 @@
1314
/**
1415
* Adds Javadoc documentation for the {@link DocumentationTrait}.
1516
*
16-
* <p>The documentation trait contents are added as the contents of the Javadoc.
17+
* <p>The documentation trait contents are converted from Markdown/HTML to
18+
* Javadoc-compatible HTML with line wrapping adjusted for the nesting level.
1719
*/
1820
final class DocumentationTraitInterceptor implements CodeInterceptor<JavadocSection, JavaWriter> {
21+
// Top-level class javadoc: " * " prefix = 3 chars at indent 0, so 3 total prefix
22+
private static final int CLASS_MAX_WIDTH = 117;
23+
// Member javadoc (getters, setters, etc.): " * " prefix = 7 chars at indent 4, so 7 total prefix
24+
private static final int MEMBER_MAX_WIDTH = 113;
1925

2026
@Override
2127
public void write(JavaWriter writer, String previousText, JavadocSection section) {
22-
writer.writeWithNoFormatting(section.targetedShape().expectTrait(DocumentationTrait.class).getValue());
28+
var markdown = section.targetedShape().expectTrait(DocumentationTrait.class).getValue();
29+
int maxWidth = section.parent() instanceof ClassSection ? CLASS_MAX_WIDTH : MEMBER_MAX_WIDTH;
30+
writer.writeWithNoFormatting(MarkdownToJavadoc.convert(markdown, maxWidth));
2331

2432
if (!previousText.isEmpty()) {
2533
// Add spacing if tags have been added to the javadoc

codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/integrations/javadoc/JavadocFormatterInterceptor.java

Lines changed: 45 additions & 166 deletions
Original file line numberDiff line numberDiff line change
@@ -5,107 +5,24 @@
55

66
package software.amazon.smithy.java.codegen.integrations.javadoc;
77

8-
import java.util.Map;
98
import java.util.Scanner;
10-
import java.util.Set;
119
import java.util.regex.Matcher;
1210
import java.util.regex.Pattern;
1311
import software.amazon.smithy.java.codegen.sections.JavadocSection;
1412
import software.amazon.smithy.java.codegen.writer.JavaWriter;
1513
import software.amazon.smithy.utils.CodeInterceptor;
16-
import software.amazon.smithy.utils.StringUtils;
1714

1815
/**
19-
* This interceptor will format any text written into a javadoc section into the correct multi-line
20-
* comment-style.
16+
* Formats text written into a javadoc section into the correct multi-line comment style.
2117
*
22-
* <p>This interceptor should be the last javadoc interceptor to run, so it can pick up the text
23-
* run by other interceptors for formatting.
18+
* <p>This interceptor should be the last javadoc interceptor to run. It wraps the content
19+
* in {@code /** ... * /} delimiters and prefixes each line with {@code *}.
20+
*
21+
* <p>Line wrapping and HTML formatting are handled upstream by {@link MarkdownToJavadoc}.
2422
*/
2523
final class JavadocFormatterInterceptor implements CodeInterceptor<JavadocSection, JavaWriter> {
26-
private static final int MAX_LINE_LENGTH = 100;
27-
private static final Pattern PATTERN = Pattern.compile("<([a-z]+)*>.*?</\\1>", Pattern.DOTALL);
2824
// Matches the start of an {@snippet ...} inline Javadoc tag (JDK 18+, JEP 413).
2925
private static final Pattern SNIPPET_START_PATTERN = Pattern.compile("\\{@snippet\\b[^:}]*:");
30-
// HTML tags supported by javadocs for Java17. Note: this list is not directly documented in JavaDocs documentation
31-
// and is instead found by inspecting the JDK doclint/HtmlTag.java file.
32-
private static final Set<String> SUPPORTED_TAGS = Set.of(
33-
"A",
34-
"ABBR",
35-
"ACRONYM",
36-
"ADDRESS",
37-
"ARTICLE",
38-
"ASIDE",
39-
"B",
40-
"BDI",
41-
"BIG",
42-
"BLOCKQUOTE",
43-
"BODY",
44-
"BR",
45-
"CAPTION",
46-
"CENTER",
47-
"CITE",
48-
"CODE",
49-
"COL",
50-
"DD",
51-
"DEL",
52-
"DFN",
53-
"DIV",
54-
"DT",
55-
"EM",
56-
"FONT",
57-
"FIGURE",
58-
"FIGCAPTION",
59-
"FRAME",
60-
"FRAMESET",
61-
"H1",
62-
"H2",
63-
"H3",
64-
"H4",
65-
"H5",
66-
"H6",
67-
"HEAD",
68-
"HR",
69-
"HTML",
70-
"I",
71-
"IFRAME",
72-
"IMG",
73-
"INS",
74-
"KBD",
75-
"LI",
76-
"LINK",
77-
"MAIN",
78-
"MARK",
79-
"META",
80-
"NAV",
81-
"NOFRAMES",
82-
"NOSCRIPT",
83-
"P",
84-
"PRE",
85-
"Q",
86-
"S",
87-
"SAMP",
88-
"SCRIPT",
89-
"SECTION",
90-
"SMALL",
91-
"SPAN",
92-
"STRIKE",
93-
"STRONG",
94-
"STYLE",
95-
"SUB",
96-
"SUP",
97-
"TD",
98-
"TEMPLATE",
99-
"TH",
100-
"TIME",
101-
"TITLE",
102-
"TT",
103-
"U",
104-
"UL",
105-
"WBR",
106-
"VAR");
107-
// Convert problematic characters to their HTML escape codes.
108-
private static final Map<String, String> REPLACEMENTS = Map.of("*", "&#42;");
10926

11027
@Override
11128
public Class<JavadocSection> sectionType() {
@@ -122,41 +39,59 @@ public void write(JavaWriter writer, String previousText, JavadocSection section
12239
private void writeDocStringContents(JavaWriter writer, String contents) {
12340
writer.writeWithNoFormatting("/**");
12441
writer.writeInlineWithNoFormatting(" * ");
125-
writeDocstringBody(writer, contents, 0);
42+
writeBody(writer, contents);
12643
writer.writeWithNoFormatting("\n */");
12744
}
12845

129-
private void writeDocstringBody(JavaWriter writer, String contents, int nestingLevel) {
130-
// Split out any {@snippet ...} inline tags first so their contents
131-
// are not line-wrapped or have characters escaped.
46+
private void writeBody(JavaWriter writer, String contents) {
47+
// Split out {@snippet ...} inline tags so their contents are not escaped.
13248
Matcher snippetMatcher = SNIPPET_START_PATTERN.matcher(contents);
133-
int lastSnippetPos = 0;
134-
StringBuilder nonSnippetContents = new StringBuilder();
49+
int lastPos = 0;
50+
StringBuilder pending = new StringBuilder();
13551
while (snippetMatcher.find()) {
136-
nonSnippetContents.append(contents, lastSnippetPos, snippetMatcher.start());
137-
// Flush any accumulated non-snippet content through HTML tag processing
138-
if (!nonSnippetContents.isEmpty()) {
139-
writeDocstringBodyHtml(writer, nonSnippetContents.toString(), nestingLevel);
140-
nonSnippetContents.setLength(0);
52+
pending.append(contents, lastPos, snippetMatcher.start());
53+
if (!pending.isEmpty()) {
54+
writeLines(writer, pending.toString());
55+
pending.setLength(0);
14156
}
142-
// Find the matching closing brace using balanced brace counting,
143-
// since snippet bodies contain Java code with its own braces.
14457
int snippetEnd = findMatchingBrace(contents, snippetMatcher.start());
145-
// Write snippet block verbatim - no wrapping, no escaping
146-
writeSnippetBlock(writer, contents.substring(snippetMatcher.start(), snippetEnd));
147-
lastSnippetPos = snippetEnd;
58+
writeVerbatimLines(writer, contents.substring(snippetMatcher.start(), snippetEnd));
59+
lastPos = snippetEnd;
60+
}
61+
pending.append(contents.substring(lastPos));
62+
if (!pending.isEmpty()) {
63+
writeLines(writer, pending.toString());
14864
}
149-
nonSnippetContents.append(contents.substring(lastSnippetPos));
150-
if (!nonSnippetContents.isEmpty()) {
151-
writeDocstringBodyHtml(writer, nonSnippetContents.toString(), nestingLevel);
65+
}
66+
67+
/**
68+
* Writes pre-formatted content, prefixing each line with {@code *} and escaping
69+
* literal {@code *} characters in the content.
70+
*/
71+
private void writeLines(JavaWriter writer, String text) {
72+
for (Scanner it = new Scanner(text); it.hasNextLine();) {
73+
var line = it.nextLine();
74+
// Escape literal * so it doesn't close the javadoc comment
75+
line = line.replace("*", "&#42;");
76+
writer.writeInlineWithNoFormatting(line);
77+
if (it.hasNextLine()) {
78+
writer.writeInlineWithNoFormatting(writer.getNewline() + " * ");
79+
}
15280
}
15381
}
15482

15583
/**
156-
* Finds the position after the closing brace that matches the opening brace of
157-
* an inline Javadoc tag starting at {@code start}. Uses balanced brace counting
158-
* to skip over braces in the snippet body (e.g. Java code blocks).
84+
* Writes snippet content verbatim (no escaping), prefixing each line with {@code *}.
15985
*/
86+
private void writeVerbatimLines(JavaWriter writer, String text) {
87+
for (Scanner it = new Scanner(text); it.hasNextLine();) {
88+
writer.writeInlineWithNoFormatting(it.nextLine());
89+
if (it.hasNextLine()) {
90+
writer.writeInlineWithNoFormatting(writer.getNewline() + " * ");
91+
}
92+
}
93+
}
94+
16095
private static int findMatchingBrace(String contents, int start) {
16196
int depth = 0;
16297
for (int i = start; i < contents.length(); i++) {
@@ -170,62 +105,6 @@ private static int findMatchingBrace(String contents, int start) {
170105
}
171106
}
172107
}
173-
// If no matching brace found, return end of contents
174108
return contents.length();
175109
}
176-
177-
private void writeSnippetBlock(JavaWriter writer, String snippet) {
178-
for (Scanner it = new Scanner(snippet); it.hasNextLine();) {
179-
writer.writeInlineWithNoFormatting(it.nextLine());
180-
if (it.hasNextLine()) {
181-
writer.writeInlineWithNoFormatting(writer.getNewline() + " * ");
182-
}
183-
}
184-
}
185-
186-
private void writeDocstringBodyHtml(JavaWriter writer, String contents, int nestingLevel) {
187-
// Split out any HTML-tag wrapped sections as we do not want to wrap
188-
// any customer documentation with tags
189-
Matcher matcher = PATTERN.matcher(contents);
190-
int lastMatchPos = 0;
191-
while (matcher.find()) {
192-
// write all contents up to the match.
193-
writeDocstringLine(writer, contents.substring(lastMatchPos, matcher.start()), nestingLevel);
194-
195-
// write match contents if the HTML tag is supported
196-
var htmlTag = matcher.group(1);
197-
if (SUPPORTED_TAGS.contains(htmlTag.toUpperCase())) {
198-
writer.writeInlineWithNoFormatting("<" + htmlTag + ">");
199-
var offsetForTagStart = 2 + htmlTag.length();
200-
var tagContents = contents.substring(matcher.start() + offsetForTagStart, matcher.end());
201-
writeDocstringBodyHtml(writer, tagContents, nestingLevel + 1);
202-
}
203-
204-
lastMatchPos = matcher.end();
205-
}
206-
// Write out all remaining contents
207-
writeDocstringLine(writer, contents.substring(lastMatchPos), nestingLevel);
208-
}
209-
210-
private void writeDocstringLine(JavaWriter writer, String string, int nestingLevel) {
211-
for (Scanner it = new Scanner(string); it.hasNextLine();) {
212-
var s = it.nextLine();
213-
214-
// Sanitize string
215-
for (var entry : REPLACEMENTS.entrySet()) {
216-
s = s.replace(entry.getKey(), entry.getValue());
217-
}
218-
219-
// If we are out of an HTML tag, wrap the string. Otherwise, ignore wrapping.
220-
var str = nestingLevel == 0
221-
? StringUtils.wrap(s, MAX_LINE_LENGTH, writer.getNewline() + " * ", false)
222-
: s;
223-
224-
writer.writeInlineWithNoFormatting(str);
225-
226-
if (it.hasNextLine()) {
227-
writer.writeInlineWithNoFormatting(writer.getNewline() + " * ");
228-
}
229-
}
230-
}
231110
}

0 commit comments

Comments
 (0)