From 74fd1f357543e3097e58e1a1e5ed3992c918402b Mon Sep 17 00:00:00 2001 From: alandefreitas Date: Mon, 10 Jul 2023 14:02:40 -0300 Subject: [PATCH] test: handlebars basic spec --- include/mrdox/Support/Handlebars.hpp | 347 +++++--- src/lib/Support/Handlebars.cpp | 820 +++++++++++------- src/test/lib/Support/Handlebars.cpp | 568 ++++++++++++- src/test_suite/detail/decomposer.hpp | 8 + src/test_suite/diff.cpp | 7 +- src/test_suite/test_suite.cpp | 6 +- src/test_suite/test_suite.hpp | 33 +- test-files/handlebars/features_test.adoc | 833 +++++++++++++++++++ test-files/handlebars/features_test.adoc.hbs | 2 - test-files/handlebars/record.adoc.hbs | 9 + 10 files changed, 2197 insertions(+), 436 deletions(-) create mode 100644 test-files/handlebars/features_test.adoc create mode 100644 test-files/handlebars/record.adoc.hbs diff --git a/include/mrdox/Support/Handlebars.hpp b/include/mrdox/Support/Handlebars.hpp index d65b8640a..b087b033a 100644 --- a/include/mrdox/Support/Handlebars.hpp +++ b/include/mrdox/Support/Handlebars.hpp @@ -18,6 +18,7 @@ #include #include #include +#include namespace clang { namespace mrdox { @@ -79,6 +80,25 @@ namespace detail { struct MRDOX_DECL safeStringWrapper { std::string v_; }; + + struct RenderState; + + // Heterogeneous lookup support + struct string_hash { + using is_transparent = void; + size_t operator()(const char *txt) const { + return std::hash{}(txt); + } + size_t operator()(std::string_view txt) const { + return std::hash{}(txt); + } + size_t operator()(const std::string &txt) const { + return std::hash{}(txt); + } + }; + + using partials_map = std::unordered_map< + std::string, std::string, string_hash, std::equal_to<>>; } /** Reference to output stream used by handlebars @@ -251,6 +271,7 @@ class MRDOX_DECL HandlebarsCallback std::string_view name_; std::vector blockParams_; std::function const* logger_; + detail::RenderState* renderState_; friend class Handlebars; public: @@ -587,6 +608,38 @@ class MRDOX_DECL HandlebarsCallback output() const { return *output_; } + + /** Lookup a property in an object + + Handlebars expressions can also use dot-separated paths to indicate + nested object values. + + @code{.handlebars} + {{person.firstname}} {{person.lastname}} + @endcode + + This expression looks up the `person` property in the input object + and in turn looks up the `firstname` and `lastname` property within + the `person` object. + + Handlebars also supports a `/` syntax so you could write the above + template as: + + @code{.handlebars} + {{person/firstname}} {{person/lastname}} + @endcode + + @param context The object to look up the property in + @param path The path to the property to look up + + @return The value of the property, or nullptr if the property does not exist + @return `true` if the property was defined, `false` otherwise + */ + MRDOX_DECL + std::pair + lookupProperty( + dom::Value const& context, + dom::Value const& path) const; }; /** A handlebars environment @@ -732,23 +785,6 @@ class MRDOX_DECL HandlebarsCallback @see https://handlebarsjs.com/ */ class Handlebars { - // Heterogeneous lookup support - struct string_hash { - using is_transparent = void; - size_t operator()(const char *txt) const { - return std::hash{}(txt); - } - size_t operator()(std::string_view txt) const { - return std::hash{}(txt); - } - size_t operator()(const std::string &txt) const { - return std::hash{}(txt); - } - }; - - using partials_map = std::unordered_map< - std::string, std::string, string_hash, std::equal_to<>>; - enum class HelperBehavior { NO_RENDER, RENDER_RESULT, @@ -760,8 +796,9 @@ class Handlebars { dom::Array const&, HandlebarsCallback const&)>; using helpers_map = std::unordered_map< - std::string, helper_type, string_hash, std::equal_to<>>; + std::string, helper_type, detail::string_hash, std::equal_to<>>; + using partials_map = detail::partials_map; partials_map partials_; helpers_map helpers_; std::function logger_; @@ -798,7 +835,7 @@ class Handlebars { std::string render( std::string_view templateText, - dom::Value const & context, + dom::Value const & context = {}, HandlebarsOptions options = {}) const; /** Render a handlebars template @@ -1058,82 +1095,64 @@ class Handlebars { void render_to( OutputRef& out, - std::string_view templateText, dom::Value const &context, HandlebarsOptions opt, - partials_map& inlinePartials, - dom::Object const& data, - dom::Object const& blockValues) const; + detail::RenderState& state) const; void renderTag( Tag const& tag, OutputRef& out, - std::string_view& templateText, dom::Value const &context, HandlebarsOptions opt, - partials_map& inlinePartials, - dom::Object const& data, - dom::Object const& blockValues) const; + detail::RenderState& state) const; void renderBlock( std::string_view blockName, Handlebars::Tag const &tag, OutputRef &out, - std::string_view &templateText, dom::Value const& context, HandlebarsOptions const& opt, - Handlebars::partials_map &extra_partials, - dom::Object const& data, - dom::Object const& blockValues) const; + detail::RenderState& state) const; void renderPartial( Handlebars::Tag const& tag, OutputRef &out, - std::string_view &templateText, dom::Value const& context, HandlebarsOptions &opt, - Handlebars::partials_map &inlinePartials, - dom::Object const& data, - dom::Object const& blockValues) const; + detail::RenderState& state) const; void renderDecorator( Handlebars::Tag const& tag, OutputRef &out, - std::string_view &templateText, dom::Value const& context, - Handlebars::partials_map &inlinePartials, - dom::Object const& private_data, - dom::Object const& blockValues) const; + detail::RenderState& state) const; void renderExpression( Handlebars::Tag const& tag, OutputRef &out, - std::string_view &templateText, dom::Value const& context, HandlebarsOptions const& opt, - dom::Object const& data, - dom::Object const& blockValues) const; + detail::RenderState& state) const; void setupArgs( std::string_view expression, dom::Value const& context, - dom::Object const& data, - dom::Object const& blockValues, + detail::RenderState& state, dom::Array &args, HandlebarsCallback &options) const; std::pair evalExpr( dom::Value const &context, - dom::Object const &data, - dom::Object const &blockValues, - std::string_view expression) const; + std::string_view expression, + detail::RenderState &state, + bool evalLiterals) const; std::pair getHelper(std::string_view name, bool isBlock) const; @@ -1254,74 +1273,200 @@ escapeExpression( OutputRef out, std::string_view str); -/** Lookup a property in an object +/** Stringify a value as JSON - Handlebars expressions can also use dot-separated paths to indicate - nested object values. + This function converts a dom::Value to a string as if + JSON.stringify() had been called on it. - @code{.handlebars} - {{person.firstname}} {{person.lastname}} - @endcode + Recursive objects are identified. + + @param v The value to stringify + @return The stringified value + */ +MRDOX_DECL +std::string +JSON_stringify(dom::Value const& value); - This expression looks up the `person` property in the input object - and in turn looks up the `firstname` and `lastname` property within - the `person` object. +/** An error thrown or returned by Handlebars - Handlebars also supports a `/` syntax so you could write the above - template as: + An error returned or thrown by Handlebars environment when + an error occurs during template rendering. - @code{.handlebars} - {{person/firstname}} {{person/lastname}} - @endcode + The error message will be the same as the error message + returned by Handlebars.js. + + The object will also contain the line, column and position + of the error in the template. These can be used to by the + caller to provide more detailed error messages. + */ +struct HandlebarsError + : public std::runtime_error +{ + static constexpr std::size_t npos = std::size_t(-1); + std::size_t line = std::size_t(-1); + std::size_t column = std::size_t(-1); + std::size_t pos = std::size_t(-1); + + HandlebarsError(std::string_view msg) + : std::runtime_error(std::string(msg)) {} + + HandlebarsError( + std::string_view msg, + std::size_t line_, + std::size_t column_, + std::size_t pos_) + : std::runtime_error(fmt::format("{} - {}:{}", msg, line_, column_)) + , line(line_) + , column(column_) + , pos(pos_) {} +}; + +/** An expected value or error + + This class is used to return a value or error from a function. - @param context The object to look up the property in - @param path The path to the property to look up + It allows the caller to check if the value is valid or if an + error occurred without having to throw an exception. - @return The value of the property, or nullptr if the property does not exist - @return `true` if the property was defined, `false` otherwise + @tparam T The type of the value */ -MRDOX_DECL -std::pair -lookupProperty( - dom::Value const & context, - std::string_view path); +template +class HandlebarsExpected +{ + std::variant value_; +public: + /** Construct a valid value -/// @copydoc lookupProperty -MRDOX_DECL -std::pair -lookupProperty( - dom::Object const & context, - std::string_view path); + @param value The value + */ + HandlebarsExpected(T const& value) + : value_(value) {} -/// @copydoc lookupProperty -MRDOX_DECL -std::pair -lookupProperty( - dom::Value const& context, - dom::Value const& path); - -/// @copydoc lookupProperty -template S> -std::pair -lookupProperty( - dom::Value const& data, - S const& path) { - return lookupProperty(data, std::string_view(path)); -} + /** Construct a valid value -/** Stringify a value as JSON + @param value The value + */ + HandlebarsExpected(T&& value) + : value_(std::move(value)) {} - This function converts a dom::Value to a string as if - JSON.stringify() had been called on it. + /** Construct an error - Recursive objects are identified. + @param error The error + */ + HandlebarsExpected(HandlebarsError const& error) + : value_(error) {} - @param v The value to stringify - @return The stringified value - */ -MRDOX_DECL -std::string -JSON_stringify(dom::Value const& value); + /** Construct an error + + @param error The error + */ + HandlebarsExpected(HandlebarsError&& error) + : value_(std::move(error)) {} + + /** Check if the value is valid + + @return True if the value is valid, false otherwise + */ + bool + has_value() const noexcept + { + return std::holds_alternative(value_); + } + + /** Check if the value is an error + + @return True if the value is an error, false otherwise + */ + bool + has_error() const noexcept + { + return std::holds_alternative(value_); + } + + /** Get the value + + @return The value + + @throws HandlebarsError if the value is an error + */ + T const& + value() const + { + if (has_error()) + throw std::get(value_); + return std::get(value_); + } + + /// @copydoc value() + T& + value() + { + if (error()) + throw std::get(value_); + return std::get(value_); + } + + /** Get the value + + @return The value + + @throws HandlebarsError if the value is an error + */ + T const& + operator*() const + { + return std::get(value_); + } + + /// @copydoc operator*() const + T& + operator*() + { + return std::get(value_); + } + + /** Get a pointer to the value + + @return The value + + @throws HandlebarsError if the value is an error + */ + T const* + operator->() const + { + return &value(); + } + + /// @copydoc operator->() const + T* + operator->() + { + return &value(); + } + + /** Get the error + + @return The error + + @throws std::logic_error if the value is not an error + */ + HandlebarsError const& + error() const + { + if (has_value()) + throw std::logic_error("value is not an error"); + return std::get(value_); + } + + /// @copydoc error() + HandlebarsError& + error() + { + if (has_value()) + throw std::logic_error("value is not an error"); + return std::get(value_); + } +}; namespace helpers { diff --git a/src/lib/Support/Handlebars.cpp b/src/lib/Support/Handlebars.cpp index f3fabec5d..279c1d9b4 100644 --- a/src/lib/Support/Handlebars.cpp +++ b/src/lib/Support/Handlebars.cpp @@ -23,9 +23,9 @@ namespace clang { namespace mrdox { -///////////////////////////////////////////////////////////////// +// ============================================================== // Utility functions -///////////////////////////////////////////////////////////////// +// ============================================================== bool isTruthy(dom::Value const& arg) @@ -214,8 +214,6 @@ format_to( out << "]"; } else if (value.isObject()) { out << "[object Object]"; - } else if (value.isNull()) { - out << "null"; } } @@ -256,9 +254,20 @@ trim_rspaces(std::string_view expression) return expression; } -///////////////////////////////////////////////////////////////// +// ============================================================== // Helper Callback -///////////////////////////////////////////////////////////////// +// ============================================================== + +namespace detail { + struct RenderState + { + std::string_view templateText0; + std::string_view templateText; + detail::partials_map inlinePartials; + dom::Object data; + dom::Object blockValues; + }; +} std::string HandlebarsCallback:: @@ -344,9 +353,242 @@ log(dom::Value const& level, (*logger_)(level, args); } -///////////////////////////////////////////////////////////////// +bool +isCurrentContextSegment(std::string_view path) +{ + return path == "." || path == "this"; +} + +std::string_view +popFirstSegment(std::string_view& path0) +{ + // Skip dot segments + std::string_view path = path0; + while (path.starts_with("./") || path.starts_with("[.]/") || path.starts_with("[.].")) + { + path.remove_prefix(path.front() == '.' ? 2 : 4); + } + + // All that's left is a dot segment, so there are no more valid paths + if (path == "." || path == "[.]") + { + path0 = {}; + return {}; + } + + // If path starts with "[" the logic changes for literal segments + if (path.starts_with('[')) + { + auto pos = path.find_first_of(']'); + if (pos == std::string_view::npos) + { + // '[' segment was never closed + path0 = {}; + return {}; + } + std::string_view seg = path.substr(0, pos + 1); + path = path.substr(pos + 1); + if (path.empty()) + { + // rest of the path is empty, so this is the last segment + path0 = path; + return seg; + } + if (path.front() != '.' && path.front() != '/') + { + // segment has no valid continuation, so it's invalid + path0 = path; + return {}; + } + path0 = path.substr(1); + return seg; + } + + // Check if we have a literal number segment, in which case the dots + // are part of the segment + if (std::ranges::all_of(path, [](char c) { return c == '.' || std::isdigit(c); })) + { + if (std::ranges::count(path, '.') < 2) + { + // Number segment + path0 = {}; + return path; + } + } + + // If path starts with dotdot segment, the delimiter needs to be a slash + // Otherwise, it can be either a slash or a dot + std::string_view delimiters = path.starts_with("..") ? "/" : "./"; + auto pos = path.find_first_of(delimiters); + // If no delimiter, the whole path is the segment + if (pos == std::string_view::npos) + { + path0 = {}; + return path; + } + // Otherwise, the segment is up to the delimiter + path0 = path.substr(pos + 1); + return path.substr(0, pos); +} + +void +checkPath(std::string_view path0, detail::RenderState const& state) +{ + std::string_view path = path0; + std::string_view seg = popFirstSegment(path); + seg = popFirstSegment(path); + while (!seg.empty()) + { + if (isCurrentContextSegment(seg)) { + std::string msg = + "Invalid path: " + + std::string(path0.substr(0, seg.data() + seg.size() - path0.data())); + std::size_t pos(-1); + std::size_t line(-1); + std::size_t col(-1); + if (path0.data() >= state.templateText0.data() && + path0.data() <= state.templateText0.data() + state.templateText0.size()) + { + pos = path0.data() - state.templateText0.data(); + line = std::ranges::count(state.templateText0.substr(0, pos), '\n') + 1; + if (line == 1) + col = pos; + else + col = pos - state.templateText0.rfind('\n', pos); + throw HandlebarsError(msg, line, col, pos); + } + throw HandlebarsError(msg); + } + seg = popFirstSegment(path); + } +} + +std::pair +lookupPropertyImpl( + dom::Object const& context, + std::string_view path, + detail::RenderState const& state) +{ + // Get first value from Object + std::string_view segment = popFirstSegment(path); + bool isLiteral = segment.starts_with('[') && segment.ends_with(']'); + std::string_view literalSegment = segment.substr(1 * isLiteral, segment.size() - 2 * isLiteral); + dom::Value cur = nullptr; + if (isCurrentContextSegment(segment)) + { + cur = context; + } + else if (!context.exists(literalSegment)) + { + return {nullptr, false}; + } + else + { + cur = context.find(literalSegment); + } + + // Recursively get more values from current value + segment = popFirstSegment(path); + isLiteral = segment.starts_with('[') && segment.ends_with(']'); + literalSegment = segment.substr(1 * isLiteral, segment.size() - 2 * isLiteral); + while (!literalSegment.empty()) + { + // If current value is an Object, get the next value from it + if (cur.isObject()) + { + auto obj = cur.getObject(); + if (obj.exists(literalSegment)) + cur = obj.find(literalSegment); + else + return {nullptr, false}; + } + // If current value is an Array, get the next value the stripped index + else if (cur.isArray()) + { + size_t index; + std::from_chars_result res = std::from_chars( + literalSegment.data(), + literalSegment.data() + literalSegment.size(), + index); + if (res.ec != std::errc()) + return {nullptr, false}; + auto& arr = cur.getArray(); + if (index >= arr.size()) + return {nullptr, false}; + cur = arr.at(index); + } + else + { + // Current value is not an Object or Array, so we can't get any more + // segments from it + return {nullptr, false}; + } + // Consume more segments to get into the array element + segment = popFirstSegment(path); + isLiteral = segment.starts_with('[') && segment.ends_with(']'); + literalSegment = segment.substr(1 * isLiteral, segment.size() - 2 * isLiteral); + } + return {cur, true}; +} + + +std::pair +lookupPropertyImpl( + dom::Value const& context, + std::string_view path, + detail::RenderState const& state) { + checkPath(path, state); + if (isCurrentContextSegment(path) || path.empty()) + return {context, true}; + if (context.kind() != dom::Kind::Object) { + return {nullptr, false}; + } + return lookupPropertyImpl(context.getObject(), path, state); +} + +template S> +std::pair +lookupPropertyImpl( + dom::Value const& data, + S const& path, + detail::RenderState const& state) +{ + return lookupPropertyImpl(data, std::string_view(path), state); +} + +std::pair +lookupPropertyImpl( + dom::Value const& context, + dom::Value const& path, + detail::RenderState const& state) +{ + if (path.isString()) + return lookupPropertyImpl(context, path.getString(), state); + if (path.isInteger()) { + if (context.isArray()) { + auto& arr = context.getArray(); + if (path.getInteger() >= static_cast(arr.size())) + return {nullptr, false}; + return {arr.at(path.getInteger()), true}; + } + return lookupPropertyImpl(context, std::to_string(path.getInteger()), state); + } + return {nullptr, false}; +} + +std::pair +HandlebarsCallback:: +lookupProperty( + dom::Value const& context, + dom::Value const& path) const +{ + return lookupPropertyImpl(context, path, *renderState_); +} + + +// ============================================================== // Engine -///////////////////////////////////////////////////////////////// +// ============================================================== struct defaultLogger { static constexpr std::array methodMap = @@ -420,8 +662,7 @@ render( { std::string out; OutputRef os(out); - render_to( - os, templateText, context, options); + render_to(os, templateText, context, options); return out; } @@ -441,8 +682,10 @@ findTag(std::string_view &tag, std::string_view templateText) // Find closing tag std::string_view closeTagToken = "}}"; + std::string_view closeTagToken2; if (templateText.substr(pos).starts_with("{{!--")) { closeTagToken = "--}}"; + closeTagToken2 = "--~}}"; } else if (templateText.substr(pos).starts_with("{{{{")) { closeTagToken = "}}}}"; } else if (templateText.substr(pos).starts_with("{{{")) { @@ -450,18 +693,34 @@ findTag(std::string_view &tag, std::string_view templateText) } auto end = templateText.find(closeTagToken, pos); if (end == std::string_view::npos) - return false; + { + if (closeTagToken2.empty()) + { + return false; + } + closeTagToken = closeTagToken2; + end = templateText.find(closeTagToken, pos); + if (end == std::string_view::npos) + { + return false; + } + } // Found tag tag = templateText.substr(pos, end - pos + closeTagToken.size()); // Check if tag is escaped verbatim - if (pos != 0 && templateText[pos - 1] == '\\') { - tag = {tag.begin() - 1, tag.end()}; + bool const escaped = pos != 0 && templateText[pos - 1] == '\\'; + if (escaped) + { + bool const doubleEscaped = pos != 1 && templateText[pos - 2] == '\\'; + tag = {tag.begin() - 1 - doubleEscaped, tag.end()}; } return true; } + + struct Handlebars::Tag { std::string_view buffer; @@ -553,6 +812,28 @@ findExpr(std::string_view & expr, std::string_view tagContent) expr = tagContent; return true; } + + // Check if whitespace is not inside a "[*]" path segment + auto openBracketPos = tagContent.substr(0, pos).find_first_of('['); + if (openBracketPos == std::string_view::npos) + { + expr = tagContent.substr(0, pos); + return pos != 0; + } + + // Consume more "[*]" path segments + while (pos != std::string_view::npos) + { + std::string_view subexpr = tagContent.substr(0, pos); + auto obrakets = std::count(subexpr.begin(), subexpr.end(), '['); + auto cbrakets = std::count(subexpr.begin(), subexpr.end(), ']'); + if (obrakets == cbrakets) + { + expr = subexpr; + return true; + } + pos = tagContent.find_first_of(" \t\r\n)", pos + 1); + } expr = tagContent.substr(0, pos); return pos != 0; } @@ -619,6 +900,13 @@ parseTag(std::string_view tagStr) return t; } + // '&' is also used to unescape expressions + if (tagStr.front() == '&') { + t.forceNoHTMLEscape = true; + tagStr.remove_prefix(1); + tagStr = trim_spaces(tagStr); + } + // Find tag type if (tagStr.starts_with('^')) { t.type = '^'; @@ -695,39 +983,38 @@ render_to( dom::Value const & context, HandlebarsOptions options) const { - partials_map inlinePartials; - dom::Object data; - data.set("root", context); - data.set("level", "warn"); - dom::Object blockValues; - render_to( - out, templateText, context, options, - inlinePartials, data, blockValues); + detail::RenderState state; + state.templateText0 = templateText; + state.templateText = templateText; + state.data.set("root", context); + state.data.set("level", "warn"); + render_to(out, context, options, state); } void Handlebars:: render_to( OutputRef& out, - std::string_view templateText, dom::Value const& context, HandlebarsOptions opt, - partials_map& inlinePartials, - dom::Object const& data, - dom::Object const& blockValues) const + detail::RenderState& state) const { - while (!templateText.empty()) { + while (!state.templateText.empty()) { std::string_view tagStr; - if (!findTag(tagStr, templateText)) + if (!findTag(tagStr, state.templateText)) { - out << templateText; + out << state.templateText; break; } - std::size_t tagStartPos = tagStr.data() - templateText.data(); + bool const doubleEscaped = tagStr.starts_with("\\\\"); + if (doubleEscaped) { + tagStr.remove_prefix(2); + } + std::size_t tagStartPos = tagStr.data() - state.templateText.data(); Tag tag = parseTag(tagStr); - std::size_t templateEndPos = tagStartPos; + std::size_t templateEndPos = tagStartPos - doubleEscaped; if (tag.removeLWhitespace) { - std::string_view beforeTag = templateText.substr(0, tagStartPos); + std::string_view beforeTag = state.templateText.substr(0, tagStartPos); auto pos = beforeTag.find_last_not_of(" \t\r\n"); if (pos != std::string_view::npos) { templateEndPos = pos + 1; @@ -735,147 +1022,16 @@ render_to( templateEndPos = 0; } } - out << templateText.substr(0, templateEndPos); - templateText.remove_prefix(tagStartPos + tagStr.size()); + out << state.templateText.substr(0, templateEndPos); + state.templateText.remove_prefix(tagStartPos + tagStr.size()); if (!tag.escaped) { - renderTag( - tag, out, templateText, - context, opt, - inlinePartials, - data, blockValues); + renderTag(tag, out, context, opt, state); } else { - out << tag.content; + out << tag.buffer.substr(1); } } } -std::pair -lookupProperty( - dom::Object const& context, - std::string_view path) -{ - auto nextSegment = [](std::string_view path) - -> std::pair { - // Skip dot segments - while (path.starts_with("./") || path.starts_with("[.]/") || path.starts_with("[.].")) - { - path.remove_prefix(path.front() == '.' ? 2 : 4); - } - // All that's left is a dot segment, so there are no more valid paths - if (path == "." || path == "[.]") - return {{}, {}}; - // If path starts with "[" the logic changes for literal segments - if (path.starts_with('[')) - { - auto pos = path.find_first_of(']'); - if (pos == std::string_view::npos) - { - // '[' segment was never closed - return {{}, {}}; - } - std::string_view seg = path.substr(1, pos - 1); - path = path.substr(pos + 1); - if (path.empty()) - { - // rest of the path is empty, so this is the last segment - return {seg, path}; - } - if (path.front() != '.' && path.front() != '/') - { - // segment has no valid continuation, so it's invalid - return {{}, path}; - } - return { seg, path.substr(1) }; - } - // If path starts with dotdot segment, the delimiter needs to be a slash - // Otherwise, it can be either a slash or a dot - std::string_view delimiters = path.starts_with("..") ? "/" : "./"; - auto pos = path.find_first_of(delimiters); - // If no delimiter, the whole path is the segment - if (pos == std::string_view::npos) - return {path, {}}; - // Otherwise, the segment is up to the delimiter - return {path.substr(0, pos), path.substr(pos + 1)}; - }; - - // Get first value from Object - std::string_view segment; - std::tie(segment, path) = nextSegment(path); - if (!context.exists(segment)) - return {nullptr, false}; - dom::Value cur = context.find(segment); - - // Recursively get more values from current value - std::tie(segment, path) = nextSegment(path); - while (!segment.empty()) - { - // If current value is an Object, get the next value from it - if (cur.isObject()) - { - auto obj = cur.getObject(); - if (obj.exists(segment)) - cur = obj.find(segment); - else - return {nullptr, false}; - } - // If current value is an Array, get the next value the stripped index - else if (cur.isArray()) - { - size_t index; - std::from_chars_result res = std::from_chars( - segment.data(), - segment.data() + segment.size(), - index); - if (res.ec != std::errc()) - return {nullptr, false}; - auto& arr = cur.getArray(); - if (index >= arr.size()) - return {nullptr, false}; - cur = arr.at(index); - } - else - { - // Current value is not an Object or Array, so we can't get any more - // segments from it - return {nullptr, false}; - } - // Consume more segments to get into the array element - std::tie(segment, path) = nextSegment(path); - } - return {cur, true}; -} - -std::pair -lookupProperty( - dom::Value const& context, - std::string_view path) { - if (path == "." || path == "this" || path == "[.]" || path == "[this]" || path.empty()) - return { context, true}; - if (context.kind() != dom::Kind::Object) { - return {nullptr, false}; - } - return lookupProperty(context.getObject(), path); -} - -std::pair -lookupProperty( - dom::Value const& context, - dom::Value const& path) -{ - if (path.isString()) - return lookupProperty(context, path.getString()); - if (path.isInteger()) { - if (context.isArray()) { - auto& arr = context.getArray(); - if (path.getInteger() >= static_cast(arr.size())) - return {nullptr, false}; - return {arr.at(path.getInteger()), true}; - } - return lookupProperty(context, std::to_string(path.getInteger())); - } - return {nullptr, false}; -} - std::string JSON_stringify( dom::Value const& value, @@ -1034,8 +1190,18 @@ unescapeChar(char c) } } -std::string unescapeString(std::string_view str) { +std::string +unescapeString(std::string_view str) { std::string unescapedString; + if (str.empty()) { + return unescapedString; + } + if (str.front() == '\"' || str.front() == '\'') { + str.remove_prefix(1); + } + if (str.back() == '\"' || str.back() == '\'') { + str.remove_suffix(1); + } unescapedString.reserve(str.length()); for (std::size_t i = 0; i < str.length(); ++i) { if (str[i] != '\\') { @@ -1086,63 +1252,66 @@ std::pair Handlebars:: evalExpr( dom::Value const & context, - dom::Object const & data, - dom::Object const & blockValues, - std::string_view expression) const + std::string_view expression, + detail::RenderState& state, + bool evalLiterals) const { expression = trim_spaces(expression); - if (is_literal_value(expression, "true")) - { - return {true, true}; - } - if (is_literal_value(expression, "false")) + if (evalLiterals) { - return {false, true}; - } - if (is_literal_value(expression, "null") || is_literal_value(expression, "undefined") || expression.empty()) - { - return {nullptr, true}; - } - if (expression == "." || expression == "this") - { - return {context, true}; - } - if (is_literal_string(expression)) - { - return {unescapeString(expression.substr(1, expression.size() - 2)), true}; - } - if (is_literal_integer(expression)) - { - std::int64_t value; - auto res = std::from_chars( - expression.data(), - expression.data() + expression.size(), - value); - if (res.ec != std::errc()) - return {std::int64_t(0), true}; - return {value, true}; - } - if (expression.starts_with('(') && expression.ends_with(')')) - { - std::string_view all = expression.substr(1, expression.size() - 2); - std::string_view helper; - findExpr(helper, all); - auto [fn, found] = getHelper(helper, false); - all.remove_prefix(helper.data() + helper.size() - all.data()); - dom::Array args = dom::newArray(); - HandlebarsCallback cb; - cb.name_ = helper; - cb.context_ = &context; - setupArgs(all, context, data, blockValues, args, cb); - return {fn(args, cb).first, true}; + if (is_literal_value(expression, "true")) + { + return {true, true}; + } + if (is_literal_value(expression, "false")) + { + return {false, true}; + } + if (is_literal_value(expression, "null") || is_literal_value(expression, "undefined") || expression.empty()) + { + return {nullptr, true}; + } + if (expression == "." || expression == "this") + { + return {context, true}; + } + if (is_literal_string(expression)) + { + return {unescapeString(expression), true}; + } + if (is_literal_integer(expression)) + { + std::int64_t value; + auto res = std::from_chars( + expression.data(), + expression.data() + expression.size(), + value); + if (res.ec != std::errc()) + return {std::int64_t(0), true}; + return {value, true}; + } + if (expression.starts_with('(') && expression.ends_with(')')) + { + std::string_view all = expression.substr(1, expression.size() - 2); + std::string_view helper; + findExpr(helper, all); + auto [fn, found] = getHelper(helper, false); + all.remove_prefix(helper.data() + helper.size() - all.data()); + dom::Array args = dom::newArray(); + HandlebarsCallback cb; + cb.name_ = helper; + cb.context_ = &context; + setupArgs(all, context, state, args, cb); + return {fn(args, cb).first, true}; + } } if (expression.starts_with('@')) { expression.remove_prefix(1); - return lookupProperty(data, expression); + return lookupPropertyImpl(state.data, expression, state); } if (expression.starts_with("..")) { - auto contextPathV = data.find("contextPath"); + auto contextPathV = state.data.find("contextPath"); if (!contextPathV.isString()) return {nullptr, false}; std::string_view contextPath = contextPathV.getString(); @@ -1158,24 +1327,24 @@ evalExpr( } } std::string absContextPath; - dom::Value root = data.find("root"); + dom::Value root = state.data.find("root"); do { absContextPath = contextPath; absContextPath += '.'; absContextPath += expression; - auto [v, defined] = lookupProperty(root, absContextPath); + auto [v, defined] = lookupPropertyImpl(root, absContextPath, state); if (defined) { return {v, defined}; } popContextSegment(contextPath); } while (!contextPath.empty()); - return lookupProperty(root, expression); + return lookupPropertyImpl(root, expression, state); } - auto [cv, defined] = lookupProperty(context, expression); + auto [cv, defined] = lookupPropertyImpl(context, expression, state); if (defined) { return {cv, defined}; } - return lookupProperty(blockValues, expression); + return lookupPropertyImpl(state.blockValues, expression, state); } auto @@ -1315,31 +1484,28 @@ Handlebars:: renderTag( Tag const& tag, OutputRef& out, - std::string_view& templateText, dom::Value const& context, HandlebarsOptions opt, - partials_map& inlinePartials, - dom::Object const& data, - dom::Object const& blockValues) const { + detail::RenderState& state) const { if (tag.type == '#') { - renderBlock(tag.helper, tag, out, templateText, - context, opt, inlinePartials, data, blockValues); + renderBlock(tag.helper, tag, out, context, opt, state); } else if (tag.type == '>') { - renderPartial(tag, out, templateText, context, - opt, inlinePartials, data, blockValues); + renderPartial(tag, out, context, opt, state); } else if (tag.type == '*') { - renderDecorator(tag, out, templateText, context, - inlinePartials, data, blockValues); + renderDecorator(tag, out, context, state); } else if (tag.type != '/' && tag.type != '!') { - renderExpression(tag, out, templateText, - context, opt, data, blockValues); + renderExpression(tag, out, context, opt, state); + } + if (tag.removeRWhitespace) + { + state.templateText = trim_lspaces(state.templateText); } } @@ -1348,11 +1514,9 @@ Handlebars:: renderExpression( Handlebars::Tag const &tag, OutputRef &out, - std::string_view &templateText, dom::Value const & context, HandlebarsOptions const &opt, - dom::Object const& data, - dom::Object const& blockValues) const + detail::RenderState& state) const { if (tag.helper.empty()) return; @@ -1360,6 +1524,7 @@ renderExpression( auto opt2 = opt; opt2.noEscape = tag.forceNoHTMLEscape || opt.noEscape; + // Evaluate helper as function auto it = helpers_.find(tag.helper); if (it != helpers_.end()) { auto [fn, found] = getHelper(tag.helper, false); @@ -1367,9 +1532,9 @@ renderExpression( HandlebarsCallback cb; cb.name_ = tag.helper; cb.context_ = &context; - cb.data_ = &data; + cb.data_ = &state.data; cb.logger_ = &logger_; - setupArgs(tag.arguments, context, data, blockValues, args, cb); + setupArgs(tag.arguments, context, state, args, cb); auto [res, render] = fn(args, cb); if (render == HelperBehavior::RENDER_RESULT) { format_to(out, res, opt2); @@ -1378,12 +1543,20 @@ renderExpression( format_to(out, res, opt2); } if (tag.removeRWhitespace) { - templateText = trim_lspaces(templateText); + state.templateText = trim_lspaces(state.templateText); } return; } - auto [v, defined] = evalExpr(context, data, blockValues, tag.helper); + // Evaluate helper as expression + std::string_view helper_expr = tag.helper; + std::string unescaped; + if (is_literal_string(tag.helper)) + { + unescaped = unescapeString(helper_expr); + helper_expr = unescaped; + } + auto [v, defined] = evalExpr(context, helper_expr, state, false); if (defined) { format_to(out, v, opt2); @@ -1391,11 +1564,11 @@ renderExpression( } // Let it decay to helperMissing hook - auto [fn, found] = getHelper(tag.helper, false); + auto [fn, found] = getHelper(helper_expr, false); dom::Array args = dom::newArray(); HandlebarsCallback cb; - cb.name_ = tag.helper; - setupArgs(tag.arguments, context, data, blockValues, args, cb); + cb.name_ = helper_expr; + setupArgs(tag.arguments, context, state, args, cb); auto [res, render] = fn(args, cb); if (render == HelperBehavior::RENDER_RESULT) { format_to(out, res, opt2); @@ -1404,7 +1577,7 @@ renderExpression( format_to(out, res, opt2); } if (tag.removeRWhitespace) { - templateText = trim_lspaces(templateText); + state.templateText = trim_lspaces(state.templateText); } } @@ -1413,8 +1586,7 @@ Handlebars:: setupArgs( std::string_view expression, dom::Value const& context, - dom::Object const& data, - dom::Object const& blockValues, + detail::RenderState & state, dom::Array &args, HandlebarsCallback &cb) const { @@ -1425,14 +1597,15 @@ setupArgs( auto [k, v] = findKeyValuePair(expr); if (k.empty()) { - args.emplace_back(evalExpr(context, data, blockValues, expr).first); + args.emplace_back(evalExpr(context, expr, state, true).first); cb.ids_.push_back(expr); } else { - cb.hashes_.set(k, evalExpr(context, data, blockValues, v).first); + cb.hashes_.set(k, evalExpr(context, v, state, true).first); } } + cb.renderState_ = &state; } void @@ -1440,11 +1613,8 @@ Handlebars:: renderDecorator( Handlebars::Tag const& tag, OutputRef &out, - std::string_view &templateText, dom::Value const& context, - Handlebars::partials_map &inlinePartials, - dom::Object const& data, - dom::Object const& blockValues) const { + detail::RenderState& state) const { // Validate decorator if (tag.helper != "inline") { @@ -1455,7 +1625,7 @@ renderDecorator( // Evaluate expression std::string_view expr; findExpr(expr, tag.arguments); - auto [value, defined] = evalExpr(context, data, blockValues, expr); + auto [value, defined] = evalExpr(context, expr, state, true); if (!value.isString()) { out << fmt::format(R"([invalid decorator expression "{}" in "{}"])", tag.arguments, tag.buffer); @@ -1468,12 +1638,12 @@ renderDecorator( std::string_view inverseBlock; Tag inverseTag; if (tag.type2 == '#') { - if (!parseBlock(tag.helper, tag, templateText, out, fnBlock, inverseBlock, inverseTag)) { + if (!parseBlock(tag.helper, tag, state.templateText, out, fnBlock, inverseBlock, inverseTag)) { return; } } fnBlock = trim_rspaces(fnBlock); - inlinePartials[std::string(partial_name)] = std::string(fnBlock); + state.inlinePartials[std::string(partial_name)] = std::string(fnBlock); } void @@ -1481,18 +1651,15 @@ Handlebars:: renderPartial( Handlebars::Tag const &tag, OutputRef &out, - std::string_view &templateText, dom::Value const &context, HandlebarsOptions &opt, - Handlebars::partials_map &inlinePartials, - dom::Object const& data, - dom::Object const& blockValues) const { + detail::RenderState& state) const { // Evaluate dynamic partial std::string helper(tag.helper); if (helper.starts_with('(')) { std::string_view expr; findExpr(expr, helper); - auto [value, defined] = evalExpr(context, data, blockValues, expr); + auto [value, defined] = evalExpr(context, expr, state, true); if (value.isString()) { helper = value.getString(); } @@ -1503,7 +1670,7 @@ renderPartial( std::string_view inverseBlock; Tag inverseTag; if (tag.type2 == '#') { - if (!parseBlock(tag.helper, tag, templateText, out, fnBlock, inverseBlock, inverseTag)) { + if (!parseBlock(tag.helper, tag, state.templateText, out, fnBlock, inverseBlock, inverseTag)) { return; } } @@ -1517,8 +1684,8 @@ renderPartial( } else { - it = inlinePartials.find(helper); - if (it == inlinePartials.end()) { + it = state.inlinePartials.find(helper); + if (it == state.inlinePartials.end()) { if (tag.type2 == '#') { partial_content = fnBlock; } else { @@ -1533,24 +1700,21 @@ renderPartial( if (tag.arguments.empty()) { if (tag.type2 == '#') { - // evaluate fnBlock to populate extra partials + // evaluate fnBlock to potentially populate extra partials OutputRef dumb{}; - this->render_to( - dumb, fnBlock, - context, opt, - inlinePartials, - data, blockValues); - // also add @partial-block to extra partials - inlinePartials["@partial-block"] = std::string(fnBlock); + std::string_view templateText = state.templateText; + state.templateText = fnBlock; + this->render_to(dumb, context, opt, state); + state.templateText = templateText; + state.inlinePartials["@partial-block"] = std::string(fnBlock); } // Render partial with current context - this->render_to( - out, partial_content, - context, opt, - inlinePartials, - data, blockValues); + std::string_view templateText = state.templateText; + state.templateText = partial_content; + this->render_to(out, context, opt, state); + state.templateText = templateText; if (tag.type2 == '#') { - inlinePartials.erase("@partial-block"); + state.inlinePartials.erase("@partial-block"); } } else @@ -1565,7 +1729,7 @@ renderPartial( auto [partialKey, contextKey] = findKeyValuePair(expr); if (partialKey.empty()) { - auto [value, defined] = evalExpr(context, data, blockValues, expr); + auto [value, defined] = evalExpr(context, expr, state, true); if (defined && value.isObject()) { partialCtx = createFrame(value.getObject()); @@ -1573,7 +1737,7 @@ renderPartial( continue; } if (contextKey != ".") { - auto [value, defined] = evalExpr(context, data, blockValues, contextKey); + auto [value, defined] = evalExpr(context, contextKey, state, true); if (defined) { partialCtx.set(partialKey, value); @@ -1582,14 +1746,14 @@ renderPartial( partialCtx.set(partialKey, context); } } - this->render_to( - out, partial_content, partialCtx, opt, - inlinePartials, - data, blockValues); + std::string_view templateText = state.templateText; + state.templateText = partial_content; + this->render_to(out, partialCtx, opt, state); + state.templateText = templateText; } if (tag.removeRWhitespace) { - templateText = trim_lspaces(templateText); + state.templateText = trim_lspaces(state.templateText); } } @@ -1599,22 +1763,19 @@ renderBlock( std::string_view blockName, Handlebars::Tag const &tag, OutputRef &out, - std::string_view &templateText, dom::Value const& context, HandlebarsOptions const& opt, - Handlebars::partials_map &inlinePartials, - dom::Object const & data, - dom::Object const &blockValues) const { + detail::RenderState& state) const { // Opening a section tag // Find closing tag if (tag.removeRWhitespace) { - templateText = trim_lspaces(templateText); + state.templateText = trim_lspaces(state.templateText); } std::string_view fnBlock; std::string_view inverseBlock; Tag inverseTag; - if (!parseBlock(blockName, tag, templateText, out, fnBlock, inverseBlock, inverseTag)) { + if (!parseBlock(blockName, tag, state.templateText, out, fnBlock, inverseBlock, inverseTag)) { return; } @@ -1622,17 +1783,26 @@ renderBlock( HandlebarsCallback cb; cb.name_ = tag.helper; cb.context_ = &context; - cb.data_ = &data; + cb.data_ = &state.data; cb.logger_ = &logger_; cb.output_ = &out; bool const isNoArgBlock = tag.arguments.empty(); auto [fn, found] = getHelper(tag.helper, isNoArgBlock); std::string_view arguments = tag.arguments; bool const emulateMustache = !found && isNoArgBlock; + std::string unescaped; if (emulateMustache) { - arguments = tag.helper; + if (is_literal_string(tag.helper)) + { + unescaped = unescapeString(tag.helper); + arguments = unescaped; + } + else + { + arguments = tag.helper; + } } - setupArgs(arguments, context, data, blockValues, args, cb); + setupArgs(arguments, context, state, args, cb); // Setup block params std::string_view expr; @@ -1645,41 +1815,62 @@ renderBlock( // Setup callback functions if (!tag.rawBlock) { - cb.fn_ = [this, fnBlock, opt, &inlinePartials, &blockValues]( + cb.fn_ = [this, fnBlock, opt, &state]( OutputRef os, dom::Value const &item, dom::Object const &data, - dom::Object const &newBlockValues) -> void { + dom::Object const &newBlockValues) -> void + { + std::string_view templateText = state.templateText; + state.templateText = fnBlock; + dom::Object state_data = state.data; + state.data = dom::Object(data); if (!newBlockValues.empty()) { dom::Object blockValuesOverlay = - createFrame(newBlockValues, blockValues); - this->render_to(os, fnBlock, item, opt, inlinePartials, data, blockValuesOverlay); + createFrame(newBlockValues, state.blockValues); + dom::Object blockValues = state.blockValues; + state.blockValues = std::move(blockValuesOverlay); + render_to(os, item, opt, state); + state.blockValues = std::move(blockValues); } else { - this->render_to(os, fnBlock, item, opt, inlinePartials, data, blockValues); + render_to(os, item, opt, state); } + state.templateText = templateText; + state.data = std::move(state_data); }; cb.inverse_ = - [this, inverseTag, inverseBlock, opt, &inlinePartials, blockName, &blockValues]( + [this, inverseTag, inverseBlock, opt, blockName, &state]( OutputRef os, dom::Value const &item, dom::Object const &data, - dom::Object const &newBlockValues) -> void { + dom::Object const &newBlockValues) -> void + { + std::string_view templateText = state.templateText; + state.templateText = inverseBlock; + dom::Object state_data = state.data; + state.data = dom::Object(data); if (inverseTag.helper.empty()) { // Inverse tag does not contain its own helper // i.e. {{#helper}}...{{^}}...{{/helper}} instead of // {{#helper}}...{{^helper2}}...{{/helper}} // Render the inverse block with the specified context - render_to(os, inverseBlock, item, opt, inlinePartials, data, blockValues); - return; + render_to(os, item, opt, state); } - std::string_view inverseText = inverseBlock; - if (!newBlockValues.empty()) { - dom::Object blockValuesOverlay = - createFrame(newBlockValues, blockValues); - renderBlock(blockName, inverseTag, os, inverseText, item, opt, inlinePartials, data, blockValuesOverlay); - } else { - renderBlock(blockName, inverseTag, os, inverseText, item, opt, inlinePartials, data, blockValues); + else + { + if (!newBlockValues.empty()) { + dom::Object blockValuesOverlay = + createFrame(newBlockValues, state.blockValues); + dom::Object blockValues = state.blockValues; + state.blockValues = std::move(blockValuesOverlay); + renderBlock(blockName, inverseTag, os, item, opt, state); + state.blockValues = std::move(blockValues); + } else { + renderBlock(blockName, inverseTag, os, item, opt, state); + } } + state.templateText = templateText; + state.data = std::move(state_data); }; } else { cb.fn_ = [fnBlock]( @@ -2087,11 +2278,11 @@ relativize_fn( } if (from.empty() && ctx.isObject()) { - auto [v, defined] = lookupProperty(ctx, "data.root.page"); + auto [v, defined] = options.lookupProperty(ctx, "data.root.page"); if (v.isString()) { from = v.getString(); } else { - std::tie(v, defined) = lookupProperty(ctx, "data.root.site.path"); + std::tie(v, defined) = options.lookupProperty(ctx, "data.root.site.path"); if (v.isString()) { std::string fromStr(v.getString()); fromStr += to; @@ -2319,7 +2510,7 @@ lookup_fn( if (!isTruthy(obj)) { return obj; } - return lookupProperty(obj, field).first; + return options.lookupProperty(obj, field).first; } void @@ -3616,7 +3807,8 @@ registerContainerHelpers(Handlebars& hbs) static constexpr auto keys_fn = []( dom::Array const& args, HandlebarsCallback const& options) { - auto const& obj = args.at(0).getObject(); + auto container = args.at(0); + auto const& obj = container.getObject(); dom::Array res; for (auto const& [key, _]: obj) { @@ -3706,9 +3898,9 @@ registerContainerHelpers(Handlebars& hbs) auto container = args.at(0); auto item = args.at(1); if (container.isObject()) { - dom::Value objV = container; + dom::Value const& objV = container; auto const& obj = objV.getObject(); - dom::Value keysV = item; + dom::Value const& keysV = item; auto const& keys = keysV.getArray(); for (std::int64_t i = 0; i < static_cast(keys.size()); ++i) { dom::Value k = keys[i]; diff --git a/src/test/lib/Support/Handlebars.cpp b/src/test/lib/Support/Handlebars.cpp index b3d7bde27..a37d517b9 100644 --- a/src/test/lib/Support/Handlebars.cpp +++ b/src/test/lib/Support/Handlebars.cpp @@ -8,7 +8,6 @@ #include #include #include -#include #include #include #include @@ -67,6 +66,7 @@ setup_fixtures() MRDOX_TEST_FILES_DIR "/handlebars/features_test.adoc.hbs"; master.partial_paths = { MRDOX_TEST_FILES_DIR "/handlebars/record-detail.adoc.hbs", + MRDOX_TEST_FILES_DIR "/handlebars/record.adoc.hbs", MRDOX_TEST_FILES_DIR "/handlebars/escaped.adoc.hbs"}; master.output_path = MRDOX_TEST_FILES_DIR "/handlebars/features_test.adoc"; @@ -101,7 +101,7 @@ setup_fixtures() } void -setup_context() +setup_context() const { dom::Object page; page.set("kind", "record"); @@ -299,8 +299,13 @@ setup_context() account_x12.set("product", "Desk"); object_array.emplace_back(account_x12); containers.set("object_array", object_array); - master.context.set("containers", containers); + + dom::Object symbol; + symbol.set("tag", "struct"); + symbol.set("kind", "record"); + symbol.set("name", "T"); + master.context.set("symbol", symbol); } void @@ -644,10 +649,565 @@ safe_string() BOOST_TEST_NOT(res == "<b>text</b>"); } -void run() +void +basic_context() +{ + // https://github.com/handlebars-lang/handlebars.js/blob/4.x/spec/basic.js + Handlebars hbs; + + // most basic + { + dom::Object ctx; + ctx.set("foo", "foo"); + BOOST_TEST(hbs.render("{{foo}}", ctx) == "foo"); + } + + // escaping + { + dom::Object ctx; + ctx.set("foo", "food"); + BOOST_TEST(hbs.render("\\{{foo}}", ctx) == "{{foo}}"); + BOOST_TEST(hbs.render("content \\{{foo}}", ctx) == "content {{foo}}"); + BOOST_TEST(hbs.render("\\\\{{foo}}", ctx) == "\\food"); + BOOST_TEST(hbs.render("\\\\{{foo}}", ctx) == "\\food"); + BOOST_TEST(hbs.render("content \\\\{{foo}}", ctx) == "content \\food"); + BOOST_TEST(hbs.render("\\\\ {{foo}}", ctx) == "\\\\ food"); + } + + // compiling with a basic context + { + dom::Object ctx; + ctx.set("cruel", "cruel"); + ctx.set("world", "world"); + BOOST_TEST(hbs.render("Goodbye\n{{cruel}}\n{{world}}!", ctx) == "Goodbye\ncruel\nworld!"); + } + + // compiling with an undefined context + { + dom::Value ctx = nullptr; + BOOST_TEST(hbs.render("Goodbye\n{{cruel}}\n{{world.bar}}!", ctx) == "Goodbye\n\n!"); + BOOST_TEST(hbs.render("{{#unless foo}}Goodbye{{../test}}{{test2}}{{/unless}}", ctx) == "Goodbye"); + } + + // comments + { + dom::Object ctx; + ctx.set("cruel", "cruel"); + ctx.set("world", "world"); + BOOST_TEST(hbs.render("{{! Goodbye}}Goodbye\\n{{cruel}}\\n{{world}}!", ctx) == "Goodbye\\ncruel\\nworld!"); + BOOST_TEST(hbs.render(" {{~! comment ~}} blah", ctx) == "blah"); + BOOST_TEST(hbs.render(" {{~!-- long-comment --~}} blah", ctx) == "blah"); + BOOST_TEST(hbs.render(" {{! comment ~}} blah", ctx) == " blah"); + BOOST_TEST(hbs.render(" {{!-- long-comment --~}} blah", ctx) == " blah"); + BOOST_TEST(hbs.render(" {{~! comment}} blah", ctx) == " blah"); + BOOST_TEST(hbs.render(" {{~!-- long-comment --}} blah", ctx) == " blah"); + } + + // boolean + { + std::string string = "{{#goodbye}}GOODBYE {{/goodbye}}cruel {{world}}!"; + dom::Object ctx; + ctx.set("goodbye", true); + ctx.set("world", "world"); + // booleans show the contents when true + BOOST_TEST(hbs.render(string, ctx) == "GOODBYE cruel world!"); + ctx.set("goodbye", false); + // booleans do not show the contents when false + BOOST_TEST(hbs.render(string, ctx) == "cruel world!"); + } + + // zeros + { + dom::Object ctx; + ctx.set("num1", 42); + ctx.set("num2", std::int64_t(0)); + BOOST_TEST(hbs.render("num1: {{num1}}, num2: {{num2}}", ctx) == "num1: 42, num2: 0"); + BOOST_TEST(hbs.render("num: {{.}}", std::int64_t(0)) == "num: 0"); + ctx = dom::Object(); + dom::Object num1; + num1.set("num2", std::int64_t(0)); + ctx.set("num1", num1); + BOOST_TEST(hbs.render("num: {{num1/num2}}", ctx) == "num: 0"); + } + + // false + { + dom::Object ctx; + ctx.set("val1", false); + ctx.set("val2", false); + BOOST_TEST(hbs.render("val1: {{val1}}, val2: {{val2}}", ctx) == "val1: false, val2: false"); + BOOST_TEST(hbs.render("val: {{.}}", false) == "val: false"); + ctx = dom::Object(); + dom::Object val1; + val1.set("val2", false); + ctx.set("val1", val1); + BOOST_TEST(hbs.render("val: {{val1/val2}}", ctx) == "val: false"); + ctx = dom::Object(); + ctx.set("val1", false); + ctx.set("val2", false); + BOOST_TEST(hbs.render("val1: {{{val1}}}, val2: {{{val2}}}", ctx) == "val1: false, val2: false"); + ctx = dom::Object(); + val1.set("val2", false); + ctx.set("val1", val1); + BOOST_TEST(hbs.render("val: {{{val1/val2}}}", ctx) == "val: false"); + } + + // should handle undefined and null + { + { + hbs.registerHelper("awesome", []( + dom::Array const& args, HandlebarsCallback const& cb) { + BOOST_TEST(args.size() == 2u); + std::string result; + if (args[0].isNull()) { + result += "true "; + } + if (args[1].isNull()) { + result += "true"; + } + return result; + }); + dom::Object ctx; + BOOST_TEST(hbs.render("{{awesome undefined null}}", nullptr) == "true true"); + hbs.unregisterHelper("awesome"); + } + { + hbs.registerHelper("undefined", []( + dom::Array const& args, HandlebarsCallback const& cb) { + BOOST_TEST(args.empty()); + return "undefined!"; + }); + dom::Object ctx; + BOOST_TEST(hbs.render("{{undefined}}", nullptr) == "undefined!"); + hbs.unregisterHelper("undefined"); + } + { + hbs.registerHelper("null", []( + dom::Array const& args, HandlebarsCallback const& cb) { + BOOST_TEST(args.empty()); + return "null!"; + }); + dom::Object ctx; + BOOST_TEST(hbs.render("{{null}}", nullptr) == "null!"); + hbs.unregisterHelper("null"); + } + } + + // newlines + { + BOOST_TEST(hbs.render("Alan's\nTest") == "Alan's\nTest"); + BOOST_TEST(hbs.render("Alan's\rTest") == "Alan's\rTest"); + } + + // escaping text + { + BOOST_TEST(hbs.render("Awesome's") == "Awesome's"); + BOOST_TEST(hbs.render("Awesome\\") == "Awesome\\"); + BOOST_TEST(hbs.render("Awesome\\\\ foo") == "Awesome\\\\ foo"); + dom::Object ctx; + ctx.set("foo", "\\"); + BOOST_TEST(hbs.render("Awesome {{foo}}", ctx) == "Awesome \\"); + BOOST_TEST(hbs.render(" ' ' ") == " ' ' "); + } + + // escaping expressions + { + dom::Object ctx; + ctx.set("awesome", "&'\\<>"); + // expressions with 3 handlebars aren't escaped + BOOST_TEST(hbs.render("{{{awesome}}}", ctx) == "&'\\<>"); + // expressions with {{& handlebars aren't escaped + BOOST_TEST(hbs.render("{{&awesome}}", ctx) == "&'\\<>"); + // by default expressions should be escaped + ctx.set("awesome", R"(&"'`\<>)"); + BOOST_TEST(hbs.render("{{awesome}}", ctx) == "&"'`\\<>"); + // escaping should properly handle amperstands + ctx.set("awesome", "Escaped, looks like: <b>"); + BOOST_TEST(hbs.render("{{awesome}}", ctx) == "Escaped, <b> looks like: &lt;b&gt;"); + } + + // functions returning safestrings shouldn't be escaped + { + hbs.registerHelper("awesome", []( + dom::Array const& args, HandlebarsCallback const& cb) { + return safeString("&'\\<>"); + }); + BOOST_TEST(hbs.render("{{awesome}}") == "&'\\<>"); + hbs.unregisterHelper("awesome"); + } + + // functions + { + hbs.registerHelper("awesome", []( + dom::Array const& args, HandlebarsCallback const& cb) { + return "Awesome"; + }); + BOOST_TEST(hbs.render("{{awesome}}") == "Awesome"); + hbs.unregisterHelper("awesome"); + + hbs.registerHelper("awesome", []( + dom::Array const& args, HandlebarsCallback const& cb) { + return cb.context().getObject().find("more"); + }); + dom::Object ctx; + ctx.set("more", "More awesome"); + BOOST_TEST(hbs.render("{{awesome}}", ctx) == "More awesome"); + hbs.unregisterHelper("awesome"); + } + + // functions with context argument + { + hbs.registerHelper("awesome", [](dom::Array const& args) { + return args[0]; + }); + dom::Object ctx; + ctx.set("frank", "Frank"); + BOOST_TEST(hbs.render("{{awesome frank}}", ctx) == "Frank"); + hbs.unregisterHelper("awesome"); + } + + // pathed functions with context argument + { + // This test uses helpers in C++. The Handlebars.js test uses a + // function in the "bar.awesome" context that is accessed as + // "bar/awesome" from the parent context. + hbs.registerHelper("awesome", [](dom::Array const& args) { + return args[0]; + }); + dom::Object ctx; + ctx.set("frank", "Frank"); + BOOST_TEST(hbs.render("{{awesome frank}}", ctx) == "Frank"); + } + + // depthed functions with context argument + { + // This test uses helpers in C++. The Handlebars.js test uses a + // function in the context that is accessed as "../awesome" from + // the "frank" context. + hbs.registerHelper("awesome", [](dom::Array const& args) { + return args[0]; + }); + dom::Object ctx; + ctx.set("frank", "Frank"); + BOOST_TEST(hbs.render("{{#with frank}}{{awesome .}}{{/with}}", ctx) == "Frank"); + } + + // block functions with context argument + { + hbs.registerHelper("awesome", []( + dom::Array const& args, HandlebarsCallback const& cb) { + return cb.fn(args[0]); + }); + BOOST_TEST(hbs.render("{{#awesome 1}}inner {{.}}{{/awesome}}") == "inner 1"); + } + + // depthed block functions with context argument + { + // This test uses helpers in C++. The Handlebars.js test uses a + // function in the context that is accessed as "../awesome" from + // the "value" context. + hbs.registerHelper("awesome", []( + dom::Array const& args, HandlebarsCallback const& cb) { + return cb.fn(args[0]); + }); + dom::Object ctx; + ctx.set("value", true); + BOOST_TEST(hbs.render("{{#with value}}{{#awesome 1}}inner {{.}}{{/awesome}}{{/with}}", ctx) == "inner 1"); + } + + // block functions without context argument + { + // block functions are called with options + hbs.registerHelper("awesome", []( + dom::Array const&, HandlebarsCallback const& cb) { + return cb.fn(); + }); + BOOST_TEST(hbs.render("{{#awesome}}inner{{/awesome}}") == "inner"); + } + + // pathed block functions without context argument + { + // This test uses helpers in C++. The Handlebars.js test uses a + // function in the "foo.awesome" context that is accessed as + // "/foo/awesome" from the root context. + hbs.registerHelper("awesome", []( + dom::Array const&, HandlebarsCallback const& cb) { + return cb.fn(); + }); + BOOST_TEST(hbs.render("{{#awesome}}inner{{/awesome}}") == "inner"); + } + + // depthed block functions without context argument + { + // This test uses helpers in C++. The Handlebars.js test uses a + // function "awesome" in the root context that is accessed as + // "../awesome" from the "value" context. + hbs.registerHelper("awesome", []( + dom::Array const&, HandlebarsCallback const& cb) { + return cb.fn(); + }); + dom::Object ctx; + ctx.set("value", true); + BOOST_TEST(hbs.render("{{#with value}}{{#awesome}}inner{{/awesome}}{{/with}}", ctx) == "inner"); + } + + // paths with hyphens + { + dom::Object foo; + foo.set("foo-bar", "baz"); + BOOST_TEST(hbs.render("{{foo-bar}}", foo) == "baz"); + + dom::Object ctx; + ctx.set("foo", foo); + BOOST_TEST(hbs.render("{{foo.foo-bar}}", ctx) == "baz"); + BOOST_TEST(hbs.render("{{foo/foo-bar}}", ctx) == "baz"); + } + + // nested paths + { + dom::Object alan; + alan.set("expression", "beautiful"); + dom::Object ctx; + ctx.set("alan", alan); + BOOST_TEST(hbs.render("Goodbye {{alan/expression}} world!", ctx) == "Goodbye beautiful world!"); + } + + // nested paths with empty string value + { + dom::Object alan; + alan.set("expression", ""); + dom::Object ctx; + ctx.set("alan", alan); + BOOST_TEST(hbs.render("Goodbye {{alan/expression}} world!", ctx) == "Goodbye world!"); + } + + // literal paths + { + dom::Object alan; + alan.set("expression", "beautiful"); + dom::Object ctx; + ctx.set("@alan", alan); + BOOST_TEST(hbs.render("Goodbye {{[@alan]/expression}} world!", ctx) == "Goodbye beautiful world!"); + + ctx = dom::Object(); + ctx.set("foo bar", alan); + BOOST_TEST(hbs.render("Goodbye {{[foo bar]/expression}} world!", ctx) == "Goodbye beautiful world!"); + } + + // literal references + { + dom::Object ctx; + ctx.set("foo bar", "beautiful"); + ctx.set("foo'bar", "beautiful"); + ctx.set("foo\"bar", "beautiful"); + ctx.set("foo[bar", "beautiful"); + BOOST_TEST(hbs.render("Goodbye {{[foo bar]}} world!", ctx) == "Goodbye beautiful world!"); + BOOST_TEST(hbs.render("Goodbye {{\"foo bar\"}} world!", ctx) == "Goodbye beautiful world!"); + BOOST_TEST(hbs.render("Goodbye {{'foo bar'}} world!", ctx) == "Goodbye beautiful world!"); + BOOST_TEST(hbs.render("Goodbye {{\"foo[bar\"}} world!", ctx) == "Goodbye beautiful world!"); + BOOST_TEST(hbs.render("Goodbye {{\"foo'bar\"}} world!", ctx) == "Goodbye beautiful world!"); + BOOST_TEST(hbs.render("Goodbye {{'foo\"bar'}} world!", ctx) == "Goodbye beautiful world!"); + } + + // that current context path ({{.}}) doesn't hit helpers + { + hbs.registerHelper("awesome", []( + dom::Array const&, HandlebarsCallback const& cb) { + return cb.fn(); + }); + BOOST_TEST(hbs.render("test: {{.}}", nullptr) == "test: "); + } + + // complex but empty paths + { + dom::Object person; + person.set("name", nullptr); + dom::Object ctx; + ctx.set("person", person); + BOOST_TEST(hbs.render("{{person/name}}", ctx).empty()); + + ctx = dom::Object(); + ctx.set("person", dom::Object()); + BOOST_TEST(hbs.render("{{person/name}}", ctx).empty()); + } + + // this keyword in paths + { + dom::Object ctx; + dom::Array goodbyes; + goodbyes.emplace_back("goodbye"); + goodbyes.emplace_back("Goodbye"); + goodbyes.emplace_back("GOODBYE"); + ctx.set("goodbyes", goodbyes); + BOOST_TEST(hbs.render("{{#goodbyes}}{{this}}{{/goodbyes}}", ctx) == "goodbyeGoodbyeGOODBYE"); + + dom::Array hellos; + dom::Object hello1; + hello1.set("text", "hello"); + hellos.emplace_back(hello1); + dom::Object hello2; + hello2.set("text", "Hello"); + hellos.emplace_back(hello2); + dom::Object hello3; + hello3.set("text", "HELLO"); + hellos.emplace_back(hello3); + ctx.set("hellos", hellos); + BOOST_TEST(hbs.render("{{#hellos}}{{this/text}}{{/hellos}}", ctx) == "helloHelloHELLO"); + } + + // this keyword nested inside path + { + BOOST_TEST_THROW_WITH( + hbs.render("{{text/this/foo}}"), + HandlebarsError, "Invalid path: text/this - 1:2"); + dom::Object ctx; + dom::Array hellos; + dom::Object hello1; + hello1.set("text", "hello"); + hellos.emplace_back(hello1); + ctx.set("hellos", hellos); + hbs.registerHelper("foo", [](dom::Array const& args) { + return args[0]; + }); + BOOST_TEST_THROW_WITH( + hbs.render("{{#hellos}}{{foo text/this/foo}}{{/hellos}}", ctx), + HandlebarsError, "Invalid path: text/this - 1:17"); + ctx.set("this", "bar"); + BOOST_TEST(hbs.render("{{foo [this]}}", ctx) == "bar"); + dom::Object this_obj; + this_obj.set("this", "bar"); + ctx.set("text", this_obj); + BOOST_TEST(hbs.render("{{foo text/[this]}}", ctx) == "bar"); + } + + // this keyword in helpers + { + hbs.registerHelper("foo", [](dom::Array const& args) { + std::string res = "bar "; + res += args[0].getString(); + return res; + }); + + // This keyword in paths evaluates to current context + dom::Object ctx; + dom::Array goodbyes; + goodbyes.emplace_back("goodbye"); + goodbyes.emplace_back("Goodbye"); + goodbyes.emplace_back("GOODBYE"); + ctx.set("goodbyes", goodbyes); + BOOST_TEST(hbs.render("{{#goodbyes}}{{foo this}}{{/goodbyes}}", ctx) == "bar goodbyebar Goodbyebar GOODBYE"); + + // This keyword evaluates in more complex paths + dom::Array hellos; + dom::Object hello1; + hello1.set("text", "hello"); + hellos.emplace_back(hello1); + dom::Object hello2; + hello2.set("text", "Hello"); + hellos.emplace_back(hello2); + dom::Object hello3; + hello3.set("text", "HELLO"); + hellos.emplace_back(hello3); + ctx.set("hellos", hellos); + BOOST_TEST(hbs.render("{{#hellos}}{{foo this/text}}{{/hellos}}", ctx) == "bar hellobar Hellobar HELLO"); + } + + // this keyword nested inside helpers param + { + hbs.registerHelper("foo", [](dom::Array const& args) { + return args[0]; + }); + dom::Object ctx; + dom::Array hellos; + dom::Object hello1; + hello1.set("text", "hello"); + hellos.emplace_back(hello1); + ctx.set("hellos", hellos); + BOOST_TEST_THROW_WITH( + hbs.render("{{#hellos}}{{foo text/this/foo}}{{/hellos}}", ctx), + HandlebarsError, "Invalid path: text/this - 1:17"); + + ctx.set("this", "bar"); + BOOST_TEST(hbs.render("{{foo [this]}}", ctx) == "bar"); + + dom::Object this_obj; + this_obj.set("this", "bar"); + ctx.set("text", this_obj); + BOOST_TEST(hbs.render("{{foo text/[this]}}", ctx) == "bar"); + + hbs.unregisterHelper("foo"); + } + + // pass string literals + { + BOOST_TEST(hbs.render("{{\"foo\"}}").empty()); + + dom::Object ctx; + ctx.set("foo", "bar"); + BOOST_TEST(hbs.render("{{\"foo\"}}", ctx) == "bar"); + + ctx = dom::Object(); + dom::Array foo; + foo.emplace_back("bar"); + foo.emplace_back("baz"); + ctx.set("foo", foo); + BOOST_TEST(hbs.render("{{#\"foo\"}}{{.}}{{/\"foo\"}}", ctx) == "barbaz"); + } + + // pass number literals + { + BOOST_TEST(hbs.render("{{12}}").empty()); + + dom::Object ctx; + ctx.set("12", "bar"); + BOOST_TEST(hbs.render("{{12}}", ctx) == "bar"); + + BOOST_TEST(hbs.render("{{12.34}}").empty()); + + ctx = dom::Object(); + ctx.set("12.34", "bar"); + BOOST_TEST(hbs.render("{{12.34}}", ctx) == "bar"); + + hbs.registerHelper("12.34", [](dom::Array const& args) { + std::string res = "bar"; + res += std::to_string(args[0].getInteger()); + return res; + }); + BOOST_TEST(hbs.render("{{12.34 1}}") == "bar1"); + hbs.unregisterHelper("12.34"); + } + + // pass boolean literals + { + BOOST_TEST(hbs.render("{{true}}").empty()); + + dom::Object ctx; + ctx.set("", "foo"); + BOOST_TEST(hbs.render("{{true}}").empty()); + + ctx = dom::Object(); + ctx.set("false", "foo"); + BOOST_TEST(hbs.render("{{false}}", ctx) == "foo"); + } + + // should handle literals in subexpression + { + hbs.registerHelper("foo", [](dom::Array const& args) { + return args[0]; + }); + hbs.registerHelper("false", [](dom::Array const& args) { + return "bar"; + }); + BOOST_TEST(hbs.render("{{foo (false)}}") == "bar"); + } +} + +void +run() { master_test(); safe_string(); + basic_context(); } }; diff --git a/src/test_suite/detail/decomposer.hpp b/src/test_suite/detail/decomposer.hpp index 4e5486052..736aebb63 100644 --- a/src/test_suite/detail/decomposer.hpp +++ b/src/test_suite/detail/decomposer.hpp @@ -47,6 +47,10 @@ namespace test_suite::detail format_value(T const& value) { std::string out; + if constexpr (std::is_convertible_v, std::string_view>) + { + out += '\"'; + } #ifdef MRDOX_TEST_HAS_FMT if constexpr (fmt::has_formatter::value) { out += fmt::format("{}", value); @@ -59,6 +63,10 @@ namespace test_suite::detail } else { out += demangle(); } + if constexpr (std::is_convertible_v, std::string_view>) + { + out += '\"'; + } return out; } diff --git a/src/test_suite/diff.cpp b/src/test_suite/diff.cpp index 00afa3e81..24bc990b6 100644 --- a/src/test_suite/diff.cpp +++ b/src/test_suite/diff.cpp @@ -277,8 +277,7 @@ BOOST_TEST_DIFF( else { // Compare rendered template with reference - auto success_contents = expected_contents; - DiffStringsResult diff = diffStrings(success_contents, rendered_contents); + DiffStringsResult diff = diffStrings(expected_contents, rendered_contents); if (diff.added > 0 || diff.removed > 0) { std::ofstream out((std::string(error_output_path))); @@ -292,8 +291,8 @@ BOOST_TEST_DIFF( BOOST_TEST(diff.added == 0); BOOST_TEST(diff.removed == 0); } - BOOST_TEST(rendered_contents.size() == success_contents.size()); - BOOST_TEST(rendered_contents == success_contents); + BOOST_TEST(rendered_contents.size() == expected_contents.size()); + BOOST_TEST((rendered_contents == expected_contents)); } } diff --git a/src/test_suite/test_suite.cpp b/src/test_suite/test_suite.cpp index 8639173dc..abe1ddce5 100644 --- a/src/test_suite/test_suite.cpp +++ b/src/test_suite/test_suite.cpp @@ -459,10 +459,10 @@ test( (void)func; log_ << "#" << id << " " << - file << "(" << line << ") " - "failed: " << expr << + file << "(" << line << ")\n" + "failed:\n " << expr << //" in " << func << - "\n"; + "\n\n"; log_.flush(); return false; } diff --git a/src/test_suite/test_suite.hpp b/src/test_suite/test_suite.hpp index 4887e3320..da08401cf 100644 --- a/src/test_suite/test_suite.hpp +++ b/src/test_suite/test_suite.hpp @@ -251,7 +251,7 @@ constexpr detail::log_type log{}; { \ DETAIL_START_WARNINGS_SUPPRESSION \ std::string d = DETAIL_STRINGIFY(__VA_ARGS__); \ - d += " ("; \ + d += "\n ("; \ DETAIL_SUPPRESS_PARENTHESES_WARNINGS \ d += (test_suite::detail::decomposer() <= __VA_ARGS__).format(); \ DETAIL_STOP_WARNINGS_SUPPRESSION \ @@ -304,6 +304,24 @@ constexpr detail::log_type log{}; #define BOOST_TEST_NOT(expr) BOOST_TEST(!(expr)) #ifndef BOOST_NO_EXCEPTIONS +# define BOOST_TEST_THROW_WITH( expr, ex, msg ) \ + try { \ + (void)(expr); \ + ::test_suite::detail::throw_failed_impl( \ + #expr, #ex, "@anon", \ + __FILE__, __LINE__); \ + } \ + catch(ex const& e) { \ + BOOST_TEST(std::string_view(e.what()) == std::string_view(msg)); \ + } \ + catch(...) { \ + ::test_suite::detail::throw_failed_impl( \ + #expr, #ex, "@anon", \ + __FILE__, __LINE__); \ + } \ + (void) 0 + // + # define BOOST_TEST_THROWS( expr, ex ) \ try { \ (void)(expr); \ @@ -311,20 +329,17 @@ constexpr detail::log_type log{}; #expr, #ex, "@anon", \ __FILE__, __LINE__); \ } \ - catch(ex const&) { \ + catch(ex const&) { \ BOOST_TEST_PASS(); \ } \ catch(...) { \ ::test_suite::detail::throw_failed_impl( \ #expr, #ex, "@anon", \ __FILE__, __LINE__); \ - } + } \ + (void) 0 // -#else - #define BOOST_TEST_THROWS( expr, ex ) -#endif -#ifndef BOOST_NO_EXCEPTIONS # define BOOST_TEST_NO_THROW( expr ) \ try { \ (void)(expr); \ @@ -340,7 +355,9 @@ constexpr detail::log_type log{}; } // #else -# define BOOST_TEST_NO_THROW( expr ) ( [&]{ expr; return true; }() ) + #define BOOST_TEST_THROWS( expr, ex ) + #define BOOST_TEST_THROW_WITH( expr, ex, msg ) + #define BOOST_TEST_NO_THROW( expr ) ( [&]{ expr; return true; }() ) #endif #define TEST_SUITE(type, name) \ diff --git a/test-files/handlebars/features_test.adoc b/test-files/handlebars/features_test.adoc new file mode 100644 index 000000000..77275422a --- /dev/null +++ b/test-files/handlebars/features_test.adoc @@ -0,0 +1,833 @@ +== from_chars + + + +=== Synopsis + +[,cpp] +---- +std::from_chars +---- + + +Declared in file + + +This is the from_chars function + + + + + + + +// Record detail partial +[,cpp] +---- +struct from_chars +{ +}; +---- + + +// #with to change context +Person: John Doe in page about `from_chars` + + +// #each to iterate, change context, and access parent context +People: +* Person: Alice Doe in page about `from_chars` +* Person: Bob Doe in page about `from_chars` +* Person: Carol Smith in page about `from_chars` + + +== Expressions + +// Render complete context with "." as key +[object Object] + +// Use to_string +{"page":{"kind":"record","name":"from_chars","decl":"std::from_chars","loc":"charconv","javadoc":{"brief":"Converts strings to numbers","details":"This function converts strings to numbers"},"synopsis":"This is the from_chars function","person":{"firstname":"John","lastname":"Doe"},"people":[{"firstname":"Alice","lastname":"Doe","book":[{},{},{},{}]},{"firstname":"Bob","lastname":"Doe","book":[{},{},{},{}]},{"firstname":"Carol","lastname":"Smith","book":[{},{},{},{}]}],"prefix":"Hello","specialChars":"& < > " ' ` =","url":"https://cppalliance.org/","author":{"firstname":"Yehuda","lastname":"Katz"}},"nav":[{"url":"foo","test":true,"title":"bar"},{"url":"bar"}],"myVariable":"lookupMyPartial","myOtherContext":{"information":"Interesting!"},"favoriteNumber":123,"prefix":"Hello","title":"My Title","body":"My Body","story":{"intro":"Before the jump","body":"After the jump"},"comments":[{"subject":"subject 1","body":"body 1"},{"subject":"subject 2","body":"body 2"}],"isActive":true,"isInactive":false,"peopleobj":{"Alice":{"firstname":"Alice","lastname":"Doe"},"Bob":{"firstname":"Bob","lastname":"Doe"},"Carol":{"firstname":"Carol","lastname":"Smith"}},"author":true,"firstname":"Yehuda","lastname":"Katz","names":["Yehuda Katz","Alan Johnson","Charles Jolley"],"namesobj":{"Yehuda":"Yehuda Katz","Alan":"Alan Johnson","Charles":"Charles Jolley"},"city":{"name":"San Francisco","summary":"San Francisco is the cultural center of Northern California","location":{"north":"37.73,","east":"-122.44"},"population":883305},"lookup_test":{"people":["Nils","Yehuda"],"cities":["Darmstadt","San Francisco"]},"lookup_test2":{"persons":[{"name":"Nils","resident-in":"darmstadt"},{"name":"Yehuda","resident-in":"san-francisco"}],"cities":{"darmstadt":{"name":"Darmstadt","country":"Germany"},"san-francisco":{"name":"San Francisco","country":"USA"}}},"containers":{"array":["a","b","c","d","e","f","g"],"array2":["e","f","g","h","i","j","k"],"object":{"a":"a","b":"b","c":"c","d":"d","e":"e","f":"f","g":"g"},"object2":{"e":"e","f":"f","g":"g","h":"h","i":"i","j":"j","k":"k"},"object_array":[{"account_id":"account-x10","product":"Chair"},{"account_id":"account-x10","product":"Bookcase"},{"account_id":"account-x11","product":"Desk"}]},"symbol":{"tag":"struct","kind":"record","name":"T"}} + +// Literals +true = Missing: true() +false = Missing: false() +null = Missing: null() +undefined = Missing: undefined() +./[true] = Missing: ./[true]() +./[false] = Missing: ./[false]() +./[null] = Missing: ./[null]() +./[undefined] = Missing: ./[undefined]() + +// Arrays +Second person is Bob Doe +Second person is Bob Doe + +// Dot segments +Second person is Bob Doe + +// Special characters (disabled for adoc) +raw: & < > " ' ` = +html-escaped: & < > " ' ` = + +// Helpers +JOHN DOE +https://cppalliance.org/[See Website] + +// Helpers with literal values +[source] +---- +** 10% Search +****************** 90% Upload stalled +******************** 100% Finish +---- + +// Undefined helper +Missing: undefinedhelper("Doe") + +// Helpers with hashes +https://chat.asciidoc.org[*project chat*^,role=green] + +// Subexpressions +****************** 90% Upload stalled +****************** 90% Upload stalled + +// Whitespace control +barEmpty +// Inline escapes +{{escaped}} +Missing: true() + +// Raw blocks +{{escaped}} + + +// Raw blocks +{{bar}} + + +// Raw block helper +{{BAR}} + + + +== Partials + +// Basic partials +[,cpp] +---- +struct from_chars +{ +}; +---- + +[,cpp] +---- +struct from_chars +{ +}; +---- + + +// Dynamic partials +Dynamo! +Found! + +// Partial context switch +Interesting! + +// Partial parameters +The result is 123 + + Hello, Alice Doe. + Hello, Bob Doe. + Hello, Carol Smith. + + +// Partial blocks + Failover content + + +// Pass templates to partials +Site Content My Content + + +// Inline partials + My Content + My Content + My Content + + +// Block inline partials + +
+ My Content +
+ +== Blocks + +// Block noop +
+

