Minifier
Some checks are pending
/ build (push) Waiting to run

again

Signed-off-by: Minecon724 <git@m724.eu>
This commit is contained in:
Minecon724 2025-03-08 13:26:36 +01:00
parent 889d1b3287
commit d6c33b4992
Signed by untrusted user who does not match committer: Minecon724
GPG key ID: 3CCC4D267742C8E8
7 changed files with 197 additions and 52 deletions

View file

@ -88,6 +88,13 @@
<artifactId>slf4j-simple</artifactId>
<version>2.0.17</version> <!-- Released Feb 25, 2025 -->
</dependency>
<!-- https://mvnrepository.com/artifact/in.wilsonl.minifyhtml/minify-html -->
<dependency>
<groupId>in.wilsonl.minifyhtml</groupId>
<artifactId>minify-html</artifactId>
<version>0.15.0</version> <!-- Released Dec 24, 2023 -->
</dependency>
</dependencies>
<build>
@ -127,6 +134,7 @@
<artifactSet>
<excludes>
<exclude>com.github.luben:zstd-jni</exclude>
<exclude>in.wilsonl.minifyhtml:minify-html</exclude>
</excludes>
</artifactSet>
<filters>

View file

@ -41,6 +41,7 @@ public class BlogBuilder {
private Site site;
private TemplateRenderer template;
private RenderOptions renderOptions;
private Minifier minifier;
private Path templateDirectory;
private Path outputDirectory;
@ -136,11 +137,19 @@ public class BlogBuilder {
if (renderOptions == null)
this.renderOptions = RenderOptions.fromConfig(workingDirectory.resolve("render.yml"));
if (this.site.minify()) {
try {
this.minifier = new Minifier();
} catch (NoClassDefFoundError e) {
LOGGER.warn("Minifier not available");
}
}
LOGGER.debug("Copying static assets...");
var fileHashes = copyStaticAssets();
if (template == null)
this.template = new TemplateRenderer(site, templateDirectory, fileHashes);
this.template = new TemplateRenderer(site, minifier, templateDirectory, fileHashes);
LOGGER.debug("Rendering articles...");
var articles = renderArticles();
@ -210,24 +219,36 @@ public class BlogBuilder {
private Map<String, String> copyStaticAssets() throws IOException {
var fileHashes = new HashMap<String, String>();
var userAssetsDir = workingDirectory.resolve("assets");
var templateStaticDir = templateDirectory.resolve("static");
var outputUserAssetsDir = outputDirectory.resolve("assets");
var outputTemplateStaticDir = outputDirectory.resolve("static");
if (renderOptions.remapAssets()) {
var assetHashes = CacheBuster.copyTree(workingDirectory.resolve("assets"), outputDirectory.resolve("assets"));
var assetHashes = CacheBuster.copyTree(userAssetsDir, outputUserAssetsDir);
assetHashes.forEach((k, v) -> {
fileHashes.put("assets/" + k, v); // TODO this seems like a hack
fileHashes.put("assets/" + k, v); // TODO this feels like a hack
});
} else {
copyTree(workingDirectory.resolve("assets"), outputDirectory.resolve("assets"));
FileUtils.copyTree(userAssetsDir, outputUserAssetsDir);
}
if (renderOptions.remapTemplateStatic()) {
var templateStaticHashes = CacheBuster.copyTree(templateDirectory.resolve("static"), outputDirectory.resolve("static"));
var templateStaticHashes = CacheBuster.copyTree(templateStaticDir, outputTemplateStaticDir);
templateStaticHashes.forEach((k, v) -> {
fileHashes.put("static/" + k, v); // TODO this seems like a hack
fileHashes.put("static/" + k, v); // TODO this feels like a hack
});
} else {
copyTree(templateDirectory.resolve("static"), outputDirectory.resolve("static"));
FileUtils.copyTree(templateStaticDir, outputTemplateStaticDir);
}
if (minifier != null) {
minifier.minifyTree(outputTemplateStaticDir);
}
return fileHashes;
@ -261,26 +282,4 @@ public class BlogBuilder {
}
}
}
/* Internal functions */
private void copyTree(Path srcDir, Path destDir) throws IOException {
try (var walk = Files.walk(srcDir)) {
for (var src : walk.collect(Collectors.toSet())) {
var rel = srcDir.relativize(src);
var dest = destDir.resolve(rel);
if (Files.isRegularFile(src)) {
var parent = dest.getParent();
if (!Files.isDirectory(parent)) {
Files.createDirectories(parent);
}
Files.copy(src, dest);
}
}
}
}
}

View file

@ -11,7 +11,6 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.zip.CRC32C;
public class CacheBuster {
@ -19,27 +18,17 @@ public class CacheBuster {
public static Map<String, String> copyTree(Path srcDir, Path destDir) throws IOException {
var map = new HashMap<String, String>();
try (var walk = Files.walk(srcDir)) {
for (var src : walk.collect(Collectors.toSet())) {
var rel = srcDir.relativize(src);
var dest = destDir.resolve(rel);
for (var e : FileUtils.srcDestMap(srcDir, destDir).entrySet()) {
var sourceFile = e.getKey();
var destinationFile = e.getValue();
if (Files.isRegularFile(src)) {
var parent = dest.getParent();
var hash = hashFile(sourceFile);
if (!Files.isDirectory(parent)) {
Files.createDirectories(parent);
}
var filename = insertHashInPath(destinationFile.getFileName().toString(), hash);
destinationFile = destinationFile.resolveSibling(filename);
var hash = hashFile(src);
var filename = insertHashInPath(dest.getFileName().toString(), hash);
dest = dest.resolveSibling(filename);
Files.copy(src, dest);
map.put(rel.toString(), hash);
}
}
Files.copy(sourceFile, destinationFile);
map.put(srcDir.relativize(sourceFile).toString(), hash);
}
return map;

View file

@ -0,0 +1,71 @@
/*
* 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;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
public class FileUtils {
public static void copyTree(Path srcDir, Path destDir) throws IOException {
for (var e : srcDestMap(srcDir, destDir).entrySet()) {
Files.copy(e.getKey(), e.getValue());
}
}
public static Map<Path, Path> srcDestMap(Path srcDir, Path destDir) throws IOException {
var map = new HashMap<Path, Path>();
for (var sourceFile : walkFilesList(srcDir)) {
var rel = srcDir.relativize(sourceFile);
var destinationFile = destDir.resolve(rel);
var parent = destinationFile.getParent();
if (!Files.isDirectory(parent)) {
Files.createDirectories(parent);
}
map.put(sourceFile, destinationFile);
}
return map;
}
/**
* Walks over a directory and only returns files.<br>
* <em>IMPORTANT</em> Don't forget to close the stream.<br>
*
* @param start the starting file
* @return the {@link Stream} of {@link Path}
* @throws IOException if an I/O error is thrown when accessing the starting file.
*
* @see FileUtils#walkFilesList(Path)
*/
public static Stream<Path> walkFilesStream(Path start) throws IOException {
return Files.walk(start).filter(Files::isRegularFile);
}
/**
* Walks over a directory and only returns files.<br>
*
* @param start the starting file
* @return the {@link List} of {@link Path}
* @throws IOException if an I/O error is thrown when accessing the starting file.
*
* @see FileUtils#walkFilesStream(Path)
*/
public static List<Path> walkFilesList(Path start) throws IOException {
try (var stream = walkFilesStream(start)) {
return stream.toList();
}
}
}

View file

@ -0,0 +1,65 @@
/*
* 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;
import in.wilsonl.minifyhtml.Configuration;
import in.wilsonl.minifyhtml.MinifyHtml;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Set;
public class Minifier {
private static final Set<String> CONTENT_TYPES_TO_MINIFY = Set.of(
"text/html",
"text/css",
"text/javascript"
);
private final Configuration configuration = new Configuration.Builder()
.setMinifyJs(true)
.setMinifyCss(true)
.setKeepHtmlAndHeadOpeningTags(true)
.build();
public String minify(String text) {
return MinifyHtml.minify(text, configuration);
}
public void minifyTree(Path dir) throws IOException {
for (var file : FileUtils.walkFilesList(dir)) {
var contentType = Files.probeContentType(file);
var doMinify = CONTENT_TYPES_TO_MINIFY.contains(contentType);
if (doMinify) {
var content = Files.readString(file);
var minified = minify(content);
Files.writeString(file, minified);
}
}
}
public void copyAndMinifyTree(Path srcDir, Path destDir) throws IOException {
for (var e : FileUtils.srcDestMap(srcDir, destDir).entrySet()) {
var sourceFile = e.getKey();
var destinationFile = e.getValue();
var contentType = Files.probeContentType(sourceFile);
var doMinify = CONTENT_TYPES_TO_MINIFY.contains(contentType);
if (doMinify) {
var content = Files.readString(sourceFile);
var minified = minify(content);
Files.writeString(destinationFile, minified);
} else {
Files.copy(sourceFile, destDir);
}
}
}
}

View file

@ -21,6 +21,7 @@ import java.util.Map;
* @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 articles with Pebble templating
* @param minify whether to minify HTML, CSS, JS, etc.
* @param custom a map of additional custom properties
*/
public record Site(
@ -30,6 +31,7 @@ public record Site(
String directory,
boolean templateArticles,
boolean minify,
Map<String, Object> custom
) {
@ -48,6 +50,7 @@ public record Site(
String name = (String) yaml.get("name");
String baseUrl = (String) yaml.getOrDefault("baseUrl", "/");
var templateArticles = (boolean) yaml.getOrDefault("templateArticles", false);
var minify = (boolean) yaml.getOrDefault("minify", true);
String directory = "/";
if (baseUrl != null) {
@ -59,7 +62,7 @@ public record Site(
}
return new Site(
name, baseUrl, directory, templateArticles, yaml
name, baseUrl, directory, templateArticles, minify, yaml
);
}
}

View file

@ -6,6 +6,7 @@
package eu.m724.blog.template;
import eu.m724.blog.Minifier;
import eu.m724.blog.object.Article;
import eu.m724.blog.object.Site;
import io.pebbletemplates.pebble.PebbleEngine;
@ -24,6 +25,7 @@ import java.util.Map;
*/
public class TemplateRenderer {
private final Site site;
private final Minifier minifier;
private final PebbleEngine pebbleEngine;
private final PebbleTemplate indexTemplate, articleTemplate;
@ -35,7 +37,11 @@ public class TemplateRenderer {
* @param templateDirectory the root directory containing the template file
* @param fileHashes file hashes. currently only applies to assets
*/
public TemplateRenderer(Site site, Path templateDirectory, Map<String, String> fileHashes) {
public TemplateRenderer(Site site, Minifier minifier, Path templateDirectory, Map<String, String> fileHashes) {
this.site = site;
this.minifier = minifier;
var loader = new FileLoader();
loader.setPrefix(templateDirectory.toString());
loader.setSuffix(".html");
@ -45,7 +51,6 @@ public class TemplateRenderer {
.extension(new TemplateExtension(site, fileHashes))
.build();
this.site = site;
this.indexTemplate = pebbleEngine.getTemplate("index_template");
this.articleTemplate = pebbleEngine.getTemplate("article_template");
}
@ -94,7 +99,12 @@ public class TemplateRenderer {
private String renderTemplate(PebbleTemplate template, Map<String, ?> context) throws IOException {
var writer = new StringWriter();
template.evaluate(writer, (Map<String, Object>) context);
var html = writer.toString();
return writer.toString();
if (minifier != null) { // not checking site.minify because the minifier may not be available
html = minifier.minify(html);
}
return html;
}
}