This is an automated email from the ASF dual-hosted git repository. kwin pushed a commit to branch feature/improve-content-rendering in repository https://gitbox.apache.org/repos/asf/maven-resolver.git
commit defd1a014d07e86d91c3e37c727255a228fb102a Author: Konrad Windszus <[email protected]> AuthorDate: Mon Jul 20 12:13:01 2026 +0200 Improve rendering of javadoc content Render all inline tags in the same way as the standard doclet. Some code simplifications. --- .../aether/tools/ConfigurationCollectorDoclet.java | 226 +++++++++++++-------- .../tools/ConfigurationCollectorDocletTest.java | 5 +- .../aether/sample/SampleConfigurationKeys.java | 7 +- 3 files changed, 156 insertions(+), 82 deletions(-) diff --git a/maven-resolver-tools/src/main/java/org/eclipse/aether/tools/ConfigurationCollectorDoclet.java b/maven-resolver-tools/src/main/java/org/eclipse/aether/tools/ConfigurationCollectorDoclet.java index 75a7401e4..4da2cbe17 100644 --- a/maven-resolver-tools/src/main/java/org/eclipse/aether/tools/ConfigurationCollectorDoclet.java +++ b/maven-resolver-tools/src/main/java/org/eclipse/aether/tools/ConfigurationCollectorDoclet.java @@ -41,24 +41,30 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Objects; import java.util.Properties; import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; +import com.sun.source.doctree.AttributeTree; import com.sun.source.doctree.DocCommentTree; import com.sun.source.doctree.DocTree; +import com.sun.source.doctree.EndElementTree; +import com.sun.source.doctree.EntityTree; import com.sun.source.doctree.LinkTree; import com.sun.source.doctree.LiteralTree; import com.sun.source.doctree.SinceTree; +import com.sun.source.doctree.StartElementTree; +import com.sun.source.doctree.SystemPropertyTree; import com.sun.source.doctree.TextTree; import com.sun.source.doctree.UnknownBlockTagTree; +import com.sun.source.doctree.ValueTree; import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.IdentifierTree; import com.sun.source.tree.MemberSelectTree; import com.sun.source.tree.VariableTree; import com.sun.source.util.DocTreePath; import com.sun.source.util.DocTrees; +import com.sun.source.util.SimpleDocTreeVisitor; import jdk.javadoc.doclet.Doclet; import jdk.javadoc.doclet.DocletEnvironment; import jdk.javadoc.doclet.Reporter; @@ -183,13 +189,17 @@ public class ConfigurationCollectorDoclet implements Doclet { Map<String, String> entry = new LinkedHashMap<>(); entry.put("key", String.valueOf(field.getConstantValue())); - entry.put("defaultValue", nvl(defValue, "")); + entry.put("defaultValue", Objects.toString(defValue, "")); entry.put("fqName", type.getQualifiedName() + "." + field.getSimpleName()); - entry.put("description", cleanseJavadoc(renderContent(docComment.getFullBody()))); - entry.put("since", nvl(getSince(type, docComment), "")); - entry.put("configurationSource", getConfigurationSource(renderContent(blockTags.get("configurationSource")))); + entry.put("description", renderContent(docComment.getFullBody(), field, docComment, true)); + entry.put("since", Objects.toString(getSince(type, docComment, field), "")); + entry.put( + "configurationSource", + getConfigurationSource(renderContent(blockTags.get("configurationSource"), field, docComment, true))); entry.put("configurationType", configurationType); - entry.put("supportRepoIdSuffix", toYesNo(renderContent(blockTags.get("configurationRepoIdSuffix")))); + entry.put( + "supportRepoIdSuffix", + toYesNo(renderContent(blockTags.get("configurationRepoIdSuffix"), field, docComment, true))); discovered.add(entry); } @@ -252,15 +262,15 @@ public class ConfigurationCollectorDoclet implements Doclet { configurationType = configurationType.substring("java.util.".length()); } - String description = docComment != null ? renderContent(docComment.getFullBody()) : ""; + String description = docComment != null ? renderContent(docComment.getFullBody(), field, docComment, true) : ""; description = description.replace("*", "\\*"); Map<String, String> entry = new LinkedHashMap<>(); entry.put("key", String.valueOf(field.getConstantValue())); - entry.put("defaultValue", nvl(defaultValue, "")); + entry.put("defaultValue", Objects.toString(defaultValue, "")); entry.put("fqName", ""); - entry.put("description", nvl(description, "")); - entry.put("since", nvl(getSince(type, docComment), "")); + entry.put("description", Objects.toString(description, "")); + entry.put("since", Objects.toString(getSince(type, docComment, field), "")); entry.put("configurationSource", source); entry.put("configurationType", configurationType); entry.put("supportRepoIdSuffix", ""); @@ -340,7 +350,7 @@ public class ConfigurationCollectorDoclet implements Doclet { } } } - return renderContent(content); + return renderContent(content, contextField, docComment, true); } /** @@ -457,51 +467,148 @@ public class ConfigurationCollectorDoclet implements Doclet { : signature; } - private String renderContent(List<? extends DocTree> content) { + /** + * Renders the content of a Javadoc tag into a string, escaping HTML special characters and rendering inline tags. + * @param content + * @param context + * @param contextDoc + * @param trim if true, trims the result string + * @return + * @see <a href="https://docs.oracle.com/en/java/javase/25/docs/specs/javadoc/doc-comment-spec.html#standard-tags">Javadoc tags</a> + * @see <a href="https://docs.oracle.com/en/java/javase/25/docs/api/jdk.compiler/com/sun/source/doctree/InlineTagTree.html">InlineTagTree (common superinterface of all inline tags)</a> + */ + private String renderContent( + List<? extends DocTree> content, VariableElement context, DocCommentTree contextDoc, boolean trim) { if (content == null) { return null; } StringBuilder sb = new StringBuilder(); + SimpleDocTreeVisitor<String, Void> visitor = new SimpleDocTreeVisitor<String, Void>() { + @Override + public String visitText(TextTree node, Void p) { + return escapeHtml(node.getBody()); + } + + @Override + public String visitLink(LinkTree node, Void p) { + String ref = node.getReference() != null ? node.getReference().getSignature() : ""; + String label = renderContent(node.getLabel(), null, null, false); + String text = label == null || label.isEmpty() ? ref : label; + return node.getKind() == DocTree.Kind.LINK_PLAIN ? escapeHtml(text) : renderAsCode(text); + } + + @Override + public String visitLiteral(LiteralTree node, Void p) { + if (node.getKind() == DocTree.Kind.LITERAL) { + return renderAsCode(node.getBody().getBody()); + } else { + return escapeHtml(node.getBody().getBody()); + } + } + + @Override + public String visitSystemProperty(SystemPropertyTree node, Void p) { + return renderAsCode(node.getPropertyName().toString()); + } + + private String renderAsCode(String text) { + return "<code>" + escapeHtml(text) + "</code>"; + } + + @Override + public String visitValue(ValueTree node, Void p) { + if (node.getReference() != null && context != null && contextDoc != null) { + DocTreePath rootPath = new DocTreePath(docTrees.getPath(context), contextDoc); + DocTreePath refPath = DocTreePath.getPath(rootPath, node.getReference()); + if (refPath != null) { + Element element = docTrees.getElement(refPath); + if (element instanceof VariableElement ve) { + String value = lookupConstant(ve); + if (value != null) { + return renderAsCode(value); + } + } + } + } + // fall back to showing the reference signature + String ref = node.getReference() != null ? node.getReference().getSignature() : ""; + return renderAsCode(ref); + } + + @Override + public String visitStartElement(StartElementTree node, Void p) { + StringBuilder sb = new StringBuilder("<"); + sb.append(node.getName()); + for (DocTree attr : node.getAttributes()) { + if (attr instanceof AttributeTree a) { + sb.append(" ").append(a.getName()); + if (a.getValueKind() != AttributeTree.ValueKind.EMPTY) { + String quote = a.getValueKind() == AttributeTree.ValueKind.SINGLE ? "'" : "\""; + sb.append("=").append(quote); + sb.append(renderContent(a.getValue(), null, null, false)); + sb.append(quote); + } + } else { + sb.append(attr.toString()); + } + } + sb.append(node.isSelfClosing() ? "/>" : ">"); + return sb.toString(); + } + + @Override + public String visitEndElement(EndElementTree node, Void p) { + return "</" + node.getName() + ">"; + } + + @Override + public String visitEntity(EntityTree node, Void p) { + return "&" + node.getName() + ";"; + } + + @Override + protected String defaultAction(DocTree node, Void p) { + return node.toString(); + } + }; for (DocTree tree : content) { - sb.append(renderTree(tree)); + sb.append(tree.accept(visitor, null)); + } + // normalized whitespace not relevant for HTML rendering + if (trim) { + String[] lines = sb.toString().split("\n"); + StringBuilder result = new StringBuilder(); + for (String line : lines) { + if (!line.trim().isEmpty()) { + result.append(line); + } + } + return result.toString().trim(); + } else { + return sb.toString(); } - return sb.toString().trim(); } - private String renderTree(DocTree tree) { - switch (tree.getKind()) { - case TEXT: - return ((TextTree) tree).getBody(); - case LINK: - case LINK_PLAIN: - LinkTree link = (LinkTree) tree; - String ref = link.getReference() != null ? link.getReference().getSignature() : ""; - return "{@link " + ref + "}"; - case CODE: - return "{@code " + ((LiteralTree) tree).getBody().getBody() + "}"; - case LITERAL: - return "{@literal " + ((LiteralTree) tree).getBody().getBody() + "}"; - default: - return tree.toString(); - } + private String escapeHtml(String text) { + return text.replace("&", "&").replace("<", "<").replace(">", ">"); } - private String getSince(TypeElement type, DocCommentTree docComment) { - String since = getSinceTag(docComment); + private String getSince(TypeElement type, DocCommentTree docComment, VariableElement fieldContext) { + String since = getSinceTag(docComment, fieldContext); if (since == null && type != null) { // fall back to the enclosing type's @since - since = getSinceTag(docTrees.getDocCommentTree(type)); + since = getSinceTag(docTrees.getDocCommentTree(type), null); } return since; } - private String getSinceTag(DocCommentTree docComment) { + private String getSinceTag(DocCommentTree docComment, VariableElement fieldContext) { if (docComment == null) { return null; } for (DocTree tag : docComment.getBlockTags()) { if (tag instanceof SinceTree sinceTree) { - return renderContent(sinceTree.getBody()); + return renderContent(sinceTree.getBody(), fieldContext, docComment, true); } } return null; @@ -509,69 +616,28 @@ public class ConfigurationCollectorDoclet implements Doclet { private String getConfigurationType(String type) { if (type != null) { - String linkPrefix = "{@link "; - String linkSuffix = "}"; - if (type.startsWith(linkPrefix) && type.endsWith(linkSuffix)) { - type = type.substring(linkPrefix.length(), type.length() - linkSuffix.length()); - } String javaLangPackage = "java.lang."; if (type.startsWith(javaLangPackage)) { type = type.substring(javaLangPackage.length()); } } - return nvl(type, "n/a"); + return Objects.toString(type, "n/a"); } private String getConfigurationSource(String source) { - if ("{@link RepositorySystemSession#getConfigProperties()}".equals(source)) { + if ("<code>RepositorySystemSession#getConfigProperties()</code>".equals(source)) { return "Session Configuration"; - } else if ("{@link System#getProperty(String,String)}".equals(source)) { + } else if ("<code>System#getProperty(String,String)</code>".equals(source)) { return "Java System Properties"; } else { return source; } } - private String cleanseJavadoc(String fullText) { - String[] lines = fullText.split("\n"); - StringBuilder result = new StringBuilder(); - for (String line : lines) { - if (!line.startsWith("@") && !line.trim().isEmpty()) { - result.append(line); - } - } - return cleanseTags(result.toString()); - } - - private String cleanseTags(String text) { - // {@code XXX} -> <code>XXX</code> - // {@link XXX} -> <code>XXX</code> - Pattern pattern = Pattern.compile("(\\{@\\w\\w\\w\\w (.+?)})"); - Matcher matcher = pattern.matcher(text); - if (!matcher.find()) { - return text; - } - int prevEnd = 0; - StringBuilder result = new StringBuilder(); - do { - result.append(text, prevEnd, matcher.start(1)); - result.append("<code>"); - result.append(matcher.group(2)); - result.append("</code>"); - prevEnd = matcher.end(1); - } while (matcher.find()); - result.append(text, prevEnd, text.length()); - return result.toString(); - } - private String toYesNo(String value) { return "yes".equalsIgnoreCase(value) || "true".equalsIgnoreCase(value) ? "Yes" : "No"; } - private String nvl(String string, String def) { - return string == null ? def : string; - } - /** * Minimal {@link Option} implementation. */ diff --git a/maven-resolver-tools/src/test/java/org/eclipse/aether/tools/ConfigurationCollectorDocletTest.java b/maven-resolver-tools/src/test/java/org/eclipse/aether/tools/ConfigurationCollectorDocletTest.java index 238360b08..ba2dace77 100644 --- a/maven-resolver-tools/src/test/java/org/eclipse/aether/tools/ConfigurationCollectorDocletTest.java +++ b/maven-resolver-tools/src/test/java/org/eclipse/aether/tools/ConfigurationCollectorDocletTest.java @@ -78,8 +78,11 @@ class ConfigurationCollectorDocletTest { assertNotNull(string, "string key missing"); assertEquals("String", string.get("configurationType")); assertEquals("\"hello\"", string.get("defaultValue")); - assertEquals("Yes", string.get("supportRepoIdSuffix")); assertEquals("", string.get("since"), "no @since expected"); + assertEquals("Yes", string.get("supportRepoIdSuffix")); + assertEquals( + "A string value with some inline tags. Value <code>\"hello\"</code> is the default. <code>some.property</code> is used. This text is code. <code>This text is literal.</code> <code>java.lang.String</code> is the type.", + string.get("description")); Map<String, String> enumKey = keys.get("sample.enum"); assertNotNull(enumKey, "enum key missing"); diff --git a/maven-resolver-tools/src/test/resources/org/eclipse/aether/sample/SampleConfigurationKeys.java b/maven-resolver-tools/src/test/resources/org/eclipse/aether/sample/SampleConfigurationKeys.java index 573fe3e00..a8c4a591a 100644 --- a/maven-resolver-tools/src/test/resources/org/eclipse/aether/sample/SampleConfigurationKeys.java +++ b/maven-resolver-tools/src/test/resources/org/eclipse/aether/sample/SampleConfigurationKeys.java @@ -38,7 +38,12 @@ public final class SampleConfigurationKeys { public static final boolean DEFAULT_BOOL = true; /** - * A string value. + * A string value with some inline tags. + * Value {@value #DEFAULT_STRING} is the default. + * {@systemProperty some.property} is used. + * {@code This text is code.} + * {@literal This text is literal.} + * {@link java.lang.String} is the type. * * @configurationSource {@link System#getProperty(String,String)} * @configurationType {@link java.lang.String}
