/* * 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 <http://www.gnu.org/licenses/>. * * * Copyright 2011, 2012 Peter Güttinger * */ package ch.njol.skript.doc; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.eclipse.jdt.annotation.Nullable; import ch.njol.skript.Skript; import ch.njol.skript.classes.ClassInfo; import ch.njol.skript.conditions.CondCompare; import ch.njol.skript.lang.ExpressionInfo; import ch.njol.skript.lang.SkriptEventInfo; 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 ch.njol.skript.util.Utils; import ch.njol.util.Callback; import ch.njol.util.NonNullPair; import ch.njol.util.StringUtils; import ch.njol.util.coll.CollectionUtils; import ch.njol.util.coll.iterator.IteratorIterable; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; /** * TODO list special expressions for events and event values * TODO compare doc in code with changed one of the webserver and warn about differences? * * @author Peter Güttinger */ @SuppressFBWarnings("ES_COMPARING_STRINGS_WITH_EQ") public class Documentation { public final static void generate() { if (!generate) return; try { final PrintWriter pw = new PrintWriter(new OutputStreamWriter(new FileOutputStream(new File(Skript.getInstance().getDataFolder(), "doc.sql")), "UTF-8")); asSql(pw); pw.flush(); pw.close(); } catch (final FileNotFoundException e) { e.printStackTrace(); return; } catch (final UnsupportedEncodingException e) { e.printStackTrace(); return; } } public final static boolean generate = Skript.testing() && new File(Skript.getInstance().getDataFolder(), "generate-doc").exists(); // don't generate the documentation on normal servers private final static void asSql(final PrintWriter pw) { pw.println("-- syntax elements"); // pw.println("DROP TABLE IF EXISTS syntax_elements;"); pw.println("CREATE TABLE IF NOT EXISTS syntax_elements (" + "id VARCHAR(20) NOT NULL PRIMARY KEY," + "name VARCHAR(100) NOT NULL," + "type ENUM('condition','effect','expression','event') NOT NULL," + "patterns VARCHAR(2000) NOT NULL," + "description VARCHAR(2000) NOT NULL," + "examples VARCHAR(2000) NOT NULL," + "since VARCHAR(100) NOT NULL" + ");"); pw.println("UPDATE syntax_elements SET patterns='';"); pw.println(); pw.println("-- expressions"); for (final ExpressionInfo<?, ?> e : new IteratorIterable<ExpressionInfo<?, ?>>(Skript.getExpressions())) { assert e != null; insertSyntaxElement(pw, e, "expression"); } pw.println(); pw.println("-- effects"); for (final SyntaxElementInfo<?> info : Skript.getEffects()) { assert info != null; insertSyntaxElement(pw, info, "effect"); } pw.println(); pw.println("-- conditions"); for (final SyntaxElementInfo<?> info : Skript.getConditions()) { assert info != null; insertSyntaxElement(pw, info, "condition"); } pw.println(); pw.println("-- events"); for (final SkriptEventInfo<?> info : Skript.getEvents()) { assert info != null; insertEvent(pw, info); } pw.println(); pw.println(); pw.println("-- classes"); // pw.println("DROP TABLE IF EXISTS classes;"); pw.println("CREATE TABLE IF NOT EXISTS classes (" + "id VARCHAR(20) NOT NULL PRIMARY KEY," + "name VARCHAR(100) NOT NULL," + "description VARCHAR(2000) NOT NULL," + "patterns VARCHAR(2000) NOT NULL," + "`usage` VARCHAR(2000) NOT NULL," + "examples VARCHAR(2000) NOT NULL," + "since VARCHAR(100) NOT NULL" + ");"); pw.println("UPDATE classes SET patterns='';"); pw.println(); for (final ClassInfo<?> info : Classes.getClassInfos()) { assert info != null; insertClass(pw, info); } pw.println(); pw.println(); pw.println("-- functions"); pw.println("CREATE TABLE IF NOT EXISTS functions (" + "name VARCHAR(100) NOT NULL," + "parameters VARCHAR(2000) NOT NULL," + "description VARCHAR(2000) NOT NULL," + "examples VARCHAR(2000) NOT NULL," + "since VARCHAR(100) NOT NULL" + ");"); for (final JavaFunction<?> func : Functions.getJavaFunctions()) { assert func != null; insertFunction(pw, func); } } private final static String convertRegex(final String regex) { if (StringUtils.containsAny(regex, ".[]\\*+")) Skript.error("Regex '" + regex + "' contains unconverted Regex syntax"); return escapeHTML("" + regex .replaceAll("\\((.+?)\\)\\?", "[$1]") .replaceAll("(.)\\?", "[$1]")); } private final static String cleanPatterns(final String patterns) { final String s = StringUtils.replaceAll("" + escapeHTML(patterns) // escape HTML .replaceAll("(?<=[\\(\\|])[-0-9]+?¦", "") // remove marks .replace("()", "") // remove empty mark setting groups (mark¦) .replaceAll("\\(([^|]+?)\\|\\)", "[$1]") // replace (mark¦x|) groups with [x] .replaceAll("\\(\\|([^|]+?)\\)", "[$1]") // dito .replaceAll("\\((.+?)\\|\\)", "[($1)]") // replace (a|b|) with [(a|b)] .replaceAll("\\(\\|(.+?)\\)", "[($1)]") // dito , "(?<!\\\\)%(.+?)(?<!\\\\)%", new Callback<String, Matcher>() { // link & fancy types @Override public String run(final Matcher m) { String s = m.group(1); if (s.startsWith("-")) s = s.substring(1); String flag = ""; if (s.startsWith("*") || s.startsWith("~")) { flag = s.substring(0, 1); s = s.substring(1); } final int a = s.indexOf("@"); if (a != -1) s = s.substring(0, a); final StringBuilder b = new StringBuilder("%"); b.append(flag); boolean first = true; for (final String c : s.split("/")) { assert c != null; if (!first) b.append("/"); first = false; final NonNullPair<String, Boolean> p = Utils.getEnglishPlural(c); final ClassInfo<?> ci = Classes.getClassInfoNoError(p.getFirst()); if (ci != null && ci.getDocName() != null && ci.getDocName() != ClassInfo.NO_DOC) { b.append("<a href='../classes/#").append(p.getFirst()).append("'>").append(ci.getName().toString(p.getSecond())).append("</a>"); } else { b.append(c); if (ci != null && ci.getDocName() != ClassInfo.NO_DOC) Skript.warning("Used class " + p.getFirst() + " has no docName/name defined"); } } return "" + b.append("%").toString(); } }); assert s != null : patterns; return s; } private final static void insertSyntaxElement(final PrintWriter pw, final SyntaxElementInfo<?> info, final String type) { if (info.c.getAnnotation(NoDoc.class) != null) return; if (info.c.getAnnotation(Name.class) == null || info.c.getAnnotation(Description.class) == null || info.c.getAnnotation(Examples.class) == null || info.c.getAnnotation(Since.class) == null) { Skript.warning("" + info.c.getSimpleName() + " is missing information"); return; } final String desc = validateHTML(StringUtils.join(info.c.getAnnotation(Description.class).value(), "<br/>"), type + "s"); final String since = validateHTML(info.c.getAnnotation(Since.class).value(), type + "s"); if (desc == null || since == null) { Skript.warning("" + info.c.getSimpleName() + "'s description or 'since' is invalid"); return; } final String patterns = cleanPatterns(StringUtils.join(info.patterns, "\n", 0, info.c == CondCompare.class ? 8 : info.patterns.length)); insertOnDuplicateKeyUpdate(pw, "syntax_elements", "id, name, type, patterns, description, examples, since", "patterns = TRIM(LEADING '\n' FROM CONCAT(patterns, '\n', '" + escapeSQL(patterns) + "'))", escapeHTML("" + info.c.getSimpleName()), escapeHTML(info.c.getAnnotation(Name.class).value()), type, patterns, desc, escapeHTML(StringUtils.join(info.c.getAnnotation(Examples.class).value(), "\n")), since); } private final static void insertEvent(final PrintWriter pw, final SkriptEventInfo<?> info) { if (info.getDescription() == SkriptEventInfo.NO_DOC) return; if (info.getDescription() == null || info.getExamples() == null || info.getSince() == null) { Skript.warning("" + info.getName() + " (" + info.c.getSimpleName() + ") is missing information"); return; } for (final SkriptEventInfo<?> i : Skript.getEvents()) { if (info.getId().equals(i.getId()) && info != i && i.getDescription() != null && i.getDescription() != SkriptEventInfo.NO_DOC) { Skript.warning("Duplicate event id '" + info.getId() + "'"); return; } } final String desc = validateHTML(StringUtils.join(info.getDescription(), "<br/>"), "events"); final String since = validateHTML(info.getSince(), "events"); if (desc == null || since == null) { Skript.warning("description or 'since' of " + info.getName() + " (" + info.c.getSimpleName() + ") is invalid"); return; } final String patterns = cleanPatterns(info.getName().startsWith("On ") ? "[on] " + StringUtils.join(info.patterns, "\n[on] ") : StringUtils.join(info.patterns, "\n")); insertOnDuplicateKeyUpdate(pw, "syntax_elements", "id, name, type, patterns, description, examples, since", "patterns = '" + escapeSQL(patterns) + "'", escapeHTML(info.getId()), escapeHTML(info.getName()), "event", patterns, desc, escapeHTML(StringUtils.join(info.getExamples(), "\n")), since); } private final static void insertClass(final PrintWriter pw, final ClassInfo<?> info) { if (info.getDocName() == ClassInfo.NO_DOC) return; if (info.getDocName() == null || info.getDescription() == null || info.getUsage() == null || info.getExamples() == null || info.getSince() == null) { Skript.warning("Class " + info.getCodeName() + " is missing information"); return; } final String desc = validateHTML(StringUtils.join(info.getDescription(), "<br/>"), "classes"); final String usage = validateHTML(StringUtils.join(info.getUsage(), "<br/>"), "classes"); final String since = info.getSince() == null ? "" : validateHTML(info.getSince(), "classes"); if (desc == null || usage == null || since == null) { Skript.warning("Class " + info.getCodeName() + "'s description, usage or 'since' is invalid"); return; } final String patterns = info.getUserInputPatterns() == null ? "" : convertRegex(StringUtils.join(info.getUserInputPatterns(), "\n")); insertOnDuplicateKeyUpdate(pw, "classes", "id, name, description, patterns, `usage`, examples, since", "patterns = TRIM(LEADING '\n' FROM CONCAT(patterns, '\n', '" + escapeSQL(patterns) + "'))", escapeHTML(info.getCodeName()), escapeHTML(info.getDocName()), desc, patterns, usage, escapeHTML(StringUtils.join(info.getExamples(), "\n")), since); } private final static void insertFunction(final PrintWriter pw, final JavaFunction<?> func) { final StringBuilder params = new StringBuilder(); for (final Parameter<?> p : func.getParameters()) { if (params.length() != 0) params.append(", "); params.append(p.toString()); } final String desc = validateHTML(StringUtils.join(func.getDescription(), "<br/>"), "functions"); final String since = validateHTML(func.getSince(), "functions"); if (desc == null || since == null) { Skript.warning("Function " + func.getName() + "'s description or 'since' is invalid"); return; } replaceInto(pw, "functions", "name, parameters, description, examples, since", escapeHTML(func.getName()), escapeHTML(params.toString()), desc, escapeHTML(StringUtils.join(func.getExamples(), "\n")), since); } private final static void insertOnDuplicateKeyUpdate(final PrintWriter pw, final String table, final String fields, final String update, final String... values) { for (int i = 0; i < values.length; i++) values[i] = escapeSQL("" + values[i]); pw.println("INSERT INTO " + table + " (" + fields + ") VALUES ('" + StringUtils.join(values, "','") + "') ON DUPLICATE KEY UPDATE " + update + ";"); } private final static void replaceInto(final PrintWriter pw, final String table, final String fields, final String... values) { for (int i = 0; i < values.length; i++) values[i] = escapeSQL("" + values[i]); pw.println("REPLACE INTO " + table + " (" + fields + ") VALUES ('" + StringUtils.join(values, "','") + "');"); } private static ArrayList<Pattern> validation = new ArrayList<Pattern>(); static { validation.add(Pattern.compile("<" + "(?!a href='|/a>|br ?/|/?(i|b|u|code|pre|ul|li|em)>)")); validation.add(Pattern.compile("(?<!</a|'|br ?/|/?(i|b|u|code|pre|ul|li|em))" + ">")); } private final static String[] urls = {"expressions", "effects", "conditions"}; @Nullable private final static String validateHTML(@Nullable String html, final String baseURL) { if (html == null) { assert false; return null; } for (final Pattern p : validation) { if (p.matcher(html).find()) return null; } html = "" + html.replaceAll("&(?!(amp|lt|gt|quot);)", "&"); final Matcher m = Pattern.compile("<a href='(.*?)'>").matcher(html); linkLoop: while (m.find()) { final String url = m.group(1); final String[] s = url.split("#"); if (s.length == 1) continue; if (s[0].isEmpty()) s[0] = "../" + baseURL + "/"; if (s[0].startsWith("../") && s[0].endsWith("/")) { if (s[0].equals("../classes/")) { if (Classes.getClassInfoNoError(s[1]) != null) continue; } else if (s[0].equals("../events/")) { for (final SkriptEventInfo<?> i : Skript.getEvents()) { if (s[1].equals(i.getId())) continue linkLoop; } } else if (s[0].equals("../functions/")) { if (Functions.getFunction("" + s[1]) != null) continue; } else { final int i = CollectionUtils.indexOf(urls, s[0].substring("../".length(), s[0].length() - 1)); if (i != -1) { try { Class.forName("ch.njol.skript." + urls[i] + "." + s[1]); continue; } catch (final ClassNotFoundException e) {} } } } Skript.warning("invalid link '" + url + "' found in '" + html + "'"); } return html; } private final static String escapeSQL(final String s) { return "" + s.replace("'", "\\'").replace("\"", "\\\""); } public final static String escapeHTML(final @Nullable String s) { if (s == null) { assert false; return ""; } return "" + s.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """); } }