diff --git a/data/circular/circular-1.mjml b/data/circular/circular-1.mjml new file mode 100644 index 0000000..baeaa44 --- /dev/null +++ b/data/circular/circular-1.mjml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/data/circular/circular-2.mjml b/data/circular/circular-2.mjml new file mode 100644 index 0000000..85e1930 --- /dev/null +++ b/data/circular/circular-2.mjml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/data/circular/include-circular.mjml b/data/circular/include-circular.mjml new file mode 100644 index 0000000..059629a --- /dev/null +++ b/data/circular/include-circular.mjml @@ -0,0 +1,14 @@ + + + + + + +

This is the Index!

+
+
+
+ + +
+
\ No newline at end of file diff --git a/data/import-head.mjml b/data/import-head.mjml new file mode 100644 index 0000000..ab9bc74 --- /dev/null +++ b/data/import-head.mjml @@ -0,0 +1,8 @@ + + + + + + test + + \ No newline at end of file diff --git a/data/include-about.mjml b/data/include-about.mjml new file mode 100644 index 0000000..e7f2368 --- /dev/null +++ b/data/include-about.mjml @@ -0,0 +1,16 @@ + + + + + + + + +

This is the About!

+
+
+
+ + +
+
\ No newline at end of file diff --git a/data/include-index.mjml b/data/include-index.mjml new file mode 100644 index 0000000..6625c3e --- /dev/null +++ b/data/include-index.mjml @@ -0,0 +1,17 @@ + + + + + + + + +

This is the Index!

+
+
+
+ + + +
+
\ No newline at end of file diff --git a/data/include-type-css.mjml b/data/include-type-css.mjml new file mode 100644 index 0000000..7cb9165 --- /dev/null +++ b/data/include-type-css.mjml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/data/include-type-html.mjml b/data/include-type-html.mjml new file mode 100644 index 0000000..decb706 --- /dev/null +++ b/data/include-type-html.mjml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/data/include/footer.mjml b/data/include/footer.mjml new file mode 100644 index 0000000..109bc93 --- /dev/null +++ b/data/include/footer.mjml @@ -0,0 +1,22 @@ + + + + + +

Cheers,

+

Me!

+
+
+
+ + + + + + + + +

Something.com ©️ 2023

