From 9c2cc909885078b3b0219f3a5ea92bc49d866593 Mon Sep 17 00:00:00 2001 From: Richard Maw Date: Sun, 5 Oct 2025 18:29:43 +0100 Subject: [PATCH 1/2] Identify .hbs files as non-HTML Asset Type Handlebars don't have their own MIME type so Tika doesn't consider checking whether something that looks like HTML has handlebars substitutions. Since identifying handlebars unambiguously by content is hard but the Handlebars partial template loader requires them to end .hbs we can use that to identify the type. --- .../java/net/rptools/maptool/model/Asset.java | 22 ++++++++++++++++++- .../rptools/maptool/util/HandlebarsUtil.java | 6 +++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/main/java/net/rptools/maptool/model/Asset.java b/src/main/java/net/rptools/maptool/model/Asset.java index 7b6781b167..d628935491 100644 --- a/src/main/java/net/rptools/maptool/model/Asset.java +++ b/src/main/java/net/rptools/maptool/model/Asset.java @@ -44,6 +44,7 @@ import net.rptools.maptool.model.library.addon.AddOnLibraryImporter; import net.rptools.maptool.server.proto.AssetDto; import net.rptools.maptool.server.proto.AssetDtoType; +import net.rptools.maptool.util.HandlebarsUtil; import org.apache.commons.io.FilenameUtils; import org.apache.tika.config.TikaConfig; import org.apache.tika.exception.TikaException; @@ -61,6 +62,8 @@ public enum Type { IMAGE(false, "", Asset::createImageAsset), // extension is determined from format. /** The {@code Asset} is an audio file. */ AUDIO(false, "", Asset::createAudioAsset), // extension is determined from format. + /** The {@code Asset} is a Handlebars template. */ + HANDLEBARS(true, "hbs", Asset::createHandlebarsAsset), /** The {@code Asset} is an HTML string. */ HTML(true, "html", Asset::createHTMLAsset), /** The {@code Asset} is some generic data. */ @@ -161,7 +164,12 @@ public static Type fromMediaType(MediaType mediaType, String filename) { case "image" -> Type.IMAGE; case "text" -> switch (subType) { - case "html" -> Type.HTML; + case "html" -> { + if (HandlebarsUtil.isAssetFileHandlebars(filename)) { + yield Type.HANDLEBARS; + } + yield Type.HTML; + } case "markdown", "x-web-markdown" -> Type.MARKDOWN; case "javascript" -> Type.JAVASCRIPT; case "css" -> Type.CSS; @@ -419,6 +427,18 @@ public static Asset createAssetDetectType(String name, byte[] data, File file) return factory.apply(name, data); } + /** + * Creates a Handlebars {@code Asset}. + * + * @param name The name of the {@code Asset}. + * @param data The data for the {@code Asset}. + * @return the Handlebars {@code Asset}. + */ + public static Asset createHandlebarsAsset(String name, byte[] data) { + return new Asset( + null, name, data, Type.HANDLEBARS, Type.HANDLEBARS.getDefaultExtension(), false); + } + /** * Creates a HTML {@code Asset}. * diff --git a/src/main/java/net/rptools/maptool/util/HandlebarsUtil.java b/src/main/java/net/rptools/maptool/util/HandlebarsUtil.java index ff17e8310d..18d1ab83b2 100644 --- a/src/main/java/net/rptools/maptool/util/HandlebarsUtil.java +++ b/src/main/java/net/rptools/maptool/util/HandlebarsUtil.java @@ -52,6 +52,12 @@ * @param The type of the bean to apply the template to. */ public class HandlebarsUtil { + public static boolean isAssetFileHandlebars(String filename) { + if (filename == null) { + return false; + } + return filename.toLowerCase().endsWith(".hbs"); + } /** The compiled template. */ private final Template template; From ed793b7f5581bb14ad10f282367b70fa348195f1 Mon Sep 17 00:00:00 2001 From: Richard Maw Date: Sun, 5 Oct 2025 19:03:44 +0100 Subject: [PATCH 2/2] Pass filenames to Asset types in more places We have filenames and passing them can help identify files correctly. --- .../client/ui/htmlframe/HTMLContent.java | 17 ++++++++--------- .../library/addon/AddOnLibraryImporter.java | 12 ++++++------ 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/main/java/net/rptools/maptool/client/ui/htmlframe/HTMLContent.java b/src/main/java/net/rptools/maptool/client/ui/htmlframe/HTMLContent.java index 0ba0913ff0..d796ab08e8 100644 --- a/src/main/java/net/rptools/maptool/client/ui/htmlframe/HTMLContent.java +++ b/src/main/java/net/rptools/maptool/client/ui/htmlframe/HTMLContent.java @@ -313,23 +313,23 @@ public HTMLContent fetchContent() throws IOException { if (!(content instanceof UrlContent urlContent)) { throw new IllegalStateException("HTMLContent is not a URL"); } + var url = urlContent.url(); try { - Optional libraryOpt = new LibraryManager().getLibrary(urlContent.url()).get(); + Optional libraryOpt = new LibraryManager().getLibrary(url).get(); if (libraryOpt.isEmpty()) { - throw new IOException( - I18N.getText("msg.error.html.loadingURL", urlContent.url().toExternalForm())); + throw new IOException(I18N.getText("msg.error.html.loadingURL", url.toExternalForm())); } var library = libraryOpt.get(); - var assetKey = library.getAssetKey(urlContent.url()).get().orElse(null); + var assetKey = library.getAssetKey(url).get().orElse(null); // Check if the asset key is null, if so try reading the resource as a string from the // library if (assetKey == null) { - String html = library.readAsString(urlContent.url()).get(); + String html = library.readAsString(url).get(); if (html != null) { - var mediaType = Asset.getMediaType("", html.getBytes(StandardCharsets.UTF_8)); - var assetType = Asset.Type.fromMediaType(mediaType); + var mediaType = Asset.getMediaType(url.getPath(), html.getBytes(StandardCharsets.UTF_8)); + var assetType = Asset.Type.fromMediaType(mediaType, url.getPath()); if (assetType == Asset.Type.HTML) { return new HTMLContent(new HtmlDocumentContent(html)); @@ -352,8 +352,7 @@ public HTMLContent fetchContent() throws IOException { } catch (InterruptedException | ExecutionException e) { throw new IOException(e); } - throw new IOException( - I18N.getText("msg.error.html.loadingURL", urlContent.url().toExternalForm())); + throw new IOException(I18N.getText("msg.error.html.loadingURL", url.toExternalForm())); } /** diff --git a/src/main/java/net/rptools/maptool/model/library/addon/AddOnLibraryImporter.java b/src/main/java/net/rptools/maptool/model/library/addon/AddOnLibraryImporter.java index e0b66d3cb4..76c7159e42 100644 --- a/src/main/java/net/rptools/maptool/model/library/addon/AddOnLibraryImporter.java +++ b/src/main/java/net/rptools/maptool/model/library/addon/AddOnLibraryImporter.java @@ -248,9 +248,9 @@ private void addMetaData( String path = METADATA_DIR + entry.getName(); try (InputStream inputStream = zip.getInputStream(entry)) { byte[] bytes = inputStream.readAllBytes(); - MediaType mediaType = Asset.getMediaType(entry.getName(), bytes); - Asset asset = - Type.fromMediaType(mediaType).getFactory().apply(namespace + "/" + path, bytes); + var assetName = namespace + "/" + path; + MediaType mediaType = Asset.getMediaType(assetName, bytes); + Asset asset = Type.fromMediaType(mediaType, assetName).getFactory().apply(assetName, bytes); addAsset(asset); pathAssetMap.put(path, Pair.with(asset.getMD5Key(), asset.getType())); } @@ -277,9 +277,9 @@ private Map> processAssets(String namespace, ZipFile String path = entry.getName().substring(CONTENT_DIRECTORY.length()); try (InputStream inputStream = zip.getInputStream(entry)) { byte[] bytes = inputStream.readAllBytes(); - MediaType mediaType = Asset.getMediaType(entry.getName(), bytes); - Asset asset = - Type.fromMediaType(mediaType).getFactory().apply(namespace + "/" + path, bytes); + var assetName = namespace + "/" + path; + MediaType mediaType = Asset.getMediaType(assetName, bytes); + Asset asset = Type.fromMediaType(mediaType, assetName).getFactory().apply(assetName, bytes); addAsset(asset); pathAssetMap.put(path, Pair.with(asset.getMD5Key(), asset.getType())); }