fallbackPaths() {
+ return Collections.unmodifiableMap(fallbackPaths);
+ }
+ }
+}
diff --git a/src/main/java/dev/ebullient/convert/io/JavadocIgnore.java b/src/main/java/dev/ebullient/convert/io/JavadocIgnore.java
new file mode 100644
index 000000000..68bddf7fe
--- /dev/null
+++ b/src/main/java/dev/ebullient/convert/io/JavadocIgnore.java
@@ -0,0 +1,12 @@
+package dev.ebullient.convert.io;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.SOURCE)
+@Target({ ElementType.METHOD, ElementType.TYPE })
+public @interface JavadocIgnore {
+
+}
diff --git a/src/main/java/dev/ebullient/convert/io/JavadocVerbatim.java b/src/main/java/dev/ebullient/convert/io/JavadocVerbatim.java
new file mode 100644
index 000000000..3d86cf3d6
--- /dev/null
+++ b/src/main/java/dev/ebullient/convert/io/JavadocVerbatim.java
@@ -0,0 +1,13 @@
+package dev.ebullient.convert.io;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.SOURCE)
+@Target({ ElementType.METHOD })
+public @interface JavadocVerbatim {
+ // include this method using the exact method or field name
+
+}
diff --git a/src/main/java/dev/ebullient/convert/io/MarkdownDoclet.java b/src/main/java/dev/ebullient/convert/io/MarkdownDoclet.java
index 85755d5d4..5b9b0a51e 100644
--- a/src/main/java/dev/ebullient/convert/io/MarkdownDoclet.java
+++ b/src/main/java/dev/ebullient/convert/io/MarkdownDoclet.java
@@ -15,10 +15,19 @@
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.lang.model.SourceVersion;
-import javax.lang.model.element.*;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.ElementKind;
+import javax.lang.model.element.Modifier;
+import javax.lang.model.element.Name;
+import javax.lang.model.element.NestingKind;
+import javax.lang.model.element.PackageElement;
+import javax.lang.model.element.QualifiedNameable;
+import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
@@ -41,6 +50,8 @@
import jdk.javadoc.doclet.Reporter;
public class MarkdownDoclet implements Doclet {
+ Pattern preformattedText = Pattern.compile("```|");
+
Reporter reporter;
DocletEnvironment environment;
Path outputDirectory;
@@ -65,7 +76,7 @@ public String getDescription() {
@Override
public Kind getKind() {
- return Doclet.Option.Kind.STANDARD;
+ return Kind.OTHER;
}
@Override
@@ -112,7 +123,10 @@ public Set extends Option> getSupportedOptions() {
@Override
public boolean run(DocletEnvironment environment) {
try {
+ System.out.println("TTRPG Convert Cli Markdown Doclet: run begin");
+ System.out.println("target: " + targetDir.getValue());
processFiles(environment);
+ System.out.println("TTRPG Convert Cli Markdown Doclet: run end");
} catch (final Exception e) {
reporter.print(Diagnostic.Kind.ERROR, e.getMessage());
e.printStackTrace();
@@ -138,37 +152,54 @@ protected void processFiles(DocletEnvironment environment) throws IOException {
Set extends Element> elements = environment.getIncludedElements();
- Map> innerClasses = ElementFilter.typesIn(elements).stream()
- .filter(t -> t.getKind() != ElementKind.INTERFACE)
- .filter(t -> t.getNestingKind() != NestingKind.TOP_LEVEL)
- .filter(t -> !isExcluded(t))
- .collect(Collectors.groupingBy(t -> (TypeElement) t.getEnclosingElement()));
-
- for (TypeElement t : innerClasses.keySet()) {
- String reference = t.getQualifiedName().toString();
- classNameMapping.put(reference, reference + ".README");
- }
+ // Find TOP_LEVEL elements that enclose (interesting) others
+ ElementFilter.typesIn(elements).stream()
+ .filter(t -> isQute(t)) // only include template-related classes
+ .filter(t -> !isIgnored(t)) // skip @JavadocIgnore and Builder classes
+ .filter(t -> t.getNestingKind() != NestingKind.TOP_LEVEL) // find nested elements
+ .filter(t -> t.getKind() != ElementKind.INTERFACE) // skip inner interfaces
+ .map(TypeElement::getEnclosingElement) // map to enclosing element
+ .distinct() // remove duplicates
+ .forEach(te -> {
+ // Append "README" to the class name to generate a README file
+ // inside the directory for GH-based documentation
+ String reference = te.toString();
+ classNameMapping.put(reference, reference + ".README");
+ });
// Print package indexes (README.md)
packages = ElementFilter.packagesIn(elements);
for (PackageElement p : packages) {
- writeReadmeFile(docTrees, p);
+ if (isQute(p) && !isIgnored(p)) {
+ writeReadmeFile(docTrees, p);
+ }
}
for (TypeElement t : ElementFilter.typesIn(elements)) {
- if (t.getKind() == ElementKind.INTERFACE) {
+ if (!isQute(t) || isExcluded(t)) {
continue;
}
+ String mapping = classNameMapping.get(t.getQualifiedName().toString());
+ System.out.println(
+ t.getKind().toString().substring(0, 4)
+ + "\t" + t.getQualifiedName()
+ + (mapping == null ? "" : "\n\t-- " + mapping));
writeReferenceFile(docTrees, t);
}
}
+ private void debugFile(String type, Name name, Path outFile) {
+ // String out = outFile.toString().replace(targetDir.getValue(), "");
+ // System.out.println(type + ", " + name.toString() + " --> " + out);
+ }
+
protected void writeReferenceFile(DocTrees docTrees, TypeElement t) throws IOException {
String name = t.getSimpleName().toString();
if (name.contains("Builder")) {
return;
}
Path outFile = getOutputFile(t);
+ debugFile("reference", t.getQualifiedName(), outFile);
try (PrintWriter out = new PrintWriter(Files.newBufferedWriter(outFile))) {
Aggregator aggregator = new Aggregator();
aggregator.add("# " + name + "\n\n");
@@ -197,6 +228,7 @@ protected void writeReferenceFile(DocTrees docTrees, TypeElement t) throws IOExc
.collect(Collectors.joining(", ")));
aggregator.add("\n\n");
+ Map> recordContent = new HashMap<>();
if (t.getKind() == ElementKind.RECORD) {
// If it's a record, then we can't retrieve the attributes as Elements, so we have to parse them from
// the comment tree instead.
@@ -206,46 +238,68 @@ protected void writeReferenceFile(DocTrees docTrees, TypeElement t) throws IOExc
.map(param -> (ParamTree) param)
.filter(p -> !p.getName().toString().startsWith("_")) // fields with "_" prefix are internal
.forEach(param -> {
- aggregator.add("\n\n### " + param.getName() + "\n\n");
- aggregator.addAll(param.getDescription());
+ recordContent.put(param.getName().toString(), param.getDescription());
});
- } else {
- for (Map.Entry entry : members.entrySet()) {
- aggregator.add("\n\n### " + entry.getKey() + "\n\n");
+ }
+
+ for (Map.Entry entry : members.entrySet()) {
+ aggregator.add("\n\n### " + entry.getKey() + "\n\n");
+ var content = recordContent.get(entry.getKey());
+ if (content != null) {
+ aggregator.addAll(content);
+ } else {
aggregator.addFullBody(docTrees.getDocCommentTree(entry.getValue()));
}
}
+
out.println(aggregator);
+ out.flush();
}
}
protected void processElement(DocTrees docTrees, Map members, Element e) {
String name = e.getSimpleName().toString();
ElementKind kind = e.getKind();
- if (!e.getModifiers().stream().anyMatch(m -> m == Modifier.PUBLIC)
- || e.getModifiers().stream().anyMatch(m -> m == Modifier.STATIC)) {
+
+ if (isIgnored(e)) {
+ // Return early if the element is annotated with @JavadocIgnore
return;
}
- if (kind == ElementKind.METHOD) {
- if (!name.startsWith("get") && !name.startsWith("is")) {
+
+ if (!isIncludedVerbatim(e)) {
+ // If the element is not annotated with @JavadocVerbatim,
+ // filter and format the element name
+
+ if (!e.getModifiers().stream().anyMatch(m -> m == Modifier.PUBLIC)
+ || e.getModifiers().stream().anyMatch(m -> m == Modifier.STATIC)) {
+ // Skip non-public and static elements
return;
}
- if (e.getAnnotation(Deprecated.class) != null) {
+ if (kind == ElementKind.METHOD) {
+ if (!name.startsWith("get") && !name.startsWith("is")) {
+ // Skip methods that don't start with "get" or "is"
+ return;
+ }
+ if (e.getAnnotation(Deprecated.class) != null) {
+ // Skip deprecated methods
+ return;
+ }
+
+ name = name.replaceFirst("(get|is)", "");
+ name = name.substring(0, 1).toLowerCase() + name.substring(1);
+ } else if (!kind.isField() && kind != ElementKind.RECORD_COMPONENT) {
+ // Skip any other non-field elements
return;
}
- } else if (!kind.isField() && kind != ElementKind.RECORD_COMPONENT) {
- return;
}
- if (kind == ElementKind.METHOD) {
- name = name.replaceFirst("(get|is)", "");
- name = name.substring(0, 1).toLowerCase() + name.substring(1);
- }
members.put(name, e);
}
void writeReadmeFile(DocTrees docTrees, PackageElement p) throws IOException {
Path outFile = getOutputFile(p);
+ debugFile("readme", p.getQualifiedName(), outFile);
+
try (PrintWriter out = new PrintWriter(Files.newBufferedWriter(outFile))) {
Aggregator aggregator = new Aggregator();
@@ -255,10 +309,10 @@ void writeReadmeFile(DocTrees docTrees, PackageElement p) throws IOException {
// Make list linking to package members
Map members = new TreeMap<>();
for (Element e : p.getEnclosedElements()) {
+ TypeElement te = (TypeElement) e;
if (isExcluded(e)) {
continue;
}
- TypeElement te = (TypeElement) e;
if (te.getKind() == ElementKind.INTERFACE) {
continue;
}
@@ -283,14 +337,36 @@ void writeReadmeFile(DocTrees docTrees, PackageElement p) throws IOException {
aggregator.add(result);
}
out.println(aggregator.toString());
+ out.flush();
}
}
+ private boolean isQute(QualifiedNameable e) {
+ return e.getQualifiedName().toString().contains("qute");
+ }
+
+ boolean isIgnored(Element element) {
+ return element.getAnnotation(JavadocIgnore.class) != null
+ || element.getSimpleName().toString().contains("Builder");
+ }
+
+ boolean isIncludedVerbatim(Element element) {
+ return element.getAnnotation(JavadocVerbatim.class) != null;
+ }
+
boolean isExcluded(Element element) {
- ElementKind kind = element.getKind();
+ if (isIncludedVerbatim(element)) {
+ return false;
+ }
+
+ boolean excludeKind = switch (element.getKind()) {
+ case CLASS, INTERFACE, RECORD, ENUM -> false;
+ default -> true;
+ };
+
return !environment.isIncluded(element)
- || element.getSimpleName().toString().contains("Builder")
- || (kind != ElementKind.CLASS && kind != ElementKind.INTERFACE && kind != ElementKind.ENUM);
+ || isIgnored(element)
+ || excludeKind;
}
String getDescription(DocTrees docTrees, TypeElement te) {
@@ -327,11 +403,11 @@ static TypeElement getSuperclassElement(TypeElement typeElement) {
}
String qualifiedNameToPath(QualifiedNameable element) {
- String reference = element.getQualifiedName().toString();
- return qualifiedNameToPath(classNameMapping.getOrDefault(reference, reference));
+ return qualifiedNameToPath(element.getQualifiedName().toString());
}
String qualifiedNameToPath(String reference) {
+ reference = classNameMapping.getOrDefault(reference, reference);
if (reference.endsWith("qute")) {
reference += ".README";
} else if (!isValidClass(reference.replace(".README", ""))) {
@@ -373,7 +449,17 @@ void addAll(List extends DocTree> docTrees) {
void add(DocTree docTree) {
switch (docTree.getKind()) {
case TEXT:
- add(((TextTree) docTree).getBody().toString().replace("\n", ""));
+ // Always remove single leading javadoc space
+ String text = ((TextTree) docTree).getBody().toString()
+ .replaceAll("\n ", "\n")
+ .replaceAll("\n\n\n", "\n\n"); // consolidate extra lines
+
+ Matcher m = preformattedText.matcher(text);
+ if (!m.find()) {
+ // if there isn't any pre-formatted text, remove any other leading whitespace
+ text = text.replaceAll("\n +", "\n");
+ }
+ add(text);
break;
case CODE:
case LITERAL:
@@ -409,8 +495,18 @@ void add(DocTree docTree) {
reference = qualifiedNameToPath(reference);
if (!reference.startsWith("http")) {
Path target = outputDirectory.resolve(reference);
- Path relative = currentResource.getParent().relativize(target);
- reference = relative.toString();
+ if (target.equals(currentResource)) {
+ reference = "";
+ } else {
+ Path relative = currentResource.getParent().relativize(target);
+ reference = relative.toString();
+ }
+ anchor = anchor
+ .replaceFirst("^#(get|is)", "#")
+ .replace("()", "").toLowerCase();
+ label = label
+ .replaceFirst("#(get|is)", "#")
+ .replace("()", "");
}
add(String.format("[%s](%s%s)", label, reference, anchor));
break;
diff --git a/src/main/java/dev/ebullient/convert/io/MarkdownWriter.java b/src/main/java/dev/ebullient/convert/io/MarkdownWriter.java
index ef1cc5fb1..7d7bc9133 100644
--- a/src/main/java/dev/ebullient/convert/io/MarkdownWriter.java
+++ b/src/main/java/dev/ebullient/convert/io/MarkdownWriter.java
@@ -93,7 +93,7 @@ public void writeFiles(Path basePath, List elements) {
}
});
- counts.forEach((k, v) -> tui.donef("Wrote %s %s files.", v, k));
+ counts.forEach((k, v) -> tui.printlnf(Msg.OK, "Wrote %s %s files.", v, k));
}
FileMap doWrite(FileMap fileMap, T qs, Map counts) {
@@ -131,7 +131,7 @@ public void writeNotes(Path dir, Collection notes, boolean compendium)
writeNote(fd, fileName, n);
}
- tui.donef("Wrote %s notes to %s.",
+ tui.printlnf(Msg.OK, "Wrote %s notes to %s.",
notes.size(),
compendium ? "compendium" : "rules");
}
diff --git a/src/main/java/dev/ebullient/convert/io/Msg.java b/src/main/java/dev/ebullient/convert/io/Msg.java
new file mode 100644
index 000000000..57682c2a5
--- /dev/null
+++ b/src/main/java/dev/ebullient/convert/io/Msg.java
@@ -0,0 +1,56 @@
+package dev.ebullient.convert.io;
+
+public enum Msg {
+ ALLDONE(Character.toString(0x1F389)), // 🎉
+ BREW(Character.toString(0x1F37A)), // 🍺
+ CLASSES(Character.toString(0x1F913)), // 🤓
+ DEBUG(Character.toString(0x1F527), "faint"), // 🔧
+ DECK(Character.toString(0x1F0CF)), // 🃏
+ DEITY(Character.toString(0x1F47C)), // 👼
+ ERR(Character.toString(0x1F6D1) + " ERR|"), // 🛑
+ FEATURE(Character.toString(0x2B50)), // ⭐️
+ FEATURETYPE(Character.toString(0x1F31F)), // 🌟
+ FILTER(Character.toString(0x1F50D)), // 🔍
+ FOLDER(Character.toString(0x1F4C1)), // 📁
+ MULTIPLE(Character.toString(0x1F4DA)), // 📚
+ NOT_SET(Character.toString(0x1FAE5) + " "), // 🫥
+ OK(Character.toString(0x2705) + " OK|"), // ✅
+ INFO(Character.toString(0x1F537) + " INFO|"), // 🔷
+ PROGRESS(Character.toString(0x23F3)), // ⏳
+ RACES(Character.toString(0x1F4D5)), // 📕
+ REPRINT(Character.toString(0x1F4F0)), // 📰
+ SOMEDAY(Character.toString(0x1F6A7)), // 🚧
+ SOURCE(Character.toString(0x1F4D8)), // 📘
+ TARGET(Character.toString(0x1F3AF)), // 🎯
+ UNKNOWN(Character.toString(0x1F47B)), // 👻
+ UNRESOLVED(Character.toString(0x1FAE3)), // 🫣
+ VERBOSE(Character.toString(0x1F539), "faint"),
+ WARN(Character.toString(0x1F538) + " WARN|"),
+ NOOP("");
+
+ final String prefix;
+ final String colorPrefix;
+
+ private Msg(String prefix) {
+ this.prefix = prefix + " ";
+ this.colorPrefix = null;
+ }
+
+ private Msg(String prefix, String color) {
+ this.prefix = prefix + " ";
+ this.colorPrefix = "@|%s %s".formatted(color, prefix);
+ }
+
+ public String color(String message) {
+ if (colorPrefix != null) {
+ return colorPrefix + message + "|@";
+ }
+ return wrap(message);
+ }
+
+ public String wrap(String message) {
+ return this == NOOP
+ ? message
+ : prefix + message;
+ }
+}
diff --git a/src/main/java/dev/ebullient/convert/io/NoStackTraceException.java b/src/main/java/dev/ebullient/convert/io/NoStackTraceException.java
new file mode 100644
index 000000000..10e81ce9c
--- /dev/null
+++ b/src/main/java/dev/ebullient/convert/io/NoStackTraceException.java
@@ -0,0 +1,26 @@
+package dev.ebullient.convert.io;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class NoStackTraceException extends RuntimeException {
+ private static final long serialVersionUID = 1L;
+
+ public NoStackTraceException(Throwable cause) {
+ super(flattenMessage(cause));
+ }
+
+ @Override
+ public synchronized Throwable fillInStackTrace() {
+ return null;
+ }
+
+ private static String flattenMessage(Throwable cause) {
+ List sb = new ArrayList<>();
+ while (cause != null) {
+ sb.add(cause.toString());
+ cause = cause.getCause();
+ }
+ return String.join("\n", sb);
+ }
+}
diff --git a/src/main/java/dev/ebullient/convert/io/Templates.java b/src/main/java/dev/ebullient/convert/io/Templates.java
index 3ab6eeab0..d2c127cba 100644
--- a/src/main/java/dev/ebullient/convert/io/Templates.java
+++ b/src/main/java/dev/ebullient/convert/io/Templates.java
@@ -44,7 +44,7 @@ private Template customTemplateOrDefault(String id) throws RuntimeException {
if (!engine.isTemplateLoaded(key)) {
Path customPath = config.getCustomTemplate(id);
if (customPath != null) {
- tui.verbosef("📝 %s template: %s", id, customPath);
+ tui.infof("%25s: %s", id, customPath);
try {
Template template = engine.parse(Files.readString(customPath));
engine.putTemplate(key, template);
@@ -73,7 +73,7 @@ public String render(QuteBase resource) {
} catch (TemplateException tex) {
Throwable cause = tex.getCause();
String message = cause != null ? cause.toString() : tex.toString();
- tui.error(tex, message);
+ tui.errorf(tex, message);
return "%% ERROR: " + message + " %%";
}
}
@@ -87,7 +87,7 @@ public String renderInlineEmbedded(QuteUtil resource) {
} catch (TemplateException tex) {
Throwable cause = tex.getCause();
String message = cause != null ? cause.toString() : tex.toString();
- tui.error(tex, message);
+ tui.errorf(tex, message);
return "%% ERROR: " + message + " %%";
}
}
@@ -102,7 +102,7 @@ public String renderIndex(String name, Collection resources) {
} catch (TemplateException tex) {
Throwable cause = tex.getCause();
String message = cause != null ? cause.toString() : tex.toString();
- tui.error(tex, message);
+ tui.errorf(tex, message);
return "%% ERROR: " + message + " %%";
}
}
@@ -121,7 +121,7 @@ public String renderCss(FontRef fontRef, InputStream data) throws IOException {
} catch (TemplateException tex) {
Throwable cause = tex.getCause();
String message = cause != null ? cause.toString() : tex.toString();
- tui.error(tex, message);
+ tui.errorf(tex, message);
return "%% ERROR: " + message + " %%";
}
}
diff --git a/src/main/java/dev/ebullient/convert/io/Tui.java b/src/main/java/dev/ebullient/convert/io/Tui.java
index e7beb9bd3..f41e75b0a 100644
--- a/src/main/java/dev/ebullient/convert/io/Tui.java
+++ b/src/main/java/dev/ebullient/convert/io/Tui.java
@@ -5,16 +5,17 @@
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.io.PrintStream;
import java.io.PrintWriter;
import java.net.URI;
import java.net.URL;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
+import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
-import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
@@ -41,8 +42,10 @@
import com.fasterxml.jackson.core.util.DefaultIndenter;
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.introspect.VisibilityChecker;
+import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactoryBuilder;
import com.github.slugify.Slugify;
@@ -79,7 +82,13 @@ public static Tui instance() {
public final static TypeReference