8352389: Remove incidental whitespace in pre/code content

Reviewed-by: liach
This commit is contained in:
Hannes Wallnöfer 2025-04-08 18:45:53 +00:00
parent 257f817c7f
commit 24ff96afe4
7 changed files with 349 additions and 12 deletions

View File

@ -34,8 +34,12 @@ import java.util.regex.Pattern;
import com.sun.source.doctree.AttributeTree.ValueKind;
import com.sun.source.doctree.DocTree;
import com.sun.source.doctree.ErroneousTree;
import com.sun.source.doctree.LiteralTree;
import com.sun.source.doctree.StartElementTree;
import com.sun.source.doctree.TextTree;
import com.sun.source.doctree.UnknownBlockTagTree;
import com.sun.source.doctree.UnknownInlineTagTree;
import com.sun.source.util.SimpleDocTreeVisitor;
import com.sun.tools.javac.parser.Tokens.Comment;
import com.sun.tools.javac.tree.DCTree;
import com.sun.tools.javac.tree.DCTree.DCAttribute;
@ -126,6 +130,9 @@ public class DocCommentParser {
private int lastNonWhite = -1;
private boolean newline = true;
/** Used for whitespace normalization in pre/code/literal tags. */
private boolean inPre = false;
private final Map<Name, TagParser> tagParsers;
/**
@ -281,6 +288,7 @@ public class DocCommentParser {
*/
protected List<DCTree> content(Phase phase) {
ListBuffer<DCTree> trees = new ListBuffer<>();
ListBuffer<DCTree> mainTrees = null;
textStart = -1;
int depth = 1; // only used when phase is INLINE
@ -349,6 +357,18 @@ public class DocCommentParser {
addPendingText(trees, bp - 1);
trees.add(html());
// Create new list for <pre> content which is merged into main list when element is closed.
if (inPre) {
if (mainTrees == null) {
mainTrees = trees;
trees = new ListBuffer<>();
}
} else if (mainTrees != null) {
mainTrees.addAll(normalizePreContent(trees));
trees = mainTrees;
mainTrees = null;
}
if (phase == Phase.PREAMBLE || phase == Phase.POSTAMBLE) {
break; // Ignore newlines after html tags, in the meta content
}
@ -476,6 +496,13 @@ public class DocCommentParser {
if (lastNonWhite != -1)
addPendingText(trees, lastNonWhite);
// Happens with unclosed <pre> element. Add content without normalizing.
if (mainTrees != null) {
mainTrees.addAll(trees);
trees = mainTrees;
mainTrees = null;
}
return (phase == Phase.INLINE)
? List.of(erroneous("dc.unterminated.inline.tag", pos))
: trees.toList();
@ -1054,6 +1081,9 @@ public class DocCommentParser {
}
if (ch == '>') {
nextChar();
if ("pre".equalsIgnoreCase(name.toString())) {
inPre = true;
}
return m.at(p).newStartElementTree(name, attrs, selfClosing).setEndPos(bp);
}
}
@ -1064,6 +1094,9 @@ public class DocCommentParser {
skipWhitespace();
if (ch == '>') {
nextChar();
if ("pre".equalsIgnoreCase(name.toString())) {
inPre = false;
}
return m.at(p).newEndElementTree(name).setEndPos(bp);
}
}
@ -1186,6 +1219,102 @@ public class DocCommentParser {
return attrs.toList();
}
/*
* Removes a newline character following a <code>, {@code or {@literal tag at the
* beginning of <pre> element content, as well as any space/tabs between the pre
* and code tags. The operation is only performed on traditional doc comments.
* If conditions are not met the list is returned unchanged.
*/
ListBuffer<DCTree> normalizePreContent(ListBuffer<DCTree> trees) {
// Do nothing if comment is not eligible for whitespace normalization.
if (textKind == DocTree.Kind.MARKDOWN || isHtmlFile) {
return trees;
}
enum State {
BEFORE_CODE, // at beginning of <pre> content, or after leading horizontal whitespace
AFTER_CODE, // after <code> start tag (not used for {@code} tag)
SUCCEEDED, // normalization succeeded, add remaining trees
FAILED; // normalization failed, return original trees
}
class Context {
State state = State.BEFORE_CODE;
// Called when an unexpected tree is encountered. Set state to
// FAILED unless normalization already terminated successfully.
void unexpectedTree() {
if (state != State.SUCCEEDED) {
state = State.FAILED;
}
}
}
var visitor = new SimpleDocTreeVisitor<DCTree, Context>() {
@Override
public DCTree visitText(TextTree text, Context cx) {
if (cx.state == State.BEFORE_CODE && text.getBody().matches("[ \t]+")) {
// <pre> ...
return null;
} else if (cx.state == State.AFTER_CODE && text.getBody().startsWith("\n")) {
// <pre><code>\n...
cx.state = State.SUCCEEDED;
return m.at(((DCText) text).pos + 1).newTextTree(text.getBody().substring(1));
}
cx.unexpectedTree();
return (DCTree) text;
}
@Override
public DCTree visitLiteral(LiteralTree literal, Context cx) {
if (cx.state == State.BEFORE_CODE && literal.getBody().getBody().startsWith("\n")) {
// <pre>{@code\n...
cx.state = State.SUCCEEDED;
DCText oldBody = (DCText) literal.getBody();
DCText newBody = m.at(oldBody.pos + 1).newTextTree(oldBody.getBody().substring(1));
m.at(((DCTree) literal).pos);
return literal.getKind() == DocTree.Kind.CODE
? m.newCodeTree(newBody)
: m.newLiteralTree(newBody);
}
cx.unexpectedTree();
return (DCTree) literal;
}
@Override
public DCTree visitStartElement(StartElementTree node, Context cx) {
if (cx.state == State.BEFORE_CODE && node.getName().toString().equalsIgnoreCase("code")) {
cx.state = State.AFTER_CODE;
} else {
cx.unexpectedTree();
}
return (DCTree) node;
}
@Override
protected DCTree defaultAction(DocTree node, Context cx) {
cx.unexpectedTree();
return (DCTree) node;
}
};
Context cx = new Context();
var normalized = new ListBuffer<DCTree>();
for (var tree : trees) {
var visited = visitor.visit(tree, cx);
// null return value means the tree should be dropped
if (visited != null) {
normalized.add(visited);
}
if (cx.state == State.FAILED) {
return trees;
}
}
return cx.state == State.SUCCEEDED ? normalized : trees;
}
protected void attrValueChar(ListBuffer<DCTree> list) {
switch (ch) {
case '&' -> entity(list);

View File

@ -133,7 +133,7 @@ pre {
line-height: var(--code-line-height);
background-color: var(--pre-background-color);
color: var(--pre-text-color);
padding: 8px;
padding: 10px;
overflow-x:auto;
}
h1 {

View File

@ -23,7 +23,7 @@
/*
* @test
* @bug 8002387 8014636 8078320 8175200 8186332 8352249
* @bug 8002387 8014636 8078320 8175200 8186332 8352249 8352389
* @summary Improve rendered HTML formatting for {@code}
* @library ../../lib
* @modules jdk.javadoc/jdk.javadoc.internal.tool
@ -70,8 +70,7 @@ public class TestLiteralCodeInPre extends JavadocTester {
"""
typical_usage_code</span>()</div>
<div class="block">Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Example: <pre><code>
line 0 @Override
Example: <pre><code> line 0 @Override
line 1 &lt;T&gt; void m(T t) {
line 2 // do something with T
line 3 }
@ -80,8 +79,7 @@ public class TestLiteralCodeInPre extends JavadocTester {
"""
typical_usage_literal</span>()</div>
<div class="block">Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Example: <pre>
line 0 @Override
Example: <pre> line 0 @Override
line 1 &lt;T&gt; void m(T t) {
line 2 // do something with T
line 3 }
@ -90,8 +88,7 @@ public class TestLiteralCodeInPre extends JavadocTester {
"""
recommended_usage_literal</span>()</div>
<div class="block">Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Example: <pre>
line 0 @Override
Example: <pre> line 0 @Override
line 1 &lt;T&gt; void m(T t) {
line 2 // do something with T
line 3 } </pre>

View File

@ -0,0 +1,165 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
/*
* @test
* @bug 8352389
* @summary Remove incidental whitespace in pre/code content
* @library /tools/lib ../../lib
* @modules jdk.javadoc/jdk.javadoc.internal.tool
* @build javadoc.tester.* toolbox.ToolBox builder.ClassBuilder
* @run main TestPreCode
*/
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import builder.AbstractBuilder;
import builder.ClassBuilder;
import toolbox.ToolBox;
import javadoc.tester.JavadocTester;
public class TestPreCode extends JavadocTester {
final ToolBox tb;
public static void main(String... args) throws Exception {
var tester = new TestPreCode();
tester.runTests();
}
TestPreCode() {
tb = new ToolBox();
}
@Test
public void testWhitespace(Path base) throws Exception {
Path srcDir = base.resolve("src");
Path outDir = base.resolve("out");
new ClassBuilder(tb, "pkg.A")
.setComments("""
Class A.
<pre> \t<code>
first line
second line
</code></pre>""")
.setModifiers("public", "class")
.addMembers(ClassBuilder.MethodBuilder.parse("public void m0() {}")
.setComments("""
Method m0.
<pre> {@code
first line
second line
}</pre>"""),
ClassBuilder.MethodBuilder.parse("public void m1() {}")
.setComments("""
Method m1.
<pre> <code> first line
second line
</code></pre>"""),
ClassBuilder.MethodBuilder.parse("public void m2() {}")
.setComments("""
Method m2.
<pre> {@code\s
first line
second line
}</pre>"""),
ClassBuilder.MethodBuilder.parse("public void m3() {}")
.setComments("""
Method m3.
<pre> .<code>
second line
</code></pre>"""))
.write(srcDir);
javadoc("-d", outDir.toString(),
"-sourcepath", srcDir.toString(),
"pkg");
checkExit(Exit.OK);
checkOrder("pkg/A.html",
"""
Class A.
<pre><code> first line
second line
</code></pre>""",
"""
Method m0.
<pre><code> first line
second line
</code></pre>""",
"""
Method m1.
<pre> <code> first line
second line
</code></pre>""",
"""
Method m2.
<pre><code> first line
second line
</code></pre>""",
"""
Method m3.
<pre> .<code>
second line
</code></pre>""");
}
@Test
public void testUnclosed(Path base) throws Exception {
Path srcDir = base.resolve("src");
Path outDir = base.resolve("out");
new ClassBuilder(tb, "pkg.A")
.setComments("""
Class A.
<pre><code>
first line
second line
</code>""")
.setModifiers("public", "class")
.write(srcDir);
javadoc("-d", outDir.toString(),
"-sourcepath", srcDir.toString(),
"pkg");
checkExit(Exit.ERROR);
// No whitespace normalization for unclosed <pre> element
checkOrder("pkg/A.html",
"""
Class A.
<pre><code>
first line
second line
</code></div>""");
}
}

View File

@ -23,7 +23,7 @@
/*
* @test
* @bug 7021614 8241780 8273244 8284908 8352249
* @bug 7021614 8241780 8273244 8284908 8352249 8352389
* @summary extend com.sun.source API to support parsing javadoc comments
* @modules jdk.compiler/com.sun.tools.javac.api
* jdk.compiler/com.sun.tools.javac.file
@ -128,7 +128,7 @@ DocComment[DOC_COMMENT, pos:0
name:pre
attributes: empty
]
Literal[CODE, pos:5, |____@Override|____void_m()_{_}|]
Literal[CODE, pos:5, ____@Override|____void_m()_{_}|]
body: 1
EndElement[END_ELEMENT, pos:44, pre]
block tags: empty

View File

@ -1023,7 +1023,9 @@ public class DocCommentTester {
.replaceFirst("\\.\\s*\\n *@(?![@*])", ".\n@") // Between block tags
.replaceAll("\n[ \t]+@(?!([@*]|(dummy|Override)))", "\n@")
.replaceAll("(?i)\\{@([a-z][a-z0-9.:-]*)\\s+}", "{@$1}")
.replaceAll("(\\{@value\\s+[^}]+)\\s+(})", "$1$2");
.replaceAll("(\\{@value\\s+[^}]+)\\s+(})", "$1$2")
.replaceAll("<pre> *\\{@code\\n", "<pre>{@code ")
.replaceAll("<pre> *<code>\\n", "<pre><code>");
}
}

View File

@ -23,7 +23,7 @@
/*
* @test
* @bug 8078320 8273244 8284908 8352249
* @bug 8078320 8273244 8284908 8352249 8352389
* @summary extend com.sun.source API to support parsing javadoc comments
* @modules jdk.compiler/com.sun.tools.javac.api
* jdk.compiler/com.sun.tools.javac.file
@ -163,6 +163,50 @@ DocComment[DOC_COMMENT, pos:1
EndElement[END_ELEMENT, pos:26, pre]
block tags: empty
]
*/
/**
* <pre> {@code
* abc }
* def</pre>
*/
public void in_pre_with_space_at_code_nl() { }
/*
DocComment[DOC_COMMENT, pos:0
firstSentence: 3
StartElement[START_ELEMENT, pos:0
name:pre
attributes: empty
]
Literal[CODE, pos:6, abc__]
Text[TEXT, pos:19, |def]
body: 1
EndElement[END_ELEMENT, pos:23, pre]
block tags: empty
]
*/
/**
* <pre> <code>
* abc
* </code></pre>
*/
public void in_pre_with_space_code_nl() { }
/*
DocComment[DOC_COMMENT, pos:0
firstSentence: 4
StartElement[START_ELEMENT, pos:0
name:pre
attributes: empty
]
StartElement[START_ELEMENT, pos:6
name:code
attributes: empty
]
Text[TEXT, pos:13, __abc|]
EndElement[END_ELEMENT, pos:19, code]
body: 1
EndElement[END_ELEMENT, pos:26, pre]
block tags: empty
]
*/
/**
* abc {@code