Better (md) index page, Improve path handling
This commit is contained in:
parent
6db5cdd701
commit
4484b87c32
@ -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,33 +45,44 @@ 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
createPostsIndex(resolved);
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,6 +90,8 @@ public class MdBlog {
|
|||||||
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<PostPath> allPaths = posts.keySet().stream()
|
||||||
List<Path> inDir = Files.list(directory)
|
.filter(p -> path == null || (p.startsWith(path) && !p.equals(path)))
|
||||||
.filter(p -> Files.isDirectory(p) || posts.values().stream().anyMatch(post -> post.getFilePath().equals(p)))
|
.map(p -> path == null ? p : p.subPath(path.length()))
|
||||||
.collect(Collectors.toList());
|
.toList();
|
||||||
Collections.sort(inDir);
|
|
||||||
|
|
||||||
HtmlDocument index = new HtmlDocument();
|
List<PostPath> directories = allPaths.stream()
|
||||||
index.setTitle("Index of " + directory.getFileName());
|
.filter(p -> p.length() > 1)
|
||||||
index.addStyleSheet("/style/index.css");
|
.map(p -> p.subPath(0, 1))
|
||||||
|
.distinct()
|
||||||
|
.toList();
|
||||||
|
|
||||||
HtmlElement ul = new HtmlElement("ul");
|
List<PostPath> postsInDir = allPaths.stream()
|
||||||
for(Path p : inDir) {
|
.filter(p -> p.length() == 1)
|
||||||
HtmlElement li = new HtmlElement("li");
|
.toList();
|
||||||
HtmlElement a = new HtmlElement("a");
|
|
||||||
String relPath = directory.relativize(p).toString();
|
|
||||||
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);
|
// 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);
|
||||||
|
|
||||||
index.createContent();
|
String blogName = path == null ? "/" : path.getName();
|
||||||
}catch(IOException e) {
|
|
||||||
e.printStackTrace();
|
HtmlDocument index = new HtmlDocument();
|
||||||
HttpRequestContext ctx = HttpRequestContext.getCurrentContext();
|
index.setTitle("Index of " + blogName);
|
||||||
ctx.respond(HttpStatusCodes.INTERNAL_SERVER_ERROR_500, new TextResponse("Failed to create index"));
|
index.addStyleSheet("/style/base.css");
|
||||||
}
|
index.addStyleSheet("/style/index.css");
|
||||||
|
|
||||||
|
String indexMd = indexTemplate;
|
||||||
|
indexMd = indexMd.replace("{name}", blogName);
|
||||||
|
indexMd = indexMd.replace("{sub_blogs}", directories.stream()
|
||||||
|
.map(p -> {
|
||||||
|
String subBlogMd = indexSubBlogTemplate;
|
||||||
|
|
||||||
|
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
104
src/main/java/me/mrletsplay/mdblog/util/PostPath.java
Normal file
104
src/main/java/me/mrletsplay/mdblog/util/PostPath.java
Normal 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
6
src/main/resources/template/index-post.md
Normal file
6
src/main/resources/template/index-post.md
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
### {title}
|
||||||
|
#### by {author} on {date}
|
||||||
|
*{tags}*
|
||||||
|
|
||||||
|
|
||||||
|
{description}
|
1
src/main/resources/template/index-sub-blog.md
Normal file
1
src/main/resources/template/index-sub-blog.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
### {name}
|
7
src/main/resources/template/index.md
Normal file
7
src/main/resources/template/index.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# Index of {name}
|
||||||
|
|
||||||
|
## Sub-Blogs
|
||||||
|
{sub_blogs}
|
||||||
|
|
||||||
|
## Posts
|
||||||
|
{posts}
|
Loading…
Reference in New Issue
Block a user