8338525: Leading and trailing code blocks by indentation

Reviewed-by: hannesw, prappo
This commit is contained in:
Jonathan Gibbons 2024-09-24 20:09:40 +00:00
parent b639661e79
commit 0b8c9f6d23
5 changed files with 197 additions and 24 deletions

View File

@ -286,16 +286,17 @@ public class DocCommentParser {
int depth = 1; // only used when phase is INLINE int depth = 1; // only used when phase is INLINE
int pos = bp; // only used when phase is INLINE int pos = bp; // only used when phase is INLINE
if (textKind == DocTree.Kind.MARKDOWN) {
initMarkdownLine();
}
loop: loop:
while (bp < buflen) { while (bp < buflen) {
switch (ch) { switch (ch) {
case '\n', '\r' -> { case '\n', '\r' -> {
nextChar(); nextChar();
if (textKind == DocTree.Kind.MARKDOWN) { if (textKind == DocTree.Kind.MARKDOWN) {
markdown.update(); initMarkdownLine();
if (markdown.isIndentedCodeBlock()) {
markdown.skipLine();
}
} }
} }
@ -488,6 +489,17 @@ public class DocCommentParser {
nextChar(); nextChar();
} }
void initMarkdownLine() {
if (textStart == -1) {
textStart = bp;
}
markdown.update();
if (markdown.isIndentedCodeBlock()) {
markdown.skipLine();
lastNonWhite = bp - 1; // do not include newline or EOF
}
}
private IllegalStateException unknownTextKind(DocTree.Kind textKind) { private IllegalStateException unknownTextKind(DocTree.Kind textKind) {
return new IllegalStateException(textKind.toString()); return new IllegalStateException(textKind.toString());
} }

View File

@ -592,27 +592,33 @@ public class DocTreeMaker implements DocTreeFactory {
case TEXT, MARKDOWN -> { case TEXT, MARKDOWN -> {
var peekedNext = iter.hasNext() ? alist.get(iter.nextIndex()) : null; var peekedNext = iter.hasNext() ? alist.get(iter.nextIndex()) : null;
var content = getContent(dt); var content = getContent(dt);
int breakOffset = getSentenceBreak(dt.getKind(), content, peekedNext); if (isFirst && dt.getKind() == Kind.MARKDOWN && isIndented(content)) {
if (breakOffset > 0) { // begins with an indented code block (unusual), so no first sentence
// the end of sentence is within the current node; body.add(dt);
// split it, skipping whitespace in between the two parts
var fsPart = newNode(dt.getKind(), dt.pos, content.substring(0, breakOffset).stripTrailing());
fs.add(fsPart);
int wsOffset = skipWhiteSpace(content, breakOffset);
if (wsOffset > 0) {
var bodyPart = newNode(dt.getKind(), dt.pos + wsOffset, content.substring(wsOffset));
body.add(bodyPart);
}
foundFirstSentence = true;
} else if (peekedNext != null && isSentenceBreak(peekedNext, false)) {
// the next node is a sentence break, so this is the end of the first sentence;
// remove trailing spaces
var fsPart = newNode(dt.getKind(), dt.pos, content.stripTrailing());
fs.add(fsPart);
foundFirstSentence = true; foundFirstSentence = true;
} else { } else {
// no sentence break found; keep scanning int breakOffset = getSentenceBreak(dt.getKind(), content, peekedNext);
fs.add(dt); if (breakOffset > 0) {
// the end of sentence is within the current node;
// split it, skipping whitespace in between the two parts
var fsPart = newNode(dt.getKind(), dt.pos, content.substring(0, breakOffset).stripTrailing());
fs.add(fsPart);
int wsOffset = skipWhiteSpace(content, breakOffset);
if (wsOffset > 0) {
var bodyPart = newNode(dt.getKind(), dt.pos + wsOffset, content.substring(wsOffset));
body.add(bodyPart);
}
foundFirstSentence = true;
} else if (peekedNext != null && isSentenceBreak(peekedNext, false)) {
// the next node is a sentence break, so this is the end of the first sentence;
// remove trailing spaces
var fsPart = newNode(dt.getKind(), dt.pos, content.stripTrailing());
fs.add(fsPart);
foundFirstSentence = true;
} else {
// no sentence break found; keep scanning
fs.add(dt);
}
} }
} }
@ -651,6 +657,11 @@ public class DocTreeMaker implements DocTreeFactory {
}; };
} }
private static final Pattern INDENT = Pattern.compile(" {4}| {0,3}\t");
private boolean isIndented(String s) {
return INDENT.matcher(s).lookingAt();
}
private DCTree newNode(DocTree.Kind kind, int pos, String text) { private DCTree newNode(DocTree.Kind kind, int pos, String text) {
return switch (kind) { return switch (kind) {
case TEXT -> m.at(pos).newTextTree(text); case TEXT -> m.at(pos).newTextTree(text);

View File

@ -486,4 +486,107 @@ public class TestMarkdownCodeBlocks extends JavadocTester {
<dd><code><a href="NullPointerException.html" title="class in p">NullPointerException</a></code> - if other is <code>null</code></dd> <dd><code><a href="NullPointerException.html" title="class in p">NullPointerException</a></code> - if other is <code>null</code></dd>
</dl>"""); </dl>""");
} }
@Test
public void testLeadingCodeBlock(Path base) throws Exception {
Path src = base.resolve("src");
tb.writeJavaFiles(src,
"""
package p;
/// Leading code block
/// Lorum ipsum.
public class C { }
""");
javadoc("-d", base.resolve("api").toString(),
"--no-platform-links",
"--source-path", src.toString(),
"p");
checkExit(Exit.OK);
// check first sentence is empty in package summary file
checkOutput("p/package-summary.html", true,
"""
<div class="col-first even-row-color class-summary class-summary-tab2"><a href="C.html" title="class in p">C</a></div>
<div class="col-last even-row-color class-summary class-summary-tab2">&nbsp;</div>""");
checkOutput("p/C.html", true,
"""
<div class="block"><pre><code>Leading code block
</code></pre>
<p>Lorum ipsum.</p>""");
}
@Test
public void testTrailingCodeBlock(Path base) throws Exception {
Path src = base.resolve("src");
tb.writeJavaFiles(src,
"""
package p;
/// Lorum ipsum.
///
/// Trailing code block
public class C { }
""");
javadoc("-d", base.resolve("api").toString(),
"--no-platform-links",
"--source-path", src.toString(),
"p");
checkExit(Exit.OK);
checkOutput("p/C.html", true,
"""
<div class="block"><p>Lorum ipsum.</p>
<pre><code>Trailing code block
</code></pre>
</div>""");
}
// this example is derived from the test case in JDK-8338525
@Test
public void testLeadingTrailingCodeBlockWithAnnotations(Path base) throws Exception {
Path src = base.resolve("src");
tb.writeJavaFiles(src,
"""
package p;
public class C {
/// @Override
/// void m() {}
///
/// Plain text
///
/// @Override
/// void m() {}
public void m() {}
}""");
javadoc("-d", base.resolve("api").toString(),
"--no-platform-links",
"--source-path", src.toString(),
"p");
checkExit(Exit.OK);
checkOutput("p/C.html", true,
"""
<div class="col-first even-row-color method-summary-table method-summary-table-tab2 \
method-summary-table-tab4"><code>void</code></div>
<div class="col-second even-row-color method-summary-table method-summary-table-tab2 \
method-summary-table-tab4"><code><a href="#m()" class="member-name-link">m</a>()</code></div>
<div class="col-last even-row-color method-summary-table method-summary-table-tab2 \
method-summary-table-tab4">&nbsp;</div>""",
"""
<div class="member-signature"><span class="modifiers">public</span>&nbsp;\
<span class="return-type">void</span>&nbsp;<span class="element-name">m</span>()</div>
<div class="block"><pre><code>@Override
void m() {}
</code></pre>
<p>Plain text</p>
<pre><code>@Override
void m() {}
</code></pre>
</div>""");
}
} }

View File

@ -1043,8 +1043,9 @@ public class DocCommentTester {
.replaceAll("(\\{@value\\s+[^}]+)\\s+(})", "$1$2"); .replaceAll("(\\{@value\\s+[^}]+)\\s+(})", "$1$2");
} }
// See comment in MarkdownTest for explanation of dummy and Override
String normalizeFragment(String s) { String normalizeFragment(String s) {
return s.replaceAll("\n[ \t]+@(?!([@*]|dummy))", "\n@"); return s.replaceAll("\n[ \t]+@(?!([@*]|(dummy|Override)))", "\n@");
} }
int copyLiteral(String s, int start, StringBuilder sb) { int copyLiteral(String s, int start, StringBuilder sb) {

View File

@ -40,6 +40,7 @@
* In the tests for code spans and code blocks, "@dummy" is used as a dummy inline * In the tests for code spans and code blocks, "@dummy" is used as a dummy inline
* or block tag to verify that it is skipped as part of the code span or code block. * or block tag to verify that it is skipped as part of the code span or code block.
* In other words, "@dummy" should appear as a literal part of the Markdown content. * In other words, "@dummy" should appear as a literal part of the Markdown content.
* ("@Override" is also treated the same way, as a commonly found annotation.)
* Conversely, standard tags are used to verify that a fragment of text is not being * Conversely, standard tags are used to verify that a fragment of text is not being
* skipped as a code span or code block. In other words, they should be recognized as tags * skipped as a code span or code block. In other words, they should be recognized as tags
* and not skipped as part of any Markdown content. * and not skipped as part of any Markdown content.
@ -409,6 +410,32 @@ DocComment[DOC_COMMENT, pos:0
RawText[MARKDOWN, pos:85, .] RawText[MARKDOWN, pos:85, .]
block tags: empty block tags: empty
] ]
*/
/// Indented Code Block
/// Lorum ipsum.
void indentedCodeBlock_leading() { }
/*
DocComment[DOC_COMMENT, pos:0
firstSentence: empty
body: 1
RawText[MARKDOWN, pos:0, ____Indented_Code_Block|Lorum_ipsum.]
block tags: empty
]
*/
/// Lorum ipsum.
///
/// Indented Code Block
void indentedCodeBlock_trailing() { }
/*
DocComment[DOC_COMMENT, pos:0
firstSentence: 1
RawText[MARKDOWN, pos:0, Lorum_ipsum.]
body: 1
RawText[MARKDOWN, pos:18, Indented_Code_Block]
block tags: empty
]
*/ */
///123. ///123.
@ -613,5 +640,24 @@ DocComment[DOC_COMMENT, pos:0
] ]
*/ */
// The following test case is derived from the test case in JDK-8338525.
/// @Override
/// void m() { }
///
/// Plain text
///
/// @Override
/// void m() { }
void leadingTrailingCodeBlocksWithAnnos() { }
/*
DocComment[DOC_COMMENT, pos:0
firstSentence: empty
body: 1
RawText[MARKDOWN, pos:0, ____@Override|____void_m()_{_}||...||____@Override|____void_m()_{_}]
block tags: empty
]
*/
} }