initial commit

This commit is contained in:
MrLetsplay 2024-01-14 19:46:30 +01:00
commit 43771ff9d6
Signed by: mr
SSH Key Fingerprint: SHA256:92jBH80vpXyaZHjaIl47pjRq+Yt7XGTArqQg1V7hSqg
13 changed files with 654 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
/.settings
/bin
/.classpath
/TEST
/target/
/files/

23
.project Normal file
View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>MdBlog</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
<nature>org.eclipse.jdt.core.javanature</nature>
</natures>
</projectDescription>

64
pom.xml Normal file
View File

@ -0,0 +1,64 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>MDTest</groupId>
<artifactId>MDTest</artifactId>
<version>0.0.1-SNAPSHOT</version>
<build>
<sourceDirectory>src/main/java</sourceDirectory>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<release>17</release>
</configuration>
</plugin>
</plugins>
</build>
<properties>
<commonmark.version>0.21.0</commonmark.version>
</properties>
<repositories>
<repository>
<id>Graphite-Official</id>
<url>https://maven.graphite-official.com/releases</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>me.mrletsplay</groupId>
<artifactId>SimpleHTTPServer</artifactId>
<version>2.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.commonmark</groupId>
<artifactId>commonmark</artifactId>
<version>${commonmark.version}</version>
</dependency>
<dependency>
<groupId>org.commonmark</groupId>
<artifactId>commonmark-ext-gfm-tables</artifactId>
<version>${commonmark.version}</version>
</dependency>
<dependency>
<groupId>org.commonmark</groupId>
<artifactId>commonmark-ext-gfm-strikethrough</artifactId>
<version>${commonmark.version}</version>
</dependency>
<dependency>
<groupId>org.commonmark</groupId>
<artifactId>commonmark-ext-ins</artifactId>
<version>${commonmark.version}</version>
</dependency>
<dependency>
<groupId>org.commonmark</groupId>
<artifactId>commonmark-ext-task-list-items</artifactId>
<version>${commonmark.version}</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,137 @@
package me.mrletsplay.mdblog;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import me.mrletsplay.mdblog.blog.Post;
import me.mrletsplay.simplehttpserver.http.HttpRequestMethod;
import me.mrletsplay.simplehttpserver.http.document.FileDocument;
import me.mrletsplay.simplehttpserver.http.request.HttpRequestContext;
import me.mrletsplay.simplehttpserver.http.server.HttpServer;
public class MdBlog {
private static final Path
FILES_PATH = Path.of("files"),
POSTS_PATH = FILES_PATH.resolve("posts");
private static HttpServer server;
private static WatchService watchService;
private static List<WatchKey> watchedDirectories;
private static Map<String, Post> posts;
public static void main(String[] args) throws IOException {
server = new HttpServer(HttpServer.newConfigurationBuilder()
.hostBindAll()
.port(3706)
.create());
server.getDocumentProvider().registerPattern(HttpRequestMethod.GET, "/posts/{path...}", () -> {
HttpRequestContext ctx = HttpRequestContext.getCurrentContext();
String path = ctx.getPathParameters().get("path");
Post post = posts.get(path);
if(post != null) {
post.getContent().createContent();
return;
}
Path resolved = POSTS_PATH.resolve(path).normalize();
if(!resolved.startsWith(POSTS_PATH) || resolved.getFileName().toString().endsWith(".md")) {
server.getDocumentProvider().getNotFoundDocument().createContent();
return;
}
if(!Files.isRegularFile(resolved)) {
server.getDocumentProvider().getNotFoundDocument().createContent();
return;
}
try {
new FileDocument(resolved).createContent();
} catch (IOException e) {
e.printStackTrace();
}
});
extractAndRegister("style/base.css");
extractAndRegister("style/post.css");
server.start();
Files.createDirectories(FILES_PATH);
Files.createDirectories(POSTS_PATH);
watchedDirectories = new ArrayList<>();
watchService = POSTS_PATH.getFileSystem().newWatchService();
posts = new HashMap<>();
updateBlogs();
watchFolders();
while(true) {
try {
WatchKey key = watchService.take();
key.pollEvents();
updateBlogs();
watchFolders();
if(!key.reset()) {
key.cancel();
watchedDirectories.remove(key);
continue;
}
} catch (InterruptedException e) {
break;
}
}
}
private static void extractAndRegister(String path) throws IOException {
Path filePath = FILES_PATH.resolve(path);
if(!Files.exists(filePath)) {
Files.createDirectories(filePath.getParent());
Files.write(filePath, MdBlog.class.getResourceAsStream("/" + path).readAllBytes());
}
server.getDocumentProvider().register(HttpRequestMethod.GET, "/" + path, new FileDocument(filePath));
}
private static void updateBlogs() throws IOException {
Iterator<Post> it = posts.values().iterator();
while(it.hasNext()) {
Post p = it.next();
if(!p.update()) it.remove();
}
Files.walk(POSTS_PATH)
.filter(Files::isRegularFile)
.filter(f -> f.getFileName().toString().endsWith(".md"))
.filter(f -> posts.values().stream().noneMatch(p -> p.getFilePath().equals(f)))
.forEach(f -> {
try {
String path = POSTS_PATH.relativize(f).toString();
path = path.substring(0, path.length() - ".md".length());
posts.put(path, new Post(f));
} catch (IOException e) {}
});
}
private static void watchFolders() throws IOException {
Files.walk(POSTS_PATH)
.filter(Files::isDirectory)
.filter(d -> watchedDirectories.stream().noneMatch(w -> w.watchable().equals(d)))
.forEach(d -> {
try {
System.out.println("Watching " + d);
watchedDirectories.add(d.register(watchService, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE, StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.OVERFLOW));
} catch (IOException e) {}
});
}
}

