55
66package software .amazon .smithy .java .codegen .integrations .javadoc ;
77
8- import java .util .Map ;
98import java .util .Scanner ;
10- import java .util .Set ;
119import java .util .regex .Matcher ;
1210import java .util .regex .Pattern ;
1311import software .amazon .smithy .java .codegen .sections .JavadocSection ;
1412import software .amazon .smithy .java .codegen .writer .JavaWriter ;
1513import 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 */
2523final 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 ("*" , "*" );
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 ("*" , "*" );
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