diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..3d575ab --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,31 @@ +name: CI + +on: + push: + branches: [ main, 'feature/**' ] + pull_request: + branches: [ main ] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + cache: maven + + - name: Build and test + run: ./mvnw clean verify + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results + path: target/surefire-reports/ diff --git a/.gitignore b/.gitignore index d3153ad..bc01af2 100644 --- a/.gitignore +++ b/.gitignore @@ -198,17 +198,4 @@ buildNumber.properties # Project-level settings /.tgitconfig -### Web ### -*.asp -*.cer -*.csr -*.css -*.htm -*.html -*.js -*.jsp -*.php -*.rss -*.xhtml - # End of https://www.gitignore.io/api/web,git,java,maven,eclipse,tortoisegit,intellij+all,intellij+iml diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties index c315043..11213b0 100644 --- a/.mvn/wrapper/maven-wrapper.properties +++ b/.mvn/wrapper/maven-wrapper.properties @@ -1 +1 @@ -distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.0/apache-maven-3.5.0-bin.zip +distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 7c4268d..0000000 --- a/.travis.yml +++ /dev/null @@ -1,22 +0,0 @@ -language: java -jdk: oraclejdk8 - -branches: - except: - - master - -cache: - directories: - - $HOME/.m2/repository - -install: mvn install - -addons: - sonarcloud: - organization: "serbroda-github" - token: - secure: e0624c074cc2912e0a56aad2df374638bbc29bed - branches: - - develop -script: - - mvn clean org.jacoco:jacoco-maven-plugin:prepare-agent install sonar:sonar \ No newline at end of file diff --git a/README.md b/README.md index fe14135..31fd0a9 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,24 @@ Thymeleaf Component Dialect =========================== -[![Build Status](https://travis-ci.org/Serbroda/thymeleaf-component-dialect.svg?branch=develop)](https://travis-ci.org/Serbroda/thymeleaf-component-dialect) +[![CI](https://github.com/Serbroda/thymeleaf-component-dialect/actions/workflows/ci.yml/badge.svg)](https://github.com/Serbroda/thymeleaf-component-dialect/actions/workflows/ci.yml) [![jitpack](https://jitpack.io/v/Serbroda/thymeleaf-component-dialect.svg)](https://jitpack.io/#Serbroda/thymeleaf-component-dialect) -[![license](https://img.shields.io/github/license/Serbroda/thymeleaf-component-dialect.svg)](https://github.com/Serbroda/thymeleaf-component-dialect/blob/master/LICENSE.txt) +[![license](https://img.shields.io/github/license/Serbroda/thymeleaf-component-dialect.svg)](https://github.com/Serbroda/thymeleaf-component-dialect/blob/main/LICENSE.txt) +A dialect for creating reusable Thymeleaf components, similar to React or Vue components. -A dialect for creating reusable thymeleaf components. +Requirements +------ + +- Java 17+ +- Thymeleaf 3.1+ Installation ------ Add the jitpack repository. -```html +```xml jitpack.io @@ -24,7 +29,7 @@ Add the jitpack repository. Add the dependency (for all available versions see [https://jitpack.io/#Serbroda/thymeleaf-component-dialect](https://jitpack.io/#Serbroda/thymeleaf-component-dialect)). -```html +```xml com.github.Serbroda thymeleaf-component-dialect @@ -47,8 +52,6 @@ public ComponentDialect componentDialect() { Usage ----- -For detailed configurations have a look at the [demo project](https://github.com/Serbroda/thymeleaf-component-dialect-demo). - ### Create a thymeleaf component Thymeleaf components uses the standard `th:fragment` attribute to register components. Just create a fragment with a `` tag which will be replaced with specific contents. @@ -66,13 +69,13 @@ Thymeleaf components uses the standard `th:fragment` attribute to register compo ### Use the component -Add the namespace `xmlns:tc="http://www.morphbit.com/thymeleaf/component"` and use the component in your application. +Add the namespace `xmlns:tc="https://github.com/Serbroda/thymeleaf-component-dialect"` and use the component in your application. ```html + xmlns:tc="https://github.com/Serbroda/thymeleaf-component-dialect"> @@ -92,10 +95,35 @@ Add the namespace `xmlns:tc="http://www.morphbit.com/thymeleaf/component"` and u A title
-

Hello world

This is my first thymeleaf component

