Signed-off-by: Minecon724 <git@m724.eu>
This commit is contained in:
parent
053ce27f3c
commit
5d98a8fd60
17 changed files with 188 additions and 106 deletions
15
README.md
15
README.md
|
@ -2,10 +2,11 @@ blog-software(config, template, content) = blog website
|
|||
|
||||
## Usage
|
||||
|
||||
1. [Download the program from here](/Minecon724/blog-software-java/releases)
|
||||
1. [Download the program from here](/Minecon724/blog-software-java/releases) \
|
||||
**Mini** or **Full**? **Full** contains dependencies, which use native libraries. Choose **Full**, if unsure.
|
||||
2. Run the program:
|
||||
```shell
|
||||
java -jar blog-0.0.1-shaded.jar -s example_workdir
|
||||
java -jar blog-0.0.2-standalone-full.jar -s example_workdir
|
||||
```
|
||||
|
||||
For tips on how to create your own project (workdir), see [Project format](#Project format) below.
|
||||
|
@ -25,11 +26,11 @@ There's an ["Example workdir"](/Minecon724/blog-software-java/src/branch/master/
|
|||
|
||||
Basically:
|
||||
- `assets/` - contains static assets
|
||||
- `posts/` - contains posts. Post format:
|
||||
- `articles/` - contains articles. 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
|
||||
- `title A title` - article title
|
||||
- `summary This is a article with a title` - article summary
|
||||
- `live` - is the article 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.
|
||||
|
@ -43,5 +44,5 @@ Basically:
|
|||
https://pebbletemplates.io is used
|
||||
|
||||
- `static/` - contains static assets
|
||||
- `article_template.html` - post template
|
||||
- `article_template.html` - article template
|
||||
- `index_template.html` - index.html template
|
|
@ -1,6 +1,9 @@
|
|||
name: my blog
|
||||
baseUrl: https://example.com/blog
|
||||
|
||||
# Whether to apply Pebble templating to posts. Disabled by default, not recommended.
|
||||
# templateArticles: true
|
||||
|
||||
coolProperty: 1231
|
||||
coolerProperty:
|
||||
isMap: true
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
</header>
|
||||
|
||||
<div>
|
||||
{{ article.htmlContent | raw }}
|
||||
{{ content | raw }}
|
||||
</div>
|
||||
</article>
|
||||
</body>
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
<body>
|
||||
<h1>{{ site.name }} - {{ site.custom.coolProperty }}</h1>
|
||||
{% for article in articles %}
|
||||
<a href="post/{{ article.slug }}.html" class="post-short">
|
||||
<a href="article/{{ article.slug }}.html" class="article-short">
|
||||
<article>
|
||||
<header>
|
||||
<p class="title">{{ article.title }}</p>
|
||||
|
|
32
pom.xml
32
pom.xml
|
@ -72,6 +72,7 @@
|
|||
<groupId>com.github.luben</groupId>
|
||||
<artifactId>zstd-jni</artifactId>
|
||||
<version>1.5.7-1</version> <!-- Released Feb 20, 2025 -->
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
|
||||
|
@ -113,6 +114,7 @@
|
|||
<version>3.6.0</version> <!-- Released May 31, 2024 -->
|
||||
<executions>
|
||||
<execution>
|
||||
<id>shade-mini</id>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
|
@ -121,6 +123,36 @@
|
|||
<minimizeJar>true</minimizeJar>
|
||||
<createDependencyReducedPom>false</createDependencyReducedPom>
|
||||
<shadedArtifactAttached>true</shadedArtifactAttached>
|
||||
<shadedClassifierName>standalone-mini</shadedClassifierName>
|
||||
<artifactSet>
|
||||
<excludes>
|
||||
<exclude>com.github.luben:zstd-jni</exclude>
|
||||
</excludes>
|
||||
</artifactSet>
|
||||
<filters>
|
||||
<filter>
|
||||
<artifact>*:*</artifact>
|
||||
<excludes>
|
||||
<exclude>META-INF/*.SF</exclude>
|
||||
<exclude>META-INF/*.DSA</exclude>
|
||||
<exclude>META-INF/*.RSA</exclude>
|
||||
</excludes>
|
||||
</filter>
|
||||
</filters>
|
||||
</configuration>
|
||||
</execution>
|
||||
|
||||
<execution>
|
||||
<id>shade-full</id>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<minimizeJar>true</minimizeJar>
|
||||
<createDependencyReducedPom>false</createDependencyReducedPom>
|
||||
<shadedArtifactAttached>true</shadedArtifactAttached>
|
||||
<shadedClassifierName>standalone-full</shadedClassifierName>
|
||||
<filters>
|
||||
<filter>
|
||||
<artifact>*:*</artifact>
|
||||
|
|
|
@ -7,8 +7,9 @@
|
|||
package eu.m724.blog;
|
||||
|
||||
import eu.m724.blog.compress.FileCompressor;
|
||||
import eu.m724.blog.compress.NoSuchAlgorithmException;
|
||||
import eu.m724.blog.object.Article;
|
||||
import eu.m724.blog.object.Feed;
|
||||
import eu.m724.blog.object.Post;
|
||||
import eu.m724.blog.object.RenderOptions;
|
||||
import eu.m724.blog.object.Site;
|
||||
import eu.m724.blog.template.TemplateRenderer;
|
||||
|
@ -28,7 +29,7 @@ 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
|
||||
* assets, articles, and rendering output files. It uses a Git repository as the
|
||||
* source for the blog's content and configuration.
|
||||
*/
|
||||
public class BlogBuilder {
|
||||
|
@ -141,14 +142,14 @@ public class BlogBuilder {
|
|||
if (template == null)
|
||||
this.template = new TemplateRenderer(site, templateDirectory, fileHashes);
|
||||
|
||||
LOGGER.debug("Rendering posts...");
|
||||
var posts = renderPosts();
|
||||
LOGGER.debug("Rendering articles...");
|
||||
var articles = renderArticles();
|
||||
|
||||
LOGGER.debug("Rendering meta...");
|
||||
posts.sort(Comparator.comparing(Post::createdAt).reversed());
|
||||
Files.writeString(outputDirectory.resolve("index.html"), template.renderIndex(posts));
|
||||
articles.sort(Comparator.comparing(Article::createdAt).reversed());
|
||||
Files.writeString(outputDirectory.resolve("index.html"), template.renderIndex(articles));
|
||||
|
||||
Files.writeString(outputDirectory.resolve("posts.rss"), Feed.generateRss(site, posts));
|
||||
Files.writeString(outputDirectory.resolve("articles.rss"), Feed.generateRss(site, articles));
|
||||
|
||||
if (!renderOptions.compress().isEmpty()) {
|
||||
LOGGER.debug("Compressing...");
|
||||
|
@ -166,43 +167,44 @@ public class BlogBuilder {
|
|||
}
|
||||
}
|
||||
|
||||
private List<Post> renderPosts() throws IOException {
|
||||
Files.createDirectory(outputDirectory.resolve("post"));
|
||||
var postDirectory = workingDirectory.resolve("posts");
|
||||
private List<Article> renderArticles() throws IOException {
|
||||
Files.createDirectory(outputDirectory.resolve("article"));
|
||||
var articleDirectory = workingDirectory.resolve("articles");
|
||||
|
||||
var posts = new ArrayList<Post>();
|
||||
var articles = new ArrayList<Article>();
|
||||
|
||||
try (var stream = Files.walk(postDirectory)) {
|
||||
try (var stream = Files.walk(articleDirectory)) {
|
||||
for (var path : stream.collect(Collectors.toSet())) {
|
||||
if (!Files.isRegularFile(path))
|
||||
continue; // directory is created below
|
||||
|
||||
if (!path.toString().endsWith(".html")) {
|
||||
LOGGER.warn("Post {}: unsupported file type", path.getFileName());
|
||||
// TODO print file type too
|
||||
LOGGER.warn("[Article {}] Unsupported file type", path.getFileName());
|
||||
continue;
|
||||
}
|
||||
|
||||
path = postDirectory.relativize(path);
|
||||
var post = Post.fromFile(git, path);
|
||||
path = articleDirectory.relativize(path);
|
||||
var article = Article.fromFile(git, path);
|
||||
|
||||
if (post.draft() && !renderDrafts) {
|
||||
LOGGER.info("Post {}: draft, ignoring", path.getFileName());
|
||||
if (article.draft() && !renderDrafts) {
|
||||
LOGGER.info("[Article {}] Draft. Ignoring, because you didn't specify to render drafts.", path.getFileName());
|
||||
continue;
|
||||
}
|
||||
|
||||
var render = template.renderPost(post);
|
||||
var outFile = outputDirectory.resolve("post").resolve(path);
|
||||
var render = template.renderArticle(article);
|
||||
var outFile = outputDirectory.resolve("article").resolve(path);
|
||||
|
||||
try {
|
||||
Files.createDirectory(outFile.getParent());
|
||||
} catch (FileAlreadyExistsException ignored) { }
|
||||
|
||||
Files.writeString(outFile, render);
|
||||
posts.add(post);
|
||||
articles.add(article);
|
||||
}
|
||||
}
|
||||
|
||||
return posts;
|
||||
return articles;
|
||||
}
|
||||
|
||||
private Map<String, String> copyStaticAssets() throws IOException {
|
||||
|
@ -232,9 +234,17 @@ public class BlogBuilder {
|
|||
}
|
||||
|
||||
private void compressOutput() throws IOException {
|
||||
var compressors = renderOptions.compress().stream()
|
||||
.map(FileCompressor::new)
|
||||
.toList();
|
||||
var compressors = new ArrayList<FileCompressor>();
|
||||
|
||||
for (var algorithm : renderOptions.compress()) {
|
||||
try {
|
||||
var compressor = new FileCompressor(algorithm);
|
||||
compressors.add(compressor);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
LOGGER.error("No such compression algorithm, ignoring: {}", e.getAlgorithm());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Set<Path> tree;
|
||||
try (var walk = Files.walk(outputDirectory)) {
|
||||
|
@ -246,7 +256,7 @@ public class BlogBuilder {
|
|||
try {
|
||||
compressor.compress(path);
|
||||
} catch (CompressorException e) {
|
||||
LOGGER.error("Error compressing {} to {}: {}", path, compressor.getAlgorithm(), e.getMessage());
|
||||
LOGGER.error("Error compressing \"{}\" to \"{}\": {}", path, compressor.getAlgorithm(), e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -51,7 +51,9 @@ public class Main {
|
|||
var end = System.nanoTime();
|
||||
LOGGER.info("Exported to {} in {} ms", outputDirectory.toAbsolutePath(), "%.2f".formatted((end - start) / 1000000.0));
|
||||
|
||||
builder.startServer(openBrowser);
|
||||
if (startServer) {
|
||||
builder.startServer(openBrowser);
|
||||
}
|
||||
}
|
||||
|
||||
private static CommandLine getCommandLine(String[] args) {
|
||||
|
|
|
@ -58,7 +58,6 @@ public class Server {
|
|||
* @throws IOException if an I/O error occurs during server initialization
|
||||
*/
|
||||
public void start() throws IOException {
|
||||
System.out.println(contextPath);
|
||||
var server = HttpServer.create(listenAddress, 0);
|
||||
server.createContext(contextPath, SimpleFileServer.createFileHandler(sitePath.toAbsolutePath()));
|
||||
server.start();
|
||||
|
|
|
@ -10,13 +10,29 @@ import org.apache.commons.compress.compressors.CompressorException;
|
|||
import org.apache.commons.compress.compressors.CompressorStreamFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.file.*;
|
||||
|
||||
public class FileCompressor {
|
||||
private final String algorithm;
|
||||
|
||||
public FileCompressor(String algorithm) {
|
||||
/**
|
||||
* Constructs a {@link FileCompressor} instance using the provided algorithm.
|
||||
*
|
||||
* @param algorithm The algorithm, for valid names see {@link CompressorStreamFactory}
|
||||
* @throws NoSuchAlgorithmException If that algorithm is unavailable or the name is invalid.
|
||||
*/
|
||||
public FileCompressor(String algorithm) throws NoSuchAlgorithmException {
|
||||
this.algorithm = algorithm;
|
||||
|
||||
try {
|
||||
var os = new CompressorStreamFactory().createCompressorOutputStream(algorithm, OutputStream.nullOutputStream());
|
||||
os.close();
|
||||
} catch (NoClassDefFoundError | CompressorException e) {
|
||||
throw new NoSuchAlgorithmException(algorithm, e);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Unexpected IOException closing test output stream", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void compress(Path source) throws IOException, CompressorException {
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright (c) 2025 blog-software-java developers
|
||||
* blog-software-java is licensed under the GNU General Public License. See the LICENSE.md file
|
||||
* in the project root for the full license text.
|
||||
*/
|
||||
|
||||
package eu.m724.blog.compress;
|
||||
|
||||
public class NoSuchAlgorithmException extends Exception {
|
||||
private final String algorithm;
|
||||
|
||||
public NoSuchAlgorithmException(String algorithm, Throwable cause) {
|
||||
super("Algorithm unavailable:" + algorithm, cause);
|
||||
|
||||
this.algorithm = algorithm;
|
||||
}
|
||||
|
||||
public String getAlgorithm() {
|
||||
return algorithm;
|
||||
}
|
||||
}
|
|
@ -19,7 +19,7 @@ import java.util.HashMap;
|
|||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* The {@code Post} class represents a blog post with various attributes including metadata and content.
|
||||
* The {@code Article} 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.
|
||||
|
@ -33,7 +33,7 @@ import java.util.Map;
|
|||
* @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 Article(
|
||||
String slug,
|
||||
String title,
|
||||
String summary,
|
||||
|
@ -48,24 +48,24 @@ public record Post(
|
|||
Map<String, String> custom,
|
||||
String rawContent
|
||||
) {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(Post.class);
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(Article.class);
|
||||
|
||||
/**
|
||||
* Creates a {@link Post} instance by reading and parsing the content of a post file.
|
||||
* Creates a {@link Article} instance by reading and parsing the content of an article 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
|
||||
* @param path the relative path to the file within the "articles" directory
|
||||
* @return a {@link Article} 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 Article fromFile(Git git, Path path) throws IOException {
|
||||
/* read properties before filtering */
|
||||
|
||||
var slug = path.getFileName().toString().split("\\.")[0];
|
||||
path = Path.of("posts").resolve(path);
|
||||
path = Path.of("articles").resolve(path);
|
||||
var lines = Files.readAllLines(git.getRepository().getDirectory().toPath().getParent().resolve(path));
|
||||
|
||||
var properties = new HashMap<String, String>();
|
||||
|
@ -81,7 +81,7 @@ public record Post(
|
|||
break;
|
||||
|
||||
if (properties.putIfAbsent(key, data) != null)
|
||||
LOGGER.warn("[Post {}] Ignoring duplicate property: {}", slug, key);
|
||||
LOGGER.warn("[Article {}] Ignoring duplicate property: {}", slug, key);
|
||||
}
|
||||
|
||||
var content = String.join("\n", lines).strip();
|
||||
|
@ -132,18 +132,9 @@ public record Post(
|
|||
}
|
||||
} catch (GitAPIException e) {
|
||||
draft = true;
|
||||
LOGGER.warn("[Post {}] Draft because of a Git exception: {}", slug, e.getMessage());
|
||||
LOGGER.warn("[Article {}] Draft because of a Git exception: {}", slug, e.getMessage());
|
||||
}
|
||||
|
||||
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;
|
||||
return new Article(slug, title, summary, draft, revisions, createdBy, createdAt, modifiedBy, modifiedAt, custom, content);
|
||||
}
|
||||
}
|
|
@ -16,21 +16,21 @@ public class Feed {
|
|||
* 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
|
||||
* @param articles the list of {@link Article} 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<Article> articles) {
|
||||
var content = new StringBuilder("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>");
|
||||
content.append("<rss version=\"2.0\"><channel>");
|
||||
content.append("<title>%s</title>".formatted(site.name()));
|
||||
content.append("<link>%s</link>".formatted(site.baseUrl()));
|
||||
|
||||
for (var post : posts) {
|
||||
for (var article : articles) {
|
||||
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("<title>%s</title>".formatted(article.title()));
|
||||
content.append("<link>%s/article/%s.html</link>".formatted(site.baseUrl(), article.slug()));
|
||||
content.append("<description>%s</description>".formatted(article.summary()));
|
||||
content.append("<pubDate>%s</pubDate>".formatted(article.createdAt().format(formatter)));
|
||||
content.append("</item>");
|
||||
}
|
||||
|
||||
|
|
|
@ -12,7 +12,6 @@ import org.snakeyaml.engine.v2.api.LoadSettings;
|
|||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
|
@ -21,6 +20,7 @@ import java.util.Map;
|
|||
* @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 templateArticles whether to parse posts with Pebble templating
|
||||
* @param custom a map of additional custom properties
|
||||
*/
|
||||
public record Site(
|
||||
|
@ -29,6 +29,8 @@ public record Site(
|
|||
|
||||
String directory,
|
||||
|
||||
boolean templateArticles,
|
||||
|
||||
Map<String, Object> custom
|
||||
) {
|
||||
/**
|
||||
|
@ -43,33 +45,21 @@ public record Site(
|
|||
var load = new Load(LoadSettings.builder().build());
|
||||
var yaml = (Map<String, Object>) load.loadFromInputStream(Files.newInputStream(path));
|
||||
|
||||
String name = null;
|
||||
String baseUrl = null;
|
||||
var custom = new HashMap<String, Object>();
|
||||
String name = (String) yaml.get("name");
|
||||
String baseUrl = (String) yaml.getOrDefault("baseUrl", "/");
|
||||
var templateArticles = (boolean) yaml.getOrDefault("templateArticles", false);
|
||||
|
||||
for (var key : yaml.keySet()) {
|
||||
var value = yaml.get(key);
|
||||
|
||||
switch (key) {
|
||||
case "name":
|
||||
name = (String) value;
|
||||
break;
|
||||
case "baseUrl":
|
||||
baseUrl = (String) value;
|
||||
break;
|
||||
default:
|
||||
custom.put(key, value);
|
||||
String directory = "/";
|
||||
if (baseUrl != null) {
|
||||
var temp = baseUrl.substring(baseUrl.indexOf(':') + 3);
|
||||
var slashIndex = temp.indexOf('/');
|
||||
if (slashIndex != -1) {
|
||||
directory += temp.substring(slashIndex + 1) + "/";
|
||||
}
|
||||
}
|
||||
|
||||
String directory = null;
|
||||
if (baseUrl != null) {
|
||||
var temp = baseUrl.substring(baseUrl.indexOf(':') + 3);
|
||||
directory = temp.substring(temp.indexOf('/'));
|
||||
}
|
||||
|
||||
return new Site(
|
||||
name, baseUrl, directory, custom
|
||||
name, baseUrl, directory, templateArticles, yaml
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,7 +43,7 @@ public class TemplateExtension extends AbstractExtension {
|
|||
path = CacheBuster.insertHashInPath(path, hash);
|
||||
}
|
||||
|
||||
return site.directory() + "/" + path;
|
||||
return site.directory() + path;
|
||||
}
|
||||
},
|
||||
"asset", new Function() {
|
||||
|
@ -61,10 +61,15 @@ public class TemplateExtension extends AbstractExtension {
|
|||
path = CacheBuster.insertHashInPath(path, hash);
|
||||
}
|
||||
|
||||
return site.directory() + "/" + path;
|
||||
return site.directory() + path;
|
||||
}
|
||||
}
|
||||
// TODO make url_for that supports relative and absolute paths
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getGlobalVariables() {
|
||||
return Map.of("site", site);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
package eu.m724.blog.template;
|
||||
|
||||
import eu.m724.blog.object.Post;
|
||||
import eu.m724.blog.object.Article;
|
||||
import eu.m724.blog.object.Site;
|
||||
import io.pebbletemplates.pebble.PebbleEngine;
|
||||
import io.pebbletemplates.pebble.loader.FileLoader;
|
||||
|
@ -24,6 +24,8 @@ import java.util.Map;
|
|||
*/
|
||||
public class TemplateRenderer {
|
||||
private final Site site;
|
||||
|
||||
private final PebbleEngine pebbleEngine;
|
||||
private final PebbleTemplate indexTemplate, articleTemplate;
|
||||
|
||||
/**
|
||||
|
@ -38,7 +40,7 @@ public class TemplateRenderer {
|
|||
loader.setPrefix(templateDirectory.toString());
|
||||
loader.setSuffix(".html");
|
||||
|
||||
var pebbleEngine = new PebbleEngine.Builder()
|
||||
this.pebbleEngine = new PebbleEngine.Builder()
|
||||
.loader(loader)
|
||||
.extension(new TemplateExtension(site, fileHashes))
|
||||
.build();
|
||||
|
@ -51,37 +53,47 @@ public class TemplateRenderer {
|
|||
/**
|
||||
* Renders the index page using this template.
|
||||
*
|
||||
* @param posts the {@link Post}s to be included in the index page
|
||||
* @param articles the {@link Article}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
|
||||
public String renderIndex(List<Article> articles) throws IOException {
|
||||
var context = Map.of(
|
||||
"articles", articles
|
||||
);
|
||||
|
||||
var writer = new StringWriter();
|
||||
indexTemplate.evaluate(writer, context);
|
||||
|
||||
return writer.toString();
|
||||
return renderTemplate(indexTemplate, context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the content of a post using this template.
|
||||
*
|
||||
* @param post the {@link Post} to be rendered
|
||||
* @param article the {@link Article} 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
|
||||
public String renderArticle(Article article) throws IOException {
|
||||
String content = article.rawContent();
|
||||
|
||||
if (site.templateArticles()) {
|
||||
var context = Map.of(
|
||||
"article", article
|
||||
);
|
||||
|
||||
content = renderTemplate(pebbleEngine.getLiteralTemplate(content), context);
|
||||
}
|
||||
|
||||
var context = Map.of(
|
||||
"article", article,
|
||||
"content", content
|
||||
);
|
||||
|
||||
return renderTemplate(articleTemplate, context);
|
||||
}
|
||||
|
||||
private String renderTemplate(PebbleTemplate template, Map<String, ?> context) throws IOException {
|
||||
var writer = new StringWriter();
|
||||
articleTemplate.evaluate(writer, context);
|
||||
template.evaluate(writer, (Map<String, Object>) context);
|
||||
|
||||
return writer.toString();
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue