commit 43771ff9d600dec4c8ac83c70fe00de6b2f2a081 Author: MrLetsplay Date: Sun Jan 14 19:46:30 2024 +0100 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..125ec90 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/.settings +/bin +/.classpath +/TEST +/target/ +/files/ diff --git a/.project b/.project new file mode 100644 index 0000000..2b59a91 --- /dev/null +++ b/.project @@ -0,0 +1,23 @@ + + + MdBlog + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.m2e.core.maven2Nature + org.eclipse.jdt.core.javanature + + diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..eb6b5ee --- /dev/null +++ b/pom.xml @@ -0,0 +1,64 @@ + + 4.0.0 + MDTest + MDTest + 0.0.1-SNAPSHOT + + src/main/java + + + maven-compiler-plugin + 3.8.1 + + 17 + + + + + + + 0.21.0 + + + + + Graphite-Official + https://maven.graphite-official.com/releases + + + + + + me.mrletsplay + SimpleHTTPServer + 2.0-SNAPSHOT + + + org.commonmark + commonmark + ${commonmark.version} + + + org.commonmark + commonmark-ext-gfm-tables + ${commonmark.version} + + + org.commonmark + commonmark-ext-gfm-strikethrough + ${commonmark.version} + + + org.commonmark + commonmark-ext-ins + ${commonmark.version} + + + org.commonmark + commonmark-ext-task-list-items + ${commonmark.version} + + + \ No newline at end of file diff --git a/src/main/java/me/mrletsplay/mdblog/MdBlog.java b/src/main/java/me/mrletsplay/mdblog/MdBlog.java new file mode 100644 index 0000000..e681776 --- /dev/null +++ b/src/main/java/me/mrletsplay/mdblog/MdBlog.java @@ -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 watchedDirectories; + private static Map 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 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) {} + }); + } + +} diff --git a/src/main/java/me/mrletsplay/mdblog/blog/Post.java b/src/main/java/me/mrletsplay/mdblog/blog/Post.java new file mode 100644 index 0000000..1059cac --- /dev/null +++ b/src/main/java/me/mrletsplay/mdblog/blog/Post.java @@ -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))); + } + +} diff --git a/src/main/java/me/mrletsplay/mdblog/blog/PostMetadata.java b/src/main/java/me/mrletsplay/mdblog/blog/PostMetadata.java new file mode 100644 index 0000000..36d15c3 --- /dev/null +++ b/src/main/java/me/mrletsplay/mdblog/blog/PostMetadata.java @@ -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 tags) { + + public static PostMetadata load(String metadataString) { + Instant date = Instant.EPOCH; + String title = "Untitled Post"; + String author = "Unknown Author"; + Set 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); + } + +} diff --git a/src/main/java/me/mrletsplay/mdblog/markdown/MdException.java b/src/main/java/me/mrletsplay/mdblog/markdown/MdException.java new file mode 100644 index 0000000..1b71315 --- /dev/null +++ b/src/main/java/me/mrletsplay/mdblog/markdown/MdException.java @@ -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); + } + +} diff --git a/src/main/java/me/mrletsplay/mdblog/markdown/MdParser.java b/src/main/java/me/mrletsplay/mdblog/markdown/MdParser.java new file mode 100644 index 0000000..00e4bce --- /dev/null +++ b/src/main/java/me/mrletsplay/mdblog/markdown/MdParser.java @@ -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; + } + +} diff --git a/src/main/java/me/mrletsplay/mdblog/markdown/MdRenderContext.java b/src/main/java/me/mrletsplay/mdblog/markdown/MdRenderContext.java new file mode 100644 index 0000000..1cdd255 --- /dev/null +++ b/src/main/java/me/mrletsplay/mdblog/markdown/MdRenderContext.java @@ -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; + } + +} diff --git a/src/main/java/me/mrletsplay/mdblog/markdown/MdRenderer.java b/src/main/java/me/mrletsplay/mdblog/markdown/MdRenderer.java new file mode 100644 index 0000000..3f99e2d --- /dev/null +++ b/src/main/java/me/mrletsplay/mdblog/markdown/MdRenderer.java @@ -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; + } + +} diff --git a/src/main/java/me/mrletsplay/mdblog/markdown/RawHtmlElement.java b/src/main/java/me/mrletsplay/mdblog/markdown/RawHtmlElement.java new file mode 100644 index 0000000..3fbe6a9 --- /dev/null +++ b/src/main/java/me/mrletsplay/mdblog/markdown/RawHtmlElement.java @@ -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; + } + +} diff --git a/src/main/resources/style/base.css b/src/main/resources/style/base.css new file mode 100644 index 0000000..fefe07f --- /dev/null +++ b/src/main/resources/style/base.css @@ -0,0 +1,7 @@ +body { + filter: invert(); +} + +h1 { + background-color: red; +} \ No newline at end of file diff --git a/src/main/resources/style/post.css b/src/main/resources/style/post.css new file mode 100644 index 0000000..654c8be --- /dev/null +++ b/src/main/resources/style/post.css @@ -0,0 +1,3 @@ +h2 { + background-color: orange; +} \ No newline at end of file