/*
* 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("\"", """);
}
}