diff --git a/include/mrdox/Support/Handlebars.hpp b/include/mrdox/Support/Handlebars.hpp index c8dc2cac1..626bb326b 100644 --- a/include/mrdox/Support/Handlebars.hpp +++ b/include/mrdox/Support/Handlebars.hpp @@ -69,6 +69,17 @@ struct HandlebarsOptions */ bool compat = false; + /** Enable tracking of ids + + When enabled, the ids of the expressions are tracked and + passed to the helpers. + + Helpers often use this information to update the context + path to the current expression, which can later be used to + look up the value of the expression with ".." segments. + */ + bool trackIds = false; + /** Custom private data object This variable can be used to pass in an object to define custom @@ -275,7 +286,7 @@ class MRDOX_DECL OutputRef } std::size_t - getIndent() noexcept + getIndent() const noexcept { return indent_; } @@ -303,17 +314,23 @@ class MRDOX_DECL HandlebarsCallback { private: using callback_type = std::function< - void(OutputRef, dom::Value const&, dom::Object const&, dom::Object const&)>; + void( + OutputRef, + dom::Value const& /* context */, + dom::Object const& /* data */, + dom::Object const& /* blockValues */, + dom::Object const& /* blockValuePaths */)>; callback_type fn_; callback_type inverse_; dom::Value const* context_{ nullptr }; OutputRef* output_{ nullptr }; dom::Object const* data_; - std::vector ids_; - dom::Object hashes_; + std::vector ids_; + dom::Object hash_; + dom::Object hashIds_; std::string_view name_; - std::vector blockParams_; + std::vector blockParamIds_; std::function const* logger_; detail::RenderState* renderState_; friend class Handlebars; @@ -398,7 +415,8 @@ class MRDOX_DECL HandlebarsCallback std::string fn(dom::Value const& context, dom::Object const& data, - dom::Object const& blockValues) const; + dom::Array const& blockParams, + dom::Array const& blockParamPaths) const; /** Render the block content with specified private data and block parameters @@ -417,7 +435,8 @@ class MRDOX_DECL HandlebarsCallback fn(OutputRef out, dom::Value const& context, dom::Object const& data, - dom::Object const& blockValues) const; + dom::Array const& blockParams, + dom::Array const& blockParamPaths) const; /** Render the inverse block content with the specified context @@ -492,7 +511,8 @@ class MRDOX_DECL HandlebarsCallback inverse( dom::Value const& context, dom::Object const& data, - dom::Object const& blockValues) const; + dom::Array const& blockParams, + dom::Array const& blockParamPaths) const; /** Render the inverse block content with private data and block parameters @@ -512,7 +532,8 @@ class MRDOX_DECL HandlebarsCallback OutputRef out, dom::Value const& context, dom::Object const& data, - dom::Object const& blockValues) const; + dom::Array const& blockParamPaths, + dom::Array const& blockParams) const; /** Determine if helper is being called from a block section @@ -554,7 +575,9 @@ class MRDOX_DECL HandlebarsCallback @return `true` if the helper is being called from a block section */ - bool isBlock() const { + bool + isBlock() const + { return static_cast(fn_); } @@ -593,40 +616,46 @@ class MRDOX_DECL HandlebarsCallback return *data_; } - /// Extra key value pairs passed to the callback - dom::Object& - hashes() { - return hashes_; - } - - /// Extra key value pairs passed to the callback - dom::Object const& - hashes() const { - return hashes_; - } - /// Ids of the expression parameters - std::vector& + std::vector& ids() { return ids_; } /// Ids of the expression parameters - std::vector const& + std::vector const& ids() const { return ids_; } - /// Block parameters passed to the callback - std::vector& - blockParams() { - return blockParams_; + /// Extra key value pairs passed to the callback + dom::Object& + hash() { + return hash_; + } + + /// Extra key value pairs passed to the callback + dom::Object const& + hash() const { + return hash_; + } + + /// Extra key value pairs passed to the callback + dom::Object& + hashIds() { + return hashIds_; + } + + /// Extra key value pairs passed to the callback + dom::Object const& + hashIds() const { + return hashIds_; } /// Block parameters passed to the callback - std::vector const& + std::size_t blockParams() const { - return blockParams_; + return blockParamIds_.size(); } /** Name of the helper being called @@ -1207,7 +1236,15 @@ class Handlebars { HandlebarsCallback& cb, HandlebarsOptions const& opt) const; - std::pair + struct evalExprResult { + dom::Value value; + bool found = false; + bool isLiteral = false; + bool isSubexpr = false; + bool fromBlockParams = false; + }; + + evalExprResult evalExpr( dom::Value const &context, std::string_view expression, diff --git a/src/lib/Support/Handlebars.cpp b/src/lib/Support/Handlebars.cpp index fb573249b..eaad12610 100644 --- a/src/lib/Support/Handlebars.cpp +++ b/src/lib/Support/Handlebars.cpp @@ -346,7 +346,9 @@ namespace detail { std::size_t partialBlockLevel = 0; dom::Object data; dom::Object blockValues; - std::vector compatStack; + dom::Object blockValuePaths; + std::vector parentContext; + dom::Value rootContext; std::vector dataStack; }; } @@ -355,15 +357,12 @@ std::string HandlebarsCallback:: fn(dom::Value const& context, dom::Object const& data, - dom::Object const& blockValues) const { - if (isBlock()) { - std::string result; - OutputRef out(result); - fn_(out, context, data, blockValues); - return result; - } else { - return {}; - } + dom::Array const& blockParams, + dom::Array const& blockParamPaths) const { + std::string result; + OutputRef out(result); + fn(out, context, data, blockParams, blockParamPaths); + return result; } void @@ -371,37 +370,57 @@ HandlebarsCallback:: fn(OutputRef out, dom::Value const& context, dom::Object const& data, - dom::Object const& blockValues) const { - if (isBlock()) { - fn_(out, context, data, blockValues); + dom::Array const& blockParams, + dom::Array const& blockParamPaths) const { + if (!isBlock()) { + return; + } + bool const sameContext = + &context == context_ || + (context.isObject() && context_->isObject() && context.getObject().impl() == context_->getObject().impl()) || + (context.isArray() && context_->isArray() && context.getArray().impl() == context_->getArray().impl()); + if (!sameContext) + { + renderState_->parentContext.push_back(*context_); + } + dom::Object blockValues; + dom::Object blockValuePaths; + for (std::size_t i = 0; i < blockParamIds_.size(); ++i) { + blockValues.set(blockParamIds_[i], blockParams[i]); + if (blockParamPaths.size() > i) { + blockValuePaths.set(blockParamIds_[i], blockParamPaths[i]); + } + } + fn_(out, context, data, blockValues, blockValuePaths); + if (!sameContext) + { + renderState_->parentContext.pop_back(); } } std::string HandlebarsCallback:: fn(dom::Value const& context) const { - return fn(context, data(), dom::Object{}); + return fn(context, data(), dom::Array{}, dom::Array{}); } void HandlebarsCallback:: fn(OutputRef out, dom::Value const& context) const { - fn(out, context, data(), dom::Object{}); + fn(out, context, data(), dom::Array{}, dom::Array{}); } std::string HandlebarsCallback:: -inverse(dom::Value const& context, - dom::Object const& data, - dom::Object const& blockValues) const { - if (isBlock()) { - std::string result; - OutputRef out(result); - inverse_(out, context, data, blockValues); - return result; - } else { - return {}; - } +inverse( + dom::Value const& context, + dom::Object const& data, + dom::Array const& blockParams, + dom::Array const& blockParamPaths) const { + std::string result; + OutputRef out(result); + inverse(out, context, data, blockParams, blockParamPaths); + return result; } void @@ -409,22 +428,38 @@ HandlebarsCallback:: inverse(OutputRef out, dom::Value const& context, dom::Object const& data, - dom::Object const& blockValues) const { - if (isBlock()) { - inverse_(out, context, data, blockValues); + dom::Array const& blockParams, + dom::Array const& blockParamPaths) const { + if (!isBlock()) { + return; + } + if (&context != context_) + { + renderState_->parentContext.push_back(*context_); + } + dom::Object blockValues; + dom::Object blockValuePaths; + for (std::size_t i = 0; i < blockParamIds_.size(); ++i) { + blockValues.set(blockParamIds_[i], blockParams[i]); + blockValuePaths.set(blockParamIds_[i], blockParamPaths[i]); + } + inverse_(out, context, data, blockValues, blockValuePaths); + if (&context != context_) + { + renderState_->parentContext.pop_back(); } } std::string HandlebarsCallback:: inverse(dom::Value const& context) const { - return inverse(context, data(), dom::Object{}); + return inverse(context, data(), dom::Array{}, dom::Array{}); } void HandlebarsCallback:: inverse(OutputRef out, dom::Value const& context) const { - inverse(out, context, data(), dom::Object{}); + inverse(out, context, data(), dom::Array{}, dom::Array{}); } void @@ -1231,7 +1266,7 @@ render_to( state.data = options.data.getObject(); } state.inlinePartials.emplace_back(); - state.compatStack.emplace_back(context); + state.rootContext = context; state.dataStack.emplace_back(state.data); render_to(out, context, options, state); } @@ -1524,8 +1559,7 @@ popContextSegment(std::string_view& contextPath) { return true; } - -std::pair +Handlebars::evalExprResult Handlebars:: evalExpr( dom::Value const& context, @@ -1542,23 +1576,23 @@ evalExpr( // ============================================================== if (is_literal_value(expression, "true")) { - return {true, true}; + return {true, true, true}; } if (is_literal_value(expression, "false")) { - return {false, true}; + return {false, true, true}; } if (is_literal_value(expression, "null") || is_literal_value(expression, "undefined") || expression.empty()) { - return {nullptr, true}; + return {nullptr, true, true}; } if (expression == "." || expression == "this") { - return {context, true}; + return {context, true, false}; } if (is_literal_string(expression)) { - return {unescapeString(expression), true}; + return {unescapeString(expression), true, true}; } if (is_literal_integer(expression)) { @@ -1568,8 +1602,8 @@ evalExpr( expression.data() + expression.size(), value); if (res.ec != std::errc()) - return {std::int64_t(0), true}; - return {value, true}; + return {std::int64_t(0), true, true}; + return {value, true, true}; } // ============================================================== // Subexpressions @@ -1597,7 +1631,7 @@ evalExpr( cb.name_ = helper; cb.context_ = &context; setupArgs(all, context, state, args, cb, opt); - return {fn(args, cb).first, true}; + return {fn(args, cb).first, true, false, true}; } } // ============================================================== @@ -1615,10 +1649,9 @@ evalExpr( { data = state.data.find("root"); } - else if (!state.compatStack.empty()) + else { - MRDOX_ASSERT(!state.compatStack.empty()); - data = state.compatStack.front(); + data = state.rootContext; } } else if (expression.starts_with("./") || expression.starts_with("../")) @@ -1637,7 +1670,7 @@ evalExpr( expression.remove_prefix(3); if (dataIt == rDataStack.end()) { - return {nullptr, false}; + return {nullptr, false, false}; } data = *dataIt; ++dataIt; @@ -1646,114 +1679,107 @@ evalExpr( break; } } - return lookupPropertyImpl(data, expression, state); + auto [res, found] = lookupPropertyImpl(data, expression, state); + return {res, found, false}; } // ============================================================== // Dotdot context path // ============================================================== if (expression.starts_with("..")) { - // Determine the context path, if any - dom::Value contextPathV = state.data.find("contextPath"); - std::string_view contextPath; - if (contextPathV.isString()) - { - contextPath = contextPathV.getString(); - } - // Remove last segment if it's a literal integer - if (!contextPath.empty()) - { - auto pos = contextPath.find_last_of('.'); - if (pos != std::string_view::npos) - { - auto lastSegment = contextPath.substr(pos + 1); - if (is_literal_integer(lastSegment)) - { - contextPath = contextPath.substr(0, pos); - } - } + // Get value from parent helper contexts + std::size_t dotdots = 1; + expression.remove_prefix(2); + if (expression.starts_with('/')) { + expression.remove_prefix(1); } while (expression.starts_with("..")) { - popContextSegment(contextPath); + ++dotdots; expression.remove_prefix(2); - if (!expression.empty()) { - if (expression.front() != '/') { - return {nullptr, false}; - } + if (expression.starts_with('/')) { expression.remove_prefix(1); } } - std::string absContextPath; - dom::Value root = nullptr; - if (state.data.exists("root")) - { - root = state.data.find("root"); - } - else - { - MRDOX_ASSERT(!state.compatStack.empty()); - root = state.compatStack.front(); + if (dotdots > state.parentContext.size()) { + return {nullptr, false}; } - do { - absContextPath = contextPath; - if (!expression.empty()) - { - if (!absContextPath.empty()) - { - absContextPath += '.'; - } - absContextPath += expression; - } - auto [v, defined] = lookupPropertyImpl(root, absContextPath, state); - if (defined) { - return {v, defined}; - } - popContextSegment(contextPath); - } while (!contextPath.empty()); - return lookupPropertyImpl(root, expression, state); + dom::Value parentCtx = + state.parentContext[state.parentContext.size() - dotdots]; + auto [res, found] = lookupPropertyImpl(parentCtx, expression, state); + return {res, found, false}; } // ============================================================== - // Whole context object key + // Pathed type // ============================================================== - if (context.kind() == dom::Kind::Object) { - auto& obj = context.getObject(); - if (obj.exists(expression)) - { - return {obj.find(expression), true}; - } + // Precedence: + // 1) Pathed context values + // 2) Block values + // 3) Context values + bool isPathedValue = false; + if (expression == "this" || + expression == "." || + expression.starts_with("this.") || + expression.starts_with("./")) + { + isPathedValue = true; } + // ============================================================== - // Context path + // Pathed context values // ============================================================== dom::Value r; bool defined; - std::tie(r, defined) = lookupPropertyImpl(context, expression, state); - if (defined) { - return {r, defined}; + if (isPathedValue) + { + std::tie(r, defined) = lookupPropertyImpl(context, expression, state); + if (defined) { + return {r, defined, false}; + } } + // ============================================================== // Block values // ============================================================== std::tie(r, defined) = lookupPropertyImpl(state.blockValues, expression, state); if (defined) { - return {r, defined}; + return {r, defined, false, false, true}; + } + + // ============================================================== + // Whole context object key + // ============================================================== + if (context.kind() == dom::Kind::Object) { + auto& obj = context.getObject(); + if (obj.exists(expression)) + { + return {obj.find(expression), true, false}; + } } + + // ============================================================== + // Context values + // ============================================================== + std::tie(r, defined) = lookupPropertyImpl(context, expression, state); + if (defined) { + return {r, defined, false}; + } + // ============================================================== // Parent contexts // ============================================================== if (opt.compat) { - auto parentContexts = std::ranges::views::reverse(state.compatStack); + auto parentContexts = std::ranges::views::reverse(state.parentContext); for (auto parentContext: parentContexts) { std::tie(r, defined) = lookupPropertyImpl(parentContext, expression, state); if (defined) { - return {r, defined}; + return {r, defined, false}; } } } - return {nullptr, false}; + return {nullptr, false, false}; } auto @@ -2035,7 +2061,9 @@ renderExpression( detail::RenderState& state) const { if (tag.helper.empty()) + { return; + } auto opt2 = opt; opt2.noEscape = tag.forceNoHTMLEscape || opt.noEscape; @@ -2089,21 +2117,21 @@ renderExpression( unescaped = unescapeString(helper_expr); helper_expr = unescaped; } - auto [v, defined] = evalExpr(context, helper_expr, state, opt, false); - if (defined) + auto resV = evalExpr(context, helper_expr, state, opt, false); + if (resV.found) { - if (v.isFunction()) + if (resV.value.isFunction()) { dom::Array args = dom::newArray(); HandlebarsCallback cb; cb.name_ = helper_expr; setupArgs(tag.arguments, context, state, args, cb, opt); - auto v2 = v.getFunction().call(args).value(); + auto v2 = resV.value.getFunction().call(args).value(); format_to(out, v2, opt2); } else { - format_to(out, v, opt2); + format_to(out, resV.value, opt2); } return; } @@ -2128,6 +2156,23 @@ renderExpression( } } +std::string_view +remove_redundant_prefixes(std::string_view expr) { + if (expr.starts_with("./")) { + expr.remove_prefix(2); + } + else if (expr.starts_with("this.")) { + expr.remove_prefix(5); + } + else if (expr == "this") { + expr.remove_prefix(4); + } + else if (expr == ".") { + expr.remove_prefix(1); + } + return expr; +} + void Handlebars:: setupArgs( @@ -2158,12 +2203,52 @@ setupArgs( auto [k, v] = findKeyValuePair(expr); if (k.empty()) { - args.emplace_back(evalExpr(context, expr, state, opt, true).first); - cb.ids_.push_back(expr); + auto res = evalExpr(context, expr, state, opt, true); + args.emplace_back(res.value); + if (opt.trackIds) { + if (res.isLiteral) { + cb.ids_.emplace_back(nullptr); + } + else if (res.isSubexpr) { + cb.ids_.emplace_back(true); + } + else if (res.fromBlockParams) { + std::size_t n = state.blockValuePaths.size(); + dom::Value IdVal = expr; + for (std::size_t i = 0; i < n; ++i) + { + auto blockValuePath = state.blockValuePaths[i]; + if (expr.starts_with(blockValuePath.key)) { + if (blockValuePath.value.isString()) { + std::string res; + res += blockValuePath.value.getString(); + res += expr.substr(blockValuePath.key.size()); + IdVal = res; + } + break; + } + } + cb.ids_.emplace_back(IdVal); + } else { + cb.ids_.emplace_back(remove_redundant_prefixes(expr)); + } + } } else { - cb.hashes_.set(k, evalExpr(context, v, state, opt, true).first); + auto res = evalExpr(context, v, state, opt, true); + cb.hash_.set(k, res.value); + if (opt.trackIds) { + if (res.isLiteral) { + cb.hashIds_.set(k, nullptr); + } + else if (res.isSubexpr) { + cb.hashIds_.set(k, true); + } + else { + cb.hashIds_.set(k, remove_redundant_prefixes(v)); + } + } } } cb.renderState_ = &state; @@ -2187,13 +2272,13 @@ renderDecorator( // Evaluate expression std::string_view expr; findExpr(expr, tag.arguments); - auto [value, defined] = evalExpr(context, expr, state, opt, true); - if (!value.isString()) + auto res = evalExpr(context, expr, state, opt, true); + if (!res.value.isString()) { out << fmt::format(R"([invalid decorator expression "{}" in "{}"])", tag.arguments, tag.buffer); return; } - std::string_view partial_name = value.getString(); + std::string_view partial_name = res.value.getString(); // Parse block std::string_view fnBlock; @@ -2230,10 +2315,10 @@ renderPartial( { std::string_view expr; findExpr(expr, partialName); - auto [value, defined] = evalExpr(context, expr, state, opt, true); - if (value.isString()) + auto res = evalExpr(context, expr, state, opt, true); + if (res.value.isString()) { - partialName = value.getString(); + partialName = res.value.getString(); } } else if (isEscapedPartialName) @@ -2317,11 +2402,12 @@ renderPartial( } // Populate with arguments + bool partialCtxChanged = false; + dom::Value prevContextPath = state.data.find("contextPath"); if (!tag.arguments.empty()) { // create context from specified keys auto tagContent = tag.arguments; - bool partialCtxDefined = false; std::string_view expr; while (findExpr(expr, tagContent)) { @@ -2331,7 +2417,7 @@ renderPartial( if (isContextReplacement) { // Check if context has been replaced before - if (partialCtxDefined) + if (partialCtxChanged) { std::size_t n = 2; while (findExpr(expr, tagContent)) @@ -2353,35 +2439,41 @@ renderPartial( } // Replace context - auto [value, defined] = evalExpr(context, expr, state, opt, true); - if (defined) + auto res = evalExpr(context, expr, state, opt, true); + if (opt.trackIds) { - if (value.isObject()) + std::string contextPath = appendContextPath( + state.data.find("contextPath"), expr); + state.data.set("contextPath", contextPath); + } + if (res.found) + { + if (res.value.isObject()) { - partialCtx = createFrame(value.getObject()); + partialCtx = createFrame(res.value.getObject()); } else { - partialCtx = value; + partialCtx = res.value; } } - partialCtxDefined = true; + partialCtxChanged = true; continue; } // Argument is key=value pair - dom::Value value; - bool defined; + evalExprResult res; if (contextKey != ".") { - std::tie(value, defined) = evalExpr(context, contextKey, state, opt, true); + res = evalExpr(context, contextKey, state, opt, true); } else { - value = context; - defined = true; + res.value = context; + res.found = true; + res.isLiteral = false; } - if (defined) + if (res.found) { bool const needs_reset_context = !partialCtx.isObject(); if (needs_reset_context) @@ -2396,7 +2488,13 @@ renderPartial( partialCtx = dom::Object{}; } } - partialCtx.getObject().set(partialKey, value); + partialCtx.getObject().set(partialKey, res.value); + } + + if (opt.trackIds) + { + // should invalidate context for partials with parameters + state.data.set("contextPath", true); } } } @@ -2412,17 +2510,29 @@ renderPartial( bool const isPartialBlock = partialName == "@partial-block"; state.partialBlockLevel -= isPartialBlock; out.setIndent(out.getIndent() + tag.standaloneIndent * !opt.preventIndent); - state.compatStack.emplace_back(context); + if (partialCtxChanged) + { + state.parentContext.emplace_back(context); + } state.dataStack.emplace_back(state.data); + // Render this->render_to(out, partialCtx, opt, state); + // Restore state - state.compatStack.pop_back(); + if (partialCtxChanged) + { + state.parentContext.pop_back(); + } state.dataStack.pop_back(); out.setIndent(out.getIndent() - tag.standaloneIndent * !opt.preventIndent); state.partialBlockLevel += isPartialBlock; state.templateText = templateText; state.templateText0 = templateText0; + if (opt.trackIds && partialCtxChanged) + { + state.data.set("contextPath", prevContextPath); + } if (tag.type2 == '#') { @@ -2504,7 +2614,7 @@ renderBlock( while (findExpr(expr, bps)) { bps = bps.substr(expr.data() + expr.size() - bps.data()); - cb.blockParams_.push_back(expr); + cb.blockParamIds_.push_back(expr); } // ============================================================== @@ -2515,20 +2625,29 @@ renderBlock( OutputRef os, dom::Value const &item, dom::Object const &data, - dom::Object const &newBlockValues) -> void + dom::Object const &newBlockValues, + dom::Object const &newBlockValuePaths) -> void { std::string_view templateText = state.templateText; state.templateText = fnBlock; dom::Object state_data = state.data; state.data = dom::Object(data); - if (!newBlockValues.empty()) { + if (!newBlockValues.empty()) + { dom::Object blockValuesOverlay = createFrame(newBlockValues, state.blockValues); dom::Object blockValues = state.blockValues; state.blockValues = std::move(blockValuesOverlay); + dom::Object blockValuePathsOverlay = + createFrame(newBlockValuePaths, state.blockValuePaths); + dom::Object blockValuePaths = state.blockValuePaths; + state.blockValuePaths = std::move(blockValuePathsOverlay); render_to(os, item, opt, state); state.blockValues = std::move(blockValues); - } else { + state.blockValuePaths = std::move(blockValuePaths); + } + else + { render_to(os, item, opt, state); } state.templateText = templateText; @@ -2539,7 +2658,8 @@ renderBlock( OutputRef os, dom::Value const &item, dom::Object const &data, - dom::Object const &newBlockValues) -> void + dom::Object const &newBlockValues, + dom::Object const &newBlockValuePaths) -> void { std::string_view templateText = state.templateText; state.templateText = inverseBlock; @@ -2562,14 +2682,22 @@ renderBlock( } else { - if (!newBlockValues.empty()) { + if (!newBlockValues.empty()) + { dom::Object blockValuesOverlay = createFrame(newBlockValues, state.blockValues); dom::Object blockValues = state.blockValues; state.blockValues = std::move(blockValuesOverlay); + dom::Object blockValuePathsOverlay = + createFrame(newBlockValuePaths, state.blockValuePaths); + dom::Object blockValuePaths = state.blockValuePaths; + state.blockValuePaths = std::move(blockValuePathsOverlay); renderBlock(blockName, inverseTag, os, item, opt, state, true); state.blockValues = std::move(blockValues); - } else { + state.blockValuePaths = std::move(blockValuePaths); + } + else + { renderBlock(blockName, inverseTag, os, item, opt, state, true); } } @@ -2581,7 +2709,8 @@ renderBlock( OutputRef os, dom::Value const & /* item */, dom::Object const & /* data */, - dom::Object const & /* newBlockValues */) -> void { + dom::Object const & /* newBlockValues */, + dom::Object const & /* newBlockValuePaths */) -> void { // Render raw fnBlock os << fnBlock; }; @@ -2589,7 +2718,8 @@ renderBlock( OutputRef /* os */, dom::Value const &/* item */, dom::Object const &/* data */, - dom::Object const &/* newBlockValues */) -> void { + dom::Object const &/* newBlockValues */, + dom::Object const &/* newBlockValuePaths */) -> void { // noop: No inverseBlock for raw block }; } @@ -2604,7 +2734,7 @@ renderBlock( // Call helper // ============================================================== state.inlinePartials.emplace_back(); - state.compatStack.emplace_back(context); + // state.parentContext.emplace_back(context); state.dataStack.emplace_back(state.data); auto [res, render] = fn(args, cb); if (render != HelperBehavior::NO_RENDER) { @@ -2623,7 +2753,7 @@ renderBlock( } #endif state.inlinePartials.pop_back(); - state.compatStack.pop_back(); + // state.parentContext.pop_back(); state.dataStack.pop_back(); } @@ -3086,8 +3216,8 @@ if_fn( std::int64_t v = conditional.getInteger(); if (v == 0) { bool includeZero = false; - if (options.hashes().exists("includeZero")) { - auto zeroV = options.hashes().find("includeZero"); + if (options.hash().exists("includeZero")) { + auto zeroV = options.hash().find("includeZero"); if (zeroV.isBoolean()) { includeZero = zeroV.getBool(); } @@ -3136,15 +3266,18 @@ with_fn( if (!isEmpty(newContext)) { dom::Object data = createFrame(options.data()); - std::string contextPath = appendContextPath( - options.data().find("contextPath"), - options.ids()[0]); - data.set("contextPath", contextPath); - dom::Object blockValues; - if (!options.blockParams().empty()) { - blockValues.set(options.blockParams()[0], newContext); - } - options.fn(out, newContext, data, blockValues); + if (!options.ids().empty()) + { + std::string contextPath = appendContextPath( + options.data().find("contextPath"), + toString(options.ids()[0])); + data.set("contextPath", contextPath); + } + dom::Array blockParams; + blockParams.emplace_back(newContext); + dom::Array blockParamPaths; + blockParamPaths.emplace_back(data.find("contextPath")); + options.fn(out, newContext, data, blockParams, blockParamPaths); return; } options.inverse(out); @@ -3154,17 +3287,19 @@ void each_fn( dom::Array const& args, HandlebarsCallback const& options) { - if (args.empty()) { + if (args.empty()) + { throw HandlebarsError("Must pass iterator to #each"); } OutputRef out = options.output(); - MRDOX_ASSERT(!options.ids().empty()); - std::string contextPath = appendContextPath( - options.data().find("contextPath"), options.ids()[0]) + '.'; - + std::string contextPath; + if (!options.ids().empty()) + { + contextPath = appendContextPath( + options.data().find("contextPath"), toString(options.ids()[0])) + '.'; + } dom::Object data = createFrame(options.data()); - dom::Object blockValues; dom::Value context = args[0]; if (context.isFunction()) { @@ -3180,15 +3315,17 @@ each_fn( data.set("index", static_cast(index)); data.set("first", index == 0); data.set("last", index == items.size() - 1); - data.set("contextPath", contextPath + std::to_string(index)); - if (!options.blockParams().empty()) { - blockValues.set(options.blockParams()[0], item); - if (options.blockParams().size() > 1) { - blockValues.set( - options.blockParams()[1], static_cast(index)); - } + if (!options.ids().empty()) + { + data.set("contextPath", contextPath + std::to_string(index)); } - options.fn(out, item, data, blockValues); + dom::Array blockParams; + blockParams.emplace_back(item); + blockParams.emplace_back(static_cast(index)); + dom::Array blockParamPaths; + blockParamPaths.emplace_back(data.find("contextPath")); + blockParamPaths.emplace_back(nullptr); + options.fn(out, item, data, blockParams, blockParamPaths); } } else if (context.kind() == dom::Kind::Object) { dom::Object const& items = context.getObject(); @@ -3198,14 +3335,17 @@ each_fn( data.set("index", static_cast(index)); data.set("first", index == 0); data.set("last", index == items.size() - 1); - data.set("contextPath", contextPath + std::string(item.key)); - if (!options.blockParams().empty()) { - blockValues.set(options.blockParams()[0], item.value); - if (options.blockParams().size() > 1) { - blockValues.set(options.blockParams()[1], item.key); - } + if (!options.ids().empty()) + { + data.set("contextPath", contextPath + std::string(item.key)); } - options.fn(out, item.value, data, blockValues); + dom::Array blockParams; + blockParams.emplace_back(item.value); + blockParams.emplace_back(item.key); + dom::Array blockParamPaths; + blockParamPaths.emplace_back(data.find("contextPath")); + blockParamPaths.emplace_back(nullptr); + options.fn(out, item.value, data, blockParams, blockParamPaths); } } if (index == 0) { @@ -3234,7 +3374,7 @@ log_fn( dom::Array const& args, HandlebarsCallback const& options) { dom::Value level = 1; - if (auto hl = options.hashes().find("level"); !hl.isNull()) + if (auto hl = options.hash().find("level"); !hl.isNull()) { level = hl; } @@ -3286,23 +3426,34 @@ block_helper_missing_fn( return; } dom::Object data = createFrame(options.data()); - std::string contextPath = appendContextPath( - options.data().find("contextPath"), options.ids()[0]) + '.'; + + std::string contextPath; + if (!options.ids().empty()) + { + contextPath = appendContextPath( + options.data().find("contextPath"), toString(options.ids()[0])) + '.'; + } for (std::size_t index = 0; index < items.size(); ++index) { dom::Value item = items.at(index); data.set("key", static_cast(index)); data.set("index", static_cast(index)); data.set("first", index == 0); data.set("last", index == items.size() - 1); - data.set("contextPath", contextPath + std::to_string(index)); - options.fn(out, item, data, {}); + if (!options.ids().empty()) + { + data.set("contextPath", contextPath + std::to_string(index)); + } + options.fn(out, item, data, {}, {}); } } else { // If the context is not an array, then we'll render the block once // with the context as the data. dom::Object data = createFrame(options.data()); - data.set("contextPath", appendContextPath(data.find("contextPath"), options.name())); - options.fn(out, context, data, {}); + if (!options.ids().empty()) { + data.set( + "contextPath", appendContextPath(data.find("contextPath"), options.name())); + } + options.fn(out, context, data, {}, {}); } } diff --git a/src/test/lib/Support/Handlebars.cpp b/src/test/lib/Support/Handlebars.cpp index ec05e2294..e1c0f324d 100644 --- a/src/test/lib/Support/Handlebars.cpp +++ b/src/test/lib/Support/Handlebars.cpp @@ -100,6 +100,7 @@ setup_fixtures() } master.options.noEscape = true; + master.options.trackIds = true; } void @@ -370,7 +371,7 @@ setup_helpers() } std::string out; - auto h = cb.hashes().find("href"); + auto h = cb.hash().find("href"); if (h.isString()) { out += h.getString(); @@ -392,7 +393,7 @@ setup_helpers() out += '['; out += args[0].getString(); // more attributes from hashes - for (auto const& [key, value] : cb.hashes()) + for (auto const& [key, value] : cb.hash()) { if (key == "href" || !value.isString()) { @@ -472,7 +473,7 @@ setup_helpers() if (!items.empty()) { std::string out = "(i)); - out += "
  • " + cb.fn(item, data, {}) + "
  • "; + out += "
  • " + cb.fn(item, data, {}, {}) + "
  • "; } return out + ""; } @@ -1603,9 +1604,10 @@ partial_blocks() // should render block from partial with context { + // { context: { value: 'success' } } + dom::Object ctx; dom::Object value; value.set("value", "success"); - dom::Object ctx; ctx.set("context", value); hbs.registerPartial("dude", "{{#with context}}{{> @partial-block }}{{/with}}"); BOOST_TEST(hbs.render("{{#> dude}}{{../context/value}}{{/dude}}", ctx) == "success"); @@ -1832,6 +1834,11 @@ partial_compat_mode() // https://github.com/handlebars-lang/handlebars.js/blob/4.x/spec/partials.js Handlebars hbs; + // { root: 'yes', + // dudes: [ + // { name: 'Yehuda', url: 'http://yehuda' }, + // { name: 'Alan', url: 'http://alan' } + // ]} dom::Object root; root.set("root", "yes"); dom::Array dudes; @@ -2396,7 +2403,7 @@ subexpressions() hbs.registerHelper("blog", []( dom::Array const& args, HandlebarsCallback const& cb) { std::string res = "val is "; - res += toString(cb.hashes().find("fun")); + res += toString(cb.hash().find("fun")); return res; }); hbs.registerHelper("equal", [](dom::Array const& args) { @@ -2411,7 +2418,7 @@ subexpressions() { hbs.registerHelper("input", []( dom::Array const& args, HandlebarsCallback const& cb) { - auto hash = cb.hashes(); + auto hash = cb.hash(); std::string ariaLabel = escapeExpression(hash.find("aria-label").getString()); std::string placeholder = escapeExpression(hash.find("placeholder").getString()); std::string res = " std::string { - if (options.hashes().find("print").getBool()) + if (options.hash().find("print").getBool()) { std::string out; out += "GOODBYE "; - out += options.hashes().find("cruel").getString(); + out += options.hash().find("cruel").getString(); out += " "; - out += options.hashes().find("world").getString(); + out += options.hash().find("world").getString(); return out; } - else if (!options.hashes().find("print").getBool()) + else if (!options.hash().find("print").getBool()) { return "NOT PRINTING"; } @@ -4142,11 +4151,11 @@ helpers() hbs.registerHelper("goodbye", [](dom::Array const& args, HandlebarsCallback const& options) { std::string out; out += "GOODBYE "; - out += options.hashes().find("cruel").getString(); + out += options.hash().find("cruel").getString(); out += " "; out += options.fn(options.context()); out += " "; - out += toString(options.hashes().find("times")); + out += toString(options.hash().find("times")); out += " TIMES"; return out; }); @@ -4160,11 +4169,11 @@ helpers() hbs.registerHelper("goodbye", [](dom::Array const& args, HandlebarsCallback const& options) { std::string out; out += "GOODBYE "; - out += options.hashes().find("cruel").getString(); + out += options.hash().find("cruel").getString(); out += " "; out += options.fn(options.context()); out += " "; - out += toString(options.hashes().find("times")); + out += toString(options.hash().find("times")); out += " TIMES"; return out; }); @@ -4174,16 +4183,16 @@ helpers() // block helpers can take an optional hash with booleans { hbs.registerHelper("goodbye", [](dom::Array const& args, HandlebarsCallback const& options) -> std::string { - if (options.hashes().find("print").getBool()) + if (options.hash().find("print").getBool()) { std::string out; out += "GOODBYE "; - out += options.hashes().find("cruel").getString(); + out += options.hash().find("cruel").getString(); out += " "; out += options.fn(options.context()); return out; } - else if (!options.hashes().find("print").getBool()) + else if (!options.hash().find("print").getBool()) { return "NOT PRINTING"; } @@ -4449,12 +4458,12 @@ helpers() dom::Object ctx; ctx.set("value", "foo"); hbs.registerHelper("goodbyes", [](dom::Array const& args, HandlebarsCallback const& options) { - BOOST_TEST(options.blockParams().size() == 1u); + BOOST_TEST(options.blockParams() == 1u); dom::Object ctx; ctx.set("value", "bar"); - dom::Object blockValues; - blockValues.set(options.blockParams()[0], 1); - return options.fn(ctx, options.data(), blockValues); + dom::Array blockParams; + blockParams.emplace_back(1); + return options.fn(ctx, options.data(), blockParams, {}); }); BOOST_TEST(hbs.render("{{#goodbyes as |value|}}{{value}}{{/goodbyes}}{{value}}", ctx) == "1foo"); } @@ -4466,10 +4475,10 @@ helpers() return "foo"; }); hbs.registerHelper("goodbyes", [](dom::Array const& args, HandlebarsCallback const& options) { - BOOST_TEST(options.blockParams().size() == 1u); - dom::Object blockValues; - blockValues.set(options.blockParams()[0], 1); - return options.fn({}, options.data(), blockValues); + BOOST_TEST(options.blockParams() == 1u); + dom::Array blockParams; + blockParams.emplace_back(1); + return options.fn({}, options.data(), blockParams, {}); }); BOOST_TEST(hbs.render(string) == "1foo"); } @@ -4482,10 +4491,10 @@ helpers() return "foo"; }); hbs.registerHelper("goodbyes", [](dom::Array const& args, HandlebarsCallback const& options) { - BOOST_TEST(options.blockParams().size() == 1u); - dom::Object blockValues; - blockValues.set(options.blockParams()[0], 1); - return options.fn(options.context(), options.data(), blockValues); + BOOST_TEST(options.blockParams() == 1u); + dom::Array blockParams; + blockParams.emplace_back(1); + return options.fn(options.context(), options.data(), blockParams, {}); }); BOOST_TEST(hbs.render("{{#goodbyes as |value|}}{{./value}}{{/goodbyes}}{{value}}", ctx) == "barfoo"); } @@ -4498,13 +4507,13 @@ helpers() hbs.registerHelper("goodbyes", [&value](dom::Array const& args, HandlebarsCallback const& options) { dom::Object ctx; ctx.set("value", "bar"); - dom::Object blockValues; - if (!options.blockParams().empty()) + dom::Array blockParams; + if (options.blockParams() != 0) { - blockValues.set(options.blockParams()[0], value); + blockParams.emplace_back(value); value += 2; } - return options.fn(ctx, options.data(), blockValues); + return options.fn(ctx, options.data(), blockParams, {}); }); std::string string = "{{#goodbyes as |value|}}{{#goodbyes}}{{value}}{{#goodbyes as |value|}}{{value}}{{/goodbyes}}{{/goodbyes}}{{/goodbyes}}{{value}}"; BOOST_TEST(hbs.render(string, ctx) == "13foo"); @@ -4515,12 +4524,12 @@ helpers() dom::Object ctx; ctx.set("value", "foo"); hbs.registerHelper("goodbyes", [](dom::Array const& args, HandlebarsCallback const& options) { - BOOST_TEST(options.blockParams().size() == 1u); - dom::Object blockValues; - blockValues.set(options.blockParams()[0], 1); + BOOST_TEST(options.blockParams() == 1u); + dom::Array blockParams; + blockParams.emplace_back(1); dom::Object ctx; ctx.set("value", "bar"); - return options.fn(ctx, options.data(), blockValues); + return options.fn(ctx, options.data(), blockParams, {}); }); BOOST_TEST(hbs.render("{{#if bar}}{{else goodbyes as |value|}}{{value}}{{/if}}{{value}}", ctx) == "1foo"); } @@ -4595,6 +4604,371 @@ void track_ids() { // https://github.com/handlebars-lang/handlebars.js/blob/4.x/spec/track-ids.js + Handlebars hbs; + + // context = { is: { a: 'foo' }, slave: { driver: 'bar' } }; + dom::Object context; + dom::Object is; + is.set("a", "foo"); + context.set("is", is); + dom::Object slave; + slave.set("driver", "bar"); + context.set("slave", slave); + + HandlebarsOptions opt; + opt.trackIds = true; + + // should not include anything without the flag + { + hbs.registerHelper("wycats", [](dom::Array const& args, HandlebarsCallback const& options) { + BOOST_TEST(options.ids().empty()); + BOOST_TEST(options.hash().empty()); + return "success"; + }); + BOOST_TEST(hbs.render("{{wycats is.a slave.driver}}", context) == "success"); + } + + // should include argument ids + { + hbs.registerHelper("wycats", [](dom::Array const& args, HandlebarsCallback const& options) { + BOOST_TEST(options.ids()[0].getString() == "is.a"); + BOOST_TEST(options.ids()[1].getString() == "slave.driver"); + std::string res = "HELP ME MY BOSS "; + res += toString(options.ids()[0]); + res += ":"; + res += toString(args[0]); + res += " "; + res += toString(options.ids()[1]); + res += ":"; + res += toString(args[1]); + return res; + }); + BOOST_TEST(hbs.render("{{wycats is.a slave.driver}}", context, opt) == "HELP ME MY BOSS is.a:foo slave.driver:bar"); + } + + // should include hash ids + { + std::string string = "{{wycats bat=is.a baz=slave.driver}}"; + hbs.registerHelper("wycats", [](dom::Array const& args, HandlebarsCallback const& options) { + BOOST_TEST(options.hashIds().find("bat").getString() == "is.a"); + BOOST_TEST(options.hashIds().find("baz").getString() == "slave.driver"); + std::string res = "HELP ME MY BOSS "; + res += toString(options.hashIds().find("bat")); + res += ":"; + res += toString(options.hash().find("bat")); + res += " "; + res += toString(options.hashIds().find("baz")); + res += ":"; + res += toString(options.hash().find("baz")); + return res; + }); + BOOST_TEST(hbs.render(string, context, opt) == "HELP ME MY BOSS is.a:foo slave.driver:bar"); + } + + // should note ../ and ./ references + { + std::string string = "{{wycats ./is.a ../slave.driver this.is.a this}}"; + hbs.registerHelper("wycats", [](dom::Array const& args, HandlebarsCallback const& options) { + BOOST_TEST(options.ids()[0].getString() == "is.a"); + BOOST_TEST(options.ids()[1].getString() == "../slave.driver"); + BOOST_TEST(options.ids()[2].getString() == "is.a"); + BOOST_TEST(options.ids()[3].getString() == ""); + std::string res = "HELP ME MY BOSS "; + res += toString(options.ids()[0]); + res += ":"; + res += toString(args[0]); + res += " "; + res += toString(options.ids()[1]); + res += ":"; + res += toString(args[1]); + return res; + }); + // BOOST_TEST(hbs.render(string, context, opt) == "HELP ME MY BOSS is.a:foo ../slave.driver:undefined"); + BOOST_TEST(hbs.render(string, context, opt) == "HELP ME MY BOSS is.a:foo ../slave.driver:null"); + } + + // should note @data references + { + std::string string = "{{wycats @is.a @slave.driver}}"; + hbs.registerHelper("wycats", [](dom::Array const& args, HandlebarsCallback const& options) { + BOOST_TEST(options.ids()[0].getString() == "@is.a"); + BOOST_TEST(options.ids()[1].getString() == "@slave.driver"); + std::string res = "HELP ME MY BOSS "; + res += toString(options.ids()[0]); + res += ":"; + res += toString(args[0]); + res += " "; + res += toString(options.ids()[1]); + res += ":"; + res += toString(args[1]); + return res; + }); + opt.data = context; + BOOST_TEST(hbs.render(string, context, opt) == "HELP ME MY BOSS @is.a:foo @slave.driver:bar"); + opt.data = nullptr; + } + + // should return null for constants + { + std::string string = "{{wycats 1 \"foo\" key=false}}"; + hbs.registerHelper("wycats", [](dom::Array const& args, HandlebarsCallback const& options) { + BOOST_TEST(options.ids()[0].isNull()); + BOOST_TEST(options.ids()[1].isNull()); + BOOST_TEST(options.hashIds().find("key").isNull()); + std::string res = "HELP ME MY BOSS "; + res += toString(args[0]); + res += " "; + res += toString(args[1]); + res += " "; + res += toString(options.hash().find("key")); + return res; + }); + BOOST_TEST(hbs.render(string, context, opt) == "HELP ME MY BOSS 1 foo false"); + } + + // should return true for subexpressions + { + std::string string = "{{wycats (sub)}}"; + hbs.registerHelper("sub", [](dom::Array const& args, HandlebarsCallback const& options) { + return 1; + }); + hbs.registerHelper("wycats", [](dom::Array const& args, HandlebarsCallback const& options) { + BOOST_TEST(options.ids()[0].getBool()); + return "HELP ME MY BOSS " + toString(args[0]); + }); + BOOST_TEST(hbs.render(string, context, opt) == "HELP ME MY BOSS 1"); + } + + // should use block param paths + { + std::string string = "{{#doIt as |is|}}{{wycats is.a slave.driver is}}{{/doIt}}"; + hbs.registerHelper("doIt", [](dom::Array const& args, HandlebarsCallback const& options) { + dom::Array blockParams; + blockParams.emplace_back(options.context().getObject().find("is")); + dom::Array blockParamPaths; + blockParamPaths.emplace_back("zomg"); + return options.fn(options.context(), options.data(), blockParams, blockParamPaths); + }); + hbs.registerHelper("wycats", [](dom::Array const& args, HandlebarsCallback const& options) { + BOOST_TEST(options.ids()[0].getString() == "zomg.a"); + BOOST_TEST(options.ids()[1].getString() == "slave.driver"); + BOOST_TEST(options.ids()[2].getString() == "zomg"); + std::string res = "HELP ME MY BOSS "; + res += toString(options.ids()[0]); + res += ":"; + res += toString(args[0]); + res += " "; + res += toString(options.ids()[1]); + res += ":"; + res += toString(args[1]); + return res; + }); + // context = { is: { a: 'foo' }, slave: { driver: 'bar' } }; + BOOST_TEST(hbs.render(string, context, opt) == "HELP ME MY BOSS zomg.a:foo slave.driver:bar"); + } + + hbs.registerHelper("blockParams", [](dom::Array const& args, HandlebarsCallback const& options) { + std::string res = toString(args[0]); + res += ":"; + res += toString(options.ids()[0]); + res += "\n"; + return res; + }); + hbs.registerHelper("wycats", [](dom::Array const& args, HandlebarsCallback const& options) { + std::string res = toString(args[0]); + res += ":"; + res += toString(options.data().find("contextPath")); + res += "\n"; + return res; + }); + + // builtin helpers + { + // #each + { + // should track contextPath for arrays + { + dom::Object ctx; + dom::Array array; + dom::Object foo; + foo.set("name", "foo"); + array.emplace_back(foo); + dom::Object bar; + bar.set("name", "bar"); + array.emplace_back(bar); + ctx.set("array", array); + BOOST_TEST(hbs.render("{{#each array}}{{wycats name}}{{/each}}", ctx, opt) == "foo:array.0\nbar:array.1\n"); + } + + // should track contextPath for keys + { + dom::Object ctx; + dom::Object object; + dom::Object foo; + foo.set("name", "foo"); + object.set("foo", foo); + dom::Object bar; + bar.set("name", "bar"); + object.set("bar", bar); + ctx.set("object", object); + BOOST_TEST(hbs.render("{{#each object}}{{wycats name}}{{/each}}", ctx, opt) == "foo:object.foo\nbar:object.bar\n"); + } + + // should handle nesting + { + // { array: [{ name: 'foo' }, { name: 'bar' }] } + dom::Object ctx; + dom::Array array; + dom::Object foo; + foo.set("name", "foo"); + array.emplace_back(foo); + dom::Object bar; + bar.set("name", "bar"); + array.emplace_back(bar); + ctx.set("array", array); + BOOST_TEST(hbs.render("{{#each .}}{{#each .}}{{wycats name}}{{/each}}{{/each}}", ctx, opt) == "foo:.array..0\nbar:.array..1\n"); + } + + // should handle block params + { + // { array: [{ name: 'foo' }, { name: 'bar' }] } + dom::Object ctx; + dom::Array array; + dom::Object foo; + foo.set("name", "foo"); + array.emplace_back(foo); + dom::Object bar; + bar.set("name", "bar"); + array.emplace_back(bar); + ctx.set("array", array); + BOOST_TEST(hbs.render("{{#each array as |value|}}{{blockParams value.name}}{{/each}}", ctx, opt) == "foo:array.0.name\nbar:array.1.name\n"); + // TODO: Just implement this thing... there's no way around it. + } + } + + // #with + { + // should track contextPath + { + // { field: { name: 'foo' } } + dom::Object ctx; + dom::Object field; + field.set("name", "foo"); + ctx.set("field", field); + BOOST_TEST(hbs.render("{{#with field}}{{wycats name}}{{/with}}", ctx, opt) == "foo:field\n"); + } + + // should handle nesting + { + // { bat: { field: { name: 'foo' } } } + dom::Object ctx; + dom::Object bat; + dom::Object field; + field.set("name", "foo"); + bat.set("field", field); + ctx.set("bat", bat); + BOOST_TEST(hbs.render("{{#with bat}}{{#with field}}{{wycats name}}{{/with}}{{/with}}", ctx, opt) == "foo:bat.field\n"); + } + } + + // #blockHelperMissing + { + // should track contextPath for arrays + { + std::string string = "{{#field}}{{wycats name}}{{/field}}"; + // { field: [{ name: 'foo' }] } + dom::Object ctx; + dom::Array array; + dom::Object foo; + foo.set("name", "foo"); + array.emplace_back(foo); + ctx.set("field", array); + BOOST_TEST(hbs.render(string, ctx, opt) == "foo:field.0\n"); + } + + // should track contextPath for keys + { + std::string string = "{{#field}}{{wycats name}}{{/field}}"; + // { field: { name: 'foo' } } + dom::Object ctx; + dom::Object field; + field.set("name", "foo"); + ctx.set("field", field); + BOOST_TEST(hbs.render(string, ctx, opt) == "foo:field\n"); + } + + // should handle nesting + { + std::string string = "{{#bat}}{{#field}}{{wycats name}}{{/field}}{{/bat}}"; + // { bat: { field: { name: 'foo' } } } + dom::Object ctx; + dom::Object bat; + dom::Object field; + field.set("name", "foo"); + bat.set("field", field); + ctx.set("bat", bat); + BOOST_TEST(hbs.render(string, ctx, opt) == "foo:bat.field\n"); + } + } + } + + // partials + { + // should pass track id for basic partial + { + std::string string = "Dudes: {{#dudes}}{{> dude}}{{/dudes}}"; + dom::Object ctx; + dom::Array dudes; + dom::Object yehuda; + yehuda.set("name", "Yehuda"); + yehuda.set("url", "http://yehuda"); + dudes.emplace_back(yehuda); + dom::Object alan; + alan.set("name", "Alan"); + alan.set("url", "http://alan"); + dudes.emplace_back(alan); + ctx.set("dudes", dudes); + hbs.registerPartial("dude", "{{wycats name}}"); + BOOST_TEST(hbs.render(string, ctx, opt) == "Dudes: Yehuda:dudes.0\nAlan:dudes.1\n"); + } + + // should pass track id for context partial + { + std::string string = "Dudes: {{> dude dudes}}"; + // { dudes: [ { name: 'Yehuda', url: 'http://yehuda' }, { name: 'Alan', url: 'http://alan' } ] } + dom::Object ctx; + dom::Array dudes; + dom::Object yehuda; + yehuda.set("name", "Yehuda"); + yehuda.set("url", "http://yehuda"); + dudes.emplace_back(yehuda); + dom::Object alan; + alan.set("name", "Alan"); + alan.set("url", "http://alan"); + dudes.emplace_back(alan); + ctx.set("dudes", dudes); + hbs.registerPartial("dude", "{{#each this}}{{wycats name}}{{/each}}"); + BOOST_TEST(hbs.render(string, ctx, opt) == "Dudes: Yehuda:dudes..0\nAlan:dudes..1\n"); + } + + // should invalidate context for partials with parameters + { + std::string string = "Dudes: {{#dudes}}{{> dude . bar=\"foo\"}}{{/dudes}}"; + dom::Object ctx; + dom::Array dudes; + dom::Object yehuda; + yehuda.set("name", "Yehuda"); + yehuda.set("url", "http://yehuda"); + dudes.emplace_back(yehuda); + dom::Object alan; + alan.set("name", "Alan"); + alan.set("url", "http://alan"); + dudes.emplace_back(alan); + ctx.set("dudes", dudes); + hbs.registerPartial("dude", "{{wycats name}}"); + BOOST_TEST(hbs.render(string, ctx, opt) == "Dudes: Yehuda:true\nAlan:true\n"); + } + } } void diff --git a/test-files/handlebars/features_test.adoc b/test-files/handlebars/features_test.adoc index 5a8f73f59..31de18fd5 100644 --- a/test-files/handlebars/features_test.adoc +++ b/test-files/handlebars/features_test.adoc @@ -28,12 +28,23 @@ struct from_chars // #with to change context Person: John Doe in page about `from_chars` + +// dotdot segments refer to parent helper context, not parent object +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` + +// dotdot segments refer to parent helper context, not parent object +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 diff --git a/test-files/handlebars/features_test.adoc.hbs b/test-files/handlebars/features_test.adoc.hbs index f8fcd0302..85ff186d7 100644 --- a/test-files/handlebars/features_test.adoc.hbs +++ b/test-files/handlebars/features_test.adoc.hbs @@ -36,12 +36,21 @@ Declared in file <{{page.loc}}> {{>record-detail}} // #with to change context -{{#with page.person}}Person: {{firstname}} {{lastname}} in page about `{{../name}}` +{{#with page}}{{#with person}}Person: {{firstname}} {{lastname}} in page about `{{../name}}` +{{/with}}{{/with}} + +// dotdot segments refer to parent helper context, not parent object +{{#with page.person}}Person: {{firstname}} {{lastname}} in page about `{{../page/name}}` {{/with}} // #each to iterate, change context, and access parent context People: -{{#each page.people}}* Person: {{firstname}} {{lastname}} in page about `{{../name}}` +{{#with page}}{{#each people}}* Person: {{firstname}} {{lastname}} in page about `{{../name}}` +{{/each}}{{/with}} + +// dotdot segments refer to parent helper context, not parent object +People: +{{#each page.people}}* Person: {{firstname}} {{lastname}} in page about `{{../page/name}}` {{/each}} == Expressions @@ -347,7 +356,7 @@ My Content // with block parameters {{#with city as | city |}} {{#with city.location as | loc |}} - {{../city.name}}: {{loc.north}} {{loc.east}} + {{../name}}: {{loc.north}} {{loc.east}} {{/with}} {{/with}}