View File

@ -0,0 +1,84 @@
package me.mrletsplay.mdblog.blog;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import me.mrletsplay.mdblog.markdown.MdParser;
import me.mrletsplay.mdblog.markdown.MdRenderer;
import me.mrletsplay.mrcore.misc.ByteUtils;
import me.mrletsplay.simplehttpserver.dom.html.HtmlDocument;
public class Post {
private static final MessageDigest MD_5;
private static final MdRenderer RENDERER = new MdRenderer();
static {
try {
MD_5 = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
private Path filePath;
private String checksum;
private PostMetadata metadata;
private HtmlDocument content;
public Post(Path filePath) throws IOException {
this.filePath = filePath;
this.checksum = checksum(filePath);
load();
}
public Path getFilePath() {
return filePath;
}
public PostMetadata getMetadata() {
return metadata;
}
public HtmlDocument getContent() {
return content;
}
private void load() throws IOException {
String postData = Files.readString(filePath);
String[] spl = postData.split("\n---\n", 2);
if(spl.length != 2) throw new IOException("Invalid post file");
this.metadata = PostMetadata.load(spl[0]);
HtmlDocument document = new HtmlDocument();
document.getBodyNode().appendChild(RENDERER.render(MdParser.parse(spl[1])));
document.setTitle(metadata.title());
document.setDescription(metadata.author());
document.addStyleSheet("/style/base.css");
document.addStyleSheet("/style/post.css");
this.content = document;
}
public boolean update() {
if(!Files.exists(filePath)) return false;
try {
String newChecksum = checksum(filePath);
if(checksum.equals(newChecksum)) return true;
this.checksum = newChecksum;
load();
return true;
} catch (IOException e) {
return false;
}
}
private static String checksum(Path filePath) throws IOException {
return ByteUtils.bytesToHex(MD_5.digest(Files.readAllBytes(filePath)));
}
}

View File

@ -0,0 +1,43 @@
package me.mrletsplay.mdblog.blog;
import java.time.DateTimeException;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collections;
import java.util.Set;
import java.util.stream.Collectors;
public record PostMetadata(Instant date, String title, String author, Set<String> tags) {
public static PostMetadata load(String metadataString) {
Instant date = Instant.EPOCH;
String title = "Untitled Post";
String author = "Unknown Author";
Set<String> tags = Collections.emptySet();
for(String line : metadataString.split("\n")) {
if(line.isEmpty()) continue;
String[] spl = line.split(":", 2);
if(spl.length != 2) {
System.err.println("Invalid metadata line: " + line);
continue;
}
String key = spl[0].toLowerCase().trim();
String value = spl[1].trim();
switch(key) {
case "date" -> {
try {
date = Instant.parse(value);
}catch(DateTimeException e) {}
}
case "title" -> title = value;
case "author" -> author = value;
case "tags" -> tags = Arrays.stream(value.split(",")).map(String::trim).collect(Collectors.toUnmodifiableSet());
}
}
return new PostMetadata(date, title, author, tags);
}
}

View File

@ -0,0 +1,23 @@
package me.mrletsplay.mdblog.markdown;
public class MdException extends Exception {
private static final long serialVersionUID = 2453345940664448784L;
public MdException() {
super();
}
public MdException(String message, Throwable cause) {
super(message, cause);
}
public MdException(String message) {
super(message);
}
public MdException(Throwable cause) {
super(cause);
}
}

View File

@ -0,0 +1,22 @@
package me.mrletsplay.mdblog.markdown;
import java.util.Arrays;
import org.commonmark.ext.gfm.strikethrough.StrikethroughExtension;
import org.commonmark.ext.gfm.tables.TablesExtension;
import org.commonmark.ext.ins.InsExtension;
import org.commonmark.ext.task.list.items.TaskListItemsExtension;
import org.commonmark.node.Node;
import org.commonmark.parser.Parser;
public class MdParser {
private static Parser parser = new Parser.Builder()
.extensions(Arrays.asList(TablesExtension.create(), StrikethroughExtension.create(), InsExtension.create(), TaskListItemsExtension.create()))
.build();
public static Node parse(String text) {
Node n = parser.parse(text);
return n;
}
}

View File

@ -0,0 +1,15 @@
package me.mrletsplay.mdblog.markdown;
public class MdRenderContext {
private MdRenderer renderer;
public MdRenderContext(MdRenderer renderer) {
this.renderer = renderer;
}
public MdRenderer getRenderer() {
return renderer;
}
}

View File

@ -0,0 +1,208 @@
package me.mrletsplay.mdblog.markdown;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import org.commonmark.ext.gfm.strikethrough.Strikethrough;
import org.commonmark.ext.gfm.tables.TableBlock;
import org.commonmark.ext.gfm.tables.TableBody;
import org.commonmark.ext.gfm.tables.TableCell;
import org.commonmark.ext.gfm.tables.TableHead;
import org.commonmark.ext.gfm.tables.TableRow;
import org.commonmark.ext.ins.Ins;
import org.commonmark.ext.task.list.items.TaskListItemMarker;
import org.commonmark.node.BlockQuote;
import org.commonmark.node.BulletList;
import org.commonmark.node.Code;
import org.commonmark.node.Document;
import org.commonmark.node.Emphasis;
import org.commonmark.node.FencedCodeBlock;
import org.commonmark.node.HardLineBreak;
import org.commonmark.node.Heading;
import org.commonmark.node.HtmlBlock;
import org.commonmark.node.HtmlInline;
import org.commonmark.node.Image;
import org.commonmark.node.IndentedCodeBlock;
import org.commonmark.node.Link;
import org.commonmark.node.LinkReferenceDefinition;
import org.commonmark.node.ListItem;
import org.commonmark.node.Node;
import org.commonmark.node.OrderedList;
import org.commonmark.node.Paragraph;
import org.commonmark.node.SoftLineBreak;
import org.commonmark.node.StrongEmphasis;
import org.commonmark.node.Text;
import org.commonmark.node.ThematicBreak;
import me.mrletsplay.simplehttpserver.dom.html.HtmlElement;
public class MdRenderer {
public HtmlElement render(Node node) {
MdRenderContext ctx = new MdRenderContext(this);
HtmlElement element = renderSingleNode(ctx, node);
if(element == null) return null;
Node ch = node.getFirstChild();
if(ch == null) return element;
do {
HtmlElement chEl = render(ch);
if(chEl == null) continue;
element.appendChild(chEl);
}while((ch = ch.getNext()) != null);
return element;
}
private HtmlElement renderSingleNode(MdRenderContext ctx, Node node) {
try {
Method m = MdRenderer.class.getDeclaredMethod("render", MdRenderContext.class, node.getClass());
return (HtmlElement) m.invoke(this, ctx, node);
}catch(NoSuchMethodException e) {
System.err.println("Warning: No render() method defined for " + node.getClass().getName());
return null;
} catch (SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
public HtmlElement render(MdRenderContext ctx, Document node) {
HtmlElement el = new HtmlElement("div");
el.setAttribute("md-document");
return el;
}
public HtmlElement render(MdRenderContext ctx, Heading node) {
return new HtmlElement("h" + Math.min(6, node.getLevel()));
}
public HtmlElement render(MdRenderContext ctx, Paragraph node) {
return new HtmlElement("p");
}
public HtmlElement render(MdRenderContext ctx, BlockQuote node) {
return new HtmlElement("blockquote");
}
public HtmlElement render(MdRenderContext ctx, BulletList node) {
return new HtmlElement("ul");
}
public HtmlElement render(MdRenderContext ctx, FencedCodeBlock node) {
HtmlElement el = new HtmlElement("pre");
HtmlElement code = new HtmlElement("code");
code.setText(node.getLiteral());
el.appendChild(code);
return el;
}
public HtmlElement render(MdRenderContext ctx, HtmlBlock node) {
return new RawHtmlElement(node.getLiteral());
}
public HtmlElement render(MdRenderContext ctx, ThematicBreak node) {
HtmlElement el = new HtmlElement("hr");
el.setSelfClosing(true);
return el;
}
public HtmlElement render(MdRenderContext ctx, IndentedCodeBlock node) {
HtmlElement el = new HtmlElement("pre");
HtmlElement code = new HtmlElement("code");
code.setText(node.getLiteral());
el.appendChild(code);
return el;
}
public HtmlElement render(MdRenderContext ctx, Link node) {
HtmlElement el = new HtmlElement("a");
el.setAttribute("href", node.getDestination());
el.setAttribute("title", node.getTitle());
return el;
}
public HtmlElement render(MdRenderContext ctx, ListItem node) {
return new HtmlElement("li");
}
public HtmlElement render(MdRenderContext ctx, OrderedList node) {
HtmlElement el = new HtmlElement("ol");
el.setAttribute("start", String.valueOf(node.getStartNumber()));
return el;
}
public HtmlElement render(MdRenderContext ctx, Image node) {
return HtmlElement.img(node.getDestination(), node.getTitle());
}
public HtmlElement render(MdRenderContext ctx, Emphasis node) {
return new HtmlElement("em");
}
public HtmlElement render(MdRenderContext ctx, StrongEmphasis node) {
return new HtmlElement("strong");
}
public HtmlElement render(MdRenderContext ctx, Text node) {
HtmlElement el = new HtmlElement("span");
el.setText(node.getLiteral());
return el;
}
public HtmlElement render(MdRenderContext ctx, Code node) {
HtmlElement el = new HtmlElement("code");
el.setText(node.getLiteral());
return el;
}
public HtmlElement render(MdRenderContext ctx, HtmlInline node) {
return new RawHtmlElement(node.getLiteral());
}
public HtmlElement render(MdRenderContext ctx, SoftLineBreak node) {
return new RawHtmlElement(" ");
}
public HtmlElement render(MdRenderContext ctx, HardLineBreak node) {
return HtmlElement.br();
}
public HtmlElement render(MdRenderContext ctx, LinkReferenceDefinition node) {
return null;
}
public HtmlElement render(MdRenderContext ctx, Strikethrough node) {
return new HtmlElement("del");
}
public HtmlElement render(MdRenderContext ctx, TableBlock node) {
return new HtmlElement("table");
}
public HtmlElement render(MdRenderContext ctx, TableHead node) {
return new HtmlElement("thead");
}
public HtmlElement render(MdRenderContext ctx, TableBody node) {
return new HtmlElement("tbody");
}
public HtmlElement render(MdRenderContext ctx, TableRow node) {
return new HtmlElement("tr");
}
public HtmlElement render(MdRenderContext ctx, TableCell node) {
return new HtmlElement("td");
}
public HtmlElement render(MdRenderContext ctx, Ins node) {
return new HtmlElement("ins");
}
public HtmlElement render(MdRenderContext ctx, TaskListItemMarker node) {
HtmlElement el = new HtmlElement("input");
el.setAttribute("type", "checkbox");
el.setAttribute("disabled");
if(node.isChecked()) el.setAttribute("checked");
return el;
}
}

View File

@ -0,0 +1,19 @@
package me.mrletsplay.mdblog.markdown;
import me.mrletsplay.simplehttpserver.dom.html.HtmlElement;
public class RawHtmlElement extends HtmlElement {
private String raw;
public RawHtmlElement(String raw) {
super("raw");
this.raw = raw;
}
@Override
public String toString() {
return raw;
}
}

View File

@ -0,0 +1,7 @@
body {
filter: invert();
}
h1 {
background-color: red;
}

View File

@ -0,0 +1,3 @@
h2 {
background-color: orange;
}