diff --git a/build.gradle b/build.gradle index 4726dfc..0692d77 100644 --- a/build.gradle +++ b/build.gradle @@ -18,7 +18,14 @@ task copyDist(type: Copy) { } compileJava.dependsOn copyDist +test { + useJUnitPlatform() +} + dependencies { - compile 'commons-cli:commons-cli:1.4' - compile files("${System.properties['java.home']}/../lib/tools.jar") + implementation('commons-cli:commons-cli:1.5.0') + implementation(files("${System.properties['java.home']}/../lib/tools.jar")) + implementation("org.yaml:snakeyaml:1.30") + testImplementation(platform("org.junit:junit-bom:5.8.2")) + testImplementation("org.junit.jupiter:junit-jupiter") } \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 0d4a951..be12496 100755 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 58c760c..1c5f38d 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Thu May 25 16:36:48 SAST 2017 +#Fri Feb 11 10:50:42 SAST 2022 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-3.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip diff --git a/src/main/java/net/spandigital/presidium/ClassWriter.java b/src/main/java/net/spandigital/presidium/ClassWriter.java index 94d6266..d85ee13 100644 --- a/src/main/java/net/spandigital/presidium/ClassWriter.java +++ b/src/main/java/net/spandigital/presidium/ClassWriter.java @@ -23,19 +23,18 @@ */ public class ClassWriter { - private RootDoc root; - private Path destination; - private String sectionUrl; + private final RootDoc root; + private final Path destination; + private final String sectionUrl; private Set knownQualifiers; - public static ClassWriter init(RootDoc root, Path destination, String sectionUrl) { - ClassWriter writer = new ClassWriter(); - writer.root = root; - writer.destination = destination; - writer.sectionUrl = sectionUrl; - return writer; + public ClassWriter(RootDoc root, Path destination, String sectionUrl) { + this.root = root; + this.destination = destination; + this.sectionUrl = sectionUrl; } + public void writeAll() throws IOException { List classes = Arrays.stream(root.classes()) @@ -58,7 +57,7 @@ public void writeAll() throws IOException { public void write(Path file, ClassDoc cls) throws IOException { FileWriter.write(file, - Markdown.join( + Markdown.lines( frontMatter(cls.name()), header(cls), newLine(), @@ -74,7 +73,7 @@ public void write(Path file, ClassDoc cls) throws IOException { private String header(ClassDoc cls) { String containingPackage = cls.containingPackage().name(); - return Markdown.join( + return Markdown.lines( anchor(cls), Markdown.siteLink(containingPackage, sectionUrl + "/packages/#" + containingPackage), h1(title(cls)) @@ -118,7 +117,7 @@ private static ClassType classType(ClassDoc cls) { private String constructorList(ClassDoc cls) { return cls.constructors().length == 0 ? "" : - Markdown.join( + Markdown.lines( h1("Constructor Summary"), tableHeader("Modifiers", "Constructor"), Arrays.stream(cls.constructors()) @@ -131,7 +130,7 @@ private String constructorList(ClassDoc cls) { private String methodList(ClassDoc cls) { return (cls.methods().length == 0) ? "" : - Markdown.join( + Markdown.lines( h1("Method Summary"), tableHeader("Modifiers", "Return", "Method"), Arrays.stream(cls.methods()) @@ -145,7 +144,7 @@ private String methodList(ClassDoc cls) { private String classDescription(ClassDoc cls) { String comment = docComment(cls); return (comment.length() == 0) ? "" : - Markdown.join( + Markdown.lines( h1("Description"), comment ); @@ -171,7 +170,7 @@ private String methodLink(ExecutableMemberDoc method) { private String constructorDetail(ClassDoc cls) { return cls.constructors().length == 0 ? "" : - Markdown.join( + Markdown.lines( h1("Constructor Detail"), Arrays.stream(cls.constructors()) .map(c -> memberDetail(c)) @@ -182,7 +181,7 @@ private String constructorDetail(ClassDoc cls) { private String methodDetail(ClassDoc cls) { return cls.methods().length == 0 ? "" : - Markdown.join( + Markdown.lines( h1("Method Detail"), anchor(cls), Arrays.stream(cls.methods()) @@ -193,7 +192,7 @@ private String methodDetail(ClassDoc cls) { } private String memberDetail(ExecutableMemberDoc member) { - return Markdown.join( + return Markdown.lines( hr(), anchor(member), h2(member.name()), diff --git a/src/main/java/net/spandigital/presidium/Doclet.java b/src/main/java/net/spandigital/presidium/Doclet.java index afa0414..3a217e9 100644 --- a/src/main/java/net/spandigital/presidium/Doclet.java +++ b/src/main/java/net/spandigital/presidium/Doclet.java @@ -12,6 +12,7 @@ import java.nio.file.Path; import java.nio.file.Paths; +@SuppressWarnings("unused") public class Doclet { private static final List opts = List.of("-d", "-t", "-u");//, "-doctitle", "-windowtitle"); @@ -20,17 +21,15 @@ public static boolean start(RootDoc root) throws IOException { Path destination = Paths.get(option(root, "-d", "docs")); String title = option(root, "-t", "javadoc"); String url = option(root, "-u", "reference/javadoc"); - clean(destination); - Files.createDirectories(destination); FileWriter.writeIndex(destination, title); - PackageWriter.init(root, destination.resolve("01-Packages"), url).writeAll(); - ClassWriter.init(root, destination.resolve("02-Classes"), url).writeAll(); - + new PackageWriter(root, destination.resolve("packages"), url).writeAll(); + new ClassWriter(root, destination.resolve("classes"), url).writeAll(); return true; } + /** * Allow custom doclet opts * @@ -54,7 +53,7 @@ private static String option(RootDoc root, String option, String defaultValue) { } private static void clean(Path target) throws IOException { - System.out.println(String.format("Cleaning: %s", target)); + System.out.printf("Cleaning: %s%n", target); if (!Files.exists(target)) { return; } diff --git a/src/main/java/net/spandigital/presidium/FileWriter.java b/src/main/java/net/spandigital/presidium/FileWriter.java index 8c7df6f..65b60a7 100644 --- a/src/main/java/net/spandigital/presidium/FileWriter.java +++ b/src/main/java/net/spandigital/presidium/FileWriter.java @@ -4,6 +4,9 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.function.Consumer; + +import static net.spandigital.presidium.Markdown.slugify; /** * Created by paco on 2017/05/26. @@ -11,15 +14,14 @@ public class FileWriter { public static String fileName(int order, String name) { - return String.format("%03d-%s.md", order, name); + return String.format("%s.md", name); } public static void writeIndex(Path path, String title) { - write(path.resolve("index.md"), Markdown.frontMatter(title)); - } - - public static void write(Path file, StringBuffer content) { - write(file, content.toString()); + Markdown.editFrontMapper(path.resolve("index.md"), (a) -> { + a.put("title", title); + a.put("slug", slugify(title)); + }); } public static void write(Path file, String content) { diff --git a/src/main/java/net/spandigital/presidium/IO.java b/src/main/java/net/spandigital/presidium/IO.java new file mode 100644 index 0000000..f2027c8 --- /dev/null +++ b/src/main/java/net/spandigital/presidium/IO.java @@ -0,0 +1,62 @@ +package net.spandigital.presidium; + +import java.io.*; + +public final class IO { + + public static final int DEFAULT_BUFFER_SIZE = 8192; + + private IO() { + } + + public static int skipLines(RandomAccessFile f, int n) throws IOException { + + if (n < 1) { + return 0; + } + + while (n > 0) { + if (f.readLine() == null) { + break; + } + n -= 1; + } + + return n; + } + + public static void copy(File source, RandomAccessFile dest) throws IOException { + byte[] buff = new byte[DEFAULT_BUFFER_SIZE]; + try (BufferedInputStream ins = new BufferedInputStream(new FileInputStream(source), DEFAULT_BUFFER_SIZE)) { + while (true) { + int n = ins.read(buff); + if (n == -1) break; + dest.write(buff, 0, n); + } + } + } + + /** + * Wraps any exception due to an IO error as a runtime exception + */ + public static class Exception extends RuntimeException { + public Exception(String message, IOException cause) { + super(message); + initCause(cause); + } + } + + public static String readText(File file) throws IOException { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + try(InputStream fs = new BufferedInputStream(new FileInputStream(file), DEFAULT_BUFFER_SIZE)) { + byte[] buff = new byte[DEFAULT_BUFFER_SIZE]; + while (true) { + int n = fs.read(buff); + if (n == -1) break; + bytes.write(buff, 0, n); + } + } + return bytes.toString("utf-8"); + } + +} diff --git a/src/main/java/net/spandigital/presidium/Markdown.java b/src/main/java/net/spandigital/presidium/Markdown.java index 53c79be..14fef33 100644 --- a/src/main/java/net/spandigital/presidium/Markdown.java +++ b/src/main/java/net/spandigital/presidium/Markdown.java @@ -2,31 +2,142 @@ import com.sun.javadoc.Doc; import com.sun.javadoc.ProgramElementDoc; - -import java.io.BufferedReader; -import java.io.StringReader; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.stream.Collectors; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.Yaml; + +import java.io.FileWriter; +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.function.Consumer; + +import static java.lang.String.format; +import static java.util.stream.Collectors.joining; +import static net.spandigital.presidium.IO.copy; +import static net.spandigital.presidium.IO.skipLines; +import static net.spandigital.presidium.Paths.requiredFile; /** * Markdown generation methods. + * * @author Paco Mendes */ public class Markdown { - public static String frontMatter(String title) { - return Markdown.join( - "---", - "title: " + title, - "---", - Markdown.newLine() + public static final String FRONT_MATTER_DELIMITER = "---"; + + static Yaml yaml() { + DumperOptions d = new DumperOptions(); + d.setPrettyFlow(true); + d.setExplicitStart(true); + d.setExplicitEnd(false); + d.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); + return new Yaml(d); + } + + + @SuppressWarnings("ResultOfMethodCallIgnored") + public static void editFrontMapper(Path path, Consumer> editor) { + + File file = requiredFile(path); + Yaml yaml = yaml(); + Map frontMatter = null; + String frontMatterContent = null; + int frontMatterRows = 0; + String updatedFrontMatter = null; + + try (RandomAccessFile source = new RandomAccessFile(file, "rw")) { + + StringBuilder sb = new StringBuilder(); + String line = source.readLine(); + boolean readingFrontMatter = line != null && (line.trim()).equals(FRONT_MATTER_DELIMITER); + boolean frontMatterFound = false; + + if (readingFrontMatter) { + while (true) { + line = source.readLine(); + if (line == null) break; + line = line.trim(); + if (line.equals(FRONT_MATTER_DELIMITER)) { + frontMatterFound = true; + break; + } + sb.append(line).append(newLine()); + ++frontMatterRows; + } + } + + String existingFrontMatter; + if (frontMatterFound) { + existingFrontMatter = sb.toString(); + frontMatter = yaml.load(existingFrontMatter); + frontMatterContent = yaml.dump(frontMatter); + frontMatterRows += 2; + } else { + frontMatterRows = 0; + frontMatter = new LinkedHashMap<>(); + } + + editor.accept(frontMatter); + updatedFrontMatter = yaml.dump(frontMatter); + + boolean updated = !Objects.equals(frontMatterContent, updatedFrontMatter); + + if (updated) { + File temp = Files.createTempFile(null, null).toFile(); + try { + try (PrintWriter edit = new PrintWriter(new BufferedWriter(new FileWriter(temp)))) { + if (frontMatter.size() > 0) { + edit.print(updatedFrontMatter); + edit.println(FRONT_MATTER_DELIMITER); + } + source.seek(0); + if (frontMatterRows > 0) { + skipLines(source, frontMatterRows); + } + while (true) { + line = source.readLine(); + if (line == null) { + break; + } + edit.println(line); + } + } + source.seek(0); + copy(temp, source); + source.setLength(temp.length()); + } finally { + temp.delete(); + } + } + + } catch (IOException e) { + throw new IO.Exception("failure editing front matter", e); + } + + + } + + public static String slugify(String str) { + return str.toLowerCase().replaceAll("\\W+", "-"); + } + + public static String frontMatter(String title, String slug) { + return Markdown.lines( + FRONT_MATTER_DELIMITER, + format("%s: \"%s\"", "title", title), + format("%s: \"%s\"", "slug", slug), + FRONT_MATTER_DELIMITER ); } - public static String join(String... elements) { - return Arrays.stream(elements).collect(Collectors.joining(newLine())); + public static String frontMatter(String title) { + return frontMatter(title, slugify(title)); + } + + public static String lines(String... lines) { + return Arrays.stream(lines).collect(joining(newLine())); } public static String h1(String title) { @@ -46,27 +157,27 @@ private static String h(int level, String title) { } public static String quote(String text) { - return String.format("%s> %s%s", Markdown.newLine(), text, Markdown.newLine()); + return format("%s> %s%s", Markdown.newLine(), text, Markdown.newLine()); } public static String hr() { - return Markdown.newLine(2) + "---" + Markdown.newLine(); + return Markdown.newLine(2) + FRONT_MATTER_DELIMITER + Markdown.newLine(); } public static String anchor(ProgramElementDoc element) { - return String.format("", element.qualifiedName()); + return format("", element.qualifiedName()); } public static String link(String value, String target) { - return String.format("[%s](%s)", value, target); + return format("[%s](%s)", value, target); } public static String siteLink(String value, String target) { - return String.format("[%s]({{'%s' | relative_url }})", value, target); + return format("[%s]({{'%s' | relative_url }})", value, target); } public static String anchorLink(String value, String target) { - return String.format("[%s](#%s)", value, target); + return format("[%s](#%s)", value, target); } public static String newLine() { @@ -78,9 +189,9 @@ public static String newLine(int count) { } public static String tableHeader(String... titles) { - StringBuffer header = new StringBuffer(); + StringBuilder header = new StringBuilder(); for (String title : titles) { - header.append(String.format("| %s ", title)); + header.append(format("| %s ", title)); } header.append(Markdown.newLine()); header.append(String.join("", Collections.nCopies(titles.length, "|:---"))); @@ -94,8 +205,8 @@ public static String tableRow(String... values) { public static String tableRow(Collection values) { return values.stream() - .map(v -> String.format("|%s ", v)) - .collect(Collectors.joining()) + Markdown.newLine(); + .map(v -> format("|%s ", v)) + .collect(joining()) + Markdown.newLine(); } public static String docSummary(Doc doc) { diff --git a/src/main/java/net/spandigital/presidium/PackageWriter.java b/src/main/java/net/spandigital/presidium/PackageWriter.java index f309a5c..c1b055f 100644 --- a/src/main/java/net/spandigital/presidium/PackageWriter.java +++ b/src/main/java/net/spandigital/presidium/PackageWriter.java @@ -21,19 +21,16 @@ */ public class PackageWriter { - private RootDoc root; - private Path destination; - private String sectionUrl; - - public static PackageWriter init(RootDoc root, Path destination, String sectionUrl) { - PackageWriter writer = new PackageWriter(); - writer.root = root; - writer.destination = destination; - writer.sectionUrl = sectionUrl; - return writer; - } + private final RootDoc root; + private final Path destination; + private final String sectionUrl; + - private PackageWriter() {} + public PackageWriter(RootDoc root, Path destination, String sectionUrl) { + this.root = root; + this.destination = destination; + this.sectionUrl = sectionUrl; + } public void writeAll() throws IOException { @@ -53,20 +50,22 @@ public void writeAll() throws IOException { String name = FileWriter.fileName(i++, pkg.name()); Path file = destination.resolve(name); writeArticle(file, pkg); + int weight = i+1; + editFrontMapper(file, fm -> fm.put("weight", weight)); } } private void writePackageList(List packages) { String name = FileWriter.fileName(0, "package-summary"); Path file = destination.resolve(name); - FileWriter.write(file, Markdown.join( + FileWriter.write(file, Markdown.lines( frontMatter("Packages"), packageList(packages) )); } private void writeArticle(Path file, PackageDoc pkg) { - FileWriter.write(file, Markdown.join( + FileWriter.write(file, Markdown.lines( frontMatter(pkg.name()), docSummary(pkg), packageClasses(pkg), @@ -77,7 +76,7 @@ private void writeArticle(Path file, PackageDoc pkg) { private static String packageList(List packages) { return packages.size() == 0 ? "" : - Markdown.join( + Markdown.lines( tableHeader("Package", "Description"), packages.stream() .sorted() @@ -96,7 +95,7 @@ private String packageClasses(PackageDoc pkg) { private String classTable(String type, ClassDoc[] classes) { return classes.length == 0 ? "" : - Markdown.join( + Markdown.lines( h1(type), tableHeader(type, "Description"), Arrays.stream(classes) diff --git a/src/main/java/net/spandigital/presidium/Paths.java b/src/main/java/net/spandigital/presidium/Paths.java new file mode 100644 index 0000000..2fe5c70 --- /dev/null +++ b/src/main/java/net/spandigital/presidium/Paths.java @@ -0,0 +1,29 @@ +package net.spandigital.presidium; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +public final class Paths { + + private Paths() {} + + public static File requiredFile(Path path) { + File file = path.toFile(); + if (!file.exists()) { + File parent = file.getParentFile(); + if (!parent.exists() && !parent.mkdirs()) { + throw new IO.Exception("failed to create path:" + path, new FileNotFoundException(file.toString())); + } + try { + Files.createFile(path); + } catch (IOException e) { + throw new IO.Exception("unable to create file: "+path, e); + } + } + return file; + } + +} diff --git a/src/test/java/net/spandigital/presidium/TestDocletMarkdown.java b/src/test/java/net/spandigital/presidium/TestDocletMarkdown.java new file mode 100644 index 0000000..6f8c738 --- /dev/null +++ b/src/test/java/net/spandigital/presidium/TestDocletMarkdown.java @@ -0,0 +1,60 @@ +package net.spandigital.presidium; + +import com.sun.tools.javac.main.JavaCompiler; +import com.sun.tools.javac.util.Context; +import com.sun.tools.javac.util.Options; +import com.sun.tools.javadoc.JavadocTool; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.io.TempDir; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Paths; + +import static net.spandigital.presidium.TestUtils.mustHaveDir; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@SuppressWarnings("unused") +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class TestDocletMarkdown { + + @TempDir + private File workDir; + private File docsDir; + private String sourcePath; + private String subpackages; + private Context context; + + + @BeforeAll + public void setupTests() { + docsDir = mustHaveDir(new File(workDir, "docs")); + sourcePath = new File(System.getProperty("user.dir"), "src/test/java").getAbsolutePath(); + subpackages = "net.spandigital.presidium.fixtures"; + assertTrue(Files.exists(Paths.get(sourcePath))); + context = new Context(); + Options compOpts = Options.instance(context); + compOpts.put("-sourcepath", sourcePath); + + JavaCompiler javadocTool = JavadocTool.instance(context); + } + + @Test + public void testMarkdown() { + assertDoesNotThrow(this::runDoclet); + } + + private void runDoclet() throws Exception { +// +// out.printf("Generating markdown docs to: %s%n", docsDir); +// Main.execute("Markdown Generator", new String[]{ +// "-sourcepath", sourcePath, +// "-subpackages", subpackages, +// "-d", docsDir.getPath() +// }); + } +} + diff --git a/src/test/java/net/spandigital/presidium/TestUtils.java b/src/test/java/net/spandigital/presidium/TestUtils.java new file mode 100644 index 0000000..6747d7e --- /dev/null +++ b/src/test/java/net/spandigital/presidium/TestUtils.java @@ -0,0 +1,16 @@ +package net.spandigital.presidium; + +import java.io.File; + +import static java.lang.String.format; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public final class TestUtils { + + public static File mustHaveDir(File dir) { + if (dir.exists()) return dir; + assertTrue(dir.mkdirs(), () -> format("unable to create directory: %s", dir)); + return dir; + } +} +