├── .gitignore ├── src └── main │ └── java │ ├── net │ └── jonathangiles │ │ └── tools │ │ └── sitebuilder │ │ ├── models │ │ ├── SiteContentStatus.java │ │ ├── Page.java │ │ ├── Post.java │ │ ├── input │ │ │ ├── InputFile.java │ │ │ ├── MarkdownFile.java │ │ │ ├── HtmlFile.java │ │ │ └── XmlFile.java │ │ └── SiteContent.java │ │ ├── util │ │ ├── FileUtils.java │ │ └── SitePaths.java │ │ └── SiteBuilder.java │ └── module-info.java ├── README.md └── pom.xml /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | /target/* 3 | .idea 4 | 5 | -------------------------------------------------------------------------------- /src/main/java/net/jonathangiles/tools/sitebuilder/models/SiteContentStatus.java: -------------------------------------------------------------------------------- 1 | package net.jonathangiles.tools.sitebuilder.models; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | public enum SiteContentStatus { 6 | 7 | @JsonProperty("publish") 8 | PUBLISH, 9 | 10 | @JsonProperty("draft") 11 | DRAFT; 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/net/jonathangiles/tools/sitebuilder/models/Page.java: -------------------------------------------------------------------------------- 1 | package net.jonathangiles.tools.sitebuilder.models; 2 | 3 | import net.jonathangiles.tools.sitebuilder.models.input.InputFile; 4 | 5 | public class Page extends SiteContent { 6 | public Page() { 7 | super(); 8 | } 9 | 10 | public Page(InputFile inputFile) { 11 | super(inputFile); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/net/jonathangiles/tools/sitebuilder/models/Post.java: -------------------------------------------------------------------------------- 1 | package net.jonathangiles.tools.sitebuilder.models; 2 | 3 | import net.jonathangiles.tools.sitebuilder.models.input.InputFile; 4 | 5 | public class Post extends SiteContent { 6 | public Post(InputFile inputFile) { 7 | super(inputFile); 8 | 9 | // by default we have posts use the post template 10 | setTemplate("post"); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Site Builder 2 | 3 | Site Builder is a simple static website generator / templating engine, used primarily (at this stage) to generate [JonathanGiles.net](http://www.jonathangiles.net). You can see how Site Builder is used to generate JonathanGiles.net by [browsing the GitHub repo](https://github.com/jonathangiles/jonathangiles.net) containing everything required to generate JonathanGiles.net. 4 | 5 | ## Releasing 6 | 7 | Releases are performed using `mvn clean deploy -Prelease`. -------------------------------------------------------------------------------- /src/main/java/module-info.java: -------------------------------------------------------------------------------- 1 | module net.jonathangiles.tools.sitebuilder { 2 | requires org.commonmark; 3 | requires org.commonmark.ext.front.matter; 4 | 5 | requires com.fasterxml.jackson.annotation; 6 | requires com.fasterxml.jackson.core; 7 | requires com.fasterxml.jackson.databind; 8 | requires com.fasterxml.jackson.datatype.jsr310; 9 | requires com.fasterxml.jackson.dataformat.xml; 10 | 11 | exports net.jonathangiles.tools.sitebuilder; 12 | exports net.jonathangiles.tools.sitebuilder.models; 13 | exports net.jonathangiles.tools.sitebuilder.models.input; 14 | exports net.jonathangiles.tools.sitebuilder.util; 15 | } -------------------------------------------------------------------------------- /src/main/java/net/jonathangiles/tools/sitebuilder/util/FileUtils.java: -------------------------------------------------------------------------------- 1 | package net.jonathangiles.tools.sitebuilder.util; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.nio.charset.StandardCharsets; 6 | import java.nio.file.Files; 7 | import java.nio.file.Path; 8 | import java.nio.file.StandardCopyOption; 9 | 10 | import static net.jonathangiles.tools.sitebuilder.util.SitePaths.createRelativePath; 11 | 12 | public class FileUtils { 13 | private FileUtils() { } 14 | 15 | public static Path getPath(String path, ClassLoader loader) { 16 | return new File(loader.getResource(path).getFile()).toPath(); 17 | } 18 | 19 | public static void writeToFile(final Path file, final String content) { 20 | try { 21 | Files.write(file, content.getBytes()); 22 | } catch (IOException e) { 23 | e.printStackTrace(); 24 | } 25 | } 26 | 27 | public static void copyFile(final Path basePath, final Path file) { 28 | try { 29 | final Path newPath = createRelativePath(basePath, file); 30 | System.out.println("Copying static file: " + newPath); 31 | newPath.toFile().mkdirs(); 32 | Files.copy(file, newPath, StandardCopyOption.REPLACE_EXISTING); 33 | } catch (IOException e) { 34 | e.printStackTrace(); 35 | } 36 | } 37 | 38 | public static String readFile(final Path file) { 39 | final StringBuilder sb = new StringBuilder(); 40 | try { 41 | Files.lines(file, StandardCharsets.UTF_8) 42 | .forEach(line -> sb.append(line).append("\n")); 43 | } catch (IOException e) { 44 | e.printStackTrace(); 45 | } 46 | return sb.toString(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/net/jonathangiles/tools/sitebuilder/models/input/InputFile.java: -------------------------------------------------------------------------------- 1 | package net.jonathangiles.tools.sitebuilder.models.input; 2 | 3 | import java.io.File; 4 | import java.nio.file.Path; 5 | import java.util.List; 6 | import java.util.Optional; 7 | 8 | public interface InputFile { 9 | 10 | String getBody(); 11 | 12 | List getFrontMatterList(String key); 13 | 14 | default Optional getFrontMatterValue(String key) { 15 | if (!hasFrontMatter(key)) { 16 | return Optional.empty(); 17 | } else { 18 | return Optional.of(getFrontMatterList(key).get(0)); 19 | } 20 | } 21 | 22 | boolean hasFrontMatter(); 23 | 24 | default boolean hasFrontMatter(String key) { 25 | return hasFrontMatter() && getFrontMatterList(key) != null; 26 | } 27 | 28 | static InputFile fromPath(Path path) { 29 | return fromFile(path.toFile()); 30 | } 31 | 32 | static InputFile fromFile(File file) { 33 | if (file.getName().endsWith(".md")) { 34 | return fromMarkdownFile(file); 35 | } else if (file.getName().endsWith(".html")) { 36 | return fromHtmlFile(file); 37 | } else if (file.getName().endsWith(".xml")) { 38 | return fromXmlFile(file); 39 | } else { 40 | throw new RuntimeException("Unknown file type: " + file.getName()); 41 | } 42 | } 43 | 44 | static MarkdownFile fromMarkdownFile(File markdownFile) { 45 | return MarkdownFile.fromFile(markdownFile); 46 | } 47 | 48 | static HtmlFile fromHtmlFile(File htmlFile) { 49 | return HtmlFile.fromFile(htmlFile); 50 | } 51 | 52 | static XmlFile fromXmlFile(File xmlFile) { 53 | return XmlFile.fromFile(xmlFile); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/net/jonathangiles/tools/sitebuilder/models/input/MarkdownFile.java: -------------------------------------------------------------------------------- 1 | package net.jonathangiles.tools.sitebuilder.models.input; 2 | 3 | import org.commonmark.ext.front.matter.YamlFrontMatterExtension; 4 | import org.commonmark.ext.front.matter.YamlFrontMatterVisitor; 5 | import org.commonmark.node.Node; 6 | import org.commonmark.parser.Parser; 7 | import org.commonmark.renderer.html.HtmlRenderer; 8 | 9 | import java.io.File; 10 | import java.io.FileReader; 11 | import java.io.IOException; 12 | import java.util.List; 13 | import java.util.Map; 14 | 15 | public class MarkdownFile implements InputFile { 16 | private static final Parser PARSER = Parser.builder().extensions(List.of(YamlFrontMatterExtension.create())).build(); 17 | private static final HtmlRenderer HTML_RENDERER = HtmlRenderer.builder().build(); 18 | 19 | private final Map> frontMatter; 20 | 21 | private final String content; 22 | 23 | private MarkdownFile(Map> frontMatter, String content) { 24 | this.frontMatter = frontMatter; 25 | this.content = content; 26 | 27 | // for markdown pages, we set the template to 'page', if one is not set, so that they look as expected 28 | if (!frontMatter.containsKey("template")) { 29 | frontMatter.put("template", List.of("page")); 30 | } 31 | } 32 | 33 | static MarkdownFile fromFile(File markdownFile) { 34 | Node document = null; 35 | try { 36 | document = PARSER.parseReader(new FileReader(markdownFile)); 37 | } catch (IOException e) { 38 | throw new RuntimeException(e); 39 | } 40 | 41 | YamlFrontMatterVisitor frontMatter = new YamlFrontMatterVisitor(); 42 | document.accept(frontMatter); 43 | return new MarkdownFile(frontMatter.getData(), HTML_RENDERER.render(document)); 44 | } 45 | 46 | @Override public String getBody() { 47 | return content; 48 | } 49 | 50 | @Override 51 | public List getFrontMatterList(String key) { 52 | return frontMatter.getOrDefault(key, null); 53 | } 54 | 55 | @Override public boolean hasFrontMatter() { 56 | return !frontMatter.isEmpty(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/net/jonathangiles/tools/sitebuilder/models/input/HtmlFile.java: -------------------------------------------------------------------------------- 1 | package net.jonathangiles.tools.sitebuilder.models.input; 2 | 3 | import net.jonathangiles.tools.sitebuilder.util.FileUtils; 4 | 5 | import java.io.File; 6 | import java.util.HashMap; 7 | import java.util.List; 8 | import java.util.Map; 9 | 10 | public class HtmlFile implements InputFile { 11 | private final Map> frontMatter; 12 | 13 | private final String content; 14 | 15 | private HtmlFile(Map> frontMatter, String content) { 16 | this.frontMatter = frontMatter; 17 | this.content = content; 18 | } 19 | 20 | static HtmlFile fromFile(File htmlFile) { 21 | // read the contents of the html file into a string, and parse the front matter into a map 22 | String html = FileUtils.readFile(htmlFile.toPath()); 23 | final Map> frontMatter = readFrontMatter(html); 24 | 25 | // strip the front matter from just the top of the html content 26 | html = html.replaceFirst("", ""); 27 | 28 | return new HtmlFile(frontMatter, html); 29 | } 30 | 31 | @Override public String getBody() { 32 | return content; 33 | } 34 | 35 | @Override 36 | public List getFrontMatterList(String key) { 37 | return frontMatter.get(key); 38 | } 39 | 40 | @Override 41 | public boolean hasFrontMatter() { 42 | return !frontMatter.isEmpty(); 43 | } 44 | 45 | // reads the front matter from an HTML comment 46 | private static Map> readFrontMatter(final String html) { 47 | final Map> frontMatter = new HashMap<>(); 48 | 49 | final String[] lines = html.split("\n"); 50 | if (lines.length > 0 && lines[0].startsWith("")) { 53 | final String line = lines[i]; 54 | final String[] split = line.split(":"); 55 | if (split.length == 2) { 56 | final String key = split[0].trim(); 57 | final String value = split[1].trim(); 58 | frontMatter.put(key, List.of(value)); 59 | } 60 | i++; 61 | } 62 | } 63 | 64 | return frontMatter; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/net/jonathangiles/tools/sitebuilder/models/input/XmlFile.java: -------------------------------------------------------------------------------- 1 | package net.jonathangiles.tools.sitebuilder.models.input; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.dataformat.xml.XmlMapper; 5 | import net.jonathangiles.tools.sitebuilder.util.FileUtils; 6 | 7 | import java.io.File; 8 | import java.util.*; 9 | 10 | /** 11 | * Old blog posts, when I exported them from WordPress, took the form of XML files. They aren't the prettiest, and 12 | * eventually I would like to convert them to markdown, but for now I have this class to handle them. 13 | */ 14 | public class XmlFile implements InputFile { 15 | private final Map> frontMatter; 16 | 17 | private final String body; 18 | 19 | private XmlFile(Map> frontMatter, String body) { 20 | this.frontMatter = frontMatter; 21 | this.body = body; 22 | } 23 | 24 | static XmlFile fromFile(File xmlFile) { 25 | // Use Jackson to read all elements of the XML file, under the root element. All values are considered 26 | // front matter, except for the element, which is the HTML content of the blog post. 27 | final Map> frontMatter = new HashMap<>(); 28 | String xmlContent = FileUtils.readFile(xmlFile.toPath()); 29 | XmlMapper mapper = new XmlMapper(); 30 | String body = ""; 31 | 32 | try { 33 | Map map = (LinkedHashMap)mapper.readValue(xmlContent, Object.class); 34 | for (Object key : map.keySet()) { 35 | if (key.equals("Content")) { 36 | body = (String)map.get(key); 37 | } else { 38 | frontMatter.put((String)key, List.of((String)map.get(key))); 39 | } 40 | } 41 | } catch (JsonProcessingException e) { 42 | throw new RuntimeException(e); 43 | } 44 | 45 | return new XmlFile(frontMatter, body); 46 | } 47 | 48 | @Override public String getBody() { 49 | return body; 50 | } 51 | 52 | @Override 53 | public List getFrontMatterList(String key) { 54 | return frontMatter.getOrDefault(key, null); 55 | } 56 | 57 | @Override public Optional getFrontMatterValue(String key) { 58 | // annoyingly, the XML front matter keys have their first letter upper-cased, so we need to change the 59 | // key to match what the files have in them 60 | key = key.substring(0, 1).toUpperCase() + key.substring(1); 61 | return InputFile.super.getFrontMatterValue(key); 62 | } 63 | 64 | @Override 65 | public boolean hasFrontMatter() { 66 | return !frontMatter.isEmpty(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/net/jonathangiles/tools/sitebuilder/util/SitePaths.java: -------------------------------------------------------------------------------- 1 | package net.jonathangiles.tools.sitebuilder.util; 2 | 3 | import net.jonathangiles.tools.sitebuilder.models.Post; 4 | import net.jonathangiles.tools.sitebuilder.models.SiteContent; 5 | 6 | import java.io.File; 7 | import java.nio.file.Path; 8 | import java.nio.file.Paths; 9 | import java.util.function.Consumer; 10 | import java.util.function.Function; 11 | 12 | import static net.jonathangiles.tools.sitebuilder.SiteBuilder.OUTPUT_PATH; 13 | import static net.jonathangiles.tools.sitebuilder.SiteBuilder.OUTPUT_DIR; 14 | 15 | public class SitePaths { 16 | 17 | private SitePaths() { } 18 | 19 | public static Path createRelativePath(final Path basePath, final Path file) { 20 | return new File(OUTPUT_DIR, basePath.relativize(file).toString()).toPath(); 21 | } 22 | 23 | // strip out the 'output/' from the path 24 | public static String createRelativePath(final Path path) { 25 | String pathStr = path.toString(); 26 | return pathStr.substring(pathStr.indexOf(OUTPUT_PATH+"/") + OUTPUT_PATH.length() + 1); 27 | } 28 | 29 | // /** 30 | // * This method will recreate the input directory structure in the output directory. It is designed with the assumption 31 | // * that each post will create a single index.html file, and therefore assumes that the directory structure has one 32 | // * post per directory underneath the 'posts' directory. 33 | // */ 34 | // public static Function recreateDirStructure() { 35 | // return postPathRequest -> { 36 | // final Path basePath = postPathRequest.baseOutputPath; 37 | // final Post post = postPathRequest.post; 38 | // 39 | // Path relativePath = Paths.get(createRelativePath(basePath.getParent(), postPathRequest.pathToPostFile).toString(), post.getSlug()); 40 | // Path fullOutputPath = Paths.get(relativePath.getParent().toString(), "index.html"); 41 | // 42 | // return new PostPath(post, relativePath.toString(), fullOutputPath); 43 | // }; 44 | // } 45 | 46 | /** 47 | * This approach will ignore the directory structure of the input files, instead creating directories based on the 48 | * slugs contained within each post. It can optionally create directories for each year. 49 | */ 50 | public static Consumer createSlugDirStructure(String prefix, boolean createYearDirs) { 51 | return siteContent -> { 52 | final String slug = siteContent.getSlug(); 53 | 54 | String relativePath = prefix + (createYearDirs ? siteContent.getDate().getYear() + "/" : ""); 55 | 56 | // if the slug starts with a forward-slash, this is a directive to not create a directory based on the slug, 57 | // and to simply append .html to the end of the filename. This is useful for things like the index.html file. 58 | Path fullOutputPath; 59 | if (slug.startsWith("/")) { 60 | fullOutputPath = new File(OUTPUT_DIR, relativePath + "/" + slug + ".html").toPath(); 61 | } else { 62 | relativePath += slug; 63 | fullOutputPath = new File(OUTPUT_DIR, relativePath + "/index.html").toPath(); 64 | } 65 | 66 | siteContent.setRelativePath(relativePath); 67 | siteContent.setFullOutputPath(fullOutputPath); 68 | }; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/net/jonathangiles/tools/sitebuilder/models/SiteContent.java: -------------------------------------------------------------------------------- 1 | package net.jonathangiles.tools.sitebuilder.models; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlCData; 6 | import net.jonathangiles.tools.sitebuilder.models.input.InputFile; 7 | 8 | import java.nio.file.Path; 9 | import java.time.LocalDate; 10 | import java.util.HashMap; 11 | import java.util.Map; 12 | import java.util.Objects; 13 | 14 | public class SiteContent { 15 | 16 | @JsonProperty("Title") 17 | private String title; 18 | 19 | @JsonProperty("Content") 20 | @JacksonXmlCData 21 | private String content; 22 | 23 | @JsonProperty("Date") 24 | private LocalDate date; 25 | 26 | @JsonProperty("Slug") 27 | private String slug; 28 | 29 | @JsonProperty("Status") 30 | private SiteContentStatus status; 31 | 32 | @JsonProperty("Template") 33 | private String template; 34 | 35 | @JsonIgnore 36 | private String relativePath; 37 | 38 | @JsonIgnore 39 | private Path fullOutputPath; 40 | 41 | @JsonIgnore 42 | private final Map properties = new HashMap<>(); 43 | 44 | public SiteContent() { } 45 | 46 | public SiteContent(InputFile inputFile) { 47 | setContent(inputFile.getBody()); 48 | inputFile.getFrontMatterValue("title").ifPresent(this::setTitle); 49 | inputFile.getFrontMatterValue("date").ifPresent(d -> setDate(LocalDate.parse(d))); 50 | inputFile.getFrontMatterValue("slug").ifPresent(this::setSlug); 51 | inputFile.getFrontMatterValue("template").ifPresent(this::setTemplate); 52 | // this.status = PostStatus.valueOf(mdFile.getFrontMatterValue("status").toUpperCase()); 53 | // this.categories = mdFile.getFrontMatterValue("categories"); 54 | // this.tags = mdFile.getFrontMatterValue("tags"); 55 | } 56 | 57 | // public int getId() { 58 | // return id; 59 | // } 60 | // 61 | // public void setId(int id) { 62 | // this.id = id; 63 | // properties.put("id", String.valueOf(id)); 64 | // } 65 | 66 | public String getTitle() { 67 | return title; 68 | } 69 | 70 | public void setTitle(String title) { 71 | this.title = title; 72 | properties.put("title", title); 73 | } 74 | 75 | public String getContent() { 76 | return content; 77 | } 78 | 79 | public void setContent(String content) { 80 | this.content = content; 81 | properties.put("content", content); 82 | } 83 | 84 | public LocalDate getDate() { 85 | return date; 86 | } 87 | 88 | public void setDate(LocalDate date) { 89 | this.date = date; 90 | properties.put("date", date.toString()); 91 | } 92 | 93 | public String getSlug() { 94 | return slug; 95 | } 96 | 97 | public void setSlug(String slug) { 98 | this.slug = slug; 99 | properties.put("slug", slug); 100 | } 101 | 102 | public String getTemplate() { 103 | return template; 104 | } 105 | 106 | public void setTemplate(String template) { 107 | this.template = template; 108 | } 109 | 110 | public SiteContentStatus getStatus() { 111 | return status; 112 | } 113 | 114 | public void setStatus(SiteContentStatus status) { 115 | this.status = status; 116 | properties.put("status", status.toString()); 117 | } 118 | 119 | public String getRelativePath() { 120 | return relativePath; 121 | } 122 | 123 | public void setRelativePath(String relativePath) { 124 | this.relativePath = relativePath; 125 | properties.put("relativePath", relativePath); 126 | } 127 | 128 | public Path getFullOutputPath() { 129 | return fullOutputPath; 130 | } 131 | 132 | public void setFullOutputPath(Path fullOutputPath) { 133 | this.fullOutputPath = fullOutputPath; 134 | properties.put("fullOutputPath", fullOutputPath.toString()); 135 | } 136 | 137 | // TODO delete this method entirely! 138 | public Map getProperties() { 139 | return properties; 140 | } 141 | 142 | @Override 143 | public boolean equals(final Object o) { 144 | if (this == o) return true; 145 | if (o == null || getClass() != o.getClass()) return false; 146 | final Post post = (Post) o; 147 | return getDate().equals(post.getDate()) && 148 | getSlug().equals(post.getSlug()); 149 | } 150 | 151 | @Override 152 | public int hashCode() { 153 | return Objects.hash(getDate(), getSlug()); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | net.jonathangiles.tools 5 | sitebuilder 6 | 0.0.5 7 | 8 | Site Builder Template Tool 9 | A simple templating tool useful for building static websites. 10 | http://github.com/jonathangiles/sitebuilder 11 | 12 | 13 | 14 | ossrh 15 | https://oss.sonatype.org/content/repositories/snapshots 16 | 17 | 18 | ossrh 19 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 20 | 21 | 22 | 23 | 24 | scm:git:git://github.com/JonathanGiles/sitebuilder.git 25 | scm:git:git@github.com:JonathanGiles/sitebuilder.git 26 | https://github.com/JonathanGiles/sitebuilder 27 | HEAD 28 | 29 | 30 | 31 | GitHub 32 | https://github.com/JonathanGiles/sitebuilder/issues 33 | 34 | 35 | 36 | 37 | MIT License 38 | http://www.opensource.org/licenses/mit-license.php 39 | repo 40 | 41 | 42 | 43 | 44 | 45 | jonathangiles 46 | Jonathan Giles 47 | http://jonathangiles.net 48 | 49 | 50 | 51 | 52 | 1.8 53 | 1.8 54 | 55 | 56 | 57 | 58 | com.fasterxml.jackson.core 59 | jackson-databind 60 | 2.14.2 61 | 62 | 63 | com.fasterxml.jackson.dataformat 64 | jackson-dataformat-xml 65 | 2.14.2 66 | 67 | 68 | com.fasterxml.jackson.datatype 69 | jackson-datatype-jsr310 70 | 2.14.2 71 | 72 | 73 | 74 | 75 | org.commonmark 76 | commonmark 77 | 0.21.0 78 | 79 | 80 | org.commonmark 81 | commonmark-ext-yaml-front-matter 82 | 0.21.0 83 | 84 | 85 | 86 | 87 | 88 | 89 | org.apache.maven.plugins 90 | maven-javadoc-plugin 91 | 3.6.0 92 | 93 | all,-missing 94 | 95 | 96 | 97 | attach-javadoc 98 | 99 | jar 100 | 101 | 102 | 103 | 104 | 105 | org.apache.maven.plugins 106 | maven-source-plugin 107 | 3.3.0 108 | 109 | 110 | attach-source 111 | 112 | jar 113 | 114 | 115 | 116 | 117 | 118 | org.apache.maven.plugins 119 | maven-compiler-plugin 120 | 3.11.0 121 | 122 | 9 123 | 9 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | release 132 | 133 | 134 | 135 | org.apache.maven.plugins 136 | maven-gpg-plugin 137 | 3.1.0 138 | 139 | 140 | sign-artifacts 141 | verify 142 | 143 | sign 144 | 145 | 146 | 147 | 148 | 149 | org.sonatype.plugins 150 | nexus-staging-maven-plugin 151 | 1.6.13 152 | true 153 | 154 | ossrh 155 | https://oss.sonatype.org/ 156 | true 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | -------------------------------------------------------------------------------- /src/main/java/net/jonathangiles/tools/sitebuilder/SiteBuilder.java: -------------------------------------------------------------------------------- 1 | package net.jonathangiles.tools.sitebuilder; 2 | 3 | import net.jonathangiles.tools.sitebuilder.models.*; 4 | 5 | import static net.jonathangiles.tools.sitebuilder.util.FileUtils.*; 6 | 7 | import java.io.File; 8 | import java.nio.file.Files; 9 | import java.nio.file.Path; 10 | import java.util.*; 11 | import java.util.function.Consumer; 12 | import java.util.regex.Pattern; 13 | import java.util.stream.Collectors; 14 | import java.util.stream.Stream; 15 | 16 | import net.jonathangiles.tools.sitebuilder.models.input.InputFile; 17 | import net.jonathangiles.tools.sitebuilder.util.SitePaths; 18 | 19 | /** 20 | * Reads in all index.xml files and creates a static index.html file from it and the header and footer files. 21 | */ 22 | public abstract class SiteBuilder { 23 | 24 | private enum ContentType { PAGE, POST } 25 | 26 | public static final String OUTPUT_PATH = "target/output"; 27 | public static final File OUTPUT_DIR = new File(OUTPUT_PATH); 28 | 29 | private final ClassLoader loader; 30 | 31 | private Map templates = new HashMap<>(); 32 | 33 | // map of slug -> content 34 | private final Map allContentMap = new HashMap<>(); 35 | 36 | private final Consumer postPathFunction = SitePaths.createSlugDirStructure("posts/", true); 37 | private final Consumer pagePathFunction = SitePaths.createSlugDirStructure("", false); 38 | 39 | protected SiteBuilder() { 40 | loader = Thread.currentThread().getContextClassLoader(); 41 | } 42 | 43 | public void init() { 44 | // ------------------------------------------------------------------------ 45 | // Templates 46 | // ------------------------------------------------------------------------ 47 | 48 | // read in the static template files as strings 49 | loadTemplates(); 50 | 51 | // with all the templates in memory, update any ${include ...} directives now in all 52 | // template files, so that all templates are complete and do not have any 'include' directives. 53 | processIncludesDirectives(); 54 | 55 | // ------------------------------------------------------------------------ 56 | // Content Discovery 57 | // ------------------------------------------------------------------------ 58 | 59 | registerContent(); 60 | } 61 | 62 | public void run() { 63 | processContent(); 64 | 65 | // copy all static resources into the appropriate locations under the output dir 66 | processStaticResources(); 67 | } 68 | 69 | private void registerContent() { 70 | registerContent(getPath("www/pages", loader), ContentType.PAGE); 71 | registerContent(getPath("www/posts", loader), ContentType.POST); 72 | } 73 | 74 | private void registerContent(final Path rootPath, final ContentType type) { 75 | try (Stream files = Files.walk(rootPath)) { 76 | // we only process pages that have front matter 77 | files.filter(Files::isRegularFile) 78 | .filter(path -> { 79 | // we process any file that ends with .xml, .html, or .md, but only if they have 80 | // front matter that we can process 81 | final String n = path.getFileName().toString(); 82 | return n.endsWith(".html") || n.endsWith(".md") || n.endsWith(".xml"); 83 | }).map(InputFile::fromPath) 84 | .filter(InputFile::hasFrontMatter) 85 | .forEach(inputFile -> { 86 | switch (type) { 87 | case PAGE: 88 | registerContent(new Page(inputFile)); 89 | break; 90 | case POST: 91 | registerContent(new Post(inputFile)); 92 | break; 93 | } 94 | }); 95 | } catch (Exception e) { 96 | throw new RuntimeException(e); 97 | } 98 | } 99 | 100 | public void registerContent(final SiteContent content) { 101 | if (content instanceof Page) { 102 | pagePathFunction.accept(content); 103 | } else if (content instanceof Post) { 104 | postPathFunction.accept(content); 105 | } 106 | 107 | if (allContentMap.containsKey(content.getSlug())) { 108 | System.err.println("Duplicate slug found '" + content.getSlug() + "' - aborting"); 109 | System.exit(-1); 110 | } 111 | allContentMap.put(content.getSlug(), content); 112 | } 113 | 114 | public Set getAllPosts() { 115 | return allContentMap.values().stream() 116 | .filter(c -> c instanceof Post) 117 | .map(c -> (Post)c) 118 | .sorted(Comparator.comparing(Post::getDate).reversed().thenComparing(Post::getSlug)) 119 | .collect(Collectors.toCollection(LinkedHashSet::new)); 120 | } 121 | 122 | // -------------------------------------------------------------------------- 123 | // Page utilities 124 | // -------------------------------------------------------------------------- 125 | 126 | private void processStaticResources() { 127 | final Path staticPath = getPath("www/static", loader); 128 | try (Stream files = Files.walk(staticPath)) { 129 | files.filter(Files::isRegularFile) 130 | .forEach(file -> copyFile(staticPath, file)); 131 | } catch (Exception e) { 132 | throw new RuntimeException(e); 133 | } 134 | } 135 | 136 | // -------------------------------------------------------------------------- 137 | // Templating 138 | // -------------------------------------------------------------------------- 139 | 140 | private void processContent() { 141 | allContentMap.values().stream() 142 | .filter(c -> c.getStatus() != SiteContentStatus.DRAFT) 143 | .forEach(content -> { 144 | System.out.println("Processing: " + content.getSlug()); 145 | processContent(content); 146 | }); 147 | } 148 | 149 | private void processContent(SiteContent siteContent) { 150 | final String template = siteContent.getTemplate(); 151 | 152 | String html = processIncludesDirectives(templates.getOrDefault(template, siteContent.getContent())); 153 | 154 | for (Map.Entry property : siteContent.getProperties().entrySet()) { 155 | html = fillTemplate(html, property.getKey(), property.getValue()); 156 | } 157 | 158 | Path outputPath = siteContent.getFullOutputPath(); 159 | outputPath.getParent().toFile().mkdirs(); 160 | writeToFile(outputPath, html); 161 | } 162 | 163 | private void loadTemplates() { 164 | final Path templatesPath = getPath("www/templates", loader); 165 | 166 | try (Stream files = Files.walk(templatesPath)) { 167 | files.filter(Files::isRegularFile) 168 | .forEach(file -> { 169 | final String filename = file.getFileName().toString(); 170 | System.out.println("Reading template: " + filename); 171 | templates.put(filename.substring(0, filename.lastIndexOf(".")), readFile(file)); 172 | }); 173 | } catch (Exception e) { 174 | throw new RuntimeException(e); 175 | } 176 | } 177 | 178 | private void processIncludesDirectives() { 179 | templates.replaceAll((k, v) -> processIncludesDirectives(v)); 180 | } 181 | 182 | private String processIncludesDirectives(String html) { 183 | for (final Map.Entry template : templates.entrySet()) { 184 | html = fillTemplate(html, "include " + template.getKey(), template.getValue()); 185 | } 186 | 187 | return html; 188 | } 189 | 190 | private static String fillTemplate(final String html, final String field, final String data) { 191 | return fillTemplate(html, field, data, null); 192 | } 193 | 194 | private static String fillTemplate(final String html, final String field, String data, final Post post) { 195 | try { 196 | // we have to escape some characters in the data 197 | data = data.replace("$", "\\$"); 198 | 199 | return html.replaceAll(Pattern.quote("${" + field + "}"), data); 200 | } catch (IndexOutOfBoundsException e) { 201 | if (post == null) { 202 | throw new RuntimeException(e); 203 | } else { 204 | System.err.println("Could not do regex on field '" + field + "' on post '" + post.getTitle() + "' with data '" + data + "'"); 205 | } 206 | return ""; 207 | } 208 | } 209 | } 210 | --------------------------------------------------------------------------------