My Title

+
+ My Body +
+
+ +// Block function +
+

My Title

+
+
My Body
+
+
+ +// Block helper parameter +
+

My Title

+
Before the jump
+
After the jump
+ +
+ +// Simple iterators +
+

My Title

+
Before the jump
+
After the jump
+ +
+
+
+

subject 1

+ body 1 +
+
+

subject 2

+ body 2 +
+ +
+ +// Custom list helper + + +// Conditionals + Active + + + Active + + + + Inactive + + +// Chained blocks +// 1 + HIT Active 1 + + +// 2 + HIT Active 2 + + +// 3 + + HIT No User + + +// Block hash arguments + + +// Private variables +
  • 0. foo +
  • 1. bar +
+ +// Iterate objects + Id: 0, Key: Alice, Name: Alice Doe + Id: 1, Key: Bob, Name: Bob Doe + Id: 2, Key: Carol, Name: Carol Smith + + +// Block parameters + Id: 0 Name: Alice + Id: 1 Name: Bob + Id: 2 Name: Carol + + +// Recursive block parameters + User Id: 0 Book Id: 0 + User Id: 0 Book Id: 1 + User Id: 0 Book Id: 2 + User Id: 0 Book Id: 3 + + User Id: 1 Book Id: 0 + User Id: 1 Book Id: 1 + User Id: 1 Book Id: 2 + User Id: 1 Book Id: 3 + + User Id: 2 Book Id: 0 + User Id: 2 Book Id: 1 + User Id: 2 Book Id: 2 + User Id: 2 Book Id: 3 + + + +== Built-in Helpers + +// Author +

