diff --git a/src/main/java/ch/njol/skript/SkriptCommand.java b/src/main/java/ch/njol/skript/SkriptCommand.java index c735541cae3..e850820aaca 100644 --- a/src/main/java/ch/njol/skript/SkriptCommand.java +++ b/src/main/java/ch/njol/skript/SkriptCommand.java @@ -21,7 +21,9 @@ import ch.njol.skript.aliases.Aliases; import ch.njol.skript.command.CommandHelp; import ch.njol.skript.doc.Documentation; +import ch.njol.skript.doc.DocumentationIdProvider; import ch.njol.skript.doc.HTMLGenerator; +import ch.njol.skript.doc.JSONGenerator; import ch.njol.skript.localization.ArgsMessage; import ch.njol.skript.localization.Language; import ch.njol.skript.localization.PluralizingArgsMessage; @@ -403,9 +405,11 @@ public boolean onCommand(CommandSender sender, Command command, String label, St } File outputDir = Documentation.getDocsOutputDirectory(); outputDir.mkdirs(); - HTMLGenerator generator = new HTMLGenerator(templateDir, outputDir); + HTMLGenerator htmlGenerator = new HTMLGenerator(templateDir, outputDir); + JSONGenerator jsonGenerator = new JSONGenerator(templateDir, outputDir); Skript.info(sender, "Generating docs..."); - generator.generate(); // Try to generate docs... hopefully + htmlGenerator.generate(); // Try to generate docs... hopefully + jsonGenerator.generate(); Skript.info(sender, "Documentation generated!"); } else if (args[0].equalsIgnoreCase("test") && TestMode.DEV_MODE) { File scriptFile; diff --git a/src/main/java/ch/njol/skript/doc/DocumentationGenerator.java b/src/main/java/ch/njol/skript/doc/DocumentationGenerator.java new file mode 100644 index 00000000000..949e59f3ca5 --- /dev/null +++ b/src/main/java/ch/njol/skript/doc/DocumentationGenerator.java @@ -0,0 +1,23 @@ +package ch.njol.skript.doc; + +import java.io.File; + +/** + * Represents a class which generates a documentation format (like HTML or JSON) + */ +public abstract class DocumentationGenerator { + + protected File templateDir; + protected File outputDir; + + public DocumentationGenerator(File templateDir, File outputDir) { + this.templateDir = templateDir; + this.outputDir = outputDir; + } + + /** + * Generates the documentation file + */ + public abstract void generate(); + +} diff --git a/src/main/java/ch/njol/skript/doc/DocumentationIdProvider.java b/src/main/java/ch/njol/skript/doc/DocumentationIdProvider.java new file mode 100644 index 00000000000..4725e4bfaf6 --- /dev/null +++ b/src/main/java/ch/njol/skript/doc/DocumentationIdProvider.java @@ -0,0 +1,145 @@ +package ch.njol.skript.doc; + +import ch.njol.skript.Skript; +import ch.njol.skript.classes.ClassInfo; +import ch.njol.skript.lang.Condition; +import ch.njol.skript.lang.Effect; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.Section; +import ch.njol.skript.lang.SkriptEventInfo; +import ch.njol.skript.lang.SyntaxElementInfo; +import ch.njol.skript.lang.function.Function; +import ch.njol.skript.lang.function.Functions; +import ch.njol.skript.registrations.Classes; +import org.skriptlang.skript.lang.structure.Structure; + +import java.util.Iterator; +import java.util.Objects; +import java.util.function.Predicate; + +public class DocumentationIdProvider { + + /** + * Some syntax classes are registered more than once. This method applies a suffix + * to the id in order to differentiate them + * @param id the potentially conflicting ID + * @param collisionCount the number of conflicts this element has + * @return the unique ID for the element + */ + private static String addCollisionSuffix(String id, int collisionCount) { + if (collisionCount == 0) { + return id; + } + return id + "-" + (collisionCount + 1); + } + + /** + * Calculates the number of collisions in an iterator + * @param potentialCollisions the iterator of potential collisions + * @param collisionCriteria a predicate which checks whether a potential collision is really a collision + * @param equalsCriteria a predicate which checks whether a potential collision equals the current element we are generating + * @return the number of collisions in potentialCollisions up until equalsCriteria was true + */ + private static int calculateCollisionCount(Iterator potentialCollisions, Predicate collisionCriteria, + Predicate equalsCriteria) { + int collisionCount = 0; + while (potentialCollisions.hasNext()) { + T potentialCollision = potentialCollisions.next(); + if (collisionCriteria.test(potentialCollision)) { + if (equalsCriteria.test(potentialCollision)) { + break; + } + collisionCount += 1; + } + } + return collisionCount; + } + + /** + * Gets the documentation ID of a syntax element + * @param syntaxInfo the SyntaxElementInfo to get the ID of + * @return the ID of the syntax element + */ + public static String getId(SyntaxElementInfo syntaxInfo) { + Class syntaxClass = syntaxInfo.getElementClass(); + Iterator> syntaxElementIterator; + if (Effect.class.isAssignableFrom(syntaxClass)) { + syntaxElementIterator = Skript.getEffects().iterator(); + } else if (Condition.class.isAssignableFrom(syntaxClass)) { + syntaxElementIterator = Skript.getConditions().iterator(); + } else if (Expression.class.isAssignableFrom(syntaxClass)) { + syntaxElementIterator = Skript.getExpressions(); + } else if (Section.class.isAssignableFrom(syntaxClass)) { + syntaxElementIterator = Skript.getSections().iterator(); + } else if (Structure.class.isAssignableFrom(syntaxClass)) { + syntaxElementIterator = Skript.getStructures().iterator(); + } else { + throw new IllegalStateException("Unsupported syntax type provided"); + } + int collisionCount = calculateCollisionCount(syntaxElementIterator, + elementInfo -> elementInfo.getElementClass() == syntaxClass, + elementInfo -> elementInfo == syntaxInfo); + DocumentationId documentationIdAnnotation = syntaxClass.getAnnotation(DocumentationId.class); + if (documentationIdAnnotation == null) { + return addCollisionSuffix(syntaxClass.getSimpleName(), collisionCount); + } + return addCollisionSuffix(documentationIdAnnotation.value(), collisionCount); + } + + /** + * Gets the documentation ID of a function + * @param function the function to get the ID of + * @return the documentation ID of the function + */ + public static String getId(Function function) { + int collisionCount = calculateCollisionCount(Functions.getJavaFunctions().iterator(), + javaFunction -> function.getName().equals(javaFunction.getName()), + javaFunction -> javaFunction == function); + return addCollisionSuffix(function.getName(), collisionCount); + } + + /** + * Gets either the explicitly declared documentation ID or code name of a classinfo + * @param classInfo the ClassInfo to get the ID of + * @return the ID of the ClassInfo + */ + private static String getClassInfoId(ClassInfo classInfo) { + return Objects.requireNonNullElse(classInfo.getDocumentationID(), classInfo.getCodeName()); + } + + /** + * Gets the documentation ID of a ClassInfo + * @param classInfo the ClassInfo to get the ID of + * @return the ID of the ClassInfo + */ + public static String getId(ClassInfo classInfo) { + String classInfoId = getClassInfoId(classInfo); + int collisionCount = calculateCollisionCount(Classes.getClassInfos().iterator(), + otherClassInfo -> classInfoId.equals(getClassInfoId(otherClassInfo)), + otherClassInfo -> classInfo == otherClassInfo); + return addCollisionSuffix(classInfoId, collisionCount); + } + + /** + * Gets either the explicitly declared documentation ID or default ID of an event + * @param eventInfo the event to get the ID of + * @return the ID of the event + */ + private static String getEventId(SkriptEventInfo eventInfo) { + return Objects.requireNonNullElse(eventInfo.getDocumentationID(), eventInfo.getId()); + } + + /** + * Gets the documentation ID of an event + * @param eventInfo the event to get the ID of + * @return the ID of the event + */ + public static String getId(SkriptEventInfo eventInfo) { + String eventId = getEventId(eventInfo); + int collisionCount = calculateCollisionCount(Skript.getEvents().iterator(), + otherEventInfo -> eventId.equals(getEventId(otherEventInfo)), + otherEventInfo -> otherEventInfo == eventInfo); + return addCollisionSuffix(eventId, collisionCount); + } + +} diff --git a/src/main/java/ch/njol/skript/doc/HTMLGenerator.java b/src/main/java/ch/njol/skript/doc/HTMLGenerator.java index 23e6018eed7..c159959c4d8 100644 --- a/src/main/java/ch/njol/skript/doc/HTMLGenerator.java +++ b/src/main/java/ch/njol/skript/doc/HTMLGenerator.java @@ -1,21 +1,3 @@ -/** - * This file is part of Skript. - * - * Skript is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Skript is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Skript. If not, see . - * - * Copyright Peter Güttinger, SkriptLang team and contributors - */ package ch.njol.skript.doc; import ch.njol.skript.Skript; @@ -30,12 +12,11 @@ import ch.njol.skript.lang.SyntaxElementInfo; import ch.njol.skript.lang.function.Functions; import ch.njol.skript.lang.function.JavaFunction; -import ch.njol.skript.lang.function.Parameter; import ch.njol.skript.registrations.Classes; import com.google.common.base.Joiner; import com.google.common.collect.Lists; import com.google.common.io.Files; -import org.apache.commons.lang.StringUtils; + import org.bukkit.event.Cancellable; import org.bukkit.event.Event; import org.bukkit.event.block.BlockCanBuildEvent; @@ -60,23 +41,19 @@ /** * Template engine, primarily used for generating Skript documentation * pages by combining data from annotations and templates. - * + * */ -public class HTMLGenerator { +public class HTMLGenerator extends DocumentationGenerator { private static final String SKRIPT_VERSION = Skript.getVersion().toString().replaceAll("-(dev|alpha|beta)\\d*", ""); // Filter branches private static final Pattern NEW_TAG_PATTERN = Pattern.compile(SKRIPT_VERSION + "(?!\\.)"); // (?!\\.) to avoid matching 2.6 in 2.6.1 etc. private static final Pattern RETURN_TYPE_LINK_PATTERN = Pattern.compile("( ?href=\"(classes\\.html|)#|)\\$\\{element\\.return-type-linkcheck}"); - private final File template; - private final File output; private final String skeleton; public HTMLGenerator(File templateDir, File outputDir) { - this.template = templateDir; - this.output = outputDir; - - this.skeleton = readFile(new File(template + "/template.html")); // Skeleton which contains every other page + super(templateDir, outputDir); + this.skeleton = readFile(new File(this.templateDir + "/template.html")); // Skeleton which contains every other page } /** @@ -128,7 +105,7 @@ private static Iterator> sortedAnnotatedItera list.sort(annotatedComparator); return list.iterator(); } - + /** * Sorts events alphabetically. */ @@ -143,19 +120,19 @@ public int compare(@Nullable SkriptEventInfo o1, @Nullable SkriptEventInfo assert false; throw new NullPointerException(); } - + if (o1.getElementClass().getAnnotation(NoDoc.class) != null) return 1; else if (o2.getElementClass().getAnnotation(NoDoc.class) != null) return -1; - + return o1.name.compareTo(o2.name); } } - + private static final EventComparator eventComparator = new EventComparator(); - + /** * Sorts class infos alphabetically. */ @@ -170,21 +147,21 @@ public int compare(@Nullable ClassInfo o1, @Nullable ClassInfo o2) { assert false; throw new NullPointerException(); } - + String name1 = o1.getDocName(); if (name1 == null) name1 = o1.getCodeName(); String name2 = o2.getDocName(); if (name2 == null) name2 = o2.getCodeName(); - + return name1.compareTo(name2); } - + } - + private static final ClassInfoComparator classInfoComparator = new ClassInfoComparator(); - + /** * Sorts functions by their names, alphabetically. */ @@ -199,40 +176,41 @@ public int compare(@Nullable JavaFunction o1, @Nullable JavaFunction o2) { assert false; throw new NullPointerException(); } - + return o1.getName().compareTo(o2.getName()); } - + } - + private static final FunctionComparator functionComparator = new FunctionComparator(); /** * Generates documentation using template and output directories * given in the constructor. */ + @Override @SuppressWarnings("unchecked") public void generate() { - for (File f : template.listFiles()) { + for (File f : templateDir.listFiles()) { if (f.getName().matches("css|js|assets")) { // Copy CSS/JS/Assets folders String slashName = "/" + f.getName(); - File fileTo = new File(output + slashName); + File fileTo = new File(outputDir + slashName); fileTo.mkdirs(); - for (File filesInside : new File(template + slashName).listFiles()) { - if (filesInside.isDirectory()) + for (File filesInside : new File(templateDir + slashName).listFiles()) { + if (filesInside.isDirectory()) continue; - + if (!filesInside.getName().toLowerCase(Locale.ENGLISH).endsWith(".png")) { // Copy images writeFile(new File(fileTo + "/" + filesInside.getName()), readFile(filesInside)); } - + else if (!filesInside.getName().matches("(?i)(.*)\\.(html?|js|css|json)")) { try { Files.copy(filesInside, new File(fileTo + "/" + filesInside.getName())); } catch (IOException e) { e.printStackTrace(); } - + } } continue; @@ -265,7 +243,7 @@ else if (!filesInside.getName().matches("(?i)(.*)\\.(html?|js|css|json)")) { } for (String name : replace) { - String temp = readFile(new File(template + "/templates/" + name)); + String temp = readFile(new File(templateDir + "/templates/" + name)); temp = temp.replace("${skript.version}", Skript.getVersion().toString()); page = page.replace("${include " + name + "}", temp); } @@ -276,15 +254,15 @@ else if (!filesInside.getName().matches("(?i)(.*)\\.(html?|js|css|json)")) { String[] genParams = page.substring(generate + 11, nextBracket).split(" "); StringBuilder generated = new StringBuilder(); - String descTemp = readFile(new File(template + "/templates/" + genParams[1])); + String descTemp = readFile(new File(templateDir + "/templates/" + genParams[1])); String genType = genParams[0]; boolean isDocsPage = genType.equals("docs"); if (genType.equals("structures") || isDocsPage) { for (Iterator> it = sortedAnnotatedIterator( - (Iterator) Skript.getStructures().stream().filter(structure -> structure.getClass() == StructureInfo.class).iterator()); - it.hasNext(); ) { + (Iterator) Skript.getStructures().stream().filter(structure -> structure.getClass() == StructureInfo.class).iterator()); + it.hasNext(); ) { StructureInfo info = it.next(); assert info != null; @@ -372,12 +350,12 @@ else if (!filesInside.getName().matches("(?i)(.*)\\.(html?|js|css|json)")) { generated.append(generateFunction(descTemp, info)); } } - + page = page.replace(page.substring(generate, nextBracket + 1), generated.toString()); - + generate = page.indexOf("${generate", nextBracket); } - + String name = f.getName(); if (name.endsWith(".html")) { // Fix some stuff specially for HTML page = page.replace("\t", "    "); // Tab to 4 non-collapsible spaces @@ -385,10 +363,10 @@ else if (!filesInside.getName().matches("(?i)(.*)\\.(html?|js|css|json)")) { page = minifyHtml(page); } assert page != null; - writeFile(new File(output + File.separator + name), page); + writeFile(new File(outputDir + File.separator + name), page); } } - + private static String minifyHtml(String page) { StringBuilder sb = new StringBuilder(page.length()); boolean space = false; @@ -403,7 +381,7 @@ private static String minifyHtml(String page) { space = false; sb.appendCodePoint(c); } - + i += Character.charCount(c); } return replaceBr(sb.toString()); @@ -416,27 +394,27 @@ private static String minifyHtml(String page) { private static String replaceBr(String page) { return page.replaceAll("
", "\n"); } - + private static String handleIf(String desc, String start, boolean value) { assert desc != null; int ifStart = desc.indexOf(start); while (ifStart != -1) { int ifEnd = desc.indexOf("${end}", ifStart); String data = desc.substring(ifStart + start.length() + 1, ifEnd); - + String before = desc.substring(0, ifStart); String after = desc.substring(ifEnd + 6); if (value) desc = before + data + after; else desc = before + after; - + ifStart = desc.indexOf(start, ifEnd); } - + return desc; } - + /** * Generates documentation entry for a type which is documented using * annotations. This means expressions, effects and conditions. @@ -465,24 +443,16 @@ private String generateAnnotated(String descTemp, SyntaxElementInfo info, @Nu Description description = c.getAnnotation(Description.class); desc = desc.replace("${element.desc}", Joiner.on("\n").join(getDefaultIfNullOrEmpty((description != null ? description.value() : null), "Unknown description.")).replace("\n\n", "

