Compare commits
24 commits
blog-0.0.1
...
master
Author | SHA1 | Date | |
---|---|---|---|
121a2c6915 | |||
0aadf69a42 | |||
159519c94d | |||
42a73ece51 | |||
1557a47987 | |||
a7ab3b400c | |||
950645dcef | |||
3d4597b198 | |||
567bbd8c37 | |||
0b91f2d7a1 | |||
d155079514 | |||
721d8d2768 | |||
734a1ef497 | |||
f265376ef8 | |||
930a22d55a | |||
37535e2c35 | |||
e89252bb71 | |||
0a6d8c09c5 | |||
8e4fae069d | |||
b339d6d239 | |||
d5f6eaf487 | |||
3c3c8e0c5c | |||
b2725d9960 | |||
9979845efa |
21 changed files with 585 additions and 268 deletions
|
@ -5,7 +5,7 @@ jobs:
|
||||||
container: eclipse-temurin:21-alpine
|
container: eclipse-temurin:21-alpine
|
||||||
steps:
|
steps:
|
||||||
- name: Install JDK and other deps
|
- name: Install JDK and other deps
|
||||||
run: apk add maven git nodejs curl
|
run: apk upgrade && apk add maven git nodejs curl
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: https://github.com/actions/checkout@v4
|
uses: https://github.com/actions/checkout@v4
|
||||||
|
@ -13,9 +13,13 @@ jobs:
|
||||||
- name: Build
|
- name: Build
|
||||||
run: mvn package
|
run: mvn package
|
||||||
|
|
||||||
|
- name: Add build data
|
||||||
|
run: "mkdir meta && git log -1 > meta/commit.txt && date > meta/build-date.txt"
|
||||||
|
|
||||||
- name: Upload artifacts
|
- name: Upload artifacts
|
||||||
uses: https://github.com/actions/upload-artifact@v3
|
uses: https://github.com/actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: built-jar
|
path: |
|
||||||
path: target/blog-software-java-*.jar
|
meta
|
||||||
|
target/blog-*.jar
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
42
README.md
42
README.md
|
@ -2,14 +2,46 @@ blog-software(config, template, content) = blog website
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
1. [Download the program](https://git.m724.eu/Minecon724/blog-software-java/actions/runs/latest/artifacts/built-jar)
|
1. [Download the program from here](/Minecon724/blog-software-java/releases)
|
||||||
2. Get a working directory. [Example](https://git.m724.eu/Minecon724/blog-software-java/src/branch/master/example_workdir) \
|
2. Run the program:
|
||||||
Don't forget to `git init`!
|
|
||||||
3. Run the program:
|
|
||||||
```shell
|
```shell
|
||||||
java -jar blog-software-java-1.0-SNAPSHOT.jar
|
java -jar blog-0.0.1-shaded.jar -s example_workdir
|
||||||
```
|
```
|
||||||
|
|
||||||
|
For tips on how to create your own project (workdir), see [Project format](#Project format) below.
|
||||||
|
|
||||||
## Important caveats
|
## Important caveats
|
||||||
Generated site must be the root of a subdomain, like `https://example.com/`. \
|
Generated site must be the root of a subdomain, like `https://example.com/`. \
|
||||||
You can't put it in a directory, like `https://example.com/blog/`
|
You can't put it in a directory, like `https://example.com/blog/`
|
||||||
|
|
||||||
|
## API
|
||||||
|
There's no "API," but it's possible to integrate this into your Java project.
|
||||||
|
|
||||||
|
See [Main.java](/Minecon724/blog-software-java/src/branch/master/src/main/java/eu/m724/blog/Main.java) for an example. \
|
||||||
|
If you need a Maven dependency, [see here](/Minecon724/-/packages/maven/eu.m724-blog)
|
||||||
|
|
||||||
|
## Project format
|
||||||
|
There's an ["Example workdir"](/Minecon724/blog-software-java/src/branch/master/example_workdir) which you can take inspiration from.
|
||||||
|
|
||||||
|
Basically:
|
||||||
|
- `assets/` - contains static assets
|
||||||
|
- `posts/` - contains posts. Post format:
|
||||||
|
- Header / metadata:
|
||||||
|
- `title A title` - post title
|
||||||
|
- `summary This is a post with a title` - post summary
|
||||||
|
- `live` - is the post live (not draft), doesn't need an argument
|
||||||
|
- Custom properties, which are Strings
|
||||||
|
- ` ` - Empty line separates header from content
|
||||||
|
- Post content in HTML. Generally not sanitized, but depends on template.
|
||||||
|
- `template/` - contains the template, see [Template format](#Template format) below
|
||||||
|
- `site-config.yml` - the site configuration
|
||||||
|
- `name: my blog` property - site name
|
||||||
|
- `baseUrl: https://example.com` property - URL of the site. It must be a root URL - folders are currently not supported. This is used only for the RSS feed.
|
||||||
|
- Custom properties, which can be anything as they are Objects
|
||||||
|
|
||||||
|
## Template format
|
||||||
|
https://pebbletemplates.io is used
|
||||||
|
|
||||||
|
- `static/` - contains static assets
|
||||||
|
- `article_template.html` - post template
|
||||||
|
- `index_template.html` - index.html template
|
5
example_workdir/render.yml
Normal file
5
example_workdir/render.yml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
# Render options here
|
||||||
|
|
||||||
|
compress:
|
||||||
|
- gz
|
||||||
|
- zstd
|
|
@ -1,9 +0,0 @@
|
||||||
{
|
|
||||||
"name": "my blog",
|
|
||||||
"baseUrl": "https://example.com",
|
|
||||||
|
|
||||||
"coolProperty": 1231,
|
|
||||||
"coolerProperty": {
|
|
||||||
"isMap": "yes"
|
|
||||||
}
|
|
||||||
}
|
|
10
example_workdir/site.yml
Normal file
10
example_workdir/site.yml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
name: my blog
|
||||||
|
baseUrl: https://example.com/blog
|
||||||
|
|
||||||
|
coolProperty: 1231
|
||||||
|
coolerProperty:
|
||||||
|
isMap: true
|
||||||
|
aList:
|
||||||
|
- a value
|
||||||
|
- another value
|
||||||
|
- check out site-config.yml!
|
|
@ -20,7 +20,12 @@
|
||||||
<h1>{{ site.name }} - {{ site.custom.coolerProperty.get('isMap') }}</h1>
|
<h1>{{ site.name }} - {{ site.custom.coolerProperty.get('isMap') }}</h1>
|
||||||
<article>
|
<article>
|
||||||
<header>
|
<header>
|
||||||
<h2><a href="">{{ article.title }}</a></h2>
|
<h2>
|
||||||
|
<a href="">{{ article.title }}</a>
|
||||||
|
{% if article.draft %}
|
||||||
|
<strong>DRAFT</strong>
|
||||||
|
{% endif %}
|
||||||
|
</h2>
|
||||||
<h4>{{ article.summary }}</h4>
|
<h4>{{ article.summary }}</h4>
|
||||||
<p>{{ article.createdAt | date("dd.MM.yyyy") }} by {{ article.createdBy }}</p>
|
<p>{{ article.createdAt | date("dd.MM.yyyy") }} by {{ article.createdBy }}</p>
|
||||||
|
|
||||||
|
|
|
@ -19,5 +19,11 @@
|
||||||
</article>
|
</article>
|
||||||
</a>
|
</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
{% for e in site.custom.coolerProperty.aList %}
|
||||||
|
<li>{{ e }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
31
pom.xml
31
pom.xml
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
<groupId>eu.m724</groupId>
|
<groupId>eu.m724</groupId>
|
||||||
<artifactId>blog</artifactId>
|
<artifactId>blog</artifactId>
|
||||||
<version>0.0.1</version>
|
<version>0.0.2-SNAPSHOT</version>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<maven.compiler.source>21</maven.compiler.source>
|
<maven.compiler.source>21</maven.compiler.source>
|
||||||
|
@ -22,7 +22,8 @@
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>commons-cli</groupId>
|
<groupId>commons-cli</groupId>
|
||||||
<artifactId>commons-cli</artifactId>
|
<artifactId>commons-cli</artifactId>
|
||||||
<version>1.8.0</version>
|
<version>1.9.0</version>
|
||||||
|
<optional>true</optional>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.eclipse.jgit</groupId>
|
<groupId>org.eclipse.jgit</groupId>
|
||||||
|
@ -32,19 +33,31 @@
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.pebbletemplates</groupId>
|
<groupId>io.pebbletemplates</groupId>
|
||||||
<artifactId>pebble</artifactId>
|
<artifactId>pebble</artifactId>
|
||||||
<version>3.2.2</version>
|
<version>3.2.3</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.json</groupId>
|
<groupId>org.snakeyaml</groupId>
|
||||||
<artifactId>json</artifactId>
|
<artifactId>snakeyaml-engine</artifactId>
|
||||||
<version>20250107</version>
|
<version>2.9</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>commons-io</groupId>
|
<groupId>commons-io</groupId> <!-- https://stackoverflow.com/questions/32184114 -->
|
||||||
<artifactId>commons-io</artifactId>
|
<artifactId>commons-io</artifactId>
|
||||||
<version>2.16.1</version>
|
<version>2.18.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.commons</groupId>
|
||||||
|
<artifactId>commons-compress</artifactId>
|
||||||
|
<version>1.27.1</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.luben</groupId>
|
||||||
|
<artifactId>zstd-jni</artifactId>
|
||||||
|
<version>1.5.6-10</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.slf4j</groupId>
|
<groupId>org.slf4j</groupId>
|
||||||
<artifactId>slf4j-api</artifactId>
|
<artifactId>slf4j-api</artifactId>
|
||||||
|
@ -149,6 +162,6 @@
|
||||||
|
|
||||||
<scm>
|
<scm>
|
||||||
<developerConnection>scm:git:git@git.m724.eu:Minecon724/blog-software-java.git</developerConnection>
|
<developerConnection>scm:git:git@git.m724.eu:Minecon724/blog-software-java.git</developerConnection>
|
||||||
<tag>blog-0.0.1</tag>
|
<tag>HEAD</tag>
|
||||||
</scm>
|
</scm>
|
||||||
</project>
|
</project>
|
|
@ -1,21 +0,0 @@
|
||||||
#release configuration
|
|
||||||
#Sat Feb 08 12:48:09 CET 2025
|
|
||||||
completedPhase=check-poms
|
|
||||||
exec.pomFileName=pom.xml
|
|
||||||
exec.snapshotReleasePluginAllowed=false
|
|
||||||
pinExternals=false
|
|
||||||
preparationGoals=clean verify
|
|
||||||
project.scm.eu.m724\:blog-software-java.developerConnection=scm\:git\:git@git.m724.eu\:Minecon724/blog-software-java.git
|
|
||||||
project.scm.eu.m724\:blog-software-java.tag=HEAD
|
|
||||||
projectVersionPolicyConfig=<projectVersionPolicyConfig>${projectVersionPolicyConfig}</projectVersionPolicyConfig>\n
|
|
||||||
projectVersionPolicyId=default
|
|
||||||
pushChanges=true
|
|
||||||
releaseStrategyId=default
|
|
||||||
remoteTagging=true
|
|
||||||
scm.branchCommitComment=@{prefix} prepare branch @{releaseLabel}
|
|
||||||
scm.commentPrefix=[maven-release-plugin]
|
|
||||||
scm.developmentCommitComment=@{prefix} prepare for next development iteration
|
|
||||||
scm.releaseCommitComment=@{prefix} prepare release @{releaseLabel}
|
|
||||||
scm.rollbackCommitComment=@{prefix} rollback the release of @{releaseLabel}
|
|
||||||
scm.tagNameFormat=@{project.artifactId}-@{project.version}
|
|
||||||
scm.url=scm\:git\:git@git.m724.eu\:Minecon724/blog-software-java.git
|
|
|
@ -1,11 +1,17 @@
|
||||||
package eu.m724.blog;
|
package eu.m724.blog;
|
||||||
|
|
||||||
|
import eu.m724.blog.compress.FileCompressor;
|
||||||
import eu.m724.blog.data.Feed;
|
import eu.m724.blog.data.Feed;
|
||||||
import eu.m724.blog.data.Post;
|
import eu.m724.blog.data.Post;
|
||||||
|
import eu.m724.blog.data.RenderOptions;
|
||||||
import eu.m724.blog.data.Site;
|
import eu.m724.blog.data.Site;
|
||||||
|
import eu.m724.blog.template.TemplateRenderer;
|
||||||
|
import org.apache.commons.compress.compressors.CompressorException;
|
||||||
import org.apache.commons.io.file.PathUtils;
|
import org.apache.commons.io.file.PathUtils;
|
||||||
import org.eclipse.jgit.api.Git;
|
import org.eclipse.jgit.api.Git;
|
||||||
import org.eclipse.jgit.lib.RepositoryBuilder;
|
import org.eclipse.jgit.lib.RepositoryBuilder;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.FileAlreadyExistsException;
|
import java.nio.file.FileAlreadyExistsException;
|
||||||
|
@ -13,19 +19,34 @@ import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@code BlogBuilder} class facilitates building a static blog by managing templates,
|
||||||
|
* assets, posts, and rendering output files. It uses a Git repository as the
|
||||||
|
* source for the blog's content and configuration.
|
||||||
|
*/
|
||||||
public class BlogBuilder {
|
public class BlogBuilder {
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(BlogBuilder.class);
|
||||||
|
|
||||||
private final Git git;
|
private final Git git;
|
||||||
private final Path workingDirectory;
|
private final Path workingDirectory;
|
||||||
|
|
||||||
private Site site;
|
private Site site;
|
||||||
private TemplateRenderer template;
|
private TemplateRenderer template;
|
||||||
|
private RenderOptions renderOptions;
|
||||||
|
|
||||||
private Path templateDirectory;
|
private Path templateDirectory;
|
||||||
private Path outputDirectory;
|
private Path outputDirectory;
|
||||||
private boolean renderDrafts = false;
|
private boolean renderDrafts = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a {@link BlogBuilder} instance using the provided Git repository.
|
||||||
|
*
|
||||||
|
* @param git the Git repository to be used for the blog.
|
||||||
|
*/
|
||||||
public BlogBuilder(Git git) {
|
public BlogBuilder(Git git) {
|
||||||
this.git = git;
|
this.git = git;
|
||||||
|
|
||||||
|
@ -34,6 +55,14 @@ public class BlogBuilder {
|
||||||
this.outputDirectory = workingDirectory.resolve("generated_out");
|
this.outputDirectory = workingDirectory.resolve("generated_out");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new {@link BlogBuilder} instance for the specified working directory.
|
||||||
|
* The directory is expected to be a Git repository.
|
||||||
|
*
|
||||||
|
* @param workingDirectory the root path of the blog, which must contain a Git repository.
|
||||||
|
* @return a {@link BlogBuilder} instance
|
||||||
|
* @throws IOException if there is an error accessing the Git repository
|
||||||
|
*/
|
||||||
public static BlogBuilder fromPath(Path workingDirectory) throws IOException {
|
public static BlogBuilder fromPath(Path workingDirectory) throws IOException {
|
||||||
var repository = new RepositoryBuilder()
|
var repository = new RepositoryBuilder()
|
||||||
.setGitDir(workingDirectory.resolve(".git").toFile())
|
.setGitDir(workingDirectory.resolve(".git").toFile())
|
||||||
|
@ -45,16 +74,34 @@ public class BlogBuilder {
|
||||||
return new BlogBuilder(git);
|
return new BlogBuilder(git);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the directory to be used for templates in the blog build process.
|
||||||
|
*
|
||||||
|
* @param templateDirectory the path to the template directory
|
||||||
|
* @return the current instance of {@link BlogBuilder}
|
||||||
|
*/
|
||||||
public BlogBuilder templateDirectory(Path templateDirectory) {
|
public BlogBuilder templateDirectory(Path templateDirectory) {
|
||||||
this.templateDirectory = templateDirectory;
|
this.templateDirectory = templateDirectory;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the directory where the output files will be saved during the blog build process.
|
||||||
|
*
|
||||||
|
* @param outputDirectory the path representing the directory for output files
|
||||||
|
* @return the current instance of {@link BlogBuilder}
|
||||||
|
*/
|
||||||
public BlogBuilder outputDirectory(Path outputDirectory) {
|
public BlogBuilder outputDirectory(Path outputDirectory) {
|
||||||
this.outputDirectory = outputDirectory;
|
this.outputDirectory = outputDirectory;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures whether drafts should be rendered in the blog build process.
|
||||||
|
*
|
||||||
|
* @param renderDrafts a boolean flag indicating whether to include drafts in the rendering process.
|
||||||
|
* @return the current instance of {@link BlogBuilder}
|
||||||
|
*/
|
||||||
public BlogBuilder renderDrafts(boolean renderDrafts) {
|
public BlogBuilder renderDrafts(boolean renderDrafts) {
|
||||||
this.renderDrafts = renderDrafts;
|
this.renderDrafts = renderDrafts;
|
||||||
return this;
|
return this;
|
||||||
|
@ -72,18 +119,55 @@ public class BlogBuilder {
|
||||||
Files.createDirectory(outputDirectory);
|
Files.createDirectory(outputDirectory);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the blog by generating templates, copying assets, and rendering posts.
|
||||||
|
*
|
||||||
|
* @throws IOException if an I/O error occurs
|
||||||
|
*/
|
||||||
public void build() throws IOException {
|
public void build() throws IOException {
|
||||||
|
LOGGER.debug("Loading site...");
|
||||||
if (site == null)
|
if (site == null)
|
||||||
this.site = Site.fromConfig(workingDirectory.resolve("site-config.json"));
|
this.site = Site.fromConfig(workingDirectory.resolve("site.yml"));
|
||||||
|
|
||||||
|
if (renderOptions == null)
|
||||||
|
this.renderOptions = RenderOptions.fromConfig(workingDirectory.resolve("render.yml"));
|
||||||
|
|
||||||
if (template == null)
|
if (template == null)
|
||||||
this.template = new TemplateRenderer(templateDirectory);
|
this.template = new TemplateRenderer(site, templateDirectory);
|
||||||
|
|
||||||
copyIfExists(workingDirectory.resolve("assets"), outputDirectory.resolve("assets"));
|
LOGGER.debug("Copying assets...");
|
||||||
copyIfExists(templateDirectory.resolve("static"), outputDirectory.resolve("static"));
|
copyTree(workingDirectory.resolve("assets"), outputDirectory.resolve("assets"));
|
||||||
|
copyTree(templateDirectory.resolve("static"), outputDirectory.resolve("static"));
|
||||||
|
|
||||||
|
LOGGER.debug("Rendering posts...");
|
||||||
|
var posts = renderPosts();
|
||||||
|
|
||||||
|
LOGGER.debug("Rendering meta...");
|
||||||
|
posts.sort(Comparator.comparing(Post::createdAt).reversed());
|
||||||
|
Files.writeString(outputDirectory.resolve("index.html"), template.renderIndex(posts));
|
||||||
|
|
||||||
|
Files.writeString(outputDirectory.resolve("posts.rss"), Feed.generateRss(site, posts));
|
||||||
|
|
||||||
|
if (!renderOptions.compress().isEmpty()) {
|
||||||
|
LOGGER.debug("Compressing...");
|
||||||
|
compressOutput();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO should server be here in builder really
|
||||||
|
public void startServer(boolean openBrowser) throws IOException {
|
||||||
|
var server = new Server(outputDirectory, site.directory());
|
||||||
|
server.start();
|
||||||
|
|
||||||
|
if (openBrowser) {
|
||||||
|
server.openBrowser();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Post> renderPosts() throws IOException {
|
||||||
Files.createDirectory(outputDirectory.resolve("post"));
|
Files.createDirectory(outputDirectory.resolve("post"));
|
||||||
var postDirectory = workingDirectory.resolve("posts");
|
var postDirectory = workingDirectory.resolve("posts");
|
||||||
|
|
||||||
var posts = new ArrayList<Post>();
|
var posts = new ArrayList<Post>();
|
||||||
|
|
||||||
try (var stream = Files.walk(postDirectory)) {
|
try (var stream = Files.walk(postDirectory)) {
|
||||||
|
@ -92,7 +176,7 @@ public class BlogBuilder {
|
||||||
continue; // directory is created below
|
continue; // directory is created below
|
||||||
|
|
||||||
if (!path.toString().endsWith(".html")) {
|
if (!path.toString().endsWith(".html")) {
|
||||||
System.out.println("Post " + path.getFileName() + ": unsupported file type");
|
LOGGER.warn("Post {}: unsupported file type", path.getFileName());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,11 +184,11 @@ public class BlogBuilder {
|
||||||
var post = Post.fromFile(git, path);
|
var post = Post.fromFile(git, path);
|
||||||
|
|
||||||
if (post.draft() && !renderDrafts) {
|
if (post.draft() && !renderDrafts) {
|
||||||
System.out.println("Post " + path.getFileName() + ": draft, ignoring");
|
LOGGER.info("Post {}: draft, ignoring", path.getFileName());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var render = template.renderPost(site, post);
|
var render = template.renderPost(post);
|
||||||
var outFile = outputDirectory.resolve("post").resolve(path);
|
var outFile = outputDirectory.resolve("post").resolve(path);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -116,20 +200,34 @@ public class BlogBuilder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
posts.sort(Comparator.comparing(Post::createdAt).reversed());
|
return posts;
|
||||||
Files.writeString(outputDirectory.resolve("index.html"), template.renderIndex(site, posts));
|
}
|
||||||
|
|
||||||
Files.writeString(outputDirectory.resolve("posts.rss"), Feed.generateRss(site, posts));
|
private void compressOutput() throws IOException {
|
||||||
|
var compressors = renderOptions.compress().stream()
|
||||||
|
.map(FileCompressor::new)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
Set<Path> tree;
|
||||||
|
try (var walk = Files.walk(outputDirectory)) {
|
||||||
|
tree = walk.filter(Files::isRegularFile).collect(Collectors.toSet());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var compressor : compressors) {
|
||||||
|
for (var path : tree) {
|
||||||
|
try {
|
||||||
|
compressor.compress(path);
|
||||||
|
} catch (CompressorException e) {
|
||||||
|
LOGGER.error("Error compressing {} to {}: {}", path, compressor.getAlgorithm(), e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Internal functions */
|
/* Internal functions */
|
||||||
|
|
||||||
private boolean copyTree(Path srcDir, Path destDir) throws IOException {
|
private void copyTree(Path srcDir, Path destDir) throws IOException {
|
||||||
if (!Files.isDirectory(srcDir)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try (var walk = Files.walk(srcDir)) {
|
try (var walk = Files.walk(srcDir)) {
|
||||||
for (var src : walk.collect(Collectors.toSet())) {
|
for (var src : walk.collect(Collectors.toSet())) {
|
||||||
var rel = srcDir.relativize(src);
|
var rel = srcDir.relativize(src);
|
||||||
|
@ -146,17 +244,5 @@ public class BlogBuilder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void copyIfExists(Path srcDir, Path destDir) throws IOException {
|
|
||||||
System.out.print(srcDir);
|
|
||||||
|
|
||||||
if (copyTree(srcDir, destDir)) {
|
|
||||||
System.out.println(" copied");
|
|
||||||
} else {
|
|
||||||
System.out.println(" doesn't exist, not copying");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
package eu.m724.blog;
|
package eu.m724.blog;
|
||||||
|
|
||||||
import org.apache.commons.cli.*;
|
import org.apache.commons.cli.*;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
|
||||||
public class Main {
|
public class Main {
|
||||||
public static void main(String[] args) throws IOException {
|
private static final Logger LOGGER = LoggerFactory.getLogger(Main.class);
|
||||||
System.out.println("Hello world!");
|
|
||||||
|
|
||||||
|
public static void main(String[] args) throws IOException {
|
||||||
var commandLine = getCommandLine(args);
|
var commandLine = getCommandLine(args);
|
||||||
|
|
||||||
if (commandLine == null)
|
if (commandLine == null)
|
||||||
|
@ -36,21 +38,15 @@ public class Main {
|
||||||
.renderDrafts(renderDrafts);
|
.renderDrafts(renderDrafts);
|
||||||
|
|
||||||
builder.mkdirs(force);
|
builder.mkdirs(force);
|
||||||
|
|
||||||
|
LOGGER.info("Building...");
|
||||||
builder.build();
|
builder.build();
|
||||||
|
|
||||||
var end = System.nanoTime();
|
var end = System.nanoTime();
|
||||||
System.out.printf("Exported to %s (%.2f ms)\n", outputDirectory, (end - start) / 1000000.0);
|
// BAD
|
||||||
|
LOGGER.info("Exported to {} in {} ms", outputDirectory.toAbsolutePath(), "%.2f".formatted((end - start) / 1000000.0));
|
||||||
|
|
||||||
/* Server process */
|
builder.startServer(openBrowser);
|
||||||
|
|
||||||
if (startServer) {
|
|
||||||
var server = new Server(outputDirectory);
|
|
||||||
server.start();
|
|
||||||
|
|
||||||
if (openBrowser) {
|
|
||||||
server.openBrowser();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static CommandLine getCommandLine(String[] args) {
|
private static CommandLine getCommandLine(String[] args) {
|
||||||
|
|
|
@ -1,71 +1,81 @@
|
||||||
package eu.m724.blog;
|
package eu.m724.blog;
|
||||||
|
|
||||||
import com.sun.net.httpserver.HttpExchange;
|
|
||||||
import com.sun.net.httpserver.HttpHandler;
|
|
||||||
import com.sun.net.httpserver.HttpServer;
|
import com.sun.net.httpserver.HttpServer;
|
||||||
|
import com.sun.net.httpserver.SimpleFileServer;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
|
||||||
public class Server implements HttpHandler {
|
/**
|
||||||
private final Path webroot;
|
* The {@code Server} class represents a basic HTTP server designed to serve files
|
||||||
|
* from a specified webroot directory.<br>
|
||||||
|
* {@link SimpleFileServer} is used.
|
||||||
|
*/
|
||||||
|
public class Server {
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(Server.class);
|
||||||
|
|
||||||
|
private final Path sitePath;
|
||||||
|
private final String contextPath;
|
||||||
private InetSocketAddress listenAddress;
|
private InetSocketAddress listenAddress;
|
||||||
|
|
||||||
public Server(Path webroot, InetSocketAddress listenAddress) {
|
/**
|
||||||
this.webroot = webroot;
|
* Constructs a {@link Server} instance with the specified webroot directory
|
||||||
|
* and the specified listening address.
|
||||||
|
*
|
||||||
|
* @param sitePath the directory from which files will be served
|
||||||
|
* @param contextPath TODO explain
|
||||||
|
* @param listenAddress the address and port the server will listen on
|
||||||
|
*/
|
||||||
|
public Server(Path sitePath, String contextPath, InetSocketAddress listenAddress) {
|
||||||
|
this.sitePath = sitePath;
|
||||||
|
this.contextPath = contextPath;
|
||||||
this.listenAddress = listenAddress;
|
this.listenAddress = listenAddress;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Server(Path webroot) {
|
/**
|
||||||
this(webroot, new InetSocketAddress("localhost", 0));
|
* Constructs a {@link Server} instance with the specified webroot directory.
|
||||||
|
* The server will bind to the localhost address with a dynamically chosen port.
|
||||||
|
*
|
||||||
|
* @param sitePath the directory from which files will be served
|
||||||
|
* @param contextPath TODO explain
|
||||||
|
*/
|
||||||
|
public Server(Path sitePath, String contextPath) {
|
||||||
|
this(sitePath, contextPath, new InetSocketAddress("localhost", 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts an HTTP server on the specified listen address.
|
||||||
|
*
|
||||||
|
* @throws IOException if an I/O error occurs during server initialization
|
||||||
|
*/
|
||||||
public void start() throws IOException {
|
public void start() throws IOException {
|
||||||
|
System.out.println(contextPath);
|
||||||
var server = HttpServer.create(listenAddress, 0);
|
var server = HttpServer.create(listenAddress, 0);
|
||||||
server.createContext("/", this);
|
server.createContext(contextPath, SimpleFileServer.createFileHandler(sitePath.toAbsolutePath()));
|
||||||
server.start();
|
server.start();
|
||||||
|
|
||||||
System.out.println("Server started on http:/" + server.getAddress());
|
LOGGER.info("Server started on http:/{}", server.getAddress());
|
||||||
|
|
||||||
this.listenAddress = server.getAddress();
|
this.listenAddress = server.getAddress();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to open the default web browser and navigate to the server's URL.
|
||||||
|
*/
|
||||||
public void openBrowser() {
|
public void openBrowser() {
|
||||||
try {
|
try {
|
||||||
var process = Runtime.getRuntime().exec(new String[] { "xdg-open", "http://" + listenAddress.getHostString() + ":" + listenAddress.getPort() });
|
var process = Runtime.getRuntime().exec(new String[] { "xdg-open", "http://" + listenAddress.getHostString() + ":" + listenAddress.getPort() + contextPath });
|
||||||
var code = process.waitFor();
|
var code = process.waitFor();
|
||||||
if (code != 0) {
|
if (code != 0) {
|
||||||
throw new Exception("Exit code " + code);
|
throw new Exception("Exit code " + code);
|
||||||
}
|
}
|
||||||
System.out.println("Opened browser");
|
|
||||||
|
LOGGER.info("Opened browser"); // TODO make this debug?
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
System.out.println("Failed to open browser: " + e);
|
LOGGER.error("Failed to open browser: {}", String.valueOf(e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handle(HttpExchange exchange) throws IOException {
|
|
||||||
var path = webroot.resolve(exchange.getRequestURI().getPath().substring(1));
|
|
||||||
|
|
||||||
if (Files.isDirectory(path)) {
|
|
||||||
path = path.resolve("index.html");
|
|
||||||
}
|
|
||||||
|
|
||||||
var code = 404;
|
|
||||||
var content = "Not found".getBytes(StandardCharsets.UTF_8);
|
|
||||||
|
|
||||||
if (Files.isRegularFile(path)) {
|
|
||||||
code = 200;
|
|
||||||
content = Files.readAllBytes(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
exchange.sendResponseHeaders(code, content.length);
|
|
||||||
|
|
||||||
var body = exchange.getResponseBody();
|
|
||||||
body.write(content);
|
|
||||||
body.close();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,87 +0,0 @@
|
||||||
package eu.m724.blog;
|
|
||||||
|
|
||||||
import eu.m724.blog.data.Post;
|
|
||||||
import eu.m724.blog.data.Site;
|
|
||||||
import io.pebbletemplates.pebble.PebbleEngine;
|
|
||||||
import io.pebbletemplates.pebble.extension.AbstractExtension;
|
|
||||||
import io.pebbletemplates.pebble.extension.Function;
|
|
||||||
import io.pebbletemplates.pebble.loader.FileLoader;
|
|
||||||
import io.pebbletemplates.pebble.template.EvaluationContext;
|
|
||||||
import io.pebbletemplates.pebble.template.PebbleTemplate;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.StringWriter;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public class TemplateRenderer {
|
|
||||||
private final PebbleTemplate indexTemplate, articleTemplate;
|
|
||||||
|
|
||||||
public TemplateRenderer(Path templateDirectory) {
|
|
||||||
var loader = new FileLoader();
|
|
||||||
loader.setPrefix(templateDirectory.toString());
|
|
||||||
loader.setSuffix(".html");
|
|
||||||
|
|
||||||
var pebbleEngine = new PebbleEngine.Builder()
|
|
||||||
.loader(loader)
|
|
||||||
.extension(new AbstractExtension() {
|
|
||||||
@Override
|
|
||||||
public Map<String, Function> getFunctions() {
|
|
||||||
return Map.of(
|
|
||||||
"static", new Function() {
|
|
||||||
@Override
|
|
||||||
public List<String> getArgumentNames() {
|
|
||||||
return List.of("path");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Object execute(Map<String, Object> args, PebbleTemplate self, EvaluationContext context, int lineNumber) {
|
|
||||||
return "/static/" + args.get("path"); // TODO for more advanced stuff
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"asset", new Function() {
|
|
||||||
@Override
|
|
||||||
public List<String> getArgumentNames() {
|
|
||||||
return List.of("path");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Object execute(Map<String, Object> args, PebbleTemplate self, EvaluationContext context, int lineNumber) {
|
|
||||||
return "/assets/" + args.get("path"); // TODO for more advanced stuff
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.build();
|
|
||||||
|
|
||||||
this.indexTemplate = pebbleEngine.getTemplate("index_template");
|
|
||||||
this.articleTemplate = pebbleEngine.getTemplate("article_template");
|
|
||||||
}
|
|
||||||
|
|
||||||
public String renderIndex(Site site, ArrayList<Post> posts) throws IOException {
|
|
||||||
Map<String, Object> context = Map.of(
|
|
||||||
"site", site,
|
|
||||||
"articles", posts
|
|
||||||
);
|
|
||||||
|
|
||||||
var writer = new StringWriter();
|
|
||||||
indexTemplate.evaluate(writer, context);
|
|
||||||
|
|
||||||
return writer.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public String renderPost(Site site, Post post) throws IOException {
|
|
||||||
Map<String, Object> context = Map.of(
|
|
||||||
"site", site,
|
|
||||||
"article", post
|
|
||||||
);
|
|
||||||
|
|
||||||
var writer = new StringWriter();
|
|
||||||
articleTemplate.evaluate(writer, context);
|
|
||||||
|
|
||||||
return writer.toString();
|
|
||||||
}
|
|
||||||
}
|
|
36
src/main/java/eu/m724/blog/compress/FileCompressor.java
Normal file
36
src/main/java/eu/m724/blog/compress/FileCompressor.java
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
package eu.m724.blog.compress;
|
||||||
|
|
||||||
|
import org.apache.commons.compress.compressors.CompressorException;
|
||||||
|
import org.apache.commons.compress.compressors.CompressorStreamFactory;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.*;
|
||||||
|
|
||||||
|
public class FileCompressor {
|
||||||
|
private final String algorithm;
|
||||||
|
|
||||||
|
public FileCompressor(String algorithm) {
|
||||||
|
this.algorithm = algorithm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void compress(Path source) throws IOException, CompressorException {
|
||||||
|
var destination = source.resolveSibling(source.getFileName() + "." + algorithm);
|
||||||
|
compress(source, destination);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void compress(Path source, Path destination) throws IOException, CompressorException {
|
||||||
|
if (Files.exists(destination))
|
||||||
|
throw new FileAlreadyExistsException(destination.toString());
|
||||||
|
|
||||||
|
try (
|
||||||
|
var outputStream = new CompressorStreamFactory()
|
||||||
|
.createCompressorOutputStream(algorithm, Files.newOutputStream(destination))
|
||||||
|
) {
|
||||||
|
Files.copy(source, outputStream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAlgorithm() {
|
||||||
|
return algorithm;
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,17 +4,32 @@ import java.time.format.DateTimeFormatter;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class Feed {
|
public class Feed {
|
||||||
|
private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss zzz");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates an RSS feed XML string for a given website and its list of blog posts.
|
||||||
|
*
|
||||||
|
* @param site the {@code Site} object representing the website for which the RSS feed is generated
|
||||||
|
* @param posts the list of {@code Post} objects representing the blog posts to include in the RSS feed
|
||||||
|
* @return a {@code String} containing the formatted RSS feed in XML
|
||||||
|
*/
|
||||||
public static String generateRss(Site site, List<Post> posts) {
|
public static String generateRss(Site site, List<Post> posts) {
|
||||||
var content = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?><rss version=\"2.0\">";
|
var content = new StringBuilder("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>");
|
||||||
content += "<channel><title>%s</title><link>%s</link>".formatted(site.name(), site.baseUrl());
|
content.append("<rss version=\"2.0\"><channel>");
|
||||||
|
content.append("<title>%s</title>".formatted(site.name()));
|
||||||
|
content.append("<link>%s</link>".formatted(site.baseUrl()));
|
||||||
|
|
||||||
var formatter = DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss zzz");
|
|
||||||
for (var post : posts) {
|
for (var post : posts) {
|
||||||
content += "<item><title>%s</title><link>%s/post/%s.html</link><description>%s</description><pubDate>%s</pubDate></item>".formatted(post.title(), site.baseUrl(), post.slug(), post.summary(), post.createdAt().format(formatter));
|
content.append("<item>");
|
||||||
|
content.append("<title>%s</title>".formatted(post.title()));
|
||||||
|
content.append("<link>%s/post/%s.html</link>".formatted(site.baseUrl(), post.slug()));
|
||||||
|
content.append("<description>%s</description>".formatted(post.summary()));
|
||||||
|
content.append("<pubDate>%s</pubDate>".formatted(post.createdAt().format(formatter)));
|
||||||
|
content.append("</item>");
|
||||||
}
|
}
|
||||||
|
|
||||||
content += "</channel></rss>";
|
content.append("</channel></rss>");
|
||||||
|
|
||||||
return content;
|
return content.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,8 @@ package eu.m724.blog.data;
|
||||||
|
|
||||||
import org.eclipse.jgit.api.Git;
|
import org.eclipse.jgit.api.Git;
|
||||||
import org.eclipse.jgit.api.errors.GitAPIException;
|
import org.eclipse.jgit.api.errors.GitAPIException;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
|
@ -10,6 +12,21 @@ import java.time.*;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@code Post} class represents a blog post with various attributes including metadata and content.
|
||||||
|
*
|
||||||
|
* @param slug A unique identifier for the post derived from the file name.
|
||||||
|
* @param title The title of the post.
|
||||||
|
* @param summary A brief summary of the post content.
|
||||||
|
* @param draft Indicates whether the post is marked as a draft or published.
|
||||||
|
* @param revisions The number of revisions the post has undergone in version control.
|
||||||
|
* @param createdBy The name of the author who created the post.
|
||||||
|
* @param createdAt The timestamp of when the post was first created.
|
||||||
|
* @param modifiedBy The name of the author who last modified the post.
|
||||||
|
* @param modifiedAt The timestamp of the last modification to the post.
|
||||||
|
* @param custom A map of custom properties or metadata associated with the post.
|
||||||
|
* @param rawContent The raw content of the post, which <em>currently</em> is usually HTML.
|
||||||
|
*/
|
||||||
public record Post(
|
public record Post(
|
||||||
String slug,
|
String slug,
|
||||||
String title,
|
String title,
|
||||||
|
@ -25,11 +42,19 @@ public record Post(
|
||||||
Map<String, String> custom,
|
Map<String, String> custom,
|
||||||
String rawContent
|
String rawContent
|
||||||
) {
|
) {
|
||||||
// this is because we'll be not only supporting html
|
private static final Logger LOGGER = LoggerFactory.getLogger(Post.class);
|
||||||
public String htmlContent() {
|
|
||||||
return rawContent;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a {@link Post} instance by reading and parsing the content of a post file.
|
||||||
|
* <p>
|
||||||
|
* The method extracts metadata properties, content, and versioning information
|
||||||
|
* based on the Git history of the file.
|
||||||
|
*
|
||||||
|
* @param git the Git repository used to retrieve versioning and commit information
|
||||||
|
* @param path the relative path to the file within the "posts" directory
|
||||||
|
* @return a {@link Post} object populated with data extracted from the specified file
|
||||||
|
* @throws IOException if an error occurs during file reading
|
||||||
|
*/
|
||||||
public static Post fromFile(Git git, Path path) throws IOException {
|
public static Post fromFile(Git git, Path path) throws IOException {
|
||||||
/* read properties before filtering */
|
/* read properties before filtering */
|
||||||
|
|
||||||
|
@ -39,8 +64,8 @@ public record Post(
|
||||||
|
|
||||||
var properties = new HashMap<String, String>();
|
var properties = new HashMap<String, String>();
|
||||||
|
|
||||||
String line;
|
while (!lines.isEmpty()) {
|
||||||
while ((line = lines.removeFirst()) != null) {
|
var line = lines.removeFirst();
|
||||||
var parts = line.split(" ");
|
var parts = line.split(" ");
|
||||||
|
|
||||||
var key = parts[0].toLowerCase();
|
var key = parts[0].toLowerCase();
|
||||||
|
@ -50,7 +75,7 @@ public record Post(
|
||||||
break;
|
break;
|
||||||
|
|
||||||
if (properties.putIfAbsent(key, data) != null)
|
if (properties.putIfAbsent(key, data) != null)
|
||||||
System.out.printf("Post %s: Duplicate property \"%s\". Only the first one will be used.\n", slug, key);
|
LOGGER.warn("[Post {}] Ignoring duplicate property: {}", slug, key);
|
||||||
}
|
}
|
||||||
|
|
||||||
var content = String.join("\n", lines).strip();
|
var content = String.join("\n", lines).strip();
|
||||||
|
@ -101,9 +126,18 @@ public record Post(
|
||||||
}
|
}
|
||||||
} catch (GitAPIException e) {
|
} catch (GitAPIException e) {
|
||||||
draft = true;
|
draft = true;
|
||||||
System.out.printf("%s: Git exception, making draft: %s\n", slug, e.getMessage());
|
LOGGER.warn("[Post {}] Draft because of a Git exception: {}\n", slug, e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Post(slug, title, summary, draft, revisions, createdBy, createdAt, modifiedBy, modifiedAt, custom, content);
|
return new Post(slug, title, summary, draft, revisions, createdBy, createdAt, modifiedBy, modifiedAt, custom, content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the raw HTML content associated with the post.
|
||||||
|
*
|
||||||
|
* @return the raw HTML content as a string
|
||||||
|
*/
|
||||||
|
public String htmlContent() {
|
||||||
|
return rawContent;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
50
src/main/java/eu/m724/blog/data/RenderOptions.java
Normal file
50
src/main/java/eu/m724/blog/data/RenderOptions.java
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
package eu.m724.blog.data;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.snakeyaml.engine.v2.api.Load;
|
||||||
|
import org.snakeyaml.engine.v2.api.LoadSettings;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public record RenderOptions(
|
||||||
|
List<String> compress // TODO rename?
|
||||||
|
) {
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(RenderOptions.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a {@link Site} object by reading and parsing the configuration file at the specified path.<br>
|
||||||
|
* The configuration file must be a JSON file.
|
||||||
|
*
|
||||||
|
* @param path the path to the configuration file
|
||||||
|
* @return a {@link Site} object initialized with the data from the configuration file
|
||||||
|
* @throws IOException if an error occurs during file reading
|
||||||
|
*/
|
||||||
|
public static RenderOptions fromConfig(Path path) throws IOException {
|
||||||
|
var load = new Load(LoadSettings.builder().build());
|
||||||
|
var yaml = (Map<String, Object>) load.loadFromInputStream(Files.newInputStream(path));
|
||||||
|
|
||||||
|
List<String> compress = new ArrayList<>();
|
||||||
|
|
||||||
|
for (var key : yaml.keySet()) {
|
||||||
|
var value = yaml.get(key);
|
||||||
|
|
||||||
|
switch (key) {
|
||||||
|
case "compress":
|
||||||
|
compress = (List<String>) value;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
LOGGER.warn("Ignoring unrecognized render option: {}", key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new RenderOptions(
|
||||||
|
compress
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
package eu.m724.blog.data;
|
package eu.m724.blog.data;
|
||||||
|
|
||||||
import org.json.JSONObject;
|
import org.snakeyaml.engine.v2.api.Load;
|
||||||
|
import org.snakeyaml.engine.v2.api.LoadSettings;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
|
@ -8,22 +9,40 @@ import java.nio.file.Path;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@code Site} class represents a website and its configuration.
|
||||||
|
*
|
||||||
|
* @param name the name of the site
|
||||||
|
* @param baseUrl the base URL of the site
|
||||||
|
* @param directory The directory that the site is installed into. Like "https://example.com/blog" turns to "/blog"
|
||||||
|
* @param custom a map of additional custom properties
|
||||||
|
*/
|
||||||
public record Site(
|
public record Site(
|
||||||
String name,
|
String name,
|
||||||
String baseUrl,
|
String baseUrl,
|
||||||
|
|
||||||
|
String directory,
|
||||||
|
|
||||||
Map<String, Object> custom
|
Map<String, Object> custom
|
||||||
) {
|
) {
|
||||||
|
/**
|
||||||
|
* Creates a {@link Site} object by reading and parsing the configuration file at the specified path.<br>
|
||||||
|
* The configuration file must be a JSON file.
|
||||||
|
*
|
||||||
|
* @param path the path to the configuration file
|
||||||
|
* @return a {@link Site} object initialized with the data from the configuration file
|
||||||
|
* @throws IOException if an error occurs during file reading
|
||||||
|
*/
|
||||||
public static Site fromConfig(Path path) throws IOException {
|
public static Site fromConfig(Path path) throws IOException {
|
||||||
var content = Files.readString(path);
|
var load = new Load(LoadSettings.builder().build());
|
||||||
var json = new JSONObject(content);
|
var yaml = (Map<String, Object>) load.loadFromInputStream(Files.newInputStream(path));
|
||||||
|
|
||||||
String name = null;
|
String name = null;
|
||||||
String baseUrl = null;
|
String baseUrl = null;
|
||||||
var custom = new HashMap<String, Object>();
|
var custom = new HashMap<String, Object>();
|
||||||
|
|
||||||
for (var key : json.keySet()) {
|
for (var key : yaml.keySet()) {
|
||||||
var value = json.get(key);
|
var value = yaml.get(key);
|
||||||
|
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case "name":
|
case "name":
|
||||||
|
@ -37,40 +56,14 @@ public record Site(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String directory = null;
|
||||||
|
if (baseUrl != null) {
|
||||||
|
var temp = baseUrl.substring(baseUrl.indexOf(':') + 3);
|
||||||
|
directory = temp.substring(temp.indexOf('/'));
|
||||||
|
}
|
||||||
|
|
||||||
return new Site(
|
return new Site(
|
||||||
name, baseUrl, custom
|
name, baseUrl, directory, custom
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* remained from gson
|
|
||||||
|
|
||||||
private static Map<String, Object> deep(JsonObject jsonObject) {
|
|
||||||
return jsonObject.entrySet().stream()
|
|
||||||
.map(e -> new AbstractMap.SimpleEntry<>(e.getKey(), eToO(e.getValue())))
|
|
||||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Object eToO(JsonElement element) {
|
|
||||||
if (element.isJsonArray()) {
|
|
||||||
return element.getAsJsonArray().asList().stream().map(Site::eToO).toList();
|
|
||||||
} else if (element.isJsonObject()) {
|
|
||||||
return deep(element.getAsJsonObject());
|
|
||||||
} else if (element.isJsonPrimitive()) {
|
|
||||||
try {
|
|
||||||
return element.getAsBoolean();
|
|
||||||
} catch (IllegalStateException e) { }
|
|
||||||
|
|
||||||
try {
|
|
||||||
return element.getAsLong();
|
|
||||||
} catch (NumberFormatException e) { }
|
|
||||||
|
|
||||||
try {
|
|
||||||
return element.getAsDouble();
|
|
||||||
} catch (NumberFormatException e) { }
|
|
||||||
|
|
||||||
// TODO
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}*/
|
|
||||||
}
|
}
|
||||||
|
|
47
src/main/java/eu/m724/blog/template/TemplateExtension.java
Normal file
47
src/main/java/eu/m724/blog/template/TemplateExtension.java
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
package eu.m724.blog.template;
|
||||||
|
|
||||||
|
import eu.m724.blog.data.Site;
|
||||||
|
import io.pebbletemplates.pebble.extension.AbstractExtension;
|
||||||
|
import io.pebbletemplates.pebble.extension.Function;
|
||||||
|
import io.pebbletemplates.pebble.template.EvaluationContext;
|
||||||
|
import io.pebbletemplates.pebble.template.PebbleTemplate;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class TemplateExtension extends AbstractExtension {
|
||||||
|
private final Site site;
|
||||||
|
|
||||||
|
public TemplateExtension(Site site) {
|
||||||
|
this.site = site;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Function> getFunctions() {
|
||||||
|
return Map.of(
|
||||||
|
"static", new Function() {
|
||||||
|
@Override
|
||||||
|
public List<String> getArgumentNames() {
|
||||||
|
return List.of("path");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object execute(Map<String, Object> args, PebbleTemplate self, EvaluationContext context, int lineNumber) {
|
||||||
|
return site.directory() + "/static/" + args.get("path");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"asset", new Function() {
|
||||||
|
@Override
|
||||||
|
public List<String> getArgumentNames() {
|
||||||
|
return List.of("path");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object execute(Map<String, Object> args, PebbleTemplate self, EvaluationContext context, int lineNumber) {
|
||||||
|
return site.directory() + "/assets/" + args.get("path");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO make url_for that supports relative and absolute paths
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
81
src/main/java/eu/m724/blog/template/TemplateRenderer.java
Normal file
81
src/main/java/eu/m724/blog/template/TemplateRenderer.java
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
package eu.m724.blog.template;
|
||||||
|
|
||||||
|
import eu.m724.blog.data.Post;
|
||||||
|
import eu.m724.blog.data.Site;
|
||||||
|
import io.pebbletemplates.pebble.PebbleEngine;
|
||||||
|
import io.pebbletemplates.pebble.loader.FileLoader;
|
||||||
|
import io.pebbletemplates.pebble.template.PebbleTemplate;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.StringWriter;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@code TemplateRenderer} class is responsible for rendering dynamic HTML templates
|
||||||
|
* using the Pebble templating engine.
|
||||||
|
*/
|
||||||
|
public class TemplateRenderer {
|
||||||
|
private final Site site;
|
||||||
|
private final PebbleTemplate indexTemplate, articleTemplate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a TemplateRenderer instance for rendering templates from the specified directory.
|
||||||
|
|
||||||
|
* @param site the {@link Site} this renderer renders
|
||||||
|
* @param templateDirectory the root directory containing the template files
|
||||||
|
*/
|
||||||
|
public TemplateRenderer(Site site, Path templateDirectory) {
|
||||||
|
var loader = new FileLoader();
|
||||||
|
loader.setPrefix(templateDirectory.toString());
|
||||||
|
loader.setSuffix(".html");
|
||||||
|
|
||||||
|
var pebbleEngine = new PebbleEngine.Builder()
|
||||||
|
.loader(loader)
|
||||||
|
.extension(new TemplateExtension(site))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
this.site = site;
|
||||||
|
this.indexTemplate = pebbleEngine.getTemplate("index_template");
|
||||||
|
this.articleTemplate = pebbleEngine.getTemplate("article_template");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the index page using this template.
|
||||||
|
*
|
||||||
|
* @param posts the {@link Post}s to be included in the index page
|
||||||
|
* @return the rendered index HTML page as a string
|
||||||
|
* @throws IOException if an error occurs during the template evaluation
|
||||||
|
*/
|
||||||
|
public String renderIndex(List<Post> posts) throws IOException {
|
||||||
|
Map<String, Object> context = Map.of(
|
||||||
|
"site", site,
|
||||||
|
"articles", posts
|
||||||
|
);
|
||||||
|
|
||||||
|
var writer = new StringWriter();
|
||||||
|
indexTemplate.evaluate(writer, context);
|
||||||
|
|
||||||
|
return writer.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the content of a post using this template.
|
||||||
|
*
|
||||||
|
* @param post the {@link Post} to be rendered
|
||||||
|
* @return the rendered post HTML page as a string
|
||||||
|
* @throws IOException if an error occurs during template evaluation
|
||||||
|
*/
|
||||||
|
public String renderPost(Post post) throws IOException {
|
||||||
|
Map<String, Object> context = Map.of(
|
||||||
|
"site", site,
|
||||||
|
"article", post
|
||||||
|
);
|
||||||
|
|
||||||
|
var writer = new StringWriter();
|
||||||
|
articleTemplate.evaluate(writer, context);
|
||||||
|
|
||||||
|
return writer.toString();
|
||||||
|
}
|
||||||
|
}
|
11
src/main/resources/simplelogger.properties
Normal file
11
src/main/resources/simplelogger.properties
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
org.slf4j.simpleLogger.defaultLogLevel=info
|
||||||
|
|
||||||
|
# TODO maybe?
|
||||||
|
#org.slf4j.simpleLogger.showDateTime=true
|
||||||
|
#org.slf4j.simpleLogger.dateTimeFormat=HH:mm:ss.SSS
|
||||||
|
|
||||||
|
org.slf4j.simpleLogger.showThreadName=false
|
||||||
|
org.slf4j.simpleLogger.showShortLogName=true
|
||||||
|
org.slf4j.simpleLogger.levelInBrackets=true
|
||||||
|
|
||||||
|
org.slf4j.simpleLogger.logFile=System.out
|
Loading…
Add table
Reference in a new issue