Skip to content

Commit e978f42

Browse files
committed
Add support for markdown doc-comments as described in JEP 467.
1 parent 74d7e0e commit e978f42

File tree

7 files changed

+212
-72
lines changed

7 files changed

+212
-72
lines changed

build.gradle

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ plugins {
44
}
55

66
group = 'org.moddingx'
7-
java.toolchain.languageVersion = JavaLanguageVersion.of(21)
7+
java.toolchain.languageVersion = JavaLanguageVersion.of(24)
88

99
repositories {
1010
mavenCentral()
@@ -14,7 +14,9 @@ dependencies {
1414
implementation 'jakarta.annotation:jakarta.annotation-api:3.0.0'
1515
implementation 'org.apache.commons:commons-text:1.12.0'
1616
implementation 'com.google.code.gson:gson:2.11.0'
17-
implementation 'org.jsoup:jsoup:1.17.2'
17+
implementation 'org.jsoup:jsoup:1.17.2'
18+
implementation 'org.commonmark:commonmark:0.24.0'
19+
implementation 'org.commonmark:commonmark-ext-gfm-tables:0.24.0'
1820
}
1921

2022
task fatjar(type: Jar) {

gradle/wrapper/gradle-wrapper.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
distributionBase=GRADLE_USER_HOME
22
distributionPath=wrapper/dists
3-
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
3+
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip
44
networkTimeout=10000
55
validateDistributionUrl=true
66
zipStoreBase=GRADLE_USER_HOME

src/main/java/org/moddingx/java_doclet_meta/record/DocData.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,12 @@ public static Optional<DocData> from(DocEnv env, Element element) {
3434
DocCommentTree tree = env.docs().getDocCommentTree(elemPath);
3535
if (tree == null) return Optional.empty();
3636
DocTreePath basePath = DocTreePath.getPath(elemPath, tree, tree);
37-
String summary = HtmlConverter.asDocHtml(env, basePath, tree.getFirstSentence());
38-
String text = HtmlConverter.asDocHtml(env, basePath, tree.getFullBody());
37+
String summary = HtmlConverter.asDocHtml(env, element, basePath, tree.getFirstSentence());
38+
String text = HtmlConverter.asDocHtml(env, element, basePath, tree.getFullBody());
3939
List<DocBlockData> properties = tree.getBlockTags().stream()
40-
.flatMap(tag -> DocBlockData.from(env, DocTreePath.getPath(basePath, tag), tag).stream())
40+
.flatMap(tag -> DocBlockData.from(env, element, DocTreePath.getPath(basePath, tag), tag).stream())
4141
.toList();
42-
List<DocBlockData> inlineProperties = DocBlockData.fromInline(env, basePath, properties, tree.getFullBody());
42+
List<DocBlockData> inlineProperties = DocBlockData.fromInline(env, element, basePath, properties, tree.getFullBody());
4343
return Optional.of(new DocData(summary, text, Stream.concat(properties.stream(), inlineProperties.stream()).toList()));
4444
}
4545
}

src/main/java/org/moddingx/java_doclet_meta/record/ParamData.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ private static Optional<String> getParamDoc(DocEnv env, String name, Element ele
4343
for (DocTree block : tree.getBlockTags()) {
4444
if (block.getKind() == DocTree.Kind.PARAM && block instanceof ParamTree pt) {
4545
if (!pt.isTypeParameter() && name.equals(pt.getName().getName().toString())) {
46-
return Optional.of(HtmlConverter.asDocHtml(env, DocTreePath.getPath(basePath, pt), pt.getDescription()));
46+
return Optional.of(HtmlConverter.asDocHtml(env, element, DocTreePath.getPath(basePath, pt), pt.getDescription()));
4747
}
4848
}
4949
}

src/main/java/org/moddingx/java_doclet_meta/record/block/DocBlockData.java

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import org.moddingx.java_doclet_meta.DocEnv;
88
import org.moddingx.java_doclet_meta.util.HtmlConverter;
99

10+
import javax.lang.model.element.Element;
1011
import java.util.*;
1112
import java.util.stream.Collectors;
1213

@@ -23,7 +24,7 @@ default JsonObject json() {
2324

2425
void addProperties(JsonObject json);
2526

26-
static List<DocBlockData> fromInline(DocEnv env, DocTreePath basePath, List<DocBlockData> blocks, List<? extends DocTree> inline) {
27+
static List<DocBlockData> fromInline(DocEnv env, Element context, DocTreePath basePath, List<DocBlockData> blocks, List<? extends DocTree> inline) {
2728
// Inline return tags in the main description should act as separate block tags.
2829
Set<DocBlockData.Type> knownTypes = new HashSet<>(blocks.stream().map(DocBlockData::type).collect(Collectors.toUnmodifiableSet()));
2930
List<DocBlockData> inlineBlocks = new ArrayList<>();
@@ -32,7 +33,7 @@ static List<DocBlockData> fromInline(DocEnv env, DocTreePath basePath, List<DocB
3233
@Override
3334
public Void visitReturn(ReturnTree tree, Void unused) {
3435
if (tree.isInline() && knownTypes.add(Type.RETURN)) {
35-
inlineBlocks.add(new TextBlock(Type.RETURN, HtmlConverter.asDocHtml(env, DocTreePath.getPath(basePath, tree), tree.getDescription())));
36+
inlineBlocks.add(new TextBlock(Type.RETURN, HtmlConverter.asDocHtml(env, context, DocTreePath.getPath(basePath, tree), tree.getDescription())));
3637
}
3738
return super.visitReturn(tree, unused);
3839
}
@@ -42,33 +43,33 @@ public Void visitReturn(ReturnTree tree, Void unused) {
4243
return List.copyOf(inlineBlocks);
4344
}
4445

45-
static Optional<DocBlockData> from(DocEnv env, DocTreePath path, DocTree tree) {
46+
static Optional<DocBlockData> from(DocEnv env, Element context, DocTreePath path, DocTree tree) {
4647
// Ignore parameters, they are merged with ParamData
4748
return Optional.ofNullable(switch (tree.getKind()) {
48-
case AUTHOR -> new TextBlock(Type.AUTHOR, HtmlConverter.asDocHtml(env, path, ((AuthorTree) tree).getName()));
49-
case DEPRECATED -> new TextBlock(Type.DEPRECATED, HtmlConverter.asDocHtml(env, path, ((DeprecatedTree) tree).getBody()));
49+
case AUTHOR -> new TextBlock(Type.AUTHOR, HtmlConverter.asDocHtml(env, context, path, ((AuthorTree) tree).getName()));
50+
case DEPRECATED -> new TextBlock(Type.DEPRECATED, HtmlConverter.asDocHtml(env, context, path, ((DeprecatedTree) tree).getBody()));
5051
case EXCEPTION -> {
5152
ThrowsTree ex = (ThrowsTree) tree;
52-
yield ClassTextBlock.from(env, Type.EXCEPTION, path, ex.getExceptionName(), HtmlConverter.asDocHtml(env, path, ex.getDescription()));
53+
yield ClassTextBlock.from(env, Type.EXCEPTION, path, ex.getExceptionName(), HtmlConverter.asDocHtml(env, context, path, ex.getDescription()));
5354
}
5455
case THROWS -> {
5556
ThrowsTree ex = (ThrowsTree) tree;
56-
yield ClassTextBlock.from(env, Type.THROWS, path, ex.getExceptionName(), HtmlConverter.asDocHtml(env, path, ex.getDescription()));
57+
yield ClassTextBlock.from(env, Type.THROWS, path, ex.getExceptionName(), HtmlConverter.asDocHtml(env, context, path, ex.getDescription()));
5758
}
5859
case PROVIDES -> {
5960
ProvidesTree provides = (ProvidesTree) tree;
60-
yield ClassTextBlock.from(env, Type.PROVIDES, path, provides.getServiceType(), HtmlConverter.asDocHtml(env, path, provides.getDescription()));
61+
yield ClassTextBlock.from(env, Type.PROVIDES, path, provides.getServiceType(), HtmlConverter.asDocHtml(env, context, path, provides.getDescription()));
6162

6263
}
6364
case USES -> {
6465
UsesTree provides = (UsesTree) tree;
65-
yield ClassTextBlock.from(env, Type.USES, path, provides.getServiceType(), HtmlConverter.asDocHtml(env, path, provides.getDescription()));
66+
yield ClassTextBlock.from(env, Type.USES, path, provides.getServiceType(), HtmlConverter.asDocHtml(env, context, path, provides.getDescription()));
6667

6768
}
68-
case RETURN -> new TextBlock(Type.RETURN, HtmlConverter.asDocHtml(env, path, ((ReturnTree) tree).getDescription()));
69-
case SERIAL -> new TextBlock(Type.SERIAL, HtmlConverter.asDocHtml(env, path, ((SerialTree) tree).getDescription()));
70-
case SINCE -> new TextBlock(Type.SINCE, HtmlConverter.asDocHtml(env, path, ((SinceTree) tree).getBody()));
71-
case UNKNOWN_BLOCK_TAG -> new TextBlock(Type.UNKNOWN, HtmlConverter.asDocHtml(env, path, ((UnknownBlockTagTree) tree).getContent()));
69+
case RETURN -> new TextBlock(Type.RETURN, HtmlConverter.asDocHtml(env, context, path, ((ReturnTree) tree).getDescription()));
70+
case SERIAL -> new TextBlock(Type.SERIAL, HtmlConverter.asDocHtml(env, context, path, ((SerialTree) tree).getDescription()));
71+
case SINCE -> new TextBlock(Type.SINCE, HtmlConverter.asDocHtml(env, context, path, ((SinceTree) tree).getBody()));
72+
case UNKNOWN_BLOCK_TAG -> new TextBlock(Type.UNKNOWN, HtmlConverter.asDocHtml(env, context, path, ((UnknownBlockTagTree) tree).getContent()));
7273
default -> null;
7374
});
7475
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package org.moddingx.java_doclet_meta.util;
2+
3+
import com.sun.source.doctree.DocTree;
4+
import com.sun.source.doctree.RawTextTree;
5+
import org.commonmark.ext.gfm.tables.TablesExtension;
6+
import org.commonmark.node.AbstractVisitor;
7+
import org.commonmark.node.Heading;
8+
import org.commonmark.node.Node;
9+
import org.commonmark.parser.Parser;
10+
import org.commonmark.renderer.html.HtmlRenderer;
11+
import org.jsoup.Jsoup;
12+
import org.jsoup.nodes.Document;
13+
import org.jsoup.nodes.Entities;
14+
15+
import javax.lang.model.element.Element;
16+
import javax.lang.model.element.ElementKind;
17+
import java.nio.charset.StandardCharsets;
18+
import java.util.ArrayList;
19+
import java.util.Collections;
20+
import java.util.List;
21+
import java.util.regex.Matcher;
22+
import java.util.regex.Pattern;
23+
24+
public class DocTreePreprocessor {
25+
26+
// See jdk.javadoc.internal.doclets.formats.html.HtmlDocletWriter$MarkdownHandler
27+
private static final Parser PARSER = Parser.builder().extensions(List.of(TablesExtension.create())).build();
28+
private static final HtmlRenderer RENDERER = HtmlRenderer.builder().omitSingleParagraphP(true).extensions(List.of(TablesExtension.create())).build();
29+
private static final Pattern REPLACEMENT_PATTERN = Pattern.compile("<!--\uFFFC(\\d+)-->");
30+
31+
public static List<ProcessedDocTree> process(Element context, List<? extends DocTree> textElems) {
32+
if (textElems.stream().noneMatch(tree -> tree.getKind() == DocTree.Kind.MARKDOWN)) {
33+
return textElems.stream().<ProcessedDocTree>map(WrappedDocTree::new).toList();
34+
}
35+
StringBuilder markdown = new StringBuilder();
36+
List<ProcessedDocTree> replacements = new ArrayList<>();
37+
for (DocTree tree : textElems) {
38+
if (tree.getKind() == DocTree.Kind.MARKDOWN) {
39+
String markdownContent = ((RawTextTree) tree).getContent();
40+
Matcher m = REPLACEMENT_PATTERN.matcher(markdownContent);
41+
int start = 0;
42+
while (m.find()) {
43+
markdown.append(markdownContent, start, m.start());
44+
int replacementIdx = replacements.size();
45+
replacements.add(new RawHtml(m.group()));
46+
markdown.append("<!--\uFFFC").append(replacementIdx).append("-->");
47+
start = m.end();
48+
}
49+
markdown.append(markdownContent.substring(start));
50+
} else {
51+
int replacementIdx = replacements.size();
52+
replacements.add(new WrappedDocTree(tree));
53+
markdown.append("<!--\uFFFC").append(replacementIdx).append("-->");
54+
}
55+
}
56+
57+
Node node = PARSER.parse(markdown.toString());
58+
adjustHeadings(context, node);
59+
String htmlText = minifyHtml(RENDERER.render(node));
60+
return replaceElements(htmlText, Collections.unmodifiableList(replacements));
61+
}
62+
63+
private static void adjustHeadings(Element context, Node markdown) {
64+
// See jdk.javadoc.internal.doclets.formats.html.HtmlDocletWriter$MarkdownHandler$HeadingNodeRenderer
65+
ElementKind kind = context.getKind();
66+
int headingInset = kind.isField() || kind.isExecutable() ? 3 : kind != ElementKind.OTHER ? 1 : 0;
67+
68+
markdown.accept(new AbstractVisitor() {
69+
70+
@Override
71+
public void visit(Heading heading) {
72+
heading.setLevel(Math.min(heading.getLevel() + headingInset, 6));
73+
super.visit(heading);
74+
}
75+
});
76+
}
77+
78+
private static String minifyHtml(String html) {
79+
Document document = Jsoup.parseBodyFragment(html);
80+
document.outputSettings(new Document.OutputSettings()
81+
.syntax(Document.OutputSettings.Syntax.html)
82+
.escapeMode(Entities.EscapeMode.base)
83+
.charset(StandardCharsets.UTF_8)
84+
.prettyPrint(true)
85+
.indentAmount(0)
86+
.maxPaddingWidth(-1)
87+
.outline(false)
88+
);
89+
return document.body().html().strip();
90+
}
91+
92+
private static List<ProcessedDocTree> replaceElements(String htmlText, List<ProcessedDocTree> replacements) {
93+
List<ProcessedDocTree> replacedText = new ArrayList<>(2 * replacements.size() + 1);
94+
Matcher m = REPLACEMENT_PATTERN.matcher(htmlText);
95+
int start = 0;
96+
while (m.find()) {
97+
replacedText.add(new RawHtml(htmlText.substring(start, m.start())));
98+
int replacementIdx = -1;
99+
try {
100+
replacementIdx = Integer.parseInt(m.group(1));
101+
} catch (NumberFormatException e) {
102+
//
103+
}
104+
if (replacementIdx >= 0 && replacementIdx < replacements.size()) {
105+
replacedText.add(replacements.get(replacementIdx));
106+
}
107+
start = m.end();
108+
}
109+
replacedText.add(new RawHtml(htmlText.substring(start)));
110+
111+
List<ProcessedDocTree> result = new ArrayList<>(replacedText.size());
112+
for (ProcessedDocTree tree : replacedText) {
113+
if (tree instanceof RawHtml(String html) && html.isEmpty()) continue;
114+
if (tree instanceof RawHtml(String html2) && !result.isEmpty() && result.getLast() instanceof RawHtml(String html1)) {
115+
result.set(result.size() - 1, new RawHtml(html1 + html2));
116+
} else {
117+
result.add(tree);
118+
}
119+
}
120+
return List.copyOf(result);
121+
}
122+
123+
public sealed interface ProcessedDocTree permits RawHtml, WrappedDocTree {}
124+
public record RawHtml(String html) implements ProcessedDocTree {}
125+
public record WrappedDocTree(DocTree tree) implements ProcessedDocTree {}
126+
}

0 commit comments

Comments
 (0)