Better (md) index page, Improve path handling

This commit is contained in:
MrLetsplay 2024-01-17 21:24:29 +01:00
parent 6db5cdd701
commit 4484b87c32
Signed by: mr
SSH Key Fingerprint: SHA256:92jBH80vpXyaZHjaIl47pjRq+Yt7XGTArqQg1V7hSqg
6 changed files with 227 additions and 53 deletions

View File

@ -6,7 +6,6 @@ import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchKey; import java.nio.file.WatchKey;
import java.nio.file.WatchService; import java.nio.file.WatchService;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
@ -14,13 +13,14 @@ import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import me.mrletsplay.mdblog.blog.Post; import me.mrletsplay.mdblog.blog.Post;
import me.mrletsplay.mdblog.markdown.MdParser;
import me.mrletsplay.mdblog.markdown.MdRenderer;
import me.mrletsplay.mdblog.util.PostPath;
import me.mrletsplay.simplehttpserver.dom.html.HtmlDocument; import me.mrletsplay.simplehttpserver.dom.html.HtmlDocument;
import me.mrletsplay.simplehttpserver.dom.html.HtmlElement; import me.mrletsplay.simplehttpserver.dom.html.HtmlElement;
import me.mrletsplay.simplehttpserver.http.HttpRequestMethod; import me.mrletsplay.simplehttpserver.http.HttpRequestMethod;
import me.mrletsplay.simplehttpserver.http.HttpStatusCodes;
import me.mrletsplay.simplehttpserver.http.document.FileDocument; import me.mrletsplay.simplehttpserver.http.document.FileDocument;
import me.mrletsplay.simplehttpserver.http.request.HttpRequestContext; import me.mrletsplay.simplehttpserver.http.request.HttpRequestContext;
import me.mrletsplay.simplehttpserver.http.response.TextResponse;
import me.mrletsplay.simplehttpserver.http.server.HttpServer; import me.mrletsplay.simplehttpserver.http.server.HttpServer;
public class MdBlog { public class MdBlog {
@ -32,7 +32,12 @@ public class MdBlog {
private static HttpServer server; private static HttpServer server;
private static WatchService watchService; private static WatchService watchService;
private static List<WatchKey> watchedDirectories; private static List<WatchKey> watchedDirectories;
private static Map<String, Post> posts; private static Map<PostPath, Post> posts;
private static String
indexTemplate,
indexSubBlogTemplate,
indexPostTemplate;
public static void main(String[] args) throws IOException { public static void main(String[] args) throws IOException {
server = new HttpServer(HttpServer.newConfigurationBuilder() server = new HttpServer(HttpServer.newConfigurationBuilder()
@ -40,40 +45,53 @@ public class MdBlog {
.port(3706) .port(3706)
.create()); .create());
server.getDocumentProvider().registerPattern(HttpRequestMethod.GET, "/posts", () -> { server.getDocumentProvider().register(HttpRequestMethod.GET, "/posts", () -> {
createPostsIndex(POSTS_PATH); HttpRequestContext ctx = HttpRequestContext.getCurrentContext();
ctx.redirect("/posts/");
});
server.getDocumentProvider().register(HttpRequestMethod.GET, "/posts/", () -> {
createPostsIndex(null);
}); });
server.getDocumentProvider().registerPattern(HttpRequestMethod.GET, "/posts/{path...}", () -> { server.getDocumentProvider().registerPattern(HttpRequestMethod.GET, "/posts/{path...}", () -> {
HttpRequestContext ctx = HttpRequestContext.getCurrentContext(); HttpRequestContext ctx = HttpRequestContext.getCurrentContext();
String path = ctx.getPathParameters().get("path"); String rawPath = ctx.getPathParameters().get("path");
PostPath path = PostPath.parse(rawPath);
Post post = posts.get(path); Post post = posts.get(path);
if(post != null) { if(post != null) {
post.getContent().createContent(); post.getContent().createContent();
return; return;
} }
Path resolved = POSTS_PATH.resolve(path).normalize(); if(posts.keySet().stream().anyMatch(p -> p.startsWith(path))) {
if(!rawPath.endsWith("/")) {
ctx.redirect("/posts/" + rawPath + "/");
return;
}
System.out.println(path);
createPostsIndex(path);
return;
}
Path resolved = POSTS_PATH.resolve(path.toNioPath()).normalize();
if(!resolved.startsWith(POSTS_PATH)) { if(!resolved.startsWith(POSTS_PATH)) {
server.getDocumentProvider().getNotFoundDocument().createContent(); server.getDocumentProvider().getNotFoundDocument().createContent();
return; return;
} }
if(!Files.isRegularFile(resolved)) { if(!Files.isRegularFile(resolved) || !Files.isReadable(resolved)) {
if(!Files.isDirectory(resolved)) {
server.getDocumentProvider().getNotFoundDocument().createContent(); server.getDocumentProvider().getNotFoundDocument().createContent();
return; return;
} }
createPostsIndex(resolved);
return;
}
try { try {
new FileDocument(resolved).createContent(); new FileDocument(resolved).createContent();
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
ctx.setException(e);
server.getDocumentProvider().getErrorDocument().createContent();
} }
}); });
@ -81,6 +99,10 @@ public class MdBlog {
extractAndRegister("style/index.css"); extractAndRegister("style/index.css");
extractAndRegister("style/post.css"); extractAndRegister("style/post.css");
indexTemplate = Files.readString(extract("template/index.md"));
indexPostTemplate = Files.readString(extract("template/index-post.md"));
indexSubBlogTemplate = Files.readString(extract("template/index-sub-blog.md"));
server.start(); server.start();
Files.createDirectories(FILES_PATH); Files.createDirectories(FILES_PATH);
@ -109,49 +131,82 @@ public class MdBlog {
} }
} }
private static void createPostsIndex(Path directory) { private static void createPostsIndex(PostPath path) {
try {
// Generate posts index // Generate posts index
List<Path> inDir = Files.list(directory) List<PostPath> allPaths = posts.keySet().stream()
.filter(p -> Files.isDirectory(p) || posts.values().stream().anyMatch(post -> post.getFilePath().equals(p))) .filter(p -> path == null || (p.startsWith(path) && !p.equals(path)))
.collect(Collectors.toList()); .map(p -> path == null ? p : p.subPath(path.length()))
Collections.sort(inDir); .toList();
List<PostPath> directories = allPaths.stream()
.filter(p -> p.length() > 1)
.map(p -> p.subPath(0, 1))
.distinct()
.toList();
List<PostPath> postsInDir = allPaths.stream()
.filter(p -> p.length() == 1)
.toList();
// List<Path> inDir = Files.list(directory)
// .filter(p -> Files.isDirectory(p) || posts.values().stream().anyMatch(post -> post.getFilePath().equals(p)))
// .collect(Collectors.toList());
// Collections.sort(inDir);
String blogName = path == null ? "/" : path.getName();
HtmlDocument index = new HtmlDocument(); HtmlDocument index = new HtmlDocument();
index.setTitle("Index of " + directory.getFileName()); index.setTitle("Index of " + blogName);
index.addStyleSheet("/style/base.css");
index.addStyleSheet("/style/index.css"); index.addStyleSheet("/style/index.css");
HtmlElement ul = new HtmlElement("ul"); String indexMd = indexTemplate;
for(Path p : inDir) { indexMd = indexMd.replace("{name}", blogName);
HtmlElement li = new HtmlElement("li"); indexMd = indexMd.replace("{sub_blogs}", directories.stream()
HtmlElement a = new HtmlElement("a"); .map(p -> {
String relPath = directory.relativize(p).toString(); String subBlogMd = indexSubBlogTemplate;
if(Files.isRegularFile(p)) relPath = relPath.substring(0, relPath.length() - Post.FILE_EXTENSION.length());
// TODO: only works with trailing /
a.setAttribute("href", relPath);
a.setText(relPath);
li.appendChild(a);
ul.appendChild(li);
}
index.getBodyNode().appendChild(ul); HtmlElement name = new HtmlElement("a");
name.setAttribute("href", p.toString());
name.setText(p.getName());
subBlogMd = subBlogMd.replace("{name}", name.toString());
return subBlogMd;
})
.collect(Collectors.joining("\n\n")));
indexMd = indexMd.replace("{posts}", postsInDir.stream()
.map(p -> {
String postMd = indexPostTemplate;
Post post = posts.get(path.concat(p));
System.out.println(p);
HtmlElement title = new HtmlElement("a");
title.setAttribute("href", p.toString());
title.setText(post.getName());
postMd = postMd.replace("{title}", title.toString());
postMd = postMd.replace("{author}", post.getMetadata().author());
postMd = postMd.replace("{date}", post.getMetadata().date().toString());
postMd = postMd.replace("{tags}", post.getMetadata().tags().stream().collect(Collectors.joining(", ")));
postMd = postMd.replace("{description}", post.getMetadata().description());
return postMd;
})
.collect(Collectors.joining("\n\n")));
index.getBodyNode().appendChild(new MdRenderer().render(MdParser.parse(indexMd)));
index.createContent(); index.createContent();
}catch(IOException e) {
e.printStackTrace();
HttpRequestContext ctx = HttpRequestContext.getCurrentContext();
ctx.respond(HttpStatusCodes.INTERNAL_SERVER_ERROR_500, new TextResponse("Failed to create index"));
}
} }
private static void extractAndRegister(String path) throws IOException { private static Path extract(String path) throws IOException {
Path filePath = FILES_PATH.resolve(path); Path filePath = FILES_PATH.resolve(path);
if(!Files.exists(filePath)) { if(!Files.exists(filePath)) {
Files.createDirectories(filePath.getParent()); Files.createDirectories(filePath.getParent());
Files.write(filePath, MdBlog.class.getResourceAsStream("/" + path).readAllBytes()); Files.write(filePath, MdBlog.class.getResourceAsStream("/" + path).readAllBytes());
} }
return filePath;
}
server.getDocumentProvider().register(HttpRequestMethod.GET, "/" + path, new FileDocument(filePath)); private static void extractAndRegister(String path) throws IOException {
server.getDocumentProvider().register(HttpRequestMethod.GET, "/" + path, new FileDocument(extract(path)));
} }
private static void updateBlogs() throws IOException { private static void updateBlogs() throws IOException {
@ -167,9 +222,9 @@ public class MdBlog {
.filter(f -> posts.values().stream().noneMatch(p -> p.getFilePath().equals(f))) .filter(f -> posts.values().stream().noneMatch(p -> p.getFilePath().equals(f)))
.forEach(f -> { .forEach(f -> {
try { try {
String path = POSTS_PATH.relativize(f).toString(); String postName = f.getFileName().toString();
path = path.substring(0, path.length() - Post.FILE_EXTENSION.length()); postName = postName.substring(0, postName.length() - Post.FILE_EXTENSION.length());
posts.put(path, new Post(f)); posts.put(PostPath.of(POSTS_PATH.relativize(f).getParent(), postName), new Post(f));
} catch (IOException e) {} } catch (IOException e) {}
}); });
} }

View File

@ -7,13 +7,14 @@ import java.util.Collections;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public record PostMetadata(Instant date, String title, String author, Set<String> tags) { public record PostMetadata(Instant date, String title, String author, Set<String> tags, String description) {
public static PostMetadata load(String metadataString) { public static PostMetadata load(String metadataString) {
Instant date = Instant.EPOCH; Instant date = Instant.EPOCH;
String title = "Untitled Post"; String title = "Untitled Post";
String author = "Unknown Author"; String author = "Unknown Author";
Set<String> tags = Collections.emptySet(); String description = "No description";
Set<String> tags = Collections.singleton("untagged");
for(String line : metadataString.split("\n")) { for(String line : metadataString.split("\n")) {
if(line.isBlank()) continue; if(line.isBlank()) continue;
String[] spl = line.split(":", 2); String[] spl = line.split(":", 2);
@ -37,7 +38,7 @@ public record PostMetadata(Instant date, String title, String author, Set<String
} }
} }
return new PostMetadata(date, title, author, tags); return new PostMetadata(date, title, author, tags, description);
} }
} }

View File

@ -0,0 +1,104 @@
package me.mrletsplay.mdblog.util;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
public record PostPath(String[] segments) {
public PostPath {
if(segments.length == 0) throw new IllegalArgumentException("Number of segments must be greater than 0");
}
public PostPath getParent() {
if(segments.length == 1) return null;
return new PostPath(Arrays.copyOfRange(segments, 0, segments.length - 1));
}
public PostPath concat(PostPath other) {
String[] newPath = Arrays.copyOf(segments, segments.length + other.length());
System.arraycopy(other.segments, 0, newPath, segments.length, other.length());
return new PostPath(newPath);
}
public boolean startsWith(PostPath other) {
if(other.segments.length > segments.length) return false;
for(int i = 0; i < other.segments.length; i++) {
if(!segments[i].equals(other.segments[i])) return false;
}
return true;
}
public PostPath subPath(int fromIndex) throws IllegalArgumentException {
if(fromIndex < 0 || fromIndex >= segments.length) throw new IllegalArgumentException("fromIndex must be less than path length");
return new PostPath(Arrays.copyOfRange(segments, fromIndex, segments.length));
}
public PostPath subPath(int fromIndex, int toIndex) throws IllegalArgumentException {
if(fromIndex < 0 || fromIndex >= segments.length) throw new IllegalArgumentException("fromIndex must be less than path length");
if(toIndex <= fromIndex || toIndex >= segments.length) throw new IllegalArgumentException("fromIndex must be less than toIndex and path length");
return new PostPath(Arrays.copyOfRange(segments, fromIndex, toIndex));
}
public String getName() {
return segments[segments.length - 1];
}
public int length() {
return segments.length;
}
public Path toNioPath() {
return Paths.get(segments[0], Arrays.copyOfRange(segments, 1, segments.length));
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + Arrays.hashCode(segments);
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
PostPath other = (PostPath) obj;
return Arrays.equals(segments, other.segments);
}
@Override
public String toString() {
return String.join("/", segments);
}
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");
String[] names = new String[path.getNameCount()];
for(int i = 0; i < path.getNameCount(); i++) {
names[i] = path.getName(i).toString();
}
return new PostPath(names);
}
public static PostPath of(Path path, String name) {
if(path == null) return new PostPath(new String[] {name});
String[] names = new String[path.getNameCount() + 1];
for(int i = 0; i < path.getNameCount(); i++) {
names[i] = path.getName(i).toString();
}
names[names.length - 1] = name;
return new PostPath(names);
}
}

View File

@ -0,0 +1,6 @@
### {title}
#### by {author} on {date}
*{tags}*
{description}

View File

@ -0,0 +1 @@
### {name}

View File

@ -0,0 +1,7 @@
# Index of {name}
## Sub-Blogs
{sub_blogs}
## Posts
{posts}