From 59c7367847de1c09a0cc1a231a3172dbbb763ab4 Mon Sep 17 00:00:00 2001 From: bubblobill Date: Thu, 12 Feb 2026 13:39:40 +0800 Subject: [PATCH 01/14] Made handlebars creation more visible. Added cache. Moved in-house helpers to a separate class. Added helpers for mark-down, simple maths, and string comparison. --- .../maptool/util/HandlebarsHelpers.java | 332 ++++++++++++++++++ .../rptools/maptool/util/HandlebarsUtil.java | 178 +++++----- 2 files changed, 431 insertions(+), 79 deletions(-) create mode 100644 src/main/java/net/rptools/maptool/util/HandlebarsHelpers.java diff --git a/src/main/java/net/rptools/maptool/util/HandlebarsHelpers.java b/src/main/java/net/rptools/maptool/util/HandlebarsHelpers.java new file mode 100644 index 0000000000..127a999c59 --- /dev/null +++ b/src/main/java/net/rptools/maptool/util/HandlebarsHelpers.java @@ -0,0 +1,332 @@ +package net.rptools.maptool.util; + +import com.github.jknack.handlebars.*; +import com.github.jknack.handlebars.helper.ConditionalHelpers; +import com.github.jknack.handlebars.helper.LogHelper; +import com.github.jknack.handlebars.helper.StringHelpers; +import com.github.jknack.handlebars.helper.ext.AssignHelper; +import com.github.jknack.handlebars.helper.ext.IncludeHelper; +import com.github.jknack.handlebars.helper.ext.NumberHelper; +import com.vladsch.flexmark.html.HtmlRenderer; +import com.vladsch.flexmark.parser.Parser; +import com.vladsch.flexmark.util.ast.Node; +import com.vladsch.flexmark.util.data.MutableDataSet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.math.BigDecimal; +import java.math.MathContext; +import java.math.RoundingMode; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; +import java.util.List; + +public class HandlebarsHelpers { + static Handlebars registerHelpers(Handlebars handlebars) { + StringHelpers.register(handlebars); + Arrays.stream(ConditionalHelpers.values()).forEach(h -> handlebars.registerHelper(h.name(), h)); + handlebars.registerHelper("json", Jackson2Helper.INSTANCE); + NumberHelper.register(handlebars); + handlebars.registerHelper(AssignHelper.NAME, AssignHelper.INSTANCE); + handlebars.registerHelper(IncludeHelper.NAME, IncludeHelper.INSTANCE); + handlebars.registerHelper(MarkdownHelper.NAME, MarkdownHelper.INSTANCE); + handlebars.registerHelper(Base64EncodeHelper.NAME, Base64EncodeHelper.INSTANCE); + Arrays.stream(HandlebarsHelpers.MathsHelpers.values()).forEach(h -> handlebars.registerHelper(h.name(), h)); + return handlebars; + } + + static class Base64EncodeHelper implements Helper { + /** + * A singleton instance of this helper. + */ + public static final Helper INSTANCE = new Base64EncodeHelper(); + + /** + * The helper's name. + */ + public static final String NAME = "base64Encode"; + + /** + * Turns the textual form of the value into a base64-encoded string. For example: + * + *
+         * <script type="application/json;base64" id="jsonProperty">
+         *   {{ base64Encode properties[0].value }}
+         * </script>
+         * <script type="application/javascript">
+         * const jsonProperty = JSON.parse(atob(document.getElementById("jsonProperty").innerText));
+         * </script>
+         * 
+ */ + + @Override + public Object apply(final Object context, final Options options) { + byte[] message = context.toString().getBytes(StandardCharsets.UTF_8); + return new Handlebars.SafeString(Base64.getUrlEncoder().encodeToString(message)); + } + } + + public enum MathsHelpers implements Helper { + add { + @Override + public Object apply(final Object a, final Options options) throws IOException { + try { + List numbers = numbers(a, options); + BigDecimal result = numbers.removeLast(); + while (!numbers.isEmpty()) { + result = result.add(numbers.removeLast()); + } + return String.valueOf(result); + } catch (Exception ignored) { + return "NaN"; + } + } + }, + subtract { + @Override + public Object apply(final Object a, final Options options) throws IOException { + try { + List numbers = numbers(a, options); + BigDecimal result = numbers.removeLast(); + while (!numbers.isEmpty()) { + result = result.subtract(numbers.removeLast()); + } + return String.valueOf(result); + } catch (Exception ignored) { + return "NaN"; + } + } + }, + multiply { + @Override + public Object apply(final Object a, final Options options) throws IOException { + try { + List numbers = numbers(a, options).reversed(); + BigDecimal result = numbers.removeLast(); + while (!numbers.isEmpty()) { + result = result.multiply(numbers.removeLast()); + } + return String.valueOf(result); + } catch (Exception ignored) { + return "NaN"; + } + } + }, + divide { + @Override + public Object apply(final Object a, final Options options) throws IOException { + try { + List numbers = numbers(a, options).reversed(); + BigDecimal result = numbers.removeLast(); + while (!numbers.isEmpty()) { + result = result.divide(numbers.removeLast(), RoundingMode.HALF_EVEN); + } + return String.valueOf(result); + } catch (Exception ignored) { + return "NaN"; + } + } + }, + max { + @Override + public Object apply(final Object a, final Options options) throws IOException { + try { + List numbers = numbers(a, options); + BigDecimal result = numbers.removeLast(); + while (!numbers.isEmpty()) { + result = result.max(numbers.removeLast()); + } + return String.valueOf(result); + } catch (Exception ignored) { + return "NaN"; + } + } + }, + min { + @Override + public Object apply(final Object a, final Options options) throws IOException { + try { + List numbers = numbers(a, options); + BigDecimal result = numbers.removeLast(); + while (!numbers.isEmpty()) { + result = result.min(numbers.removeLast()); + } + return String.valueOf(result); + } catch (Exception ignored) { + return "NaN"; + } + } + }, + mod { + @Override + public Object apply(final Object a, final Options options) throws IOException { + try { + List numbers = numbers(a, options); + BigDecimal result = numbers.removeLast(); + while (!numbers.isEmpty()) { + result = result.remainder(numbers.removeLast()); + } + return String.valueOf(result); + } catch (Exception ignored) { + return "NaN"; + } + } + }, + div { + @Override + public Object apply(final Object a, final Options options) throws IOException { + try { + List numbers = numbers(a, options); + BigDecimal result = numbers.removeLast(); + while (!numbers.isEmpty()) { + result = result.divideToIntegralValue(numbers.removeLast()); + } + return String.valueOf(result); + } catch (Exception ignored) { + return "NaN"; + } + } + }, + pow { + @Override + public Object apply(final Object a, final Options options) throws IOException { + try { + List numbers = numbers(a, options); + BigDecimal result = numbers.removeLast(); + while (!numbers.isEmpty()) { + result = result.pow(numbers.removeLast().intValue()); + } + return String.valueOf(result); + } catch (Exception ignored) { + return "NaN"; + } + } + }, + abs { + @Override + public Object apply(final Object a, final Options options) throws IOException { + try { + return new BigDecimal(a.toString()).abs().toString(); + } catch (Exception ignored) { + return "NaN"; + } + } + }, + sqrt { + @Override + public Object apply(final Object a, final Options options) throws IOException { + try { + return new BigDecimal(a.toString()).sqrt(MathContext.DECIMAL32).toString(); + } catch (Exception ignored) { + return "NaN"; + } + } + }, + + tau { + @Override + public Object apply(final Object a, final Options options) throws IOException { + return String.valueOf(Math.TAU); + } + }, + pi { + @Override + public Object apply(final Object a, final Options options) throws IOException { + return String.valueOf(Math.PI); + } + }, + + + ; + + List numbers(Object a, final Options options) { + int count = options.params.length + 1; + List values = new ArrayList<>(); + try { + values.add(new BigDecimal(a.toString())); + for (int i = 0; i < count; i++) { + values.add(new BigDecimal((double) options.param(i))); + } + + } catch (NumberFormatException ignored) { + } + return values; + } + } + + public static class HBLogger extends LogHelper { + private static final Logger log = LoggerFactory.getLogger(Handlebars.class); + /** + * A singleton instance of this helper. + */ + public static final Helper INSTANCE = new HBLogger(); + + /** + * The helper's name. + */ + public static final String NAME = "log"; + + @Override + public Object apply(Object context, Options options) throws IOException { + StringBuilder sb = new StringBuilder(); + String level = options.hash("level", "info"); + TagType tagType = options.tagType; + if (tagType.inline()) { + sb.append(context); + for (int i = 0; i < options.params.length; i++) { + sb.append(" ").append((Object) options.param(i)); + } + } else { + sb.append(options.fn()); + } + System.out.println("Handlebars(" + level + "): " + sb.toString().trim()); + switch (level) { + case "error": + log.error(sb.toString().trim()); + break; + case "debug": + log.debug(sb.toString().trim()); + break; + case "warn": + log.warn(sb.toString().trim()); + break; + case "trace": + log.trace(sb.toString().trim()); + break; + default: + log.info(sb.toString().trim()); + } + return null; + } + } + + public static class MarkdownHelper implements Helper { + private static final MutableDataSet MD_OPTIONS = new MutableDataSet(); + private static final Parser PARSER = Parser.builder(MD_OPTIONS).build(); + private static final HtmlRenderer HTML_RENDERER = HtmlRenderer.builder(MD_OPTIONS).build(); + + /** + * A singleton version of {@link MarkdownHelper}. + */ + public static final Helper INSTANCE = new MarkdownHelper(); + /** + * The helper's name. + */ + public static final String NAME = "markdown"; + + @Override + public Object apply(final Object context, final Options options) + throws IOException { + if (options.isFalsy(context)) { + return ""; + } + String markdown = context.toString(); + Node document = PARSER.parse(markdown); + return new Handlebars.SafeString(HTML_RENDERER.render(document)); + } + } +} + diff --git a/src/main/java/net/rptools/maptool/util/HandlebarsUtil.java b/src/main/java/net/rptools/maptool/util/HandlebarsUtil.java index 0aa8ff70b4..d9e79de65a 100644 --- a/src/main/java/net/rptools/maptool/util/HandlebarsUtil.java +++ b/src/main/java/net/rptools/maptool/util/HandlebarsUtil.java @@ -14,28 +14,29 @@ */ package net.rptools.maptool.util; -import com.github.jknack.handlebars.Context; -import com.github.jknack.handlebars.Handlebars; -import com.github.jknack.handlebars.Helper; -import com.github.jknack.handlebars.Options; -import com.github.jknack.handlebars.Template; +import com.github.jknack.handlebars.*; +import com.github.jknack.handlebars.cache.HighConcurrencyTemplateCache; import com.github.jknack.handlebars.context.JavaBeanValueResolver; -import com.github.jknack.handlebars.helper.ConditionalHelpers; -import com.github.jknack.handlebars.helper.StringHelpers; -import com.github.jknack.handlebars.helper.ext.AssignHelper; -import com.github.jknack.handlebars.helper.ext.IncludeHelper; -import com.github.jknack.handlebars.helper.ext.NumberHelper; +import com.github.jknack.handlebars.io.AbstractTemplateLoader; import com.github.jknack.handlebars.io.ClassPathTemplateLoader; import com.github.jknack.handlebars.io.TemplateLoader; -import com.github.jknack.handlebars.io.URLTemplateLoader; +import com.github.jknack.handlebars.io.TemplateSource; + import java.io.File; import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.Path; -import java.util.Arrays; -import java.util.Base64; +import java.util.concurrent.*; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + import net.rptools.maptool.model.Token; +import net.rptools.maptool.model.library.Library; +import net.rptools.maptool.model.library.LibraryManager; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -45,45 +46,92 @@ * @param The type of the bean to apply the template to. */ public class HandlebarsUtil { + private static final HighConcurrencyTemplateCache HIGH_CONCURRENCY_TEMPLATE_CACHE = new HighConcurrencyTemplateCache(); + + static Handlebars getHandlebarsInstance(@Nullable TemplateLoader loader) { + return HandlebarsHelpers.registerHelpers(new Handlebars() + .with(loader) + .with(HIGH_CONCURRENCY_TEMPLATE_CACHE) + .preEvaluatePartialBlocks(true) + .parentScopeResolution(false) + .setCharset(StandardCharsets.UTF_8) + ); + } + + public static boolean isAssetFileHandlebars(String filename) { + if (filename == null) { + return false; + } + return filename.toLowerCase().endsWith(".hbs"); + } - /** The compiled template. */ + /** + * The compiled template. + */ private final Template template; - /** Logging class instance. */ + /** + * 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; + /** + * Handlebars partial template source that uses Add-On files + */ + private static record LibraryTemplateSource(@Nonnull Library library, @Nonnull String filename) + implements TemplateSource { + @Override + public long lastModified() { + // No modification time is available. + return -1; + } - private LibraryTemplateLoader(String current, String prefix, String suffix) { - if (!current.startsWith("/")) { - current = "/" + current; + @Override + @Nonnull + public String content(@Nonnull final Charset charset) throws IOException { + try { + // The library API requires a URL even if it only uses the path. + var url = new URI("lib", library.getNamespace().join(), filename, null).toURL(); + try (var is = library.read(url).join()) { + return new String(is.readAllBytes(), charset); + } + } catch (URISyntaxException e) { + throw new AssertionError("lib URL of namespace and filename should be valid", e); } - this.current = new File(current).toPath(); - setPrefix(prefix); - setSuffix(suffix); } + } - private LibraryTemplateLoader(String current, String prefix) { - this(current, prefix, DEFAULT_SUFFIX); - } + /** + * Handlebars partial template loader that uses Add-On Library URIs + */ + private static class LibraryTemplateLoader extends AbstractTemplateLoader { + /** + * Path to template being resolved, relative paths are resolved relative to its parent. + */ + @Nonnull + final Path current; - private LibraryTemplateLoader(String current) { - this(current, DEFAULT_PREFIX, DEFAULT_SUFFIX); - } + @Nonnull + final Library library; - /** Normalize locations by removing redundant path components */ - @Override - protected String normalize(final String location) { - return new File(location).toPath().normalize().toString(); + private LibraryTemplateLoader(@Nonnull String current, @Nonnull Library library) { + current = current.replace('\\', '/'); + if (!current.startsWith("/")) { + current = "/" + current; + } + this.current = new File(current).toPath(); + this.library = library; + setPrefix(TemplateLoader.DEFAULT_PREFIX); + setSuffix(TemplateLoader.DEFAULT_SUFFIX); } - /** Resolve possibly relative uri relative to current rooted below prefix */ + /** + * Resolve possibly relative uri to a new location relative to current rooted below prefix + */ @Override - public String resolve(final String uri) { - var location = current.resolveSibling(uri).normalize().toString(); + @Nonnull + public String resolve(@Nonnull final String path) { + var location = current.resolveSibling(path).normalize().toString().replace('\\', '/'); if (location.startsWith("/")) { location = location.substring(1); } @@ -91,34 +139,9 @@ public String resolve(final String uri) { } @Override - protected URL getResource(String location) throws IOException { - if (location.startsWith("/")) { - location = location.substring(1); - } - return new URL("lib://" + location); - } - } - - private static enum MapToolHelpers implements Helper { - /** - * Turns the textual form of the value into a base64-encoded string. For example: - * - *
-     * <script type="application/json;base64" id="jsonProperty">
-     *   {{ base64Encode properties[0].value }}
-     * </script>
-     * <script type="application/javascript">
-     * const jsonProperty = JSON.parse(atob(document.getElementById("jsonProperty").innerText));
-     * </script>
-     * 
- */ - base64Encode { - @Override - public Object apply(final Object context, final Options options) { - byte[] message = context.toString().getBytes(StandardCharsets.UTF_8); - - return new Handlebars.SafeString(Base64.getUrlEncoder().encodeToString(message)); - } + @Nonnull + public LibraryTemplateSource sourceAt(@Nonnull final String location) { + return new LibraryTemplateSource(library, resolve(location)); } } @@ -126,20 +149,12 @@ public Object apply(final Object context, final Options options) { * Creates a new instance of the utility class. * * @param stringTemplate The template to compile. - * @param loader The template loader for loading included partial templates + * @param loader The template loader for loading included partial templates * @throws IOException If there is an error compiling the template. */ private HandlebarsUtil(String stringTemplate, TemplateLoader loader) throws IOException { + Handlebars handlebars = getHandlebarsInstance(loader); try { - Handlebars handlebars = new Handlebars(loader); - StringHelpers.register(handlebars); - Arrays.stream(ConditionalHelpers.values()) - .forEach(h -> handlebars.registerHelper(h.name(), h)); - NumberHelper.register(handlebars); - handlebars.registerHelper(AssignHelper.NAME, AssignHelper.INSTANCE); - handlebars.registerHelper(IncludeHelper.NAME, IncludeHelper.INSTANCE); - Arrays.stream(MapToolHelpers.values()).forEach(h -> handlebars.registerHelper(h.name(), h)); - template = handlebars.compileInline(stringTemplate); } catch (IOException e) { log.error("Handlebars Error: {}", e.getMessage()); @@ -151,11 +166,16 @@ private HandlebarsUtil(String stringTemplate, TemplateLoader loader) throws IOEx * 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 + * @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())); + this( + stringTemplate, + new LibraryTemplateLoader( + entry.getPath(), + // Template is defined by AddOn so library should always be present. + new LibraryManager().getLibrary(entry).join().orElseThrow())); } /** @@ -184,4 +204,4 @@ public String apply(T bean) throws IOException { throw e; } } -} +} \ No newline at end of file From 079574ff8907a292b80da3a5c34559d6c891cba3 Mon Sep 17 00:00:00 2001 From: bubblobill Date: Thu, 12 Feb 2026 16:39:43 +0800 Subject: [PATCH 02/14] formatting --- .../maptool/util/HandlebarsHelpers.java | 577 +++++++++--------- .../rptools/maptool/util/HandlebarsUtil.java | 59 +- 2 files changed, 310 insertions(+), 326 deletions(-) diff --git a/src/main/java/net/rptools/maptool/util/HandlebarsHelpers.java b/src/main/java/net/rptools/maptool/util/HandlebarsHelpers.java index 127a999c59..e5b8677413 100644 --- a/src/main/java/net/rptools/maptool/util/HandlebarsHelpers.java +++ b/src/main/java/net/rptools/maptool/util/HandlebarsHelpers.java @@ -1,3 +1,17 @@ +/* + * 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 com.github.jknack.handlebars.*; @@ -11,9 +25,6 @@ import com.vladsch.flexmark.parser.Parser; import com.vladsch.flexmark.util.ast.Node; import com.vladsch.flexmark.util.data.MutableDataSet; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.io.IOException; import java.math.BigDecimal; import java.math.MathContext; @@ -23,310 +34,298 @@ import java.util.Arrays; import java.util.Base64; import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class HandlebarsHelpers { - static Handlebars registerHelpers(Handlebars handlebars) { - StringHelpers.register(handlebars); - Arrays.stream(ConditionalHelpers.values()).forEach(h -> handlebars.registerHelper(h.name(), h)); - handlebars.registerHelper("json", Jackson2Helper.INSTANCE); - NumberHelper.register(handlebars); - handlebars.registerHelper(AssignHelper.NAME, AssignHelper.INSTANCE); - handlebars.registerHelper(IncludeHelper.NAME, IncludeHelper.INSTANCE); - handlebars.registerHelper(MarkdownHelper.NAME, MarkdownHelper.INSTANCE); - handlebars.registerHelper(Base64EncodeHelper.NAME, Base64EncodeHelper.INSTANCE); - Arrays.stream(HandlebarsHelpers.MathsHelpers.values()).forEach(h -> handlebars.registerHelper(h.name(), h)); - return handlebars; - } - - static class Base64EncodeHelper implements Helper { - /** - * A singleton instance of this helper. - */ - public static final Helper INSTANCE = new Base64EncodeHelper(); + static Handlebars registerHelpers(Handlebars handlebars) { + StringHelpers.register(handlebars); + Arrays.stream(ConditionalHelpers.values()).forEach(h -> handlebars.registerHelper(h.name(), h)); + handlebars.registerHelper("json", Jackson2Helper.INSTANCE); + NumberHelper.register(handlebars); + handlebars.registerHelper(AssignHelper.NAME, AssignHelper.INSTANCE); + handlebars.registerHelper(IncludeHelper.NAME, IncludeHelper.INSTANCE); + handlebars.registerHelper(MarkdownHelper.NAME, MarkdownHelper.INSTANCE); + handlebars.registerHelper(Base64EncodeHelper.NAME, Base64EncodeHelper.INSTANCE); + Arrays.stream(HandlebarsHelpers.MathsHelpers.values()) + .forEach(h -> handlebars.registerHelper(h.name(), h)); + return handlebars; + } - /** - * The helper's name. - */ - public static final String NAME = "base64Encode"; + static class Base64EncodeHelper implements Helper { + /** A singleton instance of this helper. */ + public static final Helper INSTANCE = new Base64EncodeHelper(); - /** - * Turns the textual form of the value into a base64-encoded string. For example: - * - *
-         * <script type="application/json;base64" id="jsonProperty">
-         *   {{ base64Encode properties[0].value }}
-         * </script>
-         * <script type="application/javascript">
-         * const jsonProperty = JSON.parse(atob(document.getElementById("jsonProperty").innerText));
-         * </script>
-         * 
- */ + /** The helper's name. */ + public static final String NAME = "base64Encode"; - @Override - public Object apply(final Object context, final Options options) { - byte[] message = context.toString().getBytes(StandardCharsets.UTF_8); - return new Handlebars.SafeString(Base64.getUrlEncoder().encodeToString(message)); - } + /** + * Turns the textual form of the value into a base64-encoded string. For example: + * + *
+     * <script type="application/json;base64" id="jsonProperty">
+     *   {{ base64Encode properties[0].value }}
+     * </script>
+     * <script type="application/javascript">
+     * const jsonProperty = JSON.parse(atob(document.getElementById("jsonProperty").innerText));
+     * </script>
+     * 
+ */ + @Override + public Object apply(final Object context, final Options options) { + byte[] message = context.toString().getBytes(StandardCharsets.UTF_8); + return new Handlebars.SafeString(Base64.getUrlEncoder().encodeToString(message)); } + } - public enum MathsHelpers implements Helper { - add { - @Override - public Object apply(final Object a, final Options options) throws IOException { - try { - List numbers = numbers(a, options); - BigDecimal result = numbers.removeLast(); - while (!numbers.isEmpty()) { - result = result.add(numbers.removeLast()); - } - return String.valueOf(result); - } catch (Exception ignored) { - return "NaN"; - } - } - }, - subtract { - @Override - public Object apply(final Object a, final Options options) throws IOException { - try { - List numbers = numbers(a, options); - BigDecimal result = numbers.removeLast(); - while (!numbers.isEmpty()) { - result = result.subtract(numbers.removeLast()); - } - return String.valueOf(result); - } catch (Exception ignored) { - return "NaN"; - } - } - }, - multiply { - @Override - public Object apply(final Object a, final Options options) throws IOException { - try { - List numbers = numbers(a, options).reversed(); - BigDecimal result = numbers.removeLast(); - while (!numbers.isEmpty()) { - result = result.multiply(numbers.removeLast()); - } - return String.valueOf(result); - } catch (Exception ignored) { - return "NaN"; - } - } - }, - divide { - @Override - public Object apply(final Object a, final Options options) throws IOException { - try { - List numbers = numbers(a, options).reversed(); - BigDecimal result = numbers.removeLast(); - while (!numbers.isEmpty()) { - result = result.divide(numbers.removeLast(), RoundingMode.HALF_EVEN); - } - return String.valueOf(result); - } catch (Exception ignored) { - return "NaN"; - } - } - }, - max { - @Override - public Object apply(final Object a, final Options options) throws IOException { - try { - List numbers = numbers(a, options); - BigDecimal result = numbers.removeLast(); - while (!numbers.isEmpty()) { - result = result.max(numbers.removeLast()); - } - return String.valueOf(result); - } catch (Exception ignored) { - return "NaN"; - } - } - }, - min { - @Override - public Object apply(final Object a, final Options options) throws IOException { - try { - List numbers = numbers(a, options); - BigDecimal result = numbers.removeLast(); - while (!numbers.isEmpty()) { - result = result.min(numbers.removeLast()); - } - return String.valueOf(result); - } catch (Exception ignored) { - return "NaN"; - } - } - }, - mod { - @Override - public Object apply(final Object a, final Options options) throws IOException { - try { - List numbers = numbers(a, options); - BigDecimal result = numbers.removeLast(); - while (!numbers.isEmpty()) { - result = result.remainder(numbers.removeLast()); - } - return String.valueOf(result); - } catch (Exception ignored) { - return "NaN"; - } - } - }, - div { - @Override - public Object apply(final Object a, final Options options) throws IOException { - try { - List numbers = numbers(a, options); - BigDecimal result = numbers.removeLast(); - while (!numbers.isEmpty()) { - result = result.divideToIntegralValue(numbers.removeLast()); - } - return String.valueOf(result); - } catch (Exception ignored) { - return "NaN"; - } - } - }, - pow { - @Override - public Object apply(final Object a, final Options options) throws IOException { - try { - List numbers = numbers(a, options); - BigDecimal result = numbers.removeLast(); - while (!numbers.isEmpty()) { - result = result.pow(numbers.removeLast().intValue()); - } - return String.valueOf(result); - } catch (Exception ignored) { - return "NaN"; - } - } - }, - abs { - @Override - public Object apply(final Object a, final Options options) throws IOException { - try { - return new BigDecimal(a.toString()).abs().toString(); - } catch (Exception ignored) { - return "NaN"; - } - } - }, - sqrt { - @Override - public Object apply(final Object a, final Options options) throws IOException { - try { - return new BigDecimal(a.toString()).sqrt(MathContext.DECIMAL32).toString(); - } catch (Exception ignored) { - return "NaN"; - } - } - }, - - tau { - @Override - public Object apply(final Object a, final Options options) throws IOException { - return String.valueOf(Math.TAU); - } - }, - pi { - @Override - public Object apply(final Object a, final Options options) throws IOException { - return String.valueOf(Math.PI); - } - }, - - - ; + public enum MathsHelpers implements Helper { + add { + @Override + public Object apply(final Object a, final Options options) throws IOException { + try { + List numbers = numbers(a, options); + BigDecimal result = numbers.removeLast(); + while (!numbers.isEmpty()) { + result = result.add(numbers.removeLast()); + } + return String.valueOf(result); + } catch (Exception ignored) { + return "NaN"; + } + } + }, + subtract { + @Override + public Object apply(final Object a, final Options options) throws IOException { + try { + List numbers = numbers(a, options); + BigDecimal result = numbers.removeLast(); + while (!numbers.isEmpty()) { + result = result.subtract(numbers.removeLast()); + } + return String.valueOf(result); + } catch (Exception ignored) { + return "NaN"; + } + } + }, + multiply { + @Override + public Object apply(final Object a, final Options options) throws IOException { + try { + List numbers = numbers(a, options).reversed(); + BigDecimal result = numbers.removeLast(); + while (!numbers.isEmpty()) { + result = result.multiply(numbers.removeLast()); + } + return String.valueOf(result); + } catch (Exception ignored) { + return "NaN"; + } + } + }, + divide { + @Override + public Object apply(final Object a, final Options options) throws IOException { + try { + List numbers = numbers(a, options).reversed(); + BigDecimal result = numbers.removeLast(); + while (!numbers.isEmpty()) { + result = result.divide(numbers.removeLast(), RoundingMode.HALF_EVEN); + } + return String.valueOf(result); + } catch (Exception ignored) { + return "NaN"; + } + } + }, + max { + @Override + public Object apply(final Object a, final Options options) throws IOException { + try { + List numbers = numbers(a, options); + BigDecimal result = numbers.removeLast(); + while (!numbers.isEmpty()) { + result = result.max(numbers.removeLast()); + } + return String.valueOf(result); + } catch (Exception ignored) { + return "NaN"; + } + } + }, + min { + @Override + public Object apply(final Object a, final Options options) throws IOException { + try { + List numbers = numbers(a, options); + BigDecimal result = numbers.removeLast(); + while (!numbers.isEmpty()) { + result = result.min(numbers.removeLast()); + } + return String.valueOf(result); + } catch (Exception ignored) { + return "NaN"; + } + } + }, + mod { + @Override + public Object apply(final Object a, final Options options) throws IOException { + try { + List numbers = numbers(a, options); + BigDecimal result = numbers.removeLast(); + while (!numbers.isEmpty()) { + result = result.remainder(numbers.removeLast()); + } + return String.valueOf(result); + } catch (Exception ignored) { + return "NaN"; + } + } + }, + div { + @Override + public Object apply(final Object a, final Options options) throws IOException { + try { + List numbers = numbers(a, options); + BigDecimal result = numbers.removeLast(); + while (!numbers.isEmpty()) { + result = result.divideToIntegralValue(numbers.removeLast()); + } + return String.valueOf(result); + } catch (Exception ignored) { + return "NaN"; + } + } + }, + pow { + @Override + public Object apply(final Object a, final Options options) throws IOException { + try { + List numbers = numbers(a, options); + BigDecimal result = numbers.removeLast(); + while (!numbers.isEmpty()) { + result = result.pow(numbers.removeLast().intValue()); + } + return String.valueOf(result); + } catch (Exception ignored) { + return "NaN"; + } + } + }, + abs { + @Override + public Object apply(final Object a, final Options options) throws IOException { + try { + return new BigDecimal(a.toString()).abs().toString(); + } catch (Exception ignored) { + return "NaN"; + } + } + }, + sqrt { + @Override + public Object apply(final Object a, final Options options) throws IOException { + try { + return new BigDecimal(a.toString()).sqrt(MathContext.DECIMAL32).toString(); + } catch (Exception ignored) { + return "NaN"; + } + } + }, - List numbers(Object a, final Options options) { - int count = options.params.length + 1; - List values = new ArrayList<>(); - try { - values.add(new BigDecimal(a.toString())); - for (int i = 0; i < count; i++) { - values.add(new BigDecimal((double) options.param(i))); - } + tau { + @Override + public Object apply(final Object a, final Options options) throws IOException { + return String.valueOf(Math.TAU); + } + }, + pi { + @Override + public Object apply(final Object a, final Options options) throws IOException { + return String.valueOf(Math.PI); + } + }, + ; - } catch (NumberFormatException ignored) { - } - return values; + List numbers(Object a, final Options options) { + int count = options.params.length + 1; + List values = new ArrayList<>(); + try { + values.add(new BigDecimal(a.toString())); + for (int i = 0; i < count; i++) { + values.add(new BigDecimal((double) options.param(i))); } + + } catch (NumberFormatException ignored) { + } + return values; } + } + + public static class HBLogger extends LogHelper { + private static final Logger log = LoggerFactory.getLogger(Handlebars.class); - public static class HBLogger extends LogHelper { - private static final Logger log = LoggerFactory.getLogger(Handlebars.class); - /** - * A singleton instance of this helper. - */ - public static final Helper INSTANCE = new HBLogger(); + /** A singleton instance of this helper. */ + public static final Helper INSTANCE = new HBLogger(); - /** - * The helper's name. - */ - public static final String NAME = "log"; + /** The helper's name. */ + public static final String NAME = "log"; - @Override - public Object apply(Object context, Options options) throws IOException { - StringBuilder sb = new StringBuilder(); - String level = options.hash("level", "info"); - TagType tagType = options.tagType; - if (tagType.inline()) { - sb.append(context); - for (int i = 0; i < options.params.length; i++) { - sb.append(" ").append((Object) options.param(i)); - } - } else { - sb.append(options.fn()); - } - System.out.println("Handlebars(" + level + "): " + sb.toString().trim()); - switch (level) { - case "error": - log.error(sb.toString().trim()); - break; - case "debug": - log.debug(sb.toString().trim()); - break; - case "warn": - log.warn(sb.toString().trim()); - break; - case "trace": - log.trace(sb.toString().trim()); - break; - default: - log.info(sb.toString().trim()); - } - return null; + @Override + public Object apply(Object context, Options options) throws IOException { + StringBuilder sb = new StringBuilder(); + String level = options.hash("level", "info"); + TagType tagType = options.tagType; + if (tagType.inline()) { + sb.append(context); + for (int i = 0; i < options.params.length; i++) { + sb.append(" ").append((Object) options.param(i)); } + } else { + sb.append(options.fn()); + } + System.out.println("Handlebars(" + level + "): " + sb.toString().trim()); + switch (level) { + case "error": + log.error(sb.toString().trim()); + break; + case "debug": + log.debug(sb.toString().trim()); + break; + case "warn": + log.warn(sb.toString().trim()); + break; + case "trace": + log.trace(sb.toString().trim()); + break; + default: + log.info(sb.toString().trim()); + } + return null; } + } - public static class MarkdownHelper implements Helper { - private static final MutableDataSet MD_OPTIONS = new MutableDataSet(); - private static final Parser PARSER = Parser.builder(MD_OPTIONS).build(); - private static final HtmlRenderer HTML_RENDERER = HtmlRenderer.builder(MD_OPTIONS).build(); + public static class MarkdownHelper implements Helper { + private static final MutableDataSet MD_OPTIONS = new MutableDataSet(); + private static final Parser PARSER = Parser.builder(MD_OPTIONS).build(); + private static final HtmlRenderer HTML_RENDERER = HtmlRenderer.builder(MD_OPTIONS).build(); - /** - * A singleton version of {@link MarkdownHelper}. - */ - public static final Helper INSTANCE = new MarkdownHelper(); - /** - * The helper's name. - */ - public static final String NAME = "markdown"; + /** A singleton version of {@link MarkdownHelper}. */ + public static final Helper INSTANCE = new MarkdownHelper(); - @Override - public Object apply(final Object context, final Options options) - throws IOException { - if (options.isFalsy(context)) { - return ""; - } - String markdown = context.toString(); - Node document = PARSER.parse(markdown); - return new Handlebars.SafeString(HTML_RENDERER.render(document)); - } + /** The helper's name. */ + public static final String NAME = "markdown"; + + @Override + public Object apply(final Object context, final Options options) throws IOException { + if (options.isFalsy(context)) { + return ""; + } + String markdown = context.toString(); + Node document = PARSER.parse(markdown); + return new Handlebars.SafeString(HTML_RENDERER.render(document)); } + } } - diff --git a/src/main/java/net/rptools/maptool/util/HandlebarsUtil.java b/src/main/java/net/rptools/maptool/util/HandlebarsUtil.java index d9e79de65a..9af6d156b9 100644 --- a/src/main/java/net/rptools/maptool/util/HandlebarsUtil.java +++ b/src/main/java/net/rptools/maptool/util/HandlebarsUtil.java @@ -21,7 +21,6 @@ import com.github.jknack.handlebars.io.ClassPathTemplateLoader; import com.github.jknack.handlebars.io.TemplateLoader; import com.github.jknack.handlebars.io.TemplateSource; - import java.io.File; import java.io.IOException; import java.net.URI; @@ -33,7 +32,6 @@ import java.util.concurrent.*; import javax.annotation.Nonnull; import javax.annotation.Nullable; - import net.rptools.maptool.model.Token; import net.rptools.maptool.model.library.Library; import net.rptools.maptool.model.library.LibraryManager; @@ -46,16 +44,17 @@ * @param The type of the bean to apply the template to. */ public class HandlebarsUtil { - private static final HighConcurrencyTemplateCache HIGH_CONCURRENCY_TEMPLATE_CACHE = new HighConcurrencyTemplateCache(); + private static final HighConcurrencyTemplateCache HIGH_CONCURRENCY_TEMPLATE_CACHE = + new HighConcurrencyTemplateCache(); static Handlebars getHandlebarsInstance(@Nullable TemplateLoader loader) { - return HandlebarsHelpers.registerHelpers(new Handlebars() + return HandlebarsHelpers.registerHelpers( + new Handlebars() .with(loader) .with(HIGH_CONCURRENCY_TEMPLATE_CACHE) .preEvaluatePartialBlocks(true) .parentScopeResolution(false) - .setCharset(StandardCharsets.UTF_8) - ); + .setCharset(StandardCharsets.UTF_8)); } public static boolean isAssetFileHandlebars(String filename) { @@ -65,21 +64,15 @@ public static boolean isAssetFileHandlebars(String filename) { return filename.toLowerCase().endsWith(".hbs"); } - /** - * The compiled template. - */ + /** The compiled template. */ private final Template template; - /** - * Logging class instance. - */ + /** Logging class instance. */ private static final Logger log = LogManager.getLogger(Token.class); - /** - * Handlebars partial template source that uses Add-On files - */ + /** Handlebars partial template source that uses Add-On files */ private static record LibraryTemplateSource(@Nonnull Library library, @Nonnull String filename) - implements TemplateSource { + implements TemplateSource { @Override public long lastModified() { // No modification time is available. @@ -101,18 +94,12 @@ public String content(@Nonnull final Charset charset) throws IOException { } } - /** - * Handlebars partial template loader that uses Add-On Library URIs - */ + /** Handlebars partial template loader that uses Add-On Library URIs */ private static class LibraryTemplateLoader extends AbstractTemplateLoader { - /** - * Path to template being resolved, relative paths are resolved relative to its parent. - */ - @Nonnull - final Path current; + /** Path to template being resolved, relative paths are resolved relative to its parent. */ + @Nonnull final Path current; - @Nonnull - final Library library; + @Nonnull final Library library; private LibraryTemplateLoader(@Nonnull String current, @Nonnull Library library) { current = current.replace('\\', '/'); @@ -125,9 +112,7 @@ private LibraryTemplateLoader(@Nonnull String current, @Nonnull Library library) setSuffix(TemplateLoader.DEFAULT_SUFFIX); } - /** - * Resolve possibly relative uri to a new location relative to current rooted below prefix - */ + /** Resolve possibly relative uri to a new location relative to current rooted below prefix */ @Override @Nonnull public String resolve(@Nonnull final String path) { @@ -149,7 +134,7 @@ public LibraryTemplateSource sourceAt(@Nonnull final String 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 + * @param loader The template loader for loading included partial templates * @throws IOException If there is an error compiling the template. */ private HandlebarsUtil(String stringTemplate, TemplateLoader loader) throws IOException { @@ -166,16 +151,16 @@ private HandlebarsUtil(String stringTemplate, TemplateLoader loader) throws IOEx * 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 + * @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.getPath(), - // Template is defined by AddOn so library should always be present. - new LibraryManager().getLibrary(entry).join().orElseThrow())); + stringTemplate, + new LibraryTemplateLoader( + entry.getPath(), + // Template is defined by AddOn so library should always be present. + new LibraryManager().getLibrary(entry).join().orElseThrow())); } /** @@ -204,4 +189,4 @@ public String apply(T bean) throws IOException { throw e; } } -} \ No newline at end of file +} From 42f9233885a0a4a7fecd1f2bb045efd7c3528d7b Mon Sep 17 00:00:00 2001 From: bubblobill Date: Thu, 12 Mar 2026 17:54:01 +0800 Subject: [PATCH 03/14] Added helper dependency --- gradle/libs.versions.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 353737ab63..57c4224764 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,6 +6,7 @@ sentry = "7.22.5" jide = "3.7.9" flatlaf = "3.6.1" handlebars = "4.4.0" +handlebars-jackson = "4.3.1" jai-imageio = "1.4.0" graalvm-js = "21.2.0" pdfbox = "3.0.0" @@ -94,6 +95,7 @@ jsvg = { group = "com.github.weisj", name = "jsvg", version = "1.4.0" } handlebars = { group = "com.github.jknack", name = "handlebars", version.ref = "handlebars" } handlebars-helpers = { group = "com.github.jknack", name = "handlebars-helpers", version.ref = "handlebars" } +handlebars-jackson-helper = { group = "com.github.jknack", name = "handlebars-jackson2", version.ref = "handlebars-jackson" } # Apache commons and other utilities # parsing of configuration data @@ -213,7 +215,7 @@ flatlaf = [ "flatlaf-extras", "flatlaf-jide-oss", ] -handlebars = ["handlebars", "handlebars-helpers"] +handlebars = ["handlebars", "handlebars-helpers", "handlebars-jackson-helper"] junit = ["junit-api", "junit-engine", "junit-params"] jai-imageio = ["jai-imageio-core", "jai-imageio-jpeg"] graalvm-js = ["graalvm-js", "graalvm-js-scriptengine"] From 4542caf2e50eefb509f642863f85a0193292d339 Mon Sep 17 00:00:00 2001 From: bubblobill Date: Thu, 19 Mar 2026 00:39:28 +0800 Subject: [PATCH 04/14] Null/blank check added to Base64EncodeHelper. --- .../java/net/rptools/maptool/util/HandlebarsHelpers.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/net/rptools/maptool/util/HandlebarsHelpers.java b/src/main/java/net/rptools/maptool/util/HandlebarsHelpers.java index e5b8677413..7f9b7dfced 100644 --- a/src/main/java/net/rptools/maptool/util/HandlebarsHelpers.java +++ b/src/main/java/net/rptools/maptool/util/HandlebarsHelpers.java @@ -73,8 +73,12 @@ static class Base64EncodeHelper implements Helper { */ @Override public Object apply(final Object context, final Options options) { - byte[] message = context.toString().getBytes(StandardCharsets.UTF_8); - return new Handlebars.SafeString(Base64.getUrlEncoder().encodeToString(message)); + if(context == null || context instanceof String s && s.isBlank()){ + return ""; + } else { + byte[] message = context.toString().getBytes(StandardCharsets.UTF_8); + return new Handlebars.SafeString(Base64.getUrlEncoder().encodeToString(message)); + } } } From d5e092756b94f90cd4706d44335059fd81678363 Mon Sep 17 00:00:00 2001 From: bubblobill Date: Thu, 19 Mar 2026 00:40:24 +0800 Subject: [PATCH 05/14] Formatting --- src/main/java/net/rptools/maptool/util/HandlebarsHelpers.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/net/rptools/maptool/util/HandlebarsHelpers.java b/src/main/java/net/rptools/maptool/util/HandlebarsHelpers.java index 7f9b7dfced..c24a26ad81 100644 --- a/src/main/java/net/rptools/maptool/util/HandlebarsHelpers.java +++ b/src/main/java/net/rptools/maptool/util/HandlebarsHelpers.java @@ -73,7 +73,7 @@ static class Base64EncodeHelper implements Helper { */ @Override public Object apply(final Object context, final Options options) { - if(context == null || context instanceof String s && s.isBlank()){ + if (context == null || context instanceof String s && s.isBlank()) { return ""; } else { byte[] message = context.toString().getBytes(StandardCharsets.UTF_8); From fde4085e1a2957377a1918fb894388b891354aa3 Mon Sep 17 00:00:00 2001 From: bubblobill Date: Thu, 19 Mar 2026 00:44:05 +0800 Subject: [PATCH 06/14] Removed println from HBLogger. Added HBLogger to helper registrations. --- src/main/java/net/rptools/maptool/util/HandlebarsHelpers.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/net/rptools/maptool/util/HandlebarsHelpers.java b/src/main/java/net/rptools/maptool/util/HandlebarsHelpers.java index c24a26ad81..1b19ebd8ba 100644 --- a/src/main/java/net/rptools/maptool/util/HandlebarsHelpers.java +++ b/src/main/java/net/rptools/maptool/util/HandlebarsHelpers.java @@ -47,6 +47,7 @@ static Handlebars registerHelpers(Handlebars handlebars) { handlebars.registerHelper(IncludeHelper.NAME, IncludeHelper.INSTANCE); handlebars.registerHelper(MarkdownHelper.NAME, MarkdownHelper.INSTANCE); handlebars.registerHelper(Base64EncodeHelper.NAME, Base64EncodeHelper.INSTANCE); + handlebars.registerHelper(HBLogger.NAME, HBLogger.INSTANCE); Arrays.stream(HandlebarsHelpers.MathsHelpers.values()) .forEach(h -> handlebars.registerHelper(h.name(), h)); return handlebars; @@ -290,7 +291,7 @@ public Object apply(Object context, Options options) throws IOException { } else { sb.append(options.fn()); } - System.out.println("Handlebars(" + level + "): " + sb.toString().trim()); + switch (level) { case "error": log.error(sb.toString().trim()); From 90c3e0349c5eda5aeedbe7726a2f2fbfb67e66fb Mon Sep 17 00:00:00 2001 From: bubblobill Date: Thu, 19 Mar 2026 00:53:33 +0800 Subject: [PATCH 07/14] Changed MathsHelpers.numbers() to enhanced for structure. --- .../java/net/rptools/maptool/util/HandlebarsHelpers.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/main/java/net/rptools/maptool/util/HandlebarsHelpers.java b/src/main/java/net/rptools/maptool/util/HandlebarsHelpers.java index 1b19ebd8ba..1f1827f541 100644 --- a/src/main/java/net/rptools/maptool/util/HandlebarsHelpers.java +++ b/src/main/java/net/rptools/maptool/util/HandlebarsHelpers.java @@ -255,14 +255,12 @@ public Object apply(final Object a, final Options options) throws IOException { ; List numbers(Object a, final Options options) { - int count = options.params.length + 1; List values = new ArrayList<>(); try { values.add(new BigDecimal(a.toString())); - for (int i = 0; i < count; i++) { - values.add(new BigDecimal((double) options.param(i))); + for(Object o: options.params) { + values.add(new BigDecimal((double) o)); } - } catch (NumberFormatException ignored) { } return values; From c249a379cb181f7cdfe851794d9eea296c6eb6da Mon Sep 17 00:00:00 2001 From: bubblobill Date: Thu, 19 Mar 2026 22:51:34 +0800 Subject: [PATCH 08/14] HandlebarsUtil - set preEvaluatePartialBlocks to false (apparently faster). Added HandlebarsHelperTest with tests for maths helpers. HandlebarsHelpers; - added debug logging to maths helpers. - added number of parameters checks to maths helpers. - set the MathContext for BigDecimal operations. - miscellaneous bug fixes to maths helpers. - improved argument parsing to accept literal array strings, and named arguments. StrListFunctionsTest: fixed annoying type cast message. --- .../maptool/util/HandlebarsHelpers.java | 205 ++++++++++++---- .../rptools/maptool/util/HandlebarsUtil.java | 2 +- .../functions/StrListFunctionsTest.java | 2 +- .../maptool/util/HandlebarsHelperTest.java | 220 ++++++++++++++++++ 4 files changed, 376 insertions(+), 53 deletions(-) create mode 100644 src/test/java/net/rptools/maptool/util/HandlebarsHelperTest.java diff --git a/src/main/java/net/rptools/maptool/util/HandlebarsHelpers.java b/src/main/java/net/rptools/maptool/util/HandlebarsHelpers.java index 1f1827f541..527997455b 100644 --- a/src/main/java/net/rptools/maptool/util/HandlebarsHelpers.java +++ b/src/main/java/net/rptools/maptool/util/HandlebarsHelpers.java @@ -30,14 +30,14 @@ import java.math.MathContext; import java.math.RoundingMode; import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Base64; -import java.util.List; +import java.util.*; +import java.util.function.Function; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class HandlebarsHelpers { + private static final Logger log = LoggerFactory.getLogger(HandlebarsHelpers.class); + static Handlebars registerHelpers(Handlebars handlebars) { StringHelpers.register(handlebars); Arrays.stream(ConditionalHelpers.values()).forEach(h -> handlebars.registerHelper(h.name(), h)); @@ -50,6 +50,7 @@ static Handlebars registerHelpers(Handlebars handlebars) { handlebars.registerHelper(HBLogger.NAME, HBLogger.INSTANCE); Arrays.stream(HandlebarsHelpers.MathsHelpers.values()) .forEach(h -> handlebars.registerHelper(h.name(), h)); + return handlebars; } @@ -72,6 +73,7 @@ static class Base64EncodeHelper implements Helper { * </script> * */ + @SuppressWarnings("SpellCheckingInspection") @Override public Object apply(final Object context, final Options options) { if (context == null || context instanceof String s && s.isBlank()) { @@ -88,13 +90,15 @@ public enum MathsHelpers implements Helper { @Override public Object apply(final Object a, final Options options) throws IOException { try { - List numbers = numbers(a, options); + List numbers = numberList(a, options); + checkOperandCount(1, -1, numbers); BigDecimal result = numbers.removeLast(); while (!numbers.isEmpty()) { - result = result.add(numbers.removeLast()); + result = result.add(numbers.removeLast(), MATH_CONTEXT); } - return String.valueOf(result); - } catch (Exception ignored) { + return result.toPlainString(); + } catch (NumberFormatException | ArithmeticException e) { + log.debug("Function \"add\" - {}", e.getLocalizedMessage(), e); return "NaN"; } } @@ -103,13 +107,15 @@ public Object apply(final Object a, final Options options) throws IOException { @Override public Object apply(final Object a, final Options options) throws IOException { try { - List numbers = numbers(a, options); + List numbers = numberList(a, options); + checkOperandCount(1, -1, numbers); BigDecimal result = numbers.removeLast(); while (!numbers.isEmpty()) { - result = result.subtract(numbers.removeLast()); + result = result.subtract(numbers.removeLast(), MATH_CONTEXT); } - return String.valueOf(result); - } catch (Exception ignored) { + return result.toPlainString(); + } catch (NumberFormatException | ArithmeticException e) { + log.debug("Function \"subtract\" - {}", e.getLocalizedMessage(), e); return "NaN"; } } @@ -118,13 +124,15 @@ public Object apply(final Object a, final Options options) throws IOException { @Override public Object apply(final Object a, final Options options) throws IOException { try { - List numbers = numbers(a, options).reversed(); + List numbers = numberList(a, options).reversed(); + checkOperandCount(1, -1, numbers); BigDecimal result = numbers.removeLast(); while (!numbers.isEmpty()) { - result = result.multiply(numbers.removeLast()); + result = result.multiply(numbers.removeLast(), MATH_CONTEXT); } - return String.valueOf(result); - } catch (Exception ignored) { + return result.toPlainString(); + } catch (NumberFormatException | ArithmeticException e) { + log.debug("Function \"multiply\" - {}", e.getLocalizedMessage(), e); return "NaN"; } } @@ -133,13 +141,15 @@ public Object apply(final Object a, final Options options) throws IOException { @Override public Object apply(final Object a, final Options options) throws IOException { try { - List numbers = numbers(a, options).reversed(); + List numbers = numberList(a, options).reversed(); + checkOperandCount(1, -1, numbers); BigDecimal result = numbers.removeLast(); while (!numbers.isEmpty()) { - result = result.divide(numbers.removeLast(), RoundingMode.HALF_EVEN); + result = result.divide(numbers.removeLast(), MATH_CONTEXT); } - return String.valueOf(result); - } catch (Exception ignored) { + return result.toPlainString(); + } catch (NumberFormatException | ArithmeticException e) { + log.debug("Function \"divide\" - {}", e.getLocalizedMessage(), e); return "NaN"; } } @@ -148,13 +158,15 @@ public Object apply(final Object a, final Options options) throws IOException { @Override public Object apply(final Object a, final Options options) throws IOException { try { - List numbers = numbers(a, options); + List numbers = numberList(a, options); + checkOperandCount(1, -1, numbers); BigDecimal result = numbers.removeLast(); while (!numbers.isEmpty()) { result = result.max(numbers.removeLast()); } - return String.valueOf(result); - } catch (Exception ignored) { + return result.toPlainString(); + } catch (NumberFormatException | ArithmeticException e) { + log.debug("Function \"max\" - {}", e.getLocalizedMessage(), e); return "NaN"; } } @@ -163,13 +175,15 @@ public Object apply(final Object a, final Options options) throws IOException { @Override public Object apply(final Object a, final Options options) throws IOException { try { - List numbers = numbers(a, options); + List numbers = numberList(a, options); + checkOperandCount(1, -1, numbers); BigDecimal result = numbers.removeLast(); while (!numbers.isEmpty()) { result = result.min(numbers.removeLast()); } - return String.valueOf(result); - } catch (Exception ignored) { + return result.toPlainString(); + } catch (NumberFormatException | ArithmeticException e) { + log.debug("Function \"min\" - {}", e.getLocalizedMessage(), e); return "NaN"; } } @@ -178,13 +192,15 @@ public Object apply(final Object a, final Options options) throws IOException { @Override public Object apply(final Object a, final Options options) throws IOException { try { - List numbers = numbers(a, options); + List numbers = numberList(a, options); + checkOperandCount(2, -1, numbers); BigDecimal result = numbers.removeLast(); while (!numbers.isEmpty()) { - result = result.remainder(numbers.removeLast()); + result = result.remainder(numbers.removeLast(), MATH_CONTEXT); } - return String.valueOf(result); - } catch (Exception ignored) { + return result.toPlainString(); + } catch (NumberFormatException | ArithmeticException e) { + log.debug("Function \"mod\" - {}", e.getLocalizedMessage(), e); return "NaN"; } } @@ -193,13 +209,15 @@ public Object apply(final Object a, final Options options) throws IOException { @Override public Object apply(final Object a, final Options options) throws IOException { try { - List numbers = numbers(a, options); + List numbers = numberList(a, options).reversed(); + checkOperandCount(2, -1, numbers); BigDecimal result = numbers.removeLast(); while (!numbers.isEmpty()) { result = result.divideToIntegralValue(numbers.removeLast()); } - return String.valueOf(result); - } catch (Exception ignored) { + return result.toPlainString(); + } catch (NumberFormatException | ArithmeticException e) { + log.debug("Function \"div\" - {}", e.getLocalizedMessage(), e); return "NaN"; } } @@ -208,13 +226,15 @@ public Object apply(final Object a, final Options options) throws IOException { @Override public Object apply(final Object a, final Options options) throws IOException { try { - List numbers = numbers(a, options); + List numbers = numberList(a, options); + checkOperandCount(2, -1, numbers); BigDecimal result = numbers.removeLast(); while (!numbers.isEmpty()) { - result = result.pow(numbers.removeLast().intValue()); + result = result.pow(numbers.removeLast().intValue(), MATH_CONTEXT); } - return String.valueOf(result); - } catch (Exception ignored) { + return result.toPlainString(); + } catch (NumberFormatException | ArithmeticException e) { + log.debug("Function \"pow\" - {}", e.getLocalizedMessage(), e); return "NaN"; } } @@ -223,8 +243,11 @@ public Object apply(final Object a, final Options options) throws IOException { @Override public Object apply(final Object a, final Options options) throws IOException { try { - return new BigDecimal(a.toString()).abs().toString(); - } catch (Exception ignored) { + List numbers = numberList(a, options); + checkOperandCount(1, 1, numbers); + return numbers.removeFirst().abs().toPlainString(); + } catch (NumberFormatException | ArithmeticException e) { + log.debug(e.getLocalizedMessage(), e); return "NaN"; } } @@ -233,13 +256,16 @@ public Object apply(final Object a, final Options options) throws IOException { @Override public Object apply(final Object a, final Options options) throws IOException { try { - return new BigDecimal(a.toString()).sqrt(MathContext.DECIMAL32).toString(); - } catch (Exception ignored) { + List numbers = numberList(a, options); + checkOperandCount(1, 1, numbers); + return new BigDecimal(a.toString()).sqrt(MATH_CONTEXT).toString(); + } catch (NumberFormatException | ArithmeticException e) { + log.debug(e.getLocalizedMessage(), e); return "NaN"; } } }, - + /** 2 x Pi */ tau { @Override public Object apply(final Object a, final Options options) throws IOException { @@ -253,22 +279,99 @@ public Object apply(final Object a, final Options options) throws IOException { } }, ; + private static final MathContext MATH_CONTEXT = new MathContext(16, RoundingMode.HALF_EVEN); - List numbers(Object a, final Options options) { - List values = new ArrayList<>(); - try { - values.add(new BigDecimal(a.toString())); - for(Object o: options.params) { - values.add(new BigDecimal((double) o)); + /** + * Validate the correct number of operands in list. + * + * @param minRequired minimum required numberList to preform operation + * @param maxAllowed maximum required numberList to preform operation + * @param operands list of BigDecimal to check + * @throws ArithmeticException if operand list is out of bounds + */ + void checkOperandCount(int minRequired, int maxAllowed, List operands) + throws ArithmeticException { + if (maxAllowed > -1 && operands.size() > maxAllowed) { + throw new ArithmeticException("Too many operands."); + } + if (operands.size() < minRequired) { + throw new ArithmeticException("Not enough operands."); + } + } + + /** Convert passed object to BigDecimal */ + private static final Function toBigDecimal = + o -> { + try { + return new BigDecimal(String.valueOf(o), MATH_CONTEXT); + } catch (NumberFormatException ignored) { + return BigDecimal.valueOf(Double.NaN); + } + }; + + /** + * Compose passed values into a list of BigDecimal + * + * @param args list of object/object[] + * @return List of BigDecimal + */ + List numberList(Object... args) { + // ("num1", 1)("num2", "-1")("arr1", new Object[]{0.5, "1", 2, "3.5"}); + List values = new LinkedList<>(); + for (Object o : args) { + switch (o) { + case String[] strings -> { + for (String string : strings) { + values.add(toBigDecimal.apply(string)); + } + } + case String string -> { + string = string.strip(); + + if (string.startsWith("[") && string.contains("]")) { + /* deal with literal arrays */ + values.addAll( + numberList((Object) string.substring(1, string.indexOf("]")).split(","))); + } else if (string.contains(" ")) { + /* split space delimited values */ + values.addAll(numberList(String.join(" ", string))); + } else { + values.add(toBigDecimal.apply(string)); + } + } + case Object[] objectArray -> { + /* contents of options.params */ + for (var object : objectArray) { + values.add(toBigDecimal.apply(object)); + } + } + case Options options -> { + /* if named arguments exist, try converting them to BigDecimal */ + for (String key : options.hash.keySet()) { + try { + BigDecimal bd = new BigDecimal(options.hash(key).toString()); + values.add(bd); + } catch (NumberFormatException ignored) { + } + } + /* add values from parameter array */ + for (Object param : options.params) { + values.addAll(numberList(param)); + } + } + case null -> {} + default -> + /* hooray, it's just a single thing to convert */ + values.add(toBigDecimal.apply(o)); } - } catch (NumberFormatException ignored) { } return values; } } + @SuppressWarnings("LoggerInitializedWithForeignClass") public static class HBLogger extends LogHelper { - private static final Logger log = LoggerFactory.getLogger(Handlebars.class); + private static final Logger log = LoggerFactory.getLogger(HandlebarsHelpers.class); /** A singleton instance of this helper. */ public static final Helper INSTANCE = new HBLogger(); @@ -292,7 +395,7 @@ public Object apply(Object context, Options options) throws IOException { switch (level) { case "error": - log.error(sb.toString().trim()); + log.debug(sb.toString().trim()); break; case "debug": log.debug(sb.toString().trim()); diff --git a/src/main/java/net/rptools/maptool/util/HandlebarsUtil.java b/src/main/java/net/rptools/maptool/util/HandlebarsUtil.java index 9af6d156b9..feefb2d1ab 100644 --- a/src/main/java/net/rptools/maptool/util/HandlebarsUtil.java +++ b/src/main/java/net/rptools/maptool/util/HandlebarsUtil.java @@ -52,7 +52,7 @@ static Handlebars getHandlebarsInstance(@Nullable TemplateLoader loader) { new Handlebars() .with(loader) .with(HIGH_CONCURRENCY_TEMPLATE_CACHE) - .preEvaluatePartialBlocks(true) + .preEvaluatePartialBlocks(false) .parentScopeResolution(false) .setCharset(StandardCharsets.UTF_8)); } diff --git a/src/test/java/net/rptools/maptool/client/functions/StrListFunctionsTest.java b/src/test/java/net/rptools/maptool/client/functions/StrListFunctionsTest.java index d1fb6767a9..4e0e6c84cc 100644 --- a/src/test/java/net/rptools/maptool/client/functions/StrListFunctionsTest.java +++ b/src/test/java/net/rptools/maptool/client/functions/StrListFunctionsTest.java @@ -28,7 +28,7 @@ public class StrListFunctionsTest { private String delim = ","; private List toParms(Object... parms) { - ArrayList result = new ArrayList(); + List result = new ArrayList<>(); for (Object arg : parms) { if (arg instanceof Integer) arg = new BigDecimal((Integer) arg); result.add(arg); diff --git a/src/test/java/net/rptools/maptool/util/HandlebarsHelperTest.java b/src/test/java/net/rptools/maptool/util/HandlebarsHelperTest.java new file mode 100644 index 0000000000..c7b8f08589 --- /dev/null +++ b/src/test/java/net/rptools/maptool/util/HandlebarsHelperTest.java @@ -0,0 +1,220 @@ +/* + * 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 static org.junit.jupiter.api.Assertions.assertEquals; + +import com.github.jknack.handlebars.Context; +import com.github.jknack.handlebars.Handlebars; +import com.github.jknack.handlebars.Template; +import com.github.jknack.handlebars.io.ClassPathTemplateLoader; +import java.io.IOException; +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.Map; +import java.util.function.BiFunction; +import org.junit.jupiter.api.Test; + +public class HandlebarsHelperTest { + private final Handlebars hb = HandlebarsUtil.getHandlebarsInstance(new ClassPathTemplateLoader()); + private final Map contextData = + new HashMap<>() { + { + put("num1", 1); + put("num2", "-1"); + put("arr1", new Object[] {0.5, "1", 2, "3.5"}); + } + }; + private final Context context = Context.newBuilder(contextData).build(); + + /* function names + abs + add + div + divide + max + min + mod + multiply + pow + sqrt + subtract + */ + + private final BiFunction process = + (helper, values) -> { + try { + Template template = hb.compileInline(String.format("{{%s %s}}", helper, values)); + return template.apply(context); + } catch (IOException ex) { + return null; + } + }; + + @Test + public void testMathsConstants() { + assertEquals("3.141592653589793", process.apply("pi", "")); + assertEquals("6.283185307179586", process.apply("tau", "")); + } + + @Test + public void testMathsHelpersOneArg() { + assertEquals("1", process.apply("abs", "1")); + assertEquals("1.1", process.apply("abs", "-1.1")); + + assertEquals("1", process.apply("add", "1")); + assertEquals("-1", process.apply("add", "-1")); + assertEquals("NaN", process.apply("div", "1")); + assertEquals("1", process.apply("divide", "1")); + + assertEquals("1", process.apply("max", "1")); + assertEquals("1", process.apply("min", "1")); + assertEquals("NaN", process.apply("mod", "1")); + assertEquals("1", process.apply("multiply", "1")); + assertEquals("NaN", process.apply("pow", "1")); + + assertEquals("2", process.apply("sqrt", "4")); + assertEquals("NaN", process.apply("sqrt", "-1")); + assertEquals("1", process.apply("subtract", "1")); + } + + @Test + public void testMathsHelpersContextVariables() { + assertEquals( + new BigDecimal("1").doubleValue(), + new BigDecimal(process.apply("abs", "num1")).doubleValue()); + assertEquals( + new BigDecimal("1").doubleValue(), + new BigDecimal(process.apply("abs", "num2")).doubleValue()); + assertEquals("NaN", process.apply("abs", "arr1")); + assertEquals( + new BigDecimal("1").doubleValue(), + new BigDecimal(process.apply("add", "num1")).doubleValue()); + assertEquals( + new BigDecimal("-1").doubleValue(), + new BigDecimal(process.apply("add", "num2")).doubleValue()); + assertEquals( + new BigDecimal("7").doubleValue(), + new BigDecimal(process.apply("add", "arr1")).doubleValue()); + assertEquals("NaN", process.apply("div", "num1")); + assertEquals("NaN", process.apply("div", "num2")); + assertEquals( + new BigDecimal("0").doubleValue(), + new BigDecimal(process.apply("div", "arr1")).doubleValue()); + assertEquals( + new BigDecimal("1").doubleValue(), + new BigDecimal(process.apply("divide", "num1")).doubleValue()); + assertEquals( + new BigDecimal("-1").doubleValue(), + new BigDecimal(process.apply("divide", "num2")).doubleValue()); + assertEquals( + new BigDecimal("0.07142857142857142").doubleValue(), + new BigDecimal(process.apply("divide", "arr1")).doubleValue()); + assertEquals( + new BigDecimal("1").doubleValue(), + new BigDecimal(process.apply("max", "num1")).doubleValue()); + assertEquals( + new BigDecimal("-1").doubleValue(), + new BigDecimal(process.apply("max", "num2")).doubleValue()); + assertEquals( + new BigDecimal("3.5").doubleValue(), + new BigDecimal(process.apply("max", "arr1")).doubleValue()); + assertEquals( + new BigDecimal("1").doubleValue(), + new BigDecimal(process.apply("min", "num1")).doubleValue()); + assertEquals( + new BigDecimal("-1").doubleValue(), + new BigDecimal(process.apply("min", "num2")).doubleValue()); + assertEquals( + new BigDecimal("0.5").doubleValue(), + new BigDecimal(process.apply("min", "arr1")).doubleValue()); + assertEquals("NaN", process.apply("mod", "num1")); + assertEquals("NaN", process.apply("mod", "num2")); + assertEquals( + new BigDecimal("0").doubleValue(), + new BigDecimal(process.apply("mod", "arr1")).doubleValue()); + assertEquals( + new BigDecimal("1").doubleValue(), + new BigDecimal(process.apply("multiply", "num1")).doubleValue()); + assertEquals( + new BigDecimal("-1").doubleValue(), + new BigDecimal(process.apply("multiply", "num2")).doubleValue()); + assertEquals( + new BigDecimal("3.5").doubleValue(), + new BigDecimal(process.apply("multiply", "arr1")).doubleValue()); + assertEquals("NaN", process.apply("pow", "num1")); + assertEquals("NaN", process.apply("pow", "num2")); + assertEquals( + new BigDecimal("1").doubleValue(), + new BigDecimal(process.apply("pow", "arr1")).doubleValue()); + assertEquals( + new BigDecimal("1").doubleValue(), + new BigDecimal(process.apply("sqrt", "num1")).doubleValue()); + assertEquals("NaN", process.apply("sqrt", "num2")); + assertEquals("NaN", process.apply("sqrt", "arr1")); + assertEquals( + new BigDecimal("1").doubleValue(), + new BigDecimal(process.apply("subtract", "num1")).doubleValue()); + assertEquals( + new BigDecimal("-1").doubleValue(), + new BigDecimal(process.apply("subtract", "num2")).doubleValue()); + assertEquals( + new BigDecimal("0").doubleValue(), + new BigDecimal(process.apply("subtract", "arr1")).doubleValue()); + } + + @Test + public void testMathsHelpersMixedArgs() { + assertEquals( + new BigDecimal("7").doubleValue(), + new BigDecimal(process.apply("add", "-2 2 \"[3,-3]\" num1 num2 arr1 x=5 y=-5")) + .doubleValue()); + } + + @Test + public void testMathsHelpersNested() { + String complex = + """ + multiply 0.05 + (add + (subtract + (multiply (pi) 2) + (tau) + ) + "0.1e1") + (mod + (add + (div + (divide 9 3 2) + 1) + (pow + (abs + (subtract 0 + (sqrt 16) + ) + ) + 2 + ) + (max arr1) + (min arr1) + ) + 20 + ) + """; + assertEquals( + new BigDecimal("1").doubleValue(), + new BigDecimal(process.apply(complex, "")).doubleValue()); + } +} From e06cf7e3b3a7601822e59ddbf481cc064debeb3f Mon Sep 17 00:00:00 2001 From: bubblobill Date: Fri, 20 Mar 2026 22:10:19 +0800 Subject: [PATCH 09/14] corrected log level --- src/main/java/net/rptools/maptool/util/HandlebarsHelpers.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/net/rptools/maptool/util/HandlebarsHelpers.java b/src/main/java/net/rptools/maptool/util/HandlebarsHelpers.java index 527997455b..3eac5662ea 100644 --- a/src/main/java/net/rptools/maptool/util/HandlebarsHelpers.java +++ b/src/main/java/net/rptools/maptool/util/HandlebarsHelpers.java @@ -395,7 +395,7 @@ public Object apply(Object context, Options options) throws IOException { switch (level) { case "error": - log.debug(sb.toString().trim()); + log.error(sb.toString().trim()); break; case "debug": log.debug(sb.toString().trim()); From aee27fbd258411bdbe0a6d8f43a1b82546359904 Mon Sep 17 00:00:00 2001 From: bubblobill Date: Sat, 21 Mar 2026 16:48:35 +0800 Subject: [PATCH 10/14] Added missing EmbeddedHelper to helper registration. Added additional tests for; - embedded - include - partial (tres simple) - json --- .../maptool/util/HandlebarsHelpers.java | 2 + .../maptool/util/HandlebarsHelperTest.java | 189 ++++++++++++------ .../util/handlebars/embeddedHelperTest.hbs | 1 + .../util/handlebars/includeHelperTest.hbs | 1 + .../util/handlebars/jsonHelperTest.hbs | 1 + .../util/handlebars/partialHelperTest.hbs | 1 + .../rptools/maptool/util/handlebars/user.hbs | 1 + 7 files changed, 136 insertions(+), 60 deletions(-) create mode 100644 src/test/resources/net/rptools/maptool/util/handlebars/embeddedHelperTest.hbs create mode 100644 src/test/resources/net/rptools/maptool/util/handlebars/includeHelperTest.hbs create mode 100644 src/test/resources/net/rptools/maptool/util/handlebars/jsonHelperTest.hbs create mode 100644 src/test/resources/net/rptools/maptool/util/handlebars/partialHelperTest.hbs create mode 100644 src/test/resources/net/rptools/maptool/util/handlebars/user.hbs diff --git a/src/main/java/net/rptools/maptool/util/HandlebarsHelpers.java b/src/main/java/net/rptools/maptool/util/HandlebarsHelpers.java index 3eac5662ea..f5cb29a99b 100644 --- a/src/main/java/net/rptools/maptool/util/HandlebarsHelpers.java +++ b/src/main/java/net/rptools/maptool/util/HandlebarsHelpers.java @@ -16,6 +16,7 @@ import com.github.jknack.handlebars.*; import com.github.jknack.handlebars.helper.ConditionalHelpers; +import com.github.jknack.handlebars.helper.EmbeddedHelper; import com.github.jknack.handlebars.helper.LogHelper; import com.github.jknack.handlebars.helper.StringHelpers; import com.github.jknack.handlebars.helper.ext.AssignHelper; @@ -43,6 +44,7 @@ static Handlebars registerHelpers(Handlebars handlebars) { Arrays.stream(ConditionalHelpers.values()).forEach(h -> handlebars.registerHelper(h.name(), h)); handlebars.registerHelper("json", Jackson2Helper.INSTANCE); NumberHelper.register(handlebars); + handlebars.registerHelper(EmbeddedHelper.NAME, EmbeddedHelper.INSTANCE); handlebars.registerHelper(AssignHelper.NAME, AssignHelper.INSTANCE); handlebars.registerHelper(IncludeHelper.NAME, IncludeHelper.INSTANCE); handlebars.registerHelper(MarkdownHelper.NAME, MarkdownHelper.INSTANCE); diff --git a/src/test/java/net/rptools/maptool/util/HandlebarsHelperTest.java b/src/test/java/net/rptools/maptool/util/HandlebarsHelperTest.java index c7b8f08589..5099b9a324 100644 --- a/src/test/java/net/rptools/maptool/util/HandlebarsHelperTest.java +++ b/src/test/java/net/rptools/maptool/util/HandlebarsHelperTest.java @@ -16,20 +16,31 @@ import static org.junit.jupiter.api.Assertions.assertEquals; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import com.github.jknack.handlebars.Context; import com.github.jknack.handlebars.Handlebars; import com.github.jknack.handlebars.Template; import com.github.jknack.handlebars.io.ClassPathTemplateLoader; import java.io.IOException; import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; import java.util.function.BiFunction; + +import com.github.jknack.handlebars.io.TemplateLoader; +import com.github.jknack.handlebars.io.TemplateSource; +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; import org.junit.jupiter.api.Test; public class HandlebarsHelperTest { - private final Handlebars hb = HandlebarsUtil.getHandlebarsInstance(new ClassPathTemplateLoader()); - private final Map contextData = + private final TemplateLoader templateLoader = new ClassPathTemplateLoader( "/net/rptools/maptool/util/handlebars/"); + private final Handlebars hb = HandlebarsUtil.getHandlebarsInstance(templateLoader); + private final Map mathsContextData = new HashMap<>() { { put("num1", 1); @@ -37,9 +48,9 @@ public class HandlebarsHelperTest { put("arr1", new Object[] {0.5, "1", 2, "3.5"}); } }; - private final Context context = Context.newBuilder(contextData).build(); + private final Context mathsContext = Context.newBuilder(mathsContextData).build(); - /* function names + /* Maths function names abs add div @@ -53,11 +64,11 @@ public class HandlebarsHelperTest { subtract */ - private final BiFunction process = + private final BiFunction applyFunction = (helper, values) -> { try { Template template = hb.compileInline(String.format("{{%s %s}}", helper, values)); - return template.apply(context); + return template.apply(mathsContext); } catch (IOException ex) { return null; } @@ -65,121 +76,121 @@ public class HandlebarsHelperTest { @Test public void testMathsConstants() { - assertEquals("3.141592653589793", process.apply("pi", "")); - assertEquals("6.283185307179586", process.apply("tau", "")); + assertEquals("3.141592653589793", applyFunction.apply("pi", "")); + assertEquals("6.283185307179586", applyFunction.apply("tau", "")); } @Test public void testMathsHelpersOneArg() { - assertEquals("1", process.apply("abs", "1")); - assertEquals("1.1", process.apply("abs", "-1.1")); - - assertEquals("1", process.apply("add", "1")); - assertEquals("-1", process.apply("add", "-1")); - assertEquals("NaN", process.apply("div", "1")); - assertEquals("1", process.apply("divide", "1")); - - assertEquals("1", process.apply("max", "1")); - assertEquals("1", process.apply("min", "1")); - assertEquals("NaN", process.apply("mod", "1")); - assertEquals("1", process.apply("multiply", "1")); - assertEquals("NaN", process.apply("pow", "1")); - - assertEquals("2", process.apply("sqrt", "4")); - assertEquals("NaN", process.apply("sqrt", "-1")); - assertEquals("1", process.apply("subtract", "1")); + assertEquals("1", applyFunction.apply("abs", "1")); + assertEquals("1.1", applyFunction.apply("abs", "-1.1")); + + assertEquals("1", applyFunction.apply("add", "1")); + assertEquals("-1", applyFunction.apply("add", "-1")); + assertEquals("NaN", applyFunction.apply("div", "1")); + assertEquals("1", applyFunction.apply("divide", "1")); + + assertEquals("1", applyFunction.apply("max", "1")); + assertEquals("1", applyFunction.apply("min", "1")); + assertEquals("NaN", applyFunction.apply("mod", "1")); + assertEquals("1", applyFunction.apply("multiply", "1")); + assertEquals("NaN", applyFunction.apply("pow", "1")); + + assertEquals("2", applyFunction.apply("sqrt", "4")); + assertEquals("NaN", applyFunction.apply("sqrt", "-1")); + assertEquals("1", applyFunction.apply("subtract", "1")); } @Test public void testMathsHelpersContextVariables() { assertEquals( new BigDecimal("1").doubleValue(), - new BigDecimal(process.apply("abs", "num1")).doubleValue()); + new BigDecimal(applyFunction.apply("abs", "num1")).doubleValue()); assertEquals( new BigDecimal("1").doubleValue(), - new BigDecimal(process.apply("abs", "num2")).doubleValue()); - assertEquals("NaN", process.apply("abs", "arr1")); + new BigDecimal(applyFunction.apply("abs", "num2")).doubleValue()); + assertEquals("NaN", applyFunction.apply("abs", "arr1")); assertEquals( new BigDecimal("1").doubleValue(), - new BigDecimal(process.apply("add", "num1")).doubleValue()); + new BigDecimal(applyFunction.apply("add", "num1")).doubleValue()); assertEquals( new BigDecimal("-1").doubleValue(), - new BigDecimal(process.apply("add", "num2")).doubleValue()); + new BigDecimal(applyFunction.apply("add", "num2")).doubleValue()); assertEquals( new BigDecimal("7").doubleValue(), - new BigDecimal(process.apply("add", "arr1")).doubleValue()); - assertEquals("NaN", process.apply("div", "num1")); - assertEquals("NaN", process.apply("div", "num2")); + new BigDecimal(applyFunction.apply("add", "arr1")).doubleValue()); + assertEquals("NaN", applyFunction.apply("div", "num1")); + assertEquals("NaN", applyFunction.apply("div", "num2")); assertEquals( new BigDecimal("0").doubleValue(), - new BigDecimal(process.apply("div", "arr1")).doubleValue()); + new BigDecimal(applyFunction.apply("div", "arr1")).doubleValue()); assertEquals( new BigDecimal("1").doubleValue(), - new BigDecimal(process.apply("divide", "num1")).doubleValue()); + new BigDecimal(applyFunction.apply("divide", "num1")).doubleValue()); assertEquals( new BigDecimal("-1").doubleValue(), - new BigDecimal(process.apply("divide", "num2")).doubleValue()); + new BigDecimal(applyFunction.apply("divide", "num2")).doubleValue()); assertEquals( new BigDecimal("0.07142857142857142").doubleValue(), - new BigDecimal(process.apply("divide", "arr1")).doubleValue()); + new BigDecimal(applyFunction.apply("divide", "arr1")).doubleValue()); assertEquals( new BigDecimal("1").doubleValue(), - new BigDecimal(process.apply("max", "num1")).doubleValue()); + new BigDecimal(applyFunction.apply("max", "num1")).doubleValue()); assertEquals( new BigDecimal("-1").doubleValue(), - new BigDecimal(process.apply("max", "num2")).doubleValue()); + new BigDecimal(applyFunction.apply("max", "num2")).doubleValue()); assertEquals( new BigDecimal("3.5").doubleValue(), - new BigDecimal(process.apply("max", "arr1")).doubleValue()); + new BigDecimal(applyFunction.apply("max", "arr1")).doubleValue()); assertEquals( new BigDecimal("1").doubleValue(), - new BigDecimal(process.apply("min", "num1")).doubleValue()); + new BigDecimal(applyFunction.apply("min", "num1")).doubleValue()); assertEquals( new BigDecimal("-1").doubleValue(), - new BigDecimal(process.apply("min", "num2")).doubleValue()); + new BigDecimal(applyFunction.apply("min", "num2")).doubleValue()); assertEquals( new BigDecimal("0.5").doubleValue(), - new BigDecimal(process.apply("min", "arr1")).doubleValue()); - assertEquals("NaN", process.apply("mod", "num1")); - assertEquals("NaN", process.apply("mod", "num2")); + new BigDecimal(applyFunction.apply("min", "arr1")).doubleValue()); + assertEquals("NaN", applyFunction.apply("mod", "num1")); + assertEquals("NaN", applyFunction.apply("mod", "num2")); assertEquals( new BigDecimal("0").doubleValue(), - new BigDecimal(process.apply("mod", "arr1")).doubleValue()); + new BigDecimal(applyFunction.apply("mod", "arr1")).doubleValue()); assertEquals( new BigDecimal("1").doubleValue(), - new BigDecimal(process.apply("multiply", "num1")).doubleValue()); + new BigDecimal(applyFunction.apply("multiply", "num1")).doubleValue()); assertEquals( new BigDecimal("-1").doubleValue(), - new BigDecimal(process.apply("multiply", "num2")).doubleValue()); + new BigDecimal(applyFunction.apply("multiply", "num2")).doubleValue()); assertEquals( new BigDecimal("3.5").doubleValue(), - new BigDecimal(process.apply("multiply", "arr1")).doubleValue()); - assertEquals("NaN", process.apply("pow", "num1")); - assertEquals("NaN", process.apply("pow", "num2")); + new BigDecimal(applyFunction.apply("multiply", "arr1")).doubleValue()); + assertEquals("NaN", applyFunction.apply("pow", "num1")); + assertEquals("NaN", applyFunction.apply("pow", "num2")); assertEquals( new BigDecimal("1").doubleValue(), - new BigDecimal(process.apply("pow", "arr1")).doubleValue()); + new BigDecimal(applyFunction.apply("pow", "arr1")).doubleValue()); assertEquals( new BigDecimal("1").doubleValue(), - new BigDecimal(process.apply("sqrt", "num1")).doubleValue()); - assertEquals("NaN", process.apply("sqrt", "num2")); - assertEquals("NaN", process.apply("sqrt", "arr1")); + new BigDecimal(applyFunction.apply("sqrt", "num1")).doubleValue()); + assertEquals("NaN", applyFunction.apply("sqrt", "num2")); + assertEquals("NaN", applyFunction.apply("sqrt", "arr1")); assertEquals( new BigDecimal("1").doubleValue(), - new BigDecimal(process.apply("subtract", "num1")).doubleValue()); + new BigDecimal(applyFunction.apply("subtract", "num1")).doubleValue()); assertEquals( new BigDecimal("-1").doubleValue(), - new BigDecimal(process.apply("subtract", "num2")).doubleValue()); + new BigDecimal(applyFunction.apply("subtract", "num2")).doubleValue()); assertEquals( new BigDecimal("0").doubleValue(), - new BigDecimal(process.apply("subtract", "arr1")).doubleValue()); + new BigDecimal(applyFunction.apply("subtract", "arr1")).doubleValue()); } @Test public void testMathsHelpersMixedArgs() { assertEquals( new BigDecimal("7").doubleValue(), - new BigDecimal(process.apply("add", "-2 2 \"[3,-3]\" num1 num2 arr1 x=5 y=-5")) + new BigDecimal(applyFunction.apply("add", "-2 2 \"[3,-3]\" num1 num2 arr1 x=5 y=-5")) .doubleValue()); } @@ -215,6 +226,64 @@ public void testMathsHelpersNested() { """; assertEquals( new BigDecimal("1").doubleValue(), - new BigDecimal(process.apply(complex, "")).doubleValue()); + new BigDecimal(applyFunction.apply(complex, "")).doubleValue()); + } + + private final Map helperContextData = new HashMap<>() { + { + put("id", new HashMap(){{ + put("firstName", "First"); + put("lastName", "Last"); + }}); + } + }; + private final Context helperContext = Context.newBuilder(helperContextData).build(); + + @Test + public void embeddedHelperTest(){ + String expected = """ + """; + try { + String templateText = templateLoader.sourceAt("embeddedHelperTest").content(StandardCharsets.UTF_8); + Template template = hb.compileInline(templateText); + assertEquals(expected, template.apply(helperContext)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + @Test + public void includeHelperTest(){ + String expected = "FirstLast"; + try { + String templateText = templateLoader.sourceAt("includeHelperTest").content(StandardCharsets.UTF_8); + Template template = hb.compileInline(templateText); + assertEquals(expected, template.apply(helperContext)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + @Test + public void jsonHelperTest(){ + String expected = "{\"id\":{\"firstName\":\"First\",\"lastName\":\"Last\"}}"; + try { + String templateText = templateLoader.sourceAt("jsonHelperTest").content(StandardCharsets.UTF_8); + Template template = hb.compileInline(templateText); + assertEquals(expected, template.apply(helperContext)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + @Test + public void partialHelperTest(){ + String expected = "FirstLast"; + try { + String templateText = templateLoader.sourceAt("partialHelperTest").content(StandardCharsets.UTF_8); + Template template = hb.compileInline(templateText); + assertEquals(expected, template.apply(helperContext)); + } catch (IOException e) { + throw new RuntimeException(e); + } } } diff --git a/src/test/resources/net/rptools/maptool/util/handlebars/embeddedHelperTest.hbs b/src/test/resources/net/rptools/maptool/util/handlebars/embeddedHelperTest.hbs new file mode 100644 index 0000000000..fa39f7af4f --- /dev/null +++ b/src/test/resources/net/rptools/maptool/util/handlebars/embeddedHelperTest.hbs @@ -0,0 +1 @@ +{{embedded "user" ["id"]}} \ No newline at end of file diff --git a/src/test/resources/net/rptools/maptool/util/handlebars/includeHelperTest.hbs b/src/test/resources/net/rptools/maptool/util/handlebars/includeHelperTest.hbs new file mode 100644 index 0000000000..bd06ce0cdb --- /dev/null +++ b/src/test/resources/net/rptools/maptool/util/handlebars/includeHelperTest.hbs @@ -0,0 +1 @@ +{{#with this.id}}{{include "user"}}{{/with}}{{log "info" "ping"}} \ No newline at end of file diff --git a/src/test/resources/net/rptools/maptool/util/handlebars/jsonHelperTest.hbs b/src/test/resources/net/rptools/maptool/util/handlebars/jsonHelperTest.hbs new file mode 100644 index 0000000000..4a798dce38 --- /dev/null +++ b/src/test/resources/net/rptools/maptool/util/handlebars/jsonHelperTest.hbs @@ -0,0 +1 @@ +{{json this}}{{log "info" "ping"}} \ No newline at end of file diff --git a/src/test/resources/net/rptools/maptool/util/handlebars/partialHelperTest.hbs b/src/test/resources/net/rptools/maptool/util/handlebars/partialHelperTest.hbs new file mode 100644 index 0000000000..6d2c9a61fe --- /dev/null +++ b/src/test/resources/net/rptools/maptool/util/handlebars/partialHelperTest.hbs @@ -0,0 +1 @@ +{{#with this.id}}{{> "user"}}{{/with}}{{log "info" "ping"}} \ No newline at end of file diff --git a/src/test/resources/net/rptools/maptool/util/handlebars/user.hbs b/src/test/resources/net/rptools/maptool/util/handlebars/user.hbs new file mode 100644 index 0000000000..70da4f61ea --- /dev/null +++ b/src/test/resources/net/rptools/maptool/util/handlebars/user.hbs @@ -0,0 +1 @@ +{{firstName}}{{lastName}} \ No newline at end of file From 90d2c91c556ee7a559486693db56cd0fbbf19a17 Mon Sep 17 00:00:00 2001 From: bubblobill Date: Sat, 21 Mar 2026 16:49:51 +0800 Subject: [PATCH 11/14] formatting --- .../maptool/util/HandlebarsHelperTest.java | 108 +++++++++--------- 1 file changed, 57 insertions(+), 51 deletions(-) diff --git a/src/test/java/net/rptools/maptool/util/HandlebarsHelperTest.java b/src/test/java/net/rptools/maptool/util/HandlebarsHelperTest.java index 5099b9a324..41b7fd4830 100644 --- a/src/test/java/net/rptools/maptool/util/HandlebarsHelperTest.java +++ b/src/test/java/net/rptools/maptool/util/HandlebarsHelperTest.java @@ -16,29 +16,22 @@ import static org.junit.jupiter.api.Assertions.assertEquals; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import com.github.jknack.handlebars.Context; import com.github.jknack.handlebars.Handlebars; import com.github.jknack.handlebars.Template; import com.github.jknack.handlebars.io.ClassPathTemplateLoader; +import com.github.jknack.handlebars.io.TemplateLoader; import java.io.IOException; import java.math.BigDecimal; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; import java.util.function.BiFunction; - -import com.github.jknack.handlebars.io.TemplateLoader; -import com.github.jknack.handlebars.io.TemplateSource; -import com.google.gson.Gson; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonPrimitive; import org.junit.jupiter.api.Test; public class HandlebarsHelperTest { - private final TemplateLoader templateLoader = new ClassPathTemplateLoader( "/net/rptools/maptool/util/handlebars/"); + private final TemplateLoader templateLoader = + new ClassPathTemplateLoader("/net/rptools/maptool/util/handlebars/"); private final Handlebars hb = HandlebarsUtil.getHandlebarsInstance(templateLoader); private final Map mathsContextData = new HashMap<>() { @@ -229,61 +222,74 @@ public void testMathsHelpersNested() { new BigDecimal(applyFunction.apply(complex, "")).doubleValue()); } - private final Map helperContextData = new HashMap<>() { - { - put("id", new HashMap(){{ - put("firstName", "First"); - put("lastName", "Last"); - }}); - } - }; + private final Map helperContextData = + new HashMap<>() { + { + put( + "id", + new HashMap() { + { + put("firstName", "First"); + put("lastName", "Last"); + } + }); + } + }; private final Context helperContext = Context.newBuilder(helperContextData).build(); @Test - public void embeddedHelperTest(){ - String expected = """ + public void embeddedHelperTest() { + String expected = + """ """; - try { - String templateText = templateLoader.sourceAt("embeddedHelperTest").content(StandardCharsets.UTF_8); - Template template = hb.compileInline(templateText); - assertEquals(expected, template.apply(helperContext)); - } catch (IOException e) { - throw new RuntimeException(e); - } + try { + String templateText = + templateLoader.sourceAt("embeddedHelperTest").content(StandardCharsets.UTF_8); + Template template = hb.compileInline(templateText); + assertEquals(expected, template.apply(helperContext)); + } catch (IOException e) { + throw new RuntimeException(e); + } } + @Test - public void includeHelperTest(){ + public void includeHelperTest() { String expected = "FirstLast"; - try { - String templateText = templateLoader.sourceAt("includeHelperTest").content(StandardCharsets.UTF_8); - Template template = hb.compileInline(templateText); - assertEquals(expected, template.apply(helperContext)); - } catch (IOException e) { - throw new RuntimeException(e); - } + try { + String templateText = + templateLoader.sourceAt("includeHelperTest").content(StandardCharsets.UTF_8); + Template template = hb.compileInline(templateText); + assertEquals(expected, template.apply(helperContext)); + } catch (IOException e) { + throw new RuntimeException(e); + } } + @Test - public void jsonHelperTest(){ + public void jsonHelperTest() { String expected = "{\"id\":{\"firstName\":\"First\",\"lastName\":\"Last\"}}"; - try { - String templateText = templateLoader.sourceAt("jsonHelperTest").content(StandardCharsets.UTF_8); - Template template = hb.compileInline(templateText); - assertEquals(expected, template.apply(helperContext)); - } catch (IOException e) { - throw new RuntimeException(e); - } + try { + String templateText = + templateLoader.sourceAt("jsonHelperTest").content(StandardCharsets.UTF_8); + Template template = hb.compileInline(templateText); + assertEquals(expected, template.apply(helperContext)); + } catch (IOException e) { + throw new RuntimeException(e); + } } + @Test - public void partialHelperTest(){ + public void partialHelperTest() { String expected = "FirstLast"; - try { - String templateText = templateLoader.sourceAt("partialHelperTest").content(StandardCharsets.UTF_8); - Template template = hb.compileInline(templateText); - assertEquals(expected, template.apply(helperContext)); - } catch (IOException e) { - throw new RuntimeException(e); - } + try { + String templateText = + templateLoader.sourceAt("partialHelperTest").content(StandardCharsets.UTF_8); + Template template = hb.compileInline(templateText); + assertEquals(expected, template.apply(helperContext)); + } catch (IOException e) { + throw new RuntimeException(e); + } } } From 00cb47a41171a07480046771cd0a68209b76ec4a Mon Sep 17 00:00:00 2001 From: bubblobill Date: Sat, 21 Mar 2026 16:57:28 +0800 Subject: [PATCH 12/14] inline partial test --- .../java/net/rptools/maptool/util/HandlebarsHelperTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/test/java/net/rptools/maptool/util/HandlebarsHelperTest.java b/src/test/java/net/rptools/maptool/util/HandlebarsHelperTest.java index 41b7fd4830..33b6464a47 100644 --- a/src/test/java/net/rptools/maptool/util/HandlebarsHelperTest.java +++ b/src/test/java/net/rptools/maptool/util/HandlebarsHelperTest.java @@ -288,6 +288,8 @@ public void partialHelperTest() { templateLoader.sourceAt("partialHelperTest").content(StandardCharsets.UTF_8); Template template = hb.compileInline(templateText); assertEquals(expected, template.apply(helperContext)); + template = hb.compileInline("{{#*inline \"myPartial\"}}success{{/inline}}{{> myPartial}}"); + assertEquals("success", template.apply(helperContext)); } catch (IOException e) { throw new RuntimeException(e); } From b19ee5613ba0970873ffc4e849f41e1e7d8e6851 Mon Sep 17 00:00:00 2001 From: bubblobill Date: Tue, 24 Mar 2026 11:35:03 +0800 Subject: [PATCH 13/14] Hyphenate stat-sheet. Change portrait on mouseover to portrait on stat-sheet. --- .../rptools/maptool/language/i18n.properties | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/main/resources/net/rptools/maptool/language/i18n.properties b/src/main/resources/net/rptools/maptool/language/i18n.properties index eee02f0da2..06fe33bac5 100644 --- a/src/main/resources/net/rptools/maptool/language/i18n.properties +++ b/src/main/resources/net/rptools/maptool/language/i18n.properties @@ -463,8 +463,8 @@ EditTokenDialog.label.opacity.tooltip = Change the opacity of the token. EditTokenDialog.label.opacity.100 = 100% EditTokenDialog.label.terrain.ignore = Ignore Terrain EditTokenDialog.label.terrain.ignore.tooltip= Select any terrain modifier types this token should ignore. -EditTokenDialog.label.statSheet = Stat Sheet -EditTokenDialog.label.defaultStatSheet = Default Stat Sheet +EditTokenDialog.label.statSheet = Stat-Sheet +EditTokenDialog.label.defaultStatSheet = Default Stat-Sheet EditTokenDialog.combo.terrain.mod = Set whether the cell cost should be added or multiplied. Use negative numbers to subtract and decimal values to divide costs. EditTokenDialog.border.title.libToken = Lib:Token properties EditTokenDialog.border.title.layout = Layout @@ -780,12 +780,12 @@ Preferences.combo.tokens.naming.filename = Use Filename Preferences.combo.tokens.naming.creature = Use "{0}" Preferences.label.tokens.dialog = Show Dialog on New Token Preferences.label.tokens.dialog.tooltip = Determines whether the New Token dialog appears when a token is dropped onto the map. -Preferences.label.tokens.statsheet = Statsheet Portrait Size -Preferences.label.tokens.statsheet.tooltip = Size of the image that appears next to the statsheet on mouseover. Set to zero to disable portraits. -Preferences.label.tokens.portrait.mouse = Show Portrait on mouseover +Preferences.label.tokens.statsheet = Stat-Sheet Portrait Size +Preferences.label.tokens.statsheet.tooltip = Size of the image to display on the pop-up stat-sheet. Set to zero to disable portraits. +Preferences.label.tokens.portrait.mouse = Show Portrait on Stat-Sheet Preferences.label.tokens.portrait.mouse.tooltip = Whether to show the portrait when the mouse hovers over a Token. -Preferences.label.tokens.statsheet.mouse = Show statsheet on mouseover -Preferences.label.tokens.statsheet.mouse.tooltip = Whether to show the statsheet when the mouse hovers over a Token. +Preferences.label.tokens.statsheet.mouse = Show stat-sheet on mouseover +Preferences.label.tokens.statsheet.mouse.tooltip = Whether to show the stat-sheet when the mouse hovers over a Token. Preferences.label.tokens.statsheet.shift = Stat sheet requires Shift key Preferences.label.tokens.statsheet.shift.tooltip = The stat sheet will only show when the mouse hovers over a Token if the Shift key is also held down. Otherwise, Shift key will hide stat sheet on mouse hovers. Preferences.label.tokens.arrow.background = Token Facing Arrow color @@ -2395,7 +2395,7 @@ msg.error.fileNotFound = File Not found. msg.error.fogexpose = Must be a GM to change the fog of war. msg.error.gmRequired = Only GMs can do that. msg.error.invalidLocalhost = "localhost" is not a valid address?! Check your /etc/hosts file. -msg.error.renderingStatSheet = Error while trying to render stat sheet. +msg.error.renderingStatSheet = Error while trying to render stat-sheet. msg.error.encryptionSetup = Unable to initialize encryption library. # JVM Related msg.error.jvm.options = "{0}" is an invalid memory setting.

Be sure to enter a valid Java JVM value that includes a memory size that ends in K, M, or G.
e.g. 4M, 6G, 2048M, 1024K, etc.

Warning: An improper setting could prevent MapTool from starting. @@ -2731,8 +2731,8 @@ dialog.NewToken.type = Type: dialog.NewToken.visible = Visible: dialog.NewToken.show = Show this dialog dialog.NewToken.tokenPropertyType = Token Property -dialog.NewToken.statSheet = Stat Sheet -dialog.NewToken.statSheetLocation = Stat Sheet Location +dialog.NewToken.statSheet = Stat-Sheet +dialog.NewToken.statSheetLocation = Stat-Sheet Location prefs.jvm.advanced.enableAssertions.tooltip = Enables Java language assertions in the MapTool code. @@ -3214,8 +3214,8 @@ Label.label=Label: # StatSheet token.statSheet.legacyStatSheetDescription = Legacy (pre 1.14) Stat Sheet -token.statSheet.noStatSheetDescription = No Stat Sheet -token.statSheet.useDefault = Default Stat Sheet for Property Type +token.statSheet.noStatSheetDescription = No Stat-Sheet +token.statSheet.useDefault = Default Stat-Sheet for Property Type # Advanced Dice Rolls advanced.roll.parserError = Dice Roll String Error line {0} column {1} "{2}". From dedb69ebd4b01487df9da7389fb738d516910978 Mon Sep 17 00:00:00 2001 From: bubblobill Date: Fri, 3 Apr 2026 12:28:24 +0800 Subject: [PATCH 14/14] Changed logger class. Added Javadoc to getHandlebarsInstance --- .../rptools/maptool/util/HandlebarsUtil.java | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/main/java/net/rptools/maptool/util/HandlebarsUtil.java b/src/main/java/net/rptools/maptool/util/HandlebarsUtil.java index feefb2d1ab..3a24d4d2da 100644 --- a/src/main/java/net/rptools/maptool/util/HandlebarsUtil.java +++ b/src/main/java/net/rptools/maptool/util/HandlebarsUtil.java @@ -32,7 +32,6 @@ import java.util.concurrent.*; import javax.annotation.Nonnull; import javax.annotation.Nullable; -import net.rptools.maptool.model.Token; import net.rptools.maptool.model.library.Library; import net.rptools.maptool.model.library.LibraryManager; import org.apache.logging.log4j.LogManager; @@ -47,14 +46,28 @@ public class HandlebarsUtil { private static final HighConcurrencyTemplateCache HIGH_CONCURRENCY_TEMPLATE_CACHE = new HighConcurrencyTemplateCache(); + /** + * Use this to obtain an instance of Handlebars instead of creating one separately. + * + *

Specify a TemplateLoader if the default ClassPathTemplateLoader is not required. + * + *

Using this ensures the instance returned has a consistent setup with all helpers registered. + * + * @param loader The TemplateLoader to use. If null, handlebars defaults to + * ClassPathTemplateLoader. + * @return A HandleBars instance with appropriate settings and registered helpers + */ static Handlebars getHandlebarsInstance(@Nullable TemplateLoader loader) { - return HandlebarsHelpers.registerHelpers( + Handlebars handlebars = new Handlebars() - .with(loader) .with(HIGH_CONCURRENCY_TEMPLATE_CACHE) .preEvaluatePartialBlocks(false) .parentScopeResolution(false) - .setCharset(StandardCharsets.UTF_8)); + .setCharset(StandardCharsets.UTF_8); + if (loader != null) { + handlebars.with(loader); + } + return HandlebarsHelpers.registerHelpers(handlebars); } public static boolean isAssetFileHandlebars(String filename) { @@ -68,7 +81,7 @@ public static boolean isAssetFileHandlebars(String filename) { private final Template template; /** Logging class instance. */ - private static final Logger log = LogManager.getLogger(Token.class); + private static final Logger log = LogManager.getLogger(HandlebarsUtil.class); /** Handlebars partial template source that uses Add-On files */ private static record LibraryTemplateSource(@Nonnull Library library, @Nonnull String filename) @@ -102,7 +115,6 @@ private static class LibraryTemplateLoader extends AbstractTemplateLoader { @Nonnull final Library library; private LibraryTemplateLoader(@Nonnull String current, @Nonnull Library library) { - current = current.replace('\\', '/'); if (!current.startsWith("/")) { current = "/" + current; } @@ -116,7 +128,7 @@ private LibraryTemplateLoader(@Nonnull String current, @Nonnull Library library) @Override @Nonnull public String resolve(@Nonnull final String path) { - var location = current.resolveSibling(path).normalize().toString().replace('\\', '/'); + var location = current.resolveSibling(path).normalize().toString(); if (location.startsWith("/")) { location = location.substring(1); }