diff --git a/llvm/include/llvm/Support/Mustache.h b/llvm/include/llvm/Support/Mustache.h new file mode 100644 index 0000000000000..41173b96d1a9a --- /dev/null +++ b/llvm/include/llvm/Support/Mustache.h @@ -0,0 +1,127 @@ +//===--- Mustache.h ---------------------------------------------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// +// +// Implementation of the Mustache templating language supports version 1.4.2 +// currently relies on llvm::json::Value for data input. +// See the Mustache spec for more information +// (https://mustache.github.io/mustache.5.html). +// +// Current Features Supported: +// - Variables +// - Sections +// - Inverted Sections +// - Partials +// - Comments +// - Lambdas +// - Unescaped Variables +// +// Features Not Supported: +// - Set Delimiter +// - Blocks +// - Parents +// - Dynamic Names +// +// The Template class is a container class that outputs the Mustache template +// string and is the main class for users. It stores all the lambdas and the +// ASTNode Tree. When the Template is instantiated it tokenizes the Template +// String and creates a vector of Tokens. Then it calls a basic recursive +// descent parser to construct the ASTNode Tree. The ASTNodes are all stored +// in an arena allocator which is freed once the template class goes out of +// scope. +// +// Usage: +// \code +// // Creating a simple template and rendering it +// auto Template = Template("Hello, {{name}}!"); +// Value Data = {{"name", "World"}}; +// std::string Out; +// raw_string_ostream OS(Out); +// T.render(Data, OS); +// // Out == "Hello, World!" +// +// // Creating a template with a partial and rendering it +// auto Template = Template("{{>partial}}"); +// Template.registerPartial("partial", "Hello, {{name}}!"); +// Value Data = {{"name", "World"}}; +// std::string Out; +// raw_string_ostream OS(Out); +// T.render(Data, OS); +// // Out == "Hello, World!" +// +// // Creating a template with a lambda and rendering it +// Value D = Object{}; +// auto T = Template("Hello, {{lambda}}!"); +// Lambda L = []() -> llvm::json::Value { return "World"; }; +// T.registerLambda("lambda", L); +// std::string Out; +// raw_string_ostream OS(Out); +// T.render(D, OS); +// // Out == "Hello, World!" +// \endcode +// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_SUPPORT_MUSTACHE +#define LLVM_SUPPORT_MUSTACHE + +#include "Error.h" +#include "llvm/ADT/StringMap.h" +#include "llvm/Support/Allocator.h" +#include "llvm/Support/JSON.h" +#include "llvm/Support/StringSaver.h" +#include +#include + +namespace llvm::mustache { + +using Lambda = std::function; +using SectionLambda = std::function; + +class ASTNode; + +// A Template represents the container for the AST and the partials +// and Lambdas that are registered with it. +class Template { +public: + Template(StringRef TemplateStr); + + Template(const Template &) = delete; + + Template &operator=(const Template &) = delete; + + Template(Template &&Other) noexcept; + + Template &operator=(Template &&Other) noexcept; + + void render(const llvm::json::Value &Data, llvm::raw_ostream &OS); + + void registerPartial(std::string Name, std::string Partial); + + void registerLambda(std::string Name, Lambda Lambda); + + void registerLambda(std::string Name, SectionLambda Lambda); + + // By default the Mustache Spec Specifies that HTML special characters + // should be escaped. This function allows the user to specify which + // characters should be escaped. + void overrideEscapeCharacters(DenseMap Escapes); + +private: + StringMap Partials; + StringMap Lambdas; + StringMap SectionLambdas; + DenseMap Escapes; + // The allocator for the ASTNode Tree + llvm::BumpPtrAllocator AstAllocator; + // Allocator for each render call resets after each render + llvm::BumpPtrAllocator RenderAllocator; + ASTNode *Tree; +}; +} // namespace llvm::mustache + +#endif // LLVM_SUPPORT_MUSTACHE diff --git a/llvm/lib/Support/CMakeLists.txt b/llvm/lib/Support/CMakeLists.txt index 122240c27b1fc..d4b2e92cf4025 100644 --- a/llvm/lib/Support/CMakeLists.txt +++ b/llvm/lib/Support/CMakeLists.txt @@ -218,6 +218,7 @@ add_llvm_component_library(LLVMSupport MD5.cpp MSP430Attributes.cpp MSP430AttributeParser.cpp + Mustache.cpp NativeFormatting.cpp OptimizedStructLayout.cpp Optional.cpp diff --git a/llvm/lib/Support/Mustache.cpp b/llvm/lib/Support/Mustache.cpp new file mode 100644 index 0000000000000..d177432ce1f61 --- /dev/null +++ b/llvm/lib/Support/Mustache.cpp @@ -0,0 +1,791 @@ +//===-- Mustache.cpp ------------------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// +#include "llvm/Support/Mustache.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/Support/Error.h" +#include "llvm/Support/raw_ostream.h" +#include + +using namespace llvm; +using namespace llvm::mustache; + +namespace { + +using Accessor = SmallVector; + +static bool isFalsey(const json::Value &V) { + return V.getAsNull() || (V.getAsBoolean() && !V.getAsBoolean().value()) || + (V.getAsArray() && V.getAsArray()->empty()); +} + +static Accessor splitMustacheString(StringRef Str) { + // We split the mustache string into an accessor. + // For example: + // "a.b.c" would be split into {"a", "b", "c"} + // We make an exception for a single dot which + // refers to the current context. + Accessor Tokens; + if (Str == ".") { + Tokens.emplace_back(Str); + return Tokens; + } + while (!Str.empty()) { + StringRef Part; + std::tie(Part, Str) = Str.split("."); + Tokens.emplace_back(Part.trim()); + } + return Tokens; +} +} // namespace + +namespace llvm::mustache { + +class Token { +public: + enum class Type { + Text, + Variable, + Partial, + SectionOpen, + SectionClose, + InvertSectionOpen, + UnescapeVariable, + Comment, + }; + + Token(std::string Str) + : TokenType(Type::Text), RawBody(std::move(Str)), TokenBody(RawBody), + Accessor({}), Indentation(0) {}; + + Token(std::string RawBody, std::string TokenBody, char Identifier) + : RawBody(std::move(RawBody)), TokenBody(std::move(TokenBody)), + Indentation(0) { + TokenType = getTokenType(Identifier); + if (TokenType == Type::Comment) + return; + StringRef AccessorStr(this->TokenBody); + if (TokenType != Type::Variable) + AccessorStr = AccessorStr.substr(1); + Accessor = splitMustacheString(StringRef(AccessorStr).trim()); + } + + Accessor getAccessor() const { return Accessor; } + + Type getType() const { return TokenType; } + + void setIndentation(size_t NewIndentation) { Indentation = NewIndentation; } + + size_t getIndentation() const { return Indentation; } + + static Type getTokenType(char Identifier) { + switch (Identifier) { + case '#': + return Type::SectionOpen; + case '/': + return Type::SectionClose; + case '^': + return Type::InvertSectionOpen; + case '!': + return Type::Comment; + case '>': + return Type::Partial; + case '&': + return Type::UnescapeVariable; + default: + return Type::Variable; + } + } + + Type TokenType; + // RawBody is the original string that was tokenized. + std::string RawBody; + // TokenBody is the original string with the identifier removed. + std::string TokenBody; + Accessor Accessor; + size_t Indentation; +}; + +class ASTNode { +public: + enum Type { + Root, + Text, + Partial, + Variable, + UnescapeVariable, + Section, + InvertSection, + }; + + ASTNode(llvm::BumpPtrAllocator &Alloc, llvm::StringMap &Partials, + llvm::StringMap &Lambdas, + llvm::StringMap &SectionLambdas, + llvm::DenseMap &Escapes) + : Allocator(Alloc), Partials(Partials), Lambdas(Lambdas), + SectionLambdas(SectionLambdas), Escapes(Escapes), Ty(Type::Root), + Parent(nullptr), ParentContext(nullptr) {} + + ASTNode(std::string Body, ASTNode *Parent, llvm::BumpPtrAllocator &Alloc, + llvm::StringMap &Partials, + llvm::StringMap &Lambdas, + llvm::StringMap &SectionLambdas, + llvm::DenseMap &Escapes) + : Allocator(Alloc), Partials(Partials), Lambdas(Lambdas), + SectionLambdas(SectionLambdas), Escapes(Escapes), Ty(Type::Text), + Body(std::move(Body)), Parent(Parent), ParentContext(nullptr) {} + + // Constructor for Section/InvertSection/Variable/UnescapeVariable Nodes + ASTNode(Type Ty, Accessor Accessor, ASTNode *Parent, + llvm::BumpPtrAllocator &Alloc, llvm::StringMap &Partials, + llvm::StringMap &Lambdas, + llvm::StringMap &SectionLambdas, + llvm::DenseMap &Escapes) + : Allocator(Alloc), Partials(Partials), Lambdas(Lambdas), + SectionLambdas(SectionLambdas), Escapes(Escapes), Ty(Ty), + Parent(Parent), Accessor(std::move(Accessor)), ParentContext(nullptr) {} + + void addChild(ASTNode *Child) { Children.emplace_back(Child); }; + + void setRawBody(std::string NewBody) { RawBody = std::move(NewBody); }; + + void setIndentation(size_t NewIndentation) { Indentation = NewIndentation; }; + + void render(const llvm::json::Value &Data, llvm::raw_ostream &OS); + +private: + void renderLambdas(const llvm::json::Value &Contexts, llvm::raw_ostream &OS, + Lambda &L); + + void renderSectionLambdas(const llvm::json::Value &Contexts, + llvm::raw_ostream &OS, SectionLambda &L); + + void renderPartial(const llvm::json::Value &Contexts, llvm::raw_ostream &OS, + ASTNode *Partial); + + void renderChild(const llvm::json::Value &Context, llvm::raw_ostream &OS); + + const llvm::json::Value *findContext(); + + llvm::BumpPtrAllocator &Allocator; + StringMap &Partials; + StringMap &Lambdas; + StringMap &SectionLambdas; + DenseMap &Escapes; + Type Ty; + size_t Indentation = 0; + std::string RawBody; + std::string Body; + ASTNode *Parent; + // TODO: switch implementation to SmallVector + std::vector Children; + const Accessor Accessor; + const llvm::json::Value *ParentContext; +}; + +// A wrapper for arena allocator for ASTNodes +ASTNode *createRootNode(void *Node, llvm::BumpPtrAllocator &Alloc, + llvm::StringMap &Partials, + llvm::StringMap &Lambdas, + llvm::StringMap &SectionLambdas, + llvm::DenseMap &Escapes) { + return new (Node) ASTNode(Alloc, Partials, Lambdas, SectionLambdas, Escapes); +} + +ASTNode *createNode(void *Node, ASTNode::Type T, Accessor A, ASTNode *Parent, + llvm::BumpPtrAllocator &Alloc, + llvm::StringMap &Partials, + llvm::StringMap &Lambdas, + llvm::StringMap &SectionLambdas, + llvm::DenseMap &Escapes) { + return new (Node) ASTNode(T, std::move(A), Parent, Alloc, Partials, Lambdas, + SectionLambdas, Escapes); +} + +ASTNode *createTextNode(void *Node, std::string Body, ASTNode *Parent, + llvm::BumpPtrAllocator &Alloc, + llvm::StringMap &Partials, + llvm::StringMap &Lambdas, + llvm::StringMap &SectionLambdas, + llvm::DenseMap &Escapes) { + return new (Node) ASTNode(std::move(Body), Parent, Alloc, Partials, Lambdas, + SectionLambdas, Escapes); +} + +// Function to check if there is meaningful text behind. +// We determine if a token has meaningful text behind +// if the right of previous token contains anything that is +// not a newline. +// For example: +// "Stuff {{#Section}}" (returns true) +// vs +// "{{#Section}} \n" (returns false) +// We make an exception for when previous token is empty +// and the current token is the second token. +// For example: +// "{{#Section}}" +bool hasTextBehind(size_t Idx, const ArrayRef &Tokens) { + if (Idx == 0) + return true; + + size_t PrevIdx = Idx - 1; + if (Tokens[PrevIdx].getType() != Token::Type::Text) + return true; + + const Token &PrevToken = Tokens[PrevIdx]; + StringRef TokenBody = StringRef(PrevToken.RawBody).rtrim(" \r\t\v"); + return !TokenBody.ends_with("\n") && !(TokenBody.empty() && Idx == 1); +} + +// Function to check if there's no meaningful text ahead. +// We determine if a token has text ahead if the left of previous +// token does not start with a newline. +bool hasTextAhead(size_t Idx, const ArrayRef &Tokens) { + if (Idx >= Tokens.size() - 1) + return true; + + size_t NextIdx = Idx + 1; + if (Tokens[NextIdx].getType() != Token::Type::Text) + return true; + + const Token &NextToken = Tokens[NextIdx]; + StringRef TokenBody = StringRef(NextToken.RawBody).ltrim(" "); + return !TokenBody.starts_with("\r\n") && !TokenBody.starts_with("\n"); +} + +bool requiresCleanUp(Token::Type T) { + // We must clean up all the tokens that could contain child nodes. + return T == Token::Type::SectionOpen || T == Token::Type::InvertSectionOpen || + T == Token::Type::SectionClose || T == Token::Type::Comment || + T == Token::Type::Partial; +} + +// Adjust next token body if there is no text ahead. +// For example: +// The template string +// "{{! Comment }} \nLine 2" +// would be considered as no text ahead and should be rendered as +// " Line 2" +void stripTokenAhead(SmallVectorImpl &Tokens, size_t Idx) { + Token &NextToken = Tokens[Idx + 1]; + StringRef NextTokenBody = NextToken.TokenBody; + // Cut off the leading newline which could be \n or \r\n. + if (NextTokenBody.starts_with("\r\n")) + NextToken.TokenBody = std::move(NextTokenBody.substr(2).str()); + else if (NextTokenBody.starts_with("\n")) + NextToken.TokenBody = std::move(NextTokenBody.substr(1).str()); +} + +// Adjust previous token body if there no text behind. +// For example: +// The template string +// " \t{{#section}}A{{/section}}" +// would be considered as having no text ahead and would be render as +// "A" +// The exception for this is partial tag which requires us to +// keep track of the indentation once it's rendered. +void stripTokenBefore(SmallVectorImpl &Tokens, size_t Idx, + Token &CurrentToken, Token::Type CurrentType) { + Token &PrevToken = Tokens[Idx - 1]; + StringRef PrevTokenBody = PrevToken.TokenBody; + StringRef Unindented = PrevTokenBody.rtrim(" \r\t\v"); + size_t Indentation = PrevTokenBody.size() - Unindented.size(); + if (CurrentType != Token::Type::Partial) + PrevToken.TokenBody = std::move(Unindented.str()); + CurrentToken.setIndentation(Indentation); +} + +// Simple tokenizer that splits the template into tokens. +// The mustache spec allows {{{ }}} to unescape variables, +// but we don't support that here. An unescape variable +// is represented only by {{& variable}}. +SmallVector tokenize(StringRef Template) { + SmallVector Tokens; + StringLiteral Open("{{"); + StringLiteral Close("}}"); + size_t Start = 0; + size_t DelimiterStart = Template.find(Open); + if (DelimiterStart == StringRef::npos) { + Tokens.emplace_back(Template.str()); + return Tokens; + } + while (DelimiterStart != StringRef::npos) { + if (DelimiterStart != Start) + Tokens.emplace_back(Template.substr(Start, DelimiterStart - Start).str()); + size_t DelimiterEnd = Template.find(Close, DelimiterStart); + if (DelimiterEnd == StringRef::npos) + break; + + // Extract the Interpolated variable without delimiters. + size_t InterpolatedStart = DelimiterStart + Open.size(); + size_t InterpolatedEnd = DelimiterEnd - DelimiterStart - Close.size(); + std::string Interpolated = + Template.substr(InterpolatedStart, InterpolatedEnd).str(); + std::string RawBody = Open.str() + Interpolated + Close.str(); + Tokens.emplace_back(RawBody, Interpolated, Interpolated[0]); + Start = DelimiterEnd + Close.size(); + DelimiterStart = Template.find(Open, Start); + } + + if (Start < Template.size()) + Tokens.emplace_back(Template.substr(Start).str()); + + // Fix up white spaces for: + // - open sections + // - inverted sections + // - close sections + // - comments + // + // This loop attempts to find standalone tokens and tries to trim out + // the surrounding whitespace. + // For example: + // if you have the template string + // {{#section}} \n Example \n{{/section}} + // The output should would be + // For example: + // \n Example \n + size_t LastIdx = Tokens.size() - 1; + for (size_t Idx = 0, End = Tokens.size(); Idx < End; ++Idx) { + Token &CurrentToken = Tokens[Idx]; + Token::Type CurrentType = CurrentToken.getType(); + // Check if token type requires cleanup. + bool RequiresCleanUp = requiresCleanUp(CurrentType); + + if (!RequiresCleanUp) + continue; + + // We adjust the token body if there's no text behind or ahead. + // A token is considered to have no text ahead if the right of the previous + // token is a newline followed by spaces. + // A token is considered to have no text behind if the left of the next + // token is spaces followed by a newline. + // eg. + // "Line 1\n {{#section}} \n Line 2 \n {{/section}} \n Line 3" + bool HasTextBehind = hasTextBehind(Idx, Tokens); + bool HasTextAhead = hasTextAhead(Idx, Tokens); + + if ((!HasTextAhead && !HasTextBehind) || (!HasTextAhead && Idx == 0)) + stripTokenAhead(Tokens, Idx); + + if ((!HasTextBehind && !HasTextAhead) || (!HasTextBehind && Idx == LastIdx)) + stripTokenBefore(Tokens, Idx, CurrentToken, CurrentType); + } + return Tokens; +} + +// Custom stream to escape strings. +class EscapeStringStream : public raw_ostream { +public: + explicit EscapeStringStream(llvm::raw_ostream &WrappedStream, + DenseMap &Escape) + : Escape(Escape), WrappedStream(WrappedStream) { + SetUnbuffered(); + } + +protected: + void write_impl(const char *Ptr, size_t Size) override { + llvm::StringRef Data(Ptr, Size); + for (char C : Data) { + auto It = Escape.find(C); + if (It != Escape.end()) + WrappedStream << It->getSecond(); + else + WrappedStream << C; + } + } + + uint64_t current_pos() const override { return WrappedStream.tell(); } + +private: + DenseMap &Escape; + llvm::raw_ostream &WrappedStream; +}; + +// Custom stream to add indentation used to for rendering partials. +class AddIndentationStringStream : public raw_ostream { +public: + explicit AddIndentationStringStream(llvm::raw_ostream &WrappedStream, + size_t Indentation) + : Indentation(Indentation), WrappedStream(WrappedStream) { + SetUnbuffered(); + } + +protected: + void write_impl(const char *Ptr, size_t Size) override { + llvm::StringRef Data(Ptr, Size); + SmallString<0> Indent; + Indent.resize(Indentation, ' '); + for (char C : Data) { + WrappedStream << C; + if (C == '\n') + WrappedStream << Indent; + } + } + + uint64_t current_pos() const override { return WrappedStream.tell(); } + +private: + size_t Indentation; + llvm::raw_ostream &WrappedStream; +}; + +class Parser { +public: + Parser(StringRef TemplateStr, BumpPtrAllocator &Allocator) + : ASTAllocator(Allocator), TemplateStr(TemplateStr) {} + + ASTNode *parse(llvm::BumpPtrAllocator &RenderAlloc, + llvm::StringMap &Partials, + llvm::StringMap &Lambdas, + llvm::StringMap &SectionLambdas, + llvm::DenseMap &Escapes); + +private: + void parseMustache(ASTNode *Parent, llvm::BumpPtrAllocator &Alloc, + llvm::StringMap &Partials, + llvm::StringMap &Lambdas, + llvm::StringMap &SectionLambdas, + llvm::DenseMap &Escapes); + + BumpPtrAllocator &ASTAllocator; + SmallVector Tokens; + size_t CurrentPtr; + StringRef TemplateStr; +}; + +ASTNode *Parser::parse(llvm::BumpPtrAllocator &RenderAlloc, + llvm::StringMap &Partials, + llvm::StringMap &Lambdas, + llvm::StringMap &SectionLambdas, + llvm::DenseMap &Escapes) { + Tokens = tokenize(TemplateStr); + CurrentPtr = 0; + void *Root = ASTAllocator.Allocate(sizeof(ASTNode), alignof(ASTNode)); + ASTNode *RootNode = createRootNode(Root, RenderAlloc, Partials, Lambdas, + SectionLambdas, Escapes); + parseMustache(RootNode, RenderAlloc, Partials, Lambdas, SectionLambdas, + Escapes); + return RootNode; +} + +void Parser::parseMustache(ASTNode *Parent, llvm::BumpPtrAllocator &Alloc, + llvm::StringMap &Partials, + llvm::StringMap &Lambdas, + llvm::StringMap &SectionLambdas, + llvm::DenseMap &Escapes) { + + while (CurrentPtr < Tokens.size()) { + Token CurrentToken = Tokens[CurrentPtr]; + CurrentPtr++; + Accessor A = CurrentToken.getAccessor(); + ASTNode *CurrentNode; + void *Node = ASTAllocator.Allocate(sizeof(ASTNode), alignof(ASTNode)); + + switch (CurrentToken.getType()) { + case Token::Type::Text: { + CurrentNode = + createTextNode(Node, std::move(CurrentToken.TokenBody), Parent, Alloc, + Partials, Lambdas, SectionLambdas, Escapes); + Parent->addChild(CurrentNode); + break; + } + case Token::Type::Variable: { + CurrentNode = + createNode(Node, ASTNode::Variable, std::move(A), Parent, Alloc, + Partials, Lambdas, SectionLambdas, Escapes); + Parent->addChild(CurrentNode); + break; + } + case Token::Type::UnescapeVariable: { + CurrentNode = + createNode(Node, ASTNode::UnescapeVariable, std::move(A), Parent, + Alloc, Partials, Lambdas, SectionLambdas, Escapes); + Parent->addChild(CurrentNode); + break; + } + case Token::Type::Partial: { + CurrentNode = + createNode(Node, ASTNode::Partial, std::move(A), Parent, Alloc, + Partials, Lambdas, SectionLambdas, Escapes); + CurrentNode->setIndentation(CurrentToken.getIndentation()); + Parent->addChild(CurrentNode); + break; + } + case Token::Type::SectionOpen: { + CurrentNode = createNode(Node, ASTNode::Section, A, Parent, Alloc, + Partials, Lambdas, SectionLambdas, Escapes); + size_t Start = CurrentPtr; + parseMustache(CurrentNode, Alloc, Partials, Lambdas, SectionLambdas, + Escapes); + const size_t End = CurrentPtr - 1; + std::string RawBody; + for (std::size_t I = Start; I < End; I++) + RawBody += Tokens[I].RawBody; + CurrentNode->setRawBody(std::move(RawBody)); + Parent->addChild(CurrentNode); + break; + } + case Token::Type::InvertSectionOpen: { + CurrentNode = createNode(Node, ASTNode::InvertSection, A, Parent, Alloc, + Partials, Lambdas, SectionLambdas, Escapes); + size_t Start = CurrentPtr; + parseMustache(CurrentNode, Alloc, Partials, Lambdas, SectionLambdas, + Escapes); + const size_t End = CurrentPtr - 1; + std::string RawBody; + for (size_t Idx = Start; Idx < End; Idx++) + RawBody += Tokens[Idx].RawBody; + CurrentNode->setRawBody(std::move(RawBody)); + Parent->addChild(CurrentNode); + break; + } + case Token::Type::Comment: + break; + case Token::Type::SectionClose: + return; + } + } +} +void toMustacheString(const json::Value &Data, raw_ostream &OS) { + switch (Data.kind()) { + case json::Value::Null: + return; + case json::Value::Number: { + auto Num = *Data.getAsNumber(); + std::ostringstream SS; + SS << Num; + OS << SS.str(); + return; + } + case json::Value::String: { + auto Str = *Data.getAsString(); + OS << Str.str(); + return; + } + + case json::Value::Array: { + auto Arr = *Data.getAsArray(); + if (Arr.empty()) + return; + [[fallthrough]]; + } + case json::Value::Object: + case json::Value::Boolean: { + llvm::json::OStream JOS(OS, 2); + JOS.value(Data); + break; + } + } +} + +void ASTNode::render(const json::Value &Data, raw_ostream &OS) { + ParentContext = &Data; + const json::Value *ContextPtr = Ty == Root ? ParentContext : findContext(); + const json::Value &Context = ContextPtr ? *ContextPtr : nullptr; + + switch (Ty) { + case Root: + renderChild(Data, OS); + return; + case Text: + OS << Body; + return; + case Partial: { + auto Partial = Partials.find(Accessor[0]); + if (Partial != Partials.end()) + renderPartial(Data, OS, Partial->getValue()); + return; + } + case Variable: { + auto Lambda = Lambdas.find(Accessor[0]); + if (Lambda != Lambdas.end()) + renderLambdas(Data, OS, Lambda->getValue()); + else { + EscapeStringStream ES(OS, Escapes); + toMustacheString(Context, ES); + } + return; + } + case UnescapeVariable: { + auto Lambda = Lambdas.find(Accessor[0]); + if (Lambda != Lambdas.end()) + renderLambdas(Data, OS, Lambda->getValue()); + else + toMustacheString(Context, OS); + return; + } + case Section: { + // Sections are not rendered if the context is falsey. + auto SectionLambda = SectionLambdas.find(Accessor[0]); + bool IsLambda = SectionLambda != SectionLambdas.end(); + if (isFalsey(Context) && !IsLambda) + return; + + if (IsLambda) { + renderSectionLambdas(Data, OS, SectionLambda->getValue()); + return; + } + + if (Context.getAsArray()) { + const json::Array *Arr = Context.getAsArray(); + for (const json::Value &V : *Arr) + renderChild(V, OS); + return; + } + renderChild(Context, OS); + return; + } + case InvertSection: { + bool IsLambda = SectionLambdas.find(Accessor[0]) != SectionLambdas.end(); + if (!isFalsey(Context) || IsLambda) + return; + renderChild(Context, OS); + return; + } + } + llvm_unreachable("Invalid ASTNode type"); +} + +const json::Value *ASTNode::findContext() { + // The mustache spec allows for dot notation to access nested values + // a single dot refers to the current context. + // We attempt to find the JSON context in the current node, if it is not + // found, then we traverse the parent nodes to find the context until we + // reach the root node or the context is found. + if (Accessor.empty()) + return nullptr; + if (Accessor[0] == ".") + return ParentContext; + + const json::Object *CurrentContext = ParentContext->getAsObject(); + StringRef CurrentAccessor = Accessor[0]; + ASTNode *CurrentParent = Parent; + + while (!CurrentContext || !CurrentContext->get(CurrentAccessor)) { + if (CurrentParent->Ty != Root) { + CurrentContext = CurrentParent->ParentContext->getAsObject(); + CurrentParent = CurrentParent->Parent; + continue; + } + return nullptr; + } + const json::Value *Context = nullptr; + for (auto [Idx, Acc] : enumerate(Accessor)) { + const json::Value *CurrentValue = CurrentContext->get(Acc); + if (!CurrentValue) + return nullptr; + if (Idx < Accessor.size() - 1) { + CurrentContext = CurrentValue->getAsObject(); + if (!CurrentContext) + return nullptr; + } else + Context = CurrentValue; + } + return Context; +} + +void ASTNode::renderChild(const json::Value &Contexts, llvm::raw_ostream &OS) { + for (ASTNode *Child : Children) + Child->render(Contexts, OS); +} + +void ASTNode::renderPartial(const json::Value &Contexts, llvm::raw_ostream &OS, + ASTNode *Partial) { + AddIndentationStringStream IS(OS, Indentation); + Partial->render(Contexts, IS); +} + +void ASTNode::renderLambdas(const json::Value &Contexts, llvm::raw_ostream &OS, + Lambda &L) { + json::Value LambdaResult = L(); + std::string LambdaStr; + raw_string_ostream Output(LambdaStr); + toMustacheString(LambdaResult, Output); + Parser P = Parser(LambdaStr, Allocator); + ASTNode *LambdaNode = + P.parse(Allocator, Partials, Lambdas, SectionLambdas, Escapes); + + EscapeStringStream ES(OS, Escapes); + if (Ty == Variable) { + LambdaNode->render(Contexts, ES); + return; + } + LambdaNode->render(Contexts, OS); +} + +void ASTNode::renderSectionLambdas(const json::Value &Contexts, + llvm::raw_ostream &OS, SectionLambda &L) { + json::Value Return = L(RawBody); + if (isFalsey(Return)) + return; + std::string LambdaStr; + raw_string_ostream Output(LambdaStr); + toMustacheString(Return, Output); + Parser P = Parser(LambdaStr, Allocator); + ASTNode *LambdaNode = + P.parse(Allocator, Partials, Lambdas, SectionLambdas, Escapes); + LambdaNode->render(Contexts, OS); + return; +} + +void Template::render(const json::Value &Data, llvm::raw_ostream &OS) { + Tree->render(Data, OS); + RenderAllocator.Reset(); +} + +void Template::registerPartial(std::string Name, std::string Partial) { + Parser P = Parser(Partial, AstAllocator); + ASTNode *PartialTree = + P.parse(RenderAllocator, Partials, Lambdas, SectionLambdas, Escapes); + Partials.insert(std::make_pair(Name, PartialTree)); +} + +void Template::registerLambda(std::string Name, Lambda L) { Lambdas[Name] = L; } + +void Template::registerLambda(std::string Name, SectionLambda L) { + SectionLambdas[Name] = L; +} + +void Template::overrideEscapeCharacters(DenseMap E) { + Escapes = std::move(E); +} + +Template::Template(StringRef TemplateStr) { + Parser P = Parser(TemplateStr, AstAllocator); + Tree = P.parse(RenderAllocator, Partials, Lambdas, SectionLambdas, Escapes); + // The default behavior is to escape html entities. + DenseMap HtmlEntities = {{'&', "&"}, + {'<', "<"}, + {'>', ">"}, + {'"', """}, + {'\'', "'"}}; + overrideEscapeCharacters(HtmlEntities); +} + +Template::Template(Template &&Other) noexcept + : Partials(std::move(Other.Partials)), Lambdas(std::move(Other.Lambdas)), + SectionLambdas(std::move(Other.SectionLambdas)), + Escapes(std::move(Other.Escapes)), Tree(Other.Tree), + AstAllocator(std::move(Other.AstAllocator)), + RenderAllocator(std::move(Other.RenderAllocator)) { + Other.Tree = nullptr; +} + +Template &Template::operator=(Template &&Other) noexcept { + if (this != &Other) { + Partials = std::move(Other.Partials); + Lambdas = std::move(Other.Lambdas); + SectionLambdas = std::move(Other.SectionLambdas); + Escapes = std::move(Other.Escapes); + Tree = Other.Tree; + AstAllocator = std::move(Other.AstAllocator); + RenderAllocator = std::move(Other.RenderAllocator); + Other.Tree = nullptr; + } + return *this; +} +} // namespace llvm::mustache diff --git a/llvm/unittests/Support/CMakeLists.txt b/llvm/unittests/Support/CMakeLists.txt index 6de8165826442..6c4e7cb689b20 100644 --- a/llvm/unittests/Support/CMakeLists.txt +++ b/llvm/unittests/Support/CMakeLists.txt @@ -61,6 +61,7 @@ add_llvm_unittest(SupportTests MemoryBufferRefTest.cpp MemoryBufferTest.cpp MemoryTest.cpp + MustacheTest.cpp ModRefTest.cpp NativeFormatTests.cpp OptimizedStructLayoutTest.cpp diff --git a/llvm/unittests/Support/MustacheTest.cpp b/llvm/unittests/Support/MustacheTest.cpp new file mode 100644 index 0000000000000..6ab3d4b01bc1b --- /dev/null +++ b/llvm/unittests/Support/MustacheTest.cpp @@ -0,0 +1,1226 @@ +//===- llvm/unittest/Support/MustacheTest.cpp ----------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// +// +// Test conforming to Mustache 1.4.2 spec found here: +// https://github.com/mustache/spec +// +//===----------------------------------------------------------------------===// + +#include "llvm/Support/Mustache.h" +#include "llvm/Support/raw_ostream.h" +#include "gtest/gtest.h" + +using namespace llvm; +using namespace llvm::mustache; +using namespace llvm::json; + +TEST(MustacheInterpolation, NoInterpolation) { + // Mustache-free templates should render as-is. + Value D = {}; + auto T = Template("Hello from {Mustache}!\n"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("Hello from {Mustache}!\n", Out); +} + +TEST(MustacheInterpolation, BasicInterpolation) { + // Unadorned tags should interpolate content into the template. + Value D = Object{{"subject", "World"}}; + auto T = Template("Hello, {{subject}}!"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("Hello, World!", Out); +} + +TEST(MustacheInterpolation, NoReinterpolation) { + // Interpolated tag output should not be re-interpolated. + Value D = Object{{"template", "{{planet}}"}, {"planet", "Earth"}}; + auto T = Template("{{template}}: {{planet}}"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("{{planet}}: Earth", Out); +} + +TEST(MustacheInterpolation, HTMLEscaping) { + // Interpolated tag output should not be re-interpolated. + Value D = Object{ + {"forbidden", "& \" < >"}, + }; + auto T = Template("These characters should be HTML escaped: {{forbidden}}\n"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("These characters should be HTML escaped: & " < >\n", + Out); +} + +TEST(MustacheInterpolation, Ampersand) { + // Interpolated tag output should not be re-interpolated. + Value D = Object{ + {"forbidden", "& \" < >"}, + }; + auto T = + Template("These characters should not be HTML escaped: {{&forbidden}}\n"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("These characters should not be HTML escaped: & \" < >\n", Out); +} + +TEST(MustacheInterpolation, BasicIntegerInterpolation) { + // Integers should interpolate seamlessly. + Value D = Object{{"mph", 85}}; + auto T = Template("{{mph}} miles an hour!"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("85 miles an hour!", Out); +} + +TEST(MustacheInterpolation, AmpersandIntegerInterpolation) { + // Integers should interpolate seamlessly. + Value D = Object{{"mph", 85}}; + auto T = Template("{{&mph}} miles an hour!"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("85 miles an hour!", Out); +} + +TEST(MustacheInterpolation, BasicDecimalInterpolation) { + // Decimals should interpolate seamlessly with proper significance. + Value D = Object{{"power", 1.21}}; + auto T = Template("{{power}} jiggawatts!"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("1.21 jiggawatts!", Out); +} + +TEST(MustacheInterpolation, BasicNullInterpolation) { + // Nulls should interpolate as the empty string. + Value D = Object{{"cannot", nullptr}}; + auto T = Template("I ({{cannot}}) be seen!"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("I () be seen!", Out); +} + +TEST(MustacheInterpolation, AmpersandNullInterpolation) { + // Nulls should interpolate as the empty string. + Value D = Object{{"cannot", nullptr}}; + auto T = Template("I ({{&cannot}}) be seen!"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("I () be seen!", Out); +} + +TEST(MustacheInterpolation, BasicContextMissInterpolation) { + // Failed context lookups should default to empty strings. + Value D = Object{}; + auto T = Template("I ({{cannot}}) be seen!"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("I () be seen!", Out); +} + +TEST(MustacheInterpolation, DottedNamesBasicInterpolation) { + // Dotted names should be considered a form of shorthand for sections. + Value D = Object{{"person", Object{{"name", "Joe"}}}}; + auto T = Template("{{person.name}} == {{#person}}{{name}}{{/person}}"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("Joe == Joe", Out); +} + +TEST(MustacheInterpolation, DottedNamesAmpersandInterpolation) { + // Dotted names should be considered a form of shorthand for sections. + Value D = Object{{"person", Object{{"name", "Joe"}}}}; + auto T = Template("{{&person.name}} == {{#person}}{{&name}}{{/person}}"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("Joe == Joe", Out); +} + +TEST(MustacheInterpolation, DottedNamesArbitraryDepth) { + // Dotted names should be functional to any level of nesting. + Value D = Object{ + {"a", + Object{{"b", + Object{{"c", + Object{{"d", + Object{{"e", Object{{"name", "Phil"}}}}}}}}}}}}; + auto T = Template("{{a.b.c.d.e.name}}"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("Phil", Out); +} + +TEST(MustacheInterpolation, DottedNamesBrokenChains) { + // Any falsey value prior to the last part of the name should yield ''. + Value D = Object{{"a", Object{}}}; + auto T = Template("{{a.b.c}} == "); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ(" == ", Out); +} + +TEST(MustacheInterpolation, DottedNamesBrokenChainResolution) { + // Each part of a dotted name should resolve only against its parent. + Value D = + Object{{"a", Object{{"b", Object{}}}}, {"c", Object{{"name", "Jim"}}}}; + auto T = Template("{{a.b.c.name}} == "); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ(" == ", Out); +} + +TEST(MustacheInterpolation, DottedNamesInitialResolution) { + // The first part of a dotted name should resolve as any other name. + Value D = Object{ + {"a", + Object{ + {"b", + Object{{"c", + Object{{"d", Object{{"e", Object{{"name", "Phil"}}}}}}}}}}}, + {"b", + Object{{"c", Object{{"d", Object{{"e", Object{{"name", "Wrong"}}}}}}}}}}; + auto T = Template("{{#a}}{{b.c.d.e.name}}{{/a}}"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("Phil", Out); +} + +TEST(MustacheInterpolation, DottedNamesContextPrecedence) { + // Dotted names should be resolved against former resolutions. + Value D = + Object{{"a", Object{{"b", Object{}}}}, {"b", Object{{"c", "ERROR"}}}}; + auto T = Template("{{#a}}{{b.c}}{{/a}}"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("", Out); +} + +TEST(MustacheInterpolation, DottedNamesAreNotSingleKeys) { + // Dotted names shall not be parsed as single, atomic keys + Value D = Object{{"a.b", "c"}}; + auto T = Template("{{a.b}}"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("", Out); +} + +TEST(MustacheInterpolation, DottedNamesNoMasking) { + // Dotted Names in a given context are unavailable due to dot splitting + Value D = Object{{"a.b", "c"}, {"a", Object{{"b", "d"}}}}; + auto T = Template("{{a.b}}"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("d", Out); +} + +TEST(MustacheInterpolation, ImplicitIteratorsBasicInterpolation) { + // Unadorned tags should interpolate content into the template. + Value D = "world"; + auto T = Template("Hello, {{.}}!\n"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("Hello, world!\n", Out); +} + +TEST(MustacheInterpolation, ImplicitIteratorsAmersand) { + // Basic interpolation should be HTML escaped. + Value D = "& \" < >"; + auto T = Template("These characters should not be HTML escaped: {{&.}}\n"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("These characters should not be HTML escaped: & \" < >\n", Out); +} + +TEST(MustacheInterpolation, ImplicitIteratorsInteger) { + // Integers should interpolate seamlessly. + Value D = 85; + auto T = Template("{{.}} miles an hour!\n"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("85 miles an hour!\n", Out); +} + +TEST(MustacheInterpolation, InterpolationSurroundingWhitespace) { + // Interpolation should not alter surrounding whitespace. + Value D = Object{{"string", "---"}}; + auto T = Template("| {{string}} |"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("| --- |", Out); +} + +TEST(MustacheInterpolation, AmersandSurroundingWhitespace) { + // Interpolation should not alter surrounding whitespace. + Value D = Object{{"string", "---"}}; + auto T = Template("| {{&string}} |"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("| --- |", Out); +} + +TEST(MustacheInterpolation, StandaloneInterpolationWithWhitespace) { + // Standalone interpolation should not alter surrounding whitespace. + Value D = Object{{"string", "---"}}; + auto T = Template(" {{string}}\n"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ(" ---\n", Out); +} + +TEST(MustacheInterpolation, StandaloneAmpersandWithWhitespace) { + // Standalone interpolation should not alter surrounding whitespace. + Value D = Object{{"string", "---"}}; + auto T = Template(" {{&string}}\n"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ(" ---\n", Out); +} + +TEST(MustacheInterpolation, InterpolationWithPadding) { + // Superfluous in-tag whitespace should be ignored. + Value D = Object{{"string", "---"}}; + auto T = Template("|{{ string }}|"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("|---|", Out); +} + +TEST(MustacheInterpolation, AmpersandWithPadding) { + // Superfluous in-tag whitespace should be ignored. + Value D = Object{{"string", "---"}}; + auto T = Template("|{{& string }}|"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("|---|", Out); +} + +TEST(MustacheInterpolation, InterpolationWithPaddingAndNewlines) { + // Superfluous in-tag whitespace should be ignored. + Value D = Object{{"string", "---"}}; + auto T = Template("|{{ string \n\n\n }}|"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("|---|", Out); +} + +TEST(MustacheSections, Truthy) { + Value D = Object{{"boolean", true}}; + auto T = Template("{{#boolean}}This should be rendered.{{/boolean}}"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("This should be rendered.", Out); +} + +TEST(MustacheSections, Falsey) { + Value D = Object{{"boolean", false}}; + auto T = Template("{{#boolean}}This should not be rendered.{{/boolean}}"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("", Out); +} + +TEST(MustacheInterpolation, IsFalseyNull) { + // Mustache-free templates should render as-is. + Value D = Object{{"boolean", nullptr}}; + auto T = Template("Hello, {{#boolean}}World{{/boolean}}"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("Hello, ", Out); +} + +TEST(MustacheInterpolation, IsFalseyArray) { + // Mustache-free templates should render as-is. + Value D = Object{{"boolean", Array()}}; + auto T = Template("Hello, {{#boolean}}World{{/boolean}}"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("Hello, ", Out); +} + +TEST(MustacheInterpolation, IsFalseyObject) { + // Mustache-free templates should render as-is. + Value D = Object{{"boolean", Object{}}}; + auto T = Template("Hello, {{#boolean}}World{{/boolean}}"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("Hello, World", Out); +} + +TEST(MustacheInterpolation, DoubleRendering) { + // Mustache-free templates should render as-is. + Value D1 = Object{{"subject", "World"}}; + auto T = Template("Hello, {{subject}}!"); + std::string Out1; + raw_string_ostream OS1(Out1); + T.render(D1, OS1); + EXPECT_EQ("Hello, World!", Out1); + std::string Out2; + raw_string_ostream OS2(Out2); + Value D2 = Object{{"subject", "New World"}}; + T.render(D2, OS2); + EXPECT_EQ("Hello, New World!", Out2); +} + +TEST(MustacheSections, NullIsFalsey) { + Value D = Object{{"null", nullptr}}; + auto T = Template("{{#null}}This should not be rendered.{{/null}}"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("", Out); +} + +TEST(MustacheSections, Context) { + Value D = Object{{"context", Object{{"name", "Joe"}}}}; + auto T = Template("{{#context}}Hi {{name}}.{{/context}}"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("Hi Joe.", Out); +} + +TEST(MustacheSections, ParentContexts) { + Value D = Object{{"a", "foo"}, + {"b", "wrong"}, + {"sec", Object{{"b", "bar"}}}, + {"c", Object{{"d", "baz"}}}}; + auto T = Template("{{#sec}}{{a}}, {{b}}, {{c.d}}{{/sec}}"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("foo, bar, baz", Out); +} + +TEST(MustacheSections, VariableTest) { + Value D = Object{{"foo", "bar"}}; + auto T = Template("{{#foo}}{{.}} is {{foo}}{{/foo}}"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("bar is bar", Out); +} + +TEST(MustacheSections, ListContexts) { + Value D = Object{ + {"tops", + Array{Object{ + {"tname", Object{{"upper", "A"}, {"lower", "a"}}}, + {"middles", + Array{Object{{"mname", "1"}, + {"bottoms", Array{Object{{"bname", "x"}}, + Object{{"bname", "y"}}}}}}}}}}}; + auto T = Template("{{#tops}}" + "{{#middles}}" + "{{tname.lower}}{{mname}}." + "{{#bottoms}}" + "{{tname.upper}}{{mname}}{{bname}}." + "{{/bottoms}}" + "{{/middles}}" + "{{/tops}}"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("a1.A1x.A1y.", Out); +} + +TEST(MustacheSections, DeeplyNestedContexts) { + Value D = Object{ + {"a", Object{{"one", 1}}}, + {"b", Object{{"two", 2}}}, + {"c", Object{{"three", 3}, {"d", Object{{"four", 4}, {"five", 5}}}}}}; + auto T = Template( + "{{#a}}\n{{one}}\n{{#b}}\n{{one}}{{two}}{{one}}\n{{#c}}\n{{one}}{{two}}{{" + "three}}{{two}}{{one}}\n{{#d}}\n{{one}}{{two}}{{three}}{{four}}{{three}}{" + "{two}}{{one}}\n{{#five}}\n{{one}}{{two}}{{three}}{{four}}{{five}}{{four}" + "}{{three}}{{two}}{{one}}\n{{one}}{{two}}{{three}}{{four}}{{.}}6{{.}}{{" + "four}}{{three}}{{two}}{{one}}\n{{one}}{{two}}{{three}}{{four}}{{five}}{{" + "four}}{{three}}{{two}}{{one}}\n{{/" + "five}}\n{{one}}{{two}}{{three}}{{four}}{{three}}{{two}}{{one}}\n{{/" + "d}}\n{{one}}{{two}}{{three}}{{two}}{{one}}\n{{/" + "c}}\n{{one}}{{two}}{{one}}\n{{/b}}\n{{one}}\n{{/a}}\n"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("1\n121\n12321\n1234321\n123454321\n12345654321\n123454321\n1234321" + "\n12321\n121\n1\n", + Out); +} + +TEST(MustacheSections, List) { + Value D = Object{{"list", Array{Object{{"item", 1}}, Object{{"item", 2}}, + Object{{"item", 3}}}}}; + auto T = Template("{{#list}}{{item}}{{/list}}"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("123", Out); +} + +TEST(MustacheSections, EmptyList) { + Value D = Object{{"list", Array{}}}; + auto T = Template("{{#list}}Yay lists!{{/list}}"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("", Out); +} + +TEST(MustacheSections, Doubled) { + Value D = Object{{"bool", true}, {"two", "second"}}; + auto T = Template("{{#bool}}\n* first\n{{/bool}}\n* " + "{{two}}\n{{#bool}}\n* third\n{{/bool}}\n"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("* first\n* second\n* third\n", Out); +} + +TEST(MustacheSections, NestedTruthy) { + Value D = Object{{"bool", true}}; + auto T = Template("| A {{#bool}}B {{#bool}}C{{/bool}} D{{/bool}} E |"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("| A B C D E |", Out); +} + +TEST(MustacheSections, NestedFalsey) { + Value D = Object{{"bool", false}}; + auto T = Template("| A {{#bool}}B {{#bool}}C{{/bool}} D{{/bool}} E |"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("| A E |", Out); +} + +TEST(MustacheSections, ContextMisses) { + Value D = Object{}; + auto T = Template("[{{#missing}}Found key 'missing'!{{/missing}}]"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("[]", Out); +} + +TEST(MustacheSections, ImplicitIteratorString) { + Value D = Object{{"list", Array{"a", "b", "c", "d", "e"}}}; + auto T = Template("{{#list}}({{.}}){{/list}}"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("(a)(b)(c)(d)(e)", Out); +} + +TEST(MustacheSections, ImplicitIteratorInteger) { + Value D = Object{{"list", Array{1, 2, 3, 4, 5}}}; + auto T = Template("{{#list}}({{.}}){{/list}}"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("(1)(2)(3)(4)(5)", Out); +} + +TEST(MustacheSections, ImplicitIteratorArray) { + Value D = Object{{"list", Array{Array{1, 2, 3}, Array{"a", "b", "c"}}}}; + auto T = Template("{{#list}}({{#.}}{{.}}{{/.}}){{/list}}"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("(123)(abc)", Out); +} + +TEST(MustacheSections, ImplicitIteratorHTMLEscaping) { + Value D = Object{{"list", Array{"&", "\"", "<", ">"}}}; + auto T = Template("{{#list}}({{.}}){{/list}}"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("(&)(")(<)(>)", Out); +} + +TEST(MustacheSections, ImplicitIteratorAmpersand) { + Value D = Object{{"list", Array{"&", "\"", "<", ">"}}}; + auto T = Template("{{#list}}({{&.}}){{/list}}"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("(&)(\")(<)(>)", Out); +} + +TEST(MustacheSections, ImplicitIteratorRootLevel) { + Value D = Array{Object{{"value", "a"}}, Object{{"value", "b"}}}; + auto T = Template("{{#.}}({{value}}){{/.}}"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("(a)(b)", Out); +} + +TEST(MustacheSections, DottedNamesTruthy) { + Value D = Object{{"a", Object{{"b", Object{{"c", true}}}}}}; + auto T = Template("{{#a.b.c}}Here{{/a.b.c}} == Here"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("Here == Here", Out); +} + +TEST(MustacheSections, DottedNamesFalsey) { + Value D = Object{{"a", Object{{"b", Object{{"c", false}}}}}}; + auto T = Template("{{#a.b.c}}Here{{/a.b.c}} == "); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ(" == ", Out); +} + +TEST(MustacheSections, DottedNamesBrokenChains) { + Value D = Object{{"a", Object{}}}; + auto T = Template("{{#a.b.c}}Here{{/a.b.c}} == "); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ(" == ", Out); +} + +TEST(MustacheSections, SurroundingWhitespace) { + Value D = Object{{"boolean", true}}; + auto T = Template(" | {{#boolean}}\t|\t{{/boolean}} | \n"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ(" | \t|\t | \n", Out); +} + +TEST(MustacheSections, InternalWhitespace) { + Value D = Object{{"boolean", true}}; + auto T = Template( + " | {{#boolean}} {{! Important Whitespace }}\n {{/boolean}} | \n"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ(" | \n | \n", Out); +} + +TEST(MustacheSections, IndentedInlineSections) { + Value D = Object{{"boolean", true}}; + auto T = + Template(" {{#boolean}}YES{{/boolean}}\n {{#boolean}}GOOD{{/boolean}}\n"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ(" YES\n GOOD\n", Out); +} + +TEST(MustacheSections, StandaloneLines) { + Value D = Object{{"boolean", true}}; + auto T = Template("| This Is\n{{#boolean}}\n|\n{{/boolean}}\n| A Line\n"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("| This Is\n|\n| A Line\n", Out); +} + +TEST(MustacheSections, IndentedStandaloneLines) { + Value D = Object{{"boolean", true}}; + auto T = Template("| This Is\n {{#boolean}}\n|\n {{/boolean}}\n| A Line\n"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("| This Is\n|\n| A Line\n", Out); +} + +TEST(MustacheSections, StandaloneLineEndings) { + Value D = Object{{"boolean", true}}; + auto T = Template("|\r\n{{#boolean}}\r\n{{/boolean}}\r\n|"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("|\r\n|", Out); +} + +TEST(MustacheSections, StandaloneWithoutPreviousLine) { + Value D = Object{{"boolean", true}}; + auto T = Template(" {{#boolean}}\n#{{/boolean}}\n/"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("#\n/", Out); +} + +TEST(MustacheSections, StandaloneWithoutNewline) { + Value D = Object{{"boolean", true}}; + auto T = Template("#{{#boolean}}\n/\n {{/boolean}}"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("#\n/\n", Out); +} + +TEST(MustacheSections, Padding) { + Value D = Object{{"boolean", true}}; + auto T = Template("|{{# boolean }}={{/ boolean }}|"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("|=|", Out); +} + +TEST(MustacheInvertedSections, Falsey) { + Value D = Object{{"boolean", false}}; + auto T = Template("{{^boolean}}This should be rendered.{{/boolean}}"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("This should be rendered.", Out); +} + +TEST(MustacheInvertedSections, Truthy) { + Value D = Object{{"boolean", true}}; + auto T = Template("{{^boolean}}This should not be rendered.{{/boolean}}"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("", Out); +} + +TEST(MustacheInvertedSections, NullIsFalsey) { + Value D = Object{{"null", nullptr}}; + auto T = Template("{{^null}}This should be rendered.{{/null}}"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("This should be rendered.", Out); +} + +TEST(MustacheInvertedSections, Context) { + Value D = Object{{"context", Object{{"name", "Joe"}}}}; + auto T = Template("{{^context}}Hi {{name}}.{{/context}}"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("", Out); +} + +TEST(MustacheInvertedSections, List) { + Value D = Object{ + {"list", Array{Object{{"n", 1}}, Object{{"n", 2}}, Object{{"n", 3}}}}}; + auto T = Template("{{^list}}{{n}}{{/list}}"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("", Out); +} + +TEST(MustacheInvertedSections, EmptyList) { + Value D = Object{{"list", Array{}}}; + auto T = Template("{{^list}}Yay lists!{{/list}}"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("Yay lists!", Out); +} + +TEST(MustacheInvertedSections, Doubled) { + Value D = Object{{"bool", false}, {"two", "second"}}; + auto T = Template("{{^bool}}\n* first\n{{/bool}}\n* " + "{{two}}\n{{^bool}}\n* third\n{{/bool}}\n"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("* first\n* second\n* third\n", Out); +} + +TEST(MustacheInvertedSections, NestedFalsey) { + Value D = Object{{"bool", false}}; + auto T = Template("| A {{^bool}}B {{^bool}}C{{/bool}} D{{/bool}} E |"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("| A B C D E |", Out); +} + +TEST(MustacheInvertedSections, NestedTruthy) { + Value D = Object{{"bool", true}}; + auto T = Template("| A {{^bool}}B {{^bool}}C{{/bool}} D{{/bool}} E |"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("| A E |", Out); +} + +TEST(MustacheInvertedSections, ContextMisses) { + Value D = Object{}; + auto T = Template("[{{^missing}}Cannot find key 'missing'!{{/missing}}]"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("[Cannot find key 'missing'!]", Out); +} + +TEST(MustacheInvertedSections, DottedNamesTruthy) { + Value D = Object{{"a", Object{{"b", Object{{"c", true}}}}}}; + auto T = Template("{{^a.b.c}}Not Here{{/a.b.c}} == "); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ(" == ", Out); +} + +TEST(MustacheInvertedSections, DottedNamesFalsey) { + Value D = Object{{"a", Object{{"b", Object{{"c", false}}}}}}; + auto T = Template("{{^a.b.c}}Not Here{{/a.b.c}} == Not Here"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("Not Here == Not Here", Out); +} + +TEST(MustacheInvertedSections, DottedNamesBrokenChains) { + Value D = Object{{"a", Object{}}}; + auto T = Template("{{^a.b.c}}Not Here{{/a.b.c}} == Not Here"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("Not Here == Not Here", Out); +} + +TEST(MustacheInvertedSections, SurroundingWhitespace) { + Value D = Object{{"boolean", false}}; + auto T = Template(" | {{^boolean}}\t|\t{{/boolean}} | \n"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ(" | \t|\t | \n", Out); +} + +TEST(MustacheInvertedSections, InternalWhitespace) { + Value D = Object{{"boolean", false}}; + auto T = Template( + " | {{^boolean}} {{! Important Whitespace }}\n {{/boolean}} | \n"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ(" | \n | \n", Out); +} + +TEST(MustacheInvertedSections, IndentedInlineSections) { + Value D = Object{{"boolean", false}}; + auto T = + Template(" {{^boolean}}NO{{/boolean}}\n {{^boolean}}WAY{{/boolean}}\n"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ(" NO\n WAY\n", Out); +} + +TEST(MustacheInvertedSections, StandaloneLines) { + Value D = Object{{"boolean", false}}; + auto T = Template("| This Is\n{{^boolean}}\n|\n{{/boolean}}\n| A Line\n"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("| This Is\n|\n| A Line\n", Out); +} + +TEST(MustacheInvertedSections, StandaloneIndentedLines) { + Value D = Object{{"boolean", false}}; + auto T = Template("| This Is\n {{^boolean}}\n|\n {{/boolean}}\n| A Line\n"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("| This Is\n|\n| A Line\n", Out); +} + +TEST(MustacheInvertedSections, StandaloneLineEndings) { + Value D = Object{{"boolean", false}}; + auto T = Template("|\r\n{{^boolean}}\r\n{{/boolean}}\r\n|"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("|\r\n|", Out); +} + +TEST(MustacheInvertedSections, StandaloneWithoutPreviousLine) { + Value D = Object{{"boolean", false}}; + auto T = Template(" {{^boolean}}\n^{{/boolean}}\n/"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("^\n/", Out); +} + +TEST(MustacheInvertedSections, StandaloneWithoutNewline) { + Value D = Object{{"boolean", false}}; + auto T = Template("^{{^boolean}}\n/\n {{/boolean}}"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("^\n/\n", Out); +} + +TEST(MustacheInvertedSections, Padding) { + Value D = Object{{"boolean", false}}; + auto T = Template("|{{^ boolean }}={{/ boolean }}|"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("|=|", Out); +} + +TEST(MustachePartials, BasicBehavior) { + Value D = Object{}; + auto T = Template("{{>text}}"); + T.registerPartial("text", "from partial"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("from partial", Out); +} + +TEST(MustachePartials, FailedLookup) { + Value D = Object{}; + auto T = Template("{{>text}}"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("", Out); +} + +TEST(MustachePartials, Context) { + Value D = Object{{"text", "content"}}; + auto T = Template("{{>partial}}"); + T.registerPartial("partial", "*{{text}}*"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("*content*", Out); +} + +TEST(MustachePartials, Recursion) { + Value D = + Object{{"content", "X"}, + {"nodes", Array{Object{{"content", "Y"}, {"nodes", Array{}}}}}}; + auto T = Template("{{>node}}"); + T.registerPartial("node", "{{content}}({{#nodes}}{{>node}}{{/nodes}})"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("X(Y())", Out); +} + +TEST(MustachePartials, Nested) { + Value D = Object{{"a", "hello"}, {"b", "world"}}; + auto T = Template("{{>outer}}"); + T.registerPartial("outer", "*{{a}} {{>inner}}*"); + T.registerPartial("inner", "{{b}}!"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("*hello world!*", Out); +} + +TEST(MustachePartials, SurroundingWhitespace) { + Value D = Object{}; + auto T = Template("| {{>partial}} |"); + T.registerPartial("partial", "\t|\t"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("| \t|\t |", Out); +} + +TEST(MustachePartials, InlineIndentation) { + Value D = Object{{"data", "|"}}; + auto T = Template(" {{data}} {{> partial}}\n"); + T.registerPartial("partial", "<\n<"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ(" | <\n<\n", Out); +} + +TEST(MustachePartials, PaddingWhitespace) { + Value D = Object{{"boolean", true}}; + auto T = Template("|{{> partial }}|"); + T.registerPartial("partial", "[]"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("|[]|", Out); +} + +TEST(MustacheLambdas, BasicInterpolation) { + Value D = Object{}; + auto T = Template("Hello, {{lambda}}!"); + Lambda L = []() -> llvm::json::Value { return "World"; }; + T.registerLambda("lambda", L); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("Hello, World!", Out); +} + +TEST(MustacheLambdas, InterpolationExpansion) { + Value D = Object{{"planet", "World"}}; + auto T = Template("Hello, {{lambda}}!"); + Lambda L = []() -> llvm::json::Value { return "{{planet}}"; }; + T.registerLambda("lambda", L); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("Hello, World!", Out); +} + +TEST(MustacheLambdas, BasicMultipleCalls) { + Value D = Object{}; + auto T = Template("{{lambda}} == {{lambda}} == {{lambda}}"); + int I = 0; + Lambda L = [&I]() -> llvm::json::Value { + I += 1; + return I; + }; + T.registerLambda("lambda", L); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("1 == 2 == 3", Out); +} + +TEST(MustacheLambdas, Escaping) { + Value D = Object{}; + auto T = Template("<{{lambda}}{{&lambda}}"); + Lambda L = []() -> llvm::json::Value { return ">"; }; + T.registerLambda("lambda", L); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("<>>", Out); +} + +TEST(MustacheLambdas, Sections) { + Value D = Object{}; + auto T = Template("<{{#lambda}}{{x}}{{/lambda}}>"); + SectionLambda L = [](StringRef Text) -> llvm::json::Value { + if (Text == "{{x}}") { + return "yes"; + } + return "no"; + }; + T.registerLambda("lambda", L); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("", Out); +} + +TEST(MustacheLambdas, SectionExpansion) { + Value D = Object{ + {"planet", "Earth"}, + }; + auto T = Template("<{{#lambda}}-{{/lambda}}>"); + SectionLambda L = [](StringRef Text) -> llvm::json::Value { + SmallString<128> Result; + Result += Text; + Result += "{{planet}}"; + Result += Text; + return Result; + }; + T.registerLambda("lambda", L); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("<-Earth->", Out); +} + +TEST(MustacheLambdas, SectionsMultipleCalls) { + Value D = Object{}; + auto T = Template("{{#lambda}}FILE{{/lambda}} != {{#lambda}}LINE{{/lambda}}"); + SectionLambda L = [](StringRef Text) -> llvm::json::Value { + SmallString<128> Result; + Result += "__"; + Result += Text; + Result += "__"; + return Result; + }; + T.registerLambda("lambda", L); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("__FILE__ != __LINE__", Out); +} + +TEST(MustacheLambdas, InvertedSections) { + Value D = Object{{"static", "static"}}; + auto T = Template("<{{^lambda}}{{static}}{{/lambda}}>"); + SectionLambda L = [](StringRef Text) -> llvm::json::Value { return false; }; + T.registerLambda("lambda", L); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("<>", Out); +} + +TEST(MustacheComments, Inline) { + // Comment blocks should be removed from the template. + Value D = {}; + auto T = Template("12345{{! Comment Block! }}67890"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("1234567890", Out); +} + +TEST(MustacheComments, Multiline) { + // Multiline comments should be permitted. + Value D = {}; + auto T = + Template("12345{{!\n This is a\n multi-line comment...\n}}67890\n"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("1234567890\n", Out); +} + +TEST(MustacheComments, Standalone) { + // All standalone comment lines should be removed. + Value D = {}; + auto T = Template("Begin.\n{{! Comment Block! }}\nEnd.\n"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("Begin.\nEnd.\n", Out); +} + +TEST(MustacheComments, IndentedStandalone) { + // All standalone comment lines should be removed. + Value D = {}; + auto T = Template("Begin.\n {{! Indented Comment Block! }}\nEnd.\n"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("Begin.\nEnd.\n", Out); +} + +TEST(MustacheComments, StandaloneLineEndings) { + // "\r\n" should be considered a newline for standalone tags. + Value D = {}; + auto T = Template("|\r\n{{! Standalone Comment }}\r\n|"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("|\r\n|", Out); +} + +TEST(MustacheComments, StandaloneWithoutPreviousLine) { + // Standalone tags should not require a newline to precede them. + Value D = {}; + auto T = Template(" {{! I'm Still Standalone }}\n!"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("!", Out); +} + +TEST(MustacheComments, StandaloneWithoutNewline) { + // Standalone tags should not require a newline to follow them. + Value D = {}; + auto T = Template("!\n {{! I'm Still Standalone }}"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("!\n", Out); +} + +TEST(MustacheComments, MultilineStandalone) { + // All standalone comment lines should be removed. + Value D = {}; + auto T = Template("Begin.\n{{!\nSomething's going on here...\n}}\nEnd.\n"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("Begin.\nEnd.\n", Out); +} + +TEST(MustacheComments, IndentedMultilineStandalone) { + // All standalone comment lines should be removed. + Value D = {}; + auto T = + Template("Begin.\n {{!\n Something's going on here...\n }}\nEnd.\n"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("Begin.\nEnd.\n", Out); +} + +TEST(MustacheComments, IndentedInline) { + // Inline comments should not strip whitespace. + Value D = {}; + auto T = Template(" 12 {{! 34 }}\n"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ(" 12 \n", Out); +} + +TEST(MustacheComments, SurroundingWhitespace) { + // Comment removal should preserve surrounding whitespace. + Value D = {}; + auto T = Template("12345 {{! Comment Block! }} 67890"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("12345 67890", Out); +} + +TEST(MustacheComments, VariableNameCollision) { + // Comments must never render, even if a variable with the same name exists. + Value D = Object{ + {"! comment", 1}, {"! comment ", 2}, {"!comment", 3}, {"comment", 4}}; + auto T = Template("comments never show: >{{! comment }}<"); + std::string Out; + raw_string_ostream OS(Out); + T.render(D, OS); + EXPECT_EQ("comments never show: ><", Out); +}