diff --git a/CHANGELOG.md b/CHANGELOG.md index b3ebb11..be1bc3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ All notable changes to `knotx-template-engine` will be documented in this file. ## Unreleased List of changes that are finished but not yet released in any final version. - +- [PR-21](https://github.com/Knotx/knotx-template-engine/pull/21) - Pebble Template Engine ## 2.1.0 - [PR-18](https://github.com/Knotx/knotx-template-engine/pull/18) - `com.github.jknack` Handlebars updated to `4.1.2`. diff --git a/README.md b/README.md index 0feae9b..e9cd4f0 100644 --- a/README.md +++ b/README.md @@ -53,8 +53,8 @@ together with Transition. ## How to use > Please note that example below uses Handlebars to process the markup. Read more about it below. -> You may read about it in the [Handlebars module docs](https://github.com/Knotx/knotx-template-engine/tree/master/handlebars) -> If you want to use another template engine, please refer to [core module docs](https://github.com/Knotx/knotx-template-engine/tree/master/core). +> You may read about it in the [Handlebars module docs](https://github.com/Knotx/knotx-template-engine/tree/master/handlebars). +> For using a different template engine, refer to [pebble module docs]() or [core module docs](https://github.com/Knotx/knotx-template-engine/tree/master/core). Define a module that creates `io.knotx.te.core.TemplateEngineKnot` instance. @@ -92,9 +92,10 @@ Now you may use it in Fragment's Tasks. Example configuration is available in the [conf](https://github.com/Knotx/knotx-template-engine/blob/master/conf) section of this module. -## Handlebars Template Engine Strategy -Currently this repository delivers `handlebars` TE strategy implementation. -You may read more bout it in the [module docs](https://github.com/Knotx/knotx-template-engine/tree/master/handlebars). +## OOTB Template Engine Strategies +Currently this repository delivers `handlebars` and `pebble` TE strategies implementation. +You may read more in the [handlebars](https://github.com/Knotx/knotx-template-engine/tree/master/handlebars) and [pebble](https://github.com/Knotx/knotx-template-engine/tree/master/pebble) module docs. + ### How to create a custom Template Engine Strategy Read more about it in the [API module docs](https://github.com/Knotx/knotx-template-engine/tree/master/api). diff --git a/gradlew.bat b/gradlew.bat index 24467a1..9618d8d 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,100 +1,100 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/pebble/README.md b/pebble/README.md new file mode 100644 index 0000000..52a2396 --- /dev/null +++ b/pebble/README.md @@ -0,0 +1,40 @@ +# Knot.x Template Engine Pebble +This section describes Pebble template engine strategy that is an alternative implementation of the +template engine used in Knot.x examples. + + +## How does it work +Template Engine Pebble uses +[Pebble Templates](https://pebbletemplates.io/) to compile and evaluate templates. +Please refer to its documentation for any details. +Additionally, Knot.x Template Engine Pebble has built-in Guava in-memory cache for compiled PEB +Templates. Key computation is performed using the `cacheKeyAlgorithm` defined in the configuration +(the default is `MD5` of the Fragment's `body`). + +## How to configure +For all configuration fields and their defaults consult [io.knotx.te.pebble.options.PebbleEngineOptions](https://github.com/Knotx/knotx-template-engine/blob/master/pebble/docs/asciidoc/dataobjects.adoc) + +### Custom delimiters +By default, the Pebble engine uses `{{` and `}}` symbols as print delimiters, `{%` and `%}` as computation delimiters, `{#` and `#}` as comment delimiters and `#{` and `}` as string interpolation delimiters. +Knot.x Template Engine Pebble allows to change them using configuration + +E.g.: +In order to use different symbols as below +```html +
+

Snippet1 - {: message :}

