diff --git a/src/main/java/me/mrletsplay/mdblog/MdBlog.java b/src/main/java/me/mrletsplay/mdblog/MdBlog.java index dc63edb..678a4cd 100644 --- a/src/main/java/me/mrletsplay/mdblog/MdBlog.java +++ b/src/main/java/me/mrletsplay/mdblog/MdBlog.java @@ -19,6 +19,7 @@ import me.mrletsplay.mdblog.blog.PostMetadata; import me.mrletsplay.mdblog.markdown.MdParser; import me.mrletsplay.mdblog.markdown.MdRenderer; import me.mrletsplay.mdblog.util.PostPath; +import me.mrletsplay.mdblog.util.TimeFormatter; import me.mrletsplay.simplehttpserver.dom.html.HtmlDocument; import me.mrletsplay.simplehttpserver.dom.html.HtmlElement; import me.mrletsplay.simplehttpserver.http.HttpRequestMethod; @@ -42,6 +43,7 @@ public class MdBlog { private static List watchedDirectories; private static Map posts; private static Map indexTemplates; + private static Map globalResources; private static String defaultIndexTemplate, @@ -54,53 +56,8 @@ public class MdBlog { .port(3706) .create()); - server.getDocumentProvider().register(HttpRequestMethod.GET, "/", () -> { - createPostsIndex(null); - }); - - server.getDocumentProvider().registerPattern(HttpRequestMethod.GET, "/{path...}", () -> { - HttpRequestContext ctx = HttpRequestContext.getCurrentContext(); - String rawPath = ctx.getPathParameters().get("path"); - PostPath path = PostPath.parse(rawPath); - Post post = posts.get(path); - if(post != null) { - post.getContent().createContent(); - return; - } - - if(posts.keySet().stream().anyMatch(p -> p.startsWith(path))) { - if(!rawPath.endsWith("/")) { - ctx.redirect(path.subPath(path.length() - 1).toString() + "/"); - return; - } - - createPostsIndex(path); - return; - } - - Path resolved = POSTS_PATH.resolve(path.toNioPath()).normalize(); - if(!resolved.startsWith(POSTS_PATH)) { - server.getDocumentProvider().getNotFoundDocument().createContent(); - return; - } - - if(!Files.isRegularFile(resolved) || !Files.isReadable(resolved)) { - server.getDocumentProvider().getNotFoundDocument().createContent(); - return; - } - - try { - new FileDocument(resolved).createContent(); - } catch (IOException e) { - e.printStackTrace(); - ctx.setException(e); - server.getDocumentProvider().getErrorDocument().createContent(); - } - }); - - extractAndRegister("style/base.css"); - extractAndRegister("style/index.css"); - extractAndRegister("style/post.css"); + server.getDocumentProvider().register(HttpRequestMethod.GET, "/", () -> createPostsIndex(PostPath.root())); + server.getDocumentProvider().registerPattern(HttpRequestMethod.GET, "/{path...}", () -> handleRequest(HttpRequestContext.getCurrentContext())); defaultIndexTemplate = Files.readString(extract("template/index.md")); defaultIndexPostTemplate = Files.readString(extract("template/index-post.md")); @@ -116,6 +73,12 @@ public class MdBlog { posts = new HashMap<>(); indexTemplates = new HashMap<>(); + globalResources = new HashMap<>(); + + extractAndRegister("style/base.css"); + extractAndRegister("style/index.css"); + extractAndRegister("style/post.css"); + updateBlogs(); watchFolders(); @@ -154,26 +117,16 @@ public class MdBlog { .filter(p -> p.length() == 1) .toList(); - String blogName = path == null ? "/" : path.getName(); + String blogName = path.getName(); - String indexTemplate; - String indexSubBlogTemplate; - String indexPostTemplate; - - if(path != null) { - indexTemplate = indexTemplates.getOrDefault(path.concat(PostPath.parse(INDEX_NAME)), defaultIndexTemplate); - indexSubBlogTemplate = indexTemplates.getOrDefault(path.concat(PostPath.parse(INDEX_SUB_BLOG_NAME)), defaultIndexSubBlogTemplate); - indexPostTemplate = indexTemplates.getOrDefault(path.concat(PostPath.parse(INDEX_POST_NAME)), defaultIndexPostTemplate); - }else { - indexTemplate = defaultIndexTemplate; - indexSubBlogTemplate = defaultIndexSubBlogTemplate; - indexPostTemplate = defaultIndexPostTemplate; - } + String indexTemplate = indexTemplates.getOrDefault(path.concat(PostPath.parse(INDEX_NAME)), defaultIndexTemplate); + String indexSubBlogTemplate = indexTemplates.getOrDefault(path.concat(PostPath.parse(INDEX_SUB_BLOG_NAME)), defaultIndexSubBlogTemplate); + String indexPostTemplate = indexTemplates.getOrDefault(path.concat(PostPath.parse(INDEX_POST_NAME)), defaultIndexPostTemplate); HtmlDocument index = new HtmlDocument(); index.setTitle("Index of " + blogName); - index.addStyleSheet("/_/style/base.css"); - index.addStyleSheet("/_/style/index.css"); + index.addStyleSheet("_/style/base.css"); + index.addStyleSheet("_/style/index.css"); String indexMd = indexTemplate; indexMd = indexMd.replace("{name}", blogName); @@ -201,7 +154,9 @@ public class MdBlog { postMd = postMd.replace("{title}", title.toString()); postMd = postMd.replace("{author}", meta.author()); - postMd = postMd.replace("{date}", meta.date().toString()); + postMd = postMd.replace("{date}", TimeFormatter.toDateOnly(meta.date())); + postMd = postMd.replace("{date_time}", TimeFormatter.toDateAndTime(meta.date())); + postMd = postMd.replace("{date_relative}", TimeFormatter.toRelativeTime(meta.date())); postMd = postMd.replace("{tags}", meta.tags().stream().collect(Collectors.joining(", "))); postMd = postMd.replace("{description}", meta.description()); return postMd; @@ -212,6 +167,59 @@ public class MdBlog { index.createContent(); } + private static void handleRequest(HttpRequestContext ctx) { + String rawPath = ctx.getPathParameters().get("path"); + PostPath path = PostPath.parse(rawPath); + int index = 0; + while(index < path.length() - 1 && !path.getSegments()[index].equals("_")) index++; + if(index < path.length() - 1) { + PostPath resourcePath = path.subPath(index + 1); + FileDocument resource = globalResources.get(resourcePath); + if(resource != null) { + resource.createContent(); + }else { + server.getDocumentProvider().getNotFoundDocument().createContent(); + } + return; + } + + Post post = posts.get(path); + + if(post != null) { + post.getContent().createContent(); + return; + } + + if(posts.keySet().stream().anyMatch(p -> p.startsWith(path))) { + if(!rawPath.endsWith("/")) { + ctx.redirect(path.subPath(path.length() - 1).toString() + "/"); + return; + } + + createPostsIndex(path); + return; + } + + Path resolved = POSTS_PATH.resolve(path.toNioPath()).normalize(); + if(!resolved.startsWith(POSTS_PATH)) { + server.getDocumentProvider().getNotFoundDocument().createContent(); + return; + } + + if(!Files.isRegularFile(resolved) || !Files.isReadable(resolved)) { + server.getDocumentProvider().getNotFoundDocument().createContent(); + return; + } + + try { + new FileDocument(resolved).createContent(); + } catch (IOException e) { + e.printStackTrace(); + ctx.setException(e); + server.getDocumentProvider().getErrorDocument().createContent(); + } + } + private static Path extract(String path) throws IOException { Path filePath = FILES_PATH.resolve(path); if(!Files.exists(filePath)) { @@ -222,7 +230,7 @@ public class MdBlog { } private static void extractAndRegister(String path) throws IOException { - server.getDocumentProvider().register(HttpRequestMethod.GET, "/_/" + path, new FileDocument(extract(path))); + globalResources.put(PostPath.parse(path), new FileDocument(extract(path))); } private static void updateBlogs() throws IOException { diff --git a/src/main/java/me/mrletsplay/mdblog/blog/PostMetadata.java b/src/main/java/me/mrletsplay/mdblog/blog/PostMetadata.java index d20def8..71e9f94 100644 --- a/src/main/java/me/mrletsplay/mdblog/blog/PostMetadata.java +++ b/src/main/java/me/mrletsplay/mdblog/blog/PostMetadata.java @@ -4,6 +4,7 @@ import java.time.DateTimeException; import java.time.Instant; import java.util.Arrays; import java.util.Collections; +import java.util.LinkedHashSet; import java.util.Set; import java.util.stream.Collectors; @@ -34,7 +35,13 @@ public record PostMetadata(Instant date, String title, String author, Set title = value; case "author" -> author = value; - case "tags" -> tags = Arrays.stream(value.split(",")).map(String::trim).collect(Collectors.toUnmodifiableSet()); + case "tags" -> { + Set t = Arrays.stream(value.split(",")) + .map(String::trim) + .collect(Collectors.toCollection(LinkedHashSet::new)); + + tags = Collections.unmodifiableSet(t); + } } } diff --git a/src/main/java/me/mrletsplay/mdblog/util/PostPath.java b/src/main/java/me/mrletsplay/mdblog/util/PostPath.java index 4198535..49fdfe4 100644 --- a/src/main/java/me/mrletsplay/mdblog/util/PostPath.java +++ b/src/main/java/me/mrletsplay/mdblog/util/PostPath.java @@ -4,14 +4,27 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.Arrays; -public record PostPath(String[] segments) { +public class PostPath { - public PostPath { + private static final PostPath ROOT = new PostPath(); + + private final String[] segments; + + private PostPath(String[] segments) { if(segments.length == 0) throw new IllegalArgumentException("Number of segments must be greater than 0"); + this.segments = segments; + } + + private PostPath() { + this.segments = new String[0]; + } + + public String[] getSegments() { + return segments; } public PostPath getParent() { - if(segments.length == 1) return null; + if(segments.length == 1) return ROOT; return new PostPath(Arrays.copyOfRange(segments, 0, segments.length - 1)); } @@ -41,6 +54,7 @@ public record PostPath(String[] segments) { } public String getName() { + if(this == ROOT) return "/"; return segments[segments.length - 1]; } @@ -49,6 +63,7 @@ public record PostPath(String[] segments) { } public Path toNioPath() { + if(this == ROOT) return Paths.get("/"); return Paths.get(segments[0], Arrays.copyOfRange(segments, 1, segments.length)); } @@ -77,13 +92,17 @@ public record PostPath(String[] segments) { return String.join("/", segments); } + public static PostPath root() { + return ROOT; + } + public static PostPath parse(String path) { if(path == null || path.isEmpty()) throw new IllegalArgumentException("Path must not be null or empty"); return new PostPath(path.split("/")); } - public static PostPath of(Path path) throws IllegalArgumentException { - if(path.getNameCount() == 0) throw new IllegalArgumentException("Path must not be a root path"); + public static PostPath of(Path path) { + if(path.getNameCount() == 0) return ROOT; String[] names = new String[path.getNameCount()]; for(int i = 0; i < path.getNameCount(); i++) { names[i] = path.getName(i).toString(); diff --git a/src/main/java/me/mrletsplay/mdblog/util/TimeFormatter.java b/src/main/java/me/mrletsplay/mdblog/util/TimeFormatter.java new file mode 100644 index 0000000..5bbfbc1 --- /dev/null +++ b/src/main/java/me/mrletsplay/mdblog/util/TimeFormatter.java @@ -0,0 +1,58 @@ +package me.mrletsplay.mdblog.util; + +import java.time.Instant; +import java.time.LocalDate; +import java.time.Period; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; + +public class TimeFormatter { + + private static final DateTimeFormatter + DATE_ONLY = DateTimeFormatter.ISO_LOCAL_DATE, + DATE_AND_TIME = new DateTimeFormatterBuilder() + .append(DateTimeFormatter.ISO_LOCAL_DATE) + .appendLiteral(" ") + .append(DateTimeFormatter.ISO_LOCAL_TIME) + .toFormatter(); + + public static String toDateOnly(Instant instant) { + return DATE_ONLY.format(instant.atZone(ZoneId.systemDefault())); + } + + public static String toDateAndTime(Instant instant) { + return DATE_AND_TIME.format(instant.atZone(ZoneId.systemDefault())); + } + + public static String toRelativeTime(Instant instant) { + // TODO: potentially use ChronoUnit#between instead to support more units + Period p = Period.between(LocalDate.now(), instant.atZone(ZoneId.systemDefault()).toLocalDate()); + + if(p.isZero()) return "today"; + boolean negative = p.isNegative(); + if(negative) p = p.negated(); + + StringBuilder b = new StringBuilder(); + + if(!negative) b.append("in "); + + if(p.getYears() > 0) { + appendTime(b, p.getYears(), "year"); + }else if(p.getMonths() > 0) { + appendTime(b, p.getMonths(), "month"); + }else if(p.getDays() > 0) { + appendTime(b, p.getDays(), "day"); + } + + if(negative) b.append(" ago"); + + return b.toString(); + } + + private static void appendTime(StringBuilder builder, int x, String unit) { + builder.append(x).append(" ").append(unit); + if(x > 1) builder.append("s"); + } + +} diff --git a/src/main/resources/template/index-post.md b/src/main/resources/template/index-post.md index c90f452..d98f1b5 100644 --- a/src/main/resources/template/index-post.md +++ b/src/main/resources/template/index-post.md @@ -1,5 +1,5 @@ ### {title} -#### by {author} on {date} +#### by {author} {date_relative} *{tags}*