8276124: Provide snippet support for properties files

Co-authored-by: Jonathan Gibbons <jjg@openjdk.org>
Co-authored-by: Hannes Wallnöfer <hannesw@openjdk.org>
Reviewed-by: jjg
This commit is contained in:
Pavel Rappo 2021-11-24 20:13:06 +00:00
parent 96fe1d0d4d
commit e785f69961
4 changed files with 263 additions and 41 deletions
src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/taglets
test/langtools/jdk/javadoc/doclet/testSnippetTag

@ -30,6 +30,7 @@ import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import javax.lang.model.element.Element;
@ -63,6 +64,38 @@ import jdk.javadoc.internal.doclets.toolkit.util.Utils;
*/
public class SnippetTaglet extends BaseTaglet {
public enum Language {
JAVA("java"),
PROPERTIES("properties");
private static final Map<String, Language> languages;
static {
Map<String, Language> tmp = new HashMap<>();
for (var language : values()) {
String id = Objects.requireNonNull(language.identifier);
if (tmp.put(id, language) != null)
throw new IllegalStateException(); // 1-1 correspondence
}
languages = Map.copyOf(tmp);
}
Language(String id) {
identifier = id;
}
private final String identifier;
public static Optional<Language> of(String identifier) {
if (identifier == null)
return Optional.empty();
return Optional.ofNullable(languages.get(identifier));
}
public String getIdentifier() {return identifier;}
}
public SnippetTaglet() {
super(DocTree.Kind.SNIPPET, true, EnumSet.allOf(Taglet.Location.class));
}
@ -217,6 +250,19 @@ public class SnippetTaglet extends BaseTaglet {
}
}
String lang = null;
AttributeTree langAttr = attributes.get("lang");
if (langAttr != null) {
lang = stringValueOf(langAttr);
} else if (containsClass) {
lang = "java";
} else if (containsFile) {
lang = languageFromFileName(fileObject.getName());
}
Optional<Language> language = Language.of(lang);
// TODO cache parsed external snippet (WeakHashMap)
StyledText inlineSnippet = null;
@ -224,7 +270,7 @@ public class SnippetTaglet extends BaseTaglet {
try {
if (inlineContent != null) {
inlineSnippet = parse(writer.configuration().getDocResources(), inlineContent);
inlineSnippet = parse(writer.configuration().getDocResources(), language, inlineContent);
}
} catch (ParseException e) {
var path = writer.configuration().utils.getCommentHelper(holder)
@ -239,7 +285,7 @@ public class SnippetTaglet extends BaseTaglet {
try {
if (externalContent != null) {
externalSnippet = parse(writer.configuration().getDocResources(), externalContent);
externalSnippet = parse(writer.configuration().getDocResources(), language, externalContent);
}
} catch (ParseException e) {
assert fileObject != null;
@ -289,15 +335,6 @@ public class SnippetTaglet extends BaseTaglet {
assert inlineSnippet != null || externalSnippet != null;
StyledText text = inlineSnippet != null ? inlineSnippet : externalSnippet;
String lang = null;
AttributeTree langAttr = attributes.get("lang");
if (langAttr != null) {
lang = stringValueOf(langAttr);
} else if (containsClass) {
lang = "java";
} else if (containsFile) {
lang = languageFromFileName(fileObject.getName());
}
AttributeTree idAttr = attributes.get("id");
String id = idAttr == null
? null
@ -326,8 +363,8 @@ public class SnippetTaglet extends BaseTaglet {
""".formatted(inline, external);
}
private StyledText parse(Resources resources, String content) throws ParseException {
Parser.Result result = new Parser(resources).parse(content);
private StyledText parse(Resources resources, Optional<Language> language, String content) throws ParseException {
Parser.Result result = new Parser(resources).parse(language, content);
result.actions().forEach(Action::perform);
return result.text();
}

@ -39,6 +39,7 @@ import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import jdk.javadoc.internal.doclets.toolkit.Resources;
import jdk.javadoc.internal.doclets.toolkit.taglets.SnippetTaglet;
/*
* Semantics of a EOL comment; plus
@ -76,10 +77,10 @@ import jdk.javadoc.internal.doclets.toolkit.Resources;
*/
public final class Parser {
// next-line tag behaves as if it were specified on the next line
private String eolMarker;
private Matcher markedUpLine;
private static final Pattern JAVA_COMMENT = Pattern.compile(
"^(?<payload>.*)//(?<markup>\\s*@\\s*\\w+.+?)$");
private static final Pattern PROPERTIES_COMMENT = Pattern.compile(
"^(?<payload>[ \t]*([#!].*)?)[#!](?<markup>\\s*@\\s*\\w+.+?)$");
private final Resources resources;
private final MarkupParser markupParser;
@ -93,32 +94,23 @@ public final class Parser {
this.markupParser = new MarkupParser(resources);
}
public Result parse(String source) throws ParseException {
return parse("//", source);
public Result parse(Optional<SnippetTaglet.Language> language, String source) throws ParseException {
SnippetTaglet.Language lang = language.orElse(SnippetTaglet.Language.JAVA);
var p = switch (lang) {
case JAVA -> JAVA_COMMENT;
case PROPERTIES -> PROPERTIES_COMMENT;
};
return parse(p, source);
}
/*
* Newline characters in the returned text are of the \n form.
*/
public Result parse(String eolMarker, String source) throws ParseException {
Objects.requireNonNull(eolMarker);
private Result parse(Pattern commentPattern, String source) throws ParseException {
Objects.requireNonNull(commentPattern);
Objects.requireNonNull(source);
if (!Objects.equals(eolMarker, this.eolMarker)) {
if (eolMarker.length() < 1) {
throw new IllegalArgumentException();
}
for (int i = 0; i < eolMarker.length(); i++) {
switch (eolMarker.charAt(i)) {
case '\f', '\n', '\r' -> throw new IllegalArgumentException();
}
}
this.eolMarker = eolMarker;
// capture the rightmost eolMarker (e.g. "//")
// The below Pattern.compile should never throw PatternSyntaxException
Pattern pattern = Pattern.compile("^(.*)(" + Pattern.quote(eolMarker)
+ "(\\s*@\\s*\\w+.+?))$");
this.markedUpLine = pattern.matcher(""); // reusable matcher
}
Matcher markedUpLine = commentPattern.matcher(""); // reusable matcher
tags.clear();
regions.clear();
@ -151,17 +143,17 @@ public final class Parser {
if (!markedUpLine.matches()) { // (1)
line = rawLine + (addLineTerminator ? "\n" : "");
} else {
String maybeMarkup = markedUpLine.group(3);
String maybeMarkup = rawLine.substring(markedUpLine.start("markup"));
List<Tag> parsedTags;
try {
parsedTags = markupParser.parse(maybeMarkup);
} catch (ParseException e) {
// translate error position from markup to file line
throw new ParseException(e::getMessage, markedUpLine.start(3) + e.getPosition());
throw new ParseException(e::getMessage, markedUpLine.start("markup") + e.getPosition());
}
for (Tag t : parsedTags) {
t.lineSourceOffset = next.offset();
t.markupLineOffset = markedUpLine.start(3);
t.markupLineOffset = markedUpLine.start("markup");
}
thisLineTags.addAll(parsedTags);
for (var tagIterator = thisLineTags.iterator(); tagIterator.hasNext(); ) {
@ -176,7 +168,7 @@ public final class Parser {
// TODO: log this with NOTICE;
line = rawLine + (addLineTerminator ? "\n" : "");
} else { // (3)
String payload = markedUpLine.group(1);
String payload = rawLine.substring(0, markedUpLine.end("payload"));
line = payload + (addLineTerminator ? "\n" : "");
}
}

@ -101,6 +101,12 @@ public class SnippetTester extends JavadocTester {
return getSnippetHtmlRepresentation(pathToHtmlFile, content, Optional.empty(), Optional.empty());
}
protected String getSnippetHtmlRepresentation(String pathToHtmlFile,
String content,
Optional<String> lang) {
return getSnippetHtmlRepresentation(pathToHtmlFile, content, lang, Optional.empty());
}
protected String getSnippetHtmlRepresentation(String pathToHtmlFile,
String content,
Optional<String> lang,

@ -0,0 +1,187 @@
/*
* Copyright (c) 2021, 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 8266666
* @summary Implementation for snippets
* @library /tools/lib ../../lib
* @modules jdk.compiler/com.sun.tools.javac.api
* jdk.compiler/com.sun.tools.javac.main
* jdk.javadoc/jdk.javadoc.internal.tool
* @build javadoc.tester.* toolbox.ToolBox toolbox.ModuleBuilder builder.ClassBuilder
* @run main TestLangProperties
*/
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
public class TestLangProperties extends SnippetTester {
public static void main(String... args) throws Exception {
new TestLangProperties().runTests(m -> new Object[]{Paths.get(m.getName())});
}
@Test
public void testPositiveOuterMarkup(Path base) throws Exception {
var testCases = new ArrayList<TestSnippetMarkup.TestCase>();
for (String whitespace1 : List.of("", " ", "\t"))
for (String commentIndicator1 : List.of("#", "!"))
for (String whitespace2 : List.of("", " ", "\t")) {
String markup = whitespace1 + commentIndicator1
+ whitespace2 + "@highlight :";
var t = new TestSnippetMarkup.TestCase(
"""
%s
coffee=espresso
tea=black
""".formatted(markup),
"""
<span class="bold">coffee=espresso
</span>tea=black
""");
testCases.add(t);
}
testPositive(base, testCases);
}
@Test
public void testPositiveInnerMarkup(Path base) throws Exception {
var testCases = new ArrayList<TestSnippetMarkup.TestCase>();
for (String whitespace1 : List.of("", " ", "\t"))
for (String commentIndicator1 : List.of("#", "!"))
for (String whitespace2 : List.of("", " ", "\t"))
for (String unrelatedComment : List.of("a comment"))
for (String whitespace3 : List.of("", " "))
for (String commentIndicator2 : List.of("#", "!")) {
String payload = whitespace1 + commentIndicator1 + whitespace2 + unrelatedComment;
String markup = payload + whitespace3 + commentIndicator2 + "@highlight :";
var t = new TestSnippetMarkup.TestCase(
"""
%s
coffee=espresso
tea=black
""".formatted(markup),
"""
%s
<span class="bold">coffee=espresso
</span>tea=black
""".formatted(payload));
testCases.add(t);
}
testPositive(base, testCases);
}
@Test
public void testPositiveIneffectiveOuterMarkup(Path base) throws Exception {
var testCases = new ArrayList<TestSnippetMarkup.TestCase>();
for (String whitespace1 : List.of("", " ", "\t"))
for (String commentIndicator1 : List.of("#", "!"))
for (String whitespace2 : List.of("", " ", "\t")) {
String ineffectiveMarkup = whitespace1
+ commentIndicator1 + whitespace2
+ "@highlight :";
var t = new TestSnippetMarkup.TestCase(
"""
coffee=espresso%s
tea=black
""".formatted(ineffectiveMarkup),
"""
coffee=espresso%s
tea=black
""".formatted(ineffectiveMarkup));
testCases.add(t);
}
testPositive(base, testCases);
}
@Test
public void testPositiveIneffectiveInnerMarkup(Path base) throws Exception {
var testCases = new ArrayList<TestSnippetMarkup.TestCase>();
for (String whitespace1 : List.of("", " ", "\t"))
for (String commentIndicator1 : List.of("#", "!"))
for (String whitespace2 : List.of("", " ", "\t"))
for (String unrelatedComment : List.of("a comment"))
for (String whitespace3 : List.of("", " "))
for (String commentIndicator2 : List.of("#", "!")) {
String ineffectiveMarkup = whitespace1
+ commentIndicator1 + whitespace2
+ unrelatedComment + whitespace3
+ commentIndicator2 + "@highlight :";
var t = new TestSnippetMarkup.TestCase(
"""
coffee=espresso%s
tea=black
""".formatted(ineffectiveMarkup),
"""
coffee=espresso%s
tea=black
""".formatted(ineffectiveMarkup));
testCases.add(t);
}
testPositive(base, testCases);
}
private void testPositive(Path base, List<TestSnippetMarkup.TestCase> testCases)
throws IOException {
StringBuilder methods = new StringBuilder();
forEachNumbered(testCases, (i, n) -> {
String r = i.region().isBlank() ? "" : "region=" + i.region();
var methodDef = """
/**
{@snippet lang="properties" %s:
%s}*/
public void case%s() {}
""".formatted(r, i.input(), n);
methods.append(methodDef);
});
var classDef = """
public class A {
%s
}
""".formatted(methods.toString());
Path src = Files.createDirectories(base.resolve("src"));
tb.writeJavaFiles(src, classDef);
javadoc("-d", base.resolve("out").toString(),
"-sourcepath", src.toString(),
src.resolve("A.java").toString());
checkExit(Exit.OK);
checkNoCrashes();
forEachNumbered(testCases, (t, index) -> {
String html = """
<span class="element-name">case%s</span>()</div>
<div class="block">
%s
</div>""".formatted(index, getSnippetHtmlRepresentation("A.html",
t.expectedOutput(), Optional.of("properties")));
checkOutput("A.html", true, html);
});
}
}