")); desc = desc.replace("${element.desc-safe}", Joiner.on("\n").join(getDefaultIfNullOrEmpty((description != null ? description.value() : null), "Unknown description.")) - .replace("\\", "\\\\").replace("\"", "\\\"").replace("\t", " ")); + .replace("\\", "\\\\").replace("\"", "\\\"").replace("\t", " ")); // Examples Examples examples = c.getAnnotation(Examples.class); desc = desc.replace("${element.examples}", Joiner.on("
").join(getDefaultIfNullOrEmpty((examples != null ? Documentation.escapeHTML(examples.value()) : null), "Missing examples."))); desc = desc.replace("${element.examples-safe}", Joiner.on("\\n").join(getDefaultIfNullOrEmpty((examples != null ? Documentation.escapeHTML(examples.value()) : null), "Missing examples.")) - .replace("\\", "\\\\").replace("\"", "\\\"").replace("\t", " ")); + .replace("\\", "\\\\").replace("\"", "\\\"").replace("\t", " ")); // Documentation ID - DocumentationId docId = c.getAnnotation(DocumentationId.class); - String ID = docId != null ? (docId != null ? docId.value() : null) : c.getSimpleName(); - // Fix duplicated IDs - if (page != null) { - if (page.contains("href=\"#" + ID + "\"")) { - ID = ID + "-" + (StringUtils.countMatches(page, "href=\"#" + ID + "\"") + 1); - } - } - desc = desc.replace("${element.id}", ID); + desc = desc.replace("${element.id}", DocumentationIdProvider.getId(info)); // Cancellable desc = handleIf(desc, "${if cancellable}", false); @@ -552,11 +522,11 @@ private String generateAnnotated(String descTemp, SyntaxElementInfo info, @Nu generate = desc.indexOf("${generate", nextBracket); } - + // Assume element.pattern generate for (String data : toGen) { String[] split = data.split(" "); - String pattern = readFile(new File(template + "/templates/" + split[1])); + String pattern = readFile(new File(templateDir + "/templates/" + split[1])); StringBuilder patterns = new StringBuilder(); for (String line : getDefaultIfNullOrEmpty(info.patterns, "Missing patterns.")) { assert line != null; @@ -564,7 +534,7 @@ private String generateAnnotated(String descTemp, SyntaxElementInfo info, @Nu String parsed = pattern.replace("${element.pattern}", line); patterns.append(parsed); } - + String toReplace = "${generate element.patterns " + split[1] + "}"; desc = desc.replace(toReplace, patterns.toString()); desc = desc.replace("${generate element.patterns-safe " + split[1] + "}", patterns.toString().replace("\\", "\\\\")); @@ -573,7 +543,7 @@ private String generateAnnotated(String descTemp, SyntaxElementInfo info, @Nu assert desc != null; return desc; } - + private String generateEvent(String descTemp, SkriptEventInfo info, @Nullable String page) { Class c = info.getElementClass(); String desc; @@ -590,7 +560,7 @@ private String generateEvent(String descTemp, SkriptEventInfo info, @Nullable String[] description = getDefaultIfNullOrEmpty(info.getDescription(), "Missing description."); desc = desc.replace("${element.desc}", Joiner.on("\n").join(description).replace("\n\n", "

