diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0609a73 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +.gradle +.idea +/build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 +gradle/wrapper/gradle-wrapper.properties \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..baae2f8 --- /dev/null +++ b/README.md @@ -0,0 +1,48 @@ +# Skript Hub Docs Tool + +Skript Hub Docs Tools is a plugin for Spigot servers to help Skript Addon Developers autogenerate their documentation. This tool offers a one command approach for plugins using Skript's Documentation Annotations to get all their documenatation into a single JSON file. This file can then be uploaded to Skript Hub or used in other tools so all Skripters can view and use the docs. + +## Prerequisites + +1. A Spigot server +2. Skript (version dev37 or greater) +3. Your Skript Addon with all the necessary supporting plugins to enable all syntax elements + + +## How to use + +1. Download the Skript Hub Docs Tool from [here]() and place it into your development servers ```/plugin/``` folder. **DO NOT USE THIS PLUGIN IN PRODUCTION!** +2. Run your server. +3. Either in console or in game run the command ```/docsgen```. +4. Verify from the output dialog that there were not any errors/id collisions for your addon that could not be merged successfully. You might need manually check these in the output file. +5. The generated JSON files can be found in the ```\plugins\SkriptHubDocsTool\documentation``` folder. +6. (Optional) Copy the contents of you addons JSON file and paste it into the Skript Hub JSON import tool and submit the docs. Skript Hub will automatically update all your public documentation. + +## Permissions + +| Permission | Description | +|------------|-------------| +| docstool.command.gendocs | Can use /gendocs | + + +## Trouble Shooting + +### How do I use the Skript Documentation Annotations? + +A tutorial is coming soon, however for now TuSKe has a good example [here](https://github.com/Tuke-Nuke/TuSKe/blob/master/src/main/java/com/github/tukenuke/tuske/effects/EffCancelDrop.java#L20). + +### How does auto merge work? + +Auto merge will try to resolve an ID conflict. If two classes with the same name have the same title and same description but a different syntax pattern, the syntax pattern will be merged to create a single syntax element with both patterns in the JSON file. + +### Help I got an ID conflict and the auto merge failed! + +ID's are generated based on the class name that a syntax is registered from. An ID collision will happen if two classes with the same name are registered. + +Some addons will have the same class name in different packages, if you do this you can either change the class name or use the ```@DocumentationID("New ID")``` annotation from Skript. + +When there is a 3-way conflict a successful merge might show up as a possible failure, make sure to check the final outputted JSON file to make sure that the conflict was resolved. Trying to upload a JSON file to Skript Hub with an ID conflicts will result in an error. + +## Credits + +This tool is based off [TuSKe's](https://github.com/Tuke-Nuke/TuSKe) documentation generation tools with a few changes and improvements. Most of the base techniques/code is from [TuSke](https://github.com/Tuke-Nuke/TuSKe), so massive thanks to [Tuke-Nuke](https://github.com/Tuke-Nuke)! diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..378cb27 --- /dev/null +++ b/build.gradle @@ -0,0 +1,65 @@ +buildscript { + ext.kotlin_version = '1.2.41' + + repositories { + mavenCentral() + } + dependencies { + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +buildscript { + repositories { + jcenter() + } + dependencies { + classpath 'com.github.jengelman.gradle.plugins:shadow:2.0.4' + } +} + + +group 'net.skripthub' +version '1.0' + +apply plugin: 'com.github.johnrengelman.shadow' +apply plugin: 'java' +apply plugin: 'kotlin' + +sourceCompatibility = 1.8 + +repositories { + mavenCentral() + maven { url "https://hub.spigotmc.org/nexus/content/repositories/snapshots" } + maven { url "https://oss.sonatype.org/content/repositories/snapshots" } + maven { url "https://raw.githubusercontent.com/bensku/mvn-repo/master" } + maven { url 'https://repo.destroystokyo.com/repository/maven-public/' } + maven { url 'http://maven.sk89q.com/repo' } + maven { url 'http://nexus.hc.to/content/repositories/pub_releases' } + maven { url 'https://jitpack.io' } +} + +dependencies { + compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + + implementation 'com.github.SkriptLang:Skript:dev37c' + + compileOnly group: 'org.spigotmc', name: 'spigot-api', version:'1.12.2-R0.1-SNAPSHOT' + compileOnly group: 'org.bukkit', name: 'bukkit', version:'1.12.2-R0.1-SNAPSHOT' + testCompile group: 'junit', name: 'junit', version: '4.12' +} + +compileKotlin { + kotlinOptions.jvmTarget = "1.8" +} +compileTestKotlin { + kotlinOptions.jvmTarget = "1.8" +} + +shadowJar { + dependencies { + include dependency("org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version") + } + configurations = [project.configurations.compile] + relocate 'kotlin', 'net.skripthub.skripthubdocstool' +} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..cab7ba5 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..cccdd3d --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..f955316 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..e58c656 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'skripthubdocstool' + diff --git a/src/main/java/net/skripthub/docstool/utils/EventValuesGetter.java b/src/main/java/net/skripthub/docstool/utils/EventValuesGetter.java new file mode 100644 index 0000000..0d90b7f --- /dev/null +++ b/src/main/java/net/skripthub/docstool/utils/EventValuesGetter.java @@ -0,0 +1,47 @@ +package net.skripthub.docstool.utils; + +import ch.njol.skript.registrations.EventValues; +import org.bukkit.event.Event; + +import java.lang.reflect.Method; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * A object to get event values of an event in Skript using reflection + * @author Tuke_Nuke on 21/07/2017 + */ +public class EventValuesGetter { + private Method m = ReflectionUtils.getMethod(EventValues.class, "getEventValuesList", int.class); + + public Class[][] getEventValues(Class[] events) { + // + // + Class[][] eventValues = new Class[3][]; + for (int x = 0; x < 3; x++) { + Set clazz = new HashSet<>(); + List values = ReflectionUtils.invokeMethod(m, null, x - 1); + if (values != null) + label: for (Class c : events) { + for (Object eventValue : values) { + Class event = ReflectionUtils.getField(eventValue.getClass(), eventValue, "event"); + if (event != null && (c.isAssignableFrom(event) || event.isAssignableFrom(c))) { + Class[] excluded = ReflectionUtils.getField(eventValue.getClass(), eventValue, "exculdes"); + if (excluded != null) { + for (Class exclude : excluded) + if (exclude.isAssignableFrom(c)) + continue label; + } + Class ret = ReflectionUtils.getField(eventValue.getClass(), eventValue, "c"); + if (ret != null) { + clazz.add(ret); + } + } + } + } + eventValues[x] = clazz.toArray(new Class[clazz.size()]); + } + return eventValues; + } +} \ No newline at end of file diff --git a/src/main/java/net/skripthub/docstool/utils/ReflectionUtils.java b/src/main/java/net/skripthub/docstool/utils/ReflectionUtils.java new file mode 100644 index 0000000..f7d5c6e --- /dev/null +++ b/src/main/java/net/skripthub/docstool/utils/ReflectionUtils.java @@ -0,0 +1,167 @@ +package net.skripthub.docstool.utils; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +import org.bukkit.Bukkit; + +/** + * Just a simple reflection class, just to not depend on Skript 2.2+ (I think it is the only thing I use from it) + * @author Tuke_Nuke + */ +public class ReflectionUtils { + + public static final String packageVersion = Bukkit.getServer().getClass().getPackage().getName().split(".v")[1]; + + /** + * Check if a class exists. + * @param clz - The class path, like 'org.bukkit.entity.Player' + * @return true if it exists + */ + public static boolean hasClass(String clz){ + try { + Class.forName(clz); + return true; + } catch (Exception e){ + e.printStackTrace(); + } + return false; + + } + /** + * Get a class from a string. + * @param clz - The string path of a class + * @return The class + */ + public static Class getClass(String clz){ + try { + return Class.forName(clz); + } catch (Exception e){ + e.printStackTrace(); + } + return null; + } + public static Constructor getConstructor(Class clz, Class...parameters){ + try { + return clz.getConstructor(parameters); + } catch (Exception e){ + e.printStackTrace(); + } + return null; + } + /** + * Checks if a method exists or not + * @param clz - The class to check. + * @param method - The method's name + * @param parameters - The parameters of method, can be null if none + * @return - true if it exists + */ + public static boolean hasMethod(Class clz, String method, Class...parameters){ + try{ + return getMethod(clz, method, parameters) != null; + } catch(Exception e){ + e.printStackTrace(); + } + return false; + } + + public static Method getMethod(Class clz, String method, Class... parameters){ + try { + return clz.getDeclaredMethod(method, parameters); + } catch (Exception e){ + e.printStackTrace(); + } + return null; + } + @SuppressWarnings("unchecked") + public static T invokeMethod(Class clz, String method, Object instance, Object... parameters){ + try { + Class[] parameterTypes = new Class[parameters.length]; + int x = 0; + for (Object obj : parameters) + parameterTypes[x++] = obj.getClass(); + Method m = clz.getDeclaredMethod(method, parameterTypes); + m.setAccessible(true); + return (T) m.invoke(instance, parameters); + } catch (Exception e){ + e.printStackTrace(); + } + return null; + } + @SuppressWarnings("unchecked") + public static T invokeMethod(Method method, Object instance, Object... parameters){ + try { + method.setAccessible(true); + return (T) method.invoke(instance, parameters); + } catch (Exception e){ + e.printStackTrace(); + } + return null; + } + /** + * Return a new instance of a class. + * @param clz - The class + * @return A instance object of clz. + */ + public static T newInstance(Class clz){ + try { + Constructor c = clz.getDeclaredConstructor(); + c.setAccessible(true); + return c.newInstance(); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + /** + * Return a new instance of a class. + * @return A instance object of clz. + */ + public static T newInstance(Constructor constructor, Object...objects){ + try { + return constructor.newInstance(objects); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + /** + * Use to set a object from a private field. + * @param from - The class to set the field + * @param obj - The instance of class, you can use null if the field is static. + * @param field - The field name + * @return True if successful. + */ + public static boolean setField(Class from, Object obj, String field, Object newValue){ + try { + Field f = from.getDeclaredField(field); + f.setAccessible(true); + f.set(obj, newValue); + return true; + } catch (Exception e){ + e.printStackTrace(); + } + return false; + } + /** + * Use to get a object from a private field. If it will return null in case it was unsuccessful. + * @param from - The class to get the field + * @param obj - The instance of class, you can use null if the field is static. + * @param field - The field name + * @return The object value. + */ + @SuppressWarnings("unchecked") + public static T getField(Class from, Object obj, String field){ + try{ + Field f = from.getDeclaredField(field); + f.setAccessible(true); + return (T) f.get(obj); + } catch (Exception e){ + e.printStackTrace(); + } + return null; + + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/skripthub/docstool/Core.kt b/src/main/kotlin/net/skripthub/docstool/Core.kt new file mode 100644 index 0000000..3fa3920 --- /dev/null +++ b/src/main/kotlin/net/skripthub/docstool/Core.kt @@ -0,0 +1,23 @@ +package net.skripthub.docstool + +import net.skripthub.docstool.commands.GenerateDocsCommand +import org.bukkit.ChatColor +import org.bukkit.plugin.java.JavaPlugin + +class Core: JavaPlugin() { + + companion object { + var plugin: JavaPlugin? = null + private set + } + + override fun onEnable() { + server.consoleSender.sendMessage("[" + ChatColor.DARK_AQUA + "Skript Hub Docs Tool" + + ChatColor.RESET + "] " + ChatColor.RED + "For development servers only! Do not run " + + "this in production!") + plugin = this + getCommand("gendocs").executor = GenerateDocsCommand() + server.consoleSender.sendMessage("[" + ChatColor.DARK_AQUA + "Skript Hub Docs Tool" + + ChatColor.RESET + "] " + ChatColor.RED + "Skript Hub Docs Tool Enabled") + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/skripthub/docstool/commands/GenerateDocsCommand.kt b/src/main/kotlin/net/skripthub/docstool/commands/GenerateDocsCommand.kt new file mode 100644 index 0000000..cec3483 --- /dev/null +++ b/src/main/kotlin/net/skripthub/docstool/commands/GenerateDocsCommand.kt @@ -0,0 +1,22 @@ +package net.skripthub.docstool.commands + + +import net.skripthub.docstool.Core +import net.skripthub.docstool.documentation.BuildDocs +import org.bukkit.Bukkit +import org.bukkit.ChatColor +import org.bukkit.command.Command +import org.bukkit.command.CommandExecutor +import org.bukkit.command.CommandSender + +class GenerateDocsCommand : CommandExecutor { + + override fun onCommand(sender: CommandSender?, command: Command?, label: String?, args: Array?): Boolean { + sender?.sendMessage("[" + ChatColor.DARK_AQUA + "Skript Hub Docs Tool" + + ChatColor.RESET + "] " + ChatColor.LIGHT_PURPLE + "Generating Docs") + + BuildDocs(Core.plugin!!, sender).load() + + return false + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/skripthub/docstool/documentation/BuildDocs.kt b/src/main/kotlin/net/skripthub/docstool/documentation/BuildDocs.kt new file mode 100644 index 0000000..1f60ba9 --- /dev/null +++ b/src/main/kotlin/net/skripthub/docstool/documentation/BuildDocs.kt @@ -0,0 +1,287 @@ +package net.skripthub.docstool.documentation + +import ch.njol.skript.Skript +import ch.njol.skript.classes.ClassInfo +import ch.njol.skript.lang.SkriptEventInfo +import ch.njol.skript.lang.SyntaxElementInfo +import ch.njol.skript.lang.function.Functions +import net.skripthub.docstool.modals.AddonData +import net.skripthub.docstool.modals.SyntaxData +import net.skripthub.docstool.utils.EventValuesGetter +import org.bukkit.Bukkit +import org.bukkit.plugin.java.JavaPlugin +import java.util.* +import ch.njol.skript.log.SkriptLogger +import ch.njol.skript.registrations.Classes +import ch.njol.skript.lang.function.JavaFunction +import net.skripthub.docstool.utils.ReflectionUtils +import org.bukkit.ChatColor +import org.bukkit.command.CommandSender +import java.io.IOException +import java.io.FileOutputStream +import java.io.OutputStreamWriter +import java.io.BufferedWriter +import java.io.File +import kotlin.collections.ArrayList + + +class BuildDocs(private val instance: JavaPlugin, private val sender: CommandSender?) : Runnable{ + + var addonMap: HashMap = hashMapOf() + + private val fileType: FileType = JsonFile(false) + + fun load() { + Bukkit.getScheduler().runTaskLaterAsynchronously(instance, this, 10L) + } + + + override fun run() { + if (Skript.isAcceptRegistrations()) + return + addonMap[Skript::class.java.`package`.name] = AddonData("Skript") + for (addon in Skript.getAddons()) + addonMap[addon.plugin.javaClass.`package`.name] = AddonData(addon.name) + // Events + val getter = EventValuesGetter() + for (eventInfoClassUnsafe in Skript.getEvents()){ + val eventInfoClass = eventInfoClassUnsafe as SkriptEventInfo<*> + val addonEvents = getAddon(eventInfoClass)?.events + // TODO Throw error when null + if (addonEvents != null) { + addSyntax(addonEvents, + GenerateSyntax.generateSyntaxFromEvent(eventInfoClass, getter)) + } + } + // Conditions + for (syntaxElementInfo in Skript.getConditions()) { +// if (EffectSection::class.java!!.isAssignableFrom(info.c)) +// //Separate effect sections to effects instead of conditions +// addSyntax(getAddon(info.c).getEffects(), SyntaxInfo(info)) +// else + val addonConditions = getAddon(syntaxElementInfo)?.conditions + if (addonConditions != null) { + addSyntax(addonConditions, + GenerateSyntax.generateSyntaxFromSyntaxElementInfo(syntaxElementInfo)) + } + } + // Effects + for (syntaxElementInfo in Skript.getEffects()) { + val addonEffects = getAddon(syntaxElementInfo)?.effects + if (addonEffects != null) { + addSyntax(addonEffects, + GenerateSyntax.generateSyntaxFromSyntaxElementInfo(syntaxElementInfo)) + } + } + //Expressions + val types = arrayOfNulls>(Classes.getClassInfos().size) + var x = 0 + for (info in Classes.getClassInfos()) + types[x++] = info.c + //A LogHandler for expressions since it catch the changers, which can throw errors in console + //such as "Expression X can only be used in event Y" + val log = SkriptLogger.startParseLogHandler() + Skript.getExpressions().forEachRemaining { info -> + val addonExpressions = getAddon(info)?.expressions + if(addonExpressions != null){ + addSyntax(addonExpressions, + GenerateSyntax.generateSyntaxFromExpression(info, types)) + } + } + log.clear() + log.stop() + + for (syntaxElementInfo in Classes.getClassInfos()) { + val addonTypes = getAddon(syntaxElementInfo)?.types + if(addonTypes != null){ + addSyntax(addonTypes, GenerateSyntax.generateSyntaxFromClassInfo(syntaxElementInfo)) + } + } + // Functions + val functions = ReflectionUtils.invokeMethod>>(Functions::class.java, "getJavaFunctions", null) + if (functions != null) { + for (info in functions) { + //For now only Skript uses this + val addonFunctions = getAddon(info.javaClass)?.functions + if (addonFunctions != null) { + addSyntax(addonFunctions, GenerateSyntax.generateSyntaxFromFunctionInfo(info)) + } + } + } + + //Error Check Each Addon! (No id collisions) + for (addon in addonMap.keys){ + val addonInfo = addonMap[addon] ?: continue + val idSet : MutableSet = mutableSetOf() + // Get results from test and attempt merge + attemptIDMerge(addonInfo.events, idCollisionTest(idSet, addonInfo.events, addon)) + attemptIDMerge(addonInfo.conditions, idCollisionTest(idSet, addonInfo.conditions, addon)) + attemptIDMerge(addonInfo.effects, idCollisionTest(idSet, addonInfo.effects, addon)) + attemptIDMerge(addonInfo.expressions, idCollisionTest(idSet, addonInfo.expressions, addon)) + attemptIDMerge(addonInfo.types, idCollisionTest(idSet, addonInfo.types, addon)) + attemptIDMerge(addonInfo.functions, idCollisionTest(idSet, addonInfo.functions, addon)) + } + + //Write to JSON + //Before, lets delete old files... + val docsDir = File(instance.dataFolder, "documentation/") + if (docsDir.exists()) { + val files = docsDir.listFiles() + if (files != null) + for (f in files) + f.delete() + } else + docsDir.mkdirs() + //Done, now let's write them all into files + for (addon in addonMap.values) { + addon.sortLists() + val file = File(docsDir, addon.name + "." + fileType.extension) + if (!file.exists()) { + file.parentFile.mkdirs() + try { + file.createNewFile() + } catch (io: IOException) { + + } + + } + try { + BufferedWriter(OutputStreamWriter(FileOutputStream(file), "UTF-8")).use { writer -> fileType.write(writer, addon) } + } catch (io: IOException) { + io.printStackTrace() + } + + } + + sender?.sendMessage("[" + ChatColor.DARK_AQUA + "Skript Hub Docs Tool" + + ChatColor.RESET + "] " + ChatColor.GREEN + "Docs have been generated!") + + } + + private fun idCollisionTest(idSet: MutableSet, listOfSyntaxData: MutableList, addon: String): MutableList{ + val idCollisions = mutableListOf() + for (syntax in listOfSyntaxData){ + val id = syntax.id ?: continue + val result = idSet.add(id) + if (!result){ + // PANIC!!! ID COLLISION!!!! + idCollisionErrorMessage(addon, id) + idCollisions.add(id) + } + } + return idCollisions + } + + private fun attemptIDMerge(listOfSyntaxData: MutableList, ids: MutableList){ + // Only merge from like Syntax Types + for(id in ids){ + attemptTypeMerge(id, listOfSyntaxData) + } + } + + private fun attemptTypeMerge(id: String, listOfSyntaxData: MutableList){ + // Message attempting merge + sender?.sendMessage("[" + ChatColor.DARK_AQUA + "Skript Hub Docs Tool" + + ChatColor.RESET + "] " + ChatColor.GREEN + "Attempting merge of $id") + val idCollisionList: ArrayList = ArrayList() + val iterator = listOfSyntaxData.listIterator() + while (iterator.hasNext()) { + val syntax = iterator.next() + val syntaxId = syntax.id ?: continue + if(id == syntaxId){ + idCollisionList.add(syntax) + iterator.remove() + } + } + // No collision, might be a different syntax type + if(idCollisionList.size < 2){ + listOfSyntaxData.addAll(idCollisionList) + sender?.sendMessage("[" + ChatColor.DARK_AQUA + "Skript Hub Docs Tool" + + ChatColor.RESET + "] " + ChatColor.GOLD + "Merge of $id unsuccessful, conflict might be " + + "between two different syntax types or it might have already been resolved") + return + } + // Use first instance as a template and try to merge into first instance + val firstInstance = idCollisionList[0] + var repairedCount = 0 + for (i in 1 until idCollisionList.size){ + val syntaxToMerge = idCollisionList[i] + val syntaxToMergeDesc = firstInstance.description + if (syntaxToMergeDesc != null && syntaxToMerge.description != null && syntaxToMerge.patterns != null) { + if(syntaxToMerge.name == firstInstance.name + && syntaxToMerge.description!!.contentEquals(syntaxToMergeDesc.clone())){ + // Match found, add to firstInstance + firstInstance.patterns = firstInstance.patterns?.plus(syntaxToMerge.patterns!!) + repairedCount += 1 + continue + } + } + // Failed add back to master list + listOfSyntaxData.add(syntaxToMerge) + } + // Add first instance back to list + listOfSyntaxData.add(firstInstance) + + if(repairedCount > 0){ + sender?.sendMessage("[" + ChatColor.DARK_AQUA + "Skript Hub Docs Tool" + + ChatColor.RESET + "] " + ChatColor.GREEN + "Merged ${repairedCount + 1} out of ${idCollisionList.size} " + + "instances of $id") + return + } + sender?.sendMessage("[" + ChatColor.DARK_AQUA + "Skript Hub Docs Tool" + + ChatColor.RESET + "] " + ChatColor.RED + "Unable to merge ${idCollisionList.size} " + + "instances of $id") + } + + private fun idCollisionErrorMessage(addon: String, id: String){ + sender?.sendMessage("[" + ChatColor.DARK_AQUA + "Skript Hub Docs Tool" + + ChatColor.RESET + "] " + ChatColor.RED + "ID CONFLICT DETECTED!\n" + + "Plugin: $addon\nMultiple syntax elements with the same id: $id") + } + + private fun addSyntax(list: MutableList, syntax: SyntaxData?) { + if(syntax == null){ + return + } + if (syntax.name == null || syntax.name!!.isEmpty()) + return + if (syntax.patterns == null || syntax.patterns!!.isEmpty()) { + return + } + list.add(syntax) + } + + private fun getAddon(info: ClassInfo<*>): AddonData? { + return when { + info.parser != null -> getAddon(info.parser!!::class.java) + info.serializer != null -> getAddon(info.serializer!!::class.java) + info.changer != null -> getAddon(info.changer!!::class.java) + else -> getAddon(info.javaClass) + } + } + + private fun getAddon(skriptEventInfo: SyntaxElementInfo<*>): AddonData? { + + var name = skriptEventInfo.c.`package`.name + + if (name == "ch.njol.skript.lang.util"){ + // Used Simple event or expression registration + name = skriptEventInfo.originClassPath + } + + + //If null, bail and throw error + return addonMap.entries + .firstOrNull { name.startsWith(it.key) } + ?.value + } + + private fun getAddon(c: Class<*>): AddonData? { + val name = c.`package`.name + //If null, bail and throw error + return addonMap.entries + .firstOrNull { name.startsWith(it.key) } + ?.value + } + +} \ No newline at end of file diff --git a/src/main/kotlin/net/skripthub/docstool/documentation/FileType.kt b/src/main/kotlin/net/skripthub/docstool/documentation/FileType.kt new file mode 100644 index 0000000..b2b29fa --- /dev/null +++ b/src/main/kotlin/net/skripthub/docstool/documentation/FileType.kt @@ -0,0 +1,15 @@ +package net.skripthub.docstool.documentation + +import net.skripthub.docstool.modals.AddonData +import java.io.BufferedWriter +import java.io.IOException + +/** + * @author Tuke_Nuke on 21/07/2017 + */ +abstract class FileType(val extension: String) { + + @Throws(IOException::class) + abstract fun write(writer: BufferedWriter, addon: AddonData) + +} \ No newline at end of file diff --git a/src/main/kotlin/net/skripthub/docstool/documentation/GenerateSyntax.kt b/src/main/kotlin/net/skripthub/docstool/documentation/GenerateSyntax.kt new file mode 100644 index 0000000..682a7a2 --- /dev/null +++ b/src/main/kotlin/net/skripthub/docstool/documentation/GenerateSyntax.kt @@ -0,0 +1,229 @@ +package net.skripthub.docstool.documentation + +import ch.njol.skript.classes.Changer +import ch.njol.skript.classes.ClassInfo +import ch.njol.skript.doc.* +import ch.njol.skript.lang.ExpressionInfo +import ch.njol.skript.lang.SkriptEventInfo +import ch.njol.skript.lang.SyntaxElementInfo +import ch.njol.skript.lang.function.JavaFunction +import ch.njol.skript.registrations.Classes +import net.skripthub.docstool.modals.SyntaxData +import net.skripthub.docstool.utils.EventValuesGetter +import net.skripthub.docstool.utils.ReflectionUtils +import org.bukkit.event.Cancellable +import java.util.* + +class GenerateSyntax { + companion object { + fun generateSyntaxFromEvent(info: SkriptEventInfo<*>, getter: EventValuesGetter?) : SyntaxData? { + if(info.description != null && info.description!!.contentEquals(SkriptEventInfo.NO_DOC)){ + return null + } + val data = SyntaxData() + data.name = info.getName() + data.id = info.id + if(info.documentationID != null){ + data.id = info.documentationID + } + data.description = removeHTML(info.description as? Array) + data.examples = cleanExamples(info.examples) + data.patterns = cleanupSyntaxPattern(info.patterns) + if (data.patterns != null && data.name != null && data.name!!.startsWith("On ")) { + for (x in 0 until data.patterns!!.size) + data.patterns!![x] = "[on] " + data.patterns!![x] + } + + val sinceString = removeHTML(info.since) + if (sinceString != null){ + data.since = arrayOf(sinceString) + } + + for (c in info.events) + if (Cancellable::class.java.isAssignableFrom(c)) { + data.cancellable = java.lang.Boolean.TRUE + } else { + data.cancellable = java.lang.Boolean.FALSE + break + } + + data.requiredPlugins = info.requiredPlugins + + if (getter != null) { + val classes = getter.getEventValues(info.events) + if (classes == null || classes.isEmpty()) + return null + val time = arrayOf("past event-", "event-", "future event-") + val times = ArrayList() + for (x in classes.indices) + (0 until classes[x].size) + .mapNotNull { Classes.getSuperClassInfo(classes[x][it]) } + .mapTo(times) { time[x] + it.codeName } + data.eventValues = times.toTypedArray() + } + + return data + } + + fun generateSyntaxFromSyntaxElementInfo(info: SyntaxElementInfo<*>): SyntaxData? { + val data = SyntaxData() + val syntaxInfoClass = info.c + if (syntaxInfoClass.isAnnotationPresent(NoDoc::class.java)) + return null + if (syntaxInfoClass.isAnnotationPresent(Name::class.java)) + data.name = syntaxInfoClass.getAnnotation(Name::class.java).value + if (data.name == null || data.name!!.isEmpty()) + data.name = syntaxInfoClass.simpleName + data.id = syntaxInfoClass.simpleName + if (syntaxInfoClass.isAnnotationPresent(DocumentationId::class.java)){ + data.id = syntaxInfoClass.getAnnotation(DocumentationId::class.java).value + } + if (syntaxInfoClass.isAnnotationPresent(Description::class.java)) + data.description = removeHTML(syntaxInfoClass.getAnnotation(Description::class.java).value) + if (syntaxInfoClass.isAnnotationPresent(Examples::class.java)) + data.examples = cleanExamples(syntaxInfoClass.getAnnotation(Examples::class.java).value) + data.patterns = cleanupSyntaxPattern(info.patterns) + if (syntaxInfoClass.isAnnotationPresent(Since::class.java)) { + val sinceString = removeHTML(syntaxInfoClass.getAnnotation(Since::class.java).value) + if (sinceString != null) { + data.since = arrayOf(sinceString) + } + } + if (syntaxInfoClass.isAnnotationPresent(RequiredPlugins::class.java)) + data.requiredPlugins = syntaxInfoClass.getAnnotation(RequiredPlugins::class.java).value + return data + } + + fun generateSyntaxFromExpression(info: ExpressionInfo<*, *>, classes: Array?>): SyntaxData? { + val data = generateSyntaxFromSyntaxElementInfo(info) ?: return null + val ci = Classes.getSuperClassInfo(info.returnType) + if (ci != null) + data.returnType = if (ci.docName == null || ci.docName!!.isEmpty()) ci.codeName else ci.docName + else + data.returnType = "Object" + val array = ArrayList() + val expr = ReflectionUtils.newInstance(info.c) + try { + for (mode in Changer.ChangeMode.values()) { + if (Changer.ChangerUtils.acceptsChange(expr, mode, *classes)) + array.add(mode.name.toLowerCase().replace('_', ' ')) + } + } catch (e: Throwable) { + array.add("unknown") + } + + data.changers = array.toTypedArray() + return data + } + + fun generateSyntaxFromClassInfo(info: ClassInfo<*>): SyntaxData? { + if (info.docName != null && info.docName.equals(ClassInfo.NO_DOC)) + return null + val data = SyntaxData() + data.name = info.docName + data.id = info.c.simpleName + data.description = removeHTML(info.description as? Array) + data.examples = cleanExamples(info.examples) + data.usage = removeHTML(info.usage as? Array) + val sinceString = removeHTML(info.since) + if (sinceString != null){ + data.since = arrayOf(sinceString) + } + + if (info.userInputPatterns != null) { + val size = info.userInputPatterns!!.size + data.patterns = Array (size, { _ -> "" }) + var x = 0 + for (p in info.userInputPatterns!!) + data.patterns!![x++] = p.pattern().replace("\\((.+?)\\)\\?".toRegex(), "[$1]").replace("(.)\\?".toRegex(), "[$1]") + } + return data + } + + fun generateSyntaxFromFunctionInfo(info: JavaFunction<*>): SyntaxData? { + val data = SyntaxData() + data.name = info.name + data.id = "function_" + info.name + data.description = removeHTML(info.description as? Array) + data.examples = cleanExamples(info.examples) + val sb = StringBuilder() + sb.append(info.name).append("(") + if (info.parameters != null) { + var index = 0 + for (p in info.parameters) { + if (index++ != 0) + //Skip the first parameter + sb.append(", ") + sb.append(p) + } + } + sb.append(")") + data.patterns = cleanupSyntaxPattern(arrayOf(sb.toString())) + val sinceString = removeHTML(info.since) + if (sinceString != null){ + data.since = arrayOf(sinceString) + } + val infoReturnType = info.returnType + if (infoReturnType != null){ + data.returnType = if (infoReturnType.docName == null || infoReturnType.docName!!.isEmpty()) + infoReturnType.codeName + else + infoReturnType.docName + } + return data + } + + private fun cleanupSyntaxPattern(patterns: Array): Array{ + if(patterns.isEmpty()){ + return patterns + } + for (i in 0 until patterns.size){ + patterns[i] = patterns[i] + .replace("""\\([()])""".toRegex(), "$1") + .replace("""-?\d+¦""".toRegex(), "") + .replace("""-?\d+¦""".toRegex(), "") + .replace("<", "<") + .replace(">", ">") + .replace("""%-(.+?)%""".toRegex()) { + match -> match.value.replace("-", "") + } + .replace("""%~(.+?)%""".toRegex()) { + match -> match.value.replace("~", "") + } + .replace("()", "") + .replace("""@-\d""".toRegex(), "") + .replace("""@\d""".toRegex(), "") + .replace("""\d¦""".toRegex(), "") + } + return patterns + } + + private fun removeHTML(description: Array?): Array{ + if(description == null || description.isEmpty()){ + return emptyArray() + } + for (i in 0 until description.size){ + description[i] = this.removeHTML(description[i])!! + } + return description + } + + private fun cleanExamples(examples: Array?): Array?{ + if (examples == null || examples.isEmpty()){ + return examples + } + if (examples.size == 1 && examples[0].isEmpty()){ + return null + } + return examples + } + + private fun removeHTML(string: String?): String?{ + if(string.isNullOrEmpty()){ + return string + } + return string?.replace("""<.+?>(.+?)""".toRegex(), "$1") + } + + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/skripthub/docstool/documentation/JsonFile.kt b/src/main/kotlin/net/skripthub/docstool/documentation/JsonFile.kt new file mode 100644 index 0000000..1360d84 --- /dev/null +++ b/src/main/kotlin/net/skripthub/docstool/documentation/JsonFile.kt @@ -0,0 +1,66 @@ +package net.skripthub.docstool.documentation + +import com.google.gson.* +import net.skripthub.docstool.modals.AddonData +import net.skripthub.docstool.modals.SyntaxData +import java.io.BufferedWriter +import java.io.IOException + + +/** + * @author Tuke_Nuke on 30/07/2017 + */ +class JsonFile(raw: Boolean) : FileType("json") { + + private val gson: Gson + + init { + val gson = GsonBuilder().disableHtmlEscaping() + if (!raw) + gson.enableComplexMapKeySerialization().setPrettyPrinting() + this.gson = gson.create() + } + + @Throws(IOException::class) + override fun write(writer: BufferedWriter, addon: AddonData) { + val json = JsonObject() + addSection(json, "events", addon.events) + addSection(json, "conditions", addon.conditions) + addSection(json, "effects", addon.effects) + addSection(json, "expressions", addon.expressions) + addSection(json, "types", addon.types) + addSection(json, "functions", addon.functions) + gson.toJson(json, writer) + } + + fun addSection(json: JsonObject, property: String, list: MutableList) { + val array = JsonArray() + for (syntax in list) { + val jsonSyntax = getJsonSyntax(syntax) + if (jsonSyntax.has("patterns")) + array.add(getJsonSyntax(syntax)) + } + if (array.size() > 0) + json.add(property, array) + } + + fun getJsonSyntax(info: SyntaxData): JsonObject { + val syntax = JsonObject() + for (entry in info.toMap().entries) { + val property = entry.key.toLowerCase().replace('_', ' ') + if (entry.value is String) + syntax.addProperty(property, entry.value as String) + else if (entry.value is Boolean) + syntax.addProperty(property, entry.value as Boolean) + else { + val json = JsonArray() + for (str in entry.value as Array) { + json.add(JsonPrimitive(str)) + } + syntax.add(property, json) + } + } + return syntax + } +} + diff --git a/src/main/kotlin/net/skripthub/docstool/modals/AddonData.kt b/src/main/kotlin/net/skripthub/docstool/modals/AddonData.kt new file mode 100644 index 0000000..057a20e --- /dev/null +++ b/src/main/kotlin/net/skripthub/docstool/modals/AddonData.kt @@ -0,0 +1,18 @@ +package net.skripthub.docstool.modals + +data class AddonData(var name: String, + var events: MutableList = mutableListOf(), + var conditions: MutableList = mutableListOf(), + var effects: MutableList = mutableListOf(), + var expressions: MutableList = mutableListOf(), + var types: MutableList = mutableListOf(), + var functions: MutableList = mutableListOf()) { + fun sortLists() { + events.sortBy { info -> info.name } + conditions.sortBy { info -> info.name } + effects.sortBy { info -> info.name } + expressions.sortBy { info -> info.name } + types.sortBy { info -> info.name } + functions.sortBy { info -> info.name } + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/skripthub/docstool/modals/SyntaxData.kt b/src/main/kotlin/net/skripthub/docstool/modals/SyntaxData.kt new file mode 100644 index 0000000..9aad872 --- /dev/null +++ b/src/main/kotlin/net/skripthub/docstool/modals/SyntaxData.kt @@ -0,0 +1,74 @@ +package net.skripthub.docstool.modals + +import java.util.* + + +data class SyntaxData(var id: String? = null, + var name: String? = null, + var since: Array? = null, + var returnType: String? = null, + var requiredPlugins: Array? = null, + var description: Array? = null, + var examples: Array? = null, + var patterns: Array? = null, + var usage: Array? = null, + var changers: Array? = null, + var eventValues: Array? = null, + var cancellable: Boolean? = null + ) { + fun toMap(): Map { + val map = LinkedHashMap() + addProperty(map, "ID", id!!) + addProperty(map, "Name", name!!) + if( description != null){ + addArray(map, "Description", description!!) + } + if (examples != null){ + addArray(map, "Examples", examples!!) + } + if(usage != null){ + addProperty(map, "Usage", *usage!!) + } + if(since != null){ + addArray(map, "Since", since!!) + } + if(returnType != null){ + addProperty(map, "Return type", returnType!!) + } + if(changers != null){ + addArray(map, "Changers", changers!!) + } + addArray(map, "Patterns", patterns!!) + if(eventValues != null){ + addArray(map, "Event values", eventValues!!) + } + if (cancellable != null) { + map["Cancellable"] = cancellable!! + } + if (requiredPlugins != null){ + addArray(map, "Required Plugins", requiredPlugins!!) + } + return map + } + + private fun addProperty(map: MutableMap, property: String, vararg value: String) { + if (value.isEmpty()) + return + val sb = StringBuilder() + for (str in value) { + if (str != null && !str.isEmpty()) { + if (sb.isNotEmpty()) + sb.append("\n") + sb.append(str) + } + } + if (sb.isNotEmpty()) + map[property] = sb.toString() + } + + private fun addArray(map: MutableMap, property: String, array: Array) { + if (array.isEmpty()) + return + map[property] = array + } +} \ No newline at end of file diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml new file mode 100644 index 0000000..81fd85d --- /dev/null +++ b/src/main/resources/plugin.yml @@ -0,0 +1,22 @@ +main: net.skripthub.docstool.Core +name: SkriptHubDocsTool +description: An tool for addon devs of Skript to easily generate docs for their addon. +author: eyesniper2 +website: https://skripthub.net/ +depend: [Skript] +prefix: Skript Hub Docs Tool +version: ${project.version} + +permissions: + docstool.command.gendocs: + description: Allows you to use the /gendocs command + + +commands: + gendocs: + description: Generate documenation based on installed plugins. + aliases: [builddocs, gendocs, gend, generatedocs, generatedocumentation] + usage: /gendocs + permission: docstool.command.gendocs + permission-message: "You must have the 'docstool.command.gendocs' permission to use this command" +