From cc37dbafacc966e87ea2f2aed29b88b2b3c516ab Mon Sep 17 00:00:00 2001 From: Vasyl Khrystiuk Date: Mon, 6 Nov 2023 02:14:55 +0200 Subject: [PATCH] [WIP] updating readme to reflect latest library state --- README.md | 84 +++++++++---------- src/main/java/liqp/Examples.java | 4 +- src/main/java/liqp/TemplateParser.java | 15 +++- src/main/java/liqp/nodes/InsertionNode.java | 6 +- src/test/java/liqp/InsertionTest.java | 14 ++-- src/test/java/liqp/ReadmeSamplesTest.java | 31 ++++++- src/test/java/liqp/TemplateTest.java | 4 +- src/test/java/liqp/nodes/BlockNodeTest.java | 2 +- src/test/java/liqp/parser/ParseTest.java | 36 ++++++++ .../java/liqp/tags/IncludeRelativeTest.java | 6 +- src/test/java/liqp/tags/IncludeTest.java | 3 +- 11 files changed, 137 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index dd897984..dd727aa1 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,8 @@ In Ruby, you'd render a template like this: With Liqp, the equivalent looks like this: ```java -Template template = Template.parse("hi {{name}}"); +TemplateParser parser = new TemplateParser.Builder().build(); +Template template = parser.parse("hi {{name}}"); String rendered = template.render("name", "tobi"); System.out.println(rendered); /* @@ -99,8 +100,8 @@ The following examples are equivalent to the previous Liqp example: #### Map example ```java -Template template = Template.parse("hi {{name}}"); -Map map = new HashMap(); +Template template = new TemplateParser.Builder().build().parse("hi {{name}}"); +Map map = new HashMap<>(); map.put("name", "tobi"); String rendered = template.render(map); System.out.println(rendered); @@ -112,7 +113,7 @@ System.out.println(rendered); #### JSON example ```java -Template template = Template.parse("hi {{name}}"); +Template template = new TemplateParser.Builder().build().parse("hi {{name}}"); String rendered = template.render("{\"name\" : \"tobi\"}"); System.out.println(rendered); /* @@ -124,10 +125,10 @@ System.out.println(rendered); ```java class MyParams implements Inspectable { - public String name = "tobi"; + public String name = "tobi"; }; -Template template = Template.parse("hi {{name}}"); -String rendered =template.render(new MyParams()); +Template template = TemplateParser.DEFAULT.parse("hi {{name}}"); +String rendered = template.render(new MyParams()); System.out.println(rendered); /* hi tobi @@ -142,7 +143,7 @@ class MyLazy implements LiquidSupport { return Collections.singletonMap("name", "tobi"); } }; -Template template = Template.parse("hi {{name}}"); +Template template = TemplateParser.DEFAULT.parse("hi {{name}}"); String rendered = template.render(new MyLazy()); System.out.println(rendered); /* @@ -150,43 +151,38 @@ System.out.println(rendered); */ ``` -#### Strict variables example +#### Controlling library behavior +The library has a set of keys to control the parsing/rendering process. All of them are set on `TemplateParser.Builder` class. Here they are: +* `withFlavor(Flavor flavor)` - flavor of the liquid language. Flavor is nothing else than a predefined set of other settings. Here are supported flavors: + * `Flavor.JEKYLL` - flavor that defines all settings, so it tries to behave like jekyll's templates + * `Flavor.LIQUID` - the same for liquid's templates + * `Flavor.LIQP` (default) - developer of this library found some default behavior of two flavors above somehow weird in selected cases. So this flavor appears. +* `withStripSingleLine(boolean stripSingleLine)`- if `true` then all blank lines left by outputless tags are removed. Default is `false`. +* `withStripSpaceAroundTags(boolean stripSpacesAroundTags)` - if `true` then all whitespaces around tags are removed. Default is `false`. +* `withObjectMapper(ObjectMapper mapper)` - if provided then this mapper is used for converting json strings to objects and internal object conversion. If not provided, then default mapper is used. Default one is good. Also, the default one is always accessible via TemplateContext instance:`context.getParser().getMapper();` +* `withTag(Tag tag)` - register custom tag to be used in templates. +* `withBlock(Block block)` - register custom block to be used in templates. The difference between tag and block is that block has open and closing tag and can contain other content like a text, tags and blocks. +* `withFilter(Filter filter)` - register custom filter to be used in templates. See below for examples. +* `withEvaluateInOutputTag(boolean evaluateInOutputTag)` - both `Flavor.JEKYLL` and `Flavor.LIQUID` are not allows to evaluate expressions in output tags, simply ignoring the expression and printing out very first token of the expression. Yes, this: `{{ 97 > 96 }}` will print `97`. This is known [bug/feature](https://github.com/Shopify/liquid/issues/1102) of those temlators. If you want to change this behavior and evaluate those expressions, set this flag to `true`. Also, the default flavor `Flavor.LIQP` has this flag set to `true` already. +* `withStrictTypedExpressions(boolean strictTypedExpressions)` - ruby is strong-typed language. So comparing different types is not allowed there. This library tries to mimic ruby's type system in a way so all not explicit types (created or manipulated inside of templates) are converted with this schema: `nil` -> `null`; `boolean` -> `boolean`; `string` -> `java.lang.String`; any numbers -> `java.math.BigDecimal`, any datetime -> `java.time.ZonedDateTime`. If you want to change this behavior, and allow comparing in expressions in a less restricted way, set this flag to `true`, then the lax (javascript-like) approach for comparing in expressions will be used. Also, the default flavor `Flavor.LIQP` has this flag set to `true` already, others has it `false` by default. +* `withLiquidStyleInclude(boolean liquidStyleInclude)` - if `true` then include tag will use [syntax from liquid](https://shopify.dev/docs/api/liquid/tags/include), otherwice [jekyll syntax](https://jekyllrb.com/docs/includes/) will be used. Default depends of flavor. `Flavor.LIQUID` and `Flavor.LIQP` has this flag set to `true` already. `Flavor.JEKYLL` has it `false`. +* `withStrictVariables(boolean strictVariables)` - if set to `true` then all variables must be defined before usage, if some variable is not defined, the exception will be thrown. If `false` then all undefined variables will be treated as `null`. Default is `false`. +* `withShowExceptionsFromInclude(boolean showExceptionsFromInclude)` - if set to `true` then all exceptions from included templates will be thrown. If `false` then all exceptions from included templates will be ignored. Default is `true`. +* `withEvaluateMode(TemplateParser.EvaluateMode evaluateMode)` - there exists two rendering modes: `TemplateParser.EvaluateMode.LAZY` and `TemplateParser.EvaluateMode.EAGER`. By default, the `lazy` one is used. This should do the work in most cases. + * In `lazy` mode the template parameters are evaluating on demand and specific properties are read from there only if they are needed. Each filter/tag trying to do its work with its own parameter object, that can be literally anything. + * In `eager` the entire parameter object is converted into plain data tree structure that are made **only** from maps and lists, so tags/filters do know how to work with these kinds of objects. Special case - temporal objects, they are consumed as is. +* `withRenderTransformer(RenderTransformer renderTransformer)` - even if most of elements (filters/tags/blocks) returns its results most cases as `String`, the task of combining all those strings into a final result is a task of `liqp.RenderTransformer` implementation. The default `liqp.RenderTransformerDefaultImpl` uses `StringBuilder` for that task, so template rendering is fast. Althought, you might have special needs or environment to render the results. +* `withLocale(Locale locale)` - locale to be used for rendering. Default is `Locale.ENGLISH`. Used mostly for time rendering. +* `withDefaultTimeZone(ZoneId defaultTimeZone)` - default time zone to be used for rendering. Default is `ZoneId.systemDefault()`. Used mostly for time rendering. +* `withEnvironmentMapConfigurator(Consumer> configurator)` - if provided then this configurator is called before each template rendering. It can be used to set some global variables for all templates built with given `TemplateParser`. +* `withSnippetsFolderName(String snippetsFolderName)` - define folder to be used for searching files by `include` tag. Defaults depend on flavor: `Flavor.LIQUID` and `Flavor.LIQP` has this set to `snippets`; `Flavor.JEKYLL` uses `_includes`. +* `withNameResolver(NameResolver nameResolver)` - if provided then this resolver is used for resolving names of included files. If not provided, then default resolver is used. Default resolver is `liqp.antlr.LocalFSNameResolver` that uses `java.nio.file.Path` for resolving names in local file system. That can be changed to any other resolver, for example, to resolve names in classpath or in remote file system or even build templates dynamically by name. +* `withMaxIterations(int maxIterations)` - maximum number of iterations allowed in a template. Default is `Integer.MAX_VALUE`. Used to prevent infinite loops. +* `withMaxSizeRenderedString(int maxSizeRenderedString)` - maximum size of rendered string. Default is `Integer.MAX_VALUE`. Used to prevent out of memory errors. +* `withMaxRenderTimeMillis(long maxRenderTimeMillis)` - maximum time allowed for template rendering. Default is `Long.MAX_VALUE`. Used to prevent never-ending rendering. +* `withMaxTemplateSizeBytes(long maxTemplateSizeBytes)` - maximum size of template. Default is `Long.MAX_VALUE`. Used to prevent out of memory errors. +* `withErrorMode(ErrorMode errorMode)` - error mode to be used. Default is `ErrorMode.STRICT`. -Strict variables means that value for every key must be provided, otherwise an exception occurs. - -```java -Template template = Template.parse("hi {{name}}") - .withRenderSettings(new RenderSettings.Builder().withStrictVariables(true).build()); -String rendered = template.render(); // no value for "name" -// exception is thrown -``` - -#### Eager and Lazy evaluate mode -There exists two rendering modes: lazy and eager. -* In `lazy` mode the template parameters are evaluating on demand and specific properties are read from there only if they are needed. Each filter/tag trying to do its work with its own parameter object, that can be literally anything. -* In `eager` the entire parameter object is converted into plain data tree structure that are made only from maps and lists, so tags/filters do know how to work with these kinds of objects. Special case - temporal objects, they are consumed as is. - -By default, the `lazy` one is used. This should do the work in most cases. - -Switching mode is possible via providing special `RenderSettings`. -Example usage of `lazy` mode: -```java -RenderSettings renderSettings = new RenderSettings.Builder() - .withEvaluateMode(RenderSettings.EvaluateMode.EAGER) - .build(); - -Map in = Collections.singletonMap("a", new Object() { - public String val = "tobi"; -}); - -String res = Template.parse("hi {{a.val}}") - .withRenderSettings(renderSettings) - .render(in); -System.out.println(res); -/* - hi tobi -*/ -``` ### 2.1 Custom filters diff --git a/src/main/java/liqp/Examples.java b/src/main/java/liqp/Examples.java index e535525a..5f7e6cbd 100644 --- a/src/main/java/liqp/Examples.java +++ b/src/main/java/liqp/Examples.java @@ -117,7 +117,7 @@ public Object apply(Object value, TemplateContext context, Object... params) { private static void customLoopBlock() { - TemplateParser parser = new TemplateParser.Builder().withInsertion(new Block("loop") { + TemplateParser parser = new TemplateParser.Builder().withBlock(new Block("loop") { @Override public Object render(TemplateContext context, LNode... nodes) { @@ -181,7 +181,7 @@ public static void demoStrictVariables() { } public static void customRandomTag() { - TemplateParser parser = new TemplateParser.Builder().withInsertion(new Tag("rand") { + TemplateParser parser = new TemplateParser.Builder().withTag(new Tag("rand") { private final Random rand = new Random(); @Override diff --git a/src/main/java/liqp/TemplateParser.java b/src/main/java/liqp/TemplateParser.java index 1177c323..40df356d 100644 --- a/src/main/java/liqp/TemplateParser.java +++ b/src/main/java/liqp/TemplateParser.java @@ -5,10 +5,12 @@ import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import liqp.antlr.LocalFSNameResolver; import liqp.antlr.NameResolver; +import liqp.blocks.Block; import liqp.filters.Filter; import liqp.filters.Filters; import liqp.parser.Flavor; import liqp.parser.LiquidSupport; +import liqp.tags.Tag; import org.antlr.v4.runtime.CharStream; import org.antlr.v4.runtime.CharStreams; @@ -127,7 +129,7 @@ public static class Builder { private Flavor flavor; private boolean stripSpacesAroundTags = false; - private boolean stripSingleLine; + private boolean stripSingleLine = false; private ObjectMapper mapper; private List insertions = new ArrayList<>(); private List filters = new ArrayList<>(); @@ -139,7 +141,7 @@ public static class Builder { private boolean strictVariables = false; - private boolean showExceptionsFromInclude; + private boolean showExceptionsFromInclude = true; private EvaluateMode evaluateMode = EvaluateMode.LAZY; private Locale locale = DEFAULT_LOCALE; private ZoneId defaultTimeZone; @@ -221,8 +223,13 @@ public Builder withObjectMapper(ObjectMapper mapper) { return this; } - public Builder withInsertion(Insertion insertion) { - this.insertions.add(insertion); + public Builder withBlock(Block block) { + this.insertions.add(block); + return this; + } + + public Builder withTag(Tag tag) { + this.insertions.add(tag); return this; } diff --git a/src/main/java/liqp/nodes/InsertionNode.java b/src/main/java/liqp/nodes/InsertionNode.java index 8dafe061..a6240344 100644 --- a/src/main/java/liqp/nodes/InsertionNode.java +++ b/src/main/java/liqp/nodes/InsertionNode.java @@ -7,11 +7,11 @@ public class InsertionNode implements LNode { - private Insertion insertion; - private LNode[] tokens; + private final Insertion insertion; + private final LNode[] tokens; public InsertionNode(Insertion insertion, List tokens) { - this(insertion.name, insertion, tokens.toArray(new LNode[tokens.size()])); + this(insertion.name, insertion, tokens.toArray(new LNode[0])); } public InsertionNode(Insertion insertion, LNode... tokens) { diff --git a/src/test/java/liqp/InsertionTest.java b/src/test/java/liqp/InsertionTest.java index 4fa66ae6..9bd4dc91 100644 --- a/src/test/java/liqp/InsertionTest.java +++ b/src/test/java/liqp/InsertionTest.java @@ -16,7 +16,7 @@ public class InsertionTest { public void testNestedCustomTagsAndBlocks() { TemplateParser templateParser = new TemplateParser.Builder() - .withInsertion(new Block("block") { + .withBlock(new Block("block") { @Override public Object render(TemplateContext context, LNode... nodes) { String data = (nodes.length >= 2 ? nodes[1].render(context) : nodes[0].render( @@ -25,7 +25,7 @@ public Object render(TemplateContext context, LNode... nodes) { return "blk[" + data + "]"; } }) - .withInsertion(new Tag("simple") { + .withTag(new Tag("simple") { @Override public Object render(TemplateContext context, LNode... nodes) { return "(sim)"; @@ -41,7 +41,7 @@ public Object render(TemplateContext context, LNode... nodes) { public void testNestedCustomTagsAndBlocksAsOneCollection() { String templateString = "{% block %}a{% simple %}b{% block %}c{% endblock %}d{% endblock %}"; - TemplateParser parser = new TemplateParser.Builder().withInsertion( + TemplateParser parser = new TemplateParser.Builder().withBlock( new Block("block") { @Override public Object render(TemplateContext context, LNode... nodes) { @@ -50,7 +50,7 @@ public Object render(TemplateContext context, LNode... nodes) { return "blk[" + data + "]"; } - }).withInsertion(new Tag("simple") { + }).withTag(new Tag("simple") { @Override public Object render(TemplateContext context, LNode... nodes) { return "(sim)"; @@ -64,7 +64,7 @@ public Object render(TemplateContext context, LNode... nodes) { @Test public void testCustomTag() throws RecognitionException { - TemplateParser parser = new TemplateParser.Builder().withInsertion(new Tag("twice") { + TemplateParser parser = new TemplateParser.Builder().withTag(new Tag("twice") { @Override public Object render(TemplateContext context, LNode... nodes) { Double number = super.asNumber(nodes[0].render(context)).doubleValue(); @@ -80,7 +80,7 @@ public Object render(TemplateContext context, LNode... nodes) { @Test public void testCustomTagBlock() throws RecognitionException { - TemplateParser templateParser = new TemplateParser.Builder().withInsertion(new Block("twice") { + TemplateParser templateParser = new TemplateParser.Builder().withBlock(new Block("twice") { @Override public Object render(TemplateContext context, LNode... nodes) { LNode blockNode = nodes[nodes.length - 1]; @@ -186,7 +186,7 @@ public void no_transformTest() throws RecognitionException { @Test public void testCustomTagRegistration() { TemplateParser parser = new TemplateParser.Builder() - .withInsertion(new Tag("custom_tag") { + .withTag(new Tag("custom_tag") { @Override public Object render(TemplateContext context, LNode... nodes) { return "xxx"; diff --git a/src/test/java/liqp/ReadmeSamplesTest.java b/src/test/java/liqp/ReadmeSamplesTest.java index 8f1caa41..1421aa11 100644 --- a/src/test/java/liqp/ReadmeSamplesTest.java +++ b/src/test/java/liqp/ReadmeSamplesTest.java @@ -5,19 +5,47 @@ import org.junit.Test; import java.util.Collections; +import java.util.HashMap; import java.util.Map; import static org.junit.Assert.assertEquals; public class ReadmeSamplesTest { + + + @Test + public void testReadMeIntro() { + TemplateParser parser = new TemplateParser.Builder().build(); + Template template = parser.parse("hi {{name}}"); + String rendered = template.render("name", "tobi"); + System.out.println(rendered); + } + + @Test + public void testReadMeMap() { + Template template = new TemplateParser.Builder().build().parse("hi {{name}}"); + Map map = new HashMap<>(); + map.put("name", "tobi"); + String rendered = template.render(map); + System.out.println(rendered); + } + + @Test + public void testReadMeJson() { + Template template = new TemplateParser.Builder().build().parse("hi {{name}}"); + String rendered = template.render("{\"name\" : \"tobi\"}"); + System.out.println(rendered); + } + @Test + @SuppressWarnings({"unused", "FieldMayBeFinal"}) public void testInspectable() { class MyParams implements Inspectable { - @SuppressWarnings("unused") public String name = "tobi"; }; Template template = TemplateParser.DEFAULT.parse("hi {{name}}"); String rendered = template.render(new MyParams()); + System.out.println(rendered); assertEquals("hi tobi", rendered); } @@ -31,6 +59,7 @@ public Map toLiquid() { }; Template template = TemplateParser.DEFAULT.parse("hi {{name}}"); String rendered = template.render(new MyLazy()); + System.out.println(rendered); assertEquals("hi tobi", rendered); } diff --git a/src/test/java/liqp/TemplateTest.java b/src/test/java/liqp/TemplateTest.java index e8169331..cb52f42e 100644 --- a/src/test/java/liqp/TemplateTest.java +++ b/src/test/java/liqp/TemplateTest.java @@ -267,7 +267,7 @@ public void testCustomTagMissingErrorReporting() { @Test public void testWithCustomTag() { // given - TemplateParser parser = new TemplateParser.Builder().withInsertion(new Tag("custom_tag") { + TemplateParser parser = new TemplateParser.Builder().withTag(new Tag("custom_tag") { @Override public Object render(TemplateContext context, LNode... nodes) { return "xxx"; @@ -283,7 +283,7 @@ public Object render(TemplateContext context, LNode... nodes) { @Test public void testWithCustomBlock() { // given - TemplateParser parser = new TemplateParser.Builder().withInsertion(new Block("custom_uppercase_block") { + TemplateParser parser = new TemplateParser.Builder().withBlock(new Block("custom_uppercase_block") { @Override public Object render(TemplateContext context, LNode... nodes) { LNode block = nodes[0]; diff --git a/src/test/java/liqp/nodes/BlockNodeTest.java b/src/test/java/liqp/nodes/BlockNodeTest.java index 0852b0b5..3c0b7900 100644 --- a/src/test/java/liqp/nodes/BlockNodeTest.java +++ b/src/test/java/liqp/nodes/BlockNodeTest.java @@ -19,7 +19,7 @@ public class BlockNodeTest { */ @Test public void customTagTest() throws RecognitionException { - TemplateParser parser = new TemplateParser.Builder().withInsertion(new Block("testtag"){ + TemplateParser parser = new TemplateParser.Builder().withBlock(new Block("testtag"){ @Override public Object render(TemplateContext context, LNode... nodes) { return null; diff --git a/src/test/java/liqp/parser/ParseTest.java b/src/test/java/liqp/parser/ParseTest.java index 12df5f4a..f0a36471 100644 --- a/src/test/java/liqp/parser/ParseTest.java +++ b/src/test/java/liqp/parser/ParseTest.java @@ -2,7 +2,10 @@ import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; +import liqp.Template; +import liqp.TemplateContext; import org.junit.Test; import liqp.TemplateParser; @@ -123,4 +126,37 @@ public void keywords_as_identifier() throws Exception { .render(" { \"var\": { \"end\": \"content\" } } "), is("var2: var2:content")); } + + @Test + public void testStripSpaces() { + String source = "a \n \n {{ a }} \n \n c"; + + // default + String res = new TemplateParser.Builder() + .build() + .parse(source).render("a", "b"); + assertEquals("a \n \n b \n \n c", res); + + + // false is default + res = new TemplateParser.Builder() + .withStripSpaceAroundTags(false) + .build() + .parse(source).render("a", "b"); + assertEquals("a \n \n b \n \n c", res); + + // true + res = new TemplateParser.Builder() + .withStripSpaceAroundTags(true) + .build() + .parse(source).render("a", "b"); + assertEquals("abc", res); + + res = new TemplateParser.Builder() + .withStripSpaceAroundTags(true) + .withStripSingleLine(true) + .build() + .parse(source).render("a", "b"); + assertEquals("a \n \nb \n c", res); + } } diff --git a/src/test/java/liqp/tags/IncludeRelativeTest.java b/src/test/java/liqp/tags/IncludeRelativeTest.java index dab0bfde..35ac13ca 100644 --- a/src/test/java/liqp/tags/IncludeRelativeTest.java +++ b/src/test/java/liqp/tags/IncludeRelativeTest.java @@ -56,7 +56,7 @@ public void testLiquidWithCustomIncludeShouldAllowOverride() throws IOException TemplateParser parser = new TemplateParser.Builder() .withFlavor(Flavor.LIQUID) - .withInsertion(new Tag("include_relative") { + .withTag(new Tag("include_relative") { @Override public Object render(TemplateContext context, LNode... nodes) { return "World"; @@ -103,14 +103,14 @@ public void testIncludeANestedFileRelativeToAPost() throws IOException { public void testCustomBlocksStackWithCustomBlockIncludeRelative() { TemplateParser parser = new TemplateParser.Builder() .withFlavor(Flavor.LIQUID) - .withInsertion(new Block("another") { + .withBlock(new Block("another") { @Override public Object render(TemplateContext context, LNode... nodes) { LNode blockNode = nodes[nodes.length - 1]; return "[" + super.asString(blockNode.render(context), context) + "]"; } }) - .withInsertion(new Block("include_relative") { + .withBlock(new Block("include_relative") { @Override public Object render(TemplateContext context, LNode... nodes) { return "World"; diff --git a/src/test/java/liqp/tags/IncludeTest.java b/src/test/java/liqp/tags/IncludeTest.java index 3106f36b..8b1ecd88 100644 --- a/src/test/java/liqp/tags/IncludeTest.java +++ b/src/test/java/liqp/tags/IncludeTest.java @@ -322,6 +322,7 @@ public void errorInIncludeCauseIgnoreErrorWhenNoExceptionsFromIncludeLegacy() th File index = new File(jekyll, "index_with_errored_include.html"); Template template = new TemplateParser.Builder() .withFlavor(Flavor.JEKYLL) + .withShowExceptionsFromInclude(false) .build() .parse(index); @@ -348,7 +349,7 @@ public void errorInIncludeCauseMissingIncludeWithCustomRendering() throws IOExce // when template.render(); - // them + // then fail(); }