")); desc = desc - .replace("${element.desc-safe}", Joiner.on("\\n").join(description) + .replace("${element.desc-safe}", Joiner.on("\\n").join(description) .replace("\\", "\\\\").replace("\"", "\\\"").replace("\t", " ")); // By Addon @@ -603,7 +573,7 @@ private String generateEvent(String descTemp, SkriptEventInfo info, @Nullable String[] examples = getDefaultIfNullOrEmpty(info.getExamples(), "Missing examples."); desc = desc.replace("${element.examples}", Joiner.on("\n
").join(Documentation.escapeHTML(examples))); desc = desc - .replace("${element.examples-safe}", Joiner.on("\\n").join(examples) + .replace("${element.examples-safe}", Joiner.on("\\n").join(examples) .replace("\\", "\\\\").replace("\"", "\\\"").replace("\t", " ")); String[] keywords = info.getKeywords(); @@ -621,14 +591,7 @@ private String generateEvent(String descTemp, SkriptEventInfo info, @Nullable desc = desc.replace("${element.cancellable}", cancellable ? "Yes" : ""); // if not cancellable the section is hidden // Documentation ID - String ID = info.getDocumentationID() != null ? info.getDocumentationID() : info.getId(); - // Fix duplicated IDs - if (page != null) { - if (page.contains("href=\"#" + ID + "\"")) { - ID = ID + "-" + (StringUtils.countMatches(page, "href=\"#" + ID + "\"") + 1); - } - } - desc = desc.replace("${element.id}", ID); + desc = desc.replace("${element.id}", DocumentationIdProvider.getId(info)); // Events Events events = c.getAnnotation(Events.class); @@ -668,14 +631,14 @@ private String generateEvent(String descTemp, SkriptEventInfo info, @Nullable int nextBracket = desc.indexOf("}", generate); String data = desc.substring(generate + 11, nextBracket); toGen.add(data); - + generate = desc.indexOf("${generate", nextBracket); } - + // Assume element.pattern generate for (String data : toGen) { String[] split = data.split(" "); - String pattern = readFile(new File(template + "/templates/" + split[1])); + String pattern = readFile(new File(templateDir + "/templates/" + split[1])); StringBuilder patterns = new StringBuilder(); for (String line : getDefaultIfNullOrEmpty(info.patterns, "Missing patterns.")) { assert line != null; @@ -683,7 +646,7 @@ private String generateEvent(String descTemp, SkriptEventInfo info, @Nullable String parsed = pattern.replace("${element.pattern}", line); patterns.append(parsed); } - + desc = desc.replace("${generate element.patterns " + split[1] + "}", patterns.toString()); desc = desc.replace("${generate element.patterns-safe " + split[1] + "}", patterns.toString().replace("\\", "\\\\")); } @@ -691,7 +654,7 @@ private String generateEvent(String descTemp, SkriptEventInfo info, @Nullable assert desc != null; return desc; } - + private String generateClass(String descTemp, ClassInfo info, @Nullable String page) { Class c = info.getC(); String desc; @@ -708,7 +671,7 @@ private String generateClass(String descTemp, ClassInfo info, @Nullable Strin String[] description = getDefaultIfNullOrEmpty(info.getDescription(), "Missing description."); desc = desc.replace("${element.desc}", Joiner.on("\n").join(description).replace("\n\n", "

")); desc = desc - .replace("${element.desc-safe}", Joiner.on("\\n").join(description) + .replace("${element.desc-safe}", Joiner.on("\\n").join(description) .replace("\\", "\\\\").replace("\"", "\\\"").replace("\t", " ")); // By Addon @@ -721,20 +684,13 @@ private String generateClass(String descTemp, ClassInfo info, @Nullable Strin String[] examples = getDefaultIfNullOrEmpty(info.getExamples(), "Missing examples."); desc = desc.replace("${element.examples}", Joiner.on("\n
").join(Documentation.escapeHTML(examples))); desc = desc.replace("${element.examples-safe}", Joiner.on("\\n").join(Documentation.escapeHTML(examples)) - .replace("\\", "\\\\").replace("\"", "\\\"").replace("\t", " ")); + .replace("\\", "\\\\").replace("\"", "\\\"").replace("\t", " ")); Keywords keywords = c.getAnnotation(Keywords.class); desc = desc.replace("${element.keywords}", keywords == null ? "" : Joiner.on(", ").join(keywords.value())); // Documentation ID - String ID = info.getDocumentationID() != null ? info.getDocumentationID() : info.getCodeName(); - // Fix duplicated IDs - if (page != null) { - if (page.contains("href=\"#" + ID + "\"")) { - ID = ID + "-" + (StringUtils.countMatches(page, "href=\"#" + ID + "\"") + 1); - } - } - desc = desc.replace("${element.id}", ID); + desc = desc.replace("${element.id}", DocumentationIdProvider.getId(info)); // Cancellable desc = handleIf(desc, "${if cancellable}", false); @@ -777,14 +733,14 @@ private String generateClass(String descTemp, ClassInfo info, @Nullable Strin int nextBracket = desc.indexOf("}", generate); String data = desc.substring(generate + 11, nextBracket); toGen.add(data); - + generate = desc.indexOf("${generate", nextBracket); } - + // Assume element.pattern generate for (String data : toGen) { String[] split = data.split(" "); - String pattern = readFile(new File(template + "/templates/" + split[1])); + String pattern = readFile(new File(templateDir + "/templates/" + split[1])); StringBuilder patterns = new StringBuilder(); String[] lines = getDefaultIfNullOrEmpty(info.getUsage(), "Missing patterns."); if (lines == null) @@ -795,15 +751,15 @@ private String generateClass(String descTemp, ClassInfo info, @Nullable Strin String parsed = pattern.replace("${element.pattern}", line); patterns.append(parsed); } - + desc = desc.replace("${generate element.patterns " + split[1] + "}", patterns.toString()); desc = desc.replace("${generate element.patterns-safe " + split[1] + "}", patterns.toString().replace("\\", "\\\\")); } - + assert desc != null; return desc; } - + private String generateFunction(String descTemp, JavaFunction info) { String desc = ""; @@ -819,7 +775,7 @@ private String generateFunction(String descTemp, JavaFunction info) { String[] description = getDefaultIfNullOrEmpty(info.getDescription(), "Missing description."); desc = desc.replace("${element.desc}", Joiner.on("\n").join(description)); desc = desc - .replace("${element.desc-safe}", Joiner.on("\\n").join(description) + .replace("${element.desc-safe}", Joiner.on("\\n").join(description) .replace("\\", "\\\\").replace("\"", "\\\"").replace("\t", " ")); // By Addon @@ -832,14 +788,14 @@ private String generateFunction(String descTemp, JavaFunction info) { String[] examples = getDefaultIfNullOrEmpty(info.getExamples(), "Missing examples."); desc = desc.replace("${element.examples}", Joiner.on("\n
").join(Documentation.escapeHTML(examples))); desc = desc - .replace("${element.examples-safe}", Joiner.on("\\n").join(examples) + .replace("${element.examples-safe}", Joiner.on("\\n").join(examples) .replace("\\", "\\\\").replace("\"", "\\\"").replace("\t", " ")); String[] keywords = info.getKeywords(); desc = desc.replace("${element.keywords}", keywords == null ? "" : Joiner.on(", ").join(keywords)); // Documentation ID - desc = desc.replace("${element.id}", info.getName()); + desc = desc.replace("${element.id}", DocumentationIdProvider.getId(info)); // Cancellable desc = handleIf(desc, "${if cancellable}", false); @@ -870,31 +826,26 @@ private String generateFunction(String descTemp, JavaFunction info) { int nextBracket = desc.indexOf("}", generate); String data = desc.substring(generate + 11, nextBracket); toGen.add(data); - + generate = desc.indexOf("${generate", nextBracket); } - + // Assume element.pattern generate for (String data : toGen) { String[] split = data.split(" "); - String pattern = readFile(new File(template + "/templates/" + split[1])); + String pattern = readFile(new File(templateDir + "/templates/" + split[1])); String patterns = ""; - Parameter[] params = info.getParameters(); - String[] types = new String[params.length]; - for (int i = 0; i < types.length; i++) { - types[i] = params[i].toString(); - } - String line = docName + "(" + Joiner.on(", ").join(types) + ")"; // Better not have nulls + String line = info.getSignature().toString(false, false); // Better not have nulls patterns += pattern.replace("${element.pattern}", line); - + desc = desc.replace("${generate element.patterns " + split[1] + "}", patterns); desc = desc.replace("${generate element.patterns-safe " + split[1] + "}", patterns.replace("\\", "\\\\")); } - + assert desc != null; return desc; } - + @SuppressWarnings("null") private static String readFile(File f) { try { @@ -904,7 +855,7 @@ private static String readFile(File f) { return ""; } } - + private static void writeFile(File f, String data) { try { Files.write(data, f, StandardCharsets.UTF_8); @@ -912,7 +863,7 @@ private static void writeFile(File f, String data) { e.printStackTrace(); } } - + private static String cleanPatterns(final String patterns) { return Documentation.cleanPatterns(patterns); } @@ -926,14 +877,14 @@ private static String cleanPatterns(final String patterns, boolean escapeHTML) { /** * Checks if a string is empty or null then it will return the message provided - * + * * @param string the String to check * @param message the String to return if either condition is true */ public String getDefaultIfNullOrEmpty(@Nullable String string, String message) { return (string == null || string.isEmpty()) ? message : string; // Null check first otherwise NullPointerException is thrown } - + public String[] getDefaultIfNullOrEmpty(@Nullable String[] string, String message) { return (string == null || string.length == 0 || string[0].equals("")) ? new String[]{ message } : string; // Null check first otherwise NullPointerException is thrown } diff --git a/src/main/java/ch/njol/skript/doc/JSONGenerator.java b/src/main/java/ch/njol/skript/doc/JSONGenerator.java new file mode 100644 index 00000000000..9e33a90f66c --- /dev/null +++ b/src/main/java/ch/njol/skript/doc/JSONGenerator.java @@ -0,0 +1,251 @@ +package ch.njol.skript.doc; + +import ch.njol.skript.Skript; +import ch.njol.skript.classes.ClassInfo; +import ch.njol.skript.lang.SkriptEventInfo; +import ch.njol.skript.lang.SyntaxElement; +import ch.njol.skript.lang.SyntaxElementInfo; +import ch.njol.skript.lang.function.Functions; +import ch.njol.skript.lang.function.JavaFunction; +import ch.njol.skript.registrations.Classes; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.lang.structure.Structure; +import org.skriptlang.skript.lang.structure.StructureInfo; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Iterator; +import java.util.Objects; +import java.util.stream.Stream; + +/** + * Generates JSON docs + */ +public class JSONGenerator extends DocumentationGenerator { + + public JSONGenerator(File templateDir, File outputDir) { + super(templateDir, outputDir); + } + + /** + * Coverts a String array to a JsonArray + * @param strings the String array to convert + * @return the JsonArray containing the Strings + */ + private static @Nullable JsonArray convertToJsonArray(String @Nullable [] strings) { + if (strings == null) + return null; + JsonArray jsonArray = new JsonArray(); + for (String string : strings) + jsonArray.add(new JsonPrimitive(string)); + return jsonArray; + } + + /** + * Generates the documentation JsonObject for an element that is annotated with documentation + * annotations (e.g. effects, conditions, etc.) + * @param syntaxInfo the syntax info element to generate the documentation object of + * @return the JsonObject representing the documentation of the provided syntax element + */ + private @Nullable JsonObject generatedAnnotatedElement(SyntaxElementInfo syntaxInfo) { + Class syntaxClass = syntaxInfo.getElementClass(); + Name nameAnnotation = syntaxClass.getAnnotation(Name.class); + if (nameAnnotation == null || syntaxClass.getAnnotation(NoDoc.class) != null) + return null; + JsonObject syntaxJsonObject = new JsonObject(); + syntaxJsonObject.addProperty("id", DocumentationIdProvider.getId(syntaxInfo)); + syntaxJsonObject.addProperty("name", nameAnnotation.value()); + + Since sinceAnnotation = syntaxClass.getAnnotation(Since.class); + syntaxJsonObject.addProperty("since", sinceAnnotation == null ? null : sinceAnnotation.value()); + + Description descriptionAnnotation = syntaxClass.getAnnotation(Description.class); + if (descriptionAnnotation != null) { + syntaxJsonObject.add("description", convertToJsonArray(descriptionAnnotation.value())); + } else { + syntaxJsonObject.add("description", new JsonArray()); + } + + Examples examplesAnnotation = syntaxClass.getAnnotation(Examples.class); + if (examplesAnnotation != null) { + syntaxJsonObject.add("examples", convertToJsonArray(examplesAnnotation.value())); + } else { + syntaxJsonObject.add("examples", new JsonArray()); + } + + + syntaxJsonObject.add("patterns", convertToJsonArray(syntaxInfo.getPatterns())); + return syntaxJsonObject; + } + + /** + * Generates the documentation JsonObject for an event + * @param eventInfo the event to generate the documentation object for + * @return a documentation JsonObject for the event + */ + private JsonObject generateEventElement(SkriptEventInfo eventInfo) { + JsonObject syntaxJsonObject = new JsonObject(); + syntaxJsonObject.addProperty("id", DocumentationIdProvider.getId(eventInfo)); + syntaxJsonObject.addProperty("name", eventInfo.name); + syntaxJsonObject.addProperty("since", eventInfo.getSince()); + syntaxJsonObject.add("description", convertToJsonArray(eventInfo.getDescription())); + syntaxJsonObject.add("examples", convertToJsonArray(eventInfo.getExamples())); + syntaxJsonObject.add("patterns", convertToJsonArray(eventInfo.patterns)); + return syntaxJsonObject; + } + + + /** + * Generates a JsonArray containing the documentation JsonObjects for each structure in the iterator + * @param infos the structures to generate documentation for + * @return a JsonArray containing the documentation JsonObjects for each structure + */ + private > JsonArray generateStructureElementArray(Iterator infos) { + JsonArray syntaxArray = new JsonArray(); + infos.forEachRemaining(info -> { + if (info instanceof SkriptEventInfo eventInfo) { + syntaxArray.add(generateEventElement(eventInfo)); + } else { + JsonObject structureElementJsonObject = generatedAnnotatedElement(info); + if (structureElementJsonObject != null) + syntaxArray.add(structureElementJsonObject); + } + }); + return syntaxArray; + } + + /** + * Generates a JsonArray containing the documentation JsonObjects for each syntax element in the iterator + * @param infos the syntax elements to generate documentation for + * @return a JsonArray containing the documentation JsonObjects for each syntax element + */ + private > JsonArray generateSyntaxElementArray(Iterator infos) { + JsonArray syntaxArray = new JsonArray(); + infos.forEachRemaining(info -> { + JsonObject syntaxJsonObject = generatedAnnotatedElement(info); + if (syntaxJsonObject != null) + syntaxArray.add(syntaxJsonObject); + }); + return syntaxArray; + } + + /** + * Generates the documentation JsonObject for a classinfo + * @param classInfo the ClassInfo to generate the documentation of + * @return the documentation Jsonobject of the ClassInfo + */ + private @Nullable JsonObject generateClassInfoElement(ClassInfo classInfo) { + if (!classInfo.hasDocs()) + return null; + JsonObject syntaxJsonObject = new JsonObject(); + syntaxJsonObject.addProperty("id", DocumentationIdProvider.getId(classInfo)); + syntaxJsonObject.addProperty("name", getClassInfoName(classInfo)); + syntaxJsonObject.addProperty("since", classInfo.getSince()); + syntaxJsonObject.add("description", convertToJsonArray(classInfo.getDescription())); + syntaxJsonObject.add("examples", convertToJsonArray(classInfo.getExamples())); + syntaxJsonObject.add("patterns", convertToJsonArray(classInfo.getUsage())); + return syntaxJsonObject; + } + + /** + * Generates a JsonArray containing the documentation JsonObjects for each classinfo in the iterator + * @param classInfos the classinfos to generate documentation for + * @return a JsonArray containing the documentation JsonObjects for each classinfo + */ + private JsonArray generateClassInfoArray(Iterator> classInfos) { + JsonArray syntaxArray = new JsonArray(); + classInfos.forEachRemaining(classInfo -> { + JsonObject classInfoElement = generateClassInfoElement(classInfo); + if (classInfoElement != null) + syntaxArray.add(classInfoElement); + }); + return syntaxArray; + } + + /** + * Gets either the explicitly declared documentation name or code name of a ClassInfo + * @param classInfo the ClassInfo to get the effective name of + * @return the effective name of the ClassInfo + */ + private String getClassInfoName(ClassInfo classInfo) { + return Objects.requireNonNullElse(classInfo.getDocName(), classInfo.getCodeName()); + } + + /** + * Generates the documentation JsonObject for a JavaFunction + * @param function the JavaFunction to generate the JsonObject of + * @return the JsonObject of the JavaFunction + */ + private JsonObject generateFunctionElement(JavaFunction function) { + JsonObject functionJsonObject = new JsonObject(); + functionJsonObject.addProperty("id", DocumentationIdProvider.getId(function)); + functionJsonObject.addProperty("name", function.getName()); + functionJsonObject.addProperty("since", function.getSince()); + functionJsonObject.add("description", convertToJsonArray(function.getDescription())); + functionJsonObject.add("examples", convertToJsonArray(function.getExamples())); + + ClassInfo returnType = function.getReturnType(); + if (returnType != null) { + functionJsonObject.addProperty("return-type", getClassInfoName(returnType)); + } + + String functionSignature = function.getSignature().toString(false, false); + functionJsonObject.add("patterns", convertToJsonArray(new String[] { functionSignature })); + return functionJsonObject; + } + + /** + * Generates a JsonArray containing the documentation JsonObjects for each function in the iterator + * @param functions the functions to generate documentation for + * @return a JsonArray containing the documentation JsonObjects for each function + */ + private JsonArray generateFunctionArray(Iterator> functions) { + JsonArray syntaxArray = new JsonArray(); + functions.forEachRemaining(function -> syntaxArray.add(generateFunctionElement(function))); + return syntaxArray; + } + + /** + * Writes the documentation JsonObject to an output path + * @param outputPath the path to write the documentation to + * @param jsonDocs the documentation JsonObject + */ + private void saveDocs(Path outputPath, JsonObject jsonDocs) { + try { + Gson jsonGenerator = new GsonBuilder().disableHtmlEscaping().setPrettyPrinting().create(); + Files.writeString(outputPath, jsonGenerator.toJson(jsonDocs)); + } catch (IOException exception) { + //noinspection ThrowableNotThrown + Skript.exception(exception, "An error occurred while trying to generate JSON documentation"); + } + } + + @Override + public void generate() { + JsonObject jsonDocs = new JsonObject(); + + jsonDocs.add("skriptVersion", new JsonPrimitive(Skript.getVersion().toString())); + jsonDocs.add("conditions", generateSyntaxElementArray(Skript.getConditions().iterator())); + jsonDocs.add("effects", generateSyntaxElementArray(Skript.getEffects().iterator())); + jsonDocs.add("expressions", generateSyntaxElementArray(Skript.getExpressions())); + jsonDocs.add("events", generateStructureElementArray(Skript.getEvents().iterator())); + jsonDocs.add("classes", generateClassInfoArray(Classes.getClassInfos().iterator())); + + Stream> structuresExcludingEvents = Skript.getStructures().stream() + .filter(structureInfo -> !(structureInfo instanceof SkriptEventInfo)); + jsonDocs.add("structures", generateStructureElementArray(structuresExcludingEvents.iterator())); + jsonDocs.add("sections", generateSyntaxElementArray(Skript.getSections().iterator())); + + jsonDocs.add("functions", generateFunctionArray(Functions.getJavaFunctions().iterator())); + + saveDocs(outputDir.toPath().resolve("docs.json"), jsonDocs); + } + +} diff --git a/src/main/java/ch/njol/skript/lang/function/Parameter.java b/src/main/java/ch/njol/skript/lang/function/Parameter.java index deee1d1593e..87707ff7b05 100644 --- a/src/main/java/ch/njol/skript/lang/function/Parameter.java +++ b/src/main/java/ch/njol/skript/lang/function/Parameter.java @@ -199,7 +199,11 @@ public boolean isSingleValue() { @Override public String toString() { - return name + ": " + Utils.toEnglishPlural(type.getCodeName(), !single) + (def != null ? " = " + def.toString(null, true) : ""); + return toString(Skript.debug()); + } + + public String toString(boolean debug) { + return name + ": " + Utils.toEnglishPlural(type.getCodeName(), !single) + (def != null ? " = " + def.toString(null, debug) : ""); } } diff --git a/src/main/java/ch/njol/skript/lang/function/Signature.java b/src/main/java/ch/njol/skript/lang/function/Signature.java index e7e8477d633..a4e4b05cd56 100644 --- a/src/main/java/ch/njol/skript/lang/function/Signature.java +++ b/src/main/java/ch/njol/skript/lang/function/Signature.java @@ -18,7 +18,9 @@ */ package ch.njol.skript.lang.function; +import ch.njol.skript.Skript; import ch.njol.skript.classes.ClassInfo; +import ch.njol.skript.util.Utils; import ch.njol.skript.util.Contract; import org.jetbrains.annotations.Nullable; @@ -171,5 +173,34 @@ public int getMinParameters() { public int hashCode() { return name.hashCode(); } - + + @Override + public String toString() { + return toString(true, Skript.debug()); + } + + public String toString(boolean includeReturnType, boolean debug) { + StringBuilder signatureBuilder = new StringBuilder(); + + if (local) + signatureBuilder.append("local "); + signatureBuilder.append(name); + + signatureBuilder.append('('); + int lastParameterIndex = parameters.length - 1; + for (int i = 0; i < parameters.length; i++) { + signatureBuilder.append(parameters[i].toString(debug)); + if (i != lastParameterIndex) + signatureBuilder.append(", "); + } + signatureBuilder.append(')'); + + if (includeReturnType && returnType != null) { + signatureBuilder.append(" :: "); + signatureBuilder.append(Utils.toEnglishPlural(returnType.getCodeName(), !single)); + } + + return signatureBuilder.toString(); + } + }