From 4ba8ef9ec4e60c4f056d073d7e9f4a8f823a2507 Mon Sep 17 00:00:00 2001 From: Richard Maw Date: Wed, 11 Sep 2024 22:28:55 +0100 Subject: [PATCH 1/5] Factor base element fixup into HTMLUtil This needs to be fixed both in macros that show HTML frames and in the stat sheets, so there should be a common util for it. --- .../functions/MacroDialogFunctions.java | 22 +-------- .../net/rptools/maptool/util/HTMLUtil.java | 48 +++++++++++++++++++ 2 files changed, 50 insertions(+), 20 deletions(-) create mode 100644 src/main/java/net/rptools/maptool/util/HTMLUtil.java diff --git a/src/main/java/net/rptools/maptool/client/functions/MacroDialogFunctions.java b/src/main/java/net/rptools/maptool/client/functions/MacroDialogFunctions.java index 886f514f5e..5203d2d8cb 100644 --- a/src/main/java/net/rptools/maptool/client/functions/MacroDialogFunctions.java +++ b/src/main/java/net/rptools/maptool/client/functions/MacroDialogFunctions.java @@ -35,13 +35,11 @@ import net.rptools.maptool.model.library.Library; import net.rptools.maptool.model.library.LibraryManager; import net.rptools.maptool.util.FunctionUtil; +import net.rptools.maptool.util.HTMLUtil; import net.rptools.parser.Parser; import net.rptools.parser.ParserException; import net.rptools.parser.VariableResolver; import net.rptools.parser.function.AbstractFunction; -import org.jsoup.Jsoup; -import org.jsoup.nodes.Element; -import org.jsoup.parser.Tag; public class MacroDialogFunctions extends AbstractFunction { private static final MacroDialogFunctions instance = new MacroDialogFunctions(); @@ -200,23 +198,7 @@ private String showURL(String name, URL url, String opts, FrameType frameType, b I18N.getText("macro.function.html5.invalidURI", url.toExternalForm())); } - htmlString = library.get().readAsString(url).get(); - - var document = Jsoup.parse(htmlString); - var head = document.select("head").first(); - if (head != null) { - String baseURL = url.toExternalForm().replaceFirst("\\?.*", ""); - baseURL = baseURL.substring(0, baseURL.lastIndexOf("/") + 1); - var baseElement = new Element(Tag.valueOf("base"), "").attr("href", baseURL); - if (head.children().isEmpty()) { - head.appendChild(baseElement); - } else { - head.child(0).before(baseElement); - } - - htmlString = document.html(); - } - + htmlString = HTMLUtil.fixHTMLBase(library.get().readAsString(url).get(), url); } catch (InterruptedException | ExecutionException | IOException e) { throw new ParserException(e); } diff --git a/src/main/java/net/rptools/maptool/util/HTMLUtil.java b/src/main/java/net/rptools/maptool/util/HTMLUtil.java new file mode 100644 index 0000000000..2c7911568e --- /dev/null +++ b/src/main/java/net/rptools/maptool/util/HTMLUtil.java @@ -0,0 +1,48 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * MapTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.maptool.util; + +import java.net.URL; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Element; +import org.jsoup.parser.Tag; + +public class HTMLUtil { + + /** + * Parses the HTML in the string and sets the base to the correct location. + * + * @param htmlString the HTML to parse. + * @param url the origin URL to set the base relative to + * @return the fixed-up HTML + */ + public static String fixHTMLBase(String htmlString, URL url) { + var document = Jsoup.parse(htmlString); + var head = document.select("head").first(); + if (head != null) { + String baseURL = url.toExternalForm().replaceFirst("\\?.*", ""); + baseURL = baseURL.substring(0, baseURL.lastIndexOf("/") + 1); + var baseElement = new Element(Tag.valueOf("base"), "").attr("href", baseURL); + if (head.children().isEmpty()) { + head.appendChild(baseElement); + } else { + head.child(0).before(baseElement); + } + + htmlString = document.html(); + } + return htmlString; + } +} From 9e955fab30afd7d89d7e5b3a1d48e5f2ecb88b0d Mon Sep 17 00:00:00 2001 From: Richard Maw Date: Wed, 11 Sep 2024 22:33:57 +0100 Subject: [PATCH 2/5] Fix stat sheet base url after handlebars templating The Jsoup HTML fixup is destructive. {{> foo}} gets turned into {{> foo}} which is not a valid template. The Library.readAsHTMLContent method was removed because this was the only place it was used for and it can't be safely used there, and I couldn't think of another time it could be used. --- .../client/ui/sheet/stats/StatSheet.java | 7 ++- .../ui/sheet/stats/StatSheetListener.java | 9 +++- .../maptool/model/library/Library.java | 43 ------------------- .../builtin/ClassPathAddOnLibrary.java | 5 --- .../model/sheet/stats/StatSheetManager.java | 2 +- 5 files changed, 13 insertions(+), 53 deletions(-) diff --git a/src/main/java/net/rptools/maptool/client/ui/sheet/stats/StatSheet.java b/src/main/java/net/rptools/maptool/client/ui/sheet/stats/StatSheet.java index ae273bdb30..331e168acc 100644 --- a/src/main/java/net/rptools/maptool/client/ui/sheet/stats/StatSheet.java +++ b/src/main/java/net/rptools/maptool/client/ui/sheet/stats/StatSheet.java @@ -15,12 +15,14 @@ package net.rptools.maptool.client.ui.sheet.stats; import java.io.IOException; +import java.net.URL; import javafx.application.Platform; import net.rptools.maptool.client.AppConstants; import net.rptools.maptool.client.MapTool; import net.rptools.maptool.model.Token; import net.rptools.maptool.model.sheet.stats.StatSheetContext; import net.rptools.maptool.model.sheet.stats.StatSheetLocation; +import net.rptools.maptool.util.HTMLUtil; import net.rptools.maptool.util.HandlebarsUtil; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -39,10 +41,11 @@ public class StatSheet { * @param content the content of the stat sheet. * @param location the location of the stat sheet. */ - public void setContent(Token token, String content, StatSheetLocation location) { + public void setContent(Token token, String content, URL entry, StatSheetLocation location) { try { var statSheetContext = new StatSheetContext(token, MapTool.getPlayer(), location); - var output = new HandlebarsUtil<>(content).apply(statSheetContext); + var output = + HTMLUtil.fixHTMLBase(new HandlebarsUtil<>(content, entry).apply(statSheetContext), entry); Platform.runLater( () -> { var overlay = diff --git a/src/main/java/net/rptools/maptool/client/ui/sheet/stats/StatSheetListener.java b/src/main/java/net/rptools/maptool/client/ui/sheet/stats/StatSheetListener.java index 766a7605cc..b5bcab3396 100644 --- a/src/main/java/net/rptools/maptool/client/ui/sheet/stats/StatSheetListener.java +++ b/src/main/java/net/rptools/maptool/client/ui/sheet/stats/StatSheetListener.java @@ -49,13 +49,18 @@ public void onHoverEnter(TokenHoverEnter event) { */ MapTool.getFrame().hideControlPanel(); statSheet = new StatSheet(); - var ss = event.token().getStatSheet(); + var ssProperties = event.token().getStatSheet(); + var ssId = ssProperties.id(); + var ssRecord = ssManager.getStatSheet(ssId); var token = event.token(); if (MapTool.getPlayer().isGM() || AppUtil.playerOwns(token) || token.getType() != Type.NPC) { statSheet.setContent( - event.token(), ssManager.getStatSheetContent(ss.id()), ss.location()); + event.token(), + ssManager.getStatSheetContent(ssId), + ssRecord.entry(), + ssProperties.location()); } } } diff --git a/src/main/java/net/rptools/maptool/model/library/Library.java b/src/main/java/net/rptools/maptool/model/library/Library.java index 2ffc1934b8..451b0f7ba4 100644 --- a/src/main/java/net/rptools/maptool/model/library/Library.java +++ b/src/main/java/net/rptools/maptool/model/library/Library.java @@ -21,16 +21,12 @@ import java.util.Optional; import java.util.Set; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; import net.rptools.lib.MD5Key; import net.rptools.maptool.client.MapToolMacroContext; import net.rptools.maptool.client.macro.MacroManager.MacroDetails; import net.rptools.maptool.model.Asset; import net.rptools.maptool.model.Token; import net.rptools.maptool.model.library.data.LibraryData; -import org.jsoup.Jsoup; -import org.jsoup.nodes.Element; -import org.jsoup.parser.Tag; /** Interface for classes that represents a framework library. */ public interface Library { @@ -77,45 +73,6 @@ public interface Library { */ CompletableFuture readAsString(URL location) throws IOException; - /** - * Reads the location as a string parses the HTML in the string and sets the base to the correct - * location. - * - * @param location the location to read. - * @return the contents of the location as a string. - * @throws IOException if there is an io error reading the location. - */ - default CompletableFuture readAsHTMLContent(URL location) throws IOException { - String htmlString = ""; - try { - Optional library = new LibraryManager().getLibrary(location).get(); - if (library.isEmpty()) { - throw new IOException("Location not found"); - } - - htmlString = library.get().readAsString(location).get(); - - var document = Jsoup.parse(htmlString); - var head = document.select("head").first(); - if (head != null) { - String baseURL = location.toExternalForm().replaceFirst("\\?.*", ""); - baseURL = baseURL.substring(0, baseURL.lastIndexOf("/") + 1); - var baseElement = new Element(Tag.valueOf("base"), "").attr("href", baseURL); - if (head.children().isEmpty()) { - head.appendChild(baseElement); - } else { - head.child(0).before(baseElement); - } - - htmlString = document.html(); - } - - } catch (InterruptedException | ExecutionException e) { - throw new IOException(e); - } - return CompletableFuture.completedFuture(htmlString); - } - /** * Returns an {@link InputStream} for the location specified. * diff --git a/src/main/java/net/rptools/maptool/model/library/builtin/ClassPathAddOnLibrary.java b/src/main/java/net/rptools/maptool/model/library/builtin/ClassPathAddOnLibrary.java index 2341953f96..473b08f70a 100644 --- a/src/main/java/net/rptools/maptool/model/library/builtin/ClassPathAddOnLibrary.java +++ b/src/main/java/net/rptools/maptool/model/library/builtin/ClassPathAddOnLibrary.java @@ -76,11 +76,6 @@ public CompletableFuture readAsString(URL location) throws IOException { return addOnLibrary.readAsString(location); } - @Override - public CompletableFuture readAsHTMLContent(URL location) throws IOException { - return BuiltInLibrary.super.readAsHTMLContent(location); - } - @Override public CompletableFuture read(URL location) throws IOException { return addOnLibrary.read(location); diff --git a/src/main/java/net/rptools/maptool/model/sheet/stats/StatSheetManager.java b/src/main/java/net/rptools/maptool/model/sheet/stats/StatSheetManager.java index b717c237da..2d0975a47f 100644 --- a/src/main/java/net/rptools/maptool/model/sheet/stats/StatSheetManager.java +++ b/src/main/java/net/rptools/maptool/model/sheet/stats/StatSheetManager.java @@ -150,7 +150,7 @@ public Set getStatSheets(String propertyType) { * @throws IOException if an error occurs reading the stat sheet. */ public void addStatSheet(StatSheet statSheet, Library library) throws IOException { - var html = library.readAsHTMLContent(statSheet.entry()).join(); + var html = library.readAsString(statSheet.entry()).join(); statSheets.put(statSheet, html); } From f864989ee477d79cdd707b581e6a0c2628c8ed30 Mon Sep 17 00:00:00 2001 From: Richard Maw Date: Wed, 11 Sep 2024 23:21:53 +0100 Subject: [PATCH 3/5] Remove redundant constructed Handlebars in HandlebarsUtil --- src/main/java/net/rptools/maptool/util/HandlebarsUtil.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/net/rptools/maptool/util/HandlebarsUtil.java b/src/main/java/net/rptools/maptool/util/HandlebarsUtil.java index 826df66f5b..2316263fa0 100644 --- a/src/main/java/net/rptools/maptool/util/HandlebarsUtil.java +++ b/src/main/java/net/rptools/maptool/util/HandlebarsUtil.java @@ -72,8 +72,6 @@ public HandlebarsUtil(String stringTemplate) throws IOException { */ public String apply(T bean) throws IOException { try { - - Handlebars handlebars = new Handlebars(); var context = Context.newBuilder(bean).resolver(JavaBeanValueResolver.INSTANCE).build(); return template.apply(context); } catch (IOException e) { From bea5d526436f20617bda0f0ea023727dc404e001 Mon Sep 17 00:00:00 2001 From: Richard Maw Date: Wed, 11 Sep 2024 23:26:52 +0100 Subject: [PATCH 4/5] Add lib: partial template loader to HandlebarsUtil --- .../rptools/maptool/util/HandlebarsUtil.java | 79 ++++++++++++++++++- 1 file changed, 77 insertions(+), 2 deletions(-) diff --git a/src/main/java/net/rptools/maptool/util/HandlebarsUtil.java b/src/main/java/net/rptools/maptool/util/HandlebarsUtil.java index 2316263fa0..a37d73740c 100644 --- a/src/main/java/net/rptools/maptool/util/HandlebarsUtil.java +++ b/src/main/java/net/rptools/maptool/util/HandlebarsUtil.java @@ -22,7 +22,13 @@ import com.github.jknack.handlebars.helper.ConditionalHelpers; import com.github.jknack.handlebars.helper.NumberHelper; import com.github.jknack.handlebars.helper.StringHelpers; +import com.github.jknack.handlebars.io.ClassPathTemplateLoader; +import com.github.jknack.handlebars.io.TemplateLoader; +import com.github.jknack.handlebars.io.URLTemplateLoader; +import java.io.File; import java.io.IOException; +import java.net.URL; +import java.nio.file.Path; import java.util.Arrays; import net.rptools.maptool.model.Token; import org.apache.logging.log4j.LogManager; @@ -41,15 +47,63 @@ public class HandlebarsUtil { /** Logging class instance. */ private static final Logger log = LogManager.getLogger(Token.class); + /** Handlebars partial template loader that uses Add-On Library URIs */ + private static class LibraryTemplateLoader extends URLTemplateLoader { + /** Path to template being resolved, relative paths are resolved relative to its parent. */ + Path current; + + private LibraryTemplateLoader(String current, String prefix, String suffix) { + if (!current.startsWith("/")) { + current = "/" + current; + } + this.current = new File(current).toPath(); + setPrefix(prefix); + setSuffix(suffix); + } + + private LibraryTemplateLoader(String current, String prefix) { + this(current, prefix, DEFAULT_SUFFIX); + } + + private LibraryTemplateLoader(String current) { + this(current, DEFAULT_PREFIX, DEFAULT_SUFFIX); + } + + /** Normalize locations by removing redundant path components */ + @Override + protected String normalize(final String location) { + return new File(location).toPath().normalize().toString(); + } + + /** Resolve possibly relative uri relative to current rooted below prefix */ + @Override + public String resolve(final String uri) { + var location = current.resolveSibling(uri).normalize().toString(); + if (location.startsWith("/")) { + location = location.substring(1); + } + return getPrefix() + location + getSuffix(); + } + + @Override + protected URL getResource(String location) throws IOException { + if (location.startsWith("/")) { + location = location.substring(1); + } + return new URL("lib://" + location); + } + } + /** * Creates a new instance of the utility class. * * @param stringTemplate The template to compile. + * @param loader The template loader for loading included partial templates * @throws IOException If there is an error compiling the template. */ - public HandlebarsUtil(String stringTemplate) throws IOException { + private HandlebarsUtil(String stringTemplate, TemplateLoader loader) throws IOException { try { - Handlebars handlebars = new Handlebars(); + Handlebars handlebars = new Handlebars(loader); StringHelpers.register(handlebars); Arrays.stream(ConditionalHelpers.values()) .forEach(h -> handlebars.registerHelper(h.name(), h)); @@ -63,6 +117,27 @@ public HandlebarsUtil(String stringTemplate) throws IOException { } } + /** + * Creates a new instance of the utility class. + * + * @param stringTemplate The template to compile. + * @param entry The lib:// URL of the template to load partial templates relative to + * @throws IOException If there is an error compiling the template. + */ + public HandlebarsUtil(String stringTemplate, URL entry) throws IOException { + this(stringTemplate, new LibraryTemplateLoader(entry.getHost() + entry.getPath())); + } + + /** + * Creates a new instance of the utility class. + * + * @param stringTemplate The template to compile. + * @throws IOException If there is an error compiling the template. + */ + public HandlebarsUtil(String stringTemplate) throws IOException { + this(stringTemplate, new ClassPathTemplateLoader()); + } + /** * Applies the template to the given bean. * From 4e6abc532209b02589b0c8ac5aa799f2d01e3d36 Mon Sep 17 00:00:00 2001 From: Richard Maw Date: Wed, 11 Sep 2024 23:38:30 +0100 Subject: [PATCH 5/5] Add include helper to HandlebarsUtil This can now be supported after fixing the premature base url handling and adding a lib:// based template loader. --- src/main/java/net/rptools/maptool/util/HandlebarsUtil.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/net/rptools/maptool/util/HandlebarsUtil.java b/src/main/java/net/rptools/maptool/util/HandlebarsUtil.java index a37d73740c..77be2a0061 100644 --- a/src/main/java/net/rptools/maptool/util/HandlebarsUtil.java +++ b/src/main/java/net/rptools/maptool/util/HandlebarsUtil.java @@ -20,6 +20,7 @@ import com.github.jknack.handlebars.context.JavaBeanValueResolver; import com.github.jknack.handlebars.helper.AssignHelper; import com.github.jknack.handlebars.helper.ConditionalHelpers; +import com.github.jknack.handlebars.helper.IncludeHelper; import com.github.jknack.handlebars.helper.NumberHelper; import com.github.jknack.handlebars.helper.StringHelpers; import com.github.jknack.handlebars.io.ClassPathTemplateLoader; @@ -109,6 +110,7 @@ private HandlebarsUtil(String stringTemplate, TemplateLoader loader) throws IOEx .forEach(h -> handlebars.registerHelper(h.name(), h)); NumberHelper.register(handlebars); handlebars.registerHelper(AssignHelper.NAME, AssignHelper.INSTANCE); + handlebars.registerHelper(IncludeHelper.NAME, IncludeHelper.INSTANCE); template = handlebars.compileInline(stringTemplate); } catch (IOException e) {