Yehuda Katz

+ + +// Unknown +
+ +

Unknown Author

+ +
+ +// Include zero +

Does render

+ + + +

Does render

+ + +// Custom +author defined +value2 undefined + +// unless +
+

WARNING: This entry does not have a license!

+ +
+ +// each with non objects +
    +
  • Yehuda Katz
  • +
  • Alan Johnson
  • +
  • Charles Jolley
  • + +
+ +// No paragraphs + +

No paragraphs

+ + +// indexes and keys + 0: Yehuda Katz 1: Alan Johnson 2: Charles Jolley + Yehuda: Yehuda Katz Alan: Alan Johnson Charles: Charles Jolley + +// with +Yehuda Katz + + +// with block parameters + San Francisco: 37.73, -122.44 + + + +// with inverse + +No city found + + +// lookup + +Nils lives in Darmstadt +Yehuda lives in San Francisco + + +// lookup2 + Nils lives in Darmstadt (Germany) + Yehuda lives in San Francisco (USA) + + +// log (there should be no rendered output) + + + + + + + + +== Hooks + +// Helper missing +Missing: foo() +Missing: foo(true) +Missing: foo(2, true) +Missing: foo(true) +Helper 'foo' not found. Printing block: block content + +// Block helper missing +Helper 'person' not found. Printing block: Yehuda Katz + + +== String helpers + +// capitalize +Hello world! +Hello world! +Hello world! +Hello world! +// center + Hello world! + Hello world! +-------------------Hello world!------------------- +-------------------Hello world!------------------- +// ljust +Hello world! +Hello world! +Hello world!-------------------------------------- +Hello world!-------------------------------------- +// pad_end +Hello world! +Hello world! +Hello world!-------------------------------------- +Hello world!-------------------------------------- +// rjust + Hello world! + Hello world! +--------------------------------------Hello world! +--------------------------------------Hello world! +// pad_start + Hello world! + Hello world! +--------------------------------------Hello world! +--------------------------------------Hello world! +// count +2 +2 +1 +1 +1 +1 +// ends_with +true +true +true +true +true +true +false +false +// starts_with +true +true +true +true +true +true +false +false +// expandtabs +Hello world! +Hello world! +Hello world! +Hello world! +Helloworld! +Helloworld! +// find +6 +6 +// index_of +6 +6 +// includes +true +true +false +false +// rfind +-1 +-1 +-1 +-1 +// rindex_of +-1 +-1 +-1 +-1 +// last_index_of +-1 +-1 +-1 +-1 +// at +e +e +// char_at +e +e +// isalnum +true +true +false +false +// isalpha +true +true +true +true +false +false +// isascii +true +true +// isdecimal +false +false +true +true +// isdigit +false +false +true +true +// islower +false +false +false +false +// isupper +false +false +false +false +// isprintable +true +true +false +false +// isspace +false +false +true +true +true +true +// istitle +false +false +true +true +// upper +HELLO WORLD! +HELLO WORLD! +// to_upper +HELLO WORLD! +HELLO WORLD! +// lower +hello world! +hello world! +// to_lower +hello world! +hello world! +// swapcase +hELLO WORLD! +hELLO WORLD! +// join +Hello,world! +Hello,world! +// concat +Hello world!,Bye! +Hello world!,Bye! +// strip +Hello world! +Hello world! +Hello world! +Hello world! +// trim +Hello world! +Hello world! +Hello world! +Hello world!--------' +// lstrip +Hello world! +Hello world! +Hello world!-------- +Hello world!-------- +// trim_start +Hello world! +Hello world! +Hello world!-------- +Hello world!-------- +// rstrip + Hello world! + Hello world! +--------Hello world! +--------Hello world! +// trim_end + Hello world! + Hello world! +--------Hello world! +--------Hello world! +// partition +[Hello, ,world!] +[Hello, ,world!] +[Hello world!,,] +[Hello world!,,] +// rpartition +[Hello, ,world!] +[Hello, ,world!] +[Hello world!,,] +[Hello world!,,] +// remove_prefix + world! + world! +// remove_suffix +Hello +Hello +Hello world +Hello world +// replace +Hello! +Hello! +// split +[Hello,world!] +[Hello,world!] +[He,] +[He,] +// rsplit +[world!,Hell] +[world!,Hell] +[d!,o wo] +[d!,o wo] +// splitlines +[Hello world!\nBye!] +[Hello world!\nBye!] +// zfill +00000000000000000000000000000000000000Hello world! +00000000000000000000000000000000000000Hello world! +00000000000000000000000000000000000000000000000000000000000000000000000000000030 +00000000000000000000000000000000000000000000000000000000000000000000000000000030 +-0000000000000000000000000000000000000000000000000000000000000000000000000000030 +-0000000000000000000000000000000000000000000000000000000000000000000000000000030 +// repeat +Hello world!Hello world!Hello world! +Hello world!Hello world!Hello world! +// escape +Hello world! +Hello world! +<Hello world!></Hello> +<Hello world!></Hello> +// slice +ello +ello +ello world! +ello world! +ello world +ello world +ell +ell +// substr +ello +ello +ello world! +ello world! +ello world +ello world +ell +ell +// safe_anchor_id +hello-world! +hello-world! +// strip_namespace +Hello world! +Hello world! +memory_order +memory_order +memory_order_acquire +memory_order_acquire +basic_string +basic_string + +== Containers + +// size +7 +7 +3 +// len +7 +7 +3 +// keys + +[a,b,c,d,e,f,g] + +// list + +list helper requires array argument: object provided + +// iter + +[a,b,c,d,e,f,g] + +// values +[a,b,c,d,e,f,g] +[a,b,c,d,e,f,g] +[{"account_id":"account-x10","product":"Chair"},{"account_id":"account-x10","product":"Bookcase"},{"account_id":"account-x11","product":"Desk"}] +// del +[a,b,d,e,f,g] +{"a":"a","b":"b","d":"d","e":"e","f":"f","g":"g"} +[{"account_id":"account-x10","product":"Chair"},{"account_id":"account-x10","product":"Bookcase"},{"account_id":"account-x11","product":"Desk"}] +// delete +[a,b,d,e,f,g] +{"a":"a","b":"b","d":"d","e":"e","f":"f","g":"g"} +[{"account_id":"account-x10","product":"Chair"},{"account_id":"account-x10","product":"Bookcase"},{"account_id":"account-x11","product":"Desk"}] +// has +true +true +false +// exist +true +false +true +false +false +// contains +true +false +true +false +false +// has_any +true +false +false +true +false +false +false +// exist_any +true +true +false +// contains_any +true +true +false +// get +c +c +{"account_id":"account-x11","product":"Desk"} +// get_or +y +y +y +// items +[a,b,c,d,e,f,g] +[[a,a],[b,b],[c,c],[d,d],[e,e],[f,f],[g,g]] +[{"account_id":"account-x10","product":"Chair"},{"account_id":"account-x10","product":"Bookcase"},{"account_id":"account-x11","product":"Desk"}] +// entries +[a,b,c,d,e,f,g] +[[a,a],[b,b],[c,c],[d,d],[e,e],[f,f],[g,g]] +[{"account_id":"account-x10","product":"Chair"},{"account_id":"account-x10","product":"Bookcase"},{"account_id":"account-x11","product":"Desk"}] +// first +a +"a" +{"account_id":"account-x10","product":"Chair"} +// head +a +"a" +{"account_id":"account-x10","product":"Chair"} +// front +a +"a" +{"account_id":"account-x10","product":"Chair"} +// last +g +"g" +{"account_id":"account-x11","product":"Desk"} +// tail +g +"g" +{"account_id":"account-x11","product":"Desk"} +// back +g +"g" +{"account_id":"account-x11","product":"Desk"} +// reverse +[g,f,e,d,c,b,a] +[["g","g"],["f","f"],["e","e"],["d","d"],["c","c"],["b","b"],["a","a"]] +[{"account_id":"account-x11","product":"Desk"},{"account_id":"account-x10","product":"Bookcase"},{"account_id":"account-x10","product":"Chair"}] +// reversed +[g,f,e,d,c,b,a] +[["g","g"],["f","f"],["e","e"],["d","d"],["c","c"],["b","b"],["a","a"]] +[{"account_id":"account-x11","product":"Desk"},{"account_id":"account-x10","product":"Bookcase"},{"account_id":"account-x10","product":"Chair"}] +// update +[a,b,c,d,e,f,g,h,i,j,k] +{"e":"e","f":"f","g":"g","h":"h","i":"i","j":"j","k":"k","a":"a","b":"b","c":"c","d":"d"} +[{"account_id":"account-x10","product":"Chair"},{"account_id":"account-x10","product":"Bookcase"},{"account_id":"account-x11","product":"Desk"},"e","f","g","h","i","j","k"] +// merge +[a,b,c,d,e,f,g,h,i,j,k] +{"e":"e","f":"f","g":"g","h":"h","i":"i","j":"j","k":"k","a":"a","b":"b","c":"c","d":"d"} +[{"account_id":"account-x10","product":"Chair"},{"account_id":"account-x10","product":"Bookcase"},{"account_id":"account-x11","product":"Desk"},"e","f","g","h","i","j","k"] +// sort +[a,b,c,d,e,f,g] +{"a":"a","b":"b","c":"c","d":"d","e":"e","f":"f","g":"g"} +[{"account_id":"account-x10","product":"Bookcase"},{"account_id":"account-x10","product":"Chair"},{"account_id":"account-x11","product":"Desk"}] +// sort_by + + +[{"account_id":"account-x10","product":"Chair"},{"account_id":"account-x10","product":"Bookcase"},{"account_id":"account-x11","product":"Desk"}] +// at +c +c +{"account_id":"account-x11","product":"Desk"} +// fill +[a,b,-,-,-,f,g] +[a,b,-,-,-,-,g] + + +// count +1 +1 +0 +// concat +[a,b,c,d,e,f,g,e,f,g,h,i,j,k] +[object Object] +[[object Object],[object Object],[object Object],e,f,g,h,i,j,k] +// replace +[a,b,d,d,e,f,g] +[a,b,d,d,e,f,g] +{"c":"d","a":"a","b":"b","c":"c","d":"d","e":"e","f":"f"} +[{"account_id":"account-x10","product":"Chair"},{"account_id":"account-x10","product":"Bookcase"},{"account_id":"account-x11","product":"Desk"}] +// chunk +[[a,b,c],[d,e,f],[g]] +[{"a":"a","b":"b","c":"c"},{"d":"d","e":"e","f":"f"},{"g":"g"}] +[[{"account_id":"account-x10","product":"Chair"},{"account_id":"account-x10","product":"Bookcase"}],[{"account_id":"account-x11","product":"Desk"}]] +// group_by + + +{"account-x10":[{"account_id":"account-x10","product":"Chair"},{"account_id":"account-x10","product":"Bookcase"}],"account-x11":[{"account_id":"account-x11","product":"Desk"}]} +{"Chair":[{"account_id":"account-x10","product":"Chair"}],"Bookcase":[{"account_id":"account-x10","product":"Bookcase"}],"Desk":[{"account_id":"account-x11","product":"Desk"}]} +// pluck + + +["account-x10","account-x10","account-x11"] +["Chair","Bookcase","Desk"] +// unique +["a","b","c","d","e","f","g","h","i","j","k"] + + + +// Inverse block with no helper expands expressions + + struct T + + diff --git a/test-files/handlebars/features_test.adoc.hbs b/test-files/handlebars/features_test.adoc.hbs index 13328c673..6844f0943 100644 --- a/test-files/handlebars/features_test.adoc.hbs +++ b/test-files/handlebars/features_test.adoc.hbs @@ -61,8 +61,6 @@ undefined = {{undefined}} ./[false] = {{./[false]}} ./[null] = {{./[null]}} ./[undefined] = {{./[undefined]}} -'See Website' = {{'See Website'}} -"See Website" = {{"See Website"}} // Arrays Second person is {{page.people.[1].firstname}} {{page.people.[1].lastname}} diff --git a/test-files/handlebars/record.adoc.hbs b/test-files/handlebars/record.adoc.hbs new file mode 100644 index 000000000..f63dca3aa --- /dev/null +++ b/test-files/handlebars/record.adoc.hbs @@ -0,0 +1,9 @@ +{{#if symbol.template}} + {{>template-head symbol.template}} + {{symbol.tag}} {{symbol.name~}} + {{#if (neq symbol.template.kind "primary")~}} + {{>template-args args=symbol.template.args}} + {{/if}} +{{else}} + {{symbol.tag}} {{symbol.name~}} +{{/if}}