+
+
+
\ No newline at end of file diff --git a/data/include/header.mjml b/data/include/header.mjml new file mode 100644 index 0000000..af20ef1 --- /dev/null +++ b/data/include/header.mjml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/data/include/import-head-styles.mjml b/data/include/import-head-styles.mjml new file mode 100644 index 0000000..fdf547b --- /dev/null +++ b/data/include/import-head-styles.mjml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/data/include/styling.mjml b/data/include/styling.mjml new file mode 100644 index 0000000..f58a916 --- /dev/null +++ b/data/include/styling.mjml @@ -0,0 +1,24 @@ + + + + + .bg-dark { + background: #2F5854 !important; + } + .bg-white { + background: #FFFFFF !important; + } + p { + font-size: 16px !important; + font-weight: 400 !important; + line-height: 24px !important; + margin: 0; + } + + + + + + + + \ No newline at end of file diff --git a/data/include/type.css b/data/include/type.css new file mode 100644 index 0000000..e9a76a1 --- /dev/null +++ b/data/include/type.css @@ -0,0 +1,7 @@ +h1 { + color: red; +} + +.my-custom-class { + border: 1px solid red; +} \ No newline at end of file diff --git a/data/include/type.html b/data/include/type.html new file mode 100644 index 0000000..381574b --- /dev/null +++ b/data/include/type.html @@ -0,0 +1,10 @@ +
hello world!
+
+ + +
ab
+ + \ No newline at end of file diff --git a/data/include/type2.css b/data/include/type2.css new file mode 100644 index 0000000..9d5a18a --- /dev/null +++ b/data/include/type2.css @@ -0,0 +1,7 @@ +h2 { + color: green; +} + +.my-custom-class-2 { + border: 1px solid green; +} \ No newline at end of file diff --git a/data/mj-divider-mj-all.mjml b/data/mj-divider-mj-all.mjml new file mode 100644 index 0000000..92d31e2 --- /dev/null +++ b/data/mj-divider-mj-all.mjml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index 547123c..469ca7d 100644 --- a/pom.xml +++ b/pom.xml @@ -52,25 +52,19 @@ org.junit.jupiter junit-jupiter-engine - 5.10.2 - test - - - commons-io - commons-io - 2.15.1 + 5.11.2 test org.graalvm.polyglot polyglot - 23.1.2 + 24.1.0 test org.graalvm.polyglot js-community - 23.1.2 + 24.1.0 pom test diff --git a/src/main/java/ch/digitalfondue/mjml4j/BaseComponent.java b/src/main/java/ch/digitalfondue/mjml4j/BaseComponent.java index 80227ab..0570bee 100644 --- a/src/main/java/ch/digitalfondue/mjml4j/BaseComponent.java +++ b/src/main/java/ch/digitalfondue/mjml4j/BaseComponent.java @@ -10,8 +10,8 @@ abstract class BaseComponent { private final Element element; - private final BaseComponent parent; private final List children = new ArrayList<>(); + private BaseComponent parent; final GlobalContext context; @@ -31,6 +31,10 @@ final void doSetupPostConstruction() { } } + public void setParent(BaseComponent parent) { + this.parent = parent; + } + void setupPostConstruction() { } @@ -39,9 +43,10 @@ LinkedHashMap allowedAttributes() { } private LinkedHashMap defaultAttributeValues() { - var kvt = allowedAttributes(); var res = new LinkedHashMap(); - kvt.forEach((k, vt) -> res.put(k, vt.value())); + for (var kvt : allowedAttributes().entrySet()) { + res.put(kvt.getKey(), kvt.getValue().value()); + } return res; } @@ -65,8 +70,9 @@ final void setupComponentAttributes() { void setAttributes() { var attributesLength = element.getAttributes().getLength(); + var elemAttr = element.getAttributes(); for (int i = 0; i < attributesLength; i++) { - var attr = element.getAttributes().item(i); + var attr = elemAttr.item(i); var userAttributeName = attr.getNodeName().toLowerCase(Locale.ROOT); var userAttributeValue = attr.getNodeValue(); if (!attributes.containsKey(userAttributeName)) { @@ -252,14 +258,14 @@ CssBoxModel getBoxModel() { parent.getContainerInnerWidth(), borders, paddings, - containerWidth.value + containerWidth.value() ); } return new CssBoxModel( - containerWidth.value, + containerWidth.value(), borders, paddings, - containerWidth.value); + containerWidth.value()); } @@ -270,7 +276,7 @@ CssBoxModel getBoxModel() { var mjAttribute = getAttribute("border"); if (!Utils.isNullOrWhiteSpace(mjAttributeDirection)) - return CssUnitParser.parse(mjAttributeDirection).value; + return CssUnitParser.parse(mjAttributeDirection).value(); if (Utils.isNullOrWhiteSpace(mjAttribute)) return 0; @@ -300,7 +306,7 @@ CssBoxModel getBoxModel() { var mjAttribute = getAttribute(attribute); if (!Utils.isNullOrWhiteSpace(mjAttributeDirection)) - return CssUnitParser.parse(mjAttributeDirection).value; + return CssUnitParser.parse(mjAttributeDirection).value(); if (Utils.isNullOrWhiteSpace(mjAttribute)) return 0; @@ -337,10 +343,10 @@ CssBoxModel getBoxModel() { break; case 1: default: - return CssUnitParser.parse(mjAttribute).value; + return CssUnitParser.parse(mjAttribute).value(); } - return CssUnitParser.parse(splittedCssValue[directions.get(direction)]).value; + return CssUnitParser.parse(splittedCssValue[directions.get(direction)]).value(); } StringBuilder htmlAttributes(LinkedHashMap htmlAttributes) { diff --git a/src/main/java/ch/digitalfondue/mjml4j/CssUnitParser.java b/src/main/java/ch/digitalfondue/mjml4j/CssUnitParser.java index c410726..759a662 100644 --- a/src/main/java/ch/digitalfondue/mjml4j/CssUnitParser.java +++ b/src/main/java/ch/digitalfondue/mjml4j/CssUnitParser.java @@ -5,23 +5,15 @@ class CssUnitParser { - static class CssParsedUnit { - final String unit; - final double value; - - final double valueFullPrecision; - - CssParsedUnit(String unit, double value, double valueFullPrecision) { - this.unit = unit; - this.value = value; - this.valueFullPrecision = valueFullPrecision; - } + record CssParsedUnit(String unit, double value, double valueFullPrecision) { boolean isPercent() { return "%".equals(unit); } - boolean isPx() { return "px".equals(unit); } + boolean isPx() { + return "px".equals(unit); + } @Override public String toString() { @@ -48,8 +40,9 @@ static CssParsedUnit parse(String cssValue) { var match = UNIT_PATTERN.matcher(cssValue); - if (!match.find()) + if (!match.find()) { throw new IllegalStateException("CssWidthParser could not parse " + cssValue + " due to invalid format"); + } var widthValue = match.group(1); var widthUnit = match.groupCount() != 2 ? "px" : match.group(2); diff --git a/src/main/java/ch/digitalfondue/mjml4j/DOMSerializer.java b/src/main/java/ch/digitalfondue/mjml4j/DOMSerializer.java index 6b24916..1262460 100644 --- a/src/main/java/ch/digitalfondue/mjml4j/DOMSerializer.java +++ b/src/main/java/ch/digitalfondue/mjml4j/DOMSerializer.java @@ -65,29 +65,11 @@ private static boolean skipEndTag(Element e) { } private static boolean isNoEndTag(String nodeName) { - switch (nodeName) { - case "area": - case "base": - case "basefont": - case "bgsound": - case "br": - case "col": - case "embed": - case "frame": - case "hr": - case "img": - case "input": - case "keygen": - case "link": - case "meta": - case "param": - case "source": - case "track": - case "wbr": - return true; - default: - return false; - } + return switch (nodeName) { + case "area", "base", "basefont", "bgsound", "br", "col", "embed", "frame", "hr", "img", "input", "keygen", + "link", "meta", "param", "source", "track", "wbr" -> true; + default -> false; + }; } private static String escapeAttributeValue(Attr attribute) { @@ -156,17 +138,9 @@ private static boolean isHtmlNS(Element element, String name) { } private static boolean isTextNodeParent(String nodeName) { - switch (nodeName) { - case "style": - case "script": - case "xmp": - case "iframe": - case "noembed": - case "noframes": - case "plaintext": - return true; - default: - return false; - } + return switch (nodeName) { + case "style", "script", "xmp", "iframe", "noembed", "noframes", "plaintext" -> true; + default -> false; + }; } } diff --git a/src/main/java/ch/digitalfondue/mjml4j/GlobalContext.java b/src/main/java/ch/digitalfondue/mjml4j/GlobalContext.java index 1f66371..579903c 100644 --- a/src/main/java/ch/digitalfondue/mjml4j/GlobalContext.java +++ b/src/main/java/ch/digitalfondue/mjml4j/GlobalContext.java @@ -2,6 +2,7 @@ import org.w3c.dom.Document; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; @@ -16,6 +17,11 @@ class GlobalContext { String backgroundColor = ""; final String language; final String dir; + // + final Mjml4j.IncludeResolver includeResolver; + final ArrayDeque currentResourcePaths = new ArrayDeque<>(); + final ArrayDeque rootComponents = new ArrayDeque<>(); + // final LinkedHashMap fonts = Utils.mapOf( "Open Sans", "https://fonts.googleapis.com/css?family=Open+Sans:300,400,500,700", @@ -39,6 +45,10 @@ class GlobalContext { this.document = document; this.dir = configuration.dir().value(); this.language = configuration.language(); + + // + this.includeResolver = configuration.includeResolver(); + // } void addFont(String name, String href) { diff --git a/src/main/java/ch/digitalfondue/mjml4j/Mjml4j.java b/src/main/java/ch/digitalfondue/mjml4j/Mjml4j.java index 9b8ec9d..1d846a2 100644 --- a/src/main/java/ch/digitalfondue/mjml4j/Mjml4j.java +++ b/src/main/java/ch/digitalfondue/mjml4j/Mjml4j.java @@ -9,9 +9,11 @@ import org.w3c.dom.Node; import org.w3c.dom.Text; -import java.util.ArrayList; -import java.util.EnumSet; -import java.util.Locale; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -19,10 +21,10 @@ /** * mjml java implementation. - * + * *

You can use the default methods {@link Mjml4j#render(String)} or for a little bit more control {@link #render(String, Configuration)} if you * specify the language (recommended).

- * + *

* See the record {@link Configuration}. */ public final class Mjml4j { @@ -34,7 +36,8 @@ private Mjml4j() { public enum TextDirection { LTR("ltr"), RTL("rtl"), AUTO("auto"); - private String value; + private final String value; + TextDirection(String value) { this.value = value; } @@ -44,7 +47,113 @@ public String value() { } } - public record Configuration(String language, TextDirection dir) { + /** + * Resolver used for mj-include. By default, no resource resolver is defined. + * See also {@link FileSystemResolver}. + */ + interface IncludeResolver { + /** + * Read the content of the resource at a given resolved path. + * + * @param resolvedResourcePath + * @return + * @throws IOException + */ + String readResource(String resolvedResourcePath) throws IOException; + + /** + * Resolve the given path. + * + * @param name + * @param parent + * @return + */ + String resolvePath(String name, String parent); + } + + @FunctionalInterface + public interface ResourceLoader { + /** + * + * @param id resource id + * @return loaded resource + * @throws IOException if the resource does not exist or a problem during loading happened + */ + String load(String id) throws IOException; + } + + /** + * Simple resource resolver, does not try to do any fancy relative / absolute handling. + * The name of the resource is the identifier used by {@link #readResource(String)}. + * The user must provide a resource handler. + */ + public static class SimpleResourceResolver implements IncludeResolver { + + private final ResourceLoader resourceHandler; + + public SimpleResourceResolver(ResourceLoader resourceHandler) { + this.resourceHandler = resourceHandler; + } + + @Override + public String readResource(String resolvedResourcePath) throws IOException { + return resourceHandler.load(resolvedResourcePath); + } + + @Override + public String resolvePath(String name, String parent) { + return name; + } + } + + /** + * Filesystem based resolver. The content _must_ be within the provided basePath. + *

+ * The basePath will also be considered the root for any absolute paths. + *

+ * The check can be customized, see {@link #checkAccess(Path, Path)}. + */ + public static class FileSystemResolver implements IncludeResolver { + + private final Path basePath; + + public FileSystemResolver(Path basePath) { + this.basePath = Objects.requireNonNull(basePath).toAbsolutePath(); + } + + @Override + public String readResource(String resolvedResourcePath) throws IOException { + return Files.readString(Path.of(resolvedResourcePath), StandardCharsets.UTF_8); + } + + public void checkAccess(Path basePath, Path resolvedPath) { + if (!resolvedPath.startsWith(basePath)) { + throw new IllegalStateException("Cannot access path outside of basePath"); + } + } + + + @Override + public String resolvePath(String name, String parent) { + var providedPath = Path.of(name); + if (providedPath.isAbsolute()) { // resolve the absolute path over the basePath + providedPath = basePath.resolve(providedPath.getRoot().relativize(providedPath)); + } + + var resolvedPath = (parent == null ? basePath.resolve(providedPath) : Path.of(parent).getParent().resolve(providedPath)).normalize().toAbsolutePath(); + checkAccess(basePath, resolvedPath); + return resolvedPath.toString(); + } + } + + public record Configuration( + String language, TextDirection dir, + IncludeResolver includeResolver + ) { + public Configuration(String language, TextDirection dir) { + this(language, dir, null); + } + public Configuration(String language) { this(language, TextDirection.AUTO); } @@ -52,6 +161,23 @@ public Configuration(String language) { private static final Configuration DEFAULT_CONFIG = new Configuration("und", TextDirection.AUTO); + + private static List parseHtmlFragment(String template) { + return JFiveParse.parseFragment(template, EnumSet.of(Option.DISABLE_IGNORE_TOKEN_IN_BODY_START_TAG, Option.INTERPRET_SELF_CLOSING_ANYTHING_ELSE, Option.DONT_TRANSFORM_ENTITIES)); + } + + private static org.w3c.dom.Document parseMjmlFragment(String template) { + // + var wrappedFragment = !template.contains("") ? "" + template + "" : template; + // + var nodes = parseHtmlFragment(wrappedFragment); + var doc = new Document(); + for (var n : nodes) { + doc.appendChild(n); + } + return W3CDom.toW3CDocument(doc); + } + /** * Render the given template with the provided configuration. * @@ -60,9 +186,7 @@ public Configuration(String language) { * @return */ public static String render(String template, Configuration configuration) { - var nodes = JFiveParse.parseFragment(template, EnumSet.of(Option.DISABLE_IGNORE_TOKEN_IN_BODY_START_TAG, Option.INTERPRET_SELF_CLOSING_ANYTHING_ELSE, Option.DONT_TRANSFORM_ENTITIES)); - - var rootElemMaybe = nodes.stream().filter(node -> "mjml".equals(node.getNodeName())).findFirst(); + var rootElemMaybe = parseHtmlFragment(template).stream().filter(node -> "mjml".equals(node.getNodeName())).findFirst(); if (rootElemMaybe.isEmpty()) { throw new IllegalStateException("no root element mjml found"); } @@ -84,22 +208,23 @@ public static String render(String template) { return render(template, DEFAULT_CONFIG); } - private static StringBuilder renderHead(MjmlComponent.MjmlRootComponent rootComponent, HtmlRenderer renderer) { + private static Optional findFirstComponent(MjmlComponent.MjmlRootComponent rootComponent, String name) { return rootComponent .getChildren() .stream() - .filter(c -> c.getTagName().equals("mj-head")) - .findFirst().map(component -> component.renderMjml(renderer)).orElseGet(StringBuilder::new); + .filter(c -> name.equals(c.getTagName())) + .findFirst(); + } + + private static StringBuilder renderHead(MjmlComponent.MjmlRootComponent rootComponent, HtmlRenderer renderer) { + return findFirstComponent(rootComponent, "mj-head").map(component -> component.renderMjml(renderer)).orElseGet(StringBuilder::new); } private static String renderBody(MjmlComponent.MjmlRootComponent rootComponent, HtmlRenderer renderer) { renderer.increaseDepth(); - return rootComponent - .getChildren() - .stream() - .filter(c -> c.getTagName().equals("mj-body")) - .findFirst().map(component -> component.renderMjml(renderer).toString()) + return findFirstComponent(rootComponent, "mj-body") + .map(component -> component.renderMjml(renderer).toString()) .orElse(""); } @@ -302,8 +427,15 @@ private static void buildFontsTags(String content, GlobalContext context, String private static MjmlComponent.MjmlRootComponent buildMjmlDocument(org.w3c.dom.Document document, GlobalContext context) { var root = (Element) document.getElementsByTagName("mjml").item(0); var rootComponent = new MjmlComponent.MjmlRootComponent(root, null, context); - traverseTree(rootComponent.getElement(), rootComponent, document, context); - rootComponent.doSetupPostConstruction(); + context.rootComponents.push(rootComponent); + try { + traverseTree(rootComponent.getElement(), rootComponent, document, context); + } finally { + context.rootComponents.pop(); + } + if (context.rootComponents.isEmpty()) { + rootComponent.doSetupPostConstruction(); + } return rootComponent; } @@ -372,6 +504,7 @@ private static BaseComponent createMjmlComponent(Element element, BaseComponent case "mj-divider" -> new MjmlComponentDivider(element, parent, context); case "mj-raw" -> new MjmlComponentRaw(element, parent, context); case "mj-image" -> new MjmlComponentImage(element, parent, context); + case "mj-include" -> handleInclude(element, parent, context); case "mj-button" -> new MjmlComponentButton(element, parent, context); case "mj-hero" -> new MjmlComponentHero(element, parent, context); case "mj-social" -> new MjmlComponentSocial(element, parent, context); @@ -390,4 +523,61 @@ private static BaseComponent createMjmlComponent(Element element, BaseComponent default -> new HtmlComponent.HtmlRawComponent(element, parent, context); }; } + + private static BaseComponent handleInclude(Element element, BaseComponent parent, GlobalContext context) { + var path = element.getAttribute("path"); + if (context.includeResolver == null || path == null || path.isEmpty()) { + return new HtmlComponent.HtmlRawComponent(element, parent, context); + } + + var includeResolver = context.includeResolver; + var resolvedPath = ""; + var resource = ""; + try { + resolvedPath = includeResolver.resolvePath(path, context.currentResourcePaths.peek()); + resource = includeResolver.readResource(resolvedPath); + if (context.currentResourcePaths.contains(resolvedPath)) { + throw new IllegalStateException("Circular inclusion detected on file : " + resolvedPath); + } + } catch (IOException e) { + resource = ""; + return new MjmlComponentRaw(element, parent, context, resource); + } + + var attributeType = element.getAttribute("type"); + if ("html".equals(attributeType)) { + return new MjmlComponentRaw(element, parent, context, resource); + } else if ("css".equals(attributeType)) { + context.addStyle(resource, "inline".equals(element.getAttribute("css-inline"))); + return new MjmlComponentRaw(element, parent, context, ""); // dummy empty component + } else { + context.currentResourcePaths.push(resolvedPath); + try { + var doc = parseMjmlFragment(resource); + var includedDoc = buildMjmlDocument(doc, context); + findFirstComponent(includedDoc, "mj-head").ifPresent(head -> { + var targetRoot = Objects.requireNonNull(context.rootComponents.peekLast()); + var targetHead = findFirstComponent(targetRoot, "mj-head").orElseGet(() -> { + var headToAppend = new MjmlComponentHead(context.document.createElement("mj-head"), targetRoot, context); + targetRoot.getChildren().add(0, headToAppend); + return headToAppend; + }); + bindToParent(targetHead, head.getChildren()); + }); + findFirstComponent(includedDoc, "mj-body").ifPresent(body -> { + bindToParent(parent, body.getChildren()); + }); + return new MjmlComponentRaw(element, parent, context, ""); // dummy empty component + } finally { + context.currentResourcePaths.pop(); + } + } + } + + private static void bindToParent(BaseComponent parent, List children) { + for (var c : children) { + c.setParent(parent); + parent.getChildren().add(c); + } + } } diff --git a/src/main/java/ch/digitalfondue/mjml4j/MjmlComponentBody.java b/src/main/java/ch/digitalfondue/mjml4j/MjmlComponentBody.java index 4455f50..b4a9b90 100644 --- a/src/main/java/ch/digitalfondue/mjml4j/MjmlComponentBody.java +++ b/src/main/java/ch/digitalfondue/mjml4j/MjmlComponentBody.java @@ -21,11 +21,6 @@ class MjmlComponentBody extends BaseComponent.BodyComponent { } } - @Override - void setupPostConstruction() { - super.setupPostConstruction(); - } - private static final LinkedHashMap ALLOWED_DEFAULT_ATTRIBUTES = mapOf( "width", of("600px", AttributeType.PIXELS), "background-color", of(null, AttributeType.COLOR) diff --git a/src/main/java/ch/digitalfondue/mjml4j/MjmlComponentButton.java b/src/main/java/ch/digitalfondue/mjml4j/MjmlComponentButton.java index a2e1e78..8631437 100644 --- a/src/main/java/ch/digitalfondue/mjml4j/MjmlComponentButton.java +++ b/src/main/java/ch/digitalfondue/mjml4j/MjmlComponentButton.java @@ -114,7 +114,7 @@ private String calculateAWidth(String content) { var innerPaddings = getShorthandAttributeValue("inner-padding", "left") + getShorthandAttributeValue("inner-padding", "right"); - var calculatedWidth = parsedWidth.value - innerPaddings - borders; + var calculatedWidth = parsedWidth.value() - innerPaddings - borders; return doubleToString(calculatedWidth) + "px"; } diff --git a/src/main/java/ch/digitalfondue/mjml4j/MjmlComponentCarousel.java b/src/main/java/ch/digitalfondue/mjml4j/MjmlComponentCarousel.java index 448b764..3261c27 100644 --- a/src/main/java/ch/digitalfondue/mjml4j/MjmlComponentCarousel.java +++ b/src/main/java/ch/digitalfondue/mjml4j/MjmlComponentCarousel.java @@ -275,7 +275,9 @@ void setupStyles(CssStyleLibraries cssStyleLibraries) { private StringBuilder generateRadios(HtmlRenderer renderer) { var res = new StringBuilder(); - carouselImages.forEach(c -> res.append(c.renderRadio(renderer))); + for (var ci : carouselImages) { + res.append(ci.renderRadio(renderer)); + } return res; } @@ -284,7 +286,9 @@ private StringBuilder generateThumbnails(HtmlRenderer renderer) { if (!"visible".equals(getAttribute("thumbnails"))) { return res; } - carouselImages.forEach(c -> res.append(c.renderThumbnail(renderer))); + for (var c : carouselImages) { + res.append(c.renderThumbnail(renderer)); + } return res; } @@ -298,7 +302,7 @@ private String getThumbnailsWidth() { return tbWidth; } var containerWidth = CssUnitParser.parse(doubleToString(getContainerOuterWidth())); - return doubleToString(Math.min(containerWidth.value, 110)); + return doubleToString(Math.min(containerWidth.value(), 110)); } @Override @@ -384,9 +388,8 @@ private StringBuilder generateControls(String direction, String icon, HtmlRender } private StringBuilder renderFallback(HtmlRenderer renderer) { - var res = new StringBuilder(); if (carouselImagesCount == 0) { - return res; + return new StringBuilder(0); } return new StringBuilder(msoConditionalTag(carouselImages.get(0).renderMjml(renderer).toString())); } diff --git a/src/main/java/ch/digitalfondue/mjml4j/MjmlComponentCarouselImage.java b/src/main/java/ch/digitalfondue/mjml4j/MjmlComponentCarouselImage.java index 54d8786..cb451e6 100644 --- a/src/main/java/ch/digitalfondue/mjml4j/MjmlComponentCarouselImage.java +++ b/src/main/java/ch/digitalfondue/mjml4j/MjmlComponentCarouselImage.java @@ -98,7 +98,7 @@ StringBuilder renderThumbnail(HtmlRenderer renderer) { "style", "thumbnails_img", "src", src, "alt", getAttribute("alt"), - "width", doubleToString(CssUnitParser.parse(getAttribute("tb-width")).value) + "width", doubleToString(CssUnitParser.parse(getAttribute("tb-width")).value()) )), res); renderer.closeTag("label", res); diff --git a/src/main/java/ch/digitalfondue/mjml4j/MjmlComponentColumn.java b/src/main/java/ch/digitalfondue/mjml4j/MjmlComponentColumn.java index 3497437..e6b2cdf 100644 --- a/src/main/java/ch/digitalfondue/mjml4j/MjmlComponentColumn.java +++ b/src/main/java/ch/digitalfondue/mjml4j/MjmlComponentColumn.java @@ -56,26 +56,26 @@ CssBoxModel getBoxModel() { var parsedWidth = CssUnitParser.parse(containerWidth); if (parsedWidth.isPercent()) { - parsedWidth = parsedWidth.withValue((sectionWidth * parsedWidth.value / 100)); + parsedWidth = parsedWidth.withValue((sectionWidth * parsedWidth.value() / 100)); } - containerWidth = doubleToString(parsedWidth.valueFullPrecision) + "px"; - childContainerWidth = doubleToString(parsedWidth.value - allPaddings) + "px"; + containerWidth = doubleToString(parsedWidth.valueFullPrecision()) + "px"; + childContainerWidth = doubleToString(parsedWidth.value() - allPaddings) + "px"; var columnWidth = CssUnitParser.parse(childContainerWidth); return new CssBoxModel( - parsedWidth.value, + parsedWidth.value(), borders, paddings, - columnWidth.value + columnWidth.value() ); } var parsedContainerWidth = CssUnitParser.parse(context.containerWidth); return new CssBoxModel( - parsedContainerWidth.value, + parsedContainerWidth.value(), borders, paddings, - parsedContainerWidth.value); + parsedContainerWidth.value()); } private CssUnitParser.CssParsedUnit getParsedWidth() { @@ -105,7 +105,7 @@ private String getMobileWidth() { if (parsedWidth.isPercent()) { return width; } else { - return doubleToString(parsedWidth.value / CssUnitParser.parse(containerWidth).value) + "%"; + return doubleToString(parsedWidth.value() / CssUnitParser.parse(containerWidth).value()) + "%"; } } @@ -122,7 +122,7 @@ String getWidthAsPixel() { private String getColumnClass() { var parsedWidth = getParsedWidth(); - var formattedClassNb = doubleToString(parsedWidth.value).replace('.', '-'); + var formattedClassNb = doubleToString(parsedWidth.value()).replace('.', '-'); var className = "mj-column-px-" + formattedClassNb; diff --git a/src/main/java/ch/digitalfondue/mjml4j/MjmlComponentDivider.java b/src/main/java/ch/digitalfondue/mjml4j/MjmlComponentDivider.java index 2f918d8..c4fb392 100644 --- a/src/main/java/ch/digitalfondue/mjml4j/MjmlComponentDivider.java +++ b/src/main/java/ch/digitalfondue/mjml4j/MjmlComponentDivider.java @@ -44,26 +44,33 @@ private String getOutlookWidth() { var parsedWidth = CssUnitParser.parse(getAttribute("width")); - switch (parsedWidth.unit.toLowerCase(Locale.ROOT)) { + switch (parsedWidth.unit().toLowerCase(Locale.ROOT)) { case "%": { - var effectiveWidth = containerWidth.value - paddingSize; - var percentMultiplier = parsedWidth.value / 100; + var effectiveWidth = containerWidth.value() - paddingSize; + var percentMultiplier = parsedWidth.value() / 100; return doubleToString(effectiveWidth * percentMultiplier) + "px"; } case "px": return parsedWidth.toString(); default: - return doubleToString(containerWidth.value - paddingSize) + "px"; + return doubleToString(containerWidth.value() - paddingSize) + "px"; } } @Override void setupStyles(CssStyleLibraries cssStyleLibraries) { + var computeAlign = "0px auto"; + var alignAttribute = getAttribute("align"); + if ("left".equals(alignAttribute)) { + computeAlign = "0px"; + } else if ("right".equals(alignAttribute)) { + computeAlign = "0px 0px 0px auto"; + } var pStyle = mapOf( "border-top", getAttribute("border-style") + " " + getAttribute("border-width") + " " + getAttribute("border-color"), "font-size", "1px", - "margin", "0px auto", + "margin", computeAlign, "width", getAttribute("width") ); @@ -80,7 +87,7 @@ private StringBuilder renderAfter(HtmlRenderer renderer) { renderer.appendCurrentSpacing(res); res.append("