diff --git a/.gitignore b/.gitignore index 2e7f557d3..ec436e630 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # gradle +.kotlin/ .gradle/ build/ out/ @@ -42,5 +43,10 @@ docs/web/[0-9]* *.af~lock~ *.af~lock~:com.dropbox.ignored +# Vitepress docgen +node_modules/ +**/.vitepress/cache/ +**/.vitepress/dist/ + # General working dir for anything that is needed during development working/ diff --git a/build.gradle.kts b/build.gradle.kts index ddbbe9273..522f206f9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -90,6 +90,14 @@ val distDirFile = distDir.asFile val docsBuildDir = layout.buildDirectory.dir("docs").get().asFile val docletJarFile = layout.projectDirectory.file("buildSrc/build/libs/buildSrc.jar").asFile +repositories { + mavenCentral() +} + +val docletClasspath = configurations.detachedConfiguration( + dependencies.create("com.google.code.gson:gson:2.9.0") +) + // Root-level properties (available in root gradle.properties) val modIdProvider = providers.gradleProperty("mod_id") val channelProvider = providers.gradleProperty("channel").orElse("release") @@ -222,8 +230,8 @@ if (isVersionedProject && hasMinecraftVersion) { classpath = documentationClasspath dependsOn(minecraftArtifactTasks) destinationDir = File(docsBuildDir, "python/JsMacrosAC") - options.doclet = "com.jsmacrosce.doclet.pydoclet.Main" - options.docletpath = mutableListOf(docletJarFile) + options.doclet = "com.jsmacrosce.doclet.core.pydoclet.Main" + options.docletpath = (listOf(docletJarFile) + docletClasspath.files).toMutableList() (options as CoreJavadocOptions).addStringOption("v", project.version.toString()) } @@ -242,8 +250,8 @@ if (isVersionedProject && hasMinecraftVersion) { classpath = documentationClasspath dependsOn(minecraftArtifactTasks) destinationDir = File(docsBuildDir, "typescript/headers") - options.doclet = "com.jsmacrosce.doclet.tsdoclet.Main" - options.docletpath = mutableListOf(docletJarFile) + options.doclet = "com.jsmacrosce.doclet.core.tsdoclet.Main" + options.docletpath = (listOf(docletJarFile) + docletClasspath.files).toMutableList() (options as CoreJavadocOptions).addStringOption("v", project.version.toString()) } @@ -262,14 +270,14 @@ if (isVersionedProject && hasMinecraftVersion) { classpath = documentationClasspath dependsOn(minecraftArtifactTasks) destinationDir = File(docsBuildDir, "web") - options.doclet = "com.jsmacrosce.doclet.webdoclet.Main" - options.docletpath = mutableListOf(docletJarFile) + options.doclet = "com.jsmacrosce.doclet.core.webdoclet.Main" + options.docletpath = (listOf(docletJarFile) + docletClasspath.files).toMutableList() (options as CoreJavadocOptions).addStringOption("v", project.version.toString()) (options as CoreJavadocOptions).addStringOption("mcv", mcVersion) (options as StandardJavadocDocletOptions).links( "https://docs.oracle.com/javase/8/docs/api/", - "https://www.javadoc.io/doc/org.slf4j/slf4j-api/1.7.30/", - "https://javadoc.io/doc/com.neovisionaries/nv-websocket-client/latest/" + "https://www.slf4j.org/apidocs-2.0.17/", + "https://takahikokawasaki.github.io/nv-websocket-client/" ) } @@ -285,10 +293,41 @@ if (isVersionedProject && hasMinecraftVersion) { } } + tasks.register("generateVitepressDoc", Javadoc::class.java) { + group = "documentation" + description = "Generates the vitepress documentation for the project" + source(documentationSources) + classpath = documentationClasspath + dependsOn(minecraftArtifactTasks) + destinationDir = File(docsBuildDir, "vitepress") + options.doclet = "com.jsmacrosce.doclet.core.mddoclet.Main" + options.docletpath = (listOf(docletJarFile) + docletClasspath.files).toMutableList() + (options as CoreJavadocOptions).addStringOption("v", project.version.toString()) + (options as CoreJavadocOptions).addStringOption("mcv", mcVersion) + (options as StandardJavadocDocletOptions).links( + "https://docs.oracle.com/javase/8/docs/api/", + "https://www.slf4j.org/apidocs-2.0.17/", + "https://takahikokawasaki.github.io/nv-websocket-client/", + "https://www.graalvm.org/sdk/javadoc/" + ) + } + + tasks.register("copyVitepressDoc", Copy::class.java) { + group = "documentation" + description = "Copies the vitepress documentation to the build folder" + dependsOn("generateVitepressDoc") + from(rootProject.file("docs/vitepress")) + into(File(docsBuildDir, "vitepress")) + inputs.property("version", project.version.toString()) + filesMatching("index.md") { + expand(mapOf("version" to project.version.toString())) + } + } + tasks.register("createDistDocs", Copy::class.java) { group = "distribution" description = "Packages generated documentation into the dist directory" - dependsOn("prepareDist", "copyPyDoc", "copyTSDoc", "copyWebDoc") + dependsOn("prepareDist", "copyPyDoc", "copyTSDoc", "copyWebDoc", "copyVitepressDoc") from(docsBuildDir) into(distDirFile) } diff --git a/buildSrc/src/main/java/com/jsmacrosce/FileHandler.java b/buildSrc/src/main/java/com/jsmacrosce/FileHandler.java index 39cbc6f6c..debd9ace5 100644 --- a/buildSrc/src/main/java/com/jsmacrosce/FileHandler.java +++ b/buildSrc/src/main/java/com/jsmacrosce/FileHandler.java @@ -1,11 +1,14 @@ package com.jsmacrosce; +import com.jsmacrosce.doclet.DocletIgnore; + import java.io.*; /** * @author Wagyourtail * @since 1.1.8 */ +@DocletIgnore public class FileHandler { private final File f; @@ -53,15 +56,15 @@ public FileHandler write(byte[] b) throws IOException { * @since 1.1.8 */ public String read() throws IOException { - String ret = ""; + StringBuilder ret = new StringBuilder(); try (BufferedReader in = new BufferedReader(new FileReader(f))) { String line = in.readLine(); while (line != null) { - ret += line + "\n"; + ret.append(line).append("\n"); line = in.readLine(); } } - return ret; + return ret.toString(); } /** diff --git a/buildSrc/src/main/java/com/jsmacrosce/MarkdownBuilder.java b/buildSrc/src/main/java/com/jsmacrosce/MarkdownBuilder.java new file mode 100644 index 000000000..45a16b884 --- /dev/null +++ b/buildSrc/src/main/java/com/jsmacrosce/MarkdownBuilder.java @@ -0,0 +1,232 @@ +package com.jsmacrosce; + +import com.jsmacrosce.doclet.DocletIgnore; + +import java.util.Map; + +/** + * Fluent builder for Markdown documents with VitePress-specific constructs. + * + *

