Repository: mbosecke/pebble Branch: master Commit: 37f69de6df9f Files: 788 Total size: 1.4 MB Directory structure: gitextract_382czipv/ ├── .gitattributes ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── config.yml │ │ └── issue.md │ ├── dependabot.yml │ └── workflows/ │ └── maven.yml ├── .gitignore ├── LICENSE ├── README.md ├── SECURITY.md ├── docs/ │ ├── pom.xml │ └── src/ │ └── orchid/ │ └── resources/ │ ├── assets/ │ │ └── css/ │ │ └── github-fork-ribbon.css │ ├── changelog/ │ │ ├── v3_0_0.md │ │ ├── v3_0_1.md │ │ ├── v3_0_10.md │ │ ├── v3_0_2.md │ │ ├── v3_0_3.md │ │ ├── v3_0_4.md │ │ ├── v3_0_5.md │ │ ├── v3_0_6.md │ │ ├── v3_0_7.md │ │ ├── v3_0_8.md │ │ ├── v3_0_9.md │ │ ├── v3_1_0.md │ │ ├── v3_1_1.md │ │ ├── v3_1_2.md │ │ ├── v3_1_3.md │ │ ├── v3_1_4.md │ │ ├── v3_1_5.md │ │ ├── v3_1_6.md │ │ ├── v3_2_0.md │ │ ├── v3_2_1.md │ │ ├── v3_2_2.md │ │ ├── v3_2_3.md │ │ ├── v3_2_4.md │ │ ├── v4_0_0.md │ │ ├── v4_1_0.md │ │ └── v4_1_1.md │ ├── config.yml │ ├── data/ │ │ ├── contributors.yml │ │ └── twig-compatibility/ │ │ ├── filters.yml │ │ ├── functions.yml │ │ ├── operators.yml │ │ ├── tags.yml │ │ └── tests.yml │ ├── homepage.md │ ├── pages/ │ │ ├── CNAME │ │ ├── changelog.md │ │ ├── contributing.md │ │ └── twig-compatibility.peb │ ├── templates/ │ │ ├── includes/ │ │ │ └── sidebar.peb │ │ └── layouts/ │ │ └── index.peb │ └── wiki/ │ ├── filter/ │ │ ├── abbreviate.md │ │ ├── abs.md │ │ ├── base64decode.md │ │ ├── base64encode.md │ │ ├── capitalize.md │ │ ├── date.md │ │ ├── default.md │ │ ├── escape.md │ │ ├── first.md │ │ ├── format.md │ │ ├── join.md │ │ ├── last.md │ │ ├── length.md │ │ ├── lower.md │ │ ├── nl2br.md │ │ ├── numberformat.md │ │ ├── raw.md │ │ ├── replace.md │ │ ├── reverse.md │ │ ├── rsort.md │ │ ├── sha256.md │ │ ├── slice.md │ │ ├── sort.md │ │ ├── split.md │ │ ├── title.md │ │ ├── trim.md │ │ ├── upper.md │ │ └── urlencode.md │ ├── function/ │ │ ├── blockFunction.md │ │ ├── i18n.md │ │ ├── max.md │ │ ├── min.md │ │ ├── parent.md │ │ └── range.md │ ├── guide/ │ │ ├── basic-usage.md │ │ ├── customize-defaults.md │ │ ├── escaping.md │ │ ├── extending-pebble.md │ │ ├── high-performance.md │ │ ├── installation.md │ │ └── spring-boot-integration.md │ ├── operator/ │ │ ├── comparisons.md │ │ ├── contains.md │ │ ├── is.md │ │ ├── logic.md │ │ ├── math.md │ │ └── others.md │ ├── summary.md │ ├── tag/ │ │ ├── autoescape.md │ │ ├── block.md │ │ ├── cache.md │ │ ├── embed.md │ │ ├── extends.md │ │ ├── filter.md │ │ ├── flush.md │ │ ├── for.md │ │ ├── from.md │ │ ├── if.md │ │ ├── import.md │ │ ├── include.md │ │ ├── macro.md │ │ ├── parallel.md │ │ ├── set.md │ │ └── verbatim.md │ └── test/ │ ├── empty.md │ ├── even.md │ ├── iterable.md │ ├── map.md │ ├── null.md │ └── odd.md ├── intellij-java-google-style.xml ├── pebble/ │ ├── pom.xml │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── io/ │ │ └── pebbletemplates/ │ │ └── pebble/ │ │ ├── PebbleEngine.java │ │ ├── attributes/ │ │ │ ├── ArrayResolver.java │ │ │ ├── AttributeResolver.java │ │ │ ├── DefaultAttributeResolver.java │ │ │ ├── ListResolver.java │ │ │ ├── MacroResolver.java │ │ │ ├── MapResolver.java │ │ │ ├── MemberCacheUtils.java │ │ │ ├── ResolvedAttribute.java │ │ │ └── methodaccess/ │ │ │ ├── BlacklistMethodAccessValidator.java │ │ │ ├── MethodAccessValidator.java │ │ │ └── NoOpMethodAccessValidator.java │ │ ├── cache/ │ │ │ ├── CacheKey.java │ │ │ ├── PebbleCache.java │ │ │ ├── tag/ │ │ │ │ ├── CaffeineTagCache.java │ │ │ │ ├── ConcurrentMapTagCache.java │ │ │ │ └── NoOpTagCache.java │ │ │ └── template/ │ │ │ ├── CaffeineTemplateCache.java │ │ │ ├── ConcurrentMapTemplateCache.java │ │ │ └── NoOpTemplateCache.java │ │ ├── error/ │ │ │ ├── AttributeNotFoundException.java │ │ │ ├── ClassAccessException.java │ │ │ ├── LoaderException.java │ │ │ ├── ParserException.java │ │ │ ├── PebbleException.java │ │ │ └── RootAttributeNotFoundException.java │ │ ├── extension/ │ │ │ ├── AbstractExtension.java │ │ │ ├── AbstractNodeVisitor.java │ │ │ ├── Extension.java │ │ │ ├── ExtensionCustomizer.java │ │ │ ├── ExtensionRegistry.java │ │ │ ├── ExtensionRegistryFactory.java │ │ │ ├── Filter.java │ │ │ ├── Function.java │ │ │ ├── NamedArguments.java │ │ │ ├── NodeVisitor.java │ │ │ ├── NodeVisitorFactory.java │ │ │ ├── Test.java │ │ │ ├── core/ │ │ │ │ ├── AbbreviateFilter.java │ │ │ │ ├── AbsFilter.java │ │ │ │ ├── AttributeResolverExtension.java │ │ │ │ ├── Base64DecoderFilter.java │ │ │ │ ├── Base64EncoderFilter.java │ │ │ │ ├── CapitalizeFilter.java │ │ │ │ ├── CoreExtension.java │ │ │ │ ├── DateFilter.java │ │ │ │ ├── DefaultFilter.java │ │ │ │ ├── DefinedTest.java │ │ │ │ ├── DisallowExtensionCustomizerBuilder.java │ │ │ │ ├── EmptyTest.java │ │ │ │ ├── EvenTest.java │ │ │ │ ├── FirstFilter.java │ │ │ │ ├── FormatFilter.java │ │ │ │ ├── IterableTest.java │ │ │ │ ├── JoinFilter.java │ │ │ │ ├── LastFilter.java │ │ │ │ ├── LengthFilter.java │ │ │ │ ├── LowerFilter.java │ │ │ │ ├── MacroAndBlockRegistrantNodeVisitor.java │ │ │ │ ├── MacroAndBlockRegistrantNodeVisitorFactory.java │ │ │ │ ├── MapTest.java │ │ │ │ ├── MaxFunction.java │ │ │ │ ├── MergeFilter.java │ │ │ │ ├── MinFunction.java │ │ │ │ ├── Nl2brFilter.java │ │ │ │ ├── NullTest.java │ │ │ │ ├── NumberFormatFilter.java │ │ │ │ ├── OddTest.java │ │ │ │ ├── RangeFunction.java │ │ │ │ ├── ReplaceFilter.java │ │ │ │ ├── ReverseFilter.java │ │ │ │ ├── RsortFilter.java │ │ │ │ ├── Sha256Filter.java │ │ │ │ ├── SliceFilter.java │ │ │ │ ├── SortFilter.java │ │ │ │ ├── SplitFilter.java │ │ │ │ ├── TitleFilter.java │ │ │ │ ├── TrimFilter.java │ │ │ │ ├── UpperFilter.java │ │ │ │ └── UrlEncoderFilter.java │ │ │ ├── debug/ │ │ │ │ ├── DebugExtension.java │ │ │ │ ├── PrettyPrintNodeVisitor.java │ │ │ │ └── PrettyPrintNodeVisitorFactory.java │ │ │ ├── escaper/ │ │ │ │ ├── EscapeFilter.java │ │ │ │ ├── EscaperExtension.java │ │ │ │ ├── EscaperNodeVisitor.java │ │ │ │ ├── EscaperNodeVisitorFactory.java │ │ │ │ ├── EscapingStrategy.java │ │ │ │ ├── RawFilter.java │ │ │ │ └── SafeString.java │ │ │ ├── i18n/ │ │ │ │ ├── I18nExtension.java │ │ │ │ ├── UTF8Control.java │ │ │ │ └── i18nFunction.java │ │ │ └── writer/ │ │ │ ├── PooledSpecializedStringWriter.java │ │ │ ├── SpecializedWriter.java │ │ │ └── StringWriterSpecializedAdapter.java │ │ ├── lexer/ │ │ │ ├── Lexer.java │ │ │ ├── LexerImpl.java │ │ │ ├── Syntax.java │ │ │ ├── TemplateSource.java │ │ │ ├── Token.java │ │ │ └── TokenStream.java │ │ ├── loader/ │ │ │ ├── AbstractServletLoader.java │ │ │ ├── ClasspathLoader.java │ │ │ ├── DelegatingLoader.java │ │ │ ├── DelegatingLoaderCacheKey.java │ │ │ ├── FileLoader.java │ │ │ ├── Loader.java │ │ │ ├── MemoryLoader.java │ │ │ ├── Servlet5Loader.java │ │ │ ├── ServletLoader.java │ │ │ └── StringLoader.java │ │ ├── node/ │ │ │ ├── AbstractRenderableNode.java │ │ │ ├── ArgumentsNode.java │ │ │ ├── AutoEscapeNode.java │ │ │ ├── BlockNode.java │ │ │ ├── BodyNode.java │ │ │ ├── CacheNode.java │ │ │ ├── EmbedNode.java │ │ │ ├── ExtendsNode.java │ │ │ ├── FlushNode.java │ │ │ ├── ForNode.java │ │ │ ├── FromNode.java │ │ │ ├── FunctionOrMacroNameNode.java │ │ │ ├── IfNode.java │ │ │ ├── ImportNode.java │ │ │ ├── IncludeNode.java │ │ │ ├── MacroNode.java │ │ │ ├── NamedArgumentNode.java │ │ │ ├── Node.java │ │ │ ├── ParallelNode.java │ │ │ ├── PositionalArgumentNode.java │ │ │ ├── PrintNode.java │ │ │ ├── RenderableNode.java │ │ │ ├── RootNode.java │ │ │ ├── SetNode.java │ │ │ ├── TestInvocationExpression.java │ │ │ ├── TextNode.java │ │ │ ├── expression/ │ │ │ │ ├── AddExpression.java │ │ │ │ ├── AndExpression.java │ │ │ │ ├── ArrayExpression.java │ │ │ │ ├── BinaryExpression.java │ │ │ │ ├── BlockFunctionExpression.java │ │ │ │ ├── ConcatenateExpression.java │ │ │ │ ├── ContainsExpression.java │ │ │ │ ├── ContextVariableExpression.java │ │ │ │ ├── DivideExpression.java │ │ │ │ ├── EqualsExpression.java │ │ │ │ ├── Expression.java │ │ │ │ ├── FilterExpression.java │ │ │ │ ├── FilterInvocationExpression.java │ │ │ │ ├── FunctionOrMacroInvocationExpression.java │ │ │ │ ├── GetAttributeExpression.java │ │ │ │ ├── GreaterThanEqualsExpression.java │ │ │ │ ├── GreaterThanExpression.java │ │ │ │ ├── LessThanEqualsExpression.java │ │ │ │ ├── LessThanExpression.java │ │ │ │ ├── LiteralBigDecimalExpression.java │ │ │ │ ├── LiteralBooleanExpression.java │ │ │ │ ├── LiteralDoubleExpression.java │ │ │ │ ├── LiteralIntegerExpression.java │ │ │ │ ├── LiteralLongExpression.java │ │ │ │ ├── LiteralNullExpression.java │ │ │ │ ├── LiteralStringExpression.java │ │ │ │ ├── MapExpression.java │ │ │ │ ├── ModulusExpression.java │ │ │ │ ├── MultiplyExpression.java │ │ │ │ ├── NegativeTestExpression.java │ │ │ │ ├── NotEqualsExpression.java │ │ │ │ ├── OrExpression.java │ │ │ │ ├── ParentFunctionExpression.java │ │ │ │ ├── PositiveTestExpression.java │ │ │ │ ├── RangeExpression.java │ │ │ │ ├── RenderableNodeExpression.java │ │ │ │ ├── SubtractExpression.java │ │ │ │ ├── TernaryExpression.java │ │ │ │ ├── UnaryExpression.java │ │ │ │ ├── UnaryMinusExpression.java │ │ │ │ ├── UnaryNotExpression.java │ │ │ │ └── UnaryPlusExpression.java │ │ │ └── fornode/ │ │ │ ├── LazyLength.java │ │ │ └── LazyRevIndex.java │ │ ├── operator/ │ │ │ ├── Associativity.java │ │ │ ├── BinaryOperator.java │ │ │ ├── BinaryOperatorImpl.java │ │ │ ├── BinaryOperatorType.java │ │ │ ├── UnaryOperator.java │ │ │ └── UnaryOperatorImpl.java │ │ ├── parser/ │ │ │ ├── ExpressionParser.java │ │ │ ├── Parser.java │ │ │ ├── ParserImpl.java │ │ │ ├── ParserOptions.java │ │ │ └── StoppingCondition.java │ │ ├── template/ │ │ │ ├── Block.java │ │ │ ├── EvaluationContext.java │ │ │ ├── EvaluationContextImpl.java │ │ │ ├── EvaluationOptions.java │ │ │ ├── GlobalContext.java │ │ │ ├── Hierarchy.java │ │ │ ├── Macro.java │ │ │ ├── MacroAttributeProvider.java │ │ │ ├── PebbleTemplate.java │ │ │ ├── PebbleTemplateImpl.java │ │ │ ├── RenderedSizeContext.java │ │ │ ├── Scope.java │ │ │ └── ScopeChain.java │ │ ├── tokenParser/ │ │ │ ├── AutoEscapeTokenParser.java │ │ │ ├── BlockTokenParser.java │ │ │ ├── CacheTokenParser.java │ │ │ ├── EmbedTokenParser.java │ │ │ ├── ExtendsTokenParser.java │ │ │ ├── FilterTokenParser.java │ │ │ ├── FlushTokenParser.java │ │ │ ├── ForTokenParser.java │ │ │ ├── FromTokenParser.java │ │ │ ├── IfTokenParser.java │ │ │ ├── ImportTokenParser.java │ │ │ ├── IncludeTokenParser.java │ │ │ ├── MacroTokenParser.java │ │ │ ├── ParallelTokenParser.java │ │ │ ├── SetTokenParser.java │ │ │ ├── TokenParser.java │ │ │ └── VerbatimTokenParser.java │ │ └── utils/ │ │ ├── Callbacks.java │ │ ├── FutureWriter.java │ │ ├── LimitedSizeWriter.java │ │ ├── OperatorUtils.java │ │ ├── Pair.java │ │ ├── PathUtils.java │ │ ├── StringLengthComparator.java │ │ ├── StringUtils.java │ │ └── TypeUtils.java │ └── test/ │ ├── java/ │ │ └── io/ │ │ └── pebbletemplates/ │ │ └── pebble/ │ │ ├── ArgumentsNodeTest.java │ │ ├── ArraySyntaxTest.java │ │ ├── AttributeSubscriptSyntaxTest.java │ │ ├── CacheTest.java │ │ ├── CompilerTest.java │ │ ├── ConcurrencyTest.java │ │ ├── ContextTest.java │ │ ├── CoreFiltersTest.java │ │ ├── CoreFunctionsTest.java │ │ ├── CoreTagsTest.java │ │ ├── CoreTestsTest.java │ │ ├── DynamicNamedArgsTest.java │ │ ├── EmbedCachingTagTest.java │ │ ├── EmbedTagTest.java │ │ ├── EnumEqualsTest.java │ │ ├── ErrorReportingTest.java │ │ ├── EscaperExtensionTest.java │ │ ├── ExtendingPebbleTest.java │ │ ├── FileLoaderTest.java │ │ ├── ForTest.java │ │ ├── GetAttributeTest.java │ │ ├── I18nExtensionTest.java │ │ ├── IncludeWithParameterTest.java │ │ ├── InheritanceTest.java │ │ ├── LoaderTest.java │ │ ├── LogicTest.java │ │ ├── MacroTest.java │ │ ├── MapSyntaxTest.java │ │ ├── MaxRenderedSizeTest.java │ │ ├── MethodAccessTemplateTest.java │ │ ├── NewlineTrimmingTest.java │ │ ├── Nl2brFilterTest.java │ │ ├── OverloadedMethodTest.java │ │ ├── OverrideCoreExtensionTest.java │ │ ├── ParsingOdditiesTest.java │ │ ├── RenderSingleBlockTest.java │ │ ├── RenderWithoutEndBlockTest.java │ │ ├── ScopeChainTest.java │ │ ├── ScopeTest.java │ │ ├── SplitFilterTest.java │ │ ├── StrictModeTest.java │ │ ├── StringInterpolationTest.java │ │ ├── TernaryExpressionTest.java │ │ ├── TestParallelParsing.java │ │ ├── TestRelativePath.java │ │ ├── WhitespaceControlTest.java │ │ ├── WritingTest.java │ │ ├── attributes/ │ │ │ └── methodaccess/ │ │ │ ├── BlacklistMethodAccessValidatorTest.java │ │ │ ├── Foo.java │ │ │ ├── InstanceProvider.java │ │ │ ├── MethodsProvider.java │ │ │ └── NoOpMethodAccessValidatorTest.java │ │ ├── extension/ │ │ │ ├── ArrayToStringFilter.java │ │ │ ├── ExtensionCustomizerTest.java │ │ │ ├── InvocationCountingFunction.java │ │ │ ├── ListToStringFilter.java │ │ │ ├── MapToStringFilter.java │ │ │ ├── TestingExtension.java │ │ │ ├── core/ │ │ │ │ └── FormatFilterTest.java │ │ │ └── escaper/ │ │ │ └── RawFilterTest.java │ │ ├── lexer/ │ │ │ ├── IdentifierTest.java │ │ │ ├── LexerImplTest.java │ │ │ └── SyntaxTest.java │ │ ├── macro/ │ │ │ ├── MacroGlobalVariablesTest.java │ │ │ ├── PebbleExtension.java │ │ │ ├── TestFilter.java │ │ │ └── TestMacroCalls.java │ │ ├── node/ │ │ │ ├── ForNodeTest.java │ │ │ ├── IfNodeTest.java │ │ │ └── expression/ │ │ │ ├── AndExpressionTest.java │ │ │ ├── ExpressionTest.java │ │ │ ├── OrExpressionTest.java │ │ │ ├── StringExpressionParserTest.java │ │ │ └── UnaryNotExpressionTest.java │ │ ├── template/ │ │ │ └── tests/ │ │ │ ├── PebbleTestContext.java │ │ │ ├── WhiteSpaceControlWithNewLineTrimmingTests.java │ │ │ └── input/ │ │ │ ├── PebbleTestItem.java │ │ │ └── PebbleTestItemType.java │ │ └── utils/ │ │ ├── LimitedSizeWriterTest.java │ │ ├── OperatorUtilsToNumberTest.java │ │ └── PathUtilsTest.java │ └── resources/ │ ├── logback-test.xml │ ├── security/ │ │ ├── allowedMethods.properties │ │ └── unsafeMethods.properties │ ├── template-tests/ │ │ ├── DoubleNestedIfStatement.peb │ │ ├── DoubleNestedIfStatement.txt │ │ ├── ForLoopWithNestedIfStatementAndMacro.peb │ │ ├── ForLoopWithNestedIfStatementAndMacro.txt │ │ ├── NestedIfStatementWithOneElseIfStatements.peb │ │ ├── NestedIfStatementWithOneElseIfStatements.txt │ │ ├── NestedIfStatementWithThreeElseIfStatements.peb │ │ ├── NestedIfStatementWithThreeElseIfStatements.txt │ │ ├── NestedIfStatementWithTwoElseIfStatements.peb │ │ └── NestedIfStatementWithTwoElseIfStatements.txt │ ├── templates/ │ │ ├── cache/ │ │ │ ├── cache1/ │ │ │ │ └── template.cache.peb │ │ │ ├── cache2/ │ │ │ │ └── template.cache.peb │ │ │ ├── template.cacheChild.peb │ │ │ └── template.cacheParent.peb │ │ ├── embed/ │ │ │ ├── cache/ │ │ │ │ ├── template.base.peb │ │ │ │ ├── template1.peb │ │ │ │ └── template2.peb │ │ │ ├── notes.md │ │ │ ├── test0/ │ │ │ │ ├── template.base.peb │ │ │ │ ├── template.peb │ │ │ │ ├── template.result.twig.txt │ │ │ │ └── template.result.txt │ │ │ ├── test1/ │ │ │ │ ├── template.base.peb │ │ │ │ ├── template.peb │ │ │ │ ├── template.result.twig.txt │ │ │ │ └── template.result.txt │ │ │ ├── test10/ │ │ │ │ ├── template.base.1.peb │ │ │ │ ├── template.base.2.peb │ │ │ │ ├── template.base.3.peb │ │ │ │ ├── template.peb │ │ │ │ ├── template.result.twig.txt │ │ │ │ └── template.result.txt │ │ │ ├── test11/ │ │ │ │ ├── template.base.1.peb │ │ │ │ ├── template.base.2.peb │ │ │ │ ├── template.base.3.peb │ │ │ │ ├── template.peb │ │ │ │ ├── template.result.twig.txt │ │ │ │ └── template.result.txt │ │ │ ├── test12/ │ │ │ │ ├── template.base.1.peb │ │ │ │ ├── template.base.2.peb │ │ │ │ ├── template.base.3.peb │ │ │ │ ├── template.peb │ │ │ │ ├── template.result.twig.txt │ │ │ │ └── template.result.txt │ │ │ ├── test13/ │ │ │ │ ├── template.embedbase.1.peb │ │ │ │ ├── template.embedbase.2.peb │ │ │ │ ├── template.embedbase.3.peb │ │ │ │ ├── template.extendsbase.1.peb │ │ │ │ ├── template.extendsbase.2.peb │ │ │ │ ├── template.extendsbase.3.peb │ │ │ │ ├── template.peb │ │ │ │ ├── template.result.twig.txt │ │ │ │ └── template.result.txt │ │ │ ├── test14/ │ │ │ │ ├── template.embedbase.1.peb │ │ │ │ ├── template.extendsbase.1.peb │ │ │ │ ├── template.extendsbase.2.peb │ │ │ │ ├── template.extendsbase.3.peb │ │ │ │ ├── template.peb │ │ │ │ ├── template.result.twig.txt │ │ │ │ └── template.result.txt │ │ │ ├── test15/ │ │ │ │ ├── template.base.peb │ │ │ │ ├── template.error.txt │ │ │ │ └── template.peb │ │ │ ├── test16/ │ │ │ │ ├── template.base.peb │ │ │ │ ├── template.error.txt │ │ │ │ └── template.peb │ │ │ ├── test17/ │ │ │ │ ├── template.base.peb │ │ │ │ ├── template.error.txt │ │ │ │ └── template.peb │ │ │ ├── test18/ │ │ │ │ ├── template.base.peb │ │ │ │ ├── template.error.txt │ │ │ │ └── template.peb │ │ │ ├── test2/ │ │ │ │ ├── template.base.peb │ │ │ │ ├── template.peb │ │ │ │ └── template.result.txt │ │ │ ├── test3/ │ │ │ │ ├── template.base.peb │ │ │ │ ├── template.peb │ │ │ │ └── template.result.txt │ │ │ ├── test4/ │ │ │ │ ├── template.base.peb │ │ │ │ ├── template.peb │ │ │ │ ├── template.result.twig.txt │ │ │ │ └── template.result.txt │ │ │ ├── test5/ │ │ │ │ ├── template.base.peb │ │ │ │ ├── template.peb │ │ │ │ ├── template.result.twig.txt │ │ │ │ └── template.result.txt │ │ │ ├── test6/ │ │ │ │ ├── template.base.peb │ │ │ │ ├── template.peb │ │ │ │ ├── template.result.twig.txt │ │ │ │ └── template.result.txt │ │ │ ├── test7/ │ │ │ │ ├── template.base.1.peb │ │ │ │ ├── template.base.2.peb │ │ │ │ ├── template.base.3.peb │ │ │ │ ├── template.peb │ │ │ │ ├── template.result.twig.txt │ │ │ │ └── template.result.txt │ │ │ ├── test8/ │ │ │ │ ├── template.base.1.peb │ │ │ │ ├── template.base.2.peb │ │ │ │ ├── template.base.3.peb │ │ │ │ ├── template.peb │ │ │ │ ├── template.result.twig.txt │ │ │ │ └── template.result.txt │ │ │ └── test9/ │ │ │ ├── template.base.1.peb │ │ │ ├── template.base.2.peb │ │ │ ├── template.base.3.peb │ │ │ ├── template.peb │ │ │ ├── template.result.twig.txt │ │ │ └── template.result.txt │ │ ├── function/ │ │ │ ├── template.block.peb │ │ │ ├── template.child.peb │ │ │ ├── template.childThenParentThenMacro.peb │ │ │ ├── template.childWithContext.peb │ │ │ ├── template.parent.peb │ │ │ ├── template.parentAccessContext.peb │ │ │ ├── template.parentThenMacro.peb │ │ │ └── template.subchild.peb │ │ ├── loader/ │ │ │ └── template.loaderTest.peb │ │ ├── macros/ │ │ │ ├── from.peb │ │ │ ├── import.as.peb │ │ │ ├── include.peb │ │ │ ├── index.peb │ │ │ ├── invalid.from.peb │ │ │ ├── invalid.from.sameAlias.peb │ │ │ ├── invalid.from.unknownMacro.peb │ │ │ ├── invalid.import.as.sameAlias.peb │ │ │ ├── invalid.macro.peb │ │ │ ├── macro.peb │ │ │ ├── setVariableBase.peb │ │ │ └── setVariableMacro.peb │ │ ├── relativepath/ │ │ │ ├── subdirectory1/ │ │ │ │ ├── template.backwardslashes.peb │ │ │ │ └── template.forwardslashes.peb │ │ │ ├── subdirectory2/ │ │ │ │ └── template.relativeinclude2.peb │ │ │ ├── template.relativeextends1.peb │ │ │ ├── template.relativeextends2.peb │ │ │ ├── template.relativeimport1.peb │ │ │ ├── template.relativeimport2.peb │ │ │ ├── template.relativeinclude1.peb │ │ │ └── template.relativeinclude2.peb │ │ ├── single-block/ │ │ │ ├── template.renderextendedblock1.peb │ │ │ └── template.renderextendedblock2.peb │ │ ├── template.autoescapeInclude1.peb │ │ ├── template.autoescapeInclude2.peb │ │ ├── template.autoescapeParent1.peb │ │ ├── template.autoescapeParent2.peb │ │ ├── template.child.peb │ │ ├── template.concurrent1.peb │ │ ├── template.concurrent2.peb │ │ ├── template.dynamicChild.peb │ │ ├── template.dynamicParent1.peb │ │ ├── template.dynamicParent2.peb │ │ ├── template.errorReporting.peb │ │ ├── template.escapeCharactersInText.peb │ │ ├── template.general.peb │ │ ├── template.grandfather.peb │ │ ├── template.import.dynamic.macro_ajax.peb │ │ ├── template.import.dynamic.macro_classic.peb │ │ ├── template.import.dynamic.peb │ │ ├── template.importWithinBlock.peb │ │ ├── template.include.dynamic.adminFooter.peb │ │ ├── template.include.dynamic.defaultFooter.peb │ │ ├── template.include.dynamic.peb │ │ ├── template.include1.peb │ │ ├── template.include2.peb │ │ ├── template.includeInheritance1.peb │ │ ├── template.includeInheritance2.peb │ │ ├── template.includeInheritance3.peb │ │ ├── template.includeOverrideBlock.peb │ │ ├── template.includeOverrideBlock2.peb │ │ ├── template.includeOverrideVariable1.peb │ │ ├── template.includeOverrideVariable2.peb │ │ ├── template.includePropagatesContext.peb │ │ ├── template.includePropagatesContext2.peb │ │ ├── template.includeWithParameter1.peb │ │ ├── template.includeWithParameter2.peb │ │ ├── template.includeWithParameterNotIsolated1.peb │ │ ├── template.includeWithParameterNotIsolated2.peb │ │ ├── template.includeWithParameterObject1.peb │ │ ├── template.includeWithParameterObject2.peb │ │ ├── template.includeWithinBlock.peb │ │ ├── template.loaderTest.peb │ │ ├── template.loaderTest.peb.suffix │ │ ├── template.macro.child.peb │ │ ├── template.macro.exploding.peb │ │ ├── template.macro.parent.peb │ │ ├── template.macro1.peb │ │ ├── template.macro2.peb │ │ ├── template.macro3.peb │ │ ├── template.macroDouble.peb │ │ ├── template.parallelInclude1.peb │ │ ├── template.parallelInclude2.peb │ │ ├── template.parallelParsing1.peb │ │ ├── template.parallelParsing2.peb │ │ ├── template.parallelWithImport.peb │ │ ├── template.parallelWithImport2.peb │ │ ├── template.parent.peb │ │ ├── template.parent2.peb │ │ ├── template.set.child.peb │ │ ├── template.set.parent.peb │ │ ├── template.skipGenerationBlock1.peb │ │ ├── template.skipGenerationBlock2.peb │ │ ├── template.skipGenerationBlock3.peb │ │ ├── template.skipGenerationMacro1.peb │ │ ├── template.skipGenerationMacro2.peb │ │ ├── template.skipGenerationMacro3.peb │ │ ├── template.strictModeComplexExpression.peb │ │ └── template.strictModeSimpleExpression.peb │ ├── testMessages.properties │ └── testMessages_es_US.properties ├── pebble-spring/ │ ├── pebble-legacy-spring-boot-starter/ │ │ ├── pom.xml │ │ └── src/ │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ └── io/ │ │ │ │ └── pebbletemplates/ │ │ │ │ └── boot/ │ │ │ │ └── autoconfigure/ │ │ │ │ ├── AbstractPebbleConfiguration.java │ │ │ │ ├── PebbleAutoConfiguration.java │ │ │ │ ├── PebbleProperties.java │ │ │ │ ├── PebbleReactiveWebConfiguration.java │ │ │ │ ├── PebbleServletWebConfiguration.java │ │ │ │ ├── PebbleTemplateAvailabilityProvider.java │ │ │ │ └── package-info.java │ │ │ └── resources/ │ │ │ └── META-INF/ │ │ │ ├── spring/ │ │ │ │ ├── aot.factories │ │ │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports │ │ │ └── spring.factories │ │ └── test/ │ │ ├── java/ │ │ │ └── io/ │ │ │ └── pebbletemplates/ │ │ │ └── boot/ │ │ │ ├── AppConfig.java │ │ │ ├── Application.java │ │ │ ├── Controllers.java │ │ │ ├── Foo.java │ │ │ └── autoconfigure/ │ │ │ ├── NonWebAppTests.java │ │ │ ├── PebbleAutoConfigurationTest.java │ │ │ ├── ReactiveAppTest.java │ │ │ └── ServletAppTest.java │ │ └── resources/ │ │ ├── messages.properties │ │ ├── messages_es.properties │ │ └── templates/ │ │ ├── beans.peb │ │ ├── contextPath.peb │ │ ├── extensions.peb │ │ ├── hello.peb │ │ ├── index.peb │ │ ├── native-image.peb │ │ ├── responseObject.peb │ │ └── session.peb │ ├── pebble-spring-boot-starter/ │ │ ├── pom.xml │ │ └── src/ │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ └── io/ │ │ │ │ └── pebbletemplates/ │ │ │ │ └── boot/ │ │ │ │ └── autoconfigure/ │ │ │ │ ├── AbstractPebbleConfiguration.java │ │ │ │ ├── PebbleAutoConfiguration.java │ │ │ │ ├── PebbleProperties.java │ │ │ │ ├── PebbleReactiveWebConfiguration.java │ │ │ │ ├── PebbleServletWebConfiguration.java │ │ │ │ ├── PebbleTemplateAvailabilityProvider.java │ │ │ │ └── package-info.java │ │ │ └── resources/ │ │ │ └── META-INF/ │ │ │ ├── spring/ │ │ │ │ ├── aot.factories │ │ │ │ ├── org.springframework.boot.autoconfigure.AutoConfiguration.imports │ │ │ │ ├── org.springframework.boot.webflux.test.autoconfigure.AutoConfigureWebFlux.imports │ │ │ │ └── org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureWebMvc.imports │ │ │ └── spring.factories │ │ └── test/ │ │ ├── java/ │ │ │ └── io/ │ │ │ └── pebbletemplates/ │ │ │ └── boot/ │ │ │ ├── AppConfig.java │ │ │ ├── Application.java │ │ │ ├── Controllers.java │ │ │ ├── Foo.java │ │ │ └── autoconfigure/ │ │ │ ├── NonWebAppTests.java │ │ │ ├── PebbleAutoConfigurationTest.java │ │ │ ├── ReactiveAppTest.java │ │ │ └── ServletAppTest.java │ │ └── resources/ │ │ ├── messages.properties │ │ ├── messages_es.properties │ │ └── templates/ │ │ ├── beans.peb │ │ ├── contextPath.peb │ │ ├── extensions.peb │ │ ├── hello.peb │ │ ├── index.peb │ │ ├── native-image.peb │ │ ├── responseObject.peb │ │ └── session.peb │ ├── pebble-spring6/ │ │ ├── pom.xml │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── io/ │ │ │ └── pebbletemplates/ │ │ │ └── spring/ │ │ │ ├── context/ │ │ │ │ └── Beans.java │ │ │ ├── extension/ │ │ │ │ ├── SpringExtension.java │ │ │ │ └── function/ │ │ │ │ ├── HrefFunction.java │ │ │ │ ├── MessageSourceFunction.java │ │ │ │ └── bindingresult/ │ │ │ │ ├── BaseBindingResultFunction.java │ │ │ │ ├── GetAllErrorsFunction.java │ │ │ │ ├── GetFieldErrorsFunction.java │ │ │ │ ├── GetGlobalErrorsFunction.java │ │ │ │ ├── HasErrorsFunction.java │ │ │ │ ├── HasFieldErrorsFunction.java │ │ │ │ └── HasGlobalErrorsFunction.java │ │ │ ├── reactive/ │ │ │ │ ├── PebbleReactiveView.java │ │ │ │ └── PebbleReactiveViewResolver.java │ │ │ └── servlet/ │ │ │ ├── PebbleView.java │ │ │ └── PebbleViewResolver.java │ │ └── test/ │ │ ├── java/ │ │ │ └── io/ │ │ │ └── pebbletemplates/ │ │ │ └── spring/ │ │ │ ├── PebbleViewResolverTest.java │ │ │ ├── bean/ │ │ │ │ └── SomeBean.java │ │ │ └── config/ │ │ │ └── MVCConfig.java │ │ └── resources/ │ │ └── io/ │ │ └── pebbletemplates/ │ │ └── spring/ │ │ ├── expectedResponse/ │ │ │ ├── beansTest.html │ │ │ ├── bindingResultTest.html │ │ │ ├── bindingResultWithMacroTest.html │ │ │ ├── hrefFunctionTest.html │ │ │ ├── messageEnTest.html │ │ │ ├── messageFrTest.html │ │ │ ├── requestTest.html │ │ │ ├── responseTest.html │ │ │ └── sessionTest.html │ │ ├── messages_en.properties │ │ ├── messages_fr.properties │ │ └── template/ │ │ ├── beansTest.html │ │ ├── bindingResultTest.html │ │ ├── bindingResultWithMacroTest.html │ │ ├── hrefFunctionTest.html │ │ ├── messageEnTest.html │ │ ├── messageFrTest.html │ │ ├── requestTest.html │ │ ├── responseTest.html │ │ └── sessionTest.html │ ├── pebble-spring7/ │ │ ├── pom.xml │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── io/ │ │ │ └── pebbletemplates/ │ │ │ └── spring/ │ │ │ ├── context/ │ │ │ │ └── Beans.java │ │ │ ├── extension/ │ │ │ │ ├── SpringExtension.java │ │ │ │ └── function/ │ │ │ │ ├── HrefFunction.java │ │ │ │ ├── MessageSourceFunction.java │ │ │ │ └── bindingresult/ │ │ │ │ ├── BaseBindingResultFunction.java │ │ │ │ ├── GetAllErrorsFunction.java │ │ │ │ ├── GetFieldErrorsFunction.java │ │ │ │ ├── GetGlobalErrorsFunction.java │ │ │ │ ├── HasErrorsFunction.java │ │ │ │ ├── HasFieldErrorsFunction.java │ │ │ │ └── HasGlobalErrorsFunction.java │ │ │ ├── reactive/ │ │ │ │ ├── PebbleReactiveView.java │ │ │ │ └── PebbleReactiveViewResolver.java │ │ │ └── servlet/ │ │ │ ├── PebbleView.java │ │ │ └── PebbleViewResolver.java │ │ └── test/ │ │ ├── java/ │ │ │ └── io/ │ │ │ └── pebbletemplates/ │ │ │ └── spring/ │ │ │ ├── PebbleViewResolverTest.java │ │ │ ├── bean/ │ │ │ │ └── SomeBean.java │ │ │ └── config/ │ │ │ └── MVCConfig.java │ │ └── resources/ │ │ └── io/ │ │ └── pebbletemplates/ │ │ └── spring/ │ │ ├── expectedResponse/ │ │ │ ├── beansTest.html │ │ │ ├── bindingResultTest.html │ │ │ ├── bindingResultWithMacroTest.html │ │ │ ├── hrefFunctionTest.html │ │ │ ├── messageEnTest.html │ │ │ ├── messageFrTest.html │ │ │ ├── requestTest.html │ │ │ ├── responseTest.html │ │ │ └── sessionTest.html │ │ ├── messages_en.properties │ │ ├── messages_fr.properties │ │ └── template/ │ │ ├── beansTest.html │ │ ├── bindingResultTest.html │ │ ├── bindingResultWithMacroTest.html │ │ ├── hrefFunctionTest.html │ │ ├── messageEnTest.html │ │ ├── messageFrTest.html │ │ ├── requestTest.html │ │ ├── responseTest.html │ │ └── sessionTest.html │ └── pom.xml └── pom.xml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ # Auto detect text files and perform LF normalization * text=auto ================================================ FILE: .github/FUNDING.yml ================================================ buy_me_a_coffee: erbussierel ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: Community Support url: https://github.com/PebbleTemplates/pebble/discussions about: Please ask and answer questions here. ================================================ FILE: .github/ISSUE_TEMPLATE/issue.md ================================================ --- name: General about: Bugs, enhancements, documentation, tasks. title: '' labels: '' assignees: '' --- ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: "maven" directories: - "/pebble" - "/pebble-spring/pebble-spring7" - "/pebble-spring/pebble-spring-boot-starter" schedule: interval: "daily" labels: - "dependencies" commit-message: prefix: "chore" include: "scope" ignore: - dependency-name: "javax.servlet:servlet-api" update-types: ["version-update:semver-major", "version-update:semver-minor", "version-update:semver-patch"] - dependency-name: "jakarta.servlet:jakarta.servlet-api" update-types: ["version-update:semver-major", "version-update:semver-minor", "version-update:semver-patch"] groups: all-dependencies: patterns: - "*" - package-ecosystem: "maven" directories: - "/pebble-spring/pebble-spring6" - "/pebble-spring/pebble-legacy-spring-boot-starter" schedule: interval: "daily" labels: - "dependencies" commit-message: prefix: "chore" include: "scope" ignore: - dependency-name: "jakarta.servlet:jakarta.servlet-api" update-types: ["version-update:semver-major", "version-update:semver-minor", "version-update:semver-patch"] - dependency-name: "*" update-types: ["version-update:semver-major"] groups: all-dependencies: patterns: - "*" - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" ================================================ FILE: .github/workflows/maven.yml ================================================ # This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven # This workflow uses actions that are not certified by GitHub. # They are provided by a third-party and are governed by # separate terms of service, privacy policy, and support # documentation. name: Build on: push: branches: [ "*" ] pull_request: branches: [ "master" ] jobs: build: runs-on: ubuntu-latest strategy: matrix: java-version: [ 17, 21, 25 ] steps: - uses: actions/checkout@v6 - name: Set up JDK uses: actions/setup-java@v5 with: java-version: ${{ matrix.java-version }} distribution: 'temurin' cache: maven - name: Build with Maven run: mvn -B package --file pom.xml # Optional: Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive #- name: Update dependency graph # uses: advanced-security/maven-dependency-submission-action@571e99aab1055c2e71a1e2309b9691de18d6b7d6 ================================================ FILE: .gitignore ================================================ *.class # Package Files # *.jar *.war *.ear # Mac OS X .DS_Store # Eclipse # .project .classpath .settings/ # IntelliJ .idea/ *.iml *.ipr *.iws target/ /target /bin/ .gradle/ build/ *.releaseBackup release.properties ================================================ FILE: LICENSE ================================================ Copyright (c) 2013, Mitchell Bösecke All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the {organization} nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: README.md ================================================ # Pebble [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://buymeacoffee.com/erbussierel) [![Build](https://github.com/PebbleTemplates/pebble/actions/workflows/maven.yml/badge.svg)](https://github.com/PebbleTemplates/pebble/actions/workflows/maven.yml) Pebble is a java templating engine inspired by [Twig](https://twig.symfony.com/). It separates itself from the crowd with its inheritance feature and its easy-to-read syntax. It ships with built-in autoescaping for security, and it includes integrated support for internationalization. For more information please visit the [website](https://pebbletemplates.io). # Breaking changes in version 4.1.x - If you do not provide a custom Loader, Pebble will now use only a `ClasspathLoader` by default, same as the spring autoconfiguration. Before that, it would have used an instance of the `DelegatingLoader` which consists of a `ClasspathLoader` and a `FileLoader` behind the scenes to find your templates. - Modify the `FileLoader` to use a mandatory base directory parameter. # Breaking changes in version 4.0.x - Use one of the following artifactId according to the spring boot version that you are using | ArtifactId | spring-boot version | |-----------------------------------|---------------------| | pebble-legacy-spring-boot-starter | 3.x.x | | pebble-spring-boot-starter | 4.x.x | - The following spring boot properties has been moved to `.servlet` or `.reactive` | Old property | New Property | |---------------------------------|-----------------------------------------| | pebble.allowRequestOverride | pebble.servlet.allowRequestOverride | | pebble.allowSessionOverride | pebble.servlet.allowSessionOverride | | pebble.cache | pebble.servlet.cache | | pebble.contentType | pebble.servlet.contentType | | pebble.exposeRequestAttributes | pebble.servlet.exposeRequestAttributes | | pebble.exposeSessionAttributes | pebble.servlet.exposeSessionAttributes | | pebble.exposeSpringMacroHelpers | pebble.servlet.exposeSpringMacroHelpers | | | pebble.reactive.mediaTypes | For more information, please consult the spring-boot integration documentation in the [Boot externalized configuration section](https://pebbletemplates.io/wiki/guide/spring-boot-integration/) # Breaking changes in version 3.2.x - Rename package from `com.mitchellbosecke` to `io.pebbletemplates` - Change default suffix to `.peb` instead of `.pebble` in spring boot autoconfiguration - Rename method `getInstance` to `createInstance` in `BinaryOperator` interface (#521) ## License Copyright (c) 2013, Mitchell Bösecke All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the {organization} nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: SECURITY.md ================================================ # Security Policy ## Reporting a Vulnerability If you think you have found a security vulnerability, please ***DO NOT*** disclose it publicly until we've had a chance to fix it. Please don't report security vulnerabilities using GitHub issues, instead just click on `Report a vulnerability` button on this page to disclose them responsibly. ================================================ FILE: docs/pom.xml ================================================ 4.0.0 io.pebbletemplates pebble-project 4.1.2-SNAPSHOT docs pom Pebble docs Pebble documentation http://pebbletemplates.io 0.17.4 doc io.github.javaeden.orchid orchid-maven-plugin ${orchid.version} io.github.javaeden.orchid OrchidDocs ${orchid.version} io.github.javaeden.orchid OrchidPluginDocs ${orchid.version} io.github.javaeden.orchid OrchidGithub ${orchid.version} Editorial ${project.version} https://pebbletemplates.io verify deploy jcenter https://jcenter.bintray.com kotlinx https://kotlin.bintray.com/kotlinx ================================================ FILE: docs/src/orchid/resources/assets/css/github-fork-ribbon.css ================================================ /*! * "Fork me on GitHub" CSS ribbon v0.2.2 | MIT License * https://github.com/simonwhitaker/github-fork-ribbon-css */ .github-fork-ribbon { width: 12.1em; height: 12.1em; position: absolute; overflow: hidden; top: 0; right: 0; z-index: 9999; pointer-events: none; font-size: 13px; text-decoration: none; text-indent: -999999px; border-bottom: none; } .github-fork-ribbon.fixed { position: fixed; } .github-fork-ribbon:hover, .github-fork-ribbon:active { background-color: rgba(0, 0, 0, 0.0); } .github-fork-ribbon:before, .github-fork-ribbon:after { /* The right and left classes determine the side we attach our banner to */ position: absolute; display: block; width: 15.38em; height: 1.54em; top: 3.23em; right: -3.23em; -webkit-box-sizing: content-box; -moz-box-sizing: content-box; box-sizing: content-box; -webkit-transform: rotate(45deg); -moz-transform: rotate(45deg); -ms-transform: rotate(45deg); -o-transform: rotate(45deg); transform: rotate(45deg); } .github-fork-ribbon:before { content: ""; /* Add a bit of padding to give some substance outside the "stitching" */ padding: .38em 0; /* Set the base colour */ background-color: #a00; /* Set a gradient: transparent black at the top to almost-transparent black at the bottom */ background-image: -webkit-gradient(linear, left top, left bottom, from(rgba(0, 0, 0, 0)), to(rgba(0, 0, 0, 0.15))); background-image: -webkit-linear-gradient(top, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.15)); background-image: -moz-linear-gradient(top, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.15)); background-image: -ms-linear-gradient(top, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.15)); background-image: -o-linear-gradient(top, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.15)); background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.15)); /* Add a drop shadow */ -webkit-box-shadow: 0 .15em .23em 0 rgba(0, 0, 0, 0.5); -moz-box-shadow: 0 .15em .23em 0 rgba(0, 0, 0, 0.5); box-shadow: 0 .15em .23em 0 rgba(0, 0, 0, 0.5); pointer-events: auto; } .github-fork-ribbon:after { /* Set the text from the data-ribbon attribute */ content: attr(data-ribbon); /* Set the text properties */ color: #fff; font: 700 1em "Helvetica Neue", Helvetica, Arial, sans-serif; line-height: 1.54em; text-decoration: none; text-shadow: 0 -.08em rgba(0, 0, 0, 0.5); text-align: center; text-indent: 0; /* Set the layout properties */ padding: .15em 0; margin: .15em 0; /* Add "stitching" effect */ border-width: .08em 0; border-style: dotted; border-color: #fff; border-color: rgba(255, 255, 255, 0.7); } .github-fork-ribbon.left-top, .github-fork-ribbon.left-bottom { right: auto; left: 0; } .github-fork-ribbon.left-bottom, .github-fork-ribbon.right-bottom { top: auto; bottom: 0; } .github-fork-ribbon.left-top:before, .github-fork-ribbon.left-top:after, .github-fork-ribbon.left-bottom:before, .github-fork-ribbon.left-bottom:after { right: auto; left: -3.23em; } .github-fork-ribbon.left-bottom:before, .github-fork-ribbon.left-bottom:after, .github-fork-ribbon.right-bottom:before, .github-fork-ribbon.right-bottom:after { top: auto; bottom: 3.23em; } .github-fork-ribbon.left-top:before, .github-fork-ribbon.left-top:after, .github-fork-ribbon.right-bottom:before, .github-fork-ribbon.right-bottom:after { -webkit-transform: rotate(-45deg); -moz-transform: rotate(-45deg); -ms-transform: rotate(-45deg); -o-transform: rotate(-45deg); transform: rotate(-45deg); } ================================================ FILE: docs/src/orchid/resources/changelog/v3_0_0.md ================================================ --- version: '3.0.0' --- - Java 8 - Macros have access to all variables within the template and are no longer restricted to a "local scope" - New signature for filters/functions/tests that accepts the PebbleTemplate, EvaluationContext, and line number - Converted EvaluationContext to an interface with limited access to the underlying implementation - A "null" used as a boolean expression now throws an error if strict variables is true, treated as "false" otherwise - Improved error reporting - Java8 Date API support on date filter - Replace guava cache with caffeine - Add String Interpolation (#235) - Add the possibility to extend attribute access (#258) - Remove DynamicAttributeProvider - Render content on demand. Expose getLiteralTemplate(String templateName) (#295) - Introduce SpecializedWriter, (#358) - Many performance enhancements - Method resolution with overloaded method signatures (#367) - Use AbstractTemplateViewResolverProperties for spring-boot (#369) ================================================ FILE: docs/src/orchid/resources/changelog/v3_0_1.md ================================================ --- version: '3.0.1' --- - Macros are restricted to a local scope (#371) - Macros have access to global attributes via "_context" variable (#242) ================================================ FILE: docs/src/orchid/resources/changelog/v3_0_10.md ================================================ --- version: '3.0.10' --- - Add support for JDK 9++ (#442, #443) - Add macro support for errors(BindingResult) in spring extension - Add spring-petclinic example ================================================ FILE: docs/src/orchid/resources/changelog/v3_0_2.md ================================================ --- version: '3.0.2' --- - Add PebbleCache interface and use ConcurrentMap instead of Caffeine as default template/tag cache (#381) - Use unbescape instead of Coverity Escapers (#380) ================================================ FILE: docs/src/orchid/resources/changelog/v3_0_3.md ================================================ --- version: '3.0.3' --- - Support more expressions for if operator (#387) - Consider adding greedyMatchMethod to PebbleProperties (#388) - Use locale from context for lower/upper filter (#390) - Restore cache:invalidateAll method (#393) ================================================ FILE: docs/src/orchid/resources/changelog/v3_0_4.md ================================================ --- version: '3.0.4' --- - Make NOT operator more tolerant to argument type (#394) - Make ternary if operator more tolerant to argument type (#399) - Adjust AndExpression and OrExpression logic (#398) - Add JSON escaping as part of the default escaping strategies (#395) ================================================ FILE: docs/src/orchid/resources/changelog/v3_0_5.md ================================================ --- version: '3.0.5' --- - Cannot access List methods (#402) - Implement to boolean smart-casting in OR and AND operators (#405) ================================================ FILE: docs/src/orchid/resources/changelog/v3_0_6.md ================================================ --- version: '3.0.6' --- - Check that parseNewVariableName() parses a valid identifier (#409) - Set up Pebble documentation site using Orchid (#411) - Add twig compatibility matrix documentation (#247) ================================================ FILE: docs/src/orchid/resources/changelog/v3_0_7.md ================================================ --- version: '3.0.7' --- - Add Automatic-Module-Name to support Java 9 modules (#416) ================================================ FILE: docs/src/orchid/resources/changelog/v3_0_8.md ================================================ --- version: '3.0.8' --- - Add split filter (#421) ================================================ FILE: docs/src/orchid/resources/changelog/v3_0_9.md ================================================ --- version: '3.0.9' --- - Add embed tag (#319, #318, #224, #378, #434) - Offline documentation (#282, #432) - Make allowGetClass check case insensitive (#435, #436) ================================================ FILE: docs/src/orchid/resources/changelog/v3_1_0.md ================================================ --- version: '3.1.0' --- - **BREAKING CHANGES**: Consider avoiding feature version in spring boot starter name (#459) - Upgrade spring boot integration to Spring Boot 2.1 (#460) - Allow override core operators (#455, #456) - Allows variables to be set in templates when provided context is an immutable map (#453) - Fixed a few typos in the docs (#452) - Remove unnecessary variable 'state' in LexerImpl by using Stack (#450) - Updates Orchid to 0.17.1 and fixes its breaking changes (#448) - Null value in a Map should not throw AttributeNotFoundException (#447, #446) ================================================ FILE: docs/src/orchid/resources/changelog/v3_1_1.md ================================================ --- version: '3.1.1' --- - Use a list of unsafeMethods and rename allowGetClass to allowUnsafeMethods (#454) - PebbleTemplateAvailabilityProvider doesn't find template files from the classpath (#464) - Added inline verbatim description to Verbatim tag documentation (#422) - Fix issue of embed tag changing cached templates (#475) - Fixes bug which allowed sequential String literals to be parsed correctly (#482) - Update to spring boot 2.2 (486) ================================================ FILE: docs/src/orchid/resources/changelog/v3_1_2.md ================================================ --- version: '3.1.2' --- - Add support for jdk13 (#487) - Add basic support for Spring 5 WebFlux reactive in Spring Boot 2 (#333) ================================================ FILE: docs/src/orchid/resources/changelog/v3_1_3.md ================================================ --- version: '3.1.3' --- - Allow treating all number literals as BigDecimals to avoid NumberFormatExceptions (#503) - Custom operator can't use external context (#497) - Allow all variable names to be retrieve from ScopeChain/Scope (#291) - Empty map in strict mode does not throw exception (#491) ================================================ FILE: docs/src/orchid/resources/changelog/v3_1_4.md ================================================ --- version: '3.1.4' --- - Slice filter: Use collection size when toIndex is greater than collection size (#504) - Adjust spring boot doc (#509) - Build with jdk14 (#508) - Set proxyBeanMethods to false and build with spring boot 2.3 (#507) - Add access to Spring Beans/request/session and response when using Pebble with WebFlux (#512) - Remove allowUnsafeMethods property and replace it with methodAccessValidator. Default one is BlacklistMethodAccessValidtor (#511) ================================================ FILE: docs/src/orchid/resources/changelog/v3_1_5.md ================================================ --- version: '3.1.5' --- - Add timeZone parameter to date filter (#530) - Add setting to limit the size of output when evaluating a template (#529) - Fix variable name starting with operator (#541) - Added Servlet5Loader to support Servlet 5.0 (#549) - Throw an error when an import alias references an invalid macro (#559) - Special for loop variables class, to support all attribute resolvers (#560) - Support all Java identifiers (#544) - Provide option to customize stock extensions (#552) - Cannot use sort filter right after split filter (#568) - Support array of List in sort filter (#569) - Extension global variables in macros (#571) - Add base64 and sha256 filters (#574) ================================================ FILE: docs/src/orchid/resources/changelog/v3_1_6.md ================================================ --- version: '3.1.6' --- - Make CacheKey class extendible (#604) - Remove support for spring 1.5.x (#628) - Fix for older Android versions (#585 #581 #580) ================================================ FILE: docs/src/orchid/resources/changelog/v3_2_0.md ================================================ --- version: '3.2.0' --- - Add support for spring framework 6 and spring-boot 3 (#630) - Bump minimum supported java version to 17 in pebble-spring6 and pebble-spring-boot-starter in order to work with spring (#630) - Add a memory loader that supports inheritance and doesn't require a filesystem. This is useful for applications that retrieve templates from a database for example (#617). - **BREAKING CHANGE**: Change default suffix to `.peb` instead of `.pebble` in spring boot autoconfiguration (#553) - **BREAKING CHANGE**: Rename method `getInstance` to `createInstance` in `BinaryOperator` interface (#521) - **BREAKING CHANGE**: Rename package from `com.mitchellbosecke` to `io.pebbletemplates` (#635) ================================================ FILE: docs/src/orchid/resources/changelog/v3_2_1.md ================================================ --- version: '3.2.1' --- - Fix the null pointer exception on the replace filter (#650) - Add native hints for GraalVM (#648) ================================================ FILE: docs/src/orchid/resources/changelog/v3_2_2.md ================================================ --- version: '3.2.2' --- - Throw a more detailed exception from DefaultAttributeResolver (#653) - Make expression nodes fully visitable (#657) - Added getTemplates function to MemoryLoader (#660) - Generate OSGi metadata into pebble jar to make it OSGi compliant (#663) - Add flag for parameter name retention in maven compiler plugin (#664) ================================================ FILE: docs/src/orchid/resources/changelog/v3_2_3.md ================================================ --- version: '3.2.3' --- - Dynamically open different feature according to different scenarios (#671) - Fix the ability to provide your own PebbleViewResolver (#686) ================================================ FILE: docs/src/orchid/resources/changelog/v3_2_4.md ================================================ --- version: '3.2.4' --- - Fix the problem of unit test failure under JDK 21 (#692) - Fix the problem of not resolving reactive values in models (#691) ================================================ FILE: docs/src/orchid/resources/changelog/v4_0_0.md ================================================ --- version: '4.0.0' --- # BREAKING CHANGES - Add support for spring boot 4 (#704) - Use one of the following artifactId according to the spring boot version that you are using | ArtifactId | spring-boot version | |-----------------------------------|---------------------| | pebble-legacy-spring-boot-starter | 3.x.x | | pebble-spring-boot-starter | 4.x.x | - The following spring boot properties has been moved to `.servlet` or `.reactive` | Old property | New Property | |---------------------------------|-----------------------------------------| | pebble.allowRequestOverride | pebble.servlet.allowRequestOverride | | pebble.allowSessionOverride | pebble.servlet.allowSessionOverride | | pebble.cache | pebble.servlet.cache | | pebble.contentType | pebble.servlet.contentType | | pebble.exposeRequestAttributes | pebble.servlet.exposeRequestAttributes | | pebble.exposeSessionAttributes | pebble.servlet.exposeSessionAttributes | | pebble.exposeSpringMacroHelpers | pebble.servlet.exposeSpringMacroHelpers | | | pebble.reactive.mediaTypes | For more information, please consult the spring-boot integration documentation in the [Boot externalized configuration section](https://pebbletemplates.io/wiki/guide/spring-boot-integration/) # New Features - Add nl2br filter (#699) ================================================ FILE: docs/src/orchid/resources/changelog/v4_1_0.md ================================================ --- version: '4.1.0' --- # BREAKING CHANGES - If you do not provide a custom Loader, Pebble will now use only a `ClasspathLoader` by default, same as the spring autoconfiguration. Before that, it would have used an instance of the `DelegatingLoader` which consists of a `ClasspathLoader` and a `FileLoader` behind the scenes to find your templates. - Modify the `FileLoader` to use a mandatory base directory parameter. # Security - Fix [CVE-2025-1686](https://nvd.nist.gov/vuln/detail/CVE-2025-1686) (#715) # New Features - Use a default existing format of `yyyy-MM-dd'T'HH:mm:ssZ` when using date filter with a string (#677) - Look for exact method / field match when doing reflection. Look for method get/is/has if none match (#712) - Support dynamic named arguments (#741) # Bug Fixes - NaN must return false instead of throwing an exception (#695) # Dependency Upgrades - Upgrade to SLF4J 2.0.17 (#709) ================================================ FILE: docs/src/orchid/resources/changelog/v4_1_1.md ================================================ --- version: '4.1.1' --- # New Features - Expose AST Root Node to Enable Custom NodeVisitor Implementation (#701) # Bug Fixes - Catch all exceptions in DelegatingLoader (#765) ================================================ FILE: docs/src/orchid/resources/config.yml ================================================ site: about: siteName: 'Pebble Templates' subtitle: 'A lightweight but rock solid Java templating engine.' theme: primaryColor: '#72A6D0' menu: - type: 'separator' title: 'About' - type: 'page' title: 'Home' itemId: 'Home' - type: 'page' itemId: 'Spring Boot Integration' - type: 'link' title: 'Spring petclinic example' url: 'https://github.com/PebbleTemplates/spring-petclinic' - type: 'page' itemId: 'Twig Compatibility' - type: 'page' itemId: 'Contributing' - type: 'page' itemId: 'Changelog' - type: 'link' title: 'Offline Documentation (PDF)' url: '#{$0|baseUrl}/wiki/book.pdf' - type: 'link' title: 'Buy me a coffee' url: 'https://buymeacoffee.com/erbussierel' - type: 'separator' title: 'Wiki' - type: 'wiki' - type: 'separator' title: 'API Docs' - type: 'page' title: 'Javadocs' itemId: 'io.pebbletemplates' extraCss: - 'assets/css/pygments.scss' - 'assets/css/orchidJavadoc.scss' - 'assets/css/github-fork-ribbon.css' services: publications: stages: - type: githubPages branch: gh-pages publishType: CleanBranch username: PebbleTemplates repo: pebble - type: githubReleases repo: 'PebbleTemplates/pebble' allPages: components: - type: 'pageContent' - type: 'prism' languages: - 'java' - 'twig' - 'markup' wiki: defaultConfig: includeIndexInPageTitle: false createPdf: true javadoc: sourceDirs: - '../../../../pebble/src/main/java' pages: menu: - type: 'javadocPackages' - type: 'javadocClasses' ================================================ FILE: docs/src/orchid/resources/data/contributors.yml ================================================ - name: Mitchell Bösecke link: https://github.com/mbosecke - name: Eric Bussieres link: https://github.com/ebussieres - name: Héctor López link: https://github.com/hectorlf - name: Djalma Olivera link: https://github.com/djalmaoliveira - name: Vladimir Loshchin link: https://github.com/badbob - name: yanxiyue link: https://github.com/yanxiyue - name: Vladimir Danilov link: https://github.com/diasonti - name: Casey Brooks link: https://github.com/cjbrooks12 - name: Bastien Jansen link: https://github.com/bjansen ================================================ FILE: docs/src/orchid/resources/data/twig-compatibility/filters.yml ================================================ - name: 'abs' twigLink: 'https://twig.symfony.com/doc/2.x/filters/abs.html' support: 'full' page: 'abs' - name: 'batch' twigLink: 'https://twig.symfony.com/doc/2.x/filters/batch.html' support: 'none' - name: 'capitalize' twigLink: 'https://twig.symfony.com/doc/2.x/filters/capitalize.html' support: 'full' page: 'capitalize' - name: 'convert_encoding' twigLink: 'https://twig.symfony.com/doc/2.x/filters/convert_encoding.html' support: 'none' - name: 'date' twigLink: 'https://twig.symfony.com/doc/2.x/filters/date.html' support: 'partial' page: 'date' notes: 'This filter uses `java.util.Date` date formatting instead of PHP-style formatting.' - name: 'date_modify' twigLink: 'https://twig.symfony.com/doc/2.x/filters/date_modify.html' support: 'none' - name: 'default' twigLink: 'https://twig.symfony.com/doc/2.x/filters/default.html' support: 'full' page: 'default' - name: 'escape' twigLink: 'https://twig.symfony.com/doc/2.x/filters/escape.html' support: 'full' page: 'escape' - name: 'first' twigLink: 'https://twig.symfony.com/doc/2.x/filters/first.html' support: 'full' page: 'first' - name: 'format' twigLink: 'https://twig.symfony.com/doc/2.x/filters/format.html' support: 'full' page: 'format' - name: 'join' twigLink: 'https://twig.symfony.com/doc/2.x/filters/join.html' support: 'none' page: 'join' - name: 'json_encode' twigLink: 'https://twig.symfony.com/doc/2.x/filters/json_encode.html' support: 'none' - name: 'keys' twigLink: 'https://twig.symfony.com/doc/2.x/filters/keys.html' support: 'none' - name: 'last' twigLink: 'https://twig.symfony.com/doc/2.x/filters/last.html' support: 'full' page: 'last' - name: 'length' twigLink: 'https://twig.symfony.com/doc/2.x/filters/length.html' support: 'full' page: 'length' - name: 'lower' twigLink: 'https://twig.symfony.com/doc/2.x/filters/lower.html' support: 'full' page: 'lower' - name: 'merge' twigLink: 'https://twig.symfony.com/doc/2.x/filters/merge.html' support: 'none' - name: 'nl2br' twigLink: 'https://twig.symfony.com/doc/2.x/filters/nl2br.html' support: 'full' page: 'nl2br' - name: 'number_format' twigLink: 'https://twig.symfony.com/doc/2.x/filters/number_format.html' support: 'partial' page: 'numberformat' notes: 'Filter is named `numberformat`, and uses `java.text.DecimalFormat` formatting instead of PHP-style formatting.' - name: 'raw' twigLink: 'https://twig.symfony.com/doc/2.x/filters/raw.html' support: 'full' page: 'raw' - name: 'replace' twigLink: 'https://twig.symfony.com/doc/2.x/filters/replace.html' support: 'full' page: 'replace' - name: 'reverse' twigLink: 'https://twig.symfony.com/doc/2.x/filters/reverse.html' support: 'full' page: 'reverse' - name: 'round' twigLink: 'https://twig.symfony.com/doc/2.x/filters/round.html' support: 'none' - name: 'slice' twigLink: 'https://twig.symfony.com/doc/2.x/filters/slice.html' support: 'full' page: 'slice' - name: 'sort' twigLink: 'https://twig.symfony.com/doc/2.x/filters/sort.html' support: 'full' page: 'sort' - name: 'split' twigLink: 'https://twig.symfony.com/doc/2.x/filters/split.html' support: 'full' page: 'split' - name: 'striptags' twigLink: 'https://twig.symfony.com/doc/2.x/filters/striptags.html' support: 'none' - name: 'title' twigLink: 'https://twig.symfony.com/doc/2.x/filters/title.html' support: 'full' page: 'title' - name: 'trim' twigLink: 'https://twig.symfony.com/doc/2.x/filters/trim.html' support: 'full' page: 'trim' - name: 'upper' twigLink: 'https://twig.symfony.com/doc/2.x/filters/upper.html' support: 'full' page: 'upper' - name: 'url_encode' twigLink: 'https://twig.symfony.com/doc/2.x/filters/url_encode.html' support: 'partial' page: 'urlencode' notes: 'Filter is named `urlencode`.' ================================================ FILE: docs/src/orchid/resources/data/twig-compatibility/functions.yml ================================================ - name: 'attribute' twigLink: 'https://twig.symfony.com/doc/2.x/functions/attribute.html' support: 'none' - name: 'block' twigLink: 'https://twig.symfony.com/doc/2.x/functions/block.html' support: 'full' page: 'block' - name: 'constant' twigLink: 'https://twig.symfony.com/doc/2.x/functions/constant.html' support: 'none' - name: 'cycle' twigLink: 'https://twig.symfony.com/doc/2.x/functions/cycle.html' support: 'none' - name: 'date' twigLink: 'https://twig.symfony.com/doc/2.x/functions/date.html' support: 'none' - name: 'dump' twigLink: 'https://twig.symfony.com/doc/2.x/functions/dump.html' support: 'none' - name: 'include' twigLink: 'https://twig.symfony.com/doc/2.x/functions/include.html' support: 'none' - name: 'max' twigLink: 'https://twig.symfony.com/doc/2.x/functions/max.html' support: 'full' page: 'max' - name: 'min' twigLink: 'https://twig.symfony.com/doc/2.x/functions/min.html' support: 'full' page: 'min' - name: 'parent' twigLink: 'https://twig.symfony.com/doc/2.x/functions/parent.html' support: 'full' page: 'parent' - name: 'random' twigLink: 'https://twig.symfony.com/doc/2.x/functions/random.html' support: 'none' - name: 'range' twigLink: 'https://twig.symfony.com/doc/2.x/functions/range.html' support: 'full' page: 'range' - name: 'source' twigLink: 'https://twig.symfony.com/doc/2.x/functions/source.html' support: 'none' - name: 'template_from_string' twigLink: 'https://twig.symfony.com/doc/2.x/functions/template_from_string.html' support: 'none' ================================================ FILE: docs/src/orchid/resources/data/twig-compatibility/operators.yml ================================================ - name: 'in' twigLink: 'https://twig.symfony.com/doc/2.x/templates.html#containment-operator' support: 'none' page: 'contains' notes: '`contains` can be used instead' - name: 'is' twigLink: 'https://twig.symfony.com/doc/2.x/templates.html#test-operator' support: 'full' page: 'is' - name: 'Math (+, -, /, %, //, *, **)' twigLink: 'https://twig.symfony.com/doc/2.x/templates.html#math' support: 'partial' page: 'math' notes: '`//` and `**` not supported' - name: 'Logic (and, or, not, (), b-and, b-xor, b-or)' twigLink: 'https://twig.symfony.com/doc/2.x/templates.html#logic' support: 'partial' page: 'logic' notes: 'Bitwise logical operators are not supported' - name: 'Comparisons (==, !=, <, >, >=, <=, ===, starts with, ends with, matches)' twigLink: 'https://twig.symfony.com/doc/2.x/templates.html#comparisons' support: 'partial' page: 'comparisons' notes: '`===`, `starts with`, `ends with`, and `matches` not supported' - name: 'Others (.., |, ~, ., [], ?:)' twigLink: 'https://twig.symfony.com/doc/2.x/templates.html#other-operators' support: 'full' page: 'others' notes: 'subscript operator `[]` index starts at 0, not 1' ================================================ FILE: docs/src/orchid/resources/data/twig-compatibility/tags.yml ================================================ - name: 'autoescape' twigLink: 'https://twig.symfony.com/doc/2.x/tags/autoescape.html' support: 'full' page: 'autoescape' - name: 'block' twigLink: 'https://twig.symfony.com/doc/2.x/tags/block.html' support: 'full' page: 'block' - name: 'do' twigLink: 'https://twig.symfony.com/doc/2.x/tags/do.html' support: 'none' - name: 'embed' twigLink: 'https://twig.symfony.com/doc/2.x/tags/embed.html' support: 'partial' page: 'embed' notes: '`only` clause and `ignore missing` clause not supported' - name: 'extends' twigLink: 'https://twig.symfony.com/doc/2.x/tags/extends.html' support: 'full' page: 'extends' - name: 'filter' twigLink: 'https://twig.symfony.com/doc/2.x/tags/filter.html' support: 'full' page: 'filter' - name: 'flush' twigLink: 'https://twig.symfony.com/doc/2.x/tags/flush.html' support: 'full' page: 'flush' - name: 'for' twigLink: 'https://twig.symfony.com/doc/2.x/tags/for.html' support: 'full' page: 'for' - name: 'from' twigLink: 'https://twig.symfony.com/doc/2.x/tags/from.html' support: 'full' page: 'from' - name: 'if' twigLink: 'https://twig.symfony.com/doc/2.x/tags/if.html' support: 'full' page: 'if' - name: 'import' twigLink: 'https://twig.symfony.com/doc/2.x/tags/import.html' support: 'full' page: 'import' - name: 'include' twigLink: 'https://twig.symfony.com/doc/2.x/tags/include.html' support: 'partial' page: 'include' notes: '`only` clause and `ignore missing` clause not supported' - name: 'macro' twigLink: 'https://twig.symfony.com/doc/2.x/tags/macro.html' support: 'full' page: 'macro' - name: 'sandbox' twigLink: 'https://twig.symfony.com/doc/2.x/tags/sandbox.html' support: 'none' - name: 'set' twigLink: 'https://twig.symfony.com/doc/2.x/tags/set.html' support: 'full' page: 'set' - name: 'spaceless' twigLink: 'https://twig.symfony.com/doc/2.x/tags/spaceless.html' support: 'none' - name: 'use' twigLink: 'https://twig.symfony.com/doc/2.x/tags/use.html' support: 'none' - name: 'verbatim' twigLink: 'https://twig.symfony.com/doc/2.x/tags/verbatim.html' support: 'full' page: 'verbatim' - name: 'with' twigLink: 'https://twig.symfony.com/doc/2.x/tags/with.html' support: 'none' ================================================ FILE: docs/src/orchid/resources/data/twig-compatibility/tests.yml ================================================ - name: 'constant' twigLink: 'https://twig.symfony.com/doc/2.x/tests/constant.html' support: 'none' - name: 'defined' twigLink: 'https://twig.symfony.com/doc/2.x/tests/defined.html' support: 'none' - name: 'divisible by' twigLink: 'https://twig.symfony.com/doc/2.x/tests/divisibleby.html' support: 'none' - name: 'empty' twigLink: 'https://twig.symfony.com/doc/2.x/tests/empty.html' support: 'full' page: 'empty' - name: 'even' twigLink: 'https://twig.symfony.com/doc/2.x/tests/even.html' support: 'full' page: 'even' - name: 'iterable' twigLink: 'https://twig.symfony.com/doc/2.x/tests/iterable.html' support: 'full' page: 'iterable' - name: 'null' twigLink: 'https://twig.symfony.com/doc/2.x/tests/null.html' support: 'full' page: 'null' - name: 'odd' twigLink: 'https://twig.symfony.com/doc/2.x/tests/odd.html' support: 'full' page: 'odd' - name: 'same as' twigLink: 'https://twig.symfony.com/doc/2.x/tests/sameas.html' support: 'none' ================================================ FILE: docs/src/orchid/resources/homepage.md ================================================ --- --- Pebble is a Java templating engine inspired by Twig and similar to the Python [Jinja Template Engine](https://palletsprojects.com/p/jinja/) syntax. It features templates inheritance and easy-to-read syntax, ships with built-in autoescaping for security, and includes integrated support for internationalization. ## Features * **Rich set of built-in tags and filters** * **Template inheritance**: extract common areas of your content in a single ‘layout’ and make your templates inherit this layout. * **Extensible language**: new tags, filters and functions can be added to Pebble very easily. If you already know Twig, you can compare both engines in {{ anchor('the compatibility matrix', 'Twig Compatibility') }}. ## Basic Usage First, add the following dependency to your pom.xml: ```xml io.pebbletemplates pebble {{ site.version }} ``` Then create a template in your WEB-INF folder. Let's start with a base template that all other templates will inherit from, name it "base.html": ```twig {% verbatim %} {% block title %}My Website{% endblock %}
{% block content %}{% endblock %}
{% endverbatim %} ``` Then create a template that extends base.html, call it "home.html": ```twig {% verbatim %} {% extends "base.html" %} {% block title %} Home {% endblock %} {% block content %}

Home

Welcome to my home page. My name is {{ name }}.

{% endblock %} {% endverbatim %} ``` Now we want to compile the template, and render it: ```java PebbleEngine engine = new PebbleEngine.Builder().build(); PebbleTemplate compiledTemplate = engine.getTemplate("home.html"); Map context = new HashMap<>(); context.put("name", "Mitchell"); Writer writer = new StringWriter(); compiledTemplate.evaluate(writer, context); String output = writer.toString(); ``` The output should result in the following: ```twig Home

Home

Welcome to my home page. My name is Mitchell.

``` For more information on installation and configuration, see {{ anchor('the installation guide', 'Installation and Configuration') }}.
For more information on basic usage, see {{ anchor('the basic usage guide', 'Basic Usage') }}.
For Spring Boot integration, see {{ anchor('the Spring Boot integration guide', 'Spring Boot Integration') }}. ================================================ FILE: docs/src/orchid/resources/pages/CNAME ================================================ --- renderMode: raw usePrettyUrl: false --- pebbletemplates.io ================================================ FILE: docs/src/orchid/resources/pages/changelog.md ================================================ --- components: - type: changelog --- ================================================ FILE: docs/src/orchid/resources/pages/contributing.md ================================================ --- --- # Contributing ## Help Wanted Contributors of all types are welcome but most importantly I am looking for help with the following: - IDE Integration - API Feedback - Testing and Bug Reports - Performance Optimizations - Improving Thread Safety General improvements are welcome, otherwise you can help tackle some of the [known issues](https://github.com/PebbleTemplates/pebble/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc). ## Getting Started - Fork the [repository](https://github.com/PebbleTemplates/pebble) - Make the fix. - Use maven to build and test: - `mvn clean install` from root to build Pebble - `mvn orchid:serve -P doc` from `docs/` to build and serve documentation on http://localhost:8080 - Submit a pull request ## Understanding the Code There are a few major components that play a vital role in the compilation process. The main `PebbleEngine`, the `LexerImpl`, and the `ParserImpl`. The `PebbleEngine` is responsible for beginning the template compilation process. It begins by passing the template source to the `Lexer`. The `Lexer` is responsible for converting the template into a bunch of `Token` objects. A token is the smallest distinguishable unit in a template, i.e. it can not be broken down into more specific objects. A token can represent a delimiter (eg. `{{ '{{' }}` ), or a function name, a string, plain text, or many other things. Once the lexer has established the entire stream of tokens which collectively make up the entire template, it returns this `TokenStream` back to the main `PebbleEngine`. The `PebbleEngine` now sends the `TokenStream` to the `Parser` which is responsible for turning those token objects into `Node` objects. The nodes are built in a hierarchical fashion and together they make up the abstract syntax tree. The parser uses the help of the main `ExpressionParser` as well as the `TokenParser` objects which are provided by the extensions. Each node is responsible for rendering itself during the template evaluation phase. The `PebbleEngine` now has a tree of node objects, beginning with the `NodeRoot`. It will then create a `PebbleTemplateImpl` object with this root node as an argument. The last step before returning this template to the user is to invoke both the internal and the user-provided `NodeVisitor` objects on the node tree. Node visitors are used to process the node tree, perhaps to add extra functionality (ex. the built-in autoescaper uses a node visitor to wrap the expressions of nodes with an escape function). Once this processing is completed, the template is placed into the cache and finally returned to the user. The user will then call `evaluate()` on this template which begins the evaluation process. The template will call `render()` on it's root node which in turn will call `render()` on each of it's children nodes. This process recurses throughout the whole node tree until all nodes have rendered themselves to the provided `Writer` object. ## Documentation The documentation website is generated using [Orchid](https://orchid.netlify.com/). The documentation files are under the `src/orchid/resources` directory. ## Contributing Code Currently there aren't any formal guidelines. Just ensure that your changes include any necessary unit tests and send me a pull request on [github](https://github.com/PebbleTemplates/pebble)! ## Standards and Guidelines ### Code Formatting Standards 1. Use [Google Style Guide](https://google.github.io/styleguide/javaguide.html) formatting conventions. 1. Configuring IntelliJ to use the Google Style Guide Formatter. 1. Clone the [Google Style Git Repo](https://github.com/google/styleguide). 2. Open **Preferences -> Editor -> Code Style -> Java**. 4. Click on the gear and select **Import Scheme -> IntelliJ IDEA code style XML** and import the file **intellij-java-google-style.xml** from the [Google Style Git Repo](https://github.com/google/styleguide). 2. Configuring Eclipse to use the Google Style Guide Formatter. 1. Clone the [Google Style Git Repo](https://github.com/google/styleguide). 2. Select **Preference -> Java -> Code Style -> Formatter**. 3. Select **Import** and specify the file **eclipse-java-google-style.xml** from the [Google Style Git Repo](https://github.com/google/styleguide). ### Unit Test Guidelines 1. Use [AssertJ]() for all Unit and Integration tests. 2. A javadoc description should be included for each test method. 3. The purpose of each test should be clear from the test method name. ## Acknowledgements Thanks to all the following contributors who are helping to make Pebble the best template engine available for Java. {%- for contributor in data.contributors %} * [{{ contributor.name }}]({{ contributor.link }}) {%- endfor %} ================================================ FILE: docs/src/orchid/resources/pages/twig-compatibility.peb ================================================ --- title: Twig Compatibility --- While Pebble was inspired by and generally has high compatibility with Twig, it is neither a complete nor an exact Twig implementation. There are features of Twig not supported by Pebble, there are features implemented in Pebble that are not supported by Twig, and some features might behave differently or use a different name or syntax. This page outlines the similarities and differences between Pebble and Twig. {{ compat('Tags', data['twig-compatibility'].tags ) }} {{ compat('Filters', data['twig-compatibility'].filters ) }} {{ compat('Functions', data['twig-compatibility'].functions ) }} {{ compat('Tests', data['twig-compatibility'].tests ) }} {{ compat('Operators', data['twig-compatibility'].operators ) }} {% macro compat(type, items) %}

{{ type }}

{% for item in items %} {% endfor %}
Name Pebble Support Notes
{{ item.name }} {{ item.support }} {% if item.page is not empty %} {{ anchor('(docs)', item.page) }} {% endif %} {{ item.notes | compileAs('md') }}
{% endmacro %} ================================================ FILE: docs/src/orchid/resources/templates/includes/sidebar.peb ================================================ ================================================ FILE: docs/src/orchid/resources/templates/layouts/index.peb ================================================ {% include '?trackingHeadStart' %} {% head %} {% styles %} {% include '?trackingHeadEnd' %} {% include '?trackingBodyStart' %}
Fork me on GitHub
{% block innerHeader %} {% include 'includes/header' %} {% endblock %} {% block banner %} {% endblock %} {% block pageContent %}

{{ page.title }}

{% page %}
{% endblock %}

{% include 'includes/footer.peb' %}
{% include 'includes/sidebar.peb' %}
{% scripts %} {% include '?trackingBodyEnd' %} ================================================ FILE: docs/src/orchid/resources/wiki/filter/abbreviate.md ================================================ # `abbreviate` The `abbreviate` filter will abbreviate a string using an ellipsis. It takes one argument which is the max width of the desired output including the length of the ellipsis. ```twig {{ "this is a long sentence." | abbreviate(7) }} ``` The above example will output the following: ```twig this... ``` ## Arguments - length ================================================ FILE: docs/src/orchid/resources/wiki/filter/abs.md ================================================ # `abs` The `abs` filter is used to obtain the absolute value. ```twig {{ -7 | abs }} {# output: 7 #} ``` ================================================ FILE: docs/src/orchid/resources/wiki/filter/base64decode.md ================================================ --- --- # `base64decode` The `base64decode` filter takes the given input, Base64-decodes it, if possible, and returns the byte array converted to UTF-8 String. Applying the filter on an incorrect base64-encoded string will throw an exception. ```twig {% verbatim %}{{ "dGVzdA==" | base64decode }}{% endverbatim %} ``` The above example will output the following: ``` test ``` ================================================ FILE: docs/src/orchid/resources/wiki/filter/base64encode.md ================================================ --- --- # `base64encode` The `base64encode` filter takes the given input, converts it to an UTF-8 String (`.toString()`) and Base64-encodes it. ```twig {% verbatim %}{{ "test" | base64encode }}{% endverbatim %} ``` The above example will output the following: ``` dGVzdA== ``` ================================================ FILE: docs/src/orchid/resources/wiki/filter/capitalize.md ================================================ --- --- # `capitalize` The `capitalize` filter will capitalize the first letter of the string. ```twig {% verbatim %} {{ "article title" | capitalize }} {% endverbatim %} ``` The above example will output the following: ```twig Article title ``` See also: {{ anchor('title') }} ================================================ FILE: docs/src/orchid/resources/wiki/filter/date.md ================================================ # `date` The `date` filter formats a date in a variety of formats. It can handle old-school `java.util.Date`, Java 8 `java.time` constructs like `OffsetDateTime` and timestamps in milliseconds from the epoch. The filter will construct a `java.text.SimpleDateFormat` or `java.time.format.DateTimeFormatter` using the provided pattern and then use this newly created format to format the provided date object. If you don't provide a pattern, either `DateTimeFormatter.ISO_DATE_TIME` or `yyyy-MM-dd'T'HH:mm:ssZ` will be used. ```twig {{ user.birthday | date("yyyy-MM-dd") }} ``` An alternative way to use this filter is to use it on a string but then provide two arguments: the first is the desired pattern for the output, and the second is the existing format used to parse the input string into a `java.util.Date` object. ```twig {{ "July 24, 2001" | date("yyyy-MM-dd", existingFormat="MMMM dd, yyyy") }} ``` The above example will output the following: ```twig 2001-07-24 ``` ## Time zones If the provided date has time zone info (e.g. `OffsetDateTime`) then it will be used. If the provided date has no time zone info, by default the system time zone will be used. If you need to use a specific time zone then you can pass in a `timeZone` parameter any string that's understood by `ZoneId` / `ZoneInfo`: ```twig {# the timeZone parameter will be ignored #} {{ someOffsetDateTime | date("yyyy-MM-dd'T'HH:mm:ssX", timeZone="UTC") }} {# the provided time zone will override the system default #} {{ someInstant | date("yyyy-MM-dd'T'HH:mm:ssX", timeZone="Pacific/Funafuti") }} ``` ## Arguments - format - existingFormat - timeZone ================================================ FILE: docs/src/orchid/resources/wiki/filter/default.md ================================================ ## `default` The `default` filter will render a default value if and only if the object being filtered is empty. A variable is empty if it is null, an empty string, an empty collection, or an empty map. ```twig {{ user.phoneNumber | default("No phone number") }} ``` In the following example, if `foo`, `bar`, or `baz` are null the output will become an empty string which is a perfect use case for the default filter: ```twig {{ foo.bar.baz | default("No baz") }} ``` Note that the default filter will suppress any `AttributeNotFoundException` exceptions that will usually be thrown when `strictVariables` is set to `true`. ## Arguments - default ================================================ FILE: docs/src/orchid/resources/wiki/filter/escape.md ================================================ --- --- # `escape` The `escape` filter will turn special characters into safe character references in order to avoid XSS vulnerabilities. This filter will typically only need to be used if you've turned off autoescaping. ```twig {% verbatim %} {{ "
" | escape }} {# output: <div> #} {% endverbatim %} ``` Please read the {{ anchor('escaping guide', 'Escaping') }} for more information about escaping. ## Arguments - strategy ================================================ FILE: docs/src/orchid/resources/wiki/filter/first.md ================================================ # `first` The `first` filter will return the first item of a collection, or the first letter of a string. ```twig {{ users | first }} {# will output the first item in the collection named 'users' #} {{ 'Mitch' | first }} {# will output 'M' #} ``` ================================================ FILE: docs/src/orchid/resources/wiki/filter/format.md ================================================ # `format` The `format` filter formats a string by replacing placeholders with the provided arguments (placeholders follows the `String.format` notation). ```twig {{ 'Hello %s!' | format('World') }} {# Hello World! #} ``` ================================================ FILE: docs/src/orchid/resources/wiki/filter/join.md ================================================ # `join` The `join` filter will concatenate all items of a collection into a string. An optional argument can be given to be used as the separator between items. ```twig {# List names = new ArrayList<>(); names.add("Alex"); names.add("Joe"); names.add("Bob"); #} {{ names | join(',') }} {# will output: Alex,Joe,Bob #} ``` ## Arguments - separator ================================================ FILE: docs/src/orchid/resources/wiki/filter/last.md ================================================ # `last` The `last` filter will return the last item of a collection, or the last letter of a string. ```twig {{ users | last }} {# will output the last item in the collection named 'users' #} {{ 'Mitch' | last }} {# will output 'h' #} ``` ================================================ FILE: docs/src/orchid/resources/wiki/filter/length.md ================================================ # `length` The `length` filter returns the number of items of collection, map or the length of a string: ```twig {% if users|length > 10 %} ... {% endif %} ``` ================================================ FILE: docs/src/orchid/resources/wiki/filter/lower.md ================================================ # `lower` The `lower` filter makes an entire string lower case. ```twig {{ "THIS IS A LOUD SENTENCE" | lower }} ``` The above example will output the following: ```twig this is a loud sentence ``` ================================================ FILE: docs/src/orchid/resources/wiki/filter/nl2br.md ================================================ # `nl2br` The `nl2br` filter converts newline characters (`\r`, `\n`, `\r\n`) in a string to HTML line break tags (`
`). This is useful when you want to preserve line breaks in text when displaying it in a web page. ```pebble {{ "I like Pebble.\nYou will like it too."|nl2br }} {# outputs I like Pebble.
You will like it too. #} ``` ================================================ FILE: docs/src/orchid/resources/wiki/filter/numberformat.md ================================================ # `numberformat` The `numberformat` filter is used to format a decimal number. Behind the scenes it uses `java.text.DecimalFormat`. ```twig {{ 3.141592653 | numberformat("#.##") }} ``` The above example will output the following: ```twig 3.14 ``` ## Arguments - format ================================================ FILE: docs/src/orchid/resources/wiki/filter/raw.md ================================================ --- --- # `raw` The `raw` filter prevents the output of an expression from being escaped by the autoescaper. The `raw` filter must be the very last operation performed within the expression otherwise the autoescaper will deem the expression as potentially unsafe and escape it regardless. ```twig {% verbatim %} {% set danger = "
" %} {{ danger | upper | raw }} {# ouptut:
#} {% endverbatim %} ``` If the `raw` filter is not the last operation performed then the expression will be escaped: ```twig {% verbatim %} {% set danger = "
" %} {{ danger | raw | upper }} {# output: <DIV> #} {% endverbatim %} ``` Please read the {{ anchor('escaping guide', 'Escaping') }} for more information about escaping. ================================================ FILE: docs/src/orchid/resources/wiki/filter/replace.md ================================================ # `replace` The 'replace' filter formats a given string by replacing the placeholders (placeholders are free-form): ```twig {{ "I like %this% and %that%." | replace({'%this%': foo, '%that%': "bar"}) }} ``` ## Arguments - placeholders to replace ================================================ FILE: docs/src/orchid/resources/wiki/filter/reverse.md ================================================ # `reverse` The 'reverse' filter reverses a List: ```twig {% for user in users | reverse %} {{ user }} {% endfor %} ``` ================================================ FILE: docs/src/orchid/resources/wiki/filter/rsort.md ================================================ # `rsort` The `rsort` filter will sort a list in reversed order. The items of the list must implement `Comparable`. ```twig {% for user in users | rsort %} {{ user.name }} {% endfor %} ``` ================================================ FILE: docs/src/orchid/resources/wiki/filter/sha256.md ================================================ --- --- # `sha256` The `sha256` filter returns the SHA-256 hash of the given UTF-8 String. ```twig {% verbatim %}{{ "test" | sha256 }}{% endverbatim %} ``` The above example will output the following: ``` 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08 ``` ================================================ FILE: docs/src/orchid/resources/wiki/filter/slice.md ================================================ # `slice` The `slice` filter returns a portion of a list, array, or string. ```twig {{ ['apple', 'peach', 'pear', 'banana'] | slice(1,3) }} {# results in: [peach, pear] #} {{ 'Mitchell' | slice(1,3) }} {# results in: 'it' #} ``` ## Arguments - `fromIndex`: 0-based and inclusive - `toIndex`: 0-based and exclusive ================================================ FILE: docs/src/orchid/resources/wiki/filter/sort.md ================================================ # `sort` The `sort` filter will sort a list. The items of the list must implement `Comparable`. ```twig {% for user in users | sort %} {{ user.name }} {% endfor %} ``` ================================================ FILE: docs/src/orchid/resources/wiki/filter/split.md ================================================ # `split` The `split` filter splits a string by the given delimiter and returns a list of strings. ```twig {% set foo = "one,two,three" | split(',') %} {# foo contains ['one', 'two', 'three'] #} ``` You can also pass a limit argument: - If `limit` is positive, then the pattern will be applied at most n - 1 times, the array's length will be no greater than n, and the array's last entry will contain all input beyond the last matched delimiter; - If `limit` is negative, then the pattern will be applied as many times as possible and the array can have any length; - If `limit` is zero, then the pattern will be applied as many times as possible, the array can have any length, and trailing empty strings will be discarded; ```twig {% set foo = "one,two,three,four,five" | split(',', 3) %} {# foo contains ['one', 'two', 'three,four,five'] #} ``` ## Arguments - delimiter: The delimiter - limit: The limit argument ================================================ FILE: docs/src/orchid/resources/wiki/filter/title.md ================================================ --- --- # `title` The `title` filter will capitalize the first letter of each word. ```twig {% verbatim %} {{ "article title" | title }} {% endverbatim %} ``` The above example will output the following: ```twig Article Title ``` See also: {{ anchor('capitalize') }} ================================================ FILE: docs/src/orchid/resources/wiki/filter/trim.md ================================================ # `trim` The `trim` filter is used to trim whitespace off the beginning and end of a string. ```twig {{ " This text has too much whitespace. " | trim }} ``` The above example will output the following: ```twig This text has too much whitespace. ``` ================================================ FILE: docs/src/orchid/resources/wiki/filter/upper.md ================================================ # `upper` The `upper` filter makes an entire string upper case. ```twig {{ "this is a quiet sentence." | upper }} ``` The above example will output the following: ```twig THIS IS A QUIET SENTENCE. ``` ================================================ FILE: docs/src/orchid/resources/wiki/filter/urlencode.md ================================================ # `urlencode` The `urlencod` translates a string into `application/x-www-form-urlencoded` format using the "UTF-8" encoding scheme. ```twig {{ "The string ü@foo-bar" | urlencode }} ``` The above example will output the following: ```twig The+string+%C3%BC%40foo-bar ``` ================================================ FILE: docs/src/orchid/resources/wiki/function/blockFunction.md ================================================ --- --- # `block` The `block` function is used to render the contents of a block more than once. It is not to be confused with the block *tag* which is used to declare blocks. The following example will render the contents of the "post" block twice; once where it was declared and again using the `block` function: ```twig {% verbatim %} {% block "post" %} content {% endblock %} {{ block("post") }} {% endverbatim %} ``` The above example will output the following: ```twig content content ``` ## Performance Warning The `block` function will impair the use of the {{ anchor('flush') }} tag used within the block being rendered. It is typically okay for a block to use the `flush` tag which will flush the already-rendered content to the user-provided `Writer` but the block function will internally use it's own `StringWriter` and therefore flushing inside the block will no longer do any good (nor will it do harm). ================================================ FILE: docs/src/orchid/resources/wiki/function/i18n.md ================================================ # `i18n` The `i18n` function is used to retrieve messages from a locale-specific `ResourceBundle`. Every `PebbleTemplate` is assigned a default locale from the `PebbleEngine`. At the point of evaluation, this locale can be changed with an argument to the `evaluate(...)` method of the individual template. The `i18n` function wraps around `ResourceBundle.getBundle(name, locale).getObject(key)`. The first argument to the `i18n` function is the name of the bundle and the second argument is the key within the bundle. ```twig {{ i18n("messages","greeting") }} ``` The above example assumes you have `messages.properties` on your classpath and that that file contains a key by the name of `greeting`. If the locale of that template was `es_US` for example, it would look for a `message_es_US.properties` file instead. Going a little further, you can use variables within your message and pass a list of params to this function which will replace your variables using `MessageFormat`: ```twig {# greeting.someone=Hello, {0} #} {{ i18n("messages","greeting", "Jacob") }} {# output: Hello, Jacob #} ``` ## Arguments - bundle - key - params ================================================ FILE: docs/src/orchid/resources/wiki/function/max.md ================================================ # `max` The `max` function will return the largest of it's numerical arguments. ```twig {{ max(user.age, 80) }} ``` ================================================ FILE: docs/src/orchid/resources/wiki/function/min.md ================================================ # `min` The `min` function will return the smallest of it's numerical arguments. ```twig {{ min(user.age, 80) }} ``` ================================================ FILE: docs/src/orchid/resources/wiki/function/parent.md ================================================ # `parent` The `parent` function is used inside of a block to render the content that the parent template would have rendered inside of the block had the current template not overriden it. It is similar to Java's `super` keyword. Let's assume you have a template, "parent.peb" that looks something like this: ```twig {% block "content" %} parent contents {% endblock %} ``` And then you have another template, "child.peb" that extends "parent.peb": ```twig {% extends "parent.peb" %} {% block "content" %} child contents {{ parent() }} {% endblock %} ``` The output will look something like the following: ```twig parent contents child contents ``` ================================================ FILE: docs/src/orchid/resources/wiki/function/range.md ================================================ # `range` The `range` function will return a list containing an arithmetic progression of numbers: ```twig {% for i in range(0, 3) %} {{ i }}, {% endfor %} {# outputs 0, 1, 2, 3, #} ``` When step is given (as the third parameter), it specifies the increment (or decrement): ```twig {% for i in range(0, 6, 2) %} {{ i }}, {% endfor %} {# outputs 0, 2, 4, 6, #} ``` Pebble built-in .. operator is just a shortcut for the range function with a step of 1+ ```twig {% for i in 0..3 %} {{ i }}, {% endfor %} {# outputs 0, 1, 2, 3, #} ``` ================================================ FILE: docs/src/orchid/resources/wiki/guide/basic-usage.md ================================================ --- --- # Basic Usage ## Introduction Pebble templates can be used to generate any sort of textual output. It is typically used to generate HTML but it can also be used to create CSS, XML, JS, etc. A template itself will contain whatever language you are attempting to output alongside Pebble-specific features and syntax. Here is a simple example that will generate a trivial HTML page: ```twig {% verbatim %} {{ websiteTitle }} {{ content }} {%- endverbatim %} ``` When you evaluate the template you will provide it with a "context" which is just a map of variables. This context should include the two variables above, `websiteTitle` and `content`. ## Set Up You will want to begin by creating a PebbleEngine object which is responsible for compiling your templates: ```java PebbleEngine engine = new PebbleEngine.Builder().build(); ``` And now, with your new PebbleEngine instance you can start compiling templates: ```java PebbleTemplate compiledTemplate = engine.getTemplate("templates/home.html"); ``` Finally, simply provide your compiled template with a java.io.Writer object and a Map of variables (the context) to get your output! ```java Writer writer = new StringWriter(); Map context = new HashMap<>(); context.put("websiteTitle", "My First Website"); context.put("content", "My Interesting Content"); compiledTemplate.evaluate(writer, context); String output = writer.toString(); ``` ## Syntax Reference There are two primary delimiters used within a Pebble template: `{% verbatim %}{{ ... }}{% endverbatim %}` and `{% verbatim %}{% ... %}{% endverbatim %}`. The first set of delimiters will output the result of an expression. Expressions can be very simple (ex. a variable name) or much more complex. The second set of delimiters is used to change the control flow of the template; it can contain an if-statement, define a parent template, define a new block, etc. ## Variables You can print variables directly to the output; for example, if the context contains a variable called `foo` which is a String with the value "bar" you can do the following which will output "bar". ```twig {% verbatim %}{{ foo }}{% endverbatim %} ``` You can use the dot (.) notation to access attributes of variables. If the attribute contains any atypical characters, you can use the subscript notation ([]) instead. ```twig {% verbatim %} {{ foo.bar }} {{ foo["bar"] }} {%- endverbatim %} ``` Behind the scenes `foo.bar` will attempt the following techniques to to access the `bar` attribute of the `foo` variable: - If `foo` is a Map, `foo.get("bar")` - `foo.getBar()` - `foo.isBar()` - `foo.hasBar()` - `foo.bar()` - `foo.bar` Additionally, if `foo` is a List, then `foo[0]` can be used instead of `foo.get(0)`. If the value of variable (or attribute) is null it will output an empty string. ## Type Safety Pebble templates are dynamically typed and any possible type safety issues won't occur until the actual runtime evaluation of your templates. Pebble does however allow you to choose how to handle type safety issues with the use of it's `strictVariables` setting. By default, `strictVariables` is set to `false` which means that the following: ```twig {% verbatim %}{{ foo.bar }}{% endverbatim %} ``` will print an empty string even if the object `foo` does not actually have an attribute called `bar`. If `strictVariables` is set to true, the above expression would throw an exception. When `strictVariables` is set to false your expressions are also null safe. The following expression will print an empty string even if foo and/or bar are null: ```twig {% verbatim %}{{ foo.bar.baz }}{% endverbatim %} ``` The {{ anchor('default') }} filter might come in handy for the above situations. ## Filters Output can be further modified with the use of filters. Filters are separated from the variable using a pipe symbol (`|`) and may have optional arguments in parentheses. Multiple filters can be chained and the output of one filter is applied to the next. ```twig {% verbatim %}{{ "If life gives you lemons, eat lemons." | upper | abbreviate(13) }}{% endverbatim %} ``` The above example will output the following: ```twig IF LIFE GI... ``` ## Functions Whereas filters are intended to modify existing content/variables, functions are intended to generate new content. Similar to other programming languages, functions are invoked via their name followed by parentheses (`()`). ```twig {% verbatim %}{{ max(user.score, highscore) }}{% endverbatim %} ``` ## Control Structure Pebble provides several tags to control the flow of your template, two of the main ones being the {{ anchor('for') }} loop, and {{ anchor('if') }} statements. ```twig {% verbatim %} {% for article in articles %}

{{ article.title }}

{{ article.content }}

{% else %}

There are no articles.

{% endfor %} {%- endverbatim %} ``` ```twig {% verbatim %} {% if category == "news" %} {{ news }} {% elseif category == "sports" %} {{ sports }} {% else %}

Please select a category

{% endif %} {%- endverbatim %} ``` ## Including other Templates The {{ anchor('include') }} tag is used to include the rendered output of one template into another. ```twig {% verbatim %} {%- endverbatim %} ``` ## Template Inheritance Template inheritance is the most powerful feature of Pebble. It allows templates to override sections of their parent template. In your parent template you define "blocks" which are the sections that are allowed to be overriden. First let us look at an example of a parent template: ```twig {% verbatim %} {% block title %}My Website{% endblock %}
{% block content %}{% endblock %}
{% endverbatim %} ``` In the above example, we have used the {{ anchor('block') }} tag to define several sections that child templates are allowed to override. A child template might look like this: ```twig {% verbatim %} {% extends "parent.html" %} {% block title %} Home {% endblock %} {% block content %}

Home

Welcome to my home page.

{% endblock %} {%- endverbatim %} ``` The first line uses the {{ anchor('extends') }} tag to declare the parent template. The extends tag should be the first tag in the template and there can only be one. Evaluating the child template will produce the following output: ```twig Home

Home

Welcome to my home page.

``` You may have noticed that in the above example, because the child template doesn't override the `footer` block, the value from the parent is used instead. Dynamic inheritance is possible by using an expression with the `extends` tag: ```twig {% verbatim %}{% extends ajax ? 'ajax.html' : 'base.html' %}{% endverbatim %} ``` ## Macros Macros are lightweight and reusable template fragments. A macro is defined via the {{ anchor('macro') }} tag: ```twig {% verbatim %} {% macro input(type, name) %} {% endmacro %} {% endverbatim %} ``` And the macro will be invoked just like a function: ```twig {% verbatim %}{{ input("text", "name", "Mitchell") }}{% endverbatim %} ``` Child templates will have access to macros defined in a parent template. To use macros located in a completely different template, you can use the {{ anchor('import') }}) tag. A macro does not have access to the main context; the only variables it can access are it's local arguments. ## Named Arguments Using named arguments allows you to be more explicit with the values you are passing to a filter, function, test or macro. They also allow you to avoid specifying arguments for which you don't want to change the default value. ```twig {% verbatim %}{{ stringDate | date(existingFormat="yyyy-MMMM-d", format="yyyy/MMMM/d") }}{% endverbatim %} ``` Positional arguments can be used in conjunction with named arguments but all positional arguments must come before any named arguments: ```twig {% verbatim %}{{ stringDate | date("yyyy/MMMM/d", existingFormat="yyyy-MMMM-d") }}{% endverbatim %} ``` Macros are a great use case for named arguments because they also allow you to define default values for unused arguments: ```twig {% verbatim %} {% macro input(type="text", name, value) %} {% endmacro %} {{ input(name="country") }} {# will output: #} {%- endverbatim %} ``` ## Escaping [XSS vulnerabilites](https://en.wikipedia.org/wiki/Cross-site_scripting) are the most common types of security vulnerabilities in web applications and in order to avoid them you must escape potentially unsafe data before presenting it to the end user. Pebble provides autoescaping of all such data which is enabled by default. Autoescaping can be turned off, in which case Pebble provides an escape filter for more fine-grained manual escaping. The following is an example of how autoescaping will escape your context variables: ```twig {% verbatim %} {% set danger = "
" %} {{ danger }} {# will output: <br> #} {%- endverbatim %} ``` If autoescaping is disabled you can still use the {{ anchor('escape') }} filter to aid with manual escaping: ```twig {% verbatim %} {% set danger = "
" %} {{ danger | escape }} {# will output: <br> #} {%- endverbatim %} ``` By default, the autoescaping mechanism and the escape filter assume that it is escaping within an HTML context. You may want to use an alternate escaping strategy depending on the context: ```twig {% verbatim %} {% set danger = "alert(...)" %} {%- endverbatim %} ``` See the {{ anchor('escaping guide', 'Escaping') }} for more information on how autoescaping works, how to disable it, and the various escaping strategies that are available. ## Whitespace The first newline after a pebble tag is automatically ignored; all other whitespace is ignored by Pebble and will be included in the rendered output. Pebble provides a whitespace control modifier to trim leading or trailing whitespace adjacent to any pebble tag. ```twig {% verbatim %}

{{- "no whitespace" -}}

{# output: "

no whitespace

" #} {%- endverbatim %} ``` It is also possible to only use the modifier on one side of the tag: ```twig {% verbatim %}

{{- "no leading whitespace" }}

{# output: "

no whitespace

" #} {%- endverbatim %} ``` ## Comments You can comment out any part of the template using the `{# ... #}` delimiters. These comments will not appear in the rendered output. ```twig {% verbatim %} {# THIS IS A COMMENT #} {% for article in articles %}

{{ article.title }}

{{ article.content }}

{% endfor %} {%- endverbatim %} ``` ## Expressions Expressions in a Pebble template are very similar to expressions found in Java. ### Literals The simplest form of expressions are literals. Literals are representations for Java types such as strings and numbers. - `"Hello World"`: Everything between two double or single quotes is a string. You can use a backslash to escape quotation marks within the string. - `"Hello #{who}"`: String interpolation is also possible using `#{}` inside quotes. In this example, if the value of the variable `who` is `"world"`, then the expression will be evaluated to `"Hello world"`. - `100 + 10l * 2.5`: Integers, longs and floating point numbers are similar to their Java counterparts. - `true` / `false`: Boolean values equivalent to their Java counterparts. - `null`: Represents no specific value, similar to it's Java counterpart. `none` is an alias for null. ### Collections Both lists and maps can be created directly within the template. - `["apple", "banana", "pear"]`: A list of strings - `{"apple":"red", "banana":"yellow", "pear":"green"}`: A map of strings The collections can contain expressions. ### Math Pebble allows you to calculate values using some basic mathematical operators. The following operators are supported: - `+`: Addition - `-`: Subtraction - `/`: Division - `%`: Modulus - `*`: Multiplication ### Logic You can combine multiple expressions with the following operators: - `and`: Returns true if both operands are true - `or`: Returns true if either operand is true - `not`: Negates an expression - `(...)`: Groups expressions together ### Comparisons The following comparison operators are supported in any expression: `==`, `!=`, `<`, `>`, `>=`, and `<=`. ```twig {% verbatim %} {% if user.age >= 18 %} ... {% endif %} {%- endverbatim %} ``` ### Tests The `is` operator performs tests. Tests can be used to test an expression for certain qualities. The right operand is the name of the test: ```twig {% verbatim %} {% if 3 is odd %} ... {% endif %} {%- endverbatim %} ``` Tests can be negated by using the is not operator: ```twig {% verbatim %} {% if name is not null %} ... {% endif %} {%- endverbatim %} ``` ### Conditional (Ternary) Operator The conditional operator is similar to its Java counterpart: ```twig {% verbatim %}{{ foo ? "yes" : "no" }}{% endverbatim %} ``` ### Operator Precedence In order from highest to lowest precedence: - `.` - `|` - `%`, `/`, `*` - `-`, `+` - `==`, `!=`, `>`, `<`, `>=`, `<=` - `is`, `is not` - `and` - `or` ### Limiting the size of the rendered output In case you’re running Pebble with templates provided by someone else, there’s an attack similar to [zip bombs](https://en.wikipedia.org/wiki/Zip_bomb) or [XML bombs](https://en.wikipedia.org/wiki/Billion_laughs_attack) that might cause your process to run out of memory. To protect against it, you can limit the size of the output when evaluating a template: ```java PebbleEngine pebble = new PebbleEngine.Builder() // Output should not exceed 10 MB. .maxRenderedSize(10 * 1024 * 1024) .build(); ``` This will throw a `PebbleException` when a template evaluation tries to write more characters than the limit you set. ### IDE's plugin If you want to add IDE's syntax highlighting, you can install this [plugin](https://plugins.jetbrains.com/idea/plugin/9407-pebble) for IntelliJ. Thank you to Bastien Jansen for his contribution. ================================================ FILE: docs/src/orchid/resources/wiki/guide/customize-defaults.md ================================================ --- --- Pebble comes with a rich set of built-in tags and filters that will help you render your templates into websites and other documents with ease. However, imagine a more specific use-case where the templates are not entirely under your control. In these cases it might be advised to consider stripping-down Pebbles' built-in functionality that may otherwise introduce security-concers regarding the integrity and stability of your application. ### Opt-Out using ExtensionCustomizer The `ExtensionCustomizer` base class can be used to gain access to the default functionality before it is loaded into Pebbles template engine. Overwrite methods to get hold on provided default-functionality and modify whatever should be available for the template engine. The following example removes the `ForTokenParser`, i.e. the ability to parse `{% for %}{{ ... }}{% endfor %}` constructs: ```java class ExampleOptOuts extends ExtensionCustomizer { public ExampleOptOuts(Extension ext) { super(ext); } @Override public List getTokenParsers() { List tokenParsers = Optional.ofNullable(super.getTokenParsers()) .map(ArrayList::new).orElseGet(ArrayList::new); tokenParsers.removeIf(x -> x instanceof ForTokenParser); return tokenParsers; } } ``` The `ExtensionCustomizer` will be used to wrap any Pebble-extension which is provided by default. It can be registered in your setup code to create `PebbleEngine`: ```java PebbleEngine engine = new PebbleEngine.Builder().registerExtensionCustomizer(ExampleOptOuts::new).build(); ``` ### Default implementation of ExtensionCustomizer The `DisallowExtensionCustomizerBuilder` class can be used to disallow some default functionality, make pebble more controllable. For example of use, see below: ```java PebbleEngine engine = new PebbleEngine.Builder() .registerExtensionCustomizer(new DisallowExtensionCustomizerBuilder() .disallowedTokenParserTags(singletonList("flush")) .disallowedFunctionKeys(singletonList("max")) .disallowedFilterKeys(singletonList("upper")) .disallowedTestKeys(singletonList("null")) .disallowedBinaryOperatorSymbols(singletonList(">")) .disallowedUnaryOperatorSymbols(singletonList("-")) .build()) .build(); ``` ================================================ FILE: docs/src/orchid/resources/wiki/guide/escaping.md ================================================ --- --- # Escaping ## Overview [XSS vulnerabilites](https://en.wikipedia.org/wiki/Cross-site_scripting) are the most common types of security vulnerabilities in web applications and in order to avoid them you must escape potentially unsafe data before presenting it to the end user. Pebble provides autoescaping of all such data which is enabled by default. Autoescaping can be turned off, in which case Pebble provides an {{ anchor('escape') }} filter for more fine-grained manual escaping. ## Autoescaping Autoescaping, which is enabled by default, will automatically escape the outcome of expressions contained within print delimiters, i.e. {% verbatim %}`{{` and `}}`{% endverbatim %}: ```twig {% verbatim %} {% set danger = "
" %} {{ danger }} {# will output: <br> #} {%- endverbatim %} ``` The {{ anchor('raw') }} filter can be used to prevent the autoescaper from escaping a particular expression. It is important that the raw filter is the last operation performed in the expression. ```twig {% verbatim %} {% set danger = "
" %} {{ danger | raw }} {# will output:
#} {%- endverbatim %} ``` If the raw filter is not the last operation performed within the expression, the expression will be deemed as possibly unsafe by the autoescaper and will be escaped. For example: ```twig {% verbatim %} {% set danger = "
" %} {{ danger | raw | uppercase }} {# will output: <BR> #} {%- endverbatim %} ``` ### Exceptions There are a few exceptions where expressions are **not** automatically escaped: - If the expression only contains a string literal, it is assumed to be safe. For example: ```twig {% verbatim %} {{ '
' }} {# will output:
#} {%- endverbatim %} ``` - The last operation contained within that expression is a filter or function that explicitly returns safe output. Such a filter or function would return an instance of `SafeString` instead of a regular String. The built-in filters that return safe markup include: `date`, `escape`, and `raw`. These filters must be the last operation performed within the expression in order for their output to be ignored by the autoescaper. For example: ```twig {% verbatim %} {% set danger = "
" %} {{ danger | uppercase | raw }} {# will output:
#} {%- endverbatim %} ``` ### Autoescape Tag The {{ anchor('autoescape') }} tag can be used to temporarily disable/re-enable the autoescaper as well as change the escaping strategy for a portion of the template. ```twig {% verbatim %} {{ danger }} {# will be escaped by default #} {% autoescape false %} {{ danger }} {# will not be escaped #} {% endautoescape %} {%- endverbatim %} ``` ```twig {% verbatim %} {{ danger }} {# will use the "html" escaping strategy #} {% autoescape "js" %} {{ danger }} {# will use the "js" escaping strategy #} {% endautoescape %} {%- endverbatim %} ``` ### Disabling Autoescaper ```java PebbleEngine engine = new PebbleEngine.Builder().autoEscaping(false).build(); ``` ## Manual Escaping If autoescaping is disabled you can still use the {{ anchor('escape') }} filter to aid with manual escaping: ```twig {% verbatim %} {% set danger = "
" %} {{ danger | escape }} {# will output: <br> #} {%- endverbatim %} ``` ## Strategies When escaping data it is crucial that you utilize the correct escaping strategy depending on the context of the data. By default, the autoescaper and the `escape` filter assume that you are escaping HTML data. I highly recommend reading the [OWASP Cheat Sheet](https://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet) to understand the significance of escaping context. Pebble provides the following escaping strategies: - html - js - css - url_param You can use the {{ anchor('autoescape') }} tag to temporarily change the strategy used by the autoescaper otherwise you can change the globally used default strategy: ```java PebbleEngine engine = new PebbleEngine.Builder().defaultEscapingStrategy("js").build(); ``` The escape filter will also accept a strategy as an argument: ```js {% verbatim %} var username ="{{ user.name | escape(strategy="js") }}"; {%- endverbatim %} ``` ### Custom Strategy You can add a custom escaping strategy by implementing `EscapingStrategy` and adding it to the `EscaperExtension`: ```java PebbleEngine engine = new PebbleEngine.Builder().addEscapingStrategy("custom", new CustomEscapingStrategy()).build(); ``` ================================================ FILE: docs/src/orchid/resources/wiki/guide/extending-pebble.md ================================================ --- --- # Extending Pebble ## Overview Pebble was designed to be flexible and accomodate the requirements of any project. You can add your own tags, functions, operators, filters, tests, and global variables. The majority of these are quite trivial to implement. Begin by creating a class that implements `Extension`. For your own convenience, I recommend extending `AbstractExtension` if you can. After implementing the required methods, register your extension with the `PebbleEngine` before compiling any templates: ```java PebbleEngine engine = new PebbleEngine.Builder().extension(new CustomExtension()).build(); ``` ## Filters To create custom filters, implement the `getFilters()` method of your extension which will return a map of filter names and their corresponding implementations. A filter implementation must implement the `Filter` interface. The `Filter` interface requires two methods to be implemented, `getArgumentNames()` and `apply()`. The `getArgumentNames()` method returns a list of Strings that define both the order and names of expected arguments. If this method returns null or an empty list, the filter supports dynamic arguments. This means the user can pass any number of arguments—either named (e.g., myFilter(param="value")) or positional (e.g., myFilter("value"))—and they will be available in the arguments map. The `apply` method is the actual filter implementation. Here's the parameters definition. | Parameter name | Description | | --- | --- | | input | the data to be filtered | | args | the map of arguments the user may have provided | | self | An instance of `PebbleTemplate` which can be used to retrieve the template name for example | | context | An instance of `EvaluationContext` which can be used to retrieve the locale for example| | lineNumber | Useful when throwing exception to provide line number | Because Pebble is dynamically typed, you will have to downcast the arguments to the expected type. Here is an example of how the {{ anchor('upper') }} filter might be implemented: ```java public class UpperFilter implements Filter { @Override public List getArgumentNames() { return null; } @Override public Object apply(Object input, Map args, PebbleTemplate self, EvaluationContext context, int lineNumber){ if(input == null){ return null; } if (input instanceof String) { return ((String) input).toUpperCase(context.getLocale()); } else { return input.toString().toUpperCase(context.getLocale()); } } } ``` ## Tests Adding custom tests is very similar to custom filters. Implement the `getTests()` method within your extension which will return a map of test names and their corresponding implementations. A test implementation will implement the `Test` interface. The `Test` interface is exactly like the `Filter` interface except the apply method returns a boolean instead of an arbitrary object of any type. Here is an example of how the {{ anchor('even') }} test might be implemented: ```java public class EvenTest implements Test { @Override public List getArgumentNames() { return null; } @Override public boolean apply(Object input, Map args, PebbleTemplate self, EvaluationContext context, int lineNumber){ if (input == null) { throw new PebbleException(null, "Can not pass null value to \"even\" test.", lineNumber, self.getName()); } if (input instanceof Integer) { return ((Integer) input) % 2 == 0; } else { return ((Long) input) % 2 == 0; } } } ``` ## Functions Adding functions is also very similar to custom filters. First and foremost, it's important to understand the different intentions behind a function and a filter because it can often be ambiguous which one should be implemented. A filter is intended to modify existing content where a function is moreso intended to produce new content. To add functions, implement the `getFunctions()` method within your extension which will return a map of function names and their corresponding implementations. A function implementation will implement the `Function` interface. The `Function` interface is very similar to the `Filter` and `Test` interfaces. Here is an example of how a fictional `fibonacciString` function might be implemented: ```java public class FibonnaciStringFunction implements Function { @Override public List getArgumentNames() { List names = new ArrayList<>(); names.add("length"); return names; } @Override public Object execute(Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { Integer length = (Integer)args.get("length"); Integer prev1 = 0; Integer prev2 = 1; StringBuilder result = new StringBuilder(); result.append("01"); for(int i = 2; i < length; i++){ Integer next = prev1 + prev2; result.append(next); prev1 = prev2; prev2 = next; } return result.toString(); } } ``` ## Positional and Named Arguments For filters, tests, and functions it is required that you implement the `getArgumentNames` method even if it returns null. Returning a list of strings will allow the end user to call your filter/test/function using named arguments. If this method returns null or an empty list, the function supports dynamic arguments. This means the user can pass any number of arguments—either named (e.g., myFunction(param="value")) or positional (e.g., myFunction("value"))—and they will be available in the arguments map. Using the above fictional fibonacci function as an example, a user can invoke it in two different ways: ```twig {% verbatim %} {{ fibonacci(10) }} {{ fibonacci(length=10) }} {%- endverbatim %} ``` If the end user excludes the names and only uses positional arguments, the argument values will still end up be mapped to the proper names when it's time to invoke the function's execute method. Your function implementation doesn't have to worry whether the user used positional or named arguments. It is important though that if the filter/function/test expects more than one argument, then the developer must communicate to the user the expected order of arguments in the chance that the user wants to invoke it without using names. Some functions such as the built in `min` and `max` functions accept an unlimited amount of arguments. For this to happen, your function must not accept any named arguments (i.e. your `getArgumentNames` method will return null or empty) and your `execute`` method will simply iterate over the values of the user provided argument map while ignoring the keys of that map (Pebble will use arbitrary keys if there are no names to map to). ## Global Variables Adding global variables, which are variables that are accessbile to all templates, is very trivial. In your custom extension, implement the `getGlobalVariables()` method which returns a `Map`. The contents of this map will be merged into the context you provide to each template at the time of rendering. ## Operators Operators are more complex to implement than filters or tests. To add custom operators, implement the `getBinaryOperators()` or the `getUnaryOperators()` method in your extension, or both. These methods return a list of `BinaryOperator` or `UnaryOperator` objects, respectively. Binary operators require the following information: - Precedence: an integer relative to other operators which defines the order of operations. - Symbol: a String representing the actual operator. This is typically a single character but doesn't have to be. - Expression Class: A class that extends `BinaryExpression`. This class will perform the actual operator implementation. - Associativity: Either left or right depending on how the operator is used. A unary operator is much the same except it's expression class must extend `UnaryExpression` and there is no associativity. The precedence values for existing core operators are as followed: - `or`: 10 - `and`: 15 - `is`: 20 - `is not`: 20 - `==`: 30 - `!=`: 30 - `>`: 30 - `<`: 30 - `>=`: 30 - `<=`: 30 - `+`: 40 - `-`: 40 - `not`: 50 (Unary) - `*`: 60 - `/`: 60 - `%`: 60 - `|`: 100 - `+`: 500 (Unary) - `-`: 500 (Unary) The following is an example of how the addition operator (`+`) might have been implemented: ```java public class AdditionOperator implements BinaryOperator { public int getPrecedence(){ return 30; } public String getSymbol(){ return "+"; } public BinaryExpression createInstance() { return new AddExpression(); } public BinaryOperatorType getType() { return BinaryOperatorType.NORMAL; } public Associativity getAssociativity(){ return Associativity.LEFT; } } ``` Alongside each operator class you will also need to implement a corresponding `BinaryExpression` class which actually implements the operator. The above example references a fictional `AdditionExpression` class which might look like the following: ```java public class AdditionExpression extends BinaryExpression { @Override public Object evaluate(PebbleTemplateImpl self, EvaluationContext context){ Integer left = (Integer)getLeftExpression().evaluate(self, context); Integer right = (Integer)getRightExpression().evaluate(self, context); return left + right; } } ``` In the above example you will notice that children of BinaryExpression have access to two other expressions, `leftExpression`, and `rightExpression`; these are the operands of your operator. Please note that in the above example both operands are casted to Integers but in reality you can't always make that assumption; the true addition expression is much more complex to handle different types of operands (Integers, Longs, Doubles, etc). ## Tags Creating new tags is one of the most powerful abilities of Pebble. Your extension should start by implementing the `getTokenParsers()` method. A `TokenParser` is responsible for converting all necessary tokens to appropriate `RenderableNodes`. A token is a significant and irreducible group of characters found in a template (such as an operator, whitespace, variable name, delimiter, etc) and a `RenderableNode` is a Pebble class that is responsible for generating output. Let us look at an example of a `TokenParser`: ```java public class SetTokenParser implements TokenParser { public String getTag(){ return "set"; } @Override public RenderableNode parse(Token token, Parser parser) { TokenStream stream = parser.getStream(); int lineNumber = token.getLineNumber(); // skip the "set" token stream.next(); // use the built in expression parser to parse the variable name String name = parser.getExpressionParser().parseNewVariableName(); stream.expect(Token.Type.PUNCTUATION, "="); // use the built in expression parser to parse the variable value Expression value = parser.getExpressionParser().parseExpression(); // expect to see "%}" stream.expect(Token.Type.EXECUTE_END); // NodeSet is composed of a name and a value return new SetNode(lineNumber, name, value); } } ``` The `getTag()` method must return the name of the tag. Pebble's main parser will use this name to determine when to delegate responsibility to your custom `TokenParser`. This example is parsing the `set` tag. The parse method is invoked whenever the primary parser encounters a set token. This method should return one `RenderableNode` instance which when rendered during the template evaluation, will write output to the provided Writer object. If the `RenderableNode` contains children nodes, it should invoke the render method of those nodes as well. The best way to learn all the details of parsing is to look at some of the tools used, as well as some examples. Here is a list of classes I suggest reading: - `TokenParser` - `Parser` - `SetTokenParser` - `ForTokenParser` - `IfNode` - `SetNode` ## Attribute resolver (v3 only) To create a new attribute resolver, implement the `getAttributeResolver()` method of your extension which will return a list of attribute resolvers to run. A attribute resolver implementation must implement the `AttributeResolver` interface. The `AttributeResolver` interface requires one method to be implemented, `resolve()`. The custom attribute resolver will be executed before all default pebble attribute resolvers. It replaces the `DynamicAttributeProvider` interface ```java public class DefaultAttributeResolver implements AttributeResolver { @Override public ResolvedAttribute resolve(Object instance, Object attributeNameValue, Object[] argumentValues, boolean isStrictVariables, String filename, int lineNumber) { if (instance instanceof CustomObject) { return "customValue"; } return null; } } ``` ================================================ FILE: docs/src/orchid/resources/wiki/guide/high-performance.md ================================================ --- --- # High Performance ## Concurrency First and foremost, a `PebbleTemplate` object, once compiled, is completely thread safe. As long as the data backing the template is also thread safe, you can render that single template instance using multiple threads at once. The actual rendering of a template will typically occur in a sequential manner, from top to bottom. If, however, you provide an `ExecutorService` to the `PebbleEngine` and make use of the {{ anchor('parallel') }} tag, you can have multiple threads render different sections of your template at one time. This is especially useful if one section of your template is costly and will otherwise block the rendering of the rest of the template. ## Streaming The use of the {{ anchor('flush') }} tag can be used to stream the rendered output as it's being rendered. This can significantly improve latency. ## Performance Pitfalls - It is typically okay for a block to use the `flush` tag unless the contents of that block is being rendered using the {{ anchor('block') }} function. Typically the flush tag will flush to the `Writer` that you provided but the block function internally uses it's own `StringWriter` and therefore flushing will do no good. ================================================ FILE: docs/src/orchid/resources/wiki/guide/installation.md ================================================ --- --- # Installation & Configuration ## Installation Pebble is hosted in the Maven Central Repository. Simply add the following dependency into your `pom.xml` file: ```xml io.pebbletemplates pebble {{ site.version }} ``` Also, snapshots of the master branch are deployed automatically with each successful commit. Instead of Maven Central, use the Sonatype snapshots repository at: ```xml https://oss.sonatype.org/content/repositories/snapshots ``` You can add the repository in your pom.xml ```xml sonatype-public Sonatype Public https://oss.sonatype.org/content/groups/public true ``` ## Set Up If you are integrating Pebble with Spring MVC, read {{ anchor('this guide', 'Spring Integration') }}. You will want to begin by creating a `PebbleEngine` which is responsible for coordinating the retrieval and compilation of your templates: ```java PebbleEngine engine = new PebbleEngine.Builder().build(); ``` And now, with your new `PebbleEngine` instance you can start compiling templates: ```java PebbleTemplate compiledTemplate = engine.getTemplate("templateName"); ``` Finally, simply provide your compiled template with a `Writer` object and a Map of variables to get your output! ```java Writer writer = new StringWriter(); Map context = new HashMap<>(); context.put("name", "Mitchell"); compiledTemplate.evaluate(writer, context); String output = writer.toString(); ``` ## Template Loader The `PebbleEngineBuilder` will also accept a `Loader` implementation as an argument. A loader is responsible for finding your templates. Pebble ships with the following loader implementations: - `DelegatingLoader`: Delegates responsibility to a collection of children loaders. - `ClasspathLoader`: Uses a classloader to search the current classpath. - `FileLoader`: Finds templates using a filesystem path. Must provide a mandatory absolute base path. - `ServletLoader`: Uses a servlet context to find the template. This is the recommended loader for use within an application server but is not enabled by default. - `Servlet5Loader`: Same as `ServletLoader`, but for Jakarta Servlet 5.0 or newer. - `MemoryLoader`: Loader that supports inheritance and doesn't require a filesystem. This is useful for applications - `StringLoader`: Considers the name of the template to be the contents of the template. Should not be used in a production environment. It is primarily for testing and debugging. Many tags may not work when using this loader, such as "extends", "imports", etc. that retrieve templates from a database for example. If you do not provide a custom Loader, Pebble will use an instance of the `ClasspathLoader` by default. ## Pebble Engine Settings All the settings are set during the construction of the `PebbleEngine` object. | Setting | Description | Default | | --- | --- | --- | | `cacheActive` | Flag to activate/desactivate template caching | true | | `templateCache` | An implementation of a ConcurrentMap cache that the Pebble engine will use to cache compiled templates. | Default implementation is `ConcurrentMapTemplateCache` and another implementation based on Caffeine is available (`CaffeineTemplateCache`) | | `tagCache` | An implementation of a ConcurrentMap cache that the Pebble engine will use for {{ anchor('cache tag', 'cache') }}. | Default implementation is `ConcurrentMapTagCache` and another implementation based on Caffeine is available (`CaffeineTagCache`) | | `defaultLocale` | The default locale which will be passed to each compiled template. The templates then use this locale for functions such as i18n, etc. A template can also be given a unique locale during evaluation. | `Locale.getDefault()` | | `executorService` | An `ExecutorService` that allows the usage of some advanced multithreading features, such as the `parallel` tag. | `null` | | `loader` | An implementation of the `Loader` interface which is used to find templates. | An implementation of the `ClasspathLoader` | | `strictVariables` | If set to true, Pebble will throw an exception if you try to access a variable or attribute that does not exist (or an attribute of a null variable). If set to false, your template will treat non-existing variables/attributes as null without ever skipping a beat. | `false` | | `methodAccessValidator` | Pebble provides two implementations. NoOpMethodAccessValidator which do nothing and BlacklistMethodAccessValidator which checks that the method being called is not blacklisted. | `BlacklistMethodAccessValidator` | `literalDecimalTreatedAsInteger` | option for treating literal decimals as `int`. Otherwise it is `long`. | `false` | | `literalNumbersAsBigDecimals` | option for toggling to enable/disable literal numbers treated as BigDecimals | `false` | | `greedyMatchMethod` | option for toggling to enable/disable greedy matching mode for finding java method. Reduce the limit of the parameter type, try to find other method which has compatible parameter types. | `false` | | `maxRenderedSize` | option for limiting the size of the rendered output | `-1 (disabled)` | ================================================ FILE: docs/src/orchid/resources/wiki/guide/spring-boot-integration.md ================================================ --- --- # Pebble Spring Boot Starter Spring Boot starter for autoconfiguring Pebble. ## Basic Usage Add the starter dependency to your pom.xml: ### spring-boot v4 ```XML io.pebbletemplates pebble-spring-boot-starter {{ site.version }} ``` Or build.gradle: ```groovy compile "io.pebbletemplates:pebble-spring-boot-starter:{{ site.version }}" ``` ### spring-boot v3 ```XML io.pebbletemplates pebble-legacy-spring-boot-starter {{ site.version }} ``` Or build.gradle: ```groovy compile "io.pebbletemplates:pebble-legacy-spring-boot-starter:{{ site.version }}" ``` This is enough for autoconfiguration to kick in. This includes: * a Loader that will pick template files ending in ``.peb`` from ``/templates/`` dir on the classpath * a PebbleEngine with default settings, configured with the previous loader * a Spring extension which offers some functionality described below * a ViewResolver that will output ``text/html`` in ``UTF-8`` PLEASE NOTE: the starter depends on ``spring-boot-starter-webmvc`` or ``spring-boot-starter-webmvcflux`` but is marked as optional, you'll need to add the dependency yourself or configure Spring MVC appropriately. ## Boot externalized configuration A number of properties can be defined in Spring Boot externalized configuration, eg. ``application.properties``, starting with the prefix ``pebble``. See the corresponding [PebbleProperties.java](https://github.com/PebbleTemplates/pebble/blob/master/pebble-spring/pebble-spring-boot-starter/src/main/java/io/pebbletemplates/boot/autoconfigure/PebbleProperties.java) for your starter version. Notable properties are: * ``pebble.prefix``: defines the prefix that will be prepended to the mvc view name. Defaults to ``/templates/`` * ``pebble.suffix``: defines the suffix that will be appended to the mvc view name. Defaults to ``.peb`` * ``pebble.charset``: defines the text encoding that will be used to configure the ViewResolver. Defaults to ``UTF-8`` * ``pebble.defaultLocale``: defines the default locale that will be used to configure the PebbleEngine. Defaults to ``null`` * ``pebble.strictVariables``: enable or disable the strict variable checking in the PebbleEngine. Defaults to ``false`` * ``pebble.greedyMatchMethod``: enable or disable the greedy matching mode for finding java method in the PebbleEngine. Defaults to ``false`` * ``pebble.servlet.cache``: enables or disables PebbleEngine caches. Defaults to ``true`` * ``pebble.servlet.contentType``: defines the content type that will be used to configure the ViewResolver. Defaults to ``text/html`` * ``pebble.servlet.exposeRequestAttributes``: defines whether all request attributes should be added to the model prior to merging with the template for the ViewResolver. Defaults to ``false`` * ``pebble.servlet.exposeSessionAttributes``: defines whether all session attributes should be added to the model prior to merging with the template for the ViewResolver. Defaults to ``false`` * ``pebble.reactive.mediaTypes``: Configure the supported media types for Pebble views when used in a reactive Spring WebFlux application. This property allows you to specify a list of MediaType objects that the Pebble view resolver should consider when rendering templates. Defaults to ``null`` ## Examples There is the spring petclinic example which has been migrated to [pebble](https://github.com/PebbleTemplates/spring-petclinic) There is also a fully working example project located on [github](https://github.com/PebbleTemplates/pebble-example-spring) which can be used as a reference. It is a very simple and bare-bones project designed to only portray the basics. To build the project, simply run `mvn install` and then deploy the resulting war file to a an application container. ## Customizing Pebble ### Pebble extensions Extensions defined as beans will be picked up and added to the PebbleEngine automatically: ```java @Bean public Extension myPebbleExtension1() { return new MyPebbleExtension1(); } @Bean public Extension myPebbleExtension2() { return new MyPebbleExtension2(); } ``` CAVEAT: Spring will not gather all the beans if they're scattered across multiple @Configuration classes. If you use this mechanism, bundle all Extension @Beans in a single @Configuration class. ### Customizing the Loader The autoconfigurer looks for a bean named ``pebbleLoader`` in the context. You can define a custom loader with that name and it will be used to configure the default PebbleEngine: ```java @Bean public Loader pebbleLoader() { return new MyCustomLoader(); } ``` **PLEASE NOTE**: this loader's prefix and suffix will be both overwritten when the ViewResolver is configured. You should use the externalized configuration for changing these properties. ### Customizing the PebbleEngine Likewise, you can build a custom engine and make it the default by using the bean name ``pebbleEngine``: ```java @Bean public PebbleEngine pebbleEngine() { return new PebbleEngine.Builder().build(); } ``` ### Customizing the MethodAccessValidator You can provide your own MethodAccessValidator or switch to NoOpMethodAccessValidator by providing a MethodAccessValidator Bean ```java @Bean public MethodAccessValidator methodAccessValidator() { return new NoOpMethodAccessValidator(); } ``` ### Customizing the ViewResolver And the same goes for the ViewResolver ```java @Bean public PebbleViewResolver pebbleViewResolver() { return new PebbleViewResolver(); } ``` For reactive app ```java @Bean public PebbleReactiveViewResolver pebbleReactiveViewResolver() { return new PebbleReactiveViewResolver(...) } ``` PLEASE NOTE: you need to change the Loader's prefix and suffix to match the custom ViewResolver's values. ## Features ### Access to Spring beans Spring beans are available to the template. ```twig {% verbatim %}{{ beans.beanName }}{% endverbatim %} ``` ### Access to http request HttpServletRequest object is available to the template. ```twig {% verbatim %}{{ request.contextPath }}{% endverbatim %} ``` ### Access to http response HttpServletResponse is available to the template. ```twig {% verbatim %}{{ response.contentType }}{% endverbatim %} ``` ### Access to http session HttpSession is available to the template. ```twig {% verbatim %}{{ session.maxInactiveInterval }}{% endverbatim %} ``` ## Spring extension This extension has many functions for spring validation and the use of message bundle. ### Href function Function to automatically add the context path to a given url ```twig {% verbatim %}Example{% endverbatim %} ``` ### Message function It achieves the same thing as the i18n function, but instead, it uses the configured spring messageSource, typically the ResourceBundleMessageSource. ```twig {% verbatim %} Label = {{ message('label.test') }} Label with params = {{ message('label.test.params', 'params1', 'params2') }} {%- endverbatim %} ``` ### Spring validations and error messages 6 validations methods and error messages are exposed using spring BindingResult. It needs as a parameter the form name and for a particular field, the field name. To check if there's any error: ```twig {% verbatim %} {{ hasErrors('formName') }} {{ hasGlobalErrors('formName') }} {{ hasFieldErrors('formName', 'fieldName') }} {%- endverbatim %} ``` To output any error: ```twig {% verbatim %} {% for err in getAllErrors('formName') %}

{{ err }}

{% endfor %} {% for err in getGlobalErrors('formName') %}

{{ err }}

{% endfor %} {% for err in getFieldErrors('formName', 'fieldName') %}

{{ err }}

{% endfor %} {%- endverbatim %} ``` ### Using Pebble for other tasks The main role of this starter is to configure Pebble for generating MVC View results (the typical HTML). You may define more PebbleEngine/Loader beans for other usage patterns (like generating email bodies). Bear in mind that you should not reuse the default Loader for other Engine instances. ================================================ FILE: docs/src/orchid/resources/wiki/operator/comparisons.md ================================================ # Comparisons Pebble provides the following comparison operators: `==`, `!=`, `<`, `>`, `<=`, `>=`. All of them except for `==` are equivalent to their Java counterparts. The `==` operator uses `java.util.Objects.equals(a, b)` behind the scenes to perform null safe value comparisons. > `equals` is an alias for `==` ```twig {% if user.name equals "Mitchell" %} ... {% endif %} ``` ================================================ FILE: docs/src/orchid/resources/wiki/operator/contains.md ================================================ # `contains` The `contains` operator can be used to determine if a collection, map, or array contains a particular item. ```twig {% if ["apple", "pear", "banana"] contains "apple" %} ... {% endif %} ``` When using maps, the contains operator checks for an existing key. ```twig {% if {"apple":"red", "banana":"yellow"} contains "banana" %} ... {% endif %} ``` The operator can be used to look for multiple items at once: ```twig {% if ["apple", "pear", "banana", "peach"] contains ["apple", "peach"] %} ... {% endif %} ``` ================================================ FILE: docs/src/orchid/resources/wiki/operator/is.md ================================================ --- --- # `is` The `is` operator will apply a test to a variable which will return a boolean. ```twig {% verbatim %} {% if 2 is even %} ... {% endif %} {%- endverbatim %} ``` The result can be negated using the {{ anchor('not', 'logic') }} operator. ================================================ FILE: docs/src/orchid/resources/wiki/operator/logic.md ================================================ # Logic The `and` operator and the `or` operator are available to join boolean expressions. ```twig {% if 2 is even and 3 is odd %} ... {% endif %} ``` The `not` operator is available to negate a boolean expression. ```twig {% if 3 is not even %} ... {% endif %} ``` Parenthesis can be used to group expressions to ensure a desired precedence. ```twig {% if (3 is not even) and (2 is odd or 3 is even) %} ... {% endif %} ``` ================================================ FILE: docs/src/orchid/resources/wiki/operator/math.md ================================================ --- --- # Math All the regular math operators are available for use. Order of operations applies. ```twig {% verbatim %} {{ 2 + 2 / ( 10 % 3 ) * (8 - 1) }} {% endverbatim %} ``` The result can be negated using the {{ anchor('not', 'logic') }} operator. ================================================ FILE: docs/src/orchid/resources/wiki/operator/others.md ================================================ # Other Operators The `|` operator is used to apply a filter to a variable. ```twig {{ user.name | capitalize }} ``` Pebble supports the use of the conditional operator (often named the ternary operator). ```twig {{ foo == null ? bar : baz }} ``` ================================================ FILE: docs/src/orchid/resources/wiki/summary.md ================================================ ## Guides - [Installation and Configuration](guide/installation.md) - [Spring Boot Integration](guide/spring-boot-integration.md) - [Spring petclinic](https://github.com/PebbleTemplates/spring-petclinic) - [Pebble Spring Example](https://github.com/PebbleTemplates/pebble-example-spring) - [Basic Usage](guide/basic-usage.md) - [Customize Defaults](guide/customize-defaults.md) - [Escaping](guide/escaping.md) - [Extending Pebble](guide/extending-pebble.md) - [High Performance Techniques](guide/high-performance.md) ### Tags - [autoescape](tag/autoescape.md) - [block](tag/block.md) - [cache](tag/cache.md) - [embed](tag/embed.md) - [extends](tag/extends.md) - [filter](tag/filter.md) - [flush](tag/flush.md) - [for](tag/for.md) - [from](tag/from.md) - [if](tag/if.md) - [import](tag/import.md) - [include](tag/include.md) - [macro](tag/macro.md) - [parallel](tag/parallel.md) - [set](tag/set.md) - [verbatim](tag/verbatim.md) ## Filters - [abbreviate](filter/abbreviate.md) - [abs](filter/abs.md) - [base64decode](filter/base64decode.md) - [base64encode](filter/base64encode.md) - [capitalize](filter/capitalize.md) - [date](filter/date.md) - [default](filter/default.md) - [escape](filter/escape.md) - [first](filter/first.md) - [join](filter/join.md) - [last](filter/last.md) - [length](filter/length.md) - [lower](filter/lower.md) - [numberformat](filter/numberformat.md) - [raw](filter/raw.md) - [replace](filter/replace.md) - [reverse](filter/reverse.md) - [rsort](filter/rsort.md) - [sha256](filter/sha256.md) - [slice](filter/slice.md) - [sort](filter/sort.md) - [split](filter/split.md) - [title](filter/title.md) - [trim](filter/trim.md) - [upper](filter/upper.md) - [urlencode](filter/urlencode.md) ## Functions - [block](function/blockFunction.md) - [i18n](function/i18n.md) - [max](function/max.md) - [min](function/min.md) - [parent](function/parent.md) - [range](function/range.md) ## Tests - [empty](test/empty.md) - [even](test/even.md) - [map](test/map.md) - [null](test/null.md) - [odd](test/odd.md) - [iterable](test/iterable.md) ## Operators - [comparisons](operator/comparisons.md) (`==`, `!=`, `<`, `>`, `<=`, `>=`, `equals`) - [contains](operator/contains.md) (`contains`) - [is](operator/is.md) - [logic](operator/logic.md) (`and`, `or`, `not`, `()`) - [math](operator/math.md) (`+`, `-`, `/`, `%`, `*`) - [others](operator/others.md) (`|`, `?:`) ================================================ FILE: docs/src/orchid/resources/wiki/tag/autoescape.md ================================================ --- --- # `autoescape` The `autoescape` tag can be used to temporarily disable/re-enable the autoescaper as well as change the escaping strategy for a portion of the template. ```twig {% verbatim %} {{ danger }} {# will be escaped by default #} {% autoescape false %} {{ danger }} {# will not be escaped #} {% endautoescape %} {%- endverbatim %} ``` ```twig {% verbatim %} {{ danger }} {# will use the "html" escaping strategy #} {% autoescape "js" %} {{ danger }} {# will use the "js" escaping strategy #} {% endautoescape %} {%- endverbatim %} ``` Please read the {{ anchor('escaping guide', 'Escaping') }} for more information about escaping. ================================================ FILE: docs/src/orchid/resources/wiki/tag/block.md ================================================ --- --- # `block` The `block` tag performs two functions. If used in a parent template, it will designate a section as being allowed to be overriden by a child template. If used in a child template, it will override the content originally declared in the parent template. See the {{ anchor('extends') }} tag for a more detailed explanation on how to implement template inheritance. The contents of a block will only be used if a child template does not override it. It is often useful to define empty blocks as placeholders for content to be provided by a child template. The `block` tag is immediately followed by the name of the block. This name will be the same name the child template uses to override it. The `endblock` tag can optionally contain the block's name for readability. In the following example we create a block with the name 'header': ```twig {% verbatim %} {% block header %}

Introduction

{% endblock header %} {%- endverbatim %} ``` A child template should not have any content outside of blocks. A child template is only used to override blocks of a parent template. ================================================ FILE: docs/src/orchid/resources/wiki/tag/cache.md ================================================ # `cache` Cache the rendering portion of a page. Cache name can be an expression or a static string. It uses the cache name and the locale as a key in the cache. In the following example we create a cache with the name 'menu': ```twig {% cache 'menu' %} {% for item in items %} {{ item.text }} .... {% endfor %} {% endcache %} ``` Cache implementation can be overriden with the PebbleEngine Builder. ```java return new PebbleEngine.Builder() .loader(this.templateLoader()) .tagCache(CacheBuilder.newBuilder().maximumSize(200).build()) .build(); ``` ================================================ FILE: docs/src/orchid/resources/wiki/tag/embed.md ================================================ --- --- # `embed` The `embed` tag allows you to insert the rendered output of another template directly into the current template, while overriding some of its blocks. It effectively combines the behavior of {{anchor('include')}} with that of {{anchor('extends')}} for creating reusable, yet flexible, template fragments, or for composing micro-layouts. {% verbatim %} For example, imagine building a template `card.peb` as a reusable component in your layout. All cards should have the same markup, but the content can change drastically throughout your site. `card.peb` might then look like: ```twig // card.peb
{% block cardContent %} {% endblock %}
``` Now, you can include that template elsewhere in your layout, and override the `cardContent` block to "inject" rich content into that template at the call-side. For example, you may want to display a grid of your store's most popular products as cards, with the last card linking to the full catalog. Embedding `card.peb` and overriding the `cardContent` block ensures that the markup for both types of cards are always the same, even though what's displayed on each card is quite different. ```twig // layout.peb {% for product in popularProducts %} {% embed 'card.peb' %} {% block cardContent %}

{{ product.name }}

{{ product.description }}

{% endblock %} {% endembed %} {% endfor %} {% embed 'card.peb' %} {% block cardContent %} See all 100+ products {% endblock %} {% endembed %} ``` Embeds can be used multiple times in the same template, and may also be used in a template that itself extends another. Each template will then maintain its own block hierarchy. In other words, block overridden within the body of the `embed` tag will not accidentally override those defined in the main template, and likewise blocks defined in the main template or its parent templates will not get mixed with those in the embedded template or its parent templates. ```twig // main.peb {% extends 'base.peb' %} {% block mainContent %} {{ parent() }} {# renders mainContent block from base.peb #} {{ block('footer') }} {# renders footer block from base.peb, the global page footer #} {% embed 'card.peb' %} {% block mainContent %} {{ parent() }} {# renders mainContent block from card.peb #} {{ block('footer') }} {# renders footer block from card.peb, the card footer (not the global page footer) #} {% endblock %} {% endembed %} {% endblock %} ``` ## Scope Embedded templates will have access to the same variables that the current template does. ```twig Top Content {% embed "advertisement" %}{% endembed %} Bottom Content {% embed "footer" %}{% endembed %} ``` You can add additional variables to the context of the embedded template by passing a map after the `with` keyword. The embedded template will have access to the same variables that the current template does plus the additional ones defined in the map passed after the `with` keyword: ```twig {% embed "advertisement" with {"foo":"bar"} %} {% block title %} Ad with title {% endblock %} {% block content %} Ad with title {% endblock %} {% endembed %} ``` ## Dynamic embed The `embed` tag will accept an expression to determine the template to embed at runtime. For example: ```twig {% embed admin ? 'adminFooter' : 'defaultFooter' %} {% endembed %} ``` {% endverbatim %} ================================================ FILE: docs/src/orchid/resources/wiki/tag/extends.md ================================================ # `extends` The `extends` tag is used to declare a parent template. It should be the very first tag used in a child template and a child template can only extend up to one parent template. The best way to understand template inheritance is to study an example. Let us look at a parent template called "base": ```twig {% block title %} {% endblock %}
{% block content %} Default content goes here. {% endblock %}
``` And now let's look at a child template called "home" which extends "base": ```twig {% extends "base" %} {% block title %} Home {% endblock %} {% block content %} Home page content. {% endblock %} ``` And finally let's look at the resulting output after evaluating "home": ```twig Home
Home page content will override the default content.
``` To summarize, parent templates define blocks and child templates will override the contents of those blocks. If a child template does not override the content of a particular block, the content provided by the parent template will be used. There is no limit to how long of an inheritance chain that you can create; i.e. a child template can itself have a child template. A lot of potential comes from this fact because you can create a hierarchy of templates to minimize how much content you have to write on the lower levels. ## Dynamic Inheritance The `extends` tag will accept an expression to determine the parent template at runtime. For example: ```twig {% extends ajax ? 'ajax' : 'base' %} ``` ================================================ FILE: docs/src/orchid/resources/wiki/tag/filter.md ================================================ # `filter` The `filter` tag allows you to apply a filter to a large chunk of template. ```twig {% filter upper %} hello {% endfilter %}} {# output: 'HELLO' #} ``` Multiple filters can be chained together. ```twig {% filter upper | escape %} hello
{% endfilter %}} {# output: 'HELLO<br>' #} ``` ================================================ FILE: docs/src/orchid/resources/wiki/tag/flush.md ================================================ # `flush` The `flush` tag allows you to flush all currently rendered output to the provided `Writer`. ```twig {{ headerText }} {% flush %} {{ content }} ``` ================================================ FILE: docs/src/orchid/resources/wiki/tag/for.md ================================================ # `for` The `for` tag is used to iterate through primitive arrays or anything that implements the `java.lang.Iterable` interface, as well as maps. ```twig {% for user in users %} {{ user.name }} lives in {{ user.city }}. {% endfor %} ``` While inside of the loop, Pebble provides a couple of special variables to help you out: - loop.index - a zero-based index that increments with every iteration. - loop.length - the size of the object we are iterating over. - loop.first - True if first iteration - loop.last - True if last iteration - loop.revindex - The number of iterations from the end of the loop ```twig {% for user in users %} {{ loop.index }} - {{ user.id }} {% endfor %} ``` The `for` tag also provides a convenient way to check if the iterable object is empty with the included `else` tag. ```twig {% for user in users %} {{ loop.index }} - {{ user.id }} {% else %} There are no users to display. {% endfor %} ``` Iterating over maps can be done like so: ```twig {% for entry in map %} {{ entry.key }} - {{ entry.value }} {% endfor %} ``` ================================================ FILE: docs/src/orchid/resources/wiki/tag/from.md ================================================ --- --- # `from` The from tag imports {{ anchor('macro') }} names into the current namespace. The tag is documented in detail in the documentation for the {{ anchor('import') }} tag. ================================================ FILE: docs/src/orchid/resources/wiki/tag/if.md ================================================ --- --- # `if` The `if` tag allows you to designate a chunk of content as conditional depending on the result of an expression ```twig {% verbatim %} {% if users is empty %} There are no users. {% elseif users|length == 1 %} There is only one user. {% else %} There are many users. {% endif %} {%- endverbatim %} ``` The expression used in the `if` statement often makes use of the {{ anchor('is') }} operator. ### Supported conditions `If` tag currently supports the following expression | Value | Boolean expression | | --- | --- | | boolean | boolean value | | Empty string | false | | Non empty string | true | | numeric zero | false | | numeric different than zero | true | ================================================ FILE: docs/src/orchid/resources/wiki/tag/import.md ================================================ --- --- # `import` The `import` tag allows you to use {{ anchor('macros', 'macro') }} defined in another template. Assuming that a macro named `input` exists in a template called `form_util` you can import it like so: ```twig {% verbatim %} {% import "form_util" %} {{ input("text", "name", "Mitchell") }} {%- endverbatim %} ``` The easiest and most flexible is importing the whole module into a variable. That way you can access the attributes: ```twig {% verbatim %} {% import 'forms.html' as forms %}
Username
{{ forms.input('username') }}
Password
{{ forms.input('password', null, 'password') }}

{{ forms.textarea('comment') }}

{%- endverbatim %} ``` Alternatively you can import names from the template into the current namespace: ```twig {% verbatim %} {% from 'forms.html' import input as input_field, textarea %}
Username
{{ input_field('username') }}
Password
{{ input_field('password', '', 'password') }}

{{ textarea('comment') }}

{%- endverbatim %} ``` ## Dynamic Import The `import` tag will accept an expression to determine the template to import at runtime. For example: ```twig {% verbatim %} {% import modern ? 'ajax_form_util' : 'simple_form_util' %} {{ input("text", "name", "Mitchell") }} {%- endverbatim %} ``` ================================================ FILE: docs/src/orchid/resources/wiki/tag/include.md ================================================ # `include` The `include` tag allows you to insert the rendered output of another template directly into the current template. The included template will have access to the same variables that the current template does. ```twig Top Content {% include "advertisement" %} Bottom Content {% include "footer" %} ``` You can add additional variables to the context of the included template by passing a map after the `with` keyword. The included template will have access to the same variables that the current template does plus the additional ones defined in the map passed after the `with` keyword: ```twig {% include "advertisement" with {"foo":"bar"} %} ``` ## Dynamic Include The `include` tag will accept an expression to determine the template to include at runtime. For example: ```twig {% include admin ? 'adminFooter' : 'defaultFooter' %} ``` ================================================ FILE: docs/src/orchid/resources/wiki/tag/macro.md ================================================ --- --- # `macro` The `macro` tag allows you to create a chunk of reusable and dynamic content. The macro can be called multiple times in the current template or even from another template with the help of the {{ anchor('import') }} tag. It doesn't matter where in the current template you define a macro, i.e. whether it's before or after you call it. Here is an example of how to define a macro: ```twig {% verbatim %} {% macro input(type="text", name, value) %} {% endmacro %} {%- endverbatim %} ``` And now the macro can be called numerous times throughout the template, like so: ```twig {% verbatim %} {{ input(name="country") }} {# will output: #} {%- endverbatim %} ``` If the macro resides in another template, use the {{ anchor('import') }} tag first. ```twig {% verbatim %} {% import "form_util" %} {{ input("text", "country", "Canada") }} {%- endverbatim %} ``` A macro does not have access to the same variables that the rest of the template has access to. A macro can only work with the variables provided as arguments. ### Access to the global context You can pass the whole context as an argument by using the special `_context` variable if you need to access variables outside of the macro scope: ```twig {% verbatim %} {% set foo = 'bar' %} {{ test(_context) }} {% macro test(_context) %} {{ _context.foo }} {% endmacro %} {# will output: bar #} {%- endverbatim %} ``` ================================================ FILE: docs/src/orchid/resources/wiki/tag/parallel.md ================================================ --- --- # `parallel` The `parallel` tag allows you to designate a chunk of content to be rendered using a new thread. This tag is only available if you provide an `ExecutorService` to the main `PebbleEngine`. ```twig {% verbatim %} {{ upperContent }} {% parallel %} {{ calculation.slowCalculation }} {% endparallel %} {{ lowerContent }} {% endverbatim %} ``` In the above example, the slow calculation will not block the `lowerContent` from being evaluated concurrently. See the {{ anchor('high performance guide', 'High Performance Techniques') }} for more tips on how to improve performance. ================================================ FILE: docs/src/orchid/resources/wiki/tag/set.md ================================================ # `set` The `set` tag allows you to define a variable in the current context, whether it currently exists or not. ```twig {% set header = "Test Page" %} {{ header }} ``` ================================================ FILE: docs/src/orchid/resources/wiki/tag/verbatim.md ================================================ # `verbatim` The `verbatim` tag allows you to write a block of Pebble syntax that won't be parsed. ```twig {% verbatim %} {% for user in users %} {{ user.name }} {% endfor %} {% endverbatim %} ```
## Inline Verbatim Text For inline verbatim text, a string literal can be used. For example, if you need to include **{{** in the output of a template, you can use `{{ "{{" }}` in string literal in the Pebble template This would be useful if you are using Pebble to generate Angular HTML component template files: ```javascript {{ "{{" }}school.name{{ "}}" }} ``` would produce the following template output: ```javascript {{school.name}} ``` ================================================ FILE: docs/src/orchid/resources/wiki/test/empty.md ================================================ # `empty` The `empty` test checks if a variable is empty. A variable is empty if it is null, an empty string, an empty collection, or an empty map. ```twig {% if user.email is empty %} ... {% endif %} ``` ================================================ FILE: docs/src/orchid/resources/wiki/test/even.md ================================================ # `even` The `even` test checks if an integer is even. ```twig {% if 2 is even %} ... {% endif %} ``` ================================================ FILE: docs/src/orchid/resources/wiki/test/iterable.md ================================================ # `iterable` The `iterable` test checks if a variable implements `java.lang.Iterable`. ```twig {% if users is iterable %} {% for user in users %} ... {% endfor %} {% endif %} ``` ================================================ FILE: docs/src/orchid/resources/wiki/test/map.md ================================================ # `map` The `map` test checks if a variable is an instance of a map. ```twig {% if {"apple":"red", "banana":"yellow"} is map %} ... {% endif %} ``` ================================================ FILE: docs/src/orchid/resources/wiki/test/null.md ================================================ # `null` The `null` test checks if a variable is null. ```twig {% if user.email is null %} ... {% endif %} ``` ================================================ FILE: docs/src/orchid/resources/wiki/test/odd.md ================================================ # `odd` The `odd` test checks if an integer is odd. ```twig {% if 3 is odd %} ... {% endif %} ``` ================================================ FILE: intellij-java-google-style.xml ================================================ ================================================ FILE: pebble/pom.xml ================================================ 4.0.0 io.pebbletemplates pebble-project 4.1.2-SNAPSHOT pebble jar Pebble Templating engine for Java. http://pebbletemplates.io 1.1.6.RELEASE 2.0.17 3.2.3 6.1.0 2.5 3.27.7 1.5.32 6.0.3 org.unbescape unbescape ${unbescape.version} org.slf4j slf4j-api ${slf4j.version} com.github.ben-manes.caffeine caffeine ${caffeine.version} true javax.servlet servlet-api ${servlet-api.version} true jakarta.servlet-api jakarta.servlet ${jakarata.servlet-api.version} true ch.qos.logback logback-classic ${logback-classic.version} test org.junit.jupiter junit-jupiter ${junit-jupiter.version} test org.assertj assertj-core ${assertj.version} test biz.aQute.bnd bnd-maven-plugin 7.2.3 generate-osgi-manifest bnd-process maven-jar-plugin ${project.build.outputDirectory}/META-INF/MANIFEST.MF maven-assembly-plugin package single jar-with-dependencies ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/PebbleEngine.java ================================================ /* * This file is part of Pebble. *

* Copyright (c) 2014 by Mitchell Bösecke *

* For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble; import io.pebbletemplates.pebble.attributes.methodaccess.BlacklistMethodAccessValidator; import io.pebbletemplates.pebble.attributes.methodaccess.MethodAccessValidator; import io.pebbletemplates.pebble.cache.CacheKey; import io.pebbletemplates.pebble.cache.PebbleCache; import io.pebbletemplates.pebble.cache.tag.ConcurrentMapTagCache; import io.pebbletemplates.pebble.cache.tag.NoOpTagCache; import io.pebbletemplates.pebble.cache.template.ConcurrentMapTemplateCache; import io.pebbletemplates.pebble.cache.template.NoOpTemplateCache; import io.pebbletemplates.pebble.error.LoaderException; import io.pebbletemplates.pebble.extension.*; import io.pebbletemplates.pebble.extension.escaper.EscapingStrategy; import io.pebbletemplates.pebble.lexer.LexerImpl; import io.pebbletemplates.pebble.lexer.Syntax; import io.pebbletemplates.pebble.lexer.TokenStream; import io.pebbletemplates.pebble.loader.ClasspathLoader; import io.pebbletemplates.pebble.loader.Loader; import io.pebbletemplates.pebble.loader.StringLoader; import io.pebbletemplates.pebble.node.RootNode; import io.pebbletemplates.pebble.parser.Parser; import io.pebbletemplates.pebble.parser.ParserImpl; import io.pebbletemplates.pebble.parser.ParserOptions; import io.pebbletemplates.pebble.template.EvaluationOptions; import io.pebbletemplates.pebble.template.PebbleTemplate; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; import io.pebbletemplates.pebble.utils.TypeUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.Reader; import java.util.Locale; import java.util.concurrent.ExecutorService; import java.util.function.Function; /** * The main class used for compiling templates. The PebbleEngine is responsible for delegating * responsibility to the lexer, parser, compiler, and template cache. * * @author Mitchell */ public class PebbleEngine { private final Logger logger = LoggerFactory.getLogger(PebbleEngine.class); private final Loader loader; private final Syntax syntax; private final boolean strictVariables; private final Locale defaultLocale; private final int maxRenderedSize; private final PebbleCache tagCache; private final ExecutorService executorService; private final PebbleCache templateCache; private final ExtensionRegistry extensionRegistry; private final ParserOptions parserOptions; private final EvaluationOptions evaluationOptions; /** * Constructor for the Pebble Engine given an instantiated Loader. This method does only load * those userProvidedExtensions listed here. * * @param loader The template loader for this engine * @param syntax the syntax to use for parsing the templates. */ private PebbleEngine(Loader loader, Syntax syntax, boolean strictVariables, Locale defaultLocale, int maxRenderedSize, PebbleCache tagCache, PebbleCache templateCache, ExecutorService executorService, ExtensionRegistry extensionRegistry, ParserOptions parserOptions, EvaluationOptions evaluationOptions) { this.loader = loader; this.syntax = syntax; this.strictVariables = strictVariables; this.defaultLocale = defaultLocale; this.maxRenderedSize = maxRenderedSize; this.tagCache = tagCache; this.executorService = executorService; this.templateCache = templateCache; this.extensionRegistry = extensionRegistry; this.parserOptions = parserOptions; this.evaluationOptions = evaluationOptions; } /** * Loads, parses, and compiles a template into an instance of PebbleTemplate and returns this * instance. * * @param templateName The name of the template * @return PebbleTemplate The compiled version of the template */ public PebbleTemplate getTemplate(String templateName) { return this.getTemplate(templateName, this.loader); } /** * Loads, parses, and compiles a template using a StringLoader into an instance of PebbleTemplate * and returns this instance. * * @param templateName The name of the template * @return PebbleTemplate The compiled version of the template */ public PebbleTemplate getLiteralTemplate(String templateName) { return this.getTemplate(templateName, new StringLoader()); } private PebbleTemplate getTemplate(String templateName, Loader loader) { /* * template name will be null if user uses the extends tag with an * expression that evaluates to null */ if (templateName == null) { return null; } if (loader == null) { throw new LoaderException(null, "Loader has not yet been specified."); } Object cacheKey = loader.createCacheKey(templateName); return this.templateCache .computeIfAbsent(cacheKey, k -> this.getPebbleTemplate(templateName, loader, cacheKey)); } private PebbleTemplate getPebbleTemplate(String templateName, Loader loader, Object cacheKey) { Reader templateReader = loader.getReader(cacheKey); try { this.logger.trace("Tokenizing template named {}", templateName); LexerImpl lexer = new LexerImpl(this.syntax, this.extensionRegistry.getUnaryOperators().values(), this.extensionRegistry.getBinaryOperators().values()); TokenStream tokenStream = lexer.tokenize(templateReader, templateName); this.logger.trace("TokenStream: {}", tokenStream); Parser parser = new ParserImpl(this.extensionRegistry.getUnaryOperators(), this.extensionRegistry.getBinaryOperators(), this.extensionRegistry.getTokenParsers(), this.parserOptions); RootNode root = parser.parse(tokenStream); PebbleTemplateImpl instance = new PebbleTemplateImpl(this, root, templateName); for (NodeVisitorFactory visitorFactory : this.extensionRegistry.getNodeVisitors()) { visitorFactory.createVisitor(instance).visit(root); } return instance; } finally { try { templateReader.close(); } catch (IOException e) { // can't do much about it } } } /** * Returns the loader * * @return The loader */ public Loader getLoader() { return this.loader; } /** * Returns the template cache * * @return The template cache */ public PebbleCache getTemplateCache() { return this.templateCache; } /** * Returns the strict variables setting * * @return The strict variables setting */ public boolean isStrictVariables() { return this.strictVariables; } /** * Returns the default locale * * @return The default locale */ public Locale getDefaultLocale() { return this.defaultLocale; } /** * Returns the max rendered size. * * @return The max rendered size. */ public int getMaxRenderedSize() { return this.maxRenderedSize; } /** * Returns the executor service * * @return The executor service */ public ExecutorService getExecutorService() { return this.executorService; } /** * Returns the syntax which is used by this PebbleEngine. * * @return the syntax used by the PebbleEngine. */ public Syntax getSyntax() { return this.syntax; } /** * Returns the extension registry. * * @return The extension registry */ public ExtensionRegistry getExtensionRegistry() { return this.extensionRegistry; } /** * Returns the tag cache * * @return The tag cache */ public PebbleCache getTagCache() { return this.tagCache; } /** * A builder to configure and construct an instance of a PebbleEngine. */ public static class Builder { private Loader loader; private Syntax syntax; private boolean strictVariables = false; private boolean enableNewLineTrimming = true; private Locale defaultLocale; private int maxRenderedSize = -1; private ExecutorService executorService; private PebbleCache templateCache; private boolean cacheActive = true; private PebbleCache tagCache; private boolean literalDecimalTreatedAsInteger = false; private boolean greedyMatchMethod = false; private boolean literalNumbersAsBigDecimals = false; private MethodAccessValidator methodAccessValidator = new BlacklistMethodAccessValidator(); private final ExtensionRegistryFactory factory = new ExtensionRegistryFactory(); /** * Creates the builder. */ public Builder() { } /** * Sets the loader used to find templates. * * @param loader A template loader * @return This builder object */ public Builder loader(Loader loader) { this.loader = loader; return this; } /** * Adds an extension, can be safely invoked several times to add different extensions. * * @param extensions One or more extensions to add * @return This builder object */ public Builder extension(Extension... extensions) { this.factory.extension(extensions); return this; } /** * Sets the syntax to be used. * * @param syntax The syntax to be used * @return This builder object */ public Builder syntax(Syntax syntax) { this.syntax = syntax; return this; } /** * Changes the strictVariables setting of the PebbleEngine. The default value of * this setting is "false". *

* The following examples will all print empty strings if strictVariables is false but will * throw exceptions if it is true: *

* {{ nonExistingVariable }} {{ nonExistingVariable.attribute }} {{ nullVariable.attribute }} {{ * existingVariable.nullAttribute.attribute }} {{ existingVariable.nonExistingAttribute }} {{ * array[-1] }} * * @param strictVariables Whether or not strict variables is used * @return This builder object */ public Builder strictVariables(boolean strictVariables) { this.strictVariables = strictVariables; return this; } /** * Changes the newLineTrimming setting of the PebbleEngine. The default value of * this setting is "true". *

* By default, Pebble will trim a newline that immediately follows a Pebble tag. For example, * {{key1}}\n{{key2}} will have the newline removed. *

*

* If newLineTrimming is set to false, then the first newline following a Pebble * tag won't be trimmed. All newlines will be preserved *

* * @param enableNewLineTrimming Whether or not the newline should be trimmed. * @return This builder object */ public Builder newLineTrimming(boolean enableNewLineTrimming) { this.enableNewLineTrimming = enableNewLineTrimming; return this; } /** * Sets the Locale passed to all templates constructed by this PebbleEngine. *

* An individual template can always be given a new locale during evaluation. * * @param defaultLocale The default locale * @return This builder object */ public Builder defaultLocale(Locale defaultLocale) { this.defaultLocale = defaultLocale; return this; } /** * Sets the maximum size of the rendered template to protect against macro bombs. * See for example https://github.com/PebbleTemplates/pebble/issues/526. * If the rendered template exceeds this limit, then a PebbleException is thrown. * The default value is -1 and it means unlimited. * @param maxRenderedSize The maximum allowed size of the rendered template. * @return This builder object. */ public Builder maxRenderedSize(int maxRenderedSize) { this.maxRenderedSize = maxRenderedSize; return this; } /** * Sets the executor service which is required if using one of Pebble's multithreading features * such as the "parallel" tag. * * @param executorService The executor service * @return This builder object */ public Builder executorService(ExecutorService executorService) { this.executorService = executorService; return this; } /** * Sets the cache used by the engine to store compiled PebbleTemplate instances. * * @param templateCache The template cache * @return This builder object */ public Builder templateCache(PebbleCache templateCache) { this.templateCache = templateCache; return this; } /** * Sets the cache used by the "cache" tag. * * @param tagCache The tag cache * @return This builder object */ public Builder tagCache(PebbleCache tagCache) { this.tagCache = tagCache; return this; } /** * Sets whether or not escaping should be performed automatically. The default value of this * setting is "true". * * @param autoEscaping The auto escaping setting. * @return This builder object */ public Builder autoEscaping(boolean autoEscaping) { this.factory.autoEscaping(autoEscaping); return this; } /** * Sets whether or not core operators overrides should be allowed. The default value of this * setting is "false". * * @param allowOverrideCoreOperators Whether or not core operators overrides should be allowed. * @return This builder object */ public Builder allowOverrideCoreOperators(boolean allowOverrideCoreOperators) { this.factory.allowOverrideCoreOperators(allowOverrideCoreOperators); return this; } /** * Sets the default escaping strategy of the built-in escaper extension. * * @param strategy The name of the default escaping strategy * @return This builder object */ public Builder defaultEscapingStrategy(String strategy) { this.factory.defaultEscapingStrategy(strategy); return this; } /** * Adds an escaping strategy to the built-in escaper extension. * * @param name The name of the escaping strategy * @param strategy The strategy implementation * @return This builder object */ public Builder addEscapingStrategy(String name, EscapingStrategy strategy) { this.factory.addEscapingStrategy(name, strategy); return this; } /** * Enable/disable all caches, i.e. cache used by the engine to store compiled PebbleTemplate * instances and tags cache. The default value of this setting is "true". * * @param cacheActive toggle to enable/disable all caches * @return This builder object */ public Builder cacheActive(boolean cacheActive) { this.cacheActive = cacheActive; return this; } /** * Validator that can be used to validate object/method access * * @param methodAccessValidator Validator that can be used to validate object/method access * @return This builder object */ public Builder methodAccessValidator(MethodAccessValidator methodAccessValidator) { this.methodAccessValidator = methodAccessValidator; return this; } /** * Enable/disable treat literal decimal as Integer. Default is disabled, treated as Long. * * @param literalDecimalTreatedAsInteger toggle to enable/disable literal decimal treated as * integer * @return This builder object */ public Builder literalDecimalTreatedAsInteger(boolean literalDecimalTreatedAsInteger) { this.literalDecimalTreatedAsInteger = literalDecimalTreatedAsInteger; return this; } /** * Enable/disable treat literal numbers as BigDecimals. Default is disabled, treated as Long/Double. * * @param literalNumbersAsBigDecimals toggle to enable/disable literal numbers treated as * BigDecimals * @return This builder object */ public Builder literalNumbersAsBigDecimals(boolean literalNumbersAsBigDecimals) { this.literalNumbersAsBigDecimals = literalNumbersAsBigDecimals; return this; } /** * Enable/disable greedy matching mode for finding java method. Default is disabled. If enabled, * when can not find perfect method (method name, parameter length and parameter type are all * satisfied), reduce the limit of the parameter type, try to find other method which has * compatible parameter types. * * For example, *

 {{ obj.number(2) }} 
* the expression can match all of below methods. *
     *   public Long getNumber(Long v) {...}
     *   public Integer getNumber(Integer v) {...}
     *   public Short getNumber(Short v) {...}
     *   public Byte getNumber(Byte v) {...}
     *   ...
     * 
* * @param greedyMatchMethod toggle to enable/disable greedy match method * @return This builder object * @see TypeUtils#compatibleCast(Object, Class) */ public Builder greedyMatchMethod(boolean greedyMatchMethod) { this.greedyMatchMethod = greedyMatchMethod; return this; } /** * Registers an implementation of {@link ExtensionCustomizer} to change runtime-behaviour of standard * functionality. * * @param customizer The customizer which wraps any non-user-provided extension * @return This build object */ public Builder registerExtensionCustomizer(Function customizer) { this.factory.registerExtensionCustomizer(customizer); return this; } /** * Creates the PebbleEngine instance. * * @return A PebbleEngine object that can be used to create PebbleTemplate objects. */ public PebbleEngine build() { ExtensionRegistry extensionRegistry = this.factory.buildExtensionRegistry(); // default loader if (this.loader == null) { this.loader = new ClasspathLoader(); } // default locale if (this.defaultLocale == null) { this.defaultLocale = Locale.getDefault(); } if (this.cacheActive) { // default caches if (this.templateCache == null) { this.templateCache = new ConcurrentMapTemplateCache(); } if (this.tagCache == null) { this.tagCache = new ConcurrentMapTagCache(); } } else { this.templateCache = new NoOpTemplateCache(); this.tagCache = new NoOpTagCache(); } if (this.syntax == null) { this.syntax = new Syntax.Builder().setEnableNewLineTrimming(this.enableNewLineTrimming) .build(); } ParserOptions parserOptions = new ParserOptions(); parserOptions.setLiteralDecimalTreatedAsInteger(this.literalDecimalTreatedAsInteger); parserOptions.setLiteralNumbersAsBigDecimals(this.literalNumbersAsBigDecimals); EvaluationOptions evaluationOptions = new EvaluationOptions(this.greedyMatchMethod, this.methodAccessValidator); return new PebbleEngine(this.loader, this.syntax, this.strictVariables, this.defaultLocale, this.maxRenderedSize, this.tagCache, this.templateCache, this.executorService, extensionRegistry, parserOptions, evaluationOptions); } } public EvaluationOptions getEvaluationOptions() { return this.evaluationOptions; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/attributes/ArrayResolver.java ================================================ package io.pebbletemplates.pebble.attributes; import io.pebbletemplates.pebble.error.AttributeNotFoundException; import io.pebbletemplates.pebble.node.ArgumentsNode; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import java.lang.reflect.Array; class ArrayResolver implements AttributeResolver { static final ArrayResolver INSTANCE = new ArrayResolver(); private ArrayResolver() { } @Override public ResolvedAttribute resolve(Object instance, Object attributeNameValue, Object[] argumentValues, ArgumentsNode args, EvaluationContextImpl context, String filename, int lineNumber) { String attributeName = String.valueOf(attributeNameValue); int index = this.getIndex(attributeName); int length = Array.getLength(instance); if (index < 0 || index >= length) { if (context.isStrictVariables()) { throw new AttributeNotFoundException(null, "Index out of bounds while accessing array with strict variables on.", attributeName, lineNumber, filename); } else { return new ResolvedAttribute(null); } } return new ResolvedAttribute(Array.get(instance, index)); } private int getIndex(String attributeName) { try { return Integer.parseInt(attributeName); } catch (NumberFormatException e) { return -1; } } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/attributes/AttributeResolver.java ================================================ package io.pebbletemplates.pebble.attributes; import io.pebbletemplates.pebble.node.ArgumentsNode; import io.pebbletemplates.pebble.template.EvaluationContextImpl; /** * Resolves instance attributes */ public interface AttributeResolver { /** * Attempts to resolve an attribute of the given instance. If this method returns {@code null}, * Pebble asks the next attribute resolver in the list. * * @param instance The object which is being accessed * @param attributeNameValue The name of the attribute to resolve * @param argumentValues fully evaluated positional arguments * @param args The arguments * @param context The evaluation context * @param filename Filename of the template * @param lineNumber the line number on which the expression is defined on. * @return a {@link ResolvedAttribute} wrapping the attribute value, or {@code null} if the * attribute could not be resolved */ ResolvedAttribute resolve(Object instance, Object attributeNameValue, Object[] argumentValues, ArgumentsNode args, EvaluationContextImpl context, String filename, int lineNumber); } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/attributes/DefaultAttributeResolver.java ================================================ package io.pebbletemplates.pebble.attributes; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.node.ArgumentsNode; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.MacroAttributeProvider; import io.pebbletemplates.pebble.utils.TypeUtils; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Member; import java.lang.reflect.Method; import java.util.List; import java.util.Map; public class DefaultAttributeResolver implements AttributeResolver { private final MemberCacheUtils memberCacheUtils = new MemberCacheUtils(); @Override public ResolvedAttribute resolve(Object instance, Object attributeNameValue, Object[] argumentValues, ArgumentsNode args, EvaluationContextImpl context, String filename, int lineNumber) { if (instance != null) { String attributeName = String.valueOf(attributeNameValue); Class[] argumentTypes = this.getArgumentTypes(argumentValues); Member member = this.memberCacheUtils.getMember(instance, attributeName, argumentTypes); if (member == null) { if (argumentValues == null) { // first we check maps if (instance instanceof Map) { return MapResolver.INSTANCE .resolve(instance, attributeNameValue, null, args, context, filename, lineNumber); } // then we check arrays if (instance.getClass().isArray()) { return ArrayResolver.INSTANCE .resolve(instance, attributeNameValue, null, args, context, filename, lineNumber); } // then lists if (instance instanceof List) { ResolvedAttribute resolvedAttribute = ListResolver.INSTANCE .resolve(instance, attributeNameValue, null, args, context, filename, lineNumber); if (resolvedAttribute != null) { return resolvedAttribute; } } } if (instance instanceof MacroAttributeProvider) { return MacroResolver.INSTANCE .resolve(instance, attributeNameValue, argumentValues, args, context, filename, lineNumber); } member = this.memberCacheUtils .cacheMember(instance, attributeName, argumentTypes, context, filename, lineNumber); } if (member != null) { return new ResolvedAttribute(this.invokeMember(instance, member, argumentValues, filename, lineNumber)); } } return null; } private Class[] getArgumentTypes(Object[] argumentValues) { if (argumentValues != null) { Class[] argumentTypes = new Class[argumentValues.length]; for (int i = 0; i < argumentValues.length; i++) { Object o = argumentValues[i]; if (o == null) { argumentTypes[i] = null; } else { argumentTypes[i] = o.getClass(); } } return argumentTypes; } return new Class[0]; } /** * Invoke the "Member" that was found via reflection. */ private Object invokeMember(Object object, Member member, Object[] argumentValues, String filename, int lineNumber) { Object result = null; try { if (member instanceof Method) { Method method = (Method) member; argumentValues = TypeUtils.compatibleCast(argumentValues, method.getParameterTypes()); result = method.invoke(object, argumentValues); } else if (member instanceof Field) { result = ((Field) member).get(object); } } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { throw new PebbleException(e, "Could not call " + member.getName(), lineNumber, filename); } return result; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/attributes/ListResolver.java ================================================ package io.pebbletemplates.pebble.attributes; import io.pebbletemplates.pebble.error.AttributeNotFoundException; import io.pebbletemplates.pebble.node.ArgumentsNode; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import java.util.List; class ListResolver implements AttributeResolver { static final ListResolver INSTANCE = new ListResolver(); private ListResolver() { } @Override public ResolvedAttribute resolve(Object instance, Object attributeNameValue, Object[] argumentValues, ArgumentsNode args, EvaluationContextImpl context, String filename, int lineNumber) { String attributeName = String.valueOf(attributeNameValue); @SuppressWarnings("unchecked") List list = (List) instance; int index; try { index = Integer.parseInt(attributeName); } catch (NumberFormatException e) { return null; } int length = list.size(); if (index < 0 || index >= length) { if (context.isStrictVariables()) { throw new AttributeNotFoundException(null, "Index out of bounds while accessing array with strict variables on.", attributeName, lineNumber, filename); } else { return new ResolvedAttribute(null); } } return new ResolvedAttribute(list.get(index)); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/attributes/MacroResolver.java ================================================ package io.pebbletemplates.pebble.attributes; import io.pebbletemplates.pebble.node.ArgumentsNode; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.MacroAttributeProvider; class MacroResolver implements AttributeResolver { static final MacroResolver INSTANCE = new MacroResolver(); private MacroResolver() { } @Override public ResolvedAttribute resolve(Object instance, Object attributeNameValue, Object[] argumentValues, ArgumentsNode args, EvaluationContextImpl context, String filename, int lineNumber) { MacroAttributeProvider macroAttributeProvider = (MacroAttributeProvider) instance; String attributeName = String.valueOf(attributeNameValue); return new ResolvedAttribute( macroAttributeProvider.macro(context, attributeName, args, false, lineNumber)); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/attributes/MapResolver.java ================================================ package io.pebbletemplates.pebble.attributes; import io.pebbletemplates.pebble.error.AttributeNotFoundException; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.node.ArgumentsNode; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import java.util.Map; class MapResolver implements AttributeResolver { static final MapResolver INSTANCE = new MapResolver(); private MapResolver() { } @Override public ResolvedAttribute resolve(Object instance, Object attributeNameValue, Object[] argumentValues, ArgumentsNode args, EvaluationContextImpl context, String filename, int lineNumber) { Map object = (Map) instance; if (object.isEmpty() && !context.isStrictVariables()) { return new ResolvedAttribute(null); } Object key; if (attributeNameValue != null && Number.class .isAssignableFrom(attributeNameValue.getClass())) { Number keyAsNumber = (Number) attributeNameValue; Class keyClass = object.keySet().iterator().next().getClass(); key = this.cast(keyAsNumber, keyClass, filename, lineNumber); } else { key = attributeNameValue; } if(context.isStrictVariables() && !object.containsKey(key)) { throw new AttributeNotFoundException(null, String.format( "Attribute [%s] of [%s] does not exist or can not be accessed and strict variables is set to true.", attributeNameValue.toString(), object.getClass().getName()), attributeNameValue.toString(), lineNumber, filename); } ResolvedAttribute resolvedAttribute = new ResolvedAttribute(object.get(key)); return resolvedAttribute; } private Object cast(Number number, Class desiredType, String filename, int lineNumber) { if (desiredType == Long.class) { return number.longValue(); } else if (desiredType == Integer.class) { return number.intValue(); } else if (desiredType == Double.class) { return number.doubleValue(); } else if (desiredType == Float.class) { return number.floatValue(); } else if (desiredType == Short.class) { return number.shortValue(); } else if (desiredType == Byte.class) { return number.byteValue(); } throw new PebbleException(null, String.format("type %s not supported for key %s", desiredType, number), lineNumber, filename); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/attributes/MemberCacheUtils.java ================================================ package io.pebbletemplates.pebble.attributes; import io.pebbletemplates.pebble.error.ClassAccessException; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.EvaluationOptions; import java.lang.reflect.AccessibleObject; import java.lang.reflect.Member; import java.lang.reflect.Method; import java.util.*; import java.util.concurrent.ConcurrentHashMap; class MemberCacheUtils { private final ConcurrentHashMap memberCache = new ConcurrentHashMap<>(100, 0.9f, 1); Member getMember(Object instance, String attributeName, Class[] argumentTypes) { return this.memberCache.get(new MemberCacheKey(instance.getClass(), attributeName, argumentTypes)); } Member cacheMember(Object instance, String attributeName, Class[] argumentTypes, EvaluationContextImpl context, String filename, int lineNumber) { Member member = this.reflect(instance, attributeName, argumentTypes, filename, lineNumber, context.getEvaluationOptions()); if (member != null) { this.memberCache.put(new MemberCacheKey(instance.getClass(), attributeName, argumentTypes), member); } return member; } /** * Performs the actual reflection to obtain a "Member" from a class. */ private Member reflect(Object object, String attributeName, Class[] parameterTypes, String filename, int lineNumber, EvaluationOptions evaluationOptions) { Class clazz = object.getClass(); // capitalize first letter of attribute for the following attempts String attributeCapitalized = Character.toUpperCase(attributeName.charAt(0)) + attributeName.substring(1); // search well known super classes first to avoid illegal reflective access List> agenda = Arrays.asList( List.class, Set.class, Map.class, Map.Entry.class, Collection.class, Iterable.class, clazz); for (Class type : agenda) { if (!type.isAssignableFrom(clazz)) { continue; } // check if attribute is a public method Member result = this.findMethod(object, type, attributeName, parameterTypes, filename, lineNumber, evaluationOptions); // public field if (result == null) { try { result = type.getField(attributeName); } catch (NoSuchFieldException | SecurityException e) { } } // check get method if (result == null) { result = this.findMethod(object, type, "get" + attributeCapitalized, parameterTypes, filename, lineNumber, evaluationOptions); } // check is method if (result == null) { result = this.findMethod(object, type, "is" + attributeCapitalized, parameterTypes, filename, lineNumber, evaluationOptions); } // check has method if (result == null) { result = this.findMethod(object, type, "has" + attributeCapitalized, parameterTypes, filename, lineNumber, evaluationOptions); } if (result != null) { ((AccessibleObject) result).setAccessible(true); return result; } } return null; } /** * Finds an appropriate method by comparing if parameter types are compatible. This is more * relaxed than class.getMethod. */ private Method findMethod(Object object, Class clazz, String name, Class[] requiredTypes, String filename, int lineNumber, EvaluationOptions evaluationOptions) { List candidates = this.getCandidates(clazz, name, requiredTypes); // perfect match Method bestMatch = null; for (Method candidate : candidates) { // check if method is even compatible boolean compatibleTypes = true; Class[] types = candidate.getParameterTypes(); for (int i = 0; i < types.length; i++) { if (requiredTypes[i] != null && !this.widen(types[i]).isAssignableFrom(requiredTypes[i])) { compatibleTypes = false; break; } } // if it is compatible, check if it is a better match than the previous best if (compatibleTypes) { if (bestMatch == null) { bestMatch = candidate; } else { Class[] bestMatchParamTypes = bestMatch.getParameterTypes(); for (int i = 0; i < types.length; i++) { // if the current method's param strictly extends the previous best, it is a better match Class widened = this.widen(bestMatchParamTypes[i]); if (widened.isAssignableFrom(types[i]) && !widened.equals(types[i])) { bestMatch = candidate; break; } } } } } if (bestMatch != null) { this.verifyUnsafeMethod(filename, lineNumber, evaluationOptions, object, bestMatch); return bestMatch; } // greedy match if (evaluationOptions.isGreedyMatchMethod()) { for (Method candidate : candidates) { boolean compatibleTypes = true; Class[] types = candidate.getParameterTypes(); for (int i = 0; i < types.length; i++) { if (requiredTypes[i] != null && !this.isCompatibleType(types[i], requiredTypes[i])) { compatibleTypes = false; break; } } if (compatibleTypes) { this.verifyUnsafeMethod(filename, lineNumber, evaluationOptions, object, candidate); return candidate; } } } return null; } private void verifyUnsafeMethod(String filename, int lineNumber, EvaluationOptions evaluationOptions, Object object, Method method) { boolean methodAccessAllowed = evaluationOptions.getMethodAccessValidator() .isMethodAccessAllowed(object, method); if (!methodAccessAllowed) { throw new ClassAccessException(method, filename, lineNumber); } } /** * Performs a widening conversion (primitive to boxed type) */ private Class widen(Class clazz) { if (clazz == int.class) { return Integer.class; } if (clazz == long.class) { return Long.class; } if (clazz == double.class) { return Double.class; } if (clazz == float.class) { return Float.class; } if (clazz == short.class) { return Short.class; } if (clazz == byte.class) { return Byte.class; } if (clazz == boolean.class) { return Boolean.class; } return clazz; } private List getCandidates(Class clazz, String name, Object[] requiredTypes) { List candidates = new ArrayList<>(); Method[] methods = clazz.getMethods(); for (Method m : methods) { if (!m.getName().equalsIgnoreCase(name)) { continue; } Class[] types = m.getParameterTypes(); if (types.length != requiredTypes.length) { continue; } candidates.add(m); } return candidates; } private boolean isCompatibleType(Class type1, Class type2) { Class widenType = this.widen(type1); return Number.class.isAssignableFrom(widenType) && Number.class.isAssignableFrom(type2); } private class MemberCacheKey { private final Class clazz; private final String attributeName; private final Class[] methodParameterTypes; public MemberCacheKey(Class clazz, String attributeName, Class[] methodParameterTypes) { this.clazz = clazz; this.attributeName = attributeName; this.methodParameterTypes = methodParameterTypes; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || this.getClass() != o.getClass()) { return false; } MemberCacheKey that = (MemberCacheKey) o; if (!this.clazz.equals(that.clazz)) { return false; } if (!this.attributeName.equals(that.attributeName)) { return false; } return Arrays.equals(this.methodParameterTypes, that.methodParameterTypes); } @Override public int hashCode() { int result = this.clazz.hashCode(); result = 31 * result + this.attributeName.hashCode(); result = 31 * result + Arrays.hashCode(this.methodParameterTypes); return result; } } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/attributes/ResolvedAttribute.java ================================================ package io.pebbletemplates.pebble.attributes; public final class ResolvedAttribute { public final Object evaluatedValue; public ResolvedAttribute(Object evaluatedValue) { this.evaluatedValue = evaluatedValue; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/attributes/methodaccess/BlacklistMethodAccessValidator.java ================================================ package io.pebbletemplates.pebble.attributes.methodaccess; import java.lang.reflect.AccessibleObject; import java.lang.reflect.Method; public class BlacklistMethodAccessValidator implements MethodAccessValidator { private static final String[] FORBIDDEN_METHODS = {"getClass", "wait", "notify", "notifyAll"}; @Override public boolean isMethodAccessAllowed(Object object, Method method) { boolean methodForbidden = object instanceof Class || object instanceof Runtime || object instanceof Thread || object instanceof ThreadGroup || object instanceof System || object instanceof AccessibleObject || this.isUnsafeMethod(method); return !methodForbidden; } private boolean isUnsafeMethod(Method member) { return this.isAnyOfMethods(member, FORBIDDEN_METHODS); } private boolean isAnyOfMethods(Method member, String... methods) { for (String method : methods) { if (this.isMethodWithName(member, method)) { return true; } } return false; } private boolean isMethodWithName(Method member, String method) { return member.getName().equals(method); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/attributes/methodaccess/MethodAccessValidator.java ================================================ package io.pebbletemplates.pebble.attributes.methodaccess; import java.lang.reflect.Method; public interface MethodAccessValidator { boolean isMethodAccessAllowed(Object object, Method method); } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/attributes/methodaccess/NoOpMethodAccessValidator.java ================================================ package io.pebbletemplates.pebble.attributes.methodaccess; import java.lang.reflect.Method; public class NoOpMethodAccessValidator implements MethodAccessValidator { @Override public boolean isMethodAccessAllowed(Object object, Method method) { return true; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/cache/CacheKey.java ================================================ package io.pebbletemplates.pebble.cache; import io.pebbletemplates.pebble.node.CacheNode; import java.util.Locale; /** * Key to be used in the cache * * @author Eric Bussieres */ public class CacheKey { private final CacheNode node; private final String name; private final Locale locale; public CacheKey(CacheNode node, String name, Locale locale) { this.node = node; this.name = name; this.locale = locale; } /** * {@inheritDoc} * * @see Object#equals(Object) */ @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (this.getClass() != obj.getClass()) { return false; } CacheKey other = (CacheKey) obj; if (!this.node.equals(other.node)) { return false; } if (this.locale == null) { if (other.locale != null) { return false; } } else if (!this.locale.equals(other.locale)) { return false; } if (this.name == null) { return other.name == null; } else { return this.name.equals(other.name); } } /** * {@inheritDoc} * * @see Object#hashCode() */ @Override public int hashCode() { final int prime = 31; int result = this.node.hashCode(); result = prime * result + ((this.locale == null) ? 0 : this.locale.hashCode()); result = prime * result + ((this.name == null) ? 0 : this.name.hashCode()); return result; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/cache/PebbleCache.java ================================================ package io.pebbletemplates.pebble.cache; import java.util.function.Function; public interface PebbleCache { V computeIfAbsent(K key, Function mappingFunction); void invalidateAll(); } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/cache/tag/CaffeineTagCache.java ================================================ package io.pebbletemplates.pebble.cache.tag; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import io.pebbletemplates.pebble.cache.CacheKey; import io.pebbletemplates.pebble.cache.PebbleCache; import java.util.function.Function; public class CaffeineTagCache implements PebbleCache { private final Cache tagCache; public CaffeineTagCache() { this.tagCache = Caffeine.newBuilder() .maximumSize(200) .build(); } public CaffeineTagCache(Cache tagCache) { this.tagCache = tagCache; } @Override public Object computeIfAbsent(CacheKey key, Function mappingFunction) { return this.tagCache.get(key, mappingFunction); } @Override public void invalidateAll() { this.tagCache.invalidateAll(); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/cache/tag/ConcurrentMapTagCache.java ================================================ package io.pebbletemplates.pebble.cache.tag; import io.pebbletemplates.pebble.cache.CacheKey; import io.pebbletemplates.pebble.cache.PebbleCache; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.function.Function; public class ConcurrentMapTagCache implements PebbleCache { private final ConcurrentMap tagCache; public ConcurrentMapTagCache() { this.tagCache = new ConcurrentHashMap<>(200); } public ConcurrentMapTagCache(ConcurrentMap tagCache) { this.tagCache = tagCache; } @Override public Object computeIfAbsent(CacheKey key, Function mappingFunction) { return this.tagCache.computeIfAbsent(key, mappingFunction); } @Override public void invalidateAll() { this.tagCache.clear(); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/cache/tag/NoOpTagCache.java ================================================ package io.pebbletemplates.pebble.cache.tag; import io.pebbletemplates.pebble.cache.CacheKey; import io.pebbletemplates.pebble.cache.PebbleCache; import java.util.function.Function; public class NoOpTagCache implements PebbleCache { @Override public Object computeIfAbsent(CacheKey key, Function mappingFunction) { return mappingFunction.apply(key); } @Override public void invalidateAll() {} } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/cache/template/CaffeineTemplateCache.java ================================================ package io.pebbletemplates.pebble.cache.template; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import io.pebbletemplates.pebble.cache.PebbleCache; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.util.function.Function; public class CaffeineTemplateCache implements PebbleCache { private final Cache templateCache; public CaffeineTemplateCache() { this.templateCache = Caffeine.newBuilder() .maximumSize(200) .build(); } public CaffeineTemplateCache(Cache templateCache) { this.templateCache = templateCache; } @Override public PebbleTemplate computeIfAbsent(Object key, Function mappingFunction) { return this.templateCache.get(key, mappingFunction); } @Override public void invalidateAll() { this.templateCache.invalidateAll(); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/cache/template/ConcurrentMapTemplateCache.java ================================================ package io.pebbletemplates.pebble.cache.template; import io.pebbletemplates.pebble.cache.PebbleCache; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.function.Function; public class ConcurrentMapTemplateCache implements PebbleCache { private final ConcurrentMap templateCache; public ConcurrentMapTemplateCache() { this.templateCache = new ConcurrentHashMap<>(200); } public ConcurrentMapTemplateCache(ConcurrentMap templateCache) { this.templateCache = templateCache; } @Override public PebbleTemplate computeIfAbsent(Object key, Function mappingFunction) { return this.templateCache.computeIfAbsent(key, mappingFunction); } @Override public void invalidateAll() { this.templateCache.clear(); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/cache/template/NoOpTemplateCache.java ================================================ package io.pebbletemplates.pebble.cache.template; import io.pebbletemplates.pebble.cache.PebbleCache; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.util.function.Function; public class NoOpTemplateCache implements PebbleCache { @Override public PebbleTemplate computeIfAbsent(Object key, Function mappingFunction) { return mappingFunction.apply(key); } @Override public void invalidateAll() {} } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/error/AttributeNotFoundException.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.error; public class AttributeNotFoundException extends PebbleException { private static final long serialVersionUID = 3863732457312917327L; private final String attributeName; public AttributeNotFoundException(Throwable cause, String message, String attributeName, int lineNumber, String filename) { super(cause, message, lineNumber, filename); this.attributeName = attributeName; } public String getAttributeName() { return this.attributeName; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/error/ClassAccessException.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.error; import java.lang.reflect.Method; public class ClassAccessException extends PebbleException { private static final long serialVersionUID = 5109892021088141417L; public ClassAccessException(Method method, String filename, Integer lineNumber) { super(null, String.format("For security reasons access to %s method is denied.", method), lineNumber, filename); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/error/LoaderException.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.error; public class LoaderException extends PebbleException { private static final long serialVersionUID = -6445262510797040243L; public LoaderException(Throwable cause, String message) { super(cause, message); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/error/ParserException.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.error; public class ParserException extends PebbleException { private static final long serialVersionUID = -3712498518512126529L; public ParserException(Throwable cause, String message, int lineNumber, String filename) { super(cause, message, lineNumber, filename); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/error/PebbleException.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.error; public class PebbleException extends RuntimeException { private static final long serialVersionUID = -2855774187093732189L; protected final Integer lineNumber; protected final String filename; protected final String message; public PebbleException(Throwable cause, String message) { this(cause, message, null, null); } public PebbleException(Throwable cause, String message, Integer lineNumber, String filename) { super(String.format("%s (%s:%s)", message, filename == null ? "?" : filename, lineNumber == null ? "?" : String.valueOf(lineNumber)), cause); this.message = message; this.lineNumber = lineNumber; this.filename = filename; } /** * Returns the line number on which the exception was thrown. * * @return the line number on which the exception was thrown. */ public Integer getLineNumber() { return this.lineNumber; } /** * Returns the filename in which the exception was thrown. * * @return the filename in which the exception was thrown. */ public String getFileName() { return this.filename; } /** * Returns the message which is set for the exception by Pebble. Its the message which is not * enhanced with the line number and filename. * * @return the message which is set for the exception by Pebble. */ public String getPebbleMessage() { return this.message; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/error/RootAttributeNotFoundException.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.error; public class RootAttributeNotFoundException extends AttributeNotFoundException { private static final long serialVersionUID = 3863732457312917327L; public RootAttributeNotFoundException(Throwable cause, String message, String attributeName, int lineNumber, String filename) { super(cause, message, attributeName, lineNumber, filename); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/AbstractExtension.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.extension; import io.pebbletemplates.pebble.attributes.AttributeResolver; import io.pebbletemplates.pebble.operator.BinaryOperator; import io.pebbletemplates.pebble.operator.UnaryOperator; import io.pebbletemplates.pebble.tokenParser.TokenParser; import java.util.List; import java.util.Map; public abstract class AbstractExtension implements Extension { @Override public List getTokenParsers() { return null; } @Override public List getBinaryOperators() { return null; } @Override public List getUnaryOperators() { return null; } @Override public Map getFilters() { return null; } @Override public Map getTests() { return null; } @Override public Map getFunctions() { return null; } @Override public Map getGlobalVariables() { return null; } @Override public List getNodeVisitors() { return null; } @Override public List getAttributeResolver() { return null; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/AbstractNodeVisitor.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.extension; import io.pebbletemplates.pebble.node.ArgumentsNode; import io.pebbletemplates.pebble.node.AutoEscapeNode; import io.pebbletemplates.pebble.node.BlockNode; import io.pebbletemplates.pebble.node.BodyNode; import io.pebbletemplates.pebble.node.ExtendsNode; import io.pebbletemplates.pebble.node.FlushNode; import io.pebbletemplates.pebble.node.ForNode; import io.pebbletemplates.pebble.node.IfNode; import io.pebbletemplates.pebble.node.ImportNode; import io.pebbletemplates.pebble.node.IncludeNode; import io.pebbletemplates.pebble.node.MacroNode; import io.pebbletemplates.pebble.node.NamedArgumentNode; import io.pebbletemplates.pebble.node.Node; import io.pebbletemplates.pebble.node.ParallelNode; import io.pebbletemplates.pebble.node.PositionalArgumentNode; import io.pebbletemplates.pebble.node.PrintNode; import io.pebbletemplates.pebble.node.RootNode; import io.pebbletemplates.pebble.node.SetNode; import io.pebbletemplates.pebble.node.TextNode; import io.pebbletemplates.pebble.node.expression.Expression; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; import io.pebbletemplates.pebble.utils.Pair; /** * A base node visitor that can be extended for the sake of using it's navigational abilities. * * @author Mitchell */ public class AbstractNodeVisitor implements NodeVisitor { private final PebbleTemplateImpl template; public AbstractNodeVisitor(PebbleTemplateImpl template) { this.template = template; } /** * Default method used for unknown nodes such as nodes from a user provided extension. */ @Override public void visit(Node node) { } /* * OVERLOADED NODES (keep alphabetized) */ @Override public void visit(ArgumentsNode node) { if (node.getNamedArgs() != null) { for (Node arg : node.getNamedArgs()) { arg.accept(this); } } if (node.getPositionalArgs() != null) { for (Node arg : node.getPositionalArgs()) { arg.accept(this); } } } @Override public void visit(AutoEscapeNode node) { node.getBody().accept(this); } @Override public void visit(BlockNode node) { node.getBody().accept(this); } @Override public void visit(BodyNode node) { for (Node child : node.getChildren()) { child.accept(this); } } @Override public void visit(ExtendsNode node) { node.getParentExpression().accept(this); } @Override public void visit(FlushNode node) { } @Override public void visit(ForNode node) { node.getIterable().accept(this); node.getBody().accept(this); if (node.getElseBody() != null) { node.getElseBody().accept(this); } } @Override public void visit(IfNode node) { for (Pair, BodyNode> pairs : node.getConditionsWithBodies()) { pairs.getLeft().accept(this); pairs.getRight().accept(this); } if (node.getElseBody() != null) { node.getElseBody().accept(this); } } @Override public void visit(ImportNode node) { node.getImportExpression().accept(this); } @Override public void visit(IncludeNode node) { node.getIncludeExpression().accept(this); } @Override public void visit(MacroNode node) { node.getBody().accept(this); node.getArgs().accept(this); } @Override public void visit(NamedArgumentNode node) { if (node.getValueExpression() != null) { node.getValueExpression().accept(this); } } @Override public void visit(ParallelNode node) { node.getBody().accept(this); } @Override public void visit(PositionalArgumentNode node) { node.getValueExpression().accept(this); } @Override public void visit(PrintNode node) { node.getExpression().accept(this); } @Override public void visit(RootNode node) { node.getBody().accept(this); } @Override public void visit(SetNode node) { node.getValue().accept(this); } @Override public void visit(TextNode node) { } protected PebbleTemplateImpl getTemplate() { return this.template; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/Extension.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.extension; import io.pebbletemplates.pebble.attributes.AttributeResolver; import io.pebbletemplates.pebble.operator.BinaryOperator; import io.pebbletemplates.pebble.operator.UnaryOperator; import io.pebbletemplates.pebble.tokenParser.TokenParser; import java.util.List; import java.util.Map; public interface Extension { /** * Use this method to provide custom filters. * * @return A list of filters. It is okay to return null. */ Map getFilters(); /** * Use this method to provide custom tests. * * @return A list of tests. It is okay to return null. */ Map getTests(); /** * Use this method to provide custom functions. * * @return A list of functions. It is okay to return null. */ Map getFunctions(); /** * Use this method to provide custom tags. * * A TokenParser is used to parse a stream of tokens into Nodes which are then responsible for * compiling themselves into Java. * * @return A list of TokenParsers. It is okay to return null. */ List getTokenParsers(); /** * Use this method to provide custom binary operators. * * @return A list of Operators. It is okay to return null; */ List getBinaryOperators(); /** * Use this method to provide custom unary operators. * * @return A list of Operators. It is okay to return null; */ List getUnaryOperators(); /** * Use this method to provide variables available to all templates * * @return Map of global variables available to all templates */ Map getGlobalVariables(); /** * Node visitors will travel the AST tree during the compilation phase. * * @return a list of node visitors */ List getNodeVisitors(); /** * AttributeResolver will resolve instance attributes * * @return a list of attribute resolver */ List getAttributeResolver(); } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/ExtensionCustomizer.java ================================================ package io.pebbletemplates.pebble.extension; import io.pebbletemplates.pebble.PebbleEngine; import io.pebbletemplates.pebble.attributes.AttributeResolver; import io.pebbletemplates.pebble.operator.BinaryOperator; import io.pebbletemplates.pebble.operator.UnaryOperator; import io.pebbletemplates.pebble.tokenParser.TokenParser; import java.util.List; import java.util.Map; /** * Base class that allows implementing a customizer to modify Pebbles build-in extensions. * It is meant to provide a way to remove or replace functions, filters, tags, etc. to change * the standard behaviour. Use-cases can be down-stripping available functionality for security * reasons. * * Implementations of this class are meant to overwrite methods and access registered functionality * before it is loaded into the PebbleEngine by calling super. * * The ExentsionCustomizer can be registred via {@link PebbleEngine.Builder#registerExtensionCustomizer} * and is applied for every non-user-provided extension. * */ public abstract class ExtensionCustomizer implements Extension { private final Extension delegate; public ExtensionCustomizer(Extension delegate) { this.delegate = delegate; } @Override public Map getFilters() { return delegate.getFilters(); } @Override public Map getTests() { return delegate.getTests(); } @Override public Map getFunctions() { return delegate.getFunctions(); } @Override public List getTokenParsers() { return delegate.getTokenParsers(); } @Override public List getBinaryOperators() { return delegate.getBinaryOperators(); } @Override public List getUnaryOperators() { return delegate.getUnaryOperators(); } @Override public Map getGlobalVariables() { return delegate.getGlobalVariables(); } @Override public List getNodeVisitors() { return delegate.getNodeVisitors(); } @Override public List getAttributeResolver() { return delegate.getAttributeResolver(); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/ExtensionRegistry.java ================================================ package io.pebbletemplates.pebble.extension; import io.pebbletemplates.pebble.attributes.AttributeResolver; import io.pebbletemplates.pebble.operator.BinaryOperator; import io.pebbletemplates.pebble.operator.UnaryOperator; import io.pebbletemplates.pebble.tokenParser.TokenParser; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Storage for the extensions and the components retrieved from the various extensions. *

* Created by mitch_000 on 2015-11-28. */ public class ExtensionRegistry { /** * Unary operators used during the lexing phase. */ private final Map unaryOperators = new HashMap<>(); /** * Binary operators used during the lexing phase. */ private final Map binaryOperators = new HashMap<>(); /** * Token parsers used during the parsing phase. */ private final Map tokenParsers = new HashMap<>(); /** * Node visitors available during the parsing phase. */ private final List nodeVisitors = new ArrayList<>(); /** * Filters used during the evaluation phase. */ private final Map filters = new HashMap<>(); /** * Tests used during the evaluation phase. */ private final Map tests = new HashMap<>(); /** * Functions used during the evaluation phase. */ private final Map functions = new HashMap<>(); /** * Global variables available during the evaluation phase. */ private final Map globalVariables = new HashMap<>(); private final List attributeResolver = new ArrayList<>(); public ExtensionRegistry() { } public ExtensionRegistry(Collection extensions) { for (Extension extension : extensions) { addExtension(extension); } } public void addOperatorOverridingExtension(Extension extension) { addExtension(extension, true); } public void addExtension(Extension extension) { addExtension(extension, false); } private void addExtension(Extension extension, boolean operatorOverriding) { // token parsers List tokenParsers = extension.getTokenParsers(); if (tokenParsers != null) { for (TokenParser tokenParser : tokenParsers) { this.tokenParsers.put(tokenParser.getTag(), tokenParser); } } // binary operators List binaryOperators = extension.getBinaryOperators(); if (binaryOperators != null) { for (BinaryOperator operator : binaryOperators) { if (operatorOverriding) { this.binaryOperators.put(operator.getSymbol(), operator); } else { this.binaryOperators.putIfAbsent(operator.getSymbol(), operator); } } } // unary operators List unaryOperators = extension.getUnaryOperators(); if (unaryOperators != null) { for (UnaryOperator operator : unaryOperators) { if (operatorOverriding) { this.unaryOperators.put(operator.getSymbol(), operator); } else { this.unaryOperators.putIfAbsent(operator.getSymbol(), operator); } } } // filters Map filters = extension.getFilters(); if (filters != null) { this.filters.putAll(filters); } // tests Map tests = extension.getTests(); if (tests != null) { this.tests.putAll(tests); } // functions Map functions = extension.getFunctions(); if (functions != null) { this.functions.putAll(functions); } // global variables Map globalVariables = extension.getGlobalVariables(); if (globalVariables != null) { this.globalVariables.putAll(globalVariables); } // node visitors List nodeVisitors = extension.getNodeVisitors(); if (nodeVisitors != null) { this.nodeVisitors.addAll(nodeVisitors); } // attribute resolver List attributeResolvers = extension.getAttributeResolver(); if (attributeResolvers != null) { this.attributeResolver.addAll(attributeResolvers); } } public Filter getFilter(String name) { return this.filters.get(name); } public Test getTest(String name) { return this.tests.get(name); } public Function getFunction(String name) { return this.functions.get(name); } public Map getBinaryOperators() { return this.binaryOperators; } public Map getUnaryOperators() { return this.unaryOperators; } public List getNodeVisitors() { return this.nodeVisitors; } public Map getGlobalVariables() { return this.globalVariables; } public Map getTokenParsers() { return this.tokenParsers; } public List getAttributeResolver() { return this.attributeResolver; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/ExtensionRegistryFactory.java ================================================ package io.pebbletemplates.pebble.extension; import io.pebbletemplates.pebble.extension.core.AttributeResolverExtension; import io.pebbletemplates.pebble.extension.i18n.I18nExtension; import io.pebbletemplates.pebble.PebbleEngine; import io.pebbletemplates.pebble.extension.core.CoreExtension; import io.pebbletemplates.pebble.extension.escaper.EscaperExtension; import io.pebbletemplates.pebble.extension.escaper.EscapingStrategy; import java.util.*; import java.util.function.Function; import java.util.stream.Stream; /** * Provides configuration methods and builds the {@link ExtensionRegistry}. Used only internally by * the {@link PebbleEngine.Builder}. * */ public class ExtensionRegistryFactory { private final List userProvidedExtensions = new ArrayList<>(); private final EscaperExtension escaperExtension = new EscaperExtension(); private boolean allowOverrideCoreOperators = false; private Function customizer = Function.identity(); public ExtensionRegistry buildExtensionRegistry() { ExtensionRegistry extensionRegistry = new ExtensionRegistry(); Stream.of(new CoreExtension(), this.escaperExtension, new I18nExtension()) .map(customizer::apply) .forEach(extensionRegistry::addExtension); for (Extension userProvidedExtension : this.userProvidedExtensions) { if (this.allowOverrideCoreOperators) { extensionRegistry.addOperatorOverridingExtension(userProvidedExtension); } else { extensionRegistry.addExtension(userProvidedExtension); } } extensionRegistry.addExtension(customizer.apply(new AttributeResolverExtension())); return extensionRegistry; } public void autoEscaping(boolean autoEscaping) { this.escaperExtension.setAutoEscaping(autoEscaping); } public void addEscapingStrategy(String name, EscapingStrategy strategy) { this.escaperExtension.addEscapingStrategy(name, strategy); } public void extension(Extension... extensions) { Collections.addAll(this.userProvidedExtensions, extensions); } public void allowOverrideCoreOperators(boolean allowOverrideCoreOperators) { this.allowOverrideCoreOperators = allowOverrideCoreOperators; } public void defaultEscapingStrategy(String strategy) { this.escaperExtension.setDefaultStrategy(strategy); } public void registerExtensionCustomizer(Function customizer) { this.customizer = customizer::apply; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/Filter.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.extension; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.util.Map; public interface Filter extends NamedArguments { Object apply(Object input, Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) throws PebbleException; } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/Function.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.extension; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.util.Map; public interface Function extends NamedArguments { Object execute(Map args, PebbleTemplate self, EvaluationContext context, int lineNumber); } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/NamedArguments.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.extension; import java.util.List; public interface NamedArguments { List getArgumentNames(); } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/NodeVisitor.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.extension; import io.pebbletemplates.pebble.node.ArgumentsNode; import io.pebbletemplates.pebble.node.AutoEscapeNode; import io.pebbletemplates.pebble.node.BlockNode; import io.pebbletemplates.pebble.node.BodyNode; import io.pebbletemplates.pebble.node.ExtendsNode; import io.pebbletemplates.pebble.node.FlushNode; import io.pebbletemplates.pebble.node.ForNode; import io.pebbletemplates.pebble.node.IfNode; import io.pebbletemplates.pebble.node.ImportNode; import io.pebbletemplates.pebble.node.IncludeNode; import io.pebbletemplates.pebble.node.MacroNode; import io.pebbletemplates.pebble.node.NamedArgumentNode; import io.pebbletemplates.pebble.node.Node; import io.pebbletemplates.pebble.node.ParallelNode; import io.pebbletemplates.pebble.node.PositionalArgumentNode; import io.pebbletemplates.pebble.node.PrintNode; import io.pebbletemplates.pebble.node.RootNode; import io.pebbletemplates.pebble.node.SetNode; import io.pebbletemplates.pebble.node.TextNode; /** * Will visit all the nodes of the AST provided by the parser. The NodeVisitor is responsible for * the navigating the tree, it can extend AbstractNodeVisitor for help with this. * * A NodeVisitor can still use method overloading to visit expressions (it's just not required). * *

* The implementor does not need to make sure that the implementation is thread-safe. * * @author Mitchell */ public interface NodeVisitor { /** * Default method invoked with unknown nodes such as nodes provided by user extensions. * * @param node Node to visit */ void visit(Node node); /* * OVERLOADED NODES (keep alphabetized) */ void visit(ArgumentsNode node); void visit(AutoEscapeNode node); void visit(BlockNode node); void visit(BodyNode node); void visit(ExtendsNode node); void visit(FlushNode node); void visit(ForNode node); void visit(IfNode node); void visit(ImportNode node); void visit(IncludeNode node); void visit(MacroNode node); void visit(NamedArgumentNode node); void visit(ParallelNode node); void visit(PositionalArgumentNode node); void visit(PrintNode node); void visit(RootNode node); void visit(SetNode node); void visit(TextNode node); } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/NodeVisitorFactory.java ================================================ package io.pebbletemplates.pebble.extension; import io.pebbletemplates.pebble.template.PebbleTemplate; /** * The node visitor factory creates {@link NodeVisitor}s. * *

* {@link Extension} can provide own implementation to provide their own {@link NodeVisitor}s. * * @author Thomas Hunziker */ public interface NodeVisitorFactory { /** * This method creates a new instance of a {@link NodeVisitor}. * *

* The method is called whenever a visitor is applied to a {@link PebbleTemplate}. * *

* The method needs to be thread-safe. However the {@link NodeVisitor} itself does not need to be * thread-safe. * * @param template the template for which a visitor should be created for. * @return the visitor. */ NodeVisitor createVisitor(PebbleTemplate template); } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/Test.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.extension; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.util.Map; public interface Test extends NamedArguments { boolean apply(Object input, Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) throws PebbleException; } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/core/AbbreviateFilter.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.extension.core; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.extension.Filter; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.util.ArrayList; import java.util.List; import java.util.Map; public class AbbreviateFilter implements Filter { private final List argumentNames = new ArrayList<>(); public AbbreviateFilter() { this.argumentNames.add("length"); } @Override public List getArgumentNames() { return this.argumentNames; } @Override public Object apply(Object input, Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { if (input == null) { return null; } String value = (String) input; int maxWidth = ((Long) args.get("length")).intValue(); if (maxWidth < 0) { throw new PebbleException(null, "Invalid argument to abbreviate filter; must be greater than zero", lineNumber, self.getName()); } String ellipsis = "..."; int length = value.length(); if (length < maxWidth) { return value; } if (length <= 3) { return value; } if (maxWidth <= 3) { return value.substring(0, maxWidth); } return value.substring(0, Math.max(0, maxWidth - 3)) + ellipsis; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/core/AbsFilter.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.extension.core; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.extension.Filter; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.math.BigDecimal; import java.math.BigInteger; import java.util.List; import java.util.Map; public class AbsFilter implements Filter { @Override public List getArgumentNames() { return null; } @Override public Number apply(Object input, Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) throws PebbleException { if (input == null) { throw new PebbleException(null, "Can not pass null value to \"abs\" filter.", lineNumber, self.getName()); } if (input instanceof Integer) { return Math.abs((Integer) input); } else if (input instanceof Byte) { return Math.abs((Byte) input); } else if (input instanceof Short) { return Math.abs((Short) input); } else if (input instanceof Float) { return Math.abs((Float) input); } else if (input instanceof Long) { return Math.abs((Long) input); } else if (input instanceof Double) { return Math.abs((Double) input); } else if (input instanceof BigDecimal) { return ((BigDecimal) input).abs(); } else if (input instanceof BigInteger) { return ((BigInteger) input).abs(); } else if (input instanceof Number) { // We make here an assumption that we have checked all special // cases. return Math.abs(((Number) input).doubleValue()); } else { throw new PebbleException(null, "The 'abs' filter does require as input a number.", lineNumber, self.getName()); } } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/core/AttributeResolverExtension.java ================================================ package io.pebbletemplates.pebble.extension.core; import io.pebbletemplates.pebble.attributes.AttributeResolver; import io.pebbletemplates.pebble.attributes.DefaultAttributeResolver; import io.pebbletemplates.pebble.extension.AbstractExtension; import java.util.ArrayList; import java.util.List; public class AttributeResolverExtension extends AbstractExtension { @Override public List getAttributeResolver() { List attributeResolvers = new ArrayList<>(); attributeResolvers.add(new DefaultAttributeResolver()); return attributeResolvers; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/core/Base64DecoderFilter.java ================================================ package io.pebbletemplates.pebble.extension.core; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.extension.Filter; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.List; import java.util.Map; /** * This class implements the 'base64encode' filter. * * @author Silviu Vergoti */ public class Base64DecoderFilter implements Filter { public static final String FILTER_NAME = "base64decode"; @Override public List getArgumentNames() { return null; } @Override public Object apply(Object input, Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) throws PebbleException { if (input == null) { return null; } String decoded = null; if (input instanceof String) { try { byte [] bytes = Base64.getDecoder().decode(((String) input).getBytes(StandardCharsets.UTF_8)); decoded = new String(bytes, StandardCharsets.UTF_8); } catch (Exception e) { throw new PebbleException(e, "Please provide a correctly Base64 encoded string containing an UTF-8 string\n", lineNumber, self.getName()); } } else { throw new PebbleException(null, "This filter applies to String\n", lineNumber, self.getName()); } return decoded; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/core/Base64EncoderFilter.java ================================================ package io.pebbletemplates.pebble.extension.core; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.extension.Filter; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.List; import java.util.Map; /** * This class implements the 'base64encode' filter. * * @author Silviu Vergoti */ public class Base64EncoderFilter implements Filter { public static final String FILTER_NAME = "base64encode"; @Override public List getArgumentNames() { return null; } @Override public Object apply(Object input, Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) throws PebbleException { if (input == null) { return null; } return Base64.getEncoder().encodeToString(input.toString().getBytes(StandardCharsets.UTF_8)); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/core/CapitalizeFilter.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.extension.core; import io.pebbletemplates.pebble.extension.Filter; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.util.Arrays; import java.util.List; import java.util.Map; public class CapitalizeFilter implements Filter { @Override public List getArgumentNames() { return null; } @Override public Object apply(Object input, Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { if (input == null) { return null; } String value = (String) input; if (value.length() == 0) { return value; } StringBuilder result = new StringBuilder(); char[] chars = value.toCharArray(); for (int i = 0; i < chars.length; i++) { char c = chars[i]; if (Character.isWhitespace(c)) { result.append(c); } else { result.append(Character.toTitleCase(c)); result.append(Arrays.copyOfRange(chars, i + 1, chars.length)); break; } } return result.toString(); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/core/CoreExtension.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.extension.core; import io.pebbletemplates.pebble.extension.AbstractExtension; import io.pebbletemplates.pebble.extension.Filter; import io.pebbletemplates.pebble.extension.Function; import io.pebbletemplates.pebble.extension.NodeVisitorFactory; import io.pebbletemplates.pebble.extension.Test; import io.pebbletemplates.pebble.node.expression.AddExpression; import io.pebbletemplates.pebble.node.expression.AndExpression; import io.pebbletemplates.pebble.node.expression.ConcatenateExpression; import io.pebbletemplates.pebble.node.expression.ContainsExpression; import io.pebbletemplates.pebble.node.expression.DivideExpression; import io.pebbletemplates.pebble.node.expression.EqualsExpression; import io.pebbletemplates.pebble.node.expression.FilterExpression; import io.pebbletemplates.pebble.node.expression.GreaterThanEqualsExpression; import io.pebbletemplates.pebble.node.expression.GreaterThanExpression; import io.pebbletemplates.pebble.node.expression.LessThanEqualsExpression; import io.pebbletemplates.pebble.node.expression.LessThanExpression; import io.pebbletemplates.pebble.node.expression.ModulusExpression; import io.pebbletemplates.pebble.node.expression.MultiplyExpression; import io.pebbletemplates.pebble.node.expression.NegativeTestExpression; import io.pebbletemplates.pebble.node.expression.NotEqualsExpression; import io.pebbletemplates.pebble.node.expression.OrExpression; import io.pebbletemplates.pebble.node.expression.PositiveTestExpression; import io.pebbletemplates.pebble.node.expression.RangeExpression; import io.pebbletemplates.pebble.node.expression.SubtractExpression; import io.pebbletemplates.pebble.node.expression.UnaryMinusExpression; import io.pebbletemplates.pebble.node.expression.UnaryNotExpression; import io.pebbletemplates.pebble.node.expression.UnaryPlusExpression; import io.pebbletemplates.pebble.operator.Associativity; import io.pebbletemplates.pebble.operator.BinaryOperator; import io.pebbletemplates.pebble.operator.BinaryOperatorImpl; import io.pebbletemplates.pebble.operator.UnaryOperator; import io.pebbletemplates.pebble.operator.UnaryOperatorImpl; import io.pebbletemplates.pebble.operator.*; import io.pebbletemplates.pebble.tokenParser.BlockTokenParser; import io.pebbletemplates.pebble.tokenParser.CacheTokenParser; import io.pebbletemplates.pebble.tokenParser.EmbedTokenParser; import io.pebbletemplates.pebble.tokenParser.ExtendsTokenParser; import io.pebbletemplates.pebble.tokenParser.FilterTokenParser; import io.pebbletemplates.pebble.tokenParser.FlushTokenParser; import io.pebbletemplates.pebble.tokenParser.ForTokenParser; import io.pebbletemplates.pebble.tokenParser.FromTokenParser; import io.pebbletemplates.pebble.tokenParser.IfTokenParser; import io.pebbletemplates.pebble.tokenParser.ImportTokenParser; import io.pebbletemplates.pebble.tokenParser.IncludeTokenParser; import io.pebbletemplates.pebble.tokenParser.MacroTokenParser; import io.pebbletemplates.pebble.tokenParser.ParallelTokenParser; import io.pebbletemplates.pebble.tokenParser.SetTokenParser; import io.pebbletemplates.pebble.tokenParser.TokenParser; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; public class CoreExtension extends AbstractExtension { @Override public List getTokenParsers() { List parsers = new ArrayList<>(); parsers.add(new BlockTokenParser()); parsers.add(new ExtendsTokenParser()); parsers.add(new EmbedTokenParser()); parsers.add(new FilterTokenParser()); parsers.add(new FlushTokenParser()); parsers.add(new ForTokenParser()); parsers.add(new IfTokenParser()); parsers.add(new ImportTokenParser()); parsers.add(new IncludeTokenParser()); parsers.add(new MacroTokenParser()); parsers.add(new ParallelTokenParser()); parsers.add(new SetTokenParser()); parsers.add(new CacheTokenParser()); parsers.add(new FromTokenParser()); // verbatim tag is implemented directly in the LexerImpl return parsers; } @Override public List getUnaryOperators() { List operators = new ArrayList<>(); operators.add(new UnaryOperatorImpl("not", 500, UnaryNotExpression.class)); operators.add(new UnaryOperatorImpl("+", 500, UnaryPlusExpression.class)); operators.add(new UnaryOperatorImpl("-", 500, UnaryMinusExpression.class)); return operators; } @Override public List getBinaryOperators() { List operators = new ArrayList<>(); operators.add(new BinaryOperatorImpl("or", 10, OrExpression::new, BinaryOperatorType.NORMAL, Associativity.LEFT)); operators.add(new BinaryOperatorImpl("and", 15, AndExpression::new, BinaryOperatorType.NORMAL, Associativity.LEFT)); operators.add(new BinaryOperatorImpl("is", 20, PositiveTestExpression::new, BinaryOperatorType.TEST, Associativity.LEFT)); operators.add(new BinaryOperatorImpl("is not", 20, NegativeTestExpression::new, BinaryOperatorType.TEST, Associativity.LEFT)); operators.add(new BinaryOperatorImpl("contains", 20, ContainsExpression::new, BinaryOperatorType.NORMAL, Associativity.LEFT)); operators.add(new BinaryOperatorImpl("==", 30, EqualsExpression::new, BinaryOperatorType.NORMAL, Associativity.LEFT)); operators.add(new BinaryOperatorImpl("equals", 30, EqualsExpression::new, BinaryOperatorType.NORMAL, Associativity.LEFT)); operators.add(new BinaryOperatorImpl("!=", 30, NotEqualsExpression::new, BinaryOperatorType.NORMAL, Associativity.LEFT)); operators.add(new BinaryOperatorImpl(">", 30, GreaterThanExpression::new, BinaryOperatorType.NORMAL, Associativity.LEFT)); operators.add(new BinaryOperatorImpl("<", 30, LessThanExpression::new, BinaryOperatorType.NORMAL, Associativity.LEFT)); operators.add(new BinaryOperatorImpl(">=", 30, GreaterThanEqualsExpression::new, BinaryOperatorType.NORMAL, Associativity.LEFT)); operators.add(new BinaryOperatorImpl("<=", 30, LessThanEqualsExpression::new, BinaryOperatorType.NORMAL, Associativity.LEFT)); operators.add(new BinaryOperatorImpl("+", 40, AddExpression::new, BinaryOperatorType.NORMAL, Associativity.LEFT)); operators.add(new BinaryOperatorImpl("-", 40, SubtractExpression::new, BinaryOperatorType.NORMAL, Associativity.LEFT)); operators.add(new BinaryOperatorImpl("*", 60, MultiplyExpression::new, BinaryOperatorType.NORMAL, Associativity.LEFT)); operators.add(new BinaryOperatorImpl("/", 60, DivideExpression::new, BinaryOperatorType.NORMAL, Associativity.LEFT)); operators.add(new BinaryOperatorImpl("%", 60, ModulusExpression::new, BinaryOperatorType.NORMAL, Associativity.LEFT)); operators.add(new BinaryOperatorImpl("|", 100, FilterExpression::new, BinaryOperatorType.FILTER, Associativity.LEFT)); operators.add(new BinaryOperatorImpl("~", 110, ConcatenateExpression::new, BinaryOperatorType.NORMAL, Associativity.LEFT)); operators.add(new BinaryOperatorImpl("..", 120, RangeExpression::new, BinaryOperatorType.NORMAL, Associativity.LEFT)); return operators; } @Override public Map getFilters() { Map filters = new HashMap<>(); filters.put("abbreviate", new AbbreviateFilter()); filters.put("abs", new AbsFilter()); filters.put("capitalize", new CapitalizeFilter()); filters.put("date", new DateFilter()); filters.put("default", new DefaultFilter()); filters.put("first", new FirstFilter()); filters.put("format", new FormatFilter()); filters.put("join", new JoinFilter()); filters.put("last", new LastFilter()); filters.put("lower", new LowerFilter()); filters.put("numberformat", new NumberFormatFilter()); filters.put("slice", new SliceFilter()); filters.put("sort", new SortFilter()); filters.put("rsort", new RsortFilter()); filters.put("reverse", new ReverseFilter()); filters.put("title", new TitleFilter()); filters.put("trim", new TrimFilter()); filters.put("upper", new UpperFilter()); filters.put("urlencode", new UrlEncoderFilter()); filters.put("length", new LengthFilter()); filters.put(ReplaceFilter.FILTER_NAME, new ReplaceFilter()); filters.put(MergeFilter.FILTER_NAME, new MergeFilter()); filters.put(SplitFilter.FILTER_NAME, new SplitFilter()); filters.put(Base64EncoderFilter.FILTER_NAME, new Base64EncoderFilter()); filters.put(Base64DecoderFilter.FILTER_NAME, new Base64DecoderFilter()); filters.put(Sha256Filter.FILTER_NAME, new Sha256Filter()); filters.put(Nl2brFilter.FILTER_NAME, new Nl2brFilter()); return filters; } @Override public Map getTests() { Map tests = new HashMap<>(); tests.put("empty", new EmptyTest()); tests.put("even", new EvenTest()); tests.put("iterable", new IterableTest()); tests.put("map", new MapTest()); tests.put("null", new NullTest()); tests.put("odd", new OddTest()); tests.put("defined", new DefinedTest()); return tests; } @Override public Map getFunctions() { Map functions = new HashMap<>(); /* * For efficiency purposes, some core functions are individually parsed * by our expression parser and compiled in their own unique way. This * includes the block and parent functions. */ functions.put("max", new MaxFunction()); functions.put("min", new MinFunction()); functions.put(RangeFunction.FUNCTION_NAME, new RangeFunction()); return functions; } @Override public Map getGlobalVariables() { return null; } @Override public List getNodeVisitors() { List visitors = new ArrayList<>(); visitors.add(new MacroAndBlockRegistrantNodeVisitorFactory()); return visitors; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/core/DateFilter.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.extension.core; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.extension.Filter; import io.pebbletemplates.pebble.extension.escaper.SafeString; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.time.DateTimeException; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.time.temporal.TemporalAccessor; import java.time.temporal.TemporalQueries; import java.util.*; import static java.lang.String.format; public class DateFilter implements Filter { private final List argumentNames = new ArrayList<>(); public DateFilter() { this.argumentNames.add("format"); this.argumentNames.add("existingFormat"); this.argumentNames.add("timeZone"); } @Override public List getArgumentNames() { return this.argumentNames; } @Override public Object apply(Object input, Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) throws PebbleException { if (input == null) { return null; } final Locale locale = context.getLocale(); final String format = (String) args.get("format"); final String timeZone = (String) args.get("timeZone"); final String existingFormat = (String) args.get("existingFormat"); if (TemporalAccessor.class.isAssignableFrom(input.getClass())) { return this.applyTemporal((TemporalAccessor) input, self, locale, lineNumber, format, timeZone); } return this.applyDate( input, self, locale, lineNumber, format, existingFormat, timeZone); } private Object applyDate(Object dateOrString, final PebbleTemplate self, final Locale locale, int lineNumber, final String format, final String existingFormatString, final String timeZone) throws PebbleException { Date date; DateFormat intendedFormat; if (dateOrString instanceof Date) { date = (Date) dateOrString; } else if (dateOrString instanceof Number) { date = new Date(((Number) dateOrString).longValue()); } else if (dateOrString instanceof String) { try { SimpleDateFormat formatter; if (existingFormatString != null){ formatter = new SimpleDateFormat(existingFormatString, locale); } else { formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", locale); } date = formatter.parse(dateOrString.toString()); } catch (Exception e) { String formatTried = existingFormatString != null ? existingFormatString : "yyyy-MM-dd'T'HH:mm:ssZ"; throw new PebbleException(e, String.format("Could not parse the string '%s' into a date, with formatting: %s", dateOrString, formatTried), lineNumber, self.getName()); } } else { throw new IllegalArgumentException( format("Unsupported argument type: %s (value: %s)", dateOrString.getClass().getName(), dateOrString)); } intendedFormat = new SimpleDateFormat(format == null ? "yyyy-MM-dd'T'HH:mm:ssZ" : format, locale); if (timeZone != null) { intendedFormat.setTimeZone(TimeZone.getTimeZone(timeZone)); } return new SafeString(intendedFormat.format(date)); } private Object applyTemporal(final TemporalAccessor input, PebbleTemplate self, final Locale locale, int lineNumber, final String format, final String timeZone) throws PebbleException { DateTimeFormatter formatter = format != null ? DateTimeFormatter.ofPattern(format, locale) : DateTimeFormatter.ISO_DATE_TIME; ZoneId zoneId = getZoneId(input, timeZone); formatter = formatter.withZone(zoneId); try { return new SafeString(formatter.format(input)); } catch (DateTimeException dte) { throw new PebbleException( dte, String.format("Could not format instance '%s' of type %s into a date.", input.toString(), input.getClass()), lineNumber, self.getName()); } } private ZoneId getZoneId(TemporalAccessor input, String timeZone) { // First try the time zone of the input. ZoneId zoneId = input.query(TemporalQueries.zone()); if (zoneId == null && timeZone != null) { // Fallback to time zone provided as filter argument. zoneId = ZoneId.of(timeZone); } if (zoneId == null) { // Fallback to system time zone. zoneId = ZoneId.systemDefault(); } return zoneId; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/core/DefaultFilter.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.extension.core; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.extension.Filter; import io.pebbletemplates.pebble.extension.Test; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; public class DefaultFilter implements Filter { private final List argumentNames = new ArrayList<>(); public DefaultFilter() { this.argumentNames.add("default"); } @Override public List getArgumentNames() { return this.argumentNames; } @Override public Object apply(Object input, Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) throws PebbleException { Object defaultObj = args.get("default"); Test emptyTest = new EmptyTest(); if (emptyTest.apply(input, new HashMap<>(), self, context, lineNumber)) { return defaultObj; } return input; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/core/DefinedTest.java ================================================ package io.pebbletemplates.pebble.extension.core; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.util.Map; /** * Implementation for the test function 'defined'. * * Inversion of 'null' test function to provide better compatibility with the original twig version * and JTwig. * * @author Thomas Hunziker */ public class DefinedTest extends NullTest { @Override public boolean apply(Object input, Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { return !super.apply(input, args, self, context, lineNumber); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/core/DisallowExtensionCustomizerBuilder.java ================================================ package io.pebbletemplates.pebble.extension.core; import io.pebbletemplates.pebble.extension.Extension; import io.pebbletemplates.pebble.extension.ExtensionCustomizer; import io.pebbletemplates.pebble.extension.Filter; import io.pebbletemplates.pebble.extension.Test; import io.pebbletemplates.pebble.operator.BinaryOperator; import io.pebbletemplates.pebble.operator.UnaryOperator; import io.pebbletemplates.pebble.tokenParser.TokenParser; import java.util.*; import java.util.function.Function; import java.util.function.Supplier; /** * @author freshchen * @since 2024/3/2 */ public class DisallowExtensionCustomizerBuilder { private Collection disallowedFilterKeys; private Collection disallowedTokenParserTags; private Collection disallowedFunctionKeys; private Collection disallowedBinaryOperatorSymbols; private Collection disallowedUnaryOperatorSymbols; private Collection disallowedTestKeys; public DisallowExtensionCustomizerBuilder disallowedFunctionKeys(Collection disallowedFunctionKeys) { this.disallowedFunctionKeys = disallowedFunctionKeys; return this; } public DisallowExtensionCustomizerBuilder disallowedTokenParserTags(Collection disallowedTokenParserTags) { this.disallowedTokenParserTags = disallowedTokenParserTags; return this; } public DisallowExtensionCustomizerBuilder disallowedFilterKeys(Collection disallowedFilterKeys) { this.disallowedFilterKeys = disallowedFilterKeys; return this; } public DisallowExtensionCustomizerBuilder disallowedUnaryOperatorSymbols(Collection disallowedUnaryOperatorSymbols) { this.disallowedUnaryOperatorSymbols = disallowedUnaryOperatorSymbols; return this; } public DisallowExtensionCustomizerBuilder disallowedBinaryOperatorSymbols(Collection disallowedBinaryOperatorSymbols) { this.disallowedBinaryOperatorSymbols = disallowedBinaryOperatorSymbols; return this; } public DisallowExtensionCustomizerBuilder disallowedTestKeys(Collection disallowedTestKeys) { this.disallowedTestKeys = disallowedTestKeys; return this; } public Function build() { return extension -> new ExtensionCustomizer(extension) { @Override public Map getTests() { return this.disallow(super::getTests, DisallowExtensionCustomizerBuilder.this.disallowedTestKeys); } @Override public List getUnaryOperators() { return this.disallow(super::getUnaryOperators, DisallowExtensionCustomizerBuilder.this.disallowedUnaryOperatorSymbols, UnaryOperator::getSymbol); } @Override public List getBinaryOperators() { return this.disallow(super::getBinaryOperators, DisallowExtensionCustomizerBuilder.this.disallowedBinaryOperatorSymbols, BinaryOperator::getSymbol); } @Override public Map getFunctions() { return this.disallow(super::getFunctions, DisallowExtensionCustomizerBuilder.this.disallowedFunctionKeys); } @Override public Map getFilters() { return this.disallow(super::getFilters, DisallowExtensionCustomizerBuilder.this.disallowedFilterKeys); } @Override public List getTokenParsers() { return this.disallow(super::getTokenParsers, DisallowExtensionCustomizerBuilder.this.disallowedTokenParserTags, TokenParser::getTag); } private List disallow(Supplier> superGetter, Collection disallowedList, Function keyGetter) { List superList = superGetter.get(); if (disallowedList == null || disallowedList.isEmpty()) { return superList; } List result = Optional.ofNullable(superList).map(ArrayList::new) .orElseGet(ArrayList::new); disallowedList.stream() .filter(Objects::nonNull) .forEach(v -> result.removeIf(t -> v.equals(keyGetter.apply(t)))); return result; } private Map disallow(Supplier> superGetter, Collection disallowedList) { Map superMap = superGetter.get(); if (disallowedList == null || disallowedList.isEmpty()) { return superMap; } Map result = Optional.ofNullable(superMap).map(HashMap::new) .orElseGet(HashMap::new); disallowedList.stream() .filter(Objects::nonNull) .forEach(result::remove); return result; } }; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/core/EmptyTest.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.extension.core; import io.pebbletemplates.pebble.extension.Test; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.util.Collection; import java.util.List; import java.util.Map; public class EmptyTest implements Test { @Override public List getArgumentNames() { return null; } @Override public boolean apply(Object input, Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { boolean isEmpty = input == null; if (!isEmpty && input instanceof String) { String value = (String) input; isEmpty = "".equals(value.trim()); } if (!isEmpty && input instanceof Collection) { isEmpty = ((Collection) input).isEmpty(); } if (!isEmpty && input instanceof Map) { isEmpty = ((Map) input).isEmpty(); } return isEmpty; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/core/EvenTest.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.extension.core; import io.pebbletemplates.pebble.extension.Test; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.util.List; import java.util.Map; public class EvenTest implements Test { @Override public List getArgumentNames() { return null; } @Override public boolean apply(Object input, Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) throws PebbleException { if (input == null) { throw new PebbleException(null, "Can not pass null value to \"even\" test.", lineNumber, self.getName()); } if (input instanceof Integer) { return ((Integer) input) % 2 == 0; } else { return ((Long) input) % 2 == 0; } } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/core/FirstFilter.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.extension.core; import io.pebbletemplates.pebble.extension.Filter; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.lang.reflect.Array; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Map; /** * Returns the first element of a collection * * @author mbosecke */ public class FirstFilter implements Filter { @Override public List getArgumentNames() { return null; } @Override public Object apply(Object input, Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { if (input == null) { return null; } if (input instanceof String) { String inputString = (String) input; return inputString.charAt(0); } if (input.getClass().isArray()) { int length = Array.getLength(input); return length > 0 ? Array.get(input, 0) : null; } Collection inputCollection = (Collection) input; Iterator iterator = inputCollection.iterator(); if (iterator.hasNext()) { return iterator.next(); } return null; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/core/FormatFilter.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.extension.core; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.extension.Filter; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.stream.IntStream; public class FormatFilter implements Filter { private final List argumentNames = new ArrayList<>(); public FormatFilter() { } @Override public List getArgumentNames() { return this.argumentNames; } @Override public Object apply(Object input, Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) throws PebbleException { if (input == null) { return null; } Object[] formatArgs = IntStream.range(0, args.size()) .mapToObj(i -> args.get(String.valueOf(i))) .toArray(); return String.format(input.toString(), formatArgs); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/core/IterableTest.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.extension.core; import io.pebbletemplates.pebble.extension.Test; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.util.List; import java.util.Map; public class IterableTest implements Test { @Override public List getArgumentNames() { return null; } @Override public boolean apply(Object input, Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { return input instanceof Iterable || input instanceof Object[]; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/core/JoinFilter.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.extension.core; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.extension.Filter; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.lang.reflect.Array; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; /** * Concatenates all entries of a collection, optionally glued together with a particular character * such as a comma. * * @author mbosecke */ public class JoinFilter implements Filter { private final List argumentNames = new ArrayList<>(); public JoinFilter() { this.argumentNames.add("separator"); } @Override public List getArgumentNames() { return this.argumentNames; } @Override public Object apply(Object input, Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) throws PebbleException { if (input == null) { return null; } String glue = null; if (args.containsKey("separator")) { glue = (String) args.get("separator"); } if (input.getClass().isArray()) { List items = new ArrayList<>(); int length = Array.getLength(input); for (int i = 0; i < length; i++) { items.add(Array.get(input, i)); } return this.join(items, glue); } else if (input instanceof Collection) { return this.join((Collection) input, glue); } else { throw new PebbleException(null, "The 'join' filter expects that the input is either a collection or an array.", lineNumber, self.getName()); } } private String join(Collection inputCollection, String glue) { StringBuilder builder = new StringBuilder(); boolean isFirst = true; for (Object entry : inputCollection) { if (!isFirst && glue != null) { builder.append(glue); } builder.append(entry); isFirst = false; } return builder.toString(); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/core/LastFilter.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.extension.core; import io.pebbletemplates.pebble.extension.Filter; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.lang.reflect.Array; import java.util.Collection; import java.util.List; import java.util.Map; /** * Returns the last element of a collection * * @author mbosecke */ public class LastFilter implements Filter { @Override public List getArgumentNames() { return null; } @Override public Object apply(Object input, Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { if (input == null) { return null; } if (input instanceof String) { String inputString = (String) input; return inputString.charAt(inputString.length() - 1); } if (input.getClass().isArray()) { int length = Array.getLength(input); return length > 0 ? Array.get(input, length - 1) : null; } Collection inputCollection = (Collection) input; Object result = null; for (Object o : inputCollection) { result = o; } return result; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/core/LengthFilter.java ================================================ package io.pebbletemplates.pebble.extension.core; import io.pebbletemplates.pebble.extension.Filter; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.lang.reflect.Array; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Map; public class LengthFilter implements Filter { @Override public List getArgumentNames() { return null; } @Override public Object apply(Object input, Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { if (input == null) { return 0; } if (input instanceof String) { return ((String) input).length(); } else if (input instanceof Collection) { return ((Collection) input).size(); } else if (input.getClass().isArray()) { return Array.getLength(input); } else if (input instanceof Map) { return ((Map) input).size(); } else if (input instanceof Iterable) { Iterator it = ((Iterable) input).iterator(); int size = 0; while (it.hasNext()) { it.next(); size++; } return size; } else if (input instanceof Iterator) { Iterator it = (Iterator) input; int size = 0; while (it.hasNext()) { it.next(); size++; } return size; } else { return 0; } } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/core/LowerFilter.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.extension.core; import io.pebbletemplates.pebble.extension.Filter; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.util.List; import java.util.Map; public class LowerFilter implements Filter { @Override public List getArgumentNames() { return null; } @Override public Object apply(Object input, Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { if (input == null) { return null; } if (input instanceof String) { return ((String) input).toLowerCase(context.getLocale()); } else { return input.toString().toLowerCase(context.getLocale()); } } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/core/MacroAndBlockRegistrantNodeVisitor.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.extension.core; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.extension.AbstractNodeVisitor; import io.pebbletemplates.pebble.node.BlockNode; import io.pebbletemplates.pebble.node.MacroNode; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; public class MacroAndBlockRegistrantNodeVisitor extends AbstractNodeVisitor { public MacroAndBlockRegistrantNodeVisitor(PebbleTemplateImpl template) { super(template); } @Override public void visit(BlockNode node) { this.getTemplate().registerBlock(node.getBlock()); super.visit(node); } @Override public void visit(MacroNode node) { try { this.getTemplate().registerMacro(node.getMacro()); } catch (PebbleException e) { throw new RuntimeException(e); } super.visit(node); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/core/MacroAndBlockRegistrantNodeVisitorFactory.java ================================================ package io.pebbletemplates.pebble.extension.core; import io.pebbletemplates.pebble.extension.NodeVisitor; import io.pebbletemplates.pebble.extension.NodeVisitorFactory; import io.pebbletemplates.pebble.template.PebbleTemplate; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; /** * Implementation of {@link NodeVisitorFactory} to handle {@link MacroAndBlockRegistrantNodeVisitor}. * * @author hunziker */ public class MacroAndBlockRegistrantNodeVisitorFactory implements NodeVisitorFactory { @Override public NodeVisitor createVisitor(PebbleTemplate template) { return new MacroAndBlockRegistrantNodeVisitor((PebbleTemplateImpl) template); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/core/MapTest.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.extension.core; import io.pebbletemplates.pebble.extension.Test; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.util.List; import java.util.Map; public class MapTest implements Test { @Override public List getArgumentNames() { return null; } @Override public boolean apply(Object input, Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { return input instanceof Map; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/core/MaxFunction.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.extension.core; import io.pebbletemplates.pebble.extension.Function; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import io.pebbletemplates.pebble.utils.OperatorUtils; import java.util.List; import java.util.Map; public class MaxFunction implements Function { @Override public List getArgumentNames() { return null; } @Override public Object execute(Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { Object min = null; int i = 0; while (args.containsKey(String.valueOf(i))) { Object candidate = args.get(String.valueOf(i)); i++; if (min == null) { min = candidate; continue; } if (OperatorUtils.gt(candidate, min)) { min = candidate; } } return min; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/core/MergeFilter.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.extension.core; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.extension.Filter; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.lang.reflect.Array; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; public class MergeFilter implements Filter { public static final String FILTER_NAME = "merge"; private final List argumentNames = new ArrayList<>(); public MergeFilter() { this.argumentNames.add("items"); } @Override public List getArgumentNames() { return this.argumentNames; } @Override public Object apply(Object input, Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) throws PebbleException { Object items = args.get("items"); if (input == null) { if (items == null) { throw new PebbleException(null, "The two arguments to be merged are null", lineNumber, self.getName()); } else { return items; } } else if (items == null) { return input; } // left hand side argument defines resulting type if (input instanceof Map) { return this.mergeAsMap((Map) input, items); } else if (input instanceof List) { return this.mergeAsList((List) input, items, lineNumber, self); } else if (input.getClass().isArray()) { return this.mergeAsArray(input, items, lineNumber, self); } else { throw new PebbleException(null, "The object being filtered is not a Map/List/Array", lineNumber, self.getName()); } } private Object mergeAsMap(Map arg1, Object arg2) { Map output; if (arg2 instanceof Map) { Map collection2 = (Map) arg2; output = new HashMap<>(arg1.size() + collection2.size() + 16); output.putAll(arg1); output.putAll(collection2); } else if (arg2 instanceof List) { List collection2 = (List) arg2; output = new HashMap<>(arg1.size() + collection2.size() + 16); output.putAll(arg1); for (Object o : collection2) { output.put(o, o); } } else { throw new UnsupportedOperationException( "Currently, only Maps and Lists can be merged with a Map. Arg2: " + arg2.getClass() .getName()); } return output; } private Object mergeAsList(List arg1, Object arg2, int lineNumber, PebbleTemplate self) throws PebbleException { List output; if (arg2 instanceof Map) { Map collection2 = (Map) arg2; output = new ArrayList<>(arg1.size() + collection2.size() + 16); output.addAll(arg1); output.addAll(collection2.entrySet()); } else if (arg2 instanceof List) { List collection2 = (List) arg2; output = new ArrayList<>(arg1.size() + collection2.size() + 16); output.addAll(arg1); output.addAll(collection2); } else { throw new PebbleException(null, "Currently, only Maps and Lists can be merged with a List. Arg2: " + arg2.getClass() .getName(), lineNumber, self.getName()); } return output; } private Object mergeAsArray(Object arg1, Object arg2, int lineNumber, PebbleTemplate self) throws PebbleException { Class arg1Class = arg1.getClass().getComponentType(); Class arg2Class = arg2.getClass().getComponentType(); if (!arg1Class.equals(arg2Class)) { throw new PebbleException(null, "Currently, only Arrays of the same component class can be merged. Arg1: " + arg1Class .getName() + ", Arg2: " + arg2Class.getName(), lineNumber, self.getName()); } Object output = Array.newInstance(arg1Class, Array.getLength(arg1) + Array.getLength(arg2)); System.arraycopy(arg1, 0, output, 0, Array.getLength(arg1)); System.arraycopy(arg2, 0, output, Array.getLength(arg1), Array.getLength(arg2)); return output; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/core/MinFunction.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.extension.core; import io.pebbletemplates.pebble.extension.Function; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import io.pebbletemplates.pebble.utils.OperatorUtils; import java.util.List; import java.util.Map; public class MinFunction implements Function { @Override public List getArgumentNames() { return null; } @Override public Object execute(Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { Object min = null; int i = 0; while (args.containsKey(String.valueOf(i))) { Object candidate = args.get(String.valueOf(i)); i++; if (min == null) { min = candidate; continue; } if (OperatorUtils.lt(candidate, min)) { min = candidate; } } return min; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/core/Nl2brFilter.java ================================================ package io.pebbletemplates.pebble.extension.core; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.extension.Filter; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.util.List; import java.util.Map; public class Nl2brFilter implements Filter { public static final String FILTER_NAME = "nl2br"; @Override public Object apply(Object input, Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) throws PebbleException { if (input == null) { return null; } if (!(input instanceof String)) { throw new IllegalArgumentException("nl2br filters only supports String input."); } String strInput = (String) input; if (strInput.indexOf('\n') == -1 && strInput.indexOf('\r') == -1) { return strInput; } // Pre-size the StringBuilder to be the input length + 16 (to account for some extra
tags) // The 16 is the default size of the StringBuilders default constructor (new StringBuilder()) StringBuilder sb = new StringBuilder(strInput.length() + 16); return convertNewlinesToBr(strInput, sb); } @Override public List getArgumentNames() { return null; } private String convertNewlinesToBr(String input, StringBuilder sb) { final int len = input.length(); for (int i = 0; i < len; i++) { char c = input.charAt(i); if (c == '\r') { // Convert CR (possibly part of CRLF) to
sb.append("
"); // Skip a following LF to avoid double converting CRLF if (i + 1 < len && input.charAt(i + 1) == '\n') { i++; // skip the '\n' } } else if (c == '\n') { // Lone LF sb.append("
"); } else { sb.append(c); } } return sb.toString(); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/core/NullTest.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.extension.core; import io.pebbletemplates.pebble.extension.Test; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.util.List; import java.util.Map; public class NullTest implements Test { @Override public List getArgumentNames() { return null; } @Override public boolean apply(Object input, Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { return input == null; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/core/NumberFormatFilter.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.extension.core; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.extension.Filter; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.text.Format; import java.text.NumberFormat; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Map; public class NumberFormatFilter implements Filter { private final List argumentNames = new ArrayList<>(); public NumberFormatFilter() { this.argumentNames.add("format"); } @Override public List getArgumentNames() { return this.argumentNames; } @Override public Object apply(Object input, Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) throws PebbleException { if (input == null) { return null; } if (!(input instanceof Number)) { throw new PebbleException(null, "The input for the 'NumberFormat' filter has to be a number.", lineNumber, self.getName()); } Number number = (Number) input; Locale locale = context.getLocale(); if (args.get("format") != null) { Format format = new DecimalFormat((String) args.get("format"), new DecimalFormatSymbols(locale)); return format.format(number); } else { NumberFormat numberFormat = NumberFormat.getInstance(locale); return numberFormat.format(number); } } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/core/OddTest.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.extension.core; import io.pebbletemplates.pebble.extension.Test; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.util.List; import java.util.Map; public class OddTest implements Test { @Override public List getArgumentNames() { return null; } @Override public boolean apply(Object input, Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) throws PebbleException { if (input == null) { throw new IllegalArgumentException("Can not pass null value to \"odd\" test."); } EvenTest evenTest = new EvenTest(); return !evenTest.apply(input, args, self, context, lineNumber); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/core/RangeFunction.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.extension.core; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.extension.Function; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.util.ArrayList; import java.util.List; import java.util.Map; /** * Range function to iterate over long or a string with a length of 1. * * @author Eric Bussieres */ public class RangeFunction implements Function { public static final String FUNCTION_NAME = "range"; private static final String PARAM_END = "end"; private static final String PARAM_INCREMENT = "increment"; private static final String PARAM_START = "start"; private final List argumentNames = new ArrayList<>(); public RangeFunction() { this.argumentNames.add(PARAM_START); this.argumentNames.add(PARAM_END); this.argumentNames.add(PARAM_INCREMENT); } @Override public Object execute(Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) throws PebbleException { Object start = args.get(PARAM_START); Object end = args.get(PARAM_END); Object increment = args.get(PARAM_INCREMENT); if (increment == null) { increment = 1L; } else if (!(increment instanceof Number)) { throw new PebbleException(null, "The increment of the range function must be a number " + increment, lineNumber, self.getName()); } long incrementNum = ((Number) increment).longValue(); List results = new ArrayList<>(); // Iterating over Number if (start instanceof Number && end instanceof Number) { long startNum = ((Number) start).longValue(); long endNum = ((Number) end).longValue(); if (incrementNum > 0) { for (long i = startNum; i <= endNum; i += incrementNum) { results.add(i); } } else if (incrementNum < 0) { for (long i = startNum; i >= endNum; i += incrementNum) { results.add(i); } } else { throw new PebbleException(null, "The increment of the range function must be different than 0", lineNumber, self.getName()); } } // Iterating over character else if (start instanceof String && end instanceof String) { String startStr = (String) start; String endStr = (String) end; if (startStr.length() != 1 || endStr.length() != 1) { throw new PebbleException(null, "Arguments of range function must be of type Number or String with " + "a length of 1", lineNumber, self.getName()); } char startChar = startStr.charAt(0); char endChar = endStr.charAt(0); if (incrementNum > 0) { for (int i = startChar; i <= endChar; i += incrementNum) { results.add((char) i); } } else if (incrementNum < 0) { for (int i = startChar; i >= endChar; i += incrementNum) { results.add((char) i); } } else { throw new PebbleException(null, "The increment of the range function must be different than 0", lineNumber, self.getName()); } } else { throw new PebbleException(null, "Arguments of range function must be of type Number or String with a " + "length of 1", lineNumber, self.getName()); } return results; } @Override public List getArgumentNames() { return this.argumentNames; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/core/ReplaceFilter.java ================================================ package io.pebbletemplates.pebble.extension.core; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.extension.Filter; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.text.MessageFormat; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Map.Entry; /** * This class implements the 'replace' filter. * * @author Thomas Hunziker */ public class ReplaceFilter implements Filter { public static final String FILTER_NAME = "replace"; private static final String ARGUMENT_NAME = "replace_pairs"; private final static List ARGS = Collections.singletonList(ARGUMENT_NAME); @Override public List getArgumentNames() { return ARGS; } @Override public Object apply(Object input, Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) throws PebbleException { if (input == null) { return null; } if (args.get(ARGUMENT_NAME) == null) { throw new PebbleException(null, MessageFormat.format("The argument ''{0}'' is required.", ARGUMENT_NAME), lineNumber, self.getName()); } Map replacePair = (Map) args.get(ARGUMENT_NAME); String data = input.toString(); for (Entry entry : replacePair.entrySet()) { data = data.replace(entry.getKey().toString(), entry.getValue().toString()); } return data; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/core/ReverseFilter.java ================================================ package io.pebbletemplates.pebble.extension.core; import io.pebbletemplates.pebble.extension.Filter; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.util.Collections; import java.util.List; import java.util.Map; /** * Revert the order of an input list * * @author Andrea La Scola */ public class ReverseFilter implements Filter { @Override public List getArgumentNames() { return null; } @SuppressWarnings({"rawtypes", "unchecked"}) @Override public Object apply(Object input, Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { if (input == null) { return null; } List collection = (List) input; Collections.reverse(collection); return collection; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/core/RsortFilter.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.extension.core; import io.pebbletemplates.pebble.extension.Filter; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.util.Collections; import java.util.List; import java.util.Map; /** * Sort list items in the reverse order * * @author Barakat Soror */ public class RsortFilter implements Filter { @Override public List getArgumentNames() { return null; } @SuppressWarnings({"rawtypes", "unchecked"}) @Override public List apply(Object input, Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { if (input == null) { return null; } List collection = (List) input; collection.sort(Collections.reverseOrder()); return collection; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/core/Sha256Filter.java ================================================ package io.pebbletemplates.pebble.extension.core; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.extension.Filter; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.util.List; import java.util.Map; /** * This class implements the 'sha256' filter. * * @author Silviu Vergoti */ public class Sha256Filter implements Filter { public static final String FILTER_NAME = "sha256"; @Override public List getArgumentNames() { return null; } @Override public Object apply(Object input, Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) throws PebbleException { if (input == null) { return null; } if (input instanceof String) { MessageDigest digest = null; byte[] encodedHash = null; try { digest = MessageDigest.getInstance("SHA-256"); encodedHash = digest.digest(((String) input).getBytes(StandardCharsets.UTF_8)); } catch (Exception e) { throw new PebbleException(e, "Hashing exception encountered\n", lineNumber, self.getName()); } return bytesToHex(encodedHash); } else { throw new PebbleException(null, "Need a string to hash\n", lineNumber, self.getName()); } } private static String bytesToHex(byte[] bytes) { StringBuilder hexString = new StringBuilder(2 * bytes.length); for (int i = 0; i < bytes.length; i++) { String hex = Integer.toHexString(0xff & bytes[i]); if (hex.length() == 1) { hexString.append('0'); } hexString.append(hex); } return hexString.toString(); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/core/SliceFilter.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.extension.core; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.extension.Filter; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.lang.reflect.Array; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; public class SliceFilter implements Filter { private final List argumentNames = new ArrayList<>(); public SliceFilter() { this.argumentNames.add("fromIndex"); this.argumentNames.add("toIndex"); } @Override public List getArgumentNames() { return this.argumentNames; } @Override public Object apply(Object input, Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) throws PebbleException { if (input == null) { return null; } // argument parsing Object argFrom = args.get("fromIndex"); if (argFrom == null) { // defaults to 0 argFrom = 0; } else if (!(argFrom instanceof Number)) { throw new PebbleException(null, "Argument fromIndex must be a number. Actual type: " + argFrom.getClass().getName(), lineNumber, self.getName()); } int from = ((Number) argFrom).intValue(); if (from < 0) { throw new IllegalArgumentException("fromIndex must be greater than 0"); } Object argTo = args.get("toIndex"); if (argTo == null) { // defaults to input length // argTo == null; } else if (!(argTo instanceof Number)) { throw new PebbleException(null, "Argument toIndex must be a number. Actual type: " + argTo.getClass().getName(), lineNumber, self.getName()); } int length; if (input instanceof List) { length = ((List) input).size(); } else if (input.getClass().isArray()) { length = Array.getLength(input); } else if (input instanceof String) { length = ((String) input).length(); } else { throw new PebbleException(null, "Slice filter can only be applied to String, List and array inputs. Actual type was: " + input.getClass().getName(), lineNumber, self.getName()); } int to; if (argTo != null) { to = ((Number) argTo).intValue(); if (to > length) { throw new PebbleException(null, "toIndex must be smaller than input size: " + length, lineNumber, self.getName()); } else if (from >= to) { throw new PebbleException(null, "toIndex must be greater than fromIndex", lineNumber, self.getName()); } } else { to = length; } // slice input if (input instanceof List) { List value = (List) input; // FIXME maybe sublist() is not the best option due to its // implementation? return value.subList(from, to); } else if (input.getClass().isArray()) { return sliceArray(input, from, to); } else { String value = (String) input; return value.substring(from, to); } } private static Object sliceArray(Object input, int from, int to) { if (input instanceof Object[]) { return Arrays.copyOfRange((Object[]) input, from, to); } else if (input instanceof boolean[]) { return Arrays.copyOfRange((boolean[]) input, from, to); } else if (input instanceof byte[]) { return Arrays.copyOfRange((byte[]) input, from, to); } else if (input instanceof char[]) { return Arrays.copyOfRange((char[]) input, from, to); } else if (input instanceof double[]) { return Arrays.copyOfRange((double[]) input, from, to); } else if (input instanceof float[]) { return Arrays.copyOfRange((float[]) input, from, to); } else if (input instanceof int[]) { return Arrays.copyOfRange((int[]) input, from, to); } else if (input instanceof long[]) { return Arrays.copyOfRange((long[]) input, from, to); } else { return Arrays.copyOfRange((short[]) input, from, to); } } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/core/SortFilter.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.extension.core; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.extension.Filter; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; public class SortFilter implements Filter { @Override public List getArgumentNames() { return null; } @SuppressWarnings({"rawtypes", "unchecked"}) @Override public List apply(Object input, Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { if (input == null) { return null; } List collection; if (input instanceof List) { collection = (List) input; } else if (input instanceof Comparable[]) { collection = Arrays.asList((Comparable[]) input); } else { throw new PebbleException(null, "Unsupported input type for sort filter", lineNumber, self.getName()); } Collections.sort(collection); return collection; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/core/SplitFilter.java ================================================ package io.pebbletemplates.pebble.extension.core; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.extension.Filter; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.util.ArrayList; import java.util.List; import java.util.Map; public class SplitFilter implements Filter { public static final String FILTER_NAME = "split"; private static final String ARGUMENT_NAME_DELIMITER = "delimiter"; private static final String ARGUMENT_NAME_LIMIT = "limit"; private final List argumentNames = new ArrayList<>(); public SplitFilter() { this.argumentNames.add(ARGUMENT_NAME_DELIMITER); this.argumentNames.add(ARGUMENT_NAME_LIMIT); } @Override public Object apply(Object input, Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) throws PebbleException { if (input == null) { return null; } String delimiter = (String) args.get(ARGUMENT_NAME_DELIMITER); Number limit = (Number) args.get(ARGUMENT_NAME_LIMIT); if (delimiter == null) { throw new PebbleException(null, "missing delimiter parameter in split filter", lineNumber, self.getName()); } if (limit == null) { return ((String) input).split(delimiter); } return ((String) input).split(delimiter, limit.intValue()); } @Override public List getArgumentNames() { return this.argumentNames; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/core/TitleFilter.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.extension.core; import io.pebbletemplates.pebble.extension.Filter; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.util.List; import java.util.Map; public class TitleFilter implements Filter { @Override public List getArgumentNames() { return null; } @Override public Object apply(Object input, Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { if (input == null) { return null; } String value = (String) input; if (value.length() == 0) { return value; } StringBuilder result = new StringBuilder(); boolean capitalizeNextCharacter = true; for (char c : value.toCharArray()) { if (Character.isWhitespace(c)) { capitalizeNextCharacter = true; } else if (capitalizeNextCharacter) { c = Character.toTitleCase(c); capitalizeNextCharacter = false; } result.append(c); } return result.toString(); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/core/TrimFilter.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.extension.core; import io.pebbletemplates.pebble.extension.Filter; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.util.List; import java.util.Map; public class TrimFilter implements Filter { @Override public List getArgumentNames() { return null; } @Override public Object apply(Object input, Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { if (input == null) { return null; } String str = (String) input; return str.trim(); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/core/UpperFilter.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.extension.core; import io.pebbletemplates.pebble.extension.Filter; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.util.List; import java.util.Map; public class UpperFilter implements Filter { @Override public List getArgumentNames() { return null; } @Override public Object apply(Object input, Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { if (input == null) { return null; } if (input instanceof String) { return ((String) input).toUpperCase(context.getLocale()); } else { return input.toString().toUpperCase(context.getLocale()); } } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/core/UrlEncoderFilter.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.extension.core; import io.pebbletemplates.pebble.extension.Filter; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.List; import java.util.Map; public class UrlEncoderFilter implements Filter { @Override public List getArgumentNames() { return null; } @Override public Object apply(Object input, Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { if (input == null) { return null; } String arg = (String) input; try { arg = URLEncoder.encode(arg, "UTF-8"); } catch (UnsupportedEncodingException e) { } return arg; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/debug/DebugExtension.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.extension.debug; import io.pebbletemplates.pebble.extension.AbstractExtension; import io.pebbletemplates.pebble.extension.NodeVisitorFactory; import java.util.ArrayList; import java.util.List; public class DebugExtension extends AbstractExtension { private final PrettyPrintNodeVisitorFactory prettyPrinter = new PrettyPrintNodeVisitorFactory(); public List getNodeVisitors() { List visitors = new ArrayList<>(); visitors.add(this.prettyPrinter); return visitors; } public String toString() { return this.prettyPrinter.toString(); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/debug/PrettyPrintNodeVisitor.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.extension.debug; import io.pebbletemplates.pebble.extension.AbstractNodeVisitor; import io.pebbletemplates.pebble.node.ArgumentsNode; import io.pebbletemplates.pebble.node.BlockNode; import io.pebbletemplates.pebble.node.BodyNode; import io.pebbletemplates.pebble.node.FlushNode; import io.pebbletemplates.pebble.node.ForNode; import io.pebbletemplates.pebble.node.IfNode; import io.pebbletemplates.pebble.node.ImportNode; import io.pebbletemplates.pebble.node.IncludeNode; import io.pebbletemplates.pebble.node.NamedArgumentNode; import io.pebbletemplates.pebble.node.Node; import io.pebbletemplates.pebble.node.ParallelNode; import io.pebbletemplates.pebble.node.PrintNode; import io.pebbletemplates.pebble.node.RootNode; import io.pebbletemplates.pebble.node.SetNode; import io.pebbletemplates.pebble.node.TestInvocationExpression; import io.pebbletemplates.pebble.node.TextNode; import io.pebbletemplates.pebble.node.expression.BinaryExpression; import io.pebbletemplates.pebble.node.expression.ContextVariableExpression; import io.pebbletemplates.pebble.node.expression.FilterInvocationExpression; import io.pebbletemplates.pebble.node.expression.FunctionOrMacroInvocationExpression; import io.pebbletemplates.pebble.node.expression.GetAttributeExpression; import io.pebbletemplates.pebble.node.expression.ParentFunctionExpression; import io.pebbletemplates.pebble.node.expression.TernaryExpression; import io.pebbletemplates.pebble.node.expression.UnaryExpression; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; public class PrettyPrintNodeVisitor extends AbstractNodeVisitor { public PrettyPrintNodeVisitor(PebbleTemplateImpl template) { super(template); } private StringBuilder output = new StringBuilder(); private int level = 0; private void write(String message) { for (int i = 0; i < this.level - 1; i++) { this.output.append("| "); } if (this.level > 0) { this.output.append("|-"); } this.output.append(message.toUpperCase()).append("\n"); } public String toString() { return this.output.toString(); } /** * Default method used for unknown nodes such as nodes from a user provided extension. */ @Override public void visit(Node node) { this.write("unknown"); this.level++; super.visit(node); this.level--; } @Override public void visit(BodyNode node) { this.write("body"); this.level++; super.visit(node); this.level--; } @Override public void visit(IfNode node) { this.write("if"); this.level++; super.visit(node); this.level--; } @Override public void visit(ForNode node) { this.write("for"); this.level++; super.visit(node); this.level--; } public void visit(BinaryExpression node) { this.write("binary"); this.level++; super.visit(node); this.level--; } public void visit(UnaryExpression node) { this.write("unary"); this.level++; super.visit(node); this.level--; } public void visit(ContextVariableExpression node) { this.write(String.format("context variable [%s]", node.getName())); this.level++; super.visit(node); this.level--; } public void visit(FilterInvocationExpression node) { this.write("filter"); this.level++; super.visit(node); this.level--; } public void visit(FunctionOrMacroInvocationExpression node) { this.write("function or macro"); this.level++; super.visit(node); this.level--; } public void visit(GetAttributeExpression node) { this.write("get attribute"); this.level++; super.visit(node); this.level--; } @Override public void visit(NamedArgumentNode node) { this.write("named argument"); this.level++; super.visit(node); this.level--; } @Override public void visit(ArgumentsNode node) { this.write("named arguments"); this.level++; super.visit(node); this.level--; } public void visit(ParentFunctionExpression node) { this.write("parent function"); this.level++; super.visit(node); this.level--; } public void visit(TernaryExpression node) { this.write("ternary"); this.level++; super.visit(node); this.level--; } public void visit(TestInvocationExpression node) { this.write("test"); this.level++; super.visit(node); this.level--; } @Override public void visit(BlockNode node) { this.write(String.format("block [%s]", node.getName())); this.level++; super.visit(node); this.level--; } @Override public void visit(FlushNode node) { this.write("flush"); this.level++; super.visit(node); this.level--; } @Override public void visit(ImportNode node) { this.write("import"); this.level++; super.visit(node); this.level--; } @Override public void visit(IncludeNode node) { this.write("include"); this.level++; super.visit(node); this.level--; } @Override public void visit(ParallelNode node) { this.write("parallel"); this.level++; super.visit(node); this.level--; } @Override public void visit(PrintNode node) { this.write("print"); this.level++; super.visit(node); this.level--; } @Override public void visit(RootNode node) { this.write("root"); this.level++; super.visit(node); this.level--; } @Override public void visit(SetNode node) { this.write("set"); this.level++; super.visit(node); this.level--; } @Override public void visit(TextNode node) { String text = new String(node.getData()); String preview = text.length() > 10 ? text.substring(0, 10) + "..." : text; this.write(String.format("text [%s]", preview)); this.level++; super.visit(node); this.level--; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/debug/PrettyPrintNodeVisitorFactory.java ================================================ package io.pebbletemplates.pebble.extension.debug; import io.pebbletemplates.pebble.extension.NodeVisitor; import io.pebbletemplates.pebble.extension.NodeVisitorFactory; import io.pebbletemplates.pebble.template.PebbleTemplate; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; /** * Implementation of {@link NodeVisitorFactory} to create {@link PrettyPrintNodeVisitor}. */ public class PrettyPrintNodeVisitorFactory implements NodeVisitorFactory { @Override public NodeVisitor createVisitor(PebbleTemplate template) { return new PrettyPrintNodeVisitor((PebbleTemplateImpl) template); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/escaper/EscapeFilter.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.extension.escaper; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.extension.Filter; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import io.pebbletemplates.pebble.utils.StringUtils; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.unbescape.css.CssEscape; import org.unbescape.html.HtmlEscape; import org.unbescape.javascript.JavaScriptEscape; import org.unbescape.json.JsonEscape; import org.unbescape.uri.UriEscape; public class EscapeFilter implements Filter { public static final String HTML_ESCAPE_STRATEGY = "html"; public static final String JAVASCRIPT_ESCAPE_STRATEGY = "js"; public static final String CSS_ESCAPE_STRATEGY = "css"; public static final String URL_PARAM_ESCAPE_STRATEGY = "url_param"; public static final String JSON_ESCAPE_STRATEGY = "json"; private String defaultStrategy = HTML_ESCAPE_STRATEGY; private final List argumentNames = new ArrayList<>(); private final Map strategies = new HashMap<>(); public EscapeFilter() { this.buildDefaultStrategies(); this.argumentNames.add("strategy"); } private void buildDefaultStrategies() { this.strategies.put(HTML_ESCAPE_STRATEGY, HtmlEscape::escapeHtml4Xml); this.strategies.put(JAVASCRIPT_ESCAPE_STRATEGY, JavaScriptEscape::escapeJavaScript); this.strategies.put(CSS_ESCAPE_STRATEGY, CssEscape::escapeCssIdentifier); this.strategies.put(URL_PARAM_ESCAPE_STRATEGY, UriEscape::escapeUriQueryParam); this.strategies.put(JSON_ESCAPE_STRATEGY, JsonEscape::escapeJson); } @Override public List getArgumentNames() { return this.argumentNames; } @Override public Object apply(Object inputObject, Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) throws PebbleException { if (inputObject == null || inputObject instanceof SafeString) { return inputObject; } String input = StringUtils.toString(inputObject); String strategy = this.defaultStrategy; if (args.get("strategy") != null) { strategy = (String) args.get("strategy"); } if (!this.strategies.containsKey(strategy)) { throw new PebbleException(null, String.format("Unknown escaping strategy [%s]", strategy), lineNumber, self.getName()); } return new SafeString(this.strategies.get(strategy).escape(input)); } public String getDefaultStrategy() { return this.defaultStrategy; } public void setDefaultStrategy(String defaultStrategy) { this.defaultStrategy = defaultStrategy; } public void addEscapingStrategy(String name, EscapingStrategy strategy) { this.strategies.put(name, strategy); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/escaper/EscaperExtension.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.extension.escaper; import io.pebbletemplates.pebble.extension.AbstractExtension; import io.pebbletemplates.pebble.extension.Filter; import io.pebbletemplates.pebble.extension.NodeVisitorFactory; import io.pebbletemplates.pebble.tokenParser.AutoEscapeTokenParser; import io.pebbletemplates.pebble.tokenParser.TokenParser; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; public class EscaperExtension extends AbstractExtension { private final EscapeFilter filter; private final EscaperNodeVisitorFactory visitorFactory; public EscaperExtension() { this.filter = new EscapeFilter(); this.visitorFactory = new EscaperNodeVisitorFactory(); } @Override public Map getFilters() { Map filters = new HashMap<>(); filters.put("escape", this.filter); filters.put("raw", new RawFilter()); return filters; } @Override public List getTokenParsers() { List parsers = new ArrayList<>(); parsers.add(new AutoEscapeTokenParser()); return parsers; } @Override public List getNodeVisitors() { List visitors = new ArrayList<>(); visitors.add(this.visitorFactory); return visitors; } /** * Sets the default escaping strategy. * * @param strategy Escaping strategy */ public void setDefaultStrategy(String strategy) { // TODO: This method is dangerous, because the state of the filter is // changed. When this is changed during the rendering of template this // can lead to unexpected results. this.filter.setDefaultStrategy(strategy); } public void setAutoEscaping(boolean auto) { this.visitorFactory.setAutoEscaping(auto); } /** * Adds a custom escaping strategy to the filter. * * @param name Name of the escaping strategy * @param strategy The implementation of the escaping strategy */ public void addEscapingStrategy(String name, EscapingStrategy strategy) { // TODO: This method is dangerous, because the state of the filter is // changed. When this is changed during the rendering of template this // can lead to unexpected results. this.filter.addEscapingStrategy(name, strategy); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/escaper/EscaperNodeVisitor.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.extension.escaper; import io.pebbletemplates.pebble.extension.AbstractNodeVisitor; import io.pebbletemplates.pebble.node.ArgumentsNode; import io.pebbletemplates.pebble.node.AutoEscapeNode; import io.pebbletemplates.pebble.node.NamedArgumentNode; import io.pebbletemplates.pebble.node.PrintNode; import io.pebbletemplates.pebble.node.expression.BlockFunctionExpression; import io.pebbletemplates.pebble.node.expression.ConcatenateExpression; import io.pebbletemplates.pebble.node.expression.Expression; import io.pebbletemplates.pebble.node.expression.FilterExpression; import io.pebbletemplates.pebble.node.expression.FilterInvocationExpression; import io.pebbletemplates.pebble.node.expression.LiteralStringExpression; import io.pebbletemplates.pebble.node.expression.ParentFunctionExpression; import io.pebbletemplates.pebble.node.expression.TernaryExpression; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; public class EscaperNodeVisitor extends AbstractNodeVisitor { private final LinkedList strategies = new LinkedList<>(); private final LinkedList active = new LinkedList<>(); public EscaperNodeVisitor(PebbleTemplateImpl template, boolean autoEscapting) { super(template); this.pushAutoEscapeState(autoEscapting); } @Override public void visit(PrintNode node) { Expression expression = node.getExpression(); if (expression instanceof TernaryExpression) { TernaryExpression ternary = (TernaryExpression) expression; Expression left = ternary.getExpression2(); Expression right = ternary.getExpression3(); if (this.isUnsafe(left)) { ternary.setExpression2(this.escape(left)); } if (this.isUnsafe(right)) { ternary.setExpression3(this.escape(right)); } } else { if (this.isUnsafe(expression)) { node.setExpression(this.escape(expression)); } } } @Override public void visit(AutoEscapeNode node) { this.active.push(node.isActive()); this.strategies.push(node.getStrategy()); node.getBody().accept(this); this.active.pop(); this.strategies.pop(); } /** * Simply wraps the input expression with a {@link EscapeFilter}. */ private Expression escape(Expression expression) { /* * Build the arguments to the escape filter. The arguments will just * include the strategy being used. */ List namedArgs = new ArrayList<>(); if (!this.strategies.isEmpty() && this.strategies.peek() != null) { String strategy = this.strategies.peek(); namedArgs.add(new NamedArgumentNode("strategy", new LiteralStringExpression(strategy, expression.getLineNumber()))); } ArgumentsNode args = new ArgumentsNode(null, namedArgs, expression.getLineNumber()); /* * Create the filter invocation with the newly created named arguments. */ FilterInvocationExpression filter = new FilterInvocationExpression("escape", args, expression.getLineNumber()); /* * The given expression and the filter invocation now become a binary * expression which is what is returned. */ FilterExpression binary = new FilterExpression(); binary.setLeft(expression); binary.setRight(filter); return binary; } private boolean isUnsafe(Expression expression) { // check whether the autoescaper is even active if (this.active.peek() == Boolean.FALSE) { return false; } boolean unsafe = true; // string literals are safe if (expression instanceof LiteralStringExpression) { unsafe = false; } else if (expression instanceof ParentFunctionExpression || expression instanceof BlockFunctionExpression) { unsafe = false; } else if (this.isSafeConcatenateExpr(expression)) { unsafe = false; } return unsafe; } /** * Returns true if {@code expr} is a {@link ConcatenateExpression} made up of two {@link * LiteralStringExpression}s. */ private boolean isSafeConcatenateExpr(Expression expr) { if (!(expr instanceof ConcatenateExpression)) { return false; } ConcatenateExpression cexpr = (ConcatenateExpression) expr; return cexpr.getLeftExpression() instanceof LiteralStringExpression && cexpr.getRightExpression() instanceof LiteralStringExpression; } public void pushAutoEscapeState(boolean auto) { this.active.push(auto); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/escaper/EscaperNodeVisitorFactory.java ================================================ package io.pebbletemplates.pebble.extension.escaper; import io.pebbletemplates.pebble.extension.NodeVisitor; import io.pebbletemplates.pebble.extension.NodeVisitorFactory; import io.pebbletemplates.pebble.template.PebbleTemplate; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; /** * Factory class for creating {@link EscaperNodeVisitor}. * * @author Thomas Hunziker */ public class EscaperNodeVisitorFactory implements NodeVisitorFactory { private boolean autoEscaping = true; @Override public NodeVisitor createVisitor(PebbleTemplate template) { return new EscaperNodeVisitor((PebbleTemplateImpl) template, this.autoEscaping); } public void setAutoEscaping(boolean auto) { autoEscaping = auto; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/escaper/EscapingStrategy.java ================================================ package io.pebbletemplates.pebble.extension.escaper; public interface EscapingStrategy { String escape(String input); } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/escaper/RawFilter.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.extension.escaper; import io.pebbletemplates.pebble.extension.Filter; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.util.List; import java.util.Map; public class RawFilter implements Filter { public List getArgumentNames() { return null; } @Override public Object apply(Object inputObject, Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { return inputObject == null ? null : new SafeString(inputObject.toString()); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/escaper/SafeString.java ================================================ package io.pebbletemplates.pebble.extension.escaper; /** * Wrap a string in this to mark the string as safe to ignore by the Escape extension. * *

* Warning: The EscaperExtension will never escape a string that is wrapped with this class. */ public class SafeString { private final String content; public SafeString(String content) { this.content = content; } @Override public String toString() { return this.content; } @Override public boolean equals(Object o) { return o instanceof SafeString && this.content.equals(((SafeString) o).content); } @Override public int hashCode() { return (this.content == null) ? 0 : this.content.hashCode(); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/i18n/I18nExtension.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.extension.i18n; import io.pebbletemplates.pebble.extension.AbstractExtension; import io.pebbletemplates.pebble.extension.Function; import java.util.HashMap; import java.util.Map; public class I18nExtension extends AbstractExtension { @Override public Map getFunctions() { Map functions = new HashMap<>(); functions.put("i18n", new i18nFunction()); return functions; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/i18n/UTF8Control.java ================================================ package io.pebbletemplates.pebble.extension.i18n; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.URL; import java.net.URLConnection; import java.nio.charset.StandardCharsets; import java.util.Locale; import java.util.PropertyResourceBundle; import java.util.ResourceBundle; public class UTF8Control extends ResourceBundle.Control { @Override public ResourceBundle newBundle(String baseName, Locale locale, String format, ClassLoader loader, boolean reload) throws IOException { String bundleName = toBundleName(baseName, locale); String resourceName = toResourceName(bundleName, "properties"); ResourceBundle bundle = null; InputStream stream = null; if (reload) { URL url = loader.getResource(resourceName); if (url != null) { URLConnection connection = url.openConnection(); if (connection != null) { connection.setUseCaches(false); stream = connection.getInputStream(); } } } else { stream = loader.getResourceAsStream(resourceName); } if (stream != null) { try { bundle = new PropertyResourceBundle(new InputStreamReader(stream, StandardCharsets.UTF_8)); } finally { stream.close(); } } return bundle; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/i18n/i18nFunction.java ================================================ /* * This file is part of Pebble. *

* Copyright (c) 2014 by Mitchell Bösecke *

* For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.extension.i18n; import io.pebbletemplates.pebble.extension.Function; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.text.MessageFormat; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.ResourceBundle; public class i18nFunction implements Function { private final List argumentNames = new ArrayList<>(); public i18nFunction() { this.argumentNames.add("bundle"); this.argumentNames.add("key"); this.argumentNames.add("params"); } @Override public List getArgumentNames() { return this.argumentNames; } @Override public Object execute(Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { String basename = (String) args.get("bundle"); String key = (String) args.get("key"); Object params = args.get("params"); Locale locale = context.getLocale(); ResourceBundle bundle = ResourceBundle.getBundle(basename, locale, new UTF8Control()); Object phraseObject = bundle.getObject(key); if (params != null) { if (params instanceof List) { List list = (List) params; return MessageFormat.format(phraseObject.toString(), list.toArray()); } else { return MessageFormat.format(phraseObject.toString(), params); } } return phraseObject; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/writer/PooledSpecializedStringWriter.java ================================================ package io.pebbletemplates.pebble.extension.writer; import java.io.Writer; /** * A ${@link SpecializedWriter} that's pooled in a ${@link ThreadLocal}. It's backed by a ${@link * StringBuilder} so it's not threadsafe but doesn't involve synchronization. Beware that it has * some limitations: *

    *
  • As it's backed by a ${@link ThreadLocal}, it might leak in environments where ClassLoaders * are rebooted at runtime
  • *
  • It doesn't take any security measure against very large payloads that would cause underlying * buffers to eat memory
  • *
*/ public class PooledSpecializedStringWriter extends Writer implements SpecializedWriter { private static final ThreadLocal POOL = ThreadLocal .withInitial(PooledSpecializedStringWriter::new); private StringBuilder sb = new StringBuilder(); private PooledSpecializedStringWriter() { } @Override public void writeSpecialized(int i) { sb.append(i); } @Override public void writeSpecialized(long l) { sb.append(l); } @Override public void writeSpecialized(double d) { sb.append(d); } @Override public void writeSpecialized(float f) { sb.append(f); } @Override public void writeSpecialized(short s) { sb.append(s); } @Override public void writeSpecialized(byte b) { sb.append(b); } @Override public void writeSpecialized(char c) { sb.append(c); } @Override public void writeSpecialized(String s) { sb.append(s); } @Override public void write(char[] cbuf, int off, int len) { sb.append(cbuf, off, len); } @Override public void flush() { } @Override public void close() { } @Override public String toString() { return sb.toString(); } public static PooledSpecializedStringWriter pooled() { PooledSpecializedStringWriter pooled = POOL.get(); pooled.sb.setLength(0); return pooled; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/writer/SpecializedWriter.java ================================================ package io.pebbletemplates.pebble.extension.writer; import java.math.BigDecimal; /** * A special type to be implemented by ${@link java.io.Writer}s so Pebble can bypass ${@link * Number}s String allocation and directly write primitives. */ public interface SpecializedWriter { void writeSpecialized(int i); void writeSpecialized(long l); void writeSpecialized(double d); void writeSpecialized(float f); void writeSpecialized(short s); void writeSpecialized(byte b); void writeSpecialized(char c); void writeSpecialized(String s); default void write(Object o) { if (o == null) { throw new IllegalArgumentException("Var can not be null"); } else if (o instanceof String) { writeSpecialized((String) o); } else if (o instanceof Integer) { writeSpecialized(((Integer) o).intValue()); } else if (o instanceof Long) { writeSpecialized(((Long) o).longValue()); } else if (o instanceof Double) { writeSpecialized(((Double) o).doubleValue()); } else if (o instanceof Float) { writeSpecialized(((Float) o).floatValue()); } else if (o instanceof Short) { writeSpecialized(((Short) o).shortValue()); } else if (o instanceof Byte) { writeSpecialized(((Byte) o).byteValue()); } else if (o instanceof Character) { writeSpecialized(((Character) o).charValue()); } else if (o instanceof BigDecimal) { writeSpecialized(((BigDecimal) o).toPlainString()); } else { writeSpecialized(o.toString()); } } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/extension/writer/StringWriterSpecializedAdapter.java ================================================ package io.pebbletemplates.pebble.extension.writer; import java.io.StringWriter; /** * A ${@link SpecializedWriter} that wraps a ${@link StringWriter}. Directly write numbers into the * underlying ${@link StringBuffer} and save String allocations (compared to ${@link * java.io.Writer}). */ public class StringWriterSpecializedAdapter implements SpecializedWriter { private final StringBuffer buff; public StringWriterSpecializedAdapter(StringWriter sw) { this.buff = sw.getBuffer(); } @Override public void writeSpecialized(int i) { buff.append(i); } @Override public void writeSpecialized(long l) { buff.append(l); } @Override public void writeSpecialized(double d) { buff.append(d); } @Override public void writeSpecialized(float f) { buff.append(f); } @Override public void writeSpecialized(short s) { buff.append(s); } @Override public void writeSpecialized(byte b) { buff.append(b); } @Override public void writeSpecialized(char i) { buff.append(i); } @Override public void writeSpecialized(String s) { buff.append(s); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/lexer/Lexer.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.lexer; import java.io.Reader; public interface Lexer { TokenStream tokenize(Reader templateReader, String name); } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/lexer/LexerImpl.java ================================================ /* * This file is part of Pebble.

Copyright (c) 2014 by Mitchell Bösecke

For the full * copyright and license information, please view the LICENSE file that was distributed with this * source code. */ package io.pebbletemplates.pebble.lexer; import io.pebbletemplates.pebble.operator.BinaryOperator; import io.pebbletemplates.pebble.operator.UnaryOperator; import io.pebbletemplates.pebble.error.ParserException; import io.pebbletemplates.pebble.lexer.Token.Type; import io.pebbletemplates.pebble.utils.Pair; import io.pebbletemplates.pebble.utils.StringLengthComparator; import io.pebbletemplates.pebble.utils.StringUtils; import java.io.IOException; import java.io.Reader; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collection; import java.util.Deque; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This class reads the template input and builds single items out of it. *

* This class is not thread safe. */ public final class LexerImpl implements Lexer { private final Logger logger = LoggerFactory.getLogger(LexerImpl.class); /** * Syntax */ private final Syntax syntax; /** * Unary operators */ private final Collection unaryOperators; /** * Binary operators */ private final Collection binaryOperators; /** * As we progress through the source we maintain a string which is the text that has yet to be * tokenized. */ private TemplateSource source; /** * The list of tokens that we find and use to create a TokenStream */ private ArrayList tokens; /** * Represents the brackets we are currently inside ordered by how recently we encountered them. * (i.e. peek() will return the most innermost bracket, getLast() will return the outermost). * Brackets in this case includes double quotes. The String value of the pair is the bracket * representation, and the Integer is the line number. */ private LinkedList> brackets; /** * The state of the lexer is important so that we know what to expect next and to help discover * errors in the template (ex. unclosed comments). */ private Deque lexerStateStack = new ArrayDeque(); private enum State { DATA, EXECUTE, PRINT, COMMENT, STRING, STRING_INTERPOLATION } /** * If we encountered an END delimiter that was preceded with a whitespace trim character (ex. {{ * foo -}}) then this boolean is toggled to "true" which tells the lexData() method to trim * leading whitespace from the next text token. */ private boolean trimLeadingWhitespaceFromNextData = false; /** * Static regular expressions for identifiers. */ private static final Pattern REGEX_IDENTIFIER; static { if ("The Android Project".equals(System.getProperty("java.vendor"))) { REGEX_IDENTIFIER = Pattern.compile("^[\\p{Letter}_][\\p{Letter}\\p{Digit}_]*"); } else { // Standard REGEX_IDENTIFIER = Pattern.compile("^[\\p{IsLetter}_][\\p{IsLetter}\\p{IsDigit}_]*"); } } private static final Pattern REGEX_LONG = Pattern.compile("^[0-9]+L"); private static final Pattern REGEX_NUMBER = Pattern.compile("^[0-9]+(\\.[0-9]+)?"); /** * Matches a double quote */ private static final Pattern REGEX_DOUBLEQUOTE = Pattern.compile("^\""); /** * Matches everything up to the first interpolation in a double quoted string */ private static final Pattern REGEX_STRING_NON_INTERPOLATED_PART = Pattern.compile("^[^#\"\\\\]*(?:(?:\\\\.|#(?!\\{))[^#\"\\\\]*)*", Pattern.DOTALL); /** * Matches single quoted strings and double quoted strings without interpolation. Extra complexity * is due to ignoring escaped quotation marks. */ private static final Pattern REGEX_STRING_PLAIN = Pattern.compile( "^\"([^#\"\\\\]*(?:\\\\.[^#\"\\\\]*)*)\"|'([^'\\\\]*(?:\\\\.[^'\\\\]*)*)'", Pattern.DOTALL); private static final String PUNCTUATION = "()[]{}?:.,|="; /** * Regular expression to find operators */ private Pattern regexOperators; /** * Constructor * * @param syntax The primary syntax * @param unaryOperators The available unary operators * @param binaryOperators The available binary operators */ public LexerImpl(Syntax syntax, Collection unaryOperators, Collection binaryOperators) { this.syntax = syntax; this.unaryOperators = unaryOperators; this.binaryOperators = binaryOperators; } /** * This is the main method used to tokenize the raw contents of a template. * * @param reader The reader provided from the Loader * @param name The name of the template (used for meaningful error messages) */ @Override public TokenStream tokenize(Reader reader, String name) { // operator regex this.buildOperatorRegex(); // standardize the character used for line breaks try { this.source = new TemplateSource(reader, name); } catch (IOException e) { throw new ParserException(e, "Can not convert template Reader into a String", 0, name); } this.tokens = new ArrayList<>(); this.lexerStateStack = new ArrayDeque<>(); this.brackets = new LinkedList<>(); /* * Start in a DATA state by pushing it to the state stack. This state basically means that we * are NOT in between a pair of meaningful delimiters. */ this.lexerStateStack.push(State.DATA); /* * loop through the entire source and apply different lexing methods depending on what kind of * state we are in at the time. * * This will always start on lexData(); */ while (this.source.length() > 0) { switch (this.lexerStateStack.peek()) { case DATA: this.tokenizeData(); break; case EXECUTE: this.tokenizeBetweenExecuteDelimiters(); break; case PRINT: this.tokenizeBetweenPrintDelimiters(); break; case COMMENT: this.tokenizeComment(); break; case STRING: this.tokenizeString(); break; case STRING_INTERPOLATION: this.tokenizeStringInterpolation(); break; default: break; } } // end of file token this.pushToken(Token.Type.EOF); this.popState(); // make sure that all brackets have been closed, else throw an error if (!this.brackets.isEmpty()) { String expected = this.brackets.pop().getLeft(); throw new ParserException(null, String.format("Unclosed \"%s\"", expected), this.source.getLineNumber(), this.source.getFilename()); } return new TokenStream(this.tokens, this.source.getFilename()); } private void tokenizeStringInterpolation() { this.logger.trace("Tokenizing String Interpolation"); String lastBracket = this.brackets.peek().getLeft(); Matcher matcher = this.syntax.getRegexInterpolationClose().matcher(this.source); if (this.syntax.getInterpolationOpenDelimiter().equals(lastBracket) && matcher.lookingAt()) { this.brackets.pop(); this.pushToken(Token.Type.STRING_INTERPOLATION_END); this.source.advance(matcher.end()); this.popState(); } else { this.tokenizeExpression(); } } private void tokenizeString() { this.logger.trace("Tokenizing String"); // interpolation Matcher matcher = this.syntax.getRegexInterpolationOpen().matcher(this.source); if (matcher.lookingAt()) { this.brackets.push( new Pair<>(this.syntax.getInterpolationOpenDelimiter(), this.source.getLineNumber())); this.pushToken(Token.Type.STRING_INTERPOLATION_START); this.source.advance(matcher.end()); this.lexerStateStack.push(State.STRING_INTERPOLATION); return; } // regular string start (always full string if single quotes) matcher = REGEX_STRING_NON_INTERPOLATED_PART.matcher(this.source); if (matcher.lookingAt() && matcher.end() > 0) { String token = this.source.substring(matcher.end()); this.source.advance(matcher.end()); this.pushToken(Token.Type.STRING, token); return; } // end of string (which may have contained interpolation) matcher = REGEX_DOUBLEQUOTE.matcher(this.source); if (matcher.lookingAt()) { String expected = this.brackets.pop().getLeft(); if (this.source.charAt(0) != '"') { throw new ParserException(null, String.format("Unclosed \"%s\"", expected), this.source.getLineNumber(), this.source.getFilename()); } this.popState(); this.source.advance(matcher.end()); } } /** * The DATA state assumes that we are current NOT in between any pair of meaningful delimiters. We * are currently looking for the next "open" or "start" delimiter, ex. the opening comment * delimiter, or the opening variable delimiter. */ private void tokenizeData() { this.logger.trace("Tokenizing Data"); // find the next start delimiter Matcher matcher = this.syntax.getRegexStartDelimiters().matcher(this.source); boolean match = matcher.find(); String text; String startDelimiter = null; // if we didn't find another start delimiter, the text // token goes all the way to the end of the template. if (!match) { this.logger .trace("Advancing to the end of the template because no start delimiter was found"); text = this.source.toString(); this.source.advance(this.source.length()); } else { text = this.source.substring(matcher.start()); startDelimiter = this.source.substring(matcher.start(), matcher.end()); this.logger.trace("Start Deliminter Token string: {}", startDelimiter); // advance to after the start delimiter this.source.advance(matcher.end()); } // trim leading whitespace from this text if we previously // encountered the appropriate whitespace trim character if (this.trimLeadingWhitespaceFromNextData) { this.logger.trace("Left Trimming text"); text = StringUtils.ltrim(text); this.trimLeadingWhitespaceFromNextData = false; } Token textToken = this.pushToken(Type.TEXT, text); if (match) { this.checkForLeadingWhitespaceTrim(textToken); if (this.syntax.getCommentOpenDelimiter().equals(startDelimiter)) { // we don't actually push any tokens for comments this.lexerStateStack.push(State.COMMENT); } else if (this.syntax.getPrintOpenDelimiter().equals(startDelimiter)) { this.pushToken(Token.Type.PRINT_START); this.lexerStateStack.push(State.PRINT); } else if ((this.syntax.getExecuteOpenDelimiter().equals(startDelimiter))) { // check for verbatim tag Matcher verbatimStartMatcher = this.syntax.getRegexVerbatimStart().matcher(this.source); if (verbatimStartMatcher.lookingAt()) { this.lexVerbatimData(verbatimStartMatcher); this.lexerStateStack.push(State.DATA); } else { this.pushToken(Token.Type.EXECUTE_START); this.lexerStateStack.push(State.EXECUTE); } } } } /** * Tokenizes between execute delimiters. */ private void tokenizeBetweenExecuteDelimiters() { this.logger.trace("Tokenize between execute delimiters"); // check for the trailing whitespace trim character this.checkForTrailingWhitespaceTrim(); Matcher matcher = this.syntax.getRegexExecuteClose().matcher(this.source); // check if we are at the execute closing delimiter if (this.brackets.isEmpty() && matcher.lookingAt()) { this.pushToken(Token.Type.EXECUTE_END, this.syntax.getExecuteCloseDelimiter()); this.source.advance(matcher.end()); this.popState(); } else { this.tokenizeExpression(); } } /** * Tokenizes between print delimiters. */ private void tokenizeBetweenPrintDelimiters() { // check for the trailing whitespace trim character this.checkForTrailingWhitespaceTrim(); Matcher matcher = this.syntax.getRegexPrintClose().matcher(this.source); // check if we are at the print closing delimiter if (this.brackets.isEmpty() && matcher.lookingAt()) { this.pushToken(Token.Type.PRINT_END, this.syntax.getPrintCloseDelimiter()); this.source.advance(matcher.end()); this.popState(); } else { this.tokenizeExpression(); } } /** * Tokenizes between comment delimiters. *

* Simply find the closing delimiter for the comment and move the cursor to that point. */ private void tokenizeComment() { // all we need to do is find the end of the comment. Matcher matcher = this.syntax.getRegexCommentClose().matcher(this.source); boolean match = matcher.find(0); if (!match) { throw new ParserException(null, "Unclosed comment.", this.source.getLineNumber(), this.source.getFilename()); } /* * check if the commented ended with the whitespace trim character by reversing the comment and * performing a regular forward regex search. */ String comment = this.source.substring(matcher.start()); String reversedComment = new StringBuilder(comment).reverse().toString(); Matcher whitespaceTrimMatcher = this.syntax.getRegexLeadingWhitespaceTrim().matcher(reversedComment); if (whitespaceTrimMatcher.lookingAt()) { this.trimLeadingWhitespaceFromNextData = true; } // move cursor to end of comment (and closing delimiter) this.source.advance(matcher.end()); this.popState(); } /** * Tokenizing an expression which can be found within both execute and print regions. */ private void tokenizeExpression() { this.logger.trace("Tokenizing Expression"); String token; this.source.advanceThroughWhitespace(); /* * Matcher matcher = REGEX_WHITESPACE.matcher(source); if (matcher.lookingAt()) { * source.advance(matcher.end()); } */ // operators Matcher matcher = this.regexOperators.matcher(this.source); if (matcher.lookingAt()) { token = this.source.substring(matcher.end()); this.pushToken(Token.Type.OPERATOR, token); this.source.advance(matcher.end()); return; } // names matcher = REGEX_IDENTIFIER.matcher(this.source); if (matcher.lookingAt()) { token = this.source.substring(matcher.end()); this.pushToken(Token.Type.NAME, token); this.source.advance(matcher.end()); return; } // long matcher = REGEX_LONG.matcher(this.source); if (matcher.lookingAt()) { token = this.source.substring(matcher.end() - 1); this.pushToken(Token.Type.LONG, token); this.source.advance(matcher.end()); return; } // numbers matcher = REGEX_NUMBER.matcher(this.source); if (matcher.lookingAt()) { token = this.source.substring(matcher.end()); this.pushToken(Token.Type.NUMBER, token); this.source.advance(matcher.end()); return; } // punctuation if (PUNCTUATION.indexOf(this.source.charAt(0)) >= 0) { String character = String.valueOf(this.source.charAt(0)); // opening bracket if ("([{".contains(character)) { this.brackets.push(new Pair<>(character, this.source.getLineNumber())); } // closing bracket else if (")]}".contains(character)) { if (this.brackets.isEmpty()) { throw new ParserException(null, "Unexpected \"" + character + "\"", this.source.getLineNumber(), this.source.getFilename()); } else { HashMap validPairs = new HashMap<>(); validPairs.put("(", ")"); validPairs.put("[", "]"); validPairs.put("{", "}"); String lastBracket = this.brackets.pop().getLeft(); String expected = validPairs.get(lastBracket); if (!expected.equals(character)) { throw new ParserException(null, "Unclosed \"" + expected + "\"", this.source.getLineNumber(), this.source.getFilename()); } } } this.pushToken(Token.Type.PUNCTUATION, character); this.source.advance(1); return; } // Plain (non-interpolated) string matcher = REGEX_STRING_PLAIN.matcher(this.source); if (matcher.lookingAt()) { token = this.source.substring(matcher.end()); this.source.advance(matcher.end()); token = this.unquoteAndUnescape(token); this.pushToken(Token.Type.STRING, token); return; } // Interpolated strings matcher = REGEX_DOUBLEQUOTE.matcher(this.source); if (matcher.lookingAt()) { this.brackets.push(new Pair<>("\"", this.source.getLineNumber())); this.lexerStateStack.push(State.STRING); this.source.advance(matcher.end()); return; } // we should have found something and returned by this point throw new ParserException(null, String.format("Unexpected character [%s]", this.source.charAt(0)), this.source.getLineNumber(), this.source.getFilename()); } /** * This method assumes the provided {@code str} starts with a single or double quote. It removes * the wrapping quotes, and un-escapes any quotes within the string. */ private String unquoteAndUnescape(String str) { char quotationType = str.charAt(0); // remove first and last quotation marks str = str.substring(1, str.length() - 1); // remove backslashes used to escape inner quotation marks if (quotationType == '\'') { str = str.replaceAll("\\\\(')", "$1"); } else if (quotationType == '"') { str = str.replaceAll("\\\\(\")", "$1"); } return str; } private void checkForLeadingWhitespaceTrim(Token leadingToken) { Matcher whitespaceTrimMatcher = this.syntax.getRegexLeadingWhitespaceTrim().matcher(this.source); if (whitespaceTrimMatcher.lookingAt()) { this.logger.trace("Found Leading Whitespace Trim Character"); if (leadingToken != null) { this.logger.trace("Right trimming leading token: {}", leadingToken); leadingToken.setValue(StringUtils.rtrim(leadingToken.getValue())); } this.source.advance(whitespaceTrimMatcher.end()); } } private void checkForTrailingWhitespaceTrim() { Matcher whitespaceTrimMatcher = this.syntax.getRegexTrailingWhitespaceTrim().matcher(this.source); if (whitespaceTrimMatcher.lookingAt()) { this.trimLeadingWhitespaceFromNextData = true; } } /** * Implementation of the "verbatim" tag */ private void lexVerbatimData(Matcher verbatimStartMatcher) { // move cursor past the opening verbatim tag this.source.advance(verbatimStartMatcher.end()); // look for the "endverbatim" tag and storing everything between // now and then into a TEXT node Matcher verbatimEndMatcher = this.syntax.getRegexVerbatimEnd().matcher(this.source); // check for EOF if (!verbatimEndMatcher.find()) { throw new ParserException(null, "Unclosed verbatim tag.", this.source.getLineNumber(), this.source.getFilename()); } String verbatimText = this.source.substring(verbatimEndMatcher.start()); // check if the verbatim start tag has a trailing whitespace trim if (verbatimStartMatcher.group(0) != null) { verbatimText = StringUtils.ltrim(verbatimText); } // check if the verbatim end tag had a leading whitespace trim if (verbatimEndMatcher.group(1) != null) { verbatimText = StringUtils.rtrim(verbatimText); } // check if the verbatim end tag had a trailing whitespace trim if (verbatimEndMatcher.group(2) != null) { this.trimLeadingWhitespaceFromNextData = true; } // move cursor past the verbatim text and end delimiter this.source.advance(verbatimEndMatcher.end()); this.pushToken(Type.TEXT, verbatimText); } /** * Create a Token with a Token Type but without no value onto the list of tokens that we are * maintaining. * * @param type The type of Token we are creating */ private Token pushToken(Token.Type type) { Token token = this.pushToken(type, null); return token; } /** * Create a Token of a certain type and value and push it into the list of tokens that we are * maintaining. ` * * @param type The type of token we are creating * @param value The value of the new token */ private Token pushToken(Token.Type type, String value) { // ignore empty text tokens if (type.equals(Token.Type.TEXT) && (value == null || "".equals(value))) { this.logger.trace("Skipping empty text token"); return null; } Token token = new Token(type, value, this.source.getLineNumber()); this.tokens.add(token); this.logger.trace("Pushing Token: {}", token); return token; } /** * Pop state from the stack */ private void popState() { this.lexerStateStack.pop(); } /** * Retrieves the operators (both unary and binary) from the PebbleEngine and then dynamically * creates one giant regular expression to detect for the existence of one of these operators. */ private void buildOperatorRegex() { List operators = new ArrayList<>(); for (UnaryOperator operator : this.unaryOperators) { operators.add(operator.getSymbol()); } for (BinaryOperator operator : this.binaryOperators) { operators.add(operator.getSymbol()); } /* * Since java's matcher doesn't conform with the posix standard of matching the longest * alternative (it matches the first alternative), we must first sort all of the operators by * length before creating the regex. This is to help match "is not" over "is". */ operators.sort(StringLengthComparator.INSTANCE); StringBuilder regex = new StringBuilder("^"); boolean isFirst = true; for (String operator : operators) { if (isFirst) { isFirst = false; } else { regex.append("|"); } regex.append(Pattern.quote(operator)); /* * If the operator ends in an alpha character we use a negative lookahead assertion to make * sure the next character in the stream is NOT an alpha character. This ensures user can type * "organization" without the "or" being parsed as an operator. */ char nextChar = operator.charAt(operator.length() - 1); if (Character.isLetter(nextChar) || Character.getType(nextChar) == Character.LETTER_NUMBER) { regex.append("(?![a-zA-Z0-9_])"); } } this.regexOperators = Pattern.compile(regex.toString()); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/lexer/Syntax.java ================================================ package io.pebbletemplates.pebble.lexer; import java.util.regex.Pattern; /** * The syntax describes the different syntax parts of the Pebble language. * *

* This object is immutable after the creation. This is to make sure the syntax cannot be changed * during the execution. */ public final class Syntax { private final String delimiterCommentOpen; private final String delimiterCommentClose; private final String delimiterExecuteOpen; private final String delimiterExecuteClose; private final String delimiterPrintOpen; private final String delimiterPrintClose; private final String delimiterInterpolationOpen; private final String delimiterInterpolationClose; private final String whitespaceTrim; /** * The regular expressions used to find the different delimiters */ private final Pattern regexPrintClose; private final Pattern regexExecuteClose; private final Pattern regexCommentClose; private final Pattern regexStartDelimiters; private final Pattern regexLeadingWhitespaceTrim; private final Pattern regexTrailingWhitespaceTrim; private final Pattern regexInterpolationOpen; private final Pattern regexInterpolationClose; /** * Regular expressions used to find "verbatim" and "endverbatim" tags. */ private final Pattern regexVerbatimStart; private final Pattern regexVerbatimEnd; private static final String POSSIBLE_NEW_LINE = "(\r\n|\n\r|\r|\n|\u0085|\u2028|\u2029)?"; public Syntax(final String delimiterCommentOpen, final String delimiterCommentClose, final String delimiterExecuteOpen, final String delimiterExecuteClose, final String delimiterPrintOpen, final String delimiterPrintClose, final String delimiterInterpolationOpen, final String delimiterInterpolationClose, final String whitespaceTrim, final boolean enableNewLineTrimming) { this.delimiterCommentClose = delimiterCommentClose; this.delimiterCommentOpen = delimiterCommentOpen; this.delimiterExecuteOpen = delimiterExecuteOpen; this.delimiterExecuteClose = delimiterExecuteClose; this.delimiterPrintOpen = delimiterPrintOpen; this.delimiterPrintClose = delimiterPrintClose; this.whitespaceTrim = whitespaceTrim; this.delimiterInterpolationClose = delimiterInterpolationClose; this.delimiterInterpolationOpen = delimiterInterpolationOpen; // Do we trim the newline following a tag? String newlineRegexSuffix = enableNewLineTrimming ? POSSIBLE_NEW_LINE : ""; // regexes used to find the individual delimiters this.regexPrintClose = Pattern.compile("^\\s*" + Pattern.quote(whitespaceTrim) + "?" + Pattern.quote(delimiterPrintClose) + newlineRegexSuffix); this.regexExecuteClose = Pattern.compile("^\\s*" + Pattern.quote(whitespaceTrim) + "?" + Pattern.quote(delimiterExecuteClose) + newlineRegexSuffix); this.regexCommentClose = Pattern .compile(Pattern.quote(delimiterCommentClose) + newlineRegexSuffix); // combination regex used to find the next START delimiter of any kind this.regexStartDelimiters = Pattern.compile(Pattern.quote(delimiterPrintOpen) + "|" + Pattern.quote(delimiterExecuteOpen) + "|" + Pattern.quote(delimiterCommentOpen)); // regex to find the verbatim tag this.regexVerbatimStart = Pattern .compile("^\\s*verbatim\\s*(" + Pattern.quote(whitespaceTrim) + ")?" + Pattern.quote(delimiterExecuteClose) + newlineRegexSuffix); this.regexVerbatimEnd = Pattern.compile(Pattern.quote(delimiterExecuteOpen) + "(" + Pattern.quote(whitespaceTrim) + ")?" + "\\s*endverbatim\\s*(" + Pattern .quote(whitespaceTrim) + ")?" + Pattern.quote(delimiterExecuteClose) + newlineRegexSuffix); // regex for the whitespace trim character this.regexLeadingWhitespaceTrim = Pattern.compile(Pattern.quote(whitespaceTrim) + "\\s+"); this.regexTrailingWhitespaceTrim = Pattern.compile("^\\s*" + Pattern.quote(whitespaceTrim) + "(" + Pattern.quote(delimiterPrintClose) + "|" + Pattern.quote(delimiterExecuteClose) + "|" + Pattern.quote(delimiterCommentClose) + ")"); this.regexInterpolationOpen = Pattern.compile("^" + Pattern.quote(delimiterInterpolationOpen)); this.regexInterpolationClose = Pattern .compile("^\\s*" + Pattern.quote(delimiterInterpolationClose)); } /** * @return the commentOpenDelimiter */ public String getCommentOpenDelimiter() { return delimiterCommentOpen; } /** * @return the commentCloseDelimiter */ public String getCommentCloseDelimiter() { return delimiterCommentClose; } /** * @return the executeOpenDelimiter */ public String getExecuteOpenDelimiter() { return delimiterExecuteOpen; } /** * @return the executeCloseDelimiter */ public String getExecuteCloseDelimiter() { return delimiterExecuteClose; } /** * @return the printOpenDelimiter */ public String getPrintOpenDelimiter() { return delimiterPrintOpen; } /** * @return the printCloseDelimiter */ public String getPrintCloseDelimiter() { return delimiterPrintClose; } public String getInterpolationOpenDelimiter() { return delimiterInterpolationOpen; } public String getInterpolationCloseDelimiter() { return delimiterInterpolationClose; } public String getWhitespaceTrim() { return whitespaceTrim; } Pattern getRegexPrintClose() { return regexPrintClose; } Pattern getRegexExecuteClose() { return regexExecuteClose; } Pattern getRegexCommentClose() { return regexCommentClose; } Pattern getRegexStartDelimiters() { return regexStartDelimiters; } Pattern getRegexLeadingWhitespaceTrim() { return regexLeadingWhitespaceTrim; } Pattern getRegexTrailingWhitespaceTrim() { return regexTrailingWhitespaceTrim; } Pattern getRegexVerbatimEnd() { return regexVerbatimEnd; } Pattern getRegexVerbatimStart() { return regexVerbatimStart; } Pattern getRegexInterpolationOpen() { return regexInterpolationOpen; } Pattern getRegexInterpolationClose() { return regexInterpolationClose; } /** * Helper class to create new instances of {@link Syntax}. */ public static class Builder { private String delimiterCommentOpen = "{#"; private String delimiterCommentClose = "#}"; private String delimiterExecuteOpen = "{%"; private String delimiterExecuteClose = "%}"; private String delimiterPrintOpen = "{{"; private String delimiterPrintClose = "}}"; private String delimiterInterpolationOpen = "#{"; private String delimiterInterpolationClose = "}"; private String whitespaceTrim = "-"; private boolean enableNewLineTrimming = true; /** * @return the commentOpenDelimiter */ public String getCommentOpenDelimiter() { return delimiterCommentOpen; } /** * @param commentOpenDelimiter the commentOpenDelimiter to set * @return This builder object */ public Builder setCommentOpenDelimiter(String commentOpenDelimiter) { this.delimiterCommentOpen = commentOpenDelimiter; return this; } /** * @return the commentCloseDelimiter */ public String getCommentCloseDelimiter() { return delimiterCommentClose; } /** * @param commentCloseDelimiter the commentCloseDelimiter to set * @return This builder object */ public Builder setCommentCloseDelimiter(String commentCloseDelimiter) { this.delimiterCommentClose = commentCloseDelimiter; return this; } /** * @return the executeOpenDelimiter */ public String getExecuteOpenDelimiter() { return delimiterExecuteOpen; } /** * @param executeOpenDelimiter the executeOpenDelimiter to set * @return This builder object */ public Builder setExecuteOpenDelimiter(String executeOpenDelimiter) { this.delimiterExecuteOpen = executeOpenDelimiter; return this; } /** * @return the executeCloseDelimiter */ public String getExecuteCloseDelimiter() { return delimiterExecuteClose; } /** * @param executeCloseDelimiter the executeCloseDelimiter to set * @return This builder object */ public Builder setExecuteCloseDelimiter(String executeCloseDelimiter) { this.delimiterExecuteClose = executeCloseDelimiter; return this; } /** * @return the printOpenDelimiter */ public String getPrintOpenDelimiter() { return delimiterPrintOpen; } /** * @param printOpenDelimiter the printOpenDelimiter to set * @return This builder object */ public Builder setPrintOpenDelimiter(String printOpenDelimiter) { this.delimiterPrintOpen = printOpenDelimiter; return this; } /** * @return the printCloseDelimiter */ public String getPrintCloseDelimiter() { return delimiterPrintClose; } /** * @param printCloseDelimiter the printCloseDelimiter to set * @return This builder object */ public Builder setPrintCloseDelimiter(String printCloseDelimiter) { this.delimiterPrintClose = printCloseDelimiter; return this; } public String getWhitespaceTrim() { return whitespaceTrim; } public Builder setWhitespaceTrim(String whitespaceTrim) { this.whitespaceTrim = whitespaceTrim; return this; } public String getInterpolationOpenDelimiter() { return delimiterInterpolationOpen; } public void setInterpolationOpenDelimiter(String delimiterInterpolationOpen) { this.delimiterInterpolationOpen = delimiterInterpolationOpen; } public String getInterpolationCloseDelimiter() { return delimiterInterpolationClose; } public void setInterpolationCloseDelimiter(String delimiterInterpolationClose) { this.delimiterInterpolationClose = delimiterInterpolationClose; } public boolean isEnableNewLineTrimming() { return enableNewLineTrimming; } public Builder setEnableNewLineTrimming(boolean enableNewLineTrimming) { this.enableNewLineTrimming = enableNewLineTrimming; return this; } public Syntax build() { return new Syntax(delimiterCommentOpen, delimiterCommentClose, delimiterExecuteOpen, delimiterExecuteClose, delimiterPrintOpen, delimiterPrintClose, delimiterInterpolationOpen, delimiterInterpolationClose, whitespaceTrim, enableNewLineTrimming); } } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/lexer/TemplateSource.java ================================================ package io.pebbletemplates.pebble.lexer; import java.io.IOException; import java.io.Reader; import java.util.Arrays; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * An implementation of CharSequence that is tuned to be used specifically by {@link LexerImpl}. It * is possible to advance through the sequence without allocating a copy and it is possible to * perform regex matches from the logical beginning of the remaining un-tokenized source. This class * will also standardize newline characters from different architectures. * * @author mbosecke */ public class TemplateSource implements CharSequence { private final Logger logger = LoggerFactory.getLogger(TemplateSource.class); /** * The characters found within the template. */ private char source[]; /** * Number of characters stored in source array remaining to be tokenized */ private int size = 0; /** * Default capacity */ private static final int DEFAULT_CAPACITY = 1024; /** * An index of the first character for the remaining un-tokenized source. */ private int offset = 0; /** * Tracking the line number that we are currently tokenizing. */ private int lineNumber = 1; /** * Filename of the template */ private final String filename; /** * Constructor * * @param reader Reader provided by the Loader * @param filename Filename of the template * @throws IOException Exceptions thrown from the reader */ public TemplateSource(Reader reader, String filename) throws IOException { this.filename = filename; this.source = new char[DEFAULT_CAPACITY]; copyReaderIntoCharArray(reader); } /** * Read the contents of the template into the internal char[]. */ private void copyReaderIntoCharArray(Reader reader) throws IOException { char[] buffer = new char[1024 * 4]; int amountJustRead; while ((amountJustRead = reader.read(buffer)) != -1) { ensureCapacity(size + amountJustRead); append(buffer, amountJustRead); } reader.close(); } /** * Append characters to the internal array. */ private void append(char[] characters, int amount) { System.arraycopy(characters, 0, source, size, amount); size += amount; } /** * Ensure that the internal array has a minimum capacity. */ private void ensureCapacity(int minCapacity) { if (source.length - minCapacity < 0) { grow(minCapacity); } } /** * Grow the internal array to at least the desired minimum capacity. */ private void grow(int minCapacity) { int oldCapacity = source.length; /* * double the capacity of the array and if that's not enough, just use * the minCapacity */ int newCapacity = Math.max(oldCapacity << 1, minCapacity); this.source = Arrays.copyOf(source, newCapacity); } /** * Moves the start index a certain amount. While traversing this amount we will count how many * newlines have been encountered. * * @param amount Amount of characters to advance by */ public void advance(int amount) { logger.trace("Advancing amount: {}", amount); int index = 0; while (index < amount) { int sizeOfNewline = advanceThroughNewline(index); if (sizeOfNewline > 0) { index += sizeOfNewline; } else { index++; } } this.size -= amount; this.offset += amount; } public void advanceThroughWhitespace() { int index = 0; while (Character.isWhitespace(this.charAt(index))) { int sizeOfNewline = advanceThroughNewline(index); if (sizeOfNewline > 0) { index += sizeOfNewline; } else { index++; } } logger.trace("Advanced through {} characters of whitespace.", index); this.size -= index; this.offset += index; } /** * Advances through possible newline character and returns how many characters were used to * represent the newline (windows uses two characters to represent one newline). * * @param index The index of the potential newline character */ private int advanceThroughNewline(int index) { char character = this.charAt(index); int numOfCharacters = 0; // windows newline if ('\r' == character && '\n' == this.charAt(index + 1)) { this.lineNumber++; numOfCharacters = 2; // various other newline characters } else if ('\n' == character || '\r' == character || '\u0085' == character || '\u2028' == character || '\u2029' == character) { this.lineNumber++; numOfCharacters = 1; } return numOfCharacters; } public String substring(int start, int end) { return new String(Arrays.copyOfRange(source, this.offset + start, this.offset + end)); } public String substring(int end) { return new String(Arrays.copyOfRange(source, offset, offset + end)); } @Override public int length() { return size; } @Override public char charAt(int index) { return source[offset + index]; } @Override public CharSequence subSequence(int start, int end) { return new String(Arrays.copyOfRange(source, this.offset + start, this.offset + end)); } public String toString() { return new String(Arrays.copyOfRange(source, offset, offset + size)); } public int getLineNumber() { return lineNumber; } public String getFilename() { return filename; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/lexer/Token.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE file that was distributed * with this source code. */ package io.pebbletemplates.pebble.lexer; import java.util.Arrays; public class Token { private String value; private Type type; private int lineNumber; public enum Type { EOF, TEXT, EXECUTE_START, EXECUTE_END, PRINT_START, PRINT_END, NAME, NUMBER, LONG, STRING, OPERATOR, PUNCTUATION, STRING_INTERPOLATION_START, STRING_INTERPOLATION_END } public Token(Type type, String value, int lineNumber) { this.type = type; this.value = value; this.lineNumber = lineNumber; } public boolean test(Type type) { return this.test(type, new String[0]); } public boolean test(Type type, String... values) { boolean test = true; if (values.length > 0) { test = Arrays.asList(values).contains(this.value); } return test && this.type.equals(type); } public String getValue() { return this.value; } public void setValue(String value) { this.value = value; } public Type getType() { return this.type; } public void setType(Type type) { this.type = type; } public int getLineNumber() { return this.lineNumber; } public void setLineNumber(int lineNumber) { this.lineNumber = lineNumber; } @Override public String toString() { return "Token [value=" + value + ", type=" + type + ", lineNumber=" + lineNumber + "]"; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/lexer/TokenStream.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.lexer; import io.pebbletemplates.pebble.error.ParserException; import java.util.ArrayList; import java.util.Collection; public class TokenStream { private ArrayList tokens = new ArrayList<>(); private int current; private String filename; /** * Constructor for a Token Stream * * @param tokens A collection of tokens * @param name The filename of the template that these tokens came from */ public TokenStream(Collection tokens, String name) { this.tokens.addAll(tokens); this.current = 0; this.filename = name; } /** * Consumes and returns the next token in the stream. * * @return The next token */ public Token next() { return this.tokens.get(++this.current); } /** * Checks the current token to see if it matches the provided type. If it doesn't match this will * throw a SyntaxException. This will consume a token. * * @param type The type of token that we expect * @return Token The current token */ public Token expect(Token.Type type) { return this.expect(type, null); } /** * Checks the current token to see if it matches the provided type. If it doesn't match this will * throw a SyntaxException. This will consume a token. * * @param type The type of token that we expect * @param value The expected value of the token * @return Token The current token */ public Token expect(Token.Type type, String value) { Token token = this.tokens.get(this.current); boolean success = value == null ? token.test(type) : token.test(type, value); if (!success) { String message = String .format("Unexpected token of value \"%s\" and type %s, expected token of type %s", token.getValue(), token.getType().toString(), type); throw new ParserException(null, message, token.getLineNumber(), this.filename); } this.next(); return token; } /** * Returns the next token in the stream without consuming it. * * @return The next token */ public Token peek() { return this.peek(1); } /** * Returns a future token in the stream without consuming any. * * @param number How many tokens to lookahead * @return The token we are peeking at */ public Token peek(int number) { return this.tokens.get(this.current + number); } public boolean isEOF() { return this.tokens.get(this.current).getType().equals(Token.Type.EOF); } @Override public String toString() { return String.format("Current: %s. All: %s", this.current(), this.tokens); } /** * Looks at the current token. Does not consume the token. * * @return Token The current token */ public Token current() { return this.tokens.get(this.current); } public String getFilename() { return this.filename; } /** * used for testing purposes * * @return List of tokens */ public ArrayList getTokens() { return this.tokens; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/loader/AbstractServletLoader.java ================================================ package io.pebbletemplates.pebble.loader; import io.pebbletemplates.pebble.error.LoaderException; import io.pebbletemplates.pebble.utils.PathUtils; import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URL; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Abstract base class for loaders which user the servlet context to load templates. * * @author mbosecke * @author chkal */ public abstract class AbstractServletLoader implements Loader { private static final Logger logger = LoggerFactory.getLogger(AbstractServletLoader.class); private String prefix; private String suffix; private String charset = "UTF-8"; private char expectedSeparator = '/'; protected abstract InputStream getResourceAsStream(String location); protected abstract URL getResource(String location) throws MalformedURLException; @Override public Reader getReader(String templateName) { InputStreamReader isr = null; Reader reader = null; InputStream is = null; String location = this.getLocation(templateName); logger.debug("Looking for template in {}.", location); is = getResourceAsStream(location); if (is == null) { throw new LoaderException(null, "Could not find template \"" + location + "\""); } try { isr = new InputStreamReader(is, this.charset); reader = new BufferedReader(isr); } catch (UnsupportedEncodingException e) { } return reader; } private String getLocation(String templateName) { // Add the prefix and make sure that it ends with a separator character StringBuilder path = new StringBuilder(128); if (this.getPrefix() != null) { path.append(this.getPrefix()); // we do NOT use OS dependent separators here; getResourceAsStream // explicitly requires forward slashes. if (!this.getPrefix().endsWith(Character.toString(this.expectedSeparator))) { path.append(this.expectedSeparator); } } path.append(templateName); if (this.getSuffix() != null) { path.append(this.getSuffix()); } return path.toString(); } public String getSuffix() { return this.suffix; } @Override public void setSuffix(String suffix) { this.suffix = suffix; } public String getPrefix() { return this.prefix; } @Override public void setPrefix(String prefix) { this.prefix = prefix; } public String getCharset() { return this.charset; } @Override public void setCharset(String charset) { this.charset = charset; } @Override public String resolveRelativePath(String relativePath, String anchorPath) { return PathUtils.resolveRelativePath(relativePath, anchorPath, this.expectedSeparator); } @Override public String createCacheKey(String templateName) { return templateName; } @Override public boolean resourceExists(String templateName) { try { return getResource(this.getLocation(templateName)) != null; } catch (MalformedURLException e) { return false; } } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/loader/ClasspathLoader.java ================================================ /* * This file is part of Pebble. *

* Copyright (c) 2014 by Mitchell Bösecke *

* For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.loader; import io.pebbletemplates.pebble.error.LoaderException; import io.pebbletemplates.pebble.utils.PathUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.io.UnsupportedEncodingException; /** * Uses a classloader to find templates located on the classpath. * * @author mbosecke */ public class ClasspathLoader implements Loader { private static final Logger logger = LoggerFactory.getLogger(ClasspathLoader.class); private String prefix; private String suffix; private String charset = "UTF-8"; private char expectedSeparator = '/'; private final ClassLoader rcl; public ClasspathLoader(ClassLoader classLoader) { this.rcl = classLoader; } public ClasspathLoader() { this(ClasspathLoader.class.getClassLoader()); } @Override public Reader getReader(String templateName) { String location = this.getLocation(templateName); logger.debug("Looking for template in {}.", location); // perform the lookup InputStream is = this.rcl.getResourceAsStream(location); if (is == null) { throw new LoaderException(null, "Could not find template \"" + location + "\""); } try { return new BufferedReader(new InputStreamReader(is, this.charset)); } catch (UnsupportedEncodingException e) { } return null; } private String getLocation(String templateName) { // append the prefix and make sure prefix ends with a separator character StringBuilder path = new StringBuilder(128); if (this.getPrefix() != null) { path.append(this.getPrefix()); // we do NOT use OS dependent separators here; getResourceAsStream // explicitly requires forward slashes. if (!this.getPrefix().endsWith(Character.toString(this.expectedSeparator))) { path.append(this.expectedSeparator); } } path.append(templateName); if (this.getSuffix() != null) { path.append(this.getSuffix()); } return path.toString(); } public String getSuffix() { return this.suffix; } @Override public void setSuffix(String suffix) { this.suffix = suffix; } public String getPrefix() { return this.prefix; } @Override public void setPrefix(String prefix) { this.prefix = prefix; } public String getCharset() { return this.charset; } @Override public void setCharset(String charset) { this.charset = charset; } @Override public String resolveRelativePath(String relativePath, String anchorPath) { return PathUtils.resolveRelativePath(relativePath, anchorPath, this.expectedSeparator); } @Override public String createCacheKey(String templateName) { return templateName; } @Override public boolean resourceExists(String templateName) { return this.rcl.getResource(this.getLocation(templateName)) != null; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/loader/DelegatingLoader.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.loader; import io.pebbletemplates.pebble.error.LoaderException; import java.io.Reader; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * This loader will delegate control to a list of children loaders. This is the default * implementation used by Pebble; it delegates to a classpath loader and a file loader to increase * the chances of finding templates with varying setups. * * @author mbosecke */ public class DelegatingLoader implements Loader { private String prefix; private String suffix; private String charset = "UTF-8"; /** * Children loaders to delegate to. The loaders are used in order and as soon as one of them finds * a template, the others will not be given a chance to do so. */ private final List> loaders; /** * Constructor provided with a list of children loaders. * * @param loaders A list of loaders to delegate to */ public DelegatingLoader(List> loaders) { this.loaders = Collections.unmodifiableList(new ArrayList<>(loaders)); } @Override public Reader getReader(DelegatingLoaderCacheKey cacheKey) { Reader reader = null; final int size = this.loaders.size(); for (int i = 0; i < size; i++) { Loader loader = this.loaders.get(i); Object delegatingKey = cacheKey.getDelegatingCacheKeys().get(i); try { reader = this.getReaderInner(loader, delegatingKey); if (reader != null) { break; } } catch (Exception e) { // do nothing } } if (reader == null) { throw new LoaderException(null, "Could not find template \"" + cacheKey.getTemplateName() + "\""); } return reader; } private Reader getReaderInner(Loader delegatingLoader, Object cacheKey) { // This unchecked cast is ok, because we ensure that the type of the // cache key corresponds to the loader when we create the key. @SuppressWarnings("unchecked") T castedKey = (T) cacheKey; return delegatingLoader.getReader(castedKey); } public String getSuffix() { return this.suffix; } @Override public void setSuffix(String suffix) { this.suffix = suffix; for (Loader loader : this.loaders) { loader.setSuffix(suffix); } } public String getPrefix() { return this.prefix; } @Override public void setPrefix(String prefix) { this.prefix = prefix; for (Loader loader : this.loaders) { loader.setPrefix(prefix); } } public String getCharset() { return this.charset; } @Override public void setCharset(String charset) { this.charset = charset; for (Loader loader : this.loaders) { loader.setCharset(charset); } } @Override public String resolveRelativePath(String relativePath, String anchorPath) { if (relativePath == null) { return null; } for (Loader loader : this.loaders) { String path = loader.resolveRelativePath(relativePath, anchorPath); if (path != null) { return path; } } return null; } @Override public DelegatingLoaderCacheKey createCacheKey(String templateName) { List keys = new ArrayList<>(); for (Loader loader : this.loaders) { keys.add(loader.createCacheKey(templateName)); } return new DelegatingLoaderCacheKey(keys, templateName); } @Override public boolean resourceExists(String templateName) { for (Loader loader : this.loaders) { if (loader.resourceExists(templateName)) { return true; } } return false; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/loader/DelegatingLoaderCacheKey.java ================================================ package io.pebbletemplates.pebble.loader; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * The delegating loader cache key is used as the cache key for {@link DelegatingLoader}. * *

* The object stores all cache keys of all loaders. Those keys together builds the key for the * delegating loader. * * @author Thomas Hunziker */ public final class DelegatingLoaderCacheKey { private final List delegatingCacheKeys; private final String templateName; private final int hashCode; DelegatingLoaderCacheKey(final List delegatingCacheKeys, final String templateName) { this.delegatingCacheKeys = Collections.unmodifiableList(new ArrayList<>(delegatingCacheKeys)); this.templateName = templateName; this.hashCode = this.caclulateHashCode(); } public String getTemplateName() { return templateName; } public List getDelegatingCacheKeys() { return delegatingCacheKeys; } private int caclulateHashCode() { final int prime = 31; int result = 1; result = prime * result + ((delegatingCacheKeys == null) ? 0 : delegatingCacheKeys.hashCode()); result = prime * result + ((templateName == null) ? 0 : templateName.hashCode()); return result; } @Override public int hashCode() { return this.hashCode; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } DelegatingLoaderCacheKey other = (DelegatingLoaderCacheKey) obj; if (delegatingCacheKeys == null) { if (other.delegatingCacheKeys != null) { return false; } } else if (!delegatingCacheKeys.equals(other.delegatingCacheKeys)) { return false; } if (templateName == null) { return other.templateName == null; } else { return templateName.equals(other.templateName); } } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/loader/FileLoader.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.loader; import io.pebbletemplates.pebble.error.LoaderException; import io.pebbletemplates.pebble.utils.PathUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.*; import java.nio.file.Path; import java.nio.file.Paths; /** * This loader searches for a file located anywhere on the filesystem. It uses java.io.File to * perform the lookup. * * @author mbosecke */ public class FileLoader implements Loader { private static final Logger logger = LoggerFactory.getLogger(FileLoader.class); private String prefix; private String suffix; private String charset = "UTF-8"; public FileLoader(String prefix) { this.setPrefix(prefix); } @Override public Reader getReader(String templateName) { File file = this.getFile(templateName); try { InputStream is = new FileInputStream(file); return new BufferedReader(new InputStreamReader(is, this.charset)); } catch (FileNotFoundException e) { throw new LoaderException(e, String.format("Could not find template [prefix='%s', templateName='%s']", this.prefix, templateName)); } catch (UnsupportedEncodingException e) { throw new LoaderException(e, String.format("Invalid charset '%s'", this.charset)); } } private File getFile(String templateName) { templateName = templateName + (this.getSuffix() == null ? "" : this.getSuffix()); templateName = PathUtils.sanitize(templateName, File.separatorChar); Path path = Paths.get(this.getPrefix(), templateName); logger.trace("Looking for template in {}.", path); this.checkIfDirectoryTraversal(templateName); return path.toFile(); } public String getSuffix() { return this.suffix; } @Override public void setSuffix(String suffix) { this.suffix = suffix; } public String getPrefix() { return this.prefix; } @Override public void setPrefix(String prefix) { if (prefix == null) { throw new LoaderException(null, "Prefix cannot be null"); } String trimmedPrefix = prefix.trim(); if (trimmedPrefix.isEmpty()) { throw new LoaderException(null, "Prefix cannot be empty"); } if (!Paths.get(trimmedPrefix).isAbsolute()) { throw new LoaderException(null, "Prefix must be an absolute path"); } this.prefix = trimmedPrefix; } public String getCharset() { return this.charset; } @Override public void setCharset(String charset) { this.charset = charset; } @Override public String resolveRelativePath(String relativePath, String anchorPath) { return PathUtils.resolveRelativePath(relativePath, anchorPath, File.separatorChar); } @Override public String createCacheKey(String templateName) { return templateName; } @Override public boolean resourceExists(String templateName) { return this.getFile(templateName).exists(); } private void checkIfDirectoryTraversal(String templateName) { Path baseDirPath = Paths.get(prefix); Path userPath = Paths.get(templateName); if (userPath.isAbsolute()) { throw new LoaderException(null, String.format("templateName '%s' must be relative", templateName)); } // Join the two paths together, then normalize so that any ".." elements // in the userPath can remove parts of baseDirPath. // (e.g. "/foo/bar/baz" + "../attack" -> "/foo/bar/attack") Path resolvedPath = baseDirPath.resolve(userPath).normalize(); // Make sure the resulting path is still within the required directory. // (In the example above, "/foo/bar/attack" is not.) if (!resolvedPath.startsWith(baseDirPath)) { throw new LoaderException(null, String.format("template is not in the base directory path [baseDir='%s', templateName='%s']", this.prefix, templateName)); } } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/loader/Loader.java ================================================ /* * This file is part of Pebble. *

* Copyright (c) 2014 by Mitchell Bösecke *

* For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.loader; import io.pebbletemplates.pebble.PebbleEngine; import java.io.Reader; /** * Interface used to find templates for Pebble. Different implementations can use different * techniques for finding templates such as looking on the classpath, looking in a database, using a * servlet context, etc. * * @author mbosecke */ public interface Loader { /** * The reader which will be used by Pebble to read the contents of the template. * * @param cacheKey the cache key to use to load create the reader. * @return A reader object, or {@code null} if the template does not exist */ Reader getReader(T cacheKey); /** * A method for end users to change the charset used by the loader. * * @param charset Character set used by the loader when building a reader object */ void setCharset(String charset); /** * Optional prefix to help find templates, ex "/WEB-INF/templates/" or "database_schema." * * @param prefix Prefix to help find templates */ void setPrefix(String prefix); /** * Optional suffix to help find templates, ex ".html", ".peb" * * @param suffix Suffix to attach to template names */ void setSuffix(String suffix); /** * Resolves the given {@code relativePath} based on the given {@code anchorPath}. * *

* A path is considered as relative when it starts either with '..' or '.' and followed either by * a '/' or '\\' otherwise the assumption is that the provided path is an absolute path. * * @param relativePath the relative path which should be resolved. * @param anchorPath the anchor path based on which the relative path should be resolved on. * @return the resolved path or {@code null} when the path could not be resolved. */ String resolveRelativePath(String relativePath, String anchorPath); /** * This method resolves the given template name to a unique object which can be used as the key * within the {@link PebbleEngine#getTemplateCache()}. The returned object will be passed with * {@link #getReader(Object)}. * *

* The resolve method can eventually add information to the cache key from the context (e.g. user * session information, servlet request etc.). * *

* As a concrete example if the loader loads a template created by a user form the database the * template name itself is not uniquely identify the template. The identification of the template * requires also the user which created the template. Hence for the key the user id and the * template name should be used. So the cache key is enhanced by some contextual information. * *

* The implementor of the method can add as many additional contextual information to the returned * object. However the following things needs to be considered: *

    *
  • This method will be called on each * {@link PebbleEngine#getTemplate(String)}. Hence the implementation needs to be fast and * eventually use some caching for the lookup process.
  • *
  • The returned object is used within a cache and hence needs to * implement {@link Object#equals(Object)} and {@link Object#hashCode()}.
  • *
  • The object is kept in memory and hence it should not be to memory * heavy.
  • *
* *

* Depending on this implementation the {@link PebbleEngine#getTemplateCache()} should be tuned in * a way it can operate optimal. E.g. when the number of potential templates is infinite the cache * should evict some templates at some point in time otherwise the stability of the memory is not * given anymore. * * @param templateName The name of the template * @return Returns the cache key */ T createCacheKey(String templateName); boolean resourceExists(String templateName); } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/loader/MemoryLoader.java ================================================ package io.pebbletemplates.pebble.loader; import io.pebbletemplates.pebble.error.LoaderException; import java.io.Reader; import java.io.StringReader; import java.util.ArrayList; import java.util.List; public class MemoryLoader implements Loader { private final List templateDefinitions = new ArrayList<>(); @Override public Reader getReader(String templateName) { String content = ""; for (TemplateDefinition templateDefinition : this.templateDefinitions) { if (templateDefinition.templateName.equals(templateName)) { content = templateDefinition.content; break; } } if (content.isEmpty()) { throw new LoaderException(null, "Could not find template \"" + templateName + "\""); } return new StringReader(content); } public void addTemplate(String templateName, String content) { if (templateName == null) { throw new IllegalArgumentException("templateName cannot be null"); } if (content == null) { throw new IllegalArgumentException("content cannot be null"); } this.templateDefinitions.add(new TemplateDefinition(templateName, content)); } public List getTemplates() { return this.templateDefinitions; } @Override public void setSuffix(String suffix) { } @Override public void setPrefix(String prefix) { } @Override public void setCharset(String charset) { } @Override public String resolveRelativePath(String relativePath, String anchorPath) { return relativePath; // hierarchy is flat } @Override public String createCacheKey(String templateName) { return templateName; } @Override public boolean resourceExists(String templateName) { for (TemplateDefinition templateDefinition : this.templateDefinitions) { if (templateDefinition.templateName.equals(templateName)) { return true; } } return false; } public static class TemplateDefinition { public final String templateName; public final String content; public TemplateDefinition(String templateName, String content) { this.templateName = templateName; this.content = content; } } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/loader/Servlet5Loader.java ================================================ package io.pebbletemplates.pebble.loader; import jakarta.servlet.ServletContext; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; /** * Loader that uses a servlet context to find templates. Requires Jakarta Servlet 5.0 or newer. * * @author mbosecke * @author chkal */ public class Servlet5Loader extends AbstractServletLoader { private final ServletContext context; public Servlet5Loader(ServletContext context) { this.context = context; } @Override protected InputStream getResourceAsStream(String location) { return context.getResourceAsStream(location); } @Override protected URL getResource(String location) throws MalformedURLException { return context.getResource(location); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/loader/ServletLoader.java ================================================ package io.pebbletemplates.pebble.loader; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import javax.servlet.ServletContext; /** * Loader that uses a servlet context to find templates. * * @author mbosecke * @author chkal */ public class ServletLoader extends AbstractServletLoader { private final ServletContext context; public ServletLoader(ServletContext context) { this.context = context; } @Override protected InputStream getResourceAsStream(String location) { return context.getResourceAsStream(location); } @Override protected URL getResource(String location) throws MalformedURLException { return context.getResource(location); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/loader/StringLoader.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.loader; import java.io.Reader; import java.io.StringReader; /** * This loader is not intended to be used in a production system; it is primarily for testing and * debugging. Many tags do not work when using this loader, such as "extends", "imports", * "include". */ public class StringLoader implements Loader { @Override public Reader getReader(String templateName) { return new StringReader(templateName); } @Override public void setPrefix(String prefix) { } @Override public void setSuffix(String suffix) { } @Override public void setCharset(String charset) { } @Override public String resolveRelativePath(String relativePath, String anchorPath) { return null; } @Override public String createCacheKey(String templateName) { return templateName; } @Override public boolean resourceExists(String templateName) { return true; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/AbstractRenderableNode.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node; import io.pebbletemplates.pebble.extension.NodeVisitor; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; import java.io.IOException; import java.io.Writer; public abstract class AbstractRenderableNode implements RenderableNode { private int lineNumber; @Override public abstract void render(PebbleTemplateImpl self, Writer writer, EvaluationContextImpl context) throws IOException; @Override public abstract void accept(NodeVisitor visitor); public AbstractRenderableNode() { } public AbstractRenderableNode(int lineNumber) { this.setLineNumber(lineNumber); } public int getLineNumber() { return this.lineNumber; } public void setLineNumber(int lineNumber) { this.lineNumber = lineNumber; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/ArgumentsNode.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.extension.NamedArguments; import io.pebbletemplates.pebble.extension.NodeVisitor; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; import java.util.HashMap; import java.util.List; import java.util.Map; public class ArgumentsNode implements Node { private final List namedArgs; private final List positionalArgs; private final int lineNumber; public ArgumentsNode(List positionalArgs, List namedArgs, int lineNumber) { this.positionalArgs = positionalArgs; this.namedArgs = namedArgs; this.lineNumber = lineNumber; } @Override public void accept(NodeVisitor visitor) { visitor.visit(this); } public List getNamedArgs() { return this.namedArgs; } public List getPositionalArgs() { return this.positionalArgs; } /** * Using hints from the filter/function/test/macro it will convert an ArgumentMap (which holds * both positional and named arguments) into a regular Map that the filter/function/test/macro is * expecting. * * @param self The template implementation * @param context The evaluation context * @param invocableWithNamedArguments The named arguments object * @return Returns a map representaion of the arguments */ public Map getArgumentMap(PebbleTemplateImpl self, EvaluationContextImpl context, NamedArguments invocableWithNamedArguments) { Map result = new HashMap<>(); List argumentNames = invocableWithNamedArguments.getArgumentNames(); if (argumentNames == null || argumentNames.isEmpty()) { /* Some functions such as min and max use un-named varags */ if (this.positionalArgs != null && !this.positionalArgs.isEmpty()) { for (int i = 0; i < this.positionalArgs.size(); i++) { result.put(String.valueOf(i), this.positionalArgs.get(i).getValueExpression().evaluate(self, context)); } } // Support dynamic varargs (Issue #740) if (this.namedArgs != null) { for (NamedArgumentNode arg: this.namedArgs) { Object value = arg.getValueExpression() == null ? null : arg.getValueExpression().evaluate(self, context); result.put(arg.getName(), value); } } } else { if (this.positionalArgs != null) { int nameIndex = 0; for (PositionalArgumentNode arg: this.positionalArgs) { if (argumentNames.size() <= nameIndex) { throw new PebbleException(null, "The argument at position " + (nameIndex + 1) + " is not allowed. Only " + argumentNames.size() + " argument(s) are allowed.", this.lineNumber, self.getName()); } result.put(argumentNames.get(nameIndex), arg.getValueExpression().evaluate(self, context)); nameIndex++; } } if (this.namedArgs != null) { for (NamedArgumentNode arg: this.namedArgs) { // check if user used an incorrect name if (!argumentNames.contains(arg.getName())) { throw new PebbleException(null, "The following named argument does not exist: " + arg.getName(), this.lineNumber, self.getName()); } Object value = arg.getValueExpression() == null ? null : arg.getValueExpression().evaluate(self, context); result.put(arg.getName(), value); } } } return result; } @Override public String toString() { return this.positionalArgs.toString(); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/AutoEscapeNode.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node; import io.pebbletemplates.pebble.extension.NodeVisitor; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; import java.io.IOException; import java.io.Writer; public class AutoEscapeNode extends AbstractRenderableNode { private final BodyNode body; private final String strategy; private final boolean active; public AutoEscapeNode(int lineNumber, BodyNode body, boolean active, String strategy) { super(lineNumber); this.body = body; this.strategy = strategy; this.active = active; } @Override public void render(PebbleTemplateImpl self, Writer writer, EvaluationContextImpl context) throws IOException { this.body.render(self, writer, context); } @Override public void accept(NodeVisitor visitor) { visitor.visit(this); } public BodyNode getBody() { return this.body; } public String getStrategy() { return this.strategy; } public boolean isActive() { return this.active; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/BlockNode.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node; import io.pebbletemplates.pebble.extension.NodeVisitor; import io.pebbletemplates.pebble.template.Block; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; import java.io.IOException; import java.io.Writer; public class BlockNode extends AbstractRenderableNode { private final BodyNode body; private String name; public BlockNode(int lineNumber, String name) { this(lineNumber, name, null); } public BlockNode(int lineNumber, String name, BodyNode body) { super(lineNumber); this.body = body; this.name = name; } @Override public void render(final PebbleTemplateImpl self, Writer writer, EvaluationContextImpl context) throws IOException { self.block(writer, context, this.name, false); } @Override public void accept(NodeVisitor visitor) { visitor.visit(this); } public Block getBlock() { return new Block() { @Override public String getName() { return BlockNode.this.name; } @Override public void evaluate(PebbleTemplateImpl self, Writer writer, EvaluationContextImpl context) throws IOException { BlockNode.this.body.render(self, writer, context); } }; } public BodyNode getBody() { return this.body; } public String getName() { return this.name; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/BodyNode.java ================================================ /* * This file is part of Pebble. *

* Copyright (c) 2014 by Mitchell Bösecke *

* For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node; import io.pebbletemplates.pebble.extension.NodeVisitor; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; import java.io.IOException; import java.io.Writer; import java.util.ArrayList; import java.util.List; public class BodyNode extends AbstractRenderableNode { private final List children; /** * When a template extends a parent template there are very few nodes in the child that should * actually get rendered such as set and import. All others should be ignored. */ private boolean onlyRenderInheritanceSafeNodes = false; public BodyNode(int lineNumber, List children) { super(lineNumber); this.children = children; } @Override public void render(PebbleTemplateImpl self, Writer writer, EvaluationContextImpl context) throws IOException { for (RenderableNode child: this.children) { if (this.onlyRenderInheritanceSafeNodes && context.getHierarchy().getParent() != null) { if (!nodesToRenderInChild.contains(child.getClass())) { continue; } } child.render(self, writer, context); } } @Override public void accept(NodeVisitor visitor) { visitor.visit(this); } public List getChildren() { return this.children; } public boolean isOnlyRenderInheritanceSafeNodes() { return this.onlyRenderInheritanceSafeNodes; } public void setOnlyRenderInheritanceSafeNodes(boolean onlyRenderInheritanceSafeNodes) { this.onlyRenderInheritanceSafeNodes = onlyRenderInheritanceSafeNodes; } private static List> nodesToRenderInChild = new ArrayList<>(); static { nodesToRenderInChild.add(SetNode.class); nodesToRenderInChild.add(ImportNode.class); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/CacheNode.java ================================================ /* * This file is part of Pebble. *

* Copyright (c) 2014 by Mitchell Bösecke *

* For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node; import io.pebbletemplates.pebble.cache.CacheKey; import io.pebbletemplates.pebble.cache.PebbleCache; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.extension.NodeVisitor; import io.pebbletemplates.pebble.node.expression.Expression; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; import io.pebbletemplates.pebble.utils.LimitedSizeWriter; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.util.concurrent.CompletionException; /** * Node for the cache tag * * @author Eric Bussieres */ public class CacheNode extends AbstractRenderableNode { private final BodyNode body; private final Expression name; public CacheNode(int lineNumber, Expression name, BodyNode body) { super(lineNumber); this.body = body; this.name = name; } @Override public void accept(NodeVisitor visitor) { visitor.visit(this); } @Override public void render(PebbleTemplateImpl self, Writer writer, EvaluationContextImpl context) throws IOException { try { final String body; PebbleCache tagCache = context.getTagCache(); CacheKey key = new CacheKey(this, (String) this.name.evaluate(self, context), context.getLocale()); body = (String) context.getTagCache().computeIfAbsent(key, k -> { try { return this.render(self, context); } catch (IOException e) { throw new RuntimeException(e); } }); writer.write(body); } catch (CompletionException e) { throw new PebbleException(e, "Could not render cache block [" + this.name + "]"); } } private String render(final PebbleTemplateImpl self, final EvaluationContextImpl context) throws IOException { Writer tempWriter = LimitedSizeWriter.from(new StringWriter(), context); CacheNode.this.body.render(self, tempWriter, context); return tempWriter.toString(); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/EmbedNode.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.extension.NodeVisitor; import io.pebbletemplates.pebble.node.expression.Expression; import io.pebbletemplates.pebble.node.expression.MapExpression; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; import java.io.IOException; import java.io.Writer; import java.util.Collections; import java.util.List; import java.util.Map; public class EmbedNode extends AbstractRenderableNode { private final Expression includeExpression; private final MapExpression mapExpression; private final List nodes; public EmbedNode(int lineNumber, Expression includeExpression, MapExpression mapExpression, List nodes) { super(lineNumber); this.includeExpression = includeExpression; this.mapExpression = mapExpression; this.nodes = nodes; } @Override public void render(PebbleTemplateImpl self, Writer writer, EvaluationContextImpl context) throws IOException { String templateName = (String) this.includeExpression.evaluate(self, context); Map map = Collections.emptyMap(); if (this.mapExpression != null) { map = this.mapExpression.evaluate(self, context); } if (templateName == null) { throw new PebbleException( null, "The template name in an embed tag evaluated to NULL. If the template name is static, make sure to wrap it in quotes.", this.getLineNumber(), self.getName()); } self.embedTemplate(getLineNumber(), writer, context, templateName, map, nodes); } @Override public void accept(NodeVisitor visitor) { visitor.visit(this); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/ExtendsNode.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node; import io.pebbletemplates.pebble.extension.NodeVisitor; import io.pebbletemplates.pebble.node.expression.Expression; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; import java.io.Writer; public class ExtendsNode extends AbstractRenderableNode { Expression parentExpression; public ExtendsNode(int lineNumber, Expression parentExpression) { super(lineNumber); this.parentExpression = parentExpression; } @Override public void render(final PebbleTemplateImpl self, Writer writer, final EvaluationContextImpl context) { self.setParent(context, (String) this.parentExpression.evaluate(self, context)); } @Override public void accept(NodeVisitor visitor) { visitor.visit(this); } public Expression getParentExpression() { return this.parentExpression; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/FlushNode.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node; import io.pebbletemplates.pebble.extension.NodeVisitor; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; import java.io.IOException; import java.io.Writer; public class FlushNode extends AbstractRenderableNode { public FlushNode(int lineNumber) { super(lineNumber); } @Override public void render(PebbleTemplateImpl self, Writer writer, EvaluationContextImpl context) throws IOException { writer.flush(); } @Override public void accept(NodeVisitor visitor) { visitor.visit(this); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/ForNode.java ================================================ /* * This file is part of Pebble. *

* Copyright (c) 2014 by Mitchell Bösecke *

* For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.extension.NodeVisitor; import io.pebbletemplates.pebble.node.expression.Expression; import io.pebbletemplates.pebble.node.fornode.LazyLength; import io.pebbletemplates.pebble.node.fornode.LazyRevIndex; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; import io.pebbletemplates.pebble.template.ScopeChain; import java.io.IOException; import java.io.Writer; import java.lang.reflect.Array; import java.util.Enumeration; import java.util.Iterator; import java.util.Map; /** * Represents a "for" loop within the template. * * @author mbosecke */ public class ForNode extends AbstractRenderableNode { private final String variableName; private final Expression iterableExpression; private final BodyNode body; private final BodyNode elseBody; public ForNode(int lineNumber, String variableName, Expression iterableExpression, BodyNode body, BodyNode elseBody) { super(lineNumber); this.variableName = variableName; this.iterableExpression = iterableExpression; this.body = body; this.elseBody = elseBody; } public static class LoopVariables { private boolean first, last; private LazyLength length; private int index; private LazyRevIndex revindex; @Override public String toString() { return "{last=" + last + ", length=" + length + ", index=" + index + ", revindex=" + revindex + ", first=" + first + "}"; } public boolean isFirst() { return first; } public boolean isLast() { return last; } public LazyLength getLength() { return length; } public int getIndex() { return index; } public LazyRevIndex getRevindex() { return revindex; } } @Override public void render(PebbleTemplateImpl self, Writer writer, EvaluationContextImpl context) throws IOException { final Object iterableEvaluation = this.iterableExpression.evaluate(self, context); Iterable iterable; if (iterableEvaluation == null) { return; } iterable = this.toIterable(iterableEvaluation); if (iterable == null) { throw new PebbleException(null, "Not an iterable object. Value = [" + iterableEvaluation.toString() + "]", this.getLineNumber(), self.getName()); } Iterator iterator = iterable.iterator(); if (iterator.hasNext()) { ScopeChain scopeChain = context.getScopeChain(); scopeChain.pushScope(); LazyLength length = new LazyLength(iterableEvaluation); int index = 0; LoopVariables loop = null; boolean usingExecutorService = context.getExecutorService() != null; while (iterator.hasNext()) { /* * If the user is using an executor service (i.e. parallel * node), we must create a new map with every iteration instead * of re-using the same one; it's imperative that each thread * would get it's own distinct copy of the context. */ if (index == 0 || usingExecutorService) { loop = new LoopVariables(); loop.first = index == 0; loop.last = !iterator.hasNext(); loop.length = length; } else if (index == 1) { // second iteration loop.first = false; } loop.revindex = new LazyRevIndex(index, length); loop.index = index++; scopeChain.put("loop", loop); scopeChain.put(this.variableName, iterator.next()); // last iteration if (!iterator.hasNext()) { loop.last = true; } this.body.render(self, writer, context); } scopeChain.popScope(); } else if (this.elseBody != null) { this.elseBody.render(self, writer, context); } } @Override public void accept(NodeVisitor visitor) { visitor.visit(this); } public String getIterationVariable() { return this.variableName; } public Expression getIterable() { return this.iterableExpression; } public BodyNode getBody() { return this.body; } public BodyNode getElseBody() { return this.elseBody; } @SuppressWarnings({"unchecked", "rawtypes"}) private Iterable toIterable(final Object obj) { Iterable result = null; if (obj instanceof Iterable) { result = (Iterable) obj; } else if (obj instanceof Map) { // raw type result = ((Map) obj).entrySet(); } else if (obj.getClass().isArray()) { result = new ArrayIterable(obj); } else if (obj instanceof Enumeration) { result = new EnumerationIterable((Enumeration) obj); } return result; } /** * Adapts an array to an Iterable */ private class ArrayIterable implements Iterable { private Object obj; ArrayIterable(Object array) { this.obj = array; } @Override public Iterator iterator() { return new Iterator() { private int index = 0; private final int length = Array.getLength(ArrayIterable.this.obj); @Override public boolean hasNext() { return this.index < this.length; } @Override public Object next() { return Array.get(ArrayIterable.this.obj, this.index++); } @Override public void remove() { throw new UnsupportedOperationException(); } }; } } /** * Adapts an Enumeration to an Iterable */ private class EnumerationIterable implements Iterable { private Enumeration obj; EnumerationIterable(Enumeration enumeration) { this.obj = enumeration; } @Override public Iterator iterator() { return new Iterator() { @Override public boolean hasNext() { return EnumerationIterable.this.obj.hasMoreElements(); } @Override public Object next() { return EnumerationIterable.this.obj.nextElement(); } @Override public void remove() { throw new UnsupportedOperationException(); } }; } } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/FromNode.java ================================================ package io.pebbletemplates.pebble.node; import io.pebbletemplates.pebble.extension.NodeVisitor; import io.pebbletemplates.pebble.node.expression.Expression; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; import io.pebbletemplates.pebble.utils.Pair; import java.io.Writer; import java.util.List; /** * From Node for * *

{% from "templateName" import macroName as alias %}

* * @author yanxiyue */ public class FromNode extends AbstractRenderableNode { private final Expression fromExpression; private final List> namedMacros; public FromNode(int lineNumber, Expression fromExpression, List> namedMacros) { super(lineNumber); this.fromExpression = fromExpression; this.namedMacros = namedMacros; } @Override public void render(PebbleTemplateImpl self, Writer writer, EvaluationContextImpl context) { String templateName = (String) fromExpression.evaluate(self, context); self.importNamedMacrosFromTemplate(templateName, namedMacros); } @Override public void accept(NodeVisitor visitor) { visitor.visit(this); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/FunctionOrMacroNameNode.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node; import io.pebbletemplates.pebble.extension.NodeVisitor; import io.pebbletemplates.pebble.node.expression.Expression; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; public class FunctionOrMacroNameNode implements Expression { private final String name; private final int lineNumber; public FunctionOrMacroNameNode(String name, int lineNumber) { this.name = name; this.lineNumber = lineNumber; } @Override public String evaluate(PebbleTemplateImpl self, EvaluationContextImpl context) { throw new UnsupportedOperationException(); } @Override public void accept(NodeVisitor visitor) { visitor.visit(this); } public String getName() { return this.name; } @Override public int getLineNumber() { return this.lineNumber; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/IfNode.java ================================================ /* * This file is part of Pebble. *

* Copyright (c) 2014 by Mitchell Bösecke *

* For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.extension.NodeVisitor; import io.pebbletemplates.pebble.node.expression.Expression; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; import io.pebbletemplates.pebble.utils.Pair; import io.pebbletemplates.pebble.utils.TypeUtils; import java.io.IOException; import java.io.Writer; import java.util.List; import static io.pebbletemplates.pebble.utils.TypeUtils.compatibleCast; public class IfNode extends AbstractRenderableNode { private final List, BodyNode>> conditionsWithBodies; private final BodyNode elseBody; public IfNode(int lineNumber, List, BodyNode>> conditionsWithBodies) { this(lineNumber, conditionsWithBodies, null); } public IfNode(int lineNumber, List, BodyNode>> conditionsWithBodies, BodyNode elseBody) { super(lineNumber); this.conditionsWithBodies = conditionsWithBodies; this.elseBody = elseBody; } @Override public void render(PebbleTemplateImpl self, Writer writer, EvaluationContextImpl context) throws IOException { boolean satisfied = false; for (Pair, BodyNode> ifStatement: this.conditionsWithBodies) { Expression conditionalExpression = ifStatement.getLeft(); try { Object result = conditionalExpression.evaluate(self, context); if (result != null) { if (result instanceof Boolean || result instanceof Number || result instanceof String) { satisfied = TypeUtils.compatibleCast(result, Boolean.class); } else { throw new PebbleException( null, String.format( "Unsupported value type %s. Expected Boolean, String, Number in \"if\" statement", result.getClass().getSimpleName()), this.getLineNumber(), self.getName()); } } else if (context.isStrictVariables()) { throw new PebbleException(null, "null value given to if statement and strict variables is set to true", this.getLineNumber(), self.getName()); } } catch (RuntimeException ex) { throw new PebbleException(ex, "Wrong operand(s) type in conditional expression", this.getLineNumber(), self.getName()); } if (satisfied) { ifStatement.getRight().render(self, writer, context); break; } } if (!satisfied && this.elseBody != null) { this.elseBody.render(self, writer, context); } } @Override public void accept(NodeVisitor visitor) { visitor.visit(this); } public List, BodyNode>> getConditionsWithBodies() { return this.conditionsWithBodies; } public BodyNode getElseBody() { return this.elseBody; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/ImportNode.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node; import io.pebbletemplates.pebble.extension.NodeVisitor; import io.pebbletemplates.pebble.node.expression.Expression; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.MacroAttributeProvider; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; import java.io.Writer; public class ImportNode extends AbstractRenderableNode { private final Expression importExpression; private final String alias; public ImportNode(int lineNumber, Expression importExpression, String alias) { super(lineNumber); this.importExpression = importExpression; this.alias = alias; } @Override public void render(PebbleTemplateImpl self, Writer writer, EvaluationContextImpl context) { String templateName = (String) this.importExpression.evaluate(self, context); if (this.alias != null) { self.importNamedTemplate(context, templateName, this.alias); // put the imported template into scope PebbleTemplateImpl template = self.getNamedImportedTemplate(context, this.alias); context.getScopeChain().put(this.alias, new MacroAttributeProvider(template)); } else { self.importTemplate(context, templateName); } } @Override public void accept(NodeVisitor visitor) { visitor.visit(this); } public Expression getImportExpression() { return this.importExpression; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/IncludeNode.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.extension.NodeVisitor; import io.pebbletemplates.pebble.node.expression.Expression; import io.pebbletemplates.pebble.node.expression.MapExpression; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; import java.io.IOException; import java.io.Writer; import java.util.Collections; import java.util.Map; public class IncludeNode extends AbstractRenderableNode { private final Expression includeExpression; private final MapExpression mapExpression; public IncludeNode(int lineNumber, Expression includeExpression, MapExpression mapExpression) { super(lineNumber); this.includeExpression = includeExpression; this.mapExpression = mapExpression; } @Override public void render(PebbleTemplateImpl self, Writer writer, EvaluationContextImpl context) throws IOException { String templateName = (String) this.includeExpression.evaluate(self, context); Map map = Collections.emptyMap(); if (this.mapExpression != null) { map = this.mapExpression.evaluate(self, context); } if (templateName == null) { throw new PebbleException( null, "The template name in an include tag evaluated to NULL. If the template name is static, make sure to wrap it in quotes.", this.getLineNumber(), self.getName()); } self.includeTemplate(writer, context, templateName, map); } @Override public void accept(NodeVisitor visitor) { visitor.visit(this); } public Expression getIncludeExpression() { return this.includeExpression; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/MacroNode.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node; import io.pebbletemplates.pebble.extension.NodeVisitor; import io.pebbletemplates.pebble.node.expression.Expression; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.Macro; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; import io.pebbletemplates.pebble.template.ScopeChain; import io.pebbletemplates.pebble.utils.LimitedSizeWriter; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.util.ArrayList; import java.util.List; import java.util.Map; public class MacroNode extends AbstractRenderableNode { private final String name; private final ArgumentsNode args; private final BodyNode body; public MacroNode(String name, ArgumentsNode args, BodyNode body) { this.name = name; this.args = args; this.body = body; } @Override public void render(PebbleTemplateImpl self, Writer writer, EvaluationContextImpl context) { // do nothing } @Override public void accept(NodeVisitor visitor) { visitor.visit(this); } public Macro getMacro() { return new Macro() { @Override public List getArgumentNames() { List names = new ArrayList<>(); for (NamedArgumentNode arg: MacroNode.this.getArgs().getNamedArgs()) { names.add(arg.getName()); } return names; } @Override public String getName() { return MacroNode.this.name; } @Override public String call(PebbleTemplateImpl self, EvaluationContextImpl context, Map macroArgs) { Writer writer = LimitedSizeWriter.from(new StringWriter(), context); ScopeChain scopeChain = context.getScopeChain(); // scope for default arguments scopeChain.pushLocalScope(); // global vars provided by extensions context.getExtensionRegistry().getGlobalVariables().forEach(scopeChain::put); for (NamedArgumentNode arg: MacroNode.this.getArgs().getNamedArgs()) { Expression valueExpression = arg.getValueExpression(); if (valueExpression == null) { scopeChain.put(arg.getName(), null); } else { scopeChain.put(arg.getName(), arg.getValueExpression().evaluate(self, context)); } } // scope for user provided arguments scopeChain.pushScope(macroArgs); try { MacroNode.this.getBody().render(self, writer, context); } catch (IOException e) { throw new RuntimeException("Could not evaluate macro [" + MacroNode.this.name + "]", e); } scopeChain.popScope(); // user arguments scopeChain.popScope(); // default arguments return writer.toString(); } }; } public BodyNode getBody() { return this.body; } public ArgumentsNode getArgs() { return this.args; } public String getName() { return this.name; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/NamedArgumentNode.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node; import io.pebbletemplates.pebble.extension.NodeVisitor; import io.pebbletemplates.pebble.node.expression.Expression; public class NamedArgumentNode implements Node { private final Expression value; private final String name; public NamedArgumentNode(String name, Expression value) { this.name = name; this.value = value; } @Override public void accept(NodeVisitor visitor) { visitor.visit(this); } public Expression getValueExpression() { return this.value; } public String getName() { return this.name; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/Node.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node; import io.pebbletemplates.pebble.extension.NodeVisitor; public interface Node { void accept(NodeVisitor visitor); } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/ParallelNode.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node; import io.pebbletemplates.pebble.extension.NodeVisitor; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; import io.pebbletemplates.pebble.utils.FutureWriter; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class ParallelNode extends AbstractRenderableNode { private final Logger logger = LoggerFactory.getLogger(ParallelNode.class); private final BodyNode body; /** * If the user is using the parallel tag but doesn't provide an ExecutorService we will warn them * that this tag will essentially be ignored but it's important that we only warn them once * because this tag may show up in a loop. */ private boolean hasWarnedAboutNonExistingExecutorService = false; public ParallelNode(int lineNumber, BodyNode body) { super(lineNumber); this.body = body; } @Override public void render(final PebbleTemplateImpl self, Writer writer, final EvaluationContextImpl context) throws IOException { ExecutorService es = context.getExecutorService(); if (es == null) { if (!this.hasWarnedAboutNonExistingExecutorService) { this.logger.info(String.format( "The parallel tag was used [%s:%d] but no ExecutorService was provided. The parallel tag will be ignored " + "and it's contents will be rendered in sequence with the rest of the template.", self.getName(), this.getLineNumber())); this.hasWarnedAboutNonExistingExecutorService = true; } /* * If user did not provide an ExecutorService, we simply ignore the * parallel tag and render it's contents like we normally would. */ this.body.render(self, writer, context); } else { final EvaluationContextImpl contextCopy = context.threadSafeCopy(self); final StringWriter newStringWriter = new StringWriter(); final Writer newFutureWriter = new FutureWriter(newStringWriter); Future future = es.submit(() -> { this.body.render(self, newFutureWriter, contextCopy); newFutureWriter.flush(); newFutureWriter.close(); return newStringWriter.toString(); }); ((FutureWriter) writer).enqueue(future); } } @Override public void accept(NodeVisitor visitor) { visitor.visit(this); } public BodyNode getBody() { return this.body; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/PositionalArgumentNode.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node; import io.pebbletemplates.pebble.extension.NodeVisitor; import io.pebbletemplates.pebble.node.expression.Expression; public class PositionalArgumentNode implements Node { private final Expression value; public PositionalArgumentNode(Expression value) { this.value = value; } @Override public void accept(NodeVisitor visitor) { visitor.visit(this); } public Expression getValueExpression() { return this.value; } @Override public String toString() { return this.value.toString(); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/PrintNode.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.extension.NodeVisitor; import io.pebbletemplates.pebble.node.expression.Expression; import io.pebbletemplates.pebble.extension.writer.SpecializedWriter; import io.pebbletemplates.pebble.extension.writer.StringWriterSpecializedAdapter; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; import io.pebbletemplates.pebble.utils.StringUtils; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; public class PrintNode extends AbstractRenderableNode { private Expression expression; public PrintNode(Expression expression, int lineNumber) { super(lineNumber); this.expression = expression; } @Override public void render(PebbleTemplateImpl self, Writer writer, EvaluationContextImpl context) throws IOException, PebbleException { Object var = this.expression.evaluate(self, context); if (var != null) { if (writer instanceof StringWriter) { new StringWriterSpecializedAdapter((StringWriter) writer).write(var); } else if (writer instanceof SpecializedWriter) { ((SpecializedWriter) writer).write(var); } else { writer.write(StringUtils.toString(var)); } } } @Override public void accept(NodeVisitor visitor) { visitor.visit(this); } public Expression getExpression() { return this.expression; } public void setExpression(Expression expression) { this.expression = expression; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/RenderableNode.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; import java.io.IOException; import java.io.Writer; public interface RenderableNode extends Node { void render(PebbleTemplateImpl self, Writer writer, EvaluationContextImpl context) throws IOException; } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/RootNode.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node; import io.pebbletemplates.pebble.extension.NodeVisitor; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; import java.io.IOException; import java.io.Writer; public class RootNode extends AbstractRenderableNode { private final BodyNode body; public RootNode(BodyNode body) { super(0); this.body = body; } @Override public void render(PebbleTemplateImpl self, Writer writer, EvaluationContextImpl context) throws IOException { this.body.setOnlyRenderInheritanceSafeNodes(true); this.body.render(self, writer, context); } @Override public void accept(NodeVisitor visitor) { visitor.visit(this); } public BodyNode getBody() { return this.body; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/SetNode.java ================================================ /* * This file is part of Pebble. *

* Copyright (c) 2014 by Mitchell Bösecke *

* For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node; import io.pebbletemplates.pebble.extension.NodeVisitor; import io.pebbletemplates.pebble.node.expression.Expression; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; import java.io.Writer; public class SetNode extends AbstractRenderableNode { private final String name; private final Expression value; public SetNode(int lineNumber, String name, Expression value) { super(lineNumber); this.name = name; this.value = value; } @Override public void render(PebbleTemplateImpl self, Writer writer, EvaluationContextImpl context) { context.getScopeChain().set(this.name, this.value.evaluate(self, context)); } @Override public void accept(NodeVisitor visitor) { visitor.visit(this); } public Expression getValue() { return this.value; } public String getName() { return this.name; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/TestInvocationExpression.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node; import io.pebbletemplates.pebble.extension.NodeVisitor; import io.pebbletemplates.pebble.node.expression.Expression; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; /** * The right hand side to the test expression. * * @author Mitchell */ public class TestInvocationExpression implements Expression { private final String testName; private final ArgumentsNode args; private final int lineNumber; @Override public Object evaluate(PebbleTemplateImpl self, EvaluationContextImpl context) { throw new UnsupportedOperationException(); } public TestInvocationExpression(int lineNumber, String testName, ArgumentsNode args) { this.testName = testName; this.args = args; this.lineNumber = lineNumber; } @Override public void accept(NodeVisitor visitor) { visitor.visit(this); } public ArgumentsNode getArgs() { return this.args; } public String getTestName() { return this.testName; } @Override public int getLineNumber() { return this.lineNumber; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/TextNode.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node; import io.pebbletemplates.pebble.extension.NodeVisitor; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; import java.io.IOException; import java.io.Writer; /** * Represents static text in a template. * * @author mbosecke */ public class TextNode extends AbstractRenderableNode { /** * Most Writers will convert strings to char[] so we might as well store it as a char[] to begin * with; small performance optimization. */ private final char[] data; public TextNode(String text, int lineNumber) { super(lineNumber); int length = text.length(); this.data = new char[text.length()]; text.getChars(0, length, this.data, 0); } @Override public void render(PebbleTemplateImpl self, Writer writer, EvaluationContextImpl context) throws IOException { writer.write(this.data); } @Override public void accept(NodeVisitor visitor) { visitor.visit(this); } public char[] getData() { return this.data; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/expression/AddExpression.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node.expression; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; import io.pebbletemplates.pebble.utils.OperatorUtils; public class AddExpression extends BinaryExpression { @Override public Object evaluate(PebbleTemplateImpl self, EvaluationContextImpl context) { try { return OperatorUtils.add(this.getLeftExpression().evaluate(self, context), this.getRightExpression().evaluate(self, context)); } catch (Exception ex) { throw new PebbleException(ex, "Could not perform addition", this.getLineNumber(), self.getName()); } } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/expression/AndExpression.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node.expression; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; import static io.pebbletemplates.pebble.utils.TypeUtils.compatibleCast; public class AndExpression extends BinaryExpression { @SuppressWarnings("unchecked") @Override public Boolean evaluate(PebbleTemplateImpl self, EvaluationContextImpl context) { Expression leftExpression = (Expression) this.getLeftExpression(); boolean left = this.evaluateExpression(self, context, leftExpression); if (left) { Expression rightExpression = (Expression) this.getRightExpression(); return this.evaluateExpression(self, context, rightExpression); } return false; } private boolean evaluateExpression(PebbleTemplateImpl self, EvaluationContextImpl context, Expression expression) { Boolean evaluatedExpression = compatibleCast(expression.evaluate(self, context), Boolean.class); if (evaluatedExpression == null) { if (context.isStrictVariables()) { throw new PebbleException(null, "null value used in and operator and strict variables is set to true", this.getLineNumber(), self.getName()); } return false; } return evaluatedExpression; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/expression/ArrayExpression.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node.expression; import io.pebbletemplates.pebble.extension.NodeVisitor; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; import java.util.ArrayList; import java.util.Collections; import java.util.List; public class ArrayExpression implements Expression> { private final List> values; private final int lineNumber; public ArrayExpression(int lineNumber) { this.values = Collections.emptyList(); this.lineNumber = lineNumber; } public ArrayExpression(List> values, int lineNumber) { if (values == null) { this.values = Collections.emptyList(); } else { this.values = values; } this.lineNumber = lineNumber; } @Override public void accept(NodeVisitor visitor) { visitor.visit(this); } @Override public List evaluate(PebbleTemplateImpl self, EvaluationContextImpl context) { List returnValues = new ArrayList<>(this.values.size()); for (int i = 0; i < this.values.size(); i++) { Expression expr = this.values.get(i); Object value = expr == null ? null : expr.evaluate(self, context); returnValues.add(value); } return returnValues; } public List> getValues() { return this.values; } @Override public int getLineNumber() { return this.lineNumber; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/expression/BinaryExpression.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node.expression; import io.pebbletemplates.pebble.extension.NodeVisitor; public abstract class BinaryExpression implements Expression { private int lineNumber; public BinaryExpression() { } /** * Sets the left and right expressions. This expression is assumed to be defined on the same line * as the left expression. */ public BinaryExpression(Expression left, Expression right) { this.setLeft(left); this.setRight(right); this.setLineNumber(left.getLineNumber()); } private Expression leftExpression; private Expression rightExpression; public void setLeft(Expression left) { this.leftExpression = left; } public void setRight(Expression right) { this.rightExpression = right; } public Expression getLeftExpression() { return this.leftExpression; } public Expression getRightExpression() { return this.rightExpression; } @Override public void accept(NodeVisitor visitor) { visitor.visit(this); } /** * Sets the line number on which the expression is defined on. * * @param lineNumber the line number on which the expression is defined on. */ public void setLineNumber(int lineNumber) { this.lineNumber = lineNumber; } @Override public int getLineNumber() { return this.lineNumber; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/expression/BlockFunctionExpression.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node.expression; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.extension.NodeVisitor; import io.pebbletemplates.pebble.node.ArgumentsNode; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; public class BlockFunctionExpression implements Expression { private final Expression blockNameExpression; private final int lineNumber; public BlockFunctionExpression(ArgumentsNode args, int lineNumber) { this.blockNameExpression = args.getPositionalArgs().get(0).getValueExpression(); this.lineNumber = lineNumber; } @Override public String evaluate(PebbleTemplateImpl self, EvaluationContextImpl context) { Writer writer = new StringWriter(); String blockName = (String) this.blockNameExpression.evaluate(self, context); try { self.block(writer, context, blockName, false); } catch (IOException e) { throw new PebbleException(e, "Could not render block [" + blockName + "]", this.getLineNumber(), self.getName()); } return writer.toString(); } @Override public void accept(NodeVisitor visitor) { visitor.visit(this); } public Expression getBlockNameExpression() { return this.blockNameExpression; } @Override public int getLineNumber() { return this.lineNumber; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/expression/ConcatenateExpression.java ================================================ package io.pebbletemplates.pebble.node.expression; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; /** * Expression which implements the string concatenation. * * @author Thomas Hunziker */ public class ConcatenateExpression extends BinaryExpression { public ConcatenateExpression() { } public ConcatenateExpression(Expression left, Expression right) { super(left, right); } @Override public String evaluate(PebbleTemplateImpl self, EvaluationContextImpl context) { Object left = getLeftExpression().evaluate(self, context); Object right = getRightExpression().evaluate(self, context); StringBuilder result = new StringBuilder(); if (left != null) { result.append(left.toString()); } if (right != null) { result.append(right.toString()); } return result.toString(); } @Override public String toString() { return String.format("%s + %s", getLeftExpression(), getRightExpression()); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/expression/ContainsExpression.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node.expression; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; import java.util.Collection; import java.util.Map; public class ContainsExpression extends BinaryExpression { @SuppressWarnings({"rawtypes", "unchecked"}) @Override public Boolean evaluate(PebbleTemplateImpl self, EvaluationContextImpl context) { Object leftValue = this.getLeftExpression().evaluate(self, context); if (leftValue == null) { return false; } Object rightValue = this.getRightExpression().evaluate(self, context); if (leftValue instanceof Collection) { if (rightValue instanceof Collection) { return ((Collection) leftValue).containsAll((Collection) rightValue); } else { return ((Collection) leftValue).contains(rightValue); } } else if (leftValue instanceof Map) { return ((Map) leftValue).containsKey(rightValue); } else if (leftValue.getClass().isArray()) { return arrayContains(leftValue, rightValue); } else if (leftValue instanceof String) { return leftValue.toString().contains(String.valueOf(rightValue)); } else { throw new PebbleException(null, "Contains operator can only be used on Collections, Maps and arrays. Actual type was: " + leftValue.getClass().getName(), this.getLineNumber(), self.getName()); } } // FIXME is this right? does it make sense to support? private static boolean arrayContains(Object input, Object value) { if (input instanceof Object[]) { return containsObject((Object[]) input, value); } else if (input instanceof boolean[]) { return containsBoolean((boolean[]) input, value); } else if (input instanceof byte[]) { return containsByte((byte[]) input, value); } else if (input instanceof char[]) { return containsChar((char[]) input, value); } else if (input instanceof double[]) { return containsDouble((double[]) input, value); } else if (input instanceof float[]) { return containsFloat((float[]) input, value); } else if (input instanceof int[]) { return containsInt((int[]) input, value); } else if (input instanceof long[]) { return containsLong((long[]) input, value); } else { return containsShort((short[]) input, value); } } private static boolean containsObject(Object[] array, Object value) { for (Object o : array) { if (value == o || (value != null && value.equals(o))) { return true; } } return false; } private static boolean containsBoolean(boolean[] array, Object value) { if (!(value instanceof Boolean)) { return false; } for (boolean b : array) { if (b == (Boolean) value) { return true; } } return false; } private static boolean containsByte(byte[] array, Object value) { if (!(value instanceof Byte)) { return false; } for (byte b : array) { if (b == (Byte) value) { return true; } } return false; } private static boolean containsChar(char[] array, Object value) { if (!(value instanceof Character)) { return false; } for (char c : array) { if (c == (Character) value) { return true; } } return false; } private static boolean containsDouble(double[] array, Object value) { if (!(value instanceof Double)) { return false; } for (double d : array) { if (d == (Double) value) { return true; } } return false; } private static boolean containsFloat(float[] array, Object value) { if (!(value instanceof Float)) { return false; } for (float f : array) { if (f == (Float) value) { return true; } } return false; } private static boolean containsInt(int[] array, Object value) { if (!(value instanceof Integer)) { return false; } for (int i : array) { if (i == (Integer) value) { return true; } } return false; } private static boolean containsLong(long[] array, Object value) { if (!(value instanceof Long)) { return false; } for (long l : array) { if (l == (Long) value) { return true; } } return false; } private static boolean containsShort(short[] array, Object value) { if (!(value instanceof Short)) { return false; } for (short s : array) { if (s == (Short) value) { return true; } } return false; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/expression/ContextVariableExpression.java ================================================ /* * This file is part of Pebble. *

* Copyright (c) 2014 by Mitchell Bösecke *

* For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node.expression; import io.pebbletemplates.pebble.error.RootAttributeNotFoundException; import io.pebbletemplates.pebble.extension.NodeVisitor; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; import io.pebbletemplates.pebble.template.ScopeChain; public class ContextVariableExpression implements Expression { protected final String name; private final int lineNumber; public ContextVariableExpression(String name, int lineNumber) { this.name = name; this.lineNumber = lineNumber; } @Override public void accept(NodeVisitor visitor) { visitor.visit(this); } public String getName() { return this.name; } @Override public Object evaluate(PebbleTemplateImpl self, EvaluationContextImpl context) { ScopeChain scopeChain = context.getScopeChain(); Object result = scopeChain.get(this.name); if (result == null && context.isStrictVariables() && !scopeChain.containsKey(this.name)) { throw new RootAttributeNotFoundException(null, String.format( "Root attribute [%s] does not exist or can not be accessed and strict variables is set to true.", this.name), this.name, this.lineNumber, self.getName()); } return result; } @Override public int getLineNumber() { return this.lineNumber; } @Override public String toString() { return String.format("[%s]", this.name); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/expression/DivideExpression.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node.expression; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; import io.pebbletemplates.pebble.utils.OperatorUtils; public class DivideExpression extends BinaryExpression { @Override public Object evaluate(PebbleTemplateImpl self, EvaluationContextImpl context) { try { return OperatorUtils.divide(this.getLeftExpression().evaluate(self, context), this.getRightExpression().evaluate(self, context)); } catch (Exception ex) { throw new PebbleException(ex, "Could not perform division", this.getLineNumber(), self.getName()); } } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/expression/EqualsExpression.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node.expression; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; import io.pebbletemplates.pebble.utils.OperatorUtils; public class EqualsExpression extends BinaryExpression { @Override public Boolean evaluate(PebbleTemplateImpl self, EvaluationContextImpl context) { try { return OperatorUtils.equals(this.getLeftExpression().evaluate(self, context), this.getRightExpression().evaluate(self, context)); } catch (Exception ex) { throw new PebbleException(ex, "Could not perform equals comparison", this.getLineNumber(), self.getName()); } } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/expression/Expression.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node.expression; import io.pebbletemplates.pebble.node.Node; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; public interface Expression extends Node { T evaluate(PebbleTemplateImpl self, EvaluationContextImpl context); /** * Returns the line number on which the expression is defined on. * * @return the line number on which the expression is defined on. */ int getLineNumber(); } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/expression/FilterExpression.java ================================================ /* * This file is part of Pebble. *

* Copyright (c) 2014 by Mitchell Bösecke *

* For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node.expression; import io.pebbletemplates.pebble.extension.Filter; import io.pebbletemplates.pebble.extension.core.DefaultFilter; import io.pebbletemplates.pebble.error.AttributeNotFoundException; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.extension.escaper.EscapeFilter; import io.pebbletemplates.pebble.extension.escaper.SafeString; import io.pebbletemplates.pebble.node.ArgumentsNode; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; import java.util.Map; public class FilterExpression extends BinaryExpression { /** * Save the filter instance on the first evaluation. */ private Filter filter = null; public FilterExpression() { super(); } @Override public Object evaluate(PebbleTemplateImpl self, EvaluationContextImpl context) { FilterInvocationExpression filterInvocation = (FilterInvocationExpression) this .getRightExpression(); ArgumentsNode args = filterInvocation.getArgs(); String filterName = filterInvocation.getFilterName(); if (this.filter == null) { this.filter = context.getExtensionRegistry().getFilter(filterInvocation.getFilterName()); } if (this.filter == null) { throw new PebbleException(null, String.format("Filter [%s] does not exist.", filterName), this.getLineNumber(), self.getName()); } Map namedArguments = args.getArgumentMap(self, context, this.filter); // This check is not nice, because we use instanceof. However this is // the only filter which should not fail in strict mode, when the variable // is not set, because this method should exactly test this. Hence a // generic solution to allow other tests to reuse this feature make no sense Object input; if (this.filter instanceof DefaultFilter) { try { input = this.getLeftExpression().evaluate(self, context); } catch (AttributeNotFoundException ex) { input = null; } } else { input = this.getLeftExpression().evaluate(self, context); } if (input instanceof SafeString && !(this.filter instanceof EscapeFilter)) { input = input.toString(); } return this.filter.apply(input, namedArguments, self, context, this.getLineNumber()); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/expression/FilterInvocationExpression.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node.expression; import io.pebbletemplates.pebble.extension.NodeVisitor; import io.pebbletemplates.pebble.node.ArgumentsNode; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; /** * The right hand side to the filter expression. * * @author Mitchell */ public class FilterInvocationExpression implements Expression { private final String filterName; private final ArgumentsNode args; private final int lineNumber; public FilterInvocationExpression(String filterName, ArgumentsNode args, int lineNumber) { this.filterName = filterName; this.args = args; this.lineNumber = lineNumber; } @Override public Object evaluate(PebbleTemplateImpl self, EvaluationContextImpl context) { // see FilterExpression.java throw new UnsupportedOperationException(); } public void accept(NodeVisitor visitor) { visitor.visit(this); } public ArgumentsNode getArgs() { return this.args; } public String getFilterName() { return this.filterName; } @Override public int getLineNumber() { return this.lineNumber; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/expression/FunctionOrMacroInvocationExpression.java ================================================ /* * This file is part of Pebble. *

* Copyright (c) 2014 by Mitchell Bösecke *

* For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node.expression; import io.pebbletemplates.pebble.extension.Function; import io.pebbletemplates.pebble.extension.NodeVisitor; import io.pebbletemplates.pebble.node.ArgumentsNode; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; import java.util.Map; public class FunctionOrMacroInvocationExpression implements Expression { private final String functionName; private final ArgumentsNode args; private final int lineNumber; public FunctionOrMacroInvocationExpression(String functionName, ArgumentsNode arguments, int lineNumber) { this.functionName = functionName; this.args = arguments; this.lineNumber = lineNumber; } @Override public Object evaluate(PebbleTemplateImpl self, EvaluationContextImpl context) { Function function = context.getExtensionRegistry().getFunction(this.functionName); if (function != null) { return this.applyFunction(self, context, function, this.args); } return self.macro(context, this.functionName, this.args, false, this.lineNumber); } private Object applyFunction(PebbleTemplateImpl self, EvaluationContextImpl context, Function function, ArgumentsNode args) { Map namedArguments = args.getArgumentMap(self, context, function); return function.execute(namedArguments, self, context, this.getLineNumber()); } @Override public void accept(NodeVisitor visitor) { visitor.visit(this); } public String getFunctionName() { return this.functionName; } public ArgumentsNode getArguments() { return this.args; } @Override public int getLineNumber() { return this.lineNumber; } @Override public String toString() { return String.format("%s%s", this.functionName, this.args); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/expression/GetAttributeExpression.java ================================================ /* * This file is part of Pebble. *

* Copyright (c) 2014 by Mitchell Bösecke *

* For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node.expression; import io.pebbletemplates.pebble.error.AttributeNotFoundException; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.error.RootAttributeNotFoundException; import io.pebbletemplates.pebble.extension.NodeVisitor; import io.pebbletemplates.pebble.attributes.AttributeResolver; import io.pebbletemplates.pebble.attributes.ResolvedAttribute; import io.pebbletemplates.pebble.node.ArgumentsNode; import io.pebbletemplates.pebble.node.PositionalArgumentNode; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; import java.util.List; /** * Used to get an attribute from an object. It will look up attributes in the following order: map * entry, array item, list item, get method, is method, has method, public method, public field. * * @author Mitchell */ public class GetAttributeExpression implements Expression { private final Expression node; private final Expression attributeNameExpression; private final ArgumentsNode args; private final String filename; private final int lineNumber; public GetAttributeExpression(Expression node, Expression attributeNameExpression, String filename, int lineNumber) { this(node, attributeNameExpression, null, filename, lineNumber); } public GetAttributeExpression(Expression node, Expression attributeNameExpression, ArgumentsNode args, String filename, int lineNumber) { this.node = node; this.attributeNameExpression = attributeNameExpression; this.args = args; this.filename = filename; this.lineNumber = lineNumber; } @Override public Object evaluate(PebbleTemplateImpl self, EvaluationContextImpl context) throws PebbleException { final Object object = this.node.evaluate(self, context); final Object attributeNameValue = this.attributeNameExpression.evaluate(self, context); final String attributeName = String.valueOf(attributeNameValue); final Object[] argumentValues = this.getArgumentValues(self, context); if (object == null && context.isStrictVariables()) { if (this.node instanceof ContextVariableExpression) { final String rootPropertyName = ((ContextVariableExpression) this.node).getName(); throw new RootAttributeNotFoundException(null, String.format( "Root attribute [%s] does not exist or can not be accessed and strict variables is set to true.", rootPropertyName), rootPropertyName, this.lineNumber, this.filename); } else { throw new RootAttributeNotFoundException(null, "Attempt to get attribute of null object and strict variables is set to true.", attributeName, this.lineNumber, this.filename); } } for (AttributeResolver attributeResolver : context.getExtensionRegistry() .getAttributeResolver()) { ResolvedAttribute resolvedAttribute = attributeResolver .resolve(object, attributeNameValue, argumentValues, this.args, context, this.filename, this.lineNumber); if (resolvedAttribute != null) { return resolvedAttribute.evaluatedValue; } } if (context.isStrictVariables()) { throw new AttributeNotFoundException(null, String.format( "Attribute [%s] of [%s] does not exist or can not be accessed and strict variables is set to true.", attributeName, object != null ? object.getClass().getName() : null), attributeName, this.lineNumber, this.filename); } return null; } /** * Fully evaluates the individual arguments. */ private Object[] getArgumentValues(PebbleTemplateImpl self, EvaluationContextImpl context) { Object[] argumentValues; if (this.args == null) { argumentValues = null; } else { List args = this.args.getPositionalArgs(); argumentValues = new Object[args.size()]; int index = 0; for (PositionalArgumentNode arg : args) { Object argumentValue = arg.getValueExpression().evaluate(self, context); argumentValues[index] = argumentValue; index++; } } return argumentValues; } @Override public void accept(NodeVisitor visitor) { visitor.visit(this); } public Expression getNode() { return this.node; } public Expression getAttributeNameExpression() { return this.attributeNameExpression; } public ArgumentsNode getArgumentsNode() { return this.args; } @Override public int getLineNumber() { return this.lineNumber; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/expression/GreaterThanEqualsExpression.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node.expression; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; import io.pebbletemplates.pebble.utils.OperatorUtils; public class GreaterThanEqualsExpression extends BinaryExpression { @Override public Boolean evaluate(PebbleTemplateImpl self, EvaluationContextImpl context) { try { return OperatorUtils.gte(this.getLeftExpression().evaluate(self, context), this.getRightExpression().evaluate(self, context)); } catch (Exception ex) { throw new PebbleException(ex, "Could not perform greater than or equals comparison", this.getLineNumber(), self .getName()); } } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/expression/GreaterThanExpression.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node.expression; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; import io.pebbletemplates.pebble.utils.OperatorUtils; public class GreaterThanExpression extends BinaryExpression { @Override public Boolean evaluate(PebbleTemplateImpl self, EvaluationContextImpl context) { try { return OperatorUtils.gt(this.getLeftExpression().evaluate(self, context), this.getRightExpression().evaluate(self, context)); } catch (Exception ex) { throw new PebbleException(ex, "Could not perform greater than comparison", this.getLineNumber(), self.getName()); } } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/expression/LessThanEqualsExpression.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node.expression; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; import io.pebbletemplates.pebble.utils.OperatorUtils; public class LessThanEqualsExpression extends BinaryExpression { @Override public Boolean evaluate(PebbleTemplateImpl self, EvaluationContextImpl context) { try { return OperatorUtils.lte(this.getLeftExpression().evaluate(self, context), this.getRightExpression().evaluate(self, context)); } catch (Exception ex) { throw new PebbleException(ex, "Could not perform less than or equals comparison", this.getLineNumber(), self .getName()); } } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/expression/LessThanExpression.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node.expression; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; import io.pebbletemplates.pebble.utils.OperatorUtils; public class LessThanExpression extends BinaryExpression { @Override public Boolean evaluate(PebbleTemplateImpl self, EvaluationContextImpl context) { try { return OperatorUtils.lt(this.getLeftExpression().evaluate(self, context), this.getRightExpression().evaluate(self, context)); } catch (Exception ex) { throw new PebbleException(ex, "Could not perform greater modulus", this.getLineNumber(), self .getName()); } } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/expression/LiteralBigDecimalExpression.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node.expression; import io.pebbletemplates.pebble.extension.NodeVisitor; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; import java.math.BigDecimal; public class LiteralBigDecimalExpression implements Expression { private final BigDecimal value; private final int lineNumber; public LiteralBigDecimalExpression(BigDecimal value, int lineNumber) { this.value = value; this.lineNumber = lineNumber; } @Override public void accept(NodeVisitor visitor) { visitor.visit(this); } @Override public BigDecimal evaluate(PebbleTemplateImpl self, EvaluationContextImpl context) { return this.value; } @Override public int getLineNumber() { return this.lineNumber; } public BigDecimal getValue() { return this.value; } @Override public String toString() { return this.value.toString(); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/expression/LiteralBooleanExpression.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node.expression; import io.pebbletemplates.pebble.extension.NodeVisitor; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; public class LiteralBooleanExpression implements Expression { private final Boolean value; private final int lineNumber; public LiteralBooleanExpression(Boolean value, int lineNumber) { this.value = value; this.lineNumber = lineNumber; } @Override public void accept(NodeVisitor visitor) { visitor.visit(this); } @Override public Boolean evaluate(PebbleTemplateImpl self, EvaluationContextImpl context) { return this.value; } @Override public int getLineNumber() { return this.lineNumber; } public Boolean getValue() { return this.value; } @Override public String toString() { return this.value.toString(); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/expression/LiteralDoubleExpression.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node.expression; import io.pebbletemplates.pebble.extension.NodeVisitor; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; public class LiteralDoubleExpression implements Expression { private final Double value; private final int lineNumber; public LiteralDoubleExpression(Double value, int lineNumber) { this.value = value; this.lineNumber = lineNumber; } @Override public void accept(NodeVisitor visitor) { visitor.visit(this); } @Override public Double evaluate(PebbleTemplateImpl self, EvaluationContextImpl context) { return this.value; } @Override public int getLineNumber() { return this.lineNumber; } public Double getValue() { return this.value; } @Override public String toString() { return this.value.toString(); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/expression/LiteralIntegerExpression.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node.expression; import io.pebbletemplates.pebble.extension.NodeVisitor; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; public class LiteralIntegerExpression implements Expression { private final Integer value; private final int lineNumber; public LiteralIntegerExpression(Integer value, int lineNumber) { this.value = value; this.lineNumber = lineNumber; } @Override public void accept(NodeVisitor visitor) { visitor.visit(this); } @Override public Integer evaluate(PebbleTemplateImpl self, EvaluationContextImpl context) { return this.value; } @Override public int getLineNumber() { return this.lineNumber; } public Integer getValue() { return this.value; } @Override public String toString() { return this.value.toString(); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/expression/LiteralLongExpression.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node.expression; import io.pebbletemplates.pebble.extension.NodeVisitor; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; public class LiteralLongExpression implements Expression { private final Long value; private final int lineNumber; public LiteralLongExpression(Long value, int lineNumber) { this.value = value; this.lineNumber = lineNumber; } @Override public void accept(NodeVisitor visitor) { visitor.visit(this); } @Override public Long evaluate(PebbleTemplateImpl self, EvaluationContextImpl context) { return this.value; } @Override public int getLineNumber() { return this.lineNumber; } public Long getValue() { return this.value; } @Override public String toString() { return this.value.toString(); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/expression/LiteralNullExpression.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node.expression; import io.pebbletemplates.pebble.extension.NodeVisitor; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; public class LiteralNullExpression implements Expression { private final int lineNumber; public LiteralNullExpression(int lineNumber) { this.lineNumber = lineNumber; } @Override public void accept(NodeVisitor visitor) { visitor.visit(this); } @Override public Object evaluate(PebbleTemplateImpl self, EvaluationContextImpl context) { return null; } @Override public int getLineNumber() { return this.lineNumber; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/expression/LiteralStringExpression.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node.expression; import io.pebbletemplates.pebble.extension.NodeVisitor; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; public class LiteralStringExpression implements Expression { private final String value; private final int lineNumber; public LiteralStringExpression(String value, int lineNumber) { this.value = value; this.lineNumber = lineNumber; } @Override public void accept(NodeVisitor visitor) { visitor.visit(this); } @Override public String evaluate(PebbleTemplateImpl self, EvaluationContextImpl context) { return this.value; } @Override public int getLineNumber() { return this.lineNumber; } public String getValue() { return this.value; } @Override public String toString() { return String.format("\"%s\"", this.value); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/expression/MapExpression.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node.expression; import io.pebbletemplates.pebble.extension.NodeVisitor; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; public class MapExpression implements Expression> { // FIXME should keys be of any type? private final Map, Expression> entries; private final int lineNumber; public MapExpression(int lineNumber) { this.entries = Collections.emptyMap(); this.lineNumber = lineNumber; } public MapExpression(Map, Expression> entries, int lineNumber) { if (entries == null) { this.entries = Collections.emptyMap(); } else { this.entries = entries; } this.lineNumber = lineNumber; } @Override public void accept(NodeVisitor visitor) { visitor.visit(this); } @Override public Map evaluate(PebbleTemplateImpl self, EvaluationContextImpl context) { Map returnEntries = new HashMap<>( Long.valueOf(Math.round(Math.ceil(this.entries.size() / 0.75))) .intValue()); for (Entry, Expression> entry: this.entries.entrySet()) { Expression keyExpr = entry.getKey(); Expression valueExpr = entry.getValue(); Object key = keyExpr == null ? null : keyExpr.evaluate(self, context); Object value = valueExpr == null ? null : valueExpr.evaluate(self, context); returnEntries.put(key, value); } return returnEntries; } public Map, Expression> getEntries() { return this.entries; } @Override public int getLineNumber() { return this.lineNumber; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/expression/ModulusExpression.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node.expression; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; import io.pebbletemplates.pebble.utils.OperatorUtils; public class ModulusExpression extends BinaryExpression { @Override public Object evaluate(PebbleTemplateImpl self, EvaluationContextImpl context) { try { return OperatorUtils.mod(this.getLeftExpression().evaluate(self, context), this.getRightExpression().evaluate(self, context)); } catch (Exception ex) { throw new PebbleException(ex, "Could not perform greater modulus", this.getLineNumber(), self .getName()); } } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/expression/MultiplyExpression.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node.expression; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; import io.pebbletemplates.pebble.utils.OperatorUtils; public class MultiplyExpression extends BinaryExpression { @Override public Object evaluate(PebbleTemplateImpl self, EvaluationContextImpl context) { try { return OperatorUtils.multiply(this.getLeftExpression().evaluate(self, context), this.getRightExpression().evaluate(self, context)); } catch (Exception ex) { throw new PebbleException(ex, "Could not perform multiplication", this.getLineNumber(), self .getName()); } } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/expression/NegativeTestExpression.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node.expression; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; public class NegativeTestExpression extends PositiveTestExpression { @Override public Object evaluate(PebbleTemplateImpl self, EvaluationContextImpl context) { return !((Boolean) super.evaluate(self, context)); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/expression/NotEqualsExpression.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node.expression; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; import io.pebbletemplates.pebble.utils.OperatorUtils; public class NotEqualsExpression extends BinaryExpression { @Override public Boolean evaluate(PebbleTemplateImpl self, EvaluationContextImpl context) { try { return !OperatorUtils .equals(this.getLeftExpression().evaluate(self, context), this.getRightExpression().evaluate(self, context)); } catch (Exception ex) { throw new PebbleException(ex, "Could not perform not equals comparison", this.getLineNumber(), self.getName()); } } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/expression/OrExpression.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node.expression; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; import static io.pebbletemplates.pebble.utils.TypeUtils.compatibleCast; public class OrExpression extends BinaryExpression { @SuppressWarnings("unchecked") @Override public Boolean evaluate(PebbleTemplateImpl self, EvaluationContextImpl context) { Expression leftExpression = (Expression) this.getLeftExpression(); boolean left = this.evaluateExpression(self, context, leftExpression); if (!left) { Expression rightExpression = (Expression) this.getRightExpression(); return this.evaluateExpression(self, context, rightExpression); } return true; } private boolean evaluateExpression(PebbleTemplateImpl self, EvaluationContextImpl context, Expression expression) { Boolean evaluatedExpression = compatibleCast(expression.evaluate(self, context), Boolean.class); if (evaluatedExpression == null) { if (context.isStrictVariables()) { throw new PebbleException(null, "null value used in and operator and strict variables is set to true", this.getLineNumber(), self.getName()); } return false; } return evaluatedExpression; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/expression/ParentFunctionExpression.java ================================================ /* * This file is part of Pebble. *

* Copyright (c) 2014 by Mitchell Bösecke *

* For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node.expression; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.extension.NodeVisitor; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.Hierarchy; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; public class ParentFunctionExpression implements Expression { private final String blockName; private final int lineNumber; public ParentFunctionExpression(String blockName, int lineNumber) { this.blockName = blockName; this.lineNumber = lineNumber; } @Override public String evaluate(PebbleTemplateImpl self, EvaluationContextImpl context) { Writer writer = new StringWriter(); try { Hierarchy hierarchy = context.getHierarchy(); if (hierarchy.getParent() == null) { throw new PebbleException(null, "Can not use parent function if template does not extend another template.", this.lineNumber, self.getName()); } PebbleTemplateImpl parent = hierarchy.getParent(); hierarchy.ascend(); parent.block(writer, context, this.blockName, true); hierarchy.descend(); } catch (IOException e) { throw new PebbleException(e, "Could not render block [" + this.blockName + "]", this.getLineNumber(), self.getName()); } return writer.toString(); } @Override public void accept(NodeVisitor visitor) { visitor.visit(this); } public String getBlockName() { return this.blockName; } @Override public int getLineNumber() { return this.lineNumber; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/expression/PositiveTestExpression.java ================================================ /* * This file is part of Pebble. *

* Copyright (c) 2014 by Mitchell Bösecke *

* For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node.expression; import io.pebbletemplates.pebble.error.AttributeNotFoundException; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.extension.Test; import io.pebbletemplates.pebble.extension.core.DefinedTest; import io.pebbletemplates.pebble.node.ArgumentsNode; import io.pebbletemplates.pebble.node.TestInvocationExpression; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; import java.util.Map; public class PositiveTestExpression extends BinaryExpression { private Test cachedTest; @Override public Object evaluate(PebbleTemplateImpl self, EvaluationContextImpl context) { TestInvocationExpression testInvocation = (TestInvocationExpression) this.getRightExpression(); ArgumentsNode args = testInvocation.getArgs(); if (this.cachedTest == null) { String testName = testInvocation.getTestName(); this.cachedTest = context.getExtensionRegistry().getTest(testInvocation.getTestName()); if (this.cachedTest == null) { throw new PebbleException(null, String.format("Test [%s] does not exist.", testName), this.getLineNumber(), self.getName()); } } Test test = this.cachedTest; Map namedArguments = args.getArgumentMap(self, context, test); // This check is not nice, because we use instanceof. However this is // the only test which should not fail in strict mode, when the variable // is not set, because this method should exactly test this. Hence a // generic solution to allow other tests to reuse this feature make no // sense. if (test instanceof DefinedTest) { Object input = null; try { input = this.getLeftExpression().evaluate(self, context); } catch (AttributeNotFoundException e) { input = null; } return test.apply(input, namedArguments, self, context, this.getLineNumber()); } else { return test .apply(this.getLeftExpression().evaluate(self, context), namedArguments, self, context, this.getLineNumber()); } } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/expression/RangeExpression.java ================================================ package io.pebbletemplates.pebble.node.expression; import io.pebbletemplates.pebble.extension.core.RangeFunction; import io.pebbletemplates.pebble.node.ArgumentsNode; import io.pebbletemplates.pebble.node.PositionalArgumentNode; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; import java.util.ArrayList; import java.util.List; /** * Expression which implements the range function. * * @author Eric Bussieres */ public class RangeExpression extends BinaryExpression { @Override public Object evaluate(PebbleTemplateImpl self, EvaluationContextImpl context) { List positionalArgs = new ArrayList<>(); positionalArgs.add(new PositionalArgumentNode(getLeftExpression())); positionalArgs.add(new PositionalArgumentNode(getRightExpression())); ArgumentsNode arguments = new ArgumentsNode(positionalArgs, null, this.getLineNumber()); FunctionOrMacroInvocationExpression function = new FunctionOrMacroInvocationExpression( RangeFunction.FUNCTION_NAME, arguments, this.getLineNumber()); return function.evaluate(self, context); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/expression/RenderableNodeExpression.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node.expression; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.node.RenderableNode; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; import io.pebbletemplates.pebble.utils.LimitedSizeWriter; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; /** * This class wraps a {@link RenderableNode} into an expression. This is used by the filter TAG to * apply a filter to large chunk of template which is contained within a renderable node. * * @author mbosecke */ public class RenderableNodeExpression extends UnaryExpression { private final RenderableNode node; private final int lineNumber; public RenderableNodeExpression(RenderableNode node, int lineNumber) { this.node = node; this.lineNumber = lineNumber; } @Override public Object evaluate(PebbleTemplateImpl self, EvaluationContextImpl context) { Writer writer = LimitedSizeWriter.from(new StringWriter(), context); try { this.node.render(self, writer, context); } catch (IOException e) { throw new PebbleException(e, "Error occurred while rendering node", this.getLineNumber(), self.getName()); } return writer.toString(); } public RenderableNode getNode() { return this.node; } @Override public int getLineNumber() { return this.lineNumber; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/expression/SubtractExpression.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node.expression; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; import io.pebbletemplates.pebble.utils.OperatorUtils; public class SubtractExpression extends BinaryExpression { @Override public Object evaluate(PebbleTemplateImpl self, EvaluationContextImpl context) { try { return OperatorUtils.subtract(this.getLeftExpression().evaluate(self, context), this.getRightExpression().evaluate(self, context)); } catch (Exception ex) { throw new PebbleException(ex, "Could not perform subtraction", this.getLineNumber(), self.getName()); } } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/expression/TernaryExpression.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node.expression; import io.pebbletemplates.pebble.extension.NodeVisitor; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; import io.pebbletemplates.pebble.utils.TypeUtils; import static io.pebbletemplates.pebble.utils.TypeUtils.compatibleCast; public class TernaryExpression implements Expression { private final Expression expression1; private Expression expression2; private Expression expression3; private final int lineNumber; public TernaryExpression(Expression expression1, Expression expression2, Expression expression3, int lineNumber, String filename) { this.expression1 = expression1; this.expression2 = expression2; this.expression3 = expression3; this.lineNumber = lineNumber; } @Override public Object evaluate(PebbleTemplateImpl self, EvaluationContextImpl context) { Object evaluatedExpression1 = this.expression1.evaluate(self, context); if (evaluatedExpression1 != null && TypeUtils.compatibleCast(evaluatedExpression1, Boolean.class)) { return this.expression2.evaluate(self, context); } else { return this.expression3.evaluate(self, context); } } @Override public void accept(NodeVisitor visitor) { visitor.visit(this); } public Expression getExpression1() { return this.expression1; } public Expression getExpression2() { return this.expression2; } public Expression getExpression3() { return this.expression3; } public void setExpression3(Expression expression3) { this.expression3 = expression3; } public void setExpression2(Expression expression2) { this.expression2 = expression2; } @Override public int getLineNumber() { return this.lineNumber; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/expression/UnaryExpression.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node.expression; import io.pebbletemplates.pebble.extension.NodeVisitor; public abstract class UnaryExpression implements Expression { private Expression childExpression; private int lineNumber; @Override public void accept(NodeVisitor visitor) { visitor.visit(this); } public Expression getChildExpression() { return this.childExpression; } public void setChildExpression(Expression childExpression) { this.childExpression = childExpression; } /** * Sets the line number on which the expression is defined on. * * @param lineNumber the line number on which the expression is defined on. */ public void setLineNumber(int lineNumber) { this.lineNumber = lineNumber; } @Override public int getLineNumber() { return this.lineNumber; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/expression/UnaryMinusExpression.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node.expression; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; import io.pebbletemplates.pebble.utils.OperatorUtils; public class UnaryMinusExpression extends UnaryExpression { @Override public Object evaluate(PebbleTemplateImpl self, EvaluationContextImpl context) { return OperatorUtils.unaryMinus(this.getChildExpression().evaluate(self, context)); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/expression/UnaryNotExpression.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node.expression; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; import io.pebbletemplates.pebble.utils.TypeUtils; import static io.pebbletemplates.pebble.utils.TypeUtils.compatibleCast; public class UnaryNotExpression extends UnaryExpression { @Override public Boolean evaluate(PebbleTemplateImpl self, EvaluationContextImpl context) { Object result = this.getChildExpression().evaluate(self, context); if (result != null) { if (result instanceof Boolean || result instanceof Number || result instanceof String) { return !TypeUtils.compatibleCast(result, Boolean.class); } throw new PebbleException( null, String.format( "Unsupported value type %s. Expected Boolean, String, Number in \"if\" statement", result.getClass().getSimpleName()), this.getLineNumber(), self.getName()); } // input is null if (context.isStrictVariables()) { throw new PebbleException(null, "null value given to not() and strict variables is set to true", this.getLineNumber(), self.getName()); } return true; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/expression/UnaryPlusExpression.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.node.expression; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; import io.pebbletemplates.pebble.utils.OperatorUtils; public class UnaryPlusExpression extends UnaryExpression { @Override public Object evaluate(PebbleTemplateImpl self, EvaluationContextImpl context) { return OperatorUtils.unaryPlus(this.getChildExpression().evaluate(self, context)); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/fornode/LazyLength.java ================================================ package io.pebbletemplates.pebble.node.fornode; import java.lang.reflect.Array; import java.util.Collection; import java.util.Enumeration; import java.util.Iterator; import java.util.Map; public class LazyLength extends Number { private final Object iterableEvaluation; private int value = -1; public LazyLength(Object iterableEvaluation) { this.iterableEvaluation = iterableEvaluation; } @Override public int intValue() { return this.getValue(); } @Override public long longValue() { return (long) this.getValue(); } @Override public float floatValue() { return (float) this.getValue(); } @Override public double doubleValue() { return (double) this.getValue(); } @Override public String toString() { return String.valueOf(this.getValue()); } private int getValue() { if (this.value == -1) { this.value = this.getIteratorSize(this.iterableEvaluation); } return this.value; } private int getIteratorSize(Object iterable) { if (iterable == null) { return 0; } if (iterable instanceof Collection) { return ((Collection) iterable).size(); } else if (iterable instanceof Map) { return ((Map) iterable).size(); } else if (iterable.getClass().isArray()) { return Array.getLength(iterable); } else if (iterable instanceof Enumeration) { Enumeration enumeration = (Enumeration) iterable; int size = 0; while (enumeration.hasMoreElements()) { size++; enumeration.nextElement(); } return size; } // assumed to be of type Iterator Iterator it = ((Iterable) iterable).iterator(); int size = 0; while (it.hasNext()) { size++; it.next(); } return size; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/node/fornode/LazyRevIndex.java ================================================ package io.pebbletemplates.pebble.node.fornode; public class LazyRevIndex extends Number { private final int value; private final LazyLength lazyLength; public LazyRevIndex(int value, LazyLength lazyLength) { this.value = value; this.lazyLength = lazyLength; } @Override public int intValue() { return this.getValue(); } @Override public long longValue() { return (long) this.getValue(); } @Override public float floatValue() { return (float) this.getValue(); } @Override public double doubleValue() { return (double) this.getValue(); } @Override public String toString() { return String.valueOf(this.getValue()); } private int getValue() { return this.lazyLength.intValue() - this.value - 1; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/operator/Associativity.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.operator; public enum Associativity { LEFT, RIGHT } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/operator/BinaryOperator.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.operator; import io.pebbletemplates.pebble.node.expression.BinaryExpression; public interface BinaryOperator { int getPrecedence(); String getSymbol(); BinaryExpression createInstance(); BinaryOperatorType getType(); Associativity getAssociativity(); } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/operator/BinaryOperatorImpl.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.operator; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.node.expression.BinaryExpression; import io.pebbletemplates.pebble.node.expression.FilterExpression; import io.pebbletemplates.pebble.node.expression.NegativeTestExpression; import io.pebbletemplates.pebble.node.expression.PositiveTestExpression; import java.util.function.Supplier; public class BinaryOperatorImpl implements BinaryOperator { private final int precedence; private final String symbol; private final Supplier> nodeSupplier; private final BinaryOperatorType type; private final Associativity associativity; /** * This constuctor left for backward compatibility with custom extensions */ public BinaryOperatorImpl(String symbol, int precedence, Class> nodeClass, Associativity associativity) { this(symbol, precedence, () -> { try { return nodeClass.newInstance(); } catch (InstantiationException | IllegalAccessException e) { throw new PebbleException(e, "Error instantiating class " + nodeClass.getName()); } }, getDefaultType(nodeClass), associativity); } /** * This constuctor allows you to completely control the instantiation of the expression class */ public BinaryOperatorImpl(String symbol, int precedence, Supplier> nodeSupplier, BinaryOperatorType type, Associativity associativity) { this.symbol = symbol; this.precedence = precedence; this.nodeSupplier = nodeSupplier; this.type = type; this.associativity = associativity; } @Override public int getPrecedence() { return this.precedence; } @Override public String getSymbol() { return this.symbol; } @Override public BinaryExpression createInstance() { return this.nodeSupplier.get(); } @Override public BinaryOperatorType getType() { return this.type; } @Override public Associativity getAssociativity() { return this.associativity; } private static BinaryOperatorType getDefaultType(Class> nodeClass) { if (FilterExpression.class.equals(nodeClass)) { return BinaryOperatorType.FILTER; } else if (PositiveTestExpression.class.equals(nodeClass) || NegativeTestExpression.class .equals(nodeClass)) { return BinaryOperatorType.TEST; } else { return BinaryOperatorType.NORMAL; } } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/operator/BinaryOperatorType.java ================================================ package io.pebbletemplates.pebble.operator; public enum BinaryOperatorType { NORMAL, FILTER, TEST } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/operator/UnaryOperator.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.operator; import io.pebbletemplates.pebble.node.expression.UnaryExpression; public interface UnaryOperator { int getPrecedence(); String getSymbol(); Class getNodeClass(); } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/operator/UnaryOperatorImpl.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.operator; import io.pebbletemplates.pebble.node.expression.UnaryExpression; public class UnaryOperatorImpl implements UnaryOperator { private final int precedence; private final String symbol; private final Class nodeClass; public UnaryOperatorImpl(String symbol, int precedence, Class nodeClass) { this.symbol = symbol; this.precedence = precedence; this.nodeClass = nodeClass; } @Override public int getPrecedence() { return this.precedence; } @Override public String getSymbol() { return this.symbol; } @Override public Class getNodeClass() { return this.nodeClass; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/parser/ExpressionParser.java ================================================ /* * This file is part of Pebble. *

* Copyright (c) 2014 by Mitchell Bösecke *

* For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.parser; import io.pebbletemplates.pebble.operator.Associativity; import io.pebbletemplates.pebble.operator.BinaryOperator; import io.pebbletemplates.pebble.operator.BinaryOperatorType; import io.pebbletemplates.pebble.operator.UnaryOperator; import io.pebbletemplates.pebble.error.ParserException; import io.pebbletemplates.pebble.lexer.Token; import io.pebbletemplates.pebble.lexer.TokenStream; import io.pebbletemplates.pebble.node.ArgumentsNode; import io.pebbletemplates.pebble.node.FunctionOrMacroNameNode; import io.pebbletemplates.pebble.node.NamedArgumentNode; import io.pebbletemplates.pebble.node.PositionalArgumentNode; import io.pebbletemplates.pebble.node.TestInvocationExpression; import io.pebbletemplates.pebble.node.expression.ArrayExpression; import io.pebbletemplates.pebble.node.expression.BinaryExpression; import io.pebbletemplates.pebble.node.expression.BlockFunctionExpression; import io.pebbletemplates.pebble.node.expression.ConcatenateExpression; import io.pebbletemplates.pebble.node.expression.ContextVariableExpression; import io.pebbletemplates.pebble.node.expression.Expression; import io.pebbletemplates.pebble.node.expression.FilterInvocationExpression; import io.pebbletemplates.pebble.node.expression.FunctionOrMacroInvocationExpression; import io.pebbletemplates.pebble.node.expression.GetAttributeExpression; import io.pebbletemplates.pebble.node.expression.LiteralBigDecimalExpression; import io.pebbletemplates.pebble.node.expression.LiteralBooleanExpression; import io.pebbletemplates.pebble.node.expression.LiteralDoubleExpression; import io.pebbletemplates.pebble.node.expression.LiteralIntegerExpression; import io.pebbletemplates.pebble.node.expression.LiteralLongExpression; import io.pebbletemplates.pebble.node.expression.LiteralNullExpression; import io.pebbletemplates.pebble.node.expression.LiteralStringExpression; import io.pebbletemplates.pebble.node.expression.MapExpression; import io.pebbletemplates.pebble.node.expression.ParentFunctionExpression; import io.pebbletemplates.pebble.node.expression.TernaryExpression; import io.pebbletemplates.pebble.node.expression.UnaryExpression; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; /** * Parses expressions. */ public class ExpressionParser { private static final Set RESERVED_KEYWORDS = new HashSet<>( Arrays.asList("true", "false", "null", "none")); private final Parser parser; private TokenStream stream; private Map binaryOperators; private Map unaryOperators; private ParserOptions parserOptions; /** * Constructor * * @param parser A reference to the main parser * @param binaryOperators All the binary operators * @param unaryOperators All the unary operators */ public ExpressionParser(Parser parser, Map binaryOperators, Map unaryOperators, ParserOptions parserOptions) { this.parser = parser; this.binaryOperators = binaryOperators; this.unaryOperators = unaryOperators; this.parserOptions = parserOptions; } /** * The public entry point for parsing an expression. * * @return NodeExpression the expression that has been parsed. */ public Expression parseExpression() { return this.parseExpression(0); } /** * A private entry point for parsing an expression. This method takes in the precedence required * to operate a "precedence climbing" parsing algorithm. It is a recursive method. * * @return The NodeExpression representing the parsed expression. * @see "http://en.wikipedia.org/wiki/Operator-precedence_parser" */ private Expression parseExpression(int minPrecedence) { this.stream = this.parser.getStream(); Token token = this.stream.current(); Expression expression; /* * The first check is to see if the expression begins with a unary * operator, or an opening bracket, or neither. */ if (this.isUnary(token)) { UnaryOperator operator = this.unaryOperators.get(token.getValue()); this.stream.next(); expression = this.parseExpression(operator.getPrecedence()); UnaryExpression unaryExpression; Class operatorNodeClass = operator.getNodeClass(); try { unaryExpression = operatorNodeClass.newInstance(); unaryExpression.setLineNumber(this.stream.current().getLineNumber()); } catch (InstantiationException | IllegalAccessException e) { throw new RuntimeException(e); } unaryExpression.setChildExpression(expression); expression = unaryExpression; } else if (token.test(Token.Type.PUNCTUATION, "(")) { this.stream.next(); expression = this.parseExpression(); this.stream.expect(Token.Type.PUNCTUATION, ")"); expression = this.parsePostfixExpression(expression); } // array definition syntax else if (token.test(Token.Type.PUNCTUATION, "[")) { // preserve [ token for array parsing expression = this.parseArrayDefinitionExpression(); // don't expect ], because it has been already expected // currently, postfix expressions are not supported for arrays // expression = parsePostfixExpression(expression); } // map definition syntax else if (token.test(Token.Type.PUNCTUATION, "{")) { // preserve { token for map parsing expression = this.parseMapDefinitionExpression(); // don't expect }, because it has been already expected // currently, postfix expressions are not supported for maps // expression = parsePostfixExpression(expression); } else { /* * starts with neither. Let's parse out the first expression that we * can find. There may be one, there may be many (separated by * binary operators); right now we are just looking for the first. */ expression = this.subparseExpression(); } /* * If, after parsing the first expression we encounter a binary operator * then we know we have another expression on the other side of the * operator that requires parsing. Otherwise we're done. */ token = this.stream.current(); while (this.isBinary(token) && this.binaryOperators.get(token.getValue()).getPrecedence() >= minPrecedence) { // find out which operator we are dealing with and then skip over it BinaryOperator operator = this.binaryOperators.get(token.getValue()); this.stream.next(); Expression expressionRight; // the right hand expression of the FILTER operator is handled in a // unique way if (operator.getType() == BinaryOperatorType.FILTER) { expressionRight = this.parseFilterInvocationExpression(); } // the right hand expression of TEST operators is handled in a // unique way else if (operator.getType() == BinaryOperatorType.TEST) { expressionRight = this.parseTestInvocationExpression(); } else { /* * parse the expression on the right hand side of the operator * while maintaining proper associativity and precedence */ expressionRight = this.parseExpression( Associativity.LEFT.equals(operator.getAssociativity()) ? operator .getPrecedence() + 1 : operator.getPrecedence()); } /* * we have to wrap the left and right side expressions into one * final expression. The operator provides us with the type of * expression we are creating. */ BinaryExpression finalExpression; try { finalExpression = operator.createInstance(); } catch (RuntimeException e) { throw new ParserException(e, "Error instantiating operator node", token.getLineNumber(), this.stream.getFilename()); } finalExpression.setLineNumber(this.stream.current().getLineNumber()); finalExpression.setLeft(expression); finalExpression.setRight(expressionRight); expression = finalExpression; token = this.stream.current(); } if (minPrecedence == 0) { return this.parseTernaryExpression(expression); } return expression; } /** * Checks if a token is a unary operator. * * @param token The token that we are checking * @return boolean Whether the token is a unary operator or not */ private boolean isUnary(Token token) { return token.test(Token.Type.OPERATOR) && this.unaryOperators.containsKey(token.getValue()); } /** * Checks if a token is a binary operator. * * @param token The token that we are checking * @return boolean Whether the token is a binary operator or not */ private boolean isBinary(Token token) { return token.test(Token.Type.OPERATOR) && this.binaryOperators.containsKey(token.getValue()); } /** * Finds and returns the next "simple" expression; an expression of which can be found on either * side of a binary operator but does not contain a binary operator. Ex. "var.field", "true", * "12", etc. * * @return NodeExpression The expression that it found. */ private Expression subparseExpression() { final Token token = this.stream.current(); Expression node; switch (token.getType()) { case NAME: switch (token.getValue()) { // a constant? case "true": case "TRUE": node = new LiteralBooleanExpression(true, token.getLineNumber()); this.stream.next(); break; case "false": case "FALSE": node = new LiteralBooleanExpression(false, token.getLineNumber()); this.stream.next(); break; case "none": case "NONE": case "null": case "NULL": node = new LiteralNullExpression(token.getLineNumber()); this.stream.next(); break; default: // name of a function? if (this.stream.peek().test(Token.Type.PUNCTUATION, "(")) { node = new FunctionOrMacroNameNode(token.getValue(), this.stream.peek().getLineNumber()); } // variable name else { node = new ContextVariableExpression(token.getValue(), token.getLineNumber()); } this.stream.next(); break; } break; case LONG: final String longValue = token.getValue(); node = new LiteralLongExpression(Long.valueOf(longValue), token.getLineNumber()); this.stream.next(); break; case NUMBER: final String numberValue = token.getValue(); if (this.parserOptions.isLiteralNumbersAsBigDecimals()) { node = new LiteralBigDecimalExpression(new BigDecimal(numberValue), token.getLineNumber()); } else { if (numberValue.contains(".")) { node = new LiteralDoubleExpression(Double.valueOf(numberValue), token.getLineNumber()); } else { if (this.parserOptions.isLiteralDecimalTreatedAsInteger()) { node = new LiteralIntegerExpression(Integer.valueOf(numberValue), token.getLineNumber()); } else { node = new LiteralLongExpression(Long.valueOf(numberValue), token.getLineNumber()); } } } this.stream.next(); break; case STRING: case STRING_INTERPOLATION_START: node = this.parseStringExpression(); break; // not found, syntax error default: throw new ParserException(null, String.format("Unexpected token \"%s\" of value \"%s\"", token.getType() .toString(), token.getValue()), token.getLineNumber(), this.stream.getFilename()); } // there may or may not be more to this expression - let's keep looking return this.parsePostfixExpression(node); } private Expression parseStringExpression() throws ParserException { List> nodes = new ArrayList<>(); Token.Type previousNodeType = null; // Sequential strings are not OK, but strings can follow interpolation while (true) { if (this.stream.current().test(Token.Type.STRING) && Token.Type.STRING != previousNodeType) { Token token = this.stream.expect(Token.Type.STRING); nodes.add(new LiteralStringExpression(token.getValue(), token.getLineNumber())); previousNodeType = Token.Type.STRING; } else if (this.stream.current().test(Token.Type.STRING_INTERPOLATION_START)) { this.stream.expect(Token.Type.STRING_INTERPOLATION_START); nodes.add(this.parseExpression()); this.stream.expect(Token.Type.STRING_INTERPOLATION_END); previousNodeType = Token.Type.STRING_INTERPOLATION_END; } else { break; } } Expression first = nodes.remove(0); if (nodes.isEmpty()) { return first; } ConcatenateExpression expr, firstExpr; expr = firstExpr = new ConcatenateExpression(first, null); for (int i = 0; i < nodes.size(); i++) { Expression node = nodes.get(i); if (i == nodes.size() - 1) { expr.setRight(node); } else { ConcatenateExpression newExpr = new ConcatenateExpression(node, null); expr.setRight(newExpr); expr = newExpr; } } return firstExpr; } @SuppressWarnings("unchecked") private Expression parseTernaryExpression(Expression expression) { // if the next token isn't a ?, we're not dealing with a ternary // expression if (!this.stream.current().test(Token.Type.PUNCTUATION, "?")) { return expression; } this.stream.next(); Expression expression2 = this.parseExpression(); this.stream.expect(Token.Type.PUNCTUATION, ":"); Expression expression3 = this.parseExpression(); expression = new TernaryExpression((Expression) expression, expression2, expression3, this.stream .current().getLineNumber(), this.stream.getFilename()); return expression; } /** * Determines if there is more to the provided expression than we originally thought. We will look * for the filter operator or perhaps we are getting an attribute from a variable (ex. * var.attribute or var['attribute'] or var.attribute(bar)). * * @param node The expression that we have already discovered * @return Either the original expression that was passed in or a slightly modified version of it, * depending on what was discovered. */ private Expression parsePostfixExpression(Expression node) { Token current; while (true) { current = this.stream.current(); if (current.test(Token.Type.PUNCTUATION, ".") || current.test(Token.Type.PUNCTUATION, "[")) { // a period represents getting an attribute from a variable or // calling a method node = this.parseBeanAttributeExpression(node); } else if (current.test(Token.Type.PUNCTUATION, "(")) { // function call node = this.parseFunctionOrMacroInvocation(node); } else { break; } } return node; } private Expression parseFunctionOrMacroInvocation(Expression node) { String functionName = ((FunctionOrMacroNameNode) node).getName(); ArgumentsNode args = this.parseArguments(); /* * The following core functions have their own Nodes and are rendered in * unique ways for the sake of performance. */ switch (functionName) { case "parent": return new ParentFunctionExpression(this.parser.peekBlockStack(), this.stream.current().getLineNumber()); case "block": return new BlockFunctionExpression(args, node.getLineNumber()); } return new FunctionOrMacroInvocationExpression(functionName, args, node.getLineNumber()); } public FilterInvocationExpression parseFilterInvocationExpression() { TokenStream stream = this.parser.getStream(); Token filterToken = stream.expect(Token.Type.NAME); ArgumentsNode args; if (stream.current().test(Token.Type.PUNCTUATION, "(")) { args = this.parseArguments(); } else { args = new ArgumentsNode(null, null, filterToken.getLineNumber()); } return new FilterInvocationExpression(filterToken.getValue(), args, filterToken.getLineNumber()); } private Expression parseTestInvocationExpression() { TokenStream stream = this.parser.getStream(); int lineNumber = stream.current().getLineNumber(); Token testToken = stream.expect(Token.Type.NAME); ArgumentsNode args; if (stream.current().test(Token.Type.PUNCTUATION, "(")) { args = this.parseArguments(); } else { args = new ArgumentsNode(null, null, testToken.getLineNumber()); } return new TestInvocationExpression(lineNumber, testToken.getValue(), args); } /** * A bean attribute expression can either be an expression getting an attribute from a variable in * the context, or calling a method from a variable. * * Ex. foo.bar or foo['bar'] or foo.bar('baz') * * @param node The expression parsed so far * @return NodeExpression The parsed subscript expression */ private Expression parseBeanAttributeExpression(Expression node) { TokenStream stream = this.parser.getStream(); if (stream.current().test(Token.Type.PUNCTUATION, ".")) { // skip over the '.' token stream.next(); Token token = stream.expect(Token.Type.NAME); ArgumentsNode args = null; if (stream.current().test(Token.Type.PUNCTUATION, "(")) { args = this.parseArguments(); if (!args.getNamedArgs().isEmpty()) { throw new ParserException(null, "Can not use named arguments when calling a bean method", stream .current().getLineNumber(), stream.getFilename()); } } node = new GetAttributeExpression(node, new LiteralStringExpression(token.getValue(), token.getLineNumber()), args, stream.getFilename(), token.getLineNumber()); } else if (stream.current().test(Token.Type.PUNCTUATION, "[")) { // skip over opening '[' bracket stream.next(); node = new GetAttributeExpression(node, this.parseExpression(), stream.getFilename(), stream.current() .getLineNumber()); // move past the closing ']' bracket stream.expect(Token.Type.PUNCTUATION, "]"); } return node; } private ArgumentsNode parseArguments() { return this.parseArguments(false); } public ArgumentsNode parseArguments(boolean isMacroDefinition) { List positionalArgs = new ArrayList<>(); List namedArgs = new ArrayList<>(); this.stream = this.parser.getStream(); this.stream.expect(Token.Type.PUNCTUATION, "("); while (!this.stream.current().test(Token.Type.PUNCTUATION, ")")) { String argumentName = null; Expression argumentValue = null; if (!namedArgs.isEmpty() || !positionalArgs.isEmpty()) { this.stream.expect(Token.Type.PUNCTUATION, ","); } /* * Most arguments consist of VALUES with optional NAMES but in the * case of a macro definition the user is specifying NAMES with * optional VALUES. Therefore the logic changes slightly. */ if (isMacroDefinition) { argumentName = this.parseNewVariableName(); if (this.stream.current().test(Token.Type.PUNCTUATION, "=")) { this.stream.expect(Token.Type.PUNCTUATION, "="); argumentValue = this.parseExpression(); } } else { if (this.stream.peek().test(Token.Type.PUNCTUATION, "=")) { argumentName = this.parseNewVariableName(); this.stream.expect(Token.Type.PUNCTUATION, "="); } argumentValue = this.parseExpression(); } if (argumentName == null) { if (!namedArgs.isEmpty()) { throw new ParserException(null, "Positional arguments must be declared before any named arguments.", this.stream.current() .getLineNumber(), this.stream.getFilename()); } positionalArgs.add(new PositionalArgumentNode(argumentValue)); } else { namedArgs.add(new NamedArgumentNode(argumentName, argumentValue)); } } this.stream.expect(Token.Type.PUNCTUATION, ")"); return new ArgumentsNode(positionalArgs, namedArgs, this.stream.current().getLineNumber()); } /** * Parses a new variable that will need to be initialized in the Java code. * * This is used for the set tag, the for loop, and in named arguments. * * @return A variable name */ public String parseNewVariableName() { // set the stream because this function may be called externally (for // and set token parsers) this.stream = this.parser.getStream(); Token token = stream.expect(Token.Type.NAME); if (RESERVED_KEYWORDS.contains(token.getValue())) { throw new ParserException(null, String.format("Can not assign a value to %s", token.getValue()), token.getLineNumber(), this.stream.getFilename()); } return token.getValue(); } private Expression parseArrayDefinitionExpression() { TokenStream stream = this.parser.getStream(); // expect the opening bracket and check for an empty array stream.expect(Token.Type.PUNCTUATION, "["); if (stream.current().test(Token.Type.PUNCTUATION, "]")) { stream.next(); return new ArrayExpression(stream.current().getLineNumber()); } // there's at least one expression in the array List> elements = new ArrayList<>(); while (true) { Expression expr = this.parseExpression(); elements.add(expr); if (stream.current().test(Token.Type.PUNCTUATION, "]")) { // this seems to be the end of the array break; } // expect the comma separator, until we either find a closing // bracket or fail the expect stream.expect(Token.Type.PUNCTUATION, ","); } // expect the closing bracket stream.expect(Token.Type.PUNCTUATION, "]"); return new ArrayExpression(elements, stream.current().getLineNumber()); } private Expression parseMapDefinitionExpression() { TokenStream stream = this.parser.getStream(); // expect the opening brace and check for an empty map stream.expect(Token.Type.PUNCTUATION, "{"); if (stream.current().test(Token.Type.PUNCTUATION, "}")) { stream.next(); return new MapExpression(stream.current().getLineNumber()); } // there's at least one expression in the map Map, Expression> elements = new HashMap<>(); while (true) { // key : value Expression keyExpr = this.parseExpression(); stream.expect(Token.Type.PUNCTUATION, ":"); Expression valueExpr = this.parseExpression(); elements.put(keyExpr, valueExpr); if (stream.current().test(Token.Type.PUNCTUATION, "}")) { // this seems to be the end of the map break; } // expect the comma separator, until we either find a closing brace // or fail the expect stream.expect(Token.Type.PUNCTUATION, ","); } // expect the closing brace stream.expect(Token.Type.PUNCTUATION, "}"); return new MapExpression(elements, stream.current().getLineNumber()); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/parser/Parser.java ================================================ /* * This file is part of Pebble. *

* Copyright (c) 2014 by Mitchell Bösecke *

* For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.parser; import io.pebbletemplates.pebble.lexer.TokenStream; import io.pebbletemplates.pebble.node.BodyNode; import io.pebbletemplates.pebble.node.RootNode; public interface Parser { RootNode parse(TokenStream stream); BodyNode subparse(); /** * Provides the stream of tokens which ultimately need to be "parsed" into Nodes. * * @return TokenStream */ TokenStream getStream(); /** * Parses the existing TokenStream, starting at the current Token, and ending when the * stopCondition is fullfilled. * * @param stopCondition The condition to stop parsing a segment of the template. * @return A node representing the parsed section */ BodyNode subparse(StoppingCondition stopCondition); ExpressionParser getExpressionParser(); String peekBlockStack(); String popBlockStack(); void pushBlockStack(String blockName); } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/parser/ParserImpl.java ================================================ /* * This file is part of Pebble. *

* Copyright (c) 2014 by Mitchell Bösecke *

* For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.parser; import io.pebbletemplates.pebble.operator.BinaryOperator; import io.pebbletemplates.pebble.operator.UnaryOperator; import io.pebbletemplates.pebble.error.ParserException; import io.pebbletemplates.pebble.lexer.Token; import io.pebbletemplates.pebble.lexer.TokenStream; import io.pebbletemplates.pebble.node.BodyNode; import io.pebbletemplates.pebble.node.PrintNode; import io.pebbletemplates.pebble.node.RenderableNode; import io.pebbletemplates.pebble.node.RootNode; import io.pebbletemplates.pebble.node.TextNode; import io.pebbletemplates.pebble.node.expression.Expression; import io.pebbletemplates.pebble.tokenParser.TokenParser; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.Map; public class ParserImpl implements Parser { /** * Binary operators */ private final Map binaryOperators; /** * Unary operators */ private final Map unaryOperators; /** * Token parsers */ private final Map tokenParsers; /** * An expression parser. */ private ExpressionParser expressionParser; /** * The TokenStream that we are converting into an Abstract Syntax Tree. */ private TokenStream stream; /** * TokenParser objects provided by the extensions. */ /** * used to keep track of the name of the block that we are currently inside of. This is purely * just for the parent() function. */ private LinkedList blockStack; /** * parser options */ private ParserOptions parserOptions; /** * Constructor * * @param binaryOperators A map of binary operators * @param unaryOperators A map of unary operators * @param tokenParsers A map of token parsers */ public ParserImpl(Map unaryOperators, Map binaryOperators, Map tokenParsers, ParserOptions parserOptions) { this.binaryOperators = binaryOperators; this.unaryOperators = unaryOperators; this.tokenParsers = tokenParsers; this.parserOptions = parserOptions; } @Override public RootNode parse(TokenStream stream) { // expression parser this.expressionParser = new ExpressionParser(this, this.binaryOperators, this.unaryOperators, this.parserOptions); this.stream = stream; this.blockStack = new LinkedList<>(); BodyNode body = this.subparse(); return new RootNode(body); } @Override public BodyNode subparse() { return this.subparse(null); } @Override /** * The main method for the parser. This method does the work of converting * a TokenStream into a Node * * @param stopCondition A stopping condition provided by a token parser * @return Node The root node of the generated Abstract Syntax Tree */ public BodyNode subparse(StoppingCondition stopCondition) { // these nodes will be the children of the root node List nodes = new ArrayList<>(); Token token; while (!this.stream.isEOF()) { switch (this.stream.current().getType()) { case TEXT: /* * The current token is a text token. Not much to do here other * than convert it to a text Node. */ token = this.stream.current(); nodes.add(new TextNode(token.getValue(), token.getLineNumber())); this.stream.next(); break; case PRINT_START: /* * We are entering a print delimited region at this point. These * regions will contain some sort of expression so let's pass * control to our expression parser. */ // go to the next token because the current one is just the // opening delimiter token = this.stream.next(); Expression expression = this.expressionParser.parseExpression(); nodes.add(new PrintNode(expression, token.getLineNumber())); // we expect to see a print closing delimiter this.stream.expect(Token.Type.PRINT_END); break; case EXECUTE_START: // go to the next token because the current one is just the // opening delimiter this.stream.next(); token = this.stream.current(); /* * We expect a name token at the beginning of every block. * * We do not use stream.expect() because it consumes the current * token. The current token may be needed by a token parser * which has provided a stopping condition. Ex. the 'if' token * parser may need to check if the current token is either * 'endif' or 'else' and act accordingly, thus we should not * consume it. */ if (!Token.Type.NAME.equals(token.getType())) { throw new ParserException(null, "A block must start with a tag name.", token.getLineNumber(), this.stream.getFilename()); } // If this method was executed using a TokenParser and // that parser provided a stopping condition (ex. checking // for the 'endif' token) let's check for that condition // now. if (stopCondition != null && stopCondition.evaluate(token)) { return new BodyNode(token.getLineNumber(), nodes); } // find an appropriate parser for this name TokenParser tokenParser = this.tokenParsers.get(token.getValue()); if (tokenParser == null) { throw new ParserException(null, String.format("Unexpected tag name \"%s\"", token.getValue()), token.getLineNumber(), this.stream.getFilename()); } RenderableNode node = tokenParser.parse(token, this); // node might be null (ex. "extend" token parser) if (node != null) { nodes.add(node); } break; default: throw new ParserException(null, "Parser ended in undefined state.", this.stream.current().getLineNumber(), this.stream.getFilename()); } } // create the root node with the children that we have found return new BodyNode(this.stream.current().getLineNumber(), nodes); } @Override public TokenStream getStream() { return this.stream; } public void setStream(TokenStream stream) { this.stream = stream; } @Override public ExpressionParser getExpressionParser() { return this.expressionParser; } @Override public String peekBlockStack() { return this.blockStack.peek(); } @Override public String popBlockStack() { return this.blockStack.pop(); } @Override public void pushBlockStack(String blockName) { this.blockStack.push(blockName); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/parser/ParserOptions.java ================================================ package io.pebbletemplates.pebble.parser; /** * Parser options. * * @author yanxiyue */ public class ParserOptions { private boolean literalDecimalTreatedAsInteger; private boolean literalNumbersAsBigDecimals; public boolean isLiteralDecimalTreatedAsInteger() { return literalDecimalTreatedAsInteger; } public ParserOptions setLiteralDecimalTreatedAsInteger(boolean literalDecimalTreatedAsInteger) { this.literalDecimalTreatedAsInteger = literalDecimalTreatedAsInteger; return this; } public boolean isLiteralNumbersAsBigDecimals() { return literalNumbersAsBigDecimals; } public ParserOptions setLiteralNumbersAsBigDecimals(boolean literalNumbersAsBigDecimals) { this.literalNumbersAsBigDecimals = literalNumbersAsBigDecimals; return this; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/parser/StoppingCondition.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.parser; import io.pebbletemplates.pebble.lexer.Token; /** * Implementations of this class are provided by the TokenParsers and handed to the main Parser. The * main parser will parse some of the template until the stopping condition evaluates to true; at * this point responsibility is transferred back to the TokenParser. * * @author Mitchell */ public interface StoppingCondition { boolean evaluate(Token data); } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/template/Block.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.template; import java.io.IOException; import java.io.Writer; public interface Block { String getName(); void evaluate(PebbleTemplateImpl self, Writer writer, EvaluationContextImpl context) throws IOException; } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/template/EvaluationContext.java ================================================ package io.pebbletemplates.pebble.template; import java.util.Locale; /** * Created by mitchell on 2016-11-13. */ public interface EvaluationContext { boolean isStrictVariables(); Locale getLocale(); Object getVariable(String key); } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/template/EvaluationContextImpl.java ================================================ /* * This file is part of Pebble. *

* Copyright (c) 2014 by Mitchell Bösecke *

* For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.template; import io.pebbletemplates.pebble.cache.CacheKey; import io.pebbletemplates.pebble.cache.PebbleCache; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.extension.ExtensionRegistry; import io.pebbletemplates.pebble.utils.Callbacks; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.atomic.AtomicInteger; /** * An evaluation context will store all stateful data that is necessary for the evaluation of a * template. Passing the entire state around will assist with thread safety. * * @author Mitchell */ public class EvaluationContextImpl implements EvaluationContext, RenderedSizeContext { private final boolean strictVariables; /** * A template will look to it's parent and children for overridden macros and other features; this * inheritance chain will help the template keep track of where in the inheritance chain it * currently is. */ private final Hierarchy hierarchy; /** * A scope is a set of visible variables. A trivial template will only have one scope. New scopes * are added with for loops and macros for example. *

* Most scopes will have a link to their parent scope which allow an evaluation to look up the * scope chain for variables. A macro is an exception to this as it only has access to it's local * variables. */ private final ScopeChain scopeChain; /** * The locale of this template. */ private final Locale locale; /** * The maximum size of the rendered template, in chars. */ private final int maxRenderedSize; /** * All the available filters/tests/functions for this template. */ private final ExtensionRegistry extensionRegistry; /** * The tag cache */ private final PebbleCache tagCache; /** * The user-provided ExecutorService (can be null). */ private final ExecutorService executorService; /** * The imported templates are used to look up macros. */ private final List importedTemplates; /** * The named imported templates are used to look up macros. */ private final Map namedImportedTemplates; /** * evaluation options */ private final EvaluationOptions evaluationOptions; /** * Total number of chars written by all writers sharing this context. */ private final AtomicInteger charsRendered = new AtomicInteger(); /** * Constructor used to provide all final variables. * * @param self The template implementation * @param strictVariables Whether strict variables is to be used * @param locale The locale of the template * @param extensionRegistry The extension registry * @param executorService The optional executor service * @param scopeChain The scope chain * @param hierarchy The inheritance chain * @param tagCache The cache used by the "cache" tag */ public EvaluationContextImpl(PebbleTemplateImpl self, boolean strictVariables, Locale locale, int maxRenderedSize, ExtensionRegistry extensionRegistry, PebbleCache tagCache, ExecutorService executorService, List importedTemplates, Map namedImportedTemplates, ScopeChain scopeChain, Hierarchy hierarchy, EvaluationOptions evaluationOptions) { if (hierarchy == null) { hierarchy = new Hierarchy(self); } this.strictVariables = strictVariables; this.locale = locale; this.maxRenderedSize = maxRenderedSize; this.extensionRegistry = extensionRegistry; this.tagCache = tagCache; this.executorService = executorService; this.importedTemplates = importedTemplates; this.namedImportedTemplates = namedImportedTemplates; this.scopeChain = scopeChain; this.hierarchy = hierarchy; this.evaluationOptions = evaluationOptions; } /** * Makes an exact copy of the evaluation context EXCEPT for the inheritance chain. This is * necessary for the "include" tag. * * @param self The template implementation * @return A copy of the evaluation context */ public EvaluationContextImpl shallowCopyWithoutInheritanceChain(PebbleTemplateImpl self) { EvaluationContextImpl result = new EvaluationContextImpl(self, this.strictVariables, this.locale, this.maxRenderedSize, this.extensionRegistry, this.tagCache, this.executorService, this.importedTemplates, this.namedImportedTemplates, this.scopeChain, null, this.evaluationOptions); return result; } /** * Makes a "snapshot" of the evaluation context. The scopeChain object will be a deep copy and the * imported templates will be a new list. This is used for the "parallel" tag. * * @param self The template implementation * @return A copy of the evaluation context */ public EvaluationContextImpl threadSafeCopy(PebbleTemplateImpl self) { EvaluationContextImpl result = new EvaluationContextImpl(self, this.strictVariables, this.locale, this.maxRenderedSize, this.extensionRegistry, this.tagCache, this.executorService, new ArrayList<>(this.importedTemplates), new HashMap<>(this.namedImportedTemplates), this.scopeChain.deepCopy(), this.hierarchy, this.evaluationOptions); return result; } /** * Returns the named imported template. * * @return the named imported template. */ public PebbleTemplateImpl getNamedImportedTemplate(String alias) { return this.namedImportedTemplates.get(alias); } public void addNamedImportedTemplates(String alias, PebbleTemplateImpl template) { if (this.namedImportedTemplates.containsKey(alias)) { throw new PebbleException(null, "More than one named template can not share the same name: " + alias); } this.namedImportedTemplates.put(alias, template); } /** * Returns whether or not this template is being evaluated in "strict templates" mode * * @return Whether or not this template is being evaluated in "strict templates" mode. */ @Override public boolean isStrictVariables() { return this.strictVariables; } /** * Returns the locale * * @return The current locale */ @Override public Locale getLocale() { return this.locale; } /** * Returns the max rendered size. * @return The max rendered size. */ @Override public int getMaxRenderedSize() { return this.maxRenderedSize; } /** * Returns the extension registry used to access all of the tests/filters/functions * * @return The extension registry */ public ExtensionRegistry getExtensionRegistry() { return this.extensionRegistry; } /** * Returns the executor service if exists or null * * @return The executor service if exists, or null */ public ExecutorService getExecutorService() { return this.executorService; } /** * Returns a list of imported templates. * * @return A list of imported templates. */ public List getImportedTemplates() { return this.importedTemplates; } /** * Returns the cache used for the "cache" tag * * @return The cache used for the "cache" tag */ public PebbleCache getTagCache() { return this.tagCache; } /** * Returns the scope chain data structure that allows variables to be added/removed from the * current scope and retrieved from the nearest visible scopes. * * @return The scope chain. */ public ScopeChain getScopeChain() { return this.scopeChain; } /** * Returns the data structure representing the entire hierarchy of the template currently being * evaluated. * * @return The inheritance chain */ public Hierarchy getHierarchy() { return this.hierarchy; } /** * Returns the evaluation options. * * @return the evaluation options */ public EvaluationOptions getEvaluationOptions() { return this.evaluationOptions; } @Override public Object getVariable(String key) { return this.scopeChain.get(key); } private void pushScope( EvaluationContextImpl newContext, Map additionalVariables, Callbacks.PebbleConsumer scopedFunction ) throws IOException { ScopeChain scopeChain = newContext.getScopeChain(); // push a new local scope scopeChain.pushScope(); // if there are additional variables to be added to this scope, add them now if(additionalVariables != null) { for (Map.Entry entry : additionalVariables.entrySet()) { scopeChain.put((String) entry.getKey(), entry.getValue()); } } // run the callback that needs to be scoped scopedFunction.accept(newContext); // pop the new local scope scopeChain.popScope(); } public void scopedShallowWithoutInheritanceChain( PebbleTemplateImpl template, Map additionalVariables, Callbacks.PebbleConsumer scopedFunction) throws IOException { pushScope( this.shallowCopyWithoutInheritanceChain(template), additionalVariables, scopedFunction ); } @Override public int addAndGet(int delta) { return charsRendered.addAndGet(delta); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/template/EvaluationOptions.java ================================================ package io.pebbletemplates.pebble.template; import io.pebbletemplates.pebble.attributes.methodaccess.MethodAccessValidator; /** * Evaluation options. * * @author yanxiyue */ public class EvaluationOptions { /** * toggle to enable/disable greedy matching mode for finding java method */ private final boolean greedyMatchMethod; /** * Validator that can be used to validate object/method access */ private final MethodAccessValidator methodAccessValidator; public EvaluationOptions(boolean greedyMatchMethod, MethodAccessValidator methodAccessValidator) { this.greedyMatchMethod = greedyMatchMethod; this.methodAccessValidator = methodAccessValidator; } public boolean isGreedyMatchMethod() { return this.greedyMatchMethod; } public MethodAccessValidator getMethodAccessValidator() { return this.methodAccessValidator; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/template/GlobalContext.java ================================================ package io.pebbletemplates.pebble.template; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; public class GlobalContext implements Map { private final ScopeChain scopeChain; public GlobalContext(ScopeChain scopeChain) { this.scopeChain = scopeChain; } @Override public Object get(Object key) { List globalScopes = this.scopeChain.getGlobalScopes(); String keyAsString = String.valueOf(key); for (Scope scope : globalScopes) { Object result = scope.get(keyAsString); if (result != null) { return result; } else if (scope.containsKey(keyAsString)) { return null; } } return null; } @Override public boolean isEmpty() { return false; } @Override public int size() { throw new UnsupportedOperationException(); } @Override public boolean containsKey(Object key) { throw new UnsupportedOperationException(); } @Override public boolean containsValue(Object value) { throw new UnsupportedOperationException(); } @Override public Object put(String key, Object value) { throw new UnsupportedOperationException(); } @Override public Object remove(Object key) { throw new UnsupportedOperationException(); } @Override public void putAll(Map m) { throw new UnsupportedOperationException(); } @Override public void clear() { throw new UnsupportedOperationException(); } @Override public Set keySet() { throw new UnsupportedOperationException(); } @Override public Collection values() { throw new UnsupportedOperationException(); } @Override public Set> entrySet() { throw new UnsupportedOperationException(); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/template/Hierarchy.java ================================================ /* * This file is part of Pebble. *

* Copyright (c) 2014 by Mitchell Bösecke *

* For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.template; import java.util.ArrayList; /** * A data structure that represents the entire inheritance hierarchy of the current template and * tracks which level in the hierarchy we are currently evaluating. */ public class Hierarchy { /** * A list of all the templates in this hierarchy. A template at index i is the child to the * template at index i+1. */ private final ArrayList hierarchy = new ArrayList<>(2); /** * Index of the template currently being evaluated. */ private int current = 0; /** * Constructs an inheritance chain with one known template. * * @param currentTemplate The current template */ public Hierarchy(PebbleTemplateImpl currentTemplate) { this.hierarchy.add(currentTemplate); } /** * Adds a known ancestor onto the inheritance chain, does not increment which template is the * "current" template being evaluated. * * @param ancestor The ancestor template */ public void pushAncestor(PebbleTemplateImpl ancestor) { this.hierarchy.add(ancestor); } /** * Signifies that the parent template in the hierarchy is now being evaluated so it should be * considered the "current" template. */ public void ascend() { this.current++; } /** * Signifies that the child template in the hierarchy is now being evaluated so i t should be * considered the "current" template. */ public void descend() { this.current--; } /** * Returns the child of the template currently being evaluated or null if there is no child. * * @return The child template if exists or null */ public PebbleTemplateImpl getChild() { if (this.current == 0) { return null; } return this.hierarchy.get(this.current - 1); } /** * Returns the parent of the template currently being evaluated or null if there is no parent. * * @return The parent template if exists or null */ public PebbleTemplateImpl getParent() { if (this.current == this.hierarchy.size() - 1) { return null; } return this.hierarchy.get(this.current + 1); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/template/Macro.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.template; import io.pebbletemplates.pebble.extension.NamedArguments; import java.util.Map; public interface Macro extends NamedArguments { String getName(); String call(PebbleTemplateImpl self, EvaluationContextImpl context, Map args); } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/template/MacroAttributeProvider.java ================================================ package io.pebbletemplates.pebble.template; import io.pebbletemplates.pebble.node.ArgumentsNode; public class MacroAttributeProvider { private final PebbleTemplateImpl template; public MacroAttributeProvider(PebbleTemplateImpl template) { this.template = template; } /** * Invokes a macro * * @param context The evaluation context * @param macroName The name of the macro * @param args The arguments * @param ignoreOverriden Whether or not to ignore macro definitions in child template * @return The results of the macro invocation */ public Object macro(EvaluationContextImpl context, String macroName, ArgumentsNode args, boolean ignoreOverriden, int lineNumber) { return template.macro(context, macroName, args, ignoreOverriden, lineNumber); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/template/PebbleTemplate.java ================================================ /* * This file is part of Pebble. *

* Copyright (c) 2014 by Mitchell Bösecke *

* For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.template; import io.pebbletemplates.pebble.PebbleEngine; import io.pebbletemplates.pebble.node.RenderableNode; import java.io.IOException; import java.io.Writer; import java.util.Locale; import java.util.Map; /** * A template object constructed by an instance of {@link PebbleEngine}. * A template by itself is stateless and can therefore be re-used over and over to provide different * outputs depending on the variables that are provided at the time of evaluation. */ public interface PebbleTemplate { /** * Evaluate the template without any provided variables. This will use the default locale provided * by the {@link PebbleEngine}. * * @param writer The results of the evaluation are written to this writer. * @throws IOException An IO exception during the evaluation */ void evaluate(Writer writer) throws IOException; /** * Evaluate the template with a particular locale but without any provided variables. * * @param writer The results of the evaluation are written to this writer. * @param locale The locale used during the evaluation of the template. * @throws IOException An IO exception during the evaluation */ void evaluate(Writer writer, Locale locale) throws IOException; /** * Evaluate the template with a set of variables and the default locale provided by the {@link * PebbleEngine} * * @param writer The results of the evaluation are written to this writer. * @param context The variables used during the evaluation of the template. * @throws IOException An IO exception during the evaluation */ void evaluate(Writer writer, Map context) throws IOException; /** * Evaluate the template with a particular locale and a set of variables. * * @param writer The results of the evaluation are written to this writer. * @param context The variables used during the evaluation of the template. * @param locale The locale used during the evaluation of the template. * @throws IOException An IO exception during the evaluation */ void evaluate(Writer writer, Map context, Locale locale) throws IOException; /** * Evaluate the template but only render the contents of a specific block. * * @param blockName The name of the template block to return. * @param writer The results of the evaluation are written to this writer. * @throws IOException An IO exception during the evaluation */ void evaluateBlock(String blockName, Writer writer) throws IOException; /** * Evaluate the template but only render the contents of a specific block. * * @param blockName The name of the template block to return. * @param writer The results of the evaluation are written to this writer. * @param locale The locale used during the evaluation of the template. * @throws IOException An IO exception during the evaluation */ void evaluateBlock(String blockName, Writer writer, Locale locale) throws IOException; /** * Evaluate the template but only render the contents of a specific block. * * @param blockName The name of the template block to return. * @param writer The results of the evaluation are written to this writer. * @param context The variables used during the evaluation of the template. * @throws IOException An IO exception during the evaluation */ void evaluateBlock(String blockName, Writer writer, Map context) throws IOException; /** * Evaluate the template but only render the contents of a specific block. * * @param blockName The name of the template block to return. * @param writer The results of the evaluation are written to this writer. * @param context The variables used during the evaluation of the template. * @param locale The locale used during the evaluation of the template. * @throws IOException An IO exception during the evaluation */ void evaluateBlock(String blockName, Writer writer, Map context, Locale locale) throws IOException; /** * Returns the name of the template * * @return The name of the template */ String getName(); /** * Returns the root node of the template * @return The name of the template */ RenderableNode getRootNode(); } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/template/PebbleTemplateImpl.java ================================================ /* * This file is part of Pebble. *

* Copyright (c) 2014 by Mitchell Bösecke *

* For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.template; import io.pebbletemplates.pebble.PebbleEngine; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.extension.escaper.SafeString; import io.pebbletemplates.pebble.node.ArgumentsNode; import io.pebbletemplates.pebble.node.BlockNode; import io.pebbletemplates.pebble.node.BodyNode; import io.pebbletemplates.pebble.node.RenderableNode; import io.pebbletemplates.pebble.node.RootNode; import io.pebbletemplates.pebble.utils.FutureWriter; import io.pebbletemplates.pebble.utils.LimitedSizeWriter; import io.pebbletemplates.pebble.utils.Pair; import java.io.IOException; import java.io.Writer; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; /** * The actual implementation of a PebbleTemplate */ public class PebbleTemplateImpl implements PebbleTemplate { /** * A template has to store a reference to the main engine so that it can compile other templates * when using the "import" or "include" tags. *

* It will also retrieve some stateful information such as the default locale when necessary. * Luckily, the engine is immutable so this should be thread safe. */ private final PebbleEngine engine; /** * Blocks defined inside this template. */ private final Map blocks = new HashMap<>(); /** * Macros defined inside this template. */ private final Map macros = new HashMap<>(); /** * The root node of the AST to be rendered. */ private final RenderableNode rootNode; /** * Name of template. Used to help with debugging. */ private final String name; /** * Constructor * * @param engine The pebble engine used to construct this template * @param root The root not to evaluate * @param name The name of the template */ public PebbleTemplateImpl(PebbleEngine engine, RenderableNode root, String name) { this.engine = engine; this.rootNode = root; this.name = name; } public void evaluate(Writer writer) throws IOException { EvaluationContextImpl context = this.initContext(null); this.evaluate(writer, context); } public void evaluate(Writer writer, Locale locale) throws IOException { EvaluationContextImpl context = this.initContext(locale); this.evaluate(writer, context); } public void evaluate(Writer writer, Map map) throws IOException { EvaluationContextImpl context = this.initContext(null); context.getScopeChain().pushScope(map); // Issue #449: if the provided map is immutable, this allows us to still set variables in the template context context.getScopeChain().pushScope(new HashMap<>()); this.evaluate(writer, context); } public void evaluate(Writer writer, Map map, Locale locale) throws IOException { EvaluationContextImpl context = this.initContext(locale); context.getScopeChain().pushScope(map); // Issue #449: if the provided map is immutable, this allows us to still set variables in the template context context.getScopeChain().pushScope(new HashMap<>()); this.evaluate(writer, context); } public void evaluateBlock(String blockName, Writer writer) throws IOException { EvaluationContextImpl context = this.initContext(null); this.evaluate(new NoopWriter(), context); this.block(writer, context, blockName, false); writer.flush(); } public void evaluateBlock(String blockName, Writer writer, Locale locale) throws IOException { EvaluationContextImpl context = this.initContext(locale); this.evaluate(new NoopWriter(), context); this.block(writer, context, blockName, false); writer.flush(); } public void evaluateBlock(String blockName, Writer writer, Map map) throws IOException { EvaluationContextImpl context = this.initContext(null); context.getScopeChain().pushScope(map); this.evaluate(new NoopWriter(), context); this.block(writer, context, blockName, false); writer.flush(); } public void evaluateBlock(String blockName, Writer writer, Map map, Locale locale) throws IOException { EvaluationContextImpl context = this.initContext(locale); context.getScopeChain().pushScope(map); this.evaluate(new NoopWriter(), context); this.block(writer, context, blockName, false); writer.flush(); } /** * This is the authoritative evaluate method. It will evaluate the template starting at the root * node. * * @param writer The writer used to write the final output of the template * @param context The evaluation context * @throws IOException Thrown from the writer object */ private void evaluate(Writer writer, EvaluationContextImpl context) throws IOException { if (context.getExecutorService() != null) { writer = new FutureWriter(writer); } writer = LimitedSizeWriter.from(writer, context); this.rootNode.render(this, writer, context); /* * If the current template has a parent then we know the current template * was only used to evaluate a very small subset of tags such as "set" and "import". * We now evaluate the parent template as to evaluate all of the actual content. * When evaluating the parent template, it will check the child template for overridden blocks. */ if (context.getHierarchy().getParent() != null) { PebbleTemplateImpl parent = context.getHierarchy().getParent(); context.getHierarchy().ascend(); parent.evaluate(writer, context); } writer.flush(); } /** * Initializes the evaluation context with settings from the engine. * * @param locale The desired locale * @return The evaluation context */ private EvaluationContextImpl initContext(Locale locale) { locale = locale == null ? this.engine.getDefaultLocale() : locale; // globals ScopeChain scopeChain = new ScopeChain(); Map globals = new HashMap<>(); globals.put("locale", locale); globals.put("template", this); globals.put("_context", new GlobalContext(scopeChain)); scopeChain.pushScope(globals); // global vars provided from extensions scopeChain.pushScope(this.engine.getExtensionRegistry().getGlobalVariables()); return new EvaluationContextImpl(this, this.engine.isStrictVariables(), locale, this.engine.getMaxRenderedSize(), this.engine.getExtensionRegistry(), this.engine.getTagCache(), this.engine.getExecutorService(), new ArrayList<>(), new HashMap<>(), scopeChain, null, this.engine.getEvaluationOptions()); } /** * Return a shallow copy of this template. * * @return A new template instance with the same data */ private PebbleTemplateImpl shallowCopy() { PebbleTemplateImpl copy = new PebbleTemplateImpl(engine, rootNode, name); copy.blocks.putAll(this.blocks); copy.macros.putAll(this.macros); return copy; } /** * Imports a template. * * @param context The evaluation context * @param name The template name */ public void importTemplate(EvaluationContextImpl context, String name) { context.getImportedTemplates() .add((PebbleTemplateImpl) this.engine.getTemplate(this.resolveRelativePath(name))); } /** * Imports a named template. * * @param context The evaluation context * @param name The template name * @param alias The template alias */ public void importNamedTemplate(EvaluationContextImpl context, String name, String alias) { context.addNamedImportedTemplates(alias, (PebbleTemplateImpl) this.engine.getTemplate(this.resolveRelativePath(name))); } /** * Imports named macros from specified template. * * @param name The template name * @param namedMacros named macros */ public void importNamedMacrosFromTemplate(String name, List> namedMacros) { PebbleTemplateImpl templateImpl = (PebbleTemplateImpl) this.engine .getTemplate(this.resolveRelativePath(name)); for (Pair pair : namedMacros) { Macro m = templateImpl.macros.get(pair.getRight()); if (m == null) { throw new PebbleException(null, "Function or Macro [" + pair.getRight() + "] referenced by alias [" + pair.getLeft() + "] does not exist."); } this.registerMacro(pair.getLeft(), m); } } /** * Returns a named template. * * @param context The evaluation context * @param alias The template alias */ public PebbleTemplateImpl getNamedImportedTemplate(EvaluationContextImpl context, String alias) { return context.getNamedImportedTemplate(alias); } /** * Includes a template with {@code name} into this template. * * @param writer the writer to which the output should be written to. * @param context the context within which the template is rendered in. * @param name the name of the template to include. * @param additionalVariables the map with additional variables provided with the include tag to * add within the include tag. * @throws IOException Any error during the loading of the template */ public void includeTemplate(Writer writer, EvaluationContextImpl context, String name, Map additionalVariables) throws IOException { PebbleTemplateImpl template = (PebbleTemplateImpl) this.engine .getTemplate(this.resolveRelativePath(name)); EvaluationContextImpl newContext = context.shallowCopyWithoutInheritanceChain(template); ScopeChain scopeChain = newContext.getScopeChain(); scopeChain.pushScope(); for (Entry entry : additionalVariables.entrySet()) { scopeChain.put((String) entry.getKey(), entry.getValue()); } template.evaluate(writer, newContext); scopeChain.popScope(); } /** * Embed a template with {@code name} into this template and override its child blocks. This has the effect of * essentially "including" a template (as with the `include` tag), but its blocks may be overridden in the calling * template similar to extending a template. * * @param lineNo the line number of the node being evaluated * @param writer the writer to which the output should be written to. * @param context the context within which the template is rendered in. * @param name the name of the template to include. * @param additionalVariables the map with additional variables provided with the include tag to * add within the embed tag. * @param overriddenBlocks the blocks parsed out of the parent template that should override blocks in the embedded template * @throws IOException Any error during the loading of the template */ public void embedTemplate( int lineNo, Writer writer, EvaluationContextImpl context, String name, Map additionalVariables, List overriddenBlocks ) throws IOException { // get the template to embed String embeddedTemplateName = this.resolveRelativePath(name); // make a shallow copy of the template so we can safely modify its blocks without affecting other templates in the // template cache. Include and extend will use the same object from the cache, so we need to make sure embeds do not // impact those other tags or change anything in the cache. final PebbleTemplateImpl embeddedTemplate = ((PebbleTemplateImpl) this.engine.getTemplate(embeddedTemplateName)).shallowCopy(); // push a child scope based on the current scope context.scopedShallowWithoutInheritanceChain(embeddedTemplate, additionalVariables, (newContext) -> { // create a fake root template to act as the parent of the embedded template. That root node simply renders the // embedded template's own RootNode, but now we're able to isolate its template hierarchy and provide new blocks // into that hierarchy BodyNode embeddedTemplateBody = ((RootNode) embeddedTemplate.rootNode).getBody(); BodyNode bodyNode = new BodyNode(lineNo, Collections.singletonList(embeddedTemplateBody)); PebbleTemplateImpl fakeRootTemplate = new PebbleTemplateImpl(engine, bodyNode, embeddedTemplateName); // push the blocks from the embedded template into the fake root, to make sure they are able to rendered if they // are not overridden for(Block block : embeddedTemplate.blocks.values()) { fakeRootTemplate.registerBlock(block); } // push the overridden blocks into the embedded template, since they were added to the host template rather than // the embdedded template during parsing. Overridden blocks must be present in the embedded template. for(BlockNode blockNode : overriddenBlocks) { embeddedTemplate.registerBlock(blockNode.getBlock()); } // push the new fake template root into the child context so blocks are resolved properly. newContext.getHierarchy().pushAncestor(fakeRootTemplate); // evaluate the embedded template. Its blocks will now override those defined in the fake root template using the // same mechanism as for overriding blocks when extending a template embeddedTemplate.evaluate(writer, newContext); }); } /** * Checks if a macro exists * * @param macroName The name of the macro * @return Whether or not the macro exists */ public boolean hasMacro(String macroName) { return this.macros.containsKey(macroName); } /** * Checks if a block exists * * @param blockName The name of the block * @return Whether or not the block exists */ public boolean hasBlock(String blockName) { return this.blocks.containsKey(blockName); } /** * This method resolves the given relative path based on this template file path. * * @param relativePath the path which should be resolved. * @return the resolved path. */ public String resolveRelativePath(String relativePath) { String resolved = this.engine.getLoader().resolveRelativePath(relativePath, this.name); if (resolved == null) { return relativePath; } else { return resolved; } } /** * Registers a block. * * @param block The block */ public void registerBlock(Block block) { this.blocks.put(block.getName(), block); } /** * Registers a macro * * @param macro The macro */ public void registerMacro(Macro macro) { if (this.macros.containsKey(macro.getName())) { throw new PebbleException(null, "More than one macro can not share the same name: " + macro.getName()); } this.macros.put(macro.getName(), macro); } /** * Registers a macro with alias * * @param macro The macro * @throws PebbleException Throws exception if macro already exists with the same name */ public void registerMacro(String alias, Macro macro) { if (this.macros.containsKey(alias)) { throw new PebbleException(null, "More than one macro can not share the same name: " + alias); } this.macros.put(alias, macro); } /** * A typical block declaration will use this method which evaluates the block using the regular * user-provided writer. * * @param blockName The name of the block * @param context The evaluation context * @param ignoreOverriden Whether or not to ignore overriden blocks * @param writer The writer * @throws IOException Thrown from the writer object */ public void block(Writer writer, EvaluationContextImpl context, String blockName, boolean ignoreOverriden) throws IOException { Hierarchy hierarchy = context.getHierarchy(); PebbleTemplateImpl childTemplate = hierarchy.getChild(); // check child if (!ignoreOverriden && childTemplate != null) { hierarchy.descend(); childTemplate.block(writer, context, blockName, false); hierarchy.ascend(); // check this template } else if (this.blocks.containsKey(blockName)) { Block block = this.blocks.get(blockName); block.evaluate(this, writer, context); // delegate to parent } else { if (hierarchy.getParent() != null) { PebbleTemplateImpl parent = hierarchy.getParent(); hierarchy.ascend(); parent.block(writer, context, blockName, true); hierarchy.descend(); } } } /** * Invokes a macro * * @param context The evaluation context * @param macroName The name of the macro * @param args The arguments * @param ignoreOverriden Whether or not to ignore macro definitions in child template * @return The results of the macro invocation */ public SafeString macro(EvaluationContextImpl context, String macroName, ArgumentsNode args, boolean ignoreOverriden, int lineNumber) { SafeString result = null; boolean found = false; PebbleTemplateImpl childTemplate = context.getHierarchy().getChild(); // check child template first if (!ignoreOverriden && childTemplate != null) { found = true; context.getHierarchy().descend(); result = childTemplate.macro(context, macroName, args, false, lineNumber); context.getHierarchy().ascend(); // check current template } else if (this.hasMacro(macroName)) { found = true; Macro macro = this.macros.get(macroName); Map namedArguments = args.getArgumentMap(this, context, macro); result = new SafeString(macro.call(this, context, namedArguments)); } // check imported templates if (!found) { for (PebbleTemplateImpl template : context.getImportedTemplates()) { if (template.hasMacro(macroName)) { found = true; result = template.macro(context, macroName, args, false, lineNumber); // If a macro was found and executed, dont search for more break; } } } // delegate to parent template if (!found) { if (context.getHierarchy().getParent() != null) { PebbleTemplateImpl parent = context.getHierarchy().getParent(); context.getHierarchy().ascend(); result = parent.macro(context, macroName, args, true, lineNumber); context.getHierarchy().descend(); } else { throw new PebbleException(null, String.format("Function or Macro [%s] does not exist.", macroName), lineNumber, this.name); } } return result; } public void setParent(EvaluationContextImpl context, String parentName) { context.getHierarchy() .pushAncestor( (PebbleTemplateImpl) this.engine.getTemplate(this.resolveRelativePath(parentName))); } /** * Returns the template name * * @return The name of the template */ public String getName() { return this.name; } /** * Returns the root node of the template AST * * @return The root node of the template AST */ public RenderableNode getRootNode() { return this.rootNode; } private static class NoopWriter extends Writer { public void write(char[] cbuf, int off, int len) { } public void flush() { } public void close() { } } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/template/RenderedSizeContext.java ================================================ package io.pebbletemplates.pebble.template; public interface RenderedSizeContext { int getMaxRenderedSize(); int addAndGet(int delta); } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/template/Scope.java ================================================ /* * This file is part of Pebble. *

* Copyright (c) 2014 by Mitchell Bösecke *

* For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.template; import java.util.HashMap; import java.util.Set; import java.util.Map; /** * A scope is a map of variables. A "local" scope ensures that the search for a particular variable * will end at this scope whether or not it was found. * * @author Mitchell */ public class Scope { /** * A "local" scope ensures that the search for a particular variable will end at this scope * whether or not it was found. */ private final boolean local; /** * The map of variables known at this scope */ private final Map backingMap; /** * Constructor * * @param backingMap The backing map of variables * @param local Whether this scope is local or not */ public Scope(Map backingMap, boolean local) { this.backingMap = backingMap == null ? new HashMap<>() : backingMap; this.local = local; } /** * Creates a shallow copy of the Scope. *

* This is used for the parallel tag because every new thread should have a "snapshot" of the * scopes, i.e. one thread should not affect rendering output of another. *

* It will construct a new collection but it will contain references to all of the original * variables therefore it is not a deep copy. This is why it is import for the user to use * thread-safe variables when using the parallel tag. * * @return A copy of the scope */ public Scope shallowCopy() { Map backingMapCopy = new HashMap<>(this.backingMap); return new Scope(backingMapCopy, this.local); } /** * Adds a variable to this scope * * @param key The name of the variable * @param value The value of the variable */ public void put(String key, Object value) { this.backingMap.put(key, value); } /** * Retrieves the variable at this scope * * @param key The name of the variable * @return The value of the variable */ public Object get(String key) { return this.backingMap.get(key); } /** * Checks if this scope contains a variable of a certain name. * * @param key The name of the variable * @return boolean stating whether or not the backing map of this scope contains that variable */ public boolean containsKey(String key) { return this.backingMap.containsKey(key); } /** * Returns whether or not this scope is "local". * * @return boolean stating whether this scope is local or not. */ public boolean isLocal() { return this.local; } /** * Returns keys of all the variables at this scope. * * @return A set of keys */ public Set getKeys(){ return backingMap.keySet(); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/template/ScopeChain.java ================================================ /* * This file is part of Pebble. *

* Copyright (c) 2014 by Mitchell Bösecke *

* For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.template; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; /** * A stack data structure used to represent the scope of variables that are currently accessible. * Pushing a new scope will allow the template to add variables with names of pre-existing variables * without overriding the originals; to access the original variables you would pop the scope * again. */ public class ScopeChain { /** * The stack of scopes */ private LinkedList stack = new LinkedList<>(); /** * Constructs an empty scope chain without any known scopes. */ public ScopeChain() { } /** * Creates a deep copy of the ScopeChain. This is used for the parallel tag because every new * thread should have a "snapshot" of the scopes, i.e. if one thread adds a new object to a scope, * it should not be available to the other threads. *

* This will construct a new scope chain and new scopes but it will continue to have references to * the original user-provided variables. This is why it is important for the user to only provide * thread-safe variables when using the "parallel" tag. * * @return A copy of the scope chain */ public ScopeChain deepCopy() { ScopeChain copy = new ScopeChain(); for (Scope originalScope : this.stack) { copy.stack.add(originalScope.shallowCopy()); } return copy; } /** * Adds an empty non-local scope to the scope chain */ public void pushScope() { this.pushScope(new HashMap<>()); } /** * Adds a new non-local scope to the scope chain * * @param map The known variables of this scope. */ public void pushScope(Map map) { Scope scope = new Scope(map, false); this.stack.push(scope); } /** * Adds a new local scope to the scope chain */ public void pushLocalScope() { Scope scope = new Scope(new HashMap<>(), true); this.stack.push(scope); } /** * Pops the most recent scope from the scope chain. */ public void popScope() { this.stack.pop(); } /** * Adds a variable to the current scope. * * @param key The name of the variable * @param value The value of the variable */ public void put(String key, Object value) { this.stack.peek().put(key, value); } /** * Retrieves a variable from the scope chain, starting at the current scope and working it's way * up all visible scopes. * * @param key The name of the variable * @return The value of the variable */ public Object get(String key) { /* * The majority of time, the requested variable will be in the first * scope so we do a quick lookup in that scope before attempting to * create an iterator, etc. This is solely for performance. * null values must not be handled as "not present". */ Scope scope = this.stack.getFirst(); Object result = scope.get(key); if (result != null) { return result; } if (this.stack.size() > 1) { if (scope.isLocal() || scope.containsKey(key)) { // key could be defined with null and override another value below in the stack return null; } Iterator iterator = this.stack.iterator(); // account for the first lookup we did iterator.next(); while (iterator.hasNext()) { scope = iterator.next(); result = scope.get(key); if (result != null) { return result; } else if (scope.isLocal() || scope.containsKey(key)) { // null value return null; } } } return null; } /** * This method checks if the given {@code key} does exists within the scope chain. * * @param key the for which the the check should be executed for. * @return {@code true} when the key does exists or {@code false} when the given key does not * exists. */ public boolean containsKey(String key) { /* * The majority of time, the requested variable will be in the first * scope so we do a quick lookup in that scope before attempting to * create an iterator, etc. This is solely for performance. */ Scope scope = this.stack.getFirst(); if (scope.containsKey(key)) { return true; } if (scope.isLocal()) { return false; } Iterator iterator = this.stack.iterator(); // account for the first lookup we did iterator.next(); while (iterator.hasNext()) { scope = iterator.next(); if (scope.containsKey(key)) { return true; } if (scope.isLocal()) { return false; } } return false; } /** * Checks if the current scope contains a variable without then looking up the scope chain. * * @param variableName The name of the variable * @return Whether or not the variable exists in the current scope */ public boolean currentScopeContainsVariable(String variableName) { return this.stack.getFirst().containsKey(variableName); } /** * Sets the value of a variable in the first scope in the chain that already contains the * variable; adds a variable to the current scope if an existing variable is not found. * * @param key The name of the variable * @param value The value of the variable */ public void set(String key, Object value) { /* * The majority of time, the requested variable will be in the first * scope so we do a quick lookup in that scope before attempting to * create an iterator, etc. This is solely for performance. */ Scope scope = this.stack.getFirst(); if (scope.isLocal() || scope.containsKey(key)) { scope.put(key, value); return; } Iterator iterator = this.stack.iterator(); // account for the first lookup we did iterator.next(); while (iterator.hasNext()) { scope = iterator.next(); if (scope.isLocal() || scope.containsKey(key)) { scope.put(key, value); return; } } // no existing variable, create a new one this.put(key, value); } public List getGlobalScopes() { List globalScopes = new ArrayList<>(); Iterator iterator = this.stack.iterator(); while (iterator.hasNext()) { Scope scope = iterator.next(); if (scope.isLocal()) { globalScopes.clear(); } else { globalScopes.add(scope); } } return globalScopes; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/tokenParser/AutoEscapeTokenParser.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.tokenParser; import io.pebbletemplates.pebble.lexer.Token; import io.pebbletemplates.pebble.lexer.TokenStream; import io.pebbletemplates.pebble.node.AutoEscapeNode; import io.pebbletemplates.pebble.node.BodyNode; import io.pebbletemplates.pebble.node.RenderableNode; import io.pebbletemplates.pebble.parser.Parser; public class AutoEscapeTokenParser implements TokenParser { @Override public RenderableNode parse(Token token, Parser parser) { TokenStream stream = parser.getStream(); int lineNumber = token.getLineNumber(); String strategy = null; boolean active = true; // skip over the 'autoescape' token stream.next(); // did user specify active boolean? if (stream.current().test(Token.Type.NAME)) { active = Boolean.parseBoolean(stream.current().getValue()); stream.next(); } // did user specify a strategy? if (stream.current().test(Token.Type.STRING)) { strategy = stream.current().getValue(); stream.next(); } stream.expect(Token.Type.EXECUTE_END); // now we parse the block body BodyNode body = parser.subparse(tkn -> tkn.test(Token.Type.NAME, "endautoescape")); // skip the 'endautoescape' token stream.next(); stream.expect(Token.Type.EXECUTE_END); return new AutoEscapeNode(lineNumber, body, active, strategy); } @Override public String getTag() { return "autoescape"; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/tokenParser/BlockTokenParser.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.tokenParser; import io.pebbletemplates.pebble.error.ParserException; import io.pebbletemplates.pebble.lexer.Token; import io.pebbletemplates.pebble.lexer.TokenStream; import io.pebbletemplates.pebble.node.BlockNode; import io.pebbletemplates.pebble.node.BodyNode; import io.pebbletemplates.pebble.node.RenderableNode; import io.pebbletemplates.pebble.parser.Parser; public class BlockTokenParser implements TokenParser { @Override public RenderableNode parse(Token token, Parser parser) { TokenStream stream = parser.getStream(); int lineNumber = token.getLineNumber(); // skip over the 'block' token to the name token Token blockName = stream.next(); // expect a name or string for the new block if (!blockName.test(Token.Type.NAME) && !blockName.test(Token.Type.STRING)) { // we already know an error has occurred but let's just call the // typical "expect" method so that we know a proper error // message is given to user stream.expect(Token.Type.NAME); } // get the name of the new block String name = blockName.getValue(); // skip over name stream.next(); stream.expect(Token.Type.EXECUTE_END); parser.pushBlockStack(name); // now we parse the block body BodyNode blockBody = parser.subparse(tkn -> tkn.test(Token.Type.NAME, "endblock")); parser.popBlockStack(); //check endblock us exist with block or not Token endblock = stream.current(); if (!endblock.test(Token.Type.NAME, "endblock")) { throw new ParserException(null, "endblock tag should be present with block tag starting line number ", token.getLineNumber(), stream.getFilename()); } // skip the 'endblock' token stream.next(); // check if user included block name in endblock Token current = stream.current(); if (current.test(Token.Type.NAME, name) || current.test(Token.Type.STRING, name)) { stream.next(); } stream.expect(Token.Type.EXECUTE_END); return new BlockNode(lineNumber, name, blockBody); } @Override public String getTag() { return "block"; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/tokenParser/CacheTokenParser.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.tokenParser; import io.pebbletemplates.pebble.lexer.Token; import io.pebbletemplates.pebble.lexer.TokenStream; import io.pebbletemplates.pebble.node.BodyNode; import io.pebbletemplates.pebble.node.CacheNode; import io.pebbletemplates.pebble.node.RenderableNode; import io.pebbletemplates.pebble.node.expression.Expression; import io.pebbletemplates.pebble.parser.Parser; /** * Token parser for the cache tag * * @author Eric Bussieres */ public class CacheTokenParser implements TokenParser { public static final String TAG_NAME = "cache"; @Override public String getTag() { return TAG_NAME; } @Override public RenderableNode parse(Token token, Parser parser) { TokenStream stream = parser.getStream(); int lineNumber = token.getLineNumber(); // skip over the 'cache' token stream.next(); Expression expression = parser.getExpressionParser().parseExpression(); // Skip the expression stream.next(); // now we parse the cache body BodyNode cacheBody = parser.subparse(tkn -> tkn.test(Token.Type.NAME, "endcache")); // skip the 'endcache' token stream.next(); stream.expect(Token.Type.EXECUTE_END); return new CacheNode(lineNumber, expression, cacheBody); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/tokenParser/EmbedTokenParser.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.tokenParser; import io.pebbletemplates.pebble.error.ParserException; import io.pebbletemplates.pebble.lexer.Token; import io.pebbletemplates.pebble.lexer.TokenStream; import io.pebbletemplates.pebble.node.BlockNode; import io.pebbletemplates.pebble.node.EmbedNode; import io.pebbletemplates.pebble.node.RenderableNode; import io.pebbletemplates.pebble.node.expression.Expression; import io.pebbletemplates.pebble.node.expression.MapExpression; import io.pebbletemplates.pebble.parser.Parser; import java.util.ArrayList; import java.util.List; public class EmbedTokenParser implements TokenParser { private BlockTokenParser blockTokenParser = new BlockTokenParser(); @Override public RenderableNode parse(Token token, Parser parser) { TokenStream stream = parser.getStream(); int lineNumber = token.getLineNumber(); // skip over the 'embed' token stream.next(); Expression embedExpression = parser.getExpressionParser().parseExpression(); Token current = stream.current(); MapExpression mapExpression = null; // We check if there is an optional 'with' parameter on the embed tag. if (current.getType().equals(Token.Type.NAME) && current.getValue().equals("with")) { // Skip over 'with' stream.next(); Expression parsedExpression = parser.getExpressionParser().parseExpression(); if (parsedExpression instanceof MapExpression) { mapExpression = (MapExpression) parsedExpression; } else { throw new ParserException(null, String.format("Unexpected expression '%1s'.", parsedExpression .getClass().getCanonicalName()), token.getLineNumber(), stream.getFilename()); } } stream.expect(Token.Type.EXECUTE_END); List blocks = parseBlocks(token, parser, stream); return new EmbedNode(lineNumber, embedExpression, mapExpression, blocks); } private List parseBlocks(Token token, Parser parser, TokenStream stream) { List blocks = new ArrayList<>(); while(true) { BlockNode node = parseBlock(token, parser, stream); if(node == null) break; blocks.add(node); } return blocks; } private BlockNode parseBlock(Token token, Parser parser, TokenStream stream) { if(stream.current().test(Token.Type.TEXT)) { Token textToken = stream.expect(Token.Type.TEXT); if(textToken.getValue().trim().length() > 0) { throw new ParserException(null, "A template that extends another one cannot include content outside blocks. Did you forget to put the content inside a {% block %} tag?", textToken.getLineNumber(), stream.getFilename()); } } stream.expect(Token.Type.EXECUTE_START); // we're finished with blocks, expect {% endembed %} if(stream.current().test(Token.Type.NAME, "end" + this.getTag())) { stream.expect(Token.Type.NAME, "end" + this.getTag()); stream.expect(Token.Type.EXECUTE_END); return null; } // otherwise start parsing a block tag and let the actual BlockTokenParser do the rest (to make sure it parses the // same as top-level blocks) return (BlockNode) blockTokenParser.parse(token, parser); } @Override public String getTag() { return "embed"; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/tokenParser/ExtendsTokenParser.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.tokenParser; import io.pebbletemplates.pebble.lexer.Token; import io.pebbletemplates.pebble.lexer.TokenStream; import io.pebbletemplates.pebble.node.ExtendsNode; import io.pebbletemplates.pebble.node.RenderableNode; import io.pebbletemplates.pebble.node.expression.Expression; import io.pebbletemplates.pebble.parser.Parser; public class ExtendsTokenParser implements TokenParser { @Override public RenderableNode parse(Token token, Parser parser) { TokenStream stream = parser.getStream(); int lineNumber = token.getLineNumber(); // skip the 'extends' token stream.next(); Expression parentTemplateExpression = parser.getExpressionParser().parseExpression(); stream.expect(Token.Type.EXECUTE_END); return new ExtendsNode(lineNumber, parentTemplateExpression); } @Override public String getTag() { return "extends"; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/tokenParser/FilterTokenParser.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.tokenParser; import io.pebbletemplates.pebble.lexer.Token; import io.pebbletemplates.pebble.lexer.TokenStream; import io.pebbletemplates.pebble.node.BodyNode; import io.pebbletemplates.pebble.node.PrintNode; import io.pebbletemplates.pebble.node.RenderableNode; import io.pebbletemplates.pebble.node.expression.Expression; import io.pebbletemplates.pebble.node.expression.FilterExpression; import io.pebbletemplates.pebble.node.expression.RenderableNodeExpression; import io.pebbletemplates.pebble.parser.Parser; import java.util.ArrayList; import java.util.List; /** * Parses the "filter" tag. It has nothing to do with implementing normal filters. */ public class FilterTokenParser implements TokenParser { @Override public RenderableNode parse(Token token, Parser parser) { TokenStream stream = parser.getStream(); int lineNumber = token.getLineNumber(); // skip the 'filter' token stream.next(); List> filterInvocationExpressions = new ArrayList<>(); filterInvocationExpressions.add(parser.getExpressionParser().parseFilterInvocationExpression()); while (stream.current().test(Token.Type.OPERATOR, "|")) { // skip the '|' token stream.next(); filterInvocationExpressions .add(parser.getExpressionParser().parseFilterInvocationExpression()); } stream.expect(Token.Type.EXECUTE_END); BodyNode body = parser.subparse(tkn -> tkn.test(Token.Type.NAME, "endfilter")); stream.next(); stream.expect(Token.Type.EXECUTE_END); Expression lastExpression = new RenderableNodeExpression(body, stream.current().getLineNumber()); for (Expression filterInvocationExpression : filterInvocationExpressions) { FilterExpression filterExpression = new FilterExpression(); filterExpression.setRight(filterInvocationExpression); filterExpression.setLeft(lastExpression); lastExpression = filterExpression; } return new PrintNode(lastExpression, lineNumber); } @Override public String getTag() { return "filter"; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/tokenParser/FlushTokenParser.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.tokenParser; import io.pebbletemplates.pebble.lexer.Token; import io.pebbletemplates.pebble.lexer.TokenStream; import io.pebbletemplates.pebble.node.FlushNode; import io.pebbletemplates.pebble.node.RenderableNode; import io.pebbletemplates.pebble.parser.Parser; public class FlushTokenParser implements TokenParser { @Override public RenderableNode parse(Token token, Parser parser) { TokenStream stream = parser.getStream(); int lineNumber = token.getLineNumber(); // skip over the 'flush' token stream.next(); stream.expect(Token.Type.EXECUTE_END); return new FlushNode(lineNumber); } @Override public String getTag() { return "flush"; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/tokenParser/ForTokenParser.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.tokenParser; import io.pebbletemplates.pebble.error.ParserException; import io.pebbletemplates.pebble.lexer.Token; import io.pebbletemplates.pebble.lexer.TokenStream; import io.pebbletemplates.pebble.node.BodyNode; import io.pebbletemplates.pebble.node.ForNode; import io.pebbletemplates.pebble.node.RenderableNode; import io.pebbletemplates.pebble.node.expression.Expression; import io.pebbletemplates.pebble.parser.Parser; public class ForTokenParser implements TokenParser { @Override public RenderableNode parse(Token token, Parser parser) { TokenStream stream = parser.getStream(); int lineNumber = token.getLineNumber(); // skip the 'for' token stream.next(); // get the iteration variable String iterationVariable = parser.getExpressionParser().parseNewVariableName(); stream.expect(Token.Type.NAME, "in"); // get the iterable variable Expression iterable = parser.getExpressionParser().parseExpression(); stream.expect(Token.Type.EXECUTE_END); BodyNode body = parser.subparse(tkn -> tkn.test(Token.Type.NAME, "else", "endfor")); BodyNode elseBody = null; if (stream.current().test(Token.Type.NAME, "else")) { // skip the 'else' token stream.next(); stream.expect(Token.Type.EXECUTE_END); elseBody = parser.subparse(tkn -> tkn.test(Token.Type.NAME, "endfor")); } if (stream.current().getValue() == null) { throw new ParserException( null, "Unexpected end of template. Pebble was looking for the \"endfor\" tag", stream.current().getLineNumber(), stream.getFilename()); } // skip the 'endfor' token stream.next(); stream.expect(Token.Type.EXECUTE_END); return new ForNode(lineNumber, iterationVariable, iterable, body, elseBody); } @Override public String getTag() { return "for"; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/tokenParser/FromTokenParser.java ================================================ package io.pebbletemplates.pebble.tokenParser; import io.pebbletemplates.pebble.error.ParserException; import io.pebbletemplates.pebble.lexer.Token; import io.pebbletemplates.pebble.lexer.TokenStream; import io.pebbletemplates.pebble.node.FromNode; import io.pebbletemplates.pebble.node.RenderableNode; import io.pebbletemplates.pebble.node.expression.Expression; import io.pebbletemplates.pebble.parser.Parser; import io.pebbletemplates.pebble.utils.Pair; import java.util.ArrayList; import java.util.List; /** * From Token parser for * *

* {% from "templateName" import macroName as alias %} *

* * @author yanxiyue */ public class FromTokenParser implements TokenParser { @Override public RenderableNode parse(Token token, Parser parser) { TokenStream stream = parser.getStream(); int lineNumber = token.getLineNumber(); // skip over the 'from' token stream.next(); // parse the teamplateName expression Expression fromExpression = parser.getExpressionParser().parseExpression(); // parse the import List> namedMacros = parseNamedMacros(parser); stream.expect(Token.Type.EXECUTE_END); return new FromNode(lineNumber, fromExpression, namedMacros); } private List> parseNamedMacros(Parser parser) { List> pairs = new ArrayList<>(); TokenStream stream = parser.getStream(); stream.expect(Token.Type.NAME, "import"); Token pre, post; while (!stream.current().getType().equals(Token.Type.EXECUTE_END)) { pre = stream.expect(Token.Type.NAME); if (stream.current().test(Token.Type.NAME, "as")) { // Skips over 'as' stream.next(); post = stream.expect(Token.Type.NAME); pairs.add(new Pair<>(post.getValue(), pre.getValue())); } else { pairs.add(new Pair<>(pre.getValue(), pre.getValue())); } Token token = stream.current(); if (token.test(Token.Type.PUNCTUATION, ",")) { // Skips over ',' stream.next(); } else if (token.getType().equals(Token.Type.EXECUTE_END)) { break; } else { String message = String.format( "Unexpected token of value \"%s\" and type %s, expected token of type %s or ',' ", token.getValue(), token.getType().toString(), Token.Type.EXECUTE_END); throw new ParserException(null, message, token.getLineNumber(), stream.getFilename()); } } return pairs; } @Override public String getTag() { return "from"; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/tokenParser/IfTokenParser.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.tokenParser; import io.pebbletemplates.pebble.error.ParserException; import io.pebbletemplates.pebble.lexer.Token; import io.pebbletemplates.pebble.lexer.TokenStream; import io.pebbletemplates.pebble.node.BodyNode; import io.pebbletemplates.pebble.node.IfNode; import io.pebbletemplates.pebble.node.RenderableNode; import io.pebbletemplates.pebble.node.expression.Expression; import io.pebbletemplates.pebble.parser.Parser; import io.pebbletemplates.pebble.parser.StoppingCondition; import io.pebbletemplates.pebble.utils.Pair; import java.util.ArrayList; import java.util.List; public class IfTokenParser implements TokenParser { @Override public RenderableNode parse(Token token, Parser parser) { TokenStream stream = parser.getStream(); int lineNumber = token.getLineNumber(); // skip the 'if' token stream.next(); List, BodyNode>> conditionsWithBodies = new ArrayList<>(); Expression expression = parser.getExpressionParser().parseExpression(); stream.expect(Token.Type.EXECUTE_END); BodyNode body = parser.subparse(DECIDE_IF_FORK); conditionsWithBodies.add(new Pair<>(expression, body)); BodyNode elseBody = null; boolean end = false; while (!end) { if (stream.current().getValue() == null) { throw new ParserException( null, "Unexpected end of template. Pebble was looking for the \"endif\" tag", stream.current().getLineNumber(), stream.getFilename()); } switch (stream.current().getValue()) { case "else": stream.next(); stream.expect(Token.Type.EXECUTE_END); elseBody = parser.subparse(tkn -> tkn.test(Token.Type.NAME, "endif")); break; case "elseif": stream.next(); expression = parser.getExpressionParser().parseExpression(); stream.expect(Token.Type.EXECUTE_END); body = parser.subparse(DECIDE_IF_FORK); conditionsWithBodies.add(new Pair<>(expression, body)); break; case "endif": stream.next(); end = true; break; default: throw new ParserException( null, "Unexpected end of template. Pebble was looking for the following tags \"else\", \"elseif\", or \"endif\"", stream.current().getLineNumber(), stream.getFilename()); } } stream.expect(Token.Type.EXECUTE_END); return new IfNode(lineNumber, conditionsWithBodies, elseBody); } private static final StoppingCondition DECIDE_IF_FORK = token -> token .test(Token.Type.NAME, "elseif", "else", "endif"); @Override public String getTag() { return "if"; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/tokenParser/ImportTokenParser.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.tokenParser; import io.pebbletemplates.pebble.lexer.Token; import io.pebbletemplates.pebble.lexer.TokenStream; import io.pebbletemplates.pebble.node.ImportNode; import io.pebbletemplates.pebble.node.RenderableNode; import io.pebbletemplates.pebble.node.expression.Expression; import io.pebbletemplates.pebble.parser.Parser; public class ImportTokenParser implements TokenParser { @Override public RenderableNode parse(Token token, Parser parser) { TokenStream stream = parser.getStream(); int lineNumber = token.getLineNumber(); // skip over the 'import' token stream.next(); Expression importExpression = parser.getExpressionParser().parseExpression(); Token current = stream.current(); String alias = null; // We check if there is an optional 'as' keyword on the import tag. if (current.getType().equals(Token.Type.NAME) && current.getValue().equals("as")) { // Skip over 'as' stream.next(); current = stream.expect(Token.Type.NAME); alias = current.getValue(); } stream.expect(Token.Type.EXECUTE_END); return new ImportNode(lineNumber, importExpression, alias); } @Override public String getTag() { return "import"; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/tokenParser/IncludeTokenParser.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.tokenParser; import io.pebbletemplates.pebble.error.ParserException; import io.pebbletemplates.pebble.lexer.Token; import io.pebbletemplates.pebble.lexer.TokenStream; import io.pebbletemplates.pebble.node.IncludeNode; import io.pebbletemplates.pebble.node.RenderableNode; import io.pebbletemplates.pebble.node.expression.Expression; import io.pebbletemplates.pebble.node.expression.MapExpression; import io.pebbletemplates.pebble.parser.Parser; public class IncludeTokenParser implements TokenParser { @Override public RenderableNode parse(Token token, Parser parser) { TokenStream stream = parser.getStream(); int lineNumber = token.getLineNumber(); // skip over the 'include' token stream.next(); Expression includeExpression = parser.getExpressionParser().parseExpression(); Token current = stream.current(); MapExpression mapExpression = null; // We check if there is an optional 'with' parameter on the include tag. if (current.getType().equals(Token.Type.NAME) && current.getValue().equals("with")) { // Skip over 'with' stream.next(); Expression parsedExpression = parser.getExpressionParser().parseExpression(); if (parsedExpression instanceof MapExpression) { mapExpression = (MapExpression) parsedExpression; } else { throw new ParserException(null, String.format("Unexpected expression '%1s'.", parsedExpression .getClass().getCanonicalName()), token.getLineNumber(), stream.getFilename()); } } stream.expect(Token.Type.EXECUTE_END); return new IncludeNode(lineNumber, includeExpression, mapExpression); } @Override public String getTag() { return "include"; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/tokenParser/MacroTokenParser.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.tokenParser; import io.pebbletemplates.pebble.lexer.Token; import io.pebbletemplates.pebble.lexer.TokenStream; import io.pebbletemplates.pebble.node.ArgumentsNode; import io.pebbletemplates.pebble.node.BodyNode; import io.pebbletemplates.pebble.node.MacroNode; import io.pebbletemplates.pebble.node.RenderableNode; import io.pebbletemplates.pebble.parser.Parser; public class MacroTokenParser implements TokenParser { @Override public RenderableNode parse(Token token, Parser parser) { TokenStream stream = parser.getStream(); // skip over the 'macro' token stream.next(); String macroName = stream.expect(Token.Type.NAME).getValue(); ArgumentsNode args = parser.getExpressionParser().parseArguments(true); stream.expect(Token.Type.EXECUTE_END); // parse the body BodyNode body = parser.subparse(tkn -> tkn.test(Token.Type.NAME, "endmacro")); // skip the 'endmacro' token stream.next(); stream.expect(Token.Type.EXECUTE_END); return new MacroNode(macroName, args, body); } @Override public String getTag() { return "macro"; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/tokenParser/ParallelTokenParser.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.tokenParser; import io.pebbletemplates.pebble.lexer.Token; import io.pebbletemplates.pebble.lexer.TokenStream; import io.pebbletemplates.pebble.node.BodyNode; import io.pebbletemplates.pebble.node.ParallelNode; import io.pebbletemplates.pebble.node.RenderableNode; import io.pebbletemplates.pebble.parser.Parser; public class ParallelTokenParser implements TokenParser { @Override public RenderableNode parse(Token token, Parser parser) { TokenStream stream = parser.getStream(); int lineNumber = token.getLineNumber(); // skip the 'parallel' token stream.next(); stream.expect(Token.Type.EXECUTE_END); BodyNode body = parser.subparse(tkn -> tkn.test(Token.Type.NAME, "endparallel")); // skip the 'endparallel' token stream.next(); stream.expect(Token.Type.EXECUTE_END); return new ParallelNode(lineNumber, body); } @Override public String getTag() { return "parallel"; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/tokenParser/SetTokenParser.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.tokenParser; import io.pebbletemplates.pebble.lexer.Token; import io.pebbletemplates.pebble.lexer.TokenStream; import io.pebbletemplates.pebble.node.RenderableNode; import io.pebbletemplates.pebble.node.SetNode; import io.pebbletemplates.pebble.node.expression.Expression; import io.pebbletemplates.pebble.parser.Parser; public class SetTokenParser implements TokenParser { @Override public RenderableNode parse(Token token, Parser parser) { TokenStream stream = parser.getStream(); int lineNumber = token.getLineNumber(); // skip the 'set' token stream.next(); String name = parser.getExpressionParser().parseNewVariableName(); stream.expect(Token.Type.PUNCTUATION, "="); Expression value = parser.getExpressionParser().parseExpression(); stream.expect(Token.Type.EXECUTE_END); return new SetNode(lineNumber, name, value); } @Override public String getTag() { return "set"; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/tokenParser/TokenParser.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.tokenParser; import io.pebbletemplates.pebble.lexer.Token; import io.pebbletemplates.pebble.node.RenderableNode; import io.pebbletemplates.pebble.parser.Parser; /** * A TokenParser is responsible for converting a stream of Tokens into a Node. A TokenParser often * has to temporarily delegate responsibility to Pebble's main Parser or Pebble's ExpressionParser. * * @author Mitchell */ public interface TokenParser { /** * The "tag" is used to determine when to use a particular instance of a TokenParser. For example, * the TokenParser that handles the "block" tag would return "block" with this method. * * @return The tag used to define this TokenParser. */ String getTag(); /** * The TokenParser is responsible to convert all the necessary tokens into appropriate Nodes. It * can access tokens using parser.getTokenStream(). * * The tag may be self contained like the "extends" tag or it may have a start and end point with * content in the middle like the "block" tag. If it contains content in the middle, it can use * parser.subparse(stopCondition) to parse the middle content at which point responsibility comes * back to the TokenParser to parse the end point. * * It is the responsibility of the TokenParser to ensure that when it is complete, the "current" * token of the primary Parser's TokenStream is pointing to the NEXT token. USUALLY this means the * last statement in this parse method, immediately prior to the return statement, is the * following which will consume one token: * * stream.expect(Token.Type.EXECUTE_END); * * Here are two relatively simple examples of how TokenParsers are implemented: * * - self contained: io.pebbletemplates.tokenParser.pebble.SetTokenParser - middle content: * io.pebbletemplates.tokenParser.pebble.BlockTokenParser * * @param token The token to parse * @param parser the parser which should be used to parse the token * @return A node representation of the token */ RenderableNode parse(Token token, Parser parser); } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/tokenParser/VerbatimTokenParser.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.tokenParser; import io.pebbletemplates.pebble.lexer.Token; import io.pebbletemplates.pebble.node.RenderableNode; import io.pebbletemplates.pebble.parser.Parser; /** * This is just a dummy class to point developers into the right direction; the verbatim tag had to * be implemented directly into the lexer. * * @author mbosecke */ public class VerbatimTokenParser implements TokenParser { @Override public RenderableNode parse(Token token, Parser parser) { throw new UnsupportedOperationException(); } @Override public String getTag() { return "verbatim"; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/utils/Callbacks.java ================================================ package io.pebbletemplates.pebble.utils; import java.io.IOException; public class Callbacks { public interface PebbleConsumer { void accept(T t) throws IOException; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/utils/FutureWriter.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.utils; import java.io.IOException; import java.io.Writer; import java.util.LinkedList; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; /** * A Writer that will wrap around the user-provided writer if the user also provided an * ExecutorService to the main PebbleEngine. A FutureWriter is capable of handling Futures that will * return a string. * * It is not thread safe but that is okay. Each thread will have its own writer, provided by the * "parallel" node; i.e. they will never share writers. * * @author Mitchell */ public class FutureWriter extends Writer { private final LinkedList> orderedFutures = new LinkedList<>(); private final Writer internalWriter; private boolean closed = false; public FutureWriter(Writer writer) { this.internalWriter = writer; } public void enqueue(Future future) throws IOException { if (this.closed) { throw new IOException("Writer is closed"); } this.orderedFutures.add(future); } @Override public void write(final char[] cbuf, final int off, final int len) throws IOException { if (this.closed) { throw new IOException("Writer is closed"); } final String result = new String(cbuf, off, len); if (this.orderedFutures.isEmpty()) { this.internalWriter.write(result); } else { Future future = new Future() { @Override public boolean cancel(boolean mayInterruptIfRunning) { return false; } @Override public boolean isCancelled() { return false; } @Override public boolean isDone() { return true; } @Override public String get() { return result; } @Override public String get(long timeout, TimeUnit unit) { return null; } }; this.orderedFutures.add(future); } } @Override public void flush() throws IOException { for (Future future: this.orderedFutures) { try { String result = future.get(); this.internalWriter.write(result); this.internalWriter.flush(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } catch (ExecutionException e) { throw new IOException(e); } } this.orderedFutures.clear(); } @Override public void close() throws IOException { this.flush(); this.internalWriter.close(); this.closed = true; } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/utils/LimitedSizeWriter.java ================================================ package io.pebbletemplates.pebble.utils; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.template.RenderedSizeContext; import java.io.IOException; import java.io.Writer; /** * A Writer that will wrap around the internal writer if the user also provided a limit * on the size of the rendered template. The context is shared between all the writers * used to evaluate a template: the one supplied by the user when calling template.evaluate * as well as any internally created writers e.g. when evaluating a macro. * * There will be false positives. For example if a function writes something and its output * is passed to a filter than we count both the output of the function and the output of the * filter, when we should only count the output of the filter. This is fine because the user * can increase the maximum allowable size accordingly. The purpose here is not to be precise * but to protect against abuse. * * If the limit is reached a PebbleException will be thrown. * If the limit is negative then no checks will be performed and the original writer used as is. * * This is thread-safe if RenderedSizeContext is thread-safe. */ public class LimitedSizeWriter extends Writer { private final Writer internalWriter; private final RenderedSizeContext context; public static Writer from(Writer internalWriter, RenderedSizeContext context) { if (context.getMaxRenderedSize() < 0) { return internalWriter; } return new LimitedSizeWriter(internalWriter, context); } private LimitedSizeWriter(Writer internalWriter, RenderedSizeContext context) { if (context.getMaxRenderedSize() < 0) { throw new IllegalArgumentException("maxRenderedSize should not be negative"); } this.internalWriter = internalWriter; this.context = context; } @Override public void write(char[] cbuf, int off, int len) throws IOException { if (this.willExceedMaxChars(len)) { throw new PebbleException(null, String.format("Tried to write more than %d chars.", this.context.getMaxRenderedSize())); } this.internalWriter.write(cbuf, off, len); } @Override public void flush() throws IOException { this.internalWriter.flush(); } @Override public void close() throws IOException { this.internalWriter.close(); } @Override public String toString() { return internalWriter.toString(); } // This has the side effect of incrementing the number of chars written. // This is necessary to maintain thread-safety, otherwise two threads might check // the size before writing, both checks might be fine but the resulting output // will be greater than the limit. // If internalWriter.write throws than the content written and the count of chars // written will get out of sync, but that's fine because at that point we don't // care about accuracy anymore. private boolean willExceedMaxChars(int charsToWrite) { return this.context.addAndGet(charsToWrite) > this.context.getMaxRenderedSize(); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/utils/OperatorUtils.java ================================================ /* * This file is part of Pebble. *

* Copyright (c) 2014 by Mitchell Bösecke *

* For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.utils; import io.pebbletemplates.pebble.extension.escaper.SafeString; import java.math.BigDecimal; import java.math.MathContext; import java.util.Collection; import java.util.List; /** * This class acts as a sort of wrapper around Java's built in operators. This is necessary because * Pebble treats all user provided variables as Objects even if they were originally primitives. *

* It's important that this class mimics the natural type conversion that Java will apply when * performing operators. This can be found in section 5.6.2 of the Java 7 spec, under Binary Numeric * Promotion. * * @author Mitchell */ public class OperatorUtils { private enum Operation { ADD, SUBTRACT, MULTIPLICATION, DIVISION, MODULUS } private enum Comparison { GREATER_THAN, GREATER_THAN_EQUALS, LESS_THAN, LESS_THAN_EQUALS, EQUALS } public static Object add(Object op1, Object op2) { if (op1 instanceof String || op2 instanceof String) { return concatenateStrings(String.valueOf(op1), String.valueOf(op2)); } else if (op1 instanceof SafeString || op2 instanceof SafeString) { return concatenateStrings(String.valueOf(op1), String.valueOf(op2)); } else if (op1 instanceof List) { return addToList((List) op1, op2); } return wideningConversionBinaryOperation(op1, op2, Operation.ADD); } public static Object subtract(Object op1, Object op2) { if (op1 instanceof List) { return subtractFromList((List) op1, op2); } return wideningConversionBinaryOperation(op1, op2, Operation.SUBTRACT); } public static Object multiply(Object op1, Object op2) { return wideningConversionBinaryOperation(op1, op2, Operation.MULTIPLICATION); } public static Object divide(Object op1, Object op2) { return wideningConversionBinaryOperation(op1, op2, Operation.DIVISION); } public static Object mod(Object op1, Object op2) { return wideningConversionBinaryOperation(op1, op2, Operation.MODULUS); } public static boolean equals(Object op1, Object op2) { if (op1 instanceof Number && op2 instanceof Number) { return wideningConversionBinaryComparison(op1, op2, Comparison.EQUALS); } else if (op1 instanceof Enum && op2 instanceof String) { return compareEnum((Enum) op1, (String) op2); } else if (op2 instanceof Enum && op1 instanceof String) { return compareEnum((Enum) op2, (String) op1); } else { return ((op1 == op2) || ((op1 != null) && op1.equals(op2))); } } private static > boolean compareEnum(Enum enumVariable, String compareToString) { return enumVariable.name().equals(compareToString); } public static boolean gt(Object op1, Object op2) { return wideningConversionBinaryComparison(op1, op2, Comparison.GREATER_THAN); } public static boolean gte(Object op1, Object op2) { return wideningConversionBinaryComparison(op1, op2, Comparison.GREATER_THAN_EQUALS); } public static boolean lt(Object op1, Object op2) { return wideningConversionBinaryComparison(op1, op2, Comparison.LESS_THAN); } public static boolean lte(Object op1, Object op2) { return wideningConversionBinaryComparison(op1, op2, Comparison.LESS_THAN_EQUALS); } public static Object unaryPlus(Object op1) { return multiply(1, op1); } public static Object unaryMinus(Object op1) { return multiply(-1, op1); } private static Object concatenateStrings(String op1, String op2) { return op1 + op2; } /** * This is not a documented feature but we are leaving this in for now. I'm unsure if there is * demand for this feature. */ @Deprecated @SuppressWarnings({"unchecked", "rawtypes"}) private static Object addToList(List op1, Object op2) { if (op2 instanceof Collection) { op1.addAll((Collection) op2); } else { ((List) op1).add(op2); } return op1; } /** * This is not a documented feature but we are leaving this in for now. I'm unsure if there is * demand for this feature. */ @Deprecated @SuppressWarnings({"unchecked", "rawtypes"}) private static Object subtractFromList(List op1, Object op2) { if (op2 instanceof Collection) { op1.removeAll((Collection) op2); } else { op1.remove(op2); } return op1; } private static Object wideningConversionBinaryOperation(Object op1, Object op2, Operation operation) { Number num1 = toNumber(op1); Number num2 = toNumber(op2); if (num1 instanceof BigDecimal || num2 instanceof BigDecimal) { return bigDecimalOperation(BigDecimal.valueOf(num1.doubleValue()), BigDecimal.valueOf(num2.doubleValue()), operation); } if (num1 instanceof Double || num2 instanceof Double) { return doubleOperation(num1.doubleValue(), num2.doubleValue(), operation); } if (num1 instanceof Float || num2 instanceof Float) { return floatOperation(num1.floatValue(), num2.floatValue(), operation); } if (num1 instanceof Long || num2 instanceof Long) { return longOperation(num1.longValue(), num2.longValue(), operation); } return integerOperation(num1.intValue(), num2.intValue(), operation); } private static boolean wideningConversionBinaryComparison(Object op1, Object op2, Comparison comparison) { if (op1 == null || op2 == null) { return false; } Number num1; Number num2; try { num1 = (Number) op1; num2 = (Number) op2; } catch (ClassCastException ex) { throw new RuntimeException( String .format("invalid operands for mathematical comparison [%s]", comparison.toString())); } return doubleComparison(num1.doubleValue(), num2.doubleValue(), comparison); } private static double doubleOperation(double op1, double op2, Operation operation) { switch (operation) { case ADD: return op1 + op2; case SUBTRACT: return op1 - op2; case MULTIPLICATION: return op1 * op2; case DIVISION: return op1 / op2; case MODULUS: return op1 % op2; default: throw new RuntimeException("Bug in OperatorUtils in pebble library"); } } private static boolean doubleComparison(double op1, double op2, Comparison comparison) { switch (comparison) { case GREATER_THAN: return op1 > op2; case GREATER_THAN_EQUALS: return op1 >= op2; case LESS_THAN: return op1 < op2; case LESS_THAN_EQUALS: return op1 <= op2; case EQUALS: return op1 == op2; default: throw new RuntimeException("Bug in OperatorUtils in pebble library"); } } private static BigDecimal bigDecimalOperation(BigDecimal op1, BigDecimal op2, Operation operation) { switch (operation) { case ADD: return op1.add(op2); case SUBTRACT: return op1.subtract(op2); case MULTIPLICATION: return op1.multiply(op2, MathContext.DECIMAL128); case DIVISION: return op1.divide(op2, MathContext.DECIMAL128); case MODULUS: return op1.remainder(op2, MathContext.DECIMAL128); default: throw new RuntimeException("Bug in OperatorUtils in pebble library"); } } private static Float floatOperation(Float op1, Float op2, Operation operation) { switch (operation) { case ADD: return op1 + op2; case SUBTRACT: return op1 - op2; case MULTIPLICATION: return op1 * op2; case DIVISION: return op1 / op2; case MODULUS: return op1 % op2; default: throw new RuntimeException("Bug in OperatorUtils in pebble library"); } } private static long longOperation(long op1, long op2, Operation operation) { switch (operation) { case ADD: return op1 + op2; case SUBTRACT: return op1 - op2; case MULTIPLICATION: return op1 * op2; case DIVISION: return op1 / op2; case MODULUS: return op1 % op2; default: throw new RuntimeException("Bug in OperatorUtils in pebble library"); } } private static long integerOperation(int op1, int op2, Operation operation) { switch (operation) { case ADD: return op1 + op2; case SUBTRACT: return op1 - op2; case MULTIPLICATION: return op1 * op2; case DIVISION: return op1 / op2; case MODULUS: return op1 % op2; default: throw new RuntimeException("Bug in OperatorUtils in pebble library"); } } static Number toNumber(Object obj) { if (obj instanceof Number) { return (Number) obj; } if (obj instanceof String) { String str = (String) obj; // If it looks like a decimal, parse as double if (str.contains(".") || str.contains("e") || str.contains("E")) { return Double.parseDouble(str); } // Otherwise parse as long (can hold larger values than int) return Long.parseLong(str); } throw new IllegalArgumentException("Cannot convert to Number: " + obj); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/utils/Pair.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.utils; /** * A small utility class used to pair relevant objects together. * * @author Mitchell */ public class Pair { private final L left; private final R right; public Pair(L left, R right) { this.left = left; this.right = right; } public L getLeft() { return this.left; } public R getRight() { return this.right; } @Override public String toString() { return String.format("(%s, %s)", this.left, this.right); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/utils/PathUtils.java ================================================ package io.pebbletemplates.pebble.utils; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Utility class to handle relative paths. * * @author Thomas Hunziker */ public final class PathUtils { public static final Pattern PATH_SEPARATOR_REGEX = Pattern.compile("[\\\\/]"); /** * Resolves the given {@code relativePath} based on the given {@code anchorPath}. * * @param relativePath the relative path which should be resolved. * @param anchorPath the anchor path based on which the relative path should be resolved on. * @param expectedSeparator The character expected to be used as a separator; dictated by the * Loader. * @return the resolved path or {@code null} when the path could not be resolved. */ public static String resolveRelativePath(String relativePath, String anchorPath, char expectedSeparator) { if (relativePath == null || relativePath.isEmpty()) { return null; } // ensure both paths use the same separator character relativePath = sanitize(relativePath, expectedSeparator); anchorPath = sanitize(anchorPath, expectedSeparator); if (relativePath.startsWith(".." + expectedSeparator) || relativePath .startsWith("." + expectedSeparator)) { return resolvePathInner(relativePath, anchorPath, expectedSeparator); } return null; } public static String sanitize(String path, char expectedSeparator) { return PATH_SEPARATOR_REGEX.matcher(path) .replaceAll(Matcher.quoteReplacement(String.valueOf(expectedSeparator))); } private static String resolvePathInner(String relativePath, String anchorPath, char separator) { StringBuilder resultingPath = new StringBuilder(); for (String segment : resolvePathSegments(determineAnchorPathSegments(anchorPath, separator), splitBySeparator(relativePath, separator))) { resultingPath.append(segment).append(separator); } // remove the erroneous separator added at the end return resultingPath.substring(0, resultingPath.length() - 1); } private static Collection determineAnchorPathSegments(String anchorPath, char separator) { if (anchorPath == null || anchorPath.isEmpty()) { return new ArrayList<>(); } ArrayDeque anchorPathSegments = new ArrayDeque<>( splitBySeparator(anchorPath, separator)); if (anchorPath.charAt(anchorPath.length() - 1) != separator) { anchorPathSegments.pollLast(); } return anchorPathSegments; } private static Collection resolvePathSegments(Collection anchorSegments, Collection relativeSegments) { ArrayDeque result = new ArrayDeque<>(anchorSegments); for (String segment : relativeSegments) { if (segment.equals(".")) { // do nothing } else if (segment.equals("..")) { result.pollLast(); } else { result.add(segment); } } return result; } private static List splitBySeparator(String path, char separator) { return Arrays.asList(path.split(Pattern.quote(String.valueOf(separator)))); } private PathUtils() { throw new IllegalAccessError(); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/utils/StringLengthComparator.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.utils; public class StringLengthComparator implements java.util.Comparator { public static StringLengthComparator INSTANCE = new StringLengthComparator(); private StringLengthComparator() { } public int compare(String s1, String s2) { return s2.length() - s1.length(); } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/utils/StringUtils.java ================================================ package io.pebbletemplates.pebble.utils; import java.math.BigDecimal; public class StringUtils { public static String ltrim(String input) { int i = 0; while (i < input.length() && Character.isWhitespace(input.charAt(i))) { i++; } return input.substring(i); } public static String rtrim(String input) { int i = input.length() - 1; while (i >= 0 && Character.isWhitespace(input.charAt(i))) { i--; } return input.substring(0, i + 1); } /** * Converts non-null objects into strings. It will use the toString() method of most objects but * handles some known exceptions. */ public static String toString(Object var) { if (var == null) { throw new IllegalArgumentException("Var can not be null"); } if (var instanceof BigDecimal) { return ((BigDecimal) var).toPlainString(); } else { return var.toString(); } } } ================================================ FILE: pebble/src/main/java/io/pebbletemplates/pebble/utils/TypeUtils.java ================================================ package io.pebbletemplates.pebble.utils; import java.math.BigDecimal; import java.math.BigInteger; import java.util.Date; /** * A small utility class to handle type operation. * * @author yanxiyue */ public class TypeUtils { public static Object[] compatibleCast(Object[] argumentValues, Class[] parameterTypes) { if (argumentValues == null || parameterTypes == null) { return argumentValues; } Object[] result = new Object[argumentValues.length]; for (int i = 0; i < result.length; i++) { result[i] = compatibleCast(argumentValues[i], parameterTypes[i]); } return result; } @SuppressWarnings("unchecked") public static T compatibleCast(Object value, Class type) { if (value == null || type == null || type.isAssignableFrom(value.getClass())) { return (T) value; } if (value instanceof Number) { Number number = (Number) value; if (type == int.class || type == Integer.class) { return (T) (Integer) number.intValue(); } if (type == long.class || type == Long.class) { return (T) (Long) number.longValue(); } if (type == double.class || type == Double.class) { return (T) (Double) number.doubleValue(); } if (type == float.class || type == Float.class) { return (T) (Float) number.floatValue(); } if (type == byte.class || type == Byte.class) { return (T) (Byte) number.byteValue(); } if (type == short.class || type == Short.class) { return (T) (Short) number.shortValue(); } if (type == BigInteger.class) { return (T) BigInteger.valueOf(number.longValue()); } if (type == BigDecimal.class) { return (T) BigDecimal.valueOf(number.doubleValue()); } if (type == Date.class) { return (T) new Date(number.longValue()); } if (type == Boolean.class) { double d = number.doubleValue(); return (T) (Boolean) (!Double.isNaN(d) && d != 0.0); } } if (value instanceof String) { String str = (String) value; if (type == Boolean.class) { return (T) (Boolean) !str.isEmpty(); } } return (T) value; } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/ArgumentsNodeTest.java ================================================ package io.pebbletemplates.pebble; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.loader.StringLoader; import io.pebbletemplates.pebble.node.ArgumentsNode; import io.pebbletemplates.pebble.template.PebbleTemplate; import org.junit.jupiter.api.Test; import java.io.StringWriter; import java.io.Writer; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; /** * Tests {@link ArgumentsNode}. */ class ArgumentsNodeTest { /** * Tests that the error description is clear when a invalid number of arguments are provided. */ @Test void testInvalidArgument() throws Exception { try { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble .getTemplate("{{ 'This is a test of the abbreviate filter' | abbreviate(16, 10) }}"); Writer writer = new StringWriter(); template.evaluate(writer); fail("Should not be reached, because an exception is expected."); } catch (PebbleException e) { assertEquals("{{ 'This is a test of the abbreviate filter' | abbreviate(16, 10) }}", e.getFileName()); assertEquals((Integer) 1, e.getLineNumber()); } } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/ArraySyntaxTest.java ================================================ /* * This file is part of Pebble. *

* Copyright (c) 2014 by Mitchell Bösecke *

* For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; import io.pebbletemplates.pebble.error.ParserException; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.loader.StringLoader; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.util.HashMap; import java.util.Map; import org.junit.jupiter.api.Test; class ArraySyntaxTest { @Test void testArraySyntax() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ [] }}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("[]", writer.toString()); } @Test void testSimpleArray() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ ['first-name'] }}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("[first-name]", writer.toString()); } @Test void test2ElementArray() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ ['first-name','last-name'] }}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("[first-name, last-name]", writer.toString()); } @Test void test2ElementArray2() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ [ 'first-name' , 'last-name' ] }}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("[first-name, last-name]", writer.toString()); } @Test void testNElementArray() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ ['repeated-name','repeated-name','repeated-name','repeated-name','repeated-name','repeated-name','repeated-name'] }}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals( "[repeated-name, repeated-name, repeated-name, repeated-name, repeated-name, repeated-name, repeated-name]", writer.toString()); } /** * The template engine should thrown an exception when processing * a template that contains incomplete array syntax. */ @Test void testIncompleteArraySyntax() throws PebbleException { // given a Pebble Engine and template text that contains incomplete array syntax PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ [,] }}"; // A Parser Exception should be thrown with a message indicating that there was an unexpected token assertThatExceptionOfType(ParserException.class).isThrownBy(() -> { pebble.getTemplate(source); } ).withMessageStartingWith("Unexpected token"); } @SuppressWarnings("serial") @Test void testArrayWithExpressions() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ ['one', 2, three, numbers['four']] }}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("three", "3"); context.put("numbers", new HashMap() { { this.put("four", "4"); } }); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("[one, 2, 3, 4]", writer.toString()); } @SuppressWarnings({"serial", "unused"}) @Test void testArrayWithComplexExpressions() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ ['one' + 'plus', 2 - 1, three.number, numbers['four'][0], numbers ['five'] .value ] }}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("three", new Object() { public final Integer number = 3; }); context.put("numbers", new HashMap() { { this.put("four", new String[]{"4"}); this.put("five", new Object() { private final String value = "five"; public String getValue() { return this.value; } }); } }); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("[oneplus, 1, 3, 4, five]", writer.toString()); } @Test void testSetCommand() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% set arr = ['repeated-name',2*5] %}{{ arr }}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("[repeated-name, 10]", writer.toString()); } @Test void testSetCommand2() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% set arr = ['repeated-name',2*5] %}{{ arr [1] }}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("10", writer.toString()); } @Test void testFirstFilter() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% set arr = ['name',2*5] %}{{ arr | first }}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("name", writer.toString()); } @Test void testFirstFilter2() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ ['name',2*5] | first }}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("name", writer.toString()); } @Test void testJoinFilter() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% set arr = ['name',2*5] %}{{ arr | join(':') }}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("name:10", writer.toString()); } @Test void testJoinFilter2() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ ['name',2*5] | join(':') }}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("name:10", writer.toString()); } @Test void testLastFilter() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% set arr = ['name',2*5] %}{{ arr | last }}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("10", writer.toString()); } @Test void testLastFilter2() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ ['name',2*5] | last }}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("10", writer.toString()); } @Test void testSliceFilter() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% set arr = ['name',2*5,'three',1.9] %}{{ arr | slice(1,3) }}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("[10, three]", writer.toString()); } @Test void testSliceFilter2() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ ['name',2*5,'three',1.9] | slice(1,3) }}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("[10, three]", writer.toString()); } @Test void testSortFilter() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% set arr = [3,2,1,0] %}{{ arr | sort }}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("[0, 1, 2, 3]", writer.toString()); } @Test void testSortFilter2() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ [3,2,1,0] | sort }}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("[0, 1, 2, 3]", writer.toString()); } @Test void testSortFilterFromArray() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ 'q,g,s,c,w' | split(',') | sort }}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("[c, g, q, s, w]", writer.toString()); } @Test void testForTag() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% set names = ['Bob','Maria','John'] %}{% for name in names %}{{ name }}{% endfor %}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("BobMariaJohn", writer.toString()); } @Test void testForTag2() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% for name in ['Bob','Maria','John'] %}{{ name }}{% endfor %}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("BobMariaJohn", writer.toString()); } @Test void testForTagInvalidIterable() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% set myVar = 'somevalue' %}{% for myVal in myVar %}{{ myVal }}{% endfor %}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); try { template.evaluate(writer, new HashMap<>()); fail("Expected PebbleException"); } catch (PebbleException e) { assertEquals("Not an iterable object. Value = [somevalue] (" + source + ":1)", e.getMessage()); } } @Test void testForElseTag() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% for name in [] %}{{ name }}{% else %}{{ 'no name' }}{% endfor %}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("no name", writer.toString()); } @Test void testIfTag() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% set names = ['Bob','Maria','John'] %}{% if names is null %}{{ 'it is' }}{% else %}{{ 'it is not' }}{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("it is not", writer.toString()); } @Test void testIfTag2() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if ['Bob','Maria','John'] is null %}{{ 'it is' }}{% else %}{{ 'it is not' }}{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("it is not", writer.toString()); } @Test void testMacroTag() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% macro firstname(names) %}{{ names | first }}{% endmacro %}{{ firstname(['Bob','Maria','John']) }}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("Bob", writer.toString()); } @Test void testMacroTagNamedArguments() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% macro firstname(names) %}{{ names | first }}{% endmacro %}{{ firstname(names=['Bob','Maria','John']) }}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("Bob", writer.toString()); } @Test void testAdditionOverloading() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% set arr = [0,1] + 2 %}{{ arr }}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("[0, 1, 2]", writer.toString()); } @Test void testAdditionOverloading2() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% set arr = [0,1] + [2,3] %}{{ arr }}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("[0, 1, 2, 3]", writer.toString()); } @Test void testAdditionOverloading3() throws PebbleException, IOException { //Arrange PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% set arr = 1 + [0,1] %}{{ arr }}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); //Act + Assert // A Pebble Exception should be thrown with a message indicating that the addition operation failed assertThatExceptionOfType(PebbleException.class).isThrownBy(() -> { template.evaluate(writer, new HashMap<>()); } ).withMessageStartingWith("Could not perform addition"); } @Test void testSubtractionOverloading() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% set arr = [0,1,2] - 1 %}{{ arr }}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("[0, 2]", writer.toString()); } @Test void testSubtractionOverloading2() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% set arr = [0,1,2] - [0,2,3] %}{{ arr }}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("[1]", writer.toString()); } @Test void testSubtractionOverloading3() throws PebbleException, IOException { //Arrange PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% set arr = 1 - [0,2] %}{{ arr }}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); //Act + Assert // A Pebble Exception should be thrown with a message indicating that subtraction failed assertThatExceptionOfType(PebbleException.class).isThrownBy(() -> { template.evaluate(writer, new HashMap<>()); } ).withMessageStartingWith("Could not perform subtraction"); } @Test void testEmptyTest() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% set arr = [0,1,2] %}{% if arr is empty %}{{ 'true' }}{% else %}{{ 'false' }}{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("false", writer.toString()); } @Test void testEmptyTest2() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if [0,1,2] is empty %}{{ 'true' }}{% else %}{{ 'false' }}{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("false", writer.toString()); } @Test void testEmptyTest3() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if [] is not empty %}{{ 'true' }}{% else %}{{ 'false' }}{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("false", writer.toString()); } @Test void testIterableTest() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% set arr = [0,1,2] %}{% if arr is iterable %}{{ 'true' }}{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("true", writer.toString()); } @Test void testIterableTest2() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if [0,1,2] is iterable %}{{ 'true' }}{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("true", writer.toString()); } @Test void testContainsOperator() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% set arr = [0,1,2] %}{% if arr contains 1 %}true{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("true", writer.toString()); } @Test void testContainsOperator2() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if [0,1,2] contains 1 %}true{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("true", writer.toString()); } @Test void testContainsOperator3() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if [0,1,2] contains [1,2] %}true{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("true", writer.toString()); } @Test void testContainsOperator4() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if [0,1,2] contains 10 %}true{% else %}false{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("false", writer.toString()); } @Test void testContainsOperator5() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if [0,1,2] contains 1 and not ([0,1] contains 0) %}true{% else %}false{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("false", writer.toString()); } @Test void testNestedArrays() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% set arr = [[]] %}{{ arr }}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("[[]]", writer.toString()); } @Test void testNestedArrays2() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% set arr = [[],['test'],[['nested'],['arrays']]] %}{{ arr }}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("[[], [test], [[nested], [arrays]]]", writer.toString()); } @Test void testNestedArrays3() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ [[],['test'],[['nested'],['arrays']]] }}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("[[], [test], [[nested], [arrays]]]", writer.toString()); } @Test void testNestedMapInArray() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ [{1:1}] }}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("[{1=1}]", writer.toString()); } // subscript syntax regression tests @SuppressWarnings("serial") @Test void testProblematicSubscriptSyntax() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ person ['first-name'] }}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("person", new HashMap() { { this.put("first-name", "Bob"); } }); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("Bob", writer.toString()); } @SuppressWarnings("serial") @Test void testProblematicSubscriptSyntax2() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ person ['first-name'][0] }}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("person", new HashMap() { { this.put("first-name", new String[]{"Bob"}); } }); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("Bob", writer.toString()); } @SuppressWarnings({"serial", "unused"}) @Test void testProblematicSubscriptSyntax3() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ person ['first-name'] .name }}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("person", new HashMap() { { this.put("first-name", new Object() { private final String name = "Bob"; public String getName() { return this.name; } }); } }); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("Bob", writer.toString()); } @SuppressWarnings("serial") @Test void testProblematicSubscriptSyntax4() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if person ['first-name'] == 'Bob' %}{{ person ['first-name'] }}{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("person", new HashMap() { { this.put("first-name", "Bob"); } }); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("Bob", writer.toString()); } @SuppressWarnings("serial") @Test void testProblematicSubscriptSyntax5() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% set name = person ['first-name'] %}{{ name }}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("person", new HashMap() { { this.put("first-name", "Bob"); } }); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("Bob", writer.toString()); } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/AttributeSubscriptSyntaxTest.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.loader.StringLoader; import io.pebbletemplates.pebble.template.PebbleTemplate; import org.junit.jupiter.api.Test; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.util.HashMap; import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; class AttributeSubscriptSyntaxTest { @SuppressWarnings("serial") @Test void testAccessingValueWithSubscript() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ person['first-name'] }}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("person", new HashMap() { { this.put("first-name", "Bob"); } }); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("Bob", writer.toString()); } @SuppressWarnings("serial") @Test void testAccessingValueWithExpressionSubscript() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source1 = "{% set var = 'apple' %}{{ colors[var] }}"; PebbleTemplate template1 = pebble.getTemplate(source1); String source2 = "{% set var = 'pear' %}{{ colors[var] }}"; PebbleTemplate template2 = pebble.getTemplate(source2); Map context = new HashMap<>(); context.put("colors", new HashMap() { { this.put("apple", "red"); this.put("pear", "green"); } }); Writer writer1 = new StringWriter(); template1.evaluate(writer1, context); assertEquals("red", writer1.toString()); Writer writer2 = new StringWriter(); template2.evaluate(writer2, context); assertEquals("green", writer2.toString()); } @SuppressWarnings("serial") @Test void testAccessingValueWithIntegerExpressionSubscript() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source1 = "{{ colors[one] }}"; PebbleTemplate template1 = pebble.getTemplate(source1); String source2 = "{{ colors[two] }}"; PebbleTemplate template2 = pebble.getTemplate(source2); Map context = new HashMap<>(); context.put("colors", new HashMap() { { this.put(1L, "red"); this.put(2L, "green"); } }); context.put("one", 1L); context.put("two", 2L); Writer writer1 = new StringWriter(); template1.evaluate(writer1, context); assertEquals("red", writer1.toString()); Writer writer2 = new StringWriter(); template2.evaluate(writer2, context); assertEquals("green", writer2.toString()); } @SuppressWarnings("serial") @Test void testAccessingNestedValuesWithSubscript() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ person['name']['first'] }}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("person", new HashMap() { { this.put("name", new HashMap() { { this.put("first", "Bob"); } }); } }); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("Bob", writer.toString()); } @SuppressWarnings("serial") @Test void testMixAndMatchingAttributeSyntax() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ person['name'].first }}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("person", new HashMap() { { this.put("name", new HashMap() { { this.put("first", "Bob"); } }); } }); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("Bob", writer.toString()); source = "{{ person.name['first'] }}"; template = pebble.getTemplate(source); writer = new StringWriter(); template.evaluate(writer, context); assertEquals("Bob", writer.toString()); } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/CacheTest.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.template.PebbleTemplate; import org.junit.jupiter.api.Test; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.security.SecureRandom; import java.util.HashMap; import java.util.Map; import java.util.Random; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; import java.util.concurrent.atomic.AtomicInteger; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; class CacheTest { private static final String LINE_SEPARATOR = System.lineSeparator(); /** * There was once an issue where the cache was unable to differentiate between templates of the * same name but under different directories. */ @Test void templatesWithSameNameOverridingCache() throws PebbleException, IOException { PebbleEngine engine = new PebbleEngine.Builder().strictVariables(false).build(); PebbleTemplate cache1 = engine.getTemplate("templates/cache/cache1/template.cache.peb"); PebbleTemplate cache2 = engine.getTemplate("templates/cache/cache2/template.cache.peb"); Writer writer1 = new StringWriter(); Writer writer2 = new StringWriter(); cache1.evaluate(writer1); cache2.evaluate(writer2); String cache1Output = writer1.toString(); String cache2Output = writer2.toString(); assertNotEquals(cache1Output, cache2Output); } /** * There was an issue where each template was storing a reference to it's child and this was being * cached. This is an issue because a template can have many different children. */ @Test void ensureChildTemplateNotCached() throws PebbleException, IOException { PebbleEngine engine = new PebbleEngine.Builder().strictVariables(false).build(); PebbleTemplate cache1 = engine.getTemplate("templates/cache/template.cacheChild.peb"); PebbleTemplate cache2 = engine.getTemplate("templates/cache/template.cacheParent.peb"); Writer writer1 = new StringWriter(); Writer writer2 = new StringWriter(); cache1.evaluate(writer1); cache2.evaluate(writer2); String cache1Output = writer1.toString(); String cache2Output = writer2.toString(); assertEquals("child", cache1Output); assertEquals("parent", cache2Output); } /** * An issue occurred where the engine would mistake the existence of the template in it's cache * with the existence of the templates bytecode in the file managers cache. This lead to * compilation issues. * * It occurred when rendering two templates that share the same parent template. */ @Test void templateCachedButBytecodeCleared() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().strictVariables(false).build(); PebbleTemplate template1 = pebble.getTemplate("templates/template.parent.peb"); PebbleTemplate template2 = pebble.getTemplate("templates/template.parent2.peb"); Writer writer1 = new StringWriter(); Writer writer2 = new StringWriter(); template1.evaluate(writer1); template2.evaluate(writer2); assertEquals("GRANDFATHER TEXT ABOVE HEAD" + LINE_SEPARATOR + LINE_SEPARATOR + "\tPARENT HEAD" + LINE_SEPARATOR + LINE_SEPARATOR + "GRANDFATHER TEXT BELOW HEAD AND ABOVE FOOT" + LINE_SEPARATOR + LINE_SEPARATOR + "\tGRANDFATHER FOOT" + LINE_SEPARATOR + LINE_SEPARATOR + "GRANDFATHER TEXT BELOW FOOT", writer1.toString()); assertEquals("GRANDFATHER TEXT ABOVE HEAD" + LINE_SEPARATOR + LINE_SEPARATOR + "\tPARENT HEAD" + LINE_SEPARATOR + LINE_SEPARATOR + "GRANDFATHER TEXT BELOW HEAD AND ABOVE FOOT" + LINE_SEPARATOR + LINE_SEPARATOR + "\tGRANDFATHER FOOT" + LINE_SEPARATOR + LINE_SEPARATOR + "GRANDFATHER TEXT BELOW FOOT", writer2.toString()); } @Test void testConcurrentCacheHitting() throws InterruptedException, PebbleException { final PebbleEngine engine = new PebbleEngine.Builder().strictVariables(false).build(); final ExecutorService es = Executors.newCachedThreadPool(); final AtomicInteger totalFailed = new AtomicInteger(); int numOfConcurrentThreads = Math.min(4, Runtime.getRuntime().availableProcessors()); final Semaphore semaphore = new Semaphore(numOfConcurrentThreads); for (int i = 0; i < 100000; i++) { semaphore.acquire(); es.submit(() -> { try { PebbleTemplate template = engine.getTemplate("templates/template.concurrent1.peb"); int a = r.nextInt(); int b = r.nextInt(); int c = r.nextInt(); TestObject testObject = new TestObject(a, b, c); StringWriter writer = new StringWriter(); Map context = new HashMap<>(); context.put("test", testObject); template.evaluate(writer, context); String expectedResult = a + ":" + b + ":" + c; String actualResult = writer.toString(); if (!expectedResult.equals(actualResult)) { System.out.println("Expected: " + expectedResult); System.out.println("Actual: " + actualResult); totalFailed.incrementAndGet(); } } catch (IOException | PebbleException e) { e.printStackTrace(); totalFailed.incrementAndGet(); } finally { semaphore.release(); } }); // quick fail if (totalFailed.intValue() > 0) { break; } } // Wait for them all to complete semaphore.acquire(numOfConcurrentThreads); es.shutdown(); assertEquals(0, totalFailed.intValue()); } private static Random r = new SecureRandom(); public static class TestObject { final public int a; final public int b; final public int c; private TestObject(int a, int b, int c) { this.a = a; this.b = b; this.c = c; } } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/CompilerTest.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.loader.StringLoader; import io.pebbletemplates.pebble.template.PebbleTemplate; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.util.HashMap; import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; class CompilerTest { @Test void testCompile() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("hello {{ foo }}"); Map context = new HashMap<>(); context.put("foo", "BAR"); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("hello BAR", writer.toString()); } /** * There was an issue where one failed template would prevent future templates from being * compiled. */ @Test @Timeout(3) void testCompilationMutexIsAlwaysReleased() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().strictVariables(false).build(); try { pebble.getTemplate("non-existing"); } catch (Exception e) { } PebbleTemplate template = pebble.getTemplate("templates/template.general.peb"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("test", writer.toString()); } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/ConcurrencyTest.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble; import io.pebbletemplates.pebble.error.LoaderException; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.loader.Loader; import io.pebbletemplates.pebble.loader.StringLoader; import io.pebbletemplates.pebble.template.PebbleTemplate; import org.junit.jupiter.api.Test; import java.io.IOException; import java.io.Reader; import java.io.StringReader; import java.io.StringWriter; import java.security.SecureRandom; import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.Random; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; import java.util.concurrent.atomic.AtomicInteger; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; class ConcurrencyTest { private static Random r = new SecureRandom(); public static class TestObject { final public int a; final public int b; final public int c; final public String d; private TestObject(int a, int b, int c, String d) { this.a = a; this.b = b; this.c = c; this.d = d; } } @Test void testConcurrentEvaluation() throws InterruptedException, PebbleException { PebbleEngine engine = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String templateSource = "{{ test.a }}:{{ test.b }}:{{ test.c }}:{{ test.d | upper }}"; final PebbleTemplate template = engine.getTemplate(templateSource); ExecutorService es = Executors.newCachedThreadPool(); final AtomicInteger totalFailed = new AtomicInteger(); int numOfConcurrentEvaluations = Math.min(4, Runtime.getRuntime().availableProcessors()); final Semaphore semaphore = new Semaphore(numOfConcurrentEvaluations); for (int i = 0; i < 10000; i++) { semaphore.acquire(); es.submit(() -> { try { int a = r.nextInt(); int b = r.nextInt(); int c = r.nextInt(); int d = r.nextInt(); TestObject testObject = new TestObject(a, b, c, "test" + d); StringWriter writer = new StringWriter(); Map context = new HashMap<>(); context.put("test", testObject); template.evaluate(writer, context); String expectedResult = a + ":" + b + ":" + c + ":" + "TEST" + d; String actualResult = writer.toString(); if (!expectedResult.equals(actualResult)) { System.out.println("Expected: " + expectedResult); System.out.println("Actual: " + actualResult); System.out.println(); totalFailed.incrementAndGet(); } } catch (IOException | PebbleException e) { e.printStackTrace(); totalFailed.incrementAndGet(); } finally { semaphore.release(); } }); if (totalFailed.intValue() > 0) { break; } } // Wait for them all to complete semaphore.acquire(numOfConcurrentEvaluations); es.shutdown(); assertEquals(0, totalFailed.intValue()); } /** * True concurrent compilation is not currently supported. The pebble engine will only compile one * at a time. This test simply makes sure that if a user attempts to trigger concurrent * compilation everything still succeeds (despite it being executed one at a time behind the * scenes). */ @Test void testThreadSafeCompilationOfMultipleTemplates() throws InterruptedException, PebbleException { final PebbleEngine engine = new PebbleEngine.Builder().templateCache(null) .strictVariables(false).build(); final ExecutorService es = Executors.newCachedThreadPool(); final AtomicInteger totalFailed = new AtomicInteger(); int numOfConcurrentEvaluations = Math.min(4, Runtime.getRuntime().availableProcessors()); final Semaphore semaphore = new Semaphore(numOfConcurrentEvaluations); for (int i = 0; i < 1000; i++) { semaphore.acquire(1); es.submit(() -> { try { PebbleTemplate template = engine.getTemplate("templates/template.concurrent1.peb"); int a = r.nextInt(); int b = r.nextInt(); int c = r.nextInt(); int d = r.nextInt(); TestObject testObject = new TestObject(a, b, c, "test" + d); StringWriter writer = new StringWriter(); Map context = new HashMap<>(); context.put("test", testObject); template.evaluate(writer, context); String expectedResult = a + ":" + b + ":" + c; String actualResult = writer.toString(); if (!expectedResult.equals(actualResult)) { System.out.println("Expected1: " + expectedResult); System.out.println("Actual1: " + actualResult); totalFailed.incrementAndGet(); } } catch (IOException | PebbleException e) { e.printStackTrace(); totalFailed.incrementAndGet(); } finally { semaphore.release(); } }); es.submit(() -> { try { PebbleTemplate template = engine.getTemplate("templates/template.concurrent2.peb"); int a = r.nextInt(); int b = r.nextInt(); int c = r.nextInt(); int d = r.nextInt(); TestObject testObject = new TestObject(a, b, c, "test" + d); StringWriter writer = new StringWriter(); Map context = new HashMap<>(); context.put("test", testObject); template.evaluate(writer, context); String expectedResult = a + ":" + b + ":" + c; String actualResult = writer.toString(); if (!expectedResult.equals(actualResult)) { System.out.println("Expected2: " + expectedResult); System.out.println("Actual2: " + actualResult); totalFailed.incrementAndGet(); } } catch (IOException | PebbleException e) { e.printStackTrace(); totalFailed.incrementAndGet(); } finally { semaphore.release(); } }); if (totalFailed.intValue() > 0) { break; } } // Wait for them all to complete semaphore.acquire(numOfConcurrentEvaluations); es.shutdown(); assertEquals(0, totalFailed.intValue()); } /** * Issue #40 */ @Test void testConcurrentEvaluationWithDifferingLocals() throws InterruptedException, PebbleException { final PebbleEngine engine = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String templateSource = "{{ 2000.234 | numberformat }}"; final PebbleTemplate template = engine.getTemplate(templateSource); ExecutorService es = Executors.newCachedThreadPool(); final AtomicInteger totalFailed = new AtomicInteger(); int numOfConcurrentEvaluations = Math.min(4, Runtime.getRuntime().availableProcessors()); final Semaphore semaphore = new Semaphore(numOfConcurrentEvaluations); final String germanResult = "2.000,234"; final String canadianResult = "2,000.234"; for (int i = 0; i < 1000; i++) { semaphore.acquire(); es.submit(() -> { try { final boolean isGerman = r.nextBoolean(); final Locale locale = isGerman ? Locale.GERMANY : Locale.CANADA; final StringWriter writer = new StringWriter(); template.evaluate(writer, locale); final String expectedResult = isGerman ? germanResult : canadianResult; final String actualResult = writer.toString(); if (!expectedResult.equals(actualResult)) { System.out.println(String.format("Locale: %s\nExpected: %s\nActual: %s\n", locale.toString(), expectedResult, actualResult)); totalFailed.incrementAndGet(); } } catch (IOException | PebbleException e) { e.printStackTrace(); totalFailed.incrementAndGet(); } finally { semaphore.release(); } }); if (totalFailed.intValue() > 0) { break; } } // Wait for them all to complete semaphore.acquire(numOfConcurrentEvaluations); es.shutdown(); assertEquals(0, totalFailed.intValue()); } @Test void testConcurrentEvaluationWithImportingMacros() throws PebbleException, IOException { Loader loader = new Loader() { private final String TEMPLATE = "{% import \"macro\" %}" + "{{test(0)}}" + "{% for i in [1,2,3,4,5,6,7,8,9,10] %}" + " {% parallel %}" + " {% import \"macro\" %}" + " {{test(i)}}" + " {% endparallel %}" + "{% endfor %}"; private final String MACRO = "{% macro test(count) %}" + " test({{count}})" + "{% endmacro %}"; @Override public Reader getReader(String cacheKey) throws LoaderException { switch (cacheKey) { case "template": return new StringReader(this.TEMPLATE); case "macro": return new StringReader(this.MACRO); default: throw new IllegalStateException("No such file"); } } @Override public void setCharset(String charset) { throw new UnsupportedOperationException( "Not supported yet."); //To change body of generated methods, choose Tools | Templates. } @Override public void setPrefix(String prefix) { throw new UnsupportedOperationException( "Not supported yet."); //To change body of generated methods, choose Tools | Templates. } @Override public void setSuffix(String suffix) { throw new UnsupportedOperationException( "Not supported yet."); //To change body of generated methods, choose Tools | Templates. } @Override public String resolveRelativePath(String relativePath, String anchorPath) { return relativePath; } @Override public String createCacheKey(String templateName) { return templateName; } @Override public boolean resourceExists(String templateName) { return true; } }; PebbleEngine engine = new PebbleEngine.Builder().loader(loader).strictVariables(false).build(); PebbleTemplate template = engine.getTemplate("template"); StringWriter singleThreadResult = new StringWriter(); template.evaluate(singleThreadResult); ExecutorService executor = Executors.newCachedThreadPool(); engine = new PebbleEngine.Builder().loader(loader).executorService(executor) .strictVariables(false).build(); template = engine.getTemplate("template"); StringWriter multipleThreadResult = new StringWriter(); template.evaluate(multipleThreadResult); executor.shutdown(); assertEquals(singleThreadResult.toString(), multipleThreadResult.toString()); } @Test void testConcurrentEvaluationWithException() throws PebbleException { PebbleEngine engine = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String templateSource = "{% for i in [1,2,3,4,5,6,7,8,9,10] %}" + "{% parallel %}" + "{{test(i)}}" + "{% endparallel %}" + "{% endfor %}"; PebbleTemplate template = engine.getTemplate(templateSource); StringWriter singleThreadResult = new StringWriter(); try { template.evaluate(singleThreadResult); fail("Expecting the single thread evaluation to throw an exception"); } catch (Exception ex) { } ExecutorService executor = Executors.newCachedThreadPool(); engine = new PebbleEngine.Builder().loader(new StringLoader()).executorService(executor) .strictVariables(false).build(); template = engine.getTemplate(templateSource); StringWriter multipleThreadResult = new StringWriter(); try { template.evaluate(multipleThreadResult); fail("Expection the multi thread evaluation to throw an exception"); } catch (Exception ex) { } executor.shutdown(); assertEquals(singleThreadResult.toString(), multipleThreadResult.toString(), "Expection the result of multiple threads and single thread execution to match."); } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/ContextTest.java ================================================ /* * This file is part of Pebble. *

* Copyright (c) 2014 by Mitchell Bösecke *

* For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.error.RootAttributeNotFoundException; import io.pebbletemplates.pebble.loader.StringLoader; import io.pebbletemplates.pebble.template.PebbleTemplate; import org.junit.jupiter.api.Test; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.util.Collection; import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.function.BiConsumer; import java.util.function.BiFunction; import java.util.function.Function; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; class ContextTest { @SuppressWarnings("serial") @Test void testLazyMap() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(true).build(); PebbleTemplate template = pebble.getTemplate("{{ eager_key }} {{ lazy_key }}"); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap() { { this.put("eager_key", "eager_value"); } @Override public Object get(final Object key) { if ("lazy_key".equals(key)) { return "lazy_value"; } return super.get(key); } @Override public boolean containsKey(Object key) { if ("lazy_key".equals(key)) { return true; } return super.containsKey(key); } }); assertEquals("eager_value lazy_value", writer.toString()); } @Test void testMissingContextVariableWithoutStrictVariables() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ foo }}"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("", writer.toString()); } @Test void testMissingContextVariableWithStrictVariables() throws PebbleException, IOException { assertThrows(RootAttributeNotFoundException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(true).build(); PebbleTemplate template = pebble.getTemplate("{{ foo }}"); Writer writer = new StringWriter(); template.evaluate(writer); }); } @Test void testExistingButNullContextVariableWithStrictVariables() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(true).build(); PebbleTemplate template = pebble.getTemplate("{% if foo == null %}YES{% endif %}"); Map context = new HashMap<>(); context.put("foo", null); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("YES", writer.toString()); } @Test void testDefaultLocale() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false) .defaultLocale(Locale.CANADA_FRENCH).build(); PebbleTemplate template = pebble.getTemplate("{{ locale.language }}"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("fr", writer.toString()); } @Test void testLocaleProvidedDuringEvaluation() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false) .defaultLocale(Locale.CANADA).build(); PebbleTemplate template = pebble.getTemplate("{{ locale }}"); Writer writer = new StringWriter(); template.evaluate(writer, Locale.CANADA); assertEquals("en_CA", writer.toString()); } @Test void testGlobalTemplateName() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ template.name }}"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("{{ template.name }}", writer.toString()); } @Test void testImmutableMapThrows() throws PebbleException, IOException { assertThrows(UnsupportedOperationException.class, () -> { Map originalMap = new HashMap<>(); originalMap.put("contextVariable", "context variable value"); Map immutableMap = new ImmutableMap<>(originalMap); immutableMap.put("templateVariable", "template variable value"); }); } @Test void testImmutableContext() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine .Builder() .loader(new StringLoader()) .strictVariables(false) .build(); PebbleTemplate template = pebble.getTemplate("" + "{% set templateVariable = 'template variable value' %}" + "{{ contextVariable }} - {{ templateVariable }}"); Writer writer = new StringWriter(); Map originalMap = new HashMap<>(); originalMap.put("contextVariable", "context variable value"); template.evaluate(writer, new ImmutableMap<>(originalMap)); assertEquals("context variable value - template variable value", writer.toString()); } private static class ImmutableMap implements Map { private final Map delegate; private ImmutableMap(Map delegate) { this.delegate = delegate; } @Override public int size() { return this.delegate.size(); } @Override public boolean isEmpty() { return this.delegate.isEmpty(); } @Override public boolean containsKey(Object key) { return this.delegate.containsKey(key); } @Override public boolean containsValue(Object value) { return this.delegate.containsValue(value); } @Override public U get(Object key) { return this.delegate.get(key); } @Override public U put(T key, U value) { throw new UnsupportedOperationException("this map is immutable"); } @Override public U remove(Object key) { throw new UnsupportedOperationException("this map is immutable"); } @Override public void putAll(Map m) { throw new UnsupportedOperationException("this map is immutable"); } @Override public void clear() { throw new UnsupportedOperationException("this map is immutable"); } @Override public Set keySet() { return this.delegate.keySet(); } @Override public Collection values() { return this.delegate.values(); } @Override public Set> entrySet() { return this.delegate.entrySet(); } @Override public boolean equals(Object o) { return this.delegate.equals(o); } @Override public int hashCode() { return this.delegate.hashCode(); } @Override public U getOrDefault(Object key, U defaultValue) { return this.delegate.getOrDefault(key, defaultValue); } @Override public void forEach(BiConsumer action) { this.delegate.forEach(action); } @Override public void replaceAll(BiFunction function) { throw new UnsupportedOperationException("this map is immutable"); } @Override public U putIfAbsent(T key, U value) { throw new UnsupportedOperationException("this map is immutable"); } @Override public boolean remove(Object key, Object value) { throw new UnsupportedOperationException("this map is immutable"); } @Override public boolean replace(T key, U oldValue, U newValue) { throw new UnsupportedOperationException("this map is immutable"); } @Override public U replace(T key, U value) { throw new UnsupportedOperationException("this map is immutable"); } @Override public U computeIfAbsent(T key, Function mappingFunction) { throw new UnsupportedOperationException("this map is immutable"); } @Override public U computeIfPresent(T key, BiFunction remappingFunction) { throw new UnsupportedOperationException("this map is immutable"); } @Override public U compute(T key, BiFunction remappingFunction) { throw new UnsupportedOperationException("this map is immutable"); } @Override public U merge(T key, U value, BiFunction remappingFunction) { throw new UnsupportedOperationException("this map is immutable"); } } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/CoreFiltersTest.java ================================================ /* * This file is part of Pebble. *

* Copyright (c) 2014 by Mitchell Bösecke *

* For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.extension.TestingExtension; import io.pebbletemplates.pebble.extension.core.*; import io.pebbletemplates.pebble.loader.StringLoader; import io.pebbletemplates.pebble.template.PebbleTemplate; import org.junit.jupiter.api.Test; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.time.*; import java.util.*; import static java.lang.Boolean.TRUE; import static java.util.Collections.emptyList; import static org.junit.jupiter.api.Assertions.*; class CoreFiltersTest { @Test void testAbs() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ -5 | abs }}"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("5", writer.toString()); } @Test void testAbsDouble() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ -5.2 | abs }}"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("5.2", writer.toString()); } @Test void testChainedFiltersWithNullInput() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ null | upper | lower }}"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("", writer.toString()); } @Test void testLower() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ 'TEMPLATE' | lower }}"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("template", writer.toString()); } @Test void testLowerWithNullInput() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ null | lower }}"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("", writer.toString()); } @Test void testUpper() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ 'template' | upper }}"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("TEMPLATE", writer.toString()); } @Test void testUpperWithNullInput() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ null | upper }}"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("", writer.toString()); } @Test void testDate() throws ParseException, PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false) .defaultLocale(Locale.ENGLISH).build(); String source = "{{ realDate | date('MM/dd/yyyy') }}{{ realDate | date(format) }}{{ stringDate | date('yyyy/MMMM/d','yyyy-MMMM-d') }}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); DateFormat format = new SimpleDateFormat("yyyy-MMMM-d", Locale.ENGLISH); Date realDate = format.parse("2012-July-01"); context.put("realDate", realDate); context.put("stringDate", format.format(realDate)); context.put("format", "yyyy-MMMM-d"); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("07/01/20122012-July-12012/July/1", writer.toString()); } @Test void testStringDateWithOnlyFormat() throws PebbleException, IOException{ PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false) .defaultLocale(Locale.ENGLISH).build(); String source = "{{ stringDate | date('MM/dd/yyyy') }}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("stringDate", "2004-02-12T15:19:21+0400"); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("02/12/2004", writer.toString()); } @Test void testStringDateWithoutFormat() throws PebbleException, IOException{ PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false) .defaultLocale(Locale.ENGLISH).build(); String source = "{{ stringDate | date }}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("stringDate", "2004-02-12T15:19:21+0400"); Writer writer = new StringWriter(); template.evaluate(writer, context); assertTrue(writer.toString().startsWith("2004-02-12T")); } @Test void testStringDateWithoutFormatAndNotISO_DATE() throws PebbleException, IOException{ PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false) .defaultLocale(Locale.ENGLISH).build(); String source = "{{ stringDate | date }}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("stringDate", "2004-02-12"); Writer writer = new StringWriter(); PebbleException exception = assertThrows(PebbleException.class, () -> template.evaluate(writer, context)); assertEquals(exception.getMessage(), "Could not parse the string '2004-02-12' into a date, with formatting: yyyy-MM-dd'T'HH:mm:ssZ ({{ stringDate | date }}:1)"); } @Test void testDateJava8() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine .Builder() .loader(new StringLoader()) .strictVariables(false) .defaultLocale(Locale.ENGLISH) .build(); final LocalDateTime localDateTime = LocalDateTime.of(2017, 6, 30, 13, 30, 35, 0); final ZonedDateTime zonedDateTime = ZonedDateTime.of(localDateTime, ZoneId.of("GMT+0100")); final LocalDate localDate = localDateTime.toLocalDate(); final LocalTime localTime = localDateTime.toLocalTime(); StringBuilder source = new StringBuilder(); source .append("{{ localDateTime | date }}") .append("{{ localDateTime | date('yyyy-MM-dd HH:mm:ss') }}") .append("{{ zonedDateTime | date('yyyy-MM-dd HH:mm:ssXXX') }}") .append("{{ localDate | date('yyyy-MM-dd') }}") .append("{{ localTime | date('HH:mm:ss') }}"); PebbleTemplate template = pebble.getTemplate(source.toString()); Map context = new HashMap<>(); context.put("localDateTime", localDateTime); context.put("zonedDateTime", zonedDateTime); context.put("localDate", localDate); context.put("localTime", localTime); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals( "2017-06-30T13:30:352017-06-30 13:30:352017-06-30 13:30:35+01:002017-06-3013:30:35", writer.toString()); } @Test void testDateWithNamedArguments() throws ParseException, PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false) .defaultLocale(Locale.ENGLISH).build(); String source = "{{ stringDate | date(existingFormat='yyyy-MMMM-d', format='yyyy/MMMM/d') }}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); DateFormat format = new SimpleDateFormat("yyyy-MMMM-d", Locale.ENGLISH); Date realDate = format.parse("2012-July-01"); context.put("stringDate", format.format(realDate)); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("2012/July/1", writer.toString()); } @Test void testDateWithNullInput() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ null | date(\"MM/dd/yyyy\") }}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("", writer.toString()); } @Test void testDateWithNumberInput() throws IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ dateAsNumber | date(\"MM/dd/yyyy\") }}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("dateAsNumber", 1518004210000L); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("02/07/2018", writer.toString()); } @Test void testDateWithDateAndExplicitTimeZone() throws IOException { PebbleEngine pebble = new PebbleEngine.Builder() .loader(new StringLoader()) .build(); PebbleTemplate template = pebble.getTemplate("{{ date | date(timeZone=\"Asia/Almaty\") }}"); Map context = new HashMap<>(); context.put("date", Date.from(Instant.ofEpochSecond(1595853935))); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("2020-07-27T18:45:35+0600", writer.toString()); } @Test void testDateWithDateAndFormatAndExplicitTimeZone() throws IOException { PebbleEngine pebble = new PebbleEngine.Builder() .loader(new StringLoader()) .build(); PebbleTemplate template = pebble.getTemplate("{{ date | date(\"yyyy-MM-dd'T'HH:mm:ssX\", timeZone=\"Asia/Almaty\") }}"); Map context = new HashMap<>(); context.put("date", Date.from(Instant.ofEpochSecond(1595853935))); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("2020-07-27T18:45:35+06", writer.toString()); } @Test void testDateWithTimestampAndExplicitTimeZone() throws IOException { PebbleEngine pebble = new PebbleEngine.Builder() .loader(new StringLoader()) .build(); PebbleTemplate template = pebble.getTemplate("{{ timestamp | date(timeZone=\"Asia/Almaty\") }}"); Map context = new HashMap<>(); context.put("timestamp", 1595853935000L); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("2020-07-27T18:45:35+0600", writer.toString()); } @Test void testDateWithOffsetDateTimeAndExplicitTimeZoneUsesTimeZoneOfInput() throws IOException { PebbleEngine pebble = new PebbleEngine.Builder() .loader(new StringLoader()) .build(); PebbleTemplate template = pebble.getTemplate("{{ offsetDateTime | date(timeZone=\"Asia/Almaty\") }}"); Map context = new HashMap<>(); context.put("offsetDateTime", OffsetDateTime.of(2020, 7, 27, 16, 12, 13, 0, ZoneOffset.ofHours(3))); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("2020-07-27T16:12:13+03:00", writer.toString()); } @Test void testDateWithOffsetDateTimeAndFormatAndExplicitTimeZoneUsesTimeZoneOfInput() throws IOException { PebbleEngine pebble = new PebbleEngine.Builder() .loader(new StringLoader()) .build(); PebbleTemplate template = pebble.getTemplate("{{ offsetDateTime | date(\"yyyy-MM-dd'T'HH:mm:ssX\", timeZone=\"Asia/Almaty\") }}"); Map context = new HashMap<>(); context.put("offsetDateTime", OffsetDateTime.of(2020, 7, 27, 16, 12, 13, 0, ZoneOffset.ofHours(3))); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("2020-07-27T16:12:13+03", writer.toString()); } @Test void testDateWithOffsetDateTimeAndFormatAndNoExplicitTimeZoneUsesTimeZoneOfInput() throws IOException { PebbleEngine pebble = new PebbleEngine.Builder() .loader(new StringLoader()) .build(); PebbleTemplate template = pebble.getTemplate("{{ offsetDateTime | date(\"yyyy-MM-dd'T'HH:mm:ssX\") }}"); Map context = new HashMap<>(); context.put("offsetDateTime", OffsetDateTime.of(2020, 7, 27, 16, 12, 13, 0, ZoneOffset.ofHours(5))); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("2020-07-27T16:12:13+05", writer.toString()); } @Test void testDateWithInstantAndExplicitTimeZone() throws IOException { PebbleEngine pebble = new PebbleEngine.Builder() .loader(new StringLoader()) .build(); PebbleTemplate template = pebble.getTemplate("{{ instant | date(timeZone=\"Asia/Almaty\") }}"); Map context = new HashMap<>(); context.put("instant", Instant.ofEpochSecond(1595853935)); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("2020-07-27T18:45:35+06:00[Asia/Almaty]", writer.toString()); } @Test void testDateWithInstantAndNoExplicitTimeZoneUsesSystemTimeZone() throws IOException { PebbleEngine pebble = new PebbleEngine.Builder() .loader(new StringLoader()) .build(); TimeZone defaultTimeZone = TimeZone.getDefault(); try { TimeZone.setDefault(TimeZone.getTimeZone("Pacific/Funafuti")); PebbleTemplate template = pebble.getTemplate("{{ instant | date() }}"); Map context = new HashMap<>(); context.put("instant", Instant.ofEpochSecond(1595853935)); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("2020-07-28T00:45:35+12:00[Pacific/Funafuti]", writer.toString()); } finally { TimeZone.setDefault(defaultTimeZone); } } @Test void testDateWithUnsupportedInput() throws IOException { assertThrows(IllegalArgumentException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ unsupportedDateType | date(\"MM/dd/yyyy\") }}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("unsupportedDateType", TRUE); Writer writer = new StringWriter(); template.evaluate(writer, context); }); } @Test void testUrlEncode() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ 'The string ü@foo-bar' | urlencode }}"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("The+string+%C3%BC%40foo-bar", writer.toString()); } @Test void testUrlEncodeWithNullInput() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ null | urlencode }}"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("", writer.toString()); } @Test void testNumberFormatFilterWithFormat() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false) .defaultLocale(Locale.ENGLISH).build(); PebbleTemplate template = pebble .getTemplate("You owe me {{ 10000.235166 | numberformat(currencyFormat) }}."); Map context = new HashMap<>(); context.put("currencyFormat", "$#,###,###,##0.00"); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("You owe me $10,000.24.", writer.toString()); } @Test void testNumberFormatFilterWithNamedArgument() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false) .defaultLocale(Locale.US).build(); PebbleTemplate template = pebble .getTemplate("You owe me {{ 10000.235166 | numberformat(format=currencyFormat) }}."); Map context = new HashMap<>(); context.put("currencyFormat", "$#,###,###,##0.00"); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("You owe me $10,000.24.", writer.toString()); } @Test void testNumberFormatFilterWithNullInput() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ null | numberformat(currencyFormat) }}"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("", writer.toString()); } @Test void testNumberFormatFilterWithLocale() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false) .defaultLocale(Locale.ENGLISH).build(); PebbleTemplate template = pebble.getTemplate("{{ 1000000 | numberformat }}"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("1,000,000", writer.toString()); } @Test void testAbbreviate() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble .getTemplate("{{ 'This is a test of the abbreviate filter' | abbreviate(16) }}"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("This is a tes...", writer.toString()); } @Test void testAbbreviateWithNamedArguments() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble .getTemplate("{{ 'This is a test of the abbreviate filter' | abbreviate(length=16) }}"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("This is a tes...", writer.toString()); } @Test void testAbbreviateWithNullInput() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ null | abbreviate(16) }}"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("", writer.toString()); } @Test void testAbbreviateWithSmallLength() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ text | abbreviate(2)}}"); Map context = new HashMap<>(); context.put("text", "1234567"); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("12", writer.toString()); } @Test void testCapitalize() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble .getTemplate("{{ 'this should be capitalized.' | capitalize }}"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("This should be capitalized.", writer.toString()); } @Test void testCapitalizeWithLeadingWhitespace() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble .getTemplate("{{ ' \nthis should be capitalized.' | capitalize }}"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals(" \nThis should be capitalized.", writer.toString()); } @Test void testCapitalizeWithNullInput() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ null | capitalize }}"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("", writer.toString()); } @Test void testCapitalizeWithEmptyString() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ '' | capitalize }}"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("", writer.toString()); } @Test void testSortFilter() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble .getTemplate("{% for word in words|sort %}{{ word }} {% endfor %}"); List words = new ArrayList<>(); words.add("zebra"); words.add("apple"); words.add(" cat"); words.add("123"); words.add("Apple"); words.add("cat"); Map context = new HashMap<>(); context.put("words", words); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals(" cat 123 Apple apple cat zebra ", writer.toString()); } @Test void testRsortFilter() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble .getTemplate("{% for word in words|rsort %}{{ word }} {% endfor %}"); List words = new ArrayList<>(); words.add("zebra"); words.add("apple"); words.add(" cat"); words.add("123"); words.add("Apple"); words.add("cat"); Map context = new HashMap<>(); context.put("words", words); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("zebra cat apple Apple 123 cat ", writer.toString()); } @Test void testReverseFilter() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble .getTemplate("{% for word in words|reverse %}{{ word }} {% endfor %}"); List words = new ArrayList<>(); words.add("one"); words.add("two"); words.add("three"); Map context = new HashMap<>(); context.put("words", words); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("three two one ", writer.toString()); } @Test void testTitle() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate( "{{ null | title }} {{ 'test' | title }} {{ 'test test' | title }} {{ 'TEST TEST' | title }}"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals(" Test Test Test TEST TEST", writer.toString()); } @Test void testTitleWithLeadingWhitespace() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ ' \ntest' | title }}"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals(" \nTest", writer.toString()); } @Test void testTrim() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble .getTemplate("{{ ' This should be trimmed. ' | trim }}"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("This should be trimmed.", writer.toString()); } @Test void testTrimWithNullInput() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ null | trim }}"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("", writer.toString()); } @Test void testDefault() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate( "{{ obj|default('ONE') }} {{ null|default('TWO') }} {{ ' ' |default('THREE') }} {{ 4 |default('FOUR') }}"); Map context = new HashMap<>(); context.put("obj", null); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("ONE TWO THREE 4", writer.toString()); } /** * Tests if the {@link DefaultFilter} is working as * expected. * * @throws Exception thrown when something went wrong. */ @Test void testDefaultFilterWithStrictMode() throws Exception { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(true).build(); PebbleTemplate template = pebble.getTemplate("{{ name | default('test') }}"); Map context = new HashMap<>(); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("test", writer.toString()); } @Test void testDefaultWithNamedArguments() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ obj|default(default='ONE') }}"); Map context = new HashMap<>(); context.put("obj", null); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("ONE", writer.toString()); } @Test void testFirst() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ names | first }}"); List names = new ArrayList<>(); names.add("Alex"); names.add("Joe"); names.add("Bob"); Map context = new HashMap<>(); context.put("names", names); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("Alex", writer.toString()); } @Test void testFirstWithNullInput() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ names | first }}"); Map context = new HashMap<>(); context.put("names", null); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("", writer.toString()); } @Test void testFirstWithStringInput() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ name | first }}"); Map context = new HashMap<>(); context.put("name", "Alex"); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("A", writer.toString()); } @Test void testFirstWithEmptyCollection() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ names | first }}"); Map context = new HashMap<>(); context.put("names", emptyList()); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("", writer.toString()); } @Test void testJoin() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ names | join(',') }}"); List names = new ArrayList<>(); names.add("Alex"); names.add("Joe"); names.add("Bob"); Map context = new HashMap<>(); context.put("names", names); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("Alex,Joe,Bob", writer.toString()); } @Test void testJoinWithoutGlue() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ names | join }}"); List names = new ArrayList<>(); names.add("Alex"); names.add("Joe"); names.add("Bob"); Map context = new HashMap<>(); context.put("names", names); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("AlexJoeBob", writer.toString()); } @Test void testJoinWithNumbers() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ numbers | join(',') }}"); List numbers = new ArrayList<>(); numbers.add(1); numbers.add(2); numbers.add(3); Map context = new HashMap<>(); context.put("numbers", numbers); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("1,2,3", writer.toString()); } @Test void testJoinWithNullInput() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ null | join(',') }}"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("", writer.toString()); } @Test void testJoinWithStringArray() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ names | join(',') }}"); String[] names = new String[]{"Alex", "Joe", "Bob"}; Map context = new HashMap<>(); context.put("names", names); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("Alex,Joe,Bob", writer.toString()); } @Test void testJoinWithStringArrayWithoutGlue() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ names | join }}"); String[] names = new String[]{"Alex", "Joe", "Bob"}; Map context = new HashMap<>(); context.put("names", names); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("AlexJoeBob", writer.toString()); } @Test void testJoinWithNumbersArray() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ numbers | join(',') }}"); int[] numbers = new int[]{1, 2, 3}; Map context = new HashMap<>(); context.put("numbers", numbers); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("1,2,3", writer.toString()); } @Test void testJoinWithEmptyNumbersArray() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ numbers | join(',') }}"); int[] numbers = new int[0]; Map context = new HashMap<>(); context.put("numbers", numbers); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("", writer.toString()); } @Test void testJoinWithFloatArray() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ numbers | join(',') }}"); float[] numbers = new float[]{1.0f, 2.5f, 3.0f}; Map context = new HashMap<>(); context.put("numbers", numbers); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("1.0,2.5,3.0", writer.toString()); } @Test void testLast() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ names | last }}"); List names = new ArrayList<>(); names.add("Alex"); names.add("Joe"); names.add("Bob"); Map context = new HashMap<>(); context.put("names", names); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("Bob", writer.toString()); } @Test void testLastWithNullInput() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ null | last }}"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("", writer.toString()); } @Test void testLastWithStringInput() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ name | last }}"); Map context = new HashMap<>(); context.put("name", "Alex"); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("x", writer.toString()); } @Test void testLastWithArrayInput() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ names | last }}"); Map context = new HashMap<>(); context.put("names", new String[]{"FirstName", "FamilyName"}); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("FamilyName", writer.toString()); } @Test void testLastWithPrimitiveArrayInput() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ ages | last }}"); Map context = new HashMap<>(); context.put("ages", new int[]{28, 30}); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("30", writer.toString()); } @Test void testFirstWithArrayInput() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ names | first }}"); Map context = new HashMap<>(); context.put("names", new String[]{"FirstName", "FamilyName"}); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("FirstName", writer.toString()); } @Test void testFirstWithPrimitiveArrayInput() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ ages | first }}"); Map context = new HashMap<>(); context.put("ages", new int[]{28, 30}); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("28", writer.toString()); } public class User { private final String username; public User(String username) { this.username = username; } public String getUsername() { return this.username; } } @Test void testSliceWithNullInput() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ null | slice }}"); Map context = new HashMap<>(); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("", writer.toString()); } @Test void testSliceWithDefaultArgs() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ name | slice }}"); Map context = new HashMap<>(); context.put("name", "Alex"); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("Alex", writer.toString()); } @Test void testSliceWithInvalidFirstArg() throws PebbleException, IOException { assertThrows(IllegalArgumentException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ name | slice(-1) }}"); Map context = new HashMap<>(); context.put("name", "Alex"); Writer writer = new StringWriter(); template.evaluate(writer, context); }); } @Test void testSliceWithIntegerArguments() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble .getTemplate("{{ 'abcdefghijklmnopqrstuvwxyz' | slice(from, to) }}"); Map context = new HashMap<>(); context.put("from", new Integer(2)); context.put("to", new Integer(4)); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("cd", writer.toString()); } @Test void testSliceWithInvalidSecondArg() throws PebbleException, IOException { assertThrows(PebbleException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ name | slice(0,-1) }}"); Map context = new HashMap<>(); context.put("name", "Alex"); Writer writer = new StringWriter(); template.evaluate(writer, context); }); } @Test void testSliceWithInvalidSecondArg2() throws PebbleException, IOException { assertThrows(PebbleException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ name | slice(0,1000) }}"); Map context = new HashMap<>(); context.put("name", "Alex"); Writer writer = new StringWriter(); template.evaluate(writer, context); }); } @Test void testSliceWithString() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ name | slice(2,5) }}"); Map context = new HashMap<>(); context.put("name", "Alexander"); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("exa", writer.toString()); } @Test void testSliceWithList() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ names | slice(2,5) }}"); List names = new ArrayList<>(); names.add("Alex"); names.add("Joe"); names.add("Bob"); names.add("Sarah"); names.add("Mary"); names.add("Marge"); Map context = new HashMap<>(); context.put("names", names); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("[Bob, Sarah, Mary]", writer.toString()); } @Test void testSliceWithStringArray() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{% set n = names | slice(2,5) %}{{ n[0] }}"); String[] names = new String[]{"Alex", "Joe", "Bob", "Sarah", "Mary", "Marge"}; Map context = new HashMap<>(); context.put("names", names); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("Bob", writer.toString()); } @Test void testSliceWithPrimitivesArray() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{% set p = primitives | slice(2,5) %}{{ p[0] }}"); Map context = new HashMap<>(); Writer writer; // boolean writer = new StringWriter(); boolean[] booleans = new boolean[]{true, false, true, false, true, false}; context.put("primitives", booleans); template.evaluate(writer, context); assertEquals("true", writer.toString()); // byte writer = new StringWriter(); byte[] bytes = new byte[]{0, 1, 2, 3, 4, 5}; context.put("primitives", bytes); template.evaluate(writer, context); assertEquals("2", writer.toString()); // char writer = new StringWriter(); char[] chars = new char[]{'a', 'b', 'c', 'd', 'e', 'f'}; context.put("primitives", chars); template.evaluate(writer, context); assertEquals("c", writer.toString()); // double writer = new StringWriter(); double[] doubles = new double[]{0.0d, 1.0d, 2.0d, 3.0d, 4.0d, 5.0d}; context.put("primitives", doubles); template.evaluate(writer, context); assertEquals("2.0", writer.toString()); // float writer = new StringWriter(); float[] floats = new float[]{0.0f, 1.0f, 2.0f, 3.0f, 4.0f, 5.0f}; context.put("primitives", floats); template.evaluate(writer, context); assertEquals("2.0", writer.toString()); // int writer = new StringWriter(); int[] ints = new int[]{0, 1, 2, 3, 4, 5}; context.put("primitives", ints); template.evaluate(writer, context); assertEquals("2", writer.toString()); // long writer = new StringWriter(); long[] longs = new long[]{0, 1, 2, 3, 4, 5}; context.put("primitives", longs); template.evaluate(writer, context); assertEquals("2", writer.toString()); // short writer = new StringWriter(); short[] shorts = new short[]{0, 1, 2, 3, 4, 5}; context.put("primitives", shorts); template.evaluate(writer, context); assertEquals("2", writer.toString()); } @Test void testSliceWithInvalidInputType() throws PebbleException, IOException { assertThrows(PebbleException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ names | slice(2,5) }}"); Map context = new HashMap<>(); context.put("names", Long.valueOf(1)); Writer writer = new StringWriter(); template.evaluate(writer, context); }); } /** * Tests {@link LengthFilter} with different inputs. */ @Test void testLengthFilterInputs() { LengthFilter filter = new LengthFilter(); assertEquals(0, filter.apply(null, null, null, null, 0)); assertEquals(4, filter.apply("test", null, null, null, 0)); assertEquals(0, filter.apply(Collections.EMPTY_LIST, null, null, null, 0)); assertEquals(2, filter.apply(Arrays.asList("tttt", "ssss"), null, null, null, 0)); assertEquals(2, filter.apply(Arrays.asList("tttt", "ssss").iterator(), null, null, null, 0)); Map test = new HashMap<>(); test.put("test", "test"); test.put("other", "other"); test.put("and_other", "other"); assertEquals(3, filter.apply(test, null, null, null, 0)); } /** * Tests {@link LengthFilter} if the length filter is working within templates. */ @Test void testLengthFilterInTemplate() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ names | length }}"); Map context = new HashMap<>(); context.put("names", "test"); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("4", writer.toString()); } /** * Tests {@link ReplaceFilter} if it can handle a null input. */ @Test void testReplaceFilterNullInput() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble .getTemplate( "{{ null |replace({'%this%': foo, '%that%': \"bar\"}) }}"); Writer writer = new StringWriter(); assertDoesNotThrow(() -> template.evaluate(writer, new HashMap<>())); } /** * Tests {@link ReplaceFilter} if the length filter is working within templates. */ @Test void testReplaceFilterInTemplate() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble .getTemplate( "{{ \"I like %this% and %that%.\"|replace({'%this%': foo, '%that%': \"bar\"}) }}"); Map context = new HashMap<>(); context.put("foo", "foo"); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("I like foo and bar.", writer.toString()); } /** * Tests {@link Base64EncoderFilter} if the base64 encoding filter is working for a string value, a string constant, null. */ @Test void testBase64EncoderFilterInTemplate() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("var=\"{{ var | base64encode }}\" const=\"{{ \"test\" | base64encode }}\" null=\"{{ null | base64encode }}\""); Map context = new HashMap<>(); context.put("var", "test"); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("var=\"dGVzdA==\" const=\"dGVzdA==\" null=\"\"", writer.toString()); } /** * Tests {@link Base64DecoderFilter} if the base64 decoder filter is working for a string value, a string constant, null. */ @Test void testBase64DecoderFilterInTemplate() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("var=\"{{ var | base64decode }}\" const=\"{{ \"dGVzdA==\" | base64decode }}\" null=\"{{ null | base64decode }}\""); Map context = new HashMap<>(); context.put("var", "dGVzdA=="); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("var=\"test\" const=\"test\" null=\"\"", writer.toString()); } @Test void testBase64DecodeFilterBadEncodedStringFail() throws PebbleException, IOException { assertThrows(PebbleException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ \"this is not a base64 encoded string\" | base64decode }}"); Map context = new HashMap<>(); Writer writer = new StringWriter(); template.evaluate(writer, context); }); } @Test void testBase64DecodeFilterNoStringFail() throws PebbleException, IOException { assertThrows(PebbleException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ {'foo':1} | base64decode }}"); Map context = new HashMap<>(); Writer writer = new StringWriter(); template.evaluate(writer, context); }); } @Test void testSha256FilterNoStringFail() throws PebbleException, IOException { assertThrows(PebbleException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ {'foo':1} | sha256 }}"); Map context = new HashMap<>(); Writer writer = new StringWriter(); template.evaluate(writer, context); }); } /** * Tests {@link Sha256Filter} if the SHA256 hashing filter is working for a string value, a string constant, null. */ @Test void testSha256FilterInTemplate() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("var=\"{{ var | sha256 }}\" const=\"{{ \"test\" | sha256}}\" null=\"{{ null | sha256 }}\""); Map context = new HashMap<>(); context.put("var", "test"); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("var=\"9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08\" const=\"9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08\" null=\"\"", writer.toString()); } @Test void testMergeOk() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .extension(new TestingExtension()).strictVariables(false).build(); PebbleTemplate template = pebble .getTemplate( "{{{'one':1}|merge({'two':2})|mapToString}} {%set m1 = {'one':1}|merge(['two'])%}{{m1['two']}} {{[1]|merge([2])|listToString}} {%set l1 = [1]|merge({'two':2})%}{{l1[1].value}} {{arr1|merge(arr2)|arrayToString}}"); Map context = new HashMap<>(); context.put("arr1", new int[]{1}); context.put("arr2", new int[]{2}); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("{one=1, two=2} two [1,2] 2 [1,2]", writer.toString()); } @Test void testMergeMapWithStringAndFail() throws PebbleException, IOException { assertThrows(UnsupportedOperationException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ {'one':1}|merge('No way!') }}"); Map context = new HashMap<>(); Writer writer = new StringWriter(); template.evaluate(writer, context); }); } @Test void testMergeListWithStringAndFail() throws PebbleException, IOException { assertThrows(PebbleException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ [1]|merge('No way!') }}"); Map context = new HashMap<>(); Writer writer = new StringWriter(); template.evaluate(writer, context); }); } @Test void testMergeDifferentArraysAndFail() throws PebbleException, IOException { assertThrows(PebbleException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ arr1|merge(arr2) }}"); Map context = new HashMap<>(); context.put("arr1", new int[]{1}); context.put("arr2", new String[]{"2"}); Writer writer = new StringWriter(); template.evaluate(writer, context); }); } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/CoreFunctionsTest.java ================================================ /** * **************************************************************************** This file is part of * Pebble. *

* Copyright (c) 2014 by Mitchell Bösecke *

* For the full copyright and license information, please view the LICENSE file that was distributed * with this source code. **************************************************************************** */ package io.pebbletemplates.pebble; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.loader.StringLoader; import io.pebbletemplates.pebble.template.PebbleTemplate; import org.junit.jupiter.api.Test; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.util.HashMap; import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; class CoreFunctionsTest { public static final String LINE_SEPARATOR = System.lineSeparator(); @Test void testBlockFunction() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("templates/function/template.block.peb"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("Default Title" + LINE_SEPARATOR + "Default Title", writer.toString()); } @Test void testParentFunction() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("templates/function/template.child.peb"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals( "parent text" + LINE_SEPARATOR + "\t\tparent head" + LINE_SEPARATOR + "\tchild head" + LINE_SEPARATOR, writer.toString()); } /** * Issue occurred where parent block didn't have access to the context when invoked via the * parent() function. */ @Test void testParentBlockHasAccessToContext() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().strictVariables(false).build(); PebbleTemplate template = pebble .getTemplate("templates/function/template.childWithContext.peb"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("bar", writer.toString()); } @Test void testParentThenMacro() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().strictVariables(false).build(); PebbleTemplate template = pebble .getTemplate("templates/function/template.childThenParentThenMacro.peb"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("test", writer.toString()); } /** * Two levels of parent functions would cause a stack overflow error, #61. */ @Test void testParentFunctionWithTwoLevels() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("templates/function/template.subchild.peb"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals( "parent text" + LINE_SEPARATOR + "\t\t\tparent head" + LINE_SEPARATOR + "\tchild head" + LINE_SEPARATOR + "\tsub child head" + LINE_SEPARATOR, writer.toString()); } @Test void testMinFunction() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ min(8.0, 1, 4, 5, object.large) }}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("object", new SimpleObject()); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("1", writer.toString()); } @Test void testMaxFunction() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ max(8.0, 1, 4, 5, object.large) }}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("object", new SimpleObject()); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("20", writer.toString()); } @Test void testRangeFunction() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% for i in range(0,5) %}{{ i }}{% endfor %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("012345", writer.toString()); } @Test void testRangeFunctionIncrement2() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% for i in range(0,10,2) %}{{ i }}{% endfor %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("0246810", writer.toString()); } @Test void testRangeFunctionDecrement2() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% for i in range(10,0,-2) %}{{ i }}{% endfor %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("1086420", writer.toString()); } @Test void testRangeFunctionIncrement0() throws PebbleException, IOException { assertThrows(PebbleException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% for i in range(0,5,0) %}{{ i }}{% endfor %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); Writer writer = new StringWriter(); template.evaluate(writer, context); }); } @Test void testRangeFunctionChar() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% for i in range('a','e') %}{{ i }}{% endfor %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("abcde", writer.toString()); } @Test void testRangeFunctionCharIncrement2() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% for i in range('a','f',2) %}{{ i }}{% endfor %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("ace", writer.toString()); } @Test void testRangeFunctionCharDecrement2() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% for i in range('f','a',-2) %}{{ i }}{% endfor %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("fdb", writer.toString()); } @Test void testRangeFunctionCharIncrement0() throws PebbleException, IOException { assertThrows(PebbleException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% for i in range('a','e',0) %}{{ i }}{% endfor %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); Writer writer = new StringWriter(); template.evaluate(writer, context); }); } @Test void testRangeFunctionLongVariable() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% for i in range(0,var) %}{{ i }}{% endfor %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("var", 5L); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("012345", writer.toString()); } @Test void testRangeFunctionDoubleVariable() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% for i in range(0,var) %}{{ i }}{% endfor %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("var", 5.5D); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("012345", writer.toString()); } @Test void testRangeFunctionIntegerVariable() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% for i in range(0,var) %}{{ i }}{% endfor %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("var", 5); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("012345", writer.toString()); } @Test void testRangeFunctionIncrementIntegerVariable() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% for i in range(0,var,increment) %}{{ i }}{% endfor %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("var", 5); context.put("increment", 2); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("024", writer.toString()); } @Test void testRangeFunctionIncrementDoubleVariable() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% for i in range(0,var,increment) %}{{ i }}{% endfor %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("var", 5); context.put("increment", 2D); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("024", writer.toString()); } public class SimpleObject { public int small = 1; public int large = 20; } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/CoreTagsTest.java ================================================ /** * **************************************************************************** his file is part of * Pebble. *

* Copyright (c) 2014 by Mitchell Bösecke *

* For the full copyright and license information, please view the LICENSE file that was distributed * with this source code. **************************************************************************** */ package io.pebbletemplates.pebble; import io.pebbletemplates.pebble.error.ParserException; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.loader.StringLoader; import io.pebbletemplates.pebble.template.PebbleTemplate; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.fail; class CoreTagsTest { public static final String LINE_SEPARATOR = System.lineSeparator(); @Test void testBlock() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("templates/template.grandfather.peb"); Writer writer = new StringWriter(); template.evaluate(writer); } /** * This ensures that block inheritance works properly even if it skips a generation. */ @Test void skipGenerationBlock() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("templates/template.skipGenerationBlock1.peb"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("success", writer.toString()); } /** * The template used to fail if the user wrapped the block name in quotes. */ @Test void testBlockWithStringLiteralName() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% block 'content' %}hello{% endblock 'content' %}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("hello", writer.toString()); } @Test void testIf() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if false or steve == true %}yes{% else %}no{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("yes", true); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("no", writer.toString()); } /** * Issue #34 */ @Test void testIfThenElse() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if alpha %}alpha{% elseif beta %}beta{% else %}gamma{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("alpha", true); context.put("beta", false); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("alpha", writer.toString()); } @Test void testIfWithDirectProperty() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if variable %}yes{% else %}no{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("variable", true); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("yes", writer.toString()); } @Test void testIfWhenInvalidOrNoEndifTag() throws PebbleException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if variable %}smth{ endif %}"; try { pebble.getTemplate(source); fail("Should fail due to invalid endif tag"); } catch (ParserException ex) { assertEquals(ex.getPebbleMessage(), "Unexpected end of template. Pebble was looking for the \"endif\" tag"); assertEquals(ex.getLineNumber(), (Integer) 1); assertEquals(ex.getFileName(), source); } } @Test void testFlush() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "start{% flush %}end"; PebbleTemplate template = pebble.getTemplate(source); FlushAwareWriter writer = new FlushAwareWriter(); template.evaluate(writer); List flushedBuffers = writer.getFlushedBuffers(); assertEquals("start", flushedBuffers.get(0)); assertEquals("startend", flushedBuffers.get(1)); } @Test void testFor() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% for user in users %}{% if loop.first %}[{{ loop.length }}]{% endif %}{% if loop.last %}[{{ loop.length }}]{% endif %}{{ loop.index }}{{ loop.revindex }}{{ user.username }}{% endfor %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); List users = new ArrayList<>(); users.add(new User("Alex")); users.add(new User("Bob")); users.add(new User("John")); context.put("users", users); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("[3]02Alex11Bob[3]20John", writer.toString()); } @Test void testForWithIterable() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% for user in users %}{% if loop.first %}[first]{% endif %}{% if loop.last %}[last]{% endif %}{{ loop.index }}{{ user.username }}{% endfor %}"; PebbleTemplate template = pebble.getTemplate(source); Iterable users = () -> new Iterator() { User[] fixture = new User[]{new User("Alex"), new User("Bob"), new User("John")}; int pos = 0; @Override public boolean hasNext() { return this.pos < this.fixture.length; } @Override public User next() { return this.fixture[this.pos++]; } @Override public void remove() { throw new UnsupportedOperationException( "Not supported yet."); //To change body of generated methods, choose Tools | Templates. } }; Map context = new HashMap<>(); context.put("users", users); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("[first]0Alex1Bob[last]2John", writer.toString()); } @Test void testForWithMap() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); Map data = new LinkedHashMap<>(); data.put("One", 1); data.put("Two", 2); data.put("Three", 3); String source = "{% for entry in data %}{{ entry.key }} {{ entry.value }}{% endfor %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("data", data); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("One 1Two 2Three 3", writer.toString()); } @Test void testForSequenceNumber() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% for i in 0..5 %}{{i}}{% endfor %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("012345", writer.toString()); } @Test void testForSequenceNumberException() throws PebbleException, IOException { assertThrows(PebbleException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% for i in 'a'..5 %}{{i}}{% endfor %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); Writer writer = new StringWriter(); template.evaluate(writer, context); }); } @Test void testForWhenInvalidOrNoEndforTag() throws PebbleException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% for i in 'a'..5 %}{{i}}% endfor %}"; try { pebble.getTemplate(source); fail("Should fail due to invalid endfor tag"); } catch (ParserException ex) { assertEquals(ex.getPebbleMessage(), "Unexpected end of template. Pebble was looking for the \"endfor\" tag"); assertEquals(ex.getLineNumber(), (Integer) 1); assertEquals(ex.getFileName(), source); } } @Test void testFilterTag() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% filter upper %}hello{% endfilter %}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("HELLO", writer.toString()); } @Test void testChainedFilterTag() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% filter lower | escape %}HELLO
{% endfilter %}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("hello<br>", writer.toString()); } /** * Issue #15 */ @Test void testForIteratingOverProperty() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% for user in classroom.users %}{{ user.username }}{% endfor %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); List users = new ArrayList<>(); users.add(new User("Alex")); users.add(new User("Bob")); Classroom classroom = new Classroom(); classroom.setUsers(users); context.put("classroom", classroom); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("AlexBob", writer.toString()); } @Test void testForWithNullIterable() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% for user in users %}{{ loop.index }}{% endfor %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("users", null); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("", writer.toString()); } @Test void testForWithArray() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% for user in users %}{{ user }}{% endfor %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); String[] users = new String[3]; users[0] = "User 1"; users[1] = "User 2"; users[2] = "User 3"; context.put("users", users); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("User 1User 2User 3", writer.toString()); } @Test void testForWithArrayOfPrimitives() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% for num in ints %}{{ num }}{% endfor %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); int[] ints = new int[3]; ints[0] = 1; ints[1] = 2; ints[2] = 3; context.put("ints", ints); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("123", writer.toString()); } /** * There were compilation issues when having two for loops in the same template due to the same * variable name being declared twice. */ @Test void multipleForLoops() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "" + "{% for user in users %}{{ user.username }}{% endfor %}" + "{% for user in users %}{{ user.username }}{% endfor %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); List users = new ArrayList<>(); users.add(new User("Alex")); users.add(new User("Bob")); context.put("users", users); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("AlexBobAlexBob", writer.toString()); } @Test void testForElse() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% for user in users %}{{ user.username }}{% else %}yes{% endfor %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); List users = new ArrayList<>(); context.put("users", users); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("yes", writer.toString()); } @Test void testCache() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% cache 'test' %}{% if foobar %}true{% else %}false{% endif %}{% endcache %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("foobar", true); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("true", writer.toString()); //Value should be cached context.put("foobar", false); writer = new StringWriter(); template.evaluate(writer, context); assertEquals("true", writer.toString()); } @Test void testDisabledCache() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false) .cacheActive(false).build(); String source = "{% cache 'test' %}{% if foobar %}true{% else %}false{% endif %}{% endcache %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("foobar", true); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("true", writer.toString()); //Value should NOT be cached context.put("foobar", false); writer = new StringWriter(); template.evaluate(writer, context); assertEquals("false", writer.toString()); } @Test void testCacheWithVariable() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% cache 'test' + var %}{% if foobar %}true{% else %}false{% endif %}{% endcache %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("foobar", true); context.put("var", 12); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("true", writer.toString()); //Value should be cached context.put("foobar", false); writer = new StringWriter(); template.evaluate(writer, context); assertEquals("true", writer.toString()); } @Test void testCacheWithNoName() throws PebbleException, IOException { assertThrows(PebbleException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% cache %}{% if foobar %}true{% else %}false{% endif %}{% endcache %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("foobar", true); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("true", writer.toString()); }); } public static class SimpleObjectA { private String value; public String getValue() { return this.value; } void setValue(String value) { this.value = value; } } /** * It is important that this object has an identical method signature as SimpleObjectA for the * following tests. * * @author mbosecke */ public static class SimpleObjectB { private String value; public String getValue() { return this.value; } void setValue(String value) { this.value = value; } } /** * Gets the attribute of an object once so that the attribute is cached within the * GetAttributeExpression then evaluates the exact same template but with a null object. *

* Issue #57 */ @Test void testMemberCacheWithNullObject() throws PebbleException, IOException { SimpleObjectA a = new SimpleObjectA(); a.setValue("A"); Map context = new HashMap<>(); context.put("object", a); PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ object.value }}"); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("A", writer.toString()); context.put("object", null); writer = new StringWriter(); template.evaluate(writer, context); assertEquals("", writer.toString()); } /** * Gets the attribute of an object once so that the attribute is cached within the * GetAttributeExpression then evaluates the exact same template but with a new type of object * that happens to have the same method signature. *

* Pull #62 */ @Test void testMemberCacheWithDifferingObjectTypes() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ object.value }}"); SimpleObjectA objectA = new SimpleObjectA(); objectA.setValue("A"); SimpleObjectB objectB = new SimpleObjectB(); objectB.setValue("B"); Map context = new HashMap<>(); context.put("object", objectA); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("A", writer.toString()); // swap out the object with similar one to try and break the cache context.put("object", objectB); writer = new StringWriter(); template.evaluate(writer, context); assertEquals("B", writer.toString()); } @Test void testImportWithinBlock() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("templates/template.importWithinBlock.peb"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("\t" + LINE_SEPARATOR, writer.toString()); } @Test void testDynamicInclude() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("templates/template.include.dynamic.peb"); Map context = new HashMap<>(); context.put("admin", false); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("default footer", writer.toString()); context.put("admin", true); writer = new StringWriter(); template.evaluate(writer, context); assertEquals("admin footer", writer.toString()); } @Test void testNonExistingMacroOrFunction() throws PebbleException, IOException { assertThrows(PebbleException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ nonExisting('test') }}"); Writer writer = new StringWriter(); template.evaluate(writer); }); } @Test void testInclude() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("templates/template.include1.peb"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals( "TEMPLATE2" + LINE_SEPARATOR + "TEMPLATE1" + LINE_SEPARATOR + "TEMPLATE2" + LINE_SEPARATOR, writer.toString()); } /** * There was an issue when including a template that had it's own inheritance chain. */ @Test void testIncludeInheritance() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("templates/template.includeInheritance1.peb"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("success", writer.toString()); } @Test void testIncludeWithinBlock() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("templates/template.includeWithinBlock.peb"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("TEMPLATE2" + LINE_SEPARATOR + "TEMPLATE1" + LINE_SEPARATOR, writer.toString()); } /** * Issue #16 */ @Test void testIncludePropagatesContext() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("templates/template.includePropagatesContext.peb"); Writer writer = new StringWriter(); Map context = new HashMap<>(); context.put("name", "Mitchell"); template.evaluate(writer, context); assertEquals("Mitchell", writer.toString()); } /** * Ensures that when including a template it is safe to have conflicting block names. */ @Test void testIncludeOverridesBlocks() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("templates/template.includeOverrideBlock.peb"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("TWO" + LINE_SEPARATOR + "ONE" + LINE_SEPARATOR + "TWO" + LINE_SEPARATOR, writer.toString()); } /** * Ensures that an include with a variable override works even if a null value is passed. */ @Test void testIncludeOverridesVariable() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("templates/template.includeOverrideVariable1.peb"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("One: one (overridden)" + LINE_SEPARATOR + "Two: ", writer.toString()); } @Test void testSet() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% set name = 'alex' %}{{ name }}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("name", "steve"); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("alex", writer.toString()); } @Test void testSetInChildTemplateOutsideOfBlock() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("templates/template.set.child.peb"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("SUCCESS", writer.toString()); } @Test void testReSetInForLoop() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% set total = 0 %}{% for i in 1..1 %}{% for item in items %}{% set total = total + item.balance %}{% endfor %}{% endfor %}{{ total }}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); List> items = new ArrayList<>(); for (int i = 1; i < 4; ++i) { Map item = new HashMap<>(); item.put("balance", i); items.add(item); } context.put("items", items); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("6", writer.toString()); } @Test void testVerbatim() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble .getTemplate("{% verbatim %}{{ foo }}{{ bar }}{% endverbatim %}"); Map context = new HashMap<>(); context.put("foo", "baz"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("{{ foo }}{{ bar }}", writer.toString()); } @Test @Timeout(value = 400, unit = TimeUnit.MILLISECONDS) void testParallel() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false) .executorService(Executors.newCachedThreadPool()).build(); String source = "beginning {% parallel %}{{ slowObject.first }}{% endparallel %} middle {% parallel %}{{ slowObject.second }}{% endparallel %} end {% parallel %}{{ slowObject.third }}{% endparallel %}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); Map context = new HashMap<>(); context.put("slowObject", new SlowObject()); template.evaluate(writer, context); assertEquals("beginning first middle second end third", writer.toString()); } /** * The for loop will add variables into the evaluation context during runtime and there was an * issue where the evaluation context wasn't thread safe. */ @Test void testParallelTagWhileEvaluationContextIsChanging() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false) .executorService(Executors.newCachedThreadPool()).build(); String source = "{% for num in array %}{% parallel %}{{ loop.index }}{% endparallel %}{% endfor%}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); Map context = new HashMap<>(); context.put("array", new int[10]); template.evaluate(writer, context); assertEquals("0123456789", writer.toString()); } /** * Nested parallel tags were throwing an error. */ @Test @Timeout(value = 600, unit = TimeUnit.MILLISECONDS) void testNestedParallel() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false) .executorService(Executors.newCachedThreadPool()).build(); // @formatter:off String source = "{% parallel %}" + "{% parallel %}{{ slowObject.fourth() }}{% endparallel %} {% parallel %}{{ slowObject.first() }}{% endparallel %} " + "{% parallel %}{{ slowObject.fourth() }}{% endparallel %} {% parallel %}{{ slowObject.first() }}{% endparallel %}" + "{% endparallel %}"; // @formatter:on PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); Map context = new HashMap<>(); context.put("slowObject", new SlowObject()); template.evaluate(writer, context); assertEquals("fourth first fourth first", writer.toString()); } @Test @Timeout(value = 300, unit = TimeUnit.MILLISECONDS) void testIncludeWithinParallelTag() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().strictVariables(true) .executorService(Executors.newCachedThreadPool()).build(); PebbleTemplate template = pebble.getTemplate("templates/template.parallelInclude1.peb"); Writer writer = new StringWriter(); Map context = new HashMap<>(); context.put("slowObject", new SlowObject()); template.evaluate(writer, context); assertEquals("first" + LINE_SEPARATOR + "TEMPLATE1" + LINE_SEPARATOR + "first", writer.toString()); } @Test void testParallelWithoutExecutorService() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "beginning {% parallel %}{{ slowObject.first }}{% endparallel %}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); Map context = new HashMap<>(); context.put("slowObject", new SlowObject()); template.evaluate(writer, context); assertEquals("beginning first", writer.toString()); } /** * Issue #159 */ @Test void testParallelWithImport() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder() .executorService(Executors.newCachedThreadPool()).build(); PebbleTemplate template = pebble.getTemplate("templates/template.parallelWithImport.peb"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("success", writer.toString()); } public class SlowObject { public String first() { try { Thread.sleep(200); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } return "first"; } public String second() { try { Thread.sleep(200); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } return "second"; } public String third() { try { Thread.sleep(200); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } return "third"; } public String fourth() { try { Thread.sleep(400); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } return "fourth"; } } public class User { private final String username; public User(String username) { this.username = username; } public String getUsername() { return this.username; } } public class Classroom { private List users = new ArrayList<>(); public List getUsers() { return this.users; } void setUsers(List users) { this.users = users; } } public class FlushAwareWriter extends StringWriter { private List buffers = new ArrayList<>(); @Override public void flush() { this.buffers.add(this.getBuffer().toString()); super.flush(); } public List getFlushedBuffers() { return this.buffers; } } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/CoreTestsTest.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.loader.StringLoader; import io.pebbletemplates.pebble.template.PebbleTemplate; import org.junit.jupiter.api.Test; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; class CoreTestsTest { @Test void testEven() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if 2 is even %}yes{% else %}no{% endif %}{% if 3 is even %}no{% else %}yes{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("yesyes", writer.toString()); } /** * Pebble parses numbers as longs so we want to make sure our numerical tests will work even if we * force it to take an int as an input. */ @Test void testEvenWithInteger() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if num is even %}yes{% else %}no{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("num", 2); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("yes", writer.toString()); } @Test void testNullEven() throws PebbleException, IOException { assertThrows(PebbleException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if null is even %}yes{% else %}no{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer); }); } @Test void testOdd() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if 2 is odd %}no{% else %}yes{% endif %}{% if 3 is odd %}yes{% else %}no{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("yesyes", writer.toString()); } /** * Pebble parses numbers as longs so we want to make sure our numerical tests will work even if we * force it to take an int as an input. */ @Test void testOddWithInteger() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if num is odd %}yes{% else %}no{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("num", 3); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("yes", writer.toString()); } @Test void testNullOdd() throws PebbleException, IOException { assertThrows(PebbleException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if null is odd %}yes{% else %}no{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer); }); } @Test void testNull() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if null is null %}yes{% endif %}{% if obj is null %}yes{% endif %}{% if 2 is null %}no{% else %}yes{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("obj", null); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("yesyesyes", writer.toString()); } @Test void testEmpty() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if null is empty() %}yes{% endif %}{% if ' ' is empty() %}yes{% endif %}{% if obj is empty() %}yes{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("obj", new ArrayList()); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("yesyesyes", writer.toString()); } @Test void testIterables() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if null is iterable() %}no{% else %}yes{% endif %}{% if obj1 is iterable() %}yes{% else %}no{% endif %}{% if obj2 is iterable() %}no{% else %}yes{% endif %}{% if obj3 is iterable() %}yes{% else %}no{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("obj1", new ArrayList()); context.put("obj2", new HashMap()); context.put("obj3", new String[]{}); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("yesyesyesyes", writer.toString()); } @Test void testIsnt() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if 2 is not odd %}yes{% else %}no{% endif %}{% if null is not iterable() %}yes{% else %}no{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("yesyes", writer.toString()); } /** * Using the unary "not" operator before a test. * * Issue #27 */ @Test void testNegativeTest() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if not (2 is odd) %}yes{% else %}no{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("yes", writer.toString()); } /** * Similar to the testNegativeTest() except with an attribute of an object in the context. * * Issue #27 */ @Test void testNegativeTestOnAttribute() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if not (classroom.students is empty) %}yes{% else %}no{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("classroom", new Classroom()); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("no", writer.toString()); } @Test void testMapTest() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if {} is map %}true{% else %}false{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("true", writer.toString()); } /** * Tests if the test function 'defined' is working. */ @Test void testDefined() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if test is defined %}yes{% else %}no{% endif %}{% if test2 is defined %}no{% else %}yes{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); Map context = new HashMap<>(); context.put("test", "yes"); template.evaluate(writer, context); assertEquals("yesyes", writer.toString()); } /** * Tests if the test function 'defined' is working on maps. */ @Test void testDefinedWithMap() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(true).build(); String source = "{% if test.test is defined %}yes{% else %}no{% endif %}{% if test.test2 is defined %}no{% else %}yes{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); Map context = new HashMap<>(); Map map = new HashMap<>(); map.put("test", "yes"); context.put("test", map); template.evaluate(writer, context); assertEquals("yesyes", writer.toString()); } public static class Classroom { public static List students = new ArrayList<>(); } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/DynamicNamedArgsTest.java ================================================ package io.pebbletemplates.pebble; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.extension.AbstractExtension; import io.pebbletemplates.pebble.extension.Filter; import io.pebbletemplates.pebble.extension.escaper.SafeString; import io.pebbletemplates.pebble.loader.StringLoader; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import org.junit.jupiter.api.Test; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.util.*; import java.util.stream.Collectors; import static org.junit.jupiter.api.Assertions.assertEquals; class DynamicNamedArgsTest { static class Operation { private String path; private List queryParameters; Operation(String path, String... queryParameters) { this.path = path; this.queryParameters = Arrays.asList(queryParameters); } public String getPath() { return path; } public List getQueryParameters() { return queryParameters; } } /** * Query parameters are dynamic. */ static class QueryStringArgsNullFilter implements Filter { @Override public Object apply(Object input, Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) throws PebbleException { if (!(input instanceof Operation)) { throw new IllegalArgumentException("Expected Operation but got " + input.getClass()); } Operation operation = (Operation) input; return new SafeString(operation.getPath() + operation.getQueryParameters().stream() .map(it -> it + "=" + args.getOrDefault(it, "string")) .collect(Collectors.joining("&", "?", ""))); } @Override public List getArgumentNames() { return null; } } static class QueryStringArgsEmptyFilter implements Filter { @Override public Object apply(Object input, Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) throws PebbleException { if (!(input instanceof Operation)) { throw new IllegalArgumentException("Expected Operation but got " + input.getClass()); } Operation operation = (Operation) input; return new SafeString(operation.getPath() + operation.getQueryParameters().stream() .map(it -> it + "=" + args.getOrDefault(it, "string")) .collect(Collectors.joining("&", "?", ""))); } @Override public List getArgumentNames() { return new ArrayList<>(); } } static class QueryStringExtension extends AbstractExtension { @Override public Map getFilters() { Map filters = new HashMap<>(); filters.put("queryStringArgsNull", new QueryStringArgsNullFilter()); filters.put("queryStringArgsEmpty", new QueryStringArgsEmptyFilter()); return filters; } } @Test void shouldSupportDynamicNamedArgumentsWhenArgumentsIsNull() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .extension(new QueryStringExtension()) .build(); // Query parameters are dynamic Operation operation = new Operation("/library", "title", "isbn"); String source = "{{operation | queryStringArgsNull(title=\"Dune\")}}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); Map context = new HashMap<>(); context.put("operation", operation); template.evaluate(writer, context); assertEquals("/library?title=Dune&isbn=string", writer.toString()); } @Test void shouldSupportDynamicNamedArgumentsWhenArgumentsIsEmpty() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .extension(new QueryStringExtension()) .build(); // Query parameters are dynamic Operation operation = new Operation("/library", "title", "isbn"); String source = "{{operation | queryStringArgsEmpty(isbn=\"1234\")}}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); Map context = new HashMap<>(); context.put("operation", operation); template.evaluate(writer, context); assertEquals("/library?title=string&isbn=1234", writer.toString()); } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/EmbedCachingTagTest.java ================================================ package io.pebbletemplates.pebble; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.loader.ClasspathLoader; import io.pebbletemplates.pebble.loader.DelegatingLoader; import io.pebbletemplates.pebble.loader.StringLoader; import io.pebbletemplates.pebble.template.PebbleTemplate; import org.junit.jupiter.api.Test; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import static java.lang.System.lineSeparator; import static org.junit.jupiter.api.Assertions.assertEquals; class EmbedCachingTagTest { private final PebbleEngine pebble; private final Map context; public EmbedCachingTagTest() { StringLoader stringLoader = new StringLoader(); ClasspathLoader classpathLoader = new ClasspathLoader(); classpathLoader.setPrefix("templates/embed/cache"); this.pebble = new PebbleEngine.Builder() .loader(new DelegatingLoader(Arrays.asList( classpathLoader, stringLoader ))) .strictVariables(false) .cacheActive(true) .build(); Writer writer = new StringWriter(); this.context = new HashMap<>(); this.context.put("foo", "FOO"); this.context.put("bar", "BAR"); } @Test void testEmbedNotChangingCachedTemplate() throws PebbleException, IOException { Writer writer1 = new StringWriter(); PebbleTemplate template1 = this.pebble.getTemplate("template1.peb"); template1.evaluate(writer1, this.context); String result1 = writer1.toString(); Writer writer2 = new StringWriter(); PebbleTemplate template2 = this.pebble.getTemplate("template2.peb"); template2.evaluate(writer2, this.context); String result2 = writer2.toString(); assertEquals("" + "BEFORE BASE" + lineSeparator() + "EMBED OVERRIDE" + lineSeparator() + "AFTER BASE", result1 ); assertEquals("" + "BEFORE BASE" + lineSeparator() + "EMBED BASE" + lineSeparator() + "AFTER BASE", result2 ); } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/EmbedTagTest.java ================================================ package io.pebbletemplates.pebble; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.loader.ClasspathLoader; import io.pebbletemplates.pebble.loader.DelegatingLoader; import io.pebbletemplates.pebble.loader.StringLoader; import io.pebbletemplates.pebble.template.PebbleTemplate; import io.pebbletemplates.pebble.utils.Pair; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import java.io.File; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.nio.file.Files; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; class EmbedTagTest { private String input; private String templateDirectory; private PebbleEngine pebble; private Writer writer; private Map context; @ParameterizedTest @ValueSource(strings = {"test0" , "test1" , "test2" , "test3" , "test4" , "test5" , "test6" , "test7" , "test8" , "test9" , "test10" , "test11" , "test12" , "test13" , "test14" , "test15" , "test16" , "test17" , "test18"}) void tests(String input) throws PebbleException, IOException { this.input = input; this.setUp(); this.renderTemplateAndCheck(); } private void setUp() { StringLoader stringLoader = new StringLoader(); ClasspathLoader classpathLoader = new ClasspathLoader(); this.templateDirectory = "templates/embed/" + this.input; classpathLoader.setPrefix(this.templateDirectory); this.pebble = new PebbleEngine.Builder() .loader(new DelegatingLoader(Arrays.asList( classpathLoader, stringLoader ))) .strictVariables(false) .build(); this.writer = new StringWriter(); this.context = new HashMap<>(); this.context.put("foo", "FOO"); this.context.put("bar", "BAR"); } private void renderTemplateAndCheck() throws PebbleException, IOException { Pair actualTemplate = this.renderTemplate(); String expectedTemplate = this.getResource("./" + this.templateDirectory + "/template.result.txt"); String expectedTwigTemplate = this.getResource("./" + this.templateDirectory + "/template.result.twig.txt"); String expectedError = this.getResource("./" + this.templateDirectory + "/template.error.txt"); // template rendered correctly if (actualTemplate.getLeft() != null) { assertNotNull(actualTemplate.getLeft()); assertNull(actualTemplate.getRight()); assertNotNull(expectedTemplate); assertNull(expectedError); assertEquals(expectedTemplate, actualTemplate.getLeft()); // if Twig could render the same template (meaning it doesn't use Pebble-specific syntax), make sure it renders // the same thing Twig does (ignoring whitespace) if (expectedTwigTemplate != null) { assertNotNull(expectedTwigTemplate); assertEquals(expectedTwigTemplate.replaceAll("\\s", ""), actualTemplate.getLeft().replaceAll("\\s", "")); } } // template did not render correctly, check the error message else { assertNull(actualTemplate.getLeft()); assertNotNull(actualTemplate.getRight()); actualTemplate.getRight().printStackTrace(); assertNull(expectedTemplate); assertNotNull(expectedError); assertEquals(expectedError, actualTemplate.getRight().getMessage()); } } private Pair renderTemplate() { try { PebbleTemplate template = this.pebble.getTemplate("template.peb"); template.evaluate(this.writer, this.context); return new Pair<>(this.writer.toString(), null); } catch (Throwable t) { return new Pair<>(null, t); } } private String getResource(String filename) { try { File file = new File( this .getClass() .getClassLoader() .getResource(filename) .getFile() ); return new String(Files.readAllBytes(file.toPath())); } catch (Exception e) { return null; } } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/EnumEqualsTest.java ================================================ package io.pebbletemplates.pebble; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.loader.StringLoader; import io.pebbletemplates.pebble.template.PebbleTemplate; import org.junit.jupiter.api.Test; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.util.HashMap; import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; /** * Tests if equals is working with enums. */ class EnumEqualsTest { @Test void testEnumComparision() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if 'MY_CONSTANT' equals obj2 %}yes{% else %}no{% endif %}{% if obj2 equals 'MY_CONSTANT' %}yes{% else %}no{% endif %}{% if obj2 equals 'OTHER_CONSTANT' %}no{% else %}yes{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("obj2", TestEnum.MY_CONSTANT); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("yesyesyes", writer.toString()); } public enum TestEnum { MY_CONSTANT, OTHER_CONSTANT, } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/ErrorReportingTest.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble; import io.pebbletemplates.pebble.error.ParserException; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.error.RootAttributeNotFoundException; import io.pebbletemplates.pebble.loader.StringLoader; import io.pebbletemplates.pebble.template.PebbleTemplate; import org.junit.jupiter.api.Test; import java.io.IOException; import java.io.StringWriter; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; class ErrorReportingTest { @Test void testLineNumberErrorReportingWithUnixNewlines() throws PebbleException { ParserException parserException = assertThrows(ParserException.class, () -> { //Arrange PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); pebble.getTemplate("test\n\n\ntest\ntest\ntest\n{% error %}\ntest"); }); assertThat(parserException.getMessage()).endsWith(":7)"); } @Test void testLineNumberErrorReportingWithWindowsNewlines() throws PebbleException { ParserException parserException = assertThrows(ParserException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); pebble.getTemplate("test\r\n\r\ntest\r\ntest\r\ntest\r\n{% error %}\r\ntest"); }); assertThat(parserException.getMessage()).endsWith(":6)"); } @Test void testLineNumberErrorReportingDuringEvaluation() throws PebbleException, IOException { PebbleException pebbleException = assertThrows(PebbleException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder().strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("templates/template.errorReporting.peb"); template.evaluate(new StringWriter()); }); assertThat(pebbleException.getMessage()).endsWith(":8)"); } /** * An error should occur when a Pebble Template Engine instance is configured with * Strict Variables set to true and a template is executed that contains a references * to an undefined property. */ @Test void testInvalidPropertyReferenceInStrictMode() throws PebbleException, IOException { RootAttributeNotFoundException rootAttributeNotFoundException = assertThrows(RootAttributeNotFoundException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(true) .build(); PebbleTemplate template = pebble.getTemplate("{{ root }}"); template.evaluate(new StringWriter()); }); assertThat(rootAttributeNotFoundException.getAttributeName()).isEqualTo("root"); assertThat(rootAttributeNotFoundException.getMessage()).contains("Root attribute [root] does not exist or can not be accessed"); } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/EscaperExtensionTest.java ================================================ /* * This file is part of Pebble. *

* Copyright (c) 2014 by Mitchell Bösecke *

* For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.extension.AbstractExtension; import io.pebbletemplates.pebble.extension.Function; import io.pebbletemplates.pebble.loader.StringLoader; import io.pebbletemplates.pebble.extension.escaper.EscapeFilter; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import org.junit.jupiter.api.Test; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.math.BigDecimal; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; class EscaperExtensionTest { @Test void testEscapeHtml() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ '&<>\"\\'' | escape }}"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("&<>"'", writer.toString()); } @Test void testPrintBigDecimal() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(true).build(); String source = "{{ num }}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); BigDecimal num = new BigDecimal("1234E+4"); context.put("num", num); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals(num.toPlainString(), writer.toString()); assertNotEquals(num.toString(), writer.toString()); } @Test void testEscapeContextVariable() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ text | escape(strategy='html') }}"); Map context = new HashMap<>(); context.put("text", "&<>\"'"); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("&<>"'", writer.toString()); } @Test void testEscapeWithNamedArguments() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ '&<>\"\\'' | escape(strategy='html') }}"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("&<>"'", writer.toString()); } @Test void testAutoescapeLiteral() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ '
' }}"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("
", writer.toString()); } @Test void testAutoescapePrintExpression() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ text }}"); Map context = new HashMap<>(); context.put("text", "
"); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("<br />", writer.toString()); } @Test void testAutoescapeNonString() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ text }}"); Map context = new HashMap<>(); context.put("text", Collections.singletonList("
")); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("[<br />]", writer.toString()); } @Test void testDisableAutoEscaping() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false) .autoEscaping(false).build(); PebbleTemplate template = pebble.getTemplate("{{ text }}"); Map context = new HashMap<>(); context.put("text", "
"); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("
", writer.toString()); } @Test void testEscapeIntoAbbreviate() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ text | escape | abbreviate(5)}}"); Map context = new HashMap<>(); context.put("text", "1234567"); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("12...", writer.toString()); } @Test void testDoubleEscaping() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ text | escape }}"); Map context = new HashMap<>(); context.put("text", "
"); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("<br />", writer.toString()); } @Test void testAutoescapeToken() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false) .autoEscaping(false).build(); PebbleTemplate template = pebble.getTemplate( "{% autoescape 'html' %}{{ text }}{% endautoescape %}" + "{% autoescape %}{{ text }}{% endautoescape %}" + "{% autoescape false %}{{ text }}{% endautoescape %}"); Map context = new HashMap<>(); context.put("text", "
"); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("<br /><br />
", writer.toString()); } @Test void testAutoEscapingMacroOutput() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble .getTemplate("{{ test(danger) }}{% macro test(input) %}<{{ input }}>{% endmacro %}"); Writer writer = new StringWriter(); Map context = new HashMap<>(); context.put("danger", "
"); template.evaluate(writer, context); assertEquals("<<br>>", writer.toString()); } @Test void testAutoEscapingInclude() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("templates/template.autoescapeInclude1.peb"); Map context = new HashMap<>(); context.put("text", "
"); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("<<br>>", writer.toString()); } @Test void testAutoEscapingParentFunction() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("templates/template.autoescapeParent1.peb"); Map context = new HashMap<>(); context.put("text", "
"); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("<<br>>", writer.toString()); } @Test void testAutoEscapingBlockFunction() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble .getTemplate("{% block header %}<{{ text }}>{% endblock %}{{ block('header') }}"); Map context = new HashMap<>(); context.put("text", "
"); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("<<br>><<br>>", writer.toString()); } @Test void testCustomEscapingStrategy() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false) .defaultEscapingStrategy("custom") .addEscapingStrategy("custom", input -> input.replace('a', 'b')).build(); // replaces all a's with b's PebbleTemplate template = pebble.getTemplate("{{ text }}"); Map context = new HashMap<>(); context.put("text", "my name is alex"); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("my nbme is blex", writer.toString()); } @Test void testEscapeFunction() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false) .extension(new TestExtension()).build(); PebbleTemplate template = pebble.getTemplate("{{ bad() }}"); Map context = new HashMap<>(); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("<script>alert("injection");</script>", writer.toString()); } @Test void testNoEscapeMacro() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble .getTemplate("{{ test() }}{% macro test() %}
{% endmacro %}"); Map context = new HashMap<>(); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("
", writer.toString()); } @Test void testCompareSafeStrings() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ text|raw == text|raw }}"); Map context = new HashMap<>(); context.put("text", "a"); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("true", writer.toString()); } @Test void testEscapeJson() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder() .loader(new StringLoader()) .strictVariables(false) .defaultEscapingStrategy(EscapeFilter.JSON_ESCAPE_STRATEGY) .build(); PebbleTemplate template = pebble.getTemplate("{{ text }}"); Map context = new HashMap<>(); context.put("text", "{\"a\": \"a/b/c\"}"); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("{\\\"a\\\": \\\"a/b/c\\\"}", writer.toString()); } public static class TestExtension extends AbstractExtension { @Override public Map getFunctions() { return Collections.singletonMap("bad", new Function() { @Override public List getArgumentNames() { return null; } @Override public Object execute(Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { return ""; } }); } } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/ExtendingPebbleTest.java ================================================ /* * This file is part of Pebble. *

* Copyright (c) 2014 by Mitchell Bösecke *

* For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.extension.AbstractExtension; import io.pebbletemplates.pebble.extension.Filter; import io.pebbletemplates.pebble.loader.StringLoader; import io.pebbletemplates.pebble.attributes.AttributeResolver; import io.pebbletemplates.pebble.attributes.ResolvedAttribute; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import org.junit.jupiter.api.Test; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; class ExtendingPebbleTest { /** * Issue #51 */ @Test void testFilterWithoutArgumentsCanAccessEvaluationContext() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder() .loader(new StringLoader()) .strictVariables(false) .extension(new CustomExtensionWithFilter()) .build(); PebbleTemplate template = pebble.getTemplate("{{ 'test' | noArgumentsButCanAccessContext }}"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("success", writer.toString()); } @Test void testCustomAttributeResolverEvaluateFirst() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder() .loader(new StringLoader()) .strictVariables(false) .extension(new CustomExtensionWithAttributeResolver()) .build(); PebbleTemplate template = pebble.getTemplate("hello {{ person.name }}"); Map context = new HashMap<>(); context.put("person", new SimplePerson()); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("hello customAttributeResolver", writer.toString()); } private static final class CustomExtensionWithFilter extends AbstractExtension { @Override public Map getFilters() { Map filters = new HashMap<>(); filters.put("noArgumentsButCanAccessContext", new Filter() { @Override public List getArgumentNames() { return null; } @Override public String apply(Object input, Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { if (context != null && self != null) { return "success"; } else { return "failure"; } } }); return filters; } } private static final class CustomExtensionWithAttributeResolver extends AbstractExtension { @Override public List getAttributeResolver() { List attributeResolvers = new ArrayList<>(); attributeResolvers.add( (instance, attribute, argumentValues, args, isStrictVariables, filename, lineNumber) -> new ResolvedAttribute("customAttributeResolver")); return attributeResolvers; } } private static class SimplePerson { public final String name = "Bob"; } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/FileLoaderTest.java ================================================ package io.pebbletemplates.pebble; import io.pebbletemplates.pebble.error.LoaderException; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.loader.FileLoader; import io.pebbletemplates.pebble.loader.Loader; import io.pebbletemplates.pebble.template.PebbleTemplate; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.net.URISyntaxException; import java.nio.file.Paths; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; public class FileLoaderTest { @Test void testFileLoaderPrefixNull() { assertThrows(LoaderException.class, () -> new FileLoader(null)); } @Test void testFileLoaderPrefixEmpty() { assertThrows(LoaderException.class, () -> new FileLoader(" ")); } @Test void testFileLoaderPrefixRelativePath() { assertThrows(LoaderException.class, () -> new FileLoader(" ../bar ")); } @Test void testFileLoader() throws PebbleException, IOException, URISyntaxException { String prefix = Paths.get(this.getClass().getClassLoader().getResource("templates").toURI()).toString(); Loader loader = new FileLoader(prefix); loader.setSuffix(".suffix"); PebbleEngine engine = new PebbleEngine.Builder().loader(loader).strictVariables(false).build(); PebbleTemplate template1 = engine.getTemplate("template.loaderTest.peb"); Writer writer1 = new StringWriter(); template1.evaluate(writer1); assertEquals("SUCCESS", writer1.toString()); } @Test void testFileLoaderAbsoluteTemplateName() throws PebbleException, URISyntaxException { String prefix = Paths.get(this.getClass().getClassLoader().getResource("templates").toURI()).toString(); Loader loader = new FileLoader(prefix); loader.setSuffix(".suffix"); PebbleEngine engine = new PebbleEngine.Builder().loader(loader).strictVariables(false).build(); assertThrows(LoaderException.class, () -> engine.getTemplate("/template.loaderTest.peb")); } @Test void testFileLoaderTemplateNameIsADirectory() throws PebbleException, URISyntaxException { String prefix = Paths.get(this.getClass().getClassLoader().getResource("templates").toURI()).toString(); Loader loader = new FileLoader(prefix); PebbleEngine engine = new PebbleEngine.Builder().loader(loader).strictVariables(false).build(); assertThrows(LoaderException.class, () -> engine.getTemplate("loader")); } @Test void testFileLoaderRelativeTemplateName() throws PebbleException, IOException, URISyntaxException { String prefix = Paths.get(this.getClass().getClassLoader().getResource("templates").toURI()).getParent().toString(); Loader loader = new FileLoader(prefix); loader.setSuffix(".suffix"); PebbleEngine engine = new PebbleEngine.Builder().loader(loader).strictVariables(false).build(); PebbleTemplate template1 = engine.getTemplate("templates/template.loaderTest.peb"); Writer writer1 = new StringWriter(); template1.evaluate(writer1); assertEquals("SUCCESS", writer1.toString()); } @Test void testFileLoaderPathTraversal() throws PebbleException, URISyntaxException { String prefix = Paths.get(this.getClass().getClassLoader().getResource("templates").toURI()).toString(); Loader loader = new FileLoader(prefix); loader.setSuffix(".peb"); PebbleEngine engine = new PebbleEngine.Builder().loader(loader).strictVariables(false).build(); assertThrows(LoaderException.class, () -> engine.getTemplate("../template-tests/DoubleNestedIfStatement")); } @ParameterizedTest @ValueSource(strings = {"%2e%2e%2f", "%2e%2e/", "..%2f", "%2e%2e%5c", "%2e%2e\\", "..%5c", "%252e%252e%255c", "..%255c"}) void testFileLoaderPathTraversalEncoded(String relativePath) throws URISyntaxException { String prefix = Paths.get(this.getClass().getClassLoader().getResource("templates").toURI()).toString(); Loader loader = new FileLoader(prefix); loader.setSuffix(".peb"); PebbleEngine engine = new PebbleEngine.Builder().loader(loader).strictVariables(false).build(); assertThrows(LoaderException.class, () -> engine.getTemplate(relativePath + "template-tests/DoubleNestedIfStatement")); } @Test void testFileLoaderUnsupportedCharset() throws PebbleException, URISyntaxException { String prefix = Paths.get(this.getClass().getClassLoader().getResource("templates").toURI()).toString(); Loader loader = new FileLoader(prefix); loader.setCharset("foobar"); PebbleEngine engine = new PebbleEngine.Builder().loader(loader).strictVariables(false).build(); assertThrows(LoaderException.class, () -> engine.getTemplate("template.loaderTest.peb")); } /** * Tests if relative includes work. Issue #162. */ @Test void testFileLoaderPathWithBackslash() throws IOException, URISyntaxException { String prefix = Paths.get(this.getClass().getClassLoader().getResource("templates").toURI()).toString(); PebbleEngine pebble = new PebbleEngine.Builder().loader(new FileLoader(prefix)).build(); PebbleTemplate template = pebble.getTemplate("relativepath/subdirectory1/template.forwardslashes.peb".replace("/", "\\")); // ensure backslashes in all environments Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("included", writer.toString()); } /** * Issue #162. */ @Test void testFileLoaderPathWithForwardSlash() throws IOException, URISyntaxException { String prefix = Paths.get(this.getClass().getClassLoader().getResource("templates").toURI()).toString(); PebbleEngine pebble = new PebbleEngine.Builder().loader(new FileLoader(prefix)).build(); PebbleTemplate template = pebble.getTemplate("relativepath/subdirectory1/template.backwardslashes.peb"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("included", writer.toString()); } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/ForTest.java ================================================ package io.pebbletemplates.pebble; import io.pebbletemplates.pebble.error.ParserException; import io.pebbletemplates.pebble.loader.StringLoader; import io.pebbletemplates.pebble.template.PebbleTemplate; import org.junit.jupiter.api.Test; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; class ForTest { @Test void testForLengthWithOperation() throws IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()).strictVariables(false).build(); String source = "{% for user in users %}{% if loop.index < ( loop.length - 1) %}{{user.username}}{% endif %}{% endfor %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); List users = new ArrayList<>(); users.add(new User("Alex")); users.add(new User("Bob")); users.add(new User("John")); context.put("users", users); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("AlexBob", writer.toString()); } @Test void testForRevIndexWithOperation() throws IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()).strictVariables(false).build(); String source = "{% for user in users %}{% if (loop.revindex - 1) >= 0 %}{{user.username}}{% endif %}{% endfor %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); List users = new ArrayList<>(); users.add(new User("Alex")); users.add(new User("Bob")); users.add(new User("John")); context.put("users", users); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("AlexBob", writer.toString()); } @Test void testInvalidIdentifierName() { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()).strictVariables(false).build(); try { String source = "{% for <= in users %}{% endfor %}"; pebble.getTemplate(source); fail("Exception not thrown"); } catch (ParserException e) { assertEquals("Unexpected token of value \"<=\" and type OPERATOR, expected token of type NAME ({% for <= in users %}{% endfor %}:1)", e.getMessage()); } } public static class User { public final String username; public User(String username) { this.username = username; } } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/GetAttributeTest.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble; import io.pebbletemplates.pebble.attributes.methodaccess.NoOpMethodAccessValidator; import io.pebbletemplates.pebble.error.AttributeNotFoundException; import io.pebbletemplates.pebble.error.ClassAccessException; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.error.RootAttributeNotFoundException; import io.pebbletemplates.pebble.loader.StringLoader; import io.pebbletemplates.pebble.template.PebbleTemplate; import org.junit.jupiter.api.Test; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.util.*; import static org.junit.jupiter.api.Assertions.*; class GetAttributeTest { @Test void testOneLayerAttributeNesting() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(true).build(); PebbleTemplate template = pebble.getTemplate("hello {{ object.name }}"); Map context = new HashMap<>(); context.put("object", new SimpleObject()); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("hello Steve", writer.toString()); } @Test void testAttributeCacheHitting() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(true).build(); PebbleTemplate template = pebble.getTemplate("hello {{ object.name }}{{ object.name }}"); Map context = new HashMap<>(); context.put("object", new SimpleObject()); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("hello SteveSteve", writer.toString()); } @Test void testMultiLayerAttributeNesting() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(true).build(); PebbleTemplate template = pebble .getTemplate("hello {{ object.simpleObject2.simpleObject.name }}"); Map context = new HashMap<>(); context.put("object", new SimpleObject3()); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("hello Steve", writer.toString()); } @Test void testHashmapAttribute() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(true).build(); PebbleTemplate template = pebble.getTemplate("hello {{ object.name }}"); Map context = new HashMap<>(); Map map = new HashMap<>(); map.put("name", "Steve"); context.put("object", map); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("hello Steve", writer.toString()); } @Test void testHashmapAttributeWithArgumentOfNull() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()).build(); PebbleTemplate template = pebble.getTemplate("hello {{ object[missingContextProperty] }}"); Map context = new HashMap<>(); Map map = new HashMap<>(); map.put("name", "Steve"); context.put("object", map); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("hello ", writer.toString()); } @Test void testNonExistingHashMapAttributeWithoutStrictVariables() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ object.nonExisting }}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); Map map = new HashMap<>(); map.put("name", "Steve"); context.put("object", map); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("", writer.toString()); } @Test void testNonExistingMapAttributeWithStrictVariables() throws PebbleException, IOException { assertThrows(AttributeNotFoundException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(true).build(); String source = "{{ object.nonExisting }}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); Map map = new HashMap<>(); map.put("name", "Steve"); context.put("object", map); Writer writer = new StringWriter(); template.evaluate(writer, context); }); } @Test void testNonExistingMapAttributeWithStrictVariablesAndEmptyMap() throws PebbleException, IOException { assertThrows(AttributeNotFoundException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(true).build(); String source = "{{ object.nonExisting }}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("object", new HashMap<>()); Writer writer = new StringWriter(); template.evaluate(writer, context); }); } @Test void testNullMapValueWithoutStrictVariables() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("hello {{ map.name }}"); Map context = new HashMap<>(); Map map = new HashMap<>(); map.put("name", null); context.put("map", map); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("hello ", writer.toString()); } /** * Issue 446 */ @Test void testNullMapValueWithStrictVariables() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(true).build(); PebbleTemplate template = pebble.getTemplate("hello {{ map.name }}"); Map context = new HashMap<>(); Map map = new HashMap<>(); map.put("name", null); context.put("map", map); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("hello ", writer.toString()); } @Test void testMethodAttribute() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(true).build(); PebbleTemplate template = pebble.getTemplate("hello {{ object.name }}"); Map context = new HashMap<>(); context.put("object", new SimpleObject4()); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("hello Steve", writer.toString()); } @Test void testTwoMethodsAlmostSameName() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(true).build(); PebbleTemplate template = pebble.getTemplate("hello {{ object.something() }}"); Map context = new HashMap<>(); context.put("object", new ObjectMethodSameName()); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("hello Steve", writer.toString()); } /** * Make sure we are properly accounting for getting the class object from an Object in all * situations: * * | AllowUnsafeMethods | Strict Variables | Access Type | Result | * | ------------------ | ---------------- | ----------- | ------- | * | true | false | property | allowed | * | true | false | method | allowed | * | true | true | property | allowed | * | true | true | method | allowed | * | false | false | property | throw | * | false | false | method | throw | * | false | true | property | throw | * | false | true | method | throw | */ @Test void testAccessingClass_AllowUnsafeMethodsOn_StrictVariableOff_Property() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .methodAccessValidator(new NoOpMethodAccessValidator()) .strictVariables(false) .build(); PebbleTemplate template = pebble.getTemplate("hello [{{ object.class }}]"); Map context = new HashMap<>(); context.put("object", new SimpleObject()); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("hello [" + SimpleObject.class.toString() + "]", writer.toString()); } @Test void testAccessingClass_AllowUnsafeMethodsOn_StrictVariableOff_Method() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .methodAccessValidator(new NoOpMethodAccessValidator()) .strictVariables(false) .build(); PebbleTemplate template = pebble.getTemplate("hello [{{ object.getClass() }}]"); Map context = new HashMap<>(); context.put("object", new SimpleObject()); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("hello [" + SimpleObject.class.toString() + "]", writer.toString()); } @Test void testAccessingClass_AllowUnsafeMethodsOn_StrictVariableOn_Property() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .methodAccessValidator(new NoOpMethodAccessValidator()) .strictVariables(true) .build(); PebbleTemplate template = pebble.getTemplate("hello [{{ object.class }}]"); Map context = new HashMap<>(); context.put("object", new SimpleObject()); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("hello [" + SimpleObject.class.toString() + "]", writer.toString()); } @Test void testAccessingClass_AllowUnsafeMethodsOn_StrictVariableOn_Method() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .methodAccessValidator(new NoOpMethodAccessValidator()) .strictVariables(true) .build(); PebbleTemplate template = pebble.getTemplate("hello [{{ object.getClass() }}]"); Map context = new HashMap<>(); context.put("object", new SimpleObject()); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("hello [" + SimpleObject.class.toString() + "]", writer.toString()); } @Test void testAccessingClass_AllowUnsafeMethodsOff_StrictVariableOff_Property() throws PebbleException, IOException { assertThrows(ClassAccessException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false) .build(); PebbleTemplate template = pebble.getTemplate("hello [{{ object.class }}]"); Map context = new HashMap<>(); context.put("object", new SimpleObject()); Writer writer = new StringWriter(); template.evaluate(writer, context); }); } @Test void testAccessingClass_AllowUnsafeMethodsOff_StrictVariableOff_Method() throws PebbleException, IOException { assertThrows(ClassAccessException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false) .build(); PebbleTemplate template = pebble.getTemplate("hello [{{ object.getClass() }}]"); Map context = new HashMap<>(); context.put("object", new SimpleObject()); Writer writer = new StringWriter(); template.evaluate(writer, context); }); } @Test void testAccessingClass_AllowUnsafeMethodsOff_StrictVariableOn_Property() throws PebbleException, IOException { assertThrows(ClassAccessException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(true) .build(); PebbleTemplate template = pebble.getTemplate("hello [{{ object.class }}]"); Map context = new HashMap<>(); context.put("object", new SimpleObject()); Writer writer = new StringWriter(); template.evaluate(writer, context); }); } @Test void testAccessingClass_AllowUnsafeMethodsOff_StrictVariableOn_Method() throws PebbleException, IOException { assertThrows(ClassAccessException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(true) .build(); PebbleTemplate template = pebble.getTemplate("hello [{{ object.getClass() }}]"); Map context = new HashMap<>(); context.put("object", new SimpleObject()); Writer writer = new StringWriter(); template.evaluate(writer, context); }); } @Test void testAccessingClass_AllowUnsafeMethodsOnIsCaseInsensitive_Property() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder() .loader(new StringLoader()) .methodAccessValidator(new NoOpMethodAccessValidator()) .build(); PebbleTemplate template = pebble.getTemplate("hello [{{ object.ClAsS }}]"); Map context = new HashMap<>(); context.put("object", new SimpleObject()); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("hello [" + SimpleObject.class.toString() + "]", writer.toString()); } @Test void testAccessingClass_AllowUnsafeMethodsOffIsCaseInsensitive_Property() throws PebbleException, IOException { assertThrows(ClassAccessException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder() .loader(new StringLoader()) .build(); PebbleTemplate template = pebble.getTemplate("hello [{{ object.ClAsS }}]"); Map context = new HashMap<>(); context.put("object", new SimpleObject()); Writer writer = new StringWriter(); template.evaluate(writer, context); }); } @Test void testAccessingClass_AllowUnsafeMethodsOnIsCaseInsensitive_Method() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder() .loader(new StringLoader()) .methodAccessValidator(new NoOpMethodAccessValidator()) .build(); PebbleTemplate template = pebble.getTemplate("hello [{{ object.GeTcLAsS() }}]"); Map context = new HashMap<>(); context.put("object", new SimpleObject()); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("hello [" + SimpleObject.class.toString() + "]", writer.toString()); } @Test void testAccessingClass_AllowUnsafeMethodsOffIsCaseInsensitive_Method() throws PebbleException, IOException { assertThrows(ClassAccessException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder() .loader(new StringLoader()) .build(); PebbleTemplate template = pebble.getTemplate("hello [{{ object.GeTcLAsS() }}]"); Map context = new HashMap<>(); context.put("object", new SimpleObject()); Writer writer = new StringWriter(); template.evaluate(writer, context); }); } @Test void testAccessingClass_AllowUnsafeMethodsOffForMethodNotify_thenThrowException() throws PebbleException, IOException { assertThrows(ClassAccessException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder() .loader(new StringLoader()) .build(); PebbleTemplate template = pebble.getTemplate("hello [{{ object.notify() }}]"); Map context = new HashMap<>(); context.put("object", new SimpleObject()); Writer writer = new StringWriter(); template.evaluate(writer, context); }); } /** * The GetAttribute expression involves caching, we test with different objects to make sure that * the caching doesnt have any negative side effects. */ @Test void testMethodAttributeWithDifferentObjects() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(true).build(); PebbleTemplate template = pebble.getTemplate("hello {{ object.name }}"); Map context1 = new HashMap<>(); context1.put("object", new CustomizableObject("Alex")); Writer writer1 = new StringWriter(); template.evaluate(writer1, context1); assertEquals("hello Alex", writer1.toString()); Map context2 = new HashMap<>(); context2.put("object", new CustomizableObject("Steve")); Writer writer2 = new StringWriter(); template.evaluate(writer2, context2); assertEquals("hello Steve", writer2.toString()); } @Test void testBeanMethodWithArgument() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(true).build(); PebbleTemplate template = pebble.getTemplate("hello {{ object.name('Steve') }}"); Map context = new HashMap<>(); context.put("object", new BeanWithMethodsThatHaveArguments()); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("hello Steve", writer.toString()); } @Test void testBeanMethodWithLongArgument() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(true).build(); PebbleTemplate template = pebble.getTemplate("hello {{ object.number(2) }}"); Map context = new HashMap<>(); context.put("object", new BeanWithMethodsThatHaveArguments()); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("hello 2", writer.toString()); } @Test void testBeanMethodWithLongArgument2() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(true).build(); PebbleTemplate template = pebble.getTemplate("hello {{ object.number(2L) }}"); Map context = new HashMap<>(); context.put("object", new BeanWithMethodsThatHaveArguments()); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("hello 2", writer.toString()); } @Test void testBeanMethodWithTreatLiteralDecimalAsLong() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(true).literalDecimalTreatedAsInteger(false) .greedyMatchMethod(false).build(); PebbleTemplate template = pebble.getTemplate("hello {{ object.integer(2) }}"); Map context = new HashMap<>(); context.put("object", new BeanWithMethodsThatHaveArguments()); try { Writer writer = new StringWriter(); template.evaluate(writer, context); fail("expected PebbleException"); } catch (PebbleException e) { assertEquals(e.getLineNumber(), (Integer) 1); assertEquals(e.getClass(), AttributeNotFoundException.class); } } @Test void testBeanMethodWithTreatNumberAsInteger() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(true).literalDecimalTreatedAsInteger(true) .build(); PebbleTemplate template = pebble.getTemplate("hello {{ object.integer(2) }}"); Map context = new HashMap<>(); context.put("object", new BeanWithMethodsThatHaveArguments()); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("hello 2", writer.toString()); } @Test void testBeanMethodWithGreedyMatchArgument() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(true).greedyMatchMethod(true).build(); PebbleTemplate template = pebble .getTemplate("hello {{ object.integer(2) }} {{ object.short(2) }}"); Map context = new HashMap<>(); context.put("object", new BeanWithMethodsThatHaveArguments()); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("hello 2 2", writer.toString()); } @Test void testBeanMethodWithNumberLiteralsAsBigDecimals() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(true).literalNumbersAsBigDecimals(true) .build(); PebbleTemplate template = pebble.getTemplate("hello {{ 1234567890123456789012345678901234567890 }}"); Map context = new HashMap<>(); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("hello 1234567890123456789012345678901234567890", writer.toString()); } @Test void testBeanMethodWithoutNumberLiteralsAsBigDecimals() throws PebbleException, IOException { assertThrows(NumberFormatException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(true).literalNumbersAsBigDecimals(false) .build(); PebbleTemplate template = pebble.getTemplate("hello {{ 1234567890123456789012345678901234567890 }}"); Map context = new HashMap<>(); Writer writer = new StringWriter(); template.evaluate(writer, context); }); } @Test void testBeanMethodWithOverloadedArgument() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(true).build(); PebbleTemplate template = pebble.getTemplate("hello {{ object.number(2.0) }}"); Map context = new HashMap<>(); context.put("object", new BeanWithMethodsThatHaveArguments()); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("hello 4.0", writer.toString()); } @Test void testBeanMethodWithTwoArguments() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(true).build(); PebbleTemplate template = pebble.getTemplate("hello {{ object.multiply(2, 3) }}"); Map context = new HashMap<>(); context.put("object", new BeanWithMethodsThatHaveArguments()); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("hello 6", writer.toString()); } @Test void testGetMethodAttribute() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("hello {{ object.name }}"); Map context = new HashMap<>(); context.put("object", new SimpleObject5()); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("hello Steve", writer.toString()); } @Test void testHasMethodAttribute() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("hello {{ object.name }}"); Map context = new HashMap<>(); context.put("object", new SimpleObject9()); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("hello Steve", writer.toString()); } @Test void testIsMethodAttribute() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("hello {{ object.name }}"); Map context = new HashMap<>(); context.put("object", new SimpleObject6()); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("hello Steve", writer.toString()); } @Test void testComplexNestedAttributes() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "hello {{ object.map.SimpleObject2.simpleObject.name }}. My name is {{ object.map.SimpleObject6.name }}."; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("object", new ComplexObject()); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("hello Steve. My name is Steve.", writer.toString()); } @Test void testAttributeOfNullObjectWithStrictVariables() throws PebbleException, IOException { assertThrows(RootAttributeNotFoundException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(true).build(); PebbleTemplate template = pebble.getTemplate("hello {{ object.name }}"); Writer writer = new StringWriter(); template.evaluate(writer); }); } @Test void testAttributeOfNullObjectWithoutStrictVariables() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("hello {{ object.name }}"); Map context = new HashMap<>(); context.put("object", null); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("hello ", writer.toString()); } @Test void testNonExistingAttributeWithoutStrictVariables() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("hello {{ object.name }}"); Map context = new HashMap<>(); context.put("object", new Object()); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("hello ", writer.toString()); } @Test void testNonExistingAttributeWithStrictVariables() throws PebbleException, IOException { assertThrows(AttributeNotFoundException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(true).build(); PebbleTemplate template = pebble.getTemplate("hello {{ object.name }}"); Map context = new HashMap<>(); context.put("object", new Object()); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("hello ", writer.toString()); }); } @Test void testNullAttributeWithoutStrictVariables() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("hello {{ object.name }}"); Map context = new HashMap<>(); context.put("object", new SimpleObject7()); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("hello ", writer.toString()); } /** * Should behave the same as it does with strictVariables = false. */ @Test void testNullAttributeWithStrictVariables() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(true).build(); PebbleTemplate template = pebble.getTemplate("hello {{ object.name }}"); Map context = new HashMap<>(); context.put("object", new SimpleObject7()); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("hello ", writer.toString()); } @Test() void testPrimitiveAttribute() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("hello {{ object.name }}"); Map context = new HashMap<>(); context.put("object", new SimpleObject8()); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("hello true", writer.toString()); } @Test void testArrayIndexAttribute() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ arr[2] }}"); Map context = new HashMap<>(); String[] data = new String[3]; data[0] = "Zero"; data[1] = "One"; data[2] = "Two"; context.put("arr", data); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("Two", writer.toString()); } @Test void testListIndexAttribute() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ arr[2] }}"); Map context = new HashMap<>(); List data = new ArrayList<>(); data.add("Zero"); data.add("One"); data.add("Two"); context.put("arr", data); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("Two", writer.toString()); } /** * Tests retrieving a non-existing index from a list with strict mode on. */ @Test void testListNonExistingIndexAttributeWithStrictMode() throws PebbleException, IOException { assertThrows(AttributeNotFoundException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(true).build(); PebbleTemplate template = pebble.getTemplate("{{ arr[1] }}"); Map context = new HashMap<>(); List data = new ArrayList<>(); context.put("arr", data); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("Two", writer.toString()); }); } /** * Tests retrieving a non-existing index from a list with strict mode off. */ @Test void testListNonExistingIndexAttribute() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ arr[1] }}"); Map context = new HashMap<>(); List data = new ArrayList<>(); context.put("arr", data); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("", writer.toString()); } @Test() void testInheritedAttribute() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(true).build(); PebbleTemplate template = pebble.getTemplate("hello {{ object.name }}"); Map context = new HashMap<>(); context.put("object", new ChildObject()); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("hello parent", writer.toString()); } public class Person { public final String name = "Name"; public final String surname = "Surname"; } @Test void testAccessingValueWithSubscriptInLoop() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(true).build(); String source = "{% for attribute in ['name', 'surname']%}{{ person[attribute] }}{% endfor %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("person", new Person()); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("NameSurname", writer.toString()); } public class SimpleObject { public final String name = "Steve"; } public class SimpleObject2 { public final SimpleObject simpleObject = new SimpleObject(); } public class SimpleObject3 { public final SimpleObject2 simpleObject2 = new SimpleObject2(); } public class SimpleObject4 { public String name() { return "Steve"; } } public class SimpleObject5 { public String getName() { return "Steve"; } } public class SimpleObject6 { public String isName() { return "Steve"; } } public class SimpleObject7 { public String name = null; } public class SimpleObject8 { public boolean name = true; } public class SimpleObject9 { public String hasName() { return "Steve"; } } public class ObjectMethodSameName { public String something() { return "Steve"; } public boolean hasSomething() { return true; } } public class BeanWithMethodsThatHaveArguments { public String getName(String name) { return name; } public Double getNumber(Double number) { return number * 2; } public Long getNumber(Long number) { return number; } public Integer getInteger(Integer number) { return number; } public Short getShort(short number) { return number; } public Long multiply(Long one, Long two) { return one * two; } } public class ComplexObject { public final Map map = new HashMap<>(); { this.map.put("SimpleObject2", new SimpleObject2()); this.map.put("SimpleObject6", new SimpleObject6()); } } public class CustomizableObject { private final String name; public CustomizableObject(String name) { this.name = name; } public String getName() { return this.name; } } public class ChildObject extends ParentObject { } public class ParentObject { public String getName() { return "parent"; } } @Test() void testPrimitiveArgument() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(true).build(); PebbleTemplate template = pebble .getTemplate("{{ obj.getStringFromLong(1) }} {{ obj.getStringFromLongs(1,2) }}" + " {{ obj.getStringFromBoolean(true) }}"); Map context = new HashMap<>(); context.put("obj", new PrimitiveArguments()); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("1 1 2 true", writer.toString()); } @Test void testBeanMethodWithNullArgument() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(true).build(); PebbleTemplate template = pebble.getTemplate("hello {{ object.name(var) }}"); Map context = new HashMap<>(); context.put("object", new BeanWithMethodsThatHaveArguments()); context.put("var", null); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("hello ", writer.toString()); } public class PrimitiveArguments { public String getStringFromLong(long id) { return String.valueOf(id); } public String getStringFromLongs(Long first, long second) { return first + " " + second; } public String getStringFromBoolean(boolean bool) { return String.valueOf(bool); } } @Test void testAttributePrimitiveAccessWithEmptyMap() throws Exception { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate(String.format("hello {{ object[1].name }}")); Map context = new HashMap<>(); context.put("object", new HashMap<>()); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("hello ", writer.toString()); } @Test void testAttributePrimitiveAccessWithInteger() throws Exception { String result = this.testAttributePrimitiveAccess(1); assertEquals("hello Steve", result); } private String testAttributePrimitiveAccess(Number value) throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("hello {{ object[key].name }}"); Map context = new HashMap<>(); context.put("key", value); context.put("object", Collections.singletonMap(value, new SimpleObject())); Writer writer = new StringWriter(); template.evaluate(writer, context); return writer.toString(); } @Test void testAttributePrimitiveAccessWithLong() throws Exception { String result = this.testAttributePrimitiveAccess(1L); assertEquals("hello Steve", result); } @Test void testAttributePrimitiveAccessWithDouble() throws Exception { String result = this.testAttributePrimitiveAccess(1.05D); assertEquals("hello Steve", result); } @Test void testAttributePrimitiveAccessWithFloat() throws Exception { String result = this.testAttributePrimitiveAccess(1.05F); assertEquals("hello Steve", result); } @Test void testAttributePrimitiveAccessWithShort() throws Exception { String result = this.testAttributePrimitiveAccess((short) 1); assertEquals("hello Steve", result); } @Test void testAttributePrimitiveAccessWithByte() throws Exception { String result = this.testAttributePrimitiveAccess((byte) 1); assertEquals("hello Steve", result); } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/I18nExtensionTest.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.loader.StringLoader; import io.pebbletemplates.pebble.template.PebbleTemplate; import org.junit.jupiter.api.Test; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.util.Locale; import static org.junit.jupiter.api.Assertions.assertEquals; class I18nExtensionTest { @Test void testSimpleLookup() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()).build(); PebbleTemplate template = pebble.getTemplate("{{ i18n('testMessages','greeting') }}"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("Hello", writer.toString()); } @Test void testMessageWithNamedArguments() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()).build(); PebbleTemplate template = pebble .getTemplate("{{ i18n(bundle='testMessages',key='greeting') }}"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("Hello", writer.toString()); } @Test void testLookupWithLocale() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()).build(); PebbleTemplate template = pebble.getTemplate("{{ i18n('testMessages','greeting') }}"); Writer writer = new StringWriter(); template.evaluate(writer, new Locale("es", "US")); assertEquals("Hola", writer.toString()); } @Test void testLookupSpecialChar() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()).build(); PebbleTemplate template = pebble .getTemplate("{{ i18n('testMessages','greeting.specialchars') }}"); Writer writer = new StringWriter(); template.evaluate(writer, new Locale("es", "US")); assertEquals("Hola español", writer.toString()); } @Test void testMessageWithParams() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()).build(); PebbleTemplate template = pebble .getTemplate("{{ i18n('testMessages','greeting.someone', 'Pebble') }}"); Writer writer = new StringWriter(); template.evaluate(writer, new Locale("es", "US")); assertEquals("Hola, Pebble", writer.toString()); } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/IncludeWithParameterTest.java ================================================ package io.pebbletemplates.pebble; import static org.junit.jupiter.api.Assertions.assertEquals; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.util.HashMap; import java.util.Map; import org.junit.jupiter.api.Test; /** * This class tests if includes with parameters work. * * @author Thomas Hunziker */ class IncludeWithParameterTest { /** * Test if parameters are processed correctly. */ @Test void testIncludeWithParameters() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("templates/template.includeWithParameter1.peb"); Map context = new HashMap<>(); context.put("contextVariable", "some-context-variable"); context.put("level", 1); Writer writer = new StringWriter(); template.evaluate(writer, context); String expectedOutput = "simple:simple-value" + "contextVariable:some-context-variable" + "map.position:left" + "map.contextVariable:some-context-variable" + "level:2" + "level-main:1"; assertEquals(expectedOutput, writer.toString()); } @Test void testIncludeWithParametersIsolated() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().strictVariables(false).build(); PebbleTemplate template = pebble .getTemplate("templates/template.includeWithParameterNotIsolated1.peb"); Map context = new HashMap<>(); Writer writer = new StringWriter(); template.evaluate(writer, context); String expectedOutput = "bazbar"; assertEquals(expectedOutput, writer.toString()); } @Test void testIncludeWithParameterObject() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().strictVariables(false).build(); PebbleTemplate template = pebble .getTemplate("templates/template.includeWithParameterObject1.peb"); Map context = new HashMap<>(); context.put("object", new TestObject()); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("Hello title", writer.toString()); } public static class TestObject { public String title = "title"; } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/InheritanceTest.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.loader.StringLoader; import io.pebbletemplates.pebble.template.PebbleTemplate; import org.junit.jupiter.api.Test; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.util.HashMap; import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; class InheritanceTest { private static final String LINE_SEPARATOR = System.lineSeparator(); @Test void testSimpleInheritance() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("templates/template.parent.peb"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("GRANDFATHER TEXT ABOVE HEAD" + LINE_SEPARATOR + LINE_SEPARATOR + "\tPARENT HEAD" + LINE_SEPARATOR + LINE_SEPARATOR + "GRANDFATHER TEXT BELOW HEAD AND ABOVE FOOT" + LINE_SEPARATOR + LINE_SEPARATOR + "\tGRANDFATHER FOOT" + LINE_SEPARATOR + LINE_SEPARATOR + "GRANDFATHER TEXT BELOW FOOT", writer.toString()); } @Test void testMultiLevelInheritance() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("templates/template.child.peb"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("GRANDFATHER TEXT ABOVE HEAD" + LINE_SEPARATOR + LINE_SEPARATOR + "\tCHILD HEAD" + LINE_SEPARATOR + LINE_SEPARATOR + "GRANDFATHER TEXT BELOW HEAD AND ABOVE FOOT" + LINE_SEPARATOR + LINE_SEPARATOR + "\tGRANDFATHER FOOT" + LINE_SEPARATOR + LINE_SEPARATOR + "GRANDFATHER TEXT BELOW FOOT", writer.toString()); } @Test void testDynamicInheritance() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("templates/template.dynamicChild.peb"); Map context = new HashMap<>(); context.put("extendNumberOne", true); Writer writer1 = new StringWriter(); template.evaluate(writer1, context); assertEquals("ONE", writer1.toString()); Writer writer2 = new StringWriter(); context.put("extendNumberOne", false); template.evaluate(writer2, context); assertEquals("TWO", writer2.toString()); } @Test void testNullParent() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble .getTemplate("{% extends null %}success"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("success", writer.toString()); } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/LoaderTest.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble; import io.pebbletemplates.pebble.error.LoaderException; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.loader.*; import io.pebbletemplates.pebble.template.PebbleTemplate; import org.junit.jupiter.api.Test; import java.io.*; import java.net.URISyntaxException; import java.net.URL; import java.net.URLClassLoader; import java.nio.file.Paths; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; class LoaderTest { @Test void testClassLoaderLoader() throws PebbleException, IOException { Loader loader = new ClasspathLoader(); loader.setPrefix("templates"); loader.setSuffix(".peb"); PebbleEngine pebble = new PebbleEngine.Builder().loader(loader).strictVariables(false).build(); PebbleTemplate template1 = pebble.getTemplate("template.loaderTest"); Writer writer1 = new StringWriter(); template1.evaluate(writer1); assertEquals("SUCCESS", writer1.toString()); } @Test void testClassLoaderLoaderWithNestedTemplate() throws PebbleException, IOException { Loader loader = new ClasspathLoader(); loader.setPrefix("templates"); loader.setSuffix(".peb"); PebbleEngine engine = new PebbleEngine.Builder().loader(loader).strictVariables(false).build(); PebbleTemplate template1 = engine.getTemplate("loader/template.loaderTest"); Writer writer1 = new StringWriter(); template1.evaluate(writer1); assertEquals("SUCCESS", writer1.toString()); } @Test void testClassLoaderLoaderWithNestedTemplateInJar() throws PebbleException, IOException { URL resource = this.getClass().getResource("/templateinjar.jar"); assertNotNull(resource); Loader loader = new ClasspathLoader(new URLClassLoader(new URL[]{resource}, null)); loader.setPrefix("templates"); loader.setSuffix(".peb"); PebbleEngine engine = new PebbleEngine.Builder().loader(loader).strictVariables(false).build(); PebbleTemplate template1 = engine.getTemplate("loader/template.loaderTest"); Writer writer1 = new StringWriter(); template1.evaluate(writer1); assertEquals("SUCCESS", writer1.toString()); } @Test void testDelegatingLoader() throws PebbleException, IOException { List> loaders = new ArrayList<>(); loaders.add(new StringLoaderFailure()); loaders.add(new StringLoaderOne()); loaders.add(new StringLoaderTwo()); Loader loader = new DelegatingLoader(loaders); PebbleEngine engine = new PebbleEngine.Builder().loader(loader).strictVariables(false).build(); PebbleTemplate template = engine.getTemplate("fake template name"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("LOADER ONE", writer.toString()); } @Test void testDelegatingLoaderWithFileLoaderThatThrowsInvalidPathException() throws URISyntaxException, IOException { String prefix = Paths.get(this.getClass().getClassLoader().getResource("templates").toURI()).toString(); List> loaders = new ArrayList<>(); loaders.add(new FileLoader(prefix)); // Throws InvalidPathException for HTML content loaders.add(new StringLoader()); // Should handle HTML content as inline template DelegatingLoader loader = new DelegatingLoader(loaders); PebbleEngine engine = new PebbleEngine.Builder().loader(loader).build(); // This simulates passing layout file content (HTML string) to Pebble String layoutContent = "

Hello {{ name }}
"; // With the fix, DelegatingLoader catches InvalidPathException and delegates to // StringLoader PebbleTemplate template = engine.getTemplate(layoutContent); Writer writer = new StringWriter(); Map context = new HashMap<>(); context.put("name", "World"); template.evaluate(writer, context); assertEquals("
Hello World
", writer.toString()); } @Test void testMemoryLoader() throws PebbleException, IOException { MemoryLoader loader = new MemoryLoader(); PebbleEngine pebble = new PebbleEngine.Builder().loader(loader).strictVariables(false).build(); loader.addTemplate("home.html", "{% extends \"layout.html\" %}{% block title %} Home {% endblock %}" + "{% block content %}" + "

Home

" + "

Welcome to my home page. My name is {{ name }}.

" + "{% endblock %}"); loader.addTemplate("layout.html", "" + "" + "Hello Pebble" + "" + "" + "{% block content %}{% endblock %}" + "" + ""); PebbleTemplate template = pebble.getTemplate("home.html"); Map context = new HashMap<>(); context.put("name", "Bob"); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("Hello Pebble

Home

Welcome to my home page. My name is Bob.

", writer.toString()); } @Test void testGetLiteralTemplate() throws IOException { PebbleEngine engine = new PebbleEngine.Builder().build(); PebbleTemplate template = engine.getLiteralTemplate("hello {{ object }}"); Map context = new HashMap<>(); context.put("object", "world"); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("hello world", writer.toString()); } /** * Always fail to find a template */ private class StringLoaderFailure extends StringLoader { @Override public Reader getReader(String templateName) throws LoaderException { throw new LoaderException(null, "Could not find template "); } } private class StringLoaderOne extends StringLoader { @Override public Reader getReader(String templateName) throws LoaderException { return new StringReader("LOADER ONE"); } } private class StringLoaderTwo extends StringLoader { @Override public Reader getReader(String templateName) throws LoaderException { return new StringReader("LOADER TWO"); } } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/LogicTest.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.loader.StringLoader; import io.pebbletemplates.pebble.template.PebbleTemplate; import io.pebbletemplates.pebble.utils.Pair; 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; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.math.BigDecimal; import java.math.BigInteger; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.params.provider.Arguments.arguments; class LogicTest { @Test void testUnaryOperators() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if -2 == -+(5 - 3) %}yes{% else %}no{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("yes", writer.toString()); } @Test void testNotUnaryOperator() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if not (val) %}yes{% else %}no{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); Writer writer; // "val" value not set at all yet writer = new StringWriter(); template.evaluate(writer, context); assertEquals("yes", writer.toString()); context.put("val", null); writer = new StringWriter(); template.evaluate(writer, context); assertEquals("yes", writer.toString()); context.put("val", false); writer = new StringWriter(); template.evaluate(writer, context); assertEquals("yes", writer.toString()); context.put("val", true); writer = new StringWriter(); template.evaluate(writer, context); assertEquals("no", writer.toString()); } @Test void testNotUnaryOperatorWithStrictVariables() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(true).build(); String source = "{% if not (val) %}yes{% else %}no{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); Writer writer; // "val" value not set at all yet try { writer = new StringWriter(); template.evaluate(writer, context); fail("Exception not thrown"); } catch (PebbleException e) { } try { context.put("val", null); writer = new StringWriter(); template.evaluate(writer, context); fail("Exception not thrown"); } catch (PebbleException e) { } context.put("val", false); writer = new StringWriter(); template.evaluate(writer, context); assertEquals("yes", writer.toString()); context.put("val", true); writer = new StringWriter(); template.evaluate(writer, context); assertEquals("no", writer.toString()); } /** * Issue #36 */ @Test void testTruthinessOfNullVariableWithoutStrictMode() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if foobar %}true{% else %}false{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); Writer writer = new StringWriter(); // "foobar" value not set at all yet template.evaluate(writer, context); assertEquals("false", writer.toString()); context.put("foobar", null); writer = new StringWriter(); template.evaluate(writer, context); assertEquals("false", writer.toString()); context.put("foobar", false); writer = new StringWriter(); template.evaluate(writer, context); assertEquals("false", writer.toString()); context.put("foobar", true); writer = new StringWriter(); template.evaluate(writer, context); assertEquals("true", writer.toString()); } @Test void testTruthinessOfNullVariableWithStrictMode() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(true).build(); String source = "{% if foobar %}true{% else %}false{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); Writer writer; // "foobar" value not set at all yet try { writer = new StringWriter(); template.evaluate(writer, context); fail("Exception not thrown"); } catch (PebbleException e) { } try { writer = new StringWriter(); context.put("foobar", null); template.evaluate(writer, context); fail("Exception not thrown"); } catch (PebbleException e) { } writer = new StringWriter(); context.put("foobar", false); template.evaluate(writer, context); assertEquals("false", writer.toString()); writer = new StringWriter(); context.put("foobar", true); template.evaluate(writer, context); assertEquals("true", writer.toString()); } @Test void testBinaryOperators() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ 8 + 5 * 4 - (6 + 10 / 2) + 44 }}-{{ 10%3 }}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("61-1", writer.toString()); } /** * Problem existed where getAttribute would return an Object type which was an invalid operand for * java's algebraic operators. */ @Test void testBinaryOperatorOnAttribute() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ 1 + item.changeInt }} " + "{{ 1 - item.changeInt }} " + "{{ 2 * item.changeInt }} " + "{{ 11 / item.changeInt }} " + "{{ 4 % item.changeInt }}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("item", new Item()); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("4 -2 6 3 1", writer.toString()); } @Test void testBinaryOperatorsBigDecimal() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ number1 + number2 * number1 / number2 }}-{{number1 % number2}}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); Map context = new HashMap<>(); context.put("number1", BigDecimal.valueOf(100d)); context.put("number2", BigDecimal.valueOf(30d)); template.evaluate(writer, context); assertEquals("200.0-10.0", writer.toString()); } @Test void testBinaryOperatorsBigDecimalWithDouble() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ number1 + number2 * number1 / number2 }}-{{number1 % number2}}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); Map context = new HashMap<>(); context.put("number1", BigDecimal.valueOf(100d)); context.put("number2", 30d); template.evaluate(writer, context); assertEquals("200.0-10.0", writer.toString()); } @Test void testBinaryOperatorsBigInteger() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ number1 + number2 * number1 / number2 }}-{{number1 % number2}}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); Map context = new HashMap<>(); context.put("number1", BigInteger.valueOf(100)); context.put("number2", BigInteger.valueOf(30)); template.evaluate(writer, context); assertEquals("200-10", writer.toString()); } @Test void testBinaryOperatorsBigIntegerWithLong() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ number1 + number2 * number1 / number2 }}-{{number1 % number2}}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); Map context = new HashMap<>(); context.put("number1", BigInteger.valueOf(100)); context.put("number2", 30L); template.evaluate(writer, context); assertEquals("200-10", writer.toString()); } @Test void testBinaryOperatorsShort() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ number1 + number2 * number1 / number2 }}-{{number1 % number2}}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); Map context = new HashMap<>(); context.put("number1", (short) 100); context.put("number2", (short) 30); template.evaluate(writer, context); assertEquals("200-10", writer.toString()); } @ParameterizedTest @MethodSource("binaryOperatorTestData") void testBinaryOperatorsWithStringOperands(Object left, Object right, String operator, String expected) throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ left " + operator + " right }}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("left", left); context.put("right", right); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals(expected, writer.toString()); } private static Stream binaryOperatorTestData() { return Stream.of( // Addition (+) arguments(10, 2, "+", "12"), // int + int arguments(10, "2", "+", "102"), // int + string = string concatenation arguments(10, 2.5f, "+", "12.5"), // int + float arguments(10, 2.5d, "+", "12.5"), // int + double arguments(10, 2L, "+", "12"), // int + long arguments("10", 2, "+", "102"), // string + int = string concatenation arguments("10", "2", "+", "102"), // string + string = string concatenation arguments("10", 2.5f, "+", "102.5"), // string + float = string concatenation arguments("10", 2.5d, "+", "102.5"), // string + double = string concatenation arguments("10", 2L, "+", "102"), // string + long = string concatenation arguments(10.5f, 2, "+", "12.5"), // float + int arguments(10.5f, "2", "+", "10.52"), // float + string = string concatenation arguments(10.5f, 2.5f, "+", "13.0"), // float + float arguments(10.5f, 2.5d, "+", "13.0"), // float + double arguments(10.5f, 2L, "+", "12.5"), // float + long arguments(10.5d, 2, "+", "12.5"), // double + int arguments(10.5d, "2", "+", "10.52"), // double + string = string concatenation arguments(10.5d, 2.5f, "+", "13.0"), // double + float arguments(10.5d, 2.5d, "+", "13.0"), // double + double arguments(10.5d, 2L, "+", "12.5"), // double + long arguments(10L, 2, "+", "12"), // long + int arguments(10L, "2", "+", "102"), // long + string = string concatenation arguments(10L, 2.5f, "+", "12.5"), // long + float arguments(10L, 2.5d, "+", "12.5"), // long + double arguments(10L, 2L, "+", "12"), // long + long // Subtraction (-) arguments(10, 2, "-", "8"), // int - int arguments(10, "2", "-", "8"), // int - string arguments(10, 2.5f, "-", "7.5"), // int - float arguments(10, 2.5d, "-", "7.5"), // int - double arguments(10, 2L, "-", "8"), // int - long arguments("10", 2, "-", "8"), // string - int arguments("10", "2", "-", "8"), // string - string arguments("10", 2.5f, "-", "7.5"), // string - float arguments("10", 2.5d, "-", "7.5"), // string - double arguments("10", 2L, "-", "8"), // string - long arguments(10.5f, 2, "-", "8.5"), // float - int arguments(10.5f, "2", "-", "8.5"), // float - string arguments(10.5f, 2.5f, "-", "8.0"), // float - float arguments(10.5f, 2.5d, "-", "8.0"), // float - double arguments(10.5f, 2L, "-", "8.5"), // float - long arguments(10.5d, 2, "-", "8.5"), // double - int arguments(10.5d, "2", "-", "8.5"), // double - string arguments(10.5d, 2.5f, "-", "8.0"), // double - float arguments(10.5d, 2.5d, "-", "8.0"), // double - double arguments(10.5d, 2L, "-", "8.5"), // double - long arguments(10L, 2, "-", "8"), // long - int arguments(10L, "2", "-", "8"), // long - string arguments(10L, 2.5f, "-", "7.5"), // long - float arguments(10L, 2.5d, "-", "7.5"), // long - double arguments(10L, 2L, "-", "8"), // long - long // Multiplication (*) arguments(10, 2, "*", "20"), // int * int arguments(10, "2", "*", "20"), // int * string arguments(10, 2.5f, "*", "25.0"), // int * float arguments(10, 2.5d, "*", "25.0"), // int * double arguments(10, 2L, "*", "20"), // int * long arguments("10", 2, "*", "20"), // string * int arguments("10", "2", "*", "20"), // string * string arguments("10", 2.5f, "*", "25.0"), // string * float arguments("10", 2.5d, "*", "25.0"), // string * double arguments("10", 2L, "*", "20"), // string * long arguments(10.5f, 2, "*", "21.0"), // float * int arguments(10.5f, "2", "*", "21.0"), // float * string arguments(10.5f, 2.5f, "*", "26.25"), // float * float arguments(10.5f, 2.5d, "*", "26.25"), // float * double arguments(10.5f, 2L, "*", "21.0"), // float * long arguments(10.5d, 2, "*", "21.0"), // double * int arguments(10.5d, "2", "*", "21.0"), // double * string arguments(10.5d, 2.5f, "*", "26.25"), // double * float arguments(10.5d, 2.5d, "*", "26.25"), // double * double arguments(10.5d, 2L, "*", "21.0"), // double * long arguments(10L, 2, "*", "20"), // long * int arguments(10L, "2", "*", "20"), // long * string arguments(10L, 2.5f, "*", "25.0"), // long * float arguments(10L, 2.5d, "*", "25.0"), // long * double arguments(10L, 2L, "*", "20"), // long * long // Division (/) arguments(10, 2, "/", "5"), // int / int arguments(10, "2", "/", "5"), // int / string arguments(10, 2.5f, "/", "4.0"), // int / float arguments(10, 2.5d, "/", "4.0"), // int / double arguments(10, 2L, "/", "5"), // int / long arguments("10", 2, "/", "5"), // string / int arguments("10", "2", "/", "5"), // string / string arguments("10", 2.5f, "/", "4.0"), // string / float arguments("10", 2.5d, "/", "4.0"), // string / double arguments("10", 2L, "/", "5"), // string / long arguments(10.5f, 2, "/", "5.25"), // float / int arguments(10.5f, "2", "/", "5.25"), // float / string arguments(10.5f, 2.5f, "/", "4.2"), // float / float arguments(10.5f, 2.5d, "/", "4.2"), // float / double arguments(10.5f, 2L, "/", "5.25"), // float / long arguments(10.5d, 2, "/", "5.25"), // double / int arguments(10.5d, "2", "/", "5.25"), // double / string arguments(10.5d, 2.5f, "/", "4.2"), // double / float arguments(10.5d, 2.5d, "/", "4.2"), // double / double arguments(10.5d, 2L, "/", "5.25"), // double / long arguments(10L, 2, "/", "5"), // long / int arguments(10L, "2", "/", "5"), // long / string arguments(10L, 2.5f, "/", "4.0"), // long / float arguments(10L, 2.5d, "/", "4.0"), // long / double arguments(10L, 2L, "/", "5"), // long / long // Modulo (%) arguments(10, 2, "%", "0"), // int % int arguments(10, "2", "%", "0"), // int % string arguments(10, 2.5f, "%", "0.0"), // int % float arguments(10, 2.5d, "%", "0.0"), // int % double arguments(10, 2L, "%", "0"), // int % long arguments("10", 2, "%", "0"), // string % int arguments("10", "2", "%", "0"), // string % string arguments("10", 2.5f, "%", "0.0"), // string % float arguments("10", 2.5d, "%", "0.0"), // string % double arguments("10", 2L, "%", "0"), // string % long arguments(10.5f, 2, "%", "0.5"), // float % int arguments(10.5f, "2", "%", "0.5"), // float % string arguments(10.5f, 2.5f, "%", "0.5"), // float % float arguments(10.5f, 2.5d, "%", "0.5"), // float % double arguments(10.5f, 2L, "%", "0.5"), // float % long arguments(10.5d, 2, "%", "0.5"), // double % int arguments(10.5d, "2", "%", "0.5"), // double % string arguments(10.5d, 2.5f, "%", "0.5"), // double % float arguments(10.5d, 2.5d, "%", "0.5"), // double % double arguments(10.5d, 2L, "%", "0.5"), // double % long arguments(10L, 2, "%", "0"), // long % int arguments(10L, "2", "%", "0"), // long % string arguments(10L, 2.5f, "%", "0.0"), // long % float arguments(10L, 2.5d, "%", "0.0"), // long % double arguments(10L, 2L, "%", "0") // long % long ); } /** * Problem existed where getAttribute would return an Object type which was an invalid operand for * java's algebraic operators. */ @Test void testUnaryOperatorOnAttribute() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if -5 > -item.changeInt %}yes{% else %}no{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("item", new Item()); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("no", writer.toString()); } @Test void testNotUnaryOperatorOnAttribute() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if not(item.truthy) %}yes{% else %}no{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("item", new Item()); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("no", writer.toString()); } @Test void testLogicOperatorOnAttributes() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if item.truthy and item.falsy %}yes{% else %}no{% endif %}" + "{% if item.truthy or item.falsy %}yes{% else %}no{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("item", new Item()); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("noyes", writer.toString()); } @Test void testLogicOperatorsWithNullValues() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if a %}yes{% else %}no{% endif %}" + "{% if b %}yes{% else %}no{% endif %}" + "{% if c %}yes{% else %}no{% endif %}" + "{% if d %}yes{% else %}no{% endif %}" + "{% if true and a %}yes{% else %}no{% endif %}" + "{% if true and b %}yes{% else %}no{% endif %}" + "{% if true and c %}yes{% else %}no{% endif %}" + "{% if true and d %}yes{% else %}no{% endif %}" + "{% if a and true %}yes{% else %}no{% endif %}" + "{% if b and true %}yes{% else %}no{% endif %}" + "{% if c and true %}yes{% else %}no{% endif %}" + "{% if d and true %}yes{% else %}no{% endif %}" + "{% if false or a %}yes{% else %}no{% endif %}" + "{% if false or b %}yes{% else %}no{% endif %}" + "{% if false or c %}yes{% else %}no{% endif %}" + "{% if false or d %}yes{% else %}no{% endif %}" + "{% if a or false %}yes{% else %}no{% endif %}" + "{% if b or false %}yes{% else %}no{% endif %}" + "{% if c or false %}yes{% else %}no{% endif %}" + "{% if d or false %}yes{% else %}no{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("b", null); context.put("c", false); context.put("d", true); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("nononoyes" + "nononoyes" + "nononoyes" + "nononoyes" + "nononoyes", writer.toString()); } @Test void testLogicOperatorsWithNullValuesWithStrictVariables() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(true).build(); String andSource = "{% if a and b %}yes{% else %}no{% endif %}"; String orSource = "{% if a or b %}yes{% else %}no{% endif %}"; PebbleTemplate andTemplate = pebble.getTemplate(andSource); PebbleTemplate orTemplate = pebble.getTemplate(orSource); Map context = new HashMap<>(); Writer writer; // values not set at all yet try { writer = new StringWriter(); andTemplate.evaluate(writer, context); fail("Exception not thrown"); } catch (PebbleException e) { } try { writer = new StringWriter(); orTemplate.evaluate(writer, context); fail("Exception not thrown"); } catch (PebbleException e) { } context.put("a", null); context.put("b", null); try { writer = new StringWriter(); andTemplate.evaluate(writer, context); fail("Exception not thrown"); } catch (PebbleException e) { } try { writer = new StringWriter(); orTemplate.evaluate(writer, context); fail("Exception not thrown"); } catch (PebbleException e) { } context.put("a", null); context.put("b", false); try { writer = new StringWriter(); andTemplate.evaluate(writer, context); fail("Exception not thrown"); } catch (PebbleException e) { } try { writer = new StringWriter(); orTemplate.evaluate(writer, context); fail("Exception not thrown"); } catch (PebbleException e) { } context.put("a", false); context.put("b", null); writer = new StringWriter(); andTemplate.evaluate(writer, context); assertEquals("no", writer.toString()); try { writer = new StringWriter(); orTemplate.evaluate(writer, context); fail("Exception not thrown"); } catch (PebbleException e) { } context.put("a", true); context.put("b", false); writer = new StringWriter(); andTemplate.evaluate(writer, context); assertEquals("no", writer.toString()); writer = new StringWriter(); orTemplate.evaluate(writer, context); assertEquals("yes", writer.toString()); } @Test void testNotOperatorPrecedence() throws IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if not item.falsy and not item.truthy %}This should not be displayed{% else %}All's good{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("item", new Item()); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("All's good", writer.toString()); } @Test void testNotOperatorWithParenthesisPrecedence() throws IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if (not item.falsy) and (not item.truthy) %}This should not be displayed{% else %}All's good{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("item", new Item()); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("All's good", writer.toString()); } @Test void testTernary() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ true ? 1 : 2 }}-{{ 1 + 4 == 5 ?(2-1) : 2 }}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("1-1", writer.toString()); } @Test void testComparisons() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if 3 > 2 %}yes{% endif %}" + "{% if 2 > 3 %}no{% endif %}" + "{% if 2 > 2 %}no{% endif %}" + "{% if 2 < 3 %}yes{% endif %}" + "{% if 3 < 2 %}no{% endif %}" + "{% if 2 < 2 %}no{% endif %}" + "{% if 3 >= 3 %}yes{% endif %}" + "{% if 3 >= 2 %}yes{% endif %}" + "{% if 2 >= 3 %}no{% endif %}" + "{% if 3 <= 3 %}yes{% endif %}" + "{% if 3 <= 2 %}no{% endif %}" + "{% if 2 <= 3 %}yes{% endif %}" + "{% if 100 <= 100 %}yes{% endif %}" + "{% if 2 == 2 %}yes{% endif %}" + "{% if 2 == 3 %}no{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("yesyesyesyesyesyesyesyes", writer.toString()); } @Test void testComparisonsOnDifferingOperands() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if 3 > 2.0 %}yes{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("yes", writer.toString()); } @Test() void testEqualsOperator() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if 'test' equals obj2 %}yes{% endif %}{% if 'blue' equals 'red' %}no{% else %}yes{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("obj2", "test"); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("yesyes", writer.toString()); } @Test() void testEqualsOperatorWithNulls() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if null equals null %}yes{% endif %}{% if null equals obj %}yes{% else %}no{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("obj", null); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("yesyes", writer.toString()); } @Test() void testNotEqualsOperator() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if 'Mitchell' != name %}no{% else %}yes{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("name", "Mitchell"); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("yes", writer.toString()); } @Test() void testEqualsOperatorWithPrimitives() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if 1 equals 1 %}yes{% endif %}{% if 3 equals item.changeInt %}yes{% else %}no{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("item", new Item()); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("yesyes", writer.toString()); } /** * There was an bug where two Number objects (Integer, Double etc.) were compared for equality * using ==. This was fixed to use equals(). * * @see https://github.com/mbosecke/pebble/issues/46 */ @SuppressWarnings({"unchecked", "rawtypes"}) @Test() void testEqualsOperatorWithNumberObjects() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if (v == 1) %}num1{% elseif (v == 999999) %}num999999{% else %}no{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); // Test Integer, Long, Float and Double. List> tests = new ArrayList<>(); tests.add(new Pair(1, "num1")); tests.add(new Pair(999999, "num999999")); tests.add(new Pair(1l, "num1")); tests.add(new Pair(999999l, "num999999")); tests.add(new Pair(1f, "num1")); tests.add(new Pair(999999f, "num999999")); tests.add(new Pair(1d, "num1")); tests.add(new Pair(999999d, "num999999")); for (Pair test : tests) { Map context = new HashMap<>(); context.put("v", test.getLeft()); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals(test.getRight(), writer.toString()); } } /** * There was an issue where if one of the comparison operands came from a variable object, the * template could not be compiled. This is because the getAttribute() method of the * AbstractPebbleTemplate returns Objects and Objects can not be compared to primitives. */ @Test() void testComparisonWithAttributeOperand() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if item.change < 2.0 %}yes{% else %}no{% endif %}" + "{% if item.change <= 2.0 %}yes{% else %}no{% endif %}" + "{% if item.change > 2.0 %}yes{% else %}no{% endif %}" + "{% if item.change >= 2.0 %}yes{% else %}no{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("item", new Item()); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("yesyesnono", writer.toString()); } @Test() void testComparisonBigDecimal() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if number1 > number2 %}yes{% endif %}" + "{% if number2 > number1 %}no{% endif %}" + "{% if number2 > number2 %}no{% endif %}" + "{% if number2 < number1 %}yes{% endif %}" + "{% if number1 < number2 %}no{% endif %}" + "{% if number2 < number2 %}no{% endif %}" + "{% if number1 >= number1 %}yes{% endif %}" + "{% if number1 >= number2 %}yes{% endif %}" + "{% if number2 >= number1 %}no{% endif %}" + "{% if number1 <= number1 %}yes{% endif %}" + "{% if number1 <= number2 %}no{% endif %}" + "{% if number2 <= number1 %}yes{% endif %}" + "{% if number2 <= number2 %}yes{% endif %}" + "{% if number2 == number2 %}yes{% endif %}" + "{% if number2 == number1 %}no{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("number1", BigDecimal.valueOf(3d)); context.put("number2", BigDecimal.valueOf(2d)); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("yesyesyesyesyesyesyesyes", writer.toString()); } @Test() void testComparisonWithNull() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if number1 > number2 %}yes{% else %}no{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("number1", null); context.put("number2", BigDecimal.valueOf(2d)); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("no", writer.toString()); } @Test() void testComparisonWithNull2() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if number1 > number2 %}yes{% else %}no{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("number1", BigDecimal.valueOf(3d)); context.put("number2", null); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("no", writer.toString()); } @Test() void testComparisonBigDecimalWithDouble() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if number1 > number2 %}yes{% endif %}" + "{% if number2 > number1 %}no{% endif %}" + "{% if number2 > number2 %}no{% endif %}" + "{% if number2 < number1 %}yes{% endif %}" + "{% if number1 < number2 %}no{% endif %}" + "{% if number2 < number2 %}no{% endif %}" + "{% if number1 >= number1 %}yes{% endif %}" + "{% if number1 >= number2 %}yes{% endif %}" + "{% if number2 >= number1 %}no{% endif %}" + "{% if number1 <= number1 %}yes{% endif %}" + "{% if number1 <= number2 %}no{% endif %}" + "{% if number2 <= number1 %}yes{% endif %}" + "{% if number2 <= number2 %}yes{% endif %}" + "{% if number2 == number2 %}yes{% endif %}" + "{% if number2 == number1 %}no{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("number1", BigDecimal.valueOf(3d)); context.put("number2", 2d); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("yesyesyesyesyesyesyesyes", writer.toString()); } public class Item { public double change = 1.234; public Integer changeInt = 3; public boolean truthy = true; public Boolean falsy = false; } @Test() void testIsOperatorPrecedence() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if 1 + 2 is odd %} true {% else %} false {% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals(" true ", writer.toString()); } @Test() void testIsOperatorPrecedenceWithAnd() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if 3 is odd and 5 is odd %} true {% else %} false {% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals(" true ", writer.toString()); } @Test void testContainsOperator() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if names contains 'John' %}yes{% else %}no{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("names", Arrays.asList("Bob", "Maria", "John")); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("yes", writer.toString()); } @Test void testContainsOperatorWithNull() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if null contains 'John' %}yes{% else %}no{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("no", writer.toString()); } @SuppressWarnings("serial") @Test void testContainsOperator2() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if names contains 'Maria' %}yes{% else %}no{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("names", new HashMap() { { this.put("Bob", "Bob"); this.put("Maria", "Maria"); this.put("John", "John"); } }); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("yes", writer.toString()); } @Test void testContainsOperator4() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if names contains 'Cobra' %}yes{% else %}no{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("names", Arrays.asList("Bob", "Maria", "John")); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("no", writer.toString()); } @Test void testContainsOperator5() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if names contains 'Cobra' %}yes{% else %}no{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("names", "Bob Maria John"); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("no", writer.toString()); } @Test void testContainsOperatorWithAnd() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if names contains 'Bob' and names contains 'Maria' %}yes{% else %}no{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("names", Arrays.asList("Bob", "Maria", "John")); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("yes", writer.toString()); } @Test void testContainsOperatorWithOr() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if names contains 'John' or names contains 'Cobra' %}yes{% else %}no{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("names", Arrays.asList("Bob", "Maria", "John")); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("yes", writer.toString()); } @Test void testContainsOperatorWithNot() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if not (names contains 'Cobra') %}yes{% else %}no{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("names", Arrays.asList("Bob", "Maria", "John")); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("yes", writer.toString()); } @Test void testContainsWithArrays() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if values contains value %}yes{% else %}no{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); Writer writer; // Objects writer = new StringWriter(); context.put("values", new String[]{"Bob", "Marley"}); context.put("value", "Bob"); template.evaluate(writer, context); assertEquals("yes", writer.toString()); // boolean writer = new StringWriter(); context.put("values", new boolean[]{true, false}); context.put("value", Boolean.TRUE); template.evaluate(writer, context); assertEquals("yes", writer.toString()); // byte writer = new StringWriter(); context.put("values", new byte[]{1, 2}); context.put("value", (byte) 1); template.evaluate(writer, context); assertEquals("yes", writer.toString()); // char writer = new StringWriter(); context.put("values", new char[]{'a', 'b'}); context.put("value", 'a'); template.evaluate(writer, context); assertEquals("yes", writer.toString()); // double writer = new StringWriter(); context.put("values", new double[]{1.0d, 2.0d}); context.put("value", 1.0d); template.evaluate(writer, context); assertEquals("yes", writer.toString()); // float writer = new StringWriter(); context.put("values", new float[]{1.0f, 2.0f}); context.put("value", 1.0f); template.evaluate(writer, context); assertEquals("yes", writer.toString()); // int writer = new StringWriter(); context.put("values", new int[]{1, 2}); context.put("value", 1); template.evaluate(writer, context); assertEquals("yes", writer.toString()); // long writer = new StringWriter(); context.put("values", new long[]{1, 2}); context.put("value", 1L); template.evaluate(writer, context); assertEquals("yes", writer.toString()); // short writer = new StringWriter(); context.put("values", new short[]{1, 2}); context.put("value", (short) 1); template.evaluate(writer, context); assertEquals("yes", writer.toString()); } /** * Tests if the string concatenation is working. */ @Test void testStringConcatenation() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ name1 ~ name2 ~ name3 | lower }}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("name1", "Bob"); context.put("name2", "Maria"); context.put("name3", "John"); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("bobmariajohn", writer.toString()); } /** * Tests if the macro output SafeString concatenation is working. */ @Test void testMacroSafeStringConcatenation() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% macro macro1() %}Bob{% endmacro %}\n" + "{% macro macro2() %}Maria{% endmacro %}\n" + "{% macro macro3() %}John{% endmacro %}\n" + "{{ (macro1() + macro2() + macro3()) | lower }}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("bobmariajohn", writer.toString()); } @Test void testListSizeEmpty() throws IOException { PebbleEngine pebble = new PebbleEngine.Builder() .loader(new StringLoader()) .strictVariables(false) .build(); String source = "{% if dirEntries.size > 0 %}\n" + "

There are available files.

\n" + "{% else %}\n" + "

There are no available files.

\n" + "{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("dirEntries", new ArrayList<>()); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("

There are no available files.

\n", writer.toString()); } @Test void testListSize() throws IOException { PebbleEngine pebble = new PebbleEngine.Builder() .loader(new StringLoader()) .strictVariables(false) .build(); String source = "{% if dirEntries.size > 0 %}\n" + "

There are available files.

\n" + "{% else %}\n" + "

There are no available files.

\n" + "{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("dirEntries", Arrays.asList("Test")); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("

There are available files.

\n", writer.toString()); } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/MacroTest.java ================================================ package io.pebbletemplates.pebble; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.extension.InvocationCountingFunction; import io.pebbletemplates.pebble.extension.TestingExtension; import io.pebbletemplates.pebble.loader.StringLoader; import io.pebbletemplates.pebble.template.PebbleTemplate; import org.junit.jupiter.api.Test; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.util.HashMap; import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; /** * Created by mitch_000 on 2016-11-13. */ class MacroTest { private static final String LINE_SEPARATOR = System.lineSeparator(); @Test void testMacro() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("templates/template.macro1.peb"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals(" " + LINE_SEPARATOR, writer.toString()); } /** * This ensures that macro inheritance works properly even if it skips a generation. */ @Test void skipGenerationMacro() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("templates/template.skipGenerationMacro1.peb"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("success", writer.toString()); } @Test void testMacrosWithSameName() throws PebbleException, IOException { assertThrows(RuntimeException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate( "{{ test() }}{% macro test(one) %}ONE{% endmacro %}{% macro test(one,two) %}TWO{% endmacro %}"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals(" " + LINE_SEPARATOR, writer.toString()); }); } @Test void testMacroWithDefaultArgument() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate( "{{ input(name='country') }}{% macro input(type='text', name) %}{{ type }} {{ name }}{% endmacro %}"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("text country", writer.toString()); } /** * There was an issue where the second invokation of a macro did not have access to the original * arguments any more. */ @Test void testMacroInvokedTwice() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("templates/template.macroDouble.peb"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("onetwo", writer.toString()); } @Test void testFunctionInMacroInvokedTwice() throws PebbleException, IOException { TestingExtension extension = new TestingExtension(); PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false) .extension(extension).build(); PebbleTemplate template = pebble .getTemplate( "{{ test() }}{% macro test() %}{{ invocationCountingFunction() }}{% endmacro %}"); Writer writer = new StringWriter(); template.evaluate(writer); InvocationCountingFunction function = extension.getInvocationCountingFunction(); assertEquals(1, function.getInvocationCount()); } @Test void testMacroInvocationWithoutAllArguments() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble .getTemplate("{{ test('1') }}{% macro test(one,two) %}{{ one }}{{ two }}{% endmacro %}"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("1", writer.toString()); } /** * I was once writing macro output directly to writer which was preventing output from being * filtered. I have fixed this now. */ @Test void testMacroBeingFiltered() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("templates/template.macro3.peb"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("HELLO" + LINE_SEPARATOR, writer.toString()); } @Test void testImportFile() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("templates/template.macro2.peb"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals(" " + LINE_SEPARATOR, writer.toString()); } @Test void testImportInChildTemplateOutsideOfBlock() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("templates/template.macro.child.peb"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals(" " + LINE_SEPARATOR, writer.toString()); } @Test void testDynamicImport() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("templates/template.import.dynamic.peb"); Map context = new HashMap<>(); context.put("modern", false); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("classic macro" + LINE_SEPARATOR, writer.toString()); context.put("modern", true); writer = new StringWriter(); template.evaluate(writer, context); assertEquals("ajax macro" + LINE_SEPARATOR, writer.toString()); } @Test void testSetVariableInsideMacro() throws IOException { PebbleEngine pebble = new PebbleEngine.Builder().strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("templates/macros/setVariableBase.peb"); Map context = new HashMap<>(); context.put("unit", "tank"); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("tankinfantrytank", writer.toString()); } @Test void testMacroHasAccessToGlobalVariables() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String templateContent = "{% set foo = 'bar' %}{{ test(_context) }}{% macro test(_context) %}{% set foo = 'foo' %}{{ _context.foo }}{{ foo }}{% endmacro %}"; PebbleTemplate template = pebble.getTemplate(templateContent); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("barfoo", writer.toString()); } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/MapSyntaxTest.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble; import io.pebbletemplates.pebble.error.ParserException; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.extension.TestingExtension; import io.pebbletemplates.pebble.loader.StringLoader; import io.pebbletemplates.pebble.template.PebbleTemplate; import org.junit.jupiter.api.Test; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.util.HashMap; import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; class MapSyntaxTest { @Test void testMapSyntax() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ {} }}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("{}", writer.toString()); } @Test void testSimpleMap() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ {'key':'value'} }}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("{key=value}", writer.toString()); } @Test void test2ElementMap() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .extension(new TestingExtension()) .strictVariables(false).build(); String source = "{{ {'key1':'value1','key2':'value2'} | mapToString }}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("{key1=value1, key2=value2}", writer.toString()); } @Test void test2ElementMap2() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .extension(new TestingExtension()) .strictVariables(false).build(); String source = "{{ {'key1' : 'value1' , 'key2' : 'value2' } | mapToString }}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("{key1=value1, key2=value2}", writer.toString()); } @Test void testNElementMap() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .extension(new TestingExtension()) .strictVariables(false).build(); String source = "{{ {'key1':'value1','key2':'value2','key3':'value3','key4':'value4','key5':'value5'} | mapToString }}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("{key1=value1, key2=value2, key3=value3, key4=value4, key5=value5}", writer.toString()); } @Test void testIncompleteMapSyntax() throws PebbleException { assertThrows(ParserException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ {,} }}"; pebble.getTemplate(source); }); } @Test void testIncompleteMapSyntax2() throws PebbleException { assertThrows(ParserException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ {'key'} }}"; pebble.getTemplate(source); }); } @Test void testIncompleteMapSyntax3() throws PebbleException { assertThrows(ParserException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ {'key':} }}"; pebble.getTemplate(source); }); } @Test void testIncompleteMapSyntax4() throws PebbleException { assertThrows(ParserException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ {:'value'} }}"; pebble.getTemplate(source); }); } @Test void testIncompleteMapSyntax5() throws PebbleException { assertThrows(ParserException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ {'key':'value',} }}"; pebble.getTemplate(source); }); } @SuppressWarnings("serial") @Test void testMapWithExpressions() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .extension(new TestingExtension()) .strictVariables(false).build(); String source = "{{ {1:'one', 'two':2, three:'three', numbers['four']:4} | mapToString }}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("three", "3"); context.put("numbers", new HashMap() { { this.put("four", "4"); } }); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("{1=one, 3=three, 4=4, two=2}", writer.toString()); } @SuppressWarnings({"serial", "unused"}) @Test void testMapWithComplexExpressions() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .extension(new TestingExtension()) .strictVariables(false).build(); String source = "{{ {'one' + 'plus':'oneplus', 2 - 1:3, three.number:(2+1), 0:numbers['four'][0], numbers ['five'] .value:'five'} | mapToString }}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("three", new Object() { public Integer number = 3; }); context.put("numbers", new HashMap() { { this.put("four", new String[]{"4"}); this.put("five", new Object() { private String value = "five"; public String getValue() { return this.value; } }); } }); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("{0=4, 1=3, 3=3, five=five, oneplus=oneplus}", writer.toString()); } @Test void testSetCommand() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% set map = {'key'+1:'value'+'1'} %}{{ map }}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("{key1=value1}", writer.toString()); } // this tests use string 'contains' semantics because entry order can't be // trusted @Test void testForTag() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% set names = {'Bob':'Marley','Maria':'Callas','John':'Cobra'} %}{% for name in names %}{{ name.key + '-' + name.value }}{% endfor %}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); String result = writer.toString(); assertTrue(result.indexOf("Bob-Marley") > -1); assertTrue(result.indexOf("Maria-Callas") > -1); assertTrue(result.indexOf("John-Cobra") > -1); } @Test void testForTag2() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% for name in {'Bob':'Marley','Maria':'Callas','John':'Cobra'} %}{{ name.key + '-' + name.value }}{% endfor %}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); String result = writer.toString(); assertTrue(result.indexOf("Bob-Marley") > -1); assertTrue(result.indexOf("Maria-Callas") > -1); assertTrue(result.indexOf("John-Cobra") > -1); } @Test void testForElseTag() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% for name in {} %}{{ name }}{% else %}{{ 'no name' }}{% endfor %}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("no name", writer.toString()); } @Test void testIfTag() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if {'Bob':'Marley','Maria':'Callas','John':'Cobra'} is null %}{{ 'it is' }}{% else %}{{ 'it is not' }}{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("it is not", writer.toString()); } @Test void testMacroTag() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% macro print(name) %}{{ name }}{% endmacro %}{{ print({'Bob':'Marley'}) }}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("{Bob=Marley}", writer.toString()); } @Test void testMacroTagNamedArguments() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% macro print(name) %}{{ name }}{% endmacro %}{{ print(name={'Bob':'Marley'}) }}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("{Bob=Marley}", writer.toString()); } // no operator overloading for maps @Test void testAdditionOverloading() throws PebbleException, IOException { assertThrows(PebbleException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% set map = {'Bob':'Marley'} + 1 %}{{ map }}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); }); } @Test void testSubtractionOverloading() throws PebbleException, IOException { assertThrows(PebbleException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% set map = {'Bob':'Marley'} - 1 %}{{ map }}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); }); } @Test void testEmptyTest() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if {'John':'Cobra'} is empty %}{{ 'true' }}{% else %}{{ 'false' }}{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("false", writer.toString()); } @Test void testEmptyTest2() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if {} is empty %}{{ 'true' }}{% else %}{{ 'false' }}{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("true", writer.toString()); } @Test void testIterableTest() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if {} is iterable %}true{% else %}false{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("false", writer.toString()); } @Test void testContainsOperator() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if {'Bob':'Marley','Maria':'Callas','John':'Cobra'} contains 'Maria' %}true{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("true", writer.toString()); } @Test void testContainsOperator2() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if not ( {'Bob':'Marley','Maria':'Callas','John':'Cobra'} contains 'Freddie') %}true{% else %}false{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("true", writer.toString()); } @Test void testContainsOperator3() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% if {'Bob':'Marley','Maria':'Callas','John':'Cobra'} contains 'John' and not ({'Freddie':'Mercury'} contains 'Bob') %}true{% else %}false{% endif %}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("true", writer.toString()); } @Test void testNestedMaps() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ { 1 : {}, 2 : { 1 : 1 }, { 3 : 3} : 3, 4 : { 4 : { 4 : 4 } } } }}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("{{3=3}=3, 1={}, 2={1=1}, 4={4={4=4}}}", writer.toString()); } @Test void testNestedArrayInMap() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ {'array':[]} }}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("{array=[]}", writer.toString()); } // brace syntax regression tests @Test void testBraceSyntax() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% set var = true %}{{ 'hi' }}{# comment #}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("hi", writer.toString()); } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/MaxRenderedSizeTest.java ================================================ package io.pebbletemplates.pebble; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.template.PebbleTemplate; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import java.io.StringWriter; import java.io.Writer; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; class MaxRenderedSizeTest { @Test void renderingExplodingMacroWithLimitWillThrowPebbleException() { PebbleEngine pebble = new PebbleEngine.Builder() .maxRenderedSize(1000) .build(); PebbleTemplate template = pebble.getTemplate("templates/template.macro.exploding.peb"); Writer writer = new StringWriter(); PebbleException thrown = assertThrows(PebbleException.class, () -> template.evaluate(writer)); String result = writer.toString(); // We didn't write more than allowed. assertTrue(result.length() <= 1000); assertTrue(thrown.getMessage().contains("1000")); } @Test @Disabled("This test passes but takes about a minute to do so. Creating a faster macro bomb would be nice.") void renderingExplodingMacroWithoutLimitWillThrowOOMException() { PebbleEngine pebble = new PebbleEngine.Builder() .build(); PebbleTemplate template = pebble.getTemplate("templates/template.macro.exploding.peb"); StringWriter writer = new StringWriter(); assertThrows(OutOfMemoryError.class, () -> template.evaluate(writer)); } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/MethodAccessTemplateTest.java ================================================ package io.pebbletemplates.pebble; import static org.junit.jupiter.api.Assertions.assertThrows; import io.pebbletemplates.pebble.error.ClassAccessException; import io.pebbletemplates.pebble.loader.StringLoader; import io.pebbletemplates.pebble.attributes.methodaccess.NoOpMethodAccessValidator; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.function.Executable; class MethodAccessTemplateTest { @Nested class ClassTest { @Test void testIfAccessIsForbiddenWhenAllowUnsafeMethodsIsFalse() { PebbleEngine pebble = MethodAccessTemplateTest.this.pebbleEngine(); assertThrows(ClassAccessException.class, this.templateEvaluation(pebble)); } @Test void testIfAccessIsAllowedWhenAllowUnsafeMethodsIsTrue() throws Throwable { PebbleEngine pebble = MethodAccessTemplateTest.this.unsafePebbleEngine(); this.templateEvaluation(pebble).execute(); } private Executable templateEvaluation(PebbleEngine pebble) { return () -> { String source = "{{clazz.getPackage()}}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("clazz", Object.class); MethodAccessTemplateTest.this.evaluateTemplate(template, context); }; } } @Nested class RuntimeTest { @Test void testIfAccessIsForbiddenWhenAllowUnsafeMethodsIsFalse() { PebbleEngine pebble = MethodAccessTemplateTest.this.pebbleEngine(); assertThrows(ClassAccessException.class, this.templateEvaluation(pebble)); } @Test void testIfAccessIsAllowedWhenAllowUnsafeMethodsIsTrue() throws Throwable { PebbleEngine pebble = MethodAccessTemplateTest.this.unsafePebbleEngine(); this.templateEvaluation(pebble).execute(); } private Executable templateEvaluation(PebbleEngine pebble) { return () -> { String source = "{{runtime.availableProcessors()}}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("runtime", Runtime.getRuntime()); MethodAccessTemplateTest.this.evaluateTemplate(template, context); }; } } @Nested class ThreadTest { @Test void testIfAccessIsForbiddenWhenAllowUnsafeMethodsIsFalse() { PebbleEngine pebble = MethodAccessTemplateTest.this.pebbleEngine(); assertThrows(ClassAccessException.class, this.templateEvaluation(pebble)); } @Test void testIfAccessIsAllowedWhenAllowUnsafeMethodsIsTrue() throws Throwable { PebbleEngine pebble = MethodAccessTemplateTest.this.unsafePebbleEngine(); this.templateEvaluation(pebble).execute(); } private Executable templateEvaluation(PebbleEngine pebble) { return () -> { String source = "{{thread.sleep(500)}}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("thread", new Thread()); MethodAccessTemplateTest.this.evaluateTemplate(template, context); }; } } @Nested class MethodTest { @Test void testIfAccessIsForbiddenWhenAllowUnsafeMethodsIsFalse() { PebbleEngine pebble = MethodAccessTemplateTest.this.pebbleEngine(); assertThrows(ClassAccessException.class, this.templateEvaluation(pebble)); } @Test void testIfAccessIsAllowedWhenAllowUnsafeMethodsIsTrue() throws Throwable { PebbleEngine pebble = MethodAccessTemplateTest.this.unsafePebbleEngine(); this.templateEvaluation(pebble).execute(); } private Executable templateEvaluation(PebbleEngine pebble) { return () -> { Class systemClass = Class.forName("java.lang.System"); Method gcMethod = systemClass.getMethod("gc"); gcMethod.setAccessible(true); gcMethod.invoke(null); String source = "{{gc.invoke(null, null)}}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("gc", gcMethod); MethodAccessTemplateTest.this.evaluateTemplate(template, context); }; } } private PebbleEngine unsafePebbleEngine() { return new PebbleEngine.Builder().loader(new StringLoader()) .methodAccessValidator(new NoOpMethodAccessValidator()) .build(); } private PebbleEngine pebbleEngine() { return new PebbleEngine.Builder().loader(new StringLoader()).build(); } private void evaluateTemplate(PebbleTemplate template, Map context) throws IOException { Writer writer = new StringWriter(); template.evaluate(writer, context); } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/NewlineTrimmingTest.java ================================================ /* * This file is part of Pebble. *

* Copyright (c) 2014 by Mitchell Bösecke *

* For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.loader.StringLoader; import io.pebbletemplates.pebble.template.PebbleTemplate; import org.junit.jupiter.api.Test; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.util.HashMap; import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; class NewlineTrimmingTest { @Test void testPrintDefault() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .build(); PebbleTemplate template = pebble.getTemplate("{{param1}}\n{{param2}}"); Writer writer = new StringWriter(); Map params = new HashMap<>(); params.put("param1", "val1"); params.put("param2", "val2"); template.evaluate(writer, params); assertEquals("val1val2", writer.toString()); } @Test void testPrintForceToTrue() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .newLineTrimming(true) .build(); PebbleTemplate template = pebble.getTemplate("{{param1}}\n{{param2}}"); Writer writer = new StringWriter(); Map params = new HashMap<>(); params.put("param1", "val1"); params.put("param2", "val2"); template.evaluate(writer, params); assertEquals("val1val2", writer.toString()); } /** * Given that Newline Trimming is disabled, * a template that contains one newline character with text on each line * should output one newline character. */ @Test void testNewLineIncludedWhen_NewLineTrimmingIsFalse() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .newLineTrimming(false) .build(); PebbleTemplate template = pebble.getTemplate("{{param1}}\n{{param2}}"); Writer writer = new StringWriter(); Map params = new HashMap<>(); params.put("param1", "val1"); params.put("param2", "val2"); template.evaluate(writer, params); assertEquals("val1\nval2", writer.toString()); } @Test void testPrintDefaultTwoNewlines() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .build(); PebbleTemplate template = pebble.getTemplate("{{param1}}\n\n{{param2}}"); Writer writer = new StringWriter(); Map params = new HashMap<>(); params.put("param1", "val1"); params.put("param2", "val2"); template.evaluate(writer, params); assertEquals("val1\nval2", writer.toString()); } /** * Given that Newline Trimming is disabled, * a template that contains one or more consecutive newline characters * should output one newline character. */ @Test void testOneNewLineWhen_NewLineTrimmingFalseAndConsecutiveNewLinesInTemplate() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .newLineTrimming(false) .build(); PebbleTemplate template = pebble.getTemplate("{{param1}}\n\n{{param2}}"); Writer writer = new StringWriter(); Map params = new HashMap<>(); params.put("param1", "val1"); params.put("param2", "val2"); template.evaluate(writer, params); assertEquals("val1\n\nval2", writer.toString()); } @Test void testCommentDefault() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .build(); PebbleTemplate template = pebble.getTemplate("{# comment1 #}\n{# comment2 #}"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("", writer.toString()); } @Test void testCommentForceToTrue() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .newLineTrimming(true) .build(); PebbleTemplate template = pebble.getTemplate("{# comment1 #}\n{# comment2 #}"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("", writer.toString()); } @Test void testCommentSetToFalse() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .newLineTrimming(false) .build(); PebbleTemplate template = pebble.getTemplate("{# comment1 #}\n{# comment2 #}"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("\n", writer.toString()); } @Test void testExecuteDefault() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .build(); PebbleTemplate template = pebble.getTemplate("{% if true %}\n{% endif %}"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("", writer.toString()); } @Test void testExecuteForceToTrue() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .newLineTrimming(true) .build(); PebbleTemplate template = pebble.getTemplate("{% if true %}\n{% endif %}"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("", writer.toString()); } @Test void testExecuteSetToFalse() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .newLineTrimming(false) .build(); PebbleTemplate template = pebble.getTemplate("{% if true %}\n{% endif %}"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("\n", writer.toString()); } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/Nl2brFilterTest.java ================================================ package io.pebbletemplates.pebble; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.extension.core.Nl2brFilter; import io.pebbletemplates.pebble.extension.escaper.SafeString; import io.pebbletemplates.pebble.loader.StringLoader; import io.pebbletemplates.pebble.template.PebbleTemplate; import org.junit.jupiter.api.Test; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; /** * Tests for nl2br filter */ class Nl2brFilterTest { private final Nl2brFilter filter = new Nl2brFilter(); private static String asString(Object o) { if (o == null) return null; if (o instanceof SafeString) return o.toString(); return String.valueOf(o); } @Test void testEmptyString() throws PebbleException { Object out = filter.apply("", null, null, null, 0); assertEquals("", asString(out)); } @Test void testNonStringInputThrows() { assertThrows(IllegalArgumentException.class, () -> filter.apply(42, null, null, null, 0)); } @Test void testLfOnly() throws PebbleException { Object out = filter.apply("A\nB\nC", null, null, null, 0); assertEquals("A
B
C", asString(out)); } @Test void testCrOnly() throws PebbleException { Object out = filter.apply("A\rB\rC", null, null, null, 0); assertEquals("A
B
C", asString(out)); } @Test void testCrLfOnly() throws PebbleException { Object out = filter.apply("A\r\nB\r\nC", null, null, null, 0); assertEquals("A
B
C", asString(out)); } @Test void testMixedNewlines() throws PebbleException { Object out = filter.apply("A\nB\rC\r\nD", null, null, null, 0); assertEquals("A
B
C
D", asString(out)); } @Test void testNoNewlineReturnsSafeString() throws PebbleException { String input = "NoNewlinesHere"; Object out = filter.apply(input, null, null, null, 0); assertEquals(input, asString(out)); } @Test void testNullInput() throws PebbleException { Object out = filter.apply(null, null, null, null, 0); assertNull(out); } @Test void testIntegrationWithEngine() throws IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()).build(); PebbleTemplate template = pebble.getTemplate("{{ txt | nl2br | raw }}"); Writer w = new StringWriter(); template.evaluate(w, java.util.Collections.singletonMap("txt", "Line1\nLine2\rLine3\r\nLine4")); assertEquals("Line1
Line2
Line3
Line4", w.toString()); } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/OverloadedMethodTest.java ================================================ /******************************************************************************* * This file is part of Pebble. *

* Copyright (c) 2014 by Mitchell Bösecke *

* For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. ******************************************************************************/ package io.pebbletemplates.pebble; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.loader.StringLoader; import io.pebbletemplates.pebble.template.PebbleTemplate; import org.junit.jupiter.api.Test; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.util.HashMap; import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; // These tests verify correct behavior when resolving overloaded methods in a model class. See issue #367 class OverloadedMethodTest { // Verify that an overloaded method will select the correct version of the method to call based on the input type //---------------------------------------------------------------------------------------------------------------------- public static class Model { public String testMethod(String input) { return "string input: " + input; } public String testMethod(Integer input) { return "Integer input: " + input; } public String testMethod(Long input) { return "Long input: " + input; } public String testMethod(Object input) { return "other input: " + input.getClass(); } public String testMethod2(String input1, String input2) { return "string-string inputs"; } public String testMethod2(Object input1, String input2) { return "object-string inputs"; } public String testMethod2(String input1, Object input2) { return "string-object inputs"; } public String testMethod2(Object input1, Object input2) { return "object-object inputs"; } } @Test void testWithLiteralString() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder() .loader(new StringLoader()) .strictVariables(false) .build(); String input = "{{ model.testMethod(\"one\") }}"; String expected = "string input: one"; Map context = new HashMap<>(); context.put("model", new Model()); PebbleTemplate template = pebble.getTemplate(input); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals(expected, writer.toString()); } @Test void testWithContextString() throws PebbleException, IOException { this.testWithModel(new Model(), "one", "string input: one"); } @Test void testWithInteger() throws PebbleException, IOException { this.testWithModel(new Model(), 1, "Integer input: 1"); } @Test void testWithLong() throws PebbleException, IOException { this.testWithModel(new Model(), 1L, "Long input: 1"); } @Test void testWithObject() throws PebbleException, IOException { this.testWithModel(new Model(), this, "other input: " + this.getClass().toString()); } @Test void testWithStringString() throws PebbleException, IOException { this.testModelWith2Inputs("", "", "string-string inputs"); } @Test void testWithStringObject() throws PebbleException, IOException { this.testModelWith2Inputs("", this, "string-object inputs"); } @Test void testWithObjectString() throws PebbleException, IOException { this.testModelWith2Inputs(this, "", "object-string inputs"); } @Test void testWithObjectObject() throws PebbleException, IOException { this.testModelWith2Inputs(this, this, "object-object inputs"); } // Verify that multiple overloaded methods can be called in the same template, and the member cache will not return the // wrong method and cause a ClassCastException. //---------------------------------------------------------------------------------------------------------------------- public static class Model2 { public String testMethod(BaseClass input) { return "BaseClass input"; } public String testMethod(ChildClass1 input) { return "ChildClass1 input"; } public String testMethod(ChildClass2 input) { return "ChildClass2 input"; } } public static class BaseClass { } public static class ChildClass1 { } public static class ChildClass2 { } @Test void testWithMultipleCalls() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder() .loader(new StringLoader()) .strictVariables(false) .build(); String input = "{{ model.testMethod(BaseClass) }}, {{ model.testMethod(ChildClass1) }}, {{ model.testMethod(ChildClass2) }}"; String expected = "BaseClass input, ChildClass1 input, ChildClass2 input"; Map context = new HashMap<>(); context.put("model", new Model2()); context.put("BaseClass", new BaseClass()); context.put("ChildClass1", new ChildClass1()); context.put("ChildClass2", new ChildClass2()); PebbleTemplate template = pebble.getTemplate(input); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals(expected, writer.toString()); } // Check that method resolution works with a deep class hierarchy //---------------------------------------------------------------------------------------------------------------------- public static class Model3 { public String testMethod(BaseClass input) { return "BaseClass input"; } public String testMethod(ChildClass1 input) { return "ChildClass1 input"; } public String testMethod(ChildClass2 input) { return "ChildClass2 input"; } public String testMethod(ChildClass3 input) { return "ChildClass3 input"; } public String testMethod(ChildClass4 input) { return "ChildClass4 input"; } public String testMethod(ChildClass5 input) { return "ChildClass5 input"; } } public static class ChildClass3 extends ChildClass2 { } public static class ChildClass4 extends ChildClass3 { } public static class ChildClass5 extends ChildClass4 { } @Test void testWithBaseClass() throws PebbleException, IOException { this.testWithModel(new Model3(), new BaseClass(), "BaseClass input"); } @Test void testWithChildClass1() throws PebbleException, IOException { this.testWithModel(new Model3(), new ChildClass1(), "ChildClass1 input"); } @Test void testWithChildClass2() throws PebbleException, IOException { this.testWithModel(new Model3(), new ChildClass2(), "ChildClass2 input"); } @Test void testWithChildClass3() throws PebbleException, IOException { this.testWithModel(new Model3(), new ChildClass3(), "ChildClass3 input"); } @Test void testWithChildClass4() throws PebbleException, IOException { this.testWithModel(new Model3(), new ChildClass4(), "ChildClass4 input"); } @Test void testWithChildClass5() throws PebbleException, IOException { this.testWithModel(new Model3(), new ChildClass5(), "ChildClass5 input"); } // test helpers //---------------------------------------------------------------------------------------------------------------------- private void testWithModel(Object model, Object modelInput, String expected) throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder() .loader(new StringLoader()) .strictVariables(false) .build(); String input = "{{ model.testMethod(input) }}"; Map context = new HashMap<>(); context.put("model", model); context.put("input", modelInput); PebbleTemplate template = pebble.getTemplate(input); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals(expected, writer.toString()); } private void testModelWith2Inputs(Object modelInput1, Object modelInput2, String expected) throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder() .loader(new StringLoader()) .strictVariables(false) .build(); String input = "{{ model.testMethod2(input1, input2) }}"; Map context = new HashMap<>(); context.put("model", new Model()); context.put("input1", modelInput1); context.put("input2", modelInput2); PebbleTemplate template = pebble.getTemplate(input); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals(expected, writer.toString()); } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/OverrideCoreExtensionTest.java ================================================ package io.pebbletemplates.pebble; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.extension.AbstractExtension; import io.pebbletemplates.pebble.extension.Filter; import io.pebbletemplates.pebble.extension.Function; import io.pebbletemplates.pebble.loader.StringLoader; import io.pebbletemplates.pebble.node.expression.UnaryExpression; import io.pebbletemplates.pebble.node.expression.BinaryExpression; import io.pebbletemplates.pebble.operator.Associativity; import io.pebbletemplates.pebble.operator.BinaryOperator; import io.pebbletemplates.pebble.operator.BinaryOperatorImpl; import io.pebbletemplates.pebble.operator.UnaryOperator; import io.pebbletemplates.pebble.operator.UnaryOperatorImpl; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.PebbleTemplate; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; import org.junit.jupiter.api.Test; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.util.HashMap; import java.util.List; import java.util.Map; import static java.util.Collections.singletonList; import static org.junit.jupiter.api.Assertions.assertEquals; class OverrideCoreExtensionTest { @Test void testOverrideCodeExtensionFunction() throws IOException { PebbleEngine pebble = new PebbleEngine.Builder() .loader(new StringLoader()) .extension(new TestExtension()) .build(); PebbleTemplate template = pebble.getTemplate("{{i18n()}}"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("custom i18n function", writer.toString()); } @Test void testOverrideCodeExtensionFilter() throws IOException { PebbleEngine pebble = new PebbleEngine.Builder() .loader(new StringLoader()) .extension(new TestExtension()) .build(); PebbleTemplate template = pebble.getTemplate("{{ null | date }}"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("custom date filter", writer.toString()); } @Test void testOverrideCoreExtensionUnaryOperator() throws IOException { PebbleEngine pebble = new PebbleEngine.Builder() .loader(new StringLoader()) .extension(new TestExtension()) .allowOverrideCoreOperators(true) .build(); PebbleTemplate template = pebble.getTemplate("{{ not true }}"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("custom unary operator", writer.toString()); } @Test void testByDefaultPreventsOverrideCoreExtensionUnaryOperator() throws IOException { PebbleEngine pebble = new PebbleEngine.Builder() .loader(new StringLoader()) .extension(new TestExtension()) .build(); PebbleTemplate template = pebble.getTemplate("{{ not true }}"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("false", writer.toString()); } @Test void testOverrideCoreExtensionBinaryOperator() throws IOException { PebbleEngine pebble = new PebbleEngine.Builder() .loader(new StringLoader()) .extension(new TestExtension()) .allowOverrideCoreOperators(true) .build(); PebbleTemplate template = pebble.getTemplate("{{ 2 == 2 }}"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("custom binary operator", writer.toString()); } @Test void testByDefaultPreventsOverrideCoreExtensionBinaryOperator() throws IOException { PebbleEngine pebble = new PebbleEngine.Builder() .loader(new StringLoader()) .extension(new TestExtension()) .build(); PebbleTemplate template = pebble.getTemplate("{{ 2 == 2 }}"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("true", writer.toString()); } private static class TestExtension extends AbstractExtension { @Override public Map getFunctions() { Map functions = new HashMap<>(); functions.put("i18n", new CustomI18nFunction()); return functions; } @Override public Map getFilters() { Map filters = new HashMap<>(); filters.put("date", new CustomDateFilter()); return filters; } @Override public List getBinaryOperators() { BinaryOperatorImpl equalsOperator = new BinaryOperatorImpl( "==", 30, FakeEqualsExpression.class, Associativity.LEFT); return singletonList(equalsOperator); } @Override public List getUnaryOperators() { UnaryOperatorImpl equalsOperator = new UnaryOperatorImpl( "not", 500, FakeUnaryNotExpression.class); return singletonList(equalsOperator); } } private static class CustomI18nFunction implements Function { @Override public Object execute(Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { return "custom i18n function"; } @Override public List getArgumentNames() { return null; } } public static class FakeEqualsExpression extends BinaryExpression { @Override public String evaluate(PebbleTemplateImpl self, EvaluationContextImpl context) { return "custom binary operator"; } } public static class FakeUnaryNotExpression extends UnaryExpression { @Override public String evaluate(PebbleTemplateImpl self, EvaluationContextImpl context) { return "custom unary operator"; } } private static class CustomDateFilter implements Filter { @Override public Object apply(Object input, Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) throws PebbleException { return "custom date filter"; } @Override public List getArgumentNames() { return null; } } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/ParsingOdditiesTest.java ================================================ /* * This file is part of Pebble. *

* Copyright (c) 2014 by Mitchell Bösecke *

* For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble; import io.pebbletemplates.pebble.error.ParserException; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.loader.StringLoader; import io.pebbletemplates.pebble.template.PebbleTemplate; import org.junit.jupiter.api.Test; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashMap; import java.util.Locale; import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; class ParsingOdditiesTest { @Test void testEscapeCharactersText() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("templates/template.escapeCharactersInText.peb"); Map context = new HashMap<>(); Writer writer = new StringWriter(); template.evaluate(writer, context); } @Test void testExpressionInArguments() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble .getTemplate("{{ input(1 + 1) }}{% macro input(value) %}{{value}}{% endmacro %}"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("2", writer.toString()); } @Test void testPositionalAndNamedArguments() throws PebbleException, IOException, ParseException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false) .defaultLocale(Locale.ENGLISH).build(); String source = "{{ stringDate | date('yyyy/MMMM/d', existingFormat='yyyy-MMMM-d') }}"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); DateFormat format = new SimpleDateFormat("yyyy-MMMM-d", Locale.ENGLISH); Date realDate = format.parse("2012-July-01"); context.put("stringDate", format.format(realDate)); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("2012/July/1", writer.toString()); } @Test void testPositionalArgumentAfterNamedArguments() throws PebbleException { assertThrows(ParserException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false) .defaultLocale(Locale.ENGLISH).build(); String source = "{{ stringDate | date(existingFormat='yyyy-MMMM-d', 'yyyy/MMMM/d') }}"; pebble.getTemplate(source); }); } @Test void testVariableNamePrefixedWithOperatorName() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble .getTemplate("{{ organization }} {{ nothing }} {{ andy }} {{ equalsy }} {{ istanbul }}"); Map context = new HashMap<>(); context.put("organization", "organization"); context.put("nothing", "nothing"); context.put("andy", "andy"); context.put("equalsy", "equalsy"); context.put("istanbul", "istanbul"); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("organization nothing andy equalsy istanbul", writer.toString()); } @Test void testAttributeNamePrefixedWithOperatorName() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ foo.org }}"); Map context = new HashMap<>(); context.put("foo", new Foo("success")); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("success", writer.toString()); } public static class Foo { public String org; public Foo(String org) { this.org = org; } } @Test void testIncorrectlyNamedArgument() throws PebbleException, IOException { assertThrows(PebbleException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble .getTemplate("{{ 'This is a test of the abbreviate filter' | abbreviate(WRONG=16) }}"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("This is a tes...", writer.toString()); }); } @Test void testStringConstantWithLinebreak() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ 'test\ntest' }}"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("test\ntest", writer.toString()); } @Test void testStringWithDifferentQuotationMarks() throws PebbleException { assertThrows(ParserException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{'test\"}}"; pebble.getTemplate(source); }); } @Test void testSingleQuoteWithinDoubleQuotes() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{\"te'st\"}}"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("te'st", writer.toString()); template = pebble.getTemplate("{{\"te\\'st\"}}"); writer = new StringWriter(); template.evaluate(writer); assertEquals("te\\'st", writer.toString()); template = pebble.getTemplate("{{'te\\'st'}}"); writer = new StringWriter(); template.evaluate(writer); assertEquals("te'st", writer.toString()); } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/RenderSingleBlockTest.java ================================================ package io.pebbletemplates.pebble; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.loader.StringLoader; import io.pebbletemplates.pebble.template.PebbleTemplate; import org.junit.jupiter.api.Test; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.util.HashMap; import java.util.Map; import static java.util.Locale.CANADA; import static org.junit.jupiter.api.Assertions.assertEquals; class RenderSingleBlockTest { @Test void testRenderSingleBlock() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "Prefix {% block block_a %}Block A{% endblock %}{% block block_b %}Block B{% endblock %} Postfix"; PebbleTemplate template = pebble.getTemplate(source); Writer writer_a = new StringWriter(); template.evaluateBlock("block_a", writer_a); assertEquals("Block A", writer_a.toString()); Writer writer_b = new StringWriter(); template.evaluateBlock("block_b", writer_b); assertEquals("Block B", writer_b.toString()); } @Test void testRenderSingleBlockWithLocale() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "Prefix {% block block_a %}Block A{% endblock %}{% block block_b %}Block B{% endblock %} Postfix"; PebbleTemplate template = pebble.getTemplate(source); Writer writer_a = new StringWriter(); template.evaluateBlock("block_a", writer_a, CANADA); assertEquals("Block A", writer_a.toString()); Writer writer_b = new StringWriter(); template.evaluateBlock("block_b", writer_b, CANADA); assertEquals("Block B", writer_b.toString()); } @Test void testRenderSingleBlockWithContext() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "Prefix {% block block_a %}{{vara}}{% endblock %}{% block block_b %}{{varb}}{% endblock %} Postfix"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("vara", "FOO"); context.put("varb", "BAR"); Writer writer_a = new StringWriter(); template.evaluateBlock("block_a", writer_a, context, CANADA); assertEquals("FOO", writer_a.toString()); Writer writer_b = new StringWriter(); template.evaluateBlock("block_b", writer_b, context, CANADA); assertEquals("BAR", writer_b.toString()); } @Test void testRenderSingleBlockWithContextAndLocale() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "Prefix {% block block_a %}{{vara}}{% endblock %}{% block block_b %}{{varb}}{% endblock %} Postfix"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("vara", "FOO"); context.put("varb", "BAR"); Writer writer_a = new StringWriter(); template.evaluateBlock("block_a", writer_a, context); assertEquals("FOO", writer_a.toString()); Writer writer_b = new StringWriter(); template.evaluateBlock("block_b", writer_b, context); assertEquals("BAR", writer_b.toString()); } @Test void testRenderSingleExtendedBlock() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().strictVariables(true).build(); PebbleTemplate template = pebble .getTemplate("templates/single-block/template.renderextendedblock1.peb"); Writer writer_a = new StringWriter(); template.evaluateBlock("container_a", writer_a); assertEquals("Block A extended", writer_a.toString()); Writer writer_b = new StringWriter(); template.evaluateBlock("container_b", writer_b); assertEquals("Block B extended", writer_b.toString()); } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/RenderWithoutEndBlockTest.java ================================================ package io.pebbletemplates.pebble; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.loader.StringLoader; import io.pebbletemplates.pebble.template.PebbleTemplate; import org.junit.jupiter.api.Test; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.util.HashMap; import java.util.Map; import static java.util.Locale.CANADA; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; class RenderWithoutEndBlockTest { @Test void testRenderWithoutEndBlock() throws PebbleException, IOException { assertThrows(PebbleException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "Prefix {% block block_a %}Block A{% block block_b %}Block B{% endblock %} Postfix"; PebbleTemplate template = pebble.getTemplate(source); Writer writer_a = new StringWriter(); template.evaluateBlock("block_a", writer_a); }); } @Test void testRenderWithEndBlock() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "Prefix {% block block_a %}Block A{% endblock %}{% block block_b %}Block B{% endblock %} Postfix"; PebbleTemplate template = pebble.getTemplate(source); Writer writer_a = new StringWriter(); template.evaluateBlock("block_a", writer_a); assertEquals("Block A", writer_a.toString()); Writer writer_b = new StringWriter(); template.evaluateBlock("block_b", writer_b); assertEquals("Block B", writer_b.toString()); } @Test void testRenderWithoutEndBlockWithLocale() throws PebbleException, IOException { assertThrows(PebbleException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "Prefix {% block block_a %}Block A{% block block_b %}Block B{% endblock %} Postfix"; PebbleTemplate template = pebble.getTemplate(source); Writer writer_a = new StringWriter(); template.evaluateBlock("block_a", writer_a, CANADA); assertEquals("Block A", writer_a.toString()); Writer writer_b = new StringWriter(); template.evaluateBlock("block_b", writer_b, CANADA); assertEquals("Block B", writer_b.toString()); }); } @Test void testRenderWithEndBlockWithLocale() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "Prefix {% block block_a %}Block A{% endblock %}{% block block_b %}Block B{% endblock %} Postfix"; PebbleTemplate template = pebble.getTemplate(source); Writer writer_a = new StringWriter(); template.evaluateBlock("block_a", writer_a, CANADA); assertEquals("Block A", writer_a.toString()); Writer writer_b = new StringWriter(); template.evaluateBlock("block_b", writer_b, CANADA); assertEquals("Block B", writer_b.toString()); } @Test void testRenderWithoutEndBlockWithContext() throws PebbleException, IOException { assertThrows(PebbleException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "Prefix {% block block_a %}{{vara}}{% endblock %}{% block block_b %}{{varb}} Postfix"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("vara", "FOO"); context.put("varb", "BAR"); Writer writer_a = new StringWriter(); template.evaluateBlock("block_a", writer_a, context, CANADA); assertEquals("FOO", writer_a.toString()); Writer writer_b = new StringWriter(); template.evaluateBlock("block_b", writer_b, context, CANADA); assertEquals("BAR", writer_b.toString()); }); } @Test void testRenderEndBlockWithContext() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "Prefix {% block block_a %}{{vara}}{% endblock %}{% block block_b %}{{varb}}{% endblock %} Postfix"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("vara", "FOO"); context.put("varb", "BAR"); Writer writer_a = new StringWriter(); template.evaluateBlock("block_a", writer_a, context, CANADA); assertEquals("FOO", writer_a.toString()); Writer writer_b = new StringWriter(); template.evaluateBlock("block_b", writer_b, context, CANADA); assertEquals("BAR", writer_b.toString()); } @Test void testRenderWithoutEndBlockWithContextAndLocale() throws PebbleException, IOException { assertThrows(PebbleException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "Prefix {% block block_a %}{{vara}}{% block block_b %}{{varb}}{% endblock %} Postfix"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("vara", "FOO"); context.put("varb", "BAR"); Writer writer_a = new StringWriter(); template.evaluateBlock("block_a", writer_a, context); assertEquals("FOO", writer_a.toString()); Writer writer_b = new StringWriter(); template.evaluateBlock("block_b", writer_b, context); assertEquals("BAR", writer_b.toString()); }); } @Test void testRenderWithEndBlockWithContextAndLocale() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "Prefix {% block block_a %}{{vara}}{% endblock %}{% block block_b %}{{varb}}{% endblock %} Postfix"; PebbleTemplate template = pebble.getTemplate(source); Map context = new HashMap<>(); context.put("vara", "FOO"); context.put("varb", "BAR"); Writer writer_a = new StringWriter(); template.evaluateBlock("block_a", writer_a, context); assertEquals("FOO", writer_a.toString()); Writer writer_b = new StringWriter(); template.evaluateBlock("block_b", writer_b, context); assertEquals("BAR", writer_b.toString()); } @Test void testRenderWithoutEndBlockTest() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().strictVariables(true).build(); PebbleTemplate template = pebble .getTemplate("templates/single-block/template.renderextendedblock1.peb"); Writer writer_a = new StringWriter(); template.evaluateBlock("container_a", writer_a); assertEquals("Block A extended", writer_a.toString()); Writer writer_b = new StringWriter(); template.evaluateBlock("container_b", writer_b); assertEquals("Block B extended", writer_b.toString()); } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/ScopeChainTest.java ================================================ /* * This file is part of Pebble. *

* Copyright (c) 2014 by Mitchell Bösecke *

* For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.template.ScopeChain; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; class ScopeChainTest { @Test void testSet() throws PebbleException { ScopeChain scopeChain = new ScopeChain(); scopeChain.pushScope(); scopeChain.set("key", "value"); assertEquals("value", scopeChain.get("key")); scopeChain.pushScope(); scopeChain.set("key", "value2"); assertEquals("value2", scopeChain.get("key")); scopeChain.popScope(); assertEquals("value2", scopeChain.get("key")); scopeChain.pushLocalScope(); scopeChain.set("key", "value3"); assertEquals("value3", scopeChain.get("key")); scopeChain.popScope(); assertEquals("value2", scopeChain.get("key")); } @Test void testGetValueWithLocalScopeFirstInChainAndValueInAnotherScope() { ScopeChain scopeChain = new ScopeChain(); scopeChain.pushScope(); scopeChain.set("key", "value"); scopeChain.pushLocalScope(); scopeChain.set("key2", "value2"); assertNull(scopeChain.get("key")); assertEquals("value2", scopeChain.get("key2")); } @Test void testGetValueWithLocalScopeNotFirstInChainAndValueInAnotherScope() { ScopeChain scopeChain = new ScopeChain(); scopeChain.pushScope(); scopeChain.set("key", "value"); scopeChain.pushLocalScope(); scopeChain.set("key2", "value2"); scopeChain.pushScope(); scopeChain.set("key3", "value3"); assertNull(scopeChain.get("key")); assertEquals("value2", scopeChain.get("key2")); assertEquals("value3", scopeChain.get("key3")); } @Test void testContainsKeyWithLocalScopeFirstInChainAndValueInAnotherScope() { ScopeChain scopeChain = new ScopeChain(); scopeChain.pushScope(); scopeChain.set("key", "value"); scopeChain.pushLocalScope(); scopeChain.set("key2", "value2"); assertFalse(scopeChain.containsKey("key")); assertTrue(scopeChain.containsKey("key2")); } @Test void testContainsKeyWithLocalScopeNotFirstInChainAndValueInAnotherScope() { ScopeChain scopeChain = new ScopeChain(); scopeChain.pushScope(); scopeChain.set("key", "value"); scopeChain.pushLocalScope(); scopeChain.set("key2", "value2"); scopeChain.pushScope(); scopeChain.set("key3", "value3"); assertFalse(scopeChain.containsKey("key")); assertTrue(scopeChain.containsKey("key2")); assertTrue(scopeChain.containsKey("key3")); } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/ScopeTest.java ================================================ package io.pebbletemplates.pebble; import io.pebbletemplates.pebble.template.Scope; import org.junit.jupiter.api.Test; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import static org.junit.jupiter.api.Assertions.assertEquals; class ScopeTest { @Test void testGetKeys() { Map map = new HashMap<>(); map.putIfAbsent("key1", new String("value1")); map.putIfAbsent("key2", new String("value2")); Scope scope = new Scope(map, false); Set expected = new HashSet<>(); expected.add("key1"); expected.add("key2"); assertEquals(expected, scope.getKeys()); } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/SplitFilterTest.java ================================================ package io.pebbletemplates.pebble; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.loader.StringLoader; import io.pebbletemplates.pebble.template.PebbleTemplate; import org.junit.jupiter.api.Test; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.util.HashMap; import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; class SplitFilterTest { @Test void whenSplit_givenInputNull_thenReturnNull() throws IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{% set foo = null | split(',') %}\n" + "{{ foo }}"); Map context = new HashMap<>(); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("", writer.toString()); } @Test void whenSplit_givenNoDelimiter_thenThrowPebbleException() throws IOException { assertThrows(PebbleException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ \"one,two,three\" | split }}"); Map context = new HashMap<>(); Writer writer = new StringWriter(); template.evaluate(writer, context); }); } @Test void whenSplit_givenInputWithDelimiter_thenSplit() throws IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{% set foo = 'one|two|three' | split('\\|') %}" + "{% for var in foo %}" + "{{ var }}" + "{% endfor %}"); Map context = new HashMap<>(); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("onetwothree", writer.toString()); } @Test void whenSplit_givenInputWithDelimiterAndPositiveLimit_thenSplit() throws IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble .getTemplate("{% set foo = \"one,two,three,four,five\" | split(',',3) %}" + "{% for var in foo %}" + "{{ var }}" + "{% endfor %}"); Map context = new HashMap<>(); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("onetwothree,four,five", writer.toString()); } @Test void whenSplit_givenInputWithDelimiterAndNegativeLimit_thenSplit() throws IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble .getTemplate("{% set foo = \"one,two,three,four,five\" | split(',',-1) %}" + "{% for var in foo %}" + "{{ var }}" + "{% endfor %}"); Map context = new HashMap<>(); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("onetwothreefourfive", writer.toString()); } @Test void whenSplit_givenInputWithDelimiterAndZeroLimit_thenSplit() throws IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble .getTemplate("{% set foo = \"one,two,three,four,five\" | split(',',0) %}" + "{% for var in foo %}" + "{{ var }}" + "{% endfor %}"); Map context = new HashMap<>(); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("onetwothreefourfive", writer.toString()); } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/StrictModeTest.java ================================================ package io.pebbletemplates.pebble; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.error.RootAttributeNotFoundException; import io.pebbletemplates.pebble.loader.StringLoader; import io.pebbletemplates.pebble.template.PebbleTemplate; import org.junit.jupiter.api.Test; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.util.HashMap; import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; /** * Tests if strict mode works in any case. * * @author Thomas Hunziker */ class StrictModeTest { /** * Tests that the line number and file name is correctly passed to the exception in strict mode. */ @Test() void testComplexVariable() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().strictVariables(true).build(); PebbleTemplate template = pebble .getTemplate("templates/template.strictModeComplexExpression.peb"); Map context = new HashMap<>(); Writer writer = new StringWriter(); try { template.evaluate(writer, context); fail("Exception " + RootAttributeNotFoundException.class.getCanonicalName() + " is expected."); } catch (RootAttributeNotFoundException e) { assertEquals(e.getFileName(), "templates/template.strictModeComplexExpression.peb"); assertEquals(e.getLineNumber(), (Integer) 2); } } /** * Tests that the line number and file name is correctly passed to the exception in strict mode. */ @Test() void testSimpleVariable() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().strictVariables(true).build(); PebbleTemplate template = pebble .getTemplate("templates/template.strictModeSimpleExpression.peb"); Map context = new HashMap<>(); Writer writer = new StringWriter(); try { template.evaluate(writer, context); fail( "Exception " + RootAttributeNotFoundException.class.getCanonicalName() + " is expected."); } catch (RootAttributeNotFoundException e) { assertEquals("templates/template.strictModeSimpleExpression.peb", e.getFileName()); assertEquals((Integer) 2, e.getLineNumber()); } } @Test void whenStrictVariableEnabledWithAndExpressionAndLeftOperandFalse_thenDontEvaluateRightExpression() throws PebbleException, IOException { PebbleEngine engine = new PebbleEngine .Builder() .loader(new StringLoader()) .strictVariables(true) .autoEscaping(false) .build(); PebbleTemplate template = engine.getTemplate("{%- set a = null -%}\n" + "{{- a is not null and a.toLowerCase() == \"abc\" -}}\n" + "{%- if a is not null and a.toLowerCase() == \"abc\" -%}\n" + "Do something" + "{%- endif -%}"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("false", writer.toString()); } @Test void whenStrictVariableEnabledWithOrExpressionAndLeftOperandTrue_thenDontEvaluateRightExpression() throws PebbleException, IOException { PebbleEngine engine = new PebbleEngine .Builder() .loader(new StringLoader()) .strictVariables(true) .autoEscaping(false) .build(); PebbleTemplate template = engine.getTemplate("{%- set a = null -%}\n" + "{{- a is null or a.toLowerCase() == \"abc\" -}}\n" + "{%- if a is null or a.toLowerCase() == \"abc\" -%}\n" + "Do something" + "{%- endif -%}"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("trueDo something", writer.toString()); } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/StringInterpolationTest.java ================================================ package io.pebbletemplates.pebble; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.loader.StringLoader; import org.junit.jupiter.api.Test; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.util.HashMap; import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; class StringInterpolationTest { @Test void testSimpleVariableInterpolation() throws Exception { String source = "{{ \"Hello, #{name}\" }}"; Map ctx = new HashMap<>(); ctx.put("name", "joe"); assertEquals("Hello, joe", this.evaluate(source, ctx)); } @Test void testExpressionInterpolation() throws Exception { String src = "{{ \"1 plus 2 equals #{1 + 2}\" }}"; assertEquals("1 plus 2 equals 3", this.evaluate(src)); } @Test void testUnclosedInterpolation() throws Exception { assertThrows(PebbleException.class, () -> { String src = "{{ \" #{ 1 +\" }}"; this.evaluate(src); }); } @Test void testDoubleClosedInterpolation() throws Exception { String src = "{{ \"#{3}}\" }}"; assertEquals("3}", this.evaluate(src)); } @Test void testFunctionInInterpolation() throws Exception { String src = "{{ \"Maximum: #{ max(5, 10) } \" }}"; assertEquals("Maximum: 10 ", this.evaluate(src)); } @Test void testVerbatimInterpolation() throws Exception { String src = "{% verbatim %}{{ \"Sum: #{ 1 + 2 }\" }}{% endverbatim %}"; assertEquals("{{ \"Sum: #{ 1 + 2 }\" }}", this.evaluate(src)); } @Test void testInterpolationWithEscapedQuotes() throws Exception { String str = "{{ \"The cow says: #{\"\\\"moo\\\"\"}\" }}"; assertEquals("The cow says: \"moo\"", this.evaluate(str)); } @Test void testNestedInterpolation0() throws Exception { String src = "{{ \"Nested: #{ outer + \" #{ inner }\" }\" }}"; Map ctx = new HashMap<>(); ctx.put("outer", "OUTER"); ctx.put("inner", "INNER"); assertEquals("Nested: OUTER INNER", this.evaluate(src, ctx)); } @Test void testNestedInterpolation1() throws Exception { String src = "{{ \"#{\"#{\"#{'hi'}\"}\"}\" }}"; assertEquals("hi", this.evaluate(src)); } @Test void testInterpolationWhitespace0() throws Exception { String src = "{{ \"Outer: #{3+4}\" }}"; assertEquals("Outer: 7", this.evaluate(src)); } @Test void testInterpolationWhitespace1() throws Exception { String src = "{{ \"Outer: #{ 3 + 4 }\" }}"; assertEquals("Outer: 7", this.evaluate(src)); } @Test void testInterpolationWhitespace2() throws Exception { String src = "{{ \"Outer:#{ 3 + 4 }\" }}"; assertEquals("Outer:7", this.evaluate(src)); } @Test void testInterpolationWhitespace3() throws Exception { String src = "{{ \"Outer:#{ 3 + 4 } \" }}"; assertEquals("Outer:7 ", this.evaluate(src)); } @Test void testInterpolationWhitespace4() throws Exception { String src = "{{ \"Outer: #{ 3 + 4 } \" }}"; assertEquals("Outer: 7 ", this.evaluate(src)); } @Test void testStringWithNumberSigns() throws Exception { String src = "{{ \"#bang #crash }!!\" }}"; assertEquals("#bang #crash }!!", this.evaluate(src)); } @Test void testStringWithNumberSignsAndInterpolation() throws Exception { String src = "{{ \"The cow said ##{'moo'}#\" }}"; assertEquals("The cow said #moo#", this.evaluate(src)); } @Test void testWhitespaceBetweenNumberSignAndCurlyBrace() throws Exception { String src = "{{ \"Green eggs and # {ham}\" }}"; assertEquals("Green eggs and # {ham}", this.evaluate(src)); } @Test void testStringInsideInterpolation() throws Exception { String src = "{{ \"Outer: #{ \"inner\" }\" }}"; assertEquals("Outer: inner", this.evaluate(src)); } @Test void testSingleQuoteNoInterpolation() throws Exception { String src = "{{ '#{3}'}}"; assertEquals("#{3}", this.evaluate(src)); } @Test void testSingleQuoteInsideInterpolation() throws Exception { String src = "{{ \"The cow says: #{'moo' + '#{moo}'}\" }}"; assertEquals("The cow says: moo#{moo}", this.evaluate(src)); } @Test void testSequentialInterpolations0() throws Exception { String src = "{{ \"#{1+1}#{2+2}\" }}"; assertEquals("24", this.evaluate(src)); } @Test void testSequentialInterpolations1() throws Exception { String src = "{{ \"The #{'cow'} says #{'moo'} and jumps #{'over'} the #{'moon'}\"}}"; assertEquals("The cow says moo and jumps over the moon", this.evaluate(src)); } @Test void testVariableContainingInterplationSyntax() throws Exception { String src = "{{ \"Hey #{name}\" }}"; Map ctx = new HashMap<>(); ctx.put("name", "#{1+1}"); assertEquals("Hey #{1+1}", this.evaluate(src, ctx)); } @Test void testNewlineInInterpolation() throws Exception { String src = "{{ \"Sum = #{ 'egg\negg'}\" }}"; assertEquals("Sum = egg\negg", this.evaluate(src)); } private String evaluate(String template) throws PebbleException, IOException { return this.evaluate(template, null); } private String evaluate(String template, Map context) throws PebbleException, IOException { Writer writer = new StringWriter(); new PebbleEngine.Builder() .loader(new StringLoader()) .strictVariables(false) .build() .getTemplate(template) .evaluate(writer, context); return writer.toString(); } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/TernaryExpressionTest.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble; import io.pebbletemplates.pebble.error.ParserException; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.loader.StringLoader; import io.pebbletemplates.pebble.template.PebbleTemplate; import org.junit.jupiter.api.Test; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.util.HashMap; import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; class TernaryExpressionTest { @Test void testTernaryFail1() throws PebbleException { assertThrows(ParserException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ 1 > 1 ? 'true' }}"; pebble.getTemplate(source); }); } @Test void testTernaryFail2() throws PebbleException { assertThrows(ParserException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ 1 > 1 ? : 'true' }}"; pebble.getTemplate(source); }); } @Test void testTernaryFail3() throws PebbleException { assertThrows(ParserException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ 1 > 1 ? 'true' : }}"; pebble.getTemplate(source); }); } @Test void testTernaryFail4() throws PebbleException { assertThrows(ParserException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ 1 > 1 ? : }}"; pebble.getTemplate(source); }); } @Test void testTernaryFail5() throws PebbleException { assertThrows(ParserException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ 1 > 1 ? : ? 'true' : 'false' }}"; pebble.getTemplate(source); }); } @Test void testTernaryFail6() throws PebbleException { assertThrows(ParserException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ 1 > 1 ? true ? 'true' : 'false' }}"; pebble.getTemplate(source); }); } @Test void testTernaryFail7() throws PebbleException { assertThrows(ParserException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ 1 > 1 ? : false ? 'true' : 'false' }}"; pebble.getTemplate(source); }); } @Test void testTernaryFail8() throws PebbleException { assertThrows(ParserException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ 1 > 1 ? 2 > 2 ? 'true' : 'false' }}"; pebble.getTemplate(source); }); } @Test void testTernaryFail9() throws PebbleException { assertThrows(ParserException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ 1 > 1 ? 2 > 2 ? : 'false' : 'false' }}"; pebble.getTemplate(source); }); } @Test void testTernaryFail10() throws PebbleException { assertThrows(ParserException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ 1 > 1 ? 2 > 2 ? : : 'false' }}"; pebble.getTemplate(source); }); } @Test void testTernaryFail11() throws PebbleException { assertThrows(ParserException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ 1 > 1 ? 'true' : 3 > 3 ? 'false' }}"; pebble.getTemplate(source); }); } @Test void testTernaryFail12() throws PebbleException { assertThrows(ParserException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ 1 > 1 ? 'true' : 3 > 3 ? : 'false' }}"; pebble.getTemplate(source); }); } @Test void testTernaryFail13() throws PebbleException { assertThrows(ParserException.class, () -> { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ 1 > 1 ? 'true' : 3 > 3 ? : }}"; pebble.getTemplate(source); }); } @Test void testTernary1() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ 1 == 1 ? 'true' : 'false' }}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("true", writer.toString()); } @Test void testTernary2() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ 1 > 1 ? 'true' : 'false' }}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("false", writer.toString()); } @Test void testTernary3() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ 1 > 1 ? true : false ? 2 > 2 ? 'a' : 'b' : 3 == 3 ? 'c' : 'd' }}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("c", writer.toString()); } @Test void testComplexTernary1() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ ('a' == 'b' ? 2 + 2 : (val - 2 is not even ? true : false) ) ? (min(otherVal,-1) | abs < 3 / 3 - 1 ? false : ['yay!'] contains 'yay!' ) : ('?' is not empty ? ''~'?' : 0) }}"; PebbleTemplate template = pebble.getTemplate(source); Map params = new HashMap<>(); params.put("val", 3); params.put("otherVal", 100); Writer writer = new StringWriter(); template.evaluate(writer, params); assertEquals("true", writer.toString()); } @Test void testComplexTernary2() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ 'a' == 'b' ? 2 + 2 : val - 2 is not even ? true : false ? min(otherVal,-1) | abs < 3 / 3 - 1 ? false : ['yay!'] contains 'yay!' : '?' is not empty ? ''~'?' : 0 }}"; PebbleTemplate template = pebble.getTemplate(source); Map params = new HashMap<>(); params.put("val", 3); params.put("otherVal", 100); Writer writer = new StringWriter(); template.evaluate(writer, params); assertEquals("true", writer.toString()); } @Test void testTernaryIntTrue() throws IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate( "{{ 1 ? 'true' : 'false' }}"); StringWriter writer = new StringWriter(); template.evaluate(writer); assertEquals("true", writer.toString()); } @Test void testTernaryIntFalse() throws IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate( "{{ 0 ? 'true' : 'false' }}"); StringWriter writer = new StringWriter(); template.evaluate(writer); assertEquals("false", writer.toString()); } @Test void testTernaryStringTrue() throws IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate( "{{ 'not empty' ? 'true' : 'false' }}"); StringWriter writer = new StringWriter(); template.evaluate(writer); assertEquals("true", writer.toString()); } @Test void testTernaryStringFalse() throws IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate( "{{ '' ? 'true' : 'false' }}"); StringWriter writer = new StringWriter(); template.evaluate(writer); assertEquals("false", writer.toString()); } @Test void testTernaryDecimalTrue() throws IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate( "{{ 0.000001 ? 'true' : 'false' }}"); StringWriter writer = new StringWriter(); template.evaluate(writer); assertEquals("true", writer.toString()); } @Test void testTernaryDecimalFalse() throws IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate( "{{ 0.00000 ? 'true' : 'false' }}"); StringWriter writer = new StringWriter(); template.evaluate(writer); assertEquals("false", writer.toString()); } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/TestParallelParsing.java ================================================ package io.pebbletemplates.pebble; import io.pebbletemplates.pebble.error.ParserException; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.extension.AbstractExtension; import io.pebbletemplates.pebble.extension.NodeVisitor; import io.pebbletemplates.pebble.lexer.Token; import io.pebbletemplates.pebble.lexer.TokenStream; import io.pebbletemplates.pebble.parser.Parser; import io.pebbletemplates.pebble.node.RenderableNode; import io.pebbletemplates.pebble.template.EvaluationContextImpl; import io.pebbletemplates.pebble.template.PebbleTemplate; import io.pebbletemplates.pebble.template.PebbleTemplateImpl; import io.pebbletemplates.pebble.tokenParser.TokenParser; import org.junit.jupiter.api.Test; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.util.Collections; import java.util.List; import java.util.concurrent.atomic.AtomicReference; import static org.junit.jupiter.api.Assertions.assertEquals; /** * This tests tests the parallel parsing / compilation of templates. * * @author Thomas Hunziker */ class TestParallelParsing { /** * Tests if the parse is working correctly within a multi threading environment. */ @Test void testParser() throws InterruptedException { final PebbleEngine pebble = new PebbleEngine.Builder().strictVariables(true) .extension(new DelayExtension()).build(); final AtomicReference resultThread1 = new AtomicReference<>(); final AtomicReference resultThread2 = new AtomicReference<>(); Thread thread1 = new Thread(() -> { try { PebbleTemplate template = pebble.getTemplate("templates/template.parallelParsing1.peb"); Writer writer = new StringWriter(); template.evaluate(writer); resultThread1.set(writer.toString()); } catch (PebbleException | IOException e) { throw new RuntimeException(e); } }); Thread thread2 = new Thread(() -> { try { PebbleTemplate template = pebble.getTemplate("templates/template.parallelParsing2.peb"); Writer writer = new StringWriter(); template.evaluate(writer); resultThread2.set(writer.toString()); } catch (PebbleException | IOException e) { throw new RuntimeException(e); } }); // Start the threads. thread1.start(); thread2.start(); // Wait until both threads completed. thread1.join(); thread2.join(); assertEquals("output in 1: a|output in 1: b|output in 1: c", resultThread1.get()); assertEquals("output in 2: a|output in 2: b|output in 2: c", resultThread2.get()); } /** * This extension provides a token parser which does introduce a delay during the parser. This * allows to provoke failing of the test when the parallel implementation is not ok. */ private static class DelayExtension extends AbstractExtension { @Override public List getTokenParsers() { return Collections.singletonList(new DelayTokenParser()); } } private static class DelayTokenParser implements TokenParser { @Override public String getTag() { return "delay"; } @Override public RenderableNode parse(Token token, Parser parser) throws ParserException { TokenStream stream = parser.getStream(); // skip over the 'delay' token Token delayName = stream.next(); // expect a name or string for the new block if (!delayName.test(Token.Type.NUMBER)) { // we already know an error has occurred but let's just call the // typical "expect" method so that we know a proper error // message is given to user stream.expect(Token.Type.NUMBER); } int delay = Integer.valueOf(delayName.getValue()); try { // We sleep for the given number of milliseconds: Thread.sleep(delay); } catch (InterruptedException e) { throw new RuntimeException(e); } // skip over the delay stream.next(); stream.expect(Token.Type.EXECUTE_END); return new RenderableNode() { @Override public void accept(NodeVisitor visitor) { visitor.visit(this); } @Override public void render(PebbleTemplateImpl self, Writer writer, EvaluationContextImpl context) throws PebbleException { // Do nothing. } }; } } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/TestRelativePath.java ================================================ package io.pebbletemplates.pebble; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.template.PebbleTemplate; import org.junit.jupiter.api.Test; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import static org.junit.jupiter.api.Assertions.assertEquals; /** * Tests if relative path works as expected. * * @author Thomas Hunziker */ class TestRelativePath { /** * Tests if relative includes work. */ @Test void testRelativeInclude() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().strictVariables(true).build(); PebbleTemplate template = pebble .getTemplate("templates/relativepath/template.relativeinclude1.peb"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("included", writer.toString()); } /** * Tests if relative extends work. */ @Test void testRelativeExtends() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().strictVariables(true).build(); PebbleTemplate template = pebble .getTemplate("templates/relativepath/template.relativeextends1.peb"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("

overridden
", writer.toString().replaceAll("\\r?\\n", "").replace("\t", "").replace(" ", "")); } /** * Tests if relative imports work. */ @Test void testRelativeImports() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().strictVariables(true).build(); PebbleTemplate template = pebble .getTemplate("templates/relativepath/template.relativeimport1.peb"); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("", writer.toString().replaceAll("\\r?\\n", "").replace("\t", "")); } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/WhitespaceControlTest.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.loader.StringLoader; import io.pebbletemplates.pebble.template.PebbleTemplate; import org.junit.jupiter.api.Test; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.util.HashMap; import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; class WhitespaceControlTest { /** * A windows newline character (i.e. \n\r) in a template should be recognized * and output as as a Windows newline character. The Windows newline character * should not be converted to a Unix newline character (i.e. \n). */ @Test void testWindowsNewlineCharacter() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(true).build(); PebbleTemplate windowsTemplate = pebble.getTemplate("\r\n"); Writer windowsWriter = new StringWriter(); windowsTemplate.evaluate(windowsWriter); assertEquals("\r\n", windowsWriter.toString()); } /** * A Unix newline character (i.e. \n\r) in a template should be recognized * and output as as a Unix newline character. The Unix newline character * should not be converted to a Windows newline character (i.e. \r\n). */ @Test void testUnixNewlineCharacter() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(true).build(); PebbleTemplate unixTemplate = pebble.getTemplate("\n"); Writer unixWriter = new StringWriter(); unixTemplate.evaluate(unixWriter); assertEquals("\n", unixWriter.toString()); } /** * A leading Whitespace Control Modifier in an expression delimiter (i.e. Pebble variable reference) * should remove whitespace before the variable reference on the same line up to any * surrounding text, i.e. a print delimiter. * * @throws PebbleException * @throws IOException */ @Test void testLeadingWhitespaceControlModifier() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(true).build(); PebbleTemplate template = pebble.getTemplate("
  • {{- foo }}
  • "); Writer writer = new StringWriter(); Map context = new HashMap<>(); context.put("foo", "bar"); template.evaluate(writer, context); assertEquals("
  • bar
  • ", writer.toString()); } /** * A trailing Whitespace Control Modifier in an expression delimiter (i.e. Pebble variable reference) * should remove whitespace after the variable reference on the same line up to any * surrounding text. * * @throws PebbleException * @throws IOException */ @Test void testTrailingWhitespaceControlModifier() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(true).build(); PebbleTemplate template = pebble.getTemplate("
  • {{ foo -}}
  • "); Writer writer = new StringWriter(); Map context = new HashMap<>(); context.put("foo", "bar"); template.evaluate(writer, context); assertEquals("
  • bar
  • ", writer.toString()); } /** * A Whitespace Control Modifier in an expression delimiter (i.e. Pebble variable reference) * should not have any effect if there is no whitespace immediately before or after the * variable reference. * * @throws PebbleException * @throws IOException */ @Test void testLeadingWhitespaceTrimWithoutOutsideText() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(true).build(); PebbleTemplate template = pebble.getTemplate("{{- foo -}}"); Writer writer = new StringWriter(); Map context = new HashMap<>(); context.put("foo", "bar"); template.evaluate(writer, context); assertEquals("bar", writer.toString()); } /** * A leading and trailing Whitespace Control Modifiers in an expression delimiter * (i.e. Pebble variable reference) should remove whitespace before and after * the variable reference on the same line up to any surrounding text. * * @throws PebbleException * @throws IOException */ @Test void testLeadingAndTrailingWhitespaceControlModifier() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(true).build(); PebbleTemplate template = pebble .getTemplate("{{ foo }}
  • {{- foo -}}
  • {{ foo }}"); Writer writer = new StringWriter(); Map context = new HashMap<>(); context.put("foo", "bar"); template.evaluate(writer, context); assertEquals("bar
  • bar
  • bar", writer.toString()); } /** * Newline characters immediately before or after a Whitespace Control Modifier * should be removed. * * @throws PebbleException * @throws IOException */ @Test void testWhitespaceControlModifierRemovesNewlines() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(true).build(); PebbleTemplate template = pebble.getTemplate("
  • \n{{- foo -}}\n
  • "); Writer writer = new StringWriter(); Map context = new HashMap<>(); context.put("foo", "bar"); template.evaluate(writer, context); assertEquals("
  • bar
  • ", writer.toString()); } @Test void testWhitespaceTrimWithExecuteDelimiter() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(true).build(); PebbleTemplate template = pebble .getTemplate("
  • {%- if true %} success {% else %} fail {% endif -%}
  • "); Writer writer = new StringWriter(); Map context = new HashMap<>(); context.put("foo", "bar"); template.evaluate(writer, context); assertEquals("
  • success
  • ", writer.toString()); } @Test void testLeadingWhitespaceTrimWithCommentDelimiter() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(true).build(); PebbleTemplate template = pebble.getTemplate("
  • {#- comment #}
  • "); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("
  • ", writer.toString()); } @Test void testTrailingWhitespaceTrimWithCommentDelimiter() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(true).build(); PebbleTemplate template = pebble.getTemplate("
  • {# comment -#}
  • "); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("
  • ", writer.toString()); } @Test void testLeadingWhitespaceTrimWithVerbatimTag() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(true).build(); PebbleTemplate template = pebble .getTemplate("
  • {%- verbatim %}{{ bar }} {%- endverbatim %}
  • "); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("
  • {{ bar }}
  • ", writer.toString()); } @Test void testTrailingWhitespaceTrimWithVerbatimTag() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(true).build(); PebbleTemplate template = pebble .getTemplate("
  • {% verbatim -%} {{ bar }}{% endverbatim -%}
  • "); Writer writer = new StringWriter(); template.evaluate(writer); assertEquals("
  • {{ bar }}
  • ", writer.toString()); } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/WritingTest.java ================================================ /* * This file is part of Pebble. *

    * Copyright (c) 2014 by Mitchell Bösecke *

    * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.loader.StringLoader; import io.pebbletemplates.pebble.template.PebbleTemplate; import org.junit.jupiter.api.Test; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.util.HashMap; import java.util.Map; import java.util.concurrent.Executors; import static org.junit.jupiter.api.Assertions.assertEquals; class WritingTest { /** * There was an issue where the pebble engine was closing the provided writer. This is wrong. */ @Test void testMultipleEvaluationsWithOneWriter() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()).build(); PebbleTemplate template1 = pebble.getTemplate("first"); PebbleTemplate template2 = pebble.getTemplate("second"); Writer writer = new UncloseableWriter(); template1.evaluate(writer); template2.evaluate(writer); assertEquals("firstsecond", writer.toString()); } public class UncloseableWriter extends StringWriter { @Override public void close() { throw new RuntimeException("Can not close this writer."); } } /** * The following test used to fail because one parallel thread would rewrite the contents of * another parallel thread's character buffer. */ @Test void testParallelCharacterBuffersBeingOverriden() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .executorService(Executors.newCachedThreadPool()).build(); String source = "beginning {% parallel %}{{ slowObject.first }}{% endparallel %} middle {% parallel %}{{ slowObject.second }}{% endparallel %} end {% parallel %}{{ slowObject.third }}{% endparallel %}"; PebbleTemplate template = pebble.getTemplate(source); for (int i = 0; i < 2; i++) { Writer writer = new StringWriter(); Map context = new HashMap<>(); context.put("slowObject", new SlowObject()); template.evaluate(writer, context); assertEquals("beginning first middle second end third", writer.toString()); } } public class SlowObject { public String first() { try { Thread.sleep(200); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } return "first"; } public String second() { try { Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } return "second"; } public String third() { try { Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } return "third"; } } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/attributes/methodaccess/BlacklistMethodAccessValidatorTest.java ================================================ package io.pebbletemplates.pebble.attributes.methodaccess; import static org.assertj.core.api.Assertions.assertThat; import java.lang.reflect.Method; import java.util.stream.Stream; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; class BlacklistMethodAccessValidatorTest { private final InstanceProvider instanceProvider = new InstanceProvider(); private final MethodAccessValidator underTest = new BlacklistMethodAccessValidator(); @ParameterizedTest @MethodSource("provideUnsafeMethod") void checkIfAccessIsForbidden(Method unsafeMethod) throws NoSuchFieldException, NoSuchMethodException { Class declaringClass = unsafeMethod.getDeclaringClass(); Object instance = this.instanceProvider.createObject(declaringClass); boolean methodAccessAllowed = this.underTest.isMethodAccessAllowed(instance, unsafeMethod); assertThat(methodAccessAllowed).isFalse(); } @ParameterizedTest @MethodSource("provideAllowedMethod") void checkIfAccessIsAllowed(Method allowedMethod) throws Throwable { Class declaringClass = allowedMethod.getDeclaringClass(); Object instance = this.instanceProvider.createObject(declaringClass); boolean methodAccessAllowed = this.underTest.isMethodAccessAllowed(instance, allowedMethod); assertThat(methodAccessAllowed).isTrue(); } private static Stream provideUnsafeMethod() { return MethodsProvider.UNSAFE_METHODS.stream(); } private static Stream provideAllowedMethod() { return MethodsProvider.ALLOWED_METHODS.stream(); } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/attributes/methodaccess/Foo.java ================================================ package io.pebbletemplates.pebble.attributes.methodaccess; public class Foo { private String x; public void getX() { } private Foo() { } public void setX(String x) { this.x = x; } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/attributes/methodaccess/InstanceProvider.java ================================================ package io.pebbletemplates.pebble.attributes.methodaccess; import java.lang.reflect.AccessibleObject; import java.lang.reflect.Constructor; public class InstanceProvider { Object createObject(Class declaringClass) throws NoSuchFieldException, NoSuchMethodException { try { Constructor constructor = declaringClass.getDeclaredConstructor(); constructor.setAccessible(true); return constructor.newInstance(); } catch (Exception e) { switch (declaringClass.getName()) { case "java.lang.reflect.Field": return Foo.class.getDeclaredField("x"); case "java.lang.reflect.Method": return Foo.class.getDeclaredMethod("getX", (Class[]) null); case "java.lang.Class": return Foo.class; case "java.lang.reflect.Constructor": return Foo.class.getDeclaredConstructor(); case "java.lang.Integer": return Integer.valueOf(1); case "java.lang.System": return System.class; case "java.lang.Runtime": return Runtime.class; case "java.lang.reflect.AccessibleObject": return AccessibleObject.class; case "java.lang.ThreadGroup": return Runtime.class; default: throw new RuntimeException( String.format("No object instance defined for class %s", declaringClass.getName())); } } } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/attributes/methodaccess/MethodsProvider.java ================================================ package io.pebbletemplates.pebble.attributes.methodaccess; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Method; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.StringTokenizer; class MethodsProvider { private static final String UNSAFE_METHODS_PROPERTIES = "/security/unsafeMethods.properties"; private static final String ALLOWED_METHODS_PROPERTIES = "/security/allowedMethods.properties"; static final Set UNSAFE_METHODS = createUnsafeMethodsSet(); static final Set ALLOWED_METHODS = createAllowedMethodsSet(); MethodsProvider() { } private static Set createAllowedMethodsSet() { return createMethodsSet(ALLOWED_METHODS_PROPERTIES); } private static Set createUnsafeMethodsSet() { return createMethodsSet(UNSAFE_METHODS_PROPERTIES); } private static Set createMethodsSet(String filename) { try { Properties props = loadProperties(filename); Set set = new HashSet<>(props.size() * 4 / 3, 1f); Map primClasses = createPrimitiveClassesMap(); for (Object key : props.keySet()) { try { set.add(parseMethodSpec((String) key, primClasses)); } catch (NoSuchMethodException | ClassNotFoundException ignored) { // Ignore when the method is not found or the class is not found // Continue to add other methods } } return set; } catch (Exception e) { throw new RuntimeException(String.format("Could not load method set from file %s", filename), e); } } private static Method parseMethodSpec(String methodSpec, Map primClasses) throws ClassNotFoundException, NoSuchMethodException { int brace = methodSpec.indexOf('('); int dot = methodSpec.lastIndexOf('.', brace); Class clazz = Class.forName(methodSpec.substring(0, dot)); String methodName = methodSpec.substring(dot + 1, brace); String argSpec = methodSpec.substring(brace + 1, methodSpec.length() - 1); StringTokenizer tok = new StringTokenizer(argSpec, ","); int argcount = tok.countTokens(); Class[] argTypes = new Class[argcount]; for (int i = 0; i < argcount; i++) { String argClassName = tok.nextToken(); argTypes[i] = primClasses.get(argClassName); if (argTypes[i] == null) { argTypes[i] = Class.forName(argClassName); } } return clazz.getMethod(methodName, argTypes); } private static Map createPrimitiveClassesMap() { Map map = new HashMap<>(); map.put("boolean", Boolean.TYPE); map.put("byte", Byte.TYPE); map.put("char", Character.TYPE); map.put("short", Short.TYPE); map.put("int", Integer.TYPE); map.put("long", Long.TYPE); map.put("float", Float.TYPE); map.put("double", Double.TYPE); return map; } private static Properties loadProperties(String resource) throws IOException { Properties props = new Properties(); try (InputStream is = MethodsProvider.class.getResourceAsStream(resource)) { props.load(is); } return props; } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/attributes/methodaccess/NoOpMethodAccessValidatorTest.java ================================================ package io.pebbletemplates.pebble.attributes.methodaccess; import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.Test; public class NoOpMethodAccessValidatorTest { private final MethodAccessValidator underTest = new NoOpMethodAccessValidator(); @Test void whenIsMethodAccessAllowed_thenReturnTrue() { boolean methodAccessAllowed = this.underTest.isMethodAccessAllowed(null, null); assertThat(methodAccessAllowed).isTrue(); } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/extension/ArrayToStringFilter.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.extension; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.lang.reflect.Array; import java.util.List; import java.util.Map; /** * Pretty-printing of java arrays */ public class ArrayToStringFilter implements Filter { @Override public List getArgumentNames() { return null; } @Override public String apply(Object input, Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { if (input == null) { return null; } StringBuilder result = new StringBuilder("["); int length = Array.getLength(input); for (int i = 0; i < length; i++) { if (i > 0) { result.append(","); } result.append(Array.get(input, i)); } result.append("]"); return result.toString(); } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/extension/ExtensionCustomizerTest.java ================================================ package io.pebbletemplates.pebble.extension; import io.pebbletemplates.pebble.PebbleEngine; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.extension.core.DisallowExtensionCustomizerBuilder; import io.pebbletemplates.pebble.loader.StringLoader; import io.pebbletemplates.pebble.template.PebbleTemplate; import org.junit.jupiter.api.Test; import java.io.StringWriter; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Optional; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; class ExtensionCustomizerTest { @Test void upperFilterCannotBeUsed() { PebbleEngine pebble = new PebbleEngine.Builder() .loader(new StringLoader()) .registerExtensionCustomizer(RemoveUpperCustomizer::new) .build(); Map obj = new HashMap<>(); obj.put("test", "abc"); PebbleTemplate template = pebble.getTemplate("{{ test | upper }}"); PebbleException exception = assertThrows(PebbleException.class, () -> template.evaluate(new StringWriter(), obj)); assertTrue(exception.getMessage().contains("upper"), () -> "Expect upper-Filter to not exist, actual Problem: " + exception.getMessage()); } @Test void setDisallowedTokenParserTags() { PebbleEngine pebbleEngine = new PebbleEngine.Builder() .loader(new StringLoader()) .registerExtensionCustomizer(new DisallowExtensionCustomizerBuilder() .disallowedTokenParserTags(Collections.singletonList("flush")) .build()) .build(); PebbleException exception = assertThrows(PebbleException.class, () -> pebbleEngine.getTemplate("{{ k1 }}\n" + "{% flush %}\n" + "{{ k2 }}")); assertTrue(exception.getMessage().contains("Unexpected tag name \"flush\"")); } @Test void setDisallowedFilters() { PebbleEngine pebbleEngine = new PebbleEngine.Builder() .loader(new StringLoader()) .registerExtensionCustomizer(new DisallowExtensionCustomizerBuilder() .disallowedFilterKeys(Collections.singletonList("upper")) .build()) .build(); Map obj = new HashMap<>(); obj.put("test", "abc"); PebbleTemplate template = pebbleEngine.getTemplate("{{ test | upper }}"); PebbleException exception = assertThrows(PebbleException.class, () -> template.evaluate(new StringWriter(), obj)); assertTrue(exception.getMessage().contains("upper"), () -> "Expect upper-Filter to not exist, actual Problem: " + exception.getMessage()); } @Test void setDisallowedFunctions() { PebbleEngine pebbleEngine = new PebbleEngine.Builder() .loader(new StringLoader()) .registerExtensionCustomizer(new DisallowExtensionCustomizerBuilder() .disallowedFunctionKeys(Collections.singletonList("max")) .build()) .build(); Map obj = new HashMap<>(); obj.put("age", 30); PebbleTemplate template = pebbleEngine.getTemplate("{{ max(age, 80) }}"); PebbleException exception = assertThrows(PebbleException.class, () -> template.evaluate(new StringWriter(), obj)); assertTrue(exception.getMessage().contains("Function or Macro [max] does not exist")); } @Test void setDisallowedBinaryOperatorSymbols() { PebbleEngine pebbleEngine = new PebbleEngine.Builder() .loader(new StringLoader()) .registerExtensionCustomizer(new DisallowExtensionCustomizerBuilder() .disallowedBinaryOperatorSymbols(Collections.singletonList(">")) .build()) .build(); PebbleException exception = assertThrows(PebbleException.class, () -> pebbleEngine.getTemplate("{% if 10 > 9 %}\n" + "{{ name }}" + "{% endif %}")); assertTrue(exception.getMessage().contains("Unexpected character [>]")); } @Test void setDisallowedUnaryOperatorSymbols() { PebbleEngine pebbleEngine = new PebbleEngine.Builder() .loader(new StringLoader()) .registerExtensionCustomizer(new DisallowExtensionCustomizerBuilder() .disallowedUnaryOperatorSymbols(Collections.singletonList("-")) .build()) .build(); PebbleException exception = assertThrows(PebbleException.class, () -> pebbleEngine.getTemplate("{{ -num }}")); assertTrue(exception.getMessage().contains("Unexpected token \"OPERATOR\" of value \"-\"")); } @Test void setDisallowedTestKeys() { PebbleEngine pebbleEngine = new PebbleEngine.Builder() .loader(new StringLoader()) .registerExtensionCustomizer(new DisallowExtensionCustomizerBuilder() .disallowedTestKeys(Collections.singletonList("null")) .build()) .build(); PebbleTemplate template = pebbleEngine.getTemplate("{% if 123 is null %}\n" + "{{ name }}" + "{% endif %}"); PebbleException exception = assertThrows(PebbleException.class, () -> template.evaluate(new StringWriter())); assertTrue(exception.getMessage().contains("Wrong operand(s) type in conditional expression")); } private static class RemoveUpperCustomizer extends ExtensionCustomizer { public RemoveUpperCustomizer(Extension core) { super(core); } @Override public Map getFilters() { Map filters = Optional.ofNullable(super.getFilters()).map(HashMap::new) .orElseGet(HashMap::new); filters.remove("upper"); return filters; } } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/extension/InvocationCountingFunction.java ================================================ package io.pebbletemplates.pebble.extension; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.util.List; import java.util.Map; /** * This function will count how many times it's been invoked (just testing purposes). Not thread * safe. * * @author mbosecke */ public class InvocationCountingFunction implements Function { private int invocationCount = 0; @Override public List getArgumentNames() { return null; } @Override public Object execute(Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { return ++invocationCount; } public int getInvocationCount() { return invocationCount; } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/extension/ListToStringFilter.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.extension; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.util.List; import java.util.Map; /** * Pretty-printing of List, implementation independent */ public class ListToStringFilter implements Filter { @Override public List getArgumentNames() { return null; } @Override public String apply(Object input, Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) throws PebbleException { if (input == null) { return null; } if (!(input instanceof List)) { throw new PebbleException(null, "The 'listToString' filter expects that the input to be a list.", lineNumber, self.getName()); } List inputList = (List) input; StringBuilder result = new StringBuilder("["); for (int i = 0; i < inputList.size(); i++) { if (i > 0) { result.append(","); } result.append(inputList.get(i)); } result.append("]"); return result.toString(); } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/extension/MapToStringFilter.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.extension; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Map.Entry; /** * There were changes in how HashMaps are serialized to strings between Java 7 and 8 so this filter * ensures a standardized result that we can test with. * * @author mbosecke */ public class MapToStringFilter implements Filter { @Override public List getArgumentNames() { return null; } @SuppressWarnings({"unchecked"}) @Override public String apply(Object input, Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { if (input == null) { return null; } Map map = (Map) input; List pairs = new ArrayList<>(); for (Entry entry : map.entrySet()) { String pair = String.valueOf(entry.getKey()) + "=" + String.valueOf(entry.getValue()); pairs.add(pair); } Collections.sort(pairs); StringBuilder result = new StringBuilder("{"); for (String pair : pairs) { result.append(pair).append(", "); } result.setLength(result.length() - 2); // remove extra comma and // space result.append("}"); return result.toString(); } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/extension/TestingExtension.java ================================================ package io.pebbletemplates.pebble.extension; import java.util.HashMap; import java.util.Map; public class TestingExtension extends AbstractExtension { private InvocationCountingFunction invocationCountingFunction = new InvocationCountingFunction(); @Override public Map getFunctions() { Map functions = new HashMap<>(); functions.put("invocationCountingFunction", this.invocationCountingFunction); return functions; } @Override public Map getFilters() { Map filters = new HashMap<>(); filters.put("mapToString", new MapToStringFilter()); filters.put("listToString", new ListToStringFilter()); filters.put("arrayToString", new ArrayToStringFilter()); return filters; } public InvocationCountingFunction getInvocationCountingFunction() { return this.invocationCountingFunction; } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/extension/core/FormatFilterTest.java ================================================ package io.pebbletemplates.pebble.extension.core; import io.pebbletemplates.pebble.PebbleEngine; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.loader.StringLoader; import io.pebbletemplates.pebble.template.PebbleTemplate; import org.junit.jupiter.api.Test; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.util.HashMap; import java.util.MissingFormatArgumentException; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; class FormatFilterTest { @Test void itShouldThrowExceptionWhenNotEnoughArguments() { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ 'I need %s and %s and %s' | format('one', 'two') }}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); assertThatExceptionOfType(MissingFormatArgumentException.class) .isThrownBy(() -> template.evaluate(writer, new HashMap<>())); } @Test void itShouldReturnNullWhenInputIsNull() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ null | format('test') }}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertThat(writer.toString()).isEmpty(); } @Test void itShouldHandleNoArguments() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ 'Just a string with no placeholders' | format() }}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertThat(writer.toString()).isEqualTo("Just a string with no placeholders"); } @Test void itShouldFormatStringWithSingleArgument() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ 'Hello %s!' | format('World') }}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertThat(writer.toString()).isEqualTo("Hello World!"); } @Test void itShouldFormatStringWithVariable() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{% set name = 'World' %}{{ 'Hello %s!' | format(name) }}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertThat(writer.toString()).isEqualTo("Hello World!"); } @Test void itShouldFormatWithDifferentTypes() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ 'Number: %d, Float: %.2f, String: %s' | format(42, 3.14159, 'test') }}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertThat(writer.toString()).isEqualTo("Number: 42, Float: 3.14, String: test"); } @Test void itShouldIgnoreExtraArguments() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ 'I only need %s' | format('one', 'two', 'three') }}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertThat(writer.toString()).isEqualTo("I only need one"); } @Test void itShouldHandleNullArgument() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ 'Value is: %s' | format(null) }}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertThat(writer.toString()).isEqualTo("Value is: null"); } @Test void itShouldHandleEscapedPercentSign() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ '100%% complete: %s' | format('done') }}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertThat(writer.toString()).isEqualTo("100% complete: done"); } @Test void itShouldFormatWithBooleanValues() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ 'Enabled: %s, Disabled: %s' | format(true, false) }}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertThat(writer.toString()).isEqualTo("Enabled: true, Disabled: false"); } @Test void itShouldFormatEmptyString() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ '' | format('ignored') }}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertThat(writer.toString()).isEmpty(); } @Test void itShouldFormatWithMixedArgumentTypes() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); String source = "{{ 'Count: %d items, Price: $%.2f, Name: %s' | format(5, 19.99, 'Widget') }}"; PebbleTemplate template = pebble.getTemplate(source); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertThat(writer.toString()).isEqualTo("Count: 5 items, Price: $19.99, Name: Widget"); } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/extension/escaper/RawFilterTest.java ================================================ package io.pebbletemplates.pebble.extension.escaper; import io.pebbletemplates.pebble.loader.StringLoader; import io.pebbletemplates.pebble.PebbleEngine; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.template.PebbleTemplate; import org.junit.jupiter.api.Test; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.util.HashMap; import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; class RawFilterTest { @Test void testRawFilter() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ text | upper | raw }}"); Map context = new HashMap<>(); context.put("text", "
    "); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("
    ", writer.toString()); } @Test void testRawFilterNotBeingLast() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ text | raw | upper}}"); Map context = new HashMap<>(); context.put("text", "
    "); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("<BR />", writer.toString()); } @Test void testRawFilterWithinAutoescapeToken() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false) .autoEscaping(false).build(); PebbleTemplate template = pebble .getTemplate("{% autoescape 'html' %}{{ text|raw }}{% endautoescape %}"); Map context = new HashMap<>(); context.put("text", "
    "); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("
    ", writer.toString()); } @Test void testRawFilterWithJsonObject() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ text | raw }}"); Map context = new HashMap<>(); context.put("text", new JsonObject()); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals(JsonObject.JSON_VALUE, writer.toString()); } @Test void testRawFilterWithNullObject() throws PebbleException, IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); PebbleTemplate template = pebble.getTemplate("{{ text | raw }}"); Writer writer = new StringWriter(); template.evaluate(writer, new HashMap<>()); assertEquals("", writer.toString()); } private class JsonObject { public static final String JSON_VALUE = "{\"menu\": {\"id\": \"file\",\"value\": \"File\",\"popup\": {\"menuitem\": [{\"value\": \"New\", \"onclick\": \"CreateNewDoc()\"},{\"value\": \"Open\", \"onclick\": \"OpenDoc()\"},{\"value\": \"Close\", \"onclick\": \"CloseDoc()\"}]}}}"; @Override public String toString() { return JSON_VALUE; } } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/lexer/IdentifierTest.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.lexer; import io.pebbletemplates.pebble.error.ParserException; import io.pebbletemplates.pebble.loader.StringLoader; import io.pebbletemplates.pebble.PebbleEngine; import org.junit.jupiter.api.Test; import java.io.IOException; import java.io.StringWriter; import java.util.HashMap; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; public class IdentifierTest { @Test void common() throws IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()).strictVariables(false).build(); HashMap context = new HashMap<>(); context.put("fromContext", "ing"); StringWriter writer = new StringWriter(); pebble.getTemplate("{% set hello1 = \"test\" %}{{ hello1 }}{{ fromContext }}").evaluate(writer, context); assertEquals("testing", writer.toString()); } @Test void digitStart() throws IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()).strictVariables(false).build(); StringWriter writer = new StringWriter(); assertThrows(ParserException.class, () -> pebble.getTemplate("{{ 0digit }}").evaluate(writer)); } @Test void extendedLatin() throws IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()).strictVariables(false).build(); HashMap context = new HashMap<>(); context.put("éabcêwçłë_0", "test string"); StringWriter writer = new StringWriter(); pebble.getTemplate("{{ éabcêwçłë_0 }}").evaluate(writer, context); assertEquals("test string", writer.toString()); } @Test void arabic() throws IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()).strictVariables(false).build(); HashMap context = new HashMap<>(); context.put("شششش", "test string"); StringWriter writer = new StringWriter(); pebble.getTemplate("{{ شششش }}").evaluate(writer, context); assertEquals("test string", writer.toString()); } @Test void chinese() throws IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()).strictVariables(false).build(); HashMap context = new HashMap<>(); context.put("水", "test string"); StringWriter writer = new StringWriter(); pebble.getTemplate("{{ 水 }}").evaluate(writer, context); assertEquals("test string", writer.toString()); } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/lexer/LexerImplTest.java ================================================ package io.pebbletemplates.pebble.lexer; import io.pebbletemplates.pebble.extension.ExtensionRegistry; import io.pebbletemplates.pebble.extension.core.CoreExtension; import io.pebbletemplates.pebble.loader.Loader; import io.pebbletemplates.pebble.loader.StringLoader; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.Reader; import static org.assertj.core.api.Assertions.assertThat; class LexerImplTest { @SuppressWarnings("unused") private final Logger logger = LoggerFactory.getLogger(LexerImplTest.class); /** * The Lexer Implementation under test. */ private LexerImpl lexer; // Don't really care what the template name is, but need to have it. // Defined here to remove clutter from the tests private final String TEMPLATE_NAME = "Template Name"; /** * Configure and instantiate a Lexer instance before each test. */ @BeforeEach void setup() { Syntax syntax = new Syntax.Builder().setEnableNewLineTrimming(false).build(); ExtensionRegistry extensionRegistry = new ExtensionRegistry(); extensionRegistry.addExtension(new CoreExtension()); this.lexer = new LexerImpl(syntax, extensionRegistry.getUnaryOperators().values(), extensionRegistry.getBinaryOperators().values()); } /** * Test Tokenizing text. */ @Test void testTokenizeText() { Loader loader = new StringLoader(); Reader templateReader = loader.getReader(" template content "); TokenStream tokenStream = this.lexer.tokenize(templateReader, this.TEMPLATE_NAME); assertThat(tokenStream.peek(0).getType()).isEqualTo(Token.Type.TEXT); assertThat(tokenStream.peek(0).getValue()).isEqualTo(" template content "); assertThat(tokenStream.peek(1).getType()).isEqualTo(Token.Type.EOF); } /** * Test tokenizing an expression, e.g. {{ expression }} */ @Test void testTokenizeExpression() { Loader loader = new StringLoader(); Reader templateReader = loader.getReader("{{ whatever }}"); TokenStream tokenStream = this.lexer.tokenize(templateReader, this.TEMPLATE_NAME); assertThat(tokenStream.peek(0).getType()).isEqualTo(Token.Type.PRINT_START); assertThat(tokenStream.peek(0).getValue()).isNull(); assertThat(tokenStream.peek(1).getType()).isEqualTo(Token.Type.NAME); assertThat(tokenStream.peek(1).getValue()).isEqualTo("whatever"); assertThat(tokenStream.peek(2).getType()).isEqualTo(Token.Type.PRINT_END); assertThat(tokenStream.peek(2).getValue()).isEqualTo("}}"); assertThat(tokenStream.peek(3).getType()).isEqualTo(Token.Type.EOF); assertThat(tokenStream.peek(3).getValue()).isNull(); } /** * Test tokenizing an expression, e.g. {{ expression }} */ @Test void testVariableNameStartingWithOperator() { Loader loader = new StringLoader(); Reader templateReader = loader.getReader("{{ is_active + contains0 }}"); TokenStream tokenStream = this.lexer.tokenize(templateReader, this.TEMPLATE_NAME); int i = 0; assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.PRINT_START); assertThat(tokenStream.peek(i++).getValue()).isNull(); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.NAME); assertThat(tokenStream.peek(i++).getValue()).isEqualTo("is_active"); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.OPERATOR); assertThat(tokenStream.peek(i++).getValue()).isEqualTo("+"); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.NAME); assertThat(tokenStream.peek(i++).getValue()).isEqualTo("contains0"); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.PRINT_END); assertThat(tokenStream.peek(i++).getValue()).isEqualTo("}}"); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.EOF); assertThat(tokenStream.peek(i++).getValue()).isNull(); } /** * Test tokenizing Punctuation, such as the dot in item.itemType */ @Test void testPunctuation() { StringBuilder stringBuilder = new StringBuilder(); stringBuilder .append("{% if item.itemType %}"); Loader loader = new StringLoader(); Reader templateReader = loader.getReader(stringBuilder.toString()); TokenStream tokenStream = this.lexer.tokenize(templateReader, this.TEMPLATE_NAME); int i = 0; assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.EXECUTE_START); assertThat(tokenStream.peek(i++).getValue()).isNull(); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.NAME); assertThat(tokenStream.peek(i++).getValue()).isEqualTo("if"); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.NAME); assertThat(tokenStream.peek(i++).getValue()).isEqualTo("item"); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.PUNCTUATION); assertThat(tokenStream.peek(i++).getValue()).isEqualTo("."); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.NAME); assertThat(tokenStream.peek(i++).getValue()).isEqualTo("itemType"); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.EXECUTE_END); assertThat(tokenStream.peek(i++).getValue()).isEqualTo("%}"); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.EOF); assertThat(tokenStream.peek(i++).getValue()).isNull(); } /** * Test tokenizing an if statement that includes an operation and a String token */ @Test void testIfStatementWithOperatorAndStringToken() { StringBuilder stringBuilder = new StringBuilder(); stringBuilder .append("{% if item equals \"string1\" %}"); Loader loader = new StringLoader(); Reader templateReader = loader.getReader(stringBuilder.toString()); TokenStream tokenStream = this.lexer.tokenize(templateReader, this.TEMPLATE_NAME); int i = 0; assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.EXECUTE_START); assertThat(tokenStream.peek(i++).getValue()).isNull(); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.NAME); assertThat(tokenStream.peek(i++).getValue()).isEqualTo("if"); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.NAME); assertThat(tokenStream.peek(i++).getValue()).isEqualTo("item"); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.OPERATOR); assertThat(tokenStream.peek(i++).getValue()).isEqualTo("equals"); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.STRING); assertThat(tokenStream.peek(i++).getValue()).isEqualTo("string1"); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.EXECUTE_END); assertThat(tokenStream.peek(i++).getValue()).isEqualTo("%}"); } /** * Test tokenize an "if" statement */ @Test void testIfStatement() { StringBuilder stringBuilder = new StringBuilder(); stringBuilder .append("{% if item equals \"whatever\" %}\n") .append("some text\n") .append("{% endif %}"); Loader loader = new StringLoader(); Reader templateReader = loader.getReader(stringBuilder.toString()); TokenStream tokenStream = this.lexer.tokenize(templateReader, this.TEMPLATE_NAME); int i = 0; assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.EXECUTE_START); assertThat(tokenStream.peek(i++).getValue()).isNull(); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.NAME); assertThat(tokenStream.peek(i++).getValue()).isEqualTo("if"); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.NAME); assertThat(tokenStream.peek(i++).getValue()).isEqualTo("item"); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.OPERATOR); assertThat(tokenStream.peek(i++).getValue()).isEqualTo("equals"); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.STRING); assertThat(tokenStream.peek(i++).getValue()).isEqualTo("whatever"); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.EXECUTE_END); assertThat(tokenStream.peek(i++).getValue()).isEqualTo("%}"); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.TEXT); assertThat(tokenStream.peek(i++).getValue()).isEqualTo("\nsome text\n"); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.EXECUTE_START); assertThat(tokenStream.peek(i++).getValue()).isNull(); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.NAME); assertThat(tokenStream.peek(i++).getValue()).isEqualTo("endif"); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.EXECUTE_END); assertThat(tokenStream.peek(i++).getValue()).isEqualTo("%}"); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.EOF); assertThat(tokenStream.peek(i++).getValue()).isNull(); } /** * Test tokenizing a For Loop */ @Test void testTokenizeForLoop() { StringBuilder stringBuilder = new StringBuilder(); stringBuilder .append("{% for item in items %}\n") .append("stuff\n") .append("{% endfor %}"); Loader loader = new StringLoader(); Reader templateReader = loader.getReader(stringBuilder.toString()); TokenStream tokenStream = this.lexer.tokenize(templateReader, this.TEMPLATE_NAME); assertThat(tokenStream.peek(0).getType()).isEqualTo(Token.Type.EXECUTE_START); assertThat(tokenStream.peek(0).getValue()).isNull(); assertThat(tokenStream.peek(1).getType()).isEqualTo(Token.Type.NAME); assertThat(tokenStream.peek(1).getValue()).isEqualTo("for"); assertThat(tokenStream.peek(2).getType()).isEqualTo(Token.Type.NAME); assertThat(tokenStream.peek(2).getValue()).isEqualTo("item"); assertThat(tokenStream.peek(3).getType()).isEqualTo(Token.Type.NAME); assertThat(tokenStream.peek(3).getValue()).isEqualTo("in"); assertThat(tokenStream.peek(4).getType()).isEqualTo(Token.Type.NAME); assertThat(tokenStream.peek(4).getValue()).isEqualTo("items"); assertThat(tokenStream.peek(5).getType()).isEqualTo(Token.Type.EXECUTE_END); assertThat(tokenStream.peek(5).getValue()).isEqualTo("%}"); assertThat(tokenStream.peek(6).getType()).isEqualTo(Token.Type.TEXT); assertThat(tokenStream.peek(6).getValue()).isEqualTo("\nstuff\n"); assertThat(tokenStream.peek(7).getType()).isEqualTo(Token.Type.EXECUTE_START); assertThat(tokenStream.peek(7).getValue()).isNull(); assertThat(tokenStream.peek(8).getType()).isEqualTo(Token.Type.NAME); assertThat(tokenStream.peek(8).getValue()).isEqualTo("endfor"); assertThat(tokenStream.peek(9).getType()).isEqualTo(Token.Type.EXECUTE_END); assertThat(tokenStream.peek(9).getValue()).isEqualTo("%}"); assertThat(tokenStream.peek(10).getType()).isEqualTo(Token.Type.EOF); assertThat(tokenStream.peek(10).getValue()).isNull(); } /** * Test tokenizing an if statement with a Whitespace Control character, i.e. the "-" */ @Test void testIfStatementWithWhitespaceControl() { StringBuilder stringBuilder = new StringBuilder(); stringBuilder .append("{% if item equals \"whatever\" -%}\n") .append("some text\n") .append("{%- endif %}"); Loader loader = new StringLoader(); Reader templateReader = loader.getReader(stringBuilder.toString()); TokenStream tokenStream = this.lexer.tokenize(templateReader, this.TEMPLATE_NAME); int i = 0; assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.EXECUTE_START); assertThat(tokenStream.peek(i++).getValue()).isNull(); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.NAME); assertThat(tokenStream.peek(i++).getValue()).isEqualTo("if"); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.NAME); assertThat(tokenStream.peek(i++).getValue()).isEqualTo("item"); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.OPERATOR); assertThat(tokenStream.peek(i++).getValue()).isEqualTo("equals"); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.STRING); assertThat(tokenStream.peek(i++).getValue()).isEqualTo("whatever"); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.EXECUTE_END); assertThat(tokenStream.peek(i++).getValue()).isEqualTo("%}"); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.TEXT); assertThat(tokenStream.peek(i++).getValue()).isEqualTo("some text"); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.EXECUTE_START); assertThat(tokenStream.peek(i++).getValue()).isNull(); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.NAME); assertThat(tokenStream.peek(i++).getValue()).isEqualTo("endif"); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.EXECUTE_END); assertThat(tokenStream.peek(i++).getValue()).isEqualTo("%}"); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.EOF); assertThat(tokenStream.peek(i++).getValue()).isNull(); } /** * Test a combination of template syntax to demonstrate a complex token stream */ @Test void testComplexTemplate() { StringBuilder stringBuilder = new StringBuilder(); stringBuilder .append("text before for loop followed by blank line\n") .append("{% for item in items %}\n") .append("{% if item.itemType equals \"ITEM_TYPE1\" -%}\n") .append("Item 1\n") .append("{% elseif item.itemType equals \"ITEM_TYPE2\" -%}\n") .append("Item 2\n") .append("{%- endif -%}") .append("{% endfor -%}") .append("text after for loop preceded by blank line"); Loader loader = new StringLoader(); Reader templateReader = loader.getReader(stringBuilder.toString()); TokenStream tokenStream = this.lexer.tokenize(templateReader, this.TEMPLATE_NAME); int i = 0; assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.TEXT); assertThat(tokenStream.peek(i++).getValue()).isEqualTo("text before for loop followed by blank line\n"); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.EXECUTE_START); assertThat(tokenStream.peek(i++).getValue()).isNull(); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.NAME); assertThat(tokenStream.peek(i++).getValue()).isEqualTo("for"); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.NAME); assertThat(tokenStream.peek(i++).getValue()).isEqualTo("item"); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.NAME); assertThat(tokenStream.peek(i++).getValue()).isEqualTo("in"); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.NAME); assertThat(tokenStream.peek(i++).getValue()).isEqualTo("items"); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.EXECUTE_END); assertThat(tokenStream.peek(i++).getValue()).isEqualTo("%}"); // note that the new line character included as part of a TEXT token assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.TEXT); assertThat(tokenStream.peek(i++).getValue()).isEqualTo("\n"); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.EXECUTE_START); assertThat(tokenStream.peek(i++).getValue()).isNull(); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.NAME); assertThat(tokenStream.peek(i++).getValue()).isEqualTo("if"); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.NAME); assertThat(tokenStream.peek(i++).getValue()).isEqualTo("item"); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.PUNCTUATION); assertThat(tokenStream.peek(i++).getValue()).isEqualTo("."); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.NAME); assertThat(tokenStream.peek(i++).getValue()).isEqualTo("itemType"); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.OPERATOR); assertThat(tokenStream.peek(i++).getValue()).isEqualTo("equals"); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.STRING); assertThat(tokenStream.peek(i++).getValue()).isEqualTo("ITEM_TYPE1"); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.EXECUTE_END); assertThat(tokenStream.peek(i++).getValue()).isEqualTo("%}"); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.TEXT); assertThat(tokenStream.peek(i++).getValue()).isEqualTo("Item 1\n"); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.EXECUTE_START); assertThat(tokenStream.peek(i++).getValue()).isNull(); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.NAME); assertThat(tokenStream.peek(i++).getValue()).isEqualTo("elseif"); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.NAME); assertThat(tokenStream.peek(i++).getValue()).isEqualTo("item"); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.PUNCTUATION); assertThat(tokenStream.peek(i++).getValue()).isEqualTo("."); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.NAME); assertThat(tokenStream.peek(i++).getValue()).isEqualTo("itemType"); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.OPERATOR); assertThat(tokenStream.peek(i++).getValue()).isEqualTo("equals"); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.STRING); assertThat(tokenStream.peek(i++).getValue()).isEqualTo("ITEM_TYPE2"); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.EXECUTE_END); assertThat(tokenStream.peek(i++).getValue()).isEqualTo("%}"); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.TEXT); assertThat(tokenStream.peek(i++).getValue()).isEqualTo("Item 2"); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.EXECUTE_START); assertThat(tokenStream.peek(i++).getValue()).isNull(); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.NAME); assertThat(tokenStream.peek(i++).getValue()).isEqualTo("endif"); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.EXECUTE_END); assertThat(tokenStream.peek(i++).getValue()).isEqualTo("%}"); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.EXECUTE_START); assertThat(tokenStream.peek(i++).getValue()).isNull(); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.NAME); assertThat(tokenStream.peek(i++).getValue()).isEqualTo("endfor"); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.EXECUTE_END); assertThat(tokenStream.peek(i++).getValue()).isEqualTo("%}"); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.TEXT); assertThat(tokenStream.peek(i++).getValue()).isEqualTo("text after for loop preceded by blank line"); assertThat(tokenStream.peek(i).getType()).isEqualTo(Token.Type.EOF); assertThat(tokenStream.peek(i++).getValue()).isNull(); } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/lexer/SyntaxTest.java ================================================ package io.pebbletemplates.pebble.lexer; import static org.assertj.core.api.Assertions.assertThat; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; // NOTE: Use regex101.com to test regular expressions through a web site class SyntaxTest { private static final String POSSIBLE_NEW_LINE = "(\r\n|\n\r|\r|\n|\u0085|\u2028|\u2029)?"; @SuppressWarnings("unused") private final Logger logger = LoggerFactory.getLogger(SyntaxTest.class); private Syntax syntax; @BeforeEach void setup() { this.syntax = new Syntax.Builder().build(); } @Test void testDelimiters() { assertThat(this.syntax.getCommentOpenDelimiter()).isEqualTo("{#"); assertThat(this.syntax.getCommentCloseDelimiter()).isEqualTo("#}"); assertThat(this.syntax.getExecuteOpenDelimiter()).isEqualTo("{%"); assertThat(this.syntax.getExecuteCloseDelimiter()).isEqualTo("%}"); assertThat(this.syntax.getPrintOpenDelimiter()).isEqualTo("{{"); assertThat(this.syntax.getPrintCloseDelimiter()).isEqualTo("}}"); assertThat(this.syntax.getInterpolationCloseDelimiter()).isEqualTo("}"); assertThat(this.syntax.getInterpolationOpenDelimiter()).isEqualTo("#{"); assertThat(this.syntax.getWhitespaceTrim()).isEqualTo("-"); } @Test void tesCommentCloseDelimiter() { assertThat(this.syntax.getCommentCloseDelimiter()).isEqualTo("#}"); } @Test void tesCommentOpenDelimiter() { assertThat(this.syntax.getCommentOpenDelimiter()).isEqualTo("{#"); } @Test void testExecuteOpenDelimiter() { assertThat(this.syntax.getExecuteOpenDelimiter()).isEqualTo("{%"); } @Test void testExecuteCloseDelimiter() { assertThat(this.syntax.getExecuteCloseDelimiter()).isEqualTo("%}"); } @Test void testPrintOpenDelimiter() { assertThat(this.syntax.getPrintCloseDelimiter()).isEqualTo("}}"); } @Test void testPrintCloseDelimiter() { assertThat(this.syntax.getPrintCloseDelimiter()).isEqualTo("}}"); } @Test void testInterpolationCloseDelimiter() { assertThat(this.syntax.getInterpolationCloseDelimiter()).isEqualTo("}"); } @Test void testInterpolationOpenDelimiter() { assertThat(this.syntax.getInterpolationOpenDelimiter()).isEqualTo("#{"); } @Test void testWhitespaceTrim() { assertThat(this.syntax.getWhitespaceTrim()).isEqualTo("-"); } @Test void testTrailingWhitespaceTrimRegex() { Pattern pattern = syntax.getRegexTrailingWhitespaceTrim(); /* matching from start of string, zero of more space characters, followed by a dash, followed by one of the following: "}}", "%}", or "#}" */ String expectedPatternString = "^\\s*\\Q-\\E(\\Q}}\\E|\\Q%}\\E|\\Q#}\\E)"; // verify that the TrailingWhitepaceTrim regex in the Syntax class is the expected pattern string assertThat(pattern.toString()).isEqualTo(expectedPatternString); StringBuilder templateText = null; Matcher whitespaceTrimMatcher = null; // Whitespace Trim character with Execution Close Delimiter should match templateText = new StringBuilder().append("-%}"); whitespaceTrimMatcher = pattern.matcher(templateText); assertThat(whitespaceTrimMatcher.lookingAt()).isEqualTo(true); // Whitespace Trim character with Print Close Delimiter should match templateText = new StringBuilder().append("-}}"); whitespaceTrimMatcher = pattern.matcher(templateText); assertThat(whitespaceTrimMatcher.lookingAt()).isEqualTo(true); // leading space characters with Whitespace Trim character with Execution Close Delimiter should match templateText = new StringBuilder().append(" -%}"); whitespaceTrimMatcher = pattern.matcher(templateText); assertThat(whitespaceTrimMatcher.lookingAt()).isEqualTo(true); // Whitespace Trim character with Comment Close Delimiter should match templateText = new StringBuilder().append("-#}"); whitespaceTrimMatcher = pattern.matcher(templateText); assertThat(whitespaceTrimMatcher.lookingAt()).isEqualTo(true); // End of expression without Whitespace Trim character should not match templateText = new StringBuilder().append("%}"); whitespaceTrimMatcher = pattern.matcher(templateText); assertThat(whitespaceTrimMatcher.lookingAt()).isEqualTo(false); // Leading non space characters should not match templateText = new StringBuilder().append("abcd -%}"); whitespaceTrimMatcher = pattern.matcher(templateText); assertThat(whitespaceTrimMatcher.lookingAt()).isEqualTo(false); } @Test void testLeadingWhitespaceTrimRegex() { Pattern pattern = syntax.getRegexLeadingWhitespaceTrim(); /* "-" followed by one more whitespace characters */ String expectedPatternString = "\\Q-\\E\\s+"; // verify that the regex in the Syntax class is the expected pattern string assertThat(pattern.toString()).isEqualTo(expectedPatternString); StringBuilder templateText = null; Matcher matcher = null; // Dash followed by spaces should match templateText = new StringBuilder().append("- "); matcher = pattern.matcher(templateText); assertThat(matcher.lookingAt()).isEqualTo(true); // Dash character followed by whitespace characters should match templateText = new StringBuilder().append("- \n\r\t"); matcher = pattern.matcher(templateText); assertThat(matcher.lookingAt()).isEqualTo(true); // Trailing non-whitespace after whitespace characters should match templateText = new StringBuilder().append("- abcd"); matcher = pattern.matcher(templateText); assertThat(matcher.lookingAt()).isEqualTo(true); // No leading dash character should not match templateText = new StringBuilder().append(" "); matcher = pattern.matcher(templateText); assertThat(matcher.lookingAt()).isEqualTo(false); // No trailing space character should not match templateText = new StringBuilder().append("-"); matcher = pattern.matcher(templateText); assertThat(matcher.lookingAt()).isEqualTo(false); // Leading characters should not match templateText = new StringBuilder().append(" abcd - }"); matcher = pattern.matcher(templateText); assertThat(matcher.lookingAt()).isEqualTo(false); } @Test void testRegexVerbatimEnd() { Pattern pattern = this.syntax.getRegexVerbatimEnd(); /* "{%" followed by an optional "-" and/or zero or more whitespace characters, followed by "endverbatim", followed by zero or more whitespace characters and/or an optional "-" followed by "%}" followed by an optional possible new line character */ String expectedPatternString = "\\Q{%\\E(\\Q-\\E)?\\s*endverbatim\\s*(\\Q-\\E)?\\Q%}\\E" + POSSIBLE_NEW_LINE; // verify that the regex in the Syntax class is the expected pattern string assertThat(pattern.toString()).isEqualTo(expectedPatternString); StringBuilder templateText = null; Matcher matcher = null; // Space and whitespacetrim characters should match templateText = new StringBuilder().append("{%- endverbatim -%}abcd\r\n"); matcher = pattern.matcher(templateText); assertThat(matcher.lookingAt()).isEqualTo(true); // No spaces or whitespacetrim characters should match templateText = new StringBuilder().append("{%endverbatim%}"); matcher = pattern.matcher(templateText); assertThat(matcher.lookingAt()).isEqualTo(true); // Missing delimiters should not match templateText = new StringBuilder().append("endverbatim"); matcher = pattern.matcher(templateText); assertThat(matcher.lookingAt()).isEqualTo(false); } @Test void testRegexStartDelimiters() { Pattern pattern = this.syntax.getRegexStartDelimiters(); /* Start delimiters must match "{{" or "{%" or "{#" */ String expectedPatternString = "\\Q{{\\E|\\Q{%\\E|\\Q{#\\E"; // verify that the regex in the Syntax class is the expected pattern string assertThat(pattern.toString()).isEqualTo(expectedPatternString); StringBuilder templateText = null; Matcher matcher = null; // "{{", "{%", and "{#" should match templateText = new StringBuilder().append("{{"); matcher = pattern.matcher(templateText); assertThat(matcher.lookingAt()).isEqualTo(true); templateText = new StringBuilder().append("{%"); matcher = pattern.matcher(templateText); assertThat(matcher.lookingAt()).isEqualTo(true); templateText = new StringBuilder().append("{#"); matcher = pattern.matcher(templateText); assertThat(matcher.lookingAt()).isEqualTo(true); // Text after a start delimiter should match templateText = new StringBuilder().append("{{ abcd"); matcher = pattern.matcher(templateText); assertThat(matcher.lookingAt()).isEqualTo(true); // Text before the start delimiter should not match templateText = new StringBuilder().append("abcd {{"); matcher = pattern.matcher(templateText); assertThat(matcher.lookingAt()).isEqualTo(false); // No start delimiter should not match templateText = new StringBuilder().append("abcd"); matcher = pattern.matcher(templateText); assertThat(matcher.lookingAt()).isEqualTo(false); } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/macro/MacroGlobalVariablesTest.java ================================================ package io.pebbletemplates.pebble.macro; import io.pebbletemplates.pebble.extension.AbstractExtension; import io.pebbletemplates.pebble.loader.StringLoader; import io.pebbletemplates.pebble.PebbleEngine; import org.junit.jupiter.api.Test; import java.io.IOException; import java.io.StringWriter; import java.util.HashMap; import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; public class MacroGlobalVariablesTest { public static final class Extension extends AbstractExtension { @Override public Map getGlobalVariables() { HashMap map = new HashMap<>(); map.put("someGlobalValue", 18181); return map; } } @Test void test() throws IOException { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()).strictVariables(true).extension(new Extension()).build(); StringWriter writer = new StringWriter(); pebble.getTemplate("{% macro m() %}{{ someGlobalValue }}{% endmacro %}{{ m() }}").evaluate(writer); assertEquals("18181", writer.toString()); } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/macro/PebbleExtension.java ================================================ package io.pebbletemplates.pebble.macro; import io.pebbletemplates.pebble.extension.AbstractExtension; import io.pebbletemplates.pebble.extension.Filter; import java.util.HashMap; import java.util.Map; public class PebbleExtension extends AbstractExtension { @Override public Map getFilters() { Map f = new HashMap<>(); f.put(TestFilter.FILTER_NAME, new TestFilter()); return f; } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/macro/TestFilter.java ================================================ package io.pebbletemplates.pebble.macro; import io.pebbletemplates.pebble.extension.Filter; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.util.Collections; import java.util.List; import java.util.Map; public class TestFilter implements Filter { static int counter = 0; public static final String FILTER_NAME = "testfilter"; @Override public List getArgumentNames() { return Collections.singletonList("content"); } @Override public Object apply(Object input, Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { String content = (String) input; counter++; content = content + "?" + "Hello"; return content; } public static int getCounter() { return counter; } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/macro/TestMacroCalls.java ================================================ package io.pebbletemplates.pebble.macro; import io.pebbletemplates.pebble.PebbleEngine; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.template.PebbleTemplate; import org.junit.jupiter.api.Test; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; /** * Test calls of macros */ class TestMacroCalls { /** * Checks, if macros are called to often */ @Test void testMultipleMacroCalls() throws PebbleException, IOException { // Build a pebble engine with one configured filter ("testfilter" - TestFilter.java) PebbleEngine pebble = new PebbleEngine.Builder() .extension(new PebbleExtension()) .strictVariables(false).build(); // Resets the counter of the filter TestFilter.counter = 0; /* * Runs the test scenario: * index-template with an import of the macro and a call to this macro (Call #1) * index-templates includes "include.peb" * * include.peb with an import of the macro and a call to this macro (Call #2) * * We track the number of macro-calls by using a small "Filter" (TestFilter) that just * counts, how often it is called. */ PebbleTemplate template = pebble.getTemplate("templates/macros/index.peb"); Writer writer = new StringWriter(); template.evaluate(writer); // We expect, that the TestFilter was called 2x assertEquals(2, TestFilter.getCounter()); } @Test void testMacroCallsWithImportAs() throws PebbleException, IOException { // Build a pebble engine with one configured filter ("testfilter" - TestFilter.java) PebbleEngine pebble = new PebbleEngine.Builder() .extension(new PebbleExtension()) .strictVariables(false).build(); // Resets the counter of the filter TestFilter.counter = 0; /* * Runs the test scenario: * import.as-template with an import-as of the macro and a call to this macro (Call #1) * import.as-templates includes "include.peb" * * include.peb with an import of the macro and a call to this macro (Call #2) * * We track the number of macro-calls by using a small "Filter" (TestFilter) that just * counts, how often it is called. */ PebbleTemplate template = pebble.getTemplate("templates/macros/import.as.peb"); Writer writer = new StringWriter(); template.evaluate(writer); // We expect, that the TestFilter was called 2x assertEquals(2, TestFilter.getCounter()); } @Test void testMacroCallsWithFromToken() throws PebbleException, IOException { // Build a pebble engine with one configured filter ("testfilter" - TestFilter.java) PebbleEngine pebble = new PebbleEngine.Builder() .extension(new PebbleExtension()) .strictVariables(false).build(); // Resets the counter of the filter TestFilter.counter = 0; /* * Runs the test scenario: * from-template with an from-import-as of these macro and a call to these macro (Call #1, Call #3) * from-templates includes "include.peb" * * include.peb with an import of the macro and a call to this macro (Call #2) * * We track the number of macro-calls by using a small "Filter" (TestFilter) that just * counts, how often it is called. */ PebbleTemplate template = pebble.getTemplate("templates/macros/from.peb"); Writer writer = new StringWriter(); template.evaluate(writer); // We expect, that the TestFilter was called 3x assertEquals(3, TestFilter.getCounter()); } @Test void testInvalidMacroWithFromToken() throws IOException { PebbleEngine pebble = new PebbleEngine.Builder().build(); try { PebbleTemplate template = pebble.getTemplate("templates/macros/invalid.from.peb"); Writer writer = new StringWriter(); template.evaluate(writer); fail("expected PebbleException"); } catch (PebbleException e) { assertEquals(e.getLineNumber(), (Integer) 3); assertEquals(e.getFileName(), "templates/macros/invalid.from.peb"); } } @Test void testInvalidMacro() throws IOException { PebbleEngine pebble = new PebbleEngine.Builder().build(); try { PebbleTemplate template = pebble.getTemplate("templates/macros/invalid.macro.peb"); Writer writer = new StringWriter(); template.evaluate(writer); fail("expected PebbleException"); } catch (PebbleException e) { assertEquals(e.getLineNumber(), (Integer) 2); assertEquals(e.getFileName(), "templates/macros/invalid.macro.peb"); } } @Test void testInvalidSameAliasMacroWithFromToken() throws IOException { PebbleEngine pebble = new PebbleEngine.Builder().build(); try { PebbleTemplate template = pebble.getTemplate("templates/macros/invalid.from.sameAlias.peb"); Writer writer = new StringWriter(); template.evaluate(writer); fail("expected PebbleException"); } catch (PebbleException e) { assertTrue( e.getPebbleMessage().startsWith("More than one macro can not share the same name")); } } @Test void testInvalidSameAliasMacroWithImportAsToken() throws IOException { PebbleEngine pebble = new PebbleEngine.Builder().build(); try { PebbleTemplate template = pebble .getTemplate("templates/macros/invalid.import.as.sameAlias.peb"); Writer writer = new StringWriter(); template.evaluate(writer); fail("expected PebbleException"); } catch (PebbleException e) { assertTrue(e.getPebbleMessage() .startsWith("More than one named template can not share the same name")); } } @Test void testInvalidAliasReferencingUnknownMacro() throws IOException { PebbleEngine pebble = new PebbleEngine.Builder().build(); try { PebbleTemplate template = pebble .getTemplate("templates/macros/invalid.from.unknownMacro.peb"); Writer writer = new StringWriter(); template.evaluate(writer); fail("expected PebbleException"); } catch (PebbleException e) { assertEquals( "Function or Macro [iDontExist] referenced by alias [macro_test] does not exist.", e.getPebbleMessage() ); } } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/node/ForNodeTest.java ================================================ package io.pebbletemplates.pebble.node; import io.pebbletemplates.pebble.loader.StringLoader; import io.pebbletemplates.pebble.PebbleEngine; import io.pebbletemplates.pebble.template.PebbleTemplate; import org.junit.jupiter.api.Test; import java.io.StringWriter; import java.io.Writer; import java.util.Enumeration; import java.util.HashMap; import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; class ForNodeTest { @Test void testVariableScope() throws Exception { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); StringBuilder source = new StringBuilder("{% set fooList = range(1, 1) %}"); source.append("{% for item in fooList %}"); source.append("{% set foo1 = 'fooValue' %}"); source.append("Foo1 value : {{ foo1 }}"); source.append("{% endfor %}"); source.append("Foo1 value : {{ foo1 }}"); source.append("{% for item in fooList %}"); source.append("{% set foo2 = 'fooValue2' %}"); source.append("Foo2 value : {{ foo2 }}"); source.append("{% endfor %}"); source.append("Foo2 value : {{ foo2 }}"); PebbleTemplate template = pebble.getTemplate(source.toString()); Map context = new HashMap<>(); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("Foo1 value : fooValueFoo1 value : Foo2 value : fooValue2Foo2 value : ", writer.toString()); } @Test void testNestedLoop() throws Exception { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); StringBuilder source = new StringBuilder("{% for i in 0..2 %}"); source.append("{% for j in 0..2 %}"); source.append("i={{ i }} j={{ j }} "); source.append("{% endfor %}"); source.append("{% endfor %}"); source.append("i={{ i }} j={{ j }} "); PebbleTemplate template = pebble.getTemplate(source.toString()); Map context = new HashMap<>(); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("i=0 j=0 i=0 j=1 i=0 j=2 i=1 j=0 i=1 j=1 i=1 j=2 i=2 j=0 i=2 j=1 i=2 j=2 i= j= ", writer.toString()); } @Test void testLoopIndex() throws Exception { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); StringBuilder source = new StringBuilder("{% for i in 0..2 %}"); source.append("{% for j in 0..2 %}"); source.append("inner={{ loop.index }} "); source.append("{% endfor %}"); source.append("outer={{ loop.index }} "); source.append("{% endfor %}"); source.append("outside loop={{ loop.index }} "); PebbleTemplate template = pebble.getTemplate(source.toString()); Map context = new HashMap<>(); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals( "inner=0 inner=1 inner=2 outer=0 inner=0 inner=1 inner=2 outer=1 inner=0 inner=1 inner=2 outer=2 outside loop= ", writer.toString()); } @Test void loopOverEnumeration() throws Exception { PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); StringBuilder source = new StringBuilder("{% for item in fooEnumeration %}"); source.append("{{ item }}"); source.append("{% endfor %}"); PebbleTemplate template = pebble.getTemplate(source.toString()); Map context = new HashMap<>(); context.put("fooEnumeration", new Enumeration() { private int value = 0; @Override public boolean hasMoreElements() { return this.value < 10; } @Override public String nextElement() { return String.valueOf(this.value++); } }); Writer writer = new StringWriter(); template.evaluate(writer, context); assertEquals("0123456789", writer.toString()); } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/node/IfNodeTest.java ================================================ package io.pebbletemplates.pebble.node; import io.pebbletemplates.pebble.PebbleEngine; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.loader.StringLoader; import io.pebbletemplates.pebble.template.PebbleTemplate; import org.junit.jupiter.api.Test; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.util.HashMap; import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; class IfNodeTest { private static final String templateSource = "{% if value %}yes{% else %}no{% endif %}"; private String render(Object foobar) throws IOException { return this.render(false, foobar); } private String render(boolean strict, Object value) throws IOException { PebbleEngine pebble = new PebbleEngine.Builder() .loader(new StringLoader()) .strictVariables(strict) .build(); PebbleTemplate template = pebble.getTemplate(templateSource); Map context = new HashMap<>(); context.put("value", value); Writer writer = new StringWriter(); template.evaluate(writer, context); return writer.toString(); } @Test void testIfNull() throws IOException { assertEquals("no", this.render(null), "Null should be interpreted as FALSE"); } @Test void testIfNullInStrictMode() throws IOException { assertThrows(PebbleException.class, () -> this.render(true, null)); } @Test void testIfTrue() throws IOException { assertEquals("yes", this.render(true), "Null should be interpreted as FALSE"); } @Test void testIfFalse() throws IOException { assertEquals("no", this.render(false), "Null should be interpreted as FALSE"); } @Test void testIfNotZeroInteger() throws IOException { assertEquals("yes", this.render(1), "Not zero integer should be interpreted as TRUE"); } @Test void testIfZeroInteger() throws IOException { assertEquals("no", this.render(0), "Zero integer should be interpreted as FALSE"); } @Test void testIfNotZeroFloat() throws IOException { assertEquals("yes", this.render(1.1), "Not zero float should be interpreted as TRUE"); } @Test void testIfZeroFloat() throws IOException { assertEquals("no", this.render(0), "Zero float should be interpreted as FALSE"); } @Test void testIfZeroDoubleNaN() throws IOException { assertEquals("no", this.render(Double.NaN), "NaN should be interpreted as FALSE"); } @Test void testIfZeroFloatNaN() throws IOException { assertEquals("no", this.render(Float.NaN), "NaN should be interpreted as FALSE"); } @Test void testIfNotEmptyString() throws IOException { assertEquals("yes", this.render("not empty string"), "Not empty string should be interpreted as TRUE"); } @Test void testIfEmptyString() throws IOException { assertEquals("no", this.render(""), "Empty string should be interpreted as FALSE"); } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/node/expression/AndExpressionTest.java ================================================ package io.pebbletemplates.pebble.node.expression; import org.junit.jupiter.api.Test; import java.io.IOException; class AndExpressionTest extends ExpressionTest { @Test void testIntExpressionPartFalse() throws IOException { this.testExpression("{{ 0 and true }}", "false"); this.testExpression("{{ false or 0 }}", "false"); } @Test void testIntExpressionPartTrue() throws IOException { this.testExpression("{{ 1 and false }}", "false"); this.testExpression("{{ true or 1 }}", "true"); } @Test void testStringExpressionPartFalse() throws IOException { this.testExpression("{{ '' and true }}", "false"); this.testExpression("{{ true and '' }}", "false"); } @Test void testStringExpressionPartTrue() throws IOException { this.testExpression("{{ 'true' and false }}", "false"); this.testExpression("{{ true or 'true' }}", "true"); } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/node/expression/ExpressionTest.java ================================================ package io.pebbletemplates.pebble.node.expression; import io.pebbletemplates.pebble.loader.StringLoader; import io.pebbletemplates.pebble.PebbleEngine; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.io.IOException; import java.io.StringWriter; import static org.junit.jupiter.api.Assertions.assertEquals; public abstract class ExpressionTest { private PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()) .strictVariables(false).build(); protected void testExpression(String templateName, String expected) throws IOException { PebbleTemplate template = this.pebble.getTemplate(templateName); StringWriter writer = new StringWriter(); template.evaluate(writer); assertEquals(expected, writer.toString()); } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/node/expression/OrExpressionTest.java ================================================ package io.pebbletemplates.pebble.node.expression; import org.junit.jupiter.api.Test; import java.io.IOException; class OrExpressionTest extends ExpressionTest { @Test void testIntExpressionPartFalse() throws IOException { this.testExpression("{{ 0 or false }}", "false"); this.testExpression("{{ true or 0 }}", "true"); } @Test void testIntExpressionPartTrue() throws IOException { this.testExpression("{{ 1 or false }}", "true"); this.testExpression("{{ true or 1 }}", "true"); } @Test void testStringExpressionPartFalse() throws IOException { this.testExpression("{{ '' or false }}", "false"); this.testExpression("{{ true or '' }}", "true"); } @Test void testStringExpressionPartTrue() throws IOException { this.testExpression("{{ 'true' or false }}", "true"); this.testExpression("{{ true or 'true' }}", "true"); } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/node/expression/StringExpressionParserTest.java ================================================ package io.pebbletemplates.pebble.node.expression; import io.pebbletemplates.pebble.error.ParserException; import io.pebbletemplates.pebble.lexer.LexerImpl; import io.pebbletemplates.pebble.lexer.TokenStream; import io.pebbletemplates.pebble.loader.StringLoader; import io.pebbletemplates.pebble.parser.Parser; import io.pebbletemplates.pebble.parser.ParserImpl; import io.pebbletemplates.pebble.parser.ParserOptions; import io.pebbletemplates.pebble.PebbleEngine; import io.pebbletemplates.pebble.node.PrintNode; import io.pebbletemplates.pebble.node.RootNode; import io.pebbletemplates.pebble.utils.Pair; import org.junit.jupiter.api.Test; import java.io.IOException; import java.io.StringReader; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; class StringExpressionParserTest { private Pair testParseExpression(String expression) { PebbleEngine pebble = new PebbleEngine.Builder() .loader(new StringLoader()) .strictVariables(false) .build(); LexerImpl lexer = new LexerImpl( pebble.getSyntax(), pebble.getExtensionRegistry().getUnaryOperators().values(), pebble.getExtensionRegistry().getBinaryOperators().values() ); TokenStream tokenStream = lexer.tokenize(new StringReader(expression), "test.peb"); Parser parser = new ParserImpl( pebble.getExtensionRegistry().getUnaryOperators(), pebble.getExtensionRegistry().getBinaryOperators(), pebble.getExtensionRegistry().getTokenParsers(), new ParserOptions() ); return new Pair<>(parser, parser.parse(tokenStream)); } /* * Sequential String literals are not allowed, a syntax error should be thrown */ @Test void testSequentialStrings_singleQuotes_isSyntaxError() throws IOException { assertThrows(ParserException.class, () -> this.testParseExpression("{{ 'one' 'two' }}")); } /* * Sequential String literals are not allowed, a syntax error should be thrown */ @Test void testSequentialStrings_doubleQuotes_isSyntaxError() throws IOException { assertThrows(ParserException.class, () -> this.testParseExpression("{{ \"one\" \"two\" }}")); } /* * + parses as Add expression. AST should be: * * AddExpression * LiteralStringExpression('one') * LiteralStringExpression('two') */ @Test void testValidExpression_singleQuotes() throws IOException { Pair underTest = this.testParseExpression("{{ 'one' + 'two' }}"); PrintNode printNode = (PrintNode) underTest.getRight().getBody().getChildren().get(0); Expression expr = printNode.getExpression(); assertThat(expr).isInstanceOf(AddExpression.class); Expression leftExpression = ((AddExpression) expr).getLeftExpression(); Expression rightExpression = ((AddExpression) expr).getRightExpression(); assertThat(leftExpression).isInstanceOf(LiteralStringExpression.class); assertThat(((LiteralStringExpression) leftExpression).getValue()).isEqualTo("one"); assertThat(rightExpression).isInstanceOf(LiteralStringExpression.class); assertThat(((LiteralStringExpression) rightExpression).getValue()).isEqualTo("two"); } /* * + parses as Add expression. AST should be: * * AddExpression * LiteralStringExpression('one') * LiteralStringExpression('two') */ @Test void testValidExpression_doubleQuotes() throws IOException { Pair underTest = this.testParseExpression("{{ \"one\" + \"two\" }}"); PrintNode printNode = (PrintNode) underTest.getRight().getBody().getChildren().get(0); Expression expr = printNode.getExpression(); assertThat(expr).isInstanceOf(AddExpression.class); Expression leftExpression = ((AddExpression) expr).getLeftExpression(); Expression rightExpression = ((AddExpression) expr).getRightExpression(); assertThat(leftExpression).isInstanceOf(LiteralStringExpression.class); assertThat(((LiteralStringExpression) leftExpression).getValue()).isEqualTo("one"); assertThat(rightExpression).isInstanceOf(LiteralStringExpression.class); assertThat(((LiteralStringExpression) rightExpression).getValue()).isEqualTo("two"); } /* * ~ parses as Concatenate expression. AST should be: * * ConcatenateExpression * LiteralStringExpression('one') * LiteralStringExpression('two') */ @Test void testValidConcatenationExpression_singleQuotes() throws IOException { Pair underTest = this.testParseExpression("{{ 'one' ~ 'two' }}"); PrintNode printNode = (PrintNode) underTest.getRight().getBody().getChildren().get(0); Expression expr = printNode.getExpression(); assertThat(expr).isInstanceOf(ConcatenateExpression.class); Expression leftExpression = ((ConcatenateExpression) expr).getLeftExpression(); Expression rightExpression = ((ConcatenateExpression) expr).getRightExpression(); assertThat(leftExpression).isInstanceOf(LiteralStringExpression.class); assertThat(((LiteralStringExpression) leftExpression).getValue()).isEqualTo("one"); assertThat(rightExpression).isInstanceOf(LiteralStringExpression.class); assertThat(((LiteralStringExpression) rightExpression).getValue()).isEqualTo("two"); } /* * ~ parses as Concatenate expression. AST should be: * * ConcatenateExpression * LiteralStringExpression('one') * LiteralStringExpression('two') */ @Test void testValidConcatenationExpression_doubleQuotes() throws IOException { Pair underTest = this.testParseExpression("{{ \"one\" ~ \"two\" }}"); PrintNode printNode = (PrintNode) underTest.getRight().getBody().getChildren().get(0); Expression expr = printNode.getExpression(); assertThat(expr).isInstanceOf(ConcatenateExpression.class); Expression leftExpression = ((ConcatenateExpression) expr).getLeftExpression(); Expression rightExpression = ((ConcatenateExpression) expr).getRightExpression(); assertThat(leftExpression).isInstanceOf(LiteralStringExpression.class); assertThat(((LiteralStringExpression) leftExpression).getValue()).isEqualTo("one"); assertThat(rightExpression).isInstanceOf(LiteralStringExpression.class); assertThat(((LiteralStringExpression) rightExpression).getValue()).isEqualTo("two"); } /* * Single-quotes does not parse interpolations. AST should be: * * LiteralStringExpression('one #{two} three') */ @Test void testValidInterpolationExpression_singleQuotes() throws IOException { Pair underTest = this.testParseExpression("{{ 'one #{two} three' }}"); PrintNode printNode = (PrintNode) underTest.getRight().getBody().getChildren().get(0); Expression expr = printNode.getExpression(); assertThat(expr).isInstanceOf(LiteralStringExpression.class); assertThat(((LiteralStringExpression) expr).getValue()).isEqualTo("one #{two} three"); } /* * Double-quotes parses interpolations. AST should be: * * ConcatenateExpression * LiteralStringExpression('one ') * ConcatenateExpression * ContextVariableExpression(two) * LiteralStringExpression(' three') */ @Test void testValidInterpolationExpression_doubleQuotes() throws IOException { Pair underTest = this.testParseExpression("{{ \"one #{two} three\" }}"); PrintNode printNode = (PrintNode) underTest.getRight().getBody().getChildren().get(0); Expression expr = printNode.getExpression(); assertThat(expr).isInstanceOf(ConcatenateExpression.class); Expression leftExpression = ((ConcatenateExpression) expr).getLeftExpression(); Expression rightExpression = ((ConcatenateExpression) expr).getRightExpression(); assertThat(leftExpression).isInstanceOf(LiteralStringExpression.class); assertThat(((LiteralStringExpression) leftExpression).getValue()).isEqualTo("one "); assertThat(rightExpression).isInstanceOf(ConcatenateExpression.class); Expression right_leftSubExpression = ((ConcatenateExpression) rightExpression).getLeftExpression(); assertThat(((ContextVariableExpression) right_leftSubExpression).getName()).isEqualTo("two"); Expression right_rightSubExpression = ((ConcatenateExpression) rightExpression).getRightExpression(); assertThat(((LiteralStringExpression) right_rightSubExpression).getValue()).isEqualTo(" three"); } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/node/expression/UnaryNotExpressionTest.java ================================================ package io.pebbletemplates.pebble.node.expression; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.loader.StringLoader; import io.pebbletemplates.pebble.PebbleEngine; import io.pebbletemplates.pebble.template.PebbleTemplate; import org.junit.jupiter.api.Test; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.util.HashMap; import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; class UnaryNotExpressionTest { private static final String templateSource = "{% if not value %}yes{% else %}no{% endif %}"; private String render(Object foobar) throws IOException { return this.render(false, foobar); } private String render(boolean strict, Object value) throws IOException { PebbleEngine pebble = new PebbleEngine.Builder() .loader(new StringLoader()) .strictVariables(strict) .build(); PebbleTemplate template = pebble.getTemplate(templateSource); Map context = new HashMap<>(); context.put("value", value); Writer writer = new StringWriter(); template.evaluate(writer, context); return writer.toString(); } @Test void testIfNotNull() throws IOException { assertEquals("yes", this.render(null), "Not Null should be interpreted as TRUE"); } @Test void testIfNullInStrictMode() throws IOException { assertThrows(PebbleException.class, () -> this.render(true, null)); } @Test void testIfNotTrue() throws IOException { assertEquals("no", this.render(true), "Not true should be interpreted as FALSE"); } @Test void testIfNotFalse() throws IOException { assertEquals("yes", this.render(false), "Not false should be interpreted as TRUE"); } @Test void testIfNotIntegerDifferentThanZero() throws IOException { assertEquals("no", this.render(1), "Not Integer one should be interpreted as FALSE"); } @Test void testIfNotIntegerZero() throws IOException { assertEquals("yes", this.render(0), "Not Integer Zero should be interpreted as TRUE"); } @Test void testIfNotFloatDifferentThanZero() throws IOException { assertEquals("no", this.render(1.1), "Not float different than zero should be interpreted as FALSE"); } @Test void testIfNotFloatZero() throws IOException { assertEquals("yes", this.render(0.0), "Not float zero should be interpreted as TRUE"); } @Test void testIfNotString() throws IOException { assertEquals("no", this.render("not empty string"), "Not string should be interpreted as FALSE"); } @Test void testIfNotEmptyString() throws IOException { assertEquals("yes", this.render(""), "Not Empty string should be interpreted as TRUE"); } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/template/tests/PebbleTestContext.java ================================================ package io.pebbletemplates.pebble.template.tests; import io.pebbletemplates.pebble.PebbleEngine; import io.pebbletemplates.pebble.loader.StringLoader; import io.pebbletemplates.pebble.template.PebbleTemplate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.net.URISyntaxException; import java.net.URL; import java.nio.file.Files; import java.nio.file.Paths; import java.util.HashMap; import java.util.Map; /** * Used by Pebble Template Tests to simply the test code and therefore * make it easier to understand what is tested by each test. * * A separate instance of this class should be instantiated for each * template test, i.e. for each instance of a template. * * @author nathanward */ public class PebbleTestContext { private final Logger logger = LoggerFactory.getLogger(PebbleTestContext.class); /** * The path relative to the project root directory where template files and * expected output files are stored for test purposes. */ private String testFileBasePath = "template-tests"; /** * The Pebble template context to be used as input for a Pebble Template. */ private Map templateContext = null; /** * Whether or not the Pebble Engine instantiated will * enable New Line Trimming on the Builder when this class instantiates * a Pebble Engine instance. Defaults to true. */ private boolean newLineTrimming = true; /** * Initialize the Pebble Template Context. */ public PebbleTestContext() { this.templateContext = new HashMap(); } public void setNewLineTrimming(boolean b) { this.newLineTrimming = b; } /** * Put an object into the Pebble template context to be used as input for * when the template is executed. One or more items can be put into the template * context. * * @param name Template input/parameter name (i.e. key) for use within the template * @param value The object that will be referred to by the given name in the template */ public void setTemplateInput(String name, Object value) { this.templateContext.put(name, value); } /** * Load the specified template file and execute the template using a * Pebble Engine using the default Builder (classpath and file builder). * * @param templateFilename The template filename relative to the Test File Base Path * * @return The output of the template as a string. * * @throws IOException Thrown if the template file is not found. */ public String executeTemplateFromFile(String templateFilename) throws IOException { PebbleEngine pebbleEngine = new PebbleEngine.Builder() .newLineTrimming(this.newLineTrimming).strictVariables(true).build(); return this.executeTemplateFromFile(templateFilename, pebbleEngine); } /** * Execute a template given a template file and a Pebble Engine instance. * * @param templateFilename The template filename relative to the Test File Base Path * @param pebbleEngine * @return * @throws IOException */ public String executeTemplateFromFile(String templateFilename, PebbleEngine pebbleEngine) throws IOException { logger.debug("Executing template file: {}", templateFilename); return this.executeTemplate(testFileBasePath + "/" + templateFilename, pebbleEngine); } /** * Load the specified template file and execute the template using a * Pebble Engine using the default Builder (classpath and file builder). * * @param templateString The template content as a string * * @return The output of the template as a string. * * @throws IOException Thrown if the template file is not found. */ public String executeTemplateFromString(String templateString) throws IOException { PebbleEngine pebbleEngine = new PebbleEngine.Builder().loader(new StringLoader()) .newLineTrimming(this.newLineTrimming).build(); return this.executeTemplate(templateString, pebbleEngine); } /** * Load the specified template file and execute the template using the template input * that has previously been specified using the setTemplateInput() method. * * @param templateFilename The template filename relative to the Test File Base Path * @param pebbleEngine The Pebble Engine to be used to execute the template * @return The output of the template as a string. * * @throws IOException Thrown if the template file is not found. */ public String executeTemplate(String templateName, PebbleEngine pebbleEngine) throws IOException { PebbleTemplate template = pebbleEngine.getTemplate(templateName); Writer writer = new StringWriter(); template.evaluate(writer, this.templateContext); String templateOutput = writer.toString(); logger.debug("Template Output:\n{}", templateOutput); return templateOutput; } /** * Get the Expected Output content for the given filename so that the * base path to the expected template output file does not have to be * specified in the actual test code. * * @param filename The name of the file that contains the expected template output. * @return The content of the expected template output file as a string. * @throws IOException Thrown if the file by the given name is not found. */ public String getExpectedOutput(String filename) throws IOException { URL resource = this.getClass().getClassLoader().getResource(testFileBasePath + "/" + filename); try { return new String(Files.readAllBytes(Paths.get(resource.toURI()))); } catch (URISyntaxException e) { throw new RuntimeException(e); } } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/template/tests/WhiteSpaceControlWithNewLineTrimmingTests.java ================================================ /* * This file is part of Pebble. * * Copyright (c) 2014 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.pebble.template.tests; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.template.tests.input.PebbleTestItem; import io.pebbletemplates.pebble.template.tests.input.PebbleTestItemType; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.io.IOException; import java.util.ArrayList; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; /** * Tests of whitespace control when New Line Trimming is enabled. * This mainly affects vertical spacing, i.e. the control of blank lines. * With New Line Trimming enabled, Pebble control statements on a line * by themselves products a blank line in the output unless * the Whitespace Control Modifier is used before and/or after the * control statement, i.e. the dash character, e.g. `{% if item.itemType equals "ITEM_TYPE1" -%}`. * * However, it can be difficult to get the blank lines to come out as expected * in the case that nested control structures are used. * * @author nathanward */ class WhiteSpaceControlWithNewLineTrimmingTests { /** * All tests in this class use this list of objects as the input to the template. */ private List listOfObjects = null; /** * Used by each test and initialized before each test to simplify the * test execution, which helps make the purpose of each test clear. */ private PebbleTestContext pebbleTestContext = null; @BeforeEach void setup() { this.pebbleTestContext = new PebbleTestContext(); this.pebbleTestContext.setNewLineTrimming(false); this.listOfObjects = new ArrayList(); this.listOfObjects.add(new PebbleTestItem("Item 1", PebbleTestItemType.ITEM_TYPE1)); this.listOfObjects.add(new PebbleTestItem("Item 2", PebbleTestItemType.ITEM_TYPE2)); this.listOfObjects.add(new PebbleTestItem("Item 3", PebbleTestItemType.ITEM_TYPE3)); this.listOfObjects.add(new PebbleTestItem("Item 4", PebbleTestItemType.ITEM_TYPE4, true)); this.pebbleTestContext.setTemplateInput("items", this.listOfObjects); } /** * Test the whitespace control for a template that has a for loop with a nested * if statement that uses a macro and the macro also has an if statement. */ @Test void testForLoopWithNestedIfStatementAndMacro() throws PebbleException, IOException { String templateOutput = this.pebbleTestContext.executeTemplateFromFile("ForLoopWithNestedIfStatementAndMacro.peb"); assertThat(templateOutput).contains(this.pebbleTestContext.getExpectedOutput("ForLoopWithNestedIfStatementAndMacro.txt")); } /** * Test the whitespace control for a template that has a for loop with a nested * if statement that uses a macro and the macro also has an if statement. */ // TODO not sure why the white space controls work the way that it does for this case. @Test void testDoubleNestedIfStatement() throws PebbleException, IOException { String templateOutput = this.pebbleTestContext.executeTemplateFromFile("DoubleNestedIfStatement.peb"); assertThat(templateOutput).contains(this.pebbleTestContext.getExpectedOutput("DoubleNestedIfStatement.txt")); } /** * Test the whitespace control for a template that has a for loop with a nested * if statement where some text is output for each item in the list. */ @Test void testNestedIfStatementWithOneElseIfStatements() throws PebbleException, IOException { String templateOutput = this.pebbleTestContext.executeTemplateFromFile("NestedIfStatementWithOneElseIfStatements.peb"); assertThat(templateOutput).contains(this.pebbleTestContext.getExpectedOutput("NestedIfStatementWithOneElseIfStatements.txt")); } /** * Test the whitespace control for a template that has a for loop with a nested * if statement where some text is output for each item in the list. */ @Test void testNestedIfStatementWithTwoElseIfStatements() throws PebbleException, IOException { String templateOutput = this.pebbleTestContext.executeTemplateFromFile("NestedIfStatementWithTwoElseIfStatements.peb"); assertThat(templateOutput).contains(this.pebbleTestContext.getExpectedOutput("NestedIfStatementWithTwoElseIfStatements.txt")); } /** * Test the whitespace control for a template that has a for loop with a nested * if statement that skips some of the items in the for loop. */ @Test void testNestedIfStatementWithThreeElseIfStatements() throws IOException { String templateOutput = this.pebbleTestContext.executeTemplateFromFile("NestedIfStatementWithThreeElseIfStatements.peb"); assertThat(templateOutput).contains(this.pebbleTestContext.getExpectedOutput("NestedIfStatementWithThreeElseIfStatements.txt")); } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/template/tests/input/PebbleTestItem.java ================================================ package io.pebbletemplates.pebble.template.tests.input; public class PebbleTestItem { private String name; private PebbleTestItemType itemType; private boolean hasPrefix; public PebbleTestItem(String name, PebbleTestItemType itemType1) { this.name = name; this.itemType = itemType1; } public PebbleTestItem(String name, PebbleTestItemType itemType1, boolean hasPrefix) { this.name = name; this.itemType = itemType1; this.hasPrefix = hasPrefix; } public String getName() { return name; } public void setName(String name) { this.name = name; } public PebbleTestItemType getItemType() { return itemType; } public void setItemType(PebbleTestItemType pebbleTestItemType) { this.itemType = pebbleTestItemType; } public boolean isHasPrefix() { return hasPrefix; } public void setHasPrefix(boolean hasPrefix) { this.hasPrefix = hasPrefix; } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/template/tests/input/PebbleTestItemType.java ================================================ package io.pebbletemplates.pebble.template.tests.input; public enum PebbleTestItemType { ITEM_TYPE1, ITEM_TYPE2, ITEM_TYPE3, ITEM_TYPE4 } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/utils/LimitedSizeWriterTest.java ================================================ package io.pebbletemplates.pebble.utils; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.template.RenderedSizeContext; import org.junit.jupiter.api.Test; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.util.concurrent.atomic.AtomicInteger; import static org.junit.jupiter.api.Assertions.*; /** * Tests {@link LimitedSizeWriter}. */ class LimitedSizeWriterTest { @Test void negativeMaxSizeReturnsTheOriginalWriter() { Writer internalWriter = new StringWriter(); Writer limitedSizeWriter = LimitedSizeWriter.from(internalWriter, new TestRenderedSizeContext(-1)); assertSame(internalWriter, limitedSizeWriter); } @Test void canWriteLessThanLimitChars() throws IOException { Writer internalWriter = new StringWriter(); Writer limitedSizeWriter = LimitedSizeWriter.from(internalWriter, new TestRenderedSizeContext(20)); limitedSizeWriter.write("0123456789"); limitedSizeWriter.write("0123456789"); assertEquals("01234567890123456789", internalWriter.toString()); } @Test void cannotWriteMoreThanLimitChars() throws IOException { Writer internalWriter = new StringWriter(); Writer limitedSizeWriter = LimitedSizeWriter.from(internalWriter, new TestRenderedSizeContext(19)); limitedSizeWriter.write("0123456789"); PebbleException thrown = assertThrows( PebbleException.class, () -> limitedSizeWriter.write("0123456789")); assertTrue(thrown.getMessage().contains("19")); } @Test void contextIsSharedBetweenWriters() throws IOException { Writer internalWriter1 = new StringWriter(); Writer internalWriter2 = new StringWriter(); TestRenderedSizeContext context = new TestRenderedSizeContext(19); Writer limitedSizeWriter1 = LimitedSizeWriter.from(internalWriter1, context); Writer limitedSizeWriter2 = LimitedSizeWriter.from(internalWriter2, context); limitedSizeWriter1.write("0123456789"); PebbleException thrown = assertThrows( PebbleException.class, () -> limitedSizeWriter2.write("0123456789")); assertTrue(thrown.getMessage().contains("19")); } // This is the exact same implementation as in EvaluationContextImpl. static private class TestRenderedSizeContext implements RenderedSizeContext { private final int maxRenderedSize; private final AtomicInteger charsRendered = new AtomicInteger(); public TestRenderedSizeContext(int maxRenderedSize) { this.maxRenderedSize = maxRenderedSize; } @Override public int getMaxRenderedSize() { return this.maxRenderedSize; } @Override public int addAndGet(int delta) { return this.charsRendered.addAndGet(delta); } } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/utils/OperatorUtilsToNumberTest.java ================================================ package io.pebbletemplates.pebble.utils; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; import java.math.BigDecimal; import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.params.provider.Arguments.arguments; class OperatorUtilsToNumberTest { @ParameterizedTest(name = "Number input {0} should be returned as-is") @MethodSource("numberInputs") void shouldReturnNumberInstanceAsIs(Number input) { Number result = OperatorUtils.toNumber(input); assertThat(result).isSameAs(input); } static Stream numberInputs() { return Stream.of(42, 3.14f, 100L, 2.718281828, 2.5d, (short) 7, new BigDecimal(12345678900L)); } @ParameterizedTest(name = "String {0} should parse to Double {1}") @MethodSource("decimalStringInputs") void shouldParseDecimalStringAsDouble(String input, double expected) { Number result = OperatorUtils.toNumber(input); assertThat(result) .isInstanceOf(Double.class) .isEqualTo(expected); } static Stream decimalStringInputs() { return Stream.of( arguments("3.14", 3.14), arguments("0.0", 0.0), arguments("-1.5", -1.5), arguments("1e10", 1e10), arguments("1E10", 1E10), arguments("2.5e3", 2.5e3), arguments("1.1E-2", 1.1E-2) ); } @ParameterizedTest(name = "String {0} should parse to Long {1}") @MethodSource("integerStringInputs") void shouldParseIntegerStringAsLong(String input, long expected) { Number result = OperatorUtils.toNumber(input); assertThat(result) .isInstanceOf(Long.class) .isEqualTo(expected); } static Stream integerStringInputs() { return Stream.of( arguments("0", 0L), arguments("42", 42L), arguments("-7", -7L), arguments("9999999999", 9999999999L) // larger than Integer.MAX_VALUE ); } @ParameterizedTest(name = "Unsupported type {0} should throw IllegalArgumentException") @MethodSource("unsupportedInputs") void shouldThrowForUnsupportedTypes(Object input) { assertThatThrownBy(() -> OperatorUtils.toNumber(input)) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("Cannot convert to Number"); } static Stream unsupportedInputs() { return Stream.of(null, true, 'x', new Object()); } @ParameterizedTest(name = "Malformed string {0} should throw NumberFormatException") @ValueSource(strings = {"abc", "1.2.3", "1e", "--1", ""}) void shouldThrowForMalformedStrings(String input) { assertThatThrownBy(() -> OperatorUtils.toNumber(input)) .isInstanceOf(NumberFormatException.class); } } ================================================ FILE: pebble/src/test/java/io/pebbletemplates/pebble/utils/PathUtilsTest.java ================================================ package io.pebbletemplates.pebble.utils; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; /** * Tests {@link PathUtils}. * * @author Thomas Hunziker */ class PathUtilsTest { private static final char FORWARD_SLASH = '/'; private static final char BACKWARD_SLASH = '\\'; /** * Tests if {@link PathUtils#resolveRelativePath(String, String, char)} is working with Unix * systems. */ @Test void testRelativePathResolutionUnixStyle() { assertEquals("test/test/sample.peb", PathUtils.resolveRelativePath("./sample.peb", "test/test/", FORWARD_SLASH)); assertEquals("test/test/sample.peb", PathUtils.resolveRelativePath("./sample.peb", "test/test/other.peb", FORWARD_SLASH)); assertEquals("test/sample.peb", PathUtils.resolveRelativePath("../sample.peb", "test/test/", FORWARD_SLASH)); assertEquals("test/sample.peb", PathUtils.resolveRelativePath("../sample.peb", "test/test/other.peb", FORWARD_SLASH)); assertNull( PathUtils.resolveRelativePath("test/sample.peb", "test/test/other.peb", FORWARD_SLASH)); } /** * Tests if {@link PathUtils#resolveRelativePath(String, String, char)} is working with Windows. */ @Test void testRelativePathResolutionWindowsStyle() { assertEquals("test\\test\\sample.peb", PathUtils.resolveRelativePath(".\\sample.peb", "test\\test\\", BACKWARD_SLASH)); assertEquals("test\\test\\sample.peb", PathUtils.resolveRelativePath(".\\sample.peb", "test\\test\\other.peb", BACKWARD_SLASH)); assertEquals("test\\sample.peb", PathUtils.resolveRelativePath("..\\sample.peb", "test\\test\\", BACKWARD_SLASH)); assertEquals("test\\sample.peb", PathUtils.resolveRelativePath("..\\sample.peb", "test\\test\\other.peb", BACKWARD_SLASH)); assertEquals(null, PathUtils.resolveRelativePath("test\\sample.peb", "test\\test\\other.peb", BACKWARD_SLASH)); } @Test void testRelativePathResolutionMixedStyle1() { assertEquals("test/test/sample.peb", PathUtils.resolveRelativePath(".\\sample.peb", "test/test/", FORWARD_SLASH)); assertEquals("test/test/sample.peb", PathUtils.resolveRelativePath(".\\sample.peb", "test/test/other.peb", FORWARD_SLASH)); assertEquals("test/sample.peb", PathUtils.resolveRelativePath("..\\sample.peb", "test/test/", FORWARD_SLASH)); assertEquals("test/sample.peb", PathUtils.resolveRelativePath("..\\sample.peb", "test/test/other.peb", FORWARD_SLASH)); assertEquals(null, PathUtils.resolveRelativePath("test\\sample.peb", "test/test/other.peb", FORWARD_SLASH)); } @Test void testRelativePathResolutionMixedStyle2() { assertEquals("test\\test\\sample.peb", PathUtils.resolveRelativePath("./sample.peb", "test\\test\\", BACKWARD_SLASH)); assertEquals("test\\test\\sample.peb", PathUtils.resolveRelativePath("./sample.peb", "test\\test\\other.peb", BACKWARD_SLASH)); assertEquals("test\\sample.peb", PathUtils.resolveRelativePath("../sample.peb", "test\\test\\", BACKWARD_SLASH)); assertEquals("test\\sample.peb", PathUtils.resolveRelativePath("../sample.peb", "test\\test\\other.peb", BACKWARD_SLASH)); assertEquals(null, PathUtils.resolveRelativePath("test/sample.peb", "test\\test\\other.peb", BACKWARD_SLASH)); } } ================================================ FILE: pebble/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n ================================================ FILE: pebble/src/test/resources/security/allowedMethods.properties ================================================ java.lang.Object.toString() java.lang.Object.hashCode() java.lang.Object.equals(java.lang.Object) io.pebbletemplates.pebble.attributes.methodaccess.Foo.getX() io.pebbletemplates.pebble.attributes.methodaccess.Foo.setX(java.lang.String) io.pebbletemplates.pebble.attributes.methodaccess.Foo.toString() io.pebbletemplates.pebble.attributes.methodaccess.Foo.hashCode() io.pebbletemplates.pebble.attributes.methodaccess.Foo.equals(java.lang.Object) ================================================ FILE: pebble/src/test/resources/security/unsafeMethods.properties ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you 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. java.lang.Object.wait() java.lang.Object.wait(long) java.lang.Object.wait(long,int) java.lang.Object.notify() java.lang.Object.notifyAll() java.lang.Object.getClass() java.lang.Class.getClassLoader() java.lang.Class.newInstance() java.lang.Class.forName(java.lang.String) java.lang.Class.forName(java.lang.String,boolean,java.lang.ClassLoader) java.lang.reflect.Constructor.newInstance([Ljava.lang.Object;) java.lang.reflect.Method.invoke(java.lang.Object,[Ljava.lang.Object;) java.lang.reflect.Field.set(java.lang.Object,java.lang.Object) java.lang.reflect.Field.setBoolean(java.lang.Object,boolean) java.lang.reflect.Field.setByte(java.lang.Object,byte) java.lang.reflect.Field.setChar(java.lang.Object,char) java.lang.reflect.Field.setDouble(java.lang.Object,double) java.lang.reflect.Field.setFloat(java.lang.Object,float) java.lang.reflect.Field.setInt(java.lang.Object,int) java.lang.reflect.Field.setLong(java.lang.Object,long) java.lang.reflect.Field.setShort(java.lang.Object,short) java.lang.reflect.AccessibleObject.setAccessible([Ljava.lang.reflect.AccessibleObject;,boolean) java.lang.reflect.AccessibleObject.setAccessible(boolean) java.lang.Thread.getContextClassLoader() java.lang.Thread.interrupt() java.lang.Thread.join() java.lang.Thread.join(long) java.lang.Thread.join(long,int) java.lang.Thread.resume() java.lang.Thread.run() java.lang.Thread.setContextClassLoader(java.lang.ClassLoader) java.lang.Thread.setDaemon(boolean) java.lang.Thread.setName(java.lang.String) java.lang.Thread.setPriority(int) java.lang.Thread.sleep(long) java.lang.Thread.sleep(long,int) java.lang.Thread.start() java.lang.Thread.stop() java.lang.Thread.suspend() java.lang.ThreadGroup.allowThreadSuspension(boolean) java.lang.ThreadGroup.destroy() java.lang.ThreadGroup.interrupt() java.lang.ThreadGroup.resume() java.lang.ThreadGroup.setDaemon(boolean) java.lang.ThreadGroup.setMaxPriority(int) java.lang.ThreadGroup.stop() java.lang.Runtime.addShutdownHook(java.lang.Thread) java.lang.Runtime.exec(java.lang.String) java.lang.Runtime.exec([Ljava.lang.String;) java.lang.Runtime.exec([Ljava.lang.String;,[Ljava.lang.String;) java.lang.Runtime.exec([Ljava.lang.String;,[Ljava.lang.String;,java.io.File) java.lang.Runtime.exec(java.lang.String,[Ljava.lang.String;) java.lang.Runtime.exec(java.lang.String,[Ljava.lang.String;,java.io.File) java.lang.Runtime.exit(int) java.lang.Runtime.halt(int) java.lang.Runtime.load(java.lang.String) java.lang.Runtime.loadLibrary(java.lang.String) java.lang.Runtime.removeShutdownHook(java.lang.Thread) java.lang.System.exit(int) java.lang.System.load(java.lang.String) java.lang.System.loadLibrary(java.lang.String) java.lang.System.setErr(java.io.PrintStream) java.lang.System.setIn(java.io.InputStream) java.lang.System.setOut(java.io.PrintStream) java.lang.System.setProperties(java.util.Properties) java.lang.System.setProperty(java.lang.String,java.lang.String) java.lang.System.setSecurityManager(java.lang.SecurityManager) ================================================ FILE: pebble/src/test/resources/template-tests/DoubleNestedIfStatement.peb ================================================ text before for loop followed by blank line {% for item in items %} {% if item.itemType equals "ITEM_TYPE1" -%} Item 1 {% elseif item.itemType equals "ITEM_TYPE2" -%} Item 2 {% elseif item.itemType equals "ITEM_TYPE3" -%} Item 3 {% elseif item.itemType equals "ITEM_TYPE4" -%} {% if item.hasPrefix -%} Item Prefix {% endif -%} Item 4 {% endif -%} {% endfor %} text after for loop preceded by blank line ================================================ FILE: pebble/src/test/resources/template-tests/DoubleNestedIfStatement.txt ================================================ text before for loop followed by blank line Item 1 Item 2 Item 3 Item Prefix Item 4 text after for loop preceded by blank line ================================================ FILE: pebble/src/test/resources/template-tests/ForLoopWithNestedIfStatementAndMacro.peb ================================================ text before for loop followed by blank line {% for item in items %} {% if item.itemType equals "ITEM_TYPE1" -%} {{ item.name }} {% else -%} {{ itemStatement(item) }} {% endif -%} {% endfor %} text after for loop preceded by blank line {%- macro itemStatement(item) -%} {%- if item.hasPrefix == true -%} Prefix Text {% endif -%} {{ item.name }} {%- endmacro -%} ================================================ FILE: pebble/src/test/resources/template-tests/ForLoopWithNestedIfStatementAndMacro.txt ================================================ text before for loop followed by blank line Item 1 Item 2 Item 3 Prefix Text Item 4 text after for loop preceded by blank line ================================================ FILE: pebble/src/test/resources/template-tests/NestedIfStatementWithOneElseIfStatements.peb ================================================ text before for loop followed by blank line {% for item in items %} {% if item.itemType equals "ITEM_TYPE1" -%} Item 1 {% elseif item.itemType equals "ITEM_TYPE2" -%} Item 2 {%- endif -%} {% endfor -%} text after for loop preceded by blank line ================================================ FILE: pebble/src/test/resources/template-tests/NestedIfStatementWithOneElseIfStatements.txt ================================================ text before for loop followed by blank line Item 1 Item 2 text after for loop preceded by blank line ================================================ FILE: pebble/src/test/resources/template-tests/NestedIfStatementWithThreeElseIfStatements.peb ================================================ text before for loop followed by blank line {% for item in items %} {% if item.itemType equals "ITEM_TYPE1" -%} Item 1 {% elseif item.itemType equals "ITEM_TYPE2" -%} Item 2 {% elseif item.itemType equals "ITEM_TYPE3" -%} Item 3 {% elseif item.itemType equals "ITEM_TYPE4" -%} Item 4 {% endif -%} {% endfor %} text after for loop preceded by blank line ================================================ FILE: pebble/src/test/resources/template-tests/NestedIfStatementWithThreeElseIfStatements.txt ================================================ text before for loop followed by blank line Item 1 Item 2 Item 3 Item 4 text after for loop preceded by blank line ================================================ FILE: pebble/src/test/resources/template-tests/NestedIfStatementWithTwoElseIfStatements.peb ================================================ text before for loop followed by blank line {% for item in items %} {% if item.itemType equals "ITEM_TYPE1" -%} Item 1 {% elseif item.itemType equals "ITEM_TYPE2" -%} Item 2 {% elseif item.itemType equals "ITEM_TYPE3" -%} Item 3 {%- endif -%} {% endfor %} text after for loop preceded by blank line ================================================ FILE: pebble/src/test/resources/template-tests/NestedIfStatementWithTwoElseIfStatements.txt ================================================ text before for loop followed by blank line Item 1 Item 2 Item 3 text after for loop preceded by blank line ================================================ FILE: pebble/src/test/resources/templates/cache/cache1/template.cache.peb ================================================ {# used by CacheTest.templatesWithSameNameOverridingCache #} cache1 ================================================ FILE: pebble/src/test/resources/templates/cache/cache2/template.cache.peb ================================================ {# used by CacheTest.templatesWithSameNameOverridingCache #} cache2 ================================================ FILE: pebble/src/test/resources/templates/cache/template.cacheChild.peb ================================================ {# used by CacheTest.ensureChildTemplateNotCached #} {% extends 'templates/cache/template.cacheParent.peb' %} {% block title %}child{% endblock %} ================================================ FILE: pebble/src/test/resources/templates/cache/template.cacheParent.peb ================================================ {# used by CacheTest.ensureChildTemplateNotCached #} {% block title %}parent{% endblock %} ================================================ FILE: pebble/src/test/resources/templates/embed/cache/template.base.peb ================================================ BEFORE BASE {% block embedBlock1 %} EMBED BASE {% endblock %} AFTER BASE ================================================ FILE: pebble/src/test/resources/templates/embed/cache/template1.peb ================================================ {% embed './template.base.peb' %} {% block 'embedBlock1' %} EMBED OVERRIDE {% endblock %} {% endembed %} ================================================ FILE: pebble/src/test/resources/templates/embed/cache/template2.peb ================================================ {% include './template.base.peb' %} ================================================ FILE: pebble/src/test/resources/templates/embed/notes.md ================================================ This is a complete example using the `embed` tag with lots of different scenarios for using the embed, and specifically how it interacts with the main template hierarchy. Ideally, the embed is a completely isolated template which maintains its own template hierarchy, and blocks are resolved entirely within its isolated hierarchy. Outside of the embed, blocks are resolved related to the normal hierarchy, and there is no mixing of the two. The majority of these tests were compared to Twig for fidelity, using https://twigfiddle.com/. ## Unit Tests - test2 - Bare minimum, can an `embed` include a template. Without overriding a block, an `embed` should work identical to an `include` - test0 - Can an `embed` override a block in an included template - test1 - Can an `embed` override a block in an included template, using Pebble's string block name syntax - test3 - Variables can be passed to the included template's scope using `with`. Overridden blocks use the scope of the included template, not the parent template, and can use Pebble's string block name syntax - test4 - Variables can be passed to the included template's scope using `with`. Overridden blocks use the scope of the included template, not the parent template - test5 - Multiple blocks can be overridden at once. When using the `block()` function, looked-up blocks will also use the overridden block rather than the one in the included template - test6 - When using the `parent()` function, looked-up blocks will use the parent block in the included template ## Integration Tests - test7 - The template being embedded may itself extend other templates. This may go many layers deep. - test8 - The parent templates of the embedded template may override blocks from their parents - test9 - The parent templates of the embedded template may override blocks from their parents. Different templates may override different blocks simultaneously - test10 - The embed tab can override blocks at the same time that parent templates do, and they are all resolved properly - test11 - `block()` and `parent()` functions can be used within the `embed` tag's overridden blocks to refer to it's parent template blocks, which are resolved with overrides properly - test12 - An embed tag can be used in a template that itself extends another template. The same parent template may be used in either case - test13 - The `extends` tag and `embed` tag maintain different template hierarchies. Blocks are resolved using the proper hierarchy for the scope the `block()` and `parent()` functions are used in - test14 - Blocks do not bleed from the parent template hierarchy into the embed template hierarchy. ## Error-checking Tests - test15 - Error thrown if content is added to the embed tag (instead of a block inside the embed) - test16 - Error thrown if content is added before a block in the the embed tag - test17 - Error thrown if content is added after a block in the the embed tag - test18 - Error thrown if content is added between blocks in the the embed tag ================================================ FILE: pebble/src/test/resources/templates/embed/test0/template.base.peb ================================================ BEFORE BASE {% block embedBlock1 %} EMBED BASE {% endblock %} AFTER BASE ================================================ FILE: pebble/src/test/resources/templates/embed/test0/template.peb ================================================ BEFORE ALL {% embed './template.base.peb' %}{% endembed %} AFTER ALL ================================================ FILE: pebble/src/test/resources/templates/embed/test0/template.result.twig.txt ================================================ BEFORE ALL BEFORE BASE EMBED BASE AFTER BASE AFTER ALL ================================================ FILE: pebble/src/test/resources/templates/embed/test0/template.result.txt ================================================ BEFORE ALL BEFORE BASE EMBED BASE AFTER BASE AFTER ALL ================================================ FILE: pebble/src/test/resources/templates/embed/test1/template.base.peb ================================================ BEFORE BASE {% block embedBlock1 %} EMBED BASE {% endblock %} AFTER BASE ================================================ FILE: pebble/src/test/resources/templates/embed/test1/template.peb ================================================ BEFORE ALL {% embed './template.base.peb' %} {% block embedBlock1 %} EMBED OVERRIDE {% endblock %} {% endembed %} AFTER ALL ================================================ FILE: pebble/src/test/resources/templates/embed/test1/template.result.twig.txt ================================================ BEFORE ALL BEFORE BASE EMBED OVERRIDE AFTER BASE AFTER ALL ================================================ FILE: pebble/src/test/resources/templates/embed/test1/template.result.txt ================================================ BEFORE ALL BEFORE BASE EMBED OVERRIDE AFTER BASE AFTER ALL ================================================ FILE: pebble/src/test/resources/templates/embed/test10/template.base.1.peb ================================================ {% extends 'template.base.2.peb' %} {% block block1 %} block1 base 1 {% endblock %} ================================================ FILE: pebble/src/test/resources/templates/embed/test10/template.base.2.peb ================================================ {% extends 'template.base.3.peb' %} {% block block2 %} block2 base 2 {% endblock %} ================================================ FILE: pebble/src/test/resources/templates/embed/test10/template.base.3.peb ================================================ BEFORE BASE 3 {% block block1 %} block1 base 3 {% endblock %} {% block block2 %} block2 base 3 {% endblock %} {% block block3 %} block3 base 3 {% endblock %} {% block block4 %} block4 base 3 {% endblock %} AFTER BASE 3 ================================================ FILE: pebble/src/test/resources/templates/embed/test10/template.peb ================================================ BEFORE ALL {% embed 'template.base.1.peb' %} {% block block4 %} block4 embed {% endblock %} {% endembed %} AFTER ALL ================================================ FILE: pebble/src/test/resources/templates/embed/test10/template.result.twig.txt ================================================ BEFORE ALL BEFORE BASE 3 block1 base 1 block2 base 2 block3 base 3 block4 embed AFTER BASE 3 AFTER ALL ================================================ FILE: pebble/src/test/resources/templates/embed/test10/template.result.txt ================================================ BEFORE ALL BEFORE BASE 3 block1 base 1 block2 base 2 block3 base 3 block4 embed AFTER BASE 3 AFTER ALL ================================================ FILE: pebble/src/test/resources/templates/embed/test11/template.base.1.peb ================================================ {% extends 'template.base.2.peb' %} {% block block1 %} block1 base 1 {% endblock %} ================================================ FILE: pebble/src/test/resources/templates/embed/test11/template.base.2.peb ================================================ {% extends 'template.base.3.peb' %} {% block block2 %} block2 base 2 {% endblock %} ================================================ FILE: pebble/src/test/resources/templates/embed/test11/template.base.3.peb ================================================ BEFORE BASE 3 {% block block1 %} block1 base 3 {% endblock %} {% block block2 %} block2 base 3 {% endblock %} {% block block3 %} block3 base 3 {% endblock %} {% block block4 %} block4 base 3 {% endblock %} AFTER BASE 3 ================================================ FILE: pebble/src/test/resources/templates/embed/test11/template.peb ================================================ BEFORE ALL {% embed 'template.base.1.peb' %} {% block block4 %} block4 embed parent={{ parent() }} block1={{ block('block1') }} block2={{ block('block2') }} block3={{ block('block3') }} {% endblock %} {% endembed %} AFTER ALL ================================================ FILE: pebble/src/test/resources/templates/embed/test11/template.result.twig.txt ================================================ BEFORE ALL BEFORE BASE 3 block1 base 1 block2 base 2 block3 base 3 block4 embed parent=block4 base 3 block1=block1 base 1 block2=block2 base 2 block3=block3 base 3 AFTER BASE 3 AFTER ALL ================================================ FILE: pebble/src/test/resources/templates/embed/test11/template.result.txt ================================================ BEFORE ALL BEFORE BASE 3 block1 base 1 block2 base 2 block3 base 3 block4 embed parent=block4 base 3 block1=block1 base 1 block2=block2 base 2 block3=block3 base 3 AFTER BASE 3 AFTER ALL ================================================ FILE: pebble/src/test/resources/templates/embed/test12/template.base.1.peb ================================================ {% extends 'template.base.2.peb' %} {% block block1 %} block1 base 1 {% endblock %} ================================================ FILE: pebble/src/test/resources/templates/embed/test12/template.base.2.peb ================================================ {% extends 'template.base.3.peb' %} {% block block2 %} block2 base 2 {% endblock %} ================================================ FILE: pebble/src/test/resources/templates/embed/test12/template.base.3.peb ================================================ BEFORE BASE 3 {% block block1 %} block1 base 3 {% endblock %} {% block block2 %} block2 base 3 {% endblock %} {% block block3 %} block3 base 3 {% endblock %} {% block block4 %} block4 base 3 {% endblock %} AFTER BASE 3 ================================================ FILE: pebble/src/test/resources/templates/embed/test12/template.peb ================================================ {% extends 'template.base.1.peb' %} {% block block4 %} BEFORE ALL {% embed 'template.base.1.peb' %} {% block block4 %} block4 embed parent={{ parent() }} block1={{ block('block1') }} block2={{ block('block2') }} block3={{ block('block3') }} {% endblock %} {% endembed %} AFTER ALL {% endblock %} ================================================ FILE: pebble/src/test/resources/templates/embed/test12/template.result.twig.txt ================================================ BEFORE BASE 3 block1 base 1 block2 base 2 block3 base 3 BEFORE ALL BEFORE BASE 3 block1 base 1 block2 base 2 block3 base 3 block4 embed parent=block4 base 3 block1=block1 base 1 block2=block2 base 2 block3=block3 base 3 AFTER BASE 3 AFTER ALL AFTER BASE 3 ================================================ FILE: pebble/src/test/resources/templates/embed/test12/template.result.txt ================================================ BEFORE BASE 3 block1 base 1 block2 base 2 block3 base 3 BEFORE ALL BEFORE BASE 3 block1 base 1 block2 base 2 block3 base 3 block4 embed parent=block4 base 3 block1=block1 base 1 block2=block2 base 2 block3=block3 base 3 AFTER BASE 3 AFTER ALL AFTER BASE 3 ================================================ FILE: pebble/src/test/resources/templates/embed/test13/template.embedbase.1.peb ================================================ {% extends 'template.embedbase.2.peb' %} {% block block1 %} block1 embed base 1 {% endblock %} ================================================ FILE: pebble/src/test/resources/templates/embed/test13/template.embedbase.2.peb ================================================ {% extends 'template.embedbase.3.peb' %} {% block block2 %} block2 embed base 2 {% endblock %} ================================================ FILE: pebble/src/test/resources/templates/embed/test13/template.embedbase.3.peb ================================================ BEFORE EMBED BASE 3 {% block block1 %} block1 embed base 3 {% endblock %} {% block block2 %} block2 embed base 3 {% endblock %} {% block block3 %} block3 embed base 3 {% endblock %} {% block block4 %} block4 embed base 3 {% endblock %} AFTER EMBED BASE 3 ================================================ FILE: pebble/src/test/resources/templates/embed/test13/template.extendsbase.1.peb ================================================ {% extends 'template.extendsbase.2.peb' %} {% block block1 %} block1 extends base 1 {% endblock %} ================================================ FILE: pebble/src/test/resources/templates/embed/test13/template.extendsbase.2.peb ================================================ {% extends 'template.extendsbase.3.peb' %} {% block block2 %} block2 extends base 2 {% endblock %} ================================================ FILE: pebble/src/test/resources/templates/embed/test13/template.extendsbase.3.peb ================================================ BEFORE EXTENDS BASE 3 {% block block1 %} block1 extends base 3 {% endblock %} {% block block2 %} block2 extends base 3 {% endblock %} {% block block3 %} block3 extends base 3 {% endblock %} {% block block4 %} block4 extends base 3 {% endblock %} AFTER EXTENDS BASE 3 ================================================ FILE: pebble/src/test/resources/templates/embed/test13/template.peb ================================================ {% extends 'template.extendsbase.1.peb' %} {% block block4 %} BEFORE ALL {% embed 'template.embedbase.1.peb' %} {% block block4 %} block4 embed embed parent={{ parent() }} block1={{ block('block1') }} block2={{ block('block2') }} block3={{ block('block3') }} {% endblock %} {% endembed %} AFTER ALL block4 extends extends parent={{ parent() }} block1={{ block('block1') }} block2={{ block('block2') }} block3={{ block('block3') }} {% endblock %} ================================================ FILE: pebble/src/test/resources/templates/embed/test13/template.result.twig.txt ================================================ BEFORE EXTENDS BASE 3 block1 extends base 1 block2 extends base 2 block3 extends base 3 BEFORE ALL BEFORE EMBED BASE 3 block1 embed base 1 block2 embed base 2 block3 embed base 3 block4 embed embed parent=block4 embed base 3 block1=block1 embed base 1 block2=block2 embed base 2 block3=block3 embed base 3 AFTER EMBED BASE 3 AFTER ALL block4 extends extends parent=block4 extends base 3 block1=block1 extends base 1 block2=block2 extends base 2 block3=block3 extends base 3 AFTER EXTENDS BASE 3 ================================================ FILE: pebble/src/test/resources/templates/embed/test13/template.result.txt ================================================ BEFORE EXTENDS BASE 3 block1 extends base 1 block2 extends base 2 block3 extends base 3 BEFORE ALL BEFORE EMBED BASE 3 block1 embed base 1 block2 embed base 2 block3 embed base 3 block4 embed embed parent=block4 embed base 3 block1=block1 embed base 1 block2=block2 embed base 2 block3=block3 embed base 3 AFTER EMBED BASE 3 AFTER ALL block4 extends extends parent=block4 extends base 3 block1=block1 extends base 1 block2=block2 extends base 2 block3=block3 extends base 3 AFTER EXTENDS BASE 3 ================================================ FILE: pebble/src/test/resources/templates/embed/test14/template.embedbase.1.peb ================================================ {% block block1 %} block1 embed base 1 {% endblock %} ================================================ FILE: pebble/src/test/resources/templates/embed/test14/template.extendsbase.1.peb ================================================ {% extends 'template.extendsbase.2.peb' %} {% block block1 %} block1 extends base 1 {% endblock %} ================================================ FILE: pebble/src/test/resources/templates/embed/test14/template.extendsbase.2.peb ================================================ {% extends 'template.extendsbase.3.peb' %} {% block block2 %} block2 extends base 2 {% endblock %} ================================================ FILE: pebble/src/test/resources/templates/embed/test14/template.extendsbase.3.peb ================================================ BEFORE EXTENDS BASE 3 {% block block1 %} block1 extends base 3 {% endblock %} {% block block2 %} block2 extends base 3 {% endblock %} {% block block3 %} block3 extends base 3 {% endblock %} {% block block4 %} block4 extends base 3 {% endblock %} AFTER EXTENDS BASE 3 ================================================ FILE: pebble/src/test/resources/templates/embed/test14/template.peb ================================================ {% extends 'template.extendsbase.1.peb' %} {% block block4 %} BEFORE ALL {% embed 'template.embedbase.1.peb' %} {% block block4 %} block4 embed embed parent={{ parent() }} block1={{ block('block1') }} block2={{ block('block2') }} block3={{ block('block3') }} {% endblock %} {% endembed %} AFTER ALL block4 extends extends parent={{ parent() }} block1={{ block('block1') }} block2={{ block('block2') }} block3={{ block('block3') }} {% endblock %} ================================================ FILE: pebble/src/test/resources/templates/embed/test14/template.result.twig.txt ================================================ BEFORE EXTENDS BASE 3 block1 extends base 1 block2 extends base 2 block3 extends base 3 BEFORE ALL block1 embed base 1 AFTER ALL block4 extends extends parent=block4 extends base 3 block1=block1 extends base 1 block2=block2 extends base 2 block3=block3 extends base 3 AFTER EXTENDS BASE 3 ================================================ FILE: pebble/src/test/resources/templates/embed/test14/template.result.txt ================================================ BEFORE EXTENDS BASE 3 block1 extends base 1 block2 extends base 2 block3 extends base 3 BEFORE ALL block1 embed base 1 AFTER ALL block4 extends extends parent=block4 extends base 3 block1=block1 extends base 1 block2=block2 extends base 2 block3=block3 extends base 3 AFTER EXTENDS BASE 3 ================================================ FILE: pebble/src/test/resources/templates/embed/test15/template.base.peb ================================================ {% block block1 %} block1 embed base 1 {% endblock %} ================================================ FILE: pebble/src/test/resources/templates/embed/test15/template.error.txt ================================================ A template that extends another one cannot include content outside blocks. Did you forget to put the content inside a {% block %} tag? (template.peb:3) ================================================ FILE: pebble/src/test/resources/templates/embed/test15/template.peb ================================================ {% embed 'template.base.peb' %} instead of blocks {% endembed %} ================================================ FILE: pebble/src/test/resources/templates/embed/test16/template.base.peb ================================================ {% block block1 %} block1 embed base 1 {% endblock %} ================================================ FILE: pebble/src/test/resources/templates/embed/test16/template.error.txt ================================================ A template that extends another one cannot include content outside blocks. Did you forget to put the content inside a {% block %} tag? (template.peb:3) ================================================ FILE: pebble/src/test/resources/templates/embed/test16/template.peb ================================================ {% embed 'template.base.peb' %} before blocks {% block block1 %} overridden {% endblock %} {% endembed %} ================================================ FILE: pebble/src/test/resources/templates/embed/test17/template.base.peb ================================================ {% block block1 %} block1 embed base 1 {% endblock %} ================================================ FILE: pebble/src/test/resources/templates/embed/test17/template.error.txt ================================================ A template that extends another one cannot include content outside blocks. Did you forget to put the content inside a {% block %} tag? (template.peb:6) ================================================ FILE: pebble/src/test/resources/templates/embed/test17/template.peb ================================================ {% embed 'template.base.peb' %} {% block block1 %} overridden {% endblock %} after blocks {% endembed %} ================================================ FILE: pebble/src/test/resources/templates/embed/test18/template.base.peb ================================================ {% block block1 %} block1 embed base {% endblock %} {% block block2 %} block2 embed base {% endblock %} ================================================ FILE: pebble/src/test/resources/templates/embed/test18/template.error.txt ================================================ A template that extends another one cannot include content outside blocks. Did you forget to put the content inside a {% block %} tag? (template.peb:6) ================================================ FILE: pebble/src/test/resources/templates/embed/test18/template.peb ================================================ {% embed 'template.base.peb' %} {% block block1 %} overridden {% endblock %} between blocks {% block block2 %} overridden {% endblock %} {% endembed %} ================================================ FILE: pebble/src/test/resources/templates/embed/test2/template.base.peb ================================================ BEFORE BASE {% block embedBlock1 %} EMBED BASE {% endblock %} AFTER BASE ================================================ FILE: pebble/src/test/resources/templates/embed/test2/template.peb ================================================ BEFORE ALL {% embed './template.base.peb' %} {% block 'embedBlock1' %} EMBED OVERRIDE {% endblock %} {% endembed %} AFTER ALL ================================================ FILE: pebble/src/test/resources/templates/embed/test2/template.result.txt ================================================ BEFORE ALL BEFORE BASE EMBED OVERRIDE AFTER BASE AFTER ALL ================================================ FILE: pebble/src/test/resources/templates/embed/test3/template.base.peb ================================================ BEFORE BASE [{{ foo }}] {% block embedBlock1 %} EMBED BASE [{{ foo }}] {% endblock %} AFTER BASE [{{ foo }}] ================================================ FILE: pebble/src/test/resources/templates/embed/test3/template.peb ================================================ BEFORE ALL [{{ foo }}] {% embed './template.base.peb' with {'foo': 'NEWFOO', 'bar': 'NEWBAR'} %} {% block 'embedBlock1' %} EMBED OVERRIDE [{{ bar }}] {% endblock %} {% endembed %} AFTER ALL [{{ bar }}] ================================================ FILE: pebble/src/test/resources/templates/embed/test3/template.result.txt ================================================ BEFORE ALL [FOO] BEFORE BASE [NEWFOO] EMBED OVERRIDE [NEWBAR] AFTER BASE [NEWFOO] AFTER ALL [BAR] ================================================ FILE: pebble/src/test/resources/templates/embed/test4/template.base.peb ================================================ BEFORE BASE [{{ foo }}] {% block embedBlock1 %} EMBED BASE [{{ foo }}] {% endblock %} AFTER BASE [{{ foo }}] ================================================ FILE: pebble/src/test/resources/templates/embed/test4/template.peb ================================================ BEFORE ALL [{{ foo }}] {% embed './template.base.peb' with {'foo': 'NEWFOO', 'bar': 'NEWBAR'} %} {% block embedBlock1 %} EMBED OVERRIDE [{{ bar }}] {% endblock %} {% endembed %} AFTER ALL [{{ bar }}] ================================================ FILE: pebble/src/test/resources/templates/embed/test4/template.result.twig.txt ================================================ BEFORE ALL [FOO] BEFORE BASE [NEWFOO] EMBED OVERRIDE [NEWBAR] AFTER BASE [NEWFOO] AFTER ALL [BAR] ================================================ FILE: pebble/src/test/resources/templates/embed/test4/template.result.txt ================================================ BEFORE ALL [FOO] BEFORE BASE [NEWFOO] EMBED OVERRIDE [NEWBAR] AFTER BASE [NEWFOO] AFTER ALL [BAR] ================================================ FILE: pebble/src/test/resources/templates/embed/test5/template.base.peb ================================================ BEFORE BASE [{{ foo }}] {% block embedBlock1 %} EMBED BASE [{{ foo }}] {% endblock %} {% block embedBlock2 %} EMBED BASE [{{ foo }}] {% endblock %} AFTER BASE [{{ foo }}] ================================================ FILE: pebble/src/test/resources/templates/embed/test5/template.peb ================================================ BEFORE ALL {% embed './template.base.peb' with {'foo': 'NEWFOO', 'bar': 'NEWBAR'} %} {% block embedBlock1 %} EMBED OVERRIDE [{{ bar }}] {% endblock %} {% block embedBlock2 %} {{ block('embedBlock1') }} {% endblock %} {% endembed %} AFTER ALL ================================================ FILE: pebble/src/test/resources/templates/embed/test5/template.result.twig.txt ================================================ BEFORE ALL BEFORE BASE [NEWFOO] EMBED OVERRIDE [NEWBAR] EMBED OVERRIDE [NEWBAR] AFTER BASE [NEWFOO] AFTER ALL ================================================ FILE: pebble/src/test/resources/templates/embed/test5/template.result.txt ================================================ BEFORE ALL BEFORE BASE [NEWFOO] EMBED OVERRIDE [NEWBAR] EMBED OVERRIDE [NEWBAR] AFTER BASE [NEWFOO] AFTER ALL ================================================ FILE: pebble/src/test/resources/templates/embed/test6/template.base.peb ================================================ BEFORE BASE [{{ foo }}] {% block embedBlock1 %} EMBED BASE [{{ foo }}] {% endblock %} AFTER BASE [{{ foo }}] ================================================ FILE: pebble/src/test/resources/templates/embed/test6/template.peb ================================================ BEFORE ALL {% embed './template.base.peb' with {'foo': 'NEWFOO', 'bar': 'NEWBAR'} %} {% block embedBlock1 %} EMBED OVERRIDE [{{ bar }}] ( parent={{ parent() }} ) {% endblock %} {% endembed %} AFTER ALL ================================================ FILE: pebble/src/test/resources/templates/embed/test6/template.result.twig.txt ================================================ BEFORE ALL BEFORE BASE [NEWFOO] EMBED OVERRIDE [NEWBAR] ( parent=EMBED BASE [NEWFOO] ) AFTER BASE [NEWFOO] AFTER ALL ================================================ FILE: pebble/src/test/resources/templates/embed/test6/template.result.txt ================================================ BEFORE ALL BEFORE BASE [NEWFOO] EMBED OVERRIDE [NEWBAR] ( parent=EMBED BASE [NEWFOO] ) AFTER BASE [NEWFOO] AFTER ALL ================================================ FILE: pebble/src/test/resources/templates/embed/test7/template.base.1.peb ================================================ {% extends 'template.base.2.peb' %} ================================================ FILE: pebble/src/test/resources/templates/embed/test7/template.base.2.peb ================================================ {% extends 'template.base.3.peb' %} ================================================ FILE: pebble/src/test/resources/templates/embed/test7/template.base.3.peb ================================================ BEFORE BASE 3 {% block block1 %} block1 base 3 {% endblock %} {% block block2 %} block2 base 3 {% endblock %} {% block block3 %} block3 base 3 {% endblock %} {% block block4 %} block4 base 3 {% endblock %} AFTER BASE 3 ================================================ FILE: pebble/src/test/resources/templates/embed/test7/template.peb ================================================ BEFORE ALL {% embed 'template.base.1.peb' %} {% endembed %} AFTER ALL ================================================ FILE: pebble/src/test/resources/templates/embed/test7/template.result.twig.txt ================================================ BEFORE ALL BEFORE BASE 3 block1 base 3 block2 base 3 block3 base 3 block4 base 3 AFTER BASE 3 AFTER ALL ================================================ FILE: pebble/src/test/resources/templates/embed/test7/template.result.txt ================================================ BEFORE ALL BEFORE BASE 3 block1 base 3 block2 base 3 block3 base 3 block4 base 3 AFTER BASE 3 AFTER ALL ================================================ FILE: pebble/src/test/resources/templates/embed/test8/template.base.1.peb ================================================ {% extends 'template.base.2.peb' %} ================================================ FILE: pebble/src/test/resources/templates/embed/test8/template.base.2.peb ================================================ {% extends 'template.base.3.peb' %} {% block block2 %} block2 base 2 {% endblock %} ================================================ FILE: pebble/src/test/resources/templates/embed/test8/template.base.3.peb ================================================ BEFORE BASE 3 {% block block1 %} block1 base 3 {% endblock %} {% block block2 %} block2 base 3 {% endblock %} {% block block3 %} block3 base 3 {% endblock %} {% block block4 %} block4 base 3 {% endblock %} AFTER BASE 3 ================================================ FILE: pebble/src/test/resources/templates/embed/test8/template.peb ================================================ BEFORE ALL {% embed 'template.base.1.peb' %} {% endembed %} AFTER ALL ================================================ FILE: pebble/src/test/resources/templates/embed/test8/template.result.twig.txt ================================================ BEFORE ALL BEFORE BASE 3 block1 base 3 block2 base 2 block3 base 3 block4 base 3 AFTER BASE 3 AFTER ALL ================================================ FILE: pebble/src/test/resources/templates/embed/test8/template.result.txt ================================================ BEFORE ALL BEFORE BASE 3 block1 base 3 block2 base 2 block3 base 3 block4 base 3 AFTER BASE 3 AFTER ALL ================================================ FILE: pebble/src/test/resources/templates/embed/test9/template.base.1.peb ================================================ {% extends 'template.base.2.peb' %} {% block block1 %} block1 base 1 {% endblock %} ================================================ FILE: pebble/src/test/resources/templates/embed/test9/template.base.2.peb ================================================ {% extends 'template.base.3.peb' %} {% block block2 %} block2 base 2 {% endblock %} ================================================ FILE: pebble/src/test/resources/templates/embed/test9/template.base.3.peb ================================================ BEFORE BASE 3 {% block block1 %} block1 base 3 {% endblock %} {% block block2 %} block2 base 3 {% endblock %} {% block block3 %} block3 base 3 {% endblock %} {% block block4 %} block4 base 3 {% endblock %} AFTER BASE 3 ================================================ FILE: pebble/src/test/resources/templates/embed/test9/template.peb ================================================ BEFORE ALL {% embed 'template.base.1.peb' %} {% endembed %} AFTER ALL ================================================ FILE: pebble/src/test/resources/templates/embed/test9/template.result.twig.txt ================================================ BEFORE ALL BEFORE BASE 3 block1 base 1 block2 base 2 block3 base 3 block4 base 3 AFTER BASE 3 AFTER ALL ================================================ FILE: pebble/src/test/resources/templates/embed/test9/template.result.txt ================================================ BEFORE ALL BEFORE BASE 3 block1 base 1 block2 base 2 block3 base 3 block4 base 3 AFTER BASE 3 AFTER ALL ================================================ FILE: pebble/src/test/resources/templates/function/template.block.peb ================================================ {# used by CoreFunctionsTest.testBlockFunction #} {% block title %}Default Title{% endblock title %} {{ block("title") }} ================================================ FILE: pebble/src/test/resources/templates/function/template.child.peb ================================================ {# used by CoreFunctionsTest.testParentFunction #} {% extends "templates/function/template.parent.peb" %} child text {% block head %} {{ parent() }} child head {% endblock head %} ================================================ FILE: pebble/src/test/resources/templates/function/template.childThenParentThenMacro.peb ================================================ {# used by CoreFunctionsTest.testParentThenMacro #} {% extends "templates/function/template.parentThenMacro.peb" %} {% block head %}{{ parent() }}{% endblock head %} ================================================ FILE: pebble/src/test/resources/templates/function/template.childWithContext.peb ================================================ {# used by CoreFunctionsTest.testParentBlockHasAccessToContext #} {% extends "templates/function/template.parentAccessContext.peb" %} {% block head %}{% set foo = 'bar' %}{{ parent() }}{% endblock head %} ================================================ FILE: pebble/src/test/resources/templates/function/template.parent.peb ================================================ {# used by CoreFunctionsTest.testParentFunction #} parent text {% block head %} parent head {% endblock head %} ================================================ FILE: pebble/src/test/resources/templates/function/template.parentAccessContext.peb ================================================ {# used by CoreFunctionsTest.testParentBlockHasAccessToContext #} {% block head %}{{ foo }}{% endblock head %} ================================================ FILE: pebble/src/test/resources/templates/function/template.parentThenMacro.peb ================================================ {# used by CoreFunctionsTest.testParentThenMacro #} {% block head %}{{ returnItem('test') }}{% endblock head %} {% macro returnItem(item) %}{{ item }}{% endmacro %} ================================================ FILE: pebble/src/test/resources/templates/function/template.subchild.peb ================================================ {# used by CoreFunctionsTest.testParentFunctionWithTwoLevels #} {% extends "templates/function/template.child.peb" %} sub child text {% block head %} {{ parent() }} sub child head {% endblock head %} ================================================ FILE: pebble/src/test/resources/templates/loader/template.loaderTest.peb ================================================ {# Used by: - LoaderTest.testClassLoaderLoaderWithNestedTemplate #} SUCCESS ================================================ FILE: pebble/src/test/resources/templates/macros/from.peb ================================================ Hello {% from "templates/macros/macro.peb" import test as macro_test, test2 as macro_test2 %} Call 1: {{ macro_test() }} Call 3: {{ macro_test2() }} {% include "templates/macros/include.peb" %} ================================================ FILE: pebble/src/test/resources/templates/macros/import.as.peb ================================================ Hello {% import "templates/macros/macro.peb" as macro %} Call 1: {{ macro.test() }} {% include "templates/macros/include.peb" %} ================================================ FILE: pebble/src/test/resources/templates/macros/include.peb ================================================ Include {% import "templates/macros/macro.peb" %} Call 2: {{ test() }} ================================================ FILE: pebble/src/test/resources/templates/macros/index.peb ================================================ Hello {% import "templates/macros/macro.peb" %} Call 1: {{ test() }} {% include "templates/macros/include.peb" %} ================================================ FILE: pebble/src/test/resources/templates/macros/invalid.from.peb ================================================ Hello {% from "templates/macros/macro.peb" import test as macro_test test2 as macro_test2 %} Call 1: {{ macro_test() }} Call 3: {{ macro_test2() }} {% include "templates/macros/include.peb" %} ================================================ FILE: pebble/src/test/resources/templates/macros/invalid.from.sameAlias.peb ================================================ Hello {% from "templates/macros/macro.peb" import test as macro_test%} {% from "templates/macros/macro.peb" import test2 as macro_test%} Call 1: {{ macro_test() }} Call 3: {{ macro_test2() }} {% include "templates/macros/include.peb" %} ================================================ FILE: pebble/src/test/resources/templates/macros/invalid.from.unknownMacro.peb ================================================ Hello {% from "templates/macros/macro.peb" import iDontExist as macro_test %} Call 1: {{ macro_test() }} ================================================ FILE: pebble/src/test/resources/templates/macros/invalid.import.as.sameAlias.peb ================================================ Hello {% import "templates/macros/macro.peb" as macro %} {% import "templates/macros/macro.peb" as macro %} Call 1: {{ macro.test() }} {% include "templates/macros/include.peb" %} ================================================ FILE: pebble/src/test/resources/templates/macros/invalid.macro.peb ================================================ {# line 1 #} {{ macroWhichDoesNotExists() }} {# line 3 #} ================================================ FILE: pebble/src/test/resources/templates/macros/macro.peb ================================================ {% macro test() %} Macro-Output: {{ "test" | testfilter }} {% endmacro %} {% macro test2() %} Macro-Output: {{ "test" | testfilter }} {% endmacro %} ================================================ FILE: pebble/src/test/resources/templates/macros/setVariableBase.peb ================================================ {{ unit }} {% import 'templates/macros/setVariableMacro.peb' %} {{ infantry() }} {{ unit }} ================================================ FILE: pebble/src/test/resources/templates/macros/setVariableMacro.peb ================================================ {% macro infantry() %} {% set unit = 'infantry' %} {{ unit }} {% endmacro %} ================================================ FILE: pebble/src/test/resources/templates/relativepath/subdirectory1/template.backwardslashes.peb ================================================ {# used by: TestRelativePath.testRelativeIncludeWithSubdirectory #} {% include '..\subdirectory2\template.relativeinclude2.peb' %} ================================================ FILE: pebble/src/test/resources/templates/relativepath/subdirectory1/template.forwardslashes.peb ================================================ {# used by: TestRelativePath.testRelativeIncludeWithSubdirectory #} {% include '../subdirectory2/template.relativeinclude2.peb' %} ================================================ FILE: pebble/src/test/resources/templates/relativepath/subdirectory2/template.relativeinclude2.peb ================================================ {# used by: TestRelativePath.testRelativeIncludeWithSubdirectory #} included ================================================ FILE: pebble/src/test/resources/templates/relativepath/template.relativeextends1.peb ================================================ {# used by: TestRelativePath.testRelativeExtends #} {% extends './template.relativeextends2.peb' %} {% block test %} overridden {% endblock %} ================================================ FILE: pebble/src/test/resources/templates/relativepath/template.relativeextends2.peb ================================================ {# used by: TestRelativePath.testRelativeExtends #}
    {% block test %} my test {% endblock %}
    ================================================ FILE: pebble/src/test/resources/templates/relativepath/template.relativeimport1.peb ================================================ {# used by: TestRelativePath.testRelativeImports #} {% import "./template.relativeimport2.peb" %} {{ input("company", "forcorp", "text") }} ================================================ FILE: pebble/src/test/resources/templates/relativepath/template.relativeimport2.peb ================================================ {# used by: TestRelativePath.testRelativeImports #} {% macro input(name, value, type) %} {% endmacro %} ================================================ FILE: pebble/src/test/resources/templates/relativepath/template.relativeinclude1.peb ================================================ {# used by: TestRelativePath.testRelativeInclude #} {% include './template.relativeinclude2.peb' %} ================================================ FILE: pebble/src/test/resources/templates/relativepath/template.relativeinclude2.peb ================================================ {# used by: TestRelativePath.testRelativeInclude #} included ================================================ FILE: pebble/src/test/resources/templates/single-block/template.renderextendedblock1.peb ================================================ {# used by RenderSingleBlockTest.testRenderSingleExtendedBlock #} {% extends "./template.renderextendedblock2.peb" %} Text in the template that should not appear in either block {% block content_a %}Block A{% endblock %} {% block content_b %}Block B{% endblock %} ================================================ FILE: pebble/src/test/resources/templates/single-block/template.renderextendedblock2.peb ================================================ {# used by RenderSingleBlockTest.testRenderSingleExtendedBlock #} Text in the parent template that should not appear in either block {% block container_a %}{% block content_a %}{% endblock %} extended{% endblock %} {% block container_b %}{% block content_b %}{% endblock %} extended{% endblock %} ================================================ FILE: pebble/src/test/resources/templates/template.autoescapeInclude1.peb ================================================ {# used by: EscaperExtensionTest.testAutoEscapingInclude #} {% include './template.autoescapeInclude2.peb' %} ================================================ FILE: pebble/src/test/resources/templates/template.autoescapeInclude2.peb ================================================ {# used by: EscaperExtensionTest.testAutoEscapingInclude #} <{{ text }}> ================================================ FILE: pebble/src/test/resources/templates/template.autoescapeParent1.peb ================================================ {# used by: EscaperExtensionTest.testAutoEscapingInclude #} {% extends 'templates/template.autoescapeParent2.peb' %} {% block nav %}{{ parent() }}{% endblock %} ================================================ FILE: pebble/src/test/resources/templates/template.autoescapeParent2.peb ================================================ {# used by: EscaperExtensionTest.testAutoEscapingInclude #} {% block nav %}<{{ text }}>{% endblock %} ================================================ FILE: pebble/src/test/resources/templates/template.child.peb ================================================ {# used by: - InheritanceTest.testMultiLevelInheritance #} {% extends "./template.parent.peb" %} {% block head %} CHILD HEAD {% endblock head %} ================================================ FILE: pebble/src/test/resources/templates/template.concurrent1.peb ================================================ {# used by: CacheTest.testConcurrentCacheHitting ConcurrentTest.testThreadSafeCompilationOfMultipleTemplates #} {{ test.a }}:{{ test.b }}:{{ test.c }} ================================================ FILE: pebble/src/test/resources/templates/template.concurrent2.peb ================================================ {# used by ConcurrentTest.testThreadSafeCompilationOfMultipleTemplates #} {{ test.a }}:{{ test.b }}:{{ test.c }} ================================================ FILE: pebble/src/test/resources/templates/template.dynamicChild.peb ================================================ {# used by: - InheritanceTest.testDynamicInheritance #} {% extends extendNumberOne? "./template.dynamicParent1.peb" : "./template.dynamicParent2.peb" %} ================================================ FILE: pebble/src/test/resources/templates/template.dynamicParent1.peb ================================================ {# used by: - InheritanceTest.testDynamicInheritance #} {% block head %}ONE{% endblock head %} ================================================ FILE: pebble/src/test/resources/templates/template.dynamicParent2.peb ================================================ {# used by: - InheritanceTest.testDynamicInheritance #} {% block head %}TWO{% endblock head %} ================================================ FILE: pebble/src/test/resources/templates/template.errorReporting.peb ================================================ {# used by ErrorReportingTest.testLineNumberErrorReportingDuringEvaluation #} {# comment #} content here {% block content %} {{ parent() }} {# error on line 7 due to no parent template#} {% endblock %} content here too ================================================ FILE: pebble/src/test/resources/templates/template.escapeCharactersInText.peb ================================================ {# used by CompilerTest.testEscapeCharactersText #} test multiple lines and tabs and of course: a bunch of escape characters! ' \' /\\//\/\////\\\ \b \f \n \r ' \\/\/" " \" \\ \t ================================================ FILE: pebble/src/test/resources/templates/template.general.peb ================================================ {# used by CompilerTest.testCompilationMutexIsAlwaysReleased #} test ================================================ FILE: pebble/src/test/resources/templates/template.grandfather.peb ================================================ {# used by: - CoreTagsTest.testBlock - CacheTest.templateCachedButBytecodeCleared - InheritanceTest.testSimpleInheritance - InheritanceTest.testMultiLevelInheritance #} GRANDFATHER TEXT ABOVE HEAD {% block head %} GRANDFATHER HEAD {% endblock %} GRANDFATHER TEXT BELOW HEAD AND ABOVE FOOT {% block foot %} GRANDFATHER FOOT {% endblock foot %} GRANDFATHER TEXT BELOW FOOT ================================================ FILE: pebble/src/test/resources/templates/template.import.dynamic.macro_ajax.peb ================================================ {# used by CoreTagsTest.testDynamicImport #} {% macro render_form() %} ajax macro {% endmacro %} ================================================ FILE: pebble/src/test/resources/templates/template.import.dynamic.macro_classic.peb ================================================ {# used by CoreTagsTest.testDynamicImport #} {% macro render_form() %} classic macro {% endmacro %} ================================================ FILE: pebble/src/test/resources/templates/template.import.dynamic.peb ================================================ {# used by CoreTagsTest.testDynamicImport #} {# depending on the boolean variable modern, different macro files will be #} {# imported. Both macro files provide a macro named render_form. #} {% import modern ? "templates/template.import.dynamic.macro_ajax.peb" : "templates/template.import.dynamic.macro_classic.peb" %} {{ render_form() }} ================================================ FILE: pebble/src/test/resources/templates/template.importWithinBlock.peb ================================================ {# used by CoreTagsTest.testImportWithinBlock #} {% block content %} {% import "templates/template.macro1.peb" %} {{ input("company", "forcorp", "text") }} {% endblock %} ================================================ FILE: pebble/src/test/resources/templates/template.include.dynamic.adminFooter.peb ================================================ {# used by CoreTagsTest.testDynamicInclude #} admin footer ================================================ FILE: pebble/src/test/resources/templates/template.include.dynamic.defaultFooter.peb ================================================ {# used by CoreTagsTest.testDynamicInclude #} default footer ================================================ FILE: pebble/src/test/resources/templates/template.include.dynamic.peb ================================================ {# used by CoreTagsTest.testDynamicInclude #} {# depending on the boolean variable admin, different templates will be included. #} {% include admin ? "templates/template.include.dynamic.adminFooter.peb" : "templates/template.include.dynamic.defaultFooter.peb" %} ================================================ FILE: pebble/src/test/resources/templates/template.include1.peb ================================================ {# used by CoreTagsTest.testInclude #} {% include "./template.include2.peb" %} TEMPLATE1 {% include "./template.include2.peb" %} ================================================ FILE: pebble/src/test/resources/templates/template.include2.peb ================================================ {# used by CoreTagsTest.testInclude #} TEMPLATE2 ================================================ FILE: pebble/src/test/resources/templates/template.includeInheritance1.peb ================================================ {# used by - CoreTagsTest.includeInheritance #} {% include './template.includeInheritance2.peb' %} ================================================ FILE: pebble/src/test/resources/templates/template.includeInheritance2.peb ================================================ {# used by - CoreTagsTest.includeInheritance #} {% extends './template.includeInheritance3.peb' %} {% block content %}success{% endblock %} ================================================ FILE: pebble/src/test/resources/templates/template.includeInheritance3.peb ================================================ {# used by - CoreTagsTest.includeInheritance #} {% block content %}fail{% endblock %} ================================================ FILE: pebble/src/test/resources/templates/template.includeOverrideBlock.peb ================================================ {# used by CoreTagsTest.testIncludeOverridesBlocks #} {% include "templates/template.includeOverrideBlock2.peb" %} {% block content %} ONE {% endblock %} {% include "templates/template.includeOverrideBlock2.peb" %} ================================================ FILE: pebble/src/test/resources/templates/template.includeOverrideBlock2.peb ================================================ {# used by CoreTagsTest.testIncludeOverridesBlocks #} {% block content %} TWO {% endblock %} ================================================ FILE: pebble/src/test/resources/templates/template.includeOverrideVariable1.peb ================================================ {# used by CoreTagsTest.testIncludeOverridesVariable #} {% set one = 'one (base)' %} {% set two = 'two (base)' %} {% include "templates/template.includeOverrideVariable2.peb" with {"one": "one (overridden)", "two": null} %} ================================================ FILE: pebble/src/test/resources/templates/template.includeOverrideVariable2.peb ================================================ {# used by CoreTagsTest.testIncludeOverridesVariable #} One: {{ one }} Two: {{ two }} ================================================ FILE: pebble/src/test/resources/templates/template.includePropagatesContext.peb ================================================ {# used by CoreTagsTest.testIncludePropagatesContext #} {% include "templates/template.includePropagatesContext2.peb" %} ================================================ FILE: pebble/src/test/resources/templates/template.includePropagatesContext2.peb ================================================ {# used by CoreTagsTest.testIncludePropagatesContext #} {{ name }} ================================================ FILE: pebble/src/test/resources/templates/template.includeWithParameter1.peb ================================================ {# used by IncludeWithParameterTest.testIncludeWithMapParameters #} {% include './template.includeWithParameter2.peb' with { 'simple': 'simple-value', 'contextVariable': contextVariable, 'level': level + 1, 'map': { 'position': 'left', 'contextVariable': contextVariable }} %} level-main:{{ level }} ================================================ FILE: pebble/src/test/resources/templates/template.includeWithParameter2.peb ================================================ simple:{{ simple }} contextVariable:{{ contextVariable }} map.position:{{ map.position }} map.contextVariable:{{ map.contextVariable }} level:{{ level }} {% set level = level + 1 %} ================================================ FILE: pebble/src/test/resources/templates/template.includeWithParameterNotIsolated1.peb ================================================ {# used by IncludeWithParameterTest.testIncludeWithMapParametersNotIsolated #} {% set foo = 'bar' %} {% include './template.includeWithParameterNotIsolated2.peb' with { 'foo' : 'baz' } %} {{ foo }} ================================================ FILE: pebble/src/test/resources/templates/template.includeWithParameterNotIsolated2.peb ================================================ {{ foo }} ================================================ FILE: pebble/src/test/resources/templates/template.includeWithParameterObject1.peb ================================================ {# used by IncludeWithParameterTest.testIncludeWithParameterObject #} {% include './template.includeWithParameterObject2.peb' with { 'sub' : object } %} ================================================ FILE: pebble/src/test/resources/templates/template.includeWithParameterObject2.peb ================================================ Hello {{ sub.title }} ================================================ FILE: pebble/src/test/resources/templates/template.includeWithinBlock.peb ================================================ {# used by CoreTagsTest.testIncludeWithinBlock #} {% block content %} {% include "templates/template.include2.peb" %} TEMPLATE1 {% endblock %} ================================================ FILE: pebble/src/test/resources/templates/template.loaderTest.peb ================================================ {# Used by: - LoaderTest.testClassLoaderLoader - LoaderTest.testFileLoader #} SUCCESS ================================================ FILE: pebble/src/test/resources/templates/template.loaderTest.peb.suffix ================================================ {# Used by: - LoaderTest.testClassLoaderLoader - LoaderTest.testFileLoader #} SUCCESS ================================================ FILE: pebble/src/test/resources/templates/template.macro.child.peb ================================================ {# used by CoreTagsTest.testImportInChildTemplateOutsideOfBlock #} {% extends "templates/template.macro.parent.peb" %} {% import "templates/template.macro1.peb" %} {% block content %}{{ input("company", "forcorp", "text") }}{% endblock %} ================================================ FILE: pebble/src/test/resources/templates/template.macro.exploding.peb ================================================ {{ m9() }} {% macro m0() %}1234567890{% endmacro %} {% macro m1() %}{{m0()}}{{m0()}}{{m0()}}{{m0()}}{{m0()}}{{m0()}}{{m0()}}{{m0()}}{{m0()}}{{m0()}}{% endmacro %} {% macro m2() %}{{m1()}}{{m1()}}{{m1()}}{{m1()}}{{m1()}}{{m1()}}{{m1()}}{{m1()}}{{m1()}}{{m1()}}{% endmacro %} {% macro m3() %}{{m2()}}{{m2()}}{{m2()}}{{m2()}}{{m2()}}{{m2()}}{{m2()}}{{m2()}}{{m2()}}{{m2()}}{% endmacro %} {% macro m4() %}{{m3()}}{{m3()}}{{m3()}}{{m3()}}{{m3()}}{{m3()}}{{m3()}}{{m3()}}{{m3()}}{{m3()}}{% endmacro %} {% macro m5() %}{{m4()}}{{m4()}}{{m4()}}{{m4()}}{{m4()}}{{m4()}}{{m4()}}{{m4()}}{{m4()}}{{m4()}}{% endmacro %} {% macro m6() %}{{m5()}}{{m5()}}{{m5()}}{{m5()}}{{m5()}}{{m5()}}{{m5()}}{{m5()}}{{m5()}}{{m5()}}{% endmacro %} {% macro m7() %}{{m6()}}{{m6()}}{{m6()}}{{m6()}}{{m6()}}{{m6()}}{{m6()}}{{m6()}}{{m6()}}{{m6()}}{% endmacro %} {% macro m8() %}{{m7()}}{{m7()}}{{m7()}}{{m7()}}{{m7()}}{{m7()}}{{m7()}}{{m7()}}{{m7()}}{{m7()}}{% endmacro %} {% macro m9() %}{{m8()}}{{m8()}}{{m8()}}{{m8()}}{{m8()}}{{m8()}}{{m8()}}{{m8()}}{{m8()}}{{m8()}}{% endmacro %} ================================================ FILE: pebble/src/test/resources/templates/template.macro.parent.peb ================================================ {# used by CoreTagsTest.testImportInChildTemplateOutsideOfBlock #} {% block content %}{% endblock %} ================================================ FILE: pebble/src/test/resources/templates/template.macro1.peb ================================================ {# used by CoreTagsTest.testMacro #} {{ input("company", "google", "text") }} {% macro input(name, value, type) %} {% endmacro %} ================================================ FILE: pebble/src/test/resources/templates/template.macro2.peb ================================================ {# used by CoreTagsTest.testMacroFromAnotherFile #} {% import "templates/template.macro1.peb" %} {{ input("company", "forcorp", "text") }} ================================================ FILE: pebble/src/test/resources/templates/template.macro3.peb ================================================ {# used by CoreTagsTest.testMacroBeingFiltered #} {{ returnItem("hello") | upper }} {% macro returnItem(item) %}{{ item }}{% endmacro %} ================================================ FILE: pebble/src/test/resources/templates/template.macroDouble.peb ================================================ {# used by CoreTagsTest.testMacroInvokedTwice #} {{ repeat('one') }} {{ repeat('two') }} {% macro repeat(input) %} {{ input }} {% endmacro %} ================================================ FILE: pebble/src/test/resources/templates/template.parallelInclude1.peb ================================================ {# used by CoreTagsTest.testIncludeWithinParallelTag #} {% parallel %}{% include "./template.parallelInclude2.peb" %}{% endparallel %} TEMPLATE1 {% include "./template.parallelInclude2.peb" %} ================================================ FILE: pebble/src/test/resources/templates/template.parallelInclude2.peb ================================================ {# used by CoreTagsTest.testIncludeWithinParallelTag #} {{ slowObject.first }} ================================================ FILE: pebble/src/test/resources/templates/template.parallelParsing1.peb ================================================ output in 1: a|{% delay 100 %}output in 1: b|{% delay 200 %}output in 1: c ================================================ FILE: pebble/src/test/resources/templates/template.parallelParsing2.peb ================================================ output in 2: a|{% delay 50 %}output in 2: b|{% delay 100 %}output in 2: c ================================================ FILE: pebble/src/test/resources/templates/template.parallelWithImport.peb ================================================ {# used by CoreTagsTest.testParallelWithImport #} {% import 'templates/template.parallelWithImport2.peb' %} {% parallel %}{{ test() }}{% endparallel %} ================================================ FILE: pebble/src/test/resources/templates/template.parallelWithImport2.peb ================================================ {# used by CoreTagsTest.testParallelWithImport #} {% macro test() %}success{% endmacro %} ================================================ FILE: pebble/src/test/resources/templates/template.parent.peb ================================================ {# used by: - CacheTest.templateCachedButBytecodeCleared - InheritanceTest.testSimpleInheritance - InheritanceTest.testMultiLevelInheritance #} {% extends "./template.grandfather.peb" %} {% block head %} PARENT HEAD {% endblock head %} ================================================ FILE: pebble/src/test/resources/templates/template.parent2.peb ================================================ {# used by CacheTest.templateCachedButBytecodeCleared #} {% extends "./template.grandfather.peb" %} {% block head %} PARENT HEAD {% endblock head %} ================================================ FILE: pebble/src/test/resources/templates/template.set.child.peb ================================================ {# used by CoreTagsTest.testSetInChildTemplateOutsideOfBlock #} {% extends "templates/template.set.parent.peb" %} {% set childDefinedVariable = 'SUCCESS' %} ================================================ FILE: pebble/src/test/resources/templates/template.set.parent.peb ================================================ {# used by CoreTagsTest.testSetInChildTemplateOutsideOfBlock #} {{ childDefinedVariable }} ================================================ FILE: pebble/src/test/resources/templates/template.skipGenerationBlock1.peb ================================================ {# used by - CoreTagsTest.skipGenerationBlock #} {% extends './template.skipGenerationBlock2.peb' %} {% block content %}success{% endblock %} ================================================ FILE: pebble/src/test/resources/templates/template.skipGenerationBlock2.peb ================================================ {# used by - CoreTagsTest.skipGenerationBlock #} {% extends './template.skipGenerationBlock3.peb' %} ================================================ FILE: pebble/src/test/resources/templates/template.skipGenerationBlock3.peb ================================================ {# used by - CoreTagsTest.skipGenerationBlock #} {% block content %}{% endblock %} ================================================ FILE: pebble/src/test/resources/templates/template.skipGenerationMacro1.peb ================================================ {# used by - CoreTagsTest.skipGenerationMacro #} {% extends './template.skipGenerationMacro2.peb' %} {% macro content() %}success{% endmacro %} ================================================ FILE: pebble/src/test/resources/templates/template.skipGenerationMacro2.peb ================================================ {# used by - CoreTagsTest.skipGenerationMacro #} {% extends './template.skipGenerationMacro3.peb' %} ================================================ FILE: pebble/src/test/resources/templates/template.skipGenerationMacro3.peb ================================================ {# used by - CoreTagsTest.skipGenerationMacro #} {{ content() }} {% macro content() %}{% endmacro %} ================================================ FILE: pebble/src/test/resources/templates/template.strictModeComplexExpression.peb ================================================

    {{ test.test }}

    ================================================ FILE: pebble/src/test/resources/templates/template.strictModeSimpleExpression.peb ================================================

    {{ test }}

    ================================================ FILE: pebble/src/test/resources/testMessages.properties ================================================ greeting=Hello greeting.someone=Hello, {0} ================================================ FILE: pebble/src/test/resources/testMessages_es_US.properties ================================================ greeting=Hola greeting.someone=Hola, {0} greeting.specialchars=Hola español ================================================ FILE: pebble-spring/pebble-legacy-spring-boot-starter/pom.xml ================================================ 4.0.0 pebble-spring io.pebbletemplates 4.1.2-SNAPSHOT pebble-legacy-spring-boot-starter Pebble Spring Boot 3 Starter Spring Boot 3 starter for Pebble Template Engine http://pebbletemplates.io 17 3.5.14 org.springframework.boot spring-boot-starter-web ${boot.version} true ch.qos.logback logback-classic org.springframework.boot spring-boot-starter-webflux ${boot.version} true io.pebbletemplates pebble-spring6 ${project.version} org.springframework.boot spring-boot-configuration-processor ${boot.version} true org.springframework.boot spring-boot-autoconfigure-processor ${boot.version} true org.springframework.boot spring-boot-starter-test ${boot.version} test maven-jar-plugin io.pebbletemplates.spring.boot ================================================ FILE: pebble-spring/pebble-legacy-spring-boot-starter/src/main/java/io/pebbletemplates/boot/autoconfigure/AbstractPebbleConfiguration.java ================================================ package io.pebbletemplates.boot.autoconfigure; abstract class AbstractPebbleConfiguration { protected String stripLeadingSlash(String value) { if (value == null) { return null; } if (value.startsWith("/")) { return value.substring(1); } return value; } } ================================================ FILE: pebble-spring/pebble-legacy-spring-boot-starter/src/main/java/io/pebbletemplates/boot/autoconfigure/PebbleAutoConfiguration.java ================================================ package io.pebbletemplates.boot.autoconfigure; import io.pebbletemplates.pebble.PebbleEngine; import io.pebbletemplates.pebble.attributes.methodaccess.MethodAccessValidator; import io.pebbletemplates.pebble.extension.Extension; import io.pebbletemplates.pebble.loader.ClasspathLoader; import io.pebbletemplates.pebble.loader.Loader; import io.pebbletemplates.pebble.node.ForNode; import io.pebbletemplates.pebble.node.expression.UnaryMinusExpression; import io.pebbletemplates.spring.extension.SpringExtension; import java.util.List; import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; import org.springframework.aot.hint.TypeReference; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.MessageSource; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; import org.springframework.lang.Nullable; @AutoConfiguration @ConditionalOnClass(PebbleEngine.class) @EnableConfigurationProperties(PebbleProperties.class) @Import({PebbleServletWebConfiguration.class, PebbleReactiveWebConfiguration.class}) public class PebbleAutoConfiguration extends AbstractPebbleConfiguration { @Bean @ConditionalOnMissingBean(name = "pebbleLoader") public Loader pebbleLoader(PebbleProperties properties) { ClasspathLoader loader = new ClasspathLoader(); loader.setCharset(properties.getCharsetName()); // classpath loader does not like leading slashes in resource paths loader.setPrefix(this.stripLeadingSlash(properties.getPrefix())); loader.setSuffix(properties.getSuffix()); return loader; } @Bean @ConditionalOnMissingBean public SpringExtension springExtension(MessageSource messageSource) { return new SpringExtension(messageSource); } @Bean @ConditionalOnMissingBean(name = "pebbleEngine") public PebbleEngine pebbleEngine(PebbleProperties properties, Loader pebbleLoader, SpringExtension springExtension, @Nullable List extensions, @Nullable MethodAccessValidator methodAccessValidator) { PebbleEngine.Builder builder = new PebbleEngine.Builder(); builder.loader(pebbleLoader); builder.extension(springExtension); if (extensions != null && !extensions.isEmpty()) { builder.extension(extensions.toArray(new Extension[extensions.size()])); } if (!properties.isCache()) { builder.cacheActive(false); } if (properties.getDefaultLocale() != null) { builder.defaultLocale(properties.getDefaultLocale()); } builder.strictVariables(properties.isStrictVariables()); builder.greedyMatchMethod(properties.isGreedyMatchMethod()); if (methodAccessValidator != null) { builder.methodAccessValidator(methodAccessValidator); } return builder.build(); } } class PebbleTemplatesHints implements RuntimeHintsRegistrar { @Override public void registerHints(RuntimeHints hints, ClassLoader classLoader) { hints.reflection() .registerType(TypeReference.of(UnaryMinusExpression.class), hint -> hint.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)) .registerType(TypeReference.of(ForNode.LoopVariables.class), hint -> hint.withMembers(MemberCategory.DECLARED_FIELDS, MemberCategory.DECLARED_CLASSES, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_METHODS)); } } ================================================ FILE: pebble-spring/pebble-legacy-spring-boot-starter/src/main/java/io/pebbletemplates/boot/autoconfigure/PebbleProperties.java ================================================ package io.pebbletemplates.boot.autoconfigure; import java.util.Locale; import org.springframework.boot.autoconfigure.template.AbstractTemplateViewResolverProperties; import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties("pebble") public class PebbleProperties extends AbstractTemplateViewResolverProperties { public static final String DEFAULT_PREFIX = "/templates/"; public static final String DEFAULT_SUFFIX = ".peb"; private Locale defaultLocale; private boolean strictVariables; private boolean greedyMatchMethod; public PebbleProperties() { super(DEFAULT_PREFIX, DEFAULT_SUFFIX); this.setCache(true); } public Locale getDefaultLocale() { return this.defaultLocale; } public void setDefaultLocale(Locale defaultLocale) { this.defaultLocale = defaultLocale; } public boolean isStrictVariables() { return this.strictVariables; } public void setStrictVariables(boolean strictVariables) { this.strictVariables = strictVariables; } public boolean isGreedyMatchMethod() { return this.greedyMatchMethod; } public void setGreedyMatchMethod(boolean greedyMatchMethod) { this.greedyMatchMethod = greedyMatchMethod; } } ================================================ FILE: pebble-spring/pebble-legacy-spring-boot-starter/src/main/java/io/pebbletemplates/boot/autoconfigure/PebbleReactiveWebConfiguration.java ================================================ package io.pebbletemplates.boot.autoconfigure; import io.pebbletemplates.pebble.PebbleEngine; import io.pebbletemplates.pebble.loader.ClasspathLoader; import io.pebbletemplates.spring.reactive.PebbleReactiveViewResolver; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication(type = Type.REACTIVE) class PebbleReactiveWebConfiguration extends AbstractPebbleConfiguration { @Bean @ConditionalOnMissingBean(name = "pebbleReactiveViewResolver") PebbleReactiveViewResolver pebbleReactiveViewResolver(PebbleProperties properties, PebbleEngine pebbleEngine) { String prefix = properties.getPrefix(); if (pebbleEngine.getLoader() instanceof ClasspathLoader) { // classpathloader doesn't like leading slashes in paths prefix = this.stripLeadingSlash(properties.getPrefix()); } PebbleReactiveViewResolver resolver = new PebbleReactiveViewResolver(pebbleEngine); resolver.setPrefix(prefix); resolver.setSuffix(properties.getSuffix()); resolver.setViewNames(properties.getViewNames()); resolver.setRequestContextAttribute(properties.getRequestContextAttribute()); return resolver; } } ================================================ FILE: pebble-spring/pebble-legacy-spring-boot-starter/src/main/java/io/pebbletemplates/boot/autoconfigure/PebbleServletWebConfiguration.java ================================================ package io.pebbletemplates.boot.autoconfigure; import io.pebbletemplates.pebble.PebbleEngine; import io.pebbletemplates.pebble.loader.ClasspathLoader; import io.pebbletemplates.spring.servlet.PebbleViewResolver; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication(type = Type.SERVLET) class PebbleServletWebConfiguration extends AbstractPebbleConfiguration { @Bean @ConditionalOnMissingBean(name = "pebbleViewResolver") PebbleViewResolver pebbleViewResolver(PebbleProperties properties, PebbleEngine pebbleEngine) { PebbleViewResolver pvr = new PebbleViewResolver(pebbleEngine); properties.applyToMvcViewResolver(pvr); if (pebbleEngine.getLoader() instanceof ClasspathLoader) { // classpathloader doesn't like leading slashes in paths pvr.setPrefix(this.stripLeadingSlash(properties.getPrefix())); } return pvr; } } ================================================ FILE: pebble-spring/pebble-legacy-spring-boot-starter/src/main/java/io/pebbletemplates/boot/autoconfigure/PebbleTemplateAvailabilityProvider.java ================================================ package io.pebbletemplates.boot.autoconfigure; import io.pebbletemplates.pebble.PebbleEngine; import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider; import org.springframework.core.env.Environment; import org.springframework.core.io.ResourceLoader; import org.springframework.util.ClassUtils; import static org.springframework.core.io.ResourceLoader.CLASSPATH_URL_PREFIX; public class PebbleTemplateAvailabilityProvider implements TemplateAvailabilityProvider { @Override public boolean isTemplateAvailable(String view, Environment environment, ClassLoader classLoader, ResourceLoader resourceLoader) { if (ClassUtils.isPresent(PebbleEngine.class.getCanonicalName(), classLoader)) { String prefix = environment.getProperty("pebble.prefix", PebbleProperties.DEFAULT_PREFIX); String suffix = environment.getProperty("pebble.suffix", PebbleProperties.DEFAULT_SUFFIX); return resourceLoader.getResource(CLASSPATH_URL_PREFIX + prefix + view + suffix).exists(); } return false; } } ================================================ FILE: pebble-spring/pebble-legacy-spring-boot-starter/src/main/java/io/pebbletemplates/boot/autoconfigure/package-info.java ================================================ /** * Auto-configuration for Pebble Template Engine. */ package io.pebbletemplates.boot.autoconfigure; ================================================ FILE: pebble-spring/pebble-legacy-spring-boot-starter/src/main/resources/META-INF/spring/aot.factories ================================================ org.springframework.aot.hint.RuntimeHintsRegistrar=\ io.pebbletemplates.boot.autoconfigure.PebbleTemplatesHints ================================================ FILE: pebble-spring/pebble-legacy-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports ================================================ io.pebbletemplates.boot.autoconfigure.PebbleAutoConfiguration ================================================ FILE: pebble-spring/pebble-legacy-spring-boot-starter/src/main/resources/META-INF/spring.factories ================================================ # Template availability providers org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider=\ io.pebbletemplates.boot.autoconfigure.PebbleTemplateAvailabilityProvider ================================================ FILE: pebble-spring/pebble-legacy-spring-boot-starter/src/test/java/io/pebbletemplates/boot/AppConfig.java ================================================ package io.pebbletemplates.boot; import io.pebbletemplates.pebble.extension.AbstractExtension; import io.pebbletemplates.pebble.extension.Extension; import io.pebbletemplates.pebble.extension.Function; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import org.springframework.context.MessageSource; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.support.ResourceBundleMessageSource; import org.springframework.web.servlet.LocaleResolver; import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @Configuration(proxyBeanMethods = false) public class AppConfig { @Bean public MessageSource messageSource() { ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource(); messageSource.setBasename("messages"); messageSource.setFallbackToSystemLocale(false); return messageSource; } @Bean public LocaleResolver localeResolver() { return new AcceptHeaderLocaleResolver(); } @Bean public Extension testExtension() { return new TestExtension(); } public static class TestExtension extends AbstractExtension { @Override public Map getFunctions() { Map functions = new HashMap(); functions.put("testFunction", new TestFunction()); return functions; } public static class TestFunction implements Function { @Override public List getArgumentNames() { return Collections.emptyList(); } @Override public Object execute(Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { return "Tested!"; } } } } ================================================ FILE: pebble-spring/pebble-legacy-spring-boot-starter/src/test/java/io/pebbletemplates/boot/Application.java ================================================ package io.pebbletemplates.boot; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } } ================================================ FILE: pebble-spring/pebble-legacy-spring-boot-starter/src/test/java/io/pebbletemplates/boot/Controllers.java ================================================ package io.pebbletemplates.boot; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.server.WebSession; @Controller public class Controllers { @RequestMapping("/hello.action") public String hello() { return "hello"; } @RequestMapping("/index.action") public String index() { return "index"; } @RequestMapping("/contextPath.action") public String contextPath() { return "contextPath"; } @RequestMapping("/extensions.action") public String extensions() { return "extensions"; } @RequestMapping("/beans.action") public String beans() { return "beans"; } @RequestMapping("/session.action") public String session(WebSession session) { session.getAttributes().put("foo", "bar"); return "session"; } @RequestMapping("/response.action") public String response() { return "responseObject"; } } ================================================ FILE: pebble-spring/pebble-legacy-spring-boot-starter/src/test/java/io/pebbletemplates/boot/Foo.java ================================================ package io.pebbletemplates.boot; import org.springframework.stereotype.Component; @Component public class Foo { public String value = "bar"; } ================================================ FILE: pebble-spring/pebble-legacy-spring-boot-starter/src/test/java/io/pebbletemplates/boot/autoconfigure/NonWebAppTests.java ================================================ package io.pebbletemplates.boot.autoconfigure; import io.pebbletemplates.boot.Application; import io.pebbletemplates.pebble.PebbleEngine; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import java.io.StringWriter; import static org.assertj.core.api.Assertions.assertThat; @SpringBootTest(classes = Application.class, properties = "spring.main.web-application-type=none") class NonWebAppTests { @Autowired private PebbleEngine pebbleEngine; @Test void testOk() throws Exception { StringWriter sw = new StringWriter(); this.pebbleEngine.getTemplate("hello").evaluate(sw); assertThat(sw.toString() != null && !sw.toString().isEmpty()).isTrue(); } } ================================================ FILE: pebble-spring/pebble-legacy-spring-boot-starter/src/test/java/io/pebbletemplates/boot/autoconfigure/PebbleAutoConfigurationTest.java ================================================ package io.pebbletemplates.boot.autoconfigure; import io.pebbletemplates.pebble.PebbleEngine; import io.pebbletemplates.pebble.attributes.methodaccess.BlacklistMethodAccessValidator; import io.pebbletemplates.pebble.attributes.methodaccess.MethodAccessValidator; import io.pebbletemplates.pebble.attributes.methodaccess.NoOpMethodAccessValidator; import io.pebbletemplates.pebble.loader.Loader; import io.pebbletemplates.spring.extension.SpringExtension; import io.pebbletemplates.spring.reactive.PebbleReactiveView; import io.pebbletemplates.spring.reactive.PebbleReactiveViewResolver; import io.pebbletemplates.spring.servlet.PebbleView; import io.pebbletemplates.spring.servlet.PebbleViewResolver; import org.junit.jupiter.api.Test; import org.springframework.boot.test.util.TestPropertyValues; import org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebApplicationContext; import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebApplicationContext; import org.springframework.context.MessageSource; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.web.reactive.result.view.UrlBasedViewResolver; import org.springframework.web.servlet.view.AbstractTemplateViewResolver; import java.util.Locale; import static java.util.Locale.CHINESE; import static java.util.Locale.FRENCH; import static org.assertj.core.api.Assertions.assertThat; class PebbleAutoConfigurationTest { private static final Locale DEFAULT_LOCALE = CHINESE; private static final Locale CUSTOM_LOCALE = FRENCH; private AnnotationConfigServletWebApplicationContext webContext; private AnnotationConfigReactiveWebApplicationContext reactiveWebContext; @Test void registerBeansForServletApp() { this.loadWithServlet(null); assertThat(this.webContext.getBeansOfType(Loader.class)).hasSize(1); assertThat(this.webContext.getBeansOfType(SpringExtension.class)).hasSize(1); assertThat(this.webContext.getBeansOfType(PebbleEngine.class)).hasSize(1); assertThat(this.webContext.getBean(PebbleEngine.class).getDefaultLocale()).isEqualTo(DEFAULT_LOCALE); assertThat(this.webContext.getBean(PebbleEngine.class).isStrictVariables()).isTrue(); assertThat(this.webContext.getBean(PebbleEngine.class).getEvaluationOptions().isGreedyMatchMethod()).isTrue(); assertThat(this.webContext.getBean(PebbleEngine.class).getEvaluationOptions().getMethodAccessValidator()).isInstanceOf(BlacklistMethodAccessValidator.class); assertThat(this.webContext.getBeansOfType(PebbleViewResolver.class)).hasSize(1); } @Test void registerCustomBeansForServletApp() { this.loadWithServlet(CustomPebbleViewResolverConfiguration.class); assertThat(this.webContext.getBeansOfType(PebbleViewResolver.class)).hasSize(0); assertThat(this.webContext.getBeansOfType(AbstractTemplateViewResolver.class)).hasSize(1); assertThat(this.webContext.getBeansOfType(CustomPebbleViewResolver.class)).hasSize(1); } @Test void registerCompilerForServletApp() { this.loadWithServlet(CustomPebbleEngineCompilerConfiguration.class); assertThat(this.webContext.getBeansOfType(Loader.class)).hasSize(1); assertThat(this.webContext.getBeansOfType(SpringExtension.class)).hasSize(1); assertThat(this.webContext.getBeansOfType(PebbleEngine.class)).hasSize(1); assertThat(this.webContext.getBean(PebbleEngine.class).getDefaultLocale()).isEqualTo(CUSTOM_LOCALE); assertThat(this.webContext.getBean(PebbleEngine.class).isStrictVariables()).isFalse(); assertThat(this.webContext.getBean(PebbleEngine.class).getEvaluationOptions().isGreedyMatchMethod()).isFalse(); assertThat(this.webContext.getBean(PebbleEngine.class).getEvaluationOptions().getMethodAccessValidator()).isInstanceOf(BlacklistMethodAccessValidator.class); assertThat(this.webContext.getBeansOfType(PebbleViewResolver.class)).hasSize(1); assertThat(this.webContext.getBeansOfType(PebbleReactiveViewResolver.class)).isEmpty(); } @Test void registerCustomMethodAccessValidatorForServletApp() { this.loadWithServlet(CustomMethodAccessValidatorConfiguration.class); assertThat(this.webContext.getBeansOfType(Loader.class)).hasSize(1); assertThat(this.webContext.getBeansOfType(SpringExtension.class)).hasSize(1); assertThat(this.webContext.getBeansOfType(PebbleEngine.class)).hasSize(1); assertThat(this.webContext.getBean(PebbleEngine.class).getDefaultLocale()).isEqualTo(DEFAULT_LOCALE); assertThat(this.webContext.getBean(PebbleEngine.class).isStrictVariables()).isTrue(); assertThat(this.webContext.getBean(PebbleEngine.class).getEvaluationOptions().isGreedyMatchMethod()).isTrue(); assertThat(this.webContext.getBean(PebbleEngine.class).getEvaluationOptions().getMethodAccessValidator()).isInstanceOf(NoOpMethodAccessValidator.class); assertThat(this.webContext.getBeansOfType(PebbleViewResolver.class)).hasSize(1); assertThat(this.webContext.getBeansOfType(PebbleReactiveViewResolver.class)).isEmpty(); } @Test void registerBeansForReactiveApp() { this.loadWithReactive(null); assertThat(this.reactiveWebContext.getBeansOfType(Loader.class)).hasSize(1); assertThat(this.reactiveWebContext.getBeansOfType(SpringExtension.class)).hasSize(1); assertThat(this.reactiveWebContext.getBeansOfType(PebbleEngine.class)).hasSize(1); assertThat(this.reactiveWebContext.getBean(PebbleEngine.class).getDefaultLocale()).isEqualTo(DEFAULT_LOCALE); assertThat(this.reactiveWebContext.getBean(PebbleEngine.class).isStrictVariables()).isTrue(); assertThat(this.reactiveWebContext.getBean(PebbleEngine.class).getEvaluationOptions().isGreedyMatchMethod()).isTrue(); assertThat(this.reactiveWebContext.getBean(PebbleEngine.class).getEvaluationOptions().getMethodAccessValidator()).isInstanceOf(BlacklistMethodAccessValidator.class); assertThat(this.reactiveWebContext.getBeansOfType(PebbleViewResolver.class)).isEmpty(); assertThat(this.reactiveWebContext.getBeansOfType(PebbleReactiveViewResolver.class)).hasSize(1); assertThat(this.reactiveWebContext.getBeansOfType(PebbleViewResolver.class)).isEmpty(); } @Test void registerCustomBeansForReactiveApp() { this.loadWithReactive(CustomPebbleReactiveViewResolverConfiguration.class); assertThat(this.reactiveWebContext.getBeansOfType(PebbleReactiveViewResolver.class)).hasSize(0); assertThat(this.reactiveWebContext.getBeansOfType(UrlBasedViewResolver.class)).hasSize(1); assertThat(this.reactiveWebContext.getBeansOfType(CustomPebbleReactiveViewResolver.class)).hasSize(1); } @Test void registerCompilerForReactiveApp() { this.loadWithReactive(CustomPebbleEngineCompilerConfiguration.class); assertThat(this.reactiveWebContext.getBeansOfType(Loader.class)).hasSize(1); assertThat(this.reactiveWebContext.getBeansOfType(SpringExtension.class)).hasSize(1); assertThat(this.reactiveWebContext.getBeansOfType(PebbleEngine.class)).hasSize(1); assertThat(this.reactiveWebContext.getBean(PebbleEngine.class).getDefaultLocale()).isEqualTo(CUSTOM_LOCALE); assertThat(this.reactiveWebContext.getBean(PebbleEngine.class).isStrictVariables()).isFalse(); assertThat(this.reactiveWebContext.getBean(PebbleEngine.class).getEvaluationOptions().isGreedyMatchMethod()).isFalse(); assertThat(this.reactiveWebContext.getBean(PebbleEngine.class).getEvaluationOptions().getMethodAccessValidator()).isInstanceOf(BlacklistMethodAccessValidator.class); assertThat(this.reactiveWebContext.getBeansOfType(PebbleReactiveViewResolver.class)).hasSize(1); assertThat(this.reactiveWebContext.getBeansOfType(PebbleViewResolver.class)).isEmpty(); } @Test void registerCustomMethodAccessValidatorForReactiveApp() { this.loadWithReactive(CustomMethodAccessValidatorConfiguration.class); assertThat(this.reactiveWebContext.getBeansOfType(Loader.class)).hasSize(1); assertThat(this.reactiveWebContext.getBeansOfType(SpringExtension.class)).hasSize(1); assertThat(this.reactiveWebContext.getBeansOfType(PebbleEngine.class)).hasSize(1); assertThat(this.reactiveWebContext.getBean(PebbleEngine.class).getDefaultLocale()).isEqualTo(DEFAULT_LOCALE); assertThat(this.reactiveWebContext.getBean(PebbleEngine.class).isStrictVariables()).isTrue(); assertThat(this.reactiveWebContext.getBean(PebbleEngine.class).getEvaluationOptions().isGreedyMatchMethod()).isTrue(); assertThat(this.reactiveWebContext.getBean(PebbleEngine.class).getEvaluationOptions().getMethodAccessValidator()).isInstanceOf(NoOpMethodAccessValidator.class); assertThat(this.reactiveWebContext.getBeansOfType(PebbleViewResolver.class)).isEmpty(); assertThat(this.reactiveWebContext.getBeansOfType(PebbleReactiveViewResolver.class)).hasSize(1); assertThat(this.reactiveWebContext.getBeansOfType(PebbleViewResolver.class)).isEmpty(); } private void loadWithServlet(Class config) { this.webContext = new AnnotationConfigServletWebApplicationContext(); TestPropertyValues.of("pebble.prefix=classpath:/templates/").applyTo(this.webContext); TestPropertyValues.of("pebble.defaultLocale=zh").applyTo(this.webContext); TestPropertyValues.of("pebble.strictVariables=true").applyTo(this.webContext); TestPropertyValues.of("pebble.greedyMatchMethod=true").applyTo(this.webContext); if (config != null) { this.webContext.register(config); } this.webContext.register(BaseConfiguration.class); this.webContext.refresh(); } private void loadWithReactive(Class config) { this.reactiveWebContext = new AnnotationConfigReactiveWebApplicationContext(); TestPropertyValues.of("pebble.prefix=classpath:/templates/").applyTo(this.reactiveWebContext); TestPropertyValues.of("pebble.defaultLocale=zh").applyTo(this.reactiveWebContext); TestPropertyValues.of("pebble.strictVariables=true").applyTo(this.reactiveWebContext); TestPropertyValues.of("pebble.greedyMatchMethod=true").applyTo(this.reactiveWebContext); if (config != null) { this.reactiveWebContext.register(config); } this.reactiveWebContext.register(BaseConfiguration.class); this.reactiveWebContext.refresh(); } @Configuration(proxyBeanMethods = false) @Import(PebbleAutoConfiguration.class) protected static class BaseConfiguration { } @Configuration(proxyBeanMethods = false) protected static class CustomPebbleEngineCompilerConfiguration { @Bean public PebbleEngine pebbleEngine() { return new PebbleEngine.Builder().defaultLocale(CUSTOM_LOCALE).build(); } @Bean public SpringExtension customSpringExtension(MessageSource messageSource) { return new SpringExtension(messageSource); } } @Configuration(proxyBeanMethods = false) protected static class CustomPebbleViewResolverConfiguration { @Bean public CustomPebbleViewResolver pebbleViewResolver() { return new CustomPebbleViewResolver(); } } @Configuration(proxyBeanMethods = false) protected static class CustomPebbleReactiveViewResolverConfiguration { @Bean public CustomPebbleReactiveViewResolver pebbleReactiveViewResolver() { return new CustomPebbleReactiveViewResolver(); } } protected static class CustomPebbleViewResolver extends AbstractTemplateViewResolver { public CustomPebbleViewResolver() { this.setViewClass(PebbleView.class); } } protected static class CustomPebbleReactiveViewResolver extends UrlBasedViewResolver { public CustomPebbleReactiveViewResolver() { this.setViewClass(PebbleReactiveView.class); } } @Configuration(proxyBeanMethods = false) protected static class CustomMethodAccessValidatorConfiguration { @Bean public MethodAccessValidator methodAccessValidator() { return new NoOpMethodAccessValidator(); } } } ================================================ FILE: pebble-spring/pebble-legacy-spring-boot-starter/src/test/java/io/pebbletemplates/boot/autoconfigure/ReactiveAppTest.java ================================================ package io.pebbletemplates.boot.autoconfigure; import io.pebbletemplates.boot.Application; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.test.web.reactive.server.WebTestClient; import static org.assertj.core.api.Assertions.assertThat; @SpringBootTest(classes = Application.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, properties = "spring.main.web-application-type=reactive") class ReactiveAppTest { @Autowired private WebTestClient client; @Test void testOk() throws Exception { String result = this.client.get().uri("/index.action").exchange() .expectStatus().isOk() .expectHeader().contentTypeCompatibleWith(MediaType.TEXT_HTML) .expectBody(String.class) .returnResult().getResponseBody(); assertThat(result).isEqualTo("Hello Pebbleworld!"); } @Test void testRequestAccess() throws Exception { String result = this.client.get().uri("/contextPath.action").exchange() .expectStatus().isOk() .expectHeader().contentTypeCompatibleWith(MediaType.TEXT_HTML) .expectBody(String.class) .returnResult().getResponseBody(); assertThat(result).isEqualTo("ctx path:/contextPath.action"); } @Test void testEnglishHello() throws Exception { String result = this.client.get().uri("/hello.action") .header("Accept-Language", "en").exchange() .expectStatus().isOk() .expectHeader().contentTypeCompatibleWith(MediaType.TEXT_HTML) .expectBody(String.class) .returnResult().getResponseBody(); assertThat(result).isEqualTo("Hello Boot!"); } @Test void testSpanishHello() throws Exception { String result = this.client.get().uri("/hello.action") .header("Accept-Language", "es").exchange() .expectStatus().isOk() .expectHeader().contentTypeCompatibleWith(MediaType.TEXT_HTML) .expectBody(String.class) .returnResult().getResponseBody(); assertThat(result).isEqualTo("Hola Boot!"); } @Test void testAdditionalExtensions() throws Exception { String result = this.client.get().uri("/extensions.action") .header("Accept-Language", "es").exchange() .expectStatus().isOk() .expectHeader().contentTypeCompatibleWith(MediaType.TEXT_HTML) .expectBody(String.class) .returnResult().getResponseBody(); assertThat(result).isEqualTo("Hola Boot! Tested!"); } @Test void testBeansAccess() throws Exception { String result = this.client.get().uri("/beans.action").exchange() .expectStatus().isOk() .expectHeader().contentTypeCompatibleWith(MediaType.TEXT_HTML) .expectBody(String.class) .returnResult().getResponseBody(); assertThat(result).isEqualTo("beans:bar"); } @Test void testResponseAccess() throws Exception { String result = this.client.get().uri("/response.action").exchange() .expectStatus().isOk() .expectHeader().contentTypeCompatibleWith(MediaType.TEXT_HTML) .expectBody(String.class) .returnResult().getResponseBody(); assertThat(result).isEqualTo("response:200 OK"); } @Test void testSessionAccess() { String result = this.client.get().uri("/session.action").exchange() .expectStatus().isOk() .expectHeader().contentTypeCompatibleWith(MediaType.TEXT_HTML) .expectBody(String.class) .returnResult().getResponseBody(); assertThat(result).isEqualTo("session:bar"); } } ================================================ FILE: pebble-spring/pebble-legacy-spring-boot-starter/src/test/java/io/pebbletemplates/boot/autoconfigure/ServletAppTest.java ================================================ package io.pebbletemplates.boot.autoconfigure; import io.pebbletemplates.boot.Application; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; import java.util.Locale; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @SpringBootTest(classes = Application.class, webEnvironment = WebEnvironment.RANDOM_PORT) class ServletAppTest { @Autowired private WebApplicationContext wac; protected MockMvc mockMvc; @BeforeEach void setup() { this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build(); } @Test void testOk() throws Exception { this.mockMvc.perform(get("/index.action")).andExpect(status().isOk()) .andExpect(content().contentTypeCompatibleWith(MediaType.TEXT_HTML)) .andExpect(content().string("Hello Pebbleworld!")); } @Test void testRequestAccess() throws Exception { MvcResult result = this.mockMvc.perform(get("/contextPath.action")).andExpect(status().isOk()) .andExpect(content().contentTypeCompatibleWith(MediaType.TEXT_HTML)).andReturn(); assertThat(result.getResponse().getContentAsString()).isEqualTo("ctx path:" + result.getRequest().getContextPath()); } @Test void testEnglishHello() throws Exception { this.mockMvc.perform(get("/hello.action").locale(Locale.forLanguageTag("en"))) .andExpect(status().isOk()) .andExpect(content().contentTypeCompatibleWith(MediaType.TEXT_HTML)) .andExpect(content().string("Hello Boot!")); } @Test void testSpanishHello() throws Exception { this.mockMvc.perform(get("/hello.action").locale(Locale.forLanguageTag("es"))) .andExpect(status().isOk()) .andExpect(content().contentTypeCompatibleWith(MediaType.TEXT_HTML)) .andExpect(content().string("Hola Boot!")); } @Test void testAdditionalExtensions() throws Exception { this.mockMvc.perform(get("/extensions.action").locale(Locale.forLanguageTag("es"))) .andExpect(status().isOk()) .andExpect(content().contentTypeCompatibleWith(MediaType.TEXT_HTML)) .andExpect(content().string("Hola Boot! Tested!")); } @Test void testBeansAccess() throws Exception { this.mockMvc.perform(get("/beans.action")) .andExpect(status().isOk()) .andExpect(content().contentTypeCompatibleWith(MediaType.TEXT_HTML)) .andExpect(content().string("beans:bar")); } @Test void testResponseAccess() throws Exception { this.mockMvc.perform(get("/response.action")) .andExpect(status().isOk()) .andExpect(content().contentTypeCompatibleWith(MediaType.TEXT_HTML)) .andExpect(content().string("response:200")); } } ================================================ FILE: pebble-spring/pebble-legacy-spring-boot-starter/src/test/resources/messages.properties ================================================ hello.boot=Hello Boot! ================================================ FILE: pebble-spring/pebble-legacy-spring-boot-starter/src/test/resources/messages_es.properties ================================================ hello.boot=Hola Boot! ================================================ FILE: pebble-spring/pebble-legacy-spring-boot-starter/src/test/resources/templates/beans.peb ================================================ beans:{{beans.foo.value}} ================================================ FILE: pebble-spring/pebble-legacy-spring-boot-starter/src/test/resources/templates/contextPath.peb ================================================ ctx path:{{request.contextPath}}{{request.path}} ================================================ FILE: pebble-spring/pebble-legacy-spring-boot-starter/src/test/resources/templates/extensions.peb ================================================ {{ message('hello.boot') }} {{ testFunction() }} ================================================ FILE: pebble-spring/pebble-legacy-spring-boot-starter/src/test/resources/templates/hello.peb ================================================ {{ message('hello.boot') }} ================================================ FILE: pebble-spring/pebble-legacy-spring-boot-starter/src/test/resources/templates/index.peb ================================================ {{'Hello Pebbleworld!'}} ================================================ FILE: pebble-spring/pebble-legacy-spring-boot-starter/src/test/resources/templates/native-image.peb ================================================ {{ foo.bar }} {{ foo["bar"] }} {{ bar[0] }} {{ "If life gives you lemons, eat lemons." | upper | abbreviate(13) }} {{ max(13, highscore) }} {% for article in articles %}

    {{ article }}

    {% else %}

    There are no articles.

    {% endfor %} {% if category == "news" %} {{ news }} {% elseif category == "sports" %} {{ sports }} {% else %}

    Please select a category

    {% endif %} {{ stringDate | date(existingFormat="yyyy-MMMM-d", format="yyyy/MMMM/d") }} {{ stringDate | date("yyyy/MMMM/d", existingFormat="yyyy-MMMM-d") }} {% set danger = "
    " %} {{ danger }} {% set danger = "
    " %} {{ danger | escape }} {# THIS IS A COMMENT #} {% if 3 is odd %} ... {% endif %} {% for article in articles %}

    number: ; content: {{ article }}

    {% else %}

    There are no articles.

    {% endfor %} {% if name is not null %} ... {% endif %} {{ folk ? "yes" : "no" }} {{ user.name | capitalize }} {% if user.name equals "Mitchell" %} David {% endif %} {% if bar contains 2 %} Two {% endif %} {% if (3 is not even) and (2 is odd or 3 is even) %} ... {% endif %} {% if user.telephone is empty %} ... {% endif %} {% if user is not map %} ... {% endif %} {% if user.email is null %} user.email is null {% endif %} {% if users is iterable %} {% for user in users %} User: {{ user }} {% endfor %} {% endif %} {{ i18n("messages","greetings") }} {% for i in range(0, 36, 2) %} {{ i }}, {% endfor %} {{ "this is a long sentence." | abbreviate(7) }} {{ -7 | abs }} {{ "dGVzdA==" | base64decode }} {{ "test" | base64encode }} {{ "article title" | capitalize }} {{ "July 24, 2001" | date("yyyy-MM-dd", existingFormat="MMMM dd, yyyy") }} {{ user.telephone | default("No phone number") }} {{ "
    " | escape }} {{ users | first }} {# will output the first item in the collection named 'users' #} {{ 'Mitch' | first }} {# will output 'M' #} {{ names | join(',') }} {# will output: Alex,Joe,Bob #} {{ users | last }} {# will output the last item in the collection named 'users' #} {{ 'Mitch' | last }} {# will output 'h' #} {% if users|length > 10 %} ... {% endif %} {{ "THIS IS A LOUD SENTENCE" | lower }} {{ 3.141592653 | numberformat("#.##") }} {% set danger = "
    " %} {{ danger | upper | raw }} {# ouptut:
    #} {% set danger = "
    " %} {{ danger | raw | upper }} {# output: <DIV> #} {{ "I like %this% and %that%." | replace({'%this%': foo, '%that%': "bar"}) }} {% for user in users | reverse %} {{ user }} {% endfor %} {% for user in users | rsort %} {{ user }} {% endfor %} {{ "test" | sha256 }} {{ ['apple', 'peach', 'pear', 'banana'] | slice(1,3) }} {# results in: [peach, pear] #} {{ 'Mitchell' | slice(1,3) }} {# results in: 'it' #} {% for user in users | sort %} {{ user }} {% endfor %} {% set foo = "one,two,three" | split(',') %} {# foo contains ['one', 'two', 'three'] #} {% set foo = "one,two,three,four,five" | split(',', 3) %} {# foo contains ['one', 'two', 'three,four,five'] #} {{ "article title" | title }} {{ " This text has too much whitespace. " | trim }} {{ "this is a quiet sentence." | upper }} {{ "The string ü@foo-bar" | urlencode }} {% block "post" %} content {% endblock %} {{ block("post") }} {{ block("post") }} {{ "this is a long sentence." | abbreviate(7) }} {{ danger }} {# will be escaped by default #} {% autoescape false %} {{ danger }} {# will not be escaped #} {% endautoescape %} {{ danger }} {# will use the "html" escaping strategy #} {% autoescape "js" %} {{ danger }} {# will use the "js" escaping strategy #} {% endautoescape %} {% cache 'menu' %} {% for item in items %} {{ item.text }} .... {% endfor %} {% endcache %} {% filter upper %} hello {% endfilter %} {% filter upper | escape %} hello
    {% endfilter %} {# output: 'HELLO<br>' #} {{ headerText }} {% flush %} {{ content }} {% for user in users %} {{ loop.index }} {{ loop.length }} {{ loop.first }} {{ loop.last }} {{ loop.revindex }} {% endfor %} {% include "advertisement.peb" with {"foo": "bar"} %} {% macro input(type="text", name, value) %} {% endmacro %} {{ input(name="country") }} {# will output: #} {% verbatim %} {% for user in users %} {{ user.name }} {% endfor %} {% endverbatim %} ================================================ FILE: pebble-spring/pebble-legacy-spring-boot-starter/src/test/resources/templates/responseObject.peb ================================================ response:{{response.status}}{{response.statusCode}} ================================================ FILE: pebble-spring/pebble-legacy-spring-boot-starter/src/test/resources/templates/session.peb ================================================ session:{{ session.attributes.foo }} ================================================ FILE: pebble-spring/pebble-spring-boot-starter/pom.xml ================================================ 4.0.0 pebble-spring io.pebbletemplates 4.1.2-SNAPSHOT pebble-spring-boot-starter Pebble Spring Boot 4 Starter Spring Boot 4 starter for Pebble Template Engine http://pebbletemplates.io 17 4.0.6 org.springframework.boot spring-boot-starter-webmvc ${boot.version} true ch.qos.logback logback-classic org.springframework.boot spring-boot-starter-webflux ${boot.version} true io.pebbletemplates pebble-spring7 ${project.version} org.springframework.boot spring-boot-starter-webflux-test ${boot.version} test org.springframework.boot spring-boot-starter-webmvc-test ${boot.version} test org.apache.maven.plugins maven-compiler-plugin maven-jar-plugin io.pebbletemplates.spring.boot ================================================ FILE: pebble-spring/pebble-spring-boot-starter/src/main/java/io/pebbletemplates/boot/autoconfigure/AbstractPebbleConfiguration.java ================================================ package io.pebbletemplates.boot.autoconfigure; abstract class AbstractPebbleConfiguration { protected String stripLeadingSlash(String value) { if (value == null) { return null; } if (value.startsWith("/")) { return value.substring(1); } return value; } } ================================================ FILE: pebble-spring/pebble-spring-boot-starter/src/main/java/io/pebbletemplates/boot/autoconfigure/PebbleAutoConfiguration.java ================================================ package io.pebbletemplates.boot.autoconfigure; import io.pebbletemplates.pebble.PebbleEngine; import io.pebbletemplates.pebble.attributes.methodaccess.MethodAccessValidator; import io.pebbletemplates.pebble.extension.Extension; import io.pebbletemplates.pebble.loader.ClasspathLoader; import io.pebbletemplates.pebble.loader.Loader; import io.pebbletemplates.pebble.node.ForNode; import io.pebbletemplates.pebble.node.expression.UnaryMinusExpression; import io.pebbletemplates.spring.extension.SpringExtension; import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; import org.springframework.aot.hint.TypeReference; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.MessageSource; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; import org.springframework.lang.Nullable; import java.util.List; @AutoConfiguration @ConditionalOnClass(PebbleEngine.class) @EnableConfigurationProperties(PebbleProperties.class) @Import({PebbleServletWebConfiguration.class, PebbleReactiveWebConfiguration.class}) public class PebbleAutoConfiguration extends AbstractPebbleConfiguration { @Bean @ConditionalOnMissingBean(name = "pebbleLoader") public Loader pebbleLoader(PebbleProperties properties) { ClasspathLoader loader = new ClasspathLoader(); loader.setCharset(properties.getCharsetName()); // classpath loader does not like leading slashes in resource paths loader.setPrefix(this.stripLeadingSlash(properties.getPrefix())); loader.setSuffix(properties.getSuffix()); return loader; } @Bean @ConditionalOnMissingBean public SpringExtension springExtension(MessageSource messageSource) { return new SpringExtension(messageSource); } @Bean @ConditionalOnMissingBean(name = "pebbleEngine") public PebbleEngine pebbleEngine(PebbleProperties properties, Loader pebbleLoader, SpringExtension springExtension, @Nullable List extensions, @Nullable MethodAccessValidator methodAccessValidator) { PebbleEngine.Builder builder = new PebbleEngine.Builder(); builder.loader(pebbleLoader); builder.extension(springExtension); if (extensions != null && !extensions.isEmpty()) { builder.extension(extensions.toArray(new Extension[extensions.size()])); } builder.cacheActive(properties.getServlet().isCache()); if (properties.getDefaultLocale() != null) { builder.defaultLocale(properties.getDefaultLocale()); } builder.strictVariables(properties.isStrictVariables()); builder.greedyMatchMethod(properties.isGreedyMatchMethod()); if (methodAccessValidator != null) { builder.methodAccessValidator(methodAccessValidator); } return builder.build(); } } class PebbleTemplatesHints implements RuntimeHintsRegistrar { @Override public void registerHints(RuntimeHints hints, ClassLoader classLoader) { hints.reflection() .registerType(TypeReference.of(UnaryMinusExpression.class), hint -> hint.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)) .registerType(TypeReference.of(ForNode.LoopVariables.class), hint -> hint.withMembers(MemberCategory.DECLARED_FIELDS, MemberCategory.DECLARED_CLASSES, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_METHODS)); } } ================================================ FILE: pebble-spring/pebble-spring-boot-starter/src/main/java/io/pebbletemplates/boot/autoconfigure/PebbleProperties.java ================================================ package io.pebbletemplates.boot.autoconfigure; import org.jspecify.annotations.Nullable; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.http.MediaType; import org.springframework.util.MimeType; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.function.Supplier; @ConfigurationProperties("pebble") public class PebbleProperties { private static final MimeType DEFAULT_CONTENT_TYPE = MimeType.valueOf("text/html"); private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; public static final String DEFAULT_PREFIX = "/templates/"; public static final String DEFAULT_SUFFIX = ".peb"; private Locale defaultLocale; private boolean strictVariables; private boolean greedyMatchMethod; private final Servlet servlet = new Servlet(this::getCharset); private final Reactive reactive = new Reactive(); /** * View names that can be resolved. */ private String @Nullable [] viewNames; /** * Name of the RequestContext attribute for all views. */ private @Nullable String requestContextAttribute; /** * Template encoding. */ private Charset charset = DEFAULT_CHARSET; /** * Whether to check that the templates location exists. */ private boolean checkTemplateLocation = true; /** * Prefix to apply to template names. */ private String prefix = DEFAULT_PREFIX; /** * Suffix to apply to template names. */ private String suffix = DEFAULT_SUFFIX; public Servlet getServlet() { return this.servlet; } public Reactive getReactive() { return this.reactive; } public String getPrefix() { return this.prefix; } public void setPrefix(String prefix) { this.prefix = prefix; } public String getSuffix() { return this.suffix; } public void setSuffix(String suffix) { this.suffix = suffix; } public String @Nullable [] getViewNames() { return this.viewNames; } public void setViewNames(String @Nullable [] viewNames) { this.viewNames = viewNames; } public @Nullable String getRequestContextAttribute() { return this.requestContextAttribute; } public void setRequestContextAttribute(@Nullable String requestContextAttribute) { this.requestContextAttribute = requestContextAttribute; } public Charset getCharset() { return this.charset; } public String getCharsetName() { return this.charset.name(); } public void setCharset(Charset charset) { this.charset = charset; } public boolean isCheckTemplateLocation() { return this.checkTemplateLocation; } public void setCheckTemplateLocation(boolean checkTemplateLocation) { this.checkTemplateLocation = checkTemplateLocation; } public static class Servlet { /** * Whether HttpServletRequest attributes are allowed to override (hide) controller * generated model attributes of the same name. */ private boolean allowRequestOverride = false; /** * Whether HttpSession attributes are allowed to override (hide) controller * generated model attributes of the same name. */ private boolean allowSessionOverride = false; /** * Whether to enable template caching. */ private boolean cache = true; /** * Content-Type value. */ private MimeType contentType = DEFAULT_CONTENT_TYPE; /** * Whether all request attributes should be added to the model prior to merging * with the template. */ private boolean exposeRequestAttributes = false; /** * Whether all HttpSession attributes should be added to the model prior to * merging with the template. */ private boolean exposeSessionAttributes = false; /** * Whether to expose a RequestContext for use by Spring's macro library, under the * name "springMacroRequestContext". */ private boolean exposeSpringMacroHelpers = true; private final Supplier<@Nullable Charset> charset; public Servlet() { this.charset = () -> null; } private Servlet(Supplier<@Nullable Charset> charset) { this.charset = charset; } public boolean isAllowRequestOverride() { return this.allowRequestOverride; } public void setAllowRequestOverride(boolean allowRequestOverride) { this.allowRequestOverride = allowRequestOverride; } public boolean isAllowSessionOverride() { return this.allowSessionOverride; } public void setAllowSessionOverride(boolean allowSessionOverride) { this.allowSessionOverride = allowSessionOverride; } public boolean isCache() { return this.cache; } public void setCache(boolean cache) { this.cache = cache; } public MimeType getContentType() { if (this.contentType != null && this.contentType.getCharset() == null) { Charset charset = this.charset.get(); if (charset != null) { Map parameters = new LinkedHashMap<>(); parameters.put("charset", charset.name()); parameters.putAll(this.contentType.getParameters()); return new MimeType(this.contentType, parameters); } } return this.contentType; } public void setContentType(MimeType contentType) { this.contentType = contentType; } public boolean isExposeRequestAttributes() { return this.exposeRequestAttributes; } public void setExposeRequestAttributes(boolean exposeRequestAttributes) { this.exposeRequestAttributes = exposeRequestAttributes; } public boolean isExposeSessionAttributes() { return this.exposeSessionAttributes; } public void setExposeSessionAttributes(boolean exposeSessionAttributes) { this.exposeSessionAttributes = exposeSessionAttributes; } public boolean isExposeSpringMacroHelpers() { return this.exposeSpringMacroHelpers; } public void setExposeSpringMacroHelpers(boolean exposeSpringMacroHelpers) { this.exposeSpringMacroHelpers = exposeSpringMacroHelpers; } } public static class Reactive { /** * Media types supported by Mustache views. */ private @Nullable List mediaTypes; public @Nullable List getMediaTypes() { return this.mediaTypes; } public void setMediaTypes(@Nullable List mediaTypes) { this.mediaTypes = mediaTypes; } } public Locale getDefaultLocale() { return this.defaultLocale; } public void setDefaultLocale(Locale defaultLocale) { this.defaultLocale = defaultLocale; } public boolean isStrictVariables() { return this.strictVariables; } public void setStrictVariables(boolean strictVariables) { this.strictVariables = strictVariables; } public boolean isGreedyMatchMethod() { return this.greedyMatchMethod; } public void setGreedyMatchMethod(boolean greedyMatchMethod) { this.greedyMatchMethod = greedyMatchMethod; } } ================================================ FILE: pebble-spring/pebble-spring-boot-starter/src/main/java/io/pebbletemplates/boot/autoconfigure/PebbleReactiveWebConfiguration.java ================================================ package io.pebbletemplates.boot.autoconfigure; import io.pebbletemplates.pebble.PebbleEngine; import io.pebbletemplates.pebble.loader.ClasspathLoader; import io.pebbletemplates.spring.reactive.PebbleReactiveViewResolver; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; @Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication(type = Type.REACTIVE) class PebbleReactiveWebConfiguration extends AbstractPebbleConfiguration { @Bean @ConditionalOnMissingBean(name = "pebbleReactiveViewResolver") PebbleReactiveViewResolver pebbleReactiveViewResolver(PebbleProperties properties, PebbleEngine pebbleEngine) { PebbleReactiveViewResolver resolver = new PebbleReactiveViewResolver(pebbleEngine); PropertyMapper map = PropertyMapper.get(); map.from(() -> { String prefix = properties.getPrefix(); if (pebbleEngine.getLoader() instanceof ClasspathLoader) { // classpathloader doesn't like leading slashes in paths prefix = this.stripLeadingSlash(properties.getPrefix()); } return prefix; }).to(resolver::setPrefix); map.from(properties::getSuffix).to(resolver::setSuffix); map.from(properties::getViewNames).to(resolver::setViewNames); map.from(properties::getRequestContextAttribute).to(resolver::setRequestContextAttribute); map.from(properties.getReactive()::getMediaTypes).to(resolver::setSupportedMediaTypes); resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10); return resolver; } } ================================================ FILE: pebble-spring/pebble-spring-boot-starter/src/main/java/io/pebbletemplates/boot/autoconfigure/PebbleServletWebConfiguration.java ================================================ package io.pebbletemplates.boot.autoconfigure; import io.pebbletemplates.pebble.PebbleEngine; import io.pebbletemplates.pebble.loader.ClasspathLoader; import io.pebbletemplates.spring.servlet.PebbleViewResolver; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; @Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication(type = Type.SERVLET) class PebbleServletWebConfiguration extends AbstractPebbleConfiguration { @Bean @ConditionalOnMissingBean(name = "pebbleViewResolver") PebbleViewResolver pebbleViewResolver(PebbleProperties properties, PebbleEngine pebbleEngine) { PebbleViewResolver pvr = new PebbleViewResolver(pebbleEngine); String prefix = properties.getPrefix(); if (pebbleEngine.getLoader() instanceof ClasspathLoader) { // classpathloader doesn't like leading slashes in paths prefix = this.stripLeadingSlash(properties.getPrefix()); } pvr.setPrefix(prefix); pvr.setSuffix(properties.getSuffix()); pvr.setCache(properties.getServlet().isCache()); if (properties.getServlet().getContentType() != null) { pvr.setContentType(properties.getServlet().getContentType().toString()); } pvr.setViewNames(properties.getViewNames()); pvr.setExposeRequestAttributes(properties.getServlet().isExposeRequestAttributes()); pvr.setAllowRequestOverride(properties.getServlet().isAllowRequestOverride()); pvr.setAllowSessionOverride(properties.getServlet().isAllowSessionOverride()); pvr.setExposeSessionAttributes(properties.getServlet().isExposeSessionAttributes()); pvr.setExposeSpringMacroHelpers(properties.getServlet().isExposeSpringMacroHelpers()); pvr.setRequestContextAttribute(properties.getRequestContextAttribute()); pvr.setCharacterEncoding(properties.getCharsetName()); pvr.setOrder(Ordered.LOWEST_PRECEDENCE - 10); return pvr; } } ================================================ FILE: pebble-spring/pebble-spring-boot-starter/src/main/java/io/pebbletemplates/boot/autoconfigure/PebbleTemplateAvailabilityProvider.java ================================================ package io.pebbletemplates.boot.autoconfigure; import io.pebbletemplates.pebble.PebbleEngine; import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider; import org.springframework.core.env.Environment; import org.springframework.core.io.ResourceLoader; import org.springframework.util.ClassUtils; import static org.springframework.core.io.ResourceLoader.CLASSPATH_URL_PREFIX; public class PebbleTemplateAvailabilityProvider implements TemplateAvailabilityProvider { @Override public boolean isTemplateAvailable(String view, Environment environment, ClassLoader classLoader, ResourceLoader resourceLoader) { if (ClassUtils.isPresent(PebbleEngine.class.getCanonicalName(), classLoader)) { String prefix = environment.getProperty("pebble.prefix", PebbleProperties.DEFAULT_PREFIX); String suffix = environment.getProperty("pebble.suffix", PebbleProperties.DEFAULT_SUFFIX); return resourceLoader.getResource(CLASSPATH_URL_PREFIX + prefix + view + suffix).exists(); } return false; } } ================================================ FILE: pebble-spring/pebble-spring-boot-starter/src/main/java/io/pebbletemplates/boot/autoconfigure/package-info.java ================================================ /** * Auto-configuration for Pebble Template Engine. */ package io.pebbletemplates.boot.autoconfigure; ================================================ FILE: pebble-spring/pebble-spring-boot-starter/src/main/resources/META-INF/spring/aot.factories ================================================ org.springframework.aot.hint.RuntimeHintsRegistrar=\ io.pebbletemplates.boot.autoconfigure.PebbleTemplatesHints ================================================ FILE: pebble-spring/pebble-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports ================================================ io.pebbletemplates.boot.autoconfigure.PebbleAutoConfiguration ================================================ FILE: pebble-spring/pebble-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.webflux.test.autoconfigure.AutoConfigureWebFlux.imports ================================================ io.pebbletemplates.boot.autoconfigure.PebbleAutoConfiguration ================================================ FILE: pebble-spring/pebble-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureWebMvc.imports ================================================ io.pebbletemplates.boot.autoconfigure.PebbleAutoConfiguration ================================================ FILE: pebble-spring/pebble-spring-boot-starter/src/main/resources/META-INF/spring.factories ================================================ # Template availability providers org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider=\ io.pebbletemplates.boot.autoconfigure.PebbleTemplateAvailabilityProvider ================================================ FILE: pebble-spring/pebble-spring-boot-starter/src/test/java/io/pebbletemplates/boot/AppConfig.java ================================================ package io.pebbletemplates.boot; import io.pebbletemplates.pebble.extension.AbstractExtension; import io.pebbletemplates.pebble.extension.Extension; import io.pebbletemplates.pebble.extension.Function; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import org.springframework.context.MessageSource; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.support.ResourceBundleMessageSource; import org.springframework.web.servlet.LocaleResolver; import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @Configuration(proxyBeanMethods = false) public class AppConfig { @Bean public MessageSource messageSource() { ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource(); messageSource.setBasename("messages"); messageSource.setFallbackToSystemLocale(false); return messageSource; } @Bean public LocaleResolver localeResolver() { return new AcceptHeaderLocaleResolver(); } @Bean public Extension testExtension() { return new TestExtension(); } public static class TestExtension extends AbstractExtension { @Override public Map getFunctions() { Map functions = new HashMap(); functions.put("testFunction", new TestFunction()); return functions; } public static class TestFunction implements Function { @Override public List getArgumentNames() { return Collections.emptyList(); } @Override public Object execute(Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { return "Tested!"; } } } } ================================================ FILE: pebble-spring/pebble-spring-boot-starter/src/test/java/io/pebbletemplates/boot/Application.java ================================================ package io.pebbletemplates.boot; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } } ================================================ FILE: pebble-spring/pebble-spring-boot-starter/src/test/java/io/pebbletemplates/boot/Controllers.java ================================================ package io.pebbletemplates.boot; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.server.WebSession; @Controller public class Controllers { @RequestMapping("/hello.action") public String hello() { return "hello"; } @RequestMapping("/index.action") public String index() { return "index"; } @RequestMapping("/contextPath.action") public String contextPath() { return "contextPath"; } @RequestMapping("/extensions.action") public String extensions() { return "extensions"; } @RequestMapping("/beans.action") public String beans() { return "beans"; } @RequestMapping("/session.action") public String session(WebSession session) { session.getAttributes().put("foo", "bar"); return "session"; } @RequestMapping("/response.action") public String response() { return "responseObject"; } } ================================================ FILE: pebble-spring/pebble-spring-boot-starter/src/test/java/io/pebbletemplates/boot/Foo.java ================================================ package io.pebbletemplates.boot; import org.springframework.stereotype.Component; @Component public class Foo { public String value = "bar"; } ================================================ FILE: pebble-spring/pebble-spring-boot-starter/src/test/java/io/pebbletemplates/boot/autoconfigure/NonWebAppTests.java ================================================ package io.pebbletemplates.boot.autoconfigure; import io.pebbletemplates.boot.Application; import io.pebbletemplates.pebble.PebbleEngine; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import java.io.StringWriter; import static org.assertj.core.api.Assertions.assertThat; @SpringBootTest(classes = Application.class, properties = "spring.main.web-application-type=none") class NonWebAppTests { @Autowired private PebbleEngine pebbleEngine; @Test void testOk() throws Exception { StringWriter sw = new StringWriter(); this.pebbleEngine.getTemplate("hello").evaluate(sw); assertThat(sw.toString() != null && !sw.toString().isEmpty()).isTrue(); } } ================================================ FILE: pebble-spring/pebble-spring-boot-starter/src/test/java/io/pebbletemplates/boot/autoconfigure/PebbleAutoConfigurationTest.java ================================================ package io.pebbletemplates.boot.autoconfigure; import io.pebbletemplates.pebble.PebbleEngine; import io.pebbletemplates.pebble.attributes.methodaccess.BlacklistMethodAccessValidator; import io.pebbletemplates.pebble.attributes.methodaccess.MethodAccessValidator; import io.pebbletemplates.pebble.attributes.methodaccess.NoOpMethodAccessValidator; import io.pebbletemplates.pebble.cache.tag.NoOpTagCache; import io.pebbletemplates.pebble.cache.template.NoOpTemplateCache; import io.pebbletemplates.pebble.loader.Loader; import io.pebbletemplates.spring.extension.SpringExtension; import io.pebbletemplates.spring.reactive.PebbleReactiveView; import io.pebbletemplates.spring.reactive.PebbleReactiveViewResolver; import io.pebbletemplates.spring.servlet.PebbleView; import io.pebbletemplates.spring.servlet.PebbleViewResolver; import org.junit.jupiter.api.Test; import org.springframework.boot.test.util.TestPropertyValues; import org.springframework.boot.web.context.reactive.AnnotationConfigReactiveWebApplicationContext; import org.springframework.boot.web.context.servlet.AnnotationConfigServletWebApplicationContext; import org.springframework.context.MessageSource; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.web.reactive.result.view.UrlBasedViewResolver; import org.springframework.web.servlet.view.AbstractTemplateViewResolver; import java.util.Locale; import static java.util.Locale.CHINESE; import static java.util.Locale.FRENCH; import static org.assertj.core.api.Assertions.assertThat; class PebbleAutoConfigurationTest { private static final Locale DEFAULT_LOCALE = CHINESE; private static final Locale CUSTOM_LOCALE = FRENCH; private AnnotationConfigServletWebApplicationContext webContext; private AnnotationConfigReactiveWebApplicationContext reactiveWebContext; @Test void registerBeansForServletApp() { this.loadWithServlet(null); assertThat(this.webContext.getBeansOfType(Loader.class)).hasSize(1); assertThat(this.webContext.getBeansOfType(SpringExtension.class)).hasSize(1); assertThat(this.webContext.getBeansOfType(PebbleEngine.class)).hasSize(1); assertThat(this.webContext.getBean(PebbleEngine.class).getDefaultLocale()).isEqualTo(DEFAULT_LOCALE); assertThat(this.webContext.getBean(PebbleEngine.class).isStrictVariables()).isTrue(); assertThat(this.webContext.getBean(PebbleEngine.class).getEvaluationOptions().isGreedyMatchMethod()).isTrue(); assertThat(this.webContext.getBean(PebbleEngine.class).getEvaluationOptions().getMethodAccessValidator()).isInstanceOf(BlacklistMethodAccessValidator.class); assertThat(this.webContext.getBean(PebbleEngine.class).getTagCache()).isInstanceOf(NoOpTagCache.class); assertThat(this.webContext.getBean(PebbleEngine.class).getTemplateCache()).isInstanceOf(NoOpTemplateCache.class); assertThat(this.webContext.getBeansOfType(PebbleViewResolver.class)).hasSize(1); } @Test void registerCustomBeansForServletApp() { this.loadWithServlet(CustomPebbleViewResolverConfiguration.class); assertThat(this.webContext.getBeansOfType(PebbleViewResolver.class)).hasSize(0); assertThat(this.webContext.getBeansOfType(AbstractTemplateViewResolver.class)).hasSize(1); assertThat(this.webContext.getBeansOfType(CustomPebbleViewResolver.class)).hasSize(1); } @Test void registerCompilerForServletApp() { this.loadWithServlet(CustomPebbleEngineCompilerConfiguration.class); assertThat(this.webContext.getBeansOfType(Loader.class)).hasSize(1); assertThat(this.webContext.getBeansOfType(SpringExtension.class)).hasSize(1); assertThat(this.webContext.getBeansOfType(PebbleEngine.class)).hasSize(1); assertThat(this.webContext.getBean(PebbleEngine.class).getDefaultLocale()) .isEqualTo(CUSTOM_LOCALE); assertThat(this.webContext.getBean(PebbleEngine.class).isStrictVariables()).isFalse(); assertThat( this.webContext.getBean(PebbleEngine.class).getEvaluationOptions().isGreedyMatchMethod()) .isFalse(); assertThat(this.webContext.getBean(PebbleEngine.class).getEvaluationOptions() .getMethodAccessValidator()).isInstanceOf( BlacklistMethodAccessValidator.class); assertThat(this.webContext.getBeansOfType(PebbleViewResolver.class)).hasSize(1); assertThat(this.webContext.getBeansOfType(PebbleReactiveViewResolver.class)).isEmpty(); } @Test void registerCustomMethodAccessValidatorForServletApp() { this.loadWithServlet(CustomMethodAccessValidatorConfiguration.class); assertThat(this.webContext.getBeansOfType(Loader.class)).hasSize(1); assertThat(this.webContext.getBeansOfType(SpringExtension.class)).hasSize(1); assertThat(this.webContext.getBeansOfType(PebbleEngine.class)).hasSize(1); assertThat(this.webContext.getBean(PebbleEngine.class).getDefaultLocale()) .isEqualTo(DEFAULT_LOCALE); assertThat(this.webContext.getBean(PebbleEngine.class).isStrictVariables()).isTrue(); assertThat( this.webContext.getBean(PebbleEngine.class).getEvaluationOptions().isGreedyMatchMethod()) .isTrue(); assertThat(this.webContext.getBean(PebbleEngine.class).getEvaluationOptions() .getMethodAccessValidator()).isInstanceOf( NoOpMethodAccessValidator.class); assertThat(this.webContext.getBeansOfType(PebbleViewResolver.class)).hasSize(1); assertThat(this.webContext.getBeansOfType(PebbleReactiveViewResolver.class)).isEmpty(); } @Test void registerBeansForReactiveApp() { this.loadWithReactive(null); assertThat(this.reactiveWebContext.getBeansOfType(Loader.class)).hasSize(1); assertThat(this.reactiveWebContext.getBeansOfType(SpringExtension.class)).hasSize(1); assertThat(this.reactiveWebContext.getBeansOfType(PebbleEngine.class)).hasSize(1); assertThat(this.reactiveWebContext.getBean(PebbleEngine.class).getDefaultLocale()) .isEqualTo(DEFAULT_LOCALE); assertThat(this.reactiveWebContext.getBean(PebbleEngine.class).isStrictVariables()).isTrue(); assertThat(this.reactiveWebContext.getBean(PebbleEngine.class).getEvaluationOptions() .isGreedyMatchMethod()).isTrue(); assertThat(this.reactiveWebContext.getBean(PebbleEngine.class).getEvaluationOptions() .getMethodAccessValidator()).isInstanceOf( BlacklistMethodAccessValidator.class); assertThat(this.reactiveWebContext.getBeansOfType(PebbleViewResolver.class)).isEmpty(); assertThat(this.reactiveWebContext.getBeansOfType(PebbleReactiveViewResolver.class)).hasSize(1); assertThat(this.reactiveWebContext.getBeansOfType(PebbleViewResolver.class)).isEmpty(); } @Test void registerCustomBeansForReactiveApp() { this.loadWithReactive(CustomPebbleReactiveViewResolverConfiguration.class); assertThat(this.reactiveWebContext.getBeansOfType(PebbleReactiveViewResolver.class)).hasSize(0); assertThat(this.reactiveWebContext.getBeansOfType(UrlBasedViewResolver.class)).hasSize(1); assertThat(this.reactiveWebContext.getBeansOfType(CustomPebbleReactiveViewResolver.class)).hasSize(1); } @Test void registerCompilerForReactiveApp() { this.loadWithReactive(CustomPebbleEngineCompilerConfiguration.class); assertThat(this.reactiveWebContext.getBeansOfType(Loader.class)).hasSize(1); assertThat(this.reactiveWebContext.getBeansOfType(SpringExtension.class)).hasSize(1); assertThat(this.reactiveWebContext.getBeansOfType(PebbleEngine.class)).hasSize(1); assertThat(this.reactiveWebContext.getBean(PebbleEngine.class).getDefaultLocale()) .isEqualTo(CUSTOM_LOCALE); assertThat(this.reactiveWebContext.getBean(PebbleEngine.class).isStrictVariables()).isFalse(); assertThat(this.reactiveWebContext.getBean(PebbleEngine.class).getEvaluationOptions() .isGreedyMatchMethod()).isFalse(); assertThat(this.reactiveWebContext.getBean(PebbleEngine.class).getEvaluationOptions() .getMethodAccessValidator()).isInstanceOf( BlacklistMethodAccessValidator.class); assertThat(this.reactiveWebContext.getBeansOfType(PebbleReactiveViewResolver.class)).hasSize(1); assertThat(this.reactiveWebContext.getBeansOfType(PebbleViewResolver.class)).isEmpty(); } @Test void registerCustomMethodAccessValidatorForReactiveApp() { this.loadWithReactive(CustomMethodAccessValidatorConfiguration.class); assertThat(this.reactiveWebContext.getBeansOfType(Loader.class)).hasSize(1); assertThat(this.reactiveWebContext.getBeansOfType(SpringExtension.class)).hasSize(1); assertThat(this.reactiveWebContext.getBeansOfType(PebbleEngine.class)).hasSize(1); assertThat(this.reactiveWebContext.getBean(PebbleEngine.class).getDefaultLocale()) .isEqualTo(DEFAULT_LOCALE); assertThat(this.reactiveWebContext.getBean(PebbleEngine.class).isStrictVariables()).isTrue(); assertThat(this.reactiveWebContext.getBean(PebbleEngine.class).getEvaluationOptions() .isGreedyMatchMethod()).isTrue(); assertThat(this.reactiveWebContext.getBean(PebbleEngine.class).getEvaluationOptions() .getMethodAccessValidator()).isInstanceOf( NoOpMethodAccessValidator.class); assertThat(this.reactiveWebContext.getBeansOfType(PebbleViewResolver.class)).isEmpty(); assertThat(this.reactiveWebContext.getBeansOfType(PebbleReactiveViewResolver.class)).hasSize(1); assertThat(this.reactiveWebContext.getBeansOfType(PebbleViewResolver.class)).isEmpty(); } private void loadWithServlet(Class config) { this.webContext = new AnnotationConfigServletWebApplicationContext(); TestPropertyValues.of("pebble.prefix=classpath:/templates/").applyTo(this.webContext); TestPropertyValues.of("pebble.defaultLocale=zh").applyTo(this.webContext); TestPropertyValues.of("pebble.strictVariables=true").applyTo(this.webContext); TestPropertyValues.of("pebble.greedyMatchMethod=true").applyTo(this.webContext); TestPropertyValues.of("pebble.servlet.cache=false").applyTo(this.webContext); if (config != null) { this.webContext.register(config); } this.webContext.register(BaseConfiguration.class); this.webContext.refresh(); } private void loadWithReactive(Class config) { this.reactiveWebContext = new AnnotationConfigReactiveWebApplicationContext(); TestPropertyValues.of("pebble.prefix=classpath:/templates/").applyTo(this.reactiveWebContext); TestPropertyValues.of("pebble.defaultLocale=zh").applyTo(this.reactiveWebContext); TestPropertyValues.of("pebble.strictVariables=true").applyTo(this.reactiveWebContext); TestPropertyValues.of("pebble.greedyMatchMethod=true").applyTo(this.reactiveWebContext); if (config != null) { this.reactiveWebContext.register(config); } this.reactiveWebContext.register(BaseConfiguration.class); this.reactiveWebContext.refresh(); } @Configuration(proxyBeanMethods = false) @Import(PebbleAutoConfiguration.class) protected static class BaseConfiguration { } @Configuration(proxyBeanMethods = false) protected static class CustomPebbleEngineCompilerConfiguration { @Bean public PebbleEngine pebbleEngine() { return new PebbleEngine.Builder().defaultLocale(CUSTOM_LOCALE).build(); } @Bean public SpringExtension customSpringExtension(MessageSource messageSource) { return new SpringExtension(messageSource); } } @Configuration(proxyBeanMethods = false) protected static class CustomPebbleViewResolverConfiguration { @Bean public CustomPebbleViewResolver pebbleViewResolver() { return new CustomPebbleViewResolver(); } } @Configuration(proxyBeanMethods = false) protected static class CustomPebbleReactiveViewResolverConfiguration { @Bean public CustomPebbleReactiveViewResolver pebbleReactiveViewResolver() { return new CustomPebbleReactiveViewResolver(); } } protected static class CustomPebbleViewResolver extends AbstractTemplateViewResolver { public CustomPebbleViewResolver() { this.setViewClass(PebbleView.class); } } protected static class CustomPebbleReactiveViewResolver extends UrlBasedViewResolver { public CustomPebbleReactiveViewResolver() { this.setViewClass(PebbleReactiveView.class); } } @Configuration(proxyBeanMethods = false) protected static class CustomMethodAccessValidatorConfiguration { @Bean public MethodAccessValidator methodAccessValidator() { return new NoOpMethodAccessValidator(); } } } ================================================ FILE: pebble-spring/pebble-spring-boot-starter/src/test/java/io/pebbletemplates/boot/autoconfigure/ReactiveAppTest.java ================================================ package io.pebbletemplates.boot.autoconfigure; import io.pebbletemplates.boot.Application; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.webtestclient.autoconfigure.AutoConfigureWebTestClient; import org.springframework.http.MediaType; import org.springframework.test.web.reactive.server.WebTestClient; import static org.assertj.core.api.Assertions.assertThat; @SpringBootTest(classes = Application.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, properties = "spring.main.web-application-type=reactive") @AutoConfigureWebTestClient class ReactiveAppTest { @Autowired private WebTestClient client; @Test void testOk() throws Exception { String result = this.client.get().uri("/index.action").exchange() .expectStatus().isOk() .expectHeader().contentTypeCompatibleWith(MediaType.TEXT_HTML) .expectBody(String.class) .returnResult().getResponseBody(); assertThat(result).isEqualTo("Hello Pebbleworld!"); } @Test void testRequestAccess() throws Exception { String result = this.client.get().uri("/contextPath.action").exchange() .expectStatus().isOk() .expectHeader().contentTypeCompatibleWith(MediaType.TEXT_HTML) .expectBody(String.class) .returnResult().getResponseBody(); assertThat(result).isEqualTo("ctx path:/contextPath.action"); } @Test void testEnglishHello() throws Exception { String result = this.client.get().uri("/hello.action") .header("Accept-Language", "en").exchange() .expectStatus().isOk() .expectHeader().contentTypeCompatibleWith(MediaType.TEXT_HTML) .expectBody(String.class) .returnResult().getResponseBody(); assertThat(result).isEqualTo("Hello Boot!"); } @Test void testSpanishHello() throws Exception { String result = this.client.get().uri("/hello.action") .header("Accept-Language", "es").exchange() .expectStatus().isOk() .expectHeader().contentTypeCompatibleWith(MediaType.TEXT_HTML) .expectBody(String.class) .returnResult().getResponseBody(); assertThat(result).isEqualTo("Hola Boot!"); } @Test void testAdditionalExtensions() throws Exception { String result = this.client.get().uri("/extensions.action") .header("Accept-Language", "es").exchange() .expectStatus().isOk() .expectHeader().contentTypeCompatibleWith(MediaType.TEXT_HTML) .expectBody(String.class) .returnResult().getResponseBody(); assertThat(result).isEqualTo("Hola Boot! Tested!"); } @Test void testBeansAccess() throws Exception { String result = this.client.get().uri("/beans.action").exchange() .expectStatus().isOk() .expectHeader().contentTypeCompatibleWith(MediaType.TEXT_HTML) .expectBody(String.class) .returnResult().getResponseBody(); assertThat(result).isEqualTo("beans:bar"); } @Test void testResponseAccess() throws Exception { String result = this.client.get().uri("/response.action").exchange() .expectStatus().isOk() .expectHeader().contentTypeCompatibleWith(MediaType.TEXT_HTML) .expectBody(String.class) .returnResult().getResponseBody(); assertThat(result).isEqualTo("response:200 OK"); } @Test void testSessionAccess() { String result = this.client.get().uri("/session.action").exchange() .expectStatus().isOk() .expectHeader().contentTypeCompatibleWith(MediaType.TEXT_HTML) .expectBody(String.class) .returnResult().getResponseBody(); assertThat(result).isEqualTo("session:bar"); } } ================================================ FILE: pebble-spring/pebble-spring-boot-starter/src/test/java/io/pebbletemplates/boot/autoconfigure/ServletAppTest.java ================================================ package io.pebbletemplates.boot.autoconfigure; import io.pebbletemplates.boot.Application; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; import java.util.Locale; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @SpringBootTest(classes = Application.class, webEnvironment = WebEnvironment.RANDOM_PORT) class ServletAppTest { @Autowired private WebApplicationContext wac; protected MockMvc mockMvc; @BeforeEach void setup() { this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build(); } @Test void testOk() throws Exception { this.mockMvc.perform(get("/index.action")).andExpect(status().isOk()) .andExpect(content().contentTypeCompatibleWith(MediaType.TEXT_HTML)) .andExpect(content().string("Hello Pebbleworld!")); } @Test void testRequestAccess() throws Exception { MvcResult result = this.mockMvc.perform(get("/contextPath.action")).andExpect(status().isOk()) .andExpect(content().contentTypeCompatibleWith(MediaType.TEXT_HTML)).andReturn(); assertThat(result.getResponse().getContentAsString()).isEqualTo("ctx path:" + result.getRequest().getContextPath()); } @Test void testEnglishHello() throws Exception { this.mockMvc.perform(get("/hello.action").locale(Locale.forLanguageTag("en"))) .andExpect(status().isOk()) .andExpect(content().contentTypeCompatibleWith(MediaType.TEXT_HTML)) .andExpect(content().string("Hello Boot!")); } @Test void testSpanishHello() throws Exception { this.mockMvc.perform(get("/hello.action").locale(Locale.forLanguageTag("es"))) .andExpect(status().isOk()) .andExpect(content().contentTypeCompatibleWith(MediaType.TEXT_HTML)) .andExpect(content().string("Hola Boot!")); } @Test void testAdditionalExtensions() throws Exception { this.mockMvc.perform(get("/extensions.action").locale(Locale.forLanguageTag("es"))) .andExpect(status().isOk()) .andExpect(content().contentTypeCompatibleWith(MediaType.TEXT_HTML)) .andExpect(content().string("Hola Boot! Tested!")); } @Test void testBeansAccess() throws Exception { this.mockMvc.perform(get("/beans.action")) .andExpect(status().isOk()) .andExpect(content().contentTypeCompatibleWith(MediaType.TEXT_HTML)) .andExpect(content().string("beans:bar")); } @Test void testResponseAccess() throws Exception { this.mockMvc.perform(get("/response.action")) .andExpect(status().isOk()) .andExpect(content().contentTypeCompatibleWith(MediaType.TEXT_HTML)) .andExpect(content().string("response:200")); } } ================================================ FILE: pebble-spring/pebble-spring-boot-starter/src/test/resources/messages.properties ================================================ hello.boot=Hello Boot! ================================================ FILE: pebble-spring/pebble-spring-boot-starter/src/test/resources/messages_es.properties ================================================ hello.boot=Hola Boot! ================================================ FILE: pebble-spring/pebble-spring-boot-starter/src/test/resources/templates/beans.peb ================================================ beans:{{beans.foo.value}} ================================================ FILE: pebble-spring/pebble-spring-boot-starter/src/test/resources/templates/contextPath.peb ================================================ ctx path:{{request.contextPath}}{{request.path}} ================================================ FILE: pebble-spring/pebble-spring-boot-starter/src/test/resources/templates/extensions.peb ================================================ {{ message('hello.boot') }} {{ testFunction() }} ================================================ FILE: pebble-spring/pebble-spring-boot-starter/src/test/resources/templates/hello.peb ================================================ {{ message('hello.boot') }} ================================================ FILE: pebble-spring/pebble-spring-boot-starter/src/test/resources/templates/index.peb ================================================ {{'Hello Pebbleworld!'}} ================================================ FILE: pebble-spring/pebble-spring-boot-starter/src/test/resources/templates/native-image.peb ================================================ {{ foo.bar }} {{ foo["bar"] }} {{ bar[0] }} {{ "If life gives you lemons, eat lemons." | upper | abbreviate(13) }} {{ max(13, highscore) }} {% for article in articles %}

    {{ article }}

    {% else %}

    There are no articles.

    {% endfor %} {% if category == "news" %} {{ news }} {% elseif category == "sports" %} {{ sports }} {% else %}

    Please select a category

    {% endif %} {{ stringDate | date(existingFormat="yyyy-MMMM-d", format="yyyy/MMMM/d") }} {{ stringDate | date("yyyy/MMMM/d", existingFormat="yyyy-MMMM-d") }} {% set danger = "
    " %} {{ danger }} {% set danger = "
    " %} {{ danger | escape }} {# THIS IS A COMMENT #} {% if 3 is odd %} ... {% endif %} {% for article in articles %}

    number: ; content: {{ article }}

    {% else %}

    There are no articles.

    {% endfor %} {% if name is not null %} ... {% endif %} {{ folk ? "yes" : "no" }} {{ user.name | capitalize }} {% if user.name equals "Mitchell" %} David {% endif %} {% if bar contains 2 %} Two {% endif %} {% if (3 is not even) and (2 is odd or 3 is even) %} ... {% endif %} {% if user.telephone is empty %} ... {% endif %} {% if user is not map %} ... {% endif %} {% if user.email is null %} user.email is null {% endif %} {% if users is iterable %} {% for user in users %} User: {{ user }} {% endfor %} {% endif %} {{ i18n("messages","greetings") }} {% for i in range(0, 36, 2) %} {{ i }}, {% endfor %} {{ "this is a long sentence." | abbreviate(7) }} {{ -7 | abs }} {{ "dGVzdA==" | base64decode }} {{ "test" | base64encode }} {{ "article title" | capitalize }} {{ "July 24, 2001" | date("yyyy-MM-dd", existingFormat="MMMM dd, yyyy") }} {{ user.telephone | default("No phone number") }} {{ "
    " | escape }} {{ users | first }} {# will output the first item in the collection named 'users' #} {{ 'Mitch' | first }} {# will output 'M' #} {{ names | join(',') }} {# will output: Alex,Joe,Bob #} {{ users | last }} {# will output the last item in the collection named 'users' #} {{ 'Mitch' | last }} {# will output 'h' #} {% if users|length > 10 %} ... {% endif %} {{ "THIS IS A LOUD SENTENCE" | lower }} {{ 3.141592653 | numberformat("#.##") }} {% set danger = "
    " %} {{ danger | upper | raw }} {# ouptut:
    #} {% set danger = "
    " %} {{ danger | raw | upper }} {# output: <DIV> #} {{ "I like %this% and %that%." | replace({'%this%': foo, '%that%': "bar"}) }} {% for user in users | reverse %} {{ user }} {% endfor %} {% for user in users | rsort %} {{ user }} {% endfor %} {{ "test" | sha256 }} {{ ['apple', 'peach', 'pear', 'banana'] | slice(1,3) }} {# results in: [peach, pear] #} {{ 'Mitchell' | slice(1,3) }} {# results in: 'it' #} {% for user in users | sort %} {{ user }} {% endfor %} {% set foo = "one,two,three" | split(',') %} {# foo contains ['one', 'two', 'three'] #} {% set foo = "one,two,three,four,five" | split(',', 3) %} {# foo contains ['one', 'two', 'three,four,five'] #} {{ "article title" | title }} {{ " This text has too much whitespace. " | trim }} {{ "this is a quiet sentence." | upper }} {{ "The string ü@foo-bar" | urlencode }} {% block "post" %} content {% endblock %} {{ block("post") }} {{ block("post") }} {{ "this is a long sentence." | abbreviate(7) }} {{ danger }} {# will be escaped by default #} {% autoescape false %} {{ danger }} {# will not be escaped #} {% endautoescape %} {{ danger }} {# will use the "html" escaping strategy #} {% autoescape "js" %} {{ danger }} {# will use the "js" escaping strategy #} {% endautoescape %} {% cache 'menu' %} {% for item in items %} {{ item.text }} .... {% endfor %} {% endcache %} {% filter upper %} hello {% endfilter %} {% filter upper | escape %} hello
    {% endfilter %} {# output: 'HELLO<br>' #} {{ headerText }} {% flush %} {{ content }} {% for user in users %} {{ loop.index }} {{ loop.length }} {{ loop.first }} {{ loop.last }} {{ loop.revindex }} {% endfor %} {% include "advertisement.peb" with {"foo": "bar"} %} {% macro input(type="text", name, value) %} {% endmacro %} {{ input(name="country") }} {# will output: #} {% verbatim %} {% for user in users %} {{ user.name }} {% endfor %} {% endverbatim %} ================================================ FILE: pebble-spring/pebble-spring-boot-starter/src/test/resources/templates/responseObject.peb ================================================ response:{{response.status}}{{response.statusCode}} ================================================ FILE: pebble-spring/pebble-spring-boot-starter/src/test/resources/templates/session.peb ================================================ session:{{ session.attributes.foo }} ================================================ FILE: pebble-spring/pebble-spring6/pom.xml ================================================ 4.0.0 pebble-spring io.pebbletemplates 4.1.2-SNAPSHOT pebble-spring6 Pebble Integration with Spring 6.x Pebble Integration with Spring 6.x http://pebbletemplates.io 17 6.0.0 6.2.18 5.23.0 5.14.4 org.springframework spring-framework-bom ${spring-framework.version} pom import io.pebbletemplates pebble jakarta.servlet jakarta.servlet-api ${servlet-api.version} provided org.springframework spring-webmvc provided org.springframework spring-webflux provided org.springframework spring-test test org.junit.jupiter junit-jupiter ${junit-jupiter.version} test org.mockito mockito-junit-jupiter ${mockito-junit-jupiter.version} test maven-jar-plugin io.pebbletemplates.spring ================================================ FILE: pebble-spring/pebble-spring6/src/main/java/io/pebbletemplates/spring/context/Beans.java ================================================ /* * Copyright (c) 2013 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.spring.context; import java.util.Arrays; import java.util.Collection; import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; import org.springframework.context.ApplicationContext; import org.springframework.util.Assert; /** *

    * Special object made available to templates in Spring MVC applications in order to access beans in * the Application Context. *

    * * @author Eric Bussieres */ public class Beans implements Map { private final ApplicationContext ctx; public Beans(ApplicationContext ctx) { Assert.notNull(ctx, "Application Context cannot be null"); this.ctx = ctx; } @Override public void clear() { throw new UnsupportedOperationException("Method \"clear\" not supported in Beans object"); } @Override public boolean containsKey(Object key) { Assert.notNull(key, "Key cannot be null"); return this.ctx.containsBean(key.toString()); } @Override public boolean containsValue(Object value) { throw new UnsupportedOperationException( "Method \"containsValue\" not supported in Beans object"); } @Override public Set> entrySet() { throw new UnsupportedOperationException("Method \"entrySet\" not supported in Beans object"); } @Override public Object get(Object key) { Assert.notNull(key, "Key cannot be null"); return this.ctx.getBean(key.toString()); } @Override public boolean isEmpty() { return this.size() <= 0; } @Override public Set keySet() { return new LinkedHashSet<>(Arrays.asList(this.ctx.getBeanDefinitionNames())); } @Override public Object put(String key, Object value) { throw new UnsupportedOperationException("Method \"put\" not supported in Beans object"); } @Override public void putAll(Map m) { throw new UnsupportedOperationException("Method \"putAll\" not supported in Beans object"); } @Override public Object remove(Object key) { throw new UnsupportedOperationException("Method \"remove\" not supported in Beans object"); } @Override public int size() { return this.ctx.getBeanDefinitionCount(); } @Override public Collection values() { throw new UnsupportedOperationException("Method \"values\" not supported in Beans object"); } } ================================================ FILE: pebble-spring/pebble-spring6/src/main/java/io/pebbletemplates/spring/extension/SpringExtension.java ================================================ /* * Copyright (c) 2013 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.spring.extension; import io.pebbletemplates.pebble.extension.AbstractExtension; import io.pebbletemplates.pebble.extension.Function; import io.pebbletemplates.spring.extension.function.HrefFunction; import io.pebbletemplates.spring.extension.function.MessageSourceFunction; import io.pebbletemplates.spring.extension.function.bindingresult.GetAllErrorsFunction; import io.pebbletemplates.spring.extension.function.bindingresult.GetFieldErrorsFunction; import io.pebbletemplates.spring.extension.function.bindingresult.GetGlobalErrorsFunction; import io.pebbletemplates.spring.extension.function.bindingresult.HasErrorsFunction; import io.pebbletemplates.spring.extension.function.bindingresult.HasFieldErrorsFunction; import io.pebbletemplates.spring.extension.function.bindingresult.HasGlobalErrorsFunction; import java.util.HashMap; import java.util.Map; import org.springframework.context.MessageSource; /** *

    * Extension for PebbleEngine to add spring functionality *

    * * @author Eric Bussieres */ public class SpringExtension extends AbstractExtension { private final MessageSource messageSource; public SpringExtension(MessageSource messageSource) { this.messageSource = messageSource; } @Override public Map getFunctions() { Map functions = new HashMap<>(); functions .put(MessageSourceFunction.FUNCTION_NAME, new MessageSourceFunction(this.messageSource)); functions.put(HasErrorsFunction.FUNCTION_NAME, new HasErrorsFunction()); functions.put(HasGlobalErrorsFunction.FUNCTION_NAME, new HasGlobalErrorsFunction()); functions.put(HasFieldErrorsFunction.FUNCTION_NAME, new HasFieldErrorsFunction()); functions.put(GetAllErrorsFunction.FUNCTION_NAME, new GetAllErrorsFunction(this.messageSource)); functions.put(GetGlobalErrorsFunction.FUNCTION_NAME, new GetGlobalErrorsFunction(this.messageSource)); functions .put(GetFieldErrorsFunction.FUNCTION_NAME, new GetFieldErrorsFunction(this.messageSource)); functions.put(HrefFunction.FUNCTION_NAME, new HrefFunction()); return functions; } } ================================================ FILE: pebble-spring/pebble-spring6/src/main/java/io/pebbletemplates/spring/extension/function/HrefFunction.java ================================================ package io.pebbletemplates.spring.extension.function; import io.pebbletemplates.pebble.extension.Function; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import io.pebbletemplates.pebble.extension.NamedArguments; import jakarta.servlet.http.HttpServletRequest; import org.springframework.util.StringUtils; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import java.util.ArrayList; import java.util.List; import java.util.Map; /** * Pebble function which adds the context path to the given url * * @author Eric Bussieres */ public class HrefFunction implements Function { public static final String FUNCTION_NAME = "href"; protected static final String PARAM_URL = "url"; protected List argumentNames; private String contextPath; /** * Constructor */ public HrefFunction() { this.argumentNames = new ArrayList<>(); this.argumentNames.add(PARAM_URL); } /** * {@inheritDoc} * * @see Function#execute(Map, PebbleTemplate, EvaluationContext, int) */ @Override public Object execute(Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { StringBuffer result = new StringBuffer(); result.append(this.getContextPath()); this.addUrlParameter(args, result); return result.toString(); } private void addUrlParameter(Map args, StringBuffer result) { String url = (String) args.get(PARAM_URL); if (StringUtils.hasText(url)) { result.append(url); } } private String getContextPath() { if (this.contextPath == null) { this.contextPath = this.getRequest().getContextPath(); } return this.contextPath; } private HttpServletRequest getRequest() { ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes(); return attr.getRequest(); } /** * {@inheritDoc} * * @see NamedArguments#getArgumentNames() */ @Override public List getArgumentNames() { return this.argumentNames; } } ================================================ FILE: pebble-spring/pebble-spring6/src/main/java/io/pebbletemplates/spring/extension/function/MessageSourceFunction.java ================================================ /* * Copyright (c) 2013 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.spring.extension.function; import io.pebbletemplates.pebble.extension.Function; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Map; import org.springframework.context.MessageSource; /** *

    * Function available to templates in Spring MVC applications in order to resolve message in the * application context *

    * * @author Eric Bussieres */ public class MessageSourceFunction implements Function { public static final String FUNCTION_NAME = "message"; private final MessageSource messageSource; public MessageSourceFunction(MessageSource messageSource) { this.messageSource = messageSource; } @Override public Object execute(Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { String key = this.extractKey(args); List arguments = this.extractArguments(args); Locale locale = context.getLocale(); return this.messageSource.getMessage(key, arguments.toArray(), "???" + key + "???", locale); } private String extractKey(Map args) { return (String) args.get("0"); } private List extractArguments(Map args) { int i = 1; List arguments = new ArrayList<>(); while (args.containsKey(String.valueOf(i))) { Object param = args.get(String.valueOf(i)); arguments.add(param); i++; } return arguments; } @Override public List getArgumentNames() { return null; } } ================================================ FILE: pebble-spring/pebble-spring6/src/main/java/io/pebbletemplates/spring/extension/function/bindingresult/BaseBindingResultFunction.java ================================================ /* * Copyright (c) 2013 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.spring.extension.function.bindingresult; import io.pebbletemplates.pebble.extension.Function; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.GlobalContext; import org.springframework.validation.BindingResult; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * Base class of the function interacting with the BindingResult * * @author Eric Bussieres */ public abstract class BaseBindingResultFunction implements Function { protected static final String PARAM_FIELD_NAME = "fieldName"; protected static final String PARAM_FORM_NAME = "formName"; private final List argumentNames = new ArrayList<>(); protected BaseBindingResultFunction(String... argumentsName) { Collections.addAll(this.argumentNames, argumentsName); } @Override public List getArgumentNames() { return this.argumentNames; } protected BindingResult getBindingResult(String formName, EvaluationContext context) { String attribute = BindingResult.MODEL_KEY_PREFIX + formName; BindingResult bindingResult = (BindingResult) context.getVariable(attribute); if (bindingResult == null) { GlobalContext globalContext = (GlobalContext) context.getVariable("_context"); if (globalContext != null) { return (BindingResult) globalContext.get(attribute); } } return bindingResult; } } ================================================ FILE: pebble-spring/pebble-spring6/src/main/java/io/pebbletemplates/spring/extension/function/bindingresult/GetAllErrorsFunction.java ================================================ /* * Copyright (c) 2013 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.spring.extension.function.bindingresult; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Map; import org.springframework.context.MessageSource; import org.springframework.validation.BindingResult; import org.springframework.validation.ObjectError; /** *

    * Function available to templates in Spring MVC applications in order to access the BindingResult * of a form *

    * * @author Eric Bussieres */ public class GetAllErrorsFunction extends BaseBindingResultFunction { public static final String FUNCTION_NAME = "getAllErrors"; private final MessageSource messageSource; public GetAllErrorsFunction(MessageSource messageSource) { super(PARAM_FORM_NAME); this.messageSource = messageSource; } @Override public Object execute(Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { String formName = (String) args.get(PARAM_FORM_NAME); Locale locale = context.getLocale(); BindingResult bindingResult = this.getBindingResult(formName, context); return this.constructErrorMessage(locale, bindingResult); } private List constructErrorMessage(Locale locale, BindingResult bindingResult) { List errorMessages = new ArrayList<>(); if (bindingResult != null) { for (ObjectError error : bindingResult.getAllErrors()) { String msg = this.messageSource.getMessage(error, locale); errorMessages.add(msg); } } return errorMessages; } } ================================================ FILE: pebble-spring/pebble-spring6/src/main/java/io/pebbletemplates/spring/extension/function/bindingresult/GetFieldErrorsFunction.java ================================================ /* * Copyright (c) 2013 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.spring.extension.function.bindingresult; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Map; import org.springframework.context.MessageSource; import org.springframework.validation.BindingResult; import org.springframework.validation.FieldError; /** *

    * Function available to templates in Spring MVC applications in order to access the BindingResult * of a form *

    * * @author Eric Bussieres */ public class GetFieldErrorsFunction extends BaseBindingResultFunction { public static final String FUNCTION_NAME = "getFieldErrors"; private final MessageSource messageSource; public GetFieldErrorsFunction(MessageSource messageSource) { super(PARAM_FORM_NAME, PARAM_FIELD_NAME); this.messageSource = messageSource; } @Override public Object execute(Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { String formName = (String) args.get(PARAM_FORM_NAME); String field = (String) args.get(PARAM_FIELD_NAME); if (field == null) { throw new IllegalArgumentException("Field parameter is required in GetFieldErrorsFunction"); } Locale locale = context.getLocale(); BindingResult bindingResult = this.getBindingResult(formName, context); return this.constructErrorMessages(field, locale, bindingResult); } private List constructErrorMessages(String field, Locale locale, BindingResult bindingResult) { List errorMessages = new ArrayList<>(); if (bindingResult != null) { for (FieldError error : bindingResult.getFieldErrors(field)) { String msg = this.messageSource.getMessage(error, locale); errorMessages.add(msg); } } return errorMessages; } } ================================================ FILE: pebble-spring/pebble-spring6/src/main/java/io/pebbletemplates/spring/extension/function/bindingresult/GetGlobalErrorsFunction.java ================================================ /* * Copyright (c) 2013 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.spring.extension.function.bindingresult; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Map; import org.springframework.context.MessageSource; import org.springframework.validation.BindingResult; import org.springframework.validation.ObjectError; /** *

    * Function available to templates in Spring MVC applications in order to access the BindingResult * of a form *

    * * @author Eric Bussieres */ public class GetGlobalErrorsFunction extends BaseBindingResultFunction { public static final String FUNCTION_NAME = "getGlobalErrors"; private final MessageSource messageSource; public GetGlobalErrorsFunction(MessageSource messageSource) { super(PARAM_FORM_NAME); this.messageSource = messageSource; } @Override public Object execute(Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { String formName = (String) args.get(PARAM_FORM_NAME); Locale locale = context.getLocale(); BindingResult bindingResult = this.getBindingResult(formName, context); return this.constructErrorMessages(locale, bindingResult); } private List constructErrorMessages(Locale locale, BindingResult bindingResult) { List errorMessages = new ArrayList<>(); if (bindingResult != null) { for (ObjectError error : bindingResult.getGlobalErrors()) { String msg = this.messageSource.getMessage(error, locale); errorMessages.add(msg); } } return errorMessages; } } ================================================ FILE: pebble-spring/pebble-spring6/src/main/java/io/pebbletemplates/spring/extension/function/bindingresult/HasErrorsFunction.java ================================================ /* * Copyright (c) 2013 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.spring.extension.function.bindingresult; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.util.Map; import org.springframework.validation.BindingResult; /** *

    * Function available to templates in Spring MVC applications in order to access the BindingResult * of a form *

    * * @author Eric Bussieres */ public class HasErrorsFunction extends BaseBindingResultFunction { public static final String FUNCTION_NAME = "hasErrors"; public HasErrorsFunction() { super(PARAM_FORM_NAME); } @Override public Object execute(Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { String formName = (String) args.get(PARAM_FORM_NAME); BindingResult bindingResult = this.getBindingResult(formName, context); return bindingResult != null && bindingResult.hasErrors(); } } ================================================ FILE: pebble-spring/pebble-spring6/src/main/java/io/pebbletemplates/spring/extension/function/bindingresult/HasFieldErrorsFunction.java ================================================ /* * Copyright (c) 2013 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.spring.extension.function.bindingresult; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.util.Map; import org.springframework.validation.BindingResult; /** *

    * Function available to templates in Spring MVC applications in order to access the BindingResult * of a form *

    * * @author Eric Bussieres */ public class HasFieldErrorsFunction extends BaseBindingResultFunction { public static final String FUNCTION_NAME = "hasFieldErrors"; public HasFieldErrorsFunction() { super(PARAM_FORM_NAME, PARAM_FIELD_NAME); } @Override public Object execute(Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { String formName = (String) args.get(PARAM_FORM_NAME); String fieldName = (String) args.get(PARAM_FIELD_NAME); BindingResult bindingResult = this.getBindingResult(formName, context); if (bindingResult != null) { if (fieldName == null) { return bindingResult.hasFieldErrors(); } else { return bindingResult.hasFieldErrors(fieldName); } } else { return false; } } } ================================================ FILE: pebble-spring/pebble-spring6/src/main/java/io/pebbletemplates/spring/extension/function/bindingresult/HasGlobalErrorsFunction.java ================================================ /* * Copyright (c) 2013 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.spring.extension.function.bindingresult; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.util.Map; import org.springframework.validation.BindingResult; /** *

    * Function available to templates in Spring MVC applications in order to access the BindingResult * of a form *

    * * @author Eric Bussieres */ public class HasGlobalErrorsFunction extends BaseBindingResultFunction { public static final String FUNCTION_NAME = "hasGlobalErrors"; public HasGlobalErrorsFunction() { super(PARAM_FORM_NAME); } @Override public Object execute(Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { String formName = (String) args.get(PARAM_FORM_NAME); BindingResult bindingResult = this.getBindingResult(formName, context); return bindingResult != null && bindingResult.hasGlobalErrors(); } } ================================================ FILE: pebble-spring/pebble-spring6/src/main/java/io/pebbletemplates/spring/reactive/PebbleReactiveView.java ================================================ package io.pebbletemplates.spring.reactive; import static java.util.Optional.ofNullable; import io.pebbletemplates.pebble.PebbleEngine; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.spring.context.Beans; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.Writer; import java.nio.charset.Charset; import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.Objects; import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.http.MediaType; import org.springframework.lang.Nullable; import org.springframework.util.MimeType; import org.springframework.web.reactive.result.view.AbstractUrlBasedView; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; public class PebbleReactiveView extends AbstractUrlBasedView { private static final String BEANS_VARIABLE_NAME = "beans"; private static final String REQUEST_VARIABLE_NAME = "request"; private static final String RESPONSE_VARIABLE_NAME = "response"; private static final String SESSION_VARIABLE_NAME = "session"; private PebbleEngine pebbleEngine; private String templateName; @Override public boolean checkResourceExists(Locale locale) { return this.pebbleEngine.getLoader().resourceExists(this.templateName); } @Override protected Mono renderInternal(Map renderAttributes, MediaType contentType, ServerWebExchange exchange) { DataBuffer dataBuffer = exchange.getResponse().bufferFactory().allocateBuffer(); if (this.logger.isDebugEnabled()) { this.logger.debug(exchange.getLogPrefix() + "Rendering [" + this.getUrl() + "]"); } Locale locale = LocaleContextHolder.getLocale(exchange.getLocaleContext()); try { Charset charset = this.getCharset(contentType); Writer writer = new OutputStreamWriter(dataBuffer.asOutputStream(), charset); this.evaluateTemplate(renderAttributes, locale, writer); } catch (Exception ex) { DataBufferUtils.release(dataBuffer); return Mono.error(ex); } return exchange.getResponse().writeWith(Flux.just(dataBuffer)); } @Override protected Mono> getModelAttributes(Map model, ServerWebExchange exchange) { return super.getModelAttributes(addVariablesToModel(model, exchange), exchange); } private Map addVariablesToModel(Map model, ServerWebExchange exchange) { Map attributes = new HashMap<>(Objects.requireNonNullElseGet(model, Map::of)); attributes.put(BEANS_VARIABLE_NAME, new Beans(this.getApplicationContext())); attributes.put(REQUEST_VARIABLE_NAME, exchange.getRequest()); attributes.put(RESPONSE_VARIABLE_NAME, exchange.getResponse()); attributes.put(SESSION_VARIABLE_NAME, exchange.getSession()); return attributes; } private Charset getCharset(@Nullable MediaType mediaType) { return ofNullable(mediaType) .map(MimeType::getCharset) .orElse(this.getDefaultCharset()); } private void evaluateTemplate(Map model, Locale locale, Writer writer) throws IOException, PebbleException { try { PebbleTemplate template = this.pebbleEngine.getTemplate(this.templateName); template.evaluate(writer, model, locale); } finally { writer.flush(); } } public PebbleEngine getPebbleEngine() { return this.pebbleEngine; } public void setPebbleEngine(PebbleEngine pebbleEngine) { this.pebbleEngine = pebbleEngine; } public String getTemplateName() { return this.templateName; } public void setTemplateName(String templateName) { this.templateName = templateName; } } ================================================ FILE: pebble-spring/pebble-spring6/src/main/java/io/pebbletemplates/spring/reactive/PebbleReactiveViewResolver.java ================================================ package io.pebbletemplates.spring.reactive; import io.pebbletemplates.pebble.PebbleEngine; import org.springframework.web.reactive.result.view.AbstractUrlBasedView; import org.springframework.web.reactive.result.view.UrlBasedViewResolver; public class PebbleReactiveViewResolver extends UrlBasedViewResolver { private final PebbleEngine pebbleEngine; public PebbleReactiveViewResolver(PebbleEngine pebbleEngine) { this.setViewClass(this.requiredViewClass()); this.pebbleEngine = pebbleEngine; } @Override protected AbstractUrlBasedView createView(String viewName) { PebbleReactiveView view = (PebbleReactiveView) super.createView(viewName); view.setPebbleEngine(this.pebbleEngine); view.setTemplateName(viewName); return view; } @Override protected Class requiredViewClass() { return PebbleReactiveView.class; } } ================================================ FILE: pebble-spring/pebble-spring6/src/main/java/io/pebbletemplates/spring/servlet/PebbleView.java ================================================ /* * Copyright (c) 2013 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.spring.servlet; import io.pebbletemplates.pebble.PebbleEngine; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.spring.context.Beans; import io.pebbletemplates.pebble.template.PebbleTemplate; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.servlet.support.RequestContextUtils; import org.springframework.web.servlet.view.AbstractTemplateView; import java.io.IOException; import java.io.Writer; import java.util.Locale; import java.util.Map; public class PebbleView extends AbstractTemplateView { public static final String REQUEST_VARIABLE_NAME = "request"; public static final String RESPONSE_VARIABLE_NAME = "response"; public static final String SESSION_VARIABLE_NAME = "session"; private static final String BEANS_VARIABLE_NAME = "beans"; private static final int NANO_PER_SECOND = 1000000; /** *

    * TIMER logger. This logger will output the time required for executing each template processing * operation. *

    *

    * The value of this constant is * io.pebbletemplates.servlet.spring.PebbleView.timer. This allows * you to set a specific configuration and/or appenders for timing info at your logging system * configuration. *

    */ private static final Logger TIMER_LOGGER = LoggerFactory .getLogger(PebbleView.class.getName() + ".timer"); private String characterEncoding = "UTF-8"; private PebbleEngine pebbleEngine; private String templateName; @Override protected void renderMergedTemplateModel(Map model, HttpServletRequest request, HttpServletResponse response) throws Exception { long startNanoTime = System.nanoTime(); this.setCharacterEncoding(response); this.addVariablesToModel(model, request, response); this.evaluateTemplate(model, request, response); this.logElapsedTime(startNanoTime, request); } private void setCharacterEncoding(HttpServletResponse response) { if (this.characterEncoding != null) { response.setCharacterEncoding(this.characterEncoding); } } private void addVariablesToModel(Map model, HttpServletRequest request, HttpServletResponse response) { model.put(BEANS_VARIABLE_NAME, new Beans(this.getApplicationContext())); model.put(REQUEST_VARIABLE_NAME, request); model.put(RESPONSE_VARIABLE_NAME, response); model.put(SESSION_VARIABLE_NAME, request.getSession(false)); } private void evaluateTemplate(Map model, HttpServletRequest request, HttpServletResponse response) throws IOException, PebbleException { Locale locale = RequestContextUtils.getLocale(request); Writer writer = response.getWriter(); try { PebbleTemplate template = this.pebbleEngine.getTemplate(this.templateName); template.evaluate(writer, model, locale); } finally { writer.flush(); } } private void logElapsedTime(long startNanoTime, HttpServletRequest request) { if (TIMER_LOGGER.isDebugEnabled()) { Locale locale = RequestContextUtils.getLocale(request); long endNanoTime = System.nanoTime(); long elapsed = endNanoTime - startNanoTime; long elapsedMs = elapsed / NANO_PER_SECOND; TIMER_LOGGER .debug("Pebble template \"{}\" with locale {} processed in {} nanoseconds (approx. {}ms)", this.templateName, locale, elapsed, elapsedMs); } } public void setCharacterEncoding(String characterEncoding) { this.characterEncoding = characterEncoding; } public void setPebbleEngine(PebbleEngine pebbleEngine) { this.pebbleEngine = pebbleEngine; } public void setTemplateName(String name) { this.templateName = name; } } ================================================ FILE: pebble-spring/pebble-spring6/src/main/java/io/pebbletemplates/spring/servlet/PebbleViewResolver.java ================================================ /* * Copyright (c) 2013 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.spring.servlet; import io.pebbletemplates.pebble.PebbleEngine; import io.pebbletemplates.pebble.loader.Loader; import org.springframework.beans.factory.InitializingBean; import org.springframework.web.servlet.view.AbstractTemplateViewResolver; import org.springframework.web.servlet.view.AbstractUrlBasedView; public class PebbleViewResolver extends AbstractTemplateViewResolver implements InitializingBean { private String characterEncoding = "UTF-8"; private final PebbleEngine pebbleEngine; public PebbleViewResolver(PebbleEngine pebbleEngine) { this.pebbleEngine = pebbleEngine; this.setViewClass(this.requiredViewClass()); } @Override public void afterPropertiesSet() { Loader templateLoader = this.pebbleEngine.getLoader(); templateLoader.setPrefix(this.getPrefix()); templateLoader.setSuffix(this.getSuffix()); } public void setCharacterEncoding(String characterEncoding) { this.characterEncoding = characterEncoding; } @Override protected AbstractUrlBasedView buildView(String viewName) throws Exception { PebbleView view = (PebbleView) super.buildView(viewName); view.setTemplateName(viewName); view.setPebbleEngine(this.pebbleEngine); view.setCharacterEncoding(this.characterEncoding); return view; } @Override protected Class requiredViewClass() { return PebbleView.class; } } ================================================ FILE: pebble-spring/pebble-spring6/src/test/java/io/pebbletemplates/spring/PebbleViewResolverTest.java ================================================ /* * Copyright (c) 2013 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.spring; import io.pebbletemplates.spring.config.MVCConfig; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.validation.BindingResult; import org.springframework.validation.FieldError; import org.springframework.validation.ObjectError; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.servlet.ViewResolver; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; /** * Unit test for the PebbleViewResolver * * @author Eric Bussieres */ @ExtendWith(SpringExtension.class) @WebAppConfiguration @ContextConfiguration(classes = MVCConfig.class) class PebbleViewResolverTest { private static final String CONTEXT_PATH = "/testContextPath"; private static final Locale DEFAULT_LOCALE = Locale.CANADA; private static final String EXPECTED_RESPONSE_PATH = "/io/pebbletemplates/spring/expectedResponse"; private static final String FORM_NAME = "formName"; private BindingResult mockBindingResult = mock(BindingResult.class); private MockHttpServletRequest mockRequest = new MockHttpServletRequest(); private MockHttpServletResponse mockResponse = new MockHttpServletResponse(); @Autowired private ViewResolver viewResolver; @BeforeEach void initRequest() { this.mockRequest.setContextPath(CONTEXT_PATH); this.mockRequest.getSession().setMaxInactiveInterval(600); RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(this.mockRequest)); } @BeforeEach void initBindingResult() { this.initBindingResultAllErrors(); this.initBindingResultGlobalErrors(); this.initBindingResultFieldErrors(); } private void initBindingResultAllErrors() { when(this.mockBindingResult.hasErrors()).thenReturn(true); List allErrors = new ArrayList<>(); allErrors.add( new ObjectError(FORM_NAME, new String[]{"error.test"}, new String[]{}, "???error.test???")); when(this.mockBindingResult.getAllErrors()).thenReturn(allErrors); } private void initBindingResultGlobalErrors() { when(this.mockBindingResult.hasGlobalErrors()).thenReturn(true); List globalErrors = new ArrayList<>(); globalErrors.add(new ObjectError(FORM_NAME, new String[]{"error.global.test.params"}, new String[]{"param1", "param2"}, "???error.global.test.params???")); when(this.mockBindingResult.getGlobalErrors()).thenReturn(globalErrors); } private void initBindingResultFieldErrors() { when(this.mockBindingResult.hasFieldErrors("testField")).thenReturn(true); List fieldErrors = new ArrayList<>(); fieldErrors.add( new FieldError(FORM_NAME, "testField", null, false, new String[]{"error.field.test.params"}, new String[]{"param1", "param2"}, "???error.field.test.params???")); when(this.mockBindingResult.getFieldErrors("testField")).thenReturn(fieldErrors); } @Test void whenRenderingAPage_givenPageWithBeanVariable_thenRenderingIsOK() throws Exception { String result = this.render("beansTest", new HashMap()); this.assertOutput(result, EXPECTED_RESPONSE_PATH + "/beansTest.html"); } @Test void whenRenderingAPage_givenPageWithBindingResult_thenRenderingIsOK() throws Exception { Map model = this.givenBindingResult(); String result = this.render("bindingResultTest", model); this.assertOutput(result, EXPECTED_RESPONSE_PATH + "/bindingResultTest.html"); } private Map givenBindingResult() { Map model = new HashMap<>(); model.put(BindingResult.MODEL_KEY_PREFIX + FORM_NAME, this.mockBindingResult); return model; } @Test void whenRenderingAPage_givenPageWithBindingResultAndMacro_thenRenderingIsOK() throws Exception { Map model = this.givenBindingResult(); String result = this.render("bindingResultWithMacroTest", model); this.assertOutput(result, EXPECTED_RESPONSE_PATH + "/bindingResultWithMacroTest.html"); } @Test void whenRenderingAPage_givenPageWithHrefFunction_thenRenderingIsOK() throws Exception { String result = this.render("hrefFunctionTest", new HashMap()); this.assertOutput(result, EXPECTED_RESPONSE_PATH + "/hrefFunctionTest.html"); } @Test void whenRenderingAPageInEnglish_givenPageWithResourceBundleMessage_thenRenderingIsOK() throws Exception { String result = this.render("messageEnTest", new HashMap()); this.assertOutput(result, EXPECTED_RESPONSE_PATH + "/messageEnTest.html"); } @Test void whenRenderingAPageInFrench_givenPageWithResourceBundleMessage_thenRenderingIsOK() throws Exception { this.mockRequest.addPreferredLocale(Locale.CANADA_FRENCH); String result = this.render("messageFrTest", new HashMap()); this.assertOutput(result, EXPECTED_RESPONSE_PATH + "/messageFrTest.html"); } @Test void whenRenderingAPage_givenPageWithHttpRequestVariable_thenRenderingIsOK() throws Exception { String result = this.render("requestTest", new HashMap()); this.assertOutput(result, EXPECTED_RESPONSE_PATH + "/requestTest.html"); } @Test void whenRenderingAPage_givenPageWithHttpResponseVariable_thenRenderingIsOK() throws Exception { String result = this.render("responseTest", new HashMap()); this.assertOutput(result, EXPECTED_RESPONSE_PATH + "/responseTest.html"); } @Test void whenRenderingAPage_givenPageWithHttpSessionVariable_thenRenderingIsOK() throws Exception { String result = this.render("sessionTest", new HashMap()); this.assertOutput(result, EXPECTED_RESPONSE_PATH + "/sessionTest.html"); } private void assertOutput(String output, String expectedOutput) throws IOException { assertEquals(this.readExpectedOutputResource(expectedOutput), output.replaceAll("\\s", "")); } private String readExpectedOutputResource(String expectedOutput) throws IOException { BufferedReader reader = new BufferedReader( new InputStreamReader(this.getClass().getResourceAsStream(expectedOutput))); StringBuilder builder = new StringBuilder(); for (String currentLine = reader.readLine(); currentLine != null; currentLine = reader.readLine()) { builder.append(currentLine); } return this.removeAllWhitespaces(builder.toString()); } private String removeAllWhitespaces(String source) { return source.replaceAll("\\s", ""); } private String render(String location, Map model) throws Exception { this.viewResolver.resolveViewName(location, DEFAULT_LOCALE) .render(model, this.mockRequest, this.mockResponse); return this.mockResponse.getContentAsString(); } } ================================================ FILE: pebble-spring/pebble-spring6/src/test/java/io/pebbletemplates/spring/bean/SomeBean.java ================================================ /* * Copyright (c) 2013 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.spring.bean; /** * Spring bean for unit test * * @author Eric Bussieres */ public class SomeBean { public String foo() { return "foo"; } } ================================================ FILE: pebble-spring/pebble-spring6/src/test/java/io/pebbletemplates/spring/config/MVCConfig.java ================================================ /* * Copyright (c) 2013 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.spring.config; import io.pebbletemplates.pebble.PebbleEngine; import io.pebbletemplates.pebble.loader.ClasspathLoader; import io.pebbletemplates.pebble.loader.Loader; import io.pebbletemplates.spring.bean.SomeBean; import io.pebbletemplates.spring.extension.SpringExtension; import io.pebbletemplates.spring.servlet.PebbleViewResolver; import org.springframework.context.MessageSource; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.support.ResourceBundleMessageSource; import org.springframework.web.servlet.ViewResolver; /** * Spring configuration for unit test * * @author Eric Bussieres */ @Configuration(proxyBeanMethods = false) public class MVCConfig { @Bean public SomeBean foo() { return new SomeBean(); } @Bean public MessageSource messageSource() { ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource(); messageSource.setBasename("io.pebbletemplates.spring.messages"); return messageSource; } @Bean public PebbleEngine pebbleEngine(SpringExtension springExtension, Loader templateLoader) { return new PebbleEngine.Builder() .loader(templateLoader) .strictVariables(false) .extension(springExtension) .build(); } @Bean public SpringExtension springExtension(MessageSource messageSource) { return new SpringExtension(messageSource); } @Bean public Loader templateLoader() { return new ClasspathLoader(); } @Bean public ViewResolver viewResolver(PebbleEngine pebbleEngine) { PebbleViewResolver viewResolver = new PebbleViewResolver(pebbleEngine); viewResolver.setPrefix("io/pebbletemplates/spring/template/"); viewResolver.setSuffix(".html"); viewResolver.setContentType("text/html"); return viewResolver; } } ================================================ FILE: pebble-spring/pebble-spring6/src/test/resources/io/pebbletemplates/spring/expectedResponse/beansTest.html ================================================ foo ================================================ FILE: pebble-spring/pebble-spring6/src/test/resources/io/pebbletemplates/spring/expectedResponse/bindingResultTest.html ================================================ test true true true false false false Some error Some global error with params param1 and param2 Some field error with params param1 and param2 ================================================ FILE: pebble-spring/pebble-spring6/src/test/resources/io/pebbletemplates/spring/expectedResponse/bindingResultWithMacroTest.html ================================================ test true true true false false false Some error Some global error with params param1 and param2 Some field error with params param1 and param2 ================================================ FILE: pebble-spring/pebble-spring6/src/test/resources/io/pebbletemplates/spring/expectedResponse/hrefFunctionTest.html ================================================ test HrefFunction string interpolation = /testContextPath/foo HrefFunction static = /testContextPath/foobar HrefFunction expression = /testContextPath/foo ================================================ FILE: pebble-spring/pebble-spring6/src/test/resources/io/pebbletemplates/spring/expectedResponse/messageEnTest.html ================================================ test Label = Some label Label with params = Some label with params params1 and params2 ================================================ FILE: pebble-spring/pebble-spring6/src/test/resources/io/pebbletemplates/spring/expectedResponse/messageFrTest.html ================================================ test Label = Un libellé Label with params = Un libellé avec les paramètres params1 et params2 ================================================ FILE: pebble-spring/pebble-spring6/src/test/resources/io/pebbletemplates/spring/expectedResponse/requestTest.html ================================================ test Context path = /testContextPath ================================================ FILE: pebble-spring/pebble-spring6/src/test/resources/io/pebbletemplates/spring/expectedResponse/responseTest.html ================================================ test Response contentType = text/html;charset=UTF-8 Response character encoding = UTF-8 ================================================ FILE: pebble-spring/pebble-spring6/src/test/resources/io/pebbletemplates/spring/expectedResponse/sessionTest.html ================================================ test Session maxInactiveInterval = 600 ================================================ FILE: pebble-spring/pebble-spring6/src/test/resources/io/pebbletemplates/spring/messages_en.properties ================================================ label.test=Some label label.test.params=Some label with params {0} and {1} error.test=Some error error.global.test.params=Some global error with params {0} and {1} error.field.test.params=Some field error with params {0} and {1} ================================================ FILE: pebble-spring/pebble-spring6/src/test/resources/io/pebbletemplates/spring/messages_fr.properties ================================================ label.test=Un libell label.test.params=Un libell avec les paramtres {0} et {1} error.test=Une erreur error.global.test.params=Une erreur globale avec les paramtres {0} et {1} error.field.test.params=Une erreur sur un champ avec les paramtres {0} et {1} ================================================ FILE: pebble-spring/pebble-spring6/src/test/resources/io/pebbletemplates/spring/template/beansTest.html ================================================ {{ beans.foo.foo() }} ================================================ FILE: pebble-spring/pebble-spring6/src/test/resources/io/pebbletemplates/spring/template/bindingResultTest.html ================================================ test {{ hasErrors('formName') }} {{ hasGlobalErrors('formName') }} {{ hasFieldErrors('formName', 'testField') }} {{ hasErrors('') }} {{ hasGlobalErrors('') }} {{ hasFieldErrors('formName', '') }} {% for err in getAllErrors('formName') %}{{ err }}{% endfor %} {% for err in getGlobalErrors('formName') %}{{ err }}{% endfor %} {% for err in getFieldErrors('formName', 'testField') %}{{ err }}{% endfor %} {% for err in getAllErrors('') %}{{ err }}{% endfor %} {% for err in getGlobalErrors('') %}{{ err }}{% endfor %} {% for err in getFieldErrors('', 'testField') %}{{ err }}{% endfor %} {% for err in getFieldErrors('formName', '') %}{{ err }}{% endfor %} ================================================ FILE: pebble-spring/pebble-spring6/src/test/resources/io/pebbletemplates/spring/template/bindingResultWithMacroTest.html ================================================ test {{ test(_context) }} {% macro test(_context) %} {{ hasErrors('formName') }} {{ hasGlobalErrors('formName') }} {{ hasFieldErrors('formName', 'testField') }} {{ hasErrors('') }} {{ hasGlobalErrors('') }} {{ hasFieldErrors('formName', '') }} {% for err in getAllErrors('formName') %}{{ err }}{% endfor %} {% for err in getGlobalErrors('formName') %}{{ err }}{% endfor %} {% for err in getFieldErrors('formName', 'testField') %}{{ err }}{% endfor %} {% for err in getAllErrors('') %}{{ err }}{% endfor %} {% for err in getGlobalErrors('') %}{{ err }}{% endfor %} {% for err in getFieldErrors('', 'testField') %}{{ err }}{% endfor %} {% for err in getFieldErrors('formName', '') %}{{ err }}{% endfor %} {% endmacro %} ================================================ FILE: pebble-spring/pebble-spring6/src/test/resources/io/pebbletemplates/spring/template/hrefFunctionTest.html ================================================ test HrefFunction string interpolation = {{ href("/#{beans.foo.foo}") }} HrefFunction static = {{ href('/foobar') }} HrefFunction expression = {{ href('/' + beans.foo.foo) }} ================================================ FILE: pebble-spring/pebble-spring6/src/test/resources/io/pebbletemplates/spring/template/messageEnTest.html ================================================ test Label = {{ message('label.test') }} Label with params = {{ message('label.test.params', 'params1', 'params2') }} ================================================ FILE: pebble-spring/pebble-spring6/src/test/resources/io/pebbletemplates/spring/template/messageFrTest.html ================================================ test Label = {{ message('label.test') }} Label with params = {{ message('label.test.params', 'params1', 'params2') }} ================================================ FILE: pebble-spring/pebble-spring6/src/test/resources/io/pebbletemplates/spring/template/requestTest.html ================================================ test Context path = {{ request.contextPath }} ================================================ FILE: pebble-spring/pebble-spring6/src/test/resources/io/pebbletemplates/spring/template/responseTest.html ================================================ test Response contentType = {{ response.contentType }} Response character encoding = {{ response.characterEncoding }} ================================================ FILE: pebble-spring/pebble-spring6/src/test/resources/io/pebbletemplates/spring/template/sessionTest.html ================================================ test Session maxInactiveInterval = {{ session.maxInactiveInterval }} ================================================ FILE: pebble-spring/pebble-spring7/pom.xml ================================================ 4.0.0 pebble-spring io.pebbletemplates 4.1.2-SNAPSHOT pebble-spring7 Pebble Integration with Spring 7.x Pebble Integration with Spring 7.x http://pebbletemplates.io 17 6.1.0 7.0.7 5.23.0 6.0.3 org.springframework spring-framework-bom ${spring-framework.version} pom import io.pebbletemplates pebble jakarta.servlet jakarta.servlet-api ${servlet-api.version} provided org.springframework spring-webmvc provided org.springframework spring-webflux provided org.springframework spring-test test org.junit.jupiter junit-jupiter ${junit-jupiter.version} test org.mockito mockito-junit-jupiter ${mockito-junit-jupiter.version} test maven-jar-plugin io.pebbletemplates.spring ================================================ FILE: pebble-spring/pebble-spring7/src/main/java/io/pebbletemplates/spring/context/Beans.java ================================================ /* * Copyright (c) 2013 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.spring.context; import java.util.Arrays; import java.util.Collection; import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; import org.springframework.context.ApplicationContext; import org.springframework.util.Assert; /** *

    * Special object made available to templates in Spring MVC applications in order to access beans in * the Application Context. *

    * * @author Eric Bussieres */ public class Beans implements Map { private final ApplicationContext ctx; public Beans(ApplicationContext ctx) { Assert.notNull(ctx, "Application Context cannot be null"); this.ctx = ctx; } @Override public void clear() { throw new UnsupportedOperationException("Method \"clear\" not supported in Beans object"); } @Override public boolean containsKey(Object key) { Assert.notNull(key, "Key cannot be null"); return this.ctx.containsBean(key.toString()); } @Override public boolean containsValue(Object value) { throw new UnsupportedOperationException( "Method \"containsValue\" not supported in Beans object"); } @Override public Set> entrySet() { throw new UnsupportedOperationException("Method \"entrySet\" not supported in Beans object"); } @Override public Object get(Object key) { Assert.notNull(key, "Key cannot be null"); return this.ctx.getBean(key.toString()); } @Override public boolean isEmpty() { return this.size() <= 0; } @Override public Set keySet() { return new LinkedHashSet<>(Arrays.asList(this.ctx.getBeanDefinitionNames())); } @Override public Object put(String key, Object value) { throw new UnsupportedOperationException("Method \"put\" not supported in Beans object"); } @Override public void putAll(Map m) { throw new UnsupportedOperationException("Method \"putAll\" not supported in Beans object"); } @Override public Object remove(Object key) { throw new UnsupportedOperationException("Method \"remove\" not supported in Beans object"); } @Override public int size() { return this.ctx.getBeanDefinitionCount(); } @Override public Collection values() { throw new UnsupportedOperationException("Method \"values\" not supported in Beans object"); } } ================================================ FILE: pebble-spring/pebble-spring7/src/main/java/io/pebbletemplates/spring/extension/SpringExtension.java ================================================ /* * Copyright (c) 2013 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.spring.extension; import io.pebbletemplates.pebble.extension.AbstractExtension; import io.pebbletemplates.pebble.extension.Function; import io.pebbletemplates.spring.extension.function.HrefFunction; import io.pebbletemplates.spring.extension.function.MessageSourceFunction; import io.pebbletemplates.spring.extension.function.bindingresult.*; import org.springframework.context.MessageSource; import java.util.HashMap; import java.util.Map; /** *

    * Extension for PebbleEngine to add spring functionality *

    * * @author Eric Bussieres */ public class SpringExtension extends AbstractExtension { private final MessageSource messageSource; public SpringExtension(MessageSource messageSource) { this.messageSource = messageSource; } @Override public Map getFunctions() { Map functions = new HashMap<>(); functions .put(MessageSourceFunction.FUNCTION_NAME, new MessageSourceFunction(this.messageSource)); functions.put(HasErrorsFunction.FUNCTION_NAME, new HasErrorsFunction()); functions.put(HasGlobalErrorsFunction.FUNCTION_NAME, new HasGlobalErrorsFunction()); functions.put(HasFieldErrorsFunction.FUNCTION_NAME, new HasFieldErrorsFunction()); functions.put(GetAllErrorsFunction.FUNCTION_NAME, new GetAllErrorsFunction(this.messageSource)); functions.put(GetGlobalErrorsFunction.FUNCTION_NAME, new GetGlobalErrorsFunction(this.messageSource)); functions .put(GetFieldErrorsFunction.FUNCTION_NAME, new GetFieldErrorsFunction(this.messageSource)); functions.put(HrefFunction.FUNCTION_NAME, new HrefFunction()); return functions; } } ================================================ FILE: pebble-spring/pebble-spring7/src/main/java/io/pebbletemplates/spring/extension/function/HrefFunction.java ================================================ package io.pebbletemplates.spring.extension.function; import io.pebbletemplates.pebble.extension.Function; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import io.pebbletemplates.pebble.extension.NamedArguments; import jakarta.servlet.http.HttpServletRequest; import org.springframework.util.StringUtils; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import java.util.ArrayList; import java.util.List; import java.util.Map; /** * Pebble function which adds the context path to the given url * * @author Eric Bussieres */ public class HrefFunction implements Function { public static final String FUNCTION_NAME = "href"; protected static final String PARAM_URL = "url"; protected List argumentNames; private String contextPath; /** * Constructor */ public HrefFunction() { this.argumentNames = new ArrayList<>(); this.argumentNames.add(PARAM_URL); } /** * {@inheritDoc} * * @see Function#execute(Map, PebbleTemplate, EvaluationContext, int) */ @Override public Object execute(Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { StringBuffer result = new StringBuffer(); result.append(this.getContextPath()); this.addUrlParameter(args, result); return result.toString(); } private void addUrlParameter(Map args, StringBuffer result) { String url = (String) args.get(PARAM_URL); if (StringUtils.hasText(url)) { result.append(url); } } private String getContextPath() { if (this.contextPath == null) { this.contextPath = this.getRequest().getContextPath(); } return this.contextPath; } private HttpServletRequest getRequest() { ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes(); return attr.getRequest(); } /** * {@inheritDoc} * * @see NamedArguments#getArgumentNames() */ @Override public List getArgumentNames() { return this.argumentNames; } } ================================================ FILE: pebble-spring/pebble-spring7/src/main/java/io/pebbletemplates/spring/extension/function/MessageSourceFunction.java ================================================ /* * Copyright (c) 2013 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.spring.extension.function; import io.pebbletemplates.pebble.extension.Function; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Map; import org.springframework.context.MessageSource; /** *

    * Function available to templates in Spring MVC applications in order to resolve message in the * application context *

    * * @author Eric Bussieres */ public class MessageSourceFunction implements Function { public static final String FUNCTION_NAME = "message"; private final MessageSource messageSource; public MessageSourceFunction(MessageSource messageSource) { this.messageSource = messageSource; } @Override public Object execute(Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { String key = this.extractKey(args); List arguments = this.extractArguments(args); Locale locale = context.getLocale(); return this.messageSource.getMessage(key, arguments.toArray(), "???" + key + "???", locale); } private String extractKey(Map args) { return (String) args.get("0"); } private List extractArguments(Map args) { int i = 1; List arguments = new ArrayList<>(); while (args.containsKey(String.valueOf(i))) { Object param = args.get(String.valueOf(i)); arguments.add(param); i++; } return arguments; } @Override public List getArgumentNames() { return null; } } ================================================ FILE: pebble-spring/pebble-spring7/src/main/java/io/pebbletemplates/spring/extension/function/bindingresult/BaseBindingResultFunction.java ================================================ /* * Copyright (c) 2013 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.spring.extension.function.bindingresult; import io.pebbletemplates.pebble.extension.Function; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.GlobalContext; import org.springframework.validation.BindingResult; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * Base class of the function interacting with the BindingResult * * @author Eric Bussieres */ public abstract class BaseBindingResultFunction implements Function { protected static final String PARAM_FIELD_NAME = "fieldName"; protected static final String PARAM_FORM_NAME = "formName"; private final List argumentNames = new ArrayList<>(); protected BaseBindingResultFunction(String... argumentsName) { Collections.addAll(this.argumentNames, argumentsName); } @Override public List getArgumentNames() { return this.argumentNames; } protected BindingResult getBindingResult(String formName, EvaluationContext context) { String attribute = BindingResult.MODEL_KEY_PREFIX + formName; BindingResult bindingResult = (BindingResult) context.getVariable(attribute); if (bindingResult == null) { GlobalContext globalContext = (GlobalContext) context.getVariable("_context"); if (globalContext != null) { return (BindingResult) globalContext.get(attribute); } } return bindingResult; } } ================================================ FILE: pebble-spring/pebble-spring7/src/main/java/io/pebbletemplates/spring/extension/function/bindingresult/GetAllErrorsFunction.java ================================================ /* * Copyright (c) 2013 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.spring.extension.function.bindingresult; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Map; import org.springframework.context.MessageSource; import org.springframework.validation.BindingResult; import org.springframework.validation.ObjectError; /** *

    * Function available to templates in Spring MVC applications in order to access the BindingResult * of a form *

    * * @author Eric Bussieres */ public class GetAllErrorsFunction extends BaseBindingResultFunction { public static final String FUNCTION_NAME = "getAllErrors"; private final MessageSource messageSource; public GetAllErrorsFunction(MessageSource messageSource) { super(PARAM_FORM_NAME); this.messageSource = messageSource; } @Override public Object execute(Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { String formName = (String) args.get(PARAM_FORM_NAME); Locale locale = context.getLocale(); BindingResult bindingResult = this.getBindingResult(formName, context); return this.constructErrorMessage(locale, bindingResult); } private List constructErrorMessage(Locale locale, BindingResult bindingResult) { List errorMessages = new ArrayList<>(); if (bindingResult != null) { for (ObjectError error : bindingResult.getAllErrors()) { String msg = this.messageSource.getMessage(error, locale); errorMessages.add(msg); } } return errorMessages; } } ================================================ FILE: pebble-spring/pebble-spring7/src/main/java/io/pebbletemplates/spring/extension/function/bindingresult/GetFieldErrorsFunction.java ================================================ /* * Copyright (c) 2013 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.spring.extension.function.bindingresult; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Map; import org.springframework.context.MessageSource; import org.springframework.validation.BindingResult; import org.springframework.validation.FieldError; /** *

    * Function available to templates in Spring MVC applications in order to access the BindingResult * of a form *

    * * @author Eric Bussieres */ public class GetFieldErrorsFunction extends BaseBindingResultFunction { public static final String FUNCTION_NAME = "getFieldErrors"; private final MessageSource messageSource; public GetFieldErrorsFunction(MessageSource messageSource) { super(PARAM_FORM_NAME, PARAM_FIELD_NAME); this.messageSource = messageSource; } @Override public Object execute(Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { String formName = (String) args.get(PARAM_FORM_NAME); String field = (String) args.get(PARAM_FIELD_NAME); if (field == null) { throw new IllegalArgumentException("Field parameter is required in GetFieldErrorsFunction"); } Locale locale = context.getLocale(); BindingResult bindingResult = this.getBindingResult(formName, context); return this.constructErrorMessages(field, locale, bindingResult); } private List constructErrorMessages(String field, Locale locale, BindingResult bindingResult) { List errorMessages = new ArrayList<>(); if (bindingResult != null) { for (FieldError error : bindingResult.getFieldErrors(field)) { String msg = this.messageSource.getMessage(error, locale); errorMessages.add(msg); } } return errorMessages; } } ================================================ FILE: pebble-spring/pebble-spring7/src/main/java/io/pebbletemplates/spring/extension/function/bindingresult/GetGlobalErrorsFunction.java ================================================ /* * Copyright (c) 2013 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.spring.extension.function.bindingresult; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Map; import org.springframework.context.MessageSource; import org.springframework.validation.BindingResult; import org.springframework.validation.ObjectError; /** *

    * Function available to templates in Spring MVC applications in order to access the BindingResult * of a form *

    * * @author Eric Bussieres */ public class GetGlobalErrorsFunction extends BaseBindingResultFunction { public static final String FUNCTION_NAME = "getGlobalErrors"; private final MessageSource messageSource; public GetGlobalErrorsFunction(MessageSource messageSource) { super(PARAM_FORM_NAME); this.messageSource = messageSource; } @Override public Object execute(Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { String formName = (String) args.get(PARAM_FORM_NAME); Locale locale = context.getLocale(); BindingResult bindingResult = this.getBindingResult(formName, context); return this.constructErrorMessages(locale, bindingResult); } private List constructErrorMessages(Locale locale, BindingResult bindingResult) { List errorMessages = new ArrayList<>(); if (bindingResult != null) { for (ObjectError error : bindingResult.getGlobalErrors()) { String msg = this.messageSource.getMessage(error, locale); errorMessages.add(msg); } } return errorMessages; } } ================================================ FILE: pebble-spring/pebble-spring7/src/main/java/io/pebbletemplates/spring/extension/function/bindingresult/HasErrorsFunction.java ================================================ /* * Copyright (c) 2013 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.spring.extension.function.bindingresult; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.util.Map; import org.springframework.validation.BindingResult; /** *

    * Function available to templates in Spring MVC applications in order to access the BindingResult * of a form *

    * * @author Eric Bussieres */ public class HasErrorsFunction extends BaseBindingResultFunction { public static final String FUNCTION_NAME = "hasErrors"; public HasErrorsFunction() { super(PARAM_FORM_NAME); } @Override public Object execute(Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { String formName = (String) args.get(PARAM_FORM_NAME); BindingResult bindingResult = this.getBindingResult(formName, context); return bindingResult != null && bindingResult.hasErrors(); } } ================================================ FILE: pebble-spring/pebble-spring7/src/main/java/io/pebbletemplates/spring/extension/function/bindingresult/HasFieldErrorsFunction.java ================================================ /* * Copyright (c) 2013 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.spring.extension.function.bindingresult; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.util.Map; import org.springframework.validation.BindingResult; /** *

    * Function available to templates in Spring MVC applications in order to access the BindingResult * of a form *

    * * @author Eric Bussieres */ public class HasFieldErrorsFunction extends BaseBindingResultFunction { public static final String FUNCTION_NAME = "hasFieldErrors"; public HasFieldErrorsFunction() { super(PARAM_FORM_NAME, PARAM_FIELD_NAME); } @Override public Object execute(Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { String formName = (String) args.get(PARAM_FORM_NAME); String fieldName = (String) args.get(PARAM_FIELD_NAME); BindingResult bindingResult = this.getBindingResult(formName, context); if (bindingResult != null) { if (fieldName == null) { return bindingResult.hasFieldErrors(); } else { return bindingResult.hasFieldErrors(fieldName); } } else { return false; } } } ================================================ FILE: pebble-spring/pebble-spring7/src/main/java/io/pebbletemplates/spring/extension/function/bindingresult/HasGlobalErrorsFunction.java ================================================ /* * Copyright (c) 2013 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.spring.extension.function.bindingresult; import io.pebbletemplates.pebble.template.EvaluationContext; import io.pebbletemplates.pebble.template.PebbleTemplate; import java.util.Map; import org.springframework.validation.BindingResult; /** *

    * Function available to templates in Spring MVC applications in order to access the BindingResult * of a form *

    * * @author Eric Bussieres */ public class HasGlobalErrorsFunction extends BaseBindingResultFunction { public static final String FUNCTION_NAME = "hasGlobalErrors"; public HasGlobalErrorsFunction() { super(PARAM_FORM_NAME); } @Override public Object execute(Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { String formName = (String) args.get(PARAM_FORM_NAME); BindingResult bindingResult = this.getBindingResult(formName, context); return bindingResult != null && bindingResult.hasGlobalErrors(); } } ================================================ FILE: pebble-spring/pebble-spring7/src/main/java/io/pebbletemplates/spring/reactive/PebbleReactiveView.java ================================================ package io.pebbletemplates.spring.reactive; import io.pebbletemplates.pebble.PebbleEngine; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.template.PebbleTemplate; import io.pebbletemplates.spring.context.Beans; import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.http.MediaType; import org.springframework.lang.Nullable; import org.springframework.util.MimeType; import org.springframework.web.reactive.result.view.AbstractUrlBasedView; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.Writer; import java.nio.charset.Charset; import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.Objects; import static java.util.Optional.ofNullable; public class PebbleReactiveView extends AbstractUrlBasedView { private static final String BEANS_VARIABLE_NAME = "beans"; private static final String REQUEST_VARIABLE_NAME = "request"; private static final String RESPONSE_VARIABLE_NAME = "response"; private static final String SESSION_VARIABLE_NAME = "session"; private PebbleEngine pebbleEngine; private String templateName; @Override public boolean checkResourceExists(Locale locale) { return this.pebbleEngine.getLoader().resourceExists(this.templateName); } @Override protected Mono renderInternal(Map renderAttributes, MediaType contentType, ServerWebExchange exchange) { DataBuffer dataBuffer = exchange.getResponse().bufferFactory().allocateBuffer(); if (this.logger.isDebugEnabled()) { this.logger.debug(exchange.getLogPrefix() + "Rendering [" + this.getUrl() + "]"); } Locale locale = LocaleContextHolder.getLocale(exchange.getLocaleContext()); try { Charset charset = this.getCharset(contentType); Writer writer = new OutputStreamWriter(dataBuffer.asOutputStream(), charset); this.evaluateTemplate(renderAttributes, locale, writer); } catch (Exception ex) { DataBufferUtils.release(dataBuffer); return Mono.error(ex); } return exchange.getResponse().writeWith(Flux.just(dataBuffer)); } @Override protected Mono> getModelAttributes(Map model, ServerWebExchange exchange) { return super.getModelAttributes(this.addVariablesToModel(model, exchange), exchange); } private Map addVariablesToModel(Map model, ServerWebExchange exchange) { Map attributes = new HashMap<>(Objects.requireNonNullElseGet(model, Map::of)); attributes.put(BEANS_VARIABLE_NAME, new Beans(this.getApplicationContext())); attributes.put(REQUEST_VARIABLE_NAME, exchange.getRequest()); attributes.put(RESPONSE_VARIABLE_NAME, exchange.getResponse()); attributes.put(SESSION_VARIABLE_NAME, exchange.getSession()); return attributes; } private Charset getCharset(@Nullable MediaType mediaType) { return ofNullable(mediaType) .map(MimeType::getCharset) .orElse(this.getDefaultCharset()); } private void evaluateTemplate(Map model, Locale locale, Writer writer) throws IOException, PebbleException { try { PebbleTemplate template = this.pebbleEngine.getTemplate(this.templateName); template.evaluate(writer, model, locale); } finally { writer.flush(); } } public PebbleEngine getPebbleEngine() { return this.pebbleEngine; } public void setPebbleEngine(PebbleEngine pebbleEngine) { this.pebbleEngine = pebbleEngine; } public String getTemplateName() { return this.templateName; } public void setTemplateName(String templateName) { this.templateName = templateName; } } ================================================ FILE: pebble-spring/pebble-spring7/src/main/java/io/pebbletemplates/spring/reactive/PebbleReactiveViewResolver.java ================================================ package io.pebbletemplates.spring.reactive; import io.pebbletemplates.pebble.PebbleEngine; import org.springframework.web.reactive.result.view.AbstractUrlBasedView; import org.springframework.web.reactive.result.view.UrlBasedViewResolver; public class PebbleReactiveViewResolver extends UrlBasedViewResolver { private final PebbleEngine pebbleEngine; public PebbleReactiveViewResolver(PebbleEngine pebbleEngine) { this.setViewClass(this.requiredViewClass()); this.pebbleEngine = pebbleEngine; } @Override protected AbstractUrlBasedView createView(String viewName) { PebbleReactiveView view = (PebbleReactiveView) super.createView(viewName); view.setPebbleEngine(this.pebbleEngine); view.setTemplateName(viewName); return view; } @Override protected Class requiredViewClass() { return PebbleReactiveView.class; } } ================================================ FILE: pebble-spring/pebble-spring7/src/main/java/io/pebbletemplates/spring/servlet/PebbleView.java ================================================ /* * Copyright (c) 2013 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.spring.servlet; import io.pebbletemplates.pebble.PebbleEngine; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.template.PebbleTemplate; import io.pebbletemplates.spring.context.Beans; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.servlet.support.RequestContextUtils; import org.springframework.web.servlet.view.AbstractTemplateView; import java.io.IOException; import java.io.Writer; import java.util.Locale; import java.util.Map; public class PebbleView extends AbstractTemplateView { public static final String REQUEST_VARIABLE_NAME = "request"; public static final String RESPONSE_VARIABLE_NAME = "response"; public static final String SESSION_VARIABLE_NAME = "session"; private static final String BEANS_VARIABLE_NAME = "beans"; private static final int NANO_PER_SECOND = 1000000; /** *

    * TIMER logger. This logger will output the time required for executing each template processing * operation. *

    *

    * The value of this constant is * io.pebbletemplates.servlet.spring.PebbleView.timer. This allows * you to set a specific configuration and/or appenders for timing info at your logging system * configuration. *

    */ private static final Logger TIMER_LOGGER = LoggerFactory .getLogger(PebbleView.class.getName() + ".timer"); private String characterEncoding = "UTF-8"; private PebbleEngine pebbleEngine; private String templateName; @Override protected void renderMergedTemplateModel(Map model, HttpServletRequest request, HttpServletResponse response) throws Exception { long startNanoTime = System.nanoTime(); this.setCharacterEncoding(response); this.addVariablesToModel(model, request, response); this.evaluateTemplate(model, request, response); this.logElapsedTime(startNanoTime, request); } private void setCharacterEncoding(HttpServletResponse response) { if (this.characterEncoding != null) { response.setCharacterEncoding(this.characterEncoding); } } private void addVariablesToModel(Map model, HttpServletRequest request, HttpServletResponse response) { model.put(BEANS_VARIABLE_NAME, new Beans(this.getApplicationContext())); model.put(REQUEST_VARIABLE_NAME, request); model.put(RESPONSE_VARIABLE_NAME, response); model.put(SESSION_VARIABLE_NAME, request.getSession(false)); } private void evaluateTemplate(Map model, HttpServletRequest request, HttpServletResponse response) throws IOException, PebbleException { Locale locale = RequestContextUtils.getLocale(request); Writer writer = response.getWriter(); try { PebbleTemplate template = this.pebbleEngine.getTemplate(this.templateName); template.evaluate(writer, model, locale); } finally { writer.flush(); } } private void logElapsedTime(long startNanoTime, HttpServletRequest request) { if (TIMER_LOGGER.isDebugEnabled()) { Locale locale = RequestContextUtils.getLocale(request); long endNanoTime = System.nanoTime(); long elapsed = endNanoTime - startNanoTime; long elapsedMs = elapsed / NANO_PER_SECOND; TIMER_LOGGER .debug("Pebble template \"{}\" with locale {} processed in {} nanoseconds (approx. {}ms)", this.templateName, locale, elapsed, elapsedMs); } } public void setCharacterEncoding(String characterEncoding) { this.characterEncoding = characterEncoding; } public void setPebbleEngine(PebbleEngine pebbleEngine) { this.pebbleEngine = pebbleEngine; } public void setTemplateName(String name) { this.templateName = name; } } ================================================ FILE: pebble-spring/pebble-spring7/src/main/java/io/pebbletemplates/spring/servlet/PebbleViewResolver.java ================================================ /* * Copyright (c) 2013 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.spring.servlet; import io.pebbletemplates.pebble.PebbleEngine; import io.pebbletemplates.pebble.loader.Loader; import org.springframework.beans.factory.InitializingBean; import org.springframework.web.servlet.view.AbstractTemplateViewResolver; import org.springframework.web.servlet.view.AbstractUrlBasedView; public class PebbleViewResolver extends AbstractTemplateViewResolver implements InitializingBean { private String characterEncoding = "UTF-8"; private final PebbleEngine pebbleEngine; public PebbleViewResolver(PebbleEngine pebbleEngine) { this.pebbleEngine = pebbleEngine; this.setViewClass(this.requiredViewClass()); } @Override public void afterPropertiesSet() { Loader templateLoader = this.pebbleEngine.getLoader(); templateLoader.setPrefix(this.getPrefix()); templateLoader.setSuffix(this.getSuffix()); } public void setCharacterEncoding(String characterEncoding) { this.characterEncoding = characterEncoding; } @Override protected AbstractUrlBasedView buildView(String viewName) throws Exception { PebbleView view = (PebbleView) super.buildView(viewName); view.setTemplateName(viewName); view.setPebbleEngine(this.pebbleEngine); view.setCharacterEncoding(this.characterEncoding); return view; } @Override protected Class requiredViewClass() { return PebbleView.class; } } ================================================ FILE: pebble-spring/pebble-spring7/src/test/java/io/pebbletemplates/spring/PebbleViewResolverTest.java ================================================ /* * Copyright (c) 2013 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.spring; import io.pebbletemplates.spring.config.MVCConfig; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.validation.BindingResult; import org.springframework.validation.FieldError; import org.springframework.validation.ObjectError; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.servlet.ViewResolver; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.util.*; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; /** * Unit test for the PebbleViewResolver * * @author Eric Bussieres */ @ExtendWith(SpringExtension.class) @WebAppConfiguration @ContextConfiguration(classes = MVCConfig.class) class PebbleViewResolverTest { private static final String CONTEXT_PATH = "/testContextPath"; private static final Locale DEFAULT_LOCALE = Locale.CANADA; private static final String EXPECTED_RESPONSE_PATH = "/io/pebbletemplates/spring/expectedResponse"; private static final String FORM_NAME = "formName"; private final BindingResult mockBindingResult = mock(BindingResult.class); private final MockHttpServletRequest mockRequest = new MockHttpServletRequest(); private final MockHttpServletResponse mockResponse = new MockHttpServletResponse(); @Autowired private ViewResolver viewResolver; @BeforeEach void initRequest() { this.mockRequest.setContextPath(CONTEXT_PATH); this.mockRequest.getSession().setMaxInactiveInterval(600); RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(this.mockRequest)); } @BeforeEach void initBindingResult() { this.initBindingResultAllErrors(); this.initBindingResultGlobalErrors(); this.initBindingResultFieldErrors(); } private void initBindingResultAllErrors() { when(this.mockBindingResult.hasErrors()).thenReturn(true); List allErrors = new ArrayList<>(); allErrors.add( new ObjectError(FORM_NAME, new String[]{"error.test"}, new String[]{}, "???error.test???")); when(this.mockBindingResult.getAllErrors()).thenReturn(allErrors); } private void initBindingResultGlobalErrors() { when(this.mockBindingResult.hasGlobalErrors()).thenReturn(true); List globalErrors = new ArrayList<>(); globalErrors.add(new ObjectError(FORM_NAME, new String[]{"error.global.test.params"}, new String[]{"param1", "param2"}, "???error.global.test.params???")); when(this.mockBindingResult.getGlobalErrors()).thenReturn(globalErrors); } private void initBindingResultFieldErrors() { when(this.mockBindingResult.hasFieldErrors("testField")).thenReturn(true); List fieldErrors = new ArrayList<>(); fieldErrors.add( new FieldError(FORM_NAME, "testField", null, false, new String[]{"error.field.test.params"}, new String[]{"param1", "param2"}, "???error.field.test.params???")); when(this.mockBindingResult.getFieldErrors("testField")).thenReturn(fieldErrors); } @Test void whenRenderingAPage_givenPageWithBeanVariable_thenRenderingIsOK() throws Exception { String result = this.render("beansTest", new HashMap()); this.assertOutput(result, EXPECTED_RESPONSE_PATH + "/beansTest.html"); } @Test void whenRenderingAPage_givenPageWithBindingResult_thenRenderingIsOK() throws Exception { Map model = this.givenBindingResult(); String result = this.render("bindingResultTest", model); this.assertOutput(result, EXPECTED_RESPONSE_PATH + "/bindingResultTest.html"); } private Map givenBindingResult() { Map model = new HashMap<>(); model.put(BindingResult.MODEL_KEY_PREFIX + FORM_NAME, this.mockBindingResult); return model; } @Test void whenRenderingAPage_givenPageWithBindingResultAndMacro_thenRenderingIsOK() throws Exception { Map model = this.givenBindingResult(); String result = this.render("bindingResultWithMacroTest", model); this.assertOutput(result, EXPECTED_RESPONSE_PATH + "/bindingResultWithMacroTest.html"); } @Test void whenRenderingAPage_givenPageWithHrefFunction_thenRenderingIsOK() throws Exception { String result = this.render("hrefFunctionTest", new HashMap()); this.assertOutput(result, EXPECTED_RESPONSE_PATH + "/hrefFunctionTest.html"); } @Test void whenRenderingAPageInEnglish_givenPageWithResourceBundleMessage_thenRenderingIsOK() throws Exception { String result = this.render("messageEnTest", new HashMap()); this.assertOutput(result, EXPECTED_RESPONSE_PATH + "/messageEnTest.html"); } @Test void whenRenderingAPageInFrench_givenPageWithResourceBundleMessage_thenRenderingIsOK() throws Exception { this.mockRequest.addPreferredLocale(Locale.CANADA_FRENCH); String result = this.render("messageFrTest", new HashMap()); this.assertOutput(result, EXPECTED_RESPONSE_PATH + "/messageFrTest.html"); } @Test void whenRenderingAPage_givenPageWithHttpRequestVariable_thenRenderingIsOK() throws Exception { String result = this.render("requestTest", new HashMap()); this.assertOutput(result, EXPECTED_RESPONSE_PATH + "/requestTest.html"); } @Test void whenRenderingAPage_givenPageWithHttpResponseVariable_thenRenderingIsOK() throws Exception { String result = this.render("responseTest", new HashMap()); this.assertOutput(result, EXPECTED_RESPONSE_PATH + "/responseTest.html"); } @Test void whenRenderingAPage_givenPageWithHttpSessionVariable_thenRenderingIsOK() throws Exception { String result = this.render("sessionTest", new HashMap()); this.assertOutput(result, EXPECTED_RESPONSE_PATH + "/sessionTest.html"); } private void assertOutput(String output, String expectedOutput) throws IOException { assertEquals(this.readExpectedOutputResource(expectedOutput), output.replaceAll("\\s", "")); } private String readExpectedOutputResource(String expectedOutput) throws IOException { BufferedReader reader = new BufferedReader( new InputStreamReader(this.getClass().getResourceAsStream(expectedOutput))); StringBuilder builder = new StringBuilder(); for (String currentLine = reader.readLine(); currentLine != null; currentLine = reader.readLine()) { builder.append(currentLine); } return this.removeAllWhitespaces(builder.toString()); } private String removeAllWhitespaces(String source) { return source.replaceAll("\\s", ""); } private String render(String location, Map model) throws Exception { this.viewResolver.resolveViewName(location, DEFAULT_LOCALE) .render(model, this.mockRequest, this.mockResponse); return this.mockResponse.getContentAsString(); } } ================================================ FILE: pebble-spring/pebble-spring7/src/test/java/io/pebbletemplates/spring/bean/SomeBean.java ================================================ /* * Copyright (c) 2013 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.spring.bean; /** * Spring bean for unit test * * @author Eric Bussieres */ public class SomeBean { public String foo() { return "foo"; } } ================================================ FILE: pebble-spring/pebble-spring7/src/test/java/io/pebbletemplates/spring/config/MVCConfig.java ================================================ /* * Copyright (c) 2013 by Mitchell Bösecke * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ package io.pebbletemplates.spring.config; import io.pebbletemplates.pebble.PebbleEngine; import io.pebbletemplates.pebble.loader.ClasspathLoader; import io.pebbletemplates.pebble.loader.Loader; import io.pebbletemplates.spring.bean.SomeBean; import io.pebbletemplates.spring.extension.SpringExtension; import io.pebbletemplates.spring.servlet.PebbleViewResolver; import org.springframework.context.MessageSource; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.support.ResourceBundleMessageSource; import org.springframework.web.servlet.ViewResolver; /** * Spring configuration for unit test * * @author Eric Bussieres */ @Configuration(proxyBeanMethods = false) public class MVCConfig { @Bean public SomeBean foo() { return new SomeBean(); } @Bean public MessageSource messageSource() { ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource(); messageSource.setBasename("io.pebbletemplates.spring.messages"); return messageSource; } @Bean public PebbleEngine pebbleEngine(SpringExtension springExtension, Loader templateLoader) { return new PebbleEngine.Builder() .loader(templateLoader) .strictVariables(false) .extension(springExtension) .build(); } @Bean public SpringExtension springExtension(MessageSource messageSource) { return new SpringExtension(messageSource); } @Bean public Loader templateLoader() { return new ClasspathLoader(); } @Bean public ViewResolver viewResolver(PebbleEngine pebbleEngine) { PebbleViewResolver viewResolver = new PebbleViewResolver(pebbleEngine); viewResolver.setPrefix("io/pebbletemplates/spring/template/"); viewResolver.setSuffix(".html"); viewResolver.setContentType("text/html"); return viewResolver; } } ================================================ FILE: pebble-spring/pebble-spring7/src/test/resources/io/pebbletemplates/spring/expectedResponse/beansTest.html ================================================ foo ================================================ FILE: pebble-spring/pebble-spring7/src/test/resources/io/pebbletemplates/spring/expectedResponse/bindingResultTest.html ================================================ test true true true false false false Some error Some global error with params param1 and param2 Some field error with params param1 and param2 ================================================ FILE: pebble-spring/pebble-spring7/src/test/resources/io/pebbletemplates/spring/expectedResponse/bindingResultWithMacroTest.html ================================================ test true true true false false false Some error Some global error with params param1 and param2 Some field error with params param1 and param2 ================================================ FILE: pebble-spring/pebble-spring7/src/test/resources/io/pebbletemplates/spring/expectedResponse/hrefFunctionTest.html ================================================ test HrefFunction string interpolation = /testContextPath/foo HrefFunction static = /testContextPath/foobar HrefFunction expression = /testContextPath/foo ================================================ FILE: pebble-spring/pebble-spring7/src/test/resources/io/pebbletemplates/spring/expectedResponse/messageEnTest.html ================================================ test Label = Some label Label with params = Some label with params params1 and params2 ================================================ FILE: pebble-spring/pebble-spring7/src/test/resources/io/pebbletemplates/spring/expectedResponse/messageFrTest.html ================================================ test Label = Un libellé Label with params = Un libellé avec les paramètres params1 et params2 ================================================ FILE: pebble-spring/pebble-spring7/src/test/resources/io/pebbletemplates/spring/expectedResponse/requestTest.html ================================================ test Context path = /testContextPath ================================================ FILE: pebble-spring/pebble-spring7/src/test/resources/io/pebbletemplates/spring/expectedResponse/responseTest.html ================================================ test Response contentType = text/html;charset=UTF-8 Response character encoding = UTF-8 ================================================ FILE: pebble-spring/pebble-spring7/src/test/resources/io/pebbletemplates/spring/expectedResponse/sessionTest.html ================================================ test Session maxInactiveInterval = 600 ================================================ FILE: pebble-spring/pebble-spring7/src/test/resources/io/pebbletemplates/spring/messages_en.properties ================================================ label.test=Some label label.test.params=Some label with params {0} and {1} error.test=Some error error.global.test.params=Some global error with params {0} and {1} error.field.test.params=Some field error with params {0} and {1} ================================================ FILE: pebble-spring/pebble-spring7/src/test/resources/io/pebbletemplates/spring/messages_fr.properties ================================================ label.test=Un libell label.test.params=Un libell avec les paramtres {0} et {1} error.test=Une erreur error.global.test.params=Une erreur globale avec les paramtres {0} et {1} error.field.test.params=Une erreur sur un champ avec les paramtres {0} et {1} ================================================ FILE: pebble-spring/pebble-spring7/src/test/resources/io/pebbletemplates/spring/template/beansTest.html ================================================ {{ beans.foo.foo() }} ================================================ FILE: pebble-spring/pebble-spring7/src/test/resources/io/pebbletemplates/spring/template/bindingResultTest.html ================================================ test {{ hasErrors('formName') }} {{ hasGlobalErrors('formName') }} {{ hasFieldErrors('formName', 'testField') }} {{ hasErrors('') }} {{ hasGlobalErrors('') }} {{ hasFieldErrors('formName', '') }} {% for err in getAllErrors('formName') %}{{ err }}{% endfor %} {% for err in getGlobalErrors('formName') %}{{ err }}{% endfor %} {% for err in getFieldErrors('formName', 'testField') %}{{ err }}{% endfor %} {% for err in getAllErrors('') %}{{ err }}{% endfor %} {% for err in getGlobalErrors('') %}{{ err }}{% endfor %} {% for err in getFieldErrors('', 'testField') %}{{ err }}{% endfor %} {% for err in getFieldErrors('formName', '') %}{{ err }}{% endfor %} ================================================ FILE: pebble-spring/pebble-spring7/src/test/resources/io/pebbletemplates/spring/template/bindingResultWithMacroTest.html ================================================ test {{ test(_context) }} {% macro test(_context) %} {{ hasErrors('formName') }} {{ hasGlobalErrors('formName') }} {{ hasFieldErrors('formName', 'testField') }} {{ hasErrors('') }} {{ hasGlobalErrors('') }} {{ hasFieldErrors('formName', '') }} {% for err in getAllErrors('formName') %}{{ err }}{% endfor %} {% for err in getGlobalErrors('formName') %}{{ err }}{% endfor %} {% for err in getFieldErrors('formName', 'testField') %}{{ err }}{% endfor %} {% for err in getAllErrors('') %}{{ err }}{% endfor %} {% for err in getGlobalErrors('') %}{{ err }}{% endfor %} {% for err in getFieldErrors('', 'testField') %}{{ err }}{% endfor %} {% for err in getFieldErrors('formName', '') %}{{ err }}{% endfor %} {% endmacro %} ================================================ FILE: pebble-spring/pebble-spring7/src/test/resources/io/pebbletemplates/spring/template/hrefFunctionTest.html ================================================ test HrefFunction string interpolation = {{ href("/#{beans.foo.foo}") }} HrefFunction static = {{ href('/foobar') }} HrefFunction expression = {{ href('/' + beans.foo.foo) }} ================================================ FILE: pebble-spring/pebble-spring7/src/test/resources/io/pebbletemplates/spring/template/messageEnTest.html ================================================ test Label = {{ message('label.test') }} Label with params = {{ message('label.test.params', 'params1', 'params2') }} ================================================ FILE: pebble-spring/pebble-spring7/src/test/resources/io/pebbletemplates/spring/template/messageFrTest.html ================================================ test Label = {{ message('label.test') }} Label with params = {{ message('label.test.params', 'params1', 'params2') }} ================================================ FILE: pebble-spring/pebble-spring7/src/test/resources/io/pebbletemplates/spring/template/requestTest.html ================================================ test Context path = {{ request.contextPath }} ================================================ FILE: pebble-spring/pebble-spring7/src/test/resources/io/pebbletemplates/spring/template/responseTest.html ================================================ test Response contentType = {{ response.contentType }} Response character encoding = {{ response.characterEncoding }} ================================================ FILE: pebble-spring/pebble-spring7/src/test/resources/io/pebbletemplates/spring/template/sessionTest.html ================================================ test Session maxInactiveInterval = {{ session.maxInactiveInterval }} ================================================ FILE: pebble-spring/pom.xml ================================================ 4.0.0 io.pebbletemplates pebble-project 4.1.2-SNAPSHOT pebble-spring pom pebble-spring6 pebble-legacy-spring-boot-starter pebble-spring7 pebble-spring-boot-starter Pebble Spring Project Pebble Spring Project http://pebbletemplates.io io.pebbletemplates pebble ${project.version} ================================================ FILE: pom.xml ================================================ 4.0.0 io.pebbletemplates pebble-project 4.1.2-SNAPSHOT pom pebble pebble-spring docs Pebble Project Pebble Project http://pebbletemplates.io UTF-8 1.8 BSD 3-Clause License http://opensource.org/licenses/BSD-3-Clause repo mitchellbosecke Mitchell Bösecke mitchellbosecke@gmail.com ebussieres Eric Bussieres erbussieres AT gmail DOT com Developer maven-compiler-plugin 3.15.0 ${java.version} ${java.version} true org.apache.maven.plugins maven-jar-plugin 3.5.0 org.apache.maven.plugins maven-surefire-plugin 3.5.5 -Dfile.encoding=${project.build.sourceEncoding} true org.sonatype.central central-publishing-maven-plugin 0.10.0 true central true org.apache.maven.plugins maven-release-plugin 3.3.1 v@{project.version} true false release deploy release org.apache.maven.plugins maven-source-plugin 3.4.0 attach-sources jar org.apache.maven.plugins maven-javadoc-plugin 3.12.0 attach-javadocs jar org.apache.maven.plugins maven-gpg-plugin 3.2.8 sign-artifacts verify sign ossrh https://oss.sonatype.org/content/repositories/snapshots spring-milestones Spring Milestones https://repo.spring.io/milestone spring-milestones Spring Milestones https://repo.spring.io/milestone scm:git:git://github.com/PebbleTemplates/pebble.git scm:git:git@github.com:PebbleTemplates/pebble.git https://github.com/PebbleTemplates/pebble.git HEAD