8275788: Create code element with suitable attributes for code snippets

Reviewed-by: prappo
This commit is contained in:
Hannes Wallnöfer 2021-11-04 13:06:24 +00:00
parent 7de653e428
commit 19075b3f6b
4 changed files with 261 additions and 141 deletions
src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets
test/langtools/jdk/javadoc/doclet/testSnippetTag

@ -381,24 +381,22 @@ public class TagletWriterImpl extends TagletWriter {
}
@Override
protected Content snippetTagOutput(Element element, SnippetTree tag, StyledText content) {
String copyText = resources.getText("doclet.Copy_snippet_to_clipboard");
String copiedText = resources.getText("doclet.Copied_snippet_to_clipboard");
HtmlTree copy = HtmlTree.DIV(HtmlStyle.snippetContainer,
HtmlTree.A("#", new HtmlTree(TagName.IMG)
.put(HtmlAttr.SRC, htmlWriter.pathToRoot.resolve(DocPaths.CLIPBOARD_SVG).getPath())
.put(HtmlAttr.ALT, copyText))
.addStyle(HtmlStyle.snippetCopy)
.put(HtmlAttr.ONCLICK, "copySnippet(this)")
.put(HtmlAttr.ARIA_LABEL, copyText)
.put(HtmlAttr.DATA_COPIED, copiedText));
HtmlTree pre = new HtmlTree(TagName.PRE)
.setStyle(HtmlStyle.snippet);
pre.add(Text.of(utils.normalizeNewlines("\n")));
protected Content snippetTagOutput(Element element, SnippetTree tag, StyledText content,
String id, String lang) {
HtmlTree pre = new HtmlTree(TagName.PRE).setStyle(HtmlStyle.snippet);
if (id != null && !id.isBlank()) {
pre.put(HtmlAttr.ID, id);
}
HtmlTree code = new HtmlTree(TagName.CODE)
.add(HtmlTree.EMPTY); // Make sure the element is always rendered
if (lang != null && !lang.isBlank()) {
code.addStyle("language-" + lang);
}
content.consumeBy((styles, sequence) -> {
CharSequence text = utils.normalizeNewlines(sequence);
if (styles.isEmpty()) {
pre.add(text);
code.add(text);
} else {
Element e = null;
String t = null;
@ -443,10 +441,20 @@ public class TagletWriterImpl extends TagletWriter {
c = HtmlTree.SPAN(Text.of(sequence));
classes.forEach(((HtmlTree) c)::addStyle);
}
pre.add(c);
code.add(c);
}
});
return copy.add(pre);
String copyText = resources.getText("doclet.Copy_snippet_to_clipboard");
String copiedText = resources.getText("doclet.Copied_snippet_to_clipboard");
HtmlTree snippetContainer = HtmlTree.DIV(HtmlStyle.snippetContainer,
HtmlTree.A("#", new HtmlTree(TagName.IMG)
.put(HtmlAttr.SRC, htmlWriter.pathToRoot.resolve(DocPaths.CLIPBOARD_SVG).getPath())
.put(HtmlAttr.ALT, copyText))
.addStyle(HtmlStyle.snippetCopy)
.put(HtmlAttr.ONCLICK, "copySnippet(this)")
.put(HtmlAttr.ARIA_LABEL, copyText)
.put(HtmlAttr.DATA_COPIED, copiedText));
return snippetContainer.add(pre.add(code));
}
/*

@ -261,7 +261,21 @@ public class SnippetTaglet extends BaseTaglet {
assert inlineSnippet != null || externalSnippet != null;
StyledText text = inlineSnippet != null ? inlineSnippet : externalSnippet;
return writer.snippetTagOutput(holder, snippetTag, text);
String lang = null;
AttributeTree langAttr = attributes.get("lang");
if (langAttr != null && langAttr.getValueKind() != AttributeTree.ValueKind.EMPTY) {
lang = stringOf(langAttr.getValue());
} else if (containsClass) {
lang = "java";
} else if (containsFile) {
lang = languageFromFileName(fileObject.getName());
}
AttributeTree idAttr = attributes.get("id");
String id = idAttr == null || idAttr.getValueKind() == AttributeTree.ValueKind.EMPTY
? null
: stringOf(idAttr.getValue());
return writer.snippetTagOutput(holder, snippetTag, text, id, lang);
}
/*
@ -298,6 +312,16 @@ public class SnippetTaglet extends BaseTaglet {
.collect(Collectors.joining());
}
private String languageFromFileName(String fileName) {
// TODO: find a way to extend/customize the list of recognized file name extensions
if (fileName.endsWith(".java")) {
return "java";
} else if (fileName.endsWith(".properties")) {
return "properties";
}
return null;
}
private void error(TagletWriter writer, Element holder, DocTree tag, String key, Object... args) {
writer.configuration().getMessages().error(
writer.configuration().utils.getCommentHelper(holder).getDocTreePath(tag), key, args);

@ -181,10 +181,13 @@ public abstract class TagletWriter {
*
* @param element The element that owns the doc comment
* @param snippetTag the snippet tag
* @param id the value of the id attribute, or null if not defined
* @param lang the value of the lang attribute, or null if not defined
*
* @return the output
*/
protected abstract Content snippetTagOutput(Element element, SnippetTree snippetTag, StyledText text);
protected abstract Content snippetTagOutput(Element element, SnippetTree snippetTag, StyledText text,
String id, String lang);
/**
* Returns the output for a {@code {@systemProperty...}} tag.

@ -23,7 +23,7 @@
/*
* @test
* @bug 8266666
* @bug 8266666 8275788
* @summary Implementation for snippets
* @library /tools/lib ../../lib
* @modules jdk.compiler/com.sun.tools.javac.api
@ -84,144 +84,237 @@ public class TestSnippetTag extends JavadocTester {
}
/*
* While the "id" and "lang" attributes are advertised in JEP 413, they are
* currently unused by the implementation. The goal of this test is to make
* sure that specifying these attributes causes no errors and exhibits no
* unexpected behavior.
* Make sure the "id" and "lang" attributes defined in JEP 413 are rendered
* properly as recommended by the HTML5 specification.
*/
@Test
public void testIdAndLangAttributes(Path base) throws IOException {
Path srcDir = base.resolve("src");
Path outDir = base.resolve("out");
// A record of a snippet content and matching expected attribute values
record SnippetAttributes(String content, String id, String lang) {
public String idAttribute() {
return id == null ? "" : " id=\"" + id + "\"";
}
public String langAttribute() {
return lang == null ? "" : " class=\"language-" + lang + "\"";
}
}
final var snippets = List.of(
"""
{@snippet id="foo" :
Hello, Snippet!
}
""",
"""
{@snippet id="foo":
Hello, Snippet!
}
""",
"""
{@snippet id='foo' :
Hello, Snippet!
}
""",
"""
{@snippet id='foo':
Hello, Snippet!
}
""",
"""
{@snippet id=foo :
Hello, Snippet!
}
""",
new SnippetAttributes("""
{@snippet id="foo1" :
Hello, Snippet!
}
""", "foo1", null),
new SnippetAttributes("""
{@snippet id="foo2":
Hello, Snippet!
}
""", "foo2", null),
new SnippetAttributes("""
{@snippet id='foo3' :
Hello, Snippet!
}
""", "foo3", null),
new SnippetAttributes("""
{@snippet id='foo4':
Hello, Snippet!
}
""", "foo4", null),
new SnippetAttributes("""
{@snippet id=foo5 :
Hello, Snippet!
}
""", "foo5", null),
// (1) Haven't yet decided on this one. It's a consistency issue. On the one
// hand, `:` is considered a part of a javadoc tag's name (e.g. JDK-4750173);
// on the other hand, snippet markup treats `:` (next-line modifier) as a value
// terminator.
// """
// {@snippet id=foo:
// Hello, Snippet!
// }
// """,
"""
{@snippet id="" :
Hello, Snippet!
}
""",
"""
{@snippet id="":
Hello, Snippet!
}
""",
"""
{@snippet id='':
Hello, Snippet!
}
""",
"""
{@snippet id=:
Hello, Snippet!
}
""",
"""
{@snippet lang="java" :
Hello, Snippet!
}
""",
"""
{@snippet lang="java":
Hello, Snippet!
}
""",
"""
{@snippet lang='java' :
Hello, Snippet!
}
""",
"""
{@snippet lang='java':
Hello, Snippet!
}
""",
"""
{@snippet lang=java :
Hello, Snippet!
}
""",
"""
{@snippet lang="properties" :
Hello, Snippet!
}
""",
"""
{@snippet lang="text" :
Hello, Snippet!
}
""",
"""
{@snippet lang="" :
Hello, Snippet!
}
""",
"""
{@snippet lang="foo" id="bar" :
Hello, Snippet!
}
"""
// new SnippetAttributes("""
// {@snippet id=foo6:
// Hello, Snippet!
// }
// """, "foo6", null),
new SnippetAttributes("""
{@snippet id="" :
Hello, Snippet!
}
""", null, null),
new SnippetAttributes("""
{@snippet id="":
Hello, Snippet!
}
""", null, null),
new SnippetAttributes("""
{@snippet id='':
Hello, Snippet!
}
""", null, null),
new SnippetAttributes("""
{@snippet id=:
Hello, Snippet!
}
""", null, null),
new SnippetAttributes("""
{@snippet lang="java" :
Hello, Snippet!
}
""", null, "java"),
new SnippetAttributes("""
{@snippet lang="java":
Hello, Snippet!
}
""", null, "java"),
new SnippetAttributes("""
{@snippet lang='java' :
Hello, Snippet!
}
""", null, "java"),
new SnippetAttributes("""
{@snippet lang='java':
Hello, Snippet!
}
""", null, "java"),
new SnippetAttributes("""
{@snippet lang=java :
Hello, Snippet!
}
""", null, "java"),
new SnippetAttributes("""
{@snippet lang="properties" :
Hello, Snippet!
}
""", null, "properties"),
new SnippetAttributes("""
{@snippet lang="text" :
Hello, Snippet!
}
""", null, "text"),
new SnippetAttributes("""
{@snippet lang="" :
Hello, Snippet!
}
""", null, null),
new SnippetAttributes("""
{@snippet lang="foo" id="bar" :
Hello, Snippet!
}
""", "bar", "foo")
);
ClassBuilder classBuilder = new ClassBuilder(tb, "pkg.A")
.setModifiers("public", "class");
forEachNumbered(snippets, (s, i) -> {
classBuilder.addMembers(
MethodBuilder.parse("public void case%s() { }".formatted(i))
.setComments(s));
.setComments("A method.", s.content()));
});
classBuilder.write(srcDir);
javadoc("-d", outDir.toString(),
"-sourcepath", srcDir.toString(),
"pkg");
checkExit(Exit.OK);
checkLinks();
for (int j = 0; j < snippets.size(); j++) {
SnippetAttributes snippet = snippets.get(j);
checkOutput("pkg/A.html", true,
"""
<span class="element-name">case%s</span>()</div>
<div class="block">
<div class="block">A method.
\s
<div class="snippet-container"><a href="#" class="snippet-copy" onclick="cop\
ySnippet(this)" aria-label="Copy" data-copied="Copied!"><img src="../copy.sv\
g" alt="Copy"></a>
<pre class="snippet">
Hello, Snippet!
</pre>
<pre class="snippet"%s><code%s> Hello, Snippet!
</code></pre>
</div>
""".formatted(j));
""".formatted(j, snippet.idAttribute(), snippet.langAttribute()));
}
}
/*
* Make sure the lang attribute is derived correctly from the snippet source file
* for external snippets when it is not defined in the snippet. Defining the lang
* attribute in the snippet should always override this mechanism.
*/
@Test
public void testExternalImplicitAttributes(Path base) throws IOException {
Path srcDir = base.resolve("src");
Path outDir = base.resolve("out");
ClassBuilder classBuilder = new ClassBuilder(tb, "com.example.Cls")
.setModifiers("public", "class");
classBuilder.setComments("""
{@snippet class="Snippets" region="code" id="snippet1"}
{@snippet file="Snippets.java" region="code" id="snippet2"}
{@snippet class="Snippets" region="code" id="snippet3" lang="none"}
{@snippet file="Snippets.java" region="code" id="snippet4" lang="none"}
{@snippet class="Snippets" region="code" id="snippet5" lang=""}
{@snippet file="Snippets.java" region="code" id="snippet6" lang=""}
{@snippet file="user.properties" id="snippet7"}
{@snippet file="user.properties" id="snippet8" lang="none"}
{@snippet file="user.properties" id="snippet9" lang=""}
""");
addSnippetFile(srcDir, "com.example", "Snippets.java", """
public class Snippets {
public static void printMessage(String msg) {
// @start region="code"
System.out.println(msg);
// @end
}
}
""");
addSnippetFile(srcDir, "com.example", "user.properties", """
user=jane
home=/home/jane
""");
classBuilder.write(srcDir);
javadoc("-d", outDir.toString(),
"-sourcepath", srcDir.toString(),
"com.example");
checkExit(Exit.OK);
checkLinks();
checkOutput("com/example/Cls.html", true,
"""
<pre class="snippet" id="snippet1"><code class="language-java">
System.out.println(msg);
</code></pre>""",
"""
<pre class="snippet" id="snippet2"><code class="language-java">
System.out.println(msg);
</code></pre>""",
"""
<pre class="snippet" id="snippet3"><code class="language-none">
System.out.println(msg);
</code></pre>""",
"""
<pre class="snippet" id="snippet4"><code class="language-none">
System.out.println(msg);
</code></pre>""",
"""
<pre class="snippet" id="snippet5"><code>
System.out.println(msg);
</code></pre>""",
"""
<pre class="snippet" id="snippet6"><code>
System.out.println(msg);
</code></pre>""",
"""
<pre class="snippet" id="snippet7"><code class="language-properties">user=jane
home=/home/jane
</code></pre>""",
"""
<pre class="snippet" id="snippet8"><code class="language-none">user=jane
home=/home/jane
</code></pre>""",
"""
<pre class="snippet" id="snippet9"><code>user=jane
home=/home/jane
</code></pre>""");
}
/*
* This is a convenience method to iterate through a list.
* Unlike List.forEach, this method provides the consumer not only with an
@ -858,8 +951,7 @@ public class TestSnippetTag extends JavadocTester {
<div class="snippet-container"><a href="#" class="snippet-copy" onclick="cop\
ySnippet(this)" aria-label="Copy" data-copied="Copied!"><img src="../copy.sv\
g" alt="Copy"></a>
<pre class="snippet">
%s</pre>
<pre class="snippet"><code>%s</code></pre>
</div>""".formatted(id, t.expectedOutput()));
});
}
@ -955,8 +1047,7 @@ public class TestSnippetTag extends JavadocTester {
<div class="snippet-container"><a href="#" class="snippet-copy" onclick="cop\
ySnippet(this)" aria-label="Copy" data-copied="Copied!"><img src="../copy.sv\
g" alt="Copy"></a>
<pre class="snippet">
%s</pre>
<pre class="snippet"><code>%s</code></pre>
</div>""".formatted(index, expectedOutput));
});
}
@ -1517,8 +1608,7 @@ public class TestSnippetTag extends JavadocTester {
<div class="snippet-container"><a href="#" class="snippet-copy" onclick="cop\
ySnippet(this)" aria-label="Copy" data-copied="Copied!"><img src="../copy.sv\
g" alt="Copy"></a>
<pre class="snippet">
%s</pre>
<pre class="snippet"><code>%s</code></pre>
</div>""".formatted(index, t.expectedOutput()));
});
}
@ -1635,8 +1725,7 @@ public class TestSnippetTag extends JavadocTester {
<div class="snippet-container"><a href="#" class="snippet-copy" onclick="copySni\
ppet(this)" aria-label="Copy" data-copied="Copied!"><img src="../copy.svg" alt="\
Copy"></a>
<pre class="snippet">
</pre>
<pre class="snippet"><code></code></pre>
</div>""");
checkOutput("pkg/A.html", true,
"""
@ -1645,8 +1734,7 @@ public class TestSnippetTag extends JavadocTester {
<div class="snippet-container"><a href="#" class="snippet-copy" onclick="copySni\
ppet(this)" aria-label="Copy" data-copied="Copied!"><img src="../copy.svg" alt="\
Copy"></a>
<pre class="snippet">
</pre>
<pre class="snippet"><code></code></pre>
</div>""");
}
@ -1747,8 +1835,7 @@ public class TestSnippetTag extends JavadocTester {
<div class="snippet-container"><a href="#" class="snippet-copy" onclick="cop\
ySnippet(this)" aria-label="Copy" data-copied="Copied!"><img src="../copy.sv\
g" alt="Copy"></a>
<pre class="snippet">
2</pre>
<pre class="snippet"><code>2</code></pre>
</div>
""".formatted(j));
}
@ -1832,8 +1919,7 @@ public class TestSnippetTag extends JavadocTester {
<div class="snippet-container"><a href="#" class="snippet-copy" onclick="cop\
ySnippet(this)" aria-label="Copy" data-copied="Copied!"><img src="../copy.sv\
g" alt="Copy"></a>
<pre class="snippet">
%s</pre>
<pre class="snippet"><code>%s</code></pre>
</div>""".formatted(index, t.expectedOutput()));
});
}
@ -2165,8 +2251,7 @@ public class TestSnippetTag extends JavadocTester {
<div class="snippet-container"><a href="#" class="snippet-copy" onclick="cop\
ySnippet(this)" aria-label="Copy" data-copied="Copied!"><img src="../copy.sv\
g" alt="Copy"></a>
<pre class="snippet">
%s</pre>
<pre class="snippet"><code>%s</code></pre>
</div>""".formatted(index, t.expectedOutput()));
});
}