Block-level methods automatically manage blank lines between elements. + * Consecutive bullet items (and items that immediately follow a + * {@link #boldHeader}) are not separated by blank lines; a blank line is + * inserted before any other block element that follows a list. + * + *

Inline helpers ({@link #codeSpan} and {@link #link}) are static so they + * can be used in contexts where only a {@code String} is needed, such as + * inside {@code MarkdownWriter} signatures. + */ +@DocletIgnore +public class MarkdownBuilder { + + /** Tracks what the last emitted element was, to decide separators. */ + private enum State { + /** Nothing has been emitted yet. */ + START, + /** Last emitted element was a regular block (heading, paragraph, …). */ + BLOCK, + /** + * Last emitted element was a list item or a bold-header (a bold label + * immediately followed by list items, e.g. {@code **Parameters:**}). + * Consecutive list elements never receive a blank-line separator. + */ + LIST + } + + private final StringBuilder sb = new StringBuilder(); + private State state = State.START; + + // ----------------------------------------------------------------------- + // Inline helpers (static — no state, return String) + // ----------------------------------------------------------------------- + + /** + * Wraps {@code text} in backtick fences, choosing a fence length that does + * not conflict with any backtick run already inside {@code text}. + * + * @param text the raw text content; must not be {@code null} + * @return a markdown inline code span + */ + public static String codeSpan(String text) { + if (text == null) { + return ""; + } + int maxTicks = 0; + int current = 0; + for (int i = 0; i < text.length(); i++) { + if (text.charAt(i) == '`') { + current++; + if (current > maxTicks) { + maxTicks = current; + } + } else { + current = 0; + } + } + String fence = "`".repeat(maxTicks + 1); + return fence + text + fence; + } + + /** + * Returns a markdown inline link: {@code [text](url)}. + * + * @param text the link label + * @param url the link target + * @return a markdown link string + */ + public static String link(String text, String url) { + return "[" + text + "](" + url + ")"; + } + + // ----------------------------------------------------------------------- + // Block-level methods + // ----------------------------------------------------------------------- + + /** + * Emits a YAML frontmatter block at the current position. + * + *

Entries are written in insertion order. Values are emitted verbatim + * (no quoting is added by this method). + * + * @param entries key-value pairs for the frontmatter + * @return {@code this} + */ + public MarkdownBuilder frontmatter(Map entries) { + beginBlock(); + sb.append("---\n"); + for (Map.Entry entry : entries.entrySet()) { + sb.append(entry.getKey()).append(": ").append(entry.getValue()).append("\n"); + } + sb.append("---\n"); + state = State.BLOCK; + return this; + } + + /** + * Emits {@code # … ## … ### …} depending on {@code level}. + * + * @param level ATX heading level (1–6) + * @param text heading text (not escaped by this method) + * @return {@code this} + */ + public MarkdownBuilder heading(int level, String text) { + beginBlock(); + sb.append("#".repeat(level)).append(" ").append(text).append("\n"); + state = State.BLOCK; + return this; + } + + /** + * Emits a heading with a VitePress custom anchor: {@code ## text {#anchor}}. + * + * @param level ATX heading level (1–6) + * @param text heading text + * @param anchor anchor identifier (without the leading {@code #}) + * @return {@code this} + */ + public MarkdownBuilder heading(int level, String text, String anchor) { + beginBlock(); + sb.append("#".repeat(level)).append(" ").append(text) + .append(" {#").append(anchor).append("}\n"); + state = State.BLOCK; + return this; + } + + /** + * Emits a paragraph block. The text is appended verbatim followed by a + * single newline; the blank-line separator before the paragraph is managed + * automatically. + * + * @param text paragraph content (may include inline markdown) + * @return {@code this} + */ + public MarkdownBuilder paragraph(String text) { + if (text == null || text.isEmpty()) { + return this; + } + beginBlock(); + sb.append(text).append("\n"); + state = State.BLOCK; + return this; + } + + /** + * Emits a bold "label" immediately followed by list items. + * + *

Example output: {@code **Parameters:**\n}. No blank line is inserted + * between this header and subsequent {@link #bulletItem} calls. + * + * @param label the bold label (without the {@code **} delimiters) + * @return {@code this} + */ + public MarkdownBuilder boldHeader(String label) { + beginBlock(); + sb.append("**").append(label).append("**\n"); + state = State.LIST; + return this; + } + + /** + * Emits a list item ({@code - text\n}). + * + *

Consecutive bullet items are never separated by blank lines. A blank + * line is inserted before the first bullet item when it follows a + * non-list block (i.e. when the preceding state is {@link State#BLOCK}). + * + * @param text list-item content (may include inline markdown) + * @return {@code this} + */ + public MarkdownBuilder bulletItem(String text) { + if (state == State.BLOCK) { + sb.append("\n"); + } + sb.append("- ").append(text).append("\n"); + state = State.LIST; + return this; + } + + /** + * Appends {@code text} verbatim without inserting any separator or + * modifying the current state. Use this for pre-formatted content whose + * structural role is best managed by the caller. + * + * @param text raw text to append + * @return {@code this} + */ + public MarkdownBuilder raw(String text) { + sb.append(text); + return this; + } + + // ----------------------------------------------------------------------- + // Rendering + // ----------------------------------------------------------------------- + + @Override + public String toString() { + return sb.toString(); + } + + // ----------------------------------------------------------------------- + // Internal helpers + // ----------------------------------------------------------------------- + + /** + * Inserts a blank line before the upcoming block element when needed. + * + *

A blank line is inserted in two situations: + *

+ * In the initial {@link State#START} state nothing is emitted. + */ + private void beginBlock() { + if (state != State.START) { + sb.append("\n"); + } + } +} diff --git a/buildSrc/src/main/java/com/jsmacrosce/Pair.java b/buildSrc/src/main/java/com/jsmacrosce/Pair.java index e1bbeff71..24c1a3455 100644 --- a/buildSrc/src/main/java/com/jsmacrosce/Pair.java +++ b/buildSrc/src/main/java/com/jsmacrosce/Pair.java @@ -1,5 +1,8 @@ package com.jsmacrosce; +import com.jsmacrosce.doclet.DocletIgnore; + +@DocletIgnore public class Pair { T key; U value; diff --git a/buildSrc/src/main/java/com/jsmacrosce/StringHelpers.java b/buildSrc/src/main/java/com/jsmacrosce/StringHelpers.java index ae0e40127..25af95280 100644 --- a/buildSrc/src/main/java/com/jsmacrosce/StringHelpers.java +++ b/buildSrc/src/main/java/com/jsmacrosce/StringHelpers.java @@ -1,6 +1,9 @@ package com.jsmacrosce; +import com.jsmacrosce.doclet.DocletIgnore; + @SuppressWarnings("unused") +@DocletIgnore public class StringHelpers { public static String toSymbolsGTLT(String s) { diff --git a/buildSrc/src/main/java/com/jsmacrosce/XMLBuilder.java b/buildSrc/src/main/java/com/jsmacrosce/XMLBuilder.java index 6cbace89e..d984cb5f1 100644 --- a/buildSrc/src/main/java/com/jsmacrosce/XMLBuilder.java +++ b/buildSrc/src/main/java/com/jsmacrosce/XMLBuilder.java @@ -1,7 +1,10 @@ package com.jsmacrosce; +import com.jsmacrosce.doclet.DocletIgnore; + import java.util.*; +@DocletIgnore public class XMLBuilder { public final Map options = new HashMap<>(); public final List children = new LinkedList<>(); diff --git a/buildSrc/src/main/java/com/jsmacrosce/doclet/DocletCategory.java b/buildSrc/src/main/java/com/jsmacrosce/doclet/DocletCategory.java new file mode 100644 index 000000000..c5c93623f --- /dev/null +++ b/buildSrc/src/main/java/com/jsmacrosce/doclet/DocletCategory.java @@ -0,0 +1,18 @@ +package com.jsmacrosce.doclet; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@DocletIgnore +@Documented +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.TYPE) +public @interface DocletCategory { + /** + * The category name used for organizing classes or events. + */ + String value(); +} diff --git a/buildSrc/src/main/java/com/jsmacrosce/doclet/DocletDeclareType.java b/buildSrc/src/main/java/com/jsmacrosce/doclet/DocletDeclareType.java index 473f15f70..fadac491a 100644 --- a/buildSrc/src/main/java/com/jsmacrosce/doclet/DocletDeclareType.java +++ b/buildSrc/src/main/java/com/jsmacrosce/doclet/DocletDeclareType.java @@ -2,6 +2,7 @@ import java.lang.annotation.*; +@DocletIgnore @Documented @Retention(RetentionPolicy.SOURCE) @Target({ElementType.METHOD, ElementType.FIELD, ElementType.TYPE}) diff --git a/buildSrc/src/main/java/com/jsmacrosce/doclet/DocletIgnore.java b/buildSrc/src/main/java/com/jsmacrosce/doclet/DocletIgnore.java index a6caeebb7..43c0a47bf 100644 --- a/buildSrc/src/main/java/com/jsmacrosce/doclet/DocletIgnore.java +++ b/buildSrc/src/main/java/com/jsmacrosce/doclet/DocletIgnore.java @@ -4,6 +4,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +@DocletIgnore @Documented @Retention(RetentionPolicy.SOURCE) public @interface DocletIgnore { diff --git a/buildSrc/src/main/java/com/jsmacrosce/doclet/DocletReplaceParams.java b/buildSrc/src/main/java/com/jsmacrosce/doclet/DocletReplaceParams.java index 58b0d05f4..6b6bf3499 100644 --- a/buildSrc/src/main/java/com/jsmacrosce/doclet/DocletReplaceParams.java +++ b/buildSrc/src/main/java/com/jsmacrosce/doclet/DocletReplaceParams.java @@ -2,6 +2,7 @@ import java.lang.annotation.*; +@DocletIgnore @Documented @Retention(RetentionPolicy.SOURCE) @Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.CONSTRUCTOR}) diff --git a/buildSrc/src/main/java/com/jsmacrosce/doclet/DocletReplaceReturn.java b/buildSrc/src/main/java/com/jsmacrosce/doclet/DocletReplaceReturn.java index c5cc52119..e927384e5 100644 --- a/buildSrc/src/main/java/com/jsmacrosce/doclet/DocletReplaceReturn.java +++ b/buildSrc/src/main/java/com/jsmacrosce/doclet/DocletReplaceReturn.java @@ -4,6 +4,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +@DocletIgnore @Documented @Retention(RetentionPolicy.SOURCE) public @interface DocletReplaceReturn { diff --git a/buildSrc/src/main/java/com/jsmacrosce/doclet/DocletReplaceTypeParams.java b/buildSrc/src/main/java/com/jsmacrosce/doclet/DocletReplaceTypeParams.java index 70ce66b1b..4fb750550 100644 --- a/buildSrc/src/main/java/com/jsmacrosce/doclet/DocletReplaceTypeParams.java +++ b/buildSrc/src/main/java/com/jsmacrosce/doclet/DocletReplaceTypeParams.java @@ -2,6 +2,7 @@ import java.lang.annotation.*; +@DocletIgnore @Documented @Retention(RetentionPolicy.SOURCE) @Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.CONSTRUCTOR}) diff --git a/buildSrc/src/main/java/com/jsmacrosce/doclet/core/BasicDocCommentParser.java b/buildSrc/src/main/java/com/jsmacrosce/doclet/core/BasicDocCommentParser.java new file mode 100644 index 000000000..0e87ffc4e --- /dev/null +++ b/buildSrc/src/main/java/com/jsmacrosce/doclet/core/BasicDocCommentParser.java @@ -0,0 +1,143 @@ +package com.jsmacrosce.doclet.core; + +import com.jsmacrosce.doclet.DocletIgnore; +import com.sun.source.doctree.DocCommentTree; +import com.sun.source.doctree.DocTree; +import com.sun.source.doctree.LinkTree; +import com.sun.source.doctree.LiteralTree; +import com.sun.source.doctree.ParamTree; +import com.sun.source.doctree.ReferenceTree; +import com.sun.source.doctree.ReturnTree; +import com.sun.source.doctree.SeeTree; +import com.sun.source.doctree.SinceTree; +import com.sun.source.doctree.TextTree; +import com.sun.source.doctree.DeprecatedTree; +import com.sun.source.util.DocTrees; +import com.jsmacrosce.doclet.core.model.DocBodyNode; +import com.jsmacrosce.doclet.core.model.DocComment; +import com.jsmacrosce.doclet.core.model.DocTag; +import com.jsmacrosce.doclet.core.model.DocTagKind; + +import javax.lang.model.element.Element; +import java.util.ArrayList; +import java.util.List; + +@DocletIgnore +public class BasicDocCommentParser implements DocCommentParser { + private final DocTrees docTrees; + + public BasicDocCommentParser(DocTrees docTrees) { + this.docTrees = docTrees; + } + + @Override + public DocComment parse(Element element) { + DocCommentTree tree = docTrees.getDocCommentTree(element); + boolean isDeprecated = element.getAnnotation(Deprecated.class) != null; + if (tree == null) { + if (isDeprecated) { + return new DocComment( + List.of(), + List.of(), + List.of(new DocTag(DocTagKind.DEPRECATED, null, List.of())) + ); + } + return new DocComment(List.of(), List.of(), List.of()); + } + + List summary = parseNodes(tree.getFirstSentence()); + List body = parseNodes(tree.getFullBody()); + List tags = new ArrayList<>(); + + for (DocTree tag : tree.getBlockTags()) { + switch (tag.getKind()) { + case PARAM -> { + ParamTree param = (ParamTree) tag; + DocTagKind kind = param.isTypeParameter() ? DocTagKind.TEMPLATE : DocTagKind.PARAM; + tags.add(new DocTag(kind, param.getName().getName().toString(), parseNodes(param.getDescription()))); + } + case RETURN -> { + ReturnTree ret = (ReturnTree) tag; + tags.add(new DocTag(DocTagKind.RETURN, null, parseNodes(ret.getDescription()))); + } + case SINCE -> { + SinceTree since = (SinceTree) tag; + tags.add(new DocTag(DocTagKind.SINCE, null, parseNodes(since.getBody()))); + } + case DEPRECATED -> { + DeprecatedTree dep = (DeprecatedTree) tag; + tags.add(new DocTag(DocTagKind.DEPRECATED, null, parseNodes(dep.getBody()))); + } + case SEE -> { + SeeTree see = (SeeTree) tag; + for (DocTree ref : see.getReference()) { + if (ref.getKind() == DocTree.Kind.REFERENCE) { + String signature = ((ReferenceTree) ref).getSignature(); + // @see signatures are already just reference strings — wrap as a Link node + // with no label so renderers can format them consistently with {@link}. + tags.add(new DocTag(DocTagKind.SEE, null, List.of(new DocBodyNode.Link(signature, null)))); + } else { + tags.add(new DocTag(DocTagKind.SEE, null, List.of(new DocBodyNode.Html(ref.toString())))); + } + } + } + default -> tags.add(new DocTag(DocTagKind.OTHER, null, List.of(new DocBodyNode.Html(tag.toString())))); + } + } + + if (isDeprecated && tags.stream().noneMatch(tag -> tag.kind() == DocTagKind.DEPRECATED)) { + tags.add(new DocTag(DocTagKind.DEPRECATED, null, List.of())); + } + + return new DocComment(summary, body, tags); + } + + /** + * Converts a list of {@link DocTree} nodes from the javadoc AST into a + * structured {@link List} of {@link DocBodyNode} values. + * + *

The javadoc AST is preserved rather than collapsed to a raw string so + * that each renderer can format links, code spans, and HTML in its own + * target-specific way without regex re-parsing. + */ + private List parseNodes(List trees) { + List nodes = new ArrayList<>(); + for (DocTree tree : trees) { + switch (tree.getKind()) { + case TEXT -> nodes.add(new DocBodyNode.Text(((TextTree) tree).getBody())); + case CODE, LITERAL -> { + // {@code ...} and {@literal ...} both produce inline code spans. + // LiteralTree.getBody() returns an ErroneousTree or TextTree; toString() gives + // the raw text, but getBody().toString() is cleaner. + nodes.add(new DocBodyNode.Code(((LiteralTree) tree).getBody().toString())); + } + case LINK, LINK_PLAIN -> { + LinkTree link = (LinkTree) tree; + String signature = link.getReference().getSignature(); + // Flatten label nodes to plain text (no nested links possible in a label). + String label = flattenToText(link.getLabel()); + nodes.add(new DocBodyNode.Link(signature, label.isBlank() ? null : label)); + } + default -> nodes.add(new DocBodyNode.Html(tree.toString())); + } + } + return nodes; + } + + /** + * Flattens a list of {@link DocTree} nodes to a plain-text string. + * Used only for extracting the label portion of a {@code {@link}} tag + * (which cannot itself contain nested links). + */ + private String flattenToText(List trees) { + StringBuilder sb = new StringBuilder(); + for (DocTree tree : trees) { + switch (tree.getKind()) { + case TEXT -> sb.append(((TextTree) tree).getBody()); + case CODE, LITERAL -> sb.append(((LiteralTree) tree).getBody()); + default -> sb.append(tree.toString()); + } + } + return sb.toString().trim(); + } +} diff --git a/buildSrc/src/main/java/com/jsmacrosce/doclet/core/BasicTypeResolver.java b/buildSrc/src/main/java/com/jsmacrosce/doclet/core/BasicTypeResolver.java new file mode 100644 index 000000000..9222f12a6 --- /dev/null +++ b/buildSrc/src/main/java/com/jsmacrosce/doclet/core/BasicTypeResolver.java @@ -0,0 +1,387 @@ +package com.jsmacrosce.doclet.core; + +import com.jsmacrosce.doclet.DocletIgnore; +import com.jsmacrosce.doclet.core.model.TypeKind; +import com.jsmacrosce.doclet.core.model.TypeRef; + +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.PackageElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.ArrayType; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.IntersectionType; +import javax.lang.model.type.NoType; +import javax.lang.model.type.PrimitiveType; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVariable; +import javax.lang.model.type.UnionType; +import javax.lang.model.type.WildcardType; +import java.util.ArrayList; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +@DocletIgnore +public class BasicTypeResolver implements TypeResolver { + private static final Set JAVA_ALIASES = Set.of( + "java.lang.Array", + "java.lang.Class", + "java.util.Collection", + "java.util.List", + "java.util.Map", + "java.util.Set" + ); + private static final Map JAVA_NUMBER_TYPES = Map.of( + "java.lang.Integer", "int", + "java.lang.Float", "float", + "java.lang.Long", "long", + "java.lang.Short", "short", + "java.lang.Character", "char", + "java.lang.Byte", "byte", + "java.lang.Double", "double", + "java.lang.Number", "number" + ); + private static final Map FUNCTIONAL_INTERFACES = Map.of( + "java.util.function.Consumer", "MethodWrapper<$0>", + "java.util.function.BiConsumer", "MethodWrapper<$0, $1>", + "java.util.function.Function", "MethodWrapper<$0, any, $1>", + "java.util.function.BiFunction", "MethodWrapper<$0, $1, $2>", + "java.util.function.Predicate", "MethodWrapper<$0, any, boolean>", + "java.util.function.BiPredicate", "MethodWrapper<$0, $1, boolean>", + "java.util.function.Supplier", "MethodWrapper", + "java.util.Comparator", "MethodWrapper<$0, $0, int>", + "java.lang.Runnable", "MethodWrapper" + ); + + private Map typeScriptAliases = Map.of(); + private boolean pythonAliasEnabled = true; + private final Set resolving = Collections.newSetFromMap(new IdentityHashMap<>()); + + public void setTypeScriptAliases(Map typeScriptAliases) { + this.typeScriptAliases = typeScriptAliases == null ? Map.of() : typeScriptAliases; + } + + public void setPythonAliasEnabled(boolean pythonAliasEnabled) { + this.pythonAliasEnabled = pythonAliasEnabled; + } + + @Override + public TypeRef resolve(TypeMirror type) { + if (type == null) { + return new TypeRef(TypeKind.UNKNOWN, "unknown", "unknown", List.of(), false, false, null, false); + } + if (!resolving.add(type)) { + // Handle self-referential type parameters (e.g., B extends SomeClass) + // by returning a TYPEVAR without bounds to avoid infinite recursion + if (type.getKind() == javax.lang.model.type.TypeKind.TYPEVAR) { + TypeVariable typeVar = (TypeVariable) type; + String name = typeVar.asElement().getSimpleName().toString(); + return new TypeRef(TypeKind.TYPEVAR, name, name, List.of(), false, false, null, false); + } + return new TypeRef(TypeKind.UNKNOWN, type.toString(), type.toString(), List.of(), false, false, null, false); + } + try { + return switch (type.getKind()) { + case BOOLEAN, BYTE, SHORT, INT, LONG, CHAR, FLOAT, DOUBLE -> + new TypeRef(TypeKind.PRIMITIVE, type.toString(), type.toString(), List.of(), false, false, null, false); + case VOID, NONE -> new TypeRef(TypeKind.VOID, "void", "void", List.of(), false, false, null, false); + case ARRAY -> { + ArrayType arrayType = (ArrayType) type; + TypeRef component = resolve(arrayType.getComponentType()); + yield new TypeRef(TypeKind.ARRAY, component.name(), component.qualifiedName(), List.of(component), true, false, null, false); + } + case DECLARED -> resolveDeclared((DeclaredType) type); + case TYPEVAR -> resolveTypeVar((TypeVariable) type); + case WILDCARD -> resolveWildcard((WildcardType) type); + case INTERSECTION -> resolveIntersection((IntersectionType) type); + case UNION -> resolveUnion((UnionType) type); + default -> new TypeRef(TypeKind.UNKNOWN, type.toString(), type.toString(), List.of(), false, false, null, false); + }; + } finally { + resolving.remove(type); + } + } + + private TypeRef resolveDeclared(DeclaredType type) { + Element el = type.asElement(); + String qualifiedName = getQualifiedName(el); + String displayName = getDisplayName(el); + List params = new ArrayList<>(); + for (TypeMirror arg : type.getTypeArguments()) { + params.add(resolve(arg)); + } + return new TypeRef(TypeKind.DECLARED, displayName, qualifiedName, params, false, false, null, false); + } + + private TypeRef resolveTypeVar(TypeVariable type) { + String name = type.asElement().getSimpleName().toString(); + TypeMirror bound = type.getUpperBound(); + TypeRef bounds = bound == null || bound.toString().equals("java.lang.Object") ? null : resolve(bound); + return new TypeRef(TypeKind.TYPEVAR, name, name, List.of(), false, false, bounds, false); + } + + private TypeRef resolveWildcard(WildcardType type) { + TypeMirror bound = type.getExtendsBound(); + if (bound == null) { + bound = type.getSuperBound(); + } + TypeRef bounds = bound == null ? null : resolve(bound); + return new TypeRef(TypeKind.WILDCARD, "?", "?", List.of(), false, false, bounds, false); + } + + private TypeRef resolveIntersection(IntersectionType type) { + List bounds = new ArrayList<>(); + for (TypeMirror bound : type.getBounds()) { + bounds.add(resolve(bound)); + } + return new TypeRef(TypeKind.INTERSECTION, "&", "&", bounds, false, false, null, false); + } + + private TypeRef resolveUnion(UnionType type) { + List bounds = new ArrayList<>(); + for (TypeMirror bound : type.getAlternatives()) { + bounds.add(resolve(bound)); + } + return new TypeRef(TypeKind.UNION, "|", "|", bounds, false, false, null, false); + } + + private String getQualifiedName(Element element) { + Element current = element; + StringBuilder name = new StringBuilder(); + while (current != null && current.getKind() != ElementKind.PACKAGE) { + if (current instanceof TypeElement typeElement) { + if (!name.isEmpty()) { + name.insert(0, "."); + } + name.insert(0, typeElement.getSimpleName()); + } + current = current.getEnclosingElement(); + } + if (current instanceof PackageElement pkg) { + String pkgName = pkg.getQualifiedName().toString(); + if (!pkgName.isEmpty()) { + name.insert(0, pkgName + "."); + } + } + return name.toString(); + } + + private String getDisplayName(Element element) { + StringBuilder name = new StringBuilder(); + Element current = element; + while (current != null && current.getKind() != ElementKind.PACKAGE) { + if (current instanceof TypeElement typeElement) { + if (!name.isEmpty()) { + name.insert(0, "."); + } + name.insert(0, typeElement.getSimpleName()); + } + current = current.getEnclosingElement(); + } + return name.toString(); + } + + @Override + public String format(TypeRef type, TargetLanguage target) { + return format(type, target, false); + } + + @Override + public String format(TypeRef type, TargetLanguage target, boolean paramType) { + if (type == null) { + return "any"; + } + + return switch (target) { + case TYPESCRIPT -> formatTypeScript(type, paramType); + case PYTHON -> formatPython(type); + case MARKDOWN, HTML -> formatJavaLike(type); + }; + } + + private String formatTypeScript(TypeRef type, boolean paramType) { + return switch (type.kind()) { + case PRIMITIVE -> switch (type.name()) { + case "boolean" -> "boolean"; + case "byte", "short", "int", "long", "float", "double", "char" -> + paramType ? type.name() : "number"; + default -> "number"; + }; + case VOID -> "void"; + case ARRAY -> { + String component = formatTypeScript(type.typeArgs().get(0), paramType); + yield paramType ? component + "[]" : "JavaArray<" + component + ">"; + } + case DECLARED -> { + String alias = typeScriptAliases.get(type.qualifiedName()); + if (alias != null) { + yield alias; + } + + if (type.qualifiedName().startsWith("net.minecraft.")) { + yield maskMinecraftType(type, paramType); + } + + if (paramType && FUNCTIONAL_INTERFACES.containsKey(type.qualifiedName())) { + yield formatFunctionalInterface(type, paramType); + } + + String base = mapDeclaredTypeScript(type, paramType); + if (!type.typeArgs().isEmpty()) { + base = base + "<" + joinTypes(type.typeArgs(), TargetLanguage.TYPESCRIPT, paramType) + ">"; + } + yield base; + } + case TYPEVAR -> type.name(); + case WILDCARD -> "any"; + case INTERSECTION -> "(" + joinTypes(type.typeArgs(), TargetLanguage.TYPESCRIPT, paramType, " & ") + ")"; + case UNION -> "(" + joinTypes(type.typeArgs(), TargetLanguage.TYPESCRIPT, paramType, " | ") + ")"; + default -> "any"; + }; + } + + private String mapDeclaredTypeScript(TypeRef type, boolean paramType) { + String qualifiedName = type.qualifiedName(); + if ("com.jsmacrosce.jsmacros.core.event.BaseEvent".equals(qualifiedName)) { + return "Events.BaseEvent"; + } + if (JAVA_NUMBER_TYPES.containsKey(qualifiedName)) { + return paramType ? JAVA_NUMBER_TYPES.get(qualifiedName) : "number"; + } + return switch (qualifiedName) { + case "java.lang.String" -> "string"; + case "java.lang.Boolean" -> "boolean"; + case "java.lang.Object" -> "any"; + default -> { + if (JAVA_ALIASES.contains(qualifiedName)) { + String base = "Java" + simpleName(qualifiedName); + if (paramType && base.equals("JavaClass")) { + base = "JavaClassArg"; + } + yield base; + } + yield "Packages." + toTsQualified(type); + } + }; + } + + private String toTsQualified(TypeRef type) { + String qualifiedName = type.qualifiedName(); + String name = type.name(); + if (qualifiedName.endsWith(name)) { + String pkg = qualifiedName.substring(0, qualifiedName.length() - name.length()); + return pkg + name.replace('.', '$'); + } + return qualifiedName.replace('.', '$'); + } + + private String formatPython(TypeRef type) { + return switch (type.kind()) { + case PRIMITIVE -> switch (type.name()) { + case "boolean" -> "bool"; + case "byte", "short", "int", "long", "char" -> "int"; + case "float", "double" -> "float"; + default -> "int"; + }; + case VOID -> "None"; + case ARRAY -> "list[" + formatPython(type.typeArgs().get(0)) + "]"; + case DECLARED -> { + String base = mapDeclaredPython(type.qualifiedName()); + if (!type.typeArgs().isEmpty()) { + base = base + "[" + joinTypes(type.typeArgs(), TargetLanguage.PYTHON) + "]"; + } + yield base; + } + case TYPEVAR -> type.name(); + case WILDCARD -> "object"; + case INTERSECTION, UNION -> "object"; + default -> "object"; + }; + } + + private String mapDeclaredPython(String qualifiedName) { + if (!pythonAliasEnabled) { + return simpleName(qualifiedName); + } + return switch (qualifiedName) { + case "java.lang.String" -> "str"; + case "java.lang.Boolean" -> "bool"; + case "java.lang.Integer", "java.lang.Long", "java.lang.Short", "java.lang.Byte" -> "int"; + case "java.lang.Float", "java.lang.Double" -> "float"; + case "java.lang.Object" -> "object"; + default -> simpleName(qualifiedName); + }; + } + + private String formatJavaLike(TypeRef type) { + return switch (type.kind()) { + case PRIMITIVE, VOID, TYPEVAR -> type.name(); + case ARRAY -> formatJavaLike(type.typeArgs().get(0)) + "[]"; + case DECLARED -> { + String base = simpleName(type.qualifiedName()); + if (!type.typeArgs().isEmpty()) { + base = base + "<" + joinTypes(type.typeArgs(), TargetLanguage.MARKDOWN) + ">"; + } + yield base; + } + case WILDCARD -> "?"; + case INTERSECTION -> "(" + joinTypes(type.typeArgs(), TargetLanguage.MARKDOWN, false, " & ") + ")"; + case UNION -> "(" + joinTypes(type.typeArgs(), TargetLanguage.MARKDOWN, false, " | ") + ")"; + default -> type.name(); + }; + } + + private String joinTypes(List types, TargetLanguage target) { + return joinTypes(types, target, false); + } + + private String joinTypes(List types, TargetLanguage target, boolean paramType) { + return joinTypes(types, target, paramType, ", "); + } + + private String joinTypes(List types, TargetLanguage target, boolean paramType, String separator) { + StringBuilder builder = new StringBuilder(); + for (TypeRef ref : types) { + builder.append(format(ref, target, paramType)).append(separator); + } + if (!types.isEmpty()) { + builder.setLength(builder.length() - separator.length()); + } + return builder.toString(); + } + + private String simpleName(String qualifiedName) { + int idx = qualifiedName.lastIndexOf('.'); + return idx == -1 ? qualifiedName : qualifiedName.substring(idx + 1); + } + + private String formatFunctionalInterface(TypeRef type, boolean paramType) { + String template = FUNCTIONAL_INTERFACES.get(type.qualifiedName()); + if (template == null) { + return formatTypeScript(type, paramType); + } + String res = template; + if (!type.typeArgs().isEmpty()) { + int size = type.typeArgs().size(); + for (int i = 0; i < size; i++) { + res = res.replace("$" + i, formatTypeScript(type.typeArgs().get(i), true)); + } + } + return res; + } + + private String maskMinecraftType(TypeRef type, boolean paramType) { + StringBuilder raw = new StringBuilder(type.qualifiedName()); + if (!type.typeArgs().isEmpty()) { + raw.append("<").append(joinTypes(type.typeArgs(), TargetLanguage.TYPESCRIPT, paramType)).append(">"); + } + String cleaned = raw.toString() + .replace("/* ", "") + .replace(" */ any", "") + .replace(" */", ""); + return "/* " + cleaned + " */ any"; + } +} diff --git a/buildSrc/src/main/java/com/jsmacrosce/doclet/core/ClassGroup.java b/buildSrc/src/main/java/com/jsmacrosce/doclet/core/ClassGroup.java new file mode 100644 index 000000000..aee40cd8b --- /dev/null +++ b/buildSrc/src/main/java/com/jsmacrosce/doclet/core/ClassGroup.java @@ -0,0 +1,10 @@ +package com.jsmacrosce.doclet.core; + +import com.jsmacrosce.doclet.DocletIgnore; + +@DocletIgnore +public enum ClassGroup { + Class, + Library, + Event, +} diff --git a/buildSrc/src/main/java/com/jsmacrosce/doclet/core/DocCommentParser.java b/buildSrc/src/main/java/com/jsmacrosce/doclet/core/DocCommentParser.java new file mode 100644 index 000000000..7d09a3463 --- /dev/null +++ b/buildSrc/src/main/java/com/jsmacrosce/doclet/core/DocCommentParser.java @@ -0,0 +1,11 @@ +package com.jsmacrosce.doclet.core; + +import com.jsmacrosce.doclet.DocletIgnore; +import com.jsmacrosce.doclet.core.model.DocComment; + +import javax.lang.model.element.Element; + +@DocletIgnore +public interface DocCommentParser { + DocComment parse(Element element); +} diff --git a/buildSrc/src/main/java/com/jsmacrosce/doclet/core/DocletModelBuilder.java b/buildSrc/src/main/java/com/jsmacrosce/doclet/core/DocletModelBuilder.java new file mode 100644 index 000000000..6c6171932 --- /dev/null +++ b/buildSrc/src/main/java/com/jsmacrosce/doclet/core/DocletModelBuilder.java @@ -0,0 +1,690 @@ +package com.jsmacrosce.doclet.core; + +import com.jsmacrosce.doclet.DocletCategory; +import com.jsmacrosce.doclet.DocletDeclareType; +import com.jsmacrosce.doclet.DocletIgnore; +import com.jsmacrosce.doclet.DocletReplaceParams; +import com.jsmacrosce.doclet.DocletReplaceReturn; +import com.jsmacrosce.doclet.DocletReplaceTypeParams; +import com.jsmacrosce.doclet.core.model.ClassDoc; +import com.jsmacrosce.doclet.core.model.ClassKind; +import com.jsmacrosce.doclet.core.model.DeclaredTypeDoc; +import com.jsmacrosce.doclet.core.model.DocComment; +import com.jsmacrosce.doclet.core.model.DocTag; +import com.jsmacrosce.doclet.core.model.DocTagKind; +import com.jsmacrosce.doclet.core.model.DocletModel; +import com.jsmacrosce.doclet.core.model.MemberDoc; +import com.jsmacrosce.doclet.core.model.MemberKind; +import com.jsmacrosce.doclet.core.model.PackageDoc; +import com.jsmacrosce.doclet.core.model.ParamDoc; +import com.jsmacrosce.doclet.core.model.TypeRef; +import com.jsmacrosce.doclet.core.util.DocBodyRenderer; +import com.jsmacrosce.doclet.core.util.ElementNameUtils; + +import com.sun.source.util.DocTrees; +import jdk.javadoc.doclet.DocletEnvironment; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValue; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.TypeParameterElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.util.Elements; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeKind; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.HashSet; +import java.util.LinkedHashSet; + +@DocletIgnore +public class DocletModelBuilder { + private final TypeResolver typeResolver; + private final DocCommentParser docCommentParser; + private final List declaredTypes = new ArrayList<>(); + private DocTrees docTrees; + private Elements elementUtils; + private Set objectMethods = Set.of(); + private Set objectMethodNames = Set.of(); + + public DocletModelBuilder( + TypeResolver typeResolver, + DocCommentParser docCommentParser + ) { + this.typeResolver = typeResolver; + this.docCommentParser = docCommentParser; + } + + public DocletModel build(DocletEnvironment environment) { + this.docTrees = environment.getDocTrees(); + this.elementUtils = environment.getElementUtils(); + initObjectMethods(); + + Map> packages = new HashMap<>(); + Map> mixinInterfaces = collectMixinInterfaces(environment); + + for (Element element : environment.getIncludedElements()) { + if (!(element instanceof TypeElement type)) { + addDeclaredType(element); + continue; + } + + if (hasDocletIgnore(type)) { + continue; + } + + addDeclaredType(type); + + String packageName = ElementNameUtils.getPackageName(type); + String displayName = ElementNameUtils.getDisplayClassName(type); + String qualifiedName = ElementNameUtils.getQualifiedName(type); + + ClassGroup group = ClassGroup.Class; + String alias = null; + boolean eventCancellable = false; + String eventFilterer = null; + String category = null; + AnnotationMirror library = findAnnotation(type, "Library"); + AnnotationMirror event = findAnnotation(type, "Event"); + if (library != null) { + group = ClassGroup.Library; + alias = String.valueOf(getAnnotationValue(library, "value")); + } else if (event != null) { + group = ClassGroup.Event; + alias = String.valueOf(getAnnotationValue(event, "value")); + Object cancellableValue = getAnnotationValue(event, "cancellable"); + eventCancellable = Boolean.TRUE.equals(cancellableValue); + Object filtererValue = getAnnotationValue(event, "filterer"); + if (filtererValue instanceof DeclaredType declared) { + String name = declared.asElement().getSimpleName().toString(); + if (!"EventFilterer".equals(name)) { + eventFilterer = name; + } + } else if (filtererValue instanceof TypeMirror mirror && mirror.getKind() == TypeKind.DECLARED) { + String name = ((DeclaredType) mirror).asElement().getSimpleName().toString(); + if (!"EventFilterer".equals(name)) { + eventFilterer = name; + } + } + } else if (hasEnclosingAnnotation(type, "Library")) { + group = ClassGroup.Library; + } else if (hasEnclosingAnnotation(type, "Event")) { + group = ClassGroup.Event; + } + DocletCategory categoryAnnotation = type.getAnnotation(DocletCategory.class); + if (categoryAnnotation != null) { + category = categoryAnnotation.value(); + } + + ClassKind kind = switch (type.getKind()) { + case INTERFACE -> ClassKind.INTERFACE; + case ENUM -> ClassKind.ENUM; + case ANNOTATION_TYPE -> ClassKind.ANNOTATION; + default -> ClassKind.CLASS; + }; + + List typeParams = new ArrayList<>(); + for (TypeParameterElement param : type.getTypeParameters()) { + typeParams.add(typeResolver.resolve(param.asType())); + } + + List extendsTypes = new ArrayList<>(); + if (type.getSuperclass() != null && type.getSuperclass().getKind() != javax.lang.model.type.TypeKind.NONE) { + String sup = type.getSuperclass().toString(); + if (!"java.lang.Object".equals(sup)) { + extendsTypes.add(typeResolver.resolve(type.getSuperclass())); + } + } + List implementsTypes = new ArrayList<>(); + Set implemented = new HashSet<>(); + for (var iface : type.getInterfaces()) { + TypeRef resolved = typeResolver.resolve(iface); + if (implemented.add(resolved.qualifiedName())) { + implementsTypes.add(resolved); + } + } + + Set superMcTypes = new LinkedHashSet<>(); + Set superTypes = new LinkedHashSet<>(); + collectSuperTypes(type, superTypes, superMcTypes, new HashSet<>()); + + boolean directExtendsMc = isMinecraftType(type.getSuperclass()); + List mixins = mixinInterfaces.get(qualifiedName); + if (mixins != null) { + for (TypeMirror iface : mixins) { + TypeRef resolved = typeResolver.resolve(iface); + if (implemented.add(resolved.qualifiedName())) { + implementsTypes.add(resolved); + } + } + } + if (directExtendsMc && !superMcTypes.isEmpty()) { + for (TypeElement mcType : superMcTypes) { + String mcQualified = ElementNameUtils.getQualifiedName(mcType); + List extraMixins = mixinInterfaces.get(mcQualified); + if (extraMixins == null) { + continue; + } + for (TypeMirror iface : extraMixins) { + TypeRef resolved = typeResolver.resolve(iface); + if (implemented.add(resolved.qualifiedName())) { + implementsTypes.add(resolved); + } + if (iface.getKind() == TypeKind.DECLARED) { + superTypes.add((TypeElement) ((DeclaredType) iface).asElement()); + } + } + } + } + + List modifiers = new ArrayList<>(); + for (Modifier modifier : type.getModifiers()) { + modifiers.add(modifier.toString()); + } + + DocComment classComment = docCommentParser.parse(type); + List members = new ArrayList<>(); + List instanceMethods = new ArrayList<>(); + + Set eventSkipNames = new HashSet<>(); + if (group == ClassGroup.Event) { + Map> eventMethodsByName = new HashMap<>(); + for (Element enclosed : type.getEnclosedElements()) { + if (enclosed.getKind() != ElementKind.METHOD) { + continue; + } + if (!enclosed.getModifiers().contains(Modifier.PUBLIC)) { + continue; + } + ExecutableElement method = (ExecutableElement) enclosed; + eventMethodsByName.computeIfAbsent(method.getSimpleName().toString(), key -> new ArrayList<>()) + .add(method); + } + for (Map.Entry> entry : eventMethodsByName.entrySet()) { + String name = entry.getKey(); + if (!objectMethodNames.contains(name)) { + continue; + } + boolean allObjectMethods = true; + for (ExecutableElement method : entry.getValue()) { + if (!isObjectMethod(method, type)) { + allObjectMethods = false; + break; + } + } + if (allObjectMethods) { + eventSkipNames.add(name); + } + } + } + + for (Element enclosed : type.getEnclosedElements()) { + if (hasDocletIgnore(enclosed)) { + continue; + } + addDeclaredType(enclosed); + if (!enclosed.getModifiers().contains(Modifier.PUBLIC)) { + continue; + } + if (enclosed.getKind() == ElementKind.FIELD || enclosed.getKind() == ElementKind.ENUM_CONSTANT) { + members.add(buildField(enclosed)); + } else if (enclosed.getKind() == ElementKind.METHOD) { + ExecutableElement method = (ExecutableElement) enclosed; + String methodName = method.getSimpleName().toString(); + if (group == ClassGroup.Event && eventSkipNames.contains(methodName)) { + continue; + } + if (group != ClassGroup.Event && isObjectMethod(method, type)) { + continue; + } + if (isObfuscated(method, type, superMcTypes)) { + continue; + } + members.add(buildMethod(method)); + if (!method.getModifiers().contains(Modifier.STATIC)) { + instanceMethods.add(method); + } + } else if (enclosed.getKind() == ElementKind.CONSTRUCTOR) { + members.add(buildConstructor((ExecutableElement) enclosed)); + } + } + + if (group != ClassGroup.Event) { + addSuperMethods(type, superTypes, superMcTypes, instanceMethods, members); + } + + members.sort(Comparator.comparing(MemberDoc::name, String.CASE_INSENSITIVE_ORDER)); + + ClassDoc classDoc = new ClassDoc( + displayName, + qualifiedName, + packageName, + kind, + group, + alias, + category, + eventCancellable, + eventFilterer, + typeParams, + extendsTypes, + implementsTypes, + modifiers, + classComment, + members + ); + + packages.computeIfAbsent(packageName, key -> new ArrayList<>()).add(classDoc); + } + + List packageDocs = new ArrayList<>(); + for (Map.Entry> entry : packages.entrySet()) { + entry.getValue().sort(Comparator.comparing(ClassDoc::qualifiedName, String.CASE_INSENSITIVE_ORDER)); + packageDocs.add(new PackageDoc(entry.getKey(), entry.getValue())); + } + packageDocs.sort(Comparator.comparing(PackageDoc::name, String.CASE_INSENSITIVE_ORDER)); + + return new DocletModel(packageDocs, declaredTypes); + } + + private void initObjectMethods() { + TypeElement objectElement = elementUtils.getTypeElement("java.lang.Object"); + if (objectElement == null) { + objectMethods = Set.of(); + objectMethodNames = Set.of(); + return; + } + Set methods = new HashSet<>(); + Set names = new HashSet<>(); + for (Element element : objectElement.getEnclosedElements()) { + if (element.getKind() != ElementKind.METHOD) { + continue; + } + if (!element.getModifiers().contains(Modifier.PUBLIC) || element.getModifiers().contains(Modifier.STATIC)) { + continue; + } + ExecutableElement method = (ExecutableElement) element; + methods.add(method); + names.add(method.getSimpleName().toString()); + } + objectMethods = methods; + objectMethodNames = names; + } + + private boolean isObjectMethod(ExecutableElement method, TypeElement owner) { + if (!objectMethodNames.contains(method.getSimpleName().toString())) { + return false; + } + if (docTrees != null && docTrees.getDocCommentTree(method) != null) { + return false; + } + for (ExecutableElement objectMethod : objectMethods) { + if (elementUtils.overrides(method, objectMethod, owner)) { + return true; + } + } + return false; + } + + private void collectSuperTypes( + TypeElement type, + Set superTypes, + Set superMcTypes, + Set visited + ) { + if (!visited.add(type)) { + return; + } + if (!type.getKind().isInterface()) { + TypeMirror superType = type.getSuperclass(); + if (superType != null && superType.getKind() == TypeKind.DECLARED) { + TypeElement superElement = (TypeElement) ((DeclaredType) superType).asElement(); + if (isMinecraftType(superType)) { + superMcTypes.add(superElement); + } else { + superTypes.add(superElement); + } + collectSuperTypes(superElement, superTypes, superMcTypes, visited); + } + } + for (TypeMirror iface : type.getInterfaces()) { + if (iface.getKind() != TypeKind.DECLARED) { + continue; + } + TypeElement ifaceElement = (TypeElement) ((DeclaredType) iface).asElement(); + if (isMinecraftType(iface)) { + superMcTypes.add(ifaceElement); + } else { + superTypes.add(ifaceElement); + } + collectSuperTypes(ifaceElement, superTypes, superMcTypes, visited); + } + } + + private boolean isMinecraftType(TypeMirror type) { + if (type == null || type.getKind() != TypeKind.DECLARED) { + return false; + } + Element element = ((DeclaredType) type).asElement(); + if (!(element instanceof TypeElement typeElement)) { + return false; + } + String pkg = ElementNameUtils.getPackageName(typeElement); + return pkg != null && pkg.startsWith("net.minecraft."); + } + + private boolean isObfuscated(ExecutableElement method, TypeElement owner, Set superMcTypes) { + if (method.getAnnotation(Override.class) == null) { + return false; + } + for (TypeElement mcType : superMcTypes) { + for (Element element : mcType.getEnclosedElements()) { + if (element.getKind() != ElementKind.METHOD) { + continue; + } + if (elementUtils.overrides(method, (ExecutableElement) element, owner)) { + return true; + } + } + } + return false; + } + + private void addSuperMethods( + TypeElement owner, + Set superTypes, + Set superMcTypes, + List instanceMethods, + List members + ) { + if (instanceMethods.isEmpty() || superTypes.isEmpty()) { + return; + } + Set methodNames = new HashSet<>(); + for (ExecutableElement method : instanceMethods) { + methodNames.add(method.getSimpleName().toString()); + } + if (methodNames.isEmpty()) { + return; + } + + List superMethods = new ArrayList<>(); + for (TypeElement superType : superTypes) { + for (Element element : superType.getEnclosedElements()) { + if (element.getKind() != ElementKind.METHOD) { + continue; + } + if (!element.getModifiers().contains(Modifier.PUBLIC) + || element.getModifiers().contains(Modifier.STATIC)) { + continue; + } + ExecutableElement method = (ExecutableElement) element; + if (!methodNames.contains(method.getSimpleName().toString())) { + continue; + } + + boolean overridden = false; + for (ExecutableElement existing : instanceMethods) { + if (existing.getSimpleName().contentEquals(method.getSimpleName()) + && elementUtils.overrides(existing, method, owner)) { + overridden = true; + break; + } + } + if (overridden) { + continue; + } + for (ExecutableElement existing : superMethods) { + if (existing.getSimpleName().contentEquals(method.getSimpleName()) + && elementUtils.overrides(existing, method, owner)) { + overridden = true; + break; + } + } + if (overridden) { + continue; + } + if (hasDocletIgnore(element)) { + continue; + } + if (isObfuscated(method, superType, superMcTypes)) { + continue; + } + superMethods.add(method); + } + } + + for (ExecutableElement method : superMethods) { + members.add(buildMethod(method)); + } + } + + private Map> collectMixinInterfaces(DocletEnvironment environment) { + Map> mixinInterfaces = new HashMap<>(); + for (Element element : environment.getIncludedElements()) { + if (!(element instanceof TypeElement type)) { + continue; + } + AnnotationMirror mixin = findAnnotation(type, "Mixin"); + if (mixin == null) { + continue; + } + List interfaces = type.getInterfaces(); + if (interfaces.isEmpty()) { + continue; + } + + Object targetsObj = getAnnotationValue(mixin, "value"); + if (!(targetsObj instanceof List targets) || targets.isEmpty()) { + continue; + } + + List mixinIfaces = new ArrayList<>(); + for (TypeMirror iface : interfaces) { + if (iface.getKind() != TypeKind.DECLARED) { + continue; + } + TypeElement ifaceElement = (TypeElement) ((DeclaredType) iface).asElement(); + if (hasDocletIgnore(ifaceElement)) { + continue; + } + mixinIfaces.add(iface); + } + if (mixinIfaces.isEmpty()) { + continue; + } + + for (Object targetObj : targets) { + if (!(targetObj instanceof AnnotationValue av)) { + continue; + } + Object value = av.getValue(); + if (!(value instanceof TypeMirror mirror) || mirror.getKind() != TypeKind.DECLARED) { + continue; + } + TypeElement targetType = (TypeElement) ((DeclaredType) mirror).asElement(); + String qualifiedName = ElementNameUtils.getQualifiedName(targetType); + mixinInterfaces.computeIfAbsent(qualifiedName, key -> new ArrayList<>()) + .addAll(mixinIfaces); + } + } + return mixinInterfaces; + } + + private void addDeclaredType(Element element) { + DocletDeclareType declare = element.getAnnotation(DocletDeclareType.class); + if (declare != null) { + declaredTypes.add(new DeclaredTypeDoc(declare.name(), declare.type())); + } + } + + private boolean hasDocletIgnore(Element element) { + Element current = element; + while (current != null) { + if (current.getAnnotation(DocletIgnore.class) != null) { + return true; + } + current = current.getEnclosingElement(); + } + return false; + } + + private MemberDoc buildField(Element element) { + DocComment comment = docCommentParser.parse(element); + List modifiers = new ArrayList<>(); + for (Modifier modifier : element.getModifiers()) { + modifiers.add(modifier.toString()); + } + + DocletReplaceReturn replaceReturn = element.getAnnotation(DocletReplaceReturn.class); + String replaceReturnValue = replaceReturn == null ? null : replaceReturn.value(); + TypeRef typeRef = typeResolver.resolve(element.asType()); + if (isNullable(element)) { + typeRef = typeRef.withNullable(true); + } + + return new MemberDoc( + MemberKind.FIELD, + element.getSimpleName().toString(), + ElementNameUtils.memberId(element), + List.of(), + List.of(), + typeRef, + null, + replaceReturnValue, + null, + modifiers, + comment + ); + } + + private MemberDoc buildConstructor(ExecutableElement element) { + return buildExecutable(element, MemberKind.CONSTRUCTOR); + } + + private MemberDoc buildMethod(ExecutableElement element) { + return buildExecutable(element, MemberKind.METHOD); + } + + private MemberDoc buildExecutable(ExecutableElement element, MemberKind kind) { + DocComment comment = docCommentParser.parse(element); + Map paramDocs = new HashMap<>(); + for (DocTag tag : comment.tags()) { + if (tag.kind() == DocTagKind.PARAM && tag.name() != null) { + paramDocs.put(tag.name(), DocBodyRenderer.toRawText(tag.body())); + } + } + + List params = new ArrayList<>(); + List rawParams = element.getParameters(); + VariableElement varArgParam = element.isVarArgs() && !rawParams.isEmpty() + ? rawParams.get(rawParams.size() - 1) + : null; + for (VariableElement param : rawParams) { + boolean isVarArgs = param.equals(varArgParam); + TypeRef paramType = typeResolver.resolve(param.asType()); + if (isNullable(param)) { + paramType = paramType.withNullable(true); + } + params.add(new ParamDoc( + param.getSimpleName().toString(), + paramType, + isVarArgs, + paramDocs.getOrDefault(param.getSimpleName().toString(), "") + )); + } + + List typeParams = new ArrayList<>(); + for (TypeParameterElement param : element.getTypeParameters()) { + typeParams.add(typeResolver.resolve(param.asType())); + } + + List modifiers = new ArrayList<>(); + for (Modifier modifier : element.getModifiers()) { + modifiers.add(modifier.toString()); + } + + DocletReplaceParams replaceParams = element.getAnnotation(DocletReplaceParams.class); + DocletReplaceReturn replaceReturn = element.getAnnotation(DocletReplaceReturn.class); + DocletReplaceTypeParams replaceTypeParams = element.getAnnotation(DocletReplaceTypeParams.class); + + String replaceParamsValue = replaceParams == null ? null : replaceParams.value(); + String replaceReturnValue = replaceReturn == null ? null : replaceReturn.value(); + String replaceTypeParamsValue = replaceTypeParams == null ? null : replaceTypeParams.value(); + + String name = kind == MemberKind.CONSTRUCTOR + ? ElementNameUtils.getDisplayClassName((TypeElement) element.getEnclosingElement()) + : element.getSimpleName().toString(); + + TypeRef returnType = typeResolver.resolve( + kind == MemberKind.CONSTRUCTOR ? element.getEnclosingElement().asType() : element.getReturnType() + ); + if (kind == MemberKind.METHOD && isNullable(element)) { + returnType = returnType.withNullable(true); + } + + return new MemberDoc( + kind, + name, + ElementNameUtils.memberId(element), + params, + typeParams, + returnType, + replaceParamsValue, + replaceReturnValue, + replaceTypeParamsValue, + modifiers, + comment + ); + } + + private AnnotationMirror findAnnotation(TypeElement type, String name) { + for (AnnotationMirror mirror : type.getAnnotationMirrors()) { + String simpleName = mirror.getAnnotationType().asElement().getSimpleName().toString(); + if (simpleName.equals(name)) { + return mirror; + } + } + return null; + } + + private boolean hasEnclosingAnnotation(TypeElement type, String annotationName) { + TypeElement enclosing = enclosingType(type.getEnclosingElement()); + while (enclosing != null) { + if (findAnnotation(enclosing, annotationName) != null) { + return true; + } + enclosing = enclosingType(enclosing.getEnclosingElement()); + } + return false; + } + + private TypeElement enclosingType(Element element) { + if (element instanceof TypeElement typeElement) { + return typeElement; + } + return null; + } + + private Object getAnnotationValue(AnnotationMirror annotation, String key) { + for (Map.Entry el : annotation.getElementValues().entrySet()) { + if (el.getKey().getSimpleName().toString().equals(key)) { + return el.getValue().getValue(); + } + } + return null; + } + + private boolean isNullable(Element element) { + return element.getAnnotationMirrors().stream() + .anyMatch(a -> a.getAnnotationType().asElement().getSimpleName().contentEquals("Nullable")); + } +} diff --git a/buildSrc/src/main/java/com/jsmacrosce/doclet/core/Renderer.java b/buildSrc/src/main/java/com/jsmacrosce/doclet/core/Renderer.java new file mode 100644 index 000000000..5fd5373f1 --- /dev/null +++ b/buildSrc/src/main/java/com/jsmacrosce/doclet/core/Renderer.java @@ -0,0 +1,9 @@ +package com.jsmacrosce.doclet.core; + +import com.jsmacrosce.doclet.DocletIgnore; +import com.jsmacrosce.doclet.core.model.DocletModel; + +@DocletIgnore +public interface Renderer { + String render(DocletModel model); +} diff --git a/buildSrc/src/main/java/com/jsmacrosce/doclet/core/TargetLanguage.java b/buildSrc/src/main/java/com/jsmacrosce/doclet/core/TargetLanguage.java new file mode 100644 index 000000000..75603bc98 --- /dev/null +++ b/buildSrc/src/main/java/com/jsmacrosce/doclet/core/TargetLanguage.java @@ -0,0 +1,11 @@ +package com.jsmacrosce.doclet.core; + +import com.jsmacrosce.doclet.DocletIgnore; + +@DocletIgnore +public enum TargetLanguage { + TYPESCRIPT, + PYTHON, + MARKDOWN, + HTML +} diff --git a/buildSrc/src/main/java/com/jsmacrosce/doclet/core/TypeResolver.java b/buildSrc/src/main/java/com/jsmacrosce/doclet/core/TypeResolver.java new file mode 100644 index 000000000..c7b2376c2 --- /dev/null +++ b/buildSrc/src/main/java/com/jsmacrosce/doclet/core/TypeResolver.java @@ -0,0 +1,16 @@ +package com.jsmacrosce.doclet.core; + +import com.jsmacrosce.doclet.DocletIgnore; +import com.jsmacrosce.doclet.core.model.TypeRef; + +import javax.lang.model.type.TypeMirror; + +@DocletIgnore +public interface TypeResolver { + TypeRef resolve(TypeMirror type); + String format(TypeRef type, TargetLanguage target); + + default String format(TypeRef type, TargetLanguage target, boolean paramType) { + return format(type, target); + } +} diff --git a/buildSrc/src/main/java/com/jsmacrosce/doclet/core/mddoclet/Main.java b/buildSrc/src/main/java/com/jsmacrosce/doclet/core/mddoclet/Main.java new file mode 100644 index 000000000..1ce83ed27 --- /dev/null +++ b/buildSrc/src/main/java/com/jsmacrosce/doclet/core/mddoclet/Main.java @@ -0,0 +1,80 @@ +package com.jsmacrosce.doclet.core.mddoclet; + +import com.jsmacrosce.doclet.DocletIgnore; +import com.sun.source.util.DocTrees; +import jdk.javadoc.doclet.Doclet; +import jdk.javadoc.doclet.DocletEnvironment; +import jdk.javadoc.doclet.Reporter; +import com.jsmacrosce.doclet.core.BasicDocCommentParser; +import com.jsmacrosce.doclet.core.BasicTypeResolver; +import com.jsmacrosce.doclet.core.DocletModelBuilder; +import com.jsmacrosce.doclet.core.render.MarkdownWriter; +import com.jsmacrosce.doclet.options.Links; +import com.jsmacrosce.doclet.options.McVersion; +import com.jsmacrosce.doclet.options.IgnoredItem; +import com.jsmacrosce.doclet.options.OutputDirectory; +import com.jsmacrosce.doclet.options.Version; + +import javax.lang.model.SourceVersion; +import javax.tools.Diagnostic; +import java.io.File; +import java.io.IOException; +import java.util.Locale; +import java.util.Set; + +@DocletIgnore +public class Main implements Doclet { + private Reporter reporter; + + @Override + public void init(Locale locale, Reporter reporter) { + this.reporter = reporter; + } + + @Override + public String getName() { + return "VitePressDoc Generator (Core)"; + } + + @Override + public Set getSupportedOptions() { + return Set.of( + new Version(), + new McVersion(), + new OutputDirectory(), + new Links(), + new IgnoredItem("-doctitle", 1), + new IgnoredItem("-notimestamp", 0), + new IgnoredItem("-windowtitle", 1) + ); + } + + @Override + public SourceVersion getSupportedSourceVersion() { + return SourceVersion.RELEASE_16; + } + + @Override + public boolean run(DocletEnvironment environment) { + DocTrees trees = environment.getDocTrees(); + BasicTypeResolver typeResolver = new BasicTypeResolver(); + DocletModelBuilder builder = new DocletModelBuilder(typeResolver, new BasicDocCommentParser(trees)); + var model = builder.build(environment); + + File outDir = new File(OutputDirectory.outputDir.toPath().resolve("content").toString(), Version.version); + if (!outDir.exists() && !outDir.mkdirs()) { + reporter.print(Diagnostic.Kind.ERROR, "Failed to create version dir\n"); + return false; + } + + MarkdownWriter writer = new MarkdownWriter(); + writer.setExternalPackages(Links.externalPackages); + try { + writer.write(model, outDir, Version.version, McVersion.mcVersion); + } catch (IOException e) { + e.printStackTrace(); + return false; + } + return true; + } +} diff --git a/buildSrc/src/main/java/com/jsmacrosce/doclet/core/model/ClassDoc.java b/buildSrc/src/main/java/com/jsmacrosce/doclet/core/model/ClassDoc.java new file mode 100644 index 000000000..6bca3eb47 --- /dev/null +++ b/buildSrc/src/main/java/com/jsmacrosce/doclet/core/model/ClassDoc.java @@ -0,0 +1,26 @@ +package com.jsmacrosce.doclet.core.model; + +import com.jsmacrosce.doclet.DocletIgnore; +import com.jsmacrosce.doclet.core.ClassGroup; + +import java.util.List; + +@DocletIgnore +public record ClassDoc( + String name, + String qualifiedName, + String packageName, + ClassKind kind, + ClassGroup group, + String alias, + String category, + boolean eventCancellable, + String eventFilterer, + List typeParams, + List extendsTypes, + List implementsTypes, + List modifiers, + DocComment docComment, + List members +) { +} diff --git a/buildSrc/src/main/java/com/jsmacrosce/doclet/core/model/ClassKind.java b/buildSrc/src/main/java/com/jsmacrosce/doclet/core/model/ClassKind.java new file mode 100644 index 000000000..662a015f1 --- /dev/null +++ b/buildSrc/src/main/java/com/jsmacrosce/doclet/core/model/ClassKind.java @@ -0,0 +1,11 @@ +package com.jsmacrosce.doclet.core.model; + +import com.jsmacrosce.doclet.DocletIgnore; + +@DocletIgnore +public enum ClassKind { + CLASS, + INTERFACE, + ENUM, + ANNOTATION +} diff --git a/buildSrc/src/main/java/com/jsmacrosce/doclet/core/model/DeclaredTypeDoc.java b/buildSrc/src/main/java/com/jsmacrosce/doclet/core/model/DeclaredTypeDoc.java new file mode 100644 index 000000000..dfefa2c7a --- /dev/null +++ b/buildSrc/src/main/java/com/jsmacrosce/doclet/core/model/DeclaredTypeDoc.java @@ -0,0 +1,7 @@ +package com.jsmacrosce.doclet.core.model; + +import com.jsmacrosce.doclet.DocletIgnore; + +@DocletIgnore +public record DeclaredTypeDoc(String name, String type) { +} diff --git a/buildSrc/src/main/java/com/jsmacrosce/doclet/core/model/DocBodyNode.java b/buildSrc/src/main/java/com/jsmacrosce/doclet/core/model/DocBodyNode.java new file mode 100644 index 000000000..434f26e91 --- /dev/null +++ b/buildSrc/src/main/java/com/jsmacrosce/doclet/core/model/DocBodyNode.java @@ -0,0 +1,71 @@ +package com.jsmacrosce.doclet.core.model; + +import com.jsmacrosce.doclet.DocletIgnore; + +/** + * A single node in a structured javadoc body. + * + *

The javadoc AST is preserved through the model boundary as a + * {@code List} rather than being eagerly collapsed to a raw string. + * Each renderer then pattern-matches on node types to produce its own + * format-specific output without regex re-parsing. + * + *

Permitted subtypes: + *

    + *
  • {@link Text} — a plain-text run
  • + *
  • {@link Code} — an inline code span ({@code {@code ...}} or {@code {@literal ...}})
  • + *
  • {@link Link} — a {@code {@link}} or {@code {@linkplain}} reference
  • + *
  • {@link Html} — raw HTML (block or inline) that doesn't fit the above
  • + *
+ */ +@DocletIgnore +public sealed interface DocBodyNode + permits DocBodyNode.Text, DocBodyNode.Code, DocBodyNode.Link, DocBodyNode.Html { + + /** + * A plain-text run originating from a {@code DocTree.Kind.TEXT} node. + * + * @param value the raw text content (may contain newlines, spaces, etc.) + */ + record Text(String value) implements DocBodyNode {} + + /** + * An inline code span originating from a {@code {@code}} or {@code {@literal}} tag. + * The {@code value} is the unescaped content; renderers wrap it as appropriate + * (e.g. {@code `value`} in Markdown, {@code value} in HTML). + * + * @param value the literal content of the code span, unescaped + */ + record Code(String value) implements DocBodyNode {} + + /** + * A {@code {@link}} or {@code {@linkplain}} reference. + * + *

{@code signature} is the raw javadoc reference string exactly as written + * (e.g. {@code "FInput#keyDown(int, int)"} or {@code "java.util.List"}). + * Each renderer resolves this string in its own way. + * + *

{@code label} is the explicit display label written after the signature, or + * {@code null} when no label was provided (renderers should then derive a label + * from the signature itself). + * + * @param signature the raw javadoc reference (class, class#member, or #member) + * @param label explicit display label, or {@code null} if none + */ + record Link(String signature, String label) implements DocBodyNode {} + + /** + * Raw HTML content that does not fit the other node types. + * + *

This covers HTML block tags ({@code

}, {@code
}, {@code

},
+     * {@code