+
+``` +You can reconfigure an engine as follows in the pebble engine entry section: +```hocon + { + factory = pebble + config = { + cacheSize = 1000 + syntax = { + delimiterPrintOpen = "{:" + delimiterPrintClose = ":}" + } + } + } +``` diff --git a/pebble/build.gradle.kts b/pebble/build.gradle.kts new file mode 100644 index 0000000..10cd3f5 --- /dev/null +++ b/pebble/build.gradle.kts @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2019 Knot.x Project + * + * 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. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import org.nosphere.apache.rat.RatTask + +plugins { + id("io.knotx.java-library") + id("io.knotx.unit-test") + id("io.knotx.maven-publish") + id("io.knotx.codegen") + id("io.knotx.jacoco") + + id("org.nosphere.apache.rat") version "0.4.0" +} + +dependencies { + implementation(platform("io.knotx:knotx-dependencies:${project.version}")) + implementation("io.knotx:knotx-commons:${project.version}") + + api(project(":knotx-template-engine-api")) + + implementation(group = "io.vertx", name = "vertx-core") + implementation(group = "io.vertx", name = "vertx-service-proxy") + implementation(group = "io.vertx", name = "vertx-rx-java2") + implementation(group = "com.google.guava", name = "guava") + implementation(group = "io.pebbletemplates", name= "pebble", version = "3.1.2") + + testImplementation("io.knotx:knotx-junit5:${project.version}") + testImplementation("org.junit.jupiter:junit-jupiter-params") + testImplementation(group = "org.mockito", name = "mockito-core") + testImplementation(group = "org.mockito", name = "mockito-junit-jupiter") +} + +tasks { + named("rat") { + excludes.addAll("*.md", "**/build/*", "**/out/*", "**/generated/*", "**/*.adoc", "**/resources/*", "gradle.properties") + } + getByName("build").dependsOn("rat") +} + +publishing { + publications { + withType(MavenPublication::class) { + from(components["java"]) + artifact(tasks["sourcesJar"]) + artifact(tasks["javadocJar"]) + } + } +} diff --git a/pebble/docs/asciidoc/dataobjects.adoc b/pebble/docs/asciidoc/dataobjects.adoc new file mode 100644 index 0000000..c202cba --- /dev/null +++ b/pebble/docs/asciidoc/dataobjects.adoc @@ -0,0 +1,80 @@ += Cheatsheets + +[[PebbleEngineOptions]] +== PebbleEngineOptions + +++++ + Describes Pebble Knot configuration. Contains cache settings and Pebble Engine settings. +++++ +''' + +[cols=">25%,25%,50%"] +[frame="topbot"] +|=== +^|Name | Type ^| Description +|[[cacheKeyAlgorithm]]`@cacheKeyAlgorithm`|`String`|+++ +Sets the algorithm used to build a hash from the Pebble snippet. The hash is to be used as a + cache key. + + The name should be a standard Java Security name (such as "SHA", "MD5", and so on). ++++ +|[[cacheSize]]`@cacheSize`|`Number (Long)`|+++ +Sets the size of the cache. After reaching the max size, new elements will replace the oldest + one. ++++ +|[[syntax]]`@syntax`|`link:dataobjects.html#PebbleEngineSyntaxOptions[PebbleEngineSyntaxOptions]`|+++ +Sets syntax options including custom Pebble markers' delimiters. ++++ +|=== + +[[PebbleEngineSyntaxOptions]] +== PebbleEngineSyntaxOptions + +++++ + Describes custom syntax for the Pebble Engine. +++++ +''' + +[cols=">25%,25%,50%"] +[frame="topbot"] +|=== +^|Name | Type ^| Description +|[[delimiterCommentClose]]`@delimiterCommentClose`|`String`|+++ +Defaults to #} ++++ +|[[delimiterCommentOpen]]`@delimiterCommentOpen`|`String`|+++ +Defaults to {# ++++ +|[[delimiterExecuteClose]]`@delimiterExecuteClose`|`String`|+++ +Defaults to %} ++++ +|[[delimiterExecuteOpen]]`@delimiterExecuteOpen`|`String`|+++ +Defaults to {% ++++ +|[[delimiterPrintClose]]`@delimiterPrintClose`|`String`|+++ +Defaults to }} ++++ +|[[delimiterPrintOpen]]`@delimiterPrintOpen`|`String`|+++ +Defaults to {{ ++++ +|[[literalDecimalTreatedAsInteger]]`@literalDecimalTreatedAsInteger`|`Boolean`|+++ +Sets whether a literal decimal should be treated as Integer.
If false, all literal + decimals will be treated as Long.
A literal decimal is considered a numeric string without + a fraction separator (a dot).
Defaults to false. ++++ +|[[newLineTrimming]]`@newLineTrimming`|`Boolean`|+++ +Sets whether a new line after a closing Pebble tag should be trimmed. Defaults to true. ++++ +|[[strictVariables]]`@strictVariables`|`Boolean`|+++ +Sets the Strict Mode of the Pebble Engine.
In Strict Mode, referencing a null or + non-existent variable or attribute will result in an exception.
When Strict Mode is + disabled, such variables/attributes will be rendered as empty. + + Strict mode is disabled by default. ++++ +|[[whitespaceTrim]]`@whitespaceTrim`|`String`|+++ +Defaults to - Note: this is used for trimming whitespaces adjacent to a Pebble + tag, not inside the content rendered by it. ++++ +|=== + diff --git a/pebble/gradle.properties b/pebble/gradle.properties new file mode 100644 index 0000000..70b8eae --- /dev/null +++ b/pebble/gradle.properties @@ -0,0 +1,3 @@ +artifactId=knotx-template-engine-pebble +publication.name=Knot.x Template Engine Pebble +publication.description=Knot.x Template Engine Pebble diff --git a/pebble/src/main/java/io/knotx/te/pebble/PebbleEngineSyntaxComposer.java b/pebble/src/main/java/io/knotx/te/pebble/PebbleEngineSyntaxComposer.java new file mode 100644 index 0000000..44f297a --- /dev/null +++ b/pebble/src/main/java/io/knotx/te/pebble/PebbleEngineSyntaxComposer.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2019 Knot.x Project + * + * 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. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.knotx.te.pebble; + +import com.mitchellbosecke.pebble.lexer.Syntax; +import io.knotx.te.pebble.options.PebbleEngineSyntaxOptions; + +final class PebbleEngineSyntaxComposer { + + private PebbleEngineSyntaxComposer() { + // utility class + } + + static Syntax compose(PebbleEngineSyntaxOptions options) { + return new Syntax.Builder() + .setCommentOpenDelimiter(options.getDelimiterCommentOpen()) + .setCommentCloseDelimiter(options.getDelimiterCommentClose()) + .setExecuteOpenDelimiter(options.getDelimiterExecuteOpen()) + .setExecuteCloseDelimiter(options.getDelimiterExecuteClose()) + .setPrintOpenDelimiter(options.getDelimiterPrintOpen()) + .setPrintCloseDelimiter(options.getDelimiterPrintClose()) + .setWhitespaceTrim(options.getWhitespaceTrim()) + .build(); + } + +} diff --git a/pebble/src/main/java/io/knotx/te/pebble/PebbleTemplateEngine.java b/pebble/src/main/java/io/knotx/te/pebble/PebbleTemplateEngine.java new file mode 100644 index 0000000..1cc07bb --- /dev/null +++ b/pebble/src/main/java/io/knotx/te/pebble/PebbleTemplateEngine.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2019 Knot.x Project + * + * 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. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.knotx.te.pebble; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.mitchellbosecke.pebble.PebbleEngine; +import com.mitchellbosecke.pebble.loader.StringLoader; +import com.mitchellbosecke.pebble.template.PebbleTemplate; +import io.knotx.commons.json.JsonConverter; +import io.knotx.fragments.api.Fragment; +import io.knotx.te.api.TemplateEngine; +import io.knotx.te.pebble.options.PebbleEngineOptions; +import io.knotx.te.pebble.options.PebbleEngineSyntaxOptions; +import io.vertx.core.logging.Logger; +import io.vertx.core.logging.LoggerFactory; +import java.io.IOException; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Map; +import java.util.concurrent.ExecutionException; + +/** + * This class is registered with Service Provider Interface (see META-INF/services) + */ +class PebbleTemplateEngine implements TemplateEngine { + + private static final Logger LOGGER = LoggerFactory.getLogger(PebbleTemplateEngine.class); + + private final PebbleEngine pebbleEngine; + private final Cache cache; + private final MessageDigest digest; + + PebbleTemplateEngine(PebbleEngineOptions options) { + LOGGER.info("<{}> instance created", this.getClass().getSimpleName()); + this.pebbleEngine = createPebbleEngine(options.getSyntax()); + this.cache = createCache(options); + this.digest = tryToCreateDigest(options); + } + + @Override + public String process(Fragment fragment) { + PebbleTemplate template = getTemplate(fragment); + traceProcessingFragment(fragment); + return tryToProcessOnEngine(template, fragment); + } + + private PebbleTemplate getTemplate(Fragment fragment) { + try { + String cacheKey = getCacheKey(fragment); + return cache.get(cacheKey, () -> { + traceCompilingFragment(fragment); + return pebbleEngine.getTemplate(fragment.getBody()); + }); + } catch (ExecutionException e) { + LOGGER.error("Could not compile fragment [{}]", fragment.abbreviate(), e); + throw new IllegalStateException(e); + } + } + + private String tryToProcessOnEngine(PebbleTemplate template, Fragment fragment) { + try { + Map context = JsonConverter.plainMapFrom(fragment.getPayload()); + StringWriter writer = new StringWriter(); + template.evaluate(writer, context); + return writer.toString(); + } catch (IOException e) { + LOGGER.error("Could not apply context to fragment [{}]", fragment.abbreviate(), e); + throw new IllegalStateException(e); + } + } + + private String getCacheKey(Fragment fragment) { + byte[] cacheKeyBytes = digest.digest(fragment.getBody().getBytes(StandardCharsets.UTF_8)); + return new String(cacheKeyBytes); + } + + private PebbleEngine createPebbleEngine(PebbleEngineSyntaxOptions syntaxOptions) { + return new PebbleEngine.Builder() + .loader(new StringLoader()) + .cacheActive(false) + .strictVariables(syntaxOptions.isStrictVariables()) + .newLineTrimming(syntaxOptions.isNewLineTrimming()) + .syntax(PebbleEngineSyntaxComposer.compose(syntaxOptions)) + .literalDecimalTreatedAsInteger(syntaxOptions.isLiteralDecimalTreatedAsInteger()) + .build(); + } + + private Cache createCache(PebbleEngineOptions options) { + return CacheBuilder.newBuilder() + .maximumSize(options.getCacheSize()) + .removalListener(listener -> LOGGER.warn( + "Cache limit exceeded. Revisit 'cacheSize' setting")) + .build(); + } + + private MessageDigest tryToCreateDigest(PebbleEngineOptions options) { + try { + return MessageDigest.getInstance(options.getCacheKeyAlgorithm()); + } catch (NoSuchAlgorithmException e) { + LOGGER.error("No such algorithm available {}.", options.getCacheKeyAlgorithm(), e); + throw new IllegalArgumentException(e); + } + } + + private static void traceProcessingFragment(Fragment fragment) { + if (LOGGER.isTraceEnabled()) { + LOGGER.trace("Processing with Pebble: {}!", fragment); + } + } + + private static void traceCompilingFragment(Fragment fragment) { + if (LOGGER.isTraceEnabled()) { + LOGGER.trace("Compiled Pebble fragment [{}]", fragment); + } + } + +} diff --git a/pebble/src/main/java/io/knotx/te/pebble/PebbleTemplateEngineFactory.java b/pebble/src/main/java/io/knotx/te/pebble/PebbleTemplateEngineFactory.java new file mode 100644 index 0000000..36872b8 --- /dev/null +++ b/pebble/src/main/java/io/knotx/te/pebble/PebbleTemplateEngineFactory.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2019 Knot.x Project + * + * 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. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.knotx.te.pebble; + +import io.knotx.te.api.TemplateEngine; +import io.knotx.te.api.TemplateEngineFactory; +import io.knotx.te.pebble.options.PebbleEngineOptions; +import io.vertx.core.json.JsonObject; +import io.vertx.reactivex.core.Vertx; + +public class PebbleTemplateEngineFactory implements TemplateEngineFactory { + + @Override + public String getName() { + return "pebble"; + } + + @Override + public TemplateEngine create(Vertx vertx, JsonObject config) { + return new PebbleTemplateEngine(new PebbleEngineOptions(config)); + } +} diff --git a/pebble/src/main/java/io/knotx/te/pebble/options/PebbleEngineOptions.java b/pebble/src/main/java/io/knotx/te/pebble/options/PebbleEngineOptions.java new file mode 100644 index 0000000..d3ab01e --- /dev/null +++ b/pebble/src/main/java/io/knotx/te/pebble/options/PebbleEngineOptions.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2019 Knot.x Project + * + * 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. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.knotx.te.pebble.options; + +import io.vertx.codegen.annotations.DataObject; +import io.vertx.core.json.JsonObject; + +/** + * Describes Pebble Knot configuration. Contains cache settings and Pebble Engine settings. + */ +@DataObject(generateConverter = true, publicConverter = false) +public class PebbleEngineOptions { + + private String cacheKeyAlgorithm = "MD5"; + private Long cacheSize; + private PebbleEngineSyntaxOptions syntax = new PebbleEngineSyntaxOptions(); + + public PebbleEngineOptions() { + } + + public PebbleEngineOptions(JsonObject json) { + PebbleEngineOptionsConverter.fromJson(json, this); + } + + public JsonObject toJson() { + JsonObject json = new JsonObject(); + PebbleEngineOptionsConverter.toJson(this, json); + return json; + } + + /** + * @return size of the cache + */ + public Long getCacheSize() { + return cacheSize; + } + + /** + * Sets the size of the cache. After reaching the max size, new elements will replace the oldest + * one. + * + * @param cacheSize size of the cache + * @return a reference to this, so the API can be used fluently + */ + public PebbleEngineOptions setCacheSize(Long cacheSize) { + this.cacheSize = cacheSize; + return this; + } + + /** + * @return name of the algorithm used to generate hash from the Pebble snippet + */ + public String getCacheKeyAlgorithm() { + return cacheKeyAlgorithm; + } + + /** + * Sets the algorithm used to build a hash from the Pebble snippet. The hash is to be used as a + * cache key. + * + * The name should be a standard Java Security name (such as "SHA", "MD5", and so on). + * + * @param cacheKeyAlgorithm algorithm name + * @return a reference to this, so the API can be used fluently + */ + public PebbleEngineOptions setCacheKeyAlgorithm(String cacheKeyAlgorithm) { + this.cacheKeyAlgorithm = cacheKeyAlgorithm; + return this; + } + + /** + * @return syntax options with custom delimiters + */ + public PebbleEngineSyntaxOptions getSyntax() { + return syntax; + } + + /** + * Sets syntax options including custom Pebble markers' delimiters. + * + * @param syntax the syntax options to be passed to the Pebble Engine + */ + public void setSyntax( + PebbleEngineSyntaxOptions syntax) { + this.syntax = syntax; + } + + @Override + public String toString() { + return "PebbleEngineOptions{" + + "cacheKeyAlgorithm='" + cacheKeyAlgorithm + '\'' + + ", cacheSize=" + cacheSize + + ", syntax=" + syntax + + '}'; + } +} diff --git a/pebble/src/main/java/io/knotx/te/pebble/options/PebbleEngineSyntaxOptions.java b/pebble/src/main/java/io/knotx/te/pebble/options/PebbleEngineSyntaxOptions.java new file mode 100644 index 0000000..94d5732 --- /dev/null +++ b/pebble/src/main/java/io/knotx/te/pebble/options/PebbleEngineSyntaxOptions.java @@ -0,0 +1,241 @@ +/* + * Copyright (C) 2019 Knot.x Project + * + * 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. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.knotx.te.pebble.options; + +import io.vertx.codegen.annotations.DataObject; +import io.vertx.core.json.JsonObject; + +/** + * Describes custom syntax for the Pebble Engine. + * + * @see Pebble Basic Usage + * @see Pebble Engine Settings in + * Installation guide + */ +@DataObject(generateConverter = true, publicConverter = false) +public class PebbleEngineSyntaxOptions { + + private boolean strictVariables = false; + private boolean newLineTrimming = true; + private boolean literalDecimalTreatedAsInteger = false; + + private String delimiterCommentOpen = "{#"; + private String delimiterCommentClose = "#}"; + private String delimiterExecuteOpen = "{%"; + private String delimiterExecuteClose = "%}"; + private String delimiterPrintOpen = "{{"; + private String delimiterPrintClose = "}}"; + private String whitespaceTrim = "-"; + + public PebbleEngineSyntaxOptions() { + } + + public PebbleEngineSyntaxOptions(JsonObject json) { + PebbleEngineSyntaxOptionsConverter.fromJson(json, this); + } + + public JsonObject toJson() { + JsonObject json = new JsonObject(); + PebbleEngineSyntaxOptionsConverter.toJson(this, json); + return json; + } + + /** + * @return flag indicating whether strict mode should be enabled + */ + public boolean isStrictVariables() { + return strictVariables; + } + + /** + * Sets the Strict Mode of the Pebble Engine.
In Strict Mode, referencing a null or + * non-existent variable or attribute will result in an exception.
When Strict Mode is + * disabled, such variables/attributes will be rendered as empty. + * + * Strict mode is disabled by default. + * + * @param strictVariables flag indicating whether strict mode should be enabled + */ + public void setStrictVariables(boolean strictVariables) { + this.strictVariables = strictVariables; + } + + /** + * @return flag indicating whether a new line character occurring after a closing Pebble tag + * should be trimmed + */ + public boolean isNewLineTrimming() { + return newLineTrimming; + } + + /** + * Sets whether a new line after a closing Pebble tag should be trimmed. Defaults to true. + * + * @param newLineTrimming flag indicating whether a new line character occurring after a closing + * Pebble tag should be trimmed + */ + public void setNewLineTrimming(boolean newLineTrimming) { + this.newLineTrimming = newLineTrimming; + } + + /** + * @return flag indicating whether a literal decimal should be treated as Integer or Long + */ + public boolean isLiteralDecimalTreatedAsInteger() { + return literalDecimalTreatedAsInteger; + } + + /** + * Sets whether a literal decimal should be treated as Integer.
If false, all literal + * decimals will be treated as Long.
A literal decimal is considered a numeric string without + * a fraction separator (a dot).
Defaults to false. + * + * @param literalDecimalTreatedAsInteger flag indicating whether a literal decimal should be + * treated as Integer or Long + */ + public void setLiteralDecimalTreatedAsInteger(boolean literalDecimalTreatedAsInteger) { + this.literalDecimalTreatedAsInteger = literalDecimalTreatedAsInteger; + } + + /** + * @return custom delimiter for comment start + */ + public String getDelimiterCommentOpen() { + return delimiterCommentOpen; + } + + /** + * Defaults to {# + * + * @param delimiterCommentOpen custom delimiter for comment start + */ + public void setDelimiterCommentOpen(String delimiterCommentOpen) { + this.delimiterCommentOpen = delimiterCommentOpen; + } + + /** + * @return custom delimiter for comment end + */ + public String getDelimiterCommentClose() { + return delimiterCommentClose; + } + + /** + * Defaults to #} + * + * @param delimiterCommentClose custom delimiter for comment end + */ + public void setDelimiterCommentClose(String delimiterCommentClose) { + this.delimiterCommentClose = delimiterCommentClose; + } + + /** + * @return custom delimiter for execution tag start + */ + public String getDelimiterExecuteOpen() { + return delimiterExecuteOpen; + } + + /** + * Defaults to {% + * + * @param delimiterExecuteOpen custom delimiter for execution tag start + */ + public void setDelimiterExecuteOpen(String delimiterExecuteOpen) { + this.delimiterExecuteOpen = delimiterExecuteOpen; + } + + /** + * @return custom delimiter for execution tag end + */ + public String getDelimiterExecuteClose() { + return delimiterExecuteClose; + } + + /** + * Defaults to %} + * + * @param delimiterExecuteClose custom delimiter for execution tag end + */ + public void setDelimiterExecuteClose(String delimiterExecuteClose) { + this.delimiterExecuteClose = delimiterExecuteClose; + } + + /** + * @return custom delimiter for print tag start + */ + public String getDelimiterPrintOpen() { + return delimiterPrintOpen; + } + + /** + * Defaults to {{ + * + * @param delimiterPrintOpen custom delimiter for print tag start + */ + public void setDelimiterPrintOpen(String delimiterPrintOpen) { + this.delimiterPrintOpen = delimiterPrintOpen; + } + + /** + * @return custom delimiter for print tag end + */ + public String getDelimiterPrintClose() { + return delimiterPrintClose; + } + + /** + * Defaults to }} + * + * @param delimiterPrintClose custom delimiter for print tag end + */ + public void setDelimiterPrintClose(String delimiterPrintClose) { + this.delimiterPrintClose = delimiterPrintClose; + } + + /** + * @return custom character used to enable leading/trailing whitespaces trimming + */ + public String getWhitespaceTrim() { + return whitespaceTrim; + } + + /** + * Defaults to - Note: this is used for trimming whitespaces adjacent to a Pebble + * tag, not inside the content rendered by it. + * + * @param whitespaceTrim custom character used to enable leading/trailing whitespaces trimming + */ + public void setWhitespaceTrim(String whitespaceTrim) { + this.whitespaceTrim = whitespaceTrim; + } + + @Override + public String toString() { + return "PebbleEngineSyntaxOptions{" + + "strictVariables=" + strictVariables + + ", newLineTrimming=" + newLineTrimming + + ", literalDecimalTreatedAsInteger=" + literalDecimalTreatedAsInteger + + ", delimiterCommentOpen='" + delimiterCommentOpen + '\'' + + ", delimiterCommentClose='" + delimiterCommentClose + '\'' + + ", delimiterExecuteOpen='" + delimiterExecuteOpen + '\'' + + ", delimiterExecuteClose='" + delimiterExecuteClose + '\'' + + ", delimiterPrintOpen='" + delimiterPrintOpen + '\'' + + ", delimiterPrintClose='" + delimiterPrintClose + '\'' + + ", whitespaceTrim='" + whitespaceTrim + '\'' + + '}'; + } +} diff --git a/pebble/src/main/java/io/knotx/te/pebble/package-info.java b/pebble/src/main/java/io/knotx/te/pebble/package-info.java new file mode 100644 index 0000000..572e1ca --- /dev/null +++ b/pebble/src/main/java/io/knotx/te/pebble/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 Knot.x Project + * + * 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. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@ModuleGen(name = "knotx-te-pebble", groupPackage = "io.knotx") +package io.knotx.te.pebble; + +import io.vertx.codegen.annotations.ModuleGen; diff --git a/pebble/src/main/resources/META-INF/services/io.knotx.te.api.TemplateEngineFactory b/pebble/src/main/resources/META-INF/services/io.knotx.te.api.TemplateEngineFactory new file mode 100644 index 0000000..4461e89 --- /dev/null +++ b/pebble/src/main/resources/META-INF/services/io.knotx.te.api.TemplateEngineFactory @@ -0,0 +1,15 @@ +# Copyright (C) 2019 Knot.x Project +# +# 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. +# See the License for the specific language governing permissions and +# limitations under the License. + +io.knotx.te.pebble.PebbleTemplateEngineFactory diff --git a/pebble/src/test/java/io/knotx/te/pebble/PebbleTemplateEngineTest.java b/pebble/src/test/java/io/knotx/te/pebble/PebbleTemplateEngineTest.java new file mode 100644 index 0000000..f198218 --- /dev/null +++ b/pebble/src/test/java/io/knotx/te/pebble/PebbleTemplateEngineTest.java @@ -0,0 +1,215 @@ +/* + * Copyright (C) 2019 Knot.x Project + * + * 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. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.knotx.te.pebble; + +import static io.knotx.junit5.assertions.KnotxAssertions.assertEqualsIgnoreWhitespace; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.google.common.util.concurrent.UncheckedExecutionException; +import com.mitchellbosecke.pebble.error.AttributeNotFoundException; +import com.mitchellbosecke.pebble.error.ParserException; +import com.mitchellbosecke.pebble.error.RootAttributeNotFoundException; +import io.knotx.fragments.api.Fragment; +import io.knotx.junit5.util.FileReader; +import io.knotx.te.pebble.options.PebbleEngineOptions; +import io.knotx.te.pebble.options.PebbleEngineSyntaxOptions; +import io.vertx.core.json.JsonObject; +import java.io.IOException; +import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class PebbleTemplateEngineTest { + + private static final String TEMPLATE_EMPTY = "templates/empty.peb"; + private static final String TEMPLATE_SAMPLE = "templates/sample.peb"; + private static final String TEMPLATE_SERVICE = "templates/service.peb"; + private static final String TEMPLATE_SERVICE_CUSTOM_SYNTAX = "templates/serviceCustomSyntax.peb"; + private static final String TEMPLATE_UNDEFINED_HELPER = "templates/undefinedHelper.peb"; + + private static final String CONTEXT_EMPTY = "data/emptyContext.json"; + private static final String CONTEXT_SAMPLE = "data/sampleContext.json"; + private static final String CONTEXT_SAMPLE_MISSING_FIELD = "data/sampleContextMissingField.json"; + private static final String CONTEXT_SERVICE = "data/serviceContext.json"; + + private static final String RESULT_EMPTY_CONTENT = "results/emptyContent"; + private static final String RESULT_EMPTY_CONTEXT = "results/emptyContext"; + private static final String RESULT_SAMPLE = "results/sample"; + private static final String RESULT_SAMPLE_MISSING_FIELD = "results/sampleMissingField"; + private static final String RESULT_SERVICE = "results/service"; + + private PebbleEngineOptions options; + + private static Stream passingInDefaultMode() { + return Stream.of( // template, context, expectedResult + Arguments.of(TEMPLATE_EMPTY, CONTEXT_SAMPLE, RESULT_EMPTY_CONTENT), + Arguments.of(TEMPLATE_SAMPLE, CONTEXT_EMPTY, RESULT_EMPTY_CONTEXT), + Arguments.of(TEMPLATE_SAMPLE, CONTEXT_SAMPLE, RESULT_SAMPLE), + Arguments.of(TEMPLATE_SERVICE, CONTEXT_SERVICE, RESULT_SERVICE), + Arguments.of(TEMPLATE_SAMPLE, CONTEXT_SAMPLE_MISSING_FIELD, RESULT_SAMPLE_MISSING_FIELD) + ); + } + + private static Stream passingInStrictMode() { + return Stream.of( // template, context, expectedResult + Arguments.of(TEMPLATE_EMPTY, CONTEXT_SAMPLE, RESULT_EMPTY_CONTENT), + Arguments.of(TEMPLATE_SAMPLE, CONTEXT_SAMPLE, RESULT_SAMPLE), + Arguments.of(TEMPLATE_SERVICE, CONTEXT_SERVICE, RESULT_SERVICE) + ); + } + + private static Stream failingInDefaultMode() { + return Stream.of( // template, context, exception + Arguments.of(TEMPLATE_UNDEFINED_HELPER, CONTEXT_EMPTY, UncheckedExecutionException.class, + ParserException.class) + ); + } + + private static Stream failingInStrictMode() { + return Stream.of( // template, context, exception + Arguments.of(TEMPLATE_SAMPLE, CONTEXT_SAMPLE_MISSING_FIELD, AttributeNotFoundException.class, null), + Arguments.of(TEMPLATE_SAMPLE, CONTEXT_EMPTY, RootAttributeNotFoundException.class, null), + Arguments.of(TEMPLATE_UNDEFINED_HELPER, CONTEXT_EMPTY, UncheckedExecutionException.class, + ParserException.class) + ); + } + + @BeforeEach + void setUp() { + options = new PebbleEngineOptions().setCacheSize(100L); + } + + @ParameterizedTest + @MethodSource("passingInDefaultMode") + @DisplayName("Expect successful template processing in default mode") + void renderTemplateInDefaultMode(String template, String context, String expectedResult) + throws IOException { + final PebbleTemplateEngine templateEngine = new PebbleTemplateEngine(options); + + final Fragment fragment = mockFragmentFromFile(template, context); + final String result = templateEngine.process(fragment).trim(); + final String expected = FileReader.readText(expectedResult).trim(); + + assertEqualsIgnoreWhitespace(expected, result); + } + + @ParameterizedTest + @MethodSource("failingInDefaultMode") + @DisplayName("Expect exception to be thrown when template/context input is invalid in default mode") + void renderTemplateInDefaultMode(String template, String context, + Class expectedRootException, Class expectedNestedException) + throws IOException { + final PebbleTemplateEngine templateEngine = new PebbleTemplateEngine(options); + + final Fragment fragment = mockFragmentFromFile(template, context); + + Throwable exception = assertThrows(expectedRootException, + () -> templateEngine.process(fragment)); + + if (expectedNestedException != null) { + assertEquals(expectedNestedException, exception.getCause().getClass()); + } + } + + @ParameterizedTest + @MethodSource("passingInStrictMode") + @DisplayName("Expect successful template processing in strict mode") + void renderTemplateInStrictMode(String template, String context, String expectedResult) + throws IOException { + options.getSyntax().setStrictVariables(true); + final PebbleTemplateEngine templateEngine = new PebbleTemplateEngine(options); + + final Fragment fragment = mockFragmentFromFile(template, context); + final String result = templateEngine.process(fragment).trim(); + final String expected = FileReader.readText(expectedResult).trim(); + + assertEqualsIgnoreWhitespace(expected, result); + } + + @ParameterizedTest + @MethodSource("failingInStrictMode") + @DisplayName("Expect exception to be thrown when template/context input is invalid in strict mode") + void renderTemplateInStrictMode(String template, String context, + Class expectedRootException, Class expectedNestedException) + throws IOException { + options.getSyntax().setStrictVariables(true); + final PebbleTemplateEngine templateEngine = new PebbleTemplateEngine(options); + + final Fragment fragment = mockFragmentFromFile(template, context); + + Throwable exception = assertThrows(expectedRootException, + () -> templateEngine.process(fragment)); + + if (expectedNestedException != null) { + assertEquals(expectedNestedException, exception.getCause().getClass()); + } + } + + @Test + @DisplayName("Expect template with custom syntax to be filled properly in default mode") + void renderTemplateWithCustomDelimitersInDefaultMode() throws IOException { + options.setSyntax(getCustomSyntaxOptions()); + final PebbleTemplateEngine templateEngine = new PebbleTemplateEngine(options); + + final Fragment fragment = mockFragmentFromFile(TEMPLATE_SERVICE_CUSTOM_SYNTAX, CONTEXT_SERVICE); + final String result = templateEngine.process(fragment).trim(); + final String expected = FileReader.readText(RESULT_SERVICE).trim(); + + assertEqualsIgnoreWhitespace(expected, result); + } + + @Test + @DisplayName("Expect template with custom syntax to be filled properly in strict mode") + void renderTemplateWithCustomDelimitersInStrictMode() throws IOException { + PebbleEngineSyntaxOptions syntaxOptions = getCustomSyntaxOptions(); + syntaxOptions.setStrictVariables(true); + options.setSyntax(syntaxOptions); + final PebbleTemplateEngine templateEngine = new PebbleTemplateEngine(options); + + final Fragment fragment = mockFragmentFromFile(TEMPLATE_SERVICE_CUSTOM_SYNTAX, CONTEXT_SERVICE); + final String result = templateEngine.process(fragment).trim(); + final String expected = FileReader.readText(RESULT_SERVICE).trim(); + + assertEqualsIgnoreWhitespace(expected, result); + } + + private Fragment mockFragmentFromFile(String bodyFilePath, String contextFilePath) + throws IOException { + final String body = FileReader.readText(bodyFilePath).trim(); + final String context = FileReader.readText(contextFilePath).trim(); + + Fragment fragment = new Fragment("snippet", new JsonObject(), body); + fragment.mergeInPayload(new JsonObject(context)); + return fragment; + } + + private PebbleEngineSyntaxOptions getCustomSyntaxOptions() { + PebbleEngineSyntaxOptions customSyntaxOptions = new PebbleEngineSyntaxOptions(); + customSyntaxOptions.setDelimiterCommentOpen("/*"); + customSyntaxOptions.setDelimiterCommentClose("*/"); + customSyntaxOptions.setDelimiterExecuteOpen("<<"); + customSyntaxOptions.setDelimiterExecuteClose(">>"); + customSyntaxOptions.setDelimiterPrintOpen("<|"); + customSyntaxOptions.setDelimiterPrintClose("|>"); + return customSyntaxOptions; + } + +} diff --git a/pebble/src/test/resources/data/emptyContext.json b/pebble/src/test/resources/data/emptyContext.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/pebble/src/test/resources/data/emptyContext.json @@ -0,0 +1 @@ +{} diff --git a/pebble/src/test/resources/data/sampleContext.json b/pebble/src/test/resources/data/sampleContext.json new file mode 100644 index 0000000..ca9599d --- /dev/null +++ b/pebble/src/test/resources/data/sampleContext.json @@ -0,0 +1,38 @@ +{ + "sample": { + "result": { + "first": "First Message", + "second": { + "foo": "Very Long Second Foo Message !! 2 &&%^$" + } + }, + "arr": [ + 1, + 2, + 3, + "foo", + "bar", + true + ] + }, + "items": [ + { + "title": "First item", + "content": "First item's content", + "category": "Category 1" + }, + { + "title": "Second item", + "content": "Second item's content", + "category": "Category 2" + }, + { + "title": "Third item", + "content": "Third item's content", + "category": "Undefined" + }, + { + "title": "Item without category or content" + } + ] +} diff --git a/pebble/src/test/resources/data/sampleContextMissingField.json b/pebble/src/test/resources/data/sampleContextMissingField.json new file mode 100644 index 0000000..cc39c49 --- /dev/null +++ b/pebble/src/test/resources/data/sampleContextMissingField.json @@ -0,0 +1,15 @@ +{ + "sample": { + "result": { + "first": "First Message" + }, + "arr": [ + 1, + 2, + 3, + "foo", + "bar", + true + ] + } +} diff --git a/pebble/src/test/resources/data/serviceContext.json b/pebble/src/test/resources/data/serviceContext.json new file mode 100644 index 0000000..3ba5105 --- /dev/null +++ b/pebble/src/test/resources/data/serviceContext.json @@ -0,0 +1,24 @@ +{ + "items": [ + { + "swatches": [ + { + "all_swatch_ids": "PX1|PC2|PG1", + "all_swatch_links": "http://my-website.com/products/1|http://my-website.com/products/2|http://my-website.com/products/3", + "all_swatch_images": "http://my-images.com/swatch1.jpg|http://my-images.com/swatch2.jpg|http://my-images.com/swatch3.jpg", + "all_swatch_names": "Product 1|Product 2|Product 3", + "description": "Sample product 1 description", + "title": "My product 1" + }, + { + "all_swatch_ids": "A1|A2|A3", + "all_swatch_links": "http://my-website.com/products/1|http://my-website.com/products/2|http://my-website.com/products/3", + "all_swatch_images": "http://my-images.com/swatch1.jpg|http://my-images.com/swatch2.jpg|http://my-images.com/swatch3.jpg", + "all_swatch_names": "Product 1|Product 2|Product 3", + "description": "Sample product 2 description", + "title": "My product 2" + } + ] + } + ] +} diff --git a/pebble/src/test/resources/results/emptyContent b/pebble/src/test/resources/results/emptyContent new file mode 100644 index 0000000..e69de29 diff --git a/pebble/src/test/resources/results/emptyContext b/pebble/src/test/resources/results/emptyContext new file mode 100644 index 0000000..5ed7baf --- /dev/null +++ b/pebble/src/test/resources/results/emptyContext @@ -0,0 +1,4 @@ + First: + Second: + Array: + Interpolation & Upper Case filter: INTERPOLATED diff --git a/pebble/src/test/resources/results/sample b/pebble/src/test/resources/results/sample new file mode 100644 index 0000000..b8ddadd --- /dev/null +++ b/pebble/src/test/resources/results/sample @@ -0,0 +1,4 @@ + First: First Message + Second: Very Long Second Foo Message !! 2 &&%^$ + Array: 1 2 3 foo bar true + Interpolation & Upper Case filter: INTERPOLATED FIRST MESSAGE diff --git a/pebble/src/test/resources/results/sampleMissingField b/pebble/src/test/resources/results/sampleMissingField new file mode 100644 index 0000000..154949f --- /dev/null +++ b/pebble/src/test/resources/results/sampleMissingField @@ -0,0 +1,4 @@ + First: First Message + Second: + Array: 1 2 3 foo bar true + Interpolation & Upper Case filter: INTERPOLATED FIRST MESSAGE diff --git a/pebble/src/test/resources/results/service b/pebble/src/test/resources/results/service new file mode 100644 index 0000000..7fb0782 --- /dev/null +++ b/pebble/src/test/resources/results/service @@ -0,0 +1,42 @@ +
+ + +
diff --git a/pebble/src/test/resources/templates/empty.peb b/pebble/src/test/resources/templates/empty.peb new file mode 100644 index 0000000..e69de29 diff --git a/pebble/src/test/resources/templates/sample.peb b/pebble/src/test/resources/templates/sample.peb new file mode 100644 index 0000000..98e33f1 --- /dev/null +++ b/pebble/src/test/resources/templates/sample.peb @@ -0,0 +1,4 @@ + First: {{ sample.result.first }} + Second: {{sample.result.second.foo }} + Array: {% for this in sample.arr %} {{this}} {% endfor %} + Interpolation & Upper Case filter: {{ "Interpolated #{ sample.result.first }" | upper }} diff --git a/pebble/src/test/resources/templates/service.peb b/pebble/src/test/resources/templates/service.peb new file mode 100644 index 0000000..38e2a9d --- /dev/null +++ b/pebble/src/test/resources/templates/service.peb @@ -0,0 +1,21 @@ +{% for item in items %} +
+ {% for swatch in item.swatches %} + {% set links = swatch.all_swatch_links | split('\|') %} + {% set ids = swatch.all_swatch_ids | split('\|') %} + {% set images = swatch.all_swatch_images | split('\|') %} +
    + {% for link in links %} +
  • +

    {{ loop.index + 1 }}

    + + + +
  • + {% endfor %} +
+ {% endfor %} +
+{% endfor %} + +{# This is a comment and will not be printed #} diff --git a/pebble/src/test/resources/templates/serviceCustomSyntax.peb b/pebble/src/test/resources/templates/serviceCustomSyntax.peb new file mode 100644 index 0000000..706cb2d --- /dev/null +++ b/pebble/src/test/resources/templates/serviceCustomSyntax.peb @@ -0,0 +1,21 @@ +<< for item in items >> +
+ << for swatch in item.swatches >> + << set links = swatch.all_swatch_links | split('\|') >> + << set ids = swatch.all_swatch_ids | split('\|') >> + << set images = swatch.all_swatch_images | split('\|') >> +
    + << for link in links >> +
  • +

    <| loop.index + 1 |>

    + + + +
  • + << endfor >> +
+ << endfor >> +
+<< endfor >> + +/* This is a comment and will not be printed */ diff --git a/pebble/src/test/resources/templates/undefinedHelper.peb b/pebble/src/test/resources/templates/undefinedHelper.peb new file mode 100644 index 0000000..5cf870b --- /dev/null +++ b/pebble/src/test/resources/templates/undefinedHelper.peb @@ -0,0 +1,3 @@ + First: {{ sample.result.first}} + Second: {{sample.result.second.foo}} + Empty: {{#notDefinedSymbol}}This will cause exception{{/notDefinedSymbol}} diff --git a/settings.gradle.kts b/settings.gradle.kts index dc60338..0ace288 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -19,8 +19,10 @@ include("knotx-template-engine-api") include("knotx-template-engine-core") include("knotx-template-engine-handlebars") include("knotx-template-engine-it-test") +include("knotx-template-engine-pebble") project(":knotx-template-engine-api").projectDir = file("api") project(":knotx-template-engine-core").projectDir = file("core") project(":knotx-template-engine-handlebars").projectDir = file("handlebars") +project(":knotx-template-engine-pebble").projectDir = file("pebble") project(":knotx-template-engine-it-test").projectDir = file("it-test")