-
-``` \ No newline at end of file +``` + +### The `tc:once` Attribute + +Use `tc:once` to ensure an element (e.g. a script tag) is only rendered once, even if the component is used multiple times on the same page: + +```html +
+ + +
+``` + +Contributing +------ + +Contributions are welcome! Feel free to open an [issue](https://github.com/Serbroda/thymeleaf-component-dialect/issues) or submit a [pull request](https://github.com/Serbroda/thymeleaf-component-dialect/pulls). + +Before submitting a PR, please make sure: +- All tests pass: `./mvnw clean verify` +- Code is formatted: `./mvnw spotless:apply` + +License +------ + +This project is licensed under the [Apache License, Version 2.0](LICENSE.txt). \ No newline at end of file diff --git a/pom.xml b/pom.xml index 43d8a54..12a78ca 100644 --- a/pom.xml +++ b/pom.xml @@ -1,47 +1,97 @@ - - - 4.0.0 - - de.morphbit - thymeleaf-component-dialect - 0.0.3-alpha - jar - - thymeleaf-component-dialect - Thymeleaf Layout Dialect - - - UTF-8 - UTF-8 - 1.8 - 1.8 - 1.8 - - - - - org.thymeleaf - thymeleaf - 3.0.7.RELEASE - - - - - - - true - org.apache.maven.plugins - maven-source-plugin - 3.0.1 - - - attach-sources - - jar-no-fork - - - - - - - + + + 4.0.0 + + de.morphbit + thymeleaf-component-dialect + 0.1.0-SNAPSHOT + jar + + thymeleaf-component-dialect + A Thymeleaf dialect for creating reusable components + + + + Apache License, Version 2.0 + https://www.apache.org/licenses/LICENSE-2.0 + + + + + UTF-8 + UTF-8 + 17 + 17 + 17 + + + + + org.thymeleaf + thymeleaf + 3.1.3.RELEASE + + + org.junit.jupiter + junit-jupiter + 5.11.4 + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + 17 + 17 + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.2 + + + com.diffplug.spotless + spotless-maven-plugin + 2.44.3 + + + + 4.33 + + + + + + + + spotless-check + verify + + check + + + + + + true + org.apache.maven.plugins + maven-source-plugin + 3.3.1 + + + attach-sources + + jar-no-fork + + + + + + + diff --git a/src/main/java/de/morphbit/thymeleaf/dialect/ComponentDialect.java b/src/main/java/de/morphbit/thymeleaf/dialect/ComponentDialect.java index 0b20098..2348231 100644 --- a/src/main/java/de/morphbit/thymeleaf/dialect/ComponentDialect.java +++ b/src/main/java/de/morphbit/thymeleaf/dialect/ComponentDialect.java @@ -16,19 +16,17 @@ package de.morphbit.thymeleaf.dialect; +import de.morphbit.thymeleaf.model.ThymeleafComponent; +import de.morphbit.thymeleaf.parser.IThymeleafComponentParser; +import de.morphbit.thymeleaf.processor.ComponentNamedElementProcessor; +import de.morphbit.thymeleaf.processor.OnceAttributeTagProcessor; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; - import org.thymeleaf.dialect.AbstractProcessorDialect; import org.thymeleaf.processor.IProcessor; -import de.morphbit.thymeleaf.model.ThymeleafComponent; -import de.morphbit.thymeleaf.parser.IThymeleafComponentParser; -import de.morphbit.thymeleaf.processor.ComponentNamedElementProcessor; -import de.morphbit.thymeleaf.processor.OnceAttributeTagProcessor; - /** * A dialect for creating reusable composite components with thymeleaf * @@ -73,14 +71,14 @@ public Set getProcessors(String dialectPrefix) { if (this.components != null) { for (ThymeleafComponent comp : this.components) { - processors.add(new ComponentNamedElementProcessor(dialectPrefix, - comp.getName(), comp.getFragmentTemplate())); + processors.add( + new ComponentNamedElementProcessor(dialectPrefix, comp.getName(), comp.getFragmentTemplate())); } } for (ThymeleafComponent comp : parseComponents()) { - processors.add(new ComponentNamedElementProcessor(dialectPrefix, - comp.getName(), comp.getFragmentTemplate())); + processors + .add(new ComponentNamedElementProcessor(dialectPrefix, comp.getName(), comp.getFragmentTemplate())); } return processors; diff --git a/src/main/java/de/morphbit/thymeleaf/exception/ComponentDialectException.java b/src/main/java/de/morphbit/thymeleaf/exception/ComponentDialectException.java new file mode 100644 index 0000000..7f493ab --- /dev/null +++ b/src/main/java/de/morphbit/thymeleaf/exception/ComponentDialectException.java @@ -0,0 +1,12 @@ +package de.morphbit.thymeleaf.exception; + +public class ComponentDialectException extends RuntimeException { + + public ComponentDialectException(String message) { + super(message); + } + + public ComponentDialectException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/de/morphbit/thymeleaf/helper/FragmentHelper.java b/src/main/java/de/morphbit/thymeleaf/helper/FragmentHelper.java index 9ab08a0..ef73373 100644 --- a/src/main/java/de/morphbit/thymeleaf/helper/FragmentHelper.java +++ b/src/main/java/de/morphbit/thymeleaf/helper/FragmentHelper.java @@ -1,7 +1,6 @@ package de.morphbit.thymeleaf.helper; import java.util.Map; - import org.thymeleaf.IEngineConfiguration; import org.thymeleaf.context.ITemplateContext; import org.thymeleaf.engine.TemplateData; @@ -18,7 +17,6 @@ import org.thymeleaf.standard.expression.FragmentSignatureUtils; import org.thymeleaf.standard.expression.IStandardExpressionParser; import org.thymeleaf.standard.expression.NoOpToken; -import org.thymeleaf.standard.expression.StandardExpressionExecutionContext; import org.thymeleaf.standard.expression.StandardExpressions; import org.thymeleaf.util.EscapedAttributeUtils; import org.thymeleaf.util.StringUtils; @@ -29,10 +27,9 @@ private FragmentHelper() { } - public static IModel getFragmentModel(final ITemplateContext context, - final String attributeValue, - final IElementModelStructureHandler structureHandler, - final String dialectPrefix, final String FRAGMENT_ATTR_NAME) { + public static IModel getFragmentModel(final ITemplateContext context, final String attributeValue, + final IElementModelStructureHandler structureHandler, final String dialectPrefix, + final String FRAGMENT_ATTR_NAME) { IEngineConfiguration configuration = context.getConfiguration(); @@ -43,9 +40,8 @@ public static IModel getFragmentModel(final ITemplateContext context, // result is not the same as the result being the empty fragment // (~{}) - throw new TemplateInputException( - "Error resolving fragment: \"" + attributeValue + "\": " - + "template or fragment could not be resolved"); + throw new TemplateInputException("Error resolving fragment: \"" + attributeValue + "\": " + + "template or fragment could not be resolved"); } @@ -55,14 +51,13 @@ public static IModel getFragmentModel(final ITemplateContext context, Map fragmentParameters = fragment.getParameters(); /* - * ONCE WE HAVE THE FRAGMENT MODEL (its events, in fact), CHECK THE - * FRAGMENT SIGNATURE Fragment signature is important because it might - * affect the way we apply the parameters to the fragment. + * ONCE WE HAVE THE FRAGMENT MODEL (its events, in fact), CHECK THE FRAGMENT + * SIGNATURE Fragment signature is important because it might affect the way we + * apply the parameters to the fragment. * - * Note this works whatever the template mode of the inserted fragment, - * given we are looking for an element containing a - * "th:fragment/data-th-fragment" in a generic, non-template-dependent - * way. + * Note this works whatever the template mode of the inserted fragment, given we + * are looking for an element containing a "th:fragment/data-th-fragment" in a + * generic, non-template-dependent way. */ // We will check types first instead of events in order to (many times) @@ -70,36 +65,26 @@ public static IModel getFragmentModel(final ITemplateContext context, // event object when calling "model.get(pos)" boolean signatureApplied = false; - final ITemplateEvent firstEvent = - fragmentModel.size() > 2 ? fragmentModel.get(1) : null; - if (firstEvent != null && IProcessableElementTag.class - .isAssignableFrom(firstEvent.getClass())) { - - final IProcessableElementTag fragmentHolderEvent = - (IProcessableElementTag) firstEvent; + final ITemplateEvent firstEvent = fragmentModel.size() > 2 ? fragmentModel.get(1) : null; + if (firstEvent instanceof IProcessableElementTag fragmentHolderEvent) { - if (fragmentHolderEvent.hasAttribute(dialectPrefix, - FRAGMENT_ATTR_NAME)) { + if (fragmentHolderEvent.hasAttribute(dialectPrefix, FRAGMENT_ATTR_NAME)) { // The selected fragment actually has a "th:fragment" attribute, // so we should process its signature - final String fragmentSignatureSpec = EscapedAttributeUtils - .unescapeAttribute(fragmentModel.getTemplateMode(), - fragmentHolderEvent.getAttributeValue(dialectPrefix, - FRAGMENT_ATTR_NAME)); + final String fragmentSignatureSpec = EscapedAttributeUtils.unescapeAttribute( + fragmentModel.getTemplateMode(), + fragmentHolderEvent.getAttributeValue(dialectPrefix, FRAGMENT_ATTR_NAME)); if (!StringUtils.isEmptyOrWhitespace(fragmentSignatureSpec)) { - final FragmentSignature fragmentSignature = - FragmentSignatureUtils.parseFragmentSignature( - configuration, fragmentSignatureSpec); + final FragmentSignature fragmentSignature = FragmentSignatureUtils + .parseFragmentSignature(configuration, fragmentSignatureSpec); if (fragmentSignature != null) { // Reshape the fragment parameters into the ones that we // will actually use, according to the signature - fragmentParameters = - FragmentSignatureUtils.processParameters( - fragmentSignature, fragmentParameters, - fragment.hasSyntheticParameters()); + fragmentParameters = FragmentSignatureUtils.processParameters(fragmentSignature, + fragmentParameters, fragment.hasSyntheticParameters()); signatureApplied = true; } @@ -118,63 +103,52 @@ public static IModel getFragmentModel(final ITemplateContext context, // assignation involved. if (!signatureApplied && fragment.hasSyntheticParameters()) { throw new TemplateProcessingException("Fragment '" + attributeValue - + "' specifies synthetic (unnamed) parameters, but the resolved fragment " - + "does not match a fragment signature (th:fragment,data-th-fragment) which could apply names to " - + "the specified parameters."); + + "' specifies synthetic (unnamed) parameters, but the resolved fragment " + + "does not match a fragment signature (th:fragment,data-th-fragment) which could apply names to " + + "the specified parameters."); } /* - * APPLY THE FRAGMENT'S TEMPLATE RESOLUTION so that all code inside the - * fragment is executed with its own template resolution info (working - * as if it were a local variable) + * APPLY THE FRAGMENT'S TEMPLATE RESOLUTION so that all code inside the fragment + * is executed with its own template resolution info (working as if it were a + * local variable) */ - final TemplateData fragmentTemplateData = - fragmentModel.getTemplateData(); + final TemplateData fragmentTemplateData = fragmentModel.getTemplateData(); structureHandler.setTemplateData(fragmentTemplateData); /* - * APPLY THE FRAGMENT PARAMETERS AS LOCAL VARIABLES, perhaps after - * reshaping it according to the fragment signature + * APPLY THE FRAGMENT PARAMETERS AS LOCAL VARIABLES, perhaps after reshaping it + * according to the fragment signature */ - if (fragmentParameters != null && fragmentParameters.size() > 0) { - for (final Map.Entry fragmentParameterEntry : fragmentParameters - .entrySet()) { - structureHandler.setLocalVariable( - fragmentParameterEntry.getKey(), - fragmentParameterEntry.getValue()); + if (fragmentParameters != null && !fragmentParameters.isEmpty()) { + for (final Map.Entry fragmentParameterEntry : fragmentParameters.entrySet()) { + structureHandler.setLocalVariable(fragmentParameterEntry.getKey(), fragmentParameterEntry.getValue()); } } return fragmentModel; } - private static Object computeFragment(final ITemplateContext context, - final String input) { + private static Object computeFragment(final ITemplateContext context, final String input) { final IStandardExpressionParser expressionParser = StandardExpressions - .getExpressionParser(context.getConfiguration()); + .getExpressionParser(context.getConfiguration()); - final FragmentExpression fragmentExpression = - (FragmentExpression) expressionParser.parseExpression(context, - "~{" + input.trim() + "}"); + final FragmentExpression fragmentExpression = (FragmentExpression) expressionParser.parseExpression(context, + "~{" + input.trim() + "}"); - final FragmentExpression.ExecutedFragmentExpression executedFragmentExpression = - FragmentExpression.createExecutedFragmentExpression(context, - fragmentExpression, - StandardExpressionExecutionContext.NORMAL); + final FragmentExpression.ExecutedFragmentExpression executedFragmentExpression = FragmentExpression + .createExecutedFragmentExpression(context, fragmentExpression); - if (executedFragmentExpression - .getFragmentSelectorExpressionResult() == null - && executedFragmentExpression.getFragmentParameters() == null) { + if (executedFragmentExpression.getFragmentSelectorExpressionResult() == null + && executedFragmentExpression.getFragmentParameters() == null) { // We might be in the scenario that what we thought was a template // name in fact was instead an expression // returning a Fragment itself, so we should simply return it - final Object templateNameExpressionResult = - executedFragmentExpression - .getTemplateNameExpressionResult(); + final Object templateNameExpressionResult = executedFragmentExpression.getTemplateNameExpressionResult(); if (templateNameExpressionResult != null) { - if (templateNameExpressionResult instanceof Fragment) { - return templateNameExpressionResult; + if (templateNameExpressionResult instanceof Fragment fragment) { + return fragment; } if (templateNameExpressionResult == NoOpToken.VALUE) { return NoOpToken.VALUE; @@ -190,7 +164,6 @@ private static Object computeFragment(final ITemplateContext context, // underlying resolution system would // have to execute a (potentially costly) resource.exists() call on the // resolved resource. - return FragmentExpression.resolveExecutedFragmentExpression(context, - executedFragmentExpression, true); + return FragmentExpression.resolveExecutedFragmentExpression(context, executedFragmentExpression, true); } } diff --git a/src/main/java/de/morphbit/thymeleaf/helper/ResourcePathFinder.java b/src/main/java/de/morphbit/thymeleaf/helper/ResourcePathFinder.java index 303ed33..e37877b 100644 --- a/src/main/java/de/morphbit/thymeleaf/helper/ResourcePathFinder.java +++ b/src/main/java/de/morphbit/thymeleaf/helper/ResourcePathFinder.java @@ -1,12 +1,12 @@ -/* +/* * Copyright 2017, Danny Rottstegge - * + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -16,10 +16,22 @@ package de.morphbit.thymeleaf.helper; -import java.io.File; +import de.morphbit.thymeleaf.exception.ComponentDialectException; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; +import java.nio.file.FileSystem; +import java.nio.file.FileSystemAlreadyExistsException; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; +import java.util.Enumeration; import java.util.List; +import java.util.Map; +import java.util.stream.Stream; public class ResourcePathFinder { @@ -28,7 +40,7 @@ public class ResourcePathFinder { /** * Constructor - * + * * @param directory * Base directory to search resource files (e.g. * templates/components) @@ -40,35 +52,47 @@ public ResourcePathFinder(String directory) { /** * Searches for resource files - * - * @param recursively - * Search files recursively + * + * Search files recursively + * * @return List of files as strings */ - public List findResourceFiles(boolean recursively) { - return getResourceFiles(directory, new ArrayList<>(), recursively); + public List findResourceFiles() { + return getResourceFiles(directory); } - private List getResourceFiles(String dir, List files, - boolean recursively) { - URL url = loader.getResource(dir); - String path = url.getPath(); - for (File file : new File(path).listFiles()) { - if (recursively && file.isDirectory()) { - return getResourceFiles(concatPath(dir, file.getName()), files, - recursively); - } else { - files.add(concatPath(dir, file.getName())); + private List getResourceFiles(String dir) { + List files = new ArrayList<>(); + try { + Enumeration resources = loader.getResources(dir); + while (resources.hasMoreElements()) { + URL resourceUrl = resources.nextElement(); + URI uri = resourceUrl.toURI(); + + Path path; + if ("jar".equals(uri.getScheme())) { + FileSystem fileSystem; + try { + fileSystem = FileSystems.newFileSystem(uri, Map.of()); + } catch (FileSystemAlreadyExistsException e) { + fileSystem = FileSystems.getFileSystem(uri); + } + path = fileSystem.getPath(dir); + } else { + path = Paths.get(uri); + } + + try (Stream walk = Files.walk(path)) { + walk.filter(Files::isRegularFile).forEach(p -> { + String relativePath = dir + "/" + path.relativize(p).toString(); + files.add(relativePath); + }); + } } + } catch (IOException | URISyntaxException ex) { + throw new ComponentDialectException("Could not find resource files in directory: " + dir, ex); } - return files; - } - private String concatPath(String path1, String path2) { - if (path1 == null) { - return path2; - } - return path1.replaceAll("[/|\\\\]*$", "") + "/" - + path2.replaceAll("^[/|\\\\]*", ""); + return files; } } diff --git a/src/main/java/de/morphbit/thymeleaf/helper/WithHelper.java b/src/main/java/de/morphbit/thymeleaf/helper/WithHelper.java index bb80bca..4e07695 100644 --- a/src/main/java/de/morphbit/thymeleaf/helper/WithHelper.java +++ b/src/main/java/de/morphbit/thymeleaf/helper/WithHelper.java @@ -1,7 +1,6 @@ package de.morphbit.thymeleaf.helper; import java.util.List; - import org.thymeleaf.context.IEngineContext; import org.thymeleaf.context.ITemplateContext; import org.thymeleaf.exceptions.TemplateProcessingException; @@ -18,16 +17,13 @@ private WithHelper() { } - public static void processWith(ITemplateContext context, - String attributeValue, - IElementModelStructureHandler structureHandler) { - final AssignationSequence assignations = - AssignationUtils.parseAssignationSequence(context, - attributeValue, false /* no parameters without value */); + public static void processWith(ITemplateContext context, String attributeValue, + IElementModelStructureHandler structureHandler, boolean registerGlobal) { + final AssignationSequence assignations = AssignationUtils.parseAssignationSequence(context, attributeValue, + false /* no parameters without value */); if (assignations == null) { throw new TemplateProcessingException( - "Could not parse value as attribute assignations: \"" - + attributeValue + "\""); + "Could not parse value as attribute assignations: \"" + attributeValue + "\""); } // Normally we would just allow the structure handler to be in charge of @@ -39,15 +35,10 @@ public static void processWith(ITemplateContext context, // a more specific interface --which shouldn't be used directly except // in this specific, special case-- and // put the local variables directly into it. - IEngineContext engineContext = null; - if (context instanceof IEngineContext) { - // NOTE this interface is internal and should not be used in users' - // code - engineContext = (IEngineContext) context; - } + // NOTE IEngineContext is internal and should not be used in users' code + var engineContext = context instanceof IEngineContext ctx ? ctx : null; - final List assignationValues = - assignations.getAssignations(); + final List assignationValues = assignations.getAssignations(); final int assignationValuesLen = assignationValues.size(); for (int i = 0; i < assignationValuesLen; i++) { @@ -60,23 +51,15 @@ public static void processWith(ITemplateContext context, final IStandardExpression rightExpr = assignation.getRight(); final Object rightValue = rightExpr.execute(context); - final String newVariableName = - leftValue == null ? null : leftValue.toString(); + final String newVariableName = leftValue == null ? null : leftValue.toString(); if (StringUtils.isEmptyOrWhitespace(newVariableName)) { throw new TemplateProcessingException( - "Variable name expression evaluated as null or empty: \"" - + leftExpr + "\""); + "Variable name expression evaluated as null or empty: \"" + leftExpr + "\""); } - if (engineContext != null) { - // The advantage of this vs. using the structure handler is that - // we will be able to - // use this newly created value in other expressions in the same - // 'th:with' + if (registerGlobal && engineContext != null) { engineContext.setVariable(newVariableName, rightValue); } else { - // The problem is, these won't be available until we execute the - // next processor structureHandler.setLocalVariable(newVariableName, rightValue); } diff --git a/src/main/java/de/morphbit/thymeleaf/model/ThymeleafComponent.java b/src/main/java/de/morphbit/thymeleaf/model/ThymeleafComponent.java index def9aa2..5c9a8d6 100644 --- a/src/main/java/de/morphbit/thymeleaf/model/ThymeleafComponent.java +++ b/src/main/java/de/morphbit/thymeleaf/model/ThymeleafComponent.java @@ -69,8 +69,7 @@ public void setFragmentTemplate(String fragmentTemplate) { @Override public String toString() { - return "Component [name=" + name + ", fragmentTemplate=" - + fragmentTemplate + "]"; + return "Component [name=" + name + ", fragmentTemplate=" + fragmentTemplate + "]"; } } diff --git a/src/main/java/de/morphbit/thymeleaf/parser/AbstractElementParser.java b/src/main/java/de/morphbit/thymeleaf/parser/AbstractElementParser.java index 23d370c..efc72cc 100644 --- a/src/main/java/de/morphbit/thymeleaf/parser/AbstractElementParser.java +++ b/src/main/java/de/morphbit/thymeleaf/parser/AbstractElementParser.java @@ -16,26 +16,21 @@ package de.morphbit.thymeleaf.parser; +import de.morphbit.thymeleaf.exception.ComponentDialectException; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.util.ArrayList; import java.util.List; - import org.attoparser.AbstractMarkupHandler; import org.attoparser.MarkupParser; import org.attoparser.ParseException; import org.attoparser.config.ParseConfiguration; import org.attoparser.dom.Element; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; public abstract class AbstractElementParser extends AbstractMarkupHandler { - private static final Logger LOG = - LoggerFactory.getLogger(AbstractElementParser.class); - protected final String dialectPrefix; private List elements; @@ -46,57 +41,38 @@ public AbstractElementParser(String dialectPrefix) { } protected List parseElements(String file) { - return parseElements(Thread.currentThread().getContextClassLoader() - .getResourceAsStream(file)); + return parseElements(Thread.currentThread().getContextClassLoader().getResourceAsStream(file)); } protected List parseElements(InputStream stream) { this.elements = new ArrayList<>(); - try (Reader reader = new InputStreamReader(stream)) { - final ParseConfiguration config = - ParseConfiguration.htmlConfiguration(); - - final ParseConfiguration autoCloseConfig = - ParseConfiguration.htmlConfiguration(); - autoCloseConfig.setElementBalancing( - ParseConfiguration.ElementBalancing.AUTO_OPEN_CLOSE); - - final MarkupParser htmlStandardParser = new MarkupParser(config); + try (stream; Reader reader = new InputStreamReader(stream)) { + var config = ParseConfiguration.htmlConfiguration(); + var htmlStandardParser = new MarkupParser(config); htmlStandardParser.parse(reader, this); - } catch (IOException | ParseException e) { - LOG.error("Error while parsing elements: {}", e); - } finally { - try { - if (stream != null) { - stream.close(); - } - } catch (IOException e) { - LOG.error("Error while closing stream: {}", e); - } + throw new ComponentDialectException("Error while parsing elements", e); } return this.elements; } @Override - public void handleAttribute(char[] buffer, int nameOffset, int nameLen, - int nameLine, int nameCol, int operatorOffset, int operatorLen, - int operatorLine, int operatorCol, int valueContentOffset, - int valueContentLen, int valueOuterOffset, int valueOuterLen, - int valueLine, int valueCol) throws ParseException { + public void handleAttribute(char[] buffer, int nameOffset, int nameLen, int nameLine, int nameCol, + int operatorOffset, int operatorLen, int operatorLine, int operatorCol, int valueContentOffset, + int valueContentLen, int valueOuterOffset, int valueOuterLen, int valueLine, int valueCol) + throws ParseException { String attributeName = new String(buffer, nameOffset, nameLen); - String attributeValue = - new String(buffer, valueContentOffset, valueContentLen); + String attributeValue = new String(buffer, valueContentOffset, valueContentLen); if (currentElement != null) { currentElement.addAttribute(attributeName, attributeValue); } } @Override - public void handleOpenElementStart(char[] buffer, int nameOffset, - int nameLen, int line, int col) throws ParseException { + public void handleOpenElementStart(char[] buffer, int nameOffset, int nameLen, int line, int col) + throws ParseException { String attributeName = new String(buffer, nameOffset, nameLen); this.currentElement = new Element(attributeName); this.elements.add(currentElement); diff --git a/src/main/java/de/morphbit/thymeleaf/parser/IThymeleafComponentParser.java b/src/main/java/de/morphbit/thymeleaf/parser/IThymeleafComponentParser.java index b12afe0..5a400a5 100644 --- a/src/main/java/de/morphbit/thymeleaf/parser/IThymeleafComponentParser.java +++ b/src/main/java/de/morphbit/thymeleaf/parser/IThymeleafComponentParser.java @@ -16,9 +16,8 @@ package de.morphbit.thymeleaf.parser; -import java.util.Set; - import de.morphbit.thymeleaf.model.ThymeleafComponent; +import java.util.Set; public interface IThymeleafComponentParser { diff --git a/src/main/java/de/morphbit/thymeleaf/parser/StandardThymeleafComponentParser.java b/src/main/java/de/morphbit/thymeleaf/parser/StandardThymeleafComponentParser.java index e0826c3..758df62 100644 --- a/src/main/java/de/morphbit/thymeleaf/parser/StandardThymeleafComponentParser.java +++ b/src/main/java/de/morphbit/thymeleaf/parser/StandardThymeleafComponentParser.java @@ -16,18 +16,15 @@ package de.morphbit.thymeleaf.parser; +import de.morphbit.thymeleaf.dialect.ComponentDialect; +import de.morphbit.thymeleaf.helper.ResourcePathFinder; +import de.morphbit.thymeleaf.model.ThymeleafComponent; import java.util.HashSet; import java.util.Set; - import org.attoparser.dom.Element; import org.thymeleaf.standard.StandardDialect; -import de.morphbit.thymeleaf.dialect.ComponentDialect; -import de.morphbit.thymeleaf.helper.ResourcePathFinder; -import de.morphbit.thymeleaf.model.ThymeleafComponent; - -public class StandardThymeleafComponentParser extends AbstractElementParser - implements IThymeleafComponentParser { +public class StandardThymeleafComponentParser extends AbstractElementParser implements IThymeleafComponentParser { protected static final String NAME_ATTRIBUTE = "selector"; protected static final String FRAGMENT_ATTRIBUTE = "fragment"; @@ -44,11 +41,10 @@ public class StandardThymeleafComponentParser extends AbstractElementParser * @param templateSuffix * Template suffix (e.g. .html) * @param directory - * Subdirectory of param templatePrefix to find components in - * (e.g. components) + * Subdirectory of param templatePrefix to find components in (e.g. + * components) */ - public StandardThymeleafComponentParser(String templatePrefix, - String templateSuffix, String directory) { + public StandardThymeleafComponentParser(String templatePrefix, String templateSuffix, String directory) { super(ComponentDialect.PREFIX); this.directory = directory; this.templatePrefix = templatePrefix; @@ -59,8 +55,7 @@ public StandardThymeleafComponentParser(String templatePrefix, public Set parse() { Set components = new HashSet<>(); - for (String file : new ResourcePathFinder(templatePrefix + directory) - .findResourceFiles(true)) { + for (String file : new ResourcePathFinder(templatePrefix + directory).findResourceFiles()) { for (Element element : parseElements(file)) { if (isThymeleafComponent(element)) { components.add(createComponent(element, file)); @@ -71,19 +66,15 @@ public Set parse() { return components; } - private ThymeleafComponent createComponent(Element element, - String htmlFile) { + private ThymeleafComponent createComponent(Element element, String htmlFile) { String templateFile = htmlFile; templateFile = templateFile.replaceAll("^" + this.templatePrefix, ""); - templateFile = templateFile - .replaceAll(this.templateSuffix.replace(".", "\\."), ""); + templateFile = templateFile.replaceAll(this.templateSuffix.replace(".", "\\."), ""); - String frag = getDynamicAttributeValue(element, StandardDialect.PREFIX, - FRAGMENT_ATTRIBUTE); + String frag = getDynamicAttributeValue(element, StandardDialect.PREFIX, FRAGMENT_ATTRIBUTE); frag = frag.replaceAll("\\(.*\\)", ""); - String name = getDynamicAttributeValue(element, this.dialectPrefix, - NAME_ATTRIBUTE); + String name = getDynamicAttributeValue(element, this.dialectPrefix, NAME_ATTRIBUTE); if (name == null) { name = frag; } @@ -92,20 +83,16 @@ private ThymeleafComponent createComponent(Element element, } private boolean isThymeleafComponent(Element element) { - return hasDynamicAttribute(element, StandardDialect.PREFIX, - FRAGMENT_ATTRIBUTE); + return hasDynamicAttribute(element, StandardDialect.PREFIX, FRAGMENT_ATTRIBUTE); } - private boolean hasDynamicAttribute(Element element, String prefix, - String dynamicAttribute) { + private boolean hasDynamicAttribute(Element element, String prefix, String dynamicAttribute) { return element.hasAttribute("data-" + prefix + "-" + dynamicAttribute) - || element.hasAttribute(prefix + ":" + dynamicAttribute); + || element.hasAttribute(prefix + ":" + dynamicAttribute); } - private String getDynamicAttributeValue(Element element, String prefix, - String dynamicAttribute) { - String value = element - .getAttributeValue("data-" + prefix + "-" + dynamicAttribute); + private String getDynamicAttributeValue(Element element, String prefix, String dynamicAttribute) { + String value = element.getAttributeValue("data-" + prefix + "-" + dynamicAttribute); if (value != null) { return value; } diff --git a/src/main/java/de/morphbit/thymeleaf/processor/ComponentNamedElementProcessor.java b/src/main/java/de/morphbit/thymeleaf/processor/ComponentNamedElementProcessor.java index 6922057..0f82de1 100644 --- a/src/main/java/de/morphbit/thymeleaf/processor/ComponentNamedElementProcessor.java +++ b/src/main/java/de/morphbit/thymeleaf/processor/ComponentNamedElementProcessor.java @@ -16,15 +16,16 @@ package de.morphbit.thymeleaf.processor; -import static java.util.Collections.singleton; +import static java.util.Set.of; +import de.morphbit.thymeleaf.helper.FragmentHelper; +import de.morphbit.thymeleaf.helper.WithHelper; import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; - import org.thymeleaf.context.ITemplateContext; import org.thymeleaf.engine.AttributeNames; import org.thymeleaf.model.IAttribute; @@ -38,18 +39,15 @@ import org.thymeleaf.standard.StandardDialect; import org.thymeleaf.templatemode.TemplateMode; -import de.morphbit.thymeleaf.helper.FragmentHelper; -import de.morphbit.thymeleaf.helper.WithHelper; - -public class ComponentNamedElementProcessor - extends AbstractElementModelProcessor { +public class ComponentNamedElementProcessor extends AbstractElementModelProcessor { private static final String FRAGMENT_ATTRIBUTE = "fragment"; private static final String REPLACE_CONTENT_TAG = "tc:content"; private static final int PRECEDENCE = 350; + private static final Pattern REPLACE_PATTERN = Pattern.compile(".*\\?\\[([\\w.\\-_]*)\\].*"); - private final Set excludeAttributes = singleton("params"); + private final Set excludeAttributes = of("tc:constructor"); private final String fragmentName; /** @@ -62,42 +60,38 @@ public class ComponentNamedElementProcessor * @param fragmentName * Fragment to search for */ - public ComponentNamedElementProcessor(final String dialectPrefix, - final String tagName, final String fragmentName) { - super(TemplateMode.HTML, dialectPrefix, tagName, true, null, false, - PRECEDENCE); + public ComponentNamedElementProcessor(final String dialectPrefix, final String tagName, final String fragmentName) { + super(TemplateMode.HTML, dialectPrefix, tagName, true, null, false, PRECEDENCE); this.fragmentName = fragmentName; } @Override - protected void doProcess(ITemplateContext context, IModel model, - IElementModelStructureHandler structureHandler) { + protected void doProcess(ITemplateContext context, IModel model, IElementModelStructureHandler structureHandler) { IProcessableElementTag tag = processElementTag(context, model); - Map attrMap = processAttribute(tag); + Map attributes = processAttribute(tag); - String param = attrMap.get("params"); + String constructorParams = attributes.get("tc:constructor"); - IModel base = model.cloneModel(); - base.remove(0); + IModel componentModel = model.cloneModel(); + componentModel.remove(0); - if (base.size() > 1) { - base.remove(base.size() - 1); + if (componentModel.size() > 1) { + componentModel.remove(componentModel.size() - 1); } - IModel frag = FragmentHelper.getFragmentModel(context, - fragmentName + (param == null ? "" : "(" + param + ")"), - structureHandler, StandardDialect.PREFIX, FRAGMENT_ATTRIBUTE); + IModel fragmentModel = FragmentHelper.getFragmentModel(context, + fragmentName + (constructorParams == null ? "" : "(" + constructorParams + ")"), structureHandler, + StandardDialect.PREFIX, FRAGMENT_ATTRIBUTE); model.reset(); - IModel replaced = replaceAllAttributeValues(attrMap, context, frag); - model.addModel(mergeModels(replaced, base, REPLACE_CONTENT_TAG)); + IModel replacedFragmentModel = replaceAllAttributeValues(attributes, context, fragmentModel); + model.addModel(mergeModels(replacedFragmentModel, componentModel, REPLACE_CONTENT_TAG)); - processVariables(attrMap, context, structureHandler, excludeAttributes); + processVariables(attributes, context, structureHandler, excludeAttributes); } - private IProcessableElementTag processElementTag(ITemplateContext context, - IModel model) { + private IProcessableElementTag processElementTag(ITemplateContext context, IModel model) { ITemplateEvent firstEvent = model.get(0); for (IProcessableElementTag tag : context.getElementStack()) { if (locationMatches(firstEvent, tag)) { @@ -108,46 +102,40 @@ private IProcessableElementTag processElementTag(ITemplateContext context, } private boolean locationMatches(ITemplateEvent a, ITemplateEvent b) { - return Objects.equals(a.getTemplateName(), b.getTemplateName()) - && Objects.equals(a.getLine(), b.getLine()) - && Objects.equals(a.getCol(), b.getCol()); + return Objects.equals(a.getTemplateName(), b.getTemplateName()) && Objects.equals(a.getLine(), b.getLine()) + && Objects.equals(a.getCol(), b.getCol()); } - private void processVariables(Map attrMap, - ITemplateContext context, - IElementModelStructureHandler structureHandler, - Set excludeAttr) { - for (Map.Entry entry : attrMap.entrySet()) { - if (excludeAttr.contains(entry.getKey()) || isDynamicAttribute( - entry.getKey(), this.getDialectPrefix())) { + private void processVariables(Map attributes, ITemplateContext context, + IElementModelStructureHandler structureHandler, Set excludeAttr) { + for (Map.Entry entry : attributes.entrySet()) { + if (excludeAttr.contains(entry.getKey()) || isDynamicAttribute(entry.getKey(), this.getDialectPrefix())) { continue; } - String val = entry.getValue(); - if (val == null) { - val = "${true}"; + String attributeValue = entry.getValue(); + if (attributeValue == null) { + attributeValue = "${true}"; } - WithHelper.processWith(context, entry.getKey() + "=" + val, - structureHandler); + WithHelper.processWith(context, entry.getKey() + "=" + attributeValue, structureHandler, false); } } private Map processAttribute(IProcessableElementTag tag) { - Map attMap = new HashMap<>(); + Map attributes = new HashMap<>(); if (tag != null) { for (final IAttribute attribute : tag.getAllAttributes()) { String completeName = attribute.getAttributeCompleteName(); if (!isDynamicAttribute(completeName, StandardDialect.PREFIX)) { - attMap.put(completeName, attribute.getValue()); + attributes.put(completeName, attribute.getValue()); } } } - return attMap; + return attributes; } private boolean isDynamicAttribute(String attribute, String prefix) { - return attribute.startsWith(prefix + ":") - || attribute.startsWith("data-" + prefix + "-"); + return attribute.startsWith(prefix + ":") || attribute.startsWith("data-" + prefix + "-"); } private IModel mergeModels(IModel base, IModel insert, String replaceTag) { @@ -159,7 +147,7 @@ private IModel mergeModels(IModel base, IModel insert, String replaceTag) { private IModel insertModel(IModel base, IModel insert, String replaceTag) { IModel clonedModel = base.cloneModel(); - int index = findTag(base, replaceTag, IElementTag.class); + int index = findTagIndex(base, replaceTag, IElementTag.class); if (index > -1) { clonedModel.insertModel(index, insert); } @@ -168,30 +156,28 @@ private IModel insertModel(IModel base, IModel insert, String replaceTag) { private IModel removeTag(IModel model, final String tag) { IModel clonedModel = model.cloneModel(); - int index = findTag(model, tag, IElementTag.class); + int index = findTagIndex(model, tag, IElementTag.class); if (index > -1) { clonedModel.remove(index); } return clonedModel; } - private int findTag(IModel model, final String search, Class clazz) { + private int findTagIndex(IModel model, final String search, Class clazz) { int size = model.size(); ITemplateEvent event = null; for (int i = 0; i < size; i++) { event = model.get(i); - if ((clazz == null || clazz.isInstance(event)) - && event.toString().contains(search)) { + if ((clazz == null || clazz.isInstance(event)) && event.toString().contains(search)) { return i; } } return -1; } - private IModel replaceAllAttributeValues(Map attributes, - ITemplateContext context, IModel model) { - Map replaceAttributes = findAllAttributesStartsWith( - attributes, super.getDialectPrefix(), "repl-", true); + private IModel replaceAllAttributeValues(Map attributes, ITemplateContext context, IModel model) { + Map replaceAttributes = findAllAttributesStartsWith(attributes, super.getDialectPrefix(), + "repl-", true); if (replaceAttributes.isEmpty()) { return model; @@ -199,8 +185,7 @@ private IModel replaceAllAttributeValues(Map attributes, IModel clonedModel = model.cloneModel(); int size = model.size(); for (int i = 0; i < size; i++) { - ITemplateEvent replacedEvent = replaceAttributeValue(context, - clonedModel.get(i), replaceAttributes); + ITemplateEvent replacedEvent = replaceAttributeValue(context, clonedModel.get(i), replaceAttributes); if (replacedEvent != null) { clonedModel.replace(i, replacedEvent); } @@ -209,46 +194,38 @@ private IModel replaceAllAttributeValues(Map attributes, return clonedModel; } - private ITemplateEvent replaceAttributeValue(ITemplateContext context, - ITemplateEvent model, Map replaceValueMap) { + private ITemplateEvent replaceAttributeValue(ITemplateContext context, ITemplateEvent model, + Map replaceValueMap) { IProcessableElementTag firstEvent = null; - if (!replaceValueMap.isEmpty() - && model instanceof IProcessableElementTag) { + if (!replaceValueMap.isEmpty() && model instanceof IProcessableElementTag processableTag) { final IModelFactory modelFactory = context.getModelFactory(); - firstEvent = (IProcessableElementTag) model; - for (Map.Entry entry : firstEvent.getAttributeMap() - .entrySet()) { + firstEvent = processableTag; + for (Map.Entry entry : firstEvent.getAttributeMap().entrySet()) { String oldAttrValue = entry.getValue(); String replacePart = getReplaceAttributePart(oldAttrValue); - if (replacePart != null - && replaceValueMap.containsKey(replacePart)) { - String newStringValue = - oldAttrValue.replace("?[" + replacePart + "]", - replaceValueMap.get(replacePart)); - firstEvent = modelFactory.replaceAttribute(firstEvent, - AttributeNames.forTextName(entry.getKey()), - entry.getKey(), newStringValue); + if (replacePart != null && replaceValueMap.containsKey(replacePart)) { + String newStringValue = oldAttrValue.replace("?[" + replacePart + "]", + replaceValueMap.get(replacePart)); + firstEvent = modelFactory.replaceAttribute(firstEvent, AttributeNames.forTextName(entry.getKey()), + entry.getKey(), newStringValue); } } } return firstEvent; } - private Map findAllAttributesStartsWith( - final Map attributes, final String prefix, - final String attributeName, boolean removeStart) { + private Map findAllAttributesStartsWith(final Map attributes, final String prefix, + final String attributeName, boolean removeStart) { Map matchingAttributes = new HashMap<>(); for (Map.Entry entry : attributes.entrySet()) { String key = entry.getKey(); String value = entry.getValue(); if (key.startsWith(prefix + ":" + attributeName) - || key.startsWith("data-" + prefix + "-" + attributeName)) { + || key.startsWith("data-" + prefix + "-" + attributeName)) { if (removeStart) { - key = key.replaceAll("^" + prefix + ":" + attributeName, - ""); - key = key.replaceAll( - "^data-" + prefix + "-" + attributeName, ""); + key = key.replaceAll("^" + prefix + ":" + attributeName, ""); + key = key.replaceAll("^data-" + prefix + "-" + attributeName, ""); } matchingAttributes.put(key, value); } @@ -257,10 +234,11 @@ private Map findAllAttributesStartsWith( } private String getReplaceAttributePart(String attributeValue) { - Pattern pattern = Pattern.compile(".*\\?\\[([\\w|\\d|.|\\-|_]*)\\].*"); - Matcher matcher = pattern.matcher(attributeValue); + Matcher matcher = REPLACE_PATTERN.matcher(attributeValue); while (matcher.find()) { - return matcher.group(1); + if (matcher.group(1) != null && !matcher.group(1).isEmpty()) { + return matcher.group(1); + } } return null; } diff --git a/src/main/java/de/morphbit/thymeleaf/processor/OnceAttributeTagProcessor.java b/src/main/java/de/morphbit/thymeleaf/processor/OnceAttributeTagProcessor.java index 42a0996..1e79b71 100644 --- a/src/main/java/de/morphbit/thymeleaf/processor/OnceAttributeTagProcessor.java +++ b/src/main/java/de/morphbit/thymeleaf/processor/OnceAttributeTagProcessor.java @@ -23,8 +23,7 @@ import org.thymeleaf.standard.processor.AbstractStandardConditionalVisibilityTagProcessor; import org.thymeleaf.templatemode.TemplateMode; -public class OnceAttributeTagProcessor - extends AbstractStandardConditionalVisibilityTagProcessor { +public class OnceAttributeTagProcessor extends AbstractStandardConditionalVisibilityTagProcessor { public static final int PRECEDENCE = 300; public static final String ATTR_NAME = "once"; @@ -40,9 +39,8 @@ public OnceAttributeTagProcessor(String dialectPrefix) { } @Override - protected boolean isVisible(ITemplateContext context, - IProcessableElementTag tag, AttributeName attributeName, - String attributeValue) { + protected boolean isVisible(ITemplateContext context, IProcessableElementTag tag, AttributeName attributeName, + String attributeValue) { Ids ids = new Ids(context); String id = ids.seq(attributeValue); return (attributeValue + "1").equals(id); diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml deleted file mode 100644 index e69de29..0000000 diff --git a/src/test/java/de/morphbit/thymeleaf/base/AbstractThymeleafComponentDialectTest.java b/src/test/java/de/morphbit/thymeleaf/base/AbstractThymeleafComponentDialectTest.java new file mode 100644 index 0000000..200fbb3 --- /dev/null +++ b/src/test/java/de/morphbit/thymeleaf/base/AbstractThymeleafComponentDialectTest.java @@ -0,0 +1,41 @@ +package de.morphbit.thymeleaf.base; + +import de.morphbit.thymeleaf.dialect.ComponentDialect; +import de.morphbit.thymeleaf.parser.StandardThymeleafComponentParser; +import org.junit.jupiter.api.BeforeAll; +import org.thymeleaf.TemplateEngine; +import org.thymeleaf.context.Context; +import org.thymeleaf.templatemode.TemplateMode; +import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver; + +public abstract class AbstractThymeleafComponentDialectTest { + + private static final String BASE_PATH = "templates"; + + private static TemplateEngine templateEngine; + + @BeforeAll + public static void beforeClass() { + setupThymeleaf(); + } + + protected String processThymeleafFile(String fileName, Context context) { + return templateEngine.process(BASE_PATH + "/" + fileName, context); + } + + private static void setupThymeleaf() { + ClassLoaderTemplateResolver templateResolver = new ClassLoaderTemplateResolver(); + templateResolver.setCacheable(false); + templateResolver.setCharacterEncoding("UTF-8"); + templateResolver.setTemplateMode(TemplateMode.HTML); + templateResolver.setSuffix(".html"); + + templateEngine = new TemplateEngine(); + templateEngine.setTemplateResolver(templateResolver); + final ComponentDialect dialect = new ComponentDialect(); + final StandardThymeleafComponentParser parser = new StandardThymeleafComponentParser("", ".html", + "templates/components"); + dialect.addParser(parser); + templateEngine.addDialect(dialect.getPrefix(), dialect); + } +} diff --git a/src/test/java/de/morphbit/thymeleaf/dialect/AttributeReplaceTest.java b/src/test/java/de/morphbit/thymeleaf/dialect/AttributeReplaceTest.java new file mode 100644 index 0000000..5d069a9 --- /dev/null +++ b/src/test/java/de/morphbit/thymeleaf/dialect/AttributeReplaceTest.java @@ -0,0 +1,43 @@ +package de.morphbit.thymeleaf.dialect; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import de.morphbit.thymeleaf.base.AbstractThymeleafComponentDialectTest; +import java.util.Collections; +import java.util.Locale; +import org.junit.jupiter.api.Test; +import org.thymeleaf.context.Context; + +public class AttributeReplaceTest extends AbstractThymeleafComponentDialectTest { + + private static final String FILE = "repl_test.html"; + + public static class User { + + private String name; + + public User(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + } + + @Test + public void itShouldReplaceAllReplAttributes() { + String html = processThymeleafFile(FILE, + new Context(Locale.ENGLISH, Collections.singletonMap("user", new User("John")))); + + assertNotNull(html); + assertFalse(html.matches(".*\\?\\[([\\w|\\d|.|\\-|_]*)\\].*")); + } + +} diff --git a/src/test/java/de/morphbit/thymeleaf/dialect/ComponentDialectTest.java b/src/test/java/de/morphbit/thymeleaf/dialect/ComponentDialectTest.java new file mode 100644 index 0000000..7e60a36 --- /dev/null +++ b/src/test/java/de/morphbit/thymeleaf/dialect/ComponentDialectTest.java @@ -0,0 +1,71 @@ +package de.morphbit.thymeleaf.dialect; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import de.morphbit.thymeleaf.model.ThymeleafComponent; +import java.util.HashSet; +import java.util.Set; +import org.junit.jupiter.api.Test; +import org.thymeleaf.TemplateEngine; +import org.thymeleaf.context.Context; +import org.thymeleaf.processor.IProcessor; +import org.thymeleaf.templatemode.TemplateMode; +import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver; + +public class ComponentDialectTest { + + @Test + public void itShouldHaveCorrectPrefixAndName() { + ComponentDialect dialect = new ComponentDialect(); + assertEquals("tc", dialect.getPrefix()); + assertEquals("Component Dialect", dialect.getName()); + } + + @Test + public void itShouldRegisterManualComponents() { + Set components = new HashSet<>(); + components.add(new ThymeleafComponent("my-comp", "templates/test :: fragment")); + + ComponentDialect dialect = new ComponentDialect(components); + Set processors = dialect.getProcessors("tc"); + + // OnceAttributeTagProcessor + 1 manual component + assertTrue(processors.size() >= 2); + } + + @Test + public void itShouldWorkWithManuallyRegisteredComponent() { + ClassLoaderTemplateResolver templateResolver = new ClassLoaderTemplateResolver(); + templateResolver.setCacheable(false); + templateResolver.setCharacterEncoding("UTF-8"); + templateResolver.setTemplateMode(TemplateMode.HTML); + templateResolver.setSuffix(".html"); + + TemplateEngine engine = new TemplateEngine(); + engine.setTemplateResolver(templateResolver); + + Set components = new HashSet<>(); + components.add(new ThymeleafComponent("link", "templates/components/link_component :: link")); + + ComponentDialect dialect = new ComponentDialect(components); + engine.addDialect(dialect.getPrefix(), dialect); + + String html = engine.process("templates/link_with_content", new Context()); + + assertNotNull(html); + assertFalse(html.contains("tc:link")); + assertTrue(html.contains("")); + } + + @Test + public void itShouldReturnOnceProcessorWithEmptyDialect() { + ComponentDialect dialect = new ComponentDialect(); + Set processors = dialect.getProcessors("tc"); + + assertFalse(processors.isEmpty()); + assertEquals(1, processors.size()); + } +} diff --git a/src/test/java/de/morphbit/thymeleaf/dialect/ContentReplaceTest.java b/src/test/java/de/morphbit/thymeleaf/dialect/ContentReplaceTest.java new file mode 100644 index 0000000..0ccdaf1 --- /dev/null +++ b/src/test/java/de/morphbit/thymeleaf/dialect/ContentReplaceTest.java @@ -0,0 +1,37 @@ +package de.morphbit.thymeleaf.dialect; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import de.morphbit.thymeleaf.base.AbstractThymeleafComponentDialectTest; +import org.junit.jupiter.api.Test; +import org.thymeleaf.context.Context; + +public class ContentReplaceTest extends AbstractThymeleafComponentDialectTest { + + private static final String FILE_LINK_WITH_CONTENT = "link_with_content.html"; + private static final String FILE_LINK_WITH_CONTENT_AND_VARIABLE = "link_with_content_and_variable.html"; + + @Test + public void itShouldNotRenderNamespaceTagsAndReplaceContent() { + String html = processThymeleafFile(FILE_LINK_WITH_CONTENT, new Context()); + + assertNotNull(html); + assertFalse(html.contains("tc:link")); + assertFalse(html.contains("tc:content")); + assertTrue(html.contains("")); + assertTrue(html.contains("Test")); + assertFalse(html.contains(">> ")); + } + + @Test + public void itShouldReplaceContentAndConsiderVariable() { + String html = processThymeleafFile(FILE_LINK_WITH_CONTENT_AND_VARIABLE, new Context()); + + assertNotNull(html); + assertTrue(html.contains("")); + assertTrue(html.contains("Test")); + assertTrue(html.contains(">> ")); + } +} diff --git a/src/test/java/de/morphbit/thymeleaf/dialect/MultipleComponentsTest.java b/src/test/java/de/morphbit/thymeleaf/dialect/MultipleComponentsTest.java new file mode 100644 index 0000000..4a0a3e4 --- /dev/null +++ b/src/test/java/de/morphbit/thymeleaf/dialect/MultipleComponentsTest.java @@ -0,0 +1,62 @@ +package de.morphbit.thymeleaf.dialect; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import de.morphbit.thymeleaf.base.AbstractThymeleafComponentDialectTest; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.junit.jupiter.api.Test; +import org.thymeleaf.context.Context; + +public class MultipleComponentsTest extends AbstractThymeleafComponentDialectTest { + + @Test + public void itShouldRenderMultipleComponentsOnSamePage() { + String html = processThymeleafFile("multiple_components.html", new Context()); + + assertNotNull(html); + assertFalse(html.contains("tc:alert")); + assertFalse(html.contains("tc:card")); + assertFalse(html.contains("tc:content")); + + // Two alert components + assertTrue(html.contains("First")); + assertTrue(html.contains("Second")); + assertTrue(html.contains("Content 1")); + assertTrue(html.contains("Content 2")); + + // One card component + assertTrue(html.contains("My Card")); + assertTrue(html.contains("

Card content

")); + } + + @Test + public void itShouldRenderCorrectAlertTypes() { + String html = processThymeleafFile("multiple_components.html", new Context()); + + assertNotNull(html); + assertTrue(html.contains("alert-success")); + assertTrue(html.contains("alert-danger")); + } + + @Test + public void itShouldRenderCorrectNumberOfAlerts() { + String html = processThymeleafFile("multiple_components.html", new Context()); + + assertNotNull(html); + int alertCount = countMatches("class=\"alert", html); + assertTrue(alertCount == 2, "Expected 2 alerts but found " + alertCount); + } + + private int countMatches(String text, String search) { + int count = 0; + Pattern pattern = Pattern.compile(Pattern.quote(text)); + Matcher matcher = pattern.matcher(search); + while (matcher.find()) { + count++; + } + return count; + } +} diff --git a/src/test/java/de/morphbit/thymeleaf/dialect/NamedSelectorTest.java b/src/test/java/de/morphbit/thymeleaf/dialect/NamedSelectorTest.java new file mode 100644 index 0000000..7db07cb --- /dev/null +++ b/src/test/java/de/morphbit/thymeleaf/dialect/NamedSelectorTest.java @@ -0,0 +1,23 @@ +package de.morphbit.thymeleaf.dialect; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import de.morphbit.thymeleaf.base.AbstractThymeleafComponentDialectTest; +import org.junit.jupiter.api.Test; +import org.thymeleaf.context.Context; + +public class NamedSelectorTest extends AbstractThymeleafComponentDialectTest { + + @Test + public void itShouldResolveComponentByNamedSelector() { + String html = processThymeleafFile("named_selector.html", new Context()); + + assertNotNull(html); + assertFalse(html.contains("tc:link-named")); + assertFalse(html.contains("tc:content")); + assertTrue(html.contains("
")); + assertTrue(html.contains("Named Link")); + } +} diff --git a/src/test/java/de/morphbit/thymeleaf/dialect/NestedComponentTest.java b/src/test/java/de/morphbit/thymeleaf/dialect/NestedComponentTest.java new file mode 100644 index 0000000..afc8a43 --- /dev/null +++ b/src/test/java/de/morphbit/thymeleaf/dialect/NestedComponentTest.java @@ -0,0 +1,27 @@ +package de.morphbit.thymeleaf.dialect; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import de.morphbit.thymeleaf.base.AbstractThymeleafComponentDialectTest; +import org.junit.jupiter.api.Test; +import org.thymeleaf.context.Context; + +public class NestedComponentTest extends AbstractThymeleafComponentDialectTest { + + @Test + public void itShouldRenderNestedComponents() { + String html = processThymeleafFile("nested_components.html", new Context()); + + assertNotNull(html); + assertFalse(html.contains("tc:card")); + assertFalse(html.contains("tc:alert")); + assertFalse(html.contains("tc:content")); + assertTrue(html.contains("class=\"card\"")); + assertTrue(html.contains("Outer Card")); + assertTrue(html.contains("class=\"alert")); + assertTrue(html.contains("Nested Alert")); + assertTrue(html.contains("Nested content")); + } +} diff --git a/src/test/java/de/morphbit/thymeleaf/dialect/OnceAttributeTest.java b/src/test/java/de/morphbit/thymeleaf/dialect/OnceAttributeTest.java new file mode 100644 index 0000000..7f32948 --- /dev/null +++ b/src/test/java/de/morphbit/thymeleaf/dialect/OnceAttributeTest.java @@ -0,0 +1,35 @@ +package de.morphbit.thymeleaf.dialect; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import de.morphbit.thymeleaf.base.AbstractThymeleafComponentDialectTest; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.junit.jupiter.api.Test; +import org.thymeleaf.context.Context; + +public class OnceAttributeTest extends AbstractThymeleafComponentDialectTest { + + private static final String FILE = "once_test.html"; + + @Test + public void itShouldHaveMultipleComponentsButOneScript() { + String html = processThymeleafFile(FILE, new Context()); + + assertNotNull(html); + assertTrue(countMatches("\\ + + + + + + \ No newline at end of file diff --git a/src/test/resources/templates/components/valid_input.html b/src/test/resources/templates/components/valid_input.html new file mode 100644 index 0000000..987279c --- /dev/null +++ b/src/test/resources/templates/components/valid_input.html @@ -0,0 +1,16 @@ + + + + + + +
+ + +
+ + + \ No newline at end of file diff --git a/src/test/resources/templates/link_with_content.html b/src/test/resources/templates/link_with_content.html new file mode 100644 index 0000000..c2e90dc --- /dev/null +++ b/src/test/resources/templates/link_with_content.html @@ -0,0 +1,15 @@ + + + + Test + + + + + Test + + + + \ No newline at end of file diff --git a/src/test/resources/templates/link_with_content_and_variable.html b/src/test/resources/templates/link_with_content_and_variable.html new file mode 100644 index 0000000..e2f15bc --- /dev/null +++ b/src/test/resources/templates/link_with_content_and_variable.html @@ -0,0 +1,15 @@ + + + + Test + + + + + Test + + + + \ No newline at end of file diff --git a/src/test/resources/templates/multiple_components.html b/src/test/resources/templates/multiple_components.html new file mode 100644 index 0000000..6a9aed9 --- /dev/null +++ b/src/test/resources/templates/multiple_components.html @@ -0,0 +1,23 @@ + + + + Test + + + + + Content 1 + + + + Content 2 + + + +

Card content

+
+ + + \ No newline at end of file diff --git a/src/test/resources/templates/named_selector.html b/src/test/resources/templates/named_selector.html new file mode 100644 index 0000000..a3628ad --- /dev/null +++ b/src/test/resources/templates/named_selector.html @@ -0,0 +1,15 @@ + + + + Test + + + + + Named Link + + + + \ No newline at end of file diff --git a/src/test/resources/templates/nested_components.html b/src/test/resources/templates/nested_components.html new file mode 100644 index 0000000..2fb23d2 --- /dev/null +++ b/src/test/resources/templates/nested_components.html @@ -0,0 +1,17 @@ + + + + Test + + + + + + Nested content + + + + + \ No newline at end of file diff --git a/src/test/resources/templates/once_test.html b/src/test/resources/templates/once_test.html new file mode 100644 index 0000000..d09b94e --- /dev/null +++ b/src/test/resources/templates/once_test.html @@ -0,0 +1,18 @@ + + + + Test + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/resources/templates/repl_test.html b/src/test/resources/templates/repl_test.html new file mode 100644 index 0000000..eee0b51 --- /dev/null +++ b/src/test/resources/templates/repl_test.html @@ -0,0 +1,15 @@ + + + + Test + + + +
+ + + + + \ No newline at end of file diff --git a/src/test/resources/templates/self_closing_component.html b/src/test/resources/templates/self_closing_component.html new file mode 100644 index 0000000..34ee458 --- /dev/null +++ b/src/test/resources/templates/self_closing_component.html @@ -0,0 +1,13 @@ + + + + Test + + + + + + + \ No newline at end of file